From f2c827f9a2395ac34144bddbb3f9a3edd4c43bd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:08:43 -0600 Subject: [PATCH 0001/2056] [runtime_image] Remove stored RAMAllocator member (#13998) --- esphome/components/runtime_image/png_decoder.cpp | 6 ++++-- esphome/components/runtime_image/png_decoder.h | 1 - esphome/components/runtime_image/runtime_image.cpp | 8 +++++--- esphome/components/runtime_image/runtime_image.h | 1 - 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/runtime_image/png_decoder.cpp b/esphome/components/runtime_image/png_decoder.cpp index 9fe4a9c4ffe..591504328d8 100644 --- a/esphome/components/runtime_image/png_decoder.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -50,7 +50,8 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { - pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); + RAMAllocator allocator; + pngle_t *pngle = allocator.allocate(1, PNGLE_T_SIZE); if (!pngle) { ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!"); return; @@ -64,7 +65,8 @@ PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { PngDecoder::~PngDecoder() { if (this->pngle_) { pngle_reset(this->pngle_); - this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE); + RAMAllocator allocator; + allocator.deallocate(this->pngle_, PNGLE_T_SIZE); } } diff --git a/esphome/components/runtime_image/png_decoder.h b/esphome/components/runtime_image/png_decoder.h index b5c1e70c2ad..24521d33a87 100644 --- a/esphome/components/runtime_image/png_decoder.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -29,7 +29,6 @@ class PngDecoder : public ImageDecoder { uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } protected: - RAMAllocator allocator_; pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 1d70f38d6b6..603ea76f01a 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -230,7 +230,8 @@ void RuntimeImage::release() { void RuntimeImage::release_buffer_() { if (this->buffer_) { ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); this->buffer_ = nullptr; this->data_start_ = nullptr; this->width_ = 0; @@ -254,11 +255,12 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { } ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); - this->buffer_ = this->allocator_.allocate(new_size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(new_size); if (!this->buffer_) { ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); return 0; } diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h index 0a5279d86d9..4bdcdcac9e0 100644 --- a/esphome/components/runtime_image/runtime_image.h +++ b/esphome/components/runtime_image/runtime_image.h @@ -165,7 +165,6 @@ class RuntimeImage : public image::Image { std::unique_ptr create_decoder_(); // Memory management - RAMAllocator allocator_{}; uint8_t *buffer_{nullptr}; // Decoder management From ffb9a00e2683d6ad0aad47fc4c6a783ca615e101 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:13 -0600 Subject: [PATCH 0002/2056] [online_image] Remove stored RAMAllocator member from DownloadBuffer (#13999) --- esphome/components/online_image/download_buffer.cpp | 10 ++++++---- esphome/components/online_image/download_buffer.h | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/online_image/download_buffer.cpp b/esphome/components/online_image/download_buffer.cpp index 999005df827..2ec6365aa17 100644 --- a/esphome/components/online_image/download_buffer.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -7,7 +7,8 @@ namespace esphome::online_image { static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->buffer_ = allocator.allocate(size); this->reset(); if (!this->buffer_) { ESP_LOGE(TAG, "Initial allocation of download buffer failed!"); @@ -38,15 +39,16 @@ size_t DownloadBuffer::resize(size_t size) { // Avoid useless reallocations; if the buffer is big enough, don't reallocate. return this->size_; } - this->allocator_.deallocate(this->buffer_, this->size_); - this->buffer_ = this->allocator_.allocate(size); + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + this->buffer_ = allocator.allocate(size); this->reset(); if (this->buffer_) { this->size_ = size; return size; } else { ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size, - this->allocator_.get_max_free_block_size()); + allocator.get_max_free_block_size()); this->size_ = 0; return 0; } diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h index 110a4b608a7..73061b23b5e 100644 --- a/esphome/components/online_image/download_buffer.h +++ b/esphome/components/online_image/download_buffer.h @@ -15,7 +15,10 @@ namespace esphome::online_image { class DownloadBuffer { public: DownloadBuffer(size_t size); - ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + ~DownloadBuffer() { + RAMAllocator allocator; + allocator.deallocate(this->buffer_, this->size_); + } uint8_t *data(size_t offset = 0); uint8_t *append() { return this->data(this->unread_); } @@ -34,7 +37,6 @@ class DownloadBuffer { size_t resize(size_t size); protected: - RAMAllocator allocator_{}; uint8_t *buffer_; size_t size_; /** Total number of downloaded bytes not yet read. */ From 81872d9822d1930e2408110acfcdb8e8e3dfd95a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:26 -0600 Subject: [PATCH 0003/2056] [camera, camera_encoder] Remove stored RAMAllocator member (#13997) --- esphome/components/camera/buffer_impl.cpp | 12 ++++++++---- esphome/components/camera/buffer_impl.h | 1 - .../camera_encoder/encoder_buffer_impl.cpp | 9 ++++++--- .../components/camera_encoder/encoder_buffer_impl.h | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp index d17a4e2707d..97cdd2e1043 100644 --- a/esphome/components/camera/buffer_impl.cpp +++ b/esphome/components/camera/buffer_impl.cpp @@ -3,18 +3,22 @@ namespace esphome::camera { BufferImpl::BufferImpl(size_t size) { - this->data_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->data_ = allocator.allocate(size); this->size_ = size; } BufferImpl::BufferImpl(CameraImageSpec *spec) { - this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + RAMAllocator allocator; + this->data_ = allocator.allocate(spec->bytes_per_image()); this->size_ = spec->bytes_per_image(); } BufferImpl::~BufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->size_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->size_); + } } } // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h index 46398295fa2..5e42df79572 100644 --- a/esphome/components/camera/buffer_impl.h +++ b/esphome/components/camera/buffer_impl.h @@ -18,7 +18,6 @@ class BufferImpl : public Buffer { ~BufferImpl() override; protected: - RAMAllocator allocator_; size_t size_{}; uint8_t *data_{}; }; diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp index db84026496a..f12c66f203f 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.cpp +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -4,7 +4,8 @@ namespace esphome::camera_encoder { bool EncoderBufferImpl::set_buffer_size(size_t size) { if (size > this->capacity_) { - uint8_t *p = this->allocator_.reallocate(this->data_, size); + RAMAllocator allocator; + uint8_t *p = allocator.reallocate(this->data_, size); if (p == nullptr) return false; @@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) { } EncoderBufferImpl::~EncoderBufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->capacity_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->capacity_); + } } } // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h index 13eccb7d564..d394daff14f 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.h +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer { ~EncoderBufferImpl() override; protected: - RAMAllocator allocator_; size_t capacity_{}; size_t size_{}; uint8_t *data_{}; From 0c4827d348e4f55807fa910490e59144711ef392 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 08:09:53 -0600 Subject: [PATCH 0004/2056] [json, core] Remove stored RAMAllocator, make constructors constexpr (#14000) --- esphome/components/json/json_util.h | 11 ++++++----- esphome/core/helpers.h | 11 ++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index ca074926bdc..c472b9a9ec5 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -18,7 +18,10 @@ namespace json { // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled struct SpiRamAllocator : ArduinoJson::Allocator { - void *allocate(size_t size) override { return allocator_.allocate(size); } + void *allocate(size_t size) override { + RAMAllocator allocator; + return allocator.allocate(size); + } void deallocate(void *ptr) override { // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. @@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator { } void *reallocate(void *ptr, size_t new_size) override { - return allocator_.reallocate(static_cast(ptr), new_size); + RAMAllocator allocator; + return allocator.reallocate(static_cast(ptr), new_size); } - - protected: - RAMAllocator allocator_{RAMAllocator::NONE}; }; #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 34c74524840..298b93fbc46 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1673,13 +1673,10 @@ template class RAMAllocator { ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. }; - RAMAllocator() = default; - RAMAllocator(uint8_t flags) { - // default is both external and internal - flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; - if (flags != 0) - this->flags_ = flags; - } + constexpr RAMAllocator() = default; + constexpr RAMAllocator(uint8_t flags) + : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) + : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {} template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } From 1517b7799a0e829a33555f02bd49915d53b7dd13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 08:21:04 -0700 Subject: [PATCH 0005/2056] [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983) Co-authored-by: Claude Opus 4.6 --- .../components/wifi/wifi_component_esp8266.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c87345f0bf0..cbf7d7d80f8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr) { - // renew already started DHCP leases - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; From f6362aa8da61be4ba14edaf8c4156ba75c5fe1ef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:36 -0500 Subject: [PATCH 0006/2056] [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004) Co-authored-by: Claude Opus 4.6 --- .../components/combination/combination.cpp | 2 +- esphome/components/combination/sensor.py | 46 +++++++++++++++---- tests/components/combination/common.yaml | 4 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d2703901..ece7cca4825 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() { } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03a..0204162e8d9 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -162,12 +190,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/tests/components/combination/common.yaml b/tests/components/combination/common.yaml index 0e5d512d08c..5d46419399e 100644 --- a/tests/components/combination/common.yaml +++ b/tests/components/combination/common.yaml @@ -27,9 +27,9 @@ sensor: name: Linearly combined temperatures sources: - source: template_temperature1 - coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" + coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" - source: template_temperature2 - coeffecient: 1.5 + coefficient: 1.5 - platform: combination type: max name: Max of combined temperatures From df29cdbf1742c0676e9bd5870a86d462c0a91dfd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:11:50 -0500 Subject: [PATCH 0007/2056] [fan] Fix preset_mode not restored on boot (#14002) Co-authored-by: Claude Opus 4.6 --- esphome/components/fan/fan.cpp | 8 +++++++- esphome/components/fan/fan.h | 4 +++- esphome/components/hbridge/fan/hbridge_fan.cpp | 8 ++++---- esphome/components/speed/fan/speed_fan.cpp | 8 ++++---- esphome/components/template/fan/template_fan.cpp | 10 +++++----- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d70a2940bc9..c1e0a3dc2e1 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -221,12 +221,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -264,6 +269,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825e..2caf3a712a2 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1ea..38e4129e66f 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index af98e3a51f9..55f7fd162c2 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -7,15 +7,15 @@ namespace speed { static const char *const TAG = "speed.fan"; void SpeedFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 0e1920a984d..cd267bd552c 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -6,15 +6,15 @@ namespace esphome::template_ { static const char *const TAG = "template.fan"; void TemplateFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); } - - // Construct traits - this->traits_ = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } From e945e9b6591602ec98c1db102aeb13586a76ab92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:23:06 -0500 Subject: [PATCH 0008/2056] [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_rmt/__init__.py | 22 +++++ .../components/esp32_rmt_led_strip/light.py | 6 +- esphome/components/remote_base/remote_base.h | 5 +- .../components/remote_receiver/__init__.py | 41 +++++++- .../remote_receiver/remote_receiver.cpp | 2 +- .../remote_receiver/remote_receiver.h | 17 ++-- .../remote_receiver/remote_receiver_esp32.cpp | 5 +- .../components/remote_transmitter/__init__.py | 93 ++++++++++++------- .../remote_transmitter/automation.h | 6 +- .../remote_transmitter/remote_transmitter.cpp | 8 +- .../remote_transmitter/remote_transmitter.h | 21 +++-- .../remote_transmitter_esp32.cpp | 11 ++- .../remote_receiver/test.esp32-c2-idf.yaml | 12 +++ .../remote_transmitter/test.esp32-c2-idf.yaml | 7 ++ 14 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 tests/components/remote_receiver/test.esp32-c2-idf.yaml create mode 100644 tests/components/remote_transmitter/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba7..1076bcabdc9 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 6d41f6b5b86..1c6943b0037 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv @@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 0cac28506fd..d73fff2b0a5 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -119,6 +119,8 @@ class RemoteComponentBase { }; #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED class RemoteRMTChannel { public: void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } @@ -137,7 +139,8 @@ class RemoteRMTChannel { uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; }; -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 class RemoteTransmitterBase : public RemoteComponentBase { public: diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b3dc213c5f6..362f6e99db4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_( def validate_config(config): if CORE.is_esp32: variant = esp32.get_esp32_variant() + if variant in esp32_rmt.VARIANTS_NO_RMT: + return config if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): max_idle = 65535 else: @@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_BUFFER_SIZE, esp32="10000b", + esp32_c2="1000b", + esp32_c61="1000b", esp8266="1000b", bk72xx="1000b", ln882x="1000b", @@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, esp32_c3=96, esp32_c5=96, esp32_c6=96, + esp32_c61=cv.UNDEFINED, esp32_h2=96, esp32_p4=192, esp32_s2=192, @@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.SplitDefault( CONF_RECEIVE_SYMBOLS, esp32=192, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( @@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers( ), cv.boolean, ), - cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( + cv.SplitDefault( + CONF_CARRIER_DUTY_PERCENT, + esp32=100, + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All( cv.only_on_esp32, cv.percentage_int, cv.Range(min=1, max=100), ), - cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( - cv.only_on_esp32, cv.frequency, cv.int_ - ), + cv.SplitDefault( + CONF_CARRIER_FREQUENCY, + esp32="0Hz", + esp32_c2=cv.UNDEFINED, + esp32_c61=cv.UNDEFINED, + ): cv.All(cv.only_on_esp32, cv.frequency, cv.int_), } ) .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_FILTER_SYMBOLS, + CONF_RECEIVE_SYMBOLS, + CONF_CARRIER_DUTY_PERCENT, + CONF_CARRIER_FREQUENCY, + ] + ) + ) .add_extra(validate_config) ) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_receiver.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index de47457dac3..d59ee636955 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) namespace esphome::remote_receiver { diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3d9199a904d..5da9283a6e7 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -6,12 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); @@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore { volatile bool prev_level{false}; volatile bool overflow{false}; }; -#elif defined(USE_ESP32) +#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore { class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void dump_config() override; void loop() override; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; @@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, RemoteReceiverComponentStore store_; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index f95244ea458..357a36d052f 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include #include @@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } // namespace esphome::remote_receiver -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 8383b9dd75d..fc772f88b22 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_( cg.Parented.template(RemoteTransmitterComponent), ) + MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( - cv.percentage_int, cv.Range(min=1, max=100) - ), - cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( - cv.only_on_esp32, - esp32_rmt.validate_clock_resolution(), - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_USE_DMA): cv.All( - esp32.only_on_variant( - supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( + cv.percentage_int, cv.Range(min=1, max=100) ), - cv.boolean, - ), - cv.SplitDefault( - CONF_RMT_SYMBOLS, - esp32=64, - esp32_c3=48, - esp32_c5=48, - esp32_c6=48, - esp32_h2=48, - esp32_p4=48, - esp32_s2=64, - esp32_s3=48, - ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), - cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), - cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), - } -).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All( + esp32.only_on_variant( + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] + ), + cv.boolean, + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32=64, + esp32_c2=cv.UNDEFINED, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_c61=cv.UNDEFINED, + esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), + cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .add_extra( + esp32_rmt.validate_rmt_not_supported( + [ + CONF_CLOCK_RESOLUTION, + CONF_EOT_LEVEL, + CONF_USE_DMA, + CONF_RMT_SYMBOLS, + CONF_NON_BLOCKING, + ] + ) + ) +) def _validate_non_blocking(config): - if CORE.is_esp32 and CONF_NON_BLOCKING not in config: + if ( + CORE.is_esp32 + and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT + and CONF_NON_BLOCKING not in config + ): _LOGGER.warning( "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" @@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if CORE.is_esp32: + if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT: # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_driver_rmt") @@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "remote_transmitter.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index bee1d0be8ac..8da4cfd95d0 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class DigitalWriteAction : public Action, public void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index d35541e2e1f..51a3c0b1d47 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,9 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->complete_trigger_.trigger(); } -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 65bd2ac8b28..aee52ea170b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,13 +6,15 @@ #include #if defined(USE_ESP32) +#include +#if SOC_RMT_SUPPORTED #include -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) // IDF version 5.5.1 and above is required because of a bug in // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 @@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore { class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED , public remote_base::RemoteRMTChannel #endif @@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void digital_write(bool value); -#if defined(USE_ESP32) +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } @@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); @@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t target_time_; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) && SOC_RMT_SUPPORTED void configure_rmt_(); void wait_for_rmt_(); @@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, Trigger<> complete_trigger_; }; -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 89d97895b26..71773e3ddf8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -3,10 +3,11 @@ #include "esphome/core/application.h" #ifdef USE_ESP32 +#include +#if SOC_RMT_SUPPORTED #include -namespace esphome { -namespace remote_transmitter { +namespace esphome::remote_transmitter { static const char *const TAG = "remote_transmitter"; @@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen } #endif -} // namespace remote_transmitter -} // namespace esphome +} // namespace esphome::remote_transmitter -#endif +#endif // SOC_RMT_SUPPORTED +#endif // USE_ESP32 diff --git a/tests/components/remote_receiver/test.esp32-c2-idf.yaml b/tests/components/remote_receiver/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..87154e19fc8 --- /dev/null +++ b/tests/components/remote_receiver/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO2 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.esp32-c2-idf.yaml b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..424cd8d2498 --- /dev/null +++ b/tests/components/remote_transmitter/test.esp32-c2-idf.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO2 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml From 5904808804b1f86e30583eb3ebbab0eb461905e0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:24:35 +1300 Subject: [PATCH 0009/2056] Bump version to 2026.2.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 572e20a694a..b43b5a428e1 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0b2 +PROJECT_NUMBER = 2026.2.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 247b2b7e4e0..fd916da92b1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b2" +__version__ = "2026.2.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4cd3f6c36a5485d6efa570e71cd861d705e7ac29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Feb 2026 21:30:57 -0600 Subject: [PATCH 0010/2056] [api] Remove unused reserve from APIServer constructor (#14017) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f25a9bc0e2a..67a117e68fd 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -28,11 +28,7 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -APIServer::APIServer() { - global_api_server = this; - // Pre-allocate shared write buffer - shared_write_buffer_.reserve(64); -} +APIServer::APIServer() { global_api_server = this; } void APIServer::setup() { ControllerRegistry::register_controller(this); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 28f60343e03..323acc2efb3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -268,7 +268,11 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; - std::vector shared_write_buffer_; // Shared proto write buffer for all connections + // Shared proto write buffer for all connections. + // Not pre-allocated: all send paths call prepare_first_message_buffer() which + // reserves the exact needed size. Pre-allocating here would cause heap fragmentation + // since the buffer would almost always reallocate on first use. + std::vector shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif From e826d71bd85b7a91b683c0502846fbcc9b9d3f02 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:57 +0100 Subject: [PATCH 0011/2056] [openthread] Fix compiler format warning (#14030) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 79cd8098093..ec212d1f681 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; From 81ed70325c164612878457033c5f08b27b19f331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 0012/2056] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522facb..cb494ed1bc1 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0f..a1b1ff94bb3 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789c..c2cdb1660ce 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 5bb863f7dadadedf3c8620d1e275d7d354fedae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:24:39 -0600 Subject: [PATCH 0013/2056] Bump actions/stale from 10.1.1 to 10.2.0 (#14036) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7e03e2a5f99..ba5c32e0167 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true From a0c4fa649640eb7618049462765e293d6458c48d Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:57 +0100 Subject: [PATCH 0014/2056] [openthread] Fix compiler format warning (#14030) --- esphome/components/openthread/openthread_esp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 79cd8098093..ec212d1f681 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); } uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); } link_mode_config.mRxOnWhenIdle = this->poll_period == 0; link_mode_config.mDeviceType = false; From d9f493ab7ae31e199a94939a9e189b2bc105ea87 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:13:41 +1300 Subject: [PATCH 0015/2056] Bump version to 2026.2.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index b43b5a428e1..f9deb328995 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0b3 +PROJECT_NUMBER = 2026.2.0b4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index fd916da92b1..f494b5d41e0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b3" +__version__ = "2026.2.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fb35ddebb9915f6bda82e4050943ee5de1ba0360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:07 -0600 Subject: [PATCH 0016/2056] [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) --- esphome/components/display/display.cpp | 3 +-- esphome/components/display/display.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index ebc3c0a9f6d..53a087803c7 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -9,8 +9,7 @@ namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); +// COLOR_OFF and COLOR_ON are now inline constexpr in display.h void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 47d40915aa7..e40f6ec963b 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -298,9 +298,9 @@ using display_writer_t = DisplayWriter; } /// Turn the pixel OFF. -extern const Color COLOR_OFF; +inline constexpr Color COLOR_OFF(0, 0, 0, 0); /// Turn the pixel ON. -extern const Color COLOR_ON; +inline constexpr Color COLOR_ON(255, 255, 255, 255); class BaseImage { public: From fb89900c64f2a88d6f86feaa43e453af8d83b53c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 08:22:36 -0600 Subject: [PATCH 0017/2056] [core] Make setup_priority and component state constants constexpr (#14041) --- esphome/core/component.cpp | 32 ++--------------------- esphome/core/component.h | 52 +++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 90aa36f4db5..a452f4d400d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -76,36 +76,8 @@ void store_component_error_message(const Component *component, const char *messa } } // namespace -namespace setup_priority { - -const float BUS = 1000.0f; -const float IO = 900.0f; -const float HARDWARE = 800.0f; -const float DATA = 600.0f; -const float PROCESSOR = 400.0; -const float BLUETOOTH = 350.0f; -const float AFTER_BLUETOOTH = 300.0f; -const float WIFI = 250.0f; -const float ETHERNET = 250.0f; -const float BEFORE_CONNECTION = 220.0f; -const float AFTER_WIFI = 200.0f; -const float AFTER_CONNECTION = 100.0f; -const float LATE = -100.0f; - -} // namespace setup_priority - -// Component state uses bits 0-2 (8 states, 5 used) -const uint8_t COMPONENT_STATE_MASK = 0x07; -const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; -const uint8_t COMPONENT_STATE_SETUP = 0x01; -const uint8_t COMPONENT_STATE_LOOP = 0x02; -const uint8_t COMPONENT_STATE_FAILED = 0x03; -const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; -// Status LED uses bits 3-4 -const uint8_t STATUS_LED_MASK = 0x18; -const uint8_t STATUS_LED_OK = 0x00; -const uint8_t STATUS_LED_WARNING = 0x08; // Bit 3 -const uint8_t STATUS_LED_ERROR = 0x10; // Bit 4 +// setup_priority, component state, and status LED constants are now +// constexpr in component.h const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again diff --git a/esphome/core/component.h b/esphome/core/component.h index 9ab77cc2f9a..848bc0ba351 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -21,33 +21,31 @@ struct LogString; namespace setup_priority { /// For communication buses like i2c/spi -extern const float BUS; +inline constexpr float BUS = 1000.0f; /// For components that represent GPIO pins like PCF8573 -extern const float IO; +inline constexpr float IO = 900.0f; /// For components that deal with hardware and are very important like GPIO switch -extern const float HARDWARE; +inline constexpr float HARDWARE = 800.0f; /// For components that import data from directly connected sensors like DHT. -extern const float DATA; -/// Alias for DATA (here for compatibility reasons) -extern const float HARDWARE_LATE; +inline constexpr float DATA = 600.0f; /// For components that use data from sensors like displays -extern const float PROCESSOR; -extern const float BLUETOOTH; -extern const float AFTER_BLUETOOTH; -extern const float WIFI; -extern const float ETHERNET; +inline constexpr float PROCESSOR = 400.0f; +inline constexpr float BLUETOOTH = 350.0f; +inline constexpr float AFTER_BLUETOOTH = 300.0f; +inline constexpr float WIFI = 250.0f; +inline constexpr float ETHERNET = 250.0f; /// For components that should be initialized after WiFi and before API is connected. -extern const float BEFORE_CONNECTION; +inline constexpr float BEFORE_CONNECTION = 220.0f; /// For components that should be initialized after WiFi is connected. -extern const float AFTER_WIFI; +inline constexpr float AFTER_WIFI = 200.0f; /// For components that should be initialized after a data connection (API/MQTT) is connected. -extern const float AFTER_CONNECTION; +inline constexpr float AFTER_CONNECTION = 100.0f; /// For components that should be initialized at the very end of the setup process. -extern const float LATE; +inline constexpr float LATE = -100.0f; } // namespace setup_priority -static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; +inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// Type-safe scheduler IDs for core base classes. /// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide @@ -65,16 +63,18 @@ void log_update_interval(const char *tag, PollingComponent *component); #define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) -extern const uint8_t COMPONENT_STATE_MASK; -extern const uint8_t COMPONENT_STATE_CONSTRUCTION; -extern const uint8_t COMPONENT_STATE_SETUP; -extern const uint8_t COMPONENT_STATE_LOOP; -extern const uint8_t COMPONENT_STATE_FAILED; -extern const uint8_t COMPONENT_STATE_LOOP_DONE; -extern const uint8_t STATUS_LED_MASK; -extern const uint8_t STATUS_LED_OK; -extern const uint8_t STATUS_LED_WARNING; -extern const uint8_t STATUS_LED_ERROR; +// Component state uses bits 0-2 (8 states, 5 used) +inline constexpr uint8_t COMPONENT_STATE_MASK = 0x07; +inline constexpr uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; +inline constexpr uint8_t COMPONENT_STATE_SETUP = 0x01; +inline constexpr uint8_t COMPONENT_STATE_LOOP = 0x02; +inline constexpr uint8_t COMPONENT_STATE_FAILED = 0x03; +inline constexpr uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; +// Status LED uses bits 3-4 +inline constexpr uint8_t STATUS_LED_MASK = 0x18; +inline constexpr uint8_t STATUS_LED_OK = 0x00; +inline constexpr uint8_t STATUS_LED_WARNING = 0x08; +inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; From 652c669777ecaa125f575aa7eac72bf7f5cc5a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:02 -0600 Subject: [PATCH 0018/2056] Bump pillow from 11.3.0 to 12.1.1 (#14048) Signed-off-by: dependabot[bot] 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 a0a29ad30a6..77d30ecd3df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==11.3.0 +pillow==12.1.1 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 From f73bcc0e7bebba965a76859f680ac16681833a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:08:12 -0600 Subject: [PATCH 0019/2056] Bump cryptography from 45.0.1 to 46.0.5 (#14049) Signed-off-by: dependabot[bot] 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 77d30ecd3df..2e6268284a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==45.0.1 +cryptography==46.0.5 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 9cd7b0b32b1a4c27a3b0673b1d10187e69e55109 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 0020/2056] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24b..8b3e1afea67 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe758..a45768b5cd6 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8be..145ebd40968 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e5183..fd4c811fb35 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43e..745dfad487e 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From 6b8264fcaa56e7d9868733f9b3e39135c886f67f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:09:33 -0500 Subject: [PATCH 0021/2056] [external_components] Clean up incomplete clone on failed ref fetch (#14051) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 4 +- esphome/git.py | 47 ++++++----- esphome/helpers.py | 20 ++++- esphome/writer.py | 27 +------ tests/unit_tests/test_git.py | 113 ++++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 49 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b78b945a24b..8b3e1afea67 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,9 +44,9 @@ from esphome.const import ( from esphome.core import CORE, HexInt, TimePeriod from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache, rmtree +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa diff --git a/esphome/git.py b/esphome/git.py index 4ff07ffe758..a45768b5cd6 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -5,12 +5,12 @@ import hashlib import logging from pathlib import Path import re -import shutil import subprocess import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds +from esphome.helpers import rmtree _LOGGER = logging.getLogger(__name__) @@ -115,24 +115,35 @@ def clone_or_update( if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) - cmd = ["git", "clone", "--depth=1"] - cmd += ["--", url, str(repo_dir)] - run_git_command(cmd) + try: + cmd = ["git", "clone", "--depth=1"] + cmd += ["--", url, str(repo_dir)] + run_git_command(cmd) - if ref is not None: - # We need to fetch the PR branch first, otherwise git will complain - # about missing objects - _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) - run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir) + if ref is not None: + # We need to fetch the PR branch first, otherwise git will complain + # about missing objects + _LOGGER.info("Fetching %s", ref) + run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir + ) - if submodules is not None: - _LOGGER.info( - "Initializing submodules (%s) for %s", ", ".join(submodules), key - ) - run_git_command( - ["git", "submodule", "update", "--init"] + submodules, git_dir=repo_dir - ) + if submodules is not None: + _LOGGER.info( + "Initializing submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, + git_dir=repo_dir, + ) + except GitException: + # Remove incomplete clone to prevent stale state. Without this, + # a failed ref fetch leaves a clone on the default branch, and + # subsequent calls skip the update due to the refresh window. + if repo_dir.is_dir(): + rmtree(repo_dir) + raise else: # Check refresh needed @@ -193,7 +204,7 @@ def clone_or_update( err, ) _LOGGER.info("Removing broken repository at %s", repo_dir) - shutil.rmtree(repo_dir) + rmtree(repo_dir) _LOGGER.info("Successfully removed broken repository, re-cloning...") # Recursively call clone_or_update to re-clone diff --git a/esphome/helpers.py b/esphome/helpers.py index ae142b7f8be..145ebd40968 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -8,6 +8,7 @@ from pathlib import Path import platform import re import shutil +import stat import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -354,6 +355,23 @@ def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows. + + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail. This handles that by removing the + read-only flag and retrying. + """ + + def _onerror(func, path, exc_info): + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + shutil.rmtree(path, onerror=_onerror) + + def walk_files(path: Path): for root, _, files in os.walk(path): for name in files: @@ -481,8 +499,6 @@ def list_starts_with(list_, sub): def file_compare(path1: Path, path2: Path) -> bool: """Return True if the files path1 and path2 have the same contents.""" - import stat - try: stat1, stat2 = path1.stat(), path2.stat() except OSError: diff --git a/esphome/writer.py b/esphome/writer.py index 661118e5183..fd4c811fb35 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,14 +1,10 @@ -from collections.abc import Callable import importlib import json import logging import os from pathlib import Path import re -import shutil -import stat import time -from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -25,6 +21,7 @@ from esphome.helpers import ( get_str_env, is_ha_addon, read_file, + rmtree, walk_files, write_file, write_file_if_changed, @@ -404,28 +401,6 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def _rmtree_error_handler( - func: Callable[[str], object], - path: str, - exc_info: tuple[type[BaseException], BaseException, TracebackType | None], -) -> None: - """Error handler for shutil.rmtree to handle read-only files on Windows. - - On Windows, git pack files and other files may be marked read-only, - causing shutil.rmtree to fail with "Access is denied". This handler - removes the read-only flag and retries the deletion. - """ - if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) - os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) - func(path) - - -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows.""" - shutil.rmtree(path, onerror=_rmtree_error_handler) - - def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 0411fe5e43e..745dfad487e 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( # Should raise on the second attempt when _recover_broken=False # This hits the "if not _recover_broken: raise" path with ( - unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree), + unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree), pytest.raises(GitCommandError, match="fatal: unable to write new index file"), ): git.clone_or_update( @@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop( stash_calls = [c for c in call_list if "stash" in c[0][0]] # Should have exactly two stash calls assert len(stash_calls) == 2 + + +def test_clone_or_update_cleans_up_on_failed_ref_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that a failed ref fetch removes the incomplete clone directory. + + When cloning with a specific ref, if `git clone` succeeds but the + subsequent `git fetch ` fails, the clone directory should be + removed so the next attempt starts fresh instead of finding a stale + clone on the default branch. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + # Simulate successful clone by creating the directory + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update( + url=url, + ref=ref, + refresh=refresh, + domain=domain, + ) + + # The incomplete clone directory should have been removed + assert not repo_dir.exists() + + # Verify clone was attempted then fetch failed + call_list = mock_run_git_command.call_args_list + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + + +def test_clone_or_update_stale_clone_is_retried_after_cleanup( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Test that after cleanup, a subsequent call does a fresh clone. + + This is the full scenario: first call fails at fetch (directory cleaned up), + second call sees no directory and clones fresh. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + call_count = {"clone": 0, "fetch": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "clone": + call_count["clone"] += 1 + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + if cmd_type == "fetch": + call_count["fetch"] += 1 + if call_count["fetch"] == 1: + # First fetch fails + raise GitCommandError("fatal: couldn't find remote ref pull/123/head") + # Second fetch succeeds + return "" + if cmd_type == "reset": + return "" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + refresh = TimePeriodSeconds(days=1) + + # First call: clone succeeds, fetch fails, directory cleaned up + with pytest.raises(GitCommandError, match="couldn't find remote ref"): + git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain) + + assert not repo_dir.exists() + + # Second call: fresh clone + fetch succeeds + result_dir, _ = git.clone_or_update( + url=url, ref=ref, refresh=refresh, domain=domain + ) + + assert result_dir == repo_dir + assert repo_dir.exists() + assert call_count["clone"] == 2 + assert call_count["fetch"] == 2 From ab572c2882c499f5e517f10c704ced98221543f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:03:44 +1300 Subject: [PATCH 0022/2056] Bump version to 2026.2.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f9deb328995..88565b4a83d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0b4 +PROJECT_NUMBER = 2026.2.0b5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index f494b5d41e0..b746a55cd4e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b4" +__version__ = "2026.2.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c89cded4b50130cf3a570971705f488f0e27fed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:30:04 +1300 Subject: [PATCH 0023/2056] Bump version to 2026.2.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 88565b4a83d..38135f91068 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0b5 +PROJECT_NUMBER = 2026.2.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index b746a55cd4e..9115055e7b1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0b5" +__version__ = "2026.2.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4a038978d27b4ed193de8fb934ff32221ae9a31f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:14 -0600 Subject: [PATCH 0024/2056] [pca9685] Make mode constants inline constexpr (#14042) --- esphome/components/pca9685/pca9685_output.cpp | 6 +----- esphome/components/pca9685/pca9685_output.h | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 77e3d5a6c6a..89a6bcdcc0b 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -8,11 +8,7 @@ namespace pca9685 { static const char *const TAG = "pca9685"; -const uint8_t PCA9685_MODE_INVERTED = 0x10; -const uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; -const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; -const uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; -const uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; +// PCA9685 mode constants are now inline constexpr in pca9685_output.h static const uint8_t PCA9685_REGISTER_SOFTWARE_RESET = 0x06; static const uint8_t PCA9685_REGISTER_MODE1 = 0x00; diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 288c923d4c3..785cc974dae 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -13,15 +13,15 @@ enum class PhaseBalancer { }; /// Inverts polarity of channel output signal -extern const uint8_t PCA9685_MODE_INVERTED; +inline constexpr uint8_t PCA9685_MODE_INVERTED = 0x10; /// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission) -extern const uint8_t PCA9685_MODE_OUTPUT_ONACK; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_ONACK = 0x08; /// Use a totem-pole (push-pull) style output rather than an open-drain structure. -extern const uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE; +inline constexpr uint8_t PCA9685_MODE_OUTPUT_TOTEM_POLE = 0x04; /// For active low output enable, sets channel output to high-impedance state -extern const uint8_t PCA9685_MODE_OUTNE_HIGHZ; +inline constexpr uint8_t PCA9685_MODE_OUTNE_HIGHZ = 0x02; /// Similarly, sets channel output to high if in totem-pole mode, otherwise -extern const uint8_t PCA9685_MODE_OUTNE_LOW; +inline constexpr uint8_t PCA9685_MODE_OUTNE_LOW = 0x01; class PCA9685Output; From 82cfa00a97a8b6c9d0361c2bec66c3723a2be6e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:04:30 -0600 Subject: [PATCH 0025/2056] [tlc59208f] Make mode constants inline constexpr (#14043) --- esphome/components/tlc59208f/tlc59208f_output.cpp | 12 +----------- esphome/components/tlc59208f/tlc59208f_output.h | 14 +++++++------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 85311a877c7..d35585fe5f3 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -26,17 +26,7 @@ const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); // 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); -// 0*: Group dimming, 1: Group blinking -const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); -// 0*: Output change on Stop command, 1: Output change on ACK -const uint8_t TLC59208F_MODE2_OCH = (1 << 3); -// 0*: WDT disabled, 1: WDT enabled -const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); -// WDT timeouts -const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); -const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); -const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); -const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); +// TLC59208F MODE2 constants are now inline constexpr in tlc59208f_output.h // --- Special function --- // Call address to perform software reset, no devices will ACK diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 68ca8061d7c..34663cd3644 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -9,16 +9,16 @@ namespace esphome { namespace tlc59208f { // 0*: Group dimming, 1: Group blinking -extern const uint8_t TLC59208F_MODE2_DMBLNK; +inline constexpr uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); // 0*: Output change on Stop command, 1: Output change on ACK -extern const uint8_t TLC59208F_MODE2_OCH; +inline constexpr uint8_t TLC59208F_MODE2_OCH = (1 << 3); // 0*: WDT disabled, 1: WDT enabled -extern const uint8_t TLC59208F_MODE2_WDTEN; +inline constexpr uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); // WDT timeouts -extern const uint8_t TLC59208F_MODE2_WDT_5MS; -extern const uint8_t TLC59208F_MODE2_WDT_15MS; -extern const uint8_t TLC59208F_MODE2_WDT_25MS; -extern const uint8_t TLC59208F_MODE2_WDT_35MS; +inline constexpr uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +inline constexpr uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); class TLC59208FOutput; From 09fc0288953d13a09bafef3e9038b36d3ff4c1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 15:16:26 -0600 Subject: [PATCH 0026/2056] [core] Remove dead global_state variable (#14060) --- esphome/core/component.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a452f4d400d..47c4a70c0f1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,6 @@ void store_component_error_message(const Component *component, const char *messa const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again -uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - float Component::get_loop_priority() const { return 0.0f; } float Component::get_setup_priority() const { return setup_priority::DATA; } From 02e310f2c9549349ae12615f3fec627d9189479a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:13 -0600 Subject: [PATCH 0027/2056] [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) --- esphome/components/esp32/core.cpp | 6 +++--- esphome/components/esp8266/core.cpp | 6 +++--- esphome/components/host/core.cpp | 6 +++--- esphome/components/libretiny/core.cpp | 6 +++--- esphome/components/rp2040/core.cpp | 6 +++--- esphome/core/application.cpp | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 09a45c14a67..202d929ab93 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -21,9 +21,9 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void IRAM_ATTR HOT yield() { vPortYield(); } +void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -44,7 +44,7 @@ void arch_init() { esp_ota_mark_app_valid_cancel_rollback(); #endif } -void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } +void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 784b87916b2..236d3022be3 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -14,9 +14,9 @@ extern "C" { namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_restart() { } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } +void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 164d622dd4a..c20a33fa378 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -11,7 +11,7 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::sched_yield(); } +void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -19,7 +19,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -void IRAM_ATTR HOT delay(uint32_t ms) { +void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; @@ -48,7 +48,7 @@ void arch_restart() { exit(0); } void arch_init() { // pass } -void IRAM_ATTR HOT arch_feed_wdt() { +void HOT arch_feed_wdt() { // pass } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index b22740f02a9..4dda7c38563 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -11,10 +11,10 @@ void loop(); namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { @@ -30,7 +30,7 @@ void arch_restart() { while (1) { } } -void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); } +void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index d88e9f54b76..37378d88bb1 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -9,9 +9,9 @@ namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,7 +27,7 @@ void arch_init() { #endif } -void IRAM_ATTR HOT arch_feed_wdt() { watchdog_update(); } +void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 449acc64cf4..406885fd81f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -240,7 +240,7 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { +void HOT Application::feed_wdt(uint32_t time) { static uint32_t last_feed = 0; // Use provided time if available, otherwise get current time uint32_t now = time ? time : millis(); From 387f615dae037ad8a19dd6f0fad7e04741eee1d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:48:30 -0600 Subject: [PATCH 0028/2056] [api] Add handshake timeout to prevent connection slot exhaustion (#14050) --- esphome/components/api/api_connection.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4d564af9e2a..5a7994a3225 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -60,6 +60,11 @@ static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +// Timeout for completing the handshake (Noise transport + HelloRequest). +// A stalled handshake from a buggy client or network glitch holds a connection +// slot, which can prevent legitimate clients from reconnecting. Also hardens +// against the less likely case of intentional connection slot exhaustion. +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -205,7 +210,12 @@ void APIConnection::loop() { this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { - this->last_traffic_ = now; + // Only update last_traffic_ after authentication to ensure the + // handshake timeout is an absolute deadline from connection start. + // Pre-auth messages (e.g. PingRequest) must not reset the timer. + if (this->is_authenticated()) { + this->last_traffic_ = now; + } // read a packet this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) @@ -223,6 +233,15 @@ void APIConnection::loop() { this->process_active_iterator_(); } + // Disconnect clients that haven't completed the handshake in time. + // Stale half-open connections from buggy clients or network issues can + // accumulate and block legitimate clients from reconnecting. + if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) { + this->on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting")); + return; + } + if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { @@ -1484,6 +1503,8 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + // Reset traffic timer so keepalive starts from authentication, not connection start + this->last_traffic_ = App.get_loop_component_start_time(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER { From d90754dc0a182785575f2232f3faa29c4d787c84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:19 -0600 Subject: [PATCH 0029/2056] [http_request] Replace heavy STL containers with std::vector for headers (#14024) --- esphome/components/http_request/__init__.py | 2 +- .../components/http_request/http_request.cpp | 22 +++---- .../components/http_request/http_request.h | 58 ++++++++++++------- .../http_request/http_request_arduino.cpp | 12 ++-- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 6 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 16 ++--- .../http_request/http_request_idf.h | 7 +-- 9 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 64d74323d67..5faffccbe4e 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -310,7 +310,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): cg.add(var.add_request_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []): - cg.add(var.add_collect_header(value)) + cg.add(var.add_collect_header(value.lower())) if response_conf := config.get(CONF_ON_RESPONSE): if capture_response: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 11dde4715ab..6590d2018ee 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto response_headers = this->get_response_headers(); - auto header_name_lower_case = str_lower_case(header_name); - if (response_headers.count(header_name_lower_case) == 0) { - ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str()); - return ""; - } else { - auto values = response_headers[header_name_lower_case]; - if (values.empty()) { - ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen", - header_name_lower_case.c_str()); - return ""; - } else { - auto header_value = values.front(); - ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str()); - return header_value; + auto lower = str_lower_case(header_name); + for (const auto &entry : this->response_headers_) { + if (entry.name == lower) { + ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + return entry.value; } } + ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); + return ""; } } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a427cc4a052..458ffe94a8c 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,6 +80,16 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/// Check if a header name should be collected (linear scan, fine for small lists) +inline bool should_collect_header(const std::vector &lower_case_collect_headers, + const std::string &lower_header_name) { + for (const auto &h : lower_case_collect_headers) { + if (h == lower_header_name) + return true; + } + return false; +} + /* * HTTP Container Read Semantics * ============================= @@ -258,20 +268,13 @@ class HttpContainer : public Parented { return !this->is_chunked_ && this->bytes_read_ >= this->content_length; } - /** - * @brief Get response headers. - * - * @return The key is the lower case response header name, the value is the header value. - */ - std::map> get_response_headers() { return this->response_headers_; } - std::string get_response_header(const std::string &header_name); protected: size_t bytes_read_{0}; bool secure_{false}; bool is_chunked_{false}; ///< True if response uses chunked transfer encoding - std::map> response_headers_{}; + std::vector
response_headers_{}; }; /// Read data from HTTP container into buffer with timeout handling @@ -333,8 +336,8 @@ class HttpRequestComponent : public Component { return this->start(url, "GET", "", request_headers); } std::shared_ptr get(const std::string &url, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "GET", "", request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { return this->start(url, "POST", body, {}); @@ -345,29 +348,40 @@ class HttpRequestComponent : public Component { } std::shared_ptr post(const std::string &url, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "POST", body, request_headers, collect_headers); + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - return this->start(url, method, body, request_headers, {}); + // Call perform() directly to avoid ambiguity with the std::set overload + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, request_headers, lower); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - std::set lower_case_collect_headers; - for (const std::string &collect_header : collect_headers) { - lower_case_collect_headers.insert(str_lower_case(collect_header)); - } + const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) = 0; + const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; @@ -389,7 +403,7 @@ template class HttpRequestSendAction : public Action { this->request_headers_.insert({key, value}); } - void add_collect_header(const char *value) { this->collect_headers_.insert(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } @@ -431,7 +445,7 @@ template class HttpRequestSendAction : public Action { } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->collect_headers_); + this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); @@ -494,7 +508,7 @@ template class HttpRequestSendAction : public Action { void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; std::map> request_headers_{}; - std::set collect_headers_{"content-type", "content-length"}; + std::vector lower_case_collect_headers_{"content-type", "content-length"}; std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index e5b919e3800..3f60b76b587 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -27,7 +27,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -107,9 +107,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } // returned needed headers must be collected before the requests - const char *header_keys[collect_headers.size()]; + const char *header_keys[lower_case_collect_headers.size()]; int index = 0; - for (auto const &header_name : collect_headers) { + for (auto const &header_name : lower_case_collect_headers) { header_keys[index++] = header_name.c_str(); } container->client_.collectHeaders(header_keys, index); @@ -160,14 +160,14 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // Still return the container, so it can be used to get the status code and error message } - container->response_headers_ = {}; + container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); - if (collect_headers.count(header_name) > 0) { + if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - container->response_headers_[header_name].push_back(header_value); + container->response_headers_.push_back({header_name, header_value}); } } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index a1084b12d54..dbd61de3643 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -50,7 +50,7 @@ class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; }; } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index b94570be124..714a73fc31d 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -19,7 +19,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -116,8 +116,8 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); auto lower_name = str_lower_case(header.first); - if (response_headers.find(lower_name) != response_headers.end()) { - container->response_headers_[lower_name].emplace_back(header.second); + if (should_collect_header(lower_case_collect_headers, lower_name)) { + container->response_headers_.push_back({lower_name, header.second}); } } container->duration_ms = millis() - start; diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 32e149e6a30..79f5b7e8179 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &response_headers) override; + const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 486984a694b..0921c50b9f8 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -19,8 +19,8 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; struct UserData { - const std::set &collect_headers; - std::map> response_headers; + const std::vector &lower_case_collect_headers; + std::vector
&response_headers; }; void HttpRequestIDF::dump_config() { @@ -38,10 +38,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); - if (user_data->collect_headers.count(header_name)) { + if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers[header_name].push_back(header_value); + user_data->response_headers.push_back({header_name, header_value}); } break; } @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); @@ -110,8 +110,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); config.event_handler = http_event_handler; - auto user_data = UserData{collect_headers, {}}; - config.user_data = static_cast(&user_data); esp_http_client_handle_t client = esp_http_client_init(&config); @@ -120,6 +118,9 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->set_secure(secure); + auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; + esp_http_client_set_user_data(client, static_cast(&user_data)); + for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } @@ -164,7 +165,6 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); - container->set_response_headers(user_data.response_headers); container->duration_ms = millis() - start; if (is_success(container->status_code)) { return container; diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 2a130eae587..9206ba6f5d7 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -21,11 +21,8 @@ class HttpContainerIDF : public HttpContainer { /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); - void set_response_headers(std::map> &response_headers) { - this->response_headers_ = std::move(response_headers); - } - protected: + friend class HttpRequestIDF; esp_http_client_handle_t client_; }; @@ -41,7 +38,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; From bd055e75b9c2def6d9e12170e771920115c9751f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 16:49:37 -0600 Subject: [PATCH 0030/2056] [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053) Co-authored-by: Claude Opus 4.6 --- esphome/core/application.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 30611227a28..e0299f3db3c 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -582,9 +582,6 @@ class Application { std::string name_; std::string friendly_name_; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; @@ -594,7 +591,8 @@ class Application { #endif // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t dump_config_at_{std::numeric_limits::max()}; // Index into components_ for dump_config progress + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration From 5f82017a310bd60123eadee17a5f2f3d36106694 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 0031/2056] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f25163..c9586d0b956 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From 3b869f172064dc994f2ff001e4c73c8da858b01e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 0032/2056] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baeec..294a5e0a152 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 565443b710eb8b56bd7bc2d53fef765d195e4303 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 0033/2056] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f5..ef4cc980f66 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From be853afc2465a291d3a4806122884c52d0bd905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:57:56 -0600 Subject: [PATCH 0034/2056] [core] Conditionally compile setup_priority override infrastructure (#14057) --- esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 19 +++++++++++++------ esphome/core/component.h | 1 + esphome/core/defines.h | 1 + esphome/cpp_helpers.py | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 406885fd81f..b216233f9b6 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -137,8 +137,10 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) // Set up wake socket for waking main loop from tasks diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 47c4a70c0f1..ba0f1663b9e 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -41,20 +41,23 @@ struct ComponentErrorMessage { bool is_flash_ptr; }; +#ifdef USE_SETUP_PRIORITY_OVERRIDE struct ComponentPriorityOverride { const Component *component; float priority; }; +// Setup priority overrides - freed after setup completes +// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector *setup_priority_overrides = nullptr; +#endif + // Error messages for failed components // Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead // This is never freed as error messages persist for the lifetime of the device // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::vector *component_error_messages = nullptr; -// Setup priority overrides - freed after setup completes -// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector *setup_priority_overrides = nullptr; // Helper to store error messages - reduces duplication between deprecated and new API // Remove before 2026.6.0 when deprecated const char* API is removed @@ -459,6 +462,7 @@ void log_update_interval(const char *tag, PollingComponent *component) { } } float Component::get_actual_setup_priority() const { +#ifdef USE_SETUP_PRIORITY_OVERRIDE // Check if there's an override in the global vector if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) @@ -468,14 +472,14 @@ float Component::get_actual_setup_priority() const { } } } +#endif return this->get_setup_priority(); } +#ifdef USE_SETUP_PRIORITY_OVERRIDE void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed if (!setup_priority_overrides) { setup_priority_overrides = new std::vector(); - // Reserve some space to avoid reallocations (most configs have < 10 overrides) - setup_priority_overrides->reserve(10); } // Check if this component already has an override @@ -489,6 +493,7 @@ void Component::set_setup_priority(float priority) { // Add new override setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority}); } +#endif bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) @@ -557,10 +562,12 @@ uint32_t WarnIfComponentBlockingGuard::finish() { WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} +#ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely delete setup_priority_overrides; setup_priority_overrides = nullptr; } +#endif } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 848bc0ba351..6f7f77dbc10 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,6 +563,7 @@ class WarnIfComponentBlockingGuard { }; // Function to clear setup priority overrides after all components are set up +// Only has an implementation when USE_SETUP_PRIORITY_OVERRIDE is defined void clear_setup_priority_overrides(); } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0d6c1a42e83..bcafcb4c605 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -109,6 +109,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR #define USE_SWITCH diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d58..954a28d3d1d 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -49,6 +49,7 @@ async def register_component(var, config): ) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: + add_define("USE_SETUP_PRIORITY_OVERRIDE") add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) if CONF_UPDATE_INTERVAL in config: add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) From e4c233b6ceef1d1a306e5879752ffc9815d1190b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 20:59:31 -0600 Subject: [PATCH 0035/2056] [mqtt] Use constexpr for compile-time constants (#14075) --- esphome/components/mqtt/mqtt_backend_esp32.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index adba0cf0046..ccc4c4026c7 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -114,11 +114,11 @@ struct QueueElement { class MQTTBackendESP32 final : public MQTTBackend { public: - static const size_t MQTT_BUFFER_SIZE = 4096; - static const size_t TASK_STACK_SIZE = 3072; - static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations - static const ssize_t TASK_PRIORITY = 5; - static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr size_t MQTT_BUFFER_SIZE = 4096; + static constexpr size_t TASK_STACK_SIZE = 3072; + static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static constexpr ssize_t TASK_PRIORITY = 5; + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } From 66d2ac8cb93a5b7380210ee7933d28620dabd3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:09 -0600 Subject: [PATCH 0036/2056] [web_server] Move climate static traits to DETAIL_ALL only (#14066) --- esphome/components/web_server/web_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 3acd2d21190..a796c1426b9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1546,16 +1546,16 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); this->add_sorting_info_(root, obj); } bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("min_temp")] = - (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); - root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; From 7e118178b3d5e208eab87e0f882b38dcb18e5ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 0037/2056] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a796c1426b9..e3d131f58ee 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1922,6 +1922,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1944,10 +1947,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 9c9365c146064177dbc371d425307f8e2cb728d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:06 -0600 Subject: [PATCH 0038/2056] [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 4 ++-- .../components/esp32_ble_client/ble_client_base.cpp | 12 ++++++------ .../components/esp32_ble_server/ble_characteristic.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index ab9aee2d816..62a035e79ff 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -35,8 +35,8 @@ using namespace esp32_ble_client; // Version 3: New connection API // Version 4: Pairing support // Version 5: Cache clear support -static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; +static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; enum BluetoothProxyFeature : uint32_t { FEATURE_PASSIVE_SCAN = 1 << 0, diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index c464c893900..3f0eeeab4ad 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,17 +16,17 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +static constexpr uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static constexpr uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. -static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s +static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s // Fastest connection parameters for devices with short discovery timeouts -static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) -static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms -static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index c2cdb1660ce..72897d1dfbe 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -57,12 +57,12 @@ class BLECharacteristic { ESPBTUUID get_uuid() { return this->uuid_; } std::vector &get_value() { return this->value_; } - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + static constexpr uint32_t PROPERTY_READ = 1 << 0; + static constexpr uint32_t PROPERTY_WRITE = 1 << 1; + static constexpr uint32_t PROPERTY_NOTIFY = 1 << 2; + static constexpr uint32_t PROPERTY_BROADCAST = 1 << 3; + static constexpr uint32_t PROPERTY_INDICATE = 1 << 4; + static constexpr uint32_t PROPERTY_WRITE_NR = 1 << 5; bool is_created(); bool is_failed(); From 76c151c6e6948fbe2f5c62356ef299797cc83480 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:07:38 -0600 Subject: [PATCH 0039/2056] [api] Use constexpr for compile-time constant (#14072) --- esphome/components/api/api_frame_helper_noise.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 1ae848deadf..492988128a9 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -474,7 +474,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s // buf_start[1], buf_start[2] to be set after encryption // Write message header (to be encrypted) - const uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = 3; buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte From ee7d63f73a3587cc86c6d6fe71e5fcd3df4e31b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:09:49 -0600 Subject: [PATCH 0040/2056] [packet_transport] Use constexpr for compile-time constants (#14074) --- esphome/components/packet_transport/packet_transport.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 365a5f2ec78..7b7a8523986 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -58,9 +58,9 @@ union FuData { float f32; }; -static const uint16_t MAGIC_NUMBER = 0x4553; -static const uint16_t MAGIC_PING = 0x5048; -static const uint32_t PREF_HASH = 0x45535043; +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; +static constexpr uint32_t PREF_HASH = 0x45535043; enum DataKey { ZERO_FILL_KEY, DATA_KEY, From 20239d1bb304ceea6f7056c0466071178ad8af50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:16:09 -0600 Subject: [PATCH 0041/2056] [remote_base] Use constexpr for compile-time constants (#14076) --- .../remote_base/abbwelcome_protocol.cpp | 8 +++---- .../remote_base/abbwelcome_protocol.h | 4 ++-- .../components/remote_base/aeha_protocol.cpp | 14 +++++------ .../remote_base/byronsx_protocol.cpp | 10 ++++---- .../remote_base/canalsat_protocol.cpp | 8 +++---- .../components/remote_base/dish_protocol.cpp | 10 ++++---- .../components/remote_base/dooya_protocol.cpp | 12 +++++----- .../remote_base/drayton_protocol.cpp | 22 ++++++++--------- .../components/remote_base/jvc_protocol.cpp | 12 +++++----- .../remote_base/keeloq_protocol.cpp | 22 ++++++++--------- .../components/remote_base/lg_protocol.cpp | 10 ++++---- .../remote_base/magiquest_protocol.cpp | 10 ++++---- .../components/remote_base/midea_protocol.h | 2 +- .../components/remote_base/nec_protocol.cpp | 10 ++++---- .../components/remote_base/nexa_protocol.cpp | 22 ++++++++--------- .../remote_base/panasonic_protocol.cpp | 10 ++++---- .../remote_base/pioneer_protocol.cpp | 12 +++++----- .../remote_base/pronto_protocol.cpp | 24 +++++++++---------- .../components/remote_base/rc5_protocol.cpp | 4 ++-- .../components/remote_base/rc6_protocol.cpp | 10 ++++---- .../remote_base/roomba_protocol.cpp | 10 ++++---- .../remote_base/samsung36_protocol.cpp | 20 ++++++++-------- .../remote_base/samsung_protocol.cpp | 14 +++++------ .../components/remote_base/sony_protocol.cpp | 10 ++++---- .../remote_base/symphony_protocol.cpp | 14 +++++------ .../remote_base/toshiba_ac_protocol.cpp | 16 ++++++------- .../components/remote_base/toto_protocol.cpp | 12 +++++----- 27 files changed, 166 insertions(+), 166 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp index 352ae10ed7c..a67ca48dbeb 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.cpp +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -6,10 +6,10 @@ namespace remote_base { static const char *const TAG = "remote.abbwelcome"; -static const uint32_t BIT_ONE_SPACE_US = 102; -static const uint32_t BIT_ZERO_MARK_US = 32; // 18-44 -static const uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; -static const uint16_t BYTE_SPACE_US = 210; +static constexpr uint32_t BIT_ONE_SPACE_US = 102; +static constexpr uint32_t BIT_ZERO_MARK_US = 32; // 18-44 +static constexpr uint32_t BIT_ZERO_SPACE_US = BIT_ONE_SPACE_US - BIT_ZERO_MARK_US; +static constexpr uint16_t BYTE_SPACE_US = 210; uint8_t ABBWelcomeData::calc_cs_() const { uint8_t checksum = 0; diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 1dddedf8ce1..66664a89f3b 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -11,8 +11,8 @@ namespace esphome { namespace remote_base { -static const uint8_t MAX_DATA_LENGTH = 15; -static const uint8_t DATA_LENGTH_MASK = 0x3f; +static constexpr uint8_t MAX_DATA_LENGTH = 15; +static constexpr uint8_t DATA_LENGTH_MASK = 0x3f; /* Message Format: diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 3b926e79815..f40cff7623d 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.aeha"; -static const uint16_t BITWISE = 425; -static const uint16_t HEADER_HIGH_US = BITWISE * 8; -static const uint16_t HEADER_LOW_US = BITWISE * 4; -static const uint16_t BIT_HIGH_US = BITWISE; -static const uint16_t BIT_ONE_LOW_US = BITWISE * 3; -static const uint16_t BIT_ZERO_LOW_US = BITWISE; -static const uint16_t TRAILER = BITWISE; +static constexpr uint16_t BITWISE = 425; +static constexpr uint16_t HEADER_HIGH_US = BITWISE * 8; +static constexpr uint16_t HEADER_LOW_US = BITWISE * 4; +static constexpr uint16_t BIT_HIGH_US = BITWISE; +static constexpr uint16_t BIT_ONE_LOW_US = BITWISE * 3; +static constexpr uint16_t BIT_ZERO_LOW_US = BITWISE; +static constexpr uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { dst->reserve(2 + 32 + (data.data.size() * 2) + 1); diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp index 6bfa4b7ff9f..f243b5bdfd3 100644 --- a/esphome/components/remote_base/byronsx_protocol.cpp +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -8,11 +8,11 @@ namespace remote_base { static const char *const TAG = "remote.byronsx"; -static const uint32_t BIT_TIME_US = 333; -static const uint8_t NBITS_ADDRESS = 8; -static const uint8_t NBITS_COMMAND = 4; -static const uint8_t NBITS_START_BIT = 1; -static const uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; +static constexpr uint32_t BIT_TIME_US = 333; +static constexpr uint8_t NBITS_ADDRESS = 8; +static constexpr uint8_t NBITS_COMMAND = 4; +static constexpr uint8_t NBITS_START_BIT = 1; +static constexpr uint8_t NBITS_DATA = NBITS_ADDRESS + NBITS_COMMAND /*+ NBITS_COMMAND*/; /* ByronSX Protocol diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index bee3d57fd83..1468b669395 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -7,10 +7,10 @@ namespace remote_base { static const char *const CANALSAT_TAG = "remote.canalsat"; static const char *const CANALSATLD_TAG = "remote.canalsatld"; -static const uint16_t CANALSAT_FREQ = 55500; -static const uint16_t CANALSATLD_FREQ = 56000; -static const uint16_t CANALSAT_UNIT = 250; -static const uint16_t CANALSATLD_UNIT = 320; +static constexpr uint16_t CANALSAT_FREQ = 55500; +static constexpr uint16_t CANALSATLD_FREQ = 56000; +static constexpr uint16_t CANALSAT_UNIT = 250; +static constexpr uint16_t CANALSATLD_UNIT = 320; CanalSatProtocol::CanalSatProtocol() { this->frequency_ = CANALSAT_FREQ; diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 754b6c3b12c..69226101bf1 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.dish"; -static const uint32_t HEADER_HIGH_US = 400; -static const uint32_t HEADER_LOW_US = 6100; -static const uint32_t BIT_HIGH_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 2800; +static constexpr uint32_t HEADER_HIGH_US = 400; +static constexpr uint32_t HEADER_LOW_US = 6100; +static constexpr uint32_t BIT_HIGH_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 2800; void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) { dst->reserve(138); diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp index 04c5fef8f32..84bdbf3e082 100644 --- a/esphome/components/remote_base/dooya_protocol.cpp +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.dooya"; -static const uint32_t HEADER_HIGH_US = 5000; -static const uint32_t HEADER_LOW_US = 1500; -static const uint32_t BIT_ZERO_HIGH_US = 350; -static const uint32_t BIT_ZERO_LOW_US = 750; -static const uint32_t BIT_ONE_HIGH_US = 750; -static const uint32_t BIT_ONE_LOW_US = 350; +static constexpr uint32_t HEADER_HIGH_US = 5000; +static constexpr uint32_t HEADER_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_HIGH_US = 350; +static constexpr uint32_t BIT_ZERO_LOW_US = 750; +static constexpr uint32_t BIT_ONE_HIGH_US = 750; +static constexpr uint32_t BIT_ONE_LOW_US = 350; void DooyaProtocol::encode(RemoteTransmitData *dst, const DooyaData &data) { dst->set_carrier_frequency(0); diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index da2e985af00..946bd9cacb1 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.drayton"; -static const uint32_t BIT_TIME_US = 500; -static const uint8_t CARRIER_KHZ = 2; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_SYNC = 4; -static const uint8_t NBITS_ADDRESS = 16; -static const uint8_t NBITS_CHANNEL = 5; -static const uint8_t NBITS_COMMAND = 7; -static const uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static const uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); +static constexpr uint32_t BIT_TIME_US = 500; +static constexpr uint8_t CARRIER_KHZ = 2; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_SYNC = 4; +static constexpr uint8_t NBITS_ADDRESS = 16; +static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_COMMAND = 7; +static constexpr uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; +static constexpr uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); -static const uint8_t CMD_ON = 0x41; -static const uint8_t CMD_OFF = 0x02; +static constexpr uint8_t CMD_ON = 0x41; +static constexpr uint8_t CMD_OFF = 0x02; /* Drayton Protocol diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index ca423b61e69..c33cae7a486 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.jvc"; -static const uint8_t NBITS = 16; -static const uint32_t HEADER_HIGH_US = 8400; -static const uint32_t HEADER_LOW_US = 4200; -static const uint32_t BIT_ONE_LOW_US = 1725; -static const uint32_t BIT_ZERO_LOW_US = 525; -static const uint32_t BIT_HIGH_US = 525; +static constexpr uint8_t NBITS = 16; +static constexpr uint32_t HEADER_HIGH_US = 8400; +static constexpr uint32_t HEADER_LOW_US = 4200; +static constexpr uint32_t BIT_ONE_LOW_US = 1725; +static constexpr uint32_t BIT_ZERO_LOW_US = 525; +static constexpr uint32_t BIT_HIGH_US = 525; void JVCProtocol::encode(RemoteTransmitData *dst, const JVCData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp index 72540c37f1f..e95c79ef25f 100644 --- a/esphome/components/remote_base/keeloq_protocol.cpp +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -8,18 +8,18 @@ namespace remote_base { static const char *const TAG = "remote.keeloq"; -static const uint32_t BIT_TIME_US = 380; -static const uint8_t NBITS_PREAMBLE = 12; -static const uint8_t NBITS_REPEAT = 1; -static const uint8_t NBITS_VLOW = 1; -static const uint8_t NBITS_SERIAL = 28; -static const uint8_t NBITS_BUTTONS = 4; -static const uint8_t NBITS_DISC = 12; -static const uint8_t NBITS_SYNC_CNT = 16; +static constexpr uint32_t BIT_TIME_US = 380; +static constexpr uint8_t NBITS_PREAMBLE = 12; +static constexpr uint8_t NBITS_REPEAT = 1; +static constexpr uint8_t NBITS_VLOW = 1; +static constexpr uint8_t NBITS_SERIAL = 28; +static constexpr uint8_t NBITS_BUTTONS = 4; +static constexpr uint8_t NBITS_DISC = 12; +static constexpr uint8_t NBITS_SYNC_CNT = 16; -static const uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; -static const uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; -static const uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; +static constexpr uint8_t NBITS_FIXED_DATA = NBITS_REPEAT + NBITS_VLOW + NBITS_BUTTONS + NBITS_SERIAL; +static constexpr uint8_t NBITS_ENCRYPTED_DATA = NBITS_BUTTONS + NBITS_DISC + NBITS_SYNC_CNT; +static constexpr uint8_t NBITS_DATA = NBITS_FIXED_DATA + NBITS_ENCRYPTED_DATA; /* KeeLoq Protocol diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index d25c59d2b12..4c54ff00bd7 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.lg"; -static const uint32_t HEADER_HIGH_US = 8000; -static const uint32_t HEADER_LOW_US = 4000; -static const uint32_t BIT_HIGH_US = 600; -static const uint32_t BIT_ONE_LOW_US = 1600; -static const uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t HEADER_HIGH_US = 8000; +static constexpr uint32_t HEADER_LOW_US = 4000; +static constexpr uint32_t BIT_HIGH_US = 600; +static constexpr uint32_t BIT_ONE_LOW_US = 1600; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; void LGProtocol::encode(RemoteTransmitData *dst, const LGData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index 1cec58a55f6..f25a982fdc3 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -10,11 +10,11 @@ namespace remote_base { static const char *const TAG = "remote.magiquest"; -static const uint32_t MAGIQUEST_UNIT = 288; // us -static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; -static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_UNIT = 288; // us +static constexpr uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static constexpr uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index ddefff867a3..334e8a7cb31 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -62,7 +62,7 @@ class MideaData { this->data_[idx] |= (value << shift); } void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } - static const uint8_t OFFSET_CS = 5; + static constexpr uint8_t OFFSET_CS = 5; // 48-bits data std::array data_; // Calculate checksum diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 6ea9a8583c7..062f81b4d60 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.nec"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index 862165714e9..28b415d4d64 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -6,18 +6,18 @@ namespace remote_base { static const char *const TAG = "remote.nexa"; -static const uint8_t NBITS = 32; -static const uint32_t HEADER_HIGH_US = 319; -static const uint32_t HEADER_LOW_US = 2610; -static const uint32_t BIT_HIGH_US = 319; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_LOW_US = 140; +static constexpr uint8_t NBITS = 32; +static constexpr uint32_t HEADER_HIGH_US = 319; +static constexpr uint32_t HEADER_LOW_US = 2610; +static constexpr uint32_t BIT_HIGH_US = 319; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_LOW_US = 140; -static const uint32_t TX_HEADER_HIGH_US = 250; -static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; -static const uint32_t TX_BIT_HIGH_US = 250; -static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; -static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; +static constexpr uint32_t TX_HEADER_HIGH_US = 250; +static constexpr uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static constexpr uint32_t TX_BIT_HIGH_US = 250; +static constexpr uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static constexpr uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; void NexaProtocol::one(RemoteTransmitData *dst) const { // '1' => '10' diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index d6cf1a160db..e0acc42692e 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.panasonic"; -static const uint32_t HEADER_HIGH_US = 3502; -static const uint32_t HEADER_LOW_US = 1750; -static const uint32_t BIT_HIGH_US = 502; -static const uint32_t BIT_ZERO_LOW_US = 400; -static const uint32_t BIT_ONE_LOW_US = 1244; +static constexpr uint32_t HEADER_HIGH_US = 3502; +static constexpr uint32_t HEADER_LOW_US = 1750; +static constexpr uint32_t BIT_HIGH_US = 502; +static constexpr uint32_t BIT_ZERO_LOW_US = 400; +static constexpr uint32_t BIT_ONE_LOW_US = 1244; void PanasonicProtocol::encode(RemoteTransmitData *dst, const PanasonicData &data) { dst->reserve(100); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index 043565282d0..f350ef66aee 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.pioneer"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t TRAILER_SPACE_US = 25500; +static constexpr uint32_t HEADER_HIGH_US = 9000; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t TRAILER_SPACE_US = 25500; void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 401a0976b2f..cff31451998 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -59,18 +59,18 @@ bool ProntoData::operator==(const ProntoData &rhs) const { } // DO NOT EXPORT from this file -static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; -static const uint16_t LEARNED_TOKEN = 0x0000U; -static const uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; -static const uint16_t BITS_IN_HEXADECIMAL = 4U; -static const uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; -static const uint16_t NUMBERS_IN_PREAMBLE = 4U; -static const uint16_t HEX_MASK = 0xFU; -static const uint32_t REFERENCE_FREQUENCY = 4145146UL; -static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; -static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; -static const uint16_t PRONTO_DEFAULT_GAP = 45000; -static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr uint16_t MICROSECONDS_T_MAX = 0xFFFFU; +static constexpr uint16_t LEARNED_TOKEN = 0x0000U; +static constexpr uint16_t LEARNED_NON_MODULATED_TOKEN = 0x0100U; +static constexpr uint16_t BITS_IN_HEXADECIMAL = 4U; +static constexpr uint16_t DIGITS_IN_PRONTO_NUMBER = 4U; +static constexpr uint16_t NUMBERS_IN_PREAMBLE = 4U; +static constexpr uint16_t HEX_MASK = 0xFU; +static constexpr uint32_t REFERENCE_FREQUENCY = 4145146UL; +static constexpr uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; +static constexpr uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; +static constexpr uint16_t PRONTO_DEFAULT_GAP = 45000; +static constexpr uint16_t MARK_EXCESS_MICROS = 20; static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 08f2f2eaa37..bb6d382d804 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -6,8 +6,8 @@ namespace remote_base { static const char *const TAG = "remote.rc5"; -static const uint32_t BIT_TIME_US = 889; -static const uint8_t NBITS = 14; +static constexpr uint32_t BIT_TIME_US = 889; +static constexpr uint8_t NBITS = 14; void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { static bool toggle = false; diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index fcb4da11a44..b442bb4c270 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const RC6_TAG = "remote.rc6"; -static const uint16_t RC6_FREQ = 36000; -static const uint16_t RC6_UNIT = 444; -static const uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); -static const uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); -static const uint16_t RC6_MODE_MASK = 0x07; +static constexpr uint16_t RC6_FREQ = 36000; +static constexpr uint16_t RC6_UNIT = 444; +static constexpr uint16_t RC6_HEADER_MARK = (6 * RC6_UNIT); +static constexpr uint16_t RC6_HEADER_SPACE = (2 * RC6_UNIT); +static constexpr uint16_t RC6_MODE_MASK = 0x07; void RC6Protocol::encode(RemoteTransmitData *dst, const RC6Data &data) { dst->reserve(44); diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp index 2d2dde114af..6b7d2163748 100644 --- a/esphome/components/remote_base/roomba_protocol.cpp +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.roomba"; -static const uint8_t NBITS = 8; -static const uint32_t BIT_ONE_HIGH_US = 3000; -static const uint32_t BIT_ONE_LOW_US = 1000; -static const uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; -static const uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; +static constexpr uint8_t NBITS = 8; +static constexpr uint32_t BIT_ONE_HIGH_US = 3000; +static constexpr uint32_t BIT_ONE_LOW_US = 1000; +static constexpr uint32_t BIT_ZERO_HIGH_US = BIT_ONE_LOW_US; +static constexpr uint32_t BIT_ZERO_LOW_US = BIT_ONE_HIGH_US; void RoombaProtocol::encode(RemoteTransmitData *dst, const RoombaData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index bd3eee58498..10e8bd2d013 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -6,17 +6,17 @@ namespace remote_base { static const char *const TAG = "remote.samsung36"; -static const uint8_t NBITS = 78; +static constexpr uint8_t NBITS = 78; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 500; -static const uint32_t BIT_ONE_LOW_US = 1500; -static const uint32_t BIT_ZERO_LOW_US = 500; -static const uint32_t MIDDLE_HIGH_US = 500; -static const uint32_t MIDDLE_LOW_US = 4500; -static const uint32_t FOOTER_HIGH_US = 500; -static const uint32_t FOOTER_LOW_US = 59000; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 500; +static constexpr uint32_t BIT_ONE_LOW_US = 1500; +static constexpr uint32_t BIT_ZERO_LOW_US = 500; +static constexpr uint32_t MIDDLE_HIGH_US = 500; +static constexpr uint32_t MIDDLE_LOW_US = 4500; +static constexpr uint32_t FOOTER_HIGH_US = 500; +static constexpr uint32_t FOOTER_LOW_US = 59000; void Samsung36Protocol::encode(RemoteTransmitData *dst, const Samsung36Data &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 2d6d5531e58..2a48cbb918e 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -7,13 +7,13 @@ namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 560; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 69f2b49c42e..504b3469251 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -6,11 +6,11 @@ namespace remote_base { static const char *const TAG = "remote.sony"; -static const uint32_t HEADER_HIGH_US = 2400; -static const uint32_t HEADER_LOW_US = 600; -static const uint32_t BIT_ONE_HIGH_US = 1200; -static const uint32_t BIT_ZERO_HIGH_US = 600; -static const uint32_t BIT_LOW_US = 600; +static constexpr uint32_t HEADER_HIGH_US = 2400; +static constexpr uint32_t HEADER_LOW_US = 600; +static constexpr uint32_t BIT_ONE_HIGH_US = 1200; +static constexpr uint32_t BIT_ZERO_HIGH_US = 600; +static constexpr uint32_t BIT_LOW_US = 600; void SonyProtocol::encode(RemoteTransmitData *dst, const SonyData &data) { dst->set_carrier_frequency(40000); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index 34b5dba07fd..f30a980d91e 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -13,16 +13,16 @@ static const char *const TAG = "remote.symphony"; // footer-gap handling used there. // Symphony protocol timing specifications (tuned to handset captures) -static const uint32_t BIT_ZERO_HIGH_US = 460; // short -static const uint32_t BIT_ZERO_LOW_US = 1260; // long -static const uint32_t BIT_ONE_HIGH_US = 1260; // long -static const uint32_t BIT_ONE_LOW_US = 460; // short -static const uint32_t CARRIER_FREQUENCY = 38000; +static constexpr uint32_t BIT_ZERO_HIGH_US = 460; // short +static constexpr uint32_t BIT_ZERO_LOW_US = 1260; // long +static constexpr uint32_t BIT_ONE_HIGH_US = 1260; // long +static constexpr uint32_t BIT_ONE_LOW_US = 460; // short +static constexpr uint32_t CARRIER_FREQUENCY = 38000; // IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) -static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +static constexpr uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); // Typical inter-frame gap (~34.8 ms observed) -static const uint32_t INTER_FRAME_GAP_US = 34760; +static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index 42241eea8c9..a20a29b84a9 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -7,14 +7,14 @@ namespace remote_base { static const char *const TAG = "remote.toshibaac"; -static const uint32_t HEADER_HIGH_US = 4500; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; -static const uint32_t FOOTER_HIGH_US = 560; -static const uint32_t FOOTER_LOW_US = 4500; -static const uint16_t PACKET_SPACE = 5500; +static constexpr uint32_t HEADER_HIGH_US = 4500; +static constexpr uint32_t HEADER_LOW_US = 4500; +static constexpr uint32_t BIT_HIGH_US = 560; +static constexpr uint32_t BIT_ONE_LOW_US = 1690; +static constexpr uint32_t BIT_ZERO_LOW_US = 560; +static constexpr uint32_t FOOTER_HIGH_US = 560; +static constexpr uint32_t FOOTER_LOW_US = 4500; +static constexpr uint16_t PACKET_SPACE = 5500; void ToshibaAcProtocol::encode(RemoteTransmitData *dst, const ToshibaAcData &data) { dst->set_carrier_frequency(38000); diff --git a/esphome/components/remote_base/toto_protocol.cpp b/esphome/components/remote_base/toto_protocol.cpp index ba21263bc84..f08258c4a3e 100644 --- a/esphome/components/remote_base/toto_protocol.cpp +++ b/esphome/components/remote_base/toto_protocol.cpp @@ -6,12 +6,12 @@ namespace remote_base { static const char *const TAG = "remote.toto"; -static const uint32_t PREAMBLE_HIGH_US = 6200; -static const uint32_t PREAMBLE_LOW_US = 2800; -static const uint32_t BIT_HIGH_US = 550; -static const uint32_t BIT_ONE_LOW_US = 1700; -static const uint32_t BIT_ZERO_LOW_US = 550; -static const uint32_t TOTO_HEADER = 0x2008; +static constexpr uint32_t PREAMBLE_HIGH_US = 6200; +static constexpr uint32_t PREAMBLE_LOW_US = 2800; +static constexpr uint32_t BIT_HIGH_US = 550; +static constexpr uint32_t BIT_ONE_LOW_US = 1700; +static constexpr uint32_t BIT_ZERO_LOW_US = 550; +static constexpr uint32_t TOTO_HEADER = 0x2008; void TotoProtocol::encode(RemoteTransmitData *dst, const TotoData &data) { uint32_t payload = 0; From dff9780d3af9f6ce41bf9b3db8e6138dc197c08f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:19:48 -0600 Subject: [PATCH 0042/2056] [core] Use constexpr for compile-time constants (#14071) --- esphome/core/application.h | 2 +- esphome/core/helpers.cpp | 6 +++--- esphome/core/scheduler.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e0299f3db3c..5b3e3dfed6d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -111,7 +111,7 @@ namespace esphome { // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down // cleanly is a warning in the log. -static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot +static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick reboot class Application { public: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c2f7f67d9a5..09e755ca713 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -846,9 +846,9 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); - const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. - // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) - // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known + constexpr uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. + // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) + // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known if (us > lag) { delay((us - lag) / 1000UL); // note: in disabled-interrupt contexts delay() won't actually sleep while (micros() - start < us - lag) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 4194c3aa9ef..3294f689e8b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -728,7 +728,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption - static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); From e7f202186450ad2f865ecbc865ca87b10b243dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:24 -0600 Subject: [PATCH 0043/2056] [http_request] Replace std::map with std::vector in action template (#14026) --- esphome/components/http_request/__init__.py | 6 +++++- esphome/components/http_request/http_request.h | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 5faffccbe4e..2d6ecae0bcd 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -302,10 +302,14 @@ async def http_request_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: + cg.add(var.init_json(len(json_))) for key in json_: template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + request_headers = config.get(CONF_REQUEST_HEADERS, {}) + if request_headers: + cg.add(var.init_request_headers(len(request_headers))) + for key, value in request_headers.items(): template_ = await cg.templatable(value, args, cg.const_char_ptr) cg.add(var.add_request_header(key, template_)) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 458ffe94a8c..2b2d05c63fd 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -399,13 +398,15 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(bool, capture_response) #endif + void init_request_headers(size_t count) { this->request_headers_.init(count); } void add_request_header(const char *key, TemplatableValue value) { - this->request_headers_.insert({key, value}); + this->request_headers_.push_back({key, value}); } void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } - void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + void init_json(size_t count) { this->json_.init(count); } + void add_json(const char *key, TemplatableValue value) { this->json_.push_back({key, value}); } void set_json(std::function json_func) { this->json_func_ = json_func; } @@ -507,9 +508,9 @@ template class HttpRequestSendAction : public Action { } void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; - std::map> request_headers_{}; + FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; - std::map> json_{}; + FixedVector>> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE Trigger, std::string &, Ts...> success_trigger_with_response_; From eaf0d03a37a85ead5f33c5b3512d80944f2bfa09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 0044/2056] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61e..cf78a1a4609 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f864970..02250c59114 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 9a8b00a42835b20db98081f1801c5cbafafc723a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:23 -0600 Subject: [PATCH 0045/2056] [nfc] Use constexpr for compile-time constants (#14077) --- esphome/components/nfc/nci_core.h | 228 +++++++++++------------ esphome/components/nfc/ndef_message.h | 2 +- esphome/components/nfc/ndef_record.h | 16 +- esphome/components/nfc/ndef_record_uri.h | 2 +- esphome/components/nfc/nfc.h | 60 +++--- 5 files changed, 154 insertions(+), 154 deletions(-) diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h index fdaf6d0cc59..6b42070ed02 100644 --- a/esphome/components/nfc/nci_core.h +++ b/esphome/components/nfc/nci_core.h @@ -8,137 +8,137 @@ namespace esphome { namespace nfc { // Header info -static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes -static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets -static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset -static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset -static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +static constexpr uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static constexpr uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static constexpr uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static constexpr uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static constexpr uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset // Important masks -static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask -static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit -static const uint8_t NCI_PKT_GID_MASK = 0x0F; -static const uint8_t NCI_PKT_OID_MASK = 0x3F; +static constexpr uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static constexpr uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static constexpr uint8_t NCI_PKT_GID_MASK = 0x0F; +static constexpr uint8_t NCI_PKT_OID_MASK = 0x3F; // Message types -static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) -static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC -static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands -static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +static constexpr uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static constexpr uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static constexpr uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static constexpr uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC // GIDs -static const uint8_t NCI_CORE_GID = 0x0; -static const uint8_t RF_GID = 0x1; -static const uint8_t NFCEE_GID = 0x1; -static const uint8_t NCI_PROPRIETARY_GID = 0xF; +static constexpr uint8_t NCI_CORE_GID = 0x0; +static constexpr uint8_t RF_GID = 0x1; +static constexpr uint8_t NFCEE_GID = 0x1; +static constexpr uint8_t NCI_PROPRIETARY_GID = 0xF; // OIDs -static const uint8_t NCI_CORE_RESET_OID = 0x00; -static const uint8_t NCI_CORE_INIT_OID = 0x01; -static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; -static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; -static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; -static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; -static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; -static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; -static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; +static constexpr uint8_t NCI_CORE_RESET_OID = 0x00; +static constexpr uint8_t NCI_CORE_INIT_OID = 0x01; +static constexpr uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static constexpr uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static constexpr uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static constexpr uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static constexpr uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static constexpr uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static constexpr uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; -static const uint8_t RF_DISCOVER_MAP_OID = 0x00; -static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; -static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; -static const uint8_t RF_DISCOVER_OID = 0x03; -static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; -static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; -static const uint8_t RF_DEACTIVATE_OID = 0x06; -static const uint8_t RF_FIELD_INFO_OID = 0x07; -static const uint8_t RF_T3T_POLLING_OID = 0x08; -static const uint8_t RF_NFCEE_ACTION_OID = 0x09; -static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; -static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; +static constexpr uint8_t RF_DISCOVER_MAP_OID = 0x00; +static constexpr uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static constexpr uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static constexpr uint8_t RF_DISCOVER_OID = 0x03; +static constexpr uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static constexpr uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static constexpr uint8_t RF_DEACTIVATE_OID = 0x06; +static constexpr uint8_t RF_FIELD_INFO_OID = 0x07; +static constexpr uint8_t RF_T3T_POLLING_OID = 0x08; +static constexpr uint8_t RF_NFCEE_ACTION_OID = 0x09; +static constexpr uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static constexpr uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; -static const uint8_t NFCEE_DISCOVER_OID = 0x00; -static const uint8_t NFCEE_MODE_SET_OID = 0x01; +static constexpr uint8_t NFCEE_DISCOVER_OID = 0x00; +static constexpr uint8_t NFCEE_MODE_SET_OID = 0x01; // Interfaces -static const uint8_t INTF_NFCEE_DIRECT = 0x00; -static const uint8_t INTF_FRAME = 0x01; -static const uint8_t INTF_ISODEP = 0x02; -static const uint8_t INTF_NFCDEP = 0x03; -static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +static constexpr uint8_t INTF_NFCEE_DIRECT = 0x00; +static constexpr uint8_t INTF_FRAME = 0x01; +static constexpr uint8_t INTF_ISODEP = 0x02; +static constexpr uint8_t INTF_NFCDEP = 0x03; +static constexpr uint8_t INTF_TAGCMD = 0x80; // NXP proprietary // Bit rates -static const uint8_t NFC_BIT_RATE_106 = 0x00; -static const uint8_t NFC_BIT_RATE_212 = 0x01; -static const uint8_t NFC_BIT_RATE_424 = 0x02; -static const uint8_t NFC_BIT_RATE_848 = 0x03; -static const uint8_t NFC_BIT_RATE_1695 = 0x04; -static const uint8_t NFC_BIT_RATE_3390 = 0x05; -static const uint8_t NFC_BIT_RATE_6780 = 0x06; +static constexpr uint8_t NFC_BIT_RATE_106 = 0x00; +static constexpr uint8_t NFC_BIT_RATE_212 = 0x01; +static constexpr uint8_t NFC_BIT_RATE_424 = 0x02; +static constexpr uint8_t NFC_BIT_RATE_848 = 0x03; +static constexpr uint8_t NFC_BIT_RATE_1695 = 0x04; +static constexpr uint8_t NFC_BIT_RATE_3390 = 0x05; +static constexpr uint8_t NFC_BIT_RATE_6780 = 0x06; // Protocols -static const uint8_t PROT_UNDETERMINED = 0x00; -static const uint8_t PROT_T1T = 0x01; -static const uint8_t PROT_T2T = 0x02; -static const uint8_t PROT_T3T = 0x03; -static const uint8_t PROT_ISODEP = 0x04; -static const uint8_t PROT_NFCDEP = 0x05; -static const uint8_t PROT_T5T = 0x06; -static const uint8_t PROT_MIFARE = 0x80; +static constexpr uint8_t PROT_UNDETERMINED = 0x00; +static constexpr uint8_t PROT_T1T = 0x01; +static constexpr uint8_t PROT_T2T = 0x02; +static constexpr uint8_t PROT_T3T = 0x03; +static constexpr uint8_t PROT_ISODEP = 0x04; +static constexpr uint8_t PROT_NFCDEP = 0x05; +static constexpr uint8_t PROT_T5T = 0x06; +static constexpr uint8_t PROT_MIFARE = 0x80; // RF Technologies -static const uint8_t NFC_RF_TECH_A = 0x00; -static const uint8_t NFC_RF_TECH_B = 0x01; -static const uint8_t NFC_RF_TECH_F = 0x02; -static const uint8_t NFC_RF_TECH_15693 = 0x03; +static constexpr uint8_t NFC_RF_TECH_A = 0x00; +static constexpr uint8_t NFC_RF_TECH_B = 0x01; +static constexpr uint8_t NFC_RF_TECH_F = 0x02; +static constexpr uint8_t NFC_RF_TECH_15693 = 0x03; // RF Technology & Modes -static const uint8_t MODE_MASK = 0xF0; -static const uint8_t MODE_LISTEN_MASK = 0x80; -static const uint8_t MODE_POLL = 0x00; +static constexpr uint8_t MODE_MASK = 0xF0; +static constexpr uint8_t MODE_LISTEN_MASK = 0x80; +static constexpr uint8_t MODE_POLL = 0x00; -static const uint8_t TECH_PASSIVE_NFCA = 0x00; -static const uint8_t TECH_PASSIVE_NFCB = 0x01; -static const uint8_t TECH_PASSIVE_NFCF = 0x02; -static const uint8_t TECH_ACTIVE_NFCA = 0x03; -static const uint8_t TECH_ACTIVE_NFCF = 0x05; -static const uint8_t TECH_PASSIVE_15693 = 0x06; +static constexpr uint8_t TECH_PASSIVE_NFCA = 0x00; +static constexpr uint8_t TECH_PASSIVE_NFCB = 0x01; +static constexpr uint8_t TECH_PASSIVE_NFCF = 0x02; +static constexpr uint8_t TECH_ACTIVE_NFCA = 0x03; +static constexpr uint8_t TECH_ACTIVE_NFCF = 0x05; +static constexpr uint8_t TECH_PASSIVE_15693 = 0x06; // Status codes -static const uint8_t STATUS_OK = 0x00; -static const uint8_t STATUS_REJECTED = 0x01; -static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; -static const uint8_t STATUS_FAILED = 0x03; -static const uint8_t STATUS_NOT_INITIALIZED = 0x04; -static const uint8_t STATUS_SYNTAX_ERROR = 0x05; -static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; -static const uint8_t STATUS_INVALID_PARAM = 0x09; -static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; -static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; -static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; -static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; -static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; -static const uint8_t RF_PROTOCOL_ERROR = 0xB1; -static const uint8_t RF_TIMEOUT_ERROR = 0xB2; -static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; -static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; -static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; -static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +static constexpr uint8_t STATUS_OK = 0x00; +static constexpr uint8_t STATUS_REJECTED = 0x01; +static constexpr uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static constexpr uint8_t STATUS_FAILED = 0x03; +static constexpr uint8_t STATUS_NOT_INITIALIZED = 0x04; +static constexpr uint8_t STATUS_SYNTAX_ERROR = 0x05; +static constexpr uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static constexpr uint8_t STATUS_INVALID_PARAM = 0x09; +static constexpr uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static constexpr uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static constexpr uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static constexpr uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static constexpr uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static constexpr uint8_t RF_PROTOCOL_ERROR = 0xB1; +static constexpr uint8_t RF_TIMEOUT_ERROR = 0xB2; +static constexpr uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static constexpr uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static constexpr uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static constexpr uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; // Deactivation types/reasons -static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; -static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; -static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; -static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +static constexpr uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static constexpr uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static constexpr uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; // RF discover map modes -static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; -static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static constexpr uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; // RF discover notification types -static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; -static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; -static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static constexpr uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static constexpr uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; // Important message offsets -static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; -static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index 2e81fb216c6..48f79b88548 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -12,7 +12,7 @@ namespace esphome { namespace nfc { -static const uint8_t MAX_NDEF_RECORDS = 4; +static constexpr uint8_t MAX_NDEF_RECORDS = 4; class NdefMessage { public: diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 76d8b6a50a2..1a7c24aee9c 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -8,14 +8,14 @@ namespace esphome { namespace nfc { -static const uint8_t TNF_EMPTY = 0x00; -static const uint8_t TNF_WELL_KNOWN = 0x01; -static const uint8_t TNF_MIME_MEDIA = 0x02; -static const uint8_t TNF_ABSOLUTE_URI = 0x03; -static const uint8_t TNF_EXTERNAL_TYPE = 0x04; -static const uint8_t TNF_UNKNOWN = 0x05; -static const uint8_t TNF_UNCHANGED = 0x06; -static const uint8_t TNF_RESERVED = 0x07; +static constexpr uint8_t TNF_EMPTY = 0x00; +static constexpr uint8_t TNF_WELL_KNOWN = 0x01; +static constexpr uint8_t TNF_MIME_MEDIA = 0x02; +static constexpr uint8_t TNF_ABSOLUTE_URI = 0x03; +static constexpr uint8_t TNF_EXTERNAL_TYPE = 0x04; +static constexpr uint8_t TNF_UNKNOWN = 0x05; +static constexpr uint8_t TNF_UNCHANGED = 0x06; +static constexpr uint8_t TNF_RESERVED = 0x07; class NdefRecord { public: diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h index 1eadda1b4fe..2f7790a9a9a 100644 --- a/esphome/components/nfc/ndef_record_uri.h +++ b/esphome/components/nfc/ndef_record_uri.h @@ -9,7 +9,7 @@ namespace esphome { namespace nfc { -static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static constexpr uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; static const char *const PAYLOAD_IDENTIFIERS[] = {"", "http://www.", "https://www.", diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 5191904833b..cdaea82af6d 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -12,47 +12,47 @@ namespace esphome { namespace nfc { -static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; -static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; -static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; -static const uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; -static const uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; +static constexpr uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static constexpr uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static constexpr uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW = 4; +static constexpr uint8_t MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH = 16; +static constexpr uint8_t MIFARE_CLASSIC_16BLOCK_SECT_START = 32; -static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; -static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; -static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; +static constexpr uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static constexpr uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; -static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; -static const uint8_t TAG_TYPE_1 = 1; -static const uint8_t TAG_TYPE_2 = 2; -static const uint8_t TAG_TYPE_3 = 3; -static const uint8_t TAG_TYPE_4 = 4; -static const uint8_t TAG_TYPE_UNKNOWN = 99; +static constexpr uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static constexpr uint8_t TAG_TYPE_1 = 1; +static constexpr uint8_t TAG_TYPE_2 = 2; +static constexpr uint8_t TAG_TYPE_3 = 3; +static constexpr uint8_t TAG_TYPE_4 = 4; +static constexpr uint8_t TAG_TYPE_UNKNOWN = 99; // Mifare Commands -static const uint8_t MIFARE_CMD_AUTH_A = 0x60; -static const uint8_t MIFARE_CMD_AUTH_B = 0x61; -static const uint8_t MIFARE_CMD_HALT = 0x50; -static const uint8_t MIFARE_CMD_READ = 0x30; -static const uint8_t MIFARE_CMD_WRITE = 0xA0; -static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; +static constexpr uint8_t MIFARE_CMD_AUTH_A = 0x60; +static constexpr uint8_t MIFARE_CMD_AUTH_B = 0x61; +static constexpr uint8_t MIFARE_CMD_HALT = 0x50; +static constexpr uint8_t MIFARE_CMD_READ = 0x30; +static constexpr uint8_t MIFARE_CMD_WRITE = 0xA0; +static constexpr uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; // Mifare Ack/Nak -static const uint8_t MIFARE_CMD_ACK = 0x0A; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; -static const uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; -static const uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; +static constexpr uint8_t MIFARE_CMD_ACK = 0x0A; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_VALID = 0x00; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_VALID = 0x01; +static constexpr uint8_t MIFARE_CMD_NAK_INVALID_XFER_BUFF_INVALID = 0x04; +static constexpr uint8_t MIFARE_CMD_NAK_CRC_ERROR_XFER_BUFF_INVALID = 0x05; static const char *const MIFARE_CLASSIC = "Mifare Classic"; static const char *const NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; static const char *const ERROR = "Error"; -static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; -static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; +static constexpr uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static constexpr uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static constexpr uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; /// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; From 2f9b76f129c2b278b0c45ba5c66430d638631ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:33:39 -0600 Subject: [PATCH 0046/2056] [pn7160] Use constexpr for compile-time constants (#14078) --- esphome/components/pn7150/pn7150.h | 111 +++++++++---------- esphome/components/pn7160/pn7160.h | 121 +++++++++++---------- esphome/components/pn7160_spi/pn7160_spi.h | 4 +- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 5feba17d211..c5dd2838325 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7150 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 3, // length @@ -64,7 +64,7 @@ static const uint8_t PMU_CFG[] = { 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -76,28 +76,29 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 1, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x01, // power state - nfc::PROT_ISODEP}; // protocol +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 9f2d10c2d53..77ab49399c5 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -14,48 +14,48 @@ namespace esphome { namespace pn7160 { -static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; -static const uint16_t NFCC_INIT_TIMEOUT = 50; -static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; +static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; +static constexpr uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; -static const uint8_t NFCC_MAX_COMM_FAILS = 3; -static const uint8_t NFCC_MAX_ERROR_COUNT = 10; +static constexpr uint8_t NFCC_MAX_COMM_FAILS = 3; +static constexpr uint8_t NFCC_MAX_ERROR_COUNT = 10; -static const uint8_t XCHG_DATA_OID = 0x10; -static const uint8_t MF_SECTORSEL_OID = 0x32; -static const uint8_t MFC_AUTHENTICATE_OID = 0x40; -static const uint8_t TEST_PRBS_OID = 0x30; -static const uint8_t TEST_ANTENNA_OID = 0x3D; -static const uint8_t TEST_GET_REGISTER_OID = 0x33; +static constexpr uint8_t XCHG_DATA_OID = 0x10; +static constexpr uint8_t MF_SECTORSEL_OID = 0x32; +static constexpr uint8_t MFC_AUTHENTICATE_OID = 0x40; +static constexpr uint8_t TEST_PRBS_OID = 0x30; +static constexpr uint8_t TEST_ANTENNA_OID = 0x3D; +static constexpr uint8_t TEST_GET_REGISTER_OID = 0x33; -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A -static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B -static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static constexpr uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; -static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, - 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; -static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, - 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; -static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; -static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; -static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; -static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; -static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; -static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; +static constexpr uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static constexpr uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static constexpr uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static constexpr uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static constexpr uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static constexpr uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; -static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0x01, // TOTAL_DURATION (low)... - 0x00}; // TOTAL_DURATION (high): 1 ms +static constexpr uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms -static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields - 0x00, // config param identifier (TOTAL_DURATION) - 0x02, // length of value - 0xF8, // TOTAL_DURATION (low)... - 0x02}; // TOTAL_DURATION (high): 760 ms +static constexpr uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms -static const uint8_t PMU_CFG[] = { +static constexpr uint8_t PMU_CFG[] = { 0x01, // Number of parameters 0xA0, 0x0E, // ext. tag 11, // length @@ -74,7 +74,7 @@ static const uint8_t PMU_CFG[] = { 0x0C, // RFU }; -static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes +static constexpr uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_FRAME, // poll mode nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, @@ -86,33 +86,34 @@ static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, nfc::INTF_TAGCMD}; // poll mode -static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = { + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode +static constexpr uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode -static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode - nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode - nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode +static constexpr uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode -static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) - 2, // number of table entries - 0x01, // type = protocol-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::PROT_ISODEP, // protocol - 0x00, // type = technology-based - 3, // length - 0, // DH NFCEE ID, a static ID representing the DH-NFCEE - 0x07, // power state - nfc::TECH_PASSIVE_NFCA}; // technology +static constexpr uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology enum class CardEmulationState : uint8_t { CARD_EMU_IDLE, diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h index 7d4460a76de..9b6e21fa2a8 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.h +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -10,8 +10,8 @@ namespace esphome { namespace pn7160_spi { -static const uint8_t TDD_SPI_READ = 0xFF; -static const uint8_t TDD_SPI_WRITE = 0x0A; +static constexpr uint8_t TDD_SPI_READ = 0xFF; +static constexpr uint8_t TDD_SPI_WRITE = 0x0A; class PN7160Spi : public pn7160::PN7160, public spi::SPIDevice Date: Wed, 18 Feb 2026 21:34:25 -0600 Subject: [PATCH 0047/2056] [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 62a035e79ff..85461755aac 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -23,9 +23,9 @@ namespace esphome::bluetooth_proxy { -static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; -static const int DONE_SENDING_SERVICES = -2; -static const int INIT_SENDING_SERVICES = -3; +static constexpr esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; From 3c227eeca46d5ca6e99e5e59a9585d0f168642fb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:50:39 -0600 Subject: [PATCH 0048/2056] [audio] Add support for sinking via an arbitrary callback (#14035) --- esphome/components/audio/audio_transfer_buffer.cpp | 2 ++ esphome/components/audio/audio_transfer_buffer.h | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index ddb669e0eb9..a8be55d62fe 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -165,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (this->ring_buffer_.use_count() > 0) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); + } else if (this->sink_callback_ != nullptr) { + bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait); } this->decrease_buffer_length(bytes_written); diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 24c0670d1a0..22c22cc9ae8 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -15,6 +15,12 @@ namespace esphome { namespace audio { +/// @brief Abstract interface for writing decoded audio data to a sink. +class AudioSinkCallback { + public: + virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0; +}; + class AudioTransferBuffer { /* * @brief Class that facilitates tranferring data between a buffer and an audio source or sink. @@ -108,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + /// @brief Adds a callback as the transfer buffer's sink. + /// @param callback Pointer to the AudioSinkCallback implementation + void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; } + void clear_buffered_data() override; bool has_buffered_data() const override; @@ -116,6 +126,7 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif + AudioSinkCallback *sink_callback_{nullptr}; }; class AudioSourceTransferBuffer : public AudioTransferBuffer { From 264c8faedd0ab956b0437ec2ecc0bacae1708923 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:01 -0600 Subject: [PATCH 0049/2056] [media_player] Add more commands to support Sendspin (#12258) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/media_player/__init__.py | 250 +++++++----------- esphome/components/media_player/automation.h | 27 ++ .../components/media_player/media_player.cpp | 52 ++++ .../components/media_player/media_player.h | 46 ++-- tests/components/media_player/common.yaml | 25 ++ 5 files changed, 232 insertions(+), 168 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index c6ffe50d790..b2afbe5e587 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -35,86 +35,73 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } - -PlayAction = media_player_ns.class_( - "PlayAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -ToggleAction = media_player_ns.class_( - "ToggleAction", automation.Action, cg.Parented.template(MediaPlayer) -) -PauseAction = media_player_ns.class_( - "PauseAction", automation.Action, cg.Parented.template(MediaPlayer) -) -StopAction = media_player_ns.class_( - "StopAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeUpAction = media_player_ns.class_( - "VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeDownAction = media_player_ns.class_( - "VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOnAction = media_player_ns.class_( - "TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer) -) -TurnOffAction = media_player_ns.class_( - "TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer) -) - +# Local config key constants CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" -StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template()) -IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template()) -PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template()) -PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template()) -AnnoucementTrigger = media_player_ns.class_( - "AnnouncementTrigger", automation.Trigger.template() +# Command actions that all share the same schema and codegen handler +_COMMAND_ACTIONS = [ + "play", + "pause", + "stop", + "toggle", + "volume_up", + "volume_down", + "turn_on", + "turn_off", + "next", + "previous", + "mute", + "unmute", + "repeat_off", + "repeat_one", + "repeat_all", + "shuffle", + "unshuffle", + "group_join", + "clear_playlist", +] + +# State triggers: (config_key, C++ class name) +_STATE_TRIGGERS = [ + (CONF_ON_STATE, "StateTrigger"), + (CONF_ON_IDLE, "IdleTrigger"), + (CONF_ON_PLAY, "PlayTrigger"), + (CONF_ON_PAUSE, "PauseTrigger"), + (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), + (CONF_ON_TURN_ON, "OnTrigger"), + (CONF_ON_TURN_OFF, "OffTrigger"), +] + +# State conditions that all share the same schema and codegen handler +_STATE_CONDITIONS = [ + "idle", + "paused", + "playing", + "announcing", + "on", + "off", + "muted", +] + +# Special action classes with custom schemas/handlers +PlayMediaAction = media_player_ns.class_( + "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) -OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template()) -OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template()) -IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) -IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) -IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) -IsAnnouncingCondition = media_player_ns.class_( - "IsAnnouncingCondition", automation.Condition +VolumeSetAction = media_player_ns.class_( + "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) -IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition) -IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition) async def setup_media_player_core_(var, config): await setup_entity(var, config, "media_player") - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_IDLE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PLAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PAUSE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ANNOUNCEMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_ON, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_OFF, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, _ in _STATE_TRIGGERS: + for conf in config.get(conf_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -133,41 +120,14 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(CONF_ON_STATE): automation.validate_automation( + cv.Optional(conf_key): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + media_player_ns.class_(class_name, automation.Trigger.template()) + ), } - ), - cv.Optional(CONF_ON_IDLE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), - } - ), - cv.Optional(CONF_ON_PLAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger), - } - ), - cv.Optional(CONF_ON_PAUSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), - } - ), - cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger), - } - ), + ) + for conf_key, class_name in _STATE_TRIGGERS } ) @@ -228,56 +188,48 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var -@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA) -@automation.register_action( - "media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA -) -@automation.register_action( - "media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA -) -async def media_player_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_announcement(announcement)) - return var +def _snake_to_camel(name): + return "".join(word.capitalize() for word in name.split("_")) -@automation.register_condition( - "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -@automation.register_condition( - "media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA -) -async def media_player_condition(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var +def _register_command_actions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) + cg.add(var.set_announcement(announcement)) + return var + + for action_name in _COMMAND_ACTIONS: + class_name = f"{_snake_to_camel(action_name)}Action" + action_class = media_player_ns.class_( + class_name, automation.Action, cg.Parented.template(MediaPlayer) + ) + automation.register_action( + f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + )(handler) + + +_register_command_actions() + + +def _register_state_conditions(): + async def handler(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + for condition_name in _STATE_CONDITIONS: + class_name = f"Is{_snake_to_camel(condition_name)}Condition" + condition_class = media_player_ns.class_(class_name, automation.Condition) + automation.register_condition( + f"media_player.is_{condition_name}", + condition_class, + MEDIA_PLAYER_CONDITION_SCHEMA, + )(handler) + + +_register_state_conditions() @automation.register_action( diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 50e7693cb51..90e7bf75b5c 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -32,6 +32,28 @@ template using TurnOnAction = MediaPlayerCommandAction; template using TurnOffAction = MediaPlayerCommandAction; +template +using NextAction = MediaPlayerCommandAction; +template +using PreviousAction = MediaPlayerCommandAction; +template +using MuteAction = MediaPlayerCommandAction; +template +using UnmuteAction = MediaPlayerCommandAction; +template +using RepeatOffAction = MediaPlayerCommandAction; +template +using RepeatOneAction = MediaPlayerCommandAction; +template +using RepeatAllAction = MediaPlayerCommandAction; +template +using ShuffleAction = MediaPlayerCommandAction; +template +using UnshuffleAction = MediaPlayerCommandAction; +template +using GroupJoinAction = MediaPlayerCommandAction; +template +using ClearPlaylistAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -105,5 +127,10 @@ template class IsOffCondition : public Condition, public bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; +template class IsMutedCondition : public Condition, public Parented { + public: + bool check(const Ts &...x) override { return this->parent_->is_muted(); } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 17d9b054dad..a53d598b0fe 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -60,11 +60,39 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "TURN_ON"; case MEDIA_PLAYER_COMMAND_TURN_OFF: return "TURN_OFF"; + case MEDIA_PLAYER_COMMAND_NEXT: + return "NEXT"; + case MEDIA_PLAYER_COMMAND_PREVIOUS: + return "PREVIOUS"; + case MEDIA_PLAYER_COMMAND_REPEAT_ALL: + return "REPEAT_ALL"; + case MEDIA_PLAYER_COMMAND_SHUFFLE: + return "SHUFFLE"; + case MEDIA_PLAYER_COMMAND_UNSHUFFLE: + return "UNSHUFFLE"; + case MEDIA_PLAYER_COMMAND_GROUP_JOIN: + return "GROUP_JOIN"; default: return "UNKNOWN"; } } +void MediaPlayerTraits::set_supports_pause(bool supports_pause) { + if (supports_pause) { + this->feature_flags_ |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY); + } +} + +void MediaPlayerTraits::set_supports_turn_off_on(bool supports_turn_off_on) { + if (supports_turn_off_on) { + this->feature_flags_ |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; + } else { + this->feature_flags_ &= ~(MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON); + } +} + void MediaPlayerCall::validate_() { if (this->media_url_.has_value()) { if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) { @@ -125,6 +153,30 @@ MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_UP")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_UP); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_DOWN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_DOWN); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("ENQUEUE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_ENQUEUE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ONE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ONE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_OFF")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_OFF); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ALL")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ALL); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLEAR_PLAYLIST")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("NEXT")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_NEXT); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PREVIOUS")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_PREVIOUS); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("SHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_SHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNSHUFFLE")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_UNSHUFFLE); + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("GROUP_JOIN")) == 0) { + this->set_command(MEDIA_PLAYER_COMMAND_GROUP_JOIN); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index f75a68dd853..35097477181 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -58,6 +58,12 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, MEDIA_PLAYER_COMMAND_TURN_ON = 12, MEDIA_PLAYER_COMMAND_TURN_OFF = 13, + MEDIA_PLAYER_COMMAND_NEXT = 14, + MEDIA_PLAYER_COMMAND_PREVIOUS = 15, + MEDIA_PLAYER_COMMAND_REPEAT_ALL = 16, + MEDIA_PLAYER_COMMAND_SHUFFLE = 17, + MEDIA_PLAYER_COMMAND_UNSHUFFLE = 18, + MEDIA_PLAYER_COMMAND_GROUP_JOIN = 19, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -74,38 +80,40 @@ struct MediaPlayerSupportedFormat { uint32_t sample_bytes; }; +// Base features always reported for all media players +static constexpr uint32_t BASE_MEDIA_PLAYER_FEATURES = + MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | MediaPlayerEntityFeature::STOP | + MediaPlayerEntityFeature::VOLUME_SET | MediaPlayerEntityFeature::VOLUME_MUTE | + MediaPlayerEntityFeature::MEDIA_ANNOUNCE; + class MediaPlayer; class MediaPlayerTraits { public: MediaPlayerTraits() = default; - void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - bool get_supports_pause() const { return this->supports_pause_; } - - void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; } - bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; } + uint32_t get_feature_flags() const { return this->feature_flags_; } + void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; } + void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; } + // Returns true only if all specified flags are set + bool has_feature_flags(uint32_t feature_flags) const { + return (this->feature_flags_ & feature_flags) == feature_flags; + } std::vector &get_supported_formats() { return this->supported_formats_; } - uint32_t get_feature_flags() const { - uint32_t flags = 0; - flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | - MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET | - MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE; - if (this->get_supports_pause()) { - flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; - } - if (this->get_supports_turn_off_on()) { - flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; - } - return flags; + // Legacy setters/getters are kept for backward compatibility + void set_supports_pause(bool supports_pause); + bool get_supports_pause() const { return this->has_feature_flags(MediaPlayerEntityFeature::PAUSE); } + + void set_supports_turn_off_on(bool supports_turn_off_on); + bool get_supports_turn_off_on() const { + return this->has_feature_flags(MediaPlayerEntityFeature::TURN_ON | MediaPlayerEntityFeature::TURN_OFF); } protected: std::vector supported_formats_{}; - bool supports_pause_{false}; - bool supports_turn_off_on_{false}; + uint32_t feature_flags_{BASE_MEDIA_PLAYER_FEATURES}; }; class MediaPlayerCall { diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 763bc231c0d..c83ee89ad43 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -23,8 +23,27 @@ media_player: - media_player.stop: - media_player.stop: announcement: true + on_announcement: + - media_player.play: + on_turn_on: + - media_player.play: + on_turn_off: + - media_player.stop: on_pause: - media_player.toggle: + - media_player.turn_on: + - media_player.turn_off: + - media_player.next: + - media_player.previous: + - media_player.mute: + - media_player.unmute: + - media_player.repeat_off: + - media_player.repeat_one: + - media_player.repeat_all: + - media_player.shuffle: + - media_player.unshuffle: + - media_player.group_join: + - media_player.clear_playlist: - wait_until: media_player.is_idle: - wait_until: @@ -33,6 +52,12 @@ media_player: media_player.is_announcing: - wait_until: media_player.is_paused: + - wait_until: + media_player.is_on: + - wait_until: + media_player.is_off: + - wait_until: + media_player.is_muted: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% From ba7134ee3f870ce00d2990d84b142b734028330f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 18 Feb 2026 21:51:16 -0600 Subject: [PATCH 0050/2056] [mdns] add Sendspin advertisement support (#14013) Co-authored-by: J. Nick Koston --- esphome/components/mdns/__init__.py | 2 +- esphome/components/mdns/mdns_component.cpp | 19 ++++++++++++++++++- esphome/core/defines.h | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3088d8ad7e9..f87f9296157 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -21,7 +21,7 @@ DEPENDENCIES = ["network"] # Components that create mDNS services at runtime # IMPORTANT: If you add a new component here, you must also update the corresponding # #ifdef blocks in mdns_component.cpp compile_records_() method -COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server") +COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "sendspin", "web_server") mdns_ns = cg.esphome_ns.namespace("mdns") MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 47db92610ab..5e5e1279d95 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -29,6 +29,10 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +#ifndef USE_SENDSPIN_PORT +#define USE_SENDSPIN_PORT 8928 // NOLINT +#endif + // Define all constant strings using the macro MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); @@ -150,6 +154,18 @@ void MDNSComponent::compile_records_(StaticVector Date: Wed, 18 Feb 2026 21:51:33 -0600 Subject: [PATCH 0051/2056] [audio, speaker] Add support for decoding Ogg Opus files (#13967) --- esphome/components/audio/__init__.py | 42 ++++++++++ esphome/components/audio/audio.cpp | 4 + esphome/components/audio/audio.h | 3 + esphome/components/audio/audio_decoder.cpp | 60 +++++++++++++- esphome/components/audio/audio_decoder.h | 15 +++- esphome/components/audio/audio_reader.cpp | 13 +++ .../speaker/media_player/__init__.py | 82 ++++++++++++++++--- .../speaker/media_player/audio_pipeline.cpp | 10 +++ esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + .../speaker/audio_dac.esp32-ard.yaml | 10 --- .../speaker/common-media_player.yaml | 7 ++ .../speaker/media_player.esp32-s3-idf.yaml | 9 -- ...idf.yaml => test-audio_dac.esp32-idf.yaml} | 0 ....yaml => test-media_player.esp32-idf.yaml} | 0 tests/components/speaker/test.esp32-ard.yaml | 10 --- 16 files changed, 222 insertions(+), 46 deletions(-) delete mode 100644 tests/components/speaker/audio_dac.esp32-ard.yaml delete mode 100644 tests/components/speaker/media_player.esp32-s3-idf.yaml rename tests/components/speaker/{audio_dac.esp32-idf.yaml => test-audio_dac.esp32-idf.yaml} (100%) rename tests/components/speaker/{media_player.esp32-idf.yaml => test-media_player.esp32-idf.yaml} (100%) delete mode 100644 tests/components/speaker/test.esp32-ard.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index f48b776dddb..d8d426ec63a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,10 +1,14 @@ +from dataclasses import dataclass + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@kahrendt"] +DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") AudioFile = audio_ns.struct("AudioFile") @@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = { "WAV": AudioFileType.WAV, "MP3": AudioFileType.MP3, "FLAC": AudioFileType.FLAC, + "OPUS": AudioFileType.OPUS, } +@dataclass +class AudioData: + flac_support: bool = False + mp3_support: bool = False + opus_support: bool = False + + +def _get_data() -> AudioData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioData() + return CORE.data[DOMAIN] + + +def request_flac_support() -> None: + """Request FLAC codec support for audio decoding.""" + _get_data().flac_support = True + + +def request_mp3_support() -> None: + """Request MP3 codec support for audio decoding.""" + _get_data().mp3_support = True + + +def request_opus_support() -> None: + """Request Opus codec support for audio decoding.""" + _get_data().opus_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -173,3 +206,12 @@ async def to_code(config): name="esphome/esp-audio-libs", ref="2.0.3", ) + + data = _get_data() + if data.flac_support: + cg.add_define("USE_AUDIO_FLAC_SUPPORT") + if data.mp3_support: + cg.add_define("USE_AUDIO_MP3_SUPPORT") + if data.opus_support: + cg.add_define("USE_AUDIO_OPUS_SUPPORT") + add_idf_component(name="esphome/micro-opus", ref="0.3.3") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 9cc9b7d0da0..40592f6107a 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -46,6 +46,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: return "MP3"; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + return "OPUS"; #endif case AudioFileType::WAV: return "WAV"; diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index e01d7eb101e..7d7db9e9444 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t { #endif #ifdef USE_AUDIO_MP3_SUPPORT MP3, +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + OPUS, #endif WAV, }; diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 8f514468c48..ee6d7d0a153 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -3,10 +3,13 @@ #ifdef USE_ESP32 #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace audio { +static const char *const TAG = "audio.decoder"; + static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data @@ -79,6 +82,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { // Always reallocate the output transfer buffer to the smallest necessary size this->output_transfer_buffer_->reallocate(this->free_buffer_required_); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + this->opus_decoder_ = make_unique(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; + break; #endif case AudioFileType::WAV: this->wav_decoder_ = make_unique(); @@ -158,8 +169,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio // Only shift data on the first loop iteration to avoid unnecessary, slow moves - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration); + // If the decoder buffers internally, then never shift + size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( + pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. @@ -195,6 +207,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { case AudioFileType::MP3: state = this->decode_mp3_(); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + state = this->decode_opus_(); + break; #endif case AudioFileType::WAV: state = this->decode_wav_(); @@ -339,6 +356,45 @@ FileDecoderState AudioDecoder::decode_mp3_() { } #endif +#ifdef USE_AUDIO_OPUS_SUPPORT +FileDecoderState AudioDecoder::decode_opus_() { + bool processed_header = this->opus_decoder_->is_initialized(); + + size_t bytes_consumed, samples_decoded; + + micro_opus::OggOpusResult result = this->opus_decoder_->decode( + this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), + this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, + samples_decoded); + + if (result == micro_opus::OGG_OPUS_OK) { + if (!processed_header && this->opus_decoder_->is_initialized()) { + // Header processed and stream info is available + this->audio_stream_info_ = + audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(), + this->opus_decoder_->get_sample_rate()); + } + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + // Some audio was processed + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the packet on the next call + this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + // Couldn't reallocate output buffer + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result); + return FileDecoderState::POTENTIALLY_FAILED; + } + return FileDecoderState::MORE_TO_PROCESS; +} +#endif + FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 2ca1d623fe1..cad16110aea 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -24,6 +24,11 @@ #endif #include +// micro-opus +#ifdef USE_AUDIO_OPUS_SUPPORT +#include +#endif + namespace esphome { namespace audio { @@ -47,7 +52,7 @@ class AudioDecoder { * @brief Class that facilitates decoding an audio file. * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker * component). - * Supports wav, flac, and mp3 formats. + * Supports wav, flac, mp3, and ogg opus formats. */ public: /// @brief Allocates the input and output transfer buffers @@ -55,7 +60,7 @@ class AudioDecoder { /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) + /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) ~AudioDecoder(); /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. @@ -108,6 +113,10 @@ class AudioDecoder { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + FileDecoderState decode_opus_(); + std::unique_ptr opus_decoder_; #endif FileDecoderState decode_wav_(); @@ -124,6 +133,8 @@ class AudioDecoder { bool end_of_file_{false}; bool wav_has_known_end_{false}; + bool decoder_buffers_internally_{false}; + bool pause_output_{false}; uint32_t accumulated_frames_written_{0}; diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4e4bd31f9bb..78d69d7a39d 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -197,6 +197,11 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { else if (str_endswith_ignore_case(url, ".flac")) { file_type = AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + else if (str_endswith_ignore_case(url, ".opus")) { + file_type = AudioFileType::OPUS; + } #endif else { file_type = AudioFileType::NONE; @@ -241,6 +246,14 @@ AudioFileType AudioReader::get_audio_type(const char *content_type) { if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { return AudioFileType::FLAC; } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } #endif return AudioFileType::NONE; } diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 034312236c6..b302bd9b23c 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -26,7 +26,6 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -37,6 +36,10 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" +CODEC_SUPPORT_ALL = "all" +CODEC_SUPPORT_NEEDED = "needed" +CODEC_SUPPORT_NONE = "none" + TYPE_LOCAL = "local" TYPE_WEB = "web" @@ -110,6 +113,8 @@ def _get_supported_format_struct(pipeline, type): args.append(("format", "flac")) elif pipeline[CONF_FORMAT] == "MP3": args.append(("format", "mp3")) + elif pipeline[CONF_FORMAT] == "OPUS": + args.append(("format", "opus")) elif pipeline[CONF_FORMAT] == "WAV": args.append(("format", "wav")) @@ -173,6 +178,13 @@ def _read_audio_file_and_type(file_config): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] elif file_type in ("flac"): media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type in ("ogg") + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] return data, media_file_type @@ -199,6 +211,10 @@ def _validate_pipeline(config): inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + # Validate the transcoder settings is compatible with the speaker audio.final_validate_audio_schema( "speaker media_player", @@ -225,12 +241,27 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Default to using codec if psram is enabled - if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: - use_codec = psram.DOMAIN in full_config.get() - conf_id = config[CONF_ID].id - core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) - core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + # Normalize boolean values to string equivalents + codec_mode = config[CONF_CODEC_SUPPORT_ENABLED] + if codec_mode is True: + codec_mode = CODEC_SUPPORT_ALL + elif codec_mode is False: + codec_mode = CODEC_SUPPORT_NONE + + use_codec = codec_mode != CODEC_SUPPORT_NONE + + # In "needed" mode, collect formats from pipelines and files + needed_formats = set() + need_all = False + if codec_mode == CODEC_SUPPORT_NEEDED: + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + # No preferred format means any format could arrive + need_all = True + else: + needed_formats.add(fmt) for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) @@ -243,6 +274,26 @@ def _final_validate(config): raise cv.Invalid( f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" ) + # In "needed" mode, add file format to needed codecs + if codec_mode == CODEC_SUPPORT_NEEDED: + for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): + if str(media_file_type) == str(fmt_enum): + if fmt_name not in ("WAV", "NONE"): + needed_formats.add(fmt_name) + break + + # Request codec support + if codec_mode == CODEC_SUPPORT_ALL or need_all: + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif codec_mode == CODEC_SUPPORT_NEEDED: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -307,7 +358,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, + cv.Optional( + CONF_CODEC_SUPPORT_ENABLED, default=CODEC_SUPPORT_NEEDED + ): cv.Any( + cv.boolean, + cv.one_of( + CODEC_SUPPORT_ALL, + CODEC_SUPPORT_NEEDED, + CODEC_SUPPORT_NONE, + lower=True, + ), + ), cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) @@ -340,11 +401,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs - cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) - cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8be37d740ab..177743feb1d 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -13,7 +13,12 @@ namespace speaker { static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024; +// Opus decoding uses more stack than other codecs +#ifdef USE_AUDIO_OPUS_SUPPORT +static const uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif static const uint32_t INFO_ERROR_QUEUE_COUNT = 5; @@ -552,6 +557,11 @@ void AudioPipeline::decode_task(void *params) { case audio::AudioFileType::FLAC: initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2 break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case audio::AudioFileType::OPUS: + initial_bytes_to_buffer /= 8; // Estimate the Opus compression factor is 8 + break; #endif default: break; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a70c6cd0d0e..5559ec88d00 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -130,6 +130,7 @@ #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT +#define USE_AUDIO_OPUS_SUPPORT #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f39ea9b3ae4..83b2d9d95c8 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.3 + esphome/micro-opus: + version: 0.3.3 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/speaker/audio_dac.esp32-ard.yaml b/tests/components/speaker/audio_dac.esp32-ard.yaml deleted file mode 100644 index 3f5d1bba7c1..00000000000 --- a/tests/components/speaker/audio_dac.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO23 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index edc9f670fc5..c958c0d9120 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -1,5 +1,11 @@ <<: !include common.yaml +wifi: + ap: + +psram: + mode: quad + media_player: - platform: speaker id: speaker_media_player_id @@ -10,3 +16,4 @@ media_player: volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true + codec_support_enabled: all diff --git a/tests/components/speaker/media_player.esp32-s3-idf.yaml b/tests/components/speaker/media_player.esp32-s3-idf.yaml deleted file mode 100644 index b3eec04d238..00000000000 --- a/tests/components/speaker/media_player.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO2 - sda_pin: GPIO3 - i2s_bclk_pin: GPIO4 - i2s_lrclk_pin: GPIO5 - i2s_mclk_pin: GPIO6 - i2s_dout_pin: GPIO7 - -<<: !include common-media_player.yaml diff --git a/tests/components/speaker/audio_dac.esp32-idf.yaml b/tests/components/speaker/test-audio_dac.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/audio_dac.esp32-idf.yaml rename to tests/components/speaker/test-audio_dac.esp32-idf.yaml diff --git a/tests/components/speaker/media_player.esp32-idf.yaml b/tests/components/speaker/test-media_player.esp32-idf.yaml similarity index 100% rename from tests/components/speaker/media_player.esp32-idf.yaml rename to tests/components/speaker/test-media_player.esp32-idf.yaml diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml deleted file mode 100644 index 13350cd0973..00000000000 --- a/tests/components/speaker/test.esp32-ard.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO27 - i2s_lrclk_pin: GPIO26 - i2s_mclk_pin: GPIO25 - i2s_dout_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml - -<<: !include common.yaml From 4d05e4d5765c037f234ddebb8442af2007c4fc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Thu, 19 Feb 2026 04:52:38 +0100 Subject: [PATCH 0052/2056] [esp32_camera] Add support for sensors without JPEG support (#9496) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 50 ++++- .../components/esp32_camera/esp32_camera.cpp | 207 +++++++++++++----- .../components/esp32_camera/esp32_camera.h | 14 ++ esphome/core/defines.h | 1 + .../common/i2c_camera/esp32-idf.yaml | 1 + 5 files changed, 212 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index db6244fb3f7..3a5d87792b6 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -22,8 +22,10 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) +from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,6 +86,18 @@ FRAME_SIZES = { "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } +ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat") +PIXEL_FORMATS = { + "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565, + "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422, + "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420, + "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE, + "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG, + "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888, + "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW, + "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444, + "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555, +} ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, @@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock" CONF_I2C_PINS = "i2c_pins" CONF_POWER_DOWN_PIN = "power_down_pin" # image +CONF_PIXEL_FORMAT = "pixel_format" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" @@ -171,6 +186,21 @@ def validate_fb_location_(value): return validator(value) +def validate_jpeg_quality(config: ConfigType) -> ConfigType: + quality = config.get(CONF_JPEG_QUALITY) + pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG") + + if quality == 0: + # Set default JPEG quality if not specified for backwards compatibility + if pixel_format == "JPEG": + config[CONF_JPEG_QUALITY] = 10 + # For pixel formats other than JPEG, the valid 0 means no conversion + elif quality < 6 or quality > 63: + raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}") + + return config + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum( + PIXEL_FORMATS, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any( + cv.one_of(0), cv.int_range(min=6, max=63) + ), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + validate_jpeg_quality, cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) def _final_validate(config): + # Check psram requirement for non-JPEG formats + if ( + config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG" + and psram_domain not in CORE.loaded_integrations + ): + raise cv.Invalid( + f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion" + ) + if CONF_I2C_PINS not in config: return fconf = fv.full_config.get() @@ -298,6 +343,7 @@ SETTERS = { CONF_RESET_PIN: "set_reset_pin", CONF_POWER_DOWN_PIN: "set_power_down_pin", # image + CONF_PIXEL_FORMAT: "set_pixel_format", CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", @@ -351,6 +397,8 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") + if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": + cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index cfe06b16732..655ae54f0a6 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif +static const char *frame_size_to_str(framesize_t size) { + switch (size) { + case FRAMESIZE_QQVGA: + return "160x120 (QQVGA)"; + case FRAMESIZE_QCIF: + return "176x155 (QCIF)"; + case FRAMESIZE_HQVGA: + return "240x176 (HQVGA)"; + case FRAMESIZE_QVGA: + return "320x240 (QVGA)"; + case FRAMESIZE_CIF: + return "400x296 (CIF)"; + case FRAMESIZE_VGA: + return "640x480 (VGA)"; + case FRAMESIZE_SVGA: + return "800x600 (SVGA)"; + case FRAMESIZE_XGA: + return "1024x768 (XGA)"; + case FRAMESIZE_SXGA: + return "1280x1024 (SXGA)"; + case FRAMESIZE_UXGA: + return "1600x1200 (UXGA)"; + case FRAMESIZE_FHD: + return "1920x1080 (FHD)"; + case FRAMESIZE_P_HD: + return "720x1280 (P_HD)"; + case FRAMESIZE_P_3MP: + return "864x1536 (P_3MP)"; + case FRAMESIZE_QXGA: + return "2048x1536 (QXGA)"; + case FRAMESIZE_QHD: + return "2560x1440 (QHD)"; + case FRAMESIZE_WQXGA: + return "2560x1600 (WQXGA)"; + case FRAMESIZE_P_FHD: + return "1080x1920 (P_FHD)"; + case FRAMESIZE_QSXGA: + return "2560x1920 (QSXGA)"; + default: + return "UNKNOWN"; + } +} + +static const char *pixel_format_to_str(pixformat_t format) { + switch (format) { + case PIXFORMAT_RGB565: + return "RGB565"; + case PIXFORMAT_YUV422: + return "YUV422"; + case PIXFORMAT_YUV420: + return "YUV420"; + case PIXFORMAT_GRAYSCALE: + return "GRAYSCALE"; + case PIXFORMAT_JPEG: + return "JPEG"; + case PIXFORMAT_RGB888: + return "RGB888"; + case PIXFORMAT_RAW: + return "RAW"; + case PIXFORMAT_RGB444: + return "RGB444"; + case PIXFORMAT_RGB555: + return "RGB555"; + default: + return "UNKNOWN"; + } +} + /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { #ifdef USE_I2C @@ -68,64 +136,9 @@ void ESP32Camera::dump_config() { this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); - switch (this->config_.frame_size) { - case FRAMESIZE_QQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); - break; - case FRAMESIZE_QCIF: - ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); - break; - case FRAMESIZE_HQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); - break; - case FRAMESIZE_QVGA: - ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); - break; - case FRAMESIZE_CIF: - ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); - break; - case FRAMESIZE_VGA: - ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); - break; - case FRAMESIZE_SVGA: - ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); - break; - case FRAMESIZE_XGA: - ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); - break; - case FRAMESIZE_SXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); - break; - case FRAMESIZE_UXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); - break; - case FRAMESIZE_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); - break; - case FRAMESIZE_P_HD: - ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); - break; - case FRAMESIZE_P_3MP: - ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); - break; - case FRAMESIZE_QXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); - break; - case FRAMESIZE_QHD: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); - break; - case FRAMESIZE_WQXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); - break; - case FRAMESIZE_P_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); - break; - case FRAMESIZE_QSXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); - break; - default: - break; - } + + ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size)); + ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format)); if (this->is_failed()) { ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); @@ -184,8 +197,19 @@ void ESP32Camera::loop() { // check if we can return the image if (this->can_return_image_()) { // return image - auto *fb = this->current_image_->get_raw_buffer(); - xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to free the data and raw buffer + auto *jpg_buf = this->current_image_->get_data_buffer(); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + auto *fb = this->current_image_->get_raw_buffer(); + this->fb_allocator_.deallocate(fb, 1); + } else +#endif + { + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + } this->current_image_.reset(); } @@ -212,6 +236,38 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } + +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to convert the frame to JPEG + uint8_t *jpg_buf; + size_t jpg_buf_len; + size_t width = fb->width; + size_t height = fb->height; + struct timeval timestamp = fb->timestamp; + bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len); + // return the original frame buffer to the queue + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + if (!ok) { + ESP_LOGE(TAG, "Failed to convert frame to JPEG!"); + return; + } + // create a new camera_fb_t for the JPEG data + fb = this->fb_allocator_.allocate(1); + if (fb == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!"); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + return; + } + memset(fb, 0, sizeof(camera_fb_t)); + fb->buf = jpg_buf; + fb->len = jpg_buf_len; + fb->width = width; + fb->height = height; + fb->format = PIXFORMAT_JPEG; + fb->timestamp = timestamp; + } +#endif this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -342,6 +398,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { break; } } +void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { + switch (format) { + case ESP32_PIXEL_FORMAT_RGB565: + this->config_.pixel_format = PIXFORMAT_RGB565; + break; + case ESP32_PIXEL_FORMAT_YUV422: + this->config_.pixel_format = PIXFORMAT_YUV422; + break; + case ESP32_PIXEL_FORMAT_YUV420: + this->config_.pixel_format = PIXFORMAT_YUV420; + break; + case ESP32_PIXEL_FORMAT_GRAYSCALE: + this->config_.pixel_format = PIXFORMAT_GRAYSCALE; + break; + case ESP32_PIXEL_FORMAT_JPEG: + this->config_.pixel_format = PIXFORMAT_JPEG; + break; + case ESP32_PIXEL_FORMAT_RGB888: + this->config_.pixel_format = PIXFORMAT_RGB888; + break; + case ESP32_PIXEL_FORMAT_RAW: + this->config_.pixel_format = PIXFORMAT_RAW; + break; + case ESP32_PIXEL_FORMAT_RGB444: + this->config_.pixel_format = PIXFORMAT_RGB444; + break; + case ESP32_PIXEL_FORMAT_RGB555: + this->config_.pixel_format = PIXFORMAT_RGB555; + break; + } +} void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index eea93b7e015..9fbd3848f28 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -41,6 +41,18 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; +enum ESP32CameraPixelFormat { + ESP32_PIXEL_FORMAT_RGB565, + ESP32_PIXEL_FORMAT_YUV422, + ESP32_PIXEL_FORMAT_YUV420, + ESP32_PIXEL_FORMAT_GRAYSCALE, + ESP32_PIXEL_FORMAT_JPEG, + ESP32_PIXEL_FORMAT_RGB888, + ESP32_PIXEL_FORMAT_RAW, + ESP32_PIXEL_FORMAT_RGB444, + ESP32_PIXEL_FORMAT_RGB555, +}; + enum ESP32AgcGainCeiling { ESP32_GAINCEILING_2X = GAINCEILING_2X, ESP32_GAINCEILING_4X = GAINCEILING_4X, @@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera { void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ + void set_pixel_format(ESP32CameraPixelFormat format); void set_frame_size(ESP32CameraFrameSize size); void set_jpeg_quality(uint8_t quality); void set_vertical_flip(bool vertical_flip); @@ -220,6 +233,7 @@ class ESP32Camera : public camera::Camera { #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C + RAMAllocator fb_allocator_{RAMAllocator::ALLOC_INTERNAL}; }; class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5559ec88d00..a7fb9f197c2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -43,6 +43,7 @@ #define USE_DEVICES #define USE_DISPLAY #define USE_ENTITY_ICON +#define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index 443ebbebd9f..07ab6cdc8da 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -30,6 +30,7 @@ esp32_camera: resolution: 640x480 jpeg_quality: 10 frame_buffer_location: PSRAM + pixel_format: JPEG on_image: then: - lambda: |- From 4cc1e6a9103dc804e9e4516ff1f6fe313701ccf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 19 Feb 2026 15:23:22 +0100 Subject: [PATCH 0053/2056] [esp32_ble_server] add test for lambda characteristic (#14091) --- tests/components/esp32_ble_server/common.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index e9576a8262c..7fe0b2eb5f1 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -33,6 +33,10 @@ esp32_ble_server: - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d advertise: false characteristics: + - id: test_lambda_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc12c + read: true + value: !lambda return { 1, 2 }; - id: test_change_characteristic uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c read: true From 7b53a9895000219aac5895de48c5bd15715d7aea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:39:44 -0600 Subject: [PATCH 0054/2056] [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index aa37386d700..6e95f5bc7af 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -680,6 +680,11 @@ class LWIPRawListenImpl final : public LWIPRawImpl { }; std::unique_ptr socket(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; From 6daca09794d7d48478e4c6751907ae5a4b5aa7b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:08 -0600 Subject: [PATCH 0055/2056] [logger] Replace LogListener virtual interface with LogCallback struct (#14084) --- esphome/components/api/api_server.cpp | 5 +- esphome/components/api/api_server.h | 6 +- esphome/components/ble_nus/ble_nus.cpp | 5 +- esphome/components/ble_nus/ble_nus.h | 9 +-- esphome/components/logger/logger.h | 66 ++++++++++---------- esphome/components/mqtt/mqtt_client.cpp | 5 +- esphome/components/mqtt/mqtt_client.h | 9 +-- esphome/components/syslog/esphome_syslog.cpp | 7 ++- esphome/components/syslog/esphome_syslog.h | 5 +- esphome/components/web_server/web_server.cpp | 5 +- esphome/components/web_server/web_server.h | 11 +--- 11 files changed, 65 insertions(+), 68 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 67a117e68fd..1a1d0b229b1 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -92,7 +92,10 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 323acc2efb3..3b9ba0e23b4 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -37,10 +37,6 @@ struct SavedNoisePsk { class APIServer : public Component, public Controller -#ifdef USE_LOGGER - , - public logger::LogListener -#endif #ifdef USE_CAMERA , public camera::CameraListener @@ -56,7 +52,7 @@ class APIServer : public Component, void on_shutdown() override; bool teardown() override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 0de65b623f6..a10132eb3ef 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,7 +87,10 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif } diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index ef20fc5e5b5..b2b0ee7713a 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -10,12 +10,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class BLENUS : public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -29,7 +24,7 @@ class BLENUS : public Component size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif protected: diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 835542dd8f1..2a7552af925 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,26 +40,25 @@ struct device; namespace esphome::logger { -/** Interface for receiving log messages without std::function overhead. +/** Lightweight callback for receiving log messages without virtual dispatch overhead. * - * Components can implement this interface instead of using lambdas with std::function - * to reduce flash usage from std::function type erasure machinery. + * Replaces the former LogListener virtual interface to eliminate per-implementer + * vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener). * * Usage: - * class MyComponent : public Component, public LogListener { - * public: - * void setup() override { - * if (logger::global_logger != nullptr) - * logger::global_logger->add_log_listener(this); - * } - * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - * // Handle log message - * } - * }; + * // In your component's setup(): + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_callback( + * this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + * static_cast(self)->on_log(level, tag, message, message_len); + * }); */ -class LogListener { - public: - virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +struct LogCallback { + void *instance; + void (*fn)(void *, uint8_t, const char *, const char *, size_t); + void invoke(uint8_t level, const char *tag, const char *message, size_t message_len) const { + this->fn(this->instance, level, tag, message, message_len); + } }; #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -187,11 +186,13 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); #ifdef USE_LOG_LISTENERS - /// Register a log listener to receive log messages - void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } + /// Register a log callback to receive log messages + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) { + this->log_callbacks_.push_back(LogCallback{instance, fn}); + } #else /// No-op when log listeners are disabled - void add_log_listener(LogListener *listener) {} + void add_log_callback(void *instance, void (*fn)(void *, uint8_t, const char *, const char *, size_t)) {} #endif #ifdef USE_LOGGER_LEVEL_LISTENERS @@ -253,11 +254,11 @@ class Logger : public Component { } #endif - // Helper to notify log listeners + // Helper to notify log callbacks inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { #ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, buf.data, buf.pos); + for (auto &cb : this->log_callbacks_) + cb.invoke(level, tag, buf.data, buf.pos); #endif } @@ -341,8 +342,8 @@ class Logger : public Component { std::map log_levels_{}; #endif #ifdef USE_LOG_LISTENERS - StaticVector - log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) + StaticVector + log_callbacks_; // Log message callbacks (API, MQTT, syslog, etc.) #endif #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector level_listeners_; // Log level change listeners @@ -478,15 +479,16 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger, public LogListener { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } - - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { - (void) message_len; - if (level <= this->level_) { - this->trigger(level, tag, message); - } + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { + parent->add_log_callback(this, + [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + auto *trigger = static_cast(self); + if (level <= trigger->level_) { + trigger->trigger(level, tag, message); + } + }); } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 90b423c3867..9905b4677e6 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -64,7 +64,10 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 38bc0b4da37..21edd53eda5 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -99,12 +99,7 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class MQTTClientComponent : public Component { public: MQTTClientComponent(); @@ -252,7 +247,7 @@ class MQTTClientComponent : public Component float get_setup_priority() const override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif void on_message(const std::string &topic, const std::string &payload); diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 376de54db47..790d08ffa6d 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -18,7 +18,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { logger::global_logger->add_log_listener(this); } +void Syslog::setup() { + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); +} void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { this->log_(level, tag, message, message_len); diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index bde6ab5ed49..be4fa91436f 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,17 +2,16 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome::syslog { -class Syslog : public Component, public Parented, public logger::LogListener { +class Syslog : public Component, public Parented { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e3d131f58ee..a44a47379e4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -395,7 +395,10 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 026da763ea7..6afe618b599 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -186,14 +186,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 19 Feb 2026 08:40:23 -0600 Subject: [PATCH 0056/2056] [core] Devirtualize call_loop() and mark_failed() in Component (#14083) --- esphome/core/component.cpp | 12 +++++------- esphome/core/component.h | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index ba0f1663b9e..f283a690643 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -207,7 +207,7 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop() { this->loop(); } +void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { this->dump_config(); @@ -258,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop(); + this->call_loop_(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing @@ -497,16 +497,14 @@ void Component::set_setup_priority(float priority) { bool Component::has_overridden_loop() const { #if defined(USE_HOST) || defined(CLANG_TIDY) - bool loop_overridden = true; - bool call_loop_overridden = true; + return true; #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpmf-conversions" bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); - bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); #pragma GCC diagnostic pop + return loop_overridden; #endif - return loop_overridden || call_loop_overridden; } PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} diff --git a/esphome/core/component.h b/esphome/core/component.h index 6f7f77dbc10..d4dad3c9a65 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -165,7 +165,7 @@ class Component { * For example, i2c based components can check if the remote device is responding and otherwise * mark the component as failed. Eventually this will also enable smart status LEDs. */ - virtual void mark_failed(); + void mark_failed(); // Remove before 2026.6.0 ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " @@ -286,7 +286,7 @@ class Component { protected: friend class Application; - virtual void call_loop(); + void call_loop_(); virtual void call_setup(); virtual void call_dump_config(); From 535980b9bd37ebf405a6305e34e980e3f16a5af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 08:40:41 -0600 Subject: [PATCH 0057/2056] [cse7761] Use constexpr for compile-time constants (#14081) --- esphome/components/cse7761/cse7761.cpp | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 7c5ee833a44..f4966357d4b 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -15,29 +15,29 @@ static const char *const TAG = "cse7761"; * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino \*********************************************************************************************/ -static const int CSE7761_UREF = 42563; // RmsUc -static const int CSE7761_IREF = 52241; // RmsIAC -static const int CSE7761_PREF = 44513; // PowerPAC +static constexpr int CSE7761_UREF = 42563; // RmsUc +static constexpr int CSE7761_IREF = 52241; // RmsIAC +static constexpr int CSE7761_PREF = 44513; // PowerPAC -static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) -static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) -static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) -static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) +static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) -static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) -static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) -static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) -static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register +static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register -static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum -static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient +static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient -static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command -static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets -static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation -static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation +static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static constexpr uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; From 01a46f665f550085022bed7d994e043045f5553a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:42:22 -0500 Subject: [PATCH 0058/2056] Bump esptool from 5.1.0 to 5.2.0 (#14058) Signed-off-by: dependabot[bot] 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 2e6268284a0..be3445dceb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 -esptool==5.1.0 +esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 aioesphomeapi==44.0.0 From b5a8e1c94cfa2d82c2e9b9241ae62c1abd7dd3a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:06:46 -0600 Subject: [PATCH 0059/2056] [ci] Update lint message to recommend constexpr over static const (#14099) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8c405b04aea..231f587068e 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -301,7 +301,7 @@ def highlight(s): ], ) def lint_no_defines(fname, match): - s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") + s = highlight(f"static constexpr uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " f"{s} style instead (replace uint8_t with the appropriate " From 0484b2852dcf76161f5ae36d447e8d9e1cdad747 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 0060/2056] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 41878579014..941927122cc 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2e..72da9ddebeb 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e57581..aa5c740454e 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 916cf0d8b7f945bb6e3ee30702469f22013b09c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:28:00 -0600 Subject: [PATCH 0061/2056] [e131] Replace std::map with std::vector for universe tracking (#14087) --- esphome/components/e131/e131.h | 9 ++++-- esphome/components/e131/e131_packet.cpp | 39 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 72da9ddebeb..fee447b678e 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -9,7 +9,6 @@ #include "esphome/core/component.h" #include -#include #include #include @@ -27,6 +26,11 @@ struct E131Packet { uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; }; +struct UniverseConsumer { + uint16_t universe; + uint16_t consumers; +}; + class E131Component : public esphome::Component { public: E131Component(); @@ -45,6 +49,7 @@ class E131Component : public esphome::Component { bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); + UniverseConsumer *find_universe_(int universe); void join_(int universe); void leave_(int universe); @@ -55,7 +60,7 @@ class E131Component : public esphome::Component { WiFiUDP udp_; #endif std::vector light_effects_; - std::map universe_consumers_; + std::vector universe_consumers_; }; } // namespace e131 diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index aa5c740454e..b90e6d5c914 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -60,19 +60,19 @@ union E131RawPacket { const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); bool E131Component::join_igmp_groups_() { - if (listen_method_ != E131_MULTICAST) + if (this->listen_method_ != E131_MULTICAST) return false; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; #endif - for (auto universe : universe_consumers_) { - if (!universe.second) + for (auto &entry : this->universe_consumers_) { + if (!entry.consumers) continue; ip4_addr_t multicast_addr = - network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); + network::IPAddress(239, 255, ((entry.universe >> 8) & 0xff), ((entry.universe >> 0) & 0xff)); err_t err; { @@ -81,34 +81,47 @@ bool E131Component::join_igmp_groups_() { } if (err) { - ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", entry.universe); } } return true; } +UniverseConsumer *E131Component::find_universe_(int universe) { + for (auto &entry : this->universe_consumers_) { + if (entry.universe == universe) + return &entry; + } + return nullptr; +} + void E131Component::join_(int universe) { // store only latest received packet for the given universe - auto consumers = ++universe_consumers_[universe]; - - if (consumers > 1) { - return; // we already joined before + auto *consumer = this->find_universe_(universe); + if (consumer != nullptr) { + if (consumer->consumers++ > 0) { + return; // we already joined before + } + } else { + this->universe_consumers_.push_back({static_cast(universe), 1}); } - if (join_igmp_groups_()) { + if (this->join_igmp_groups_()) { ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); } } void E131Component::leave_(int universe) { - auto consumers = --universe_consumers_[universe]; + auto *consumer = this->find_universe_(universe); + if (consumer == nullptr) + return; - if (consumers > 0) { + if (--consumer->consumers > 0) { return; // we have other consumers of the given universe } - if (listen_method_ == E131_MULTICAST) { + if (this->listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); LwIPLock lock; From a8171da0033ce797ef59125a69b270cfe35e36eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:38:57 -0600 Subject: [PATCH 0062/2056] [web_server] Reduce set_json_id flash and stack usage (#14029) --- esphome/components/web_server/web_server.cpp | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a44a47379e4..f352e5896c5 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -374,7 +374,7 @@ std::string WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); - root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); char comment_buffer[ESPHOME_COMMENT_SIZE]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; @@ -513,21 +513,27 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J size_t device_len = device_name ? strlen(device_name) : 0; #endif - // Build id into stack buffer - ArduinoJson copies the string - // Format: {prefix}/{device?}/{name} + // Single stack buffer for both id formats - ArduinoJson copies the string before we overwrite // Buffer sizes use constants from entity_base.h validated in core/config.py // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN // (hostname) + // Without USE_DEVICES: legacy id ({prefix}-{object_id}) is the largest format + // With USE_DEVICES: name_id ({prefix}/{device}/{name}) is the largest format + static constexpr size_t LEGACY_ID_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN; #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = - ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, + LEGACY_ID_SIZE); #else - static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + static constexpr size_t ID_BUF_SIZE = + std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, LEGACY_ID_SIZE); #endif char id_buf[ID_BUF_SIZE]; - char *p = id_buf; - memcpy(p, prefix, prefix_len); - p += prefix_len; + memcpy(id_buf, prefix, prefix_len); // NOLINT(bugprone-not-null-terminated-result) + + // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this + // Remove in 2026.8.0 when id switches to new format permanently + char *p = id_buf + prefix_len; *p++ = '/'; #ifdef USE_DEVICES if (device_name) { @@ -538,31 +544,25 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J #endif memcpy(p, name.c_str(), name_len); p[name_len] = '\0'; - - // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this - // Remove in 2026.8.0 when id switches to new format permanently root[ESPHOME_F("name_id")] = id_buf; // id: old format {prefix}-{object_id} for backward compatibility - // Will switch to new format in 2026.8.0 - char legacy_buf[ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN]; - char *lp = legacy_buf; - memcpy(lp, prefix, prefix_len); - lp += prefix_len; - *lp++ = '-'; - obj->write_object_id_to(lp, sizeof(legacy_buf) - (lp - legacy_buf)); - root[ESPHOME_F("id")] = legacy_buf; + // Will switch to new format in 2026.8.0 - reuses prefix already in id_buf + id_buf[prefix_len] = '-'; + obj->write_object_id_to(id_buf + prefix_len + 1, ID_BUF_SIZE - prefix_len - 1); + root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { root[ESPHOME_F("domain")] = prefix; - root[ESPHOME_F("name")] = name; + // Use .c_str() to avoid instantiating set template (saves ~24B) + root[ESPHOME_F("name")] = name.c_str(); #ifdef USE_DEVICES if (device_name) { root[ESPHOME_F("device")] = device_name; } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref(); + root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); @@ -632,7 +632,7 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); if (!uom_ref.empty()) - root[ESPHOME_F("uom")] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref.c_str(); } return builder.serialize(); @@ -1147,7 +1147,7 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf); root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) - root[ESPHOME_F("uom")] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref.c_str(); this->add_sorting_info_(root, obj); } From 5304750215a7be803d87bb9f059d43952fe084f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 0063/2056] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162eadc..6154c497e0c 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 00000000000..aaf49f16112 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 00000000000..da1324b17ef --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 00000000000..fdd52c574e1 --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 00000000000..e0c5d7cea3a --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From f7459670d34b47f66210eaaca7586be33f84205c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 10:10:22 -0600 Subject: [PATCH 0064/2056] [core] Optimize WarnIfComponentBlockingGuard::finish() hot path (#14040) Co-authored-by: Claude Opus 4.6 --- esphome/core/component.cpp | 38 ++++++++++++++++++-------------------- esphome/core/component.h | 7 ++++--- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f283a690643..b458ea2a84f 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -82,8 +82,8 @@ void store_component_error_message(const Component *component, const char *messa // setup_priority, component state, and status LED constants are now // constexpr in component.h -const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning -const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = + 10U; ///< How long the blocking time must be larger to warn again float Component::get_loop_priority() const { return 0.0f; } @@ -529,37 +529,35 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) - : started_(start_time), component_(component) {} +static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time) { + bool should_warn; + if (component != nullptr) { + should_warn = component->should_warn_of_blocking(blocking_time); + } else { + should_warn = true; // Already checked > WARN_IF_BLOCKING_OVER_MS in caller + } + if (should_warn) { + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms), max is 30 ms", + component == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component->get_component_log_str()), + blocking_time); + } +} + 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); - } else { - should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { + warn_blocking(this->component_, blocking_time); } - if (should_warn) { - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", - component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), - blocking_time); - ESP_LOGW(TAG, "Components should block for at most 30 ms"); - } - return curr_time; } -WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} - #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely diff --git a/esphome/core/component.h b/esphome/core/component.h index d4dad3c9a65..b99641a2753 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -79,7 +79,7 @@ inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -extern const uint16_t WARN_IF_BLOCKING_OVER_MS; +inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; class Component { public: @@ -550,12 +550,13 @@ class PollingComponent : public Component { class WarnIfComponentBlockingGuard { public: - WarnIfComponentBlockingGuard(Component *component, uint32_t start_time); + WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) + : started_(start_time), component_(component) {} // Finish the timing operation and return the current time uint32_t finish(); - ~WarnIfComponentBlockingGuard(); + ~WarnIfComponentBlockingGuard() = default; protected: uint32_t started_; From b11ad26c4f70050d1656200234b0642760bdea68 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:20:19 -0600 Subject: [PATCH 0065/2056] [audio] Support decoding audio directly from flash (#14098) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/audio/audio_decoder.cpp | 102 ++++++++++-------- esphome/components/audio/audio_decoder.h | 26 +++-- .../audio/audio_transfer_buffer.cpp | 17 ++- .../components/audio/audio_transfer_buffer.h | 67 +++++++++++- 4 files changed, 159 insertions(+), 53 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index ee6d7d0a153..7794b5b0d32 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -15,8 +15,8 @@ static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring a static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10; -AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) { - this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); +AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) + : input_buffer_size_(input_buffer_size) { this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } @@ -29,11 +29,20 @@ AudioDecoder::~AudioDecoder() { } esp_err_t AudioDecoder::add_source(std::weak_ptr &input_ring_buffer) { - if (this->input_transfer_buffer_ != nullptr) { - this->input_transfer_buffer_->set_source(input_ring_buffer); - return ESP_OK; + auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); + if (source == nullptr) { + return ESP_ERR_NO_MEM; } - return ESP_ERR_NO_MEM; + source->set_source(input_ring_buffer); + this->input_buffer_ = std::move(source); + return ESP_OK; +} + +esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) { + auto source = make_unique(); + source->set_data(data_pointer, length); + this->input_buffer_ = std::move(source); + return ESP_OK; } esp_err_t AudioDecoder::add_sink(std::weak_ptr &output_ring_buffer) { @@ -54,8 +63,16 @@ esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) { } #endif +esp_err_t AudioDecoder::add_sink(AudioSinkCallback *callback) { + if (this->output_transfer_buffer_ != nullptr) { + this->output_transfer_buffer_->set_sink(callback); + return ESP_OK; + } + return ESP_ERR_NO_MEM; +} + esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { - if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { + if (this->output_transfer_buffer_ == nullptr) { return ESP_ERR_NO_MEM; } @@ -112,6 +129,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { } AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { + if (this->input_buffer_ == nullptr) { + return AudioDecoderState::FAILED; + } + if (stop_gracefully) { if (this->output_transfer_buffer_->available() == 0) { if (this->end_of_file_) { @@ -119,7 +140,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { return AudioDecoderState::FINISHED; } - if (!this->input_transfer_buffer_->has_buffered_data()) { + if (!this->input_buffer_->has_buffered_data()) { // If all the internal buffers are empty, the decoding is done return AudioDecoderState::FINISHED; } @@ -170,10 +191,10 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Only shift data on the first loop iteration to avoid unnecessary, slow moves // If the decoder buffers internally, then never shift - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source( - pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), first_loop_iteration && !this->decoder_buffers_internally_); + size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), + first_loop_iteration && !this->decoder_buffers_internally_); - if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { + if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. // This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer // will shift the remaining data to the start and copy more from the source the next time the decode function is @@ -181,19 +202,21 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { break; } - bytes_available_before_processing = this->input_transfer_buffer_->available(); + bytes_available_before_processing = this->input_buffer_->available(); if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) { // Failed to decode in last attempt and there is no new data - if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { - // The input buffer is full. Since it previously failed on the exact same data, we can never recover + if ((this->input_buffer_->free() == 0) && first_loop_iteration) { + // The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact + // same data, we can never recover. For const sources this is correct: the entire file is already available, so + // a decode failure is genuine, not a transient out-of-data condition. state = FileDecoderState::FAILED; } else { // Attempt to get more data next time state = FileDecoderState::IDLE; } - } else if (this->input_transfer_buffer_->available() == 0) { + } else if (this->input_buffer_->available() == 0) { // No data to decode, attempt to get more data next time state = FileDecoderState::IDLE; } else { @@ -224,7 +247,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { } first_loop_iteration = false; - bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available(); + bytes_processed = bytes_available_before_processing - this->input_buffer_->available(); if (state == FileDecoderState::POTENTIALLY_FAILED) { ++this->potentially_failed_count_; @@ -243,8 +266,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { FileDecoderState AudioDecoder::decode_flac_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available()); + auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { // Serrious error reading FLAC header, there is no recovery @@ -252,7 +274,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { return FileDecoderState::MORE_TO_PROCESS; @@ -273,8 +295,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available(), + auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), &output_samples); if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { @@ -283,7 +304,7 @@ FileDecoderState AudioDecoder::decode_flac_() { } size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { // Corrupted frame, don't retry with current buffer content, wait for new sync @@ -305,26 +326,25 @@ FileDecoderState AudioDecoder::decode_flac_() { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState AudioDecoder::decode_mp3_() { // Look for the next sync word - int buffer_length = (int) this->input_transfer_buffer_->available(); - int32_t offset = - esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length); + int buffer_length = (int) this->input_buffer_->available(); + int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length); if (offset < 0) { // New data may have the sync word - this->input_transfer_buffer_->decrease_buffer_length(buffer_length); + this->input_buffer_->consume(buffer_length); return FileDecoderState::POTENTIALLY_FAILED; } // Advance read pointer to match the offset for the syncword - this->input_transfer_buffer_->decrease_buffer_length(offset); - const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start(); + this->input_buffer_->consume(offset); + const uint8_t *buffer_start = this->input_buffer_->data(); - buffer_length = (int) this->input_transfer_buffer_->available(); + buffer_length = (int) this->input_buffer_->available(); int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); - size_t consumed = this->input_transfer_buffer_->available() - buffer_length; - this->input_transfer_buffer_->decrease_buffer_length(consumed); + size_t consumed = this->input_buffer_->available() - buffer_length; + this->input_buffer_->consume(consumed); if (err) { switch (err) { @@ -363,9 +383,8 @@ FileDecoderState AudioDecoder::decode_opus_() { size_t bytes_consumed, samples_decoded; micro_opus::OggOpusResult result = this->opus_decoder_->decode( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), - this->output_transfer_buffer_->get_buffer_end(), this->output_transfer_buffer_->free(), bytes_consumed, - samples_decoded); + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); if (result == micro_opus::OGG_OPUS_OK) { if (!processed_header && this->opus_decoder_->is_initialized()) { @@ -379,7 +398,7 @@ FileDecoderState AudioDecoder::decode_opus_() { this->output_transfer_buffer_->increase_buffer_length( this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); } - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); + this->input_buffer_->consume(bytes_consumed); } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { // Reallocate to decode the packet on the next call this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); @@ -399,11 +418,11 @@ FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed - esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); + esp_audio_libs::wav_decoder::WAVDecoderResult result = + this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed()); + this->input_buffer_->consume(this->wav_decoder_->bytes_processed()); this->audio_stream_info_ = audio::AudioStreamInfo( this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); @@ -419,7 +438,7 @@ FileDecoderState AudioDecoder::decode_wav_() { } } else { if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { - size_t bytes_to_copy = this->input_transfer_buffer_->available(); + size_t bytes_to_copy = this->input_buffer_->available(); if (this->wav_has_known_end_) { bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); @@ -428,9 +447,8 @@ FileDecoderState AudioDecoder::decode_wav_() { bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); if (bytes_to_copy > 0) { - std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(), - bytes_to_copy); - this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy); + std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy); + this->input_buffer_->consume(bytes_to_copy); this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); if (this->wav_has_known_end_) { this->wav_bytes_left_ -= bytes_to_copy; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index cad16110aea..726baa289e2 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -50,12 +50,12 @@ enum class FileDecoderState : uint8_t { class AudioDecoder { /* * @brief Class that facilitates decoding an audio file. - * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker - * component). + * The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink + * (ring buffer, speaker component, or callback). * Supports wav, flac, mp3, and ogg opus formats. */ public: - /// @brief Allocates the input and output transfer buffers + /// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source() /// @param input_buffer_size Size of the input transfer buffer in bytes. /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); @@ -80,6 +80,17 @@ class AudioDecoder { esp_err_t add_sink(speaker::Speaker *speaker); #endif + /// @brief Adds a const data pointer as the source for raw file data. Does not allocate a transfer buffer. + /// @param data_pointer Pointer to the const audio data (e.g., stored in flash memory) + /// @param length Size of the data in bytes + /// @return ESP_OK + esp_err_t add_source(const uint8_t *data_pointer, size_t length); + + /// @brief Adds a callback as the sink for decoded audio. + /// @param callback Pointer to the AudioSinkCallback implementation + /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated + esp_err_t add_sink(AudioSinkCallback *callback); + /// @brief Sets up decoding the file /// @param audio_file_type AudioFileType of the file /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if @@ -120,25 +131,26 @@ class AudioDecoder { #endif FileDecoderState decode_wav_(); - std::unique_ptr input_transfer_buffer_; + std::unique_ptr input_buffer_; std::unique_ptr output_transfer_buffer_; AudioFileType audio_file_type_{AudioFileType::NONE}; optional audio_stream_info_{}; + size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; size_t wav_bytes_left_{0}; uint32_t potentially_failed_count_{0}; + uint32_t accumulated_frames_written_{0}; + uint32_t playback_ms_{0}; + bool end_of_file_{false}; bool wav_has_known_end_{false}; bool decoder_buffers_internally_{false}; bool pause_output_{false}; - - uint32_t accumulated_frames_written_{0}; - uint32_t playback_ms_{0}; }; } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index a8be55d62fe..5cd7cf9e631 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -142,7 +142,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ this->data_start_ = this->buffer_; } - size_t bytes_to_read = this->free(); + size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { if (this->ring_buffer_.use_count() > 0) { @@ -193,6 +193,21 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->available() > 0); } +size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); } + +bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); } + +void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) { + this->data_start_ = data; + this->length_ = length; +} + +void ConstAudioSourceBuffer::consume(size_t bytes) { + bytes = std::min(bytes, this->length_); + this->length_ -= bytes; + this->data_start_ += bytes; +} + } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 22c22cc9ae8..c32d4d0e41d 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -32,7 +32,7 @@ class AudioTransferBuffer { /// @brief Destructor that deallocates the transfer buffer ~AudioTransferBuffer(); - /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read + /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of existing data can be read uint8_t *get_buffer_start() const { return this->data_start_; } /// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written @@ -129,10 +129,41 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { AudioSinkCallback *sink_callback_{nullptr}; }; -class AudioSourceTransferBuffer : public AudioTransferBuffer { +/// @brief Abstract interface for reading audio data from a buffer. +/// Provides a common read interface for both mutable transfer buffers and read-only const buffers. +class AudioReadableBuffer { + public: + virtual ~AudioReadableBuffer() = default; + + /// @brief Returns a pointer to the start of readable data + virtual const uint8_t *data() const = 0; + + /// @brief Returns the number of bytes available to read + virtual size_t available() const = 0; + + /// @brief Returns the number of free bytes available to write. Defaults to 0 for read-only buffers. + virtual size_t free() const { return 0; } + + /// @brief Advances past consumed data + /// @param bytes Number of bytes consumed + virtual void consume(size_t bytes) = 0; + + /// @brief Tests if there is any buffered data + virtual bool has_buffered_data() const = 0; + + /// @brief Refills the buffer from its source. No-op by default for read-only buffers. + /// @param ticks_to_wait FreeRTOS ticks to block while waiting for data + /// @param pre_shift If true, shifts existing data to the start of the buffer before reading + /// @return Number of bytes read + virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; } + size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); } +}; + +class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer { /* * @brief A class that implements a transfer buffer for audio sources. * Supports reading audio data from a ring buffer into the transfer buffer for processing. + * Implements AudioReadableBuffer for use by consumers that only need read access. */ public: /// @brief Creates a new source transfer buffer. @@ -140,7 +171,7 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @return unique_ptr if successfully allocated, nullptr otherwise static std::unique_ptr create(size_t buffer_size); - /// @brief Reads any available data from the sink into the transfer buffer. + /// @brief Reads any available data from the source into the transfer buffer. /// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data /// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the /// source. Defaults to true. @@ -150,6 +181,36 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @brief Adds a ring buffer as the transfer buffer's source. /// @param ring_buffer weak_ptr to the allocated ring buffer void set_source(const std::weak_ptr &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }; + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->buffer_length_; } + size_t free() const override; + void consume(size_t bytes) override { this->decrease_buffer_length(bytes); } + bool has_buffered_data() const override; + size_t fill(TickType_t ticks_to_wait, bool pre_shift) override { + return this->transfer_data_from_source(ticks_to_wait, pre_shift); + } +}; + +/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory). +/// Does not allocate memory or transfer data from external sources. +class ConstAudioSourceBuffer : public AudioReadableBuffer { + public: + /// @brief Sets the data pointer and length for the buffer + /// @param data Pointer to the const audio data + /// @param length Size of the data in bytes + void set_data(const uint8_t *data, size_t length); + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->length_; } + void consume(size_t bytes) override; + bool has_buffered_data() const override { return this->length_ > 0; } + + protected: + const uint8_t *data_start_{nullptr}; + size_t length_{0}; }; } // namespace audio From bd50b80882d990855561216548965d4c01aad9fd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:34:40 -0500 Subject: [PATCH 0066/2056] [opentherm] Remove deprecated opentherm_version config option (#14103) Co-authored-by: Claude Opus 4.6 --- esphome/components/opentherm/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index dddb9dc8918..36f85a97661 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -1,4 +1,3 @@ -import logging from typing import Any from esphome import automation, pins @@ -24,7 +23,6 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" -CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed CONF_BEFORE_SEND = "before_send" CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" @@ -38,8 +36,6 @@ BeforeProcessResponseTrigger = generate.opentherm_ns.class_( automation.Trigger.template(generate.OpenthermData.operator("ref")), ) -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -54,7 +50,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), @@ -123,11 +118,6 @@ async def to_code(config: dict[str, Any]) -> None: cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) settings.append(key) else: - if key == CONF_OPENTHERM_VERSION: - _LOGGER.warning( - "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" - "Please change to 'opentherm_version_controller'." - ) cg.add(getattr(var, f"set_{key}")(value)) if len(input_sensors) > 0: From bf2e22da4f7ebf660d517f27702500a67d11ea9b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:55:03 -0500 Subject: [PATCH 0067/2056] [esp32] Remove deprecated add_idf_component() parameters and IDF component refresh option (#14105) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 68 ++++++---------------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8b3e1afea67..b1b3f0dc168 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -25,7 +25,6 @@ from esphome.const import ( CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, - CONF_REFRESH, CONF_SAFE_MODE, CONF_SOURCE, CONF_TYPE, @@ -41,7 +40,7 @@ from esphome.const import ( ThreadModel, __version__, ) -from esphome.core import CORE, HexInt, TimePeriod +from esphome.core import CORE, HexInt from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed @@ -499,49 +498,24 @@ def add_idf_component( repo: str | None = None, ref: str | None = None, path: str | None = None, - refresh: TimePeriod | None = None, - components: list[str] | None = None, - submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" if not repo and not ref and not path: raise ValueError("Requires at least one of repo, ref or path") - if refresh or submodules or components: - _LOGGER.warning( - "The refresh, components and submodules parameters in add_idf_component() are " - "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " - "an issue to the external_component author and ask them to update it." - ) components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] - if components: - for comp in components: - existing = components_registry.get(comp) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - comp, - existing.get(KEY_REF), - ref, - ) - components_registry[comp] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: f"{path}/{comp}" if path else comp, - } - else: - existing = components_registry.get(name) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - name, - existing.get(KEY_REF), - ref, - ) - components_registry[name] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: path, - } + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + } def exclude_builtin_idf_component(name: str) -> None: @@ -1037,16 +1011,6 @@ def _parse_idf_component(value: str) -> ConfigType: ) -def _validate_idf_component(config: ConfigType) -> ConfigType: - """Validate IDF component config and warn about deprecated options.""" - if CONF_REFRESH in config: - _LOGGER.warning( - "The 'refresh' option for IDF components is deprecated and has no effect. " - "It will be removed in ESPHome 2026.1. Please remove it from your configuration." - ) - return config - - FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.Schema( @@ -1135,13 +1099,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_SOURCE): cv.git_ref, cv.Optional(CONF_REF): cv.string, cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), } ), ), - _validate_idf_component, ) ), } From ed74790eed87afff0f252fb2330dceb9b1a293de Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:56:06 -0500 Subject: [PATCH 0068/2056] [i2c] Remove deprecated stop parameter overloads and readv/writev methods (#14106) Co-authored-by: Claude Opus 4.6 --- esphome/components/i2c/i2c.h | 31 ----------------- esphome/components/i2c/i2c_bus.h | 58 -------------------------------- 2 files changed, 89 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 48a6e751cfe..aab98d5f468 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -267,37 +267,6 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } - // Deprecated functions - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register16(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register16(a_register, data, len); - } - protected: uint8_t address_{0x00}; ///< store the address of the device on the bus I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 3de5d5ca7b7..2bc0dc1ef91 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,8 +1,6 @@ #pragma once #include #include -#include -#include #include #include @@ -24,18 +22,6 @@ enum ErrorCode { ERROR_CRC = 7, ///< bytes received with a CRC error }; -/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length -struct ReadBuffer { - uint8_t *data; ///< pointer to the read buffer - size_t len; ///< length of the buffer -}; - -/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length -struct WriteBuffer { - const uint8_t *data; ///< pointer to the write buffer - size_t len; ///< length of the buffer -}; - /// @brief This Class provides the methods to read and write bytes from an I2CBus. /// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required /// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and @@ -68,50 +54,6 @@ class I2CBus { return this->write_readv(address, buffer, len, nullptr, 0); } - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += read_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small - uint8_t *buffer = buffer_alloc.get(); - - auto err = this->write_readv(address, nullptr, 0, buffer, total_len); - if (err != ERROR_OK) - return err; - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); - pos += read_buffers[i].len; - } - } - return ERROR_OK; - } - - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += write_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small - uint8_t *buffer = buffer_alloc.get(); - - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); - pos += write_buffers[i].len; - } - - return this->write_readv(address, buffer, total_len, nullptr, 0); - } - protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. From d2026b4cd74bbeb9b86941bf1887f3d93d639387 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 19 Feb 2026 10:56:34 -0600 Subject: [PATCH 0069/2056] [audio] Disable FLAC CRC validation to improve decoding efficiency (#14108) --- esphome/components/audio/audio_decoder.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 7794b5b0d32..bc05bc0006a 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -85,6 +85,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { #ifdef USE_AUDIO_FLAC_SUPPORT case AudioFileType::FLAC: this->flac_decoder_ = make_unique(); + // CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source + // or built into the firmware, so the data integrity is already verified by the time it gets to the decoder, + // making the CRC check unnecessary. + this->flac_decoder_->set_crc_check_enabled(false); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header break; From da616e05574af7216a3b5668ab1d2d0e3dce0ca1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 0070/2056] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d410..935d2004d49 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From 9aa17984df3a0766c18c02ed60f4575ba69a7fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 0071/2056] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66b..7a68858099c 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 00000000000..cd15cc781d6 --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From 7a5c3cee0d0f3fd77423438f036e8b6596a3246b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 0072/2056] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf2..d2020ada223 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..f8defaf28ff --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From f2c98d612607d61c483d08cc77e92795af85c131 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 0073/2056] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a0..6cae4bf9d5d 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From 4aa8f57d3609c12c10c3877c83b06d0c52975e7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 14:08:44 -0600 Subject: [PATCH 0074/2056] [json] Add SerializationBuffer for stack-first JSON serialization (#13625) --- esphome/components/api/user_services.h | 4 +- esphome/components/json/json_util.cpp | 81 +++++++- esphome/components/json/json_util.h | 111 +++++++++- esphome/components/mqtt/mqtt_client.cpp | 4 +- esphome/components/web_server/web_server.cpp | 191 +++++++++--------- esphome/components/web_server/web_server.h | 138 +++++++------ .../web_server_idf/web_server_idf.cpp | 6 +- .../web_server_idf/web_server_idf.h | 3 +- tests/components/json/common.yaml | 13 +- 9 files changed, 367 insertions(+), 184 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 85fba2a4359..0fc529108c7 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -264,9 +264,9 @@ template class APIRespondAction : public Action { // Build and send JSON response json::JsonBuilder builder; this->json_builder_(x..., builder.root()); - std::string json_str = builder.serialize(); + auto json_buf = builder.serialize(); this->parent_->send_action_response(call_id, success, StringRef(error_message), - reinterpret_cast(json_str.data()), json_str.size()); + reinterpret_cast(json_buf.data()), json_buf.size()); return; } #endif diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 69f8bfc61a9..6c60a04d20e 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "json"; static SpiRamAllocator global_json_allocator; #endif -std::string build_json(const json_build_t &f) { +SerializationBuffer<> build_json(const json_build_t &f) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonBuilder builder; JsonObject root = builder.root(); @@ -66,14 +66,83 @@ JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -std::string JsonBuilder::serialize() { +SerializationBuffer<> JsonBuilder::serialize() { + // =========================================================================================== + // CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING + // =========================================================================================== + // + // This function is carefully structured to enable NRVO. The compiler constructs `result` + // directly in the caller's stack frame, eliminating the move constructor call entirely. + // + // WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which + // must memcpy up to 512 bytes of stack buffer content. This happens on EVERY JSON + // serialization (sensor updates, web server responses, MQTT publishes, etc.). + // + // WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly + // where the caller needs it. + // + // Requirements for NRVO to work: + // 1. Single named variable (`result`) returned from ALL paths + // 2. All paths must return the SAME variable (not different variables) + // 3. No std::move() on the return statement + // + // If you must modify this function: + // - Keep a single `result` variable declared at the top + // - All code paths must return `result` (not a different variable) + // - Verify NRVO still works by checking the disassembly for move constructor calls + // - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer" + // Should show only destructor, NOT move constructor + // + // Try stack buffer first. 640 bytes covers 99.9% of JSON payloads (sensors ~200B, + // lights ~170B, climate ~500-700B). Only entities with 40+ options exceed this. + // + // IMPORTANT: ArduinoJson's serializeJson() with a bounded buffer returns the actual + // bytes written (truncated count), NOT the would-be size like snprintf(). When the + // payload exceeds the buffer, the return value equals the buffer capacity. The heap + // fallback doubles the buffer size until the payload fits. This avoids instantiating + // measureJson()'s DummyWriter templates (~736 bytes flash) at the cost of temporarily + // over-allocating heap (at most 2x) for the rare payloads that exceed 640 bytes. + // + // =========================================================================================== + constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE; + SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null) + if (doc_.overflowed()) { ESP_LOGE(TAG, "JSON document overflow"); - return "{}"; + auto *buf = result.data_writable_(); + buf[0] = '{'; + buf[1] = '}'; + buf[2] = '\0'; + result.set_size_(2); + return result; } - std::string output; - serializeJson(doc_, output); - return output; + + size_t size = serializeJson(doc_, result.data_writable_(), buf_size); + if (size < buf_size) { + // Fits in stack buffer - update size to actual length + result.set_size_(size); + return result; + } + + // Payload exceeded stack buffer. Double the buffer and retry until it fits. + // In practice, one iteration (1024 bytes) covers all known entity types. + // Payloads exceeding 1024 bytes are not known to exist in real configurations. + // Cap at 5120 as a safety limit to prevent runaway allocation. + constexpr size_t max_heap_size = 5120; + size_t heap_size = buf_size * 2; + while (heap_size <= max_heap_size) { + result.reallocate_heap_(heap_size - 1); + size = serializeJson(doc_, result.data_writable_(), heap_size); + if (size < heap_size) { + result.set_size_(size); + return result; + } + heap_size *= 2; + } + // Payload exceeds 5120 bytes - return truncated result + ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size); + result.set_size_(size); + return result; } } // namespace json diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index c472b9a9ec5..0dc9ff883cc 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "esphome/core/defines.h" @@ -14,6 +16,108 @@ namespace esphome { namespace json { +/// Buffer for JSON serialization that uses stack allocation for small payloads. +/// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes). +/// Supports move semantics for efficient return-by-value. +template class SerializationBuffer { + public: + static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation + + /// Construct with known size (typically from measureJson) + explicit SerializationBuffer(size_t size) : size_(size) { + if (size + 1 <= STACK_SIZE) { + buffer_ = stack_buffer_; + } else { + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + } + buffer_[0] = '\0'; + } + + ~SerializationBuffer() { delete[] heap_buffer_; } + + // Move constructor - works with same template instantiation + SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) { + if (other.buffer_ == other.stack_buffer_) { + // Stack buffer - must copy content + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + // Heap buffer - steal ownership + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + + // Move assignment + SerializationBuffer &operator=(SerializationBuffer &&other) noexcept { + if (this != &other) { + delete[] heap_buffer_; + heap_buffer_ = other.heap_buffer_; + size_ = other.size_; + if (other.buffer_ == other.stack_buffer_) { + std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1); + buffer_ = stack_buffer_; + } else { + buffer_ = heap_buffer_; + other.heap_buffer_ = nullptr; + } + // Leave moved-from object in valid empty state + other.stack_buffer_[0] = '\0'; + other.buffer_ = other.stack_buffer_; + other.size_ = 0; + } + return *this; + } + + // Delete copy operations + SerializationBuffer(const SerializationBuffer &) = delete; + SerializationBuffer &operator=(const SerializationBuffer &) = delete; + + /// Get null-terminated C string + const char *c_str() const { return buffer_; } + /// Get data pointer + const char *data() const { return buffer_; } + /// Get string length (excluding null terminator) + size_t size() const { return size_; } + + /// Implicit conversion to std::string for backward compatibility + /// WARNING: This allocates a new std::string on the heap. Prefer using + /// c_str() or data()/size() directly when possible to avoid allocation. + operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor) + + private: + friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods + + /// Get writable buffer (for serialization) + char *data_writable_() { return buffer_; } + /// Set actual size after serialization (must not exceed allocated size) + /// Also ensures null termination for c_str() safety + void set_size_(size_t size) { + size_ = size; + buffer_[size] = '\0'; + } + + /// Reallocate to heap buffer with new size (for when stack buffer is too small) + /// This invalidates any previous buffer content. Used by JsonBuilder::serialize(). + void reallocate_heap_(size_t size) { + delete[] heap_buffer_; + heap_buffer_ = new char[size + 1]; + buffer_ = heap_buffer_; + size_ = size; + buffer_[0] = '\0'; + } + + char stack_buffer_[STACK_SIZE]; + char *heap_buffer_{nullptr}; + char *buffer_; + size_t size_; +}; + #ifdef USE_PSRAM // Build an allocator for the JSON Library using the RAMAllocator class // This is only compiled when PSRAM is enabled @@ -47,7 +151,8 @@ using json_parse_t = std::function; using json_build_t = std::function; /// Build a JSON string with the provided json build function. -std::string build_json(const json_build_t &f); +/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string. +SerializationBuffer<> build_json(const json_build_t &f); /// Parse a JSON string and run the provided json parse function if it's valid. bool parse_json(const std::string &data, const json_parse_t &f); @@ -72,7 +177,9 @@ class JsonBuilder { return root_; } - std::string serialize(); + /// Serialize the JSON document to a SerializationBuffer (stack-first allocation) + /// Uses 512-byte stack buffer by default, falls back to heap for larger JSON + SerializationBuffer<> serialize(); private: #ifdef USE_PSRAM diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9905b4677e6..c433804dd97 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -543,8 +543,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t } bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) { - std::string message = json::build_json(f); - return this->publish(topic, message.c_str(), message.length(), qos, retain); + auto message = json::build_json(f); + return this->publish(topic, message.c_str(), message.size(), qos, retain); } void MQTTClientComponent::enable() { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f352e5896c5..4b572417c1d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -214,7 +214,7 @@ DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_gener void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -271,7 +271,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * // deferred queue still not empty which means downstream event queue full, no point trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); } else { @@ -325,7 +325,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource ws->defer([ws, source]() { // Configure reconnect timeout and send config // this should always go through since the AsyncEventSourceClient event queue is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -334,10 +334,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource JsonObject root = builder.root(); root[ESPHOME_F("name")] = group.second.name; root[ESPHOME_F("sorting_weight")] = group.second.weight; - message = builder.serialize(); + auto group_msg = builder.serialize(); // up to 31 groups should be able to be queued initially without defer - source->try_send_nodefer(message.c_str(), "sorting_group"); + source->try_send_nodefer(group_msg.c_str(), "sorting_group"); } #endif @@ -370,7 +370,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ = void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } #endif -std::string WebServer::get_config_json() { +json::SerializationBuffer<> WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -606,20 +606,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->sensor_json_(obj, obj->state, detail); + auto data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -653,23 +653,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json_(obj, obj->state, detail); + auto data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -714,7 +714,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->switch_json_(obj, obj->state, detail); + auto data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -739,13 +739,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) { return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); } -std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) { return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } -std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -767,7 +767,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->button_json_(obj, detail); + auto data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { DEFER_ACTION(obj, obj->press()); @@ -780,10 +780,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) { return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -810,22 +810,23 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json_(obj, obj->state, detail); + auto data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } } request->send(404); } -std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); } -std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -852,7 +853,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->fan_json_(obj, detail); + auto data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -893,13 +894,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } request->send(404); } -std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } -std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) { return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -933,7 +934,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->light_json_(obj, detail); + auto data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { DEFER_ACTION(obj, obj->toggle().perform()); @@ -972,13 +973,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } -std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) { return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1012,7 +1013,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->cover_json_(obj, detail); + auto data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1060,13 +1061,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } -std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) { return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1101,7 +1102,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->number_json_(obj, obj->state, detail); + auto data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1120,13 +1121,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(404); } -std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) { return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); } -std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) { return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } -std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1168,7 +1169,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->date_json_(obj, detail); + auto data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1194,13 +1195,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } -std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) { return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1229,7 +1230,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->time_json_(obj, detail); + auto data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1254,13 +1255,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) { return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1289,7 +1290,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur continue; if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->datetime_json_(obj, detail); + auto data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1314,13 +1315,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur } request->send(404); } -std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } -std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1351,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_json_(obj, obj->state, detail); + auto data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1370,13 +1371,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } -std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) { return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); } -std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) { return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1408,7 +1409,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); + auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1427,15 +1428,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE); } -std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL); } -std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1467,7 +1468,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->climate_json_(obj, detail); + auto data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1500,15 +1501,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } request->send(404); } -std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); } -std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); } -std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1641,7 +1642,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->lock_json_(obj, obj->state, detail); + auto data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1666,13 +1667,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } -std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) { return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); } -std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) { return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } -std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1700,7 +1701,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->valve_json_(obj, detail); + auto data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1746,13 +1747,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } -std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) { return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1785,7 +1786,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); + auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1825,19 +1826,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } request->send(404); } -std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), DETAIL_STATE); } -std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), DETAIL_ALL); } -std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1866,7 +1867,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->water_heater_json_(obj, detail); + auto data = this->water_heater_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1902,14 +1903,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons request->send(404); } -std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { return web_server->water_heater_json_(static_cast(source), DETAIL_STATE); } -std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->water_heater_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; @@ -1971,7 +1972,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->infrared_json_(obj, detail); + auto data = this->infrared_json_(obj, detail); request->send(200, ESPHOME_F("application/json"), data.c_str()); return; } @@ -2031,12 +2032,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur request->send(404); } -std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->infrared_json_(static_cast(source), DETAIL_ALL); } -std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2071,7 +2072,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa // Note: request->method() is always HTTP_GET here (canHandle ensures this) if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->event_json_(obj, StringRef(), detail); + auto data = this->event_json_(obj, StringRef(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -2081,16 +2082,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); } -std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); } // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson -std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); } -std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -2124,7 +2125,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->update_json_(obj, detail); + auto data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -2140,15 +2141,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { +json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { +json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6afe618b599..76c1c8b0bd5 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,7 @@ #include "list_entities.h" +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server_base/web_server_base.h" #ifdef USE_WEBSERVER #include "esphome/core/component.h" @@ -103,7 +104,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; can be forgotten. */ #if !defined(USE_ESP32) && defined(USE_ARDUINO) -using message_generator_t = std::string(WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(WebServer *, void *); class DeferredUpdateEventSourceList; class DeferredUpdateEventSource : public AsyncEventSource { @@ -257,7 +258,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_index_request(AsyncWebServerRequest *request); /// Return the webserver configuration as JSON. - std::string get_config_json(); + json::SerializationBuffer<> get_config_json(); #ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. @@ -279,8 +280,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string sensor_state_json_generator(WebServer *web_server, void *source); - static std::string sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SWITCH @@ -289,8 +290,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string switch_state_json_generator(WebServer *web_server, void *source); - static std::string switch_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BUTTON @@ -298,7 +299,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); // Buttons are stateless, so there is no button_state_json_generator - static std::string button_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_BINARY_SENSOR @@ -307,8 +308,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); - static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_FAN @@ -317,8 +318,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string fan_state_json_generator(WebServer *web_server, void *source); - static std::string fan_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LIGHT @@ -327,8 +328,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string light_state_json_generator(WebServer *web_server, void *source); - static std::string light_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT_SENSOR @@ -337,8 +338,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); - static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_COVER @@ -347,8 +348,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string cover_state_json_generator(WebServer *web_server, void *source); - static std::string cover_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_NUMBER @@ -356,8 +357,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string number_state_json_generator(WebServer *web_server, void *source); - static std::string number_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATE @@ -365,8 +366,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string date_state_json_generator(WebServer *web_server, void *source); - static std::string date_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_TIME @@ -374,8 +375,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string time_state_json_generator(WebServer *web_server, void *source); - static std::string time_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_DATETIME_DATETIME @@ -383,8 +384,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string datetime_state_json_generator(WebServer *web_server, void *source); - static std::string datetime_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_TEXT @@ -392,8 +393,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string text_state_json_generator(WebServer *web_server, void *source); - static std::string text_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_SELECT @@ -401,8 +402,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string select_state_json_generator(WebServer *web_server, void *source); - static std::string select_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_CLIMATE @@ -410,8 +411,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string climate_state_json_generator(WebServer *web_server, void *source); - static std::string climate_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_LOCK @@ -420,8 +421,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string lock_state_json_generator(WebServer *web_server, void *source); - static std::string lock_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_VALVE @@ -430,8 +431,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string valve_state_json_generator(WebServer *web_server, void *source); - static std::string valve_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -440,8 +441,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); - static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_WATER_HEATER @@ -450,22 +451,22 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a water_heater request under '/water_heater//'. void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string water_heater_state_json_generator(WebServer *web_server, void *source); - static std::string water_heater_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_INFRARED /// Handle an infrared request under '/infrared//transmit'. void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string infrared_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source); #endif #ifdef USE_EVENT void on_event(event::Event *obj) override; - static std::string event_state_json_generator(WebServer *web_server, void *source); - static std::string event_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source); /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -477,8 +478,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string update_state_json_generator(WebServer *web_server, void *source); - static std::string update_all_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source); + static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source); #endif /// Override the web handler's canHandle method. @@ -586,71 +587,74 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { private: #ifdef USE_SENSOR - std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); #endif #ifdef USE_SWITCH - std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); + json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON - std::string button_json_(button::Button *obj, JsonDetail start_config); + json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config); #endif #ifdef USE_BINARY_SENSOR - std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); + json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, + JsonDetail start_config); #endif #ifdef USE_FAN - std::string fan_json_(fan::Fan *obj, JsonDetail start_config); + json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT - std::string light_json_(light::LightState *obj, JsonDetail start_config); + json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config); #endif #ifdef USE_TEXT_SENSOR - std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); + json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config); #endif #ifdef USE_COVER - std::string cover_json_(cover::Cover *obj, JsonDetail start_config); + json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER - std::string number_json_(number::Number *obj, float value, JsonDetail start_config); + json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATE - std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_TIME - std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_DATETIME_DATETIME - std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); #endif #ifdef USE_TEXT - std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); + json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_SELECT - std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config); + json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config); #endif #ifdef USE_CLIMATE - std::string climate_json_(climate::Climate *obj, JsonDetail start_config); + json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config); #endif #ifdef USE_LOCK - std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); + json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif #ifdef USE_VALVE - std::string valve_json_(valve::Valve *obj, JsonDetail start_config); + json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config); #endif #ifdef USE_ALARM_CONTROL_PANEL - std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); + json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config); #endif #ifdef USE_EVENT - std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); + json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config); #endif #ifdef USE_WATER_HEATER - std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); + json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); #endif #ifdef USE_INFRARED - std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config); + json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config); #endif #ifdef USE_UPDATE - std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); + json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif }; diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 1798159e7f7..4034a22586b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -563,7 +563,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // Configure reconnect timeout and send config // this should always go through since the tcp send buffer is empty on connect - std::string message = ws->get_config_json(); + auto message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING @@ -617,7 +617,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g void AsyncEventSourceResponse::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); - std::string message = de.message_generator_(web_server_, de.source_); + auto message = de.message_generator_(web_server_, de.source_); if (this->try_send_nodefer(message.c_str(), "state")) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); @@ -854,7 +854,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e // trying to send first deq_push_back_with_dedup_(source, message_generator); } else { - std::string message = message_generator(web_server_, source); + auto message = message_generator(web_server_, source); if (!this->try_send_nodefer(message.c_str(), "state")) { deq_push_back_with_dedup_(source, message_generator); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 74601ffda88..76ddfa35fda 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -16,6 +16,7 @@ #include #ifdef USE_WEBSERVER +#include "esphome/components/json/json_util.h" #include "esphome/components/web_server/list_entities.h" #endif @@ -250,7 +251,7 @@ class AsyncWebHandler { class AsyncEventSource; class AsyncEventSourceResponse; -using message_generator_t = std::string(esphome::web_server::WebServer *, void *); +using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *); /* This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function diff --git a/tests/components/json/common.yaml b/tests/components/json/common.yaml index c36c7f2a5a6..c4bf6c38316 100644 --- a/tests/components/json/common.yaml +++ b/tests/components/json/common.yaml @@ -4,15 +4,16 @@ interval: - interval: 60s then: - lambda: |- - // Test build_json - std::string json_str = esphome::json::build_json([](JsonObject root) { + // Test build_json - returns SerializationBuffer, use auto to avoid heap allocation + auto json_buf = esphome::json::build_json([](JsonObject root) { root["sensor"] = "temperature"; root["value"] = 23.5; root["unit"] = "°C"; }); - ESP_LOGD("test", "Built JSON: %s", json_str.c_str()); + ESP_LOGD("test", "Built JSON: %s", json_buf.c_str()); - // Test parse_json + // Test parse_json - implicit conversion to std::string for backward compatibility + std::string json_str = json_buf; bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { if (root["sensor"].is() && root["value"].is()) { const char* sensor = root["sensor"]; @@ -26,10 +27,10 @@ interval: }); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); - // Test JsonBuilder class + // Test JsonBuilder class - returns SerializationBuffer esphome::json::JsonBuilder builder; JsonObject obj = builder.root(); obj["test"] = "direct_builder"; obj["count"] = 42; - std::string result = builder.serialize(); + auto result = builder.serialize(); ESP_LOGD("test", "JsonBuilder result: %s", result.c_str()); From 17a810b939ba7ab0019fa65149e2db2622d2466c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 0075/2056] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de86634..afceec6c548 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From cceb109303f16a8723b11e3e9bf9a26f985be197 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 0076/2056] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a69..ea7a09fee60 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From 1b4de55efda47b59e1572f432ef40b932311da38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 0077/2056] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f66..5e62c0a4107 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d29288547ec2d5b9e68b61c2186522efaad041b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:33 -0600 Subject: [PATCH 0078/2056] [core] Use constexpr for PROGMEM arrays (#14127) Co-authored-by: Claude Opus 4.6 --- esphome/cpp_generator.py | 2 +- tests/unit_tests/test_cpp_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 83f2d6cf81f..fe666bdd6e1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -424,7 +424,7 @@ class ProgmemAssignmentExpression(AssignmentExpression): super().__init__(type_, "", name, rhs) def __str__(self): - return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" + return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}" class StaticConstAssignmentExpression(AssignmentExpression): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 8755e6e2a11..049d21027fa 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -325,7 +325,7 @@ class TestStatements: ), ( cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar"), - 'static const uint16_t foo[] PROGMEM = "bar"', + 'static constexpr uint16_t foo[] PROGMEM = "bar"', ), ), ) From 94712b3961dd962d0a43636301c5edccd08121b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:44 -0600 Subject: [PATCH 0079/2056] [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/gpio.py | 4 ++-- esphome/components/web_server/__init__.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2e8d6496bce..43508afaf92 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -202,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 294a5e0a152..9305a2de613 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -280,10 +280,8 @@ def add_resource_as_progmem( content_encoded = gzip.compress(content_encoded) content_encoded_size = len(content_encoded) bytes_as_int = ", ".join(str(x) for x in content_encoded) - uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" - size_t = ( - f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" - ) + uint8_t = f"constexpr uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" + size_t = f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" cg.add_global(cg.RawExpression(uint8_t)) cg.add_global(cg.RawExpression(size_t)) From c1265a9490711813a6036b8a772dd36e3e871089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 18:54:57 -0600 Subject: [PATCH 0080/2056] [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129) Co-authored-by: Claude Opus 4.6 --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/bme680/bme680.cpp | 8 +++--- .../components/captive_portal/captive_index.h | 4 +-- esphome/components/display/display.cpp | 6 ++-- esphome/components/ili9xxx/ili9xxx_init.h | 28 +++++++++---------- esphome/components/max7219/max7219.cpp | 2 +- esphome/components/max7219digit/max7219font.h | 2 +- .../components/ssd1306_base/ssd1306_base.cpp | 2 +- esphome/components/st7735/st7735.cpp | 2 +- esphome/components/tm1621/tm1621.cpp | 2 +- esphome/components/tm1637/tm1637.cpp | 2 +- esphome/components/tm1638/sevenseg.h | 2 +- .../components/web_server/server_index_v2.h | 4 +-- .../components/web_server/server_index_v3.h | 4 +-- 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 492988128a9..2aad732f7fa 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -19,7 +19,7 @@ namespace esphome::api { static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 -static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; #else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 5e52c84b3db..e3cd80de004 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -22,11 +22,11 @@ static const uint8_t BME680_REGISTER_CHIPID = 0xD0; static const uint8_t BME680_REGISTER_FIELD0 = 0x1D; -const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, - 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, + 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; -const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, - -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, + -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f4..a81edc19005 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 53a087803c7..2bd7d036006 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -810,9 +810,9 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } -const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' - {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' - {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' +constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 7b176ed57aa..f0c6a94a656 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { // clang-format off -static const uint8_t PROGMEM INITCMD_M5STACK[] = { +static constexpr uint8_t PROGMEM INITCMD_M5STACK[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -37,7 +37,7 @@ static const uint8_t PROGMEM INITCMD_M5STACK[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_M5CORE[] = { +static constexpr uint8_t PROGMEM INITCMD_M5CORE[] = { ILI9XXX_SETEXTC, 3, 0xFF,0x93,0x42, // Turn on the external command ILI9XXX_PWCTR1 , 2, 0x12, 0x12, ILI9XXX_PWCTR2 , 1, 0x03, @@ -56,7 +56,7 @@ static const uint8_t PROGMEM INITCMD_M5CORE[] = { -static const uint8_t PROGMEM INITCMD_ILI9341[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9341[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -86,7 +86,7 @@ static const uint8_t PROGMEM INITCMD_ILI9341[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9481[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -105,7 +105,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9481_18[] = { ILI9XXX_SLPOUT , 0x80, // Exit sleep mode ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, @@ -124,7 +124,7 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ILI9486[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, ILI9XXX_PWCTR3, 1, 0x44, @@ -173,7 +173,7 @@ static const uint8_t INITCMD_WAVESHARE_RES_3_5[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { +static constexpr uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, @@ -206,7 +206,7 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { 0x00 // end }; -static const uint8_t PROGMEM INITCMD_ST7796[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7796[] = { // This ST7796S initilization routine was copied from https://github.com/prenticedavid/Adafruit_ST7796S_kbv/blob/master/Adafruit_ST7796S_kbv.cpp ILI9XXX_SWRESET, 0x80, // Soft reset, then delay 150 ms ILI9XXX_CSCON, 1, 0xC3, // ?? Unlock Manufacturer @@ -226,7 +226,7 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOX[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOX[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -256,7 +256,7 @@ static const uint8_t PROGMEM INITCMD_S3BOX[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { +static constexpr uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0xEF, 3, 0x03, 0x80, 0x02, 0xCF, 3, 0x00, 0xC1, 0x30, 0xED, 4, 0x64, 0x03, 0x12, 0x81, @@ -286,7 +286,7 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7789V[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7789V[] = { ILI9XXX_SLPOUT , 0x80, // Exit Sleep ILI9XXX_DISPON , 0x80, // Display on ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR @@ -313,7 +313,7 @@ static const uint8_t PROGMEM INITCMD_ST7789V[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9A01A[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9A01A[] = { 0xEF, 0, 0xEB, 1, 0x14, // ? 0xFE, 0, @@ -367,7 +367,7 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_GC9D01N[] = { +static constexpr uint8_t PROGMEM INITCMD_GC9D01N[] = { // Enable Inter_command 0xFE, 0, // Inter Register Enable 1 (FEh) 0xEF, 0, // Inter Register Enable 2 (EFh) @@ -426,7 +426,7 @@ static const uint8_t PROGMEM INITCMD_GC9D01N[] = { 0x00 // End of list }; -static const uint8_t PROGMEM INITCMD_ST7735[] = { +static constexpr uint8_t PROGMEM INITCMD_ST7735[] = { ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms ILI9XXX_DELAY(10), ILI9XXX_SLPOUT , 0, // Exit Sleep, delay diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index d701e6fc867..bec62ea0057 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -15,7 +15,7 @@ static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; static const uint8_t MAX7219_REGISTER_TEST = 0x0F; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { +constexpr uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 22d64d1ecde..53674dc60f1 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -7,7 +7,7 @@ namespace max7219digit { // bit patterns for the CP437 font -const uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { +constexpr uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index be99bd93da1..5bd83ec8a8e 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -49,7 +49,7 @@ struct ModelDimensions { uint8_t width; uint8_t height; }; -static const ModelDimensions MODEL_DIMS[] PROGMEM = { +static constexpr ModelDimensions MODEL_DIMS[] PROGMEM = { {128, 32}, // SSD1306_MODEL_128_32 {128, 64}, // SSD1306_MODEL_128_64 {96, 16}, // SSD1306_MODEL_96_16 diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 1a74b5ce1e2..58459b79bb0 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -68,7 +68,7 @@ static const uint8_t ST7735_GMCTRP1 = 0xE0; static const uint8_t ST7735_GMCTRN1 = 0xE1; // clang-format off -static const uint8_t PROGMEM +static constexpr uint8_t PROGMEM BCMD[] = { // Init commands for 7735B screens 18, // 18 commands in list: ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index 68599738576..c82d3064601 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -23,7 +23,7 @@ enum Tm1621Device { TM1621_USER, TM1621_POWR316D, TM1621_THR316D }; const uint8_t TM1621_COMMANDS[] = {TM1621_SYS_EN, TM1621_LCD_ON, TM1621_BIAS, TM1621_TIMER_DIS, TM1621_WDT_DIS, TM1621_TONE_OFF, TM1621_IRQ_DIS}; -const char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; +constexpr char TM1621_KCHAR[] PROGMEM = {"0|1|2|3|4|5|6|7|8|9|-| "}; // 0 1 2 3 4 5 6 7 8 9 - off const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x6F, 0x51, 0x7F, 0x7B, 0x20, 0x00}, {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 49da01472f2..f9c876f40c5 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -27,7 +27,7 @@ const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address // --- // D X // XABCDEFG -const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { +constexpr uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h index e20a55a69f6..a4c16c7422c 100644 --- a/esphome/components/tm1638/sevenseg.h +++ b/esphome/components/tm1638/sevenseg.h @@ -4,7 +4,7 @@ namespace esphome { namespace tm1638 { namespace TM1638Translation { -const unsigned char SEVEN_SEG[] PROGMEM = { +constexpr unsigned char SEVEN_SEG[] PROGMEM = { 0x00, /* (space) */ 0x86, /* ! */ 0x22, /* " */ diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index cc37db6a6bd..b5dac9ae4c3 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -697,7 +697,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index bd47071dce5..1f61b19fb5e 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4104,7 +4104,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x37, 0x7a, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, From afbc45bf32fe8146183d0d44fc16213cdebddbdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:35:42 -0600 Subject: [PATCH 0081/2056] [e131] Drain all queued packets per loop iteration (#14133) --- esphome/components/e131/e131.cpp | 26 +++++--------------------- esphome/components/e131/e131.h | 9 +++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 941927122cc..a7a695c1679 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -70,27 +70,12 @@ void E131Component::loop() { E131Packet packet; int universe = 0; uint8_t buf[1460]; + ssize_t len; -#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - ssize_t len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - return; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - + // Drain all queued packets so multi-universe frames are applied + // atomically before the light writes. Without this, each universe + // packet would trigger a separate full-strip write causing tearing. + while ((len = this->read_(buf, sizeof(buf))) > 0) { if (!this->packet_(buf, (size_t) len, universe, packet)) { ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); continue; @@ -100,7 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index fee447b678e..8f0b808946c 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -46,6 +46,15 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: + inline ssize_t read_(uint8_t *buf, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + return this->socket_->read(buf, len); +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.parsePacket()) + return -1; + return this->udp_.read(buf, len); +#endif + } bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); From 7a2a149061c3b9480a3d1900f3e8cad1f1ad75ac Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:43:29 -0500 Subject: [PATCH 0082/2056] [esp32] Bump ESP-IDF to 5.5.3 (#14122) Co-authored-by: Claude Opus 4.6 --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 17 +++++++++++++---- esphome/components/esp32_ble/__init__.py | 15 +++++++++------ platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index d6d401ee661..df584fa7164 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e +3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b1b3f0dc168..cdde1c4ed50 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -643,7 +643,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 2), + cv.Version(3, 3, 7): cv.Version(5, 5, 3), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +662,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 2), - "latest": cv.Version(5, 5, 2), - "dev": cv.Version(5, 5, 2), + "recommended": cv.Version(5, 5, 3), + "latest": cv.Version(5, 5, 3), + "dev": cv.Version(5, 5, 3), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), @@ -1471,6 +1472,14 @@ async def to_code(config): f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 + # from y to n. PlatformIO uses sections.ld.in (for rev <3) or + # sections.rev3.ld.in (for rev >=3) based on board definition. + # Set the sdkconfig option to match the board's revision. + if variant == VARIANT_ESP32P4: + is_rev3 = "_r3" in config[CONF_BOARD] + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d2020ada223..80fcf051b9a 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -413,18 +413,21 @@ def final_validation(config): add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) - # Check if BLE Server is needed - has_ble_server = "esp32_ble_server" in full_config - # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) has_ble_client = ( "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled - # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a + # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED + # guard, causing a compile error when both are disabled. + # Additionally, when GATT Client is enabled, GATT Server must also be enabled + # as an internal dependency in the Bluedroid stack. # See: https://github.com/espressif/esp-idf/issues/17724 - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: + # has_ble_server = "esp32_ble_server" in full_config + # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index 09b3d8722de..fdd6a364284 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz framework = espidf lib_deps = From b67b2cc3ab86ebbc7cfb3d0a893bf96bd50ef9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 0083/2056] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271a..2af45235a39 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d05..44e5912b2ad 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 00000000000..d5ffbe1295b --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 00000000000..68b1dd68817 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From a2f0607c1e860d1173c80e0a763fda99e71e69f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 0084/2056] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768ae..a3c2193d672 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 5af871acce9c33284d483620131056af2ad48071 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 0085/2056] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c59114..358793fe64f 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From efe54e3b5e3d3ddefee703048ff37bb4ec9b0b5f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 0086/2056] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d672..f8f782f804a 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb764..687ed21d1d2 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a39..d30c1647695 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2ad..30f96c0a9c1 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd68817..cb97f633bfb 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From efe8a6c8ebfd3e831b9757356db7f08fc331472e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 17 Feb 2026 18:45:21 +0100 Subject: [PATCH 0087/2056] [esp32_ble_server] fix infinitely large characteristic value (#14011) --- .../components/esp32_ble_server/__init__.py | 2 +- .../esp32_ble_server/ble_characteristic.cpp | 28 +++++++++++++++---- .../esp32_ble_server/ble_characteristic.h | 1 - 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522facb..cb494ed1bc1 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0f..a1b1ff94bb3 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789c..c2cdb1660ce 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -77,7 +77,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; From 8c0cc3a2d8bfc978865e940b03e30262f9331ead Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:00 -0600 Subject: [PATCH 0088/2056] [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) --- esphome/components/udp/__init__.py | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index bfaa5f25163..c9586d0b956 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -14,6 +14,7 @@ import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.core import ID from esphome.cpp_generator import MockObj +from esphome.types import ConfigType CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -65,33 +66,47 @@ RELOCATED = { ) } -CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.Any( - cv.port, - cv.Schema( + +def _consume_udp_sockets(config: ConfigType) -> ConfigType: + """Register socket needs for UDP component.""" + from esphome.components import socket + + # UDP uses up to 2 sockets: 1 broadcast + 1 listen + # Whether each is used depends on code generation, so register worst case + socket.consume_sockets(2, "udp")(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( { - cv.Required(CONF_LISTEN_PORT): cv.port, - cv.Required(CONF_BROADCAST_PORT): cv.port, + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), } ), - ), - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(trigger_args) - ), - } - ), - } -).extend(RELOCATED) + } + ).extend(RELOCATED), + _consume_udp_sockets, +) async def register_udp_client(var, config): From e4aa23abaac53320b89d99ecd39efe9e60cbe7bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:01:37 -0600 Subject: [PATCH 0089/2056] [web_server] Double socket allocation to prevent connection exhaustion (#14067) --- esphome/components/web_server/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8b02a6baeec..294a5e0a152 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 2 concurrent client connections - # (browser makes 2 connections for page + event stream) - sockets_needed = 3 + # Web server needs 1 listening socket + typically 5 concurrent client connections + # (browser opens connections for page resources, SSE event stream, and POST + # requests for entity control which may linger before closing) + sockets_needed = 6 socket.consume_sockets(sockets_needed, "web_server")(config) return config From 887172d663c5f7bd1ffc4a4ac35f99eecccf2a6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 19:08:53 -0600 Subject: [PATCH 0090/2056] [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 8ac5a28d8f5..ef4cc980f66 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT -#include +#include #include #endif @@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t apb_freq; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From cb8b14e64b6c4fbd01f96e75106f0b2ed452ffcc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:00:24 -0600 Subject: [PATCH 0091/2056] [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) --- esphome/components/web_server/web_server.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6be..c894d32a4bc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1913,6 +1913,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe JsonArray modes = root[ESPHOME_F("modes")].to(); for (auto m : traits.get_supported_modes()) modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + root[ESPHOME_F("min_temp")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temp")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); this->add_sorting_info_(root, obj); } @@ -1935,10 +1938,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe root[ESPHOME_F("target_temperature")] = target; } - root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); - root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); - root[ESPHOME_F("step")] = traits.get_target_temperature_step(); - if (traits.get_supports_away_mode()) { root[ESPHOME_F("away")] = obj->is_away(); } From 2491b4f85c1825b7dd8bc4273eafca643cf8c120 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Feb 2026 21:32:37 -0600 Subject: [PATCH 0092/2056] [ld2420] Use constexpr for compile-time constants (#14079) --- esphome/components/ld2420/ld2420.cpp | 118 +++++++++++++-------------- esphome/components/ld2420/ld2420.h | 6 +- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 69b69f4a61e..cf78a1a4609 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -63,73 +63,73 @@ namespace esphome::ld2420 { static const char *const TAG = "ld2420"; // Local const's -static const uint16_t REFRESH_RATE_MS = 1000; +static constexpr uint16_t REFRESH_RATE_MS = 1000; // Command sets -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; +static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE; +static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF; +static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002; +static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008; +static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020; +static constexpr uint16_t CMD_READ_REGISTER = 0x0002; +static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013; +static constexpr uint16_t CMD_READ_VERSION = 0x0000; +static constexpr uint16_t CMD_RESTART = 0x0068; +static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001; +static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012; -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static constexpr uint8_t CMD_MAX_BYTES = 0x64; +static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static constexpr uint8_t LD2420_ERROR_NONE = 0x00; +static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01; // Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; +static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000; +static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001; +static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004; +static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static constexpr uint16_t FACTORY_TIMEOUT = 120; +static constexpr uint16_t FACTORY_MIN_GATE = 1; +static constexpr uint16_t FACTORY_MAX_GATE = 12; // COMMAND_BYTE Header & Footer -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const int CALIBRATE_VERSION_MIN = 154; -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304; +static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static constexpr int CALIBRATE_VERSION_MIN = 154; +static constexpr uint8_t CMD_FRAME_COMMAND = 6; +static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4; +static constexpr uint8_t CMD_FRAME_STATUS = 7; +static constexpr uint8_t CMD_ERROR_WORD = 8; +static constexpr uint8_t ENERGY_SENSOR_START = 9; +static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4; static const char *const OP_NORMAL_MODE_STRING = "Normal"; static const char *const OP_SIMPLE_MODE_STRING = "Simple"; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 6d81f864970..02250c59114 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,9 +20,9 @@ namespace esphome::ld2420 { -static const uint8_t CALIBRATE_SAMPLES = 64; -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 16; +static constexpr uint8_t CALIBRATE_SAMPLES = 64; +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, From 25b14f995309a7dfbb4857d53f8c9a67aa8a7308 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 09:27:05 -0600 Subject: [PATCH 0093/2056] [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) --- esphome/components/e131/e131.cpp | 31 ++++++++++++++++++++++++- esphome/components/e131/e131.h | 8 +++++++ esphome/components/e131/e131_packet.cpp | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 41878579014..941927122cc 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,6 +55,13 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } @@ -59,19 +71,36 @@ void E131Component::loop() { int universe = 0; uint8_t buf[1460]; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) ssize_t len = this->socket_->read(buf, sizeof(buf)); if (len == -1) { return; } if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zd.", len); + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); return; } if (!this->process_(universe, packet)) { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + while (auto packet_size = this->udp_.parsePacket()) { + auto len = this->udp_.read(buf, sizeof(buf)); + if (len <= 0) + continue; + + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } + + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d4b272eae2e..72da9ddebeb 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,11 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include @@ -45,7 +49,11 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; std::map universe_consumers_; }; diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index ed081e57581..aa5c740454e 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif for (auto universe : universe_consumers_) { if (!universe.second) From 2d2178c90a7d8357a1b51cd51c02b6f800d70559 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:00:34 -0500 Subject: [PATCH 0094/2056] [socket] Fix IPv6 compilation error on host platform (#14101) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/socket.cpp | 10 ++++++++-- tests/components/socket/common.yaml | 11 +++++++++++ tests/components/socket/test-ipv6.esp32-idf.yaml | 4 ++++ tests/components/socket/test-ipv6.host.yaml | 4 ++++ tests/components/socket/test.esp32-idf.yaml | 1 + tests/components/socket/test.host.yaml | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/components/socket/common.yaml create mode 100644 tests/components/socket/test-ipv6.esp32-idf.yaml create mode 100644 tests/components/socket/test-ipv6.host.yaml create mode 100644 tests/components/socket/test.esp32-idf.yaml create mode 100644 tests/components/socket/test.host.yaml diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 2fcc162eadc..6154c497e0c 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s #if USE_NETWORK_IPV6 else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) { const auto *addr = reinterpret_cast(addr_ptr); -#ifndef USE_SOCKET_IMPL_LWIP_TCP - // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) +#ifdef USE_HOST + // Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union) + if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) && + esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#elif !defined(USE_SOCKET_IMPL_LWIP_TCP) + // Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout) if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { diff --git a/tests/components/socket/common.yaml b/tests/components/socket/common.yaml new file mode 100644 index 00000000000..aaf49f16112 --- /dev/null +++ b/tests/components/socket/common.yaml @@ -0,0 +1,11 @@ +substitutions: + network_enable_ipv6: "false" + +socket: + +wifi: + ssid: MySSID + password: password1 + +network: + enable_ipv6: ${network_enable_ipv6} diff --git a/tests/components/socket/test-ipv6.esp32-idf.yaml b/tests/components/socket/test-ipv6.esp32-idf.yaml new file mode 100644 index 00000000000..da1324b17ef --- /dev/null +++ b/tests/components/socket/test-ipv6.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "true" + +<<: !include common.yaml diff --git a/tests/components/socket/test-ipv6.host.yaml b/tests/components/socket/test-ipv6.host.yaml new file mode 100644 index 00000000000..fdd52c574e1 --- /dev/null +++ b/tests/components/socket/test-ipv6.host.yaml @@ -0,0 +1,4 @@ +socket: + +network: + enable_ipv6: true diff --git a/tests/components/socket/test.esp32-idf.yaml b/tests/components/socket/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/socket/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.host.yaml b/tests/components/socket/test.host.yaml new file mode 100644 index 00000000000..e0c5d7cea3a --- /dev/null +++ b/tests/components/socket/test.host.yaml @@ -0,0 +1,3 @@ +socket: + +network: From a343ff19895c0699671766f7ee8a0b0ec81bd321 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:05 -0500 Subject: [PATCH 0095/2056] [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 52f5f44d410..935d2004d49 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -218,12 +218,19 @@ def _validate(config): ) elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) del config[CONF_CLK_MODE] elif CONF_CLK not in config: raise cv.Invalid("'clk' is a required option for [ethernet].") From ac76fc44098f2a7adb74b212093c9353ff0c8912 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:26 -0500 Subject: [PATCH 0096/2056] [pulse_counter] Fix build failure when use_pcnt is false (#14111) Co-authored-by: Claude Opus 4.6 --- .../components/pulse_counter/pulse_counter_sensor.h | 4 ++-- .../pulse_counter/test-no-pcnt.esp32-idf.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index a7913d5d66b..7a68858099c 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -8,10 +8,10 @@ #if defined(USE_ESP32) #include -#ifdef SOC_PCNT_SUPPORTED +#if defined(SOC_PCNT_SUPPORTED) && __has_include() #include #define HAS_PCNT -#endif // SOC_PCNT_SUPPORTED +#endif // defined(SOC_PCNT_SUPPORTED) && __has_include() #endif // USE_ESP32 namespace esphome { diff --git a/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml new file mode 100644 index 00000000000..cd15cc781d6 --- /dev/null +++ b/tests/components/pulse_counter/test-no-pcnt.esp32-idf.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: pulse_counter + name: Pulse Counter + pin: 4 + use_pcnt: false + count_mode: + rising_edge: INCREMENT + falling_edge: DECREMENT + internal_filter: 13us + update_interval: 15s From d78496321e2a65208fe83d7ea93d7107914419b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 11:41:00 -0600 Subject: [PATCH 0097/2056] [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32_ble/__init__.py | 10 ++++++++++ tests/components/esp32_ble/test.esp32-c2-idf.yaml | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-c2-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf2..d2020ada223 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -387,6 +388,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() diff --git a/tests/components/esp32_ble/test.esp32-c2-idf.yaml b/tests/components/esp32_ble/test.esp32-c2-idf.yaml new file mode 100644 index 00000000000..f8defaf28ff --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-c2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false From 0fc09462ff73e82b380389c3ae1f885947fc70f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:45:04 -0500 Subject: [PATCH 0098/2056] [safe_mode] Log brownout as reset reason on OTA rollback (#14113) Co-authored-by: Claude Opus 4.6 --- esphome/components/safe_mode/safe_mode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index f32511531a0..6cae4bf9d5d 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -11,6 +11,7 @@ #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include +#include #endif namespace esphome::safe_mode { @@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() { "OTA rollback detected! Rolled back from partition '%s'\n" "The device reset before the boot was marked successful", last_invalid->label); + if (esp_reset_reason() == ESP_RST_BROWNOUT) { + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + } } #endif } From f412ab4f8b954ca7f81125237df452832decaa56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:14:48 -0500 Subject: [PATCH 0099/2056] [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e865de86634..afceec6c548 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,4 +1,5 @@ import logging +import math from esphome import automation from esphome.automation import Condition @@ -493,6 +494,13 @@ async def to_code(config): cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + if CORE.is_esp32: + # Set PHY max TX power to match output_power so calibration also uses + # reduced power. This prevents brownout during PHY init on marginal + # power supplies, which is critical for OTA updates with rollback enabled. + # Kconfig range is 10-20, ESPHome allows 8.5-20.5 + phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER]))) + add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power) # enable_on_boot defaults to true in C++ - only set if false if not config[CONF_ENABLE_ON_BOOT]: cg.add(var.set_enable_on_boot(False)) From 7bdeb32a8a1ca43e07b64e80daa0afa13bc68206 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 17:48:18 -0600 Subject: [PATCH 0100/2056] [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6c242220a69..ea7a09fee60 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -19,6 +19,13 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual state from the boot console that requires +/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) { // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks // UART on default UART0 pins that may have residual state from boot console. // Reset these pins before configuring UART to ensure they're in a clean state. - if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(tx)) { gpio_reset_pin(static_cast(tx)); } - if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + if (is_default_uart0_pin(rx)) { gpio_reset_pin(static_cast(rx)); } - // Setup pins after reset to preserve open drain/pullup/pulldown flags + // Setup pins after reset to configure GPIO direction and pull resistors. + // For UART0 default pins, setup() must always be called because gpio_reset_pin() + // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), + // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for + // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). + // For other pins, only call setup() if pull or open-drain flags are set to avoid + // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; From e7e1acc0a2fb1e1c3c5ce7fc448cca2050bb2d17 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:12:37 -0500 Subject: [PATCH 0101/2056] [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132) Co-authored-by: Claude Opus 4.6 --- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ef4cc980f66..5e62c0a4107 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -117,7 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } if (this->filter_us != 0) { - uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq(); + uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u); pcnt_glitch_filter_config_t filter_config = { .max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns), }; From d19c1b689af4dbbe85e24f02580e586f6025ea73 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:56:20 -0500 Subject: [PATCH 0102/2056] [ld2450] Add frame header synchronization to fix initialization regression (#14135) Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ld2450/ld2450.cpp | 22 ++- esphome/components/ld2450/ld2450.h | 1 + tests/components/ld2450/common.h | 61 +++++++ tests/components/ld2450/ld2450_readline.cpp | 181 ++++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 tests/components/ld2450/common.h create mode 100644 tests/components/ld2450/ld2450_readline.cpp diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 1ea5c18271a..2af45235a39 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,15 +769,33 @@ void LD2450Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from accumulating mid-frame data after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index fe69cd81d05..44e5912b2ad 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h new file mode 100644 index 00000000000..d5ffbe1295b --- /dev/null +++ b/tests/components/ld2450/common.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/ld2450/ld2450.h" +#include "esphome/components/uart/uart_component.h" + +namespace esphome::ld2450::testing { + +// Mock UART component to satisfy UARTDevice parent requirement. +class MockUARTComponent : public uart::UARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override {} + MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(size_t, available, (), (override)); + MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +// Expose protected members for testing. +class TestableLD2450 : public LD2450Component { + public: + using LD2450Component::buffer_data_; + using LD2450Component::buffer_pos_; + using LD2450Component::readline_; + + void feed(const std::vector &data) { + for (uint8_t byte : data) { + this->readline_(byte); + } + } +}; + +// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes +// All-zero targets means no presence detected. +inline std::vector make_periodic_frame(uint8_t fill = 0x00) { + std::vector frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER + for (int i = 0; i < 24; i++) { + frame.push_back(fill); // 3 targets * 8 bytes + } + frame.push_back(0x55); // DATA_FRAME_FOOTER + frame.push_back(0xCC); + return frame; +} + +// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful. +// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes +inline std::vector make_ack_frame() { + return { + 0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER + 0x04, 0x00, // length = 4 + 0xFF, 0x01, // command = enable_conf, status = success + 0x00, 0x00, // result = ok + 0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER + }; +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp new file mode 100644 index 00000000000..68b1dd68817 --- /dev/null +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -0,0 +1,181 @@ +#include "common.h" + +namespace esphome::ld2450::testing { + +class LD2450ReadlineTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->mock_uart_); + // Ensure clean state + ASSERT_EQ(this->ld2450_.buffer_pos_, 0); + } + + MockUARTComponent mock_uart_; + TestableLD2450 ld2450_; +}; + +// --- Good data tests --- + +TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) { + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // After a complete valid frame, buffer should be reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) { + auto frame = make_ack_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) { + auto frame = make_periodic_frame(); + for (int i = 0; i < 5; i++) { + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed"; + } +} + +TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { + auto periodic = make_periodic_frame(); + auto ack = make_ack_frame(); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(ack); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + this->ld2450_.feed(periodic); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Garbage rejection tests --- + +TEST_F(LD2450ReadlineTest, GarbageDiscarded) { + // Feed bytes that don't match any header start byte + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; + this->ld2450_.feed(garbage); + // Header sync should discard all of these + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; + this->ld2450_.feed(garbage); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Header synchronization tests --- + +TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { + // Start of a data frame header, then invalid byte + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) + // Parser should have reset + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { + // Start of a command frame header, then invalid byte + this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { + // Partial header that fails, then a complete valid frame + this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { + // Start data header, mismatch at byte 2, but mismatch byte is start of command header + this->ld2450_.feed({0xAA, 0xFF}); + EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header + + this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] + // Parser should reset and start new frame with 0xFD + EXPECT_EQ(this->ld2450_.buffer_pos_, 1); + EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); +} + +// --- Mid-frame / overflow recovery tests --- + +TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { + // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) + // These bytes would be part of target data in a real frame + std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; + this->ld2450_.feed(mid_frame); + // All discarded (none match header start bytes) + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now feed a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, OverflowRecovery) { + // Feed a valid data frame header followed by enough filler to cause overflow. + // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x11); // Filler that won't match any footer + } + // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Feed a valid frame and verify recovery + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { + // Simulate the bug scenario: repeated overflows should not prevent recovery. + // Feed 3 rounds of overflow-inducing data. + for (int round = 0; round < 3; round++) { + std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; + for (int i = 0; i < 37; i++) { + overflow_data.push_back(0x22); + } + this->ld2450_.feed(overflow_data); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; + } + + // Parser should still recover and process a valid frame + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { + // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) + // followed by normal periodic data. + // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage + std::vector restart_noise = { + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data + 0x55, 0xCC, // stale footer bytes + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + }; + + this->ld2450_.feed(restart_noise); + // All garbage should be discarded + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + // Now the LD2450 starts sending valid frames + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +} // namespace esphome::ld2450::testing From 49afe53a2cfae55feb304695ee1735a5f0e5bc43 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:04:38 -0500 Subject: [PATCH 0103/2056] [ld2410] Add frame header synchronization to readline_() (#14136) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 95a04f768ae..a3c2193d672 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,15 +601,33 @@ void LD2410Component::readline_(int readch) { return; // No data available } + // Frame header synchronization: verify first 4 bytes match a known frame header. + // This prevents the parser from getting stuck in an overflow loop after losing sync + // (e.g. after module restart or UART noise). + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { + const uint8_t byte = static_cast(readch); + // Verify header bytes match the frame type established by byte 0 + if (this->buffer_pos_ > 0) { + const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; + if (byte != expected[this->buffer_pos_]) { + this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame + } + } + // First byte must match start of a data or command frame header + if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { + return; + } + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { - // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; + return; } - if (this->buffer_pos_ < 4) { + if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { From 4c8e0575f94fb660bba85403347b999a424abe92 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:36:12 -0500 Subject: [PATCH 0104/2056] [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 02250c59114..358793fe64f 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -21,7 +21,9 @@ namespace esphome::ld2420 { static constexpr uint8_t CALIBRATE_SAMPLES = 64; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands +// inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; static constexpr uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { From 28d510191c150ac3163bfdc80f23738c2cddb908 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:25:25 -0500 Subject: [PATCH 0105/2056] [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2410/ld2410.cpp | 19 +-- esphome/components/ld2410/ld2410.h | 6 +- esphome/components/ld2450/ld2450.cpp | 19 +-- esphome/components/ld2450/ld2450.h | 8 +- tests/components/ld2450/ld2450_readline.cpp | 158 ++++++++------------ 5 files changed, 72 insertions(+), 138 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index a3c2193d672..f8f782f804a 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -601,28 +601,11 @@ void LD2410Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from getting stuck in an overflow loop after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index efe585fb764..687ed21d1d2 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -33,8 +33,10 @@ namespace esphome::ld2410 { using namespace ld24xx; -static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 50; +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 2af45235a39..d30c1647695 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -769,28 +769,11 @@ void LD2450Component::readline_(int readch) { return; // No data available } - // Frame header synchronization: verify first 4 bytes match a known frame header. - // This prevents the parser from accumulating mid-frame data after losing sync - // (e.g. after module restart or UART noise). - if (this->buffer_pos_ < HEADER_FOOTER_SIZE) { - const uint8_t byte = static_cast(readch); - // Verify header bytes match the frame type established by byte 0 - if (this->buffer_pos_ > 0) { - const uint8_t *expected = (this->buffer_data_[0] == DATA_FRAME_HEADER[0]) ? DATA_FRAME_HEADER : CMD_FRAME_HEADER; - if (byte != expected[this->buffer_pos_]) { - this->buffer_pos_ = 0; // Reset and fall through to check if this byte starts a new frame - } - } - // First byte must match start of a data or command frame header - if (this->buffer_pos_ == 0 && byte != DATA_FRAME_HEADER[0] && byte != CMD_FRAME_HEADER[0]) { - return; - } - } - if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { this->buffer_data_[this->buffer_pos_++] = readch; this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; return; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44e5912b2ad..30f96c0a9c1 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -38,9 +38,11 @@ using namespace ld24xx; // Constants static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always +// lands inside the buffer during footer-based resynchronization after losing sync. +static constexpr uint8_t MAX_LINE_LENGTH = 45; +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, diff --git a/tests/components/ld2450/ld2450_readline.cpp b/tests/components/ld2450/ld2450_readline.cpp index 68b1dd68817..cb97f633bfb 100644 --- a/tests/components/ld2450/ld2450_readline.cpp +++ b/tests/components/ld2450/ld2450_readline.cpp @@ -48,19 +48,39 @@ TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Garbage rejection tests --- - -TEST_F(LD2450ReadlineTest, GarbageDiscarded) { - // Feed bytes that don't match any header start byte - std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99, 0x00, 0xFF, 0x7F}; - this->ld2450_.feed(garbage); - // Header sync should discard all of these - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} +// --- Garbage then valid frame tests --- TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { + // Garbage bytes accumulate in the buffer but don't match any footer. + // A valid frame follows; its footer resets the buffer and resyncs. std::vector garbage = {0x01, 0x02, 0x03, 0x42, 0x99}; this->ld2450_.feed(garbage); + EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + // Footer from the valid frame resyncs the parser + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +// --- Footer-based resynchronization tests --- + +TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) { + // Garbage containing a periodic frame footer (0x55 0xCC) triggers + // a buffer reset, allowing the next frame to be parsed cleanly. + std::vector garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC}; + this->ld2450_.feed(garbage_with_footer); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer + + auto frame = make_periodic_frame(); + this->ld2450_.feed(frame); + EXPECT_EQ(this->ld2450_.buffer_pos_, 0); +} + +TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) { + // Garbage containing a command frame footer (04 03 02 01) also resyncs. + std::vector garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01}; + this->ld2450_.feed(garbage_with_footer); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); auto frame = make_periodic_frame(); @@ -68,112 +88,56 @@ TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) { EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -// --- Header synchronization tests --- +// --- Overflow recovery tests --- -TEST_F(LD2450ReadlineTest, PartialDataHeaderThenMismatch) { - // Start of a data frame header, then invalid byte - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // 0x42 doesn't match DATA_FRAME_HEADER[2] (0x03) - // Parser should have reset - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialCmdHeaderThenMismatch) { - // Start of a command frame header, then invalid byte - this->ld2450_.feed({0xFD, 0xFC, 0xFB, 0x42}); // 0x42 doesn't match CMD_FRAME_HEADER[3] (0xFA) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, PartialHeaderThenValidFrame) { - // Partial header that fails, then a complete valid frame - this->ld2450_.feed({0xAA, 0xFF, 0x42}); // Fails at byte 3 - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, HeaderMismatchRecoveryOnNewHeaderByte) { - // Start data header, mismatch at byte 2, but mismatch byte is start of command header - this->ld2450_.feed({0xAA, 0xFF}); - EXPECT_EQ(this->ld2450_.buffer_pos_, 2); // Accumulating header - - this->ld2450_.feed({0xFD}); // Doesn't match DATA_FRAME_HEADER[2]=0x03, but IS CMD_FRAME_HEADER[0] - // Parser should reset and start new frame with 0xFD - EXPECT_EQ(this->ld2450_.buffer_pos_, 1); - EXPECT_EQ(this->ld2450_.buffer_data_[0], 0xFD); -} - -// --- Mid-frame / overflow recovery tests --- - -TEST_F(LD2450ReadlineTest, MidFrameDataRecovery) { - // Simulate starting mid-frame: feed the tail end of a periodic frame (no valid header) - // These bytes would be part of target data in a real frame - std::vector mid_frame = {0x10, 0x20, 0x30, 0x40, 0x55, 0xCC}; - this->ld2450_.feed(mid_frame); - // All discarded (none match header start bytes) - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now feed a valid frame - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); -} - -TEST_F(LD2450ReadlineTest, OverflowRecovery) { - // Feed a valid data frame header followed by enough filler to cause overflow. - // Header (4) + 36 filler = 40 bytes in buffer. The 41st byte triggers overflow. - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; // Valid header - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x11); // Filler that won't match any footer - } - // 41 bytes total: 40 stored, 41st triggers overflow and resets buffer_pos_ to 0 +TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) { + // Fill the buffer to capacity with filler that won't match any footer. + // MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); + this->ld2450_.feed(overflow_data); + // After overflow, buffer_pos_ resets to 0 (via the < 4 early return path) + EXPECT_LT(this->ld2450_.buffer_pos_, 4); +} + +TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) { + // Overflow, then a valid frame should be processed. + std::vector overflow_data(MAX_LINE_LENGTH, 0x11); this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - // Feed a valid frame and verify recovery auto frame = make_periodic_frame(); this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, RepeatedOverflowDoesNotLoop) { - // Simulate the bug scenario: repeated overflows should not prevent recovery. - // Feed 3 rounds of overflow-inducing data. - for (int round = 0; round < 3; round++) { - std::vector overflow_data = {0xAA, 0xFF, 0x03, 0x00}; - for (int i = 0; i < 37; i++) { - overflow_data.push_back(0x22); - } - this->ld2450_.feed(overflow_data); - EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Overflow round " << round; - } - - // Parser should still recover and process a valid frame +TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) { + // The key fix: the buffer (45) is large enough that a desynced periodic frame's + // footer (at most 30 bytes into the stream) will land inside the buffer before overflow. + // Simulate starting 10 bytes into a periodic frame, then a full frame follows. + std::vector mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}; + // Then a complete periodic frame whose footer will land at position 40 (10 + 30), + // well within the buffer size of 45. auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); + mid_frame.insert(mid_frame.end(), frame.begin(), frame.end()); + + this->ld2450_.feed(mid_frame); + // The footer from the frame should have triggered a reset EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } -TEST_F(LD2450ReadlineTest, SimulatedRestartGarbageThenFrames) { - // Simulate LD2450 restart: burst of garbage bytes (partial frames, noise) - // followed by normal periodic data. - // Partial periodic frame (as if we started reading mid-frame), a stale footer, and more garbage +TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) { + // Simulate LD2450 restart: burst of garbage followed by valid periodic frames. + // The garbage + first frame should fit in the buffer so the footer resyncs. std::vector restart_noise = { - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, // mid-frame data - 0x55, 0xCC, // stale footer bytes - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, // more garbage + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data }; + auto frame = make_periodic_frame(); + // 8 garbage + 30 frame = 38 bytes, well within buffer of 45 + restart_noise.insert(restart_noise.end(), frame.begin(), frame.end()); this->ld2450_.feed(restart_noise); - // All garbage should be discarded - EXPECT_EQ(this->ld2450_.buffer_pos_, 0); - - // Now the LD2450 starts sending valid frames - auto frame = make_periodic_frame(); - this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); + // Subsequent frames should work normally this->ld2450_.feed(frame); EXPECT_EQ(this->ld2450_.buffer_pos_, 0); } From 8aaf0b8d8546471d5733b7e565a29f2be2d4582e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:17:12 -0500 Subject: [PATCH 0106/2056] Bump version to 2026.2.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 38135f91068..d41a79b0dc3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0 +PROJECT_NUMBER = 2026.2.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 9115055e7b1..b3c15b1e27c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.0" +__version__ = "2026.2.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b0a35559b3aa905a46b662b5834689a45b840e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 10:19:01 -0600 Subject: [PATCH 0107/2056] [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 25 +++++++++++++++--------- esphome/components/esp32_ble/__init__.py | 15 ++++++-------- platformio.ini | 4 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index df584fa7164..777c8463715 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -3258307fa645ba77307e502075c02c4d710e92c48250839db3526d36a9655444 +5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cdde1c4ed50..b6682100f7f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -587,16 +587,22 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" -def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: +def _format_framework_espidf_version( + ver: cv.Version, release: str | None = None +) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): ext = "tar.xz" else: ext = "zip" + # Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1") + ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" + if ver.extra: + ver_str += f".{ver.extra}" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" def _is_framework_url(source: str) -> bool: @@ -643,7 +649,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { - cv.Version(3, 3, 7): cv.Version(5, 5, 3), + cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -662,11 +668,12 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3), - "latest": cv.Version(5, 5, 3), - "dev": cv.Version(5, 5, 3), + "recommended": cv.Version(5, 5, 3, "1"), + "latest": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 3, "1"), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), @@ -730,7 +737,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + _format_framework_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -1428,7 +1435,7 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver, None)], + [_format_framework_espidf_version(idf_ver)], ) # Use stub package to skip downloading precompiled libs stubs_dir = CORE.relative_build_path("arduino_libs_stub") diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 80fcf051b9a..c0e2f78bde3 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -418,16 +418,13 @@ def final_validation(config): "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) - # Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a - # GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED - # guard, causing a compile error when both are disabled. - # Additionally, when GATT Client is enabled, GATT Server must also be enabled - # as an internal dependency in the Bluedroid stack. + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled + # This is an internal dependency in the Bluedroid stack # See: https://github.com/espressif/esp-idf/issues/17724 - # TODO: Revert to conditional once the gatt_main.c bug is fixed upstream: - # has_ble_server = "esp32_ble_server" in full_config - # add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) - add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True) + add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) # Handle max_connections: check for deprecated location in esp32_ble_tracker diff --git a/platformio.ini b/platformio.ini index fdd6a364284..e35dce2228b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extends = common:arduino platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3/esp-idf-v5.5.3.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz framework = espidf lib_deps = From 9ce01fc369c5451e1f980e49cea08e51b6a0cc53 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:05 -0500 Subject: [PATCH 0108/2056] [esp32] Add engineering_sample option for ESP32-P4 (#14139) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 43 +++++++++++++++++-- esphome/components/esp32/boards.py | 5 ++- script/generate-esp32-boards.py | 15 ++++++- tests/components/esp32/test.esp32-p4-idf.yaml | 1 + 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b6682100f7f..06677006ea3 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -87,6 +87,7 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" @@ -785,6 +786,15 @@ def _detect_variant(value): # variant has already been validated against the known set value = value.copy() value[CONF_BOARD] = STANDARD_BOARDS[variant] + if variant == VARIANT_ESP32P4: + engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) + if engineering_sample is None: + _LOGGER.warning( + "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " + "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + ) + elif engineering_sample: + value[CONF_BOARD] = "esp32-p4-evboard" elif board in BOARDS: variant = variant or BOARDS[board][KEY_VARIANT] if variant != BOARDS[board][KEY_VARIANT]: @@ -848,6 +858,30 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' is only supported on {VARIANT_ESP32P4}", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if ( + config[CONF_VARIANT] == VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + board_is_es = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + if config[CONF_ENGINEERING_SAMPLE] != board_is_es: + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -1197,6 +1231,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), + cv.Optional(CONF_ENGINEERING_SAMPLE): cv.boolean, cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), @@ -1482,10 +1517,12 @@ async def to_code(config): # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 # from y to n. PlatformIO uses sections.ld.in (for rev <3) or # sections.rev3.ld.in (for rev >=3) based on board definition. - # Set the sdkconfig option to match the board's revision. + # Set the sdkconfig option to match the board's chip revision. if variant == VARIANT_ESP32P4: - is_rev3 = "_r3" in config[CONF_BOARD] - add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", not is_rev3) + is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample) # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 66367d63ae6..2bd08e7c392 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -20,7 +20,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C6: "esp32-c6-devkitm-1", VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", - VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32P4: "esp32-p4_r3-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", VARIANT_ESP32S3: "esp32-s3-devkitc-1", } @@ -1713,10 +1713,12 @@ BOARDS = { "esp32-p4": { "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4-evboard": { "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4_r3": { "name": "Espressif ESP32-P4 rev.300 generic", @@ -2141,6 +2143,7 @@ BOARDS = { "m5stack-tab5-p4": { "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index 81b78b04be2..ab4a38ced5b 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -43,10 +43,14 @@ def get_boards(): name = board_info["name"] board = fname.stem variant = mcu.upper() - boards[board] = { + chip_variant = board_info["build"].get("chip_variant", "") + entry = { "name": name, "variant": f"VARIANT_{variant}", } + if chip_variant.endswith("_es"): + entry["engineering_sample"] = True + boards[board] = entry return boards @@ -55,6 +59,12 @@ TEMPLATE = """ "%s": { "variant": %s, },""" +TEMPLATE_ES = """ "%s": { + "name": "%s", + "variant": %s, + "engineering_sample": True, + },""" + def main(check: bool): boards = get_boards() @@ -66,7 +76,8 @@ def main(check: bool): if line == "BOARDS = {": parts.append(line) parts.extend( - TEMPLATE % (board, info["name"], info["variant"]) + (TEMPLATE_ES if info.get("engineering_sample") else TEMPLATE) + % (board, info["name"], info["variant"]) for board, info in sorted(boards.items()) ) parts.append("}") diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index bc054f5aeea..fd42fac5a39 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -1,5 +1,6 @@ esp32: variant: esp32p4 + engineering_sample: true flash_size: 32MB cpu_frequency: 400MHz framework: From 403235e2d4ec6a27ac4ad35f79b7dc2afb413611 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:20:29 -0500 Subject: [PATCH 0109/2056] [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148) Co-authored-by: Claude Opus 4.6 --- esphome/components/wifi/__init__.py | 22 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 16 ++++++++++++++ esphome/components/wifi/wifi_component.h | 13 +++++++++++ .../wifi/wifi_component_esp_idf.cpp | 7 ++++++ tests/components/wifi/test.esp32-c5-idf.yaml | 5 +++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/components/wifi/test.esp32-c5-idf.yaml diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index afceec6c548..540d0a0ab18 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,12 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM -from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + const, + get_esp32_variant, + only_on_variant, +) from esphome.components.network import ( has_high_performance_networking, ip_address_literal, @@ -64,6 +69,7 @@ _LOGGER = logging.getLogger(__name__) NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" @@ -90,6 +96,13 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } +WiFiBandMode = cg.global_ns.enum("wifi_band_mode_t") +WIFI_BAND_MODES = { + "AUTO": WiFiBandMode.WIFI_BAND_MODE_AUTO, + "2.4GHZ": WiFiBandMode.WIFI_BAND_MODE_2G_ONLY, + "5GHZ": WiFiBandMode.WIFI_BAND_MODE_5G_ONLY, +} + WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") WIFI_MIN_AUTH_MODES = { "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, @@ -353,6 +366,11 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All( cv.boolean, cv.only_on_esp32 ), + cv.Optional(CONF_BAND_MODE): cv.All( + cv.enum(WIFI_BAND_MODES, upper=True), + cv.only_on_esp32, + only_on_variant(supported=[const.VARIANT_ESP32C5]), + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -527,6 +545,8 @@ async def to_code(config): cg.add(var.set_btm(config[CONF_ENABLE_BTM])) if config[CONF_ENABLE_RRM]: cg.add(var.set_rrm(config[CONF_ENABLE_RRM])) + if CONF_BAND_MODE in config: + cg.add(var.set_band_mode(config[CONF_BAND_MODE])) if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2efac8d262..8b3060c7c3e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1469,6 +1469,22 @@ void WiFiComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + const char *band_mode_s; + switch (this->band_mode_) { + case WIFI_BAND_MODE_2G_ONLY: + band_mode_s = "2.4GHz"; + break; + case WIFI_BAND_MODE_5G_ONLY: + band_mode_s = "5GHz"; + break; + case WIFI_BAND_MODE_AUTO: + default: + band_mode_s = "Auto"; + break; + } + ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s); +#endif if (this->is_connected()) { this->print_connect_params_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4a038f602c6..5f903e092a0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -45,6 +45,10 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) +#include +#endif + #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) #include #include @@ -435,6 +439,9 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } +#endif void set_passive_scan(bool passive); @@ -652,6 +659,9 @@ class WiFiComponent : public Component { bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + bool wifi_apply_band_mode_(); +#endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); @@ -774,6 +784,9 @@ class WiFiComponent : public Component { // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 52ee4821215..57bbceb1b85 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -292,6 +292,10 @@ bool WiFiComponent::wifi_apply_power_save_() { return success; } +#ifdef SOC_WIFI_SUPPORT_5G +bool WiFiComponent::wifi_apply_band_mode_() { return esp_wifi_set_band_mode(this->band_mode_) == ESP_OK; } +#endif + bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // enable STA if (!this->wifi_mode_(true, {})) @@ -726,6 +730,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_started = true; // re-apply power save mode wifi_apply_power_save_(); +#ifdef SOC_WIFI_SUPPORT_5G + wifi_apply_band_mode_(); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); diff --git a/tests/components/wifi/test.esp32-c5-idf.yaml b/tests/components/wifi/test.esp32-c5-idf.yaml new file mode 100644 index 00000000000..92a52db09ee --- /dev/null +++ b/tests/components/wifi/test.esp32-c5-idf.yaml @@ -0,0 +1,5 @@ +wifi: + band_mode: 5GHZ + +packages: + - !include common.yaml From 9c0eed8a67a392868d2a8e6c8a220314271c847e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:03:39 -0500 Subject: [PATCH 0110/2056] [e131] Remove dead LWIP TCP code path from loop() (#14155) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/e131/e131.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d04ab8a58d2..a7a695c1679 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -85,22 +85,6 @@ void E131Component::loop() { ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } -#elif defined(USE_SOCKET_IMPL_LWIP_TCP) - while (auto packet_size = this->udp_.parsePacket()) { - auto len = this->udp_.read(buf, sizeof(buf)); - if (len <= 0) - continue; - - if (!this->packet_(buf, (size_t) len, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); - continue; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } - } -#endif } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { From b85a49cdb3c65c24ffdb725a56dc9f48c2fd7f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:27:15 -0600 Subject: [PATCH 0111/2056] Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 376825bad60..5d7c32eaa97 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: category: "/language:${{matrix.language}}" From 1a376328911b184cbc5b7251688607f7ff62384a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:27:45 +0000 Subject: [PATCH 0112/2056] Bump pylint from 4.0.4 to 4.0.5 (#14160) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9e99855f6fe..611e5528292 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.4 +pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.15.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating From edfc3e3501ec3b0652a7a341d5542f70c488b79c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:32:41 +0000 Subject: [PATCH 0113/2056] Bump ruff from 0.15.1 to 0.15.2 (#14159) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d89060b0df..07d02e0e3c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.1 + rev: v0.15.2 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 611e5528292..3e5dc8a90ca 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.1 # also change in .pre-commit-config.yaml when updating +ruff==0.15.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 48115eca18b8c9937595a0fe26ce50fde4ebb15f Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:08:31 +0100 Subject: [PATCH 0114/2056] [safe_mode] Extract RTC_KEY constant for shared use (#14121) Co-authored-by: J. Nick Koston --- esphome/components/safe_mode/safe_mode.cpp | 2 +- esphome/components/safe_mode/safe_mode.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 6cae4bf9d5d..bd80048c64c 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -104,7 +104,7 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_enable_time_ = enable_time; this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences->make_preference(233825507UL, false); + this->rtc_ = global_preferences->make_preference(RTC_KEY, false); #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index d6f669f39ff..a4d27c15da7 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -11,6 +11,9 @@ namespace esphome::safe_mode { +/// RTC key for storing boot loop counter - used by safe_mode and preferences backends +constexpr uint32_t RTC_KEY = 233825507UL; + /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent : public Component { public: From db6aa58f40b4c6be322f8c26312605bb2fca8dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:06:46 -0500 Subject: [PATCH 0115/2056] [max7219digit] Fix typo in action names (#14162) Co-authored-by: Claude Opus 4.6 --- esphome/components/max7219digit/display.py | 22 +++++++++++----------- tests/components/max7219digit/common.yaml | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e6d53efc5dd..a251eaccea5 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA ) -async def max7129digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 525b7b8d3e3..4d2dbb781d8 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -13,10 +13,10 @@ esphome: on_boot: - priority: 100 then: - - max7129digit.invert_off: - - max7129digit.invert_on: - - max7129digit.turn_on: - - max7129digit.turn_off: - - max7129digit.reverse_on: - - max7129digit.reverse_off: - - max7129digit.intensity: 10 + - max7219digit.invert_off: + - max7219digit.invert_on: + - max7219digit.turn_on: + - max7219digit.turn_off: + - max7219digit.reverse_on: + - max7219digit.reverse_off: + - max7219digit.intensity: 10 From 1d3054ef5e463f1b018e297ffe547c7e621ddfd4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 20 Feb 2026 23:12:50 +0100 Subject: [PATCH 0116/2056] [nrf52,logger] Early debug (#11685) --- esphome/components/debug/debug_zephyr.cpp | 45 +-------- esphome/components/logger/__init__.py | 24 +++++ esphome/components/logger/logger.cpp | 3 + esphome/components/logger/logger.h | 1 + esphome/components/logger/logger_zephyr.cpp | 97 +++++++++++++++++++ esphome/components/nrf52/__init__.py | 1 + esphome/components/zephyr/gpio.h | 6 +- esphome/components/zephyr/preferences.h | 6 +- esphome/components/zephyr/reset_reason.cpp | 63 ++++++++++++ esphome/components/zephyr/reset_reason.h | 18 ++++ esphome/config_validation.py | 2 + esphome/core/defines.h | 2 + .../logger/test.nrf52-adafruit.yaml | 2 + 13 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 esphome/components/zephyr/reset_reason.cpp create mode 100644 esphome/components/zephyr/reset_reason.h diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 0291cc3061d..ecca7150bd9 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include #include "esphome/core/log.h" +#include #include #include #include @@ -15,16 +16,6 @@ static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; -static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { - if (!set) { - return pos; - } - if (pos > 0) { - pos = buf_append_printf(buf, size, pos, ", "); - } - return buf_append_printf(buf, size, pos, "%s", reason); -} - static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } @@ -57,39 +48,7 @@ static inline uint32_t sd_version_get() { } const char *DebugComponent::get_reset_reason_(std::span buffer) { - char *buf = buffer.data(); - const size_t size = RESET_REASON_BUFFER_SIZE; - - uint32_t cause; - auto ret = hwinfo_get_reset_cause(&cause); - if (ret) { - ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); - buf[0] = '\0'; - return buf; - } - size_t pos = 0; - - pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); - pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); - pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); - - // Ensure null termination if nothing was written - if (pos == 0) { - buf[0] = '\0'; - } - + const char *buf = zephyr::get_reset_reason(buffer); ESP_LOGD(TAG, "Reset Reason: %s", buf); return buf; } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index b2952d79956..c8f3c529112 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -101,6 +101,8 @@ CONF_INITIAL_LEVEL = "initial_level" CONF_LOGGER_ID = "logger_id" CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" +CONF_WAIT_FOR_CDC = "wait_for_cdc" +CONF_EARLY_MESSAGE = "early_message" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], @@ -208,6 +210,12 @@ def validate_initial_no_higher_than_global(config): return config +def validate_wait_for_cdc(config): + if config.get(CONF_WAIT_FOR_CDC) and config.get(CONF_HARDWARE_UART) != USB_CDC: + raise cv.Invalid("wait_for_cdc requires hardware_uart: USB_CDC") + return config + + Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", @@ -300,10 +308,18 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault( CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True ): cv.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_WAIT_FOR_CDC, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), + cv.boolean, + ), + cv.SplitDefault(CONF_EARLY_MESSAGE, nrf52=False): cv.All( + cv.only_on(PLATFORM_NRF52), cv.boolean + ), } ).extend(cv.COMPONENT_SCHEMA), validate_local_no_higher_than_global, validate_initial_no_higher_than_global, + validate_wait_for_cdc, ) @@ -425,13 +441,21 @@ async def to_code(config): except cv.Invalid: pass + if config.get(CONF_WAIT_FOR_CDC): + cg.add_define("USE_LOGGER_WAIT_FOR_CDC") + if config.get(CONF_EARLY_MESSAGE): + cg.add_define("USE_LOGGER_EARLY_MESSAGE") + if CORE.is_nrf52: + # esphome implement own fatal error handler which save PC/LR before reset + zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: zephyr_add_overlay("""&uart1 { status = "okay";};""") if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") zephyr_add_prj_conf("UART_LINE_CTRL", True) zephyr_add_cdc_acm(config, 0) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index e1b49bcb614..87963c5fc52 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -261,6 +261,9 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif +#ifdef USE_ZEPHYR + dump_crash_(); +#endif } void Logger::set_log_level(uint8_t level) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 2a7552af925..c6c379f6c60 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -317,6 +317,7 @@ class Logger : public Component { Stream *hw_serial_{nullptr}; #endif #if defined(USE_ZEPHYR) + void dump_crash_(); const device *uart_dev_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index f565c5760cd..d6193ff36b5 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -8,9 +8,30 @@ #include #include #include +#ifdef USE_LOGGER_EARLY_MESSAGE +#include +#endif + +namespace esphome::zephyr_coredump { + +__attribute__((weak)) void print_coredump() {} + +} // namespace esphome::zephyr_coredump namespace esphome::logger { +static const uint32_t CRASH_MAGIC = 0xDEADBEEF; + +__attribute__((section(".noinit"))) struct { + uint32_t magic; + uint32_t reason; + uint32_t pc; + uint32_t lr; +#if defined(CONFIG_THREAD_NAME) + char thread[CONFIG_THREAD_MAX_NAME_LEN]; +#endif +} crash_buf; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC @@ -57,10 +78,26 @@ void Logger::pre_setup() { ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; +#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) + uint32_t dtr = 0; + uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs + while (dtr == 0 && count-- != 0) { + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + delay(10); + arch_feed_wdt(); + } +#endif } } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_LOGGER_EARLY_MESSAGE + char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE]; + const char *reset_reason = zephyr::get_reset_reason(std::span(reason_buffer)); + ESP_LOGI(TAG, "Reset reason: %s", reset_reason); + dump_crash_(); + zephyr_coredump::print_coredump(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { @@ -93,6 +130,66 @@ const LogString *Logger::get_uart_selection_() { } } +static const uint8_t REASON_BUF_SIZE = 32; + +static const char *reason_to_str(unsigned int reason, char *buf) { + switch (reason) { + case K_ERR_CPU_EXCEPTION: + return "CPU exception"; + case K_ERR_SPURIOUS_IRQ: + return "Unhandled interrupt"; + case K_ERR_STACK_CHK_FAIL: + return "Stack overflow"; + case K_ERR_KERNEL_OOPS: + return "Kernel oops"; + case K_ERR_KERNEL_PANIC: + return "Kernel panic"; + default: + snprintf(buf, REASON_BUF_SIZE, "Unknown error (%u)", reason); + return buf; + } +} + +void Logger::dump_crash_() { + ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); + if (crash_buf.magic == CRASH_MAGIC) { + char reason_buf[REASON_BUF_SIZE]; + ESP_LOGE(TAG, "Last crash:"); + ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, + crash_buf.lr); +#if defined(CONFIG_THREAD_NAME) + ESP_LOGE(TAG, "Thread: %s", crash_buf.thread); +#endif + } +} + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + crash_buf.magic = CRASH_MAGIC; + crash_buf.reason = reason; + if (esf) { + crash_buf.pc = esf->basic.pc; + crash_buf.lr = esf->basic.lr; + } +#if defined(CONFIG_THREAD_NAME) + auto thread = k_current_get(); + const char *name = k_thread_name_get(thread); + if (name) { + strncpy(crash_buf.thread, name, sizeof(crash_buf.thread) - 1); + crash_buf.thread[sizeof(crash_buf.thread) - 1] = '\0'; + } else { + crash_buf.thread[0] = '\0'; + } +#endif + arch_restart(); +} + } // namespace esphome::logger +extern "C" { + +void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { + esphome::logger::k_sys_fatal_error_handler(reason, esf); +} +} + #endif diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 7d3d59f0ad9..95e36701249 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -266,6 +266,7 @@ async def to_code(config: ConfigType) -> None: }; """ ) + zephyr_add_prj_conf("REBOOT", True) @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 94f25f02ac7..907fbe9f9cc 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -3,8 +3,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/hal.h" #include -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: @@ -39,7 +38,6 @@ class ZephyrGPIOPin : public InternalGPIOPin { bool value_{false}; }; -} // namespace zephyr -} // namespace esphome +} // namespace esphome::zephyr #endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h index 6a37e41b46a..4bee96d79e8 100644 --- a/esphome/components/zephyr/preferences.h +++ b/esphome/components/zephyr/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_ZEPHYR -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { void setup_preferences(); -} // namespace zephyr -} // namespace esphome +} #endif diff --git a/esphome/components/zephyr/reset_reason.cpp b/esphome/components/zephyr/reset_reason.cpp new file mode 100644 index 00000000000..24b32196db5 --- /dev/null +++ b/esphome/components/zephyr/reset_reason.cpp @@ -0,0 +1,63 @@ +#include "reset_reason.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +static const char *const TAG = "zephyr"; + +static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { + if (!set) { + return pos; + } + if (pos > 0) { + pos = buf_append_printf(buf, size, pos, ", "); + } + return buf_append_printf(buf, size, pos, "%s", reason); +} + +const char *get_reset_reason(std::span buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + + uint32_t cause; + auto ret = hwinfo_get_reset_cause(&cause); + if (ret) { + ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); + buf[0] = '\0'; + return buf; + } + size_t pos = 0; + + if (cause == 0) { + pos = append_reset_reason(buf, size, pos, true, "None"); + } else { + pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); + pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); + pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); + pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); + pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); + pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); + pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); + } + + // Ensure null termination if nothing was written + if (pos == 0) { + buf[0] = '\0'; + } + return buf; +} +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/reset_reason.h b/esphome/components/zephyr/reset_reason.h new file mode 100644 index 00000000000..2c2e7b8470b --- /dev/null +++ b/esphome/components/zephyr/reset_reason.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ZEPHYR) && (defined(USE_LOGGER_EARLY_MESSAGE) || defined(USE_DEBUG)) + +#include +#include + +namespace esphome::zephyr { + +static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; + +const char *get_reset_reason(std::span buffer); + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a9d1a72e5a2..ef1c66a20e1 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -70,6 +70,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, SCHEDULER_DONT_RUN, TYPE_GIT, @@ -695,6 +696,7 @@ def only_with_framework( only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) +only_on_nrf52 = only_on(PLATFORM_NRF52) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a7fb9f197c2..c82b222a3d2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -334,6 +334,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml index 821a1362507..062451188ff 100644 --- a/tests/components/logger/test.nrf52-adafruit.yaml +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -5,4 +5,6 @@ esphome: logger: level: DEBUG + wait_for_cdc: true + early_message: true task_log_buffer_size: 0 From d206c75b0b815c1cdd5d4f0741f0b04f22918138 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:44 -0600 Subject: [PATCH 0117/2056] [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) --- esphome/components/logger/logger.cpp | 13 ++++++------- esphome/components/logger/logger.h | 10 +++++----- esphome/components/logger/logger_zephyr.cpp | 2 +- esphome/core/defines.h | 4 ++++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 87963c5fc52..22a95e4835b 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -170,19 +170,19 @@ void Logger::init_log_buffer(size_t total_buffer_size) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Start with loop disabled when using task buffer // The loop will be enabled automatically when messages arrive + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() this->disable_loop_when_buffer_empty_(); #endif } #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { this->process_messages_(); -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) this->cdc_loop_(); #endif } @@ -204,8 +204,7 @@ void Logger::process_messages_() { this->write_log_buffer_to_console_(buf); } } -// Zephyr needs loop working to check when CDC port is open -#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) +#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c6c379f6c60..8bf1edebb8d 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -147,7 +147,7 @@ class Logger : public Component { #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -229,7 +229,7 @@ class Logger : public Component { void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, const char *thread_name); #endif -#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC) +#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) void cdc_loop_(); #endif void process_messages_(); @@ -465,9 +465,9 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -// Zephyr needs loop working to check when CDC port is open -#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) - // Disable loop when task buffer is empty (with USB CDC check on ESP32) +#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) + // Disable loop when task buffer is empty + // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index d6193ff36b5..c2d24d6efc4 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -34,7 +34,7 @@ __attribute__((section(".noinit"))) struct { static const char *const TAG = "logger"; -#ifdef USE_LOGGER_USB_CDC +#ifdef USE_LOGGER_UART_SELECTION_USB_CDC void Logger::cdc_loop_() { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c82b222a3d2..5109dd36f43 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -270,10 +270,12 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) #define USE_LOGGER_USB_CDC +#define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_SERIAL_JTAG #endif #endif @@ -335,6 +337,8 @@ #ifdef USE_NRF52 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE +#define USE_LOGGER_UART_SELECTION_USB_CDC +#define USE_LOGGER_USB_CDC #define USE_LOGGER_WAIT_FOR_CDC #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 From 35037d1a5b7c5ad7f4dbabaaa60dab2e7d76af01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:20:58 -0600 Subject: [PATCH 0118/2056] [core] Deduplicate base64 encode/decode logic (#14143) Co-authored-by: Claude Opus 4.6 --- esphome/core/helpers.cpp | 88 ++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 09e755ca713..9f850b5df8f 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -545,38 +545,36 @@ static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast(char_array_4[j])]; +} + std::string base64_encode(const uint8_t *buf, size_t buf_len) { std::string ret; int i = 0; - int j = 0; char char_array_3[3]; - char char_array_4[4]; while (buf_len--) { char_array_3[i++] = *(buf++); if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[static_cast(char_array_4[i])]; + base64_encode_triple(char_array_3, 4, ret); i = 0; } } if (i) { - for (j = i; j < 3; j++) + for (int j = i; j < 3; j++) char_array_3[j] = '\0'; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[static_cast(char_array_4[j])]; + base64_encode_triple(char_array_3, i + 1, ret); while ((i++ < 3)) ret += '='; @@ -589,13 +587,33 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } +// Decode 4 base64 characters to up to 'count' output bytes, returns true if truncated. +static inline bool base64_decode_quad(uint8_t *char_array_4, int count, uint8_t *buf, size_t buf_len, size_t &out) { + for (int i = 0; i < 4; i++) + char_array_4[i] = base64_find_char(char_array_4[i]); + + uint8_t char_array_3[3]; + char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + bool truncated = false; + for (int j = 0; j < count; j++) { + if (out < buf_len) { + buf[out++] = char_array_3[j]; + } else { + truncated = true; + } + } + return truncated; +} + size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { size_t in_len = encoded_len; int i = 0; - int j = 0; size_t in = 0; size_t out = 0; - uint8_t char_array_4[4], char_array_3[3]; + uint8_t char_array_4[4]; bool truncated = false; // SAFETY: The loop condition checks is_base64() before processing each character. @@ -605,42 +623,16 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { - for (i = 0; i < 4; i++) - char_array_4[i] = base64_find_char(char_array_4[i]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (i = 0; i < 3; i++) { - if (out < buf_len) { - buf[out++] = char_array_3[i]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, 3, buf, buf_len, out); i = 0; } } if (i) { - for (j = i; j < 4; j++) + for (int j = i; j < 4; j++) char_array_4[j] = 0; - for (j = 0; j < 4; j++) - char_array_4[j] = base64_find_char(char_array_4[j]); - - char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); - char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); - char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - - for (j = 0; j < i - 1; j++) { - if (out < buf_len) { - buf[out++] = char_array_3[j]; - } else { - truncated = true; - } - } + truncated |= base64_decode_quad(char_array_4, i - 1, buf, buf_len, out); } if (truncated) { From a3f279c1cf150f6908b84df9976e72821995e7b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:14 -0600 Subject: [PATCH 0119/2056] [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) --- esphome/components/usb_host/usb_host.h | 21 +++++++++++-------- .../components/usb_host/usb_host_client.cpp | 17 ++++++++++++++- esphome/components/usb_uart/usb_uart.cpp | 11 +++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index d1ec356613f..a6a97d0bd71 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -73,12 +73,12 @@ static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than mai // used to report a transfer status struct TransferStatus { - bool success; - uint16_t error_code; uint8_t *data; size_t data_len; - uint8_t endpoint; void *user_data; + uint16_t error_code; + uint8_t endpoint; + bool success; }; using transfer_cb_t = std::function; @@ -127,7 +127,7 @@ class USBClient : public Component { friend class USBHost; public: - USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid), trq_in_use_(0) {} + USBClient(uint16_t vid, uint16_t pid) : trq_in_use_(0), vid_(vid), pid_(pid) {} void setup() override; void loop() override; // setup must happen after the host bus has been setup @@ -148,6 +148,10 @@ class USBClient : public Component { EventPool event_pool; protected: + // Process USB events from the queue. Returns true if any work was done. + // Subclasses should call this instead of USBClient::loop() to combine + // with their own work check for a single disable_loop() decision. + bool process_usb_events_(); void handle_open_state_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); @@ -161,20 +165,19 @@ class USBClient : public Component { static void usb_task_fn(void *arg); [[noreturn]] void usb_task_loop() const; + // Members ordered to minimize struct padding on 32-bit platforms + TransferRequest requests_[MAX_REQUESTS]{}; TaskHandle_t usb_task_handle_{nullptr}; - usb_host_client_handle_t handle_{}; usb_device_handle_t device_handle_{}; int device_addr_{-1}; int state_{USB_CLIENT_INIT}; - uint16_t vid_{}; - uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) - // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots std::atomic trq_in_use_; - TransferRequest requests_[MAX_REQUESTS]{}; + uint16_t vid_{}; + uint16_t pid_{}; }; class USBHost : public Component { public: diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 0612d7a841b..a9be38fb03f 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -197,6 +197,9 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + // Re-enable component loop to process the queued event + client->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); @@ -243,10 +246,13 @@ void USBClient::usb_task_loop() const { } } -void USBClient::loop() { +bool USBClient::process_usb_events_() { + bool had_work = false; + // Process any events from the USB task UsbEvent *event; while ((event = this->event_queue.pop()) != nullptr) { + had_work = true; switch (event->type) { case EVENT_DEVICE_NEW: this->on_opened(event->data.device_new.address); @@ -266,8 +272,17 @@ void USBClient::loop() { } if (this->state_ == USB_CLIENT_OPEN) { + had_work = true; this->handle_open_state_(); } + + return had_work; +} + +void USBClient::loop() { + if (!this->process_usb_events_()) { + this->disable_loop(); + } } void USBClient::handle_open_state_() { diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index edd01c26c67..5c2806c4560 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -172,11 +172,12 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) { } void USBUartComponent::setup() { USBClient::setup(); } void USBUartComponent::loop() { - USBClient::loop(); + bool had_work = this->process_usb_events_(); // Process USB data from the lock-free queue UsbDataChunk *chunk; while ((chunk = this->usb_data_queue_.pop()) != nullptr) { + had_work = true; auto *channel = chunk->channel; #ifdef USE_UART_DEBUGGER @@ -198,6 +199,11 @@ void USBUartComponent::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped); } + + // Disable loop when idle. Callbacks re-enable via enable_loop_soon_any_context(). + if (!had_work) { + this->disable_loop(); + } } void USBUartComponent::dump_config() { USBClient::dump_config(); @@ -264,6 +270,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + // Re-enable component loop to process the queued data + this->enable_loop_soon_any_context(); + // Wake main loop immediately to process USB data instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); From 0e38acd67a178ca6490284093c7034e8bfde5532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 19:21:56 -0600 Subject: [PATCH 0120/2056] [api] Warn when clients connect with outdated API version (#14145) --- esphome/components/api/api_connection.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5a7994a3225..5b02bee5371 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1534,6 +1534,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); + // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) + if (!this->client_supports_api_version(1, 14)) { + ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), + this->client_api_version_major_, this->client_api_version_minor_); + } + HelloResponse resp; resp.api_version_major = 1; resp.api_version_minor = 14; From 8589f80d8fea4751aeac15134c91ca79fdf850a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 20:59:49 -0600 Subject: [PATCH 0121/2056] [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) --- esphome/components/api/api_server.cpp | 30 +++++++++---------- esphome/components/api/api_server.h | 9 +++++- .../captive_portal/dns_server_esp32_idf.cpp | 9 ++---- .../captive_portal/dns_server_esp32_idf.h | 9 ++++-- .../components/esphome/ota/ota_esphome.cpp | 26 +++++++++------- esphome/components/esphome/ota/ota_esphome.h | 3 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1a1d0b229b1..5b096788f52 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -30,6 +30,12 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c APIServer::APIServer() { global_api_server = this; } +void APIServer::socket_failed_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); + this->destroy_socket_(); + this->mark_failed(); +} + void APIServer::setup() { ControllerRegistry::register_controller(this); @@ -48,22 +54,20 @@ void APIServer::setup() { #endif #endif - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); - this->mark_failed(); + this->socket_failed_(LOG_STR("creation")); return; } int enable = 1; int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); // we can still continue } err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); + this->socket_failed_(LOG_STR("nonblocking")); return; } @@ -71,22 +75,19 @@ void APIServer::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("set sockaddr")); return; } err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("bind")); return; } err = this->socket_->listen(this->listen_backlog_); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("listen")); return; } @@ -622,10 +623,7 @@ void APIServer::on_shutdown() { this->shutting_down_ = true; // Close the listening socket to prevent new connections - if (this->socket_) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); // Change batch delay to 5ms for quick flushing during shutdown this->batch_delay_ = 5; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3b9ba0e23b4..fed29016b37 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -249,8 +249,15 @@ class APIServer : public Component, void add_state_subscription_(std::string entity_id, optional attribute, std::function f, bool once); #endif // USE_API_HOMEASSISTANT_STATES + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } + void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - std::unique_ptr socket_ = nullptr; + socket::Socket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5743cbd671b..bd9989a40cb 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -53,7 +53,7 @@ void DNSServer::start(const network::IPAddress &ip) { #endif // Create loop-monitored UDP socket - this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release(); if (this->socket_ == nullptr) { ESP_LOGE(TAG, "Socket create failed"); return; @@ -70,17 +70,14 @@ void DNSServer::start(const network::IPAddress &ip) { int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); if (err != 0) { ESP_LOGE(TAG, "Bind failed: %d", errno); - this->socket_ = nullptr; + this->destroy_socket_(); return; } ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); } void DNSServer::stop() { - if (this->socket_ != nullptr) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); ESP_LOGV(TAG, "Stopped"); } diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 3e0ac07373a..f8e4cfec84e 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,7 +1,6 @@ #pragma once #ifdef USE_ESP32 -#include #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/socket/socket.h" @@ -15,9 +14,15 @@ class DNSServer { void process_next_request(); protected: + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } static constexpr size_t DNS_BUFFER_SIZE = 192; - std::unique_ptr socket_{nullptr}; + socket::Socket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index df2ea98f2c8..a1cdf59d2b7 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -28,10 +28,9 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_(LOG_STR("creation")); - this->mark_failed(); + this->server_failed_(LOG_STR("creation")); return; } int enable = 1; @@ -42,8 +41,7 @@ void ESPHomeOTAComponent::setup() { } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_(LOG_STR("non-blocking")); - this->mark_failed(); + this->server_failed_(LOG_STR("nonblocking")); return; } @@ -51,22 +49,19 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_(LOG_STR("set sockaddr")); - this->mark_failed(); + this->server_failed_(LOG_STR("set sockaddr")); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_(LOG_STR("bind")); - this->mark_failed(); + this->server_failed_(LOG_STR("bind")); return; } err = this->server_->listen(1); // Only one client at a time if (err != 0) { - this->log_socket_error_(LOG_STR("listen")); - this->mark_failed(); + this->server_failed_(LOG_STR("listen")); return; } } @@ -455,6 +450,15 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } +void ESPHomeOTAComponent::server_failed_(const LogString *msg) { + this->log_socket_error_(msg); + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + delete this->server_; + this->server_ = nullptr; + this->mark_failed(); +} + bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e199b7e406d..c9e89c82ba0 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -66,6 +66,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { this->handshake_buf_pos_ = 0; // Reset buffer position for next state } + void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); @@ -83,7 +84,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - std::unique_ptr server_; + socket::Socket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; From abe37c98413c6b78a7a01bec82f4b2f14e68cd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:08:49 -0600 Subject: [PATCH 0122/2056] [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) --- .../uptime/sensor/uptime_seconds_sensor.cpp | 27 +++++-------------- .../uptime/sensor/uptime_seconds_sensor.h | 9 ++----- .../uptime/sensor/uptime_timestamp_sensor.cpp | 6 ++--- .../uptime/sensor/uptime_timestamp_sensor.h | 6 ++--- .../uptime/text_sensor/uptime_text_sensor.cpp | 24 ++++------------- .../uptime/text_sensor/uptime_text_sensor.h | 8 ++---- esphome/core/scheduler.cpp | 2 ++ esphome/core/scheduler.h | 3 +++ 8 files changed, 24 insertions(+), 61 deletions(-) diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 54260d7e808..20e8ed8fdae 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,30 +1,16 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint32_t ms = millis(); - const uint64_t ms_mask = (1ULL << 32) - 1ULL; - const uint32_t last_ms = this->uptime_ & ms_mask; - if (ms < last_ms) { - this->uptime_ += ms_mask + 1ULL; - ESP_LOGD(TAG, "Detected roll-over \xf0\x9f\xa6\x84"); - } - this->uptime_ &= ~ms_mask; - this->uptime_ |= ms; - - // Do separate second and milliseconds conversion to avoid floating point division errors - // Probably some IEEE standard already guarantees this division can be done without loss - // of precision in a single division, but let's do it like this to be sure. - const uint64_t seconds_int = this->uptime_ / 1000ULL; - const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; + const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t seconds_int = uptime / 1000ULL; + const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); } float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } @@ -33,5 +19,4 @@ void UptimeSecondsSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Seconds"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 210195052f6..1b80a4480af 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { public: @@ -12,10 +11,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { void dump_config() override; float get_setup_priority() const override; - - protected: - uint64_t uptime_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index 69033be11cd..4e0f06be1c3 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -33,7 +32,6 @@ void UptimeTimestampSensor::dump_config() { ESP_LOGCONFIG(TAG, " Type: Timestamp"); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h index f38b5d53b4f..912c0b76555 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.h +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTimestampSensor : public sensor::Sensor, public Component { public: @@ -24,7 +23,6 @@ class UptimeTimestampSensor : public sensor::Sensor, public Component { time::RealTimeClock *time_; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime #endif // USE_TIME diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index acd3980a1ad..88ae53fbfcd 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,11 +1,10 @@ #include "uptime_text_sensor.h" -#include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; @@ -17,22 +16,10 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label); } -void UptimeTextSensor::setup() { - this->last_ms_ = millis(); - if (this->last_ms_ < 60 * 1000) - this->last_ms_ = 0; - this->update(); -} +void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - auto now = millis(); - // get whole seconds since last update. Note that even if the millis count has overflowed between updates, - // the difference will still be correct due to the way twos-complement arithmetic works. - uint32_t delta = now - this->last_ms_; - this->last_ms_ = now - delta % 1000; // save remainder for next update - delta /= 1000; - this->uptime_ += delta; - uint32_t uptime = this->uptime_; + uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units @@ -89,5 +76,4 @@ void UptimeTextSensor::update() { float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 947d9c91e93..a97ba332bbe 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace uptime { +namespace esphome::uptime { class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -35,9 +34,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent const char *seconds_text_; const char *separator_; bool expand_{}; - uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years - uint32_t last_ms_{0}; }; -} // namespace uptime -} // namespace esphome +} // namespace esphome::uptime diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3294f689e8b..e82efcc520a 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -675,6 +675,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } +uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } + uint64_t Scheduler::millis_64_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 394178a831c..afe11aaca69 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -116,6 +116,9 @@ class Scheduler { ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0") bool cancel_retry(Component *component, uint32_t id); + /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) + uint64_t millis_64(); + // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached // Returns the time in milliseconds until the next scheduled item, or nullopt if no items From f8f98bf428e1db199f4b5bf79a4b3362b7d5672d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:16:49 -0600 Subject: [PATCH 0123/2056] [logger] Reduce UART driver heap waste on ESP32 (#14168) --- esphome/components/logger/logger_esp32.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index dfa643d5e94..9d64771aecf 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -77,9 +77,10 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; uart_param_config(uart_num, &uart_config); - const int uart_buffer_size = tx_buffer_size; - // Install UART driver using an event queue here - uart_driver_install(uart_num, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + // The logger only writes to UART, never reads, so use the minimum RX buffer. + // ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes). + const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1; + uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0); } void Logger::pre_setup() { From f77da803c9b316c78a51e602563dcd6d916682f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Feb 2026 21:39:18 -0600 Subject: [PATCH 0124/2056] [api] Write protobuf encode output to pre-sized buffer directly (#14018) --- esphome/components/api/api_connection.cpp | 35 ++--- esphome/components/api/api_pb2.cpp | 180 +++++++++++----------- esphome/components/api/api_pb2.h | 176 ++++++++++----------- esphome/components/api/proto.cpp | 15 ++ esphome/components/api/proto.h | 108 ++++++++----- script/api_protobuf/api_protobuf.py | 12 +- 6 files changed, 286 insertions(+), 240 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5b02bee5371..9fc263abbd0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -347,9 +347,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess #endif // Calculate size - ProtoSize size_calc; - msg.calculate_size(size_calc); - uint32_t calculated_size = size_calc.get_size(); + uint32_t calculated_size = msg.calculated_size(); // Cache frame sizes to avoid repeated virtual calls const uint8_t header_padding = conn->helper_->frame_header_padding(); @@ -377,19 +375,14 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess shared_buf.resize(current_size + footer_size + header_padding); } - // Encode directly into buffer - size_t size_before_encode = shared_buf.size(); - msg.encode({&shared_buf}); + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); - // Calculate actual encoded size (not including header that was already added) - size_t actual_payload_size = shared_buf.size() - size_before_encode; - - // Return actual total size (header + actual payload + footer) - size_t actual_total_size = header_padding + actual_payload_size + footer_size; - - // Verify that calculate_size() returned the correct value - assert(calculated_size == actual_payload_size); - return static_cast(actual_total_size); + // Return total size (header + payload + footer) + return static_cast(header_padding + calculated_size + footer_size); } #ifdef USE_BINARY_SENSOR @@ -1854,12 +1847,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); + uint32_t payload_size = msg.calculated_size(); std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, size.get_size()); - msg.encode({&shared_buf}); - return this->send_buffer({&shared_buf}, message_type); + this->prepare_first_message_buffer(shared_buf, payload_size); + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + payload_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + msg.encode(buffer); + return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 743f51dac77..5c50a8aa5b9 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,7 +31,7 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer buffer) const { +void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); @@ -44,7 +44,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer buffer) const { +void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } @@ -54,7 +54,7 @@ void AreaInfo::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer buffer) const { +void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); @@ -65,7 +65,7 @@ void DeviceInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->area_id); } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -111,7 +111,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area); + buffer.encode_message(22, this->area, false); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -176,7 +176,7 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #endif } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -206,7 +206,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -224,7 +224,7 @@ void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -260,7 +260,7 @@ void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { +void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); @@ -317,7 +317,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -359,7 +359,7 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void FanStateResponse::encode(ProtoWriteBuffer buffer) const { +void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); @@ -443,7 +443,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -489,7 +489,7 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_uint32(2, this->device_id); #endif } -void LightStateResponse::encode(ProtoWriteBuffer buffer) const { +void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); @@ -635,7 +635,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -671,7 +671,7 @@ void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -689,7 +689,7 @@ void SensorStateResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -719,7 +719,7 @@ void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { +void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -760,7 +760,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -788,7 +788,7 @@ void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -818,7 +818,7 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } @@ -839,11 +839,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { +void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } @@ -851,7 +851,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { size.add_length(1, this->key.size()); size.add_length(1, this->value.size()); } -void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { buffer.encode_message(2, it); @@ -924,7 +924,7 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); @@ -976,7 +976,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } @@ -984,7 +984,7 @@ void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { size.add_length(1, this->name.size()); size.add_uint32(1, static_cast(this->type)); } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { @@ -1103,7 +1103,7 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { +void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->call_id); buffer.encode_bool(2, this->success); buffer.encode_string(3, this->error_message); @@ -1121,7 +1121,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1147,7 +1147,7 @@ void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { +void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); @@ -1178,7 +1178,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1276,7 +1276,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(2, this->feature_flags); } -void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { +void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); @@ -1407,7 +1407,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1449,7 +1449,7 @@ void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { } size.add_uint32(1, this->supported_features); } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { +void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); @@ -1515,7 +1515,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1553,7 +1553,7 @@ void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { +void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1596,7 +1596,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1630,7 +1630,7 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { +void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -1681,7 +1681,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1719,7 +1719,7 @@ void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { +void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES @@ -1789,7 +1789,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1823,7 +1823,7 @@ void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void LockStateResponse::encode(ProtoWriteBuffer buffer) const { +void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -1878,7 +1878,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1930,7 +1930,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); @@ -1944,7 +1944,7 @@ void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->purpose)); size.add_uint32(1, this->sample_bytes); } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -1978,7 +1978,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { #endif size.add_uint32(1, this->feature_flags); } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); @@ -2062,7 +2062,7 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); @@ -2074,7 +2074,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->address_type); size.add_length(1, this->data_len); } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } @@ -2103,7 +2103,7 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->connected); buffer.encode_uint32(3, this->mtu); @@ -2125,7 +2125,7 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2141,7 +2141,7 @@ void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2163,7 +2163,7 @@ void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->descriptors); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2183,7 +2183,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->characteristics); size.add_uint32(1, this->short_uuid); } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { buffer.encode_message(2, it); @@ -2193,7 +2193,7 @@ void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_repeated_message(1, this->services); } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } @@ -2210,7 +2210,7 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt valu } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2302,7 +2302,7 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt va } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); @@ -2312,7 +2312,7 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_length(1, this->data_len_); } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); for (const auto &it : this->allocated) { @@ -2330,7 +2330,7 @@ void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { } } } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); @@ -2340,7 +2340,7 @@ void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->handle); size.add_int32(1, this->error); } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2348,7 +2348,7 @@ void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } @@ -2356,7 +2356,7 @@ void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); size.add_uint32(1, this->handle); } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); @@ -2366,7 +2366,7 @@ void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->paired); size.add_int32(1, this->error); } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2376,7 +2376,7 @@ void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); @@ -2386,7 +2386,7 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); size.add_int32(1, this->error); } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); @@ -2421,7 +2421,7 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarIn } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); @@ -2431,11 +2431,11 @@ void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->auto_gain); size.add_float(1, this->volume_multiplier); } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { @@ -2516,7 +2516,7 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } @@ -2587,9 +2587,9 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { @@ -2656,7 +2656,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it); } @@ -2686,7 +2686,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2718,7 +2718,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) cons size.add_uint32(1, this->device_id); #endif } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES @@ -2770,7 +2770,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2804,7 +2804,7 @@ void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TextStateResponse::encode(ProtoWriteBuffer buffer) const { +void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); @@ -2855,7 +2855,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2881,7 +2881,7 @@ void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); @@ -2934,7 +2934,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -2960,7 +2960,7 @@ void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); @@ -3013,7 +3013,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3049,7 +3049,7 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void EventResponse::encode(ProtoWriteBuffer buffer) const { +void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES @@ -3065,7 +3065,7 @@ void EventResponse::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3099,7 +3099,7 @@ void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { +void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); @@ -3148,7 +3148,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3174,7 +3174,7 @@ void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { +void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); @@ -3217,7 +3217,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3245,7 +3245,7 @@ void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); #endif } -void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { +void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); @@ -3314,7 +3314,7 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3338,7 +3338,7 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { +void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } @@ -3348,7 +3348,7 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); @@ -3419,7 +3419,7 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d001f869c50..c90873d9931 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -382,7 +382,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -447,7 +447,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -462,7 +462,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -527,7 +527,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -558,7 +558,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -575,7 +575,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -597,7 +597,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -615,7 +615,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -657,7 +657,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -677,7 +677,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -751,7 +751,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -815,7 +815,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -832,7 +832,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -851,7 +851,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -867,7 +867,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -901,7 +901,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -918,7 +918,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -957,7 +957,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -990,7 +990,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1033,7 +1033,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1077,7 +1077,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1138,7 +1138,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1157,7 +1157,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1227,7 +1227,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1244,7 +1244,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1266,7 +1266,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1317,7 +1317,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1345,7 +1345,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1403,7 +1403,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1424,7 +1424,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1468,7 +1468,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1485,7 +1485,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1519,7 +1519,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1536,7 +1536,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1573,7 +1573,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1589,7 +1589,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1634,7 +1634,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1687,7 +1687,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1719,7 +1719,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1737,7 +1737,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1755,7 +1755,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1811,7 +1811,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1828,7 +1828,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1865,7 +1865,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1893,7 +1893,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1908,7 +1908,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1922,7 +1922,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1939,7 +1939,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1994,7 +1994,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2089,7 +2089,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2107,7 +2107,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2125,7 +2125,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2142,7 +2142,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2159,7 +2159,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2177,7 +2177,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2195,7 +2195,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2213,7 +2213,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2231,7 +2231,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2277,7 +2277,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2297,7 +2297,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2359,7 +2359,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2417,7 +2417,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2430,7 +2430,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2480,7 +2480,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2515,7 +2515,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2531,7 +2531,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2570,7 +2570,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2587,7 +2587,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2621,7 +2621,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2640,7 +2640,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2675,7 +2675,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2694,7 +2694,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2731,7 +2731,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2747,7 +2747,7 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2768,7 +2768,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2785,7 +2785,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2820,7 +2820,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2837,7 +2837,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2871,7 +2871,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2895,7 +2895,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2930,7 +2930,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2949,7 +2949,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2969,7 +2969,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3016,7 +3016,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer buffer) const override; + void encode(ProtoWriteBuffer &buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 764dd3f3912..73a3bab12a2 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -70,6 +70,21 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +#ifdef ESPHOME_DEBUG_API +void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { + if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { + ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, + this->pos_ - this->buffer_->data(), this->buffer_->size()); + abort(); + } +} +void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) { + ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id, + expected, actual); + abort(); +} +#endif + void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 8ac79633cf5..4522fc96655 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -217,21 +217,26 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} - void write(uint8_t value) { this->buffer_->push_back(value); } + ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(std::vector *buffer, size_t write_pos) + : buffer_(buffer), pos_(buffer->data() + write_pos) {} void encode_varint_raw(uint32_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { - this->buffer_->push_back(static_cast(value | 0x80)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); value >>= 7; } - this->buffer_->push_back(static_cast(value)); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); } /** * Encode a field key (tag/wire type combination). @@ -245,23 +250,18 @@ class ProtoWriteBuffer { * * Following https://protobuf.dev/programming-guides/encoding/#structure */ - void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); - this->encode_varint_raw(val); - } + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - - // Using resize + memcpy instead of insert provides significant performance improvement: - // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings - // as it avoids iterator checks and potential element moves that insert performs - size_t old_size = this->buffer_->size(); - this->buffer_->resize(old_size + len); - std::memcpy(this->buffer_->data() + old_size, string, len); + // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks + // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. + this->debug_check_bounds_(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -288,17 +288,26 @@ class ProtoWriteBuffer { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->buffer_->push_back(value ? 0x01 : 0x00); + this->debug_check_bounds_(1); + *this->pos_++ = value ? 0x01 : 0x00; } - void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy + __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); + this->debug_check_bounds_(4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian, so direct copy works + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // not supported to reduce overhead on embedded systems. All ESPHome devices are @@ -334,11 +343,20 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - void encode_message(uint32_t field_id, const ProtoMessage &value); + /// Encode a nested message field (force=true for repeated, false for singular) + void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); std::vector *get_buffer() const { return buffer_; } protected: +#ifdef ESPHOME_DEBUG_API + void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); +#else + void debug_check_bounds_([[maybe_unused]] size_t bytes) {} +#endif + std::vector *buffer_; + uint8_t *pos_; }; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,9 +434,11 @@ class ProtoMessage { public: virtual ~ProtoMessage() = default; // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer buffer) const {} + virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields virtual void calculate_size(ProtoSize &size) const {} + // Convenience: calculate and return size directly (defined after ProtoSize) + uint32_t calculated_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } @@ -877,6 +897,14 @@ class ProtoSize { } }; +// Implementation of methods that depend on ProtoSize being fully defined + +inline uint32_t ProtoMessage::calculated_size() const { + ProtoSize size; + this->calculate_size(size); + return size.get_size(); +} + // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -897,30 +925,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { // Calculate the message size first ProtoSize msg_size; value.calculate_size(msg_size); uint32_t msg_length_bytes = msg_size.get_size(); - // Calculate how many bytes the length varint needs - uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); + // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) + // Repeated messages (force=true) are always encoded since an empty item is meaningful + if (msg_length_bytes == 0 && !force) + return; - // Reserve exact space for the length varint - size_t begin = this->buffer_->size(); - this->buffer_->resize(this->buffer_->size() + varint_length_bytes); + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - // Write the length varint directly - encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); - - // Now encode the message content - it will append to the buffer - value.encode(*this); + // Write the length varint directly through pos_ + this->encode_varint_raw(msg_length_bytes); + // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API - // Verify that the encoded size matches what we calculated - assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); + uint8_t *start = this->pos_; + value.encode(*this); + if (static_cast(this->pos_ - start) != msg_length_bytes) + this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); +#else + value.encode(*this); #endif } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4fbee49daee..cc881caa5cf 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -689,6 +689,14 @@ class MessageType(TypeInfo): def encode_func(self) -> str: return "encode_message" + @property + def encode_content(self) -> str: + # Singular message fields pass force=false (skip empty messages) + # The default for encode_nested_message is force=true (for repeated fields) + return ( + f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" + ) + @property def decode_length(self) -> str: # Override to return None for message types because we can't use template-based @@ -2186,7 +2194,7 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" + o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} }}\n" else: @@ -2194,7 +2202,7 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const override;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used From 2eac106f119627e2ac58d39f4ebd92990895366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Sat, 21 Feb 2026 05:20:27 +0100 Subject: [PATCH 0125/2056] [mqtt] add missing precision in HA autodiscovery (#14010) --- esphome/components/mqtt/mqtt_const.h | 1 + esphome/components/mqtt/mqtt_sensor.cpp | 4 ++++ esphome/components/sensor/sensor.h | 2 ++ 3 files changed, 7 insertions(+) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 221af00371e..36a7dfb41a4 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -243,6 +243,7 @@ X(MQTT_STATE_VALUE_TEMPLATE, "stat_val_tpl", "state_value_template") \ X(MQTT_STEP, "step", "step") \ X(MQTT_SUBTYPE, "stype", "subtype") \ + X(MQTT_SUGGESTED_DISPLAY_PRECISION, "sug_dsp_prc", "suggested_display_precision") \ X(MQTT_SUPPORTED_COLOR_MODES, "sup_clrm", "supported_color_modes") \ X(MQTT_SUPPORTED_FEATURES, "sup_feat", "supported_features") \ X(MQTT_SWING_MODE_COMMAND_TEMPLATE, "swing_mode_cmd_tpl", "swing_mode_command_template") \ diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index e83eab6732f..a7d311d194a 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -49,6 +49,10 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_DEVICE_CLASS] = device_class; } + if (this->sensor_->has_accuracy_decimals()) { + root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); + } + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f9a45cb1d09..80981b8e288 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -48,6 +48,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa int8_t get_accuracy_decimals(); /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); + /// Check if the accuracy in decimals has been manually set. + bool has_accuracy_decimals() const { return this->sensor_flags_.has_accuracy_override; } /// Get the state class, using the manual override if set. StateClass get_state_class(); From 518f08b9097bdb3a980483f1358e309987de7402 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:13 -1000 Subject: [PATCH 0126/2056] [mipi_dsi] Disallow swap_xy (#14124) --- esphome/components/mipi_dsi/display.py | 26 +++++-------------- .../mipi_dsi/fixtures/mipi_dsi.yaml | 7 ++++- .../mipi_dsi/test_mipi_dsi_config.py | 6 +++-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index c288b33cd2f..de3791b3a47 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -87,38 +87,24 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] + model.defaults[CONF_SWAP_XY] = cv.UNDEFINED transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), } ) - if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED: - transform = transform.extend( - { - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by this model" - ) - } - ) - else: - transform = transform.extend( - { - cv.Required(CONF_SWAP_XY): cv.boolean, - } - ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None else cv.Optional(CONF_INIT_SEQUENCE) ) - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required - ) + # Dimensions are optional if the model has a default width + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") schema = display.FULL_DISPLAY_SCHEMA.extend( { diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 7d1fc841215..6de2bd5a770 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -15,8 +15,13 @@ esp_ldo: display: - platform: mipi_dsi + id: p4_nano model: WAVESHARE-P4-NANO-10.1 - + rotation: 90 + - platform: mipi_dsi + id: p4_86 + model: "WAVESHARE-P4-86-PANEL" + rotation: 180 i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index f8a9af02796..d465a8c81bf 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,9 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6ecb01dedc00595aa2565bcf302cde3d7ad1d4e9 Mon Sep 17 00:00:00 2001 From: Sxt Fov <140247217+sxtfov@users.noreply.github.com> Date: Sat, 21 Feb 2026 14:45:15 +0100 Subject: [PATCH 0127/2056] [cc1101] actions to change general and tuner settings (#14141) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cc1101/__init__.py | 92 +++++++++++++++++++++++++ esphome/components/cc1101/cc1101.h | 78 ++++++++++++++++++++++ tests/components/cc1101/common.yaml | 96 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index fbdd7010b40..14b92a18a40 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_VALUE, CONF_WAIT_TIME, ) from esphome.core import ID @@ -333,3 +334,94 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) return var + + +# Setter action definitions: (setter_name, validator, template_type, enum_map) +_SETTER_ACTIONS = [ + ( + "set_frequency", + cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), + float, + None, + ), + ("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None), + ("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION), + ("set_symbol_rate", cv.float_range(min=600, max=500000), float, None), + ( + "set_rx_attenuation", + cv.enum(RX_ATTENUATION, upper=False), + RxAttenuation, + RX_ATTENUATION, + ), + ("set_dc_blocking_filter", cv.boolean, bool, None), + ("set_manchester", cv.boolean, bool, None), + ( + "set_filter_bandwidth", + cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), + float, + None, + ), + ( + "set_fsk_deviation", + cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), + float, + None, + ), + ("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None), + ("set_channel", cv.uint8_t, cg.uint8, None), + ( + "set_channel_spacing", + cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), + float, + None, + ), + ( + "set_if_frequency", + cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), + float, + None, + ), +] + + +def _register_setter_actions(): + for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS: + class_name = ( + "".join(word.capitalize() for word in setter_name.split("_")) + "Action" + ) + action_cls = ns.class_( + class_name, automation.Action, cg.Parented.template(CC1101Component) + ) + schema = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_VALUE): cv.templatable(validator), + }, + key=CONF_VALUE, + ) + + async def _setter_action_to_code( + config, + action_id, + template_arg, + args, + _setter=setter_name, + _type=templ_type, + _map=enum_map, + ): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_VALUE] + if cg.is_template(data): + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) + else: + cg.add(getattr(var, _setter)(_map[data] if _map else data)) + return var + + automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( + _setter_action_to_code + ) + + +_register_setter_actions() diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index e55071e7e36..2efd9e082d5 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -161,4 +161,82 @@ template class SendPacketAction : public Action, public P size_t data_static_len_{0}; }; +template class SetSymbolRateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, symbol_rate) + void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); } +}; + +template class SetFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, frequency) + void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); } +}; + +template class SetOutputPowerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, output_power) + void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); } +}; + +template class SetModulationTypeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Modulation, modulation_type) + void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); } +}; + +template class SetRxAttenuationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation) + void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); } +}; + +template class SetDcBlockingFilterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, dc_blocking_filter) + void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); } +}; + +template class SetManchesterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, manchester) + void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); } +}; + +template class SetFilterBandwidthAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, filter_bandwidth) + void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); } +}; + +template class SetFskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, fsk_deviation) + void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); } +}; + +template class SetMskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, msk_deviation) + void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); } +}; + +template class SetChannelSpacingAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, channel_spacing) + void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); } +}; + +template class SetIfFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, if_frequency) + void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); } +}; + } // namespace esphome::cc1101 diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml index 42ec50911f7..9784bfce8b1 100644 --- a/tests/components/cc1101/common.yaml +++ b/tests/components/cc1101/common.yaml @@ -35,3 +35,99 @@ button: data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] - cc1101.send_packet: !lambda |- return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + - cc1101.set_frequency: !lambda |- + return 433.91e6; + - cc1101.set_frequency: + value: "433.91MHz" + - cc1101.set_frequency: + value: 433911000 + - cc1101.set_frequency: 433912000 + + - cc1101.set_output_power: !lambda |- + return -29.9; + - cc1101.set_output_power: + value: "-28" + - cc1101.set_output_power: + value: 10 + - cc1101.set_output_power: 11 + + - cc1101.set_modulation_type: !lambda |- + return cc1101::Modulation::MODULATION_2_FSK; + - cc1101.set_modulation_type: + value: "4-FSK" + - cc1101.set_modulation_type: "GFSK" + + - cc1101.set_symbol_rate: !lambda |- + return 6000.0; + - cc1101.set_symbol_rate: + value: "7000.0" + - cc1101.set_symbol_rate: + value: 8000.0 + - cc1101.set_symbol_rate: 9000 + + - cc1101.set_rx_attenuation: !lambda |- + return cc1101::RxAttenuation::RX_ATTENUATION_0DB; + - cc1101.set_rx_attenuation: + value: "6dB" + - cc1101.set_rx_attenuation: "12dB" + + - cc1101.set_dc_blocking_filter: !lambda |- + return false; + - cc1101.set_dc_blocking_filter: + value: true + - cc1101.set_dc_blocking_filter: false + + - cc1101.set_manchester: !lambda |- + return false; + - cc1101.set_manchester: + value: true + - cc1101.set_manchester: false + + - cc1101.set_filter_bandwidth: !lambda |- + return 58e3; + - cc1101.set_filter_bandwidth: + value: "59kHz" + - cc1101.set_filter_bandwidth: + value: 60000 + - cc1101.set_filter_bandwidth: "61kHz" + + - cc1101.set_fsk_deviation: !lambda |- + return 1.5e3; + - cc1101.set_fsk_deviation: + value: "1.6kHz" + - cc1101.set_fsk_deviation: + value: 1700 + - cc1101.set_fsk_deviation: "1.8kHz" + + - cc1101.set_msk_deviation: !lambda |- + return 1; + - cc1101.set_msk_deviation: + value: "2" + - cc1101.set_msk_deviation: + value: 3 + - cc1101.set_msk_deviation: "4" + + - cc1101.set_channel: !lambda |- + return 0; + - cc1101.set_channel: + value: "1" + - cc1101.set_channel: + value: 3 + - cc1101.set_channel: 3 + + - cc1101.set_channel_spacing: !lambda |- + return 25e3; + - cc1101.set_channel_spacing: + value: "26kHz" + - cc1101.set_channel_spacing: + value: 27000 + - cc1101.set_channel_spacing: "28kHz" + + - cc1101.set_if_frequency: !lambda |- + return 25e3; + - cc1101.set_if_frequency: + value: "26kHz" + - cc1101.set_if_frequency: + value: 27000 + - cc1101.set_if_frequency: "28kHz" From 7fb09da7cf228b3a5d2bbdfd60b3e6fb571b0331 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:08:13 -0600 Subject: [PATCH 0128/2056] [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) --- esphome/components/dsmr/dsmr.h | 3 +++ tests/components/dsmr/common.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index fafcf62b876..dc81ba9b2af 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 038bf2806b8..d11ce37d598 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,4 +1,13 @@ +esphome: + on_boot: + then: + - lambda: |- + // Test deprecated std::string overload still compiles + std::string key = "00112233445566778899aabbccddeeff"; + id(dsmr_instance).set_decryption_key(key); + dsmr: + id: dsmr_instance decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 request_pin: ${request_pin} From 416b97311b800e9563e48da0e403ed22e717ff61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:12:35 -0600 Subject: [PATCH 0129/2056] [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) --- esphome/__main__.py | 14 --------- esphome/components/mqtt/__init__.py | 21 -------------- .../components/mqtt/mqtt_backend_esp8266.h | 5 ---- .../components/mqtt/mqtt_backend_libretiny.h | 5 ---- esphome/components/mqtt/mqtt_client.cpp | 7 ----- esphome/components/mqtt/mqtt_client.h | 15 ---------- esphome/const.py | 1 - esphome/mqtt.py | 29 ++----------------- 8 files changed, 2 insertions(+), 95 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c86b5604e14..488955f5030 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1451,13 +1444,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index fe153fedfa2..44e88364873 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,5 +1,3 @@ -import re - from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -46,7 +44,6 @@ from esphome.const import ( CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, CONF_SUBSCRIBE_QOS, CONF_TOPIC, @@ -221,13 +218,6 @@ def validate_config(value): return out -def validate_fingerprint(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{40}$", value) is None: - raise cv.Invalid("fingerprint must be valid SHA1 hash") - return value - - def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" # MQTT needs 1 socket for the broker connection @@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( - cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) - ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -444,14 +431,6 @@ async def to_code(config): if CONF_LEVEL in log_topic: cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]])) - if CONF_SSL_FINGERPRINTS in config: - for fingerprint in config[CONF_SSL_FINGERPRINTS]: - arr = [ - cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) - ] - cg.add(var.add_ssl_fingerprint(arr)) - cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") - cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 470d1e6a8be..0bf5b510a4c 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 24bf018a909..5fa3406193c 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c433804dd97..1a03c5329ee 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -749,13 +749,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call this->on_disconnect_.add(std::move(callback_copy)); } -#if ASYNC_TCP_SSL_ENABLED -void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_backend_.setSecure(true); - this->mqtt_backend_.addServerFingerprint(fingerprint.data()); -} -#endif - MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // MQTTMessageTrigger diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 21edd53eda5..127e4073b01 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -137,21 +137,6 @@ class MQTTClientComponent : public Component { bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; -#if ASYNC_TCP_SSL_ENABLED - /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. - * - * To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag. - * This function can be called multiple times and any certificate that matches any of the provided fingerprints - * will match. Calling this method will also automatically disable all non-ssl connections. - * - * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add - * a separate fingerprint for every certificate you use. Additionally, the hashing - * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. - * - * @param fingerprint The SSL fingerprint as a 20 value long std::array. - */ - void add_ssl_fingerprint(const std::array &fingerprint); -#endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } diff --git a/esphome/const.py b/esphome/const.py index f72cbc88933..0179aa129e9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -943,7 +943,6 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 042df12d67d..cbf78bd3f6e 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,6 +1,5 @@ import contextlib from datetime import datetime -import hashlib import json import logging import ssl @@ -22,14 +21,12 @@ from esphome.const import ( CONF_PASSWORD, CONF_PORT, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, ) -from esphome.core import CORE, EsphomeError +from esphome.core import EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import AnsiFore, color from esphome.types import ConfigType from esphome.util import safe_print @@ -102,9 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( - CONF_CERTIFICATE_AUTHORITY - ): + if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY): context = ssl.create_default_context( cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY) ) @@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): client.publish(msg.topic, None, retain=True) return initialize(config, [topic], on_message, None, username, password, client_id) - - -# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py -def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) - _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) - try: - cert_pem = ssl.get_server_certificate(addr) - except OSError as err: - _LOGGER.error("Unable to connect to server: %s", err) - return 1 - cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - - sha1 = hashlib.sha1(cert_der).hexdigest() - - safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") - safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" - ) - return 0 From 6ff17fbf7c3d4b24ae0e20103c0828823d49b1dc Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:17:54 -1000 Subject: [PATCH 0130/2056] [epaper_spi] Fix color mapping for weact (#14134) --- esphome/components/epaper_spi/epaper_spi.h | 2 +- .../components/epaper_spi/epaper_weact_3c.cpp | 43 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index a8c2fe9b561..a7439855185 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -76,7 +76,7 @@ class EPaperBase : public Display, static uint8_t color_to_bit(Color color) { // It's always a shade of gray. Map to BLACK or WHITE. // We split the luminance at a suitable point - if ((static_cast(color.r) + color.g + color.b) > 512) { + if ((color.r + color.g + color.b) >= 382) { return 1; } return 0; diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp index bd83105dd77..d4dac7076c4 100644 --- a/esphome/components/epaper_spi/epaper_weact_3c.cpp +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -5,9 +5,24 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_weact_3c"; +enum class BwrState : uint8_t { + BWR_BLACK, + BWR_WHITE, + BWR_RED, +}; + +static BwrState color_to_bwr(Color color) { + if (color.r > color.g + color.b && color.r > 127) { + return BwrState::BWR_RED; + } + if (color.r + color.g + color.b >= 382) { + return BwrState::BWR_WHITE; + } + return BwrState::BWR_BLACK; +} // SSD1680 3-color display notes: // - Buffer uses 1 bit per pixel, 8 pixels per byte -// - Buffer first half (black_offset): Black/White plane (1=black, 0=white) +// - Buffer first half (black_offset): Black/White plane (0=black, 1=white) // - Buffer second half (red_offset): Red plane (1=red, 0=no red) // - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) // - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) @@ -23,20 +38,20 @@ void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { // Use luminance threshold for B/W mapping // Split at halfway point (382 = (255*3)/2) - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; + auto bwr = color_to_bwr(color); // Update black/white plane (first half of buffer) - if (is_white) { - // White pixel - clear bit in black plane - this->buffer_[pos] &= ~bit; - } else { - // Black pixel - set bit in black plane + if (bwr == BwrState::BWR_WHITE) { + // White pixel - set bit in black plane this->buffer_[pos] |= bit; + } else { + // Black pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; } // Update red plane (second half of buffer) // Red if red component is dominant (r > g+b) - if (color.r > color.g + color.b) { + if (bwr == BwrState::BWR_RED) { // Red pixel - set bit in red plane this->buffer_[red_offset + pos] |= bit; } else { @@ -53,21 +68,20 @@ void EPaperWeAct3C::fill(Color color) { const size_t half_buffer = this->buffer_length_ / 2u; // Use luminance threshold for B/W mapping - bool is_white = (static_cast(color.r) + color.g + color.b) > 382; - bool is_red = color.r > color.g + color.b; + auto bits = color_to_bwr(color); // Fill both planes - if (is_white) { - // White - both planes = 0x00 + if (bits == BwrState::BWR_BLACK) { + // Black - both planes = 0x00 this->buffer_.fill(0x00); - } else if (is_red) { + } else if (bits == BwrState::BWR_RED) { // Red - black plane = 0x00, red plane = 0xFF for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0x00; for (size_t i = 0; i < half_buffer; i++) this->buffer_[half_buffer + i] = 0xFF; } else { - // Black - black plane = 0xFF, red plane = 0x00 + // White - black plane = 0xFF, red plane = 0x00 for (size_t i = 0; i < half_buffer; i++) this->buffer_[i] = 0xFF; for (size_t i = 0; i < half_buffer; i++) @@ -112,7 +126,6 @@ bool HOT EPaperWeAct3C::transfer_data() { ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); // Use a local buffer for SPI transfers - static constexpr size_t MAX_TRANSFER_SIZE = 128; uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; // First, send the RED buffer (0x26 = WRITE_COLOR) From 48ba007c22d7c542e9de8740aacb8c94ac438a10 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 21 Feb 2026 20:16:20 +0100 Subject: [PATCH 0131/2056] [nrf52] print line number after crash in logs (#14165) --- esphome/__main__.py | 19 ++++++++++--- esphome/components/nrf52/__init__.py | 40 ++++++++++++++++++++++++++++ tests/unit_tests/test_main.py | 6 +++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 488955f5030..ffedb90bde4 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -431,6 +431,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) + process_stacktrace = None + + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + process_stacktrace = getattr(module, "process_stacktrace") + except AttributeError: + pass + backtrace_state = False ser = serial.Serial() ser.baudrate = baud_rate @@ -472,9 +480,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + if process_stacktrace: + backtrace_state = process_stacktrace( + config, line, backtrace_state + ) + else: + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 95e36701249..a12d1db1ab7 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio import logging from pathlib import Path +import re +import subprocess from esphome import pins import esphome.codegen as cg @@ -380,3 +382,41 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: asyncio.run(logger_connect(address)) return True return False + + +def _addr2line(addr2line: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [addr2line, "-e", elf, addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip().splitlines()[0] + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Running command failed: %s", err) + return "" + + +def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool: + if "Last crash:" in line: + return True + if backtrace_state: + match = re.search(r"PC=(0x[0-9a-fA-F]+)\s+LR=(0x[0-9a-fA-F]+)", line) + if match: + pc = match.group(1) + lr = match.group(2) + from esphome.analyze_memory.toolchain import find_tool + + addr2line = find_tool("addr2line") + if addr2line is None: + return False + elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf") + if not elf.exists(): + _LOGGER.warning("%s does not exists", elf) + return False + _LOGGER.error("=== CRASH ===") + _LOGGER.error("PC: %s", _addr2line(addr2line, elf, pc)) + _LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr)) + + return False diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index c9aa4463238..cef561c54b7 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2951,6 +2951,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp( mock_serial = MockSerial([chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -2989,6 +2990,7 @@ def test_run_miniterm_different_chunks_different_timestamps( mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3019,6 +3021,7 @@ def test_run_miniterm_handles_split_lines() -> None: mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3057,6 +3060,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None: mock_serial = MockSerial([backtrace_chunk, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3122,6 +3126,7 @@ def test_run_miniterm_handles_empty_reads( mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, @@ -3194,6 +3199,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END]) + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32} config = { CONF_LOGGER: { CONF_BAUD_RATE: 115200, From 9571a979eb7ff1d881a4a833a639ff54c861174e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:53:45 -0600 Subject: [PATCH 0132/2056] [ci] Suggest StringRef instead of std::string_view (#14183) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 231f587068e..df819e0f049 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -494,6 +494,22 @@ def lint_no_byte_datatype(fname, match): ) +@lint_re_check( + r"(?:std\s*::\s*string_view|#include\s*)" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_string_view(fname, match): + return ( + f"{highlight('std::string_view')} is not allowed in ESPHome. " + f"It pulls in significant STL template machinery that bloats flash on " + f"resource-constrained embedded targets, does not work well with ArduinoJson, " + f"and duplicates functionality already provided by {highlight('StringRef')}.\n" + f"Please use {highlight('StringRef')} from {highlight('esphome/core/string_ref.h')} " + f"for non-owning string references, or {highlight('const char *')} for simple cases.\n" + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_post_check def lint_constants_usage(): errs = [] From 5a07908dfa9ef499b20e8a53f23e5ea94b9eb682 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:20 -0600 Subject: [PATCH 0133/2056] [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) --- esphome/components/api/homeassistant_service.h | 2 ++ tests/components/homeassistant/common.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 2322d96eefc..340699e1a6d 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 9c6cb71b8b3..60e3defd496 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -90,6 +90,19 @@ text_sensor: id: ha_hello_world_text2 attribute: some_attribute +event: + - platform: template + name: Test Event + id: test_event + event_types: + - test_event_type + on_event: + - homeassistant.event: + event: esphome.test_event + data: + event_name: !lambda |- + return event_type; + time: - platform: homeassistant on_time: From e521522b388dce7066ade34d96869187e7c47ca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:43 -0600 Subject: [PATCH 0134/2056] [haier] Fix uninitialized HonSettings causing API connection failures (#14188) --- esphome/components/haier/hon_climate.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89c..608d5e7f210 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -29,10 +29,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase { int big_data_sensors_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; From 6f198adb0c79bcc8e14cdb07cbf484bb23aa1e52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 14:29:50 -0600 Subject: [PATCH 0135/2056] [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) --- esphome/core/scheduler.cpp | 23 +++++++++++ esphome/core/scheduler.h | 79 ++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e82efcc520a..b810df7e1cf 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -421,6 +421,29 @@ void Scheduler::full_cleanup_removed_items_() { this->to_remove_ = 0; } +#ifndef ESPHOME_THREAD_SINGLE +void Scheduler::compact_defer_queue_locked_() { + // Rare case: new items were added during processing - compact the vector + // This only happens when: + // 1. A deferred callback calls defer() again, or + // 2. Another thread calls defer() while we're processing + // + // Move unprocessed items (added during this loop) to the front for next iteration + // + // SAFETY: Compacted items may include cancelled items (marked for removal via + // cancel_item_locked_() during execution). This is safe because should_skip_item_() + // checks is_item_removed_() before executing, so cancelled items will be skipped + // and recycled on the next loop iteration. + size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; + for (size_t i = 0; i < remaining; i++) { + this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + } + // Use erase() instead of resize() to avoid instantiating _M_default_append + // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. + this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); +} +#endif /* not ESPHOME_THREAD_SINGLE */ + void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index afe11aaca69..041c6f46872 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -387,41 +387,46 @@ class Scheduler { // No lock needed: single consumer (main loop), stale read just means we process less this iteration size_t defer_queue_end = this->defer_queue_.size(); + // Fast path: nothing to process, avoid lock entirely. + // Safe without lock: single consumer (main loop) reads front_, and a stale size() read + // from a concurrent push can only make us see fewer items — they'll be processed next loop. + if (this->defer_queue_front_ >= defer_queue_end) + return; + + // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), + // recycle each item after re-acquiring the lock for the next iteration (N+1 total). + // The lock is held across: recycle → loop condition → move-out, then released for execution. + std::unique_ptr item; + + this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - std::unique_ptr item; - { - LockGuard lock(this->lock_); - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); - this->defer_queue_front_++; - } + // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. + // This is intentional and safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = std::move(this->defer_queue_[this->defer_queue_front_]); + this->defer_queue_front_++; + this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { now = this->execute_item_(item.get(), now); } - // Recycle the defer item after execution - { - LockGuard lock(this->lock_); - this->recycle_item_main_loop_(std::move(item)); - } - } - // If we've consumed all items up to the snapshot point, clean up the dead space - // Single consumer (main loop), so no lock needed for this check - if (this->defer_queue_front_ >= defer_queue_end) { - LockGuard lock(this->lock_); - this->cleanup_defer_queue_locked_(); + this->lock_.lock(); + this->recycle_item_main_loop_(std::move(item)); } + // Clean up the queue (lock already held from last recycle or initial acquisition) + this->cleanup_defer_queue_locked_(); + this->lock_.unlock(); } - // Helper to cleanup defer_queue_ after processing + // Helper to cleanup defer_queue_ after processing. + // Keeps the common clear() path inline, outlines the rare compaction to keep + // cold code out of the hot instruction cache lines. // IMPORTANT: Caller must hold the scheduler lock before calling this function. inline void cleanup_defer_queue_locked_() { // Check if new items were added by producers during processing @@ -429,27 +434,17 @@ class Scheduler { // Common case: no new items - clear everything this->defer_queue_.clear(); } else { - // Rare case: new items were added during processing - compact the vector - // This only happens when: - // 1. A deferred callback calls defer() again, or - // 2. Another thread calls defer() while we're processing - // - // Move unprocessed items (added during this loop) to the front for next iteration - // - // SAFETY: Compacted items may include cancelled items (marked for removal via - // cancel_item_locked_() during execution). This is safe because should_skip_item_() - // checks is_item_removed_() before executing, so cancelled items will be skipped - // and recycled on the next loop iteration. - size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; - for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); - } - // Use erase() instead of resize() to avoid instantiating _M_default_append - // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. - this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); + // Rare case: new items were added during processing - outlined to keep cold code + // out of the hot instruction cache lines + this->compact_defer_queue_locked_(); } this->defer_queue_front_ = 0; } + + // Cold path for compacting defer_queue_ when new items were added during processing. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines. + void __attribute__((noinline)) compact_defer_queue_locked_(); #endif /* not ESPHOME_THREAD_SINGLE */ // Helper to check if item is marked for removal (platform-specific) From 462ac2956312f21e506b1e2e13a6960e18d0d813 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 17:29:41 -0600 Subject: [PATCH 0136/2056] [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) --- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b810df7e1cf..36b65f6ff7c 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() { // Compact in-place: move valid items forward, recycle removed ones size_t write = 0; for (size_t read = 0; read < this->items_.size(); ++read) { - if (!is_item_removed_(this->items_[read].get())) { + if (!is_item_removed_locked_(this->items_[read].get())) { if (write != read) { this->items_[write] = std::move(this->items_[read]); } @@ -531,7 +531,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { + if (is_item_removed_locked_(item.get())) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -568,7 +568,7 @@ void HOT Scheduler::call(uint32_t now) { // during the function call and know if we were cancelled. auto executed_item = this->pop_raw_locked_(); - if (executed_item->remove) { + if (this->is_item_removed_locked_(executed_item.get())) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; this->recycle_item_main_loop_(std::move(executed_item)); @@ -595,7 +595,7 @@ void HOT Scheduler::call(uint32_t now) { void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (is_item_removed_(it.get())) { + if (is_item_removed_locked_(it.get())) { // Recycle cancelled items this->recycle_item_main_loop_(std::move(it)); continue; @@ -628,7 +628,7 @@ size_t HOT Scheduler::cleanup_() { LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; - if (!item->remove) + if (!this->is_item_removed_locked_(item.get())) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 041c6f46872..384d76b6b08 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -314,8 +314,8 @@ class Scheduler { // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) return false; - if (item->component != component || item->type != type || (skip_removed && item->remove) || - (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || + (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -463,6 +463,18 @@ class Scheduler { #endif } + // Helper to check if item is marked for removal when lock is already held. + // Uses relaxed ordering since the mutex provides all necessary synchronization. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool is_item_removed_locked_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Lock already held - relaxed is sufficient, mutex provides ordering + return item->remove.load(std::memory_order_relaxed); +#else + return item->remove; +#endif + } + // Helper to set item removal flag (platform-specific) // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this // function. Uses memory_order_release when setting to true (for cancellation synchronization), @@ -519,7 +531,7 @@ class Scheduler { // it will iterate over these nullptr items. This check prevents crashes. if (!item) continue; - if (is_item_removed_(item.get()) && + if (this->is_item_removed_locked_(item.get()) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, match_retry, /* skip_removed= */ false)) { return true; From a4682615234ac61ff7825e2a922807b11e6772fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:26 -0600 Subject: [PATCH 0137/2056] [scheduler] De-template and consolidate scheduler helper functions (#14164) --- esphome/core/scheduler.cpp | 14 +++++++++---- esphome/core/scheduler.h | 41 +++++++------------------------------- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 36b65f6ff7c..e4e0751e100 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -119,10 +119,16 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { // Remove before 2026.8.0 along with all retry code bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id) { - return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true); + for (auto *container : {&this->items_, &this->to_add_}) { + for (auto &item : *container) { + if (item && this->is_item_removed_locked_(item.get()) && + this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, + /* match_retry= */ true, /* skip_removed= */ false)) { + return true; + } + } + } + return false; } // Common implementation for both timeout and interval diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 384d76b6b08..16b0ded3126 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -308,8 +308,8 @@ class Scheduler { SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and - // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check + // provides defense-in-depth: helper // functions should be safe regardless of caller behavior. // Fixes: https://github.com/esphome/esphome/issues/11940 if (!item) @@ -403,8 +403,7 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ - // and has_cancelled_timeout_in_container_locked_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -497,19 +496,16 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - template - size_t mark_matching_items_removed_locked_(Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, - bool match_retry) { + size_t mark_matching_items_removed_locked_(std::vector> &container, + Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the // vector as nullptr until cleanup. Even though this function is called with lock held, // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (!item) - continue; - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item.get(), true); count++; } @@ -517,29 +513,6 @@ class Scheduler { return count; } - // Template helper to check if any item in a container matches our criteria - // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id - // IMPORTANT: Must be called with scheduler lock held - template - bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - bool match_retry) const { - for (const auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. If this function is called during defer queue processing, - // it will iterate over these nullptr items. This check prevents crashes. - if (!item) - continue; - if (this->is_item_removed_locked_(item.get()) && - this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, - match_retry, /* skip_removed= */ false)) { - return true; - } - } - return false; - } - Mutex lock_; std::vector> items_; std::vector> to_add_; From d5c9c56fdfcdd0112e1913f05f84e297908460a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:41:43 -0600 Subject: [PATCH 0138/2056] [platformio] Add exponential backoff and session reset to download retries (#14191) --- esphome/platformio_api.py | 41 ++++- tests/unit_tests/test_platformio_api.py | 196 +++++++++++++++++++++++- 2 files changed, 231 insertions(+), 6 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d42f89d0290..5d4065207f0 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,6 +5,7 @@ import os from pathlib import Path import re import subprocess +import time from typing import Any from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE @@ -44,31 +45,61 @@ def patch_structhash(): def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors.""" + """Patch PlatformIO's FileDownloader to retry on PackageException errors. + + PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry + for 502/503 errors. We add retries with exponential backoff and close the + session between attempts to force a fresh TCP connection, which may route + to a different CDN edge node. + """ from platformio.package.download import FileDownloader from platformio.package.exception import PackageException + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + original_init = FileDownloader.__init__ def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 3 + max_retries = 5 for attempt in range(max_retries): try: - return original_init(self, *args, **kwargs) + original_init(self, *args, **kwargs) + return except PackageException as e: if attempt < max_retries - 1: + # Exponential backoff: 2, 4, 8, 16 seconds + delay = 2 ** (attempt + 1) _LOGGER.warning( - "Package download failed: %s. Retrying... (attempt %d/%d)", + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", str(e), + delay, attempt + 1, max_retries, ) + # Close the response and session to free resources + # and force a new TCP connection on retry, which may + # route to a different CDN edge node + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) else: # Final attempt - re-raise raise - return None + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access FileDownloader.__init__ = patched_init diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 4d7b635e595..16861442777 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -6,7 +6,7 @@ import os from pathlib import Path import shutil from types import SimpleNamespace -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -673,6 +673,200 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_patch_file_downloader_succeeds_first_try() -> None: + """Test patch_file_downloader succeeds on first attempt.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + original_init = MagicMock() + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": original_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + original_init.assert_called_once() + + +def test_patch_file_downloader_retries_on_failure() -> None: + """Test patch_file_downloader retries with backoff on PackageException.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def failing_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise mock_exception_cls(f"502 error attempt {call_count}") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have been called 3 times (2 failures + 1 success) + assert call_count == 3 + + # Should have slept with exponential backoff: 2s, 4s + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(2) + mock_sleep.assert_any_call(4) + + +def test_patch_file_downloader_raises_after_max_retries() -> None: + """Test patch_file_downloader raises after exhausting all retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + + def always_failing_init(self, *args, **kwargs): + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": always_failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + with pytest.raises(mock_exception_cls, match="502 error"): + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should have slept 4 times (before attempts 2-5), not on final attempt + assert mock_sleep.call_count == 4 + mock_sleep.assert_has_calls([call(2), call(4), call(8), call(16)]) + + +def test_patch_file_downloader_closes_session_and_response_between_retries() -> None: + """Test patch_file_downloader closes HTTP session and response between retries.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + mock_session = MagicMock() + mock_response = MagicMock() + call_count = 0 + + def failing_init_with_session(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + self._http_session = mock_session + self._http_response = mock_response + if call_count < 2: + raise mock_exception_cls("502 error") + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", + (), + {"__init__": failing_init_with_session}, + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep"), + ): + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Both response and session should have been closed between retries + mock_response.close.assert_called_once() + mock_session.close.assert_called_once() + + +def test_patch_file_downloader_idempotent() -> None: + """Test patch_file_downloader does not stack wrappers when called multiple times.""" + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def counting_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + + with patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type("FileDownloader", (), {"__init__": counting_init}) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ): + # Patch multiple times + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + platformio_api.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + # Should only be called once, not 3 times from stacked wrappers + assert call_count == 1 + + def test_platformio_log_filter_allows_non_platformio_messages() -> None: """Test that non-platformio logger messages are allowed through.""" log_filter = platformio_api.PlatformioLogFilter() From 49e4ae54beb20f7de57a24274c851c13a382d0e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 23:22:59 -0600 Subject: [PATCH 0139/2056] [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) --- esphome/components/bme68x_bsec2/__init__.py | 5 ++++- tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6a..4200b2f0b85 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,8 +178,11 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 00000000000..7c503b0ccb6 --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml From e013b4867519c44b570fd6cf05872a7324845009 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 22 Feb 2026 06:44:06 +0100 Subject: [PATCH 0140/2056] [nextion] Add error log for failed HTTP status during TFT upload (#14190) --- esphome/components/nextion/nextion_upload_arduino.cpp | 1 + esphome/components/nextion/nextion_upload_esp32.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 220c75f9d39..a433eff883b 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -220,6 +220,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } if (code != 200 and code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index c4e6ff71821..46352afd750 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -238,6 +238,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { + ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); return this->upload_end_(false); } From 1753074eef3ea117dff2cb02995330227b242379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:01:07 -0600 Subject: [PATCH 0141/2056] [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) --- esphome/components/web_server_base/__init__.py | 3 +-- .../web_server_base/web_server_base.cpp | 18 ++---------------- .../web_server_base/web_server_base.h | 18 +++++++----------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 7986ac964df..183b907ae61 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -20,7 +20,7 @@ def AUTO_LOAD(): web_server_base_ns = cg.esphome_ns.namespace("web_server_base") -WebServerBase = web_server_base_ns.class_("WebServerBase", cg.Component) +WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" CONFIG_SCHEMA = cv.Schema( @@ -33,7 +33,6 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.WEB_SERVER_BASE) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}")) if CORE.is_esp32: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 6e7097338c3..dbbcd10d8df 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,19 +1,11 @@ #include "web_server_base.h" #ifdef USE_NETWORK -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" -namespace esphome { -namespace web_server_base { - -static const char *const TAG = "web_server_base"; +namespace esphome::web_server_base { WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void WebServerBase::add_handler(AsyncWebHandler *handler) { - // remove all handlers - #ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); @@ -25,11 +17,5 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { } } -float WebServerBase::get_setup_priority() const { - // Before WiFi (captive portal) - return setup_priority::WIFI + 2.0f; -} - -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 0c25467f1bb..54421c851e5 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,11 +1,9 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK -#include #include #include -#include "esphome/core/component.h" #include "esphome/core/progmem.h" #if USE_ESP32 @@ -21,8 +19,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server_base { +namespace esphome::web_server_base { class WebServerBase; extern WebServerBase *global_web_server_base; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -91,14 +88,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler { } // namespace internal -class WebServerBase : public Component { +class WebServerBase { public: void init() { if (this->initialized_) { this->initialized_++; return; } - this->server_ = std::make_unique(this->port_); + this->server_ = new AsyncWebServer(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. // NOTE: Currently 1 header. If more are added, update in __init__.py: // cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) @@ -113,11 +110,11 @@ class WebServerBase : public Component { void deinit() { this->initialized_--; if (this->initialized_ == 0) { + delete this->server_; this->server_ = nullptr; } } - AsyncWebServer *get_server() const { return this->server_.get(); } - float get_setup_priority() const override; + AsyncWebServer *get_server() const { return this->server_; } #ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } @@ -132,13 +129,12 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::unique_ptr server_{nullptr}; + AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; #endif }; -} // namespace web_server_base -} // namespace esphome +} // namespace esphome::web_server_base #endif From 509f06afac19fc54b3fae2a67802771312b91133 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:56:09 -0600 Subject: [PATCH 0142/2056] [network] Improve IPAddress::str() deprecation warning with usage example (#14195) --- esphome/components/network/ip_address.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d0ac8164af2..b2a2c563e28 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -61,7 +61,9 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -150,7 +152,9 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From ede2da2fbc167814c5598adb6d866c239a0772c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:57:23 -0600 Subject: [PATCH 0143/2056] [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) --- esphome/components/ch422g/ch422g.cpp | 2 ++ esphome/components/ch422g/ch422g.h | 2 ++ esphome/components/ch423/ch423.cpp | 2 ++ esphome/components/ch423/ch423.h | 2 ++ esphome/components/deep_sleep/deep_sleep_component.cpp | 2 ++ esphome/components/deep_sleep/deep_sleep_component.h | 2 ++ esphome/components/pca9554/pca9554.cpp | 2 ++ esphome/components/pca9554/pca9554.h | 2 ++ esphome/components/pcf8574/pcf8574.cpp | 2 ++ esphome/components/pcf8574/pcf8574.h | 2 ++ esphome/components/status_led/light/status_led_light.h | 2 ++ esphome/components/status_led/status_led.cpp | 2 ++ esphome/components/status_led/status_led.h | 2 ++ esphome/components/wifi/__init__.py | 7 +++++++ esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/core/application.cpp | 2 ++ esphome/core/component.cpp | 2 ++ esphome/core/component.h | 3 +++ esphome/core/defines.h | 1 + 20 files changed, 45 insertions(+) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index eef95b9ba28..5f5e848c76e 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,9 +124,11 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 8ed63db90ae..1b965682093 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,7 +23,9 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 4abbbe7adff..805d8df8774 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,9 +129,11 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index 7adc7de6a12..d85648a8f96 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,7 +22,9 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; protected: diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 8066b411ffa..0511518419d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,9 +40,11 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } +#ifdef USE_LOOP_PRIORITY float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } +#endif void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 3e6eda2257a..1998b815f37 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,7 +113,9 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index c574ce6593a..d94767ef070 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -122,8 +122,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index bf752e50c99..6dd15ccb4bf 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -23,7 +23,9 @@ class PCA9554Component : public Component, float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index b7d3848f0eb..fa9496e7e42 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -99,8 +99,10 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +#ifdef USE_LOOP_PRIORITY // Run our loop() method early to invalidate cache before any other components access the pins float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI +#endif void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 5203030142c..23bccc26c9d 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -26,7 +26,9 @@ class PCF8574Component : public Component, void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif void dump_config() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index bfa144526ad..a5c98d90d46 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,7 +30,9 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override { return 50.0f; } +#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 344c1e30707..93a8d4b38e9 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,7 +28,9 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } +#ifdef USE_LOOP_PRIORITY float StatusLED::get_loop_priority() const { return 50.0f; } +#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index 490557f3e78..f262eb260c4 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,7 +14,9 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif protected: GPIOPin *pin_; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 540d0a0ab18..8c1deb62178 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -536,6 +536,13 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) + # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart + # mDNS when the network interface reconnects. However, this callback is disabled + # in the arduino-pico framework. As a workaround, we block component setup until + # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an + # active connection. This define enables the loop priority sorting infrastructure + # used during the setup blocking phase. + cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b3060c7c3e..d5d04193957 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -968,9 +968,11 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP +#ifdef USE_LOOP_PRIORITY float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } +#endif void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5f903e092a0..984930c80cd 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -458,7 +458,9 @@ class WiFiComponent : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; +#ifdef USE_LOOP_PRIORITY float get_loop_priority() const override; +#endif /// Reconnect WiFi if required. void loop() override; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b216233f9b6..c6597897dce 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -109,9 +109,11 @@ void Application::setup() { if (component->can_proceed()) continue; +#ifdef USE_LOOP_PRIORITY // Sort components 0 through i by loop priority insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( this->components_.begin(), this->components_.begin() + i + 1); +#endif do { uint8_t new_app_state = STATUS_LED_WARNING; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b458ea2a84f..b4a19a07765 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,7 +85,9 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +#ifdef USE_LOOP_PRIORITY float Component::get_loop_priority() const { return 0.0f; } +#endif float Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/core/component.h b/esphome/core/component.h index b99641a2753..7ea9fdf3b36 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -117,7 +118,9 @@ class Component { * * @return The loop priority of this component */ +#ifdef USE_LOOP_PRIORITY virtual float get_loop_priority() const; +#endif void call(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5109dd36f43..02335e2745f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -312,6 +312,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) +#define USE_LOOP_PRIORITY #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From ee1f52132573ab5498eaa566d3289d1103a4a186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 16:59:54 -0600 Subject: [PATCH 0144/2056] [http_request] Replace std::list
with std::vector in perform() chain (#14027) --- .../components/http_request/http_request.h | 89 +++++++++++++++---- .../http_request/http_request_arduino.cpp | 2 +- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 2 +- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 2 +- .../http_request/http_request_idf.h | 2 +- .../components/online_image/online_image.cpp | 2 +- 8 files changed, 79 insertions(+), 24 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 2b2d05c63fd..8bdea470b5f 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -330,38 +330,72 @@ class HttpRequestComponent : public Component { void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - std::shared_ptr get(const std::string &url) { return this->start(url, "GET", "", {}); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + std::shared_ptr get(const std::string &url) { + return this->start(url, "GET", "", std::vector
{}); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers, + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { - return this->start(url, "POST", body, {}); + return this->start(url, "POST", body, std::vector
{}); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers) { + const std::vector
&request_headers) { return this->start(url, "POST", body, request_headers); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->start(url, "POST", body, request_headers, lower_case_collect_headers); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers, + const std::vector &collect_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers, + const std::vector &collect_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers) { + // Call perform() directly to avoid ambiguity with the deprecated overloads + return this->perform(url, method, body, request_headers, {}); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers) { - // Call perform() directly to avoid ambiguity with the std::set overload - return this->perform(url, method, body, request_headers, {}); + return this->start(url, method, body, std::vector
(request_headers.begin(), request_headers.end())); } // Remove before 2027.1.0 ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::set &collect_headers) { std::vector lower; lower.reserve(collect_headers.size()); @@ -371,15 +405,39 @@ class HttpRequestComponent : public Component { return this->perform(url, method, body, request_headers, lower); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list, and collect_headers as " + "std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), + lower_case_collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, - const std::string &body, const std::list
&request_headers, + const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; @@ -436,13 +494,10 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); body = json::build_json(f); } - std::list
request_headers; - for (const auto &item : this->request_headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - request_headers.push_back(header); + std::vector
request_headers; + request_headers.reserve(this->request_headers_.size()); + for (const auto &[key, val] : this->request_headers_) { + request_headers.push_back({key, val.value(x...)}); } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 3f60b76b587..56b51e8b5a3 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -26,7 +26,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000; std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index dbd61de3643..d5ce5c0ff3c 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -49,7 +49,7 @@ class HttpContainerArduino : public HttpContainer { class HttpRequestArduino : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; }; diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 714a73fc31d..60ab4d68a04 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -18,7 +18,7 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 79f5b7e8179..52be0e8a16e 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -19,7 +19,7 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 0921c50b9f8..dda61e24007 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -54,7 +54,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 9206ba6f5d7..9ed1a97b1ac 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -37,7 +37,7 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 6f5b82116db..da866599c9d 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -49,7 +49,7 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); - std::list headers; + std::vector headers; // Add caching headers if we have them if (!this->etag_.empty()) { From 5fddce6638f47c296e1267bbeeadaf677277d635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:02:05 -0600 Subject: [PATCH 0145/2056] [logger] Make tx_buffer_ compile-time sized (#14205) --- esphome/components/logger/__init__.py | 3 ++- esphome/components/logger/logger.cpp | 7 ++----- esphome/components/logger/logger.h | 9 +++++---- esphome/components/logger/logger_esp32.cpp | 6 +++--- esphome/components/logger/logger_host.cpp | 4 ++-- esphome/core/defines.h | 1 + tests/dummy_main.cpp | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index c8f3c529112..264197c175d 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -329,10 +329,11 @@ async def to_code(config): level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + tx_buffer_size = config[CONF_TX_BUFFER_SIZE] + cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( config[CONF_ID], baud_rate, - config[CONF_TX_BUFFER_SIZE], ) if CORE.is_esp32: cg.add(log.create_pthread_key()) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 22a95e4835b..497809cd2ee 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,10 +152,7 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { - // add 1 to buffer size for null terminator - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; +Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) @@ -196,7 +193,7 @@ void Logger::process_messages_() { uint16_t text_length; while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8bf1edebb8d..90722ee79c7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,7 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger : public Component { public: - explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); + explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER void init_log_buffer(size_t total_buffer_size); #endif @@ -281,7 +281,7 @@ class Logger : public Component { inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); - LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; + LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; #ifdef USE_STORE_LOG_STR_IN_FLASH if constexpr (std::is_same_v) { this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); @@ -312,7 +312,6 @@ class Logger : public Component { // Group 4-byte aligned members first uint32_t baud_rate_; - char *tx_buffer_{nullptr}; #if defined(USE_ARDUINO) && !defined(USE_ESP32) Stream *hw_serial_{nullptr}; #endif @@ -354,7 +353,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; @@ -371,6 +369,9 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif + // Large buffer placed last to keep frequently-accessed member offsets small + char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator + // --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 9d64771aecf..d6ad77ff4fa 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -89,16 +89,16 @@ void Logger::pre_setup() { switch (this->uart_) { case UART_SELECTION_UART0: this->uart_num_ = UART_NUM_0; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; case UART_SELECTION_UART1: this->uart_num_ = UART_NUM_1; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #ifdef USE_ESP32_VARIANT_ESP32 case UART_SELECTION_UART2: this->uart_num_ = UART_NUM_2; - init_uart(this->uart_num_, baud_rate_, tx_buffer_size_); + init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE); break; #endif #ifdef USE_LOGGER_USB_CDC diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index be12b6df6a2..fe094f6e9e5 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -5,8 +5,8 @@ namespace esphome::logger { void HOT Logger::write_msg_(const char *msg, uint16_t len) { static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" - // tx_buffer_size_ defaults to 512, so 768 covers default + headroom - char buffer[TIMESTAMP_LEN + 768]; + static constexpr size_t HEADROOM = 128; // Extra space for ANSI codes, newline, etc. + char buffer[TIMESTAMP_LEN + ESPHOME_LOGGER_TX_BUFFER_SIZE + HEADROOM]; time_t rawtime; time(&rawtime); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 02335e2745f..1128df94f05 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -21,6 +21,7 @@ // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define ESPHOME_LOGGER_TX_BUFFER_SIZE 512 #define USE_LOG_LISTENERS #define ESPHOME_LOG_MAX_LISTENERS 8 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index e6fe7338070..52f1fbd319c 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -13,7 +13,7 @@ using namespace esphome; void setup() { App.pre_setup("livingroom", "LivingRoom", false); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component(log); From b539a5aa51e08db837d30be5310b917fa61f631b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:07:56 -0600 Subject: [PATCH 0146/2056] [water_heater] Fix device_id missing from state responses (#14212) --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/device_id_in_state.yaml | 115 ++++++++++++ tests/integration/test_device_id_in_state.py | 173 ++++++++++++------ 3 files changed, 232 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9fc263abbd0..7bc9c45c057 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1346,9 +1346,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index c8548617b88..a5dfb7ee45c 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -46,6 +46,7 @@ sensor: binary_sensor: - platform: template + id: motion_detected name: Motion Detected device_id: motion_sensor lambda: return true; @@ -82,3 +83,117 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); + +cover: + - platform: template + name: Garage Door + device_id: motion_sensor + optimistic: true + +fan: + - platform: template + name: Ceiling Fan + device_id: humidity_monitor + speed_count: 3 + has_oscillating: false + has_direction: false + +lock: + - platform: template + name: Front Door Lock + device_id: motion_sensor + optimistic: true + +number: + - platform: template + name: Target Temperature + device_id: temperature_monitor + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: Mode Select + device_id: humidity_monitor + optimistic: true + options: + - "Auto" + - "Manual" + +text: + - platform: template + name: Device Label + device_id: temperature_monitor + optimistic: true + mode: text + +valve: + - platform: template + name: Water Valve + device_id: humidity_monitor + optimistic: true + +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + +water_heater: + - platform: template + name: Test Boiler + device_id: temperature_monitor + optimistic: true + current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" + away: !lambda "return id(global_away);" + is_on: !lambda "return id(global_is_on);" + supported_modes: + - "off" + - electric + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 + set_action: + - lambda: |- + ESP_LOGD("test", "Water heater set"); + +alarm_control_panel: + - platform: template + name: House Alarm + device_id: motion_sensor + codes: + - "1234" + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: motion_detected + +datetime: + - platform: template + name: Schedule Date + device_id: temperature_monitor + type: date + optimistic: true + - platform: template + name: Schedule Time + device_id: humidity_monitor + type: time + optimistic: true + - platform: template + name: Schedule DateTime + device_id: motion_sensor + type: datetime + optimistic: true + +event: + - platform: template + name: Doorbell + device_id: motion_sensor + event_types: + - "press" + - "double_press" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 51088bcbf79..48de94a85a8 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,11 +4,80 @@ from __future__ import annotations import asyncio -from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState +from aioesphomeapi import ( + AlarmControlPanelEntityState, + BinarySensorState, + CoverState, + DateState, + DateTimeState, + EntityState, + FanState, + LightState, + LockEntityState, + NumberState, + SelectState, + SensorState, + SwitchState, + TextSensorState, + TextState, + TimeState, + ValveState, + WaterHeaterState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction +# Mapping of entity name to device name for all entities with device_id +ENTITY_TO_DEVICE = { + # Original entities + "Temperature": "Temperature Monitor", + "Humidity": "Humidity Monitor", + "Motion Detected": "Motion Sensor", + "Temperature Monitor Power": "Temperature Monitor", + "Temperature Status": "Temperature Monitor", + "Motion Light": "Motion Sensor", + # New entity types + "Garage Door": "Motion Sensor", + "Ceiling Fan": "Humidity Monitor", + "Front Door Lock": "Motion Sensor", + "Target Temperature": "Temperature Monitor", + "Mode Select": "Humidity Monitor", + "Device Label": "Temperature Monitor", + "Water Valve": "Humidity Monitor", + "Test Boiler": "Temperature Monitor", + "House Alarm": "Motion Sensor", + "Schedule Date": "Temperature Monitor", + "Schedule Time": "Humidity Monitor", + "Schedule DateTime": "Motion Sensor", + "Doorbell": "Motion Sensor", +} + +# Entities without device_id (should have device_id 0) +NO_DEVICE_ENTITIES = {"No Device Sensor"} + +# State types that should have non-zero device_id, mapped by their aioesphomeapi class +EXPECTED_STATE_TYPES = [ + (SensorState, "sensor"), + (BinarySensorState, "binary_sensor"), + (SwitchState, "switch"), + (TextSensorState, "text_sensor"), + (LightState, "light"), + (CoverState, "cover"), + (FanState, "fan"), + (LockEntityState, "lock"), + (NumberState, "number"), + (SelectState, "select"), + (TextState, "text"), + (ValveState, "valve"), + (WaterHeaterState, "water_heater"), + (AlarmControlPanelEntityState, "alarm_control_panel"), + (DateState, "date"), + (TimeState, "time"), + (DateTimeState, "datetime"), + # Event is stateless (no initial state sent on subscribe) +] + @pytest.mark.asyncio async def test_device_id_in_state( @@ -40,34 +109,35 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - # All entities have name and key attributes - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 + if entity.name in ENTITY_TO_DEVICE: + expected_device = ENTITY_TO_DEVICE[entity.name] + entity_device_mapping[entity.key] = device_ids[expected_device] + elif entity.name in NO_DEVICE_ENTITIES: entity_device_mapping[entity.key] = 0 - assert len(entity_device_mapping) >= 6, ( - f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" + expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES) + assert len(entity_device_mapping) >= expected_count, ( + f"Expected at least {expected_count} mapped entities, " + f"got {len(entity_device_mapping)}. " + f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}" + ) + + # Subscribe to states and wait for all mapped entities + # Event entities are stateless (no initial state on subscribe), + # so exclude them from the expected count + stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"} + stateful_count = len(entity_device_mapping) - len( + stateless_keys & entity_device_mapping.keys() ) - # Subscribe to states loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have states for all mapped entities - if len(states) >= len(entity_device_mapping) and not states_future.done(): + if state.key in entity_device_mapping: + states[state.key] = state + if len(states) >= stateful_count and not states_future.done(): states_future.set_result(True) client.subscribe_states(on_state) @@ -76,9 +146,16 @@ async def test_device_id_in_state( try: await asyncio.wait_for(states_future, timeout=10.0) except TimeoutError: + received_names = {e.name for e in all_entities if e.key in states} + missing_names = ( + (set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES) + - received_names + - {"Doorbell"} + ) pytest.fail( f"Did not receive all entity states within 10 seconds. " - f"Received {len(states)} states, expected {len(entity_device_mapping)}" + f"Received {len(states)} states. " + f"Missing: {missing_names}" ) # Verify each state has the correct device_id @@ -86,51 +163,33 @@ async def test_device_id_in_state( for key, expected_device_id in entity_device_mapping.items(): if key in states: state = states[key] + entity_name = next( + (e.name for e in all_entities if e.key == key), f"key={key}" + ) assert state.device_id == expected_device_id, ( - f"State for key {key} has device_id {state.device_id}, " - f"expected {expected_device_id}" + f"State for '{entity_name}' (type={type(state).__name__}) " + f"has device_id {state.device_id}, expected {expected_device_id}" ) verified_count += 1 - assert verified_count >= 6, ( - f"Only verified {verified_count} states, expected at least 6" + # All stateful entities should be verified (everything except Doorbell event) + expected_verified = expected_count - 1 # exclude Doorbell + assert verified_count >= expected_verified, ( + f"Only verified {verified_count} states, expected at least {expected_verified}" ) - # Test specific state types to ensure device_id is present - # Find a sensor state with device_id - sensor_state = next( - ( + # Verify each expected state type has at least one instance with non-zero device_id + for state_type, type_name in EXPECTED_STATE_TYPES: + matching = [ s for s in states.values() - if isinstance(s, SensorState) - and isinstance(s.state, float) - and s.device_id != 0 - ), - None, - ) - assert sensor_state is not None, "No sensor state with device_id found" - assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id" - - # Find a binary sensor state - binary_sensor_state = next( - (s for s in states.values() if isinstance(s, BinarySensorState)), - None, - ) - assert binary_sensor_state is not None, "No binary sensor state found" - assert binary_sensor_state.device_id > 0, ( - "Binary sensor state should have non-zero device_id" - ) - - # Find a text sensor state - text_sensor_state = next( - (s for s in states.values() if isinstance(s, TextSensorState)), - None, - ) - assert text_sensor_state is not None, "No text sensor state found" - assert text_sensor_state.device_id > 0, ( - "Text sensor state should have non-zero device_id" - ) + if isinstance(s, state_type) and s.device_id != 0 + ] + assert matching, ( + f"No {type_name} state (type={state_type.__name__}) " + f"with non-zero device_id found" + ) # Verify the "No Device Sensor" has device_id = 0 no_device_key = next( From ded457c2c1a5b27aba9af05b7b29a38dd832e7fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 18:52:46 -0600 Subject: [PATCH 0147/2056] [libretiny] Tune oversized lwIP defaults for ESPHome (#14186) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/__init__.py | 4 +- esphome/components/captive_portal/__init__.py | 6 +- esphome/components/esp32/__init__.py | 59 +++---- .../esp32_camera_web_server/__init__.py | 6 +- esphome/components/esphome/ota/__init__.py | 5 +- esphome/components/libretiny/__init__.py | 153 +++++++++++++++++- esphome/components/mdns/__init__.py | 2 +- esphome/components/socket/__init__.py | 90 ++++++++++- esphome/components/udp/__init__.py | 2 +- esphome/components/web_server/__init__.py | 6 +- .../components/web_server_base/__init__.py | 25 ++- esphome/components/wifi/__init__.py | 20 +++ .../socket/test_wake_loop_threadsafe.py | 8 +- 13 files changed, 327 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9bff9f56355..3f7cafb4851 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType: # API needs 1 listening socket + typically 3 concurrent client connections # (not max_connections, which is the upper limit rarely reached) - sockets_needed = 1 + 3 - socket.consume_sockets(sockets_needed, "api")(config) + socket.consume_sockets(3, "api")(config) + socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 049618219e1..6c190814c03 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 TCP sockets for captive portal detection probes + configuration requests # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. # Need headroom for actual user configuration requests. # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + # The listening socket is registered by web_server_base (shared HTTP server). from esphome.components import socket - socket.consume_sockets(4, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 06677006ea3..4c211b2f2ad 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1258,21 +1258,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None: This function runs in to_code() after all components have registered their socket needs. User-provided sdkconfig_options take precedence. """ - from esphome.components.socket import KEY_SOCKET_CONSUMERS + from esphome.components.socket import get_socket_counts # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") - socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) - total_sockets = sum(socket_consumers.values()) - - # Early return if no sockets registered and no user override - if total_sockets == 0 and user_max_sockets is None: - return - - components_list = ", ".join( - f"{name}={count}" for name, count in sorted(socket_consumers.items()) - ) + # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket + # types (TCP clients, TCP listeners, and UDP). Include all three counts. + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -1281,22 +1275,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None: user_max_sockets, ) - # Warn if user's value is less than what components need - if total_sockets > 0: - user_sockets_int = 0 - with contextlib.suppress(ValueError, TypeError): - user_sockets_int = int(user_max_sockets) + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) - if user_sockets_int < total_sockets: - _LOGGER.warning( - "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " - "needs %d sockets (registered: %s). You may experience socket " - "exhaustion errors. Consider increasing to at least %d.", - user_sockets_int, - total_sockets, - components_list, - total_sockets, - ) + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may " + "experience socket exhaustion errors. Consider increasing to " + "at least %d.", + user_sockets_int, + total_sockets, + sc.tcp, + sc.udp, + sc.tcp_listen, + total_sockets, + ) # User's value already added via sdkconfig_options processing return @@ -1305,11 +1300,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None: max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + sock_min = " (min)" if max_sockets > total_sockets else "" _LOGGER.log( log_level, - "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + "Setting CONFIG_LWIP_MAX_SOCKETS to %d%s " + "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, - components_list, + sock_min, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index ed1aaa2e076..da260ad7a1c 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # Each camera web server instance needs 1 listening socket + 2 client connections - sockets_needed = 3 - socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + socket.consume_sockets(2, "esp32_camera_web_server")(config) + socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)( + config + ) return config diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 2f637d714d1..337064dd271 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket - # OTA needs 1 listening socket (client connections are temporary during updates) - socket.consume_sockets(1, "ota")(config) + # OTA needs 1 listening socket. The active transfer connection during an update + # uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom. + socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 01445da7ee7..2291114d9a0 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -275,6 +275,146 @@ BASE_SCHEMA.add_extra(_detect_variant) BASE_SCHEMA.add_extra(_update_core_data) +def _configure_lwip(config: dict) -> None: + """Configure lwIP options for LibreTiny platforms. + + The BK/RTL/LN SDKs each ship different lwIP defaults. BK72XX defaults are + wildly oversized for ESPHome's IoT use case, causing OOM on BK7231N. + RTL87XX and LN882H have more conservative defaults but still need tuning + for ESPHome's socket usage patterns. + + See https://github.com/esphome/esphome/issues/14095 + + Comparison of SDK defaults vs ESPHome targets (TCP_MSS=1460 on all LT): + + Setting ESP8266 ESP32 BK SDK RTL SDK LN SDK New + ──────────────────────────────────────────────────────────────────────────── + TCP_SND_BUF 2×MSS 4×MSS 10×MSS 5×MSS 7×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 3/10×MSS 2×MSS 3×MSS 4×MSS + MEM_LIBC_MALLOC 1 1 0 0 1 1 + MEMP_MEM_MALLOC 1 1 0 0 0 1 + MEM_SIZE N/A* N/A* 16/32KB 5KB N/A* N/A* BK + PBUF_POOL_SIZE 10 16 3/10 20 20 10 BK + MAX_SOCKETS_TCP 5 16 12 —** —** dynamic + MAX_SOCKETS_UDP 4 16 22 —** —** dynamic + TCP_SND_QUEUELEN ~8 17 20 20 35 17 + MEMP_NUM_TCP_SEG 10 16 40 20 =qlen 17 + MEMP_NUM_TCP_PCB 5 16 12 10 8 =TCP + MEMP_NUM_TCP_PCB_LISTEN 4 16 4 5 3 dynamic + MEMP_NUM_UDP_PCB 4 16 25*** 7**** 7**** =UDP + MEMP_NUM_NETCONN 0 10 38 4***** =sum =sum + MEMP_NUM_NETBUF 0 2 16 2***** 8 4 + MEMP_NUM_TCPIP_MSG_INPKT 4 8 16 8***** 12 8 + + * ESP8266/ESP32/LN882H use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool). + ESP8266/ESP32 also use MEMP_MEM_MALLOC=1 (MEMP pools from heap, not static). + ** RTL/LN SDKs don't define MAX_SOCKETS_TCP/UDP (LibreTiny-specific). + *** BK LT overlay: MAX_SOCKETS_UDP+2+1 = 25. + **** RTL/LN LT overlay overrides to flat 7. + ***** Not defined in RTL SDK — lwIP opt.h defaults shown. + "dynamic" = auto-calculated from component socket registrations via + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. + """ + from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, + MIN_TCP_SOCKETS, + MIN_UDP_SOCKETS, + get_socket_counts, + ) + + sc = get_socket_counts() + # Apply platform minimums — ensure headroom for ESPHome's needs + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) + # Listening sockets — registered by components (api, ota, web_server_base, etc.) + # Not all components register yet, so ensure a minimum for baseline operation. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) + + # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per + # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). + # 4×MSS=5,840 matches ESP32. RTL(5×) and LN(7×) are close already. + tcp_snd_buf = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 5×MSS, LN: 7×MSS + + # TCP_WND: receive window. 4×MSS matches ESP32. + # RTL SDK uses only 2×MSS; increasing to 4× is safe and improves throughput. + tcp_wnd = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 2×MSS, LN: 3×MSS + + # TCP_SND_QUEUELEN: max pbufs queued for send buffer + # ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS + # With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32 + tcp_snd_queuelen = 17 # BK: 20, RTL: 20, LN: 35 + # MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check) + memp_num_tcp_seg = tcp_snd_queuelen # BK: 40, RTL: 20, LN: =qlen + + lwip_opts: list[str] = [ + # Disable statistics — not needed for production, saves RAM + "LWIP_STATS=0", # BK: 1, RTL: 0 already, LN: 0 already + "MEM_STATS=0", + "MEMP_STATS=0", + # TCP send buffer — 4×MSS matches ESP32 + f"TCP_SND_BUF={tcp_snd_buf}", + # TCP receive window — 4×MSS matches ESP32 + f"TCP_WND={tcp_wnd}", + # Socket counts — auto-calculated from component registrations + f"MAX_SOCKETS_TCP={tcp_sockets}", + f"MAX_SOCKETS_UDP={udp_sockets}", + # Listening sockets — BK SDK uses this to derive MEMP_NUM_TCP_PCB_LISTEN; + # RTL/LN don't use it, but we set MEMP_NUM_TCP_PCB_LISTEN explicitly below. + f"MAX_LISTENING_SOCKETS_TCP={listening_tcp}", + # Queued segment limits — derived from 4×MSS buffer size + f"TCP_SND_QUEUELEN={tcp_snd_queuelen}", + f"MEMP_NUM_TCP_SEG={memp_num_tcp_seg}", # must be >= queuelen + # PCB pools — active connections + listening sockets + f"MEMP_NUM_TCP_PCB={tcp_sockets}", # BK: 12, RTL: 10, LN: 8 + f"MEMP_NUM_TCP_PCB_LISTEN={listening_tcp}", # BK: =MAX_LISTENING, RTL: 5, LN: 3 + # UDP PCB pool — includes wifi.lwip_internal (DHCP + DNS) + f"MEMP_NUM_UDP_PCB={udp_sockets}", # BK: 25, RTL/LN: 7 via LT + # Netconn pool — each socket (active + listening) needs a netconn + f"MEMP_NUM_NETCONN={tcp_sockets + udp_sockets + listening_tcp}", + # Netbuf pool + "MEMP_NUM_NETBUF=4", # BK: 16, RTL: 2 (opt.h), LN: 8 + # Inbound message pool + "MEMP_NUM_TCPIP_MSG_INPKT=8", # BK: 16, RTL: 8 (opt.h), LN: 12 + ] + + # Use system heap for all lwIP allocations on all LibreTiny platforms. + # - MEM_LIBC_MALLOC=1: Use system heap instead of dedicated lwIP heap. + # LN882H already ships with this. BK SDK defaults to a 16/32KB dedicated + # pool that fragments during OTA. RTL SDK defaults to a 5KB pool. + # All three SDKs wire malloc → pvPortMalloc (FreeRTOS thread-safe heap). + # - MEMP_MEM_MALLOC=1: Allocate MEMP pools from heap on demand instead + # of static arrays. Saves ~20KB RAM on BK72XX. Safe because WiFi + # receive paths run in task context, not ISR context. ESP32 and ESP8266 + # both ship with MEMP_MEM_MALLOC=1. + lwip_opts.append("MEM_LIBC_MALLOC=1") + lwip_opts.append("MEMP_MEM_MALLOC=1") + + # BK72XX-specific: PBUF_POOL_SIZE override + # BK SDK "reduced plan" sets this to only 3 — too few for multiple + # concurrent connections (API + web_server + OTA). BK default plan + # uses 10; match that. RTL(20) and LN(20) need no override. + # With MEMP_MEM_MALLOC=1, this is a max count (allocated on demand). + if CORE.is_bk72xx: + lwip_opts.append("PBUF_POOL_SIZE=10") + + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" + _LOGGER.info( + "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", + tcp_sockets, + tcp_min, + sc.tcp_details, + udp_sockets, + udp_min, + sc.udp_details, + listening_tcp, + listen_min, + sc.tcp_listen_details, + ) + cg.add_platformio_option("custom_options.lwip", lwip_opts) + + # pylint: disable=use-dict-literal async def component_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -389,11 +529,12 @@ async def component_to_code(config): "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS ) - # Disable LWIP statistics to save RAM - not needed in production - # Must explicitly disable all sub-stats to avoid redefinition warnings - cg.add_platformio_option( - "custom_options.lwip", - ["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"], - ) + # Tune lwIP for ESPHome's actual needs. + # The SDK defaults (TCP_SND_BUF=10*MSS, MAX_SOCKETS_TCP=12, MEM_SIZE=32KB) + # are wildly oversized for an IoT device. ESPAsyncWebServer allocates + # malloc(tcp_sndbuf()) per response chunk — at 14.6KB this causes silent + # OOM on memory-constrained chips like BK7231N. + # See https://github.com/esphome/esphome/issues/14095 + _configure_lwip(config) await cg.register_component(var, config) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f87f9296157..420e6a60e30 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -56,7 +56,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # mDNS needs 2 sockets (IPv4 + IPv6 multicast) - socket.consume_sockets(2, "mdns")(config) + socket.consume_sockets(2, "mdns", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e364da78f8f..d82f0c7abad 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,9 +1,14 @@ from collections.abc import Callable, MutableMapping +from dataclasses import dataclass +from enum import StrEnum +import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" @@ -13,33 +18,110 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Socket tracking infrastructure # Components register their socket needs and platforms read this to configure appropriately -KEY_SOCKET_CONSUMERS = "socket_consumers" +KEY_SOCKET_CONSUMERS_TCP = "socket_consumers_tcp" +KEY_SOCKET_CONSUMERS_UDP = "socket_consumers_udp" +KEY_SOCKET_CONSUMERS_TCP_LISTEN = "socket_consumers_tcp_listen" + +# Recommended minimum socket counts. +# Platforms should apply these (or their own) on top of get_socket_counts(). +# These cover minimal configs (e.g. api-only without web_server). +# When web_server is present, its 5 registered sockets push past the TCP minimum. +MIN_TCP_SOCKETS = 8 +MIN_UDP_SOCKETS = 6 +# Minimum listening sockets — at least api + ota baseline. +MIN_TCP_LISTEN_SOCKETS = 2 # Wake loop threadsafe support tracking KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" +class SocketType(StrEnum): + TCP = "tcp" + UDP = "udp" + TCP_LISTEN = "tcp_listen" + + +_SOCKET_TYPE_KEYS = { + SocketType.TCP: KEY_SOCKET_CONSUMERS_TCP, + SocketType.UDP: KEY_SOCKET_CONSUMERS_UDP, + SocketType.TCP_LISTEN: KEY_SOCKET_CONSUMERS_TCP_LISTEN, +} + + def consume_sockets( - value: int, consumer: str + value: int, consumer: str, socket_type: SocketType = SocketType.TCP ) -> Callable[[MutableMapping], MutableMapping]: """Register socket usage for a component. Args: value: Number of sockets needed by the component consumer: Name of the component consuming the sockets + socket_type: Type of socket (SocketType.TCP, SocketType.UDP, or SocketType.TCP_LISTEN) Returns: A validator function that records the socket usage """ + typed_key = _SOCKET_TYPE_KEYS[socket_type] def _consume_sockets(config: MutableMapping) -> MutableMapping: - consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) + consumers: dict[str, int] = CORE.data.setdefault(typed_key, {}) consumers[consumer] = consumers.get(consumer, 0) + value return config return _consume_sockets +def _format_consumers(consumers: dict[str, int]) -> str: + """Format consumer dict as 'name=count, ...' or 'none'.""" + if not consumers: + return "none" + return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items())) + + +@dataclass(frozen=True) +class SocketCounts: + """Socket counts and component details for platform configuration.""" + + tcp: int + udp: int + tcp_listen: int + tcp_details: str + udp_details: str + tcp_listen_details: str + + +def get_socket_counts() -> SocketCounts: + """Return socket counts and component details for platform configuration. + + Platforms call this during code generation to configure lwIP socket limits. + All components will have registered their needs by then. + + Platforms should apply their own minimums on top of these values. + """ + tcp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP, {}) + udp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_UDP, {}) + tcp_listen_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP_LISTEN, {}) + tcp = sum(tcp_consumers.values()) + udp = sum(udp_consumers.values()) + tcp_listen = sum(tcp_listen_consumers.values()) + + tcp_details = _format_consumers(tcp_consumers) + udp_details = _format_consumers(udp_consumers) + tcp_listen_details = _format_consumers(tcp_listen_consumers) + _LOGGER.debug( + "Socket counts: TCP=%d (%s), UDP=%d (%s), TCP_LISTEN=%d (%s)", + tcp, + tcp_details, + udp, + udp_details, + tcp_listen, + tcp_listen_details, + ) + return SocketCounts( + tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + ) + + def require_wake_loop_threadsafe() -> None: """Mark that wake_loop_threadsafe support is required by a component. @@ -66,7 +148,7 @@ def require_wake_loop_threadsafe() -> None: CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe")({}) + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index c9586d0b956..37dd871a6c1 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -73,7 +73,7 @@ def _consume_udp_sockets(config: ConfigType) -> ConfigType: # UDP uses up to 2 sockets: 1 broadcast + 1 listen # Whether each is used depends on code generation, so register worst case - socket.consume_sockets(2, "udp")(config) + socket.consume_sockets(2, "udp", socket.SocketType.UDP)(config) return config diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 9305a2de613..84910b6f90f 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -144,11 +144,11 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType: """Register socket needs for web_server component.""" from esphome.components import socket - # Web server needs 1 listening socket + typically 5 concurrent client connections + # Web server needs typically 5 concurrent client connections # (browser opens connections for page resources, SSE event stream, and POST # requests for entity control which may linger before closing) - sockets_needed = 6 - socket.consume_sockets(sockets_needed, "web_server")(config) + # The listening socket is registered by web_server_base (shared with captive_portal) + socket.consume_sockets(5, "web_server")(config) return config diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 183b907ae61..b587841dfd1 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -23,10 +23,27 @@ web_server_base_ns = cg.esphome_ns.namespace("web_server_base") WebServerBase = web_server_base_ns.class_("WebServerBase") CONF_WEB_SERVER_BASE_ID = "web_server_base_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(WebServerBase), - } + + +def _consume_web_server_base_sockets(config): + """Register the shared listening socket for the HTTP server. + + web_server_base is the shared HTTP server used by web_server and captive_portal. + The listening socket is registered here rather than in each consumer. + """ + from esphome.components import socket + + socket.consume_sockets(1, "web_server_base", socket.SocketType.TCP_LISTEN)(config) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerBase), + } + ), + _consume_web_server_base_sockets, ) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8c1deb62178..b89245b10e2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -58,6 +58,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType from . import wpa2_eap @@ -269,9 +270,28 @@ def final_validate(config): ) +def _consume_wifi_sockets(config: ConfigType) -> ConfigType: + """Register UDP PCBs used internally by lwIP for DHCP and DNS. + + Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw + PCB pool shared by both application sockets and lwIP internals like DHCP/DNS). + On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — + DHCP/DNS use raw udp_new() which bypasses it entirely. + """ + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x): + return config + from esphome.components import socket + + # lwIP allocates UDP PCBs for DHCP client and DNS resolver internally. + # These are not application sockets but consume MEMP_NUM_UDP_PCB pool entries. + socket.consume_sockets(2, "wifi.lwip_internal", socket.SocketType.UDP)(config) + return config + + FINAL_VALIDATE_SCHEMA = cv.All( final_validate, validate_variant, + _consume_wifi_sockets, ) diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index b4bc95176db..a40b6068a8a 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -66,12 +66,12 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - CORE.config = {"logger": {}} # Track initial socket consumer state - initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) # Call require_wake_loop_threadsafe socket.require_wake_loop_threadsafe() # Verify no socket was consumed - consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) - assert "socket.wake_loop_threadsafe" not in consumers - assert consumers == initial_consumers + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + assert udp_consumers == initial_udp From 263fff0ba2fc5ea7b9c849da88600f17852a86aa Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:00 +0100 Subject: [PATCH 0148/2056] Move CONF_OUTPUT_POWER into const.py (#14201) --- esphome/components/cc1101/__init__.py | 2 +- esphome/components/wifi/__init__.py | 2 +- esphome/const.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 14b92a18a40..e2e5986daff 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_OUTPUT_POWER, CONF_VALUE, CONF_WAIT_TIME, ) @@ -22,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) # Config keys -CONF_OUTPUT_POWER = "output_power" CONF_RX_ATTENUATION = "rx_attenuation" CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" CONF_IF_FREQUENCY = "if_frequency" diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b89245b10e2..2aa63b87cc4 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -42,6 +42,7 @@ from esphome.const import ( CONF_ON_CONNECT, CONF_ON_DISCONNECT, CONF_ON_ERROR, + CONF_OUTPUT_POWER, CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_PRIORITY, @@ -344,7 +345,6 @@ def _validate(config): return config -CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( diff --git a/esphome/const.py b/esphome/const.py index 0179aa129e9..ccc9d56dbb0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -748,6 +748,7 @@ CONF_OTA = "ota" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTPUT = "output" CONF_OUTPUT_ID = "output_id" +CONF_OUTPUT_POWER = "output_power" CONF_OUTPUT_SPEAKER = "output_speaker" CONF_OUTPUTS = "outputs" CONF_OVERSAMPLING = "oversampling" From 6e70987451dfce381645e06ec61e87fab745b900 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:30 -0600 Subject: [PATCH 0149/2056] [binary_sensor] Conditionally compile filter infrastructure (#14215) --- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/binary_sensor/binary_sensor.cpp | 6 ++++++ esphome/components/binary_sensor/binary_sensor.h | 6 ++++++ esphome/components/binary_sensor/filter.cpp | 5 +++++ esphome/components/binary_sensor/filter.h | 5 +++++ esphome/core/defines.h | 1 + 6 files changed, 24 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d39..4500168cb1d 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -562,6 +562,7 @@ async def setup_binary_sensor_core_(var, config): if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) cg.add(var.add_filters(filters)) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c3b06970d0..c4d3a29a1e5 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -18,11 +18,15 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } void BinarySensor::publish_state(bool new_state) { +#ifdef USE_BINARY_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->send_state_internal(new_state); +#ifdef USE_BINARY_SENSOR_FILTER } else { this->filter_list_->input(new_state); } +#endif } void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); @@ -47,6 +51,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { return false; } +#ifdef USE_BINARY_SENSOR_FILTER void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; if (this->filter_list_ == nullptr) { @@ -63,6 +68,7 @@ void BinarySensor::add_filters(std::initializer_list filters) { this->add_filter(filter); } } +#endif // USE_BINARY_SENSOR_FILTER bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 83c992bfede..4b655e1bd18 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -2,7 +2,9 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_BINARY_SENSOR_FILTER #include "esphome/components/binary_sensor/filter.h" +#endif #include @@ -45,8 +47,10 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl */ void publish_initial_state(bool new_state); +#ifdef USE_BINARY_SENSOR_FILTER void add_filter(Filter *filter); void add_filters(std::initializer_list filters); +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -60,7 +64,9 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl bool state{}; protected: +#ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; +#endif bool set_new_state(const optional &new_state) override; }; diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index d69671c5bf1..25a69c413a6 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "filter.h" #include "binary_sensor.h" @@ -142,3 +145,5 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 59bc43eeba0..2735a32ab07 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -138,3 +141,5 @@ class SettleFilter : public Filter, public Component { }; } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1128df94f05..99e80785ec8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_ALARM_CONTROL_PANEL #define USE_AREAS #define USE_BINARY_SENSOR +#define USE_BINARY_SENSOR_FILTER #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE From 93ce582ad30d1059cf59335882a6bd231a93127e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:35:51 -0600 Subject: [PATCH 0150/2056] [sensor] Conditionally compile filter infrastructure (#14214) Co-authored-by: Claude Opus 4.6 --- esphome/components/bme680_bsec/bme680_bsec.h | 1 + esphome/components/haier/hon_climate.h | 1 + esphome/components/kamstrup_kmp/kamstrup_kmp.h | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/filter.cpp | 5 +++++ esphome/components/sensor/filter.h | 6 +++++- esphome/components/sensor/sensor.cpp | 6 ++++++ esphome/components/sensor/sensor.h | 8 ++++++++ esphome/components/sound_level/sound_level.h | 1 + esphome/components/wireguard/wireguard.h | 1 + esphome/core/defines.h | 1 + 11 files changed, 31 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index ec919f31df2..22aa2789e6b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -7,6 +7,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/defines.h" #include +#include #ifdef USE_BSEC #include diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 608d5e7f210..4565ed2981d 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,6 +1,7 @@ #pragma once #include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index f84e3601323..725cf20abfa 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ebbe0fbccca..b0e0c28bda6 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -903,6 +903,7 @@ async def setup_sensor_core_(var, config): if config[CONF_FORCE_UPDATE]: cg.add(var.set_force_update(True)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ea0e2f0d7c3..cd4db984576 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include "filter.h" #include #include "esphome/core/application.h" @@ -580,3 +583,5 @@ void StreamingMovingAverageFilter::reset_batch() { } } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 573b916a5d3..8bfcdb37cfb 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -1,6 +1,8 @@ #pragma once -#include +#include "esphome/core/defines.h" +#ifdef USE_SENSOR_FILTER + #include #include #include "esphome/core/automation.h" @@ -638,3 +640,5 @@ class StreamingMovingAverageFilter : public StreamingFilter { }; } // namespace esphome::sensor + +#endif // USE_SENSOR_FILTER diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ae2ee3e3d10..a7af6403efc 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -68,11 +68,15 @@ void Sensor::publish_state(float state) { ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); +#ifdef USE_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->internal_send_state_to_frontend(state); +#ifdef USE_SENSOR_FILTER } else { this->filter_list_->input(state); } +#endif } void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } @@ -80,6 +84,7 @@ void Sensor::add_on_raw_state_callback(std::function &&callback) { this->raw_callback_.add(std::move(callback)); } +#ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -109,6 +114,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_SENSOR_FILTER float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 80981b8e288..54e75ee2a13 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -4,13 +4,17 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_SENSOR_FILTER #include "esphome/components/sensor/filter.h" +#endif #include #include namespace esphome::sensor { +class Sensor; + void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); #define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -67,6 +71,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Set force update mode. void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } +#ifdef USE_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -87,6 +92,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa /// Clear the entire filter chain. void clear_filters(); +#endif /// Getter-syntax for .state. float get_state() const; @@ -130,7 +136,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif // Group small members together to avoid padding int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index dc35f69fe25..a1021eb1e82 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -6,6 +6,7 @@ #include "esphome/components/microphone/microphone_source.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/ring_buffer.h" diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index e8470c75cd9..c11d592cd13 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -4,6 +4,7 @@ #include #include +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/time/real_time_clock.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 99e80785ec8..ff32edff16f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -112,6 +112,7 @@ #define USE_SAFE_MODE_CALLBACK #define USE_SELECT #define USE_SENSOR +#define USE_SENSOR_FILTER #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR From d239a2400dddfb9854ea5e6c7f476002b6bf22c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 21:36:21 -0600 Subject: [PATCH 0151/2056] [text_sensor] Conditionally compile filter infrastructure (#14213) --- esphome/components/text_sensor/__init__.py | 1 + esphome/components/text_sensor/filter.cpp | 5 +++++ esphome/components/text_sensor/filter.h | 5 +++++ esphome/components/text_sensor/text_sensor.cpp | 18 +++++++++++++----- esphome/components/text_sensor/text_sensor.h | 8 ++++++++ esphome/core/defines.h | 1 + 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 0d22400a8eb..2e8edb43c90 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -204,6 +204,7 @@ async def setup_text_sensor_core_(var, config): cg.add(var.set_device_class(device_class)) if config.get(CONF_FILTERS): # must exist and not be empty + cg.add_define("USE_TEXT_SENSOR_FILTER") filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 4ee12e86027..f6552c7c668 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "filter.h" #include "text_sensor.h" #include "esphome/core/log.h" @@ -106,3 +109,5 @@ bool MapFilter::new_value(std::string &value) { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 1922b503ca4..f88e8645ccc 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_TEXT_SENSOR_FILTER + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -164,3 +167,5 @@ class MapFilter : public Filter { } // namespace text_sensor } // namespace esphome + +#endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c48bdf4b828..c66d08ec40a 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -24,7 +24,9 @@ void TextSensor::publish_state(const std::string &state) { this->publish_state(s void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } void TextSensor::publish_state(const char *state, size_t len) { +#ifdef USE_TEXT_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif // No filters: raw_state == state, store once and use for both callbacks // Only assign if changed to avoid heap allocation if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { @@ -33,6 +35,7 @@ void TextSensor::publish_state(const char *state, size_t len) { this->raw_callback_.call(this->state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER } else { // Has filters: need separate raw storage #pragma GCC diagnostic push @@ -46,8 +49,10 @@ void TextSensor::publish_state(const char *state, size_t len) { this->filter_list_->input(this->raw_state); #pragma GCC diagnostic pop } +#endif } +#ifdef USE_TEXT_SENSOR_FILTER void TextSensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of // filters @@ -77,6 +82,7 @@ void TextSensor::clear_filters() { } this->filter_list_ = nullptr; } +#endif // USE_TEXT_SENSOR_FILTER void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); @@ -87,14 +93,16 @@ void TextSensor::add_on_raw_state_callback(std::functionstate; } const std::string &TextSensor::get_raw_state() const { - if (this->filter_list_ == nullptr) { - return this->state; // No filters, raw == filtered - } -// Suppress deprecation warning - get_raw_state() is the replacement API +#ifdef USE_TEXT_SENSOR_FILTER + if (this->filter_list_ != nullptr) { + // Suppress deprecation warning - get_raw_state() is the replacement API #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->raw_state; + return this->raw_state; #pragma GCC diagnostic pop + } +#endif + return this->state; // No filters, raw == filtered } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->internal_send_state_to_frontend(state.data(), state.size()); diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 1352a8c1e48..97373dc7169 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -3,7 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR_FILTER #include "esphome/components/text_sensor/filter.h" +#endif #include #include @@ -11,6 +13,8 @@ namespace esphome { namespace text_sensor { +class TextSensor; + void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj); #define LOG_TEXT_SENSOR(prefix, type, obj) log_text_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) @@ -45,6 +49,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void publish_state(const char *state); void publish_state(const char *state, size_t len); +#ifdef USE_TEXT_SENSOR_FILTER /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -56,6 +61,7 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); +#endif void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. @@ -73,7 +79,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. LazyCallbackManager callback_; ///< Storage for filtered state callbacks. +#ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. +#endif }; } // namespace text_sensor diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ff32edff16f..a1e3d1707fb 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -119,6 +119,7 @@ #define USE_SWITCH #define USE_TEXT #define USE_TEXT_SENSOR +#define USE_TEXT_SENSOR_FILTER #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER From 5c388a5200b65a9e4470d4f4a681e41c49517915 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:39:36 +0100 Subject: [PATCH 0152/2056] [openthread_info] Optimize: Devirtualize/unify (#14208) --- .../openthread_info_text_sensor.h | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index ac5623e0c1a..10e83281f04 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -25,7 +25,7 @@ class OpenThreadInstancePollingComponent : public PollingComponent { virtual void update_instance(otInstance *instance) = 0; }; -class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressOpenThreadInfo final : public PollingComponent, public text_sensor::TextSensor { public: void update() override { std::optional address = openthread::global_openthread_component->get_omr_address(); @@ -48,7 +48,7 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex std::string last_ip_; }; -class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class RoleOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otDeviceRole role = otThreadGetDeviceRole(instance); @@ -64,7 +64,7 @@ class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public tex otDeviceRole last_role_; }; -class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Rloc16OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint16_t rloc16 = otThreadGetRloc16(instance); @@ -75,14 +75,13 @@ class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public t this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_rloc16_; }; -class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ExtAddrOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { const auto *extaddr = otLinkGetExtendedAddress(instance); @@ -93,14 +92,13 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_extaddr_{}; }; -class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class Eui64OpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { otExtAddress addr; @@ -113,14 +111,13 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_eui64_{}; }; -class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { +class ChannelOpenThreadInfo final : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor { public: void update_instance(otInstance *instance) override { uint8_t channel = otLinkGetChannel(instance); @@ -131,7 +128,6 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: @@ -153,7 +149,7 @@ class DatasetOpenThreadInfo : public OpenThreadInstancePollingComponent { virtual void update_dataset(otOperationalDataset *dataset) = 0; }; -class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkNameOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (this->last_network_name_ != dataset->mNetworkName.m8) { @@ -161,14 +157,13 @@ class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sens this->publish_state(this->last_network_name_); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::string last_network_name_; }; -class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class NetworkKeyOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { @@ -178,14 +173,13 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: std::array last_key_{}; }; -class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class PanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { uint16_t panid = dataset->mPanId; @@ -196,14 +190,13 @@ class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::Te this->publish_state(buf); } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: uint16_t last_panid_; }; -class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor { +class ExtPanIdOpenThreadInfo final : public DatasetOpenThreadInfo, public text_sensor::TextSensor { public: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { @@ -214,7 +207,6 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: } } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: From 680160453355210018b073e0036c256d45a143ce Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:40:40 +0100 Subject: [PATCH 0153/2056] [openthread] Add Thread version DEBUG trace (#14196) --- esphome/components/openthread/openthread_esp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index ec212d1f681..4f8db0634a2 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -104,6 +104,8 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion()); + otLinkModeConfig link_mode_config{}; #if CONFIG_OPENTHREAD_FTD link_mode_config.mRxOnWhenIdle = true; From ee94bc471541a460c989b51cf317530b7ffa4611 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:43:42 +0100 Subject: [PATCH 0154/2056] [openthread] Refactor to optimize and match code rules (#14156) --- esphome/components/openthread/openthread.cpp | 4 +-- esphome/components/openthread/openthread.h | 10 +++---- .../components/openthread/openthread_esp.cpp | 29 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 90da17e2d3b..d22a14aeae6 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -35,9 +35,9 @@ void OpenThreadComponent::dump_config() { #elif CONFIG_OPENTHREAD_MTD ESP_LOGCONFIG(TAG, " Device Type: MTD"); // TBD: Synchronized Sleepy End Device - if (this->poll_period > 0) { + if (this->poll_period_ > 0) { ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); - uint32_t duration = this->poll_period / 1000; + uint32_t duration = this->poll_period_ / 1000; ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); } else { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3c60acaaddb..9e429f289b8 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -36,22 +36,22 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); #if CONFIG_OPENTHREAD_MTD - void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } + void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif protected: std::optional get_omr_address_(InstanceLock &lock); + std::function factory_reset_external_callback_; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period_{0}; +#endif bool teardown_started_{false}; bool teardown_complete_{false}; - std::function factory_reset_external_callback_; private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; -#if CONFIG_OPENTHREAD_MTD - uint32_t poll_period{0}; -#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 4f8db0634a2..bb707014715 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -81,9 +81,11 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Fetch OT instance once to avoid repeated call into OT stack + otInstance *instance = esp_openthread_get_instance(); #if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE - ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance())); + ESP_ERROR_CHECK(esp_openthread_state_indicator_init(instance)); #endif #if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC @@ -112,28 +114,29 @@ void OpenThreadComponent::ot_main() { link_mode_config.mDeviceType = true; link_mode_config.mNetworkData = true; #elif CONFIG_OPENTHREAD_MTD - if (this->poll_period > 0) { - if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + if (this->poll_period_ > 0) { + if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pollperiod"); } - uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period); + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); } - link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; link_mode_config.mDeviceType = false; link_mode_config.mNetworkData = false; #endif - if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set linkmode"); } - link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); +#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG + link_mode_config = otThreadGetLinkMode(instance); ESP_LOGD(TAG, "Link Mode Device Type: %s\n" "Link Mode Network Data: %s\n" "Link Mode RX On When Idle: %s", - link_mode_config.mDeviceType ? "true" : "false", link_mode_config.mNetworkData ? "true" : "false", - link_mode_config.mRxOnWhenIdle ? "true" : "false"); + TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), + TRUEFALSE(link_mode_config.mRxOnWhenIdle)); +#endif // Run the main loop #if CONFIG_OPENTHREAD_CLI @@ -144,7 +147,7 @@ void OpenThreadComponent::ot_main() { #ifndef USE_OPENTHREAD_FORCE_DATASET // Check if openthread has a valid dataset from a previous execution - otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); + otError error = otDatasetGetActiveTlvs(instance, &dataset); if (error != OT_ERROR_NONE) { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; From 417f4535af983e90d5ac2407847a29f15cb4905e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 22:25:20 -0600 Subject: [PATCH 0155/2056] [logger] Use subtraction-based line number formatting to avoid division (#14219) --- esphome/components/logger/log_buffer.h | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 3d872782480..a56276f732f 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -75,18 +75,13 @@ struct LogBuffer { *p++ = ':'; - // Format line number without modulo operations + // Format line number using subtraction loops (no division - important for ESP8266 which lacks hardware divider) if (line > 999) [[unlikely]] { - int thousands = line / 1000; - *p++ = '0' + thousands; - line -= thousands * 1000; + write_digit(p, line, 1000); } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - *p++ = '0' + hundreds; - *p++ = '0' + tens; - *p++ = '0' + (remainder - tens * 10); + write_digit(p, line, 100); + write_digit(p, line, 10); + *p++ = '0' + line; *p++ = ']'; #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) @@ -162,6 +157,15 @@ struct LogBuffer { this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); } #endif + // Extract one decimal digit via subtraction (no division - important for ESP8266) + static inline void ESPHOME_ALWAYS_INLINE write_digit(char *&p, int &value, int divisor) { + char d = '0'; + while (value >= divisor) { + d++; + value -= divisor; + } + *p++ = d; + } // Write ANSI color escape sequence to buffer, updates pointer in place // Caller is responsible for ensuring buffer has sufficient space void write_ansi_color_(char *&p, uint8_t level) { From fb6c7d81d5b3832c19eb1dd7b61c70a944742d8a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:08:40 -0500 Subject: [PATCH 0156/2056] [core] Fix multiline log continuations without leading whitespace (#14217) Co-authored-by: Claude Opus 4.6 --- .../absolute_humidity/absolute_humidity.cpp | 5 +- esphome/components/anova/anova.cpp | 6 +- esphome/components/as3935/as3935.cpp | 6 +- esphome/components/at581x/at581x.cpp | 16 ++--- .../components/binary_sensor/automation.cpp | 6 +- esphome/components/bl0940/bl0940.cpp | 5 +- .../current_based/current_based_cover.cpp | 8 +-- esphome/components/debug/debug_esp32.cpp | 60 ++++++++++--------- esphome/components/debug/debug_esp8266.cpp | 18 +++--- esphome/components/debug/debug_libretiny.cpp | 14 +++-- esphome/components/debug/debug_zephyr.cpp | 42 +++++++------ .../components/dfrobot_sen0395/commands.cpp | 16 ++--- esphome/components/emmeti/emmeti.cpp | 19 ++---- .../components/ens160_base/ens160_base.cpp | 13 ++-- esphome/components/es8388/es8388.cpp | 7 ++- .../esp32_ble_client/ble_client_base.cpp | 6 +- .../update/esp32_hosted_update.cpp | 11 ++-- .../packet_transport/espnow_transport.cpp | 8 +-- .../ethernet/ethernet_component.cpp | 5 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 6 +- esphome/components/gcja5/gcja5.cpp | 8 +-- .../graphical_display_menu.cpp | 14 ++--- .../components/honeywellabp/honeywellabp.cpp | 5 +- .../http_request/ota/ota_http_request.cpp | 3 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +- esphome/components/ld2410/ld2410.cpp | 7 +-- esphome/components/ld2412/ld2412.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 6 +- esphome/components/max6956/max6956.cpp | 6 +- .../components/mqtt/mqtt_backend_esp32.cpp | 5 +- .../nextion/nextion_upload_arduino.cpp | 22 ++----- .../nextion/nextion_upload_esp32.cpp | 30 ++-------- .../components/openthread/openthread_esp.cpp | 8 +-- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 7 ++- esphome/components/pipsolar/pipsolar.cpp | 3 +- esphome/components/pn532/pn532.cpp | 5 +- esphome/components/pn7150/pn7150.cpp | 15 +++-- esphome/components/pn7160/pn7160.cpp | 16 +++-- esphome/components/qmp6988/qmp6988.cpp | 26 ++++---- .../remote_base/pronto_protocol.cpp | 5 +- esphome/components/safe_mode/safe_mode.cpp | 10 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/sonoff_d1/sonoff_d1.cpp | 10 +--- esphome/components/sun/sun.cpp | 29 ++++----- .../components/usb_host/usb_host_client.cpp | 44 +++++++------- esphome/components/usb_uart/cp210x.cpp | 6 +- .../voice_assistant/voice_assistant.cpp | 4 +- esphome/components/wl_134/wl_134.cpp | 13 ++-- esphome/components/xgzp68xx/xgzp68xx.cpp | 6 +- script/ci-custom.py | 33 ++++++++++ 50 files changed, 291 insertions(+), 345 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 9c66531d053..40676f8655c 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -92,10 +92,7 @@ void AbsoluteHumidityComponent::loop() { // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); - ESP_LOGD(TAG, - "Saturation vapor pressure %f kPa\n" - "Publishing absolute humidity %f g/m³", - es, absolute_humidity); + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish absolute humidity this->status_clear_warning(); diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 50544880899..2693224a97b 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, - "[%s] No control service found at device, not an Anova..?\n" - "[%s] Note, this component does not currently support Anova Nano.", - this->get_name().c_str(), this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index dd0ab714f7c..c4dc0466a04 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() { uint8_t tune_val = this->read_capacitance(); ESP_LOGI(TAG, "Starting antenna tuning\n" - "Division Ratio is set to: %d\n" - "Internal Capacitor is set to: %d\n" - "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + " Division Ratio is set to: %d\n" + " Internal Capacitor is set to: %d\n" + " Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6804a7f4b5a..728fbe20c69 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } bool AT581XComponent::i2c_write_config() { ESP_LOGCONFIG(TAG, "Writing new config for AT581X\n" - "Frequency: %dMHz\n" - "Sensing distance: %d\n" - "Power: %dµA\n" - "Gain: %d\n" - "Trigger base time: %dms\n" - "Trigger keep time: %dms\n" - "Protect time: %dms\n" - "Self check time: %dms", + " Frequency: %dMHz\n" + " Sensing distance: %d\n" + " Power: %dµA\n" + " Gain: %d\n" + " Trigger base time: %dms\n" + " Trigger keep time: %dms\n" + " Protect time: %dms\n" + " Self check time: %dms", this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_, this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_); diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index faebe7e88fe..7e43d42357c 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -29,10 +29,8 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, - "START min=%" PRIu32 " max=%" PRIu32 "\n" - "Multi Click: Starting multi click action!", - evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); + ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 42e20eb69b9..31625ebf6d6 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -182,7 +182,10 @@ void BL0940::recalibrate_() { ESP_LOGD(TAG, "Recalibrated reference values:\n" - "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + " Voltage: %f\n" + " Current: %f\n" + " Power: %f\n" + " Energy: %f", this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, this->energy_reference_cal_); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 5dfaeeff390..58ae7cbc34a 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -148,14 +148,14 @@ void CurrentBasedCover::dump_config() { } ESP_LOGCONFIG(TAG, " Close Duration: %.1fs\n" - "Obstacle Rollback: %.1f%%", + " Obstacle Rollback: %.1f%%", this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, - "Start sensing delay: %.1fs\n" - "Malfunction detection: %s", + " Start sensing delay: %.1fs\n" + " Malfunction detection: %s", this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aad4c7426c9..6898621dd05 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -79,7 +79,6 @@ const char *DebugComponent::get_reset_reason_(std::span } uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT - ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode); #endif @@ -194,39 +191,46 @@ size_t DebugComponent::get_device_info_(std::span if (info.features != 0) { pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } - ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; - ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - // Framework detection -#ifdef USE_ARDUINO - ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); -#elif defined(USE_ESP32) - ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); -#else - ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); -#endif - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); - - uint8_t mac[6]; - get_mac_address_raw(mac); - ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], - mac[4], mac[5]); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); - pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); + + uint8_t mac[6]; + get_mac_address_raw(mac); + + ESP_LOGD(TAG, + "ESP32 debug info:\n" + " Chip: %s\n" + " Cores: %u\n" + " Revision: %u\n" + " CPU Frequency: %" PRIu32 " MHz\n" + " ESP-IDF Version: %s\n" + " EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Reset Reason: %s\n" + " Wakeup Cause: %s", + model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5], reset_reason, wakeup_cause); +#if defined(USE_ARDUINO) + ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); +#endif + // Framework detection +#ifdef USE_ARDUINO + ESP_LOGD(TAG, " Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); +#else + ESP_LOGD(TAG, " Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); +#endif + + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 1a07ec4f3ac..4df4aaa8513 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -128,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span // NOLINTEND(readability-static-accessed-through-instance) ESP_LOGD(TAG, - "Chip ID: 0x%08" PRIX32 "\n" - "SDK Version: %s\n" - "Core Version: %s\n" - "Boot Version=%u Mode=%u\n" - "CPU Frequency: %u\n" - "Flash Chip ID=0x%08" PRIX32 "\n" - "Reset Reason: %s\n" - "Reset Info: %s", + "ESP8266 debug info:\n" + " Chip ID: 0x%08" PRIX32 "\n" + " SDK Version: %s\n" + " Core Version: %s\n" + " Boot Version: %u\n" + " Boot Mode: %u\n" + " CPU Frequency: %u\n" + " Flash Chip ID: 0x%08" PRIX32 "\n" + " Reset Reason: %s\n" + " Reset Info: %s", chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 14bbdb945a9..39269d6f2f2 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span uint32_t mac_id = lt_cpu_get_mac_id(); ESP_LOGD(TAG, - "LibreTiny Version: %s\n" - "Chip: %s (%04x) @ %u MHz\n" - "Chip ID: 0x%06" PRIX32 "\n" - "Board: %s\n" - "Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n" - "Reset Reason: %s", + "LibreTiny debug info:\n" + " Version: %s\n" + " Chip: %s (%04x) @ %u MHz\n" + " Chip ID: 0x%06" PRIX32 "\n" + " Board: %s\n" + " Flash: %" PRIu32 " KiB\n" + " RAM: %" PRIu32 " KiB\n" + " Reset Reason: %s", lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index ecca7150bd9..bd6432e9499 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -79,13 +79,13 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size\n" - "--------------------------------------------" + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size\n" - "-----------------------------------------" + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); @@ -284,11 +284,12 @@ size_t DebugComponent::get_device_info_(std::span char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, - "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" - "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" - "Device address type: %s, address: %s\n" - "Part code: nRF%x, version: %c%c%c%c, package: %s\n" - "RAM: %ukB, Flash: %ukB, production test: %sdone", + "nRF debug info:\n" + " Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + " Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + " Device address type: %s, address: %s\n" + " Part code: nRF%x, version: %c%c%c%c, package: %s\n" + " RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, @@ -299,23 +300,22 @@ size_t DebugComponent::get_device_info_(std::span (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( - TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO(n_reset_enabled)); if (n_reset_enabled) { uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; - ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin); } #ifdef USE_BOOTLOADER_MCUBOOT - ESP_LOGD(TAG, "bootloader: mcuboot"); + ESP_LOGD(TAG, " Bootloader: mcuboot"); #else - ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, - "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" - "MBR param page addr 0x%08x, UICR param page addr 0x%08x", - read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -326,7 +326,7 @@ size_t DebugComponent::get_device_info_(std::span ver[1] = (sd_version - ver[0] * 1000000) / 1000; ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); - ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); + ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); #ifdef USE_SOFTDEVICE_ID #ifdef USE_SOFTDEVICE_VERSION if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { @@ -352,10 +352,8 @@ size_t DebugComponent::get_device_info_(std::span } return res; }; - ESP_LOGD(TAG, - "NRFFW %s\n" - "NRFHW %s", - uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); return pos; } diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 2c44c6fba97..0f69c82f39d 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated detection area config:\n" - "Detection area 1 from %.02fm to %.02fm.", + " Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { - ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } if (this->min3_ >= 0 && this->max3_ >= 0) { - ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); } if (this->min4_ >= 0 && this->max4_ >= 0) { - ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); } - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet. @@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:\n" - "Signal that someone was detected is delayed by %.03f s.\n" - "Signal that nobody is detected anymore is delayed by %.03f s.", + " Signal that someone was detected is delayed by %.03f s.\n" + " Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_detection_, this->delay_after_disappear_); - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 5286f962b8d..d3e923cbefc 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, - "Expected checksum: %X\n" - "Checksum received: %X", - expected, checksum); + ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum); return checksum == expected; } @@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Swing: %d\n" - "Sleep: %d", - (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Turbo: %d\n" - "Light: %d\n" - "Tree: %d\n" - "Blow: %d", - (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, - (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01, + (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 785b053f04f..e1cee5005cb 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -152,12 +152,13 @@ void ENS160Component::update() { // verbose status logging ESP_LOGV(TAG, - "Status: ENS160 STATAS bit 0x%x\n" - "Status: ENS160 STATER bit 0x%x\n" - "Status: ENS160 VALIDITY FLAG 0x%02x\n" - "Status: ENS160 NEWDAT bit 0x%x\n" - "Status: ENS160 NEWGPR bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + "ENS160 Status Register: 0x%02x\n" + " STATAS bit 0x%x\n" + " STATER bit 0x%x\n" + " VALIDITY FLAG 0x%02x\n" + " NEWDAT bit 0x%x\n" + " NEWGPR bit 0x%x", + status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 9deb29416f3..72026a2a842 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) { }; ESP_LOGV(TAG, - "Setting ES8388_DACPOWER to 0x%02X\n" - "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" - "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + "DAC output config:\n" + " DACPOWER: 0x%02X\n" + " DACCONTROL24/25: 0x%02X\n" + " DACCONTROL26/27: 0x%02X", dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 3f0eeeab4ad..e6a85c784a9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -423,10 +423,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, - "[%d] [%s] Service UUID: %s\n" - "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", - this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index c8e2e879d46..dcd6e643c2e 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -106,11 +106,12 @@ void Esp32HostedUpdate::setup() { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { ESP_LOGD(TAG, - "Firmware version: %s\n" - "Project name: %s\n" - "Build date: %s\n" - "Build time: %s\n" - "IDF version: %s", + "ESP32 Hosted firmware:\n" + " Firmware version: %s\n" + " Project name: %s\n" + " Build date: %s\n" + " Build time: %s\n" + " IDF version: %s", app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 3d16f28c7de..6e4f606466f 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,11 +21,9 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, - "Registering ESP-NOW handlers\n" - "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", - this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], - this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], + this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], + this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f9d98ad51b6..fcd32223e44 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -866,10 +866,7 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi } #endif - ESP_LOGD(TAG, - "Writing to PHY Register Address: 0x%02" PRIX32 "\n" - "Writing to PHY Register Value: 0x%04" PRIX32, - register_data.address, register_data.value); + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9d2f4fc6878..bf6e3926b87 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() { if (current_char == '\0') { ESP_LOGV(TAG, "Read Response from device: %s\n" - "First Component: %s\n" - "Second Component: %s\n" - "Third Component: %s", + " First Component: %s\n" + " Second Component: %s\n" + " Third Component: %s", (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, (char *) third_parameter_buffer); diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index f7f7f8d02c6..43b2fa20d3f 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() { ESP_LOGI(TAG, "GCJA5 Status\n" - "Overall Status : %i\n" - "PD Status : %i\n" - "LD Status : %i\n" - "Fan Status : %i", + " Overall Status : %i\n" + " PD Status : %i\n" + " LD Status : %i\n" + " Fan Status : %i", (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 2b120a746f6..cf1672f217a 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() { void GraphicalDisplayMenu::dump_config() { ESP_LOGCONFIG(TAG, "Graphical Display Menu\n" - "Has Display: %s\n" - "Popup Mode: %s\n" - "Advanced Drawing Mode: %s\n" - "Has Font: %s\n" - "Mode: %s\n" - "Active: %s\n" - "Menu items:", + " Has Display: %s\n" + " Popup Mode: %s\n" + " Advanced Drawing Mode: %s\n" + " Has Font: %s\n" + " Mode: %s\n" + " Active: %s\n" + " Menu items:", YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), YESNO(this->font_ != nullptr), this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_)); diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index c204325dfc7..58c5df230f4 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,10 +35,7 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, - "Sensor pressure_count_ %d\n" - "Sensor temperature_count_ %d", - pressure_count_, temperature_count_); + ESP_LOGV(TAG, "Sensor pressure_count_ %d, temperature_count_ %d", pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 882def4d7f0..d77a7682118 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -105,8 +105,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized\n" - "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index de01c99a191..8a20192c1e4 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -362,10 +362,8 @@ bool INA2XX::configure_shunt_() { ESP_LOGW(TAG, "Shunt value too high"); } this->shunt_cal_ &= 0x7FFF; - ESP_LOGV(TAG, - "Given Rshunt=%f Ohm and Max_current=%.3f\n" - "New CURRENT_LSB=%f, SHUNT_CAL=%u", - this->shunt_resistance_ohm_, this->max_current_a_, this->current_lsb_, this->shunt_cal_); + ESP_LOGV(TAG, "Rshunt=%f Ohm, max current=%.3f A, current LSB=%f, shunt cal=%u", this->shunt_resistance_ohm_, + this->max_current_a_, this->current_lsb_, this->shunt_cal_); return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index f8f782f804a..dd1d53857d0 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -489,11 +489,8 @@ bool LD2410Component::handle_ack_data_() { this->out_pin_level_ = this->buffer_data_[12]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u\n" - "Out pin level: %s", - light_function_str, this->light_threshold_, out_pin_level_str); + ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_, + out_pin_level_str); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 95e19e0d5fe..484d5bd281c 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -530,10 +530,7 @@ bool LD2412Component::handle_ack_data_() { this->light_function_ = this->buffer_data_[10]; this->light_threshold_ = this->buffer_data_[11]; const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); - ESP_LOGV(TAG, - "Light function: %s\n" - "Light threshold: %u", - light_function_str, this->light_threshold_); + ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_); #ifdef USE_SELECT if (this->light_function_select_ != nullptr) { this->light_function_select_->publish_state(light_function_str); diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a203dde1154..a01d42ac8b2 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -130,10 +130,8 @@ void LEDCOutput::setup() { } int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, - "Configured frequency %f with a bit depth of %u bits\n" - "Angle of %.1f° results in hpoint %u", - this->frequency_, this->bit_depth_, this->phase_angle_, hpoint); + ESP_LOGV(TAG, "Configured frequency %f with bit depth %u, angle %.1f° hpoint %u", this->frequency_, this->bit_depth_, + this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = this->pin_->get_pin(); diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 6ba17f11d11..a350e66ee0e 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -146,11 +146,11 @@ void MAX6956::dump_config() { if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { ESP_LOGCONFIG(TAG, - "current mode: global\n" - "global brightness: %u", + " Current mode: global\n" + " Brightness: %u", global_brightness_); } else { - ESP_LOGCONFIG(TAG, "current mode: segment"); + ESP_LOGCONFIG(TAG, " Current mode: segment"); } } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index c12c79499fc..8a7fb965e96 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -165,10 +165,7 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, - "Last error code reported from esp-tls: 0x%x\n" - "Last tls stack error number: 0x%x\n" - "Last captured errno : %d (%s)", + ESP_LOGE(TAG, "Last esp-tls error: 0x%x, tls stack error: 0x%x, socket errno: %d (%s)", event.error_handle.esp_tls_last_esp_err, event.error_handle.esp_tls_stack_err, event.error_handle.esp_transport_sock_errno, strerror(event.error_handle.esp_transport_sock_errno)); } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index a433eff883b..46a04c1b2e1 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -25,11 +25,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGE(TAG, "Invalid range"); - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -138,11 +134,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -172,10 +164,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - EspClass::getFreeHeap()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along @@ -262,10 +251,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, - "Heap: %" PRIu32 "\n" - "Upload cmd: %s", - EspClass::getFreeHeap(), command); + ESP_LOGV(TAG, "Heap: %" PRIu32 ", upload cmd: %s", EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 46352afd750..43f59a8d4b4 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -27,11 +27,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, - "Range end: %" PRIu32 "\n" - "Range size: %" PRIu32, - range_end, range_size); - ESP_LOGE(TAG, "Invalid range"); + ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -159,11 +155,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, - "TFT upload requested\n" - "Exit reparse: %s\n" - "URL: %s", - YESNO(exit_reparse), this->tft_url_.c_str()); + ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -193,10 +185,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, - "Init HTTP client\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -220,10 +209,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, - "Check connection\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check connection, heap: %" PRIu32, esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -232,10 +218,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, - "Check status\n" - "Heap: %" PRIu32, - esp_get_free_heap_size()); + ESP_LOGV(TAG, "Check status, heap: %" PRIu32, esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", status_code); @@ -344,8 +327,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete\n" - "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete, closing HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index bb707014715..9dd68a1ccce 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -130,10 +130,7 @@ void OpenThreadComponent::ot_main() { } #ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG link_mode_config = otThreadGetLinkMode(instance); - ESP_LOGD(TAG, - "Link Mode Device Type: %s\n" - "Link Mode Network Data: %s\n" - "Link Mode RX On When Idle: %s", + ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif @@ -152,8 +149,7 @@ void OpenThreadComponent::ot_main() { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; } else { - ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration\n" - "(set force_dataset: true to override)"); + ESP_LOGI(TAG, "Found existing dataset, ignoring config (force_dataset: true to override)"); } #endif diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index fdff11dedb4..9247e114f04 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -144,9 +144,10 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, - "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" - "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + "Wrote GPIO config:\n" + " modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + " pull enable: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), BYTE_TO_BINARY(this->pull_enable_mask_)); #endif diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index e6831ad19e0..f95bf4aedb9 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -748,8 +748,7 @@ esphome::optional Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) { } void Pipsolar::dump_config() { - ESP_LOGCONFIG(TAG, "Pipsolar:\n" - "enabled polling commands:"); + ESP_LOGCONFIG(TAG, "Pipsolar enabled polling commands:"); for (auto &enabled_polling_command : this->enabled_polling_commands_) { if (enabled_polling_command.length != 0) { ESP_LOGCONFIG(TAG, "%s", enabled_polling_command.command); diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 5366aab54e4..1ab0da3df78 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,10 +31,7 @@ void PN532::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, - "Found chip PN5%02X\n" - "Firmware ver. %d.%d", - version_data[0], version_data[1], version_data[2]); + ESP_LOGD(TAG, "Found chip PN5%02X, Firmware v%d.%d", version_data[0], version_data[1], version_data[2]); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 7bec1e08a96..8c76c8b88c9 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -243,9 +243,7 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); @@ -274,11 +272,12 @@ uint8_t PN7150::init_core_() { uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; ESP_LOGD(TAG, - "Manufacturer ID: 0x%02X\n" - "Hardware version: 0x%02X\n" - "ROM code version: 0x%02X\n" - "FLASH major version: 0x%02X\n" - "FLASH minor version: 0x%02X", + "PN7150 chip info:\n" + " Manufacturer ID: 0x%02X\n" + " Hardware version: 0x%02X\n" + " ROM code version: 0x%02X\n" + " FLASH major version: 0x%02X\n" + " FLASH minor version: 0x%02X", manf_id, hw_version, rom_code_version, flash_major_version, flash_minor_version); return rx.get_simple_status_response(); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 28907b8e30a..3fcd1221a72 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -265,10 +265,7 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, - "Configuration %s\n" - "NCI version: %s\n" - "Manufacturer ID: 0x%02X", + ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X", rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0", rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); @@ -301,11 +298,12 @@ uint8_t PN7160::init_core_() { char feat_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, - "Hardware version: %u\n" - "ROM code version: %u\n" - "FLASH major version: %u\n" - "FLASH minor version: %u\n" - "Features: %s", + "PN7160 chip info:\n" + " Hardware version: %u\n" + " ROM code version: %u\n" + " FLASH major version: %u\n" + " FLASH minor version: %u\n" + " Features: %s", hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes_to(feat_buf, features)); diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 4e1ef27d5e1..24fe34e7852 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -128,15 +128,14 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, - "<-----------calibration data-------------->\n" - "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]", + "Calibration data:\n" + " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" + " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, - qmp6988_data_.qmp6988_cali.COE_b00); - ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, - qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); - ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, + qmp6988_data_.qmp6988_cali.COE_b00, qmp6988_data_.qmp6988_cali.COE_bt1, qmp6988_data_.qmp6988_cali.COE_bt2, + qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11, qmp6988_data_.qmp6988_cali.COE_bp2, qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3); - ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 @@ -153,14 +152,13 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, - "<----------- int calibration data -------------->\n" - "a0[%d] a1[%d] a2[%d] b00[%d]", - qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); - ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, - qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); - ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, + "Int calibration data:\n" + " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" + " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", + qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, + qmp6988_data_.ik.bt2, qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11, qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3); - ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); return true; } diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index cff31451998..43029cbc2fc 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -104,10 +104,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vectorlabel); + ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); + ESP_LOGW(TAG, "The device reset before the boot was marked successful"); if (esp_reset_reason() == ESP_RST_BROWNOUT) { - ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" - "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!"); + ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } #endif diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 251e18648b6..2115c72cefa 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -326,7 +326,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (ok || message.compare(0, 6, "+CMGL:") == 0) { ESP_LOGD(TAG, "Received SMS from: %s\n" - "%s", + " %s", this->sender_.c_str(), this->message_.c_str()); this->sms_received_callback_.call(this->message_, this->sender_); this->state_ = STATE_RECEIVED_SMS; diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 7b990865468..03586b63984 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -93,10 +93,7 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { if (this->read_array(cmd, 6)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(6)]; - ESP_LOGV(TAG, - "[%04d] Reading from dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); + ESP_LOGV(TAG, "[%04d] Reading from dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); #endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { @@ -190,10 +187,7 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a do { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; - ESP_LOGV(TAG, - "[%04d] Writing to the dimmer:\n" - "[%04d] %s", - this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); + ESP_LOGV(TAG, "[%04d] Writing to the dimmer: %s", this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); #endif this->write_array(cmd, len); this->write_count_++; diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index e8fc4e44d1e..d55a14f192e 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -174,20 +174,21 @@ struct SunAtTime { // debug output like in example 25.a, p. 165 auto eq = equatorial_coordinate(); ESP_LOGV(TAG, - "jde: %f\n" - "T: %f\n" - "L_0: %f\n" - "M: %f\n" - "e: %f\n" - "C: %f\n" - "Odot: %f\n" - "Omega: %f\n" - "lambda: %f\n" - "epsilon_0: %f\n" - "epsilon: %f\n" - "v: %f\n" - "right_ascension: %f\n" - "declination: %f", + "Sun position:\n" + " jde: %f\n" + " T: %f\n" + " L_0: %f\n" + " M: %f\n" + " e: %f\n" + " C: %f\n" + " Odot: %f\n" + " Omega: %f\n" + " lambda: %f\n" + " epsilon_0: %f\n" + " epsilon: %f\n" + " v: %f\n" + " right_ascension: %f\n" + " declination: %f", jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(), apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension, eq.declination); diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index a9be38fb03f..422d74095c6 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -70,14 +70,14 @@ static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { ESP_LOGV(TAG, "*** Configuration descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "wTotalLength %d\n" - "bNumInterfaces %d\n" - "bConfigurationValue %d\n" - "iConfiguration %d\n" - "bmAttributes 0x%x\n" - "bMaxPower %dmA", + " bLength %d\n" + " bDescriptorType %d\n" + " wTotalLength %d\n" + " bNumInterfaces %d\n" + " bConfigurationValue %d\n" + " iConfiguration %d\n" + " bmAttributes 0x%x\n" + " bMaxPower %dmA", cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces, cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2); } @@ -89,20 +89,20 @@ static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_des ESP_LOGV(TAG, "*** Device descriptor ***\n" - "bLength %d\n" - "bDescriptorType %d\n" - "bcdUSB %d.%d0\n" - "bDeviceClass 0x%x\n" - "bDeviceSubClass 0x%x\n" - "bDeviceProtocol 0x%x\n" - "bMaxPacketSize0 %d\n" - "idVendor 0x%x\n" - "idProduct 0x%x\n" - "bcdDevice %d.%d0\n" - "iManufacturer %d\n" - "iProduct %d\n" - "iSerialNumber %d\n" - "bNumConfigurations %d", + " bLength %d\n" + " bDescriptorType %d\n" + " bcdUSB %d.%d0\n" + " bDeviceClass 0x%x\n" + " bDeviceSubClass 0x%x\n" + " bDeviceProtocol 0x%x\n" + " bMaxPacketSize0 %d\n" + " idVendor 0x%x\n" + " idProduct 0x%x\n" + " bcdDevice %d.%d0\n" + " iManufacturer %d\n" + " iProduct %d\n" + " iSerialNumber %d\n" + " bNumConfigurations %d", devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass, devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct, diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 483286560a2..fa8c2331b2c 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -58,10 +58,8 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - ESP_LOGD(TAG, - "bDeviceClass: %u, bDeviceSubClass: %u\n" - "bNumInterfaces: %u", - device_desc->bDeviceClass, device_desc->bDeviceSubClass, config_desc->bNumInterfaces); + ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u, bNumInterfaces: %u", device_desc->bDeviceClass, + device_desc->bDeviceSubClass, config_desc->bNumInterfaces); if (device_desc->bDeviceClass != 0) { ESP_LOGE(TAG, "bDeviceClass != 0"); return {}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 641d4d6ff88..d6cbfd4b215 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -434,8 +434,8 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr char new_peername[socket::SOCKADDR_STR_LEN]; ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant\n" - "Current client: %s (%s)\n" - "New client: %s (%s)", + " Current client: %s (%s)\n" + " New client: %s (%s)", this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(), client->get_peername_to(new_peername)); return; diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index a589f71c84c..a902adfddd0 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -70,12 +70,13 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); ESP_LOGV(TAG, - "Tag id: %012lld\n" - "Country: %03d\n" - "isData: %s\n" - "isAnimal: %s\n" - "Reserved0: %d\n" - "Reserved1: %" PRId32, + "RFID134 Tag:\n" + " Tag id: %012lld\n" + " Country: %03d\n" + " isData: %s\n" + " isAnimal: %s\n" + " Reserved0: %d\n" + " Reserved1: %" PRId32, reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", reading.reserved0, reading.reserved1); diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index b5b786c105b..5e816469acf 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -72,10 +72,8 @@ void XGZP68XXComponent::update() { temperature_raw = encode_uint16(data[3], data[4]); // Convert the pressure data to hPa - ESP_LOGV(TAG, - "Got raw pressure=%" PRIu32 ", raw temperature=%u\n" - "K value is %u", - pressure_raw, temperature_raw, this->k_value_); + ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u, K value=%u", pressure_raw, temperature_raw, + this->k_value_); // Sign extend the pressure float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); diff --git a/script/ci-custom.py b/script/ci-custom.py index df819e0f049..85a446ba0dd 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,39 @@ def lint_no_scanf(fname, match): ) +LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) +LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') +LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') + + +@lint_content_check(include=cpp_include) +def lint_log_multiline_continuation(fname, content): + errs = [] + for log_match in LOG_MULTILINE_RE.finditer(content): + log_text = log_match.group(0) + for bad_match in LOG_BAD_CONTINUATION_RE.finditer(log_text): + # %s may expand to a whitespace prefix at runtime, skip those + if LOG_PERCENT_S_CONTINUATION_RE.match(log_text, bad_match.start()): + continue + # Calculate line number from position in full content + abs_pos = log_match.start() + bad_match.start() + lineno = content.count("\n", 0, abs_pos) + 1 + col = abs_pos - content.rfind("\n", 0, abs_pos) + errs.append( + ( + lineno, + col, + "Multi-line log message has a continuation line that does " + "not start with a space. The log viewer uses leading " + "whitespace to detect continuation lines and re-add the " + f"log tag prefix (e.g. {highlight('[C][component:042]:')}).\n" + "Either start the continuation with a space/indent, or " + "split into separate ESP_LOG* calls.", + ) + ) + return errs + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 1f945a334a472420d3f3e62c453f3ab3c5246c27 Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Tue, 24 Feb 2026 04:01:23 +1100 Subject: [PATCH 0157/2056] [hdc302x] Add new component (#10160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc302x/__init__.py | 1 + esphome/components/hdc302x/hdc302x.cpp | 171 ++++++++++++++++++ esphome/components/hdc302x/hdc302x.h | 68 +++++++ esphome/components/hdc302x/sensor.py | 135 ++++++++++++++ tests/components/hdc302x/common.yaml | 19 ++ tests/components/hdc302x/test.esp32-idf.yaml | 4 + .../components/hdc302x/test.esp8266-ard.yaml | 4 + tests/components/hdc302x/test.rp2040-ard.yaml | 4 + 9 files changed, 407 insertions(+) create mode 100644 esphome/components/hdc302x/__init__.py create mode 100644 esphome/components/hdc302x/hdc302x.cpp create mode 100644 esphome/components/hdc302x/hdc302x.h create mode 100644 esphome/components/hdc302x/sensor.py create mode 100644 tests/components/hdc302x/common.yaml create mode 100644 tests/components/hdc302x/test.esp32-idf.yaml create mode 100644 tests/components/hdc302x/test.esp8266-ard.yaml create mode 100644 tests/components/hdc302x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2aa0656343c..6728e76bbad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py new file mode 100644 index 00000000000..d8165281bfe --- /dev/null +++ b/esphome/components/hdc302x/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@joshuasing"] diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp new file mode 100644 index 00000000000..b50d34169a4 --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -0,0 +1,171 @@ +#include "hdc302x.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::hdc302x { + +static const char *const TAG = "hdc302x.sensor"; + +// Commands (per datasheet Table 7-4) +static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; +static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; + +static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24; + +static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d}; +static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66}; +static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e}; + +void HDC302XComponent::setup() { + // Soft reset the device + if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Soft reset failed")); + return; + } + // Delay SensorRR (reset ready), per datasheet, 6.5. + delay(3); + + // Clear status register + if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Clear status failed")); + return; + } +} + +void HDC302XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "HDC302x:\n" + " Heater: %s", + this->heater_active_ ? "active" : "inactive"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temp_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC302XComponent::update() { + uint8_t cmd[] = { + HDC302X_CMD_TRIGGER_MSB, + this->power_mode_, + }; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Read data after ADC conversion has completed + this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); }); +} + +void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { + if (!this->disable_heater_()) { + ESP_LOGD(TAG, "Heater disable before start failed"); + } + if (!this->configure_heater_(power) || !this->enable_heater_()) { + ESP_LOGW(TAG, "Heater start failed"); + return; + } + this->heater_active_ = true; + this->cancel_timeout("heater_off"); + if (duration_ms > 0) { + this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + } +} + +void HDC302XComponent::stop_heater() { + this->cancel_timeout("heater_off"); + if (!this->disable_heater_()) { + ESP_LOGW(TAG, "Heater stop failed"); + } + this->heater_active_ = false; +} + +bool HDC302XComponent::enable_heater_() { + if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Enable heater failed"); + return false; + } + return true; +} + +bool HDC302XComponent::configure_heater_(uint16_t power_level) { + if (power_level > 0x3fff) { + ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level); + return false; + } + + // Heater current level config. + uint8_t config[] = { + static_cast((power_level >> 8) & 0xff), // MSB + static_cast(power_level & 0xff) // LSB + }; + + // Configure level of heater current (per datasheet 7.5.7.8). + uint8_t cmd[] = { + HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1], + crc8(config, 2, 0xff, 0x31, true), + }; + if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Configure heater failed"); + return false; + } + + return true; +} + +bool HDC302XComponent::disable_heater_() { + if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Disable heater failed"); + return false; + } + return true; +} + +void HDC302XComponent::read_data_() { + uint8_t buf[6]; + if (this->read(buf, 6) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Check checksums + if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) { + this->status_set_warning(LOG_STR("Read data: invalid CRC")); + return; + } + + this->status_clear_warning(); + + if (this->temp_sensor_ != nullptr) { + uint16_t raw_t = encode_uint16(buf[0], buf[1]); + // Calculate temperature in Celsius per datasheet section 7.3.3. + float temp = -45 + 175 * (float(raw_t) / 65535.0f); + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + uint16_t raw_rh = encode_uint16(buf[3], buf[4]); + // Calculate RH% per datasheet section 7.3.3. + float humidity = 100 * (float(raw_rh) / 65535.0f); + this->humidity_sensor_->publish_state(humidity); + } +} + +uint32_t HDC302XComponent::conversion_delay_ms_() { + // ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand + switch (this->power_mode_) { + case HDC302XPowerMode::BALANCED: + return 8; + case HDC302XPowerMode::LOW_POWER: + return 5; + case HDC302XPowerMode::ULTRA_LOW_POWER: + return 4; + case HDC302XPowerMode::HIGH_ACCURACY: + default: + return 13; + } +} + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/hdc302x.h b/esphome/components/hdc302x/hdc302x.h new file mode 100644 index 00000000000..6afea0a8c0f --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::hdc302x { + +enum HDC302XPowerMode : uint8_t { + HIGH_ACCURACY = 0x00, + BALANCED = 0x0b, + LOW_POWER = 0x16, + ULTRA_LOW_POWER = 0xff, +}; + +/** + HDC302x Temperature and humidity sensor. + + Datasheet: + https://www.ti.com/lit/ds/symlink/hdc3020.pdf + */ +class HDC302XComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + void start_heater(uint16_t power, uint32_t duration_ms); + void stop_heater(); + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; } + + protected: + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY}; + bool heater_active_{false}; + + bool enable_heater_(); + bool configure_heater_(uint16_t power_level); + bool disable_heater_(); + void read_data_(); + uint32_t conversion_delay_ms_(); +}; + +template class HeaterOnAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, power) + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto power_val = this->power_.value(x...); + auto duration_val = this->duration_.value(x...); + this->parent_->start_heater(power_val, duration_val); + } +}; + +template class HeaterOffAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_heater(); } +}; + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py new file mode 100644 index 00000000000..7215a4cfb7a --- /dev/null +++ b/esphome/components/hdc302x/sensor.py @@ -0,0 +1,135 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DURATION, + CONF_HUMIDITY, + CONF_ID, + CONF_POWER, + CONF_POWER_MODE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc302x_ns = cg.esphome_ns.namespace("hdc302x") +HDC302XComponent = hdc302x_ns.class_( + "HDC302XComponent", cg.PollingComponent, i2c.I2CDevice +) + +HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode") +POWER_MODE_OPTIONS = { + "HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY, + "BALANCED": HDC302XPowerMode.BALANCED, + "LOW_POWER": HDC302XPowerMode.LOW_POWER, + "ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER, +} + +# Actions +HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action) +HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC302XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum( + POWER_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2. +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temp_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + + +# HDC302x heater power configs, per datasheet Table 7-15. +HDC302X_HEATER_POWER_MAP = { + "QUARTER": 0x009F, + "HALF": 0x03FF, + "FULL": 0x3FFF, +} + + +def heater_power_value(value): + """Accept enum names or raw uint16 values""" + if isinstance(value, cv.Lambda): + return value + if isinstance(value, str): + upper = value.upper() + if upper in HDC302X_HEATER_POWER_MAP: + return HDC302X_HEATER_POWER_MAP[upper] + raise cv.Invalid( + f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383" + ) + return cv.int_range(min=0, max=0x3FFF)(value) + + +HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)}) + +HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HDC302XComponent), + cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value), + cv.Optional(CONF_DURATION, default="5s"): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) + + +@automation.register_action( + "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA +) +async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) + cg.add(var.set_power(template_)) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA +) +async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/components/hdc302x/common.yaml b/tests/components/hdc302x/common.yaml new file mode 100644 index 00000000000..56bb31effe5 --- /dev/null +++ b/tests/components/hdc302x/common.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - hdc302x.heater_on: + id: hdc302x_sensor + power: QUARTER + duration: 5s + - hdc302x.heater_off: + id: hdc302x_sensor + +sensor: + - platform: hdc302x + id: hdc302x_sensor + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc302x/test.esp32-idf.yaml b/tests/components/hdc302x/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/hdc302x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.esp8266-ard.yaml b/tests/components/hdc302x/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/hdc302x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc302x/test.rp2040-ard.yaml b/tests/components/hdc302x/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/hdc302x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From e199145f1c3b168e8584c4613f1d814c1694a4f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 12:20:55 -0600 Subject: [PATCH 0158/2056] [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) --- esphome/core/lock_free_queue.h | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index e96b739b585..522fbd36e1a 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -38,13 +38,27 @@ template class LockFreeQueue { } protected: + // Advance ring buffer index by one, wrapping at SIZE. + // Power-of-2 sizes use modulo (compiler emits single mask instruction). + // Non-power-of-2 sizes use comparison to avoid expensive multiply-shift sequences. + static constexpr uint8_t next_index(uint8_t index) { + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (index + 1) % SIZE; + } else { + uint8_t next = index + 1; + if (next >= SIZE) [[unlikely]] + next = 0; + return next; + } + } + // Internal push that reports queue state - for use by derived classes bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) { if (element == nullptr) return false; uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; + uint8_t next_tail = next_index(current_tail); // Read head before incrementing tail uint8_t head_before = head_.load(std::memory_order_acquire); @@ -73,14 +87,21 @@ template class LockFreeQueue { } T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); + head_.store(next_index(current_head), std::memory_order_release); return element; } size_t size() const { uint8_t tail = tail_.load(std::memory_order_acquire); uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; + if constexpr ((SIZE & (SIZE - 1)) == 0) { + return (tail - head + SIZE) % SIZE; + } else { + int diff = static_cast(tail) - static_cast(head); + if (diff < 0) + diff += SIZE; + return static_cast(diff); + } } uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } @@ -90,7 +111,7 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire); } From 0d32a5321ca20943eea2d5c9c9d3b3aeac44b568 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:46:53 -0500 Subject: [PATCH 0159/2056] [remote_transmitter/remote_receiver] Rename _esp32.cpp to _rmt.cpp (#14226) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/remote_receiver/__init__.py | 2 +- .../{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} | 0 esphome/components/remote_transmitter/__init__.py | 2 +- ...{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename esphome/components/remote_receiver/{remote_receiver_esp32.cpp => remote_receiver_rmt.cpp} (100%) rename esphome/components/remote_transmitter/{remote_transmitter_esp32.cpp => remote_transmitter_rmt.cpp} (100%) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 362f6e99db4..53a0f8fb778 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -237,7 +237,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_receiver_esp32.cpp": { + "remote_receiver_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp similarity index 100% rename from esphome/components/remote_receiver/remote_receiver_esp32.cpp rename to esphome/components/remote_receiver/remote_receiver_rmt.cpp diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index fc772f88b22..371dbb685f7 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -171,7 +171,7 @@ async def to_code(config): FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "remote_transmitter_esp32.cpp": { + "remote_transmitter_rmt.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp similarity index 100% rename from esphome/components/remote_transmitter/remote_transmitter_esp32.cpp rename to esphome/components/remote_transmitter/remote_transmitter_rmt.cpp From daee71a2c17871b1f23e782059ae9e906abeb9f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:21:29 -0500 Subject: [PATCH 0160/2056] [http_request] Retry update check on startup until network is ready (#14228) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../update/http_request_update.cpp | 24 ++++++++++++++++++- .../http_request/update/http_request_update.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 85609bd31f4..1900f69a691 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -24,8 +24,29 @@ namespace http_request { static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; +static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6; -void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } +void HttpRequestUpdate::setup() { + this->ota_parent_->add_state_listener(this); + + // Check periodically until network is ready + // Only if update interval is > total retry window to avoid redundant checks + if (this->get_update_interval() != SCHEDULER_DONT_RUN && + this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) { + this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->update(); + } + } + }); + } +} void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { @@ -45,6 +66,7 @@ void HttpRequestUpdate::update() { ESP_LOGD(TAG, "Network not connected, skipping update check"); return; } + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index cf34ace18eb..b8350346f9e 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -40,6 +40,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32 TaskHandle_t update_task_handle_{nullptr}; #endif + uint8_t initial_check_remaining_{0}; }; } // namespace http_request From 063c6a9e45576eb5b22c94977cddac1f1eab1aef Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:06:20 +0100 Subject: [PATCH 0161/2056] [esp32,core] Move CONF_ENABLE_OTA_ROLLBACK to core (#14231) --- esphome/components/esp32/__init__.py | 2 +- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4c211b2f2ad..62367443dae 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, @@ -90,7 +91,6 @@ CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" -CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" diff --git a/esphome/const.py b/esphome/const.py index ccc9d56dbb0..0b1037d0918 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -354,6 +354,7 @@ CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" +CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_PRIVATE_NETWORK_ACCESS = "enable_private_network_access" CONF_ENABLE_RRM = "enable_rrm" From 918bbfb0d3c73be7616fc7e7ab57238f27bc35ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:22:32 +0000 Subject: [PATCH 0162/2056] Bump aioesphomeapi from 44.0.0 to 44.1.0 (#14232) 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 be3445dceb0..d22097b3caa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 -aioesphomeapi==44.0.0 +aioesphomeapi==44.1.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 02c37bb6d63d9ccb457a13f35b6f2e3c80c35cc9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 23 Feb 2026 21:23:40 +0100 Subject: [PATCH 0163/2056] [nrf52,logger] generate crash magic in python (#14173) --- esphome/components/logger/logger_zephyr.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index c2d24d6efc4..6b46b93c61e 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -20,8 +20,6 @@ __attribute__((weak)) void print_coredump() {} namespace esphome::logger { -static const uint32_t CRASH_MAGIC = 0xDEADBEEF; - __attribute__((section(".noinit"))) struct { uint32_t magic; uint32_t reason; @@ -152,7 +150,7 @@ static const char *reason_to_str(unsigned int reason, char *buf) { void Logger::dump_crash_() { ESP_LOGD(TAG, "Crash buffer address %p", &crash_buf); - if (crash_buf.magic == CRASH_MAGIC) { + if (crash_buf.magic == App.get_config_hash()) { char reason_buf[REASON_BUF_SIZE]; ESP_LOGE(TAG, "Last crash:"); ESP_LOGE(TAG, "Reason=%s PC=0x%08x LR=0x%08x", reason_to_str(crash_buf.reason, reason_buf), crash_buf.pc, @@ -164,7 +162,7 @@ void Logger::dump_crash_() { } void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { - crash_buf.magic = CRASH_MAGIC; + crash_buf.magic = App.get_config_hash(); crash_buf.reason = reason; if (esf) { crash_buf.pc = esf->basic.pc; From 4a529003527b3d475742b29ae2fed82e55b0066c Mon Sep 17 00:00:00 2001 From: James Myatt Date: Mon, 23 Feb 2026 21:16:32 +0000 Subject: [PATCH 0164/2056] [nfc] Fix logging tag for nfc helpers (#14235) --- esphome/components/nfc/nfc_helpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp index bfaed6e486f..fb0954a8333 100644 --- a/esphome/components/nfc/nfc_helpers.cpp +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -39,7 +39,7 @@ std::string get_random_ha_tag_ndef() { for (int i = 0; i < 12; i++) { uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; } - ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + ESP_LOGD(TAG, "Payload to be written: %s", uri.c_str()); return uri; } From 869678953dfbe22f2b784e8ec927bfa394676671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 17:03:24 -0600 Subject: [PATCH 0165/2056] [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114) Co-authored-by: Claude Opus 4.6 --- esphome/components/sensor/filter.cpp | 5 +++-- esphome/core/helpers.cpp | 11 +++++++++-- esphome/core/helpers.h | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index cd4db984576..0fe1effe179 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -5,6 +5,7 @@ #include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "sensor.h" @@ -240,7 +241,7 @@ ValueListFilter::ValueListFilter(std::initializer_list> bool ValueListFilter::value_matches_any_(float sensor_value) { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); + float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); for (auto &filter_value : this->values_) { @@ -472,7 +473,7 @@ optional ClampFilter::new_value(float value) { RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {} optional RoundFilter::new_value(float value) { if (std::isfinite(value)) { - float accuracy_mult = powf(10.0f, this->precision_); + float accuracy_mult = pow10_int(this->precision_); return roundf(accuracy_mult * value) / accuracy_mult; } return value; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9f850b5df8f..6d801e7ebc4 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -468,8 +468,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { - auto multiplier = powf(10.0f, accuracy_decimals); - value = roundf(value * multiplier) / multiplier; + float divisor; + if (accuracy_decimals == -1) { + divisor = 10.0f; + } else if (accuracy_decimals == -2) { + divisor = 100.0f; + } else { + divisor = pow10_int(-accuracy_decimals); + } + value = roundf(value / divisor) * divisor; accuracy_decimals = 0; } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 298b93fbc46..b606e68df31 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -439,6 +439,21 @@ template class SmallBufferWithHeapFallb /// @name Mathematics ///@{ +/// Compute 10^exp using iterative multiplication/division. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +inline float pow10_int(int8_t exp) { + float result = 1.0f; + if (exp >= 0) { + for (int8_t i = 0; i < exp; i++) + result *= 10.0f; + } else { + for (int8_t i = exp; i < 0; i++) + result /= 10.0f; + } + return result; +} + /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; From ebf1047da79805e86ec7a7e1058a2c50c55e42b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:51:56 -0500 Subject: [PATCH 0166/2056] [core] Move build_info_data.h out of application.h to fix incremental rebuilds (#14230) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/version/version_text_sensor.cpp | 1 + esphome/components/web_server/web_server.cpp | 2 +- esphome/core/application.cpp | 11 +++++++++++ esphome/core/application.h | 18 ++++++++---------- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 2e5686008b9..74bb4c76e8a 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,5 +1,6 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" +#include "esphome/core/build_info_data.h" #include "esphome/core/log.h" #include "esphome/core/version.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4b572417c1d..682008c40ea 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -375,7 +375,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { JsonObject root = builder.root(); root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); - char comment_buffer[ESPHOME_COMMENT_SIZE]; + char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX]; App.get_comment_string(comment_buffer); root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c6597897dce..1cb7dc00755 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -749,4 +749,15 @@ void Application::get_build_time_string(std::span buf buffer[buffer.size() - 1] = '\0'; } +void Application::get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, ESPHOME_COMMENT_SIZE); + buffer[ESPHOME_COMMENT_SIZE - 1] = '\0'; +} + +uint32_t Application::get_config_hash() { return ESPHOME_CONFIG_HASH; } + +uint32_t Application::get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + +time_t Application::get_build_time() { return ESPHOME_BUILD_TIME; } + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 5b3e3dfed6d..cd275bb97fc 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -6,7 +6,6 @@ #include #include #include -#include "esphome/core/build_info_data.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -274,16 +273,15 @@ class Application { return ""; } + /// Maximum size of the comment buffer (including null terminator) + static constexpr size_t ESPHOME_COMMENT_SIZE_MAX = 256; + /// Copy the comment string into the provided buffer - /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) - void get_comment_string(std::span buffer) { - ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); - buffer[buffer.size() - 1] = '\0'; - } + void get_comment_string(std::span buffer); /// Get the comment of this Application as a string std::string get_comment() { - char buffer[ESPHOME_COMMENT_SIZE]; + char buffer[ESPHOME_COMMENT_SIZE_MAX]; this->get_comment_string(buffer); return std::string(buffer); } @@ -294,13 +292,13 @@ class Application { static constexpr size_t BUILD_TIME_STR_SIZE = 26; /// Get the config hash as a 32-bit integer - constexpr uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; } + uint32_t get_config_hash(); /// Get the config hash extended with ESPHome version - constexpr uint32_t get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + uint32_t get_config_version_hash(); /// Get the build time as a Unix timestamp - constexpr time_t get_build_time() { return ESPHOME_BUILD_TIME; } + time_t get_build_time(); /// Copy the build time string into the provided buffer /// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced) From 30cc51eac97948449aa87a4e741bab84bb79d6a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:00 -0600 Subject: [PATCH 0167/2056] [version] Use C++17 nested namespace syntax (#14240) --- esphome/components/version/version_text_sensor.cpp | 6 ++---- esphome/components/version/version_text_sensor.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 74bb4c76e8a..4a08001cc4a 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace version { +namespace esphome::version { static const char *const TAG = "version.text_sensor"; @@ -36,5 +35,4 @@ void VersionTextSensor::setup() { void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } -} // namespace version -} // namespace esphome +} // namespace esphome::version diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index b7d80011202..6153c5dd7c5 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace version { +namespace esphome::version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: @@ -16,5 +15,4 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { bool hide_timestamp_{false}; }; -} // namespace version -} // namespace esphome +} // namespace esphome::version From 843d06df3f5bf63e9447d1a10eaaa6987f0b5705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:15 -0600 Subject: [PATCH 0168/2056] [switch] Use C++17 nested namespace syntax (#14241) --- esphome/components/switch/automation.cpp | 6 ++---- esphome/components/switch/automation.h | 6 ++---- .../switch/binary_sensor/switch_binary_sensor.cpp | 6 ++---- .../switch/binary_sensor/switch_binary_sensor.h | 6 ++---- esphome/components/switch/switch.cpp | 6 ++---- esphome/components/switch/switch.h | 14 ++++++-------- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/esphome/components/switch/automation.cpp b/esphome/components/switch/automation.cpp index 5989ae9ce3e..9a0221fe56b 100644 --- a/esphome/components/switch/automation.cpp +++ b/esphome/components/switch/automation.cpp @@ -1,10 +1,8 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch.automation"; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 27d3474c971..ed1f056c8b6 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { template class TurnOnAction : public Action { public: @@ -104,5 +103,4 @@ template class SwitchPublishAction : public Action { Switch *switch_; }; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp index ba571544460..19995fb1ae3 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "switch_binary_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch.binary_sensor"; @@ -13,5 +12,4 @@ void SwitchBinarySensor::setup() { void SwitchBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Switch Binary Sensor", this); } -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 53b07da9037..0b77cdd9202 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component { public: @@ -17,5 +16,4 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component Switch *source_; }; -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 61a273d25cd..9e9af213680 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { static const char *const TAG = "switch"; @@ -107,5 +106,4 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o } } -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 9319adf9ed7..982c640cf94 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace switch_ { +namespace esphome::switch_ { #define SUB_SWITCH(name) \ protected: \ @@ -16,10 +15,10 @@ namespace switch_ { void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; } // bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled -const int RESTORE_MODE_ON_MASK = 0x01; -const int RESTORE_MODE_PERSISTENT_MASK = 0x02; -const int RESTORE_MODE_INVERTED_MASK = 0x04; -const int RESTORE_MODE_DISABLED_MASK = 0x08; +constexpr int RESTORE_MODE_ON_MASK = 0x01; +constexpr int RESTORE_MODE_PERSISTENT_MASK = 0x02; +constexpr int RESTORE_MODE_INVERTED_MASK = 0x04; +constexpr int RESTORE_MODE_DISABLED_MASK = 0x08; enum SwitchRestoreMode : uint8_t { SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, @@ -146,5 +145,4 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj); -} // namespace switch_ -} // namespace esphome +} // namespace esphome::switch_ From 63c1496115befafbdf8565f48b34ab7a6ea57753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:25 -0600 Subject: [PATCH 0169/2056] [text] Use C++17 nested namespace syntax (#14242) --- esphome/components/text/automation.h | 6 ++---- esphome/components/text/text.cpp | 6 ++---- esphome/components/text/text.h | 6 ++---- esphome/components/text/text_call.cpp | 6 ++---- esphome/components/text/text_call.h | 6 ++---- esphome/components/text/text_traits.h | 6 ++---- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h index e7667fe491f..ac8166d0bec 100644 --- a/esphome/components/text/automation.h +++ b/esphome/components/text/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "text.h" -namespace esphome { -namespace text { +namespace esphome::text { class TextStateTrigger : public Trigger { public: @@ -29,5 +28,4 @@ template class TextSetAction : public Action { Text *text_; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index e3f74b685b9..d8ab6b1b92c 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace text { +namespace esphome::text { static const char *const TAG = "text"; @@ -34,5 +33,4 @@ void Text::add_on_state_callback(std::function &&call this->state_callback_.add(std::move(callback)); } -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 3a1bea56cb7..7d255e56880 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -6,8 +6,7 @@ #include "text_call.h" #include "text_traits.h" -namespace esphome { -namespace text { +namespace esphome::text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -47,5 +46,4 @@ class Text : public EntityBase { LazyCallbackManager state_callback_; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index 0d0a1d228d5..8a1630c5ca0 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "text.h" -namespace esphome { -namespace text { +namespace esphome::text { static const char *const TAG = "text"; @@ -52,5 +51,4 @@ void TextCall::perform() { this->parent_->control(target_value); } -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_call.h b/esphome/components/text/text_call.h index 9f75a25c6be..532fae34b2b 100644 --- a/esphome/components/text/text_call.h +++ b/esphome/components/text/text_call.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "text_traits.h" -namespace esphome { -namespace text { +namespace esphome::text { class Text; @@ -21,5 +20,4 @@ class TextCall { void validate_(); }; -} // namespace text -} // namespace esphome +} // namespace esphome::text diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h index 473daafb8eb..72e65b83ced 100644 --- a/esphome/components/text/text_traits.h +++ b/esphome/components/text/text_traits.h @@ -4,8 +4,7 @@ #include "esphome/core/string_ref.h" -namespace esphome { -namespace text { +namespace esphome::text { enum TextMode : uint8_t { TEXT_MODE_TEXT = 0, @@ -37,5 +36,4 @@ class TextTraits { TextMode mode_{TEXT_MODE_TEXT}; }; -} // namespace text -} // namespace esphome +} // namespace esphome::text From 500aa7bf1d27378ea94141babdad6c83b0176f4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:35 -0600 Subject: [PATCH 0170/2056] [text_sensor] Use C++17 nested namespace syntax (#14243) --- esphome/components/text_sensor/automation.h | 6 ++---- esphome/components/text_sensor/filter.cpp | 6 ++---- esphome/components/text_sensor/filter.h | 6 ++---- esphome/components/text_sensor/text_sensor.cpp | 6 ++---- esphome/components/text_sensor/text_sensor.h | 6 ++---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 709c54c1409..ab303627742 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -6,8 +6,7 @@ #include "esphome/core/automation.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensorStateTrigger : public Trigger { public: @@ -46,5 +45,4 @@ template class TextSensorPublishAction : public Action { TextSensor *sensor_; }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index f6552c7c668..f7c6a695fba 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { static const char *const TAG = "text_sensor.filter"; @@ -107,7 +106,6 @@ bool MapFilter::new_value(std::string &value) { return true; // Pass through if no match } -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor #endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index f88e8645ccc..8a8bc55c8e1 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensor; @@ -165,7 +164,6 @@ class MapFilter : public Filter { FixedVector mappings_; }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor #endif // USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c66d08ec40a..91561c5f420 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { static const char *const TAG = "text_sensor"; @@ -125,5 +124,4 @@ void TextSensor::notify_frontend_() { #endif } -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 97373dc7169..9916aa63b23 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace text_sensor { +namespace esphome::text_sensor { class TextSensor; @@ -84,5 +83,4 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { #endif }; -} // namespace text_sensor -} // namespace esphome +} // namespace esphome::text_sensor From a694003fe3f2aca98e930f9d1836d43ac2ee05d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:49:48 -0600 Subject: [PATCH 0171/2056] [usb_host] Use C++17 nested namespace syntax (#14244) --- esphome/components/usb_host/usb_host.h | 26 +++++++++---------- .../components/usb_host/usb_host_client.cpp | 6 ++--- .../usb_host/usb_host_component.cpp | 6 ++--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index a6a97d0bd71..2eec0c9699b 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -12,8 +12,7 @@ #include "esphome/core/event_pool.h" #include -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { // THREADING MODEL: // This component uses a dedicated USB task for event processing to prevent data loss. @@ -44,16 +43,16 @@ struct TransferRequest; class USBClient; // constants for setup packet type -static const uint8_t USB_RECIP_DEVICE = 0; -static const uint8_t USB_RECIP_INTERFACE = 1; -static const uint8_t USB_RECIP_ENDPOINT = 2; -static const uint8_t USB_TYPE_STANDARD = 0 << 5; -static const uint8_t USB_TYPE_CLASS = 1 << 5; -static const uint8_t USB_TYPE_VENDOR = 2 << 5; -static const uint8_t USB_DIR_MASK = 1 << 7; -static const uint8_t USB_DIR_IN = 1 << 7; -static const uint8_t USB_DIR_OUT = 0; -static const size_t SETUP_PACKET_SIZE = 8; +static constexpr uint8_t USB_RECIP_DEVICE = 0; +static constexpr uint8_t USB_RECIP_INTERFACE = 1; +static constexpr uint8_t USB_RECIP_ENDPOINT = 2; +static constexpr uint8_t USB_TYPE_STANDARD = 0 << 5; +static constexpr uint8_t USB_TYPE_CLASS = 1 << 5; +static constexpr uint8_t USB_TYPE_VENDOR = 2 << 5; +static constexpr uint8_t USB_DIR_MASK = 1 << 7; +static constexpr uint8_t USB_DIR_IN = 1 << 7; +static constexpr uint8_t USB_DIR_OUT = 0; +static constexpr size_t SETUP_PACKET_SIZE = 8; static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); @@ -189,7 +188,6 @@ class USBHost : public Component { std::vector clients_{}; }; -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 422d74095c6..5b0aed4c598 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -10,8 +10,7 @@ #include #include #include -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { #pragma GCC diagnostic ignored "-Wparentheses" @@ -568,6 +567,5 @@ void USBClient::release_trq(TransferRequest *trq) { this->trq_in_use_.fetch_and(mask, std::memory_order_release); } -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index 790fe6713b2..8ce0a70dc93 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -4,8 +4,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace usb_host { +namespace esphome::usb_host { void USBHost::setup() { usb_host_config_t config{}; @@ -28,7 +27,6 @@ void USBHost::loop() { } } -} // namespace usb_host -} // namespace esphome +} // namespace esphome::usb_host #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 From 1614eb9c9cc3be784d22f262171b7978b21ebbb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:00 -0600 Subject: [PATCH 0172/2056] [i2c] Use C++17 nested namespace syntax (#14245) --- esphome/components/i2c/i2c.cpp | 6 ++---- esphome/components/i2c/i2c.h | 6 ++---- esphome/components/i2c/i2c_bus.h | 6 ++---- esphome/components/i2c/i2c_bus_arduino.cpp | 6 ++---- esphome/components/i2c/i2c_bus_arduino.h | 6 ++---- esphome/components/i2c/i2c_bus_esp_idf.cpp | 6 ++---- esphome/components/i2c/i2c_bus_esp_idf.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index b9b5d794289..76b3ec3b4d9 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c"; @@ -109,5 +108,4 @@ uint8_t I2CRegister16::get() const { return value; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index aab98d5f468..00929db6207 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -6,8 +6,7 @@ #include "esphome/core/optional.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); @@ -272,5 +271,4 @@ class I2CDevice { I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 2bc0dc1ef91..0c5e80bfe5f 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { @@ -69,5 +68,4 @@ class InternalI2CBus : public I2CBus { virtual int get_port() const = 0; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index edd6b81588f..5120eb4c007 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.arduino"; @@ -262,7 +261,6 @@ void ArduinoI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 2d69e7684ca..edc14af7bce 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -45,7 +44,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { bool initialized_ = false; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 7a965ce5ad0..eaefabf75be 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -10,8 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.idf"; @@ -312,6 +311,5 @@ void IDFI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 84f46169678..c23f9f0c54f 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -6,8 +6,7 @@ #include "i2c_bus.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -56,7 +55,6 @@ class IDFI2CBus : public InternalI2CBus, public Component { #endif }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 From 70e47f301d0dd936ce0d30241aa1427fbab02812 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:11 -0600 Subject: [PATCH 0173/2056] [ethernet] Use C++17 nested namespace syntax (#14246) --- esphome/components/ethernet/ethernet_component.cpp | 6 ++---- esphome/components/ethernet/ethernet_component.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fcd32223e44..f855bc89cc7 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -19,8 +19,7 @@ #include #endif -namespace esphome { -namespace ethernet { +namespace esphome::ethernet { #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) // work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 @@ -881,7 +880,6 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi #endif -} // namespace ethernet -} // namespace esphome +} // namespace esphome::ethernet #endif // USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b4859c308d1..1cd44d2b2cf 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,8 +15,7 @@ #include "esp_mac.h" #include "esp_idf_version.h" -namespace esphome { -namespace ethernet { +namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS /** Listener interface for Ethernet IP state changes. @@ -218,7 +217,6 @@ extern EthernetComponent *global_eth_component; extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif -} // namespace ethernet -} // namespace esphome +} // namespace esphome::ethernet #endif // USE_ESP32 From 7d9d90d3f8c4b431b74ad197f9f354f9a4795f59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 19:50:22 -0600 Subject: [PATCH 0174/2056] [cse7766] Use C++17 nested namespace syntax (#14247) --- esphome/components/cse7766/cse7766.cpp | 6 ++---- esphome/components/cse7766/cse7766.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 7ffdf757a0f..806b79e19e0 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { static const char *const TAG = "cse7766"; @@ -258,5 +257,4 @@ void CSE7766Component::dump_config() { this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN); } -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 66a4e04633f..77b80dd8244 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; @@ -49,5 +48,4 @@ class CSE7766Component : public Component, public uart::UARTDevice { uint16_t cf_pulses_last_{0}; }; -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 From ad2da0af52469c18b8741a60e6b4aa977415bf8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 20:00:21 -0600 Subject: [PATCH 0175/2056] [network] Use C++17 nested namespace syntax (#14248) --- esphome/components/network/ip_address.h | 6 ++---- esphome/components/network/util.cpp | 6 ++---- esphome/components/network/util.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index b2a2c563e28..c0e7b2886c5 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -37,8 +37,7 @@ using ip4_addr_t = in_addr; #include #endif -namespace esphome { -namespace network { +namespace esphome::network { /// Buffer size for IP address string (IPv6 max: 39 chars + null) static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40; @@ -187,6 +186,5 @@ struct IPAddress { using IPAddresses = std::array; -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 5e741fd2445..e397d770775 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -17,8 +17,7 @@ #include "esphome/components/modem/modem_component.h" #endif -namespace esphome { -namespace network { +namespace esphome::network { // The order of the components is important: WiFi should come after any possible main interfaces (it may be used as // an AP that use a previous interface for NAT). @@ -109,6 +108,5 @@ const char *get_use_address() { #endif } -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 3dc12232aa8..ae949ab0a85 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -4,8 +4,7 @@ #include #include "ip_address.h" -namespace esphome { -namespace network { +namespace esphome::network { /// Return whether the node is connected to the network (through wifi, eth, ...) bool is_connected(); @@ -15,6 +14,5 @@ bool is_disabled(); const char *get_use_address(); IPAddresses get_ip_addresses(); -} // namespace network -} // namespace esphome +} // namespace esphome::network #endif From abf7074518379503dec1a19dd46b8019c1d3a789 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:27:48 -0500 Subject: [PATCH 0176/2056] [esp32] Improve ESP32-P4 engineering sample warning message (#14252) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 62367443dae..a34747a183a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -790,8 +790,15 @@ def _detect_variant(value): engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) if engineering_sample is None: _LOGGER.warning( - "No board specified for ESP32-P4. Defaulting to production silicon (rev3). " - "If you have an early engineering sample (pre-rev3), set 'engineering_sample: true'." + "No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n" + "If you have an early engineering sample (pre-rev3), add this to your config:\n" + "\n" + " esp32:\n" + " engineering_sample: true\n" + "\n" + "To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n" + "Engineering samples will show a revision below v3.0.\n" + "The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)." ) elif engineering_sample: value[CONF_BOARD] = "esp32-p4-evboard" From 72263eda855c7ffee631a5281fe4c5c9549f4deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Tue, 24 Feb 2026 14:31:58 +0100 Subject: [PATCH 0177/2056] [version] text sensor add option `hide_hash` to restore the pre-2026.1 behavior (#14251) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/version/text_sensor.py | 11 +++++- .../version/version_text_sensor.cpp | 34 ++++++++++++++----- .../components/version/version_text_sensor.h | 2 ++ esphome/const.py | 1 + tests/components/version/common.yaml | 15 +++++++- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index ba8c493d4ba..a55e18a5a75 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -from esphome.const import CONF_HIDE_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, ICON_NEW_BOX +from esphome.const import ( + CONF_HIDE_HASH, + CONF_HIDE_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_NEW_BOX, +) version_ns = cg.esphome_ns.namespace("version") VersionTextSensor = version_ns.class_( @@ -16,6 +21,9 @@ CONFIG_SCHEMA = ( .extend( { cv.GenerateID(): cv.declare_id(VersionTextSensor), + # Hide the config hash suffix and restore the pre-2026.1 + # version text format when set to true. + cv.Optional(CONF_HIDE_HASH, default=False): cv.boolean, cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, } ) @@ -26,4 +34,5 @@ CONFIG_SCHEMA = ( async def to_code(config): var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) + cg.add(var.set_hide_hash(config[CONF_HIDE_HASH])) cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 4a08001cc4a..8aec98d2da3 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,37 +1,53 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" #include "esphome/core/build_info_data.h" -#include "esphome/core/log.h" -#include "esphome/core/version.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/progmem.h" +#include "esphome/core/version.h" namespace esphome::version { static const char *const TAG = "version.text_sensor"; void VersionTextSensor::setup() { - static const char PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x"; + static const char HASH_PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x"; + static const char VERSION_PREFIX[] PROGMEM = ESPHOME_VERSION; static const char BUILT_STR[] PROGMEM = ", built "; - // Buffer size: PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null - constexpr size_t buf_size = sizeof(PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2; + + // Buffer size: HASH_PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null + constexpr size_t buf_size = + sizeof(HASH_PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2; char version_str[buf_size]; - ESPHOME_strncpy_P(version_str, PREFIX, sizeof(version_str)); + // hide_hash restores the pre-2026.1 base format by omitting + // the " (config hash 0x...)" suffix entirely. + if (this->hide_hash_) { + ESPHOME_strncpy_P(version_str, VERSION_PREFIX, sizeof(version_str)); + } else { + ESPHOME_strncpy_P(version_str, HASH_PREFIX, sizeof(version_str)); - size_t len = strlen(version_str); - snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash()); + size_t len = strlen(version_str); + snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash()); + } + // Keep hide_timestamp behavior independent from hide_hash so all + // combinations remain available to users. if (!this->hide_timestamp_) { size_t len = strlen(version_str); ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1); ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1); } - strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1); + // The closing parenthesis is part of the config-hash suffix and must + // only be appended when that suffix is present. + if (!this->hide_hash_) { + strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1); + } version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } +void VersionTextSensor::set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 6153c5dd7c5..fec898ae038 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -7,11 +7,13 @@ namespace esphome::version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_hash(bool hide_hash); void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; protected: + bool hide_hash_{false}; bool hide_timestamp_{false}; }; diff --git a/esphome/const.py b/esphome/const.py index 0b1037d0918..1611aeb1019 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -463,6 +463,7 @@ CONF_HEAT_OVERRUN = "heat_overrun" CONF_HEATER = "heater" CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" +CONF_HIDE_HASH = "hide_hash" CONF_HIDE_TIMESTAMP = "hide_timestamp" CONF_HIGH = "high" CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" diff --git a/tests/components/version/common.yaml b/tests/components/version/common.yaml index 7713afc37c2..f9ff778e14b 100644 --- a/tests/components/version/common.yaml +++ b/tests/components/version/common.yaml @@ -1,3 +1,16 @@ text_sensor: - platform: version - name: "ESPHome Version" + name: "ESPHome Version Full" + + - platform: version + name: "ESPHome Version No Timestamp" + hide_timestamp: true + + - platform: version + name: "ESPHome Version No Hash" + hide_hash: true + + - platform: version + name: "ESPHome Version Shortest" + hide_timestamp: true + hide_hash: true From 4abbed0cd4ee94845cee7034a1d06d9a279167a3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:33:33 +1100 Subject: [PATCH 0178/2056] [mipi_dsi] Allow transform disable; fix warnings (#14216) --- esphome/components/mipi_dsi/display.py | 24 +++++++++++-------- esphome/components/mipi_dsi/mipi_dsi.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.h | 10 ++++---- .../mipi_dsi/fixtures/mipi_dsi.yaml | 17 +++++++++++++ .../mipi_dsi/test_mipi_dsi_config.py | 3 ++- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index de3791b3a47..85bfad7f1a8 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -39,6 +39,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_COLOR_ORDER, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, @@ -88,14 +89,17 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] model.defaults[CONF_SWAP_XY] = cv.UNDEFINED - transform = cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by DSI displays" - ), - } + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), + } + ), + cv.one_of(CONF_DISABLED, lower=True), ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( @@ -199,9 +203,9 @@ async def to_code(config): cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) - cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6))) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6)) cg.add(var.set_lanes(int(config[CONF_LANES]))) - cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6))) + cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6)) if reset_pin := config.get(CONF_RESET_PIN): reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 18cafab6844..4d45cfb7990 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -374,7 +374,7 @@ void MIPI_DSI::dump_config() { "\n Swap X/Y: %s" "\n Rotation: %d degrees" "\n DSI Lanes: %u" - "\n Lane Bit Rate: %uMbps" + "\n Lane Bit Rate: %.0fMbps" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -385,7 +385,7 @@ void MIPI_DSI::dump_config() { "\n Display Pixel Mode: %d bit" "\n Color Order: %s" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz", + "\n Pixel Clock: %.1fMHz", this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 1cffe3b1781..6e27912aa51 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -47,7 +47,7 @@ class MIPI_DSI : public display::Display { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } - void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_frequency(float pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } @@ -58,7 +58,7 @@ class MIPI_DSI : public display::Display { void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } void set_model(const char *model) { this->model_ = model; } - void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } + void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } @@ -95,9 +95,9 @@ class MIPI_DSI : public display::Display { uint16_t vsync_front_porch_ = 10; const char *model_{"Unknown"}; std::vector init_sequence_{}; - uint16_t pclk_frequency_ = 16; // in MHz - uint16_t lane_bit_rate_{1500}; // in Mbps - uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes + float pclk_frequency_ = 16; // in MHz + float lane_bit_rate_{1500}; // in Mbps + uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes bool invert_colors_{}; display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 6de2bd5a770..6f76dcb1d1a 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -22,6 +22,23 @@ display: id: p4_86 model: "WAVESHARE-P4-86-PANEL" rotation: 180 + - platform: mipi_dsi + model: custom + id: custom_id + dimensions: + width: 400 + height: 1280 + hsync_back_porch: 40 + hsync_pulse_width: 30 + hsync_front_porch: 40 + vsync_back_porch: 20 + vsync_pulse_width: 10 + vsync_front_porch: 20 + pclk_frequency: 48Mhz + lane_bit_rate: 1.2Gbps + rotation: 180 + transform: disabled + init_sequence: i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index d465a8c81bf..92f56b5451a 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -123,7 +123,8 @@ def test_code_generation( in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp + assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 6554ad7c7ef933a094b31583bb2dd285831bc8b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 12:08:51 -0600 Subject: [PATCH 0179/2056] [core] Prevent inlining of mark_matching_items_removed_locked_ on Thumb-1 (#14256) --- esphome/core/scheduler.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 16b0ded3126..ed457b87f6d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -496,9 +496,9 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - size_t mark_matching_items_removed_locked_(std::vector> &container, - Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { + __attribute__((noinline)) size_t mark_matching_items_removed_locked_( + std::vector> &container, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) From fe3c2ba5553f4d07328d1c5b1e927ca0cbea25e1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:15:22 -0500 Subject: [PATCH 0180/2056] [http_request.ota] Percent-encode credentials in URL (#14257) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http_request/ota/ota_http_request.cpp | 22 +++++++++++++++++++ .../http_request/ota/ota_http_request.h | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index d77a7682118..0db3a50b47d 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,5 +1,7 @@ #include "ota_http_request.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -209,6 +211,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() { return ota::OTA_RESPONSE_OK; } +// URL-encode characters that are not unreserved per RFC 3986 section 2.3. +// This is needed for embedding userinfo (username/password) in URLs safely. +static std::string url_encode(const std::string &str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + result += '%'; + result += format_hex_pretty_char((static_cast(c) >> 4) & 0x0F); + result += format_hex_pretty_char(static_cast(c) & 0x0F); + } + } + return result; +} + +void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); } +void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); } + std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { if (this->username_.empty() || this->password_.empty()) { return url; diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 8735189e993..e3f1a4aa90e 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -29,9 +29,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parentedmd5_expected_ = md5; } - void set_password(const std::string &password) { this->password_ = password; } + void set_password(const std::string &password); void set_url(const std::string &url); - void set_username(const std::string &username) { this->username_ = username; } + void set_username(const std::string &username); std::string md5_computed() { return this->md5_computed_; } std::string md5_expected() { return this->md5_expected_; } From af00d601be022c0ba3b5d9656c9906c95b4edb64 Mon Sep 17 00:00:00 2001 From: Andrew Rankin Date: Tue, 24 Feb 2026 16:19:13 -0500 Subject: [PATCH 0181/2056] [esp32_ble_server] add max_clients option for multi-client support (#14239) --- .../components/esp32_ble_server/__init__.py | 20 +++++++++++++++++++ .../esp32_ble_server/ble_server.cpp | 11 +++++++++- .../components/esp32_ble_server/ble_server.h | 4 ++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index cb494ed1bc1..b08e791e7e3 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE +import esphome.final_validate as fv from esphome.schema_extractors import SCHEMA_EXTRACT AUTO_LOAD = ["esp32_ble", "bytebuffer"] @@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version" CONF_INDICATE = "indicate" CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_MAX_CLIENTS = "max_clients" CONF_ON_WRITE = "on_write" CONF_READ = "read" CONF_STRING = "string" @@ -287,6 +289,22 @@ def create_device_information_service(config): def final_validate_config(config): + # Validate max_clients does not exceed esp32_ble max_connections + max_clients = config[CONF_MAX_CLIENTS] + if max_clients > 1: + full_config = fv.full_config.get() + ble_config = full_config.get("esp32_ble", {}) + max_connections = ble_config.get( + "max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS + ) + if max_clients > max_connections: + raise cv.Invalid( + f"'max_clients' ({max_clients}) cannot exceed esp32_ble " + f"'max_connections' ({max_connections}). " + f"Please set 'max_connections: {max_clients}' in the " + f"'esp32_ble' component." + ) + # Check if all characteristics that require notifications have the notify property set for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): # Look for the characteristic in the configuration @@ -428,6 +446,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MODEL): value_schema("string", templatable=False), cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), + cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9), cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), @@ -552,6 +571,7 @@ async def to_code(config): esp32_ble.register_ble_status_event_handler(parent, var) cg.add(var.set_parent(parent)) cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) + cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS])) if CONF_MANUFACTURER_DATA in config: cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) for service_config in config[CONF_SERVICES]: diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 2c13a8ac365..f292cf87220 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -175,6 +175,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); + // Resume advertising so additional clients can discover and connect + if (this->client_count_ < this->max_clients_) { + this->parent_->advertising_start(); + } this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } @@ -241,7 +245,12 @@ void BLEServer::ble_before_disabled_event_handler() { float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } -void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } +void BLEServer::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 BLE Server:\n" + " Max clients: %u", + this->max_clients_); +} BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 6fa86dd67fe..ff7e0044e4c 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -39,6 +39,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv this->restart_advertising_(); } + void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } + uint8_t get_max_clients() const { return this->max_clients_; } + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15); void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0); BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0); @@ -95,6 +98,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint8_t client_count_{0}; + uint8_t max_clients_{1}; std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; From cca4777f6458c1bf41e2cdd79f91aafe699e9c08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 17:51:01 -0600 Subject: [PATCH 0182/2056] [web_server_idf] Pass std::function by rvalue reference (#14262) --- esphome/components/web_server_idf/multipart.h | 4 ++-- esphome/components/web_server_idf/web_server_idf.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index cb1e0ecd1db..8b94b0491ed 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -33,8 +33,8 @@ class MultipartReader { ~MultipartReader(); // Set callbacks for handling data - void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); } - void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); } + void set_data_callback(DataCallback &&callback) { data_callback_ = std::move(callback); } + void set_part_complete_callback(PartCompleteCallback &&callback) { part_complete_callback_ = std::move(callback); } // Parse incoming data size_t parse(const char *data, size_t len); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 76ddfa35fda..9d81d89ec76 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -204,7 +204,7 @@ class AsyncWebServer { ~AsyncWebServer() { this->end(); } // NOLINTNEXTLINE(readability-identifier-naming) - void onNotFound(std::function fn) { on_not_found_ = std::move(fn); } + void onNotFound(std::function &&fn) { on_not_found_ = std::move(fn); } void begin(); void end(); @@ -327,7 +327,7 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) void handleRequest(AsyncWebServerRequest *request) override; // NOLINTNEXTLINE(readability-identifier-naming) - void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } + void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); } void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); From 4dc6b12ec51d9e8610ca5be0fc77ebf9c3f39da3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 17:56:43 -0600 Subject: [PATCH 0183/2056] [api] Pass std::function by rvalue reference in state subscriptions (#14261) --- esphome/components/api/api_server.cpp | 20 ++++++++++---------- esphome/components/api/api_server.h | 20 ++++++++++---------- esphome/components/api/user_services.h | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 5b096788f52..0352d7347bb 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef #ifdef USE_API_HOMEASSISTANT_STATES // Helper to add subscription (reduces duplication) -void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once) { +void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, + std::function &&f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) @@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri // Helper to add subscription with heap-allocated strings (reduces duplication) void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { HomeAssistantStateSubscription sub; // Allocate heap storage for the strings sub.entity_id_dynamic_storage = std::make_unique(std::move(entity_id)); @@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), false); } void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), true); } // std::string overload with StringRef callback (zero-allocation callback) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } // Legacy helper: wraps std::string callback and delegates to StringRef version void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { // Wrap callback to convert StringRef -> std::string, then delegate this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::function([f = std::move(f)](StringRef state) { f(state.str()); }), @@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index fed29016b37..3abf68358c0 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -201,20 +201,20 @@ class APIServer : public Component, }; // New const char* overload (for internal components - zero allocation) - void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f); - void get_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); + void get_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); // std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); // Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); const std::vector &get_state_subs() const; #endif @@ -241,13 +241,13 @@ class APIServer : public Component, #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication - void add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once); - void add_state_subscription_(std::string entity_id, optional attribute, std::function f, + void add_state_subscription_(const char *entity_id, const char *attribute, std::function &&f, bool once); + void add_state_subscription_(std::string entity_id, optional attribute, + std::function &&f, bool once); // Legacy helper: wraps std::string callback and delegates to StringRef version void add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once); + std::function &&f, bool once); #endif // USE_API_HOMEASSISTANT_STATES // No explicit close() needed — listen sockets have no active connections on // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 0fc529108c7..d1b8a6ef0d0 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -230,7 +230,7 @@ template class APIRespondAction : public Action { void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - void set_data(std::function func) { + void set_data(std::function &&func) { this->json_builder_ = std::move(func); this->has_data_ = true; } From 08dc487b5b668f1dbf4ec6573aceca2de0d1fdd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:08:07 -0600 Subject: [PATCH 0184/2056] [core] Pass std::function by rvalue reference in scheduler (#14260) --- esphome/core/scheduler.cpp | 15 ++++++++------- esphome/core/scheduler.h | 18 +++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e4e0751e100..674d70abcf9 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -135,7 +135,7 @@ bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_t // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, uint32_t hash_or_id, uint32_t delay, - std::function func, bool is_retry, bool skip_cancel) { + std::function &&func, bool is_retry, bool skip_cancel) { if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if we have a name/id if (!skip_cancel) { @@ -216,17 +216,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type target->push_back(std::move(item)); } -void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { +void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, + std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::STATIC_STRING, name, 0, timeout, std::move(func)); } void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), timeout, std::move(func)); } -void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func) { +void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout, std::move(func)); } @@ -240,17 +241,17 @@ bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) { return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), interval, std::move(func)); } void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, - std::function func) { + std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval, std::move(func)); } -void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function func) { +void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval, std::move(func)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ed457b87f6d..b6a8336606e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -33,7 +33,7 @@ class Scheduler { // std::string overload - deprecated, use const char* or uint32_t instead // Remove before 2026.7.0 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); + void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func); /** Set a timeout with a const char* name. * @@ -43,11 +43,11 @@ class Scheduler { * - A static const char* variable * - A pointer with lifetime >= the scheduled task */ - void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + void set_timeout(Component *component, const char *name, uint32_t timeout, std::function &&func); /// Set a timeout with a numeric ID (zero heap allocation) - void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func); + void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function &&func); /// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID) - void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function func) { + void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function &&func) { this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(id), timeout, std::move(func)); } @@ -62,7 +62,7 @@ class Scheduler { } ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); /** Set an interval with a const char* name. * @@ -72,11 +72,11 @@ class Scheduler { * - A static const char* variable * - A pointer with lifetime >= the scheduled task */ - void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + void set_interval(Component *component, const char *name, uint32_t interval, std::function &&func); /// Set an interval with a numeric ID (zero heap allocation) - void set_interval(Component *component, uint32_t id, uint32_t interval, std::function func); + void set_interval(Component *component, uint32_t id, uint32_t interval, std::function &&func); /// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID) - void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function func) { + void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function &&func) { this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(id), interval, std::move(func)); } @@ -255,7 +255,7 @@ class Scheduler { // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name, - uint32_t hash_or_id, uint32_t delay, std::function func, bool is_retry = false, + uint32_t hash_or_id, uint32_t delay, std::function &&func, bool is_retry = false, bool skip_cancel = false); // Common implementation for retry - Remove before 2026.8.0 From 2ff876c629a3181e1907fc52594d1257f5165139 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:18:44 -0600 Subject: [PATCH 0185/2056] [core] Use custom deleter for SchedulerItem unique_ptr to prevent destructor inlining (#14258) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/core/scheduler.cpp | 22 ++++++++++------- esphome/core/scheduler.h | 48 ++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 674d70abcf9..d9c66b2000b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -33,6 +33,11 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; +// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines +// ~unique_ptr (~30 bytes each) at every destruction site. Defining +// the deleter in the .cpp file ensures a single copy of the destructor + operator delete. +void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; } + #if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER) // Helper struct for formatting scheduler item names consistently in logs // Uses a stack buffer to avoid heap allocation @@ -468,7 +473,7 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; - std::vector> old_items; + std::vector old_items; #ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); @@ -481,7 +486,7 @@ void HOT Scheduler::call(uint32_t now) { // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { - std::unique_ptr item; + SchedulerItemPtr item; { LockGuard guard{this->lock_}; item = this->pop_raw_locked_(); @@ -642,7 +647,7 @@ size_t HOT Scheduler::cleanup_() { } return this->items_.size(); } -std::unique_ptr HOT Scheduler::pop_raw_locked_() { +Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); // Move the item out before popping - this is the item that was at the front of the heap @@ -865,8 +870,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { #endif } -bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, - const std::unique_ptr &b) { +bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) @@ -877,7 +881,7 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, // IMPORTANT: Caller must hold the scheduler lock before calling this function. // This protects scheduler_item_pool_ from concurrent access by other threads // that may be acquiring items from the pool in set_timer_common_(). -void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { +void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) { if (!item) return; @@ -920,8 +924,8 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, // Helper to get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. -std::unique_ptr Scheduler::get_item_from_pool_locked_() { - std::unique_ptr item; +Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() { + SchedulerItemPtr item; if (!this->scheduler_item_pool_.empty()) { item = std::move(this->scheduler_item_pool_.back()); this->scheduler_item_pool_.pop_back(); @@ -929,7 +933,7 @@ std::unique_ptr Scheduler::get_item_from_pool_locked_( ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif } else { - item = make_unique(); + item = SchedulerItemPtr(new SchedulerItem()); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Allocated new item (pool empty)"); #endif diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index b6a8336606e..840ee7159a7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -142,6 +142,19 @@ class Scheduler { }; protected: + struct SchedulerItem; + + // Custom deleter for SchedulerItem unique_ptr that prevents the compiler from + // inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC + // inlines ~unique_ptr (~30 bytes: null check + ~std::function + + // operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline + // it into a single helper. This noinline deleter ensures only one copy exists. + // operator() is defined in scheduler.cpp to prevent inlining. + struct SchedulerItemDeleter { + void operator()(SchedulerItem *ptr) const noexcept; + }; + using SchedulerItemPtr = std::unique_ptr; + struct SchedulerItem { // Ordered by size to minimize padding Component *component; @@ -233,7 +246,7 @@ class Scheduler { name_type_ = type; } - static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); + static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b); // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. // The upper 16 bits of the 64-bit value are always zero, which is fine since @@ -276,10 +289,10 @@ class Scheduler { size_t cleanup_(); // Remove and return the front item from the heap // IMPORTANT: Caller must hold the scheduler lock before calling this function. - std::unique_ptr pop_raw_locked_(); + SchedulerItemPtr pop_raw_locked_(); // Get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. - std::unique_ptr get_item_from_pool_locked_(); + SchedulerItemPtr get_item_from_pool_locked_(); private: // Helper to cancel items - must be called with lock held @@ -303,9 +316,9 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held - inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, - NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, + bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check @@ -340,7 +353,7 @@ class Scheduler { // IMPORTANT: Only call from main loop context! Recycling clears the callback, // so calling from another thread while the callback is executing causes use-after-free. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - void recycle_item_main_loop_(std::unique_ptr item); + void recycle_item_main_loop_(SchedulerItemPtr item); // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); @@ -396,7 +409,7 @@ class Scheduler { // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), // recycle each item after re-acquiring the lock for the next iteration (N+1 total). // The lock is held across: recycle → loop condition → move-out, then released for execution. - std::unique_ptr item; + SchedulerItemPtr item; this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { @@ -496,9 +509,10 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_( - std::vector> &container, Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { + __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector &container, + Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -514,15 +528,15 @@ class Scheduler { } Mutex lock_; - std::vector> items_; - std::vector> to_add_; + std::vector items_; + std::vector to_add_; #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop - std::vector> defer_queue_; // FIFO queue for defer() calls - size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + std::vector defer_queue_; // FIFO queue for defer() calls + size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; // Memory pool for recycling SchedulerItem objects to reduce heap churn. @@ -533,7 +547,7 @@ class Scheduler { // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) - std::vector> scheduler_item_pool_; + std::vector scheduler_item_pool_; #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* From 3460a8c9225f504fdfdd9166d603fe2c3fb41474 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 18:44:50 -0600 Subject: [PATCH 0186/2056] [dlms_meter/kamstrup_kmp] Replace powf with pow10_int (#14125) Co-authored-by: Claude Opus 4.6 --- esphome/components/dlms_meter/dlms_meter.cpp | 4 +--- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 11d05b3a088..bd2150e8ddf 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -1,7 +1,5 @@ #include "dlms_meter.h" -#include - #if defined(USE_ESP8266_FRAMEWORK_ARDUINO) #include #elif defined(USE_ESP32) @@ -410,7 +408,7 @@ void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_lengt if (current_position + 1 < message_length) { int8_t scaler = static_cast(plaintext[current_position + 1]); if (scaler != 0) { - value *= powf(10.0f, scaler); + value *= pow10_int(scaler); } } diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 534939f9af5..00c65a19379 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -222,11 +222,11 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ } // Calculate exponent - float exponent = msg[6] & 0x3F; + int8_t exp_val = msg[6] & 0x3F; if (msg[6] & 0x40) { - exponent = -exponent; + exp_val = -exp_val; } - exponent = powf(10, exponent); + float exponent = pow10_int(exp_val); if (msg[6] & 0x80) { exponent = -exponent; } From 905e81330ee3af27a82f2f6cf3675497bd6fcc70 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:28:19 +1300 Subject: [PATCH 0187/2056] Don't get stuck forever on a failed component can_proceed (#14267) --- esphome/core/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1cb7dc00755..6a7683a987d 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -134,7 +134,7 @@ void Application::setup() { this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); - } while (!component->can_proceed()); + } while (!component->can_proceed() && !component->is_failed()); } ESP_LOGI(TAG, "setup() finished successfully!"); From 1dac501b049ffea5a1767df5dfbd429d768c0af2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 21:39:51 -0600 Subject: [PATCH 0188/2056] [light] Add additional light effect test cases (#14266) --- tests/components/light/common.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index 55525fc67ff..e5fab62a793 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -71,6 +71,32 @@ esphome: - light.control: id: test_monochromatic_light state: on + # Test static effect name resolution at codegen time + - light.turn_on: + id: test_monochromatic_light + effect: Strobe + - light.turn_on: + id: test_monochromatic_light + effect: none + # Test resolving a different effect on the same light + - light.control: + id: test_monochromatic_light + effect: My Flicker + # Test effect: None (capitalized) + - light.control: + id: test_monochromatic_light + effect: None + # Test effect lambda with no args (on_boot has empty Ts...) + - light.turn_on: + id: test_monochromatic_light + effect: !lambda 'return "Strobe";' + # Test effect lambda with non-empty args (repeat passes uint32_t iteration) + - repeat: + count: 3 + then: + - light.turn_on: + id: test_monochromatic_light + effect: !lambda 'return iteration > 1 ? "Strobe" : "none";' - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% From 2e705a919fc8a4e9990e4fc5870e4677a05f6e46 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:26:00 -0500 Subject: [PATCH 0189/2056] [pid] Fix deadband threshold conversion for Fahrenheit (#14268) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/pid/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 5919d2cac87..5fa3166f9df 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema( { - cv.Required(CONF_THRESHOLD_HIGH): cv.temperature, - cv.Required(CONF_THRESHOLD_LOW): cv.temperature, + cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta, + cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta, cv.Optional(CONF_KP_MULTIPLIER, default=0.1): cv.float_, cv.Optional(CONF_KI_MULTIPLIER, default=0.0): cv.float_, cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, From b134c4679ca5f609633a2b97681a41867e62c12d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Feb 2026 22:33:57 -0600 Subject: [PATCH 0190/2056] [light] Replace std::lerp with lightweight lerp_fast in LightColorValues::lerp (#14238) --- .../components/light/light_color_values.cpp | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/light/light_color_values.cpp b/esphome/components/light/light_color_values.cpp index 2f22bb3c688..e54cc0a12a3 100644 --- a/esphome/components/light/light_color_values.cpp +++ b/esphome/components/light/light_color_values.cpp @@ -1,27 +1,30 @@ #include "light_color_values.h" -#include - namespace esphome::light { +// Lightweight lerp: a + t * (b - a). +// Avoids std::lerp's NaN/infinity handling which Clang doesn't optimize out, +// adding ~200 bytes per call. Safe because all values are finite floats. +static float __attribute__((noinline)) lerp_fast(float a, float b, float t) { return a + t * (b - a); } + LightColorValues LightColorValues::lerp(const LightColorValues &start, const LightColorValues &end, float completion) { // Directly interpolate the raw values to avoid getter/setter overhead. // This is safe because: - // - All LightColorValues have their values clamped when set via the setters - // - std::lerp guarantees output is in the same range as inputs + // - All LightColorValues except color_temperature_ have their values clamped when set via the setters + // - lerp_fast output stays in range when inputs are in range and 0 <= completion <= 1 // - Therefore the output doesn't need clamping, so we can skip the setters LightColorValues v; v.color_mode_ = end.color_mode_; - v.state_ = std::lerp(start.state_, end.state_, completion); - v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion); - v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion); - v.red_ = std::lerp(start.red_, end.red_, completion); - v.green_ = std::lerp(start.green_, end.green_, completion); - v.blue_ = std::lerp(start.blue_, end.blue_, completion); - v.white_ = std::lerp(start.white_, end.white_, completion); - v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion); - v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion); - v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion); + v.state_ = lerp_fast(start.state_, end.state_, completion); + v.brightness_ = lerp_fast(start.brightness_, end.brightness_, completion); + v.color_brightness_ = lerp_fast(start.color_brightness_, end.color_brightness_, completion); + v.red_ = lerp_fast(start.red_, end.red_, completion); + v.green_ = lerp_fast(start.green_, end.green_, completion); + v.blue_ = lerp_fast(start.blue_, end.blue_, completion); + v.white_ = lerp_fast(start.white_, end.white_, completion); + v.color_temperature_ = lerp_fast(start.color_temperature_, end.color_temperature_, completion); + v.cold_white_ = lerp_fast(start.cold_white_, end.cold_white_, completion); + v.warm_white_ = lerp_fast(start.warm_white_, end.warm_white_, completion); return v; } From bb05cfb7119102b877ad583d9d9091ff57663a44 Mon Sep 17 00:00:00 2001 From: Big Mike Date: Wed, 25 Feb 2026 06:34:58 -0600 Subject: [PATCH 0191/2056] [sensirion_common] Move sen5x's sensirion_convert_to_string_in_place() function to sensirion_common (#14269) --- esphome/components/sen5x/sen5x.cpp | 9 --------- esphome/components/sensirion_common/i2c_sensirion.h | 11 +++++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index fc4932d8675..09dda8bca46 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -56,15 +56,6 @@ static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { } } -// This function performs an in-place conversion of the provided buffer -// from uint16_t values to big endianness -static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) { - for (size_t i = 0; i < length; i++) { - array[i] = convert_big_endian(array[i]); - } - return reinterpret_cast(array); -} - void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index f3eb3761f60..3c2c14ccb81 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -21,6 +21,17 @@ class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; + /** + * This function performs an in-place conversion of the provided buffer + * from uint16_t values to big endianness. Useful for Sensirion strings in SEN5X and SEN6X + */ + static inline const char *sensirion_convert_to_string_in_place(uint16_t *array, size_t length) { + for (size_t i = 0; i < length; i++) { + array[i] = convert_big_endian(array[i]); + } + return reinterpret_cast(array); + } + /** Read data words from I2C device. * handles CRC check used by Sensirion sensors * @param data pointer to raw result From 228874a52b268070199dc37f4622cca1361e729b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:45:50 -0500 Subject: [PATCH 0192/2056] [config] Improve dimensions validation and fix online_image resize aspect ratio (#14274) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/runtime_image/runtime_image.cpp | 9 +++++++++ esphome/config_validation.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 603ea76f01a..5a4a2ea7d69 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -2,6 +2,7 @@ #include "image_decoder.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include #include #ifdef USE_RUNTIME_IMAGE_BMP @@ -43,6 +44,14 @@ int RuntimeImage::resize(int width, int height) { int target_width = this->fixed_width_ ? this->fixed_width_ : width; int target_height = this->fixed_height_ ? this->fixed_height_ : height; + // When both fixed dimensions are set, scale uniformly to preserve aspect ratio + if (this->fixed_width_ && this->fixed_height_ && width > 0 && height > 0) { + float scale = + std::min(static_cast(this->fixed_width_) / width, static_cast(this->fixed_height_) / height); + target_width = static_cast(width * scale); + target_height = static_cast(height * scale); + } + size_t result = this->resize_buffer_(target_width, target_height); if (result > 0 && this->progressive_display_) { // Update display dimensions for progressive display diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef1c66a20e1..3b0e4da298a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1640,7 +1640,10 @@ def dimensions(value): if width <= 0 or height <= 0: raise Invalid("Width and height must at least be 1") return [width, height] - value = string(value) + if not isinstance(value, str): + raise Invalid( + "Dimensions must be a string (WIDTHxHEIGHT). Got a number instead, try quoting the value." + ) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: raise Invalid( From 1beeb9ab5c34b1b28ba3c8dc8e78cc12be511dc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 09:54:32 -0600 Subject: [PATCH 0193/2056] [web_server] Fix uptime display overflow after ~24.8 days (#13739) --- esphome/components/web_server/web_server.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 682008c40ea..e2f9c213314 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,6 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; + root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); return builder.serialize(); } @@ -411,7 +412,12 @@ void WebServer::setup() { // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events - this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); + this->set_interval(10000, [this]() { + char buf[32]; + auto uptime = static_cast(App.scheduler.millis_64() / 1000); + buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); + this->events_.try_send_nodefer(buf, "ping", millis(), 30000); + }); } void WebServer::loop() { this->events_.loop(); } From 78ab63581b48575d04448e49d1ecc298627bce1a Mon Sep 17 00:00:00 2001 From: esphomebot Date: Thu, 26 Feb 2026 05:09:45 +1300 Subject: [PATCH 0194/2056] Update webserver local assets to 20260225-155043 (#14275) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/captive_portal/captive_index.h | 4 +- .../components/web_server/server_index_v2.h | 2286 ++--- .../components/web_server/server_index_v3.h | 8037 +++++++++-------- 3 files changed, 5166 insertions(+), 5161 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index a81edc19005..645ebb7a2f4 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { +const uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index b5dac9ae4c3..ffa9c87b3a9 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -41,10 +41,10 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x90, 0xe0, 0xbe, 0xe3, 0x24, 0x21, 0x7d, 0x18, 0xf1, 0x84, 0x3b, 0xb0, 0x8a, 0xe9, 0xd8, 0x4a, 0x08, 0xb1, 0x73, 0xd1, 0xd6, 0x1e, 0x24, 0x5e, 0xd2, 0xb0, 0x6d, 0x2c, 0xa1, 0xc4, 0x09, 0x47, 0xf8, 0x23, 0x71, 0x12, 0xec, 0xba, 0x2e, 0x47, 0xa4, 0xbf, 0xd0, 0x58, 0x49, 0x8c, 0x79, 0x0e, 0x92, 0x61, 0x7b, 0xe4, 0x71, 0x37, 0xa3, 0xe1, 0x3c, - 0xa0, 0x8e, 0xc3, 0x70, 0x8e, 0x53, 0x44, 0xfa, 0xac, 0xe1, 0x64, 0xa4, 0x0f, 0xcb, 0x9d, 0xd5, 0xd7, 0x9a, 0x90, - 0x9d, 0x36, 0x52, 0x30, 0x66, 0x1a, 0x40, 0xc0, 0xb0, 0x82, 0x27, 0x23, 0xc4, 0x4e, 0xe6, 0xd3, 0x4b, 0x9a, 0xd9, + 0xa0, 0x8e, 0xc3, 0x70, 0x8e, 0x33, 0x44, 0xfa, 0xac, 0xe1, 0xa4, 0xa4, 0x0f, 0xcb, 0x9d, 0xd6, 0xd7, 0x9a, 0x90, + 0x9d, 0x36, 0x52, 0x30, 0xa6, 0x1a, 0x40, 0xc0, 0xb0, 0x82, 0x27, 0x25, 0xc4, 0x4e, 0xe6, 0xd3, 0x4b, 0x9a, 0xd9, 0x65, 0xb5, 0x5e, 0x8d, 0x2c, 0xe6, 0x39, 0xb5, 0x82, 0x3c, 0xb7, 0xc6, 0xf3, 0x24, 0xe0, 0x2c, 0x4d, 0x2c, 0xbb, - 0x91, 0x35, 0x6c, 0x49, 0x0e, 0x25, 0x35, 0xd8, 0xa8, 0x40, 0x4e, 0x8e, 0x1a, 0xc9, 0x30, 0x6d, 0x74, 0x46, 0x18, + 0x91, 0x36, 0x6c, 0x49, 0x0e, 0x25, 0x35, 0xd8, 0xa8, 0x40, 0x4e, 0x8e, 0x1a, 0xc9, 0x30, 0x6b, 0x74, 0x46, 0x18, 0xa0, 0x44, 0x3d, 0xd5, 0x9f, 0x42, 0x00, 0xc5, 0x09, 0xcc, 0xb1, 0xc0, 0x4f, 0x38, 0xcc, 0x52, 0x4c, 0x31, 0xe7, 0x83, 0xc4, 0x5d, 0xdf, 0x28, 0x84, 0xbb, 0x53, 0x7f, 0xe6, 0x50, 0xd2, 0xa7, 0x82, 0xb8, 0xfc, 0x24, 0x00, 0x58, 0x6b, 0xeb, 0x36, 0xa0, 0x1e, 0x75, 0x2b, 0x92, 0x42, 0x1e, 0x77, 0xc7, 0x69, 0xf6, 0xcc, 0x0f, 0x22, 0x68, 0x57, @@ -85,8 +85,8 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x12, 0x0d, 0x1c, 0x44, 0x95, 0xd0, 0x39, 0xec, 0x81, 0xd6, 0x3d, 0x3c, 0xfb, 0xfc, 0xdc, 0x6e, 0x70, 0xac, 0xd0, 0x30, 0xa1, 0x7a, 0xe8, 0xdb, 0xa7, 0x54, 0x6a, 0x57, 0x42, 0xf7, 0x58, 0xc3, 0x8c, 0xdc, 0x41, 0x6e, 0x48, 0xc7, 0x2c, 0x31, 0xa6, 0x5d, 0x03, 0x09, 0x73, 0x9c, 0xa3, 0xc2, 0x58, 0xd0, 0x8d, 0x5d, 0x0b, 0xb5, 0x46, 0xae, 0xdc, - 0x62, 0x22, 0x54, 0x09, 0x63, 0x19, 0x87, 0x74, 0x54, 0x60, 0x81, 0x7a, 0x3d, 0x9b, 0x54, 0x00, 0x3a, 0xe4, 0xa3, - 0x9e, 0x7a, 0x4f, 0x72, 0x89, 0xb9, 0x8c, 0xfe, 0x32, 0xa7, 0x39, 0x97, 0x74, 0xec, 0x70, 0x9c, 0x62, 0x06, 0xfc, + 0x62, 0x22, 0x54, 0x09, 0x63, 0x19, 0x87, 0x74, 0x54, 0x60, 0x81, 0x7a, 0x3d, 0x9b, 0x4c, 0x00, 0x3a, 0xe4, 0xa3, + 0x9e, 0x7a, 0x4f, 0x72, 0x89, 0xb9, 0x8c, 0xfe, 0x32, 0xa7, 0x39, 0x97, 0x74, 0xec, 0x70, 0x9c, 0x61, 0x06, 0xfc, 0x3a, 0x4d, 0xc6, 0x6c, 0x32, 0xcf, 0x40, 0xe3, 0x81, 0xcd, 0x48, 0x93, 0xf9, 0x94, 0xea, 0xa7, 0x4d, 0xb0, 0xbd, 0x99, 0x81, 0x4c, 0xcc, 0x81, 0xa6, 0xef, 0x26, 0x27, 0x80, 0x95, 0xa3, 0xe5, 0xf2, 0xaf, 0xba, 0x93, 0x6a, 0x29, 0x4b, 0x2d, 0x6d, 0x65, 0x4d, 0x28, 0x47, 0x4a, 0x26, 0xef, 0x74, 0x14, 0xf8, 0x7c, 0x44, 0x76, 0xda, 0x25, 0x0d, @@ -120,15 +120,15 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x11, 0x53, 0x33, 0x73, 0x80, 0x0b, 0x7c, 0x92, 0xe2, 0x34, 0x3f, 0x50, 0x74, 0x07, 0x06, 0x47, 0xb1, 0x02, 0x08, 0x47, 0x8b, 0x22, 0x64, 0xf9, 0x76, 0x0c, 0xfc, 0x21, 0x50, 0x3e, 0x35, 0x46, 0xb8, 0x2f, 0xa0, 0x25, 0x8f, 0x53, 0x5a, 0x73, 0x09, 0x99, 0xd2, 0x27, 0x34, 0xa3, 0xf9, 0x06, 0x74, 0x17, 0x41, 0xef, 0x6f, 0xe4, 0x2b, 0xd0, 0xca, - 0x00, 0x8a, 0xbc, 0x67, 0xaa, 0x13, 0x35, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0xd6, 0x2c, 0x48, 0xa5, 0xb1, - 0x4b, 0x23, 0x5c, 0xb1, 0xdc, 0x8c, 0x38, 0x8e, 0x93, 0x83, 0x11, 0xa7, 0x75, 0xfb, 0x6a, 0x12, 0xf9, 0xda, 0x24, + 0x00, 0x8a, 0xbc, 0x67, 0xaa, 0x13, 0x35, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0xd5, 0x2c, 0x48, 0xa5, 0xb1, + 0x4b, 0x23, 0x5c, 0xb1, 0xdc, 0x94, 0x38, 0x8e, 0x93, 0x83, 0x11, 0xa7, 0x75, 0xfb, 0x6a, 0x12, 0xf9, 0xda, 0x24, 0x72, 0xd7, 0x30, 0xb4, 0x50, 0x45, 0xcb, 0x46, 0x73, 0x8f, 0x73, 0x64, 0xd6, 0x02, 0x7d, 0xd5, 0x05, 0x06, 0x8d, - 0x4a, 0x7e, 0x1b, 0x13, 0x8e, 0x33, 0x65, 0xe5, 0x28, 0x52, 0x03, 0x8e, 0x51, 0x35, 0x49, 0x91, 0xdc, 0x1b, 0x35, - 0x93, 0x37, 0xc5, 0x19, 0x5a, 0x51, 0xee, 0x8b, 0x42, 0x21, 0x89, 0x22, 0xb5, 0x38, 0x35, 0xad, 0xd8, 0x40, 0x0b, - 0x4e, 0x89, 0xd2, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xd2, 0x0a, 0x37, 0x35, 0x95, 0x52, - 0xeb, 0x56, 0x29, 0xc2, 0x91, 0x56, 0x4a, 0xb3, 0x6a, 0xe2, 0x84, 0xd8, 0xda, 0x27, 0x61, 0x0f, 0x16, 0x35, 0xbb, - 0xd0, 0x33, 0xaa, 0x15, 0x1e, 0xf0, 0xd4, 0x6c, 0x13, 0xbe, 0x37, 0x11, 0x4d, 0xad, 0x1f, 0x03, 0xe3, 0x59, 0x0d, - 0xe3, 0x06, 0x6a, 0x53, 0xc9, 0xbb, 0xd2, 0x11, 0x89, 0xea, 0x8d, 0x1d, 0x8a, 0x33, 0xb9, 0x10, 0x6b, 0x58, 0x5c, + 0x4a, 0x7e, 0x1b, 0x13, 0x8e, 0x53, 0x65, 0xe5, 0x28, 0x52, 0x03, 0x8e, 0x51, 0x35, 0xc9, 0x90, 0xdc, 0x1b, 0x35, + 0x93, 0x37, 0xc3, 0x29, 0x5a, 0x51, 0xee, 0x8b, 0x42, 0x21, 0x89, 0x22, 0xb5, 0x38, 0x35, 0xad, 0xd8, 0x40, 0x0b, + 0xce, 0x88, 0xd2, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xb2, 0x0a, 0x37, 0x35, 0x95, 0x52, + 0xeb, 0x56, 0x19, 0xc2, 0x91, 0x56, 0x4a, 0xd3, 0x6a, 0xe2, 0x84, 0xd8, 0xda, 0x27, 0x61, 0x0f, 0x16, 0x35, 0xbb, + 0xd0, 0x33, 0xaa, 0x15, 0x1e, 0xf0, 0xd4, 0x74, 0x13, 0xbe, 0x37, 0x11, 0x4d, 0xad, 0x1f, 0x03, 0xe3, 0x69, 0x0d, + 0xe3, 0x06, 0x6a, 0x33, 0xc9, 0xbb, 0xb2, 0x11, 0x89, 0xea, 0x8d, 0x1d, 0x8a, 0x53, 0xb9, 0x10, 0x6b, 0x58, 0x5c, 0x55, 0x3e, 0x05, 0x11, 0x82, 0x19, 0x9b, 0x83, 0x7a, 0x67, 0x4a, 0x08, 0x07, 0x80, 0x67, 0xcb, 0xe5, 0x1a, 0xd9, 0x6d, 0xd4, 0x41, 0x91, 0x5b, 0x59, 0x86, 0xcb, 0xe5, 0x33, 0x8e, 0x1c, 0xa5, 0xfd, 0x62, 0x8a, 0x06, 0x9a, 0xe7, 0x9e, 0xbc, 0x84, 0x5a, 0x42, 0x19, 0xad, 0x4a, 0x4a, 0xb3, 0xa1, 0x4e, 0xb5, 0xf5, 0x85, 0xe2, 0x06, 0xe3, 0x3e, @@ -141,7 +141,7 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x65, 0x01, 0xf4, 0x56, 0x0c, 0xef, 0xca, 0x64, 0xaf, 0x09, 0xd3, 0x52, 0xfe, 0x4a, 0xb7, 0xa0, 0xa6, 0xcc, 0xdc, 0x34, 0xf1, 0x95, 0x4f, 0xb5, 0x27, 0xdd, 0x26, 0x3b, 0x9d, 0x5e, 0x69, 0xf7, 0x69, 0x6a, 0xe8, 0x49, 0xf7, 0x86, 0x12, 0xaa, 0xe9, 0x3c, 0x0e, 0x15, 0xb0, 0x0c, 0x61, 0xaa, 0xe8, 0xe8, 0x9a, 0xc5, 0x71, 0x55, 0xfa, 0x39, 0x9c, - 0x3d, 0x57, 0x9c, 0x3d, 0xd5, 0x9c, 0x1d, 0x58, 0x05, 0x70, 0x76, 0xd9, 0x5d, 0xd5, 0x3c, 0x5d, 0xdb, 0x9e, 0xa9, + 0x3d, 0x57, 0x9c, 0x3d, 0xd3, 0x9c, 0x1d, 0x58, 0x05, 0x70, 0x76, 0xd9, 0x5d, 0xd5, 0x3c, 0x5b, 0xdb, 0x9e, 0x99, 0xe4, 0xe9, 0xb9, 0xb0, 0xa5, 0x61, 0xbc, 0xb9, 0x86, 0x00, 0x95, 0xba, 0xd7, 0x47, 0x47, 0xb9, 0x62, 0xc0, 0x08, 0x94, 0x9e, 0x4c, 0x6a, 0xba, 0x29, 0x3e, 0x3a, 0x08, 0xe7, 0x05, 0x2d, 0x29, 0xfb, 0xe4, 0x19, 0xf8, 0xea, 0x8c, 0xe9, 0x80, 0x18, 0x13, 0xc5, 0x9f, 0xa5, 0x46, 0xe9, 0xd9, 0x31, 0x35, 0xbb, 0x5c, 0xcf, 0x0e, 0x78, 0x7d, 0x35, @@ -181,1135 +181,1137 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x19, 0xb3, 0x7a, 0xcb, 0x65, 0xa8, 0x00, 0xa4, 0x6c, 0xdd, 0x1d, 0x03, 0x2b, 0xbe, 0x93, 0xac, 0xf9, 0xac, 0x32, 0xff, 0xda, 0x46, 0xf5, 0xf8, 0x28, 0x4b, 0xae, 0xfc, 0x98, 0x85, 0x16, 0xa7, 0xd3, 0x59, 0xec, 0x73, 0x6a, 0xa9, 0xf9, 0x5a, 0x3e, 0x74, 0x64, 0x97, 0x3a, 0xc3, 0xcc, 0xb0, 0x39, 0x67, 0x3a, 0xf0, 0x04, 0x7b, 0xc5, 0x81, 0x28, - 0x95, 0xd2, 0x3b, 0x9e, 0x56, 0x41, 0xb0, 0xd5, 0x38, 0x5f, 0xb3, 0x03, 0xbe, 0xb0, 0x91, 0x90, 0xcf, 0x39, 0x86, - 0xa0, 0x25, 0x21, 0xdd, 0x81, 0x7d, 0x9c, 0x5f, 0x4d, 0xfa, 0x36, 0xc4, 0x68, 0x32, 0xf2, 0x41, 0xb8, 0x86, 0xa0, - 0x42, 0x44, 0xda, 0xbd, 0xe8, 0x98, 0xf6, 0xa2, 0x46, 0x43, 0x6b, 0xd1, 0x3e, 0x49, 0x86, 0x91, 0x6c, 0x1e, 0xe0, - 0x10, 0xcf, 0x49, 0xb3, 0x83, 0x67, 0xa4, 0x2d, 0x9a, 0xf4, 0x66, 0xc7, 0xbe, 0x1a, 0x66, 0x6f, 0xcf, 0xc9, 0xdc, - 0xd8, 0xcf, 0xf9, 0x0b, 0xb0, 0xf7, 0xc9, 0x0c, 0x87, 0x24, 0x73, 0xe9, 0x0d, 0x0d, 0x1c, 0x1f, 0xe1, 0x50, 0x71, - 0x1a, 0xd4, 0x43, 0x33, 0x62, 0x54, 0x03, 0x33, 0x82, 0x7c, 0x18, 0x84, 0xc3, 0xce, 0x88, 0x10, 0x62, 0xef, 0x34, - 0x9b, 0xf6, 0x20, 0x23, 0x13, 0xee, 0x41, 0x89, 0xa1, 0x2c, 0x93, 0x29, 0x14, 0x75, 0x8d, 0x22, 0xe7, 0x0d, 0x77, - 0x39, 0xcd, 0xb9, 0x03, 0xc5, 0xe0, 0x01, 0xc8, 0x35, 0x61, 0xdb, 0xc7, 0x2d, 0xbb, 0x01, 0xa5, 0x82, 0x38, 0x11, - 0xce, 0xc8, 0x35, 0xf2, 0xc2, 0xe1, 0xfe, 0xc8, 0x14, 0x00, 0xa2, 0x10, 0x06, 0xbf, 0x1e, 0x84, 0xc3, 0xb6, 0x18, - 0xbc, 0x6f, 0x0f, 0x9c, 0x8c, 0xe4, 0x52, 0x43, 0x1b, 0xe4, 0xde, 0x07, 0x31, 0x55, 0xe4, 0x29, 0xe0, 0xd4, 0xb8, - 0x73, 0xd2, 0xec, 0x7a, 0xce, 0xdc, 0x9c, 0x44, 0x13, 0x06, 0x53, 0x58, 0xc0, 0x01, 0x81, 0xfa, 0x38, 0x23, 0x30, - 0x62, 0xd5, 0xec, 0xda, 0x53, 0xcf, 0x0f, 0xec, 0x07, 0x83, 0x73, 0xee, 0x8d, 0xb9, 0x1c, 0xfe, 0x9c, 0x2f, 0x97, - 0xf0, 0xef, 0x98, 0x0f, 0x32, 0x72, 0x2d, 0x8a, 0x26, 0xaa, 0x68, 0x0a, 0x45, 0x1f, 0x3c, 0x00, 0x15, 0xe7, 0xa5, - 0x96, 0x25, 0xd7, 0x64, 0x4a, 0x04, 0xec, 0x7b, 0x7b, 0xc9, 0x30, 0x6a, 0x74, 0x46, 0xe0, 0xe4, 0xcf, 0x78, 0xfe, - 0x1d, 0xe3, 0x91, 0x63, 0xb7, 0xfa, 0x36, 0x1a, 0xd8, 0x16, 0x2c, 0x6d, 0x2f, 0x6d, 0x10, 0x89, 0x61, 0xbf, 0xf1, - 0x8a, 0x7b, 0xf3, 0x3e, 0x69, 0x0f, 0x1c, 0xa6, 0x5c, 0x7a, 0x08, 0xfb, 0x8a, 0x71, 0xb6, 0xf1, 0x1c, 0x35, 0x18, - 0x6f, 0xe8, 0xe7, 0x39, 0x6a, 0xdc, 0x36, 0xa6, 0xc8, 0xf3, 0x1b, 0xb7, 0x0d, 0x67, 0x4e, 0x08, 0x69, 0x76, 0xcb, - 0x66, 0x5a, 0xfc, 0x45, 0xc8, 0x9b, 0x6a, 0x7f, 0xe7, 0x50, 0x6c, 0x87, 0xb4, 0xe1, 0x24, 0x43, 0x3a, 0x5a, 0x2e, - 0xed, 0xe3, 0x41, 0xdf, 0x46, 0x0d, 0x47, 0x13, 0x5a, 0x4b, 0x53, 0x1a, 0x42, 0x98, 0x8d, 0x0a, 0x15, 0x4f, 0x7a, - 0x52, 0x8b, 0x1d, 0x2d, 0xaa, 0xcd, 0x6e, 0xf0, 0x00, 0x5a, 0x94, 0x86, 0x8c, 0x54, 0x58, 0x67, 0x30, 0x4d, 0x4d, - 0xcc, 0x29, 0x69, 0xe3, 0x8c, 0x68, 0xf7, 0x75, 0x44, 0x78, 0x45, 0xf0, 0x3e, 0xa9, 0xaa, 0xe3, 0x61, 0x80, 0xc3, - 0x11, 0x79, 0x2a, 0x0d, 0x92, 0x9e, 0x76, 0x8e, 0xd3, 0x98, 0x3c, 0x59, 0x89, 0xe2, 0x06, 0x10, 0x60, 0xb9, 0x71, - 0x83, 0x79, 0x96, 0xd1, 0x84, 0xbf, 0x4e, 0x43, 0xa5, 0xa7, 0xd1, 0x18, 0x4c, 0x25, 0x08, 0xcf, 0x62, 0x50, 0xd2, - 0xba, 0x7a, 0x67, 0xcc, 0xd7, 0x5e, 0xcf, 0xc8, 0x5c, 0xea, 0x4f, 0x22, 0x68, 0xdb, 0x9b, 0x29, 0xcb, 0xd8, 0x41, - 0x78, 0xae, 0xa2, 0xb9, 0x8e, 0xeb, 0xba, 0x33, 0x37, 0x80, 0xd7, 0x30, 0x40, 0x8e, 0x0a, 0xb1, 0x8f, 0x9c, 0x9c, - 0xdc, 0xb8, 0x09, 0xbd, 0x11, 0xa3, 0x3a, 0xa8, 0x92, 0xcc, 0x7a, 0x7b, 0x1d, 0x47, 0x3d, 0xc1, 0x6e, 0x72, 0x37, - 0x49, 0x43, 0x0a, 0xe8, 0x81, 0xf8, 0xbd, 0x2a, 0x8a, 0xfc, 0xdc, 0x0c, 0x52, 0x55, 0xf0, 0x0d, 0x4d, 0xff, 0xf5, - 0x0c, 0x9c, 0xbe, 0x42, 0xd9, 0x2a, 0x2b, 0x4b, 0x4f, 0x38, 0x42, 0x6c, 0xec, 0xcc, 0x5c, 0x08, 0xee, 0x09, 0x12, - 0x62, 0x60, 0xcb, 0xcd, 0x4c, 0xa2, 0xba, 0x2d, 0xfb, 0x9c, 0x92, 0x70, 0x98, 0x35, 0x1a, 0xc2, 0x11, 0x3d, 0x97, - 0x24, 0x31, 0x43, 0x78, 0x5a, 0xee, 0x2d, 0x5d, 0xef, 0x2d, 0xa9, 0x8f, 0xe4, 0x4c, 0xeb, 0x0e, 0xdd, 0x06, 0xe3, - 0x48, 0xf8, 0x0a, 0xb9, 0x73, 0x8b, 0xf0, 0x98, 0xb4, 0x9c, 0xa1, 0x3b, 0xf8, 0xf3, 0x08, 0x0d, 0x1c, 0xf7, 0x2b, - 0xd4, 0x92, 0x8c, 0x63, 0x8a, 0x7a, 0xbe, 0x1c, 0x62, 0x21, 0xa2, 0x98, 0x1d, 0x2c, 0x7c, 0x89, 0x5e, 0x8a, 0x13, - 0x7f, 0x4a, 0xbd, 0x31, 0xec, 0x71, 0x4d, 0x37, 0x6f, 0x31, 0xd0, 0x91, 0x37, 0x56, 0x9c, 0xc4, 0xb5, 0x07, 0xbf, - 0xf0, 0xf2, 0x69, 0x60, 0x0f, 0xbe, 0xae, 0x9e, 0xfe, 0x6c, 0x0f, 0xbe, 0xe5, 0xde, 0xb7, 0x85, 0x72, 0x77, 0xd7, - 0x86, 0x78, 0xa8, 0x87, 0x28, 0xe4, 0xc2, 0x18, 0x98, 0x9b, 0xa3, 0x75, 0x47, 0xc7, 0x0c, 0x15, 0x6c, 0x5c, 0xb2, - 0xa2, 0xdc, 0xe5, 0xfe, 0x04, 0x50, 0x6a, 0xac, 0x40, 0x6e, 0x46, 0xf7, 0xab, 0x09, 0x03, 0xa1, 0x68, 0x6a, 0x05, - 0x54, 0xce, 0xfa, 0x6d, 0xb4, 0xa8, 0xd5, 0x15, 0x1a, 0x53, 0x3d, 0x9a, 0x5e, 0x72, 0xe9, 0x29, 0x69, 0xf7, 0xa6, - 0xc7, 0xb3, 0xde, 0xb4, 0xd1, 0x40, 0xb9, 0x26, 0xac, 0xf9, 0x70, 0x3a, 0xc2, 0xaf, 0xc1, 0xab, 0x67, 0x52, 0x12, - 0xae, 0x4d, 0xaf, 0xab, 0xa6, 0xd7, 0x68, 0xa4, 0x05, 0xea, 0x19, 0x4d, 0x67, 0xb2, 0x69, 0x51, 0x48, 0x9c, 0xac, - 0x12, 0xda, 0x11, 0x12, 0x25, 0x90, 0x12, 0x45, 0x08, 0x39, 0xe3, 0x68, 0x63, 0xaf, 0xd0, 0x27, 0x34, 0x17, 0x3b, - 0x16, 0x98, 0xa7, 0x94, 0x11, 0x0e, 0x60, 0x01, 0x9a, 0x96, 0xae, 0xe0, 0x5b, 0x3c, 0x6f, 0x74, 0x04, 0x91, 0x37, - 0x3b, 0xbd, 0x7a, 0x5f, 0x8f, 0xaa, 0xbe, 0xf0, 0xbc, 0x41, 0x6e, 0x4b, 0x2c, 0x15, 0x69, 0xa3, 0x51, 0xd4, 0xe3, - 0x9d, 0x7a, 0xdf, 0xd6, 0x22, 0x10, 0x27, 0xab, 0xa9, 0x19, 0x5a, 0xbe, 0x56, 0x12, 0x95, 0xb9, 0x2c, 0x49, 0x68, - 0x06, 0x32, 0x94, 0x70, 0xcc, 0x8a, 0xa2, 0x94, 0xeb, 0x6f, 0x40, 0x88, 0x62, 0x4a, 0x12, 0xe0, 0x3b, 0xc2, 0xec, - 0xc2, 0x29, 0xce, 0x70, 0x24, 0xb8, 0x06, 0x21, 0xe4, 0x54, 0x27, 0xb5, 0x70, 0xc1, 0x81, 0x7c, 0xc2, 0x0c, 0x89, - 0x94, 0x13, 0xea, 0x9e, 0xef, 0x9e, 0xa6, 0x77, 0x9a, 0x64, 0x43, 0x36, 0xf2, 0x44, 0xb5, 0x58, 0xf1, 0xad, 0x80, - 0xbc, 0x73, 0x38, 0x2a, 0xc3, 0x23, 0xae, 0x60, 0x7f, 0x4f, 0x59, 0x46, 0x85, 0x06, 0xbe, 0xab, 0xcd, 0x3e, 0xbf, - 0xae, 0x3e, 0xfa, 0xa6, 0xf3, 0x06, 0x10, 0x19, 0x80, 0x6f, 0x27, 0x25, 0x6b, 0xd5, 0xce, 0x77, 0x4f, 0xde, 0x6c, - 0x32, 0x81, 0x97, 0x4b, 0x65, 0xfc, 0xfa, 0xa0, 0xd9, 0xe0, 0xa0, 0x82, 0xd4, 0x57, 0x3f, 0x3c, 0xc7, 0x17, 0x0a, - 0x52, 0xe0, 0x24, 0x40, 0x45, 0xe7, 0xbb, 0x27, 0xef, 0x9d, 0x44, 0xb8, 0x96, 0x10, 0x36, 0xa7, 0xed, 0x64, 0xc4, - 0x89, 0x08, 0x45, 0x72, 0xee, 0x25, 0xe3, 0xca, 0x0c, 0xf1, 0xed, 0x45, 0xe2, 0x25, 0xd8, 0x0f, 0x43, 0x36, 0x22, - 0xbe, 0xc2, 0x00, 0xf1, 0x11, 0xf6, 0x6b, 0x66, 0x19, 0x81, 0x05, 0x10, 0x63, 0x9d, 0xc1, 0x4a, 0xb8, 0x52, 0xf1, - 0x43, 0xd8, 0x17, 0xa3, 0xf2, 0x42, 0x8a, 0x8e, 0x9f, 0xd7, 0x72, 0xd3, 0x2a, 0x6b, 0xf4, 0x5b, 0xb0, 0x9c, 0xf4, - 0xc3, 0x6b, 0xd5, 0x75, 0x59, 0xf0, 0x54, 0x27, 0x91, 0x9d, 0xef, 0x9e, 0xbc, 0x52, 0x79, 0x64, 0x33, 0x5f, 0x73, - 0xfb, 0x35, 0x0b, 0xf3, 0xe4, 0x95, 0x5b, 0xbd, 0x15, 0x95, 0xcf, 0x77, 0x4f, 0x3e, 0x6c, 0xaa, 0x06, 0xe5, 0xc5, - 0xbc, 0x32, 0xf1, 0x05, 0x7c, 0x0b, 0x1a, 0x7b, 0x0b, 0x25, 0x1a, 0x3c, 0x56, 0x60, 0x21, 0x8e, 0xbc, 0xbc, 0x28, - 0x3d, 0x23, 0x4f, 0x71, 0x4a, 0x44, 0x1c, 0xa8, 0xbe, 0x6a, 0x4a, 0xc9, 0x63, 0x69, 0x72, 0x16, 0xa4, 0x33, 0xba, - 0x25, 0x38, 0x74, 0x82, 0x5c, 0x36, 0x85, 0x04, 0x1a, 0x01, 0x3a, 0xc3, 0x3b, 0x6d, 0xd4, 0xab, 0x0b, 0xaf, 0x54, - 0x10, 0x69, 0x56, 0x93, 0x2c, 0x38, 0x22, 0x6d, 0xec, 0x93, 0x36, 0x0e, 0x48, 0x3e, 0x6c, 0x4b, 0xf1, 0xd0, 0x0b, - 0xca, 0x7e, 0xa5, 0x90, 0x81, 0xdc, 0xb0, 0x40, 0xee, 0x56, 0x29, 0x7e, 0xc3, 0x5e, 0x20, 0x5c, 0x8f, 0x42, 0xa2, - 0x87, 0xd2, 0x68, 0x75, 0x32, 0x9c, 0x89, 0x8e, 0xcf, 0xd8, 0x65, 0x0c, 0xd9, 0x25, 0x30, 0x2b, 0xcc, 0x91, 0x57, - 0x56, 0xed, 0xa8, 0xaa, 0x81, 0x2b, 0xd6, 0x29, 0xc3, 0x81, 0x0b, 0x8c, 0x1b, 0x07, 0x2a, 0x19, 0x27, 0x5f, 0x6f, - 0xf2, 0x70, 0x6f, 0xcf, 0x91, 0x8d, 0xbe, 0xe3, 0x4e, 0xa6, 0xdf, 0x57, 0xa1, 0xbb, 0x6f, 0x25, 0xaf, 0x08, 0x91, - 0x80, 0xbf, 0xd1, 0xf0, 0x47, 0x05, 0xc4, 0xa1, 0x9d, 0xa0, 0x8e, 0x41, 0x0d, 0xbc, 0xd0, 0xf4, 0xea, 0xd3, 0x6f, - 0x34, 0xca, 0x30, 0x6d, 0x1d, 0x5b, 0x27, 0x38, 0x2d, 0xae, 0x9c, 0x32, 0xff, 0xa7, 0xbd, 0x96, 0x35, 0xa5, 0x41, - 0x40, 0xcc, 0xa4, 0x59, 0xa6, 0x27, 0x63, 0x6c, 0x09, 0x06, 0xf5, 0x5e, 0xa8, 0xc4, 0x05, 0x2c, 0x72, 0xac, 0x54, - 0x25, 0xcd, 0xce, 0xba, 0xc8, 0xd3, 0x95, 0x20, 0x2c, 0x05, 0x95, 0x1a, 0x85, 0x22, 0xef, 0x57, 0xeb, 0x99, 0x97, - 0x38, 0x47, 0xca, 0xc7, 0x25, 0xa0, 0x10, 0xc8, 0xea, 0x96, 0x48, 0x79, 0x4e, 0x26, 0xdb, 0x49, 0xfe, 0xc4, 0x20, - 0xf9, 0x27, 0x84, 0x1a, 0xe4, 0x2f, 0x3d, 0x1c, 0x6e, 0xaa, 0x5c, 0x0b, 0xb9, 0x7e, 0x75, 0x3a, 0x23, 0xe0, 0x43, - 0xab, 0x63, 0xb4, 0x16, 0x57, 0xdc, 0xc2, 0x50, 0xcc, 0x1d, 0x22, 0xbc, 0x90, 0x58, 0x07, 0x81, 0x9d, 0x2a, 0xaa, - 0x06, 0x43, 0x6f, 0x72, 0xe9, 0x99, 0x1c, 0xf0, 0xe4, 0xc3, 0xdd, 0x01, 0xd1, 0xd3, 0xd9, 0xfa, 0xce, 0x35, 0x32, - 0x40, 0x61, 0xd6, 0xc6, 0xc6, 0xad, 0xe7, 0x83, 0xc2, 0xf8, 0x65, 0x20, 0xbb, 0xce, 0x7c, 0x56, 0x36, 0xa1, 0x96, - 0x7f, 0x00, 0x6d, 0xa7, 0x23, 0x6a, 0x50, 0xa3, 0x5b, 0xe0, 0x47, 0x32, 0x0f, 0xd5, 0xcf, 0xb6, 0xb0, 0x8f, 0x13, - 0x51, 0x81, 0x26, 0xe1, 0xe6, 0xd7, 0x4f, 0x0a, 0x45, 0x26, 0x12, 0x34, 0xb4, 0x00, 0xfe, 0x27, 0x49, 0x1e, 0xe8, - 0x46, 0xc8, 0x05, 0x40, 0xd0, 0x44, 0xe0, 0xa9, 0x42, 0x98, 0x6d, 0x57, 0xce, 0xf7, 0xe7, 0x3b, 0x84, 0x4c, 0x2a, - 0xe7, 0xe3, 0xbb, 0x2a, 0xfb, 0x0a, 0xc8, 0x02, 0x79, 0x60, 0x3c, 0x96, 0x05, 0x32, 0x7e, 0x79, 0xaa, 0xab, 0x0b, - 0x03, 0xd2, 0xad, 0xf4, 0x6d, 0x23, 0xb6, 0x29, 0xbc, 0x72, 0xf2, 0xbd, 0x46, 0xc3, 0xca, 0xdb, 0x5d, 0x78, 0xfb, - 0x92, 0x0b, 0x18, 0xe1, 0xf9, 0xbd, 0xa8, 0xad, 0xfb, 0x2d, 0x3e, 0xae, 0xa6, 0xb0, 0xac, 0x2c, 0x8a, 0xcb, 0x92, - 0x9c, 0x66, 0xfc, 0x09, 0x1d, 0xa7, 0x19, 0x84, 0x2c, 0x4a, 0x9c, 0xa0, 0x62, 0xd7, 0x70, 0xdb, 0x89, 0xf9, 0x19, - 0x71, 0x82, 0x95, 0x09, 0x8a, 0x5f, 0x1f, 0x45, 0xd4, 0xfa, 0x7c, 0xb5, 0xd5, 0x64, 0x6f, 0xef, 0x5d, 0x85, 0x26, - 0x05, 0xa5, 0x80, 0xc2, 0x60, 0x5a, 0x52, 0xa5, 0x51, 0xa1, 0xdc, 0x5d, 0xa7, 0x74, 0x01, 0x68, 0x86, 0x61, 0xf2, - 0x9e, 0xe7, 0x84, 0x17, 0x93, 0x55, 0x16, 0xaf, 0x5c, 0x13, 0xcc, 0x34, 0x5b, 0x80, 0xc3, 0x83, 0xa1, 0x2d, 0x7d, - 0x45, 0x79, 0x95, 0x12, 0x5b, 0xc2, 0x70, 0x0a, 0xc8, 0x72, 0x84, 0x11, 0x62, 0x50, 0xe0, 0x46, 0xa3, 0xe4, 0x2d, - 0xe8, 0x95, 0x11, 0xce, 0xdd, 0x08, 0x92, 0x60, 0x6b, 0x5b, 0x16, 0x21, 0x2c, 0x33, 0x73, 0x8c, 0x5c, 0x82, 0x93, - 0xe7, 0x9b, 0x3c, 0xca, 0x9a, 0xa8, 0xa9, 0x90, 0x3a, 0x50, 0x23, 0x45, 0x65, 0x03, 0xf7, 0xca, 0x61, 0x4a, 0x71, - 0xd3, 0x71, 0x33, 0x60, 0xc0, 0x3f, 0x73, 0x47, 0xc6, 0xa2, 0x40, 0x66, 0x64, 0xee, 0xdc, 0xa9, 0x0d, 0xdd, 0xcb, - 0x44, 0x33, 0xac, 0x10, 0x17, 0x99, 0x68, 0xca, 0x44, 0x5c, 0xef, 0xb4, 0xe2, 0xa5, 0x57, 0x32, 0x8f, 0x9a, 0x6b, - 0x2e, 0x58, 0x65, 0x92, 0x18, 0xd3, 0xbf, 0x92, 0xa9, 0xd1, 0x65, 0x25, 0x50, 0xc3, 0xe8, 0xb5, 0xf5, 0x44, 0xac, - 0x01, 0x2d, 0x80, 0xbe, 0x16, 0xa7, 0xdc, 0x58, 0x51, 0xed, 0xc3, 0x16, 0x63, 0x1a, 0x52, 0xff, 0x1d, 0xe4, 0xba, - 0xac, 0xee, 0xf9, 0xe7, 0x42, 0x16, 0x32, 0x9c, 0xd7, 0x18, 0x7b, 0x2a, 0x18, 0x3b, 0x02, 0x3d, 0x4d, 0xa7, 0x7f, - 0x0f, 0x54, 0xca, 0x8b, 0xca, 0x5d, 0x74, 0x14, 0x89, 0xbd, 0x2e, 0xc3, 0xe5, 0xc6, 0xef, 0x95, 0xd5, 0xf0, 0x18, - 0x81, 0x34, 0x20, 0xac, 0x38, 0x7b, 0x8a, 0x70, 0xde, 0x68, 0xf4, 0xf2, 0x63, 0x5a, 0xb9, 0x48, 0x2a, 0x18, 0x19, - 0x44, 0x74, 0x81, 0xe0, 0x6b, 0x32, 0x14, 0x62, 0xfe, 0x3a, 0x3f, 0x3b, 0x07, 0x57, 0xfb, 0xc9, 0x3b, 0xc7, 0xe4, - 0x6a, 0x66, 0xdd, 0x32, 0x68, 0x0a, 0xf3, 0x71, 0xaa, 0x78, 0xcb, 0xdb, 0xbb, 0x33, 0x3c, 0x00, 0xee, 0x9d, 0x0e, - 0x86, 0x6c, 0x34, 0xd4, 0xe3, 0x92, 0x25, 0x94, 0xbb, 0xaf, 0x87, 0xaa, 0xc4, 0x44, 0x73, 0xb0, 0x1e, 0xaf, 0x4c, - 0x59, 0x4e, 0xf2, 0xa2, 0xc8, 0x69, 0x15, 0xdf, 0x5f, 0xc9, 0xc0, 0x14, 0xc2, 0x65, 0xdd, 0xd9, 0x7e, 0x3a, 0x23, - 0x1c, 0x1b, 0x84, 0xfa, 0x76, 0x5b, 0xe8, 0xa3, 0x02, 0x13, 0xf6, 0xb5, 0x12, 0x8a, 0xdf, 0x6e, 0x12, 0x8a, 0x38, - 0x55, 0x5b, 0x5e, 0x08, 0xc4, 0xce, 0x3d, 0x04, 0xa2, 0x72, 0xb2, 0x6b, 0x99, 0x08, 0xea, 0x48, 0x4d, 0x26, 0xd6, - 0x97, 0x94, 0xa4, 0x98, 0xa9, 0xd5, 0xe8, 0x77, 0x97, 0x4b, 0x36, 0x6c, 0x83, 0x13, 0xc9, 0xb6, 0xe1, 0x67, 0x47, - 0xfe, 0x34, 0x38, 0xb1, 0x74, 0x02, 0x3b, 0xac, 0x34, 0x59, 0x90, 0x0b, 0x69, 0xce, 0x8e, 0xc8, 0xca, 0x12, 0x34, - 0xad, 0x28, 0x48, 0x11, 0x38, 0x61, 0x65, 0x94, 0x09, 0x20, 0x16, 0xb2, 0x42, 0x19, 0x90, 0xce, 0xc6, 0xf4, 0x3f, - 0x6d, 0x5e, 0x7e, 0x5a, 0x13, 0xad, 0xc9, 0x15, 0xa9, 0x3e, 0xd4, 0x12, 0x0e, 0x14, 0x04, 0x4a, 0x3f, 0xdc, 0x11, - 0x26, 0x68, 0x25, 0xca, 0x91, 0x29, 0x87, 0x70, 0x1b, 0x5c, 0x68, 0x3b, 0xef, 0x64, 0x80, 0x77, 0x83, 0x34, 0xc1, - 0x99, 0x41, 0xd7, 0xcf, 0x09, 0xaf, 0xb1, 0x92, 0x88, 0x28, 0x4b, 0x09, 0x07, 0x82, 0x4c, 0x39, 0x49, 0x87, 0xed, - 0x11, 0x28, 0xa0, 0x3d, 0xff, 0x38, 0xad, 0x4c, 0x60, 0xbf, 0xd1, 0x40, 0x81, 0x1e, 0x35, 0x1a, 0xb2, 0x86, 0x3f, - 0xc2, 0x14, 0xfb, 0xd2, 0x30, 0x39, 0xdd, 0xdb, 0x73, 0x82, 0x6a, 0xdc, 0xa1, 0x3f, 0x42, 0x38, 0x5b, 0x2e, 0x1d, - 0x01, 0x56, 0x80, 0x96, 0xcb, 0xc0, 0x04, 0x4b, 0xbc, 0x86, 0x66, 0x93, 0x01, 0x27, 0x13, 0x21, 0x00, 0x27, 0x00, - 0x61, 0x83, 0x38, 0x81, 0x72, 0xee, 0x05, 0xe0, 0x8c, 0x6a, 0xa4, 0x43, 0xbf, 0xd1, 0x19, 0x19, 0x8c, 0x6b, 0xe8, - 0x8f, 0x48, 0x50, 0x40, 0x72, 0x6b, 0xae, 0x44, 0xe4, 0xcf, 0x20, 0xca, 0x7e, 0x16, 0x92, 0x45, 0x76, 0x68, 0xae, - 0xc6, 0xaa, 0x33, 0xa0, 0xa4, 0x28, 0xb5, 0xac, 0xba, 0x5e, 0x2d, 0x0b, 0xa2, 0xac, 0x84, 0x55, 0x2c, 0x78, 0x00, - 0x96, 0x7d, 0x49, 0xe6, 0xbf, 0xf0, 0x32, 0xcd, 0xfa, 0xdb, 0x8d, 0xc9, 0xd5, 0xae, 0xeb, 0xfa, 0xd9, 0x44, 0x44, - 0x32, 0x74, 0x14, 0x56, 0x10, 0xff, 0xbe, 0x02, 0xd3, 0x18, 0x78, 0x58, 0x8e, 0x35, 0x22, 0x12, 0x7c, 0xad, 0xda, - 0xe8, 0x13, 0x25, 0xbf, 0x6e, 0xf4, 0x32, 0x48, 0x48, 0xbe, 0xfe, 0xad, 0x90, 0x1c, 0x28, 0x48, 0x24, 0x79, 0xac, - 0xe0, 0x6c, 0x0b, 0x2e, 0x7e, 0xe5, 0x2b, 0x38, 0xdb, 0x8e, 0xdb, 0x92, 0x21, 0x6c, 0x83, 0xcf, 0xe0, 0x0d, 0x12, - 0xd0, 0xaa, 0xc0, 0x80, 0xf2, 0x70, 0x55, 0xf7, 0x92, 0xac, 0x14, 0x84, 0x29, 0x27, 0x0e, 0xab, 0x6f, 0x80, 0x4a, - 0x1b, 0x35, 0x0c, 0x5f, 0xe6, 0x4d, 0x90, 0xe1, 0x12, 0xa8, 0xa7, 0xae, 0x00, 0x39, 0x29, 0x5f, 0x3b, 0xa4, 0x22, - 0xec, 0x48, 0x25, 0xce, 0x0d, 0xfc, 0x19, 0x9f, 0x67, 0xa0, 0x4a, 0xe5, 0xfa, 0x37, 0x14, 0xc3, 0x59, 0x10, 0x51, - 0x06, 0x3f, 0xa0, 0x60, 0xe6, 0xe7, 0x39, 0xbb, 0x92, 0x65, 0xea, 0x37, 0xce, 0x88, 0x26, 0xe5, 0x5c, 0xea, 0x84, - 0x29, 0xea, 0xa5, 0x8a, 0x4e, 0xeb, 0x68, 0x7b, 0x76, 0x45, 0x13, 0xfe, 0x92, 0xe5, 0x9c, 0x26, 0x30, 0xfd, 0x8a, - 0xe2, 0x60, 0x46, 0x39, 0x82, 0x0d, 0x5b, 0x6b, 0xe5, 0x87, 0xe1, 0x9d, 0x4d, 0x78, 0x5d, 0x07, 0x8a, 0xfc, 0x24, - 0x8c, 0xe5, 0x20, 0x66, 0x42, 0xa3, 0x4e, 0xe2, 0x2c, 0x6b, 0x9a, 0xf9, 0x34, 0x95, 0xb2, 0x21, 0xb8, 0xbb, 0xc3, - 0x88, 0x96, 0x04, 0x5a, 0x7a, 0xde, 0xa9, 0xb5, 0x40, 0xc0, 0x7b, 0xcb, 0x22, 0x98, 0x33, 0xc1, 0xdc, 0xe0, 0xa8, - 0x6e, 0x1d, 0x4e, 0x4d, 0x37, 0xdf, 0x6d, 0x3c, 0xd8, 0xb6, 0x49, 0x38, 0x08, 0x3a, 0x79, 0xb8, 0xdd, 0xb2, 0x7a, - 0xa5, 0x25, 0x87, 0x96, 0x16, 0xec, 0xbe, 0x8c, 0x19, 0x2d, 0x34, 0x79, 0x21, 0xbd, 0x15, 0x77, 0x39, 0xf9, 0x05, - 0x4e, 0x0e, 0x3d, 0xe7, 0xd3, 0x78, 0xe5, 0x80, 0x4c, 0x6f, 0xb7, 0xd4, 0xfe, 0x77, 0xb9, 0xf3, 0x04, 0xbf, 0x82, - 0xb0, 0xee, 0x37, 0x55, 0xf5, 0xf5, 0x70, 0xee, 0x37, 0x15, 0x82, 0xbe, 0xf1, 0xd6, 0xea, 0x19, 0x61, 0xdc, 0xae, - 0x7b, 0xe4, 0xb6, 0x6d, 0xad, 0x2d, 0xfd, 0x28, 0x83, 0x48, 0x32, 0xd5, 0x52, 0xec, 0x07, 0x5c, 0x25, 0xaa, 0x41, - 0xc2, 0x5c, 0xdd, 0x42, 0xa2, 0x2a, 0xc5, 0x50, 0xea, 0xf0, 0xdb, 0x96, 0x47, 0xc9, 0x98, 0x54, 0xda, 0x19, 0x6f, - 0xfd, 0x8c, 0xef, 0xc2, 0x2e, 0xcb, 0xd6, 0x4e, 0xe3, 0x45, 0x04, 0x3c, 0x68, 0xf7, 0x1b, 0xc2, 0x30, 0xb6, 0x73, - 0x79, 0x18, 0xc8, 0xec, 0x9f, 0x64, 0x5a, 0x77, 0xab, 0x5b, 0x19, 0xaf, 0xc1, 0xfe, 0x47, 0x38, 0xd2, 0x47, 0xe4, - 0xa8, 0xe2, 0xc0, 0xd4, 0x5b, 0x14, 0xa5, 0x53, 0x20, 0x93, 0xca, 0x5b, 0x82, 0x70, 0x56, 0x88, 0xf0, 0xf6, 0xf7, - 0xf8, 0x07, 0xc5, 0x12, 0xcf, 0x4b, 0x8e, 0xf3, 0xec, 0xbe, 0x1c, 0x51, 0x82, 0x5f, 0x46, 0xef, 0x81, 0x8e, 0x05, - 0x85, 0x16, 0x9a, 0x8a, 0x9e, 0xa6, 0x6a, 0x22, 0x5b, 0xf3, 0x52, 0x31, 0x2d, 0x33, 0x6a, 0xc4, 0x30, 0x1b, 0x12, - 0x39, 0xb5, 0x95, 0xcd, 0xcb, 0x5d, 0x55, 0x1b, 0x17, 0x6d, 0xc1, 0x62, 0x15, 0x58, 0x5c, 0x2e, 0x9d, 0x3a, 0xaa, - 0x09, 0x33, 0xe2, 0x18, 0x08, 0x33, 0x23, 0xa1, 0xa2, 0xa6, 0x59, 0xcb, 0x36, 0x0e, 0x5a, 0xcd, 0x27, 0xd2, 0xba, - 0x79, 0x0d, 0x0e, 0xd3, 0x85, 0x20, 0x9b, 0x9b, 0x3e, 0x05, 0x2c, 0x67, 0x57, 0x0e, 0x64, 0x60, 0xe8, 0xc7, 0x32, - 0x57, 0xb6, 0x4a, 0x6a, 0xdd, 0x80, 0x5f, 0x74, 0x47, 0xb6, 0xac, 0x42, 0xdd, 0xfa, 0x7b, 0x23, 0xd7, 0xe8, 0x69, - 0xba, 0x2d, 0xd7, 0xa8, 0xa6, 0xed, 0xee, 0xb4, 0xd1, 0xdd, 0x79, 0xa9, 0x72, 0xac, 0xcd, 0x55, 0x7e, 0xc3, 0x70, - 0x1d, 0xa0, 0x4d, 0x89, 0x66, 0xcd, 0x55, 0x4e, 0x8b, 0xe2, 0xbc, 0x3c, 0x4d, 0x20, 0x52, 0x77, 0xce, 0x25, 0xfd, - 0x2b, 0xab, 0x51, 0x1c, 0xca, 0x75, 0xbe, 0x27, 0x93, 0x38, 0xbd, 0xf4, 0xe3, 0xf7, 0x30, 0x5e, 0xf5, 0xf2, 0xf9, - 0x6d, 0x98, 0xf9, 0x9c, 0x2a, 0xee, 0x52, 0xc1, 0xf0, 0xbd, 0x01, 0xc3, 0xf7, 0x92, 0x4f, 0x57, 0xed, 0xf1, 0xe2, - 0x65, 0xd9, 0x81, 0x77, 0x5e, 0x68, 0x96, 0x71, 0xcb, 0x37, 0x8f, 0xb1, 0xca, 0xc2, 0x6e, 0x4b, 0x16, 0x76, 0xcb, - 0x9d, 0xd5, 0xae, 0x1c, 0xe7, 0x87, 0xcd, 0xbd, 0xac, 0x73, 0xb6, 0x1f, 0xaa, 0x8d, 0xff, 0x83, 0x77, 0x67, 0x1b, - 0x83, 0xcb, 0xed, 0xbb, 0xfb, 0x22, 0x59, 0x45, 0x82, 0xfc, 0x12, 0x92, 0x0e, 0x38, 0xe9, 0x1b, 0x87, 0x0e, 0x2a, - 0x39, 0xa5, 0xf3, 0x80, 0x9c, 0x60, 0x9e, 0xf3, 0x74, 0xaa, 0xfa, 0xcc, 0xd5, 0x49, 0x23, 0xf1, 0x12, 0x5c, 0xd1, - 0x22, 0xd6, 0xee, 0xd5, 0xcf, 0x72, 0x2d, 0x3e, 0xb2, 0x24, 0xf4, 0x72, 0xac, 0xa4, 0x48, 0xee, 0xa5, 0x05, 0xd1, - 0xd9, 0xc6, 0xeb, 0xef, 0xf0, 0x98, 0x25, 0x2c, 0x8f, 0x68, 0xe6, 0x64, 0x68, 0xb1, 0x6d, 0xb0, 0x0c, 0x02, 0x32, - 0x72, 0x30, 0xfc, 0xd7, 0xea, 0xd4, 0x9f, 0x0b, 0xbd, 0x81, 0x1f, 0x68, 0x4a, 0x79, 0x94, 0x86, 0x90, 0x96, 0xe2, - 0x86, 0xe5, 0xa1, 0xa6, 0xbd, 0xbd, 0x1d, 0xc7, 0x16, 0x6e, 0x09, 0x38, 0x00, 0x6e, 0xbe, 0x41, 0x83, 0x05, 0x9c, - 0xcf, 0xa9, 0x86, 0xa6, 0x68, 0x41, 0x57, 0x8f, 0xb2, 0x70, 0xf7, 0x23, 0xbd, 0xc5, 0x09, 0x2a, 0x0a, 0x4f, 0x42, - 0x6d, 0x8f, 0x19, 0x8d, 0x43, 0x1b, 0x7f, 0xa4, 0xb7, 0x5e, 0x79, 0x66, 0x5c, 0x1c, 0x71, 0x16, 0x0b, 0x68, 0xa7, - 0xd7, 0x89, 0x8d, 0xab, 0x41, 0xbc, 0x45, 0x81, 0xd3, 0x8c, 0x4d, 0x80, 0x38, 0xbf, 0xa1, 0xb7, 0x9e, 0xec, 0x8f, - 0x19, 0xe7, 0xf5, 0xd0, 0x42, 0xa3, 0xde, 0x35, 0x8a, 0xcd, 0x65, 0x50, 0x06, 0xc5, 0x50, 0xb4, 0x1d, 0x91, 0x5a, - 0xbd, 0xca, 0x3c, 0x44, 0xa8, 0xb8, 0xef, 0x54, 0xf0, 0x37, 0xa6, 0x68, 0xe3, 0xb5, 0xcc, 0xd7, 0x95, 0x46, 0x14, - 0x1a, 0x54, 0x99, 0x1e, 0xbb, 0x4e, 0xa2, 0x77, 0x9d, 0x3a, 0x84, 0x60, 0x38, 0xc2, 0xbe, 0xe1, 0xaa, 0x53, 0xef, - 0xaf, 0x32, 0x21, 0xa4, 0x8a, 0x24, 0xbd, 0xa8, 0xda, 0x59, 0xbb, 0x0e, 0xe0, 0x1d, 0x12, 0x5a, 0x7c, 0x71, 0x26, - 0xb3, 0xd0, 0xd9, 0xa2, 0x7f, 0xe3, 0xc4, 0x59, 0xe8, 0x29, 0x78, 0x89, 0x89, 0x45, 0x5e, 0x00, 0x15, 0x2a, 0xfa, - 0x92, 0x09, 0x80, 0x6c, 0xec, 0xb0, 0x35, 0xa9, 0x99, 0x0a, 0xa9, 0xe9, 0x1a, 0x18, 0xdf, 0x22, 0x25, 0xa9, 0x40, - 0x86, 0x50, 0x22, 0x85, 0xd0, 0x53, 0x8b, 0xab, 0x48, 0xc8, 0x5c, 0xd0, 0xf2, 0x04, 0x9d, 0x5c, 0xf3, 0xb4, 0x06, - 0x96, 0x23, 0xfa, 0x41, 0x85, 0x07, 0x53, 0xa2, 0xb2, 0x42, 0x51, 0x1e, 0xcd, 0xd6, 0xe9, 0xad, 0x4e, 0xe6, 0xea, - 0x69, 0x11, 0x8d, 0x12, 0x27, 0x42, 0x8b, 0xc4, 0x89, 0x70, 0x0a, 0xe9, 0x88, 0x59, 0x51, 0xc2, 0x4f, 0xcd, 0xd5, - 0xa8, 0x25, 0x2b, 0x6f, 0x3e, 0xe5, 0x07, 0xca, 0x3c, 0x87, 0x14, 0x4d, 0x9c, 0x68, 0x9e, 0x92, 0x38, 0xe2, 0xb8, - 0x9d, 0xb1, 0x6c, 0xdf, 0xab, 0x04, 0x1d, 0x05, 0xd8, 0xdf, 0xb8, 0xb3, 0x30, 0x66, 0x61, 0x9e, 0xe8, 0x56, 0xa7, - 0xfe, 0x54, 0xb0, 0xaf, 0xca, 0x21, 0x75, 0x72, 0xb2, 0x22, 0x71, 0xee, 0x4e, 0xb5, 0xfc, 0x65, 0x4e, 0xb3, 0xdb, - 0x33, 0x0a, 0xa9, 0xce, 0x29, 0x1c, 0xf8, 0xad, 0x96, 0xa1, 0xca, 0x53, 0x1f, 0xa4, 0x42, 0x59, 0x29, 0xea, 0xe7, - 0x00, 0x57, 0x4f, 0x09, 0x16, 0x22, 0xda, 0x68, 0x38, 0x62, 0xe4, 0x6e, 0xa1, 0x5b, 0xcf, 0x4f, 0xd2, 0x1e, 0x03, - 0xff, 0x5a, 0x85, 0x69, 0x15, 0x2c, 0xc0, 0x99, 0x79, 0x26, 0x75, 0x98, 0x8f, 0x56, 0xbd, 0x32, 0x50, 0x04, 0xe1, - 0xbb, 0x74, 0xfb, 0x54, 0x37, 0x25, 0xcd, 0x6e, 0x9f, 0x6a, 0x2d, 0xe8, 0x27, 0x12, 0x7e, 0xb0, 0x1a, 0xa7, 0x3c, - 0xc1, 0xcc, 0x8a, 0x02, 0x15, 0x00, 0xde, 0x5f, 0x7a, 0x8e, 0xf3, 0x17, 0x95, 0x32, 0xe8, 0x42, 0x2c, 0xf6, 0x2c, - 0x4e, 0x35, 0x13, 0xaf, 0xc6, 0xff, 0xcb, 0xda, 0xf8, 0x7f, 0x31, 0x4e, 0x9d, 0x82, 0x69, 0x34, 0x49, 0x68, 0xa8, - 0x59, 0x27, 0x92, 0x04, 0x28, 0xf4, 0xb6, 0x8c, 0x93, 0x8f, 0x17, 0x1e, 0x68, 0x5c, 0x8b, 0x71, 0x9a, 0xf0, 0xe6, - 0xd8, 0x9f, 0xb2, 0xf8, 0xd6, 0x9b, 0xb3, 0xe6, 0x34, 0x4d, 0xd2, 0x7c, 0xe6, 0x07, 0x14, 0xe7, 0xb7, 0x39, 0xa7, - 0xd3, 0xe6, 0x9c, 0xe1, 0xe7, 0x34, 0xbe, 0xa2, 0x9c, 0x05, 0x3e, 0xb6, 0x4f, 0x32, 0xe6, 0xc7, 0xd6, 0x6b, 0x3f, - 0xcb, 0xd2, 0x6b, 0x1b, 0xbf, 0x4b, 0x2f, 0x53, 0x9e, 0xe2, 0x37, 0x37, 0xb7, 0x13, 0x9a, 0xe0, 0x0f, 0x97, 0xf3, - 0x84, 0xcf, 0x71, 0xee, 0x27, 0x79, 0x33, 0xa7, 0x19, 0x1b, 0xf7, 0x82, 0x34, 0x4e, 0xb3, 0x26, 0x64, 0x6c, 0x4f, - 0xa9, 0x17, 0xb3, 0x49, 0xc4, 0xad, 0xd0, 0xcf, 0x3e, 0xf6, 0x9a, 0xcd, 0x59, 0xc6, 0xa6, 0x7e, 0x76, 0xdb, 0x14, - 0x35, 0xbc, 0x2f, 0xdb, 0xfb, 0xfe, 0xe3, 0xf1, 0x41, 0x8f, 0x67, 0x7e, 0x92, 0x33, 0x58, 0x26, 0xcf, 0x8f, 0x63, - 0x6b, 0xff, 0xb0, 0x3d, 0xcd, 0x77, 0x64, 0x20, 0xcf, 0x4f, 0x78, 0x71, 0x81, 0xdf, 0x03, 0xdc, 0xee, 0x25, 0x4f, - 0xf0, 0xe5, 0x9c, 0xf3, 0x34, 0x59, 0x04, 0xf3, 0x2c, 0x4f, 0x33, 0x6f, 0x96, 0xb2, 0x84, 0xd3, 0xac, 0x77, 0x99, - 0x66, 0x21, 0xcd, 0x9a, 0x99, 0x1f, 0xb2, 0x79, 0xee, 0x1d, 0xcc, 0x6e, 0x7a, 0xa0, 0x59, 0x4c, 0xb2, 0x74, 0x9e, - 0x84, 0x6a, 0x2c, 0x96, 0x44, 0x34, 0x63, 0xdc, 0x7c, 0x21, 0x2e, 0x32, 0xf1, 0x62, 0x96, 0x50, 0x3f, 0x6b, 0x4e, - 0xa0, 0x31, 0x98, 0x45, 0xed, 0x90, 0x4e, 0x70, 0x36, 0xb9, 0xf4, 0x9d, 0x4e, 0xf7, 0x11, 0xd6, 0xff, 0xbb, 0x87, - 0xc8, 0x6a, 0x6f, 0x2e, 0xee, 0xb4, 0xdb, 0x7f, 0x42, 0xbd, 0x95, 0x51, 0x04, 0x40, 0x5e, 0x67, 0x76, 0x63, 0xe5, - 0x29, 0x64, 0xb4, 0x6d, 0x6a, 0xd9, 0x9b, 0xf9, 0x21, 0xe4, 0x03, 0x7b, 0xdd, 0xd9, 0x4d, 0x01, 0xb3, 0xf3, 0x64, - 0x8a, 0xa9, 0x9a, 0xa4, 0x7a, 0x5a, 0xfc, 0x56, 0x88, 0x8f, 0x36, 0x43, 0xdc, 0xd5, 0x10, 0x57, 0x58, 0x6f, 0x86, - 0xf3, 0x4c, 0xc4, 0x56, 0xbd, 0x4e, 0x2e, 0x01, 0x89, 0xd2, 0x2b, 0x9a, 0x69, 0x38, 0xc4, 0xc3, 0x6f, 0x06, 0xa3, - 0xbb, 0x19, 0x8c, 0xa3, 0x4f, 0x81, 0x91, 0x25, 0xe1, 0xa2, 0xbe, 0xae, 0x9d, 0x8c, 0x4e, 0x7b, 0x11, 0x05, 0x7a, - 0xf2, 0xba, 0xf0, 0xfb, 0x9a, 0x85, 0x3c, 0x92, 0x3f, 0x05, 0x39, 0x5f, 0xcb, 0x77, 0x87, 0xed, 0xb6, 0x7c, 0xce, - 0xd9, 0xaf, 0xd4, 0xeb, 0xb8, 0x50, 0xa1, 0xb8, 0xc0, 0x3f, 0x94, 0xa7, 0x79, 0xeb, 0xdc, 0x13, 0xff, 0xc5, 0x3c, - 0xe6, 0x6b, 0xa4, 0x28, 0x56, 0x87, 0xa2, 0x71, 0xaa, 0x65, 0xa5, 0x14, 0x3e, 0xe0, 0xb6, 0x13, 0xdc, 0x91, 0xb0, - 0x7e, 0x79, 0x8c, 0x93, 0x0d, 0xfe, 0x22, 0xf3, 0x2e, 0x3c, 0x88, 0x74, 0x18, 0xa9, 0x86, 0x59, 0x2f, 0xed, 0x93, - 0x76, 0x2f, 0x6d, 0x36, 0x91, 0x93, 0x91, 0x64, 0x98, 0xaa, 0xe4, 0x3c, 0x87, 0x0d, 0xa4, 0xb1, 0x9d, 0x23, 0x2f, - 0x83, 0xb3, 0xa6, 0xcb, 0x65, 0x15, 0x06, 0x60, 0xe2, 0xb4, 0xc6, 0x0f, 0x5c, 0x55, 0xc0, 0xb9, 0xc1, 0xc9, 0x7d, - 0x7d, 0xbd, 0x4b, 0xa2, 0x79, 0x45, 0x9c, 0x06, 0x02, 0x73, 0xee, 0xcc, 0xe7, 0x11, 0x78, 0x29, 0x4a, 0xf1, 0x53, - 0xa5, 0x30, 0xd9, 0x2d, 0x1b, 0x0d, 0x92, 0x32, 0xbf, 0x0d, 0xf2, 0xf8, 0x92, 0x02, 0x7a, 0xb9, 0xe4, 0x04, 0x7a, - 0xac, 0xfa, 0xff, 0xc0, 0x0d, 0x49, 0x9d, 0xb8, 0x2c, 0x09, 0xe2, 0x79, 0x48, 0x73, 0xd1, 0x43, 0x25, 0xce, 0xe1, - 0x6e, 0x88, 0xb2, 0x96, 0x68, 0x02, 0xbd, 0x8b, 0x6c, 0x1e, 0xa8, 0x08, 0xb7, 0xa8, 0x94, 0xcf, 0x4d, 0xf1, 0x5c, - 0xb5, 0x7d, 0x5d, 0x25, 0x8b, 0x42, 0x4b, 0x77, 0x9e, 0xb0, 0x5f, 0xe6, 0xf4, 0x9c, 0x85, 0xc6, 0xc9, 0x5d, 0x9a, - 0x04, 0x69, 0x48, 0x3f, 0xbc, 0x7b, 0x01, 0xd9, 0xee, 0x69, 0x02, 0x24, 0x96, 0x48, 0x7f, 0x17, 0xce, 0x49, 0xe2, - 0x86, 0xf4, 0x8a, 0x05, 0x74, 0x70, 0xb1, 0xbb, 0xd8, 0x58, 0x51, 0xbe, 0x46, 0x45, 0xeb, 0x02, 0xfc, 0x77, 0x12, - 0xca, 0x8b, 0xdd, 0xc5, 0x25, 0x2f, 0x5a, 0xbb, 0x8b, 0xc4, 0x0d, 0xd3, 0xa9, 0xcf, 0x12, 0xf8, 0x9d, 0x17, 0xbb, - 0x0b, 0x06, 0x3f, 0x78, 0x71, 0x51, 0x54, 0x89, 0xa2, 0x25, 0x44, 0xc6, 0x14, 0x14, 0xee, 0x3a, 0xc8, 0xfd, 0x39, - 0x65, 0x89, 0x28, 0xba, 0xab, 0x67, 0xaa, 0x7b, 0x05, 0x24, 0xff, 0x4a, 0xa4, 0xc1, 0xac, 0xcd, 0xe5, 0xf3, 0xfb, - 0x9a, 0xcb, 0x34, 0xe1, 0x4c, 0xa4, 0xc5, 0xeb, 0x70, 0x4e, 0xe4, 0xe7, 0xe7, 0x81, 0x3c, 0x89, 0x9a, 0x57, 0xa7, - 0x2e, 0x7c, 0x81, 0x58, 0x69, 0x01, 0x53, 0x69, 0xec, 0xd3, 0xed, 0x47, 0x25, 0x93, 0xbb, 0x8c, 0xbf, 0x92, 0xaa, - 0xf2, 0x74, 0x9e, 0x05, 0x10, 0xeb, 0x55, 0x2a, 0xc5, 0xba, 0x57, 0xcc, 0x16, 0xfa, 0x9b, 0x8d, 0xb9, 0x91, 0x64, - 0xcb, 0xe1, 0x4c, 0x5f, 0x75, 0x6d, 0x07, 0x15, 0xf1, 0x44, 0x58, 0x33, 0x26, 0x56, 0xef, 0x9c, 0x85, 0x10, 0x78, - 0x61, 0xa1, 0x4a, 0x58, 0xac, 0x4d, 0x12, 0x54, 0xa4, 0x50, 0x64, 0x90, 0xc2, 0x65, 0x3b, 0x59, 0xb5, 0x0a, 0x84, - 0x10, 0x19, 0xd7, 0x03, 0xe1, 0xdb, 0xec, 0xec, 0xed, 0xe5, 0xd5, 0x89, 0x36, 0xa6, 0x70, 0xbe, 0x5c, 0x72, 0xea, - 0xe4, 0xf2, 0xd4, 0x4d, 0x44, 0x40, 0x19, 0x63, 0x58, 0xbe, 0xf1, 0x32, 0x5c, 0xf6, 0xe4, 0xe5, 0x45, 0x2f, 0x12, - 0x48, 0x94, 0x28, 0x23, 0x1a, 0xa9, 0x27, 0x5a, 0x25, 0xc3, 0xe6, 0xeb, 0xf2, 0x20, 0x7f, 0x0d, 0xeb, 0xed, 0x95, - 0xc5, 0x91, 0x56, 0x55, 0xb4, 0x5a, 0x9a, 0xa7, 0x19, 0x77, 0x1c, 0x1f, 0x07, 0x88, 0xf4, 0x7d, 0x31, 0xfb, 0x63, - 0x99, 0xef, 0x31, 0x68, 0x76, 0xbc, 0x4e, 0xe9, 0x0f, 0xa9, 0x9d, 0xaf, 0x96, 0xd9, 0x66, 0xea, 0x8c, 0x2e, 0xe0, - 0x09, 0x97, 0xbf, 0x15, 0xfa, 0xaa, 0x02, 0x39, 0xbb, 0xea, 0xb9, 0x9c, 0x24, 0x56, 0x0c, 0x4d, 0x2a, 0x03, 0x4e, - 0x0d, 0xaa, 0x61, 0x3a, 0xc2, 0x6c, 0xcb, 0xd8, 0xa8, 0xa8, 0x10, 0x51, 0x6e, 0xee, 0x0b, 0xa9, 0x04, 0x9d, 0x1b, - 0xd4, 0x7d, 0xc1, 0xb4, 0x1b, 0xaf, 0x4e, 0x77, 0x85, 0x42, 0x91, 0xc1, 0x19, 0x36, 0x55, 0x93, 0xb0, 0xdc, 0x92, - 0x64, 0x23, 0xf1, 0xba, 0xf2, 0x91, 0x66, 0xa4, 0x0a, 0x28, 0xae, 0x75, 0x00, 0xc9, 0x90, 0x9b, 0x00, 0x03, 0xc7, - 0x40, 0xce, 0xf5, 0x14, 0x80, 0xc7, 0x8c, 0x29, 0x9c, 0x54, 0x52, 0x1c, 0x07, 0x2f, 0xa4, 0x76, 0xef, 0xd9, 0x6f, - 0xdf, 0x9c, 0xbd, 0xb7, 0x31, 0x5c, 0x75, 0x46, 0xb3, 0xdc, 0x5b, 0xd8, 0x2a, 0xc7, 0xb0, 0x09, 0xf1, 0x6a, 0xdb, - 0xb3, 0xfd, 0x19, 0x1c, 0xda, 0x16, 0x4c, 0xb5, 0x75, 0xd3, 0xbc, 0xbe, 0xbe, 0x6e, 0xc2, 0x89, 0xb2, 0xe6, 0x3c, - 0x8b, 0x25, 0xbb, 0x09, 0xed, 0xa2, 0x40, 0x2e, 0x8f, 0x68, 0x52, 0x5e, 0x86, 0x94, 0xc6, 0xd4, 0x8d, 0xd3, 0x89, - 0x3c, 0x0f, 0xbb, 0xea, 0x9e, 0x88, 0x2f, 0x8e, 0xc5, 0x25, 0x5f, 0xfd, 0x63, 0x2e, 0xaf, 0x57, 0xe3, 0x19, 0xfc, - 0xec, 0x43, 0xf0, 0xea, 0xb8, 0xc5, 0x23, 0xf1, 0x70, 0x06, 0xbb, 0x49, 0x3c, 0xed, 0x2e, 0xd6, 0xa8, 0x6e, 0x00, - 0x5d, 0x44, 0x7d, 0x39, 0xb5, 0x5c, 0xd4, 0xba, 0xf0, 0xe2, 0x8b, 0x8b, 0xe2, 0xb8, 0x05, 0x7d, 0xb5, 0x74, 0xbf, - 0x97, 0x69, 0x78, 0xab, 0xdb, 0x97, 0x94, 0x08, 0x97, 0x3d, 0x25, 0xa4, 0x0f, 0x5d, 0xc0, 0xb8, 0x61, 0x5f, 0xe0, - 0x4c, 0xb1, 0xd0, 0x61, 0xf5, 0x50, 0x8c, 0x2c, 0x60, 0x98, 0x05, 0x94, 0x00, 0xb9, 0x41, 0xe7, 0x61, 0xd9, 0x40, - 0xec, 0x76, 0x59, 0xb4, 0x0d, 0x40, 0x59, 0xb1, 0xda, 0x3f, 0xd2, 0xcd, 0x5d, 0x91, 0x85, 0x86, 0x38, 0x34, 0x81, - 0xbf, 0x40, 0xf0, 0xaf, 0x00, 0xfc, 0xb8, 0x25, 0xd1, 0x74, 0x61, 0x5e, 0x3b, 0x23, 0x2f, 0x84, 0x28, 0x91, 0x39, - 0xcc, 0x38, 0x7e, 0xcf, 0xf1, 0xc7, 0x0b, 0x51, 0x55, 0x6b, 0x09, 0xa0, 0xbe, 0x82, 0x36, 0xd5, 0xd6, 0xea, 0x60, - 0x90, 0xc6, 0xb1, 0x3f, 0xcb, 0xa9, 0xa7, 0x7f, 0x28, 0x85, 0x01, 0xf4, 0x8e, 0x75, 0x0d, 0x4d, 0xe5, 0x3d, 0x9d, - 0x82, 0x1e, 0xb7, 0xae, 0x3e, 0x5e, 0xf9, 0x99, 0xd3, 0x6c, 0x06, 0xcd, 0xcb, 0x09, 0x2a, 0x78, 0xb4, 0x30, 0xd5, - 0x8d, 0x87, 0xed, 0x76, 0x0f, 0x92, 0x54, 0x9b, 0x7e, 0xcc, 0x26, 0x89, 0x17, 0xd3, 0x31, 0x2f, 0x38, 0x9c, 0x1e, - 0x5c, 0x68, 0xfd, 0xce, 0xed, 0x1e, 0x66, 0x74, 0x6a, 0xb9, 0xf0, 0xf7, 0xee, 0x81, 0x0b, 0x1e, 0x7a, 0x09, 0x8f, - 0x9a, 0x22, 0x19, 0x1a, 0x8e, 0x72, 0xf0, 0xa8, 0xf6, 0xbc, 0x30, 0x06, 0x0a, 0x28, 0xe8, 0xbe, 0x05, 0xcf, 0x2c, - 0x1e, 0x61, 0x9e, 0x99, 0xf5, 0x12, 0xb4, 0x58, 0x9b, 0xc1, 0xba, 0x0a, 0xb6, 0x8f, 0x8a, 0x5c, 0x58, 0x2c, 0x8b, - 0x35, 0xbc, 0x18, 0xaa, 0x74, 0xc1, 0x92, 0xd9, 0x9c, 0x0f, 0x85, 0xe7, 0x3f, 0x83, 0x33, 0x24, 0x23, 0x6c, 0x94, - 0x00, 0x3c, 0x23, 0xd5, 0x3e, 0xf0, 0xe3, 0xc0, 0x81, 0x4e, 0xac, 0xa6, 0x75, 0x94, 0xd1, 0x29, 0xea, 0x4d, 0x59, - 0xd2, 0x94, 0xef, 0x0e, 0x0d, 0xdd, 0xcd, 0x7d, 0x04, 0x4f, 0x85, 0x2b, 0x7a, 0xc3, 0x22, 0xc1, 0x77, 0xc3, 0xbc, - 0x2e, 0x46, 0x45, 0xd1, 0x4b, 0xb9, 0x33, 0x7c, 0xe1, 0xa0, 0x11, 0xfe, 0xd5, 0xb8, 0xc4, 0xc6, 0xd6, 0x54, 0x6d, - 0xe3, 0x2e, 0xda, 0x52, 0xc5, 0xa4, 0x4b, 0x51, 0xed, 0x57, 0x02, 0x15, 0x5f, 0x3a, 0x36, 0xcd, 0x67, 0x4d, 0xc9, - 0x7e, 0x9a, 0x82, 0x7c, 0x6c, 0x68, 0x8a, 0x94, 0x3b, 0x9b, 0xd2, 0x85, 0xe0, 0x2c, 0xea, 0x1c, 0x8b, 0xf4, 0xb8, - 0x8c, 0xca, 0x73, 0x4f, 0xea, 0xd9, 0x3c, 0xe9, 0x84, 0x6a, 0x5b, 0xff, 0xe2, 0xa4, 0xce, 0xa6, 0x40, 0xfe, 0x97, - 0x77, 0xfd, 0xf9, 0x71, 0x0c, 0x03, 0x5e, 0x68, 0xa5, 0xc1, 0xbc, 0x1a, 0x65, 0xc8, 0x47, 0x0e, 0x2a, 0xd4, 0x9e, - 0x79, 0x22, 0xf4, 0x6e, 0xe3, 0x82, 0xc1, 0x1d, 0xae, 0x23, 0x6a, 0xf2, 0x04, 0x33, 0x83, 0x9c, 0x80, 0x5a, 0xee, - 0x78, 0xaf, 0x62, 0x33, 0x52, 0x6b, 0xb7, 0xc4, 0x84, 0x88, 0x9d, 0x25, 0xa1, 0x6d, 0xfd, 0x39, 0x88, 0x59, 0xf0, - 0x91, 0xd8, 0xbb, 0x0b, 0x07, 0xad, 0x1f, 0x0d, 0x15, 0x3b, 0x54, 0xf3, 0x5c, 0x54, 0x8f, 0x36, 0xa4, 0xae, 0xc1, - 0x4e, 0xe5, 0xed, 0x41, 0x76, 0x1f, 0x54, 0x9b, 0xe3, 0x96, 0x1c, 0xa7, 0x7f, 0x51, 0x9c, 0x57, 0xb7, 0x82, 0x55, - 0x50, 0x00, 0x9a, 0x65, 0xb9, 0x25, 0xe8, 0x8f, 0xd8, 0x72, 0x0b, 0xd5, 0x2c, 0x40, 0x6c, 0xd2, 0x3e, 0xb2, 0x2d, - 0xc9, 0x60, 0x00, 0x4e, 0xae, 0x78, 0x8d, 0x6d, 0xfd, 0xb9, 0x2c, 0xa3, 0xa5, 0xdb, 0x47, 0xe4, 0xad, 0x10, 0x1b, - 0xc6, 0x02, 0x5b, 0xdf, 0x0d, 0x29, 0xf7, 0x59, 0x2c, 0x9b, 0xf4, 0xb4, 0x97, 0x62, 0x65, 0x46, 0xcb, 0x65, 0x5e, - 0x9f, 0x0b, 0xab, 0x63, 0x50, 0xcc, 0xec, 0xb8, 0x55, 0xc1, 0x2d, 0x66, 0x26, 0xf6, 0x87, 0x19, 0x3f, 0xad, 0x66, - 0x28, 0xdf, 0x59, 0x7f, 0x0e, 0xc4, 0xc9, 0x2a, 0x00, 0x30, 0x53, 0x00, 0x42, 0x64, 0x5f, 0x2a, 0x21, 0x8e, 0x4f, - 0x32, 0x97, 0xfb, 0xd9, 0x84, 0xf2, 0x15, 0xc4, 0xfa, 0x32, 0x91, 0xb7, 0xa7, 0xa3, 0xf8, 0x6b, 0xd0, 0x06, 0x75, - 0x68, 0x41, 0xcf, 0x2d, 0x06, 0xa0, 0xaa, 0x92, 0x8d, 0x1a, 0x6f, 0x84, 0x40, 0xf6, 0x89, 0xc5, 0x91, 0xdc, 0x3e, - 0x13, 0xdc, 0x5e, 0xc6, 0xe1, 0x2c, 0x31, 0x96, 0x00, 0xb1, 0xb0, 0xad, 0x81, 0x84, 0x9c, 0x86, 0x12, 0x66, 0x92, - 0x8a, 0x56, 0x59, 0x71, 0xdc, 0x92, 0xb5, 0x25, 0x3b, 0x96, 0x95, 0x00, 0x09, 0x62, 0x9f, 0x56, 0x38, 0x80, 0xe4, - 0x6f, 0x13, 0x0f, 0x21, 0xbb, 0x2a, 0x89, 0x4d, 0x9c, 0x31, 0xeb, 0x1f, 0xc7, 0xfe, 0x25, 0x8d, 0xfb, 0xbb, 0x8b, - 0x74, 0xb9, 0x6c, 0x17, 0xc7, 0x2d, 0xf9, 0x68, 0x1d, 0x0b, 0xbe, 0x21, 0xef, 0x06, 0x15, 0x4b, 0x0c, 0x07, 0x37, - 0x21, 0x25, 0x56, 0xe7, 0x82, 0x79, 0xaa, 0x83, 0xc2, 0xb6, 0x44, 0x16, 0x8a, 0xa8, 0x54, 0xea, 0x34, 0x85, 0x6d, - 0xb1, 0x70, 0xbd, 0x2c, 0xe7, 0x74, 0x06, 0xa5, 0xd1, 0x72, 0xd9, 0x29, 0x6c, 0x6b, 0xca, 0x12, 0x78, 0x4a, 0x97, - 0x4b, 0x71, 0x26, 0x72, 0xca, 0x12, 0xa7, 0x0d, 0x64, 0x6b, 0x5b, 0x53, 0xff, 0x46, 0x4c, 0x58, 0xbf, 0xf1, 0x6f, - 0x9c, 0x8e, 0x7a, 0xe5, 0x96, 0xf8, 0xc9, 0x81, 0xe2, 0xaa, 0x15, 0xf5, 0xd5, 0x8a, 0x86, 0x78, 0x2e, 0x4f, 0x7b, - 0x11, 0x27, 0x24, 0xfe, 0xe6, 0x15, 0x0d, 0xf5, 0x8a, 0xce, 0xb7, 0xac, 0xe8, 0xfc, 0x8e, 0x15, 0x0d, 0xd4, 0xea, - 0x59, 0x25, 0xee, 0xb2, 0xe5, 0xb2, 0xd3, 0xae, 0xb0, 0x77, 0xdc, 0x0a, 0xd9, 0x15, 0xac, 0x06, 0x68, 0x6a, 0x9c, - 0x4d, 0xe9, 0x66, 0xa2, 0xac, 0xa3, 0x98, 0x7e, 0x16, 0x26, 0x2b, 0x2c, 0xa4, 0x75, 0x2c, 0x98, 0x74, 0x5d, 0x06, - 0x26, 0xff, 0x48, 0xca, 0x66, 0x80, 0x87, 0x1c, 0xf0, 0x10, 0xe9, 0xbb, 0x42, 0x1d, 0xfb, 0xbd, 0x8d, 0x6d, 0xcb, - 0xd6, 0x64, 0x7d, 0x51, 0x9c, 0x83, 0x8c, 0x10, 0xf3, 0xbb, 0x17, 0x2d, 0x42, 0x6d, 0xbb, 0xbf, 0x9d, 0xe6, 0x20, - 0x87, 0xe0, 0x3a, 0xcd, 0x42, 0xdb, 0x93, 0x55, 0x3f, 0x0b, 0x55, 0x53, 0x96, 0xa8, 0x8c, 0xb4, 0xad, 0xb4, 0x56, - 0xbd, 0x37, 0x29, 0xae, 0x7b, 0x78, 0x28, 0x6b, 0xcc, 0x7c, 0xce, 0x69, 0x96, 0x28, 0xca, 0xb5, 0xed, 0xff, 0x10, - 0x54, 0xb8, 0x81, 0xaf, 0x04, 0x7a, 0x01, 0x34, 0x01, 0x2a, 0x9d, 0x5b, 0xf1, 0x7c, 0x29, 0x9e, 0x76, 0x2a, 0x65, - 0xf3, 0x16, 0x99, 0x7a, 0xbf, 0x2c, 0x02, 0x33, 0x64, 0x3e, 0xa5, 0xe1, 0xb9, 0x60, 0xd0, 0x83, 0xf8, 0x42, 0x29, - 0x8f, 0x2b, 0xe2, 0xae, 0x6a, 0x80, 0xed, 0x9f, 0xe6, 0xdd, 0x47, 0x07, 0xa7, 0x36, 0x96, 0x3c, 0x3e, 0x1d, 0x8f, - 0x6d, 0x54, 0x58, 0xf7, 0x6b, 0xd6, 0x39, 0xf8, 0x69, 0xfe, 0xf5, 0xb3, 0xf6, 0xd7, 0x65, 0xe3, 0x04, 0x88, 0x48, - 0x25, 0x41, 0x68, 0x51, 0x65, 0xc0, 0xab, 0x67, 0x34, 0xf6, 0x93, 0xed, 0xd3, 0x19, 0x9a, 0xd3, 0xc9, 0x67, 0x94, - 0x86, 0x40, 0x9c, 0x78, 0xad, 0xf4, 0x3c, 0xa6, 0x57, 0x54, 0xdf, 0xd0, 0xb8, 0x61, 0xb0, 0x0d, 0x2d, 0x82, 0x74, - 0x9e, 0x70, 0x95, 0x0d, 0xa2, 0x58, 0xad, 0x31, 0xa5, 0x0b, 0x31, 0x07, 0x53, 0x9d, 0xbf, 0x95, 0x72, 0xae, 0x2e, - 0xbd, 0x8a, 0x0b, 0x6c, 0x1b, 0x00, 0x6c, 0x85, 0x6c, 0xb0, 0xa5, 0xdc, 0x6b, 0xe3, 0xf6, 0x36, 0xd8, 0x70, 0x07, - 0x79, 0xb6, 0x3d, 0xd2, 0x78, 0x12, 0x0e, 0xdd, 0xda, 0xa5, 0x1a, 0x5b, 0xf1, 0xf5, 0x49, 0x0c, 0x5c, 0x66, 0xd0, - 0x59, 0x42, 0xf3, 0x7c, 0x2b, 0x02, 0xca, 0x45, 0xc4, 0x76, 0x55, 0xdb, 0xde, 0xd2, 0x0b, 0x6e, 0x63, 0xd8, 0x61, - 0x02, 0xe0, 0x32, 0xac, 0xac, 0x6a, 0xd1, 0xf1, 0x98, 0x06, 0xa5, 0x3f, 0x1c, 0x02, 0x84, 0x63, 0x16, 0x73, 0x88, - 0x93, 0x89, 0x00, 0x96, 0xfd, 0x3a, 0x4d, 0xa8, 0x8d, 0x74, 0xca, 0xab, 0x82, 0x5f, 0xc9, 0xff, 0xcd, 0xf0, 0xc8, - 0x1e, 0xeb, 0xb0, 0xa8, 0x51, 0x96, 0x4b, 0xed, 0xae, 0xa9, 0x95, 0xd7, 0x11, 0x99, 0x0a, 0x7f, 0xcc, 0xb6, 0x0d, - 0x74, 0xbf, 0x6d, 0xb2, 0xe8, 0x7c, 0x7d, 0xd8, 0x69, 0x17, 0x36, 0xb6, 0xa1, 0xbb, 0xfb, 0xee, 0x12, 0xd1, 0x6a, - 0x1f, 0x5a, 0xcd, 0x93, 0xcf, 0x69, 0xd7, 0xed, 0x3c, 0xee, 0xd8, 0x58, 0xde, 0xb5, 0x80, 0x8a, 0x92, 0x19, 0x04, - 0xe0, 0x21, 0xfe, 0xdd, 0x53, 0xa9, 0x77, 0x7e, 0x3f, 0x78, 0x1e, 0x76, 0xda, 0x36, 0xb6, 0x73, 0x9e, 0xce, 0x3e, - 0x63, 0x0a, 0xfb, 0x36, 0xb6, 0x83, 0x38, 0xcd, 0xa9, 0x39, 0x07, 0xa9, 0xce, 0xfe, 0xfe, 0x49, 0x48, 0x88, 0x66, - 0x19, 0xcd, 0x73, 0xcb, 0xec, 0x5f, 0x91, 0xd2, 0x27, 0x18, 0xe6, 0x46, 0x8a, 0xcb, 0x29, 0x17, 0x78, 0x91, 0xd7, - 0x20, 0x98, 0x54, 0x25, 0xcb, 0xd6, 0x88, 0x4d, 0x88, 0x80, 0x92, 0xb1, 0x49, 0xed, 0xea, 0x93, 0x23, 0x6f, 0xd8, - 0x7a, 0x72, 0x60, 0x19, 0x38, 0x5f, 0x1f, 0xa0, 0x56, 0x32, 0x65, 0xc9, 0xf9, 0x86, 0x52, 0xff, 0x66, 0x43, 0x29, - 0xa8, 0x6c, 0x25, 0x74, 0xea, 0x8a, 0x9e, 0x4f, 0x63, 0xbd, 0x52, 0x7c, 0x4c, 0x10, 0x43, 0xe1, 0x7f, 0xfc, 0x04, - 0xa4, 0xc6, 0x32, 0x88, 0x1e, 0x7e, 0xfb, 0x70, 0x50, 0xf2, 0x39, 0xc3, 0x95, 0xbd, 0xfc, 0xbe, 0x19, 0x42, 0x69, - 0x13, 0x9c, 0xfc, 0xf1, 0x67, 0xcd, 0x95, 0xde, 0x7c, 0x9a, 0xe0, 0x0c, 0xad, 0xea, 0x77, 0x2c, 0xbd, 0x3a, 0xea, - 0xbf, 0xba, 0xf6, 0x1b, 0x8a, 0x95, 0xe2, 0x53, 0xae, 0x7f, 0x10, 0xb3, 0x69, 0x45, 0x02, 0xeb, 0x60, 0x0a, 0x8d, - 0x07, 0x32, 0xbe, 0xcc, 0x4e, 0xa4, 0xea, 0x73, 0x0e, 0xe7, 0x58, 0xe1, 0xaa, 0x90, 0x79, 0x46, 0xcf, 0xe3, 0xf4, - 0x7a, 0xf5, 0xf2, 0xb3, 0xed, 0x95, 0x23, 0x36, 0x89, 0x8c, 0xc3, 0x69, 0x94, 0x94, 0x8b, 0x70, 0xe7, 0x00, 0xc5, - 0xbf, 0xfc, 0xb3, 0xeb, 0xfe, 0xcb, 0x3f, 0x7f, 0xb2, 0x2a, 0x74, 0x5f, 0x5c, 0x60, 0x5e, 0x75, 0xbb, 0x7d, 0x77, - 0x6d, 0x1e, 0xa9, 0x8e, 0xf3, 0xcd, 0x75, 0xd6, 0x16, 0x01, 0xde, 0xaf, 0x2d, 0xc1, 0x5a, 0xa1, 0xdc, 0x7d, 0xd6, - 0x6f, 0x01, 0x0c, 0xe6, 0xf5, 0x49, 0xc8, 0xa0, 0xd2, 0xef, 0x02, 0xed, 0x02, 0x79, 0xf7, 0x5a, 0x91, 0xdf, 0x8e, - 0xe1, 0x4f, 0xcd, 0xe1, 0x77, 0x82, 0xaf, 0xfc, 0x13, 0xf1, 0xc5, 0x45, 0x99, 0x85, 0x68, 0x36, 0x85, 0x3b, 0x0e, - 0x06, 0x6b, 0x25, 0x4a, 0xf1, 0xf0, 0xda, 0xa8, 0x2f, 0xce, 0x50, 0x92, 0xf8, 0xe2, 0x15, 0x5c, 0x6c, 0x74, 0x7c, - 0x99, 0x69, 0x67, 0xeb, 0x1d, 0xc2, 0x01, 0xba, 0xa8, 0xcf, 0x4a, 0x74, 0xba, 0x26, 0x19, 0xa0, 0x14, 0xcc, 0x0d, - 0x00, 0x13, 0xc7, 0x17, 0xca, 0xda, 0x3c, 0x95, 0x6e, 0x18, 0x6f, 0x95, 0xb4, 0x95, 0x7b, 0xa6, 0x86, 0x74, 0x6c, - 0xbd, 0x17, 0xf8, 0x12, 0x95, 0x69, 0x65, 0xdd, 0x0b, 0x57, 0x17, 0xd8, 0x11, 0x25, 0xfb, 0xb9, 0xf2, 0xe3, 0xab, - 0xfb, 0x31, 0xbe, 0xed, 0x02, 0x75, 0x69, 0x2d, 0xff, 0xd1, 0x2a, 0xc1, 0xb2, 0xb9, 0xdc, 0xa4, 0x0f, 0x5c, 0xfb, - 0x9c, 0x66, 0xe7, 0x11, 0x24, 0x42, 0x65, 0x9f, 0x60, 0x4e, 0xb0, 0xd2, 0x98, 0x8a, 0xbf, 0x8c, 0xa8, 0x8b, 0xa4, - 0xff, 0x41, 0x9c, 0x8a, 0x41, 0x16, 0x23, 0x0c, 0x65, 0x2c, 0xc2, 0xff, 0xe7, 0x5b, 0xff, 0x61, 0xf8, 0xd6, 0xdd, - 0x43, 0xd4, 0xce, 0x48, 0x7f, 0xf6, 0x42, 0xfe, 0xc7, 0x66, 0x77, 0xb9, 0x60, 0x77, 0xbf, 0x81, 0xd1, 0xe5, 0xff, - 0x18, 0x46, 0x27, 0x6c, 0x64, 0xcd, 0xe9, 0xd6, 0x42, 0xcd, 0xb7, 0xae, 0x7f, 0xed, 0xdf, 0x56, 0xfb, 0x2a, 0xbe, - 0x38, 0xb9, 0xf6, 0x6f, 0xab, 0x45, 0xd8, 0xce, 0x2e, 0x56, 0xfb, 0x18, 0xd8, 0x6f, 0x5e, 0xdb, 0x9e, 0xfd, 0xe6, - 0xeb, 0xaf, 0x6d, 0x7c, 0x91, 0x53, 0x3e, 0x80, 0x42, 0xb2, 0xbb, 0xd8, 0x59, 0xad, 0x08, 0x6e, 0x14, 0x98, 0xa2, - 0x08, 0x7b, 0xe1, 0xac, 0x06, 0x0c, 0xcb, 0xcf, 0xd3, 0xc4, 0x84, 0xe6, 0x2d, 0x58, 0xf6, 0x9f, 0x0b, 0x8e, 0xe8, - 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, 0xac, 0x41, 0x54, 0x5d, 0x19, 0x74, 0x37, 0xff, 0xaf, 0xae, 0x45, - 0x90, 0xb7, 0x7d, 0x44, 0x83, 0xf8, 0xe2, 0x73, 0xc4, 0x87, 0x1c, 0xac, 0xf2, 0xd8, 0x69, 0x77, 0xa7, 0x5f, 0xec, - 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, 0xa7, 0xa9, 0xd8, 0x24, 0x5c, 0x72, 0xf8, 0x93, 0xc2, 0x9f, 0xac, - 0x62, 0xd4, 0x2c, 0x19, 0x67, 0x7e, 0x46, 0xc3, 0xed, 0x4c, 0xba, 0xbc, 0xdf, 0x48, 0x91, 0x86, 0x4c, 0xc0, 0xce, - 0xcf, 0x45, 0xea, 0xd1, 0x94, 0x81, 0x3e, 0xba, 0x63, 0x7e, 0xc5, 0x47, 0x5d, 0x88, 0x56, 0x7e, 0x04, 0xc0, 0x44, - 0x38, 0x25, 0x79, 0x99, 0xeb, 0x00, 0xb7, 0x6a, 0xaa, 0xec, 0x10, 0x6c, 0x23, 0xe1, 0x75, 0x0f, 0x49, 0x5f, 0xa4, - 0x3d, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, 0x53, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, 0x49, 0x7f, - 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0x2e, 0xe2, 0x12, 0x9f, 0xee, 0x81, 0x43, 0x92, 0x4f, 0xe6, 0xe3, 0x31, - 0xcd, 0x1c, 0x7d, 0x00, 0xf0, 0xab, 0x03, 0x38, 0x63, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, 0x6b, 0x27, - 0x43, 0xbd, 0xb4, 0xba, 0x9c, 0x31, 0xc4, 0x73, 0x44, 0xfa, 0x11, 0x24, 0xc6, 0xbf, 0x48, 0xf8, 0x7e, 0xd7, 0x99, - 0x7f, 0x75, 0x80, 0x43, 0xb8, 0xf2, 0x42, 0x67, 0x75, 0xcb, 0xbb, 0x4a, 0x3e, 0xb0, 0x84, 0x1f, 0xc9, 0x63, 0x98, - 0x19, 0x52, 0xee, 0xc3, 0x32, 0x23, 0xc6, 0xf2, 0xcb, 0x0e, 0x43, 0xd2, 0x0f, 0x1a, 0x44, 0x1e, 0xca, 0x14, 0xb7, - 0xec, 0x9e, 0x46, 0x7e, 0x76, 0x0a, 0x07, 0xbe, 0x01, 0xd0, 0x4b, 0x9e, 0xfa, 0x4e, 0x50, 0x7e, 0xc9, 0xc9, 0x69, - 0xfd, 0xd4, 0x68, 0x4d, 0xb0, 0x48, 0x8a, 0xa9, 0x8a, 0x5a, 0x50, 0x74, 0x6e, 0x16, 0x91, 0xc6, 0x6e, 0x0b, 0xc3, - 0x1e, 0xec, 0x6d, 0xf4, 0xd1, 0xea, 0xa5, 0x6b, 0x5e, 0x67, 0xfe, 0xac, 0x8c, 0x1b, 0x9c, 0xfa, 0x59, 0xc6, 0x68, - 0x66, 0x39, 0xcf, 0x7f, 0x45, 0xde, 0xbf, 0xfc, 0xf3, 0xe6, 0xf8, 0x81, 0x0a, 0x19, 0x58, 0x90, 0x5c, 0xd2, 0x14, - 0xe9, 0xd8, 0xc4, 0x0e, 0x64, 0x43, 0x5b, 0x87, 0x3b, 0xf6, 0x8f, 0xda, 0xed, 0xb6, 0x0a, 0x09, 0x74, 0xe4, 0x4f, - 0x88, 0x01, 0xc0, 0x4f, 0x78, 0x10, 0x51, 0x65, 0x62, 0xcb, 0x00, 0xe5, 0x51, 0x7b, 0x76, 0x63, 0xf7, 0x61, 0x3b, - 0x28, 0x28, 0xde, 0xd1, 0x19, 0xf5, 0xf9, 0x67, 0x8d, 0x9f, 0x89, 0x26, 0xe5, 0xf0, 0x1d, 0x3d, 0x74, 0x35, 0xee, - 0xca, 0xa0, 0x87, 0xab, 0x83, 0xbe, 0x67, 0x53, 0x71, 0x75, 0xd3, 0xb6, 0x51, 0x85, 0xa7, 0xba, 0x36, 0x26, 0x97, - 0x2d, 0x6c, 0x4b, 0x60, 0x3c, 0x4a, 0xe3, 0x90, 0x66, 0xc4, 0xa6, 0xee, 0xc4, 0xb5, 0x1e, 0xb7, 0xdb, 0x6d, 0xdc, - 0x3c, 0x38, 0x6c, 0xb7, 0xf1, 0xe1, 0xc3, 0x36, 0x6e, 0xc2, 0x1f, 0xd7, 0x75, 0x57, 0x60, 0xb8, 0x2b, 0x6a, 0xdb, - 0x69, 0x67, 0x74, 0xaa, 0x00, 0xbc, 0x33, 0xac, 0x58, 0xed, 0x09, 0xb8, 0x60, 0x5a, 0xed, 0x7b, 0x29, 0xd9, 0xd4, - 0x05, 0x07, 0x2a, 0x1d, 0x55, 0xf8, 0x0b, 0xb3, 0x2a, 0x68, 0x4a, 0xe5, 0xc5, 0x7f, 0x2f, 0x14, 0x21, 0x78, 0xd6, - 0x29, 0xdc, 0x5e, 0x2a, 0xe2, 0xa5, 0x90, 0x0a, 0x04, 0x1f, 0x48, 0xe3, 0x3e, 0x4b, 0xe0, 0xdb, 0x59, 0x3a, 0x6a, - 0xaa, 0x19, 0x55, 0xb6, 0x92, 0x74, 0xfb, 0x40, 0x86, 0xa5, 0x37, 0x11, 0xc4, 0xe8, 0x01, 0xc2, 0xfe, 0x7d, 0x1a, - 0xa8, 0x15, 0x84, 0xfa, 0xc1, 0x7d, 0xea, 0x6b, 0xec, 0x8f, 0x1e, 0x88, 0xe4, 0xa4, 0x9d, 0x68, 0xb9, 0xdc, 0xf1, - 0x97, 0xcb, 0x9d, 0xe0, 0xfe, 0x33, 0x94, 0xcb, 0xab, 0x4f, 0x41, 0xc0, 0xcd, 0x9f, 0x12, 0xe8, 0x17, 0x50, 0xee, - 0x45, 0x58, 0x82, 0x24, 0x9f, 0x7c, 0xac, 0x06, 0x94, 0x8f, 0x41, 0xb1, 0x82, 0x94, 0x90, 0x44, 0xd2, 0x3e, 0x5f, - 0x2e, 0x15, 0xf1, 0xe3, 0x39, 0xf1, 0xcb, 0xa2, 0x8e, 0x8d, 0x67, 0x24, 0x28, 0x1f, 0x6d, 0x01, 0xf2, 0x4c, 0x71, - 0xa9, 0x0a, 0xe2, 0x6b, 0x3f, 0x4b, 0x4c, 0x80, 0x5f, 0xa7, 0x96, 0x1a, 0xd6, 0x9a, 0x65, 0xe9, 0x15, 0x83, 0xe4, - 0x97, 0x95, 0x81, 0xa7, 0x04, 0x2e, 0xfe, 0xea, 0x99, 0xa1, 0x70, 0xa3, 0x83, 0xf7, 0x9a, 0xcf, 0xc2, 0x2d, 0x93, - 0xe5, 0x04, 0xbd, 0x50, 0xcd, 0xcd, 0x9b, 0xeb, 0x69, 0xbd, 0xf3, 0xaf, 0xbd, 0x99, 0x7e, 0x78, 0x26, 0xf3, 0x6c, - 0xbc, 0x69, 0x79, 0xb2, 0xe6, 0x2d, 0x79, 0x0d, 0xb1, 0x1f, 0x5b, 0xf3, 0x6d, 0xb8, 0x67, 0x53, 0xf2, 0xb8, 0x77, - 0x2f, 0xcf, 0xa8, 0x9f, 0x05, 0xd1, 0x5b, 0x3f, 0xf3, 0xa7, 0x79, 0x6f, 0xac, 0x6f, 0xf1, 0xd2, 0x14, 0x70, 0x3e, - 0x16, 0x99, 0x4e, 0x49, 0x70, 0x6b, 0xe3, 0x10, 0xe1, 0xea, 0xbd, 0x84, 0x40, 0xfa, 0xb9, 0x6d, 0x3c, 0x37, 0x5f, - 0xc1, 0x3a, 0xdb, 0x78, 0x8a, 0xb0, 0x4c, 0x20, 0x7a, 0xfb, 0x47, 0xa6, 0x0e, 0x61, 0xc8, 0x75, 0xf1, 0xc6, 0x6e, - 0xf5, 0x95, 0x3b, 0x9d, 0x4c, 0xf4, 0x7e, 0x25, 0x99, 0x68, 0x03, 0x1a, 0xad, 0x8c, 0xe6, 0xb3, 0x34, 0xc9, 0xa9, - 0x8d, 0xdf, 0x43, 0x3b, 0x79, 0x15, 0xb3, 0xd9, 0x70, 0x8d, 0xe6, 0xca, 0xa6, 0xe2, 0x8d, 0x6c, 0x07, 0x41, 0x9d, - 0xf7, 0xdf, 0x97, 0x71, 0x7c, 0x1d, 0xdf, 0x11, 0x89, 0xe8, 0x8c, 0x6e, 0xc9, 0x95, 0xcd, 0xe9, 0x27, 0x73, 0x65, - 0xe3, 0x7b, 0xe5, 0xca, 0xe6, 0xf4, 0x8f, 0xce, 0x95, 0x65, 0xd4, 0xc8, 0x95, 0x05, 0x39, 0xf7, 0xf5, 0xbd, 0x52, - 0x2e, 0x75, 0x26, 0x5c, 0x7a, 0x9d, 0x93, 0x8e, 0x8a, 0x81, 0xc4, 0xe9, 0x04, 0xf2, 0x2d, 0xff, 0xf1, 0xe9, 0x93, - 0x71, 0x3a, 0x31, 0x93, 0x27, 0xe1, 0xc3, 0x24, 0x40, 0x76, 0x38, 0x25, 0x0b, 0xfb, 0xa7, 0x9b, 0xce, 0x93, 0x61, - 0xa7, 0xb7, 0xdf, 0x99, 0xda, 0x9e, 0x0d, 0x4e, 0x47, 0x51, 0xd0, 0xee, 0xed, 0xef, 0x43, 0xc1, 0xb5, 0x51, 0xd0, - 0x85, 0x02, 0x66, 0x14, 0x1c, 0x42, 0x41, 0x60, 0x14, 0x3c, 0x84, 0x82, 0xd0, 0x28, 0x78, 0x04, 0x05, 0x57, 0x76, - 0x31, 0x64, 0x65, 0x42, 0xf0, 0x23, 0x24, 0x6e, 0x30, 0xdc, 0x49, 0xeb, 0xa7, 0xb7, 0x23, 0xa2, 0xab, 0x3c, 0x2a, - 0x6f, 0x7e, 0x68, 0x1e, 0xe8, 0x8b, 0x0a, 0x2f, 0xbe, 0xb8, 0x00, 0xd6, 0x0a, 0x17, 0xb1, 0x60, 0x88, 0x49, 0xca, - 0x9a, 0xfb, 0xfa, 0xb5, 0xed, 0x95, 0x59, 0xb3, 0x6d, 0xdc, 0xd5, 0x79, 0xb3, 0x9e, 0x8d, 0x04, 0x5f, 0x92, 0x2f, - 0x0e, 0x1b, 0xa1, 0xea, 0x16, 0xee, 0x00, 0xac, 0x2e, 0xe0, 0xdc, 0x47, 0x78, 0xaa, 0x15, 0x20, 0xea, 0xc0, 0x07, - 0x18, 0xde, 0xb3, 0x29, 0xd5, 0xfb, 0x45, 0x0f, 0x60, 0x89, 0xcc, 0xe2, 0x5e, 0x54, 0x29, 0x46, 0x6f, 0xf1, 0xb8, - 0xba, 0xf3, 0xf5, 0x3d, 0x91, 0x77, 0xe8, 0xa5, 0x58, 0x86, 0xb9, 0x66, 0x98, 0xfb, 0x13, 0x0f, 0x52, 0x28, 0x21, - 0x63, 0xc4, 0x1b, 0x13, 0x42, 0xda, 0x83, 0xb9, 0xf7, 0x16, 0x5f, 0x47, 0x34, 0xf1, 0xa6, 0x45, 0xaf, 0x5c, 0x7f, - 0x99, 0xd2, 0xf9, 0xbe, 0xbc, 0x28, 0x5c, 0xd0, 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, 0xce, 0x9f, - 0xa1, 0xec, 0x8c, 0xe3, 0xf4, 0xba, 0x09, 0xe2, 0x7e, 0x63, 0x1e, 0x20, 0xcc, 0xad, 0xcc, 0x03, 0x7c, 0x09, 0xb0, - 0x96, 0x4f, 0xef, 0xfd, 0x49, 0xf9, 0xfb, 0x15, 0xcd, 0x73, 0x7f, 0xa2, 0x6a, 0x6e, 0xcf, 0xfb, 0x13, 0x20, 0x9a, - 0x39, 0x7f, 0x1a, 0x08, 0x48, 0xce, 0x03, 0x84, 0x40, 0x40, 0x57, 0xe5, 0xea, 0xc1, 0xcc, 0xeb, 0x69, 0x7e, 0x02, - 0x55, 0xf5, 0x22, 0xee, 0x4f, 0xaa, 0x82, 0xe3, 0x59, 0x46, 0x55, 0x02, 0x21, 0x60, 0xb1, 0x38, 0x6e, 0x41, 0x81, - 0x7c, 0xbd, 0x25, 0x9d, 0x4f, 0x73, 0x97, 0xed, 0x49, 0x7d, 0x96, 0x4e, 0xe7, 0x33, 0x4f, 0xa6, 0x94, 0xc7, 0x52, - 0xd6, 0x33, 0xf2, 0xbe, 0xec, 0x04, 0xf0, 0x9f, 0x3a, 0x78, 0xf1, 0xe5, 0x78, 0x3c, 0xbe, 0x33, 0xbd, 0xef, 0xcb, - 0x70, 0x4c, 0xbb, 0xf4, 0xb0, 0x07, 0xa7, 0x16, 0x9a, 0x2a, 0x11, 0xad, 0x53, 0x08, 0xdc, 0x2d, 0xee, 0x57, 0x19, - 0x72, 0xd6, 0x78, 0xb4, 0xb8, 0x7f, 0xaa, 0x5f, 0x31, 0xcb, 0xe8, 0x62, 0xea, 0x67, 0x13, 0x96, 0x78, 0xed, 0xc2, - 0xbd, 0x5a, 0x28, 0x50, 0x8f, 0x8e, 0x8e, 0x0a, 0x37, 0xd4, 0x4f, 0xed, 0x30, 0x2c, 0xdc, 0x60, 0x51, 0x4e, 0xa3, - 0xdd, 0x1e, 0x8f, 0x0b, 0x97, 0xe9, 0x82, 0xfd, 0x6e, 0x10, 0xee, 0x77, 0x0b, 0xf7, 0xda, 0xa8, 0x51, 0xb8, 0x54, - 0x3d, 0x65, 0x34, 0xac, 0x1d, 0x7d, 0x78, 0xd4, 0x6e, 0x17, 0xae, 0x24, 0xb4, 0x05, 0xc4, 0xe4, 0xe4, 0x4f, 0xcf, - 0x9f, 0xf3, 0xb4, 0xb8, 0x28, 0x8a, 0x5e, 0xcc, 0x9d, 0xe1, 0xae, 0xba, 0x56, 0x52, 0x7e, 0x87, 0xb1, 0x40, 0x23, - 0xfc, 0xb5, 0x99, 0x39, 0x07, 0xc4, 0x2c, 0x32, 0xe6, 0x62, 0x9d, 0x58, 0x57, 0x7b, 0x0d, 0x94, 0x25, 0x5e, 0x7f, - 0x4d, 0xe2, 0x2a, 0xa1, 0x0e, 0xf8, 0x18, 0xd4, 0x94, 0xb7, 0x9f, 0x27, 0xdb, 0xa4, 0x47, 0xf6, 0x69, 0xe9, 0x71, - 0x79, 0x1f, 0xe1, 0x91, 0xfd, 0xe1, 0xc2, 0x23, 0x31, 0x85, 0x87, 0x64, 0x1d, 0xd7, 0x9c, 0xd8, 0x41, 0x44, 0x83, - 0x8f, 0x97, 0xe9, 0x4d, 0x13, 0xb6, 0x44, 0x66, 0x0b, 0xb1, 0x72, 0xf5, 0x5b, 0x33, 0xf9, 0x75, 0x67, 0xc6, 0x47, - 0x1c, 0x85, 0x8e, 0xff, 0x26, 0x21, 0xf6, 0x1b, 0x1d, 0xd8, 0x93, 0x25, 0xe3, 0x31, 0xb1, 0xdf, 0x8c, 0xc7, 0xb6, - 0xbe, 0x1c, 0xc7, 0xe7, 0x54, 0xd4, 0x7a, 0x5d, 0x2b, 0x11, 0xb5, 0xc0, 0xd0, 0xaf, 0xca, 0xcc, 0x02, 0x95, 0x77, - 0x67, 0xe6, 0xd8, 0xa9, 0x37, 0x21, 0xcb, 0x61, 0xab, 0xc1, 0xb7, 0x25, 0xeb, 0x97, 0xf3, 0x27, 0xb5, 0x2f, 0x29, - 0x95, 0x00, 0x6f, 0xf8, 0xfc, 0xd3, 0xea, 0xcd, 0x70, 0x13, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0xe7, 0x9a, - 0xa3, 0x82, 0xe5, 0x6f, 0x92, 0x95, 0x5b, 0x1f, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0x2a, 0x78, 0x3a, 0x99, 0xc4, 0xe2, - 0x30, 0x49, 0xcd, 0xe0, 0x96, 0xcd, 0x07, 0xb5, 0xf9, 0x7a, 0x66, 0x43, 0xf5, 0x79, 0x0d, 0xf1, 0xbd, 0x61, 0x79, - 0x5a, 0xf8, 0x4a, 0x7d, 0x78, 0x56, 0xc4, 0x04, 0x17, 0x8a, 0xc7, 0x2f, 0xe4, 0x19, 0x53, 0x8e, 0x59, 0x28, 0x9b, - 0xb3, 0xb0, 0x28, 0xd4, 0xe9, 0xfc, 0x90, 0xe5, 0x33, 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0x5f, 0x1b, - 0xc9, 0x6d, 0xb5, 0xe5, 0x2a, 0xb4, 0x9d, 0x26, 0xb3, 0x85, 0xae, 0x79, 0x61, 0x2b, 0x93, 0x4d, 0x23, 0xd1, 0xb6, - 0x24, 0x3e, 0x65, 0xda, 0x9d, 0x31, 0x43, 0xc8, 0xfc, 0x29, 0x17, 0x44, 0xbf, 0xd2, 0x05, 0x85, 0x69, 0x65, 0x89, - 0x37, 0x12, 0x5b, 0x22, 0x55, 0x2c, 0x9f, 0xf9, 0x89, 0x36, 0xe6, 0x24, 0x3f, 0xd8, 0x5d, 0x54, 0x2b, 0x5f, 0xd8, - 0x1a, 0x6c, 0x49, 0xbc, 0xfd, 0xe3, 0x16, 0x34, 0xe8, 0x5b, 0x35, 0xd0, 0x93, 0xb5, 0x0c, 0xb3, 0xbb, 0xf3, 0xae, - 0x3f, 0x5e, 0xb8, 0xf9, 0x35, 0x76, 0xf3, 0x6b, 0xeb, 0xab, 0x45, 0xf3, 0x9a, 0x5e, 0x7e, 0x64, 0xbc, 0xc9, 0xfd, - 0x59, 0x13, 0xbc, 0xa7, 0x22, 0x33, 0x44, 0xb1, 0x67, 0xa1, 0xa3, 0x4b, 0xd3, 0xaf, 0x37, 0xcf, 0x21, 0x3d, 0x5b, - 0x98, 0x51, 0x5e, 0x92, 0x26, 0xb4, 0x57, 0x3f, 0xbe, 0x67, 0x66, 0x18, 0x6b, 0x6c, 0x8d, 0x16, 0x29, 0xa4, 0x73, - 0xf3, 0x5b, 0xaf, 0xad, 0xd8, 0x7a, 0x5b, 0xa7, 0x0f, 0xb7, 0x37, 0xd6, 0xf7, 0x14, 0x72, 0x1b, 0x42, 0x7a, 0x65, - 0xeb, 0xf9, 0xcf, 0xdb, 0xf2, 0xbb, 0x3f, 0x75, 0x98, 0x0d, 0xf2, 0x49, 0xf4, 0xff, 0xc6, 0x29, 0xc0, 0xd5, 0x62, - 0x71, 0x98, 0xed, 0x3e, 0x90, 0x79, 0xfe, 0x98, 0xd3, 0x0c, 0xdf, 0xa7, 0xe6, 0xa5, 0xb8, 0x77, 0x62, 0x01, 0x62, - 0xc6, 0xeb, 0x1c, 0xd5, 0x53, 0xb1, 0xef, 0xee, 0xfe, 0xee, 0xe9, 0x17, 0x0a, 0x47, 0xfa, 0x1e, 0x56, 0xdb, 0xee, - 0xc1, 0x46, 0x88, 0xfd, 0x5b, 0x8f, 0x25, 0x42, 0xe6, 0x5d, 0x42, 0x52, 0x48, 0x6f, 0x96, 0xaa, 0x53, 0x99, 0x19, - 0x8d, 0xc5, 0xa7, 0xd7, 0xd5, 0x52, 0xec, 0x3f, 0x9c, 0xdd, 0xe8, 0xd5, 0xe8, 0xac, 0x9c, 0xb6, 0xfc, 0x43, 0x0f, - 0x55, 0x6e, 0x3f, 0xc5, 0x59, 0x3f, 0x18, 0x78, 0x38, 0xbb, 0xe9, 0x49, 0x41, 0xdb, 0xcc, 0x24, 0x54, 0xed, 0xd9, - 0x8d, 0x79, 0xac, 0xb4, 0xea, 0xc8, 0x72, 0xf7, 0x73, 0x8b, 0xfa, 0x39, 0xed, 0xc1, 0x97, 0xa6, 0x58, 0xe0, 0xc7, - 0x4a, 0x98, 0x4f, 0x59, 0x18, 0xc6, 0xb4, 0xa7, 0xe5, 0xb5, 0xd5, 0x79, 0x08, 0xa7, 0x32, 0xcd, 0x25, 0xab, 0xaf, - 0x8a, 0x81, 0xbc, 0x12, 0x4f, 0xfe, 0x65, 0x9e, 0xc6, 0xf0, 0x9d, 0xc7, 0x8d, 0xe8, 0x54, 0xc7, 0x15, 0xdb, 0x15, - 0xf2, 0xc4, 0xef, 0xfa, 0x5c, 0x0e, 0xdb, 0x7f, 0xea, 0x89, 0x05, 0x6f, 0xf7, 0x78, 0x3a, 0xf3, 0x9a, 0xfb, 0xf5, - 0x89, 0xc0, 0xab, 0x72, 0x0a, 0x78, 0xc3, 0xb4, 0x30, 0x48, 0x2b, 0xc9, 0xa7, 0x2d, 0xb7, 0xa3, 0xca, 0x44, 0x07, - 0x60, 0x84, 0x96, 0x45, 0x45, 0x7d, 0x32, 0xff, 0x98, 0xdd, 0xf2, 0x78, 0xf3, 0x6e, 0x79, 0xac, 0x77, 0xcb, 0xdd, - 0x14, 0xfb, 0xe5, 0xb8, 0x03, 0xff, 0xf5, 0xaa, 0x09, 0x79, 0x6d, 0x6b, 0x7f, 0x76, 0x63, 0x81, 0x9e, 0xd6, 0xec, - 0xce, 0x6e, 0xe4, 0xa1, 0x5a, 0x48, 0x5c, 0x6b, 0xc3, 0x31, 0x53, 0xdc, 0xb6, 0xa0, 0x10, 0xfe, 0x6f, 0xd7, 0x5e, - 0x75, 0x0e, 0xe0, 0x1d, 0xb4, 0x3a, 0x5c, 0x7f, 0xd7, 0xbd, 0x7b, 0xd3, 0x7a, 0x49, 0xca, 0x1d, 0x4f, 0x73, 0x63, - 0xe4, 0x72, 0xff, 0xf2, 0x92, 0x86, 0xde, 0x38, 0x0d, 0xe6, 0xf9, 0x3f, 0x29, 0xf8, 0x15, 0x12, 0xef, 0xdc, 0xd2, - 0x2b, 0xfd, 0xe8, 0xa6, 0xf2, 0x88, 0xaf, 0xee, 0x61, 0x51, 0xae, 0x93, 0x97, 0x07, 0x7e, 0x4c, 0x9d, 0xae, 0x7b, - 0xb0, 0x61, 0x13, 0xfc, 0x9b, 0xac, 0xcd, 0xc6, 0xc9, 0xfc, 0x5e, 0x64, 0xdc, 0x89, 0x84, 0xcf, 0xc2, 0x81, 0xb9, - 0x86, 0xed, 0xa3, 0xcd, 0xe0, 0x0e, 0xf5, 0x48, 0x23, 0x2d, 0x14, 0x94, 0xdc, 0x09, 0xe9, 0xd8, 0x9f, 0xc7, 0xfc, - 0xee, 0x5e, 0xb7, 0x51, 0xc6, 0x5a, 0xaf, 0x77, 0x30, 0xf4, 0xaa, 0xee, 0x3d, 0xb9, 0xf4, 0x97, 0x8f, 0x0f, 0xe0, - 0x3f, 0x79, 0xf8, 0xe5, 0xb2, 0xd2, 0xd5, 0xa5, 0xd5, 0x0b, 0xba, 0xfa, 0x55, 0x4d, 0x19, 0x97, 0x22, 0x5c, 0xe8, - 0xe3, 0xf7, 0xad, 0x0d, 0x5a, 0xe5, 0xbd, 0xaa, 0x2b, 0x2d, 0xeb, 0xb3, 0x6a, 0x7f, 0x5e, 0xe7, 0xf7, 0xac, 0x1b, - 0x48, 0xcd, 0xb5, 0x5e, 0x57, 0x7d, 0x7a, 0x7e, 0xad, 0xb2, 0xc6, 0xb8, 0xa8, 0x7f, 0x45, 0x2e, 0x4b, 0x13, 0x45, - 0xa6, 0xa2, 0x82, 0x95, 0x72, 0x25, 0xad, 0x94, 0x94, 0x92, 0x8b, 0xe3, 0xc1, 0xcd, 0x34, 0xb6, 0xae, 0xe4, 0xfd, - 0x38, 0xc4, 0xee, 0xb8, 0x6d, 0xdb, 0x12, 0x4e, 0x3a, 0xf8, 0x4c, 0x97, 0xfd, 0xe1, 0xfd, 0xd7, 0xcd, 0x23, 0x7b, - 0x00, 0x9a, 0xd6, 0xd5, 0x44, 0x68, 0x76, 0x2f, 0xfd, 0x5b, 0x9a, 0x9d, 0x77, 0x95, 0x0b, 0x5e, 0xe6, 0x8b, 0x8b, - 0x32, 0xab, 0x6b, 0x5b, 0x37, 0xd3, 0x38, 0xc9, 0x89, 0x1d, 0x71, 0x3e, 0xf3, 0x5a, 0xad, 0xeb, 0xeb, 0x6b, 0xf7, - 0x7a, 0xdf, 0x4d, 0xb3, 0x49, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, 0x8b, 0x1f, 0xb6, 0x75, 0xc5, 0xe8, 0xf5, 0x93, 0xf4, - 0x86, 0xd8, 0x6d, 0xab, 0x6d, 0x75, 0xba, 0x47, 0x56, 0xa7, 0x7b, 0xe0, 0x3e, 0x3c, 0xb2, 0xfb, 0x5f, 0x58, 0xd6, - 0x71, 0x48, 0xc7, 0x39, 0xfc, 0xb0, 0xac, 0x63, 0xa1, 0x78, 0xc9, 0xdf, 0x96, 0xe5, 0x06, 0x71, 0xde, 0xec, 0x58, - 0x0b, 0xf5, 0x68, 0x59, 0x70, 0x8b, 0x90, 0x67, 0x7d, 0x39, 0xee, 0x8e, 0x0f, 0xc6, 0x8f, 0x7b, 0xaa, 0xb8, 0xf8, - 0xa2, 0x56, 0x1d, 0xcb, 0x7f, 0xbb, 0x46, 0xb3, 0x9c, 0x67, 0xe9, 0x47, 0xaa, 0x5c, 0xfb, 0x16, 0x88, 0x9e, 0x8d, - 0x4d, 0xbb, 0xeb, 0x23, 0x75, 0x8e, 0x2e, 0x83, 0x71, 0xb7, 0xaa, 0x2e, 0x60, 0x6c, 0x95, 0x40, 0x1e, 0xb7, 0x34, - 0xe8, 0xc7, 0x26, 0x9a, 0x3a, 0xcd, 0x4d, 0x88, 0xea, 0xd8, 0x6a, 0x8e, 0x13, 0x3d, 0xbf, 0x63, 0x38, 0xb4, 0xae, - 0x75, 0x55, 0x01, 0x81, 0x6d, 0x85, 0xc4, 0x7e, 0xd5, 0xe9, 0x1e, 0xe1, 0x4e, 0xe7, 0xa1, 0xfb, 0xf0, 0x28, 0x68, - 0xe3, 0x03, 0xf7, 0xa0, 0xb9, 0xef, 0x3e, 0xc4, 0x47, 0xcd, 0x23, 0x7c, 0xf4, 0xfc, 0x28, 0x68, 0x1e, 0xb8, 0x07, - 0xb8, 0xdd, 0x3c, 0x82, 0xc2, 0xe6, 0x51, 0xf3, 0xe8, 0xaa, 0x79, 0x70, 0x14, 0xb4, 0x45, 0x69, 0xd7, 0x3d, 0x3c, - 0x6c, 0x76, 0xda, 0xee, 0xe1, 0x21, 0x3e, 0x74, 0x1f, 0x3e, 0x6c, 0x76, 0xf6, 0xdd, 0x87, 0x0f, 0x5f, 0x1e, 0x1e, - 0xb9, 0xfb, 0xf0, 0x6e, 0x7f, 0x3f, 0xd8, 0x77, 0x3b, 0x9d, 0x26, 0xfc, 0xc1, 0x47, 0x6e, 0x57, 0xfe, 0xe8, 0x74, - 0xdc, 0xfd, 0x0e, 0x6e, 0xc7, 0x87, 0x5d, 0xf7, 0xe1, 0x63, 0x2c, 0xfe, 0x8a, 0x6a, 0x58, 0xfc, 0x81, 0x6e, 0xf0, - 0x63, 0xb7, 0xfb, 0x50, 0xfe, 0x12, 0x1d, 0x5e, 0x1d, 0x1c, 0xfd, 0x68, 0xb7, 0xb6, 0xce, 0xa1, 0x23, 0xe7, 0x70, - 0x74, 0xe8, 0xee, 0xef, 0xe3, 0x83, 0x8e, 0x7b, 0xb4, 0x1f, 0x35, 0x0f, 0xba, 0xee, 0xc3, 0x47, 0x41, 0xb3, 0xe3, - 0x3e, 0x7a, 0x84, 0xdb, 0xcd, 0x7d, 0xb7, 0x8b, 0x3b, 0xee, 0xc1, 0xbe, 0xf8, 0xb1, 0xef, 0x76, 0xaf, 0x1e, 0x3d, - 0x76, 0x1f, 0x1e, 0x46, 0x0f, 0xdd, 0x83, 0x6f, 0x0f, 0x8e, 0xdc, 0xee, 0x7e, 0xb4, 0xff, 0xd0, 0xed, 0x3e, 0xba, - 0x7a, 0xe8, 0x1e, 0x44, 0xcd, 0xee, 0xc3, 0x3b, 0x5b, 0x76, 0xba, 0x2e, 0xe0, 0x48, 0xbc, 0x86, 0x17, 0x58, 0xbd, - 0x80, 0xff, 0x23, 0xd1, 0xf6, 0xdf, 0xb0, 0x9b, 0x7c, 0xbd, 0xe9, 0x63, 0xf7, 0xe8, 0x51, 0x20, 0xab, 0x43, 0x41, - 0x53, 0xd7, 0x80, 0x26, 0x57, 0x4d, 0x39, 0xac, 0xe8, 0xae, 0xa9, 0x3b, 0xd2, 0xff, 0xab, 0xc1, 0xae, 0x9a, 0x30, - 0xb0, 0x1c, 0xf7, 0xdf, 0xb5, 0x9f, 0x72, 0xc9, 0x8f, 0x5b, 0x13, 0x49, 0xfa, 0x93, 0xfe, 0x17, 0xf2, 0x73, 0x3e, - 0x5f, 0x5c, 0x60, 0x7f, 0x9b, 0xe3, 0x23, 0xfe, 0xb4, 0xe3, 0x23, 0xa2, 0xf7, 0xf1, 0x7c, 0xc4, 0x7f, 0xb8, 0xe7, - 0xc3, 0x5f, 0x75, 0x9b, 0xdf, 0xf0, 0x35, 0x07, 0xc7, 0xaa, 0x55, 0xfc, 0x82, 0x3b, 0xc3, 0x14, 0x3e, 0x1d, 0x5d, - 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, 0x3f, - 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, 0xe1, - 0x29, 0x38, 0x13, 0xcf, 0x0e, 0xdc, 0x9a, 0xd1, 0xb0, 0x5b, 0xf4, 0x4a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, 0xe7, - 0x04, 0x1d, 0xe4, 0x15, 0x39, 0xc4, 0x56, 0x6e, 0x99, 0x99, 0x90, 0x3a, 0xea, 0xa1, 0x14, 0x4a, 0x5d, 0xb7, 0xed, - 0xb6, 0x4b, 0x97, 0x0e, 0x5c, 0x8b, 0x44, 0x16, 0x29, 0xf7, 0xbd, 0x9d, 0x0e, 0x8e, 0xd3, 0x09, 0x5c, 0x96, 0x24, - 0x3e, 0x1f, 0x07, 0x27, 0x1e, 0x02, 0xf9, 0xe5, 0x3e, 0x48, 0x9f, 0x50, 0x8e, 0x1e, 0x3f, 0xfb, 0xf8, 0x37, 0x08, - 0x62, 0xea, 0x98, 0xc4, 0x14, 0xbc, 0x1d, 0xaf, 0x68, 0xc8, 0x7c, 0xc7, 0x76, 0x66, 0x19, 0x1d, 0xd3, 0x2c, 0x6f, - 0xd6, 0xee, 0xeb, 0x11, 0x57, 0xf5, 0x20, 0x5b, 0x41, 0x38, 0xce, 0xe0, 0x73, 0x48, 0x64, 0xa8, 0xfc, 0x8d, 0xb6, - 0x32, 0xc0, 0xec, 0x02, 0xeb, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xd9, 0x52, 0x6b, 0xeb, 0xb8, 0xdd, 0x43, 0x64, - 0x89, 0x62, 0xf8, 0xd0, 0xcc, 0x0f, 0x4e, 0x73, 0xbf, 0xfd, 0x27, 0x64, 0x34, 0x2b, 0x3b, 0x1a, 0x29, 0x77, 0x5b, - 0x52, 0x7e, 0x8e, 0x70, 0x25, 0xec, 0x6a, 0x4b, 0x8a, 0xf8, 0x52, 0xce, 0xdd, 0x46, 0xbd, 0x44, 0x25, 0xcd, 0xc9, - 0x2b, 0x01, 0xc7, 0x6c, 0xe2, 0x18, 0xd7, 0x4d, 0x24, 0xf2, 0x43, 0x36, 0x70, 0x5b, 0x3d, 0x42, 0x45, 0x55, 0x25, - 0x41, 0x0b, 0x11, 0x6d, 0x61, 0x89, 0x95, 0x2c, 0x97, 0x4e, 0x02, 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, 0x0d, - 0xc9, 0x09, 0x2e, 0x01, 0x12, 0x08, 0x26, 0x89, 0xfc, 0xb7, 0x2a, 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, 0x64, - 0x02, 0x54, 0xe8, 0x27, 0x93, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, 0xc9, - 0xed, 0x7b, 0x75, 0xe3, 0xb5, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, 0xbe, 0xce, 0xd3, 0x20, 0x0f, 0x8e, 0xa7, 0x94, - 0xfb, 0xf2, 0xb0, 0x1a, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, 0x54, 0x05, 0xf5, 0x89, 0x3c, 0xcc, 0x2f, 0x45, - 0x3d, 0xb6, 0xd4, 0x55, 0x71, 0x4d, 0xb1, 0x34, 0xa4, 0x83, 0xa5, 0x3f, 0x26, 0xf0, 0xc5, 0x71, 0x64, 0x92, 0xa4, - 0x76, 0xff, 0x41, 0x99, 0xeb, 0xb2, 0x6d, 0x11, 0x62, 0x96, 0x7c, 0x1c, 0x66, 0x34, 0xfe, 0x27, 0xf2, 0x80, 0x05, - 0x69, 0xf2, 0x60, 0x64, 0xa3, 0x1e, 0x77, 0xa3, 0x8c, 0x8e, 0xc9, 0x03, 0x90, 0xf1, 0x9e, 0xb0, 0x3e, 0x80, 0x11, - 0x36, 0x6e, 0xa6, 0x31, 0x16, 0x1a, 0xd3, 0x3d, 0x14, 0x22, 0x09, 0xae, 0xdd, 0x3d, 0xb4, 0x2d, 0x69, 0x13, 0x8b, - 0xdf, 0x7d, 0x29, 0x4e, 0x85, 0x12, 0x60, 0x75, 0xba, 0xee, 0x61, 0xd4, 0x75, 0x1f, 0x5f, 0x3d, 0x72, 0x8f, 0xa2, - 0xce, 0xa3, 0xab, 0x26, 0xfc, 0xdb, 0x75, 0x1f, 0xc7, 0xcd, 0xae, 0xfb, 0x18, 0xfe, 0xff, 0xf6, 0xc0, 0x3d, 0x8c, - 0x9a, 0x1d, 0xf7, 0xe8, 0x6a, 0xdf, 0xdd, 0x7f, 0xd9, 0xe9, 0xba, 0xfb, 0x56, 0xc7, 0x92, 0xed, 0x80, 0x5d, 0x4b, - 0xee, 0xfc, 0x60, 0x65, 0x43, 0x6c, 0x08, 0xc6, 0xc9, 0x03, 0x77, 0x36, 0x16, 0x67, 0xa4, 0xcd, 0xfd, 0xa9, 0x9c, - 0x75, 0x4f, 0xfd, 0x0c, 0xbe, 0x6c, 0x5a, 0xdf, 0xbb, 0xb5, 0x77, 0xb8, 0xc6, 0x2f, 0x36, 0x0c, 0x31, 0x13, 0x11, - 0x70, 0xf3, 0xae, 0x35, 0xb8, 0xa8, 0x4c, 0x7c, 0x08, 0x4a, 0xdf, 0x87, 0xbf, 0x9d, 0xb4, 0x65, 0x45, 0x7d, 0xe7, - 0xbe, 0x60, 0x16, 0x4c, 0x7c, 0x72, 0x4f, 0x0c, 0xf2, 0x22, 0x2c, 0x56, 0xc7, 0x87, 0x73, 0x7f, 0x59, 0x6a, 0x5c, - 0x37, 0x47, 0xab, 0x20, 0x7f, 0xc8, 0xe0, 0x82, 0xc0, 0xa2, 0xd0, 0xa0, 0xd7, 0xdc, 0xb4, 0x15, 0x96, 0x04, 0xbf, - 0xa0, 0xf9, 0xc0, 0x86, 0x22, 0xdb, 0xb3, 0x85, 0x8b, 0xcf, 0x2e, 0xbf, 0xee, 0x5a, 0x12, 0x76, 0x55, 0x80, 0xc5, - 0xed, 0x0f, 0xb0, 0x6b, 0x01, 0x3f, 0x36, 0xda, 0xdb, 0xdb, 0xba, 0x5f, 0x84, 0x02, 0x09, 0x73, 0xad, 0x3e, 0x0a, - 0x69, 0xb2, 0x22, 0xdb, 0x44, 0x74, 0xd9, 0xaf, 0x40, 0x21, 0x52, 0x78, 0xba, 0xa4, 0x3e, 0x77, 0xfd, 0x44, 0x9e, - 0x20, 0x30, 0x18, 0x16, 0xee, 0xd0, 0x7d, 0x54, 0xa4, 0xdc, 0x97, 0x49, 0x61, 0xe6, 0x3e, 0x4f, 0xb9, 0xaf, 0x2f, - 0x4f, 0xf2, 0x79, 0xed, 0xe0, 0x7c, 0xd4, 0xed, 0xbf, 0x79, 0x7f, 0x62, 0xc9, 0xed, 0x79, 0xdc, 0x8a, 0xba, 0xfd, - 0x63, 0xe1, 0x33, 0x91, 0x61, 0x7f, 0x22, 0xc3, 0xfe, 0x96, 0xba, 0x35, 0x06, 0x22, 0x69, 0x45, 0x4b, 0x4e, 0x5b, - 0xd8, 0x0c, 0xd2, 0xdb, 0x3b, 0x9d, 0xc7, 0x9c, 0xc1, 0x47, 0x8d, 0x5a, 0x22, 0xe6, 0x2f, 0x72, 0x08, 0xf4, 0x21, - 0x54, 0xa5, 0x1d, 0x5e, 0xf2, 0x44, 0xfb, 0x86, 0xc7, 0x2c, 0xa6, 0xfa, 0xd8, 0xa9, 0xea, 0xaa, 0x4c, 0xf8, 0x59, - 0xaf, 0x9d, 0xcf, 0x2f, 0x21, 0xe9, 0x41, 0xa7, 0x17, 0x7d, 0x50, 0x0d, 0x8e, 0xc5, 0x50, 0x10, 0xb9, 0x97, 0x62, - 0x5a, 0x7f, 0xbe, 0xb5, 0xbe, 0xa4, 0x6a, 0xf6, 0x4a, 0x42, 0xc0, 0x55, 0x1d, 0xd1, 0x7e, 0xbf, 0x74, 0x17, 0x9b, - 0xef, 0x8a, 0xe3, 0x56, 0xb4, 0xdf, 0xbf, 0xf0, 0x26, 0xaa, 0xbf, 0x97, 0xe9, 0x64, 0x73, 0x5f, 0x71, 0x3a, 0x19, - 0x88, 0x73, 0xf0, 0xf2, 0x4e, 0x27, 0xad, 0xfc, 0xa6, 0xb1, 0xdd, 0x3f, 0x56, 0xca, 0x80, 0x25, 0xc2, 0xea, 0xf6, - 0x61, 0x5b, 0x1f, 0xad, 0x8f, 0xd3, 0x09, 0x6c, 0x48, 0xd9, 0xc4, 0x18, 0xa4, 0xe6, 0x71, 0x8f, 0x3a, 0xfd, 0x63, - 0xdf, 0x12, 0xbc, 0x45, 0x30, 0x8f, 0xdc, 0x6b, 0x41, 0xe3, 0x28, 0x9d, 0x52, 0x97, 0xa5, 0xad, 0x6b, 0x7a, 0xd9, - 0xf4, 0x67, 0xac, 0x72, 0x6f, 0x83, 0xd2, 0x51, 0x0e, 0x99, 0xae, 0xa4, 0x58, 0x75, 0x2b, 0x77, 0xdb, 0x01, 0xd8, - 0x3c, 0xda, 0x35, 0x27, 0x7c, 0x72, 0x06, 0x58, 0x69, 0xff, 0xb8, 0xe5, 0xaf, 0x60, 0x44, 0xf0, 0xfb, 0x42, 0x39, - 0xda, 0xc1, 0xb0, 0xb9, 0x14, 0xf9, 0x5d, 0x52, 0x1c, 0x68, 0x87, 0xbc, 0x12, 0xd4, 0x85, 0xdd, 0xff, 0xd7, 0xff, - 0xf1, 0xbf, 0x94, 0x8f, 0xfd, 0xb8, 0x15, 0x75, 0x74, 0x5f, 0x2b, 0xab, 0x52, 0x1c, 0xc3, 0x45, 0x35, 0x55, 0x50, - 0x98, 0xde, 0x34, 0x27, 0x19, 0x0b, 0x9b, 0x91, 0x1f, 0x8f, 0xed, 0xfe, 0x76, 0x6c, 0xca, 0xfc, 0xc3, 0xa6, 0x0e, - 0xa7, 0xae, 0x17, 0x01, 0xbd, 0xfe, 0xa6, 0x5b, 0x17, 0x74, 0x4a, 0x97, 0xd8, 0xda, 0xe6, 0x1d, 0x0c, 0xd5, 0xee, - 0xab, 0xdd, 0xc3, 0x90, 0xa8, 0x6f, 0x42, 0x2b, 0x0e, 0x98, 0xd4, 0xae, 0x5f, 0x28, 0x6c, 0xab, 0x0c, 0x6a, 0xfd, - 0xdf, 0xff, 0xf9, 0x5f, 0xfe, 0x9b, 0x7e, 0x84, 0x58, 0xd5, 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, - 0x85, 0xfc, 0x33, 0x15, 0xcf, 0x12, 0x4c, 0xc5, 0xaa, 0x82, 0x59, 0x92, 0xbb, 0x58, 0x70, 0xaa, 0x6d, 0xca, 0x72, - 0xce, 0x82, 0xfa, 0x85, 0x0c, 0x67, 0x62, 0x40, 0xb1, 0x33, 0x15, 0x74, 0x62, 0x87, 0x17, 0x15, 0x41, 0xd5, 0x50, - 0x2e, 0x08, 0xb7, 0x38, 0x6e, 0x01, 0xbe, 0xef, 0x77, 0x9f, 0x8c, 0x5b, 0x2e, 0xc7, 0x42, 0x93, 0x09, 0x94, 0x14, - 0x55, 0xb9, 0x05, 0xb1, 0x95, 0x05, 0x3c, 0x7a, 0x5d, 0xa3, 0x58, 0xac, 0x5e, 0xad, 0x4d, 0xef, 0xe7, 0x79, 0xce, - 0xd9, 0x18, 0x50, 0x2e, 0xfd, 0xc4, 0x22, 0x8c, 0xdd, 0x04, 0x5d, 0x31, 0xbe, 0x2d, 0x44, 0x2f, 0x92, 0x40, 0x0f, - 0x8e, 0xfe, 0x54, 0xfc, 0x79, 0x0a, 0x1a, 0x99, 0xe5, 0x4c, 0xfd, 0x1b, 0x65, 0x9e, 0x3f, 0x6c, 0xb7, 0x67, 0x37, - 0x68, 0x51, 0x8d, 0x80, 0xb7, 0x0d, 0x26, 0xe8, 0xd8, 0xec, 0x50, 0xc4, 0xbf, 0x4b, 0x37, 0x76, 0xdb, 0x02, 0x5f, - 0xb8, 0xd5, 0x2e, 0x8a, 0xaf, 0x16, 0xc2, 0x93, 0xca, 0x7e, 0x85, 0x38, 0xb5, 0x72, 0x3a, 0x5f, 0xa6, 0xe6, 0xe4, - 0x16, 0x46, 0xab, 0xae, 0x6c, 0x15, 0x75, 0xd6, 0xaf, 0x66, 0x31, 0xe3, 0xec, 0x66, 0x84, 0xfc, 0x00, 0x62, 0xde, - 0x51, 0x07, 0x47, 0xdd, 0x45, 0xd9, 0x3d, 0xe7, 0xe9, 0xd4, 0x0c, 0xac, 0x53, 0x9f, 0x06, 0x74, 0xac, 0x9d, 0xf5, - 0xea, 0xbd, 0x0c, 0x9a, 0x17, 0xd1, 0xfe, 0x86, 0xb1, 0x14, 0x48, 0x22, 0xa0, 0x6e, 0xb5, 0x8b, 0x2f, 0x61, 0x07, - 0x2e, 0xc6, 0x71, 0xea, 0x73, 0x4f, 0x10, 0x6c, 0xcf, 0x0c, 0xcf, 0xfb, 0xc0, 0x93, 0xd2, 0x85, 0x01, 0x4f, 0x4f, - 0x56, 0x05, 0xb7, 0x79, 0xfd, 0x8a, 0xc6, 0xc2, 0x15, 0xcd, 0xcd, 0xae, 0xa4, 0xd7, 0xed, 0x3b, 0x15, 0xf5, 0x7e, - 0x5e, 0x73, 0x57, 0x29, 0x81, 0xd4, 0x45, 0x9b, 0xdf, 0x4b, 0xb9, 0x2e, 0xdf, 0x7e, 0xcf, 0x1d, 0x5b, 0x80, 0x69, - 0xaf, 0xd6, 0x12, 0x85, 0x50, 0xeb, 0x39, 0xf9, 0xae, 0x34, 0x99, 0xfc, 0xd9, 0x4c, 0x54, 0x44, 0xbd, 0xe3, 0x96, - 0xd4, 0x74, 0x81, 0x7b, 0x88, 0x94, 0x0e, 0x99, 0x41, 0xa1, 0x2a, 0xa9, 0xad, 0x20, 0x7f, 0xa9, 0xdc, 0x0a, 0xf8, - 0x56, 0x78, 0xff, 0xff, 0x01, 0xa2, 0x89, 0x8c, 0x0d, 0xc4, 0x97, 0x00, 0x00}; + 0x95, 0xd2, 0x3b, 0x9e, 0x56, 0x41, 0xb0, 0xd5, 0x38, 0x5f, 0xb3, 0x03, 0xbe, 0xb0, 0x91, 0x90, 0xcf, 0x39, 0xce, + 0x08, 0x48, 0xd1, 0xee, 0xc0, 0x3e, 0xce, 0xaf, 0x26, 0x7d, 0x1b, 0x62, 0x34, 0x29, 0xf9, 0x20, 0x5c, 0x43, 0x50, + 0x21, 0x22, 0xed, 0x5e, 0x74, 0x4c, 0x7b, 0x51, 0xa3, 0xa1, 0xb5, 0x68, 0x9f, 0x24, 0xc3, 0x48, 0x36, 0x0f, 0x70, + 0x88, 0xe7, 0xa4, 0xd9, 0xc1, 0x33, 0xd2, 0x16, 0x4d, 0x7a, 0xb3, 0x63, 0x5f, 0x0d, 0xb3, 0xb7, 0xe7, 0xa4, 0x6e, + 0xec, 0xe7, 0xfc, 0x05, 0xd8, 0xfb, 0x64, 0x86, 0x43, 0x92, 0xba, 0xf4, 0x86, 0x06, 0x8e, 0x8f, 0x70, 0xa8, 0x38, + 0x0d, 0xea, 0xa1, 0x19, 0x31, 0xaa, 0x81, 0x19, 0x41, 0x3e, 0x0c, 0xc2, 0x61, 0x67, 0x44, 0x08, 0xb1, 0x77, 0x9a, + 0x4d, 0x7b, 0x90, 0x92, 0x09, 0xf7, 0xa0, 0xc4, 0x50, 0x96, 0xc9, 0x14, 0x8a, 0xba, 0x46, 0x91, 0xf3, 0x86, 0xbb, + 0x9c, 0xe6, 0xdc, 0x81, 0x62, 0xf0, 0x00, 0xe4, 0x9a, 0xb0, 0xed, 0xe3, 0x96, 0xdd, 0x80, 0x52, 0x41, 0x9c, 0x08, + 0xa7, 0xe4, 0x1a, 0x79, 0xe1, 0x70, 0x7f, 0x64, 0x0a, 0x00, 0x51, 0x08, 0x83, 0x5f, 0x0f, 0xc2, 0x61, 0x5b, 0x0c, + 0xde, 0xb7, 0x07, 0x4e, 0x4a, 0x72, 0xa9, 0xa1, 0x0d, 0x72, 0xef, 0x83, 0x98, 0x2a, 0xf2, 0x14, 0x70, 0x6a, 0xdc, + 0x39, 0x69, 0x76, 0x3d, 0x67, 0x6e, 0x4e, 0xa2, 0x09, 0x83, 0x29, 0x2c, 0xe0, 0x80, 0x40, 0x7d, 0x9c, 0x12, 0x18, + 0xb1, 0x6a, 0x76, 0xed, 0xa9, 0xe7, 0x07, 0xf6, 0x83, 0xc1, 0x39, 0xf7, 0xc6, 0x5c, 0x0e, 0x7f, 0xce, 0x97, 0x4b, + 0xf8, 0x77, 0xcc, 0x07, 0x29, 0xb9, 0x16, 0x45, 0x13, 0x55, 0x34, 0x85, 0xa2, 0x0f, 0x1e, 0x80, 0x8a, 0xf3, 0x52, + 0xcb, 0x92, 0x6b, 0x32, 0x25, 0x02, 0xf6, 0xbd, 0xbd, 0x64, 0x18, 0x35, 0x3a, 0x23, 0x70, 0xf2, 0x67, 0x3c, 0xff, + 0x8e, 0xf1, 0xc8, 0xb1, 0x5b, 0x7d, 0x1b, 0x0d, 0x6c, 0x0b, 0x96, 0xb6, 0x97, 0x35, 0x88, 0xc4, 0xb0, 0xdf, 0x78, + 0xc5, 0xbd, 0x79, 0x9f, 0xb4, 0x07, 0x0e, 0x53, 0x2e, 0x3d, 0x84, 0x7d, 0xc5, 0x38, 0xdb, 0x78, 0x8e, 0x1a, 0x8c, + 0x37, 0xf4, 0xf3, 0x1c, 0x35, 0x6e, 0x1b, 0x53, 0xe4, 0xf9, 0x8d, 0xdb, 0x86, 0x33, 0x27, 0x84, 0x34, 0xbb, 0x65, + 0x33, 0x2d, 0xfe, 0x22, 0xe4, 0x4d, 0xb5, 0xbf, 0x73, 0x28, 0xb6, 0x43, 0xd6, 0x70, 0x92, 0x21, 0x1d, 0x2d, 0x97, + 0xf6, 0xf1, 0xa0, 0x6f, 0xa3, 0x86, 0xa3, 0x09, 0xad, 0xa5, 0x29, 0x0d, 0x21, 0xcc, 0x46, 0x85, 0x8a, 0x27, 0x3d, + 0xa9, 0xc5, 0x8e, 0x16, 0xd5, 0x66, 0x37, 0x78, 0x00, 0x2d, 0x4a, 0x43, 0x46, 0x2a, 0xac, 0x33, 0x98, 0xa6, 0x26, + 0xe6, 0x8c, 0xb4, 0x71, 0x4a, 0xb4, 0xfb, 0x3a, 0x22, 0xbc, 0x22, 0x78, 0x9f, 0x54, 0xd5, 0xf1, 0x30, 0xc0, 0xe1, + 0x88, 0x3c, 0x95, 0x06, 0x49, 0x4f, 0x3b, 0xc7, 0x69, 0x4c, 0x9e, 0xac, 0x44, 0x71, 0x03, 0x08, 0xb0, 0xdc, 0xb8, + 0xc1, 0x3c, 0xcb, 0x68, 0xc2, 0x5f, 0xa7, 0xa1, 0xd2, 0xd3, 0x68, 0x0c, 0xa6, 0x12, 0x84, 0x67, 0x31, 0x28, 0x69, + 0x5d, 0xbd, 0x33, 0xe6, 0x6b, 0xaf, 0x67, 0x64, 0x2e, 0xf5, 0x27, 0x11, 0xb4, 0xed, 0xcd, 0x94, 0x65, 0xec, 0x20, + 0x3c, 0x57, 0xd1, 0x5c, 0xc7, 0x75, 0xdd, 0x99, 0x1b, 0xc0, 0x6b, 0x18, 0x20, 0x47, 0x85, 0xd8, 0x47, 0x4e, 0x4e, + 0x6e, 0xdc, 0x84, 0xde, 0x88, 0x51, 0x1d, 0x54, 0x49, 0x66, 0xbd, 0xbd, 0x8e, 0xa3, 0x9e, 0x60, 0x37, 0xb9, 0x9b, + 0xa4, 0x21, 0x05, 0xf4, 0x40, 0xfc, 0x5e, 0x15, 0x45, 0x7e, 0x6e, 0x06, 0xa9, 0x2a, 0xf8, 0x86, 0xa6, 0xff, 0x7a, + 0x06, 0x4e, 0x5f, 0xa1, 0x6c, 0x95, 0x95, 0xa5, 0x27, 0x1c, 0x21, 0x36, 0x76, 0x66, 0x2e, 0x04, 0xf7, 0x04, 0x09, + 0x31, 0xb0, 0xe5, 0x66, 0x26, 0x51, 0xdd, 0x96, 0x7d, 0x4e, 0x49, 0x38, 0x4c, 0x1b, 0x0d, 0xe1, 0x88, 0x9e, 0x4b, + 0x92, 0x98, 0x21, 0x3c, 0x2d, 0xf7, 0x96, 0xae, 0xf7, 0x96, 0xd4, 0x47, 0x72, 0xa6, 0x75, 0x87, 0x6e, 0x83, 0x71, + 0x24, 0x7c, 0x85, 0xdc, 0xb9, 0x45, 0x78, 0x4c, 0x5a, 0xce, 0xd0, 0x1d, 0xfc, 0x79, 0x84, 0x06, 0x8e, 0xfb, 0x15, + 0x6a, 0x49, 0xc6, 0x31, 0x45, 0x3d, 0x5f, 0x0e, 0xb1, 0x10, 0x51, 0xcc, 0x0e, 0x16, 0xbe, 0x44, 0x2f, 0xc3, 0x89, + 0x3f, 0xa5, 0xde, 0x18, 0xf6, 0xb8, 0xa6, 0x9b, 0xb7, 0x18, 0xe8, 0xc8, 0x1b, 0x2b, 0x4e, 0xe2, 0xda, 0x83, 0x5f, + 0x78, 0xf9, 0x34, 0xb0, 0x07, 0x5f, 0x57, 0x4f, 0x7f, 0xb6, 0x07, 0xdf, 0x72, 0xef, 0xdb, 0x42, 0xb9, 0xbb, 0x6b, + 0x43, 0x3c, 0xd4, 0x43, 0x14, 0x72, 0x61, 0x0c, 0xcc, 0xcd, 0xd1, 0xba, 0xa3, 0x63, 0x86, 0x0a, 0x36, 0x2e, 0x59, + 0x51, 0xee, 0x72, 0x7f, 0x02, 0x28, 0x35, 0x56, 0x20, 0x37, 0xa3, 0xfb, 0xd5, 0x84, 0x81, 0x50, 0x34, 0xb5, 0x02, + 0x2a, 0x67, 0xfd, 0x36, 0x5a, 0xd4, 0xea, 0x0a, 0x8d, 0xa9, 0x1e, 0x4d, 0x2f, 0xb9, 0xf4, 0x94, 0xb4, 0x7b, 0xd3, + 0xe3, 0x59, 0x6f, 0xda, 0x68, 0xa0, 0x5c, 0x13, 0xd6, 0x7c, 0x38, 0x1d, 0xe1, 0xd7, 0xe0, 0xd5, 0x33, 0x29, 0x09, + 0xd7, 0xa6, 0xd7, 0x55, 0xd3, 0x6b, 0x34, 0xb2, 0x02, 0xf5, 0x8c, 0xa6, 0x33, 0xd9, 0xb4, 0x28, 0x24, 0x4e, 0x56, + 0x09, 0xed, 0x08, 0x89, 0x12, 0x48, 0x89, 0x22, 0x84, 0x9c, 0x71, 0xb4, 0xb1, 0x57, 0xe8, 0x13, 0x9a, 0x8b, 0x1d, + 0x0b, 0xcc, 0x53, 0xca, 0x08, 0x07, 0xb0, 0x00, 0x4d, 0x4b, 0x57, 0xf0, 0x2d, 0x9e, 0x37, 0x3a, 0x82, 0xc8, 0x9b, + 0x9d, 0x5e, 0xbd, 0xaf, 0x47, 0x55, 0x5f, 0x78, 0xde, 0x20, 0xb7, 0x25, 0x96, 0x8a, 0xac, 0xd1, 0x28, 0xea, 0xf1, + 0x4e, 0xbd, 0x6f, 0x6b, 0x11, 0x88, 0x93, 0xd5, 0xd4, 0x0c, 0x2d, 0x5f, 0x2b, 0x89, 0xca, 0x5c, 0x96, 0x24, 0x34, + 0x03, 0x19, 0x4a, 0x38, 0x66, 0x45, 0x51, 0xca, 0xf5, 0x37, 0x20, 0x44, 0x31, 0x25, 0x09, 0xf0, 0x1d, 0x61, 0x76, + 0xe1, 0x0c, 0xa7, 0x38, 0x12, 0x5c, 0x83, 0x10, 0x72, 0xaa, 0x93, 0x5a, 0xb8, 0xe0, 0x40, 0x3e, 0x61, 0x86, 0x44, + 0xca, 0x09, 0x75, 0xcf, 0x77, 0x4f, 0xd3, 0x3b, 0x4d, 0xb2, 0x21, 0x1b, 0x79, 0xa2, 0x5a, 0xac, 0xf8, 0x56, 0x40, + 0xde, 0x39, 0x1c, 0x95, 0xe1, 0x11, 0x57, 0xb0, 0xbf, 0xa7, 0x2c, 0xa3, 0x42, 0x03, 0xdf, 0xd5, 0x66, 0x9f, 0x5f, + 0x57, 0x1f, 0x7d, 0xd3, 0x79, 0x03, 0x88, 0x0c, 0xc0, 0xb7, 0x93, 0x91, 0xb5, 0x6a, 0xe7, 0xbb, 0x27, 0x6f, 0x36, + 0x99, 0xc0, 0xcb, 0xa5, 0x32, 0x7e, 0x7d, 0xd0, 0x6c, 0x70, 0x50, 0x41, 0xea, 0xab, 0x1f, 0x9e, 0xe3, 0x0b, 0x05, + 0x29, 0x70, 0x12, 0xa0, 0xa2, 0xf3, 0xdd, 0x93, 0xf7, 0x4e, 0x22, 0x5c, 0x4b, 0x08, 0x9b, 0xd3, 0x76, 0x52, 0xe2, + 0x44, 0x84, 0x22, 0x39, 0xf7, 0x92, 0x71, 0xa5, 0x86, 0xf8, 0xf6, 0x22, 0xf1, 0x12, 0xec, 0x87, 0x21, 0x1b, 0x11, + 0x5f, 0x61, 0x80, 0xf8, 0x08, 0xfb, 0x35, 0xb3, 0x8c, 0xc0, 0x02, 0x88, 0xb1, 0xce, 0x60, 0x25, 0x5c, 0xa9, 0xf8, + 0x21, 0xec, 0x8b, 0x51, 0x79, 0x21, 0x45, 0xc7, 0xcf, 0x6b, 0xb9, 0x69, 0x95, 0x35, 0xfa, 0x2d, 0x58, 0x4e, 0xfa, + 0xe1, 0xb5, 0xea, 0xba, 0x2c, 0x78, 0xaa, 0x93, 0xc8, 0xce, 0x77, 0x4f, 0x5e, 0xa9, 0x3c, 0xb2, 0x99, 0xaf, 0xb9, + 0xfd, 0x9a, 0x85, 0x79, 0xf2, 0xca, 0xad, 0xde, 0x8a, 0xca, 0xe7, 0xbb, 0x27, 0x1f, 0x36, 0x55, 0x83, 0xf2, 0x62, + 0x5e, 0x99, 0xf8, 0x02, 0xbe, 0x05, 0x8d, 0xbd, 0x85, 0x12, 0x0d, 0x1e, 0x2b, 0xb0, 0x10, 0x47, 0x5e, 0x5e, 0x94, + 0x9e, 0x91, 0xa7, 0x38, 0x23, 0x22, 0x0e, 0x54, 0x5f, 0x35, 0xa5, 0xe4, 0xb1, 0x34, 0x39, 0x0b, 0xd2, 0x19, 0xdd, + 0x12, 0x1c, 0x3a, 0x41, 0x2e, 0x9b, 0x42, 0x02, 0x8d, 0x00, 0x9d, 0xe1, 0x9d, 0x36, 0xea, 0xd5, 0x85, 0x57, 0x26, + 0x88, 0x34, 0xad, 0x49, 0x16, 0x1c, 0x91, 0x36, 0xf6, 0x49, 0x1b, 0x07, 0x24, 0x1f, 0xb6, 0xa5, 0x78, 0xe8, 0x05, + 0x65, 0xbf, 0x52, 0xc8, 0x40, 0x6e, 0x58, 0x20, 0x77, 0xab, 0x14, 0xbf, 0x61, 0x2f, 0x10, 0xae, 0x47, 0x21, 0xd1, + 0x43, 0x69, 0xb4, 0x3a, 0x29, 0x4e, 0x45, 0xc7, 0x67, 0xec, 0x32, 0x86, 0xec, 0x12, 0x98, 0x15, 0xe6, 0xc8, 0x2b, + 0xab, 0x76, 0x54, 0xd5, 0xc0, 0x15, 0xeb, 0x94, 0xe2, 0xc0, 0x05, 0xc6, 0x8d, 0x03, 0x95, 0x8c, 0x93, 0xaf, 0x37, + 0x79, 0xb8, 0xb7, 0xe7, 0xc8, 0x46, 0xdf, 0x71, 0x27, 0xd5, 0xef, 0xab, 0xd0, 0xdd, 0xb7, 0x92, 0x57, 0x84, 0x48, + 0xc0, 0xdf, 0x68, 0xf8, 0xa3, 0x02, 0xe2, 0xd0, 0x4e, 0x50, 0xc7, 0xa0, 0x06, 0x5e, 0x68, 0x7a, 0xf5, 0xe9, 0x37, + 0x1a, 0x65, 0x98, 0xb6, 0x8e, 0xad, 0x13, 0x9c, 0x15, 0x57, 0x4e, 0x99, 0xff, 0xd3, 0x5e, 0xcb, 0x9a, 0xd2, 0x20, + 0x20, 0x66, 0xd2, 0x2c, 0xd3, 0x93, 0x31, 0xb6, 0x04, 0x83, 0x7a, 0x2f, 0x54, 0xe2, 0x02, 0x16, 0x39, 0x56, 0xaa, + 0x92, 0x66, 0x67, 0x5d, 0xe4, 0xe9, 0x4a, 0x10, 0x96, 0x82, 0x4a, 0x8d, 0x42, 0x91, 0xf7, 0xab, 0xf5, 0xcc, 0x4b, + 0x9c, 0x23, 0xe5, 0xe3, 0x12, 0x50, 0x08, 0x64, 0x75, 0x4b, 0xa4, 0x3c, 0x27, 0x93, 0xed, 0x24, 0x7f, 0x62, 0x90, + 0xfc, 0x13, 0x42, 0x0d, 0xf2, 0x97, 0x1e, 0x0e, 0x37, 0x55, 0xae, 0x85, 0x5c, 0xbf, 0x3a, 0x9d, 0x11, 0xf0, 0xa1, + 0xd5, 0x31, 0x5a, 0x8b, 0x2b, 0x6e, 0x61, 0x28, 0xe6, 0x0e, 0x11, 0x5e, 0x48, 0xac, 0x83, 0xc0, 0x4e, 0x15, 0x55, + 0x83, 0xa1, 0x37, 0xb9, 0xf4, 0x4c, 0x0e, 0x78, 0xf2, 0xe1, 0xee, 0x80, 0xe8, 0xe9, 0x6c, 0x7d, 0xe7, 0x1a, 0x19, + 0xa0, 0x30, 0x6b, 0x63, 0xe3, 0xd6, 0xf3, 0x41, 0x61, 0xfc, 0x32, 0x90, 0x5d, 0x67, 0x3e, 0x2b, 0x9b, 0x50, 0xcb, + 0x3f, 0x80, 0xb6, 0xd3, 0x11, 0x35, 0xa8, 0xd1, 0x2d, 0xf0, 0x23, 0x99, 0x87, 0xea, 0x67, 0x5b, 0xd8, 0xc7, 0x89, + 0xa8, 0x40, 0x93, 0x70, 0xf3, 0xeb, 0x27, 0x85, 0x22, 0x13, 0x09, 0x1a, 0x5a, 0x00, 0xff, 0x93, 0x24, 0x0f, 0x74, + 0x23, 0xe4, 0x02, 0x20, 0x68, 0x22, 0xf0, 0x54, 0x21, 0xcc, 0xb6, 0x2b, 0xe7, 0xfb, 0xf3, 0x1d, 0x42, 0x26, 0x95, + 0xf3, 0xf1, 0x5d, 0x95, 0x7d, 0x05, 0x64, 0x81, 0x3c, 0x30, 0x1e, 0xcb, 0x02, 0x19, 0xbf, 0x3c, 0xd5, 0xd5, 0x85, + 0x01, 0xe9, 0x56, 0xfa, 0xb6, 0x11, 0xdb, 0x14, 0x5e, 0x39, 0xf9, 0x5e, 0xa3, 0x61, 0xe5, 0xed, 0x2e, 0xbc, 0x7d, + 0xc9, 0x05, 0x8c, 0xf0, 0xfc, 0x5e, 0xd4, 0xd6, 0xfd, 0x16, 0x1f, 0x57, 0x53, 0x58, 0x56, 0x16, 0xc5, 0x65, 0x49, + 0x4e, 0x33, 0xfe, 0x84, 0x8e, 0xd3, 0x0c, 0x42, 0x16, 0x25, 0x4e, 0x50, 0xb1, 0x6b, 0xb8, 0xed, 0xc4, 0xfc, 0x8c, + 0x38, 0xc1, 0xca, 0x04, 0xc5, 0xaf, 0x8f, 0x22, 0x6a, 0x7d, 0xbe, 0xda, 0x6a, 0xb2, 0xb7, 0xf7, 0xae, 0x42, 0x93, + 0x82, 0x52, 0x40, 0x61, 0x30, 0x2d, 0xa9, 0xd2, 0xa8, 0x50, 0xee, 0xae, 0x53, 0xba, 0x00, 0x34, 0xc3, 0x30, 0x79, + 0xcf, 0x73, 0xc2, 0x8b, 0xc9, 0x2a, 0x8b, 0x57, 0xae, 0x09, 0x66, 0x9a, 0x2d, 0xc0, 0xe1, 0xc1, 0xd0, 0x96, 0xbe, + 0xa2, 0xbc, 0x4a, 0x89, 0x2d, 0x61, 0x38, 0x05, 0x64, 0x39, 0xc2, 0x08, 0x31, 0x28, 0x70, 0xa3, 0x51, 0xf2, 0x16, + 0xf4, 0xca, 0x08, 0xe7, 0x6e, 0x04, 0x49, 0xb0, 0xb5, 0x2d, 0x8b, 0x10, 0x96, 0x99, 0x39, 0x46, 0x2e, 0xc1, 0xc9, + 0xf3, 0x4d, 0x1e, 0x65, 0x4d, 0xd4, 0x54, 0x48, 0x1d, 0xa8, 0x91, 0xa1, 0xb2, 0x81, 0x7b, 0xe5, 0x30, 0xa5, 0xb8, + 0xe9, 0xb8, 0x19, 0x30, 0xe0, 0x9f, 0xb9, 0x23, 0x63, 0x51, 0x20, 0x33, 0x52, 0x77, 0xee, 0xd4, 0x86, 0xee, 0xa5, + 0xa2, 0x19, 0x56, 0x88, 0x8b, 0x4c, 0x34, 0xa5, 0x22, 0xae, 0x77, 0x5a, 0xf1, 0xd2, 0x2b, 0x99, 0x47, 0xcd, 0x35, + 0x17, 0xac, 0x32, 0x49, 0x8c, 0xe9, 0x5f, 0xc9, 0xd4, 0xe8, 0xb2, 0x12, 0xa8, 0x61, 0xf4, 0xda, 0x7a, 0x22, 0xd6, + 0x80, 0x16, 0x40, 0x5f, 0x8b, 0x53, 0x6e, 0xac, 0xa8, 0xf6, 0x61, 0x8b, 0x31, 0x0d, 0xa9, 0xff, 0x0e, 0x72, 0x5d, + 0x56, 0xf7, 0xfc, 0x73, 0x21, 0x0b, 0x19, 0xce, 0x6b, 0x8c, 0x3d, 0x13, 0x8c, 0x1d, 0x81, 0x9e, 0xa6, 0xd3, 0xbf, + 0x07, 0x2a, 0xe5, 0x45, 0xe5, 0x2e, 0x3a, 0x8a, 0xc4, 0x5e, 0x97, 0xe1, 0x72, 0xe3, 0xf7, 0xca, 0x6a, 0x78, 0x8c, + 0x40, 0x1a, 0x10, 0x56, 0x9c, 0x3d, 0x43, 0x38, 0x6f, 0x34, 0x7a, 0xf9, 0x31, 0xad, 0x5c, 0x24, 0x15, 0x8c, 0x0c, + 0x22, 0xba, 0x40, 0xf0, 0x35, 0x19, 0x0a, 0x31, 0x7f, 0x9d, 0x9f, 0x9d, 0x83, 0xab, 0xfd, 0xe4, 0x9d, 0x63, 0x72, + 0x35, 0xb3, 0x6e, 0x19, 0x34, 0x85, 0xf9, 0x38, 0x55, 0xbc, 0xe5, 0xed, 0xdd, 0x19, 0x1e, 0x00, 0xf7, 0x4e, 0x07, + 0x43, 0x36, 0x1a, 0xea, 0x71, 0xc9, 0x12, 0xca, 0xdd, 0xd7, 0x43, 0x55, 0x62, 0xa2, 0x39, 0x58, 0x8f, 0x57, 0xa6, + 0x2c, 0x27, 0x79, 0x51, 0xe4, 0xb4, 0x8a, 0xef, 0xaf, 0x64, 0x60, 0x0a, 0xe1, 0xb2, 0xee, 0x6c, 0x3f, 0x9d, 0x11, + 0x8e, 0x0d, 0x42, 0x7d, 0xbb, 0x2d, 0xf4, 0x51, 0x81, 0x09, 0xfb, 0x5a, 0x09, 0xc5, 0x6f, 0x37, 0x09, 0x45, 0x9c, + 0xa9, 0x2d, 0x2f, 0x04, 0x62, 0xe7, 0x1e, 0x02, 0x51, 0x39, 0xd9, 0xb5, 0x4c, 0x04, 0x75, 0xa4, 0x26, 0x13, 0xeb, + 0x4b, 0x4a, 0x32, 0xcc, 0xd4, 0x6a, 0xf4, 0xbb, 0xcb, 0x25, 0x1b, 0xb6, 0xc1, 0x89, 0x64, 0xdb, 0xf0, 0xb3, 0x23, + 0x7f, 0x1a, 0x9c, 0x58, 0x3a, 0x81, 0x1d, 0x56, 0x9a, 0x2c, 0xc8, 0x85, 0x34, 0x67, 0x47, 0x64, 0x65, 0x09, 0x9a, + 0x56, 0x14, 0xa4, 0x08, 0x9c, 0xb0, 0x32, 0xca, 0x04, 0x10, 0x0b, 0x59, 0xa1, 0x0c, 0x48, 0x67, 0x63, 0xfa, 0x9f, + 0x36, 0x2f, 0x3f, 0xad, 0x89, 0xd6, 0xe4, 0x8a, 0x54, 0x1f, 0x6a, 0x09, 0x07, 0x0a, 0x02, 0xa5, 0x1f, 0xee, 0x08, + 0x13, 0xb4, 0x12, 0xe5, 0xc8, 0x94, 0x43, 0xb8, 0x0d, 0x2e, 0xb4, 0x9d, 0x77, 0x32, 0xc0, 0xbb, 0x41, 0x9a, 0xe0, + 0xd4, 0xa0, 0xeb, 0xe7, 0x84, 0xd7, 0x58, 0x49, 0x44, 0x94, 0xa5, 0x84, 0x03, 0x41, 0xa6, 0x9c, 0x64, 0xc3, 0xf6, + 0x08, 0x14, 0xd0, 0x9e, 0x7f, 0x9c, 0x55, 0x26, 0xb0, 0xdf, 0x68, 0xa0, 0x40, 0x8f, 0x1a, 0x0d, 0x59, 0xc3, 0x1f, + 0x61, 0x8a, 0x7d, 0x69, 0x98, 0x9c, 0xee, 0xed, 0x39, 0x41, 0x35, 0xee, 0xd0, 0x1f, 0x21, 0x9c, 0x2e, 0x97, 0x8e, + 0x00, 0x2b, 0x40, 0xcb, 0x65, 0x60, 0x82, 0x25, 0x5e, 0x43, 0xb3, 0xc9, 0x80, 0x93, 0x89, 0x10, 0x80, 0x13, 0x80, + 0xb0, 0x41, 0x9c, 0x40, 0x39, 0xf7, 0x02, 0x70, 0x46, 0x35, 0xb2, 0xa1, 0xdf, 0xe8, 0x8c, 0x0c, 0xc6, 0x35, 0xf4, + 0x47, 0x24, 0x28, 0xd2, 0xbd, 0xbd, 0x9d, 0x5c, 0x89, 0xc8, 0x9f, 0x41, 0x94, 0xfd, 0x2c, 0x24, 0x8b, 0xec, 0xd0, + 0x5c, 0x8d, 0x55, 0x67, 0x40, 0x49, 0x51, 0x6a, 0x59, 0x75, 0xbd, 0x5a, 0x16, 0x44, 0x59, 0x09, 0xab, 0x58, 0xf0, + 0x00, 0x2c, 0xfb, 0x92, 0xcc, 0x7f, 0xe1, 0x65, 0x9a, 0xf5, 0xb7, 0x1b, 0x93, 0xab, 0x5d, 0xd7, 0xf5, 0xb3, 0x89, + 0x88, 0x64, 0xe8, 0x28, 0xac, 0x20, 0xfe, 0x7d, 0x05, 0xa6, 0x31, 0xf0, 0xb0, 0x1c, 0x6b, 0x44, 0x24, 0xf8, 0x5a, + 0xb5, 0xd1, 0x27, 0x4a, 0x7e, 0xdd, 0xe8, 0x65, 0x90, 0x90, 0x7c, 0xfd, 0x5b, 0x21, 0x39, 0x50, 0x90, 0x48, 0xf2, + 0x58, 0xc1, 0xd9, 0x16, 0x5c, 0xfc, 0xca, 0x57, 0x70, 0xb6, 0x1d, 0xb7, 0x25, 0x43, 0xd8, 0x06, 0x9f, 0xc1, 0x1b, + 0x24, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0xe1, 0xaa, 0xee, 0x25, 0x59, 0x29, 0x08, 0x53, 0x4e, 0x1c, 0x56, 0xdf, 0x00, + 0x95, 0x36, 0x6a, 0x18, 0xbe, 0xcc, 0x9b, 0x20, 0xc3, 0x25, 0x50, 0x4f, 0x5d, 0x01, 0x72, 0x52, 0xbe, 0x76, 0x48, + 0x45, 0xd8, 0x91, 0x4a, 0x9c, 0x1b, 0xf8, 0x33, 0x3e, 0xcf, 0x40, 0x95, 0xca, 0xf5, 0x6f, 0x28, 0x86, 0xb3, 0x20, + 0xa2, 0x0c, 0x7e, 0x40, 0xc1, 0xcc, 0xcf, 0x73, 0x76, 0x25, 0xcb, 0xd4, 0x6f, 0x9c, 0x12, 0x4d, 0xca, 0xb9, 0xd4, + 0x09, 0x33, 0xd4, 0xcb, 0x14, 0x9d, 0xd6, 0xd1, 0xf6, 0xec, 0x8a, 0x26, 0xfc, 0x25, 0xcb, 0x39, 0x4d, 0x60, 0xfa, + 0x15, 0xc5, 0xc1, 0x8c, 0x72, 0x04, 0x1b, 0xb6, 0xd6, 0xca, 0x0f, 0xc3, 0x3b, 0x9b, 0xf0, 0xba, 0x0e, 0x14, 0xf9, + 0x49, 0x18, 0xcb, 0x41, 0xcc, 0x84, 0x46, 0x9d, 0xc4, 0x59, 0xd6, 0x34, 0xf3, 0x69, 0x2a, 0x65, 0x43, 0x70, 0x77, + 0x87, 0x11, 0x2d, 0x09, 0xb4, 0xf4, 0xbc, 0x53, 0x6b, 0x81, 0x80, 0xf7, 0x96, 0x45, 0x30, 0x67, 0x82, 0xb9, 0xc1, + 0x51, 0xdd, 0x3a, 0x9c, 0x9a, 0x6e, 0xbe, 0xdb, 0x78, 0xb0, 0x6d, 0x93, 0x70, 0x10, 0x74, 0xf2, 0x70, 0xbb, 0x65, + 0xf5, 0x4a, 0x4b, 0x0e, 0x2d, 0x2d, 0xd8, 0x7d, 0x19, 0x33, 0x5a, 0x68, 0xf2, 0x42, 0x7a, 0x2b, 0xee, 0x72, 0xf2, + 0x0b, 0x9c, 0x1c, 0x7a, 0xce, 0xa7, 0xf1, 0xca, 0x01, 0x99, 0xde, 0x6e, 0xa9, 0xfd, 0xef, 0x72, 0xe7, 0x09, 0x7e, + 0x05, 0x61, 0xdd, 0x6f, 0xaa, 0xea, 0xeb, 0xe1, 0xdc, 0x6f, 0x2a, 0x04, 0x7d, 0xe3, 0xad, 0xd5, 0x33, 0xc2, 0xb8, + 0x5d, 0xf7, 0xc8, 0x6d, 0xdb, 0x5a, 0x5b, 0xfa, 0x51, 0x06, 0x91, 0x64, 0xaa, 0xa5, 0xd8, 0x0f, 0xb8, 0x4a, 0x54, + 0x83, 0x84, 0xb9, 0xba, 0x85, 0x44, 0x55, 0x8a, 0xa1, 0xd4, 0xe1, 0xb7, 0x2d, 0x8f, 0x92, 0x31, 0x99, 0xb4, 0x33, + 0xde, 0xfa, 0x19, 0xdf, 0x85, 0x5d, 0x96, 0xae, 0x9d, 0xc6, 0x8b, 0x08, 0x78, 0xd0, 0xee, 0x37, 0x84, 0x61, 0x6c, + 0xe7, 0xf2, 0x30, 0x90, 0xd9, 0x3f, 0x49, 0xb5, 0xee, 0x56, 0xb7, 0x32, 0x5e, 0x83, 0xfd, 0x8f, 0x70, 0xa4, 0x8f, + 0xc8, 0x51, 0xc5, 0x81, 0xa9, 0xb7, 0x28, 0x4a, 0xa7, 0x40, 0x2a, 0x95, 0xb7, 0x04, 0xe1, 0xb4, 0x10, 0xe1, 0xed, + 0xef, 0xf1, 0x0f, 0x8a, 0x25, 0x9e, 0x97, 0x1c, 0xe7, 0xd9, 0x7d, 0x39, 0xa2, 0x04, 0xbf, 0x8c, 0xde, 0x03, 0x1d, + 0x0b, 0x0a, 0x2d, 0x34, 0x15, 0x3d, 0x4d, 0xd5, 0x44, 0xb6, 0xe6, 0xa5, 0x62, 0x5a, 0x66, 0xd4, 0x88, 0x61, 0x36, + 0x24, 0x72, 0x6a, 0x2b, 0x9b, 0x97, 0xbb, 0xaa, 0x36, 0x2e, 0xda, 0x82, 0xc5, 0x2a, 0xb0, 0xb8, 0x5c, 0x3a, 0x75, + 0x54, 0x13, 0x66, 0xc4, 0x31, 0x10, 0x66, 0x46, 0x42, 0x45, 0x4d, 0xb3, 0x96, 0x6d, 0x1c, 0xb4, 0x9a, 0x4f, 0xa4, + 0x75, 0xf3, 0x1a, 0x1c, 0xa6, 0x0b, 0x41, 0x36, 0x37, 0x7d, 0x0a, 0x58, 0xce, 0xae, 0x1c, 0xc8, 0xc0, 0xd0, 0x8f, + 0x65, 0xae, 0x6c, 0x95, 0xd4, 0xba, 0x01, 0xbf, 0xe8, 0x8e, 0x6c, 0x59, 0x85, 0xba, 0xf5, 0xf7, 0x46, 0xae, 0xd1, + 0xd3, 0x74, 0x5b, 0xae, 0x51, 0x4d, 0xdb, 0xdd, 0x69, 0xa3, 0xbb, 0xf3, 0x52, 0xe5, 0x58, 0x9b, 0xab, 0xfc, 0x86, + 0xe1, 0x3a, 0x40, 0x9b, 0x12, 0xcd, 0x9a, 0xab, 0x9c, 0x16, 0xc5, 0x79, 0x79, 0x9a, 0x40, 0xa4, 0xee, 0x9c, 0x4b, + 0xfa, 0x57, 0x56, 0xa3, 0x38, 0x94, 0xeb, 0x7c, 0x4f, 0x26, 0x71, 0x7a, 0xe9, 0xc7, 0xef, 0x61, 0xbc, 0xea, 0xe5, + 0xf3, 0xdb, 0x30, 0xf3, 0x39, 0x55, 0xdc, 0xa5, 0x82, 0xe1, 0x7b, 0x03, 0x86, 0xef, 0x25, 0x9f, 0xae, 0xda, 0xe3, + 0xc5, 0xcb, 0xb2, 0x03, 0xef, 0xbc, 0xd0, 0x2c, 0xe3, 0x96, 0x6f, 0x1e, 0x63, 0x95, 0x85, 0xdd, 0x96, 0x2c, 0xec, + 0x96, 0x3b, 0xab, 0x5d, 0x39, 0xce, 0x0f, 0x9b, 0x7b, 0x59, 0xe7, 0x6c, 0x3f, 0x54, 0x1b, 0xff, 0x07, 0xef, 0xce, + 0x36, 0x06, 0x97, 0xdb, 0x77, 0xf7, 0x45, 0xb2, 0x8a, 0x04, 0xf9, 0x25, 0x24, 0x1d, 0x70, 0xd2, 0x37, 0x0e, 0x1d, + 0x54, 0x72, 0x4a, 0xe7, 0x01, 0x39, 0xc1, 0x3c, 0xe7, 0xe9, 0x54, 0xf5, 0x99, 0xab, 0x93, 0x46, 0xe2, 0x25, 0xb8, + 0xa2, 0x45, 0xac, 0xdd, 0xab, 0x9f, 0xe5, 0x5a, 0x7c, 0x64, 0x49, 0xe8, 0xe5, 0x58, 0x49, 0x91, 0xdc, 0xcb, 0x0a, + 0xa2, 0xb3, 0x8d, 0xd7, 0xdf, 0xe1, 0x31, 0x4b, 0x58, 0x1e, 0xd1, 0xcc, 0x49, 0xd1, 0x62, 0xdb, 0x60, 0x29, 0x04, + 0x64, 0xe4, 0x60, 0xf8, 0xaf, 0xd5, 0xa9, 0x3f, 0x17, 0x7a, 0x03, 0x3f, 0xd0, 0x94, 0xf2, 0x28, 0x0d, 0x21, 0x2d, + 0xc5, 0x0d, 0xcb, 0x43, 0x4d, 0x7b, 0x7b, 0x3b, 0x8e, 0x2d, 0xdc, 0x12, 0x70, 0x00, 0xdc, 0x7c, 0x83, 0x06, 0x0b, + 0x38, 0x9f, 0x53, 0x0d, 0x4d, 0xd1, 0x82, 0xae, 0x1e, 0x65, 0xe1, 0xee, 0x47, 0x7a, 0x8b, 0x13, 0x54, 0x14, 0x9e, + 0x84, 0xda, 0x1e, 0x33, 0x1a, 0x87, 0x36, 0xfe, 0x48, 0x6f, 0xbd, 0xf2, 0xcc, 0xb8, 0x38, 0xe2, 0x2c, 0x16, 0xd0, + 0x4e, 0xaf, 0x13, 0x1b, 0x57, 0x83, 0x78, 0x8b, 0x02, 0xa7, 0x19, 0x9b, 0x00, 0x71, 0x7e, 0x43, 0x6f, 0x3d, 0xd9, + 0x1f, 0x33, 0xce, 0xeb, 0xa1, 0x85, 0x46, 0xbd, 0x6b, 0x14, 0x9b, 0xcb, 0xa0, 0x0c, 0x8a, 0xa1, 0x68, 0x3b, 0x22, + 0xb5, 0x7a, 0x95, 0x79, 0x88, 0x50, 0x71, 0xdf, 0xa9, 0xe0, 0x6f, 0x4c, 0xd1, 0xc6, 0x6b, 0x99, 0xaf, 0x2b, 0x8d, + 0x28, 0x34, 0xa8, 0x32, 0x3d, 0x76, 0x9d, 0x44, 0xef, 0x3a, 0x75, 0x08, 0xc1, 0x70, 0x84, 0x7d, 0xc3, 0x55, 0xa7, + 0xde, 0x5f, 0x65, 0x42, 0x48, 0x15, 0x49, 0x7a, 0x51, 0xb5, 0xb3, 0x76, 0x1d, 0xc0, 0x3b, 0x24, 0xb4, 0xf8, 0xe2, + 0x4c, 0x66, 0xa1, 0xb3, 0x45, 0xff, 0xc6, 0x89, 0xb3, 0xd0, 0x53, 0xf0, 0x12, 0x13, 0x8b, 0xbc, 0x00, 0x2a, 0x54, + 0xf4, 0x25, 0x13, 0x00, 0xd9, 0xd8, 0x61, 0x6b, 0x52, 0x33, 0x13, 0x52, 0xd3, 0x35, 0x30, 0xbe, 0x45, 0x4a, 0x52, + 0x81, 0x0c, 0xa1, 0x44, 0x0a, 0xa1, 0xa7, 0x16, 0x57, 0x91, 0x90, 0xb9, 0xa0, 0xe5, 0x09, 0x3a, 0xb9, 0xe6, 0x59, + 0x0d, 0x2c, 0x47, 0xf4, 0x83, 0x0a, 0x0f, 0xa6, 0x44, 0x65, 0x85, 0xa2, 0x3c, 0x9a, 0xad, 0xd3, 0x5b, 0x9d, 0xd4, + 0xd5, 0xd3, 0x22, 0x1a, 0x25, 0x4e, 0x84, 0x16, 0x89, 0x13, 0xe1, 0x0c, 0xd2, 0x11, 0xd3, 0xa2, 0x84, 0x9f, 0x9a, + 0xab, 0x51, 0x4b, 0x56, 0xde, 0x7c, 0xca, 0x0f, 0x94, 0x79, 0x0e, 0x29, 0x9a, 0x38, 0xd1, 0x3c, 0x25, 0x71, 0xc4, + 0x71, 0x3b, 0x63, 0xd9, 0xbe, 0x57, 0x09, 0x3a, 0x0a, 0xb0, 0xbf, 0x71, 0x67, 0x61, 0xcc, 0xc2, 0x3c, 0xd1, 0xad, + 0x4e, 0xfd, 0xa9, 0x60, 0x5f, 0x95, 0x43, 0xea, 0xe4, 0x64, 0x45, 0xe2, 0xdc, 0x9d, 0x6a, 0xf9, 0xcb, 0x9c, 0x66, + 0xb7, 0x67, 0x14, 0x52, 0x9d, 0x53, 0x38, 0xf0, 0x5b, 0x2d, 0x43, 0x95, 0xa7, 0x3e, 0xc8, 0x84, 0xb2, 0x52, 0xd4, + 0xcf, 0x01, 0xae, 0x9e, 0x12, 0x2c, 0x44, 0xb4, 0xd1, 0x70, 0xc4, 0xc8, 0xdd, 0x42, 0xb7, 0x9e, 0x9f, 0xa4, 0x3d, + 0x06, 0xfe, 0xb5, 0x0a, 0xd3, 0x2a, 0x58, 0x80, 0x53, 0xf3, 0x4c, 0xea, 0x30, 0x1f, 0xad, 0x7a, 0x65, 0xa0, 0x08, + 0xc2, 0x77, 0xd9, 0xf6, 0xa9, 0x6e, 0x4a, 0x9a, 0xdd, 0x3e, 0xd5, 0x5a, 0xd0, 0x4f, 0x24, 0xfc, 0x60, 0x35, 0x4e, + 0x79, 0x82, 0x99, 0x15, 0x05, 0x2a, 0x00, 0xbc, 0xbf, 0xf4, 0x1c, 0xe7, 0x2f, 0x2a, 0x65, 0xd0, 0x85, 0x58, 0xec, + 0x59, 0x9c, 0x6a, 0x26, 0x5e, 0x8d, 0xff, 0x97, 0xb5, 0xf1, 0xff, 0x62, 0x9c, 0x3a, 0x05, 0xd3, 0x68, 0x92, 0xd0, + 0x50, 0xb3, 0x4e, 0x24, 0x09, 0x50, 0xe8, 0x6d, 0x19, 0x27, 0x1f, 0x2f, 0x3c, 0xd0, 0xb8, 0x16, 0xe3, 0x34, 0xe1, + 0xcd, 0xb1, 0x3f, 0x65, 0xf1, 0xad, 0x37, 0x67, 0xcd, 0x69, 0x9a, 0xa4, 0xf9, 0xcc, 0x0f, 0x28, 0xce, 0x6f, 0x73, + 0x4e, 0xa7, 0xcd, 0x39, 0xc3, 0xcf, 0x69, 0x7c, 0x45, 0x39, 0x0b, 0x7c, 0x6c, 0x9f, 0x64, 0xcc, 0x8f, 0xad, 0xd7, + 0x7e, 0x96, 0xa5, 0xd7, 0x36, 0x7e, 0x97, 0x5e, 0xa6, 0x3c, 0xc5, 0x6f, 0x6e, 0x6e, 0x27, 0x34, 0xc1, 0x1f, 0x2e, + 0xe7, 0x09, 0x9f, 0xe3, 0xdc, 0x4f, 0xf2, 0x66, 0x4e, 0x33, 0x36, 0xee, 0x05, 0x69, 0x9c, 0x66, 0x4d, 0xc8, 0xd8, + 0x9e, 0x52, 0x2f, 0x66, 0x93, 0x88, 0x5b, 0xa1, 0x9f, 0x7d, 0xec, 0x35, 0x9b, 0xb3, 0x8c, 0x4d, 0xfd, 0xec, 0xb6, + 0x29, 0x6a, 0x78, 0x5f, 0xb6, 0xf7, 0xfd, 0xc7, 0xe3, 0x83, 0x1e, 0xcf, 0xfc, 0x24, 0x67, 0xb0, 0x4c, 0x9e, 0x1f, + 0xc7, 0xd6, 0xfe, 0x61, 0x7b, 0x9a, 0xef, 0xc8, 0x40, 0x9e, 0x9f, 0xf0, 0xe2, 0x02, 0xbf, 0x07, 0xb8, 0xdd, 0x4b, + 0x9e, 0xe0, 0xcb, 0x39, 0xe7, 0x69, 0xb2, 0x08, 0xe6, 0x59, 0x9e, 0x66, 0xde, 0x2c, 0x65, 0x09, 0xa7, 0x59, 0xef, + 0x32, 0xcd, 0x42, 0x9a, 0x35, 0x33, 0x3f, 0x64, 0xf3, 0xdc, 0x3b, 0x98, 0xdd, 0xf4, 0x40, 0xb3, 0x98, 0x64, 0xe9, + 0x3c, 0x09, 0xd5, 0x58, 0x2c, 0x89, 0x68, 0xc6, 0xb8, 0xf9, 0x42, 0x5c, 0x64, 0xe2, 0xc5, 0x2c, 0xa1, 0x7e, 0xd6, + 0x9c, 0x40, 0x63, 0x30, 0x8b, 0xda, 0x21, 0x9d, 0xe0, 0x6c, 0x72, 0xe9, 0x3b, 0x9d, 0xee, 0x23, 0xac, 0xff, 0x77, + 0x0f, 0x91, 0xd5, 0xde, 0x5c, 0xdc, 0x69, 0xb7, 0xff, 0x84, 0x7a, 0x2b, 0xa3, 0x08, 0x80, 0xbc, 0xce, 0xec, 0xc6, + 0xca, 0x53, 0xc8, 0x68, 0xdb, 0xd4, 0xb2, 0x37, 0xf3, 0x43, 0xc8, 0x07, 0xf6, 0xba, 0xb3, 0x9b, 0x02, 0x66, 0xe7, + 0xc9, 0x14, 0x53, 0x35, 0x49, 0xf5, 0xb4, 0xf8, 0xad, 0x10, 0x1f, 0x6d, 0x86, 0xb8, 0xab, 0x21, 0xae, 0xb0, 0xde, + 0x0c, 0xe7, 0x99, 0x88, 0xad, 0x7a, 0x9d, 0x5c, 0x02, 0x12, 0xa5, 0x57, 0x34, 0xd3, 0x70, 0x88, 0x87, 0xdf, 0x0c, + 0x46, 0x77, 0x33, 0x18, 0x47, 0x9f, 0x02, 0x23, 0x4b, 0xc2, 0x45, 0x7d, 0x5d, 0x3b, 0x19, 0x9d, 0xf6, 0x22, 0x0a, + 0xf4, 0xe4, 0x75, 0xe1, 0xf7, 0x35, 0x0b, 0x79, 0x24, 0x7f, 0x0a, 0x72, 0xbe, 0x96, 0xef, 0x0e, 0xdb, 0x6d, 0xf9, + 0x9c, 0xb3, 0x5f, 0xa9, 0xd7, 0x71, 0xa1, 0x42, 0x71, 0x81, 0x7f, 0x28, 0x4f, 0xf3, 0xd6, 0xb9, 0x27, 0xfe, 0x8b, + 0x79, 0xcc, 0xd7, 0x48, 0x51, 0xac, 0x0e, 0x45, 0xe3, 0x54, 0xcb, 0x4a, 0x29, 0x7c, 0xc0, 0x6d, 0x27, 0xb8, 0x23, + 0x61, 0xfd, 0xf2, 0x18, 0x27, 0x1b, 0xfc, 0x45, 0xe6, 0x5d, 0x78, 0x10, 0xe9, 0x30, 0x52, 0x0d, 0xd3, 0x5e, 0xd6, + 0x27, 0xed, 0x5e, 0xd6, 0x6c, 0x22, 0x27, 0x25, 0xc9, 0x30, 0x53, 0xc9, 0x79, 0x0e, 0x1b, 0xa4, 0xc2, 0xd8, 0xce, + 0x91, 0x97, 0xc2, 0x59, 0xd3, 0xe5, 0xb2, 0x0a, 0x03, 0x30, 0x71, 0x5a, 0xe3, 0x07, 0xae, 0x2a, 0xe0, 0xdc, 0xe0, + 0xe4, 0xbe, 0xbe, 0xde, 0x25, 0xd1, 0xbc, 0x22, 0x4e, 0x03, 0x81, 0x39, 0x77, 0xe6, 0xf3, 0x08, 0xbc, 0x14, 0xa5, + 0xf8, 0xa9, 0x52, 0x98, 0xec, 0x96, 0x8d, 0x06, 0x49, 0x99, 0xdf, 0x06, 0x79, 0x7c, 0x49, 0x01, 0xbd, 0x5c, 0x72, + 0x02, 0x3d, 0x56, 0xfd, 0x7f, 0xe0, 0x86, 0xa4, 0x4e, 0x5c, 0x96, 0x04, 0xf1, 0x3c, 0xa4, 0xb9, 0xe8, 0xa1, 0x12, + 0xe7, 0x70, 0x37, 0x44, 0x59, 0x4b, 0x34, 0x81, 0xde, 0x45, 0x36, 0x0f, 0x54, 0x84, 0x5b, 0x54, 0xca, 0xe7, 0xa6, + 0x78, 0xae, 0xda, 0xbe, 0xae, 0x92, 0x45, 0xa1, 0xa5, 0x3b, 0x4f, 0xd8, 0x2f, 0x73, 0x7a, 0xce, 0x42, 0xe3, 0xe4, + 0x2e, 0x4d, 0x82, 0x34, 0xa4, 0x1f, 0xde, 0xbd, 0x80, 0x6c, 0xf7, 0x34, 0x01, 0x12, 0x4b, 0xa4, 0xbf, 0x0b, 0xe7, + 0x24, 0x71, 0x43, 0x7a, 0xc5, 0x02, 0x3a, 0xb8, 0xd8, 0x5d, 0x6c, 0xac, 0x28, 0x5f, 0xa3, 0xa2, 0x75, 0x21, 0x92, + 0xfe, 0x04, 0x94, 0x17, 0xbb, 0x8b, 0x4b, 0x5e, 0xb4, 0x76, 0x17, 0x89, 0x1b, 0xa6, 0x53, 0x9f, 0x25, 0xf0, 0x3b, + 0x2f, 0x76, 0x17, 0x0c, 0x7e, 0xf0, 0xe2, 0xa2, 0xa8, 0x12, 0x45, 0x4b, 0x88, 0x8c, 0x29, 0x28, 0xdc, 0x75, 0x90, + 0xfb, 0x73, 0xca, 0x12, 0x51, 0x74, 0x57, 0xcf, 0x54, 0xf7, 0x0a, 0x48, 0xfe, 0x95, 0x48, 0x83, 0x59, 0x9b, 0xcb, + 0xe7, 0xf7, 0x35, 0x97, 0x69, 0xc2, 0x99, 0x48, 0x8b, 0xd7, 0xe1, 0x9c, 0xc8, 0xcf, 0xcf, 0x03, 0x79, 0x12, 0x35, + 0xaf, 0x4e, 0x5d, 0xf8, 0x02, 0xb1, 0xd2, 0x02, 0xa6, 0x99, 0x30, 0xf6, 0xe9, 0xf6, 0xa3, 0x92, 0xc9, 0x5d, 0xc6, + 0x5f, 0x49, 0x55, 0x79, 0x3a, 0xcf, 0x02, 0x88, 0xf5, 0x2a, 0x95, 0x62, 0xdd, 0x2b, 0x66, 0x0b, 0xfd, 0xcd, 0xc6, + 0xdc, 0x48, 0xb2, 0xe5, 0x70, 0xa6, 0xaf, 0xba, 0xb6, 0x83, 0x8a, 0x78, 0x22, 0xac, 0x19, 0x13, 0xab, 0x77, 0xce, + 0x42, 0x08, 0xbc, 0xb0, 0x50, 0x25, 0x2c, 0xd6, 0x26, 0x09, 0x2a, 0x52, 0x28, 0x32, 0x48, 0xe1, 0xb2, 0x9d, 0xb4, + 0x5a, 0x05, 0x42, 0x88, 0x8c, 0xeb, 0x81, 0xf0, 0x6d, 0x76, 0xf6, 0xf6, 0xf2, 0xea, 0x44, 0x1b, 0x53, 0x38, 0x5f, + 0x2e, 0x39, 0x75, 0x72, 0x79, 0xea, 0x26, 0x22, 0xa0, 0x8c, 0x31, 0x2c, 0xdf, 0x78, 0x29, 0x2e, 0x7b, 0xf2, 0xf2, + 0xa2, 0x17, 0x09, 0x24, 0x4a, 0x94, 0x11, 0x8d, 0xd4, 0x13, 0xad, 0x92, 0x61, 0xf3, 0x75, 0x79, 0x90, 0xbf, 0x86, + 0xf5, 0xf6, 0xca, 0xe2, 0x48, 0xab, 0x2a, 0x5a, 0x2d, 0xcd, 0xd3, 0x8c, 0x3b, 0x8e, 0x8f, 0x03, 0x44, 0xfa, 0xbe, + 0x98, 0xfd, 0xb1, 0xcc, 0xf7, 0x18, 0x34, 0x3b, 0x5e, 0xa7, 0xf4, 0x87, 0xd4, 0xce, 0x57, 0xcb, 0x6c, 0x33, 0x75, + 0x46, 0x17, 0xf0, 0x84, 0xcb, 0xdf, 0x0a, 0x7d, 0x55, 0x81, 0x9c, 0x5d, 0xf5, 0x5c, 0x4e, 0x12, 0x2b, 0x86, 0x26, + 0x95, 0x01, 0xa7, 0x06, 0xd5, 0x30, 0x1b, 0x61, 0xb6, 0x65, 0x6c, 0x54, 0x54, 0x88, 0x28, 0x37, 0xf7, 0x85, 0x54, + 0x82, 0xce, 0x0d, 0xea, 0xbe, 0x60, 0xda, 0x8d, 0x57, 0xa7, 0xbb, 0x42, 0xa1, 0xc8, 0xe0, 0x0c, 0x9b, 0xaa, 0x49, + 0x58, 0x6e, 0x49, 0xb2, 0x91, 0x78, 0x5d, 0xf9, 0x48, 0x25, 0x6d, 0x6c, 0xae, 0x22, 0x92, 0x21, 0x37, 0x01, 0x06, + 0x8e, 0x81, 0x9c, 0xeb, 0x29, 0x00, 0x8f, 0x19, 0x53, 0x38, 0xa9, 0xa4, 0x38, 0x0e, 0x5e, 0x48, 0xed, 0xde, 0xb3, + 0xdf, 0xbe, 0x39, 0x7b, 0x6f, 0x63, 0xb8, 0xea, 0x8c, 0x66, 0xb9, 0xb7, 0xb0, 0x55, 0x8e, 0x61, 0x13, 0xe2, 0xd5, + 0xb6, 0x67, 0xfb, 0x33, 0x38, 0xb4, 0x2d, 0x98, 0x6a, 0xeb, 0xa6, 0x79, 0x7d, 0x7d, 0xdd, 0x84, 0x13, 0x65, 0xcd, + 0x79, 0x16, 0x4b, 0x76, 0x13, 0xda, 0x45, 0x81, 0x5c, 0x1e, 0xd1, 0xa4, 0xbc, 0x0c, 0x29, 0x8d, 0xa9, 0x1b, 0xa7, + 0x13, 0x79, 0x1e, 0x76, 0xd5, 0x3d, 0x11, 0x5f, 0x1c, 0x8b, 0x4b, 0xbe, 0xfa, 0xc7, 0x5c, 0x5e, 0xaf, 0xc6, 0x33, + 0xf8, 0xd9, 0x87, 0xe0, 0xd5, 0x71, 0x8b, 0x47, 0xe2, 0xe1, 0x0c, 0x76, 0x93, 0x78, 0xda, 0x5d, 0xac, 0x51, 0xdd, + 0x00, 0xba, 0x88, 0xfa, 0x72, 0x6a, 0xb9, 0xa8, 0x75, 0xe1, 0xc5, 0x17, 0x17, 0xc5, 0x71, 0x0b, 0xfa, 0x6a, 0xe9, + 0x7e, 0x2f, 0xd3, 0xf0, 0x56, 0xb7, 0x2f, 0x29, 0x11, 0x2e, 0x7b, 0x4a, 0x48, 0x1f, 0xba, 0x80, 0x71, 0xc3, 0xbe, + 0xc0, 0x99, 0x62, 0xa1, 0xc3, 0xea, 0xa1, 0x18, 0x59, 0xc0, 0x30, 0x0b, 0x28, 0x01, 0x72, 0x83, 0xce, 0xc3, 0xb2, + 0x81, 0xd8, 0xed, 0xb2, 0x68, 0x1b, 0x80, 0xb2, 0x62, 0xb5, 0x7f, 0xa4, 0x9b, 0xbb, 0x22, 0x0b, 0x0d, 0x71, 0x68, + 0x02, 0x7f, 0x81, 0xe0, 0x5f, 0x01, 0xf8, 0x71, 0x4b, 0xa2, 0xe9, 0xc2, 0xbc, 0x76, 0x46, 0x5e, 0x08, 0x51, 0x22, + 0x73, 0x98, 0x71, 0xfc, 0x9e, 0xe3, 0x8f, 0x17, 0xa2, 0xaa, 0xd6, 0x12, 0x40, 0x7d, 0x05, 0x6d, 0xaa, 0xad, 0xd5, + 0xc1, 0x20, 0x8d, 0x63, 0x7f, 0x96, 0x53, 0x4f, 0xff, 0x50, 0x0a, 0x03, 0xe8, 0x1d, 0xeb, 0x1a, 0x9a, 0xca, 0x7b, + 0x3a, 0x05, 0x3d, 0x6e, 0x5d, 0x7d, 0xbc, 0xf2, 0x33, 0xa7, 0xd9, 0x0c, 0x9a, 0x97, 0x13, 0x54, 0xf0, 0x68, 0x61, + 0xaa, 0x1b, 0x0f, 0xdb, 0xed, 0x1e, 0x24, 0xa9, 0x36, 0xfd, 0x98, 0x4d, 0x12, 0x2f, 0xa6, 0x63, 0x5e, 0x70, 0x38, + 0x3d, 0xb8, 0xd0, 0xfa, 0x9d, 0xdb, 0x3d, 0xcc, 0xe8, 0xd4, 0x72, 0xe1, 0xef, 0xdd, 0x03, 0x17, 0x3c, 0xf4, 0x12, + 0x1e, 0x35, 0x45, 0x32, 0x34, 0x1c, 0xe5, 0xe0, 0x51, 0xed, 0x79, 0x61, 0x0c, 0x14, 0x50, 0xd0, 0x7d, 0x0b, 0x9e, + 0x59, 0x3c, 0xc2, 0x3c, 0x33, 0xeb, 0x25, 0x68, 0xb1, 0x36, 0x83, 0x75, 0x15, 0x6c, 0x1f, 0x15, 0xb9, 0xb0, 0x58, + 0x16, 0x6b, 0x78, 0x31, 0x54, 0xe9, 0x82, 0x25, 0xb3, 0x39, 0x1f, 0x0a, 0xcf, 0x7f, 0x06, 0x67, 0x48, 0x46, 0xd8, + 0x28, 0x01, 0x78, 0x46, 0xaa, 0x7d, 0xe0, 0xc7, 0x81, 0x03, 0x9d, 0x58, 0x4d, 0xeb, 0x28, 0xa3, 0x53, 0xd4, 0x9b, + 0xb2, 0xa4, 0x29, 0xdf, 0x1d, 0x1a, 0xba, 0x9b, 0xfb, 0x08, 0x9e, 0x0a, 0x57, 0xf4, 0x86, 0x45, 0x82, 0xef, 0x86, + 0x79, 0x5d, 0x8c, 0x8a, 0xa2, 0x97, 0x72, 0x67, 0xf8, 0xc2, 0x41, 0x23, 0xfc, 0xab, 0x71, 0x89, 0x8d, 0xad, 0xa9, + 0xda, 0xc6, 0x5d, 0xb4, 0xa5, 0x8a, 0x49, 0x97, 0xa2, 0xda, 0xaf, 0x04, 0x2a, 0xbe, 0x74, 0x6c, 0x9a, 0xcf, 0x9a, + 0x92, 0xfd, 0x34, 0x05, 0xf9, 0xd8, 0xd0, 0x14, 0x29, 0x77, 0x36, 0xa5, 0x0b, 0xc1, 0x59, 0xd4, 0x39, 0x16, 0xe9, + 0x71, 0x19, 0x95, 0xe7, 0x9e, 0xd4, 0xb3, 0x79, 0xd2, 0x09, 0xd5, 0xb6, 0xfe, 0xc5, 0x49, 0x9d, 0x4d, 0x81, 0xfc, + 0x2f, 0xef, 0xfa, 0xf3, 0xe3, 0x18, 0x06, 0xbc, 0xd0, 0x4a, 0x83, 0x79, 0x35, 0xca, 0x90, 0x8f, 0x1c, 0x54, 0xa8, + 0x3d, 0xf3, 0x44, 0xe8, 0xdd, 0xc6, 0x05, 0x83, 0x3b, 0x5c, 0x47, 0xd4, 0xe4, 0x09, 0x66, 0x06, 0x39, 0x01, 0xb5, + 0xdc, 0xf1, 0x5e, 0xc5, 0x66, 0xa4, 0xd6, 0x6e, 0x89, 0x09, 0x11, 0x3b, 0x4b, 0x42, 0xdb, 0xfa, 0x73, 0x10, 0xb3, + 0xe0, 0x23, 0xb1, 0x77, 0x17, 0x0e, 0x5a, 0x3f, 0x1a, 0x2a, 0x76, 0xa8, 0xe6, 0xb9, 0xa8, 0x1e, 0x6d, 0xc8, 0x5c, + 0x83, 0x9d, 0xca, 0xdb, 0x83, 0xec, 0x3e, 0xa8, 0x36, 0xc7, 0x2d, 0x39, 0x4e, 0xff, 0xa2, 0x38, 0xaf, 0x6e, 0x05, + 0xab, 0xa0, 0x00, 0x34, 0xcb, 0x72, 0x4b, 0xd0, 0x1f, 0xb1, 0xe5, 0x16, 0xaa, 0x59, 0x80, 0xd8, 0xa4, 0x7d, 0x64, + 0x5b, 0x92, 0xc1, 0x00, 0x9c, 0x5c, 0xf1, 0x1a, 0xdb, 0xfa, 0x73, 0x59, 0x46, 0x4b, 0xb7, 0x8f, 0xc8, 0x5b, 0x21, + 0x36, 0x8c, 0x05, 0xb6, 0xbe, 0x1b, 0x52, 0xee, 0xb3, 0x58, 0x36, 0xe9, 0x69, 0x2f, 0xc5, 0xca, 0x8c, 0x96, 0xcb, + 0xbc, 0x3e, 0x17, 0x56, 0xc7, 0xa0, 0x98, 0xd9, 0x71, 0xab, 0x82, 0x5b, 0xcc, 0x4c, 0xec, 0x0f, 0x33, 0x7e, 0x5a, + 0xcd, 0x50, 0xbe, 0xb3, 0xfe, 0x1c, 0x88, 0x93, 0x55, 0x00, 0x60, 0xaa, 0x00, 0x84, 0xc8, 0xbe, 0x54, 0x42, 0x1c, + 0x9f, 0xa4, 0x2e, 0xf7, 0xb3, 0x09, 0xe5, 0x2b, 0x88, 0xf5, 0x65, 0x22, 0x6f, 0x4f, 0x47, 0xf1, 0xd7, 0xa0, 0x0d, + 0xea, 0xd0, 0x82, 0x9e, 0x5b, 0x0c, 0x40, 0x55, 0x25, 0x1b, 0x35, 0xde, 0x08, 0x81, 0xec, 0x13, 0x8b, 0x23, 0xb9, + 0x7d, 0x2a, 0xb8, 0xbd, 0x8c, 0xc3, 0x59, 0x62, 0x2c, 0x01, 0x62, 0x61, 0x5b, 0x03, 0x09, 0x39, 0x0d, 0x25, 0xcc, + 0x24, 0x13, 0xad, 0xd2, 0xe2, 0xb8, 0x25, 0x6b, 0x4b, 0x76, 0x2c, 0x2b, 0x01, 0x12, 0xc4, 0x3e, 0xad, 0x70, 0x00, + 0xc9, 0xdf, 0x26, 0x1e, 0x42, 0x76, 0x55, 0x12, 0x9b, 0x38, 0x63, 0xd6, 0x3f, 0x8e, 0xfd, 0x4b, 0x1a, 0xf7, 0x77, + 0x17, 0xd9, 0x72, 0xd9, 0x2e, 0x8e, 0x5b, 0xf2, 0xd1, 0x3a, 0x16, 0x7c, 0x43, 0xde, 0x0d, 0x2a, 0x96, 0x18, 0x0e, + 0x6e, 0x42, 0x4a, 0xac, 0xce, 0x05, 0xf3, 0x54, 0x07, 0x85, 0x6d, 0x89, 0x2c, 0x14, 0x51, 0xa9, 0xd4, 0x69, 0x0a, + 0xdb, 0x62, 0xe1, 0x7a, 0x59, 0xce, 0xe9, 0x0c, 0x4a, 0xa3, 0xe5, 0xb2, 0x53, 0xd8, 0xd6, 0x94, 0x25, 0xf0, 0x94, + 0x2d, 0x97, 0xe2, 0x4c, 0xe4, 0x94, 0x25, 0x4e, 0x1b, 0xc8, 0xd6, 0xb6, 0xa6, 0xfe, 0x8d, 0x98, 0xb0, 0x7e, 0xe3, + 0xdf, 0x38, 0x1d, 0xf5, 0xca, 0x2d, 0xf1, 0x93, 0x03, 0xc5, 0x55, 0x2b, 0xea, 0xab, 0x15, 0x0d, 0xf1, 0x5c, 0x9e, + 0xf6, 0x22, 0x4e, 0x48, 0xfc, 0xcd, 0x2b, 0x1a, 0xea, 0x15, 0x9d, 0x6f, 0x59, 0xd1, 0xf9, 0x1d, 0x2b, 0x1a, 0xa8, + 0xd5, 0xb3, 0x4a, 0xdc, 0xa5, 0xcb, 0x65, 0xa7, 0x5d, 0x61, 0xef, 0xb8, 0x15, 0xb2, 0x2b, 0x58, 0x0d, 0xd0, 0xd4, + 0x38, 0x9b, 0xd2, 0xcd, 0x44, 0x59, 0x47, 0x31, 0xfd, 0x2c, 0x4c, 0x56, 0x58, 0xc8, 0xea, 0x58, 0x30, 0xe9, 0xba, + 0x0c, 0x4c, 0xfe, 0x91, 0x94, 0xcd, 0x00, 0x0f, 0x39, 0xe0, 0x21, 0xd2, 0x77, 0x85, 0x3a, 0xf6, 0x7b, 0x1b, 0xdb, + 0x96, 0xad, 0xc9, 0xfa, 0xa2, 0x38, 0x07, 0x19, 0x21, 0xe6, 0x77, 0x2f, 0x5a, 0x84, 0xda, 0x76, 0x7f, 0x3b, 0xcd, + 0x41, 0x0e, 0xc1, 0x75, 0x9a, 0x85, 0xb6, 0x27, 0xab, 0x7e, 0x16, 0xaa, 0xa6, 0x2c, 0x51, 0x19, 0x69, 0x5b, 0x69, + 0xad, 0x7a, 0x6f, 0x52, 0x5c, 0xf7, 0xf0, 0x50, 0xd6, 0x98, 0xf9, 0x9c, 0xd3, 0x2c, 0x51, 0x94, 0x6b, 0xdb, 0xff, + 0x21, 0xa8, 0x70, 0x03, 0x5f, 0x09, 0xf4, 0x02, 0x68, 0x02, 0x54, 0x3a, 0xb7, 0xe2, 0xf9, 0x52, 0x3c, 0xed, 0x54, + 0xca, 0xe6, 0x2d, 0x32, 0xf5, 0x7e, 0x59, 0x04, 0x66, 0xc8, 0x7c, 0x4a, 0xc3, 0x73, 0xc1, 0xa0, 0x07, 0xf1, 0x85, + 0x52, 0x1e, 0x57, 0xc4, 0x5d, 0xd5, 0x00, 0xdb, 0x3f, 0xcd, 0xbb, 0x8f, 0x0e, 0x4e, 0x6d, 0x2c, 0x79, 0x7c, 0x3a, + 0x1e, 0xdb, 0xa8, 0xb0, 0xee, 0xd7, 0xac, 0x73, 0xf0, 0xd3, 0xfc, 0xeb, 0x67, 0xed, 0xaf, 0xcb, 0xc6, 0x09, 0x10, + 0x91, 0x4a, 0x82, 0xd0, 0xa2, 0xca, 0x80, 0x57, 0xcf, 0x68, 0xec, 0x27, 0xdb, 0xa7, 0x33, 0x34, 0xa7, 0x93, 0xcf, + 0x28, 0x0d, 0x81, 0x38, 0xf1, 0x5a, 0xe9, 0x79, 0x4c, 0xaf, 0xa8, 0xbe, 0xa1, 0x71, 0xc3, 0x60, 0x1b, 0x5a, 0x04, + 0xe9, 0x3c, 0xe1, 0x2a, 0x1b, 0x44, 0xb1, 0x5a, 0x63, 0x4a, 0x17, 0x62, 0x0e, 0xa6, 0x3a, 0x7f, 0x2b, 0xe5, 0x5c, + 0x5d, 0x7a, 0x15, 0x17, 0xd8, 0x36, 0x00, 0xd8, 0x0a, 0xd9, 0x60, 0x4b, 0xb9, 0xd7, 0xc6, 0xed, 0x6d, 0xb0, 0xe1, + 0x0e, 0xf2, 0x6c, 0x7b, 0xa4, 0xf1, 0x24, 0x1c, 0xba, 0xb5, 0x4b, 0x35, 0xb6, 0xe2, 0xeb, 0x93, 0x18, 0xb8, 0xcc, + 0xa0, 0xb3, 0x84, 0xe6, 0xf9, 0x56, 0x04, 0x94, 0x8b, 0x88, 0xed, 0xaa, 0xb6, 0xbd, 0xa5, 0x17, 0xdc, 0xc6, 0xb0, + 0xc3, 0x04, 0xc0, 0x65, 0x58, 0x59, 0xd5, 0xa2, 0xe3, 0x31, 0x0d, 0x4a, 0x7f, 0x38, 0x04, 0x08, 0xc7, 0x2c, 0xe6, + 0x10, 0x27, 0x13, 0x01, 0x2c, 0xfb, 0x75, 0x9a, 0x50, 0x1b, 0xe9, 0x94, 0x57, 0x05, 0xbf, 0x92, 0xff, 0x9b, 0xe1, + 0x91, 0x3d, 0xd6, 0x61, 0x51, 0xa3, 0x2c, 0x97, 0xda, 0x5d, 0x53, 0x2b, 0xaf, 0x23, 0x32, 0x15, 0xfe, 0x98, 0x6d, + 0x1b, 0xe8, 0x7e, 0xdb, 0x64, 0xd1, 0xf9, 0xfa, 0xb0, 0xd3, 0x2e, 0x6c, 0x6c, 0x43, 0x77, 0xf7, 0xdd, 0x25, 0xa2, + 0xd5, 0x3e, 0xb4, 0x9a, 0x27, 0x9f, 0xd3, 0xae, 0xdb, 0x79, 0xdc, 0xb1, 0xb1, 0xbc, 0x6b, 0x01, 0x15, 0x25, 0x33, + 0x08, 0xc0, 0x43, 0xfc, 0xbb, 0xa7, 0x52, 0xef, 0xfc, 0x7e, 0xf0, 0x3c, 0xec, 0xb4, 0x6d, 0x6c, 0xe7, 0x3c, 0x9d, + 0x7d, 0xc6, 0x14, 0xf6, 0x6d, 0x6c, 0x07, 0x71, 0x9a, 0x53, 0x73, 0x0e, 0x52, 0x9d, 0xfd, 0xfd, 0x93, 0x90, 0x10, + 0xcd, 0x32, 0x9a, 0xe7, 0x96, 0xd9, 0xbf, 0x22, 0xa5, 0x4f, 0x30, 0xcc, 0x8d, 0x14, 0x97, 0x53, 0x2e, 0xf0, 0x22, + 0xaf, 0x41, 0x30, 0xa9, 0x4a, 0x96, 0xad, 0x11, 0x9b, 0x10, 0x01, 0x25, 0x63, 0x93, 0xda, 0xd5, 0x27, 0x47, 0xde, + 0xb0, 0xf5, 0xe4, 0xc0, 0x32, 0x70, 0xbe, 0x3e, 0x40, 0xad, 0x64, 0xca, 0x92, 0xf3, 0x0d, 0xa5, 0xfe, 0xcd, 0x86, + 0x52, 0x50, 0xd9, 0x4a, 0xe8, 0xd4, 0x15, 0x3d, 0x9f, 0xc6, 0x7a, 0xa5, 0xf8, 0x98, 0x20, 0x86, 0xc2, 0xff, 0xf8, + 0x09, 0x48, 0x8d, 0x65, 0x10, 0x3d, 0xfc, 0xf6, 0xe1, 0xa0, 0xe4, 0x73, 0x86, 0x2b, 0x7b, 0xf9, 0x7d, 0x33, 0x84, + 0xd2, 0x26, 0x38, 0xf9, 0xe3, 0xcf, 0x9a, 0x2b, 0xbd, 0xf9, 0x34, 0xc1, 0x19, 0x5a, 0xd5, 0xef, 0x58, 0x7a, 0x75, + 0xd4, 0x7f, 0x75, 0xed, 0x37, 0x14, 0x2b, 0xc5, 0xa7, 0x5c, 0xff, 0x20, 0x66, 0xd3, 0x8a, 0x04, 0xd6, 0xc1, 0x14, + 0x1a, 0x0f, 0x64, 0x7c, 0x99, 0x9d, 0x48, 0xd5, 0xe7, 0x1c, 0xce, 0xb1, 0xc2, 0x55, 0x21, 0xf3, 0x8c, 0x9e, 0xc7, + 0xe9, 0xf5, 0xea, 0xe5, 0x67, 0xdb, 0x2b, 0x47, 0x6c, 0x12, 0x19, 0x87, 0xd3, 0x28, 0x29, 0x17, 0xe1, 0xce, 0x01, + 0x8a, 0x7f, 0xf9, 0x67, 0xd7, 0xfd, 0x97, 0x7f, 0xfe, 0x64, 0x55, 0xe8, 0xbe, 0xb8, 0xc0, 0xbc, 0xea, 0x76, 0xfb, + 0xee, 0xda, 0x3c, 0x52, 0x1d, 0xe7, 0x9b, 0xeb, 0xac, 0x2d, 0x02, 0xbc, 0x5f, 0x5b, 0x82, 0xb5, 0x42, 0xb9, 0xfb, + 0xac, 0xdf, 0x02, 0x18, 0xcc, 0xeb, 0x93, 0x90, 0x41, 0xa5, 0xdf, 0x05, 0xda, 0x05, 0xf2, 0xee, 0xb5, 0x22, 0xbf, + 0x1d, 0xc3, 0x9f, 0x9a, 0xc3, 0xef, 0x04, 0x5f, 0xf9, 0x27, 0xe2, 0x8b, 0x8b, 0x32, 0x0b, 0xd1, 0x6c, 0x0a, 0x77, + 0x1c, 0x0c, 0xd6, 0x4a, 0x94, 0xe2, 0xe1, 0xb5, 0x51, 0x5f, 0x9c, 0xa1, 0x24, 0xf1, 0xc5, 0x2b, 0xb8, 0xd8, 0xe8, + 0xf8, 0x32, 0xd3, 0xce, 0xd6, 0x3b, 0x84, 0x03, 0x74, 0x51, 0x9f, 0x95, 0xe8, 0x74, 0x4d, 0x32, 0x40, 0x29, 0x98, + 0x1b, 0x00, 0x26, 0x8e, 0x2f, 0x94, 0xb5, 0x79, 0x2a, 0xdd, 0x30, 0xde, 0x2a, 0x69, 0x2b, 0xf7, 0x4c, 0x0d, 0xe9, + 0xd8, 0x7a, 0x2f, 0xf0, 0x25, 0x2a, 0xd3, 0xca, 0xba, 0x17, 0xae, 0x2e, 0xb0, 0x23, 0x4a, 0xf6, 0x73, 0xe5, 0xc7, + 0x57, 0xf7, 0x63, 0x7c, 0xdb, 0x05, 0xea, 0xd2, 0x5a, 0xfe, 0xa3, 0x55, 0x82, 0x65, 0x73, 0xb9, 0x49, 0x1f, 0xb8, + 0xf6, 0x39, 0xcd, 0xce, 0x23, 0x48, 0x84, 0xca, 0x3e, 0xc1, 0x9c, 0x60, 0xa5, 0x31, 0x15, 0x7f, 0x19, 0x51, 0x17, + 0x49, 0xff, 0x83, 0x38, 0x15, 0x83, 0x2c, 0x46, 0x18, 0xca, 0x58, 0x84, 0xff, 0xcf, 0xb7, 0xfe, 0xc3, 0xf0, 0xad, + 0xbb, 0x87, 0xa8, 0x9d, 0x91, 0xfe, 0xec, 0x85, 0xfc, 0x8f, 0xcd, 0xee, 0x72, 0xc1, 0xee, 0x7e, 0x03, 0xa3, 0xcb, + 0xff, 0x31, 0x8c, 0x4e, 0xd8, 0xc8, 0x9a, 0xd3, 0xad, 0x85, 0x9a, 0x6f, 0x5d, 0xff, 0xda, 0xbf, 0xad, 0xf6, 0x55, + 0x7c, 0x71, 0x72, 0xed, 0xdf, 0x56, 0x8b, 0xb0, 0x9d, 0x5d, 0xac, 0xf6, 0x31, 0xb0, 0xdf, 0xbc, 0xb6, 0x3d, 0xfb, + 0xcd, 0xd7, 0x5f, 0xdb, 0xf8, 0x22, 0xa7, 0x7c, 0x00, 0x85, 0x64, 0x77, 0xb1, 0xb3, 0x5a, 0x11, 0xdc, 0x28, 0x30, + 0x45, 0x11, 0xf6, 0x82, 0xa4, 0x43, 0xe3, 0x3d, 0xcb, 0xcf, 0xd3, 0xc4, 0x84, 0xe6, 0x2d, 0x58, 0xf6, 0x9f, 0x0b, + 0x8e, 0xe8, 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, 0xac, 0x41, 0x54, 0x5d, 0x19, 0x74, 0x37, 0xff, 0xaf, + 0xae, 0x45, 0x90, 0xb7, 0x7d, 0x44, 0x83, 0xf8, 0xe2, 0x73, 0xc4, 0x87, 0x1c, 0xac, 0xf2, 0xd8, 0x69, 0x77, 0xa7, + 0x5f, 0xec, 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, 0xa7, 0xa9, 0xd8, 0x24, 0x5c, 0x72, 0xf8, 0x93, 0xc1, + 0x9f, 0xb4, 0x62, 0xd4, 0x2c, 0x19, 0x67, 0x7e, 0x46, 0xc3, 0xed, 0x4c, 0xba, 0xbc, 0xdf, 0x48, 0x91, 0x86, 0x4c, + 0xc0, 0xce, 0xcf, 0x45, 0xea, 0xd1, 0x94, 0x81, 0x3e, 0xba, 0x63, 0x7e, 0xc5, 0x47, 0x5d, 0x88, 0x56, 0x7e, 0x04, + 0xc0, 0x44, 0x38, 0x25, 0x79, 0x99, 0xeb, 0x00, 0xb7, 0x6a, 0xaa, 0xec, 0x10, 0x6c, 0x23, 0xe1, 0x75, 0x0f, 0x49, + 0x5f, 0xa4, 0x3d, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, 0x53, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, + 0x49, 0x7f, 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0x2e, 0xe2, 0x12, 0x9f, 0xee, 0x81, 0x43, 0x92, 0x4f, 0xe6, + 0xe3, 0x31, 0xcd, 0x1c, 0x7d, 0x00, 0xf0, 0xab, 0x03, 0x38, 0x63, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, + 0x6b, 0x27, 0x45, 0xbd, 0xac, 0xba, 0x9c, 0x31, 0xc4, 0x73, 0x44, 0xfa, 0x11, 0x24, 0xc6, 0xbf, 0x48, 0xf8, 0x7e, + 0xd7, 0x99, 0x7f, 0x75, 0x80, 0x43, 0xb8, 0xf2, 0x42, 0x67, 0x75, 0xcb, 0xbb, 0x4a, 0x3e, 0xb0, 0x84, 0x1f, 0xc9, + 0x63, 0x98, 0x29, 0x52, 0xee, 0xc3, 0x32, 0x23, 0xc6, 0xf2, 0xcb, 0x0e, 0x43, 0xd2, 0x0f, 0x1a, 0x44, 0x1e, 0xca, + 0x14, 0xb7, 0xec, 0x9e, 0x46, 0x7e, 0x76, 0x0a, 0x07, 0xbe, 0x01, 0xd0, 0x4b, 0x9e, 0xfa, 0x4e, 0x50, 0x7e, 0xc9, + 0xc9, 0x69, 0xfd, 0xd4, 0x68, 0x4d, 0xb0, 0x48, 0x8a, 0xa9, 0x8a, 0x5a, 0x50, 0x74, 0x6e, 0x16, 0x91, 0xc6, 0x6e, + 0x0b, 0xc3, 0x1e, 0xec, 0x6d, 0xf4, 0xd1, 0xea, 0xa5, 0x6b, 0x5e, 0x67, 0xfe, 0xac, 0x8c, 0x1b, 0x9c, 0xfa, 0x59, + 0xc6, 0x68, 0x66, 0x39, 0xcf, 0x7f, 0x45, 0xde, 0xbf, 0xfc, 0xf3, 0xe6, 0xf8, 0x81, 0x0a, 0x19, 0x58, 0x90, 0x5c, + 0xd2, 0x14, 0xe9, 0xd8, 0xc4, 0x0e, 0x64, 0x43, 0x5b, 0x87, 0x3b, 0xf6, 0x8f, 0xda, 0xed, 0xb6, 0x0a, 0x09, 0x74, + 0xe4, 0x4f, 0x88, 0x01, 0xc0, 0x4f, 0x78, 0x10, 0x51, 0x65, 0x62, 0xcb, 0x00, 0xe5, 0x51, 0x7b, 0x76, 0x63, 0xf7, + 0x61, 0x3b, 0x28, 0x28, 0xde, 0xd1, 0x19, 0xf5, 0xf9, 0x67, 0x8d, 0x9f, 0x89, 0x26, 0xe5, 0xf0, 0x1d, 0x3d, 0x74, + 0x35, 0xee, 0xca, 0xa0, 0x87, 0xab, 0x83, 0xbe, 0x67, 0x53, 0x71, 0x75, 0xd3, 0xb6, 0x51, 0x85, 0xa7, 0xba, 0x36, + 0x26, 0x97, 0x2d, 0x6c, 0x4b, 0x60, 0x3c, 0x4a, 0xe3, 0x90, 0x66, 0xc4, 0xa6, 0xee, 0xc4, 0xb5, 0x1e, 0xb7, 0xdb, + 0x6d, 0xdc, 0x3c, 0x38, 0x6c, 0xb7, 0xf1, 0xe1, 0xc3, 0x36, 0x6e, 0xc2, 0x1f, 0xd7, 0x75, 0x57, 0x60, 0xb8, 0x2b, + 0x6a, 0xdb, 0x69, 0x67, 0x74, 0xaa, 0x00, 0xbc, 0x33, 0xac, 0x58, 0xed, 0x09, 0xb8, 0x60, 0x5a, 0xed, 0x7b, 0x29, + 0xd9, 0xd4, 0x05, 0x07, 0x2a, 0x1d, 0x55, 0xf8, 0x0b, 0xd3, 0x2a, 0x68, 0x4a, 0xe5, 0xc5, 0x7f, 0x2f, 0x14, 0x21, + 0x78, 0xd6, 0x29, 0xdc, 0x5e, 0x2a, 0xe2, 0xa5, 0x90, 0x0a, 0x04, 0x1f, 0x48, 0xe3, 0x3e, 0x4b, 0xe0, 0xdb, 0x59, + 0x3a, 0x6a, 0xaa, 0x19, 0x55, 0xba, 0x92, 0x74, 0xfb, 0x40, 0x86, 0xa5, 0x37, 0x11, 0xc4, 0xe8, 0x01, 0xc2, 0xfe, + 0x7d, 0x1a, 0xa8, 0x15, 0x84, 0xfa, 0xc1, 0x7d, 0xea, 0x6b, 0xec, 0x8f, 0x1e, 0x88, 0xe4, 0xa4, 0x9d, 0x68, 0xb9, + 0xdc, 0xf1, 0x97, 0xcb, 0x9d, 0xe0, 0xfe, 0x33, 0x94, 0xcb, 0xab, 0x4f, 0x41, 0xc0, 0xcd, 0x9f, 0x12, 0xe8, 0x17, + 0x50, 0xee, 0x45, 0x58, 0x82, 0x24, 0x9f, 0x7c, 0xac, 0x06, 0x94, 0x8f, 0x41, 0xb1, 0x82, 0x94, 0x90, 0x44, 0xd2, + 0x3e, 0x5f, 0x2e, 0x15, 0xf1, 0xe3, 0x39, 0xf1, 0xcb, 0xa2, 0x8e, 0x8d, 0x67, 0x24, 0x28, 0x1f, 0x6d, 0x01, 0xf2, + 0x4c, 0x71, 0xa9, 0x0a, 0xe2, 0x6b, 0x3f, 0x4b, 0x4c, 0x80, 0x5f, 0xa7, 0x96, 0x1a, 0xd6, 0x9a, 0x65, 0xe9, 0x15, + 0x83, 0xe4, 0x97, 0x95, 0x81, 0xa7, 0x04, 0x2e, 0xfe, 0xea, 0x99, 0xa1, 0x70, 0xa3, 0x83, 0xf7, 0x9a, 0xcf, 0xc2, + 0x2d, 0x93, 0xe5, 0x04, 0xbd, 0x50, 0xcd, 0xcd, 0x9b, 0xeb, 0x69, 0xbd, 0xf3, 0xaf, 0xbd, 0x99, 0x7e, 0x78, 0x26, + 0xf3, 0x6c, 0xbc, 0x69, 0x79, 0xb2, 0xe6, 0x2d, 0x79, 0x0d, 0xb1, 0x1f, 0x5b, 0xf3, 0x6d, 0xb8, 0x67, 0x53, 0xf2, + 0xb8, 0x77, 0x2f, 0xcf, 0xa8, 0x9f, 0x05, 0xd1, 0x5b, 0x3f, 0xf3, 0xa7, 0x79, 0x6f, 0xac, 0x6f, 0xf1, 0xd2, 0x14, + 0x70, 0x3e, 0x16, 0x99, 0x4e, 0x49, 0x70, 0x6b, 0xe3, 0x10, 0xe1, 0xea, 0xbd, 0x84, 0x40, 0xfa, 0xb9, 0x6d, 0x3c, + 0x37, 0x5f, 0xc1, 0x3a, 0xdb, 0x78, 0x8a, 0xb0, 0x4c, 0x20, 0x7a, 0xfb, 0x47, 0xa6, 0x0e, 0x61, 0xc8, 0x75, 0xf1, + 0xc6, 0x6e, 0xf5, 0x95, 0x3b, 0x9d, 0x4c, 0xf4, 0x7e, 0x25, 0x99, 0x68, 0x03, 0x1a, 0xad, 0x8c, 0xe6, 0xb3, 0x34, + 0xc9, 0xa9, 0x8d, 0xdf, 0x43, 0x3b, 0x79, 0x15, 0xb3, 0xd9, 0x70, 0x8d, 0xe6, 0xca, 0xa6, 0xe2, 0x8d, 0x6c, 0x07, + 0x41, 0x9d, 0xf7, 0xdf, 0x97, 0x71, 0x7c, 0x1d, 0xdf, 0x11, 0x89, 0xe8, 0x8c, 0x6e, 0xc9, 0x95, 0xcd, 0xe9, 0x27, + 0x73, 0x65, 0xe3, 0x7b, 0xe5, 0xca, 0xe6, 0xf4, 0x8f, 0xce, 0x95, 0x65, 0xd4, 0xc8, 0x95, 0x05, 0x39, 0xf7, 0xf5, + 0xbd, 0x52, 0x2e, 0x75, 0x26, 0x5c, 0x7a, 0x9d, 0x93, 0x8e, 0x8a, 0x81, 0xc4, 0xe9, 0x04, 0xf2, 0x2d, 0xff, 0xf1, + 0xe9, 0x93, 0x71, 0x3a, 0x31, 0x93, 0x27, 0xe1, 0xc3, 0x24, 0x40, 0x76, 0x38, 0x23, 0x0b, 0xfb, 0xa7, 0x9b, 0xce, + 0x93, 0x61, 0xa7, 0xb7, 0xdf, 0x99, 0xda, 0x9e, 0x0d, 0x4e, 0x47, 0x51, 0xd0, 0xee, 0xed, 0xef, 0x43, 0xc1, 0xb5, + 0x51, 0xd0, 0x85, 0x02, 0x66, 0x14, 0x1c, 0x42, 0x41, 0x60, 0x14, 0x3c, 0x84, 0x82, 0xd0, 0x28, 0x78, 0x04, 0x05, + 0x57, 0x76, 0x31, 0x64, 0x65, 0x42, 0xf0, 0x23, 0x24, 0x6e, 0x30, 0xdc, 0xc9, 0xea, 0xa7, 0xb7, 0x23, 0xa2, 0xab, + 0x3c, 0x2a, 0x6f, 0x7e, 0x68, 0x1e, 0xe8, 0x8b, 0x0a, 0x2f, 0xbe, 0xb8, 0x00, 0xd6, 0x0a, 0x17, 0xb1, 0x60, 0x88, + 0x49, 0xca, 0x9a, 0xfb, 0xfa, 0xb5, 0xed, 0x95, 0x59, 0xb3, 0x6d, 0xdc, 0xd5, 0x79, 0xb3, 0x9e, 0x8d, 0x04, 0x5f, + 0x92, 0x2f, 0x0e, 0x1b, 0xa1, 0xea, 0x16, 0xee, 0x00, 0xac, 0x2e, 0xe0, 0xdc, 0x47, 0x78, 0xaa, 0x15, 0x20, 0xea, + 0xc0, 0x07, 0x18, 0xde, 0xb3, 0x29, 0xd5, 0xfb, 0x45, 0x0f, 0x60, 0x89, 0xcc, 0xe2, 0x5e, 0x54, 0x29, 0x46, 0x6f, + 0xf1, 0xb8, 0xba, 0xf3, 0xf5, 0x3d, 0x91, 0x77, 0xe8, 0x65, 0x58, 0x86, 0xb9, 0x66, 0x98, 0xfb, 0x13, 0x0f, 0x52, + 0x28, 0x21, 0x63, 0xc4, 0x1b, 0x13, 0x42, 0xda, 0x83, 0xb9, 0xf7, 0x16, 0x5f, 0x47, 0x34, 0xf1, 0xa6, 0x45, 0xaf, + 0x5c, 0x7f, 0x99, 0xd2, 0xf9, 0xbe, 0xbc, 0x28, 0x5c, 0xd0, 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, + 0xce, 0x9f, 0xa1, 0xec, 0x8c, 0xe3, 0xf4, 0xba, 0x09, 0xe2, 0x7e, 0x63, 0x1e, 0x20, 0xcc, 0xad, 0xcc, 0x03, 0x7c, + 0x09, 0xb0, 0x96, 0x4f, 0xef, 0xfd, 0x49, 0xf9, 0xfb, 0x15, 0xcd, 0x73, 0x7f, 0xa2, 0x6a, 0x6e, 0xcf, 0xfb, 0x13, + 0x20, 0x9a, 0x39, 0x7f, 0x1a, 0x08, 0x48, 0xce, 0x03, 0x84, 0x40, 0x40, 0x57, 0xe5, 0xea, 0xc1, 0xcc, 0xeb, 0x69, + 0x7e, 0x02, 0x55, 0xf5, 0x22, 0xee, 0x4f, 0xaa, 0x82, 0xe3, 0x59, 0x46, 0x55, 0x02, 0x21, 0x60, 0xb1, 0x38, 0x6e, + 0x41, 0x81, 0x7c, 0xbd, 0x25, 0x9d, 0x4f, 0x73, 0x97, 0xed, 0x49, 0x7d, 0x96, 0x4e, 0xe7, 0x33, 0x4f, 0xa6, 0x94, + 0xc7, 0x52, 0xd6, 0x33, 0xf2, 0xbe, 0xec, 0x04, 0xf0, 0x9f, 0x3a, 0x78, 0xf1, 0xe5, 0x78, 0x3c, 0xbe, 0x33, 0xbd, + 0xef, 0xcb, 0x70, 0x4c, 0xbb, 0xf4, 0xb0, 0x07, 0xa7, 0x16, 0x9a, 0x2a, 0x11, 0xad, 0x53, 0x08, 0xdc, 0x2d, 0xee, + 0x57, 0x19, 0x72, 0xd6, 0x78, 0xb4, 0xb8, 0x7f, 0xaa, 0x5f, 0x31, 0xcb, 0xe8, 0x62, 0xea, 0x67, 0x13, 0x96, 0x78, + 0xed, 0xc2, 0xbd, 0x5a, 0x28, 0x50, 0x8f, 0x8e, 0x8e, 0x0a, 0x37, 0xd4, 0x4f, 0xed, 0x30, 0x2c, 0xdc, 0x60, 0x51, + 0x4e, 0xa3, 0xdd, 0x1e, 0x8f, 0x0b, 0x97, 0xe9, 0x82, 0xfd, 0x6e, 0x10, 0xee, 0x77, 0x0b, 0xf7, 0xda, 0xa8, 0x51, + 0xb8, 0x54, 0x3d, 0x65, 0x34, 0xac, 0x1d, 0x7d, 0x78, 0xd4, 0x6e, 0x17, 0xae, 0x24, 0xb4, 0x05, 0xc4, 0xe4, 0xe4, + 0x4f, 0xcf, 0x9f, 0x73, 0x30, 0x98, 0x8a, 0x5e, 0xcc, 0x9d, 0xe1, 0xae, 0xba, 0x56, 0x52, 0x7e, 0x87, 0xb1, 0x40, + 0x23, 0xfc, 0xb5, 0x99, 0x39, 0x07, 0xc4, 0x2c, 0x32, 0xe6, 0x62, 0x9d, 0x58, 0x57, 0x7b, 0x0d, 0x94, 0x25, 0x5e, + 0x7f, 0x4d, 0xe2, 0x2a, 0xa1, 0x0e, 0xf8, 0x18, 0xd4, 0x94, 0xb7, 0x9f, 0x27, 0xdb, 0xa4, 0x47, 0xf6, 0x69, 0xe9, + 0x71, 0x79, 0x1f, 0xe1, 0x91, 0xfd, 0xe1, 0xc2, 0x23, 0x31, 0x85, 0x87, 0x64, 0x1d, 0xd7, 0x9c, 0xd8, 0x41, 0x44, + 0x83, 0x8f, 0x97, 0xe9, 0x4d, 0x13, 0xb6, 0x44, 0x66, 0x0b, 0xb1, 0x72, 0xf5, 0x5b, 0x33, 0xf9, 0x75, 0x67, 0xc6, + 0x47, 0x1c, 0x85, 0x8e, 0xff, 0x26, 0x21, 0xf6, 0x1b, 0x1d, 0xd8, 0x93, 0x25, 0xe3, 0x31, 0xb1, 0xdf, 0x8c, 0xc7, + 0xb6, 0xbe, 0x1c, 0xc7, 0xe7, 0x54, 0xd4, 0x7a, 0x5d, 0x2b, 0x11, 0xb5, 0xc0, 0xd0, 0xaf, 0xca, 0xcc, 0x02, 0x95, + 0x77, 0x67, 0xe6, 0xd8, 0xa9, 0x37, 0x21, 0xcb, 0x61, 0xab, 0xc1, 0xb7, 0x25, 0xeb, 0x97, 0xf3, 0x27, 0xb5, 0x2f, + 0x29, 0x95, 0x00, 0x6f, 0xf8, 0xfc, 0xd3, 0xea, 0xcd, 0x70, 0x13, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0xe7, + 0x9a, 0xa3, 0x82, 0xe5, 0x6f, 0x92, 0x95, 0x5b, 0x1f, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0x2a, 0x78, 0x3a, 0x99, 0xc4, + 0xe2, 0x30, 0x49, 0xcd, 0xe0, 0x96, 0xcd, 0x07, 0xb5, 0xf9, 0x7a, 0x66, 0x43, 0xf5, 0x79, 0x0d, 0xf1, 0xbd, 0x61, + 0x79, 0x5a, 0xf8, 0x4a, 0x7d, 0x78, 0x56, 0xc4, 0x04, 0x17, 0x8a, 0xc7, 0x2f, 0xe4, 0x19, 0x53, 0x8e, 0x59, 0x28, + 0x9b, 0xb3, 0xb0, 0x28, 0xd4, 0xe9, 0xfc, 0x90, 0xe5, 0x33, 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0x5f, + 0x1b, 0xc9, 0x6d, 0xb5, 0xe5, 0x2a, 0xb4, 0x9d, 0x26, 0xb3, 0x85, 0xae, 0x79, 0x61, 0x2b, 0x93, 0x4d, 0x23, 0xd1, + 0xb6, 0x24, 0x3e, 0x65, 0xda, 0x9d, 0x31, 0x43, 0xc8, 0xfc, 0x29, 0x17, 0x44, 0xbf, 0xd2, 0x05, 0x85, 0x69, 0x65, + 0x89, 0x37, 0x12, 0x5b, 0x22, 0x55, 0x2c, 0x9f, 0xf9, 0x89, 0x36, 0xe6, 0x24, 0x3f, 0xd8, 0x5d, 0x54, 0x2b, 0x5f, + 0xd8, 0x1a, 0x6c, 0x49, 0xbc, 0xfd, 0xe3, 0x16, 0x34, 0xe8, 0x5b, 0x35, 0xd0, 0x93, 0xb5, 0x0c, 0xb3, 0xbb, 0xf3, + 0xae, 0x3f, 0x5e, 0xb8, 0xf9, 0x35, 0x76, 0xf3, 0x6b, 0xeb, 0xab, 0x45, 0xf3, 0x9a, 0x5e, 0x7e, 0x64, 0xbc, 0xc9, + 0xfd, 0x59, 0x13, 0xbc, 0xa7, 0x22, 0x33, 0x44, 0xb1, 0x67, 0xa1, 0xa3, 0x4b, 0xd3, 0xaf, 0x37, 0xcf, 0x21, 0x3d, + 0x5b, 0x98, 0x51, 0x5e, 0x92, 0x26, 0xb4, 0x57, 0x3f, 0xbe, 0x67, 0x66, 0x18, 0x6b, 0x6c, 0x8d, 0x16, 0x29, 0xa4, + 0x73, 0xf3, 0x5b, 0xaf, 0xad, 0xd8, 0x7a, 0x5b, 0xa7, 0x0f, 0xb7, 0x37, 0xd6, 0xf7, 0x14, 0x72, 0x1b, 0x42, 0x7a, + 0x65, 0xeb, 0xf9, 0xcf, 0xdb, 0xf2, 0xbb, 0x3f, 0x75, 0x98, 0x0d, 0xf2, 0x49, 0xf4, 0xff, 0xc6, 0x29, 0xc0, 0xd5, + 0x62, 0x71, 0x98, 0xed, 0x3e, 0x90, 0x79, 0xfe, 0x98, 0xd3, 0x0c, 0xdf, 0xa7, 0xe6, 0xa5, 0xb8, 0x77, 0x62, 0x01, + 0x62, 0xc6, 0xeb, 0x1c, 0xd5, 0x53, 0xb1, 0xef, 0xee, 0xfe, 0xee, 0xe9, 0x17, 0x0a, 0x47, 0xfa, 0x1e, 0x56, 0xdb, + 0xee, 0xc1, 0x46, 0x88, 0xfd, 0x5b, 0x8f, 0x25, 0x42, 0xe6, 0x5d, 0x42, 0x52, 0x48, 0x6f, 0x96, 0xaa, 0x53, 0x99, + 0x19, 0x8d, 0xc5, 0xa7, 0xd7, 0xd5, 0x52, 0xec, 0x3f, 0x9c, 0xdd, 0xe8, 0xd5, 0xe8, 0xac, 0x9c, 0xb6, 0xfc, 0x43, + 0x0f, 0x55, 0x6e, 0x3f, 0xc5, 0x59, 0x3f, 0x18, 0x78, 0x38, 0xbb, 0xe9, 0x49, 0x41, 0xdb, 0xcc, 0x24, 0x54, 0xed, + 0xd9, 0x8d, 0x79, 0xac, 0xb4, 0xea, 0xc8, 0x72, 0xf7, 0x73, 0x8b, 0xfa, 0x39, 0xed, 0xc1, 0x97, 0xa6, 0x58, 0xe0, + 0xc7, 0x4a, 0x98, 0x4f, 0x59, 0x18, 0xc6, 0xb4, 0xa7, 0xe5, 0xb5, 0xd5, 0x79, 0x08, 0xa7, 0x32, 0xcd, 0x25, 0xab, + 0xaf, 0x8a, 0x81, 0xbc, 0x12, 0x4f, 0xfe, 0x65, 0x9e, 0xc6, 0xf0, 0x9d, 0xc7, 0x8d, 0xe8, 0x54, 0xc7, 0x15, 0xdb, + 0x15, 0xf2, 0xc4, 0xef, 0xfa, 0x5c, 0x0e, 0xdb, 0x7f, 0xea, 0x89, 0x05, 0x6f, 0xf7, 0x78, 0x3a, 0xf3, 0x9a, 0xfb, + 0xf5, 0x89, 0xc0, 0xab, 0x72, 0x0a, 0x78, 0xc3, 0xb4, 0x30, 0x48, 0x2b, 0xc9, 0xa7, 0x2d, 0xb7, 0xa3, 0xca, 0x44, + 0x07, 0x60, 0x84, 0x96, 0x45, 0x45, 0x7d, 0x32, 0xff, 0x98, 0xdd, 0xf2, 0x78, 0xf3, 0x6e, 0x79, 0xac, 0x77, 0xcb, + 0xdd, 0x14, 0xfb, 0xe5, 0xb8, 0x03, 0xff, 0xf5, 0xaa, 0x09, 0x79, 0x6d, 0x6b, 0x7f, 0x76, 0x63, 0x81, 0x9e, 0xd6, + 0xec, 0xce, 0x6e, 0xe4, 0xa1, 0x5a, 0x48, 0x5c, 0x6b, 0xc3, 0x31, 0x53, 0xdc, 0xb6, 0xa0, 0x10, 0xfe, 0x6f, 0xd7, + 0x5e, 0x75, 0x0e, 0xe0, 0x1d, 0xb4, 0x3a, 0x5c, 0x7f, 0xd7, 0xbd, 0x7b, 0xd3, 0x7a, 0x49, 0xca, 0x1d, 0x4f, 0x73, + 0x63, 0xe4, 0x72, 0xff, 0xf2, 0x92, 0x86, 0xde, 0x38, 0x0d, 0xe6, 0xf9, 0x3f, 0x29, 0xf8, 0x15, 0x12, 0xef, 0xdc, + 0xd2, 0x2b, 0xfd, 0xe8, 0xa6, 0xf2, 0x88, 0xaf, 0xee, 0x61, 0x51, 0xae, 0x93, 0x97, 0x07, 0x7e, 0x4c, 0x9d, 0xae, + 0x7b, 0xb0, 0x61, 0x13, 0xfc, 0x9b, 0xac, 0xcd, 0xc6, 0xc9, 0xfc, 0x5e, 0x64, 0xdc, 0x89, 0x84, 0xcf, 0xc2, 0x81, + 0xb9, 0x86, 0xed, 0xa3, 0xcd, 0xe0, 0x0e, 0xf5, 0x48, 0x23, 0x2d, 0x14, 0x94, 0xdc, 0x09, 0xe9, 0xd8, 0x9f, 0xc7, + 0xfc, 0xee, 0x5e, 0xb7, 0x51, 0xc6, 0x5a, 0xaf, 0x77, 0x30, 0xf4, 0xaa, 0xee, 0x3d, 0xb9, 0xf4, 0x97, 0x8f, 0x0f, + 0xe0, 0x3f, 0x79, 0xf8, 0xe5, 0xb2, 0xd2, 0xd5, 0xa5, 0xd5, 0x0b, 0xba, 0xfa, 0x55, 0x4d, 0x19, 0x97, 0x22, 0x5c, + 0xe8, 0xe3, 0xf7, 0xad, 0x0d, 0x5a, 0xe5, 0xbd, 0xaa, 0x2b, 0x2d, 0xeb, 0xb3, 0x6a, 0x7f, 0x5e, 0xe7, 0xf7, 0xac, + 0x1b, 0x48, 0xcd, 0xb5, 0x5e, 0x57, 0x7d, 0x7a, 0x7e, 0xad, 0xb2, 0xc6, 0xb8, 0xa8, 0x7f, 0x45, 0x2e, 0x4b, 0x13, + 0x45, 0xa6, 0xa2, 0x82, 0x95, 0x72, 0x25, 0xad, 0x94, 0x94, 0x92, 0x8b, 0xe3, 0xc1, 0xcd, 0x34, 0xb6, 0xae, 0xe4, + 0xfd, 0x38, 0xc4, 0xee, 0xb8, 0x6d, 0xdb, 0x12, 0x4e, 0x3a, 0xf8, 0x4c, 0x97, 0xfd, 0xe1, 0xfd, 0xd7, 0xcd, 0x23, + 0x7b, 0x00, 0x9a, 0xd6, 0xd5, 0x44, 0x68, 0x76, 0x2f, 0xfd, 0x5b, 0x9a, 0x9d, 0x77, 0x95, 0x0b, 0x5e, 0xe6, 0x8b, + 0x8b, 0x32, 0xab, 0x6b, 0x5b, 0x37, 0xd3, 0x38, 0xc9, 0x89, 0x1d, 0x71, 0x3e, 0xf3, 0x5a, 0xad, 0xeb, 0xeb, 0x6b, + 0xf7, 0x7a, 0xdf, 0x4d, 0xb3, 0x49, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, 0x8b, 0x1f, 0xb6, 0x75, 0xc5, 0xe8, 0xf5, 0x93, + 0xf4, 0x86, 0xd8, 0x6d, 0xab, 0x6d, 0x75, 0xba, 0x47, 0x56, 0xa7, 0x7b, 0xe0, 0x3e, 0x3c, 0xb2, 0xfb, 0x5f, 0x58, + 0xd6, 0x71, 0x48, 0xc7, 0x39, 0xfc, 0xb0, 0xac, 0x63, 0xa1, 0x78, 0xc9, 0xdf, 0x96, 0xe5, 0x06, 0x71, 0xde, 0xec, + 0x58, 0x0b, 0xf5, 0x68, 0x59, 0x70, 0x8b, 0x90, 0x67, 0x7d, 0x39, 0xee, 0x8e, 0x0f, 0xc6, 0x8f, 0x7b, 0xaa, 0xb8, + 0xf8, 0xa2, 0x56, 0x1d, 0xcb, 0x7f, 0xbb, 0x46, 0xb3, 0x9c, 0x67, 0xe9, 0x47, 0xaa, 0x5c, 0xfb, 0x16, 0x88, 0x9e, + 0x8d, 0x4d, 0xbb, 0xeb, 0x23, 0x75, 0x8e, 0x2e, 0x83, 0x71, 0xb7, 0xaa, 0x2e, 0x60, 0x6c, 0x95, 0x40, 0x1e, 0xb7, + 0x34, 0xe8, 0xc7, 0x26, 0x9a, 0x3a, 0xcd, 0x4d, 0x88, 0xea, 0xd8, 0x6a, 0x8e, 0x13, 0x3d, 0xbf, 0x63, 0x38, 0xb4, + 0xae, 0x75, 0x55, 0x01, 0x81, 0x6d, 0x85, 0xc4, 0x7e, 0xd5, 0xe9, 0x1e, 0xe1, 0x4e, 0xe7, 0xa1, 0xfb, 0xf0, 0x28, + 0x68, 0xe3, 0x03, 0xf7, 0xa0, 0xb9, 0xef, 0x3e, 0xc4, 0x47, 0xcd, 0x23, 0x7c, 0xf4, 0xfc, 0x28, 0x68, 0x1e, 0xb8, + 0x07, 0xb8, 0xdd, 0x3c, 0x82, 0xc2, 0xe6, 0x51, 0xf3, 0xe8, 0xaa, 0x79, 0x70, 0x14, 0xb4, 0x45, 0x69, 0xd7, 0x3d, + 0x3c, 0x6c, 0x76, 0xda, 0xee, 0xe1, 0x21, 0x3e, 0x74, 0x1f, 0x3e, 0x6c, 0x76, 0xf6, 0xdd, 0x87, 0x0f, 0x5f, 0x1e, + 0x1e, 0xb9, 0xfb, 0xf0, 0x6e, 0x7f, 0x3f, 0xd8, 0x77, 0x3b, 0x9d, 0x26, 0xfc, 0xc1, 0x47, 0x6e, 0x57, 0xfe, 0xe8, + 0x74, 0xdc, 0xfd, 0x0e, 0x6e, 0xc7, 0x87, 0x5d, 0xf7, 0xe1, 0x63, 0x2c, 0xfe, 0x8a, 0x6a, 0x58, 0xfc, 0x81, 0x6e, + 0xf0, 0x63, 0xb7, 0xfb, 0x50, 0xfe, 0x12, 0x1d, 0x5e, 0x1d, 0x1c, 0xfd, 0x68, 0xb7, 0xb6, 0xce, 0xa1, 0x23, 0xe7, + 0x70, 0x74, 0xe8, 0xee, 0xef, 0xe3, 0x83, 0x8e, 0x7b, 0xb4, 0x1f, 0x35, 0x0f, 0xba, 0xee, 0xc3, 0x47, 0x41, 0xb3, + 0xe3, 0x3e, 0x7a, 0x84, 0xdb, 0xcd, 0x7d, 0xb7, 0x8b, 0x3b, 0xee, 0xc1, 0xbe, 0xf8, 0xb1, 0xef, 0x76, 0xaf, 0x1e, + 0x3d, 0x76, 0x1f, 0x1e, 0x46, 0x0f, 0xdd, 0x83, 0x6f, 0x0f, 0x8e, 0xdc, 0xee, 0x7e, 0xb4, 0xff, 0xd0, 0xed, 0x3e, + 0xba, 0x7a, 0xe8, 0x1e, 0x44, 0xcd, 0xee, 0xc3, 0x3b, 0x5b, 0x76, 0xba, 0x2e, 0xe0, 0x48, 0xbc, 0x86, 0x17, 0x58, + 0xbd, 0x80, 0xff, 0x23, 0xd1, 0xf6, 0xdf, 0xb0, 0x9b, 0x7c, 0xbd, 0xe9, 0x63, 0xf7, 0xe8, 0x51, 0x20, 0xab, 0x43, + 0x41, 0x53, 0xd7, 0x80, 0x26, 0x57, 0x4d, 0x39, 0xac, 0xe8, 0xae, 0xa9, 0x3b, 0xd2, 0xff, 0xab, 0xc1, 0xae, 0x9a, + 0x30, 0xb0, 0x1c, 0xf7, 0xdf, 0xb5, 0x9f, 0x72, 0xc9, 0x8f, 0x5b, 0x13, 0x49, 0xfa, 0x93, 0xfe, 0x17, 0xf2, 0x73, + 0x3e, 0x5f, 0x5c, 0x60, 0x7f, 0x9b, 0xe3, 0x23, 0xfe, 0xb4, 0xe3, 0x23, 0xa2, 0xf7, 0xf1, 0x7c, 0xc4, 0x7f, 0xb8, + 0xe7, 0xc3, 0x5f, 0x75, 0x9b, 0xdf, 0xf0, 0x35, 0x07, 0xc7, 0xaa, 0x55, 0xfc, 0x82, 0x3b, 0xc3, 0x14, 0x3e, 0x1d, + 0x5d, 0xf4, 0x6e, 0x38, 0x89, 0xa8, 0xe9, 0x07, 0x4a, 0x81, 0xc5, 0xde, 0x70, 0xc9, 0x63, 0x83, 0x6d, 0x08, 0x09, + 0x3f, 0x8d, 0x90, 0xef, 0xee, 0x83, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x60, 0xe2, 0xa3, 0xe6, 0xc9, 0x17, 0x9e, 0x06, + 0xe1, 0x29, 0x38, 0x13, 0xcf, 0x0e, 0xdc, 0x9a, 0xd1, 0xb0, 0x5b, 0xf4, 0x4a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, + 0xe7, 0x04, 0x1d, 0xe4, 0x15, 0x39, 0xc4, 0x56, 0x6e, 0x99, 0x99, 0x90, 0x3a, 0xea, 0xa1, 0x14, 0x4a, 0x5d, 0xb7, + 0xed, 0xb6, 0x4b, 0x97, 0x0e, 0x5c, 0x8b, 0x44, 0x16, 0x29, 0xf7, 0xbd, 0x9d, 0x0e, 0x8e, 0xd3, 0x09, 0x5c, 0x96, + 0x24, 0x3e, 0x1f, 0x07, 0x27, 0x1e, 0x02, 0xf9, 0xe5, 0x3e, 0x48, 0x9f, 0x50, 0x8e, 0x1e, 0x3f, 0xfb, 0xf8, 0x37, + 0x08, 0x62, 0xea, 0x98, 0xc4, 0x14, 0xbc, 0x1d, 0xaf, 0x68, 0xc8, 0x7c, 0xc7, 0x76, 0x66, 0x19, 0x1d, 0xd3, 0x2c, + 0x6f, 0xd6, 0xee, 0xeb, 0x11, 0x57, 0xf5, 0x20, 0x5b, 0x41, 0x38, 0xce, 0xe0, 0x73, 0x48, 0x64, 0xa8, 0xfc, 0x8d, + 0xb6, 0x32, 0xc0, 0xec, 0x02, 0xeb, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xd9, 0x52, 0x6b, 0xeb, 0xb8, 0xdd, 0x43, + 0x64, 0x89, 0x62, 0xf8, 0xd0, 0xcc, 0x0f, 0x4e, 0x73, 0xbf, 0xfd, 0x27, 0x64, 0x34, 0x2b, 0x3b, 0x1a, 0x29, 0x77, + 0x5b, 0x52, 0x7e, 0x8e, 0x70, 0x25, 0xec, 0x6a, 0x4b, 0x8a, 0xf8, 0x52, 0xce, 0xdd, 0x46, 0xbd, 0x44, 0x25, 0xcd, + 0xc9, 0x2b, 0x01, 0xc7, 0x6c, 0xe2, 0x18, 0xd7, 0x4d, 0x24, 0xf2, 0x43, 0x36, 0x70, 0x5b, 0x3d, 0x42, 0x45, 0x55, + 0x25, 0x41, 0x0b, 0x11, 0x6d, 0x61, 0x89, 0x95, 0x2c, 0x97, 0x4e, 0x02, 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, + 0x0d, 0xc9, 0x09, 0x2e, 0x01, 0x12, 0x08, 0x26, 0x89, 0xfc, 0xb7, 0x2a, 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, + 0x64, 0x02, 0x54, 0xe8, 0x27, 0x93, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, + 0xc9, 0xed, 0x7b, 0x75, 0xe3, 0xb5, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, 0xbe, 0xce, 0xd3, 0x20, 0x0f, 0x8e, 0xa7, + 0x94, 0xfb, 0xf2, 0xb0, 0x1a, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, 0x54, 0x05, 0xf5, 0x89, 0x3c, 0xcc, 0x2f, + 0x45, 0x3d, 0xb6, 0xd4, 0x55, 0x71, 0x4d, 0xb1, 0x34, 0xa4, 0x83, 0xa5, 0x3f, 0x26, 0xf0, 0xc5, 0x71, 0x64, 0x92, + 0xa4, 0x76, 0xff, 0x41, 0x99, 0xeb, 0xb2, 0x6d, 0x11, 0x62, 0x96, 0x7c, 0x1c, 0x66, 0x34, 0xfe, 0x27, 0xf2, 0x80, + 0x05, 0x69, 0xf2, 0x60, 0x64, 0xa3, 0x1e, 0x77, 0xa3, 0x8c, 0x8e, 0xc9, 0x03, 0x90, 0xf1, 0x9e, 0xb0, 0x3e, 0x80, + 0x11, 0x36, 0x6e, 0xa6, 0x31, 0x16, 0x1a, 0xd3, 0x3d, 0x14, 0x22, 0x09, 0xae, 0xdd, 0x3d, 0xb4, 0x2d, 0x69, 0x13, + 0x8b, 0xdf, 0x7d, 0x29, 0x4e, 0x85, 0x12, 0x60, 0x75, 0xba, 0xee, 0x61, 0xd4, 0x75, 0x1f, 0x5f, 0x3d, 0x72, 0x8f, + 0xa2, 0xce, 0xa3, 0xab, 0x26, 0xfc, 0xdb, 0x75, 0x1f, 0xc7, 0xcd, 0xae, 0xfb, 0x18, 0xfe, 0xff, 0xf6, 0xc0, 0x3d, + 0x8c, 0x9a, 0x1d, 0xf7, 0xe8, 0x6a, 0xdf, 0xdd, 0x7f, 0xd9, 0xe9, 0xba, 0xfb, 0x56, 0xc7, 0x92, 0xed, 0x80, 0x5d, + 0x4b, 0xee, 0xfc, 0x60, 0x65, 0x43, 0x6c, 0x08, 0xc6, 0xc9, 0x03, 0x77, 0x36, 0x16, 0x67, 0xa4, 0xcd, 0xfd, 0xa9, + 0x9c, 0x75, 0x4f, 0xfd, 0x0c, 0xbe, 0x6c, 0x5a, 0xdf, 0xbb, 0xb5, 0x77, 0xb8, 0xc6, 0x2f, 0x36, 0x0c, 0x31, 0x13, + 0x11, 0x70, 0xf3, 0xae, 0x35, 0x2a, 0xee, 0xb0, 0x93, 0xdf, 0x82, 0x52, 0x51, 0xb0, 0x32, 0xbb, 0xc8, 0x20, 0x6b, + 0x59, 0x03, 0x12, 0x80, 0x04, 0x0d, 0xae, 0xe6, 0x8f, 0x56, 0x74, 0x9e, 0xc1, 0xfd, 0x04, 0x9a, 0x97, 0x30, 0xf1, + 0x45, 0x3e, 0x01, 0xc3, 0x8b, 0xb0, 0x58, 0x05, 0x0f, 0x8e, 0x05, 0x66, 0xa9, 0x71, 0x1b, 0x1d, 0xad, 0x72, 0x00, + 0x42, 0x06, 0xf7, 0x07, 0x16, 0x85, 0x9e, 0x59, 0xcd, 0x8b, 0x5b, 0x21, 0x51, 0xb0, 0x13, 0x9a, 0x0f, 0x6c, 0x28, + 0xb2, 0x3d, 0x5b, 0x78, 0x00, 0xed, 0xf2, 0xe3, 0xaf, 0x25, 0xdd, 0x57, 0x05, 0x58, 0x5c, 0x0e, 0x01, 0x9b, 0x1a, + 0xd0, 0x67, 0xa3, 0xbd, 0xbd, 0xad, 0xdb, 0x49, 0xe8, 0x97, 0x30, 0xb5, 0xea, 0x9b, 0x91, 0x26, 0xa7, 0xb2, 0xcd, + 0x75, 0x28, 0xfb, 0x15, 0x18, 0x46, 0x0a, 0x2d, 0x97, 0xd4, 0xe7, 0xae, 0x9f, 0xc8, 0x03, 0x06, 0x06, 0x3f, 0xc3, + 0x1d, 0xba, 0x8f, 0x8a, 0x94, 0xfb, 0x32, 0x67, 0xcc, 0x64, 0x03, 0x29, 0xf7, 0xf5, 0xdd, 0x4a, 0x3e, 0xaf, 0x9d, + 0xab, 0x8f, 0xba, 0xfd, 0x37, 0xef, 0x4f, 0x2c, 0xb9, 0x7b, 0x8f, 0x5b, 0x51, 0xb7, 0x7f, 0x2c, 0x5c, 0x2a, 0x32, + 0x2b, 0x80, 0xc8, 0xac, 0x00, 0x4b, 0x5d, 0x2a, 0x03, 0x81, 0xb6, 0xa2, 0x25, 0xa7, 0x2d, 0x4c, 0x0a, 0xe9, 0x0c, + 0x9e, 0xce, 0x63, 0xce, 0xe0, 0x9b, 0x47, 0x2d, 0x91, 0x12, 0x20, 0x52, 0x0c, 0xf4, 0x19, 0x55, 0xa5, 0x3c, 0x5e, + 0xf2, 0x44, 0xbb, 0x8e, 0xc7, 0x2c, 0xa6, 0xfa, 0x54, 0xaa, 0xea, 0xaa, 0xcc, 0x07, 0x5a, 0xaf, 0x9d, 0xcf, 0x2f, + 0x21, 0x27, 0x42, 0x67, 0x1f, 0x7d, 0x50, 0x0d, 0x8e, 0xc5, 0x50, 0x10, 0xd8, 0x97, 0x52, 0x5c, 0x7f, 0xdd, 0xb5, + 0xbe, 0xa4, 0x6a, 0xf6, 0x4a, 0x80, 0xc0, 0x4d, 0x1e, 0xd1, 0x7e, 0xbf, 0xf4, 0x26, 0x9b, 0xef, 0x8a, 0xe3, 0x56, + 0xb4, 0xdf, 0xbf, 0xf0, 0x26, 0xaa, 0xbf, 0x97, 0xe9, 0x64, 0x73, 0x5f, 0x71, 0x3a, 0x19, 0x88, 0x63, 0xf2, 0xf2, + 0xca, 0x27, 0xad, 0x1b, 0xa7, 0xb1, 0xdd, 0x3f, 0x56, 0xba, 0x82, 0x25, 0xa2, 0xee, 0xf6, 0x61, 0x5b, 0x9f, 0xbc, + 0x8f, 0xd3, 0x09, 0xec, 0x57, 0xd9, 0xc4, 0x18, 0xa4, 0xe6, 0x90, 0x8f, 0x3a, 0xfd, 0x63, 0xdf, 0x12, 0xac, 0x47, + 0xf0, 0x96, 0xdc, 0x6b, 0x41, 0xe3, 0x28, 0x9d, 0x52, 0x97, 0xa5, 0xad, 0x6b, 0x7a, 0xd9, 0xf4, 0x67, 0xac, 0xf2, + 0x7e, 0x83, 0x4e, 0x52, 0x0e, 0x99, 0xae, 0x64, 0x60, 0x75, 0x2b, 0x6f, 0xdc, 0x01, 0x98, 0x44, 0xda, 0x73, 0x27, + 0x5c, 0x76, 0x06, 0x58, 0x69, 0xff, 0xb8, 0xe5, 0xaf, 0x60, 0x44, 0x6c, 0xc5, 0x42, 0xf9, 0xe1, 0xc1, 0xee, 0xb9, + 0x14, 0xe9, 0x5f, 0x52, 0x5a, 0x68, 0x7f, 0xbd, 0x92, 0xe3, 0x85, 0xdd, 0xff, 0xd7, 0xff, 0xf1, 0xbf, 0x94, 0x0b, + 0xfe, 0xb8, 0x15, 0x75, 0x74, 0x5f, 0x2b, 0xab, 0x52, 0x1c, 0xc3, 0x3d, 0x36, 0x55, 0xcc, 0x98, 0xde, 0x34, 0x27, + 0x19, 0x0b, 0x9b, 0x91, 0x1f, 0x8f, 0xed, 0xfe, 0x76, 0x6c, 0xca, 0xf4, 0xc4, 0xa6, 0x8e, 0xb6, 0xae, 0x17, 0x01, + 0xbd, 0xfe, 0xa6, 0x4b, 0x19, 0x74, 0xc6, 0x97, 0xd8, 0xda, 0xe6, 0x15, 0x0d, 0xd5, 0xee, 0xab, 0x5d, 0xd3, 0x90, + 0xa8, 0x4f, 0x46, 0x2b, 0x06, 0x99, 0xd4, 0x6e, 0x67, 0x28, 0x6c, 0xab, 0x8c, 0x79, 0xfd, 0xdf, 0xff, 0xf9, 0x5f, + 0xfe, 0x9b, 0x7e, 0x84, 0x50, 0xd6, 0xbf, 0xfe, 0xf7, 0xff, 0xfc, 0x7f, 0xfe, 0xf7, 0x7f, 0x85, 0xf4, 0x34, 0x15, + 0xee, 0x12, 0x4c, 0xc5, 0xaa, 0x62, 0x5d, 0x92, 0xbb, 0x58, 0x70, 0xe8, 0x6d, 0xca, 0x72, 0xce, 0x82, 0xfa, 0x7d, + 0x0d, 0x67, 0x62, 0x40, 0xb1, 0x33, 0x15, 0x74, 0x62, 0x87, 0x17, 0x15, 0x41, 0xd5, 0x50, 0x2e, 0x08, 0xb7, 0x38, + 0x6e, 0x01, 0xbe, 0xef, 0x77, 0xdd, 0x8c, 0x5b, 0x2e, 0xc7, 0x42, 0x93, 0x09, 0x94, 0x14, 0x55, 0xb9, 0x05, 0xa1, + 0x97, 0x05, 0x3c, 0x7a, 0x5d, 0xa3, 0x58, 0xac, 0x5e, 0xad, 0x4d, 0xef, 0xe7, 0x79, 0xce, 0xd9, 0x18, 0x50, 0x2e, + 0xdd, 0xc8, 0x22, 0xca, 0xdd, 0x04, 0x55, 0x32, 0xbe, 0x2d, 0x44, 0x2f, 0x92, 0x40, 0x0f, 0x8e, 0xfe, 0x54, 0xfc, + 0x79, 0x0a, 0x0a, 0x9b, 0xe5, 0x4c, 0xfd, 0x1b, 0x65, 0xbd, 0x3f, 0x6c, 0xb7, 0x67, 0x37, 0x68, 0x51, 0x8d, 0x80, + 0xb7, 0x0d, 0x26, 0xe8, 0xd8, 0xec, 0x50, 0x84, 0xc7, 0x4b, 0x2f, 0x77, 0xdb, 0x02, 0x57, 0xb9, 0xd5, 0x2e, 0x8a, + 0xaf, 0x16, 0xc2, 0xd1, 0xca, 0x7e, 0x85, 0x30, 0xb6, 0xf2, 0x49, 0x5f, 0xa6, 0xe6, 0xe4, 0x16, 0x46, 0xab, 0xae, + 0x6c, 0x15, 0x75, 0xd6, 0x6f, 0x6e, 0x31, 0xc3, 0xf0, 0x66, 0x00, 0xfd, 0x00, 0x42, 0xe2, 0x51, 0x07, 0x47, 0xdd, + 0x45, 0xd9, 0x3d, 0xe7, 0xe9, 0xd4, 0x8c, 0xbb, 0x53, 0x9f, 0x06, 0x74, 0xac, 0x7d, 0xf9, 0xea, 0xbd, 0x8c, 0xa9, + 0x17, 0xd1, 0xfe, 0x86, 0xb1, 0x14, 0x48, 0x22, 0xde, 0x6e, 0xb5, 0x8b, 0x2f, 0x61, 0x07, 0x2e, 0xc6, 0x71, 0xea, + 0x73, 0x4f, 0x10, 0x6c, 0xcf, 0x8c, 0xde, 0xfb, 0xc0, 0x93, 0xd2, 0x85, 0x01, 0x4f, 0x4f, 0x56, 0x05, 0xaf, 0x7a, + 0xfd, 0x06, 0xc7, 0xc2, 0x15, 0xcd, 0xcd, 0xae, 0xa4, 0x53, 0xee, 0x3b, 0x15, 0x14, 0x7f, 0x5e, 0xf3, 0x66, 0x29, + 0x81, 0xd4, 0x45, 0x9b, 0xdf, 0x4b, 0xb1, 0x2f, 0xdf, 0x7e, 0xcf, 0x1d, 0x5b, 0x80, 0x69, 0xaf, 0xd6, 0x12, 0x85, + 0x50, 0xeb, 0x39, 0xf9, 0xae, 0xb4, 0xa8, 0xfc, 0xd9, 0x4c, 0x54, 0x44, 0xbd, 0xe3, 0x96, 0x54, 0x84, 0x81, 0x7b, + 0x88, 0x8c, 0x0f, 0x99, 0x60, 0xa1, 0x2a, 0xa9, 0xad, 0x20, 0x7f, 0xa9, 0xd4, 0x0b, 0xf8, 0x94, 0x78, 0xff, 0xff, + 0x01, 0x65, 0x21, 0x07, 0x4b, 0xe3, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0xc3, 0x97, 0x11, 0x55, 0xb5, 0x65, 0x2c, 0x8a, 0x8a, 0x55, 0x0b, 0xd0, 0xba, 0x80, 0x1b, 0x32, 0xb0, 0x81, - 0x4f, 0x27, 0x63, 0xf1, 0x7e, 0x88, 0xe3, 0xd8, 0x52, 0x84, 0x55, 0xe8, 0x35, 0x5b, 0x2b, 0x82, 0xe1, 0xed, 0x1f, - 0xfd, 0xde, 0x63, 0x38, 0x3a, 0x71, 0x78, 0xb0, 0x42, 0x17, 0x15, 0x54, 0x23, 0xe1, 0xaa, 0x28, 0x11, 0x94, 0x23, - 0xb4, 0xf4, 0x91, 0x3c, 0xfc, 0xff, 0xab, 0x5a, 0x56, 0x2d, 0x07, 0xcb, 0x09, 0x6f, 0x79, 0x15, 0x1c, 0xd2, 0x87, - 0x40, 0x38, 0x97, 0xce, 0x9d, 0x87, 0x67, 0xe0, 0xa0, 0x4d, 0x49, 0x1a, 0x27, 0xf0, 0xf5, 0x8d, 0x9d, 0x72, 0x02, - 0x12, 0x45, 0x49, 0x2b, 0x48, 0xe0, 0x6a, 0xff, 0x5f, 0x6d, 0x55, 0xf1, 0x54, 0x12, 0xc1, 0x2f, 0x1e, 0xc8, 0x83, - 0x0c, 0x92, 0x0d, 0x75, 0xd8, 0xf5, 0xdd, 0xc7, 0x75, 0x50, 0x6c, 0xa1, 0xb2, 0x85, 0xad, 0x6d, 0x63, 0xd1, 0x96, - 0x38, 0xaa, 0x65, 0x4d, 0x1e, 0x6c, 0x14, 0x4e, 0x10, 0xed, 0xbe, 0xaf, 0xf3, 0xff, 0xeb, 0xf7, 0xc6, 0x6f, 0xd9, - 0x77, 0x24, 0x81, 0xbb, 0x26, 0xd0, 0x31, 0x81, 0xae, 0x49, 0x8d, 0x23, 0xb0, 0x5a, 0x62, 0xe5, 0x49, 0x4a, 0x17, - 0xa7, 0x66, 0xda, 0xb2, 0x6a, 0x9d, 0xf0, 0x4d, 0xa4, 0x1d, 0x59, 0xe6, 0x00, 0x55, 0x58, 0x63, 0xf9, 0x0c, 0xfe, - 0xa0, 0xbd, 0xa5, 0xfa, 0x75, 0x9f, 0xcb, 0x49, 0x22, 0x0c, 0xe1, 0xd5, 0xb0, 0xd8, 0x13, 0x72, 0xd3, 0x25, 0x4e, - 0x48, 0x1b, 0xa2, 0x76, 0x4f, 0x5c, 0x42, 0xe2, 0x98, 0x6d, 0x9b, 0x80, 0x3e, 0x69, 0x90, 0xcf, 0x69, 0xa5, 0x2e, - 0xc0, 0x47, 0x4f, 0x5c, 0xac, 0x83, 0x99, 0xe7, 0xfc, 0xff, 0xdf, 0x96, 0x7e, 0xa5, 0xd5, 0x2d, 0x98, 0x45, 0x4a, - 0x81, 0x52, 0x20, 0x67, 0x6d, 0x8d, 0xbd, 0xc4, 0x49, 0xb0, 0x41, 0xa8, 0xba, 0xf7, 0xbe, 0x77, 0x47, 0x45, 0x3d, - 0xea, 0xaa, 0x52, 0xcf, 0x6f, 0xb2, 0x2d, 0xea, 0x11, 0x1f, 0x0b, 0x6d, 0x7e, 0xef, 0x55, 0x75, 0xab, 0xaa, 0x5b, - 0xf6, 0x74, 0x4b, 0xf6, 0x18, 0xe7, 0x1c, 0x59, 0xf6, 0xfe, 0x01, 0xd2, 0xff, 0x8b, 0xe4, 0xf9, 0xe7, 0x2c, 0x52, - 0x84, 0x51, 0x02, 0x9c, 0xed, 0xc9, 0x31, 0x0a, 0x01, 0xd3, 0xcd, 0x36, 0xdc, 0x74, 0x55, 0x87, 0x2a, 0x51, 0x4e, - 0xcf, 0x28, 0xc5, 0x71, 0xec, 0x2d, 0x23, 0x97, 0xcb, 0x55, 0x7d, 0xfd, 0xd6, 0x83, 0x1d, 0xc6, 0x0a, 0x21, 0x9e, - 0x5d, 0x46, 0xd3, 0xfc, 0xcd, 0xca, 0x21, 0x21, 0x24, 0xce, 0x75, 0x5d, 0x7f, 0xa6, 0x0d, 0xf7, 0x70, 0x16, 0xd1, - 0xc4, 0x38, 0xe2, 0x00, 0xf9, 0x14, 0x84, 0xa1, 0x23, 0x9d, 0x6e, 0xcc, 0x71, 0xee, 0xa1, 0xc8, 0x1a, 0xc1, 0xb4, - 0xda, 0x43, 0x30, 0xcf, 0xe1, 0xc0, 0x01, 0x34, 0xb2, 0x3c, 0xb7, 0x7f, 0xf5, 0x81, 0xad, 0xdb, 0xf5, 0x23, 0x32, - 0xe8, 0xf1, 0x66, 0xa5, 0x04, 0xdc, 0x46, 0x71, 0x3d, 0x0e, 0x94, 0x8d, 0x00, 0x35, 0xab, 0xb1, 0x1b, 0x92, 0xef, - 0xcd, 0xef, 0x3f, 0x1d, 0x1c, 0x84, 0x98, 0xe9, 0x3f, 0x54, 0xae, 0x9d, 0x84, 0x17, 0xa2, 0x2e, 0x69, 0x5b, 0xc0, - 0xd5, 0x10, 0x62, 0x1e, 0x06, 0x1e, 0xa2, 0xe0, 0xb5, 0xd7, 0xe2, 0xe9, 0xb4, 0xc6, 0x33, 0x43, 0xc6, 0x96, 0x8b, - 0x5c, 0x0f, 0xd4, 0x5c, 0x18, 0x1c, 0x0e, 0xba, 0x54, 0x85, 0xf3, 0x4c, 0x2e, 0xa2, 0x4d, 0xd7, 0x9a, 0x23, 0xba, - 0x9a, 0xf4, 0xba, 0xa4, 0xf4, 0xdc, 0xdf, 0x7c, 0x53, 0x67, 0xdc, 0x17, 0x7a, 0x7e, 0x49, 0x87, 0x3f, 0xe3, 0xbc, - 0x98, 0x12, 0x88, 0xe8, 0x78, 0x4f, 0x91, 0x72, 0x75, 0x32, 0xc8, 0xd7, 0x95, 0xca, 0xd2, 0xcf, 0x7f, 0x83, 0x7d, - 0x06, 0x6e, 0x11, 0x1b, 0xc7, 0xf9, 0x71, 0x99, 0x5f, 0x17, 0x63, 0x5e, 0x35, 0xf3, 0xd5, 0xe1, 0x70, 0xd9, 0xbb, - 0xc1, 0x75, 0x93, 0x66, 0x1f, 0xd6, 0x83, 0xa5, 0xdb, 0x37, 0x7f, 0x59, 0xd3, 0xe6, 0x66, 0x37, 0x45, 0x5b, 0x5b, - 0x7e, 0xf1, 0xd4, 0xd3, 0x0b, 0xb5, 0x90, 0xaf, 0xeb, 0x69, 0xc2, 0xcd, 0x5c, 0x30, 0xca, 0x16, 0xda, 0x1d, 0xf0, - 0xb9, 0xea, 0xb2, 0xfc, 0xba, 0x5d, 0x25, 0xf3, 0xe3, 0xe4, 0x1b, 0xf1, 0xdb, 0x25, 0x73, 0x7d, 0x31, 0x43, 0x95, - 0x9a, 0x88, 0x6a, 0xf8, 0x47, 0x20, 0x2d, 0xb7, 0xd7, 0x78, 0x6f, 0xe2, 0xbb, 0xa1, 0x85, 0x75, 0xa4, 0xae, 0x6a, - 0x11, 0x25, 0xb7, 0xdf, 0xcd, 0xab, 0xa1, 0x2c, 0x20, 0xff, 0xd6, 0x84, 0xc8, 0x33, 0xee, 0x86, 0x44, 0x55, 0x79, - 0x98, 0x27, 0x37, 0x80, 0x50, 0xa9, 0x8e, 0x88, 0xb5, 0xcc, 0x13, 0xf0, 0x74, 0x38, 0xc7, 0xd8, 0x86, 0xc0, 0x7b, - 0x1d, 0x9e, 0xa6, 0x3b, 0xf3, 0xc3, 0xb5, 0x00, 0x77, 0xc3, 0xca, 0x83, 0x98, 0xba, 0x41, 0x85, 0x3c, 0xd9, 0x29, - 0xc8, 0x79, 0x52, 0x60, 0x25, 0xbb, 0xa6, 0x39, 0xca, 0x76, 0xf2, 0xa6, 0x7d, 0x57, 0xa3, 0xcc, 0xd6, 0xb8, 0xe7, - 0xcd, 0xdf, 0xf9, 0x24, 0x84, 0x14, 0x7f, 0x63, 0x51, 0x9b, 0x98, 0x4a, 0x48, 0xb8, 0x74, 0x9a, 0x74, 0xbf, 0xf1, - 0x9d, 0x48, 0x62, 0x9e, 0xe7, 0x8a, 0x92, 0x75, 0xc8, 0x64, 0x1b, 0xbf, 0xde, 0x54, 0x9b, 0xb6, 0x5c, 0x42, 0xc3, - 0x1a, 0x1e, 0x3f, 0xa7, 0x59, 0xa4, 0x90, 0x50, 0xb2, 0xa7, 0x25, 0x61, 0x65, 0x41, 0xde, 0x83, 0x2f, 0x53, 0x38, - 0x7c, 0xb9, 0xd3, 0xe7, 0x0b, 0x42, 0x59, 0xb8, 0xa9, 0xc0, 0xc4, 0x7b, 0x1b, 0x2b, 0xcd, 0xd7, 0x51, 0x43, 0x30, - 0x93, 0x3f, 0x13, 0xd6, 0x31, 0xfe, 0x55, 0x33, 0xb5, 0x25, 0x44, 0x09, 0x3e, 0xfc, 0x5c, 0x85, 0xac, 0x1b, 0xc1, - 0xd4, 0xb4, 0x54, 0xf2, 0x05, 0x97, 0x72, 0x0e, 0x24, 0x80, 0x50, 0x03, 0x26, 0x7f, 0xce, 0x9a, 0xbe, 0x9f, 0xf1, - 0xf2, 0x7e, 0xc4, 0x9b, 0x26, 0x24, 0x96, 0x37, 0x92, 0x0d, 0x75, 0xff, 0x64, 0xa0, 0xec, 0x38, 0xa6, 0x7a, 0xc8, - 0x7c, 0x1f, 0x76, 0x7b, 0x1a, 0x19, 0x21, 0xc8, 0x7d, 0x33, 0x42, 0xc3, 0x6c, 0x5e, 0xf0, 0x0b, 0x41, 0xaf, 0x8c, - 0x34, 0xa9, 0x8a, 0x2a, 0xbc, 0xff, 0xf5, 0x0b, 0x21, 0x7a, 0x1c, 0xea, 0xd1, 0xff, 0x4e, 0xe9, 0x2e, 0xd5, 0x12, - 0xc3, 0x7a, 0x28, 0xbc, 0x54, 0xe7, 0x95, 0xaa, 0xcd, 0x05, 0x02, 0x30, 0xe4, 0x56, 0x22, 0xfb, 0x9b, 0x91, 0x04, - 0xec, 0x30, 0x53, 0xfe, 0x75, 0x2d, 0xc2, 0x32, 0xc1, 0xe5, 0xcf, 0x59, 0x65, 0xaf, 0xe2, 0x93, 0x94, 0x3e, 0x9a, - 0x23, 0xaa, 0x2c, 0x61, 0x7c, 0x59, 0x10, 0xa4, 0x3c, 0x9b, 0x17, 0x9b, 0xc6, 0x8d, 0xdc, 0x51, 0x7b, 0x10, 0xaf, - 0x72, 0x1d, 0xc7, 0x12, 0x95, 0xad, 0x72, 0x02, 0x90, 0x3c, 0xbb, 0xef, 0x06, 0x61, 0xb0, 0x9c, 0x10, 0xa9, 0x2e, - 0x23, 0xfc, 0x73, 0xae, 0x0a, 0x6e, 0x25, 0x9a, 0x55, 0xcd, 0xfd, 0x37, 0xe8, 0x62, 0xb7, 0xe0, 0x8e, 0xcf, 0xeb, - 0xb9, 0xe1, 0x2a, 0xbc, 0x29, 0xfc, 0xb6, 0x64, 0x90, 0x5e, 0x59, 0x8e, 0x26, 0xd1, 0xaa, 0x8e, 0x38, 0x89, 0x68, - 0x81, 0xb1, 0xd9, 0x7f, 0xd2, 0xe2, 0xbd, 0xa0, 0x13, 0x2a, 0x6d, 0x2f, 0xd5, 0xe5, 0x74, 0xc6, 0x0f, 0x2e, 0xa8, - 0xd7, 0xc5, 0xf9, 0x94, 0x45, 0x50, 0xe1, 0xdb, 0xd4, 0x9f, 0xe9, 0x9c, 0x7a, 0x9f, 0x2f, 0x37, 0xcd, 0x73, 0x8f, - 0x65, 0xb7, 0x5b, 0x6b, 0x14, 0xb7, 0xae, 0x42, 0x6a, 0xc3, 0x8d, 0x97, 0x71, 0x5b, 0x2b, 0x28, 0xae, 0x08, 0x4f, - 0xb5, 0xa6, 0x89, 0x34, 0x76, 0x89, 0x53, 0x36, 0xc6, 0xfb, 0x77, 0x4b, 0xdc, 0x57, 0x4b, 0x99, 0x32, 0xc4, 0x34, - 0x3c, 0xa1, 0xee, 0x5e, 0x9a, 0x1a, 0x0c, 0x0b, 0x1e, 0xbb, 0x45, 0x7c, 0x21, 0x55, 0x09, 0x0a, 0x46, 0xe5, 0x34, - 0x4f, 0xbc, 0x78, 0xe8, 0x5d, 0xb0, 0x04, 0x88, 0xb7, 0xe8, 0xf2, 0x7e, 0x01, 0xc1, 0x8a, 0xd6, 0x0a, 0xb8, 0x13, - 0x4d, 0x90, 0xf0, 0x02, 0x1d, 0x06, 0x19, 0xea, 0x0d, 0xc8, 0x66, 0x95, 0xe8, 0x9d, 0xb3, 0x63, 0x50, 0x5a, 0xcd, - 0xa2, 0xbd, 0x76, 0x9e, 0xde, 0x05, 0xb6, 0xe4, 0xfc, 0x9c, 0x66, 0x63, 0xc6, 0x48, 0x9c, 0x5e, 0x14, 0x31, 0x65, - 0x9e, 0xa8, 0xb9, 0xb6, 0x44, 0x75, 0x9a, 0xbb, 0x3b, 0x63, 0xc6, 0x89, 0xfd, 0x7a, 0x15, 0x7d, 0xd7, 0xc7, 0x55, - 0xcd, 0x80, 0x0b, 0xcc, 0x86, 0xb5, 0xf1, 0xff, 0x69, 0x28, 0x94, 0x82, 0xbf, 0x9a, 0x75, 0x83, 0xe2, 0x5e, 0x2c, - 0xa7, 0xae, 0x27, 0x42, 0xd7, 0xdf, 0x19, 0xd8, 0x8f, 0x77, 0x84, 0x4f, 0x50, 0x46, 0x36, 0x76, 0xfb, 0xa6, 0x34, - 0xc2, 0xf5, 0x2a, 0xf9, 0xbc, 0x7f, 0x6a, 0xfb, 0x86, 0xfa, 0xfc, 0x58, 0x1c, 0xfb, 0x57, 0x6f, 0x28, 0xa6, 0x0e, - 0xdc, 0xc5, 0xec, 0xb9, 0x68, 0xbe, 0xb5, 0xce, 0xd1, 0x83, 0x87, 0xfc, 0x30, 0xec, 0x9d, 0x6e, 0x2c, 0xa6, 0xa6, - 0x8d, 0x07, 0x1a, 0x43, 0x02, 0xbf, 0x66, 0x0e, 0x6b, 0xf5, 0x40, 0x1c, 0xb1, 0x6a, 0x93, 0x53, 0x91, 0xfa, 0x8d, - 0x2a, 0x63, 0x85, 0x79, 0x2d, 0xae, 0x64, 0x21, 0x95, 0x75, 0x18, 0x28, 0x64, 0xa4, 0x3d, 0xa3, 0xdc, 0xb3, 0x82, - 0x87, 0xb9, 0xfe, 0x5d, 0x70, 0x87, 0xaf, 0xef, 0xed, 0x87, 0x26, 0xbe, 0x85, 0xf1, 0xa2, 0xec, 0x54, 0x66, 0x89, - 0x72, 0xcc, 0x02, 0x51, 0x24, 0xcf, 0x88, 0xe6, 0xf8, 0x9a, 0x8d, 0x21, 0x01, 0x72, 0x23, 0x20, 0xc7, 0xdd, 0x7b, - 0xc5, 0xb1, 0x4d, 0x88, 0x40, 0xa1, 0xdd, 0x2d, 0x90, 0x85, 0x82, 0x4c, 0x12, 0x49, 0xee, 0x8e, 0x86, 0x12, 0xfb, - 0x63, 0xf5, 0xd2, 0x85, 0x5b, 0x44, 0xb2, 0xb1, 0x1b, 0x12, 0x08, 0xa4, 0xf1, 0x3e, 0xd5, 0xe7, 0x02, 0x61, 0x29, - 0x40, 0xc7, 0xc1, 0x3f, 0x49, 0x09, 0xab, 0x99, 0x0c, 0x69, 0x36, 0x70, 0x57, 0xe6, 0x65, 0x37, 0xec, 0x7f, 0x67, - 0xa3, 0x02, 0xc2, 0xf1, 0xa1, 0x7f, 0xec, 0x26, 0x28, 0x32, 0x50, 0xb4, 0x42, 0x3d, 0x14, 0x94, 0x6e, 0x28, 0x62, - 0x50, 0xed, 0x8f, 0x9b, 0xc2, 0xdc, 0xdd, 0xc0, 0x64, 0x89, 0x8a, 0xd6, 0x3c, 0x79, 0x2f, 0xea, 0xdb, 0x88, 0xc1, - 0x27, 0x33, 0x76, 0xe8, 0x66, 0xa2, 0x92, 0x5d, 0xaa, 0x0c, 0xac, 0x83, 0x75, 0x2a, 0x95, 0x02, 0x2f, 0x6a, 0x72, - 0xf7, 0x2d, 0x20, 0x2a, 0xde, 0xa9, 0x8b, 0xce, 0xa0, 0x85, 0x57, 0x5a, 0xe8, 0x0c, 0xfa, 0xe5, 0x8c, 0x42, 0xd2, - 0xb1, 0xa6, 0x76, 0xb9, 0x4e, 0x54, 0x0c, 0xf6, 0x84, 0x0d, 0x4a, 0xb4, 0xfc, 0x43, 0x9b, 0x92, 0x88, 0x30, 0xd7, - 0x3d, 0x1f, 0xfe, 0x71, 0x66, 0x48, 0x1f, 0xde, 0xea, 0x21, 0x95, 0x24, 0xc2, 0x27, 0x7c, 0x39, 0x88, 0xd7, 0x1d, - 0x90, 0x14, 0xb8, 0x77, 0x5d, 0xb1, 0x76, 0x2f, 0x3b, 0xe2, 0xe5, 0x64, 0x4b, 0xcd, 0xb8, 0x2e, 0x53, 0xd0, 0xa8, - 0xe3, 0x78, 0xcb, 0xa7, 0xb0, 0xe1, 0x5d, 0xe9, 0x33, 0x3a, 0x16, 0x98, 0x25, 0x90, 0x08, 0x91, 0xde, 0x3f, 0xba, - 0x73, 0xa1, 0xe6, 0x75, 0x92, 0x19, 0x0a, 0x91, 0x5a, 0x25, 0x37, 0x41, 0x85, 0xe3, 0xa9, 0x42, 0xec, 0x48, 0x4a, - 0xca, 0x84, 0x23, 0x4c, 0x8f, 0x2b, 0xa2, 0xa3, 0xe4, 0x3e, 0x69, 0x2a, 0x19, 0x73, 0xf5, 0xbf, 0x4c, 0x69, 0x82, - 0xd9, 0x95, 0xc3, 0x21, 0x11, 0x20, 0x65, 0x5a, 0x6a, 0x37, 0x78, 0x3f, 0x22, 0x3e, 0x15, 0x80, 0x4a, 0x44, 0xa2, - 0x70, 0xcf, 0x2e, 0xfa, 0xb3, 0x28, 0x21, 0x62, 0x30, 0x13, 0xd3, 0x59, 0xf2, 0xfe, 0x5a, 0xe9, 0xe4, 0x75, 0xa3, - 0xab, 0x51, 0xcd, 0xeb, 0x07, 0x95, 0x8f, 0x98, 0x2b, 0x97, 0x4f, 0x02, 0x19, 0x5b, 0x4d, 0xae, 0xa9, 0xcc, 0xb3, - 0xb2, 0xb8, 0x0a, 0x0b, 0x54, 0x1b, 0xb5, 0x80, 0xeb, 0x73, 0x9d, 0xdb, 0x10, 0x75, 0xce, 0x41, 0xe4, 0x80, 0x1d, - 0x56, 0xb3, 0xd0, 0xd9, 0x31, 0x7d, 0x70, 0x99, 0x1c, 0xe1, 0x34, 0x9d, 0xb9, 0x63, 0x68, 0xa7, 0xf7, 0x22, 0x39, - 0x0c, 0x2e, 0x56, 0xa0, 0x0b, 0x28, 0xa7, 0x86, 0x31, 0x8a, 0x1c, 0x50, 0x62, 0xa9, 0x94, 0x72, 0x01, 0x40, 0x8b, - 0xa2, 0xab, 0xa0, 0x0c, 0x85, 0x2a, 0x69, 0x64, 0x0b, 0x07, 0x2b, 0x8e, 0x11, 0xaf, 0xbd, 0xfa, 0x21, 0x10, 0xb2, - 0x68, 0xbb, 0x06, 0xea, 0x40, 0xa9, 0xe6, 0xad, 0x7f, 0x17, 0xb9, 0xe8, 0xc2, 0xb3, 0x32, 0x40, 0x99, 0x3f, 0xaa, - 0x36, 0xeb, 0x4e, 0x26, 0x2f, 0xae, 0x8c, 0x17, 0xaa, 0x6c, 0x78, 0x90, 0x3c, 0x4b, 0x64, 0x4a, 0x28, 0x70, 0xca, - 0x92, 0xc6, 0x3e, 0xf1, 0xc1, 0x8e, 0xfc, 0xf6, 0x84, 0xb9, 0x39, 0x56, 0x46, 0x35, 0xe2, 0xe9, 0x8b, 0xaa, 0xeb, - 0x9a, 0x21, 0x42, 0xcd, 0x3f, 0x7c, 0xed, 0xac, 0xff, 0xa6, 0x1b, 0x8d, 0xde, 0x91, 0x75, 0xd6, 0xe4, 0xdf, 0x86, - 0xc1, 0x9d, 0xce, 0x9e, 0xd5, 0x55, 0x83, 0x58, 0x2b, 0x28, 0x02, 0xe1, 0x80, 0x07, 0xbf, 0xa9, 0x8e, 0xf6, 0x9b, - 0x80, 0x25, 0xef, 0x18, 0xda, 0x93, 0xea, 0x8a, 0x09, 0x4d, 0xcb, 0xe7, 0x1f, 0x41, 0x73, 0xa5, 0x86, 0xb2, 0x2c, - 0xf8, 0xf0, 0x01, 0x65, 0x06, 0xa5, 0xca, 0x51, 0x3b, 0xdd, 0x86, 0x5c, 0x13, 0x68, 0xa2, 0x27, 0x41, 0x9e, 0xaf, - 0xbf, 0x70, 0x57, 0xa5, 0x32, 0x90, 0x6f, 0x98, 0xec, 0xc2, 0x5d, 0xb2, 0xba, 0xca, 0x39, 0xfb, 0x2f, 0x51, 0xcc, - 0xf3, 0xbc, 0xa7, 0x23, 0x23, 0xbd, 0x67, 0x47, 0x6e, 0x6a, 0xc5, 0xf9, 0x29, 0x4a, 0x48, 0x16, 0x6d, 0x59, 0x68, - 0xcb, 0x11, 0x8c, 0x81, 0x12, 0x3a, 0x12, 0xb9, 0x0c, 0x59, 0xd6, 0xb0, 0xcc, 0xbe, 0xe5, 0x7f, 0xe3, 0x90, 0x49, - 0x4a, 0x72, 0x9a, 0x5c, 0xf7, 0xb2, 0xb8, 0xec, 0xca, 0x12, 0x95, 0xd8, 0x51, 0x6b, 0xba, 0x12, 0x43, 0xf2, 0xd5, - 0x7b, 0xda, 0xb4, 0xd4, 0x7e, 0x84, 0x74, 0x67, 0x46, 0x0a, 0xfa, 0xa0, 0xea, 0x6d, 0x74, 0xc1, 0xd1, 0x06, 0x90, - 0x63, 0x49, 0xf2, 0x3c, 0x29, 0x06, 0xd9, 0x44, 0x0a, 0x25, 0xea, 0x49, 0x0e, 0x63, 0x59, 0xc2, 0xdc, 0x8f, 0x12, - 0xcc, 0xd2, 0xb2, 0x8c, 0x91, 0x3e, 0x2d, 0x62, 0xa7, 0x14, 0x8f, 0x50, 0xf9, 0x38, 0xbb, 0xef, 0xa6, 0xa1, 0x24, - 0xd5, 0x49, 0x99, 0x20, 0x68, 0xcf, 0x81, 0xd0, 0x31, 0x01, 0xf3, 0xbd, 0xa9, 0xe8, 0x2f, 0x7f, 0x8e, 0x2b, 0x16, - 0xf6, 0x1f, 0x52, 0x3c, 0x33, 0xb3, 0x9b, 0x5f, 0x59, 0xce, 0x91, 0xe5, 0xcc, 0xd0, 0x49, 0x9b, 0x42, 0x0a, 0x1b, - 0x67, 0xbb, 0xfe, 0x82, 0x34, 0xef, 0x8d, 0x0e, 0x47, 0x7a, 0x09, 0xbf, 0x2f, 0x04, 0xd7, 0x87, 0x24, 0x8c, 0x90, - 0xab, 0x2e, 0xaa, 0xdd, 0x3c, 0x79, 0x91, 0xc2, 0x6a, 0x47, 0x47, 0x5c, 0x8a, 0xdd, 0xdb, 0x59, 0xc4, 0x5a, 0x91, - 0x0a, 0xf6, 0xbd, 0x32, 0x91, 0x89, 0xcd, 0xa0, 0x04, 0x67, 0x2b, 0x03, 0x7a, 0x56, 0xbb, 0x78, 0x26, 0xaa, 0xa3, - 0x26, 0xd4, 0x59, 0x81, 0x67, 0xa8, 0x01, 0x64, 0xeb, 0xbc, 0x69, 0xc9, 0x9e, 0x0e, 0x96, 0x93, 0xd4, 0x50, 0x9a, - 0x45, 0x04, 0xfe, 0xd0, 0xdd, 0xde, 0x93, 0x88, 0x0c, 0x03, 0x3f, 0xce, 0x46, 0x94, 0x07, 0xa8, 0x19, 0xc3, 0x89, - 0xa5, 0x49, 0x92, 0x35, 0x5c, 0x98, 0xf7, 0x56, 0x04, 0x71, 0xdf, 0xc7, 0xb6, 0x88, 0xfa, 0xdb, 0x51, 0x26, 0xd8, - 0x57, 0xeb, 0x04, 0xa2, 0x62, 0x16, 0xca, 0xe4, 0x5b, 0x42, 0x78, 0xcb, 0x03, 0xc3, 0xd5, 0xf9, 0x86, 0xd9, 0x58, - 0xb5, 0x92, 0x23, 0x5f, 0x55, 0x0b, 0x3b, 0x20, 0x1c, 0xb5, 0x2f, 0x1d, 0xeb, 0x67, 0xb7, 0x2a, 0x7a, 0x3d, 0x2d, - 0xbf, 0xda, 0xd4, 0x95, 0xaa, 0x03, 0xb9, 0x18, 0xa3, 0x6c, 0xc6, 0xa4, 0xd1, 0x00, 0xb5, 0x80, 0x5c, 0x59, 0x47, - 0xaa, 0x78, 0x9a, 0x96, 0x70, 0x88, 0x07, 0x1e, 0xaf, 0xae, 0x1d, 0xe9, 0x25, 0xdb, 0xa1, 0x03, 0xda, 0x7a, 0x87, - 0x6f, 0xbd, 0x76, 0x2d, 0xc6, 0x8d, 0x05, 0xbc, 0x01, 0xa0, 0x12, 0x95, 0x0a, 0x2a, 0xd1, 0x20, 0xe5, 0x52, 0xc5, - 0x75, 0xd0, 0x29, 0xd7, 0xb4, 0x58, 0x59, 0x2b, 0xbc, 0xcb, 0x03, 0xfc, 0x69, 0x07, 0xa4, 0xb2, 0xae, 0x82, 0x62, - 0xd1, 0xfd, 0x16, 0x84, 0x14, 0xe2, 0xcd, 0xf4, 0xcd, 0x1c, 0xcc, 0xc9, 0x92, 0x35, 0x5f, 0xcb, 0x13, 0x61, 0xb1, - 0x72, 0x6b, 0x4e, 0xce, 0x91, 0x51, 0x49, 0xdf, 0xde, 0x03, 0xa2, 0x6e, 0x76, 0x79, 0xbf, 0xf8, 0xd1, 0x33, 0xee, - 0x29, 0xf0, 0xf1, 0x76, 0xbf, 0x1b, 0x1c, 0x7e, 0x78, 0xcb, 0xe1, 0x90, 0x33, 0x48, 0x43, 0x1a, 0x1b, 0xc8, 0x10, - 0xbc, 0x58, 0x15, 0x16, 0xfc, 0xb1, 0x6e, 0x75, 0x89, 0xe8, 0x3c, 0x05, 0xfc, 0x3d, 0x73, 0x17, 0xba, 0x3f, 0x20, - 0x72, 0x17, 0xf2, 0x78, 0xde, 0x65, 0x52, 0x3e, 0x42, 0x1a, 0x49, 0xee, 0xdf, 0x47, 0x9a, 0xca, 0xe4, 0x7c, 0xf3, - 0x97, 0x3c, 0x2a, 0x54, 0xd6, 0xc1, 0x14, 0x1a, 0x94, 0xd4, 0x39, 0x60, 0xd0, 0x46, 0xc6, 0x55, 0xbd, 0x1c, 0x3a, - 0x69, 0xf5, 0x79, 0xb6, 0x07, 0x99, 0x22, 0x10, 0x9d, 0x1e, 0x94, 0x51, 0x06, 0x42, 0x00, 0xbc, 0x80, 0x00, 0x68, - 0x09, 0x06, 0x03, 0xf8, 0x48, 0xf7, 0x74, 0xd0, 0x98, 0x8c, 0xd3, 0xa7, 0x92, 0x0c, 0x75, 0xa9, 0xfc, 0x9a, 0x58, - 0x8f, 0x92, 0x25, 0x62, 0x52, 0x41, 0x0b, 0xc5, 0x8c, 0xe2, 0x53, 0xf3, 0xce, 0xdc, 0xd2, 0xfb, 0x92, 0x19, 0xc6, - 0x99, 0x66, 0xa9, 0xd3, 0x46, 0xf2, 0x91, 0xba, 0x4f, 0x79, 0xb0, 0x4a, 0x10, 0x9e, 0x12, 0xaa, 0x32, 0x3c, 0xd4, - 0x35, 0x53, 0x8a, 0x67, 0x38, 0x86, 0xe3, 0xf2, 0xad, 0x45, 0xea, 0xe0, 0x83, 0xc4, 0xa7, 0xef, 0x63, 0xa8, 0xad, - 0xf9, 0xe3, 0xaf, 0x1d, 0x09, 0xbf, 0x8c, 0x32, 0xcc, 0x95, 0x88, 0x03, 0x2d, 0x53, 0x60, 0x87, 0x0b, 0x3d, 0x4f, - 0xbc, 0xdc, 0x97, 0xb8, 0xa3, 0x40, 0x0f, 0x46, 0xec, 0xa9, 0x0f, 0x99, 0xdb, 0x33, 0x91, 0xb5, 0x2d, 0xea, 0xf5, - 0xdf, 0x17, 0xdf, 0xe5, 0xc9, 0xed, 0x98, 0x27, 0x75, 0x8a, 0x0a, 0xb4, 0x52, 0x7b, 0xcb, 0x7c, 0x5f, 0xcc, 0xc2, - 0xa3, 0xef, 0x56, 0xc8, 0x18, 0x43, 0x33, 0xf2, 0x60, 0x63, 0x44, 0x50, 0x16, 0x3b, 0x98, 0xdf, 0x29, 0x24, 0x3f, - 0x3e, 0xc8, 0x41, 0x23, 0x0a, 0x82, 0xaa, 0xfd, 0x05, 0x85, 0xb2, 0x30, 0xf6, 0x37, 0xbe, 0xf6, 0x3d, 0xb2, 0xea, - 0x14, 0xcb, 0xe3, 0xec, 0x33, 0x3e, 0x67, 0x71, 0xc5, 0xaa, 0x05, 0x59, 0x91, 0x7b, 0x25, 0x9e, 0x8e, 0x59, 0xda, - 0x66, 0xd1, 0xb4, 0x4e, 0x00, 0xef, 0x63, 0xe7, 0xb6, 0xdc, 0x0f, 0xb5, 0xfe, 0x08, 0xe2, 0xea, 0x8b, 0xb0, 0xf6, - 0x3c, 0x08, 0x2b, 0xaf, 0xa2, 0x40, 0x31, 0x6d, 0x4b, 0x7e, 0xde, 0xdb, 0xcf, 0xd8, 0x46, 0xf1, 0x44, 0xb6, 0x9b, - 0xf7, 0xb6, 0x67, 0xdb, 0x38, 0x65, 0x4a, 0xfd, 0x6d, 0xae, 0x0f, 0xfe, 0x0f, 0x11, 0x3f, 0xe6, 0x40, 0xbf, 0x5f, - 0xac, 0xa6, 0xf9, 0xa9, 0xf9, 0x64, 0x85, 0xd2, 0x07, 0xf0, 0xe2, 0xb2, 0x79, 0x31, 0x7a, 0xb5, 0xf6, 0x52, 0xc4, - 0xf2, 0x20, 0xf4, 0x2d, 0x62, 0x68, 0x3b, 0xc8, 0x2e, 0xdf, 0xcf, 0x05, 0xba, 0x60, 0xac, 0x47, 0xbf, 0x01, 0x9e, - 0xb4, 0xda, 0x38, 0x64, 0xe0, 0x32, 0x99, 0x73, 0x13, 0x24, 0xdd, 0x07, 0x55, 0xbf, 0xb7, 0xaf, 0xf2, 0xb8, 0xa2, - 0x07, 0x2e, 0xa8, 0x30, 0xdd, 0x71, 0xd7, 0xb5, 0xba, 0xfa, 0xdb, 0x1d, 0x78, 0x9e, 0x1b, 0xed, 0x45, 0xef, 0x6f, - 0xe2, 0x50, 0xc4, 0x95, 0xec, 0x0b, 0x88, 0x44, 0x26, 0x7e, 0xb3, 0x99, 0x24, 0xf6, 0xe5, 0x02, 0x66, 0x2a, 0x99, - 0x66, 0xb1, 0xb6, 0x74, 0x3b, 0xa7, 0x07, 0xaf, 0x86, 0xe5, 0x6c, 0x30, 0x3c, 0xcb, 0xc0, 0xe5, 0x97, 0xd7, 0xb8, - 0xe3, 0xa6, 0xad, 0x7f, 0xe5, 0x96, 0xf0, 0x25, 0xb2, 0xd4, 0xb5, 0xe4, 0xcf, 0x28, 0xb5, 0x1f, 0x34, 0x95, 0x7f, - 0x12, 0xf4, 0xb4, 0xf4, 0x3e, 0x2f, 0x9d, 0xca, 0xaf, 0xdf, 0xb7, 0x3d, 0xad, 0x16, 0xaf, 0xfe, 0x3a, 0xb9, 0x5e, - 0xff, 0xaf, 0xed, 0x1a, 0x3d, 0xfa, 0x39, 0xbd, 0xb2, 0xbf, 0x13, 0x9b, 0x85, 0x61, 0x77, 0x3d, 0x5d, 0x5c, 0x8b, - 0xea, 0xcf, 0x0f, 0xfc, 0xb3, 0xd3, 0x4a, 0x98, 0x8f, 0xbf, 0xb7, 0x98, 0x4c, 0xad, 0x66, 0xb7, 0xa6, 0x7b, 0xf8, - 0xe9, 0xa7, 0x1e, 0x04, 0xda, 0x95, 0x9c, 0xbb, 0xa6, 0x61, 0x98, 0x45, 0x85, 0x1a, 0x4e, 0xd1, 0x83, 0x3e, 0xda, - 0x1e, 0x37, 0xdf, 0x64, 0xeb, 0xc0, 0xed, 0x65, 0x93, 0x27, 0xcd, 0xd6, 0xf5, 0xb5, 0xc5, 0xf5, 0x5c, 0xa6, 0xcb, - 0x29, 0xb8, 0x91, 0x09, 0x1f, 0xe2, 0xbf, 0x6c, 0xc1, 0x64, 0x15, 0x90, 0xfc, 0x81, 0x14, 0xd7, 0xb7, 0xab, 0xab, - 0x5f, 0x65, 0xb9, 0x12, 0xb3, 0x8e, 0xb4, 0x06, 0x5a, 0x07, 0x41, 0xa3, 0xe6, 0x71, 0x21, 0x66, 0xae, 0xe9, 0x68, - 0x57, 0x72, 0x82, 0x0b, 0x50, 0x18, 0x4d, 0xfd, 0xc7, 0x4f, 0xa1, 0x36, 0xf7, 0x1a, 0xe5, 0x08, 0xae, 0x89, 0x62, - 0xf7, 0x9a, 0xb0, 0xdd, 0x82, 0x71, 0xaa, 0x78, 0x87, 0xaa, 0xca, 0x8d, 0xf6, 0xc5, 0x4c, 0x03, 0xa2, 0xca, 0x72, - 0x12, 0xe0, 0x10, 0x6f, 0x16, 0x6e, 0x92, 0xc4, 0xff, 0x1e, 0xbb, 0x48, 0x99, 0x14, 0xfe, 0xb9, 0x22, 0x64, 0xb1, - 0xd0, 0x2f, 0xb7, 0x8e, 0xd8, 0xd4, 0x0d, 0xab, 0xeb, 0x0f, 0x92, 0x99, 0x90, 0x9a, 0x89, 0xb0, 0xa2, 0x66, 0xbe, - 0x05, 0xdc, 0x93, 0x82, 0x89, 0xe7, 0x22, 0x6f, 0x6d, 0xd7, 0xfc, 0x7d, 0xea, 0xb3, 0xc5, 0x47, 0x91, 0x05, 0xd0, - 0x0d, 0x01, 0x02, 0x1a, 0xd7, 0xa7, 0xc5, 0x7a, 0x6f, 0x3b, 0xef, 0x5f, 0xd2, 0x58, 0x74, 0x3b, 0xe3, 0xca, 0x79, - 0xab, 0x68, 0xec, 0xe9, 0x02, 0x28, 0x69, 0xf5, 0xe5, 0xc7, 0x55, 0x3f, 0xbc, 0xdf, 0xc4, 0xad, 0xde, 0xc7, 0xbb, - 0xfe, 0xb5, 0x50, 0x8e, 0x34, 0x7c, 0x3a, 0xa5, 0xe1, 0xe5, 0x4e, 0x35, 0x31, 0x67, 0x3c, 0x2d, 0xaf, 0x5c, 0xd8, - 0x0d, 0xd9, 0xc6, 0xef, 0x7c, 0x68, 0xeb, 0x81, 0x5f, 0x76, 0x98, 0x22, 0x34, 0x27, 0x9b, 0x29, 0x2f, 0x90, 0x98, - 0xeb, 0x9b, 0xad, 0x38, 0xd8, 0x7b, 0x84, 0x36, 0xe1, 0xaf, 0x97, 0x24, 0x2b, 0xc4, 0xa8, 0xc2, 0x41, 0x5e, 0x1e, - 0xf6, 0x29, 0x4c, 0x82, 0xf5, 0x25, 0x3c, 0x04, 0x8a, 0x3e, 0x62, 0x14, 0xcb, 0xf1, 0x16, 0xea, 0x58, 0x8e, 0x83, - 0xde, 0x2c, 0x1f, 0xb5, 0xb1, 0x8b, 0x9f, 0x31, 0xb0, 0x62, 0xab, 0x90, 0xd3, 0x88, 0x35, 0x5b, 0xf8, 0xde, 0xdc, - 0xfa, 0xa1, 0xde, 0x73, 0x37, 0x24, 0x1c, 0x6e, 0x2b, 0xcf, 0x2f, 0xaa, 0x2d, 0x2b, 0xb5, 0x01, 0xde, 0xd2, 0x9d, - 0x79, 0x50, 0x49, 0x9b, 0xf7, 0x44, 0x76, 0x6f, 0x54, 0xf5, 0xd7, 0x4e, 0x16, 0xe2, 0x97, 0xdf, 0x45, 0xdb, 0x59, - 0x0a, 0x22, 0x5c, 0x84, 0x21, 0xd4, 0x56, 0xa5, 0x47, 0x51, 0x9e, 0xba, 0xfd, 0x14, 0x37, 0xa5, 0xdd, 0x06, 0x19, - 0xe9, 0xfe, 0x0d, 0x90, 0x82, 0x5e, 0x80, 0x40, 0x88, 0x7b, 0xd9, 0x1c, 0x0a, 0x98, 0x64, 0x90, 0x40, 0x2f, 0x13, - 0xac, 0x03, 0xfe, 0xa0, 0xe4, 0xbd, 0xed, 0xc0, 0x03, 0xe0, 0x80, 0xc2, 0x61, 0xa4, 0x7c, 0x65, 0x4e, 0x75, 0x46, - 0x66, 0xcb, 0xaa, 0x75, 0x13, 0x8f, 0xa6, 0xeb, 0x0a, 0x9c, 0x37, 0x15, 0x9b, 0x7f, 0x26, 0x0a, 0xdb, 0x4e, 0xc4, - 0xa9, 0x2c, 0x14, 0x98, 0xa8, 0x31, 0x48, 0x4f, 0x0d, 0x96, 0xad, 0x4a, 0xd3, 0xf8, 0x10, 0xcb, 0x6e, 0x1c, 0xa5, - 0xeb, 0x7a, 0x63, 0xe3, 0xcc, 0x41, 0x98, 0xbd, 0xb1, 0xd7, 0xee, 0xf9, 0x96, 0xb5, 0x37, 0xad, 0x0e, 0xc5, 0x63, - 0x65, 0x71, 0x53, 0xf9, 0xf2, 0xcd, 0xb9, 0x31, 0xe5, 0x59, 0xe6, 0x1c, 0xc2, 0x5a, 0xf0, 0xdf, 0xb0, 0x1b, 0x36, - 0x6d, 0x2c, 0xf9, 0x72, 0x5f, 0x10, 0xc1, 0xee, 0x92, 0xbd, 0xc6, 0xec, 0xb2, 0xa4, 0x0a, 0x43, 0xf0, 0xce, 0x67, - 0x2a, 0x11, 0xcb, 0x1f, 0x51, 0x94, 0x6f, 0x3c, 0xb6, 0xfb, 0xc2, 0x2a, 0xc1, 0xb0, 0x5e, 0x13, 0x78, 0xe1, 0x79, - 0x4b, 0x49, 0x8b, 0x49, 0x79, 0x59, 0x66, 0xf7, 0xcb, 0xee, 0xca, 0x0d, 0xc2, 0xc3, 0xbd, 0xa4, 0x15, 0x62, 0x18, - 0x35, 0x95, 0x2a, 0x70, 0x31, 0x22, 0xea, 0x98, 0xa7, 0xdb, 0xb2, 0xc4, 0x2f, 0x4d, 0x25, 0x49, 0x2d, 0x74, 0x6b, - 0x07, 0x87, 0xdf, 0x59, 0xd2, 0x44, 0x88, 0x2d, 0x28, 0x5e, 0x77, 0x48, 0x60, 0xe7, 0x9c, 0x52, 0xbe, 0x9f, 0x3f, - 0xca, 0x2c, 0x19, 0x5e, 0x95, 0x03, 0x55, 0xc8, 0xce, 0x0c, 0x19, 0xad, 0xb8, 0xeb, 0x44, 0x12, 0xf4, 0x2e, 0x12, - 0x83, 0x15, 0xc2, 0x7e, 0x28, 0xca, 0x12, 0xfd, 0x80, 0x57, 0x93, 0x1b, 0x78, 0x11, 0x58, 0x75, 0xe5, 0xb1, 0x53, - 0x12, 0xe4, 0x00, 0xf9, 0x49, 0x70, 0xd6, 0xc3, 0x4c, 0xee, 0x8e, 0x40, 0x04, 0x8f, 0x77, 0x32, 0x11, 0xd0, 0x06, - 0xf2, 0x55, 0x5d, 0xdc, 0x89, 0x44, 0x15, 0x82, 0xbf, 0x24, 0x77, 0xf4, 0x6e, 0xab, 0x34, 0x1a, 0xed, 0x6c, 0x28, - 0x4c, 0xdc, 0xc6, 0x75, 0xe3, 0x1d, 0x2f, 0x98, 0xa3, 0xcd, 0xe8, 0xae, 0x58, 0x9e, 0xe7, 0xcd, 0x4d, 0x69, 0x04, - 0x4b, 0xea, 0x2b, 0xcc, 0x4b, 0xb3, 0x56, 0x74, 0x64, 0x6c, 0x11, 0x24, 0x06, 0xcc, 0x69, 0x67, 0xc9, 0x62, 0xf5, - 0xdb, 0x58, 0x8b, 0x1c, 0x47, 0x58, 0x96, 0x04, 0x10, 0xcd, 0x0b, 0x91, 0x17, 0x43, 0x8e, 0xa3, 0x25, 0xaa, 0x14, - 0x18, 0x73, 0x8b, 0x18, 0x76, 0xb1, 0xc9, 0xc9, 0x6d, 0x69, 0xa4, 0x68, 0xf8, 0x26, 0xa7, 0xa7, 0xf7, 0xd6, 0xf0, - 0xda, 0x88, 0x64, 0xe4, 0x60, 0x7b, 0x2e, 0xb7, 0x25, 0xac, 0xf2, 0x53, 0xbf, 0x5b, 0x6b, 0x15, 0x2d, 0x97, 0xe3, - 0x60, 0xb4, 0xd7, 0x92, 0x45, 0xbb, 0x01, 0x35, 0x7f, 0xc9, 0xba, 0x87, 0x8c, 0x35, 0xda, 0xb0, 0xd5, 0x41, 0x5d, - 0x88, 0x73, 0x3e, 0xf6, 0x15, 0xfe, 0x46, 0x9e, 0xc0, 0x4c, 0x53, 0x72, 0x91, 0xff, 0xbe, 0xbf, 0x54, 0x25, 0xf9, - 0xeb, 0x60, 0x27, 0x3c, 0x16, 0x6a, 0x64, 0xbf, 0x57, 0x13, 0x33, 0x99, 0x61, 0x87, 0xf7, 0x09, 0xb8, 0x42, 0x7c, - 0x31, 0xc0, 0xce, 0x2c, 0x9d, 0x4b, 0xda, 0xa9, 0x8c, 0x19, 0x97, 0xe2, 0x38, 0x93, 0x26, 0x1b, 0x5b, 0x53, 0x7f, - 0x7b, 0x89, 0x55, 0x4b, 0x9e, 0x5a, 0x9f, 0x5b, 0x5f, 0xe3, 0xe3, 0x1c, 0xf2, 0xd6, 0x87, 0x19, 0xff, 0x64, 0x2b, - 0x4c, 0xd8, 0x3b, 0xa2, 0x45, 0xb0, 0x63, 0xa3, 0x81, 0x9f, 0xde, 0x8b, 0xa3, 0x65, 0x09, 0xda, 0x3f, 0x80, 0x55, - 0x5d, 0x85, 0x9c, 0xc9, 0xf0, 0xf3, 0xa4, 0x91, 0x58, 0x24, 0xc5, 0x02, 0x38, 0xdf, 0xab, 0xd9, 0xef, 0xd5, 0xeb, - 0x93, 0xd5, 0x64, 0xc8, 0xa8, 0xb3, 0xa4, 0x2e, 0xd4, 0x9f, 0x5d, 0xdf, 0x36, 0x75, 0x1d, 0x9c, 0x88, 0xeb, 0x4f, - 0xd0, 0xb6, 0x75, 0xc7, 0xd0, 0x9c, 0xa0, 0xa6, 0x3c, 0x53, 0x1c, 0xeb, 0x4e, 0xbf, 0x19, 0x8f, 0x6c, 0x6e, 0x3e, - 0xda, 0x44, 0x36, 0x5e, 0x8c, 0xc4, 0x16, 0x5e, 0xe8, 0x05, 0xd0, 0xa2, 0xbe, 0xc7, 0x42, 0xfc, 0xe4, 0xe8, 0x19, - 0x4e, 0x60, 0x04, 0x72, 0x2d, 0x64, 0xa0, 0xa4, 0x27, 0x1a, 0xb6, 0x55, 0x73, 0x0e, 0x07, 0x2f, 0x3f, 0xb1, 0x2d, - 0x79, 0x44, 0x07, 0x75, 0x86, 0xfe, 0x4a, 0x3e, 0xdb, 0x4f, 0x15, 0xef, 0x30, 0xd5, 0xf6, 0xeb, 0x72, 0x9c, 0x35, - 0xd3, 0x79, 0xd7, 0x90, 0x85, 0xe8, 0xf9, 0x60, 0x7b, 0x86, 0xd4, 0xb1, 0x8a, 0x56, 0xcd, 0xe2, 0xf5, 0xf0, 0xee, - 0x91, 0x25, 0x66, 0xeb, 0x76, 0x67, 0x74, 0xd8, 0x41, 0x50, 0x43, 0x0c, 0xd6, 0x6d, 0x21, 0x91, 0x19, 0x25, 0xc7, - 0xba, 0x10, 0x1f, 0xc1, 0x0d, 0x41, 0x29, 0xf4, 0xb0, 0x97, 0xd2, 0x4a, 0x3f, 0xea, 0x22, 0x19, 0x26, 0x83, 0x47, - 0xb3, 0x5b, 0x36, 0xd7, 0x62, 0x17, 0x55, 0xfd, 0xb3, 0xec, 0xda, 0x15, 0xf4, 0xd1, 0x14, 0x75, 0x42, 0x0d, 0x22, - 0x90, 0xde, 0xca, 0xdf, 0xe2, 0xf8, 0x92, 0x6e, 0x5c, 0xbf, 0x1e, 0xde, 0x84, 0xcc, 0xf9, 0x00, 0x0e, 0x00, 0xd3, - 0xc9, 0xfb, 0x77, 0x0e, 0x25, 0x55, 0x85, 0x46, 0x5a, 0xdf, 0x91, 0x1b, 0x6c, 0xc7, 0xe4, 0x21, 0x3a, 0xbe, 0x76, - 0x33, 0x40, 0x80, 0x36, 0x16, 0xfa, 0x1c, 0x5a, 0x6f, 0x24, 0xad, 0x04, 0x4b, 0xa0, 0xb3, 0xfa, 0xa1, 0xa5, 0xf0, - 0xd2, 0x90, 0x98, 0xfa, 0x0d, 0x96, 0x45, 0xa2, 0x24, 0xe6, 0xcf, 0xc2, 0x2b, 0xdb, 0xaa, 0xf0, 0x30, 0xc6, 0x0a, - 0xd0, 0x86, 0x58, 0xfb, 0x15, 0xbb, 0x22, 0xb0, 0xf0, 0xde, 0x12, 0xc0, 0xbb, 0x66, 0x8e, 0x02, 0x01, 0xc7, 0x2b, - 0x1c, 0x44, 0x1a, 0x8c, 0x3f, 0x5b, 0x89, 0x23, 0x47, 0x9a, 0xd5, 0x47, 0xef, 0xdb, 0xfd, 0x69, 0x8a, 0x47, 0xea, - 0x1c, 0xb7, 0x9e, 0x64, 0x81, 0x3f, 0xec, 0x9e, 0x0b, 0x2f, 0xac, 0xc8, 0x5e, 0xf6, 0x77, 0xf7, 0x92, 0xc5, 0x4d, - 0x2f, 0xf1, 0x55, 0x2f, 0x93, 0xeb, 0x95, 0x43, 0x0d, 0x6a, 0x60, 0xf3, 0x7d, 0x8f, 0xaa, 0x82, 0xdc, 0x80, 0xbf, - 0x77, 0xf3, 0x11, 0xc6, 0xb5, 0x0b, 0x9c, 0xe3, 0x52, 0x3d, 0xcc, 0x45, 0xcc, 0xa8, 0xa4, 0x76, 0x94, 0x5c, 0x40, - 0xaa, 0x93, 0x94, 0x0b, 0x32, 0xac, 0x5c, 0xa0, 0xb7, 0xfa, 0x14, 0xaf, 0x9c, 0x2d, 0x4d, 0x82, 0x28, 0xea, 0xb9, - 0x7b, 0x85, 0x0a, 0xc1, 0x7f, 0x1d, 0xc8, 0x8c, 0x29, 0x2b, 0x30, 0x57, 0x13, 0xea, 0x30, 0x4b, 0x26, 0xbc, 0x3c, - 0x8a, 0xe1, 0xc5, 0x08, 0x32, 0x6d, 0xa9, 0x22, 0xaa, 0xdf, 0x43, 0xdf, 0xa4, 0x68, 0x56, 0xa3, 0x10, 0x73, 0x8e, - 0x25, 0x15, 0x5c, 0xaf, 0xea, 0x10, 0x7e, 0x44, 0xad, 0xee, 0x15, 0x8e, 0xeb, 0x9c, 0x31, 0xb9, 0xf3, 0xad, 0x07, - 0x9c, 0x9e, 0xc7, 0xe9, 0x01, 0xb9, 0x6d, 0x2a, 0xa2, 0xfa, 0x18, 0x0f, 0x43, 0x58, 0xab, 0x59, 0x66, 0x08, 0x99, - 0xb7, 0x0d, 0xc5, 0xaf, 0x84, 0x66, 0xf3, 0x67, 0x57, 0x43, 0xf2, 0x79, 0xf1, 0xc4, 0x8d, 0xeb, 0x54, 0xc5, 0x12, - 0x93, 0xbc, 0x08, 0x2b, 0xcc, 0xe1, 0xb2, 0x37, 0x63, 0xd1, 0xf9, 0xc9, 0x79, 0x08, 0x09, 0x58, 0x94, 0x8f, 0x68, - 0xed, 0x19, 0x50, 0x50, 0x2d, 0xc7, 0xa9, 0x5a, 0x03, 0xc6, 0xf6, 0x3c, 0x78, 0x4b, 0xf9, 0x2f, 0xdb, 0x03, 0xd4, - 0x93, 0xed, 0x9c, 0x62, 0x30, 0x86, 0xf0, 0x94, 0x6e, 0x03, 0x9c, 0x90, 0xca, 0x16, 0x0e, 0xaa, 0xf5, 0x37, 0x37, - 0x2f, 0xb4, 0x81, 0x12, 0x7f, 0x49, 0x91, 0xd6, 0x8a, 0x14, 0x8f, 0x30, 0x34, 0x05, 0x9f, 0xf1, 0xd8, 0x93, 0x78, - 0xcf, 0xdd, 0x97, 0x35, 0x22, 0x05, 0x71, 0x2e, 0xfc, 0x85, 0x08, 0x2b, 0x47, 0x88, 0x79, 0xca, 0x0d, 0x95, 0x64, - 0x5f, 0xda, 0xb3, 0xfa, 0x2a, 0x0b, 0x78, 0xea, 0xb2, 0x4f, 0xe1, 0x85, 0xc0, 0x1c, 0xae, 0x3d, 0x7a, 0x52, 0x3f, - 0x9a, 0x01, 0xab, 0x7a, 0x6e, 0x31, 0x8d, 0x50, 0x51, 0xb8, 0x84, 0x20, 0x6e, 0x29, 0x41, 0x84, 0x61, 0x66, 0x8d, - 0x87, 0x4f, 0x0c, 0xeb, 0x29, 0x0c, 0x00, 0xa6, 0xa4, 0xa1, 0x7b, 0x86, 0x32, 0x81, 0x7b, 0x69, 0x17, 0x28, 0xcf, - 0x5c, 0x65, 0x5e, 0x4e, 0xee, 0x31, 0x0c, 0x9c, 0xcc, 0xd6, 0x16, 0xd3, 0xc8, 0x88, 0xac, 0x03, 0x2d, 0xd4, 0x91, - 0xbf, 0x57, 0x2b, 0x2b, 0xbb, 0x6e, 0x02, 0xdd, 0x8b, 0x7a, 0x2f, 0xfd, 0x22, 0x42, 0xdc, 0x5e, 0xa5, 0xa7, 0x80, - 0x69, 0xf8, 0x14, 0xf7, 0x33, 0xa9, 0x10, 0x78, 0x15, 0xf4, 0x3c, 0x90, 0xc8, 0x1e, 0xe2, 0x0e, 0xea, 0x06, 0x00, - 0x92, 0x5b, 0xe2, 0x4a, 0x41, 0x5a, 0x1b, 0x52, 0x25, 0xeb, 0x3f, 0xb5, 0xd5, 0x28, 0x01, 0xc8, 0x47, 0xf6, 0xcd, - 0x91, 0xc6, 0x2b, 0x08, 0x2d, 0x5c, 0x48, 0x39, 0xcc, 0xe0, 0x51, 0x13, 0x74, 0x22, 0x33, 0xc1, 0xe6, 0x56, 0xb0, - 0x8d, 0x9f, 0xbc, 0x79, 0x1d, 0x89, 0x0a, 0xd3, 0x8f, 0xe2, 0xdf, 0xab, 0x3b, 0x6e, 0x8a, 0xb2, 0xe2, 0x21, 0xf7, - 0xfd, 0x1c, 0x3f, 0x37, 0x45, 0x49, 0x9a, 0x13, 0xbe, 0x54, 0x32, 0x9d, 0xb4, 0x17, 0x47, 0x6d, 0x8e, 0x96, 0x3d, - 0x00, 0x79, 0xc5, 0x4b, 0x60, 0xdd, 0x63, 0xe5, 0xbb, 0x96, 0x56, 0x99, 0xf9, 0x88, 0xaa, 0x2a, 0x2a, 0x24, 0x9a, - 0xe0, 0x22, 0xb3, 0x26, 0xb6, 0x71, 0x5a, 0x1c, 0x06, 0x90, 0x42, 0x63, 0x58, 0x31, 0xe4, 0xa1, 0xf9, 0xb1, 0x98, - 0x17, 0xe1, 0x89, 0x2c, 0xba, 0x2e, 0x28, 0xe5, 0x67, 0x48, 0x69, 0xa6, 0x8c, 0x6c, 0x8b, 0x65, 0x38, 0x9c, 0x01, - 0xc0, 0x5c, 0xeb, 0x88, 0xa8, 0x54, 0x98, 0xec, 0x98, 0x54, 0xc6, 0x5e, 0xc9, 0x9f, 0xa5, 0x84, 0x88, 0x08, 0xab, - 0xb3, 0x6d, 0x03, 0x62, 0x77, 0x0e, 0xea, 0x11, 0x9e, 0x2d, 0x33, 0xd8, 0xc4, 0xb2, 0x08, 0xce, 0x8a, 0xaa, 0xaa, - 0x46, 0xa3, 0xf9, 0xb0, 0x37, 0x34, 0xde, 0x64, 0xee, 0x0c, 0x85, 0xb0, 0x75, 0x3b, 0x42, 0x50, 0x6e, 0x3c, 0xdb, - 0x86, 0xd6, 0x86, 0x0f, 0x33, 0x37, 0xf5, 0x41, 0x5e, 0x2e, 0xa2, 0xaa, 0xd1, 0x66, 0xcf, 0x53, 0xca, 0xd3, 0x68, - 0xef, 0xa9, 0x3b, 0x29, 0x09, 0x95, 0xdd, 0x24, 0x2a, 0x0a, 0x6c, 0xe1, 0x59, 0x12, 0x12, 0x40, 0x71, 0xfb, 0xeb, - 0x50, 0x83, 0xfc, 0x8b, 0x91, 0x83, 0x03, 0x5f, 0xd5, 0x81, 0xb5, 0x2b, 0x20, 0xcd, 0xcf, 0x64, 0xd2, 0x5b, 0x7c, - 0xef, 0x6e, 0xe8, 0xc3, 0x47, 0x91, 0x22, 0xdc, 0x47, 0x68, 0x1d, 0x27, 0x21, 0x6e, 0x01, 0xf7, 0x9b, 0x6d, 0x2f, - 0x3e, 0x6a, 0x3a, 0x41, 0x66, 0x68, 0x5c, 0x1b, 0x2f, 0x9b, 0x56, 0xa4, 0x16, 0x28, 0x54, 0x20, 0x1f, 0xe9, 0x7e, - 0x2b, 0x01, 0x29, 0xc7, 0xa9, 0xdc, 0x76, 0xaf, 0x93, 0xf2, 0x1c, 0xee, 0x86, 0x8a, 0xe8, 0x75, 0x3d, 0xda, 0x12, - 0xd0, 0x62, 0x4e, 0x06, 0x7e, 0x3a, 0x5a, 0x45, 0xbe, 0x9c, 0x38, 0xc5, 0x39, 0x55, 0x2d, 0xd6, 0x3c, 0xf8, 0x78, - 0x97, 0x07, 0xc2, 0xfe, 0x92, 0x8c, 0x93, 0x31, 0x00, 0x59, 0xc5, 0x02, 0x73, 0xe0, 0x40, 0xed, 0x36, 0x27, 0x6a, - 0x03, 0x0e, 0xd4, 0xea, 0xce, 0x9e, 0xa6, 0x32, 0xd9, 0xa7, 0x94, 0xe3, 0x57, 0xf8, 0x82, 0xa5, 0xac, 0x46, 0xda, - 0x8a, 0x6a, 0x79, 0xae, 0xa5, 0x05, 0x50, 0x4b, 0x65, 0xa9, 0x6c, 0x29, 0xa4, 0x18, 0xdf, 0xec, 0xf1, 0x31, 0x12, - 0xba, 0x52, 0x3c, 0x4f, 0x2e, 0x03, 0xed, 0xf2, 0x37, 0x9e, 0xc2, 0x74, 0xc6, 0x04, 0xa6, 0xa9, 0x89, 0x2b, 0xb2, - 0x79, 0xff, 0xfe, 0xda, 0xa9, 0xb6, 0xec, 0x48, 0xaa, 0x40, 0xda, 0x2c, 0x76, 0x74, 0x69, 0x4c, 0x81, 0xe0, 0xaa, - 0xa1, 0xdb, 0xe2, 0x68, 0x77, 0xfe, 0xe1, 0xce, 0x2c, 0xa4, 0x6b, 0xa2, 0x1c, 0x49, 0xe4, 0xc7, 0xa5, 0x10, 0x50, - 0xa3, 0xbe, 0x10, 0x4e, 0xa4, 0xfe, 0xc8, 0x90, 0x4b, 0x17, 0xb3, 0x43, 0x6b, 0x54, 0xd7, 0x2d, 0x00, 0x2d, 0x7d, - 0x0f, 0x23, 0x43, 0x21, 0x6c, 0xc4, 0xb0, 0xcf, 0x53, 0xea, 0xe3, 0xca, 0x49, 0x17, 0x5d, 0x62, 0x29, 0x64, 0xde, - 0xc7, 0x24, 0x6f, 0x92, 0x46, 0x89, 0xc8, 0xeb, 0x2c, 0xe5, 0xa4, 0x2e, 0x4e, 0xe2, 0x28, 0x6f, 0x29, 0x64, 0x20, - 0xd5, 0x4d, 0x2a, 0xdd, 0x96, 0x4b, 0x25, 0xf4, 0x58, 0xf6, 0xb7, 0xe4, 0x06, 0xaf, 0xfb, 0x72, 0x1c, 0xfc, 0x31, - 0xf2, 0xcf, 0x13, 0x5b, 0x2c, 0x45, 0x07, 0xd0, 0x83, 0x20, 0xa5, 0x35, 0x40, 0xc2, 0xcf, 0xeb, 0x5b, 0xdf, 0x09, - 0xbe, 0x76, 0x04, 0xb4, 0x42, 0xb0, 0x72, 0xbd, 0x0a, 0x35, 0xdd, 0x5e, 0x36, 0x56, 0x65, 0x54, 0x75, 0xb0, 0x83, - 0x68, 0x89, 0x24, 0x04, 0xf8, 0x9c, 0xbc, 0x43, 0xea, 0x87, 0x9a, 0x74, 0xeb, 0x4b, 0xbe, 0x88, 0xd6, 0xb5, 0x92, - 0x67, 0x04, 0x57, 0xdf, 0xa8, 0xc9, 0xc2, 0xad, 0xe3, 0x27, 0x51, 0xd7, 0x4e, 0xd5, 0x15, 0x31, 0x07, 0x04, 0x98, - 0xaa, 0x86, 0x11, 0x75, 0x9f, 0x24, 0xc9, 0xbf, 0xc4, 0x54, 0x80, 0x0a, 0x96, 0x49, 0xbd, 0xfa, 0xbf, 0x6f, 0xb5, - 0xee, 0x7f, 0xbc, 0xc1, 0xba, 0x9a, 0xe7, 0xb7, 0x77, 0x7a, 0x00, 0x30, 0x80, 0x1f, 0x83, 0xaa, 0x0e, 0x5e, 0x6e, - 0xc7, 0x0b, 0xbb, 0x32, 0x05, 0xa9, 0x09, 0xf8, 0xac, 0x92, 0xfe, 0xcf, 0x91, 0x06, 0x82, 0xe6, 0x6b, 0x64, 0x6d, - 0x6c, 0x46, 0x24, 0x72, 0xdf, 0x65, 0x83, 0x8f, 0x57, 0xe1, 0xd9, 0x11, 0xf8, 0x65, 0x6c, 0x9d, 0xd3, 0x31, 0xcb, - 0x07, 0x09, 0x2c, 0x17, 0x6a, 0xbf, 0x7a, 0xcc, 0xf9, 0x44, 0x88, 0x53, 0x54, 0xa8, 0x27, 0xa8, 0x08, 0x32, 0x81, - 0x62, 0x91, 0x96, 0xa8, 0xe3, 0x2a, 0xce, 0x11, 0x16, 0x10, 0x5a, 0xa7, 0x44, 0x44, 0xbc, 0x1d, 0xd0, 0x11, 0xbc, - 0xad, 0x21, 0x27, 0xee, 0x38, 0x37, 0x6b, 0x1b, 0x98, 0xcb, 0x20, 0xd5, 0xa0, 0xe9, 0xee, 0x0b, 0x6c, 0xc0, 0x43, - 0x9c, 0x37, 0x8e, 0x4f, 0xe2, 0x72, 0x8b, 0x2c, 0x72, 0x0e, 0x45, 0x5d, 0x5e, 0xd4, 0xc8, 0xc4, 0x24, 0xa1, 0x0e, - 0x4f, 0x21, 0xa4, 0xdb, 0x17, 0x30, 0x98, 0x16, 0x4c, 0xe3, 0xac, 0x4e, 0x12, 0xc0, 0x2d, 0x9f, 0xde, 0x0f, 0xc3, - 0x97, 0x1e, 0x6a, 0x07, 0xd1, 0xb9, 0x88, 0xf8, 0x4d, 0xdb, 0xd4, 0x28, 0x4c, 0x1e, 0xae, 0xad, 0xef, 0xa9, 0xe1, - 0x23, 0x24, 0xe1, 0x5f, 0xc3, 0xa2, 0x08, 0x48, 0xdc, 0xa6, 0xb7, 0x5c, 0x30, 0xe9, 0x9d, 0x66, 0x21, 0xb4, 0xd9, - 0x0c, 0x52, 0xa5, 0x6a, 0x3e, 0xc0, 0xca, 0xb4, 0xd3, 0xff, 0xe4, 0xe4, 0xb6, 0x24, 0x05, 0x41, 0xb4, 0xd2, 0xef, - 0x4c, 0x99, 0xb0, 0xc6, 0x98, 0x40, 0xde, 0x15, 0x25, 0x9c, 0x67, 0xd0, 0x49, 0x2c, 0x00, 0x3b, 0x5a, 0x7f, 0x2f, - 0xff, 0x0e, 0x8b, 0xd1, 0xa9, 0xd1, 0x9b, 0x4e, 0x92, 0xa9, 0xd6, 0x7f, 0x7b, 0x00, 0x7f, 0x9c, 0x81, 0xb5, 0x3e, - 0x77, 0x81, 0xb5, 0xdb, 0x4d, 0x12, 0x52, 0xba, 0x25, 0xaf, 0xaa, 0xaf, 0x62, 0xdd, 0xa4, 0x54, 0xee, 0x67, 0xbf, - 0xbf, 0xbd, 0xd8, 0x32, 0x82, 0xc3, 0x3a, 0xa7, 0x18, 0x58, 0x80, 0x0d, 0x73, 0x19, 0x6e, 0x56, 0x3b, 0x81, 0xa0, - 0xa4, 0x97, 0xe4, 0xc3, 0x36, 0x43, 0xb2, 0xe3, 0x53, 0x2d, 0x91, 0xd0, 0x33, 0x9e, 0xf6, 0x35, 0x80, 0xc0, 0xbb, - 0x73, 0x93, 0xd2, 0x7a, 0xf3, 0x19, 0x79, 0xaf, 0x4a, 0x14, 0x91, 0x61, 0x4a, 0x8c, 0x67, 0x4e, 0x08, 0xd2, 0x7e, - 0xcd, 0x60, 0x6b, 0xbf, 0x19, 0x3c, 0x8d, 0x9b, 0x87, 0xe6, 0x88, 0x20, 0x72, 0x31, 0x7d, 0x90, 0xc2, 0x9f, 0x25, - 0xe3, 0xf2, 0x85, 0xcf, 0x6c, 0xc3, 0x75, 0x5a, 0x49, 0x80, 0x73, 0x8a, 0xbb, 0x79, 0xca, 0x33, 0xb1, 0x58, 0x9e, - 0xbc, 0x7c, 0xb5, 0xac, 0xd2, 0x14, 0x38, 0x83, 0x0f, 0x71, 0x19, 0xa9, 0x34, 0xc8, 0x94, 0x14, 0x3f, 0x9e, 0x27, - 0x93, 0x6e, 0x6f, 0x6a, 0x95, 0xb0, 0xab, 0x06, 0x87, 0xea, 0x13, 0x2a, 0x4b, 0x4f, 0xdb, 0x52, 0x0b, 0xcc, 0x63, - 0x1f, 0x04, 0x98, 0x7c, 0x93, 0x7d, 0xcc, 0xda, 0x11, 0x74, 0x0f, 0x4a, 0xe5, 0x32, 0x1e, 0x06, 0xb8, 0xa9, 0x82, - 0x00, 0xd2, 0xc7, 0x7a, 0x0a, 0x73, 0x79, 0x8f, 0x89, 0x0e, 0x8b, 0x1e, 0x46, 0xa0, 0x2e, 0xd4, 0xc2, 0x08, 0xcc, - 0x90, 0x07, 0x53, 0xb1, 0x74, 0xe2, 0x4f, 0x37, 0xd8, 0xfa, 0xdc, 0x8f, 0x73, 0x4d, 0xbb, 0x63, 0x96, 0x06, 0x48, - 0xaa, 0xa3, 0xee, 0x37, 0xfa, 0x46, 0x3d, 0x9e, 0x75, 0x8b, 0xf7, 0x69, 0x33, 0xa6, 0xbd, 0xa9, 0x3c, 0x1e, 0x55, - 0xdb, 0xef, 0xdf, 0x16, 0x97, 0xa8, 0x96, 0x92, 0xa5, 0x3d, 0xc8, 0xbe, 0x3b, 0xa3, 0x00, 0xb7, 0xef, 0x78, 0x13, - 0x1e, 0x20, 0xcf, 0x91, 0x4e, 0xcc, 0x6d, 0xd8, 0x53, 0x9d, 0x03, 0xed, 0xfc, 0xe7, 0x47, 0xa9, 0xb0, 0xf3, 0xf7, - 0xa7, 0x61, 0xe9, 0x3d, 0x49, 0x18, 0x05, 0x8e, 0xd2, 0xef, 0xde, 0x77, 0x59, 0xad, 0xf4, 0x71, 0x81, 0xcb, 0x9d, - 0xd7, 0x56, 0x9c, 0x78, 0xb1, 0x61, 0x3d, 0x25, 0x8f, 0x63, 0x14, 0x63, 0x6f, 0x7a, 0xb2, 0xee, 0x8c, 0xdd, 0x3d, - 0x85, 0xb5, 0x67, 0x3d, 0x25, 0x48, 0xb7, 0x92, 0x1f, 0xf7, 0xfe, 0x23, 0xb6, 0x53, 0x25, 0xdd, 0xf4, 0x07, 0xdb, - 0x2f, 0x3f, 0x3a, 0x3b, 0x88, 0x07, 0xad, 0xb3, 0x32, 0x9d, 0xa5, 0xeb, 0x2a, 0xe9, 0x72, 0xb8, 0x40, 0xde, 0xcd, - 0xc4, 0x73, 0x61, 0xae, 0xbf, 0xce, 0x36, 0x42, 0x05, 0xf6, 0xe5, 0x6a, 0xbc, 0xbd, 0x47, 0xcc, 0xe7, 0x72, 0x2e, - 0xfb, 0xde, 0xa2, 0x29, 0x04, 0x7d, 0x8b, 0x91, 0x72, 0xc1, 0x38, 0x4e, 0x90, 0x0f, 0xb8, 0x34, 0xde, 0x07, 0xb4, - 0x18, 0xa3, 0x5b, 0xf8, 0x79, 0x0c, 0xdb, 0x03, 0x6c, 0xcd, 0xfc, 0x73, 0xc2, 0x63, 0x5e, 0x88, 0x30, 0x4d, 0x1e, - 0x50, 0x53, 0xb2, 0x81, 0x0f, 0x36, 0x9c, 0x9f, 0x15, 0x12, 0xc3, 0x00, 0xcb, 0x23, 0x4f, 0xa7, 0x8d, 0xec, 0x69, - 0xa8, 0x2e, 0xcf, 0x73, 0xb5, 0xde, 0x82, 0x9e, 0x30, 0x9d, 0xe5, 0x65, 0x1a, 0xee, 0xd2, 0x3b, 0x13, 0xec, 0x70, - 0xb9, 0x6b, 0x38, 0x69, 0x99, 0x22, 0x55, 0x8e, 0xf3, 0xc6, 0x71, 0x9a, 0x33, 0x06, 0xe2, 0x29, 0xe6, 0xf5, 0xeb, - 0x54, 0x60, 0xd1, 0x62, 0x5c, 0xbe, 0x40, 0x69, 0x60, 0xea, 0x2f, 0x36, 0x32, 0x53, 0xa1, 0x75, 0x00, 0x31, 0x59, - 0x82, 0x3f, 0x1b, 0xa4, 0xb4, 0xa0, 0x10, 0x8d, 0x0a, 0xb7, 0xd5, 0x3f, 0xdc, 0x15, 0xb5, 0x4a, 0x13, 0xd1, 0xee, - 0x5d, 0xf1, 0xce, 0x10, 0xdb, 0xc5, 0x5b, 0x4e, 0x07, 0x50, 0x8c, 0x1a, 0x1d, 0xd0, 0xa4, 0x60, 0x7b, 0xb4, 0xfe, - 0x26, 0x29, 0xe5, 0x79, 0x66, 0x44, 0xf6, 0x58, 0xc0, 0xfa, 0x6e, 0x70, 0x18, 0x5b, 0x5b, 0x55, 0x58, 0xef, 0x9b, - 0x36, 0xc1, 0x1c, 0x80, 0xfd, 0x96, 0x44, 0xf1, 0xde, 0x27, 0x7f, 0x49, 0x8c, 0xd1, 0xf5, 0x3d, 0x17, 0x59, 0x7a, - 0x63, 0x28, 0x26, 0x48, 0xae, 0x69, 0x56, 0xc9, 0xe2, 0x18, 0x8d, 0x46, 0x41, 0xc9, 0x39, 0x31, 0x8e, 0x50, 0x36, - 0x40, 0x3d, 0x4d, 0x49, 0xe9, 0x02, 0x40, 0x66, 0x58, 0x4d, 0x0f, 0x0a, 0x60, 0x19, 0x8d, 0xb4, 0x40, 0xe5, 0x59, - 0x74, 0x14, 0xee, 0x79, 0x72, 0x9a, 0x6b, 0x66, 0xd5, 0xe0, 0xf0, 0x96, 0x07, 0x8a, 0xca, 0x79, 0x7a, 0x36, 0x99, - 0x8f, 0xe8, 0x20, 0xd2, 0x6b, 0x1a, 0xff, 0xb6, 0xaf, 0x44, 0x74, 0x70, 0x1b, 0x09, 0x16, 0x9c, 0xfd, 0x90, 0x93, - 0x21, 0x72, 0x7f, 0xb5, 0xce, 0xf4, 0x83, 0x0a, 0xfd, 0x6e, 0x35, 0x84, 0x48, 0xf9, 0x4a, 0xd8, 0xd8, 0x94, 0x3b, - 0x35, 0xe8, 0xbc, 0xd3, 0x30, 0x91, 0xa1, 0x70, 0x7c, 0xcf, 0x6d, 0xe9, 0x13, 0x6d, 0x56, 0x37, 0xce, 0xfc, 0xe3, - 0x01, 0x3f, 0x59, 0x9a, 0x22, 0x68, 0x4d, 0xa5, 0x4e, 0x07, 0xe8, 0x96, 0x48, 0x83, 0xa3, 0x7c, 0x60, 0x5e, 0x78, - 0xd0, 0x52, 0xbb, 0xa1, 0x44, 0x57, 0x74, 0x14, 0xee, 0x91, 0xe7, 0x02, 0x1a, 0x67, 0x0a, 0xba, 0x1e, 0x71, 0x54, - 0x3b, 0x63, 0x28, 0xc5, 0x1c, 0x0c, 0xe6, 0x50, 0xd6, 0x64, 0x8d, 0x7d, 0x6c, 0xbd, 0x44, 0x77, 0xe3, 0x19, 0x22, - 0xd7, 0x13, 0x1a, 0x87, 0xa4, 0xeb, 0x99, 0x91, 0xc2, 0x30, 0x84, 0x77, 0x40, 0xf2, 0xee, 0xc4, 0xfa, 0x5c, 0xa9, - 0xa4, 0x1d, 0x69, 0x63, 0x60, 0x17, 0xcf, 0x62, 0xb6, 0xb0, 0x22, 0x3b, 0x71, 0x6d, 0xbd, 0xb6, 0x76, 0xfd, 0x15, - 0xa2, 0xc2, 0x78, 0x6c, 0xeb, 0x70, 0x9e, 0x33, 0x72, 0xc5, 0x0d, 0x13, 0x3f, 0xcb, 0xae, 0xcf, 0x3c, 0x95, 0xf4, - 0x5f, 0x84, 0xd6, 0x4f, 0x5d, 0x71, 0x81, 0x75, 0xb7, 0x4d, 0xec, 0xfa, 0x75, 0x4a, 0x6a, 0x5d, 0xb9, 0x0b, 0xfd, - 0x8f, 0xad, 0xed, 0x58, 0x6d, 0xe6, 0x69, 0xde, 0x7f, 0xe8, 0x44, 0x1d, 0xe4, 0x9f, 0x7e, 0xb5, 0x1b, 0xb9, 0x91, - 0x16, 0x2f, 0xc9, 0xc7, 0xbb, 0x9e, 0xe6, 0x0b, 0x1e, 0xfb, 0xf3, 0x66, 0xd8, 0xf3, 0x32, 0xbf, 0x16, 0xec, 0xcf, - 0xd3, 0xd9, 0x67, 0x8e, 0x1e, 0xdf, 0x1f, 0xd2, 0xc4, 0xa3, 0xe9, 0xf3, 0xe4, 0xcf, 0xe4, 0x5c, 0x24, 0x8f, 0xc9, - 0x5e, 0xf5, 0xb6, 0xed, 0x22, 0xa5, 0x11, 0xa0, 0x8e, 0xde, 0xac, 0xdf, 0x85, 0x74, 0x4d, 0x32, 0x15, 0x0f, 0xca, - 0xa7, 0x7c, 0x20, 0xbe, 0xae, 0xbf, 0x4c, 0xf7, 0x50, 0x88, 0x78, 0x11, 0xf8, 0xef, 0xf9, 0xfe, 0x23, 0xb6, 0x59, - 0x57, 0x5a, 0x9d, 0xcd, 0x8d, 0x1e, 0xba, 0x7d, 0x75, 0x32, 0xf5, 0x89, 0x94, 0x51, 0x7a, 0x5d, 0x88, 0x97, 0x18, - 0x17, 0x37, 0xf9, 0x21, 0xdb, 0x7e, 0x18, 0x9f, 0xd7, 0xf8, 0x81, 0x08, 0x8a, 0xb0, 0x8f, 0x19, 0x32, 0x3e, 0x40, - 0x4d, 0xe5, 0x54, 0xb0, 0x62, 0x5a, 0xa9, 0x4a, 0x03, 0xa0, 0x69, 0xf4, 0x4b, 0x94, 0x7f, 0xa6, 0x07, 0xf2, 0xc3, - 0x1f, 0xbd, 0x75, 0xde, 0x3f, 0xa7, 0xff, 0xbe, 0xff, 0xfc, 0xa3, 0x06, 0x26, 0x05, 0x64, 0xdd, 0x87, 0x95, 0x6d, - 0x12, 0x8e, 0xca, 0xc6, 0x55, 0x56, 0x13, 0x75, 0x07, 0x99, 0x5e, 0xcd, 0x6c, 0xf7, 0xcd, 0x5b, 0xf6, 0xa1, 0x17, - 0xd1, 0x4c, 0xc9, 0xa3, 0x52, 0xe4, 0x1d, 0x72, 0x71, 0xf5, 0x39, 0x7c, 0x19, 0xeb, 0xaa, 0x90, 0x5f, 0xa9, 0x8a, - 0xe7, 0xa5, 0x0f, 0x82, 0xa8, 0x73, 0x72, 0x0c, 0x12, 0xc7, 0x91, 0x07, 0x14, 0xd8, 0x9f, 0xeb, 0x39, 0x74, 0xcf, - 0xeb, 0xcb, 0x09, 0x3c, 0x0d, 0x97, 0xb0, 0x5d, 0xef, 0xbc, 0x4b, 0x1f, 0x6a, 0x32, 0x4a, 0xb0, 0x8d, 0x74, 0x73, - 0xe8, 0xa0, 0x51, 0x3b, 0x7a, 0xe4, 0xe3, 0x9e, 0xf1, 0xd1, 0x05, 0x8a, 0xbe, 0xc7, 0xb9, 0xd1, 0x33, 0x57, 0x0e, - 0xfa, 0x5c, 0xae, 0xbb, 0xa6, 0xbd, 0xaa, 0x13, 0xa3, 0x63, 0x52, 0x79, 0x29, 0x0a, 0x20, 0x49, 0xaa, 0xa7, 0x2d, - 0x52, 0xfb, 0xa9, 0x9c, 0x0d, 0x6c, 0x9e, 0xe1, 0x5e, 0x3c, 0x13, 0x4a, 0x42, 0x37, 0xfc, 0xc5, 0xb9, 0xa6, 0x7d, - 0x61, 0x99, 0xaa, 0x30, 0xb8, 0x61, 0x35, 0x2d, 0x21, 0xe8, 0x35, 0x08, 0x36, 0x0d, 0xee, 0x3f, 0x8e, 0x20, 0xd8, - 0x04, 0x5a, 0x3b, 0x83, 0x94, 0x81, 0x33, 0x36, 0xe2, 0x1f, 0xae, 0x68, 0x10, 0xc9, 0xcd, 0x03, 0x4f, 0xe2, 0xe5, - 0xb0, 0x24, 0x52, 0xde, 0x40, 0x28, 0x08, 0x7a, 0x2a, 0xb8, 0x08, 0x52, 0xd0, 0x98, 0xf6, 0x98, 0x1d, 0xa8, 0x36, - 0x38, 0x6e, 0x80, 0xcb, 0x57, 0x49, 0xd9, 0xa4, 0xda, 0xd4, 0x65, 0xaa, 0x62, 0xc7, 0xe0, 0x91, 0x97, 0xd6, 0x41, - 0x7a, 0x81, 0x22, 0x68, 0x8a, 0x52, 0xa4, 0x57, 0x35, 0x1d, 0x85, 0xb6, 0xa8, 0x36, 0x18, 0x3d, 0xa8, 0x18, 0x28, - 0xa9, 0xb0, 0xd8, 0xc8, 0x46, 0xd1, 0x9f, 0x19, 0x22, 0x8c, 0xc2, 0x0f, 0xed, 0xca, 0xc8, 0x87, 0x8f, 0x6a, 0x98, - 0xbd, 0x9b, 0x44, 0xb1, 0xc8, 0x4b, 0x7d, 0x5e, 0xf3, 0x88, 0x9a, 0x9d, 0x26, 0xf9, 0xfc, 0x66, 0x35, 0x70, 0x8a, - 0x49, 0xc9, 0x4e, 0x78, 0xb7, 0x4a, 0x4c, 0x82, 0x88, 0xad, 0xdf, 0x3e, 0xf5, 0xbc, 0x1b, 0xb8, 0xb4, 0xf7, 0x23, - 0x61, 0x7b, 0x59, 0xf2, 0xe8, 0xf0, 0xb2, 0xa8, 0xb9, 0xf9, 0xc6, 0x9c, 0xea, 0x2a, 0xd5, 0x1b, 0x02, 0x7e, 0x95, - 0x8e, 0x5e, 0x94, 0x09, 0x0a, 0xa7, 0x36, 0xdd, 0x07, 0x93, 0x11, 0xd0, 0xd1, 0xb3, 0x1a, 0xcd, 0xf2, 0xf4, 0xd5, - 0x32, 0xb1, 0xc3, 0xc6, 0xe8, 0x23, 0x0a, 0xbc, 0x6c, 0x55, 0x06, 0x47, 0x1a, 0x55, 0xca, 0xcc, 0x0b, 0xa2, 0xea, - 0x44, 0xad, 0x60, 0x2f, 0x35, 0xf8, 0x0f, 0x08, 0xd3, 0x25, 0x0f, 0x9c, 0x1a, 0x80, 0x1c, 0xb3, 0x88, 0x74, 0x54, - 0x80, 0xdf, 0x3e, 0x4d, 0xcf, 0x98, 0x6b, 0xb8, 0xcb, 0x1a, 0x44, 0x11, 0x6d, 0x1f, 0xb1, 0x44, 0xd2, 0x1d, 0x2e, - 0x8c, 0x29, 0x42, 0xb8, 0x39, 0x2a, 0x04, 0x01, 0xac, 0x30, 0xf8, 0x12, 0xe3, 0x80, 0xb4, 0xa8, 0x7b, 0x14, 0x5e, - 0xb6, 0x0a, 0xbe, 0xcb, 0x05, 0xc7, 0x58, 0xd9, 0xbb, 0x90, 0x58, 0x17, 0xa2, 0x41, 0xb7, 0xfc, 0x7b, 0x84, 0xfc, - 0x6a, 0x68, 0x66, 0xb5, 0xf9, 0x0a, 0xee, 0x5b, 0xaf, 0x9d, 0x4d, 0x26, 0x30, 0xbb, 0x12, 0x55, 0x21, 0x8b, 0x90, - 0xb2, 0x17, 0x22, 0xd3, 0xb4, 0x95, 0x28, 0x39, 0x47, 0x40, 0x12, 0xd8, 0x02, 0x01, 0x36, 0xf8, 0xa1, 0x5a, 0x96, - 0x43, 0x09, 0x55, 0x0d, 0x8c, 0x90, 0xef, 0xc5, 0x02, 0x88, 0x5a, 0x56, 0xbd, 0xa2, 0x0c, 0xec, 0x68, 0xd9, 0xeb, - 0xac, 0x67, 0x40, 0xc9, 0x7e, 0x83, 0x40, 0x78, 0x1b, 0x9e, 0xbe, 0xff, 0x26, 0xe4, 0xd1, 0x99, 0x63, 0x4d, 0x58, - 0x78, 0x44, 0x6e, 0x1c, 0x60, 0xe5, 0x73, 0x5b, 0x82, 0x90, 0x05, 0xa5, 0xdf, 0x95, 0x2b, 0x7b, 0xd4, 0x67, 0xa6, - 0x46, 0x15, 0x82, 0xbc, 0xb9, 0xec, 0x03, 0x69, 0xa9, 0x03, 0xed, 0x1f, 0x90, 0x81, 0xc1, 0x09, 0xdc, 0x3b, 0x55, - 0x84, 0xb2, 0xc7, 0x18, 0xfe, 0xdc, 0xa6, 0xa6, 0x4d, 0xdc, 0xf3, 0x33, 0x98, 0x14, 0x03, 0x92, 0x95, 0x92, 0x7b, - 0x9e, 0xff, 0xae, 0x86, 0x2a, 0x48, 0x28, 0x4c, 0x4b, 0xf0, 0x24, 0x2b, 0x23, 0x84, 0xc8, 0x44, 0xc7, 0x41, 0xe7, - 0x40, 0xbc, 0xba, 0x37, 0x30, 0x9f, 0xd9, 0x31, 0x4b, 0x7e, 0xf7, 0x68, 0xb9, 0x4e, 0xc4, 0xb2, 0x86, 0x1f, 0x46, - 0xb3, 0x1b, 0xfb, 0x89, 0x70, 0xdd, 0xc2, 0x1a, 0x97, 0x06, 0xcf, 0xd0, 0xad, 0xf6, 0xf8, 0x4d, 0xce, 0x50, 0x4c, - 0xdb, 0x74, 0xac, 0x0e, 0xaf, 0xaf, 0xd5, 0xac, 0xb2, 0x85, 0x6a, 0xb7, 0x9c, 0x5f, 0xab, 0x6a, 0xcd, 0xd6, 0x6e, - 0xa1, 0x95, 0xd5, 0xe7, 0x3f, 0x8b, 0xf9, 0x9c, 0xc2, 0x62, 0x7e, 0x30, 0x80, 0xbb, 0x28, 0xe2, 0xc5, 0x89, 0xbb, - 0xe6, 0xda, 0xfe, 0xa0, 0xf6, 0xca, 0xe5, 0xe3, 0x6b, 0x8f, 0xfb, 0xef, 0x22, 0x46, 0xbd, 0xb0, 0x8f, 0x02, 0xb8, - 0x56, 0x23, 0x1e, 0x0f, 0x1f, 0x5d, 0xcc, 0xab, 0x35, 0xf5, 0x49, 0x1d, 0x79, 0xcf, 0x5d, 0xef, 0x5b, 0x5a, 0xb2, - 0x38, 0xad, 0x3c, 0xcd, 0x3e, 0x10, 0x23, 0xb3, 0x81, 0xd6, 0x9b, 0x34, 0x43, 0x86, 0x3b, 0x12, 0x7c, 0xb2, 0x52, - 0xf4, 0xe2, 0x64, 0xf7, 0xd4, 0x20, 0x52, 0xb2, 0xd1, 0xcc, 0x42, 0xa0, 0x96, 0x97, 0x21, 0xd3, 0x74, 0x2c, 0x0a, - 0x51, 0x0e, 0x2c, 0x28, 0x0f, 0x9a, 0x30, 0xc5, 0x93, 0x70, 0x1a, 0x47, 0x92, 0x62, 0x35, 0x0d, 0xb9, 0xcd, 0x49, - 0x89, 0x1a, 0xd2, 0xd5, 0xb9, 0xc1, 0x03, 0xad, 0x16, 0x98, 0xc0, 0xa1, 0x24, 0x05, 0x98, 0x6b, 0xa4, 0x67, 0x88, - 0x28, 0x04, 0x03, 0xf4, 0xfa, 0x96, 0x9d, 0x87, 0xce, 0xbb, 0x93, 0x72, 0x59, 0x53, 0x10, 0x6f, 0x3e, 0xf6, 0xa1, - 0x65, 0xa6, 0x75, 0x27, 0x37, 0x54, 0xf2, 0x7c, 0x09, 0xb5, 0x34, 0x81, 0xfb, 0x84, 0x8b, 0x6a, 0x26, 0x54, 0x21, - 0xff, 0x26, 0xf7, 0xfd, 0x62, 0xef, 0xc2, 0xbc, 0xba, 0x7d, 0x80, 0xcf, 0x8f, 0x97, 0x2a, 0x47, 0xe1, 0x93, 0x91, - 0xdc, 0x6a, 0x25, 0x9f, 0x67, 0x10, 0x32, 0xf3, 0x85, 0x9b, 0xbb, 0x1f, 0xb5, 0xe9, 0xc3, 0x26, 0x7f, 0xd6, 0x81, - 0xe5, 0xb6, 0x79, 0x25, 0x26, 0x7f, 0xac, 0x76, 0x2c, 0xda, 0x7b, 0x77, 0x85, 0x3e, 0x8a, 0xf5, 0xe9, 0x84, 0xbb, - 0x7f, 0xa8, 0x7c, 0x5e, 0x36, 0xfa, 0x48, 0x5d, 0xae, 0x8f, 0x7f, 0x85, 0xee, 0x8b, 0x84, 0x6a, 0x58, 0xe3, 0xbd, - 0x4c, 0xb9, 0x30, 0x7b, 0x81, 0x8d, 0xb9, 0x3a, 0xed, 0x66, 0x52, 0x82, 0x6e, 0xdb, 0xfb, 0x8f, 0xfc, 0x08, 0x67, - 0x11, 0x05, 0x37, 0xf9, 0x9d, 0x19, 0xcb, 0xa1, 0x3f, 0xdf, 0x99, 0x41, 0x2f, 0x1f, 0xc2, 0xde, 0xc5, 0xbb, 0xf4, - 0x01, 0xb9, 0x1e, 0xa7, 0x4a, 0x3e, 0xfb, 0xb1, 0xfe, 0x56, 0xc9, 0x3f, 0x8b, 0x84, 0x79, 0x6b, 0x62, 0x85, 0xb9, - 0x31, 0x49, 0x7e, 0xfb, 0xeb, 0xb6, 0xa5, 0x9d, 0xc9, 0xed, 0x62, 0xd3, 0xba, 0x85, 0x67, 0x42, 0x36, 0x81, 0x89, - 0xd9, 0x2f, 0x52, 0xd0, 0x95, 0x32, 0x35, 0x6e, 0x92, 0xd2, 0xca, 0xb3, 0xce, 0xdb, 0x77, 0xcc, 0x1c, 0xd7, 0xa7, - 0x2a, 0x0b, 0x72, 0xab, 0x28, 0xe4, 0x14, 0xda, 0x63, 0xe4, 0x12, 0x3a, 0x4c, 0x97, 0xdc, 0x45, 0xcc, 0x65, 0x11, - 0xd3, 0x7b, 0x79, 0xee, 0x93, 0x10, 0xd3, 0x66, 0x3b, 0xe5, 0x95, 0xfc, 0x0f, 0xb9, 0x79, 0x90, 0x55, 0x41, 0x1d, - 0x80, 0xe9, 0xfd, 0xd5, 0xfa, 0xf3, 0xd9, 0xd2, 0xe0, 0x54, 0x71, 0xf0, 0xf2, 0x53, 0x69, 0x72, 0xf3, 0xc6, 0x39, - 0xdd, 0x10, 0x95, 0x4a, 0x39, 0x16, 0x83, 0x8e, 0x91, 0xa3, 0x6a, 0x34, 0x8b, 0xf9, 0x04, 0x75, 0xed, 0x24, 0xfe, - 0x78, 0x26, 0x67, 0x35, 0x54, 0x73, 0x17, 0x7c, 0x72, 0x6c, 0x79, 0x37, 0x0d, 0xc5, 0x70, 0x7f, 0xb7, 0x55, 0x3f, - 0x67, 0x74, 0xd6, 0x4d, 0x0f, 0x05, 0x37, 0x70, 0xbe, 0xeb, 0xe1, 0x4b, 0x69, 0xdd, 0xaa, 0x59, 0x2a, 0x51, 0x10, - 0xaa, 0xa4, 0xb9, 0x7a, 0xc3, 0xd4, 0x40, 0x1f, 0x6a, 0xfe, 0x8e, 0x32, 0x98, 0xe2, 0x12, 0x00, 0x35, 0xc9, 0xe1, - 0xdb, 0xd4, 0x42, 0xc9, 0x48, 0x6f, 0x05, 0xe6, 0x18, 0xff, 0x1b, 0x48, 0x43, 0x26, 0x03, 0x6e, 0xf5, 0x35, 0xbf, - 0x99, 0xe4, 0xdf, 0x74, 0xdf, 0x07, 0xe7, 0xd3, 0x38, 0x7d, 0x0d, 0x05, 0xf6, 0x41, 0x7b, 0x9f, 0xf6, 0x9c, 0x29, - 0x69, 0x7b, 0x5c, 0x6d, 0xc5, 0x57, 0xdc, 0x4d, 0x61, 0xf0, 0x4f, 0x0f, 0x84, 0x22, 0xfa, 0x6e, 0xe0, 0x50, 0xb8, - 0x1d, 0x3f, 0x31, 0x8d, 0xa8, 0x43, 0xa6, 0xaa, 0x2f, 0x49, 0x3e, 0xda, 0xfc, 0x21, 0xac, 0x09, 0x8e, 0x1c, 0xe3, - 0xa6, 0x67, 0xa8, 0x88, 0xcc, 0x13, 0xaf, 0x76, 0x0f, 0x9c, 0x9a, 0x80, 0xeb, 0x79, 0x64, 0xde, 0xa7, 0xa9, 0x6d, - 0x70, 0xf1, 0x04, 0xb9, 0x73, 0x03, 0x78, 0xa7, 0x56, 0x57, 0xfb, 0x97, 0x5a, 0xef, 0x42, 0x84, 0x5b, 0x00, 0x51, - 0x8e, 0x5f, 0x64, 0x13, 0xb9, 0x7f, 0x70, 0xe6, 0x62, 0x4e, 0xc3, 0x3d, 0xd2, 0xa1, 0xe4, 0xee, 0x10, 0xb5, 0xce, - 0x2a, 0x67, 0x72, 0xa3, 0x98, 0x25, 0x93, 0x42, 0x00, 0x04, 0xa6, 0x55, 0xbe, 0x22, 0x02, 0xb8, 0x0a, 0x0b, 0x8d, - 0xa6, 0x28, 0xf2, 0x2b, 0xaa, 0xed, 0x67, 0xb4, 0x5b, 0xf6, 0xa3, 0xa3, 0x6b, 0xc7, 0x4c, 0x6a, 0x35, 0x71, 0xe9, - 0x48, 0x0a, 0x66, 0x98, 0xbc, 0xb9, 0x28, 0xe4, 0x15, 0x1f, 0xcc, 0x0f, 0x43, 0x02, 0x53, 0x69, 0x05, 0x85, 0x5c, - 0xaf, 0xb1, 0x33, 0x47, 0xa8, 0xe1, 0x34, 0x6b, 0x3c, 0x3d, 0x7d, 0x5e, 0x8a, 0xd7, 0x8e, 0x53, 0xb5, 0xcd, 0x38, - 0x1d, 0x2c, 0xc2, 0x79, 0x91, 0x76, 0x59, 0xb6, 0x12, 0x81, 0xec, 0xc7, 0xf4, 0x6f, 0xe3, 0xbc, 0xd0, 0xbf, 0x59, - 0xe7, 0x58, 0x9e, 0xc0, 0xc8, 0x12, 0x2d, 0x54, 0xc7, 0xfc, 0xa7, 0x56, 0x6c, 0xad, 0xf0, 0x9d, 0xa8, 0x24, 0xc6, - 0xab, 0x73, 0x69, 0xcf, 0x75, 0xe7, 0x87, 0x10, 0x2c, 0x0f, 0xf0, 0xb3, 0xb8, 0xca, 0xf7, 0x67, 0x85, 0x5b, 0xe9, - 0x3f, 0xeb, 0xe6, 0xef, 0x8a, 0xde, 0x93, 0x8f, 0x1f, 0x52, 0xc4, 0x34, 0x81, 0xf9, 0xae, 0x01, 0x34, 0x55, 0x48, - 0x58, 0x94, 0x2a, 0x6e, 0x43, 0x8e, 0xf7, 0xcf, 0xeb, 0x5e, 0xc4, 0xa2, 0x94, 0x23, 0xbb, 0x8e, 0x3b, 0x7e, 0x29, - 0x30, 0x03, 0x5c, 0xc0, 0xf7, 0x50, 0x4e, 0xa0, 0x1f, 0x3b, 0x8f, 0x8f, 0x44, 0x51, 0x38, 0x65, 0xbc, 0x53, 0x58, - 0xeb, 0xf0, 0x42, 0x79, 0x97, 0x6e, 0x14, 0xd3, 0xa8, 0x89, 0x9f, 0x32, 0xbe, 0xb1, 0x1a, 0x3d, 0xd6, 0x35, 0xba, - 0x1f, 0xcd, 0x8f, 0x82, 0x36, 0xb0, 0x88, 0xbd, 0xff, 0xd3, 0x21, 0xc5, 0xc4, 0xe4, 0xbc, 0x65, 0x2c, 0x70, 0x24, - 0xa4, 0xca, 0xad, 0xcc, 0xf7, 0xa9, 0x88, 0xca, 0xf4, 0x2b, 0x9c, 0xf1, 0x3b, 0x42, 0x44, 0x15, 0x16, 0xfb, 0xa7, - 0xd6, 0x3d, 0x66, 0xd2, 0xcd, 0xa6, 0x3e, 0x55, 0x20, 0x0d, 0x45, 0x9e, 0xaa, 0xe9, 0x25, 0x34, 0xb7, 0xbb, 0xcf, - 0xcf, 0x67, 0x8f, 0x0a, 0xf2, 0xf9, 0xef, 0x0f, 0xfa, 0xfe, 0xbe, 0x90, 0x07, 0xad, 0x6f, 0xe5, 0x33, 0xd4, 0xfe, - 0x75, 0x95, 0x3d, 0x35, 0xc0, 0x99, 0x22, 0x92, 0x97, 0xfc, 0x08, 0xa7, 0x6b, 0x6e, 0x96, 0xbe, 0x7a, 0xca, 0x75, - 0x3f, 0x5d, 0xce, 0x6a, 0x91, 0x1c, 0x33, 0x44, 0xd0, 0xde, 0xc8, 0xb8, 0xc7, 0xf7, 0x59, 0x22, 0x9d, 0x85, 0x09, - 0xba, 0x88, 0xea, 0xf6, 0x68, 0x43, 0xd9, 0xad, 0xe6, 0x2d, 0xf7, 0xc6, 0x1f, 0xe8, 0x7b, 0xd6, 0x8b, 0xa0, 0x34, - 0x94, 0x60, 0xc7, 0xdd, 0xa8, 0x71, 0x44, 0x3a, 0xd7, 0x1d, 0xa4, 0x91, 0x7b, 0xa1, 0x58, 0x52, 0xde, 0x77, 0xb3, - 0xa3, 0x30, 0x69, 0x81, 0x15, 0x76, 0xea, 0xf2, 0xe0, 0x6e, 0x5a, 0x98, 0x75, 0x0a, 0x85, 0xca, 0x74, 0x31, 0xf0, - 0xc5, 0xa6, 0xb4, 0xae, 0x57, 0x0e, 0xc8, 0x01, 0x8c, 0x8e, 0x83, 0x14, 0x26, 0xd5, 0x58, 0x92, 0xca, 0xd0, 0xd1, - 0x72, 0x68, 0x59, 0xaa, 0x14, 0x14, 0xbd, 0x03, 0x0c, 0xca, 0x78, 0xf6, 0xff, 0xc3, 0x9c, 0x0b, 0x83, 0x58, 0x0e, - 0xec, 0x57, 0x64, 0x5f, 0x5d, 0x77, 0xc2, 0x49, 0x54, 0x10, 0xa6, 0xba, 0x91, 0xa9, 0x47, 0x15, 0xd8, 0x84, 0x1c, - 0xd2, 0x24, 0xc9, 0xe9, 0xc0, 0x04, 0x72, 0xe4, 0x69, 0x13, 0x51, 0x17, 0x92, 0xca, 0xe5, 0x97, 0xce, 0xb7, 0x5f, - 0x71, 0xe6, 0x2f, 0x30, 0x92, 0x53, 0x4a, 0xc7, 0x3a, 0x8b, 0xf1, 0x3b, 0xed, 0x7e, 0x3a, 0x6f, 0xfb, 0x9c, 0x5f, - 0x72, 0x80, 0xde, 0x42, 0x55, 0xce, 0x10, 0x90, 0x1e, 0xfa, 0xfe, 0x7a, 0x47, 0xb5, 0xa0, 0x3b, 0x6e, 0xfa, 0xe4, - 0x33, 0xce, 0x5e, 0xad, 0x93, 0xcf, 0x4f, 0xde, 0x28, 0xc4, 0x48, 0x04, 0x5d, 0x3b, 0x52, 0x95, 0x76, 0x9f, 0x6f, - 0xe4, 0x2a, 0x5c, 0xae, 0x41, 0xb3, 0x83, 0xa2, 0x53, 0xda, 0xcf, 0x94, 0xb1, 0xae, 0x7e, 0x4a, 0x0e, 0x1b, 0xb0, - 0xd9, 0x10, 0xe3, 0x48, 0xdc, 0x78, 0xa5, 0x62, 0x80, 0x33, 0x37, 0xaa, 0x61, 0xa5, 0xb7, 0x9d, 0x93, 0x5f, 0xe2, - 0x95, 0x06, 0xcf, 0x13, 0x2c, 0xd1, 0x45, 0x9f, 0x57, 0x8f, 0xd3, 0x51, 0xe6, 0x1b, 0xea, 0xdf, 0xa0, 0xda, 0x26, - 0x5d, 0x88, 0x75, 0x9a, 0xce, 0x21, 0xcf, 0x95, 0x1f, 0xdd, 0x99, 0x20, 0x73, 0x47, 0x85, 0x6b, 0xd5, 0xa0, 0x40, - 0x53, 0xf6, 0xc9, 0x4a, 0x78, 0x74, 0xeb, 0x93, 0x4c, 0xda, 0x47, 0x6b, 0xe5, 0xc3, 0xad, 0x28, 0xfb, 0x77, 0xcf, - 0xbf, 0xf7, 0x99, 0xde, 0x22, 0x1d, 0xd7, 0xac, 0xf6, 0x2f, 0xf8, 0x29, 0xe7, 0x34, 0xde, 0x12, 0xa5, 0x44, 0xe5, - 0x87, 0xe3, 0x80, 0x58, 0xbc, 0x41, 0xfc, 0xd1, 0x0f, 0xdc, 0xec, 0x45, 0xac, 0x2f, 0x55, 0x5a, 0x54, 0x7f, 0xb2, - 0xc7, 0x55, 0x0d, 0x8e, 0x1f, 0xeb, 0xf9, 0x75, 0xac, 0xbc, 0x13, 0x0d, 0xb0, 0x56, 0x02, 0x1f, 0xdb, 0x95, 0x80, - 0x02, 0x22, 0xbd, 0x25, 0x6f, 0xcf, 0xff, 0x77, 0x8b, 0xfd, 0x7e, 0x47, 0xf7, 0xd3, 0xce, 0x8d, 0xaa, 0xd1, 0x69, - 0x53, 0x58, 0x0a, 0xdb, 0xee, 0xb3, 0xc0, 0x45, 0xc6, 0x80, 0x40, 0x35, 0xe6, 0x1f, 0x92, 0x30, 0xa7, 0xc0, 0xbb, - 0x3c, 0x38, 0x8e, 0xfc, 0x96, 0xfa, 0xc6, 0x0a, 0xf7, 0xef, 0xf6, 0xae, 0xb7, 0x20, 0x42, 0x65, 0x9f, 0xa0, 0xdc, - 0x91, 0x3f, 0xf6, 0xd3, 0x0b, 0xd4, 0x47, 0x61, 0xaf, 0x60, 0xd5, 0xd1, 0xa2, 0x6b, 0x07, 0x3a, 0x07, 0xbd, 0x1b, - 0x51, 0x51, 0xf9, 0x98, 0x8d, 0xa5, 0x66, 0x7c, 0x04, 0x30, 0x02, 0x58, 0x0c, 0x71, 0xe2, 0xda, 0x2b, 0xf7, 0x35, - 0xba, 0x02, 0x02, 0x8c, 0xe1, 0x1f, 0x36, 0x38, 0xb7, 0x5e, 0x59, 0x06, 0x9a, 0x1c, 0xe0, 0xd4, 0x26, 0x8b, 0x53, - 0x8b, 0x53, 0xbc, 0xdf, 0xf9, 0xc6, 0xe8, 0xed, 0x05, 0x39, 0x1d, 0xf0, 0x1e, 0x7b, 0x14, 0x15, 0x35, 0x64, 0xa0, - 0x85, 0x6f, 0xbb, 0x21, 0x62, 0x65, 0x30, 0x0b, 0xfa, 0x70, 0xee, 0xfb, 0xf3, 0x4b, 0x2a, 0x54, 0xff, 0x57, 0xaf, - 0xbd, 0xae, 0x5a, 0xf0, 0xc4, 0x35, 0xec, 0x2e, 0xb8, 0x72, 0x4a, 0xed, 0x58, 0xa5, 0xa7, 0x9d, 0x64, 0x50, 0x10, - 0xda, 0x21, 0x85, 0x67, 0xff, 0x67, 0x51, 0x7d, 0x9e, 0x5b, 0xcd, 0xa1, 0xfa, 0xcc, 0x3a, 0x0e, 0x88, 0x7f, 0x3f, - 0xca, 0xbb, 0x3a, 0x08, 0x50, 0x35, 0xe4, 0x93, 0x02, 0xf3, 0x5f, 0xf1, 0x2c, 0x6f, 0x44, 0xba, 0x9d, 0xd9, 0x7d, - 0x8d, 0xcb, 0x99, 0xdc, 0x4e, 0xe6, 0x9b, 0x79, 0xb8, 0xd9, 0x79, 0x7f, 0xbd, 0xa5, 0x4a, 0xda, 0x7a, 0xa5, 0x3e, - 0x3d, 0xe0, 0x38, 0x20, 0xd2, 0x66, 0x19, 0x46, 0x73, 0x7e, 0xee, 0x06, 0xbe, 0x1f, 0x16, 0x61, 0xbe, 0x5f, 0xfb, - 0xb5, 0xe0, 0xc6, 0x24, 0x6f, 0x24, 0x4e, 0xd4, 0xc0, 0x65, 0x8a, 0xa1, 0x2b, 0x05, 0xbc, 0x89, 0x43, 0x5f, 0xc3, - 0x94, 0x1d, 0xf4, 0x5e, 0xb8, 0x1e, 0xf5, 0x74, 0xc4, 0x05, 0xae, 0xba, 0x79, 0x24, 0x93, 0xcc, 0x37, 0x94, 0x31, - 0xde, 0xf0, 0xaa, 0xdf, 0xb8, 0x73, 0xaf, 0x93, 0x32, 0x80, 0x5d, 0x2a, 0x28, 0x7e, 0xbc, 0x6a, 0x55, 0x53, 0x9f, - 0x8a, 0x10, 0xb2, 0x90, 0x4b, 0x01, 0xee, 0xf2, 0xfc, 0x99, 0x7c, 0x1e, 0x5d, 0xdc, 0x0d, 0x55, 0x03, 0xd0, 0x6a, - 0xea, 0xeb, 0x02, 0xc6, 0x91, 0x27, 0x45, 0xca, 0x70, 0x66, 0x6d, 0x78, 0x51, 0xab, 0x4f, 0xb9, 0xa4, 0x21, 0xa3, - 0xb6, 0x53, 0xea, 0x41, 0xbe, 0xd6, 0xd9, 0xec, 0x91, 0x37, 0xb8, 0xa1, 0x65, 0xbb, 0x92, 0x8f, 0x20, 0x8a, 0x26, - 0xc0, 0x72, 0x96, 0xb6, 0x09, 0x32, 0xf8, 0x0e, 0x2d, 0x92, 0xc1, 0x10, 0xb1, 0xc0, 0x9e, 0x77, 0xab, 0xe2, 0xb5, - 0xbd, 0x9c, 0x6a, 0x27, 0xd3, 0xef, 0x72, 0x74, 0xf6, 0x81, 0x3a, 0x1c, 0xac, 0xea, 0x65, 0x17, 0x6a, 0xfd, 0xbb, - 0x1f, 0xda, 0x0a, 0x02, 0x59, 0x03, 0x27, 0x4a, 0x8a, 0xbd, 0x52, 0x65, 0x6b, 0xe4, 0x24, 0xc0, 0x5d, 0x1f, 0xcf, - 0x44, 0x14, 0xb3, 0x74, 0x4e, 0xbf, 0x0b, 0x42, 0x8f, 0x31, 0xac, 0x90, 0x6a, 0x02, 0x8d, 0x9f, 0x5c, 0xd1, 0xdd, - 0x60, 0x35, 0xd9, 0x33, 0xd2, 0x17, 0x63, 0x3d, 0xdd, 0xd9, 0x36, 0xa8, 0x43, 0xfb, 0x6c, 0xb6, 0xaf, 0x2b, 0x8c, - 0x58, 0xf9, 0xa2, 0x1a, 0x7b, 0x61, 0x0b, 0xdc, 0xa9, 0x0b, 0x91, 0xb1, 0x62, 0x66, 0x7a, 0xc2, 0x50, 0x70, 0x5c, - 0x23, 0x1f, 0xe3, 0xc4, 0x4c, 0xa4, 0xb4, 0x2b, 0x76, 0x9a, 0x12, 0x70, 0x0a, 0x84, 0x8e, 0x3d, 0xed, 0xd6, 0x6a, - 0x41, 0xf2, 0x60, 0xe9, 0xb2, 0x1f, 0xe2, 0x7a, 0x3c, 0x2d, 0x29, 0x84, 0x20, 0x5c, 0x9c, 0x9e, 0x75, 0xb3, 0x8c, - 0xe3, 0xc1, 0x94, 0xa6, 0xc3, 0xfe, 0x69, 0x3f, 0xad, 0x0c, 0x3e, 0xde, 0x57, 0x2b, 0x3c, 0x76, 0x20, 0xf8, 0x3c, - 0xfa, 0xde, 0x20, 0xcf, 0xff, 0x34, 0xc9, 0xff, 0x3a, 0x86, 0x40, 0x0d, 0x5b, 0xb1, 0xa0, 0x1b, 0xbe, 0xb1, 0x09, - 0x5e, 0xae, 0x99, 0x57, 0x3a, 0x5b, 0x8b, 0xb3, 0x88, 0x0c, 0x0b, 0x78, 0x7c, 0xf3, 0xe3, 0xfa, 0x23, 0x9c, 0x8d, - 0xcb, 0x18, 0xc3, 0x4b, 0x6e, 0x80, 0x24, 0x21, 0x5c, 0x42, 0xcc, 0x58, 0x77, 0xcf, 0x0d, 0x6e, 0xab, 0x8d, 0xaf, - 0x01, 0xb7, 0x9e, 0x2f, 0xc6, 0xcb, 0x84, 0xf3, 0xe9, 0xcf, 0xca, 0x75, 0x4f, 0xa5, 0xb9, 0x51, 0x6f, 0xb5, 0xfe, - 0xe3, 0xd9, 0xef, 0x1e, 0xb0, 0x24, 0xdc, 0x4f, 0x2d, 0xc3, 0x7c, 0xf2, 0xea, 0x92, 0x89, 0x10, 0xcf, 0x02, 0x5a, - 0x0e, 0x51, 0x88, 0x0f, 0x17, 0x90, 0xe7, 0xee, 0xe8, 0x70, 0xe4, 0x76, 0x9a, 0x4b, 0x23, 0x47, 0x76, 0x30, 0xb4, - 0xa8, 0xa4, 0x0b, 0xab, 0x95, 0x69, 0x9f, 0x4a, 0x37, 0x22, 0x72, 0x20, 0x31, 0x59, 0xb1, 0xcf, 0x30, 0xd3, 0x0e, - 0x9c, 0x7a, 0x40, 0xfc, 0xcf, 0x7f, 0x11, 0x19, 0xca, 0x09, 0x2f, 0xb5, 0xb0, 0x84, 0xa5, 0xca, 0x6c, 0x1f, 0x8f, - 0x9c, 0xca, 0xcc, 0xaa, 0x57, 0x8b, 0x78, 0xeb, 0x8d, 0x86, 0xa6, 0x13, 0x23, 0xc5, 0x07, 0xbe, 0x8c, 0x02, 0x2a, - 0x34, 0xe9, 0xa1, 0x88, 0xd7, 0xf3, 0xcc, 0x39, 0x04, 0xe0, 0x1b, 0x7d, 0xb7, 0x54, 0x75, 0x5d, 0x5f, 0xec, 0x77, - 0x29, 0xf7, 0x25, 0x05, 0xb1, 0xe4, 0xdc, 0x3d, 0xc6, 0x39, 0x1c, 0x39, 0x78, 0x6a, 0x24, 0x15, 0x75, 0x16, 0x89, - 0xc4, 0x82, 0x96, 0xd2, 0xed, 0xb0, 0xdc, 0x03, 0xa6, 0x51, 0xa8, 0x6a, 0xa3, 0xc7, 0x5d, 0x97, 0x00, 0xda, 0xec, - 0x24, 0x84, 0xf7, 0xf0, 0x7d, 0xb6, 0x98, 0x0b, 0xb9, 0xe0, 0x6c, 0x7f, 0x9b, 0x53, 0x29, 0x57, 0xb1, 0x67, 0xa3, - 0xb4, 0x43, 0xf3, 0xed, 0xdc, 0xb3, 0x5a, 0x32, 0x2b, 0x62, 0x0c, 0x91, 0xd3, 0x37, 0x92, 0xb6, 0x0d, 0xd2, 0xe1, - 0x70, 0xcd, 0x90, 0xe0, 0x73, 0x5e, 0x6b, 0x2f, 0xc5, 0x93, 0x71, 0x52, 0xf8, 0x6f, 0x26, 0xd9, 0x79, 0x6d, 0xfd, - 0xa3, 0x3f, 0x24, 0x5b, 0x01, 0xc1, 0x13, 0x7e, 0x35, 0xd9, 0xcc, 0xae, 0xb9, 0xf9, 0xbd, 0x86, 0xf6, 0xd4, 0x52, - 0xb0, 0x66, 0x02, 0xad, 0x4d, 0xca, 0xe7, 0xa2, 0x8f, 0xaf, 0x57, 0x77, 0x24, 0xee, 0xd3, 0x67, 0xd2, 0x35, 0x25, - 0x2f, 0x19, 0x0b, 0xca, 0x37, 0xbc, 0xd9, 0x1f, 0xa3, 0x08, 0xa8, 0x1a, 0x72, 0x05, 0xf5, 0x75, 0x4f, 0xa6, 0xcf, - 0xda, 0x41, 0x58, 0xaf, 0x02, 0x9b, 0x80, 0x22, 0x11, 0xfd, 0xb7, 0x79, 0x8f, 0x3e, 0xe4, 0xd0, 0x1d, 0xb2, 0x37, - 0xb0, 0x9e, 0xac, 0x74, 0x27, 0x11, 0x8a, 0x0a, 0x9f, 0x3b, 0x01, 0xa5, 0x64, 0x07, 0xa5, 0x06, 0x7c, 0xd6, 0x4b, - 0xe4, 0x98, 0xf9, 0xc6, 0xf1, 0x2e, 0xf1, 0x8e, 0xb2, 0xf8, 0xc0, 0x81, 0x9d, 0xea, 0xe7, 0xef, 0x63, 0xf9, 0x72, - 0x8c, 0x07, 0x91, 0xd1, 0xef, 0x45, 0x01, 0xbe, 0xec, 0x37, 0xcd, 0xc8, 0xf3, 0xaf, 0xbf, 0xa5, 0x8d, 0x3c, 0xf3, - 0x6b, 0xa9, 0x84, 0x65, 0xdd, 0x9d, 0x3f, 0x45, 0xbb, 0x94, 0xd8, 0x70, 0x5b, 0xf4, 0xc1, 0x20, 0x97, 0x1b, 0x00, - 0x1f, 0x70, 0x2e, 0xff, 0x99, 0xa3, 0x50, 0x3e, 0x52, 0xeb, 0xf2, 0x64, 0x29, 0xc4, 0x98, 0xfc, 0x8d, 0x51, 0x2d, - 0x67, 0xae, 0x8f, 0xfc, 0x7e, 0xc1, 0x2f, 0x9c, 0x9d, 0xee, 0x07, 0xc8, 0x02, 0x41, 0x8b, 0x15, 0x5d, 0xe9, 0xa1, - 0xa8, 0xa5, 0xaa, 0x18, 0x4a, 0xf3, 0x62, 0x2c, 0x2d, 0x8a, 0x69, 0x63, 0x0f, 0x5e, 0x20, 0x52, 0x70, 0x3d, 0x35, - 0x4b, 0xa6, 0xd0, 0x43, 0xcf, 0xe0, 0x9e, 0xa9, 0xa4, 0xac, 0x75, 0x4e, 0xc3, 0xc0, 0x0a, 0x66, 0xc4, 0x0a, 0x6b, - 0xab, 0x93, 0x96, 0xbd, 0x02, 0x31, 0x96, 0x05, 0x0a, 0x14, 0xaa, 0x83, 0x58, 0x49, 0x15, 0x12, 0xcc, 0xd9, 0x16, - 0x23, 0x3d, 0x5b, 0x49, 0x95, 0xee, 0x34, 0x34, 0xe7, 0x67, 0x85, 0xd1, 0x1b, 0x01, 0xdd, 0xa2, 0xb8, 0xbf, 0x5f, - 0xd7, 0xb0, 0x41, 0x66, 0xa5, 0x2a, 0xaa, 0x17, 0x51, 0x95, 0x69, 0x46, 0x5a, 0x82, 0xec, 0xf9, 0x0c, 0xe4, 0xaa, - 0x72, 0xd4, 0x1e, 0xd3, 0x5e, 0x00, 0xd5, 0xe8, 0xb3, 0xe8, 0xe8, 0x45, 0x9a, 0x43, 0x5d, 0x37, 0x6c, 0xa9, 0x15, - 0x65, 0x52, 0x50, 0xac, 0x91, 0xdf, 0x4e, 0xb2, 0x46, 0x44, 0x76, 0xb3, 0x8b, 0xef, 0xe9, 0xae, 0x92, 0x4d, 0xb2, - 0xf1, 0x69, 0x1c, 0x20, 0xb4, 0x4e, 0x48, 0x2c, 0x6a, 0xbc, 0xe0, 0x2d, 0xe1, 0xd2, 0x72, 0x18, 0x7f, 0x74, 0xbc, - 0xbf, 0xe4, 0x0a, 0x0f, 0xac, 0x48, 0xeb, 0x84, 0x45, 0x7e, 0x32, 0x2e, 0xe0, 0xd7, 0xfe, 0xac, 0x90, 0x59, 0x33, - 0x16, 0xb9, 0xf0, 0x59, 0x94, 0x27, 0xb1, 0xae, 0x2d, 0xdd, 0x93, 0x71, 0xff, 0xd8, 0x99, 0x3a, 0xde, 0x9f, 0x74, - 0x08, 0xfc, 0x02, 0x50, 0xaa, 0x1e, 0x10, 0xfb, 0xc0, 0xf7, 0x78, 0x65, 0x51, 0x04, 0x97, 0xe1, 0xf6, 0x90, 0x8a, - 0xf2, 0x34, 0x77, 0xd5, 0xb4, 0xf2, 0x17, 0x21, 0x09, 0xaa, 0xe1, 0x9b, 0x94, 0xa4, 0x40, 0xe9, 0xa3, 0x1c, 0x26, - 0x3d, 0x62, 0x9f, 0xdf, 0xfb, 0xd2, 0x67, 0xa7, 0xf0, 0xa5, 0x4f, 0xba, 0x66, 0x6d, 0x85, 0xb7, 0xff, 0x36, 0xb0, - 0x83, 0x99, 0x1e, 0xc5, 0xef, 0x87, 0x38, 0x2d, 0xd6, 0x32, 0xea, 0xce, 0x2f, 0x96, 0xbd, 0x0d, 0xf9, 0x3f, 0xfc, - 0xee, 0x82, 0xd3, 0xf0, 0xfd, 0x69, 0xd5, 0xdd, 0x78, 0x5b, 0x39, 0x74, 0xbf, 0x75, 0xbf, 0x6d, 0xa5, 0x5b, 0x3a, - 0x79, 0xe1, 0x7f, 0x7b, 0xef, 0x9c, 0xfb, 0x96, 0x8b, 0xcf, 0x24, 0x33, 0x5e, 0x7d, 0x12, 0xeb, 0x74, 0xac, 0x20, - 0xbd, 0x72, 0x91, 0x49, 0xaf, 0xb7, 0xc6, 0xf1, 0x8b, 0xc4, 0xda, 0xee, 0xc4, 0x9a, 0x7d, 0x50, 0x35, 0x79, 0x8c, - 0xc1, 0xa3, 0xad, 0x2c, 0xa6, 0x3f, 0x58, 0xe7, 0xd1, 0xff, 0x36, 0xb2, 0x39, 0x6e, 0x5c, 0x8e, 0x36, 0x7f, 0xb1, - 0x44, 0x3c, 0x5b, 0xed, 0x57, 0xde, 0xa7, 0xb5, 0x25, 0x8b, 0xbf, 0x49, 0x4b, 0xd2, 0x5a, 0x8c, 0xef, 0xe5, 0x83, - 0x25, 0x7d, 0x8a, 0x3d, 0x07, 0x11, 0x1a, 0xb1, 0xe6, 0x82, 0x1a, 0xb9, 0x4e, 0x57, 0x02, 0xb1, 0x0b, 0x3f, 0x1f, - 0x7a, 0x8a, 0x0b, 0xa4, 0x3a, 0xc8, 0xcb, 0x40, 0x35, 0x51, 0x40, 0x3f, 0x50, 0x53, 0x82, 0x9c, 0x67, 0x7b, 0xc7, - 0xb7, 0x87, 0xaf, 0xf1, 0xf6, 0xcc, 0xd2, 0x46, 0x64, 0x7e, 0x8b, 0x07, 0x47, 0x58, 0x9a, 0xdb, 0x09, 0x93, 0x45, - 0xd8, 0x42, 0xd1, 0xf2, 0x0e, 0xd9, 0x63, 0xf3, 0x4b, 0x03, 0xd5, 0x85, 0x1c, 0xb1, 0x9e, 0xff, 0x5a, 0xef, 0xd2, - 0x15, 0xbc, 0x4b, 0xfb, 0xa2, 0x97, 0x66, 0x3d, 0x04, 0x40, 0x77, 0xea, 0xcf, 0x40, 0x95, 0xb9, 0xa1, 0x28, 0x7d, - 0x7e, 0x9d, 0xb5, 0xa7, 0xda, 0x55, 0xe0, 0xde, 0x69, 0x78, 0x76, 0x42, 0x56, 0x2e, 0x11, 0xfd, 0x18, 0xec, 0x73, - 0x02, 0xfd, 0x41, 0x33, 0xb4, 0xaf, 0x3b, 0x3a, 0x71, 0x09, 0x28, 0x13, 0x75, 0xb8, 0x24, 0x46, 0x30, 0x7e, 0x90, - 0x83, 0x0d, 0xbc, 0x5a, 0xdf, 0xa5, 0x24, 0xae, 0xba, 0x73, 0x08, 0xd1, 0x6b, 0xde, 0xff, 0xea, 0xd7, 0x60, 0xbf, - 0xde, 0xe8, 0x26, 0x23, 0xf2, 0xf7, 0xfc, 0x9c, 0x4b, 0x34, 0x63, 0x88, 0x10, 0x55, 0x2c, 0x5a, 0xf3, 0x1c, 0x0b, - 0x58, 0x9e, 0x97, 0xe0, 0xbb, 0x7c, 0xde, 0x75, 0x62, 0xab, 0x89, 0xa5, 0x6a, 0x30, 0x62, 0x17, 0x0e, 0xde, 0x07, - 0x29, 0x30, 0x70, 0x59, 0x76, 0xd2, 0x7d, 0x47, 0x32, 0x1b, 0x1f, 0x34, 0x07, 0x04, 0x37, 0x3d, 0xc8, 0x02, 0x7f, - 0x23, 0x8c, 0xa1, 0x85, 0x07, 0xeb, 0xa1, 0xd9, 0xa6, 0xbc, 0x06, 0x17, 0xdc, 0x74, 0xa5, 0x22, 0xd5, 0x97, 0xb6, - 0x5a, 0x06, 0x0a, 0x4b, 0xb3, 0x60, 0xe0, 0x5b, 0x5a, 0x2c, 0x8b, 0x10, 0x83, 0x3c, 0x5c, 0xe7, 0x24, 0x84, 0xf2, - 0x04, 0xa1, 0x0e, 0x05, 0x66, 0x3c, 0x38, 0x9d, 0x22, 0x70, 0x30, 0xaf, 0x39, 0xaf, 0x3b, 0xbf, 0x67, 0xc2, 0x32, - 0x4b, 0x8f, 0x7b, 0x0d, 0x97, 0x4b, 0x13, 0x39, 0xd0, 0xbd, 0xd9, 0xfb, 0xdb, 0x2d, 0xf2, 0xbe, 0x11, 0x5a, 0xb1, - 0x0d, 0x17, 0x15, 0x17, 0x59, 0xb4, 0xba, 0xc4, 0xb8, 0x93, 0xae, 0x57, 0xee, 0x85, 0xdd, 0x7a, 0xea, 0x9e, 0xac, - 0x37, 0x4c, 0xe1, 0xd3, 0xb0, 0x8c, 0x25, 0xc4, 0x1a, 0x4b, 0x47, 0xff, 0xbb, 0x2c, 0x7c, 0x96, 0xb5, 0x07, 0x0c, - 0x85, 0xb8, 0x3f, 0xd3, 0xe3, 0x27, 0x00, 0x0e, 0xc7, 0x13, 0x1f, 0x27, 0xe0, 0x40, 0x6b, 0x49, 0xa1, 0x5b, 0x33, - 0x01, 0x11, 0x6b, 0x4b, 0xfe, 0x4b, 0x5f, 0x89, 0x52, 0xf4, 0xd9, 0xb1, 0xeb, 0xdc, 0xe4, 0xfd, 0xdb, 0xea, 0x58, - 0x35, 0x2d, 0x21, 0xef, 0xba, 0x05, 0xea, 0x61, 0xf9, 0xfb, 0xae, 0x58, 0xd1, 0x11, 0x08, 0x3c, 0x7f, 0x63, 0x5a, - 0xac, 0x4f, 0x06, 0x48, 0xd7, 0x83, 0x4f, 0x58, 0x24, 0x9e, 0x82, 0x63, 0x20, 0x8e, 0xab, 0xe1, 0x23, 0xf3, 0xc3, - 0xff, 0x2c, 0x27, 0x56, 0xa6, 0x9d, 0xc9, 0xa9, 0x63, 0xaa, 0x23, 0x83, 0x00, 0xfa, 0x22, 0xf7, 0xb8, 0xee, 0xbf, - 0x3a, 0xb5, 0x54, 0x31, 0x6c, 0xb6, 0x37, 0x71, 0x6b, 0xee, 0xc4, 0x7a, 0xa5, 0xcb, 0xe0, 0xd3, 0xca, 0xfd, 0xe2, - 0xf4, 0x43, 0x26, 0xcc, 0xe0, 0x6c, 0xf1, 0xe5, 0x83, 0x4d, 0x0f, 0x19, 0x0d, 0x80, 0x09, 0x4b, 0xe9, 0x27, 0x83, - 0x94, 0x3e, 0x3f, 0x31, 0x12, 0xda, 0x36, 0xf8, 0x67, 0x6e, 0x2e, 0x47, 0x39, 0xd0, 0x7f, 0x18, 0xec, 0x62, 0xa3, - 0xb7, 0x49, 0x18, 0x3f, 0x40, 0x9a, 0xbd, 0xd1, 0x8f, 0x7a, 0xcd, 0xa1, 0x25, 0x16, 0xe5, 0xd5, 0x93, 0xfc, 0x98, - 0x65, 0x46, 0x01, 0xf8, 0x90, 0xf7, 0x1e, 0xfa, 0xe7, 0x98, 0x62, 0xce, 0xe1, 0xeb, 0xf8, 0xd7, 0x0e, 0x62, 0x6d, - 0xed, 0x74, 0xcd, 0xff, 0xce, 0x5c, 0x78, 0x6a, 0x73, 0xc2, 0x8c, 0xb6, 0x5f, 0xbd, 0xba, 0xda, 0x74, 0x18, 0x01, - 0x34, 0xbe, 0xc2, 0xe8, 0xb1, 0x09, 0xdd, 0x06, 0x66, 0x24, 0xe0, 0x9e, 0x67, 0xd2, 0x95, 0x8e, 0x3f, 0x16, 0xf0, - 0x66, 0xe6, 0x77, 0xa0, 0x09, 0x77, 0x57, 0xd2, 0x68, 0x4b, 0x92, 0x1c, 0xf9, 0x6d, 0xc1, 0x44, 0xb1, 0x75, 0xeb, - 0x26, 0xbc, 0x16, 0xf8, 0xff, 0xf8, 0x41, 0x00, 0xf2, 0x6e, 0x51, 0xb3, 0xa4, 0x76, 0x9a, 0xe6, 0x2b, 0x4d, 0x29, - 0xbb, 0xb0, 0x72, 0x93, 0x5d, 0xce, 0xfc, 0xff, 0x30, 0x82, 0x9b, 0x1c, 0x3e, 0x89, 0x1e, 0xda, 0x57, 0x80, 0xa4, - 0x07, 0x44, 0x17, 0x0f, 0x5a, 0x38, 0x7e, 0x23, 0xca, 0x1c, 0x2c, 0x6c, 0x0b, 0x96, 0x05, 0x83, 0x28, 0x7b, 0x04, - 0xf3, 0x0b, 0x1d, 0x98, 0xfc, 0x4d, 0xef, 0x28, 0xe7, 0x10, 0xe9, 0xd5, 0x77, 0x25, 0xa7, 0xae, 0x9d, 0x5e, 0xfa, - 0xbf, 0x69, 0x02, 0x4c, 0xe6, 0xb6, 0xbe, 0x4e, 0x2d, 0x5f, 0xf8, 0x3c, 0x6b, 0x2f, 0x8a, 0x71, 0xfc, 0xed, 0x8a, - 0x8e, 0x77, 0xc6, 0xac, 0x77, 0x14, 0x35, 0xad, 0xae, 0x7d, 0x75, 0xd3, 0x82, 0x6e, 0x9c, 0x10, 0x3c, 0xc6, 0x87, - 0x58, 0x7e, 0xde, 0x7c, 0x93, 0x50, 0xc7, 0xdf, 0xc6, 0x1e, 0x6f, 0x9b, 0x29, 0x3c, 0xb1, 0x03, 0x7e, 0x47, 0xf7, - 0x96, 0x8e, 0x57, 0x57, 0x4c, 0x7c, 0x08, 0x4e, 0x45, 0x80, 0xd7, 0x93, 0x24, 0xbe, 0x29, 0x89, 0x83, 0x0d, 0xa7, - 0xd6, 0xb6, 0xc2, 0x7d, 0x59, 0xbb, 0x43, 0x16, 0x4e, 0xe3, 0x9b, 0xa8, 0x1b, 0x1e, 0x71, 0xc6, 0x49, 0xdc, 0xea, - 0xa9, 0xef, 0xb6, 0x41, 0x8c, 0xc0, 0xe1, 0x0a, 0x30, 0x3e, 0x11, 0x3e, 0xc4, 0x6c, 0x6a, 0x00, 0xa7, 0x22, 0xb0, - 0xea, 0x87, 0x03, 0x4c, 0xd9, 0xfd, 0x94, 0x0f, 0xe9, 0x88, 0xc0, 0xcc, 0xf4, 0x10, 0xe5, 0xfd, 0xe7, 0xf0, 0x48, - 0xde, 0x9f, 0x4f, 0x86, 0xe1, 0x50, 0xc8, 0xb5, 0xd9, 0xb0, 0x07, 0x56, 0xbe, 0xe0, 0x06, 0xe7, 0x6b, 0xb6, 0xed, - 0xda, 0x84, 0xdc, 0xfc, 0x23, 0x9e, 0x61, 0x97, 0x98, 0xde, 0xdc, 0x3b, 0x5d, 0x47, 0x3d, 0xdf, 0x2b, 0x17, 0x52, - 0xc3, 0x3e, 0xd1, 0xe2, 0xd1, 0xf3, 0x6c, 0xa4, 0x75, 0xc7, 0xc9, 0x5b, 0xfd, 0x4b, 0x72, 0x24, 0x04, 0xc5, 0x4f, - 0xb2, 0xf6, 0x3e, 0x91, 0x6d, 0xdf, 0x05, 0xcf, 0xad, 0xf6, 0x3a, 0x42, 0x77, 0xfa, 0xd3, 0x23, 0x6c, 0x4a, 0xe7, - 0xe7, 0x0e, 0xa0, 0x3a, 0x92, 0xf3, 0xd9, 0xe5, 0x1e, 0x06, 0xe5, 0x70, 0xc5, 0x33, 0x70, 0xb7, 0x62, 0xe6, 0x21, - 0xd2, 0x35, 0x5a, 0x7f, 0xf7, 0x82, 0x37, 0x5c, 0x33, 0x59, 0x1b, 0x91, 0xdc, 0x16, 0xf2, 0x30, 0xc7, 0x08, 0x65, - 0xbe, 0xa0, 0xfc, 0x70, 0xd1, 0x66, 0x72, 0x96, 0x84, 0x2f, 0x24, 0x0a, 0xfc, 0x49, 0x95, 0x67, 0xae, 0x1c, 0x2f, - 0x57, 0x6c, 0x60, 0x2e, 0xe8, 0x8b, 0x51, 0x40, 0x22, 0x73, 0xb7, 0xfc, 0x32, 0xcf, 0x90, 0xfc, 0x35, 0x72, 0x6d, - 0x07, 0x58, 0xbe, 0xce, 0x53, 0x06, 0x37, 0x2e, 0x5a, 0x0b, 0x1d, 0x5f, 0x4a, 0x26, 0x41, 0x91, 0x42, 0xa8, 0xb4, - 0x48, 0x68, 0xd4, 0x2a, 0x15, 0x30, 0x6e, 0xa1, 0xa1, 0xdf, 0x6b, 0xd5, 0xe7, 0x4f, 0xd8, 0xf9, 0x63, 0x94, 0xff, - 0x51, 0x5c, 0x04, 0xc8, 0x91, 0xb7, 0xa8, 0x1b, 0xf0, 0x4c, 0x91, 0xd4, 0x66, 0x8e, 0x4b, 0x24, 0x1c, 0x83, 0x64, - 0x61, 0xb7, 0x61, 0xef, 0x7f, 0xc7, 0xd7, 0x54, 0x90, 0x30, 0x88, 0xf0, 0xeb, 0x2c, 0x83, 0x6e, 0xe0, 0x22, 0x98, - 0x6a, 0x84, 0x07, 0x1c, 0x45, 0xdd, 0x35, 0xab, 0x80, 0x13, 0x28, 0x41, 0xc9, 0x22, 0x89, 0x1f, 0x77, 0xa0, 0xff, - 0x5f, 0x8a, 0x51, 0x7d, 0xd6, 0xd7, 0xb7, 0x15, 0xd4, 0x43, 0x87, 0x02, 0x15, 0x19, 0x37, 0xc0, 0x66, 0x8f, 0x8f, - 0x45, 0x0e, 0xd8, 0x30, 0xf9, 0xaf, 0xb0, 0xb0, 0x52, 0xd9, 0x72, 0x3a, 0xfc, 0xcb, 0x35, 0x8b, 0x83, 0x3d, 0x3c, - 0x4c, 0xe3, 0x30, 0x3e, 0xa5, 0x25, 0x7d, 0x5e, 0xe8, 0xa4, 0x51, 0xd1, 0xf9, 0x71, 0x9e, 0xf5, 0x7d, 0x57, 0xf2, - 0xf8, 0x35, 0x5e, 0x9f, 0xd9, 0x53, 0x74, 0x9d, 0x1f, 0x7e, 0xf4, 0xa3, 0xb1, 0x65, 0xfc, 0x37, 0x7a, 0x61, 0x4f, - 0x17, 0x94, 0x96, 0x81, 0xf7, 0xe9, 0xd1, 0x62, 0x25, 0xfb, 0x82, 0x1c, 0x7d, 0x8c, 0x8e, 0xf6, 0x78, 0x4e, 0xf9, - 0x79, 0x16, 0xe7, 0xfd, 0xed, 0x6b, 0xbc, 0x38, 0xf3, 0xac, 0x5c, 0xeb, 0xcd, 0xa7, 0xde, 0x06, 0xec, 0x2d, 0x70, - 0x3f, 0x89, 0xdd, 0x40, 0x84, 0x93, 0x60, 0x0c, 0xd3, 0xbd, 0x69, 0x44, 0x03, 0xec, 0x77, 0xed, 0xf9, 0xc0, 0x03, - 0xfd, 0xcf, 0xe6, 0xf5, 0xe0, 0xdc, 0x6e, 0x54, 0x53, 0x0a, 0x70, 0xc1, 0x64, 0x45, 0x31, 0x46, 0x82, 0x48, 0x23, - 0xbd, 0x1d, 0x1d, 0xb9, 0xa8, 0x2b, 0x9c, 0x26, 0xba, 0xe4, 0x69, 0xe2, 0x26, 0x65, 0x6b, 0x99, 0x00, 0x50, 0x96, - 0x64, 0xec, 0xd0, 0xf3, 0x7a, 0x80, 0xf4, 0x0e, 0x72, 0x42, 0x2c, 0xc7, 0x25, 0x90, 0x2d, 0x19, 0x7c, 0xfb, 0x0f, - 0xab, 0x40, 0x5e, 0x6f, 0xe8, 0xb0, 0x09, 0x69, 0xf6, 0x38, 0x3d, 0x7d, 0x71, 0x00, 0xae, 0x44, 0xa6, 0x67, 0x9a, - 0x06, 0x17, 0x7d, 0x8e, 0x3e, 0x34, 0xc2, 0x5a, 0x60, 0x2a, 0xea, 0xb4, 0xe5, 0xad, 0x52, 0x71, 0xf3, 0xe0, 0x78, - 0x0a, 0x07, 0x43, 0x33, 0x30, 0x22, 0x7f, 0xfa, 0x0f, 0x1b, 0xc6, 0x72, 0x24, 0xad, 0x6c, 0x98, 0xbf, 0xec, 0x72, - 0x2b, 0x37, 0x4b, 0x12, 0x9a, 0x86, 0x5e, 0x3d, 0x88, 0x15, 0xde, 0xa9, 0xff, 0xe7, 0x41, 0x69, 0x83, 0x38, 0x87, - 0x64, 0x01, 0x51, 0x3c, 0x47, 0x38, 0xc5, 0xa0, 0xc5, 0x6c, 0x90, 0xc3, 0x94, 0x81, 0xc0, 0x2b, 0xab, 0x37, 0x81, - 0x1b, 0x71, 0xb9, 0xec, 0xe9, 0xd4, 0x6b, 0xae, 0x9d, 0xd4, 0x26, 0xb2, 0x08, 0x57, 0xf8, 0xcd, 0x07, 0x80, 0xae, - 0x36, 0xd4, 0x51, 0x08, 0xe4, 0x08, 0x9b, 0xe7, 0x8a, 0x14, 0xdd, 0x78, 0x7c, 0x1b, 0xf6, 0x2d, 0x47, 0x88, 0xcd, - 0x8f, 0xb9, 0x6b, 0x8d, 0x06, 0x8d, 0x4c, 0x32, 0x6c, 0x5c, 0x0a, 0x76, 0x92, 0xa0, 0x87, 0x1a, 0xc7, 0x38, 0x94, - 0x15, 0x7a, 0x1e, 0x19, 0xe3, 0x88, 0xaf, 0x7c, 0xc9, 0x9a, 0x93, 0x68, 0x91, 0x8a, 0x81, 0xfd, 0x1c, 0xbe, 0xce, - 0x0b, 0x41, 0x2e, 0x8e, 0xb8, 0xe9, 0xa9, 0x21, 0xa7, 0x3e, 0x19, 0x14, 0xa8, 0x88, 0x5b, 0xaf, 0x2d, 0x68, 0x98, - 0x47, 0x01, 0x71, 0x6e, 0x16, 0x38, 0xc2, 0x29, 0x2c, 0x6a, 0xff, 0xe0, 0xe8, 0xbc, 0x75, 0xb4, 0x40, 0x90, 0xda, - 0x09, 0x67, 0x38, 0x99, 0xd1, 0x11, 0x32, 0xc3, 0xe5, 0xf1, 0x71, 0x53, 0xd3, 0x5a, 0x53, 0xa7, 0x95, 0x22, 0xc9, - 0x0c, 0x69, 0x26, 0xb0, 0xc4, 0x0f, 0xdb, 0xde, 0x5c, 0xa4, 0x62, 0x45, 0xe0, 0x2d, 0x66, 0xfc, 0x5c, 0xd8, 0x81, - 0xe2, 0xd5, 0x84, 0x0e, 0x6c, 0xaa, 0xfc, 0xdc, 0xe6, 0xa6, 0x27, 0xfc, 0xc2, 0x61, 0xfa, 0x75, 0x26, 0xfa, 0x59, - 0x98, 0xa3, 0xd5, 0x41, 0x2f, 0x5c, 0x21, 0xe3, 0xc4, 0x33, 0x64, 0xd9, 0x94, 0x43, 0xf7, 0x1a, 0x25, 0x8a, 0xa4, - 0x01, 0x39, 0xda, 0x43, 0x4e, 0x2e, 0xf3, 0xa4, 0xd5, 0x34, 0x2a, 0xbb, 0x24, 0xe1, 0x2d, 0x7e, 0xe4, 0x31, 0xa1, - 0x44, 0xf9, 0x3c, 0xcd, 0x33, 0x92, 0xac, 0x71, 0xb7, 0xa3, 0xe0, 0x1a, 0xbd, 0xb5, 0xba, 0xac, 0xd5, 0xb0, 0x9f, - 0xc8, 0xbf, 0x52, 0x47, 0x6f, 0x52, 0x3c, 0x18, 0x04, 0x19, 0x86, 0xab, 0x96, 0xdd, 0x43, 0x8b, 0x1e, 0xfb, 0xa2, - 0xfa, 0x77, 0x83, 0x60, 0xe2, 0x49, 0x21, 0x84, 0x96, 0x91, 0x03, 0xfd, 0x37, 0x55, 0xaa, 0x25, 0x12, 0xde, 0x3b, - 0x9f, 0xb3, 0x77, 0x13, 0xa4, 0x04, 0xb3, 0x4d, 0x95, 0x7b, 0x20, 0x5c, 0x87, 0x80, 0xd7, 0x0d, 0x77, 0xa8, 0x77, - 0x91, 0x5b, 0x11, 0x74, 0x29, 0x05, 0x88, 0x88, 0x00, 0x8c, 0x5e, 0x0c, 0x34, 0x1c, 0xa6, 0x19, 0xac, 0x44, 0x82, - 0x7f, 0x95, 0x85, 0x21, 0xb5, 0xec, 0x28, 0x07, 0xc0, 0x66, 0xb3, 0x11, 0x8c, 0xbe, 0x60, 0x05, 0x7c, 0x36, 0x89, - 0xff, 0xc6, 0xa9, 0x6a, 0xa6, 0x75, 0x23, 0xe5, 0xdd, 0xb8, 0xb1, 0x76, 0x81, 0x18, 0xa6, 0xa5, 0x75, 0x3b, 0x21, - 0x09, 0x28, 0x0d, 0x0b, 0x9f, 0x3c, 0xbf, 0x3d, 0x46, 0xd7, 0xdf, 0x1f, 0x3e, 0x30, 0x49, 0x14, 0x19, 0x55, 0x32, - 0x30, 0x4d, 0x84, 0x8c, 0x6f, 0x11, 0x3a, 0x1e, 0x8e, 0xa6, 0x45, 0x7e, 0xea, 0x75, 0x6c, 0x37, 0x50, 0x8f, 0x2f, - 0xbf, 0xb6, 0xdc, 0x39, 0xd1, 0xda, 0xe9, 0xb8, 0x3f, 0x04, 0x9a, 0x9c, 0x88, 0xaa, 0xb2, 0x18, 0x25, 0xfb, 0x87, - 0x81, 0x6d, 0x24, 0x39, 0xf5, 0x54, 0x18, 0x77, 0x6f, 0x73, 0x4f, 0x87, 0x89, 0x8b, 0x23, 0xff, 0xcb, 0x1f, 0xc1, - 0xb5, 0x52, 0xbc, 0xf2, 0x1d, 0xd8, 0x72, 0x09, 0x57, 0x0e, 0x56, 0x0d, 0x02, 0xa2, 0x94, 0x00, 0x72, 0x1d, 0x1f, - 0x1d, 0xe3, 0x24, 0x79, 0xd7, 0x33, 0xd6, 0xd4, 0x6c, 0x49, 0x69, 0xe6, 0x63, 0x5f, 0x53, 0x69, 0x1e, 0xe7, 0x9a, - 0x60, 0x96, 0x64, 0xd9, 0x49, 0x56, 0x80, 0xd7, 0x74, 0x0d, 0xf3, 0x55, 0x85, 0x40, 0x30, 0xdf, 0x55, 0x99, 0x8b, - 0x53, 0x85, 0xb8, 0x1d, 0x09, 0x6d, 0xaa, 0x45, 0x78, 0xe1, 0xc0, 0x38, 0x9c, 0x5f, 0x33, 0x2d, 0x06, 0x06, 0x20, - 0x57, 0x52, 0x6f, 0x84, 0xf3, 0xf4, 0x20, 0x6f, 0x43, 0xf1, 0xa4, 0xc0, 0x56, 0xac, 0x78, 0xf0, 0xad, 0x97, 0x46, - 0xb3, 0xca, 0x68, 0x97, 0x5b, 0x71, 0xa6, 0xf3, 0xa4, 0xcf, 0x4e, 0x9b, 0xd2, 0xad, 0x87, 0x80, 0x0a, 0xe5, 0xf9, - 0x19, 0xcf, 0xc7, 0x2b, 0xc4, 0x39, 0xe6, 0xac, 0x09, 0xd5, 0x73, 0x61, 0xfd, 0x3a, 0x3d, 0x64, 0xff, 0x7a, 0xbc, - 0xb5, 0xce, 0x54, 0xdc, 0x3c, 0x53, 0xc8, 0x54, 0xfc, 0x15, 0xf7, 0x5e, 0x3c, 0x30, 0x5c, 0x93, 0xc7, 0x90, 0x67, - 0x2a, 0x27, 0x7c, 0x69, 0xa0, 0xe9, 0x20, 0xaa, 0xd3, 0xad, 0x10, 0x57, 0x5d, 0x18, 0xa2, 0x70, 0xf7, 0x29, 0x2f, - 0x48, 0xeb, 0xb0, 0xf7, 0x54, 0xa3, 0xc3, 0xa0, 0xa6, 0x40, 0x9d, 0x16, 0x83, 0x95, 0xb4, 0x5c, 0x50, 0x0e, 0x05, - 0x33, 0xd5, 0x06, 0x1e, 0xd8, 0x9a, 0xff, 0x7f, 0x40, 0x21, 0x7a, 0xdc, 0xf6, 0xaf, 0xfc, 0x05, 0x73, 0xc2, 0x8c, - 0x25, 0x33, 0x3c, 0xbc, 0xda, 0x19, 0x7f, 0x0a, 0xba, 0xb1, 0x6a, 0xa3, 0x8c, 0x55, 0x39, 0x51, 0x06, 0xf7, 0x93, - 0xa5, 0x98, 0x3a, 0x85, 0x0b, 0x31, 0x95, 0x97, 0x7d, 0xa4, 0x92, 0x52, 0x1c, 0x79, 0x51, 0x01, 0xc0, 0xbc, 0x0b, - 0x34, 0x65, 0xca, 0x93, 0x20, 0x09, 0x5c, 0x54, 0x01, 0xd1, 0x8c, 0x97, 0x73, 0x7a, 0xe3, 0x2b, 0x8e, 0x7b, 0xc4, - 0x49, 0x74, 0xe6, 0x10, 0xd4, 0xf5, 0xf9, 0x29, 0x23, 0x62, 0x41, 0xd8, 0x57, 0x8c, 0x43, 0xd9, 0x4e, 0x58, 0x5f, - 0xac, 0xd7, 0x77, 0xde, 0x24, 0x8a, 0xa4, 0x2b, 0xb3, 0x7c, 0xe4, 0xfb, 0x0c, 0x99, 0xad, 0x92, 0x8d, 0x38, 0x86, - 0x32, 0xce, 0x19, 0x8f, 0x62, 0x03, 0x81, 0xb3, 0xa5, 0x2f, 0xb5, 0x90, 0xe3, 0xb2, 0x34, 0x94, 0xc7, 0xc0, 0xb9, - 0x2b, 0xdb, 0x9b, 0xd7, 0xf1, 0x31, 0xe7, 0xfd, 0xb7, 0xeb, 0x4d, 0xad, 0xa8, 0x3f, 0x63, 0xd5, 0x86, 0x7d, 0x81, - 0xa2, 0x79, 0x30, 0xeb, 0x74, 0x8e, 0xf2, 0x88, 0x27, 0x9c, 0x3c, 0x8b, 0x3a, 0xd6, 0xae, 0x8f, 0x92, 0x17, 0x67, - 0xaf, 0xa3, 0xe2, 0x34, 0x88, 0x7e, 0x59, 0xcd, 0x52, 0x07, 0xd7, 0x77, 0xc8, 0x5e, 0xe6, 0x72, 0xed, 0x44, 0xa9, - 0x86, 0x06, 0xce, 0x9f, 0xe4, 0xab, 0xd8, 0x3e, 0xe1, 0x2c, 0x67, 0xa2, 0xca, 0x7e, 0x9d, 0x07, 0xa9, 0x30, 0xe7, - 0xf8, 0xe3, 0xd2, 0x86, 0xe8, 0x2b, 0x22, 0x24, 0xe6, 0xac, 0xfb, 0xce, 0xa6, 0x2c, 0x71, 0xe9, 0xc3, 0x08, 0xa1, - 0x9f, 0x18, 0xa1, 0x19, 0x47, 0x3d, 0x6f, 0xfe, 0xb7, 0x91, 0xef, 0xb3, 0x9e, 0x53, 0x74, 0xc7, 0x7b, 0xb2, 0x1a, - 0x2a, 0xdb, 0xe9, 0x67, 0xee, 0xd4, 0x9e, 0x82, 0x53, 0x7b, 0x82, 0x4a, 0x90, 0xc0, 0xcf, 0x0b, 0xcf, 0xf1, 0xa4, - 0xd9, 0x54, 0x17, 0x4f, 0xdb, 0x5f, 0x8d, 0xcf, 0x2f, 0x95, 0x8d, 0xaf, 0x38, 0xbb, 0x52, 0x68, 0x67, 0x78, 0x4b, - 0x67, 0xae, 0x0c}; +const uint8_t INDEX_BR[] PROGMEM = { + 0x1b, 0xe2, 0x97, 0xa3, 0x90, 0xa2, 0x95, 0x55, 0x51, 0x04, 0x1b, 0x07, 0x80, 0x20, 0x79, 0x0e, 0x50, 0xab, 0x02, + 0xdb, 0x98, 0x16, 0xf4, 0x7b, 0x22, 0xa3, 0x4d, 0xd3, 0x86, 0xc1, 0x26, 0x48, 0x49, 0x60, 0xbe, 0xb3, 0xc9, 0xa1, + 0x8c, 0x96, 0x10, 0x1b, 0x21, 0xcf, 0x48, 0x68, 0xce, 0x10, 0x34, 0x32, 0x7c, 0xbf, 0x71, 0x7b, 0x03, 0x8f, 0xdd, + 0x37, 0x06, 0x9a, 0x30, 0x50, 0xe4, 0x08, 0x47, 0x68, 0xec, 0x93, 0xdc, 0x7d, 0x53, 0xf5, 0x4f, 0xd7, 0x8a, 0xcf, + 0x2f, 0x85, 0x3a, 0x6c, 0xa9, 0x63, 0xcb, 0xf5, 0xc8, 0x18, 0xe3, 0xf5, 0xdb, 0x0c, 0x05, 0x9b, 0x48, 0x2c, 0x42, + 0x21, 0xa0, 0x2c, 0x25, 0xf7, 0xcb, 0xb7, 0xaa, 0x65, 0xd5, 0x7f, 0x3e, 0x2f, 0x94, 0x2b, 0x53, 0xa7, 0x0f, 0x4e, + 0x56, 0xa9, 0xf7, 0xce, 0x54, 0x88, 0x39, 0xef, 0xea, 0x3d, 0x69, 0x56, 0xd0, 0x52, 0x96, 0x0a, 0x5b, 0x35, 0xd4, + 0x42, 0xd6, 0x35, 0x10, 0xf3, 0x7f, 0xec, 0x95, 0xd3, 0x2a, 0xfe, 0x4d, 0x22, 0x6a, 0xd6, 0x69, 0x6e, 0x7b, 0x5a, + 0xdd, 0x33, 0x58, 0x21, 0xa4, 0x33, 0xd6, 0x61, 0x05, 0xf5, 0x08, 0xa9, 0x10, 0x32, 0xf5, 0xdc, 0x04, 0x59, 0x72, + 0x41, 0xf4, 0x09, 0xfa, 0x08, 0x08, 0x9b, 0xcc, 0xf3, 0x2d, 0x71, 0xb4, 0x1c, 0xe3, 0x04, 0x64, 0x9a, 0x96, 0x5b, + 0x16, 0x58, 0xed, 0xbf, 0x37, 0xd5, 0x2a, 0x6d, 0x80, 0x66, 0xcf, 0xba, 0xd4, 0xb8, 0xd4, 0x38, 0x65, 0x10, 0x57, + 0x3a, 0xe7, 0x93, 0xe0, 0x82, 0x90, 0x78, 0xef, 0xfd, 0xff, 0x96, 0xed, 0x30, 0x44, 0x77, 0x13, 0x3b, 0x70, 0x92, + 0xe8, 0xb0, 0xf4, 0x25, 0x5a, 0xc9, 0xff, 0xff, 0xbb, 0x01, 0x76, 0x03, 0x94, 0x16, 0x20, 0xa5, 0x2d, 0x8a, 0xe2, + 0x56, 0x51, 0xd4, 0xdc, 0x18, 0xcb, 0x99, 0xb3, 0x4e, 0x3b, 0x55, 0x67, 0x5d, 0x64, 0xa3, 0xc4, 0xf8, 0xec, 0x2a, + 0xb7, 0x51, 0x68, 0x6c, 0x7a, 0xd9, 0x85, 0x97, 0x9e, 0xcb, 0x61, 0x26, 0xbe, 0xeb, 0x3a, 0x6b, 0xe5, 0x38, 0xd8, + 0x63, 0xa8, 0xb5, 0xba, 0xbe, 0xbd, 0x1d, 0xe3, 0xc4, 0x11, 0xa3, 0x08, 0xc4, 0xfe, 0x26, 0x3a, 0xfb, 0x01, 0x5d, + 0xb4, 0xfc, 0x1d, 0x78, 0xca, 0xb2, 0xac, 0x61, 0x39, 0x21, 0xe5, 0x6b, 0x5a, 0x70, 0x76, 0x57, 0x91, 0x4c, 0x8c, + 0x3d, 0x0e, 0x50, 0x4e, 0x41, 0x1c, 0xda, 0x62, 0xd2, 0xf1, 0x25, 0xce, 0x03, 0xf4, 0xfa, 0x3b, 0xc1, 0xc4, 0xed, + 0xc1, 0xdf, 0x8f, 0xe1, 0xc0, 0x0e, 0x34, 0x72, 0x3c, 0xb3, 0x3f, 0xfa, 0xc0, 0xe6, 0xcd, 0xf4, 0x01, 0x19, 0xf4, + 0x28, 0x5b, 0xde, 0x02, 0x6e, 0xa2, 0x24, 0x59, 0x76, 0x94, 0x8d, 0x00, 0x35, 0xab, 0xbe, 0xd9, 0xc0, 0xfb, 0xfa, + 0x97, 0x4f, 0x8f, 0x6e, 0xa4, 0x28, 0x0a, 0xfd, 0x43, 0xd9, 0xf2, 0xb2, 0xc2, 0x75, 0x49, 0x97, 0x8c, 0xcd, 0x61, + 0xb3, 0x64, 0x52, 0x1e, 0x46, 0x1e, 0xa2, 0xe0, 0xb5, 0x26, 0xd3, 0xf5, 0x3c, 0x9e, 0x19, 0x32, 0x45, 0xb9, 0x28, + 0xf5, 0x40, 0xcf, 0xa5, 0xf1, 0xcd, 0x8d, 0x29, 0x55, 0xe3, 0x2c, 0x43, 0x12, 0xa2, 0x4d, 0x37, 0xda, 0x94, 0x3e, + 0x49, 0x88, 0x4f, 0x2c, 0xa5, 0x67, 0xbe, 0xb7, 0x75, 0x28, 0xb8, 0x2f, 0x0d, 0x75, 0xce, 0x87, 0x3f, 0xe3, 0x30, + 0x5a, 0x25, 0x92, 0x30, 0xd3, 0x2d, 0x45, 0xca, 0xe5, 0x49, 0xc7, 0x4d, 0x13, 0x95, 0xa5, 0x9f, 0x7f, 0x2d, 0x49, + 0x46, 0x5a, 0x49, 0x11, 0x12, 0xd2, 0xdd, 0x38, 0x3c, 0x31, 0x63, 0x5e, 0xb6, 0xe6, 0xde, 0xcd, 0xcd, 0x6d, 0x67, + 0x46, 0x53, 0x16, 0x86, 0xd4, 0xad, 0x07, 0xac, 0xdb, 0xd7, 0x9f, 0x48, 0xcc, 0xa6, 0x4d, 0x9f, 0x6c, 0x8b, 0xf2, + 0xcb, 0xcd, 0x1b, 0x1e, 0xa9, 0x39, 0x37, 0x4d, 0xcd, 0x80, 0x9b, 0x19, 0x67, 0x64, 0x70, 0xb0, 0x38, 0x00, 0x9f, + 0xab, 0x26, 0xca, 0xb7, 0xbb, 0x55, 0x50, 0xcf, 0x31, 0x65, 0x12, 0xb6, 0x2b, 0x36, 0x9d, 0x17, 0x2b, 0x50, 0xd1, + 0x13, 0x72, 0x80, 0x7f, 0x88, 0x62, 0xe4, 0xee, 0x40, 0xb7, 0x56, 0xd2, 0x66, 0xa3, 0xb0, 0x0e, 0x31, 0xeb, 0x9a, + 0x27, 0xc1, 0xd5, 0x7b, 0x63, 0xb3, 0x84, 0x1b, 0xc8, 0xbf, 0x35, 0x29, 0x8a, 0x3c, 0xd0, 0x66, 0x03, 0x2a, 0x3e, + 0xb9, 0x79, 0x4c, 0x16, 0x01, 0xaa, 0xe8, 0x0e, 0x01, 0x1c, 0x73, 0x05, 0x78, 0x3a, 0x9c, 0x23, 0xb8, 0x18, 0x78, + 0xc5, 0xcd, 0x53, 0x7f, 0xb4, 0x61, 0xb8, 0x11, 0xa4, 0xcd, 0xc6, 0x27, 0x27, 0xb6, 0xae, 0x51, 0x01, 0x1d, 0xec, + 0xe4, 0x6b, 0x99, 0xe4, 0xdb, 0x2d, 0xbb, 0x66, 0x38, 0x54, 0xf5, 0xf2, 0xba, 0xc3, 0x24, 0x41, 0xfa, 0xae, 0x46, + 0x43, 0xdd, 0xfd, 0x9d, 0x0d, 0x52, 0x98, 0x1c, 0x7f, 0xab, 0x29, 0x83, 0x0f, 0xb9, 0x11, 0xe0, 0xd3, 0x49, 0x86, + 0xef, 0xbb, 0xdf, 0x0a, 0x04, 0x76, 0x11, 0x07, 0x48, 0x7f, 0x86, 0x4c, 0x3a, 0x84, 0xf5, 0xb6, 0x32, 0x54, 0x59, + 0xed, 0xd5, 0xf1, 0xf0, 0xf8, 0x39, 0x2d, 0x10, 0x85, 0x11, 0xd2, 0xef, 0x72, 0xcb, 0xd2, 0x8f, 0xf2, 0x2e, 0x7c, + 0x9b, 0x28, 0xa6, 0x07, 0x7f, 0x7a, 0x7c, 0x43, 0x28, 0x0b, 0x3f, 0xe5, 0x98, 0x64, 0x6f, 0x63, 0xad, 0xd9, 0x90, + 0x34, 0x84, 0x30, 0xf9, 0x53, 0x6e, 0xea, 0xe3, 0x5f, 0x36, 0x39, 0xe7, 0x26, 0x49, 0xf0, 0xe9, 0xe7, 0x32, 0x50, + 0x58, 0x41, 0x1e, 0xaa, 0x98, 0x6f, 0x6b, 0xfa, 0x94, 0x4b, 0x20, 0x01, 0x84, 0x2a, 0x32, 0x84, 0x73, 0x5a, 0x39, + 0x4a, 0x57, 0xbc, 0xbd, 0x86, 0xa4, 0xb2, 0x77, 0x99, 0xe5, 0xdd, 0x44, 0x5d, 0xd5, 0xde, 0x5b, 0x94, 0x7e, 0xec, + 0x53, 0xdd, 0x67, 0xb8, 0x8d, 0xbb, 0x1d, 0x65, 0xf4, 0xe8, 0xe4, 0x73, 0x3d, 0xbc, 0xba, 0xd9, 0x30, 0xbe, 0x1f, + 0xeb, 0x0b, 0x21, 0xaf, 0x24, 0x9a, 0x44, 0xa2, 0x0a, 0xbf, 0xfe, 0xfa, 0x06, 0x14, 0x59, 0x76, 0x6d, 0x97, 0x7e, + 0xe0, 0x70, 0x8c, 0x41, 0x28, 0xac, 0x0b, 0xad, 0x4b, 0xb5, 0xbb, 0x54, 0xeb, 0x0d, 0x05, 0x24, 0xc7, 0xad, 0x04, + 0xfb, 0x9b, 0x12, 0x44, 0xec, 0x20, 0x03, 0xff, 0xba, 0x91, 0xa0, 0x50, 0xba, 0x24, 0xed, 0x9c, 0x96, 0x7e, 0xef, + 0x6f, 0x24, 0x6c, 0xd1, 0x8c, 0x55, 0xe9, 0x0f, 0x8c, 0x2f, 0x8b, 0x62, 0x44, 0x3c, 0x1b, 0x46, 0x3b, 0x49, 0x99, + 0xdc, 0xd6, 0x7a, 0x70, 0x5d, 0xe5, 0x2a, 0x62, 0x2e, 0x54, 0xab, 0x44, 0xf2, 0xf4, 0x61, 0xb2, 0xd8, 0x07, 0x8b, + 0x01, 0x3e, 0x04, 0x19, 0xe1, 0x5d, 0x8e, 0x2a, 0xff, 0x86, 0xa3, 0x59, 0xe5, 0xcc, 0x8d, 0xd3, 0x51, 0x6f, 0xc1, + 0x15, 0x9f, 0x37, 0x73, 0x3d, 0x49, 0x99, 0xca, 0x53, 0x3a, 0x96, 0x0c, 0x92, 0x2b, 0x8b, 0xde, 0x08, 0x68, 0x52, + 0x87, 0x31, 0xb2, 0x68, 0x81, 0xb1, 0xe9, 0x9f, 0x78, 0xf1, 0x22, 0xe8, 0x84, 0x48, 0xdb, 0x49, 0x4d, 0xd2, 0xea, + 0x80, 0x1f, 0xec, 0x50, 0x77, 0x66, 0xe7, 0x13, 0x36, 0x02, 0x85, 0x6f, 0xdd, 0x68, 0xe0, 0x4b, 0x6c, 0x5b, 0xbe, + 0x18, 0xca, 0xaf, 0x92, 0x97, 0xdd, 0x4e, 0x90, 0x28, 0x4e, 0x48, 0x42, 0x62, 0xc3, 0xf1, 0xf7, 0x71, 0x59, 0x2b, + 0x24, 0x2e, 0x4b, 0xf1, 0x52, 0x2d, 0x7b, 0xbf, 0x8f, 0x5d, 0x1a, 0x29, 0x6b, 0xdd, 0xed, 0x8b, 0x0d, 0xa3, 0xaf, + 0x1a, 0x94, 0x32, 0xc4, 0x54, 0x3d, 0xa1, 0xee, 0x41, 0x42, 0x00, 0xc3, 0xc2, 0x23, 0x57, 0x52, 0x9c, 0x48, 0x54, + 0x42, 0x82, 0x61, 0xb1, 0xcb, 0x1b, 0xae, 0x8f, 0xfa, 0x30, 0x6c, 0x00, 0xc4, 0x1b, 0x74, 0x7c, 0x99, 0x51, 0x60, + 0x45, 0x6d, 0x05, 0xe0, 0x44, 0x15, 0x24, 0x98, 0xb1, 0x40, 0x5f, 0xa1, 0x5e, 0x43, 0x55, 0xae, 0x10, 0xbd, 0x9d, + 0x80, 0x41, 0x6e, 0x35, 0x5d, 0xe8, 0xb2, 0x34, 0x7a, 0x1b, 0xb8, 0x29, 0xad, 0x6d, 0xd3, 0xb4, 0x4f, 0x32, 0x0e, + 0x4e, 0xd7, 0xb3, 0x98, 0x12, 0x37, 0xd4, 0x5c, 0x19, 0xbd, 0x26, 0xaa, 0xbb, 0x5b, 0x7d, 0x92, 0xd3, 0xb7, 0xd3, + 0x2e, 0xfa, 0x6e, 0xf6, 0x2b, 0xaa, 0xc4, 0x24, 0x46, 0x6d, 0x58, 0xe9, 0x7e, 0x8d, 0x27, 0x23, 0x14, 0xbc, 0x15, + 0xaf, 0x1b, 0x88, 0x7b, 0xd1, 0x9d, 0xba, 0x9c, 0x08, 0xd2, 0xf9, 0x9b, 0x81, 0xfd, 0xf8, 0x94, 0xc1, 0x0a, 0xf1, + 0xc8, 0xfa, 0x4e, 0x5b, 0x87, 0x86, 0x34, 0xed, 0x92, 0xcf, 0xfd, 0x49, 0xda, 0xd7, 0x25, 0xc9, 0xe3, 0x22, 0xdf, + 0x9e, 0xdd, 0x53, 0x30, 0x15, 0xe0, 0x2c, 0x5a, 0xcf, 0x41, 0xb3, 0x0d, 0xa4, 0x3a, 0x7b, 0x70, 0xc8, 0x9e, 0x4e, + 0x6b, 0xa7, 0xeb, 0xa3, 0x55, 0xd5, 0xc6, 0x45, 0x89, 0x21, 0x81, 0x5f, 0xb1, 0x29, 0x01, 0xe4, 0x40, 0xe4, 0xd1, + 0x6b, 0xe3, 0x4b, 0xe1, 0xfa, 0xf5, 0x12, 0x7d, 0x82, 0x59, 0xb9, 0xfa, 0x07, 0x0d, 0xa9, 0xa4, 0xd5, 0x80, 0x90, + 0x91, 0xfa, 0x8c, 0xf2, 0xcc, 0x0a, 0xee, 0x97, 0xce, 0x17, 0x31, 0x3a, 0x3c, 0xfd, 0x6e, 0x3f, 0x34, 0xf6, 0x2d, + 0x94, 0x17, 0x65, 0xa5, 0x32, 0x73, 0x94, 0x13, 0x80, 0x24, 0x96, 0x3c, 0x25, 0xd2, 0xc6, 0xb7, 0xad, 0x2d, 0x11, + 0xc1, 0x37, 0x7c, 0x88, 0x77, 0xee, 0x05, 0xc7, 0x26, 0x21, 0x81, 0x0a, 0xed, 0x76, 0x01, 0x14, 0x54, 0x90, 0x89, + 0x23, 0xc9, 0xd5, 0xd1, 0x20, 0xb1, 0x3f, 0x56, 0x36, 0x1d, 0x3c, 0x22, 0x92, 0xb5, 0xcd, 0x06, 0xd0, 0x91, 0xc6, + 0xab, 0x4a, 0x92, 0x83, 0x08, 0x4b, 0x00, 0x3a, 0x56, 0xfe, 0x49, 0x4a, 0x5c, 0x4e, 0xd0, 0x85, 0x41, 0xc1, 0x5d, + 0x1a, 0xc6, 0xcd, 0x26, 0xb9, 0xb0, 0x52, 0x01, 0xfd, 0x78, 0xe8, 0xc7, 0x63, 0x0f, 0x45, 0x0a, 0x82, 0x56, 0x88, + 0x87, 0x9c, 0xd2, 0x81, 0x22, 0xfa, 0xa5, 0xfe, 0x71, 0x9b, 0x37, 0xbf, 0x26, 0xe6, 0x46, 0x89, 0x8a, 0xe6, 0x3c, + 0xa6, 0x52, 0xd4, 0xc7, 0x88, 0xc1, 0x3f, 0x66, 0xec, 0xd0, 0x61, 0xa2, 0x92, 0x5e, 0xaa, 0x54, 0xac, 0x83, 0x75, + 0x26, 0x95, 0x02, 0xed, 0xd4, 0xf8, 0xe2, 0x9b, 0x48, 0x12, 0xbc, 0x13, 0xb3, 0xce, 0x20, 0x85, 0x97, 0x2a, 0xac, + 0x95, 0xe8, 0x97, 0x2d, 0x0a, 0xa2, 0xc4, 0x35, 0xb4, 0x0e, 0x69, 0x42, 0x11, 0xec, 0x09, 0x1d, 0x94, 0x68, 0xf9, + 0x87, 0xb6, 0xca, 0x48, 0x82, 0x72, 0xdf, 0xf3, 0xc1, 0xbb, 0xcb, 0x80, 0xf4, 0xf0, 0x51, 0x0f, 0x29, 0x24, 0x16, + 0x3e, 0x61, 0xcb, 0x01, 0x5d, 0xb7, 0x41, 0x52, 0x00, 0xef, 0xaa, 0x62, 0x79, 0xd9, 0x2c, 0x88, 0xbb, 0x93, 0x35, + 0x35, 0x63, 0xbf, 0x4c, 0x60, 0xa7, 0x82, 0xa3, 0xd5, 0xb6, 0x09, 0x6b, 0xa9, 0x96, 0x24, 0xa3, 0x63, 0x81, 0x59, + 0x02, 0x89, 0x10, 0xe9, 0xfe, 0x58, 0x9c, 0x03, 0x31, 0xaf, 0x93, 0xcc, 0x80, 0xf3, 0xd4, 0x2a, 0x47, 0x13, 0x28, + 0x1c, 0xc7, 0x72, 0xbe, 0x26, 0x29, 0xc9, 0x13, 0x0e, 0xb0, 0x1a, 0xaf, 0xb0, 0x8e, 0x82, 0xfb, 0xb8, 0xa6, 0xa4, + 0xcc, 0xee, 0x7f, 0x99, 0xd2, 0xc4, 0x60, 0x57, 0xa2, 0x03, 0x12, 0x40, 0x4a, 0xb3, 0xd4, 0x62, 0xf0, 0x79, 0x44, + 0x3c, 0x16, 0x82, 0x89, 0x88, 0x44, 0xe1, 0x2b, 0x5d, 0xcb, 0xcf, 0xbc, 0x04, 0x84, 0xca, 0x4c, 0x83, 0xce, 0x92, + 0xd7, 0xaa, 0xa4, 0x86, 0xf6, 0x1b, 0xed, 0x46, 0x35, 0x2b, 0x9f, 0x14, 0x3e, 0x64, 0x1d, 0xb9, 0x7f, 0x1a, 0x98, + 0x64, 0xbd, 0xc9, 0x29, 0x95, 0x76, 0x96, 0xaf, 0xfe, 0xf5, 0x05, 0x8a, 0x8d, 0xaa, 0xa3, 0xe9, 0xb6, 0x3e, 0xda, + 0x10, 0x75, 0xf6, 0x11, 0x71, 0xc0, 0x13, 0x56, 0x33, 0x97, 0x5f, 0x65, 0xf8, 0xe1, 0x32, 0x39, 0x20, 0x45, 0x73, + 0x66, 0xa2, 0x6b, 0xfa, 0xef, 0x22, 0x39, 0x70, 0x89, 0xad, 0xc0, 0x14, 0x50, 0x46, 0x15, 0x63, 0x64, 0x39, 0x90, + 0xc4, 0x52, 0xc9, 0xe5, 0x7c, 0x84, 0x16, 0x59, 0x57, 0x4e, 0x19, 0x0a, 0x95, 0xd3, 0xc8, 0x1c, 0x36, 0x29, 0x8e, + 0x61, 0x5e, 0x96, 0xea, 0x79, 0x86, 0x90, 0x26, 0xdd, 0xd5, 0xa7, 0x88, 0x42, 0xcd, 0xaa, 0x7d, 0x17, 0xa6, 0xbe, + 0x08, 0x57, 0x85, 0x01, 0xf2, 0xfc, 0x61, 0x2d, 0xb2, 0xce, 0xa4, 0xf1, 0x62, 0x67, 0xbc, 0xa0, 0xb2, 0x61, 0x24, + 0x59, 0x96, 0x38, 0x28, 0x41, 0xe0, 0x94, 0x90, 0xc6, 0x3e, 0x71, 0xb8, 0x2d, 0x3f, 0x1e, 0x33, 0xb7, 0xe9, 0x50, + 0x46, 0x31, 0xe2, 0xea, 0x49, 0x95, 0x75, 0x0d, 0xe7, 0x21, 0xe6, 0x0f, 0x2f, 0x8b, 0xda, 0x6f, 0xba, 0xd2, 0xe8, + 0xc6, 0xa1, 0xb3, 0x02, 0x6d, 0x4f, 0x27, 0x73, 0x3a, 0x7d, 0x11, 0x57, 0x75, 0x52, 0x10, 0x50, 0x04, 0xc2, 0x1e, + 0x8f, 0xfe, 0x51, 0x1a, 0xed, 0x1f, 0x01, 0x4b, 0xd6, 0x31, 0xd8, 0x93, 0x6a, 0x8f, 0x09, 0x49, 0xcb, 0xdb, 0x1f, + 0x81, 0xb9, 0x52, 0x25, 0xd1, 0x43, 0xf0, 0xe1, 0x08, 0xa5, 0x05, 0x85, 0x64, 0xd3, 0x93, 0x6e, 0x43, 0xa6, 0x09, + 0x98, 0xe8, 0x71, 0x90, 0x67, 0xc3, 0x1b, 0x17, 0x55, 0x88, 0x3e, 0x3e, 0x30, 0xd9, 0xa5, 0x63, 0xb4, 0x69, 0x95, + 0x6d, 0xf6, 0x9f, 0xa1, 0xd8, 0xef, 0xf7, 0xd7, 0xcc, 0xa1, 0x48, 0xef, 0x3a, 0x23, 0x37, 0xb1, 0xe0, 0xfc, 0x14, + 0x25, 0xc6, 0xb3, 0xb6, 0x34, 0xa4, 0xe5, 0x10, 0x45, 0x48, 0x0e, 0x1d, 0x82, 0xbe, 0x0c, 0x19, 0x56, 0x57, 0xe8, + 0xf0, 0x2d, 0xfd, 0x82, 0x43, 0x26, 0x29, 0x39, 0xd2, 0x64, 0xbf, 0x97, 0xc4, 0x64, 0x57, 0xba, 0xa8, 0x40, 0x87, + 0xd5, 0xb4, 0x13, 0x43, 0xb2, 0xd5, 0xbb, 0xda, 0x66, 0xa9, 0xe5, 0x08, 0xee, 0xce, 0x03, 0xc9, 0x1f, 0x81, 0xaa, + 0xe7, 0xd1, 0x19, 0x47, 0x0b, 0x44, 0x9d, 0x4b, 0x92, 0xdb, 0x49, 0x31, 0xc8, 0x26, 0x52, 0x28, 0x90, 0xae, 0x10, + 0x8d, 0x61, 0x31, 0x6d, 0x3f, 0x08, 0x1c, 0x2c, 0x75, 0x9b, 0x64, 0xa4, 0xcf, 0x9d, 0xdd, 0x26, 0xc5, 0x23, 0x54, + 0x1e, 0xb5, 0xee, 0xbb, 0x69, 0x49, 0x90, 0xea, 0x24, 0x4f, 0x10, 0xb4, 0x67, 0x63, 0xef, 0x98, 0x80, 0xf9, 0xde, + 0x54, 0xcc, 0xaf, 0xa7, 0x6e, 0xc2, 0xc2, 0xee, 0x43, 0x8a, 0x5b, 0x66, 0x76, 0xf2, 0x9d, 0xf9, 0x1c, 0x69, 0xce, + 0x0c, 0x9d, 0xd4, 0x29, 0x24, 0xb3, 0xb1, 0xb7, 0xf4, 0x17, 0xa4, 0x79, 0x77, 0x2f, 0x3a, 0x94, 0x4d, 0xf8, 0x3d, + 0x21, 0xb8, 0x1e, 0x91, 0xc3, 0x08, 0xbe, 0xea, 0x90, 0xd8, 0xcd, 0x46, 0x2b, 0x52, 0x68, 0xed, 0x68, 0x88, 0x4b, + 0xb6, 0x7b, 0x37, 0x0b, 0x00, 0x88, 0x90, 0xd3, 0xef, 0x95, 0x86, 0x8c, 0x2d, 0xfd, 0xe2, 0x8c, 0xad, 0x14, 0xe8, + 0x59, 0x2d, 0xe2, 0x09, 0xaf, 0x09, 0x29, 0x41, 0x67, 0x85, 0x63, 0x86, 0xea, 0x43, 0xd0, 0xce, 0x1b, 0x4a, 0xb6, + 0x74, 0x30, 0x9c, 0xb8, 0x86, 0x92, 0x2d, 0x8c, 0x18, 0x1f, 0xba, 0xd9, 0x7b, 0x5a, 0x24, 0x43, 0xc1, 0x8f, 0x54, + 0x11, 0xe5, 0x22, 0x6a, 0x46, 0x68, 0x6c, 0x69, 0x30, 0x8a, 0x36, 0x9c, 0x9b, 0x77, 0x57, 0x04, 0x71, 0xd9, 0x27, + 0x56, 0x52, 0xc4, 0x8f, 0x83, 0xc4, 0xe9, 0x57, 0xab, 0x11, 0xf4, 0x32, 0x67, 0xa1, 0x34, 0xbe, 0x29, 0x85, 0x79, + 0xe4, 0x81, 0xc1, 0xb2, 0xb5, 0x0d, 0xd3, 0x3e, 0x69, 0x59, 0x3d, 0x5f, 0x55, 0x03, 0xdb, 0x20, 0x1c, 0xb5, 0x2c, + 0x1d, 0xeb, 0xe7, 0xb3, 0x8a, 0x5e, 0x37, 0xf2, 0xaf, 0x56, 0xac, 0xc5, 0x17, 0x20, 0x3b, 0x63, 0x98, 0xcd, 0x98, + 0x34, 0x2a, 0xa0, 0x16, 0x92, 0x29, 0x6b, 0x8b, 0x8a, 0xa7, 0x49, 0x09, 0x1b, 0x1a, 0x70, 0x34, 0x2d, 0x0b, 0xe9, + 0xc5, 0xeb, 0xa1, 0x7d, 0x70, 0xd6, 0xe1, 0x73, 0xcb, 0xd2, 0x23, 0x58, 0x0d, 0x78, 0x8d, 0x88, 0x12, 0x44, 0x2a, + 0xa4, 0x44, 0x85, 0x94, 0x43, 0x15, 0xd3, 0x41, 0xa7, 0x5c, 0x53, 0x67, 0xa5, 0x95, 0x79, 0x97, 0xc6, 0xf8, 0xd3, + 0x22, 0xa4, 0xb0, 0xae, 0x80, 0xc1, 0xa2, 0xf8, 0x0d, 0x04, 0xc0, 0x8b, 0x35, 0xd3, 0x33, 0x31, 0x30, 0xc7, 0x4b, + 0x5a, 0xde, 0x4b, 0x13, 0x66, 0xb1, 0x74, 0x63, 0x53, 0xa8, 0x8f, 0x8c, 0x42, 0x7a, 0xce, 0x25, 0x20, 0xea, 0xa6, + 0xc7, 0x97, 0xd9, 0x7a, 0xcf, 0xb8, 0x24, 0xff, 0x75, 0xbe, 0xdd, 0x9b, 0x15, 0x0e, 0xcf, 0x3d, 0x72, 0x38, 0x70, + 0x06, 0xa9, 0x48, 0x63, 0x06, 0x39, 0x05, 0x2f, 0x7a, 0x85, 0x19, 0x7f, 0xa4, 0x2b, 0x59, 0x22, 0x0a, 0x4f, 0x00, + 0x7f, 0x57, 0x2d, 0x42, 0xb7, 0x07, 0x84, 0xef, 0x42, 0xc6, 0x67, 0x35, 0x4c, 0xf2, 0x47, 0x18, 0x23, 0xf1, 0xe5, + 0x7b, 0x70, 0x53, 0x99, 0x8c, 0x6f, 0x7e, 0xcb, 0x92, 0x40, 0x65, 0x19, 0x4c, 0x53, 0x83, 0x92, 0x3a, 0xfb, 0x04, + 0x79, 0xe4, 0xbc, 0xaa, 0x1b, 0xa6, 0x4e, 0x9a, 0x49, 0x1e, 0xf4, 0x41, 0xa6, 0x08, 0x44, 0xa7, 0x8b, 0x61, 0xe4, + 0x81, 0x10, 0x00, 0xcf, 0x11, 0x88, 0xb4, 0x04, 0xce, 0x00, 0x8e, 0xe9, 0x9c, 0x0c, 0x1a, 0x91, 0xd1, 0xf8, 0xa9, + 0x51, 0x84, 0x8a, 0x54, 0xae, 0x63, 0xc7, 0xe1, 0x68, 0x89, 0x68, 0x94, 0xdf, 0x40, 0x31, 0x05, 0xff, 0xd2, 0xb8, + 0xb5, 0x69, 0xd7, 0x7b, 0xe2, 0x19, 0xc6, 0x96, 0xa6, 0x99, 0xa6, 0x45, 0xd1, 0x48, 0xdd, 0x67, 0x0c, 0x57, 0x2c, + 0x41, 0x9b, 0x84, 0xa2, 0x0c, 0xa3, 0x3a, 0xa6, 0x4a, 0x71, 0x0b, 0x47, 0x68, 0x54, 0xbe, 0xb5, 0x08, 0xed, 0xfd, + 0xc4, 0xf1, 0xe9, 0x32, 0x42, 0x5a, 0x9f, 0x1f, 0xbd, 0x2c, 0x30, 0xfd, 0x32, 0x9c, 0xa1, 0xaf, 0x44, 0x44, 0x34, + 0xad, 0x02, 0x3b, 0x1c, 0xe8, 0x6a, 0xc3, 0x4b, 0x73, 0x17, 0xb7, 0x35, 0xd1, 0x83, 0x33, 0xf6, 0x54, 0x86, 0xf4, + 0xed, 0x99, 0xc8, 0xba, 0x28, 0xea, 0xf6, 0xb7, 0x93, 0xaf, 0xe1, 0xb1, 0xf9, 0x78, 0x4c, 0xea, 0x14, 0xe5, 0x6b, + 0xa2, 0xd6, 0xea, 0x5a, 0x1f, 0x82, 0x99, 0x79, 0xf4, 0x5c, 0x31, 0x19, 0xe3, 0xd4, 0x8c, 0x8c, 0xac, 0xef, 0x59, + 0xe2, 0xc5, 0x36, 0xf1, 0x3b, 0x85, 0xe4, 0x47, 0xc7, 0x19, 0xd2, 0x88, 0x82, 0xa0, 0xca, 0xfc, 0x8a, 0x42, 0x19, + 0x18, 0xe9, 0xe7, 0xb6, 0xf6, 0x03, 0x72, 0xc5, 0x28, 0x96, 0xf1, 0x6c, 0x33, 0x3e, 0xe5, 0xea, 0x1f, 0x57, 0x34, + 0xc8, 0xb2, 0xb4, 0xdf, 0x89, 0xa7, 0x6d, 0x1e, 0xda, 0x66, 0x5e, 0xd9, 0x24, 0x02, 0x78, 0x95, 0x26, 0xd9, 0xf6, + 0x70, 0xaa, 0xf5, 0x47, 0xe0, 0x57, 0x5e, 0x41, 0x80, 0xcb, 0x49, 0x58, 0xb9, 0x8b, 0x02, 0x45, 0xb5, 0x2d, 0xb8, + 0x7c, 0xb0, 0x4b, 0x9f, 0x47, 0xb1, 0x44, 0x36, 0xf7, 0xc0, 0x6c, 0x51, 0x44, 0x78, 0x4a, 0xbd, 0xad, 0x51, 0xef, + 0xdf, 0x4d, 0x11, 0x1f, 0x71, 0x24, 0x77, 0x27, 0xab, 0x6e, 0x1c, 0x95, 0x47, 0x5a, 0x28, 0xfd, 0x00, 0x2f, 0x2e, + 0x9a, 0x4b, 0x97, 0x8a, 0xc7, 0x5e, 0x0a, 0xd9, 0x46, 0xc2, 0xdc, 0x22, 0x4e, 0x6d, 0xfb, 0x6a, 0xf2, 0xfd, 0x5c, + 0xd0, 0x24, 0x31, 0xeb, 0x4b, 0x97, 0xd6, 0x86, 0x4f, 0x32, 0xbd, 0xb3, 0xcf, 0x7a, 0xf6, 0x64, 0xce, 0xe4, 0xc6, + 0xe0, 0x39, 0xa8, 0xfa, 0xbd, 0xfd, 0x94, 0xba, 0x6e, 0x78, 0x94, 0xc4, 0x94, 0x26, 0x7f, 0xe1, 0x4e, 0x92, 0xe9, + 0xae, 0x33, 0x1f, 0x25, 0xdd, 0x37, 0x1c, 0xce, 0xde, 0xdf, 0xc6, 0x5d, 0x81, 0x54, 0x16, 0x1f, 0x43, 0x24, 0x3c, + 0xf1, 0xeb, 0xad, 0x31, 0xe0, 0xa1, 0x40, 0xc0, 0x83, 0x4a, 0xba, 0x59, 0xac, 0x15, 0x1d, 0xe7, 0x74, 0xff, 0x66, + 0x13, 0xce, 0x0a, 0xc3, 0x93, 0x1c, 0x27, 0xb1, 0xcb, 0xab, 0xdc, 0x4e, 0xa5, 0xad, 0x7e, 0x9a, 0x6c, 0xc0, 0x5b, + 0x68, 0x43, 0xd1, 0x72, 0x7c, 0x86, 0x5d, 0xf5, 0x43, 0x53, 0xf9, 0x27, 0xa1, 0x14, 0xc7, 0x36, 0x0d, 0x63, 0x0d, + 0xb9, 0xfe, 0xbe, 0x1d, 0xc8, 0xb4, 0x7a, 0xf3, 0xcf, 0xd9, 0xf7, 0xea, 0xed, 0x58, 0x37, 0x3c, 0x91, 0xde, 0x0e, + 0xfe, 0x7a, 0x48, 0x8a, 0x62, 0x79, 0x7b, 0x55, 0xfd, 0xd7, 0x16, 0xbf, 0x7e, 0xac, 0x2e, 0x6b, 0x2c, 0x96, 0xf9, + 0xf8, 0xb2, 0x1a, 0x4f, 0x2d, 0xef, 0xdf, 0x4e, 0xf5, 0x87, 0x2f, 0x3f, 0xf5, 0x1a, 0xb8, 0x3a, 0x73, 0x9e, 0xa4, + 0x57, 0x14, 0xfb, 0x28, 0x57, 0xc1, 0x4b, 0xf8, 0x20, 0x3f, 0x6d, 0x8f, 0xeb, 0xa7, 0xfb, 0x65, 0x3d, 0x1f, 0x68, + 0xc9, 0xe3, 0x66, 0xeb, 0xed, 0x8d, 0xe6, 0xd5, 0x5e, 0xa6, 0x75, 0x0e, 0x1b, 0x13, 0x7c, 0x28, 0xb7, 0x8a, 0x82, + 0xf1, 0x26, 0x20, 0xf9, 0x03, 0x31, 0xaf, 0xde, 0x36, 0xbb, 0xbe, 0xfc, 0x58, 0xac, 0x59, 0x5e, 0x61, 0x01, 0x96, + 0x35, 0x1a, 0x9a, 0xb3, 0x01, 0x67, 0x49, 0x7a, 0xaf, 0xae, 0xce, 0x9c, 0xe0, 0x9c, 0xc9, 0xed, 0x4d, 0xfc, 0xc7, + 0x4f, 0x53, 0x6d, 0xce, 0x32, 0xcb, 0xe1, 0x2f, 0x8e, 0x62, 0x67, 0x71, 0xd8, 0x6e, 0xc0, 0xfa, 0xaa, 0xe3, 0x2d, + 0xaa, 0xca, 0x56, 0xe7, 0x62, 0x26, 0x4b, 0x44, 0xe5, 0x76, 0xd2, 0xe1, 0x40, 0x37, 0x73, 0x6b, 0x1f, 0xf8, 0xdf, + 0x63, 0x17, 0x2a, 0x9d, 0xc2, 0x3f, 0x97, 0x47, 0x05, 0x17, 0x72, 0x9b, 0x6c, 0x2e, 0xb9, 0x91, 0xee, 0x58, 0x9f, + 0xbc, 0xb1, 0x33, 0x13, 0x46, 0x33, 0x11, 0x56, 0xd8, 0x0f, 0x47, 0xc0, 0x3d, 0x2e, 0x18, 0x7b, 0x2e, 0xfc, 0xd6, + 0xc5, 0x96, 0xbd, 0x77, 0x7d, 0x36, 0xf9, 0x28, 0x64, 0x01, 0xfb, 0x0d, 0x81, 0x1d, 0x68, 0xdc, 0x1c, 0x47, 0x3b, + 0x24, 0xeb, 0x08, 0xe6, 0xa2, 0x5b, 0xc9, 0x56, 0x06, 0xbf, 0x55, 0xb8, 0x9f, 0xdc, 0x05, 0x20, 0x69, 0xf5, 0xee, + 0xc7, 0x5e, 0xdf, 0x7f, 0xd5, 0xcf, 0x5b, 0xbd, 0xca, 0xd8, 0x3e, 0x19, 0xf8, 0x48, 0x83, 0xae, 0x77, 0xc3, 0xcb, + 0x95, 0x6a, 0xa2, 0xcd, 0xb8, 0x59, 0x5e, 0xc9, 0xe8, 0x0d, 0xe9, 0xda, 0xee, 0x3c, 0x54, 0x27, 0x37, 0x5e, 0xb6, + 0x4c, 0x06, 0x78, 0x55, 0x67, 0x33, 0xf9, 0x05, 0x62, 0x7d, 0x7d, 0xd3, 0xc5, 0x16, 0x9a, 0xd9, 0x23, 0xd4, 0x09, + 0x7f, 0xbd, 0x8c, 0xa2, 0x52, 0x24, 0x7a, 0x69, 0x76, 0xf2, 0x78, 0xde, 0x1b, 0x6f, 0x0c, 0x59, 0x4e, 0xa6, 0x87, + 0x20, 0xd1, 0x47, 0xf4, 0x92, 0xc5, 0xf5, 0x0e, 0x83, 0xcf, 0x73, 0x94, 0x66, 0xd9, 0xb0, 0xf2, 0x45, 0xfc, 0x8c, + 0x8e, 0x25, 0x1b, 0xf9, 0xb8, 0x85, 0xad, 0x64, 0xd9, 0xe6, 0x7e, 0xd7, 0x3b, 0x5b, 0xd5, 0xcb, 0x37, 0x1b, 0xcb, + 0xee, 0x58, 0x79, 0x7e, 0x51, 0xa9, 0x59, 0x0a, 0x3b, 0x7c, 0x8e, 0x47, 0x6b, 0xc1, 0x92, 0xdb, 0xbc, 0x1c, 0x21, + 0xbd, 0x97, 0xa4, 0xbf, 0x76, 0xb2, 0x14, 0xc9, 0x87, 0xb2, 0xac, 0xf2, 0x1f, 0x92, 0x24, 0x62, 0x55, 0x14, 0xa6, + 0x1c, 0x64, 0x9e, 0x7c, 0x28, 0x37, 0xbd, 0x78, 0xe7, 0xab, 0xc2, 0x4f, 0x75, 0xd8, 0x4b, 0xeb, 0x37, 0x20, 0x0a, + 0x66, 0x01, 0x7c, 0x21, 0xee, 0x45, 0xb3, 0xeb, 0x99, 0x3c, 0x06, 0x09, 0xf4, 0x39, 0xf0, 0x0f, 0x3e, 0xfa, 0x01, + 0x05, 0x9f, 0x8b, 0x0e, 0x8c, 0x00, 0x00, 0x72, 0xc7, 0xa1, 0xec, 0xf9, 0xbd, 0x29, 0x57, 0x28, 0xab, 0xa1, 0x5a, + 0x9b, 0xfc, 0x49, 0x73, 0x1d, 0x09, 0xce, 0x7b, 0xa4, 0xc8, 0x3f, 0xf1, 0x7a, 0x36, 0x69, 0x1a, 0x62, 0x17, 0x15, + 0x0a, 0x4c, 0x54, 0xe9, 0x24, 0x4f, 0x95, 0x2c, 0xb5, 0x2a, 0x59, 0xa3, 0x07, 0x1f, 0x76, 0xeb, 0xb5, 0x79, 0x55, + 0x1e, 0x92, 0x9f, 0x25, 0x10, 0xa6, 0x7f, 0xa5, 0x65, 0xb9, 0xbd, 0xa1, 0x4a, 0xe5, 0x20, 0x0f, 0xc1, 0x23, 0xab, + 0xfc, 0xba, 0x7c, 0xe2, 0xc1, 0x8d, 0x28, 0x7f, 0xa5, 0xcf, 0x21, 0xa8, 0x04, 0xfe, 0x5b, 0x36, 0x9b, 0xbe, 0xf2, + 0xf9, 0xf6, 0xc7, 0x73, 0x41, 0x04, 0x4f, 0x97, 0xec, 0x0d, 0xe6, 0x6a, 0x6f, 0x50, 0x9a, 0xac, 0x7b, 0x67, 0x43, + 0xcc, 0x05, 0xcb, 0x1f, 0xd1, 0xe6, 0xdf, 0x27, 0xdf, 0xec, 0x85, 0x96, 0x40, 0xd3, 0x7a, 0x0d, 0xd0, 0x32, 0xcf, + 0x3b, 0x32, 0xd8, 0x23, 0xef, 0x52, 0x1e, 0x56, 0x1c, 0x57, 0xbd, 0xe4, 0xef, 0xe1, 0x2d, 0xdc, 0x69, 0x1b, 0x29, + 0x12, 0xf5, 0x3a, 0xab, 0x14, 0x81, 0xf3, 0x1e, 0xbe, 0x9a, 0xf3, 0x74, 0xaa, 0x21, 0xf1, 0x4b, 0xc7, 0xdc, 0x86, + 0x0a, 0xeb, 0x65, 0x31, 0xbb, 0xbf, 0x7b, 0x83, 0xdc, 0x12, 0x9b, 0xdf, 0x3b, 0x6f, 0x01, 0x48, 0xb5, 0xfa, 0x94, + 0xb2, 0x23, 0xff, 0x51, 0xaa, 0x1d, 0xf0, 0x2a, 0x1f, 0xa8, 0xa0, 0x1a, 0x33, 0xa4, 0xb4, 0xe2, 0xaa, 0x13, 0x49, + 0xd0, 0xdb, 0xa2, 0x64, 0xb0, 0x91, 0x29, 0xec, 0x43, 0x5e, 0x94, 0xe8, 0xfb, 0x52, 0xc9, 0x72, 0x0b, 0xaf, 0x1c, + 0xcb, 0x5a, 0xee, 0x1b, 0x25, 0x7e, 0x98, 0x20, 0x3f, 0x0d, 0x2f, 0x32, 0xf2, 0xe4, 0x6e, 0x71, 0x24, 0xf0, 0xf1, + 0x49, 0x26, 0x82, 0x7d, 0x03, 0x79, 0x92, 0x5c, 0x3c, 0x89, 0x44, 0x65, 0x62, 0xbb, 0xe4, 0xe8, 0xe8, 0xde, 0x24, + 0xa9, 0xd7, 0xd2, 0xe8, 0x50, 0xe8, 0xb8, 0x8d, 0x42, 0x6d, 0x1d, 0xcf, 0xd9, 0x94, 0x8d, 0x47, 0x77, 0xc9, 0x76, + 0x11, 0xb7, 0x87, 0xd2, 0x08, 0x95, 0xd4, 0x26, 0xe8, 0x58, 0x9a, 0x06, 0x91, 0xc7, 0x03, 0x5b, 0x84, 0x88, 0x3e, + 0x9b, 0x4a, 0x67, 0x39, 0xc4, 0x6a, 0x3b, 0x1f, 0x58, 0x8e, 0x2d, 0x87, 0x2c, 0x09, 0x28, 0x9a, 0x95, 0x22, 0xe1, + 0x60, 0xe0, 0x38, 0x9a, 0xa3, 0x4a, 0x81, 0x31, 0x73, 0x35, 0x87, 0x9d, 0xaf, 0x33, 0x72, 0x2c, 0x8d, 0x34, 0x1b, + 0xbe, 0x2e, 0x45, 0x77, 0x6b, 0xeb, 0x63, 0x6d, 0x44, 0x32, 0xb2, 0xc9, 0x9e, 0xcb, 0xdc, 0x13, 0x56, 0xe9, 0xa9, + 0xdc, 0x8d, 0x95, 0x94, 0x15, 0xe7, 0xf9, 0x64, 0xb4, 0xdb, 0x90, 0x45, 0xab, 0x06, 0xab, 0xf1, 0x25, 0xd3, 0xee, + 0xb3, 0x2f, 0xb5, 0x0d, 0xda, 0x6a, 0x52, 0x17, 0x68, 0xce, 0xc3, 0xba, 0xc2, 0xdf, 0xc8, 0x13, 0x38, 0x93, 0x9e, + 0xbd, 0xea, 0x2e, 0x3f, 0xaa, 0x51, 0xda, 0xee, 0x2d, 0x5c, 0x31, 0x8f, 0xb9, 0x0a, 0xc1, 0xae, 0xd5, 0x44, 0x4f, + 0x66, 0x90, 0xc3, 0xf7, 0x04, 0x5c, 0x8e, 0xfc, 0x66, 0x80, 0xed, 0xd9, 0x38, 0x97, 0xb4, 0x53, 0xde, 0x27, 0x2d, + 0x45, 0x7e, 0xc6, 0x4d, 0xd6, 0x9c, 0x28, 0xff, 0x97, 0x42, 0xac, 0xb2, 0xe0, 0x0b, 0xeb, 0x23, 0xeb, 0xef, 0xd1, + 0xa9, 0x4e, 0x79, 0xeb, 0xd5, 0x8c, 0x7e, 0x2b, 0x2a, 0x4c, 0xd8, 0x3b, 0xa0, 0xc1, 0x64, 0xc7, 0x5a, 0x0d, 0xed, + 0x6e, 0xd9, 0xd1, 0xa2, 0x38, 0x6d, 0xcf, 0x68, 0x55, 0x7b, 0x21, 0x23, 0x1e, 0x7e, 0x6e, 0x34, 0x12, 0x8b, 0xa4, + 0x58, 0x40, 0xe7, 0x2b, 0xea, 0xfd, 0xb5, 0x9c, 0xd3, 0x93, 0xd6, 0x64, 0xf0, 0xa8, 0x33, 0xa7, 0xce, 0x55, 0x9f, + 0xbd, 0xde, 0xd5, 0xa1, 0xf2, 0x27, 0xe2, 0xfa, 0x13, 0x34, 0x55, 0x55, 0x73, 0xd7, 0x4a, 0x90, 0x9a, 0xb2, 0x6c, + 0xe2, 0xc8, 0xa7, 0xc9, 0xf7, 0xf9, 0x4c, 0x87, 0xec, 0xc3, 0x75, 0xa8, 0x8a, 0x17, 0x23, 0xb1, 0x05, 0x21, 0xb9, + 0x70, 0x82, 0x65, 0x51, 0xdf, 0x63, 0x29, 0x8a, 0xa3, 0x84, 0x92, 0xe1, 0x05, 0x8a, 0xc0, 0x51, 0x0b, 0x0c, 0x94, + 0xe4, 0x44, 0x1d, 0x1a, 0xc9, 0xce, 0xd3, 0xc1, 0x8b, 0x4f, 0x6c, 0xf2, 0x2d, 0xa1, 0x43, 0x3a, 0x43, 0x79, 0x05, + 0xdf, 0x8b, 0x77, 0x45, 0x9e, 0x30, 0xd5, 0xd2, 0x31, 0xcf, 0xbd, 0x66, 0xfa, 0xd8, 0x35, 0x78, 0x21, 0xba, 0x0e, + 0x97, 0x67, 0x48, 0x19, 0xab, 0x48, 0xd5, 0x34, 0xed, 0x87, 0x77, 0x87, 0x28, 0x49, 0x55, 0xb6, 0xdb, 0x7b, 0xa7, + 0x2d, 0x44, 0x09, 0xa4, 0xc9, 0xba, 0x0d, 0x8c, 0x64, 0x7a, 0xce, 0xb1, 0x2a, 0x45, 0x31, 0x86, 0x19, 0x82, 0x5c, + 0xe8, 0xb0, 0x15, 0x92, 0x4a, 0x3f, 0xca, 0x22, 0xe8, 0x26, 0x9d, 0xf1, 0x60, 0x96, 0x81, 0x51, 0xe1, 0xf8, 0xa4, + 0xde, 0x85, 0x77, 0x6d, 0x73, 0x72, 0x68, 0x15, 0x69, 0x02, 0x75, 0x2c, 0x90, 0x1e, 0xe5, 0x6f, 0xbe, 0x7b, 0x8c, + 0x6b, 0xd3, 0xaf, 0x8b, 0x55, 0x21, 0x33, 0x76, 0x02, 0x07, 0x90, 0x69, 0xbb, 0xf9, 0x9d, 0x7d, 0xa3, 0x24, 0x05, + 0x23, 0xad, 0xe7, 0x9e, 0x19, 0x5c, 0x8c, 0xc9, 0x42, 0xb4, 0xad, 0x76, 0xd3, 0x47, 0x07, 0x6d, 0x64, 0xe5, 0x35, + 0x00, 0xab, 0x24, 0x2d, 0x39, 0x1b, 0xc0, 0xc2, 0x6a, 0xc7, 0x36, 0x4c, 0x2f, 0x0d, 0x80, 0x4d, 0xbf, 0x05, 0x58, + 0xc0, 0x0b, 0xa2, 0xfd, 0xcc, 0xbc, 0xd2, 0x2c, 0xc2, 0x03, 0xef, 0x13, 0x80, 0x0d, 0xb1, 0x52, 0x51, 0x2d, 0x16, + 0x0b, 0xaf, 0x14, 0x0b, 0x5a, 0xd3, 0xcc, 0x96, 0x4e, 0xc0, 0xfa, 0x72, 0x47, 0xa1, 0x3a, 0xe5, 0xaf, 0xa8, 0xc4, + 0x9a, 0x43, 0x55, 0xf1, 0xd1, 0xfd, 0x66, 0x7d, 0x9a, 0x62, 0x91, 0xda, 0xa7, 0xd6, 0x93, 0x0c, 0xf0, 0x76, 0x8b, + 0xe7, 0xc0, 0x4b, 0xcb, 0xa2, 0xf7, 0xbd, 0x9d, 0xb5, 0x64, 0x51, 0xdd, 0x72, 0x7c, 0xd5, 0xca, 0xe4, 0x74, 0x25, + 0x97, 0x82, 0x32, 0x14, 0xf9, 0x9e, 0x27, 0x49, 0x21, 0xae, 0x4f, 0x1f, 0xcc, 0x7c, 0x84, 0x71, 0x65, 0xc6, 0x8b, + 0x6f, 0xc5, 0xc3, 0x8c, 0x27, 0xa5, 0x48, 0x6a, 0x79, 0x51, 0x01, 0xa9, 0xc6, 0x28, 0xbe, 0x20, 0x43, 0xcf, 0xf9, + 0x7a, 0xd4, 0xa7, 0xb8, 0x73, 0xb6, 0x24, 0x0e, 0xa2, 0x70, 0x98, 0x3e, 0x2b, 0x54, 0x08, 0xfe, 0x3b, 0x20, 0x26, + 0x19, 0x82, 0x00, 0x73, 0x22, 0xa1, 0x0e, 0xb2, 0xf0, 0x84, 0x67, 0x7b, 0xc9, 0x68, 0x25, 0xa3, 0x13, 0xa9, 0x32, + 0x11, 0x51, 0xf9, 0xed, 0xca, 0x26, 0x41, 0xb3, 0xec, 0xa5, 0x28, 0xdc, 0x88, 0x25, 0x11, 0x5c, 0x2b, 0x83, 0x9b, + 0x7e, 0x44, 0xa9, 0xee, 0x96, 0x86, 0xeb, 0x8c, 0x65, 0x72, 0xe1, 0x1b, 0x6f, 0xe8, 0xfa, 0xd2, 0xf5, 0x67, 0xe4, + 0xb6, 0xa8, 0xf0, 0xc9, 0x47, 0xf2, 0xc0, 0xb9, 0xbe, 0x9a, 0x66, 0x3a, 0xc7, 0xbc, 0x8b, 0x50, 0x5c, 0x63, 0xb2, + 0xcd, 0x7e, 0xdb, 0x2c, 0xc1, 0xeb, 0xfc, 0x59, 0xb2, 0x9c, 0xa6, 0x02, 0x76, 0x31, 0xf1, 0x8b, 0xa0, 0x44, 0x1b, + 0xae, 0x7a, 0xff, 0xde, 0xd6, 0x7a, 0xf7, 0xd9, 0x07, 0x1c, 0x16, 0xe5, 0x6f, 0xd4, 0xf6, 0x0c, 0x28, 0xa8, 0xe4, + 0xb9, 0xab, 0xd6, 0x80, 0xb1, 0x1d, 0xc3, 0x0f, 0x51, 0x1f, 0xed, 0x1a, 0xa0, 0xae, 0xd9, 0xca, 0x29, 0x06, 0x63, + 0x00, 0x1d, 0xdd, 0xfa, 0xd4, 0x20, 0x75, 0x58, 0xd8, 0x94, 0xd6, 0xdb, 0x58, 0xbc, 0xd0, 0x22, 0xe6, 0x72, 0x95, + 0x2c, 0xad, 0xf9, 0x1a, 0xfe, 0x37, 0xb8, 0xa6, 0xe0, 0x77, 0x94, 0xbf, 0x92, 0x78, 0xd7, 0x9d, 0xd3, 0x0a, 0x89, + 0x82, 0x79, 0x2e, 0xbc, 0x22, 0xc2, 0x4a, 0x9f, 0x10, 0x73, 0xcc, 0x75, 0x99, 0x93, 0x7d, 0xe1, 0xd0, 0x1a, 0xa9, + 0x43, 0xc0, 0xa5, 0xfb, 0x1e, 0x4f, 0x2f, 0xf8, 0x12, 0x5d, 0x1d, 0xdf, 0xcb, 0x3c, 0x9a, 0x01, 0xab, 0xba, 0x6f, + 0x31, 0x09, 0x53, 0x51, 0x46, 0x09, 0x41, 0xdc, 0x54, 0x22, 0x0b, 0x43, 0xcf, 0x1a, 0x57, 0x1f, 0x3b, 0xad, 0xa7, + 0x0c, 0x00, 0x28, 0x25, 0x09, 0xdd, 0x33, 0x94, 0x31, 0xa3, 0x97, 0x56, 0x81, 0x72, 0xcb, 0xd5, 0xc1, 0xcb, 0xce, + 0x3d, 0x86, 0x81, 0x9d, 0xd9, 0x5a, 0x67, 0x1a, 0x07, 0x22, 0xcb, 0x40, 0x80, 0x38, 0xc8, 0xb7, 0xa9, 0xd2, 0x58, + 0x74, 0x03, 0xd4, 0xb5, 0xa8, 0x0f, 0xd2, 0x8e, 0x2c, 0xc4, 0x19, 0x26, 0x3a, 0x06, 0xf6, 0xa7, 0x97, 0x68, 0xa4, + 0xa4, 0x42, 0xe0, 0x95, 0x31, 0xf3, 0x40, 0x22, 0x7b, 0xa0, 0x1d, 0x94, 0x0d, 0x80, 0x24, 0x67, 0x8e, 0x2b, 0x05, + 0x69, 0x2d, 0x23, 0x96, 0xd0, 0xff, 0x13, 0x2b, 0x8d, 0x32, 0x01, 0xf9, 0xc8, 0xa1, 0x4d, 0x49, 0xe3, 0x79, 0x78, + 0x2d, 0x1c, 0x48, 0x3e, 0x4c, 0x7f, 0x9c, 0x05, 0x8d, 0xc8, 0x94, 0xd3, 0xb9, 0x15, 0x6c, 0xe3, 0x8b, 0x3b, 0x4f, + 0x23, 0x51, 0x61, 0xfa, 0xcc, 0x77, 0x96, 0xb7, 0x93, 0x55, 0x84, 0xe5, 0x8f, 0xb9, 0xec, 0xa7, 0xe8, 0x7f, 0xad, + 0xa2, 0x24, 0xc9, 0x06, 0x5f, 0x2a, 0x99, 0x8e, 0xdb, 0xf3, 0x6b, 0xad, 0x8d, 0x96, 0xee, 0x01, 0xce, 0x78, 0x0f, + 0xaa, 0x3b, 0x12, 0x7a, 0xc8, 0x69, 0xcd, 0x53, 0x87, 0xa8, 0xaa, 0xa0, 0x84, 0x44, 0x63, 0x5c, 0xa4, 0xd6, 0x44, + 0xea, 0x9b, 0xc5, 0xd3, 0x00, 0x92, 0x69, 0x0c, 0x2a, 0xaa, 0xdc, 0x57, 0xcf, 0x6c, 0x5e, 0x4c, 0x4f, 0xa4, 0xc9, + 0x74, 0x41, 0x2e, 0x3f, 0xc5, 0xe8, 0x66, 0x4a, 0xc9, 0xb2, 0x58, 0x86, 0xc3, 0x1e, 0x23, 0xcc, 0x01, 0x43, 0x44, + 0xa5, 0xc2, 0x78, 0xc3, 0xa4, 0xbc, 0x9f, 0x94, 0xfc, 0x69, 0x8a, 0xf3, 0x0b, 0x61, 0xf5, 0x61, 0x5b, 0x07, 0xb8, + 0x3a, 0x07, 0xe5, 0x08, 0xb7, 0x96, 0x07, 0xd8, 0xd8, 0x98, 0x47, 0x7b, 0x39, 0x55, 0x25, 0x22, 0xd3, 0xac, 0x3b, + 0x58, 0x52, 0xde, 0xa4, 0xef, 0x0c, 0x99, 0xb0, 0x75, 0x33, 0x14, 0x41, 0x6e, 0x3c, 0xc9, 0xae, 0xb1, 0xd5, 0x07, + 0x81, 0xb3, 0x7a, 0x44, 0x5e, 0xc6, 0xa3, 0xaa, 0xce, 0xaa, 0xed, 0x94, 0xfc, 0x34, 0xbc, 0x4f, 0xae, 0x3b, 0xc9, + 0x09, 0x95, 0xd5, 0x24, 0x2a, 0x0a, 0x8a, 0xc2, 0xf3, 0x24, 0x20, 0x82, 0xe2, 0x8c, 0xfa, 0xa1, 0x8a, 0xfa, 0xa6, + 0xc8, 0xfe, 0x71, 0xea, 0xd5, 0xbe, 0xe5, 0x25, 0x80, 0x24, 0x3f, 0x93, 0x49, 0x77, 0x8c, 0x7b, 0x67, 0x5d, 0x1e, + 0x3e, 0x4a, 0x14, 0xe6, 0x3e, 0x14, 0x57, 0x31, 0x49, 0x51, 0x2c, 0x01, 0xf7, 0xca, 0x65, 0x2f, 0x1e, 0x49, 0x3a, + 0x41, 0x66, 0xa8, 0x5c, 0x1b, 0xef, 0x9b, 0x7a, 0xa4, 0xea, 0x49, 0x96, 0x02, 0x79, 0xe4, 0xee, 0x77, 0x46, 0x50, + 0xf2, 0xfc, 0xb7, 0xec, 0x8a, 0xd7, 0x49, 0x79, 0x86, 0x36, 0x1b, 0x22, 0x7a, 0x5d, 0x8e, 0x36, 0x05, 0xcc, 0x31, + 0x23, 0x02, 0xfd, 0x73, 0xb0, 0x0a, 0xf9, 0xb2, 0xe3, 0x14, 0xdb, 0x54, 0x01, 0x4a, 0xb9, 0xf7, 0xfd, 0x55, 0x1e, + 0x08, 0xfb, 0x33, 0x92, 0x9c, 0x49, 0x46, 0x44, 0x50, 0xb6, 0xc0, 0x23, 0xb0, 0x2f, 0xa4, 0x8b, 0x13, 0xb5, 0x0a, + 0xfb, 0x82, 0x9a, 0xb3, 0x67, 0xa9, 0x88, 0xea, 0x14, 0x7d, 0xfc, 0xca, 0xb8, 0x60, 0x2e, 0xab, 0x91, 0xb6, 0x22, + 0x5a, 0x9e, 0xaa, 0x04, 0x04, 0xb5, 0x10, 0x0b, 0xb1, 0xa9, 0x80, 0x60, 0x7c, 0xaf, 0xa7, 0x27, 0x18, 0x31, 0x0b, + 0xc5, 0x8b, 0xcc, 0xe5, 0x44, 0xbb, 0xfc, 0x87, 0x9b, 0x30, 0x9d, 0x33, 0x86, 0x34, 0x22, 0xa9, 0x47, 0xd6, 0xef, + 0x5f, 0x2f, 0x2f, 0x54, 0x65, 0x13, 0x49, 0x65, 0x23, 0xee, 0xe6, 0x73, 0x9d, 0x1b, 0xd3, 0x44, 0x70, 0xc5, 0x92, + 0xd9, 0x62, 0xe3, 0xe9, 0xfc, 0xc3, 0x95, 0x59, 0x48, 0xd7, 0x44, 0x39, 0x92, 0xc8, 0x4f, 0x0a, 0xc1, 0x43, 0x8d, + 0xf2, 0x42, 0x18, 0x91, 0xfa, 0x6f, 0x86, 0xdc, 0x75, 0x29, 0xda, 0xd5, 0x46, 0x75, 0xd9, 0x02, 0xd8, 0xd2, 0xd7, + 0x30, 0x32, 0x14, 0x42, 0x47, 0x0c, 0x92, 0xdc, 0xa5, 0x3e, 0x2a, 0x19, 0xc8, 0xa2, 0x2b, 0xcc, 0x40, 0x99, 0x7b, + 0xe8, 0xe4, 0x8d, 0x93, 0x28, 0x01, 0xb9, 0x9f, 0x99, 0x4f, 0xea, 0xec, 0x24, 0xf2, 0x62, 0x2d, 0x05, 0x74, 0xa4, + 0xba, 0x4e, 0x25, 0x56, 0x59, 0xad, 0x84, 0x9e, 0x08, 0x76, 0x17, 0xcd, 0xe0, 0x55, 0x9b, 0xe7, 0xe9, 0xb1, 0xe6, + 0x9f, 0xc7, 0x57, 0x94, 0xb7, 0x35, 0x91, 0x16, 0x74, 0x22, 0xb4, 0xfa, 0xc0, 0x7d, 0xdd, 0x5c, 0xd9, 0x1a, 0xe4, + 0x65, 0x01, 0xd0, 0x72, 0xce, 0x72, 0x7a, 0x12, 0xca, 0xba, 0x79, 0x5e, 0x26, 0x99, 0x7b, 0x15, 0x07, 0x5b, 0x40, + 0x73, 0x01, 0x37, 0xc1, 0x67, 0x1f, 0x27, 0xa4, 0x7e, 0xa8, 0x3c, 0x56, 0x36, 0xdf, 0xd6, 0x60, 0xee, 0xdb, 0xc4, + 0xe9, 0xb0, 0xd9, 0x24, 0x22, 0x26, 0x73, 0x37, 0xb6, 0xde, 0x08, 0x67, 0x2d, 0x54, 0xed, 0x11, 0xf3, 0x84, 0x00, + 0x53, 0xd5, 0x20, 0x7c, 0xda, 0xc7, 0x49, 0x4c, 0x6f, 0x11, 0x15, 0xa0, 0x5c, 0x62, 0x52, 0xaf, 0xdc, 0xa5, 0xa5, + 0xd6, 0xbd, 0x4f, 0x17, 0x58, 0x57, 0xba, 0x78, 0xbc, 0xd3, 0x7d, 0xe0, 0x00, 0x70, 0x3f, 0x83, 0xaa, 0x55, 0x5e, + 0xaa, 0xea, 0x0b, 0x6a, 0x69, 0x82, 0x94, 0x04, 0xbc, 0x55, 0x49, 0xef, 0xe7, 0x99, 0x06, 0x82, 0xe6, 0x6b, 0x64, + 0x75, 0xe4, 0x0b, 0x91, 0xc8, 0x43, 0xcf, 0x4b, 0x7c, 0xbc, 0x08, 0xcf, 0x09, 0x1e, 0xbf, 0x8c, 0xad, 0x0b, 0x3a, + 0x65, 0xfe, 0x20, 0x81, 0xe5, 0x40, 0xed, 0xda, 0xe5, 0xeb, 0x38, 0x11, 0xec, 0x14, 0x05, 0xea, 0x29, 0x2a, 0x40, + 0x83, 0x40, 0xd1, 0x48, 0x0b, 0xe8, 0x24, 0xf9, 0x39, 0xa6, 0x05, 0x84, 0xd4, 0x29, 0x10, 0x31, 0xdf, 0x0e, 0xcb, + 0x11, 0xdc, 0x95, 0x22, 0x27, 0x9e, 0x38, 0x37, 0x6b, 0xe5, 0xcb, 0x7d, 0x88, 0xaa, 0x73, 0x7f, 0x7c, 0x83, 0x3b, + 0x70, 0x15, 0xdb, 0x8d, 0xe3, 0x1f, 0x71, 0xbf, 0x49, 0x16, 0x72, 0x0e, 0x44, 0x8a, 0xbc, 0x1c, 0x11, 0x22, 0x13, + 0x87, 0x3a, 0xdc, 0x84, 0x90, 0x8e, 0x2f, 0xa0, 0x3f, 0x8e, 0x98, 0xc6, 0x56, 0x9d, 0x26, 0x20, 0xe7, 0x3c, 0xbe, + 0x3d, 0x9d, 0xde, 0xba, 0xa8, 0x1e, 0x44, 0xdb, 0x22, 0xe2, 0x87, 0xb6, 0xa8, 0x51, 0xa8, 0x3c, 0x9c, 0x5a, 0x5f, + 0x53, 0xc3, 0x31, 0xc4, 0xe1, 0xdf, 0x06, 0x48, 0x00, 0x85, 0xdd, 0x26, 0xd7, 0x5c, 0xd0, 0xe9, 0x9d, 0xa4, 0x23, + 0xb4, 0xd6, 0xf4, 0x53, 0xb9, 0x6a, 0xd6, 0xc1, 0xca, 0xb4, 0xd3, 0xfb, 0x6c, 0xe3, 0xb6, 0x38, 0x01, 0x41, 0xb4, + 0xd2, 0xeb, 0x9b, 0x30, 0x61, 0x89, 0x31, 0x06, 0xde, 0x17, 0x62, 0xce, 0x53, 0x98, 0x49, 0xcc, 0xc7, 0x70, 0xb4, + 0x3a, 0x8b, 0x77, 0x6e, 0xd1, 0xa5, 0xbd, 0xd1, 0x9b, 0x36, 0x92, 0xa9, 0x84, 0x8e, 0x05, 0xf0, 0xc7, 0xe9, 0xa8, + 0x1d, 0x71, 0x07, 0x04, 0xd8, 0xca, 0x12, 0xe3, 0xd2, 0x2d, 0xd8, 0xaa, 0x6c, 0xf9, 0xb4, 0x71, 0xae, 0xdc, 0xcf, + 0xd6, 0x2e, 0x74, 0x44, 0x70, 0x58, 0x97, 0x34, 0x07, 0xe6, 0x63, 0xc1, 0x5c, 0x8a, 0x8b, 0xd5, 0x4e, 0x01, 0x12, + 0xb4, 0x92, 0x3c, 0x5c, 0x66, 0x48, 0x7a, 0x7c, 0xa2, 0x2e, 0x12, 0x72, 0xc6, 0x8d, 0xb6, 0x06, 0xec, 0xe0, 0xdd, + 0x5e, 0x8f, 0xb4, 0xee, 0xbc, 0x45, 0xde, 0x8b, 0xe2, 0x05, 0xa4, 0x9a, 0x02, 0x71, 0x65, 0x83, 0x20, 0xed, 0x3a, + 0x25, 0xac, 0xbf, 0x19, 0x2c, 0x8d, 0xdb, 0x77, 0x6d, 0x4a, 0x0f, 0x7a, 0x76, 0xa6, 0x87, 0x5c, 0xf8, 0xb3, 0xa2, + 0x6f, 0x5f, 0x79, 0xcb, 0x36, 0xec, 0xa7, 0xa5, 0x00, 0xb2, 0xba, 0xb8, 0x1b, 0xe7, 0xbc, 0x60, 0x8b, 0xa5, 0xc9, + 0xe9, 0xab, 0x65, 0x85, 0x9a, 0xc0, 0x1e, 0x7c, 0xa0, 0x65, 0xa4, 0x52, 0x5f, 0x29, 0x29, 0x5a, 0x1e, 0x1a, 0x93, + 0x6c, 0x6d, 0x6a, 0x85, 0xb8, 0xaa, 0x06, 0xab, 0xea, 0xe1, 0x12, 0x4b, 0x4b, 0xdb, 0x52, 0x0b, 0xcd, 0x75, 0xef, + 0x05, 0x98, 0x7c, 0x8f, 0x1e, 0xb1, 0xbc, 0x00, 0xba, 0xfb, 0x85, 0x7c, 0x19, 0x87, 0x41, 0x5a, 0x54, 0x41, 0x00, + 0xe9, 0x75, 0x1d, 0xc3, 0xa6, 0x61, 0x8d, 0x89, 0x0e, 0x8b, 0x3e, 0x8d, 0x40, 0x45, 0xa8, 0x81, 0x21, 0xd8, 0x42, + 0xae, 0x4c, 0xc5, 0xd2, 0xa9, 0x97, 0xc9, 0xe2, 0xd2, 0xe7, 0x5e, 0x6c, 0x6b, 0xd2, 0x15, 0xb3, 0x54, 0x41, 0x5c, + 0x1a, 0x75, 0xbd, 0xd1, 0x37, 0x6a, 0x79, 0xd0, 0x35, 0xde, 0xe3, 0x26, 0x19, 0xd6, 0xa6, 0x72, 0x7d, 0x94, 0x6c, + 0xb7, 0xff, 0x59, 0xb9, 0x44, 0xb5, 0xe4, 0x2c, 0xad, 0xb1, 0xea, 0x61, 0x8b, 0x02, 0x5c, 0xbe, 0xe3, 0x4e, 0xc6, + 0x00, 0x59, 0x8e, 0xb4, 0x61, 0x6e, 0x1d, 0xce, 0x65, 0x1b, 0x68, 0xfb, 0xcd, 0xa7, 0x92, 0x60, 0xeb, 0x57, 0x4f, + 0xa7, 0xb1, 0x4d, 0x91, 0xc3, 0x28, 0x70, 0x14, 0x9e, 0xbb, 0xe7, 0xbc, 0x5a, 0x29, 0xe3, 0x12, 0xdb, 0xed, 0x73, + 0xd3, 0x4f, 0x5e, 0xd9, 0x86, 0xf5, 0x14, 0x5f, 0x8f, 0x91, 0x8d, 0xbd, 0xe7, 0xc9, 0x7a, 0x32, 0x16, 0x77, 0x0c, + 0x80, 0x8b, 0x92, 0x62, 0xb8, 0x5b, 0xc1, 0xc5, 0x83, 0x5f, 0xf8, 0x7c, 0x2a, 0xa7, 0x9b, 0x34, 0xee, 0xcd, 0xbf, + 0xed, 0xed, 0x85, 0x07, 0xcd, 0x24, 0x7d, 0x99, 0xa5, 0xcb, 0x2a, 0xb9, 0x16, 0xc8, 0xb5, 0x1b, 0x9e, 0x8b, 0x72, + 0xbd, 0x69, 0x6b, 0x23, 0x4c, 0x50, 0xbc, 0x1c, 0xf8, 0xdb, 0xbb, 0xf8, 0xed, 0xb9, 0xec, 0xf9, 0xb9, 0xb7, 0x68, + 0x08, 0x31, 0xdf, 0xbc, 0x8f, 0x2a, 0x2b, 0x8e, 0x63, 0xe2, 0x7d, 0x3e, 0x34, 0xde, 0xeb, 0x1a, 0x2d, 0x63, 0xae, + 0xf0, 0xf3, 0x18, 0xaa, 0xda, 0xc7, 0xcd, 0x2d, 0xff, 0x6c, 0x77, 0x9d, 0x95, 0xa2, 0x30, 0x9b, 0xc9, 0xc3, 0xd2, + 0x94, 0xb4, 0x3b, 0x0e, 0x36, 0xdc, 0x3e, 0x2b, 0x00, 0x73, 0x00, 0x2c, 0x8f, 0x74, 0x7d, 0x16, 0x7b, 0x16, 0xca, + 0xb6, 0x8b, 0x38, 0x54, 0x6f, 0x61, 0x57, 0xdc, 0x9c, 0xe5, 0x59, 0xea, 0x6e, 0xe3, 0x0b, 0x03, 0x54, 0x3d, 0xe4, + 0x8e, 0x39, 0x92, 0x96, 0x09, 0xa6, 0x4a, 0x7e, 0xbb, 0x71, 0xdc, 0xcc, 0x19, 0x3b, 0xf1, 0x0c, 0xb3, 0x79, 0xea, + 0x0d, 0x2b, 0x9a, 0xf7, 0xed, 0x2b, 0xf7, 0x34, 0x30, 0xf1, 0xad, 0x8d, 0xca, 0x54, 0xf6, 0x75, 0x00, 0x94, 0x2c, + 0xd1, 0x9f, 0x76, 0x51, 0x5a, 0x57, 0x08, 0xa3, 0xc2, 0xa9, 0xf2, 0x0f, 0xd6, 0x92, 0x56, 0x31, 0x11, 0x8b, 0xa3, + 0x23, 0xcd, 0x19, 0xe0, 0x96, 0x78, 0xcb, 0xa8, 0x03, 0xc5, 0x98, 0xd1, 0xc6, 0x4c, 0xca, 0x6a, 0x8f, 0x66, 0x07, + 0xc2, 0xc8, 0x73, 0x6d, 0x11, 0xe9, 0x28, 0x60, 0xbd, 0x54, 0x70, 0xe0, 0x37, 0xef, 0x55, 0xa0, 0x79, 0xdf, 0xb3, + 0x01, 0xe5, 0x00, 0xee, 0x37, 0x74, 0x94, 0xd4, 0xa6, 0x8d, 0xbf, 0xe4, 0x8a, 0xd1, 0xd5, 0x83, 0x24, 0xd0, 0x36, + 0x63, 0xc0, 0x07, 0x4c, 0xae, 0xa8, 0x42, 0xfa, 0x34, 0x46, 0xde, 0x28, 0x90, 0x9c, 0x63, 0xd3, 0x50, 0x4c, 0x3b, + 0xac, 0x27, 0x91, 0x94, 0x0e, 0x22, 0x64, 0x8a, 0xc5, 0xf4, 0xa0, 0x0e, 0x96, 0x64, 0xa4, 0x75, 0x2a, 0x6f, 0x45, + 0x47, 0xfd, 0x9e, 0x8d, 0xa0, 0x39, 0xb6, 0xac, 0x2a, 0xd4, 0x37, 0xcb, 0x2d, 0x13, 0x95, 0x74, 0xf3, 0x6c, 0x2a, + 0x1f, 0x97, 0x83, 0xc8, 0xa6, 0x69, 0xc7, 0x6f, 0xfb, 0xbc, 0xc7, 0x0c, 0xee, 0x62, 0x84, 0x82, 0xac, 0x6d, 0xc8, + 0x60, 0x8f, 0x3c, 0x5c, 0xd0, 0x2d, 0xfd, 0x40, 0xa1, 0xdf, 0xae, 0x96, 0x00, 0x7e, 0x4a, 0xe0, 0x2b, 0x41, 0x6f, + 0x37, 0xb9, 0x53, 0xbb, 0xce, 0x3d, 0xef, 0x13, 0xd9, 0x0b, 0x27, 0x0f, 0x92, 0x6d, 0x5b, 0xa2, 0x6d, 0xd5, 0x8d, + 0x5b, 0xfe, 0xb1, 0xc3, 0x4f, 0x4a, 0x53, 0x44, 0xad, 0x49, 0xea, 0xb4, 0xb1, 0xdc, 0x12, 0xb5, 0xa3, 0xc1, 0x51, + 0xba, 0x11, 0x5e, 0xb8, 0xdf, 0x86, 0xfd, 0x86, 0x82, 0xb1, 0x1c, 0xbd, 0x72, 0x17, 0x1d, 0x0b, 0x68, 0x1c, 0x29, + 0xe8, 0xd8, 0x1e, 0x47, 0xb5, 0x31, 0x86, 0x72, 0xcc, 0xde, 0x70, 0x0c, 0x65, 0x35, 0x06, 0x6a, 0x63, 0xeb, 0x26, + 0x74, 0x37, 0x9e, 0x88, 0xe4, 0x30, 0xa0, 0x71, 0x40, 0xea, 0x96, 0x19, 0xa9, 0xfc, 0x3a, 0x27, 0x2c, 0x10, 0x83, + 0x3b, 0xf6, 0x78, 0xa1, 0x7d, 0xc1, 0x30, 0xc4, 0x11, 0xe8, 0x16, 0x8f, 0x62, 0xb6, 0xa8, 0x0c, 0xf5, 0xe2, 0xca, + 0x5a, 0x98, 0xc0, 0xda, 0x11, 0xa2, 0x42, 0x7f, 0x6c, 0xf3, 0x5d, 0x3b, 0x14, 0xe4, 0x8a, 0x1f, 0xc6, 0xfe, 0x32, + 0xfd, 0x68, 0xe4, 0xa9, 0xa4, 0xff, 0x22, 0x8c, 0x7e, 0xea, 0x84, 0x95, 0x13, 0x40, 0xf0, 0x27, 0x48, 0x72, 0xdb, + 0x78, 0x3f, 0x4c, 0x69, 0xa6, 0xff, 0xb1, 0xb1, 0xe9, 0x8a, 0xf7, 0x43, 0x3f, 0xcc, 0x1f, 0x3a, 0x51, 0x07, 0xf9, + 0xa7, 0x5f, 0x3c, 0x74, 0x5c, 0x8f, 0xed, 0x63, 0x4c, 0xdd, 0xb1, 0xa5, 0xf9, 0x78, 0xec, 0xda, 0x4b, 0xb6, 0xdb, + 0xf6, 0xe3, 0xf0, 0x64, 0x78, 0x38, 0x64, 0x43, 0x1a, 0xb8, 0xf7, 0xfc, 0x72, 0x8e, 0x3d, 0x4f, 0xde, 0x3d, 0xf4, + 0xe9, 0x81, 0x9c, 0x8b, 0x94, 0x31, 0xd9, 0x2d, 0x9e, 0xb6, 0x5d, 0xa4, 0x34, 0x02, 0xd4, 0xd1, 0x1b, 0xe1, 0x63, + 0x41, 0xd7, 0x24, 0x55, 0xc8, 0xa0, 0x7c, 0x86, 0x49, 0xa3, 0xea, 0x0f, 0xf1, 0x0c, 0x85, 0x88, 0x83, 0xc0, 0x7f, + 0xf9, 0x67, 0x8f, 0xd6, 0x13, 0xa7, 0xd5, 0x69, 0x6d, 0x78, 0xec, 0xf7, 0x65, 0x97, 0xa5, 0x9e, 0x94, 0x51, 0xba, + 0xcd, 0xc4, 0x4b, 0x8c, 0xcc, 0x4d, 0x7e, 0xc8, 0xb1, 0x3d, 0x75, 0x0f, 0x93, 0xff, 0x42, 0x04, 0x45, 0xb8, 0xc7, + 0x02, 0x19, 0xef, 0x21, 0x50, 0x39, 0x15, 0xa2, 0x98, 0x96, 0x8b, 0xb3, 0x05, 0xb0, 0x6b, 0xf4, 0x4b, 0x64, 0x45, + 0x3c, 0x33, 0x9e, 0xdf, 0xb5, 0x36, 0xd7, 0x01, 0xfc, 0x7e, 0x6d, 0xf4, 0x64, 0x46, 0xab, 0x80, 0xac, 0xfb, 0xa0, + 0x0c, 0x2e, 0x09, 0x4f, 0xa5, 0x3d, 0x97, 0xd5, 0x58, 0xd3, 0x7e, 0xa0, 0x57, 0x33, 0xfd, 0x69, 0xfb, 0xac, 0x21, + 0x74, 0x3d, 0x9a, 0x29, 0x05, 0x54, 0xaa, 0x7c, 0x50, 0x66, 0x5f, 0x5f, 0x40, 0x38, 0xa2, 0x55, 0xc8, 0x2f, 0x15, + 0xa7, 0x87, 0xb1, 0x8d, 0x82, 0x20, 0xdf, 0x79, 0x86, 0xc8, 0x0f, 0xc9, 0x13, 0x2a, 0xec, 0xce, 0xfd, 0x02, 0xf4, + 0x45, 0x85, 0xa7, 0xf4, 0xfd, 0x69, 0x8e, 0xdb, 0xd5, 0xbc, 0x8f, 0xef, 0x03, 0x19, 0x25, 0x58, 0x46, 0xba, 0x39, + 0x74, 0xd2, 0xa8, 0x1d, 0x3d, 0xf2, 0x95, 0x48, 0x8e, 0x2e, 0xd0, 0xf4, 0x3d, 0xd6, 0x86, 0x17, 0x49, 0x4a, 0xd0, + 0xa7, 0x72, 0x2d, 0xc9, 0xb0, 0x57, 0x75, 0x60, 0x74, 0x44, 0xde, 0x5e, 0x8a, 0x0d, 0x90, 0x24, 0xd5, 0xd3, 0x12, + 0xa1, 0xfd, 0x50, 0xce, 0x7a, 0x53, 0x7e, 0x89, 0x7b, 0xf1, 0x84, 0x57, 0x46, 0x74, 0xc3, 0x5f, 0x7c, 0x13, 0xe2, + 0x5e, 0x28, 0xee, 0x8b, 0x02, 0x96, 0x25, 0x54, 0x11, 0x41, 0x6f, 0x1a, 0xa8, 0x1c, 0x0c, 0xfd, 0xb1, 0x28, 0xf0, + 0x6c, 0x05, 0x58, 0x96, 0x09, 0x29, 0x03, 0x47, 0x6c, 0x44, 0xff, 0x4a, 0x9a, 0xfa, 0x29, 0xa5, 0xb9, 0x6f, 0x49, + 0xbc, 0xec, 0x17, 0x84, 0x94, 0x37, 0x10, 0x0a, 0x82, 0x96, 0x0a, 0xde, 0x04, 0x29, 0x68, 0x4c, 0x3b, 0xcc, 0x95, + 0x41, 0xd9, 0xe3, 0xb8, 0x01, 0x2e, 0x5f, 0x39, 0xa8, 0x4d, 0xd5, 0xeb, 0x24, 0xb6, 0x2a, 0x6e, 0xf4, 0x9f, 0xe8, + 0xd6, 0xda, 0x0f, 0x07, 0x28, 0x82, 0xb6, 0x28, 0x9b, 0xf4, 0x8a, 0xc6, 0xb3, 0x30, 0x16, 0x96, 0x3d, 0x46, 0x0f, + 0x6a, 0x06, 0x4a, 0x2a, 0xac, 0x36, 0x54, 0x28, 0xe6, 0x53, 0xbb, 0x08, 0xa3, 0xf0, 0x41, 0x53, 0x19, 0x79, 0xf8, + 0xd0, 0x9d, 0x46, 0xef, 0xc6, 0x51, 0x2c, 0x72, 0x43, 0x9b, 0xd7, 0x2c, 0x45, 0xc2, 0xa4, 0x49, 0x3e, 0xbd, 0x6c, + 0xd6, 0xb3, 0x66, 0xd2, 0xb2, 0x15, 0x7a, 0xd5, 0x78, 0x63, 0x20, 0x52, 0xd4, 0x6f, 0xbe, 0x4e, 0x7a, 0xb5, 0x9e, + 0xc3, 0xec, 0x47, 0xc2, 0xf2, 0xa2, 0xe8, 0x7a, 0xa6, 0xdb, 0xbc, 0x6a, 0xa3, 0x3b, 0x73, 0xaa, 0xaf, 0xd4, 0x60, + 0x08, 0xf8, 0x95, 0x73, 0x79, 0x50, 0x26, 0xa8, 0x9c, 0xd8, 0x76, 0x0f, 0x6d, 0x46, 0x40, 0x07, 0xcf, 0xb2, 0xd3, + 0xcc, 0x97, 0xaf, 0x96, 0x49, 0x31, 0xac, 0x77, 0xa9, 0x43, 0x81, 0x97, 0x7b, 0x95, 0xfe, 0x81, 0x46, 0x95, 0x32, + 0xf2, 0x82, 0xa8, 0x3a, 0xd1, 0x5e, 0x70, 0x10, 0xc7, 0x1d, 0xfe, 0x3d, 0xe2, 0x70, 0xc9, 0x3d, 0x87, 0x1d, 0x40, + 0x4e, 0x59, 0x44, 0x3a, 0xca, 0xc7, 0x77, 0x8f, 0xbe, 0x65, 0xcc, 0x31, 0xd2, 0x65, 0xf5, 0x53, 0x11, 0x6d, 0x1f, + 0x51, 0x12, 0xe9, 0x0e, 0x07, 0xfb, 0x14, 0x21, 0xde, 0x6c, 0x8a, 0x41, 0x00, 0x2b, 0x74, 0xbe, 0x44, 0x74, 0x42, + 0x5a, 0xd4, 0x03, 0x0a, 0x87, 0xad, 0x82, 0xcf, 0x72, 0xc1, 0x09, 0x96, 0xfe, 0x10, 0x13, 0xab, 0x52, 0x24, 0x3b, + 0x34, 0xcb, 0xbf, 0x4c, 0x6d, 0xaf, 0x96, 0xa6, 0x51, 0x6d, 0x1e, 0xc1, 0x7d, 0xe3, 0xb2, 0xa4, 0x68, 0x05, 0x76, + 0x97, 0xbd, 0x54, 0xc8, 0xc2, 0x86, 0x6b, 0x2f, 0x79, 0xa6, 0x6d, 0x4b, 0x5e, 0x34, 0x78, 0x40, 0x12, 0xd8, 0x7c, + 0x01, 0xac, 0xff, 0x71, 0xb5, 0x2c, 0x43, 0x2d, 0x54, 0x35, 0x30, 0x42, 0xbe, 0xdb, 0x75, 0x04, 0xd1, 0x9e, 0x55, + 0x37, 0xbf, 0x06, 0x26, 0x5a, 0xf6, 0x26, 0xb0, 0x74, 0x90, 0x45, 0x0b, 0x81, 0x60, 0xe7, 0xfe, 0x7c, 0xed, 0xb2, + 0xd8, 0xce, 0x78, 0x8c, 0x35, 0x61, 0xe1, 0x11, 0xb9, 0x71, 0x80, 0x95, 0xc7, 0x65, 0x09, 0x42, 0x56, 0x94, 0x61, + 0x57, 0xee, 0x1c, 0x50, 0x8f, 0x85, 0x1a, 0x55, 0x08, 0xb2, 0xd6, 0x67, 0xaf, 0xa7, 0x8a, 0x35, 0xc9, 0xfd, 0x3e, + 0x28, 0x30, 0x38, 0x83, 0xbb, 0x4d, 0x45, 0x28, 0x7d, 0x48, 0xe1, 0x4f, 0x6d, 0xba, 0x3e, 0x4b, 0x7b, 0x9e, 0x82, + 0x49, 0xb1, 0x20, 0x5e, 0x2b, 0xf9, 0xe7, 0xe9, 0x2f, 0x12, 0xa8, 0x83, 0x94, 0xdc, 0x98, 0x3e, 0xe2, 0xb5, 0x11, + 0x42, 0x64, 0xac, 0xe7, 0xa0, 0x71, 0x20, 0x9c, 0x52, 0x30, 0xa8, 0x9c, 0xd9, 0x32, 0x8b, 0xe9, 0x78, 0x67, 0x4b, + 0x9d, 0x90, 0x6d, 0x0d, 0x3f, 0xf0, 0x66, 0x1a, 0xfb, 0x89, 0x70, 0xdd, 0xdc, 0xe4, 0x5b, 0x83, 0x67, 0xe8, 0x14, + 0x33, 0x7e, 0x93, 0x31, 0x14, 0xd3, 0xd6, 0x3d, 0x17, 0x4f, 0x4f, 0x4f, 0xc5, 0xa8, 0xb2, 0xb9, 0xe2, 0x61, 0xbc, + 0x1c, 0xab, 0x6a, 0x55, 0x15, 0xd3, 0x42, 0x2b, 0xab, 0xcf, 0x7f, 0x16, 0xc3, 0x25, 0xba, 0xc5, 0x70, 0xb6, 0x08, + 0x6d, 0xa2, 0x88, 0x16, 0x8d, 0x74, 0xcd, 0xd5, 0xfd, 0x4e, 0xdd, 0x95, 0xec, 0xe3, 0xab, 0x77, 0xfb, 0x1f, 0x12, + 0x46, 0xad, 0x97, 0xee, 0x14, 0x90, 0x57, 0x23, 0x9e, 0xf7, 0x5f, 0xcf, 0x29, 0xaf, 0x5a, 0x5e, 0x1a, 0x7d, 0x14, + 0x3c, 0x67, 0xfa, 0xdc, 0xd0, 0xf8, 0x45, 0xd3, 0x28, 0xcd, 0x3e, 0x50, 0x23, 0xbb, 0x81, 0xd6, 0x9b, 0xb4, 0x43, + 0xc6, 0x3b, 0x12, 0x7c, 0xb2, 0x42, 0x78, 0x69, 0xdc, 0x9e, 0x38, 0x89, 0x94, 0x62, 0x34, 0x55, 0x29, 0x54, 0xb5, + 0xce, 0x0a, 0x4d, 0x5b, 0x55, 0x21, 0xc9, 0x81, 0x03, 0xa5, 0x93, 0x21, 0xcc, 0xf1, 0xa4, 0x9c, 0xc4, 0x93, 0xa4, + 0x59, 0xcd, 0x43, 0x4e, 0x79, 0x51, 0x92, 0x86, 0xf4, 0x75, 0xe6, 0x14, 0x80, 0x66, 0x03, 0x25, 0x70, 0x28, 0x49, + 0x01, 0x66, 0x1a, 0xd2, 0x33, 0x44, 0x14, 0x82, 0x01, 0x7a, 0x73, 0x15, 0x13, 0x8f, 0x13, 0x6f, 0x1b, 0xed, 0xb2, + 0xa6, 0x20, 0x9e, 0x7c, 0xec, 0x7d, 0xb3, 0x98, 0xd6, 0x9d, 0x5c, 0x50, 0xc9, 0xf3, 0xc5, 0xd4, 0xd2, 0x04, 0xee, + 0x13, 0x32, 0xd5, 0x8c, 0xa9, 0x42, 0xfe, 0x4d, 0xee, 0xdb, 0xd1, 0x7e, 0x2c, 0x8e, 0xc5, 0xbb, 0x33, 0x34, 0xdd, + 0xcd, 0x55, 0x8e, 0xdc, 0x37, 0x23, 0xb9, 0xd5, 0xb2, 0xa6, 0x11, 0x84, 0x2c, 0x7c, 0xe1, 0x7a, 0xed, 0xf5, 0xf1, + 0x7d, 0xd6, 0xfd, 0xab, 0x0d, 0xc7, 0x8b, 0xe6, 0x25, 0x1f, 0xd2, 0x5d, 0x31, 0xb1, 0x68, 0xaf, 0xfc, 0x24, 0xa9, + 0x77, 0x6a, 0x3d, 0x66, 0xc2, 0xdd, 0x3f, 0x94, 0xa6, 0x31, 0xd3, 0x3b, 0xea, 0x78, 0x3f, 0xba, 0xc3, 0x1c, 0x8a, + 0x98, 0x6a, 0x58, 0xdd, 0x48, 0xa5, 0x5c, 0x98, 0x9e, 0x61, 0x63, 0xae, 0x4e, 0x3b, 0x4a, 0x4a, 0xd0, 0xa9, 0x5a, + 0xff, 0x51, 0x1e, 0xe1, 0x34, 0x55, 0xc1, 0x4f, 0x5e, 0x6d, 0xc7, 0xa2, 0x6b, 0x2f, 0x47, 0x6b, 0xd1, 0xb3, 0x1d, + 0xe5, 0x84, 0x7d, 0x7c, 0x8f, 0x50, 0x75, 0x7d, 0xb1, 0x3e, 0xfd, 0xb5, 0xfe, 0x56, 0xee, 0x06, 0x2a, 0x81, 0x3a, + 0x1b, 0xcb, 0xec, 0x5a, 0x13, 0x17, 0xb6, 0xbf, 0x6e, 0x53, 0xab, 0x06, 0x4e, 0xf6, 0x6a, 0xc3, 0xca, 0x9a, 0xcf, + 0x84, 0x6c, 0x7c, 0x93, 0xb2, 0x5f, 0x88, 0xe1, 0x27, 0xa9, 0x4d, 0x4d, 0x9b, 0xa4, 0xb5, 0xfc, 0x2c, 0xd7, 0xcd, + 0xdb, 0x56, 0xc4, 0xe9, 0xbe, 0x28, 0x82, 0x9c, 0x22, 0x09, 0xd9, 0xc6, 0x78, 0x84, 0xb0, 0x85, 0x0e, 0xe2, 0x5c, + 0xba, 0x88, 0xb1, 0x2c, 0x62, 0x78, 0x2f, 0x8f, 0x7d, 0x12, 0x6a, 0xda, 0x68, 0xa7, 0x2c, 0xb2, 0xff, 0x3e, 0xd3, + 0x8f, 0x8b, 0x2a, 0xa8, 0x03, 0x30, 0xbd, 0xbf, 0x6a, 0x7b, 0xb9, 0x38, 0xea, 0x37, 0x15, 0x07, 0x57, 0xff, 0x94, + 0x36, 0x37, 0x6c, 0xaa, 0xf9, 0x86, 0xa8, 0x54, 0xca, 0xbe, 0x18, 0xf4, 0x8c, 0xec, 0x55, 0xa3, 0x51, 0xcc, 0xa7, + 0xd0, 0xb2, 0x44, 0xfc, 0xf1, 0x54, 0x28, 0x6a, 0xa8, 0xe6, 0x2e, 0xe4, 0xe4, 0xd8, 0x30, 0xf6, 0x27, 0x93, 0xdd, + 0x9e, 0xb6, 0xea, 0xa7, 0xac, 0x67, 0x48, 0x87, 0x87, 0x82, 0x1f, 0xb8, 0xdc, 0x75, 0xf1, 0xa6, 0xec, 0xdd, 0xaa, + 0x45, 0x2a, 0x51, 0x10, 0x2a, 0x9b, 0x7d, 0xf5, 0x86, 0xa9, 0x81, 0x1e, 0x6a, 0xf4, 0x40, 0x19, 0x4c, 0xf1, 0x09, + 0x80, 0x9a, 0xd6, 0xe1, 0xd3, 0xd4, 0x42, 0xd9, 0x48, 0xdf, 0x0b, 0xcc, 0x30, 0xfd, 0xd7, 0x61, 0xb2, 0x42, 0x06, + 0xfc, 0xea, 0x69, 0x79, 0x33, 0xce, 0xbf, 0xe7, 0xb6, 0x87, 0xde, 0xa7, 0x7e, 0xfa, 0x2a, 0x89, 0x71, 0x0f, 0xf6, + 0xf7, 0x69, 0xe6, 0x4c, 0xc9, 0xd8, 0x51, 0x01, 0x24, 0x54, 0xdc, 0x4c, 0x61, 0x08, 0x4f, 0x17, 0x82, 0x22, 0x86, + 0xae, 0x6f, 0xd7, 0xf3, 0x3b, 0xbe, 0x62, 0x1e, 0x51, 0xbb, 0x4c, 0xd5, 0x50, 0xd2, 0xfa, 0x30, 0x1b, 0x10, 0xd6, + 0x04, 0x4f, 0x8e, 0x70, 0xc3, 0xd2, 0x55, 0x44, 0x66, 0xc1, 0x0a, 0xcf, 0xc0, 0xa9, 0x09, 0xb8, 0x6e, 0x8a, 0xcc, + 0x7b, 0x9c, 0x00, 0xce, 0xc7, 0x63, 0x1c, 0xed, 0x29, 0xe0, 0xed, 0xb2, 0xba, 0xda, 0x5b, 0x6a, 0xbd, 0x73, 0x1e, + 0xda, 0x44, 0x10, 0x95, 0xf8, 0x79, 0x36, 0x91, 0xfb, 0x07, 0x6f, 0xce, 0xab, 0xc9, 0x96, 0xa4, 0x43, 0xc9, 0xdf, + 0x41, 0xd1, 0x9b, 0xac, 0xb0, 0x92, 0x1b, 0xc5, 0x22, 0x99, 0x34, 0x02, 0x20, 0x30, 0xaf, 0xf2, 0x1d, 0x11, 0xc0, + 0x55, 0x58, 0x68, 0x34, 0x45, 0x51, 0x5e, 0x51, 0x6d, 0x9e, 0xd1, 0xee, 0xd8, 0xaf, 0xe7, 0xb8, 0x2c, 0xd7, 0x96, + 0xd4, 0x6a, 0x2c, 0xeb, 0x48, 0x8a, 0x66, 0x18, 0xbc, 0x39, 0x2f, 0x05, 0x2f, 0xf1, 0xc1, 0x3c, 0x6f, 0x89, 0xaf, + 0x54, 0x5a, 0x41, 0x23, 0xd7, 0x6b, 0x8a, 0x99, 0x03, 0x9a, 0xd3, 0x65, 0x7a, 0x97, 0xe2, 0xfd, 0xeb, 0x15, 0xbf, + 0x2c, 0x5b, 0xaa, 0xba, 0xee, 0xfa, 0x93, 0x15, 0x71, 0x5c, 0x64, 0xb1, 0x6f, 0x59, 0xb4, 0x19, 0xec, 0x10, 0xfb, + 0x31, 0xed, 0xf3, 0x28, 0xcf, 0xb5, 0xcf, 0x36, 0x3f, 0x97, 0x10, 0x47, 0x96, 0x68, 0xbd, 0x3a, 0x62, 0x3f, 0xb5, + 0x64, 0x63, 0xb9, 0xef, 0x44, 0x29, 0x76, 0xb4, 0xb8, 0x90, 0xe6, 0x42, 0x1f, 0x3c, 0xd7, 0x83, 0xa5, 0x0c, 0x7f, + 0x16, 0x57, 0xb6, 0xf4, 0xaa, 0x1c, 0xad, 0xf4, 0x9f, 0x75, 0xa3, 0x87, 0x53, 0x9b, 0x62, 0xea, 0xde, 0x47, 0xc2, + 0x34, 0xa1, 0xf9, 0xbe, 0x21, 0x36, 0x55, 0x4c, 0x14, 0x44, 0x23, 0x6d, 0x03, 0xc7, 0xfb, 0xe7, 0xf5, 0x95, 0xa7, + 0xbc, 0x94, 0xfc, 0xe1, 0x3a, 0x6e, 0x79, 0x63, 0x68, 0x32, 0xf1, 0x06, 0xad, 0x07, 0x39, 0x81, 0x6d, 0x6c, 0x9f, + 0x1e, 0x69, 0x8f, 0xc2, 0x09, 0xe9, 0x4e, 0x39, 0xb4, 0x0e, 0xd7, 0x27, 0xef, 0xd0, 0x85, 0x28, 0x8d, 0x4c, 0xfc, + 0x84, 0xf4, 0xc6, 0x69, 0x74, 0xaa, 0xab, 0x7f, 0xf2, 0xbc, 0xb3, 0xd8, 0x37, 0xb0, 0xa0, 0xde, 0xff, 0xe9, 0xc6, + 0x50, 0x62, 0x3c, 0x6f, 0x19, 0x71, 0x4c, 0x84, 0xa4, 0xdc, 0x4a, 0xbe, 0x4f, 0x22, 0x2a, 0xb5, 0x52, 0x38, 0xa3, + 0x17, 0xf4, 0x88, 0x1a, 0x2c, 0x9e, 0x9f, 0x5a, 0xe7, 0xc0, 0xa4, 0x1b, 0xe5, 0xa5, 0x51, 0x20, 0x0d, 0x22, 0x4f, + 0xcd, 0xf4, 0x0c, 0x9a, 0xb7, 0x0f, 0xaf, 0x03, 0xf7, 0x9e, 0x20, 0x9f, 0xff, 0xfe, 0x30, 0xdc, 0xde, 0x1a, 0x68, + 0x96, 0xf5, 0x39, 0x76, 0x51, 0xeb, 0x8b, 0x15, 0x7a, 0x58, 0x80, 0xdd, 0x13, 0x92, 0xeb, 0x3f, 0x05, 0xe8, 0x1a, + 0xcc, 0xb2, 0x55, 0xc7, 0xbc, 0x6d, 0xfb, 0xb7, 0xf3, 0x2a, 0xdc, 0x1d, 0x33, 0x10, 0x68, 0x77, 0xc6, 0x38, 0x87, + 0xff, 0x67, 0x89, 0x64, 0x15, 0xc6, 0xe4, 0xa2, 0xbd, 0x6e, 0x0f, 0x97, 0xc4, 0x6e, 0xb5, 0x66, 0x39, 0xd3, 0x76, + 0x60, 0xeb, 0x39, 0x2f, 0xa2, 0xd2, 0x20, 0xc1, 0x4e, 0x6a, 0x43, 0x03, 0x44, 0x32, 0xe8, 0xf6, 0x52, 0xc6, 0xbd, + 0x20, 0x9f, 0x01, 0x7d, 0x6d, 0x67, 0x2e, 0xbd, 0x31, 0x35, 0xae, 0x70, 0x52, 0x97, 0x9d, 0xbb, 0xc9, 0x70, 0xd6, + 0x3e, 0x16, 0xca, 0xd7, 0x63, 0x81, 0x2f, 0xac, 0x8f, 0xd3, 0xf4, 0xc1, 0x1d, 0xd9, 0x47, 0x93, 0x63, 0x2f, 0xa6, + 0xa4, 0x2a, 0x33, 0x18, 0x65, 0x08, 0xb4, 0x74, 0x2d, 0xcb, 0x94, 0x62, 0x8f, 0xde, 0x3e, 0x9c, 0x32, 0x6e, 0xfa, + 0x79, 0x98, 0x73, 0xd0, 0x89, 0x65, 0x8b, 0xe7, 0x15, 0xd9, 0xc3, 0xd4, 0x9d, 0x00, 0x89, 0x04, 0x61, 0xa2, 0x0b, + 0x95, 0x7a, 0x90, 0x61, 0x4d, 0x78, 0x84, 0x34, 0x71, 0x71, 0x3a, 0x32, 0x61, 0x77, 0xe4, 0x49, 0x07, 0x51, 0x07, + 0x86, 0xca, 0xd5, 0x73, 0xfe, 0xd0, 0x63, 0xb2, 0x17, 0x14, 0xd9, 0xf6, 0x48, 0xe1, 0x9c, 0x79, 0xf3, 0x21, 0x7b, + 0xe8, 0x5f, 0x37, 0xbd, 0xe6, 0x88, 0x05, 0xf7, 0xb7, 0x50, 0x81, 0x32, 0x04, 0xdc, 0x1f, 0xfa, 0xee, 0x36, 0x47, + 0xad, 0xa0, 0x33, 0x30, 0x7d, 0xb2, 0xcf, 0xf4, 0x62, 0x4d, 0x69, 0xb8, 0x6f, 0x46, 0xce, 0xe0, 0x4e, 0xd0, 0xb5, + 0x33, 0xa9, 0xb4, 0xbb, 0x7c, 0x21, 0xa8, 0xf0, 0xe1, 0x1a, 0xb4, 0x3a, 0x88, 0x9c, 0x92, 0xfe, 0x4e, 0x48, 0x75, + 0xb5, 0x29, 0x26, 0xdc, 0x40, 0xcd, 0x06, 0x8a, 0xa3, 0x70, 0xe3, 0x07, 0x89, 0x01, 0x66, 0x6e, 0xa4, 0x61, 0x25, + 0xaf, 0x9d, 0x87, 0x5f, 0xec, 0x07, 0x39, 0xcf, 0x63, 0x2a, 0xd1, 0x43, 0x9f, 0x56, 0x75, 0xfd, 0x21, 0xe6, 0x1b, + 0x6a, 0x9f, 0x41, 0x6d, 0x93, 0x10, 0xa2, 0x4e, 0xd3, 0x3e, 0xe6, 0x59, 0xf9, 0xd1, 0xc1, 0x84, 0x98, 0x7b, 0x32, + 0xd0, 0xaa, 0x5d, 0x81, 0xa5, 0xec, 0x52, 0x95, 0x70, 0xed, 0xd4, 0x6f, 0x2a, 0x69, 0x17, 0xab, 0x95, 0x57, 0xa7, + 0xd8, 0xb3, 0x7f, 0xe7, 0xda, 0xfb, 0x90, 0xf1, 0x99, 0xe8, 0x58, 0xb3, 0xda, 0xbd, 0xee, 0x27, 0xce, 0x69, 0xbc, + 0xc4, 0x46, 0x09, 0xe5, 0x87, 0x69, 0x40, 0x3c, 0x78, 0x83, 0x78, 0xd7, 0x4f, 0x6c, 0xf6, 0xe2, 0xaa, 0x2f, 0x35, + 0x5a, 0xa8, 0x3f, 0xe9, 0xc3, 0xa3, 0x1a, 0x9c, 0x3c, 0x5c, 0x86, 0x27, 0x5f, 0x79, 0x3b, 0x19, 0xe0, 0xb1, 0x12, + 0xf8, 0xdc, 0x5a, 0x02, 0x4a, 0x47, 0x24, 0xaf, 0xe4, 0x03, 0xfa, 0x7f, 0x0e, 0xcf, 0x87, 0x5d, 0x8f, 0x9f, 0x2d, + 0x6d, 0xa8, 0x45, 0x27, 0x1d, 0x61, 0x09, 0x6a, 0x7b, 0x48, 0x43, 0x88, 0x8c, 0x1d, 0x81, 0x69, 0xcc, 0x9f, 0x14, + 0x61, 0x1e, 0x81, 0xf7, 0x39, 0x03, 0x8e, 0xda, 0x96, 0xf8, 0xc2, 0x09, 0x77, 0xef, 0xf2, 0xe1, 0x37, 0xf0, 0xbd, + 0xb2, 0x4b, 0x58, 0x6e, 0xab, 0x1d, 0xbb, 0xd9, 0x04, 0x9a, 0xa3, 0x28, 0x6e, 0xbf, 0x99, 0x68, 0xd1, 0xb3, 0xc3, + 0x7e, 0x0e, 0xba, 0x97, 0xa1, 0x42, 0xf9, 0x98, 0xf6, 0x99, 0xdc, 0xaf, 0x47, 0x80, 0x22, 0xe0, 0x10, 0x43, 0x6c, + 0xff, 0xd8, 0x2b, 0x0f, 0xb5, 0x9e, 0x05, 0x04, 0x14, 0xc3, 0x9f, 0x5c, 0x70, 0x66, 0xfa, 0xe0, 0x18, 0x30, 0x39, + 0x00, 0xd4, 0x06, 0x17, 0x8d, 0xc5, 0x29, 0xfe, 0xbf, 0xf3, 0x8d, 0xe4, 0xed, 0xba, 0x38, 0x1d, 0xf1, 0x2e, 0x9f, + 0x51, 0x54, 0xcc, 0x90, 0x42, 0x0b, 0xbf, 0xe8, 0x06, 0xc2, 0x4a, 0x11, 0x0b, 0x7a, 0x2b, 0x1f, 0xdb, 0xcb, 0x63, + 0x14, 0xaa, 0xff, 0xab, 0x97, 0xec, 0x8f, 0x5a, 0xf0, 0xd8, 0xa5, 0x58, 0xde, 0xf0, 0x91, 0x53, 0xaa, 0x87, 0xbb, + 0x78, 0xb3, 0x1d, 0x06, 0x05, 0xbd, 0x1d, 0x10, 0x6f, 0xfd, 0x9f, 0x25, 0x49, 0xb6, 0xdc, 0x6a, 0x86, 0x24, 0xb9, + 0xae, 0x8e, 0x3b, 0xe2, 0xdf, 0x8f, 0x78, 0x57, 0x1b, 0x1d, 0xaa, 0xf6, 0x7c, 0x5c, 0x67, 0xfe, 0x2b, 0xce, 0xf2, + 0x86, 0xa4, 0xd3, 0xcc, 0xee, 0x6b, 0x5c, 0xce, 0x65, 0x3b, 0x99, 0x2f, 0x66, 0x77, 0xb3, 0xfd, 0xf2, 0xfd, 0x96, + 0x2a, 0x63, 0xeb, 0xf9, 0x45, 0xf3, 0x31, 0xc7, 0x1d, 0x91, 0x94, 0x65, 0x18, 0xcb, 0xf9, 0xb9, 0x4b, 0xf3, 0xe3, + 0x0f, 0xc2, 0x9b, 0x1f, 0xbf, 0x78, 0x28, 0x38, 0x9d, 0x62, 0x2a, 0x23, 0x4e, 0x95, 0xce, 0x9c, 0x24, 0x86, 0xa9, + 0x14, 0x68, 0x26, 0xba, 0xbe, 0x06, 0xc9, 0x00, 0xbd, 0x82, 0xa6, 0xc3, 0xd0, 0x9f, 0xf1, 0x01, 0xae, 0x3a, 0x79, + 0xa6, 0x92, 0xcc, 0x17, 0x8c, 0x31, 0x5e, 0xf0, 0x43, 0xbf, 0xf0, 0xe4, 0x5e, 0x3b, 0x32, 0x80, 0x21, 0x15, 0x7b, + 0xfc, 0x78, 0xd1, 0x7c, 0x79, 0x69, 0x44, 0x08, 0x55, 0xc8, 0x52, 0x80, 0xa7, 0x3c, 0x7f, 0x26, 0xab, 0xeb, 0xd9, + 0x6f, 0x36, 0x5d, 0x69, 0xb8, 0xaf, 0xa6, 0x9e, 0x2a, 0x60, 0x6c, 0xb9, 0x91, 0x8f, 0x29, 0x66, 0xd6, 0x06, 0xeb, + 0x74, 0x50, 0xab, 0xc7, 0x1c, 0xe3, 0xa9, 0xa0, 0x2e, 0xa6, 0xd4, 0x93, 0x3c, 0xd6, 0xd9, 0xf4, 0x41, 0x36, 0xb8, + 0x81, 0x71, 0xc5, 0xc9, 0x47, 0x10, 0x45, 0x13, 0x60, 0x39, 0x4f, 0x5b, 0x44, 0x11, 0x7c, 0x87, 0x66, 0x14, 0xc1, + 0x10, 0xb1, 0x88, 0x2d, 0xef, 0x56, 0xc9, 0xbc, 0xbd, 0xec, 0x72, 0x92, 0xe9, 0xb7, 0xa5, 0xcc, 0x49, 0xa2, 0xc1, + 0xc1, 0x2a, 0x9f, 0xb5, 0xea, 0xa6, 0x1f, 0xec, 0x4b, 0x28, 0x00, 0x8e, 0xcc, 0xc0, 0x81, 0x92, 0x62, 0x56, 0xaa, + 0x8a, 0x1a, 0x39, 0x08, 0x70, 0xf2, 0xc3, 0x3f, 0x54, 0x5f, 0x84, 0xa5, 0xb3, 0xdb, 0x29, 0x08, 0x3d, 0xc1, 0x08, + 0x91, 0x40, 0xe3, 0x27, 0x97, 0x6c, 0xfa, 0xef, 0xdc, 0xcc, 0x48, 0x5f, 0xfe, 0xbd, 0x9e, 0xec, 0x6d, 0x6b, 0x50, + 0x30, 0xb9, 0x1e, 0xed, 0xeb, 0x58, 0x2b, 0x96, 0x4e, 0xa8, 0x4b, 0x7f, 0x71, 0x05, 0x3e, 0xa9, 0x09, 0x91, 0xb1, + 0x62, 0xa6, 0x32, 0x6b, 0x29, 0x78, 0xae, 0x7e, 0xcc, 0x65, 0x60, 0x26, 0x52, 0xda, 0x15, 0x93, 0xa6, 0x34, 0xf3, + 0x29, 0x17, 0xd1, 0xb3, 0x67, 0x5d, 0xa7, 0xa1, 0xb5, 0x0e, 0xac, 0xcb, 0x7e, 0x88, 0xb7, 0xf9, 0xd5, 0x99, 0xa6, + 0x30, 0xca, 0xf9, 0xab, 0xf3, 0x0e, 0x8b, 0x72, 0xb3, 0xbe, 0x62, 0x3e, 0xec, 0x1d, 0xda, 0x69, 0x65, 0xf4, 0xf1, + 0x5c, 0xad, 0x70, 0xdf, 0x81, 0x90, 0xf3, 0xe8, 0x7b, 0x03, 0x1e, 0xff, 0x0a, 0xff, 0xbf, 0x3e, 0x04, 0xda, 0xb1, + 0x15, 0x0c, 0xdd, 0xf0, 0x89, 0x4d, 0x70, 0x8f, 0x86, 0x99, 0xd3, 0xd9, 0xca, 0xef, 0x43, 0x22, 0xea, 0x16, 0x70, + 0xb7, 0x8b, 0x1f, 0xd7, 0x3e, 0xc3, 0xd5, 0xc8, 0xc6, 0x18, 0x0e, 0xb9, 0x01, 0xb2, 0x84, 0xf0, 0x09, 0x09, 0x63, + 0xdd, 0x39, 0x3f, 0x38, 0xa3, 0x31, 0xbe, 0xfb, 0x5b, 0xe7, 0xf9, 0x66, 0xbc, 0x8d, 0xf9, 0x75, 0xf2, 0x4d, 0xe7, + 0x7a, 0xa0, 0xf3, 0xf4, 0xa0, 0xd6, 0x6a, 0xfd, 0xc3, 0x4d, 0xef, 0x5d, 0x0c, 0x4b, 0xb8, 0x9f, 0x3a, 0xba, 0xb9, + 0x7b, 0x13, 0x11, 0x11, 0xa8, 0x3f, 0x78, 0x68, 0xd1, 0xf3, 0x09, 0xd4, 0xe9, 0x12, 0x22, 0xfa, 0xa3, 0xcd, 0x9e, + 0xdb, 0xc9, 0x9c, 0x3a, 0x79, 0xb2, 0x8d, 0xae, 0x45, 0x25, 0x5f, 0x58, 0x2c, 0xf3, 0x3e, 0x6d, 0xdd, 0x88, 0xc8, + 0x81, 0xc4, 0x64, 0xc5, 0x36, 0xc3, 0xd4, 0xd0, 0x71, 0xea, 0x22, 0xf1, 0x3f, 0xef, 0xeb, 0xc4, 0x50, 0xf2, 0xb2, + 0xd4, 0x02, 0x0b, 0x4b, 0x55, 0xd8, 0x3e, 0xee, 0x39, 0x95, 0x85, 0x55, 0x37, 0x46, 0xbc, 0x75, 0xdf, 0x76, 0x4d, + 0xc7, 0x26, 0x8a, 0xd7, 0x5f, 0xbf, 0x02, 0xad, 0x21, 0x3d, 0x16, 0xf1, 0x7e, 0x91, 0x8e, 0x63, 0x00, 0xde, 0x31, + 0x74, 0x0b, 0x77, 0xcb, 0xb2, 0x6a, 0xcf, 0xfb, 0x74, 0x0c, 0x25, 0x45, 0xb1, 0x94, 0xdc, 0x3d, 0x62, 0xeb, 0x71, + 0x94, 0xe0, 0xa9, 0xee, 0x3d, 0xbd, 0x45, 0x2a, 0x91, 0xa5, 0xa3, 0xf4, 0xd8, 0xcf, 0x29, 0x60, 0xea, 0xa5, 0xf8, + 0x7d, 0xf4, 0x68, 0x59, 0x32, 0x40, 0x8b, 0x8d, 0x58, 0xe5, 0x1d, 0x5b, 0xc1, 0x5a, 0x9c, 0x92, 0x63, 0xbc, 0xed, + 0xdd, 0x97, 0x54, 0xca, 0x5d, 0xcc, 0x6c, 0x94, 0x76, 0x6a, 0xbc, 0x1c, 0x1c, 0xa7, 0xa5, 0xb0, 0x22, 0xc6, 0x18, + 0x39, 0xbb, 0x12, 0xb4, 0x35, 0x42, 0x77, 0xb8, 0x66, 0x89, 0xff, 0xbe, 0xac, 0x2d, 0x6e, 0x25, 0x90, 0x91, 0x2f, + 0xc3, 0x37, 0xe5, 0x9b, 0xa0, 0xad, 0xfe, 0x62, 0x8f, 0xbe, 0x56, 0x10, 0x32, 0xe1, 0x57, 0x7c, 0x35, 0xba, 0xe6, + 0xf6, 0x7d, 0xd9, 0x4d, 0x56, 0x69, 0x92, 0x9d, 0x40, 0x6b, 0x93, 0xca, 0xb9, 0xf0, 0xf0, 0x39, 0x77, 0x47, 0x92, + 0x3e, 0x7d, 0x2a, 0xcc, 0x28, 0x79, 0xc9, 0x54, 0x50, 0x3a, 0xc8, 0x66, 0x7f, 0x82, 0x25, 0xa8, 0x87, 0x7c, 0x41, + 0x6d, 0xdd, 0xe3, 0xe9, 0xf3, 0x1a, 0x88, 0xeb, 0x65, 0xc3, 0x0a, 0x44, 0x22, 0xfa, 0x6f, 0xb3, 0x8f, 0x3e, 0x64, + 0x73, 0x42, 0xf6, 0xfa, 0x66, 0x8e, 0xd3, 0x9d, 0x44, 0x28, 0xca, 0x1d, 0xb7, 0x03, 0x4a, 0x29, 0x0e, 0x4a, 0xd5, + 0xf0, 0xd8, 0x2c, 0x91, 0x63, 0xe6, 0x07, 0xa7, 0xbb, 0xd8, 0x4f, 0x5c, 0x8b, 0x5f, 0xd8, 0xb1, 0x53, 0x79, 0xf3, + 0xcf, 0xbe, 0x7c, 0xd9, 0xc7, 0x83, 0xc8, 0xe8, 0x0f, 0x42, 0x11, 0x5f, 0xf6, 0x9b, 0x26, 0xf1, 0xe2, 0x17, 0xdf, + 0xd2, 0x53, 0x3c, 0xf7, 0x6b, 0x75, 0x11, 0xb7, 0x75, 0xf7, 0xbe, 0x8a, 0x76, 0x29, 0xb1, 0xe1, 0x36, 0x0c, 0x4f, + 0x93, 0xe4, 0xf4, 0x00, 0xe0, 0x03, 0xce, 0xe5, 0x3f, 0x73, 0x14, 0xca, 0x47, 0x2e, 0xc3, 0xf9, 0x62, 0x11, 0x62, + 0x4c, 0xfe, 0xc6, 0x18, 0xa5, 0x35, 0x6f, 0x9f, 0xb7, 0x77, 0xbf, 0x71, 0x6c, 0x78, 0x6d, 0xbc, 0x89, 0x86, 0x8a, + 0x16, 0xe5, 0x4d, 0xe1, 0x53, 0x5e, 0x17, 0x76, 0x79, 0xaf, 0xf0, 0x98, 0xf7, 0x0b, 0x4f, 0xf9, 0x60, 0xed, 0xd1, + 0x68, 0x45, 0x48, 0xc1, 0xb5, 0x40, 0xd6, 0x85, 0x42, 0x97, 0x71, 0x04, 0xf7, 0x94, 0x17, 0x6d, 0xcd, 0xef, 0xd0, + 0x44, 0x96, 0xff, 0x07, 0x62, 0x85, 0xd5, 0xe9, 0x07, 0x4d, 0xf1, 0x0a, 0xc4, 0x58, 0xe6, 0x58, 0x8a, 0xd5, 0xed, + 0x7f, 0xd6, 0x52, 0x31, 0x1e, 0x73, 0xb6, 0x99, 0x81, 0xbe, 0x5a, 0xbe, 0xc2, 0xc6, 0x40, 0xe3, 0xeb, 0x4d, 0x69, + 0xf5, 0x1a, 0x58, 0x8b, 0xfd, 0x7c, 0x4d, 0x23, 0x59, 0x89, 0xb0, 0x52, 0xe5, 0x61, 0x60, 0xa2, 0x2a, 0xf3, 0x8c, + 0x74, 0x04, 0xc5, 0xf3, 0xe9, 0x0b, 0xbe, 0x72, 0xd4, 0xda, 0x67, 0x05, 0xa8, 0x86, 0xc7, 0x42, 0x47, 0x2f, 0x8c, + 0xec, 0xea, 0xba, 0xa5, 0xa6, 0xb6, 0x67, 0x5f, 0x12, 0x6b, 0xe4, 0xb7, 0xe3, 0x67, 0x52, 0x24, 0xb4, 0x6c, 0xfc, + 0x3e, 0x8f, 0x77, 0xb1, 0xf7, 0x95, 0x86, 0x34, 0x40, 0x68, 0x9d, 0x90, 0x59, 0xd4, 0x74, 0xc1, 0x4b, 0xc2, 0xa7, + 0xa5, 0x8f, 0xe9, 0x47, 0xc7, 0xfb, 0x8b, 0xaf, 0xf0, 0x00, 0x47, 0x5a, 0xbb, 0xd8, 0xe4, 0xc7, 0xe3, 0x02, 0x7e, + 0xed, 0x37, 0x1d, 0x0a, 0x6b, 0xc6, 0x2a, 0x97, 0xde, 0xb4, 0xab, 0x8b, 0xe0, 0x6b, 0x4b, 0x9f, 0xf1, 0xb8, 0x7f, + 0xec, 0x4d, 0x1d, 0xef, 0x4f, 0x7a, 0x04, 0xbe, 0x01, 0x28, 0x15, 0x35, 0x88, 0x7d, 0x10, 0x7a, 0xbc, 0xb3, 0x2a, + 0x82, 0xcb, 0xf0, 0x38, 0xa4, 0xed, 0xf9, 0x32, 0xb3, 0xab, 0xc7, 0xf8, 0x8d, 0x90, 0x04, 0xdd, 0xf0, 0x4e, 0x5a, + 0x12, 0xa0, 0xf4, 0x51, 0x09, 0x93, 0x1c, 0xb1, 0xcf, 0x2f, 0x5a, 0xf6, 0xa6, 0x8d, 0x4e, 0xe1, 0x5b, 0x8f, 0x98, + 0x67, 0x6d, 0x99, 0xf3, 0x9f, 0x06, 0x71, 0x30, 0x93, 0xa3, 0xf8, 0xfd, 0x10, 0xe7, 0x45, 0x15, 0x75, 0xe9, 0xc5, + 0x6c, 0x6f, 0x03, 0xb6, 0xf0, 0xbb, 0x0f, 0xb3, 0x81, 0xef, 0x4f, 0x7d, 0xb9, 0xd6, 0xa1, 0x9e, 0xd1, 0xfd, 0x56, + 0x75, 0xdb, 0xc7, 0x91, 0x75, 0xf2, 0x9c, 0xc5, 0xc3, 0xe8, 0xdd, 0xf7, 0x85, 0xaf, 0x71, 0x66, 0xb4, 0xf8, 0x24, + 0x2a, 0x0a, 0x2b, 0x97, 0x41, 0xb9, 0x7c, 0x4d, 0x55, 0xb5, 0x47, 0x9b, 0x2f, 0x62, 0x74, 0x5e, 0xfc, 0x5e, 0xa7, + 0x8f, 0xba, 0xc6, 0xeb, 0x48, 0xf9, 0x68, 0x5f, 0x16, 0xc3, 0x1f, 0xac, 0x20, 0xb4, 0x98, 0xd8, 0xec, 0xb1, 0x5f, + 0x8e, 0x16, 0xa7, 0x67, 0x69, 0x33, 0xec, 0x34, 0x6d, 0xb5, 0x71, 0x3b, 0xd8, 0x6f, 0x1d, 0xd2, 0x92, 0xc4, 0x8b, + 0xf1, 0x15, 0x2a, 0x7f, 0xc0, 0x43, 0xec, 0x39, 0x48, 0xd0, 0x88, 0x35, 0xe7, 0xb7, 0xc8, 0x75, 0xba, 0x16, 0x48, + 0x5d, 0xf8, 0x7a, 0xe8, 0x61, 0xd2, 0x22, 0xd5, 0x41, 0x59, 0x06, 0xba, 0x89, 0x02, 0xfa, 0x9e, 0xba, 0x2d, 0xc8, + 0x45, 0xf6, 0xf7, 0x9c, 0x9d, 0xbe, 0xc6, 0xfb, 0x73, 0x0b, 0x3b, 0x51, 0xf8, 0xcd, 0x1f, 0x93, 0x18, 0xd6, 0xdc, + 0x76, 0x91, 0x2d, 0x82, 0xde, 0x6c, 0x5a, 0x3e, 0x28, 0x07, 0x6c, 0x7e, 0x69, 0xa1, 0xca, 0xc8, 0x11, 0xeb, 0xf9, + 0x6f, 0xf7, 0x63, 0x97, 0x98, 0x57, 0x41, 0xa8, 0x5e, 0xa9, 0x2a, 0x31, 0x80, 0x3e, 0xa9, 0x3d, 0x03, 0x75, 0x66, + 0x76, 0x55, 0xe9, 0xf5, 0xeb, 0xac, 0x3e, 0xd4, 0xee, 0x02, 0xf7, 0x4e, 0xc3, 0xb3, 0x13, 0x6b, 0x25, 0x8b, 0xe8, + 0x23, 0x24, 0x61, 0x02, 0xfd, 0x7e, 0xd7, 0xb5, 0xaf, 0x7b, 0x3a, 0x96, 0x05, 0x94, 0x89, 0x3a, 0x5c, 0x9c, 0x20, + 0x18, 0x3f, 0xc8, 0x71, 0x80, 0x6d, 0xe4, 0xc7, 0x2e, 0x8b, 0xab, 0xfe, 0x1c, 0x28, 0x92, 0xa0, 0xb9, 0x96, 0xfb, + 0x35, 0xb8, 0xaf, 0xef, 0x74, 0x93, 0x15, 0xd9, 0x65, 0x98, 0x33, 0xde, 0x30, 0xc6, 0x08, 0x51, 0xc5, 0x22, 0x9e, + 0xe7, 0xb8, 0x81, 0xe5, 0x71, 0x09, 0xde, 0x58, 0xce, 0x3b, 0xa3, 0xda, 0xf2, 0x6c, 0x80, 0xa6, 0xb4, 0x62, 0x1b, + 0x95, 0x6a, 0x65, 0x0c, 0x0c, 0x64, 0xcb, 0x4e, 0xa6, 0xef, 0xa9, 0x2c, 0xc6, 0xfb, 0x77, 0x47, 0x04, 0x37, 0x3d, + 0xca, 0x7c, 0x7d, 0x10, 0xc6, 0xd0, 0xdc, 0xc3, 0xa0, 0x62, 0xb7, 0x4d, 0x39, 0x06, 0x17, 0x5c, 0x74, 0xa2, 0x26, + 0x35, 0x94, 0x45, 0xb5, 0x8c, 0x14, 0x5e, 0xcd, 0x8a, 0xbe, 0xee, 0x69, 0xf1, 0x5a, 0x84, 0x18, 0x94, 0xe1, 0xba, + 0x24, 0x21, 0x54, 0x26, 0x08, 0x7d, 0xa8, 0x30, 0xa5, 0xc2, 0xeb, 0x94, 0x80, 0xfd, 0x3d, 0xcf, 0x79, 0xdd, 0xfb, + 0x5d, 0x3b, 0x2c, 0xb3, 0xe4, 0xb8, 0xd7, 0x70, 0xbb, 0x82, 0xbb, 0x23, 0xcf, 0x46, 0x76, 0x6b, 0x64, 0xf2, 0xbe, + 0x56, 0x0c, 0xe9, 0xb6, 0x60, 0x2a, 0x2e, 0x8a, 0x68, 0x95, 0xc5, 0xb8, 0x1d, 0xf8, 0x95, 0xbb, 0x45, 0xb3, 0x9e, + 0x3a, 0x93, 0xf5, 0x86, 0x21, 0x7c, 0x1a, 0x96, 0xb1, 0x84, 0x58, 0xbd, 0x1e, 0xf9, 0x7f, 0x97, 0x85, 0x47, 0x45, + 0xbb, 0x4f, 0x28, 0xc4, 0xbd, 0xc9, 0x8c, 0x37, 0x03, 0x70, 0x90, 0x63, 0x88, 0x63, 0x70, 0xa0, 0xb5, 0xac, 0xd0, + 0xa9, 0x91, 0x80, 0x88, 0xb5, 0x25, 0x7f, 0xd3, 0x5b, 0xec, 0x2a, 0x7a, 0x6d, 0xdb, 0x77, 0x8e, 0x7f, 0xfe, 0xb6, + 0xda, 0xd6, 0x4d, 0x2c, 0xe4, 0x9d, 0x91, 0x41, 0x3d, 0xb0, 0xbf, 0xef, 0x88, 0x13, 0x6d, 0x81, 0xc0, 0xd5, 0x07, + 0xd3, 0x62, 0x7d, 0xbc, 0x10, 0x31, 0x3f, 0xf8, 0x18, 0x26, 0xf1, 0x14, 0x1d, 0x7d, 0xc6, 0xe7, 0x86, 0x8f, 0xc2, + 0x0f, 0xff, 0xb3, 0x1c, 0x58, 0x99, 0x74, 0x24, 0xa7, 0x8e, 0xa9, 0x8e, 0x02, 0x02, 0xe8, 0x4c, 0xee, 0x91, 0xef, + 0xbf, 0x3a, 0xb4, 0x54, 0xb1, 0x6c, 0x3a, 0x43, 0xb3, 0x93, 0x4e, 0xac, 0x5b, 0xcc, 0x06, 0x9f, 0x38, 0xf7, 0x8b, + 0xcb, 0x0f, 0xe9, 0xc9, 0x61, 0x7f, 0x7b, 0xd2, 0x68, 0xd3, 0x63, 0x46, 0x03, 0x60, 0x0c, 0x2b, 0xfd, 0x78, 0x90, + 0xd2, 0xeb, 0x27, 0x6a, 0xa2, 0x65, 0x43, 0x78, 0x66, 0x3c, 0xba, 0x0c, 0x91, 0xfe, 0xc3, 0xa0, 0x78, 0xd8, 0x6c, + 0xbd, 0x32, 0x5f, 0xb0, 0x9a, 0x83, 0xd1, 0x0b, 0x82, 0x66, 0xc3, 0x16, 0x8b, 0xca, 0xea, 0x71, 0x7e, 0x84, 0x59, + 0x50, 0x00, 0x3e, 0x65, 0x6d, 0x80, 0xfe, 0x39, 0xe6, 0x98, 0x0b, 0x88, 0x46, 0xa3, 0x36, 0x52, 0x6d, 0xf5, 0xbc, + 0xe2, 0x9f, 0xa9, 0x38, 0x50, 0xeb, 0x3d, 0x39, 0x66, 0x7b, 0xca, 0xea, 0x6a, 0x93, 0x4a, 0x03, 0xb4, 0xbe, 0x4c, + 0xf0, 0xb5, 0x0e, 0xb5, 0x04, 0x72, 0x56, 0xc0, 0x67, 0x96, 0x56, 0x97, 0xd9, 0x3d, 0xe7, 0xf8, 0xbd, 0x78, 0xf7, + 0xa0, 0x33, 0xee, 0x36, 0xdf, 0x6d, 0x06, 0x3b, 0x2b, 0x91, 0xdf, 0x0f, 0x1c, 0xb0, 0xf5, 0xce, 0xf1, 0xb2, 0x16, + 0x78, 0xbf, 0x85, 0x41, 0x00, 0xf2, 0x7e, 0x81, 0x5d, 0xd2, 0x38, 0x0d, 0xf3, 0x95, 0xb6, 0x94, 0xc6, 0xb8, 0x72, + 0xfc, 0x94, 0x33, 0xff, 0x3f, 0xd4, 0x58, 0x19, 0xc7, 0x4f, 0x6c, 0x80, 0x76, 0x15, 0x20, 0xc9, 0x01, 0xd1, 0xc1, + 0x93, 0x16, 0x8f, 0xdf, 0x08, 0x0a, 0xfd, 0x6f, 0xae, 0xf9, 0xf5, 0x86, 0x41, 0x6c, 0x7b, 0x84, 0xf0, 0x0b, 0x6d, + 0xd8, 0xfc, 0x4d, 0x67, 0xcd, 0x25, 0x44, 0x72, 0xfd, 0x1d, 0x29, 0xa9, 0xab, 0xe7, 0x91, 0xfb, 0x93, 0x06, 0xc0, + 0xa4, 0xb2, 0xfa, 0x3a, 0xed, 0xf9, 0xc2, 0xeb, 0x79, 0x07, 0xb1, 0x19, 0xc7, 0xef, 0x8e, 0x98, 0xf8, 0x50, 0x54, + 0xd5, 0x59, 0xd4, 0xb4, 0x3a, 0xf6, 0xd6, 0x49, 0x07, 0x3a, 0x71, 0x41, 0xf0, 0x18, 0xbf, 0x04, 0xfb, 0x79, 0xf3, + 0x43, 0x42, 0x1d, 0xbf, 0xeb, 0x87, 0xe4, 0x7a, 0x37, 0x85, 0x07, 0x76, 0xc0, 0xf7, 0xf0, 0xc1, 0xda, 0x44, 0xd3, + 0xb9, 0x10, 0x1f, 0x42, 0x52, 0x11, 0x90, 0xf5, 0x24, 0x4e, 0x6e, 0x4a, 0x92, 0x60, 0xc3, 0x5e, 0xd6, 0xb6, 0x82, + 0xc3, 0xb9, 0x76, 0x87, 0x22, 0x9c, 0x46, 0x07, 0xdd, 0x0c, 0x8f, 0x38, 0xe3, 0xa4, 0x6e, 0x65, 0xea, 0xb3, 0x6d, + 0x10, 0x89, 0x91, 0x70, 0x05, 0x04, 0x9f, 0x08, 0x1e, 0x8c, 0x98, 0x1a, 0x20, 0xa9, 0x08, 0x70, 0xfd, 0xb0, 0x8d, + 0x50, 0x76, 0x3f, 0xe5, 0x27, 0x7c, 0x12, 0x43, 0x0e, 0x39, 0xac, 0xc3, 0xf3, 0xe7, 0x70, 0xd1, 0x50, 0x2c, 0xce, + 0x1c, 0x67, 0x5e, 0x94, 0xd5, 0xb4, 0x50, 0x9c, 0x58, 0xf9, 0x82, 0x07, 0x5c, 0x6f, 0xc0, 0xbc, 0x9d, 0x0a, 0x76, + 0xc6, 0x33, 0x5e, 0x61, 0x4a, 0x4c, 0x6f, 0x77, 0xce, 0x2b, 0x5d, 0xb9, 0x55, 0x14, 0xaf, 0x1a, 0xb4, 0x67, 0x46, + 0x5c, 0xf8, 0x3b, 0xad, 0x8d, 0x6e, 0xd9, 0xa5, 0x71, 0xf8, 0x37, 0x4a, 0x24, 0x04, 0x9b, 0x9f, 0x78, 0xe3, 0x3d, + 0xb4, 0x6b, 0xdf, 0x05, 0x87, 0x59, 0x7e, 0xfb, 0x1a, 0xfd, 0xe9, 0x4d, 0xcf, 0xb0, 0x28, 0xbd, 0x9f, 0x99, 0x83, + 0xea, 0x40, 0x56, 0x57, 0x87, 0x03, 0x0c, 0xda, 0xe1, 0x8e, 0x57, 0x90, 0x6e, 0xc5, 0x2c, 0x43, 0xa4, 0x33, 0x19, + 0xfd, 0xdd, 0x8b, 0x79, 0xc1, 0x3a, 0x04, 0x66, 0x1f, 0x0d, 0x73, 0x02, 0x17, 0xab, 0x0c, 0x0a, 0xa1, 0x0a, 0x21, + 0x7c, 0x1c, 0xe6, 0x8a, 0x9c, 0x06, 0x52, 0xe1, 0x8a, 0x9c, 0xfa, 0xa4, 0x83, 0x72, 0x1d, 0x3a, 0x5f, 0xad, 0x71, + 0x3c, 0xc5, 0x84, 0xbe, 0x18, 0x78, 0xa8, 0xaf, 0xd8, 0x2c, 0x3e, 0xf7, 0x42, 0x64, 0xfd, 0x0d, 0x98, 0xdc, 0xe0, + 0x65, 0x75, 0x9f, 0x85, 0x10, 0xb3, 0x70, 0x99, 0x19, 0xa9, 0x5f, 0x8a, 0x5a, 0x4f, 0xa3, 0x11, 0xa0, 0xd6, 0x3c, + 0xa0, 0x55, 0xcb, 0x10, 0x61, 0xfc, 0x25, 0xb4, 0xf4, 0x7b, 0xed, 0xe0, 0x86, 0x5f, 0xc5, 0x34, 0x1c, 0xc3, 0xfc, + 0x47, 0x11, 0x7a, 0x88, 0x01, 0x97, 0x71, 0x4d, 0xad, 0x5c, 0x8d, 0x06, 0xb9, 0x62, 0x7c, 0x01, 0x90, 0x32, 0x18, + 0x60, 0xac, 0x59, 0x28, 0x9e, 0x7f, 0xc7, 0x1f, 0x82, 0x08, 0xf5, 0x6a, 0x1f, 0xfb, 0xd1, 0x0d, 0x31, 0xa6, 0x36, + 0x3e, 0x26, 0x38, 0xf8, 0xd8, 0x5a, 0x69, 0xdf, 0x74, 0x95, 0x35, 0xc2, 0x09, 0xb4, 0xe0, 0xca, 0x3c, 0x88, 0x0f, + 0xa7, 0x36, 0xff, 0x2f, 0xc5, 0xaa, 0x1e, 0xbb, 0xfb, 0xfb, 0x23, 0x5c, 0x0f, 0x9d, 0x72, 0x90, 0x57, 0xb8, 0x00, + 0x2e, 0xbb, 0xea, 0x9c, 0x57, 0xbe, 0xb2, 0x4c, 0xfe, 0x16, 0x0e, 0x96, 0x0f, 0xca, 0x71, 0x3a, 0xfd, 0xcb, 0xb5, + 0x8b, 0xa3, 0x3d, 0x98, 0x4f, 0xd3, 0x30, 0xfe, 0x49, 0x2c, 0x7d, 0x5e, 0xd0, 0xd9, 0x6f, 0x48, 0x1b, 0x3f, 0x2e, + 0xb2, 0x7d, 0xe8, 0xba, 0x3c, 0x7f, 0x8d, 0xb7, 0xe7, 0x76, 0x4d, 0x9b, 0xce, 0xf7, 0x3f, 0xa5, 0xb3, 0x71, 0xcf, + 0xf8, 0x6f, 0xf4, 0x44, 0x27, 0xdf, 0x18, 0x7f, 0x48, 0x6b, 0xe3, 0xd3, 0x20, 0xbe, 0x6c, 0x0b, 0xb2, 0x87, 0x73, + 0x78, 0x1a, 0xce, 0x17, 0x94, 0x5f, 0x64, 0x71, 0xd1, 0x9f, 0xbe, 0xc6, 0x8b, 0x73, 0xcf, 0xcb, 0xb5, 0xd6, 0x7c, + 0x6a, 0x6d, 0xc0, 0xd6, 0x02, 0xe7, 0x46, 0xed, 0x96, 0x49, 0xaa, 0x56, 0xde, 0x88, 0xe9, 0x6c, 0x1a, 0x51, 0x07, + 0xfb, 0x7d, 0x7b, 0xdc, 0xf1, 0x40, 0xff, 0xb3, 0x79, 0x5d, 0x71, 0x6d, 0xd5, 0x4d, 0x77, 0x56, 0xe0, 0x0d, 0x93, + 0xa5, 0x23, 0x3c, 0x2b, 0x88, 0x34, 0xd2, 0x07, 0xa4, 0x65, 0x6d, 0xdb, 0x12, 0x43, 0xbb, 0x59, 0xc9, 0x34, 0x71, + 0x5b, 0x33, 0x5c, 0xe2, 0x4c, 0x08, 0x10, 0x49, 0xa6, 0x18, 0xba, 0xd6, 0x0c, 0x90, 0xde, 0x41, 0x49, 0x88, 0x65, + 0xbf, 0x04, 0x8a, 0x25, 0x83, 0x4f, 0xff, 0x61, 0x45, 0x4c, 0x8e, 0x37, 0x74, 0x70, 0x2a, 0x68, 0xf6, 0xd8, 0x8e, + 0xb9, 0x08, 0xc2, 0x97, 0x28, 0xf4, 0x4c, 0x63, 0x27, 0x57, 0x6d, 0x8e, 0x9e, 0xd8, 0x09, 0x6b, 0x1a, 0x05, 0x55, + 0xbb, 0xdf, 0xde, 0x2a, 0x15, 0x37, 0x57, 0x9c, 0xcf, 0x60, 0x8c, 0x27, 0x1d, 0x41, 0xe4, 0xcf, 0xfe, 0x02, 0xca, + 0xd0, 0x25, 0x8c, 0xb2, 0x65, 0xde, 0x8f, 0x26, 0xb7, 0x52, 0xc7, 0x92, 0xd0, 0xd4, 0xf5, 0xea, 0x8a, 0x54, 0xe1, + 0xfe, 0x2e, 0xfc, 0xb3, 0x06, 0x71, 0x87, 0x38, 0x87, 0x64, 0x01, 0x51, 0x3d, 0x63, 0x25, 0xc5, 0x20, 0x66, 0x36, + 0x28, 0x61, 0x4a, 0x9f, 0xb4, 0xda, 0x6a, 0x9d, 0x1c, 0x7b, 0x5c, 0xae, 0xea, 0x42, 0xd6, 0x2d, 0x7f, 0xa4, 0x45, + 0x22, 0x2d, 0x70, 0x85, 0xef, 0x2c, 0x00, 0x5d, 0x09, 0xe0, 0x29, 0x04, 0x72, 0x98, 0x84, 0xbf, 0x95, 0x55, 0xf4, + 0xe0, 0xfe, 0x6d, 0x98, 0x5b, 0x8e, 0x40, 0xc2, 0x87, 0xb9, 0x69, 0x8d, 0x3a, 0x8d, 0x4c, 0x6b, 0xd8, 0xba, 0x04, + 0xe2, 0x24, 0x41, 0x0b, 0x35, 0xf6, 0x71, 0x28, 0x1c, 0x7a, 0x1e, 0xb9, 0x49, 0xae, 0xe5, 0xca, 0x97, 0xa2, 0x39, + 0x89, 0x3d, 0x52, 0xd1, 0xb1, 0x9f, 0x91, 0xe3, 0xbc, 0x10, 0xe4, 0xe2, 0x48, 0x9a, 0x9e, 0x6a, 0x92, 0x43, 0x9b, + 0x0c, 0x2a, 0x94, 0xdb, 0x2c, 0x68, 0x73, 0x1b, 0xb1, 0xbf, 0x8e, 0x88, 0x0b, 0x1b, 0x40, 0x22, 0x9c, 0x5c, 0x55, + 0xfd, 0x2d, 0xb9, 0xbe, 0x6e, 0x7c, 0x55, 0x0b, 0x19, 0x0f, 0x28, 0x19, 0x4e, 0xea, 0xed, 0x19, 0x0a, 0xc3, 0xc5, + 0xfc, 0xb4, 0xbe, 0xb0, 0xd6, 0xd4, 0x6e, 0xa5, 0x48, 0x0a, 0x43, 0x9a, 0xf2, 0x44, 0xe2, 0x87, 0x65, 0x77, 0xb1, + 0x49, 0xc5, 0x8a, 0xc0, 0xfb, 0x9c, 0xf9, 0x73, 0xe1, 0xd4, 0x1a, 0xff, 0x21, 0xc0, 0xad, 0x39, 0x38, 0xa8, 0xbf, + 0x8b, 0xdc, 0x64, 0xab, 0x1e, 0x38, 0x4d, 0x7e, 0x74, 0x45, 0x3f, 0x8b, 0x62, 0xdc, 0x83, 0x41, 0x9e, 0xb3, 0x46, + 0x1c, 0x27, 0x5e, 0xa1, 0xc8, 0xa6, 0x12, 0xba, 0xdb, 0x75, 0xa6, 0x88, 0xeb, 0x90, 0xa3, 0x19, 0x72, 0x72, 0x38, + 0x4e, 0x5a, 0xcd, 0xa3, 0xb2, 0x49, 0x12, 0x9e, 0xe2, 0x47, 0xee, 0x13, 0x8a, 0x5d, 0x9f, 0x85, 0x32, 0x23, 0xce, + 0x19, 0x67, 0xdb, 0x0b, 0xae, 0xd1, 0x5b, 0x73, 0x90, 0x8e, 0x1d, 0xf6, 0xfc, 0x89, 0x22, 0x4c, 0x21, 0x65, 0xa7, + 0x26, 0x6d, 0xd2, 0x55, 0x97, 0x71, 0x9f, 0x0e, 0x75, 0x1c, 0x52, 0x3d, 0x3b, 0x1c, 0xea, 0xa5, 0x2d, 0x4f, 0x1c, + 0xe2, 0xca, 0x87, 0xfe, 0x38, 0xf2, 0xeb, 0xc2, 0x7a, 0x51, 0xc8, 0xf8, 0xa4, 0xd0, 0x49, 0x4b, 0x95, 0x78, 0x00, + 0xb7, 0x95, 0x4d, 0x6f, 0xcb, 0xd4, 0xda, 0xd0, 0x71, 0xe9, 0x6f, 0x02, 0xa4, 0x90, 0xc5, 0xa9, 0x5c, 0x0a, 0xe5, + 0x9a, 0xf1, 0xe2, 0xb0, 0xe2, 0xf6, 0xd5, 0x7d, 0xda, 0x57, 0x14, 0x1d, 0x20, 0x10, 0x11, 0x5a, 0x01, 0xc2, 0x17, + 0x26, 0x70, 0x75, 0x95, 0xa5, 0xb0, 0x8e, 0x09, 0xc1, 0x53, 0xf8, 0x46, 0x6a, 0xa5, 0x55, 0x46, 0xc4, 0x05, 0xdb, + 0x8d, 0x50, 0xf6, 0x00, 0x1a, 0x10, 0xc3, 0x49, 0xfc, 0x2f, 0x4f, 0x55, 0xcb, 0xb4, 0x5b, 0xc9, 0xa5, 0x91, 0x76, + 0xa3, 0x2d, 0xde, 0x98, 0x56, 0x14, 0x14, 0x13, 0x92, 0xbe, 0xd2, 0xa0, 0xd5, 0xb1, 0xf5, 0x9b, 0xbd, 0x5e, 0xbc, + 0x3a, 0xbe, 0xe3, 0xe4, 0x60, 0x94, 0x63, 0xc9, 0x20, 0x53, 0x11, 0xca, 0xc5, 0x45, 0xd8, 0x7a, 0xd8, 0xd9, 0x16, + 0xda, 0x69, 0xd0, 0x71, 0xb7, 0x82, 0x1a, 0x84, 0xf9, 0xd0, 0x73, 0xa7, 0xdb, 0x3e, 0x5d, 0x19, 0xb7, 0x8b, 0x78, + 0x95, 0xe3, 0x54, 0x55, 0x09, 0xa4, 0x64, 0xf3, 0x31, 0x48, 0x95, 0x24, 0x47, 0xa6, 0x0a, 0xeb, 0x1e, 0x6c, 0xef, + 0x98, 0x30, 0x09, 0x79, 0xe4, 0x7d, 0xf8, 0x27, 0x84, 0x5a, 0x8a, 0x7e, 0xdb, 0xf6, 0x6d, 0xc9, 0xe1, 0x95, 0xa3, + 0x55, 0x83, 0x80, 0xd8, 0x88, 0x00, 0x35, 0x8f, 0x8f, 0xf6, 0x26, 0x6e, 0xbd, 0xa3, 0x72, 0x37, 0x35, 0x7e, 0xcf, + 0x56, 0x76, 0x1e, 0xf9, 0x1d, 0xaf, 0xec, 0xe3, 0x42, 0x15, 0xec, 0x92, 0x12, 0x3d, 0xc9, 0xfa, 0xf1, 0xca, 0xa6, + 0x35, 0xfb, 0x79, 0x7d, 0x41, 0xc8, 0xe6, 0x55, 0xf6, 0xc8, 0xab, 0x42, 0xbd, 0x18, 0x09, 0x63, 0xaa, 0x43, 0x78, + 0xe3, 0xc8, 0xd8, 0x9f, 0x17, 0x32, 0x8d, 0x81, 0x05, 0x28, 0xb4, 0xd4, 0xbb, 0x11, 0x4f, 0x8f, 0x65, 0x56, 0xa4, + 0x75, 0x27, 0x5c, 0xc5, 0x7a, 0x09, 0x3f, 0xba, 0x0d, 0x58, 0x58, 0x29, 0xdd, 0x22, 0x97, 0x77, 0x75, 0x91, 0xf5, + 0xd9, 0x6b, 0x13, 0x43, 0xef, 0x92, 0x42, 0x85, 0xb2, 0x63, 0xca, 0x8a, 0xf9, 0x0a, 0x69, 0x8e, 0x05, 0x6f, 0x42, + 0xfd, 0xbc, 0x2d, 0x7f, 0x87, 0x2a, 0x16, 0x7f, 0x5d, 0xd1, 0x5b, 0xa7, 0x6a, 0xb6, 0xcf, 0x14, 0x33, 0x65, 0x3b, + 0x17, 0xee, 0x8b, 0xfb, 0x8d, 0x6f, 0x88, 0xa7, 0x62, 0xd5, 0x77, 0x45, 0x71, 0xe4, 0xa0, 0xc9, 0x20, 0xaa, 0x93, + 0xb5, 0x10, 0x77, 0x5d, 0x19, 0x92, 0x70, 0xe7, 0x09, 0x33, 0x48, 0xe7, 0xb0, 0x71, 0x55, 0x23, 0xd3, 0xa0, 0xe6, + 0x40, 0x9d, 0x54, 0x83, 0x15, 0xb4, 0x42, 0x52, 0xf6, 0x14, 0x33, 0x51, 0x07, 0xee, 0xf7, 0x9a, 0xfd, 0x3f, 0xa0, + 0x12, 0x7d, 0xdd, 0xf5, 0x57, 0x7d, 0x0b, 0xe7, 0x82, 0x05, 0x4b, 0x2a, 0xfb, 0x72, 0x5b, 0x6f, 0xfc, 0x29, 0x6c, + 0xea, 0xd4, 0xad, 0xbb, 0x5d, 0xea, 0x72, 0x9a, 0x0d, 0xce, 0x3b, 0x47, 0x31, 0x77, 0x0a, 0x1f, 0x62, 0x2e, 0x2f, + 0xd9, 0x44, 0x25, 0x57, 0x71, 0xe2, 0x45, 0x0d, 0x00, 0xf3, 0x0e, 0x90, 0x9c, 0x29, 0x61, 0x94, 0xf8, 0x73, 0x52, + 0x01, 0xd5, 0x94, 0xae, 0xb3, 0xb3, 0xee, 0x17, 0x7b, 0xfe, 0x8a, 0xbc, 0xbe, 0x72, 0x0c, 0xea, 0xe6, 0xbc, 0x20, + 0xa7, 0x98, 0x5f, 0x34, 0x25, 0x63, 0x4f, 0xb7, 0xad, 0xaa, 0x93, 0xb5, 0xcb, 0x8b, 0xda, 0x44, 0x89, 0x74, 0xc9, + 0x0d, 0x2f, 0xf5, 0xb6, 0xbc, 0x66, 0xcb, 0x93, 0x75, 0x7a, 0x2a, 0xd6, 0xd8, 0xbe, 0x08, 0x63, 0x7d, 0x18, 0x5d, + 0xe9, 0x49, 0x07, 0x39, 0x2d, 0x4b, 0x4b, 0xb9, 0x8b, 0x9c, 0x5b, 0xba, 0x5d, 0x3a, 0xcc, 0x8f, 0x19, 0x6b, 0x6f, + 0x8d, 0x8d, 0xad, 0xe5, 0xe6, 0xbf, 0xae, 0x6c, 0xc3, 0x54, 0xa1, 0x68, 0x01, 0x4c, 0xcf, 0x26, 0x87, 0xf5, 0x01, + 0x35, 0x53, 0x6f, 0x51, 0xbb, 0xe2, 0xf5, 0x4e, 0xf4, 0xbc, 0xfb, 0x0e, 0x6a, 0x86, 0x5a, 0x8f, 0x92, 0x68, 0xa9, + 0x7d, 0xef, 0x5b, 0x4a, 0x5b, 0xe6, 0xb1, 0xf2, 0xa2, 0xd4, 0x43, 0xfd, 0xea, 0x8f, 0xd3, 0xda, 0xb8, 0x27, 0xbc, + 0x65, 0xa3, 0xae, 0xe2, 0x63, 0x9f, 0xe7, 0xc2, 0xcc, 0x2c, 0x3e, 0x97, 0xd6, 0x83, 0x5f, 0x4e, 0xbb, 0x99, 0x39, + 0x3d, 0xbe, 0xa7, 0x83, 0xc4, 0x5c, 0x7a, 0x2f, 0x43, 0xa0, 0x68, 0x85, 0x66, 0x1d, 0x35, 0xcc, 0x79, 0x9f, 0x3a, + 0xc6, 0xcf, 0x7b, 0x4c, 0xc9, 0x1d, 0x3f, 0xe3, 0xf5, 0xd0, 0xa6, 0x9f, 0x3e, 0x66, 0xce, 0x87, 0x89, 0xf0, 0x6a, + 0x57, 0xa3, 0x13, 0x56, 0xe0, 0xeb, 0xa5, 0xc7, 0xc9, 0xa7, 0xbd, 0xaa, 0x5a, 0x5a, 0xdf, 0x7f, 0x6b, 0x62, 0x80, + 0xa9, 0x52, 0x7e, 0x45, 0xfb, 0xb9, 0xc6, 0x62, 0x86, 0x97, 0x74, 0xd9, 0xcb, 0x00}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index 1f61b19fb5e..b7c15df32ba 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -constexpr uint8_t INDEX_GZ[] PROGMEM = { +const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -3634,58 +3634,58 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x36, 0x16, 0x43, 0x14, 0xa2, 0x85, 0xb1, 0x9f, 0x44, 0x7a, 0x6a, 0xf7, 0x8b, 0xa8, 0x63, 0x84, 0xe7, 0x2e, 0x8e, 0x40, 0xbb, 0x60, 0xb2, 0xd8, 0xed, 0x3a, 0x06, 0xb0, 0x93, 0x12, 0x46, 0xf3, 0x4c, 0x91, 0xc8, 0x45, 0x3d, 0x95, 0x78, 0xf0, 0xa9, 0x53, 0x8d, 0x99, 0x83, 0xf0, 0x54, 0x31, 0xd4, 0xc0, 0xc7, 0x7a, 0xaf, 0x4d, 0xc8, 0x99, 0xff, - 0xaf, 0xbd, 0x67, 0x5b, 0x6e, 0xdb, 0x48, 0xf6, 0x3d, 0x5f, 0x01, 0xc1, 0x5e, 0x1b, 0xb0, 0x01, 0x08, 0x20, 0x75, - 0xa1, 0x49, 0x81, 0x4a, 0x6c, 0xcb, 0x49, 0x76, 0x95, 0x38, 0x65, 0x2b, 0xde, 0x8b, 0x56, 0x25, 0x82, 0xe4, 0x90, - 0xc4, 0x1a, 0x04, 0x58, 0x00, 0x28, 0x4a, 0xa1, 0xb1, 0xdf, 0xb2, 0x9f, 0x70, 0xbe, 0x61, 0xbf, 0xec, 0x54, 0x77, - 0xcf, 0x00, 0x83, 0x0b, 0x29, 0x2a, 0x76, 0xb2, 0x7b, 0xaa, 0x4e, 0x25, 0xb6, 0x89, 0xc1, 0xcc, 0xa0, 0xe7, 0xd6, - 0xdd, 0xd3, 0xd7, 0x32, 0xa5, 0xb4, 0x2b, 0xf8, 0x57, 0x58, 0x21, 0x57, 0x4a, 0x69, 0xcb, 0x81, 0x98, 0xd3, 0xed, - 0xe3, 0xaa, 0x81, 0x55, 0xa1, 0xb8, 0x27, 0x83, 0x99, 0x60, 0x65, 0xd7, 0x89, 0x79, 0x98, 0x75, 0x69, 0xc3, 0x1b, - 0x81, 0x0b, 0xa6, 0x87, 0x1b, 0x6a, 0x8d, 0xbb, 0x5e, 0x29, 0x14, 0x66, 0x5c, 0x9e, 0xd4, 0x13, 0xaf, 0xfc, 0x0c, - 0x5b, 0xba, 0x52, 0x05, 0x7c, 0x63, 0x2a, 0x95, 0x40, 0x0a, 0x16, 0x9c, 0xd2, 0xe7, 0xad, 0x34, 0x3a, 0x8f, 0x56, - 0x42, 0x70, 0x7c, 0xe2, 0x35, 0x14, 0xe2, 0x39, 0xe9, 0x8e, 0x4e, 0x02, 0xfa, 0xe1, 0x64, 0x1b, 0x28, 0x40, 0x56, - 0x4c, 0x70, 0xce, 0xb6, 0x0e, 0xe8, 0x32, 0x75, 0xfd, 0x78, 0x2d, 0xb6, 0x5c, 0x36, 0xf0, 0xf3, 0xb4, 0xb0, 0x25, - 0x96, 0x23, 0xe3, 0x53, 0x2f, 0x47, 0x21, 0x6d, 0xa8, 0xc6, 0xf7, 0x87, 0xeb, 0x37, 0xf2, 0x2d, 0x16, 0x3d, 0x32, - 0xa2, 0xe9, 0xcd, 0x55, 0xd8, 0x2d, 0x1b, 0x69, 0x4d, 0x80, 0x91, 0xe0, 0x49, 0x0c, 0x01, 0x03, 0x33, 0x23, 0xea, - 0xff, 0x36, 0xae, 0xa2, 0x7e, 0xb4, 0xbf, 0xcb, 0x91, 0xff, 0x4f, 0x6f, 0xdf, 0x5f, 0x80, 0x5e, 0xcb, 0x43, 0x45, - 0xf4, 0x5a, 0xe5, 0x36, 0x2c, 0x26, 0x68, 0x8a, 0xd4, 0xae, 0xea, 0x2d, 0x80, 0x3a, 0xe3, 0x8d, 0x61, 0xff, 0xd6, - 0x5c, 0xad, 0x56, 0x26, 0x58, 0xb4, 0x9a, 0xcb, 0x38, 0x20, 0xee, 0x70, 0xac, 0x66, 0x02, 0xa9, 0xb3, 0x0a, 0x52, - 0x87, 0x70, 0xb8, 0x3c, 0x9f, 0xca, 0xfb, 0x59, 0xb4, 0xfa, 0x26, 0x08, 0x64, 0xb1, 0x8d, 0x60, 0xe2, 0xb8, 0x24, - 0xa3, 0x84, 0x0c, 0x34, 0xd0, 0x3e, 0x59, 0x7e, 0x72, 0xcd, 0xed, 0x05, 0xc6, 0xd7, 0xc3, 0xbb, 0x6b, 0xae, 0x93, - 0xc8, 0xe3, 0x11, 0xbf, 0x1f, 0x9c, 0x8c, 0xfd, 0x1b, 0x05, 0x39, 0x4d, 0x57, 0x05, 0x67, 0xae, 0x80, 0x0d, 0x97, - 0x69, 0x1a, 0x85, 0x66, 0x1c, 0xad, 0xd4, 0xfe, 0x09, 0x3d, 0x88, 0x0a, 0x1e, 0x3d, 0xaa, 0xca, 0xd7, 0xa3, 0xc0, - 0x1f, 0x7d, 0x74, 0xd5, 0xc7, 0x6b, 0xdf, 0xed, 0x57, 0xf8, 0x49, 0x3b, 0x53, 0xfb, 0x00, 0xab, 0xf2, 0x4d, 0x10, - 0x9c, 0xec, 0x53, 0x8b, 0xfe, 0xc9, 0xfe, 0xd8, 0xbf, 0xe9, 0x4b, 0xa9, 0x61, 0xb8, 0xde, 0xd4, 0xe5, 0x21, 0x38, - 0x73, 0x4b, 0xb3, 0x04, 0x63, 0x3a, 0x8c, 0x98, 0x56, 0x5c, 0x7e, 0x21, 0xd6, 0x0c, 0xc1, 0xab, 0x8d, 0x50, 0x9c, - 0x1e, 0xc0, 0x55, 0xef, 0xd3, 0x27, 0x2d, 0xb7, 0x43, 0x9d, 0x49, 0x41, 0xda, 0x50, 0xcd, 0x87, 0x55, 0x0c, 0x8c, - 0x34, 0xa3, 0x6b, 0x22, 0x94, 0x5c, 0xa0, 0x1b, 0xa3, 0xcc, 0xc0, 0x0c, 0x3b, 0xde, 0x02, 0x34, 0x8e, 0xfc, 0xa7, - 0x74, 0x23, 0x1e, 0x41, 0x56, 0x6d, 0x09, 0x89, 0xeb, 0x92, 0xce, 0x85, 0x4e, 0x21, 0x8f, 0x13, 0x08, 0xca, 0x12, - 0xfc, 0x0e, 0xe9, 0x41, 0xb4, 0x40, 0x87, 0xac, 0x6e, 0x79, 0x70, 0x1e, 0x2f, 0x13, 0x79, 0xd4, 0xc4, 0xbc, 0x9c, - 0x96, 0x56, 0xa8, 0x5b, 0x5d, 0x2f, 0x11, 0x35, 0x72, 0x2f, 0xd9, 0xb4, 0x64, 0xa0, 0xc3, 0xd3, 0x52, 0xa3, 0x42, - 0x73, 0xc1, 0xab, 0x4f, 0x52, 0x1c, 0x31, 0x43, 0xbb, 0x4c, 0x8c, 0xe8, 0xaa, 0xa0, 0x53, 0x09, 0x21, 0xca, 0x6e, - 0x94, 0x15, 0x01, 0x9c, 0x69, 0xd5, 0xfb, 0x8f, 0xd7, 0x21, 0x12, 0xb6, 0xc4, 0xed, 0x97, 0xf7, 0x41, 0xea, 0x0d, - 0x4d, 0xda, 0xcc, 0xaa, 0xf2, 0xf5, 0x78, 0x18, 0xe4, 0x8b, 0x4d, 0x87, 0x60, 0xe6, 0x85, 0xe3, 0x80, 0x5d, 0x78, - 0xc3, 0xef, 0xb0, 0xce, 0xeb, 0x61, 0xf0, 0x0a, 0x2a, 0x64, 0x6a, 0xff, 0xf1, 0x9a, 0x48, 0x77, 0x13, 0xc2, 0xce, - 0x68, 0x0b, 0x54, 0xbf, 0xc3, 0x53, 0x2e, 0xb1, 0x98, 0x5a, 0x23, 0xb0, 0x44, 0x6e, 0x29, 0x8e, 0x6d, 0x19, 0x32, - 0x9e, 0xf2, 0x07, 0xf6, 0xa6, 0xc2, 0x4f, 0x2d, 0xc0, 0x15, 0x89, 0x13, 0x2c, 0xef, 0x4c, 0x19, 0x58, 0x22, 0xab, - 0xef, 0xa2, 0x95, 0x80, 0x94, 0x4f, 0x00, 0x85, 0xa8, 0x3c, 0x7d, 0x3f, 0x38, 0x91, 0xd5, 0x42, 0x28, 0x3b, 0xa7, - 0x7e, 0xe1, 0x57, 0xa6, 0x2a, 0x45, 0x02, 0xa8, 0xc5, 0xad, 0xda, 0x3f, 0xd9, 0x97, 0x6b, 0xf7, 0x07, 0xdd, 0x33, - 0x69, 0x70, 0xd8, 0xab, 0xb8, 0x37, 0x5f, 0x16, 0x0f, 0xd9, 0x95, 0x02, 0xb7, 0xe4, 0x0c, 0x4a, 0x60, 0x8e, 0xca, - 0x4d, 0x6a, 0xe4, 0x07, 0x52, 0x26, 0x16, 0x04, 0x8a, 0x76, 0x8f, 0xc0, 0x8f, 0x91, 0xde, 0xcd, 0x97, 0x90, 0x2c, - 0x33, 0x45, 0x6f, 0x03, 0xfe, 0x6f, 0x31, 0x25, 0x28, 0xe9, 0x66, 0x61, 0x12, 0xc5, 0x2a, 0x0c, 0xb3, 0x9a, 0x37, - 0x49, 0x91, 0xf2, 0xb5, 0xe1, 0x80, 0x1b, 0xc9, 0x2a, 0x4c, 0xd8, 0x7e, 0xb5, 0xa9, 0x34, 0xee, 0x81, 0x5e, 0xfc, - 0x50, 0xf8, 0x60, 0x2a, 0x48, 0x2b, 0x07, 0x70, 0x73, 0x3e, 0xaa, 0xcb, 0xc7, 0xbe, 0xf1, 0xe7, 0xc8, 0x18, 0x7a, - 0xc6, 0xb5, 0x67, 0xfc, 0x10, 0x5e, 0x65, 0x8d, 0x8b, 0x97, 0xe7, 0x92, 0x33, 0x58, 0x4f, 0x83, 0x08, 0x4c, 0xe5, - 0x4b, 0x85, 0x6f, 0x71, 0x9b, 0x91, 0x0b, 0x2f, 0x9e, 0x32, 0x91, 0xc2, 0x4d, 0xbc, 0x15, 0xb2, 0x03, 0x5d, 0x9a, - 0x16, 0x08, 0x4f, 0xb6, 0xc7, 0x4d, 0xeb, 0x7c, 0x6b, 0x94, 0xc6, 0xc1, 0x9f, 0xd8, 0x1d, 0xb0, 0x59, 0x49, 0x1a, - 0x2d, 0x40, 0x66, 0xe5, 0x4d, 0xb9, 0x0e, 0xc2, 0xd0, 0xd8, 0x6e, 0x9f, 0xfb, 0xf4, 0x89, 0x49, 0x59, 0xc5, 0xd2, - 0x68, 0x3a, 0x0d, 0x98, 0x26, 0x65, 0x1f, 0xcb, 0x3f, 0x73, 0xba, 0x67, 0x8b, 0xc8, 0xd5, 0x7a, 0xb6, 0xe9, 0x60, + 0xaf, 0xbd, 0xa7, 0xdd, 0x6e, 0x13, 0x49, 0xf6, 0xff, 0x3c, 0x05, 0x26, 0xd9, 0x04, 0x12, 0xc0, 0x20, 0xf9, 0x43, + 0x91, 0x8c, 0x3c, 0x93, 0xc4, 0x99, 0x8f, 0xf5, 0x4c, 0xe6, 0x24, 0x9e, 0xec, 0xbd, 0xeb, 0xf5, 0xb1, 0x90, 0xd4, + 0x92, 0xd8, 0x20, 0xd0, 0x01, 0x64, 0xd9, 0xa3, 0xb0, 0xcf, 0xb2, 0x8f, 0x70, 0x9f, 0x61, 0x9f, 0xec, 0x9e, 0xaa, + 0xea, 0x86, 0x06, 0x81, 0x2c, 0x4f, 0x32, 0xb3, 0x7b, 0xcf, 0xb9, 0x67, 0x26, 0x89, 0x68, 0xba, 0x9b, 0xea, 0xaf, + 0xaa, 0xea, 0xfa, 0x2c, 0x53, 0x4a, 0xbb, 0x82, 0x7f, 0x85, 0x15, 0x72, 0xa5, 0x94, 0xb6, 0x1c, 0x88, 0x39, 0xdd, + 0x3e, 0xae, 0x1a, 0x58, 0x15, 0x8a, 0x7b, 0x32, 0x98, 0x09, 0x56, 0x76, 0x9d, 0x98, 0x87, 0x59, 0x97, 0x36, 0xbc, + 0x11, 0xb8, 0x60, 0x7a, 0xd8, 0x50, 0x6b, 0xdc, 0xf5, 0x4a, 0xa1, 0x30, 0xe3, 0xf2, 0xa4, 0x9e, 0x78, 0xe5, 0x67, + 0xd8, 0xd2, 0x95, 0x2a, 0xe0, 0x1b, 0x53, 0xa9, 0x04, 0x52, 0xb0, 0xe0, 0x94, 0x3e, 0x6f, 0xa5, 0xd1, 0x79, 0xb4, + 0x12, 0x82, 0xe3, 0x13, 0xaf, 0xa6, 0x10, 0xcf, 0x49, 0x77, 0x74, 0x12, 0xd0, 0x0f, 0x27, 0x6b, 0xa0, 0x00, 0x59, + 0x31, 0xc1, 0x39, 0xdb, 0x3a, 0xa0, 0xcb, 0xd4, 0xf5, 0xe3, 0xb5, 0xd8, 0x72, 0xd9, 0xc0, 0xcf, 0xd3, 0xc2, 0x96, + 0x58, 0x8e, 0x8c, 0x4f, 0xbd, 0x1c, 0x85, 0xb4, 0xa6, 0x1a, 0xdf, 0x1f, 0xae, 0x5f, 0xcb, 0xb7, 0x58, 0xf4, 0xc8, + 0x88, 0xa6, 0xd7, 0x57, 0x61, 0xb7, 0x6c, 0xa4, 0xd5, 0x01, 0x46, 0x82, 0x27, 0x31, 0x04, 0x0c, 0xcc, 0x8c, 0xa8, + 0xff, 0xdb, 0xb8, 0x8a, 0xfa, 0xd1, 0xfe, 0x2e, 0x47, 0xfe, 0x3f, 0xbf, 0x7d, 0x7f, 0x01, 0x7a, 0x2d, 0x0f, 0x15, + 0xd1, 0x6b, 0x95, 0xdb, 0xb0, 0x98, 0xa0, 0x29, 0x52, 0xbb, 0xaa, 0xb7, 0x00, 0xea, 0x8c, 0x37, 0x86, 0xfd, 0x5b, + 0x73, 0xb5, 0x5a, 0x99, 0x60, 0xd1, 0x6a, 0x2e, 0xe3, 0x80, 0xb8, 0xc3, 0xb1, 0x9a, 0x09, 0xa4, 0xce, 0x2a, 0x48, + 0x1d, 0xc2, 0xe1, 0xf2, 0x7c, 0x2a, 0xef, 0x67, 0xd1, 0xea, 0x9b, 0x20, 0x90, 0xc5, 0x36, 0x82, 0x89, 0xe3, 0x92, + 0x8c, 0x12, 0x32, 0xd0, 0x40, 0xfb, 0x64, 0xf9, 0xc9, 0x35, 0xb7, 0x17, 0x18, 0x5f, 0x0f, 0xef, 0xae, 0xb9, 0x4e, + 0x22, 0x8f, 0x47, 0xfc, 0x7e, 0x70, 0x32, 0xf6, 0x6f, 0x14, 0xe4, 0x34, 0x5d, 0x15, 0x9c, 0xb9, 0x02, 0x36, 0x5c, + 0xa6, 0x69, 0x14, 0x9a, 0x71, 0xb4, 0x52, 0xfb, 0x27, 0xf4, 0x20, 0x2a, 0x78, 0xf4, 0xa8, 0x2a, 0x5f, 0x8f, 0x02, + 0x7f, 0xf4, 0xd1, 0x55, 0x1f, 0xaf, 0x7d, 0xb7, 0x5f, 0xe1, 0x27, 0xed, 0x4c, 0xed, 0x03, 0xac, 0xca, 0x37, 0x41, + 0x70, 0xb2, 0x4f, 0x2d, 0xfa, 0x27, 0xfb, 0x63, 0xff, 0xa6, 0x2f, 0xa5, 0x86, 0xe1, 0x7a, 0x53, 0x97, 0x87, 0xe0, + 0xcc, 0x2d, 0xcd, 0x12, 0x8c, 0xe9, 0x30, 0x62, 0x5a, 0x71, 0xf9, 0x85, 0x58, 0x33, 0x04, 0xaf, 0x36, 0x42, 0x71, + 0x7a, 0x00, 0x57, 0xbd, 0x4f, 0x9f, 0xb4, 0xdc, 0x0e, 0x75, 0x26, 0x05, 0x69, 0x43, 0x35, 0x1f, 0x56, 0x31, 0x30, + 0xd2, 0x8c, 0xae, 0x89, 0x50, 0x72, 0x81, 0x6e, 0x8c, 0x32, 0x03, 0x33, 0xec, 0x78, 0x0b, 0xd0, 0x38, 0xf2, 0x9f, + 0xd2, 0x8d, 0x78, 0x04, 0x59, 0xb5, 0x25, 0x24, 0xae, 0x4b, 0x3a, 0x17, 0x3a, 0x85, 0x3c, 0x4e, 0x20, 0x28, 0x4b, + 0xf0, 0x3b, 0xa4, 0x07, 0xd1, 0x02, 0x1d, 0xb2, 0xba, 0xe5, 0xc1, 0x79, 0xbc, 0x4c, 0xe4, 0x51, 0x13, 0xf3, 0x72, + 0x5a, 0x5a, 0xa1, 0x6e, 0x75, 0xbd, 0x44, 0xd4, 0xc8, 0xbd, 0xa4, 0x69, 0xc9, 0x40, 0x87, 0xa7, 0xa5, 0x46, 0x85, + 0xe6, 0x82, 0x57, 0x9f, 0xa4, 0x38, 0x62, 0x86, 0x76, 0x99, 0x18, 0xd1, 0x55, 0x41, 0xa7, 0x12, 0x42, 0x94, 0xdd, + 0x28, 0x2b, 0x02, 0x38, 0xd3, 0xaa, 0xf7, 0x1f, 0xaf, 0x43, 0x24, 0x6c, 0x89, 0xdb, 0x2f, 0xef, 0x83, 0xd4, 0x1b, + 0x9a, 0xb4, 0x99, 0x55, 0xe5, 0xeb, 0xf1, 0x30, 0xc8, 0x17, 0x9b, 0x0e, 0xc1, 0xcc, 0x0b, 0xc7, 0x01, 0xbb, 0xf0, + 0x86, 0xdf, 0x61, 0x9d, 0xd7, 0xc3, 0xe0, 0x15, 0x54, 0xc8, 0xd4, 0xfe, 0xe3, 0x35, 0x91, 0xee, 0x3a, 0x84, 0x9d, + 0xd1, 0x16, 0xa8, 0x7e, 0x87, 0xa7, 0x5c, 0x62, 0x31, 0xb5, 0x46, 0x60, 0x89, 0xdc, 0x52, 0x1c, 0xdb, 0x32, 0x64, + 0x3c, 0xe5, 0x0f, 0xec, 0x4d, 0x85, 0x9f, 0x5a, 0x80, 0x2b, 0x12, 0x27, 0x58, 0xde, 0x99, 0x32, 0xb0, 0x44, 0x56, + 0xdf, 0x45, 0x2b, 0x01, 0x29, 0x9f, 0x00, 0x0a, 0x51, 0x79, 0xfa, 0x7e, 0x70, 0x22, 0xab, 0x85, 0x50, 0x76, 0x4e, + 0xfd, 0xc2, 0xaf, 0x4c, 0x55, 0x8a, 0x04, 0x50, 0x8b, 0x5b, 0xb5, 0x7f, 0xb2, 0x2f, 0xd7, 0xee, 0x0f, 0xba, 0x67, + 0xd2, 0xe0, 0xb0, 0x57, 0x71, 0x6f, 0xbe, 0x2c, 0x1e, 0xb2, 0x2b, 0x05, 0x6e, 0xc9, 0x19, 0x94, 0xc0, 0x1c, 0x95, + 0x9b, 0x6c, 0x90, 0x1f, 0x48, 0x99, 0x58, 0x10, 0x28, 0xda, 0x3d, 0x02, 0x3f, 0x46, 0x7a, 0x37, 0x5f, 0x42, 0xb2, + 0xcc, 0x14, 0xbd, 0x0d, 0xf8, 0xbf, 0xc5, 0x94, 0xa0, 0xa4, 0x9b, 0x85, 0x49, 0x14, 0xab, 0x30, 0xcc, 0x6a, 0xde, + 0x24, 0x45, 0xca, 0xd7, 0x86, 0x03, 0xae, 0x25, 0xab, 0x30, 0x61, 0xfb, 0xd5, 0xa6, 0xd2, 0xb8, 0x07, 0x7a, 0xf1, + 0x43, 0xe1, 0x83, 0xa9, 0x20, 0xad, 0x1c, 0xc0, 0xe6, 0x7c, 0x54, 0x97, 0x8f, 0x7d, 0xe3, 0x2f, 0x91, 0x31, 0xf4, + 0x8c, 0x6b, 0xcf, 0xf8, 0x31, 0xbc, 0xca, 0x6a, 0x17, 0x2f, 0xcf, 0x25, 0x67, 0xb0, 0x9e, 0x06, 0x11, 0x98, 0xca, + 0x97, 0x0a, 0xdf, 0xe2, 0x36, 0x23, 0x17, 0x5e, 0x3c, 0x65, 0x22, 0x85, 0x9b, 0x78, 0x2b, 0x64, 0x07, 0xba, 0x34, + 0x2d, 0x10, 0x9e, 0x6c, 0x8f, 0x9b, 0xd6, 0xf9, 0xd6, 0x28, 0x8d, 0x83, 0x3f, 0xb3, 0x3b, 0x60, 0xb3, 0x92, 0x34, + 0x5a, 0x80, 0xcc, 0xca, 0x9b, 0x72, 0x1d, 0x84, 0xa1, 0xb1, 0xdd, 0x3e, 0xf7, 0xe9, 0x13, 0x93, 0xb2, 0x8a, 0xa5, + 0xd1, 0x74, 0x1a, 0x30, 0x4d, 0xca, 0x3e, 0x96, 0x7f, 0xe6, 0x74, 0xcf, 0x16, 0x91, 0xab, 0xf5, 0xac, 0xe9, 0x60, 0x89, 0x11, 0xb3, 0x9c, 0x1b, 0x04, 0xc4, 0x45, 0xc6, 0x55, 0xc8, 0x90, 0x6b, 0xe2, 0x5c, 0x14, 0x07, 0xd7, 0x1c, 0x47, 0xcb, 0x61, 0xc0, 0x4c, 0x3c, 0x0d, 0xf0, 0xc9, 0xf5, 0x70, 0x39, 0x1c, 0x06, 0x94, 0x2e, 0x0c, 0xe2, 0xaf, 0x45, 0x09, 0xca, 0x45, 0x33, 0xbd, 0x07, 0x83, 0xb2, 0xd2, 0x2a, 0xf8, 0x60, 0x33, 0x09, 0x37, 0x07, 0xfa, 0x40, - 0x0a, 0x32, 0xd0, 0xfa, 0x99, 0x76, 0x55, 0xb8, 0xb1, 0xb0, 0x44, 0xed, 0x35, 0xb0, 0x74, 0xee, 0xa5, 0xfa, 0x1e, - 0x67, 0x58, 0xf1, 0xc2, 0xb1, 0xf2, 0x8a, 0xf6, 0xae, 0x6a, 0xa8, 0x64, 0xfa, 0xc5, 0xb3, 0xcb, 0xa9, 0x86, 0xfa, - 0xda, 0xf7, 0xa6, 0x61, 0x94, 0xa4, 0xfe, 0x48, 0xbd, 0xea, 0xbd, 0xf6, 0xb5, 0xcb, 0x79, 0xaa, 0xe9, 0x57, 0xc6, - 0xb7, 0x72, 0x1e, 0x30, 0x81, 0x29, 0x31, 0x0d, 0xd8, 0x86, 0x3a, 0xf2, 0xe9, 0xd9, 0x56, 0x4f, 0x60, 0x64, 0xac, + 0x0a, 0x32, 0xd0, 0xcd, 0x33, 0xed, 0xaa, 0x70, 0x63, 0x61, 0x89, 0xda, 0xab, 0x61, 0xe9, 0xdc, 0x4b, 0xf5, 0x3d, + 0xce, 0xb0, 0xe2, 0x85, 0x63, 0xe5, 0x15, 0xed, 0x5d, 0xd5, 0x50, 0xc9, 0xf4, 0x8b, 0x67, 0x97, 0x53, 0x0d, 0xf5, + 0xb5, 0xef, 0x4d, 0xc3, 0x28, 0x49, 0xfd, 0x91, 0x7a, 0xd5, 0x7b, 0xed, 0x6b, 0x97, 0xf3, 0x54, 0xd3, 0xaf, 0x8c, + 0x6f, 0xe5, 0x3c, 0x60, 0x02, 0x53, 0x62, 0x1a, 0xb0, 0x86, 0x3a, 0xf2, 0xe9, 0xd9, 0x56, 0x4f, 0x60, 0x64, 0xac, 0xf3, 0xad, 0x0b, 0xb5, 0x2a, 0x19, 0xc5, 0x30, 0x55, 0x24, 0x64, 0x14, 0xfb, 0x56, 0xef, 0x91, 0x10, 0xe6, 0x9b, 0xe5, 0x1a, 0x99, 0x86, 0xb4, 0x20, 0xbe, 0x18, 0x04, 0x5f, 0x78, 0x8e, 0xd2, 0xf3, 0x9e, 0xec, 0xf5, 0x50, 0x22, 0xe3, 0x83, 0x6f, 0xca, 0x1c, 0xc8, 0xe3, 0x75, 0x9a, 0x81, 0xc9, 0x61, 0x18, 0xa5, 0x0a, 0x44, 0x76, 0x83, 0x0f, @@ -3695,3994 +3695,3997 @@ constexpr uint8_t INDEX_GZ[] PROGMEM = { 0xba, 0x25, 0xf4, 0xaa, 0x09, 0x76, 0x37, 0x16, 0x31, 0xf3, 0xb9, 0x18, 0x45, 0x30, 0x60, 0x95, 0xa3, 0x07, 0xc1, 0x9a, 0x72, 0xde, 0x2a, 0x05, 0x8b, 0x6b, 0x24, 0x18, 0x80, 0xb9, 0x38, 0x8f, 0x30, 0xc8, 0xae, 0x81, 0x91, 0x84, 0x08, 0x66, 0x62, 0x8c, 0x46, 0x24, 0x27, 0x91, 0xf3, 0xc3, 0xc5, 0x32, 0xc5, 0xc8, 0xf4, 0x00, 0x00, 0xcb, 0x54, - 0x05, 0x2f, 0x8c, 0x80, 0xeb, 0x8b, 0x0b, 0x4f, 0xa6, 0x2a, 0xfe, 0xb8, 0x5e, 0xc6, 0xa5, 0x33, 0x80, 0xe3, 0x70, - 0x18, 0xa8, 0xd7, 0x81, 0xc7, 0x98, 0x0f, 0x63, 0x64, 0x14, 0x69, 0x5d, 0xb4, 0x11, 0xda, 0x3f, 0x34, 0x20, 0x90, - 0x11, 0xf5, 0xd3, 0xd3, 0x82, 0xc6, 0xc1, 0x42, 0xb4, 0xea, 0xd2, 0x30, 0x07, 0x20, 0xa3, 0x3c, 0x85, 0xb9, 0x73, - 0xe1, 0x52, 0x2f, 0x4c, 0xeb, 0xd4, 0x0b, 0x95, 0xec, 0xea, 0x06, 0x38, 0x0d, 0x83, 0xec, 0xba, 0x70, 0x74, 0x2d, - 0xc6, 0x0b, 0x5b, 0x92, 0xca, 0x15, 0xb4, 0x74, 0x73, 0xb9, 0x3d, 0xdb, 0x22, 0xf6, 0xe7, 0x5e, 0x7c, 0x47, 0xe6, - 0x6f, 0x86, 0x6c, 0x23, 0xa7, 0xab, 0x0a, 0xd1, 0x03, 0x9a, 0x00, 0x22, 0x0d, 0xaa, 0xf2, 0x75, 0x5e, 0xc6, 0xf8, - 0x68, 0x73, 0x1b, 0x20, 0xf8, 0xd6, 0xb5, 0xfa, 0x9c, 0x59, 0x24, 0x7f, 0xa4, 0x26, 0x3d, 0x2d, 0xd9, 0x30, 0xbc, - 0xa4, 0x3c, 0xbc, 0xb0, 0xbc, 0xd1, 0x70, 0x30, 0x44, 0x29, 0x08, 0x6e, 0x1c, 0x19, 0x26, 0xc1, 0x6c, 0x5e, 0x51, - 0x7a, 0xf7, 0xbb, 0x2e, 0x07, 0x83, 0xe5, 0x08, 0x61, 0x39, 0x1a, 0x44, 0xb3, 0x9e, 0x58, 0x11, 0xe0, 0x45, 0x80, + 0x05, 0x2f, 0x8c, 0x80, 0xeb, 0x8b, 0x0b, 0x4f, 0xa6, 0x2a, 0xfe, 0x78, 0xb3, 0x8c, 0x4b, 0x67, 0x00, 0xc7, 0xe1, + 0x30, 0x50, 0xaf, 0x03, 0x8f, 0x31, 0x1f, 0xc6, 0xc8, 0x28, 0xd2, 0xba, 0x68, 0x23, 0xb4, 0x7f, 0xa8, 0x41, 0x20, + 0x23, 0xea, 0xa7, 0xa7, 0x05, 0xb5, 0x83, 0x85, 0x68, 0xd5, 0xa5, 0x61, 0x0e, 0x40, 0x46, 0x79, 0x0a, 0x73, 0xe7, + 0xc2, 0xa5, 0x5e, 0x98, 0xd6, 0xa9, 0x17, 0x2a, 0xd9, 0xd5, 0x0d, 0x70, 0x1a, 0x06, 0xd9, 0x75, 0xe1, 0xe8, 0x5a, + 0x8c, 0x17, 0xb6, 0x24, 0x95, 0x2b, 0x68, 0xe9, 0xe6, 0x72, 0x7b, 0xb6, 0x45, 0xec, 0xcf, 0xbd, 0xf8, 0x8e, 0xcc, + 0xdf, 0x0c, 0xd9, 0x46, 0x4e, 0x57, 0x15, 0xa2, 0x07, 0x34, 0x01, 0x44, 0x1a, 0x54, 0xe5, 0xeb, 0xbc, 0x8c, 0xf1, + 0xd1, 0xe6, 0x36, 0x40, 0xf0, 0xad, 0x6b, 0xf5, 0x39, 0xb3, 0x48, 0xfe, 0x48, 0x4d, 0x7a, 0x5a, 0xd2, 0x30, 0xbc, + 0xa4, 0x3c, 0xbc, 0xb0, 0xbc, 0xd1, 0x70, 0x30, 0x44, 0x29, 0x08, 0x6e, 0x1c, 0x19, 0x26, 0xc1, 0xac, 0x5f, 0x51, + 0x7a, 0xf7, 0x87, 0x2e, 0x07, 0x83, 0xe5, 0x08, 0x61, 0x39, 0x6a, 0x44, 0xb3, 0x9e, 0x58, 0x11, 0xe0, 0x45, 0x80, 0x0b, 0x89, 0x91, 0x03, 0xa1, 0xfc, 0x98, 0x4a, 0xbe, 0x85, 0x62, 0x38, 0x1a, 0x04, 0x3b, 0x1d, 0x8d, 0xd8, 0x75, 0x23, 0x6c, 0x15, 0x67, 0x27, 0xfb, 0x54, 0x9b, 0x88, 0x22, 0x55, 0x82, 0x69, 0x88, 0x61, 0x84, 0xc5, 0x2c, 0x40, 0x82, 0x70, 0xd7, 0x29, 0x2e, 0x3a, 0xd6, 0x1c, 0xd5, 0xd2, 0xce, 0x69, 0x99, 0xe1, 0xc1, 0x56, 0x6a, 0xff, 0x04, 0x53, 0x7e, 0x02, 0x59, 0x87, 0xa0, 0x58, 0x27, 0xfb, 0xf4, 0xa8, 0x54, 0x4e, 0x44, 0xd1, 0x89, 0x90, 0x41, 0x76, 0x79, 0x07, 0x0f, 0x3a, 0x2a, 0x49, 0xca, 0x16, 0x50, 0xea, 0x65, 0xaa, 0x32, 0xe7, 0x0c, 0x16, 0x8f, 0xbe, 0x07, 0xa1, 0x79, 0x6c, 0x70, 0x89, 0x50, 0x95, 0xb9, 0x77, 0x8b, 0x23, 0x17, 0x6f, 0xbc, 0x5b, 0xcd, 0xe1, 0xaf, 0x8a, - 0xb3, 0x96, 0x94, 0xcf, 0xda, 0xa8, 0x76, 0x43, 0x0e, 0xe0, 0x86, 0x3c, 0x6a, 0x5e, 0xdc, 0x99, 0x58, 0xdc, 0xf1, + 0xb3, 0x96, 0x94, 0xcf, 0xda, 0x68, 0xe3, 0x86, 0x1c, 0xc0, 0x0d, 0x79, 0x54, 0xbf, 0xb8, 0x33, 0xb1, 0xb8, 0xe3, 0x86, 0xc5, 0x1d, 0x6f, 0x59, 0xdc, 0x80, 0x2f, 0xa4, 0x92, 0x4f, 0x5d, 0x8c, 0xbe, 0xd4, 0xf9, 0xe4, 0x71, 0x7e, - 0xa4, 0xcb, 0xcf, 0x19, 0xce, 0x93, 0x99, 0x04, 0x60, 0x4b, 0xbc, 0x61, 0xae, 0x9a, 0xe6, 0x45, 0x9a, 0x88, 0xfa, - 0xc0, 0xf3, 0x53, 0x27, 0xc6, 0x0d, 0x29, 0xbc, 0xb5, 0xa0, 0x3a, 0x5e, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x66, 0x1b, - 0x46, 0x3a, 0xdb, 0x32, 0xd2, 0x51, 0xe9, 0xe8, 0xf2, 0x61, 0xd3, 0x21, 0x94, 0x07, 0x05, 0x7b, 0x10, 0xfc, 0x2b, - 0x70, 0xcb, 0x94, 0xf7, 0xe1, 0x66, 0x1c, 0x2b, 0xed, 0xa8, 0x85, 0x97, 0x24, 0xab, 0x28, 0x06, 0x03, 0x05, 0xe8, - 0xe6, 0x61, 0x5b, 0x6a, 0xee, 0x87, 0x3c, 0xf6, 0xd9, 0xc6, 0xcd, 0x54, 0xbc, 0x97, 0xb7, 0x54, 0xeb, 0xf0, 0x90, - 0x6a, 0x2c, 0xbc, 0x34, 0x65, 0x31, 0x4e, 0xba, 0x07, 0x49, 0x32, 0xfe, 0x4b, 0xb6, 0x59, 0x03, 0x0e, 0x09, 0x24, - 0xac, 0x8e, 0x18, 0x7a, 0x01, 0x2c, 0x18, 0x69, 0x24, 0x43, 0x7d, 0x2d, 0xc5, 0x51, 0x8d, 0xf3, 0x89, 0xff, 0x11, - 0x8f, 0xab, 0x16, 0x4b, 0x9e, 0xbe, 0xce, 0x91, 0x6e, 0x2d, 0xbc, 0xf1, 0x7b, 0xb0, 0x83, 0xd1, 0x5a, 0x06, 0xf8, - 0xb4, 0xc8, 0x51, 0x53, 0x63, 0xe2, 0x09, 0x47, 0x05, 0x92, 0x44, 0x2c, 0xc9, 0x2d, 0x86, 0x21, 0xd8, 0x80, 0x67, - 0x4e, 0xae, 0xd6, 0xad, 0x6c, 0x7f, 0xea, 0xeb, 0x35, 0xac, 0x09, 0xa8, 0x2d, 0x70, 0xfb, 0xb9, 0xd0, 0x2d, 0x30, - 0x9c, 0x23, 0x1d, 0x14, 0xa5, 0x97, 0x90, 0x0e, 0xdd, 0x16, 0x97, 0xe9, 0x41, 0x0c, 0x54, 0x0b, 0xd4, 0x8a, 0x4f, - 0xa6, 0xf8, 0xcb, 0xb9, 0xca, 0x9e, 0x0c, 0xf1, 0x57, 0xeb, 0x2a, 0x57, 0x62, 0x55, 0xa4, 0x08, 0xd2, 0x98, 0xd5, - 0x7e, 0x69, 0x3f, 0x91, 0xb9, 0xf6, 0x03, 0xb6, 0x0d, 0x5f, 0xe0, 0x47, 0x8f, 0xd7, 0x09, 0x04, 0x28, 0x90, 0xc7, - 0x10, 0x5a, 0xb1, 0x9e, 0x35, 0x96, 0x4f, 0x37, 0x94, 0x0f, 0xf5, 0xdf, 0x99, 0xf0, 0xe3, 0x2e, 0x89, 0x0a, 0x9a, - 0x52, 0x96, 0x81, 0x5c, 0x0f, 0xfd, 0xd0, 0x8b, 0xef, 0xae, 0xe9, 0x16, 0xa2, 0x09, 0x16, 0x3f, 0x97, 0xed, 0x10, - 0x2f, 0x5a, 0xb6, 0x0e, 0x49, 0x25, 0x45, 0xd5, 0x1d, 0x27, 0xf4, 0xee, 0x9f, 0x62, 0x89, 0xbf, 0x2b, 0x5d, 0x63, - 0xf9, 0x82, 0x94, 0x3e, 0x74, 0xfd, 0x78, 0xad, 0xb1, 0x7a, 0x37, 0x95, 0xd1, 0x56, 0x18, 0x48, 0x58, 0x1e, 0xbc, - 0x12, 0xcf, 0xc7, 0x7e, 0x17, 0xcd, 0x3f, 0x86, 0xd1, 0xad, 0xf9, 0x78, 0x9d, 0x9e, 0xaa, 0x73, 0x2f, 0xfe, 0xc8, - 0xc6, 0xe6, 0xc8, 0x8f, 0x47, 0x01, 0x30, 0x8f, 0xc3, 0xc0, 0x0b, 0x3f, 0xf2, 0x47, 0x33, 0x5a, 0xa6, 0x68, 0xd0, - 0x75, 0xef, 0x0d, 0x5a, 0xcc, 0x09, 0x09, 0x12, 0x91, 0xab, 0x6d, 0x98, 0x05, 0xe5, 0xfd, 0x40, 0x5c, 0xeb, 0x0b, - 0x46, 0xb1, 0xa8, 0x65, 0x80, 0x3f, 0x02, 0xd8, 0x98, 0x41, 0x80, 0x07, 0x43, 0xc5, 0xf5, 0x52, 0x0d, 0x79, 0xa8, - 0xa4, 0x55, 0xcb, 0x33, 0x14, 0x5f, 0x63, 0x0f, 0xbf, 0xfe, 0x73, 0x50, 0xf2, 0x90, 0xcf, 0xe5, 0xbd, 0x7c, 0xde, - 0x08, 0xa1, 0xd4, 0x24, 0xc7, 0xc2, 0x07, 0x7c, 0x9c, 0x33, 0x98, 0x9b, 0x3f, 0x2d, 0x37, 0xf6, 0x92, 0x64, 0x39, - 0x67, 0x63, 0x52, 0x89, 0x9d, 0x16, 0x40, 0x95, 0xef, 0x21, 0x32, 0x60, 0x7f, 0x5f, 0xb6, 0x8e, 0x0f, 0x5e, 0x81, - 0x81, 0x1f, 0x30, 0x94, 0xd1, 0x64, 0xa2, 0x16, 0xa2, 0x80, 0x7b, 0x9a, 0x39, 0x07, 0x7f, 0x5f, 0xbe, 0x39, 0xb3, - 0xdf, 0xe4, 0x8d, 0x43, 0x60, 0x8c, 0x85, 0xb5, 0x12, 0xe7, 0x8b, 0x25, 0x78, 0xc5, 0x88, 0x26, 0x5e, 0xb8, 0x79, - 0x38, 0x97, 0xa5, 0x2d, 0xbe, 0x60, 0x6c, 0x0c, 0x0c, 0xb7, 0x51, 0x2b, 0xbd, 0x0e, 0xd8, 0x0d, 0xcb, 0x2d, 0xa1, - 0xea, 0x1f, 0x6b, 0x68, 0x81, 0xa1, 0x5a, 0xb9, 0xee, 0x91, 0x73, 0x75, 0xd2, 0x90, 0x06, 0x38, 0x06, 0x3e, 0x72, - 0xf9, 0x88, 0x55, 0x8e, 0xd4, 0xc0, 0x50, 0x25, 0x00, 0x36, 0x42, 0x76, 0xba, 0xa1, 0xbc, 0x0b, 0x88, 0x7a, 0x03, - 0x6c, 0x86, 0xa3, 0x77, 0x21, 0xb5, 0x05, 0x9f, 0xa7, 0x00, 0x4e, 0x9e, 0x56, 0x48, 0x4d, 0x36, 0xcd, 0x58, 0x93, - 0xa8, 0x4d, 0x25, 0x21, 0x8d, 0x70, 0x0e, 0x40, 0x2f, 0x19, 0x21, 0xae, 0x6a, 0x5c, 0x1b, 0xa5, 0x3c, 0xf2, 0x21, - 0x26, 0x7e, 0x0f, 0x59, 0x92, 0x6c, 0x9c, 0xb0, 0x7c, 0xd1, 0x0d, 0xb5, 0xa8, 0x5d, 0x9e, 0x8f, 0xa2, 0xdc, 0xb0, - 0x0d, 0x60, 0x09, 0x70, 0x80, 0xd5, 0x6f, 0x21, 0x79, 0xb9, 0x9e, 0x73, 0xf3, 0xce, 0x78, 0x3a, 0x54, 0xb9, 0xe9, - 0xdd, 0xa6, 0xf7, 0x2b, 0x95, 0x03, 0x55, 0x22, 0xd3, 0x8d, 0xa0, 0x69, 0x25, 0xd4, 0x5b, 0x93, 0x2a, 0x61, 0x07, - 0x02, 0xa6, 0x0a, 0x7e, 0x65, 0x93, 0x09, 0x1b, 0xa5, 0x89, 0x2e, 0x64, 0x4c, 0x79, 0xb0, 0x75, 0x70, 0xb2, 0xdd, - 0x73, 0xd5, 0x1f, 0x21, 0xe4, 0x8c, 0x88, 0x49, 0xc8, 0x01, 0x12, 0x77, 0xa6, 0xe6, 0x69, 0xa2, 0x1e, 0xcb, 0x53, - 0xc4, 0xbf, 0x02, 0x52, 0xe8, 0x86, 0x72, 0x04, 0x8d, 0xd3, 0x9f, 0x62, 0x5f, 0x44, 0xb9, 0x11, 0xe8, 0x76, 0x54, - 0xb4, 0xed, 0xf8, 0xae, 0x9d, 0x37, 0x87, 0x8e, 0x9d, 0xa9, 0x06, 0xb8, 0x3a, 0x7f, 0xac, 0x6c, 0x63, 0x22, 0x50, - 0xae, 0x7a, 0xfe, 0xf6, 0xd5, 0x9f, 0xce, 0x5e, 0xef, 0x8a, 0x11, 0xb0, 0xcb, 0x36, 0x74, 0xb9, 0x0c, 0xb7, 0x74, - 0xfa, 0xf3, 0x8f, 0x0f, 0xeb, 0xb6, 0xe5, 0xbc, 0x70, 0x54, 0x83, 0xac, 0xd3, 0x25, 0xbc, 0x38, 0x8a, 0x6e, 0x58, - 0xfc, 0xd9, 0xd3, 0x20, 0x77, 0xde, 0x0c, 0xee, 0xdb, 0x9f, 0xce, 0x7e, 0xdc, 0x19, 0xd4, 0x23, 0xc7, 0x06, 0xdc, - 0x9e, 0x46, 0x8b, 0x07, 0x8c, 0xae, 0xad, 0x1a, 0xea, 0x28, 0x88, 0x12, 0xb6, 0x01, 0x82, 0x57, 0xe7, 0x6f, 0xdf, - 0xe3, 0x74, 0x15, 0x2c, 0x08, 0x75, 0xf5, 0x79, 0x83, 0xff, 0xe9, 0xdd, 0xd9, 0xfb, 0xf7, 0xaa, 0x81, 0xc9, 0xba, - 0x13, 0xb9, 0x77, 0xbe, 0x89, 0xef, 0xa1, 0x38, 0x8d, 0x7b, 0x9d, 0xa8, 0x1a, 0x5d, 0xa4, 0xcb, 0xa3, 0xa1, 0xb2, - 0xda, 0x36, 0xe7, 0xd4, 0x8e, 0x7f, 0x99, 0x6e, 0xbf, 0x3b, 0x8d, 0xab, 0x01, 0x1f, 0x6d, 0x27, 0xa9, 0xa5, 0x92, - 0xb9, 0x1f, 0x5e, 0x37, 0x94, 0x7a, 0xb7, 0x0d, 0xa5, 0x70, 0x7d, 0xac, 0xe1, 0xc7, 0x65, 0x34, 0x97, 0xd8, 0x11, - 0x76, 0x7b, 0xff, 0x74, 0x49, 0x77, 0xb8, 0xcf, 0x00, 0x9a, 0x27, 0x5b, 0xa9, 0x42, 0xdd, 0x50, 0xcc, 0x2f, 0x5e, - 0xf9, 0xdc, 0x8e, 0x02, 0xb0, 0xc9, 0x67, 0xb2, 0x1a, 0xb2, 0xcc, 0xaa, 0x72, 0x8f, 0x1a, 0xb7, 0x72, 0x2b, 0xa0, - 0x66, 0xa4, 0xba, 0xe1, 0x34, 0x65, 0xe1, 0x8d, 0xc1, 0xd0, 0xdd, 0x1c, 0x46, 0x69, 0x1a, 0xcd, 0xbb, 0x8e, 0xbd, - 0xb8, 0x55, 0x95, 0x9e, 0x10, 0x76, 0x70, 0x3b, 0xfc, 0xee, 0xbf, 0xff, 0x55, 0x41, 0xf3, 0x54, 0x7e, 0x9d, 0xb2, - 0xf9, 0x82, 0xc5, 0x5e, 0xba, 0x8c, 0x59, 0xa6, 0xfc, 0xfb, 0x7f, 0x5e, 0x55, 0x2e, 0xf6, 0x3d, 0xb9, 0x0d, 0xb1, - 0xf4, 0x72, 0x93, 0xeb, 0x20, 0x5a, 0xed, 0x15, 0x1e, 0x77, 0xf7, 0x54, 0x9e, 0xf9, 0xd3, 0x59, 0x5e, 0xfb, 0x34, - 0xdd, 0x32, 0x36, 0x01, 0x3d, 0xe9, 0x03, 0x94, 0xf3, 0x68, 0xd5, 0xfd, 0xf7, 0xbf, 0x72, 0x81, 0xcd, 0xbd, 0xbb, - 0xae, 0x19, 0xd0, 0xf2, 0x8a, 0x36, 0xd7, 0xa9, 0x2d, 0x31, 0xbc, 0xaf, 0x2d, 0x70, 0xad, 0x90, 0x76, 0x65, 0x5d, - 0x37, 0xb7, 0x65, 0x4c, 0xdf, 0xf9, 0xd3, 0xd9, 0xe7, 0x0e, 0x0a, 0x26, 0xf4, 0xde, 0x51, 0x41, 0xa5, 0x2f, 0x30, - 0xac, 0x41, 0x77, 0xf7, 0x05, 0xfb, 0xcc, 0x71, 0xdd, 0x37, 0xa4, 0x2f, 0x31, 0x1a, 0x2e, 0xb9, 0x7d, 0x3f, 0x18, - 0xe4, 0xc9, 0x6a, 0xe5, 0xf6, 0xe0, 0x33, 0x78, 0x5a, 0x2b, 0xe1, 0xec, 0x45, 0xd7, 0xd6, 0x29, 0x98, 0xcf, 0x0e, - 0x13, 0x82, 0xd6, 0xef, 0x0d, 0xd3, 0xb1, 0x19, 0x5f, 0x93, 0x13, 0x5b, 0xed, 0xdb, 0x35, 0x64, 0x0d, 0xa5, 0x98, - 0xe8, 0x34, 0xd7, 0x1a, 0x1a, 0xcd, 0xe0, 0xac, 0x62, 0x6f, 0x41, 0x4a, 0x02, 0x05, 0x35, 0x26, 0x20, 0x74, 0xa9, - 0xdc, 0xa2, 0x6f, 0xbc, 0xe0, 0x66, 0xb7, 0x0b, 0xd5, 0x66, 0x0a, 0x86, 0xa4, 0xf9, 0x3f, 0x47, 0xbc, 0x91, 0x2e, - 0x3f, 0x98, 0x76, 0xaf, 0xbc, 0x94, 0xc5, 0xd7, 0x33, 0xf0, 0xf6, 0x15, 0xd2, 0x03, 0x88, 0xa3, 0xbb, 0x0d, 0x29, - 0x97, 0xd8, 0xd2, 0x06, 0x34, 0x5a, 0x60, 0xb8, 0x5f, 0x87, 0xbb, 0xbf, 0x10, 0xe6, 0xee, 0x9e, 0x81, 0x3f, 0xe6, - 0x6f, 0x86, 0xbd, 0xb7, 0x51, 0xa6, 0xff, 0xc7, 0xde, 0xff, 0x8d, 0xd8, 0x7b, 0xeb, 0x77, 0x7e, 0xcd, 0xc2, 0xfe, - 0x1f, 0xc0, 0xf2, 0x5d, 0xe6, 0x9e, 0x71, 0x4c, 0xaf, 0x69, 0x9e, 0xab, 0xc5, 0xa5, 0xc3, 0x8b, 0x78, 0xb5, 0xa6, - 0x60, 0xe5, 0xc1, 0xd6, 0xb8, 0xe5, 0xa0, 0x87, 0xc8, 0x7e, 0xcb, 0x51, 0xfe, 0xed, 0x11, 0x7d, 0x42, 0x19, 0xaa, + 0xa4, 0xcb, 0xcf, 0x19, 0xce, 0x93, 0x99, 0x04, 0x60, 0x4b, 0xdc, 0x30, 0x57, 0x75, 0xf3, 0x22, 0x4d, 0xc4, 0xe6, + 0xc0, 0xf3, 0x53, 0x27, 0xc6, 0x0d, 0x29, 0xbc, 0xb5, 0xa0, 0x3a, 0x5e, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x66, 0x0d, + 0x23, 0x9d, 0x6d, 0x19, 0xe9, 0xa8, 0x74, 0x74, 0xf9, 0xb0, 0xe9, 0x10, 0xca, 0x83, 0x82, 0x3d, 0x08, 0xfe, 0x15, + 0xb8, 0x65, 0xca, 0xfb, 0xb0, 0x19, 0xc7, 0x4a, 0x3b, 0x6a, 0xe1, 0x25, 0xc9, 0x2a, 0x8a, 0xc1, 0x40, 0x01, 0xba, + 0x79, 0xd8, 0x96, 0x9a, 0xfb, 0x21, 0x8f, 0x7d, 0xd6, 0xb8, 0x99, 0x8a, 0xf7, 0xf2, 0x96, 0x6a, 0x1d, 0x1e, 0x52, + 0x8d, 0x85, 0x97, 0xa6, 0x2c, 0xc6, 0x49, 0xf7, 0x20, 0x49, 0xc6, 0x7f, 0xc8, 0x36, 0xab, 0xc1, 0x21, 0x81, 0x84, + 0xd5, 0x11, 0x43, 0x2f, 0x80, 0x05, 0x23, 0x8d, 0x64, 0xa8, 0xaf, 0xa5, 0x38, 0xaa, 0x71, 0x3e, 0xf1, 0x3f, 0xe1, + 0x71, 0xd5, 0x62, 0xc9, 0xd3, 0xd7, 0x39, 0xd2, 0xad, 0x85, 0x37, 0x7e, 0x0f, 0x76, 0x30, 0x5a, 0xcb, 0x00, 0x9f, + 0x16, 0x39, 0x6a, 0x6a, 0x4c, 0x3c, 0xe1, 0xa8, 0x40, 0x92, 0x88, 0x25, 0xb9, 0xc5, 0x30, 0x04, 0x1b, 0xf0, 0xcc, + 0xc9, 0xd5, 0xba, 0x95, 0xed, 0x4f, 0x7d, 0x7d, 0x03, 0x6b, 0x02, 0x6a, 0x0b, 0xdc, 0x7e, 0x2e, 0x74, 0x0b, 0x0c, + 0xe7, 0x48, 0x07, 0x45, 0xe9, 0x25, 0xa4, 0x43, 0xb7, 0xc5, 0x65, 0x7a, 0x10, 0x03, 0xd5, 0x02, 0xb5, 0xe2, 0x93, + 0x29, 0xfe, 0x72, 0xae, 0xb2, 0x27, 0x43, 0xfc, 0xd5, 0xba, 0xca, 0x95, 0x58, 0x15, 0x29, 0x82, 0x34, 0x66, 0xb5, + 0x5f, 0xda, 0x4f, 0x64, 0xae, 0xfd, 0x80, 0x6d, 0xc3, 0x17, 0xf8, 0xd1, 0xe3, 0x75, 0x02, 0x01, 0x0a, 0xe4, 0x31, + 0x84, 0x56, 0xac, 0x67, 0xb5, 0xe5, 0xd3, 0x86, 0xf2, 0xa1, 0xfe, 0x07, 0x13, 0x7e, 0xdc, 0x25, 0x51, 0x41, 0x53, + 0xca, 0x32, 0x90, 0xeb, 0xa1, 0x1f, 0x7a, 0xf1, 0xdd, 0x35, 0xdd, 0x42, 0x34, 0xc1, 0xe2, 0xe7, 0xb2, 0x1d, 0xe2, + 0x45, 0xcb, 0xd6, 0x21, 0xa9, 0xa4, 0xa8, 0xba, 0xe3, 0x84, 0xde, 0xfd, 0x73, 0x2c, 0xf1, 0x77, 0xa5, 0x6b, 0x2c, + 0x5f, 0x90, 0xd2, 0x87, 0xae, 0x1f, 0xaf, 0x35, 0xb6, 0xd9, 0x4d, 0x65, 0xb4, 0x15, 0x06, 0x12, 0x96, 0x07, 0xaf, + 0xc4, 0xf3, 0xb1, 0xdf, 0x45, 0xf3, 0x8f, 0x61, 0x74, 0x6b, 0x3e, 0x5e, 0xa7, 0xa7, 0xea, 0xdc, 0x8b, 0x3f, 0xb2, + 0xb1, 0x39, 0xf2, 0xe3, 0x51, 0x00, 0xcc, 0xe3, 0x30, 0xf0, 0xc2, 0x8f, 0xfc, 0xd1, 0x8c, 0x96, 0x29, 0x1a, 0x74, + 0xdd, 0x7b, 0x83, 0x16, 0x73, 0x42, 0x82, 0x44, 0xe4, 0x6a, 0x6b, 0x66, 0x41, 0x79, 0x3f, 0x10, 0xd7, 0xfa, 0x82, + 0x51, 0x2c, 0x6a, 0x19, 0xe0, 0x8f, 0x00, 0x36, 0x66, 0x10, 0xe0, 0xc1, 0x50, 0x71, 0xbd, 0x54, 0x43, 0x1e, 0x2a, + 0x69, 0xd5, 0xf2, 0x0c, 0xc5, 0xd7, 0xd8, 0xc3, 0x6f, 0xff, 0x1c, 0x94, 0x3c, 0xe4, 0x73, 0x79, 0x2f, 0x9f, 0x37, + 0x42, 0x28, 0x35, 0xc9, 0xb1, 0xf0, 0x01, 0x1f, 0xe7, 0x0c, 0x66, 0xf3, 0xa7, 0xe5, 0xc6, 0x5e, 0x92, 0x2c, 0xe7, + 0x6c, 0x4c, 0x2a, 0xb1, 0xd3, 0x02, 0xa8, 0xf2, 0x3d, 0x44, 0x06, 0xec, 0x6f, 0xcb, 0xd6, 0xf1, 0xc1, 0x2b, 0x30, + 0xf0, 0x03, 0x86, 0x32, 0x9a, 0x4c, 0xd4, 0x42, 0x14, 0x70, 0x4f, 0x33, 0xe7, 0xe0, 0x6f, 0xcb, 0x37, 0x67, 0xf6, + 0x9b, 0xbc, 0x71, 0x08, 0x8c, 0xb1, 0xb0, 0x56, 0xe2, 0x7c, 0xb1, 0x04, 0xaf, 0x18, 0xd1, 0xc4, 0x0b, 0x9b, 0x87, + 0x73, 0x59, 0xda, 0xe2, 0x0b, 0xc6, 0xc6, 0xc0, 0x70, 0x1b, 0x1b, 0xa5, 0xd7, 0x01, 0xbb, 0x61, 0xb9, 0x25, 0xd4, + 0xe6, 0xc7, 0x6a, 0x5a, 0x60, 0xa8, 0x56, 0xae, 0x7b, 0xe4, 0x5c, 0x9d, 0x34, 0xa4, 0x01, 0x8e, 0x81, 0x8f, 0x5c, + 0x3e, 0x62, 0x95, 0x23, 0x35, 0x30, 0x54, 0x09, 0x80, 0x46, 0xc8, 0x4e, 0x1b, 0xca, 0xbb, 0x80, 0xa8, 0x1b, 0x60, + 0x33, 0x1c, 0xbd, 0x0b, 0xa9, 0x2d, 0xf8, 0x3c, 0x05, 0x70, 0xf2, 0xb4, 0x42, 0x6a, 0xd2, 0x34, 0x63, 0x75, 0xa2, + 0x36, 0x95, 0x84, 0x34, 0xc2, 0x39, 0x00, 0xbd, 0x64, 0x84, 0xb8, 0xaa, 0x76, 0x6d, 0x94, 0xf2, 0xc8, 0x87, 0x98, + 0xf8, 0x3d, 0x64, 0x49, 0xd2, 0x38, 0x61, 0xf9, 0xa2, 0x1b, 0x6a, 0x51, 0xbb, 0x3c, 0x1f, 0x45, 0xb9, 0x61, 0x1b, + 0xc0, 0x12, 0xe0, 0x00, 0xab, 0xdf, 0x42, 0xf2, 0x72, 0x3d, 0xe7, 0xe6, 0x9d, 0xf1, 0x74, 0xa8, 0x72, 0xd3, 0xbb, + 0xa6, 0xf7, 0x2b, 0x95, 0x03, 0x55, 0x22, 0xd3, 0xb5, 0xa0, 0x69, 0x25, 0xd4, 0xbb, 0x21, 0x55, 0xc2, 0x0e, 0x04, + 0x4c, 0x15, 0xfc, 0xca, 0x26, 0x13, 0x36, 0x4a, 0x13, 0x5d, 0xc8, 0x98, 0xf2, 0x60, 0xeb, 0xe0, 0x64, 0xbb, 0xe7, + 0xaa, 0x3f, 0x41, 0xc8, 0x19, 0x11, 0x93, 0x90, 0x03, 0x24, 0xee, 0x4c, 0xf5, 0xd3, 0x44, 0x3d, 0x96, 0xa7, 0x88, + 0x7f, 0x05, 0xa4, 0xd0, 0x35, 0xe5, 0x08, 0x1a, 0xa7, 0x3f, 0xc5, 0xbe, 0x88, 0x72, 0x23, 0xd0, 0xed, 0xa8, 0x68, + 0xdb, 0xf1, 0x5d, 0x3b, 0x6f, 0x0e, 0x1d, 0x3b, 0x53, 0x0d, 0x70, 0x75, 0xfe, 0x58, 0xd9, 0xc6, 0x44, 0xa0, 0x5c, + 0xf5, 0xfc, 0xed, 0xab, 0x3f, 0x9f, 0xbd, 0xde, 0x15, 0x23, 0x60, 0x97, 0x6d, 0xe8, 0x72, 0x19, 0x6e, 0xe9, 0xf4, + 0x97, 0x9f, 0x1e, 0xd6, 0x6d, 0xcb, 0x79, 0xe1, 0xa8, 0x06, 0x59, 0xa7, 0x4b, 0x78, 0x71, 0x14, 0xdd, 0xb0, 0xf8, + 0xb3, 0xa7, 0x41, 0xee, 0xbc, 0x1e, 0xdc, 0xb7, 0x3f, 0x9f, 0xfd, 0xb4, 0x33, 0xa8, 0x47, 0x8e, 0x0d, 0xb8, 0x3d, + 0x8d, 0x16, 0x0f, 0x18, 0x5d, 0x5b, 0x35, 0xd4, 0x51, 0x10, 0x25, 0xac, 0x01, 0x82, 0x57, 0xe7, 0x6f, 0xdf, 0xe3, + 0x74, 0x15, 0x2c, 0x08, 0x75, 0xf5, 0x79, 0x83, 0xff, 0xf9, 0xdd, 0xd9, 0xfb, 0xf7, 0xaa, 0x81, 0xc9, 0xba, 0x13, + 0xb9, 0x77, 0xbe, 0x89, 0xef, 0xa1, 0x38, 0xb5, 0x7b, 0x9d, 0xa8, 0x1a, 0x5d, 0xa4, 0xcb, 0xa3, 0xa1, 0xb2, 0x8d, + 0x6d, 0xce, 0xa9, 0x1d, 0xff, 0x32, 0xdd, 0x7e, 0x77, 0x1a, 0x57, 0x0d, 0x3e, 0xda, 0x4e, 0x52, 0x4b, 0x25, 0x73, + 0x3f, 0xbc, 0xae, 0x29, 0xf5, 0x6e, 0x6b, 0x4a, 0xe1, 0xfa, 0xb8, 0x81, 0x1f, 0x97, 0xd1, 0x5c, 0x62, 0x47, 0xd8, + 0xed, 0xfd, 0xd3, 0x25, 0xdd, 0xe1, 0x3e, 0x03, 0x68, 0x9e, 0x6c, 0xa5, 0x0a, 0x75, 0x4d, 0x31, 0xbf, 0x78, 0xe5, + 0x73, 0x3b, 0x0a, 0xc0, 0x26, 0x9f, 0xc9, 0x6a, 0xc8, 0x32, 0xab, 0xca, 0x3d, 0x6a, 0xdc, 0xca, 0xad, 0x80, 0x9a, + 0x91, 0xea, 0x86, 0xd3, 0x94, 0x85, 0x37, 0x06, 0x43, 0x77, 0x73, 0x18, 0xa5, 0x69, 0x34, 0xef, 0x3a, 0xf6, 0xe2, + 0x56, 0x55, 0x7a, 0x42, 0xd8, 0xc1, 0xed, 0xf0, 0xbb, 0xff, 0xfa, 0x67, 0x05, 0xcd, 0x53, 0xf9, 0x75, 0xca, 0xe6, + 0x0b, 0x16, 0x7b, 0xe9, 0x32, 0x66, 0x99, 0xf2, 0xaf, 0xff, 0x79, 0x55, 0xb9, 0xd8, 0xf7, 0xe4, 0x36, 0xc4, 0xd2, + 0xcb, 0x4d, 0xae, 0x83, 0x68, 0xb5, 0x57, 0x78, 0xdc, 0xdd, 0x53, 0x79, 0xe6, 0x4f, 0x67, 0x79, 0xed, 0xd3, 0x74, + 0xcb, 0xd8, 0x04, 0xf4, 0xa4, 0x0f, 0x50, 0xce, 0xa3, 0x55, 0xf7, 0x5f, 0xff, 0xcc, 0x05, 0x36, 0xf7, 0xee, 0xba, + 0x7a, 0x40, 0xcb, 0x2b, 0x5a, 0x5f, 0x67, 0x63, 0x89, 0xe1, 0xfd, 0xc6, 0x02, 0x6f, 0x14, 0xd2, 0xae, 0xdc, 0xd4, + 0xcd, 0x6d, 0x19, 0xd3, 0x77, 0xfe, 0x74, 0xf6, 0xb9, 0x83, 0x82, 0x09, 0xbd, 0x77, 0x54, 0x50, 0xe9, 0x0b, 0x0c, + 0x6b, 0xd0, 0xdd, 0x7d, 0xc1, 0x3e, 0x73, 0x5c, 0xf7, 0x0d, 0xe9, 0x4b, 0x8c, 0x86, 0x4b, 0x6e, 0xdf, 0x0f, 0x06, + 0x79, 0xb2, 0x5a, 0xb9, 0x3d, 0xf8, 0x0c, 0x9e, 0x6e, 0x94, 0x70, 0xf6, 0xa2, 0x6b, 0xeb, 0x14, 0xcc, 0x67, 0x87, + 0x09, 0x41, 0xeb, 0xf7, 0x9a, 0xe9, 0x68, 0xc6, 0xd7, 0xe4, 0xc4, 0xb6, 0xf1, 0xed, 0x0d, 0x64, 0x0d, 0xa5, 0x98, + 0xe8, 0x34, 0xd7, 0x1a, 0x1a, 0xf5, 0xe0, 0xac, 0x62, 0x6f, 0x41, 0x4a, 0x02, 0x05, 0x35, 0x26, 0x20, 0x74, 0xa9, + 0xdc, 0xa2, 0x6f, 0xbc, 0xe0, 0x66, 0xb7, 0x0b, 0x55, 0x33, 0x05, 0x43, 0xd2, 0xfc, 0xef, 0x23, 0xde, 0x48, 0x97, + 0x1f, 0x4c, 0xbb, 0x57, 0x5e, 0xca, 0xe2, 0xeb, 0x19, 0x78, 0xfb, 0x0a, 0xe9, 0x01, 0xc4, 0xd1, 0xdd, 0x86, 0x94, + 0x4b, 0x6c, 0x69, 0x0d, 0x1a, 0x2d, 0x30, 0xdc, 0x6f, 0xc3, 0xdd, 0x5f, 0x08, 0x73, 0x77, 0xcf, 0xc0, 0x1f, 0xf3, + 0x77, 0xc3, 0xde, 0xdb, 0x28, 0xd3, 0xff, 0x63, 0xef, 0xff, 0x44, 0xec, 0xbd, 0xf5, 0x3b, 0xbf, 0x65, 0x61, 0xff, + 0x0f, 0x60, 0xf9, 0x2e, 0x73, 0xcf, 0x38, 0xa6, 0xd7, 0x34, 0xcf, 0xd5, 0xe2, 0xd2, 0xe1, 0x45, 0xbc, 0xba, 0xa1, + 0x60, 0xe5, 0xc1, 0xd6, 0xb8, 0xe5, 0xa0, 0x87, 0xc8, 0x7e, 0xcb, 0x51, 0xfe, 0xfd, 0x11, 0x7d, 0x42, 0x19, 0xaa, 0x24, 0x4c, 0xdf, 0x3d, 0x33, 0x92, 0xd2, 0x48, 0xbc, 0x95, 0x77, 0xb7, 0x0b, 0xde, 0x11, 0xc0, 0x7e, 0xb3, 0xf2, - 0xee, 0x9a, 0x80, 0xdd, 0x88, 0x5e, 0xab, 0x1f, 0x3b, 0x05, 0x2f, 0x9f, 0x2e, 0xba, 0xf8, 0x18, 0x83, 0x84, 0xa5, - 0xa7, 0x50, 0xe8, 0x3e, 0x5e, 0xef, 0x55, 0x2b, 0x66, 0x03, 0xf0, 0x7f, 0x96, 0x00, 0x8f, 0x4a, 0x80, 0xfb, 0xc9, - 0x75, 0x14, 0x3e, 0x04, 0xf2, 0x9f, 0x40, 0xf8, 0xf3, 0xab, 0x41, 0xc7, 0xcf, 0xd5, 0x60, 0xc7, 0xd2, 0x2a, 0xf0, - 0x58, 0x58, 0x85, 0xbe, 0xd7, 0x2c, 0xab, 0xaf, 0x10, 0x5a, 0xa4, 0xb1, 0x8c, 0x08, 0xad, 0x02, 0x7a, 0x15, 0x05, - 0x74, 0x5c, 0x15, 0x92, 0xeb, 0x87, 0x93, 0xd8, 0x8b, 0xd9, 0x78, 0xf3, 0x15, 0xa0, 0x64, 0x9d, 0x7c, 0x67, 0x25, - 0xcb, 0xc5, 0x22, 0x8a, 0xd3, 0xe4, 0x1a, 0xe3, 0xb4, 0xcc, 0x7d, 0xb8, 0x50, 0x40, 0x46, 0xb1, 0x3c, 0x6a, 0xef, - 0x59, 0x93, 0x7c, 0xdb, 0x60, 0x6e, 0x39, 0xd9, 0x06, 0xf7, 0xbe, 0x31, 0xb8, 0xff, 0xce, 0x4c, 0xd2, 0x5f, 0xcc, - 0xac, 0x34, 0xf6, 0xe7, 0x9a, 0x6e, 0x38, 0xb6, 0xae, 0x0b, 0xf9, 0xca, 0xcc, 0xed, 0xef, 0x51, 0xb4, 0xe1, 0x99, - 0x0e, 0x51, 0x0b, 0xd1, 0xa3, 0x05, 0x6c, 0xe5, 0x5e, 0x2e, 0x27, 0x13, 0x16, 0x6b, 0x22, 0x2c, 0x23, 0xc4, 0x85, - 0x25, 0x63, 0x40, 0xf0, 0x73, 0xfc, 0xe0, 0xb3, 0x15, 0xe4, 0x7f, 0x2a, 0xc2, 0xaa, 0x83, 0xaf, 0x27, 0x99, 0x93, - 0x43, 0x6e, 0xb9, 0xb4, 0xdd, 0xd2, 0xc6, 0xcf, 0x0e, 0x8c, 0x19, 0x04, 0x63, 0x2a, 0xdc, 0xe3, 0x31, 0xce, 0x9f, - 0x1f, 0xa6, 0x1d, 0xfc, 0x02, 0x74, 0x00, 0x87, 0x37, 0x70, 0x73, 0xbf, 0x28, 0x65, 0x94, 0x77, 0x38, 0x73, 0xfb, - 0xc1, 0x73, 0x97, 0xf4, 0x3c, 0x68, 0xb7, 0xf7, 0x6a, 0xe6, 0xc5, 0xaf, 0xa2, 0x31, 0x43, 0x40, 0x87, 0x69, 0x04, - 0xde, 0x9a, 0x52, 0x18, 0x1e, 0x8c, 0xc2, 0x63, 0x96, 0x22, 0xf3, 0xec, 0x43, 0xd1, 0xb5, 0x5c, 0xe4, 0x3e, 0x7f, - 0xbc, 0x6f, 0xc0, 0x49, 0x6b, 0x5e, 0x69, 0xb1, 0x68, 0x7c, 0xa9, 0x1b, 0x5f, 0xc9, 0xbb, 0xf5, 0x95, 0x17, 0xc7, - 0x3e, 0x8b, 0x15, 0xed, 0xbb, 0x5f, 0x74, 0x79, 0xd3, 0x96, 0x14, 0x3a, 0x5c, 0xcb, 0xac, 0x60, 0x34, 0xba, 0x89, - 0xcf, 0x82, 0xb1, 0xab, 0x8e, 0xa8, 0x61, 0xae, 0xbc, 0x69, 0x77, 0x6c, 0xdb, 0xe6, 0x0a, 0x53, 0x87, 0x7e, 0x82, - 0xc2, 0x14, 0x7e, 0xc2, 0x43, 0x49, 0xbc, 0xd8, 0x21, 0x2e, 0xa2, 0x46, 0xce, 0x1a, 0x21, 0x7c, 0x47, 0xf1, 0x7c, + 0xee, 0xea, 0x80, 0x6d, 0x44, 0xaf, 0xd5, 0x8f, 0x9d, 0x82, 0x97, 0x4f, 0x17, 0x5d, 0x7c, 0x8c, 0x41, 0xc2, 0xd2, + 0x53, 0x28, 0x74, 0x1f, 0xaf, 0xf7, 0xaa, 0x15, 0xb3, 0x01, 0xf8, 0x3f, 0x4b, 0x80, 0x47, 0x25, 0xc0, 0xfd, 0xe4, + 0x3a, 0x0a, 0x1f, 0x02, 0xf9, 0xcf, 0x20, 0xfc, 0xf9, 0xcd, 0xa0, 0xe3, 0xe7, 0x36, 0x60, 0xc7, 0xd2, 0x2a, 0xf0, + 0x58, 0x58, 0x85, 0xbe, 0x57, 0x2f, 0xab, 0xaf, 0x10, 0x5a, 0xa4, 0xb1, 0x8c, 0x08, 0xad, 0x02, 0x7a, 0x15, 0x05, + 0x74, 0x5c, 0x15, 0x92, 0xeb, 0x87, 0x93, 0xd8, 0x8b, 0xd9, 0xb8, 0xf9, 0x0a, 0x50, 0xb2, 0x4e, 0xbe, 0xb3, 0x92, + 0xe5, 0x62, 0x11, 0xc5, 0x69, 0x72, 0x8d, 0x71, 0x5a, 0xe6, 0x3e, 0x5c, 0x28, 0x20, 0xa3, 0x58, 0x1e, 0xb5, 0xf7, + 0xac, 0x4e, 0xbe, 0x6d, 0x30, 0xb7, 0x9c, 0x6c, 0x83, 0x7b, 0xdf, 0x18, 0xdc, 0x7f, 0x67, 0x26, 0xe9, 0x2f, 0x66, + 0x56, 0x1a, 0xfb, 0x73, 0x4d, 0x37, 0x1c, 0x5b, 0xd7, 0x85, 0x7c, 0x65, 0xe6, 0xf6, 0xf7, 0x28, 0xda, 0xf0, 0x4c, + 0x87, 0xa8, 0x85, 0xe8, 0xd1, 0x02, 0xb6, 0x72, 0x2f, 0x97, 0x93, 0x09, 0x8b, 0x35, 0x11, 0x96, 0x11, 0xe2, 0xc2, + 0x92, 0x31, 0x20, 0xf8, 0x39, 0x7e, 0xf0, 0xd9, 0x0a, 0xf2, 0x3f, 0x15, 0x61, 0xd5, 0xc1, 0xd7, 0x93, 0xcc, 0xc9, + 0x21, 0xb7, 0x5c, 0xda, 0x6e, 0x69, 0xe3, 0x67, 0x07, 0xc6, 0x0c, 0x82, 0x31, 0x15, 0xee, 0xf1, 0x18, 0xe7, 0xcf, + 0x0f, 0xd3, 0x0e, 0x7e, 0x01, 0x3a, 0x80, 0xc3, 0x1b, 0xb8, 0xb9, 0x5f, 0x94, 0x32, 0xca, 0x3b, 0x9c, 0xb9, 0xfd, + 0xe0, 0xb9, 0x4b, 0x7a, 0x1e, 0xb4, 0xdb, 0x7b, 0x35, 0xf3, 0xe2, 0x57, 0xd1, 0x98, 0x21, 0xa0, 0xc3, 0x34, 0x02, + 0x6f, 0x4d, 0x29, 0x0c, 0x0f, 0x46, 0xe1, 0x31, 0x4b, 0x91, 0x79, 0xf6, 0xa1, 0xe8, 0x5a, 0x2e, 0x72, 0x9f, 0x3f, + 0xde, 0x37, 0xe0, 0xa4, 0xd5, 0xaf, 0xb4, 0x58, 0x34, 0xbe, 0xd4, 0xb5, 0xaf, 0xe4, 0xdd, 0xfa, 0xca, 0x8b, 0x63, + 0x9f, 0xc5, 0x8a, 0xf6, 0xdd, 0xaf, 0xba, 0xbc, 0x69, 0x4b, 0x0a, 0x1d, 0xae, 0x65, 0x56, 0x30, 0x1a, 0xdd, 0xc4, + 0x67, 0xc1, 0xd8, 0x55, 0x47, 0xd4, 0x30, 0x57, 0xde, 0xb4, 0x3b, 0xb6, 0x6d, 0x73, 0x85, 0xa9, 0x43, 0x3f, 0x41, + 0x61, 0x0a, 0x3f, 0xe1, 0xa1, 0x24, 0x5e, 0xec, 0x10, 0x17, 0xb1, 0x41, 0xce, 0x6a, 0x21, 0x7c, 0x47, 0xf1, 0x7c, 0x1e, 0x02, 0x1b, 0x8f, 0xfb, 0x23, 0x40, 0x73, 0x04, 0x58, 0x05, 0x4c, 0x15, 0x80, 0x0e, 0x1f, 0x02, 0xd0, 0x85, - 0x3f, 0xf7, 0xc3, 0x69, 0xb2, 0x11, 0x22, 0x54, 0x9b, 0x96, 0xe0, 0x49, 0xa9, 0x85, 0xaa, 0xe0, 0x1a, 0xce, 0xa2, - 0x00, 0xf2, 0x10, 0xa9, 0xcc, 0x9a, 0x5a, 0xca, 0x0b, 0xdb, 0xb6, 0x0d, 0xf3, 0x00, 0x32, 0xfe, 0x1d, 0x1e, 0xd9, - 0x86, 0x09, 0x7f, 0x59, 0x96, 0xd5, 0x20, 0x8f, 0xed, 0xcd, 0xfd, 0xd0, 0xa4, 0xc7, 0x96, 0xbd, 0x1b, 0xbc, 0xf7, - 0x5a, 0xf5, 0x26, 0x5c, 0x37, 0x36, 0xcc, 0x5d, 0x47, 0xb5, 0xa1, 0x9b, 0x94, 0x6d, 0xdd, 0x2c, 0x0a, 0xb8, 0xc4, - 0xe3, 0x61, 0x54, 0x88, 0xd1, 0xb0, 0xfc, 0x16, 0xd9, 0xd2, 0xb8, 0x9a, 0xc7, 0x42, 0xfd, 0x9e, 0x83, 0xd5, 0x55, - 0x5e, 0x45, 0xcb, 0x60, 0x8c, 0xe6, 0x50, 0x60, 0xbb, 0xac, 0x14, 0x56, 0xa1, 0x95, 0x64, 0x53, 0x90, 0x4b, 0x1c, - 0x13, 0xad, 0xbd, 0x47, 0xe2, 0x14, 0xc5, 0xda, 0x53, 0x9c, 0xe2, 0xcb, 0xa6, 0x2d, 0x78, 0xf5, 0x14, 0xe2, 0x09, - 0xed, 0xd0, 0x80, 0xef, 0x0b, 0xa8, 0x1f, 0xec, 0x52, 0x5f, 0xac, 0xdb, 0xd5, 0x53, 0x0a, 0x3a, 0xeb, 0x7d, 0xfa, - 0xb4, 0x37, 0xfa, 0xf4, 0x69, 0xaf, 0x96, 0xa9, 0x63, 0xf3, 0x08, 0x69, 0x63, 0x30, 0x1e, 0x62, 0x04, 0xe2, 0x06, - 0x11, 0xd0, 0xdf, 0x43, 0x79, 0xd7, 0xe3, 0xd1, 0xaa, 0xe8, 0x69, 0x64, 0xf0, 0x0f, 0xd2, 0x63, 0x90, 0x55, 0x26, - 0x65, 0xe6, 0x7a, 0x24, 0xe6, 0xf9, 0xf4, 0x89, 0x1f, 0x37, 0x63, 0xec, 0x8e, 0xf2, 0x22, 0x47, 0x35, 0x96, 0x6e, - 0x90, 0x3f, 0xaa, 0x08, 0xf2, 0x92, 0x63, 0xcc, 0x02, 0xe2, 0x95, 0x17, 0x87, 0x32, 0xc0, 0x3f, 0x46, 0x0a, 0xff, - 0xac, 0xc2, 0x23, 0xa2, 0x8e, 0xab, 0xab, 0x31, 0x71, 0x99, 0xb6, 0x24, 0x1c, 0x28, 0x2c, 0xdd, 0xa4, 0x0e, 0x2e, - 0x04, 0xb6, 0xc7, 0xb4, 0xa8, 0x62, 0x80, 0xe8, 0x5f, 0x8d, 0x27, 0x77, 0x2c, 0x86, 0xf5, 0xce, 0x5b, 0x75, 0x97, - 0xe2, 0xe1, 0x8c, 0x4c, 0xe2, 0xbb, 0x93, 0xdc, 0x6f, 0x79, 0x41, 0x5e, 0x87, 0x53, 0xf7, 0xdb, 0x58, 0x5b, 0x18, - 0xa9, 0xa1, 0x0a, 0x32, 0xa2, 0xea, 0xc6, 0xbc, 0x29, 0xc0, 0x6a, 0x6f, 0xce, 0xc3, 0xcd, 0x68, 0x62, 0x2b, 0x5c, - 0x4f, 0xd0, 0x57, 0x21, 0x1c, 0xdd, 0x61, 0x00, 0xe5, 0xe2, 0x3d, 0x81, 0x72, 0xcd, 0xb3, 0xef, 0x8d, 0xe5, 0x57, - 0xb0, 0xe0, 0xaa, 0x31, 0xd1, 0x0d, 0xf2, 0x01, 0x98, 0x7e, 0x49, 0x73, 0x7f, 0x8a, 0xa9, 0x3c, 0x97, 0xc2, 0xbd, - 0x0a, 0x07, 0x80, 0xeb, 0x8a, 0x03, 0x40, 0xc3, 0x7c, 0x2a, 0x31, 0x4b, 0x16, 0x51, 0x08, 0x77, 0xc5, 0xeb, 0xc2, - 0xc3, 0xeb, 0xba, 0xee, 0xe1, 0xd5, 0xd0, 0x14, 0xdf, 0x50, 0x3b, 0x50, 0x49, 0x5f, 0xfc, 0xa5, 0x62, 0xa1, 0x2f, - 0x48, 0x3d, 0x66, 0x29, 0x3f, 0xdb, 0xe4, 0xd9, 0xfd, 0xfd, 0xfd, 0x9e, 0xdd, 0xe7, 0x3b, 0x79, 0x76, 0x7f, 0xff, + 0x3f, 0xf7, 0xc3, 0x69, 0xd2, 0x08, 0x11, 0xaa, 0x4d, 0x4b, 0xf0, 0xa4, 0xd4, 0x42, 0x55, 0x70, 0x0d, 0x67, 0x51, + 0x00, 0x79, 0x88, 0x54, 0x66, 0x4d, 0x2d, 0xe5, 0x85, 0x6d, 0xdb, 0x86, 0x79, 0x00, 0x19, 0xff, 0x0e, 0x8f, 0x6c, + 0xc3, 0x84, 0xbf, 0x2c, 0xcb, 0xaa, 0x91, 0xc7, 0xf6, 0xe6, 0x7e, 0x68, 0xd2, 0x63, 0xcb, 0xde, 0x0d, 0xde, 0x7b, + 0xad, 0x7a, 0x13, 0xae, 0x1b, 0x1b, 0xe6, 0xae, 0xa3, 0xda, 0xd0, 0x4d, 0xca, 0xb6, 0x6e, 0x16, 0x05, 0x5c, 0xe2, + 0xf1, 0x30, 0x2a, 0xc4, 0x68, 0x58, 0x7e, 0x8b, 0x6c, 0x69, 0x5c, 0xcd, 0x63, 0xa1, 0x7e, 0xcf, 0xc1, 0xea, 0x2a, + 0xaf, 0xa2, 0x65, 0x30, 0x46, 0x73, 0x28, 0xb0, 0x5d, 0x56, 0x0a, 0xab, 0xd0, 0x4a, 0xb2, 0x29, 0xc8, 0x25, 0x8e, + 0x89, 0xd6, 0xde, 0x23, 0x71, 0x8a, 0x62, 0xed, 0x29, 0x4e, 0xf1, 0x65, 0xdd, 0x16, 0xbc, 0x7a, 0x0a, 0xf1, 0x84, + 0x76, 0x68, 0xc0, 0xf7, 0x05, 0xd4, 0x0f, 0x76, 0xa9, 0x2f, 0xd6, 0xed, 0xea, 0x29, 0x05, 0x9d, 0xf5, 0x3e, 0x7d, + 0xda, 0x1b, 0x7d, 0xfa, 0xb4, 0xb7, 0x91, 0xa9, 0xa3, 0x79, 0x84, 0xb4, 0x31, 0x18, 0x0f, 0x31, 0x02, 0x71, 0x83, + 0x08, 0xe8, 0xef, 0xa1, 0xbc, 0xeb, 0xf1, 0x68, 0x55, 0xf4, 0x34, 0x32, 0xf8, 0x07, 0xe9, 0x31, 0xc8, 0x2a, 0x93, + 0x32, 0x73, 0x3d, 0x12, 0xf3, 0x7c, 0xfa, 0xc4, 0x8f, 0x9b, 0x31, 0x76, 0x47, 0x79, 0x91, 0xa3, 0x1a, 0x4b, 0x37, + 0xc8, 0x1f, 0x55, 0x04, 0x79, 0xc9, 0x31, 0x66, 0x01, 0xf1, 0xca, 0x8b, 0x43, 0x19, 0xe0, 0x9f, 0x22, 0x85, 0x7f, + 0x56, 0xe1, 0x11, 0x51, 0xc7, 0xd5, 0xd5, 0x98, 0xb8, 0x4c, 0x5b, 0x12, 0x0e, 0x14, 0x96, 0x6e, 0x52, 0x07, 0x17, + 0x02, 0xdb, 0x63, 0x5a, 0x54, 0x31, 0x40, 0xf4, 0xaf, 0xc6, 0x93, 0x3b, 0x16, 0xc3, 0x7a, 0xe7, 0xad, 0xba, 0x4b, + 0xf1, 0x70, 0x46, 0x26, 0xf1, 0xdd, 0x49, 0xee, 0xb7, 0xbc, 0x20, 0xaf, 0xc3, 0xa9, 0xfb, 0x6d, 0xac, 0x2d, 0x8c, + 0xd4, 0x50, 0x05, 0x19, 0x51, 0x75, 0x63, 0x5e, 0x17, 0x60, 0xb5, 0x37, 0xe7, 0xe1, 0x66, 0x34, 0xb1, 0x15, 0xae, + 0x27, 0xe8, 0xab, 0x10, 0x8e, 0xee, 0x30, 0x80, 0x72, 0xf1, 0x9e, 0x40, 0xb9, 0xe6, 0xd9, 0xf7, 0xc6, 0xf2, 0x2b, + 0x58, 0x70, 0xd5, 0x98, 0xe8, 0x06, 0xf9, 0x00, 0x4c, 0xbf, 0xa4, 0xb9, 0x3f, 0xc5, 0x54, 0x9e, 0x4b, 0xe1, 0x5e, + 0x85, 0x03, 0xc0, 0x75, 0xc5, 0x01, 0xa0, 0x66, 0x3e, 0x95, 0x98, 0x25, 0x8b, 0x28, 0x84, 0xbb, 0xe2, 0x75, 0xe1, + 0xe1, 0x75, 0xbd, 0xe9, 0xe1, 0x55, 0xd3, 0x14, 0xdf, 0x50, 0x3b, 0x50, 0x49, 0x5f, 0xfc, 0x57, 0xc5, 0x42, 0x5f, + 0x90, 0x7a, 0xcc, 0x52, 0x7e, 0xd6, 0xe4, 0xd9, 0xfd, 0xfd, 0xfd, 0x9e, 0xdd, 0xe7, 0x3b, 0x79, 0x76, 0x7f, 0xff, 0xc5, 0x3d, 0xbb, 0xcf, 0x64, 0xcf, 0x6e, 0x20, 0xc1, 0x67, 0x6c, 0x27, 0x47, 0x5a, 0xe1, 0xd2, 0x12, 0xad, 0x12, - 0xd7, 0xe1, 0x9a, 0xb5, 0x64, 0x34, 0x63, 0x60, 0xaa, 0xc0, 0x59, 0xdd, 0x20, 0x9a, 0x82, 0xbf, 0xeb, 0x66, 0x8f, - 0xd6, 0x2f, 0xe5, 0xcf, 0x1a, 0x44, 0x53, 0x55, 0xca, 0xd3, 0x16, 0x8a, 0x3c, 0x6d, 0x10, 0x9b, 0xee, 0xef, 0xb7, - 0xce, 0xcb, 0x4b, 0xa7, 0xd7, 0x76, 0x20, 0xce, 0x29, 0x68, 0x9f, 0xb1, 0xc0, 0xee, 0xb5, 0xdb, 0x50, 0xb0, 0x92, - 0x0a, 0x5a, 0x50, 0xe0, 0x4b, 0x05, 0x87, 0x50, 0x30, 0x92, 0x0a, 0x8e, 0xa0, 0x60, 0x2c, 0x15, 0x1c, 0x43, 0xc1, - 0x8d, 0x9a, 0x5d, 0x86, 0xb9, 0xdf, 0xfa, 0xb1, 0x7e, 0x55, 0x4a, 0xd1, 0x99, 0x9b, 0x4a, 0x88, 0x2a, 0xc7, 0x86, - 0xc8, 0x17, 0x61, 0x1e, 0xe8, 0x9c, 0x47, 0x1b, 0x7c, 0x35, 0x00, 0xcc, 0x0b, 0x96, 0x23, 0x06, 0xd8, 0xdd, 0x50, - 0xcd, 0xb6, 0x78, 0xad, 0x76, 0x73, 0x3f, 0x6f, 0xdb, 0x68, 0x09, 0xbf, 0xe9, 0x2e, 0x46, 0xf1, 0x10, 0x95, 0x0f, - 0x9f, 0xcf, 0xf2, 0xe0, 0xd1, 0x4b, 0xb7, 0x08, 0x86, 0xd3, 0x86, 0x14, 0x3a, 0x9c, 0x57, 0x63, 0x1a, 0xd8, 0xcb, - 0x40, 0xac, 0x13, 0x71, 0x8a, 0xc4, 0x07, 0x14, 0x74, 0x86, 0xef, 0x79, 0x05, 0x0f, 0xc7, 0x43, 0xad, 0x13, 0xf4, - 0xf3, 0x3c, 0x82, 0x35, 0xe9, 0x52, 0x97, 0x46, 0xea, 0x4d, 0xbb, 0x33, 0x83, 0x0c, 0xa9, 0xba, 0x53, 0x48, 0x49, - 0x72, 0x3a, 0xee, 0x2e, 0x8c, 0xd5, 0x8c, 0x85, 0xdd, 0x09, 0x77, 0x3b, 0x84, 0xf5, 0x27, 0x4f, 0x92, 0xb9, 0x2e, - 0x5c, 0xa0, 0x70, 0x4f, 0x14, 0x6f, 0x09, 0x4a, 0x33, 0xdf, 0x4a, 0x85, 0xf7, 0x8e, 0x26, 0x1b, 0x59, 0x7d, 0x09, - 0x5f, 0x8b, 0xd7, 0x6c, 0xb8, 0x9c, 0x2a, 0xe7, 0xd1, 0xf4, 0x5e, 0xbf, 0x0a, 0xf9, 0x15, 0x40, 0xa9, 0x92, 0x35, - 0xa9, 0x29, 0xb6, 0x37, 0xff, 0x16, 0x3d, 0x66, 0xe5, 0xfa, 0x29, 0xc0, 0xa6, 0xa4, 0xc4, 0x36, 0xc0, 0x77, 0x60, - 0xb6, 0x25, 0xcf, 0x85, 0x73, 0x98, 0x3f, 0xe9, 0xf9, 0xc2, 0x93, 0xe0, 0xe9, 0xff, 0xc0, 0x92, 0xc4, 0x9b, 0x32, - 0x19, 0xb5, 0x94, 0x3a, 0x07, 0x2c, 0x98, 0xab, 0x93, 0x71, 0x02, 0x81, 0xb1, 0xf7, 0x6b, 0xfe, 0x28, 0xe0, 0x32, + 0xd7, 0xe1, 0x9a, 0xb5, 0x64, 0x34, 0x63, 0x60, 0xaa, 0xc0, 0x59, 0xdd, 0x20, 0x9a, 0x82, 0xbf, 0x6b, 0xb3, 0x47, + 0xeb, 0x97, 0xf2, 0x67, 0x0d, 0xa2, 0xa9, 0x2a, 0xe5, 0x69, 0x0b, 0x45, 0x9e, 0x36, 0x88, 0x4d, 0xf7, 0xb7, 0x5b, + 0xe7, 0xe5, 0xa5, 0xd3, 0x6b, 0x3b, 0x10, 0xe7, 0x14, 0xb4, 0xcf, 0x58, 0x60, 0xf7, 0xda, 0x6d, 0x28, 0x58, 0x49, + 0x05, 0x2d, 0x28, 0xf0, 0xa5, 0x82, 0x43, 0x28, 0x18, 0x49, 0x05, 0x47, 0x50, 0x30, 0x96, 0x0a, 0x8e, 0xa1, 0xe0, + 0x46, 0xcd, 0x2e, 0xc3, 0xdc, 0x6f, 0xfd, 0x58, 0xbf, 0x2a, 0xa5, 0xe8, 0xcc, 0x4d, 0x25, 0x44, 0x95, 0x63, 0x43, + 0xe4, 0x8b, 0x30, 0x0f, 0x74, 0xce, 0xa3, 0x0d, 0xbe, 0x1a, 0x00, 0xe6, 0x05, 0xcb, 0x11, 0x03, 0xec, 0x6e, 0xa8, + 0x66, 0x5b, 0xbc, 0x56, 0xbb, 0xb9, 0x9f, 0xb7, 0x6d, 0xb4, 0x84, 0xdf, 0x74, 0x17, 0xa3, 0x78, 0x88, 0xca, 0x87, + 0xcf, 0x67, 0x79, 0xf0, 0xe8, 0xa5, 0x5b, 0x04, 0xc3, 0x69, 0x43, 0x0a, 0x1d, 0xce, 0xab, 0x31, 0x0d, 0xec, 0x65, + 0x20, 0xd6, 0x89, 0x38, 0x45, 0xe2, 0x03, 0x0a, 0x3a, 0xc3, 0xf7, 0xbc, 0x82, 0x87, 0xe3, 0xa1, 0xd6, 0x09, 0xfa, + 0x79, 0x1e, 0xc1, 0x9a, 0x74, 0xa9, 0x4b, 0x23, 0xf5, 0xa6, 0xdd, 0x99, 0x41, 0x86, 0x54, 0xdd, 0x29, 0xa4, 0x24, + 0x39, 0x1d, 0x77, 0x17, 0xc6, 0x6a, 0xc6, 0xc2, 0xee, 0x84, 0xbb, 0x1d, 0xc2, 0xfa, 0x93, 0x27, 0xc9, 0x5c, 0x17, + 0x2e, 0x50, 0xb8, 0x27, 0x8a, 0xb7, 0x04, 0xa5, 0x99, 0x6f, 0xa5, 0xc2, 0x7b, 0x47, 0x93, 0x8d, 0xac, 0xbe, 0x84, + 0xaf, 0xc5, 0x6b, 0x36, 0x5c, 0x4e, 0x95, 0xf3, 0x68, 0x7a, 0xaf, 0x5f, 0x85, 0xfc, 0x0a, 0xa0, 0x54, 0xc9, 0x9a, + 0xd4, 0x14, 0xdb, 0x9b, 0x7f, 0x8b, 0x1e, 0xb3, 0x72, 0xfd, 0x14, 0x60, 0x53, 0x52, 0x62, 0x1b, 0xe0, 0x3b, 0x30, + 0xdb, 0x92, 0xe7, 0xc2, 0x39, 0xcc, 0x9f, 0xf4, 0x7c, 0xe1, 0x49, 0xf0, 0xf4, 0x7f, 0x64, 0x49, 0xe2, 0x4d, 0x99, + 0x8c, 0x5a, 0x4a, 0x9d, 0x03, 0x16, 0xcc, 0xd5, 0xc9, 0x38, 0x81, 0xc0, 0xd8, 0xfb, 0x1b, 0xfe, 0x28, 0xe0, 0x32, 0x0b, 0x7e, 0x5a, 0xb0, 0x68, 0x85, 0xf3, 0x86, 0x6f, 0xc1, 0xf2, 0x94, 0xfd, 0x28, 0x00, 0x89, 0xdc, 0xb0, 0xa0, 0x5a, 0x98, 0x7a, 0xd3, 0x6a, 0x11, 0xad, 0x75, 0x56, 0x42, 0x7b, 0x7a, 0xe9, 0x51, 0xe0, 0xc2, 0xcf, 0xb0, 0xcb, - 0x0f, 0xa2, 0xe9, 0x6f, 0x6a, 0x94, 0xbf, 0xc5, 0x99, 0xe2, 0x87, 0xd0, 0x08, 0xd3, 0x81, 0x85, 0x73, 0xac, 0x58, + 0x0f, 0xa2, 0xe9, 0xef, 0x6a, 0x94, 0xbf, 0xc5, 0x99, 0xe2, 0xc7, 0xd0, 0x08, 0xd3, 0x81, 0x85, 0x73, 0xac, 0x58, 0x30, 0x85, 0xdd, 0x30, 0x9d, 0x99, 0x18, 0x58, 0x4e, 0x6b, 0x85, 0xba, 0x61, 0xe1, 0xda, 0xae, 0xab, 0xe1, 0x34, 0xbb, 0xf1, 0x74, 0xe8, 0x69, 0x4e, 0xeb, 0xd8, 0x10, 0x7f, 0x2c, 0xfb, 0x50, 0xcf, 0xb0, 0x07, 0x65, 0xec, 0xdf, 0xac, 0x27, 0x51, 0x98, 0x9a, 0x13, 0x6f, 0xee, 0x07, 0x77, 0xdd, 0x79, 0x14, 0x46, 0xc9, 0xc2, 0x1b, 0xb1, 0x9e, 0xc4, 0x8f, 0x62, 0xa0, 0x66, 0x1e, 0x2b, 0xd0, 0xb1, 0x5a, 0x31, 0x9b, 0x53, 0xeb, 0x3c, 0x0e, 0xf3, 0x24, 0x60, 0xb7, 0x19, 0xff, 0x7c, 0xa9, 0x32, 0x55, 0xc5, 0x2d, 0x47, 0x2d, 0x80, 0x65, 0xe6, 0x41, 0x9e, 0x21, 0xb5, 0x41, - 0x8f, 0x4b, 0x1d, 0xbb, 0x56, 0xeb, 0x30, 0x66, 0x73, 0xc5, 0x3a, 0xdc, 0xd8, 0x79, 0x1c, 0xad, 0xfa, 0x00, 0x2d, - 0x36, 0x36, 0x13, 0x16, 0x4c, 0xf0, 0x8d, 0x89, 0x71, 0xa5, 0x44, 0x3f, 0x26, 0xda, 0x15, 0x40, 0x6f, 0x6c, 0xde, - 0x83, 0xd7, 0xdd, 0x96, 0x62, 0x4b, 0xfc, 0xf4, 0xb1, 0xbd, 0x90, 0xfa, 0x92, 0xe7, 0x4f, 0x5f, 0x63, 0x75, 0x47, - 0xb1, 0x7b, 0xa0, 0x3f, 0x9e, 0x04, 0xd1, 0xaa, 0x3b, 0xf3, 0xc7, 0x63, 0x16, 0xf6, 0x10, 0xe6, 0xbc, 0x90, 0x05, - 0x81, 0xbf, 0x48, 0xfc, 0xa4, 0x37, 0xf7, 0x6e, 0x79, 0xaf, 0x07, 0x9b, 0x7a, 0x6d, 0xf3, 0x5e, 0xdb, 0x3b, 0xf7, - 0x2a, 0x75, 0x03, 0x31, 0xac, 0xa8, 0x1f, 0x0e, 0xda, 0xa1, 0x62, 0x57, 0xc6, 0xb9, 0x73, 0xaf, 0x8b, 0x98, 0xad, - 0xe7, 0x5e, 0x3c, 0xf5, 0xc3, 0xae, 0x9d, 0x59, 0x37, 0x6b, 0xda, 0x18, 0x8f, 0x3a, 0x9d, 0x4e, 0x66, 0x8d, 0xc5, - 0x93, 0x3d, 0x1e, 0x67, 0xd6, 0x48, 0x3c, 0x4d, 0x26, 0xb6, 0x3d, 0x99, 0x64, 0x96, 0x2f, 0x0a, 0xda, 0xad, 0xd1, - 0xb8, 0xdd, 0xca, 0xac, 0x95, 0x54, 0x23, 0xb3, 0x18, 0x7f, 0x8a, 0xd9, 0xb8, 0x87, 0x1b, 0x89, 0xfb, 0x3f, 0x1f, - 0xdb, 0x76, 0x86, 0x18, 0xe0, 0xb2, 0x84, 0x9b, 0xd0, 0x74, 0xe5, 0x6a, 0xbd, 0x73, 0x4d, 0xa5, 0xf8, 0xdc, 0x68, - 0xd4, 0x58, 0x6f, 0xec, 0xc5, 0x1f, 0xaf, 0x14, 0x69, 0x14, 0x9e, 0x47, 0xd5, 0xd6, 0x62, 0x1a, 0xcc, 0xdb, 0x2e, - 0x24, 0xec, 0xe8, 0x0d, 0xa3, 0x18, 0xce, 0x6c, 0xec, 0x8d, 0xfd, 0x65, 0xd2, 0x75, 0x5a, 0x8b, 0x5b, 0x51, 0xc4, - 0xf7, 0x7a, 0x51, 0x80, 0x67, 0xaf, 0x9b, 0x44, 0x81, 0x3f, 0x16, 0x45, 0x9b, 0xce, 0x92, 0xd3, 0xd2, 0x7b, 0xc8, - 0xbf, 0xfa, 0x18, 0x74, 0xd9, 0x0b, 0x02, 0xc5, 0x6a, 0x27, 0x0a, 0xf3, 0x12, 0x34, 0x97, 0x53, 0xec, 0x84, 0xe6, - 0x05, 0x43, 0xd3, 0x3a, 0x07, 0x8b, 0xdb, 0x7c, 0xcf, 0x3b, 0x47, 0x8b, 0xdb, 0xec, 0xeb, 0x39, 0x1b, 0xfb, 0x9e, - 0xa2, 0x15, 0xbb, 0xc9, 0xb1, 0xc1, 0xa4, 0x4e, 0x5f, 0x6f, 0xd8, 0xa6, 0xe2, 0x58, 0x40, 0x62, 0xa3, 0x3d, 0x7f, - 0x0e, 0x72, 0x18, 0x2f, 0x4c, 0xb3, 0x6c, 0x70, 0x95, 0x65, 0xbd, 0x73, 0x5f, 0xbb, 0xfc, 0xab, 0x46, 0xb4, 0x90, - 0x4c, 0x50, 0x33, 0xfd, 0xca, 0x38, 0x63, 0xb2, 0xbb, 0x0c, 0x90, 0x31, 0x74, 0x95, 0x91, 0x2b, 0x13, 0xbd, 0xad, - 0x57, 0xa6, 0x49, 0xce, 0xab, 0x93, 0xf7, 0x4d, 0xb9, 0x0a, 0x52, 0x20, 0xa8, 0x70, 0xc6, 0xdc, 0x73, 0xc9, 0xf7, - 0x06, 0x98, 0x1e, 0xac, 0x4c, 0x51, 0x85, 0x5e, 0x6f, 0xe2, 0x3d, 0x2f, 0xee, 0xe7, 0x3d, 0xff, 0x96, 0xee, 0xc2, - 0x7b, 0x5e, 0x7c, 0x71, 0xde, 0xf3, 0x75, 0x3d, 0xaa, 0xd0, 0x45, 0xe4, 0xaa, 0xb9, 0xc1, 0x24, 0x90, 0xa6, 0x98, - 0xe2, 0xf5, 0xbf, 0x4e, 0x7f, 0x6d, 0x78, 0x17, 0xd1, 0x1b, 0x12, 0x05, 0xce, 0xa7, 0x82, 0x98, 0xf5, 0x6d, 0xe8, - 0xfe, 0x29, 0x96, 0x9f, 0x27, 0x13, 0xf7, 0x75, 0x24, 0x15, 0xe4, 0x4f, 0xdc, 0x97, 0xa4, 0x14, 0x5b, 0x99, 0xde, - 0xe4, 0xde, 0x3e, 0x90, 0x7d, 0x1a, 0x42, 0xb3, 0x92, 0x6b, 0xf7, 0x38, 0xf7, 0xb9, 0xeb, 0x95, 0x41, 0xd0, 0x72, - 0x27, 0x57, 0x11, 0x80, 0xab, 0x66, 0x19, 0x35, 0x65, 0x42, 0x06, 0xf0, 0xf2, 0xee, 0xfb, 0xb1, 0x76, 0x11, 0xe9, - 0x99, 0x9f, 0xbc, 0xad, 0x86, 0xbf, 0x12, 0x7a, 0x2e, 0x79, 0x38, 0x19, 0xf7, 0x9b, 0x93, 0xa2, 0xdc, 0xe2, 0x6b, - 0x6a, 0x7e, 0x5a, 0x1a, 0x69, 0x57, 0x6e, 0xd8, 0xa3, 0x98, 0xdf, 0x35, 0x62, 0xcc, 0xc3, 0xc4, 0xac, 0x39, 0x97, + 0x8f, 0x4b, 0x1d, 0xbb, 0x56, 0xeb, 0x30, 0x66, 0x73, 0xc5, 0x3a, 0x6c, 0xec, 0x3c, 0x8e, 0x56, 0x7d, 0x80, 0x16, + 0x1b, 0x9b, 0x09, 0x0b, 0x26, 0xf8, 0xc6, 0xc4, 0xb8, 0x52, 0xa2, 0x1f, 0x13, 0xed, 0x0a, 0xa0, 0x37, 0x36, 0xef, + 0xc1, 0xeb, 0x6e, 0x4b, 0xb1, 0x25, 0x7e, 0xfa, 0xd8, 0x5e, 0x48, 0x7d, 0xc9, 0xf3, 0xa7, 0xaf, 0xb1, 0xba, 0xa3, + 0xd8, 0x3d, 0xd0, 0x1f, 0x4f, 0x82, 0x68, 0xd5, 0x9d, 0xf9, 0xe3, 0x31, 0x0b, 0x7b, 0x08, 0x73, 0x5e, 0xc8, 0x82, + 0xc0, 0x5f, 0x24, 0x7e, 0xd2, 0x9b, 0x7b, 0xb7, 0xbc, 0xd7, 0x83, 0xa6, 0x5e, 0xdb, 0xbc, 0xd7, 0xf6, 0xce, 0xbd, + 0x4a, 0xdd, 0x40, 0x0c, 0x2b, 0xea, 0x87, 0x83, 0x76, 0xa8, 0xd8, 0x95, 0x71, 0xee, 0xdc, 0xeb, 0x22, 0x66, 0xeb, + 0xb9, 0x17, 0x4f, 0xfd, 0xb0, 0x6b, 0x67, 0xd6, 0xcd, 0x9a, 0x36, 0xc6, 0xa3, 0x4e, 0xa7, 0x93, 0x59, 0x63, 0xf1, + 0x64, 0x8f, 0xc7, 0x99, 0x35, 0x12, 0x4f, 0x93, 0x89, 0x6d, 0x4f, 0x26, 0x99, 0xe5, 0x8b, 0x82, 0x76, 0x6b, 0x34, + 0x6e, 0xb7, 0x32, 0x6b, 0x25, 0xd5, 0xc8, 0x2c, 0xc6, 0x9f, 0x62, 0x36, 0xee, 0xe1, 0x46, 0xe2, 0xfe, 0xcf, 0xc7, + 0xb6, 0x9d, 0x21, 0x06, 0xb8, 0x2c, 0xe1, 0x26, 0x34, 0x5d, 0xb9, 0x5a, 0xef, 0x5c, 0x53, 0x29, 0x3e, 0x37, 0x1a, + 0xd5, 0xd6, 0x1b, 0x7b, 0xf1, 0xc7, 0x2b, 0x45, 0x1a, 0x85, 0xe7, 0x51, 0xb5, 0xb5, 0x98, 0x06, 0xf3, 0xb6, 0x0b, + 0x09, 0x3b, 0x7a, 0xc3, 0x28, 0x86, 0x33, 0x1b, 0x7b, 0x63, 0x7f, 0x99, 0x74, 0x9d, 0xd6, 0xe2, 0x56, 0x14, 0xf1, + 0xbd, 0x5e, 0x14, 0xe0, 0xd9, 0xeb, 0x26, 0x51, 0xe0, 0x8f, 0x45, 0x51, 0xd3, 0x59, 0x72, 0x5a, 0x7a, 0x0f, 0xf9, + 0x57, 0x1f, 0x83, 0x2e, 0x7b, 0x41, 0xa0, 0x58, 0xed, 0x44, 0x61, 0x5e, 0x82, 0xe6, 0x72, 0x8a, 0x9d, 0xd0, 0xbc, + 0x60, 0x68, 0x5a, 0xe7, 0x60, 0x71, 0x9b, 0xef, 0x79, 0xe7, 0x68, 0x71, 0x9b, 0x7d, 0x3d, 0x67, 0x63, 0xdf, 0x53, + 0xb4, 0x62, 0x37, 0x39, 0x36, 0x98, 0xd4, 0xe9, 0xeb, 0x86, 0x6d, 0x2a, 0x8e, 0x05, 0x24, 0x36, 0xda, 0xf3, 0xe7, + 0x20, 0x87, 0xf1, 0xc2, 0x34, 0xcb, 0x06, 0x57, 0x59, 0xd6, 0x3b, 0xf7, 0xb5, 0xcb, 0xff, 0xd6, 0x88, 0x16, 0x92, + 0x09, 0x6a, 0xa6, 0x5f, 0x19, 0x67, 0x4c, 0x76, 0x97, 0x01, 0x32, 0x86, 0xae, 0x32, 0x72, 0x65, 0xa2, 0xb7, 0x9b, + 0x95, 0x69, 0x92, 0xf3, 0xea, 0xe4, 0x7d, 0x53, 0xae, 0x82, 0x14, 0x08, 0x2a, 0x9c, 0x31, 0xf7, 0x5c, 0xf2, 0xbd, + 0x01, 0xa6, 0x07, 0x2b, 0x53, 0x54, 0xa1, 0xd7, 0x4d, 0xbc, 0xe7, 0xc5, 0xfd, 0xbc, 0xe7, 0x5f, 0xd3, 0x5d, 0x78, + 0xcf, 0x8b, 0x2f, 0xce, 0x7b, 0xbe, 0xde, 0x8c, 0x2a, 0x74, 0x11, 0xb9, 0x6a, 0x6e, 0x30, 0x09, 0xa4, 0x29, 0xa6, + 0x78, 0xfd, 0xaf, 0xd3, 0xdf, 0x1a, 0xde, 0x45, 0xf4, 0x86, 0x44, 0x81, 0xf3, 0xa9, 0x20, 0x66, 0x7d, 0x1b, 0xba, + 0x7f, 0x8e, 0xe5, 0xe7, 0xc9, 0xc4, 0x7d, 0x1d, 0x49, 0x05, 0xf9, 0x13, 0xf7, 0x25, 0x29, 0xc5, 0x56, 0xa6, 0x37, + 0xb9, 0xb7, 0x0f, 0x64, 0x9f, 0x86, 0xd0, 0xac, 0xe4, 0xda, 0x3d, 0xce, 0x7d, 0xee, 0x7a, 0x65, 0x10, 0xb4, 0xdc, + 0xc9, 0x55, 0x04, 0xe0, 0xda, 0xb0, 0x8c, 0x9a, 0x32, 0x21, 0x03, 0x78, 0x79, 0xf7, 0xfd, 0x58, 0xbb, 0x88, 0xf4, + 0xcc, 0x4f, 0xde, 0x56, 0xc3, 0x5f, 0x09, 0x3d, 0x97, 0x3c, 0x9c, 0x8c, 0xfb, 0xcd, 0x49, 0x51, 0x6e, 0xf1, 0x35, + 0x35, 0x3f, 0x2d, 0x8d, 0xb4, 0x2b, 0x37, 0xec, 0x51, 0xcc, 0xef, 0x0d, 0x62, 0xcc, 0xc3, 0xc4, 0xac, 0x39, 0x97, 0xb7, 0xc6, 0x67, 0x88, 0x1a, 0x3a, 0xa6, 0xe6, 0xfe, 0x38, 0xcb, 0xf4, 0x9e, 0x98, 0x08, 0x89, 0xd0, 0xb2, 0xfb, 0x98, 0xb8, 0xa4, 0x10, 0x02, 0x71, 0x89, 0x0f, 0x59, 0x33, 0x5f, 0x80, 0x7f, 0x00, 0xb7, 0x7d, 0xe6, 0x73, 0xa6, - 0x2a, 0x34, 0x7d, 0xe4, 0x37, 0x22, 0x0d, 0x08, 0x0c, 0xda, 0x65, 0x6f, 0xab, 0xd2, 0x82, 0xd4, 0x1d, 0x5b, 0x69, - 0x72, 0xd0, 0xc1, 0x01, 0x62, 0xfc, 0x0a, 0xb1, 0x10, 0xa1, 0x1d, 0x5e, 0x07, 0x1f, 0x32, 0x35, 0xe7, 0xfd, 0x70, - 0xfb, 0xf5, 0x4f, 0xf6, 0xa1, 0x41, 0xbf, 0xa2, 0x74, 0xbb, 0xc7, 0x2f, 0x13, 0x58, 0x89, 0x64, 0x65, 0x58, 0xc9, - 0x4a, 0x79, 0xb6, 0x16, 0xf1, 0xb1, 0x53, 0x6f, 0x61, 0x82, 0x96, 0x07, 0x71, 0x2f, 0xc7, 0x78, 0x52, 0x28, 0xee, - 0xde, 0x32, 0x01, 0xdc, 0x88, 0x72, 0x14, 0xc4, 0x3f, 0xbd, 0xd1, 0x32, 0x4e, 0xa2, 0xb8, 0xbb, 0x88, 0xfc, 0x30, - 0x65, 0x71, 0x46, 0x82, 0x15, 0x9c, 0x1f, 0x31, 0x3d, 0x57, 0xeb, 0x68, 0xe1, 0x8d, 0xfc, 0xf4, 0xae, 0x6b, 0x73, - 0x96, 0xc2, 0xee, 0x71, 0xee, 0xc0, 0x6e, 0xac, 0xdf, 0xe5, 0xb3, 0xf9, 0x1c, 0x19, 0xbf, 0xb8, 0xce, 0xce, 0xc8, - 0xdb, 0xbc, 0x27, 0xbd, 0xa5, 0x08, 0xe1, 0xc0, 0x7e, 0x78, 0xb1, 0x39, 0x05, 0x2c, 0x0f, 0x4b, 0x6d, 0x8f, 0xd9, - 0xd4, 0x40, 0xac, 0x0d, 0x66, 0x86, 0xe2, 0x8f, 0x75, 0xa8, 0x2b, 0x76, 0x73, 0x31, 0x70, 0x3c, 0xfa, 0x2e, 0x90, - 0x75, 0xbd, 0x49, 0xca, 0x62, 0x63, 0x97, 0x9a, 0x43, 0x36, 0x89, 0x62, 0x46, 0xd9, 0xe4, 0x9c, 0xce, 0xe2, 0x76, - 0xf7, 0xee, 0xb7, 0x0f, 0xbf, 0xb9, 0x9f, 0x30, 0x4a, 0x35, 0xd1, 0x99, 0x7e, 0x4f, 0x6f, 0x75, 0x7a, 0x06, 0xac, - 0x21, 0xcd, 0xfc, 0x88, 0xa4, 0x20, 0x10, 0x09, 0xac, 0x31, 0x69, 0xc7, 0x22, 0xe2, 0x34, 0x2f, 0x66, 0x81, 0x97, - 0xfa, 0x37, 0x82, 0x67, 0x6c, 0x1f, 0x2d, 0x6e, 0xc5, 0x1a, 0x23, 0xc1, 0x7b, 0xc0, 0x22, 0x55, 0x40, 0x11, 0x8b, - 0x54, 0x2d, 0xc6, 0x45, 0xea, 0xd5, 0x46, 0x23, 0xe2, 0x58, 0x57, 0x28, 0xfd, 0xe1, 0xe2, 0x56, 0x26, 0xd1, 0x45, - 0xb3, 0x9c, 0x52, 0x57, 0x13, 0x90, 0xcc, 0xfd, 0xf1, 0x38, 0x60, 0x59, 0x69, 0xa1, 0xcb, 0x6b, 0x29, 0x4d, 0x4e, - 0x3e, 0x0f, 0xde, 0x30, 0x89, 0x82, 0x65, 0xca, 0x9a, 0xa7, 0x4b, 0x48, 0x74, 0x8b, 0xc9, 0xc1, 0xdf, 0x65, 0x58, - 0x0f, 0x81, 0xdd, 0x86, 0x6d, 0x62, 0xf7, 0x20, 0xdf, 0xa0, 0xd9, 0x2e, 0x83, 0x0e, 0xaf, 0x72, 0xa0, 0x8d, 0x86, - 0x81, 0x18, 0x40, 0x96, 0x08, 0x7b, 0x2b, 0x96, 0xc3, 0xcb, 0xf2, 0x9c, 0x6b, 0x79, 0x51, 0x56, 0x1e, 0xcc, 0x6f, - 0x73, 0xc6, 0x5e, 0x34, 0x9f, 0xb1, 0x17, 0xe2, 0x8c, 0x6d, 0xdf, 0x99, 0x8f, 0x26, 0x0e, 0xfc, 0xd7, 0x2b, 0x06, - 0xd4, 0xb5, 0x95, 0xf6, 0xe2, 0x56, 0x71, 0x16, 0xb7, 0x8a, 0xd9, 0x5a, 0xdc, 0x2a, 0xd8, 0x35, 0xba, 0xb7, 0x18, - 0x56, 0x4b, 0x37, 0x6c, 0x05, 0x0a, 0xe1, 0x8f, 0x5d, 0x7a, 0xe5, 0x1c, 0xc0, 0x3b, 0x68, 0x75, 0x58, 0x7f, 0xd7, - 0xda, 0x7e, 0xd4, 0xe9, 0x2c, 0x09, 0xa4, 0xad, 0x5b, 0xa9, 0x37, 0x1c, 0x82, 0x28, 0x33, 0x1a, 0x2d, 0x93, 0x7f, - 0x72, 0xf8, 0xf9, 0x24, 0x6e, 0x45, 0x04, 0x95, 0x7e, 0x44, 0x53, 0x50, 0x14, 0xde, 0x30, 0xd1, 0xc3, 0x3a, 0x5f, - 0xa7, 0x2e, 0x25, 0x47, 0x6c, 0x59, 0x07, 0x0d, 0x9b, 0xbc, 0x79, 0xa2, 0x7f, 0xb3, 0x55, 0xda, 0x8c, 0x62, 0x3e, - 0x63, 0x5a, 0xb6, 0x4e, 0xc7, 0xc3, 0x67, 0x83, 0xaf, 0xa6, 0xdd, 0x69, 0x06, 0xf7, 0x52, 0x7c, 0xe9, 0x4a, 0x10, - 0x15, 0x4e, 0xb7, 0x78, 0x28, 0x8e, 0xed, 0xbd, 0x6e, 0xda, 0x23, 0xb5, 0x5e, 0xb7, 0x10, 0x84, 0xa2, 0xee, 0x8e, - 0x58, 0xfe, 0xd1, 0x8b, 0x03, 0xf8, 0x8f, 0xb8, 0xfa, 0xbf, 0xa5, 0x4d, 0x8c, 0xfa, 0xeb, 0xb4, 0xc4, 0xa8, 0x13, - 0xab, 0x84, 0x8c, 0xf8, 0xee, 0xf5, 0x27, 0x93, 0x87, 0x35, 0xd8, 0xb9, 0x36, 0x79, 0x86, 0x55, 0x6b, 0xbf, 0x8c, - 0xa2, 0x80, 0x79, 0x61, 0xbd, 0xba, 0x98, 0x1e, 0x72, 0xf3, 0x4f, 0x5d, 0x68, 0x24, 0xee, 0x11, 0xe4, 0x94, 0xa0, - 0x62, 0x1b, 0xba, 0x4a, 0x9c, 0x6f, 0xba, 0x4a, 0xbc, 0xbb, 0xff, 0x2a, 0xf1, 0xc7, 0x9d, 0xae, 0x12, 0xef, 0xbe, - 0xf8, 0x55, 0xe2, 0xbc, 0x7e, 0x95, 0x38, 0x8f, 0x84, 0x3b, 0xb0, 0xf1, 0x66, 0xc9, 0x7f, 0x7e, 0x20, 0x7b, 0xdf, - 0x77, 0x91, 0x7b, 0x68, 0x53, 0xc2, 0xc3, 0x8b, 0x5f, 0x7d, 0xb1, 0xc0, 0x8d, 0xf8, 0x0e, 0xbd, 0xe3, 0x8a, 0xab, - 0x05, 0xc7, 0xec, 0xf8, 0x1d, 0xa9, 0x38, 0x88, 0xc2, 0xe9, 0x4f, 0x60, 0xef, 0x0d, 0xe2, 0xc0, 0x58, 0x7a, 0xe1, - 0x27, 0x3f, 0x45, 0x8b, 0xe5, 0x02, 0x15, 0x55, 0x1f, 0xfc, 0xc4, 0x1f, 0x06, 0x2c, 0x8f, 0x30, 0x49, 0x5a, 0x57, - 0x2e, 0x5b, 0x07, 0xc5, 0xab, 0xf8, 0xe9, 0xdd, 0x8a, 0x9f, 0xe8, 0x62, 0xcb, 0x7f, 0x93, 0x9b, 0xa0, 0xda, 0x7c, - 0x11, 0x11, 0x16, 0x62, 0x12, 0xd0, 0x0f, 0xbf, 0x8c, 0x9c, 0x8b, 0x58, 0x5e, 0xa5, 0x51, 0x0a, 0xf7, 0x8d, 0x8d, - 0xfd, 0xb0, 0x6a, 0x3f, 0x6f, 0x96, 0xba, 0x91, 0x27, 0xe0, 0xa8, 0x8b, 0xf3, 0xe7, 0xd1, 0x32, 0x61, 0xe3, 0x68, - 0x15, 0xaa, 0x46, 0xc8, 0xf5, 0xaa, 0x11, 0xca, 0xd4, 0xf3, 0x36, 0x65, 0x85, 0xa3, 0x6a, 0x2d, 0x60, 0x0e, 0x4d, - 0xd2, 0x60, 0x9b, 0x38, 0x44, 0x55, 0x84, 0x6c, 0xea, 0xed, 0x69, 0x5a, 0xe4, 0x3e, 0xac, 0xa5, 0xf0, 0x3c, 0x89, - 0x2c, 0x2e, 0x15, 0x4e, 0xb4, 0x50, 0x08, 0x17, 0x45, 0x14, 0xec, 0x86, 0x85, 0xe3, 0x6f, 0x28, 0x42, 0x64, 0xf1, - 0x16, 0x74, 0x55, 0xd9, 0x92, 0xaf, 0x07, 0x8f, 0x09, 0x4d, 0x8f, 0xaf, 0xa4, 0x69, 0x7c, 0x7b, 0xc3, 0xe2, 0xc0, - 0xbb, 0xd3, 0xf4, 0x2c, 0x0a, 0x7f, 0x80, 0x09, 0x78, 0x1d, 0xad, 0x42, 0xb9, 0x02, 0xa6, 0x6a, 0x6f, 0xd8, 0x4b, - 0x8d, 0xd1, 0xcb, 0x21, 0x66, 0x87, 0x04, 0x81, 0x6f, 0x2d, 0xbc, 0x29, 0xfb, 0x8b, 0x41, 0xff, 0xfe, 0x55, 0xcf, - 0x8c, 0x77, 0x51, 0xfe, 0xa1, 0x9f, 0x17, 0x3b, 0x7c, 0xe6, 0xc9, 0x93, 0xbd, 0xcd, 0xc3, 0xd6, 0x46, 0x01, 0xf3, - 0x62, 0x01, 0x45, 0x43, 0x6b, 0x7d, 0xe3, 0x29, 0x00, 0x28, 0x2e, 0xa2, 0xe5, 0x68, 0x86, 0x7e, 0xbb, 0x5f, 0x6e, - 0xbc, 0x29, 0xf4, 0xc9, 0x92, 0x4b, 0xfb, 0x2a, 0x1f, 0x7a, 0xa5, 0xa8, 0x98, 0x05, 0xfc, 0xfe, 0x19, 0xa4, 0xdf, - 0xfa, 0x0f, 0x4e, 0x43, 0x7d, 0xd7, 0xe4, 0x21, 0xbf, 0x1e, 0xb4, 0x79, 0x7b, 0x3e, 0x44, 0xe5, 0xa1, 0xc0, 0xd6, - 0x42, 0x49, 0xd7, 0x8c, 0x64, 0xb2, 0xea, 0xa4, 0xc9, 0x49, 0x64, 0x36, 0xe5, 0xc7, 0x11, 0x5f, 0x61, 0x56, 0xc9, - 0x6a, 0xc4, 0x60, 0x1c, 0x5b, 0x55, 0x90, 0x0c, 0xf7, 0xa6, 0x60, 0x88, 0xbe, 0xaa, 0xef, 0xe6, 0x7e, 0x68, 0x60, - 0x0e, 0xd8, 0xfa, 0x1b, 0xef, 0x16, 0xb2, 0x20, 0x02, 0x72, 0xab, 0xbe, 0x82, 0x42, 0x43, 0x8e, 0x16, 0xe4, 0x8d, - 0xc7, 0x9a, 0xda, 0x38, 0x13, 0x42, 0x1b, 0x38, 0xf8, 0x4a, 0x51, 0x14, 0x25, 0xbf, 0x46, 0x28, 0xf9, 0x3d, 0x02, - 0xcb, 0xf1, 0x3a, 0x00, 0xda, 0x92, 0x6c, 0x71, 0x4b, 0x25, 0x70, 0x33, 0x40, 0xfb, 0x69, 0x51, 0xc0, 0x13, 0xfd, - 0x80, 0x71, 0x0b, 0x15, 0x88, 0x0b, 0x3d, 0xa8, 0xbe, 0xbd, 0x18, 0xf2, 0x01, 0x76, 0x15, 0xbc, 0xb0, 0xe3, 0x5b, - 0x2e, 0x09, 0x56, 0x6c, 0x7a, 0x1c, 0xf4, 0x58, 0x73, 0x46, 0x98, 0x50, 0xc2, 0x82, 0xa0, 0x75, 0xa8, 0x24, 0x78, - 0x34, 0x58, 0x03, 0x6e, 0xc4, 0x7b, 0xd1, 0x6d, 0x3a, 0x67, 0xe1, 0x52, 0x35, 0xc0, 0xea, 0x04, 0x33, 0xf4, 0x40, - 0x9d, 0xd7, 0xc4, 0x6c, 0x01, 0xb6, 0x69, 0x6e, 0x39, 0x23, 0x5a, 0x28, 0x4c, 0x55, 0x3c, 0x63, 0xc4, 0x03, 0xe0, - 0x24, 0x1c, 0xb7, 0x55, 0x29, 0x04, 0x5f, 0xd2, 0xa8, 0x8c, 0xcd, 0x79, 0xc8, 0x2b, 0xe4, 0x14, 0xc8, 0x46, 0x8c, - 0x8b, 0x8b, 0xc4, 0xb4, 0x6b, 0x5e, 0x75, 0xd1, 0x72, 0x8d, 0x8c, 0x57, 0x11, 0x14, 0xc5, 0x7a, 0xbd, 0x1b, 0x0e, - 0x27, 0xa4, 0x25, 0xd8, 0xd8, 0xcf, 0xa8, 0xd6, 0xcf, 0x86, 0x41, 0x7f, 0x64, 0x77, 0x44, 0x48, 0x68, 0xaa, 0x3e, - 0xb2, 0x3b, 0x30, 0x0e, 0x3f, 0x03, 0x69, 0x8a, 0xba, 0x05, 0x5d, 0x1b, 0x90, 0xe8, 0x77, 0x04, 0xa9, 0x2a, 0xb6, - 0x1c, 0x20, 0x3b, 0xdb, 0x82, 0xc5, 0x29, 0x1c, 0xa9, 0x91, 0xf4, 0xc4, 0x21, 0xe6, 0x11, 0x0b, 0xb4, 0xc6, 0x39, - 0x36, 0x1b, 0x8e, 0x86, 0xfe, 0xcc, 0xb1, 0xed, 0xfd, 0x5a, 0x7d, 0x10, 0x64, 0x37, 0xd5, 0xd6, 0x8d, 0xd4, 0x75, - 0x6c, 0xd3, 0x7f, 0x66, 0xb5, 0x7a, 0x35, 0x1a, 0x2d, 0x65, 0x92, 0x1a, 0xa0, 0xf8, 0xab, 0xff, 0x78, 0xad, 0xd5, - 0x0e, 0xa4, 0x5e, 0x8d, 0x00, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xa8, 0x93, 0x7e, 0xca, 0x63, 0x45, 0x59, 0xcd, - 0x07, 0x90, 0x0b, 0x51, 0x83, 0x63, 0xf4, 0x07, 0xe5, 0xb9, 0xa2, 0xd1, 0xf1, 0xd1, 0xf5, 0x41, 0x4f, 0x60, 0x14, - 0x11, 0x22, 0x47, 0xee, 0xa0, 0xf2, 0xc5, 0xa4, 0x8a, 0xe1, 0x78, 0xd6, 0x35, 0x56, 0x68, 0xf4, 0xb6, 0x72, 0x0b, - 0xd8, 0xff, 0x06, 0xf2, 0x69, 0x0d, 0x21, 0xc6, 0x23, 0xd4, 0x80, 0xcc, 0xa9, 0xf7, 0x76, 0x08, 0xe1, 0x79, 0xe5, - 0xee, 0xca, 0x44, 0x72, 0xf7, 0xce, 0x90, 0xe8, 0xa0, 0x0e, 0x2d, 0xef, 0xaf, 0x99, 0xdc, 0x3d, 0xb0, 0x4b, 0x16, - 0x8e, 0xcb, 0x1d, 0x56, 0xe8, 0xd7, 0xee, 0xdd, 0x95, 0x30, 0x0a, 0xa4, 0x14, 0x8e, 0x1a, 0x30, 0x4a, 0x16, 0x85, - 0xb8, 0xf9, 0xe9, 0xb8, 0xf9, 0x3b, 0x71, 0x31, 0xd8, 0x80, 0xf2, 0x81, 0xe4, 0xcd, 0x24, 0xa1, 0x38, 0xe4, 0xad, - 0xc4, 0x08, 0x5a, 0x9a, 0x60, 0x44, 0x37, 0xee, 0xc4, 0x54, 0xb8, 0x2b, 0x16, 0x6d, 0x7c, 0x9e, 0x89, 0x6a, 0x57, - 0xa9, 0xb5, 0x7f, 0xbf, 0xd4, 0x3a, 0xbd, 0x4f, 0x6a, 0x4d, 0xd1, 0x61, 0xb8, 0x3d, 0xa8, 0x88, 0x92, 0x23, 0x98, - 0x73, 0x39, 0xce, 0x50, 0x49, 0xd4, 0x8d, 0xc1, 0x64, 0x1a, 0xac, 0x48, 0xa9, 0x37, 0x72, 0x40, 0x44, 0xf1, 0xb7, - 0x74, 0x41, 0x11, 0x0a, 0x75, 0x59, 0x36, 0x7e, 0x5e, 0xc8, 0xc6, 0xe9, 0x56, 0x53, 0xc4, 0x05, 0x11, 0xdc, 0xbf, - 0x14, 0x73, 0x27, 0xbf, 0x1d, 0x14, 0xb1, 0x77, 0x0a, 0x48, 0xa5, 0x68, 0x32, 0xc5, 0x45, 0x43, 0x8a, 0x51, 0x24, - 0x6e, 0x19, 0xe5, 0x50, 0x45, 0xe5, 0xaa, 0x45, 0x30, 0x99, 0xa2, 0x1c, 0xa4, 0xee, 0x08, 0x72, 0x5e, 0x2c, 0x6f, - 0x9b, 0x72, 0x34, 0x11, 0xf9, 0xb5, 0xb4, 0x49, 0xf2, 0xb0, 0x1f, 0x34, 0xc1, 0x42, 0x4c, 0x5f, 0xd1, 0x6b, 0xe7, - 0x36, 0x10, 0x08, 0x64, 0x43, 0x94, 0xa2, 0xfb, 0xa5, 0xf3, 0x94, 0x2d, 0xb9, 0x50, 0x5d, 0x3b, 0x48, 0xdd, 0x49, - 0x13, 0x2c, 0xcb, 0x23, 0x70, 0xae, 0xaf, 0x24, 0x09, 0x42, 0xd7, 0x56, 0xec, 0x5e, 0x03, 0x03, 0x80, 0xf4, 0xbf, - 0xfa, 0xcc, 0x59, 0x01, 0x90, 0x44, 0x2a, 0xb6, 0xac, 0xf3, 0xc7, 0x43, 0x6c, 0x92, 0x25, 0x3b, 0x56, 0xad, 0x7f, - 0x93, 0xe4, 0x3d, 0x6b, 0x1e, 0x13, 0xa4, 0x2c, 0xce, 0xe7, 0x35, 0xba, 0x02, 0x0e, 0xbe, 0xcb, 0xe2, 0x65, 0x88, - 0x49, 0x70, 0xcd, 0x34, 0xf6, 0x46, 0x1f, 0xd7, 0xd2, 0xf7, 0xb8, 0x48, 0x14, 0xc4, 0xc5, 0x65, 0xa5, 0x42, 0xcf, - 0xc3, 0x9c, 0x51, 0xac, 0x6b, 0xb5, 0x12, 0x49, 0x50, 0xd3, 0x7d, 0x64, 0xb7, 0xbd, 0x17, 0x93, 0x83, 0x8a, 0xfc, - 0xb4, 0x75, 0x58, 0x96, 0xae, 0xe7, 0x70, 0xcc, 0xa3, 0x5f, 0x78, 0xf4, 0xa4, 0xdf, 0xff, 0xd3, 0x09, 0xff, 0x66, - 0x65, 0x8d, 0x3e, 0x07, 0x04, 0x68, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x5a, 0xc9, 0x26, 0xb0, 0x26, 0x7e, 0x10, - 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xcf, 0x12, 0xb2, 0x15, 0x7e, 0x46, 0x3f, 0xe5, 0x9d, 0x92, - 0xce, 0x96, 0xf3, 0xe1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0xba, 0xcc, 0x5c, 0xfe, 0xec, 0x64, 0x32, 0x29, 0x4b, - 0x8d, 0x6d, 0xe5, 0x00, 0x25, 0xbf, 0x8f, 0x6c, 0xdb, 0xae, 0xce, 0xef, 0xa6, 0x83, 0x42, 0x07, 0xc3, 0x44, 0x21, - 0x7c, 0xe7, 0xfe, 0x3d, 0xf5, 0x3b, 0x41, 0x4b, 0x5d, 0x6d, 0x3a, 0x8f, 0xb4, 0xd5, 0xfe, 0x2b, 0x40, 0x41, 0xd4, - 0x70, 0xdf, 0xf1, 0xaf, 0xef, 0x95, 0x2d, 0x3d, 0x55, 0x0f, 0xf0, 0xc3, 0x1a, 0xdf, 0xb3, 0xd7, 0x77, 0x68, 0xba, - 0x69, 0x7b, 0x67, 0x56, 0x41, 0x76, 0x4b, 0x36, 0x4b, 0x3d, 0xb2, 0x54, 0xf2, 0x53, 0x36, 0x4f, 0xba, 0x23, 0x86, - 0x0a, 0x52, 0x4b, 0xa2, 0xb6, 0x68, 0xd5, 0x63, 0x4e, 0xc1, 0x8e, 0xcb, 0x11, 0x78, 0xd8, 0x56, 0x50, 0x59, 0x55, - 0xd3, 0xac, 0x89, 0x8f, 0x20, 0x15, 0x5b, 0xd7, 0x15, 0x4e, 0xb8, 0x4d, 0x0f, 0xed, 0x3f, 0x94, 0xea, 0x29, 0xc0, - 0x9d, 0xae, 0x85, 0xb5, 0x09, 0x29, 0x4f, 0xf0, 0xef, 0x5c, 0x39, 0xf7, 0x62, 0x71, 0x5b, 0x36, 0xee, 0xea, 0x80, - 0xba, 0xa9, 0x20, 0x65, 0x04, 0x75, 0x13, 0xea, 0xcb, 0x4d, 0x80, 0x26, 0xb2, 0x75, 0x0b, 0x58, 0xd0, 0x88, 0x29, - 0xa8, 0xe8, 0x08, 0x73, 0x50, 0xf1, 0x3a, 0x0b, 0x3b, 0xaf, 0x90, 0xef, 0xe3, 0x2f, 0xc8, 0x52, 0x0e, 0xe9, 0x4e, - 0xfe, 0x60, 0x3c, 0xef, 0xa0, 0x72, 0xaf, 0xb4, 0x55, 0xd1, 0x54, 0x06, 0xf7, 0x80, 0xb8, 0x91, 0x2a, 0xcb, 0x38, - 0x30, 0x29, 0x71, 0xbd, 0xa6, 0xaf, 0xeb, 0xe3, 0xde, 0xdc, 0xbd, 0x73, 0x08, 0x7a, 0x8d, 0xfa, 0x54, 0xed, 0xa4, - 0xda, 0xab, 0xea, 0xb0, 0x05, 0x9c, 0xb0, 0x02, 0xe0, 0x33, 0xab, 0xa0, 0xd1, 0x90, 0x52, 0xc1, 0x7d, 0x34, 0xe8, - 0xfc, 0xad, 0x8c, 0xac, 0xc5, 0x38, 0xb1, 0xbb, 0xe6, 0x2a, 0xd4, 0xb7, 0xd0, 0x0c, 0xc2, 0xdc, 0x71, 0xec, 0x84, - 0xcf, 0x26, 0xec, 0x18, 0x19, 0x5d, 0x39, 0xb8, 0x83, 0xf0, 0x94, 0x9a, 0x94, 0xfc, 0x84, 0x4e, 0x29, 0xea, 0x12, - 0xfe, 0xd8, 0x28, 0xbc, 0xbf, 0x28, 0x49, 0xe3, 0x79, 0xd0, 0x89, 0x96, 0xbe, 0x53, 0xed, 0xb9, 0x1f, 0xee, 0x5e, - 0xd7, 0xbb, 0xdd, 0xb9, 0x2e, 0x30, 0x87, 0x3b, 0x57, 0x06, 0xee, 0x12, 0x2b, 0x5f, 0xa4, 0xee, 0x1f, 0x25, 0xe5, - 0x81, 0x1c, 0x30, 0x51, 0xc5, 0x56, 0x74, 0xa3, 0xff, 0x71, 0xe9, 0x0e, 0x4e, 0x4e, 0x6f, 0xe7, 0x81, 0x72, 0xc3, - 0xe2, 0x04, 0x12, 0x4a, 0xa8, 0x8e, 0x65, 0xab, 0x0a, 0x1a, 0xf4, 0xfb, 0xe1, 0xd4, 0x55, 0x7f, 0xbe, 0x78, 0x63, - 0x76, 0xd4, 0x53, 0x30, 0xc7, 0xb8, 0x99, 0x22, 0x8b, 0x7b, 0xee, 0xdd, 0xb1, 0xf8, 0xba, 0xc5, 0x3d, 0x7e, 0x88, - 0xb9, 0xc5, 0x32, 0xa5, 0xa5, 0xee, 0x90, 0x12, 0x5e, 0xb9, 0xf1, 0xd9, 0xea, 0x65, 0x74, 0xeb, 0xaa, 0x80, 0x58, - 0x9d, 0x56, 0x47, 0x71, 0x5a, 0x07, 0xd6, 0x51, 0x47, 0xed, 0x7f, 0xa5, 0x28, 0x27, 0x63, 0x36, 0x49, 0xfa, 0x28, - 0x8e, 0x39, 0x41, 0x7e, 0x90, 0x7e, 0x2b, 0x8a, 0x35, 0x0a, 0x12, 0xd3, 0x51, 0xd6, 0xfc, 0x51, 0x51, 0x00, 0x19, - 0x75, 0x95, 0x47, 0x93, 0xd6, 0xe4, 0x60, 0xf2, 0xa2, 0xc7, 0x8b, 0xb3, 0xaf, 0x4a, 0xd5, 0x0d, 0xfa, 0xb7, 0x25, - 0x35, 0x4b, 0xd2, 0x38, 0xfa, 0xc8, 0x38, 0x2f, 0xa9, 0xe4, 0x82, 0xa2, 0x6a, 0xd3, 0x56, 0xfd, 0x4b, 0x4e, 0x67, - 0x38, 0x9a, 0xb4, 0x8a, 0xea, 0x08, 0xe3, 0x7e, 0x0e, 0xe4, 0xc9, 0xbe, 0x00, 0xfd, 0x44, 0x9e, 0x26, 0xc7, 0x6c, - 0x9a, 0x28, 0x47, 0xe5, 0x63, 0x9c, 0x8a, 0xf1, 0x9d, 0x40, 0xc6, 0xb5, 0xc2, 0x7b, 0x31, 0xc1, 0x66, 0xae, 0xfa, - 0x83, 0xd3, 0xea, 0x18, 0x8e, 0x73, 0x64, 0x1d, 0x75, 0x46, 0xb6, 0x71, 0x60, 0x1d, 0x98, 0x6d, 0xeb, 0xc8, 0xe8, - 0x98, 0x1d, 0xa3, 0xf3, 0x5d, 0x67, 0x64, 0x1e, 0x58, 0x07, 0x86, 0x6d, 0x76, 0xa0, 0xd0, 0xec, 0x98, 0x9d, 0x1b, - 0xf3, 0xa0, 0x33, 0xb2, 0xb1, 0xb4, 0x65, 0x1d, 0x1e, 0x9a, 0x8e, 0x6d, 0x1d, 0x1e, 0x1a, 0x87, 0xd6, 0xd1, 0x91, - 0xe9, 0xb4, 0xad, 0xa3, 0xa3, 0xf3, 0xc3, 0x8e, 0xd5, 0x86, 0x77, 0xed, 0xf6, 0xa8, 0x6d, 0x39, 0x8e, 0x09, 0x7f, - 0x19, 0x1d, 0xab, 0x45, 0x3f, 0x1c, 0xc7, 0x6a, 0x3b, 0x86, 0x1d, 0x1c, 0xb6, 0xac, 0xa3, 0x17, 0x06, 0xfe, 0x8d, - 0xd5, 0x0c, 0xfc, 0x0b, 0xba, 0x31, 0x5e, 0x58, 0xad, 0x23, 0xfa, 0x85, 0x1d, 0xde, 0x1c, 0x74, 0xfe, 0xa6, 0xee, - 0x6f, 0x1c, 0x83, 0x43, 0x63, 0xe8, 0x1c, 0x5a, 0xed, 0xb6, 0x71, 0xe0, 0x58, 0x9d, 0xf6, 0xcc, 0x3c, 0x68, 0x59, - 0x47, 0xc7, 0x23, 0xd3, 0xb1, 0x8e, 0x8f, 0x0d, 0xdb, 0x6c, 0x5b, 0x2d, 0xc3, 0xb1, 0x0e, 0xda, 0xf8, 0xa3, 0x6d, - 0xb5, 0x6e, 0x8e, 0x5f, 0x58, 0x47, 0x87, 0xb3, 0x23, 0xeb, 0xe0, 0xc3, 0x41, 0xc7, 0x6a, 0xb5, 0x67, 0xed, 0x23, - 0xab, 0x75, 0x7c, 0x73, 0x64, 0x1d, 0xcc, 0xcc, 0xd6, 0xd1, 0xd6, 0x96, 0x4e, 0xcb, 0x82, 0x39, 0xc2, 0xd7, 0xf0, - 0xc2, 0xe0, 0x2f, 0xe0, 0xcf, 0x0c, 0xdb, 0xfe, 0x8e, 0xdd, 0x24, 0xf5, 0xa6, 0x2f, 0xac, 0xce, 0xf1, 0x88, 0xaa, - 0x43, 0x81, 0x29, 0x6a, 0x40, 0x93, 0x1b, 0x93, 0x3e, 0x8b, 0xdd, 0x99, 0xa2, 0x23, 0xf1, 0x87, 0x7f, 0xec, 0xc6, - 0x84, 0x0f, 0xd3, 0x77, 0xff, 0xa3, 0xfd, 0xe4, 0x4b, 0x7e, 0xb2, 0x3f, 0xa5, 0xad, 0x3f, 0xed, 0x7f, 0x75, 0x02, - 0x87, 0xbb, 0x3f, 0x30, 0x7e, 0xd9, 0xa4, 0x94, 0xfc, 0xc7, 0xfd, 0x4a, 0xc9, 0x97, 0xcb, 0x5d, 0x94, 0x92, 0xff, - 0xf8, 0xe2, 0x4a, 0xc9, 0x5f, 0xaa, 0xbe, 0x35, 0x6f, 0xaa, 0x59, 0xa8, 0xff, 0xb8, 0xae, 0x8a, 0x1c, 0x12, 0x4f, - 0xbb, 0xfc, 0x71, 0x79, 0x05, 0xf1, 0xe3, 0xdf, 0x44, 0xee, 0xcb, 0x65, 0xc9, 0xe0, 0x33, 0x02, 0x1c, 0xfb, 0x26, - 0x22, 0x1c, 0xfb, 0x61, 0xe9, 0x82, 0x95, 0x19, 0x67, 0x73, 0xfc, 0xb1, 0x39, 0xf3, 0x82, 0x49, 0xce, 0x22, 0x41, - 0x49, 0x0f, 0x8b, 0xc1, 0x6f, 0x1e, 0xc8, 0x33, 0xdc, 0x64, 0x96, 0xf3, 0x30, 0x01, 0x8b, 0x60, 0xb0, 0xe4, 0x98, - 0xc4, 0x59, 0xa5, 0xb1, 0x25, 0x22, 0xee, 0x5f, 0x73, 0x8f, 0xe2, 0x8d, 0xef, 0xd1, 0x00, 0xb8, 0xb9, 0x77, 0xa7, - 0xde, 0xaf, 0x02, 0x96, 0x75, 0xc2, 0x40, 0x1a, 0xb8, 0xfd, 0xa6, 0xf7, 0x65, 0x33, 0xdc, 0x8a, 0xe1, 0xf5, 0x66, - 0x48, 0x01, 0x92, 0x6a, 0x7b, 0xa7, 0x6c, 0xc6, 0x7b, 0xdf, 0x30, 0x1b, 0x3e, 0x5f, 0x6a, 0xbe, 0xc5, 0x86, 0x38, - 0xef, 0xb8, 0x3a, 0x55, 0xeb, 0x12, 0x9f, 0xd6, 0x3c, 0x21, 0xc5, 0x05, 0xb5, 0x30, 0x34, 0x2e, 0x38, 0x55, 0x5b, - 0x41, 0x7e, 0xc7, 0x96, 0xde, 0x95, 0xfa, 0x94, 0x8d, 0x93, 0x9f, 0xad, 0xf1, 0x5e, 0xe1, 0xff, 0x02, 0x9c, 0x28, - 0xe7, 0x78, 0x86, 0x91, 0x3c, 0xcf, 0x6b, 0xa9, 0x5f, 0x92, 0x46, 0x64, 0x33, 0x67, 0x5d, 0xe7, 0x45, 0x37, 0xba, - 0x25, 0x38, 0x6c, 0x2e, 0xb8, 0x20, 0xfc, 0x3c, 0x39, 0x01, 0x64, 0xe4, 0xa8, 0x81, 0x7e, 0x0e, 0xdb, 0x3a, 0x13, - 0xf5, 0x1e, 0xc1, 0x26, 0xe6, 0x9e, 0x80, 0x8a, 0x1c, 0xd2, 0x74, 0x3d, 0x09, 0x22, 0x2f, 0xed, 0x22, 0x9b, 0x26, - 0xb1, 0xbc, 0x2d, 0xf4, 0x58, 0xe8, 0x6d, 0x31, 0xa6, 0x93, 0x3b, 0xe6, 0x9d, 0xa0, 0xe7, 0xc3, 0x36, 0xfb, 0xbb, - 0xdc, 0xe1, 0x6c, 0x5d, 0x32, 0x47, 0x71, 0x0e, 0x8f, 0x0d, 0xe7, 0xc8, 0xb0, 0x8e, 0x0f, 0xf5, 0x4c, 0x1c, 0x38, - 0xb9, 0xcb, 0xd2, 0x84, 0x80, 0x03, 0x44, 0x0e, 0xa6, 0x1f, 0xfa, 0xa9, 0xef, 0x05, 0x19, 0xf0, 0xc3, 0xe5, 0x4b, - 0xca, 0x3f, 0x96, 0x49, 0x0a, 0x63, 0x14, 0x4c, 0x2f, 0x3a, 0x7f, 0x98, 0x43, 0x96, 0xae, 0x18, 0x0b, 0x37, 0x18, - 0xc6, 0x54, 0x7d, 0x49, 0x7e, 0x3b, 0xcb, 0xfa, 0x8c, 0xac, 0xd6, 0x86, 0x69, 0xc8, 0xf7, 0x87, 0x70, 0x7c, 0xc8, - 0x06, 0xc6, 0x77, 0x9b, 0x10, 0xee, 0xcf, 0xf7, 0x23, 0xdc, 0x94, 0xed, 0x82, 0x70, 0x7f, 0xfe, 0xe2, 0x08, 0xf7, - 0x3b, 0x19, 0xe1, 0x96, 0xfc, 0x07, 0x0b, 0x0d, 0xd3, 0x7b, 0x7c, 0xd6, 0xc0, 0x45, 0xf6, 0xb9, 0xba, 0x4f, 0x0c, - 0xbc, 0xaa, 0x17, 0xd9, 0x6b, 0xff, 0xbc, 0x94, 0x2d, 0xa8, 0x51, 0x00, 0x8a, 0x79, 0x1d, 0x7d, 0x74, 0x5d, 0xf6, - 0xc1, 0xd5, 0x4d, 0x84, 0x61, 0x80, 0x3e, 0xbf, 0x0f, 0xd3, 0xc0, 0x7a, 0xc7, 0xef, 0x91, 0xa0, 0xd0, 0x7d, 0x13, - 0xc5, 0x73, 0x0f, 0x53, 0x8c, 0xa8, 0x3a, 0xb8, 0xd3, 0xc1, 0x83, 0x0d, 0x81, 0x40, 0x46, 0x51, 0x38, 0xce, 0xb5, - 0x92, 0xcc, 0xbd, 0x24, 0x8e, 0x5b, 0xbd, 0x63, 0x5e, 0xac, 0x1a, 0xf4, 0x1a, 0x16, 0xf7, 0x59, 0xdb, 0x7e, 0xd6, - 0x3a, 0x78, 0x76, 0x64, 0xc3, 0xff, 0x0e, 0x6b, 0x67, 0x06, 0xaf, 0x38, 0x8f, 0xc2, 0x74, 0x56, 0xd4, 0xdc, 0x54, - 0x6d, 0xc5, 0xd8, 0xc7, 0xa2, 0xd6, 0x71, 0x73, 0xa5, 0xb1, 0x77, 0x57, 0xd4, 0x69, 0xac, 0x31, 0x8b, 0x96, 0x12, - 0x58, 0x0d, 0xd0, 0xf8, 0xe1, 0x12, 0xe4, 0xec, 0x52, 0x0d, 0xf9, 0x35, 0x1f, 0x6e, 0x31, 0x2e, 0xd6, 0xce, 0xae, - 0x44, 0x0e, 0x05, 0xb5, 0x27, 0xd2, 0xea, 0xdd, 0x3b, 0x83, 0x5c, 0x45, 0x69, 0x63, 0xce, 0x29, 0xcc, 0x6c, 0x08, - 0x19, 0xa7, 0x98, 0x58, 0x20, 0x8f, 0x16, 0x28, 0x8d, 0x97, 0xe1, 0x48, 0xc3, 0x9f, 0xde, 0x30, 0xd1, 0xfc, 0xfd, - 0xd8, 0xe2, 0x1f, 0xd6, 0x71, 0xd5, 0xbc, 0xbe, 0x5d, 0x24, 0x9d, 0x4f, 0xc4, 0xaa, 0x78, 0xcf, 0x52, 0x23, 0x46, - 0x3d, 0x36, 0x2d, 0xad, 0xe9, 0x7a, 0xcf, 0xf2, 0x86, 0xcf, 0x52, 0x23, 0x7c, 0x0e, 0xba, 0x4f, 0xd7, 0x7e, 0xf2, - 0x84, 0x6a, 0xed, 0xb9, 0x62, 0x58, 0xa7, 0xa3, 0x22, 0x33, 0x85, 0xe2, 0x4d, 0x23, 0x4a, 0x4e, 0xd1, 0x1d, 0x19, - 0xd1, 0xf3, 0xe7, 0x7d, 0xd7, 0xd1, 0x87, 0x31, 0xf3, 0x3e, 0x66, 0x22, 0xdc, 0x77, 0x88, 0xf9, 0x69, 0xcf, 0x77, - 0x33, 0x34, 0xd2, 0x1b, 0x5d, 0x69, 0x17, 0x70, 0x67, 0xb2, 0x85, 0x3b, 0x02, 0xc7, 0x5e, 0x90, 0xbb, 0x9e, 0x0c, - 0x0a, 0x3c, 0x61, 0xf0, 0x23, 0xea, 0xe4, 0xb7, 0xae, 0xb6, 0x65, 0x5b, 0xb6, 0x9a, 0x37, 0x9c, 0xf8, 0x53, 0x77, - 0x1d, 0xa5, 0x5e, 0x77, 0xcf, 0x31, 0x82, 0x68, 0x0a, 0x7e, 0x74, 0xa9, 0x9f, 0x06, 0xac, 0xab, 0xaa, 0xe0, 0x50, - 0x37, 0xa7, 0x7b, 0x79, 0xc6, 0xbd, 0x1b, 0xbc, 0x18, 0xd2, 0x96, 0xc7, 0x77, 0xc2, 0x15, 0x17, 0x83, 0xa5, 0xff, - 0x00, 0xc4, 0x50, 0x53, 0x35, 0x90, 0x0d, 0xb0, 0x38, 0x31, 0x65, 0x6f, 0xa1, 0xae, 0x02, 0x6d, 0x74, 0x95, 0x0f, - 0x62, 0x12, 0x7b, 0x73, 0xc8, 0xab, 0xbb, 0xce, 0x0c, 0x8e, 0x69, 0x55, 0x8e, 0x6a, 0x15, 0xe7, 0xc5, 0x91, 0xa1, - 0xb4, 0x1c, 0x43, 0xb1, 0x01, 0xdd, 0xaa, 0x99, 0xb1, 0xce, 0xae, 0x7a, 0xf7, 0x19, 0x3c, 0x10, 0x7e, 0x79, 0x44, - 0xe3, 0x20, 0x53, 0x07, 0xae, 0x4a, 0x4a, 0x29, 0x49, 0x8e, 0x26, 0x65, 0xd0, 0xf4, 0x49, 0xe9, 0x79, 0xc1, 0x6e, - 0x53, 0x1d, 0x34, 0x47, 0xa2, 0x8a, 0xaf, 0xaf, 0xd1, 0x61, 0xd8, 0x0f, 0x15, 0xff, 0xd3, 0x27, 0xcd, 0x07, 0x67, - 0x26, 0x57, 0x9a, 0x1f, 0x78, 0xd6, 0x4b, 0x13, 0xe6, 0x17, 0x6a, 0x7a, 0x9c, 0x2c, 0xf0, 0x34, 0x84, 0x7f, 0x8b, - 0x62, 0xf1, 0x83, 0x9b, 0x49, 0x58, 0x81, 0x17, 0x4e, 0x01, 0xa5, 0x79, 0xe1, 0xb4, 0x66, 0x8e, 0x45, 0x3e, 0xcf, - 0x95, 0xd2, 0xa2, 0xab, 0xc2, 0x54, 0x2a, 0x79, 0x79, 0x77, 0xe1, 0x4d, 0x7f, 0xf4, 0xe6, 0x4c, 0x53, 0x81, 0xca, - 0xa1, 0x8b, 0x6e, 0xa1, 0xc9, 0x7d, 0xee, 0x3e, 0x3d, 0x99, 0xb3, 0xd4, 0x23, 0x35, 0x10, 0x5c, 0x7e, 0x81, 0x1d, - 0x50, 0x38, 0xa1, 0xe1, 0x01, 0x2f, 0x5c, 0xca, 0xa5, 0x45, 0x74, 0xc2, 0x50, 0x38, 0x9d, 0x32, 0xd1, 0xe2, 0xd3, - 0x75, 0x0c, 0x72, 0x38, 0x18, 0x79, 0x98, 0x4f, 0xc7, 0x0d, 0x23, 0xb5, 0xff, 0x34, 0xf7, 0xcd, 0xdc, 0xb4, 0x08, - 0x81, 0x1f, 0x7e, 0xbc, 0x8c, 0x59, 0xf0, 0x4f, 0xf7, 0x29, 0x10, 0xee, 0xa7, 0x57, 0xaa, 0xde, 0x4b, 0xad, 0x59, - 0xcc, 0x26, 0xee, 0x53, 0xb8, 0x90, 0x76, 0xd1, 0x3c, 0x16, 0xb8, 0xf6, 0xe7, 0xb7, 0xf3, 0xc0, 0xc0, 0xeb, 0x3d, - 0xc1, 0xa2, 0xb6, 0x5b, 0x45, 0x5c, 0xf3, 0xf6, 0x4e, 0x97, 0xfa, 0x3e, 0xbf, 0xad, 0xc3, 0x0d, 0x70, 0x5d, 0xba, - 0x63, 0x3b, 0x3d, 0xbc, 0x3f, 0x0f, 0x03, 0x6f, 0xf4, 0xb1, 0x47, 0x6f, 0x4a, 0x0f, 0x26, 0x50, 0xeb, 0x91, 0xb7, - 0xe8, 0x22, 0x79, 0x95, 0x0b, 0xc1, 0x7b, 0x9a, 0x4a, 0x73, 0xce, 0xae, 0x71, 0x2f, 0xe3, 0x56, 0x5e, 0xe3, 0x97, - 0xf1, 0x53, 0xab, 0x99, 0x9f, 0x32, 0xf1, 0x29, 0x7c, 0xc8, 0x32, 0x71, 0x51, 0xa7, 0x2b, 0x2a, 0x5e, 0xac, 0xad, - 0xb6, 0xe2, 0x74, 0xbe, 0x3b, 0xbc, 0x71, 0xec, 0x59, 0xcb, 0xb1, 0x3a, 0x1f, 0x9c, 0xce, 0xac, 0x6d, 0x1d, 0x07, - 0x66, 0xdb, 0x3a, 0x86, 0x3f, 0x1f, 0x8e, 0xad, 0xce, 0xcc, 0x6c, 0x59, 0x07, 0x1f, 0x9c, 0x56, 0x60, 0x76, 0xac, - 0x63, 0xf8, 0x73, 0x4e, 0xad, 0xe0, 0x02, 0x44, 0xf7, 0x9d, 0xa7, 0x25, 0x2c, 0x20, 0xfd, 0xce, 0x75, 0xb2, 0x46, - 0x89, 0xbc, 0x35, 0xe8, 0x75, 0x17, 0x18, 0x45, 0x42, 0xe4, 0xaf, 0x09, 0x7b, 0x5a, 0xe8, 0x32, 0x4a, 0x2a, 0x2b, - 0xcc, 0xdb, 0x84, 0x1f, 0xba, 0xc8, 0xe6, 0xd9, 0x78, 0x8c, 0x78, 0x9b, 0xe6, 0x0c, 0x96, 0xba, 0xc8, 0x08, 0x8c, - 0xcf, 0x3f, 0x2f, 0x30, 0xfe, 0xba, 0x48, 0xc3, 0x2c, 0x61, 0x25, 0xf0, 0x3d, 0xb7, 0xc2, 0x68, 0x85, 0xb6, 0x15, - 0xf7, 0x01, 0x8e, 0xde, 0xfc, 0x4c, 0x58, 0x76, 0x7d, 0xd9, 0xbe, 0xa5, 0xcc, 0xd7, 0x9f, 0xd5, 0x0f, 0x0f, 0x0b, - 0x21, 0x67, 0x9f, 0x1c, 0xfb, 0x71, 0x0e, 0x9e, 0x84, 0xa2, 0x9d, 0xe6, 0xd4, 0x9f, 0xba, 0x41, 0xc1, 0x91, 0x58, - 0x7c, 0xe3, 0x05, 0x92, 0x21, 0x9b, 0xd4, 0x72, 0x2f, 0xc7, 0xfc, 0x4f, 0x9e, 0x14, 0xc0, 0x99, 0x15, 0xb8, 0x4f, - 0x9c, 0x43, 0x20, 0xbb, 0x87, 0xac, 0xbd, 0xd5, 0xa6, 0x92, 0x6e, 0x3a, 0xdb, 0x7c, 0xab, 0x8b, 0x4c, 0x47, 0xc2, - 0x6e, 0x4a, 0x58, 0x6c, 0x6c, 0x34, 0xec, 0xac, 0xd9, 0x6b, 0x40, 0xaa, 0xb8, 0xca, 0x55, 0x47, 0xd5, 0x7b, 0xa1, - 0x30, 0x3f, 0x08, 0xb7, 0x24, 0x79, 0xe3, 0x77, 0x31, 0x15, 0xa6, 0x66, 0xcb, 0x38, 0xee, 0x71, 0x10, 0xff, 0x4f, - 0x0f, 0x02, 0x9d, 0x35, 0xc1, 0x5e, 0xa2, 0x72, 0x5a, 0x4b, 0xce, 0x7b, 0x39, 0x5d, 0x25, 0x82, 0xca, 0x92, 0x53, - 0x15, 0x8a, 0xd4, 0xae, 0x8a, 0x8e, 0x61, 0x6a, 0x6e, 0x2c, 0x9a, 0x53, 0x8b, 0xa2, 0xc0, 0xf0, 0x31, 0xa1, 0xa6, - 0x70, 0x1c, 0xd5, 0x9f, 0x3c, 0xd9, 0x48, 0x84, 0xc8, 0x38, 0x27, 0x61, 0xa9, 0x60, 0xd0, 0x35, 0x55, 0xc6, 0x6f, - 0xaa, 0x8c, 0x62, 0xf2, 0x7e, 0x11, 0x6b, 0x08, 0x1b, 0x57, 0xda, 0x7b, 0xf8, 0x73, 0xc8, 0xbc, 0xd4, 0xe2, 0xca, - 0x52, 0x4d, 0x22, 0xee, 0x86, 0xc3, 0xda, 0x60, 0xdd, 0xca, 0xd3, 0x34, 0xf0, 0x34, 0x28, 0x8f, 0xd7, 0x7f, 0x5e, - 0xf2, 0xa8, 0x0e, 0xd0, 0xc7, 0x27, 0xbb, 0x88, 0xc3, 0xf9, 0x36, 0xf5, 0x28, 0x0e, 0x9a, 0x4c, 0x72, 0xa3, 0xd4, - 0x23, 0x7b, 0x0e, 0x1f, 0x43, 0xd7, 0x34, 0x47, 0xe4, 0x92, 0x22, 0x3f, 0xf4, 0xdf, 0x5e, 0x7c, 0xa3, 0xf0, 0xfd, - 0x4f, 0xd6, 0x02, 0x78, 0x91, 0xa1, 0x78, 0x33, 0x2e, 0xc5, 0x9b, 0x51, 0x78, 0x26, 0x63, 0xc8, 0xb9, 0x9a, 0xed, - 0xd3, 0x0c, 0xa2, 0x00, 0x9a, 0x6c, 0x28, 0xe6, 0xcb, 0x20, 0xf5, 0x17, 0x5e, 0x9c, 0xee, 0x63, 0xb0, 0x19, 0x0c, - 0x5e, 0xb3, 0x29, 0x1e, 0x04, 0x99, 0x61, 0x88, 0xec, 0x20, 0x69, 0x28, 0xec, 0x30, 0x26, 0x7e, 0x90, 0x9b, 0x61, - 0x88, 0x0f, 0x78, 0xa3, 0x11, 0x5b, 0xa4, 0x6e, 0x29, 0xa8, 0x4d, 0x34, 0x4a, 0x59, 0x6a, 0x26, 0x69, 0xcc, 0xbc, - 0xb9, 0x9a, 0x07, 0xb9, 0xaa, 0xf7, 0x97, 0x2c, 0x87, 0x10, 0xa5, 0x47, 0x84, 0xdb, 0xa2, 0x01, 0x82, 0x41, 0x04, - 0x80, 0x08, 0x41, 0x66, 0x68, 0x0a, 0xcf, 0xa3, 0x69, 0x65, 0x47, 0x15, 0x9c, 0xcb, 0x29, 0x26, 0x09, 0xa3, 0x9b, - 0x0c, 0x48, 0x8b, 0x47, 0x51, 0x70, 0xcd, 0x63, 0x58, 0xe4, 0xd9, 0x66, 0xd4, 0xfe, 0x09, 0xbf, 0xde, 0x2a, 0x18, - 0xbe, 0x45, 0x3d, 0xb4, 0x21, 0x0d, 0xda, 0xa6, 0xe8, 0x16, 0xfb, 0xbc, 0x32, 0x90, 0x26, 0xea, 0x19, 0x33, 0x59, - 0x12, 0x2c, 0x17, 0xc0, 0x08, 0x95, 0x0c, 0x66, 0x66, 0x4e, 0x3f, 0x77, 0xa7, 0x44, 0xa8, 0x90, 0x57, 0xfa, 0xf4, - 0xe9, 0xfd, 0xe0, 0xdf, 0xff, 0x82, 0x74, 0x9b, 0x33, 0x47, 0xc4, 0x94, 0xb8, 0x94, 0x6b, 0x71, 0xee, 0xd3, 0x18, - 0xa0, 0xb1, 0x14, 0x1b, 0x8b, 0x68, 0x7f, 0x62, 0x6b, 0x65, 0x83, 0x2b, 0x11, 0xa7, 0x0e, 0x12, 0xf5, 0xea, 0x22, - 0xf2, 0xc5, 0x00, 0x96, 0x77, 0x20, 0x62, 0xa2, 0x28, 0x7f, 0xbf, 0x7d, 0x79, 0xac, 0x14, 0xe1, 0x13, 0x9b, 0x2c, - 0x7a, 0x68, 0x0f, 0xf5, 0x4f, 0x3c, 0x05, 0x99, 0x16, 0x64, 0x3f, 0x92, 0xee, 0x3e, 0x0c, 0x73, 0x16, 0xcd, 0x99, - 0xe5, 0x47, 0xfb, 0x2b, 0x36, 0x34, 0xbd, 0x85, 0x4f, 0x76, 0x39, 0x28, 0x77, 0x53, 0x88, 0xf3, 0xcb, 0xcd, 0x5d, - 0x88, 0xbf, 0xce, 0x8a, 0xa9, 0x8c, 0x2a, 0x81, 0xd0, 0x5a, 0x85, 0x1e, 0xf0, 0x80, 0x07, 0x19, 0x13, 0x35, 0xfb, - 0x27, 0xfb, 0x5e, 0xbf, 0x9c, 0x79, 0xc6, 0x12, 0x19, 0x54, 0xcb, 0x44, 0xe0, 0x94, 0x12, 0xc8, 0x88, 0x5c, 0x31, - 0xc5, 0x83, 0x19, 0x4d, 0x26, 0x72, 0xb6, 0x18, 0xab, 0x0c, 0x5e, 0x3e, 0x69, 0xc5, 0x96, 0x8e, 0x16, 0xf4, 0xa5, - 0xfa, 0x27, 0xf2, 0x9f, 0x6a, 0x17, 0xd3, 0x44, 0xc1, 0x98, 0xe1, 0xb8, 0xd7, 0xb2, 0xce, 0xe4, 0x33, 0xf6, 0x88, - 0x2a, 0x71, 0x3c, 0x52, 0xcd, 0x71, 0xb8, 0x81, 0x73, 0xd9, 0x73, 0x5d, 0x42, 0x73, 0x55, 0x6c, 0x07, 0x93, 0xd8, - 0x90, 0x4d, 0x16, 0x06, 0x9b, 0x42, 0x43, 0x93, 0xdc, 0x65, 0xb1, 0x51, 0x75, 0x38, 0x75, 0x18, 0xf7, 0x3d, 0xb1, - 0xfd, 0x4a, 0x1b, 0x14, 0x36, 0x1e, 0x5f, 0x77, 0xc0, 0xef, 0xa2, 0x9f, 0x0a, 0x9a, 0x57, 0xbe, 0x26, 0x8c, 0x6e, - 0x06, 0xde, 0x5d, 0x24, 0x99, 0x31, 0xf1, 0x88, 0x26, 0xe7, 0x58, 0x7a, 0x21, 0x3c, 0x89, 0x6b, 0x07, 0x0d, 0x49, - 0x18, 0x64, 0xdd, 0xac, 0x1f, 0xb6, 0x82, 0xfe, 0x06, 0xec, 0xbe, 0xb3, 0x26, 0xd7, 0x2d, 0x0f, 0x06, 0x91, 0x67, - 0x56, 0x9c, 0xc3, 0xd2, 0x4b, 0x44, 0x0b, 0xd9, 0xc9, 0x3e, 0x8c, 0x0f, 0xa2, 0xb0, 0x94, 0x18, 0x27, 0x5f, 0x87, - 0x50, 0x2f, 0x5e, 0x42, 0xa6, 0x58, 0xdf, 0x8f, 0x05, 0xcf, 0x87, 0x17, 0x4b, 0x29, 0x97, 0xbc, 0x54, 0xa5, 0xce, - 0xcb, 0xd8, 0xcd, 0x4c, 0xe0, 0xfd, 0x29, 0x6a, 0x3f, 0x2c, 0x31, 0x3f, 0x6d, 0xd6, 0x4b, 0x99, 0x08, 0x72, 0x70, - 0x9e, 0x6e, 0x88, 0x83, 0xb0, 0xa9, 0x0a, 0xf1, 0xb3, 0x5b, 0x2a, 0x14, 0xfb, 0x78, 0x5b, 0xad, 0x82, 0x73, 0x2a, - 0xaa, 0x79, 0x9a, 0xfa, 0x08, 0x77, 0x7c, 0xad, 0x36, 0x96, 0x62, 0x74, 0x86, 0xd4, 0x85, 0xaa, 0x42, 0x16, 0xef, - 0x2d, 0x16, 0x54, 0x59, 0xef, 0x9d, 0xec, 0xd3, 0xb5, 0xb4, 0x4f, 0x3b, 0xac, 0x7f, 0x02, 0xa6, 0xdc, 0xb4, 0xe8, - 0xde, 0x62, 0xc1, 0x97, 0x94, 0x7e, 0xd1, 0x9b, 0xfd, 0x59, 0x3a, 0x0f, 0xfa, 0xff, 0x0b, 0x3a, 0x5f, 0xcc, 0x86, - 0x37, 0x7a, 0x03, 0x00}; + 0x2a, 0x34, 0x7d, 0xe4, 0x37, 0x22, 0x0d, 0x08, 0x0c, 0xda, 0x65, 0x6f, 0xab, 0xd2, 0x82, 0x6c, 0x3a, 0xb6, 0xd2, + 0xe4, 0xa0, 0x83, 0x03, 0xc4, 0xf8, 0x15, 0x62, 0x21, 0x42, 0x3b, 0xbc, 0x0e, 0x3e, 0x64, 0x6a, 0xce, 0xfb, 0xe1, + 0xf6, 0xeb, 0x9f, 0xec, 0x43, 0x83, 0x7e, 0x45, 0xe9, 0x76, 0x8f, 0x5f, 0x26, 0xb0, 0x12, 0xc9, 0xca, 0xb0, 0x92, + 0x95, 0xf2, 0x6c, 0x2d, 0xe2, 0x63, 0xa7, 0xde, 0xc2, 0x04, 0x2d, 0x0f, 0xe2, 0x5e, 0x8e, 0xf1, 0xa4, 0x50, 0xdc, + 0xbd, 0x65, 0x02, 0xb8, 0x11, 0xe5, 0x28, 0x88, 0x7f, 0x7a, 0xa3, 0x65, 0x9c, 0x44, 0x71, 0x77, 0x11, 0xf9, 0x61, + 0xca, 0xe2, 0x8c, 0x04, 0x2b, 0x38, 0x3f, 0x62, 0x7a, 0xae, 0xd6, 0xd1, 0xc2, 0x1b, 0xf9, 0xe9, 0x5d, 0xd7, 0xe6, + 0x2c, 0x85, 0xdd, 0xe3, 0xdc, 0x81, 0x5d, 0x5b, 0xbf, 0xcb, 0x67, 0xf3, 0x39, 0x32, 0x7e, 0xf1, 0x26, 0x3b, 0x23, + 0x6f, 0xf3, 0x9e, 0xf4, 0x96, 0x22, 0x84, 0x03, 0xfb, 0xe1, 0xc5, 0xe6, 0x14, 0xb0, 0x3c, 0x2c, 0xb5, 0x3d, 0x66, + 0x53, 0x03, 0xb1, 0x36, 0x98, 0x19, 0x8a, 0x3f, 0xd6, 0xa1, 0xae, 0xd8, 0xf5, 0xc5, 0xc0, 0xf1, 0xe8, 0xbb, 0x40, + 0xd6, 0xf5, 0x26, 0x29, 0x8b, 0x8d, 0x5d, 0x6a, 0x0e, 0xd9, 0x24, 0x8a, 0x19, 0x65, 0x93, 0x73, 0x3a, 0x8b, 0xdb, + 0xdd, 0xbb, 0xdf, 0x3e, 0xfc, 0xfa, 0x7e, 0xc2, 0x28, 0xd5, 0x44, 0x67, 0xfa, 0x3d, 0xbd, 0x6d, 0xd2, 0x33, 0x60, + 0x0d, 0x69, 0xe6, 0x47, 0x24, 0x05, 0x81, 0x48, 0x60, 0xb5, 0x49, 0x3b, 0x16, 0x11, 0xa7, 0x79, 0x31, 0x0b, 0xbc, + 0xd4, 0xbf, 0x11, 0x3c, 0x63, 0xfb, 0x68, 0x71, 0x2b, 0xd6, 0x18, 0x09, 0xde, 0x03, 0x16, 0xa9, 0x02, 0x8a, 0x58, + 0xa4, 0x6a, 0x31, 0x2e, 0x52, 0x6f, 0x63, 0x34, 0x22, 0x8e, 0x75, 0x85, 0xd2, 0x1f, 0x2e, 0x6e, 0x65, 0x12, 0x5d, + 0x34, 0xcb, 0x29, 0x75, 0x35, 0x01, 0xc9, 0xdc, 0x1f, 0x8f, 0x03, 0x96, 0x95, 0x16, 0xba, 0xbc, 0x96, 0xd2, 0xe4, + 0xe4, 0xf3, 0xe0, 0x0d, 0x93, 0x28, 0x58, 0xa6, 0xac, 0x7e, 0xba, 0x84, 0x44, 0xb7, 0x98, 0x1c, 0xfc, 0x5d, 0x86, + 0xf5, 0x10, 0xd8, 0x6d, 0xd8, 0x26, 0x76, 0x0f, 0xf2, 0x0d, 0x9a, 0xed, 0x32, 0xe8, 0xf0, 0x2a, 0x07, 0xda, 0xa8, + 0x19, 0x88, 0x01, 0x64, 0x89, 0xb0, 0xb7, 0x62, 0x39, 0xbc, 0x2c, 0xcf, 0xb9, 0x96, 0x17, 0x65, 0xe5, 0xc1, 0xfc, + 0x3e, 0x67, 0xec, 0x45, 0xfd, 0x19, 0x7b, 0x21, 0xce, 0xd8, 0xf6, 0x9d, 0xf9, 0x68, 0xe2, 0xc0, 0x7f, 0xbd, 0x62, + 0x40, 0x5d, 0x5b, 0x69, 0x2f, 0x6e, 0x15, 0x67, 0x71, 0xab, 0x98, 0xad, 0xc5, 0xad, 0x82, 0x5d, 0xa3, 0x7b, 0x8b, + 0x61, 0xb5, 0x74, 0xc3, 0x56, 0xa0, 0x10, 0xfe, 0xd8, 0xa5, 0x57, 0xce, 0x01, 0xbc, 0x83, 0x56, 0x87, 0x9b, 0xef, + 0x5a, 0xdb, 0x8f, 0x3a, 0x9d, 0x25, 0x81, 0xb4, 0x75, 0x2b, 0xf5, 0x86, 0x43, 0x10, 0x65, 0x46, 0xa3, 0x65, 0xf2, + 0x0f, 0x0e, 0x3f, 0x9f, 0xc4, 0xad, 0x88, 0xa0, 0xd2, 0x8f, 0x68, 0x0a, 0x8a, 0xc2, 0x1b, 0x26, 0x7a, 0x58, 0xe7, + 0xeb, 0xd4, 0xa5, 0xe4, 0x88, 0x2d, 0xeb, 0xa0, 0x66, 0x93, 0xd7, 0x4f, 0xf4, 0xef, 0xb6, 0x4a, 0xcd, 0x28, 0xe6, + 0x33, 0xa6, 0x65, 0xeb, 0x74, 0x3c, 0x7c, 0x36, 0xf8, 0x6a, 0xda, 0x9d, 0x7a, 0x70, 0x2f, 0xc5, 0x97, 0xae, 0x04, + 0x51, 0xe1, 0x74, 0x8b, 0x87, 0xe2, 0xd8, 0xde, 0x6b, 0xd3, 0x1e, 0xd9, 0xe8, 0x75, 0x0b, 0x41, 0x28, 0xea, 0xee, + 0x88, 0xe5, 0x1f, 0xbd, 0x38, 0x80, 0xff, 0x88, 0xab, 0xff, 0x6b, 0x5a, 0xc7, 0xa8, 0xbf, 0x4e, 0x4b, 0x8c, 0x3a, + 0xb1, 0x4a, 0xc8, 0x88, 0xef, 0x5e, 0x7f, 0x32, 0x79, 0x58, 0x83, 0x9d, 0x6b, 0x93, 0x67, 0x58, 0xb5, 0xf6, 0xcb, + 0x28, 0x0a, 0x98, 0x17, 0x6e, 0x56, 0x17, 0xd3, 0x43, 0x6e, 0xfe, 0xa9, 0x0b, 0x8d, 0xc4, 0x3d, 0x82, 0x9c, 0x12, + 0x54, 0x6c, 0x43, 0x57, 0x89, 0xf3, 0xa6, 0xab, 0xc4, 0xbb, 0xfb, 0xaf, 0x12, 0x3f, 0xec, 0x74, 0x95, 0x78, 0xf7, + 0xc5, 0xaf, 0x12, 0xe7, 0x9b, 0x57, 0x89, 0xf3, 0x48, 0xb8, 0x03, 0x1b, 0x6f, 0x96, 0xfc, 0xe7, 0x07, 0xb2, 0xf7, + 0x7d, 0x17, 0xb9, 0x87, 0x36, 0x25, 0x3c, 0xbc, 0xf8, 0xcd, 0x17, 0x0b, 0xdc, 0x88, 0xef, 0xd0, 0x3b, 0xae, 0xb8, + 0x5a, 0x70, 0xcc, 0x8e, 0xdf, 0x91, 0x8a, 0x83, 0x28, 0x9c, 0xfe, 0x0c, 0xf6, 0xde, 0x20, 0x0e, 0x8c, 0xa5, 0x17, + 0x7e, 0xf2, 0x73, 0xb4, 0x58, 0x2e, 0x50, 0x51, 0xf5, 0xc1, 0x4f, 0xfc, 0x61, 0xc0, 0xf2, 0x08, 0x93, 0xa4, 0x75, + 0xe5, 0xb2, 0x75, 0x50, 0xbc, 0x8a, 0x9f, 0xde, 0xad, 0xf8, 0x89, 0x2e, 0xb6, 0xfc, 0x37, 0xb9, 0x09, 0xaa, 0xf5, + 0x17, 0x11, 0x61, 0x21, 0x26, 0x01, 0xfd, 0xf0, 0xcb, 0xc8, 0xb9, 0x88, 0xe5, 0x55, 0x1a, 0xa5, 0x70, 0xdf, 0x68, + 0xec, 0x87, 0x55, 0xfb, 0x79, 0xb3, 0xd4, 0x8d, 0x3c, 0x01, 0xc7, 0xa6, 0x38, 0x7f, 0x1e, 0x2d, 0x13, 0x36, 0x8e, + 0x56, 0xa1, 0x6a, 0x84, 0x5c, 0xaf, 0x1a, 0xa1, 0x4c, 0x3d, 0x6f, 0x53, 0x56, 0x38, 0xaa, 0xd6, 0x02, 0xe6, 0xd0, + 0x24, 0x0d, 0xb6, 0x89, 0x43, 0x54, 0x45, 0xc8, 0xa6, 0xde, 0x9e, 0xa6, 0x45, 0xee, 0xc3, 0x5a, 0x0a, 0xcf, 0x93, + 0xc8, 0xe2, 0x52, 0xe1, 0x44, 0x0b, 0x85, 0x70, 0x51, 0x44, 0xc1, 0xae, 0x59, 0x38, 0xfe, 0x86, 0x22, 0x44, 0x16, + 0x6f, 0x41, 0x57, 0x95, 0x2d, 0xf9, 0x7a, 0xf0, 0x98, 0xd0, 0xf4, 0xf8, 0x4a, 0x9a, 0xc6, 0xb7, 0x37, 0x2c, 0x0e, + 0xbc, 0x3b, 0x4d, 0xcf, 0xa2, 0xf0, 0x47, 0x98, 0x80, 0xd7, 0xd1, 0x2a, 0x94, 0x2b, 0x60, 0xaa, 0xf6, 0x9a, 0xbd, + 0x54, 0x1b, 0xbd, 0x1c, 0x62, 0x76, 0x48, 0x10, 0xf8, 0xd6, 0xc2, 0x9b, 0xb2, 0xff, 0x32, 0xe8, 0xdf, 0xff, 0xd6, + 0x33, 0xe3, 0x5d, 0x94, 0x7f, 0xe8, 0x97, 0xc5, 0x0e, 0x9f, 0x79, 0xf2, 0x64, 0xaf, 0x79, 0xd8, 0xda, 0x28, 0x60, + 0x5e, 0x2c, 0xa0, 0xa8, 0x69, 0xad, 0x37, 0x9e, 0x02, 0x80, 0xe2, 0x22, 0x5a, 0x8e, 0x66, 0xe8, 0xb7, 0xfb, 0xe5, + 0xc6, 0x9b, 0x42, 0x9f, 0x2c, 0xb9, 0xb4, 0xaf, 0xf2, 0xa1, 0x57, 0x8a, 0x8a, 0x59, 0xc0, 0xef, 0x9f, 0x41, 0xfa, + 0xad, 0x7f, 0xe3, 0x34, 0x6c, 0xee, 0x9a, 0x3c, 0xe4, 0xd7, 0x83, 0x36, 0x6f, 0xcf, 0x87, 0xa8, 0x3c, 0x14, 0xd8, + 0x5a, 0x28, 0xe9, 0xea, 0x91, 0x4c, 0x56, 0x9d, 0x34, 0x39, 0x89, 0x4c, 0x53, 0x7e, 0x1c, 0xf1, 0x15, 0x66, 0x95, + 0xac, 0x46, 0x0c, 0xc6, 0xb1, 0x55, 0x05, 0xc9, 0x70, 0x6f, 0x0a, 0x86, 0xe8, 0xab, 0xfa, 0x6e, 0xee, 0x87, 0x06, + 0xe6, 0x80, 0xdd, 0x7c, 0xe3, 0xdd, 0x42, 0x16, 0x44, 0x40, 0x6e, 0xd5, 0x57, 0x50, 0x68, 0xc8, 0xd1, 0x82, 0xbc, + 0xf1, 0x58, 0x53, 0x6b, 0x67, 0x42, 0x68, 0x03, 0x07, 0x5f, 0x29, 0x8a, 0xa2, 0xe4, 0xd7, 0x08, 0x25, 0xbf, 0x47, + 0x60, 0x39, 0x5e, 0x07, 0x40, 0x5b, 0x92, 0x2d, 0x6e, 0xa9, 0x04, 0x6e, 0x06, 0x68, 0x3f, 0x2d, 0x0a, 0x78, 0xa2, + 0x1f, 0x30, 0x6e, 0xa1, 0x02, 0x71, 0xa1, 0x07, 0xd5, 0xb7, 0x17, 0x43, 0x3e, 0xc0, 0xae, 0x82, 0x17, 0x76, 0x7c, + 0xcb, 0x25, 0xc1, 0x8a, 0x4d, 0x8f, 0x83, 0x1e, 0xab, 0xcf, 0x08, 0x13, 0x4a, 0x58, 0x10, 0xb4, 0x0e, 0x95, 0x04, + 0x8f, 0x06, 0xab, 0xc1, 0x8d, 0x78, 0x2f, 0xba, 0x4d, 0xe7, 0x2c, 0x5c, 0xaa, 0x06, 0x58, 0x9d, 0x60, 0x86, 0x1e, + 0xa8, 0xf3, 0x9a, 0x98, 0x2d, 0xc0, 0x36, 0xf5, 0x2d, 0x67, 0x44, 0x0b, 0x85, 0xa9, 0x8a, 0x67, 0x8c, 0x78, 0x00, + 0x9c, 0x84, 0xe3, 0xb6, 0x2a, 0x85, 0xe0, 0x4b, 0x1a, 0x95, 0xb1, 0x39, 0x0f, 0x79, 0x85, 0x9c, 0x02, 0xd9, 0x88, + 0x71, 0x71, 0x91, 0x98, 0x76, 0xcd, 0xab, 0x2e, 0x5a, 0xae, 0x91, 0xf1, 0x2a, 0x82, 0xa2, 0x58, 0xdf, 0xec, 0x86, + 0xc3, 0x09, 0x69, 0x09, 0x1a, 0xfb, 0x19, 0x6d, 0xf4, 0xd3, 0x30, 0xe8, 0x8f, 0xec, 0x8e, 0x08, 0x09, 0x4d, 0xd5, + 0x47, 0x76, 0x07, 0xc6, 0xe1, 0x67, 0x20, 0x4d, 0x51, 0xb7, 0xa0, 0x6b, 0x03, 0x12, 0xfd, 0x8e, 0x20, 0x55, 0xc5, + 0x96, 0x03, 0x64, 0x67, 0x5b, 0xb0, 0x38, 0x85, 0x23, 0x35, 0x92, 0x9e, 0x38, 0xc4, 0x3c, 0x62, 0x81, 0x56, 0x3b, + 0xc7, 0x66, 0xcd, 0xd1, 0xd0, 0x9f, 0x39, 0xb6, 0xbd, 0xbf, 0x51, 0x1f, 0x04, 0xd9, 0x75, 0xb5, 0x75, 0x23, 0x75, + 0x1d, 0xdb, 0xf4, 0x9f, 0x59, 0xad, 0xde, 0x06, 0x8d, 0x96, 0x32, 0x49, 0x0d, 0x50, 0xfc, 0xd5, 0x7f, 0xbc, 0xd6, + 0x36, 0x0e, 0xa4, 0x5e, 0x8d, 0x00, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xd8, 0x24, 0xfd, 0x94, 0xc7, 0x8a, 0xb2, + 0x9a, 0x0f, 0x20, 0x17, 0xa2, 0x06, 0xc7, 0xe8, 0x4f, 0xca, 0x73, 0x45, 0xa3, 0xe3, 0xa3, 0xeb, 0x83, 0x9e, 0xc0, + 0x28, 0x22, 0x44, 0x8e, 0xdc, 0x41, 0xe5, 0x8b, 0x49, 0x15, 0xc3, 0xf1, 0xac, 0x6b, 0xac, 0xd0, 0xe8, 0x6d, 0xe5, + 0x16, 0xb0, 0xff, 0x06, 0xf2, 0x69, 0x0d, 0x21, 0xc6, 0x23, 0xd4, 0x80, 0xcc, 0xa9, 0xf7, 0x76, 0x08, 0xe1, 0x79, + 0xe5, 0xee, 0xca, 0x44, 0x72, 0xf7, 0xce, 0x90, 0xe8, 0xa0, 0x0e, 0x2d, 0xef, 0xaf, 0x9e, 0xdc, 0x3d, 0xb0, 0x4b, + 0x16, 0x8e, 0xcb, 0x1d, 0x56, 0xe8, 0xd7, 0xee, 0xdd, 0x95, 0x30, 0x0a, 0xa4, 0x14, 0x8e, 0x6a, 0x30, 0x4a, 0x16, + 0x85, 0xb8, 0xf9, 0xe9, 0xb8, 0xf9, 0x3b, 0x71, 0x31, 0xd8, 0x80, 0xf2, 0x81, 0xe4, 0xcd, 0x24, 0xa1, 0x38, 0xe4, + 0xad, 0xc4, 0x08, 0x5a, 0x9a, 0x60, 0x44, 0x1b, 0x77, 0x62, 0x2a, 0xdc, 0x15, 0x8b, 0x36, 0x3e, 0xcf, 0x44, 0xb5, + 0xab, 0xd4, 0xda, 0xbf, 0x5f, 0x6a, 0x9d, 0xde, 0x27, 0xb5, 0xa6, 0xe8, 0x30, 0xdc, 0x1e, 0x54, 0x44, 0xc9, 0x11, + 0xcc, 0xb9, 0x1c, 0x67, 0xa8, 0x24, 0xea, 0xc6, 0x60, 0x32, 0x35, 0x56, 0xa4, 0xd4, 0x1b, 0x39, 0x20, 0xa2, 0xf8, + 0x5b, 0xba, 0xa0, 0x08, 0x85, 0xba, 0x2c, 0x1b, 0x3f, 0x2f, 0x64, 0xe3, 0x74, 0xab, 0x29, 0xe2, 0x82, 0x08, 0xee, + 0x5f, 0x8a, 0xb9, 0x93, 0xdf, 0x0e, 0x8a, 0xd8, 0x3b, 0x05, 0xa4, 0x52, 0x34, 0x99, 0xe2, 0xa2, 0x21, 0xc5, 0x28, + 0x12, 0xb7, 0x8c, 0x72, 0xa8, 0xa2, 0x72, 0xd5, 0x22, 0x98, 0x4c, 0x51, 0x0e, 0x52, 0x77, 0x04, 0x39, 0x2f, 0x96, + 0xb7, 0x4d, 0x39, 0x9a, 0x88, 0xfc, 0x5a, 0xda, 0x24, 0x79, 0xd8, 0x0f, 0x9a, 0x60, 0x21, 0xa6, 0xaf, 0xe8, 0xb5, + 0x73, 0x1b, 0x08, 0x04, 0xb2, 0x26, 0x4a, 0xd1, 0xfd, 0xd2, 0x79, 0xca, 0x96, 0x5c, 0xa8, 0xae, 0x1d, 0xa4, 0xee, + 0xa4, 0x09, 0x96, 0xe5, 0x11, 0x38, 0xd7, 0x57, 0x92, 0x04, 0xa1, 0x6b, 0x2b, 0x76, 0xaf, 0x86, 0x01, 0x40, 0xfa, + 0x5f, 0x7d, 0xe6, 0xac, 0x00, 0x48, 0x22, 0x15, 0x5b, 0xd6, 0xf9, 0xe3, 0x21, 0x36, 0xc9, 0x92, 0x1d, 0xab, 0x6e, + 0x7e, 0x93, 0xe4, 0x3d, 0x6b, 0x1e, 0x13, 0xa4, 0x2c, 0xce, 0xe7, 0x35, 0xba, 0x02, 0x0e, 0xbe, 0xcb, 0xe2, 0x65, + 0x88, 0x49, 0x70, 0xcd, 0x34, 0xf6, 0x46, 0x1f, 0xd7, 0xd2, 0xf7, 0xb8, 0x48, 0x14, 0xc4, 0xc5, 0x65, 0xa5, 0x42, + 0xcf, 0xc3, 0x9c, 0x51, 0xac, 0x6b, 0xb5, 0x12, 0x49, 0x50, 0xd3, 0x7d, 0x64, 0xb7, 0xbd, 0x17, 0x93, 0x83, 0x8a, + 0xfc, 0xb4, 0x75, 0x58, 0x96, 0xae, 0xe7, 0x70, 0xcc, 0xa3, 0x5f, 0x79, 0xf4, 0xa4, 0x3f, 0xfe, 0xd3, 0x09, 0xff, + 0x66, 0x65, 0x8d, 0x3e, 0x07, 0x04, 0x68, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x8d, 0x92, 0x26, 0xb0, 0x26, 0x7e, + 0x10, 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xcf, 0x12, 0xb2, 0x15, 0x7e, 0x46, 0x3f, 0xe5, 0x9d, + 0x92, 0xce, 0x96, 0xf3, 0xe1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0x4d, 0x99, 0xb9, 0xfc, 0xd9, 0xc9, 0x64, 0x52, + 0x96, 0x1a, 0xdb, 0xca, 0x01, 0x4a, 0x7e, 0x1f, 0xd9, 0xb6, 0x5d, 0x9d, 0xdf, 0xa6, 0x83, 0x42, 0x07, 0xc3, 0x44, + 0x21, 0x7c, 0xe7, 0xfe, 0x3d, 0xf5, 0x07, 0x41, 0x4b, 0x5d, 0x35, 0x9d, 0x47, 0xda, 0x6a, 0xff, 0x11, 0xa0, 0x20, + 0x6a, 0xb8, 0xef, 0xf8, 0x6f, 0xee, 0x95, 0x2d, 0x3d, 0x55, 0x0f, 0xf0, 0xc3, 0x1a, 0xdf, 0xb3, 0xd7, 0x77, 0x68, + 0xda, 0xb4, 0xbd, 0x33, 0xab, 0x20, 0xbb, 0x25, 0x9b, 0xa5, 0x1e, 0x59, 0x2a, 0xf9, 0x29, 0x9b, 0x27, 0xdd, 0x11, + 0x43, 0x05, 0xa9, 0x25, 0x51, 0x5b, 0xb4, 0xea, 0x31, 0xa7, 0x60, 0xc7, 0xe5, 0x08, 0x3c, 0x6c, 0x2b, 0xa8, 0xac, + 0xda, 0xd0, 0xac, 0x89, 0x8f, 0x20, 0x15, 0x5b, 0x6f, 0x2a, 0x9c, 0x70, 0x9b, 0x1e, 0xda, 0x7f, 0x2a, 0xd5, 0x53, + 0x80, 0x3b, 0x5d, 0x0b, 0x6b, 0x13, 0x52, 0x9e, 0xe0, 0xdf, 0xb9, 0x72, 0xee, 0xc5, 0xe2, 0xb6, 0x6c, 0xdc, 0xd5, + 0x01, 0x75, 0x53, 0x41, 0xca, 0x08, 0xea, 0x3a, 0xd4, 0x97, 0x9b, 0x00, 0x4d, 0x64, 0xeb, 0x16, 0xb0, 0xa0, 0x11, + 0x53, 0x50, 0xd1, 0x11, 0xe6, 0xa0, 0xe2, 0x75, 0x16, 0x76, 0x5e, 0x21, 0xdf, 0xc7, 0x5f, 0x90, 0xa5, 0x1c, 0xd2, + 0x9d, 0xfc, 0xc9, 0x78, 0xde, 0x41, 0xe5, 0x5e, 0x69, 0xab, 0xa2, 0xa9, 0x0c, 0xee, 0x01, 0x71, 0x23, 0x55, 0x96, + 0x71, 0x60, 0x52, 0xe2, 0x7a, 0x4d, 0x5f, 0x6f, 0x8e, 0xbb, 0xb9, 0x7b, 0xe7, 0x10, 0xf4, 0x1a, 0x9b, 0x53, 0xb5, + 0x93, 0x6a, 0xaf, 0xaa, 0xc3, 0x16, 0x70, 0xc2, 0x0a, 0x80, 0xcf, 0xac, 0x82, 0x46, 0x43, 0x4a, 0x05, 0xf7, 0xd1, + 0xa0, 0xf3, 0xb7, 0x32, 0xb2, 0x16, 0xe3, 0xc4, 0xee, 0xea, 0xab, 0x50, 0xdf, 0x42, 0x33, 0x08, 0x73, 0xc7, 0xb1, + 0x13, 0x3e, 0x9b, 0xb0, 0x63, 0x64, 0x74, 0xe5, 0xe0, 0x0e, 0xc2, 0x53, 0x6a, 0x52, 0xf2, 0x13, 0x3a, 0xa5, 0xa8, + 0x4b, 0xf8, 0xa1, 0x56, 0x78, 0x7f, 0x51, 0x92, 0xc6, 0xf3, 0xa0, 0x13, 0x2d, 0x7d, 0xa7, 0xda, 0x73, 0x3f, 0xdc, + 0xbd, 0xae, 0x77, 0xbb, 0x73, 0x5d, 0x60, 0x0e, 0x77, 0xae, 0x0c, 0xdc, 0x25, 0x56, 0xbe, 0x48, 0xdd, 0x1f, 0x24, + 0xe5, 0x81, 0x1c, 0x30, 0x51, 0xc5, 0x56, 0x74, 0xa3, 0xff, 0x69, 0xe9, 0x0e, 0x4e, 0x4e, 0x6f, 0xe7, 0x81, 0x72, + 0xc3, 0xe2, 0x04, 0x12, 0x4a, 0xa8, 0x8e, 0x65, 0xab, 0x0a, 0x1a, 0xf4, 0xfb, 0xe1, 0xd4, 0x55, 0x7f, 0xb9, 0x78, + 0x63, 0x76, 0xd4, 0x53, 0x30, 0xc7, 0xb8, 0x99, 0x22, 0x8b, 0x7b, 0xee, 0xdd, 0xb1, 0xf8, 0xba, 0xc5, 0x3d, 0x7e, + 0x88, 0xb9, 0xc5, 0x32, 0xa5, 0xa5, 0xee, 0x90, 0x12, 0x5e, 0xb9, 0xf1, 0xd9, 0xea, 0x65, 0x74, 0xeb, 0xaa, 0x80, + 0x58, 0x9d, 0x56, 0x47, 0x71, 0x5a, 0x07, 0xd6, 0x51, 0x47, 0xed, 0x7f, 0xa5, 0x28, 0x27, 0x63, 0x36, 0x49, 0xfa, + 0x28, 0x8e, 0x39, 0x41, 0x7e, 0x90, 0x7e, 0x2b, 0x8a, 0x35, 0x0a, 0x12, 0xd3, 0x51, 0xd6, 0xfc, 0x51, 0x51, 0x00, + 0x19, 0x75, 0x95, 0x47, 0x93, 0xd6, 0xe4, 0x60, 0xf2, 0xa2, 0xc7, 0x8b, 0xb3, 0xaf, 0x4a, 0xd5, 0x0d, 0xfa, 0xb7, + 0x25, 0x35, 0x4b, 0xd2, 0x38, 0xfa, 0xc8, 0x38, 0x2f, 0xa9, 0xe4, 0x82, 0xa2, 0x6a, 0xd3, 0xd6, 0xe6, 0x97, 0x9c, + 0xce, 0x70, 0x34, 0x69, 0x15, 0xd5, 0x11, 0xc6, 0xfd, 0x1c, 0xc8, 0x93, 0x7d, 0x01, 0xfa, 0x89, 0x3c, 0x4d, 0x8e, + 0x59, 0x37, 0x51, 0x8e, 0xca, 0xc7, 0x38, 0x15, 0xe3, 0x3b, 0x81, 0x8c, 0x6b, 0x85, 0xf7, 0x62, 0x82, 0xcd, 0x5c, + 0xf5, 0x47, 0xa7, 0xd5, 0x31, 0x1c, 0xe7, 0xc8, 0x3a, 0xea, 0x8c, 0x6c, 0xe3, 0xc0, 0x3a, 0x30, 0xdb, 0xd6, 0x91, + 0xd1, 0x31, 0x3b, 0x46, 0xe7, 0xbb, 0xce, 0xc8, 0x3c, 0xb0, 0x0e, 0x0c, 0xdb, 0xec, 0x40, 0xa1, 0xd9, 0x31, 0x3b, + 0x37, 0xe6, 0x41, 0x67, 0x64, 0x63, 0x69, 0xcb, 0x3a, 0x3c, 0x34, 0x1d, 0xdb, 0x3a, 0x3c, 0x34, 0x0e, 0xad, 0xa3, + 0x23, 0xd3, 0x69, 0x5b, 0x47, 0x47, 0xe7, 0x87, 0x1d, 0xab, 0x0d, 0xef, 0xda, 0xed, 0x51, 0xdb, 0x72, 0x1c, 0x13, + 0xfe, 0x32, 0x3a, 0x56, 0x8b, 0x7e, 0x38, 0x8e, 0xd5, 0x76, 0x0c, 0x3b, 0x38, 0x6c, 0x59, 0x47, 0x2f, 0x0c, 0xfc, + 0x1b, 0xab, 0x19, 0xf8, 0x17, 0x74, 0x63, 0xbc, 0xb0, 0x5a, 0x47, 0xf4, 0x0b, 0x3b, 0xbc, 0x39, 0xe8, 0xfc, 0x55, + 0xdd, 0x6f, 0x1c, 0x83, 0x43, 0x63, 0xe8, 0x1c, 0x5a, 0xed, 0xb6, 0x71, 0xe0, 0x58, 0x9d, 0xf6, 0xcc, 0x3c, 0x68, + 0x59, 0x47, 0xc7, 0x23, 0xd3, 0xb1, 0x8e, 0x8f, 0x0d, 0xdb, 0x6c, 0x5b, 0x2d, 0xc3, 0xb1, 0x0e, 0xda, 0xf8, 0xa3, + 0x6d, 0xb5, 0x6e, 0x8e, 0x5f, 0x58, 0x47, 0x87, 0xb3, 0x23, 0xeb, 0xe0, 0xc3, 0x41, 0xc7, 0x6a, 0xb5, 0x67, 0xed, + 0x23, 0xab, 0x75, 0x7c, 0x73, 0x64, 0x1d, 0xcc, 0xcc, 0xd6, 0xd1, 0xd6, 0x96, 0x4e, 0xcb, 0x82, 0x39, 0xc2, 0xd7, + 0xf0, 0xc2, 0xe0, 0x2f, 0xe0, 0xcf, 0x0c, 0xdb, 0xfe, 0x81, 0xdd, 0x24, 0x9b, 0x4d, 0x5f, 0x58, 0x9d, 0xe3, 0x11, + 0x55, 0x87, 0x02, 0x53, 0xd4, 0x80, 0x26, 0x37, 0x26, 0x7d, 0x16, 0xbb, 0x33, 0x45, 0x47, 0xe2, 0x0f, 0xff, 0xd8, + 0x8d, 0x09, 0x1f, 0xa6, 0xef, 0xfe, 0x5b, 0xfb, 0xc9, 0x97, 0xfc, 0x64, 0x7f, 0x4a, 0x5b, 0x7f, 0xda, 0xff, 0xea, + 0x04, 0x0e, 0x77, 0x7f, 0x60, 0xfc, 0xda, 0xa4, 0x94, 0xfc, 0xfb, 0xfd, 0x4a, 0xc9, 0x97, 0xcb, 0x5d, 0x94, 0x92, + 0x7f, 0xff, 0xe2, 0x4a, 0xc9, 0x5f, 0xab, 0xbe, 0x35, 0x6f, 0xaa, 0x59, 0xa8, 0x7f, 0x58, 0x57, 0x45, 0x0e, 0x89, + 0xa7, 0x5d, 0xfe, 0xb4, 0xbc, 0x82, 0xf8, 0xf1, 0x6f, 0x22, 0xf7, 0xe5, 0xb2, 0x64, 0xf0, 0x19, 0x01, 0x8e, 0x7d, + 0x13, 0x11, 0x8e, 0xfd, 0xb0, 0x74, 0xc1, 0xca, 0x8c, 0xb3, 0x39, 0xfe, 0xd8, 0x9c, 0x79, 0xc1, 0x24, 0x67, 0x91, + 0xa0, 0xa4, 0x87, 0xc5, 0xe0, 0x37, 0x0f, 0xe4, 0x19, 0x6e, 0x32, 0xcb, 0x79, 0x98, 0x80, 0x45, 0x30, 0x58, 0x72, + 0x4c, 0xe2, 0xac, 0xd2, 0xd8, 0x12, 0x11, 0xf7, 0xaf, 0xb9, 0x47, 0x71, 0xe3, 0x7b, 0x34, 0x00, 0xae, 0xef, 0xdd, + 0xd9, 0xec, 0x57, 0x01, 0xcb, 0x3a, 0x61, 0x20, 0x0d, 0xdc, 0x7e, 0xdd, 0xfb, 0xb2, 0x19, 0x6e, 0xc5, 0xf0, 0xba, + 0x19, 0x52, 0x80, 0xa4, 0xda, 0xde, 0x29, 0x9b, 0xf1, 0xde, 0x37, 0xcc, 0x9a, 0xcf, 0x97, 0x9a, 0x6f, 0xb1, 0x21, + 0xce, 0x3b, 0xae, 0x4e, 0xd5, 0xba, 0xc4, 0xa7, 0xd5, 0x4f, 0x48, 0x71, 0x41, 0x2d, 0x0c, 0x8d, 0x0b, 0x4e, 0xd5, + 0x56, 0x90, 0xdf, 0xb1, 0xa5, 0x77, 0xa5, 0x3e, 0x65, 0xe3, 0xe4, 0x67, 0x6b, 0xbc, 0x57, 0xf8, 0xbf, 0x02, 0x27, + 0xca, 0x39, 0x9e, 0x61, 0x24, 0xcf, 0xf3, 0x5a, 0xea, 0x97, 0xa4, 0x11, 0xd9, 0xcc, 0x59, 0x6f, 0xf2, 0xa2, 0x8d, + 0x6e, 0x09, 0x0e, 0x9b, 0x0b, 0x2e, 0x08, 0x3f, 0x4f, 0x4e, 0x00, 0x19, 0x39, 0x6a, 0xa0, 0x9f, 0xc3, 0xb6, 0xce, + 0x44, 0xbd, 0x47, 0xb0, 0x89, 0xb9, 0x27, 0xa0, 0x22, 0x87, 0x34, 0x5d, 0x4f, 0x82, 0xc8, 0x4b, 0xbb, 0xc8, 0xa6, + 0x49, 0x2c, 0x6f, 0x0b, 0x3d, 0x16, 0x7a, 0x5b, 0x8c, 0xe9, 0xe4, 0x8e, 0x79, 0x27, 0xe8, 0xf9, 0xb0, 0xcd, 0xfe, + 0x2e, 0x77, 0x38, 0x5b, 0x97, 0xcc, 0x51, 0x9c, 0xc3, 0x63, 0xc3, 0x39, 0x32, 0xac, 0xe3, 0x43, 0x3d, 0x13, 0x07, + 0x4e, 0xee, 0xb2, 0x34, 0x21, 0xe0, 0x00, 0x91, 0x83, 0xe9, 0x87, 0x7e, 0xea, 0x7b, 0x41, 0x06, 0xfc, 0x70, 0xf9, + 0x92, 0xf2, 0xf7, 0x65, 0x92, 0xc2, 0x18, 0x05, 0xd3, 0x8b, 0xce, 0x1f, 0xe6, 0x90, 0xa5, 0x2b, 0xc6, 0xc2, 0x06, + 0xc3, 0x98, 0xaa, 0x2f, 0xc9, 0xef, 0x67, 0x59, 0x9f, 0x91, 0xd5, 0xda, 0x30, 0x0d, 0xf9, 0xfe, 0x10, 0x8e, 0x0f, + 0xd9, 0xc0, 0xf8, 0xae, 0x09, 0xe1, 0xfe, 0x72, 0x3f, 0xc2, 0x4d, 0xd9, 0x2e, 0x08, 0xf7, 0x97, 0x2f, 0x8e, 0x70, + 0xbf, 0x93, 0x11, 0x6e, 0xc9, 0x7f, 0xb0, 0xd0, 0x30, 0xbd, 0xc7, 0x67, 0x0d, 0x5c, 0x64, 0x9f, 0xab, 0xfb, 0xc4, + 0xc0, 0xab, 0x7a, 0x91, 0xbd, 0xf6, 0x2f, 0x4b, 0xd9, 0x82, 0x1a, 0x05, 0xa0, 0x98, 0xd7, 0xd1, 0x47, 0xd7, 0x65, + 0x1f, 0x5c, 0xdd, 0x44, 0x18, 0x06, 0xe8, 0xf3, 0xfb, 0x30, 0x0d, 0xac, 0x77, 0xfc, 0x1e, 0x09, 0x0a, 0xdd, 0x37, + 0x51, 0x3c, 0xf7, 0x30, 0xc5, 0x88, 0xaa, 0x83, 0x3b, 0x1d, 0x3c, 0xd8, 0x10, 0x08, 0x64, 0x14, 0x85, 0xe3, 0x5c, + 0x2b, 0xc9, 0xdc, 0x4b, 0xe2, 0xb8, 0xd5, 0x3b, 0xe6, 0xc5, 0xaa, 0x41, 0xaf, 0x61, 0x71, 0x9f, 0xb5, 0xed, 0x67, + 0xad, 0x83, 0x67, 0x47, 0x36, 0xfc, 0xef, 0xb0, 0x76, 0x66, 0xf0, 0x8a, 0xf3, 0x28, 0x4c, 0x67, 0x45, 0xcd, 0xa6, + 0x6a, 0x2b, 0xc6, 0x3e, 0x16, 0xb5, 0x8e, 0xeb, 0x2b, 0x8d, 0xbd, 0xbb, 0xa2, 0x4e, 0x6d, 0x8d, 0x59, 0xb4, 0x94, + 0xc0, 0xaa, 0x81, 0xc6, 0x0f, 0x97, 0x20, 0x67, 0x97, 0x6a, 0xc8, 0xaf, 0xf9, 0x70, 0x8b, 0x71, 0xb1, 0x76, 0x76, + 0x25, 0x72, 0x28, 0xa8, 0x3d, 0x91, 0x56, 0xef, 0xde, 0x19, 0xe4, 0x2a, 0x4a, 0x1b, 0x73, 0x4e, 0x61, 0x66, 0x43, + 0xc8, 0x38, 0xc5, 0xc4, 0x02, 0x79, 0xb4, 0x40, 0x69, 0xbc, 0x0c, 0x47, 0x1a, 0xfe, 0xf4, 0x86, 0x89, 0xe6, 0xef, + 0xc7, 0x16, 0xff, 0xb0, 0x8e, 0xab, 0xe6, 0xf5, 0xed, 0x22, 0xe9, 0x7c, 0x22, 0x56, 0xc5, 0x7b, 0x96, 0x1a, 0x31, + 0xea, 0xb1, 0x69, 0x69, 0x4d, 0xd7, 0x7b, 0x96, 0x37, 0x7c, 0x96, 0x1a, 0xe1, 0x73, 0xd0, 0x7d, 0xba, 0xf6, 0x93, + 0x27, 0x54, 0x6b, 0xcf, 0x15, 0xc3, 0x3a, 0x1d, 0x15, 0x99, 0x29, 0x14, 0x6f, 0x1a, 0x51, 0x72, 0x8a, 0xee, 0xc8, + 0x88, 0x9e, 0x3f, 0xef, 0xbb, 0x8e, 0x3e, 0x8c, 0x99, 0xf7, 0x31, 0x13, 0xe1, 0xbe, 0x43, 0xcc, 0x4f, 0x7b, 0xbe, + 0x9b, 0xa1, 0x91, 0x5e, 0xeb, 0x4a, 0xbb, 0x80, 0x3b, 0x93, 0x2d, 0xdc, 0x11, 0x38, 0xf6, 0x82, 0xdc, 0xf5, 0x64, + 0x50, 0xe0, 0x09, 0x83, 0x1f, 0x51, 0xe7, 0x7a, 0xe6, 0x25, 0x3f, 0x24, 0x51, 0xf8, 0xcb, 0x02, 0x82, 0x1f, 0x17, + 0x16, 0x45, 0xe2, 0x32, 0xd6, 0xb6, 0x6c, 0xcb, 0x56, 0xf3, 0xfe, 0x26, 0xfe, 0xd4, 0x5d, 0x47, 0xa9, 0xd7, 0xdd, + 0x73, 0x8c, 0x20, 0x9a, 0x82, 0x7b, 0x5d, 0xea, 0xa7, 0x01, 0xeb, 0xaa, 0x2a, 0xf8, 0xd9, 0xcd, 0xe9, 0xba, 0x9e, + 0x71, 0xa7, 0x07, 0x2f, 0x86, 0x6c, 0xe6, 0xf1, 0x9d, 0xf0, 0xd0, 0xc5, 0x18, 0xea, 0x3f, 0x02, 0x8d, 0xd4, 0x54, + 0x0d, 0x44, 0x06, 0x2c, 0x4e, 0x4c, 0xd9, 0x89, 0xa8, 0xab, 0x40, 0x1b, 0x5d, 0xe5, 0x63, 0x9b, 0xc4, 0xde, 0x1c, + 0xd2, 0xed, 0xae, 0x33, 0x83, 0x23, 0x60, 0x95, 0x63, 0x60, 0xc5, 0x79, 0x71, 0x64, 0x28, 0x2d, 0xc7, 0x50, 0x6c, + 0xc0, 0xc2, 0x6a, 0x66, 0xac, 0xb3, 0xab, 0xde, 0x7d, 0x76, 0x10, 0x84, 0x76, 0x1e, 0xd1, 0x38, 0xc8, 0x02, 0x82, + 0x6b, 0x98, 0x52, 0xca, 0x9d, 0xa3, 0x49, 0x89, 0x35, 0x7d, 0xd2, 0x85, 0x5e, 0xb0, 0xdb, 0x54, 0x07, 0x85, 0x92, + 0xa8, 0xe2, 0xeb, 0x6b, 0xf4, 0x23, 0xf6, 0x43, 0xc5, 0xff, 0xf4, 0x49, 0xf3, 0xc1, 0xc7, 0xc9, 0x95, 0xe6, 0x07, + 0x9e, 0xf5, 0xd2, 0x84, 0xf9, 0x85, 0xf6, 0x1e, 0x27, 0x0b, 0x1c, 0x10, 0xe1, 0xdf, 0xa2, 0x58, 0xfc, 0xe0, 0xd6, + 0x13, 0x56, 0xe0, 0x85, 0x53, 0xc0, 0x74, 0x5e, 0x38, 0xdd, 0xb0, 0xd2, 0x22, 0x57, 0xe8, 0x4a, 0x69, 0xd1, 0x55, + 0x61, 0x41, 0x95, 0xbc, 0xbc, 0xbb, 0xf0, 0xa6, 0x3f, 0x79, 0x73, 0xa6, 0xa9, 0x40, 0xfc, 0xd0, 0x73, 0xb7, 0x50, + 0xf0, 0x3e, 0x77, 0x9f, 0x9e, 0xcc, 0x59, 0xea, 0x91, 0x76, 0x08, 0xee, 0xc4, 0xc0, 0x25, 0x28, 0x9c, 0xfe, 0xf0, + 0x38, 0x18, 0x2e, 0xa5, 0xd8, 0x22, 0xf2, 0x61, 0x28, 0x9c, 0x7c, 0x99, 0x68, 0x08, 0xea, 0x3a, 0x06, 0xf9, 0x21, + 0x8c, 0x3c, 0x4c, 0xb3, 0xe3, 0x86, 0x91, 0xda, 0x7f, 0x9a, 0xbb, 0x6c, 0x36, 0x2d, 0x42, 0xe0, 0x87, 0x1f, 0x2f, + 0x63, 0x16, 0xfc, 0xc3, 0x7d, 0x0a, 0xf4, 0xfc, 0xe9, 0x95, 0xaa, 0xf7, 0x52, 0x6b, 0x16, 0xb3, 0x89, 0xfb, 0x14, + 0xee, 0xa9, 0x5d, 0xb4, 0x9a, 0x05, 0x66, 0xfe, 0xf9, 0xed, 0x3c, 0x30, 0xf0, 0xd6, 0x4f, 0xb0, 0xa8, 0xed, 0x56, + 0x11, 0xee, 0xbc, 0xbd, 0xd3, 0x5d, 0xbf, 0xcf, 0x2f, 0xf1, 0x70, 0x31, 0x5c, 0x97, 0xae, 0xde, 0x4e, 0x0f, 0xaf, + 0xd5, 0xc3, 0xc0, 0x1b, 0x7d, 0xec, 0xd1, 0x9b, 0xd2, 0x83, 0x09, 0x44, 0x7c, 0xe4, 0x2d, 0xba, 0x48, 0x75, 0xe5, + 0x42, 0x70, 0xaa, 0xa6, 0xd2, 0x9c, 0xe1, 0xab, 0xdd, 0xcb, 0xb8, 0x95, 0xd7, 0xf8, 0x65, 0xfc, 0xd4, 0x6a, 0xe6, + 0xa7, 0x4c, 0x7c, 0x0a, 0x1f, 0xb2, 0x4c, 0xdc, 0xdf, 0xe9, 0xe6, 0x8a, 0xf7, 0x6d, 0xab, 0xad, 0x38, 0x9d, 0xef, + 0x0e, 0x6f, 0x1c, 0x7b, 0xd6, 0x72, 0xac, 0xce, 0x07, 0xa7, 0x33, 0x6b, 0x5b, 0xc7, 0x81, 0xd9, 0xb6, 0x8e, 0xe1, + 0xcf, 0x87, 0x63, 0xab, 0x33, 0x33, 0x5b, 0xd6, 0xc1, 0x07, 0xa7, 0x15, 0x98, 0x1d, 0xeb, 0x18, 0xfe, 0x9c, 0x53, + 0x2b, 0xb8, 0x17, 0xd1, 0x35, 0xe8, 0x69, 0x09, 0x39, 0x48, 0xbf, 0x73, 0x55, 0xad, 0x51, 0xa2, 0x7a, 0x35, 0xea, + 0xde, 0x05, 0x06, 0x97, 0x10, 0x69, 0x6d, 0x30, 0xf4, 0x90, 0x16, 0xba, 0x8c, 0xd2, 0xcd, 0x0a, 0xc3, 0x37, 0xe1, + 0xa1, 0x5e, 0xe4, 0x3f, 0x95, 0x4e, 0x10, 0xaf, 0xdb, 0x4b, 0x68, 0xbb, 0x4b, 0x61, 0xe5, 0xb4, 0xca, 0xb1, 0x6b, + 0xc8, 0x7c, 0xac, 0x1b, 0xa0, 0x3a, 0x06, 0xc4, 0x54, 0x84, 0x83, 0xd2, 0x6a, 0xd1, 0x96, 0xc0, 0x66, 0x09, 0x4b, + 0xa9, 0x48, 0x13, 0x2d, 0x81, 0xd8, 0xe8, 0xba, 0x48, 0x58, 0x8c, 0x1d, 0xf3, 0x1a, 0x8c, 0x67, 0x56, 0xae, 0x7d, + 0xd5, 0xab, 0xe2, 0x4b, 0xf0, 0x8a, 0xb7, 0xc2, 0x68, 0x85, 0x56, 0x1f, 0xf7, 0xcd, 0x1d, 0xc6, 0x19, 0x60, 0xc2, + 0xe6, 0xac, 0x0c, 0x2c, 0x0f, 0x9d, 0x5d, 0xfd, 0xe0, 0x06, 0x82, 0x7e, 0xd0, 0x07, 0xa5, 0x44, 0xdd, 0x9f, 0xd5, + 0x0f, 0x8f, 0x62, 0x21, 0x27, 0xcb, 0x1c, 0xfb, 0x71, 0x0e, 0x9e, 0x44, 0x51, 0x9c, 0xfa, 0x4c, 0xa5, 0xba, 0x41, + 0xb1, 0x9c, 0x58, 0x7c, 0xe3, 0x05, 0x92, 0xdd, 0x9d, 0xd4, 0x72, 0x2f, 0x27, 0x54, 0x4f, 0x9e, 0x14, 0xc0, 0x99, + 0x15, 0xb8, 0x4f, 0x9c, 0x43, 0xe0, 0x12, 0x0e, 0x59, 0x7b, 0xab, 0x09, 0x28, 0x5d, 0xcc, 0xb6, 0xb9, 0x82, 0x17, + 0x89, 0x99, 0x84, 0x99, 0x97, 0x30, 0x30, 0x69, 0xb4, 0x43, 0xdd, 0x30, 0x2f, 0x81, 0xcc, 0x76, 0x95, 0x9b, 0x99, + 0xaa, 0xf7, 0x42, 0x61, 0x2d, 0x11, 0x6e, 0xc9, 0x49, 0xc7, 0xaf, 0x8e, 0x2a, 0x4c, 0xcd, 0x96, 0x71, 0xdc, 0xe3, + 0xcf, 0xfe, 0xef, 0x1e, 0x04, 0xfa, 0x96, 0x82, 0x79, 0x47, 0x05, 0x8b, 0x94, 0x7c, 0x0d, 0x73, 0x7a, 0x4f, 0x84, + 0x9e, 0x25, 0xa7, 0x2a, 0x14, 0xa9, 0x5d, 0x15, 0xfd, 0xd8, 0xd4, 0xdc, 0xb6, 0x35, 0xa7, 0x62, 0x45, 0x81, 0xe1, + 0x63, 0xfe, 0x4f, 0xe1, 0xe7, 0xaa, 0x3f, 0x79, 0xd2, 0x48, 0x1c, 0xc9, 0x96, 0x28, 0x61, 0xa9, 0xb8, 0x4f, 0x68, + 0xaa, 0x8c, 0x77, 0x55, 0x19, 0xf5, 0xe5, 0xfd, 0x22, 0x36, 0x13, 0x26, 0xb9, 0xb4, 0xf7, 0xf0, 0xe7, 0x90, 0x79, + 0xa9, 0xc5, 0x75, 0xbb, 0x9a, 0xc4, 0x74, 0x18, 0x80, 0x36, 0x32, 0x42, 0x21, 0xf9, 0x30, 0x07, 0x8f, 0xd7, 0x7f, + 0x59, 0xf2, 0x20, 0x14, 0xd0, 0xc7, 0xa7, 0x4f, 0x76, 0x11, 0x37, 0xf4, 0x6d, 0xea, 0x51, 0xdc, 0x36, 0x99, 0x17, + 0x88, 0x52, 0x8f, 0xec, 0x4f, 0x7c, 0x0c, 0xb5, 0x53, 0x1f, 0x41, 0x4c, 0x8a, 0x54, 0xd1, 0x7f, 0x7b, 0xf1, 0x8d, + 0xc2, 0x0f, 0x00, 0x59, 0x37, 0xe0, 0xc5, 0x8b, 0xe2, 0xe3, 0xb8, 0x14, 0x1f, 0x47, 0xe1, 0x99, 0x97, 0x21, 0x47, + 0x6c, 0xb6, 0x4f, 0x53, 0x88, 0x02, 0x73, 0xb2, 0xf9, 0x98, 0x2f, 0x83, 0xd4, 0x5f, 0x78, 0x71, 0xba, 0x8f, 0xc1, + 0x71, 0x30, 0xd8, 0x4e, 0x53, 0xfc, 0x0a, 0x32, 0x1b, 0x11, 0xd9, 0x4c, 0xd2, 0x50, 0xd8, 0x8d, 0x4c, 0xfc, 0x20, + 0x37, 0x1b, 0x11, 0x1f, 0xf0, 0x46, 0x23, 0xb6, 0x48, 0xdd, 0x52, 0x10, 0x9e, 0x68, 0x94, 0xb2, 0xd4, 0x4c, 0xd2, + 0x98, 0x79, 0x73, 0x35, 0x0f, 0xca, 0xb5, 0xd9, 0x5f, 0xb2, 0x1c, 0x42, 0x54, 0x21, 0x11, 0x1e, 0x8c, 0x06, 0x08, + 0x06, 0x1c, 0x00, 0x22, 0x04, 0xc5, 0xa1, 0x29, 0x3c, 0x8f, 0xa6, 0x95, 0x2d, 0x55, 0xb0, 0x54, 0xa7, 0x98, 0xd4, + 0x8c, 0x6e, 0x5e, 0x20, 0xdd, 0x1e, 0x45, 0xc1, 0x35, 0x8f, 0xb9, 0x91, 0x67, 0xc7, 0x51, 0xfb, 0x27, 0xfc, 0x3a, + 0xae, 0x60, 0xb8, 0x19, 0xf5, 0xd0, 0x86, 0xb4, 0x6d, 0x4d, 0xd1, 0x38, 0xf6, 0x79, 0x65, 0xa0, 0x99, 0xd4, 0x33, + 0x66, 0xde, 0x24, 0x58, 0x2e, 0x80, 0x64, 0x95, 0x0c, 0x7c, 0x66, 0x4e, 0x3f, 0x77, 0xff, 0x44, 0xa8, 0x90, 0xaa, + 0x7d, 0xfa, 0xf4, 0x7e, 0xf0, 0xaf, 0x7f, 0x42, 0x7a, 0xd0, 0x99, 0x23, 0x62, 0x60, 0x5c, 0xca, 0xb5, 0x38, 0x5b, + 0x6c, 0x0c, 0xd0, 0xb8, 0x8b, 0x8d, 0x45, 0x74, 0x42, 0xb1, 0xb7, 0xb2, 0xc1, 0x95, 0x88, 0xab, 0x07, 0x89, 0x85, + 0x75, 0x11, 0xa9, 0x63, 0x00, 0xcb, 0x3b, 0x10, 0x31, 0x5c, 0x94, 0xbf, 0xdd, 0xbe, 0x3c, 0x56, 0x8a, 0x70, 0x8f, + 0x75, 0x16, 0x48, 0xb4, 0x87, 0xfa, 0x27, 0x9e, 0x82, 0xdc, 0x14, 0xf2, 0x45, 0x49, 0x77, 0x1f, 0x86, 0x39, 0x8b, + 0xe6, 0xcc, 0xf2, 0xa3, 0xfd, 0x15, 0x1b, 0x9a, 0xde, 0xc2, 0x27, 0x3b, 0x22, 0x94, 0x13, 0x2a, 0xc4, 0x92, 0xe6, + 0xe6, 0x39, 0xc4, 0xf8, 0x67, 0xc5, 0x54, 0x46, 0x95, 0xc0, 0x6d, 0xad, 0x42, 0x6f, 0x79, 0xc0, 0x83, 0xa2, 0x89, + 0x9a, 0xfd, 0x93, 0x7d, 0xaf, 0x5f, 0xce, 0x94, 0x63, 0x89, 0x8c, 0xaf, 0x65, 0x2a, 0x70, 0x4a, 0x09, 0x6f, 0x44, + 0x6e, 0x9b, 0xe2, 0xc1, 0x8c, 0x26, 0x13, 0x39, 0xbb, 0x8d, 0x55, 0x06, 0x2f, 0x9f, 0xb4, 0x62, 0x4b, 0x47, 0x0b, + 0xfa, 0xd2, 0xe6, 0x27, 0xf2, 0x9f, 0x6a, 0x17, 0xd3, 0x5a, 0xc1, 0x98, 0xe1, 0xbc, 0x6f, 0x64, 0xc9, 0xc9, 0x67, + 0xec, 0x11, 0x55, 0xe2, 0x88, 0xa4, 0x9a, 0x93, 0xb1, 0x81, 0xa5, 0xda, 0x73, 0x5d, 0xc2, 0x73, 0x55, 0x74, 0x07, + 0x93, 0x58, 0x93, 0xfd, 0x16, 0x06, 0x9b, 0x42, 0x43, 0x93, 0xdc, 0x7b, 0xb1, 0x51, 0x75, 0x38, 0x9b, 0x30, 0xee, + 0x7b, 0x62, 0xfb, 0x95, 0x36, 0x28, 0x6c, 0x3c, 0xbe, 0xee, 0x80, 0xe0, 0x45, 0x3f, 0x15, 0x3c, 0xaf, 0x7c, 0x4d, + 0x28, 0xdd, 0x0c, 0xbc, 0xbb, 0x48, 0x32, 0xbb, 0xe2, 0x11, 0x58, 0xce, 0xb1, 0xf4, 0x42, 0x78, 0x3e, 0x6f, 0x1c, + 0x34, 0xa4, 0x61, 0x90, 0x25, 0x74, 0xf3, 0xb0, 0x15, 0x04, 0x38, 0x60, 0xf7, 0x9d, 0x35, 0xb9, 0x6e, 0x79, 0x30, + 0x88, 0x3c, 0xb3, 0xe2, 0x1c, 0x96, 0x5e, 0x22, 0x5a, 0xc8, 0x4e, 0xf6, 0x61, 0x7c, 0x94, 0xf7, 0x50, 0x30, 0x79, + 0xc2, 0xbe, 0x10, 0x6f, 0xbd, 0x7e, 0xd3, 0xad, 0xb7, 0xca, 0xa3, 0x94, 0x59, 0x2f, 0x5f, 0x87, 0xd8, 0x36, 0x5e, + 0x42, 0xb6, 0x67, 0xdf, 0x8f, 0x39, 0x61, 0x90, 0xbe, 0x92, 0x07, 0xc3, 0x2c, 0xd5, 0xd3, 0xb7, 0x06, 0x89, 0xa1, + 0x8c, 0xbb, 0x1f, 0x96, 0x98, 0x6e, 0x37, 0xeb, 0xa5, 0x4c, 0xc4, 0x6c, 0x38, 0x4f, 0x1b, 0xc2, 0x3a, 0x34, 0x55, + 0x21, 0x3e, 0x7c, 0x4b, 0x85, 0x62, 0x9b, 0x6f, 0xab, 0x55, 0x70, 0x56, 0x45, 0x35, 0x4f, 0x53, 0x1f, 0xe1, 0x81, + 0xd8, 0xa8, 0x8d, 0xa5, 0x18, 0x6c, 0x22, 0x75, 0xa1, 0xaa, 0x50, 0x2d, 0x78, 0x8b, 0x05, 0x55, 0xd6, 0x7b, 0x27, + 0xfb, 0x74, 0x9d, 0xee, 0xd3, 0x06, 0xec, 0x9f, 0x80, 0x65, 0x3a, 0xed, 0x09, 0x6f, 0xb1, 0xe0, 0x2b, 0x4e, 0xbf, + 0xe8, 0xcd, 0xfe, 0x2c, 0x9d, 0x07, 0xfd, 0xff, 0x05, 0x64, 0x23, 0xa6, 0xdb, 0x06, 0x7b, 0x03, 0x00}; #else // Brotli (default, smaller) -constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x5b, 0x36, 0x7a, 0x53, 0xc2, 0x36, 0x06, 0x5a, 0x1f, 0xd4, 0x4e, 0x00, 0xb3, 0xd6, 0xea, 0xff, 0x0a, 0xab, 0x51, - 0x94, 0xb1, 0xe6, 0xb0, 0x2e, 0x61, 0xbb, 0x1a, 0x70, 0x3b, 0xd8, 0x06, 0xfd, 0x7d, 0x2f, 0x1a, 0x00, 0x55, 0x35, - 0xe3, 0xa8, 0x1c, 0x62, 0xca, 0xd3, 0xb4, 0x00, 0xdb, 0x5e, 0x43, 0xa7, 0x14, 0x08, 0xa4, 0x51, 0x99, 0x96, 0xb6, - 0xf5, 0x0e, 0x99, 0x80, 0x52, 0x31, 0xe8, 0x10, 0x27, 0x1c, 0x04, 0x11, 0x58, 0xc5, 0x1c, 0xcc, 0xd4, 0x74, 0x4c, - 0x33, 0x11, 0xbb, 0xdb, 0xb0, 0xb4, 0x0a, 0x87, 0x13, 0x12, 0xb4, 0x8f, 0xe7, 0xd1, 0xc8, 0x85, 0x26, 0x07, 0xab, - 0x2e, 0x7a, 0x6e, 0x93, 0xb9, 0x4c, 0xb4, 0x84, 0x5b, 0x59, 0xda, 0xde, 0x2f, 0x74, 0x88, 0x3c, 0x53, 0x3b, 0xfd, - 0x28, 0xf8, 0x60, 0x4b, 0xf2, 0xc2, 0x03, 0xda, 0x33, 0x59, 0xe4, 0x2a, 0x48, 0x26, 0xde, 0x20, 0x6e, 0xcb, 0x83, - 0xef, 0x81, 0x7a, 0x6b, 0x36, 0x9a, 0x3f, 0x38, 0x8e, 0xfd, 0xf9, 0x7e, 0x73, 0xab, 0x6e, 0x85, 0xf0, 0x62, 0xe0, - 0x60, 0x60, 0x23, 0x1b, 0x11, 0xaf, 0x39, 0x32, 0xed, 0x79, 0x20, 0x27, 0x04, 0x1f, 0x6b, 0xf4, 0xd9, 0x97, 0x2f, - 0x38, 0xe4, 0x5b, 0x75, 0xbb, 0xb4, 0xfe, 0xd5, 0xfb, 0x5f, 0xa5, 0xc2, 0x5c, 0x88, 0xc6, 0xdf, 0xd0, 0x1e, 0x10, - 0xa7, 0x44, 0x3b, 0xdd, 0xb8, 0xbd, 0x2c, 0x7a, 0x10, 0x61, 0xbe, 0x8b, 0xbb, 0x3c, 0x10, 0x0e, 0x5f, 0x06, 0xc6, - 0xe0, 0xf2, 0xe6, 0x88, 0xa8, 0xb8, 0xa2, 0xab, 0xf7, 0x6b, 0xab, 0xe9, 0xfb, 0xbd, 0xa6, 0xfe, 0xd7, 0xef, 0x23, - 0xd3, 0x86, 0xa9, 0xdc, 0x57, 0x3a, 0xf7, 0xb8, 0x49, 0x6e, 0x45, 0x76, 0xda, 0xa6, 0x21, 0xc3, 0x2b, 0x0f, 0xac, - 0xc9, 0x85, 0x03, 0x60, 0xdd, 0x28, 0x5d, 0xe5, 0x3b, 0xfb, 0xf3, 0xbb, 0xdc, 0xc2, 0x94, 0xab, 0x90, 0x2b, 0x05, - 0xc8, 0x64, 0x3f, 0x3f, 0xc8, 0x1a, 0xe3, 0xef, 0x17, 0x88, 0x77, 0x9f, 0x18, 0x8d, 0xda, 0xd2, 0xc0, 0xb8, 0x5b, - 0x99, 0x6e, 0xd9, 0x12, 0xaa, 0xdc, 0x2f, 0xeb, 0xff, 0xff, 0xfb, 0x4e, 0xff, 0xbf, 0x7e, 0xed, 0x55, 0x22, 0xe6, - 0x8a, 0x96, 0x64, 0x4c, 0xdf, 0x4b, 0x2c, 0xef, 0x43, 0x42, 0x69, 0x18, 0x37, 0x29, 0x19, 0x40, 0x1f, 0x19, 0x8a, - 0x6b, 0x84, 0xb5, 0x31, 0x6a, 0x1d, 0xd9, 0x57, 0x5b, 0x04, 0xbb, 0xd6, 0xde, 0xfa, 0x52, 0xfd, 0xbe, 0x7e, 0x1f, - 0xe7, 0x5d, 0xc3, 0x72, 0xc9, 0x69, 0x3a, 0x6f, 0xf7, 0x1e, 0x6d, 0x29, 0x74, 0xce, 0x4b, 0xde, 0x98, 0xe5, 0x62, - 0x40, 0x34, 0x7a, 0xba, 0x6d, 0x0c, 0x30, 0x01, 0xd0, 0x92, 0x98, 0x29, 0xed, 0xfb, 0xea, 0xeb, 0x7f, 0xfd, 0x56, - 0x3a, 0x29, 0x0a, 0x1f, 0xd9, 0xb7, 0x84, 0x3a, 0x26, 0xfa, 0xd6, 0xce, 0x76, 0x3a, 0x32, 0x19, 0x0a, 0xb2, 0x94, - 0xc7, 0x80, 0xbe, 0x24, 0x74, 0x27, 0x0a, 0xff, 0x5f, 0x5f, 0xd3, 0xfa, 0xfa, 0x95, 0x08, 0xbb, 0xcc, 0xa5, 0x4e, - 0x51, 0xa8, 0x7b, 0x7f, 0x4f, 0x49, 0xce, 0xb2, 0xec, 0x9e, 0x0a, 0x41, 0xcb, 0xb4, 0x8d, 0x17, 0xe1, 0x00, 0xdc, - 0x13, 0x13, 0x59, 0xf7, 0x30, 0x55, 0xeb, 0xbf, 0x9f, 0x97, 0x0d, 0x67, 0x15, 0x30, 0x14, 0x61, 0x0c, 0x48, 0xaa, - 0xd8, 0x41, 0x5a, 0x75, 0x4b, 0xd3, 0x16, 0xa5, 0xc1, 0xd4, 0xc8, 0x9c, 0x84, 0x1c, 0x78, 0x01, 0xe8, 0x24, 0x45, - 0xca, 0xff, 0x73, 0xbe, 0x2d, 0xad, 0xfe, 0x74, 0x75, 0x79, 0xa2, 0xba, 0x1f, 0x6a, 0x09, 0x10, 0xc6, 0x50, 0xb3, - 0x6c, 0x4a, 0xe7, 0x9f, 0x5d, 0xbf, 0x26, 0x78, 0xa6, 0x37, 0x07, 0xce, 0xa5, 0x55, 0xaf, 0xef, 0x06, 0xd4, 0x72, - 0x4f, 0x48, 0xdf, 0x15, 0x1b, 0xec, 0xd7, 0x48, 0xa6, 0xe5, 0xbd, 0xf3, 0x85, 0x88, 0xaa, 0x1c, 0x80, 0x11, 0xb4, - 0x51, 0x68, 0xa0, 0xbc, 0xbd, 0xf6, 0xaa, 0x6f, 0x55, 0x94, 0x8e, 0x0d, 0x8c, 0xc0, 0x32, 0xcb, 0xaf, 0x77, 0x27, - 0x94, 0x5c, 0xad, 0xbd, 0x6f, 0x92, 0xf0, 0x18, 0x70, 0x5a, 0x6c, 0x58, 0x38, 0x2f, 0xd9, 0x90, 0xa8, 0xf9, 0x9a, - 0xad, 0xc0, 0x05, 0x0a, 0xeb, 0x98, 0xcb, 0xaa, 0x7a, 0x8b, 0x0a, 0x89, 0xf8, 0x75, 0xfb, 0x7e, 0xc6, 0x7d, 0x4c, - 0x37, 0xc3, 0x0c, 0x86, 0x8d, 0x82, 0x27, 0x18, 0xd7, 0x33, 0xb5, 0x4c, 0x5f, 0x6f, 0xa7, 0x52, 0x3e, 0xdd, 0x19, - 0x97, 0x57, 0x40, 0x5b, 0x29, 0xed, 0xd9, 0x0b, 0xb1, 0xcb, 0x58, 0x04, 0x08, 0x0d, 0x20, 0x51, 0xe9, 0x1b, 0x22, - 0xf6, 0x9a, 0x6f, 0xe8, 0xe9, 0xbc, 0x01, 0x2c, 0x70, 0xf8, 0x86, 0x54, 0x72, 0xa5, 0x38, 0x22, 0xd0, 0xcb, 0x95, - 0xe1, 0xc1, 0xb5, 0xb9, 0x59, 0x01, 0xae, 0xd5, 0x5a, 0x4a, 0xfa, 0x13, 0x37, 0x9b, 0xd6, 0xff, 0x3e, 0x2f, 0xce, - 0xdb, 0xec, 0x44, 0xba, 0xfe, 0x92, 0xe3, 0xe4, 0x4a, 0xe9, 0xd9, 0x82, 0x09, 0x25, 0xcc, 0xb0, 0x86, 0x01, 0x93, - 0xa6, 0xd5, 0x3e, 0x3c, 0x24, 0x1f, 0x50, 0xab, 0x6f, 0xf8, 0x83, 0xe5, 0xff, 0xef, 0xad, 0xb4, 0xdc, 0xfe, 0x88, - 0x74, 0x20, 0x44, 0xf6, 0x00, 0xe4, 0x38, 0xc3, 0x91, 0xf5, 0xfb, 0xce, 0xcc, 0x2a, 0x50, 0x0d, 0x90, 0x6c, 0x59, - 0xbf, 0xd6, 0x62, 0x53, 0x71, 0xcd, 0xbb, 0xcc, 0xef, 0xa2, 0x33, 0x7e, 0x18, 0x56, 0x64, 0x44, 0x66, 0x23, 0x5d, - 0x0d, 0xcb, 0x36, 0xb2, 0x9c, 0x06, 0xf6, 0xbe, 0xf7, 0x7f, 0x14, 0xfe, 0xff, 0x91, 0xc5, 0x89, 0x88, 0x2c, 0x50, - 0x91, 0x59, 0xc5, 0x39, 0x99, 0x05, 0xcc, 0xa8, 0x0a, 0xe0, 0x8c, 0x0a, 0x60, 0x1f, 0x1d, 0x90, 0x63, 0x41, 0xd0, - 0x68, 0x9a, 0x6c, 0xf6, 0xd1, 0x90, 0x2d, 0x6f, 0x77, 0x5a, 0xac, 0x38, 0x94, 0xeb, 0x19, 0xd9, 0x96, 0xfc, 0x6e, - 0x07, 0xca, 0x9a, 0xa5, 0xb4, 0xd2, 0xd1, 0x7d, 0x73, 0x6f, 0x76, 0xca, 0xa9, 0x23, 0x50, 0x69, 0xd5, 0x56, 0x08, - 0xd7, 0xcc, 0xec, 0x06, 0x76, 0x3f, 0xe7, 0x97, 0x36, 0x1f, 0x37, 0xc5, 0xa4, 0x98, 0x94, 0xe0, 0x80, 0xe4, 0x19, - 0x7b, 0xc6, 0x12, 0x0a, 0xc7, 0xf2, 0xde, 0xa9, 0x3f, 0x86, 0xdf, 0x43, 0x69, 0x61, 0x30, 0x75, 0xa6, 0x69, 0xfa, - 0xef, 0xb6, 0x31, 0xab, 0x2f, 0xd7, 0xca, 0xcc, 0x2d, 0xcd, 0x10, 0x42, 0x0a, 0xa0, 0xa2, 0xeb, 0xff, 0x36, 0xbe, - 0xd6, 0x57, 0x8e, 0xda, 0xfa, 0x75, 0xd4, 0xdd, 0x7e, 0x5c, 0x67, 0x08, 0x10, 0x02, 0xed, 0x3c, 0x64, 0x4a, 0x75, - 0xdb, 0x71, 0x9e, 0x3d, 0x4d, 0x08, 0x21, 0x04, 0xe2, 0xa8, 0xe2, 0xf8, 0x5f, 0x23, 0x9d, 0x4a, 0x1b, 0x02, 0x0d, - 0xa4, 0x48, 0xfd, 0x1b, 0xeb, 0x6f, 0x0d, 0xdb, 0xf7, 0xf4, 0x10, 0x9d, 0xd8, 0xd3, 0x00, 0xa1, 0x36, 0x49, 0xbe, - 0xd8, 0x9a, 0xa7, 0xb5, 0x6d, 0x74, 0x2c, 0x43, 0x2d, 0x7f, 0xdf, 0x4c, 0xbf, 0x6d, 0x30, 0x06, 0x2c, 0x33, 0x25, - 0x81, 0x19, 0xf2, 0xb9, 0x6e, 0x04, 0xf7, 0xcf, 0x4c, 0x26, 0xce, 0x1e, 0x07, 0x80, 0xd3, 0xb1, 0x47, 0x5b, 0x88, - 0x19, 0x28, 0xc7, 0xb9, 0x83, 0x9b, 0x5b, 0x23, 0x98, 0xf6, 0xf6, 0x50, 0xb2, 0xdd, 0x47, 0x21, 0xd6, 0x20, 0x5a, - 0x78, 0x61, 0x76, 0xf4, 0x81, 0xc9, 0xdb, 0x4e, 0x15, 0xbd, 0xa5, 0x5b, 0x7e, 0xc2, 0x1c, 0x81, 0x66, 0xf4, 0x92, - 0x5f, 0x04, 0xf0, 0xbe, 0x7d, 0x0f, 0x45, 0x99, 0x04, 0xca, 0xfe, 0xfa, 0xc9, 0x96, 0x91, 0x9d, 0x9f, 0xb8, 0x6b, - 0x7b, 0xc3, 0xe6, 0xe0, 0x21, 0x83, 0x27, 0x75, 0x98, 0xd9, 0x7e, 0x29, 0x3f, 0xe1, 0x6a, 0x19, 0xe6, 0x36, 0xe6, - 0xf3, 0xfd, 0x14, 0x5d, 0xa1, 0x21, 0x63, 0x41, 0xea, 0xb9, 0xf7, 0x87, 0xc6, 0xf2, 0xc7, 0x64, 0x59, 0x06, 0x6e, - 0xcb, 0x89, 0xc7, 0x92, 0xfa, 0xc5, 0x52, 0x4d, 0xdf, 0x93, 0x26, 0x01, 0x80, 0xac, 0xb5, 0x0d, 0xfd, 0x08, 0x57, - 0x71, 0x7d, 0xad, 0x5c, 0x14, 0xe3, 0x9a, 0x6f, 0x87, 0x09, 0x06, 0x92, 0xf5, 0x13, 0x28, 0x6e, 0x7b, 0xf7, 0x6f, - 0xcf, 0x6e, 0x6d, 0x59, 0x64, 0xd4, 0x74, 0x38, 0xbb, 0x0f, 0x9d, 0x69, 0x03, 0x8a, 0xb8, 0xc3, 0xdd, 0xa7, 0x63, - 0x4d, 0x41, 0x62, 0xc3, 0x21, 0x83, 0xe7, 0x02, 0x1d, 0xcb, 0x3e, 0xa9, 0xa5, 0x24, 0x6b, 0x72, 0xe6, 0xd2, 0x10, - 0x66, 0xa3, 0x73, 0x76, 0x1b, 0x4b, 0x07, 0xdc, 0x96, 0x33, 0xda, 0x45, 0x26, 0xf7, 0xbc, 0x89, 0xef, 0x68, 0xcc, - 0x2f, 0xbb, 0xc0, 0xde, 0xfa, 0x20, 0xdb, 0x42, 0xd0, 0x6c, 0xcc, 0xec, 0xc0, 0x37, 0xde, 0x77, 0xd3, 0x21, 0x09, - 0x12, 0x4d, 0xdf, 0xb7, 0xa0, 0x79, 0x31, 0xfa, 0xb8, 0xee, 0x61, 0xab, 0x50, 0xdf, 0xfe, 0x88, 0x87, 0x19, 0x9e, - 0x78, 0x39, 0x23, 0x5f, 0xef, 0xdd, 0xe6, 0x65, 0xe7, 0xd1, 0x17, 0x31, 0xbe, 0x3d, 0xbb, 0x7d, 0xb9, 0x79, 0x64, - 0xf7, 0x11, 0xd6, 0xbe, 0x1b, 0x12, 0x76, 0x35, 0xbf, 0x77, 0x1b, 0x7e, 0xf4, 0xda, 0x4b, 0x35, 0xab, 0xc9, 0x7f, - 0xfa, 0xfe, 0x13, 0xda, 0xa8, 0x1d, 0xdc, 0xc3, 0xfd, 0x05, 0xc2, 0xb3, 0xa6, 0x38, 0x17, 0x58, 0x73, 0x18, 0x7f, - 0xb6, 0x66, 0xc9, 0x3f, 0x3b, 0x22, 0xf4, 0x39, 0xcc, 0xd7, 0x39, 0x6f, 0xcf, 0x62, 0x6a, 0xfd, 0x4a, 0x44, 0x61, - 0x22, 0x2a, 0x83, 0x26, 0x28, 0xca, 0x2b, 0x27, 0x7d, 0xc7, 0x45, 0x49, 0x20, 0x91, 0xdd, 0x52, 0x2b, 0xa5, 0x75, - 0xe1, 0xe4, 0xde, 0xef, 0x00, 0x02, 0xfd, 0x39, 0xb6, 0x2c, 0xbb, 0xe4, 0xf5, 0x4b, 0x49, 0x7d, 0xc7, 0x3c, 0xcd, - 0x3d, 0x00, 0x91, 0x0a, 0xdd, 0x2c, 0x65, 0x61, 0x89, 0x12, 0x79, 0x36, 0x9e, 0xeb, 0xbc, 0xca, 0xd0, 0x43, 0xb7, - 0xef, 0xdb, 0x25, 0xf2, 0xf0, 0x7c, 0x86, 0x03, 0x7c, 0xc7, 0x9c, 0x53, 0xbd, 0xc2, 0x84, 0xbc, 0x72, 0x56, 0xdc, - 0x8f, 0xa1, 0xd4, 0x68, 0x22, 0x56, 0xe4, 0x66, 0x34, 0xc8, 0x7b, 0x46, 0x6e, 0xaa, 0xfd, 0x52, 0x5b, 0x33, 0x33, - 0xd7, 0xb7, 0xad, 0x96, 0x71, 0x86, 0x72, 0x2f, 0xaa, 0x3a, 0xaa, 0xef, 0x49, 0x20, 0x3d, 0xcb, 0x19, 0xd7, 0x37, - 0x6f, 0x56, 0x94, 0x5f, 0x2b, 0x95, 0x50, 0xf6, 0x0d, 0x35, 0x79, 0xcd, 0xec, 0x4b, 0xea, 0x1a, 0xe6, 0x29, 0xd2, - 0x76, 0x3a, 0xf2, 0x5c, 0x14, 0x32, 0x68, 0x05, 0x7b, 0x9e, 0x3c, 0xba, 0xf6, 0x65, 0x5e, 0xe2, 0x2f, 0x2b, 0xcb, - 0x88, 0x04, 0x68, 0xe4, 0x5f, 0x10, 0xcd, 0x0c, 0x54, 0xc5, 0xd3, 0x64, 0xe1, 0x24, 0x68, 0x71, 0xa2, 0x0d, 0x9e, - 0xfd, 0x7e, 0xdb, 0xec, 0x83, 0x12, 0x2e, 0xaa, 0xd4, 0x7d, 0x4f, 0x13, 0xf2, 0xf0, 0x73, 0x91, 0xac, 0x65, 0xa0, - 0xd7, 0x60, 0x9e, 0x26, 0x20, 0x15, 0x96, 0xc9, 0x58, 0xe8, 0x82, 0x9a, 0xd1, 0x4d, 0x93, 0xe9, 0x11, 0x5f, 0xde, - 0xe2, 0xa2, 0xb8, 0xd5, 0x6a, 0xcb, 0x37, 0xb3, 0xb4, 0x63, 0xda, 0x02, 0xda, 0x1f, 0x3a, 0x81, 0x27, 0x6c, 0x1e, - 0x0b, 0xef, 0x26, 0x27, 0x23, 0x8c, 0x3f, 0xf7, 0x72, 0xa4, 0x1d, 0x6a, 0x77, 0x42, 0xf4, 0xea, 0x40, 0xe0, 0xbe, - 0x50, 0xb6, 0x62, 0xec, 0x4d, 0x12, 0x51, 0xdd, 0x7f, 0x40, 0xb7, 0xf6, 0xbf, 0x59, 0xbb, 0xf4, 0x45, 0xd0, 0x58, - 0x09, 0x47, 0xd2, 0xfa, 0x6d, 0xa8, 0x3d, 0x44, 0x00, 0x4e, 0xaf, 0x82, 0x19, 0x76, 0xdb, 0x80, 0xd9, 0x71, 0x51, - 0x8e, 0xfb, 0x7c, 0xc2, 0x32, 0x3f, 0xd0, 0x32, 0xba, 0xa9, 0xfd, 0x71, 0xfc, 0x21, 0x63, 0x96, 0xb6, 0x30, 0xb5, - 0xaf, 0x1a, 0x7f, 0x69, 0xd0, 0x48, 0xc5, 0x79, 0xed, 0xd0, 0xc0, 0x4f, 0xec, 0x0b, 0x96, 0x83, 0x23, 0x41, 0x2f, - 0xf3, 0xc1, 0x33, 0x97, 0x6a, 0xb0, 0xbc, 0x39, 0x72, 0xa0, 0x10, 0xb6, 0x14, 0xa1, 0x6c, 0xb0, 0xb5, 0xd2, 0x8a, - 0x55, 0x4f, 0x18, 0x9b, 0xf7, 0xa7, 0xb7, 0x26, 0xb2, 0x29, 0x5b, 0x38, 0x4c, 0xf5, 0x85, 0x82, 0x64, 0x7f, 0x16, - 0x77, 0x59, 0x26, 0xc0, 0x41, 0xc2, 0xf0, 0x82, 0x8e, 0x24, 0xdf, 0x07, 0x6f, 0xfa, 0x2c, 0x58, 0x18, 0x6d, 0x9b, - 0x1f, 0x6d, 0xbd, 0x17, 0xcf, 0x2c, 0x88, 0x6f, 0x16, 0x14, 0xdb, 0xb2, 0xe2, 0x08, 0x4b, 0x35, 0x6c, 0x42, 0x68, - 0x05, 0x11, 0xa8, 0xa9, 0x3f, 0xd7, 0x83, 0x6d, 0xd6, 0xbe, 0x6a, 0x55, 0xa5, 0xb3, 0x24, 0xd5, 0x38, 0x82, 0x42, - 0x0e, 0x06, 0x45, 0x10, 0xba, 0xb3, 0x7b, 0xf0, 0x13, 0x07, 0xe3, 0x42, 0xe0, 0x86, 0x96, 0xa7, 0xae, 0x5b, 0xc7, - 0x2d, 0x03, 0x53, 0x7d, 0xb9, 0xd2, 0x3c, 0xee, 0xa1, 0x75, 0xb0, 0x6b, 0x3b, 0x92, 0xcf, 0xb5, 0x78, 0x42, 0xdf, - 0x9d, 0xe8, 0x04, 0x86, 0x87, 0x23, 0x4f, 0xbc, 0x3c, 0x90, 0x80, 0x87, 0x00, 0xb3, 0xf2, 0xdd, 0x0f, 0xa1, 0x7a, - 0x7d, 0x11, 0x50, 0x40, 0x7c, 0x55, 0x6e, 0xfb, 0x03, 0x04, 0x2b, 0xea, 0x58, 0x00, 0x27, 0x2a, 0x4e, 0xc9, 0x01, - 0x09, 0xc6, 0x11, 0xea, 0x1b, 0xa0, 0x9b, 0x0f, 0x95, 0xf2, 0x2f, 0x5c, 0x4f, 0xbc, 0x28, 0xcf, 0xfa, 0xf9, 0x96, - 0x7c, 0xba, 0x6a, 0x51, 0x70, 0xaa, 0xb6, 0x49, 0xa6, 0x50, 0xba, 0xc1, 0x61, 0x4a, 0x11, 0xa7, 0x89, 0x44, 0x2d, - 0x04, 0x60, 0x5b, 0x45, 0xb4, 0xf4, 0xeb, 0x75, 0xd8, 0xd1, 0x52, 0xd8, 0xb2, 0xe0, 0x0b, 0xf5, 0x43, 0x8d, 0xb0, - 0xd8, 0xa8, 0xed, 0x5c, 0x6a, 0xfe, 0xf3, 0x35, 0xc9, 0x86, 0xd6, 0x2e, 0x7b, 0x0b, 0x41, 0x4d, 0xe1, 0x02, 0xb5, - 0xe5, 0x45, 0x32, 0xb5, 0x83, 0x98, 0xfd, 0xc8, 0x44, 0x72, 0x62, 0x79, 0x65, 0x6f, 0x2a, 0x5b, 0xd7, 0xa6, 0xdd, - 0x37, 0x25, 0x18, 0x7e, 0xd4, 0x52, 0x7a, 0x36, 0xec, 0xfc, 0x5a, 0x59, 0x7a, 0x0c, 0x6b, 0x67, 0x4a, 0x20, 0x70, - 0xf9, 0x17, 0xa7, 0xed, 0x02, 0x93, 0x5b, 0x3d, 0xa6, 0xf4, 0x52, 0x8f, 0xf9, 0xee, 0x75, 0x48, 0x95, 0x2c, 0x04, - 0xc9, 0x61, 0xa0, 0xbf, 0x16, 0x13, 0xa5, 0x40, 0x0b, 0x89, 0x50, 0x6e, 0x23, 0x09, 0x70, 0xff, 0x5e, 0x95, 0x31, - 0xc0, 0xb6, 0x0e, 0x3f, 0x4b, 0xb3, 0xab, 0xe7, 0x62, 0x40, 0xd8, 0x18, 0x3d, 0x4c, 0x9d, 0x11, 0xc2, 0x49, 0x53, - 0x7b, 0xe7, 0x2a, 0x12, 0xc9, 0x51, 0x8a, 0x58, 0x6c, 0x9c, 0x95, 0x2e, 0x35, 0xc2, 0x5a, 0x18, 0xcb, 0xb1, 0x02, - 0x92, 0x33, 0xb7, 0x59, 0x79, 0x7c, 0xdb, 0x1c, 0x24, 0xc0, 0x3d, 0xe2, 0xe2, 0x1f, 0x9c, 0x04, 0xc8, 0x35, 0x41, - 0x82, 0x84, 0xf6, 0xd9, 0x80, 0x4c, 0x18, 0x90, 0x91, 0x31, 0x89, 0x6e, 0x04, 0x92, 0x7b, 0x4d, 0xa7, 0xfa, 0x18, - 0x60, 0x2a, 0x27, 0xab, 0x09, 0x44, 0x42, 0x1c, 0x6f, 0x6a, 0xc3, 0x4e, 0x60, 0x2c, 0x03, 0xec, 0xb8, 0x74, 0x54, - 0x72, 0x2e, 0x0e, 0x30, 0xac, 0x9a, 0xf9, 0x85, 0x4d, 0x97, 0xc0, 0x10, 0x47, 0xb4, 0x0b, 0x28, 0xc8, 0xa9, 0xeb, - 0x73, 0x0b, 0x46, 0x0e, 0x12, 0xd7, 0xe8, 0x4e, 0x8b, 0x64, 0x14, 0x91, 0x97, 0xc4, 0xb8, 0xf8, 0x75, 0x4c, 0x93, - 0xb8, 0x58, 0x4f, 0x73, 0x9f, 0x8a, 0xde, 0xb9, 0x0d, 0xfe, 0x71, 0xa3, 0x27, 0x5d, 0x27, 0x2c, 0x01, 0x58, 0x8f, - 0x94, 0x64, 0xc0, 0xa5, 0x9a, 0x9e, 0xfc, 0x3a, 0x48, 0x09, 0xbc, 0x72, 0xd0, 0x39, 0xc4, 0xf1, 0x85, 0x12, 0xd1, - 0xe0, 0xdf, 0xe4, 0xc8, 0x23, 0xf0, 0xeb, 0x67, 0xcc, 0x30, 0xcd, 0x12, 0xd8, 0x63, 0xe5, 0x99, 0x88, 0xf9, 0xab, - 0x46, 0xd2, 0x48, 0x58, 0xf1, 0xb4, 0xd5, 0xd2, 0x54, 0xf8, 0xd8, 0x08, 0x05, 0xc2, 0x1e, 0x80, 0xa6, 0x00, 0xde, - 0x1b, 0x12, 0xf3, 0xe5, 0xa9, 0xa6, 0x24, 0xe5, 0x29, 0x3a, 0x9b, 0xb3, 0x60, 0xfa, 0x2c, 0x2c, 0xa0, 0x9b, 0x63, - 0xca, 0xc3, 0x4d, 0x2c, 0xf3, 0x32, 0xcc, 0x94, 0xcc, 0xa8, 0x25, 0x98, 0x08, 0x93, 0xe1, 0x75, 0x72, 0x01, 0xcb, - 0x7b, 0x9b, 0x66, 0xc6, 0x2d, 0xa3, 0x57, 0xae, 0x4f, 0xa0, 0x79, 0xdc, 0x93, 0x65, 0x91, 0xa6, 0x0c, 0x57, 0x38, - 0x00, 0xe9, 0xaf, 0x98, 0xc7, 0xc2, 0x29, 0x35, 0x2b, 0xb9, 0x71, 0xc3, 0xc5, 0x42, 0x6a, 0x5c, 0xdd, 0x95, 0x77, - 0x22, 0x04, 0x48, 0x65, 0x4b, 0x27, 0x83, 0x67, 0x40, 0xf1, 0x9e, 0x00, 0x02, 0x11, 0x8d, 0xc2, 0x67, 0x7e, 0x92, - 0xa3, 0x55, 0x4e, 0x10, 0x0b, 0x73, 0x55, 0x3b, 0x2f, 0xde, 0x2a, 0x44, 0x94, 0x0b, 0x8e, 0x36, 0x00, 0x5b, 0xb4, - 0x2f, 0x72, 0x9f, 0x41, 0xc0, 0xb7, 0x8f, 0x33, 0xc3, 0x5c, 0x9a, 0x76, 0x48, 0x95, 0xcf, 0xc6, 0xe1, 0xe7, 0xb2, - 0xc0, 0x33, 0x4e, 0x99, 0xd8, 0xfd, 0x67, 0xab, 0xba, 0x21, 0xea, 0xfc, 0x35, 0x75, 0xc0, 0xa9, 0xb6, 0xd9, 0xd9, - 0x8d, 0x41, 0x97, 0xc5, 0xc9, 0x01, 0x93, 0xb2, 0xb3, 0x75, 0xb0, 0xa2, 0x1c, 0xff, 0x72, 0xf2, 0x03, 0x19, 0x0b, - 0x34, 0x5e, 0x15, 0x44, 0x45, 0x46, 0xa6, 0x03, 0x41, 0xbc, 0x34, 0x7c, 0x2e, 0x06, 0x68, 0x91, 0x79, 0x55, 0x52, - 0xa0, 0xd0, 0xac, 0x46, 0x94, 0x37, 0xd0, 0x20, 0x9b, 0xbb, 0x9a, 0x6a, 0x14, 0x1c, 0x21, 0x8f, 0x5a, 0x6c, 0x4c, - 0x8f, 0xc5, 0x52, 0x4c, 0xcb, 0xb4, 0x39, 0x45, 0x12, 0x81, 0xc5, 0x01, 0x71, 0xfd, 0x59, 0xa9, 0xb1, 0x41, 0x94, - 0x79, 0xde, 0x8c, 0x30, 0xe8, 0x6e, 0xe8, 0x49, 0x36, 0x31, 0xd6, 0x4a, 0x10, 0x39, 0x75, 0xd0, 0xd8, 0x8f, 0x5d, - 0x9f, 0xc8, 0xdb, 0x5d, 0x53, 0x4c, 0x75, 0xb9, 0x3f, 0x51, 0x4c, 0x0c, 0x2d, 0xed, 0xfa, 0x79, 0x06, 0x51, 0x3f, - 0x3d, 0x78, 0x9a, 0x72, 0xb6, 0xf6, 0x18, 0x1e, 0xaa, 0xce, 0x25, 0x79, 0xbf, 0xd4, 0x0d, 0x01, 0xf9, 0xb5, 0xc0, - 0xea, 0x91, 0x93, 0x88, 0x22, 0x10, 0xf6, 0xb3, 0xfe, 0x96, 0x30, 0xfa, 0x9b, 0x81, 0x25, 0xbb, 0x1e, 0x6c, 0x4b, - 0x5d, 0x62, 0x2c, 0x6b, 0x65, 0xcd, 0x29, 0x30, 0x5c, 0xba, 0x54, 0x4e, 0x1e, 0x48, 0x3c, 0x54, 0x0e, 0x16, 0xd3, - 0xe7, 0xe9, 0xc2, 0x01, 0x23, 0x85, 0xec, 0xfd, 0x34, 0xc8, 0x8b, 0xd3, 0x8b, 0x24, 0xb5, 0x18, 0x31, 0x76, 0xa9, - 0xb6, 0xb1, 0xf4, 0x08, 0xab, 0xb6, 0x36, 0xf0, 0xfc, 0xc4, 0x76, 0xbb, 0xdd, 0xb0, 0x53, 0x41, 0x56, 0x52, 0x27, - 0x72, 0x33, 0x8b, 0xce, 0x8f, 0x26, 0x91, 0xca, 0x13, 0x46, 0x01, 0x79, 0x39, 0x63, 0x5b, 0x20, 0x8b, 0x6e, 0x8a, - 0x5e, 0x18, 0xe3, 0xd6, 0xb3, 0x5c, 0x7d, 0xeb, 0x37, 0x38, 0x14, 0x92, 0x32, 0x35, 0xcd, 0x94, 0x7b, 0x9d, 0xcd, - 0x77, 0x55, 0x44, 0x8b, 0x72, 0xd6, 0x5c, 0x9b, 0x5f, 0x24, 0xab, 0xbd, 0x14, 0xd9, 0xd2, 0xe9, 0x30, 0x7b, 0x97, - 0x8a, 0xd4, 0x92, 0x04, 0xaa, 0x1d, 0xc7, 0x66, 0x1c, 0xe7, 0x18, 0xf4, 0x56, 0x30, 0xb3, 0x86, 0x97, 0x80, 0xec, - 0x22, 0x85, 0x45, 0x56, 0x5a, 0xbc, 0xd1, 0x2d, 0x25, 0xef, 0x07, 0x89, 0xca, 0xd2, 0xc3, 0x30, 0x93, 0xbe, 0x14, - 0xf6, 0x80, 0x14, 0x8f, 0x50, 0x45, 0x98, 0xbb, 0xdf, 0x45, 0x50, 0xa0, 0x3a, 0xc3, 0x13, 0x98, 0xf6, 0x7c, 0x94, - 0x8e, 0x0b, 0x98, 0x9f, 0xcd, 0x44, 0xbb, 0x7a, 0xb5, 0x06, 0x2c, 0xbc, 0xfa, 0x90, 0xe2, 0x3a, 0xa5, 0xb7, 0xf2, - 0x55, 0xf8, 0x1c, 0x2b, 0xcf, 0x02, 0x1d, 0x2b, 0x15, 0x86, 0xd9, 0x5c, 0x18, 0x23, 0x49, 0x9a, 0x0f, 0xe7, 0xde, - 0xa0, 0x9b, 0x21, 0x28, 0x11, 0x53, 0xdc, 0x10, 0x62, 0x31, 0xcc, 0x58, 0x03, 0xca, 0xdd, 0xa2, 0x99, 0x93, 0xac, - 0xb9, 0x93, 0x49, 0xce, 0x7c, 0xf7, 0x95, 0x5e, 0xa5, 0x94, 0x10, 0x4d, 0xc7, 0x57, 0x39, 0x59, 0x3e, 0x46, 0xc3, - 0x2c, 0xae, 0x1c, 0x13, 0xb4, 0x4e, 0xe2, 0x84, 0xa2, 0x70, 0x48, 0x50, 0x5b, 0x61, 0xba, 0x53, 0x23, 0x9c, 0x0a, - 0x7a, 0x3f, 0xe9, 0xe6, 0x0e, 0xba, 0x13, 0xdb, 0x50, 0xd1, 0x9a, 0x86, 0x0a, 0x62, 0xdb, 0xbf, 0xf7, 0x33, 0x3a, - 0x74, 0xfc, 0x56, 0x34, 0xa6, 0x42, 0xa0, 0x66, 0x0e, 0x97, 0xe7, 0xbe, 0x98, 0x14, 0xe2, 0x4a, 0x5a, 0x9e, 0x08, - 0x92, 0xb4, 0x8f, 0x8d, 0x79, 0xb1, 0xb7, 0x83, 0xc2, 0x34, 0xac, 0xcb, 0x06, 0x44, 0xad, 0x17, 0x2a, 0xf3, 0xeb, - 0xb6, 0x8c, 0x3b, 0x4d, 0x18, 0xaf, 0x9b, 0x81, 0x98, 0xd7, 0xa0, 0x8d, 0x18, 0x8c, 0x55, 0x3b, 0x0e, 0x40, 0x39, - 0x3a, 0x2d, 0x1b, 0xeb, 0x4b, 0xab, 0x46, 0x6f, 0x68, 0x0c, 0x6c, 0xd7, 0x62, 0x91, 0x04, 0xa4, 0x30, 0x66, 0xdd, - 0x8d, 0x49, 0xa7, 0x0a, 0xea, 0x81, 0xec, 0x59, 0x9f, 0x75, 0x3c, 0x4b, 0x4c, 0xf8, 0x25, 0x03, 0x47, 0xf3, 0xe9, - 0xa4, 0x97, 0xae, 0x89, 0x8e, 0x68, 0x6d, 0x21, 0xfe, 0xd4, 0xe0, 0xd6, 0xe2, 0xa5, 0x0e, 0x7c, 0x05, 0xa0, 0x16, - 0x99, 0x0a, 0x21, 0x51, 0x25, 0x15, 0x57, 0x65, 0x3c, 0xd8, 0x94, 0xeb, 0x2a, 0xac, 0x7c, 0x72, 0xef, 0x7a, 0x07, - 0x7f, 0xb6, 0x07, 0x4b, 0xeb, 0x0e, 0xf3, 0xc1, 0xc9, 0x5f, 0x45, 0x48, 0x11, 0xf6, 0xcc, 0xd0, 0xc1, 0xc6, 0x3c, - 0x73, 0xe4, 0xe3, 0xb5, 0x1d, 0xf1, 0xad, 0x0b, 0x6f, 0x98, 0xe4, 0xee, 0x3d, 0x72, 0x19, 0xda, 0x52, 0x40, 0xd4, - 0x6d, 0x6e, 0xfb, 0x83, 0x74, 0xfc, 0x49, 0x4a, 0xf1, 0xef, 0x5d, 0x05, 0x51, 0xbb, 0x68, 0x21, 0x79, 0xa7, 0xe7, - 0xc0, 0x1a, 0x8c, 0x26, 0x8d, 0x11, 0x4c, 0xef, 0x01, 0x97, 0x8a, 0xe2, 0xfc, 0xd1, 0x49, 0x98, 0x70, 0xe2, 0x19, - 0xe0, 0x2f, 0x8d, 0x49, 0xd8, 0x16, 0x01, 0x77, 0xbb, 0x18, 0xff, 0xa2, 0xdd, 0x84, 0x41, 0xde, 0x5d, 0xdf, 0x91, - 0x7e, 0xc4, 0x3d, 0x6c, 0x2e, 0xfb, 0x5b, 0x5e, 0x8a, 0x56, 0xa2, 0x8a, 0x10, 0xa6, 0x46, 0x42, 0x43, 0x9d, 0x23, - 0x81, 0x38, 0xa6, 0x89, 0x35, 0xcc, 0xf6, 0x93, 0xf5, 0x61, 0x97, 0x0a, 0xa1, 0x50, 0x04, 0xa2, 0x33, 0x84, 0x1b, - 0x75, 0x9e, 0x30, 0xc0, 0x3b, 0x04, 0xa0, 0x25, 0xe8, 0xc7, 0x10, 0x9f, 0x5b, 0x27, 0x84, 0xe6, 0x62, 0x9e, 0x3e, - 0x66, 0x0a, 0x4a, 0x52, 0x7d, 0x2d, 0x6f, 0xb3, 0xe6, 0x05, 0x67, 0x2a, 0x2e, 0xa0, 0x68, 0x2b, 0x9e, 0xfa, 0xef, - 0x98, 0x8a, 0x3e, 0x8a, 0x4e, 0x6d, 0xcc, 0x69, 0x9e, 0x30, 0xe7, 0x88, 0x7e, 0xa0, 0xee, 0xc6, 0xf5, 0x6e, 0xc3, - 0x9d, 0xca, 0x12, 0xca, 0x32, 0xf4, 0xba, 0x65, 0xba, 0x94, 0xe4, 0x70, 0x8e, 0xf3, 0xfc, 0x57, 0x0c, 0x71, 0xff, - 0x35, 0xc7, 0xa7, 0xf7, 0x59, 0xda, 0x65, 0x7e, 0xf4, 0xe0, 0xa5, 0x05, 0x66, 0x76, 0xc6, 0x6e, 0x1e, 0xf6, 0x58, - 0x47, 0x02, 0x3b, 0xe2, 0x18, 0xea, 0x1a, 0x67, 0xbc, 0xde, 0x8a, 0x78, 0xa0, 0xb6, 0x1e, 0x6c, 0xbc, 0xa7, 0x34, - 0x4c, 0xb7, 0xa4, 0x8b, 0xac, 0x6b, 0xa2, 0xb2, 0xdf, 0x1f, 0x22, 0xbb, 0xa7, 0xc7, 0x93, 0x3a, 0x69, 0x53, 0x54, - 0x2c, 0x81, 0xce, 0x8d, 0x43, 0xff, 0xe4, 0x2c, 0xcc, 0x63, 0xe8, 0x98, 0xc9, 0x38, 0x5b, 0x67, 0x8c, 0xe7, 0xf6, - 0x33, 0x89, 0xb4, 0x93, 0x81, 0xdf, 0x29, 0x92, 0x9f, 0x7f, 0x58, 0x80, 0x46, 0x14, 0x82, 0xda, 0xed, 0x07, 0x0a, - 0xc5, 0xb1, 0xef, 0x7f, 0x84, 0xb5, 0x7d, 0x8f, 0xd8, 0x85, 0x5d, 0x2c, 0x01, 0xc4, 0x6e, 0x6c, 0xec, 0xff, 0x75, - 0x77, 0xab, 0x91, 0x0d, 0xab, 0x0f, 0x24, 0xd4, 0x57, 0x7b, 0xe6, 0xd9, 0x35, 0xcf, 0x8d, 0x08, 0xce, 0x44, 0x47, - 0xa0, 0x5e, 0xb5, 0xb9, 0xfe, 0x9b, 0xa4, 0xbb, 0x88, 0x22, 0x08, 0x56, 0xd6, 0x20, 0x6b, 0x36, 0xb2, 0xa0, 0x54, - 0xdc, 0x91, 0x1b, 0x7b, 0xbc, 0xc7, 0xf7, 0x76, 0x12, 0x4d, 0x19, 0x25, 0xd7, 0xaa, 0x29, 0x27, 0x0e, 0x63, 0x77, - 0x6f, 0x3c, 0x6b, 0x35, 0x5e, 0x28, 0xfe, 0xa6, 0x06, 0x61, 0xc5, 0x8d, 0xe3, 0x66, 0x83, 0x70, 0xff, 0x6c, 0x51, - 0xa7, 0x6b, 0x91, 0xf1, 0xbc, 0x5a, 0xaf, 0x7d, 0xb6, 0x1b, 0xa9, 0x26, 0xb5, 0x27, 0x34, 0x37, 0x62, 0x9b, 0x77, - 0xdc, 0x6d, 0xc0, 0xe7, 0xc0, 0xc5, 0xd4, 0x91, 0x78, 0xef, 0x51, 0x2f, 0x59, 0xb3, 0xd7, 0x5b, 0x13, 0x1e, 0x1c, - 0x7a, 0x65, 0xb7, 0x7a, 0x22, 0x3e, 0x9f, 0x56, 0xff, 0x74, 0x7f, 0x27, 0x3f, 0xbb, 0x97, 0xb7, 0x6a, 0xca, 0x51, - 0xfa, 0xd4, 0xc4, 0x26, 0x7b, 0x3d, 0x6b, 0x1c, 0xde, 0x6a, 0xdc, 0xee, 0xa4, 0x1b, 0x4d, 0xfb, 0x2f, 0x97, 0x32, - 0x5b, 0xd0, 0x2c, 0x0f, 0x7e, 0x0e, 0x16, 0xdf, 0xb3, 0xd0, 0x7b, 0x15, 0x31, 0xa7, 0x6c, 0x70, 0x48, 0x55, 0x33, - 0xfd, 0x30, 0xde, 0x89, 0x26, 0x6e, 0x3d, 0xda, 0x25, 0xee, 0xa2, 0x46, 0x9c, 0xe4, 0x01, 0xd6, 0x5c, 0xef, 0x0a, - 0xa6, 0xd4, 0x2e, 0xff, 0x04, 0xd2, 0xe2, 0xa5, 0x09, 0x9b, 0xb4, 0xa9, 0xb5, 0x4d, 0x1b, 0xae, 0x02, 0xcb, 0x3f, - 0x16, 0xdb, 0x7c, 0x57, 0xf5, 0x9b, 0x6e, 0xae, 0xf2, 0xf5, 0xcf, 0xe0, 0xc7, 0x6a, 0xaf, 0xe5, 0xa7, 0xff, 0xe1, - 0xfb, 0xff, 0x6a, 0xdc, 0x09, 0x66, 0xbb, 0x39, 0x0b, 0xbe, 0x6a, 0x70, 0x91, 0x65, 0xc3, 0xe7, 0x49, 0xf9, 0xff, - 0xea, 0x7a, 0xf7, 0x8f, 0xab, 0x7f, 0xbf, 0x68, 0x06, 0xf3, 0x11, 0x57, 0x9e, 0x49, 0x5d, 0x9c, 0xfd, 0x37, 0xc4, - 0xd5, 0xd3, 0x7b, 0x1f, 0xb1, 0xc6, 0x15, 0xfb, 0xcd, 0x6e, 0xb2, 0x6c, 0x16, 0x55, 0x96, 0xf0, 0xd4, 0xab, 0x9a, - 0x5d, 0x41, 0x90, 0xcf, 0x7b, 0x9d, 0xcd, 0x47, 0x87, 0x8f, 0x35, 0xa6, 0x7d, 0xa6, 0xdc, 0xfb, 0xfc, 0xc2, 0xf3, - 0x8b, 0x59, 0x39, 0x9e, 0xf7, 0xbc, 0xf4, 0x6a, 0xae, 0xec, 0xc7, 0x9a, 0xe4, 0x4c, 0x67, 0x70, 0xfe, 0x59, 0x39, - 0xbf, 0xf8, 0x7c, 0x7f, 0x76, 0x5f, 0xbe, 0xcc, 0x50, 0xc3, 0x31, 0xcf, 0xac, 0xf9, 0x88, 0x77, 0xa2, 0x56, 0x67, - 0xf1, 0xad, 0xd9, 0xcd, 0xf1, 0x64, 0xc5, 0x11, 0xae, 0xd0, 0x63, 0x3b, 0xac, 0xfc, 0x5d, 0x4f, 0x0d, 0x71, 0xc1, - 0xd0, 0x04, 0x12, 0x0f, 0xfd, 0xdf, 0xc1, 0xd0, 0x0f, 0xbd, 0x97, 0x9f, 0x4d, 0x1a, 0x0e, 0x80, 0xb0, 0xab, 0x96, - 0xc2, 0x6b, 0xa5, 0x3a, 0x62, 0x1b, 0x75, 0x54, 0xf8, 0x40, 0x45, 0x3b, 0x47, 0xdf, 0x5b, 0xde, 0xeb, 0x77, 0xa1, - 0xf4, 0xe0, 0xfa, 0xe1, 0xf1, 0x6b, 0xb1, 0x2e, 0x03, 0xc8, 0x16, 0xc1, 0xe9, 0x86, 0x77, 0xdb, 0xdb, 0x86, 0xe1, - 0x14, 0xaa, 0x3a, 0x51, 0x59, 0x53, 0xeb, 0xcc, 0xbe, 0x8f, 0x16, 0xdd, 0x00, 0x3c, 0x1f, 0xef, 0x13, 0x42, 0xf6, - 0x2e, 0xd2, 0x73, 0xd2, 0x09, 0x23, 0x10, 0xd8, 0x22, 0x0a, 0x6c, 0x1a, 0xe4, 0x7d, 0x7c, 0x87, 0x6e, 0x48, 0xcb, - 0xcf, 0x68, 0xae, 0x3e, 0x77, 0x31, 0xa5, 0xec, 0xe1, 0x6a, 0x6e, 0xb5, 0x4f, 0x01, 0x24, 0xb1, 0x4d, 0x08, 0x58, - 0xfe, 0x93, 0xc7, 0x4d, 0xc3, 0xd6, 0x9b, 0x74, 0xe9, 0x85, 0xd4, 0x9d, 0x9a, 0x34, 0x65, 0x84, 0x91, 0xe9, 0x85, - 0xe7, 0x15, 0x9d, 0x70, 0x97, 0x63, 0xa2, 0x57, 0x8c, 0x8c, 0x59, 0x71, 0xa7, 0xde, 0xb6, 0xbc, 0xfe, 0x5c, 0x07, - 0x41, 0x48, 0x37, 0x36, 0xa4, 0xcb, 0xcc, 0xdd, 0x2b, 0xb8, 0x99, 0x11, 0xb0, 0x79, 0x09, 0x75, 0xc6, 0xef, 0x99, - 0x24, 0xd1, 0x9d, 0x35, 0x4d, 0x9c, 0xf1, 0xb7, 0xd5, 0x6c, 0xa1, 0x8a, 0xc4, 0x0b, 0x73, 0x03, 0x12, 0x84, 0xfa, - 0x86, 0x5a, 0x61, 0xd5, 0x37, 0x98, 0xb4, 0x1f, 0x38, 0x39, 0xcf, 0x34, 0x83, 0x4e, 0xe3, 0x7d, 0x1d, 0x33, 0x26, - 0xba, 0x9a, 0xa2, 0x50, 0x18, 0x6d, 0xe7, 0xcf, 0xe2, 0x66, 0x96, 0xf5, 0x60, 0x02, 0xf0, 0x36, 0x0f, 0xa0, 0x9f, - 0xd7, 0x0a, 0x66, 0xf2, 0x06, 0x3f, 0xfc, 0x12, 0x06, 0x64, 0x7c, 0xe8, 0x41, 0xce, 0xe6, 0x27, 0x05, 0xfd, 0xc7, - 0x20, 0x5e, 0x77, 0x96, 0xb3, 0x5b, 0x42, 0x6b, 0x29, 0xd6, 0x8a, 0x71, 0x96, 0x91, 0xeb, 0xd4, 0x6f, 0xea, 0x2a, - 0x99, 0xaf, 0xed, 0xea, 0x72, 0xf1, 0x6d, 0x4f, 0xee, 0x30, 0x38, 0x05, 0xbc, 0xa0, 0xa1, 0x20, 0x66, 0x2a, 0x3f, - 0xaf, 0xce, 0x6e, 0x28, 0xf5, 0x41, 0x32, 0x7d, 0xae, 0xf0, 0xaa, 0x1a, 0xff, 0x85, 0x45, 0x5f, 0x6a, 0x25, 0xf4, - 0x17, 0x60, 0xf8, 0xd0, 0xb6, 0xcd, 0x84, 0x67, 0x67, 0x0b, 0xf7, 0x34, 0xf9, 0x54, 0x73, 0xb7, 0xda, 0x88, 0x39, - 0xcf, 0x01, 0x81, 0xb4, 0x50, 0x6a, 0xfc, 0x89, 0xd3, 0x12, 0x24, 0xb9, 0xf1, 0xb3, 0x85, 0x32, 0x3a, 0xba, 0xe2, - 0x14, 0xa7, 0x8b, 0xd7, 0x82, 0x55, 0xd4, 0xfe, 0xb6, 0xa9, 0xe3, 0x8b, 0xef, 0xd2, 0xb1, 0x6d, 0xb9, 0x7f, 0x37, - 0x7d, 0xdb, 0xa0, 0x38, 0x13, 0x36, 0x06, 0x7f, 0xe9, 0xbe, 0x6d, 0x0e, 0x34, 0x7c, 0x88, 0x3e, 0x77, 0xaf, 0xfe, - 0xb8, 0x26, 0x5d, 0x81, 0x6e, 0x89, 0xa7, 0x67, 0x3a, 0x28, 0x9a, 0x87, 0x92, 0x8b, 0x17, 0x25, 0xb0, 0x56, 0x30, - 0x9a, 0xce, 0x78, 0x11, 0x9c, 0x14, 0xda, 0xe7, 0x0d, 0x93, 0x9f, 0x16, 0x5c, 0xfb, 0x51, 0x49, 0xef, 0x14, 0x6a, - 0x8c, 0xef, 0xaf, 0x9b, 0x6c, 0xbd, 0x6b, 0x5e, 0x4b, 0xaa, 0x28, 0x8c, 0x7e, 0x87, 0x01, 0xf2, 0xd5, 0x97, 0x25, - 0x32, 0xfa, 0xf6, 0x4e, 0xdd, 0x6a, 0xbe, 0x77, 0x39, 0xd3, 0x19, 0x81, 0x3f, 0xdf, 0x8c, 0x59, 0xd3, 0x74, 0x33, - 0x46, 0x36, 0x02, 0x73, 0x17, 0x10, 0xa5, 0x89, 0x34, 0x28, 0x2b, 0xc8, 0x77, 0x57, 0xc3, 0x56, 0xa9, 0xcd, 0xde, - 0x9f, 0xfd, 0xdf, 0x4f, 0x0b, 0xee, 0x91, 0xf4, 0xe2, 0x8f, 0x1e, 0xaf, 0x63, 0x21, 0xcd, 0xbd, 0x50, 0xe3, 0x67, - 0xed, 0x71, 0xca, 0x6d, 0x3c, 0x40, 0xb3, 0x86, 0x0e, 0x0d, 0xdb, 0x87, 0x74, 0x5c, 0x1c, 0x5f, 0x63, 0xe8, 0xfb, - 0x06, 0x52, 0xc2, 0xd4, 0xe4, 0x3d, 0xa5, 0x15, 0x75, 0x60, 0x04, 0x15, 0xe5, 0x85, 0x72, 0x62, 0xd6, 0x56, 0xf4, - 0x6e, 0xc3, 0xc4, 0xd9, 0xa0, 0xfe, 0x66, 0xcb, 0xcb, 0x1e, 0x7b, 0x1f, 0xf0, 0xf5, 0x4c, 0x01, 0xc7, 0x38, 0xa1, - 0x46, 0xb0, 0x1d, 0xa4, 0xc8, 0x22, 0xf0, 0x09, 0x33, 0x6a, 0x08, 0x65, 0xcd, 0xd4, 0x96, 0xf2, 0x24, 0x48, 0xaf, - 0x2b, 0x2b, 0x5d, 0xd8, 0xe5, 0xdb, 0x2a, 0xba, 0x39, 0xbd, 0x83, 0x3d, 0xf9, 0x5e, 0xf3, 0xde, 0x7a, 0xa9, 0xd4, - 0x6a, 0xda, 0x90, 0xb0, 0x90, 0xb5, 0xc0, 0xae, 0x5b, 0xe7, 0x47, 0xd7, 0x31, 0x66, 0xf1, 0x08, 0x3e, 0x23, 0xd8, - 0x0b, 0x72, 0x53, 0xe7, 0xd6, 0xce, 0x60, 0x61, 0x42, 0xbe, 0xcf, 0xcf, 0x12, 0xb0, 0xb4, 0x63, 0xd3, 0x74, 0x70, - 0x3e, 0xa4, 0xef, 0xa1, 0x77, 0x4b, 0x01, 0x59, 0x38, 0x35, 0x7b, 0x57, 0x8b, 0x02, 0x4f, 0x1c, 0x92, 0x1a, 0xd0, - 0xf7, 0xec, 0x75, 0xfc, 0x46, 0x9f, 0x58, 0x24, 0x52, 0xd3, 0xdb, 0xf8, 0x9b, 0x67, 0xdf, 0xe8, 0xab, 0x53, 0xcf, - 0x84, 0xaf, 0x3e, 0x94, 0x5a, 0xcd, 0x86, 0x06, 0xec, 0x30, 0xd3, 0xc6, 0xb9, 0x0e, 0x4a, 0x7d, 0x33, 0x61, 0x27, - 0xe4, 0x65, 0x71, 0xe3, 0x28, 0x0d, 0xc1, 0x40, 0x7a, 0x11, 0x8f, 0xa2, 0xfc, 0xbe, 0xea, 0xc9, 0xcc, 0xfa, 0x08, - 0x4f, 0x2f, 0x1f, 0xfe, 0xf1, 0xb5, 0x2a, 0xbe, 0xbc, 0xb7, 0x1b, 0xf9, 0xc9, 0x4e, 0x41, 0xe2, 0xd0, 0x54, 0xec, - 0x01, 0x4d, 0x0e, 0x1c, 0x02, 0x71, 0x43, 0x79, 0xec, 0xc3, 0xfe, 0x5c, 0xf3, 0x0d, 0x01, 0x35, 0x56, 0x12, 0x54, - 0x4b, 0x67, 0xfe, 0x72, 0x25, 0x17, 0x2e, 0xde, 0x5d, 0x3c, 0xb7, 0x28, 0x7b, 0xff, 0xf3, 0x96, 0xfe, 0xd1, 0x7e, - 0x6f, 0x28, 0xb7, 0x97, 0x33, 0xff, 0xef, 0x35, 0x85, 0x0b, 0x81, 0x07, 0x24, 0xa9, 0x85, 0xe4, 0x4a, 0x87, 0x8f, - 0xdf, 0x1c, 0xea, 0xbc, 0xc7, 0x74, 0x5f, 0x61, 0x56, 0x17, 0x44, 0xc1, 0xc7, 0x07, 0xa8, 0x6c, 0x03, 0xc5, 0x08, - 0xe1, 0x02, 0x46, 0x1f, 0xee, 0xeb, 0x46, 0x2d, 0x02, 0x69, 0x57, 0xae, 0xfe, 0x78, 0x61, 0xe0, 0x95, 0xc3, 0x6f, - 0x2d, 0xb9, 0x2f, 0x25, 0xbe, 0xd0, 0xd8, 0x9e, 0x5c, 0x4b, 0xe3, 0x89, 0x8c, 0x8a, 0x50, 0x55, 0x91, 0x7c, 0xce, - 0xbd, 0x5a, 0x7d, 0x31, 0x8c, 0x7c, 0x26, 0x30, 0xd7, 0x9b, 0x36, 0x76, 0x9c, 0x54, 0x97, 0xfc, 0xa7, 0x1e, 0x60, - 0x30, 0xd8, 0x97, 0x6d, 0xd3, 0xf4, 0x7e, 0xe7, 0xa4, 0xe1, 0x09, 0x92, 0xc0, 0x1a, 0x0c, 0x5c, 0x95, 0x24, 0x48, - 0x6f, 0xcc, 0x8a, 0x2e, 0x4d, 0xc9, 0x7b, 0xea, 0x29, 0xd9, 0x88, 0xe4, 0x21, 0xa0, 0x23, 0xc1, 0x45, 0xff, 0x51, - 0x6b, 0x23, 0x5c, 0x6b, 0xf9, 0x39, 0x9f, 0x6c, 0xe8, 0x74, 0xb3, 0x2b, 0xb2, 0xa3, 0x0f, 0xa3, 0x3c, 0x9c, 0x3b, - 0x19, 0xe6, 0x61, 0x24, 0xb0, 0x92, 0xb9, 0x79, 0xdc, 0x00, 0xf1, 0x4d, 0x96, 0x64, 0xb7, 0xe4, 0x7f, 0xfc, 0x29, - 0xaf, 0x23, 0xa4, 0x64, 0x5b, 0xdf, 0x55, 0x34, 0x3a, 0x85, 0x93, 0x5c, 0xa7, 0x65, 0x79, 0x21, 0x9c, 0x50, 0x20, - 0x6c, 0x69, 0xd4, 0x48, 0x7e, 0xff, 0xfe, 0xc7, 0x10, 0x2c, 0xfa, 0xf8, 0xa6, 0x99, 0x75, 0x5b, 0x81, 0x31, 0x82, - 0x46, 0x9d, 0x99, 0xdd, 0xe8, 0xf4, 0x26, 0x23, 0x11, 0x28, 0x49, 0x63, 0x8a, 0xb4, 0x87, 0xc3, 0xdd, 0xe6, 0xab, - 0x3f, 0xb2, 0x1d, 0x4b, 0xaa, 0xb6, 0x59, 0xa8, 0x2d, 0x40, 0x80, 0x51, 0xbf, 0x37, 0x10, 0x4d, 0x34, 0x05, 0x05, - 0x2b, 0x6f, 0xe8, 0xdc, 0x8e, 0x6e, 0xcd, 0x6e, 0x41, 0xfb, 0x55, 0xfd, 0x19, 0xa1, 0x83, 0xdb, 0x4a, 0x7a, 0x4e, - 0x4a, 0x55, 0xa4, 0x2e, 0x38, 0xa7, 0x20, 0xb1, 0xb5, 0x0d, 0xb4, 0x7d, 0x6a, 0x4c, 0xe4, 0xfd, 0x45, 0xc5, 0x55, - 0x44, 0x00, 0x02, 0x84, 0x97, 0xe5, 0x5d, 0xc2, 0x27, 0xa3, 0x04, 0x00, 0xd3, 0xe3, 0xd2, 0x4b, 0xc6, 0x52, 0xd1, - 0xf0, 0xb0, 0x55, 0x50, 0x6d, 0xbb, 0x40, 0xe5, 0x80, 0x0b, 0xac, 0xac, 0xc3, 0x3c, 0x13, 0x52, 0x35, 0x29, 0x2e, - 0xba, 0x99, 0x5d, 0xa4, 0x3c, 0xdd, 0xa7, 0xa9, 0x24, 0x6c, 0x5a, 0x7f, 0x67, 0x7c, 0x19, 0x87, 0x68, 0x96, 0xbe, - 0x38, 0x6e, 0x3c, 0x5a, 0xde, 0x8e, 0xa6, 0x03, 0xd3, 0xda, 0x79, 0x12, 0x01, 0xca, 0x4e, 0x95, 0x70, 0xf5, 0x3c, - 0x04, 0x45, 0xa8, 0xf1, 0x43, 0xb7, 0x41, 0xc1, 0x6f, 0xe4, 0xe7, 0xa7, 0x86, 0x02, 0x84, 0xf1, 0xd2, 0x01, 0x0f, - 0xdd, 0xe4, 0xc5, 0x96, 0xb2, 0x73, 0xc0, 0xd8, 0x1b, 0xd1, 0x0b, 0x48, 0x6b, 0x62, 0xea, 0x4e, 0x72, 0x14, 0x5d, - 0x9c, 0x53, 0x5e, 0xc5, 0x2d, 0xd3, 0x65, 0xe9, 0x63, 0xea, 0x9d, 0x08, 0x9f, 0x13, 0x2b, 0x84, 0xff, 0x1d, 0x91, - 0xc3, 0xac, 0x94, 0x69, 0x81, 0x11, 0x6b, 0x89, 0x17, 0x38, 0xdf, 0x09, 0xd1, 0x4c, 0xd5, 0x4c, 0x57, 0x84, 0x79, - 0xaa, 0xaf, 0xf5, 0x9e, 0x3c, 0xc9, 0x1e, 0xa8, 0xf2, 0x61, 0xaf, 0xbb, 0x24, 0x98, 0xd7, 0xb2, 0xa9, 0xb7, 0x61, - 0xa2, 0xb0, 0x0f, 0x16, 0xf2, 0xb8, 0x6a, 0x08, 0x38, 0x3d, 0xf5, 0xab, 0x6f, 0xf5, 0x61, 0xdd, 0xb4, 0x2b, 0x04, - 0x9f, 0x93, 0x44, 0x84, 0x9f, 0xdb, 0x25, 0xce, 0xca, 0xab, 0xeb, 0xec, 0xb3, 0x58, 0xad, 0x41, 0xe6, 0xe5, 0x29, - 0xd1, 0xb6, 0xff, 0xd9, 0x41, 0x79, 0xde, 0x4d, 0x12, 0x3c, 0x4c, 0x47, 0x14, 0x33, 0x71, 0x8e, 0xa4, 0x21, 0x13, - 0xcf, 0xf9, 0xe2, 0x8b, 0x1a, 0xbd, 0x9f, 0x13, 0x4a, 0xc7, 0xa4, 0xf9, 0x8d, 0x0a, 0xa1, 0x0b, 0x09, 0x1d, 0x3f, - 0x74, 0xf9, 0xba, 0xb0, 0x76, 0xf3, 0x89, 0x88, 0xf5, 0x1f, 0xdc, 0x88, 0xa2, 0xd2, 0x79, 0x2c, 0x96, 0x40, 0x32, - 0xc6, 0x4f, 0xf4, 0x1b, 0x33, 0x4f, 0xba, 0x7a, 0xf8, 0x0c, 0x1b, 0x0d, 0xc7, 0x41, 0x9c, 0x03, 0x9e, 0xbf, 0x0c, - 0x7b, 0x5b, 0x1f, 0x15, 0xbf, 0x7f, 0x7d, 0x40, 0x54, 0x6b, 0xb8, 0xa2, 0xf4, 0x67, 0x1c, 0xe2, 0x12, 0xc9, 0x40, - 0x8b, 0x19, 0x7e, 0x21, 0xd2, 0xea, 0x7f, 0x45, 0xce, 0x3d, 0x0e, 0xec, 0x84, 0xfc, 0x17, 0xb7, 0xbd, 0x07, 0x5d, - 0x15, 0x42, 0xde, 0x8e, 0xa8, 0x91, 0x22, 0x0e, 0xef, 0xee, 0xcd, 0xd7, 0xd6, 0x22, 0xe7, 0xc0, 0xac, 0xdd, 0x4d, - 0x99, 0x85, 0xbb, 0x48, 0x6d, 0x31, 0x6d, 0x9a, 0x6d, 0x82, 0x97, 0x61, 0x27, 0x9d, 0x2c, 0x3e, 0xb5, 0x81, 0x50, - 0x55, 0x04, 0x48, 0x25, 0x0b, 0xfd, 0x0b, 0x94, 0xae, 0x5a, 0x2c, 0x43, 0x4b, 0x25, 0xe7, 0xba, 0x12, 0x4b, 0x3f, - 0xa1, 0xc0, 0x20, 0xfd, 0xe2, 0x56, 0x69, 0x3a, 0x2b, 0xa4, 0x88, 0x44, 0x8f, 0xd7, 0x96, 0xdd, 0x5d, 0xa8, 0x3c, - 0x92, 0xee, 0x33, 0x39, 0xc0, 0xf5, 0x4e, 0xaa, 0x8e, 0x95, 0x04, 0xed, 0x40, 0xf7, 0x00, 0xa5, 0x7e, 0x6a, 0x3d, - 0x44, 0x5a, 0x21, 0xf0, 0x12, 0x6e, 0xcc, 0x90, 0x68, 0x1f, 0xa8, 0x87, 0xc4, 0x04, 0xa0, 0x29, 0x38, 0xc1, 0x96, - 0x42, 0xdb, 0xb9, 0x91, 0x5c, 0xa1, 0x80, 0x95, 0xb1, 0x46, 0x35, 0x66, 0x1e, 0x5a, 0x61, 0x22, 0x8e, 0xb3, 0xcd, - 0xc8, 0x43, 0x1c, 0xa9, 0x43, 0xb4, 0xfd, 0x42, 0xe2, 0xa0, 0xc5, 0x99, 0x33, 0x8d, 0x5c, 0x21, 0x1c, 0x9f, 0x82, - 0x34, 0x8c, 0x60, 0xc3, 0xf5, 0x51, 0x6d, 0xac, 0x32, 0x21, 0x72, 0xab, 0x6e, 0x24, 0xed, 0xd7, 0xf1, 0x3b, 0x97, - 0x58, 0xc9, 0xb2, 0xe2, 0x9b, 0xa6, 0xd4, 0x33, 0xe5, 0xd5, 0x67, 0x56, 0x26, 0xbd, 0xdc, 0x47, 0x79, 0xc4, 0x5b, - 0xb0, 0xb8, 0x29, 0xf9, 0x21, 0xa6, 0xa0, 0x06, 0xf0, 0x68, 0x5c, 0x3b, 0x5c, 0x41, 0xb1, 0x18, 0x18, 0x69, 0x3a, - 0xad, 0x1c, 0xf3, 0xa5, 0x9a, 0x0d, 0x0c, 0xf3, 0x18, 0x4f, 0x2c, 0x74, 0x62, 0x7f, 0xcd, 0xf3, 0xb9, 0x45, 0x23, - 0x4f, 0xd3, 0x06, 0x59, 0x7e, 0x9b, 0xd6, 0x6b, 0x95, 0xe3, 0xf1, 0xb6, 0x02, 0x88, 0xdf, 0x41, 0x59, 0x50, 0x0c, - 0x15, 0x4d, 0x8a, 0x19, 0x0c, 0x97, 0xc6, 0x0b, 0x32, 0x01, 0xba, 0x0c, 0x33, 0x6b, 0x8b, 0xaa, 0xbc, 0x3d, 0x16, - 0xc3, 0x7d, 0x50, 0xaa, 0x48, 0x7e, 0xa5, 0x9a, 0x2d, 0x8f, 0x2a, 0xfc, 0xc7, 0x50, 0x1d, 0x43, 0xa4, 0x9d, 0xfc, - 0xab, 0xa3, 0x46, 0xc7, 0x7d, 0x71, 0xc8, 0x8e, 0x3d, 0x3f, 0x63, 0x20, 0x42, 0x4e, 0xba, 0x15, 0x6e, 0xf1, 0x49, - 0x7a, 0xd4, 0x08, 0xf4, 0x15, 0x04, 0x57, 0x6b, 0xde, 0x9d, 0x51, 0x61, 0xab, 0x51, 0x8e, 0x82, 0x32, 0x0c, 0x51, - 0x0d, 0x4f, 0xd1, 0x38, 0xf0, 0xb2, 0xc0, 0x01, 0x13, 0x01, 0x28, 0xe1, 0xb9, 0x24, 0xba, 0xe8, 0x7f, 0x4b, 0x73, - 0xf4, 0x86, 0x15, 0xec, 0xc8, 0x7c, 0xe9, 0x40, 0x8a, 0x80, 0x90, 0x4a, 0xb9, 0xaa, 0x7f, 0x30, 0x10, 0x8e, 0x87, - 0x91, 0xc1, 0xe4, 0x67, 0xc8, 0x87, 0xf2, 0x66, 0x86, 0x97, 0x47, 0x6e, 0x20, 0x4d, 0x4c, 0xa9, 0xc7, 0xb4, 0x46, - 0x6a, 0xb7, 0xdb, 0xc1, 0x55, 0x2a, 0xdd, 0xa0, 0xc6, 0x17, 0x45, 0x30, 0xfa, 0x97, 0x1b, 0x08, 0x3f, 0xfc, 0x2f, - 0x6e, 0x4b, 0xb0, 0x29, 0x7a, 0x8b, 0x03, 0x90, 0xf6, 0x6d, 0xa9, 0xba, 0x1e, 0x20, 0xc6, 0x96, 0x05, 0xfc, 0xe7, - 0xe0, 0x14, 0x11, 0x4b, 0x67, 0x2c, 0x66, 0xab, 0x53, 0x18, 0x72, 0xff, 0x8b, 0xa1, 0x23, 0x08, 0xfb, 0xd7, 0x19, - 0x76, 0x0c, 0x33, 0x40, 0x26, 0x7b, 0x90, 0x8a, 0x38, 0x52, 0x4c, 0x63, 0x1e, 0xad, 0x05, 0x8e, 0x14, 0x69, 0xf1, - 0x3e, 0x2a, 0x15, 0xdd, 0x97, 0x3c, 0x70, 0x40, 0x55, 0x0e, 0xe1, 0xb7, 0x16, 0x7d, 0x2b, 0x21, 0xf3, 0xba, 0xc9, - 0x02, 0x50, 0x17, 0x63, 0x31, 0xd6, 0x13, 0x92, 0x91, 0x9f, 0xb4, 0xa3, 0xc7, 0x68, 0x68, 0xf2, 0xf1, 0xa9, 0xee, - 0xb8, 0xe9, 0x9e, 0xbf, 0x51, 0x43, 0xb1, 0x7f, 0x2f, 0x13, 0x7d, 0x23, 0x8b, 0x64, 0xef, 0xec, 0xb3, 0xd9, 0x19, - 0xe6, 0xa6, 0xa7, 0xc6, 0x48, 0x37, 0xab, 0x3b, 0x9b, 0x2d, 0x53, 0x3b, 0x72, 0x07, 0x34, 0x67, 0x7c, 0x9d, 0xde, - 0x40, 0x1c, 0xef, 0x85, 0xc4, 0xcd, 0x74, 0xc4, 0x94, 0x7e, 0xdc, 0x88, 0x80, 0x1a, 0x45, 0x07, 0x1b, 0x99, 0xf6, - 0x6f, 0x81, 0x9c, 0x4d, 0xd0, 0x41, 0x15, 0x54, 0x5b, 0xcc, 0x4c, 0x0b, 0x39, 0x34, 0xd2, 0x82, 0x82, 0x95, 0xc6, - 0x60, 0xb0, 0x52, 0x95, 0x64, 0x2f, 0x4a, 0x2c, 0x3d, 0xcb, 0x59, 0xe8, 0x50, 0x36, 0x1d, 0x3c, 0x5f, 0x0a, 0x97, - 0x44, 0xbc, 0xac, 0x85, 0x61, 0xda, 0x6c, 0xa5, 0xa5, 0x65, 0x45, 0x25, 0xec, 0xe4, 0xfa, 0xbc, 0x94, 0x85, 0x79, - 0xa2, 0xb6, 0xf1, 0x8c, 0xdc, 0xcc, 0x50, 0xbe, 0x52, 0xfd, 0x30, 0xbd, 0x5f, 0xf8, 0x77, 0x90, 0x40, 0x9f, 0x22, - 0x60, 0x05, 0x5d, 0x12, 0x6c, 0xa4, 0xf4, 0xcf, 0x17, 0x97, 0x35, 0x0a, 0x7f, 0x7a, 0x01, 0xaf, 0xd2, 0xbd, 0x6e, - 0x87, 0xa1, 0xc8, 0xd7, 0x9e, 0x7c, 0x35, 0x9d, 0xfc, 0x91, 0xc2, 0xe1, 0xba, 0x92, 0x9e, 0x4b, 0x6f, 0x16, 0xf4, - 0x73, 0xeb, 0xd5, 0x57, 0xea, 0x2d, 0x54, 0x4f, 0x93, 0x88, 0xdd, 0x2d, 0xbd, 0x8f, 0x9e, 0x8d, 0x42, 0x68, 0x56, - 0xde, 0x7c, 0xff, 0x90, 0xb4, 0x4c, 0xd4, 0x2a, 0x17, 0x77, 0xb3, 0x81, 0xd0, 0xd6, 0x38, 0x47, 0xd8, 0xbb, 0x71, - 0xee, 0x5f, 0x5a, 0x92, 0x00, 0x55, 0x4c, 0x28, 0xbe, 0xa3, 0xd3, 0x40, 0xee, 0x83, 0x3b, 0x3a, 0x7e, 0xbb, 0xf3, - 0xb5, 0x9a, 0x06, 0xf6, 0x08, 0x53, 0x0f, 0xa2, 0xbf, 0xc1, 0xaa, 0x97, 0x5e, 0x2f, 0x17, 0xd8, 0x9c, 0x97, 0x3c, - 0xb0, 0x54, 0x90, 0x95, 0x57, 0xee, 0xc0, 0xa4, 0xf6, 0x08, 0x02, 0xe8, 0x2f, 0x1b, 0x37, 0xf7, 0x77, 0x22, 0x15, - 0xc1, 0x5d, 0x76, 0x9c, 0x8c, 0xd6, 0xf4, 0x3b, 0x3b, 0x8e, 0x05, 0x63, 0xa7, 0x97, 0x09, 0xab, 0x30, 0xd0, 0x8a, - 0xa3, 0xf5, 0x55, 0xf2, 0x8f, 0x2a, 0xc3, 0xac, 0x15, 0x05, 0xec, 0xfd, 0x52, 0x85, 0xf5, 0x41, 0x29, 0xaa, 0x8b, - 0x78, 0x42, 0xc7, 0x93, 0x66, 0xc3, 0x01, 0x8b, 0xa1, 0x45, 0x8c, 0x2d, 0xf6, 0x48, 0x87, 0xcd, 0x38, 0xa9, 0x77, - 0x7c, 0x56, 0xe1, 0xbc, 0x71, 0x1c, 0xb7, 0xc1, 0x6b, 0x8d, 0xca, 0xf2, 0x05, 0x6e, 0xe0, 0x17, 0xaf, 0x54, 0x8f, - 0x7f, 0xf8, 0xf6, 0xba, 0xb8, 0xa8, 0x3a, 0x0c, 0x6d, 0xf1, 0xa7, 0x0d, 0x69, 0x4c, 0x1a, 0xf6, 0x70, 0xfd, 0x4a, - 0x9a, 0x7c, 0xc1, 0xa8, 0xef, 0x90, 0x8d, 0xd9, 0xba, 0x5f, 0x00, 0x8f, 0x79, 0xef, 0x7a, 0xa9, 0x5f, 0x4a, 0x52, - 0x35, 0xa2, 0x15, 0x35, 0xf1, 0xcd, 0x1a, 0x37, 0xc9, 0x5a, 0x90, 0x84, 0xb6, 0x47, 0xed, 0x88, 0x0f, 0xf1, 0xfb, - 0xb7, 0x29, 0x54, 0x81, 0x78, 0x6f, 0x76, 0x5d, 0x06, 0xbb, 0xd5, 0xb3, 0x94, 0x84, 0x95, 0x1b, 0x30, 0x35, 0xd5, - 0xd2, 0x6c, 0x58, 0x85, 0x7c, 0x0e, 0x4c, 0xd2, 0x9a, 0x74, 0x4e, 0x69, 0x21, 0xd4, 0x32, 0xec, 0x9f, 0x92, 0x45, - 0xc4, 0xc7, 0x32, 0xf8, 0xbc, 0x90, 0x53, 0x77, 0xd0, 0x88, 0x2c, 0x46, 0xad, 0xdc, 0x82, 0xdd, 0x8e, 0xd6, 0x3e, - 0x55, 0x09, 0x88, 0xf5, 0xbb, 0x66, 0xe3, 0x6c, 0x14, 0xd8, 0x43, 0xe8, 0x07, 0xdf, 0xf1, 0x36, 0xcb, 0x5d, 0x60, - 0x4a, 0x79, 0xa4, 0xda, 0x52, 0xfa, 0x8c, 0x17, 0x3f, 0xf2, 0x1e, 0x5a, 0xca, 0xdd, 0x7d, 0xc5, 0x1f, 0x3f, 0x5d, - 0xe7, 0xb5, 0x98, 0x4e, 0xe2, 0x5c, 0x9a, 0x63, 0x49, 0xd9, 0x23, 0xc7, 0x71, 0x71, 0xf7, 0x29, 0x14, 0x9a, 0x51, - 0x11, 0x86, 0x34, 0x12, 0x94, 0x9f, 0x29, 0xae, 0xb8, 0xf6, 0x09, 0xad, 0x25, 0x02, 0x64, 0xf0, 0xfd, 0xa3, 0x4c, - 0x57, 0xee, 0xf3, 0x00, 0x7f, 0xb7, 0x68, 0x95, 0xb0, 0x66, 0x51, 0x84, 0x6d, 0x02, 0x92, 0xf5, 0xdd, 0x61, 0x87, - 0xec, 0xec, 0x86, 0x08, 0x02, 0x75, 0xd7, 0x21, 0x40, 0x18, 0xaf, 0x11, 0xca, 0xe5, 0x5f, 0x2b, 0xe3, 0x76, 0x25, - 0x13, 0xea, 0x30, 0xca, 0x2e, 0x28, 0xf0, 0xde, 0x8b, 0x7e, 0xe9, 0x6d, 0x95, 0x1b, 0x5a, 0x4e, 0xd6, 0x47, 0x2f, - 0x3f, 0x29, 0xbb, 0x24, 0x7f, 0xc8, 0x68, 0x01, 0x48, 0xd9, 0x98, 0x66, 0xe3, 0x98, 0xae, 0x5a, 0xb7, 0xcc, 0x47, - 0xd9, 0x06, 0xc3, 0x76, 0x88, 0x51, 0x3c, 0x68, 0xd5, 0x90, 0x5c, 0xb1, 0x69, 0x8f, 0x7b, 0xe9, 0xc1, 0x6d, 0xf7, - 0x7e, 0x38, 0x38, 0x17, 0xf2, 0x88, 0xd9, 0x4b, 0x98, 0x8f, 0x1a, 0xbb, 0x42, 0xa6, 0x19, 0x0e, 0xe2, 0x20, 0x07, - 0xd9, 0x76, 0xdd, 0x05, 0x53, 0x8e, 0x69, 0x71, 0xbb, 0xc5, 0x46, 0x36, 0x90, 0x11, 0x5a, 0xb0, 0xc9, 0xf8, 0x50, - 0x2c, 0xd3, 0xa1, 0x74, 0xdf, 0x63, 0x02, 0xc6, 0x5a, 0x0f, 0x13, 0xbf, 0x66, 0x1d, 0xa2, 0x4b, 0x16, 0xa4, 0xa9, - 0xd1, 0xe3, 0x9b, 0x3e, 0xa5, 0x5b, 0x66, 0x43, 0xc3, 0xf7, 0x3a, 0xcc, 0x1a, 0x0c, 0x2b, 0xf6, 0x89, 0xb2, 0x57, - 0xf5, 0xc7, 0xdb, 0x71, 0x50, 0x4b, 0x39, 0x73, 0x79, 0x9b, 0xd1, 0xbf, 0x99, 0xc3, 0x40, 0xe3, 0x85, 0xc2, 0x7b, - 0x39, 0x81, 0x9a, 0x96, 0x34, 0x29, 0x78, 0xbb, 0xbc, 0x5a, 0x6d, 0xc2, 0xb8, 0x7f, 0xf7, 0xa1, 0x54, 0x5c, 0xff, - 0xee, 0x7d, 0xdd, 0x55, 0xbd, 0x2f, 0x6f, 0x94, 0x4b, 0x3a, 0xaf, 0xd6, 0x57, 0x10, 0xa0, 0xd2, 0x5b, 0x99, 0xb2, - 0x21, 0xdb, 0x83, 0xd7, 0x75, 0xbd, 0x77, 0x55, 0x7e, 0xdc, 0x81, 0xdd, 0x6d, 0x72, 0x16, 0x6b, 0x0b, 0xea, 0xa9, - 0xd3, 0xb8, 0x7b, 0x29, 0xe5, 0x86, 0x83, 0x53, 0x8a, 0xba, 0xc5, 0x37, 0xbc, 0x3f, 0x67, 0xd7, 0xa9, 0x4b, 0xbd, - 0xd5, 0x6f, 0xd7, 0xbf, 0xf2, 0xd4, 0x58, 0xe5, 0xa0, 0x86, 0xf5, 0xab, 0xf6, 0x35, 0x99, 0x5d, 0x83, 0x99, 0x31, - 0x48, 0xe1, 0x72, 0xae, 0x3e, 0x1b, 0x1c, 0x85, 0x79, 0x4e, 0xa8, 0x60, 0x0b, 0x42, 0xfd, 0xf8, 0x25, 0x31, 0x95, - 0xcc, 0x3f, 0x1c, 0x57, 0x46, 0x3f, 0x08, 0x7d, 0xbb, 0x6a, 0xbd, 0x0c, 0x75, 0x4e, 0x91, 0x8f, 0xb9, 0x9a, 0xe0, - 0x97, 0xd4, 0xc1, 0xd1, 0x2c, 0xfc, 0x53, 0x1d, 0xb6, 0x3b, 0x9c, 0x8f, 0x1e, 0x68, 0x5c, 0xed, 0x1b, 0xf0, 0x46, - 0xb4, 0xb3, 0xb0, 0xe3, 0xdd, 0xa7, 0x69, 0xac, 0xc3, 0xe1, 0xcc, 0xb0, 0xa4, 0x8c, 0x38, 0x0c, 0x98, 0x87, 0x6e, - 0xc9, 0x76, 0xb9, 0x6e, 0x93, 0x83, 0x94, 0xf5, 0x1e, 0x4a, 0x31, 0x8f, 0xe6, 0xb9, 0x69, 0xef, 0x79, 0x2f, 0xba, - 0xc2, 0x70, 0x79, 0x30, 0x32, 0x1f, 0xb3, 0x42, 0xaa, 0xae, 0x53, 0xd7, 0x71, 0xa6, 0x35, 0x46, 0xe4, 0x23, 0xc6, - 0xcd, 0xf7, 0x96, 0xb0, 0x68, 0x57, 0xb7, 0x2b, 0x88, 0x33, 0xac, 0xfc, 0x5f, 0x19, 0x9b, 0xa9, 0xa7, 0x0b, 0xb6, - 0xa7, 0x16, 0xfc, 0x79, 0x93, 0xb2, 0xa2, 0x82, 0x1b, 0x1d, 0x41, 0xa9, 0x7f, 0x7c, 0x5e, 0xd4, 0xaa, 0x66, 0x64, - 0xcd, 0x6f, 0x89, 0x77, 0xc6, 0x78, 0x5d, 0xd7, 0x15, 0xb2, 0xdf, 0xc5, 0xa9, 0xd1, 0x87, 0x26, 0x35, 0x8a, 0x64, - 0xfd, 0xa5, 0x68, 0x0e, 0x0c, 0x61, 0x84, 0xc6, 0x9b, 0xb5, 0xce, 0xc9, 0xe0, 0x24, 0xce, 0xaf, 0x3a, 0xb0, 0xde, - 0xce, 0xb1, 0xbd, 0x82, 0x41, 0x10, 0xf8, 0x57, 0x11, 0xa3, 0x55, 0xfd, 0xbc, 0x33, 0x33, 0x54, 0xf5, 0x72, 0x9a, - 0xac, 0x6c, 0xfe, 0x18, 0x53, 0x0d, 0xca, 0x4b, 0xd9, 0x55, 0xf6, 0x89, 0x8c, 0xfa, 0xb1, 0xa0, 0x1e, 0x5d, 0x9e, - 0x33, 0x94, 0xb7, 0x60, 0xcf, 0x52, 0x6f, 0x06, 0x08, 0x91, 0xb6, 0xab, 0x61, 0xc2, 0x31, 0xcc, 0xe9, 0xc8, 0x8a, - 0x55, 0x99, 0xc0, 0x47, 0x11, 0x5f, 0x34, 0xa7, 0x05, 0xce, 0xac, 0x2e, 0x3b, 0xbc, 0x15, 0xa2, 0xa2, 0xb8, 0xe3, - 0x7e, 0x42, 0x6b, 0x3e, 0x0e, 0x33, 0x31, 0x5e, 0xb3, 0x78, 0xde, 0xfd, 0x05, 0x04, 0x4d, 0x9d, 0xd0, 0x60, 0xe1, - 0xdd, 0x0f, 0x05, 0x44, 0xc9, 0x6b, 0x2b, 0x72, 0x92, 0xe1, 0xb7, 0x02, 0xc9, 0x74, 0x84, 0xd0, 0xc2, 0x25, 0x70, - 0xb3, 0xfd, 0x74, 0xdc, 0x05, 0xc7, 0x48, 0x91, 0x58, 0x38, 0x9e, 0x26, 0x6c, 0x3e, 0xb1, 0x26, 0x96, 0xe3, 0xa4, - 0x43, 0xe9, 0x2a, 0x34, 0xd5, 0x2a, 0x06, 0xad, 0xab, 0xfa, 0xc9, 0xde, 0x29, 0x88, 0xdb, 0x96, 0x20, 0xa2, 0x26, - 0xc7, 0x37, 0x1d, 0xb4, 0x3d, 0xb1, 0xc8, 0x1a, 0x65, 0x14, 0xbe, 0xf3, 0x08, 0x65, 0x8c, 0xc0, 0x7d, 0x95, 0x1a, - 0x63, 0x43, 0x59, 0x66, 0x7f, 0x30, 0x7d, 0x33, 0xc1, 0x44, 0x2f, 0xa1, 0xcc, 0x68, 0x95, 0x9c, 0x20, 0xfa, 0x34, - 0x97, 0x72, 0x3c, 0x22, 0xfa, 0x86, 0x85, 0xbf, 0xc4, 0xe2, 0x2a, 0xe6, 0xdd, 0xe5, 0xed, 0x74, 0x6d, 0x91, 0xcf, - 0xd4, 0x16, 0x63, 0x97, 0xcc, 0xa1, 0xf6, 0xb0, 0x21, 0x3b, 0xf1, 0x86, 0x9d, 0x16, 0xa5, 0x7c, 0x3b, 0x4a, 0x11, - 0xf6, 0x5d, 0xd1, 0xbf, 0x5d, 0x6f, 0x0a, 0x73, 0xed, 0x8a, 0xc9, 0xdf, 0xea, 0xeb, 0x19, 0x5a, 0x4f, 0x7c, 0x35, - 0x74, 0x73, 0x58, 0xf3, 0xc7, 0x02, 0xdf, 0x22, 0x2c, 0xb7, 0xb7, 0xd1, 0xc4, 0xb6, 0xce, 0x4b, 0x4f, 0x60, 0xb0, - 0x10, 0x7e, 0x37, 0x4b, 0xf1, 0x80, 0xd5, 0x83, 0xe8, 0x83, 0x02, 0x13, 0x53, 0xb9, 0x7a, 0xb5, 0x62, 0x8f, 0xd0, - 0x1e, 0xc6, 0x3a, 0x91, 0x5a, 0xf9, 0x36, 0xb8, 0x5a, 0xe1, 0x95, 0xbe, 0xde, 0x14, 0xb1, 0x5e, 0x79, 0x58, 0xdb, - 0xea, 0x97, 0xdc, 0xc2, 0xe5, 0xdf, 0xb6, 0x2a, 0x02, 0x7c, 0xc2, 0x55, 0x88, 0x23, 0xd0, 0x77, 0x69, 0x15, 0x05, - 0xf5, 0x97, 0x1c, 0x72, 0xea, 0xfc, 0x88, 0x70, 0x3e, 0x5f, 0x57, 0xf5, 0x1c, 0x47, 0x78, 0xab, 0xfc, 0x0f, 0x96, - 0x26, 0x6f, 0xe2, 0x7a, 0xb4, 0xe7, 0x25, 0x1d, 0x3e, 0xc1, 0xa5, 0x3b, 0x0a, 0x23, 0xbc, 0x94, 0x31, 0x8d, 0x17, - 0xe7, 0x1a, 0x30, 0x7b, 0x83, 0xe4, 0xf5, 0x38, 0x10, 0x95, 0x1c, 0x87, 0x2d, 0x16, 0xb5, 0x9e, 0x14, 0x1a, 0x91, - 0x37, 0xac, 0xca, 0x50, 0x44, 0x4b, 0x62, 0x07, 0x88, 0xfc, 0x58, 0x14, 0x86, 0x0e, 0xd5, 0x22, 0xb4, 0x6b, 0x7c, - 0xbf, 0xa9, 0x8f, 0xd0, 0x12, 0xab, 0x89, 0xf0, 0x61, 0x41, 0xde, 0x03, 0x0c, 0x73, 0x98, 0x24, 0xad, 0xa3, 0x74, - 0x90, 0xe3, 0x2b, 0x41, 0x32, 0x39, 0x30, 0x93, 0xde, 0x41, 0x6c, 0xe7, 0x7c, 0x31, 0x79, 0xbf, 0x11, 0x3f, 0x85, - 0x34, 0x74, 0x95, 0xbd, 0xc6, 0x22, 0xfb, 0x60, 0x82, 0xb5, 0x2f, 0x0f, 0x97, 0x99, 0xd7, 0xe0, 0x27, 0xfc, 0xff, - 0x34, 0x4b, 0x0a, 0xba, 0x0b, 0xb8, 0x98, 0xbd, 0x0f, 0xc3, 0x04, 0x3e, 0x19, 0x4d, 0x54, 0x96, 0xe8, 0x85, 0x05, - 0x4a, 0xd9, 0xef, 0xb7, 0xd0, 0xe0, 0x10, 0xcc, 0x4b, 0xde, 0xf9, 0xaa, 0x5b, 0x29, 0xaf, 0xef, 0xe4, 0xc8, 0xe4, - 0xf3, 0x29, 0xda, 0x1a, 0xb6, 0x56, 0xc0, 0x4b, 0x08, 0x03, 0x41, 0x34, 0xa4, 0xb8, 0xa4, 0xc3, 0x15, 0xc8, 0x1a, - 0xb9, 0x63, 0xd1, 0x4a, 0x19, 0x4e, 0x1b, 0x37, 0x13, 0xb5, 0xfb, 0xb4, 0x10, 0xc3, 0x79, 0x49, 0x87, 0xb1, 0xa4, - 0x0f, 0x4c, 0xb8, 0xc1, 0xb2, 0xad, 0x0a, 0xec, 0x80, 0xe6, 0x79, 0xd1, 0x68, 0x95, 0x9a, 0x0c, 0xa9, 0xa4, 0x93, - 0xf0, 0x11, 0xaf, 0x1d, 0x29, 0x19, 0xeb, 0x44, 0x4e, 0x13, 0x86, 0xb0, 0xfd, 0xd0, 0x48, 0xd4, 0x41, 0x61, 0x4d, - 0x21, 0x38, 0x2f, 0x34, 0xe0, 0xa6, 0x8d, 0xee, 0x07, 0xa8, 0xba, 0xd0, 0x48, 0xb3, 0xd2, 0x16, 0xb9, 0x6e, 0x2c, - 0x0e, 0xca, 0x8f, 0x78, 0x6d, 0x5e, 0x67, 0x55, 0x61, 0x23, 0x91, 0x7b, 0x28, 0x86, 0xb0, 0x19, 0xd3, 0x1f, 0xae, - 0xdc, 0xe3, 0x12, 0xeb, 0xb8, 0xc7, 0x80, 0x2d, 0xaf, 0x90, 0xc6, 0xec, 0x55, 0x72, 0x60, 0x21, 0x03, 0x34, 0xaf, - 0xc4, 0xf0, 0xbe, 0xf5, 0xcb, 0xa1, 0xf1, 0xad, 0x5a, 0x99, 0x4b, 0xcf, 0x84, 0x89, 0x11, 0x1e, 0x08, 0x83, 0x5f, - 0xab, 0x3f, 0x9d, 0xf6, 0xb2, 0x4e, 0x71, 0xbf, 0xca, 0x21, 0x37, 0x27, 0x2c, 0x1a, 0xe8, 0xa0, 0x4c, 0x4e, 0x17, - 0x39, 0x07, 0xf5, 0xcd, 0xdd, 0x82, 0xbc, 0x40, 0x1c, 0x6b, 0x3c, 0x8e, 0x5c, 0xf7, 0x62, 0xde, 0x66, 0x22, 0xd8, - 0x9b, 0x0a, 0xfe, 0x39, 0xc4, 0x29, 0x21, 0x80, 0x35, 0x48, 0x6c, 0xd6, 0xe5, 0x1e, 0xdb, 0xcb, 0xd8, 0xae, 0x13, - 0x99, 0xa2, 0xb2, 0x82, 0xe4, 0xe7, 0x11, 0x76, 0x81, 0x1a, 0x0f, 0x36, 0x49, 0x0f, 0xb2, 0x32, 0x0d, 0x23, 0x96, - 0x6f, 0x57, 0xc5, 0x29, 0xcc, 0x6b, 0xb5, 0x0e, 0x85, 0x20, 0x99, 0xd9, 0x6d, 0x23, 0x9f, 0x33, 0x4f, 0xc2, 0xa0, - 0x63, 0x47, 0x69, 0x83, 0x0a, 0xe5, 0xd8, 0x56, 0xf3, 0x68, 0x82, 0x5e, 0xf6, 0xd6, 0x39, 0x24, 0x73, 0x5b, 0x4e, - 0x0b, 0x56, 0x04, 0x24, 0x1e, 0xd7, 0xe2, 0xa3, 0xa9, 0xbb, 0xa1, 0xce, 0x11, 0x16, 0x39, 0x30, 0xcb, 0x96, 0x89, - 0xa8, 0xc5, 0xa5, 0x67, 0x6e, 0x1a, 0x6c, 0x2a, 0xcb, 0x4c, 0x7a, 0x11, 0xb2, 0x68, 0xa5, 0x89, 0x2d, 0xcc, 0xc5, - 0x38, 0x33, 0x07, 0x96, 0xf6, 0x11, 0x1a, 0x06, 0xcb, 0x48, 0x48, 0x63, 0x4b, 0x96, 0xb7, 0xc8, 0xa9, 0xc0, 0xd1, - 0xfe, 0x67, 0x96, 0x3b, 0x62, 0x2b, 0xf7, 0xd0, 0x82, 0xef, 0xf7, 0x57, 0x51, 0xc3, 0xd0, 0xf6, 0x57, 0xfe, 0xbd, - 0xe4, 0x22, 0xa8, 0x57, 0x90, 0x0f, 0x49, 0x26, 0x15, 0x38, 0x28, 0x0c, 0xd4, 0xc9, 0xb8, 0x11, 0xad, 0x4d, 0x78, - 0x24, 0x87, 0x48, 0x13, 0x79, 0x6d, 0x29, 0x2a, 0x87, 0x22, 0x6b, 0xaf, 0xd4, 0x2a, 0x21, 0x00, 0xbd, 0xf5, 0x4e, - 0xb7, 0x1a, 0x0d, 0x6f, 0xd4, 0x24, 0xca, 0x41, 0x7c, 0x38, 0x0d, 0x4f, 0xda, 0xe8, 0x8a, 0xf3, 0x72, 0xe2, 0x33, - 0x75, 0x77, 0x40, 0xa0, 0x81, 0xb3, 0x80, 0xc3, 0x0b, 0x83, 0x59, 0x5d, 0x55, 0x56, 0xdb, 0x05, 0x09, 0xb2, 0xa9, - 0x7f, 0x41, 0x7f, 0x58, 0x9b, 0x23, 0xb5, 0x49, 0x30, 0x1a, 0x47, 0x93, 0xf5, 0xbf, 0x8b, 0x09, 0xbc, 0xa2, 0x2e, - 0xd0, 0x1e, 0x9f, 0xb6, 0x73, 0x2a, 0x4a, 0xb6, 0xfd, 0xe7, 0x43, 0x39, 0x81, 0xfd, 0x5e, 0x76, 0x62, 0x76, 0x78, - 0x2a, 0x47, 0x3d, 0xbd, 0x4a, 0xc5, 0xd8, 0x43, 0x0c, 0xf5, 0x76, 0x04, 0x2c, 0xeb, 0x1b, 0x6b, 0x96, 0xcd, 0x76, - 0x24, 0x5b, 0x73, 0xf4, 0x72, 0x96, 0x48, 0xee, 0x1e, 0x34, 0x58, 0xce, 0xc4, 0x96, 0xcf, 0xe8, 0x79, 0xbb, 0xb5, - 0xc7, 0xe4, 0xed, 0x2c, 0x3b, 0x82, 0x2f, 0x5a, 0xdf, 0xd8, 0xd5, 0x5e, 0xf4, 0xc8, 0x75, 0xec, 0xc3, 0x04, 0x92, - 0x60, 0xb1, 0x00, 0x17, 0x71, 0xcf, 0x27, 0x67, 0xd6, 0x62, 0x9f, 0x8a, 0xd8, 0x45, 0x5a, 0xa8, 0x9f, 0xd2, 0x32, - 0x22, 0xe9, 0xf0, 0xf2, 0x63, 0xcf, 0x48, 0x1e, 0xd7, 0x51, 0xaa, 0xd4, 0x6f, 0x3d, 0x8e, 0x32, 0xb2, 0xc8, 0x51, - 0x27, 0x5c, 0xc2, 0x63, 0xdb, 0xc9, 0x4e, 0x77, 0xac, 0x5a, 0xc8, 0x3e, 0x80, 0x00, 0x49, 0xc8, 0x96, 0xb4, 0xdd, - 0x45, 0x6a, 0xe4, 0xef, 0x71, 0x31, 0x2c, 0x46, 0x84, 0xf0, 0xb0, 0x6e, 0xf0, 0x4f, 0xba, 0xcc, 0xd4, 0xdb, 0xa9, - 0x7c, 0xf0, 0x82, 0x38, 0x1a, 0x0f, 0x8f, 0xd4, 0x28, 0xb4, 0xfa, 0x64, 0x1c, 0x35, 0x29, 0x9c, 0xa1, 0x95, 0xd3, - 0xf6, 0x6f, 0xa0, 0xb7, 0x53, 0xd3, 0x45, 0xa0, 0x45, 0xe1, 0x8b, 0x47, 0x28, 0x01, 0xb1, 0x44, 0x58, 0x31, 0xa4, - 0x92, 0x30, 0x95, 0x09, 0xb9, 0x64, 0xcc, 0x65, 0xc6, 0x9d, 0x5a, 0x05, 0x77, 0x59, 0x15, 0xf7, 0x58, 0x3d, 0xee, - 0xb3, 0x06, 0x3c, 0xa8, 0x35, 0xe2, 0x21, 0xab, 0xe1, 0x1c, 0xa2, 0x7a, 0x51, 0xbd, 0xac, 0x5e, 0x55, 0xd7, 0xca, - 0x75, 0xaf, 0x43, 0x26, 0x30, 0xfe, 0x25, 0x49, 0xb4, 0xfa, 0x10, 0x2d, 0x4d, 0x79, 0xbe, 0x73, 0xf7, 0xde, 0xfd, - 0x07, 0x0f, 0xc7, 0x05, 0x2a, 0x71, 0x56, 0xf7, 0x6d, 0x59, 0xd2, 0x00, 0xb6, 0x50, 0x3a, 0x7d, 0x4b, 0x49, 0x83, - 0x89, 0x42, 0xdb, 0xf9, 0x93, 0xa5, 0xbf, 0x3c, 0xfe, 0xa0, 0xea, 0xb9, 0x79, 0x56, 0xac, 0xbc, 0xf5, 0x88, 0xd3, - 0x02, 0x2d, 0x9e, 0x5e, 0xb0, 0x90, 0x4e, 0x07, 0x1d, 0xf3, 0x6a, 0x1e, 0xf3, 0x90, 0x51, 0x8f, 0xad, 0x85, 0xa7, - 0x5a, 0xc9, 0xf2, 0x07, 0x1d, 0xc0, 0xb1, 0x38, 0x9e, 0xc9, 0xbe, 0x79, 0x24, 0x66, 0xa2, 0x69, 0xda, 0x6a, 0x42, - 0xd5, 0x3f, 0xdc, 0xba, 0xa1, 0xea, 0xc0, 0xc6, 0xec, 0xf8, 0xed, 0x27, 0x0a, 0xb0, 0x4c, 0x13, 0x98, 0x70, 0xe3, - 0x38, 0x6b, 0x16, 0x2e, 0x66, 0xcb, 0xe6, 0xf9, 0x88, 0x01, 0xea, 0x99, 0x0a, 0xa0, 0x92, 0x1e, 0xf8, 0x67, 0xc7, - 0xfe, 0x74, 0xee, 0x2e, 0x6c, 0x18, 0xfd, 0xce, 0xec, 0x7e, 0xf5, 0x1b, 0x71, 0x02, 0xa9, 0xc7, 0xa2, 0xb2, 0x52, - 0xcd, 0xc4, 0x8b, 0x6a, 0x6f, 0xc4, 0xd1, 0xcd, 0x4c, 0x73, 0x63, 0x6f, 0x6f, 0x82, 0xd1, 0xee, 0xc8, 0x00, 0x88, - 0x40, 0xfd, 0xe7, 0xf4, 0x94, 0xd5, 0xfa, 0x76, 0xfa, 0x4d, 0xca, 0xa6, 0x48, 0x09, 0x3e, 0x2d, 0xfe, 0xe0, 0x9f, - 0xba, 0x6f, 0x5a, 0x6c, 0x85, 0x36, 0xf2, 0xb9, 0xca, 0x89, 0x23, 0x91, 0x27, 0xbe, 0x63, 0xeb, 0xac, 0xf2, 0xb0, - 0xf1, 0x53, 0x58, 0xde, 0x66, 0x5a, 0x5c, 0x8a, 0x63, 0x6d, 0x9c, 0xc3, 0x22, 0x37, 0xff, 0x4c, 0xb5, 0xa3, 0xf1, - 0x8b, 0x7d, 0xfb, 0x2b, 0x53, 0xe5, 0x61, 0x39, 0xbe, 0xf2, 0xef, 0xf2, 0x73, 0xf4, 0x38, 0x2f, 0x72, 0xb5, 0xfe, - 0x3e, 0x35, 0xcb, 0x96, 0x0f, 0x4f, 0x72, 0x5f, 0x71, 0x99, 0xa7, 0xa6, 0xf9, 0xa5, 0xaf, 0x86, 0x3c, 0x77, 0xad, - 0xe9, 0xdd, 0xcd, 0xcf, 0x58, 0xfe, 0x49, 0x35, 0xab, 0xf6, 0xa0, 0x7f, 0x95, 0x3d, 0x3b, 0x9e, 0x8a, 0x11, 0x99, - 0x1a, 0x2b, 0x73, 0x40, 0x75, 0x7f, 0x7e, 0x16, 0x09, 0x6e, 0xfc, 0xa7, 0xc7, 0x25, 0x3d, 0x83, 0xa4, 0xb7, 0x75, - 0xfe, 0x42, 0xe8, 0xa6, 0x9e, 0xf4, 0xe0, 0x10, 0xd4, 0x2b, 0xfe, 0x37, 0x0f, 0xe1, 0x0b, 0x4c, 0x5d, 0x20, 0x10, - 0x6f, 0x60, 0x2a, 0xf4, 0xf3, 0xcb, 0xe8, 0x34, 0xd1, 0xdd, 0xa4, 0x65, 0xaa, 0xa2, 0xa6, 0x94, 0x13, 0x3b, 0x42, - 0xc1, 0xb7, 0x93, 0x91, 0x5e, 0x32, 0xda, 0x3a, 0x7f, 0x2d, 0x74, 0x4b, 0x91, 0xdd, 0x4d, 0xbc, 0x55, 0xe7, 0x3d, - 0x2b, 0xe7, 0xcf, 0xf5, 0x74, 0x7b, 0x82, 0x5d, 0x7d, 0x06, 0x54, 0xcb, 0x02, 0x9c, 0x97, 0x6f, 0x60, 0xda, 0x2f, - 0x02, 0x94, 0xd1, 0x62, 0x35, 0xf4, 0x1b, 0xf5, 0x96, 0xc9, 0xfa, 0xdb, 0x8f, 0x6b, 0x0d, 0xd8, 0x39, 0xfc, 0x73, - 0x03, 0x86, 0xe7, 0x48, 0xf4, 0x1c, 0x41, 0x97, 0xae, 0x31, 0x97, 0x35, 0xda, 0x90, 0x3d, 0xd5, 0xd8, 0xbc, 0x05, - 0x97, 0x82, 0xf0, 0xb6, 0x61, 0x36, 0xcd, 0x7c, 0xac, 0x62, 0xae, 0xce, 0x65, 0x9e, 0x3e, 0x23, 0x21, 0xdf, 0xb7, - 0xac, 0x8d, 0x05, 0x53, 0x10, 0xcc, 0x6c, 0x98, 0x32, 0x96, 0xaf, 0x8b, 0xa6, 0x14, 0x7e, 0x1f, 0xc5, 0xb0, 0xee, - 0x19, 0x8b, 0xa4, 0x60, 0x5d, 0xf9, 0x2f, 0x39, 0xe6, 0x31, 0x8f, 0xa5, 0x83, 0x9e, 0x1b, 0xe6, 0xd1, 0x0c, 0x7c, - 0xcc, 0xa8, 0x4a, 0x9c, 0xc1, 0x8e, 0x17, 0xcf, 0x88, 0x82, 0x16, 0xcd, 0xf5, 0xc7, 0xb6, 0xd6, 0x87, 0x4c, 0xdd, - 0xad, 0x98, 0xcd, 0xcf, 0xf4, 0x6d, 0xb6, 0xe9, 0x80, 0xc9, 0x12, 0x41, 0x98, 0x43, 0xf3, 0x7b, 0xf5, 0xa1, 0xb2, - 0x93, 0x5b, 0xa3, 0xb5, 0x76, 0x46, 0xe3, 0x7c, 0xa8, 0x48, 0x75, 0x0e, 0x7d, 0x91, 0xa8, 0xcb, 0x88, 0x71, 0x93, - 0x2d, 0xbd, 0xdb, 0x2f, 0x4b, 0xe3, 0xbf, 0x96, 0xad, 0x32, 0xe2, 0xa9, 0xb9, 0x02, 0xba, 0xcb, 0x35, 0x5c, 0x56, - 0xc4, 0x64, 0x8a, 0x79, 0xb8, 0x6d, 0xe5, 0x6c, 0x16, 0x72, 0x69, 0x3e, 0x81, 0x33, 0xa0, 0x0a, 0xd7, 0x98, 0x05, - 0xd1, 0x5c, 0xb0, 0x00, 0xd6, 0x02, 0xa7, 0x97, 0x27, 0x73, 0x79, 0xd6, 0x31, 0x4a, 0x8c, 0x65, 0xed, 0x1f, 0x2f, - 0x2f, 0x0d, 0xfa, 0x49, 0x16, 0xf2, 0xcc, 0x77, 0xdc, 0xa9, 0xda, 0xa7, 0x78, 0x3a, 0xfa, 0xdd, 0x4f, 0xf9, 0x7b, - 0x0e, 0xdd, 0x76, 0x49, 0xb6, 0x6f, 0x9f, 0x3a, 0x14, 0xe0, 0x48, 0x17, 0xf2, 0x6b, 0x5b, 0xec, 0xb9, 0x5b, 0xf6, - 0x21, 0xa6, 0x85, 0xb0, 0x31, 0xf3, 0x68, 0x11, 0x70, 0x59, 0xde, 0xbb, 0x51, 0xeb, 0x79, 0x4b, 0x02, 0x2e, 0xf1, - 0x9e, 0x9f, 0xea, 0x68, 0x29, 0x6d, 0x47, 0xee, 0xb3, 0x83, 0x28, 0x67, 0x57, 0x5d, 0x3f, 0x31, 0x37, 0xfe, 0xdb, - 0x9d, 0x3d, 0x53, 0x6b, 0xb5, 0x20, 0x29, 0x97, 0x7e, 0x9e, 0x1f, 0x9a, 0x99, 0x4a, 0x26, 0xf0, 0xf0, 0x02, 0x12, - 0xbf, 0xf0, 0xd3, 0xbf, 0x1f, 0xa8, 0xb2, 0xae, 0x1c, 0x22, 0x3d, 0x03, 0x92, 0xe7, 0xe3, 0xc7, 0xd7, 0x85, 0xc7, - 0x3b, 0xa2, 0x4b, 0xbd, 0x9e, 0xe7, 0x16, 0x12, 0xe7, 0x49, 0x77, 0x4e, 0x5c, 0xad, 0x5c, 0xa0, 0x67, 0xa6, 0x21, - 0xe1, 0x5c, 0xf5, 0x8f, 0x0f, 0x81, 0x7f, 0x05, 0x0e, 0x24, 0xa9, 0xab, 0x0b, 0x15, 0x02, 0x9d, 0xd0, 0x7e, 0xde, - 0x12, 0x48, 0x78, 0xf7, 0x22, 0xd8, 0x62, 0x90, 0x7b, 0x2d, 0xa8, 0xa8, 0x2a, 0x54, 0x30, 0x6f, 0x44, 0x25, 0x78, - 0xe4, 0x1f, 0x30, 0x68, 0x9e, 0x98, 0x99, 0xe1, 0x3c, 0x82, 0x88, 0x24, 0x39, 0xb1, 0x45, 0x7c, 0x00, 0xa0, 0x8e, - 0x77, 0x82, 0xf1, 0x4a, 0xe2, 0x30, 0x42, 0x09, 0x2e, 0xbf, 0x17, 0xad, 0x47, 0x71, 0x67, 0x87, 0x83, 0x7f, 0x41, - 0x9a, 0xc7, 0xed, 0xde, 0x1f, 0x43, 0x7f, 0xf6, 0x01, 0xcd, 0xd0, 0xee, 0x04, 0xf4, 0x71, 0xb7, 0x26, 0xed, 0x7e, - 0x33, 0x3d, 0x13, 0x6d, 0xb7, 0x49, 0x62, 0x73, 0x20, 0x63, 0xde, 0x9e, 0x88, 0x0d, 0x6d, 0xfc, 0x01, 0x7e, 0x6b, - 0x6c, 0x56, 0x5d, 0x26, 0x9e, 0x59, 0x3d, 0x3c, 0x9e, 0x89, 0x27, 0x56, 0xab, 0x8d, 0xd8, 0xb1, 0xfa, 0x3f, 0xd4, - 0xf7, 0xf8, 0x96, 0x55, 0x78, 0x54, 0xfd, 0x17, 0xda, 0x81, 0x87, 0xb0, 0xb1, 0x36, 0x8f, 0x9e, 0x35, 0x6c, 0xf0, - 0x60, 0x75, 0x09, 0x3a, 0xf8, 0xf1, 0x57, 0x06, 0x8f, 0x88, 0xdd, 0x0f, 0x06, 0x2b, 0xab, 0x29, 0xb0, 0x3c, 0xde, - 0x1f, 0xdd, 0xff, 0x3f, 0x6f, 0x1a, 0x1e, 0xba, 0xd6, 0xd3, 0x1a, 0x2c, 0x2a, 0xa1, 0xc2, 0xfc, 0x7f, 0x56, 0x0f, - 0x62, 0xc6, 0x6a, 0x9d, 0x89, 0x29, 0xab, 0x3a, 0x13, 0x13, 0x56, 0xfb, 0xbc, 0x5e, 0x6f, 0x88, 0x1e, 0x2b, 0x5f, - 0x88, 0x31, 0xab, 0xe5, 0x9d, 0xe8, 0xb3, 0xaa, 0xb6, 0x7f, 0x03, 0x31, 0x60, 0xe5, 0x13, 0x31, 0x8c, 0x0c, 0x56, - 0x30, 0xfa, 0x1b, 0x2f, 0x77, 0x32, 0xb4, 0x5b, 0x3d, 0xb7, 0xc6, 0xff, 0x45, 0x27, 0xea, 0xd3, 0xe5, 0xc4, 0x95, - 0x67, 0x12, 0x70, 0xd1, 0xfe, 0x5b, 0x78, 0xbd, 0x09, 0x8f, 0x79, 0x60, 0xa4, 0x62, 0x69, 0x06, 0xc0, 0x38, 0x3f, - 0xfc, 0x4f, 0x77, 0x84, 0xb9, 0x91, 0x04, 0x46, 0x56, 0x29, 0x6b, 0xa3, 0xff, 0x97, 0xae, 0x20, 0x2a, 0x83, 0x6c, - 0xfb, 0xf0, 0xb6, 0x3a, 0x35, 0x7a, 0x0d, 0x6d, 0x18, 0xbd, 0xbc, 0xce, 0x59, 0x17, 0xd0, 0x51, 0x4b, 0x0a, 0xa1, - 0xeb, 0xba, 0x7b, 0x62, 0x7a, 0x5b, 0xbc, 0x23, 0x98, 0x11, 0xd4, 0x44, 0x04, 0x49, 0xd3, 0xfe, 0x4f, 0xce, 0xb6, - 0xe5, 0x58, 0x7b, 0xca, 0x62, 0xf9, 0xf0, 0x7d, 0x75, 0x75, 0x2a, 0x4c, 0x99, 0x09, 0x2e, 0xf3, 0xb0, 0xad, 0xde, - 0x53, 0x3d, 0x8c, 0xa5, 0xdb, 0xd3, 0xf0, 0x12, 0x31, 0x7c, 0x4b, 0xae, 0x5a, 0x10, 0xef, 0x09, 0xe6, 0xd8, 0x2d, - 0x81, 0x58, 0x29, 0x5c, 0x8d, 0xd7, 0x2d, 0x24, 0x8e, 0x19, 0xa9, 0xf1, 0x57, 0xeb, 0xfd, 0xdb, 0xcd, 0x14, 0xa7, - 0xc0, 0xa1, 0xe8, 0x36, 0xe0, 0x1d, 0x11, 0xe5, 0x94, 0x43, 0x96, 0x3c, 0xe2, 0x60, 0x87, 0x13, 0x07, 0x64, 0x99, - 0x26, 0xda, 0xac, 0x95, 0xd7, 0x04, 0xef, 0xea, 0xd1, 0x29, 0x93, 0x63, 0x6b, 0x03, 0xc7, 0x20, 0xf9, 0x17, 0xbc, - 0xea, 0x15, 0xa0, 0x0f, 0xd6, 0x54, 0x5d, 0xe8, 0xdb, 0x97, 0xf3, 0x3b, 0xd5, 0xa6, 0x5d, 0xe1, 0x95, 0xfd, 0x86, - 0xad, 0xce, 0xea, 0x1b, 0x41, 0x6a, 0xdd, 0x6d, 0xa4, 0x21, 0x40, 0xf7, 0x43, 0xbe, 0xbb, 0xa7, 0x23, 0x9c, 0x6c, - 0xed, 0x1c, 0x61, 0xa1, 0x99, 0x11, 0xfa, 0xdb, 0x08, 0xb8, 0x43, 0x01, 0x55, 0xdc, 0xda, 0x33, 0xa3, 0x48, 0x44, - 0xb4, 0x24, 0x15, 0xf1, 0x1e, 0x5c, 0xcf, 0xc6, 0xb0, 0x6c, 0xa4, 0x9d, 0xbd, 0xbe, 0xc9, 0xb6, 0x28, 0x82, 0xb0, - 0x75, 0xbd, 0x23, 0x0c, 0xe4, 0x2f, 0x03, 0xff, 0xd7, 0xea, 0xbd, 0xb4, 0x5a, 0xba, 0xa9, 0x7b, 0x7c, 0x0b, 0x7e, - 0xa8, 0x4b, 0xf9, 0x99, 0xa4, 0x98, 0x9d, 0x26, 0x3e, 0xf5, 0x49, 0x79, 0x8a, 0x97, 0x5d, 0x06, 0x40, 0xe4, 0x7a, - 0x4e, 0xc1, 0x87, 0xbc, 0xdb, 0x4c, 0xa9, 0x8b, 0xcc, 0x63, 0x82, 0x01, 0x66, 0x97, 0xd2, 0xc5, 0x3a, 0x35, 0x5c, - 0x6c, 0xa4, 0x1c, 0x6e, 0x3a, 0x9c, 0x36, 0xf4, 0xaa, 0x18, 0x17, 0x91, 0x1d, 0xdf, 0x35, 0x8d, 0x6f, 0x72, 0x23, - 0xf4, 0xde, 0xb9, 0xe1, 0xa9, 0xcf, 0x18, 0xf3, 0xb7, 0x82, 0x50, 0x19, 0x63, 0x3b, 0x9c, 0xad, 0x28, 0xd5, 0x4a, - 0x7e, 0x39, 0x6c, 0x73, 0xd8, 0xbf, 0xc9, 0xad, 0x6d, 0x0c, 0x18, 0xf1, 0x05, 0xe3, 0xbb, 0x10, 0xbf, 0x2f, 0x58, - 0x23, 0x7a, 0xc1, 0x25, 0xcb, 0x53, 0xb0, 0xf0, 0x50, 0x02, 0x53, 0xb6, 0x87, 0x26, 0xe0, 0xde, 0xe7, 0x26, 0x7e, - 0x3b, 0xe4, 0xe6, 0xd1, 0x87, 0x78, 0x2d, 0x94, 0x2a, 0x11, 0xf6, 0xad, 0x78, 0xd6, 0xa8, 0xe0, 0x99, 0x9b, 0x95, - 0xcc, 0x00, 0x28, 0x04, 0xba, 0xd5, 0x1c, 0xbe, 0xeb, 0x8f, 0x7b, 0x75, 0x80, 0xce, 0x6a, 0x5f, 0xce, 0x84, 0x8c, - 0x91, 0xe7, 0xf4, 0x20, 0xe8, 0xe9, 0xa3, 0x77, 0xbc, 0xbd, 0xac, 0x2a, 0x43, 0x8e, 0xc5, 0xc8, 0xa1, 0x99, 0x3c, - 0x2a, 0x4b, 0x1a, 0xb2, 0xe0, 0x72, 0x2a, 0xd7, 0xa4, 0x5a, 0xf5, 0x25, 0xe9, 0x7d, 0xcd, 0xd9, 0x0e, 0x68, 0xec, - 0x79, 0xbf, 0x85, 0xe0, 0x18, 0x84, 0x90, 0x30, 0x27, 0x36, 0xf7, 0xfe, 0xce, 0x00, 0xe7, 0xdb, 0x97, 0x50, 0x6f, - 0xbe, 0xf9, 0x81, 0x5b, 0xf4, 0x13, 0x8e, 0xa1, 0xb4, 0x38, 0xd5, 0x84, 0xab, 0xa3, 0x37, 0xb2, 0x36, 0x52, 0xd2, - 0x79, 0x1d, 0xbd, 0x1b, 0x1b, 0xc5, 0x78, 0xc7, 0x40, 0x54, 0x44, 0x79, 0xb8, 0x1f, 0x4b, 0xa2, 0x72, 0x73, 0x82, - 0x6b, 0x8a, 0x48, 0x44, 0xe1, 0x20, 0xdd, 0xc5, 0xd5, 0xf8, 0xc2, 0xa1, 0xc6, 0x34, 0x33, 0x07, 0x06, 0x48, 0xaa, - 0x43, 0x5d, 0x9b, 0xdd, 0x37, 0x02, 0xe1, 0x25, 0x17, 0xb0, 0x5c, 0x81, 0xcb, 0x43, 0xfe, 0x22, 0xf5, 0x5d, 0xcc, - 0x3b, 0x6d, 0x42, 0x24, 0xe7, 0xce, 0x80, 0x18, 0x38, 0xa4, 0x08, 0x25, 0xd3, 0x8d, 0x4b, 0x4e, 0xe2, 0xd6, 0x6c, - 0xce, 0x5c, 0x28, 0x19, 0x33, 0xf7, 0x88, 0xfe, 0x83, 0xf7, 0x36, 0x21, 0x5e, 0x70, 0x90, 0x45, 0x25, 0x3a, 0x1b, - 0x46, 0x3a, 0x91, 0xf0, 0x6b, 0x2c, 0xde, 0x60, 0x47, 0x96, 0x06, 0xd1, 0x4d, 0x91, 0x31, 0x84, 0x4d, 0x3b, 0xac, - 0x0e, 0xcd, 0x46, 0x49, 0x42, 0x0e, 0x08, 0xb5, 0xf3, 0x24, 0x27, 0x1d, 0xe1, 0xdd, 0xbb, 0xdc, 0xa6, 0xea, 0xc5, - 0xaf, 0x32, 0xd1, 0xa8, 0x36, 0x11, 0x2b, 0xbf, 0x9e, 0xc8, 0xca, 0xc2, 0x97, 0x02, 0x9a, 0x02, 0x25, 0x49, 0xfe, - 0xf6, 0xd7, 0xb4, 0xcc, 0xd3, 0x6b, 0x1d, 0x64, 0x30, 0x98, 0x7c, 0x25, 0x72, 0x8d, 0x1a, 0xd9, 0xe0, 0x3b, 0xe9, - 0x5c, 0xc6, 0x2a, 0xf7, 0x65, 0x88, 0x78, 0x9a, 0x9b, 0xfe, 0xa5, 0x0f, 0x2a, 0xd4, 0xea, 0x43, 0x23, 0xbb, 0x93, - 0xb6, 0x3e, 0x49, 0xd1, 0x48, 0x56, 0xec, 0xe2, 0x8b, 0x4b, 0x34, 0x5a, 0x32, 0x15, 0x4f, 0xca, 0x22, 0x63, 0x93, - 0x6b, 0xaf, 0x8d, 0x4d, 0xd4, 0x1d, 0x8c, 0x25, 0xb2, 0x34, 0x7c, 0x3b, 0x3d, 0xda, 0x10, 0xb7, 0xf7, 0x50, 0x57, - 0x37, 0x65, 0x1f, 0xde, 0xcd, 0xa8, 0x42, 0x73, 0xb3, 0x4a, 0x9d, 0x71, 0x70, 0x86, 0x5f, 0xaf, 0x50, 0x68, 0x4e, - 0x9f, 0x1a, 0x92, 0xe3, 0xec, 0x6b, 0x69, 0x00, 0xed, 0xab, 0xc9, 0x28, 0x16, 0x61, 0x21, 0xc4, 0x25, 0x1c, 0x80, - 0x5c, 0x3e, 0x4c, 0x97, 0x58, 0x6b, 0x95, 0x2b, 0xe9, 0x95, 0x48, 0x16, 0x28, 0xf1, 0x51, 0x9d, 0x5a, 0xa9, 0xac, - 0xe6, 0x4c, 0x62, 0x06, 0x0a, 0x98, 0xbc, 0x35, 0xbd, 0x91, 0x9e, 0xe5, 0x96, 0xb9, 0x4c, 0x70, 0xe6, 0x42, 0xb4, - 0xe6, 0x87, 0x6f, 0x72, 0x43, 0x56, 0x54, 0xd3, 0x88, 0x14, 0x3f, 0x38, 0x33, 0x10, 0x13, 0x20, 0x96, 0x31, 0xd7, - 0x27, 0x98, 0x60, 0x83, 0xf5, 0x66, 0x4d, 0xd7, 0xb0, 0xa1, 0xfc, 0x74, 0xff, 0xe4, 0x17, 0x63, 0x9e, 0x23, 0x80, - 0x95, 0x99, 0x78, 0x5e, 0xf2, 0xe9, 0x54, 0x29, 0x74, 0x89, 0x28, 0xce, 0x68, 0xd1, 0xd8, 0x99, 0x86, 0x65, 0x6c, - 0x23, 0x43, 0x00, 0xb4, 0x37, 0xf9, 0xf5, 0x65, 0x16, 0x25, 0xb5, 0x32, 0x21, 0xf2, 0x11, 0xd3, 0x8c, 0x3c, 0x39, - 0xd5, 0x21, 0x6b, 0x0d, 0x1e, 0x46, 0x95, 0x48, 0xfe, 0x78, 0x2a, 0xb9, 0x90, 0x44, 0xef, 0x61, 0x3b, 0x54, 0x59, - 0xb2, 0x60, 0xc5, 0x2a, 0x7a, 0xdf, 0xde, 0xee, 0xfa, 0x6b, 0x64, 0xc2, 0x5e, 0x00, 0xa2, 0xd7, 0x1e, 0x1c, 0xe3, - 0x95, 0xc9, 0x27, 0x57, 0x19, 0x2c, 0x28, 0x25, 0x42, 0x4d, 0x38, 0xda, 0x98, 0xcb, 0x32, 0x53, 0x70, 0xd5, 0x23, - 0xd9, 0xb2, 0x94, 0x39, 0xc9, 0xb0, 0xde, 0x06, 0x92, 0xf1, 0x11, 0xb5, 0xfc, 0xb5, 0x60, 0x6f, 0x1d, 0xd4, 0x29, - 0x04, 0x71, 0x92, 0xff, 0xe6, 0xf1, 0xba, 0xc5, 0xf7, 0xcb, 0x4f, 0x9b, 0x2c, 0x46, 0x92, 0xbd, 0x48, 0x53, 0xe9, - 0xbf, 0xd0, 0x2c, 0x0d, 0x0e, 0x4a, 0x4b, 0x6f, 0xcf, 0x05, 0x57, 0x7a, 0x21, 0x8a, 0x59, 0x00, 0x4f, 0x48, 0xa9, - 0x37, 0xba, 0x92, 0x68, 0x9d, 0x61, 0x75, 0x2c, 0xce, 0x6a, 0x11, 0x7a, 0x95, 0x4e, 0x88, 0xc5, 0x53, 0x23, 0xf2, - 0x9b, 0xac, 0x38, 0x47, 0xf7, 0xc6, 0xe3, 0x6b, 0x76, 0xbe, 0x2c, 0x43, 0x65, 0xea, 0x47, 0x88, 0xbe, 0x14, 0x1c, - 0x21, 0x36, 0x12, 0x75, 0x1b, 0x56, 0x8c, 0x10, 0x4c, 0x78, 0x75, 0x62, 0x96, 0x4b, 0xf4, 0xda, 0xfa, 0xe3, 0x7d, - 0xba, 0x67, 0xd5, 0x30, 0x7a, 0x65, 0x3e, 0xfe, 0x25, 0x91, 0xcd, 0x30, 0xfa, 0x13, 0xf8, 0x81, 0xc5, 0x9d, 0x9b, - 0xe9, 0x41, 0x38, 0x31, 0x4f, 0x4a, 0x2a, 0xb3, 0xf9, 0x83, 0xbd, 0xc3, 0x30, 0xba, 0xa0, 0xfb, 0xc1, 0x9b, 0x4e, - 0xad, 0x76, 0xbf, 0x21, 0xba, 0x8a, 0xa7, 0xdd, 0xfd, 0xaa, 0x3f, 0x98, 0x24, 0x0a, 0xd1, 0x8b, 0x06, 0x00, 0x3c, - 0xcd, 0x80, 0x67, 0x92, 0x62, 0x63, 0xf2, 0x66, 0x96, 0xae, 0x9c, 0x13, 0x5f, 0x50, 0xe7, 0xd9, 0x86, 0xac, 0xc9, - 0xbd, 0x24, 0xc8, 0x29, 0x55, 0x6e, 0x0d, 0xca, 0x18, 0x15, 0x55, 0x62, 0x78, 0x9a, 0x46, 0xb0, 0x01, 0x72, 0x4b, - 0x5b, 0x74, 0x3d, 0x23, 0x35, 0xa7, 0x73, 0x48, 0xd5, 0xca, 0x12, 0xdf, 0xa3, 0x3f, 0xca, 0xd0, 0x78, 0x48, 0xc4, - 0x56, 0x5d, 0xd3, 0xdc, 0xaf, 0xeb, 0x5d, 0x3a, 0x6e, 0xf8, 0x9d, 0x89, 0xc2, 0x6f, 0x5e, 0xe0, 0x3d, 0xb7, 0xd0, - 0xd1, 0x06, 0x37, 0x8e, 0xec, 0xe0, 0xcf, 0x60, 0x02, 0x63, 0x3f, 0x6f, 0x86, 0x83, 0xcb, 0x19, 0x9a, 0xaf, 0xa7, - 0xb9, 0xc7, 0xbd, 0x23, 0x6e, 0xd9, 0x5c, 0xe3, 0x7f, 0x73, 0x0b, 0x05, 0xe5, 0xfb, 0xcc, 0xb0, 0xa4, 0xd9, 0xde, - 0x8a, 0xa4, 0xf2, 0x35, 0x03, 0xd8, 0x85, 0x94, 0x9a, 0x0a, 0xb3, 0x69, 0x36, 0x99, 0x60, 0x00, 0x74, 0x91, 0xdf, - 0x5a, 0x2a, 0x88, 0x70, 0x89, 0xc6, 0x80, 0x1b, 0x40, 0x7d, 0x00, 0x86, 0x32, 0xe7, 0x10, 0x1e, 0x82, 0xaf, 0xb0, - 0x91, 0x18, 0xd9, 0x25, 0x18, 0xe3, 0x71, 0xdb, 0xc7, 0xaf, 0xc5, 0x5e, 0xd3, 0xec, 0x94, 0xef, 0x31, 0x36, 0xb1, - 0x79, 0x16, 0x1e, 0xd2, 0x07, 0x39, 0xf3, 0x9d, 0x19, 0x2b, 0xa2, 0x75, 0x7e, 0x2e, 0xec, 0x2f, 0x2d, 0x91, 0x60, - 0xd2, 0x52, 0xdb, 0x1b, 0x90, 0xf2, 0x89, 0x80, 0xa5, 0x34, 0x2f, 0x42, 0x5b, 0x35, 0xc8, 0x03, 0x25, 0xf4, 0x76, - 0x1a, 0xb5, 0xd7, 0x36, 0xeb, 0x85, 0x62, 0x5d, 0x70, 0xdc, 0x6a, 0x01, 0x43, 0x66, 0x38, 0x62, 0x03, 0xd6, 0x81, - 0x2b, 0x99, 0xa9, 0x66, 0xe2, 0x42, 0x9c, 0xd7, 0x99, 0xd1, 0x8c, 0x9f, 0xf3, 0xd4, 0x6e, 0xab, 0xcd, 0x95, 0x38, - 0x43, 0xff, 0xae, 0x5e, 0xbd, 0x99, 0xc6, 0xe8, 0x6e, 0x4c, 0x20, 0xdb, 0x4a, 0x1f, 0x0d, 0x7f, 0x3c, 0x9c, 0xa5, - 0x18, 0x06, 0xdc, 0x9f, 0x53, 0x15, 0xb4, 0xbf, 0x40, 0x79, 0x96, 0x97, 0x36, 0x76, 0x88, 0xd4, 0x64, 0xa0, 0x54, - 0x79, 0xbe, 0x97, 0x6c, 0x95, 0x48, 0xd0, 0x20, 0xb9, 0x91, 0x27, 0xcf, 0xe1, 0x0b, 0x32, 0xe2, 0x8f, 0xc6, 0xef, - 0x2f, 0x20, 0xc2, 0xce, 0x30, 0x93, 0x07, 0x06, 0x33, 0x77, 0x67, 0x03, 0x4f, 0x92, 0xd6, 0x9f, 0x8e, 0x12, 0xcd, - 0x97, 0x9b, 0xbb, 0x92, 0x4c, 0x71, 0x42, 0xf3, 0x93, 0x0f, 0x33, 0x54, 0x14, 0x97, 0x7f, 0xe0, 0x7a, 0xd6, 0x9e, - 0xa4, 0xc0, 0xf5, 0x7e, 0x08, 0x59, 0x11, 0xa9, 0xa8, 0x05, 0xf5, 0xd0, 0x8c, 0xc6, 0xf2, 0x43, 0x73, 0xfb, 0x45, - 0xaf, 0x08, 0x6c, 0xe2, 0x51, 0x8d, 0x9f, 0xf4, 0x5f, 0x3f, 0x30, 0x20, 0xe6, 0xbf, 0x4b, 0x62, 0x45, 0x55, 0xe3, - 0xdc, 0x65, 0x89, 0xaf, 0x60, 0xd5, 0x9f, 0x87, 0x44, 0x11, 0x64, 0xb7, 0x52, 0x84, 0x10, 0x9a, 0x2a, 0x62, 0xda, - 0x03, 0x38, 0xbd, 0x41, 0xdf, 0x51, 0x84, 0x85, 0x0a, 0x37, 0xa6, 0x9f, 0x90, 0x9a, 0x04, 0xa3, 0xd3, 0xd1, 0x40, - 0xe5, 0x80, 0xa4, 0x6f, 0x77, 0xbe, 0xbd, 0x47, 0x26, 0x59, 0xab, 0x5b, 0x99, 0xa4, 0x00, 0x81, 0x36, 0x7c, 0xc8, - 0xed, 0xed, 0x79, 0x9e, 0xe7, 0x2a, 0xab, 0xd7, 0xf1, 0x67, 0x1b, 0x0e, 0x13, 0xdb, 0xb1, 0x35, 0x3c, 0x78, 0xa2, - 0x85, 0x74, 0xfc, 0x45, 0x53, 0x34, 0xe8, 0x1a, 0xf1, 0x11, 0x05, 0xfa, 0x9c, 0x5d, 0x48, 0x1a, 0x37, 0xef, 0xca, - 0x75, 0xe0, 0x32, 0xb8, 0x29, 0x19, 0x1c, 0xd8, 0x9d, 0x0a, 0x99, 0x39, 0x35, 0x17, 0x54, 0x1e, 0x0f, 0xa4, 0xfe, - 0x90, 0xca, 0x8d, 0x59, 0xaa, 0x10, 0x1b, 0x54, 0xa7, 0x06, 0x7c, 0xd9, 0x13, 0x41, 0xcb, 0x13, 0xc8, 0xde, 0xab, - 0x21, 0xc5, 0x98, 0xe4, 0x72, 0x97, 0x03, 0xb6, 0xa1, 0x03, 0xd5, 0xa0, 0xe9, 0x18, 0x40, 0xb8, 0xbb, 0xf8, 0x96, - 0xf4, 0xbf, 0x7d, 0x9c, 0x7e, 0xaa, 0xee, 0x3c, 0x02, 0x4d, 0xb2, 0x56, 0x74, 0xbf, 0xd4, 0xa2, 0x21, 0x48, 0x78, - 0x1b, 0x1e, 0x22, 0xfe, 0xf4, 0x77, 0xe4, 0xd2, 0xc0, 0x5a, 0xd7, 0xa1, 0xff, 0x8e, 0x6c, 0xa6, 0x50, 0xa6, 0x15, - 0x52, 0xea, 0x54, 0x2d, 0x9c, 0x3b, 0x45, 0x59, 0x1a, 0x54, 0x3f, 0x48, 0x65, 0x85, 0x03, 0x09, 0x89, 0x44, 0x45, - 0x19, 0x26, 0x15, 0xaf, 0x69, 0x7d, 0x9f, 0x62, 0x13, 0x9e, 0xdd, 0xad, 0xfc, 0x90, 0x39, 0x88, 0x97, 0xc2, 0x1f, - 0x0f, 0xc6, 0xd7, 0x48, 0x6b, 0xa8, 0x67, 0x87, 0x87, 0x23, 0xcc, 0x51, 0xc4, 0xfa, 0x14, 0x65, 0xa8, 0x04, 0x72, - 0x29, 0xc5, 0x13, 0x86, 0x97, 0x98, 0xa8, 0xe8, 0x1c, 0x72, 0xd0, 0xe6, 0x7c, 0x80, 0x85, 0x87, 0x5e, 0xf0, 0xaf, - 0xe8, 0x21, 0x77, 0xaf, 0x8c, 0x88, 0xa6, 0x32, 0xa5, 0xdb, 0x3a, 0xe5, 0x1e, 0x3a, 0x5b, 0x04, 0xbd, 0x7d, 0xcf, - 0x3f, 0x7d, 0xa7, 0xef, 0xd4, 0xcf, 0x3e, 0xe5, 0x63, 0x7d, 0xca, 0xbf, 0xee, 0xfe, 0x63, 0xdb, 0x21, 0xff, 0x78, - 0xc9, 0xa6, 0x6d, 0x58, 0xd3, 0x6e, 0x4a, 0x54, 0xba, 0x4f, 0x14, 0x66, 0xe2, 0xa5, 0x18, 0xff, 0xb6, 0x28, 0x6b, - 0x7d, 0xb9, 0xb0, 0x82, 0x74, 0x32, 0x9b, 0xf0, 0xf5, 0xaf, 0x0b, 0x47, 0x08, 0x2d, 0x02, 0x3b, 0x49, 0xe9, 0x7c, - 0x92, 0xb5, 0x05, 0x34, 0x97, 0xa4, 0xb3, 0x84, 0x59, 0xc2, 0xd6, 0xf9, 0x04, 0xf4, 0x40, 0xb3, 0xa9, 0x5e, 0xe0, - 0x3a, 0x72, 0x0c, 0xc5, 0xf1, 0x6a, 0xe7, 0xa3, 0xdf, 0xde, 0x8a, 0x6f, 0x31, 0xd8, 0x85, 0xa5, 0x16, 0xd4, 0x8c, - 0x9a, 0x55, 0x0b, 0x38, 0x83, 0xb3, 0x78, 0x16, 0x14, 0xe8, 0xe7, 0x82, 0x21, 0xb8, 0x80, 0xd6, 0x06, 0xfa, 0x60, - 0xda, 0x78, 0x84, 0x45, 0x59, 0xe4, 0x1d, 0xf5, 0xe2, 0x66, 0x5d, 0xf5, 0xde, 0xdf, 0x56, 0x8c, 0x4a, 0xbc, 0xe5, - 0x7f, 0x05, 0x55, 0x22, 0xe9, 0xae, 0x90, 0xc8, 0xbb, 0x2a, 0x3e, 0x5e, 0x9b, 0xd6, 0x07, 0xb1, 0xac, 0xda, 0xb2, - 0xfe, 0x8e, 0xb0, 0x33, 0xe1, 0x71, 0xe8, 0x9e, 0xaa, 0x50, 0x10, 0xd3, 0x38, 0x77, 0xc9, 0xf7, 0x43, 0xbe, 0xea, - 0x7c, 0x37, 0xfc, 0x5b, 0x54, 0xcb, 0x5c, 0xcf, 0x24, 0x02, 0xa2, 0x9c, 0x74, 0xc1, 0xca, 0x61, 0x78, 0xe2, 0x9e, - 0xe6, 0x75, 0x91, 0x9c, 0x17, 0xd4, 0x33, 0x2c, 0x6e, 0xdf, 0xcb, 0x5f, 0x84, 0x6d, 0x8a, 0xca, 0x5f, 0xd8, 0x8c, - 0x3d, 0x1c, 0x51, 0x4d, 0xb7, 0x4f, 0xa8, 0xa0, 0x55, 0xa5, 0x1c, 0x4f, 0xbb, 0xf0, 0x22, 0x72, 0xb6, 0x37, 0xb0, - 0x53, 0xe6, 0xb6, 0x66, 0xdb, 0x1d, 0xe9, 0xef, 0x62, 0x15, 0x45, 0xec, 0x8a, 0xc3, 0x3e, 0xad, 0xa4, 0xeb, 0x8a, - 0x9a, 0xc3, 0x10, 0x8d, 0xe3, 0x0d, 0x14, 0xe5, 0xdb, 0x00, 0x1b, 0x1d, 0x20, 0xf4, 0xdb, 0x06, 0x4f, 0x80, 0x39, - 0xcc, 0xac, 0x36, 0x2e, 0x5e, 0xcd, 0x96, 0x4a, 0xee, 0xa9, 0xea, 0xfd, 0x5b, 0x0e, 0x8c, 0xbd, 0xcd, 0xfe, 0x29, - 0x46, 0x1f, 0xf1, 0xd9, 0xfb, 0xdb, 0x4f, 0x18, 0x82, 0x3c, 0x73, 0xe5, 0x7d, 0xdd, 0x83, 0x98, 0x94, 0xd9, 0x2a, - 0x35, 0x6d, 0x2b, 0x5c, 0x32, 0x08, 0xaf, 0x1d, 0x6d, 0x58, 0xa2, 0x4e, 0x61, 0x7f, 0xad, 0x92, 0xd5, 0x4d, 0x80, - 0xcf, 0x93, 0x5a, 0x62, 0x4e, 0x95, 0xc7, 0xfe, 0xea, 0xc5, 0x49, 0x26, 0x7f, 0x62, 0x02, 0x75, 0x06, 0x97, 0xb0, - 0x29, 0xcb, 0x1f, 0xc4, 0xfe, 0xdd, 0xec, 0x6f, 0x97, 0x46, 0xfe, 0xe6, 0x28, 0xb1, 0x08, 0x29, 0xac, 0x60, 0x5c, - 0xca, 0xd7, 0x4b, 0x3a, 0x80, 0x46, 0x36, 0x29, 0x46, 0x2f, 0x68, 0x5f, 0x7e, 0xee, 0xbe, 0xc2, 0xdc, 0x53, 0x32, - 0x76, 0x71, 0x30, 0xcb, 0xb5, 0x45, 0xe1, 0x48, 0x83, 0x65, 0xf0, 0xa2, 0xb7, 0x3a, 0xc2, 0x47, 0xe6, 0x88, 0x8f, - 0xcf, 0xfb, 0xe5, 0x82, 0x68, 0x51, 0x9a, 0x3f, 0x0e, 0x9e, 0x06, 0x74, 0x5c, 0x6a, 0xdb, 0xf4, 0x1e, 0x39, 0x75, - 0x40, 0xe8, 0x1a, 0x9b, 0x4c, 0x3f, 0x56, 0x28, 0x25, 0x35, 0x4b, 0xdb, 0xe9, 0xb1, 0xb1, 0x53, 0x53, 0xa2, 0xf8, - 0xae, 0xef, 0xba, 0x3b, 0x45, 0xb5, 0xae, 0x9f, 0x72, 0x44, 0x3e, 0xea, 0x82, 0x78, 0x35, 0x72, 0x6d, 0x87, 0x5c, - 0x7d, 0xd9, 0xa9, 0xea, 0x41, 0x5d, 0xec, 0x7f, 0xc8, 0x95, 0x9c, 0x66, 0xe3, 0x5b, 0x6f, 0xd0, 0xea, 0x26, 0x0d, - 0x3d, 0xe4, 0xc0, 0x02, 0x87, 0x14, 0xe1, 0x46, 0x0c, 0x6d, 0x6b, 0x24, 0x78, 0xac, 0x98, 0xc2, 0x83, 0xb8, 0x3f, - 0x8e, 0x4c, 0x80, 0xaa, 0xe8, 0x45, 0xa8, 0x8d, 0x6d, 0x0e, 0x3d, 0x03, 0x5c, 0x0f, 0xe9, 0xaf, 0x82, 0x9c, 0xef, - 0xe0, 0x6e, 0x30, 0x5a, 0x67, 0xcf, 0x8b, 0xf2, 0x81, 0x6a, 0x5c, 0x6f, 0xdb, 0xe1, 0x90, 0x5d, 0x63, 0xb7, 0x8b, - 0xa4, 0x76, 0x59, 0xe8, 0x33, 0x5b, 0x83, 0x91, 0x62, 0x6c, 0xbd, 0x05, 0xbe, 0xd9, 0x96, 0x41, 0x65, 0xd7, 0x7e, - 0x23, 0x29, 0xa1, 0xd1, 0xc5, 0xd0, 0x60, 0xbc, 0x81, 0x40, 0x55, 0xb0, 0x3c, 0x8b, 0x69, 0x2b, 0x61, 0x34, 0x1a, - 0xf7, 0xb4, 0x9f, 0x46, 0xf5, 0xb1, 0xfc, 0x91, 0x6e, 0xa6, 0xdc, 0x48, 0x97, 0x1f, 0xa6, 0xcb, 0x3d, 0x04, 0x53, - 0x61, 0xf9, 0x52, 0xad, 0x24, 0x02, 0x6e, 0xb9, 0x82, 0xd2, 0x60, 0x7f, 0x3f, 0xaf, 0xc0, 0xcc, 0x4b, 0x9e, 0x63, - 0x68, 0x78, 0xb1, 0x51, 0xb1, 0x71, 0xe2, 0xa7, 0xbb, 0x44, 0xcb, 0xa9, 0x19, 0x3c, 0x45, 0x3c, 0x20, 0xd5, 0x6d, - 0x0b, 0x5e, 0x1e, 0x3c, 0x46, 0x23, 0x4b, 0x57, 0xca, 0x2e, 0x93, 0x67, 0xf5, 0x50, 0x8e, 0x2a, 0x71, 0xd0, 0x4b, - 0xa4, 0x51, 0x57, 0xde, 0xfa, 0xc6, 0x4b, 0x60, 0x95, 0xb4, 0x4c, 0x4e, 0xbf, 0xef, 0x88, 0x34, 0x48, 0xb8, 0x94, - 0x42, 0xf1, 0x57, 0x89, 0x90, 0x7a, 0x6f, 0x0d, 0x1d, 0xc3, 0xc0, 0xfd, 0x75, 0x3e, 0xe2, 0xac, 0xf8, 0xec, 0x17, - 0x07, 0xd0, 0xa5, 0x2a, 0x1b, 0xa4, 0x5d, 0xac, 0xdc, 0x99, 0xef, 0xf7, 0xe8, 0x6d, 0x95, 0x62, 0xf1, 0x2d, 0xa3, - 0x9f, 0x58, 0xbc, 0x15, 0x32, 0xd8, 0x3d, 0x3f, 0xc0, 0x83, 0x1d, 0x9a, 0x48, 0x5d, 0x25, 0x04, 0x30, 0x41, 0x27, - 0xbd, 0x9c, 0xbc, 0x42, 0x14, 0xa1, 0x05, 0xee, 0xc9, 0xa1, 0x8d, 0x4a, 0x61, 0xbe, 0x82, 0xf0, 0x8f, 0x72, 0xf9, - 0x1e, 0x03, 0xd3, 0xe0, 0x12, 0x0d, 0xe5, 0x03, 0x22, 0xd2, 0x93, 0x91, 0x14, 0x81, 0x17, 0xf2, 0x3e, 0x11, 0x4c, - 0x5c, 0xa3, 0x75, 0x13, 0xbc, 0xa7, 0xc5, 0xd1, 0x4d, 0xf3, 0xd4, 0xc2, 0x8c, 0xf8, 0x19, 0x13, 0x46, 0xe1, 0x32, - 0xc4, 0x77, 0x16, 0x14, 0x9e, 0x60, 0xa7, 0x1a, 0x54, 0xaf, 0x8f, 0xda, 0xf4, 0x62, 0x37, 0xf8, 0x2b, 0x37, 0x1f, - 0xcf, 0x45, 0x3a, 0xf2, 0x42, 0x5c, 0xf2, 0xdc, 0xf9, 0x01, 0x9a, 0x10, 0x9e, 0xbb, 0x61, 0x77, 0x89, 0x0e, 0xac, - 0x93, 0xc9, 0x86, 0x15, 0x4d, 0xdc, 0x74, 0x02, 0x0c, 0xf2, 0xdc, 0x39, 0xb4, 0x6a, 0xe2, 0xe9, 0x3f, 0x55, 0xb9, - 0x5d, 0xf2, 0xbc, 0xc3, 0xee, 0x9a, 0xda, 0x35, 0x36, 0x06, 0x22, 0xe2, 0x62, 0x34, 0xc7, 0xd2, 0x4b, 0xfc, 0x1c, - 0xee, 0xdc, 0x7b, 0x5c, 0x3f, 0xc5, 0x18, 0x20, 0x1f, 0xde, 0x42, 0xb6, 0x80, 0x9e, 0xc5, 0x79, 0x9f, 0xa1, 0x17, - 0xde, 0xed, 0x25, 0x66, 0x44, 0x72, 0x7f, 0xa6, 0xf5, 0x91, 0x28, 0x47, 0x7a, 0x09, 0x29, 0x4e, 0x70, 0x94, 0xec, - 0x44, 0xc0, 0xfe, 0xbf, 0x10, 0x6d, 0x27, 0x88, 0xf2, 0x6d, 0xc2, 0xcd, 0xdd, 0xed, 0x58, 0xb9, 0x7d, 0xcb, 0x13, - 0x42, 0xa9, 0xf8, 0x84, 0x71, 0x88, 0x69, 0x27, 0x13, 0xbb, 0x23, 0x43, 0x44, 0x0f, 0x4b, 0x70, 0x1d, 0xb8, 0x19, - 0x7e, 0x74, 0xf6, 0x36, 0x8e, 0xe4, 0xe2, 0x73, 0xf5, 0xf3, 0x67, 0xdb, 0x60, 0x71, 0xed, 0xf6, 0xf2, 0x02, 0xc8, - 0x44, 0x3e, 0xea, 0x48, 0xbc, 0xa2, 0x01, 0x1a, 0xa7, 0xd7, 0x80, 0x11, 0xa7, 0x2c, 0x7d, 0x41, 0x87, 0x89, 0xca, - 0x23, 0xf5, 0xa0, 0x11, 0x3f, 0xc2, 0x90, 0xed, 0xb2, 0xac, 0x89, 0xb4, 0x30, 0xda, 0xb7, 0x40, 0xe1, 0x04, 0x58, - 0xb9, 0x0d, 0x0b, 0xf4, 0x6b, 0x21, 0x23, 0xaf, 0x81, 0x86, 0xfa, 0x7c, 0xf3, 0xda, 0xdf, 0x4f, 0xf4, 0x4f, 0x8b, - 0xe6, 0x90, 0x96, 0xd4, 0x23, 0xbf, 0x0f, 0xb6, 0xc7, 0xd6, 0xe2, 0xe7, 0x9d, 0xaf, 0x32, 0xa6, 0x25, 0x18, 0x91, - 0x77, 0x63, 0x08, 0xf9, 0x20, 0xc7, 0x2a, 0x08, 0x25, 0x5f, 0xab, 0x5a, 0x3b, 0xc4, 0x7a, 0xca, 0xdb, 0x14, 0x79, - 0xdb, 0x7c, 0x54, 0x51, 0x58, 0xad, 0xc0, 0xfe, 0xaa, 0xa1, 0xac, 0xc4, 0x0b, 0xfd, 0x57, 0x42, 0xa2, 0x0a, 0x89, - 0x45, 0x07, 0x3d, 0x12, 0xce, 0x3f, 0x08, 0x51, 0xd0, 0xe5, 0x96, 0x6a, 0xd9, 0x6e, 0x5f, 0x1a, 0x0a, 0x57, 0x81, - 0x58, 0x60, 0xb7, 0xf1, 0xbc, 0xad, 0xe3, 0x45, 0x1c, 0x97, 0x99, 0xb5, 0x6f, 0xbc, 0xe2, 0x2b, 0xec, 0x05, 0x81, - 0xfd, 0x1a, 0xce, 0x9a, 0xfc, 0xdf, 0xcf, 0xf0, 0x9a, 0x99, 0xc5, 0xcd, 0xc5, 0xf0, 0x6f, 0x67, 0xb3, 0xfc, 0x62, - 0x78, 0xb3, 0xd9, 0x21, 0xb5, 0x98, 0xd3, 0x68, 0xda, 0x7c, 0x78, 0xfd, 0xf0, 0xf2, 0x20, 0x9d, 0xde, 0xf3, 0x93, - 0xbc, 0x75, 0x76, 0x71, 0x0c, 0x59, 0xf0, 0x09, 0x3f, 0x6b, 0xb0, 0x39, 0xfb, 0xaf, 0xad, 0xd0, 0x1e, 0xd7, 0xde, - 0x33, 0xbb, 0x12, 0xab, 0xd3, 0xd8, 0xeb, 0xed, 0xbe, 0x9a, 0x02, 0xfe, 0x13, 0x81, 0x26, 0xbe, 0xf2, 0xc9, 0xa4, - 0x14, 0x07, 0x40, 0xa0, 0xba, 0x35, 0xf8, 0x7b, 0x18, 0x8c, 0x32, 0x92, 0xf1, 0xf6, 0x13, 0x92, 0xc8, 0xc6, 0xe1, - 0xe9, 0xdc, 0x42, 0xb1, 0x1e, 0xe9, 0xfb, 0x3c, 0xcd, 0xb4, 0x7e, 0x5b, 0x46, 0xd5, 0xa9, 0x81, 0x2c, 0x68, 0x9c, - 0x69, 0x10, 0xac, 0x7f, 0xdb, 0x58, 0x9d, 0x59, 0xf8, 0x66, 0x21, 0xa2, 0x59, 0x16, 0xff, 0x78, 0x4e, 0xb6, 0xd0, - 0xa0, 0xc0, 0x8f, 0x9a, 0x66, 0xde, 0xb5, 0x6e, 0x75, 0x01, 0x21, 0xef, 0x96, 0xa7, 0xf5, 0xa5, 0x3f, 0xff, 0x82, - 0x35, 0xbb, 0xf1, 0x5f, 0x5d, 0x8f, 0xd6, 0x8b, 0x10, 0x25, 0x5b, 0x81, 0x00, 0x71, 0xb1, 0x8d, 0xe3, 0x2d, 0x79, - 0xe3, 0x34, 0x17, 0xc9, 0x3c, 0x7c, 0x75, 0x92, 0x66, 0x05, 0xa1, 0x9a, 0xdf, 0x26, 0xf1, 0x0a, 0xd4, 0x59, 0x89, - 0x8f, 0x8a, 0x77, 0xe3, 0xde, 0xf5, 0xc4, 0xf6, 0xbf, 0xf2, 0x25, 0x14, 0xc4, 0xf7, 0xfb, 0x16, 0xe8, 0x86, 0x9f, - 0x30, 0xc5, 0xdb, 0x8f, 0xd7, 0xe3, 0x37, 0xf9, 0xad, 0xc0, 0x3d, 0x16, 0x78, 0x67, 0xbd, 0x34, 0x97, 0xf2, 0x24, - 0x41, 0x66, 0x05, 0xae, 0xb8, 0xec, 0x07, 0x0f, 0x96, 0x2d, 0x4d, 0x80, 0x66, 0xab, 0xc8, 0x00, 0x19, 0xca, 0x25, - 0x08, 0x69, 0x83, 0x8c, 0xfe, 0x2d, 0x88, 0xa2, 0x24, 0xc7, 0xa7, 0xb3, 0x27, 0xd1, 0x0d, 0x95, 0x3e, 0x39, 0x32, - 0xb0, 0xb2, 0x0e, 0x50, 0x4b, 0x32, 0x54, 0x88, 0x84, 0x90, 0x64, 0x02, 0x60, 0x9f, 0x14, 0x1a, 0x0a, 0x9f, 0x6b, - 0x39, 0xed, 0xfc, 0xc2, 0xb7, 0x4c, 0x90, 0x78, 0x4c, 0x8e, 0x5a, 0xc9, 0x84, 0xb6, 0x7b, 0xad, 0xf9, 0xe8, 0xee, - 0xe2, 0xa9, 0x5d, 0x1c, 0x63, 0x3e, 0xf2, 0x3f, 0x4a, 0x73, 0x22, 0xf2, 0xb5, 0x0e, 0xc0, 0x4a, 0xde, 0x0a, 0x94, - 0x2d, 0x6f, 0x98, 0x17, 0x58, 0xfc, 0x56, 0x4b, 0x76, 0xf5, 0x0c, 0x02, 0xb4, 0x21, 0x9c, 0xb4, 0xe3, 0x5c, 0xe1, - 0xba, 0x99, 0x62, 0x0d, 0xe5, 0xf5, 0x0a, 0x47, 0x15, 0x9a, 0x26, 0xc6, 0x74, 0x73, 0x2d, 0x7a, 0x31, 0xf5, 0x9a, - 0x7a, 0x42, 0x92, 0xbc, 0x08, 0x67, 0xd5, 0x9e, 0xae, 0xfd, 0x67, 0x06, 0x48, 0x3d, 0x27, 0x17, 0xca, 0x36, 0x17, - 0xe3, 0xbb, 0x79, 0xe3, 0x59, 0xc6, 0xcd, 0xbd, 0x57, 0x6b, 0xaf, 0x9e, 0x67, 0xb7, 0x98, 0x8f, 0x25, 0xe4, 0xd3, - 0x0e, 0x31, 0x37, 0xb2, 0x50, 0x72, 0x84, 0x71, 0xd7, 0x86, 0x21, 0x13, 0x37, 0x2e, 0x2c, 0x98, 0x90, 0x1e, 0x1b, - 0x89, 0x71, 0x90, 0x35, 0xdf, 0xd5, 0x7e, 0x71, 0x7c, 0xfb, 0xa3, 0xb8, 0x48, 0x0b, 0xf6, 0xf0, 0xa4, 0xb3, 0x5f, - 0xf9, 0x4c, 0xa3, 0x6e, 0x23, 0x47, 0x02, 0x51, 0x0c, 0x44, 0x02, 0xba, 0x56, 0xa9, 0xd0, 0xcb, 0x8a, 0x79, 0xa8, - 0xc0, 0x67, 0x2d, 0xb4, 0x51, 0xc1, 0xaa, 0x57, 0xf8, 0x89, 0x08, 0x65, 0xa6, 0xd6, 0x6b, 0x80, 0x5c, 0x64, 0x6b, - 0xad, 0x9f, 0xf5, 0x76, 0x6d, 0xb8, 0x9c, 0x27, 0xf0, 0x57, 0xef, 0xe2, 0xd6, 0xc7, 0x04, 0x5d, 0x6e, 0xed, 0x9f, - 0x68, 0xcf, 0x32, 0x46, 0x12, 0x55, 0x9e, 0xc3, 0xd3, 0x0a, 0xe4, 0xeb, 0x8e, 0x34, 0x5e, 0x62, 0x43, 0xf6, 0x16, - 0xa5, 0x9f, 0x52, 0xc6, 0xbd, 0xfc, 0xa4, 0xd8, 0x03, 0xf6, 0xdc, 0x03, 0x82, 0x35, 0xfb, 0x5a, 0x5f, 0x6e, 0x4d, - 0xd3, 0x60, 0xff, 0xa3, 0x03, 0x4f, 0x40, 0xd9, 0xbe, 0x1c, 0x47, 0x18, 0x8f, 0xdc, 0x92, 0x31, 0x65, 0x08, 0x39, - 0xc1, 0x62, 0xbb, 0xd7, 0x1d, 0x6b, 0x68, 0x39, 0x95, 0x28, 0x16, 0x49, 0xee, 0x7e, 0x94, 0xce, 0x68, 0xff, 0xd4, - 0x4e, 0x25, 0xb4, 0xf5, 0xb7, 0x9a, 0x9d, 0x14, 0x4d, 0xd7, 0xf5, 0x0e, 0xe8, 0x3c, 0x4a, 0x94, 0x4f, 0x2c, 0x8f, - 0x5d, 0x7e, 0x79, 0xb7, 0x6b, 0x64, 0xbb, 0x29, 0xb7, 0xb5, 0x37, 0x1a, 0xa9, 0x18, 0x69, 0xe8, 0x81, 0xed, 0x70, - 0xd9, 0x29, 0x6d, 0x02, 0x22, 0x64, 0x54, 0x2f, 0x8e, 0xa4, 0x96, 0xfa, 0x42, 0x9c, 0x75, 0xe7, 0x23, 0xad, 0x68, - 0x2f, 0x82, 0x02, 0x10, 0x5a, 0x1d, 0xa9, 0x92, 0x35, 0x5c, 0x97, 0x11, 0x6c, 0x0f, 0xe3, 0xf5, 0x0d, 0x14, 0x55, - 0x79, 0x7a, 0x2d, 0xd8, 0x8a, 0x1f, 0xc7, 0xa6, 0xf0, 0xb0, 0x8b, 0x0c, 0xf6, 0xe0, 0x66, 0x8e, 0x13, 0x3c, 0x5d, - 0x9b, 0xd3, 0x92, 0x3d, 0xd9, 0x20, 0xb3, 0xe6, 0xeb, 0xdb, 0x21, 0x5e, 0xb8, 0x71, 0x3d, 0x2c, 0xd9, 0x25, 0x1c, - 0x5c, 0xfa, 0x04, 0x3e, 0xf8, 0xb5, 0x1d, 0xb3, 0x41, 0xa1, 0xab, 0xcb, 0x09, 0xf6, 0x92, 0xc6, 0xe7, 0xf9, 0x6f, - 0x66, 0x73, 0x51, 0xb2, 0x02, 0xac, 0xbd, 0xba, 0x6f, 0x11, 0x2e, 0x73, 0x07, 0x5f, 0xfe, 0x3f, 0x75, 0xd4, 0x76, - 0xf3, 0xf9, 0x2d, 0x9c, 0x9b, 0x1c, 0x3c, 0xc3, 0x21, 0xea, 0xf2, 0x40, 0xae, 0x5c, 0xbf, 0xfa, 0x4f, 0xa0, 0xe4, - 0x9d, 0x4e, 0x9a, 0x7f, 0xdd, 0x77, 0x61, 0x31, 0x36, 0x76, 0xd9, 0x3e, 0xe2, 0x84, 0xd1, 0x75, 0xa2, 0xd0, 0x7e, - 0xcf, 0xc4, 0xb6, 0x1a, 0x64, 0x04, 0x8b, 0x90, 0xd7, 0x77, 0xb6, 0x00, 0xf4, 0xfd, 0x65, 0x0f, 0x8b, 0xb7, 0x7e, - 0x45, 0xe2, 0x4d, 0xb1, 0xf0, 0xef, 0x47, 0x64, 0xe1, 0xda, 0xfe, 0x8a, 0x03, 0x04, 0xdb, 0x5a, 0xfb, 0x5a, 0xdc, - 0x72, 0xfb, 0xe8, 0x04, 0x20, 0x28, 0x61, 0x2d, 0x9e, 0x44, 0xed, 0x5f, 0x22, 0x23, 0x52, 0xf7, 0x19, 0x33, 0x35, - 0x4c, 0xc4, 0x9d, 0x15, 0xc7, 0xb0, 0xd2, 0x7d, 0x74, 0xe5, 0x36, 0xb9, 0x1a, 0x44, 0xd1, 0x1e, 0x9a, 0x88, 0x3b, - 0x36, 0x04, 0x79, 0x4f, 0xc8, 0x63, 0x81, 0xaa, 0xac, 0xc2, 0x96, 0x47, 0x29, 0x82, 0xe1, 0x39, 0x74, 0x01, 0xb6, - 0x52, 0xd4, 0x3a, 0x73, 0xc9, 0xe2, 0xc4, 0x81, 0xd7, 0x8f, 0x07, 0xbc, 0x1a, 0xdd, 0xcd, 0x91, 0x40, 0xdc, 0x49, - 0xb2, 0x5b, 0x50, 0xf5, 0xe6, 0x65, 0xe8, 0x56, 0x3c, 0xaf, 0x14, 0x77, 0xa3, 0xad, 0xbe, 0x0d, 0x21, 0x22, 0x0e, - 0xd1, 0xae, 0x1d, 0xa5, 0x07, 0x74, 0x20, 0x8b, 0x2e, 0xaa, 0x9a, 0x94, 0xe0, 0xbf, 0x29, 0xb3, 0x36, 0xa1, 0x86, - 0x09, 0x05, 0x05, 0xe7, 0x6c, 0xdc, 0x4a, 0xf8, 0xf4, 0x46, 0xc6, 0xdc, 0x52, 0xe0, 0x55, 0x68, 0x6e, 0xaa, 0x03, - 0xb9, 0x22, 0x1a, 0x74, 0xc9, 0xb5, 0xed, 0x32, 0xc7, 0x87, 0x59, 0xbb, 0xf0, 0xfc, 0xb9, 0xd8, 0x2f, 0x95, 0x52, - 0xe4, 0xa5, 0xa0, 0x10, 0x71, 0x6a, 0xa3, 0x12, 0x9a, 0xfa, 0x00, 0xd6, 0x74, 0x44, 0x70, 0x67, 0x3c, 0x7a, 0x87, - 0x48, 0xf2, 0x3f, 0x95, 0x52, 0x67, 0x23, 0x79, 0x04, 0x4c, 0xeb, 0x81, 0x49, 0xfd, 0x92, 0x6d, 0x81, 0xe0, 0xf0, - 0x40, 0x7f, 0x0c, 0x14, 0xc9, 0x13, 0x89, 0x58, 0x50, 0xc7, 0xd3, 0xa8, 0x6f, 0xec, 0x4b, 0xb5, 0x27, 0xf7, 0x76, - 0x7c, 0xd6, 0x1e, 0x7b, 0x88, 0x24, 0x63, 0x7e, 0x10, 0x41, 0x12, 0x24, 0xc2, 0x18, 0x8b, 0x3c, 0xc4, 0x40, 0x34, - 0x3f, 0xb9, 0xc1, 0xe0, 0x54, 0x53, 0x6d, 0xfd, 0x58, 0xa2, 0x23, 0x10, 0xa8, 0xcb, 0x34, 0xaa, 0xd8, 0xaa, 0x4c, - 0x10, 0xcc, 0x67, 0xfd, 0xbb, 0x76, 0x44, 0x60, 0xa6, 0x1d, 0x03, 0xb4, 0xc9, 0x1b, 0xcc, 0x47, 0x44, 0x66, 0xcb, - 0xce, 0x27, 0x89, 0x09, 0xa6, 0xae, 0xda, 0x23, 0xb3, 0x71, 0xc6, 0xc9, 0xa6, 0xe9, 0xd4, 0xee, 0xaa, 0x32, 0xb8, - 0x08, 0x2f, 0x5e, 0xa2, 0x11, 0x80, 0xe8, 0x5a, 0xbe, 0x16, 0x9e, 0x1c, 0x08, 0x20, 0xcc, 0x2d, 0x0d, 0xfc, 0x96, - 0x69, 0xfb, 0x27, 0x5d, 0xab, 0x1b, 0x22, 0x36, 0x0a, 0x11, 0xfe, 0xd3, 0x04, 0xd9, 0xf5, 0x5b, 0xab, 0x1d, 0xfb, - 0xfb, 0x4e, 0x5b, 0xf6, 0x5b, 0x8b, 0x59, 0x4a, 0x43, 0x17, 0xc2, 0x86, 0x61, 0x6b, 0x35, 0x14, 0x56, 0xcb, 0x59, - 0x73, 0xe8, 0x80, 0xcc, 0xbc, 0x10, 0x90, 0x65, 0xee, 0x57, 0x3e, 0x42, 0x67, 0x07, 0xf6, 0x3f, 0xf2, 0x9d, 0xe4, - 0x1c, 0x1b, 0x76, 0x88, 0xaf, 0x77, 0xc1, 0xa2, 0x89, 0xac, 0x24, 0x94, 0x8f, 0xa0, 0x7c, 0xae, 0x5d, 0x5a, 0x47, - 0x6a, 0xe7, 0x46, 0x87, 0x46, 0x06, 0xfc, 0xc1, 0x98, 0x09, 0x1c, 0x88, 0x40, 0x2f, 0x05, 0xdd, 0x87, 0xef, 0x3b, - 0x26, 0xd3, 0x6f, 0x0d, 0x04, 0xff, 0xfe, 0x8d, 0xfb, 0x2d, 0x25, 0x60, 0x09, 0x88, 0x3b, 0x2d, 0xa0, 0x1b, 0xc4, - 0xfc, 0x7a, 0x69, 0x88, 0xc9, 0x8b, 0x43, 0x1b, 0xbd, 0x2c, 0x64, 0x78, 0xed, 0xc1, 0xc3, 0xe7, 0x99, 0xf7, 0xb2, - 0x53, 0x71, 0x86, 0x6b, 0xb3, 0x9b, 0x5e, 0xe2, 0xb6, 0xe3, 0xd7, 0x23, 0xbf, 0x45, 0xdc, 0xc0, 0xcd, 0x6e, 0x50, - 0xe6, 0x21, 0xcc, 0x3c, 0x0b, 0xdc, 0xa3, 0x61, 0x4a, 0x7f, 0xc3, 0x42, 0xac, 0x1b, 0xb2, 0xf5, 0x99, 0xc1, 0xea, - 0xb6, 0x8a, 0x41, 0x2c, 0x4f, 0x72, 0x3c, 0xc1, 0xc8, 0x42, 0xba, 0x61, 0x91, 0x93, 0x84, 0x37, 0x49, 0x1c, 0x71, - 0xaf, 0x8d, 0xd9, 0x56, 0x60, 0xea, 0x10, 0x1a, 0x72, 0x7b, 0xfc, 0xbe, 0x0b, 0x09, 0x66, 0x1e, 0x67, 0xa9, 0x8a, - 0x46, 0xea, 0xed, 0x5f, 0x3c, 0xb1, 0x47, 0xf5, 0x11, 0x6a, 0x7b, 0xcb, 0x92, 0xdb, 0xd5, 0xbf, 0xf7, 0xad, 0xd3, - 0x80, 0x5e, 0x30, 0x73, 0xc3, 0x70, 0xdc, 0x37, 0x36, 0x00, 0xd9, 0x48, 0x0a, 0x0c, 0x84, 0xd5, 0x08, 0x56, 0xd2, - 0x62, 0xec, 0xe9, 0x5d, 0xfd, 0xec, 0x18, 0x01, 0x2e, 0x81, 0xf5, 0x63, 0xa5, 0xb0, 0xe1, 0x14, 0xec, 0x7a, 0x03, - 0xf4, 0xdd, 0x76, 0x7b, 0x14, 0x4a, 0x93, 0x1b, 0x1a, 0x78, 0x9f, 0x0d, 0x04, 0x36, 0xfd, 0x14, 0xcf, 0xa1, 0x77, - 0x5d, 0xbf, 0xee, 0xfd, 0xbd, 0x31, 0x10, 0x21, 0xad, 0x91, 0xa0, 0xb5, 0xef, 0x7b, 0x2f, 0x69, 0x66, 0x65, 0x94, - 0xa1, 0x29, 0xdb, 0x54, 0xfb, 0x69, 0x38, 0xb6, 0x2d, 0x8b, 0x40, 0xed, 0x00, 0xaf, 0x5c, 0xe7, 0xe0, 0x3a, 0x53, - 0x14, 0xba, 0x12, 0x1f, 0x4a, 0x27, 0x78, 0xb7, 0x8d, 0x62, 0x12, 0x10, 0xe7, 0x07, 0x2b, 0xb8, 0x41, 0xc8, 0x59, - 0x23, 0x04, 0xd6, 0x66, 0xbb, 0xeb, 0x23, 0xd3, 0x95, 0xf8, 0xb5, 0x07, 0x59, 0x43, 0xa5, 0xa8, 0x14, 0xb8, 0xb4, - 0x38, 0x25, 0x79, 0xd2, 0x60, 0x78, 0x5d, 0x3f, 0xad, 0x69, 0x55, 0x25, 0xbe, 0xd6, 0xc5, 0x4e, 0xa9, 0x80, 0xb9, - 0xcf, 0xe9, 0xa9, 0x75, 0xa4, 0x78, 0x6b, 0xad, 0xad, 0x4f, 0x18, 0xe6, 0xf6, 0xde, 0x69, 0x0f, 0x20, 0x7f, 0xcc, - 0x67, 0x26, 0xc1, 0xc0, 0x88, 0x30, 0xc0, 0x5b, 0xa2, 0x97, 0x33, 0x26, 0x4f, 0xd0, 0x4c, 0x5f, 0xdc, 0xa3, 0xdc, - 0xbb, 0xdc, 0x7d, 0xca, 0x37, 0x2a, 0xb3, 0x47, 0x37, 0x5d, 0x04, 0xb4, 0xd6, 0x4d, 0x94, 0x1a, 0x1e, 0xc7, 0xb5, - 0xcb, 0x0b, 0xb1, 0x94, 0xc4, 0xeb, 0x10, 0xcd, 0xbf, 0xcb, 0x4f, 0x0e, 0x9b, 0x94, 0x95, 0x25, 0xdf, 0x19, 0x4b, - 0xc3, 0x8f, 0x15, 0xf2, 0xc2, 0x46, 0xaa, 0x01, 0x14, 0x57, 0x7a, 0x1d, 0xed, 0x64, 0xed, 0x5d, 0x56, 0x41, 0xa3, - 0x54, 0xc8, 0xd1, 0xa3, 0x35, 0x70, 0x94, 0x3a, 0x21, 0xd9, 0xc0, 0x5b, 0x60, 0x26, 0xaf, 0x0c, 0x4e, 0x01, 0xb5, - 0xf2, 0x48, 0x78, 0xe6, 0x42, 0x5e, 0x9a, 0xfc, 0x4c, 0xde, 0x8d, 0xc0, 0x78, 0xca, 0x07, 0x9e, 0xb8, 0xb0, 0x4c, - 0xfc, 0xb7, 0xec, 0x0f, 0x10, 0x95, 0x4c, 0x06, 0x15, 0x08, 0x4c, 0x83, 0x5d, 0x7c, 0x2d, 0x8d, 0xd4, 0x7a, 0x08, - 0xc1, 0xc9, 0xd5, 0x46, 0x7f, 0x30, 0xeb, 0x6b, 0x40, 0xa9, 0x7a, 0x83, 0x8a, 0x46, 0xec, 0xca, 0xf6, 0xd3, 0xbc, - 0x3e, 0x98, 0xa8, 0x7d, 0xa3, 0x81, 0x1b, 0xb6, 0xf9, 0xd5, 0x1e, 0xc5, 0xae, 0x8d, 0xe7, 0x4b, 0x60, 0x13, 0xb5, - 0xbc, 0x65, 0x52, 0x14, 0x1c, 0xda, 0x34, 0xa8, 0x76, 0x04, 0x23, 0x66, 0xba, 0x83, 0xce, 0x5b, 0xdb, 0x20, 0x3a, - 0x1d, 0x9c, 0x46, 0xd0, 0x19, 0x8c, 0x8b, 0x53, 0x5b, 0x35, 0x42, 0x49, 0x8c, 0x2f, 0xc7, 0xd0, 0x2f, 0xb2, 0x78, - 0xa3, 0x66, 0xda, 0x00, 0x5d, 0x49, 0x05, 0xf3, 0x6c, 0xc4, 0x4c, 0x0a, 0xb7, 0xec, 0xb9, 0x5d, 0x8a, 0xff, 0xa5, - 0x3b, 0xd7, 0xf7, 0x3c, 0x11, 0xe4, 0x03, 0x59, 0x3a, 0x0e, 0xfe, 0xb5, 0x98, 0xe1, 0xe7, 0x19, 0x8c, 0x5e, 0x64, - 0xd6, 0xc6, 0x2c, 0xc9, 0x17, 0x7c, 0x67, 0xf8, 0xa5, 0x06, 0x93, 0x9f, 0xb0, 0x9c, 0x21, 0xfa, 0x1a, 0x04, 0x38, - 0x72, 0xb5, 0xeb, 0x69, 0xc3, 0x78, 0x07, 0x8b, 0x17, 0xc5, 0x02, 0x51, 0xd4, 0xfb, 0x6a, 0x8e, 0xc3, 0xe2, 0x9c, - 0xa4, 0x04, 0x33, 0x9b, 0x1a, 0x49, 0x21, 0x64, 0xef, 0x9b, 0x93, 0x57, 0x56, 0x1a, 0x52, 0x9c, 0xc0, 0xcb, 0x81, - 0x5e, 0x23, 0xd2, 0xf1, 0xb1, 0x3a, 0x6b, 0x28, 0x4e, 0x1a, 0x99, 0x62, 0x36, 0xb1, 0x90, 0xce, 0xaa, 0x07, 0x1b, - 0xf3, 0x69, 0x91, 0x2b, 0xaf, 0xeb, 0x08, 0x7f, 0xad, 0xc2, 0x70, 0x96, 0x5e, 0x6f, 0xbe, 0x18, 0x06, 0x1d, 0xfe, - 0xaf, 0xd5, 0x84, 0x6f, 0xf0, 0x6d, 0x3f, 0x5f, 0x44, 0x44, 0xa8, 0xca, 0x0f, 0x74, 0xa2, 0x1d, 0xea, 0xe8, 0x34, - 0xf4, 0xd0, 0xcc, 0x56, 0x50, 0xb0, 0x48, 0xfb, 0x7d, 0x37, 0xbd, 0xf5, 0x35, 0x39, 0x7b, 0xe7, 0xba, 0xa6, 0x35, - 0xc1, 0xfc, 0xf8, 0x35, 0xd0, 0x9a, 0x8d, 0x84, 0x93, 0xe5, 0xf7, 0xc8, 0xde, 0x6c, 0xaf, 0x76, 0x67, 0xd4, 0xbe, - 0x3e, 0x1a, 0xde, 0x34, 0x8f, 0x19, 0x1f, 0x65, 0x93, 0x26, 0x6a, 0x3a, 0x73, 0x2d, 0xe0, 0x73, 0x6a, 0xea, 0x4e, - 0x24, 0x3a, 0x70, 0x76, 0xb5, 0x3c, 0xc5, 0x6f, 0x45, 0x64, 0xfa, 0x35, 0x89, 0xea, 0x96, 0x66, 0x50, 0xe4, 0x52, - 0x5a, 0xa8, 0xba, 0xad, 0x2a, 0x80, 0x7d, 0x8d, 0xa8, 0x19, 0xa8, 0x31, 0x0b, 0xdd, 0x29, 0x1a, 0x21, 0x8d, 0xb5, - 0x8c, 0xed, 0x87, 0x9a, 0x76, 0xa5, 0xaa, 0x1e, 0xdb, 0x25, 0x0e, 0x45, 0x03, 0x34, 0x2d, 0xcc, 0xf5, 0x6f, 0x76, - 0x75, 0xb3, 0x6d, 0x4b, 0xbd, 0x41, 0x5c, 0xf2, 0x9f, 0x87, 0x2d, 0xac, 0x9d, 0x29, 0x85, 0xc3, 0x15, 0xad, 0xe8, - 0x91, 0x6c, 0x1c, 0xb4, 0x0a, 0x83, 0xa8, 0x51, 0x65, 0xda, 0x88, 0x61, 0x44, 0xc2, 0x08, 0x85, 0x42, 0xe1, 0x3e, - 0x62, 0x5d, 0x6c, 0xca, 0xa3, 0x87, 0xd2, 0xea, 0x92, 0x1f, 0xb3, 0x58, 0xa3, 0x79, 0x5a, 0x7b, 0x2c, 0x64, 0xa9, - 0xc3, 0xc7, 0x2b, 0xc1, 0xe8, 0xf7, 0xcb, 0x84, 0x56, 0x6e, 0x31, 0x41, 0xa9, 0x0e, 0x24, 0x76, 0x3b, 0x79, 0x8b, - 0xf4, 0x63, 0x8b, 0x42, 0x12, 0xb2, 0x3f, 0xbd, 0x2c, 0x93, 0xa7, 0x8a, 0xe1, 0x55, 0xe4, 0x2c, 0x47, 0x09, 0xf1, - 0x0e, 0xfc, 0xa4, 0x5f, 0x7f, 0x92, 0x7a, 0xad, 0xba, 0xad, 0x75, 0x54, 0xd4, 0xce, 0x6d, 0xe9, 0x86, 0x71, 0x9d, - 0x0c, 0xaa, 0xe0, 0x06, 0x4c, 0xd2, 0xe8, 0x5b, 0x27, 0xa8, 0x4f, 0x31, 0x9a, 0xf2, 0x6a, 0x07, 0x65, 0x2d, 0xc3, - 0x60, 0x8d, 0xf1, 0x61, 0xf8, 0xc0, 0x64, 0xc6, 0x18, 0x61, 0x6c, 0xc3, 0x1c, 0xf9, 0x6c, 0xfa, 0xeb, 0x17, 0x42, - 0xea, 0x4d, 0x12, 0x11, 0x81, 0x7c, 0x90, 0x7c, 0x30, 0x22, 0xfd, 0xa7, 0x25, 0x56, 0x3b, 0xbc, 0x70, 0x48, 0x9f, - 0xc4, 0x16, 0x0e, 0x84, 0xcd, 0xfa, 0xd1, 0x6f, 0x98, 0x64, 0xde, 0xbe, 0x38, 0x41, 0x7e, 0x09, 0x6e, 0xd8, 0xde, - 0x6a, 0x08, 0xaa, 0x18, 0xad, 0x10, 0xc4, 0x0a, 0x1a, 0x21, 0x9e, 0xc0, 0xf9, 0x26, 0x63, 0xd5, 0xab, 0x25, 0x2e, - 0x73, 0x45, 0x83, 0x7f, 0xf6, 0x6d, 0x5a, 0x24, 0x3d, 0x88, 0xf7, 0x03, 0x59, 0xcf, 0xb0, 0x87, 0xa0, 0xc7, 0xc2, - 0x8a, 0xe4, 0xbb, 0x42, 0x96, 0xae, 0xe3, 0xd3, 0x49, 0xaa, 0xf7, 0xa4, 0x5f, 0x3f, 0xc0, 0x1e, 0xb4, 0xa9, 0x2d, - 0x34, 0x7f, 0x85, 0xaa, 0x0a, 0xf3, 0x7a, 0x33, 0xca, 0xa3, 0x25, 0x9b, 0xee, 0x08, 0x74, 0x10, 0x08, 0xb5, 0xd6, - 0x4b, 0x03, 0x8c, 0xe3, 0xfb, 0xb0, 0x19, 0x3d, 0x7e, 0x5d, 0xc4, 0x84, 0xab, 0x97, 0x2d, 0x45, 0x69, 0x93, 0x46, - 0x8f, 0xfb, 0xae, 0x59, 0x76, 0x19, 0x22, 0x88, 0xc4, 0x1f, 0x47, 0xd0, 0x66, 0x5c, 0x0b, 0x17, 0xd1, 0x09, 0x86, - 0x96, 0x2b, 0x9e, 0xb8, 0x47, 0xdd, 0x2f, 0xbb, 0xe7, 0xdb, 0xe6, 0x49, 0x0c, 0x58, 0x8a, 0xf8, 0xae, 0xac, 0xcd, - 0x39, 0x94, 0xa2, 0x74, 0x9b, 0xc0, 0x2c, 0x47, 0x7a, 0x8c, 0x47, 0xf2, 0x48, 0xd4, 0x01, 0x83, 0x68, 0x54, 0x7c, - 0x67, 0x65, 0xe0, 0x6e, 0xad, 0xb5, 0xf8, 0xf2, 0xf7, 0x7e, 0xfd, 0x7a, 0xb7, 0x42, 0xbd, 0x0c, 0x5e, 0x4e, 0xed, - 0x19, 0xef, 0xbc, 0x20, 0xa5, 0xbe, 0x88, 0xc1, 0xeb, 0xc7, 0xbc, 0x8a, 0x66, 0xdf, 0x35, 0x04, 0xa1, 0x85, 0xcb, - 0x7f, 0x0b, 0x8f, 0x3a, 0x2d, 0xd3, 0xa5, 0xa7, 0xaf, 0xa6, 0x9b, 0x4e, 0x97, 0xef, 0xe8, 0xc1, 0xad, 0x10, 0x21, - 0x61, 0x54, 0x63, 0xed, 0x93, 0x73, 0x8b, 0xc9, 0x97, 0xd1, 0xda, 0xa5, 0x55, 0x51, 0xc1, 0xe7, 0x1c, 0xdd, 0x0d, - 0xaa, 0x5b, 0xb8, 0xa9, 0x72, 0xfb, 0xe8, 0xad, 0xa8, 0xa2, 0xa1, 0x87, 0x0b, 0xa7, 0x44, 0x12, 0x85, 0xc8, 0x4b, - 0x98, 0xd8, 0x7d, 0x37, 0xa4, 0x81, 0x71, 0x75, 0x75, 0x4a, 0x75, 0x83, 0xc7, 0xd0, 0xc3, 0x10, 0x24, 0xae, 0xd9, - 0xf9, 0xff, 0xd2, 0xeb, 0xc1, 0x9b, 0x97, 0x3e, 0x25, 0x99, 0x17, 0xfe, 0x5d, 0x5a, 0xb8, 0xc5, 0x17, 0xfc, 0x8c, - 0x96, 0xa0, 0x65, 0xcb, 0xa3, 0xb2, 0x03, 0xeb, 0xa1, 0x3d, 0xd0, 0xbf, 0xae, 0x27, 0x9b, 0x55, 0x00, 0x5a, 0x5b, - 0x9e, 0x64, 0x34, 0x31, 0x7a, 0x72, 0xde, 0xa1, 0x50, 0x44, 0x42, 0x0e, 0xa3, 0x44, 0xad, 0x75, 0x20, 0xc3, 0x55, - 0x77, 0x5a, 0x0a, 0xb7, 0xb1, 0xbb, 0x9e, 0x59, 0x88, 0xe8, 0x48, 0x2f, 0x49, 0x66, 0x2e, 0x34, 0x21, 0xa8, 0x92, - 0xc8, 0x0f, 0x88, 0x6d, 0x81, 0xe3, 0x41, 0x73, 0x62, 0xeb, 0xa3, 0xd0, 0x52, 0x40, 0x18, 0xb7, 0x57, 0xf1, 0x35, - 0x01, 0x84, 0xd2, 0xba, 0xf3, 0x66, 0xbb, 0x70, 0xf9, 0x37, 0x6d, 0x65, 0x0c, 0x36, 0x3a, 0x77, 0x56, 0x71, 0x81, - 0x5b, 0xdd, 0x8b, 0x21, 0x88, 0x02, 0x25, 0x05, 0x31, 0x9c, 0x04, 0xd5, 0x07, 0x73, 0x20, 0x01, 0x97, 0xc8, 0x83, - 0x52, 0xe3, 0x5c, 0xb8, 0xf1, 0x46, 0x21, 0xc4, 0x62, 0x24, 0xaa, 0x62, 0xb2, 0x41, 0x70, 0x4c, 0x05, 0xda, 0xfd, - 0xf4, 0xdc, 0x7b, 0xe1, 0xfe, 0xa1, 0xa6, 0x56, 0x73, 0xa1, 0x08, 0xa3, 0xdd, 0xc9, 0xbd, 0xa0, 0x85, 0x64, 0xab, - 0x5e, 0xae, 0x91, 0xbd, 0xf0, 0xcd, 0x73, 0xef, 0x2b, 0x25, 0x20, 0xec, 0xdf, 0x19, 0x07, 0x02, 0x60, 0x2e, 0xed, - 0x6a, 0x2d, 0xd1, 0xdf, 0x9e, 0x48, 0xb3, 0xa1, 0xa5, 0x58, 0x37, 0xf3, 0x50, 0x01, 0xd6, 0xd4, 0xea, 0x09, 0x4b, - 0x59, 0xe5, 0x8d, 0x66, 0xa7, 0x86, 0xb7, 0x1d, 0x74, 0x75, 0x92, 0xc2, 0x93, 0xee, 0xff, 0xbd, 0x1f, 0x5e, 0xab, - 0x6e, 0x85, 0xa0, 0x3a, 0x93, 0x74, 0x16, 0x32, 0xd5, 0x7a, 0x1a, 0x53, 0x9c, 0xa4, 0xef, 0x04, 0x45, 0x45, 0x68, - 0xfc, 0x53, 0xd1, 0xd9, 0xf8, 0xa9, 0xeb, 0x03, 0xf7, 0x03, 0x2e, 0x28, 0xbe, 0xc9, 0x3a, 0x7e, 0x98, 0x45, 0x44, - 0x64, 0xe5, 0x67, 0xbb, 0xbc, 0x76, 0xdd, 0xa6, 0x33, 0xf3, 0xd2, 0x6d, 0x74, 0x9b, 0x7f, 0x83, 0x25, 0x1f, 0x8a, - 0x92, 0x97, 0xb4, 0x85, 0xa3, 0x5f, 0xe0, 0x7a, 0x28, 0xd3, 0x81, 0x21, 0x73, 0xeb, 0xba, 0xfe, 0x51, 0x31, 0xd2, - 0xd4, 0x11, 0x4f, 0x99, 0x43, 0x05, 0x9e, 0x9a, 0x9b, 0x4a, 0x0e, 0x94, 0xce, 0x30, 0x5f, 0x13, 0xe1, 0xcd, 0xde, - 0x31, 0xfd, 0x73, 0x41, 0x74, 0x7c, 0x04, 0xd3, 0x86, 0x7c, 0x58, 0x85, 0xe7, 0xe2, 0x58, 0xfd, 0x60, 0x35, 0x89, - 0x3c, 0x89, 0x03, 0xbc, 0x0f, 0x2c, 0x52, 0x61, 0x62, 0xe0, 0x6b, 0x76, 0x3b, 0xce, 0x17, 0x80, 0x19, 0x0f, 0xb9, - 0x4f, 0x77, 0xfc, 0x10, 0x04, 0x8e, 0x17, 0xaa, 0xc5, 0xcd, 0xe1, 0x2d, 0x08, 0x80, 0x8c, 0x59, 0x71, 0x5a, 0x8c, - 0xf2, 0x24, 0x25, 0xe0, 0x99, 0x5c, 0xba, 0x55, 0x43, 0x2a, 0xb3, 0x3f, 0x04, 0x94, 0x4b, 0xd7, 0x42, 0x0a, 0x6e, - 0xd5, 0x17, 0xa6, 0x84, 0x74, 0xd7, 0x68, 0xb0, 0x85, 0x7f, 0x2c, 0x88, 0x25, 0x0d, 0xea, 0x1a, 0xbf, 0xed, 0x57, - 0xee, 0xa4, 0x73, 0xe2, 0x3a, 0x4a, 0x5d, 0x22, 0x89, 0xfb, 0x3c, 0x8c, 0x04, 0x82, 0x99, 0x3d, 0x21, 0xb2, 0x18, - 0x62, 0x1f, 0x47, 0x3b, 0x02, 0xf0, 0x04, 0xa2, 0x23, 0xcf, 0xec, 0x5e, 0x40, 0x7c, 0x7a, 0x82, 0xb0, 0x2c, 0x7f, - 0x23, 0xe3, 0xd7, 0xd7, 0xc3, 0xec, 0x89, 0x62, 0x78, 0x25, 0xf1, 0x54, 0xc5, 0x12, 0x49, 0x43, 0x6b, 0xc0, 0xd2, - 0x15, 0xc9, 0x65, 0xe4, 0x19, 0x71, 0x7e, 0x66, 0x7d, 0x02, 0x7e, 0xec, 0x63, 0x38, 0xf0, 0xeb, 0x40, 0x5f, 0xa4, - 0x54, 0xf9, 0x36, 0x12, 0xe7, 0xd5, 0x7b, 0x73, 0xfd, 0x70, 0x27, 0xe9, 0xea, 0xd5, 0xa3, 0x6d, 0x68, 0x6c, 0x92, - 0xf4, 0x6f, 0xcd, 0x43, 0x80, 0x63, 0x9d, 0xa4, 0x33, 0x14, 0xc6, 0x64, 0x79, 0xd8, 0xb0, 0xea, 0x62, 0xe8, 0xce, - 0x03, 0xee, 0xa6, 0xba, 0x23, 0x75, 0xf8, 0xd2, 0xa4, 0x27, 0x85, 0x59, 0xd2, 0xd8, 0x25, 0xe9, 0xdf, 0xaa, 0x74, - 0x38, 0x54, 0x29, 0x46, 0xe4, 0xb2, 0x22, 0x46, 0xa6, 0x1d, 0xdc, 0x49, 0xfc, 0xb6, 0xe4, 0x9d, 0x80, 0x97, 0x36, - 0xf5, 0x79, 0x57, 0x2b, 0x26, 0xb4, 0x8c, 0xc9, 0x93, 0xb7, 0x76, 0x58, 0x69, 0xa1, 0x54, 0xb2, 0x40, 0x36, 0x65, - 0xa1, 0x51, 0xf0, 0x53, 0xdf, 0xfc, 0xf0, 0x83, 0xa2, 0x32, 0x10, 0x70, 0x3b, 0x58, 0xfd, 0xe3, 0x83, 0x78, 0x19, - 0x43, 0x3c, 0x32, 0x32, 0xa6, 0x7f, 0xc9, 0x50, 0xf7, 0x4f, 0x20, 0x93, 0xf0, 0x75, 0x76, 0xbf, 0x34, 0xf7, 0x17, - 0x6a, 0x1d, 0x8c, 0xeb, 0x68, 0x43, 0x13, 0xf8, 0x26, 0x14, 0xee, 0x87, 0xca, 0xc0, 0x46, 0x29, 0x43, 0xbe, 0x2f, - 0x6d, 0x9e, 0x7b, 0xe2, 0x27, 0x41, 0xf1, 0x69, 0x86, 0x59, 0xc8, 0x08, 0xa0, 0xfa, 0x70, 0x32, 0xe9, 0xba, 0x43, - 0x6d, 0x7b, 0xab, 0xa9, 0x74, 0x76, 0xd4, 0x31, 0x41, 0xce, 0xf3, 0xa3, 0x19, 0x56, 0x9e, 0xbf, 0x36, 0xf9, 0x06, - 0x81, 0xcf, 0x9d, 0xf7, 0xa6, 0x5a, 0x07, 0x6a, 0xbf, 0x9c, 0x11, 0xb4, 0x2d, 0x03, 0x1c, 0xa9, 0x72, 0xa8, 0x8e, - 0x54, 0x0c, 0x2b, 0x33, 0xde, 0x98, 0xe2, 0xc5, 0x16, 0x7b, 0x9c, 0x2f, 0x21, 0x15, 0xb0, 0x1f, 0x90, 0xb2, 0xe4, - 0x98, 0xd5, 0x22, 0x65, 0x6f, 0x22, 0x05, 0x11, 0xca, 0x6f, 0x5e, 0x1e, 0xfc, 0xdd, 0x64, 0x84, 0x15, 0x58, 0xab, - 0x8e, 0x24, 0x5b, 0x9b, 0xc8, 0x69, 0x2d, 0xa9, 0x8a, 0xf3, 0x46, 0x19, 0x66, 0xbf, 0xeb, 0xcf, 0xcb, 0x26, 0x98, - 0xda, 0xc5, 0xa7, 0xe6, 0x60, 0x8d, 0x16, 0xa6, 0xa7, 0xc2, 0xfe, 0x82, 0x0f, 0x3d, 0x21, 0xbf, 0x19, 0xfc, 0xb2, - 0xaf, 0x6d, 0x18, 0x3c, 0x6a, 0x5f, 0x61, 0x43, 0xa5, 0x75, 0x31, 0xf5, 0xa2, 0x61, 0xb2, 0x2d, 0x7f, 0x7e, 0x2a, - 0xf7, 0x18, 0xb9, 0xc2, 0x8c, 0xc0, 0x1a, 0x95, 0x77, 0xc1, 0x01, 0xe1, 0x63, 0x44, 0x91, 0x1b, 0xb6, 0x45, 0x06, - 0x05, 0xdb, 0x46, 0xd3, 0xae, 0xf4, 0x31, 0x07, 0x79, 0x12, 0x84, 0x4e, 0x76, 0xc2, 0x3b, 0x5f, 0xbd, 0x32, 0x2c, - 0x8b, 0x24, 0xcd, 0x8b, 0xa8, 0xd2, 0x87, 0x55, 0xd5, 0x48, 0xe3, 0xbf, 0x00, 0x60, 0x14, 0xce, 0x97, 0xc2, 0xbf, - 0x29, 0x5c, 0xe3, 0x9d, 0xde, 0xaa, 0x91, 0x72, 0x8e, 0xc7, 0xbc, 0x9b, 0xb1, 0xc3, 0x0f, 0xc2, 0x97, 0xdc, 0xcf, - 0xa8, 0xc0, 0x3c, 0x2d, 0x0b, 0xb3, 0x35, 0xfc, 0x5a, 0x81, 0xdc, 0x3e, 0x12, 0xe7, 0x36, 0x6c, 0xa6, 0x53, 0x7b, - 0xd3, 0x97, 0x1b, 0x84, 0xcc, 0x19, 0xcd, 0xf2, 0xf5, 0x87, 0x87, 0x01, 0x75, 0x11, 0x7e, 0x3b, 0x16, 0xea, 0x51, - 0xb6, 0x74, 0xf0, 0x02, 0x1e, 0xae, 0x69, 0x29, 0x7a, 0xbe, 0xa9, 0x9d, 0xc8, 0x1f, 0x6f, 0x7c, 0x00, 0x6d, 0xef, - 0x75, 0x8c, 0x36, 0xd1, 0x43, 0x4a, 0x01, 0x22, 0x0b, 0x6d, 0xab, 0xaa, 0x66, 0xc8, 0xb2, 0x60, 0xb9, 0x0d, 0xbc, - 0xa7, 0x96, 0x08, 0x87, 0xef, 0xda, 0xd3, 0x85, 0x35, 0xfe, 0x58, 0x00, 0xfc, 0xfb, 0x8c, 0x88, 0x0a, 0xe5, 0x7f, - 0x87, 0xed, 0x19, 0x26, 0x22, 0xee, 0x08, 0xc9, 0x60, 0xc0, 0xc8, 0x43, 0x36, 0x23, 0x29, 0xd8, 0x6a, 0xa5, 0x00, - 0xbe, 0x87, 0x40, 0xe3, 0xba, 0x06, 0x21, 0xd8, 0xa0, 0xd7, 0x40, 0x3c, 0x1c, 0x2f, 0xa8, 0x6b, 0xbc, 0x36, 0x7e, - 0xbb, 0xd6, 0x9a, 0x30, 0xe2, 0xdb, 0xaa, 0x59, 0xbc, 0x56, 0x3d, 0x52, 0x39, 0x92, 0x10, 0x79, 0xa3, 0xa0, 0xd5, - 0x9b, 0x4e, 0xf7, 0xd3, 0x64, 0x44, 0x0a, 0x6a, 0x9d, 0x1b, 0x91, 0xf2, 0x4b, 0xb1, 0x39, 0xf5, 0x87, 0x94, 0x87, - 0x2c, 0x8f, 0xb9, 0x12, 0xd5, 0xb5, 0x08, 0x61, 0xd8, 0x75, 0xf4, 0xaf, 0xa1, 0x84, 0x21, 0x5f, 0x52, 0x19, 0x14, - 0x32, 0x53, 0x59, 0x20, 0x0d, 0xe5, 0x49, 0xe4, 0xee, 0x87, 0xe9, 0xea, 0xdd, 0xab, 0xdc, 0x27, 0xa9, 0x28, 0x89, - 0xdd, 0x85, 0x48, 0x93, 0x89, 0x37, 0xa6, 0xfd, 0xf6, 0x3f, 0x23, 0xe5, 0xdf, 0x54, 0x1d, 0x53, 0x8d, 0x25, 0xb1, - 0xd0, 0x00, 0xa5, 0x7c, 0xc4, 0xd3, 0x36, 0x5d, 0x8e, 0xa2, 0xad, 0xd2, 0x1e, 0x6a, 0x1e, 0x78, 0x58, 0x58, 0x13, - 0xc7, 0x11, 0xf4, 0x74, 0x84, 0xd8, 0x92, 0xda, 0x42, 0xd7, 0xa7, 0x99, 0x67, 0x45, 0x6d, 0x76, 0x8f, 0x54, 0xcb, - 0xe8, 0xa0, 0x35, 0x91, 0x34, 0xfb, 0xa9, 0xcb, 0x34, 0x90, 0x0a, 0x81, 0xef, 0x82, 0xdc, 0x2a, 0x4a, 0x5c, 0xaf, - 0xee, 0xdd, 0x43, 0xb5, 0xf2, 0x3b, 0xff, 0x74, 0x0b, 0x22, 0x23, 0x81, 0x81, 0x14, 0xd1, 0xbc, 0xa0, 0x9f, 0x18, - 0x96, 0xc1, 0x90, 0x33, 0x25, 0xb3, 0x9c, 0xb7, 0x00, 0xed, 0x91, 0xb6, 0x82, 0x0b, 0x12, 0x46, 0xe7, 0x58, 0x2b, - 0x83, 0xcf, 0x91, 0x22, 0x5f, 0xb5, 0xc5, 0xcc, 0x5e, 0xdd, 0xd3, 0x02, 0x53, 0x01, 0xac, 0x2b, 0x05, 0xcb, 0x59, - 0xdf, 0x28, 0xaa, 0xd8, 0xc8, 0xe4, 0xc7, 0x5f, 0xd0, 0x3d, 0xa5, 0x0c, 0x9d, 0x15, 0xae, 0x34, 0x6d, 0x3b, 0x97, - 0xc7, 0xee, 0x83, 0x02, 0x7c, 0xa2, 0x81, 0xcc, 0x4b, 0xf2, 0x9f, 0x9d, 0x6d, 0xd8, 0x7d, 0x52, 0xce, 0x77, 0x13, - 0xfd, 0xde, 0x48, 0xb3, 0xbf, 0x2e, 0x75, 0x28, 0xdb, 0xaf, 0x3c, 0xae, 0x5a, 0xe6, 0x3d, 0xd2, 0xe7, 0xc6, 0x64, - 0x44, 0x26, 0xb4, 0xcb, 0xfa, 0x36, 0xc2, 0xcd, 0xed, 0x33, 0x85, 0xc7, 0x37, 0xbc, 0x9c, 0x3f, 0x69, 0xc5, 0x36, - 0xa5, 0x8e, 0x94, 0xd8, 0x48, 0x14, 0x18, 0x64, 0x91, 0x9f, 0x78, 0x8a, 0x6e, 0xef, 0xa8, 0xad, 0x77, 0xea, 0xae, - 0x93, 0x13, 0x71, 0xe6, 0xea, 0x46, 0x52, 0xa5, 0x11, 0x76, 0xf3, 0x3c, 0xf2, 0xb2, 0x56, 0xd0, 0x8c, 0xd2, 0x43, - 0xed, 0xf6, 0x8e, 0x11, 0x1a, 0x56, 0x4a, 0x8a, 0x6c, 0x51, 0x1b, 0x2d, 0x07, 0x7a, 0xc8, 0x5f, 0x6b, 0x7e, 0xfe, - 0x67, 0x0b, 0x71, 0xe3, 0x70, 0xfa, 0x8a, 0x44, 0x44, 0x61, 0x10, 0xa7, 0x55, 0x24, 0xdb, 0x99, 0x40, 0x61, 0x1c, - 0x4c, 0xb0, 0x75, 0xa3, 0xa9, 0x87, 0x45, 0xa2, 0x96, 0x48, 0xc3, 0xf6, 0x28, 0x0f, 0x81, 0x2a, 0x09, 0x3d, 0x6d, - 0xad, 0x4e, 0xa2, 0xf5, 0x87, 0x6b, 0x9f, 0x11, 0xa1, 0x55, 0x8a, 0xac, 0xca, 0x2b, 0x57, 0x68, 0x04, 0x26, 0x12, - 0x89, 0x8e, 0x20, 0x0e, 0x4d, 0x08, 0x1f, 0x1e, 0xd2, 0x4b, 0xab, 0x22, 0x86, 0x56, 0x5c, 0x43, 0x66, 0x01, 0xc4, - 0x26, 0x63, 0xfa, 0x7a, 0xbf, 0x63, 0x19, 0xd7, 0xcb, 0x83, 0x56, 0xff, 0x91, 0x88, 0x13, 0xa4, 0xfb, 0xa2, 0x03, - 0x8c, 0x06, 0x1c, 0x55, 0xc1, 0xb7, 0x8f, 0xa1, 0xa8, 0x74, 0xa0, 0x5e, 0x8e, 0xdc, 0x22, 0xf2, 0x2b, 0xe0, 0xda, - 0xdd, 0x8a, 0x08, 0xdf, 0x2e, 0x9f, 0xd3, 0xac, 0x96, 0x11, 0xd4, 0xbe, 0x8f, 0x68, 0x95, 0x89, 0xb0, 0xb9, 0xb8, - 0xeb, 0xb1, 0x5c, 0x43, 0xd5, 0x87, 0x56, 0x5c, 0x44, 0xe1, 0xcd, 0x1c, 0xa4, 0x8d, 0xc9, 0x78, 0x49, 0x3f, 0xb5, - 0x1d, 0x95, 0x80, 0x5e, 0x28, 0x0b, 0x4d, 0x06, 0x49, 0xc1, 0xa3, 0xf7, 0x40, 0x98, 0xa4, 0x57, 0xce, 0x98, 0xfd, - 0x94, 0xa7, 0x24, 0x4e, 0xdf, 0x82, 0x76, 0x36, 0x45, 0xf0, 0x70, 0xd5, 0x5e, 0xda, 0x53, 0xd0, 0x7e, 0xcf, 0x74, - 0xb6, 0xbd, 0xac, 0x91, 0x78, 0x2f, 0x3a, 0xf0, 0x2a, 0xfa, 0x3c, 0x32, 0xc3, 0x98, 0xc1, 0xfd, 0x90, 0x90, 0xe4, - 0xb3, 0x94, 0x0a, 0x66, 0x41, 0x0f, 0xe4, 0x51, 0x91, 0x8c, 0xd2, 0x4c, 0xb7, 0xfd, 0x91, 0xe8, 0xfa, 0x69, 0xaa, - 0x76, 0xb5, 0xf6, 0x14, 0x55, 0x5e, 0xbe, 0x5e, 0xf8, 0xed, 0xf4, 0x8c, 0xae, 0xdc, 0x01, 0xc9, 0x7a, 0x46, 0x6f, - 0x5a, 0xb0, 0x5a, 0x28, 0x1d, 0xa9, 0x20, 0xf6, 0x3e, 0x27, 0xb0, 0x18, 0xd7, 0x43, 0x1a, 0xd6, 0xe0, 0x03, 0xa6, - 0x27, 0xab, 0xd3, 0x77, 0xce, 0x7d, 0xd9, 0xff, 0xf6, 0xdd, 0x76, 0x83, 0x35, 0x73, 0xf1, 0x07, 0xb9, 0x4b, 0x40, - 0xcd, 0x21, 0xd3, 0x64, 0xd2, 0x57, 0x69, 0x37, 0x27, 0xaf, 0x12, 0xb3, 0x70, 0x80, 0xfe, 0xdd, 0x1c, 0x72, 0xc2, - 0x3a, 0xde, 0xb8, 0x44, 0xf4, 0xd4, 0xbb, 0x70, 0xda, 0x13, 0x07, 0xf0, 0x2f, 0x18, 0x26, 0x4a, 0x88, 0x64, 0x2e, - 0xe1, 0x61, 0x5e, 0xc2, 0x51, 0xf2, 0x43, 0xb6, 0xcd, 0x54, 0xef, 0xa8, 0x6e, 0xde, 0x14, 0xd8, 0xf3, 0x14, 0x17, - 0xbb, 0x55, 0xc2, 0x8c, 0x55, 0xec, 0xb5, 0xd8, 0x43, 0x8f, 0x37, 0x28, 0x92, 0xcc, 0xf6, 0x19, 0x0d, 0x9d, 0xf9, - 0x4d, 0x7e, 0x7d, 0x71, 0x73, 0x37, 0xfd, 0x7f, 0x5c, 0x9d, 0x18, 0x87, 0x15, 0x7c, 0x36, 0xf4, 0x65, 0xbf, 0x2a, - 0xcb, 0xe7, 0x12, 0x5f, 0xad, 0x96, 0x77, 0x94, 0x8c, 0x7b, 0x9b, 0xf8, 0xc1, 0x00, 0x06, 0x6f, 0x5a, 0x48, 0x1e, - 0x4a, 0x14, 0x61, 0x64, 0xde, 0x1f, 0x2e, 0x29, 0xf0, 0x72, 0xf9, 0x55, 0x70, 0x91, 0x98, 0xfe, 0xb6, 0xf5, 0x4a, - 0x2a, 0xe2, 0x90, 0xaa, 0x23, 0xde, 0x4d, 0x10, 0x69, 0x51, 0x0e, 0x1d, 0xfd, 0xc4, 0x01, 0x93, 0x35, 0x38, 0x10, - 0x81, 0x82, 0xce, 0x67, 0x2f, 0x89, 0xc4, 0x8f, 0xd0, 0x5f, 0x79, 0x16, 0x75, 0xda, 0x8b, 0xf6, 0xb3, 0xed, 0x85, - 0xff, 0x7f, 0xba, 0xb4, 0xaa, 0x73, 0x6c, 0x40, 0xb0, 0xfe, 0x2f, 0x90, 0xb8, 0xd0, 0x0e, 0x4a, 0x90, 0xee, 0x53, - 0xf8, 0xc4, 0x9f, 0xdd, 0x69, 0x96, 0x13, 0x96, 0xaf, 0xd5, 0x6a, 0x19, 0x4f, 0xe8, 0x9c, 0x5c, 0x68, 0x1c, 0x27, - 0x6a, 0xca, 0x10, 0x3c, 0xe3, 0xe8, 0xed, 0xb5, 0x0c, 0xbd, 0xdb, 0x98, 0x4a, 0x82, 0x4e, 0xe2, 0x71, 0xc5, 0x52, - 0xac, 0xa2, 0x75, 0xbf, 0x6a, 0x63, 0xb6, 0x99, 0xc1, 0x19, 0x30, 0xce, 0xb2, 0x80, 0xd1, 0x83, 0xa5, 0x7a, 0x38, - 0x37, 0x0e, 0x98, 0x5e, 0xcb, 0x6b, 0x4c, 0x47, 0x04, 0x8d, 0xdd, 0xf2, 0xf5, 0x7a, 0x14, 0x51, 0x49, 0x26, 0x22, - 0xde, 0x2d, 0xf0, 0x40, 0x2f, 0x7e, 0x3e, 0x56, 0xbd, 0xf6, 0xa4, 0x6e, 0xe5, 0x2b, 0x15, 0x61, 0x59, 0x47, 0x53, - 0x19, 0x30, 0x5e, 0xdd, 0xac, 0x8a, 0xd0, 0xae, 0x84, 0xb8, 0x78, 0x21, 0x7b, 0x02, 0x31, 0x31, 0x92, 0x8f, 0x38, - 0x93, 0x60, 0x93, 0x83, 0x4c, 0x05, 0xe5, 0xe3, 0x43, 0xcd, 0x16, 0x04, 0xa1, 0xab, 0xdb, 0x50, 0x0b, 0x72, 0x2c, - 0x9c, 0x27, 0x92, 0x11, 0x4d, 0x12, 0x03, 0x57, 0x59, 0x49, 0xb0, 0x6a, 0x26, 0xe3, 0x65, 0xd6, 0x9d, 0x15, 0x7b, - 0x60, 0x53, 0xad, 0x39, 0x18, 0xdd, 0xff, 0x98, 0x17, 0x18, 0xa2, 0x28, 0x5a, 0xcf, 0x12, 0xc5, 0xf3, 0x88, 0x79, - 0xc0, 0x2c, 0x4b, 0xa0, 0x51, 0x84, 0x22, 0xf4, 0x6f, 0x89, 0x3d, 0xd4, 0x67, 0x95, 0x91, 0x58, 0x58, 0x0c, 0x27, - 0x54, 0xc5, 0xe9, 0x28, 0xed, 0x95, 0x85, 0xf7, 0xc1, 0x6e, 0x10, 0xc6, 0x57, 0xff, 0xd7, 0xf8, 0xee, 0x6d, 0x0f, - 0xa9, 0xed, 0xf9, 0x38, 0x9b, 0x99, 0x8f, 0xe9, 0x46, 0xef, 0xf6, 0x4f, 0x37, 0x22, 0x36, 0xbb, 0xad, 0x6c, 0x03, - 0x91, 0x4c, 0x14, 0x94, 0x9a, 0x3f, 0xb3, 0xdb, 0x03, 0xb6, 0xe2, 0xa0, 0x78, 0xdd, 0xa9, 0xa8, 0xcf, 0xd6, 0x6a, - 0x2c, 0x01, 0x08, 0xc7, 0x92, 0x46, 0xda, 0x25, 0xb6, 0x20, 0xd2, 0xfa, 0x1c, 0x19, 0x12, 0xbc, 0xb6, 0xce, 0xf3, - 0x00, 0x0e, 0x59, 0x32, 0x4d, 0x04, 0x2b, 0xd2, 0x4b, 0x00, 0x69, 0x1b, 0x21, 0xbd, 0xc8, 0x9e, 0x41, 0x09, 0xc0, - 0xab, 0x88, 0xf6, 0x46, 0x65, 0x22, 0x92, 0x37, 0x59, 0xa3, 0x14, 0xb6, 0xe3, 0x03, 0xe1, 0x65, 0x7e, 0x44, 0xb0, - 0xc4, 0xd4, 0x68, 0xcf, 0x96, 0x4e, 0x13, 0x50, 0x8b, 0xed, 0x34, 0x42, 0x98, 0xef, 0xd7, 0x8d, 0xc1, 0xc3, 0xe1, - 0x62, 0x49, 0x8a, 0x71, 0xb5, 0xde, 0x7c, 0x2c, 0xe5, 0x76, 0xe4, 0x5a, 0xec, 0x98, 0x3b, 0x70, 0x34, 0xe9, 0x9e, - 0xa6, 0x5d, 0xbd, 0xd1, 0xcd, 0xaf, 0xf8, 0xcd, 0xeb, 0x69, 0x89, 0x97, 0xc7, 0x47, 0x42, 0xf7, 0xae, 0x46, 0xa0, - 0xb9, 0x71, 0xce, 0x0f, 0xfd, 0xee, 0x5d, 0x9d, 0xe0, 0x8d, 0xff, 0x95, 0x1a, 0x4f, 0xca, 0xe4, 0xe4, 0x6f, 0xe6, - 0xd0, 0xf8, 0x8c, 0xf1, 0xb4, 0xb8, 0xa9, 0x40, 0xa0, 0x40, 0xd5, 0x01, 0xec, 0xce, 0xa2, 0xd2, 0x7f, 0x33, 0x62, - 0x7c, 0x04, 0x5c, 0x28, 0x3a, 0x35, 0xf8, 0x69, 0x49, 0x78, 0xc6, 0xf3, 0xd9, 0x2d, 0xd4, 0x22, 0x69, 0xa5, 0xce, - 0xc4, 0x4e, 0x1d, 0x7e, 0x2e, 0xf6, 0x0f, 0x2b, 0x22, 0x3a, 0x14, 0xf1, 0x61, 0x37, 0x05, 0x05, 0x41, 0xc3, 0x0b, - 0x62, 0x99, 0xa0, 0x0b, 0xa1, 0x7c, 0xd4, 0xec, 0xbe, 0x4c, 0x52, 0xfd, 0xc3, 0xfe, 0x82, 0x9f, 0x7b, 0xdb, 0xd7, - 0x22, 0x17, 0x4b, 0xa1, 0x59, 0xd9, 0x48, 0x27, 0x0f, 0x2e, 0x15, 0x6d, 0xaf, 0x82, 0xec, 0x6c, 0xde, 0x9a, 0x35, - 0x99, 0x03, 0xc9, 0x22, 0x2d, 0xc2, 0xfa, 0xc8, 0x73, 0xfe, 0xd9, 0xe2, 0x73, 0xba, 0x74, 0xdf, 0x37, 0x63, 0xdb, - 0x64, 0x98, 0xf4, 0x24, 0x81, 0x30, 0x51, 0x82, 0x6f, 0xcb, 0xe8, 0x01, 0x60, 0x7d, 0x6d, 0x39, 0xa2, 0xcc, 0x56, - 0x01, 0x99, 0xc9, 0x76, 0x7e, 0xed, 0x29, 0x20, 0xea, 0x6c, 0x05, 0x12, 0x31, 0x94, 0x3e, 0x03, 0x17, 0x34, 0x3e, - 0xb5, 0x20, 0x49, 0x89, 0x09, 0xc9, 0x0c, 0x1f, 0x01, 0x99, 0x02, 0x6e, 0xbe, 0xf3, 0x64, 0x0b, 0xfd, 0x54, 0xc6, - 0x29, 0xb0, 0x61, 0x5a, 0x0f, 0xe6, 0x2c, 0x8c, 0x67, 0x1a, 0x32, 0xe2, 0xf8, 0xe7, 0x1b, 0x81, 0xd5, 0x88, 0x3c, - 0x8f, 0xcc, 0x6c, 0x24, 0x2e, 0x60, 0x70, 0xd9, 0xf7, 0x92, 0x26, 0x10, 0xd7, 0xaf, 0x71, 0x8d, 0xf9, 0x24, 0x8f, - 0xd7, 0xea, 0xe6, 0xb7, 0xe6, 0x6f, 0x3d, 0x94, 0x67, 0xf9, 0xe2, 0xa4, 0x36, 0xad, 0x15, 0xf4, 0xcb, 0xb5, 0x56, - 0x1b, 0xa0, 0x56, 0xc1, 0xcc, 0x96, 0x8c, 0x5b, 0x69, 0x51, 0x04, 0x8e, 0x9a, 0xe5, 0xad, 0xd9, 0x1d, 0xdf, 0x6e, - 0x90, 0x3f, 0xd1, 0x10, 0x31, 0x3e, 0x55, 0x81, 0x4b, 0xd4, 0xf1, 0x73, 0xf4, 0x36, 0x87, 0x76, 0x18, 0xaf, 0xe2, - 0x87, 0x6d, 0xbc, 0xc8, 0xd8, 0xca, 0xf2, 0xf2, 0x75, 0x74, 0x88, 0x2b, 0x47, 0xeb, 0x30, 0x34, 0xfd, 0x34, 0x8d, - 0x39, 0x52, 0xa8, 0x27, 0xec, 0xf8, 0x7b, 0x0c, 0xf8, 0x4f, 0xed, 0xf1, 0xab, 0xfe, 0x93, 0xe3, 0x5f, 0x86, 0x5d, - 0xe5, 0x65, 0xfe, 0x63, 0x90, 0xbf, 0x87, 0x4f, 0xa9, 0xf2, 0xb3, 0x89, 0x2c, 0x54, 0x9b, 0x43, 0xf2, 0x68, 0x2d, - 0x87, 0x05, 0xfa, 0xea, 0xc3, 0xc9, 0xe9, 0x04, 0xc3, 0x9a, 0x53, 0xe7, 0x07, 0xa4, 0xaf, 0x6b, 0xe2, 0xc2, 0x2f, - 0xc6, 0xa7, 0xab, 0xb9, 0x0c, 0xdd, 0x74, 0xb9, 0x87, 0x54, 0xee, 0x8b, 0x4c, 0xb5, 0xad, 0xac, 0x9d, 0xfd, 0xfd, - 0x60, 0x70, 0x47, 0x04, 0x36, 0x9c, 0x7b, 0xf3, 0x85, 0xe7, 0x9f, 0xf4, 0x9f, 0xee, 0x29, 0x5a, 0x33, 0x16, 0x5f, - 0xe8, 0x33, 0xad, 0x5c, 0xbe, 0x71, 0x6d, 0xca, 0x36, 0xf4, 0x99, 0xda, 0x74, 0x03, 0x20, 0x26, 0x15, 0x34, 0x28, - 0xeb, 0xdc, 0xc7, 0x1f, 0x14, 0x7d, 0x48, 0x28, 0xfa, 0xd2, 0xe7, 0xa3, 0xda, 0x17, 0x06, 0x11, 0x00, 0x49, 0x0c, - 0x33, 0xf3, 0x97, 0x82, 0xee, 0x84, 0x86, 0x07, 0xfa, 0xe6, 0xe9, 0x27, 0x4e, 0x7d, 0x06, 0xde, 0x1a, 0xf5, 0x0f, - 0x52, 0x5c, 0x5f, 0x81, 0x04, 0xb0, 0x70, 0x9e, 0x2e, 0xff, 0x4f, 0x5e, 0x10, 0x42, 0xd2, 0x9c, 0x2c, 0x6a, 0x3b, - 0x57, 0xdd, 0xff, 0x98, 0x6c, 0xf0, 0x11, 0x1b, 0x24, 0xf8, 0x38, 0x29, 0x26, 0x9e, 0x05, 0x41, 0x75, 0x45, 0xcf, - 0x4d, 0xae, 0xfa, 0xb3, 0x8c, 0x43, 0xfe, 0xf7, 0xb1, 0x3c, 0x5f, 0x40, 0xce, 0x63, 0xc6, 0x77, 0x7e, 0x88, 0xb0, - 0x24, 0xa7, 0x61, 0x67, 0x76, 0x5f, 0xed, 0xcf, 0xa3, 0x81, 0xfa, 0x6a, 0x77, 0x7e, 0x03, 0x09, 0xf8, 0xc2, 0x0f, - 0x6b, 0xa8, 0x51, 0xf9, 0xa3, 0x39, 0xad, 0xbd, 0x4a, 0x1b, 0x7d, 0x42, 0x25, 0x22, 0xb8, 0x94, 0x1c, 0xa0, 0xbe, - 0x48, 0x3a, 0x3f, 0x99, 0x38, 0x66, 0xda, 0x94, 0x1c, 0x74, 0xe3, 0x5c, 0x72, 0xa1, 0x1c, 0x5a, 0x49, 0x1d, 0xa5, - 0x80, 0x9c, 0x94, 0xd1, 0x81, 0x15, 0xdd, 0x5e, 0x4a, 0xc6, 0x8f, 0x8a, 0xd0, 0x2e, 0x9b, 0xcd, 0x35, 0x2b, 0x8b, - 0x99, 0x44, 0x8c, 0x54, 0x70, 0x59, 0xb2, 0x32, 0x41, 0xbf, 0xa8, 0x8d, 0xa9, 0x8d, 0x64, 0xba, 0xb7, 0x59, 0x9d, - 0xba, 0x08, 0x34, 0xb8, 0x72, 0x40, 0x5e, 0xf1, 0x2a, 0x60, 0x4b, 0x48, 0x26, 0xcc, 0x22, 0x56, 0x70, 0xa1, 0xdc, - 0xf7, 0x99, 0xfb, 0x60, 0x7f, 0x9c, 0x04, 0xe7, 0x5b, 0x6f, 0xd1, 0xef, 0x12, 0x9c, 0x82, 0xea, 0xf5, 0xe7, 0xd4, - 0xb5, 0xb8, 0xbc, 0x62, 0x0a, 0x0a, 0x1c, 0x83, 0x7c, 0xba, 0xcb, 0x03, 0x22, 0xf0, 0x43, 0xfb, 0xa4, 0x8e, 0xd3, - 0xd6, 0x7f, 0x46, 0xc7, 0x0c, 0xb2, 0x81, 0x54, 0xe7, 0xbe, 0x2c, 0xb0, 0x9d, 0x2e, 0x5b, 0x22, 0xb7, 0x02, 0x77, - 0x4a, 0x75, 0xdb, 0x0b, 0x94, 0x29, 0xd1, 0x51, 0x11, 0x81, 0x6d, 0x06, 0xd0, 0xb3, 0x15, 0xc1, 0x4b, 0x1f, 0x79, - 0xd6, 0x2c, 0x68, 0xe8, 0x8f, 0x88, 0x34, 0x99, 0xd0, 0x80, 0x41, 0x2e, 0x26, 0x5e, 0x4c, 0xbd, 0xf4, 0x98, 0x45, - 0xc7, 0x19, 0x70, 0x67, 0xe7, 0x2c, 0x2c, 0xe6, 0xf5, 0x34, 0xe5, 0x4b, 0x8f, 0x7e, 0x33, 0xbc, 0xec, 0x5e, 0xba, - 0xf9, 0xe9, 0x73, 0xba, 0xc1, 0xc4, 0x06, 0xba, 0x6b, 0x75, 0x1c, 0xcd, 0x83, 0x87, 0x16, 0x30, 0x93, 0xd2, 0x1c, - 0xef, 0x5b, 0xd0, 0xd2, 0xb9, 0x08, 0x99, 0xc8, 0xcb, 0x83, 0x9e, 0xed, 0x1b, 0xd2, 0x50, 0xb3, 0x13, 0xe0, 0xb3, - 0xb3, 0x34, 0x9a, 0x2b, 0x5e, 0xd0, 0x42, 0x09, 0x23, 0xc4, 0x65, 0xfd, 0x13, 0xe0, 0x81, 0xaf, 0x01, 0xca, 0x9f, - 0x94, 0xeb, 0x81, 0x67, 0x8e, 0xb9, 0x43, 0x38, 0xe1, 0xa7, 0x8a, 0xc0, 0xdd, 0x45, 0x85, 0xfe, 0xc9, 0x8c, 0x21, - 0x85, 0x01, 0x5f, 0x15, 0xe3, 0x29, 0xe1, 0x15, 0x68, 0x1a, 0x94, 0x07, 0x87, 0x01, 0xaa, 0x51, 0xf7, 0x7d, 0xed, - 0x53, 0x38, 0xa2, 0x09, 0x0f, 0x79, 0x87, 0xbc, 0x1a, 0xaa, 0x85, 0x26, 0x3e, 0xe4, 0x1d, 0x93, 0xe0, 0x9b, 0x0f, - 0xf7, 0x32, 0x7d, 0x44, 0x95, 0xa3, 0x17, 0xfa, 0x84, 0x05, 0x92, 0x53, 0x3d, 0x5e, 0x4b, 0xd1, 0x5e, 0xd1, 0x1c, - 0x5a, 0x09, 0x54, 0x6c, 0x4c, 0x87, 0x70, 0xe9, 0xd9, 0x5e, 0x7f, 0x7a, 0x93, 0xf8, 0x8b, 0x38, 0x63, 0x0d, 0x7b, - 0x02, 0xce, 0x99, 0x0d, 0xaa, 0xef, 0x64, 0x97, 0xb0, 0x48, 0x92, 0x9e, 0x67, 0x4e, 0xc7, 0x04, 0xb9, 0x79, 0xcd, - 0x1d, 0xdc, 0xcc, 0x43, 0xb9, 0xaa, 0x4f, 0x44, 0xa1, 0x49, 0xdd, 0x49, 0x41, 0xc4, 0x5b, 0xf7, 0xd7, 0x09, 0x6a, - 0x19, 0x75, 0x75, 0xd3, 0xdf, 0x8a, 0x78, 0x64, 0xcc, 0xbf, 0x56, 0x91, 0xd1, 0x2a, 0xf7, 0xdb, 0xd6, 0x5f, 0x57, - 0x66, 0x0f, 0xd9, 0xcf, 0xab, 0x5a, 0xf7, 0x3f, 0x6b, 0xd8, 0x6b, 0x5e, 0x2b, 0x45, 0x11, 0xce, 0x5c, 0x4d, 0x7a, - 0x64, 0xbe, 0x41, 0x2f, 0xee, 0xf6, 0xf0, 0x9c, 0x04, 0x28, 0x13, 0x27, 0x21, 0x0e, 0x24, 0xe4, 0x18, 0x6b, 0xbe, - 0xa2, 0x3d, 0xe6, 0xba, 0x61, 0x51, 0x99, 0x6b, 0x7e, 0x20, 0x68, 0x40, 0x05, 0xf4, 0x87, 0x1d, 0x4e, 0x73, 0x6f, - 0x42, 0xc3, 0x6f, 0x42, 0x3e, 0x43, 0xf0, 0xbb, 0xfc, 0x63, 0x81, 0xa1, 0xd0, 0x52, 0x43, 0x86, 0xeb, 0x54, 0x37, - 0x63, 0xb2, 0xda, 0x04, 0xfe, 0xb7, 0xb4, 0x9b, 0x51, 0x43, 0x64, 0x41, 0x84, 0x0c, 0x30, 0x90, 0xd1, 0x16, 0x03, - 0xaf, 0x69, 0xa6, 0xd9, 0x6a, 0x30, 0x56, 0x1f, 0x36, 0x59, 0x0a, 0x76, 0xaf, 0x9c, 0xa9, 0x1c, 0x06, 0xb5, 0x81, - 0xe1, 0x5d, 0xfa, 0xde, 0x3e, 0x2c, 0xe3, 0x45, 0xad, 0x90, 0xfb, 0xe9, 0x10, 0x19, 0x6e, 0x52, 0xc7, 0x6a, 0xa6, - 0xff, 0x19, 0x33, 0x31, 0x3e, 0x35, 0x38, 0x04, 0x36, 0x8c, 0x5d, 0xd5, 0x82, 0x61, 0xda, 0x28, 0xed, 0x9d, 0xb2, - 0x2e, 0x68, 0x21, 0x2c, 0xad, 0x5a, 0xe3, 0x80, 0x71, 0x4e, 0x2f, 0x2f, 0xd4, 0xe9, 0xc4, 0x9b, 0xfa, 0xdb, 0xc8, - 0x84, 0x0f, 0xfe, 0x35, 0x87, 0xba, 0x5a, 0x18, 0x85, 0xe3, 0xe1, 0xa3, 0x94, 0x07, 0x49, 0x68, 0x89, 0x85, 0xa6, - 0x6c, 0x6e, 0x48, 0x54, 0x87, 0x62, 0x23, 0x28, 0xf8, 0x7c, 0x85, 0x08, 0xb8, 0xc3, 0x61, 0x4b, 0x49, 0xf7, 0x16, - 0xcf, 0x90, 0x24, 0x2a, 0x23, 0x17, 0x9b, 0x60, 0x0f, 0x04, 0xc7, 0xa5, 0xa6, 0xac, 0xe5, 0xf9, 0xaf, 0x92, 0xe8, - 0x9f, 0xa0, 0xe0, 0x25, 0x29, 0xe5, 0xfc, 0x3b, 0x16, 0x7f, 0xd6, 0xd7, 0xc7, 0x88, 0xde, 0xfb, 0x0b, 0xc9, 0x67, - 0x7d, 0xb6, 0x6d, 0x1b, 0x23, 0xf3, 0xf2, 0x66, 0xae, 0x1f, 0xeb, 0xb8, 0x8c, 0x5d, 0xa2, 0x80, 0xe8, 0x6f, 0x58, - 0x2e, 0xd3, 0x3c, 0xc0, 0xf2, 0xb0, 0x3c, 0x8a, 0xfc, 0xb6, 0x22, 0x59, 0xa2, 0x81, 0xb7, 0x38, 0x2d, 0xd2, 0xbb, - 0xed, 0x78, 0x97, 0x2b, 0x75, 0x3a, 0x74, 0x2b, 0x63, 0x49, 0x34, 0xaa, 0x94, 0x43, 0x10, 0x03, 0x77, 0xba, 0x38, - 0x06, 0xbb, 0x33, 0x99, 0x3d, 0x4e, 0xdb, 0x98, 0xea, 0x4e, 0xdc, 0xd1, 0x13, 0xbc, 0xc4, 0x27, 0x2d, 0x35, 0xbb, - 0xa3, 0x17, 0x5a, 0x86, 0x4a, 0xc8, 0xea, 0xf4, 0x85, 0x56, 0x3f, 0x8a, 0x90, 0x24, 0x07, 0xfc, 0xaa, 0xf2, 0x97, - 0xaf, 0xd7, 0x63, 0xa1, 0x52, 0x5d, 0x54, 0x14, 0x68, 0x7e, 0x9e, 0x26, 0x1e, 0xa3, 0xdd, 0x6f, 0xa5, 0xaf, 0xce, - 0x1b, 0xc2, 0x07, 0xa0, 0x02, 0x92, 0x5b, 0x39, 0x34, 0xa4, 0xa1, 0x65, 0x3e, 0x3c, 0xce, 0x50, 0x28, 0x02, 0x74, - 0x26, 0x75, 0xe1, 0xa9, 0x8b, 0xf3, 0xea, 0x1f, 0x13, 0x54, 0x6c, 0x3f, 0xa7, 0x0c, 0x80, 0x93, 0x34, 0x40, 0x34, - 0x1a, 0xcf, 0x59, 0xa7, 0xd1, 0x85, 0x52, 0x3c, 0x03, 0x47, 0xc0, 0x7a, 0x89, 0x5c, 0x7b, 0x45, 0xeb, 0x5a, 0x86, - 0x34, 0xe9, 0xfe, 0xed, 0x51, 0xbf, 0x0d, 0xc1, 0x1d, 0x98, 0x34, 0x12, 0x3a, 0xaa, 0xae, 0x98, 0x1b, 0xc9, 0x38, - 0x50, 0x77, 0xda, 0x4d, 0x29, 0x37, 0x56, 0xd8, 0xcb, 0x5c, 0xb6, 0xaa, 0x63, 0xb9, 0x44, 0x58, 0xc4, 0xc5, 0x51, - 0x62, 0x45, 0x80, 0xef, 0x91, 0x41, 0x54, 0xaa, 0xf2, 0x24, 0x8a, 0x90, 0xe4, 0x0d, 0x16, 0x18, 0x73, 0x0b, 0xfd, - 0x26, 0x12, 0x3c, 0xf8, 0xfa, 0xa7, 0x15, 0x49, 0xa1, 0x12, 0x10, 0x40, 0x83, 0x81, 0x16, 0x50, 0xcd, 0xfc, 0xa3, - 0x51, 0xb7, 0x18, 0x3a, 0xcf, 0xe2, 0x39, 0x25, 0xc9, 0xa0, 0x3f, 0xfe, 0xfb, 0x09, 0x61, 0xd0, 0x06, 0xb0, 0x90, - 0x11, 0x07, 0xdc, 0x30, 0xd7, 0x9b, 0x44, 0xfa, 0x6c, 0x51, 0x2c, 0xb6, 0xd8, 0xcb, 0xb9, 0x4d, 0xe9, 0x05, 0x8d, - 0x97, 0x50, 0xf2, 0x8e, 0xe1, 0x2b, 0xaf, 0x83, 0x99, 0xb7, 0xbc, 0xc6, 0x0d, 0x16, 0xfa, 0x05, 0x8b, 0xae, 0x4a, - 0x72, 0xef, 0x97, 0x3d, 0x00, 0x99, 0x4f, 0x27, 0xaa, 0x37, 0x04, 0x0f, 0x21, 0x65, 0x63, 0xcc, 0x98, 0x73, 0x83, - 0x7b, 0xe7, 0x53, 0x45, 0x0a, 0x23, 0x17, 0x86, 0x00, 0x4e, 0x62, 0x94, 0xd1, 0xa6, 0x9e, 0xce, 0x42, 0x9d, 0x7e, - 0xe1, 0xad, 0x03, 0xf5, 0x6b, 0x53, 0x09, 0xc5, 0x5e, 0xd6, 0x03, 0x22, 0x6a, 0xaa, 0xae, 0x04, 0xe5, 0xa6, 0x44, - 0xf5, 0xed, 0x27, 0x78, 0xa6, 0xe3, 0x50, 0xfc, 0xcc, 0xc0, 0x48, 0x4c, 0x84, 0xe4, 0x6c, 0x90, 0x24, 0x4f, 0xd2, - 0x65, 0x2d, 0x09, 0xea, 0xda, 0xaf, 0x12, 0xc2, 0x23, 0x92, 0x71, 0x7e, 0xd6, 0x87, 0x7a, 0xe0, 0xca, 0x06, 0xbb, - 0x1c, 0x4f, 0xbf, 0x08, 0xd7, 0xdd, 0xa6, 0x3d, 0x35, 0xbc, 0xfb, 0x54, 0x64, 0x72, 0xcb, 0x56, 0xcd, 0x62, 0x63, - 0x82, 0x9f, 0xf8, 0x3c, 0xe8, 0x3e, 0xee, 0x37, 0x24, 0xa1, 0x9f, 0x6f, 0x73, 0x3f, 0xb1, 0xd2, 0xe8, 0x03, 0x78, - 0xd0, 0x58, 0x8a, 0x4b, 0x2b, 0x50, 0x63, 0xc1, 0x97, 0xdb, 0x92, 0xbc, 0xcd, 0x3c, 0xf0, 0x9d, 0xb8, 0x10, 0xba, - 0xc8, 0x7d, 0xe4, 0x6b, 0xe4, 0xad, 0xf4, 0x6e, 0xd5, 0x46, 0xfe, 0xe0, 0x57, 0x7f, 0xc8, 0x4a, 0x4f, 0xe9, 0x8f, - 0xca, 0x9c, 0xc5, 0x37, 0x5c, 0xa2, 0x9b, 0x81, 0x1d, 0xc1, 0x49, 0x5d, 0xf5, 0xe1, 0x0d, 0xa0, 0xb5, 0x85, 0xb7, - 0x1c, 0x4d, 0x13, 0x81, 0xe7, 0x66, 0xb8, 0xc4, 0x15, 0xd9, 0xe0, 0x06, 0x8a, 0x3d, 0x28, 0x60, 0x20, 0x36, 0xca, - 0x74, 0x3c, 0x00, 0xfc, 0x1c, 0xe2, 0x6e, 0xcc, 0xcf, 0xba, 0x66, 0x1b, 0x2d, 0x70, 0x4e, 0x91, 0x05, 0x64, 0x2f, - 0x43, 0x03, 0x0a, 0xd0, 0x09, 0x45, 0x50, 0xda, 0xac, 0xb1, 0x03, 0x26, 0x84, 0x15, 0x46, 0xd3, 0x00, 0xc7, 0x86, - 0x98, 0xb3, 0x97, 0xe6, 0x16, 0x81, 0x2f, 0x97, 0x88, 0x5c, 0xd3, 0x23, 0xa1, 0x7c, 0x86, 0x14, 0x38, 0xd1, 0xcf, - 0xd3, 0x7f, 0x03, 0x26, 0xbd, 0x9b, 0x13, 0xb4, 0xa7, 0x83, 0x69, 0xbf, 0xd4, 0x10, 0x28, 0x2d, 0x23, 0xf7, 0x87, - 0xe6, 0xf9, 0x7a, 0x31, 0xa6, 0x6b, 0xac, 0x76, 0x1b, 0xcd, 0x3f, 0xc5, 0xf5, 0x3c, 0x19, 0xb7, 0x88, 0x5c, 0x18, - 0x00, 0x7e, 0x6f, 0x06, 0x8d, 0xb7, 0xde, 0x4f, 0x2a, 0x3c, 0x67, 0x0d, 0x4b, 0x28, 0xdd, 0x23, 0xf2, 0x6d, 0xfe, - 0x87, 0x69, 0x3a, 0x28, 0x82, 0x53, 0x1b, 0xf2, 0xde, 0x83, 0x7b, 0x58, 0x87, 0x82, 0xa1, 0xaf, 0xc3, 0x94, 0xf9, - 0xc9, 0xce, 0xb2, 0x81, 0xd2, 0xb6, 0xb3, 0x5d, 0x35, 0x25, 0xed, 0x42, 0x32, 0x5c, 0x91, 0x18, 0xa4, 0xda, 0xc8, - 0x8f, 0xb6, 0xb2, 0xb2, 0x66, 0x6e, 0xa7, 0x38, 0xf2, 0x73, 0xa7, 0x33, 0xec, 0xde, 0xd8, 0xac, 0x1b, 0x1e, 0x80, - 0x34, 0xd7, 0x29, 0x00, 0x08, 0xc2, 0xa5, 0x6c, 0xe2, 0x1d, 0x4b, 0x95, 0xed, 0x40, 0x7d, 0xb0, 0xd0, 0x1c, 0x5c, - 0xe7, 0xa3, 0x09, 0xf9, 0xb8, 0xd9, 0xac, 0xf4, 0x3c, 0x07, 0x88, 0xc7, 0x71, 0x52, 0x19, 0x0c, 0x91, 0x82, 0x9f, - 0x4a, 0xb0, 0xc3, 0xd1, 0xe2, 0x3c, 0xfa, 0x6b, 0xe4, 0xbc, 0x4f, 0x50, 0x0d, 0x15, 0x58, 0x15, 0x6c, 0xc3, 0xc6, - 0xd3, 0x8b, 0xc4, 0x65, 0xbb, 0x53, 0xa1, 0x45, 0x87, 0x83, 0x5a, 0xf8, 0xd0, 0x01, 0xfd, 0x90, 0x14, 0x0a, 0x71, - 0x2e, 0xc2, 0x79, 0x16, 0x92, 0xa7, 0x0d, 0x14, 0x46, 0x5e, 0x8c, 0xdf, 0xc6, 0x70, 0xb6, 0xeb, 0x16, 0x23, 0x4b, - 0xd7, 0x74, 0x6b, 0x9c, 0x67, 0x93, 0x1f, 0xd1, 0x13, 0x27, 0x55, 0x24, 0xd9, 0x44, 0x2a, 0xa8, 0x51, 0x1a, 0x6c, - 0xfc, 0x15, 0x00, 0x05, 0x73, 0xc3, 0x6b, 0x1a, 0x8f, 0xb4, 0x4c, 0xc4, 0x5d, 0x8e, 0x06, 0x9b, 0xcc, 0xc1, 0xe3, - 0x60, 0xd0, 0x2b, 0xc4, 0x19, 0xda, 0x05, 0x4e, 0x72, 0xab, 0xc4, 0x1e, 0x7f, 0x91, 0xef, 0x25, 0x5e, 0xd8, 0x94, - 0xa1, 0x47, 0x3c, 0x48, 0x2c, 0x9b, 0x8f, 0xc1, 0xea, 0xfc, 0xbb, 0xfd, 0x90, 0x68, 0x34, 0xd0, 0x01, 0xe8, 0xc8, - 0x97, 0x70, 0xde, 0x13, 0xd3, 0xa4, 0x7b, 0xac, 0xbb, 0xf8, 0xd6, 0xfd, 0xf5, 0x69, 0xf0, 0xdd, 0x21, 0xa3, 0x2f, - 0x70, 0x3b, 0x69, 0xc2, 0x76, 0x6e, 0x5a, 0xeb, 0xfa, 0x03, 0x58, 0x00, 0xa7, 0xcc, 0x78, 0x26, 0x86, 0x97, 0x0d, - 0xfa, 0x46, 0x17, 0xe4, 0xb9, 0xbb, 0x73, 0x1d, 0xf7, 0xe7, 0xb8, 0xf5, 0xe2, 0x94, 0x43, 0x6a, 0x61, 0x11, 0xa8, - 0x19, 0x40, 0x05, 0xe4, 0x65, 0x34, 0x25, 0x5d, 0x10, 0xfc, 0xda, 0xa0, 0xe8, 0x7c, 0x87, 0x31, 0x40, 0xbd, 0xea, - 0x39, 0xcd, 0xfb, 0x5b, 0x4e, 0xf2, 0x42, 0xc4, 0x7b, 0x9b, 0xa6, 0xfa, 0xa7, 0x95, 0x7d, 0xca, 0x94, 0xd7, 0x2f, - 0xcd, 0xd1, 0xd9, 0x84, 0xaa, 0xad, 0xdd, 0xa6, 0x1a, 0xe2, 0x73, 0xbc, 0x64, 0x1e, 0x51, 0xbc, 0x80, 0x81, 0xe6, - 0x81, 0x1f, 0x79, 0xaf, 0xed, 0xdd, 0x68, 0x56, 0x5e, 0xa7, 0x0b, 0xfa, 0x7e, 0x4e, 0xb3, 0x12, 0xbc, 0x67, 0x67, - 0xab, 0x4a, 0xf5, 0x92, 0x0a, 0x86, 0x79, 0xd7, 0x2b, 0x7f, 0x46, 0xbf, 0xd1, 0x13, 0x3f, 0xe5, 0x4d, 0xe3, 0x37, - 0x25, 0xf3, 0xdb, 0x24, 0x4c, 0xea, 0x1c, 0xd6, 0xf4, 0x28, 0xdd, 0x4e, 0x28, 0xe7, 0x41, 0xee, 0x5e, 0xf6, 0x35, - 0xb2, 0xdd, 0x78, 0xa5, 0xfc, 0xe2, 0x8b, 0x96, 0x7f, 0xd7, 0xab, 0xba, 0xd2, 0xa4, 0x79, 0x1a, 0x3b, 0x57, 0x0f, - 0xf5, 0xe9, 0x43, 0x47, 0xa6, 0xfb, 0x23, 0xbe, 0xcc, 0xc8, 0xd0, 0x22, 0x33, 0xdf, 0x55, 0x56, 0x43, 0x5c, 0x6b, - 0x35, 0xf1, 0xee, 0x54, 0xe6, 0xcc, 0x8e, 0xdd, 0x78, 0x91, 0x64, 0x30, 0xaa, 0x8e, 0x4c, 0x0d, 0x89, 0xe4, 0x84, - 0xa8, 0x5f, 0x31, 0x08, 0x98, 0x75, 0x8d, 0x7f, 0xb5, 0xaf, 0x29, 0xb7, 0xe5, 0x5b, 0x8f, 0xb9, 0xfe, 0x36, 0x4e, - 0xb7, 0x5f, 0x13, 0x60, 0xdb, 0x56, 0x5c, 0xea, 0xc5, 0x28, 0xb6, 0x36, 0x32, 0xb1, 0x82, 0x96, 0x27, 0xe0, 0xf0, - 0x80, 0x44, 0xa2, 0xc2, 0xa4, 0xd9, 0x37, 0x92, 0x4a, 0x76, 0xd8, 0xf0, 0xcd, 0x7d, 0xab, 0xb8, 0xa0, 0x8f, 0xf6, - 0x8f, 0x3a, 0x55, 0xd3, 0xcb, 0x42, 0x51, 0x55, 0x1d, 0xf4, 0xa0, 0x29, 0x95, 0xd2, 0xe7, 0x5f, 0xee, 0x4c, 0x12, - 0x10, 0x59, 0x25, 0x81, 0x31, 0x7d, 0x80, 0x62, 0x2a, 0xed, 0x5a, 0x03, 0xa2, 0xc6, 0xbf, 0x21, 0xe1, 0xb7, 0xfa, - 0x31, 0x3d, 0xba, 0x47, 0xdf, 0xfd, 0x5c, 0xda, 0xb5, 0x08, 0x56, 0xe9, 0xad, 0xfe, 0x25, 0x3f, 0xf5, 0x44, 0xd5, - 0xbc, 0x52, 0xbd, 0x33, 0xb4, 0xa7, 0xf9, 0xd3, 0x39, 0x3f, 0x7f, 0x3a, 0xe7, 0x75, 0x30, 0xcc, 0x8c, 0xad, 0x5b, - 0x15, 0xbc, 0x5c, 0xe0, 0xba, 0x84, 0x32, 0x3c, 0xf5, 0xe5, 0x3f, 0x3c, 0x0b, 0x53, 0x19, 0x90, 0x9a, 0x6c, 0x60, - 0x4c, 0x65, 0xe0, 0x74, 0x73, 0x43, 0x47, 0xd3, 0x8d, 0x56, 0x26, 0x9a, 0x19, 0x4f, 0x86, 0x1a, 0x72, 0x1a, 0x73, - 0xb0, 0xa7, 0x87, 0x8c, 0x43, 0xad, 0x66, 0xfc, 0x68, 0xb4, 0x05, 0x6d, 0x40, 0xe1, 0x67, 0x30, 0x53, 0x01, 0x06, - 0xd1, 0xf9, 0x36, 0xce, 0x3e, 0x49, 0x7f, 0x67, 0x99, 0xf3, 0x7c, 0xd9, 0x10, 0xc1, 0xaf, 0x34, 0xe9, 0x76, 0x59, - 0x04, 0xfc, 0x98, 0xd1, 0x58, 0xc6, 0xef, 0xc5, 0xa0, 0x7f, 0x91, 0x3e, 0xaa, 0x11, 0x38, 0x17, 0x20, 0xaf, 0x46, - 0x98, 0x06, 0x8a, 0x86, 0xf6, 0x1a, 0xfa, 0x42, 0xa9, 0x32, 0x7d, 0xed, 0x69, 0x4d, 0x2b, 0xb2, 0xb4, 0xd2, 0x4f, - 0xc6, 0x14, 0xb3, 0x2a, 0x71, 0xec, 0x69, 0xde, 0x37, 0xc8, 0x24, 0x5f, 0x38, 0xcb, 0x68, 0x29, 0xa7, 0xc6, 0x02, - 0xdd, 0x2a, 0xd4, 0xda, 0x85, 0xa7, 0x37, 0x68, 0x1c, 0x18, 0x2a, 0xca, 0xbe, 0x5f, 0x62, 0x8f, 0x78, 0xdf, 0x7e, - 0xc0, 0x47, 0x28, 0xb8, 0xed, 0x39, 0x49, 0x18, 0xa9, 0xfa, 0x5e, 0x51, 0xbc, 0xef, 0x9b, 0x64, 0x24, 0x37, 0x34, - 0x31, 0xd8, 0xa8, 0x85, 0xd3, 0xd3, 0x38, 0x5d, 0x38, 0x3d, 0xc5, 0xa9, 0x45, 0x5b, 0x32, 0xd3, 0xc8, 0x48, 0xac, - 0x5d, 0xe1, 0x44, 0x2d, 0xbf, 0xc9, 0xaf, 0xb2, 0x0d, 0x09, 0x14, 0x95, 0xd6, 0x5e, 0x95, 0x1e, 0xf4, 0x02, 0x36, - 0xe0, 0x4e, 0x75, 0x20, 0xf2, 0x12, 0x5f, 0x2d, 0x40, 0x00, 0x46, 0xf7, 0x3f, 0x58, 0x4d, 0xe9, 0xb4, 0xd1, 0x6e, - 0x4e, 0x39, 0x26, 0x50, 0x72, 0xcd, 0x07, 0x93, 0x90, 0x37, 0x38, 0x71, 0x5e, 0x43, 0xa0, 0x42, 0x1c, 0x43, 0x90, - 0x67, 0xc6, 0x16, 0xf3, 0xfb, 0xb7, 0xaa, 0xfa, 0xa1, 0xa2, 0x53, 0xa8, 0x21, 0x66, 0x5f, 0x68, 0x9e, 0xf0, 0x2b, - 0x72, 0x5e, 0xe6, 0xe4, 0xd2, 0x73, 0x8f, 0xe4, 0xe3, 0xd1, 0xd9, 0x63, 0x24, 0xe8, 0xdb, 0xd5, 0x8e, 0xe6, 0x26, - 0x2c, 0xc5, 0x97, 0xec, 0x67, 0x2a, 0xff, 0x54, 0x2d, 0x14, 0xcf, 0xac, 0xce, 0x3b, 0x65, 0xb1, 0xab, 0x2a, 0x76, - 0x49, 0x67, 0xd0, 0x29, 0x78, 0x33, 0x20, 0x82, 0x8e, 0xe0, 0x24, 0x49, 0x20, 0x11, 0x8d, 0x02, 0xed, 0xd9, 0x4c, - 0x24, 0xc1, 0x5c, 0x80, 0x25, 0xcb, 0x1b, 0x2e, 0x76, 0xed, 0x2f, 0xdd, 0x92, 0xcc, 0x13, 0x70, 0xd1, 0x3c, 0xd3, - 0xe9, 0xe5, 0x3a, 0xf2, 0x39, 0xc2, 0xd4, 0xba, 0xa9, 0xcd, 0xab, 0x84, 0xf3, 0x55, 0xb9, 0x89, 0x95, 0x78, 0x1b, - 0xa8, 0x19, 0xaf, 0x56, 0x2a, 0x7f, 0x62, 0x72, 0x4a, 0x6b, 0x64, 0xa4, 0xb0, 0x3f, 0xa4, 0xaa, 0x6d, 0x94, 0x70, - 0x1b, 0xd2, 0x6f, 0xe7, 0xa7, 0xed, 0x42, 0xf1, 0x5b, 0x3b, 0x18, 0xf2, 0xff, 0xf5, 0x1a, 0xdb, 0x85, 0xa8, 0x51, - 0x60, 0x84, 0x70, 0xb1, 0x08, 0x10, 0x9e, 0x0b, 0xa7, 0x6c, 0x88, 0x85, 0x07, 0x8f, 0xc2, 0xd9, 0xeb, 0xac, 0xab, - 0xde, 0x6f, 0xfe, 0x3e, 0xa8, 0xbf, 0x8f, 0x42, 0xe3, 0x5c, 0xaf, 0xf1, 0x6f, 0x15, 0x3f, 0xfe, 0xd0, 0x42, 0xb1, - 0x81, 0x11, 0x6e, 0x22, 0x68, 0xa5, 0x68, 0xb6, 0x25, 0xd5, 0xfa, 0xaa, 0x80, 0x22, 0x84, 0xdd, 0x49, 0x55, 0x0b, - 0x26, 0xac, 0x3b, 0x3d, 0xc1, 0xf2, 0xf9, 0xc0, 0xe9, 0x3f, 0xa6, 0xed, 0x39, 0x49, 0x54, 0xfa, 0xac, 0x4f, 0x85, - 0x27, 0x4f, 0xa2, 0xd5, 0x3e, 0xf3, 0x2f, 0xc1, 0xad, 0xaf, 0x7e, 0xb5, 0xac, 0x46, 0xca, 0x4c, 0x31, 0x8b, 0xc2, - 0x6b, 0xb7, 0x86, 0x99, 0xf1, 0x8a, 0x62, 0xd2, 0x34, 0x7b, 0x58, 0x0a, 0x8a, 0xbb, 0xba, 0x66, 0x65, 0x4e, 0xe7, - 0x65, 0x46, 0xe6, 0x54, 0x82, 0x71, 0x54, 0x7c, 0x8f, 0x33, 0x7e, 0xa3, 0xad, 0x18, 0x18, 0x75, 0x3c, 0xec, 0xf4, - 0x1c, 0xeb, 0x84, 0x01, 0x7a, 0xe5, 0xb9, 0x89, 0xf0, 0x1a, 0x2f, 0x81, 0x53, 0xa7, 0x36, 0x7d, 0x10, 0x69, 0xe5, - 0xa8, 0x29, 0xcf, 0xb0, 0xc5, 0x94, 0x5e, 0xe3, 0x36, 0x91, 0x76, 0xfb, 0x2f, 0x76, 0x5e, 0x05, 0x9f, 0xd8, 0xd3, - 0x11, 0x1e, 0xd1, 0xa4, 0xb4, 0x08, 0x5e, 0x24, 0xca, 0xc3, 0xf6, 0xc0, 0x73, 0x7e, 0x25, 0xd6, 0x5e, 0x4e, 0x94, - 0xf2, 0x7c, 0xe6, 0x39, 0x88, 0x21, 0x33, 0xb4, 0x84, 0x8e, 0xcf, 0x05, 0x47, 0x62, 0x5c, 0xbd, 0x4f, 0x90, 0xc4, - 0x49, 0xb0, 0xf7, 0xd7, 0x3e, 0x17, 0x69, 0x75, 0xfc, 0xf2, 0x3e, 0x61, 0x21, 0xb6, 0x6b, 0xda, 0x05, 0x2a, 0x64, - 0x3f, 0xf8, 0x53, 0x02, 0x5e, 0x13, 0xf2, 0xb2, 0xef, 0xd4, 0x6e, 0x9d, 0x75, 0xce, 0xff, 0x0b, 0x87, 0x43, 0x8f, - 0x5e, 0x39, 0x9a, 0x6b, 0x26, 0x60, 0x72, 0xc0, 0x51, 0xd5, 0xf7, 0xa2, 0xc4, 0xd1, 0xf0, 0x40, 0xa0, 0xac, 0x12, - 0xff, 0xb0, 0x7e, 0x30, 0x24, 0xa0, 0xf2, 0x88, 0xfb, 0xee, 0xd6, 0xee, 0x9a, 0x76, 0x65, 0x13, 0x2e, 0x57, 0x32, - 0xfe, 0x1b, 0x10, 0xa6, 0xef, 0x03, 0xce, 0xe1, 0xe8, 0xac, 0xfb, 0x02, 0x6e, 0x32, 0xe7, 0x14, 0xa3, 0xb8, 0x96, - 0x7c, 0xc1, 0xf6, 0x94, 0x90, 0xd7, 0xc4, 0x2e, 0xbf, 0x58, 0x47, 0xa1, 0x57, 0x9d, 0xd4, 0xcb, 0xb2, 0xe8, 0xbf, - 0x80, 0x75, 0x82, 0x78, 0x79, 0x9e, 0xe9, 0xb2, 0x6f, 0x12, 0x87, 0x2b, 0xac, 0xb0, 0x99, 0x95, 0x86, 0x66, 0x61, - 0xfa, 0x98, 0xd2, 0x75, 0x2c, 0x30, 0x0e, 0x55, 0xce, 0x76, 0x09, 0x6c, 0x49, 0xaa, 0x99, 0xc6, 0xfb, 0x0d, 0x59, - 0x85, 0x49, 0x4b, 0x2b, 0x8d, 0x17, 0xe8, 0x1e, 0x6b, 0x3c, 0xff, 0x4b, 0x30, 0x4c, 0x18, 0x67, 0x2f, 0xc3, 0xc4, - 0x4f, 0x2a, 0xb8, 0xaa, 0x79, 0x82, 0xc3, 0xe6, 0x9d, 0xf9, 0xab, 0xb6, 0x75, 0xc0, 0x4f, 0xb2, 0x2f, 0x1a, 0xc3, - 0x98, 0x2c, 0x2c, 0x06, 0xee, 0xd2, 0xd8, 0xcf, 0xc1, 0xc2, 0x0d, 0xfb, 0xe8, 0x1b, 0x05, 0x5f, 0xfa, 0xf7, 0x77, - 0xa0, 0x4e, 0x62, 0xd4, 0x75, 0x69, 0x25, 0xa5, 0xbf, 0x46, 0xbe, 0x68, 0x03, 0x88, 0x34, 0x4f, 0x7d, 0x8c, 0x85, - 0x53, 0x41, 0xc4, 0x92, 0x12, 0x61, 0xed, 0x8c, 0x30, 0x6b, 0x65, 0xf9, 0xfe, 0x6c, 0xea, 0x08, 0x05, 0x5d, 0x3b, - 0xcb, 0x9f, 0x73, 0xe9, 0x66, 0x11, 0x5d, 0x37, 0xc0, 0x58, 0x96, 0x1d, 0x51, 0x0e, 0x9e, 0x60, 0xdb, 0xea, 0xae, - 0x88, 0xbc, 0xa1, 0x3e, 0x49, 0xac, 0x4e, 0xe8, 0x23, 0x8e, 0xd6, 0xf9, 0x68, 0x13, 0x4d, 0xbe, 0x59, 0xe5, 0xf2, - 0xd3, 0x26, 0xe6, 0x34, 0xd4, 0xc5, 0x0c, 0x62, 0x38, 0xf8, 0x4e, 0x84, 0xce, 0xa6, 0x7d, 0xdc, 0xa0, 0xcd, 0xc2, - 0x19, 0x9a, 0x86, 0xeb, 0x10, 0x6f, 0x2a, 0x61, 0x51, 0xd9, 0x42, 0xd3, 0x29, 0x9d, 0x70, 0x75, 0x57, 0x98, 0x9b, - 0x73, 0xee, 0xa9, 0xe5, 0x1a, 0xba, 0x26, 0x02, 0x85, 0xe2, 0xb1, 0xe5, 0x1a, 0x7d, 0x75, 0x50, 0xa9, 0x42, 0xa7, - 0xdb, 0x79, 0xf6, 0x2a, 0x16, 0x1c, 0xae, 0x8c, 0xc5, 0x1c, 0x60, 0xe0, 0xa7, 0x71, 0xf2, 0xbe, 0x8a, 0xec, 0x54, - 0x5a, 0x72, 0xde, 0xa5, 0xe4, 0xc0, 0x25, 0xf5, 0xcf, 0x6d, 0x5e, 0x9d, 0x2f, 0x73, 0x9b, 0x29, 0x78, 0x0b, 0x6e, - 0xe9, 0xb4, 0x1e, 0x87, 0xa2, 0xed, 0x10, 0x53, 0xe5, 0x5e, 0x29, 0x25, 0xcf, 0x9a, 0x6a, 0x28, 0x59, 0xe7, 0x18, - 0xeb, 0x8f, 0x0e, 0x51, 0x3e, 0xc4, 0x34, 0xe2, 0x70, 0xa7, 0x62, 0x55, 0xf2, 0x64, 0x25, 0x48, 0x88, 0xd5, 0x36, - 0xcc, 0x0c, 0x5a, 0x59, 0x26, 0xaa, 0x7d, 0x77, 0x9e, 0x47, 0x89, 0x31, 0xbe, 0x32, 0xcb, 0xc2, 0xd7, 0xe5, 0x0e, - 0x74, 0x82, 0x34, 0xb7, 0x9b, 0xc0, 0xb2, 0x5e, 0xa3, 0x11, 0xe6, 0xd3, 0x38, 0x4a, 0x48, 0x40, 0x24, 0xd5, 0x2f, - 0x48, 0x97, 0x12, 0xee, 0x7a, 0xf4, 0x67, 0xf9, 0x20, 0x84, 0xf9, 0xf0, 0xe5, 0x84, 0x53, 0x97, 0x60, 0x3b, 0xde, - 0xe4, 0xb5, 0x02, 0x95, 0x44, 0xd3, 0x6b, 0x2b, 0x4e, 0x75, 0xa9, 0x7a, 0x5b, 0x8c, 0xe2, 0x34, 0xfd, 0xaa, 0x27, - 0xb9, 0xd9, 0x62, 0x61, 0x2c, 0x18, 0x04, 0x1a, 0x50, 0xd1, 0x4d, 0x00, 0xab, 0x31, 0x16, 0x11, 0xaf, 0xcb, 0x0f, - 0x99, 0x54, 0x53, 0x3a, 0x54, 0xed, 0xda, 0xef, 0x0f, 0xee, 0xdd, 0xf0, 0x70, 0xfc, 0xf8, 0x9f, 0xfb, 0xbd, 0x1e, - 0x54, 0x41, 0x97, 0xf0, 0x71, 0x67, 0x3b, 0xa6, 0x42, 0x01, 0xb2, 0xb2, 0x7d, 0x79, 0x01, 0x50, 0x63, 0x2a, 0x4e, - 0xba, 0x6b, 0xeb, 0xde, 0xb4, 0xe0, 0xd3, 0xba, 0x09, 0xdf, 0xfb, 0xe6, 0x7c, 0x6f, 0x98, 0x5a, 0x77, 0xf8, 0xdc, - 0xe5, 0x33, 0x9e, 0x02, 0x99, 0x0b, 0x83, 0xf7, 0x90, 0xe2, 0x26, 0x4c, 0x32, 0xe4, 0x6c, 0x9a, 0x77, 0xda, 0xb2, - 0xbc, 0xd6, 0x52, 0xb2, 0x23, 0x26, 0xec, 0xb9, 0xf2, 0x47, 0xde, 0x93, 0xf8, 0x48, 0x35, 0x04, 0xe0, 0x04, 0xa5, - 0x8d, 0xc0, 0x5c, 0xc5, 0xcd, 0xfc, 0xa9, 0x21, 0x88, 0x08, 0xf4, 0x4c, 0xe1, 0xde, 0xce, 0x1f, 0xce, 0xc6, 0x08, - 0x41, 0x2a, 0xf8, 0x66, 0xa5, 0x36, 0x6b, 0x78, 0xe9, 0x3f, 0x66, 0x67, 0x3b, 0x32, 0xd3, 0xdd, 0x26, 0x51, 0x5b, - 0xb6, 0xa6, 0x02, 0xcc, 0x20, 0x1a, 0x03, 0x17, 0x3c, 0x30, 0xa6, 0xf1, 0xd1, 0x9b, 0x71, 0x62, 0xad, 0xdd, 0xf2, - 0xe5, 0x8c, 0x8f, 0x1c, 0x7d, 0x4e, 0x16, 0xa8, 0x71, 0x77, 0x18, 0xcb, 0xcb, 0xf8, 0x6e, 0x3d, 0x6e, 0x56, 0xf7, - 0x20, 0x0b, 0x08, 0xd0, 0x6b, 0xb1, 0xae, 0x99, 0xe8, 0x55, 0xc2, 0x9d, 0x54, 0x9a, 0x27, 0x95, 0x98, 0x59, 0xe5, - 0xe5, 0xb5, 0xd3, 0xcb, 0x90, 0xbc, 0x0c, 0xd6, 0x6e, 0x54, 0xa1, 0xc7, 0xd6, 0x58, 0xe3, 0xb8, 0x66, 0x92, 0xa5, - 0xf1, 0xd7, 0xf0, 0x51, 0x3f, 0xbc, 0x5c, 0x45, 0xeb, 0xa6, 0x5a, 0xb4, 0x5e, 0x5b, 0x39, 0xcc, 0x97, 0xbc, 0xf8, - 0x85, 0x2d, 0xb6, 0xf0, 0x7a, 0xb1, 0xb9, 0x8f, 0xa8, 0x4c, 0x25, 0xaa, 0xd3, 0x4a, 0x54, 0xa6, 0x89, 0xc9, 0xcf, - 0x9e, 0x77, 0x01, 0x5c, 0x7f, 0xd4, 0xa9, 0x55, 0xfc, 0xa8, 0x32, 0xaa, 0xfc, 0x51, 0x23, 0x54, 0xeb, 0x13, 0x40, - 0x94, 0xc0, 0xac, 0x91, 0x87, 0x99, 0xb5, 0x61, 0x32, 0x29, 0xeb, 0x0b, 0x72, 0x85, 0xc3, 0xb4, 0xaa, 0x56, 0xbd, - 0xff, 0xe5, 0x86, 0xeb, 0x2f, 0x9b, 0xfc, 0x6e, 0xc6, 0xf5, 0xbe, 0x92, 0xfd, 0x60, 0x39, 0x18, 0x2c, 0xb4, 0xf1, - 0xe3, 0x16, 0xea, 0x7e, 0x8c, 0x1e, 0x86, 0xe0, 0xca, 0xf4, 0x35, 0x88, 0xfa, 0x95, 0x20, 0xf3, 0x23, 0xf2, 0x5a, - 0x01, 0x72, 0xb1, 0xd7, 0x37, 0xd2, 0xbb, 0xd6, 0x20, 0x1a, 0x1b, 0x12, 0xa9, 0xb3, 0x48, 0x86, 0x21, 0xb5, 0x7d, - 0xb8, 0xce, 0xe8, 0x68, 0xde, 0xe4, 0x2d, 0xbe, 0xa9, 0x86, 0x26, 0xcc, 0xb7, 0x0a, 0xab, 0x91, 0x9e, 0x93, 0xa6, - 0x29, 0x69, 0x56, 0xf6, 0x4d, 0xd0, 0xaf, 0x5e, 0x44, 0x26, 0x34, 0x06, 0x5f, 0x64, 0x70, 0xe0, 0xb7, 0x2b, 0x3a, - 0x0a, 0xd1, 0x4f, 0x79, 0x73, 0xff, 0x55, 0x30, 0x4a, 0xfd, 0x00, 0xb1, 0x6f, 0xd1, 0x05, 0x66, 0x67, 0x05, 0x7c, - 0x0b, 0xeb, 0xed, 0x05, 0x79, 0x94, 0xc6, 0xce, 0xc2, 0x11, 0x35, 0x61, 0xad, 0xf7, 0xb0, 0x31, 0xb2, 0xde, 0xf9, - 0xe7, 0xba, 0x2b, 0x51, 0x84, 0x4b, 0xcf, 0x65, 0x82, 0xea, 0xc0, 0x45, 0xe5, 0x5b, 0x6a, 0x2f, 0xa5, 0x09, 0xa7, - 0x22, 0x5f, 0x0c, 0x1b, 0xde, 0x87, 0xc3, 0xbe, 0x86, 0xc3, 0x0f, 0x7c, 0xd3, 0x5a, 0x6b, 0x26, 0x5b, 0x35, 0xb2, - 0x0b, 0x2e, 0xb8, 0xc0, 0xb0, 0x83, 0x81, 0xf5, 0xec, 0x7d, 0x1e, 0x82, 0xa6, 0x03, 0x61, 0xc1, 0x58, 0x34, 0xb7, - 0x01, 0x4f, 0x3e, 0x60, 0xa0, 0xd4, 0xcd, 0xdb, 0x6d, 0xd9, 0x22, 0xb9, 0x0d, 0x0c, 0xa9, 0xc5, 0x38, 0xcb, 0xe2, - 0xc0, 0x99, 0x3a, 0x9b, 0xe7, 0xfa, 0x46, 0x19, 0x77, 0xad, 0x44, 0x49, 0x1b, 0xbf, 0x9e, 0x0d, 0xee, 0x19, 0x29, - 0x74, 0x93, 0xff, 0x2f, 0x21, 0xe5, 0x5d, 0xae, 0x0a, 0x82, 0x6e, 0x70, 0xd2, 0xd7, 0x5a, 0xba, 0xc8, 0x4c, 0x44, - 0x77, 0x80, 0x2b, 0x68, 0x52, 0xae, 0x3d, 0x41, 0xd2, 0x67, 0x2b, 0xb7, 0x39, 0x11, 0xdb, 0x3d, 0x23, 0x9d, 0xd9, - 0x12, 0xea, 0xf3, 0x0b, 0x56, 0x97, 0x77, 0xee, 0x28, 0xe5, 0x73, 0x65, 0xcc, 0x78, 0x24, 0xcd, 0xb3, 0x3f, 0x4a, - 0x21, 0x4d, 0xc6, 0xf5, 0x22, 0x32, 0x9f, 0x97, 0xdb, 0x5b, 0x81, 0x07, 0x9e, 0xaa, 0xc0, 0x10, 0xb4, 0xde, 0x4b, - 0x7c, 0xf3, 0x55, 0x0d, 0xca, 0x3a, 0x19, 0x3f, 0x3e, 0x56, 0xbf, 0x35, 0xf6, 0xa2, 0xd2, 0xe4, 0x10, 0xcd, 0xd4, - 0x76, 0x5a, 0xe7, 0x2d, 0xa8, 0x65, 0x6a, 0xd7, 0x09, 0xa4, 0xd1, 0x39, 0xcf, 0x56, 0x91, 0x98, 0x6a, 0xfe, 0x6b, - 0xa6, 0x84, 0xbe, 0xaf, 0x50, 0x25, 0xfe, 0x19, 0x17, 0x89, 0x30, 0xe2, 0x3c, 0x50, 0x0f, 0x61, 0xd5, 0x12, 0x87, - 0xca, 0xbb, 0x31, 0x8c, 0x0b, 0x66, 0xe1, 0x10, 0x1b, 0x65, 0x7e, 0x16, 0x93, 0x4f, 0x97, 0x05, 0x4f, 0xce, 0xaf, - 0x64, 0x99, 0x75, 0xb3, 0x4f, 0x88, 0x87, 0x47, 0xed, 0x21, 0xd5, 0x2d, 0x0b, 0xad, 0x59, 0x59, 0x2f, 0xca, 0x6e, - 0xd4, 0x3e, 0x8b, 0x2f, 0xc8, 0x68, 0xd2, 0x1e, 0x78, 0xdc, 0x4b, 0x12, 0x48, 0x55, 0x2d, 0xdb, 0xcc, 0x21, 0xf2, - 0xf0, 0xf2, 0x21, 0x4f, 0xf9, 0xac, 0x4c, 0x54, 0x94, 0xb6, 0xc3, 0xe0, 0x81, 0xfb, 0x3a, 0x9a, 0xa0, 0x53, 0xac, - 0xd3, 0x15, 0x44, 0x6f, 0xc0, 0xac, 0x37, 0x8a, 0x3d, 0xab, 0xac, 0x4a, 0x36, 0xed, 0xe5, 0x1a, 0xbf, 0x71, 0x90, - 0x16, 0xdc, 0xd8, 0xe3, 0x48, 0x5d, 0x2f, 0x4a, 0xd7, 0xf5, 0x66, 0x6f, 0xb9, 0xd3, 0xea, 0x03, 0x3e, 0xfd, 0x94, - 0x6c, 0xc9, 0x5f, 0x2f, 0x5d, 0x93, 0x56, 0x6e, 0x0b, 0x1a, 0x35, 0xd6, 0x64, 0xbc, 0xe9, 0x4b, 0x10, 0x15, 0x55, - 0x09, 0x9e, 0x53, 0x7e, 0x36, 0x8c, 0x46, 0x32, 0xd1, 0x80, 0x7c, 0x69, 0xed, 0x7e, 0xae, 0x55, 0xbc, 0xb5, 0x3a, - 0x74, 0xca, 0xea, 0xe0, 0xf8, 0xd2, 0xb9, 0xd9, 0xba, 0x28, 0x14, 0x20, 0x01, 0xf6, 0x50, 0x43, 0x8e, 0x9d, 0xd5, - 0x8c, 0xdb, 0xdb, 0x6f, 0x1b, 0x31, 0x90, 0x62, 0x2e, 0xfb, 0xae, 0x1f, 0x22, 0x64, 0x32, 0x23, 0x4c, 0xac, 0x91, - 0xdd, 0x18, 0xa3, 0x09, 0x09, 0xc9, 0x78, 0x2d, 0x84, 0x84, 0xde, 0xea, 0x3c, 0x01, 0x5c, 0x12, 0x4f, 0x06, 0x6b, - 0x0a, 0x66, 0x45, 0x5e, 0x57, 0x5b, 0x71, 0x25, 0x16, 0x89, 0xf0, 0x75, 0x51, 0x23, 0xdb, 0x26, 0x58, 0x41, 0x8b, - 0x62, 0x0e, 0x14, 0x3a, 0xf3, 0x0d, 0x5f, 0x30, 0xe1, 0x9c, 0xdf, 0x75, 0x6b, 0x94, 0x96, 0x99, 0xa0, 0xaf, 0x1a, - 0x16, 0xb6, 0xdb, 0x2f, 0x10, 0xd7, 0x34, 0x03, 0x03, 0x8a, 0xb2, 0x43, 0x35, 0xbf, 0x03, 0x4b, 0x94, 0x9c, 0xb4, - 0x91, 0x9b, 0x5f, 0xa1, 0x63, 0x82, 0x18, 0x58, 0x68, 0x6c, 0x2f, 0x64, 0x2b, 0xd1, 0x9a, 0x1a, 0xb2, 0x11, 0x36, - 0xc1, 0x07, 0xa7, 0xa7, 0x70, 0x6d, 0x02, 0x55, 0xd3, 0x94, 0x46, 0x60, 0x9e, 0xf0, 0x40, 0x69, 0xbe, 0x9f, 0x5d, - 0x10, 0xd8, 0x98, 0xb7, 0x22, 0x8b, 0x03, 0x92, 0x12, 0x1b, 0x58, 0x78, 0xd4, 0x58, 0x2f, 0xef, 0x34, 0x8e, 0x2e, - 0xab, 0x51, 0x57, 0xa4, 0x58, 0x2a, 0x69, 0xc6, 0xa2, 0xc1, 0x98, 0x92, 0x90, 0x64, 0x2d, 0x04, 0x6b, 0x36, 0xfc, - 0xcd, 0x7b, 0x54, 0x7f, 0x6b, 0x2e, 0x60, 0x4f, 0x30, 0xdb, 0x79, 0x31, 0xbd, 0xbe, 0x1a, 0x65, 0xdb, 0xba, 0x85, - 0x79, 0x4f, 0x61, 0xd0, 0x9c, 0x8f, 0x29, 0x65, 0xce, 0x33, 0x40, 0x99, 0x49, 0x16, 0x01, 0x39, 0x0c, 0xb8, 0x07, - 0xa4, 0x2b, 0x2f, 0x0e, 0x7b, 0x19, 0xad, 0x9c, 0xb9, 0xf0, 0xf2, 0x72, 0x75, 0x34, 0x41, 0x39, 0x50, 0xe4, 0xc0, - 0x8b, 0xeb, 0x17, 0x4f, 0x73, 0x2d, 0x56, 0x59, 0x6f, 0x58, 0xa8, 0xae, 0xb7, 0xf4, 0xb9, 0xf5, 0x31, 0xf0, 0xf9, - 0xb5, 0x96, 0x5a, 0x34, 0x7f, 0xe8, 0xb5, 0xd5, 0xa4, 0xa0, 0xdd, 0xbf, 0xf5, 0x64, 0x14, 0x39, 0xa5, 0xd5, 0xb2, - 0xf8, 0x54, 0xbb, 0xe8, 0x29, 0x5a, 0xca, 0x26, 0xef, 0xb2, 0x87, 0xaa, 0x0b, 0xb3, 0xd6, 0x51, 0x98, 0xd5, 0xf6, - 0x28, 0xef, 0xec, 0xbd, 0x36, 0x0b, 0xca, 0x94, 0x56, 0x9f, 0x70, 0xbd, 0xf1, 0x02, 0xaa, 0xf9, 0x96, 0x1a, 0x8b, - 0x63, 0x7e, 0x49, 0x5d, 0xd9, 0x28, 0x7b, 0x9e, 0xd6, 0xb8, 0xbc, 0xeb, 0xa2, 0x17, 0x53, 0xc0, 0x29, 0xca, 0x4a, - 0x17, 0x37, 0x72, 0x09, 0x5d, 0x2b, 0xd2, 0x1a, 0xf8, 0x0c, 0x8c, 0x62, 0xb2, 0x9a, 0x7c, 0x90, 0xf4, 0xdf, 0x99, - 0xf5, 0x57, 0x9d, 0xb9, 0xfe, 0xa6, 0x17, 0xae, 0xbf, 0x5e, 0x54, 0x56, 0x85, 0x7d, 0x63, 0xec, 0x19, 0x70, 0x05, - 0x93, 0x4a, 0x7c, 0xa5, 0x73, 0x8e, 0x08, 0x6a, 0x0c, 0x15, 0xb2, 0x9b, 0x2f, 0x2c, 0x6c, 0x88, 0x3c, 0x3b, 0xbb, - 0xcc, 0xd2, 0x35, 0xd6, 0x98, 0xdc, 0xdf, 0xbb, 0x82, 0x59, 0x17, 0x56, 0x2d, 0xe3, 0x55, 0xce, 0xa5, 0x28, 0x92, - 0x62, 0x72, 0x91, 0x83, 0x14, 0x21, 0x80, 0x90, 0x8b, 0x24, 0xd0, 0x51, 0xda, 0xa2, 0x68, 0x24, 0x14, 0x80, 0xfa, - 0x72, 0xbe, 0x05, 0x08, 0x1c, 0x82, 0x39, 0x21, 0x08, 0x46, 0xf2, 0x2c, 0x20, 0x72, 0x42, 0xf6, 0x4e, 0x54, 0x88, - 0x30, 0xab, 0x83, 0x13, 0x68, 0x50, 0x16, 0xd8, 0xa2, 0x79, 0x99, 0x09, 0x8a, 0x2a, 0x44, 0x84, 0x65, 0xc5, 0xe5, - 0xea, 0x8f, 0x2e, 0x6d, 0xbd, 0x5c, 0x53, 0xe8, 0x92, 0xe5, 0xd3, 0xec, 0x1a, 0xca, 0xfc, 0x00, 0xfc, 0x6b, 0x51, - 0x07, 0xf6, 0xb4, 0x83, 0x34, 0xb0, 0x15, 0x17, 0xa7, 0xe2, 0xfa, 0xe7, 0x9c, 0x02, 0x42, 0x49, 0x4f, 0x2b, 0xc4, - 0x5c, 0xa0, 0x73, 0x0f, 0x71, 0x0d, 0x0a, 0x80, 0xe1, 0x92, 0xf1, 0xc2, 0x50, 0xdb, 0x7a, 0x7a, 0xea, 0xfc, 0x1e, - 0xc9, 0x35, 0x3a, 0x34, 0xf1, 0x22, 0xca, 0xdc, 0x65, 0x61, 0x3b, 0x52, 0xbc, 0xe7, 0x46, 0xb5, 0xc7, 0x94, 0x97, - 0xe7, 0x7b, 0xe8, 0x2f, 0xc4, 0xbc, 0x6d, 0x82, 0xaa, 0xa7, 0x7b, 0x6f, 0xad, 0x8b, 0xc0, 0x8f, 0x5e, 0x16, 0x05, - 0xea, 0xdb, 0x15, 0x23, 0x0d, 0x3d, 0xd9, 0xb1, 0x42, 0x97, 0x69, 0x59, 0xdb, 0xbb, 0xcd, 0xfa, 0xb6, 0x06, 0x83, - 0x8c, 0x7d, 0xa5, 0x78, 0x05, 0x84, 0x4d, 0xf1, 0x64, 0x26, 0xda, 0x6a, 0x08, 0x4e, 0x10, 0xca, 0xe9, 0xea, 0xf0, - 0xad, 0x20, 0x45, 0x45, 0x60, 0xeb, 0x7e, 0xac, 0x3d, 0xd4, 0xbe, 0x1b, 0x4a, 0xa7, 0x67, 0x8f, 0x1a, 0x1c, 0x3d, - 0xe5, 0x05, 0x3b, 0x34, 0x52, 0x97, 0x16, 0x21, 0x55, 0xf1, 0xb6, 0x01, 0xab, 0xf4, 0xe0, 0xd3, 0x06, 0x33, 0x9f, - 0xb1, 0xe2, 0x0e, 0x72, 0x15, 0x1b, 0xd1, 0x08, 0x05, 0xdd, 0x23, 0xf2, 0xf3, 0xfe, 0x82, 0x3d, 0x37, 0xe6, 0x56, - 0xf0, 0x4b, 0xc0, 0x30, 0xd5, 0x2b, 0x4c, 0x58, 0x2d, 0x8d, 0x16, 0x60, 0xe9, 0x8d, 0x67, 0xab, 0x66, 0xaf, 0x7c, - 0x2a, 0x95, 0x14, 0xab, 0x10, 0xbe, 0x53, 0x65, 0x05, 0x27, 0x1f, 0x62, 0x30, 0xc4, 0x4f, 0xdf, 0x56, 0x7e, 0xbd, - 0xea, 0xe6, 0x50, 0xf1, 0xa8, 0xb1, 0xa7, 0x3d, 0x8c, 0x92, 0xda, 0xf0, 0xaa, 0xc3, 0x10, 0x77, 0x67, 0xd9, 0x99, - 0x3d, 0x45, 0xd6, 0x54, 0x02, 0xf8, 0x15, 0x9b, 0xa2, 0x2e, 0x83, 0x8f, 0x88, 0x79, 0x23, 0x60, 0xfe, 0x66, 0x50, - 0x8c, 0xe6, 0x4d, 0x15, 0xad, 0x16, 0xf7, 0x26, 0x74, 0xc9, 0xb8, 0x44, 0xd9, 0xd3, 0x87, 0xf4, 0x3b, 0x24, 0x18, - 0x39, 0xdd, 0xac, 0xb8, 0xaf, 0x07, 0x87, 0x63, 0x1f, 0x5b, 0x97, 0x30, 0x05, 0x40, 0x8b, 0x5c, 0x4c, 0x80, 0xe9, - 0x7a, 0xcd, 0xb1, 0x90, 0xad, 0x0b, 0x49, 0x34, 0x34, 0x85, 0xa2, 0x6e, 0x41, 0x30, 0x31, 0x2a, 0xed, 0xf6, 0x83, - 0xb4, 0x30, 0x9e, 0x33, 0x95, 0x5f, 0x90, 0x1f, 0x4e, 0x7d, 0xd9, 0x1a, 0x7b, 0xa3, 0x63, 0x56, 0x34, 0xf1, 0xa4, - 0x99, 0x80, 0x48, 0x00, 0x2f, 0x17, 0xd1, 0x66, 0x9c, 0xa7, 0x92, 0x9a, 0xd7, 0x76, 0x81, 0x98, 0x01, 0x02, 0x9d, - 0x6a, 0x49, 0xa5, 0x78, 0x73, 0x3e, 0x48, 0x71, 0x10, 0x80, 0xb2, 0x63, 0x36, 0xb4, 0xa5, 0xa0, 0x1e, 0x32, 0xb4, - 0xd9, 0x5c, 0xdb, 0x5a, 0xee, 0xd4, 0xd9, 0xac, 0x45, 0x6d, 0x99, 0x3f, 0xdc, 0xe6, 0x17, 0x11, 0xe3, 0xa2, 0xee, - 0x13, 0x09, 0xd5, 0x14, 0x23, 0xd0, 0x79, 0x02, 0xf2, 0x7a, 0x38, 0xe1, 0xcd, 0x7d, 0xbf, 0x6f, 0xe9, 0x9a, 0x64, - 0xf1, 0xa2, 0xc0, 0xb9, 0x2f, 0x53, 0x78, 0x99, 0x70, 0x02, 0x97, 0x78, 0xa8, 0x33, 0x1f, 0x67, 0x5b, 0x9d, 0x29, - 0x46, 0xa0, 0xa4, 0x16, 0x91, 0x4d, 0x7a, 0x43, 0x90, 0x9a, 0xf1, 0x32, 0x10, 0x6a, 0x47, 0xa9, 0x01, 0xc9, 0xfb, - 0xba, 0x32, 0x5e, 0x4b, 0xb6, 0x2e, 0x42, 0xd9, 0x6c, 0xc7, 0xb5, 0xbb, 0x9c, 0x4e, 0x77, 0x37, 0x2b, 0xe4, 0x0e, - 0x28, 0x9d, 0x0d, 0x97, 0x11, 0xdf, 0xd0, 0xec, 0x40, 0x81, 0xd0, 0x6e, 0xdf, 0x66, 0x65, 0xcc, 0xc2, 0xe2, 0x75, - 0x43, 0x8e, 0x4a, 0xfe, 0x50, 0xde, 0x9d, 0xf5, 0x6e, 0xc3, 0x53, 0xdb, 0xc1, 0x7a, 0x50, 0x28, 0xfb, 0xd8, 0xa7, - 0x46, 0xe1, 0x0f, 0xdc, 0x2a, 0x91, 0x75, 0x08, 0xeb, 0xec, 0xc2, 0x3b, 0x2a, 0xd3, 0x31, 0x6d, 0x3b, 0x9b, 0x87, - 0xcd, 0x46, 0x41, 0xba, 0x2c, 0xe1, 0x78, 0x6d, 0xa5, 0xee, 0xd4, 0xc3, 0x73, 0x37, 0x4a, 0xdf, 0x97, 0x58, 0x5e, - 0xb6, 0x51, 0xf7, 0x76, 0x12, 0x4b, 0xf8, 0xcc, 0x3a, 0x71, 0x09, 0xee, 0x80, 0xb9, 0xca, 0x4e, 0x44, 0x2d, 0x90, - 0xd4, 0x7f, 0xe1, 0xe5, 0x8f, 0x06, 0xe3, 0x92, 0x93, 0xab, 0x5e, 0x4d, 0x20, 0x31, 0x13, 0x32, 0x47, 0xab, 0x77, - 0x03, 0x9a, 0x82, 0xae, 0x6b, 0x91, 0x03, 0x02, 0x4f, 0x6c, 0x7a, 0xf9, 0xed, 0x08, 0xe2, 0xec, 0x2e, 0x27, 0x34, - 0xac, 0xe1, 0x59, 0x76, 0xb1, 0x92, 0xb1, 0x6b, 0x8f, 0xa7, 0xc7, 0x2e, 0x95, 0x96, 0x4d, 0x18, 0xf3, 0xdb, 0xba, - 0xde, 0x28, 0x9e, 0x22, 0xa6, 0x5d, 0x9c, 0xca, 0x18, 0xae, 0x56, 0x9f, 0xe3, 0x79, 0x51, 0x05, 0x49, 0x5c, 0x12, - 0xa5, 0x37, 0xd6, 0x6f, 0xb9, 0x1c, 0x55, 0x15, 0xcb, 0xd9, 0xf9, 0x6d, 0xca, 0xab, 0xdf, 0x83, 0x7f, 0x7c, 0x95, - 0xb1, 0x08, 0xaa, 0x8c, 0xc8, 0x8c, 0x7d, 0x74, 0x11, 0x2d, 0xf4, 0xb3, 0xb6, 0x74, 0x15, 0x5d, 0xaf, 0xcc, 0x6b, - 0x88, 0x20, 0x70, 0xab, 0xea, 0x14, 0x52, 0x66, 0xd1, 0x98, 0x67, 0x15, 0xb3, 0x6b, 0x3d, 0xc6, 0x31, 0x67, 0x03, - 0xe1, 0x26, 0x93, 0x13, 0x24, 0x27, 0xe1, 0x33, 0x95, 0xd9, 0x96, 0x11, 0xf5, 0xc8, 0x6b, 0xa4, 0x8b, 0x9a, 0x35, - 0xe7, 0x6d, 0xd7, 0x59, 0xbc, 0x60, 0x71, 0xde, 0xaf, 0x6e, 0x44, 0x42, 0x80, 0x70, 0x11, 0xfe, 0x1c, 0xc0, 0xff, - 0x6d, 0x33, 0xc5, 0xfd, 0xdd, 0xfc, 0x92, 0x77, 0x4d, 0x1b, 0x07, 0xe0, 0x80, 0x82, 0xc5, 0xc9, 0xe0, 0x02, 0xc9, - 0x08, 0x43, 0xbd, 0x42, 0xb4, 0xc1, 0x52, 0x31, 0xce, 0x2d, 0x3d, 0x8f, 0xec, 0x68, 0xd0, 0xa7, 0xe5, 0xc4, 0x5c, - 0x79, 0x83, 0x31, 0x5b, 0xa3, 0x12, 0x42, 0xed, 0x08, 0x31, 0x85, 0xc9, 0x74, 0x56, 0x16, 0x25, 0x7f, 0x15, 0x26, - 0xb4, 0x82, 0x49, 0x4a, 0x9b, 0x51, 0x63, 0x88, 0x8d, 0x8a, 0x50, 0xbd, 0xe7, 0x94, 0x35, 0x04, 0x73, 0x7b, 0x42, - 0xfa, 0x35, 0x44, 0xd7, 0x3f, 0xd6, 0xcf, 0x13, 0x4e, 0x6a, 0xdb, 0xf9, 0xba, 0xd0, 0x82, 0x83, 0x6b, 0x2a, 0xaa, - 0x72, 0x35, 0x0c, 0x51, 0x40, 0xa1, 0xd4, 0x91, 0x3a, 0xd4, 0x12, 0x59, 0x9b, 0x55, 0x3a, 0xd9, 0x61, 0xb4, 0x9c, - 0x4c, 0x89, 0x2b, 0x48, 0x6b, 0x5d, 0x39, 0x57, 0xbe, 0xd1, 0x97, 0x6d, 0xd0, 0x1b, 0x8d, 0x44, 0x2e, 0x3b, 0x8f, - 0x3f, 0xdf, 0xfa, 0x1c, 0xa0, 0xd6, 0xff, 0x6a, 0xed, 0x72, 0xc9, 0x02, 0x76, 0xb1, 0xab, 0x23, 0xf1, 0x7e, 0xde, - 0x0a, 0xb8, 0xbe, 0x10, 0x08, 0x75, 0xdd, 0x85, 0x72, 0xd2, 0x15, 0xab, 0xa2, 0x5f, 0xbe, 0x47, 0xd1, 0xac, 0xb7, - 0x11, 0x94, 0x4d, 0x90, 0xd6, 0xbb, 0x3a, 0x0e, 0x29, 0x21, 0x51, 0x59, 0x4c, 0x75, 0x61, 0x8d, 0x1e, 0xe8, 0x0e, - 0x5b, 0x45, 0x34, 0xa7, 0xe9, 0x26, 0xfb, 0xfe, 0x50, 0xa1, 0x04, 0x22, 0xfc, 0xbf, 0x7b, 0xd3, 0x33, 0xd0, 0x20, - 0x49, 0x5d, 0x80, 0x4a, 0x49, 0xfb, 0x85, 0xd3, 0xfe, 0x50, 0x65, 0x0b, 0x80, 0xc2, 0x1e, 0x6f, 0x14, 0x6d, 0xcb, - 0xef, 0x66, 0x3d, 0x28, 0xd1, 0xfa, 0x3f, 0x2a, 0x43, 0x16, 0x10, 0x6d, 0x47, 0xd7, 0x6a, 0xe9, 0x95, 0x4f, 0x52, - 0x0c, 0x47, 0x13, 0x62, 0xfb, 0x9d, 0xbe, 0x7c, 0x87, 0xea, 0xc2, 0x5a, 0xe2, 0xdc, 0x4b, 0x6a, 0x4b, 0x16, 0xb0, - 0x9f, 0x31, 0x62, 0xba, 0x51, 0xc1, 0x2f, 0x1f, 0x75, 0xb9, 0x9a, 0x85, 0xab, 0x21, 0x60, 0x66, 0x5f, 0x5d, 0xf1, - 0x20, 0x58, 0xc0, 0xd4, 0xb0, 0x30, 0x63, 0xc7, 0x51, 0x9f, 0x39, 0x96, 0xb2, 0xcf, 0x7d, 0x46, 0xd7, 0x37, 0xc7, - 0xfe, 0x11, 0xeb, 0xf6, 0x5b, 0xec, 0x8a, 0x71, 0x3c, 0xb0, 0xaf, 0x2e, 0xb2, 0x81, 0x69, 0x42, 0x92, 0xf5, 0xcb, - 0x29, 0x90, 0xaa, 0xd5, 0x83, 0x98, 0xab, 0x3a, 0x01, 0x8c, 0xf6, 0x5d, 0x51, 0xf0, 0x88, 0x1c, 0x7f, 0x22, 0x8d, - 0x0e, 0x98, 0xe2, 0x0e, 0x84, 0x30, 0x74, 0x47, 0xbc, 0xd9, 0x5b, 0x81, 0x60, 0x44, 0xbb, 0x20, 0xfc, 0x8d, 0xf3, - 0x12, 0x5b, 0xd0, 0x36, 0x5a, 0x2f, 0x02, 0x68, 0x88, 0x44, 0xf2, 0x63, 0xe4, 0xf9, 0x70, 0x76, 0xee, 0x41, 0x31, - 0xdc, 0xa4, 0x2e, 0x88, 0xeb, 0xe9, 0x05, 0xdb, 0x65, 0x42, 0x32, 0x51, 0xe8, 0xd0, 0x14, 0x58, 0x59, 0x3b, 0x71, - 0x3a, 0xc0, 0x87, 0xf7, 0xf7, 0xf0, 0xc0, 0x76, 0x54, 0xfc, 0x40, 0x02, 0xb7, 0x2f, 0xac, 0xe0, 0x50, 0x67, 0xc1, - 0x0c, 0x3a, 0xe0, 0x91, 0xde, 0xa7, 0x46, 0x8c, 0x66, 0xd6, 0x3b, 0x40, 0x14, 0x11, 0x65, 0xb6, 0x4d, 0x6e, 0x87, - 0xbb, 0xe3, 0x29, 0x10, 0x20, 0x63, 0x5a, 0x15, 0x96, 0x61, 0x26, 0xb0, 0xc4, 0x7c, 0x33, 0xbe, 0x68, 0xd1, 0x8f, - 0xfd, 0x3e, 0xaa, 0xe4, 0xa2, 0x52, 0x83, 0xb1, 0x8d, 0x79, 0x63, 0x8b, 0x9e, 0xe0, 0x1b, 0x8d, 0x74, 0xf4, 0x0c, - 0x63, 0xb9, 0x84, 0x39, 0x58, 0xe9, 0x1c, 0xf5, 0x23, 0x58, 0x51, 0x05, 0x88, 0xb3, 0x1f, 0xa7, 0x48, 0x0d, 0x98, - 0x25, 0x3f, 0xa4, 0x45, 0x4d, 0x4e, 0x03, 0x7e, 0xcd, 0x40, 0xcf, 0x1e, 0x55, 0xc6, 0x3d, 0x79, 0x09, 0x5c, 0x9a, - 0xde, 0x7a, 0xda, 0x77, 0x6f, 0xc0, 0x31, 0x16, 0xe4, 0x0d, 0xe6, 0xec, 0x4e, 0x30, 0xc5, 0x8a, 0x6d, 0x5d, 0x2d, - 0xf3, 0x6a, 0xfd, 0x40, 0x67, 0x25, 0x18, 0x4e, 0x93, 0x48, 0xe2, 0x04, 0x4c, 0xa3, 0x18, 0x7f, 0x60, 0xbb, 0xbc, - 0xdb, 0xea, 0x13, 0xbf, 0x0d, 0x7f, 0x1d, 0x29, 0x55, 0x9f, 0x7f, 0x12, 0x0b, 0x33, 0x99, 0xd8, 0x6f, 0xe4, 0xe8, - 0x0c, 0x32, 0x2b, 0xf0, 0x55, 0x3d, 0xe3, 0x59, 0xf2, 0x5c, 0x79, 0xca, 0xcd, 0x8a, 0x2d, 0xb3, 0xe0, 0xe7, 0x51, - 0x49, 0x8d, 0xbd, 0x19, 0xd5, 0xa9, 0x56, 0x8c, 0x51, 0x9d, 0x9e, 0x1c, 0x08, 0x97, 0x29, 0xc0, 0x2a, 0x3b, 0x80, - 0xc6, 0xf3, 0xeb, 0xd2, 0x23, 0x11, 0xd9, 0x2a, 0xa6, 0x1e, 0x83, 0x97, 0x8a, 0xa0, 0x77, 0x10, 0x85, 0x18, 0x1c, - 0x49, 0xdf, 0x68, 0xf5, 0xc5, 0x9f, 0xf8, 0x7d, 0xaf, 0x97, 0x70, 0x17, 0xec, 0x7c, 0x53, 0x63, 0xe9, 0x2c, 0x41, - 0x63, 0xf6, 0x3f, 0x87, 0xac, 0x45, 0x58, 0xe4, 0x34, 0xd3, 0x10, 0x34, 0x41, 0xf1, 0x47, 0xd0, 0xc0, 0x66, 0x4d, - 0xd7, 0x7a, 0x13, 0x94, 0x51, 0x48, 0x82, 0xff, 0x57, 0x19, 0x2f, 0x87, 0x2a, 0x27, 0x93, 0xa8, 0x05, 0xf7, 0x89, - 0x9b, 0x6a, 0x68, 0x05, 0xea, 0xec, 0xe1, 0x29, 0xf4, 0x64, 0x2c, 0xa2, 0x67, 0x58, 0xc4, 0x46, 0x9b, 0xc0, 0x78, - 0x24, 0xf3, 0xb0, 0x2e, 0xa2, 0xdd, 0x72, 0x36, 0xc5, 0x57, 0x76, 0xcc, 0xdb, 0x6e, 0x1f, 0xbb, 0x09, 0x95, 0x78, - 0xfa, 0x7d, 0x57, 0xcc, 0xbe, 0xc7, 0xbe, 0x94, 0xd2, 0x3d, 0x70, 0x58, 0x4a, 0xeb, 0x22, 0x28, 0x9c, 0x3a, 0xd8, - 0x02, 0x9a, 0xec, 0xe4, 0x6c, 0x1a, 0x25, 0x58, 0x9c, 0xb9, 0x49, 0xc0, 0xaf, 0x74, 0x12, 0x42, 0x2a, 0x1b, 0xbe, - 0x63, 0x2d, 0xf9, 0x2b, 0x90, 0x6b, 0xfe, 0xe2, 0x69, 0x20, 0x44, 0x6d, 0x23, 0x14, 0x01, 0x6b, 0xe2, 0xca, 0xbc, - 0x33, 0x08, 0xae, 0xe8, 0x2f, 0x7b, 0x0d, 0xff, 0xdc, 0x98, 0xf6, 0xad, 0x90, 0xda, 0xd0, 0xc1, 0x5a, 0x44, 0xc6, - 0xf3, 0x50, 0xf8, 0x6f, 0xf8, 0xd8, 0x73, 0x84, 0x48, 0x22, 0x17, 0xc9, 0x8f, 0x28, 0x6e, 0x31, 0xdd, 0x42, 0xb9, - 0xb5, 0x9d, 0x8f, 0x23, 0x61, 0xd0, 0x3c, 0x6a, 0xf5, 0x92, 0x94, 0xf7, 0xd4, 0x6a, 0xe6, 0x1e, 0x05, 0xb7, 0x8b, - 0xa5, 0x86, 0x17, 0x88, 0xd2, 0xd5, 0x0f, 0x0a, 0xcd, 0xe2, 0x3f, 0x66, 0xb5, 0x79, 0xea, 0xf6, 0x51, 0xc9, 0x37, - 0xc9, 0xca, 0x91, 0x05, 0x27, 0x51, 0xf8, 0x43, 0x08, 0xbc, 0xd4, 0x19, 0x4f, 0xf5, 0x36, 0x62, 0x1e, 0x0a, 0x4d, - 0x41, 0xae, 0x07, 0xed, 0x13, 0x4d, 0x8e, 0xdc, 0x90, 0x63, 0x7a, 0xd0, 0x3e, 0xac, 0x81, 0xed, 0x08, 0x71, 0x71, - 0x9f, 0x88, 0xe1, 0xb4, 0xea, 0x72, 0x02, 0xe4, 0xce, 0x79, 0xd2, 0x32, 0x04, 0x35, 0x72, 0x13, 0xd4, 0xb8, 0x73, - 0x9c, 0xda, 0x45, 0xd1, 0xed, 0x4b, 0x2e, 0x91, 0x62, 0x94, 0xe9, 0xbe, 0xf4, 0xdf, 0xab, 0xad, 0xa2, 0x01, 0x64, - 0x03, 0xbe, 0xde, 0x7b, 0xc7, 0xe8, 0x00, 0xf5, 0x72, 0xeb, 0xa6, 0x6c, 0x5e, 0x9e, 0xd3, 0x6c, 0x6b, 0xb8, 0xc7, - 0xd0, 0xfe, 0x12, 0xea, 0x9c, 0xfb, 0xac, 0xf8, 0xad, 0xbc, 0x0b, 0xc4, 0xe4, 0xe4, 0x66, 0x23, 0x4f, 0x93, 0x75, - 0x84, 0x75, 0x8f, 0xa1, 0xb9, 0x88, 0x7f, 0x69, 0xac, 0x5c, 0x10, 0x9e, 0x58, 0xc9, 0x82, 0xbf, 0x30, 0xcc, 0x60, - 0x53, 0x79, 0x4d, 0x7f, 0x87, 0x39, 0x80, 0xf7, 0xdb, 0xcd, 0x5a, 0x41, 0x3e, 0x25, 0xb5, 0xe3, 0x6b, 0xad, 0xe3, - 0x97, 0x6f, 0xd0, 0x83, 0xd4, 0xc4, 0x63, 0x51, 0x3d, 0x10, 0xb3, 0xa4, 0x37, 0x2f, 0x71, 0xf4, 0xcd, 0x4f, 0x9b, - 0x67, 0x5c, 0xe3, 0xb9, 0x08, 0xc9, 0x80, 0xb5, 0xc1, 0xa5, 0xbd, 0x37, 0x12, 0x77, 0x9f, 0x95, 0xa9, 0x45, 0x6b, - 0x63, 0x26, 0x0a, 0xb4, 0xb0, 0xee, 0x12, 0xf1, 0x7c, 0xf9, 0xa6, 0xbf, 0x76, 0xa4, 0x58, 0x9a, 0x8f, 0x64, 0x1e, - 0x55, 0x29, 0xe1, 0x8f, 0x01, 0x8d, 0x7f, 0x43, 0x5e, 0x24, 0x31, 0xd0, 0x60, 0x91, 0x1a, 0x2b, 0xef, 0x13, 0x70, - 0x88, 0xa1, 0x89, 0xa8, 0x4d, 0xb4, 0x13, 0xb8, 0xa3, 0xf1, 0x89, 0xa4, 0x3e, 0x26, 0x95, 0x34, 0x01, 0x1e, 0xdd, - 0xc5, 0xe4, 0x64, 0xec, 0x02, 0x7c, 0x81, 0xc7, 0xc7, 0xd3, 0x6f, 0xda, 0xd5, 0xd1, 0x0d, 0x52, 0x6e, 0x2a, 0xc8, - 0x26, 0x60, 0xad, 0x05, 0xe0, 0x29, 0xd7, 0x44, 0xf3, 0x8e, 0x54, 0xbf, 0x0c, 0x02, 0xf6, 0xbb, 0x8b, 0x7a, 0xee, - 0x4d, 0x63, 0x65, 0xf9, 0x38, 0xf1, 0x52, 0xd3, 0x08, 0xb1, 0x62, 0x9f, 0x71, 0xca, 0x11, 0x11, 0xef, 0xf0, 0x6b, - 0xeb, 0xcd, 0x22, 0xbd, 0x4d, 0x8a, 0x73, 0x93, 0x01, 0x86, 0x91, 0x6b, 0x84, 0x5f, 0xcc, 0xb4, 0xb3, 0x75, 0xe5, - 0xc3, 0x02, 0xc9, 0x68, 0x29, 0xfc, 0xad, 0xc8, 0xac, 0xb6, 0x59, 0x8b, 0x10, 0xff, 0x50, 0xf4, 0xb3, 0x43, 0x69, - 0x14, 0x90, 0x57, 0x5f, 0x2e, 0x2b, 0x36, 0x39, 0x05, 0x9d, 0xf6, 0xb9, 0x79, 0x67, 0x59, 0x7e, 0xfc, 0xf9, 0x8f, - 0x73, 0x3b, 0x61, 0x8b, 0x99, 0x27, 0x6e, 0xb1, 0x8c, 0xb2, 0xf2, 0xa2, 0xd5, 0x79, 0x4b, 0xd6, 0xcd, 0xec, 0xba, - 0x40, 0x09, 0xff, 0xd4, 0x8f, 0x0e, 0x67, 0xe5, 0x0c, 0x7a, 0x85, 0x56, 0x16, 0xf6, 0x28, 0x6d, 0xdf, 0xda, 0x97, - 0x03, 0x9d, 0xc6, 0x5d, 0xd8, 0x1c, 0x27, 0x48, 0x52, 0x79, 0x28, 0x3f, 0xf3, 0x14, 0x67, 0xdf, 0x59, 0x4d, 0x47, - 0x3b, 0x7a, 0xc7, 0xd1, 0xe5, 0x60, 0xb1, 0x43, 0x94, 0xac, 0x0f, 0xce, 0xb6, 0x59, 0x7c, 0x70, 0x94, 0x69, 0x3e, - 0xe3, 0x15, 0x0b, 0xa4, 0x34, 0x4f, 0x9f, 0x22, 0xe8, 0x09, 0x64, 0x62, 0x0c, 0xbd, 0x0b, 0x36, 0x4d, 0x81, 0x63, - 0xce, 0xb7, 0x89, 0xa0, 0xcd, 0x32, 0x9a, 0x45, 0xf4, 0x62, 0x64, 0x29, 0xbc, 0xf6, 0x8e, 0x7a, 0xae, 0x64, 0x5d, - 0x42, 0xab, 0x23, 0xab, 0x1f, 0x6c, 0xf7, 0x69, 0xe1, 0x07, 0xf3, 0xbb, 0xd5, 0x42, 0x7d, 0x65, 0xac, 0x7e, 0x8c, - 0xcc, 0x52, 0xe7, 0x2c, 0x67, 0xb7, 0xd3, 0xd8, 0xc0, 0xeb, 0x64, 0xb3, 0xf5, 0xeb, 0x76, 0x7f, 0xb9, 0xe4, 0xdf, - 0x66, 0xca, 0xdb, 0x24, 0x47, 0xd8, 0xef, 0x13, 0x59, 0x03, 0xb2, 0x3e, 0x6d, 0x71, 0x96, 0x92, 0x3a, 0x56, 0x49, - 0x94, 0x18, 0xdb, 0x09, 0x5c, 0x61, 0x10, 0x12, 0xcf, 0x66, 0x75, 0x25, 0x4c, 0xce, 0xab, 0x78, 0xa7, 0x30, 0x57, - 0x22, 0x59, 0x2c, 0xf2, 0x04, 0x45, 0xda, 0x37, 0xcb, 0xe5, 0xa5, 0x3c, 0x35, 0xa5, 0x1d, 0x09, 0x8d, 0xbc, 0xa4, - 0xff, 0x0c, 0xb8, 0x24, 0x44, 0x2a, 0x50, 0x89, 0xcf, 0x7d, 0x47, 0x2a, 0xd1, 0xa4, 0x8a, 0x52, 0x14, 0xd4, 0xca, - 0xf4, 0x8f, 0x98, 0x97, 0xa6, 0xb4, 0xee, 0x81, 0xc0, 0x75, 0x9b, 0x2b, 0x89, 0xa7, 0x7f, 0x99, 0xcc, 0x2e, 0x00, - 0xe7, 0x65, 0xb9, 0xc1, 0x2f, 0x63, 0xc2, 0xe5, 0xd1, 0x65, 0x4d, 0x20, 0xd8, 0xf1, 0x06, 0x7e, 0x98, 0x48, 0x10, - 0x1c, 0x57, 0x24, 0x22, 0x16, 0x9c, 0xa1, 0x88, 0xa7, 0x60, 0x00, 0x48, 0xce, 0xbf, 0x4f, 0x9f, 0x17, 0x34, 0x7f, - 0x40, 0x54, 0xe1, 0xa8, 0x02, 0xc4, 0x01, 0x09, 0x06, 0x5d, 0x78, 0x27, 0x8b, 0x6c, 0x35, 0x3b, 0x5e, 0x9e, 0x93, - 0xce, 0x9d, 0x45, 0x44, 0x7a, 0x51, 0x12, 0x41, 0x9c, 0x61, 0xf1, 0x83, 0xa0, 0xc4, 0xe8, 0xf5, 0xba, 0x20, 0x8c, - 0x2e, 0x96, 0x64, 0xa3, 0xd1, 0x20, 0x20, 0xfd, 0x23, 0xc4, 0x4c, 0xb6, 0x4b, 0x39, 0x66, 0x5f, 0x7b, 0xc5, 0x39, - 0x6b, 0xcd, 0x10, 0x4a, 0x06, 0x76, 0x6f, 0x09, 0xa4, 0x3a, 0x87, 0x32, 0x9a, 0x4a, 0x53, 0x7e, 0x21, 0x47, 0x50, - 0xeb, 0xd0, 0x6b, 0x93, 0xa1, 0xdf, 0x06, 0x4f, 0x22, 0x20, 0x45, 0x0a, 0xcf, 0x4b, 0x60, 0xc1, 0x64, 0xe7, 0xb6, - 0x64, 0x16, 0x1f, 0x3f, 0xa4, 0x38, 0xc9, 0x9c, 0xcd, 0x40, 0xff, 0x42, 0x13, 0x5c, 0x2c, 0xd2, 0x11, 0x23, 0xab, - 0xe0, 0x72, 0x58, 0xf7, 0xbf, 0xed, 0x72, 0xe8, 0xa6, 0x20, 0xb7, 0x39, 0x1b, 0x33, 0xe5, 0x78, 0xdc, 0xcd, 0x59, - 0x5f, 0xfa, 0xcb, 0x24, 0x8d, 0x34, 0x15, 0x4a, 0x67, 0xd6, 0x77, 0xf7, 0xbb, 0x7a, 0xec, 0x96, 0x47, 0xf7, 0x16, - 0x10, 0xd0, 0xc6, 0x1d, 0x39, 0x65, 0x05, 0x96, 0x84, 0x63, 0x12, 0x0e, 0x1f, 0x00, 0x73, 0xad, 0x1f, 0x44, 0x25, - 0xfd, 0x5d, 0xb2, 0x4f, 0x07, 0x22, 0x3f, 0xd7, 0x65, 0x7d, 0x96, 0xfa, 0x93, 0x69, 0xf7, 0x71, 0xec, 0xe3, 0x19, - 0xa7, 0x39, 0x42, 0x52, 0x96, 0xe4, 0xd7, 0xcb, 0xcd, 0x71, 0xb6, 0x95, 0xfc, 0x4f, 0x28, 0xce, 0x1f, 0x94, 0xd1, - 0x3a, 0x5b, 0x36, 0x7d, 0xb6, 0x60, 0x38, 0x67, 0x92, 0x96, 0xe0, 0x94, 0x4f, 0xfc, 0x4b, 0xd5, 0xe1, 0xf1, 0x69, - 0x8f, 0x58, 0x0f, 0x22, 0x49, 0xf0, 0x5f, 0x73, 0xc2, 0xe3, 0x53, 0x33, 0xe1, 0x87, 0x67, 0x88, 0x4f, 0x6f, 0x8c, - 0x8e, 0xa9, 0xd4, 0x1c, 0xcb, 0x8a, 0x4b, 0x2f, 0x2a, 0x82, 0x53, 0x5d, 0xd8, 0xe0, 0xd9, 0x9d, 0x3e, 0xa5, 0x39, - 0xcd, 0x41, 0x78, 0x92, 0x66, 0x3b, 0xb7, 0x68, 0xb1, 0xa4, 0x05, 0x94, 0x92, 0xca, 0x49, 0xb4, 0x9a, 0xc6, 0x91, - 0xad, 0x23, 0xcc, 0x0b, 0x9c, 0xdd, 0x46, 0x62, 0x84, 0xb5, 0x33, 0x9e, 0xa8, 0x91, 0x9a, 0x92, 0x9b, 0x3a, 0x22, - 0x59, 0x8f, 0xc1, 0xfc, 0x9f, 0x1f, 0x7b, 0x5c, 0x63, 0x66, 0x67, 0xe1, 0x8a, 0x72, 0xfb, 0x6a, 0xaa, 0x76, 0xb2, - 0xa5, 0x2b, 0xaf, 0x5b, 0x3b, 0xa7, 0xd2, 0xe6, 0xc2, 0x15, 0x87, 0x6e, 0xb8, 0x7a, 0x6d, 0x17, 0x24, 0xd7, 0xcf, - 0x91, 0xdf, 0x0c, 0x83, 0x25, 0x89, 0xd4, 0xcd, 0x9d, 0x27, 0x65, 0x4b, 0xa9, 0xba, 0xaf, 0xc0, 0xe2, 0xb0, 0x34, - 0x54, 0xbb, 0x0a, 0xca, 0xf2, 0x46, 0x0d, 0x61, 0x11, 0xd6, 0xd4, 0x0b, 0x0e, 0xa7, 0x74, 0x9e, 0x05, 0x35, 0xb5, - 0x38, 0x3f, 0x69, 0xd4, 0x5e, 0x52, 0xe4, 0x54, 0x40, 0xbc, 0x89, 0x22, 0x17, 0x2f, 0x51, 0xaf, 0xf2, 0xb8, 0x82, - 0xfd, 0x91, 0x92, 0xaa, 0x9d, 0x5e, 0xa8, 0xc2, 0xe9, 0x99, 0x2a, 0x9f, 0x5e, 0x9e, 0xae, 0x70, 0x98, 0x4b, 0xb5, - 0x2b, 0x91, 0x45, 0x59, 0x52, 0x96, 0xe3, 0xca, 0x95, 0xf1, 0xdc, 0x9e, 0xbb, 0x8c, 0x4c, 0xd5, 0x29, 0x06, 0x93, - 0x32, 0xa5, 0xd5, 0x63, 0xdb, 0x11, 0x43, 0xc3, 0x04, 0x82, 0x5d, 0xd6, 0xca, 0x68, 0x7d, 0xbf, 0x78, 0x62, 0x51, - 0xa8, 0x2d, 0xad, 0x4f, 0x4f, 0x92, 0x90, 0xb5, 0xbe, 0xb4, 0x09, 0x94, 0xd8, 0x79, 0x3f, 0x56, 0xd1, 0x5e, 0x3c, - 0x77, 0xcf, 0xda, 0x83, 0x08, 0xb8, 0x5e, 0xeb, 0xcb, 0x0f, 0xc7, 0xf4, 0x90, 0xbd, 0x6c, 0x91, 0xa2, 0xfc, 0x81, - 0x04, 0xce, 0x07, 0x84, 0x30, 0x13, 0x58, 0x05, 0x0b, 0xe5, 0x95, 0x04, 0x56, 0x81, 0x8f, 0x18, 0xb5, 0x98, 0x9d, - 0x96, 0xde, 0xfb, 0xa4, 0x58, 0xe3, 0x26, 0xc4, 0x0b, 0x40, 0x5e, 0x4f, 0x21, 0xb2, 0x85, 0x28, 0xd0, 0x4c, 0x11, - 0x24, 0xfc, 0x80, 0x7d, 0x78, 0x81, 0xd6, 0x8f, 0xe9, 0xc8, 0x57, 0xb3, 0x72, 0x07, 0x6d, 0x3d, 0xb6, 0xa7, 0x2a, - 0x5d, 0x35, 0x29, 0x3e, 0x4a, 0xbc, 0x93, 0x58, 0x34, 0xf0, 0xca, 0x15, 0x3b, 0xbd, 0xf3, 0x81, 0xdf, 0xb0, 0x2d, - 0x73, 0xfc, 0xf2, 0x34, 0xc7, 0x15, 0xa8, 0x1a, 0x55, 0x68, 0xbb, 0x3d, 0x40, 0xa6, 0xa6, 0x57, 0x09, 0xe2, 0xb0, - 0x69, 0x1a, 0x2e, 0x40, 0x07, 0x0e, 0x51, 0x09, 0xa4, 0x4c, 0x35, 0x0b, 0x34, 0x72, 0x8d, 0x14, 0x36, 0x5b, 0xb3, - 0xa8, 0x4d, 0xd8, 0xe7, 0xdf, 0xd0, 0xbc, 0xb6, 0x2d, 0x9f, 0x88, 0x3b, 0x54, 0xf2, 0x19, 0xbc, 0xf4, 0xe1, 0x1e, - 0xdf, 0x03, 0x76, 0xe4, 0x4a, 0xc5, 0xc8, 0x94, 0xc4, 0xf6, 0x78, 0x41, 0xb5, 0xc9, 0x3c, 0x79, 0x54, 0xa7, 0x26, - 0x6c, 0x28, 0x57, 0x38, 0x61, 0xfb, 0x11, 0xbb, 0x80, 0x77, 0x28, 0x31, 0x37, 0xd5, 0x6f, 0x0e, 0xa1, 0xab, 0x3d, - 0xf0, 0xae, 0x8c, 0x7e, 0x79, 0xf9, 0x62, 0x8b, 0xb7, 0xb9, 0x83, 0xbf, 0xa6, 0xc1, 0xb6, 0x50, 0x1c, 0xea, 0xae, - 0x80, 0xf4, 0xb2, 0x97, 0x2b, 0x45, 0x49, 0x6f, 0xcd, 0xe0, 0xa9, 0xde, 0x20, 0x5d, 0x34, 0x05, 0xea, 0x60, 0xd2, - 0x83, 0x30, 0x21, 0xc8, 0x01, 0x95, 0xd1, 0xbb, 0x2b, 0xd9, 0xe2, 0x5e, 0xf0, 0x6c, 0x08, 0xc8, 0xd0, 0x8a, 0xe4, - 0xd3, 0x28, 0x8d, 0xba, 0x64, 0x68, 0x8f, 0x4d, 0x2c, 0x13, 0x80, 0x64, 0x57, 0xaf, 0x2c, 0x91, 0x09, 0x60, 0x0b, - 0xec, 0xd9, 0x3c, 0x86, 0xe1, 0xdb, 0xed, 0xc9, 0x80, 0xb1, 0x65, 0xf6, 0xbe, 0xa7, 0x9b, 0x8f, 0x26, 0xe4, 0x1a, - 0x6a, 0x0d, 0xc7, 0x39, 0x30, 0x64, 0xaa, 0x68, 0xf0, 0xc9, 0x86, 0x68, 0xc2, 0xda, 0x5c, 0x76, 0x5d, 0x08, 0x61, - 0xd0, 0x63, 0x53, 0x58, 0x41, 0x5c, 0x3b, 0xd6, 0xb0, 0xbe, 0x58, 0x46, 0xa0, 0x69, 0x4d, 0x1f, 0xc8, 0x98, 0xb6, - 0x97, 0x08, 0x75, 0x27, 0xca, 0x37, 0xcc, 0x69, 0x16, 0xc4, 0x7d, 0xaf, 0xcb, 0xe7, 0x1a, 0x36, 0x7e, 0xa2, 0x62, - 0xae, 0xa7, 0xba, 0x85, 0x01, 0xea, 0x40, 0x5c, 0x0c, 0xf8, 0x78, 0x1b, 0x42, 0x5f, 0xf9, 0x77, 0xd8, 0xf7, 0x4a, - 0x29, 0x8f, 0x3a, 0x3e, 0x2d, 0x35, 0x72, 0xd4, 0x5e, 0xf6, 0x7f, 0xb2, 0xfa, 0x90, 0x3f, 0x56, 0xa8, 0xd0, 0x84, - 0x34, 0x34, 0x89, 0xba, 0x79, 0x02, 0xb1, 0xed, 0x7b, 0xae, 0xd0, 0x8b, 0x45, 0xa4, 0x3c, 0x02, 0xba, 0x29, 0x8f, - 0x77, 0xab, 0x19, 0x46, 0x7c, 0xab, 0xd7, 0xda, 0x68, 0x4b, 0x34, 0x8b, 0x23, 0xde, 0x45, 0x3b, 0x3b, 0x9c, 0xca, - 0x48, 0xcf, 0x4e, 0xe1, 0x38, 0x27, 0xd1, 0xbb, 0x74, 0xd8, 0x69, 0xae, 0xbe, 0x7e, 0x67, 0x43, 0x1f, 0xe2, 0x6a, - 0x21, 0x6a, 0x7b, 0xce, 0x68, 0x6e, 0x26, 0x2e, 0x10, 0x0b, 0xa0, 0xd9, 0xbb, 0x57, 0xa9, 0xa6, 0xc9, 0x98, 0x71, - 0x59, 0xcc, 0x12, 0x29, 0xc2, 0x0e, 0xe8, 0x25, 0x9a, 0x30, 0x51, 0x75, 0x9c, 0x1b, 0xb1, 0xe7, 0xa3, 0xba, 0x29, - 0x77, 0x25, 0x19, 0x94, 0x45, 0xeb, 0xb6, 0xeb, 0xe5, 0x25, 0xf4, 0x7e, 0x1e, 0x70, 0x5d, 0x1b, 0x2b, 0x38, 0x61, - 0x0b, 0x13, 0x9f, 0x25, 0x41, 0x6e, 0x8d, 0x24, 0x5b, 0x84, 0xa5, 0x7a, 0x67, 0xfe, 0x69, 0xe9, 0xd5, 0x76, 0xa4, - 0x5e, 0x38, 0xcc, 0xdc, 0x9e, 0x85, 0xe5, 0x57, 0xc0, 0xe3, 0xbc, 0xf7, 0xbc, 0x11, 0x9a, 0xf2, 0xc7, 0xab, 0x3d, - 0xa8, 0x88, 0x66, 0x63, 0x47, 0x3d, 0x91, 0x6b, 0xba, 0xa9, 0x82, 0x6b, 0x32, 0xd1, 0xea, 0x41, 0x9c, 0x59, 0xd1, - 0x76, 0x62, 0x19, 0xfc, 0x33, 0xd8, 0xe0, 0x1b, 0xd8, 0x17, 0x4b, 0x00, 0xeb, 0x37, 0xc6, 0x57, 0x21, 0x0f, 0xcb, - 0xf7, 0x74, 0x7e, 0x86, 0xb0, 0xaf, 0x30, 0x57, 0x24, 0x2c, 0x4f, 0x95, 0x5a, 0xc9, 0x41, 0xc5, 0xb4, 0x7c, 0x6e, - 0xc1, 0x27, 0xd5, 0x56, 0x29, 0x5e, 0xff, 0x55, 0x5c, 0xab, 0xd0, 0xf9, 0x79, 0xa2, 0x10, 0xe2, 0xfe, 0x23, 0x12, - 0x55, 0x94, 0x9f, 0x86, 0xdb, 0x66, 0xdf, 0xc3, 0x8f, 0x1b, 0x7e, 0xd0, 0x65, 0x81, 0xca, 0xaa, 0x71, 0x83, 0x71, - 0xb8, 0x3c, 0xcd, 0xaa, 0x11, 0x0b, 0x45, 0xf8, 0xc6, 0xa5, 0x03, 0x47, 0x6f, 0x63, 0xab, 0xe6, 0x52, 0x85, 0x2a, - 0x20, 0xf6, 0x14, 0x7a, 0xde, 0x44, 0x35, 0x52, 0x2a, 0x12, 0x08, 0x93, 0x06, 0xed, 0x12, 0x17, 0xec, 0x16, 0xab, - 0x76, 0xb5, 0xbb, 0x15, 0xf3, 0x9a, 0x4c, 0x04, 0x8c, 0xf1, 0x0e, 0xb4, 0x6e, 0x66, 0x4b, 0x06, 0x74, 0x4e, 0xec, - 0xa8, 0xc0, 0x79, 0x8c, 0x71, 0x70, 0xb8, 0xc7, 0xcd, 0xf4, 0xa4, 0x92, 0x1d, 0x66, 0xe4, 0xa1, 0x39, 0x74, 0x86, - 0x2b, 0x0f, 0xe5, 0x21, 0x2b, 0x71, 0xb6, 0xc0, 0xcb, 0x35, 0x72, 0x95, 0xe8, 0xaa, 0x25, 0x68, 0x78, 0x20, 0xb9, - 0xdb, 0x37, 0xdf, 0xbd, 0xd3, 0xbb, 0x01, 0xa7, 0xd2, 0xdf, 0x0c, 0xd8, 0x1d, 0x2c, 0x78, 0xb7, 0x3a, 0x1d, 0x4b, - 0x0c, 0x00, 0x64, 0xd7, 0xf4, 0x83, 0xb0, 0x85, 0xee, 0x74, 0x87, 0x6b, 0xc7, 0x55, 0x04, 0x6d, 0x88, 0xaa, 0x8c, - 0xa1, 0x23, 0xbb, 0x88, 0x04, 0xb2, 0xeb, 0x88, 0x15, 0xdd, 0x32, 0x16, 0xc2, 0x09, 0x3c, 0xee, 0x01, 0xf5, 0x83, - 0x23, 0xa4, 0x54, 0x44, 0x42, 0xc9, 0x85, 0xf8, 0xdb, 0x34, 0xd4, 0xac, 0xe0, 0x6e, 0xb3, 0x21, 0x76, 0x93, 0x88, - 0xfe, 0xa0, 0x2a, 0xbc, 0x39, 0x8f, 0xf2, 0xad, 0x03, 0x0a, 0x1f, 0xcd, 0xc8, 0xc0, 0x59, 0xda, 0xb7, 0xa7, 0x5d, - 0x7b, 0x37, 0xe6, 0xa5, 0xb4, 0x94, 0x0a, 0xc1, 0xcd, 0x1d, 0x3c, 0xeb, 0x1f, 0x5c, 0x49, 0x13, 0x9b, 0x9a, 0x7d, - 0x99, 0x73, 0xb4, 0x33, 0xe5, 0x79, 0x14, 0x5f, 0x6b, 0xd9, 0xf3, 0xb6, 0x79, 0x36, 0x76, 0x67, 0xb7, 0x8b, 0xfd, - 0x0c, 0x49, 0x61, 0x8b, 0x19, 0xcc, 0x35, 0x89, 0x62, 0x12, 0x18, 0x6d, 0x80, 0xbd, 0x89, 0x66, 0xd8, 0x45, 0x0b, - 0x94, 0xbd, 0x5b, 0x77, 0x6b, 0xc3, 0xf1, 0xdb, 0xcc, 0xd7, 0xaa, 0xf6, 0xc2, 0x9d, 0x12, 0x05, 0xe7, 0xc3, 0xde, - 0x39, 0xaf, 0xff, 0xa3, 0xc4, 0x9b, 0x19, 0xc6, 0x92, 0x48, 0xb4, 0x36, 0x10, 0x3c, 0x4a, 0xeb, 0xb5, 0x59, 0x96, - 0x20, 0x3b, 0xb5, 0xbc, 0xfd, 0x07, 0x1d, 0x20, 0x15, 0xe3, 0xdd, 0xe2, 0xe6, 0x0c, 0x0b, 0x8e, 0x49, 0xa9, 0x2d, - 0x37, 0xbf, 0xfe, 0x49, 0x32, 0xa5, 0xa2, 0x4d, 0xae, 0x27, 0x9a, 0xe7, 0xe2, 0xca, 0x01, 0x80, 0x40, 0x69, 0x36, - 0xac, 0x8b, 0xeb, 0xcd, 0x64, 0x73, 0xab, 0xd0, 0x11, 0x66, 0xaa, 0xc0, 0xf8, 0x9b, 0x55, 0x4a, 0x4f, 0xa9, 0x56, - 0x49, 0xc2, 0xdc, 0x4e, 0x5f, 0xab, 0x44, 0x68, 0x3f, 0x06, 0xe2, 0xdb, 0xc9, 0x77, 0xf5, 0xa7, 0x6c, 0x8b, 0x3c, - 0x8e, 0x03, 0x93, 0xb3, 0xb7, 0x76, 0x50, 0xd0, 0xa8, 0xed, 0x5c, 0x8e, 0xd7, 0x3c, 0x2b, 0xa8, 0x7d, 0xe5, 0x57, - 0xb3, 0xb5, 0xc7, 0x17, 0xee, 0x08, 0xb2, 0x02, 0xa9, 0xc7, 0xe4, 0xc1, 0x34, 0x46, 0xa5, 0xd9, 0x39, 0x2f, 0x76, - 0x58, 0x1e, 0x93, 0x64, 0xd7, 0xf8, 0xcf, 0xc8, 0xa5, 0x14, 0x48, 0xfe, 0xc4, 0xb9, 0x13, 0x2a, 0x16, 0xb3, 0x64, - 0x61, 0x6a, 0xd7, 0x24, 0x2f, 0xdf, 0xc5, 0x75, 0x3c, 0x2d, 0xc7, 0x7f, 0x56, 0x4c, 0xf4, 0x24, 0x10, 0x52, 0xeb, - 0x1d, 0x0d, 0x1e, 0x40, 0xdd, 0x3a, 0x83, 0x6f, 0x64, 0xf3, 0x50, 0x24, 0x83, 0x8c, 0xd9, 0x56, 0xdd, 0xa5, 0x1a, - 0x89, 0x7a, 0xb0, 0x0c, 0xb4, 0xdb, 0x49, 0xe0, 0x12, 0xb5, 0xf6, 0x10, 0x1c, 0x54, 0xf4, 0x3e, 0x54, 0xc1, 0x52, - 0x33, 0x58, 0xaa, 0xac, 0xd4, 0x06, 0x6b, 0x2f, 0xd5, 0xda, 0x32, 0xa3, 0x2b, 0x2f, 0x0f, 0x8e, 0x39, 0x0e, 0x00, - 0x5b, 0xcf, 0xa5, 0x0e, 0x03, 0xe8, 0x44, 0x36, 0x70, 0x03, 0x32, 0x00, 0x65, 0x2d, 0xa1, 0x72, 0xd3, 0x82, 0x73, - 0xad, 0x4d, 0x29, 0x96, 0x80, 0x44, 0x70, 0xc6, 0xfe, 0xe8, 0x51, 0xe9, 0xed, 0xc8, 0x11, 0xae, 0x5a, 0x37, 0x6d, - 0x05, 0x6b, 0xeb, 0x0c, 0x69, 0xe3, 0x31, 0xde, 0x65, 0x3f, 0x01, 0xdf, 0xc5, 0x8b, 0xd6, 0x91, 0x19, 0x6f, 0x71, - 0xa4, 0x20, 0x14, 0xba, 0xde, 0x31, 0x16, 0xa6, 0x04, 0x86, 0xd9, 0xdd, 0x15, 0x61, 0x7a, 0x7b, 0x29, 0x20, 0x58, - 0xb8, 0xb1, 0x16, 0x37, 0x0e, 0xcf, 0x6f, 0x1c, 0x26, 0x8a, 0x70, 0x68, 0xa6, 0x4a, 0xf8, 0x5c, 0xaa, 0x0c, 0x05, - 0x39, 0x35, 0x38, 0x0a, 0xdc, 0xdf, 0xbe, 0x77, 0xb4, 0x28, 0x12, 0x82, 0x2c, 0x2e, 0x43, 0x13, 0xe5, 0x75, 0xc6, - 0x05, 0xe9, 0xcb, 0xe1, 0xfe, 0x62, 0x6e, 0x87, 0xa9, 0x59, 0x99, 0xb7, 0x48, 0x7c, 0x6f, 0x5a, 0x8c, 0x11, 0xe1, - 0x7c, 0xaf, 0x5d, 0x60, 0x8b, 0xb5, 0xec, 0x6f, 0x3f, 0xee, 0x09, 0x57, 0x16, 0x0e, 0x0c, 0x5d, 0x64, 0xda, 0xab, - 0x75, 0xb7, 0x52, 0xc4, 0xf9, 0x47, 0xf4, 0xc8, 0xfc, 0xc1, 0x38, 0x8e, 0x1d, 0xdc, 0xee, 0x84, 0xda, 0xe7, 0xfc, - 0x86, 0x85, 0x3a, 0xa2, 0xd5, 0x0d, 0xd4, 0xb0, 0x06, 0x97, 0xca, 0x2c, 0x2d, 0xe6, 0x9f, 0xdd, 0xdc, 0x3c, 0x25, - 0xe0, 0x24, 0xf1, 0x05, 0x24, 0xd9, 0xe1, 0x7a, 0xf7, 0xe9, 0x2d, 0x93, 0xbe, 0x0d, 0x92, 0x12, 0xbb, 0x95, 0xca, - 0x76, 0x49, 0xd3, 0x94, 0x1d, 0xee, 0x8a, 0xaa, 0x35, 0xd8, 0x13, 0x13, 0xa5, 0xa3, 0xbe, 0x10, 0x26, 0x4d, 0xec, - 0x4b, 0x18, 0xef, 0x8b, 0x09, 0x9c, 0x37, 0x0c, 0xf1, 0xaa, 0x03, 0xa5, 0x50, 0x22, 0x65, 0x2f, 0xbb, 0xe3, 0x4d, - 0x69, 0x26, 0x1f, 0x51, 0xc5, 0x81, 0x96, 0xde, 0x5a, 0xee, 0x4a, 0x00, 0xd0, 0xbd, 0xba, 0xbc, 0xfc, 0xfd, 0xc1, - 0x7d, 0x8c, 0x95, 0xc8, 0x37, 0xef, 0xf7, 0xc3, 0xd3, 0xfd, 0x17, 0x12, 0xc1, 0x81, 0xe6, 0x71, 0x7a, 0xf9, 0x5d, - 0xa5, 0x8b, 0x5b, 0xd5, 0xf7, 0xab, 0xa0, 0x8c, 0xd4, 0xe3, 0xee, 0x2c, 0x6c, 0x09, 0x26, 0xac, 0x0d, 0x38, 0x67, - 0x3e, 0x08, 0x65, 0x2e, 0xff, 0xfa, 0x2c, 0xce, 0xdd, 0x78, 0x58, 0x78, 0x26, 0xb0, 0xb1, 0x31, 0xd4, 0x61, 0xae, - 0x3b, 0xf3, 0xe9, 0xe0, 0x19, 0xb9, 0xee, 0x1a, 0x32, 0x2c, 0x8d, 0x03, 0xbe, 0xde, 0xfa, 0xf1, 0xfe, 0x3f, 0x8f, - 0x5f, 0x06, 0xe6, 0x81, 0x99, 0xf1, 0x1c, 0x95, 0xf6, 0xb0, 0xa4, 0xc1, 0x61, 0x64, 0x3b, 0xea, 0xda, 0xbf, 0x47, - 0x23, 0x82, 0x8c, 0x10, 0x21, 0xc7, 0xa1, 0x1d, 0x43, 0x39, 0x3d, 0x8e, 0x55, 0x95, 0xf6, 0xa2, 0x37, 0x18, 0x37, - 0xb2, 0x85, 0x22, 0x60, 0x4a, 0xf4, 0xfd, 0xea, 0xac, 0x2a, 0xee, 0x4d, 0xff, 0xf2, 0xe8, 0x8b, 0xec, 0xaa, 0x51, - 0x03, 0xe1, 0x77, 0x24, 0xaa, 0xa2, 0x37, 0x96, 0xef, 0xb4, 0x05, 0x5b, 0x43, 0x0e, 0x8c, 0x1a, 0x49, 0x9b, 0x11, - 0x3b, 0x6f, 0x32, 0xe7, 0x92, 0x2f, 0xd4, 0x58, 0x7a, 0x94, 0x93, 0x65, 0x0a, 0x00, 0xd3, 0x95, 0x16, 0x11, 0x17, - 0x18, 0x82, 0x2b, 0x0e, 0xab, 0x5b, 0xc8, 0x8c, 0xf5, 0x6c, 0x77, 0x16, 0x8d, 0x26, 0x08, 0xd3, 0xfa, 0x90, 0xa8, - 0x30, 0x73, 0xca, 0xa4, 0x0c, 0x97, 0xda, 0x09, 0xc8, 0x93, 0xdf, 0xd2, 0x8a, 0x01, 0x98, 0x31, 0x91, 0x5c, 0x6e, - 0x6c, 0x22, 0xeb, 0x90, 0xcf, 0x49, 0xbf, 0x99, 0xf3, 0xe1, 0x9b, 0x18, 0x1f, 0x5c, 0x9c, 0x06, 0xeb, 0x0f, 0x50, - 0xf2, 0xdc, 0x0d, 0x97, 0xab, 0x4d, 0xda, 0x72, 0x5b, 0xd1, 0x16, 0x8c, 0x89, 0x76, 0x79, 0x61, 0x9b, 0xa8, 0x40, - 0x9f, 0x49, 0x6f, 0xb8, 0x06, 0xa2, 0x1c, 0x06, 0xf1, 0x52, 0x0e, 0xc5, 0xcd, 0xda, 0x23, 0x54, 0x69, 0x2c, 0x50, - 0x03, 0x2b, 0x7c, 0xc2, 0x30, 0xaa, 0x26, 0xd8, 0x7d, 0xff, 0xd8, 0xe0, 0xcb, 0xd5, 0xb7, 0x83, 0x35, 0x6f, 0x5a, - 0x26, 0xda, 0x21, 0x3a, 0x9c, 0x83, 0x8a, 0x87, 0xd8, 0x69, 0x92, 0xd3, 0x60, 0xea, 0x7a, 0x72, 0xb9, 0x21, 0x63, - 0x33, 0x19, 0x69, 0x7a, 0xc0, 0x1d, 0xe6, 0xb6, 0x1f, 0x1a, 0xcc, 0x21, 0x8e, 0x8d, 0xa3, 0xba, 0x71, 0x9d, 0x31, - 0x84, 0x40, 0x27, 0x48, 0xa7, 0x3b, 0xa3, 0xcb, 0x8b, 0xf2, 0xd6, 0xda, 0x34, 0x74, 0x64, 0xdf, 0x9a, 0xee, 0x38, - 0xc2, 0x88, 0x88, 0xc7, 0x4c, 0x17, 0x2c, 0x2c, 0xb5, 0xb3, 0xb8, 0x29, 0x62, 0x39, 0xb6, 0x23, 0xac, 0x06, 0x60, - 0x16, 0xd8, 0xef, 0xcc, 0x4b, 0xef, 0x35, 0x7a, 0x21, 0x7c, 0xb0, 0x91, 0xf3, 0xb2, 0x98, 0x91, 0xb9, 0xef, 0xd0, - 0x14, 0x1e, 0xb8, 0x3f, 0x55, 0xa7, 0x15, 0x1c, 0xc4, 0xda, 0x71, 0xf4, 0xf7, 0x03, 0x6a, 0x89, 0x17, 0x04, 0x21, - 0x9c, 0x8a, 0xcd, 0x96, 0x0e, 0x88, 0x7d, 0x88, 0x65, 0x6a, 0x00, 0x42, 0x50, 0x0e, 0x56, 0xbb, 0x4f, 0x3b, 0x7d, - 0x8f, 0xd0, 0xf7, 0x11, 0xf3, 0x4d, 0x80, 0xcc, 0x14, 0x94, 0x27, 0x6a, 0x9f, 0x92, 0x88, 0x9e, 0xfc, 0xa4, 0x9b, - 0x6c, 0xd6, 0xa6, 0x4e, 0x02, 0xa5, 0x23, 0x4e, 0xde, 0x62, 0x14, 0xce, 0x8b, 0x13, 0x06, 0x74, 0xbd, 0x14, 0x83, - 0x69, 0xe3, 0x8b, 0xe2, 0x95, 0x2d, 0xa7, 0x86, 0xfd, 0x38, 0xb7, 0x35, 0x27, 0x1c, 0x8e, 0x32, 0x51, 0xf6, 0x4e, - 0x95, 0x1e, 0x0a, 0xac, 0x9b, 0x06, 0xea, 0xfd, 0x84, 0x5d, 0x70, 0xb7, 0x3d, 0x3e, 0xa6, 0x72, 0x04, 0x15, 0x42, - 0x21, 0x41, 0x2d, 0x53, 0xfa, 0x23, 0xe6, 0x39, 0x35, 0x62, 0xaf, 0x3c, 0x2a, 0x65, 0x22, 0x88, 0xc7, 0x3e, 0x7b, - 0xb0, 0xc7, 0x16, 0x08, 0x87, 0x1d, 0x4e, 0x74, 0xa5, 0x80, 0x7e, 0x90, 0x36, 0x82, 0x9d, 0x8f, 0x85, 0x22, 0x59, - 0x80, 0x62, 0x68, 0x37, 0xe2, 0xa4, 0xca, 0xee, 0x92, 0xd0, 0xef, 0xc5, 0x02, 0x67, 0x76, 0x2e, 0x81, 0xe4, 0x3a, - 0x5b, 0x18, 0x64, 0x54, 0x08, 0xed, 0x16, 0x12, 0x10, 0xa6, 0x74, 0x91, 0x0f, 0xf8, 0x91, 0x5e, 0x2a, 0x97, 0x0a, - 0xc9, 0xd3, 0xa5, 0xcf, 0xe1, 0x97, 0x1d, 0xb5, 0xe2, 0xc6, 0x5b, 0x1b, 0xe5, 0x1a, 0xe5, 0x62, 0xd6, 0xfc, 0x47, - 0xec, 0x71, 0x89, 0x74, 0x6c, 0x81, 0xb5, 0xa1, 0x1b, 0x54, 0x96, 0xd2, 0xc0, 0x89, 0x07, 0x12, 0xa9, 0xdb, 0x0e, - 0x47, 0xda, 0xa2, 0xf6, 0x93, 0xbd, 0x57, 0xd7, 0xa0, 0xf4, 0xcc, 0x7a, 0x2b, 0x71, 0x68, 0x2a, 0x64, 0x91, 0x55, - 0xd5, 0x80, 0x95, 0x7c, 0x1c, 0xd2, 0x64, 0x88, 0xee, 0x92, 0xc4, 0x93, 0xcc, 0xe9, 0x37, 0x99, 0xe9, 0x45, 0xff, - 0xa3, 0x12, 0x95, 0x0f, 0x65, 0xff, 0x93, 0x1c, 0xcf, 0x3a, 0xa9, 0x1f, 0x85, 0xd3, 0x90, 0xc6, 0x26, 0x13, 0x30, - 0x80, 0xd5, 0x86, 0x39, 0x94, 0x19, 0x2d, 0x5b, 0xc5, 0xb9, 0xdb, 0x46, 0x4a, 0x6c, 0xe8, 0x27, 0x3b, 0x06, 0xec, - 0x8f, 0xbf, 0x02, 0x71, 0xc0, 0x23, 0x66, 0x1c, 0xec, 0xad, 0x98, 0xb4, 0xa9, 0x28, 0xf8, 0x5d, 0x69, 0x34, 0x81, - 0x6b, 0x3a, 0xa4, 0x69, 0x73, 0xe5, 0x18, 0x32, 0xbd, 0x6c, 0xcc, 0x84, 0x98, 0x39, 0x78, 0x46, 0x28, 0xf6, 0xdf, - 0xfd, 0x77, 0x09, 0x8e, 0x16, 0x8d, 0xf2, 0xe4, 0xb4, 0x0e, 0xe6, 0x56, 0x5d, 0x7a, 0xe7, 0x7e, 0x08, 0x69, 0x03, - 0x80, 0xca, 0x9d, 0xed, 0x59, 0x88, 0xbb, 0xdb, 0x2a, 0x44, 0x1f, 0xcc, 0x52, 0x93, 0xf2, 0xae, 0x97, 0x6c, 0x2c, - 0x61, 0x9e, 0x32, 0x2b, 0x87, 0xd6, 0x81, 0x9d, 0xfd, 0x63, 0xfa, 0x1f, 0xc9, 0xf7, 0x9b, 0xfc, 0x7c, 0xb7, 0x46, - 0x14, 0x98, 0x91, 0x57, 0xf4, 0x3e, 0x07, 0xa0, 0xde, 0x40, 0x24, 0x97, 0xe5, 0x3d, 0x5c, 0xd4, 0x3d, 0xfc, 0x65, - 0x2e, 0x1a, 0x1f, 0x78, 0xcc, 0x57, 0x94, 0xdb, 0x0f, 0x1b, 0x1e, 0x08, 0x44, 0xee, 0x02, 0x23, 0x4c, 0xff, 0x3e, - 0x39, 0xe6, 0xe3, 0xa9, 0xf0, 0xca, 0xab, 0x17, 0xb0, 0xea, 0x89, 0x0f, 0xaf, 0xcf, 0xb0, 0xb5, 0xff, 0x44, 0x66, - 0x15, 0x97, 0x60, 0x66, 0xb0, 0xa8, 0xb8, 0x5f, 0x73, 0x65, 0x07, 0x17, 0xad, 0xee, 0x3b, 0x19, 0xff, 0x7c, 0x19, - 0xee, 0xbe, 0x7e, 0xee, 0x14, 0x8d, 0x73, 0x78, 0x8f, 0x71, 0xc4, 0x35, 0x2e, 0xe1, 0xed, 0xc7, 0x67, 0x55, 0x37, - 0xf7, 0x8c, 0x7d, 0xd6, 0x74, 0x63, 0x55, 0x33, 0xb4, 0x21, 0x71, 0xfe, 0xc3, 0xd6, 0x5f, 0x2c, 0xbc, 0xd8, 0xfd, - 0xc4, 0x4e, 0x8a, 0xac, 0x0b, 0x5a, 0xb7, 0x5d, 0xab, 0xf2, 0x83, 0x01, 0x97, 0x3a, 0x1e, 0x4b, 0xb6, 0x3a, 0xbb, - 0x5f, 0x8c, 0x3f, 0x9a, 0x09, 0xb4, 0x3f, 0xfa, 0xe0, 0x66, 0x09, 0x55, 0x7b, 0x9c, 0xd1, 0xdd, 0xb7, 0x3f, 0x7b, - 0x39, 0x76, 0x59, 0x9a, 0xf8, 0xdc, 0x27, 0xc7, 0xc8, 0x13, 0xe9, 0x2d, 0xb4, 0x0a, 0xc3, 0xf4, 0xdc, 0x3d, 0x44, - 0x6a, 0x91, 0x2c, 0x3d, 0x7b, 0x0b, 0x97, 0x9c, 0xd0, 0x99, 0x7e, 0x29, 0x09, 0x75, 0xdb, 0x6b, 0xc5, 0x25, 0x62, - 0x7e, 0x8d, 0xd4, 0xc0, 0x55, 0x12, 0x3c, 0x44, 0x44, 0xa0, 0xb3, 0x17, 0xe5, 0x33, 0x45, 0x75, 0x85, 0x57, 0x7f, - 0x8d, 0xb2, 0x80, 0x57, 0x66, 0xe3, 0x61, 0xe5, 0x4c, 0x1f, 0x9d, 0xd6, 0x59, 0xae, 0xcb, 0x00, 0x72, 0x71, 0x01, - 0x4e, 0xec, 0xdf, 0x72, 0x06, 0xc3, 0xda, 0x86, 0xfb, 0x23, 0x35, 0x1a, 0xa3, 0xe4, 0x1b, 0x02, 0x30, 0x0a, 0x8a, - 0x36, 0xb3, 0xef, 0x36, 0xa4, 0x0b, 0x19, 0xd5, 0xfb, 0xfd, 0xf7, 0xfc, 0xe5, 0xd1, 0x77, 0xbe, 0x5d, 0x7a, 0xad, - 0x85, 0x49, 0x65, 0x91, 0xad, 0xa3, 0x83, 0xec, 0xae, 0x87, 0x6d, 0x90, 0xdf, 0x74, 0x9f, 0x49, 0x37, 0x2f, 0x06, - 0xd8, 0xd2, 0xf6, 0x23, 0x32, 0x8d, 0x24, 0x51, 0xc8, 0xb1, 0x96, 0x22, 0xa8, 0x65, 0x20, 0x15, 0x47, 0x0e, 0x0f, - 0x4f, 0x46, 0xbe, 0x99, 0x33, 0x0e, 0x2d, 0x69, 0x0b, 0xd8, 0x18, 0xd6, 0xdd, 0xd7, 0x52, 0x9b, 0x65, 0xd6, 0xab, - 0x47, 0x76, 0x22, 0xbc, 0xe0, 0x08, 0x4a, 0xec, 0x53, 0x48, 0x0b, 0xab, 0xb1, 0x0c, 0x6e, 0x5e, 0x4f, 0x28, 0xa0, - 0x6d, 0x2e, 0x9d, 0x53, 0xab, 0xc8, 0x57, 0xfc, 0x7c, 0x58, 0x83, 0x21, 0xf9, 0xd6, 0x4a, 0xc1, 0xc6, 0xae, 0x55, - 0xa5, 0xf1, 0x1c, 0x6f, 0x68, 0x52, 0x1c, 0x1d, 0xed, 0x51, 0x76, 0x08, 0x47, 0x63, 0x70, 0x73, 0x6f, 0xa8, 0xa4, - 0x4c, 0x63, 0xdf, 0x4b, 0xd2, 0xbf, 0xea, 0xcb, 0x50, 0x25, 0x24, 0x8a, 0xf9, 0x1f, 0x54, 0x63, 0x0e, 0x3c, 0x52, - 0x1f, 0xbd, 0xc8, 0x04, 0xa3, 0x85, 0x42, 0x74, 0x83, 0x87, 0x9d, 0x3a, 0x11, 0xcf, 0x5e, 0xa2, 0x70, 0xd2, 0xbd, - 0x24, 0x9a, 0x17, 0xfe, 0xd9, 0x6f, 0x9e, 0x7b, 0x01, 0xd0, 0x29, 0x2c, 0x9d, 0x31, 0x70, 0xca, 0x9a, 0x74, 0xa4, - 0xe0, 0xd6, 0x68, 0xa0, 0x09, 0x6c, 0xc1, 0xd3, 0xa9, 0x0c, 0xb9, 0x28, 0x67, 0x96, 0xf4, 0x64, 0x17, 0x53, 0x6a, - 0xcd, 0xf7, 0x85, 0xb2, 0xb0, 0x7e, 0xb7, 0x79, 0x94, 0x3b, 0x47, 0x66, 0x25, 0x82, 0x45, 0x9e, 0x02, 0xaf, 0x5c, - 0xde, 0x78, 0xd1, 0xe8, 0x39, 0x78, 0x99, 0x9a, 0x79, 0x0e, 0x07, 0x79, 0xe9, 0x2f, 0xbc, 0x78, 0xfb, 0x7e, 0x0f, - 0xfa, 0x1a, 0xb9, 0x0a, 0x8b, 0xa8, 0x07, 0xe4, 0xbc, 0xe3, 0xa8, 0xbb, 0xfb, 0xe0, 0x93, 0x8e, 0x97, 0x5c, 0x35, - 0x3e, 0x84, 0xbf, 0xa4, 0xd1, 0x17, 0x92, 0xa0, 0x39, 0x15, 0x52, 0x60, 0xe0, 0xaf, 0x5b, 0xd8, 0xf8, 0x3e, 0x4b, - 0xb7, 0x23, 0x26, 0x7f, 0xf5, 0xbe, 0xd2, 0x93, 0x5d, 0x8f, 0x49, 0x3d, 0x05, 0x8a, 0x3a, 0x3b, 0x5a, 0x36, 0x23, - 0xad, 0xd4, 0xbc, 0x5b, 0xb8, 0xf5, 0x81, 0x4f, 0xe9, 0xc0, 0x8e, 0x02, 0x77, 0x41, 0x2c, 0x9e, 0x71, 0x7e, 0x6d, - 0x66, 0xb7, 0x3e, 0xfb, 0x2e, 0x03, 0x8c, 0x5a, 0x4f, 0xf4, 0x41, 0x10, 0xdf, 0x67, 0x47, 0xac, 0xbb, 0x04, 0x96, - 0x60, 0x4c, 0x4f, 0xdb, 0x24, 0x9c, 0x96, 0xfb, 0x64, 0x7e, 0xc8, 0xc6, 0x04, 0x8a, 0x4a, 0x31, 0x57, 0x81, 0x4f, - 0x26, 0x40, 0xcc, 0x21, 0x25, 0xdb, 0xab, 0x33, 0xf9, 0x44, 0xcc, 0x85, 0x2a, 0x45, 0x73, 0x31, 0x02, 0x42, 0x90, - 0xc3, 0x8c, 0xed, 0x3f, 0xc2, 0x85, 0x08, 0x70, 0x87, 0x83, 0x2c, 0x73, 0xde, 0xe0, 0xaa, 0xcc, 0x2f, 0x00, 0x73, - 0x19, 0xea, 0xad, 0xc6, 0x4e, 0x8f, 0x61, 0xf9, 0x7d, 0x1a, 0x64, 0xbd, 0x22, 0x77, 0x61, 0x19, 0xc2, 0xeb, 0xa2, - 0x54, 0x8d, 0x40, 0xba, 0x3b, 0x8c, 0xd3, 0xaf, 0x20, 0x61, 0xfa, 0x59, 0x02, 0x9e, 0xa3, 0x38, 0x11, 0x0b, 0xfe, - 0xdc, 0xd0, 0xa5, 0x13, 0xe4, 0x80, 0xa1, 0x1e, 0x9e, 0x5e, 0x51, 0xf7, 0x92, 0x1d, 0xdd, 0x6d, 0x59, 0xa5, 0xec, - 0x6f, 0x27, 0xf2, 0x63, 0xd9, 0x39, 0x5e, 0xf2, 0xa6, 0xbb, 0x89, 0xdf, 0x22, 0x8e, 0x02, 0x88, 0x63, 0x55, 0x76, - 0xa1, 0x4a, 0x44, 0xbe, 0x2e, 0x9c, 0x39, 0xe5, 0x79, 0x64, 0xc9, 0xce, 0xdb, 0xdd, 0x77, 0xa6, 0xd8, 0x91, 0x66, - 0x76, 0xce, 0x7b, 0xc5, 0x4f, 0x95, 0x12, 0xd3, 0x37, 0x0e, 0xce, 0xfd, 0x9d, 0xf4, 0xfd, 0xf1, 0x70, 0x2c, 0xb1, - 0x9e, 0x5f, 0x73, 0xd5, 0xf6, 0x94, 0xaa, 0x65, 0xad, 0xbf, 0x53, 0xbe, 0xa6, 0x6c, 0xdd, 0xec, 0x67, 0xb0, 0x23, - 0xd7, 0xcc, 0x97, 0x2e, 0xa4, 0x77, 0x7d, 0x39, 0xc9, 0xae, 0x0a, 0xec, 0xd1, 0x07, 0x06, 0xd0, 0xb4, 0xae, 0x0c, - 0xc5, 0x57, 0x6a, 0x19, 0xb9, 0x4c, 0x80, 0xd7, 0xc1, 0x4f, 0x5f, 0xcc, 0x7c, 0x39, 0x66, 0xab, 0x77, 0xde, 0x1f, - 0x31, 0x2f, 0xba, 0xb3, 0xe7, 0x7a, 0x87, 0xb8, 0x18, 0xe7, 0x7d, 0x07, 0x66, 0xe9, 0xb7, 0x1e, 0xf3, 0x79, 0x7f, - 0x9d, 0x60, 0x7f, 0x64, 0x45, 0x30, 0xc8, 0xe0, 0xae, 0x7a, 0xc1, 0x71, 0x16, 0x86, 0x68, 0xda, 0x76, 0x5f, 0xd4, - 0xcc, 0x6d, 0x49, 0xd3, 0xe7, 0xbc, 0xa5, 0x12, 0xf6, 0x8b, 0x3b, 0xce, 0xac, 0xef, 0xbc, 0x83, 0xac, 0xb5, 0xea, - 0xd0, 0xaf, 0x48, 0xbd, 0x0c, 0xeb, 0x3f, 0x81, 0x62, 0xbc, 0xec, 0xb0, 0xda, 0x5a, 0x69, 0x7a, 0xae, 0xca, 0xde, - 0xe1, 0x49, 0x05, 0xa0, 0x14, 0x01, 0x9d, 0x75, 0xe3, 0xb8, 0x9b, 0x02, 0xf5, 0xc5, 0x29, 0xda, 0xf5, 0xf7, 0xd7, - 0xc0, 0x28, 0x88, 0xd4, 0xf7, 0xab, 0xbc, 0x27, 0xfd, 0x95, 0xf8, 0x58, 0x78, 0x45, 0xa1, 0xdb, 0xf2, 0xf8, 0x2f, - 0x8a, 0x94, 0xe9, 0x27, 0x21, 0xdc, 0xf9, 0xb9, 0xba, 0x85, 0x89, 0xf9, 0x74, 0xe9, 0xf9, 0x3d, 0x5a, 0x87, 0x2b, - 0x68, 0x7d, 0xe6, 0x07, 0x69, 0xcc, 0xff, 0x39, 0x56, 0x59, 0xe2, 0x1d, 0x9a, 0xe5, 0xdb, 0x04, 0xc7, 0x74, 0x78, - 0x4a, 0x3a, 0xcf, 0x71, 0x42, 0xa1, 0x1b, 0x94, 0x7a, 0xa7, 0x0e, 0x35, 0x93, 0xc0, 0x42, 0x81, 0x93, 0x7e, 0x44, - 0xf3, 0xa8, 0x38, 0x12, 0xc0, 0xc8, 0xf4, 0xfa, 0xdb, 0x5c, 0x5b, 0xe4, 0xc3, 0x5e, 0xfb, 0x65, 0xe3, 0x5e, 0x1f, - 0x05, 0xc9, 0x7f, 0xc7, 0x01, 0x12, 0x6b, 0x43, 0xf6, 0x26, 0x60, 0x19, 0x51, 0xcc, 0x51, 0xf0, 0x6d, 0x41, 0x52, - 0xa8, 0x54, 0x82, 0x0b, 0x7b, 0x84, 0x85, 0x4b, 0x2d, 0x2d, 0x63, 0x2d, 0x3c, 0x6f, 0x01, 0x3a, 0x3a, 0x7c, 0x5d, - 0x7c, 0x97, 0x9d, 0x5e, 0x0c, 0x92, 0x73, 0x8f, 0x10, 0x24, 0xa8, 0xc7, 0x45, 0x09, 0xb8, 0x6f, 0x56, 0xe3, 0x6b, - 0x41, 0x4d, 0x9a, 0xd4, 0x5d, 0x05, 0xa7, 0xbb, 0x50, 0xc0, 0x65, 0x74, 0xd6, 0x40, 0xd0, 0xf0, 0xdd, 0x91, 0x0c, - 0xb0, 0x2a, 0x48, 0x90, 0xb8, 0xe4, 0x87, 0xc4, 0x4a, 0x45, 0x77, 0x78, 0x47, 0x63, 0xbc, 0xa3, 0xb6, 0x2e, 0x3b, - 0xed, 0x6b, 0xef, 0x36, 0x0c, 0xc2, 0x88, 0xf1, 0x99, 0x81, 0x8e, 0xec, 0xed, 0x80, 0x4d, 0x9e, 0x9d, 0xb0, 0x01, - 0x8f, 0xe5, 0x8e, 0x8c, 0xd6, 0xf9, 0x35, 0xcb, 0x17, 0x7b, 0xda, 0xe7, 0x9e, 0x84, 0x8c, 0x8d, 0x23, 0x70, 0xa3, - 0x06, 0x64, 0x4a, 0x98, 0x25, 0xfc, 0xc8, 0xa1, 0xfa, 0x2c, 0x09, 0xfe, 0x2b, 0x6d, 0x40, 0x01, 0x39, 0xda, 0x93, - 0x4a, 0x92, 0x79, 0x0c, 0xb3, 0x26, 0x85, 0x0f, 0xc8, 0x50, 0xe6, 0xf8, 0x69, 0xa8, 0x29, 0xd6, 0x89, 0xa1, 0x1a, - 0x99, 0x26, 0x86, 0xef, 0x1a, 0xf3, 0x57, 0xdc, 0xfc, 0xd9, 0xab, 0xaa, 0xa7, 0x43, 0xf0, 0x10, 0x4a, 0x09, 0xca, - 0xcd, 0x4c, 0x28, 0x03, 0xe8, 0x17, 0x69, 0xb2, 0x01, 0xad, 0x1f, 0xa1, 0xc3, 0xf7, 0x9b, 0x23, 0x38, 0xb9, 0x2c, - 0x55, 0x58, 0x17, 0x3f, 0xfe, 0x4a, 0x60, 0xef, 0xdd, 0x61, 0xba, 0x51, 0xce, 0xe6, 0xd4, 0x96, 0x4c, 0x5d, 0xf0, - 0x75, 0xb9, 0x3e, 0x09, 0x5e, 0x59, 0x20, 0x35, 0x0b, 0xab, 0x75, 0xe2, 0x12, 0x59, 0xb4, 0x38, 0x4d, 0xde, 0xcd, - 0x5f, 0x9e, 0x66, 0x13, 0xaf, 0x5c, 0x0a, 0x4c, 0x7e, 0x16, 0x55, 0xe2, 0x22, 0xb3, 0x5c, 0x36, 0xfc, 0xcd, 0x01, - 0x9f, 0x67, 0x7d, 0x3d, 0xf0, 0xbb, 0xfe, 0x5c, 0xdf, 0x1e, 0xf2, 0x90, 0x50, 0x8b, 0xdb, 0x1a, 0x67, 0x4e, 0x8d, - 0x6d, 0xe6, 0xbd, 0x5d, 0xda, 0xc7, 0x71, 0xcc, 0x7c, 0x44, 0x45, 0xba, 0xa2, 0x24, 0xec, 0x4e, 0x87, 0xa4, 0x53, - 0x4c, 0x56, 0x9c, 0x39, 0xf5, 0x54, 0xb8, 0x2d, 0xce, 0x6b, 0x7c, 0xb8, 0x44, 0x74, 0x82, 0xa9, 0x03, 0x24, 0xd7, - 0xb1, 0x25, 0xb8, 0xab, 0x08, 0x5c, 0x9a, 0x5a, 0xa8, 0xa2, 0x78, 0xc6, 0x59, 0xec, 0x16, 0x52, 0xf3, 0x53, 0xf5, - 0xb8, 0xd4, 0xad, 0x2a, 0xe1, 0x95, 0x6c, 0x85, 0x29, 0x90, 0xc9, 0x8a, 0xa4, 0x39, 0x89, 0x15, 0x0e, 0xfa, 0x9e, - 0x43, 0x92, 0xbd, 0x58, 0xf6, 0xb6, 0x7f, 0xeb, 0x6a, 0xcd, 0x0a, 0xa3, 0x5d, 0xac, 0x16, 0xc5, 0x8b, 0x54, 0x6d, - 0x1f, 0xa8, 0xbb, 0xca, 0x7d, 0xc7, 0x40, 0xa3, 0x46, 0x2a, 0x5b, 0x51, 0x47, 0x6a, 0x78, 0xcc, 0x5f, 0x9b, 0xe9, - 0x88, 0x31, 0x6c, 0xd8, 0xd1, 0x41, 0xb3, 0xb9, 0x0c, 0x8a, 0xa9, 0xc5, 0x61, 0x54, 0x1a, 0xba, 0x8d, 0xc8, 0x57, - 0x28, 0xcf, 0xec, 0x1b, 0x63, 0x43, 0x2c, 0xd9, 0x53, 0xbc, 0x06, 0xc2, 0x24, 0xa5, 0xcf, 0x62, 0x8b, 0xc2, 0xa6, - 0xcd, 0xed, 0x99, 0x63, 0x03, 0x0e, 0xae, 0x92, 0x52, 0xa6, 0xab, 0xc2, 0xab, 0x40, 0x29, 0xac, 0x44, 0x67, 0x09, - 0x21, 0x63, 0x9e, 0xbd, 0xf3, 0x53, 0xd3, 0x73, 0x8f, 0x80, 0x68, 0xf6, 0x05, 0x1c, 0x05, 0x1f, 0xc4, 0x88, 0x8f, - 0x34, 0xe4, 0x1c, 0xbe, 0x72, 0x98, 0xbe, 0xb7, 0x85, 0xe4, 0x47, 0x3f, 0x1f, 0x2f, 0x94, 0x29, 0x49, 0xb5, 0x83, - 0xd0, 0x06, 0x12, 0x67, 0x80, 0x78, 0x96, 0x81, 0x25, 0x28, 0x8d, 0x01, 0x83, 0x83, 0xcf, 0x47, 0xbb, 0x22, 0xd4, - 0x12, 0xa1, 0xbb, 0x2c, 0x5d, 0x80, 0xb3, 0x6e, 0x90, 0xd1, 0x26, 0xf6, 0x70, 0x7f, 0xe1, 0x80, 0xee, 0xc4, 0xe0, - 0xc8, 0xc9, 0xec, 0xb2, 0x25, 0xc1, 0xc4, 0xbf, 0x8b, 0xa6, 0x8d, 0x25, 0x52, 0x21, 0xde, 0x58, 0x3a, 0xc0, 0x4c, - 0xa1, 0x3d, 0x55, 0xeb, 0x8e, 0x48, 0xf1, 0x1b, 0xe0, 0x41, 0x34, 0x42, 0x03, 0x47, 0xa2, 0x7e, 0x1e, 0xa3, 0x25, - 0xc6, 0x23, 0xce, 0x7f, 0x4c, 0x2d, 0x07, 0x93, 0x04, 0x72, 0x18, 0xed, 0x1e, 0x3b, 0x13, 0x8a, 0xb3, 0x9d, 0xb4, - 0x6c, 0x3d, 0xfd, 0xdc, 0xa6, 0x0f, 0x66, 0xef, 0x15, 0xde, 0x10, 0x5c, 0x28, 0xfa, 0xcb, 0x2d, 0xcf, 0x30, 0x02, - 0x0c, 0x86, 0xdd, 0x60, 0xfe, 0xfd, 0xe9, 0x24, 0x3a, 0x3c, 0xaa, 0x1f, 0xae, 0x7a, 0x3b, 0x98, 0x3a, 0x93, 0xc1, - 0xf9, 0xe4, 0x97, 0x89, 0xbb, 0xef, 0x44, 0xf2, 0xc5, 0x94, 0x79, 0x8e, 0x7c, 0xd2, 0x09, 0xcc, 0x76, 0x0d, 0xa3, - 0x9a, 0x5a, 0x02, 0x91, 0x88, 0xa9, 0xd0, 0x8d, 0x94, 0xf3, 0x72, 0x7b, 0x4b, 0xe1, 0xfb, 0x6d, 0xaa, 0x52, 0xa5, - 0x46, 0x11, 0x96, 0x9b, 0xf4, 0x83, 0x83, 0xee, 0xf7, 0xa5, 0xbc, 0x5c, 0x4e, 0x6b, 0x91, 0xc7, 0x43, 0x21, 0xea, - 0x7c, 0xa4, 0xbd, 0x7f, 0xa2, 0xf3, 0x33, 0x49, 0xc8, 0xae, 0xff, 0x54, 0x11, 0x60, 0xfc, 0x15, 0xa2, 0xae, 0x4d, - 0x32, 0xa8, 0xd4, 0x4b, 0x2b, 0xbc, 0x83, 0xaf, 0x88, 0xdc, 0x0a, 0xfa, 0x95, 0x51, 0xe5, 0xad, 0x57, 0x6d, 0x97, - 0xb3, 0x2f, 0xb0, 0x60, 0xd3, 0x9a, 0x0e, 0x5e, 0xf9, 0xeb, 0xe0, 0xa8, 0xa0, 0x37, 0x9c, 0x3a, 0x23, 0xf5, 0x10, - 0xef, 0xe7, 0x02, 0x05, 0x27, 0xc4, 0x3f, 0x0a, 0x86, 0x46, 0xe9, 0x5a, 0x6a, 0x63, 0x6c, 0x0f, 0x98, 0xaf, 0x57, - 0x95, 0x71, 0x95, 0xdd, 0x09, 0x1e, 0x3b, 0x37, 0x3e, 0x85, 0x91, 0xb4, 0x3c, 0xc0, 0x39, 0xab, 0x43, 0x07, 0xce, - 0x6b, 0xf6, 0x85, 0x6a, 0x3d, 0x14, 0x33, 0x12, 0x6d, 0x4d, 0xb0, 0x8c, 0x3c, 0x9b, 0xb5, 0xe7, 0xa9, 0x49, 0x66, - 0x35, 0xd2, 0x66, 0x7c, 0x6a, 0xfa, 0xaf, 0x01, 0xb1, 0x1e, 0x74, 0xf9, 0x6d, 0xa5, 0xfa, 0x5a, 0x21, 0xeb, 0x11, - 0xc7, 0x4a, 0x95, 0x6d, 0x83, 0x63, 0x07, 0x6e, 0x35, 0x1e, 0x0f, 0xbe, 0x17, 0xd2, 0x58, 0x9d, 0x04, 0x2e, 0x9d, - 0x50, 0xf9, 0x86, 0x2b, 0x06, 0x76, 0x12, 0xdd, 0x2c, 0x17, 0x51, 0x22, 0x45, 0xfe, 0x36, 0x70, 0x8a, 0xe1, 0x50, - 0x08, 0x0f, 0xe2, 0xdf, 0x24, 0x09, 0xf3, 0x3a, 0x52, 0x9d, 0x58, 0xed, 0xe0, 0x7a, 0x95, 0x1e, 0x05, 0x07, 0x6b, - 0xaa, 0xa4, 0x0d, 0x25, 0xea, 0x52, 0x8f, 0x61, 0x4d, 0x0f, 0x87, 0x7a, 0x71, 0xe3, 0x70, 0xe5, 0x63, 0xcd, 0xa2, - 0xf5, 0x17, 0x35, 0x1c, 0xab, 0x11, 0x36, 0x53, 0x11, 0xcd, 0xec, 0xff, 0x88, 0x2b, 0x1d, 0xb2, 0x0b, 0x80, 0xda, - 0x8f, 0xf8, 0x06, 0x55, 0x31, 0x02, 0xb4, 0x9f, 0x96, 0x6f, 0xa4, 0x3e, 0xe5, 0x19, 0x8b, 0xeb, 0x16, 0x51, 0xe4, - 0x22, 0x18, 0x6b, 0x8a, 0x0d, 0x00, 0x61, 0xd9, 0x02, 0x1b, 0x88, 0xa2, 0x59, 0x94, 0x4d, 0xdd, 0x60, 0xb7, 0x78, - 0x01, 0xd1, 0x9a, 0xc7, 0x67, 0x62, 0xcd, 0x9c, 0x1b, 0xa9, 0x2c, 0x2b, 0x7c, 0xff, 0xea, 0x8a, 0xb9, 0x42, 0x83, - 0xf7, 0xf6, 0xdc, 0xca, 0x1e, 0x9d, 0x0f, 0x76, 0x33, 0xfd, 0x0b, 0xbb, 0x0e, 0x6f, 0xd9, 0x26, 0xcc, 0x08, 0x9f, - 0xdc, 0x3e, 0xfe, 0x8a, 0x35, 0xe1, 0xfc, 0x47, 0x51, 0x31, 0x28, 0x5c, 0x41, 0xb0, 0xa8, 0x35, 0xe3, 0x94, 0xc2, - 0x63, 0x1f, 0xa8, 0xd0, 0x1e, 0x24, 0x26, 0x08, 0xa3, 0x2a, 0x53, 0x25, 0xb2, 0xe7, 0xe2, 0x57, 0x6d, 0x22, 0x83, - 0xc9, 0x38, 0x94, 0x0d, 0xdc, 0xd4, 0xae, 0x39, 0x33, 0x3b, 0x4b, 0xeb, 0xdf, 0x6b, 0x8e, 0x75, 0x58, 0xb0, 0x44, - 0x6b, 0xa8, 0x99, 0x5e, 0x56, 0x2d, 0xc2, 0x5b, 0xc3, 0x74, 0x78, 0x08, 0x52, 0xcb, 0x22, 0xe1, 0x0f, 0xdd, 0x77, - 0xd0, 0x22, 0x18, 0xa3, 0x11, 0x58, 0x19, 0xa7, 0x90, 0xeb, 0xfc, 0x38, 0x25, 0x0a, 0xd4, 0xb2, 0xde, 0x67, 0x2c, - 0x73, 0xe4, 0x35, 0x2b, 0xf3, 0x34, 0x2b, 0x7a, 0x8f, 0xb2, 0xa1, 0xe3, 0xfa, 0x73, 0x26, 0x1a, 0x49, 0x87, 0x86, - 0x3a, 0x1d, 0xe7, 0xc4, 0x95, 0x35, 0x47, 0x53, 0x24, 0xb7, 0xf5, 0x40, 0xda, 0xcd, 0x6c, 0x25, 0x4c, 0xb6, 0xd8, - 0x6c, 0x46, 0xd8, 0xee, 0x68, 0xec, 0x33, 0x4f, 0x1c, 0xd7, 0x10, 0x3d, 0xd0, 0xe6, 0xce, 0x4b, 0x6e, 0x5c, 0xfc, - 0xef, 0xa0, 0x88, 0x6e, 0x1e, 0x8e, 0x08, 0xe6, 0x72, 0x4e, 0x51, 0x3c, 0xdd, 0x1c, 0x87, 0xc0, 0x86, 0xf5, 0x9f, - 0x9b, 0xe8, 0x4a, 0x8e, 0xe7, 0xa8, 0xd2, 0x23, 0x05, 0x71, 0x62, 0x7b, 0x76, 0x0d, 0x49, 0xfb, 0x11, 0x09, 0xcf, - 0x29, 0xeb, 0x6c, 0x74, 0xae, 0x73, 0x5d, 0x7a, 0x1f, 0x7f, 0x25, 0x3d, 0x21, 0x08, 0x0c, 0xf3, 0xa7, 0xb8, 0x9f, - 0xc0, 0x8a, 0x0b, 0xab, 0x52, 0xae, 0x78, 0xe1, 0x5f, 0x73, 0xc6, 0xf7, 0xb4, 0x2a, 0x2b, 0xd9, 0x71, 0x79, 0xa5, - 0x73, 0xd6, 0x50, 0xa5, 0x63, 0xa6, 0xcb, 0x8a, 0xc5, 0xf4, 0x8e, 0xfd, 0xba, 0x36, 0x04, 0x34, 0x74, 0xe7, 0xdc, - 0x51, 0x31, 0x93, 0xe0, 0x69, 0x88, 0xa5, 0x52, 0x80, 0xae, 0xd0, 0x67, 0xe6, 0xe4, 0x9b, 0x61, 0x1e, 0x0c, 0xf9, - 0x59, 0x00, 0x08, 0x57, 0x26, 0xa8, 0xac, 0xc0, 0xb3, 0xe2, 0x5a, 0xd1, 0x79, 0x0d, 0xe6, 0x22, 0xa2, 0xde, 0x6b, - 0xa4, 0xff, 0x00, 0x09, 0x97, 0x60, 0x2f, 0x05, 0x2e, 0x06, 0x74, 0xf9, 0xcc, 0x1d, 0x5a, 0x97, 0x08, 0x31, 0xd6, - 0x80, 0xa4, 0xb6, 0xf1, 0xcb, 0xc5, 0x84, 0x7b, 0xde, 0xcf, 0x03, 0xce, 0xba, 0x7e, 0x06, 0x90, 0x07, 0xf9, 0xf3, - 0x57, 0xb7, 0x72, 0x39, 0xc8, 0x09, 0x48, 0x5c, 0x5c, 0xb8, 0xf2, 0x88, 0x76, 0x4e, 0x8b, 0xb6, 0xcc, 0xd5, 0x28, - 0xe3, 0xb6, 0x06, 0x29, 0x52, 0xb8, 0xd8, 0x48, 0xfb, 0x18, 0xb8, 0x20, 0xe9, 0x89, 0x0d, 0x85, 0x84, 0x1d, 0xbb, - 0xf6, 0x62, 0x2a, 0xb7, 0x33, 0xea, 0x06, 0xfa, 0x62, 0xeb, 0x6f, 0x34, 0xfe, 0xb4, 0xb1, 0x76, 0xa6, 0xef, 0x19, - 0x5c, 0x11, 0xa9, 0x46, 0x9f, 0x57, 0x58, 0x7d, 0xda, 0xef, 0xca, 0x1d, 0xac, 0xd6, 0x97, 0xd1, 0x57, 0x15, 0x1b, - 0xf5, 0x89, 0x0d, 0x82, 0x49, 0x92, 0x54, 0x72, 0x6b, 0x50, 0x52, 0xd0, 0x98, 0xb7, 0x51, 0x43, 0x56, 0x4a, 0x6b, - 0x26, 0x7b, 0xf1, 0xbf, 0x73, 0xc5, 0xcc, 0xc4, 0xc0, 0x8f, 0xb1, 0xa5, 0x3e, 0x79, 0xf4, 0xc4, 0x5b, 0xeb, 0xf7, - 0x9c, 0xa1, 0x63, 0xf6, 0x00, 0x81, 0x42, 0x60, 0x5e, 0xba, 0xc4, 0x9c, 0x5b, 0x33, 0x6b, 0xd6, 0xd4, 0xcb, 0x7f, - 0x66, 0x57, 0xba, 0xc0, 0xd8, 0x27, 0x82, 0xfe, 0x5c, 0xda, 0xed, 0xd4, 0x37, 0x66, 0xef, 0x06, 0x9c, 0x06, 0x98, - 0xb9, 0x78, 0x53, 0xe9, 0xdd, 0xd5, 0xe6, 0x11, 0x0b, 0x60, 0x72, 0x36, 0xfa, 0x97, 0xa6, 0x22, 0xf8, 0xcb, 0xa3, - 0xb3, 0x17, 0xeb, 0x23, 0x0a, 0x05, 0x5f, 0x46, 0x23, 0xde, 0x65, 0xf4, 0x2f, 0x1a, 0x5a, 0xff, 0x03, 0xfb, 0x60, - 0x1b, 0x97, 0x61, 0x0f, 0xed, 0xc3, 0x24, 0x76, 0x45, 0xd0, 0xd6, 0xc6, 0x82, 0x20, 0x6b, 0xea, 0x72, 0x60, 0x44, - 0x8a, 0xdf, 0x5a, 0x27, 0x9d, 0xd7, 0xb1, 0xef, 0xda, 0x89, 0xf3, 0x21, 0x11, 0x23, 0xf0, 0x5b, 0xf4, 0x7c, 0x24, - 0xa1, 0x82, 0x4b, 0x47, 0x2f, 0x13, 0x3c, 0xea, 0x12, 0x27, 0xd5, 0xae, 0x97, 0xa3, 0xf6, 0xcf, 0xfa, 0x66, 0x3f, - 0x18, 0x94, 0xae, 0x1b, 0x86, 0x6f, 0xe9, 0xb5, 0xcc, 0x91, 0x87, 0x77, 0x7d, 0xa3, 0xb5, 0x05, 0xd6, 0xba, 0x6c, - 0x0b, 0x45, 0x9d, 0xf0, 0xfa, 0x5d, 0xe3, 0xf8, 0xbf, 0x94, 0x59, 0xc1, 0x50, 0x98, 0xcc, 0x44, 0xbd, 0xd9, 0x82, - 0x74, 0x16, 0x7a, 0x7b, 0xd7, 0xbf, 0x54, 0x9a, 0x03, 0xb6, 0x98, 0x31, 0x38, 0xd5, 0x83, 0x66, 0xf0, 0x12, 0x0a, - 0x84, 0xb9, 0x77, 0x86, 0xce, 0xa0, 0xfb, 0xd5, 0x09, 0xca, 0x44, 0x31, 0xe8, 0x59, 0x0a, 0x25, 0x6d, 0x42, 0x6a, - 0xdd, 0xef, 0x0d, 0x6e, 0x7d, 0xe8, 0xdf, 0xcc, 0x28, 0xa2, 0x51, 0xef, 0x9c, 0x24, 0xa0, 0xe8, 0x15, 0x07, 0x3a, - 0x51, 0xde, 0x6c, 0x89, 0x11, 0xeb, 0x78, 0x9c, 0xe4, 0xea, 0xe0, 0xf1, 0x4a, 0xc9, 0xf1, 0xaa, 0x10, 0x7a, 0x0e, - 0x60, 0x88, 0x23, 0x70, 0x2f, 0x87, 0x05, 0x74, 0x01, 0xcf, 0xf4, 0x8e, 0x7a, 0x36, 0x73, 0xb4, 0xfb, 0x7f, 0x97, - 0x7b, 0xd4, 0x5b, 0x3c, 0xdb, 0x24, 0x0e, 0x58, 0xd6, 0x34, 0x02, 0xdf, 0xfc, 0xf4, 0xae, 0xd6, 0x63, 0xc9, 0x9b, - 0x2d, 0x95, 0x39, 0xd8, 0x10, 0x5d, 0xa7, 0x45, 0xd2, 0xa7, 0x5c, 0x1d, 0xdb, 0x14, 0x50, 0xc3, 0xfd, 0xb4, 0x73, - 0x45, 0x78, 0x9c, 0xb0, 0x86, 0x73, 0x2a, 0x1c, 0x76, 0x70, 0xb4, 0x11, 0x46, 0x37, 0xe4, 0x18, 0x4b, 0xea, 0x20, - 0xbe, 0x1d, 0xe0, 0x13, 0x7c, 0xbf, 0x30, 0xca, 0x97, 0x0e, 0xf1, 0x47, 0x06, 0x8d, 0x0e, 0x72, 0x89, 0x95, 0x3c, - 0x61, 0xea, 0xaf, 0x95, 0xda, 0xca, 0x75, 0xb9, 0xb9, 0xb7, 0x57, 0xb7, 0x92, 0x59, 0x38, 0xc9, 0x28, 0x3e, 0x92, - 0x1e, 0xd5, 0x2b, 0xf9, 0xcf, 0xed, 0xc6, 0x20, 0x99, 0xb9, 0xbd, 0x7b, 0x27, 0x30, 0x76, 0xa8, 0x74, 0xa2, 0xe0, - 0x5f, 0x22, 0xe1, 0x67, 0xa3, 0x11, 0x29, 0x28, 0x2c, 0xb9, 0x0a, 0x55, 0x68, 0x9f, 0xb9, 0xe9, 0xa5, 0xa2, 0x72, - 0x8c, 0x51, 0x31, 0x9b, 0xf1, 0x8b, 0xa1, 0x1a, 0x23, 0xf5, 0xd3, 0x9c, 0x6d, 0xbf, 0xf5, 0x44, 0xaf, 0x45, 0x73, - 0x20, 0x09, 0x1a, 0x57, 0x02, 0x14, 0xe0, 0x10, 0x13, 0x8c, 0xc9, 0x5d, 0x62, 0xd0, 0x34, 0xc3, 0xf3, 0x14, 0xea, - 0x5a, 0x8d, 0x27, 0x95, 0x6f, 0x6d, 0x97, 0x95, 0x54, 0xb6, 0x13, 0xa3, 0x79, 0x27, 0x41, 0xe2, 0xa8, 0x71, 0x8a, - 0x82, 0x55, 0xf5, 0x0c, 0x29, 0xc3, 0x12, 0x20, 0xad, 0x38, 0x87, 0x6f, 0xcf, 0x43, 0x66, 0x17, 0x96, 0xd8, 0x2b, - 0xdd, 0x2c, 0x85, 0x08, 0x6e, 0x17, 0x15, 0x09, 0xb9, 0xbe, 0x65, 0x93, 0x2c, 0x74, 0xe9, 0x5b, 0x67, 0xe8, 0x12, - 0xd2, 0x87, 0x1c, 0xf5, 0xdb, 0xbd, 0x04, 0x9c, 0x20, 0x8c, 0x8d, 0x09, 0xd9, 0x7c, 0xd4, 0x0b, 0xf2, 0x28, 0x6f, - 0x05, 0x8d, 0xab, 0xcd, 0xd2, 0xfb, 0x9f, 0x30, 0x1a, 0xca, 0x65, 0x43, 0x26, 0xb3, 0xa2, 0x83, 0xe6, 0xab, 0x65, - 0x6c, 0x0e, 0x2a, 0xc8, 0x31, 0x27, 0x01, 0x7a, 0x5c, 0x81, 0xe7, 0x96, 0x45, 0xbd, 0x49, 0xf5, 0x67, 0xc5, 0x0b, - 0x5d, 0x83, 0xdd, 0xd7, 0x0e, 0x62, 0xc7, 0x26, 0x53, 0xbb, 0x58, 0x05, 0x4a, 0xe2, 0x88, 0x6e, 0x85, 0x3e, 0x85, - 0x2a, 0x77, 0xa4, 0x10, 0xc3, 0x3a, 0xc0, 0xc2, 0x59, 0xc9, 0x4c, 0xd8, 0x3e, 0xcc, 0xe7, 0x8f, 0x51, 0x6b, 0x01, - 0xd3, 0x43, 0x08, 0xf5, 0xdd, 0x1d, 0xee, 0x28, 0x3a, 0x3a, 0x93, 0xc9, 0x5d, 0x56, 0xc8, 0xa0, 0x5f, 0xf8, 0x58, - 0xc0, 0x05, 0x57, 0xe4, 0x92, 0xb1, 0xa0, 0xe9, 0x14, 0x4c, 0xcb, 0xd4, 0xb9, 0xfc, 0xdd, 0xfb, 0x98, 0x40, 0x2d, - 0x88, 0x45, 0xd3, 0x84, 0x13, 0xd4, 0xd0, 0x1d, 0x44, 0x6b, 0xda, 0x93, 0xc7, 0x8b, 0xec, 0x19, 0xc6, 0xca, 0x09, - 0xfe, 0xd4, 0xe5, 0xba, 0xfa, 0xf2, 0x5d, 0x90, 0x4a, 0xef, 0x8d, 0x4e, 0x4b, 0xd2, 0x3b, 0xca, 0x11, 0xd1, 0xa4, - 0xe3, 0x6f, 0x1f, 0x91, 0xb7, 0x20, 0x13, 0x1b, 0x3e, 0x5c, 0xd6, 0x9a, 0xf7, 0x5f, 0x51, 0xb0, 0x4a, 0x11, 0xce, - 0x7e, 0xa2, 0x49, 0x1c, 0xb2, 0x15, 0x21, 0xed, 0x8b, 0x60, 0xa4, 0xa3, 0x82, 0xd8, 0x8a, 0xed, 0x6a, 0x6d, 0xb9, - 0x87, 0x40, 0xc4, 0x39, 0xb8, 0x42, 0x66, 0x19, 0x9c, 0x63, 0xaf, 0x7e, 0x79, 0x80, 0xe0, 0xf2, 0x14, 0xf5, 0xbf, - 0x5e, 0x16, 0x7e, 0xf4, 0x70, 0xa0, 0x75, 0x64, 0x65, 0x65, 0x4e, 0xbd, 0x54, 0x1f, 0xcb, 0x3a, 0x1e, 0xad, 0xfa, - 0x9a, 0x7e, 0xa3, 0x94, 0x46, 0x9b, 0x41, 0x8b, 0xdb, 0x94, 0x95, 0x1a, 0xc3, 0x9f, 0x59, 0x2d, 0xea, 0xa1, 0xc2, - 0x1d, 0xae, 0x0d, 0xde, 0xb3, 0x77, 0x30, 0x91, 0x22, 0xef, 0xdb, 0x3f, 0x35, 0xb8, 0x21, 0x61, 0x3a, 0xe1, 0x90, - 0x3b, 0x70, 0x05, 0xd3, 0x93, 0x4e, 0xdd, 0x35, 0xc4, 0xd7, 0x22, 0xc9, 0x8e, 0xfe, 0x1b, 0x05, 0xcf, 0x17, 0x32, - 0xd6, 0x84, 0x8c, 0x6e, 0x0b, 0x6b, 0x11, 0x69, 0xa5, 0xc1, 0xc4, 0x18, 0xc5, 0x7c, 0x4a, 0x94, 0x88, 0x65, 0xb7, - 0x25, 0x23, 0xb1, 0xcf, 0xd6, 0x96, 0xbd, 0xd5, 0x4d, 0x4b, 0x82, 0x96, 0xa5, 0x20, 0x5e, 0x2e, 0xcf, 0x44, 0x15, - 0xd0, 0xb5, 0x71, 0x03, 0x22, 0x4e, 0xef, 0xac, 0xf6, 0x16, 0x04, 0xd0, 0x3e, 0xff, 0xfb, 0x4a, 0xe9, 0xe2, 0x56, - 0x85, 0x12, 0x82, 0x1f, 0xb2, 0x4c, 0x96, 0x40, 0x19, 0xe4, 0x63, 0xcb, 0x07, 0xf7, 0x15, 0x56, 0xeb, 0xbb, 0xf5, - 0x10, 0xb1, 0x79, 0x3e, 0x84, 0xb4, 0x83, 0xe1, 0x99, 0x02, 0x4f, 0xf6, 0x2f, 0xdb, 0x87, 0x0d, 0xd0, 0xba, 0xc9, - 0x50, 0x7e, 0x57, 0xaa, 0x89, 0x32, 0x82, 0x8f, 0x5f, 0xed, 0x70, 0x61, 0x43, 0xed, 0xc0, 0x68, 0x1a, 0x76, 0xcb, - 0x3f, 0x20, 0x56, 0x48, 0xe8, 0xea, 0x08, 0x60, 0xeb, 0x32, 0x26, 0x7c, 0xc9, 0xbe, 0x41, 0x18, 0x00, 0x89, 0xdf, - 0xfe, 0xaa, 0x1d, 0x9f, 0x98, 0xeb, 0xf2, 0xfb, 0xb6, 0xbd, 0x4a, 0x44, 0xef, 0x63, 0x27, 0x66, 0x3b, 0xf6, 0x01, - 0x2b, 0x1e, 0x56, 0x8d, 0xe8, 0xd8, 0xf3, 0xa1, 0x70, 0x9f, 0xe2, 0xd1, 0xd6, 0x21, 0xfa, 0x9d, 0x28, 0xb2, 0xc5, - 0x76, 0xc9, 0xfe, 0x42, 0x4b, 0xe7, 0xd3, 0x07, 0x9a, 0x41, 0xdd, 0x1e, 0x23, 0xaf, 0x22, 0x80, 0x78, 0x0c, 0x76, - 0xe1, 0xeb, 0x32, 0xef, 0x99, 0xbc, 0x02, 0xfc, 0x9c, 0x72, 0xf2, 0x97, 0xe7, 0x8b, 0x26, 0x22, 0xe8, 0xb3, 0x2e, - 0x49, 0x02, 0x22, 0xe2, 0x71, 0x3a, 0x3b, 0x36, 0x4d, 0x7a, 0x19, 0x39, 0x3c, 0x62, 0x33, 0x2b, 0xdf, 0xb1, 0xaa, - 0x8b, 0xb3, 0x5b, 0x3e, 0xda, 0x5f, 0xe8, 0x41, 0x67, 0x90, 0xa8, 0x5d, 0x9c, 0xc9, 0x68, 0x76, 0x64, 0x1a, 0x63, - 0x43, 0xb4, 0x97, 0x8a, 0x29, 0x19, 0x66, 0x39, 0x46, 0x1d, 0xd7, 0x46, 0x4e, 0x97, 0x93, 0x25, 0x0e, 0xc3, 0x12, - 0xe3, 0x7d, 0x1a, 0x10, 0xf4, 0x72, 0x05, 0x1d, 0xec, 0xe2, 0x5c, 0x6f, 0x87, 0x1c, 0x1a, 0x10, 0x97, 0x1a, 0xef, - 0xe2, 0x5c, 0xf7, 0xa0, 0xca, 0x53, 0x64, 0xc5, 0xc3, 0x9f, 0x52, 0xbf, 0x54, 0x8e, 0xf1, 0x9e, 0x81, 0xc4, 0xd8, - 0x6f, 0x6c, 0xcf, 0xfd, 0x26, 0x28, 0x66, 0x99, 0xa2, 0x91, 0x9e, 0x17, 0xee, 0xc1, 0x6c, 0x4f, 0xdb, 0xab, 0xd1, - 0x54, 0xc1, 0xcc, 0xa2, 0x13, 0xc0, 0xe6, 0x0f, 0xc4, 0x54, 0x45, 0x57, 0x3c, 0x52, 0x08, 0xc2, 0x70, 0xb5, 0xde, - 0x91, 0xed, 0xb3, 0x42, 0x68, 0xb9, 0x63, 0x26, 0x19, 0xf8, 0xb9, 0xf1, 0x61, 0xd6, 0x35, 0xbe, 0xa8, 0x27, 0x40, - 0x33, 0x71, 0xe5, 0xc3, 0xc7, 0xc9, 0x42, 0x61, 0x82, 0x92, 0xd1, 0x4f, 0xae, 0xa6, 0x5a, 0xd2, 0x9d, 0x74, 0xd8, - 0x9b, 0x2d, 0x5f, 0x27, 0x65, 0x1d, 0x76, 0x29, 0xfb, 0x58, 0xca, 0x03, 0xed, 0x76, 0x33, 0xdb, 0xc3, 0xdf, 0x70, - 0xf3, 0x01, 0xa0, 0x8b, 0x84, 0x95, 0x49, 0x6e, 0xd1, 0x80, 0x5f, 0x7c, 0x30, 0x38, 0x19, 0xc3, 0xf6, 0xe0, 0xc5, - 0xdc, 0x61, 0x9d, 0x63, 0xff, 0xd6, 0x91, 0x9b, 0x38, 0x0a, 0xa4, 0xe4, 0xab, 0x85, 0x45, 0x15, 0xa2, 0xc3, 0x40, - 0xe3, 0xaa, 0xcf, 0x13, 0xb0, 0x90, 0x33, 0xb5, 0x26, 0xd9, 0xfc, 0x53, 0x05, 0xc4, 0xf3, 0xd9, 0x72, 0x08, 0x24, - 0xc8, 0xb7, 0xb2, 0x5a, 0x16, 0xaf, 0x09, 0x27, 0xb0, 0x3d, 0x82, 0x45, 0x63, 0x77, 0x04, 0x00, 0x5a, 0xe8, 0x20, - 0xa4, 0xd4, 0x85, 0x0b, 0x65, 0x2f, 0xd7, 0xc8, 0x86, 0xa9, 0x6b, 0x81, 0x17, 0xdf, 0x4e, 0x38, 0xfa, 0xf7, 0x47, - 0x43, 0xb2, 0x8e, 0x00, 0x2e, 0x27, 0x78, 0x1f, 0x36, 0x8d, 0x3d, 0x03, 0xce, 0x48, 0xfb, 0xa2, 0x70, 0x45, 0x3f, - 0x0c, 0xac, 0x0b, 0xf1, 0x2c, 0x38, 0x47, 0x26, 0xbb, 0x12, 0xfa, 0x45, 0xd1, 0x0c, 0x09, 0x5e, 0x30, 0x8e, 0x6d, - 0xe0, 0x73, 0x07, 0xf4, 0xd3, 0x98, 0x8b, 0xb6, 0x05, 0x1e, 0x2b, 0xaa, 0xcc, 0x29, 0x87, 0x6e, 0x10, 0xad, 0xbd, - 0xfa, 0x5c, 0xea, 0x3b, 0x9c, 0x95, 0xce, 0x8a, 0x7b, 0x97, 0x55, 0x0f, 0x05, 0x9f, 0x20, 0xc7, 0xfb, 0x57, 0x14, - 0xfb, 0x9f, 0x36, 0xe2, 0x68, 0xc1, 0xa6, 0x00, 0x0c, 0x20, 0x21, 0xd3, 0x08, 0xdb, 0x3a, 0x09, 0x3a, 0x7e, 0x28, - 0x3d, 0x46, 0x1c, 0x4a, 0x5a, 0x61, 0x70, 0x98, 0xaa, 0x6f, 0x83, 0x0c, 0x29, 0x79, 0xb9, 0x94, 0x1e, 0x86, 0x18, - 0x39, 0x20, 0x95, 0xb9, 0xf2, 0x3d, 0x7b, 0x55, 0x3c, 0x51, 0xea, 0xc4, 0x07, 0x10, 0x8b, 0xa1, 0x47, 0x46, 0x7d, - 0x20, 0x53, 0x5d, 0x80, 0x26, 0x86, 0x90, 0x51, 0x02, 0x88, 0x8d, 0xa1, 0x11, 0x02, 0x25, 0xe4, 0xd8, 0xfa, 0xc5, - 0xac, 0x0a, 0x12, 0xa1, 0x88, 0x45, 0x4b, 0xb4, 0x38, 0x62, 0x14, 0x60, 0x86, 0x34, 0xd0, 0x63, 0xee, 0x9a, 0x0e, - 0x8c, 0x0b, 0x30, 0xa6, 0xe2, 0x1e, 0x40, 0x7e, 0x33, 0x86, 0xb1, 0x88, 0xe0, 0xe5, 0xae, 0x3c, 0x4f, 0x1a, 0x35, - 0x58, 0xc3, 0x5a, 0x34, 0x17, 0xab, 0xb7, 0x81, 0x99, 0x72, 0x0c, 0xc9, 0x55, 0xab, 0x52, 0xd8, 0xe9, 0xcd, 0x7e, - 0x1f, 0xf2, 0xb9, 0x83, 0xd0, 0xd6, 0xc1, 0x99, 0x25, 0x28, 0x33, 0x12, 0xdb, 0x98, 0x50, 0x40, 0x32, 0xd0, 0x81, - 0xd4, 0x15, 0x88, 0x90, 0x90, 0x64, 0x92, 0x84, 0xe6, 0x64, 0x8a, 0x44, 0x7c, 0x71, 0xc2, 0x5c, 0x1f, 0xc4, 0xc9, - 0x12, 0xd9, 0xbc, 0x6f, 0x97, 0xc0, 0xfc, 0x81, 0x91, 0x59, 0x91, 0xab, 0xaa, 0xa0, 0x01, 0x12, 0x09, 0xa3, 0xd5, - 0x09, 0x43, 0xe7, 0xf5, 0xd9, 0xdf, 0x07, 0x8c, 0x2d, 0x4c, 0xe8, 0x40, 0x30, 0x0c, 0x65, 0x51, 0xa8, 0xe4, 0x4f, - 0x0a, 0x1c, 0x56, 0x68, 0x78, 0x7f, 0x16, 0x7c, 0xf1, 0xd4, 0x62, 0x61, 0x15, 0x1e, 0x09, 0xb9, 0x1f, 0x6a, 0x89, - 0xb3, 0x02, 0x92, 0x13, 0x84, 0x56, 0xf7, 0xef, 0x7f, 0x77, 0x54, 0x12, 0xe6, 0x45, 0x8b, 0xd2, 0xab, 0x23, 0x6e, - 0x73, 0xb5, 0xc0, 0xd0, 0xa4, 0xd9, 0x21, 0xdf, 0x3e, 0x55, 0x22, 0x6e, 0x14, 0x5c, 0xee, 0x42, 0x2c, 0x01, 0x69, - 0x33, 0x18, 0x7c, 0x69, 0x3d, 0xa5, 0x1f, 0x20, 0xf4, 0x8d, 0x7b, 0x76, 0xfa, 0x38, 0x46, 0x32, 0x26, 0x17, 0xd6, - 0xcf, 0xac, 0x6a, 0x35, 0x71, 0x44, 0x42, 0xce, 0x59, 0xe8, 0x50, 0xec, 0xab, 0x61, 0x39, 0x73, 0xc5, 0xd9, 0xc3, - 0xc3, 0x68, 0x05, 0x24, 0x1d, 0x69, 0xb8, 0x21, 0xc7, 0xb3, 0x0f, 0x50, 0xe7, 0x51, 0x30, 0x92, 0x4a, 0xe6, 0xbd, - 0x62, 0x38, 0x6f, 0x88, 0xb6, 0xd4, 0xb3, 0xd6, 0x20, 0x70, 0x4e, 0x16, 0x49, 0xc9, 0x9b, 0x20, 0xb5, 0xf2, 0xf2, - 0x64, 0x1e, 0x31, 0xc5, 0xe9, 0x54, 0x59, 0x61, 0x74, 0x72, 0xd1, 0x73, 0x64, 0x94, 0x5d, 0xb0, 0xa1, 0x9a, 0x4f, - 0x4b, 0x53, 0xee, 0x2b, 0xac, 0x94, 0xae, 0xb4, 0xc0, 0x74, 0x24, 0xc6, 0xea, 0x66, 0x8e, 0xea, 0x81, 0x41, 0xc4, - 0x7a, 0xf9, 0x06, 0x91, 0x87, 0x34, 0xbf, 0x70, 0xa4, 0x22, 0x6d, 0x09, 0xcf, 0x4a, 0x3e, 0x60, 0x36, 0x03, 0xd2, - 0xca, 0xfb, 0x04, 0x5c, 0xf9, 0x4d, 0x81, 0x82, 0xe4, 0x8b, 0xf3, 0x04, 0xcd, 0x20, 0x7e, 0x1d, 0x64, 0xb3, 0xb1, - 0x11, 0xe3, 0xf9, 0xd6, 0xe0, 0xd5, 0x10, 0x39, 0x58, 0x1d, 0xfd, 0xba, 0x1b, 0xb0, 0x75, 0xb8, 0x4d, 0xa7, 0x67, - 0x5f, 0x6a, 0x81, 0x16, 0x83, 0xe3, 0xa9, 0x98, 0xe2, 0xa4, 0x7a, 0x44, 0x2c, 0x53, 0x61, 0x1a, 0x13, 0x5d, 0x21, - 0x6b, 0x6c, 0x29, 0xd8, 0x7c, 0xcb, 0x7b, 0x5e, 0x64, 0x48, 0xb8, 0x6b, 0x44, 0x17, 0xc3, 0x18, 0x04, 0x2f, 0x2f, - 0xa5, 0x73, 0x5f, 0x1b, 0x25, 0x56, 0xcc, 0x13, 0x1f, 0x5e, 0x37, 0x49, 0xf2, 0x82, 0xb4, 0x66, 0xcf, 0x6a, 0x2c, - 0x7f, 0x78, 0xf3, 0x83, 0xa9, 0x4a, 0xac, 0xd9, 0xc9, 0x4f, 0x52, 0xb6, 0xef, 0x87, 0xa6, 0x41, 0xde, 0x56, 0x2c, - 0x7e, 0x69, 0xf2, 0x0d, 0xa2, 0x0b, 0x46, 0xc9, 0x4e, 0x17, 0x8b, 0x75, 0x03, 0xf7, 0xeb, 0x25, 0xe8, 0xca, 0x0c, - 0x83, 0x76, 0xef, 0x6b, 0xdd, 0x2f, 0x8a, 0x48, 0x8f, 0xb1, 0x0f, 0x19, 0x29, 0x5a, 0x89, 0x5a, 0xcb, 0xfc, 0x6c, - 0x5b, 0xeb, 0x08, 0x09, 0x33, 0xd1, 0x4b, 0x73, 0xb4, 0x43, 0x22, 0x56, 0x33, 0x13, 0xa1, 0xc1, 0xba, 0x19, 0x79, - 0x57, 0x53, 0xfe, 0xb4, 0x84, 0x0e, 0x8f, 0xb5, 0xae, 0xda, 0xdc, 0xcb, 0x68, 0x3a, 0x23, 0xae, 0xe7, 0x69, 0xea, - 0x9a, 0xd2, 0xd3, 0xa0, 0xc3, 0x9d, 0x14, 0xb1, 0xc5, 0xad, 0xff, 0xc0, 0x4c, 0x8b, 0x42, 0x42, 0x35, 0x94, 0xb9, - 0xbd, 0xae, 0x1e, 0x4b, 0xd5, 0x53, 0xb2, 0xfb, 0x9e, 0xe8, 0x6b, 0xac, 0xd2, 0xbe, 0x46, 0xb2, 0x6a, 0x85, 0xc7, - 0xc6, 0xb8, 0x0e, 0x9e, 0xf5, 0x1b, 0xdc, 0x24, 0x8a, 0x10, 0xc3, 0xb8, 0xf4, 0x0b, 0x1f, 0xe1, 0x5c, 0xe0, 0xf5, - 0x30, 0x6d, 0xdd, 0x0e, 0xa9, 0xa6, 0x20, 0x8e, 0xdd, 0x16, 0xce, 0xd9, 0xad, 0x39, 0x78, 0xe8, 0x8e, 0xa3, 0xbc, - 0x50, 0x8f, 0xf3, 0x0e, 0x85, 0x76, 0x28, 0x69, 0x78, 0x5c, 0xb7, 0xa3, 0xc9, 0x83, 0x23, 0x9a, 0xb8, 0x5d, 0x6e, - 0x7f, 0x26, 0x94, 0x79, 0x1a, 0x20, 0xa2, 0x31, 0xfc, 0xfb, 0x92, 0x3d, 0x19, 0xd3, 0x09, 0x49, 0x64, 0x43, 0x66, - 0x1b, 0x30, 0xf6, 0x90, 0x48, 0x8f, 0xbf, 0x22, 0xf7, 0x6f, 0x8d, 0x82, 0xe3, 0xa5, 0xb8, 0xa1, 0xa4, 0x3f, 0x2c, - 0xc2, 0x4c, 0x27, 0x31, 0x4d, 0x3c, 0x90, 0xc5, 0x55, 0x00, 0x2e, 0xd3, 0xae, 0xb0, 0x40, 0x96, 0x0b, 0x2c, 0x90, - 0xb2, 0xfa, 0x1c, 0x25, 0x91, 0xb8, 0x47, 0x42, 0x76, 0x3a, 0x79, 0x2f, 0x8e, 0x71, 0xc1, 0x73, 0x35, 0x39, 0xba, - 0xe0, 0xc5, 0x4c, 0x10, 0xb5, 0x3b, 0x8d, 0xf4, 0x22, 0x34, 0xef, 0xe5, 0xea, 0x3a, 0xd2, 0xa7, 0xd0, 0x82, 0x0a, - 0xf5, 0x0b, 0x69, 0xbf, 0x7f, 0x9d, 0xc8, 0x80, 0xa3, 0x41, 0x93, 0x0d, 0x3b, 0x24, 0xac, 0x90, 0xd7, 0x2e, 0xbe, - 0x10, 0x3a, 0x22, 0x33, 0x7a, 0x94, 0x61, 0x7a, 0x99, 0x8f, 0xd1, 0xce, 0x5b, 0x39, 0x9a, 0x2e, 0x1c, 0xfc, 0xe7, - 0xb0, 0xb7, 0x40, 0x87, 0xab, 0xe3, 0x22, 0xdd, 0x4f, 0xce, 0x5c, 0xfc, 0x0f, 0xa6, 0xab, 0xae, 0x7d, 0x36, 0x13, - 0x5f, 0xc9, 0x63, 0x44, 0x7d, 0xd5, 0x0b, 0xa7, 0x34, 0x1b, 0xd5, 0x4c, 0x1f, 0x45, 0xe4, 0x79, 0xa8, 0x72, 0x5b, - 0x30, 0x9e, 0xd6, 0x60, 0xf8, 0xe8, 0x28, 0xe1, 0x10, 0x34, 0xc1, 0x99, 0xb9, 0x1f, 0x51, 0x65, 0x64, 0x09, 0xe3, - 0xc6, 0x02, 0xcb, 0x9b, 0xe9, 0x3c, 0x8e, 0x4b, 0xa1, 0xe5, 0x33, 0xc6, 0xdf, 0xdf, 0xa2, 0xcf, 0x4f, 0x85, 0xcd, - 0x12, 0x17, 0x3f, 0xe8, 0xc4, 0x51, 0x2f, 0x5c, 0x69, 0xeb, 0x14, 0xab, 0x52, 0xd9, 0x4d, 0xed, 0x7c, 0x6c, 0x5b, - 0x5e, 0x4a, 0xc6, 0xa7, 0x14, 0xe5, 0x24, 0xd7, 0x14, 0x8a, 0xc1, 0xc0, 0x1b, 0x59, 0xf5, 0xe7, 0x0b, 0x98, 0xc9, - 0x0d, 0x78, 0xa6, 0xaf, 0x63, 0xbd, 0x03, 0x3c, 0xd8, 0x73, 0x0b, 0x33, 0x57, 0x90, 0xc8, 0xe3, 0xf1, 0x1c, 0x8f, - 0x75, 0xc0, 0xf9, 0x83, 0xdc, 0x3b, 0x0a, 0xf8, 0x6e, 0x00, 0x62, 0x76, 0xde, 0x08, 0xf0, 0x0b, 0xec, 0x70, 0xb6, - 0xc4, 0x12, 0x54, 0x29, 0xd4, 0x82, 0x1d, 0x19, 0x7c, 0x96, 0x60, 0x35, 0x13, 0x70, 0x96, 0x20, 0x28, 0xca, 0x62, - 0xbe, 0x20, 0x28, 0x71, 0x14, 0x4a, 0x66, 0x2e, 0x3f, 0x35, 0x9b, 0xa2, 0x28, 0x12, 0xe1, 0xa7, 0x76, 0x70, 0x9e, - 0x11, 0x2e, 0xf1, 0xb5, 0xa2, 0xca, 0x07, 0x06, 0x5f, 0x10, 0x68, 0x80, 0xfe, 0xcd, 0x54, 0x44, 0xfb, 0x73, 0xd2, - 0x28, 0x29, 0xdc, 0xb3, 0xb0, 0x18, 0x67, 0x9d, 0x59, 0xd2, 0x6f, 0xb2, 0xcc, 0x6b, 0xd1, 0xcc, 0xaf, 0x6c, 0xc8, - 0x5a, 0xe7, 0xbb, 0x9f, 0xf7, 0x03, 0xa5, 0x9d, 0xf5, 0xcc, 0x92, 0x7d, 0xb4, 0x67, 0x9a, 0x36, 0x0b, 0x87, 0x9e, - 0xc5, 0xd5, 0x0d, 0x53, 0x10, 0x07, 0x5e, 0x9e, 0x46, 0x2a, 0x03, 0x7f, 0x2a, 0x0a, 0x38, 0x52, 0x4e, 0xf1, 0x5b, - 0x4a, 0x78, 0x37, 0xbf, 0x20, 0x8e, 0xdd, 0x5d, 0xfd, 0x0a, 0x90, 0xb5, 0x85, 0xd5, 0xc1, 0x4d, 0x8e, 0x9b, 0xa8, - 0x21, 0xca, 0xc1, 0xdb, 0x40, 0xbe, 0x34, 0x4f, 0x5a, 0xe2, 0xa8, 0x97, 0x45, 0xab, 0xcf, 0xd3, 0xdc, 0x10, 0xf8, - 0xa9, 0x0b, 0xc7, 0xe3, 0x3c, 0xfa, 0xe6, 0xd0, 0x34, 0xf2, 0x63, 0xd2, 0xf6, 0x80, 0x81, 0xa4, 0x99, 0x68, 0xe3, - 0x23, 0x5b, 0x4e, 0x77, 0x3b, 0x0b, 0xe9, 0x7a, 0x3d, 0x0d, 0xa5, 0xb0, 0x58, 0xb8, 0x70, 0x34, 0x66, 0x9f, 0xd0, - 0xe9, 0xd6, 0x6c, 0x48, 0x74, 0x07, 0xc3, 0x95, 0x18, 0xb9, 0x0e, 0xe3, 0x9c, 0xd9, 0x70, 0x84, 0x95, 0xea, 0xb1, - 0x37, 0x6e, 0x1b, 0x12, 0x3c, 0xa1, 0xe2, 0xc8, 0x03, 0x8f, 0xf0, 0x59, 0x1d, 0x74, 0x78, 0x98, 0x07, 0x2e, 0xf9, - 0x06, 0x73, 0x75, 0x04, 0x03, 0xe5, 0x08, 0x42, 0x11, 0xf9, 0xfe, 0x0e, 0x73, 0xe1, 0xb1, 0x7c, 0x83, 0x99, 0x5a, - 0x79, 0xe1, 0x73, 0xbd, 0xe4, 0x76, 0xc0, 0xf3, 0xf6, 0x13, 0x2f, 0xe9, 0x1a, 0xc1, 0xe1, 0x47, 0x7e, 0xd5, 0x62, - 0xfd, 0x75, 0x1f, 0xf3, 0xe7, 0x41, 0xaa, 0x4b, 0xb8, 0x2a, 0x0c, 0x80, 0x3f, 0xba, 0x32, 0xee, 0x06, 0x0c, 0xeb, - 0x23, 0x44, 0x8d, 0xf0, 0x88, 0xfd, 0xe1, 0xa9, 0x17, 0x00, 0xca, 0x9d, 0x9b, 0x81, 0xc8, 0x42, 0x34, 0x3f, 0x2f, - 0x57, 0xdb, 0xe6, 0x65, 0x68, 0x4b, 0x4b, 0x37, 0x8f, 0x13, 0x49, 0xd8, 0x4c, 0x9c, 0x5a, 0xa8, 0x5e, 0x11, 0x31, - 0x45, 0xcc, 0x02, 0xad, 0x97, 0xf1, 0x7b, 0x7c, 0x67, 0x08, 0xa3, 0x36, 0x6c, 0x84, 0xd7, 0xed, 0x68, 0x6d, 0xf0, - 0x7e, 0xbf, 0xd6, 0x46, 0x21, 0xd8, 0xb7, 0xf4, 0x0b, 0x14, 0x69, 0xd8, 0xd2, 0x8e, 0xff, 0x79, 0xc0, 0x17, 0xfd, - 0x43, 0x08, 0x9b, 0xc4, 0x06, 0x05, 0x85, 0x97, 0xda, 0x64, 0x6f, 0x03, 0x25, 0x4c, 0x62, 0xad, 0xd6, 0x13, 0xf0, - 0xa2, 0x0d, 0x20, 0x15, 0xba, 0x67, 0xcc, 0xaf, 0xc8, 0xe4, 0xf9, 0x13, 0xd2, 0xb2, 0x85, 0x71, 0xca, 0x27, 0xd1, - 0x8e, 0x04, 0x3b, 0x3f, 0x45, 0x91, 0xbc, 0xe2, 0xbb, 0x44, 0x92, 0xaf, 0x4f, 0xbb, 0xf9, 0xcb, 0xdd, 0x83, 0x26, - 0x85, 0x40, 0x07, 0x8f, 0xee, 0x08, 0x19, 0x6a, 0xb5, 0x8c, 0xea, 0xf0, 0x18, 0x8b, 0x4c, 0xcf, 0x1f, 0xce, 0xea, - 0x8b, 0x0c, 0x03, 0x27, 0x96, 0xc0, 0x28, 0x95, 0x5d, 0x6e, 0xd9, 0xd8, 0x9f, 0xf4, 0xde, 0x78, 0x89, 0x52, 0x75, - 0x3c, 0xc7, 0xad, 0x1a, 0xba, 0x43, 0x57, 0xc4, 0x1b, 0x3e, 0xf0, 0xd8, 0xbf, 0xba, 0x31, 0xa8, 0x63, 0x4d, 0x9b, - 0x08, 0x5e, 0x07, 0xfd, 0xcc, 0x14, 0x9c, 0x6c, 0x7c, 0x4a, 0x74, 0x0a, 0x83, 0x04, 0x0a, 0x66, 0x28, 0xf6, 0x99, - 0x96, 0x8f, 0x4b, 0xe9, 0xce, 0x5a, 0x2a, 0xea, 0xd8, 0x38, 0x33, 0xca, 0xfa, 0xe5, 0x72, 0x69, 0xe3, 0x6d, 0x04, - 0xf4, 0x92, 0x7b, 0x79, 0x7f, 0xc5, 0x49, 0xe3, 0x18, 0x91, 0x2c, 0x38, 0x1e, 0x1e, 0xc7, 0x1c, 0xf2, 0xc6, 0xad, - 0x05, 0x1d, 0x26, 0xb4, 0x06, 0x36, 0x3b, 0x67, 0x39, 0xe5, 0x6b, 0x11, 0xce, 0xb2, 0xcb, 0x6f, 0x36, 0x40, 0x04, - 0x84, 0x9e, 0x16, 0x91, 0x04, 0x3e, 0x2b, 0x90, 0x31, 0x47, 0x4e, 0x72, 0x64, 0x79, 0xad, 0xe4, 0x11, 0x48, 0x26, - 0x46, 0x8a, 0xb7, 0xe1, 0xa6, 0x9f, 0xa2, 0x4b, 0x76, 0xb0, 0x51, 0x37, 0x08, 0xa2, 0x04, 0x3b, 0xc0, 0x5f, 0xf8, - 0xf3, 0xa1, 0xef, 0xfc, 0xe9, 0xb7, 0x5b, 0x87, 0xff, 0x27, 0xb8, 0xb4, 0x8f, 0x18, 0x3b, 0xfd, 0x25, 0x56, 0x7d, - 0xf5, 0x7f, 0x73, 0xd7, 0xd0, 0x3a, 0xf0, 0xe1, 0x03, 0x17, 0x1e, 0x7f, 0x1b, 0x96, 0xd0, 0x6a, 0x6b, 0x77, 0x58, - 0x52, 0x88, 0x13, 0xe5, 0xc4, 0x8e, 0xea, 0x3d, 0x8a, 0xf6, 0xc5, 0xd3, 0xfb, 0x23, 0x01, 0xac, 0xbf, 0x7f, 0xe3, - 0x51, 0x69, 0xa4, 0xbb, 0x5f, 0x82, 0x4c, 0x6c, 0xad, 0x4d, 0x90, 0xab, 0xd4, 0x7e, 0x7e, 0xee, 0x5b, 0xeb, 0xa8, - 0xa5, 0xab, 0x6c, 0x70, 0x7f, 0xd1, 0x55, 0x7b, 0xb0, 0xc9, 0xf2, 0x61, 0xbb, 0xb9, 0xb5, 0x4f, 0x2b, 0x57, 0x19, - 0xe1, 0x43, 0x01, 0x02, 0xec, 0x54, 0x99, 0x9c, 0x3c, 0xe3, 0xb7, 0x52, 0xf0, 0x8e, 0xa5, 0x9e, 0xf6, 0x37, 0x9b, - 0xe0, 0xef, 0x0d, 0x6b, 0xbb, 0xab, 0x47, 0xeb, 0x03, 0x08, 0xca, 0xa5, 0xd7, 0x50, 0xc1, 0x21, 0xc4, 0x4b, 0x0a, - 0x12, 0x72, 0x18, 0xce, 0x5c, 0x74, 0x92, 0x43, 0x4c, 0x1b, 0x31, 0xac, 0xab, 0xb4, 0x55, 0x71, 0xe2, 0xb5, 0x3c, - 0xb0, 0x5b, 0x18, 0xb7, 0x60, 0x61, 0x58, 0x64, 0x30, 0xf2, 0x0c, 0xec, 0x70, 0x2e, 0x1e, 0x7a, 0x35, 0x0b, 0x5e, - 0x90, 0x26, 0x5c, 0x96, 0xfa, 0x7d, 0xb0, 0x38, 0x66, 0xf5, 0x55, 0x0b, 0x7e, 0xcd, 0xc1, 0xa9, 0x29, 0x6a, 0x43, - 0x7e, 0xb5, 0x6f, 0x66, 0x84, 0xcb, 0x0b, 0xb9, 0xc7, 0x42, 0x10, 0x2a, 0xdb, 0xb8, 0x65, 0xd2, 0xc1, 0xc9, 0x50, - 0xdf, 0xa7, 0x0d, 0x61, 0x84, 0x17, 0x04, 0x32, 0x4d, 0x51, 0xca, 0xf0, 0x5b, 0xb8, 0xaf, 0x1d, 0xca, 0x06, 0xb9, - 0x99, 0x0e, 0x23, 0xe1, 0x8a, 0xec, 0x38, 0xf0, 0x2c, 0xcd, 0xa7, 0x6a, 0x7f, 0x6c, 0x5d, 0x07, 0xfd, 0xce, 0x25, - 0x44, 0xed, 0x91, 0x9a, 0xf1, 0x31, 0x9b, 0x76, 0x0a, 0xfe, 0xe6, 0x73, 0x29, 0x36, 0x10, 0x1f, 0x69, 0xb9, 0x4b, - 0xa9, 0x89, 0x63, 0xb9, 0xb4, 0xca, 0x38, 0xd4, 0xd0, 0x29, 0x0b, 0x6d, 0x23, 0x97, 0x19, 0x44, 0xda, 0x2e, 0x4e, - 0x49, 0x95, 0x49, 0x1e, 0x8b, 0x13, 0x62, 0xc8, 0x42, 0xbf, 0xc0, 0xda, 0xfe, 0x72, 0xf3, 0x4b, 0x32, 0x54, 0x21, - 0x76, 0xee, 0x10, 0xfa, 0xb0, 0xc0, 0xe6, 0xa5, 0xb4, 0x14, 0x46, 0x15, 0xa6, 0xae, 0xda, 0xea, 0xb9, 0xa5, 0x6d, - 0x48, 0x32, 0x90, 0xcc, 0xb2, 0x84, 0x8f, 0xb2, 0x81, 0x41, 0x8e, 0xff, 0x6d, 0x00, 0xd9, 0xf6, 0x20, 0xd8, 0xde, - 0x32, 0x65, 0xa9, 0xef, 0x2d, 0x7e, 0x9a, 0x84, 0x4f, 0x4c, 0x08, 0x5c, 0x06, 0x5c, 0x75, 0xfe, 0x6c, 0x76, 0x8d, - 0xff, 0x10, 0x06, 0xfe, 0x1b, 0x6e, 0xf4, 0x0d, 0xbe, 0x4a, 0x3f, 0x77, 0xc9, 0xfd, 0xc8, 0xfb, 0x91, 0x3c, 0xdb, - 0x96, 0xc6, 0x4f, 0x5c, 0xac, 0x78, 0x53, 0x7e, 0x0a, 0x7f, 0x33, 0x9a, 0xef, 0xcb, 0xfa, 0xce, 0xb6, 0xd3, 0x47, - 0x60, 0x33, 0xd8, 0x23, 0x3b, 0x74, 0xd7, 0x47, 0xa3, 0x54, 0xcc, 0x1c, 0xf1, 0xed, 0xc3, 0x9f, 0xdb, 0xda, 0x2f, - 0xce, 0x86, 0xe8, 0x3a, 0x30, 0x85, 0xd3, 0xd7, 0x01, 0xca, 0x0e, 0x59, 0x62, 0xda, 0x81, 0x4a, 0x14, 0x1d, 0x74, - 0x66, 0x5d, 0x0a, 0xb0, 0x7c, 0xe3, 0xe8, 0x67, 0x0d, 0xae, 0x95, 0xa4, 0xc3, 0x50, 0x6b, 0x11, 0x9f, 0x4d, 0xa7, - 0xf7, 0xa3, 0x58, 0x51, 0xc0, 0x02, 0xe6, 0xeb, 0x04, 0x76, 0x91, 0xde, 0xbc, 0x3c, 0x92, 0xe0, 0x9c, 0x70, 0x38, - 0x72, 0x81, 0x00, 0x2a, 0xb4, 0x5d, 0x48, 0x13, 0x7e, 0x9d, 0x3b, 0xba, 0xb6, 0x9f, 0x90, 0x5a, 0xb2, 0x1c, 0xe8, - 0xa5, 0xfa, 0xbf, 0xee, 0xee, 0x7e, 0x51, 0x1e, 0x2f, 0xec, 0xed, 0x89, 0x70, 0xcb, 0xb3, 0xaf, 0xac, 0xb0, 0xea, - 0x15, 0xf7, 0xfb, 0x24, 0x13, 0xad, 0xdd, 0x5c, 0x1f, 0xac, 0x4e, 0xd4, 0x2a, 0x78, 0xe8, 0xab, 0xf4, 0x3f, 0x33, - 0xbd, 0xdc, 0x73, 0x53, 0x1e, 0x4a, 0x84, 0x03, 0x5f, 0x34, 0x34, 0x3e, 0x43, 0x35, 0x44, 0xf1, 0x58, 0x0d, 0x38, - 0x8c, 0x49, 0x73, 0xdc, 0x27, 0x58, 0xc9, 0xd4, 0x89, 0x51, 0xb5, 0x11, 0x05, 0x24, 0x98, 0x82, 0xce, 0xa5, 0x2d, - 0xa1, 0x40, 0x05, 0xcd, 0xa2, 0x84, 0x46, 0xdf, 0xf3, 0x61, 0x45, 0x1a, 0x1d, 0xdc, 0x13, 0xc8, 0x08, 0x82, 0xca, - 0xb2, 0xf9, 0xcd, 0x76, 0x35, 0x8a, 0xc2, 0xa9, 0xef, 0x13, 0x0a, 0xca, 0x7f, 0x9c, 0xf9, 0xd2, 0x66, 0xc7, 0xdd, - 0xa3, 0x81, 0x50, 0x54, 0xeb, 0x12, 0x2f, 0x5b, 0x6d, 0xe4, 0x26, 0x37, 0x45, 0xa4, 0x09, 0xc4, 0x1e, 0xfe, 0x04, - 0x4d, 0x52, 0xc4, 0x74, 0x11, 0x37, 0x97, 0xe6, 0xe2, 0xe0, 0x4a, 0xe9, 0xea, 0x81, 0xdb, 0xd0, 0xc8, 0xab, 0x89, - 0x5e, 0xed, 0xe2, 0x0f, 0x02, 0xd1, 0x09, 0x4b, 0x26, 0xf2, 0x8a, 0x81, 0x48, 0x82, 0x81, 0x02, 0x45, 0xdb, 0x82, - 0x29, 0x0a, 0xbd, 0x6e, 0xeb, 0xc5, 0x71, 0x7e, 0x21, 0x53, 0x11, 0x64, 0x2a, 0x6d, 0x6e, 0x80, 0xab, 0x9f, 0xb6, - 0xec, 0x07, 0x1a, 0xff, 0x93, 0x9c, 0x70, 0xd3, 0x43, 0xcf, 0x42, 0x7c, 0xea, 0x3e, 0xb6, 0xde, 0x55, 0xa0, 0x30, - 0xbd, 0x78, 0x11, 0x2d, 0x90, 0xa2, 0x6e, 0xcc, 0x89, 0x25, 0x9f, 0xab, 0x16, 0xdf, 0x57, 0xe5, 0x97, 0x54, 0x50, - 0x43, 0x40, 0x98, 0x09, 0x20, 0x2b, 0xb1, 0x92, 0xcd, 0x2b, 0x72, 0xee, 0x4b, 0xb6, 0x61, 0x27, 0x78, 0x53, 0x6b, - 0x6e, 0x77, 0x46, 0x8c, 0xe0, 0xfd, 0x10, 0x01, 0x21, 0xaa, 0x15, 0x99, 0x25, 0xbf, 0x2a, 0x45, 0x9b, 0x01, 0x0f, - 0xa1, 0x20, 0x2c, 0xce, 0x5e, 0x21, 0xf3, 0x58, 0x2c, 0xf4, 0x03, 0x72, 0x8d, 0xb8, 0x87, 0x43, 0x04, 0x60, 0xd8, - 0xef, 0xee, 0x11, 0x31, 0xd2, 0xe1, 0xc2, 0x44, 0x0c, 0x03, 0x48, 0xd8, 0x06, 0x2e, 0xb3, 0xf3, 0xf1, 0xbe, 0x7b, - 0xff, 0xc7, 0x18, 0xce, 0x0d, 0xd6, 0x4a, 0xb8, 0x75, 0x74, 0xd5, 0x09, 0xf2, 0xf2, 0x3e, 0xe2, 0xd3, 0xdc, 0x8e, - 0xa8, 0x97, 0x03, 0x51, 0x69, 0x35, 0x9e, 0x6d, 0x84, 0x87, 0x65, 0x0a, 0x8f, 0x7d, 0x2e, 0x28, 0x9d, 0x79, 0x09, - 0x2e, 0x01, 0xd5, 0x07, 0x19, 0x5f, 0x79, 0x23, 0xd1, 0xab, 0xcc, 0xc6, 0x9f, 0xc7, 0xf3, 0x3d, 0x6c, 0xd3, 0x45, - 0x1b, 0xd7, 0xd3, 0xe9, 0x1d, 0x4a, 0x32, 0xc1, 0xb4, 0xbb, 0x49, 0x36, 0xec, 0xfa, 0x89, 0xc9, 0x37, 0x2a, 0xe2, - 0x06, 0xa4, 0xf6, 0xdd, 0x38, 0xd0, 0x54, 0xb0, 0xde, 0x7c, 0x4a, 0xa2, 0x81, 0xe9, 0x11, 0xc9, 0xdc, 0xac, 0xd7, - 0xf6, 0x66, 0x0d, 0x01, 0x20, 0x05, 0x8b, 0x96, 0xe0, 0xbd, 0x2b, 0x67, 0x4d, 0x93, 0x12, 0x5b, 0x00, 0x31, 0xdd, - 0x40, 0xe2, 0x38, 0xa2, 0x5a, 0xe3, 0xee, 0x9b, 0xa5, 0x87, 0xf7, 0x3b, 0x62, 0xf7, 0xee, 0x48, 0x6a, 0x7a, 0xe5, - 0x84, 0xed, 0xde, 0x91, 0x53, 0xa3, 0x1c, 0x1f, 0xd5, 0xb3, 0x1b, 0xb6, 0xb4, 0x8e, 0xe5, 0xc9, 0x8c, 0x1e, 0x05, - 0xbe, 0x64, 0xde, 0xbb, 0x7a, 0x50, 0x92, 0x70, 0xf6, 0x0b, 0x01, 0xe2, 0x68, 0xfd, 0x4b, 0xad, 0xd2, 0xa5, 0xe6, - 0x94, 0xfb, 0xbd, 0x0d, 0xfb, 0xaa, 0xb0, 0x72, 0x49, 0x2d, 0x7a, 0x39, 0x99, 0xaa, 0x9e, 0xca, 0xd7, 0x5e, 0xcb, - 0x35, 0xce, 0x86, 0x1a, 0xda, 0x43, 0xef, 0x35, 0x4d, 0xd5, 0xb2, 0x15, 0xce, 0xa2, 0x98, 0xb6, 0x77, 0xd1, 0x9d, - 0x42, 0x63, 0x1f, 0x39, 0x91, 0x38, 0x61, 0x6e, 0xfd, 0x55, 0x1e, 0x89, 0x1d, 0x1e, 0xc1, 0x16, 0xbe, 0x91, 0x74, - 0x48, 0xca, 0x41, 0xc7, 0x09, 0xb8, 0xad, 0x0c, 0x4f, 0x33, 0x10, 0xb1, 0x5a, 0x44, 0x9a, 0xcc, 0x00, 0xc6, 0x31, - 0x45, 0x5c, 0xab, 0x60, 0xa8, 0x41, 0x72, 0xae, 0x06, 0xc1, 0x4c, 0xc7, 0x82, 0x9d, 0xf9, 0x28, 0x3f, 0x41, 0x5b, - 0x1b, 0xb3, 0xb0, 0xd0, 0xb3, 0x31, 0x35, 0xbb, 0x29, 0x01, 0xac, 0x11, 0x74, 0x7b, 0x49, 0x77, 0xcf, 0x0d, 0xc2, - 0xfb, 0xe5, 0xc8, 0xe5, 0x8c, 0xc1, 0x7a, 0xec, 0xa3, 0x6c, 0x71, 0xea, 0xc1, 0x83, 0x00, 0x33, 0x82, 0xc3, 0x56, - 0xb9, 0x81, 0xf6, 0x6c, 0xe8, 0x3f, 0xf0, 0x4d, 0x34, 0xfb, 0xa2, 0xc6, 0x82, 0x83, 0x33, 0xeb, 0xb3, 0x78, 0x57, - 0xc5, 0x04, 0x59, 0xc4, 0x90, 0x24, 0x67, 0x4d, 0x31, 0x37, 0xeb, 0x62, 0x3d, 0x83, 0x40, 0xb0, 0x7c, 0x85, 0xc9, - 0x00, 0xe1, 0x60, 0x76, 0xa3, 0x21, 0x26, 0xd6, 0x93, 0x77, 0xfd, 0x08, 0x80, 0xc0, 0x00, 0xdc, 0xc5, 0xb9, 0xd0, - 0x26, 0x3a, 0x80, 0x22, 0xbf, 0x07, 0x07, 0x40, 0x12, 0x98, 0xa1, 0x48, 0x50, 0xd0, 0xab, 0xd6, 0xbe, 0xe6, 0xc5, - 0x18, 0x0a, 0x2d, 0x24, 0x04, 0xc1, 0x56, 0xee, 0x92, 0x35, 0x2a, 0xb3, 0x75, 0xd0, 0x90, 0xf0, 0xed, 0x59, 0x51, - 0x49, 0x8a, 0x90, 0x5f, 0xe7, 0x81, 0xf4, 0x4f, 0x07, 0x34, 0xf6, 0x1c, 0x25, 0xa7, 0x9b, 0x4c, 0xcc, 0x1a, 0xe2, - 0xe5, 0x69, 0x3d, 0x5b, 0x84, 0x62, 0x0f, 0xdd, 0xa0, 0xcc, 0xc9, 0xd8, 0x89, 0x2f, 0xa8, 0x11, 0x49, 0xfd, 0xe3, - 0x14, 0xd5, 0x83, 0x7a, 0x14, 0x23, 0x93, 0x71, 0x3d, 0xa1, 0x96, 0xaf, 0xb5, 0x1b, 0x81, 0x36, 0x29, 0xcf, 0xb8, - 0xc9, 0xd8, 0x52, 0xbf, 0x54, 0xa8, 0x65, 0xa7, 0xa6, 0x14, 0xec, 0xe4, 0x3c, 0x2f, 0x38, 0x7a, 0x2a, 0x76, 0xc2, - 0x38, 0x08, 0xf6, 0xa7, 0xd3, 0x6e, 0x8d, 0xf7, 0x7c, 0x82, 0x78, 0xbc, 0xea, 0xdc, 0x3e, 0x64, 0x6a, 0xd5, 0xd4, - 0x14, 0x68, 0xc6, 0xd3, 0xf4, 0xfe, 0x3f, 0x80, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0xa3, 0x10, 0xb7, 0x83, 0x18, 0x7f, - 0xd0, 0xc2, 0x4f, 0xf8, 0x1a, 0x25, 0x5c, 0xff, 0x2d, 0x09, 0xd0, 0xf1, 0x83, 0x56, 0x82, 0x2d, 0x49, 0x9c, 0xce, - 0x45, 0xaa, 0x3b, 0xc7, 0x0c, 0xab, 0x20, 0x17, 0x44, 0x8e, 0xe7, 0x3a, 0x8d, 0xca, 0x42, 0x96, 0x22, 0xe1, 0xc6, - 0x2f, 0x7e, 0xcd, 0x96, 0x0a, 0x3f, 0x06, 0x0e, 0x02, 0x51, 0x01, 0x24, 0xec, 0xa7, 0x97, 0xda, 0x73, 0x66, 0xe7, - 0x01, 0x43, 0x16, 0x48, 0x4b, 0x1d, 0xfb, 0x0a, 0x9d, 0x04, 0x00, 0x44, 0xc7, 0xc4, 0x18, 0xc8, 0xab, 0x1d, 0x55, - 0x7f, 0x80, 0x43, 0xef, 0xa4, 0x63, 0x6d, 0xee, 0x26, 0x10, 0x45, 0x08, 0x08, 0x90, 0x58, 0x1b, 0x0a, 0x22, 0x6b, - 0x39, 0x88, 0xa0, 0x4a, 0xec, 0x04, 0x8e, 0xd2, 0x66, 0xc1, 0x8d, 0x78, 0x44, 0x1a, 0x01, 0xf4, 0x0a, 0x2e, 0xc4, - 0x8c, 0xc0, 0x28, 0xcb, 0x48, 0xe3, 0x17, 0x58, 0x68, 0x5c, 0x04, 0xc1, 0xe7, 0x94, 0xb5, 0xde, 0x83, 0x78, 0x3e, - 0xb7, 0x8a, 0xe6, 0x63, 0x42, 0x88, 0x35, 0x00, 0x6b, 0x28, 0xf3, 0xdf, 0xb2, 0x18, 0x30, 0x1a, 0x28, 0xd9, 0xde, - 0xe3, 0xcc, 0x54, 0x2f, 0x2d, 0x57, 0x55, 0x98, 0x32, 0x8f, 0xc8, 0xa5, 0xf3, 0xae, 0x3f, 0x85, 0xf5, 0xa2, 0x76, - 0x41, 0xd3, 0x84, 0xc7, 0xea, 0xa5, 0x7a, 0xd6, 0xc8, 0x0d, 0xc5, 0x7f, 0x52, 0x9a, 0x1b, 0xe3, 0xa8, 0xfc, 0x62, - 0x5a, 0xf5, 0xc9, 0xe8, 0xb0, 0xde, 0x45, 0x76, 0xa7, 0xa2, 0x02, 0xe0, 0xb4, 0x5b, 0x61, 0x9c, 0xd3, 0x2b, 0x7f, - 0xb5, 0xc3, 0x47, 0xab, 0xcc, 0xdc, 0xa2, 0x2e, 0xb3, 0x86, 0x82, 0xf2, 0xd1, 0x54, 0x7e, 0x87, 0xab, 0xbb, 0x3c, - 0x61, 0xf4, 0xa9, 0x2c, 0x8a, 0x53, 0x77, 0x0f, 0x47, 0xfe, 0x75, 0xd8, 0x12, 0x62, 0xa7, 0xba, 0xf5, 0x17, 0x17, - 0x1e, 0x4c, 0x7d, 0xe2, 0x15, 0x6e, 0xdc, 0x42, 0x9f, 0xb1, 0xd7, 0x8c, 0xa1, 0x13, 0x02, 0xc0, 0x3b, 0x4b, 0x14, - 0x65, 0x41, 0xf8, 0xf7, 0x47, 0x9b, 0xa7, 0x45, 0x34, 0x4f, 0xfa, 0x36, 0xde, 0x4e, 0x40, 0x53, 0x60, 0x83, 0x75, - 0x20, 0x30, 0x1f, 0xd0, 0xbf, 0x19, 0x6c, 0xa3, 0xc6, 0xf7, 0xad, 0x2e, 0x8a, 0x10, 0x5b, 0x18, 0x7c, 0x69, 0xfd, - 0xa5, 0x20, 0xb2, 0x3e, 0xa9, 0x01, 0x6d, 0x3f, 0x4d, 0xd6, 0x5d, 0x61, 0x28, 0x79, 0xda, 0xad, 0x87, 0x11, 0x3b, - 0x68, 0x96, 0xf4, 0x86, 0xc9, 0x1f, 0xd2, 0x41, 0xe1, 0x26, 0x26, 0x8b, 0x44, 0xf9, 0xbb, 0x1f, 0x53, 0x92, 0xdc, - 0xf5, 0x0e, 0x67, 0x29, 0xea, 0x2a, 0x4c, 0xfd, 0x59, 0x79, 0xbf, 0x52, 0xff, 0x96, 0xde, 0xd8, 0x42, 0xc3, 0x91, - 0xb5, 0x20, 0x91, 0xd3, 0x30, 0xe4, 0x5a, 0x1d, 0xce, 0x9c, 0xb8, 0xb5, 0xce, 0x76, 0x84, 0x04, 0x1e, 0x96, 0x9c, - 0x25, 0x4c, 0xd5, 0x9b, 0x5a, 0x10, 0x1c, 0x26, 0x82, 0xc2, 0x74, 0x51, 0x9c, 0x22, 0x61, 0xf1, 0x66, 0x87, 0x16, - 0xa7, 0xcb, 0x60, 0xe7, 0xab, 0xfd, 0x44, 0x85, 0x67, 0x6c, 0x16, 0x0b, 0x50, 0x2d, 0xa2, 0xfc, 0x78, 0x31, 0xc0, - 0xee, 0x9f, 0xf0, 0xb1, 0x74, 0x12, 0xb6, 0x1e, 0x74, 0x4d, 0x6a, 0xb9, 0x54, 0x6a, 0x54, 0x5b, 0xc6, 0x35, 0xd7, - 0x50, 0x71, 0xed, 0xf0, 0xd0, 0x76, 0xf8, 0xee, 0x83, 0xf7, 0x45, 0xe2, 0x19, 0x4c, 0xe5, 0x91, 0x43, 0x10, 0x2d, - 0x6e, 0x59, 0xb7, 0x3e, 0x0c, 0x35, 0x97, 0xa7, 0xb0, 0x8f, 0x86, 0x72, 0xba, 0x88, 0x97, 0x24, 0xdf, 0x41, 0x1d, - 0x48, 0x0f, 0x1d, 0x26, 0x7a, 0x7b, 0x5f, 0x35, 0xeb, 0x0e, 0x34, 0xdf, 0xf4, 0x88, 0x40, 0x9b, 0xbb, 0x6a, 0x31, - 0xaf, 0x98, 0xba, 0x44, 0xb7, 0xa4, 0x96, 0x20, 0xee, 0xba, 0x3c, 0x6e, 0x2d, 0x5f, 0x02, 0x29, 0xa5, 0x84, 0x43, - 0xcb, 0xa5, 0xe6, 0xae, 0xf7, 0x1d, 0x87, 0x84, 0xad, 0xd0, 0x92, 0x75, 0xeb, 0x70, 0x1b, 0x6b, 0xfd, 0x29, 0x30, - 0xa9, 0x7f, 0x69, 0x45, 0x38, 0x78, 0x75, 0xc1, 0xba, 0x2d, 0x3e, 0x78, 0x61, 0x5d, 0x83, 0xae, 0x3d, 0xac, 0x44, - 0x87, 0x1d, 0x56, 0xa1, 0xd5, 0x66, 0x2d, 0x71, 0xb5, 0x12, 0xe3, 0x1b, 0xfa, 0xc3, 0x05, 0x27, 0x96, 0x9d, 0x65, - 0x48, 0xe3, 0x91, 0x93, 0xde, 0x8a, 0x3c, 0x55, 0x64, 0xbf, 0x62, 0x46, 0xc5, 0x4f, 0xd7, 0x91, 0xd6, 0x0b, 0x38, - 0x23, 0x94, 0xbd, 0xfc, 0x80, 0x8d, 0x63, 0x0e, 0xb6, 0x65, 0xd6, 0xde, 0xbb, 0x90, 0x56, 0x62, 0x87, 0x08, 0x5e, - 0x71, 0x17, 0xc3, 0x03, 0xcd, 0x0a, 0xc8, 0x98, 0x82, 0x98, 0x50, 0xf0, 0xf7, 0xba, 0x22, 0x64, 0xec, 0xf0, 0xa4, - 0x73, 0x6c, 0xd9, 0xf1, 0x09, 0x0a, 0x70, 0x64, 0x19, 0x18, 0x8f, 0x51, 0xa5, 0xa2, 0x3d, 0x9d, 0xe1, 0x18, 0xd5, - 0x2c, 0xad, 0x98, 0x5f, 0xc5, 0x02, 0x59, 0x01, 0xbb, 0x71, 0xd6, 0xb2, 0xd7, 0x16, 0xb9, 0x44, 0xf1, 0x86, 0xec, - 0x4e, 0x15, 0x99, 0x85, 0xb1, 0x4e, 0x95, 0x2c, 0xb0, 0xf4, 0xb8, 0x26, 0x94, 0xf1, 0x3f, 0x4d, 0x09, 0xca, 0xb7, - 0xfb, 0x9a, 0x4e, 0x2a, 0x34, 0x0a, 0xd7, 0x64, 0x7d, 0x9a, 0x5f, 0xd1, 0x13, 0xb9, 0xc0, 0xba, 0x24, 0x09, 0xe3, - 0x06, 0x31, 0xaa, 0xda, 0x84, 0x80, 0x6e, 0x08, 0xc5, 0x9b, 0x82, 0xd0, 0x94, 0x21, 0xb4, 0x9c, 0xe4, 0xa8, 0x1e, - 0x70, 0x96, 0xc8, 0xcd, 0xc1, 0x6b, 0x04, 0x57, 0xd1, 0x0e, 0x52, 0x54, 0x61, 0xb8, 0x8b, 0x6a, 0x90, 0xe6, 0xda, - 0x23, 0xa5, 0xe0, 0xaf, 0x09, 0xd0, 0x01, 0x08, 0xc3, 0xca, 0xdf, 0xdc, 0xa8, 0xe0, 0x15, 0xca, 0x4a, 0xe9, 0x54, - 0x73, 0x98, 0x26, 0xa6, 0xa5, 0x53, 0x46, 0x3a, 0xe1, 0x07, 0xaf, 0x11, 0xe7, 0x82, 0xa0, 0xb6, 0xab, 0xc5, 0x6a, - 0x30, 0x4c, 0xea, 0xa4, 0x2b, 0x40, 0x3e, 0x6a, 0x1a, 0x4c, 0x68, 0xb7, 0x94, 0xe8, 0x45, 0xd8, 0x2b, 0xb0, 0x9c, - 0x76, 0xb3, 0x5d, 0x03, 0x88, 0xd5, 0x5a, 0xd8, 0x41, 0x06, 0xc6, 0x32, 0xfe, 0x08, 0xc8, 0x03, 0x9f, 0x3e, 0x2f, - 0xad, 0x78, 0x64, 0xbd, 0x72, 0xf8, 0xe1, 0xe3, 0xaf, 0x29, 0x18, 0x2c, 0x15, 0x0d, 0x39, 0xbd, 0xd7, 0xe7, 0xf4, - 0x9d, 0x6c, 0x30, 0xd6, 0xa2, 0x73, 0x10, 0xf9, 0x2e, 0xb4, 0x23, 0xdd, 0x95, 0x75, 0x99, 0x91, 0xed, 0xeb, 0x81, - 0x2c, 0xf4, 0x5c, 0x5f, 0x8a, 0x20, 0xd5, 0x82, 0xc2, 0xdf, 0x01, 0x8a, 0x4b, 0x43, 0x28, 0x0d, 0xe5, 0xa0, 0x8c, - 0x14, 0x8e, 0x32, 0x19, 0xee, 0x34, 0x90, 0x02, 0x32, 0x22, 0x10, 0xcc, 0x99, 0x65, 0xed, 0x2d, 0x16, 0xd8, 0x92, - 0x9d, 0xa9, 0x5b, 0xb5, 0x6b, 0x4c, 0x98, 0x97, 0x39, 0x34, 0x7a, 0xe0, 0xd4, 0x96, 0xd3, 0xa3, 0x68, 0xa9, 0x9e, - 0x4e, 0x86, 0xa2, 0x99, 0x95, 0xa4, 0xb3, 0x97, 0xcf, 0xab, 0x86, 0x56, 0x92, 0x7e, 0x67, 0xa1, 0x06, 0xa4, 0x38, - 0x81, 0x3f, 0xbe, 0x08, 0x21, 0x5f, 0x72, 0x1f, 0xee, 0xe9, 0x2f, 0x3b, 0x0b, 0x4e, 0x2f, 0x51, 0x83, 0x9a, 0xbf, - 0x2c, 0x9c, 0xe9, 0x8d, 0x29, 0x1d, 0x94, 0x38, 0x16, 0x84, 0x3d, 0xbc, 0x97, 0xbe, 0xa8, 0x46, 0xdb, 0x45, 0x45, - 0xc1, 0x74, 0x00, 0xa8, 0x68, 0x1a, 0x0e, 0x1d, 0xd7, 0x9a, 0xa4, 0xac, 0xa4, 0xe2, 0xda, 0xcd, 0x15, 0x9f, 0x3e, - 0x76, 0x8c, 0xd4, 0xba, 0x03, 0x93, 0x78, 0x00, 0xcb, 0x3f, 0x07, 0xde, 0x8f, 0x09, 0x20, 0x5c, 0x4a, 0x79, 0x7e, - 0x71, 0x36, 0xe8, 0xf1, 0xdb, 0xad, 0xb8, 0x17, 0xde, 0xab, 0x8e, 0x31, 0x22, 0x66, 0x0b, 0x21, 0x79, 0xc8, 0x96, - 0x48, 0x6c, 0x36, 0x37, 0x4e, 0xba, 0xdb, 0x1c, 0x75, 0x78, 0x7f, 0xf0, 0x7a, 0xc9, 0x3b, 0x76, 0xa7, 0x69, 0xf0, - 0x41, 0xab, 0x53, 0x23, 0xad, 0xe9, 0x3f, 0xf8, 0xb7, 0x72, 0x91, 0x4e, 0xea, 0x1a, 0x90, 0xe8, 0x7c, 0x09, 0x09, - 0xf6, 0x07, 0x49, 0x91, 0x15, 0x5d, 0x2a, 0x65, 0x1b, 0x15, 0xeb, 0x97, 0x66, 0x39, 0x0b, 0xd7, 0x9b, 0x92, 0x7e, - 0xd9, 0xa5, 0x9b, 0x9c, 0x81, 0x75, 0xc1, 0xaa, 0xec, 0x39, 0xc7, 0xe2, 0x19, 0x32, 0xb1, 0xb0, 0xd7, 0x25, 0xca, - 0x52, 0x17, 0x36, 0x90, 0x64, 0xc7, 0xf0, 0x96, 0xf1, 0xe8, 0x4f, 0x9b, 0xc3, 0xbb, 0x9f, 0xf6, 0xed, 0x83, 0xfc, - 0x79, 0x1d, 0xed, 0x0c, 0x0a, 0x71, 0x29, 0xe9, 0xc2, 0xc3, 0x45, 0x0d, 0x2e, 0x09, 0x2d, 0xbc, 0x2d, 0x21, 0x2e, - 0x1e, 0xc3, 0x79, 0xfb, 0x0e, 0x41, 0xad, 0xac, 0xd8, 0xde, 0x71, 0xc4, 0x42, 0x3a, 0xeb, 0x95, 0x00, 0xfa, 0x2d, - 0x95, 0xb5, 0xb8, 0x23, 0xa7, 0x05, 0x94, 0x44, 0xca, 0x2e, 0xd1, 0xd3, 0xd1, 0xa9, 0xad, 0x3d, 0x9b, 0x0f, 0x6b, - 0x4b, 0xd1, 0x36, 0x12, 0x55, 0x9c, 0x43, 0x1c, 0xa3, 0x61, 0x68, 0x73, 0x6d, 0x6d, 0x8b, 0x3a, 0xcc, 0x50, 0x1d, - 0x6b, 0x08, 0x9b, 0x6e, 0x29, 0xe6, 0x5f, 0xaa, 0x1d, 0x97, 0x6e, 0x0d, 0x86, 0x09, 0xc9, 0x83, 0xa0, 0x4c, 0xc2, - 0xa5, 0xbc, 0xbd, 0xf0, 0x21, 0xdd, 0xd7, 0xeb, 0x77, 0x28, 0xff, 0x6e, 0x41, 0x5b, 0x8b, 0x6f, 0x9a, 0xff, 0x20, - 0xff, 0x2f, 0x1b, 0x30, 0x34, 0xe6, 0xf1, 0xe1, 0x58, 0xd2, 0x46, 0x19, 0x2d, 0xe5, 0x14, 0x1e, 0x3b, 0xd3, 0xf4, - 0x12, 0x4b, 0x87, 0x70, 0x77, 0x27, 0x99, 0x05, 0x87, 0x2d, 0x9b, 0x03, 0x24, 0x28, 0xc1, 0xe4, 0xcd, 0xc5, 0x68, - 0xd3, 0x63, 0xba, 0xc2, 0xe1, 0xbb, 0x15, 0x49, 0x36, 0x7b, 0x8d, 0x8b, 0x18, 0x20, 0x3d, 0x57, 0x30, 0x81, 0x02, - 0xfe, 0x30, 0x43, 0x51, 0x77, 0xe3, 0x5a, 0x4a, 0x31, 0x65, 0x8d, 0x20, 0x98, 0xe5, 0x2d, 0x9e, 0x63, 0xc8, 0xb4, - 0xad, 0x9e, 0xbb, 0x4f, 0x7a, 0xc0, 0x80, 0x13, 0x39, 0xfb, 0xd5, 0x62, 0x43, 0xa8, 0x6a, 0xdd, 0xae, 0xbd, 0x26, - 0xba, 0x42, 0x24, 0x7a, 0x72, 0xd2, 0x69, 0x40, 0x6c, 0x8b, 0x30, 0xe4, 0x50, 0xc8, 0xf8, 0xb8, 0x15, 0x39, 0x93, - 0xf0, 0x19, 0xdf, 0xb2, 0x4b, 0x16, 0x77, 0xa2, 0x99, 0x63, 0xc8, 0x67, 0x26, 0x41, 0xc4, 0xe8, 0x5a, 0x2a, 0xe7, - 0x84, 0x14, 0x5d, 0xa9, 0x47, 0xdf, 0x0f, 0xc8, 0xd2, 0x48, 0x82, 0x38, 0x3a, 0x55, 0x63, 0x9e, 0xff, 0x9d, 0x59, - 0x44, 0x67, 0xf0, 0x0f, 0xe3, 0xcc, 0xb3, 0xaf, 0x88, 0x7d, 0x96, 0x70, 0x32, 0xe9, 0xd5, 0xd6, 0x7a, 0x18, 0x44, - 0x20, 0xe0, 0xf3, 0xdd, 0xe8, 0xcd, 0x46, 0x5b, 0x37, 0x68, 0xbc, 0xa3, 0x79, 0x3a, 0xec, 0xcf, 0xc8, 0xdd, 0xa0, - 0x99, 0xd6, 0x6a, 0x53, 0xe2, 0x33, 0x08, 0x9c, 0xcb, 0x48, 0x35, 0x67, 0x19, 0x98, 0x60, 0xbf, 0x5f, 0x6c, 0x7d, - 0x01, 0xd5, 0x99, 0x11, 0x48, 0xfd, 0xae, 0x7a, 0xa9, 0x55, 0x9a, 0x31, 0xa6, 0xd3, 0x45, 0x6d, 0xaf, 0x0d, 0x1c, - 0xf8, 0x3e, 0xd9, 0xc4, 0xa4, 0xad, 0x5e, 0xe2, 0x04, 0x45, 0x77, 0x68, 0xd1, 0xf9, 0x5e, 0x35, 0xd1, 0x54, 0x66, - 0xec, 0xc9, 0xb8, 0x90, 0xed, 0xeb, 0xed, 0x7e, 0x43, 0xe6, 0xe8, 0x5a, 0xc7, 0x48, 0xc9, 0x45, 0x7d, 0x8e, 0xb8, - 0xca, 0x90, 0x7f, 0x5e, 0xc8, 0x62, 0x47, 0x1c, 0x6e, 0x7f, 0x87, 0x87, 0xd5, 0xa2, 0x2e, 0x66, 0xc7, 0x81, 0x38, - 0x46, 0xfe, 0x21, 0x72, 0x7e, 0x14, 0xb0, 0x19, 0x7e, 0x9a, 0xe1, 0x33, 0x68, 0xb3, 0x37, 0xfb, 0xc9, 0x36, 0xbf, - 0xf5, 0xd8, 0xf5, 0xef, 0x1a, 0x5e, 0xf9, 0xc6, 0x2a, 0x1c, 0x76, 0xdf, 0x76, 0x62, 0xcc, 0xfb, 0xf3, 0xd3, 0xaf, - 0x35, 0x46, 0xde, 0x10, 0xb0, 0xd9, 0xc1, 0xfb, 0x38, 0x67, 0xbf, 0xa5, 0xc3, 0x42, 0x2f, 0x6a, 0x15, 0x90, 0x51, - 0xe7, 0x3e, 0x71, 0x7d, 0x0b, 0x90, 0x56, 0x68, 0xa1, 0xd5, 0xa3, 0x5b, 0x42, 0xf7, 0x12, 0x21, 0xeb, 0x9b, 0x4b, - 0xb1, 0xe9, 0xb4, 0x67, 0x4d, 0x25, 0x25, 0x4d, 0xf1, 0x96, 0x14, 0x8a, 0xdf, 0xcf, 0xa8, 0x93, 0x07, 0xb8, 0xcf, - 0xa7, 0x8d, 0x64, 0xa6, 0xee, 0x26, 0xeb, 0xf9, 0x93, 0xd9, 0x13, 0x4a, 0xdb, 0x30, 0x9a, 0x43, 0x7e, 0xd3, 0x68, - 0x40, 0x8f, 0x47, 0x8b, 0x89, 0xd8, 0x0f, 0x02, 0x14, 0x7c, 0x1a, 0x2a, 0xa0, 0x7a, 0xa0, 0xdf, 0xf6, 0xd7, 0x01, - 0x27, 0x15, 0x31, 0x06, 0x7b, 0x03, 0x50, 0x30, 0x44, 0xb6, 0x91, 0xc5, 0x7b, 0xa1, 0x43, 0xd1, 0x27, 0x09, 0x9d, - 0xe9, 0x85, 0x12, 0x91, 0xd0, 0x23, 0x88, 0xce, 0xe9, 0xae, 0xf8, 0xc6, 0xe6, 0xc3, 0xeb, 0x58, 0xec, 0x59, 0x26, - 0xdf, 0x61, 0xb3, 0xb2, 0x0e, 0xf5, 0x35, 0x93, 0x86, 0xee, 0x45, 0xfb, 0xa8, 0x71, 0xeb, 0x45, 0x42, 0xc7, 0x5f, - 0xce, 0xeb, 0x91, 0x55, 0x6f, 0x89, 0x18, 0xa6, 0x98, 0x79, 0xcf, 0xa2, 0xde, 0xba, 0x68, 0x09, 0xd7, 0xac, 0xab, - 0x0e, 0x82, 0xa6, 0xc4, 0xd3, 0x7a, 0x70, 0x9d, 0x0b, 0xb1, 0xf8, 0xc9, 0x24, 0x5a, 0x3f, 0xf9, 0x6d, 0xdc, 0xa0, - 0xe4, 0x5c, 0x68, 0xd0, 0x85, 0x02, 0xa1, 0xf7, 0xde, 0x7b, 0x9b, 0x8f, 0xf6, 0x36, 0x35, 0xfd, 0x85, 0x79, 0xf1, - 0x47, 0x72, 0xd6, 0x6f, 0x76, 0x39, 0x70, 0x10, 0x4a, 0x9c, 0x30, 0x22, 0x5c, 0xd8, 0x34, 0x97, 0xbc, 0x94, 0x59, - 0xb9, 0x70, 0x86, 0x03, 0xd1, 0x19, 0xf1, 0x0d, 0x3f, 0xd8, 0xb6, 0x40, 0x20, 0x6e, 0xb5, 0x4c, 0x14, 0xcf, 0x88, - 0x38, 0x91, 0x65, 0x0e, 0x93, 0x9a, 0xe6, 0x72, 0xa6, 0x15, 0xbb, 0x6d, 0x05, 0x8d, 0x6f, 0x8c, 0x73, 0x2c, 0x81, - 0xde, 0xac, 0xd0, 0xce, 0xa5, 0x92, 0x8f, 0xfd, 0x8e, 0xaa, 0x9d, 0xeb, 0x2f, 0xaf, 0x65, 0x5e, 0xee, 0x3c, 0xbb, - 0x36, 0xcd, 0xcb, 0x35, 0x86, 0xce, 0x40, 0x66, 0x47, 0x75, 0x95, 0xa9, 0xbb, 0xd8, 0xe0, 0x8e, 0x42, 0x75, 0xb5, - 0x20, 0x1c, 0x80, 0x22, 0x9a, 0xe6, 0x98, 0x1b, 0xcc, 0xa2, 0xaf, 0xae, 0xf0, 0x4e, 0x07, 0x6d, 0xb5, 0xb4, 0x01, - 0x25, 0x20, 0x9c, 0x74, 0xd1, 0x61, 0x89, 0x07, 0x77, 0xa7, 0xee, 0x54, 0xd2, 0x60, 0x5c, 0x2c, 0xce, 0xc3, 0xb3, - 0x28, 0xee, 0x0a, 0xd3, 0xcc, 0x68, 0xf4, 0x03, 0x4d, 0xb4, 0xe7, 0x9b, 0xa5, 0xc4, 0x92, 0x0b, 0x76, 0xb9, 0xc7, - 0xf6, 0x03, 0x45, 0xe2, 0xa5, 0x3c, 0x56, 0x3a, 0xa5, 0xc4, 0x4e, 0x4d, 0x3b, 0x2b, 0xd3, 0x1c, 0x7a, 0x96, 0x65, - 0xe2, 0xb9, 0xf4, 0x3b, 0xaa, 0x67, 0x5b, 0x66, 0x7d, 0x53, 0xb8, 0xdb, 0x3b, 0x91, 0x12, 0x3f, 0x38, 0xd6, 0xf0, - 0xb6, 0xe8, 0x76, 0x9a, 0xbe, 0x2d, 0xdc, 0xfa, 0x05, 0x63, 0x0f, 0x8b, 0x55, 0xac, 0xbe, 0x28, 0x8e, 0x26, 0x14, - 0xd8, 0xea, 0xdf, 0xe4, 0x24, 0x4d, 0xdc, 0x4a, 0xe3, 0xaf, 0x69, 0x09, 0x53, 0x75, 0xaa, 0x7b, 0x2f, 0xb1, 0x8a, - 0xb0, 0x70, 0xff, 0x7d, 0xf5, 0x70, 0x28, 0x64, 0xb6, 0x79, 0xd6, 0x3c, 0x42, 0xba, 0x92, 0x7b, 0xc8, 0xa7, 0x4a, - 0xa6, 0xe6, 0x93, 0x93, 0xec, 0x86, 0xbb, 0x56, 0xab, 0x56, 0xc2, 0x9b, 0x66, 0xab, 0xc3, 0x75, 0xae, 0xd8, 0x68, - 0x99, 0x4d, 0x6a, 0xbb, 0x82, 0xe9, 0xdc, 0x3a, 0xf1, 0x38, 0x44, 0x22, 0x94, 0xb1, 0xbb, 0xbd, 0x51, 0x07, 0x17, - 0xb0, 0x29, 0xc1, 0x5d, 0x29, 0x38, 0x37, 0xd9, 0xe0, 0x2e, 0x88, 0xd4, 0x28, 0xae, 0x74, 0xdc, 0xdb, 0x86, 0x48, - 0xc1, 0x4e, 0x7a, 0xa4, 0x88, 0xc5, 0x69, 0xba, 0xf0, 0x34, 0xbe, 0xf2, 0x66, 0xd7, 0x34, 0x53, 0xdf, 0xa1, 0x46, - 0x8e, 0x68, 0x54, 0xee, 0x65, 0x48, 0x4c, 0x81, 0x87, 0x56, 0xe3, 0x59, 0xaa, 0x42, 0x6e, 0x30, 0xa3, 0x5b, 0xae, - 0xdb, 0xfd, 0xe2, 0xe3, 0x71, 0x39, 0x13, 0xd1, 0x85, 0xf1, 0x95, 0x1a, 0x92, 0x95, 0xec, 0x27, 0x22, 0x2f, 0x38, - 0xa6, 0xb3, 0x37, 0x45, 0x02, 0x6e, 0xe9, 0x8d, 0x8b, 0xb4, 0xa1, 0x5c, 0xcb, 0x06, 0x9d, 0x26, 0x39, 0x15, 0x54, - 0x88, 0x99, 0xb1, 0x66, 0xf1, 0xbe, 0x04, 0x09, 0x87, 0x3d, 0x85, 0x03, 0xd9, 0xd4, 0xcc, 0x6d, 0x87, 0x32, 0xd7, - 0xa1, 0x1a, 0x47, 0x62, 0xa3, 0x72, 0x08, 0x8e, 0xce, 0xdc, 0xee, 0xb1, 0xb0, 0xae, 0x60, 0x4e, 0x15, 0x59, 0x1e, - 0x9c, 0xae, 0xf6, 0x5f, 0xb8, 0x23, 0xfa, 0x62, 0x20, 0xfa, 0x9d, 0x56, 0x4d, 0xb4, 0xc0, 0x43, 0x8b, 0xeb, 0xda, - 0x42, 0x63, 0x0a, 0xe2, 0x80, 0xf4, 0x66, 0x82, 0xa2, 0xe1, 0x93, 0x66, 0x98, 0x83, 0x9e, 0xea, 0x9b, 0x9f, 0x3b, - 0x75, 0xf6, 0x65, 0x9a, 0x5e, 0x18, 0x66, 0x97, 0x06, 0xee, 0x8c, 0xa3, 0xa6, 0x18, 0x36, 0x5f, 0x8c, 0xbe, 0x89, - 0x5c, 0x9e, 0x7b, 0x56, 0x33, 0xc1, 0x34, 0x1d, 0x73, 0xe4, 0xbf, 0xc6, 0xf3, 0x7e, 0xc1, 0x71, 0x8c, 0x4a, 0x2f, - 0xbf, 0x28, 0x73, 0xa6, 0x25, 0x1b, 0xef, 0xab, 0x0b, 0xb8, 0x9e, 0x8c, 0x72, 0x24, 0x1e, 0x96, 0x59, 0x2c, 0x3f, - 0x80, 0x6f, 0x46, 0x2e, 0x41, 0x1b, 0xbb, 0x97, 0x89, 0x01, 0xc0, 0xb2, 0x5d, 0x73, 0x52, 0xbb, 0x46, 0xbe, 0x0a, - 0xb5, 0x55, 0xd7, 0xee, 0x24, 0xf3, 0x95, 0x08, 0xf6, 0x55, 0xfa, 0xe3, 0xa7, 0xa8, 0x07, 0xb5, 0xb7, 0x43, 0x92, - 0xab, 0x4d, 0xc2, 0xbe, 0x5f, 0x56, 0xa7, 0x27, 0xde, 0xbf, 0xc2, 0xe3, 0xe0, 0x02, 0x36, 0x3d, 0xf4, 0xf5, 0xb6, - 0x19, 0x89, 0x51, 0x77, 0x0d, 0xfe, 0xa0, 0xea, 0x21, 0x99, 0x1e, 0x74, 0x92, 0x47, 0x22, 0x30, 0xeb, 0xa9, 0x8e, - 0x89, 0xfc, 0x93, 0xf0, 0x73, 0xb5, 0xe7, 0xff, 0xf2, 0xf5, 0xd2, 0xcc, 0x9e, 0x21, 0xbc, 0x3b, 0xbc, 0xf9, 0xaa, - 0xd0, 0x75, 0xc6, 0xe5, 0xb1, 0x08, 0xe7, 0xce, 0xdf, 0x03, 0x70, 0xe5, 0x75, 0x79, 0xbb, 0x98, 0xef, 0x38, 0xed, - 0x2e, 0x6d, 0xde, 0xad, 0xa3, 0x86, 0x9f, 0x7f, 0xb0, 0x8d, 0x8a, 0x1f, 0xa9, 0x22, 0xfa, 0x75, 0x93, 0x05, 0x45, - 0x20, 0xe4, 0xe9, 0xeb, 0x84, 0x18, 0xff, 0x0c, 0x68, 0xfa, 0xa6, 0x50, 0xd9, 0x7f, 0xc3, 0x15, 0xa6, 0x0e, 0xe1, - 0x8f, 0xcc, 0xea, 0x60, 0x40, 0x73, 0x5b, 0xb8, 0x27, 0xfd, 0x17, 0x88, 0x35, 0x77, 0x10, 0xe0, 0x44, 0x91, 0xa4, - 0xe2, 0x87, 0x3e, 0xbc, 0x82, 0x26, 0xf7, 0x89, 0x14, 0xd4, 0x0c, 0xc5, 0x6d, 0x1b, 0xb8, 0x59, 0x0b, 0xca, 0x47, - 0x87, 0xa8, 0x73, 0xf4, 0x88, 0xdd, 0x5f, 0xda, 0x9d, 0xc9, 0xc3, 0x37, 0x94, 0xac, 0x89, 0x50, 0x31, 0x98, 0x50, - 0xfe, 0x5c, 0xf7, 0x4b, 0xde, 0xb3, 0xf2, 0x95, 0xb1, 0x28, 0xb8, 0xd8, 0x1b, 0x54, 0xfd, 0x00, 0x16, 0xd0, 0x59, - 0x24, 0xa0, 0x62, 0xb7, 0x13, 0xd6, 0xa9, 0xc6, 0xf1, 0x93, 0x58, 0x36, 0xf1, 0xc3, 0xf2, 0x0d, 0xff, 0xa5, 0x21, - 0x24, 0xa1, 0x88, 0x39, 0xa9, 0xc3, 0x60, 0x47, 0x2c, 0x6e, 0x63, 0x36, 0x0f, 0xa5, 0xe6, 0x61, 0x39, 0x71, 0xde, - 0x41, 0x0b, 0x10, 0x97, 0xa3, 0xee, 0xaa, 0xb5, 0x4b, 0xa7, 0x6b, 0x1d, 0x86, 0x93, 0xd8, 0x29, 0x56, 0x78, 0x18, - 0x5b, 0x8f, 0x1c, 0x23, 0xfc, 0x77, 0x20, 0x8f, 0x2f, 0x69, 0x7e, 0x78, 0x7b, 0x47, 0x83, 0x24, 0x1a, 0x2b, 0x15, - 0xa9, 0x78, 0x4a, 0x0f, 0x2b, 0x92, 0x21, 0x4d, 0x24, 0x7a, 0x78, 0x2f, 0xdf, 0xd2, 0x78, 0x58, 0xa5, 0x62, 0x43, - 0xc7, 0xcd, 0x56, 0x07, 0x92, 0x8f, 0xb2, 0xdd, 0x5f, 0x2f, 0xbd, 0x15, 0x9a, 0x75, 0x0a, 0x9b, 0x97, 0x1e, 0xb7, - 0xd8, 0xbb, 0x67, 0x31, 0xf5, 0x53, 0xa0, 0xc6, 0x91, 0x1c, 0x88, 0x89, 0xb1, 0xa9, 0x80, 0x3c, 0xf3, 0xe4, 0xe4, - 0xfd, 0xe0, 0xf5, 0x87, 0x63, 0x1f, 0x4f, 0xa4, 0x7c, 0xcc, 0xce, 0x70, 0xcf, 0xa7, 0x5e, 0x7e, 0xa6, 0x59, 0x1e, - 0x88, 0x9d, 0x8e, 0xe2, 0x21, 0x1f, 0xdd, 0x89, 0x50, 0x23, 0x2c, 0x27, 0x6b, 0xd5, 0x4a, 0x6b, 0x0c, 0x6a, 0x85, - 0x32, 0x97, 0xfb, 0x58, 0xdc, 0xda, 0xfd, 0x68, 0x93, 0xef, 0x7e, 0xa6, 0x88, 0xe7, 0x24, 0x02, 0xb9, 0xfe, 0x61, - 0x90, 0x96, 0x82, 0x79, 0x69, 0xa4, 0x95, 0xfa, 0x13, 0x4a, 0x39, 0xf0, 0x10, 0xf0, 0x25, 0x11, 0x97, 0x86, 0xb6, - 0xfe, 0x07, 0x4c, 0x5e, 0xd7, 0xbd, 0x6f, 0x25, 0xce, 0x9a, 0x70, 0x6e, 0x89, 0x7b, 0xac, 0xe5, 0x27, 0xb5, 0x24, - 0x0f, 0x0a, 0xa3, 0xbd, 0x9d, 0x1e, 0x1a, 0xa6, 0xc5, 0x2b, 0x16, 0xc5, 0x27, 0x7d, 0x2a, 0xbf, 0x07, 0xb5, 0xeb, - 0x2c, 0x75, 0xd9, 0x0b, 0xe5, 0x4c, 0xa9, 0xce, 0x0a, 0xbf, 0x76, 0x18, 0x5a, 0xe9, 0x48, 0x9a, 0x25, 0xce, 0xd5, - 0x7b, 0xec, 0x26, 0x4e, 0xb8, 0x21, 0x0d, 0x14, 0xa8, 0x64, 0x36, 0x1c, 0xd1, 0x53, 0x18, 0xdb, 0xfa, 0x32, 0xc3, - 0xed, 0x87, 0x32, 0xee, 0xe0, 0x68, 0xb2, 0x9a, 0x22, 0x5f, 0x27, 0x45, 0x2c, 0x14, 0x49, 0xd8, 0x85, 0x4b, 0x3b, - 0xbf, 0xc1, 0x5a, 0x69, 0x7e, 0x31, 0x5e, 0x30, 0xde, 0x65, 0x5d, 0xc9, 0x87, 0xcf, 0xba, 0x3b, 0x47, 0x04, 0xc8, - 0xa3, 0x9c, 0xd4, 0x3c, 0x82, 0xdb, 0x84, 0xa8, 0xb7, 0xb7, 0x3d, 0xb9, 0xe1, 0xcc, 0xb6, 0x45, 0x8b, 0x55, 0x2f, - 0x57, 0xb2, 0xdf, 0x9e, 0x95, 0x85, 0x82, 0xec, 0x6e, 0xe0, 0xc8, 0x9d, 0xe9, 0xc4, 0x6f, 0x18, 0x48, 0xef, 0x41, - 0x2d, 0x38, 0xba, 0x6e, 0x01, 0xa8, 0x35, 0xb4, 0x91, 0x4e, 0x5f, 0x23, 0xdb, 0xc8, 0xb8, 0xbc, 0x77, 0x1c, 0x41, - 0x71, 0xc0, 0xf8, 0xfa, 0xde, 0x31, 0x9d, 0x96, 0x80, 0xa4, 0x8f, 0x98, 0x0f, 0x03, 0x8c, 0x82, 0x18, 0x03, 0xd5, - 0xea, 0xf1, 0x01, 0x4f, 0x40, 0xc4, 0x91, 0xad, 0x0e, 0x6e, 0xdc, 0x20, 0x6f, 0x1d, 0x19, 0x07, 0x9f, 0x90, 0x6e, - 0x28, 0x61, 0x30, 0x5e, 0xfe, 0xc8, 0x40, 0x75, 0xa1, 0x8e, 0x0d, 0xae, 0x6d, 0x14, 0x34, 0xce, 0x0c, 0x10, 0x08, - 0x3e, 0xbd, 0x5d, 0xe9, 0xaf, 0xe3, 0x0f, 0x3a, 0xab, 0x37, 0x05, 0xa9, 0x95, 0xd3, 0xa3, 0x36, 0x5b, 0xe8, 0x2a, - 0xa0, 0x70, 0xa6, 0x7a, 0xc2, 0x80, 0xeb, 0x0f, 0x1b, 0x06, 0xe6, 0x3d, 0x27, 0x94, 0xd9, 0x1c, 0x09, 0x7f, 0x49, - 0xb3, 0x6f, 0xd6, 0x30, 0xcf, 0xe5, 0xd8, 0x83, 0x1d, 0x02, 0xb9, 0x7a, 0x18, 0xfb, 0x2d, 0xb6, 0x4d, 0x10, 0xe6, - 0xb0, 0xfc, 0xf8, 0x9f, 0x0a, 0xb5, 0x15, 0x4a, 0xed, 0xcd, 0x8f, 0x1c, 0xd6, 0xce, 0x73, 0x79, 0xfc, 0x4f, 0x28, - 0xf2, 0xd9, 0x3c, 0xe4, 0x79, 0xb2, 0xd8, 0x36, 0x88, 0x3f, 0x3d, 0xb2, 0x77, 0x36, 0xbb, 0xd6, 0x3e, 0xc8, 0xcf, - 0x60, 0x97, 0x7f, 0x0f, 0x09, 0xd5, 0xb0, 0x65, 0x05, 0x3f, 0x8c, 0x47, 0x04, 0x80, 0x85, 0x5e, 0xbf, 0xd9, 0x37, - 0xe4, 0x66, 0x1f, 0x90, 0x19, 0xf4, 0x39, 0xa2, 0x91, 0x67, 0xc6, 0x35, 0xec, 0xcc, 0x73, 0x3e, 0xf7, 0x0c, 0xe7, - 0x07, 0xca, 0x7a, 0xca, 0x9c, 0xe7, 0x25, 0x1b, 0xf7, 0xb6, 0x70, 0x06, 0xba, 0xd5, 0x8c, 0x5d, 0xd8, 0x82, 0xe5, - 0x3b, 0x6b, 0xc1, 0xa9, 0x1b, 0x30, 0x7b, 0x7b, 0xee, 0x4f, 0x74, 0xe0, 0xcf, 0x50, 0xde, 0xc9, 0xa8, 0xd5, 0x6f, - 0xbe, 0x75, 0x3b, 0x8d, 0x01, 0x6f, 0x84, 0xa7, 0x8a, 0xea, 0xcc, 0x39, 0x7b, 0x0a, 0x72, 0x21, 0xfe, 0xa2, 0x1b, - 0x7c, 0x42, 0xb7, 0x2a, 0x0a, 0x01, 0x5f, 0xda, 0x62, 0x44, 0xc8, 0x3a, 0xb4, 0xa4, 0x94, 0x27, 0x6d, 0x3e, 0x51, - 0x73, 0xa7, 0xe8, 0x34, 0xb7, 0x32, 0x3f, 0x9c, 0x39, 0x81, 0x0d, 0x02, 0x49, 0x48, 0x11, 0xc2, 0x3f, 0xc5, 0x8e, - 0x7b, 0x67, 0x6c, 0xb9, 0x91, 0xd0, 0xa0, 0x5d, 0x94, 0x8a, 0x18, 0x1f, 0x95, 0x4e, 0x23, 0xae, 0x7b, 0x8f, 0xf0, - 0x0f, 0xf6, 0x3f, 0xd3, 0xa8, 0x4c, 0xff, 0x9d, 0x46, 0x61, 0xfa, 0xcf, 0x69, 0x08, 0xa6, 0xff, 0x9e, 0x06, 0xbb, - 0x4b, 0xad, 0x0e, 0xec, 0xab, 0x23, 0xfb, 0xea, 0xce, 0x1e, 0xa7, 0xd9, 0x1e, 0x5a, 0x7b, 0x5f, 0x83, 0x76, 0x6c, - 0x3f, 0xf1, 0x2d, 0x39, 0xe0, 0xad, 0x63, 0x59, 0xb2, 0xf1, 0x76, 0x8a, 0xbd, 0xcf, 0xe9, 0xd2, 0xe5, 0x71, 0x1f, - 0xc5, 0x53, 0x1e, 0x87, 0xd5, 0x74, 0x56, 0x51, 0x67, 0x5a, 0xa6, 0x91, 0x3a, 0xbb, 0x7b, 0x28, 0x9e, 0x6a, 0x3e, - 0x42, 0xde, 0xad, 0x25, 0x9c, 0x81, 0xd2, 0x04, 0xf9, 0xad, 0xe7, 0x8f, 0x8d, 0x62, 0x2f, 0x1a, 0x6f, 0xbb, 0xfb, - 0x99, 0x21, 0xce, 0x5f, 0x0c, 0x91, 0x54, 0xa6, 0x15, 0x26, 0xda, 0xc1, 0xd4, 0x6d, 0xcd, 0x5a, 0xac, 0x29, 0x20, - 0xb3, 0x3d, 0x8f, 0xb2, 0x25, 0x08, 0xe1, 0xb9, 0x6d, 0xe1, 0x3f, 0x0b, 0x58, 0x75, 0xb1, 0x85, 0x5e, 0x73, 0x39, - 0xe8, 0xb4, 0x52, 0xe9, 0x3e, 0x6b, 0x10, 0xbb, 0xa1, 0x4c, 0x77, 0x84, 0x8c, 0xe1, 0x05, 0x8b, 0x2b, 0x28, 0xea, - 0x17, 0x62, 0x71, 0x17, 0xb3, 0x87, 0xe7, 0x27, 0x65, 0x1a, 0xfc, 0xbf, 0x16, 0xdb, 0x41, 0x77, 0x42, 0x53, 0xe3, - 0x92, 0x4b, 0x2a, 0xec, 0x17, 0x62, 0xdc, 0x9e, 0xdb, 0x45, 0xd7, 0xb7, 0x4e, 0x19, 0x89, 0xcf, 0xf9, 0x0c, 0xe4, - 0x7a, 0xe9, 0xa7, 0xfa, 0xf4, 0x88, 0x0b, 0xb2, 0xa8, 0xa7, 0x39, 0xc1, 0xaa, 0x10, 0x33, 0x52, 0x87, 0x9a, 0x12, - 0x9f, 0xbf, 0xfa, 0x9f, 0xf6, 0x6b, 0x49, 0x3c, 0x68, 0xa7, 0x5f, 0xf9, 0xf5, 0xb1, 0x10, 0x97, 0xf6, 0x33, 0xf1, - 0xe3, 0xad, 0x62, 0xed, 0x0f, 0xa8, 0x7a, 0x9c, 0xaa, 0xff, 0x3d, 0x6a, 0xd1, 0xaf, 0xc3, 0x65, 0xd3, 0x7f, 0x2d, - 0x89, 0x07, 0xec, 0xf5, 0xeb, 0xf3, 0x3b, 0x18, 0xfc, 0x13, 0x43, 0xf2, 0xc8, 0x76, 0x02, 0x94, 0xe3, 0x47, 0xd1, - 0xe4, 0x38, 0xe4, 0x4c, 0x53, 0xae, 0x2b, 0x3c, 0xbd, 0xea, 0x68, 0x4c, 0x95, 0x8b, 0x23, 0xe9, 0xf4, 0x7c, 0x02, - 0x13, 0xd9, 0xf0, 0x96, 0xd9, 0xa5, 0xc8, 0xde, 0xc3, 0x11, 0x64, 0xb7, 0xcd, 0x27, 0x31, 0xcb, 0x67, 0x11, 0x2d, - 0xdb, 0x35, 0xd8, 0xe8, 0x94, 0xc3, 0x54, 0x5c, 0x38, 0xc0, 0xbe, 0xb7, 0x5c, 0x18, 0xec, 0x46, 0x6a, 0x1f, 0xa2, - 0x72, 0x7a, 0x1b, 0xd1, 0x6f, 0xca, 0x71, 0xf4, 0x7e, 0x1b, 0xac, 0x96, 0xc2, 0xc3, 0x43, 0x83, 0x58, 0xb5, 0xc3, - 0x2b, 0x46, 0xfd, 0xe2, 0x3a, 0xd4, 0x6e, 0x00, 0x4e, 0x9c, 0x69, 0xd3, 0xf5, 0xe3, 0xfc, 0xc2, 0x9f, 0xea, 0xd3, - 0x95, 0xd5, 0x53, 0x0f, 0x5d, 0xc4, 0xd1, 0x19, 0x97, 0x9d, 0x83, 0x12, 0x23, 0x8c, 0x19, 0x9e, 0xbf, 0x37, 0x2b, - 0x4b, 0x28, 0x48, 0x0b, 0xbd, 0x16, 0x54, 0x19, 0xfd, 0xfb, 0x03, 0xc5, 0xb9, 0xbc, 0x7f, 0xae, 0x7b, 0xff, 0x1e, - 0x33, 0xb4, 0xcd, 0x8c, 0x7a, 0xab, 0xe0, 0x3e, 0x9f, 0x24, 0xb0, 0x48, 0x96, 0x58, 0xdb, 0xe2, 0xff, 0xea, 0x12, - 0xeb, 0x34, 0xaa, 0xbd, 0xc2, 0xd5, 0x99, 0xb6, 0xe6, 0xab, 0xfa, 0x52, 0x73, 0xaf, 0xee, 0x47, 0x3f, 0xd8, 0x30, - 0x8d, 0x4b, 0x7b, 0x5a, 0x90, 0x9b, 0x64, 0xcf, 0xa2, 0xc7, 0xe6, 0x64, 0x1c, 0x5a, 0xf5, 0x43, 0x93, 0x00, 0x51, - 0xc6, 0xa9, 0x47, 0x9a, 0xf2, 0x59, 0xee, 0xc3, 0x12, 0x2f, 0xb8, 0x10, 0xd7, 0xc3, 0xed, 0xee, 0x9e, 0x91, 0x1d, - 0xa8, 0xf2, 0x9b, 0x77, 0x87, 0xf7, 0x7d, 0xa4, 0xfc, 0x0e, 0x54, 0xb3, 0xbe, 0x59, 0xa9, 0x08, 0xd4, 0x15, 0x28, - 0x02, 0x5c, 0xbe, 0x67, 0x95, 0xe0, 0xae, 0xe6, 0x79, 0x18, 0xb1, 0x92, 0x84, 0x9a, 0x2b, 0x05, 0x87, 0xc5, 0xa6, - 0x29, 0x45, 0x61, 0xb1, 0x26, 0xfa, 0x75, 0xcd, 0xa6, 0xd3, 0x45, 0x0d, 0x9c, 0x1b, 0x98, 0xa5, 0x9b, 0x35, 0xa2, - 0x1f, 0x12, 0xf2, 0x0e, 0x9e, 0x66, 0x8b, 0x6d, 0x20, 0x86, 0x5a, 0x5c, 0x63, 0x60, 0x7b, 0xf8, 0x90, 0x07, 0xf4, - 0xa4, 0xff, 0x74, 0x0d, 0xd1, 0x23, 0xdb, 0x80, 0xc5, 0x6f, 0x26, 0x75, 0x78, 0xf7, 0x30, 0x3d, 0xe3, 0xa5, 0x4f, - 0xc6, 0x2f, 0x0e, 0x6d, 0x86, 0x9f, 0x1e, 0x51, 0x54, 0x24, 0x2a, 0x77, 0x76, 0xd9, 0xcf, 0x86, 0x4c, 0xed, 0xe9, - 0x78, 0xb2, 0xbf, 0x60, 0x6e, 0xfd, 0x60, 0x7f, 0x18, 0xc7, 0x83, 0x04, 0x35, 0x14, 0x1b, 0xfe, 0x71, 0xa3, 0x58, - 0x24, 0x3d, 0x5b, 0x6f, 0xfb, 0xe0, 0x95, 0x50, 0xce, 0x2b, 0xd7, 0x32, 0x3d, 0xd3, 0xb1, 0x83, 0x67, 0xfa, 0xc1, - 0xea, 0x32, 0x01, 0x95, 0xdf, 0x85, 0x89, 0x81, 0x73, 0x24, 0xca, 0x11, 0x19, 0xf1, 0xa2, 0xe8, 0x4b, 0x36, 0x87, - 0x56, 0x58, 0x0d, 0xba, 0xa5, 0xe8, 0xaf, 0x57, 0x76, 0x97, 0xfa, 0xae, 0xcf, 0x5e, 0xe4, 0xf6, 0xe6, 0x63, 0x19, - 0x3a, 0x14, 0x29, 0x25, 0xa7, 0xfe, 0x64, 0x0c, 0x79, 0x7d, 0x3d, 0x75, 0xa6, 0xf8, 0xcf, 0x6c, 0x90, 0xc4, 0x6c, - 0x00, 0x0a, 0x59, 0x34, 0x8f, 0x00, 0x60, 0x49, 0x5f, 0x24, 0x81, 0x37, 0xfc, 0x43, 0xac, 0x59, 0x37, 0x44, 0xcc, - 0x57, 0xfb, 0xe6, 0xe2, 0x32, 0x0b, 0x77, 0x76, 0xec, 0xd1, 0x3d, 0x79, 0x10, 0x95, 0x25, 0x99, 0x4d, 0x67, 0xed, - 0x3d, 0xa4, 0xaf, 0x0c, 0x79, 0x06, 0x99, 0x32, 0x40, 0x02, 0xa6, 0x23, 0xac, 0x33, 0x3c, 0xb9, 0xe6, 0xdc, 0x6a, - 0xb2, 0x50, 0x82, 0x43, 0x74, 0x1c, 0xdd, 0x0a, 0x49, 0xd6, 0xdc, 0x6b, 0xbe, 0xd4, 0x0f, 0x52, 0x4e, 0x3e, 0xad, - 0x98, 0x27, 0x8e, 0xe3, 0x37, 0x24, 0xa2, 0x27, 0x11, 0xe5, 0x69, 0xd7, 0x39, 0xe4, 0xb7, 0xac, 0x54, 0xcc, 0x0c, - 0x54, 0x3d, 0xf2, 0x54, 0x13, 0xac, 0xbb, 0xc5, 0x1d, 0x88, 0xa7, 0x0f, 0x44, 0x13, 0x8a, 0x93, 0xac, 0xf2, 0x5a, - 0x0f, 0xb3, 0xe1, 0x2b, 0x62, 0x73, 0x35, 0xda, 0xec, 0x58, 0xcc, 0xd8, 0x0a, 0x9a, 0x60, 0x40, 0x9d, 0x11, 0x4e, - 0xbb, 0x76, 0xf7, 0x28, 0x30, 0xb2, 0xe9, 0x94, 0x7e, 0x8c, 0xa8, 0xd0, 0x6d, 0xbe, 0x8c, 0x0a, 0x71, 0x54, 0x84, - 0x9e, 0x87, 0xa1, 0x50, 0xfa, 0x69, 0x59, 0x14, 0xf1, 0x59, 0x3f, 0xe7, 0xae, 0xc6, 0x18, 0x53, 0x34, 0x95, 0x65, - 0xd7, 0x15, 0xc8, 0x1b, 0xb1, 0x35, 0x44, 0x80, 0x3c, 0x5f, 0x35, 0xed, 0xea, 0xd7, 0x9b, 0xa8, 0xfc, 0x3b, 0x36, - 0xba, 0x8d, 0x76, 0x13, 0x78, 0x56, 0x9c, 0xb9, 0x2d, 0x80, 0x35, 0x3a, 0xd7, 0x25, 0x71, 0xe4, 0xe8, 0x71, 0xbd, - 0x1f, 0xcc, 0xfe, 0xa4, 0xb5, 0x78, 0x90, 0x6f, 0x91, 0xa9, 0x95, 0x52, 0x17, 0xea, 0xb5, 0x65, 0x1a, 0xcf, 0x07, - 0x99, 0x49, 0xf9, 0x84, 0xd1, 0xf9, 0xd2, 0x4d, 0xf3, 0xc2, 0x66, 0x81, 0xc9, 0x44, 0x25, 0x4e, 0x61, 0x3a, 0xb7, - 0x4b, 0x84, 0xa4, 0x3b, 0x82, 0x53, 0x59, 0x56, 0x14, 0x77, 0xb7, 0xad, 0xd9, 0x37, 0x93, 0xbf, 0x26, 0x3d, 0x1c, - 0xe3, 0x2e, 0xe8, 0xd8, 0x28, 0x6f, 0x26, 0xdb, 0x83, 0xc9, 0xc3, 0xea, 0x42, 0xe9, 0xb4, 0x9a, 0x6e, 0xea, 0x19, - 0xb9, 0xb9, 0x71, 0xea, 0x6a, 0xa2, 0xeb, 0x12, 0x30, 0x9e, 0x8d, 0xe2, 0x1e, 0x5b, 0xe4, 0x1a, 0x79, 0x6d, 0x2d, - 0x41, 0xb7, 0x2c, 0x14, 0x8b, 0xd1, 0xd4, 0xc0, 0x18, 0x61, 0x52, 0x11, 0x83, 0xd7, 0xe7, 0xb0, 0xc9, 0x13, 0x13, - 0xa8, 0xea, 0xdf, 0x94, 0x93, 0x25, 0xbb, 0x98, 0xa5, 0x91, 0x0c, 0xcb, 0x41, 0xd9, 0x7b, 0xa2, 0xa5, 0x8f, 0x78, - 0x2e, 0x70, 0x6d, 0xdb, 0xf6, 0x61, 0x6d, 0x6b, 0xc0, 0xc0, 0xfb, 0xa6, 0x7d, 0x07, 0xc1, 0x15, 0xbb, 0xd5, 0x9c, - 0x67, 0xf0, 0x78, 0xc0, 0xec, 0x5b, 0xf2, 0x7c, 0x5e, 0xa8, 0xb2, 0x7d, 0xa2, 0xb3, 0xfb, 0x02, 0x42, 0x31, 0xbb, - 0xd1, 0xe5, 0xd9, 0x6e, 0xbb, 0x87, 0x0c, 0x59, 0x90, 0xb1, 0x24, 0x29, 0x3c, 0xf2, 0x9d, 0x5e, 0x6d, 0x19, 0x6b, - 0x3e, 0x73, 0x19, 0xb7, 0xa4, 0x16, 0x94, 0x6a, 0x0f, 0x6d, 0x8a, 0xf2, 0x5a, 0x84, 0x49, 0x15, 0xd6, 0x6e, 0xf3, - 0x99, 0xca, 0xd1, 0x16, 0x91, 0x09, 0x1e, 0x17, 0x12, 0x3b, 0x83, 0x25, 0x82, 0x0c, 0x9d, 0x26, 0x68, 0x6a, 0x9f, - 0x44, 0xb3, 0xb8, 0xe6, 0xc1, 0xa8, 0xa6, 0xca, 0xe1, 0x75, 0x13, 0x26, 0xcc, 0xe3, 0x42, 0xda, 0x76, 0xc4, 0x24, - 0x5d, 0xc7, 0xf9, 0x6a, 0x25, 0xeb, 0x51, 0x8e, 0xcc, 0x73, 0xa5, 0xb3, 0x95, 0x2e, 0xe6, 0x41, 0x29, 0xca, 0xcb, - 0x50, 0x20, 0x91, 0x93, 0xad, 0x66, 0x6f, 0x2f, 0x8d, 0xd5, 0x40, 0xa4, 0x57, 0xd6, 0xc7, 0x23, 0x58, 0x4c, 0x17, - 0x29, 0xa5, 0x60, 0x03, 0x85, 0xb0, 0xd1, 0xd8, 0xb3, 0x56, 0xfe, 0xf9, 0xb9, 0xa6, 0x5a, 0xf5, 0x67, 0x84, 0x6d, - 0xf6, 0x8b, 0xf7, 0xe5, 0x58, 0x05, 0x18, 0x75, 0x2f, 0xb2, 0x22, 0x79, 0xab, 0xcb, 0x5b, 0x24, 0x6f, 0xbe, 0xbc, - 0x32, 0x71, 0xc2, 0xf3, 0xb5, 0xd6, 0x86, 0x09, 0x77, 0x6e, 0x55, 0xeb, 0x88, 0x2b, 0x31, 0xd7, 0x7e, 0xdf, 0xa0, - 0x9f, 0x26, 0xa2, 0xec, 0x5f, 0xcd, 0xa8, 0x90, 0x4d, 0x9f, 0x12, 0xaa, 0xcd, 0xbc, 0x8c, 0x90, 0xbb, 0x17, 0x83, - 0x49, 0xa9, 0x4e, 0x5d, 0xdd, 0xe6, 0xe3, 0x8b, 0x31, 0xb1, 0x7e, 0xf9, 0xd7, 0xb8, 0x38, 0x5f, 0x30, 0x1c, 0xba, - 0xe2, 0xce, 0x7b, 0xd6, 0x0a, 0xe5, 0x0a, 0x73, 0xc0, 0x39, 0x5a, 0xaa, 0x2a, 0x63, 0xd4, 0xb6, 0xea, 0xdc, 0x01, - 0x8f, 0x23, 0x08, 0xfc, 0x8e, 0xae, 0x1a, 0x49, 0x46, 0xa9, 0xef, 0xea, 0x18, 0xfc, 0x65, 0xa4, 0xf1, 0xe1, 0xf7, - 0x05, 0x11, 0xf4, 0xd2, 0x55, 0xe5, 0xda, 0xa3, 0x2a, 0xa2, 0xac, 0x82, 0x24, 0xe6, 0x64, 0xb9, 0x0b, 0x47, 0xb9, - 0xf1, 0xe7, 0x93, 0x5d, 0xad, 0x0d, 0x42, 0xcc, 0x62, 0xcc, 0xd9, 0x52, 0xcc, 0x59, 0x11, 0xbe, 0x7d, 0x1e, 0xfd, - 0xce, 0x55, 0x25, 0x10, 0xf9, 0x68, 0xe3, 0x51, 0x7c, 0xf9, 0x22, 0xe0, 0x69, 0x55, 0x7d, 0x20, 0x24, 0xbe, 0xb3, - 0xd3, 0x2e, 0xf9, 0x6b, 0x7f, 0xa6, 0x44, 0x32, 0x69, 0x89, 0x21, 0x70, 0x47, 0xfc, 0xce, 0x75, 0x03, 0x19, 0x80, - 0x9c, 0xd1, 0xae, 0x01, 0x73, 0x12, 0x4d, 0x43, 0x42, 0xd5, 0xb4, 0x96, 0x77, 0xf3, 0x0a, 0x7d, 0x22, 0x89, 0x7e, - 0x97, 0x37, 0xc3, 0xaf, 0xb4, 0x48, 0xe5, 0x9c, 0xbf, 0xef, 0xe2, 0x57, 0x75, 0x64, 0xe7, 0x4c, 0x63, 0xa5, 0xc0, - 0x97, 0x01, 0xb8, 0x81, 0x76, 0xc5, 0x8d, 0x38, 0xce, 0x92, 0x1e, 0xd9, 0x19, 0x3d, 0x50, 0xdb, 0x57, 0xb2, 0x68, - 0x29, 0x5e, 0x98, 0xa6, 0x10, 0xca, 0xe0, 0x22, 0x3e, 0x91, 0xb9, 0xa8, 0xb2, 0xe6, 0x55, 0x5f, 0xe0, 0x36, 0x22, - 0x66, 0x44, 0x79, 0x22, 0x92, 0x9c, 0x95, 0xba, 0xa1, 0xc1, 0xe2, 0xe8, 0xd2, 0x62, 0x4d, 0x4e, 0x90, 0xcc, 0x97, - 0x88, 0xe0, 0x5f, 0x2e, 0xd5, 0xb3, 0xfb, 0x9b, 0xb5, 0x67, 0x85, 0xb6, 0x46, 0x60, 0xb2, 0x8b, 0x5e, 0xbd, 0xe8, - 0x35, 0xb7, 0x86, 0xf2, 0x21, 0x51, 0xc8, 0xef, 0x49, 0xdd, 0x1a, 0xd6, 0x4c, 0x52, 0x30, 0xdf, 0x17, 0xb5, 0x45, - 0xeb, 0xce, 0xb1, 0x97, 0x3f, 0xea, 0x71, 0xf7, 0x54, 0x82, 0x82, 0xd0, 0x6d, 0x26, 0xb5, 0x40, 0x24, 0x59, 0x63, - 0x8b, 0x7d, 0x22, 0x7a, 0xc7, 0x74, 0xa5, 0x74, 0x71, 0x64, 0xfb, 0xe5, 0x1d, 0x32, 0x93, 0x9c, 0x5d, 0x2a, 0x31, - 0xe5, 0x3f, 0x47, 0xd9, 0xe4, 0x62, 0x67, 0x8b, 0x3e, 0x73, 0x90, 0x36, 0x53, 0x13, 0x61, 0x0a, 0xf6, 0xce, 0xcc, - 0x46, 0x08, 0x8f, 0x24, 0x93, 0xc2, 0x28, 0xb1, 0x97, 0x7c, 0x8a, 0xa7, 0x90, 0xdf, 0x2a, 0x34, 0xb4, 0xe4, 0x99, - 0xfe, 0x07, 0xeb, 0x08, 0xdf, 0x4a, 0xe0, 0x20, 0xc9, 0x3f, 0x60, 0xc1, 0x6d, 0xe9, 0x7a, 0xc7, 0x6c, 0x90, 0x84, - 0xfb, 0x67, 0x96, 0xcf, 0x76, 0x7b, 0x10, 0xbf, 0x29, 0x12, 0x82, 0x2f, 0x3a, 0xaa, 0x5d, 0xb2, 0x8c, 0x4a, 0xaa, - 0x45, 0xe5, 0x7e, 0x7c, 0xcc, 0xcb, 0x23, 0x2a, 0x2f, 0xe0, 0x97, 0xef, 0xd6, 0x1c, 0x98, 0x81, 0xaf, 0xb4, 0xd5, - 0x44, 0xc2, 0x5e, 0x18, 0xec, 0x29, 0x94, 0x2c, 0xe2, 0xc0, 0x6e, 0x36, 0xc4, 0x5c, 0xe8, 0x5a, 0x9b, 0xed, 0xc3, - 0x18, 0x6a, 0x75, 0xca, 0xf4, 0x26, 0xae, 0x6a, 0x84, 0xb9, 0x4d, 0x23, 0x8e, 0x4a, 0x66, 0xda, 0x97, 0x1b, 0x4c, - 0xd3, 0x21, 0x7b, 0x1b, 0x68, 0x2d, 0xdf, 0x1c, 0xeb, 0xca, 0x9b, 0x69, 0x8f, 0x42, 0xc0, 0x18, 0xb1, 0xe6, 0x8a, - 0x5e, 0x6b, 0x65, 0x3f, 0x48, 0xb1, 0x3f, 0xaa, 0x05, 0x22, 0xaa, 0x22, 0xa9, 0xd9, 0xb0, 0xcb, 0x5e, 0xad, 0xbd, - 0xac, 0x0b, 0xb0, 0x74, 0x6a, 0x39, 0xd6, 0xbc, 0x60, 0x30, 0x94, 0xa9, 0x5a, 0x2d, 0x73, 0x87, 0xab, 0xec, 0xa9, - 0x96, 0x97, 0xb2, 0x20, 0x61, 0x2f, 0x81, 0xe8, 0xc4, 0xf7, 0x74, 0x4f, 0x22, 0xdf, 0x99, 0xd3, 0x37, 0x66, 0x32, - 0xc4, 0xe8, 0xac, 0x58, 0x81, 0x55, 0xbd, 0x0d, 0x0c, 0x15, 0xb5, 0xc6, 0x86, 0xee, 0xf2, 0x98, 0x7d, 0x8e, 0xc3, - 0x7d, 0x61, 0xbf, 0x2d, 0xc8, 0x7d, 0xf8, 0xef, 0x59, 0x7e, 0xdb, 0xd8, 0x2c, 0xcc, 0xb2, 0xde, 0x3c, 0x46, 0x8e, - 0xf0, 0x7a, 0x4b, 0xa5, 0xca, 0x16, 0x0c, 0xd9, 0x6b, 0x58, 0xf7, 0xab, 0x99, 0xba, 0x90, 0x6e, 0x62, 0x46, 0x8c, - 0xda, 0x9d, 0x48, 0x12, 0xf4, 0x14, 0x33, 0x28, 0xa1, 0x79, 0x91, 0xd6, 0x66, 0x23, 0xf7, 0x60, 0x9d, 0x8e, 0x4c, - 0x44, 0x97, 0x60, 0x8a, 0x73, 0x36, 0xdf, 0xdf, 0x61, 0xc8, 0x61, 0x8f, 0x25, 0xaa, 0xe4, 0x41, 0xbd, 0x6f, 0x65, - 0xa5, 0xa6, 0xd8, 0x74, 0x2c, 0x23, 0x2e, 0x37, 0x80, 0x83, 0x1d, 0x6d, 0xa7, 0x86, 0x39, 0xb5, 0x73, 0x09, 0x76, - 0x72, 0x53, 0x7b, 0xb7, 0x22, 0x03, 0x4b, 0x1e, 0x08, 0x55, 0x18, 0xf0, 0x69, 0x5f, 0x11, 0x00, 0x9a, 0xe3, 0x14, - 0x89, 0x3f, 0x8c, 0xe4, 0xef, 0x77, 0x24, 0x9d, 0x84, 0xe3, 0x6e, 0x24, 0x70, 0x7a, 0x1c, 0xc0, 0x28, 0xf5, 0x64, - 0xf3, 0x03, 0x10, 0xe5, 0x22, 0x7f, 0x95, 0x18, 0x1d, 0x31, 0x44, 0x38, 0xf0, 0x53, 0x71, 0x21, 0x6d, 0xbd, 0x59, - 0x2e, 0x8e, 0x46, 0x41, 0xd7, 0xd5, 0x01, 0xf7, 0x91, 0x0a, 0x00, 0x6d, 0x36, 0xe4, 0xba, 0xbe, 0x77, 0x88, 0xd8, - 0x7d, 0x5a, 0xb8, 0x41, 0x04, 0x7e, 0x5c, 0xa6, 0xe6, 0x5b, 0x38, 0x5c, 0xd3, 0x4c, 0xbd, 0x95, 0xc7, 0xd3, 0x7d, - 0xdd, 0xed, 0x0c, 0xf0, 0x2f, 0x97, 0x38, 0x60, 0xfe, 0x3b, 0xa9, 0xe2, 0x60, 0xc4, 0x1c, 0x1d, 0xe3, 0x92, 0x66, - 0x66, 0x6a, 0xc8, 0x73, 0x73, 0xe5, 0x29, 0xca, 0x81, 0xcc, 0x27, 0xd3, 0x43, 0x76, 0x13, 0xf8, 0x35, 0x40, 0x5d, - 0x3c, 0x24, 0x54, 0x80, 0x7a, 0x82, 0xc0, 0xd5, 0x04, 0xc2, 0xb2, 0xf9, 0x73, 0x6c, 0xee, 0x99, 0x68, 0x02, 0x1a, - 0x3a, 0x50, 0xe9, 0xf4, 0xa4, 0x2c, 0x80, 0x7a, 0xa8, 0xfd, 0x10, 0x09, 0x0f, 0x7a, 0xd9, 0x74, 0xd0, 0xb8, 0x8e, - 0x46, 0x48, 0x9a, 0x50, 0x90, 0xb8, 0xc0, 0x29, 0xfa, 0x6a, 0xc9, 0xfc, 0x55, 0x22, 0xdf, 0xa8, 0x72, 0xd1, 0xe0, - 0x5f, 0xa2, 0x45, 0x56, 0xcf, 0x18, 0x99, 0x1d, 0x6d, 0xca, 0x4a, 0x65, 0xbc, 0x00, 0xf0, 0xe1, 0x10, 0x1c, 0x49, - 0x44, 0x2c, 0x93, 0x68, 0x22, 0x1b, 0x87, 0xf2, 0xa7, 0x97, 0x77, 0x0a, 0xe0, 0x7a, 0x1e, 0x09, 0x9a, 0x08, 0x7c, - 0x6c, 0x09, 0x38, 0x33, 0x83, 0x00, 0x67, 0xab, 0x4d, 0x23, 0x30, 0x11, 0x5a, 0x79, 0x6a, 0xd8, 0xc7, 0x48, 0x94, - 0xe3, 0x12, 0x61, 0xe4, 0x76, 0x4d, 0x19, 0xb9, 0x44, 0x24, 0x36, 0xe6, 0xc8, 0x73, 0xfd, 0x51, 0x0a, 0xfd, 0xcb, - 0x2c, 0x7c, 0x52, 0xa2, 0xfa, 0xd2, 0xa0, 0x78, 0xd3, 0xc6, 0x07, 0x3b, 0x18, 0xd7, 0x52, 0xcb, 0x61, 0xc2, 0x60, - 0xbe, 0xf6, 0xff, 0xc6, 0xd7, 0x4a, 0xf5, 0x95, 0xf3, 0x11, 0x5d, 0xf1, 0x18, 0x1c, 0xd6, 0x89, 0x9c, 0x5f, 0x37, - 0x4d, 0xe4, 0xf8, 0x73, 0x79, 0xb7, 0x37, 0x9e, 0xee, 0x36, 0x82, 0xca, 0xa5, 0x34, 0x67, 0x9e, 0x11, 0x7d, 0x18, - 0x58, 0xbe, 0x58, 0xa3, 0xca, 0x34, 0xbe, 0x74, 0x40, 0xb9, 0xe2, 0xf0, 0xf4, 0x24, 0x10, 0x2f, 0x07, 0x7b, 0x8a, - 0x03, 0x62, 0x45, 0x89, 0xb2, 0x67, 0x2a, 0x22, 0x8d, 0x43, 0x60, 0xbd, 0x11, 0x19, 0x19, 0x02, 0x52, 0x86, 0xed, - 0x9a, 0xf5, 0xaf, 0x58, 0x51, 0x7c, 0x0e, 0xc9, 0x26, 0xcb, 0x66, 0x7c, 0x8a, 0x1d, 0x8d, 0x57, 0x22, 0xa9, 0x68, - 0xd9, 0xf4, 0xa7, 0x6d, 0x47, 0xf6, 0x5e, 0x82, 0x43, 0xe2, 0x00, 0xa7, 0xf4, 0xce, 0x9f, 0xcb, 0x3b, 0xab, 0x23, - 0xdf, 0x83, 0xfe, 0x95, 0x97, 0xfc, 0x95, 0xef, 0xc1, 0x9e, 0xf2, 0x92, 0xa7, 0x7c, 0x0f, 0xf8, 0x94, 0x17, 0x89, - 0xe2, 0x34, 0x7d, 0xe5, 0x70, 0x1e, 0x8c, 0x11, 0x43, 0xb9, 0x3c, 0x91, 0xad, 0xe4, 0xe0, 0x17, 0xef, 0x13, 0xee, - 0xb3, 0x81, 0x94, 0x3c, 0x66, 0xa6, 0x58, 0x89, 0x4a, 0x56, 0x96, 0x49, 0x01, 0x7c, 0xea, 0xdb, 0xc5, 0x99, 0xed, - 0x17, 0xfc, 0xe1, 0x97, 0x48, 0x1a, 0x03, 0x71, 0xa7, 0x04, 0x5d, 0xbb, 0xd3, 0xd7, 0x9e, 0xdb, 0x8a, 0x08, 0xca, - 0x72, 0xc4, 0xe8, 0x44, 0xa5, 0x1d, 0x67, 0x89, 0xde, 0xbd, 0x55, 0x18, 0x08, 0xfa, 0x76, 0xa1, 0x8f, 0xc8, 0x61, - 0xfd, 0xfd, 0x17, 0x20, 0x64, 0x5c, 0x12, 0x88, 0xc0, 0x3e, 0xf6, 0xea, 0x39, 0xd4, 0xda, 0xf3, 0xa4, 0x8a, 0x06, - 0x5c, 0x93, 0x83, 0x71, 0x8b, 0x90, 0xa4, 0xa7, 0xff, 0x88, 0x1f, 0x92, 0xb3, 0x74, 0xd1, 0x3c, 0x0b, 0xf7, 0x18, - 0x2d, 0x07, 0x34, 0x27, 0x41, 0xd5, 0xcc, 0x72, 0x45, 0x34, 0x9a, 0xd3, 0x9e, 0x79, 0xe0, 0x64, 0xa9, 0xb6, 0xae, - 0xc2, 0x9d, 0xf7, 0xf8, 0x19, 0x9f, 0xd1, 0x71, 0x9a, 0x1e, 0x19, 0xb0, 0x2f, 0x72, 0x7a, 0x1f, 0x5c, 0xe1, 0x58, - 0x6b, 0x30, 0x3d, 0x91, 0x6c, 0x2d, 0xae, 0xaf, 0xc6, 0xf8, 0x82, 0xb4, 0xca, 0xfb, 0x52, 0x44, 0xc3, 0x4f, 0xc9, - 0xc4, 0x66, 0xa4, 0xa5, 0x21, 0x5f, 0x5b, 0x6a, 0xb4, 0x59, 0xb1, 0x04, 0x4b, 0x6e, 0xbf, 0x75, 0x85, 0x4d, 0xdf, - 0x64, 0x2e, 0x92, 0x6c, 0x54, 0xdd, 0x14, 0x69, 0x53, 0xe0, 0x53, 0x92, 0x15, 0xc1, 0x23, 0x50, 0x64, 0xee, 0x48, - 0x72, 0xbc, 0x74, 0xd4, 0x72, 0xaa, 0x4a, 0x88, 0xc2, 0x67, 0x15, 0x56, 0xb5, 0x14, 0x1d, 0x2f, 0x0e, 0x44, 0x08, - 0x39, 0x8c, 0x4b, 0xa7, 0xa6, 0xd1, 0x75, 0xad, 0xf6, 0x16, 0xf2, 0x1c, 0x7c, 0xf9, 0x69, 0x88, 0x25, 0x2c, 0x59, - 0x8d, 0x31, 0xe0, 0xcc, 0xb3, 0x1b, 0x7a, 0xe4, 0xb9, 0xbc, 0x1f, 0x80, 0xa3, 0x17, 0xbb, 0x16, 0x8a, 0xb9, 0xeb, - 0x33, 0x12, 0x09, 0x24, 0x32, 0x5f, 0x00, 0x70, 0x00, 0xc0, 0x55, 0x2f, 0xab, 0x3a, 0x80, 0x41, 0x2b, 0x55, 0xa0, - 0x67, 0x0a, 0x6e, 0x81, 0xcc, 0xd0, 0x72, 0x50, 0xf9, 0x23, 0x0a, 0x7c, 0xed, 0x90, 0x2c, 0x26, 0xb2, 0x34, 0x94, - 0xac, 0x63, 0x42, 0x3b, 0x1f, 0xc6, 0xd3, 0x4b, 0x14, 0xee, 0x22, 0xa5, 0xa3, 0xb6, 0xe8, 0xa7, 0x67, 0x8f, 0x7a, - 0x5a, 0x38, 0x2d, 0x22, 0x2f, 0xc7, 0xc6, 0xbf, 0xe5, 0xed, 0xba, 0x5c, 0x7f, 0x64, 0x2b, 0x9a, 0x82, 0x36, 0x04, - 0x9f, 0x3d, 0x5b, 0xf5, 0x8c, 0xbe, 0x42, 0x4e, 0x8b, 0xe5, 0xd0, 0xeb, 0xb7, 0x59, 0x6d, 0x0e, 0x1f, 0x9e, 0xd1, - 0x03, 0xd1, 0xa5, 0xbb, 0xd7, 0x48, 0xa0, 0xb9, 0x44, 0xb0, 0x18, 0x9e, 0xe3, 0xd2, 0x6e, 0x72, 0x28, 0x29, 0x0a, - 0x62, 0x15, 0xf8, 0x80, 0xde, 0xde, 0xc1, 0x90, 0xc1, 0xae, 0xdb, 0x18, 0xc0, 0xd5, 0x40, 0xf3, 0xac, 0xc5, 0x61, - 0xaf, 0x4f, 0xc0, 0xc6, 0xd2, 0xf9, 0x6a, 0xdb, 0x45, 0xb1, 0xd7, 0x5c, 0x91, 0xee, 0xda, 0x0a, 0x81, 0x3f, 0x17, - 0x9f, 0xfc, 0xed, 0x79, 0xd1, 0xfe, 0xd6, 0x7f, 0x29, 0xbd, 0x77, 0xaa, 0xf6, 0x7b, 0xa3, 0x01, 0xf5, 0xc7, 0xa9, - 0x65, 0xa9, 0x2c, 0x87, 0x59, 0xe9, 0xf9, 0x68, 0x54, 0x8b, 0xdb, 0x6b, 0x8a, 0x88, 0x8f, 0x92, 0xe0, 0x66, 0x53, - 0x2b, 0x0f, 0xee, 0x9d, 0xed, 0x85, 0xbe, 0x87, 0x81, 0xea, 0x5e, 0x0b, 0x4d, 0x77, 0x2e, 0x25, 0xa8, 0xc9, 0xc8, - 0x68, 0xa6, 0xd9, 0x98, 0xb7, 0xbe, 0xf9, 0x61, 0xaa, 0x5f, 0xd0, 0xa7, 0x52, 0x72, 0x20, 0x3b, 0xab, 0x8b, 0x52, - 0xb1, 0x2a, 0x09, 0xc1, 0xe2, 0xfa, 0xbb, 0x38, 0x24, 0x30, 0x70, 0xad, 0xba, 0x0c, 0x18, 0x89, 0x7d, 0xb1, 0xf8, - 0xa8, 0xe8, 0xf8, 0xad, 0x1d, 0x64, 0x70, 0xb3, 0xcb, 0xbc, 0x0c, 0x33, 0xfc, 0x20, 0xac, 0xd3, 0x9a, 0x82, 0x40, - 0x4d, 0x9d, 0xbc, 0x4a, 0x82, 0x62, 0xf9, 0x66, 0x4f, 0x1b, 0x7b, 0x61, 0x6c, 0x03, 0xe8, 0xd9, 0xa6, 0x91, 0x18, - 0x4c, 0xfc, 0x93, 0x63, 0xf7, 0x57, 0xa1, 0xd2, 0xaa, 0x6b, 0x51, 0x7b, 0x51, 0x1e, 0x84, 0x0e, 0xa1, 0x43, 0x51, - 0x2b, 0xb7, 0x75, 0x58, 0x9f, 0xd7, 0xb2, 0x3c, 0xe9, 0x9f, 0x78, 0xbb, 0x48, 0xd7, 0xc5, 0x1d, 0x0c, 0x35, 0x12, - 0xc8, 0x8e, 0x2a, 0x36, 0x69, 0xdc, 0x51, 0xa9, 0xb2, 0xdc, 0x7d, 0xd8, 0xa0, 0x5e, 0x5b, 0x44, 0x90, 0xa9, 0x3e, - 0xae, 0x08, 0x35, 0x86, 0x3d, 0xa1, 0x34, 0x05, 0x4c, 0x65, 0x95, 0xc5, 0x53, 0x73, 0x77, 0x7e, 0xc8, 0x99, 0xd2, - 0x70, 0xc0, 0xc7, 0xb0, 0x98, 0x0d, 0xfc, 0xfb, 0x21, 0xd2, 0xc0, 0x4d, 0xad, 0x67, 0xa1, 0x4c, 0x20, 0xad, 0x50, - 0xcc, 0x47, 0x32, 0x0a, 0x13, 0x7c, 0xcf, 0x19, 0x14, 0x39, 0x29, 0xd9, 0x68, 0xfc, 0x66, 0xdd, 0x63, 0xe8, 0x38, - 0x33, 0x3e, 0xcc, 0xd3, 0x15, 0x7b, 0xa5, 0xc0, 0xd5, 0x21, 0x14, 0x5c, 0x8e, 0xa3, 0x1a, 0xd7, 0x05, 0xd9, 0x40, - 0x49, 0x5e, 0x78, 0x8f, 0x24, 0xa4, 0x2e, 0xf4, 0x94, 0x6a, 0x47, 0x21, 0x19, 0x96, 0x60, 0x7a, 0xdc, 0x9d, 0xb1, - 0x8d, 0x2b, 0x69, 0xdb, 0xd9, 0x69, 0xa1, 0x6e, 0xcf, 0xc0, 0x83, 0x5d, 0xd5, 0x3b, 0x28, 0x47, 0x56, 0x06, 0x1a, - 0x8c, 0x80, 0xb6, 0x2c, 0x49, 0xaa, 0x89, 0xd8, 0x68, 0x14, 0x46, 0x0d, 0xa5, 0xb5, 0x92, 0x9d, 0x9c, 0xdf, 0xeb, - 0xaa, 0x98, 0x26, 0xeb, 0x50, 0x1c, 0xf4, 0xc4, 0x24, 0xa5, 0x79, 0x5d, 0xb6, 0x78, 0x7f, 0xa0, 0xf6, 0x8b, 0x54, - 0x7b, 0x62, 0x6f, 0x6e, 0x24, 0x4a, 0xb3, 0xef, 0x29, 0x94, 0x87, 0x46, 0xe7, 0xb5, 0xd1, 0x79, 0x79, 0x1d, 0xa5, - 0xff, 0x9b, 0xa8, 0x65, 0xca, 0xc6, 0x98, 0xa2, 0x0e, 0x68, 0x3e, 0x31, 0x82, 0xf6, 0xfd, 0x4b, 0xa1, 0x8e, 0x50, - 0x34, 0x4c, 0x3d, 0xce, 0xb0, 0x18, 0xe9, 0x06, 0xcf, 0x97, 0x84, 0x04, 0xf7, 0xee, 0xc0, 0xc0, 0x23, 0xc2, 0x3c, - 0x91, 0x31, 0x9d, 0x20, 0x0c, 0x51, 0x59, 0x27, 0x67, 0xde, 0xe7, 0xe6, 0xdf, 0x6e, 0x4a, 0xdb, 0x6e, 0xfa, 0x0d, - 0x26, 0xe7, 0xfd, 0x94, 0xde, 0x7b, 0x79, 0xb4, 0x69, 0x7f, 0x31, 0xb2, 0x5b, 0xf0, 0xc2, 0xe2, 0x3d, 0x16, 0xf6, - 0x2f, 0x65, 0x3e, 0x73, 0xac, 0xf4, 0x36, 0x63, 0x20, 0x83, 0x67, 0xd6, 0x58, 0xfe, 0x4a, 0xd0, 0x8e, 0x42, 0xa0, - 0x9d, 0xd8, 0x09, 0x59, 0x05, 0x09, 0x88, 0xc4, 0x58, 0xdb, 0xce, 0xc1, 0x40, 0x3b, 0xd6, 0x99, 0x5b, 0xb4, 0xf4, - 0xdd, 0x53, 0x4e, 0x4a, 0x00, 0xca, 0x4b, 0xe5, 0x9f, 0x5d, 0x9c, 0x1a, 0xfb, 0x38, 0xc1, 0xd8, 0x0a, 0xec, 0x43, - 0x02, 0xa9, 0x2a, 0x26, 0x74, 0x9a, 0x22, 0xa0, 0x8b, 0x63, 0x13, 0x7f, 0x6e, 0xdc, 0x9d, 0xc1, 0xea, 0x69, 0xbe, - 0x64, 0x9b, 0xdd, 0x4b, 0x8c, 0xa9, 0xcd, 0x3a, 0xdb, 0x16, 0xf3, 0x8c, 0xdc, 0x95, 0xc5, 0xda, 0x04, 0x52, 0xb6, - 0xe4, 0xca, 0xb5, 0x45, 0xc8, 0x84, 0x21, 0xeb, 0x1a, 0x42, 0x52, 0x20, 0xf8, 0x2d, 0x25, 0x81, 0xd5, 0xfb, 0xa5, - 0xae, 0xd4, 0xb3, 0x88, 0x76, 0x99, 0xa0, 0x1d, 0x1c, 0x39, 0xd2, 0x79, 0xe1, 0xff, 0xb7, 0x12, 0x42, 0x38, 0xd3, - 0x86, 0x6e, 0x4b, 0x68, 0x92, 0xe2, 0xe8, 0x2a, 0x5a, 0x40, 0xbe, 0xeb, 0xf5, 0x2f, 0x8d, 0xcd, 0xfb, 0x0e, 0x9e, - 0x0d, 0x22, 0x81, 0x8d, 0xa8, 0xa9, 0x51, 0x0d, 0xab, 0xad, 0x6e, 0xda, 0x6e, 0x1e, 0xdd, 0xde, 0xc8, 0xc7, 0x50, - 0xe1, 0xe8, 0xe7, 0x40, 0x89, 0xe3, 0xde, 0x34, 0xa5, 0x4d, 0x54, 0xfe, 0x17, 0xaa, 0x05, 0x4e, 0xe1, 0x93, 0x1b, - 0x9c, 0x0a, 0x4e, 0xbb, 0xa7, 0x86, 0xe2, 0x7e, 0xbf, 0x54, 0xd1, 0xf5, 0x71, 0x73, 0x95, 0x01, 0x9a, 0xf0, 0x08, - 0x72, 0x39, 0xf2, 0xfd, 0xbc, 0xae, 0xfc, 0x22, 0xbf, 0xf4, 0xed, 0x6b, 0x47, 0xd8, 0x42, 0x8d, 0xb4, 0xd0, 0xe3, - 0x24, 0xbf, 0x2c, 0x6f, 0x92, 0xee, 0x90, 0x81, 0xeb, 0x2f, 0x6b, 0xec, 0x1d, 0xaa, 0xf2, 0xd8, 0x6d, 0x8f, 0x14, - 0x08, 0xd3, 0x49, 0x97, 0xca, 0x5d, 0x29, 0x25, 0x62, 0xd4, 0x86, 0xb3, 0x4e, 0xd5, 0xa2, 0xb6, 0xb0, 0x9e, 0x2d, - 0x74, 0x93, 0x0a, 0x08, 0xd5, 0xf7, 0x94, 0x87, 0x63, 0x60, 0xd8, 0x3b, 0x5f, 0x1e, 0x27, 0x0b, 0xe7, 0x53, 0x5d, - 0x2b, 0xe7, 0xa9, 0xb6, 0xeb, 0xbe, 0xce, 0x50, 0x60, 0x6c, 0xe9, 0x1d, 0xbb, 0x0c, 0xe7, 0xb7, 0xea, 0x0a, 0x4f, - 0x96, 0x11, 0x3c, 0x4b, 0x7d, 0x42, 0xf0, 0x55, 0xf2, 0x48, 0xe1, 0xc1, 0xd1, 0xcd, 0x59, 0x40, 0x07, 0xd3, 0x49, - 0xe0, 0xc1, 0xf1, 0xb6, 0x56, 0xc9, 0xfa, 0xe0, 0xe5, 0x98, 0x10, 0x06, 0x94, 0xac, 0x0f, 0xb6, 0xdd, 0x58, 0xb9, - 0x44, 0xe3, 0xf5, 0x23, 0x0b, 0x2d, 0xba, 0x7e, 0xd0, 0xa6, 0xe7, 0x01, 0x53, 0x39, 0xaa, 0xae, 0xe7, 0x29, 0x67, - 0x87, 0x98, 0x27, 0x8c, 0x74, 0x62, 0xd0, 0xc8, 0x0e, 0x98, 0x47, 0xa9, 0xf9, 0xa9, 0x75, 0x9b, 0x6f, 0xf6, 0xe1, - 0x33, 0x61, 0xac, 0xd5, 0x46, 0x91, 0xcf, 0x53, 0x68, 0xcf, 0x8e, 0xbc, 0xdf, 0xa8, 0x21, 0x23, 0x6b, 0xd3, 0x15, - 0x90, 0x8c, 0x05, 0x7f, 0x36, 0x43, 0x58, 0xd4, 0x4a, 0xc6, 0x69, 0x62, 0x9f, 0x4d, 0x37, 0x91, 0xae, 0xf2, 0x55, - 0x04, 0x78, 0xbf, 0xe1, 0x4c, 0x9a, 0xc7, 0x96, 0x5b, 0x8c, 0x98, 0xbc, 0xd4, 0x84, 0xda, 0xa2, 0x89, 0x28, 0x01, - 0xa0, 0xd7, 0xc3, 0x3e, 0x02, 0x95, 0xbe, 0x43, 0x38, 0x37, 0x4f, 0x6c, 0x69, 0x93, 0x9a, 0x0b, 0x0a, 0xc3, 0x1d, - 0x3b, 0xd9, 0x8b, 0x4d, 0x26, 0xf6, 0x0c, 0xe6, 0xa1, 0xc5, 0x5a, 0x76, 0xf3, 0x47, 0xb1, 0xe3, 0x07, 0x34, 0x90, - 0xf9, 0x01, 0x0b, 0x92, 0x3f, 0x96, 0x0e, 0x71, 0x2e, 0x04, 0xc5, 0x43, 0x5a, 0xc9, 0x22, 0x16, 0xa4, 0xdb, 0xf1, - 0x22, 0xce, 0x65, 0x4e, 0x6e, 0x01, 0x41, 0x75, 0x20, 0x16, 0xb2, 0xdc, 0x40, 0x1a, 0x6f, 0x70, 0xe1, 0xbc, 0xc9, - 0x89, 0x24, 0xb0, 0xf5, 0x4c, 0x24, 0x93, 0x45, 0x39, 0x16, 0x81, 0xdf, 0x7c, 0xec, 0xfe, 0x66, 0x3e, 0x36, 0x1b, - 0x7b, 0xda, 0x2c, 0xdf, 0x2c, 0xc2, 0xcc, 0xda, 0xaa, 0xc2, 0x84, 0x25, 0x92, 0x71, 0xc2, 0x5b, 0x7b, 0xf8, 0xca, - 0xad, 0xe1, 0x33, 0xf8, 0xdd, 0xcc, 0x16, 0x73, 0x69, 0x3b, 0x5b, 0x24, 0xe9, 0x20, 0x4c, 0x37, 0xe1, 0x1f, 0x62, - 0x64, 0xba, 0xd8, 0xac, 0xe8, 0x47, 0x83, 0x44, 0xf1, 0x76, 0xe3, 0xe5, 0xe1, 0xb7, 0x08, 0x0e, 0x76, 0x0b, 0xb2, - 0xb9, 0xfb, 0x72, 0xa4, 0xe2, 0x21, 0x2b, 0xaa, 0x1a, 0x63, 0x84, 0x52, 0x88, 0x63, 0x88, 0xba, 0xdc, 0xbe, 0x6a, - 0xcb, 0x43, 0xba, 0xfa, 0x5a, 0x64, 0x14, 0xde, 0xca, 0x7f, 0x63, 0xbe, 0xe3, 0x9f, 0x31, 0x95, 0xd4, 0x79, 0x0e, - 0xf0, 0xb5, 0xdf, 0xc1, 0x20, 0x21, 0x2a, 0x22, 0x7f, 0x2d, 0x11, 0x20, 0x66, 0xbd, 0xc4, 0x00, 0xee, 0xbc, 0xba, - 0x95, 0x93, 0x90, 0xbe, 0x60, 0xe8, 0x6d, 0x8b, 0x85, 0x79, 0x3c, 0x92, 0x7c, 0x87, 0xb1, 0x88, 0x9d, 0xf5, 0xc1, - 0x8c, 0x9d, 0xba, 0xe6, 0xc3, 0x74, 0xf6, 0x1f, 0x23, 0x2c, 0x70, 0x04, 0x43, 0xad, 0x85, 0x9f, 0xae, 0x02, 0xb8, - 0xd3, 0x7f, 0x08, 0x52, 0xe0, 0x36, 0x7a, 0xe2, 0x67, 0xba, 0x17, 0xd8, 0x04, 0xa7, 0x62, 0xaf, 0x88, 0xed, 0x39, - 0xd0, 0xab, 0x55, 0x0d, 0xa5, 0xbb, 0x73, 0x3a, 0x08, 0x53, 0x2c, 0x0a, 0x63, 0xbd, 0x8e, 0x12, 0x9b, 0x55, 0xcb, - 0x69, 0xc2, 0x90, 0xed, 0x2a, 0xd4, 0x9e, 0xe4, 0xc2, 0xa2, 0x84, 0x23, 0x37, 0x89, 0x37, 0xd5, 0x3a, 0xa0, 0x7e, - 0xeb, 0xd7, 0x26, 0xb8, 0xf5, 0x82, 0xa5, 0x63, 0x41, 0xa1, 0xa5, 0x88, 0xc1, 0x23, 0x44, 0x06, 0xaf, 0xcb, 0x15, - 0xe2, 0xf4, 0x22, 0xfd, 0xbe, 0x75, 0x57, 0x4a, 0x4b, 0x77, 0xb3, 0x88, 0xd9, 0xcf, 0xe9, 0xaf, 0x86, 0x97, 0xd7, - 0x9c, 0x91, 0x71, 0xd1, 0xb4, 0xe8, 0xa9, 0xd7, 0xb8, 0xdc, 0x80, 0xd9, 0x43, 0xab, 0x63, 0x86, 0xfd, 0x33, 0x2d, - 0x2d, 0xc6, 0xf8, 0x9d, 0x28, 0xa6, 0xdd, 0xef, 0x3e, 0x75, 0xe2, 0x9e, 0x5e, 0xe8, 0xa0, 0xf7, 0x96, 0x78, 0xdb, - 0xe9, 0x9d, 0xae, 0x3e, 0x9b, 0x8e, 0x41, 0xf4, 0x8d, 0x32, 0x7d, 0x37, 0x0b, 0x5d, 0x2e, 0x63, 0xd6, 0x68, 0x93, - 0xf6, 0xe5, 0x72, 0xfa, 0xa5, 0x97, 0xb9, 0xf1, 0x6f, 0xe1, 0x7a, 0x32, 0x24, 0x92, 0x96, 0x2a, 0x95, 0x2a, 0x9c, - 0x74, 0x81, 0xc4, 0x9a, 0x89, 0x5a, 0xae, 0x51, 0x67, 0x1c, 0x0f, 0x97, 0x0e, 0xcb, 0xef, 0x9b, 0x0f, 0x2a, 0xbd, - 0x7c, 0x1f, 0x88, 0x7d, 0x76, 0x25, 0x19, 0x50, 0x4e, 0xe1, 0x8c, 0xec, 0xfe, 0x73, 0x58, 0xed, 0x0a, 0xa0, 0x66, - 0x18, 0xbd, 0x5c, 0x1a, 0x76, 0x50, 0x94, 0x7e, 0x3a, 0xe9, 0xa6, 0x20, 0xac, 0xaf, 0xce, 0xc2, 0xeb, 0x5a, 0x92, - 0xe8, 0x12, 0x7f, 0x25, 0xdd, 0x4f, 0x38, 0xc9, 0xbe, 0x43, 0x7d, 0x55, 0x97, 0x00, 0xd0, 0x21, 0x5e, 0xa9, 0x40, - 0x9a, 0x79, 0x4b, 0xba, 0x4a, 0x64, 0x5d, 0x7c, 0x48, 0x01, 0x5c, 0x59, 0x6f, 0x9f, 0x66, 0x21, 0x5d, 0x6b, 0x89, - 0x9d, 0x84, 0x6e, 0xb8, 0x9f, 0x30, 0x02, 0x24, 0xd8, 0xf1, 0x00, 0x9a, 0xbc, 0x13, 0x9e, 0x63, 0xbd, 0x9a, 0x58, - 0x83, 0x20, 0xa2, 0x7b, 0x2f, 0xc1, 0x6e, 0x4e, 0xf3, 0xac, 0xb0, 0x09, 0xd1, 0xec, 0xc8, 0x7d, 0x3f, 0x39, 0xf0, - 0x7a, 0x61, 0x53, 0xb1, 0x51, 0x99, 0x3a, 0xb9, 0xc5, 0x38, 0xc0, 0x3e, 0x2d, 0x05, 0xd4, 0x70, 0x15, 0xa5, 0x2c, - 0xa7, 0x29, 0xa1, 0xc5, 0x38, 0xe0, 0xf4, 0x25, 0x07, 0xff, 0x27, 0x82, 0x26, 0x64, 0x1d, 0x7e, 0xfc, 0xb1, 0x05, - 0x7f, 0x24, 0xad, 0x69, 0x56, 0x44, 0xab, 0x3d, 0xc5, 0x1a, 0x34, 0x2f, 0x93, 0x8f, 0x0d, 0x03, 0xd8, 0xbc, 0x16, - 0xb2, 0xfa, 0x89, 0x9b, 0x56, 0x3c, 0x50, 0x7e, 0xca, 0x41, 0xed, 0xa9, 0x35, 0xf6, 0x42, 0xb2, 0xd3, 0xac, 0xa8, - 0x88, 0xe2, 0x7a, 0xb2, 0x5d, 0x17, 0xef, 0xbe, 0x48, 0x54, 0xfc, 0xe9, 0x06, 0x62, 0x48, 0x40, 0x02, 0x83, 0x15, - 0xd4, 0x90, 0xd0, 0x51, 0x5f, 0x6f, 0xbe, 0xbc, 0xd5, 0x20, 0xd0, 0x7c, 0xe9, 0x14, 0x10, 0x33, 0x15, 0xb3, 0xf3, - 0x21, 0xa0, 0x8a, 0xf7, 0x6f, 0x30, 0x69, 0x36, 0x7e, 0xb7, 0x8e, 0x6b, 0x5d, 0x38, 0xff, 0x51, 0xab, 0x66, 0xc0, - 0x86, 0xb8, 0xa0, 0x4a, 0xd1, 0xb0, 0xca, 0x58, 0x20, 0x1a, 0x3d, 0x7d, 0x1c, 0x59, 0x0a, 0xfb, 0xe7, 0xe6, 0xcb, - 0x67, 0xad, 0x4e, 0x6d, 0x5e, 0xcd, 0x5c, 0x4a, 0xa2, 0xe0, 0x32, 0x7d, 0xc3, 0x57, 0x00, 0x20, 0x33, 0x6b, 0x10, - 0x14, 0x34, 0xf8, 0x1a, 0x7c, 0x6e, 0x45, 0xc8, 0x38, 0xd0, 0xfd, 0x90, 0xf1, 0x87, 0x9b, 0xf6, 0x8a, 0xde, 0xd2, - 0xca, 0x7c, 0x4b, 0xaf, 0xf7, 0xb4, 0xd5, 0xf5, 0x4b, 0x8b, 0x19, 0x75, 0xa9, 0x5a, 0x9e, 0x7e, 0xde, 0xee, 0x7b, - 0xe2, 0xd6, 0xda, 0x9f, 0x8a, 0x32, 0xb6, 0x27, 0xe5, 0xa0, 0x31, 0x37, 0xfe, 0x05, 0xea, 0x35, 0x0d, 0x8d, 0x8a, - 0x42, 0x79, 0x5b, 0xf7, 0xad, 0x50, 0xe6, 0xed, 0x89, 0x8c, 0x23, 0xa9, 0x3b, 0x86, 0xbc, 0x2f, 0x6d, 0xe3, 0xf3, - 0x9a, 0x22, 0x50, 0xf8, 0x95, 0xe9, 0x94, 0x02, 0x5d, 0x13, 0x2e, 0x11, 0x3c, 0xb4, 0xbc, 0x2e, 0xdd, 0x98, 0x41, - 0xe5, 0xe8, 0x03, 0xbd, 0xa5, 0x0d, 0xc1, 0x8f, 0x8b, 0xf0, 0x8b, 0x9d, 0xd0, 0x4c, 0xae, 0x02, 0x35, 0x13, 0x55, - 0xf6, 0x90, 0xac, 0x0c, 0x96, 0x13, 0xa9, 0x49, 0xdf, 0xd4, 0x99, 0x40, 0x83, 0xa9, 0x57, 0x3e, 0xeb, 0x82, 0xa1, - 0x8b, 0x5d, 0x69, 0x61, 0x60, 0x11, 0x26, 0x5f, 0x84, 0x5f, 0x1f, 0x7d, 0x2d, 0x8d, 0x16, 0x18, 0x02, 0x84, 0xe6, - 0x5e, 0x5e, 0x34, 0x0a, 0xb6, 0xbf, 0xbb, 0x69, 0xaf, 0x57, 0x78, 0xf0, 0xed, 0x83, 0x79, 0x89, 0xc5, 0x81, 0x9e, - 0x9b, 0xfc, 0x71, 0x27, 0xed, 0x44, 0x41, 0x48, 0x6d, 0x2e, 0x70, 0x08, 0x50, 0xd5, 0xec, 0x21, 0xce, 0x56, 0x29, - 0x3b, 0x71, 0x04, 0x64, 0xf9, 0x6d, 0x04, 0xbe, 0x7c, 0xb7, 0xc6, 0xde, 0x63, 0x91, 0xb9, 0x28, 0xed, 0xf1, 0xb7, - 0xbb, 0x0b, 0xab, 0xbb, 0x79, 0x78, 0x2c, 0x28, 0xec, 0xfc, 0xe1, 0x3c, 0xee, 0xeb, 0xba, 0x7b, 0x00, 0x98, 0x81, - 0xf0, 0x71, 0x21, 0x1c, 0x02, 0xd1, 0x4c, 0x37, 0xeb, 0xf6, 0xa3, 0x4a, 0xaa, 0x8a, 0xa7, 0x00, 0x27, 0x1c, 0x02, - 0xef, 0x4c, 0x3d, 0x36, 0x4b, 0xb0, 0xd9, 0x0e, 0xc0, 0x10, 0x4a, 0xd3, 0x1c, 0x37, 0xe5, 0x06, 0xc8, 0x77, 0x11, - 0xc3, 0x14, 0xf7, 0x62, 0x8f, 0x86, 0x0f, 0x28, 0x8a, 0x68, 0xe9, 0xbc, 0x20, 0xdf, 0x76, 0x11, 0xcb, 0x9d, 0x27, - 0xa3, 0x0c, 0x3e, 0x11, 0xf9, 0x16, 0x29, 0x64, 0xee, 0x47, 0x9a, 0xc2, 0x6a, 0x9b, 0xd6, 0x8f, 0x01, 0x91, 0x5b, - 0x5a, 0x37, 0x26, 0x5a, 0x03, 0x17, 0x7a, 0x13, 0xf9, 0x02, 0x5a, 0xdb, 0x2a, 0x76, 0x9f, 0x5f, 0x89, 0x64, 0xf0, - 0x40, 0x98, 0x7f, 0xe3, 0xe1, 0xad, 0xd1, 0x31, 0xa5, 0x66, 0x76, 0x49, 0xfb, 0xe3, 0xbd, 0xb5, 0x97, 0xad, 0xfb, - 0xd6, 0xbb, 0x6a, 0x4d, 0x9e, 0xd3, 0x21, 0x95, 0xd8, 0x49, 0x05, 0x50, 0x04, 0x1f, 0x5b, 0x3e, 0x3f, 0x07, 0xa8, - 0x13, 0x05, 0x5c, 0xa8, 0x72, 0xe2, 0xcc, 0x38, 0xcc, 0xf2, 0x2b, 0x69, 0x73, 0x70, 0xfb, 0x79, 0x93, 0x62, 0x20, - 0x84, 0x42, 0x07, 0x64, 0xea, 0x0d, 0x4e, 0x6a, 0x6b, 0xda, 0x02, 0x8b, 0x39, 0x2c, 0x10, 0x39, 0x82, 0x00, 0xe4, - 0x10, 0xe9, 0x5a, 0xa5, 0xfb, 0x78, 0xd0, 0x0d, 0x28, 0x6f, 0x04, 0x66, 0xa4, 0x3f, 0x80, 0xd8, 0xb1, 0xb6, 0x73, - 0x95, 0x88, 0x30, 0x89, 0x34, 0x16, 0x1e, 0xfe, 0x25, 0x53, 0x52, 0x3e, 0x62, 0xb1, 0x1b, 0x82, 0x69, 0x31, 0xaf, - 0x48, 0x0e, 0x29, 0x48, 0x17, 0xea, 0xea, 0x5b, 0x8e, 0xc9, 0x39, 0xcd, 0x32, 0x14, 0xa6, 0x48, 0x18, 0x39, 0x9b, - 0x36, 0x13, 0x59, 0x40, 0x33, 0x56, 0x45, 0xa0, 0x5c, 0x91, 0xf5, 0xa8, 0x92, 0x3f, 0x72, 0xfe, 0x4d, 0x58, 0x05, - 0x97, 0x63, 0xc7, 0xba, 0x61, 0x9d, 0x9d, 0x17, 0x95, 0x69, 0x99, 0x7c, 0x5b, 0x96, 0x24, 0x5e, 0xc2, 0x63, 0x21, - 0xde, 0x2f, 0xfa, 0x6d, 0xf5, 0x14, 0xfa, 0xe5, 0xae, 0x1d, 0x42, 0x85, 0x54, 0x0c, 0x6a, 0x09, 0x13, 0x45, 0x8b, - 0xd2, 0xf8, 0xbd, 0x00, 0x5b, 0x1e, 0x03, 0xda, 0x58, 0xfa, 0xc1, 0x4a, 0x5c, 0x95, 0x23, 0x6a, 0x59, 0xbd, 0x95, - 0xa2, 0x93, 0xcb, 0xca, 0x32, 0xda, 0x22, 0x92, 0x80, 0x00, 0xe7, 0x2b, 0x65, 0x35, 0xbf, 0xe4, 0x3a, 0xac, 0x5b, - 0xbf, 0xb2, 0x54, 0x2a, 0x4c, 0xd1, 0x62, 0xb0, 0x8c, 0x08, 0xe3, 0x36, 0xd7, 0xba, 0x38, 0x7e, 0xd7, 0xfc, 0x0d, - 0xe8, 0xb7, 0x70, 0x97, 0xbb, 0x6a, 0x05, 0x6e, 0x5e, 0x44, 0x74, 0x41, 0x2e, 0x03, 0xb9, 0x0b, 0xaa, 0x37, 0x68, - 0xc1, 0x96, 0xac, 0xb7, 0xe6, 0x32, 0x2f, 0x0f, 0x7d, 0x15, 0x83, 0x3c, 0x7d, 0xa8, 0x68, 0x75, 0xa8, 0xf5, 0x71, - 0x6f, 0xff, 0x69, 0xaf, 0xda, 0x69, 0x40, 0x07, 0xe4, 0xbe, 0xd6, 0xf1, 0x75, 0x97, 0xff, 0xf9, 0x87, 0xdb, 0x22, - 0x91, 0xfb, 0x25, 0x75, 0x03, 0x1d, 0x82, 0xd2, 0x81, 0x60, 0x3b, 0x5e, 0x5a, 0x7e, 0x72, 0xd0, 0x0b, 0x6b, 0x42, - 0x2d, 0xbc, 0x29, 0x4f, 0x83, 0x11, 0x21, 0x94, 0x92, 0x58, 0xe7, 0xb4, 0xd9, 0xc3, 0x82, 0x3e, 0xdc, 0xe1, 0xac, - 0x36, 0xa6, 0x3f, 0x21, 0xaa, 0x4c, 0xb4, 0x07, 0x76, 0x17, 0x4d, 0x6c, 0x78, 0xd8, 0x0f, 0x4a, 0x52, 0x42, 0xb5, - 0xaf, 0xe8, 0x03, 0x65, 0x62, 0x8e, 0x2f, 0x3b, 0x14, 0xfc, 0x55, 0x6a, 0x89, 0x4d, 0x0c, 0xf6, 0x1d, 0x87, 0x25, - 0x51, 0x31, 0x80, 0xcd, 0x65, 0x8c, 0x59, 0x5f, 0x60, 0x8b, 0xd8, 0x9f, 0x56, 0x03, 0x65, 0xbf, 0x5b, 0xf7, 0x7d, - 0x67, 0x05, 0x50, 0xe6, 0x9a, 0x9f, 0xf4, 0x7b, 0xe4, 0x7b, 0xb0, 0xa8, 0x5f, 0x87, 0xa0, 0x45, 0xd7, 0xb5, 0x35, - 0x71, 0xe6, 0xb3, 0x74, 0xcf, 0x0d, 0x17, 0xfe, 0xbe, 0x2a, 0x90, 0x09, 0xd2, 0x74, 0xa8, 0x62, 0x33, 0x2e, 0xca, - 0x28, 0xb6, 0x0c, 0xf7, 0xc2, 0xef, 0xa4, 0x20, 0x42, 0x84, 0x8c, 0x61, 0x5a, 0x22, 0xe8, 0xd4, 0x7c, 0x9e, 0x36, - 0x02, 0xd5, 0x35, 0x29, 0x73, 0x4f, 0x97, 0x3b, 0xe2, 0x41, 0x89, 0x1e, 0x59, 0x02, 0xf4, 0x7f, 0xab, 0xe7, 0x1a, - 0xb7, 0x9c, 0x11, 0xa4, 0x59, 0x10, 0x19, 0x9c, 0xc1, 0x71, 0x8e, 0x1b, 0x2d, 0x24, 0x48, 0x14, 0x77, 0x27, 0xa1, - 0x4f, 0xda, 0x38, 0x35, 0xf8, 0x05, 0xb9, 0x28, 0x37, 0x2a, 0x00, 0xb5, 0x7b, 0x88, 0x16, 0x05, 0x73, 0x66, 0xc8, - 0x8e, 0xbc, 0x87, 0x18, 0x1e, 0xda, 0x52, 0x69, 0x1e, 0x73, 0x72, 0x02, 0x51, 0x73, 0x99, 0x27, 0x35, 0xf6, 0x0a, - 0xfa, 0x06, 0x14, 0xa7, 0xd0, 0xc6, 0x98, 0x38, 0x5d, 0x9e, 0xfa, 0x54, 0x0d, 0x44, 0xe9, 0xd9, 0x7c, 0x5d, 0xac, - 0x23, 0x76, 0xc0, 0x2e, 0x34, 0x63, 0x0c, 0x7e, 0x23, 0x94, 0x02, 0x0e, 0x32, 0x9f, 0x08, 0x3a, 0xf2, 0x83, 0x81, - 0x93, 0x19, 0xe3, 0x5d, 0xd6, 0x84, 0x03, 0x3d, 0x94, 0x52, 0x7d, 0x01, 0x9b, 0x21, 0x04, 0xe8, 0xaf, 0xc4, 0x7b, - 0x67, 0xad, 0x9e, 0x51, 0x89, 0x17, 0x13, 0x39, 0x28, 0xc2, 0x84, 0x87, 0x48, 0x8d, 0x28, 0x74, 0x24, 0xda, 0x43, - 0x05, 0xb3, 0xec, 0x6c, 0x5b, 0x53, 0xde, 0x17, 0x75, 0xea, 0x34, 0x07, 0xbf, 0xbe, 0x17, 0x6f, 0xe4, 0xea, 0x31, - 0xa0, 0xc7, 0xbe, 0x6e, 0x09, 0xd9, 0xbd, 0x53, 0x40, 0x80, 0x7c, 0xb1, 0x43, 0xc6, 0x84, 0xe8, 0x58, 0xd3, 0x92, - 0xaa, 0xd9, 0x47, 0x8b, 0xd0, 0x5f, 0xae, 0x8f, 0x33, 0x2c, 0x13, 0x42, 0x6d, 0x61, 0x02, 0x88, 0xd0, 0x93, 0x4e, - 0x09, 0x96, 0xe4, 0x3e, 0x78, 0xd9, 0xb0, 0xc3, 0xc1, 0x76, 0x55, 0x0c, 0x4f, 0x0e, 0x3f, 0x0f, 0x81, 0xed, 0x98, - 0x80, 0x4e, 0xb3, 0x14, 0x0a, 0xb1, 0xe1, 0x3e, 0x56, 0x33, 0x49, 0x05, 0x31, 0x4d, 0x54, 0x3e, 0xe0, 0x0f, 0x6a, - 0x23, 0x6e, 0xd2, 0x8e, 0xe2, 0xdd, 0x08, 0x7b, 0x89, 0x43, 0x37, 0x84, 0xeb, 0x80, 0xa8, 0xd1, 0x3e, 0x97, 0x35, - 0x37, 0xa2, 0xcd, 0x33, 0x32, 0x70, 0x89, 0xd4, 0x5f, 0xa1, 0x75, 0x50, 0x69, 0x41, 0x3d, 0x8b, 0xdf, 0x02, 0x3c, - 0xb7, 0x86, 0x96, 0xfb, 0x15, 0x92, 0xb8, 0x53, 0x8c, 0x66, 0xb4, 0x47, 0x78, 0x99, 0xee, 0x10, 0xe0, 0xde, 0x6a, - 0xdd, 0x69, 0xba, 0x1e, 0xa0, 0x8c, 0x9d, 0xb8, 0xe9, 0xb6, 0xca, 0x49, 0x1a, 0x03, 0x6a, 0xcc, 0xbf, 0x47, 0xef, - 0x07, 0xdf, 0x73, 0xa4, 0xe3, 0x76, 0x59, 0xe8, 0x5a, 0xa1, 0xf9, 0xf4, 0x39, 0xd9, 0x69, 0xe9, 0x16, 0xf7, 0x26, - 0x11, 0xea, 0xf0, 0x11, 0x5e, 0x30, 0x17, 0x4a, 0x77, 0x5c, 0xd4, 0xbd, 0xf9, 0xc9, 0xc2, 0x4d, 0x51, 0x53, 0x98, - 0x42, 0xc9, 0x66, 0xc3, 0x2b, 0x89, 0x59, 0xa0, 0xb9, 0x5c, 0x29, 0x84, 0x96, 0xf7, 0x40, 0xed, 0x35, 0x24, 0x6e, - 0xad, 0xd7, 0x16, 0x6e, 0xd3, 0x45, 0x6b, 0x35, 0x59, 0xd0, 0xc6, 0x48, 0xca, 0x98, 0x39, 0x74, 0x56, 0x64, 0xba, - 0x2a, 0x0a, 0x86, 0x35, 0xb5, 0x5e, 0x5d, 0x3b, 0x7e, 0x7e, 0x69, 0x7a, 0x08, 0x13, 0x0e, 0xac, 0x56, 0xd0, 0x3b, - 0xec, 0x34, 0x7f, 0x7a, 0xe1, 0x6a, 0x0b, 0x83, 0x44, 0x01, 0x01, 0x5d, 0x72, 0xf4, 0x3e, 0x96, 0x9e, 0xa2, 0x20, - 0x52, 0xa7, 0xab, 0xae, 0x32, 0x12, 0x82, 0x95, 0x8a, 0xff, 0x76, 0x60, 0x42, 0x8e, 0x70, 0x8e, 0xc8, 0xed, 0x75, - 0x25, 0xe7, 0xc7, 0x03, 0xd2, 0xd3, 0x11, 0x91, 0xd0, 0xd3, 0x1b, 0x43, 0x70, 0x39, 0x68, 0xec, 0xef, 0x02, 0xae, - 0xf0, 0x01, 0x86, 0xaf, 0x87, 0xae, 0x94, 0x1b, 0x2c, 0xec, 0xfb, 0x02, 0x69, 0xca, 0x45, 0x04, 0x07, 0xaa, 0x1d, - 0xf9, 0x9c, 0x1d, 0xf9, 0xad, 0x79, 0x15, 0x38, 0x37, 0x27, 0xbb, 0x46, 0x11, 0xca, 0x14, 0xbb, 0xb7, 0x03, 0x23, - 0x51, 0x92, 0xf0, 0xbb, 0x43, 0x42, 0x6b, 0xad, 0xf3, 0x3b, 0xee, 0x07, 0xbc, 0x28, 0x22, 0xf9, 0x07, 0xb1, 0x79, - 0x4f, 0x93, 0xf3, 0xf2, 0x1a, 0x5b, 0xb7, 0x15, 0x23, 0x00, 0xcd, 0x4d, 0xe6, 0x6d, 0x95, 0xc1, 0x0d, 0x56, 0x06, - 0x79, 0xb2, 0x24, 0x18, 0xcf, 0x52, 0x0d, 0xe6, 0xd9, 0xb1, 0x93, 0x96, 0x05, 0x0b, 0x81, 0x22, 0xd7, 0xd4, 0x66, - 0x75, 0x12, 0x57, 0xb4, 0x63, 0xf7, 0x5b, 0xb6, 0xc0, 0x09, 0x48, 0x3d, 0x71, 0xb2, 0xb4, 0x0d, 0x3e, 0x50, 0x48, - 0x76, 0x67, 0x19, 0x06, 0xd9, 0x85, 0xff, 0x2a, 0xe8, 0x07, 0x54, 0x57, 0x21, 0x54, 0xa4, 0x4d, 0x6c, 0x95, 0x94, - 0x22, 0x6b, 0x84, 0xd6, 0xd9, 0x16, 0x64, 0xc5, 0xd9, 0x1e, 0xf1, 0xa8, 0x99, 0xc0, 0x83, 0xc9, 0x6d, 0x91, 0xcd, - 0x19, 0xee, 0x89, 0x40, 0xc7, 0xa6, 0x50, 0x66, 0xda, 0x84, 0x6d, 0xdc, 0x93, 0xcd, 0xda, 0xbb, 0xad, 0xa8, 0x19, - 0x34, 0xa2, 0x6f, 0x69, 0x9a, 0xfd, 0x7b, 0x7d, 0xf0, 0x59, 0x89, 0xbe, 0x91, 0x43, 0x4c, 0xd7, 0x6d, 0xa4, 0x49, - 0x95, 0x5a, 0xe2, 0xd7, 0x6d, 0x3e, 0xbd, 0xa7, 0xa1, 0x1c, 0x52, 0x00, 0x27, 0x94, 0x02, 0x33, 0xe4, 0x73, 0x0c, - 0xc1, 0x9d, 0x82, 0x6d, 0x24, 0xcb, 0xad, 0xc8, 0x65, 0xd6, 0x58, 0xdd, 0xf1, 0x0f, 0x16, 0x80, 0x42, 0x5f, 0xde, - 0xa1, 0xa0, 0x1f, 0x6b, 0xbd, 0x4f, 0xd4, 0x91, 0x12, 0x93, 0xe2, 0xd3, 0xa5, 0x9b, 0xac, 0x02, 0x6a, 0xae, 0x5e, - 0x17, 0x0d, 0xe8, 0x35, 0x61, 0x00, 0xa1, 0x47, 0x74, 0xd8, 0x42, 0x18, 0xfd, 0xd1, 0x14, 0xc2, 0x7a, 0x5f, 0xc5, - 0x6d, 0xb7, 0x29, 0xba, 0xa7, 0xb3, 0x3b, 0x46, 0x6a, 0x90, 0x99, 0x56, 0x34, 0xc7, 0x70, 0x7a, 0xc0, 0x9d, 0xe2, - 0xb1, 0x63, 0x81, 0xcd, 0x26, 0xd5, 0x63, 0x8c, 0x01, 0x8e, 0xcc, 0x58, 0x6c, 0x53, 0x69, 0xad, 0x8c, 0x91, 0xda, - 0x16, 0xfd, 0xb2, 0xe6, 0x4e, 0x51, 0xdc, 0xfe, 0x08, 0x80, 0x79, 0x6e, 0x32, 0xad, 0xa3, 0x98, 0xa2, 0x46, 0x49, - 0xdb, 0xc5, 0xf1, 0x52, 0x54, 0x5e, 0x7c, 0x22, 0x70, 0x6f, 0x84, 0xca, 0x95, 0xa3, 0x03, 0xab, 0x33, 0xa5, 0x1f, - 0x6e, 0xf1, 0x98, 0x39, 0x89, 0x78, 0x01, 0xa3, 0xcf, 0x98, 0x0d, 0xe7, 0x0b, 0xef, 0x88, 0xb9, 0x45, 0x4e, 0xe1, - 0x5d, 0xf1, 0x56, 0xf8, 0xa5, 0x0b, 0xa8, 0xee, 0x41, 0x9c, 0xee, 0x54, 0xd6, 0x7a, 0x9d, 0x11, 0x21, 0xe1, 0x3b, - 0x92, 0xec, 0x95, 0x8c, 0x9d, 0xf8, 0x2c, 0x32, 0x3d, 0x38, 0x86, 0x85, 0x67, 0x8c, 0xe4, 0xf6, 0x99, 0x3a, 0x9a, - 0xb3, 0xc7, 0x3a, 0xd7, 0x45, 0x77, 0x5e, 0x7b, 0x6f, 0x2b, 0xd2, 0x53, 0x33, 0x9b, 0x4e, 0xbc, 0x69, 0x80, 0x3a, - 0x1f, 0xbc, 0xf6, 0x48, 0xe7, 0x7c, 0x07, 0x47, 0x71, 0x28, 0x5c, 0xb7, 0x6a, 0xf4, 0xd9, 0xf5, 0x1e, 0x72, 0x35, - 0x6c, 0xba, 0x8b, 0xc7, 0x65, 0x8f, 0x26, 0x7f, 0xb1, 0x22, 0x10, 0xfb, 0x18, 0x1e, 0x9f, 0xd3, 0xe0, 0xd6, 0xda, - 0xce, 0xb4, 0xd5, 0x36, 0x02, 0xd5, 0xa6, 0xa9, 0x05, 0x7e, 0xd2, 0xd5, 0x71, 0x3e, 0x71, 0xbc, 0xbc, 0x6b, 0xe0, - 0x4b, 0xfc, 0x02, 0x84, 0x55, 0xe9, 0xf5, 0xee, 0xf1, 0x1d, 0x67, 0x99, 0x2d, 0x73, 0xaf, 0x01, 0x59, 0x0e, 0x73, - 0x9d, 0xc5, 0xf1, 0xae, 0x3a, 0x22, 0x95, 0xda, 0xbe, 0xf2, 0xbf, 0x33, 0xe3, 0x42, 0x97, 0x1d, 0x41, 0x1c, 0xc8, - 0x15, 0x39, 0x53, 0x2a, 0xac, 0xc2, 0x69, 0x69, 0x4d, 0x43, 0xe3, 0x3a, 0x14, 0x04, 0x64, 0xf8, 0x7f, 0x20, 0x1c, - 0x44, 0xe6, 0xad, 0x13, 0x92, 0xaa, 0x6a, 0x03, 0x6b, 0xb4, 0x17, 0x7b, 0x01, 0x52, 0x78, 0x28, 0x92, 0xad, 0x2f, - 0xda, 0xaf, 0x4b, 0x64, 0x21, 0x03, 0xc1, 0x28, 0x93, 0x24, 0xc0, 0xd6, 0xd1, 0xad, 0x9e, 0xee, 0x92, 0x5e, 0x26, - 0xa0, 0xef, 0xf4, 0x3c, 0xfe, 0x10, 0x87, 0xa2, 0xac, 0x39, 0x7f, 0x6e, 0x49, 0xb6, 0xf3, 0xe8, 0xae, 0x6a, 0xac, - 0x43, 0x2c, 0x36, 0x97, 0x1c, 0x1d, 0xe7, 0x45, 0x81, 0xb3, 0x0c, 0x7d, 0x07, 0x8c, 0x85, 0x77, 0x36, 0x0c, 0xd5, - 0x5c, 0x48, 0x35, 0x7d, 0xc5, 0xa7, 0x70, 0x1d, 0x1e, 0x52, 0x4a, 0xdb, 0x16, 0xeb, 0xc1, 0x72, 0x88, 0x97, 0xdc, - 0x50, 0xa1, 0x71, 0xb5, 0x34, 0x9b, 0xd4, 0x73, 0x98, 0xc6, 0x8a, 0x2f, 0xd0, 0xa4, 0xdc, 0x45, 0xc5, 0x53, 0x07, - 0x53, 0x87, 0x6e, 0x52, 0x88, 0x7e, 0x3a, 0x32, 0x27, 0x92, 0x34, 0xe9, 0xc7, 0xb6, 0x51, 0x05, 0x01, 0xd0, 0xd1, - 0x5a, 0x16, 0xb4, 0xfb, 0xde, 0xaf, 0x7e, 0x65, 0xa3, 0x4d, 0x8f, 0x1a, 0x92, 0xb9, 0xd6, 0xe1, 0xd6, 0xd7, 0x72, - 0x7c, 0x77, 0x45, 0x18, 0xcd, 0xdb, 0x03, 0xab, 0xc2, 0xb9, 0x88, 0x14, 0xe3, 0x16, 0xad, 0x20, 0x61, 0x1e, 0x20, - 0xc7, 0xbb, 0x61, 0xda, 0x9f, 0x2b, 0x93, 0xa3, 0xf3, 0x3c, 0x3c, 0x6f, 0xae, 0x58, 0x68, 0x45, 0x2f, 0xce, 0xb1, - 0xdf, 0xb8, 0xf7, 0xbd, 0x8c, 0xa9, 0xe7, 0x61, 0xe3, 0xdd, 0x28, 0x4f, 0x4f, 0x30, 0x3a, 0x3f, 0x44, 0x37, 0x77, - 0xa4, 0xaf, 0xec, 0x02, 0xf4, 0x7a, 0x4f, 0x8e, 0xef, 0x67, 0xdf, 0x8b, 0x8e, 0x33, 0xb8, 0x30, 0xd2, 0x28, 0xce, - 0x17, 0x84, 0x96, 0x98, 0xa3, 0x34, 0xe3, 0x45, 0x3d, 0x11, 0x8e, 0x78, 0x79, 0x0a, 0x76, 0x2c, 0xec, 0xa6, 0x3f, - 0x17, 0xbe, 0xb0, 0xf0, 0xd7, 0xe9, 0x4c, 0xe0, 0x71, 0xff, 0x6f, 0x93, 0xfd, 0x82, 0x44, 0x22, 0x7a, 0x18, 0x47, - 0x7a, 0x6c, 0xcb, 0x42, 0xef, 0x99, 0xd8, 0x22, 0xf5, 0xfa, 0xfd, 0x84, 0x50, 0xea, 0x86, 0x52, 0xea, 0x0e, 0xca, - 0x64, 0x59, 0x02, 0x1b, 0x37, 0x85, 0x10, 0x72, 0xfc, 0x33, 0x6e, 0x9e, 0x22, 0x7c, 0xd6, 0x88, 0xd2, 0xac, 0xa6, - 0xa6, 0xe0, 0xce, 0x60, 0x00, 0x56, 0xe2, 0x04, 0x07, 0x88, 0xf2, 0xa1, 0x2e, 0xbc, 0xc2, 0xc4, 0x6a, 0x55, 0x56, - 0x02, 0xb5, 0xc2, 0x50, 0x82, 0x08, 0x4e, 0x68, 0x2f, 0x22, 0xac, 0xeb, 0x98, 0x94, 0x7b, 0xb0, 0x45, 0x3b, 0xb7, - 0xf0, 0xba, 0xdb, 0xc2, 0xca, 0xc3, 0x7b, 0x35, 0xeb, 0xb1, 0x2b, 0xbb, 0xe3, 0x01, 0x72, 0x94, 0x9c, 0xfd, 0x04, - 0x88, 0xe0, 0x41, 0x12, 0xd8, 0xea, 0xad, 0x3d, 0x6c, 0xed, 0x0e, 0xa1, 0xdf, 0x16, 0xf8, 0x74, 0x07, 0xcc, 0x46, - 0x69, 0x37, 0xfb, 0xfc, 0xa7, 0x0e, 0x0e, 0x4b, 0x6f, 0x02, 0x7c, 0x9d, 0xea, 0x4e, 0xd6, 0x74, 0x1b, 0xb8, 0xc7, - 0x3f, 0x7b, 0xe8, 0x8a, 0x44, 0x3a, 0x62, 0x16, 0xb7, 0x38, 0xaa, 0xcb, 0xce, 0xea, 0xae, 0x91, 0x73, 0x5b, 0x12, - 0x57, 0xa5, 0x84, 0xec, 0x72, 0x44, 0xa5, 0x24, 0x7b, 0x44, 0x19, 0x9c, 0xf6, 0xf6, 0xf2, 0xdc, 0x98, 0x7b, 0x18, - 0x67, 0x01, 0xa8, 0x09, 0x58, 0x2e, 0xc8, 0xc6, 0x3b, 0x01, 0x80, 0x51, 0x5a, 0x35, 0x75, 0x46, 0xef, 0xe2, 0x56, - 0x5d, 0x6f, 0x1e, 0x64, 0x46, 0x33, 0x51, 0x33, 0xb9, 0x3b, 0xa0, 0x8a, 0xd1, 0xc2, 0x20, 0xfb, 0xa5, 0x54, 0x7c, - 0x5a, 0x95, 0x68, 0xa5, 0x43, 0xcd, 0x68, 0xbf, 0xfb, 0x34, 0xd0, 0x51, 0x22, 0xb6, 0x5c, 0x4c, 0xbb, 0xf2, 0x76, - 0x98, 0x78, 0x1a, 0xd8, 0x2a, 0x33, 0x22, 0x4d, 0xd9, 0xc2, 0xcb, 0x11, 0x49, 0x7e, 0xd4, 0x5d, 0xb3, 0x6a, 0x52, - 0x1b, 0x67, 0x2d, 0x3c, 0xdd, 0x52, 0xe4, 0x14, 0x0a, 0x2e, 0xda, 0xee, 0x83, 0x0c, 0x82, 0x69, 0xd3, 0xc6, 0x59, - 0x6f, 0xbb, 0x2f, 0xa2, 0x8e, 0x57, 0x74, 0x5c, 0x24, 0x6c, 0x6e, 0xf7, 0x14, 0x65, 0x07, 0x89, 0xf2, 0x24, 0x26, - 0xd3, 0x91, 0x03, 0x95, 0xb4, 0xa1, 0xd4, 0xd2, 0x7b, 0xc9, 0x8a, 0x8b, 0xb8, 0xf8, 0xbf, 0xca, 0xb2, 0xad, 0x2f, - 0xbe, 0x48, 0xb0, 0xa0, 0x83, 0x39, 0xa2, 0xc0, 0x3c, 0x97, 0xd2, 0x41, 0x89, 0x44, 0x11, 0xf9, 0x49, 0xc1, 0xec, - 0xaa, 0x64, 0x0d, 0x3e, 0x68, 0xa5, 0x3b, 0x93, 0x49, 0x43, 0x22, 0xe5, 0x9a, 0xd4, 0xda, 0x16, 0x1b, 0x19, 0xf1, - 0xcc, 0x0f, 0x56, 0x89, 0x30, 0x12, 0x0f, 0x32, 0x25, 0x96, 0xc2, 0xb3, 0x85, 0x94, 0xf8, 0x22, 0x67, 0x9f, 0xeb, - 0xc5, 0x6e, 0xb4, 0xc8, 0x62, 0x7e, 0x98, 0xf9, 0xe5, 0x70, 0xb3, 0x5b, 0x11, 0xa3, 0xde, 0x9a, 0xb3, 0x3c, 0x2b, - 0x99, 0x8d, 0x6b, 0x47, 0x8e, 0xb9, 0x04, 0x1a, 0x29, 0x04, 0x0a, 0xe9, 0x93, 0x81, 0x53, 0x8b, 0xcb, 0x81, 0x0d, - 0x1a, 0xdf, 0xf1, 0xc9, 0xb3, 0xa5, 0xbb, 0x8b, 0xa1, 0x61, 0xdb, 0x21, 0x11, 0x44, 0x68, 0xbc, 0x21, 0xd1, 0x32, - 0x34, 0x3f, 0x78, 0x3a, 0xef, 0xcc, 0xf4, 0xea, 0x8e, 0xa4, 0x67, 0x69, 0xe1, 0x11, 0xe0, 0x7c, 0x52, 0x91, 0xe6, - 0xd6, 0x3e, 0xc9, 0x21, 0xeb, 0xfe, 0x45, 0xb3, 0x7e, 0x45, 0x00, 0x77, 0x92, 0x80, 0x10, 0xa0, 0xe1, 0x75, 0x6b, - 0x21, 0x8c, 0x25, 0xcc, 0x38, 0x84, 0xee, 0x2a, 0xf8, 0x6f, 0x12, 0xa6, 0xe7, 0xa5, 0x09, 0x8d, 0x2f, 0x4a, 0xc5, - 0x60, 0x27, 0x0b, 0x84, 0x5b, 0x40, 0x14, 0x04, 0x81, 0x20, 0x63, 0x16, 0x8a, 0xa9, 0x84, 0x76, 0xa0, 0x15, 0x14, - 0x48, 0x80, 0x89, 0x37, 0x4e, 0x85, 0x41, 0x55, 0x6d, 0xc5, 0xd3, 0x1c, 0xb3, 0xe1, 0xa4, 0x61, 0x50, 0x58, 0x7f, - 0x02, 0x5d, 0x9c, 0x62, 0x92, 0x0c, 0xfb, 0xbb, 0x78, 0xe3, 0xc9, 0xba, 0x9d, 0x89, 0x52, 0x29, 0xf6, 0x59, 0x93, - 0x27, 0x34, 0xc3, 0x79, 0x21, 0xea, 0xcb, 0xba, 0xb4, 0xee, 0x83, 0xd5, 0x74, 0x07, 0x4f, 0x9e, 0x75, 0xa4, 0xb7, - 0x71, 0x60, 0xb9, 0x83, 0x44, 0x80, 0x45, 0xda, 0x03, 0xcd, 0xc8, 0x32, 0x64, 0xa8, 0x02, 0xac, 0x35, 0xe6, 0x4e, - 0x0d, 0x6d, 0x0f, 0xe5, 0x46, 0x72, 0x6d, 0x12, 0xac, 0x1e, 0x96, 0xbe, 0xbc, 0xba, 0x6e, 0x73, 0x63, 0x00, 0xbb, - 0xef, 0xd8, 0xb2, 0xa0, 0xcb, 0x11, 0x19, 0x8e, 0x27, 0xb7, 0x09, 0xd5, 0x03, 0x14, 0x8a, 0x6a, 0xaa, 0x69, 0xed, - 0x1f, 0x7e, 0xe6, 0x9d, 0x38, 0xd4, 0x39, 0x21, 0x36, 0x2a, 0x8f, 0xd0, 0x31, 0x8f, 0x7d, 0xa2, 0xcf, 0x25, 0xef, - 0x69, 0xbe, 0x41, 0xea, 0xc8, 0xc5, 0xd5, 0x79, 0x92, 0xa8, 0x1b, 0x63, 0xb5, 0x15, 0x5b, 0x84, 0x01, 0x16, 0x73, - 0x0c, 0x87, 0xe8, 0x54, 0x70, 0xb4, 0x24, 0xbd, 0x8d, 0xa5, 0xea, 0xe5, 0xf6, 0xdb, 0xaa, 0x4b, 0x6b, 0xa7, 0x09, - 0x83, 0xe8, 0xf4, 0x90, 0x01, 0x07, 0x42, 0xc6, 0xda, 0x3e, 0x58, 0xc6, 0x71, 0x46, 0xeb, 0x32, 0x68, 0x04, 0x63, - 0xe8, 0x00, 0xe5, 0xac, 0x7a, 0x1c, 0xec, 0x04, 0x62, 0x79, 0x48, 0x6f, 0x9a, 0xcc, 0x00, 0xd9, 0x22, 0x97, 0x5f, - 0x6a, 0xa2, 0x9d, 0x85, 0x8e, 0x2d, 0xfb, 0x1e, 0xd0, 0xae, 0x03, 0x47, 0x5f, 0x07, 0x1c, 0x75, 0xe2, 0x45, 0x2d, - 0x85, 0x36, 0xc7, 0xc0, 0xb9, 0xb0, 0x38, 0xd5, 0xf3, 0x6c, 0x68, 0xc7, 0xbd, 0x03, 0xbc, 0x98, 0xd2, 0xf5, 0x02, - 0xfc, 0x76, 0x70, 0x19, 0xf8, 0xc4, 0x83, 0xdb, 0xea, 0xb0, 0x63, 0x67, 0x92, 0xc6, 0x79, 0x34, 0x75, 0x73, 0xce, - 0xb9, 0xd0, 0xe5, 0xdc, 0xff, 0x9e, 0x6e, 0xfd, 0xfe, 0x8a, 0xf1, 0x69, 0xad, 0x3d, 0x61, 0xb9, 0xca, 0x69, 0x97, - 0x45, 0x2c, 0x59, 0x71, 0x8e, 0xbe, 0x10, 0xc8, 0xd7, 0xeb, 0xfc, 0x7e, 0xa1, 0x41, 0xe7, 0xd4, 0x41, 0x74, 0x8e, - 0x71, 0xb2, 0xd3, 0x83, 0xc9, 0x7b, 0x65, 0x71, 0x68, 0xac, 0x52, 0x66, 0xf1, 0x7d, 0xe3, 0x96, 0xde, 0x9e, 0x50, - 0x76, 0x29, 0x85, 0x14, 0xca, 0xb2, 0xe1, 0xb6, 0xc7, 0x81, 0xa6, 0xed, 0x36, 0x92, 0xdd, 0xd6, 0xb7, 0xef, 0x34, - 0x89, 0x48, 0xd2, 0xdd, 0x05, 0x51, 0x78, 0x86, 0xd0, 0x18, 0x50, 0xb0, 0x37, 0xa7, 0xd6, 0xe5, 0x6b, 0x2f, 0x2b, - 0xf1, 0x8a, 0x78, 0x57, 0x0c, 0xc6, 0xca, 0x09, 0x1d, 0x2c, 0xd2, 0x34, 0x50, 0xc7, 0x4e, 0x92, 0xb8, 0x55, 0x49, - 0xfc, 0xd0, 0xf2, 0x2f, 0xa4, 0xb9, 0x51, 0x79, 0x2a, 0xe2, 0xeb, 0x10, 0x7d, 0xe6, 0xb8, 0x54, 0xf7, 0x46, 0x35, - 0x83, 0x72, 0xcc, 0x93, 0x79, 0xc3, 0x5c, 0xa6, 0xdb, 0x29, 0x32, 0x4f, 0xf6, 0xbc, 0xb9, 0x99, 0x51, 0xa2, 0x44, - 0xa4, 0x2e, 0xf4, 0x32, 0xd7, 0x56, 0xa1, 0x23, 0x8d, 0xd8, 0xb4, 0x56, 0xb3, 0x89, 0x3d, 0x0e, 0x67, 0x3f, 0x59, - 0xd9, 0x13, 0xbc, 0xeb, 0x3d, 0xef, 0xec, 0xc3, 0xe6, 0x82, 0xeb, 0xd0, 0x88, 0x21, 0x33, 0x60, 0xa6, 0x59, 0x3a, - 0x53, 0x20, 0x8b, 0xb8, 0xaf, 0xee, 0x48, 0x94, 0x31, 0xfd, 0x13, 0xab, 0x75, 0x7d, 0xad, 0x54, 0x1d, 0x93, 0x1f, - 0xbe, 0xa3, 0x6b, 0x38, 0x77, 0x50, 0x94, 0x18, 0x4e, 0x34, 0xed, 0xe6, 0x52, 0x03, 0x10, 0x7e, 0x67, 0x87, 0x51, - 0x58, 0xc1, 0xb6, 0xa8, 0xb7, 0xea, 0x2a, 0x60, 0xa0, 0x86, 0x79, 0x32, 0xf6, 0x46, 0x11, 0x19, 0xf5, 0x1b, 0x76, - 0x23, 0xaf, 0x2c, 0xba, 0x65, 0x8d, 0xcf, 0x56, 0x39, 0xa5, 0xfe, 0x48, 0x6a, 0xe5, 0x0e, 0x0b, 0xe4, 0x86, 0xeb, - 0x42, 0x21, 0xa9, 0x37, 0x1c, 0x9b, 0x6d, 0x6b, 0xe6, 0x99, 0x06, 0xba, 0x6d, 0x4d, 0xef, 0x13, 0x3b, 0x70, 0xc3, - 0x6d, 0xdd, 0x30, 0x55, 0x6d, 0x3b, 0x8f, 0x5f, 0xef, 0xd3, 0x22, 0xac, 0x09, 0x6d, 0x18, 0xc6, 0x1a, 0xd8, 0xb6, - 0x45, 0x31, 0x17, 0x83, 0x98, 0xf6, 0xd8, 0x62, 0xdf, 0x81, 0x6c, 0xdf, 0xfd, 0xb5, 0x4a, 0x42, 0x4e, 0xae, 0xd2, - 0xf9, 0x35, 0xf9, 0x49, 0x27, 0x8b, 0x44, 0x66, 0x76, 0x91, 0xbf, 0xc6, 0x95, 0xfd, 0xa3, 0x95, 0xdd, 0xab, 0x95, - 0x2e, 0x52, 0x40, 0x14, 0xa6, 0xc2, 0x73, 0x08, 0x4c, 0x97, 0xac, 0xfc, 0x1f, 0xe8, 0x38, 0x27, 0x63, 0x4a, 0x68, - 0x6f, 0x34, 0x9a, 0x40, 0x37, 0x24, 0x14, 0x43, 0x0b, 0xcb, 0xe9, 0x79, 0xa9, 0x41, 0x57, 0x3b, 0x5c, 0x47, 0x96, - 0xfb, 0x22, 0xc0, 0x4f, 0x14, 0x50, 0x67, 0x6a, 0x82, 0xdd, 0x4f, 0x02, 0x63, 0x69, 0xd6, 0x59, 0xfa, 0x45, 0x87, - 0xd3, 0x15, 0x75, 0xf7, 0xf4, 0x2b, 0x86, 0x44, 0x77, 0xf8, 0x95, 0x42, 0xeb, 0x13, 0x33, 0x73, 0x81, 0x46, 0x3a, - 0x6e, 0x60, 0x10, 0x2e, 0x6a, 0x0b, 0xfe, 0x80, 0x5c, 0xc5, 0x4d, 0xe1, 0x6e, 0x72, 0x00, 0x97, 0xca, 0x6d, 0xcb, - 0xb3, 0x4a, 0x13, 0x98, 0x7d, 0x92, 0x32, 0x3a, 0x71, 0x8c, 0xba, 0x6f, 0x77, 0x3f, 0x4a, 0x52, 0xde, 0x3e, 0x7d, - 0xf3, 0x7a, 0x95, 0x35, 0xca, 0xde, 0x33, 0xb3, 0xd4, 0x55, 0xfc, 0xa9, 0x49, 0xee, 0x6a, 0xee, 0x3b, 0xe9, 0x56, - 0x20, 0x30, 0xca, 0x79, 0x85, 0xe5, 0xce, 0xb2, 0x90, 0xc3, 0xe6, 0x5e, 0xba, 0x4f, 0x4b, 0x9a, 0xec, 0x44, 0x55, - 0x62, 0x8c, 0x49, 0xa1, 0xfd, 0xf2, 0x74, 0xee, 0x8f, 0x0e, 0xdf, 0xa3, 0xa3, 0xbe, 0x4b, 0xd3, 0x72, 0xda, 0x8a, - 0xed, 0xf2, 0xc4, 0x0e, 0xa6, 0xe1, 0x9a, 0x30, 0x2d, 0x03, 0x84, 0xee, 0xea, 0x03, 0xe8, 0x5f, 0xe2, 0x1f, 0xfa, - 0xf1, 0x9c, 0xa2, 0x0f, 0xd0, 0x83, 0xd9, 0x9a, 0xca, 0x25, 0x6a, 0x50, 0x22, 0xb2, 0x4d, 0xbb, 0x34, 0x01, 0x53, - 0xe4, 0x20, 0xdd, 0x42, 0x06, 0xa2, 0x64, 0x21, 0x98, 0x41, 0xe5, 0x17, 0xf1, 0x3a, 0xf1, 0x75, 0xbe, 0x5a, 0xf0, - 0x92, 0x9e, 0x70, 0x55, 0xc8, 0xd5, 0x0d, 0xa3, 0xc5, 0xbc, 0x3a, 0xed, 0xa4, 0xda, 0x38, 0x34, 0xa8, 0x51, 0x87, - 0x48, 0xd7, 0xf1, 0xfe, 0x6f, 0x36, 0x52, 0x37, 0x18, 0xfd, 0xe4, 0x24, 0xe0, 0xfb, 0xc6, 0x48, 0xa5, 0xb3, 0x87, - 0x38, 0xb5, 0x16, 0x3c, 0x5e, 0x28, 0x7b, 0x34, 0xea, 0x11, 0xb5, 0xc6, 0x5e, 0x0e, 0x32, 0xad, 0x0d, 0x27, 0x85, - 0xd2, 0x79, 0xb8, 0x94, 0x77, 0x49, 0xe1, 0xd2, 0x1b, 0x95, 0x88, 0xf2, 0x00, 0x76, 0xc2, 0x96, 0x8a, 0x1b, 0x95, - 0xb4, 0x80, 0xea, 0x99, 0x9e, 0x0c, 0x89, 0xe9, 0x9c, 0x44, 0x8c, 0x19, 0x9e, 0xd2, 0xcd, 0x38, 0x44, 0x6b, 0x68, - 0x86, 0x3d, 0xbd, 0x8f, 0xd5, 0x13, 0xe4, 0x80, 0x9d, 0x8d, 0xeb, 0x0c, 0x21, 0x76, 0x52, 0xe1, 0x67, 0x6a, 0x55, - 0x6c, 0x99, 0x7f, 0x24, 0xa8, 0x6d, 0xf3, 0xb6, 0x8f, 0x88, 0xf2, 0xd6, 0xd2, 0x37, 0xb9, 0xbf, 0xe2, 0xca, 0x78, - 0x25, 0x81, 0x66, 0x96, 0x97, 0xfc, 0x1c, 0xe6, 0x67, 0xbf, 0xb1, 0x03, 0x13, 0x88, 0x38, 0xdb, 0x68, 0xd4, 0x53, - 0x72, 0x34, 0xd7, 0x39, 0xef, 0x5b, 0x70, 0x46, 0xc9, 0x34, 0x10, 0x62, 0x2d, 0x8b, 0x04, 0xe2, 0xc8, 0x24, 0x71, - 0x56, 0x38, 0xeb, 0x68, 0x27, 0x8f, 0x0e, 0x7a, 0x6f, 0x22, 0xf7, 0x45, 0x4d, 0x7a, 0x06, 0xfe, 0xd8, 0x51, 0x63, - 0x31, 0x8a, 0x6e, 0x5e, 0x04, 0xea, 0xe6, 0x2c, 0x8f, 0x43, 0xbd, 0xf4, 0x66, 0xae, 0xfd, 0xd2, 0xd3, 0x5a, 0xaa, - 0x0b, 0x74, 0x71, 0xe8, 0x31, 0x6a, 0x51, 0x5e, 0x41, 0x1a, 0x4c, 0x7b, 0xa0, 0xec, 0x35, 0x4c, 0xe8, 0x01, 0xbf, - 0x54, 0x82, 0x8c, 0x06, 0xef, 0x7c, 0xb4, 0xc5, 0xc5, 0x74, 0x92, 0xf3, 0x66, 0x01, 0x05, 0xb7, 0xeb, 0x2d, 0xa9, - 0x89, 0xd0, 0x1a, 0x37, 0x28, 0x6c, 0x91, 0xf0, 0x4f, 0x34, 0x87, 0xb3, 0x2b, 0x24, 0x75, 0x88, 0x4d, 0x31, 0xc2, - 0x04, 0xb4, 0x66, 0x5c, 0x6c, 0x68, 0x61, 0x37, 0xd1, 0x03, 0x6b, 0xf3, 0x20, 0x19, 0x87, 0x3b, 0x9a, 0x69, 0x33, - 0x90, 0x6b, 0x09, 0xfe, 0x2c, 0x11, 0xbd, 0xc3, 0xae, 0x0f, 0x77, 0x64, 0xd8, 0xdc, 0x12, 0xb2, 0x52, 0x66, 0x7a, - 0x78, 0x09, 0xb4, 0xeb, 0xb7, 0x8a, 0xed, 0x50, 0xc1, 0x9f, 0x22, 0x87, 0xa4, 0x98, 0x7e, 0x9f, 0xbd, 0x3a, 0x80, - 0x18, 0xc4, 0xa9, 0xd3, 0x7d, 0x53, 0x60, 0x9d, 0x43, 0x49, 0xb1, 0x21, 0x8c, 0x71, 0xc6, 0x51, 0xbb, 0xdb, 0xd1, - 0xc6, 0x7e, 0x24, 0x86, 0x40, 0xe9, 0xf0, 0xe5, 0x8e, 0x56, 0x5e, 0xb7, 0xb3, 0x75, 0xdb, 0x6b, 0xda, 0x91, 0x0f, - 0xc9, 0x11, 0x4c, 0x8a, 0x48, 0x5a, 0x36, 0x10, 0x9a, 0x31, 0x78, 0x8b, 0xb4, 0x60, 0x6d, 0xcf, 0x80, 0x96, 0xb9, - 0x5e, 0x28, 0xb4, 0xc0, 0xb3, 0x66, 0x0c, 0x4c, 0x0a, 0x8b, 0x0e, 0x2e, 0x69, 0xef, 0x74, 0x82, 0xd9, 0x40, 0xb5, - 0x5a, 0xdb, 0x6d, 0x59, 0xdf, 0x34, 0x0a, 0x84, 0xed, 0xbb, 0x72, 0x33, 0xfd, 0xc8, 0xcf, 0xac, 0x05, 0xf0, 0x55, - 0x6c, 0xbb, 0xce, 0x83, 0x76, 0x5f, 0x5b, 0xe5, 0xde, 0xc7, 0xfe, 0x5a, 0xe2, 0xc7, 0x50, 0xc3, 0xb2, 0x74, 0xc2, - 0x74, 0x65, 0x50, 0xbc, 0xe5, 0x9a, 0xfb, 0xc2, 0xc6, 0x64, 0x7a, 0x87, 0x12, 0x9b, 0xf8, 0xba, 0xb3, 0x1b, 0xcc, - 0x2d, 0x23, 0x7a, 0x59, 0xbf, 0xd3, 0xb7, 0xb2, 0xb5, 0xeb, 0x01, 0x88, 0xa9, 0x57, 0x96, 0x8c, 0x87, 0x19, 0x5d, - 0x3c, 0x04, 0xa5, 0xc9, 0x6b, 0x54, 0x92, 0xc1, 0x67, 0xf5, 0xae, 0x85, 0x44, 0xe4, 0xdb, 0x7a, 0x95, 0x27, 0xb3, - 0x91, 0x0d, 0xc7, 0xae, 0xa7, 0xe5, 0x81, 0x90, 0x32, 0x9a, 0xfd, 0x6d, 0x93, 0xd6, 0x5c, 0x4b, 0xc3, 0x2f, 0x91, - 0xe2, 0xa2, 0xd9, 0x38, 0xca, 0x36, 0x12, 0x04, 0x5d, 0xd5, 0x42, 0xb2, 0x58, 0x78, 0x58, 0xc8, 0x50, 0xbe, 0xac, - 0x84, 0x65, 0x2f, 0xee, 0xa6, 0x13, 0xf9, 0xe6, 0xc6, 0xcd, 0x8f, 0x42, 0xdb, 0xad, 0xaf, 0xdd, 0xf5, 0x43, 0x1b, - 0x51, 0x56, 0xbf, 0xe8, 0xc1, 0x57, 0xaa, 0xb1, 0x3e, 0xb2, 0xfe, 0xff, 0xa1, 0x9f, 0xd4, 0x89, 0xe4, 0xcc, 0x69, - 0x3b, 0x60, 0x7b, 0xa6, 0x80, 0xad, 0xd0, 0xf6, 0xb0, 0xa9, 0xf4, 0xfe, 0xce, 0x25, 0x81, 0x5c, 0x88, 0xf8, 0x84, - 0x85, 0x40, 0x92, 0xe2, 0x91, 0x4e, 0x03, 0x4c, 0x2d, 0x03, 0xea, 0x11, 0xec, 0xfb, 0xc1, 0x8e, 0x7c, 0xf3, 0x92, - 0xed, 0xf2, 0xb6, 0xee, 0x27, 0x28, 0xeb, 0x3e, 0x0f, 0x81, 0x8d, 0xdb, 0xd2, 0xe5, 0x80, 0x49, 0x1c, 0xc8, 0xc4, - 0x4c, 0xfb, 0x65, 0x6a, 0x2f, 0xdf, 0x2a, 0x67, 0x9c, 0xa0, 0x5b, 0x8a, 0x5a, 0x0e, 0x29, 0x71, 0x48, 0x5b, 0x79, - 0xe7, 0x86, 0xbd, 0x91, 0x7e, 0x88, 0x73, 0x8b, 0x1e, 0x07, 0x46, 0xd4, 0x69, 0xce, 0x66, 0x21, 0x42, 0x4d, 0xae, - 0x1a, 0xe5, 0xf1, 0x03, 0x57, 0xe9, 0x5a, 0xaa, 0xee, 0x2b, 0x94, 0xcc, 0x9e, 0x08, 0x93, 0x39, 0xb4, 0x3b, 0x64, - 0x49, 0x89, 0x66, 0x1f, 0xbb, 0x75, 0x40, 0x60, 0x27, 0x60, 0x9e, 0x96, 0xc8, 0xeb, 0x94, 0x4c, 0xfc, 0xf6, 0xed, - 0x3f, 0xca, 0xeb, 0x08, 0x86, 0xbd, 0xe4, 0x30, 0xa7, 0x02, 0x11, 0xc7, 0x71, 0xfe, 0xbc, 0x91, 0x0b, 0x12, 0x63, - 0xfd, 0xf9, 0x6b, 0xac, 0x5c, 0x23, 0x81, 0x56, 0x49, 0x43, 0xe1, 0x99, 0x1b, 0x67, 0xae, 0xec, 0xd4, 0xb3, 0x8c, - 0x2b, 0x76, 0x5b, 0xf4, 0x93, 0xc8, 0xa3, 0x16, 0xcd, 0x94, 0x1e, 0xa9, 0x72, 0x91, 0x94, 0xb4, 0xca, 0xa1, 0x96, - 0x6c, 0x05, 0xca, 0x61, 0x72, 0x2c, 0xe3, 0x3a, 0x73, 0x1a, 0x9a, 0xb3, 0x2c, 0x01, 0x72, 0x8b, 0x25, 0x38, 0xc7, - 0x4c, 0x2e, 0xc3, 0x4a, 0x2a, 0x24, 0x60, 0x1c, 0x84, 0xc2, 0x4f, 0xfe, 0xb1, 0xd2, 0xfe, 0x4e, 0x86, 0x5c, 0x62, - 0xf8, 0x0b, 0x1f, 0x93, 0x9e, 0x2b, 0x1f, 0x0a, 0x66, 0xb0, 0x18, 0xa2, 0xb7, 0x8c, 0x60, 0x5b, 0xee, 0xc4, 0x5b, - 0x34, 0xcb, 0xd2, 0xb9, 0x7f, 0x81, 0x66, 0xdd, 0xac, 0xd5, 0x7d, 0x8b, 0x22, 0xaf, 0x67, 0x4c, 0x9a, 0x70, 0xd2, - 0x4a, 0xa9, 0x94, 0xa6, 0xa0, 0x23, 0x4a, 0x32, 0x01, 0xcc, 0x0c, 0x50, 0xb2, 0x93, 0x8c, 0x4a, 0x0f, 0xca, 0xc9, - 0x70, 0x62, 0x31, 0xd3, 0xe8, 0x2c, 0x06, 0xac, 0x5e, 0x35, 0x3e, 0xce, 0x27, 0x1d, 0xff, 0x03, 0x40, 0xf5, 0xb5, - 0xd7, 0x82, 0xd7, 0x3c, 0x97, 0x93, 0xae, 0xe9, 0xba, 0x3a, 0xf6, 0x3f, 0xee, 0x45, 0x57, 0x50, 0x35, 0x66, 0xbf, - 0xd8, 0x5f, 0xd2, 0x38, 0x0c, 0x13, 0x62, 0x7c, 0x70, 0x1f, 0x70, 0xd4, 0x01, 0x5b, 0xac, 0x7a, 0x72, 0x91, 0x64, - 0x96, 0xa4, 0xb7, 0xb9, 0xe8, 0x3a, 0x7e, 0x70, 0x60, 0xa8, 0x2e, 0x6d, 0xba, 0xe7, 0x91, 0x15, 0x6f, 0x8d, 0x25, - 0xc0, 0x52, 0xcc, 0x81, 0x2e, 0x18, 0x1d, 0xaf, 0x08, 0xce, 0xb6, 0x13, 0x20, 0x0a, 0x63, 0x2d, 0x8a, 0x2c, 0x4e, - 0xf8, 0x5b, 0xd2, 0x26, 0x60, 0x4b, 0xc6, 0x28, 0x8d, 0x7d, 0x0e, 0xce, 0x95, 0xf9, 0xe0, 0xb1, 0xea, 0x17, 0x75, - 0xba, 0xba, 0x0c, 0xb1, 0xcf, 0x4f, 0x72, 0x79, 0xfd, 0x1d, 0xea, 0x4b, 0xbf, 0x8d, 0xd2, 0x17, 0x78, 0x67, 0x09, - 0x39, 0xef, 0x5e, 0xff, 0x54, 0xb4, 0x38, 0x30, 0x0b, 0x5d, 0xc5, 0x16, 0xb5, 0xe0, 0xfc, 0xe9, 0x85, 0x44, 0x14, - 0x65, 0x8f, 0x09, 0x90, 0xa9, 0xa6, 0xac, 0x7e, 0x53, 0xa4, 0x40, 0xc6, 0x45, 0x55, 0x9c, 0x3c, 0x06, 0xdf, 0xce, - 0x37, 0x77, 0x94, 0xc1, 0x68, 0xc8, 0xba, 0x5f, 0xef, 0xd8, 0xc4, 0x6f, 0xc4, 0xfc, 0x8f, 0x09, 0x27, 0xbd, 0x7a, - 0x4a, 0x80, 0x4a, 0xda, 0x49, 0x2a, 0x7d, 0x50, 0xe0, 0x85, 0x89, 0x26, 0x67, 0xa8, 0x41, 0x56, 0x78, 0x02, 0x9d, - 0x01, 0xcb, 0xd8, 0x62, 0x4a, 0xd9, 0x6d, 0x9b, 0xf8, 0x19, 0x85, 0x37, 0xb6, 0xb5, 0xd5, 0x18, 0xa4, 0xa7, 0x0a, - 0xd0, 0xf6, 0x38, 0x53, 0x85, 0x67, 0xd1, 0x85, 0x73, 0x6e, 0xde, 0x39, 0x70, 0x3e, 0xac, 0xcd, 0xc3, 0x97, 0xbf, - 0x20, 0x07, 0x85, 0x5d, 0x93, 0x3a, 0xa8, 0xea, 0x9a, 0x97, 0x74, 0xc2, 0x3f, 0x61, 0x7b, 0x89, 0xc5, 0x4c, 0x5e, - 0xd2, 0x7e, 0x0a, 0x1d, 0x21, 0x6d, 0x1e, 0x3a, 0xcd, 0xf6, 0x6f, 0x8a, 0x99, 0x1c, 0x2f, 0xb6, 0x9a, 0xfd, 0xb2, - 0x31, 0xfe, 0x2d, 0x92, 0x02, 0xee, 0x2b, 0xe7, 0xc3, 0x2a, 0x32, 0x89, 0x3a, 0xae, 0x8d, 0x17, 0x94, 0x3e, 0x86, - 0xe9, 0x68, 0xb1, 0xea, 0xb2, 0x8c, 0x7b, 0xa5, 0xcc, 0x91, 0x51, 0x82, 0xc3, 0x53, 0x55, 0x44, 0x15, 0x3a, 0xaf, - 0x21, 0x2f, 0xcd, 0xfc, 0xba, 0x4a, 0x45, 0xe8, 0x43, 0x99, 0x73, 0xce, 0x5b, 0xa2, 0xee, 0x7a, 0xa2, 0xf8, 0x71, - 0x81, 0x42, 0x5b, 0x22, 0x8c, 0x7c, 0x70, 0x06, 0xa7, 0x49, 0x82, 0x47, 0x26, 0x22, 0x8f, 0x3c, 0xc5, 0xf5, 0x8b, - 0x51, 0x49, 0x2f, 0x2f, 0xe1, 0x91, 0x73, 0x97, 0xc5, 0xdd, 0x47, 0xfc, 0x5a, 0x7d, 0x89, 0x3e, 0x2c, 0xe1, 0x28, - 0x88, 0x95, 0x76, 0xbf, 0x0c, 0x9f, 0xd4, 0x81, 0xca, 0xf9, 0x3f, 0xa8, 0xbf, 0x86, 0x2c, 0xb2, 0x88, 0x26, 0xeb, - 0x0a, 0x73, 0x70, 0xd4, 0x0f, 0x8b, 0x10, 0xf9, 0x22, 0x43, 0x48, 0x16, 0xdd, 0xea, 0xe5, 0x21, 0xb4, 0x9e, 0xfc, - 0xdd, 0x32, 0xf7, 0xfb, 0xbb, 0x6a, 0x7a, 0x38, 0x8d, 0x14, 0xfc, 0x48, 0x45, 0x5f, 0x76, 0x75, 0x7b, 0x12, 0xdf, - 0xf5, 0x7c, 0x0f, 0x01, 0xb3, 0x8f, 0x34, 0x44, 0xf2, 0x66, 0xd9, 0x3a, 0xf4, 0x4d, 0x6c, 0x71, 0x45, 0x6b, 0xd0, - 0xa5, 0xd4, 0xf4, 0x51, 0x81, 0x33, 0xbc, 0x12, 0x74, 0x90, 0xe1, 0x68, 0xb9, 0xf2, 0xa6, 0x42, 0xb0, 0x38, 0xa9, - 0xda, 0x6e, 0x8b, 0x32, 0xdb, 0x33, 0x38, 0x89, 0x16, 0x51, 0x93, 0x99, 0xfc, 0x3e, 0x7d, 0x14, 0xab, 0xdc, 0x28, - 0x82, 0x3b, 0xfa, 0xc2, 0x2a, 0xad, 0xbd, 0x34, 0xe4, 0x97, 0x5a, 0xb2, 0x85, 0x06, 0x54, 0x4a, 0x79, 0xa1, 0x6a, - 0x5c, 0x2e, 0x85, 0x2b, 0x63, 0x6b, 0xf4, 0x30, 0xd3, 0xaa, 0x78, 0x15, 0x57, 0x48, 0xc7, 0xd7, 0x88, 0x45, 0xe8, - 0xb2, 0x0c, 0x12, 0x1e, 0xce, 0x72, 0x2e, 0x79, 0x72, 0x0d, 0x56, 0x3c, 0xb7, 0x60, 0x0e, 0x66, 0x5d, 0xab, 0x27, - 0xd5, 0xd8, 0x80, 0x91, 0x14, 0xf9, 0x2a, 0xa6, 0x73, 0xd5, 0x01, 0x75, 0x5c, 0x43, 0x60, 0x16, 0xee, 0x23, 0x3f, - 0x4a, 0x49, 0xaf, 0x94, 0x91, 0x33, 0xdc, 0x56, 0x49, 0x36, 0xe3, 0x64, 0x24, 0xdc, 0xd1, 0xc6, 0xce, 0x45, 0x01, - 0x3f, 0x0a, 0x39, 0x53, 0x02, 0x1a, 0xd2, 0x10, 0x49, 0x2e, 0x76, 0xcf, 0x13, 0xab, 0x21, 0xd2, 0x93, 0x05, 0x05, - 0x10, 0x79, 0x58, 0xfb, 0x60, 0x49, 0x79, 0x34, 0xb5, 0x80, 0x89, 0x79, 0xae, 0xfc, 0xa8, 0xbd, 0x85, 0xaf, 0xd6, - 0xa1, 0x04, 0xcc, 0xe1, 0xcb, 0x24, 0xd6, 0x4a, 0x9b, 0xf1, 0xb8, 0x2c, 0x8f, 0xca, 0x5b, 0xcb, 0x6a, 0xba, 0xa2, - 0x7a, 0xd0, 0x98, 0x1e, 0xae, 0x53, 0x62, 0x6c, 0x2c, 0xb3, 0x4e, 0x5c, 0x2a, 0xe6, 0xbf, 0x4f, 0x5f, 0xaa, 0x8b, - 0xaa, 0x96, 0x6f, 0x23, 0xae, 0x67, 0x8c, 0x6a, 0x51, 0xc3, 0x03, 0xe6, 0x5f, 0x96, 0x31, 0x2c, 0xd7, 0x04, 0xb3, - 0x5c, 0x13, 0xbd, 0xad, 0x86, 0xa0, 0xed, 0x78, 0x14, 0x95, 0xa0, 0x1b, 0x31, 0xa0, 0x91, 0x52, 0x23, 0x60, 0x9b, - 0x14, 0x62, 0xf0, 0x1b, 0xb0, 0x3f, 0x76, 0x08, 0x48, 0x05, 0x47, 0xf0, 0x80, 0x70, 0xc9, 0x71, 0xf9, 0x61, 0xd2, - 0xdd, 0x42, 0x02, 0xa9, 0x78, 0x39, 0x2b, 0x9f, 0x96, 0x08, 0x46, 0x31, 0x28, 0x8b, 0xd0, 0x0c, 0x91, 0x52, 0x37, - 0x2b, 0x32, 0xea, 0xe0, 0x8d, 0xc1, 0x37, 0x22, 0x06, 0xbc, 0x52, 0x50, 0xc8, 0x63, 0x4e, 0x4e, 0x96, 0xcb, 0xd7, - 0xb9, 0x4b, 0x7f, 0x47, 0x4f, 0xe5, 0x38, 0x75, 0x47, 0xd0, 0xa6, 0x8f, 0x62, 0x9c, 0x8b, 0x4a, 0x82, 0xeb, 0xe5, - 0xb4, 0xf7, 0x98, 0xd1, 0x7c, 0xe1, 0xea, 0x69, 0x3c, 0x68, 0x8b, 0xdf, 0x8f, 0x39, 0xfb, 0xf0, 0x29, 0x35, 0xba, - 0x80, 0x02, 0x0f, 0x3b, 0x75, 0xdb, 0xc8, 0x29, 0x46, 0x80, 0xbf, 0xad, 0xaf, 0xc7, 0xa3, 0xcd, 0x96, 0xb9, 0x20, - 0x35, 0xec, 0x1b, 0x7c, 0x39, 0x18, 0x7f, 0x45, 0xb4, 0xc3, 0xd7, 0x64, 0xdd, 0x43, 0x83, 0xee, 0x5e, 0xd6, 0xf0, - 0x05, 0x0b, 0x74, 0x7e, 0x89, 0x21, 0x6f, 0xd9, 0x81, 0xe5, 0x3e, 0xcf, 0xf5, 0x04, 0x99, 0xc6, 0xc3, 0x78, 0x0d, - 0xc8, 0x35, 0x9e, 0xe5, 0xbd, 0x1b, 0xf5, 0x7b, 0xb6, 0x9c, 0x77, 0xc4, 0xd6, 0xd4, 0xbb, 0x6c, 0x03, 0x8c, 0xa3, - 0xea, 0x7f, 0xdf, 0x54, 0x22, 0x18, 0x99, 0xa1, 0x7d, 0x5a, 0xeb, 0xa3, 0xca, 0xd3, 0xff, 0x37, 0xb3, 0x75, 0x61, - 0x65, 0x98, 0x43, 0x30, 0xe3, 0x2b, 0x7c, 0x9a, 0x71, 0x1e, 0x44, 0x78, 0x20, 0xed, 0x5e, 0x66, 0x37, 0xd6, 0x9c, - 0xd1, 0x8f, 0xd0, 0x9d, 0x92, 0xec, 0x02, 0xc7, 0xf1, 0x6f, 0x83, 0x9e, 0x0a, 0xb5, 0x1f, 0xd5, 0x81, 0xc5, 0xdf, - 0x05, 0xa9, 0x09, 0xc9, 0x50, 0x80, 0x03, 0xb9, 0x6a, 0xd9, 0x7b, 0xba, 0x9d, 0x5d, 0x4c, 0x13, 0xc4, 0xa5, 0xb3, - 0xd5, 0x97, 0xd7, 0xad, 0x17, 0x68, 0xdf, 0xec, 0xfd, 0x68, 0x63, 0xa6, 0x98, 0xc7, 0xeb, 0xbb, 0xa6, 0xe3, 0x37, - 0x14, 0x86, 0xd6, 0xf8, 0x2a, 0x62, 0xba, 0x6f, 0x68, 0xde, 0xcb, 0xb5, 0x37, 0xed, 0x3d, 0x7e, 0xb1, 0xd7, 0xea, - 0x2c, 0xb0, 0xf1, 0x46, 0x01, 0x57, 0x26, 0x2e, 0x67, 0x94, 0xe4, 0xc3, 0x0d, 0x82, 0xeb, 0xf8, 0xd1, 0xaa, 0x19, - 0xee, 0x7a, 0x7c, 0xa3, 0x13, 0x96, 0x61, 0x60, 0xba, 0x85, 0xeb, 0x40, 0x8c, 0x61, 0x6c, 0x11, 0x42, 0x91, 0xfa, - 0x91, 0xc6, 0x30, 0x0a, 0xc6, 0xcf, 0x4f, 0xd6, 0x98, 0xd9, 0xf1, 0x1f, 0x51, 0x00, 0xae, 0x5d, 0x84, 0x23, 0x30, - 0xcc, 0x2c, 0xa8, 0x50, 0xe1, 0x42, 0x1f, 0x89, 0x2b, 0x9b, 0xb2, 0xa7, 0xf0, 0x02, 0xf2, 0x25, 0xa6, 0xfd, 0x51, - 0xd7, 0xf9, 0x83, 0x13, 0xf9, 0x49, 0xed, 0xee, 0xf7, 0x4a, 0xb7, 0x17, 0xe1, 0x32, 0xd3, 0x75, 0xc4, 0x00, 0xc9, - 0x63, 0x30, 0xc1, 0x68, 0x31, 0x64, 0xc7, 0x4d, 0xed, 0x59, 0x9d, 0xce, 0xc9, 0xf1, 0xe0, 0x7f, 0xad, 0x82, 0xd9, - 0xf8, 0x28, 0xde, 0x56, 0x8d, 0x9c, 0xed, 0x49, 0xba, 0xf9, 0x63, 0xa5, 0xc9, 0xec, 0xff, 0x61, 0x7e, 0x9d, 0x05, - 0x0b, 0xe6, 0x8b, 0x45, 0x8b, 0xac, 0x21, 0x6a, 0x80, 0x1f, 0xee, 0x34, 0xa6, 0x7c, 0x46, 0x19, 0x61, 0xab, 0x5a, - 0x2b, 0x6d, 0x68, 0x80, 0xe6, 0x94, 0x9d, 0x20, 0x45, 0x09, 0x24, 0xef, 0x58, 0x85, 0x91, 0xf9, 0xc0, 0x30, 0x78, - 0xec, 0x7d, 0x6a, 0xdd, 0x9a, 0xa2, 0xae, 0xf0, 0x3e, 0xd1, 0xd8, 0x8d, 0x59, 0x29, 0xf5, 0x73, 0x2b, 0xf4, 0x1f, - 0xd1, 0x0b, 0x11, 0x58, 0x22, 0x3f, 0x04, 0xf5, 0x93, 0xa0, 0x96, 0xf5, 0x27, 0x95, 0xb7, 0x7c, 0x6e, 0xcf, 0x2e, - 0xb6, 0xe5, 0xd3, 0x5c, 0x67, 0x20, 0xb1, 0x33, 0xbe, 0xa9, 0x2f, 0xbe, 0x48, 0xb5, 0x96, 0x5b, 0xba, 0xe5, 0xd1, - 0x34, 0xc4, 0xe8, 0x00, 0x80, 0x94, 0x01, 0xe3, 0x27, 0xf8, 0x81, 0x3a, 0xe3, 0x9f, 0xcf, 0x6f, 0xea, 0x9c, 0xea, - 0xaf, 0xde, 0x48, 0xe8, 0x2d, 0x2d, 0x01, 0xdf, 0x85, 0xfc, 0xdf, 0xfe, 0x95, 0x6e, 0x1d, 0x63, 0x45, 0x60, 0x76, - 0x70, 0x75, 0x92, 0x9d, 0xe9, 0x69, 0x6b, 0xe2, 0x2a, 0x06, 0xef, 0x87, 0x68, 0x9d, 0xfb, 0x91, 0xc0, 0x68, 0x0a, - 0x6f, 0xb3, 0xd8, 0xb4, 0x55, 0xae, 0x57, 0x33, 0x61, 0xb6, 0x8d, 0x2e, 0x91, 0x1a, 0x82, 0xeb, 0x7d, 0x2c, 0xa3, - 0x8b, 0x27, 0x83, 0xb3, 0xba, 0xbe, 0x64, 0x2a, 0xc0, 0x85, 0x14, 0xf5, 0x27, 0xca, 0xb2, 0xdd, 0xa3, 0x2e, 0x35, - 0xdd, 0x1f, 0xb4, 0x76, 0xcf, 0xa5, 0xc5, 0x36, 0x5d, 0x36, 0x7d, 0x6a, 0x3d, 0x10, 0xc1, 0xbe, 0xa5, 0x1b, 0xf6, - 0x02, 0x00, 0x1d, 0xe0, 0x85, 0x6a, 0x13, 0x5d, 0x57, 0xfd, 0x63, 0x0f, 0x48, 0x6b, 0x7c, 0x8f, 0x4d, 0xaa, 0xdc, - 0xc8, 0xa4, 0xda, 0x45, 0x82, 0xb2, 0xc3, 0xf8, 0xf8, 0x0e, 0x7b, 0xad, 0x87, 0x17, 0x6a, 0x55, 0x0a, 0x6b, 0xcb, - 0xdc, 0x9b, 0x31, 0xa9, 0x69, 0xeb, 0x0f, 0x5e, 0x0b, 0xeb, 0x86, 0x2e, 0x85, 0xcb, 0xe2, 0x51, 0xab, 0x03, 0x90, - 0x93, 0x0d, 0x84, 0x70, 0xc4, 0xd3, 0x3f, 0x92, 0x9c, 0x02, 0xbc, 0x0e, 0xdc, 0x15, 0xc7, 0xec, 0xb2, 0x1d, 0x77, - 0xc3, 0x96, 0x5b, 0xf8, 0xb3, 0x5b, 0x20, 0xc5, 0xba, 0xea, 0xdc, 0xb0, 0x83, 0xd7, 0x65, 0x8a, 0xa0, 0x54, 0x48, - 0x40, 0x38, 0x5c, 0xce, 0x2e, 0x05, 0xa1, 0x24, 0x60, 0xac, 0x0a, 0xf7, 0x87, 0xb2, 0xb7, 0xdd, 0x6e, 0xd8, 0x92, - 0x47, 0x92, 0x61, 0xa0, 0x6a, 0x3d, 0xa6, 0xf5, 0xaf, 0x76, 0x3a, 0x81, 0x4a, 0xd6, 0x6c, 0x7a, 0xa4, 0x7f, 0x58, - 0x8f, 0xf4, 0x52, 0xf0, 0x48, 0xc4, 0xe2, 0x1d, 0x19, 0xa3, 0xab, 0x1f, 0x2f, 0x90, 0xd9, 0x3b, 0x2e, 0x7e, 0x98, - 0xc3, 0xda, 0xb0, 0xcb, 0x80, 0x27, 0x98, 0x49, 0x83, 0x7a, 0xd5, 0x55, 0xf4, 0x54, 0x90, 0x0e, 0x8b, 0x86, 0x81, - 0x85, 0x53, 0xea, 0x57, 0xe9, 0x2d, 0x6f, 0x74, 0x16, 0x34, 0x86, 0x12, 0x2d, 0x65, 0xa1, 0x2c, 0x37, 0x93, 0x87, - 0x0d, 0x25, 0x2b, 0xae, 0xa9, 0x6d, 0x67, 0xab, 0x68, 0xd1, 0x0a, 0xc2, 0x1f, 0xd7, 0x30, 0x23, 0xea, 0x2f, 0x64, - 0x9a, 0x75, 0x3b, 0xfc, 0x0c, 0x69, 0xb5, 0xa4, 0x76, 0x6e, 0x81, 0xf6, 0x82, 0x06, 0xfc, 0x1a, 0x82, 0xd6, 0x92, - 0x52, 0x13, 0x9f, 0xd6, 0xb9, 0xe0, 0xf1, 0x86, 0xe1, 0xd3, 0x26, 0xa9, 0x97, 0xd9, 0xc6, 0xd5, 0x0d, 0x4f, 0x73, - 0x29, 0x3a, 0x18, 0x74, 0x90, 0x90, 0x52, 0x73, 0xa8, 0xc8, 0xdf, 0x5d, 0xac, 0x0b, 0xa7, 0x09, 0xc9, 0x66, 0x2a, - 0x59, 0x4e, 0x4a, 0xf6, 0x8c, 0x10, 0x47, 0x3f, 0x20, 0x65, 0xf2, 0x08, 0x35, 0xc9, 0xab, 0x17, 0x90, 0xc9, 0xeb, - 0x51, 0x4b, 0x8a, 0x0b, 0x6d, 0x32, 0xb1, 0x14, 0x70, 0x32, 0x8e, 0x20, 0x13, 0xac, 0xa7, 0x64, 0x75, 0x0d, 0x90, - 0xf4, 0x92, 0x3c, 0x35, 0xb0, 0x60, 0x6a, 0xef, 0x94, 0x02, 0x8b, 0x14, 0x80, 0xa1, 0x9d, 0x34, 0x2a, 0x4b, 0xf2, - 0x58, 0x76, 0xdf, 0x96, 0x65, 0x4f, 0xa1, 0xa0, 0x60, 0xc4, 0xd9, 0x63, 0x9f, 0x9d, 0x05, 0x82, 0xf2, 0x70, 0x06, - 0x65, 0xfa, 0x9c, 0x60, 0x23, 0xcf, 0x11, 0x2c, 0xf2, 0x62, 0x40, 0x8e, 0x2a, 0x5e, 0xd6, 0x08, 0xff, 0xd5, 0x02, - 0xb9, 0xc0, 0xe0, 0xe1, 0x9e, 0x74, 0x7a, 0xad, 0xdf, 0x94, 0x53, 0x50, 0x30, 0xfa, 0x94, 0xd5, 0xcd, 0x38, 0x37, - 0xd4, 0x32, 0x9a, 0x31, 0xf5, 0x67, 0xee, 0xe2, 0x49, 0xbe, 0x55, 0x32, 0xa3, 0x22, 0x93, 0x89, 0x17, 0x7e, 0x00, - 0x3b, 0x3f, 0x2f, 0x26, 0x06, 0x85, 0x27, 0x2e, 0xea, 0x98, 0x11, 0x87, 0xb8, 0x2a, 0x27, 0xbf, 0x2d, 0xc0, 0xa8, - 0xc1, 0x60, 0x72, 0x8b, 0x7a, 0x4d, 0xa9, 0xf7, 0x50, 0x9f, 0x19, 0x0c, 0xb5, 0x71, 0xac, 0x57, 0x56, 0x82, 0x0d, - 0x0d, 0x2f, 0xf9, 0x54, 0xcd, 0x3a, 0x8c, 0x15, 0x7e, 0xa5, 0x02, 0xcc, 0x05, 0xa6, 0x79, 0x1a, 0x87, 0x80, 0x95, - 0x96, 0x6a, 0x18, 0x7d, 0x75, 0xee, 0x10, 0x4a, 0xdd, 0xe8, 0x05, 0x6c, 0x00, 0xc3, 0x21, 0xa2, 0x2d, 0x7a, 0x79, - 0xe1, 0x2b, 0x0d, 0x52, 0xb5, 0x23, 0x4b, 0x5e, 0x1d, 0x72, 0x22, 0x8f, 0x27, 0xe2, 0x7f, 0x26, 0x0c, 0x49, 0x9b, - 0x1b, 0x88, 0xb7, 0x94, 0xdd, 0xd4, 0x71, 0x9a, 0x39, 0x48, 0xef, 0xe9, 0x60, 0xaf, 0x95, 0xaf, 0x6c, 0x93, 0x19, - 0x7a, 0x35, 0x1a, 0x87, 0x82, 0xb4, 0xbc, 0x98, 0x2d, 0x32, 0x69, 0x12, 0xdd, 0x96, 0x16, 0x03, 0x7a, 0x88, 0xec, - 0xcc, 0x43, 0xb1, 0xe2, 0x7d, 0x3d, 0x99, 0x16, 0x14, 0x1d, 0xc2, 0x2d, 0xe4, 0x26, 0x1a, 0xf5, 0x13, 0x5d, 0xb5, - 0x2b, 0x94, 0xd9, 0x7e, 0x26, 0x74, 0x80, 0x97, 0x16, 0x27, 0x00, 0xec, 0xd1, 0x34, 0x2e, 0xb8, 0x6d, 0x09, 0xc3, - 0xd4, 0x86, 0x6a, 0x2e, 0x3b, 0xdd, 0xd6, 0x99, 0x5c, 0x0b, 0x14, 0x83, 0x00, 0x3a, 0xcf, 0x37, 0xef, 0x4f, 0x5e, - 0xfe, 0x0c, 0xc7, 0xb1, 0x83, 0xd1, 0xc9, 0x8c, 0xaa, 0xb8, 0x4d, 0xa2, 0xde, 0x6f, 0xe9, 0xa6, 0x81, 0xbc, 0xef, - 0x41, 0x35, 0x7b, 0xd6, 0xef, 0x4e, 0xd7, 0xc6, 0x3b, 0xf5, 0x9b, 0xd9, 0x00, 0xa0, 0xbc, 0x48, 0x9a, 0x0f, 0x70, - 0xdc, 0xe0, 0xe7, 0x19, 0xab, 0x15, 0x6a, 0x24, 0x22, 0x88, 0x02, 0x12, 0xa6, 0xfe, 0x59, 0x38, 0xbc, 0xc7, 0x77, - 0x2c, 0x3b, 0x51, 0x70, 0x48, 0xea, 0xab, 0xc1, 0xd1, 0x83, 0x6e, 0x4c, 0x05, 0xc3, 0x1a, 0xe3, 0x84, 0x9b, 0x6f, - 0xb1, 0xef, 0x5a, 0x2b, 0x8a, 0xeb, 0xc2, 0x3e, 0xf7, 0x9d, 0xa2, 0x9e, 0x7d, 0x76, 0x43, 0x8f, 0xf3, 0xe0, 0x15, - 0x53, 0x56, 0x29, 0xd6, 0x5d, 0x8e, 0x3c, 0x3e, 0x01, 0x52, 0xf3, 0x5d, 0xc9, 0xdf, 0x60, 0xac, 0x20, 0x0a, 0xbc, - 0x5f, 0x6d, 0x8a, 0x74, 0x39, 0xb1, 0x22, 0x0a, 0x83, 0x20, 0xf3, 0x2a, 0x42, 0xbc, 0xa2, 0x92, 0xdf, 0xb7, 0x03, - 0x38, 0x81, 0x3c, 0x1c, 0xb6, 0x09, 0xbe, 0xdf, 0xd1, 0x40, 0xa8, 0x18, 0x37, 0xd2, 0x76, 0x4b, 0x4e, 0x37, 0x8c, - 0x7b, 0x3a, 0x69, 0xf6, 0x26, 0x91, 0x9b, 0x44, 0x0d, 0x47, 0x11, 0xcb, 0xd7, 0x64, 0x77, 0x45, 0x81, 0x42, 0x80, - 0xc8, 0x2e, 0xf9, 0x1c, 0x96, 0x9a, 0xca, 0xf5, 0xb5, 0xe4, 0x17, 0x48, 0x82, 0x8c, 0xdb, 0x40, 0xea, 0x9f, 0x14, - 0xa1, 0xec, 0x1c, 0xb5, 0x61, 0xc7, 0x8f, 0x26, 0xaa, 0x0e, 0x76, 0xa7, 0x55, 0x9b, 0x1d, 0x8d, 0x60, 0xdf, 0x6b, - 0x85, 0x96, 0x83, 0xd6, 0x59, 0x9f, 0x9a, 0xdc, 0x10, 0x3f, 0x3e, 0xe7, 0x72, 0x80, 0x00, 0x3a, 0x59, 0xa0, 0xc2, - 0xfd, 0xd0, 0x51, 0xdf, 0xae, 0x0e, 0x69, 0x02, 0x45, 0xe5, 0xa0, 0xb8, 0xe3, 0x38, 0x85, 0x5d, 0x91, 0x1d, 0xfd, - 0x42, 0x34, 0x4e, 0xd8, 0xe1, 0x23, 0x6b, 0x9a, 0x3f, 0xc4, 0x09, 0xca, 0x17, 0xf3, 0x50, 0x70, 0x89, 0x3a, 0x1b, - 0x52, 0x46, 0x00, 0x74, 0x47, 0xbb, 0xf5, 0x90, 0x7e, 0x5c, 0xdb, 0x14, 0x7b, 0xee, 0x09, 0xfa, 0xbc, 0x6f, 0x60, - 0xdc, 0x11, 0xd8, 0xd1, 0x40, 0x12, 0xda, 0x47, 0x95, 0xfa, 0x33, 0x8f, 0xc5, 0x98, 0xd9, 0xa7, 0xdb, 0x66, 0x82, - 0xca, 0x9d, 0xec, 0x52, 0x09, 0xd2, 0xe0, 0x0d, 0xf2, 0x70, 0xd8, 0xd7, 0xfd, 0x5e, 0x6a, 0xda, 0xe6, 0xc9, 0xed, - 0x2b, 0xab, 0xd5, 0x94, 0xef, 0x76, 0x99, 0x40, 0x7c, 0x71, 0x0e, 0x65, 0xbc, 0xe7, 0x81, 0xaa, 0xef, 0x1b, 0x59, - 0x43, 0xc0, 0x7d, 0xb3, 0x30, 0xcc, 0x09, 0x3a, 0xc2, 0x20, 0x69, 0xe6, 0xc1, 0x9f, 0x00, 0x6d, 0xde, 0xcb, 0xeb, - 0x55, 0x88, 0x73, 0x40, 0x77, 0xf8, 0xf2, 0x84, 0xb5, 0x8e, 0xd8, 0xe3, 0x83, 0x79, 0xc6, 0x28, 0x37, 0xbc, 0x44, - 0xc7, 0x88, 0xdb, 0xde, 0x95, 0x17, 0x32, 0x5d, 0x3e, 0xfb, 0x96, 0x04, 0xbe, 0x31, 0x52, 0x81, 0x14, 0x68, 0xc4, - 0xb1, 0x0f, 0x36, 0xdf, 0x87, 0x43, 0xb3, 0x5f, 0xe8, 0x8d, 0xc2, 0xf4, 0x72, 0xfc, 0xe5, 0xc6, 0xfc, 0x16, 0x8e, - 0xb8, 0xda, 0x2a, 0xc4, 0xc3, 0x5e, 0x8e, 0xb9, 0xd0, 0x9a, 0x07, 0xbf, 0x30, 0x27, 0xcb, 0x42, 0xe2, 0xdd, 0x45, - 0x7d, 0xc3, 0x7a, 0xcd, 0x96, 0x3d, 0x93, 0x59, 0x13, 0x0f, 0x92, 0xf5, 0xb4, 0xf2, 0x70, 0x7a, 0x2a, 0xcf, 0xb1, - 0xd9, 0x0b, 0x0b, 0xb2, 0xa1, 0xab, 0xa7, 0xb6, 0x5c, 0xf7, 0xd6, 0x34, 0x24, 0x2f, 0xf1, 0x8b, 0xab, 0x68, 0x01, - 0x4a, 0x4c, 0xd4, 0xce, 0xac, 0x5d, 0x90, 0x0a, 0xf6, 0x7a, 0x59, 0x40, 0x83, 0x63, 0xe5, 0xd8, 0x96, 0xd0, 0x53, - 0x91, 0x19, 0x9f, 0x55, 0x29, 0x20, 0x7d, 0x4d, 0xd4, 0xed, 0x45, 0x54, 0x5a, 0x42, 0x82, 0xc0, 0xc7, 0x4f, 0x92, - 0x52, 0xec, 0xcb, 0x0d, 0x20, 0x30, 0x54, 0xbc, 0xef, 0x02, 0xbe, 0xbf, 0xa9, 0x48, 0x64, 0x72, 0xb0, 0x92, 0xf7, - 0x44, 0x97, 0x14, 0xf8, 0x9f, 0x9f, 0xef, 0xac, 0x54, 0x2a, 0x72, 0x39, 0x86, 0x11, 0xc5, 0x5e, 0x33, 0x45, 0x61, - 0x6e, 0x1a, 0xa1, 0x20, 0x50, 0xcb, 0x3f, 0x5c, 0x7f, 0xa1, 0xbf, 0xa4, 0x04, 0xa7, 0x96, 0x40, 0x5c, 0x5e, 0x9d, - 0x87, 0x04, 0x67, 0xf5, 0x16, 0x79, 0xac, 0x20, 0xd8, 0x63, 0xae, 0x35, 0x3b, 0xcc, 0x81, 0x64, 0x57, 0x0b, 0x8c, - 0xb6, 0x44, 0x29, 0xf5, 0x02, 0x62, 0x97, 0x4c, 0xf7, 0x75, 0x45, 0x91, 0xee, 0x51, 0xf4, 0x98, 0xca, 0x68, 0x39, - 0x3e, 0xd1, 0xd8, 0xdf, 0x18, 0xaa, 0x96, 0xfa, 0x2a, 0x7b, 0xc2, 0xd7, 0xbb, 0xc3, 0x17, 0x1b, 0x3f, 0x12, 0xfe, - 0x3e, 0x57, 0x4c, 0x3f, 0x67, 0xf7, 0xa3, 0x5d, 0xc2, 0x68, 0xa0, 0x7a, 0xae, 0x39, 0x6e, 0x2c, 0x37, 0x5e, 0x6e, - 0x5f, 0x74, 0xc5, 0x56, 0x99, 0x9f, 0xbb, 0x05, 0xb9, 0x26, 0xdd, 0x6b, 0x32, 0xcf, 0x49, 0x6e, 0xf0, 0xce, 0xf4, - 0x20, 0x9d, 0x08, 0xae, 0xfd, 0xcb, 0xf3, 0xaf, 0x3b, 0x5c, 0x85, 0x6d, 0x5b, 0x91, 0x57, 0x66, 0x40, 0x39, 0xb4, - 0xdb, 0x04, 0xd4, 0x97, 0x6e, 0xd2, 0x1d, 0xd1, 0x36, 0xb6, 0xf0, 0x12, 0xe2, 0x35, 0x50, 0xdc, 0xd2, 0xc4, 0x57, - 0x67, 0xa3, 0x90, 0xa6, 0x64, 0xb2, 0x07, 0x84, 0x62, 0xc2, 0x02, 0xfd, 0xd3, 0x71, 0xd2, 0xac, 0x0a, 0x5a, 0xef, - 0x95, 0xaa, 0x8e, 0x95, 0xd3, 0xd5, 0x57, 0x61, 0x66, 0xa3, 0x19, 0xf1, 0x20, 0x27, 0x1b, 0x87, 0x28, 0x77, 0x9d, - 0xe9, 0xe8, 0x50, 0x3c, 0xa6, 0xdc, 0x49, 0x9d, 0x5c, 0x9c, 0xb3, 0x23, 0x09, 0xc5, 0x7f, 0xeb, 0x9c, 0x08, 0x85, - 0x6f, 0x61, 0x2b, 0x02, 0xf9, 0xda, 0xb4, 0xfc, 0xaf, 0x1d, 0xf5, 0x39, 0xe1, 0x8e, 0x76, 0xe5, 0x6a, 0xc6, 0x29, - 0xb2, 0xe1, 0x40, 0xe6, 0xe3, 0x46, 0x05, 0xaf, 0x3c, 0x55, 0x65, 0xbf, 0x8d, 0x98, 0xf4, 0x89, 0x3d, 0x9b, 0x1c, - 0x26, 0xa5, 0xa3, 0xf6, 0x13, 0x5c, 0x16, 0x1d, 0x4c, 0xc3, 0xa2, 0x0d, 0x11, 0x20, 0x6a, 0xb5, 0xd1, 0x0e, 0x8b, - 0x88, 0x04, 0x8d, 0x2f, 0x56, 0x2f, 0xe3, 0x81, 0x8f, 0xe6, 0x18, 0xc5, 0x3e, 0x6d, 0x6b, 0x49, 0xf6, 0xbd, 0x31, - 0x46, 0xca, 0x40, 0x7d, 0xa2, 0x73, 0xa0, 0x4c, 0x2c, 0xf2, 0x31, 0xc9, 0x4b, 0x2d, 0x56, 0xb8, 0x4b, 0x5e, 0x47, - 0x25, 0x60, 0x45, 0xf2, 0x57, 0x70, 0x19, 0x25, 0x08, 0x46, 0x8f, 0xa2, 0x2f, 0xfd, 0x12, 0xdc, 0x72, 0xdf, 0x1f, - 0x32, 0x05, 0x76, 0x8a, 0xb1, 0x8f, 0x18, 0xbd, 0x94, 0x22, 0xf3, 0x21, 0x12, 0x8f, 0xdf, 0xb3, 0x04, 0x29, 0x28, - 0x7d, 0x69, 0x1b, 0x60, 0x70, 0x13, 0xe8, 0xb2, 0x6e, 0x6a, 0x9c, 0x01, 0x72, 0x22, 0x57, 0xd4, 0x76, 0xdc, 0xf3, - 0xc9, 0x0e, 0x05, 0x6d, 0x0d, 0x32, 0x66, 0x7a, 0xa1, 0x59, 0xa2, 0xc0, 0xf0, 0xfd, 0x56, 0xe3, 0x28, 0x18, 0xb0, - 0x6b, 0xac, 0x47, 0xbf, 0x52, 0xd2, 0x21, 0x53, 0xfa, 0x91, 0x16, 0xce, 0xe5, 0x4c, 0xa6, 0x7a, 0xf3, 0x7b, 0x21, - 0x05, 0x10, 0x17, 0x6f, 0x25, 0xa2, 0xb7, 0x64, 0x7f, 0x1d, 0x14, 0xa0, 0x60, 0x1a, 0x19, 0x23, 0xfd, 0x5f, 0x2c, - 0x0b, 0x72, 0x3b, 0x0a, 0x6b, 0x86, 0x04, 0x06, 0x15, 0x1f, 0x77, 0x68, 0x8e, 0xbf, 0x0e, 0xff, 0x6b, 0x03, 0x50, - 0x57, 0xee, 0xbc, 0xa1, 0xac, 0xf9, 0x01, 0x29, 0x45, 0x66, 0x2f, 0xdf, 0xbd, 0x6a, 0x85, 0x3a, 0x68, 0xb1, 0xcd, - 0x75, 0x9e, 0xd7, 0x16, 0xbf, 0x9e, 0x42, 0xb7, 0x37, 0xf9, 0xcd, 0x6c, 0x57, 0x5d, 0x3d, 0x36, 0x6a, 0xd4, 0x1b, - 0x04, 0xa3, 0xb7, 0x37, 0xc3, 0x6e, 0x9d, 0x0f, 0x67, 0x25, 0xa0, 0x95, 0xcd, 0x5e, 0xfd, 0x9b, 0x08, 0x0a, 0x7d, - 0xad, 0x9f, 0x47, 0xba, 0xca, 0xb8, 0x7c, 0x96, 0x80, 0x97, 0xc6, 0x87, 0x46, 0x95, 0x6a, 0x59, 0x58, 0xb2, 0x26, - 0x11, 0x08, 0x0e, 0x7f, 0xd0, 0xac, 0x67, 0xda, 0x8f, 0xea, 0x36, 0xdf, 0xd4, 0x75, 0x15, 0x41, 0xfb, 0x11, 0xd6, - 0x74, 0xa5, 0xff, 0x37, 0x71, 0x78, 0x38, 0x45, 0xff, 0xa3, 0xf9, 0x83, 0x82, 0xed, 0xdd, 0x66, 0x53, 0x42, 0x85, - 0x6b, 0xe6, 0x5e, 0x3d, 0xc5, 0x33, 0x45, 0x62, 0x17, 0xa5, 0x67, 0x55, 0xdb, 0xc1, 0xb2, 0xa1, 0xb6, 0xd7, 0x90, - 0xb0, 0x45, 0x90, 0x6a, 0x0a, 0xc6, 0xcd, 0xd3, 0x3b, 0xdc, 0x1d, 0x71, 0xcc, 0xa0, 0x1c, 0x1a, 0x45, 0x99, 0xdf, - 0x0e, 0x93, 0xe6, 0x54, 0x6d, 0x6f, 0x51, 0xe0, 0x47, 0x00, 0x9f, 0x2b, 0x6a, 0x07, 0xf2, 0xf4, 0x61, 0x94, 0xaf, - 0x27, 0xb9, 0xef, 0xc4, 0x11, 0x09, 0xd6, 0x0e, 0x6c, 0x79, 0xc9, 0xab, 0xd3, 0x95, 0xd5, 0x3d, 0x89, 0xaf, 0x3b, - 0xc6, 0xf9, 0x21, 0x71, 0xed, 0x47, 0x4f, 0x53, 0x0e, 0xdb, 0xa2, 0x9e, 0xe0, 0xb0, 0x38, 0xb4, 0xdd, 0x10, 0xdd, - 0x76, 0x96, 0x46, 0xef, 0x00, 0x6d, 0xb1, 0xc9, 0x8b, 0x27, 0x9d, 0x63, 0x5c, 0x1f, 0x2e, 0x27, 0x69, 0xd9, 0x3f, - 0x95, 0x1a, 0xa2, 0xbe, 0xa5, 0x74, 0x8f, 0xd4, 0x1d, 0x1d, 0x6c, 0xcd, 0xde, 0x3f, 0x16, 0xcd, 0x43, 0xe4, 0xb5, - 0x1c, 0x36, 0x6d, 0x52, 0xce, 0x87, 0x2f, 0x1b, 0x7d, 0x59, 0x5e, 0x6d, 0x4a, 0xb6, 0x41, 0xea, 0x4c, 0xb4, 0x79, - 0x0c, 0xa8, 0x6f, 0x0d, 0xbd, 0x0a, 0xbe, 0x60, 0xcd, 0x16, 0xfa, 0xe6, 0xbc, 0x5b, 0x60, 0x2c, 0xc1, 0x67, 0x0c, - 0x6d, 0x73, 0xee, 0xbe, 0x93, 0xee, 0xb3, 0x1c, 0xa6, 0x5c, 0x54, 0x4e, 0x51, 0x22, 0x89, 0xba, 0xff, 0x2f, 0xaf, - 0xf7, 0x52, 0x46, 0x7a, 0x79, 0x42, 0x87, 0x9d, 0xc2, 0xc3, 0x25, 0xab, 0x80, 0x62, 0xac, 0xad, 0xf4, 0xbc, 0x72, - 0x0a, 0x52, 0xa3, 0xa3, 0xb8, 0xd0, 0x7f, 0xf8, 0xca, 0x5d, 0xef, 0x36, 0xd6, 0xf4, 0x63, 0xca, 0x92, 0xbf, 0xf6, - 0x8d, 0x04, 0x6d, 0x5d, 0x11, 0x99, 0xfc, 0x9f, 0x48, 0x4c, 0x8e, 0x2c, 0xc4, 0xa3, 0x03, 0x68, 0x60, 0xa7, 0x4e, - 0xb6, 0xa0, 0xc5, 0x24, 0x09, 0x88, 0x2e, 0xd1, 0x1c, 0x4e, 0x00, 0x6d, 0xd2, 0x12, 0x4c, 0xc8, 0x6f, 0xf4, 0xbe, - 0xcb, 0x98, 0x27, 0xfc, 0x65, 0x1e, 0x4e, 0xa0, 0xfb, 0xe0, 0xd0, 0xa2, 0x09, 0x58, 0x45, 0x92, 0x86, 0xb5, 0xb6, - 0x9d, 0x0f, 0x27, 0xdb, 0x09, 0x49, 0xaa, 0xf7, 0xfb, 0xdc, 0x90, 0x42, 0xc8, 0xed, 0x28, 0x45, 0x4d, 0xe7, 0x7c, - 0xd5, 0xea, 0xcd, 0x21, 0xd6, 0xc5, 0x0c, 0x75, 0xcf, 0x40, 0x49, 0xdb, 0xce, 0x16, 0xe8, 0xf6, 0x09, 0xff, 0xf8, - 0xcb, 0x40, 0x13, 0x14, 0xcd, 0x1a, 0xb0, 0xa4, 0x00, 0xdc, 0xc6, 0x9c, 0xef, 0x35, 0x4f, 0xa9, 0xa2, 0xba, 0x83, - 0x30, 0x77, 0xd8, 0x90, 0x62, 0x54, 0xf7, 0xe1, 0x84, 0x05, 0x41, 0xbc, 0xf6, 0x44, 0x0e, 0x22, 0x3d, 0xa8, 0x4f, - 0x3a, 0x10, 0x32, 0xeb, 0x81, 0xb3, 0x86, 0x55, 0xd2, 0xad, 0xae, 0x59, 0xd7, 0x19, 0x7f, 0xf2, 0x43, 0xd6, 0xd9, - 0x40, 0xff, 0x64, 0xa3, 0xa4, 0x73, 0x5d, 0x44, 0x04, 0x4f, 0xe2, 0x65, 0x0e, 0x94, 0xe7, 0x3d, 0x4d, 0x39, 0xb5, - 0xfc, 0xf8, 0xef, 0x5b, 0x32, 0x87, 0xfa, 0x92, 0x35, 0xf9, 0x7b, 0xa7, 0x3f, 0x59, 0x44, 0x5e, 0x31, 0x35, 0x5f, - 0x2d, 0x26, 0x2b, 0x2f, 0x32, 0xce, 0x29, 0x91, 0x0a, 0x4e, 0xad, 0xe8, 0x7c, 0x22, 0x97, 0xd8, 0xc6, 0x1f, 0x04, - 0x32, 0x67, 0x8f, 0xec, 0x3d, 0x3b, 0xa8, 0x18, 0x2d, 0xa1, 0x20, 0x66, 0x51, 0x35, 0xf0, 0xed, 0xc1, 0x9b, 0x31, - 0xb3, 0xe7, 0xa4, 0x40, 0x8b, 0x51, 0x60, 0xcb, 0x85, 0x18, 0x0d, 0xf1, 0xaa, 0x64, 0xae, 0x24, 0xe1, 0xcf, 0x96, - 0x99, 0x12, 0x3f, 0x64, 0xa5, 0x0e, 0xee, 0xbc, 0x58, 0xb9, 0x64, 0xb9, 0x7c, 0x7e, 0xfd, 0x08, 0xec, 0x7a, 0xef, - 0x11, 0x31, 0xe3, 0xf5, 0x93, 0x05, 0xbb, 0x56, 0x80, 0x12, 0x19, 0xdd, 0x30, 0xee, 0x22, 0xa1, 0x46, 0xd9, 0x61, - 0x74, 0x05, 0x2a, 0x8e, 0x75, 0x2a, 0x0a, 0x00, 0xfe, 0x78, 0x3d, 0x54, 0x2e, 0x70, 0x7f, 0x3c, 0x11, 0x80, 0x32, - 0xca, 0xf4, 0x9d, 0xc9, 0x18, 0x10, 0x1d, 0x35, 0x13, 0xf8, 0x37, 0x61, 0xac, 0x9e, 0xfb, 0xec, 0xf8, 0x28, 0xee, - 0x65, 0x23, 0x0c, 0x34, 0x96, 0x65, 0x93, 0xcd, 0xba, 0x75, 0x5b, 0xe1, 0x4f, 0xc5, 0x0a, 0xa4, 0x29, 0x40, 0xf3, - 0x31, 0x6d, 0x04, 0x9c, 0x81, 0x31, 0xfb, 0x32, 0x81, 0x9a, 0x2a, 0x18, 0x47, 0x5f, 0x5b, 0x36, 0x3c, 0x1f, 0xd5, - 0xdd, 0x0f, 0x2e, 0x73, 0x81, 0x50, 0x16, 0x0b, 0x6c, 0x7b, 0xa1, 0x4e, 0xfc, 0x56, 0x90, 0x79, 0x7c, 0x5f, 0x0d, - 0x8b, 0x36, 0x1d, 0x2d, 0x2b, 0x2b, 0xac, 0x0f, 0x7a, 0xb4, 0x47, 0xb0, 0x1a, 0x29, 0x5a, 0xcf, 0x71, 0xb7, 0x02, - 0x1b, 0xd1, 0xe3, 0xd4, 0x60, 0xf5, 0x83, 0x49, 0x81, 0xe4, 0x60, 0xc8, 0xb6, 0x23, 0x96, 0x1a, 0x18, 0x82, 0x9a, - 0x97, 0xa7, 0x00, 0x6b, 0xa4, 0x76, 0xd3, 0xd2, 0x68, 0xf2, 0xaf, 0xda, 0xa2, 0xdf, 0xfa, 0x37, 0xb3, 0xde, 0x35, - 0x42, 0x24, 0xdb, 0xc3, 0xf9, 0xec, 0x0c, 0x2d, 0x98, 0x41, 0xa3, 0x22, 0xb4, 0x87, 0x50, 0x6a, 0x4e, 0x23, 0x31, - 0xa8, 0x85, 0x10, 0xd9, 0x9f, 0xb8, 0xb7, 0x9c, 0xf0, 0x3c, 0xe0, 0x1e, 0x9e, 0x95, 0x34, 0xe9, 0x34, 0x5f, 0x2a, - 0x23, 0xb8, 0x2b, 0x70, 0x8a, 0x12, 0xcc, 0x16, 0xf4, 0x4f, 0x7e, 0x7b, 0x57, 0x8a, 0x18, 0xae, 0x0b, 0x08, 0xa5, - 0xcf, 0x9e, 0x11, 0x45, 0xbb, 0xc8, 0x88, 0x56, 0x25, 0x4b, 0x70, 0x81, 0xec, 0x23, 0xdb, 0xcf, 0x46, 0x16, 0xcc, - 0x1a, 0xf6, 0x53, 0xdd, 0x88, 0xf6, 0x21, 0x30, 0x23, 0x36, 0xc7, 0x5e, 0x4f, 0x9e, 0x40, 0x43, 0xf4, 0xb0, 0x64, - 0x5a, 0x17, 0xb4, 0x74, 0x95, 0x62, 0xa5, 0x42, 0x37, 0xf1, 0xa8, 0x1f, 0xa9, 0x51, 0xab, 0xe5, 0xed, 0x10, 0x7d, - 0x04, 0x6b, 0x5e, 0xef, 0x9f, 0xe2, 0x5d, 0x43, 0x81, 0x98, 0x85, 0x3b, 0x56, 0xd6, 0x58, 0xd9, 0x63, 0x61, 0xe2, - 0xf0, 0x8d, 0x10, 0x0b, 0x4f, 0x85, 0xde, 0x4f, 0xf3, 0xbf, 0x36, 0x78, 0xf5, 0xb5, 0x50, 0xd6, 0x04, 0xe5, 0x87, - 0xc1, 0xc2, 0x99, 0x0b, 0x7c, 0x8c, 0xb1, 0xd3, 0xe1, 0x37, 0x8a, 0x68, 0x83, 0x44, 0x4b, 0x8a, 0x61, 0x0b, 0xb7, - 0x57, 0x12, 0x57, 0x49, 0x15, 0x1c, 0x45, 0x18, 0x5f, 0x70, 0xeb, 0xf1, 0x4b, 0xd6, 0x18, 0x13, 0x8e, 0xce, 0x39, - 0x28, 0x5b, 0x11, 0x12, 0xcc, 0x02, 0x9b, 0xf4, 0x70, 0x83, 0x65, 0x5a, 0x21, 0x25, 0x08, 0x31, 0xc9, 0x74, 0x3f, - 0x86, 0xa1, 0x12, 0x5b, 0x05, 0x41, 0x46, 0x65, 0x76, 0xe8, 0xc4, 0x19, 0x6d, 0x71, 0x98, 0x62, 0x8d, 0xf0, 0xa9, - 0xa6, 0x17, 0x21, 0x4a, 0x22, 0xef, 0x99, 0x5d, 0x63, 0x98, 0x40, 0x2b, 0x32, 0x55, 0x32, 0xfa, 0x2a, 0x06, 0xdc, - 0xfa, 0x6b, 0xed, 0x43, 0xc1, 0x3a, 0xb8, 0x86, 0x5e, 0xaa, 0xe2, 0xaf, 0x4e, 0xa1, 0x55, 0xea, 0x92, 0x54, 0x49, - 0x4f, 0xa6, 0x90, 0xe6, 0xbc, 0x82, 0x1e, 0xce, 0x79, 0x88, 0xb7, 0x02, 0xde, 0x2a, 0xf8, 0x04, 0x5a, 0xd2, 0x08, - 0xf7, 0x2d, 0xbb, 0xda, 0x3e, 0x2b, 0x91, 0xed, 0xe7, 0xe6, 0x64, 0xc4, 0xb9, 0x0e, 0x34, 0x7a, 0x0e, 0x0b, 0x2f, - 0x83, 0x16, 0x7d, 0xa7, 0x06, 0xee, 0x4a, 0x44, 0xdf, 0xfa, 0x43, 0x0b, 0x8a, 0x35, 0xab, 0x54, 0xc0, 0x9e, 0xa9, - 0xf7, 0x23, 0x21, 0xf1, 0x58, 0xfe, 0xb1, 0xa7, 0xc7, 0x24, 0x51, 0xb5, 0x3c, 0x81, 0x91, 0x08, 0x51, 0x93, 0x41, - 0xd6, 0xfa, 0x04, 0x83, 0xae, 0x59, 0xae, 0x52, 0x73, 0x85, 0x30, 0x87, 0x32, 0xdd, 0xd5, 0xda, 0x2e, 0x00, 0x4e, - 0x5f, 0xad, 0xe7, 0x2b, 0xd0, 0x69, 0x61, 0x06, 0x28, 0x71, 0xa6, 0x47, 0x75, 0xc6, 0xc1, 0xa9, 0x6e, 0x11, 0xff, - 0xeb, 0x95, 0x4a, 0x58, 0x7b, 0xf0, 0x70, 0x50, 0xf1, 0xa4, 0x82, 0xfc, 0x6c, 0xa0, 0x29, 0x0d, 0x03, 0x52, 0x70, - 0x4e, 0x62, 0x57, 0x2c, 0xa7, 0x8b, 0x47, 0x5e, 0x19, 0x23, 0x9c, 0xc0, 0xba, 0xd3, 0xa7, 0xd3, 0x41, 0x31, 0x2e, - 0xd1, 0x52, 0x17, 0x35, 0xe7, 0xd6, 0x49, 0x5a, 0xee, 0x40, 0xf1, 0x57, 0x96, 0xa8, 0x6b, 0x91, 0x4e, 0x96, 0x2d, - 0xae, 0xea, 0xab, 0x31, 0xed, 0x82, 0x08, 0x2b, 0x6a, 0xe4, 0xd6, 0x42, 0x9d, 0xed, 0x77, 0x5e, 0xde, 0x50, 0x8c, - 0xe3, 0x39, 0xbf, 0xd6, 0xca, 0xc3, 0xb3, 0x96, 0x52, 0x2f, 0xde, 0x32, 0x47, 0xd3, 0x89, 0x35, 0x5f, 0x6a, 0x84, - 0x67, 0xe2, 0x2e, 0x22, 0xc3, 0x68, 0x80, 0xe9, 0xdb, 0xaa, 0x45, 0x2c, 0xa4, 0x1d, 0x40, 0x3f, 0x17, 0xd4, 0x39, - 0x00, 0x0c, 0x45, 0x28, 0x3b, 0x00, 0xae, 0x42, 0xb5, 0x5e, 0xcf, 0x2b, 0x6d, 0x6c, 0xf6, 0x27, 0x72, 0x42, 0x10, - 0x56, 0xbc, 0xa4, 0x50, 0x0a, 0x99, 0x40, 0x5e, 0xe2, 0x52, 0x95, 0xdc, 0x4e, 0xcb, 0x66, 0xd3, 0xb9, 0xc3, 0x37, - 0xd2, 0x00, 0x44, 0x4d, 0x5a, 0x66, 0xb2, 0x81, 0x0d, 0x55, 0xca, 0x94, 0xa7, 0x49, 0xad, 0x06, 0x5c, 0xf3, 0xc1, - 0x35, 0x70, 0x24, 0xe0, 0xec, 0xc0, 0xb5, 0x20, 0x0e, 0xbb, 0x66, 0xc8, 0x35, 0x75, 0x4e, 0x79, 0x8c, 0xfe, 0x6b, - 0xab, 0x35, 0xb6, 0x5f, 0x7d, 0x2d, 0x4d, 0xde, 0x4f, 0xc7, 0x48, 0x2b, 0x03, 0x52, 0x3b, 0xf9, 0xbf, 0x36, 0xa4, - 0x9c, 0xfd, 0x58, 0x88, 0xb5, 0xff, 0x9b, 0x91, 0x39, 0x9f, 0x57, 0xcf, 0x0e, 0x13, 0x37, 0x18, 0x53, 0x21, 0x8e, - 0x71, 0x12, 0x5e, 0x6c, 0x87, 0x57, 0x8d, 0x41, 0xed, 0xd7, 0x0b, 0x18, 0x72, 0xdc, 0xb1, 0xf7, 0x1e, 0x18, 0xce, - 0xbe, 0xd8, 0x5b, 0x34, 0x56, 0x87, 0xb4, 0x28, 0x96, 0x7d, 0x00, 0xe9, 0x67, 0xf9, 0xfe, 0x7f, 0xdc, 0xdc, 0xa5, - 0x41, 0x2d, 0x23, 0x2f, 0x71, 0xc9, 0x42, 0xb3, 0xfc, 0x5e, 0x52, 0xac, 0x4f, 0x1b, 0xe1, 0x12, 0xcd, 0x95, 0xd5, - 0xff, 0x82, 0x65, 0xcb, 0xea, 0x2e, 0xe5, 0xe1, 0xde, 0x81, 0x09, 0x8d, 0x6f, 0x6e, 0x7c, 0x4c, 0x9d, 0x35, 0x95, - 0x6e, 0xc6, 0xbb, 0x38, 0xc4, 0xae, 0xb7, 0x8d, 0x2a, 0xb6, 0x8b, 0x8c, 0xa9, 0x68, 0x6a, 0xf5, 0xd1, 0x0c, 0x22, - 0x37, 0xb4, 0xa0, 0xfd, 0x5b, 0x4c, 0x3a, 0x58, 0x3c, 0x28, 0xc3, 0xa5, 0xd1, 0xf2, 0xba, 0x10, 0x3b, 0x0a, 0x2e, - 0xc9, 0x48, 0x4a, 0x12, 0x64, 0x48, 0xf7, 0x1d, 0x17, 0x0f, 0x9a, 0x42, 0xd5, 0x88, 0xdb, 0x95, 0x64, 0xbf, 0xe2, - 0xfe, 0xa5, 0x7e, 0xdc, 0x30, 0xea, 0xca, 0x39, 0x50, 0x89, 0xcf, 0x9a, 0x6c, 0x4e, 0x88, 0x8e, 0xda, 0x36, 0xeb, - 0x28, 0xca, 0x91, 0x5f, 0x29, 0x25, 0xea, 0x5f, 0xd1, 0x1b, 0x48, 0xb6, 0x08, 0x60, 0x60, 0x1b, 0x80, 0xd5, 0x6f, - 0xd6, 0x2c, 0xd5, 0x32, 0x40, 0xe3, 0x57, 0xb0, 0x6b, 0x3e, 0x3e, 0x75, 0x37, 0xfa, 0x05, 0xd1, 0xd8, 0x5a, 0xd1, - 0x04, 0x97, 0xdd, 0x0b, 0xab, 0x37, 0xe2, 0xf7, 0xd4, 0xdb, 0x23, 0x88, 0x0d, 0xe4, 0xd3, 0x74, 0xbf, 0x4b, 0x4d, - 0x1f, 0x90, 0xf4, 0x3d, 0x18, 0x63, 0x1f, 0x83, 0x5d, 0x51, 0x4f, 0xad, 0xde, 0x54, 0x95, 0x43, 0x20, 0xf7, 0x74, - 0x35, 0x2a, 0xe6, 0xf1, 0x57, 0xb4, 0x5b, 0x6b, 0xd9, 0x61, 0x78, 0x95, 0x2f, 0xa0, 0x6c, 0xd1, 0xae, 0x29, 0x22, - 0xc9, 0x65, 0x8c, 0x4b, 0x15, 0x80, 0x12, 0x58, 0x90, 0x93, 0x1a, 0xbb, 0x3a, 0xdd, 0xb2, 0x79, 0xf9, 0x3a, 0x9a, - 0x90, 0x6f, 0xfd, 0xb4, 0xf2, 0xb9, 0x19, 0x1c, 0x55, 0xd4, 0x21, 0x02, 0xd3, 0x40, 0x1d, 0x16, 0x70, 0x18, 0xa9, - 0xf3, 0x52, 0x04, 0x0e, 0x78, 0x37, 0xe8, 0x73, 0xcd, 0x40, 0x51, 0x70, 0x88, 0xbc, 0x8b, 0x1a, 0x2c, 0xf0, 0x1c, - 0x3c, 0x49, 0xb4, 0x71, 0x74, 0xf8, 0xef, 0x82, 0x8e, 0xa2, 0x43, 0xb2, 0x94, 0xf5, 0xbd, 0x32, 0x15, 0xc9, 0x49, - 0xea, 0x22, 0xe9, 0xfc, 0x54, 0x9e, 0xa9, 0x4d, 0x6e, 0xcd, 0x5f, 0x24, 0x9f, 0xc6, 0xc9, 0xd8, 0x0b, 0xd8, 0xaf, - 0x61, 0xc4, 0xae, 0xf3, 0x17, 0x36, 0x9f, 0xf6, 0xcc, 0xb2, 0x46, 0xab, 0x33, 0xe0, 0x81, 0xa4, 0x13, 0x61, 0x29, - 0xbb, 0x64, 0x2e, 0x65, 0x00, 0x28, 0xd7, 0xc6, 0xcb, 0xbb, 0x21, 0xc4, 0xe7, 0xe2, 0xfa, 0x8e, 0x48, 0xa8, 0x4c, - 0x35, 0x33, 0xe3, 0xb9, 0x47, 0x11, 0x21, 0x2c, 0xd5, 0xce, 0x2c, 0x6e, 0xb3, 0xed, 0xed, 0x6c, 0x78, 0x5e, 0xb3, - 0xfd, 0xb1, 0xc0, 0x14, 0xf5, 0xa0, 0xbf, 0x8b, 0x8b, 0xa4, 0xca, 0x20, 0x44, 0xcc, 0xe0, 0x03, 0xae, 0x86, 0xb0, - 0x4b, 0xa5, 0xa2, 0x3f, 0xdb, 0x25, 0x8a, 0x9f, 0x5e, 0xa5, 0xaa, 0xc2, 0xe5, 0x48, 0xc8, 0xc4, 0x96, 0xda, 0x80, - 0x25, 0x02, 0x3c, 0xf2, 0xe4, 0x16, 0xb7, 0x65, 0xb9, 0x1b, 0x11, 0x9c, 0x16, 0x2d, 0x9d, 0x9c, 0xb0, 0x4c, 0xe8, - 0x3b, 0xd9, 0xf5, 0xae, 0x29, 0xc2, 0xec, 0x7e, 0x93, 0x6e, 0x7f, 0x94, 0xd2, 0x57, 0x95, 0xc6, 0x1d, 0xb8, 0xc6, - 0x12, 0xb8, 0xf0, 0x18, 0xd1, 0x6a, 0x68, 0x54, 0x9f, 0x7a, 0x44, 0xf1, 0xa8, 0xd6, 0x24, 0xc7, 0x41, 0xeb, 0x30, - 0x71, 0xa5, 0xa5, 0x81, 0xe2, 0x42, 0xec, 0xac, 0x43, 0x76, 0x3a, 0x0b, 0xf9, 0x92, 0x73, 0xb3, 0x75, 0x92, 0xc8, - 0x17, 0xb5, 0x0f, 0x45, 0x33, 0x12, 0x73, 0xf5, 0x5d, 0x7e, 0xce, 0xd1, 0x8f, 0x77, 0x57, 0xf9, 0x8a, 0xb7, 0x8e, - 0x73, 0x92, 0xcc, 0x27, 0xf1, 0xa2, 0xe5, 0x9f, 0xcb, 0xd2, 0x46, 0x0b, 0x4f, 0xe2, 0xd1, 0x0f, 0xa7, 0x8a, 0xfa, - 0x35, 0x12, 0x9a, 0x75, 0x52, 0xeb, 0x59, 0x79, 0x25, 0xe5, 0x7c, 0xb7, 0x47, 0x8a, 0x25, 0x62, 0x8e, 0x71, 0xb9, - 0xe4, 0x69, 0x55, 0x2d, 0x1d, 0x7d, 0x7f, 0x86, 0xe7, 0x52, 0x76, 0x02, 0x60, 0x22, 0xa9, 0x8f, 0xb0, 0xa2, 0xbd, - 0x8c, 0x1a, 0x21, 0xf6, 0x82, 0xd1, 0xb2, 0x84, 0x17, 0xfb, 0xcd, 0xad, 0x07, 0x21, 0x5b, 0x92, 0xee, 0xee, 0x2d, - 0x08, 0x5f, 0xf0, 0xd3, 0x03, 0xa7, 0x75, 0xa4, 0x26, 0x2f, 0xce, 0x42, 0x94, 0x78, 0x89, 0x74, 0x18, 0xb5, 0xb5, - 0x9c, 0x9b, 0xb0, 0x92, 0x34, 0x86, 0xdc, 0x1a, 0x65, 0xd5, 0xb0, 0xa5, 0x18, 0x73, 0x20, 0xe3, 0x91, 0x79, 0x06, - 0xfa, 0x1e, 0xe0, 0x4d, 0x6e, 0x6d, 0x49, 0xb2, 0xee, 0x9e, 0xca, 0xc8, 0xbc, 0xe8, 0xb3, 0xe4, 0xfc, 0xb8, 0x95, - 0xd8, 0x50, 0xdc, 0x49, 0xb9, 0x62, 0x3d, 0x71, 0x90, 0x5d, 0x9a, 0xbc, 0x2f, 0x51, 0x44, 0xc9, 0x4a, 0xa7, 0xff, - 0x39, 0x37, 0x1c, 0x77, 0x3a, 0x34, 0xd1, 0xea, 0xd8, 0x76, 0x68, 0x25, 0xe6, 0xe1, 0xd7, 0xe5, 0x9a, 0xaa, 0x05, - 0xb4, 0x80, 0x39, 0x22, 0x4a, 0xdd, 0x0c, 0x71, 0x93, 0xa4, 0xe3, 0x05, 0xc2, 0xdf, 0x6e, 0x33, 0x13, 0x5f, 0x76, - 0x7f, 0x97, 0x23, 0x34, 0x35, 0x94, 0xe4, 0x01, 0x14, 0x97, 0x6f, 0xc2, 0x9b, 0x31, 0x15, 0xf1, 0x0d, 0xfb, 0xcc, - 0x59, 0x6a, 0x0f, 0x5e, 0xa0, 0x4d, 0x4f, 0x82, 0xd5, 0x89, 0x1b, 0x40, 0x89, 0x4c, 0x10, 0x20, 0xbb, 0xc7, 0x00, - 0x96, 0x06, 0xd9, 0x8b, 0x26, 0xd3, 0x00, 0x22, 0x5b, 0x8c, 0xad, 0x61, 0x8e, 0xcd, 0x15, 0xa0, 0x05, 0x3b, 0xf3, - 0x4b, 0xa0, 0x6c, 0x60, 0x87, 0x77, 0xf4, 0x3f, 0x79, 0x43, 0x0c, 0x31, 0xa6, 0xa9, 0x4d, 0x3b, 0xeb, 0x55, 0x90, - 0xbd, 0xeb, 0x63, 0x16, 0x07, 0xe4, 0xc0, 0x75, 0x43, 0x69, 0xec, 0x8c, 0xd5, 0x35, 0x0d, 0x68, 0xa5, 0xa8, 0xae, - 0x08, 0x84, 0x24, 0x10, 0xf3, 0xf2, 0x50, 0x48, 0x4d, 0x42, 0xb5, 0x74, 0xd3, 0xa9, 0x6d, 0x82, 0xc2, 0xec, 0x78, - 0x6a, 0xf2, 0xd0, 0x4b, 0xe0, 0xed, 0xdb, 0x5b, 0x8b, 0x01, 0x47, 0xe1, 0x4a, 0x96, 0x32, 0xea, 0x57, 0xe6, 0xcd, - 0x7a, 0x58, 0xcb, 0x5f, 0x1c, 0xd0, 0x6e, 0x57, 0x8e, 0x19, 0xd5, 0x4e, 0xf5, 0x42, 0x70, 0x7a, 0x67, 0x80, 0x46, - 0x44, 0x02, 0x6c, 0xe0, 0x47, 0xfd, 0x8e, 0x54, 0x2c, 0x51, 0xd6, 0x56, 0x5e, 0xcd, 0xfa, 0x03, 0x16, 0x22, 0x8d, - 0x2b, 0x6c, 0x9c, 0xb3, 0x68, 0x55, 0x23, 0x99, 0x90, 0xa0, 0x07, 0x32, 0xb2, 0x73, 0x56, 0x93, 0xe0, 0xeb, 0x94, - 0x06, 0x5f, 0x70, 0x7a, 0xfc, 0xb5, 0x0e, 0x50, 0x8e, 0x7f, 0x71, 0xf6, 0xba, 0x57, 0xe1, 0x88, 0xeb, 0x11, 0xf3, - 0x45, 0x9d, 0x97, 0x3f, 0xdc, 0x19, 0x39, 0xfd, 0x7b, 0xcb, 0x0c, 0x40, 0x95, 0xbf, 0x58, 0x26, 0x80, 0x54, 0x1e, - 0xdc, 0x79, 0x23, 0x3a, 0x4b, 0x76, 0x14, 0x8d, 0x59, 0x3b, 0x69, 0x09, 0x3b, 0x98, 0x15, 0x47, 0x10, 0x2a, 0xfe, - 0xc5, 0x08, 0x20, 0x71, 0x14, 0xb4, 0xec, 0x68, 0xd0, 0x8a, 0xf6, 0x40, 0x9d, 0x93, 0x12, 0x61, 0x63, 0x5e, 0x88, - 0x0d, 0xf9, 0xfa, 0xe6, 0x04, 0x07, 0x59, 0x43, 0x12, 0x3c, 0xa8, 0xb7, 0x6f, 0x8a, 0x6c, 0x97, 0x1d, 0xa6, 0xde, - 0xf4, 0xf8, 0x3d, 0x97, 0x80, 0x90, 0x16, 0x0f, 0x91, 0x8f, 0xdd, 0x48, 0xcc, 0x6e, 0x3d, 0xde, 0x76, 0xc4, 0xa2, - 0x6f, 0x27, 0xa2, 0x54, 0xea, 0xb8, 0x36, 0x0f, 0x51, 0x10, 0x56, 0x18, 0x4a, 0x70, 0xf9, 0x55, 0x40, 0x6c, 0xa2, - 0xa0, 0xb1, 0x8f, 0xe5, 0x4c, 0x39, 0x6c, 0xb2, 0x0f, 0xe7, 0x2f, 0x75, 0xad, 0xff, 0x08, 0xb5, 0xce, 0x9e, 0xc0, - 0xaf, 0x18, 0xd8, 0x7b, 0x28, 0x83, 0x75, 0x4a, 0xdc, 0xb5, 0xe0, 0xa1, 0x0c, 0xca, 0x7d, 0x18, 0x48, 0x08, 0xc5, - 0xf5, 0x71, 0xd8, 0x14, 0xbb, 0x96, 0x18, 0x01, 0x3e, 0x4a, 0x66, 0xa5, 0x36, 0x1d, 0xc3, 0x95, 0x30, 0xe0, 0xf2, - 0x52, 0x8f, 0xe7, 0xa3, 0x9b, 0xdd, 0x95, 0x46, 0x1a, 0xfa, 0x6e, 0xe0, 0x78, 0xb9, 0x39, 0x4c, 0x95, 0x45, 0x5b, - 0x37, 0x25, 0x2c, 0x75, 0x81, 0xc8, 0x8c, 0x10, 0x31, 0xb7, 0x6c, 0xd2, 0x90, 0x38, 0xdb, 0xe9, 0x04, 0x7d, 0x6c, - 0x60, 0x38, 0x83, 0xd9, 0x54, 0xb5, 0xb5, 0x7b, 0x53, 0x58, 0xff, 0xcc, 0xb2, 0x0d, 0xfc, 0x7c, 0x39, 0x23, 0x21, - 0xa0, 0x61, 0xa1, 0x17, 0x11, 0xc2, 0x7a, 0x38, 0xca, 0xb3, 0x97, 0xd8, 0x70, 0x21, 0x43, 0x87, 0xe3, 0x87, 0x63, - 0xb3, 0x17, 0x34, 0xc7, 0xcf, 0xa7, 0xc7, 0xc6, 0xbe, 0x56, 0xd3, 0x24, 0x0b, 0x2e, 0x65, 0xe1, 0x74, 0xfd, 0xc8, - 0x11, 0xc5, 0x67, 0xda, 0x75, 0xdf, 0xc1, 0xe6, 0x33, 0x29, 0x73, 0x52, 0xe9, 0x26, 0x02, 0x95, 0x81, 0x4c, 0xde, - 0xed, 0x05, 0xc0, 0xb6, 0x01, 0xfa, 0xa2, 0xb9, 0xc8, 0x4c, 0x65, 0x9f, 0x74, 0x5e, 0x1e, 0x22, 0x65, 0x7b, 0x78, - 0x73, 0x58, 0x86, 0x80, 0xd7, 0xa7, 0x35, 0xfb, 0x37, 0x3c, 0x0d, 0xd2, 0x75, 0xb4, 0x31, 0x2a, 0x92, 0xe6, 0x82, - 0xc9, 0x35, 0x2a, 0xa6, 0xc1, 0x43, 0x38, 0x69, 0xc0, 0xa9, 0x52, 0x4e, 0xb8, 0x20, 0x79, 0x81, 0x49, 0x2a, 0xf6, - 0x04, 0x5a, 0xdb, 0x40, 0x44, 0x45, 0x0d, 0x3f, 0xba, 0x8c, 0x8b, 0x47, 0x69, 0x67, 0x4f, 0xa2, 0xa2, 0xfe, 0xda, - 0x7b, 0xd2, 0x0a, 0x61, 0x9f, 0x53, 0xdd, 0xf5, 0x1a, 0x8f, 0xcd, 0x08, 0x8a, 0x5e, 0xd3, 0xd4, 0xff, 0x65, 0x18, - 0x84, 0xbb, 0xcb, 0x76, 0x0e, 0x82, 0x82, 0x1c, 0x21, 0xc0, 0x4f, 0x5e, 0xd0, 0x97, 0x00, 0x6b, 0xe8, 0x88, 0x03, - 0x79, 0x7e, 0x6d, 0x8f, 0xa4, 0x73, 0xf5, 0xd5, 0xb9, 0xef, 0x7f, 0xc5, 0xd1, 0x1a, 0xef, 0x9f, 0x61, 0xec, 0x1f, - 0x9f, 0x29, 0x6d, 0xce, 0x1e, 0x33, 0xf1, 0xe8, 0x44, 0xf6, 0xb7, 0x8d, 0x49, 0x8a, 0xb7, 0xc7, 0x4a, 0x81, 0x7f, - 0xf8, 0x40, 0xf2, 0x36, 0x0b, 0xe7, 0x46, 0x12, 0xf3, 0xdb, 0xd9, 0xaa, 0x93, 0x9f, 0x1c, 0xd7, 0xca, 0x7d, 0xd6, - 0x24, 0x7f, 0xcc, 0xa5, 0x1d, 0xf0, 0x9d, 0xab, 0xce, 0xce, 0xad, 0xe4, 0xd6, 0x38, 0xe7, 0xf8, 0xcd, 0xb7, 0xbb, - 0x55, 0xea, 0xcd, 0xff, 0x95, 0xd5, 0xe2, 0x3a, 0x75, 0xc3, 0x25, 0xde, 0x40, 0x41, 0x50, 0xb8, 0xc3, 0x3a, 0xbd, - 0xcc, 0x5d, 0xe3, 0x0e, 0xa3, 0xc1, 0xda, 0xfa, 0xaa, 0xc8, 0x3c, 0x32, 0x17, 0x31, 0xce, 0x67, 0xe2, 0x65, 0x35, - 0x65, 0xdb, 0xa0, 0xdf, 0x35, 0x15, 0xe6, 0x3f, 0xbf, 0x86, 0x3a, 0xdb, 0xb1, 0xf9, 0x53, 0xe5, 0xdf, 0x80, 0x6b, - 0xe8, 0x50, 0x8e, 0xa2, 0xe0, 0xc4, 0x75, 0xcd, 0xb4, 0x4d, 0xce, 0xb5, 0x70, 0x5c, 0xbb, 0x1c, 0x78, 0xb5, 0x49, - 0x9c, 0x41, 0x94, 0x56, 0xc6, 0x3d, 0xa7, 0x4f, 0xbb, 0xfc, 0xce, 0xb8, 0x63, 0xd8, 0x75, 0x30, 0x0a, 0x82, 0x01, - 0x25, 0x16, 0x6d, 0x50, 0x77, 0x32, 0xba, 0x9a, 0xd8, 0xb3, 0x06, 0x62, 0x09, 0xac, 0x68, 0x7e, 0xab, 0x04, 0xa0, - 0xa5, 0x1d, 0x78, 0x59, 0xaf, 0x12, 0xc9, 0x92, 0xd5, 0x37, 0x0e, 0xe6, 0x7f, 0x88, 0x50, 0x04, 0xe7, 0xdb, 0x38, - 0xc4, 0x8b, 0x4a, 0x91, 0x98, 0x53, 0xec, 0xd1, 0x9b, 0xec, 0xa3, 0x5e, 0x82, 0x34, 0xff, 0x06, 0x18, 0x20, 0x60, - 0xc3, 0x71, 0x2c, 0x10, 0x94, 0xcc, 0xcf, 0xf1, 0xe5, 0xce, 0x5e, 0xbe, 0xf9, 0x04, 0x53, 0xfb, 0x37, 0x9e, 0xdb, - 0xc8, 0xfd, 0xdb, 0x50, 0xc9, 0xed, 0xcf, 0x2c, 0xee, 0xff, 0x2c, 0x9e, 0xdd, 0xbf, 0xe5, 0x1f, 0xbf, 0x6e, 0x5a, - 0xe0, 0x9d, 0xce, 0xfb, 0x48, 0x02, 0x35, 0x3f, 0x5f, 0x67, 0xa4, 0x60, 0x18, 0x11, 0x7c, 0xed, 0xf8, 0x30, 0xa2, - 0xfb, 0xad, 0x67, 0x03, 0x6b, 0x62, 0x1f, 0xe3, 0x16, 0xd5, 0xeb, 0x79, 0x81, 0xed, 0x6a, 0x5c, 0xcb, 0xf4, 0xb2, - 0xd0, 0x9a, 0xa7, 0xca, 0x2e, 0x15, 0x1a, 0x09, 0x07, 0x5b, 0xc0, 0x3b, 0xb8, 0xeb, 0xca, 0x9d, 0xbd, 0xb4, 0x66, - 0x36, 0xe5, 0x49, 0x82, 0x9c, 0x0e, 0x5c, 0x34, 0x7d, 0xf5, 0xd4, 0xb6, 0xc4, 0x18, 0xfe, 0x9c, 0x37, 0xd5, 0x18, - 0x15, 0x3d, 0x46, 0x23, 0x19, 0xb1, 0x72, 0x56, 0x46, 0xcb, 0x8b, 0x61, 0x68, 0x4b, 0xc6, 0xc5, 0xac, 0xb2, 0xa0, - 0x0c, 0xb8, 0x07, 0x42, 0xd6, 0x0b, 0xfa, 0xd6, 0x4e, 0x91, 0x0f, 0xd5, 0x27, 0x1c, 0xb0, 0x79, 0x3c, 0xc1, 0x71, - 0xe9, 0xa3, 0x7a, 0x40, 0x9a, 0xc4, 0x55, 0xb8, 0x86, 0xb3, 0xc9, 0xaa, 0x8a, 0xe7, 0xcd, 0x4f, 0xdb, 0x00, 0x73, - 0x5a, 0xb0, 0x7f, 0x73, 0x5b, 0xa2, 0xdc, 0x4f, 0x82, 0xda, 0x2e, 0x1a, 0x55, 0x8d, 0xb2, 0x00, 0xa2, 0x4c, 0x9f, - 0xde, 0x40, 0x02, 0xd1, 0x39, 0xd5, 0x8b, 0xe6, 0xdb, 0xd4, 0x76, 0x38, 0x37, 0x45, 0xa0, 0x16, 0x2e, 0x8d, 0xd1, - 0x6c, 0xa6, 0x70, 0xc2, 0x7b, 0x97, 0xf6, 0x3c, 0x5d, 0x20, 0x4f, 0xb6, 0x80, 0x49, 0xdf, 0x0b, 0x3c, 0x6b, 0x00, - 0x0f, 0x08, 0xf4, 0x28, 0xaa, 0xd0, 0xc0, 0x9a, 0x82, 0x1d, 0x8c, 0x8a, 0x34, 0x0e, 0x80, 0x64, 0x9f, 0x46, 0xdc, - 0x80, 0x83, 0x73, 0x34, 0x86, 0x8e, 0x6d, 0xcf, 0xe4, 0x95, 0x14, 0x82, 0xa0, 0xca, 0x66, 0x89, 0xcd, 0x68, 0xb2, - 0x17, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, 0x18, 0xfb, 0x9d, 0xce, 0x00, 0xa6, 0xac, 0xa2, 0x77, 0x48, 0xcd, 0x88, - 0x17, 0x3a, 0x29, 0x9a, 0x1c, 0x88, 0x48, 0x46, 0xcc, 0x75, 0xe3, 0xb7, 0x7f, 0x1e, 0xe5, 0x66, 0x63, 0x5b, 0x6c, - 0x56, 0x3c, 0x23, 0x58, 0xef, 0xe0, 0xea, 0x2c, 0xbc, 0xd6, 0x33, 0xb2, 0x50, 0xf8, 0xc7, 0x30, 0xb9, 0x53, 0xdf, - 0xf7, 0xc4, 0x88, 0xe6, 0xf2, 0x7f, 0x97, 0xb1, 0xab, 0xca, 0x69, 0x34, 0x86, 0x86, 0x48, 0x46, 0x36, 0x01, 0x48, - 0xe6, 0x59, 0xd3, 0x31, 0x9a, 0x8d, 0xd5, 0xb6, 0x73, 0x9a, 0x66, 0x3f, 0x7e, 0xe5, 0xf4, 0xd7, 0x06, 0xc7, 0x03, - 0xe4, 0xe7, 0xce, 0x8d, 0x72, 0xf6, 0x03, 0x5b, 0xcc, 0xa1, 0xc7, 0xb9, 0x5c, 0xd5, 0x37, 0x8a, 0x5c, 0x8d, 0x90, - 0x8b, 0x41, 0xdf, 0x0d, 0x2a, 0x1e, 0x10, 0x40, 0x7f, 0x02, 0x5f, 0x79, 0x79, 0xfe, 0x5f, 0xa3, 0xb9, 0xe3, 0x91, - 0x60, 0x63, 0xe5, 0x32, 0x9c, 0xc4, 0xcb, 0x61, 0x3c, 0xe0, 0xe8, 0x39, 0x91, 0xf8, 0xb4, 0x22, 0xe9, 0x11, 0x89, - 0x0c, 0xe3, 0x91, 0x59, 0x1a, 0x52, 0x9c, 0x61, 0x84, 0xe2, 0x2f, 0xab, 0xdf, 0xae, 0xbb, 0x6f, 0x20, 0xc5, 0xbf, - 0x71, 0x5d, 0x1d, 0xcf, 0x8d, 0x2a, 0x33, 0xe9, 0x65, 0x73, 0xdc, 0x92, 0xb3, 0x9a, 0x56, 0x33, 0x9f, 0xb5, 0x4b, - 0xa6, 0xed, 0xe6, 0xb1, 0x9c, 0x19, 0x3f, 0x4f, 0x13, 0xc9, 0xe0, 0x6f, 0xce, 0x61, 0x80, 0x16, 0x06, 0xda, 0x4b, - 0xec, 0xd4, 0xa0, 0xd3, 0xd5, 0x9b, 0x0d, 0x31, 0xda, 0xae, 0xd6, 0xe9, 0x07, 0x5c, 0x2f, 0xe8, 0x18, 0x76, 0xd6, - 0x74, 0xf2, 0x9c, 0x10, 0xbb, 0x28, 0xf8, 0xf1, 0xfb, 0xae, 0xa0, 0xd4, 0x34, 0xa0, 0x5f, 0xe7, 0xe5, 0xe5, 0xae, - 0x76, 0x91, 0x81, 0x9a, 0x09, 0xe8, 0x90, 0xdd, 0x30, 0xe6, 0x58, 0x17, 0x63, 0x0f, 0xd2, 0x85, 0x71, 0x6b, 0xf6, - 0x41, 0x68, 0x8c, 0xb2, 0x70, 0x65, 0x4c, 0x2e, 0x0a, 0x5f, 0x93, 0x93, 0x6b, 0xb8, 0xa0, 0x25, 0xb4, 0xcf, 0xbd, - 0x77, 0x0e, 0xd3, 0x3d, 0xc2, 0x51, 0x5b, 0x7a, 0x91, 0x16, 0xea, 0xcd, 0x42, 0x79, 0x56, 0x80, 0x16, 0x2c, 0x52, - 0x4f, 0xab, 0xe5, 0xc8, 0xe5, 0x5d, 0x3f, 0x3a, 0x3d, 0x75, 0xab, 0xb5, 0xdc, 0x63, 0x4a, 0x03, 0xe1, 0x1d, 0xad, - 0xec, 0xbf, 0xe2, 0x25, 0x47, 0x2a, 0x6c, 0xd5, 0x2c, 0x93, 0xaf, 0xc8, 0xf5, 0x3f, 0x6a, 0x7a, 0x13, 0xef, 0x13, - 0xae, 0x0a, 0x84, 0x3b, 0x12, 0xa1, 0x33, 0x9e, 0x32, 0xeb, 0x68, 0x1d, 0xaf, 0xa9, 0x13, 0x3b, 0x1e, 0x1e, 0x17, - 0x28, 0x86, 0xdf, 0x9a, 0xd1, 0x80, 0xe7, 0xbe, 0x78, 0x11, 0xec, 0x5e, 0xfb, 0x2e, 0x39, 0x33, 0x8b, 0x6c, 0x7f, - 0xd5, 0x6a, 0xdc, 0x85, 0xd8, 0xb4, 0xca, 0xdc, 0x15, 0xfb, 0xec, 0x30, 0x9c, 0x6b, 0xc6, 0x17, 0x07, 0xb7, 0x7b, - 0x23, 0x77, 0x07, 0x6f, 0x9e, 0x12, 0x47, 0xd7, 0x02, 0x1e, 0x97, 0x9b, 0xbc, 0x5b, 0x55, 0xba, 0x5f, 0x1b, 0xf5, - 0x6a, 0x5f, 0x23, 0xfb, 0x92, 0x80, 0xe4, 0x71, 0x3d, 0xa4, 0x88, 0xe3, 0xa9, 0xc8, 0xd6, 0x84, 0xb1, 0x0e, 0x0a, - 0x1e, 0x6a, 0x3d, 0xb7, 0xed, 0xa4, 0xf6, 0x83, 0x72, 0xb7, 0x2e, 0xca, 0xca, 0xc3, 0xe1, 0xb7, 0xcd, 0x8f, 0x07, - 0xee, 0x25, 0x85, 0xe2, 0xa1, 0xfa, 0x2a, 0x02, 0x03, 0xee, 0x57, 0xd4, 0x9a, 0xcc, 0x8a, 0xe3, 0x27, 0xec, 0x94, - 0xca, 0x14, 0xa3, 0x83, 0x9b, 0xb6, 0x3b, 0x4d, 0x03, 0x18, 0x1d, 0xd3, 0x75, 0xb2, 0x33, 0xf5, 0xf8, 0x84, 0x88, - 0x1c, 0x53, 0xfb, 0x9f, 0xca, 0x8b, 0xd3, 0x01, 0xde, 0x07, 0xfb, 0xaf, 0x98, 0x40, 0x6c, 0xad, 0xbb, 0x49, 0x9d, - 0x86, 0x48, 0xc1, 0x87, 0x35, 0x6d, 0xfc, 0x5f, 0x7c, 0x2e, 0x8c, 0x26, 0xa2, 0x77, 0xea, 0xad, 0x2b, 0x25, 0xb0, - 0x5d, 0xed, 0x52, 0xe8, 0xb6, 0xb7, 0xba, 0x89, 0x71, 0x59, 0xf0, 0x86, 0x4e, 0xef, 0xc8, 0xfa, 0x49, 0xd0, 0x86, - 0xdc, 0x3b, 0x75, 0x19, 0x71, 0x88, 0x91, 0x94, 0xb3, 0x8b, 0xb1, 0xd4, 0x86, 0xd0, 0xc5, 0x16, 0x65, 0x4d, 0xee, - 0xfa, 0xfb, 0x53, 0x74, 0x18, 0x5a, 0x22, 0x8d, 0xf2, 0xbc, 0x03, 0xb6, 0x5e, 0xdd, 0x29, 0xaa, 0xb8, 0x1d, 0xae, - 0x6c, 0x5d, 0x02, 0xbd, 0x4e, 0x0c, 0xbe, 0xd8, 0x51, 0x83, 0x02, 0xde, 0x08, 0xdd, 0x64, 0xd2, 0x35, 0xc4, 0x70, - 0x36, 0xce, 0x3e, 0xa6, 0x1c, 0xf3, 0x73, 0xbf, 0x34, 0x5f, 0x56, 0x52, 0x3b, 0x41, 0x4c, 0x18, 0x90, 0xeb, 0x59, - 0x71, 0xa4, 0xfa, 0xf6, 0xf4, 0xaf, 0x4d, 0xe1, 0xff, 0x8e, 0x0d, 0xdf, 0xea, 0x30, 0x22, 0x19, 0xf5, 0x0a, 0x3b, - 0x04, 0x3a, 0x83, 0xba, 0xa4, 0x30, 0xe9, 0x43, 0x40, 0x9d, 0xb8, 0xc6, 0xf5, 0x54, 0x1e, 0x21, 0xf4, 0x4d, 0x1a, - 0x54, 0xce, 0x6d, 0x3b, 0xd4, 0x5b, 0xdf, 0x13, 0x51, 0x02, 0x84, 0x47, 0xa3, 0x00, 0x5a, 0x64, 0x32, 0xb8, 0x37, - 0x38, 0x80, 0xb0, 0x2e, 0xa5, 0x9c, 0xd1, 0x5a, 0xd2, 0x75, 0x68, 0x3e, 0x6e, 0xb1, 0xbe, 0xd5, 0x09, 0x39, 0x82, - 0x54, 0xea, 0xe9, 0x53, 0x35, 0x5d, 0xa4, 0x97, 0x98, 0x2d, 0x9d, 0xf2, 0x79, 0x80, 0xd8, 0x86, 0x5e, 0x58, 0x74, - 0x9f, 0xcf, 0xe5, 0x21, 0x40, 0xa6, 0xb9, 0x04, 0x24, 0x5c, 0x52, 0x50, 0x3f, 0x02, 0x93, 0x72, 0xf9, 0x1f, 0x15, - 0xd2, 0xeb, 0xdc, 0x1d, 0xbe, 0x7a, 0xbd, 0x58, 0xd5, 0x1a, 0x59, 0xbf, 0xf1, 0x03, 0x5d, 0xe5, 0xf5, 0xaa, 0xd6, - 0x9e, 0x2f, 0xd8, 0x8c, 0x0e, 0xd2, 0x8d, 0xf4, 0x3f, 0xf9, 0x07, 0x63, 0xa9, 0xb3, 0x23, 0xfa, 0x16, 0x57, 0xe2, - 0xba, 0xaf, 0xa7, 0xf7, 0x50, 0x5e, 0x3c, 0x49, 0x93, 0x65, 0xca, 0x6a, 0xd3, 0xfa, 0xb0, 0x53, 0x04, 0x42, 0xd4, - 0xd1, 0xcb, 0xb8, 0xe4, 0xc0, 0x45, 0x59, 0xba, 0x5e, 0x80, 0x7f, 0xf6, 0x0f, 0xa3, 0x13, 0x68, 0xa0, 0xd8, 0xb0, - 0xfb, 0x7e, 0x47, 0x65, 0xe7, 0x42, 0x0e, 0x4d, 0xf4, 0xbe, 0xda, 0x29, 0x93, 0x33, 0x75, 0x67, 0x9f, 0x93, 0xe8, - 0x86, 0x3a, 0x90, 0x57, 0x06, 0x1c, 0xa7, 0x5e, 0xec, 0xf7, 0x60, 0x98, 0x15, 0xbe, 0xec, 0x59, 0xb7, 0xfd, 0x09, - 0x83, 0x29, 0xc8, 0x5a, 0xee, 0x2c, 0x8a, 0xe5, 0x5d, 0xc8, 0xab, 0xa8, 0xb1, 0x5c, 0x4c, 0xac, 0x10, 0xca, 0x02, - 0xb6, 0x9c, 0xdc, 0x8f, 0x42, 0x90, 0x7b, 0x9c, 0xe3, 0xcd, 0xce, 0x39, 0x52, 0xee, 0x12, 0xcc, 0xee, 0xb0, 0xc5, - 0x69, 0x22, 0xf5, 0xba, 0x8d, 0xe0, 0x52, 0x62, 0x4a, 0x54, 0x51, 0xe4, 0xa6, 0x98, 0xa8, 0xe2, 0xa8, 0x6b, 0x7b, - 0xa3, 0x6d, 0x23, 0x65, 0x90, 0xc8, 0x30, 0x23, 0xa4, 0xa7, 0xf9, 0xe1, 0xee, 0xcd, 0x3e, 0x99, 0x32, 0x06, 0x11, - 0xd0, 0xa8, 0x7a, 0x06, 0x10, 0x7a, 0xbe, 0xac, 0x5d, 0xf2, 0x58, 0x56, 0x30, 0x92, 0xbe, 0x02, 0x1a, 0x2e, 0x9a, - 0x74, 0xc3, 0x2f, 0xc1, 0x49, 0xac, 0x78, 0x9a, 0x00, 0x45, 0xa3, 0xad, 0xf2, 0x6c, 0x88, 0xaf, 0x3c, 0x0e, 0x3a, - 0x51, 0x4f, 0x1a, 0x14, 0xc1, 0x60, 0x9a, 0x8d, 0x24, 0xdc, 0x52, 0x93, 0x41, 0xac, 0x0c, 0xc4, 0xd1, 0xbf, 0xb4, - 0x52, 0xa6, 0x54, 0xbb, 0x5a, 0x30, 0x32, 0xa3, 0x07, 0xd3, 0xdf, 0x85, 0xa8, 0x61, 0xd5, 0x08, 0xfd, 0x45, 0xa6, - 0x4e, 0x75, 0xca, 0xc8, 0x4b, 0x8c, 0xd3, 0xc4, 0xc0, 0x18, 0x72, 0xa0, 0x71, 0xc0, 0x76, 0x03, 0x79, 0x5a, 0x73, - 0xb6, 0x8c, 0x9a, 0x49, 0xf7, 0xba, 0x76, 0xf4, 0x69, 0x6e, 0xe9, 0xfa, 0xcf, 0x65, 0xb6, 0x61, 0xc7, 0x9c, 0xbf, - 0xf4, 0xd3, 0x6e, 0xfa, 0x30, 0xc6, 0xbc, 0x19, 0x07, 0xc3, 0x0c, 0xae, 0xbf, 0x48, 0x8b, 0x47, 0x45, 0x83, 0x2c, - 0x5f, 0x6a, 0x8c, 0x23, 0xed, 0xef, 0x07, 0xaa, 0xb7, 0xbb, 0x8d, 0x49, 0xd2, 0x00, 0x28, 0x47, 0x68, 0x44, 0x70, - 0xec, 0x8a, 0xff, 0x38, 0xaa, 0xfc, 0xef, 0xee, 0x7a, 0x8b, 0x1e, 0x84, 0x2f, 0xf6, 0xa6, 0x4f, 0xa3, 0x80, 0x39, - 0x6b, 0xdd, 0xae, 0x3e, 0x8d, 0xa9, 0x21, 0xfd, 0x35, 0x01, 0xe3, 0xc6, 0xb1, 0xfa, 0xc7, 0x34, 0x25, 0xbf, 0xd7, - 0x63, 0x12, 0x5f, 0x2c, 0xfa, 0xca, 0x1a, 0x55, 0xea, 0xd1, 0x65, 0x38, 0x6d, 0xc9, 0x68, 0x4f, 0xca, 0xb7, 0xba, - 0xc3, 0xd3, 0xb6, 0x4b, 0x6a, 0x36, 0xef, 0x89, 0xf9, 0xec, 0xba, 0xda, 0x56, 0xe2, 0x88, 0xf4, 0x20, 0x5f, 0x4f, - 0x19, 0xa5, 0xa3, 0x4f, 0xd1, 0xde, 0xef, 0x8e, 0x03, 0x99, 0xa7, 0xc7, 0xa1, 0x56, 0xd7, 0xae, 0xec, 0xf8, 0x56, - 0x9c, 0x98, 0xd4, 0x58, 0x86, 0xec, 0xd7, 0xb8, 0x11, 0x0d, 0x3a, 0xee, 0x7d, 0xd5, 0x7a, 0xdd, 0xd4, 0x98, 0x0e, - 0x4e, 0x31, 0x04, 0xcd, 0x57, 0x5d, 0x12, 0x55, 0xc4, 0x82, 0x37, 0xc4, 0x07, 0x71, 0x01, 0x80, 0x9c, 0x93, 0x16, - 0xb5, 0xec, 0x18, 0x4b, 0xa2, 0x7c, 0x57, 0x81, 0x5a, 0xf2, 0xec, 0xa2, 0xa2, 0x53, 0x77, 0xa2, 0x57, 0xa7, 0x5e, - 0xa5, 0x39, 0x8d, 0xd0, 0xf5, 0xf0, 0x99, 0xe7, 0xa8, 0x64, 0x59, 0xf7, 0xae, 0x42, 0x5f, 0xb1, 0xd7, 0x5e, 0x49, - 0xc9, 0x3b, 0x52, 0x1a, 0x0a, 0x19, 0xc5, 0x1a, 0x34, 0xb6, 0xce, 0x5d, 0x62, 0x49, 0x27, 0xcb, 0xa3, 0x86, 0xc2, - 0x17, 0x73, 0x1f, 0xb7, 0xc6, 0x51, 0x39, 0xe6, 0x1c, 0xc0, 0x9e, 0x54, 0xe9, 0x64, 0xaa, 0x1c, 0xc0, 0xaf, 0x69, - 0x16, 0x11, 0x83, 0x94, 0xda, 0xe9, 0xb8, 0x8b, 0xb3, 0x64, 0x3b, 0x61, 0xd0, 0xb1, 0xe8, 0x39, 0x7a, 0x20, 0xd2, - 0x79, 0x1c, 0x44, 0xf7, 0x91, 0xc7, 0x0d, 0x32, 0x0c, 0xb6, 0x67, 0x2d, 0x79, 0x94, 0xb9, 0xe2, 0x28, 0xbb, 0x12, - 0x53, 0xcb, 0xb3, 0xa9, 0xdb, 0x33, 0xba, 0x62, 0xad, 0xac, 0xe9, 0xee, 0x88, 0x4c, 0x05, 0xf7, 0x7d, 0x7b, 0x86, - 0x4f, 0x47, 0x46, 0x8e, 0x33, 0x89, 0xa3, 0x3a, 0x84, 0xb9, 0x71, 0x22, 0x78, 0x82, 0xd1, 0xb2, 0x25, 0xf3, 0x94, - 0x53, 0x0a, 0xb5, 0xf7, 0xbf, 0x34, 0x1e, 0xa1, 0x6a, 0xae, 0x61, 0x7a, 0xcb, 0xd0, 0x1d, 0xc2, 0x76, 0xfd, 0x43, - 0x74, 0x32, 0xa2, 0x05, 0xef, 0x2f, 0x92, 0x0a, 0xc6, 0x5a, 0x5a, 0x95, 0xb6, 0xbe, 0xdd, 0x43, 0x02, 0x96, 0xa7, - 0x56, 0x9d, 0xa1, 0x80, 0x15, 0xa6, 0xcf, 0xf9, 0x9b, 0xb9, 0xc6, 0x21, 0x77, 0x2d, 0x11, 0x10, 0x1b, 0x81, 0xdd, - 0xd0, 0x09, 0x12, 0x18, 0xaa, 0x10, 0xfb, 0xac, 0x55, 0xf1, 0x9c, 0x37, 0x85, 0x1e, 0xf0, 0x23, 0x9f, 0xc4, 0x92, - 0xfa, 0x09, 0x92, 0xfc, 0x09, 0x97, 0x84, 0xd0, 0xa7, 0xfc, 0x22, 0xf6, 0xaa, 0xc9, 0x4d, 0xad, 0x34, 0xdb, 0x0e, - 0xc5, 0xcf, 0xfc, 0x62, 0xdc, 0xdd, 0x68, 0x88, 0x21, 0x62, 0x85, 0x11, 0x0a, 0xc6, 0x9c, 0xa0, 0x6e, 0xf2, 0x57, - 0xa4, 0xf8, 0x74, 0xce, 0xe6, 0x5b, 0xf8, 0x4e, 0xdb, 0xe9, 0x1d, 0x14, 0x0a, 0x31, 0xea, 0x0c, 0x2d, 0x61, 0xd8, - 0xc3, 0x93, 0xf9, 0xec, 0xc2, 0x9c, 0x84, 0x24, 0x15, 0x2d, 0x4a, 0x38, 0x43, 0xfc, 0x06, 0xc0, 0x04, 0x9a, 0xac, - 0x44, 0xa9, 0xa8, 0x81, 0x3d, 0x82, 0x5f, 0xb8, 0xd9, 0xe6, 0xf3, 0x56, 0xe4, 0xe1, 0x40, 0x1a, 0xe5, 0x0a, 0x6d, - 0x20, 0xa6, 0x7a, 0x6e, 0x23, 0xb1, 0x18, 0x19, 0x45, 0x6b, 0xc9, 0x97, 0x5a, 0x42, 0x5d, 0xec, 0x3c, 0x08, 0xd6, - 0x55, 0x77, 0x95, 0x9d, 0xa1, 0x59, 0x31, 0x83, 0x03, 0x39, 0x2e, 0xd0, 0x30, 0x44, 0xba, 0x31, 0xd9, 0xa6, 0x98, - 0x65, 0x23, 0x7c, 0x5f, 0xc5, 0xbc, 0xc9, 0x6b, 0x21, 0xf2, 0x5a, 0x9d, 0x49, 0xb0, 0x86, 0x09, 0x79, 0x6a, 0x60, - 0x96, 0x24, 0xa4, 0x61, 0x09, 0xcb, 0x13, 0x3e, 0x43, 0xbd, 0x64, 0x98, 0x66, 0x64, 0xfa, 0xe0, 0x49, 0xbf, 0x65, - 0xfd, 0x89, 0x37, 0xf2, 0xf3, 0x46, 0x13, 0x78, 0x51, 0x09, 0x55, 0x2e, 0xb6, 0x19, 0x22, 0xba, 0xd5, 0x52, 0xc3, - 0x73, 0xea, 0x96, 0x27, 0x40, 0xe2, 0x49, 0x9f, 0x19, 0x7e, 0xb4, 0xcd, 0x08, 0x81, 0x54, 0xe9, 0xad, 0x8b, 0x90, - 0xd9, 0x27, 0x65, 0xe5, 0xe1, 0xf0, 0xe4, 0xd2, 0x69, 0x09, 0x95, 0xc0, 0xf5, 0x9b, 0xd7, 0x05, 0x54, 0x81, 0x99, - 0xa1, 0x58, 0x63, 0x53, 0x3d, 0x1b, 0x6f, 0x90, 0x66, 0x30, 0x2e, 0x22, 0xa1, 0x42, 0xe6, 0xce, 0x25, 0x9a, 0xba, - 0x5e, 0xcc, 0x19, 0xcb, 0xcb, 0x3e, 0xec, 0xf9, 0xd2, 0x53, 0xcc, 0x2e, 0xbc, 0xd6, 0x2f, 0x99, 0xe3, 0xf6, 0x59, - 0x48, 0xb3, 0xdc, 0x9d, 0x22, 0x35, 0x7b, 0xac, 0x92, 0x9a, 0x07, 0xb0, 0xae, 0xb3, 0x2b, 0x3b, 0xd3, 0xa5, 0x1c, - 0x61, 0x7f, 0x82, 0x3b, 0x80, 0x63, 0x04, 0x43, 0x12, 0x70, 0x1b, 0xf9, 0x8d, 0x5b, 0x30, 0xf2, 0xcd, 0xc7, 0x41, - 0x1b, 0x82, 0xc8, 0x04, 0x89, 0x10, 0x31, 0x91, 0xc7, 0xf0, 0xf3, 0x01, 0xce, 0xbe, 0xba, 0x4c, 0x34, 0x51, 0xbc, - 0x11, 0x8a, 0x69, 0x78, 0x0d, 0x77, 0xeb, 0xc0, 0x8c, 0xce, 0x7b, 0x3a, 0x45, 0x57, 0xd0, 0x24, 0xa6, 0x56, 0x4f, - 0x9b, 0xf7, 0xdc, 0x23, 0xc2, 0x2f, 0x74, 0x51, 0xdc, 0xdd, 0xc1, 0x7a, 0xbd, 0x80, 0x25, 0x13, 0xf9, 0x96, 0x33, - 0xf3, 0x66, 0xca, 0x1e, 0x92, 0x63, 0x9f, 0x3e, 0x3c, 0x6e, 0x17, 0xfb, 0xe4, 0x39, 0x2e, 0xb2, 0x5e, 0x52, 0x85, - 0x3d, 0x29, 0xff, 0x9b, 0x32, 0xe6, 0x44, 0x04, 0x35, 0x95, 0xe9, 0xda, 0xa2, 0xe3, 0xcf, 0x2a, 0x9a, 0x2c, 0x8d, - 0x60, 0x6b, 0x54, 0x91, 0x7e, 0x69, 0x94, 0x52, 0x1d, 0x51, 0x0f, 0x43, 0x9b, 0x48, 0xb1, 0xd0, 0x28, 0x70, 0x74, - 0xa9, 0x4d, 0xf0, 0x6c, 0x15, 0xf4, 0x48, 0x7c, 0xa4, 0x9d, 0xd8, 0xe6, 0xfc, 0x7c, 0x4f, 0xf1, 0x4d, 0xf2, 0x5b, - 0xfa, 0x5b, 0x70, 0x93, 0x42, 0x93, 0xc5, 0xb5, 0xbc, 0xb5, 0x72, 0x8b, 0xdf, 0xe5, 0x63, 0x5f, 0xa3, 0x8b, 0x83, - 0x51, 0x3a, 0x99, 0xf3, 0x5e, 0x78, 0xc8, 0xb5, 0x93, 0x57, 0x62, 0xaf, 0x66, 0x2b, 0xc5, 0x95, 0x60, 0xe1, 0xc1, - 0xa9, 0x2b, 0x99, 0x8a, 0x56, 0x10, 0xc8, 0xbc, 0x71, 0xdb, 0xaf, 0x7f, 0x20, 0xa3, 0x6d, 0x08, 0x19, 0xcc, 0xda, - 0xea, 0x25, 0xa6, 0xf3, 0xbe, 0x45, 0xbe, 0x64, 0x6f, 0x6c, 0x59, 0xf7, 0x10, 0x88, 0xfe, 0xe4, 0x78, 0x37, 0x64, - 0x05, 0x16, 0x0a, 0xfb, 0x12, 0xd0, 0x93, 0xa8, 0xaa, 0xd4, 0x5e, 0xea, 0x50, 0xae, 0xab, 0x19, 0x2a, 0x54, 0xcb, - 0xeb, 0x1f, 0x40, 0x22, 0x8e, 0x52, 0x06, 0xed, 0xe9, 0xa2, 0x2a, 0x23, 0x82, 0xc0, 0xb8, 0x08, 0x0d, 0x2b, 0xc4, - 0x14, 0x76, 0x59, 0xc5, 0x38, 0x4e, 0x57, 0xf7, 0xf5, 0x8b, 0x53, 0x88, 0xbd, 0xee, 0x86, 0xac, 0x12, 0x74, 0xee, - 0xba, 0xec, 0xd3, 0x1c, 0xfa, 0x99, 0xae, 0x7f, 0x04, 0x37, 0x39, 0x87, 0x35, 0x29, 0x38, 0x35, 0xf5, 0x39, 0x8b, - 0x25, 0xdf, 0x08, 0x15, 0x4e, 0x5b, 0x32, 0xda, 0xb1, 0x63, 0x56, 0xe5, 0xc7, 0x2e, 0x4b, 0x69, 0x60, 0x48, 0xa2, - 0xba, 0x36, 0xe8, 0x18, 0xb4, 0x24, 0x72, 0xf3, 0xea, 0x70, 0x49, 0x7b, 0x43, 0xc8, 0x0f, 0x37, 0xa7, 0xfb, 0x94, - 0xd0, 0xc6, 0x6a, 0xf1, 0xca, 0xc5, 0x97, 0x44, 0xa4, 0xbc, 0xe0, 0x1e, 0x01, 0xeb, 0x77, 0x22, 0xfc, 0xbb, 0xe8, - 0xc1, 0x81, 0x47, 0x00, 0x8b, 0xf0, 0x56, 0xdc, 0x57, 0xde, 0x26, 0x94, 0x56, 0xa0, 0x2e, 0xd7, 0xa6, 0x51, 0x82, - 0x37, 0xa4, 0xff, 0xf0, 0xc8, 0xbe, 0xcc, 0x13, 0xb6, 0x51, 0x21, 0xf1, 0x0e, 0xbf, 0xf3, 0x77, 0x4f, 0xc7, 0x9c, - 0x00, 0xbb, 0xa5, 0xd3, 0xbc, 0x6a, 0x0b, 0x90, 0x16, 0x5d, 0x0c, 0x62, 0x9c, 0x82, 0xe5, 0x95, 0x00, 0xe9, 0x87, - 0x57, 0x61, 0xa1, 0xeb, 0xf9, 0x7b, 0x4d, 0xf7, 0xca, 0x5e, 0x87, 0x69, 0xf2, 0x65, 0xef, 0x88, 0x46, 0xd0, 0xcb, - 0xd5, 0xc9, 0xe5, 0xfb, 0x94, 0x72, 0xe1, 0x5f, 0xd2, 0xd5, 0xcf, 0x54, 0x89, 0xe6, 0xaa, 0x6f, 0xaa, 0xf8, 0x94, - 0xab, 0xf1, 0x09, 0xa4, 0xda, 0x9c, 0x57, 0x13, 0xe6, 0xca, 0x55, 0x9f, 0xdc, 0x77, 0x17, 0x98, 0x56, 0x6f, 0xfd, - 0xdb, 0xfd, 0x30, 0xd0, 0x9f, 0xdd, 0xdf, 0x1c, 0x7c, 0x9d, 0x5d, 0x74, 0x76, 0x3a, 0xf6, 0x2f, 0xe4, 0xc7, 0x11, - 0xba, 0xac, 0x87, 0xa2, 0x26, 0x72, 0xc2, 0x7b, 0xea, 0xa8, 0x61, 0x2f, 0xb7, 0x94, 0x79, 0x31, 0x7d, 0x2f, 0x59, - 0x05, 0x94, 0xdf, 0xb7, 0xd9, 0xa5, 0xd5, 0x84, 0xe2, 0x02, 0x92, 0x2e, 0x73, 0x9a, 0x95, 0x6e, 0xa4, 0x50, 0xb3, - 0xc9, 0x5e, 0x46, 0x56, 0xe9, 0xb5, 0x12, 0xec, 0x57, 0x8b, 0x60, 0x58, 0x56, 0xe9, 0x2a, 0x8f, 0x3a, 0x6c, 0xd6, - 0xae, 0xad, 0xd3, 0x7f, 0xb9, 0xb9, 0x9c, 0x09, 0xa2, 0xec, 0xa0, 0x56, 0xf2, 0x8c, 0x2b, 0x7d, 0xce, 0xb5, 0x52, - 0x77, 0x3a, 0xde, 0xab, 0x3f, 0x57, 0xcd, 0x27, 0x4b, 0x4b, 0xf7, 0xbd, 0x0e, 0xff, 0xd9, 0x95, 0xb5, 0x14, 0x41, - 0x16, 0x43, 0xea, 0x3d, 0x63, 0x67, 0x25, 0x53, 0x42, 0x01, 0xc4, 0x2f, 0x3c, 0xae, 0x5d, 0x07, 0xcd, 0xbb, 0xd2, - 0xed, 0xa7, 0xab, 0xd6, 0xaa, 0x90, 0xf2, 0x78, 0x63, 0x19, 0x51, 0x98, 0xb8, 0xaa, 0x95, 0x61, 0x9a, 0x37, 0x7f, - 0xef, 0x9e, 0xf4, 0x57, 0xc5, 0xcb, 0x6a, 0x22, 0x8a, 0x98, 0xae, 0x79, 0xbc, 0xb1, 0x7a, 0x37, 0x87, 0xb5, 0x79, - 0xf1, 0x5c, 0x8d, 0x2f, 0x5a, 0xff, 0x5c, 0x75, 0x6c, 0x0d, 0x63, 0x52, 0x39, 0xcf, 0x3b, 0xbf, 0x29, 0x29, 0x8d, - 0xb4, 0x8c, 0x36, 0x4e, 0x8a, 0x99, 0x0a, 0x2f, 0x57, 0xef, 0x54, 0x27, 0xaa, 0x10, 0x19, 0x1c, 0x3c, 0x23, 0xb8, - 0xbf, 0xfd, 0xf3, 0x51, 0x59, 0xb7, 0xb6, 0x8d, 0xe5, 0x8d, 0xbc, 0x92, 0x68, 0xfc, 0x2e, 0x96, 0xcb, 0x16, 0xe6, - 0x5b, 0xfb, 0xa6, 0x29, 0x97, 0xb5, 0x89, 0xa4, 0x8e, 0xd2, 0x27, 0xc5, 0x65, 0xa4, 0x2a, 0xbd, 0x4f, 0x40, 0xa2, - 0x97, 0x46, 0xfa, 0x0c, 0x23, 0xa5, 0x1e, 0xc9, 0x8e, 0x10, 0x21, 0x40, 0x80, 0x66, 0xe7, 0xaa, 0xbd, 0x4c, 0x97, - 0x0c, 0xce, 0xc8, 0xa4, 0x40, 0x9f, 0x61, 0x76, 0x35, 0x17, 0x09, 0xc1, 0x19, 0xa1, 0x03, 0xe9, 0x26, 0x63, 0x25, - 0xf8, 0x67, 0xa4, 0x1a, 0x35, 0x6d, 0xa6, 0xae, 0xb1, 0xf3, 0xb5, 0xb0, 0xe6, 0x87, 0x0e, 0xd9, 0xc5, 0x89, 0x45, - 0x89, 0xbe, 0x70, 0x24, 0x66, 0xe9, 0x49, 0x5d, 0x69, 0x0d, 0xdd, 0x85, 0x5b, 0x5c, 0xef, 0x5c, 0x76, 0xc9, 0x2f, - 0xe3, 0x4d, 0x2b, 0xd2, 0x1c, 0x53, 0x74, 0xf9, 0x26, 0x58, 0x0b, 0x70, 0xa0, 0xcc, 0xcb, 0x57, 0x3d, 0x02, 0x57, - 0x7e, 0x80, 0x8b, 0xe8, 0x65, 0x3e, 0x82, 0x08, 0xce, 0x4d, 0x95, 0x16, 0x5a, 0x98, 0x3d, 0x02, 0x3c, 0xd6, 0x6b, - 0xfe, 0x14, 0xfa, 0x99, 0x29, 0x5e, 0x0a, 0x27, 0xcf, 0x5a, 0xa3, 0x76, 0x0f, 0x31, 0xf8, 0x94, 0xac, 0xd6, 0xc4, - 0x22, 0xa7, 0x71, 0x9d, 0x53, 0x8f, 0x8f, 0x66, 0xcb, 0x7c, 0x90, 0x98, 0x15, 0xc0, 0xe4, 0x34, 0xae, 0x51, 0xe2, - 0x6d, 0xa6, 0xaa, 0x76, 0x46, 0x39, 0x8d, 0x2f, 0xc4, 0x90, 0x4c, 0x52, 0x31, 0xdf, 0x3e, 0x90, 0x51, 0x86, 0xc4, - 0x45, 0xc9, 0xad, 0xd5, 0x14, 0xa7, 0xad, 0x79, 0x43, 0x52, 0x7e, 0xc1, 0x28, 0xeb, 0xe6, 0xef, 0x52, 0x5f, 0xef, - 0xfe, 0x28, 0xa6, 0x4b, 0x8f, 0xab, 0xc3, 0x9b, 0x79, 0x75, 0x34, 0x91, 0x9e, 0xe6, 0xd4, 0x20, 0xf1, 0x5b, 0x0b, - 0xfe, 0x98, 0x1f, 0x2f, 0x35, 0xa6, 0x1a, 0x9a, 0xf8, 0xc8, 0x66, 0x8b, 0x2e, 0x2b, 0xbc, 0x71, 0x6e, 0x85, 0x2f, - 0xb5, 0x29, 0x16, 0xe3, 0xb3, 0xcf, 0x3b, 0x0d, 0xae, 0xa3, 0x78, 0x0f, 0x87, 0xd4, 0xd5, 0x8b, 0xa2, 0xa5, 0x3f, - 0x36, 0xfb, 0x3c, 0x8e, 0xf8, 0xc3, 0x9b, 0xfd, 0xb0, 0x04, 0xe6, 0xee, 0xd0, 0x4a, 0x8b, 0x03, 0x69, 0x2b, 0x39, - 0x5a, 0xef, 0xda, 0x7e, 0x8a, 0xd6, 0x44, 0x56, 0x63, 0x53, 0x41, 0xa9, 0x5e, 0x90, 0xff, 0xef, 0x6d, 0x6c, 0x55, - 0x32, 0x55, 0x3a, 0xe8, 0x3d, 0x24, 0xbd, 0x34, 0xf4, 0x15, 0x7d, 0xee, 0xe9, 0xb1, 0xde, 0xa9, 0x44, 0xbc, 0x8b, - 0xcb, 0x9c, 0x61, 0x36, 0x1b, 0xe6, 0xe6, 0x11, 0xbd, 0x95, 0x5e, 0xb1, 0xdb, 0x98, 0xf4, 0x34, 0x88, 0x65, 0x79, - 0x99, 0x53, 0xf7, 0x39, 0x09, 0x24, 0xfe, 0x39, 0x3c, 0x00, 0xff, 0xa4, 0x6b, 0xd0, 0x1c, 0x49, 0xe5, 0x72, 0x53, - 0xaf, 0x43, 0xbc, 0x6b, 0x77, 0x3c, 0x16, 0xe9, 0xeb, 0x26, 0x1a, 0xdf, 0xd0, 0x0d, 0xa5, 0xa8, 0xa9, 0x8c, 0x3a, - 0x8e, 0x0c, 0x97, 0x8c, 0xbc, 0x59, 0x91, 0x6b, 0xbf, 0x02, 0x79, 0x55, 0x00, 0x21, 0x48, 0x6b, 0x11, 0x4d, 0x4c, - 0xf6, 0x57, 0x43, 0xcd, 0x51, 0x9a, 0xd9, 0x26, 0x7f, 0xda, 0xc4, 0xee, 0xba, 0x05, 0xdc, 0xad, 0x1c, 0xa2, 0x8b, - 0xed, 0x31, 0x0f, 0x79, 0x04, 0xa3, 0x2d, 0x24, 0x0a, 0x59, 0x15, 0xa2, 0x85, 0xd3, 0xfc, 0x49, 0x3e, 0x55, 0x9e, - 0x02, 0x3c, 0x44, 0x41, 0x13, 0x96, 0xb2, 0x9b, 0xee, 0x4b, 0xb2, 0x74, 0xf4, 0x3c, 0x82, 0x0f, 0x50, 0x09, 0x0e, - 0xd0, 0x45, 0xce, 0xeb, 0xee, 0xc5, 0xb6, 0x11, 0xd9, 0xc8, 0xd6, 0x75, 0x4f, 0x07, 0x59, 0x6e, 0x2d, 0x2d, 0xfc, - 0xef, 0x8f, 0xbd, 0xaf, 0xee, 0x82, 0x1d, 0x60, 0x28, 0xef, 0x3e, 0x84, 0x16, 0xee, 0x38, 0xd4, 0xea, 0xc5, 0x8a, - 0x12, 0x05, 0x4f, 0x22, 0xf3, 0xc7, 0x4a, 0x76, 0xba, 0xc7, 0x56, 0x24, 0xde, 0x53, 0x37, 0xa8, 0xdb, 0xe5, 0x56, - 0x5d, 0x35, 0x7b, 0xb9, 0x2a, 0xec, 0x3f, 0x1b, 0xfa, 0xd1, 0x54, 0xc1, 0x07, 0x4c, 0x2f, 0x6e, 0x23, 0x2e, 0x0a, - 0x85, 0x35, 0x72, 0xfe, 0x01, 0x8c, 0xca, 0x9a, 0x17, 0x6e, 0x52, 0x2c, 0x83, 0xcb, 0xd0, 0x28, 0xee, 0xc4, 0x2d, - 0x86, 0x1a, 0x0f, 0x06, 0x3d, 0x0b, 0x4b, 0x90, 0x46, 0xf7, 0xe9, 0x3d, 0xce, 0x70, 0x12, 0xa4, 0xd5, 0xe7, 0xcd, - 0x09, 0x72, 0x8d, 0x77, 0x52, 0x6b, 0x44, 0x22, 0xcd, 0x1e, 0x47, 0x65, 0x6d, 0xf8, 0x18, 0xa6, 0xd1, 0x39, 0xa0, - 0xa8, 0x4d, 0x85, 0xad, 0x76, 0x8a, 0x50, 0xaa, 0xe3, 0x20, 0xb0, 0x69, 0xe9, 0xe3, 0x24, 0x2d, 0xe2, 0x40, 0x4f, - 0x25, 0x78, 0x5e, 0xd2, 0xfc, 0x96, 0x8a, 0xbc, 0x9f, 0x77, 0x82, 0x66, 0xfa, 0xbd, 0x82, 0x48, 0x79, 0xac, 0x44, - 0x1a, 0x46, 0x1d, 0x0c, 0x76, 0x6c, 0xc3, 0xab, 0x03, 0x18, 0xcf, 0x91, 0x4a, 0x46, 0x0d, 0x5c, 0xb9, 0xe2, 0xee, - 0x4b, 0x9b, 0x32, 0xe5, 0xda, 0x2a, 0xfc, 0xc8, 0x7c, 0xc9, 0x10, 0x2b, 0x5f, 0x35, 0x43, 0x89, 0xab, 0xd4, 0xb0, - 0xf6, 0x8b, 0xa9, 0x9b, 0x58, 0x5b, 0xc8, 0xe7, 0x8b, 0xbf, 0x46, 0x87, 0xb0, 0x0f, 0x20, 0xab, 0x9f, 0x2e, 0xc4, - 0x94, 0x5c, 0xc2, 0x04, 0x39, 0x57, 0x8c, 0x89, 0x77, 0x93, 0xc9, 0xa5, 0x3f, 0xcd, 0x16, 0xd8, 0x67, 0xd3, 0x4a, - 0xba, 0x5f, 0x92, 0x42, 0xfc, 0x1e, 0x0f, 0x1a, 0xd2, 0x13, 0x84, 0x98, 0x3d, 0x05, 0x8f, 0x6e, 0x56, 0x6e, 0xd4, - 0x1b, 0x49, 0xd0, 0x8e, 0xad, 0xd8, 0x02, 0x24, 0x38, 0xa0, 0x9e, 0xa8, 0xf1, 0x7d, 0xf0, 0x52, 0xe5, 0x97, 0x2f, - 0x0f, 0x11, 0x6a, 0x06, 0xe3, 0x89, 0xa4, 0x19, 0x3b, 0x54, 0x24, 0xf3, 0x15, 0x34, 0xf3, 0xe1, 0xae, 0xa7, 0x23, - 0xde, 0xec, 0xd0, 0x2b, 0x6d, 0xdc, 0xba, 0x27, 0xba, 0xb8, 0x22, 0x61, 0x68, 0xf5, 0xf1, 0xa0, 0xf2, 0xfe, 0x7c, - 0x39, 0x94, 0x57, 0xb6, 0xf2, 0x83, 0x70, 0x98, 0xb5, 0x3b, 0x78, 0xbe, 0x8e, 0x8c, 0x0f, 0x33, 0x92, 0xb3, 0x0c, - 0x16, 0x81, 0x07, 0x73, 0x96, 0xa2, 0x85, 0x6f, 0xca, 0x32, 0x1b, 0x64, 0x6a, 0xb4, 0x80, 0xc5, 0x8b, 0xfc, 0x1b, - 0x1b, 0x5f, 0x96, 0xd9, 0x58, 0xc1, 0xec, 0x75, 0x20, 0x3b, 0x25, 0x90, 0x98, 0xa3, 0xda, 0x9d, 0x0d, 0xa8, 0xa2, - 0x87, 0x27, 0x00, 0x57, 0xf0, 0x87, 0xc7, 0x2c, 0xd0, 0x39, 0x35, 0xce, 0xd7, 0xb0, 0x56, 0x1e, 0x35, 0x36, 0x59, - 0xd7, 0x44, 0x50, 0xa4, 0x16, 0xab, 0xd0, 0x4b, 0x92, 0x48, 0x1d, 0xaa, 0xfc, 0x8f, 0x2f, 0x9c, 0x53, 0x73, 0xef, - 0x68, 0xeb, 0x31, 0xd7, 0x13, 0xd2, 0x56, 0x51, 0x93, 0x33, 0x3d, 0x2e, 0xa1, 0xa0, 0xfc, 0x9c, 0x0a, 0x95, 0xe2, - 0xcb, 0x74, 0xe7, 0x66, 0x55, 0xc1, 0x00, 0x6a, 0x06, 0x30, 0xfa, 0x51, 0x40, 0x46, 0x6a, 0xfc, 0x78, 0xa2, 0x5e, - 0xf7, 0x31, 0x37, 0x74, 0xd0, 0xe2, 0x4c, 0x37, 0xb0, 0x47, 0xb2, 0x1d, 0x8e, 0x6a, 0x80, 0xf2, 0x21, 0x9e, 0x04, - 0x9e, 0x22, 0x9a, 0xa4, 0x59, 0x5f, 0xf1, 0xb7, 0xe9, 0xdc, 0xf6, 0x64, 0x1d, 0x00, 0x2d, 0x2c, 0x98, 0x41, 0xb3, - 0x49, 0xdf, 0x4b, 0xd0, 0x80, 0x1f, 0xc6, 0xce, 0x30, 0xdf, 0x3b, 0xa1, 0xf6, 0xce, 0x0e, 0xbd, 0x9e, 0x7d, 0xe5, - 0x9c, 0x70, 0x3e, 0x53, 0x2f, 0xc6, 0x51, 0xd8, 0x45, 0x9d, 0xc5, 0x93, 0x3f, 0x7c, 0xa6, 0xc2, 0x78, 0x4c, 0xc4, - 0x45, 0x2c, 0x35, 0xb8, 0x20, 0x89, 0x2d, 0x9b, 0xcd, 0x32, 0x0e, 0x7e, 0x26, 0xc3, 0x41, 0xc6, 0x04, 0x2f, 0x27, - 0xf4, 0xfe, 0x96, 0x48, 0x08, 0xb2, 0x21, 0x94, 0x4c, 0xd3, 0x90, 0x1a, 0xaf, 0x36, 0x97, 0x71, 0x99, 0xd1, 0x25, - 0xe3, 0xff, 0x66, 0x17, 0x14, 0xea, 0xb5, 0xa2, 0xe0, 0xfb, 0x2d, 0xdc, 0xf6, 0x1a, 0x9d, 0xb1, 0x67, 0xc8, 0xf4, - 0xa1, 0x39, 0x4c, 0x19, 0x29, 0x0c, 0x77, 0xed, 0x29, 0x48, 0x90, 0x99, 0x97, 0xe1, 0xfd, 0x86, 0xfd, 0x36, 0x60, - 0x0a, 0x1e, 0xdf, 0xfb, 0x66, 0x05, 0xd8, 0x1c, 0x69, 0xa8, 0x7b, 0xee, 0x29, 0xa0, 0x1c, 0xe6, 0xc2, 0xc3, 0x1c, - 0xba, 0x42, 0xb5, 0x0f, 0xb9, 0xab, 0xa7, 0x7a, 0x15, 0x0b, 0xcb, 0xc1, 0xa6, 0x6e, 0x54, 0x9b, 0x84, 0xea, 0xb8, - 0x5c, 0x03, 0xd2, 0x9e, 0xd0, 0x0c, 0xb4, 0x1e, 0x46, 0x54, 0xeb, 0x64, 0x97, 0xde, 0x4a, 0x30, 0xba, 0x24, 0x91, - 0x06, 0x26, 0xcb, 0x9c, 0xd4, 0x00, 0xa6, 0x45, 0x98, 0x03, 0xbf, 0x23, 0x39, 0xae, 0x91, 0x80, 0xce, 0x71, 0xd8, - 0x35, 0xac, 0x26, 0xa5, 0xf3, 0x5c, 0xb5, 0x24, 0x15, 0xa4, 0x22, 0x42, 0x25, 0x53, 0x25, 0xa5, 0x63, 0xc2, 0x39, - 0xae, 0x06, 0x24, 0xc3, 0x94, 0x0a, 0x6a, 0x6f, 0xa3, 0x52, 0x1a, 0xcb, 0x59, 0x18, 0x3e, 0x71, 0xf9, 0x33, 0xaa, - 0xf9, 0xb2, 0x65, 0x23, 0x89, 0xec, 0x35, 0xd3, 0xc5, 0x82, 0x3b, 0xf3, 0x27, 0x70, 0x07, 0xbe, 0xfb, 0x8a, 0x9a, - 0xf2, 0xbe, 0x3c, 0x18, 0x25, 0x26, 0x32, 0x7e, 0x4d, 0xf5, 0x15, 0xcc, 0x65, 0x3e, 0x43, 0x28, 0xd3, 0x6f, 0x3d, - 0x56, 0x67, 0x0b, 0x61, 0x53, 0x49, 0xec, 0xfe, 0xfd, 0xe4, 0x87, 0x02, 0x5e, 0xf0, 0x43, 0x8f, 0xcf, 0x56, 0x13, - 0x04, 0x89, 0x45, 0xb3, 0x0a, 0x7b, 0x8b, 0x9c, 0x18, 0x40, 0x54, 0xf6, 0x68, 0x6e, 0x2f, 0xa9, 0xa1, 0x23, 0x52, - 0x8f, 0x3b, 0x27, 0xac, 0xec, 0x6d, 0x4b, 0x9e, 0xbd, 0x5a, 0xd5, 0x53, 0xaa, 0x63, 0xc2, 0x80, 0x9b, 0xbf, 0xf1, - 0x75, 0x6e, 0xeb, 0xbb, 0x1b, 0x30, 0xd8, 0x12, 0xed, 0xc7, 0x91, 0x22, 0x80, 0x9d, 0x60, 0x8a, 0x43, 0xce, 0xf9, - 0xf1, 0xa6, 0x7a, 0xf9, 0xbf, 0x27, 0x47, 0x15, 0xae, 0xcf, 0x11, 0xb8, 0xc4, 0xe8, 0x74, 0x33, 0x76, 0xee, 0xee, - 0xa8, 0xf4, 0x0d, 0x1f, 0x80, 0x5d, 0x67, 0x10, 0xf4, 0x90, 0x7b, 0x12, 0xde, 0x52, 0x2a, 0x9c, 0xf6, 0x4d, 0x67, - 0xa4, 0xc7, 0xa2, 0xe5, 0x43, 0x78, 0x6c, 0x77, 0x10, 0xac, 0x67, 0xcb, 0xb2, 0xa0, 0xa9, 0x04, 0x68, 0xa6, 0x18, - 0xe0, 0xc8, 0x43, 0xec, 0x1c, 0xc9, 0xac, 0x1c, 0xe3, 0x6e, 0x8e, 0xf7, 0x32, 0x88, 0x0e, 0xd1, 0x11, 0x4f, 0xa5, - 0x25, 0xb2, 0x8c, 0x6d, 0xa9, 0xc2, 0xb5, 0x4f, 0x71, 0x5a, 0xd8, 0xa2, 0xab, 0xca, 0x44, 0xbf, 0xfc, 0x08, 0xc4, - 0xd3, 0x37, 0x7a, 0x5e, 0xbb, 0xd9, 0x24, 0xb2, 0x37, 0x74, 0xb5, 0xfc, 0x92, 0x5b, 0x94, 0x56, 0xae, 0xc6, 0x00, - 0x2b, 0xf6, 0x3a, 0xef, 0x09, 0x84, 0x33, 0x25, 0x0e, 0xb7, 0xf9, 0x9d, 0x61, 0xb6, 0xb4, 0xb1, 0xba, 0x19, 0x9d, - 0x62, 0xdc, 0xd6, 0xdb, 0xfd, 0x40, 0x67, 0x37, 0x24, 0x7c, 0x78, 0xe3, 0xfb, 0xd0, 0x03, 0xa9, 0x24, 0xb8, 0xe2, - 0xee, 0xca, 0x7b, 0x0b, 0xc2, 0xec, 0x81, 0x9c, 0x3e, 0x7a, 0x42, 0x82, 0x5e, 0xc0, 0xfe, 0x7c, 0x1e, 0x1e, 0xf3, - 0x92, 0x38, 0x36, 0xca, 0xc7, 0x1f, 0xd6, 0x58, 0xe1, 0x96, 0xe8, 0x70, 0x89, 0x48, 0xdf, 0xc3, 0xe0, 0xc5, 0x13, - 0x86, 0xa4, 0xea, 0x7f, 0xbc, 0x91, 0x50, 0xc5, 0x33, 0x85, 0xce, 0xed, 0xb4, 0x39, 0x27, 0x88, 0xe5, 0xce, 0x55, - 0x66, 0xaf, 0x1d, 0x85, 0xbd, 0xe3, 0xea, 0x96, 0x74, 0x5f, 0xf6, 0x95, 0x9a, 0x5b, 0x8d, 0x48, 0xb0, 0x91, 0xe1, - 0x79, 0x6e, 0xf5, 0x22, 0xb3, 0x43, 0xd6, 0x45, 0x0e, 0xb0, 0xe9, 0x4c, 0x16, 0x47, 0xed, 0xcd, 0xe8, 0x8b, 0xea, - 0x8a, 0xed, 0xaa, 0x6a, 0x95, 0xc6, 0x25, 0x1d, 0xb4, 0xe6, 0x54, 0xc9, 0x0f, 0xc0, 0x36, 0x22, 0x7f, 0xa7, 0x47, - 0x9d, 0xa9, 0x87, 0x4a, 0x39, 0xad, 0x75, 0x20, 0x8c, 0x87, 0x91, 0xde, 0xcf, 0xc0, 0x40, 0x51, 0x09, 0x6c, 0xb7, - 0x43, 0xe7, 0xa7, 0x5b, 0x93, 0x92, 0x91, 0x53, 0x50, 0x52, 0x56, 0x03, 0xd3, 0x44, 0x91, 0x75, 0x9e, 0x63, 0xf6, - 0x77, 0xc7, 0xd3, 0x9f, 0x3b, 0x0e, 0x1a, 0x31, 0xb3, 0x0b, 0x63, 0xff, 0xb8, 0x77, 0x4d, 0x36, 0xa4, 0x70, 0xea, - 0xc0, 0x24, 0xce, 0x92, 0xb4, 0xfe, 0x7d, 0xad, 0x99, 0xfe, 0xba, 0xe9, 0x7a, 0x09, 0x1f, 0xe8, 0xe1, 0xd8, 0x6e, - 0xb5, 0xf9, 0xa1, 0x7c, 0x60, 0x25, 0xbe, 0xf0, 0xdb, 0x35, 0x1d, 0xbe, 0x0c, 0x3d, 0xf7, 0x39, 0xcc, 0x61, 0x6d, - 0xc5, 0xde, 0x48, 0xa6, 0x05, 0x81, 0xde, 0x6e, 0x77, 0x12, 0xc6, 0x80, 0xfb, 0x7a, 0x8e, 0xa8, 0x7c, 0xe6, 0x72, - 0xf4, 0x4c, 0xa2, 0x04, 0xa6, 0xa3, 0xc1, 0xa3, 0x16, 0xa0, 0xe2, 0x13, 0x8b, 0xd3, 0xe1, 0x01, 0x36, 0x38, 0xb8, - 0x3b, 0x8c, 0xd1, 0x8f, 0x75, 0xf7, 0x6d, 0xea, 0xb3, 0x6c, 0xf8, 0x1a, 0x8e, 0x45, 0x5d, 0xfe, 0x70, 0x55, 0x1b, - 0xc7, 0xa2, 0xc7, 0xea, 0x2a, 0x3e, 0x1a, 0x17, 0xf5, 0x06, 0x43, 0xac, 0xce, 0x03, 0x1c, 0x55, 0xa4, 0x6c, 0xce, - 0x6c, 0xa1, 0x24, 0x81, 0xea, 0xad, 0xd5, 0xfc, 0x32, 0xb0, 0x5b, 0x83, 0x0d, 0xd1, 0xfc, 0x6c, 0xbd, 0x87, 0xef, - 0xe2, 0x27, 0x9f, 0x6d, 0xc9, 0x7c, 0x9b, 0x9d, 0x00, 0x77, 0x96, 0x5d, 0x79, 0x92, 0xd5, 0x8a, 0x77, 0x5b, 0x5f, - 0xbc, 0xef, 0x5f, 0x58, 0x2f, 0x84, 0x84, 0xf3, 0x4b, 0x7a, 0xbb, 0x96, 0x43, 0x1a, 0xc4, 0xf6, 0xaf, 0x26, 0x90, - 0x7f, 0x4a, 0x33, 0x77, 0xfe, 0x58, 0x19, 0x82, 0x63, 0x84, 0x1a, 0x6f, 0x09, 0x16, 0x5c, 0x7a, 0x45, 0x4a, 0xb1, - 0xcd, 0x6a, 0xa7, 0x95, 0x8c, 0xb5, 0xe6, 0xbe, 0xd2, 0x96, 0xb4, 0xca, 0x9d, 0x55, 0x40, 0x5c, 0x5d, 0x9a, 0xf8, - 0x50, 0x60, 0x35, 0x7b, 0x52, 0x96, 0xa4, 0x50, 0x1a, 0x2f, 0xfe, 0x91, 0x78, 0x47, 0x40, 0xe5, 0xea, 0x25, 0x42, - 0x30, 0xae, 0xbf, 0xef, 0xec, 0x2c, 0x3b, 0xcd, 0x1e, 0x32, 0xd5, 0x23, 0x2f, 0x2f, 0xc3, 0x39, 0x8a, 0x52, 0xa5, - 0xf1, 0x1d, 0x9c, 0x71, 0x23, 0x46, 0xbd, 0x7b, 0xf6, 0x14, 0x21, 0xef, 0xc8, 0x6f, 0x64, 0x92, 0xc3, 0x30, 0xef, - 0xbe, 0x3a, 0x19, 0x91, 0xe6, 0xf6, 0x0e, 0xe8, 0x62, 0x93, 0x29, 0xeb, 0x2c, 0xd8, 0x92, 0x0a, 0x12, 0x09, 0xd1, - 0xed, 0x90, 0x90, 0xbd, 0xb4, 0x6f, 0x48, 0x51, 0x54, 0xa7, 0x7a, 0xc8, 0x50, 0x4f, 0x3b, 0x7e, 0x5c, 0x47, 0x4c, - 0xc7, 0xda, 0x22, 0x1d, 0x91, 0x0c, 0x20, 0x18, 0x33, 0x5e, 0x42, 0xa6, 0x5a, 0x33, 0xbc, 0x56, 0x13, 0x4f, 0x19, - 0xdd, 0x59, 0x0f, 0x0c, 0x13, 0xa9, 0x20, 0x76, 0xde, 0xd7, 0x24, 0x52, 0x76, 0xeb, 0xc5, 0x67, 0xa5, 0x0c, 0xcb, - 0x7b, 0x78, 0xd6, 0xb5, 0xa7, 0x6c, 0x52, 0x06, 0x24, 0x96, 0xfb, 0x95, 0x8d, 0x5f, 0xeb, 0xe8, 0x4a, 0x9e, 0xd1, - 0xce, 0x03, 0x00, 0xa6, 0x96, 0xf8, 0x7d, 0xea, 0x32, 0x5f, 0xba, 0xd5, 0x8b, 0xed, 0x35, 0xba, 0x05, 0xb8, 0xf6, - 0xa8, 0x66, 0x9e, 0xf6, 0x16, 0xbb, 0xa7, 0x42, 0x07, 0x74, 0xd5, 0x30, 0x5b, 0xfc, 0xe5, 0x8d, 0x0f, 0xb7, 0xc4, - 0xbd, 0x3a, 0x95, 0xe8, 0x63, 0x7e, 0x2d, 0x2e, 0xfc, 0xa7, 0xdc, 0x91, 0x80, 0xd1, 0x31, 0x3e, 0x29, 0xa4, 0x0d, - 0xab, 0x22, 0x64, 0x42, 0x75, 0xbf, 0x38, 0x4d, 0xe0, 0x00, 0x03, 0xd3, 0xb9, 0xc9, 0x62, 0x96, 0xee, 0xae, 0x9c, - 0xea, 0x3e, 0x18, 0xc0, 0xaa, 0x76, 0xda, 0x9c, 0x7a, 0xea, 0x6e, 0x43, 0xeb, 0x18, 0x17, 0xdf, 0x42, 0x4d, 0x86, - 0xb0, 0xb5, 0x5e, 0xa8, 0x48, 0xd3, 0xbc, 0xc5, 0xca, 0x9f, 0x64, 0xdb, 0x1b, 0x60, 0xe8, 0x42, 0x62, 0x6b, 0x3e, - 0x28, 0x41, 0x7c, 0x50, 0x17, 0xc2, 0xbe, 0xa3, 0x81, 0x68, 0x71, 0x86, 0x75, 0x93, 0x2a, 0xd3, 0x7e, 0x46, 0x8e, - 0x26, 0xd4, 0xfa, 0x3e, 0xf6, 0xcf, 0xba, 0x73, 0xfa, 0x57, 0x24, 0xb5, 0x4c, 0xd3, 0x1c, 0xc9, 0xe8, 0x44, 0xd8, - 0xd8, 0x60, 0x20, 0x8d, 0x11, 0x2f, 0x3d, 0xfd, 0x9c, 0xbb, 0x75, 0xcd, 0x28, 0xb0, 0x7e, 0x83, 0xf1, 0x7a, 0xe0, - 0xe4, 0x9a, 0x5c, 0x04, 0x7a, 0x26, 0x46, 0x59, 0x0f, 0xa9, 0x67, 0x5e, 0x2f, 0xd5, 0xfb, 0x9c, 0x8b, 0x09, 0x42, - 0x85, 0xd7, 0x1c, 0x87, 0xf4, 0x13, 0xc0, 0xe3, 0x26, 0x5b, 0x24, 0x3f, 0x6a, 0x70, 0x1e, 0xf6, 0x49, 0xac, 0x2c, - 0x0e, 0x2f, 0x68, 0x7a, 0xf6, 0xbc, 0x0a, 0xf3, 0x03, 0xf9, 0xd3, 0xb9, 0x32, 0xc0, 0x18, 0xc9, 0xdd, 0xc4, 0xae, - 0x08, 0x4d, 0x01, 0x1c, 0x2a, 0xb5, 0x8e, 0x83, 0x68, 0x80, 0x39, 0xec, 0xfb, 0x72, 0x4b, 0x94, 0x91, 0x02, 0x58, - 0x9d, 0xe1, 0x0c, 0x60, 0x07, 0x25, 0xd9, 0x31, 0xd6, 0x62, 0x64, 0x01, 0x8f, 0x86, 0xab, 0x89, 0xd3, 0xa2, 0xda, - 0x8b, 0x8b, 0x31, 0x31, 0xf0, 0x18, 0xd1, 0x32, 0x69, 0xdc, 0x0c, 0xa6, 0xb9, 0x21, 0xe8, 0x66, 0x87, 0xce, 0xdc, - 0xdc, 0xb6, 0xb3, 0x08, 0x4e, 0x6f, 0x7f, 0x06, 0xce, 0x0f, 0xe2, 0xbe, 0x76, 0x45, 0xc4, 0xfd, 0x2b, 0x19, 0x70, - 0x25, 0x85, 0xe7, 0x6c, 0x82, 0xa0, 0x1f, 0xad, 0x7d, 0xa6, 0x41, 0x3c, 0x63, 0xcf, 0xa5, 0x4e, 0x05, 0x0c, 0xfe, - 0xa2, 0x11, 0xaf, 0x53, 0x4f, 0x4c, 0x87, 0x45, 0xf4, 0x3d, 0xd1, 0x6c, 0xa0, 0x31, 0x32, 0xdd, 0x6d, 0xef, 0x9a, - 0x21, 0x44, 0x9f, 0x98, 0x52, 0x96, 0x08, 0x80, 0xf3, 0x2f, 0x2b, 0x84, 0xfb, 0xb7, 0x82, 0x84, 0x05, 0x92, 0xe7, - 0x6a, 0xd7, 0xc4, 0x0d, 0xb0, 0x56, 0xcb, 0x19, 0x77, 0x24, 0x82, 0xd9, 0x98, 0xcb, 0x4c, 0xf4, 0x48, 0x12, 0x67, - 0x90, 0xca, 0x66, 0x5b, 0xc3, 0xdc, 0xdb, 0x06, 0x33, 0x21, 0xca, 0x11, 0x0c, 0xde, 0xbd, 0x85, 0x0d, 0x26, 0xb5, - 0x29, 0x25, 0x4e, 0x43, 0x35, 0x24, 0xf9, 0xb2, 0x17, 0xdb, 0xd5, 0x9d, 0x74, 0x1b, 0x68, 0x32, 0x7f, 0xf7, 0xc5, - 0xc1, 0x7d, 0x64, 0xfb, 0xbc, 0x55, 0xec, 0x85, 0x49, 0xb5, 0x7c, 0xda, 0xba, 0x74, 0xae, 0xbd, 0xb8, 0x46, 0x2f, - 0x4d, 0x5f, 0xb5, 0xdf, 0x58, 0x9f, 0xe7, 0x20, 0x47, 0x45, 0x9f, 0xf7, 0x97, 0x0b, 0x08, 0x9a, 0xba, 0x8c, 0x3b, - 0x01, 0x2e, 0x18, 0x51, 0x7a, 0xae, 0x33, 0x02, 0x5b, 0xc2, 0x3c, 0x2d, 0x9b, 0x2b, 0xbc, 0x3c, 0x3f, 0x38, 0x4d, - 0xa8, 0x54, 0xe8, 0x35, 0xbf, 0xaf, 0xde, 0xab, 0xb5, 0xc7, 0xe5, 0x61, 0xff, 0xbd, 0x48, 0xce, 0x40, 0x91, 0x76, - 0x46, 0x7e, 0xb4, 0xac, 0x83, 0x78, 0xdb, 0x9a, 0xbe, 0xbd, 0x96, 0x3f, 0x4c, 0x48, 0xa6, 0xca, 0x6d, 0x08, 0x16, - 0x93, 0xbe, 0xdf, 0x65, 0xf0, 0x93, 0x6c, 0x45, 0x4a, 0x0c, 0x34, 0x8a, 0x5d, 0xc6, 0x3c, 0xd9, 0xa4, 0x5e, 0x37, - 0x15, 0xdd, 0xf8, 0x50, 0xcf, 0x76, 0x18, 0x6f, 0xe0, 0xb1, 0x9e, 0x7c, 0x34, 0x77, 0xaa, 0xee, 0x5a, 0xf8, 0xba, - 0xba, 0x13, 0xda, 0xed, 0xed, 0xeb, 0x45, 0x69, 0x5e, 0x77, 0x27, 0xda, 0x3a, 0x45, 0xcf, 0xeb, 0xff, 0xeb, 0x39, - 0xe3, 0xe0, 0x6d, 0x0a, 0xef, 0x05, 0xf8, 0x76, 0x7c, 0xf6, 0x3c, 0x03, 0x8a, 0x96, 0x59, 0xb4, 0x32, 0xb9, 0xc6, - 0x39, 0x0e, 0x18, 0x55, 0xa8, 0xf3, 0x9a, 0xa9, 0x36, 0x4e, 0x6c, 0x58, 0xef, 0x78, 0x79, 0x55, 0x00, 0x71, 0x87, - 0x6b, 0x59, 0x6e, 0xe2, 0xc2, 0xfc, 0xe6, 0x99, 0x12, 0x92, 0xcd, 0x63, 0x6d, 0xd5, 0xe9, 0x77, 0x49, 0x49, 0x0e, - 0x03, 0x6e, 0x73, 0xe9, 0xc3, 0x4d, 0xe5, 0xa1, 0x0b, 0xdd, 0x2e, 0xca, 0x09, 0x22, 0x95, 0xba, 0x13, 0xa8, 0x70, - 0x6c, 0x8b, 0x15, 0x75, 0xa9, 0xed, 0x1b, 0xdf, 0x17, 0xfc, 0xb2, 0x10, 0x7c, 0x63, 0x27, 0x36, 0x31, 0x5b, 0xa9, - 0x66, 0x24, 0xe1, 0x67, 0x10, 0xcc, 0x71, 0xe5, 0x99, 0xda, 0xed, 0xf0, 0x7f, 0x14, 0x4d, 0x45, 0x0a, 0xe8, 0x12, - 0x87, 0x08, 0x99, 0x99, 0x63, 0x8a, 0x1e, 0xac, 0x10, 0x3a, 0x8b, 0x94, 0x0f, 0x76, 0x73, 0xf0, 0x7d, 0xeb, 0xe7, - 0xb6, 0xae, 0xda, 0xe5, 0x5e, 0xd1, 0xd3, 0x34, 0x25, 0x5a, 0x52, 0xa8, 0xa4, 0x91, 0xb5, 0x43, 0x7d, 0xad, 0xaf, - 0xdd, 0x48, 0x41, 0x2d, 0xb2, 0x20, 0x7a, 0x9d, 0xaf, 0x0d, 0x20, 0x4d, 0x2a, 0xfe, 0xc2, 0xbf, 0x7f, 0x16, 0x89, - 0x37, 0xb5, 0x88, 0x86, 0xfa, 0x3a, 0x6d, 0x5d, 0x7d, 0x15, 0x8f, 0x0d, 0xd7, 0x56, 0xfd, 0x18, 0xe5, 0xe6, 0x46, - 0xca, 0xfb, 0x89, 0xf9, 0xf3, 0xaf, 0x36, 0x0d, 0x8d, 0xc0, 0x49, 0xf3, 0xe6, 0x76, 0xee, 0x30, 0xe7, 0x9e, 0x23, - 0x35, 0x1c, 0xb2, 0x6f, 0x40, 0x6e, 0x91, 0x2f, 0xb5, 0x6b, 0x22, 0x71, 0x81, 0xb0, 0x89, 0x60, 0x43, 0xdc, 0x47, - 0xc6, 0x8c, 0x6e, 0x5d, 0xe3, 0xe0, 0xdd, 0xa5, 0x4c, 0x9d, 0x96, 0x6a, 0x2e, 0xa7, 0x42, 0x99, 0x49, 0x2a, 0xfa, - 0xd5, 0x46, 0x7f, 0x76, 0xe5, 0x94, 0xb8, 0x0e, 0x2a, 0xbf, 0x8d, 0x38, 0x75, 0x1b, 0xcd, 0xb4, 0xbf, 0x95, 0xaf, - 0x7a, 0x5c, 0xd4, 0x5f, 0xd2, 0xe3, 0xbd, 0xb5, 0x47, 0x6e, 0x4d, 0x2d, 0x3d, 0xe2, 0xfe, 0x6a, 0xbb, 0xaf, 0xf2, - 0x39, 0x0e, 0x22, 0x54, 0x3b, 0x21, 0xc6, 0xa5, 0x88, 0x02, 0x0e, 0xe0, 0x15, 0xf1, 0x5f, 0x90, 0xeb, 0xf1, 0xac, - 0x4e, 0xd1, 0x8f, 0x3d, 0xd0, 0xde, 0x6d, 0x9e, 0x03, 0x4e, 0xa9, 0x72, 0xca, 0xbe, 0x23, 0x6b, 0xb3, 0x2c, 0xd2, - 0xae, 0x77, 0x66, 0xb3, 0xa8, 0x58, 0x11, 0x00, 0xc8, 0xde, 0xe9, 0x53, 0x97, 0x75, 0x28, 0xb7, 0x1b, 0x08, 0xb7, - 0x4a, 0x66, 0xc2, 0x4c, 0x14, 0xfe, 0xba, 0x63, 0xc0, 0x0b, 0x21, 0xce, 0x0d, 0x5f, 0x79, 0x49, 0xe3, 0x44, 0x45, - 0x6c, 0x88, 0x1f, 0x94, 0x07, 0xc7, 0xe1, 0xd6, 0xfe, 0xb0, 0x6d, 0x64, 0x22, 0x87, 0xe8, 0x60, 0x35, 0x4a, 0xf7, - 0xc6, 0x77, 0x44, 0x76, 0x3f, 0xde, 0x5f, 0x6b, 0x89, 0x40, 0x4b, 0xcb, 0xf7, 0x6a, 0x57, 0x13, 0xce, 0x9f, 0xde, - 0x76, 0x15, 0x9b, 0x94, 0x19, 0xc5, 0xb7, 0x54, 0xb6, 0xaf, 0xbe, 0xff, 0x8a, 0x7e, 0x16, 0x25, 0x99, 0xc2, 0xd7, - 0xb2, 0x85, 0xe7, 0xd6, 0x32, 0x23, 0x0d, 0x00, 0x55, 0x24, 0x46, 0x73, 0xc9, 0xd3, 0x2e, 0xe9, 0x30, 0x10, 0x6d, - 0xfc, 0x58, 0x6c, 0x9a, 0x45, 0xa8, 0x28, 0x7b, 0xc0, 0x66, 0xa3, 0x1b, 0x32, 0x08, 0x4f, 0xd0, 0x8b, 0x7f, 0xa5, - 0x03, 0x2f, 0x2a, 0xe7, 0xca, 0xd2, 0xad, 0x2f, 0x6f, 0xeb, 0x6f, 0xd2, 0xf5, 0xa4, 0xd6, 0xbb, 0x32, 0x5c, 0x2c, - 0x68, 0x46, 0xbe, 0xf2, 0x5f, 0x0d, 0xe0, 0x75, 0x48, 0xd3, 0x19, 0x0b, 0x7f, 0x62, 0xea, 0x1e, 0x79, 0x5b, 0x99, - 0xf7, 0xdb, 0x65, 0x73, 0x3e, 0x68, 0x1f, 0xbc, 0xa4, 0xaa, 0x3f, 0xe0, 0xf8, 0xc8, 0x79, 0xb8, 0xbf, 0x8a, 0x69, - 0x6e, 0x45, 0xc1, 0x80, 0xe7, 0xa3, 0x15, 0x4d, 0xba, 0xab, 0x47, 0x2b, 0x22, 0x8c, 0x25, 0x4e, 0x2d, 0x6e, 0x75, - 0x21, 0x93, 0xa3, 0xdc, 0x42, 0xdf, 0xc9, 0xcb, 0xdc, 0xe2, 0x3a, 0xda, 0xcb, 0xcc, 0xf4, 0x94, 0x55, 0xf7, 0x1b, - 0xc2, 0xa8, 0x8f, 0xcc, 0x2e, 0x5a, 0x05, 0xa7, 0x95, 0x46, 0xb8, 0xa1, 0x5e, 0x6b, 0x8a, 0x05, 0xce, 0x8d, 0x82, - 0x5a, 0xd5, 0x3b, 0x4f, 0xbb, 0xc6, 0x41, 0xb6, 0x99, 0xd3, 0x8a, 0xd0, 0xed, 0x57, 0xb8, 0xa7, 0xb0, 0xae, 0xf3, - 0xe0, 0x6a, 0x4e, 0x34, 0x38, 0x8d, 0xdb, 0x6d, 0xb3, 0x88, 0x16, 0xb2, 0x8b, 0x15, 0xfd, 0x7a, 0x00, 0xfe, 0x8b, - 0x1d, 0x8a, 0x0f, 0x5b, 0xa9, 0xb1, 0x15, 0x23, 0x2b, 0x34, 0xf5, 0x76, 0x8e, 0x08, 0xff, 0xc2, 0xb7, 0xe4, 0x76, - 0x5b, 0xaa, 0x08, 0x35, 0x75, 0xb3, 0x6a, 0x7b, 0xed, 0x64, 0xbf, 0x34, 0x49, 0xfb, 0x61, 0x9e, 0x9e, 0x10, 0x2a, - 0x51, 0x7b, 0x73, 0x68, 0x88, 0x25, 0xd7, 0x46, 0x9c, 0x1b, 0x4c, 0x48, 0xe3, 0xbf, 0xbf, 0x11, 0x90, 0x13, 0x29, - 0xe9, 0x70, 0x39, 0xf6, 0x2c, 0xc5, 0x48, 0xa2, 0xf9, 0xc8, 0xe0, 0x75, 0x0a, 0x8b, 0xb4, 0x95, 0x27, 0xd7, 0x2d, - 0x75, 0x43, 0xdd, 0x35, 0x7d, 0x92, 0xaa, 0xe3, 0xbc, 0x38, 0xc2, 0xdd, 0xa9, 0x82, 0x46, 0xf5, 0xe6, 0xe4, 0x0c, - 0x49, 0xdb, 0x99, 0x17, 0x42, 0xf2, 0x41, 0xbc, 0x96, 0x44, 0x8a, 0xed, 0x27, 0x59, 0xea, 0x3e, 0xbe, 0x39, 0x88, - 0x0a, 0x84, 0x8b, 0x70, 0x8c, 0xc4, 0xfe, 0x14, 0x63, 0x8a, 0xee, 0x2c, 0x4a, 0x82, 0x4d, 0xd5, 0xc9, 0x19, 0x3a, - 0xd3, 0x7c, 0x02, 0x81, 0x65, 0x37, 0xc8, 0xe8, 0xa0, 0x2e, 0x62, 0x7e, 0xf4, 0xed, 0x10, 0x37, 0xbf, 0xe5, 0xe0, - 0x1a, 0x6d, 0xcf, 0x8c, 0x33, 0xa5, 0xb6, 0xf8, 0xa7, 0x39, 0x5c, 0x9f, 0xc0, 0xec, 0xee, 0x50, 0xc2, 0x89, 0x38, - 0x92, 0x50, 0xaf, 0x3f, 0x57, 0x3f, 0x6c, 0x22, 0x85, 0xce, 0x09, 0xad, 0x0d, 0xb4, 0xf8, 0x34, 0xa7, 0xab, 0x05, - 0x1f, 0xc6, 0x61, 0xc7, 0x90, 0xa9, 0x92, 0xfc, 0x2e, 0xfa, 0xdc, 0xcf, 0x05, 0x18, 0xde, 0x43, 0x5c, 0xe7, 0x7b, - 0x67, 0x47, 0xcd, 0xc2, 0x2d, 0x84, 0xed, 0x4f, 0xa3, 0x84, 0x1c, 0xf4, 0x6b, 0xe5, 0xe7, 0x88, 0x5f, 0x7d, 0xa4, - 0x67, 0xb2, 0xe1, 0x87, 0x43, 0xb4, 0xb8, 0x96, 0xb0, 0x24, 0xc3, 0xe8, 0xfd, 0x8b, 0x57, 0x18, 0xf6, 0x12, 0x18, - 0x3c, 0x83, 0xbd, 0x05, 0x02, 0xe0, 0xf6, 0xe8, 0x27, 0x0c, 0xb5, 0x54, 0x0a, 0xc2, 0xb9, 0xe4, 0x21, 0x41, 0x62, - 0x5c, 0xca, 0xd5, 0xda, 0xa4, 0x4f, 0xc0, 0x5a, 0x3b, 0x4e, 0x1d, 0x34, 0x26, 0x3d, 0xcf, 0x92, 0xe6, 0xcb, 0x98, - 0x3f, 0x0b, 0x14, 0x2c, 0x3f, 0x34, 0x35, 0xdd, 0x83, 0xa0, 0xea, 0xca, 0x18, 0x6b, 0xba, 0xa3, 0x1d, 0x04, 0xef, - 0xaf, 0xd5, 0x33, 0xa2, 0xfc, 0xdd, 0x1a, 0x93, 0x1d, 0x04, 0x85, 0x82, 0x2d, 0x6e, 0xc8, 0xa1, 0x10, 0x62, 0x57, - 0xe3, 0xce, 0xbe, 0x8b, 0x4e, 0x65, 0xa9, 0x99, 0xdc, 0x6e, 0x94, 0x4d, 0x33, 0x4c, 0x98, 0x62, 0x87, 0x56, 0xf2, - 0x05, 0x45, 0x89, 0x5d, 0xbb, 0x5a, 0x94, 0x33, 0xbf, 0xdb, 0xfa, 0x38, 0xb6, 0x50, 0x58, 0xf5, 0x39, 0x98, 0xe5, - 0xc4, 0xb4, 0x6d, 0x97, 0x81, 0xdc, 0xd9, 0x1b, 0x64, 0xaa, 0x29, 0x1b, 0x43, 0x98, 0x77, 0xcc, 0x47, 0xe6, 0xf0, - 0x3d, 0xb2, 0x3b, 0x0f, 0x99, 0xbb, 0xc9, 0x65, 0x2f, 0x3f, 0xeb, 0xf5, 0xcf, 0x1c, 0xa0, 0x90, 0xc6, 0xc0, 0xb1, - 0x79, 0xde, 0x10, 0x6b, 0x99, 0x98, 0x2e, 0x3b, 0x56, 0xba, 0x83, 0x41, 0xc1, 0xeb, 0x1c, 0x28, 0x54, 0xd2, 0x94, - 0x38, 0x71, 0x3d, 0x84, 0x35, 0xa2, 0x1c, 0xde, 0x3b, 0x31, 0xd7, 0xc9, 0x44, 0xb2, 0xf1, 0xaf, 0xf6, 0x3e, 0x5c, - 0xb4, 0x01, 0xfa, 0x78, 0x66, 0xa3, 0x16, 0x16, 0x89, 0x38, 0x85, 0x21, 0x3f, 0xaa, 0x79, 0xac, 0x49, 0xe8, 0x03, - 0x27, 0x03, 0xa9, 0xa0, 0x97, 0x0a, 0xfc, 0x6f, 0x91, 0x9c, 0xb1, 0x72, 0x4a, 0x01, 0x2a, 0xa2, 0xb5, 0x6b, 0xfe, - 0x75, 0xef, 0x7a, 0x4c, 0x82, 0x7a, 0xb5, 0x00, 0x6e, 0x2d, 0x25, 0x92, 0x9f, 0xfb, 0xdb, 0x30, 0x3a, 0xcc, 0x8c, - 0x93, 0xce, 0xf3, 0xea, 0x57, 0x4f, 0x2e, 0x22, 0x99, 0xa2, 0x2d, 0x04, 0x4f, 0x5d, 0x0c, 0x4c, 0xe4, 0x21, 0x9e, - 0x9b, 0x76, 0xd0, 0xa5, 0xc6, 0xa1, 0xfc, 0xcb, 0xae, 0xe3, 0x68, 0x6c, 0x16, 0xe3, 0x04, 0x42, 0x95, 0xea, 0xf2, - 0x3c, 0x73, 0x5d, 0xd6, 0x8b, 0x3d, 0x69, 0xa2, 0xae, 0xac, 0xf4, 0x5b, 0xa8, 0x98, 0x37, 0xba, 0x3a, 0x45, 0x6d, - 0x31, 0xad, 0x93, 0x97, 0x6d, 0x56, 0x66, 0xd5, 0x04, 0x6f, 0x43, 0xb6, 0x11, 0x4e, 0x76, 0xc1, 0x7e, 0x3a, 0xc7, - 0x4b, 0x77, 0x0d, 0x8d, 0x12, 0xbc, 0x84, 0x54, 0xd1, 0xdf, 0x99, 0x16, 0x0e, 0x24, 0x5a, 0x51, 0xb2, 0xf6, 0xa5, - 0xff, 0x66, 0x37, 0x9c, 0xe4, 0x5c, 0x47, 0xef, 0x50, 0x7b, 0x1c, 0x8a, 0x66, 0x3c, 0x26, 0x6b, 0x9c, 0xe7, 0x74, - 0x29, 0x70, 0xc9, 0x92, 0x72, 0xee, 0x05, 0xbb, 0x2b, 0x90, 0xf2, 0xfa, 0xcb, 0x16, 0x09, 0x99, 0x70, 0xfb, 0x3c, - 0x19, 0xb8, 0x8c, 0x09, 0xd2, 0x83, 0xde, 0xf5, 0x03, 0xd5, 0x58, 0xe0, 0xee, 0x97, 0x39, 0xe7, 0x7f, 0xae, 0x48, - 0x92, 0x86, 0x78, 0x68, 0x11, 0x1c, 0xa6, 0xda, 0xaf, 0xc0, 0xad, 0x63, 0xc0, 0xb5, 0x59, 0x99, 0x3e, 0xf8, 0xf5, - 0xf8, 0x40, 0x34, 0x02, 0xff, 0xf9, 0xf8, 0x2b, 0xe2, 0xd0, 0x83, 0x67, 0x2b, 0x42, 0xb2, 0xae, 0x87, 0x8b, 0x34, - 0xff, 0xd5, 0xee, 0x13, 0x80, 0x45, 0xb8, 0x91, 0x74, 0x2c, 0x01, 0x1d, 0xdf, 0x0d, 0x0b, 0xcc, 0x53, 0x60, 0x97, - 0xd1, 0x1f, 0xb3, 0x87, 0x95, 0x6b, 0x1c, 0x2a, 0x4e, 0xb4, 0x85, 0x71, 0xb8, 0x24, 0x58, 0xde, 0xd2, 0xb9, 0x8a, - 0x40, 0x06, 0x07, 0xa4, 0x5e, 0xde, 0x19, 0xc7, 0x23, 0xf7, 0x91, 0x15, 0x1c, 0xf8, 0x66, 0x58, 0xb6, 0x47, 0x06, - 0x1c, 0xea, 0x88, 0x1e, 0xd0, 0xe1, 0xd6, 0x20, 0x43, 0x4d, 0x14, 0x63, 0x0b, 0x3e, 0x3e, 0xaa, 0xc7, 0x0c, 0xf2, - 0x5c, 0x0e, 0xf8, 0xfa, 0xc0, 0xa0, 0xe2, 0x30, 0x81, 0xfc, 0x5d, 0x08, 0x83, 0x3a, 0xec, 0xad, 0x05, 0x80, 0xd2, - 0x27, 0x88, 0xa1, 0x13, 0x87, 0xd4, 0x1b, 0xd0, 0xe4, 0x7b, 0x90, 0xd2, 0x08, 0xfe, 0xa2, 0x22, 0x33, 0x1a, 0x3d, - 0x15, 0xb3, 0x50, 0x18, 0x45, 0x2b, 0x64, 0xa8, 0x4d, 0x88, 0x34, 0x75, 0xf7, 0x96, 0x11, 0xf9, 0xb1, 0x3d, 0xf0, - 0x65, 0x69, 0xaf, 0x45, 0x22, 0x55, 0xce, 0x78, 0x1f, 0x40, 0xa9, 0x39, 0xb8, 0x0a, 0xd4, 0x3d, 0x53, 0x7d, 0x4e, - 0xc5, 0xda, 0xcc, 0xb2, 0x58, 0x78, 0x20, 0x1f, 0xe2, 0x62, 0x7c, 0x15, 0x5d, 0xbe, 0xad, 0x28, 0x9e, 0xc5, 0xdf, - 0xfd, 0xba, 0xe9, 0x43, 0x0a, 0xff, 0x52, 0xf0, 0xd5, 0x59, 0x73, 0xe5, 0x84, 0x75, 0x9e, 0x20, 0x5d, 0x37, 0x18, - 0x74, 0x5b, 0x4b, 0x2c, 0x4f, 0xce, 0xf4, 0xeb, 0x76, 0x31, 0xa5, 0x6a, 0xaa, 0xde, 0xce, 0x03, 0x08, 0xa4, 0xf6, - 0x9d, 0x49, 0x67, 0xd0, 0x2c, 0x24, 0xcb, 0x0c, 0x13, 0xfc, 0xb9, 0xc3, 0x6e, 0x50, 0x91, 0x06, 0x5e, 0xb6, 0x96, - 0x5e, 0xe1, 0x73, 0x3c, 0xae, 0xe8, 0x25, 0xa7, 0xf1, 0xb7, 0x77, 0xa4, 0x3c, 0x6d, 0xfd, 0x54, 0x2e, 0xe7, 0x65, - 0xd1, 0x97, 0xa6, 0x5f, 0xd1, 0x6f, 0x52, 0xb7, 0x3c, 0xee, 0x22, 0x02, 0xe9, 0xff, 0x2a, 0xd7, 0x35, 0x8d, 0xbe, - 0x0a, 0x7b, 0xb1, 0x8b, 0x60, 0xf4, 0xec, 0xb6, 0x6e, 0x0e, 0x39, 0x53, 0x5a, 0x94, 0x1a, 0x0c, 0x4d, 0x3a, 0xfe, - 0x32, 0x0a, 0x4b, 0xd7, 0x94, 0x76, 0xee, 0xa7, 0x3b, 0xbd, 0x54, 0x47, 0x26, 0xfe, 0x6d, 0x2f, 0x7f, 0xc8, 0x3a, - 0x6a, 0x44, 0x17, 0xfe, 0x0f, 0xfe, 0x3c, 0xa2, 0x9c, 0x2f, 0x75, 0x4a, 0xa5, 0x1d, 0xd4, 0x47, 0x55, 0x72, 0x3c, - 0x1d, 0x07, 0xca, 0x68, 0x14, 0xcf, 0xd4, 0xf1, 0xcc, 0x69, 0x26, 0xe8, 0x89, 0x6e, 0x90, 0xac, 0xe5, 0x00, 0x2d, - 0x80, 0x9a, 0x92, 0x11, 0xa7, 0xea, 0x04, 0x37, 0x13, 0xa7, 0xd7, 0x45, 0x27, 0x48, 0x4e, 0x0b, 0xc7, 0xe8, 0x73, - 0x59, 0x0c, 0x51, 0x29, 0xeb, 0xdb, 0xab, 0x23, 0xaa, 0x1e, 0x65, 0xdb, 0xd2, 0xb7, 0x8a, 0x8d, 0x76, 0xa8, 0x83, - 0x15, 0x73, 0x60, 0x97, 0x97, 0xcc, 0xd4, 0x72, 0xe6, 0x60, 0xe6, 0xa7, 0x67, 0xc0, 0x9e, 0x03, 0x66, 0xe7, 0x0c, - 0x31, 0x47, 0x11, 0xaa, 0xc4, 0xd2, 0x18, 0x14, 0x17, 0x76, 0x92, 0x48, 0x7d, 0x3e, 0xef, 0x8e, 0x52, 0x15, 0x73, - 0x6a, 0x2a, 0xaf, 0x07, 0xb0, 0x2d, 0xb1, 0xf2, 0x57, 0x34, 0xa1, 0x1f, 0xe9, 0x16, 0x23, 0xfc, 0x8d, 0x8a, 0xe3, - 0xfc, 0x7e, 0x7e, 0x9b, 0x9a, 0x29, 0x01, 0x13, 0x43, 0x4e, 0x5d, 0x9d, 0x60, 0x5d, 0xa5, 0x98, 0x96, 0xc5, 0x99, - 0x96, 0xe7, 0x7c, 0x36, 0xb6, 0x25, 0xd6, 0x42, 0x38, 0x5b, 0xde, 0xf6, 0xc6, 0x5d, 0x5e, 0x30, 0x26, 0x92, 0x24, - 0x96, 0x6d, 0x5e, 0x4d, 0x07, 0x20, 0xc1, 0x1d, 0x62, 0x9b, 0x7e, 0xc1, 0xb7, 0xa2, 0x88, 0x07, 0xb0, 0x9b, 0xcc, - 0xce, 0x62, 0xab, 0x4c, 0x07, 0xe3, 0xe0, 0x96, 0xff, 0xd5, 0xb6, 0x86, 0x02, 0x21, 0x11, 0x9f, 0x08, 0x70, 0x49, - 0x74, 0x36, 0x83, 0x3a, 0x85, 0x0c, 0x37, 0xf1, 0x9d, 0xa2, 0xc9, 0x77, 0xb4, 0xfa, 0x8e, 0x88, 0xec, 0xdb, 0xab, - 0x88, 0x28, 0x4a, 0xb9, 0x3c, 0x6a, 0xc5, 0x49, 0x8e, 0x68, 0x4e, 0xc6, 0x97, 0x8e, 0xa4, 0x9d, 0x34, 0xe3, 0x4a, - 0x4d, 0x6f, 0x8f, 0xdf, 0x65, 0x10, 0xe9, 0x57, 0xe7, 0xb9, 0x15, 0xc7, 0x79, 0x21, 0x0a, 0xca, 0x07, 0xdc, 0x86, - 0x51, 0x8d, 0x56, 0xbe, 0x99, 0xf3, 0x80, 0x76, 0x66, 0x78, 0x38, 0x9d, 0xb5, 0x6f, 0xb6, 0x2d, 0xf8, 0x72, 0x1c, - 0x0e, 0x63, 0xd3, 0xbe, 0x7f, 0xfe, 0xae, 0x7e, 0x6f, 0xc6, 0x87, 0x57, 0xde, 0x49, 0xea, 0x1d, 0x0f, 0x60, 0xea, - 0xda, 0x98, 0xad, 0x73, 0x70, 0x9e, 0xc6, 0xd8, 0x22, 0xfa, 0x5f, 0xda, 0xc6, 0x67, 0xa5, 0x7f, 0x02, 0xee, 0xc1, - 0x9d, 0x64, 0x59, 0xfa, 0xc5, 0x99, 0x46, 0x8b, 0xfc, 0x89, 0xe5, 0x49, 0xad, 0x1e, 0x74, 0x5c, 0x9a, 0x5c, 0xbc, - 0x42, 0xba, 0x3c, 0x4b, 0xbf, 0x9c, 0x2d, 0xf4, 0x8f, 0xd3, 0x55, 0x00, 0xff, 0x8f, 0x73, 0xa4, 0xb8, 0x3f, 0xa6, - 0xd9, 0x8b, 0x74, 0xe3, 0xfb, 0xb3, 0x9b, 0xd5, 0xeb, 0x82, 0x3d, 0x3a, 0x4f, 0xb6, 0x6c, 0x5d, 0x0b, 0xad, 0xa9, - 0x1b, 0x17, 0xd4, 0x9d, 0xdd, 0x66, 0xed, 0x1b, 0xeb, 0x53, 0x6b, 0xe8, 0xbb, 0x98, 0x48, 0x3f, 0x7f, 0x44, 0x3f, - 0x5d, 0x7b, 0x8a, 0x0b, 0xc3, 0x7e, 0xa7, 0xba, 0x1e, 0x35, 0x33, 0x9d, 0x0a, 0x12, 0x9a, 0x97, 0x3c, 0xdd, 0x37, - 0x39, 0xaf, 0xe5, 0xf8, 0x72, 0xf4, 0x34, 0xa2, 0xa6, 0x7d, 0x47, 0x19, 0xdd, 0x4b, 0x82, 0x31, 0xea, 0x2a, 0x35, - 0x30, 0xfa, 0xe2, 0x55, 0x05, 0x06, 0x01, 0xaa, 0xf3, 0xfa, 0x40, 0x8a, 0xc0, 0xe0, 0xc3, 0x21, 0x8f, 0xe5, 0x06, - 0x03, 0x27, 0x4b, 0xeb, 0x20, 0xf5, 0xf2, 0x20, 0x1c, 0xa9, 0xea, 0xe2, 0x6d, 0x26, 0xa0, 0xc0, 0xeb, 0xa9, 0xfe, - 0x1b, 0xdd, 0x9c, 0x1b, 0xe7, 0x69, 0xc6, 0x87, 0x73, 0x43, 0xe9, 0x52, 0x71, 0xf1, 0xda, 0xae, 0x62, 0x1c, 0x16, - 0xd5, 0x56, 0x25, 0x53, 0x32, 0x65, 0x0e, 0x13, 0xf3, 0x33, 0x41, 0x7a, 0xde, 0xa8, 0x43, 0xee, 0x97, 0x4f, 0xf2, - 0x9a, 0x2e, 0x71, 0x65, 0x92, 0x8d, 0x42, 0xf8, 0x3f, 0x34, 0x55, 0x6b, 0x0e, 0xa4, 0x46, 0xe0, 0x72, 0x70, 0xb5, - 0x54, 0xde, 0xb6, 0xb4, 0x9f, 0x3f, 0x2e, 0xdf, 0xa7, 0xb7, 0x95, 0x24, 0xf9, 0x2f, 0x4d, 0xd8, 0x98, 0xf3, 0xc9, - 0x28, 0xb4, 0x29, 0xc4, 0x0d, 0x4c, 0x45, 0x3b, 0xc6, 0x4f, 0x0a, 0x2f, 0x08, 0xea, 0xf3, 0x0e, 0x45, 0x03, 0xb0, - 0x79, 0x95, 0x8a, 0xdc, 0x19, 0x68, 0x59, 0xa2, 0x6c, 0xdd, 0xe8, 0x6b, 0xc3, 0xf7, 0x38, 0x78, 0xd5, 0x70, 0xeb, - 0xde, 0xcb, 0xa6, 0x0a, 0x94, 0x4d, 0x5b, 0x59, 0xbc, 0x0a, 0x25, 0xcf, 0xd4, 0x4b, 0x9d, 0x2b, 0x69, 0x17, 0x0e, - 0x7e, 0xa6, 0xe2, 0xe8, 0x57, 0x12, 0x81, 0x5d, 0x39, 0xc8, 0x00, 0xc7, 0xed, 0x36, 0xc7, 0x19, 0x02, 0x11, 0x94, - 0x85, 0x56, 0x20, 0xd4, 0x22, 0x55, 0xa7, 0xbe, 0x33, 0x62, 0x35, 0x01, 0xe4, 0x8a, 0xbd, 0x8b, 0x56, 0xc8, 0x9f, - 0x65, 0x06, 0x3a, 0xb0, 0xa3, 0x3d, 0x37, 0x2e, 0xbe, 0x3e, 0x25, 0xe8, 0xd7, 0x12, 0x7b, 0x67, 0x54, 0xc7, 0xc8, - 0x69, 0x3e, 0x3f, 0x58, 0x26, 0xc6, 0x6d, 0x31, 0xde, 0xb6, 0x91, 0x39, 0x81, 0x29, 0x50, 0x89, 0x99, 0xd6, 0xaa, - 0x65, 0x04, 0x39, 0x4c, 0xb2, 0x13, 0x8f, 0x34, 0x19, 0x2b, 0x96, 0xf7, 0x40, 0x60, 0xce, 0x30, 0x6e, 0xd3, 0x98, - 0x55, 0x2b, 0xa4, 0x60, 0x04, 0xc3, 0xd0, 0xf8, 0x60, 0x31, 0x12, 0xe6, 0x95, 0x80, 0x0c, 0x1c, 0x29, 0x52, 0x10, - 0xdf, 0xed, 0x68, 0x7e, 0x30, 0xa5, 0x47, 0x9c, 0xa8, 0x70, 0x8f, 0xca, 0x29, 0xdd, 0x60, 0xa8, 0xe7, 0x82, 0x05, - 0x4c, 0x31, 0xc5, 0x46, 0x72, 0xa0, 0x32, 0xdc, 0xaa, 0x90, 0xb1, 0x5c, 0xf7, 0xb6, 0x3f, 0xbd, 0x97, 0x34, 0x6c, - 0xfa, 0x4a, 0x48, 0x1a, 0xd4, 0x5a, 0x71, 0xe1, 0x03, 0x76, 0xd1, 0xb3, 0xf7, 0x4d, 0x76, 0xc8, 0x34, 0x91, 0x31, - 0xda, 0x4b, 0xa2, 0x7c, 0x69, 0x7f, 0xac, 0x15, 0x5b, 0x63, 0x80, 0xab, 0xde, 0xe9, 0xfa, 0x84, 0x9c, 0xf2, 0x4e, - 0x8b, 0x82, 0x0c, 0x32, 0x2c, 0x23, 0xfa, 0xf0, 0x9f, 0x2e, 0xf2, 0xcd, 0x58, 0x3f, 0x4b, 0xa8, 0x53, 0x93, 0xd6, - 0x2f, 0x7a, 0xb3, 0xcd, 0xce, 0xc9, 0x6c, 0x01, 0xa0, 0xf0, 0x5f, 0xad, 0x3f, 0xb1, 0x35, 0x22, 0xd4, 0x50, 0x04, - 0x2f, 0x01, 0x57, 0x1c, 0xf0, 0xa8, 0xf6, 0x34, 0x42, 0xe1, 0x20, 0x09, 0x4d, 0x99, 0xb3, 0xdd, 0xdf, 0x10, 0xb4, - 0x71, 0x4d, 0x41, 0x87, 0x3e, 0x85, 0xa6, 0xff, 0xea, 0xb3, 0x5f, 0xa0, 0x5a, 0x45, 0xd1, 0x26, 0x76, 0x4d, 0xb1, - 0x38, 0xa4, 0x70, 0x93, 0x6b, 0x87, 0x77, 0x89, 0x10, 0xe0, 0xec, 0x5f, 0xcc, 0x29, 0x4e, 0x16, 0xd6, 0x9d, 0x4d, - 0x08, 0x96, 0x0a, 0x46, 0x52, 0xa2, 0x43, 0x19, 0x73, 0x9d, 0x39, 0x1e, 0x56, 0xe3, 0x97, 0x2e, 0xe8, 0xe1, 0x10, - 0x5e, 0xc7, 0xf8, 0xfc, 0xe1, 0x79, 0xc7, 0x3b, 0x56, 0x68, 0x99, 0xb5, 0x84, 0x29, 0xa4, 0x87, 0x7c, 0x0f, 0x83, - 0xca, 0x63, 0xcf, 0x05, 0xd3, 0xea, 0xfe, 0xa1, 0x54, 0x68, 0xe7, 0x39, 0xa8, 0xa9, 0x17, 0xc0, 0xc4, 0xc2, 0x4d, - 0x29, 0x0d, 0xbb, 0x92, 0x40, 0x6a, 0x53, 0x04, 0x30, 0xfe, 0xe4, 0x13, 0x22, 0x1e, 0xc4, 0x41, 0xa9, 0x96, 0xd0, - 0xf1, 0xe6, 0x68, 0xa3, 0x56, 0x77, 0xb1, 0x30, 0xbe, 0x05, 0x2b, 0x80, 0xb6, 0xc4, 0x86, 0xe1, 0x61, 0xf1, 0xa9, - 0x94, 0x37, 0x21, 0x01, 0xb5, 0xab, 0x20, 0x85, 0x95, 0x83, 0xb5, 0x1f, 0x4c, 0x80, 0xaa, 0x5d, 0x93, 0x28, 0xfd, - 0xb6, 0x52, 0x44, 0x0a, 0x8b, 0x42, 0x35, 0x8f, 0xec, 0xde, 0x96, 0x75, 0xda, 0x50, 0x35, 0x4f, 0x91, 0x2e, 0x95, - 0xda, 0x2e, 0x71, 0x6d, 0xff, 0xa7, 0x99, 0x42, 0xe6, 0x3e, 0x3b, 0x61, 0xf5, 0xb6, 0xf6, 0x14, 0xea, 0x64, 0x54, - 0x4f, 0xf1, 0xf2, 0x51, 0xb5, 0xc2, 0xdf, 0x56, 0xe6, 0xa0, 0x01, 0x0f, 0xc6, 0x45, 0xfa, 0x67, 0xef, 0xc3, 0x35, - 0xe4, 0x9e, 0xbc, 0x6f, 0x55, 0xa1, 0x48, 0x8e, 0x07, 0x33, 0xec, 0x2f, 0x3a, 0x81, 0xe3, 0x09, 0xdb, 0x36, 0x09, - 0x58, 0xeb, 0xf8, 0x1e, 0x49, 0x41, 0x8a, 0xfc, 0x36, 0xd6, 0x86, 0xc4, 0xdc, 0xf0, 0xa3, 0x44, 0x71, 0x4b, 0x29, - 0x5d, 0x25, 0x4f, 0x4e, 0x6d, 0xbb, 0x82, 0x52, 0x13, 0x47, 0xe0, 0xd8, 0xfa, 0xca, 0x11, 0xff, 0x6c, 0xfb, 0x6a, - 0x97, 0x2b, 0x89, 0x43, 0xb1, 0xc8, 0x4f, 0x75, 0x3f, 0x29, 0x0f, 0x93, 0x01, 0xac, 0x26, 0xd4, 0x19, 0x0b, 0x47, - 0x95, 0x24, 0x80, 0xc0, 0x04, 0xa8, 0x25, 0x3c, 0x17, 0x6a, 0x91, 0xdb, 0x30, 0xa9, 0xd9, 0x56, 0xce, 0x55, 0xfb, - 0x64, 0x6b, 0x6a, 0xcd, 0xc0, 0xcd, 0xc5, 0xc6, 0x71, 0x75, 0x67, 0x07, 0xb2, 0xd2, 0x43, 0x02, 0x9d, 0x7b, 0x53, - 0x62, 0x41, 0x53, 0xe0, 0xc3, 0xd1, 0xee, 0xbe, 0x03, 0x10, 0x45, 0x23, 0x46, 0xff, 0x59, 0xc1, 0xe4, 0xa4, 0xdf, - 0xe8, 0x06, 0x7c, 0x4b, 0x95, 0x79, 0x41, 0xd9, 0xe0, 0x72, 0x77, 0x7e, 0x63, 0xd5, 0x03, 0xcf, 0x4c, 0x98, 0x93, - 0x72, 0xed, 0xc1, 0x46, 0x66, 0x89, 0x9a, 0x70, 0xfd, 0x7f, 0x35, 0xd7, 0x90, 0x1c, 0x80, 0x5c, 0x8c, 0x7d, 0xab, - 0xac, 0xc0, 0xd5, 0x2c, 0x74, 0x40, 0x61, 0x1f, 0x0c, 0x9c, 0x0a, 0x1b, 0x76, 0x03, 0x6e, 0x7e, 0x90, 0xa6, 0x95, - 0xef, 0x12, 0xe8, 0xfe, 0xa7, 0x58, 0x63, 0xdf, 0xbb, 0x25, 0x6b, 0xd8, 0xe8, 0x4d, 0x41, 0xd3, 0xe8, 0x5e, 0x33, - 0x59, 0xd3, 0xd9, 0xca, 0x0c, 0x55, 0x23, 0xaf, 0xd6, 0x8f, 0x45, 0xdc, 0x1a, 0x9e, 0x9f, 0xc9, 0x79, 0xbd, 0x8f, - 0xe9, 0xa5, 0x6e, 0x3c, 0xf6, 0x45, 0xdd, 0xe1, 0xab, 0x1b, 0xd1, 0x86, 0xb3, 0xa2, 0x88, 0x9d, 0x0f, 0xeb, 0x9e, - 0x8a, 0xb4, 0x5b, 0x47, 0xbb, 0x78, 0x5b, 0x30, 0xa7, 0x64, 0x54, 0x67, 0xd0, 0x14, 0xba, 0x0a, 0xc7, 0xba, 0x7e, - 0xbe, 0xb8, 0x02, 0xeb, 0x8e, 0x96, 0x55, 0x82, 0x37, 0x26, 0x5d, 0xd4, 0x61, 0xd7, 0xf7, 0x8c, 0x0f, 0x89, 0xaa, - 0x8f, 0x46, 0xeb, 0x34, 0xf7, 0x05, 0x34, 0xa0, 0xe5, 0x0b, 0x3a, 0xb4, 0x21, 0xab, 0xd1, 0x5d, 0x69, 0xf2, 0xa4, - 0x56, 0xd5, 0x6f, 0x79, 0x0c, 0xde, 0xb1, 0x7c, 0x35, 0x56, 0x9d, 0x8e, 0x7f, 0x19, 0xbe, 0xbc, 0xac, 0xee, 0x90, - 0xf4, 0xb9, 0x97, 0x01, 0x60, 0xda, 0xe6, 0x93, 0x41, 0xbf, 0x2f, 0x02, 0x91, 0x91, 0xdf, 0x62, 0x3c, 0x7b, 0x29, - 0x4b, 0x00, 0x1d, 0x57, 0x05, 0xbd, 0x33, 0x4d, 0x7a, 0x79, 0x4f, 0x24, 0xe2, 0xc6, 0xc0, 0x78, 0x83, 0x42, 0x15, - 0x52, 0xef, 0x34, 0x81, 0xb8, 0x47, 0x1d, 0x13, 0xe9, 0x45, 0xd5, 0xf7, 0xab, 0x04, 0x07, 0xcf, 0xa2, 0xd5, 0x6e, - 0xff, 0xb3, 0x68, 0x0a, 0xe4, 0xc4, 0xc1, 0x44, 0x5d, 0xe1, 0x84, 0xc7, 0x3f, 0x9e, 0x68, 0xbf, 0x64, 0x47, 0xaa, - 0xe9, 0x30, 0x5f, 0xc5, 0x57, 0x76, 0xaa, 0x5a, 0xf1, 0xcb, 0xbc, 0xef, 0x67, 0x8b, 0xb4, 0xf1, 0x32, 0xd2, 0xab, - 0xd9, 0x5e, 0xed, 0x6c, 0xa2, 0xba, 0x53, 0x58, 0x1e, 0x35, 0x59, 0x51, 0x1d, 0x13, 0xab, 0x56, 0x5f, 0x1d, 0x7a, - 0xe5, 0xed, 0x65, 0xf6, 0x9b, 0x25, 0x61, 0xe6, 0xec, 0x69, 0xe1, 0xde, 0xec, 0x6c, 0xc9, 0x83, 0xee, 0xe7, 0xe0, - 0xbf, 0xc7, 0x46, 0x8a, 0x2d, 0x53, 0xbb, 0x28, 0x47, 0x02, 0x00, 0x0e, 0x12, 0xbf, 0x3a, 0xbd, 0xf9, 0x7b, 0x2d, - 0x2a, 0x75, 0xb3, 0xda, 0x69, 0x71, 0xe9, 0x1f, 0x23, 0x4a, 0x4b, 0xe3, 0x38, 0xe5, 0x08, 0xa2, 0x71, 0x6d, 0x7e, - 0xc1, 0x24, 0x73, 0xdf, 0x62, 0xb5, 0x12, 0x6b, 0xc9, 0x09, 0x14, 0x18, 0xb9, 0xd7, 0x35, 0xcf, 0x5d, 0xab, 0x53, - 0x58, 0xa6, 0xb6, 0xe6, 0xb0, 0xb5, 0xc3, 0xbe, 0x83, 0xaa, 0xaf, 0xa9, 0xc3, 0x55, 0x16, 0xbe, 0xad, 0x78, 0x21, - 0x5f, 0x4a, 0x79, 0x72, 0xea, 0xe6, 0x8d, 0x60, 0x29, 0x3e, 0x0a, 0x54, 0x73, 0x46, 0xf0, 0xa2, 0x56, 0x5f, 0x59, - 0xc4, 0x4a, 0x7e, 0x28, 0x09, 0xbd, 0xd8, 0x3d, 0x17, 0xd9, 0x80, 0xab, 0xb2, 0xfe, 0xbe, 0xfa, 0xdc, 0x23, 0x95, - 0x7d, 0x74, 0x61, 0x95, 0xda, 0x8e, 0x63, 0x6e, 0xa3, 0xa6, 0x2a, 0x78, 0xf3, 0xde, 0x35, 0xd8, 0x35, 0xb0, 0x3c, - 0x19, 0xcb, 0x25, 0x46, 0x06, 0x3e, 0xd6, 0xd6, 0x53, 0x7d, 0x65, 0x5e, 0x3d, 0x5a, 0xc9, 0x58, 0x48, 0xca, 0x2b, - 0x04, 0xa2, 0xd7, 0x7f, 0x96, 0x2b, 0x35, 0xac, 0xd5, 0xd9, 0x0a, 0x25, 0x1a, 0x31, 0xda, 0xbb, 0x54, 0x4e, 0x76, - 0x4d, 0x8f, 0x8c, 0xe9, 0xf3, 0xee, 0x47, 0xd5, 0xd2, 0x92, 0xd9, 0x86, 0x16, 0xff, 0x54, 0x64, 0x2c, 0xa9, 0x88, - 0x6d, 0xc5, 0x16, 0x7b, 0x16, 0x77, 0x01, 0x4c, 0x3e, 0x5d, 0x30, 0x77, 0x9f, 0x50, 0x0e, 0xc6, 0xea, 0x57, 0xaa, - 0x2a, 0x37, 0x3e, 0x4f, 0xbc, 0xbe, 0x6f, 0x60, 0x26, 0x91, 0x45, 0x1e, 0x05, 0x36, 0x2b, 0xeb, 0x69, 0x4f, 0x6f, - 0x73, 0x52, 0xa3, 0x5f, 0x18, 0x85, 0x56, 0x79, 0xc0, 0xe7, 0x9a, 0x79, 0xf2, 0xe6, 0x7d, 0xa7, 0x1b, 0x41, 0x86, - 0xa3, 0x8d, 0xb4, 0x41, 0xdb, 0x6d, 0x48, 0x7a, 0x8b, 0xf8, 0x0f, 0x29, 0xd3, 0x5f, 0x94, 0xf9, 0xea, 0xfb, 0xe1, - 0x7c, 0x5d, 0x4e, 0x50, 0x35, 0x7b, 0xc0, 0xe1, 0xd1, 0x58, 0x02, 0x2c, 0x20, 0x8e, 0x3e, 0x12, 0xb2, 0xb6, 0x26, - 0x28, 0x27, 0x3c, 0x52, 0xe5, 0x6c, 0x94, 0x77, 0x26, 0x7a, 0x42, 0xab, 0xca, 0xd9, 0x00, 0x5b, 0xd0, 0x8d, 0x5d, - 0xf2, 0x6d, 0x2c, 0x3c, 0x5d, 0xee, 0x77, 0x8e, 0xed, 0x81, 0x2b, 0xd7, 0x5c, 0xc0, 0x17, 0xde, 0x03, 0x77, 0xa1, - 0x5a, 0x40, 0x6b, 0x10, 0xff, 0x51, 0x54, 0xd9, 0xdd, 0xe6, 0xdc, 0x48, 0x24, 0x59, 0x28, 0x13, 0x2a, 0x6b, 0xf1, - 0x73, 0x83, 0x9c, 0xeb, 0x71, 0xe0, 0x1c, 0x29, 0x01, 0x82, 0x63, 0xc4, 0x24, 0xf6, 0xa6, 0x34, 0x54, 0x70, 0x8e, - 0x3e, 0x79, 0x2d, 0xbf, 0x60, 0xca, 0xd0, 0x05, 0x3a, 0x3d, 0xcf, 0x42, 0xc1, 0x7e, 0x60, 0x5d, 0x38, 0x3a, 0x69, - 0x39, 0xeb, 0x9f, 0x1d, 0x18, 0x01, 0xf2, 0x58, 0x79, 0x99, 0x24, 0x6c, 0x2d, 0x5a, 0xbd, 0xc9, 0xfb, 0x71, 0xa5, - 0x10, 0x2d, 0x16, 0xa8, 0xba, 0xfd, 0x02, 0x17, 0xa7, 0x25, 0x95, 0xac, 0x14, 0xb7, 0x0a, 0x15, 0x88, 0xd6, 0x9b, - 0x50, 0xf5, 0x3a, 0x7d, 0x6d, 0xdb, 0x51, 0x79, 0x69, 0x28, 0x36, 0x31, 0x04, 0x86, 0x58, 0x1f, 0x7e, 0xaa, 0xb6, - 0xe1, 0xb6, 0xb3, 0x2e, 0xee, 0x73, 0xdb, 0x7e, 0x0d, 0x5f, 0x8f, 0xc4, 0x9b, 0xca, 0xdb, 0xa6, 0x78, 0x58, 0x20, - 0xe2, 0x44, 0xd7, 0x6b, 0x0d, 0x9b, 0x93, 0x4f, 0x7f, 0x55, 0xa7, 0x4c, 0x8e, 0xe8, 0x63, 0x0f, 0x21, 0xe6, 0x42, - 0x44, 0x85, 0x48, 0xbf, 0x6f, 0x47, 0xe7, 0xca, 0x3d, 0x23, 0x44, 0xe7, 0xd8, 0x88, 0x75, 0x6c, 0x27, 0x81, 0xa7, - 0xb6, 0x14, 0xc4, 0x09, 0xdc, 0x7d, 0x27, 0x16, 0x7c, 0xf2, 0x85, 0x34, 0xe7, 0xe1, 0xf9, 0xcb, 0xdf, 0xfe, 0x4a, - 0x56, 0xea, 0xb9, 0xee, 0x2c, 0xba, 0xa6, 0xbb, 0xa4, 0x52, 0x97, 0xcf, 0x71, 0x17, 0xb3, 0xf0, 0x26, 0x6b, 0xff, - 0x7a, 0x78, 0x4b, 0x37, 0x6d, 0x48, 0x91, 0x4c, 0x51, 0xee, 0xfe, 0x4d, 0x3c, 0x35, 0x22, 0xc3, 0x5f, 0xf0, 0x8c, - 0xb1, 0xee, 0xcb, 0xaa, 0xf9, 0x60, 0xac, 0x04, 0xec, 0x3d, 0x27, 0x23, 0x73, 0x51, 0x70, 0xa3, 0x28, 0x44, 0x2b, - 0xd6, 0x83, 0xed, 0x40, 0x53, 0xf9, 0x80, 0xf1, 0x0f, 0x53, 0x6a, 0xb9, 0x8b, 0xcd, 0xf5, 0xfd, 0xd8, 0xf4, 0x38, - 0x26, 0x25, 0x23, 0x9d, 0x39, 0x1a, 0xa8, 0x15, 0xd8, 0xf6, 0x58, 0x7e, 0x39, 0x44, 0xe7, 0xb4, 0x2d, 0xb0, 0x4d, - 0xcb, 0xc7, 0x37, 0x87, 0x6c, 0x2e, 0x5f, 0x9a, 0xf1, 0x5e, 0xba, 0x79, 0xf2, 0x62, 0x99, 0xde, 0x0a, 0x7b, 0xc2, - 0x40, 0x14, 0x51, 0x05, 0x9d, 0x84, 0x22, 0xec, 0xb4, 0x5b, 0x7b, 0x82, 0x94, 0x16, 0x83, 0xf0, 0x13, 0x5c, 0x9f, - 0xb4, 0xaf, 0x45, 0x6d, 0x6a, 0x1d, 0x35, 0x41, 0xed, 0x72, 0x9e, 0x06, 0x48, 0x47, 0xc5, 0x73, 0x0b, 0xa1, 0xcf, - 0x28, 0xd0, 0x16, 0x34, 0x59, 0x29, 0xd2, 0x08, 0x43, 0x91, 0x73, 0x63, 0xaa, 0x26, 0x73, 0xb1, 0x5c, 0xf8, 0xb3, - 0x46, 0x9b, 0x34, 0xc4, 0x24, 0xe4, 0xda, 0x96, 0x5d, 0xe6, 0xeb, 0x04, 0xeb, 0x2b, 0x6b, 0x7f, 0x3d, 0xf9, 0x5b, - 0x41, 0x32, 0x05, 0xec, 0x47, 0x16, 0xaf, 0xdb, 0x97, 0xb8, 0x02, 0xde, 0xd4, 0x40, 0x51, 0x03, 0xca, 0xac, 0x3a, - 0xad, 0xdb, 0xf0, 0x80, 0x32, 0xfb, 0xcd, 0x40, 0x95, 0x9a, 0x5c, 0x59, 0xc5, 0xb7, 0xba, 0x8d, 0xb8, 0x5e, 0xb2, - 0x81, 0xb4, 0xf1, 0x76, 0xca, 0xad, 0xd3, 0x54, 0xb9, 0x12, 0xe7, 0x41, 0x25, 0xa9, 0x71, 0x0f, 0x30, 0x98, 0xe6, - 0xe9, 0x3b, 0x34, 0xe6, 0xdf, 0x8a, 0x83, 0x49, 0x5f, 0x18, 0x24, 0xab, 0xb4, 0x63, 0x01, 0x20, 0x40, 0xb7, 0x5d, - 0x72, 0xd3, 0xe4, 0x08, 0x46, 0xe4, 0x1f, 0xd0, 0xbb, 0xe0, 0x88, 0xec, 0x1d, 0xd8, 0x9d, 0xe9, 0xc3, 0xc0, 0xcc, - 0xbb, 0x9a, 0x92, 0x5d, 0x8a, 0xc2, 0x37, 0xd1, 0x37, 0xdb, 0xc5, 0x4a, 0x00, 0xdc, 0x30, 0xbb, 0xcc, 0x17, 0xaa, - 0x4c, 0xe6, 0x62, 0xab, 0xf2, 0x90, 0x9b, 0xa9, 0x4c, 0x71, 0x55, 0x13, 0x3c, 0x08, 0x82, 0x80, 0x82, 0xbc, 0x81, - 0x5c, 0xc7, 0x17, 0x0d, 0x20, 0xe8, 0x41, 0x58, 0x0c, 0x13, 0xcf, 0x8d, 0xb2, 0xbb, 0x3e, 0xaa, 0x98, 0xc2, 0xf8, - 0x54, 0x4a, 0x72, 0x72, 0xee, 0xf5, 0xc9, 0x64, 0xd8, 0x6a, 0x36, 0xec, 0xe4, 0x24, 0x21, 0xb4, 0x24, 0xb6, 0x90, - 0x53, 0xea, 0xf6, 0xae, 0x0e, 0xbd, 0x3c, 0x96, 0x05, 0x8c, 0xb6, 0x11, 0xac, 0x3b, 0x1c, 0xed, 0x3d, 0x25, 0xc2, - 0x8b, 0x65, 0x63, 0xbe, 0x13, 0xf1, 0x45, 0xaa, 0x8f, 0x81, 0x06, 0xcc, 0x9b, 0x3f, 0x07, 0xb2, 0x9a, 0xe0, 0x6f, - 0xc2, 0x8b, 0x65, 0x71, 0x9f, 0x39, 0x21, 0x2a, 0x36, 0x8b, 0xfb, 0x67, 0x1b, 0x14, 0x98, 0xae, 0x09, 0xdd, 0x40, - 0xaa, 0x81, 0x45, 0xc3, 0x1d, 0x3d, 0x58, 0xb4, 0x3f, 0xa2, 0xab, 0x62, 0x59, 0x61, 0xf4, 0xe8, 0xc1, 0x51, 0x3d, - 0x95, 0x1d, 0x4b, 0x6b, 0xa4, 0x39, 0xe2, 0x37, 0xcf, 0x11, 0x2d, 0xea, 0x36, 0xc6, 0x44, 0x63, 0x69, 0xe6, 0x1f, - 0x44, 0x79, 0xb4, 0xaf, 0x41, 0xd8, 0x3f, 0x83, 0x4d, 0xe2, 0x63, 0xc6, 0x20, 0x6f, 0x8e, 0x06, 0xf6, 0x72, 0x40, - 0x1d, 0x07, 0xcb, 0x93, 0x92, 0x82, 0x9b, 0x8b, 0x95, 0x2a, 0xcd, 0x32, 0x8a, 0x3d, 0xaf, 0x13, 0x59, 0xcb, 0x1e, - 0xe1, 0x24, 0x23, 0x26, 0xfa, 0x3c, 0x50, 0x90, 0x77, 0x5a, 0x2f, 0xff, 0xd3, 0x0a, 0xcc, 0x3a, 0x74, 0x32, 0xd6, - 0x64, 0x94, 0x2c, 0x84, 0x08, 0x6d, 0x08, 0xb7, 0x36, 0x24, 0xd7, 0x22, 0xb4, 0x1d, 0x99, 0x43, 0x1f, 0xe6, 0x4b, - 0xc1, 0x19, 0x5e, 0x82, 0x9e, 0x76, 0xb9, 0x7d, 0x71, 0xfa, 0xcd, 0x85, 0x72, 0x67, 0x83, 0x83, 0x08, 0xa4, 0xe8, - 0x5c, 0x9f, 0x1e, 0x8a, 0x17, 0x85, 0x83, 0x88, 0xb8, 0x39, 0xbc, 0x1e, 0xac, 0x3e, 0x26, 0x74, 0x56, 0x75, 0x4f, - 0x7b, 0xff, 0x85, 0x0b, 0xdf, 0xb5, 0xb5, 0x22, 0x8a, 0xd3, 0x1b, 0xb6, 0x1e, 0xa5, 0x79, 0x26, 0xad, 0x1e, 0xbb, - 0x62, 0xe0, 0x51, 0xa6, 0x22, 0xc7, 0x6f, 0xd6, 0x63, 0x8c, 0x6d, 0x08, 0x28, 0x43, 0xaa, 0xb7, 0x18, 0x02, 0x20, - 0x63, 0x9e, 0x8e, 0xfd, 0x71, 0xce, 0x26, 0xc8, 0x7b, 0x8d, 0x31, 0x17, 0xf1, 0x76, 0xed, 0x4f, 0xe0, 0xa1, 0x50, - 0x36, 0x12, 0xd7, 0xf2, 0x28, 0xc5, 0xb9, 0x07, 0x15, 0x9f, 0x46, 0xc4, 0xd6, 0x61, 0xea, 0x7c, 0x42, 0x18, 0xb2, - 0x07, 0x31, 0x66, 0x17, 0x26, 0x74, 0x7a, 0x89, 0xbe, 0x32, 0xbd, 0x0d, 0xa8, 0xbe, 0x15, 0x5b, 0x24, 0x9a, 0x97, - 0x44, 0xa2, 0xe8, 0xec, 0x84, 0xd8, 0x6c, 0x45, 0x8e, 0x1a, 0xab, 0xbd, 0x83, 0xc1, 0xe5, 0x33, 0x4e, 0x6b, 0xeb, - 0x0a, 0x30, 0xf9, 0x63, 0x98, 0x0a, 0x06, 0x9c, 0x1b, 0x4e, 0x2c, 0x79, 0x37, 0xa2, 0x1f, 0x7d, 0x20, 0xe3, 0xb7, - 0xd2, 0x22, 0xd8, 0xa3, 0x81, 0x18, 0xa9, 0x8a, 0x61, 0x05, 0xd3, 0x47, 0x21, 0xc1, 0x53, 0x17, 0x8e, 0xaa, 0x3a, - 0xf4, 0x0b, 0x22, 0xaa, 0x9d, 0x0b, 0xbb, 0x56, 0x0c, 0x98, 0x68, 0xcc, 0x1c, 0x1a, 0x2d, 0x5c, 0xc0, 0xf3, 0x74, - 0xfc, 0x7e, 0xe2, 0x7e, 0x76, 0xfe, 0xa0, 0x19, 0xf4, 0xbf, 0x26, 0xa3, 0xce, 0xf1, 0xd3, 0xfb, 0xdb, 0xf6, 0x69, - 0xbf, 0xb6, 0x33, 0xf7, 0x07, 0xea, 0xfb, 0x4f, 0xfc, 0xcb, 0x24, 0x80, 0xfc, 0x52, 0xc7, 0x6e, 0xc3, 0xf5, 0x53, - 0xe2, 0x35, 0x78, 0xb8, 0x7e, 0x72, 0x89, 0x50, 0xef, 0x00, 0xed, 0x8d, 0x4a, 0xdb, 0x86, 0x4b, 0x4c, 0xe2, 0x91, - 0xf2, 0x64, 0x35, 0x56, 0x64, 0x49, 0xad, 0x60, 0x65, 0x92, 0x6f, 0xe4, 0xae, 0xcf, 0x2e, 0xc1, 0x3d, 0xbe, 0x15, - 0xd9, 0x53, 0xae, 0x3e, 0x00, 0x17, 0xa5, 0xf3, 0x57, 0xcc, 0x3b, 0xff, 0x13, 0x55, 0xd6, 0x1d, 0xd4, 0x0c, 0xb5, - 0x96, 0x44, 0xad, 0x9a, 0x59, 0x5d, 0xb0, 0xb7, 0x04, 0xb4, 0xa6, 0x56, 0x1f, 0xca, 0xcd, 0x21, 0x7f, 0x6c, 0xb1, - 0x2e, 0x8c, 0x13, 0x4d, 0x93, 0x01, 0x79, 0x6a, 0x7e, 0xe9, 0x12, 0x43, 0x92, 0x41, 0xfd, 0xbf, 0xbe, 0x7b, 0x77, - 0x74, 0xd0, 0x4c, 0x5a, 0xde, 0x85, 0x3d, 0xde, 0xab, 0xa2, 0x62, 0x49, 0xe7, 0x1b, 0x7d, 0x7c, 0x90, 0x3c, 0xc9, - 0xc3, 0xf6, 0x79, 0xea, 0xcf, 0x0e, 0xfc, 0xd8, 0x80, 0xbe, 0xe3, 0x6d, 0xd3, 0x8e, 0xd2, 0xc7, 0x21, 0x04, 0x2c, - 0xd5, 0x2e, 0x68, 0x56, 0xc3, 0x23, 0x34, 0x58, 0xb7, 0x49, 0x55, 0x38, 0x8a, 0xa7, 0x1c, 0x1f, 0xfe, 0x03, 0xd0, - 0x41, 0x02, 0x3c, 0x6a, 0x2e, 0xcb, 0xc3, 0xb4, 0x03, 0x6b, 0x23, 0x6c, 0xf7, 0x26, 0x44, 0x2f, 0x16, 0x47, 0x6b, - 0xa7, 0x36, 0x61, 0x11, 0x69, 0xbc, 0x2b, 0x09, 0x5d, 0xdd, 0x07, 0xbd, 0x1e, 0x75, 0x9a, 0x35, 0x09, 0xa1, 0x9d, - 0x6c, 0xeb, 0xb8, 0x7a, 0xd0, 0xab, 0xac, 0xcf, 0x5f, 0x30, 0xfd, 0x58, 0xdf, 0xe3, 0x23, 0x86, 0xf5, 0x1b, 0xde, - 0x1f, 0x5c, 0x4a, 0x70, 0xb1, 0x69, 0xec, 0x7c, 0x33, 0x27, 0x0e, 0xbb, 0x59, 0x0a, 0x0b, 0x09, 0xa6, 0x97, 0x48, - 0x1b, 0xc6, 0x6a, 0x70, 0x7c, 0x11, 0x1f, 0xeb, 0xf9, 0x62, 0x40, 0x20, 0x52, 0xd9, 0x29, 0xf2, 0xc2, 0x00, 0x13, - 0xf5, 0xed, 0xcd, 0x87, 0xd4, 0xff, 0x10, 0xdf, 0xec, 0x03, 0xa9, 0x93, 0xe4, 0xd1, 0x8b, 0x45, 0x51, 0x24, 0x54, - 0xf4, 0x14, 0xff, 0xe2, 0x10, 0xca, 0xf0, 0x32, 0xd1, 0xd9, 0xa4, 0xe8, 0xf6, 0xce, 0x2d, 0x5f, 0x24, 0xbc, 0x71, - 0xe5, 0x72, 0xe9, 0x61, 0x60, 0xda, 0x02, 0x36, 0x50, 0x41, 0xc6, 0x31, 0x4b, 0xf1, 0x63, 0xe4, 0x2a, 0x43, 0x94, - 0xdd, 0xea, 0x31, 0xd4, 0x70, 0x11, 0x98, 0x3b, 0x94, 0x49, 0xc2, 0x68, 0xa1, 0x9e, 0xdb, 0xc0, 0xd3, 0x73, 0x66, - 0xe7, 0xe9, 0xdc, 0xde, 0xab, 0x62, 0xc7, 0x84, 0x89, 0x0c, 0x8a, 0xe8, 0xb1, 0xc2, 0x86, 0x5a, 0xcd, 0x97, 0x99, - 0x53, 0x6c, 0x7a, 0xe4, 0x0f, 0x43, 0x2d, 0x37, 0xe9, 0x96, 0x1d, 0xbd, 0xd2, 0x47, 0xfd, 0xd1, 0xa2, 0x13, 0x0c, - 0xd1, 0xe2, 0xee, 0xac, 0x8d, 0x72, 0xc4, 0x28, 0x6c, 0xde, 0x17, 0x80, 0xd9, 0xb7, 0x6e, 0x4b, 0xba, 0xfa, 0xc4, - 0xd5, 0xf7, 0xc2, 0xdc, 0xf3, 0x00, 0x62, 0x24, 0xcf, 0xe5, 0xc8, 0x45, 0x91, 0x03, 0x92, 0xbc, 0xab, 0x23, 0x2d, - 0x91, 0x8a, 0x10, 0x4e, 0x67, 0x1c, 0x0c, 0x8b, 0xd3, 0xb9, 0x6a, 0xea, 0xbb, 0x2c, 0x10, 0x09, 0x65, 0xb2, 0x9f, - 0xa2, 0x67, 0x7b, 0xa3, 0x71, 0x47, 0x87, 0xb5, 0x76, 0xeb, 0x20, 0x14, 0xae, 0x4c, 0xb5, 0x99, 0x70, 0xf7, 0x8c, - 0xfe, 0xeb, 0x8d, 0x97, 0x14, 0xab, 0x8e, 0x7b, 0xef, 0x53, 0x7d, 0xf9, 0x6b, 0x5e, 0x00, 0xf5, 0xfb, 0x81, 0xb3, - 0x21, 0x7f, 0xcb, 0x7d, 0xb0, 0x98, 0x32, 0x40, 0x80, 0x8f, 0x68, 0x86, 0x3a, 0xed, 0xab, 0xd9, 0x8d, 0x6f, 0x88, - 0xd9, 0xb3, 0xda, 0xd0, 0x77, 0x7e, 0xf8, 0xae, 0xae, 0xa0, 0xc1, 0x45, 0x65, 0xf4, 0x7f, 0x9e, 0x2a, 0x20, 0x98, - 0x0a, 0xfe, 0x3e, 0x6e, 0x87, 0xe3, 0x5b, 0xf0, 0x1c, 0x46, 0x7d, 0x1c, 0x69, 0xa2, 0x7b, 0x27, 0xee, 0x52, 0xaf, - 0x32, 0x4d, 0x32, 0xaf, 0x68, 0x97, 0x35, 0x2e, 0x58, 0xd6, 0x35, 0x5d, 0x5b, 0x76, 0xb0, 0x66, 0x5f, 0x02, 0x20, - 0x23, 0xdb, 0x9b, 0xaa, 0xa6, 0xf0, 0xeb, 0x4b, 0xb1, 0x08, 0x24, 0xf0, 0x9d, 0xb2, 0xbf, 0xba, 0x76, 0x7d, 0xd3, - 0xae, 0x16, 0xf1, 0xc1, 0xc0, 0x81, 0x50, 0xae, 0xf3, 0x86, 0x7b, 0x59, 0xa1, 0xcd, 0xf3, 0x25, 0xe7, 0xc6, 0xcb, - 0x88, 0x4a, 0x43, 0x21, 0x89, 0xda, 0x40, 0x9f, 0x8e, 0x3d, 0x0d, 0x10, 0x1e, 0x12, 0x4b, 0x1d, 0x64, 0x65, 0xfa, - 0x47, 0xd2, 0xde, 0x9b, 0x1b, 0xc3, 0xeb, 0xe1, 0x16, 0x97, 0x19, 0x45, 0x14, 0x76, 0x0c, 0x3c, 0x72, 0x2b, 0x56, - 0x7b, 0xb7, 0xfe, 0xe1, 0xc1, 0xd3, 0xbb, 0xab, 0x8f, 0x9f, 0x16, 0xb7, 0x43, 0xaa, 0xd5, 0x4f, 0xa7, 0xd6, 0xb2, - 0xe6, 0x93, 0x76, 0x98, 0xf7, 0x8e, 0x15, 0xbb, 0x84, 0x13, 0x69, 0x07, 0x31, 0x56, 0x6e, 0x26, 0x55, 0xa7, 0x09, - 0x70, 0x20, 0x35, 0x75, 0x9f, 0x3d, 0x73, 0xcd, 0x94, 0x3c, 0x36, 0xe8, 0x25, 0x51, 0x57, 0xa5, 0x11, 0x58, 0xa6, - 0xfd, 0xe3, 0xf1, 0xd2, 0x4b, 0x3d, 0x4d, 0xb0, 0x89, 0x6e, 0x18, 0x88, 0xc3, 0xdf, 0xb1, 0xc1, 0xaf, 0x67, 0xf7, - 0x64, 0x49, 0xa0, 0x70, 0x69, 0xeb, 0x86, 0x32, 0x0d, 0xda, 0x52, 0x21, 0x38, 0x2e, 0x5e, 0xdc, 0x2a, 0xc6, 0x93, - 0xac, 0xa9, 0x16, 0xc5, 0x43, 0x24, 0x1a, 0x70, 0x19, 0x5b, 0xca, 0x4d, 0xbe, 0x8d, 0x01, 0x0e, 0xd2, 0x11, 0xca, - 0xf5, 0xb2, 0x0a, 0xb0, 0xdb, 0xf0, 0xd7, 0xe3, 0x69, 0x1e, 0x80, 0x98, 0x1e, 0x1b, 0xf6, 0x74, 0x6f, 0xa3, 0xc9, - 0xad, 0xb9, 0x93, 0x12, 0x3f, 0x4a, 0xd9, 0x62, 0x4b, 0x0c, 0x83, 0x73, 0xa5, 0x13, 0x20, 0xa0, 0xe5, 0x6e, 0x09, - 0x20, 0xb5, 0x2c, 0x39, 0x89, 0x83, 0x85, 0x13, 0xd9, 0xde, 0x62, 0xbc, 0xdd, 0x93, 0x9e, 0x1a, 0xcf, 0xdc, 0x46, - 0xc6, 0xa4, 0xac, 0x7c, 0xbf, 0x20, 0x93, 0xf7, 0x79, 0x06, 0x16, 0xcd, 0x65, 0xf4, 0xf4, 0x5d, 0x71, 0x2a, 0x7e, - 0x98, 0x45, 0xe7, 0xe1, 0x69, 0xd4, 0xcd, 0x61, 0x96, 0x78, 0xc0, 0x4e, 0x38, 0xd3, 0x6a, 0x60, 0xac, 0x5d, 0xda, - 0x8e, 0xb4, 0x3b, 0xfb, 0x02, 0x29, 0x61, 0xcd, 0x6e, 0x76, 0x82, 0x63, 0x46, 0x5c, 0x0e, 0xba, 0xd7, 0x1b, 0x30, - 0xac, 0x6c, 0x17, 0x73, 0x73, 0x4f, 0xee, 0xa4, 0xd5, 0x53, 0x81, 0x9c, 0x81, 0x2c, 0x59, 0xd7, 0xf0, 0xbe, 0x40, - 0xb5, 0xbe, 0x78, 0x70, 0xf0, 0x76, 0x6f, 0x19, 0x77, 0x4d, 0x19, 0x65, 0x3b, 0xa2, 0x08, 0x7e, 0x65, 0x40, 0xba, - 0x56, 0xf6, 0x23, 0x77, 0x37, 0x4b, 0x1d, 0xa4, 0x14, 0xfa, 0x74, 0x3d, 0x5d, 0x1b, 0x09, 0x6f, 0x66, 0xaa, 0xe3, - 0x08, 0xf9, 0x44, 0x87, 0x41, 0x59, 0x49, 0x8a, 0xfe, 0x9f, 0xb1, 0xdf, 0x81, 0x82, 0x7a, 0xe0, 0xd7, 0xbf, 0x0b, - 0x12, 0x1c, 0xd8, 0x9d, 0xa0, 0xb5, 0xe2, 0x63, 0x09, 0xb2, 0x5b, 0x85, 0xb9, 0xa0, 0x44, 0xad, 0x7f, 0xcf, 0xcd, - 0xf5, 0xfa, 0xfb, 0x4b, 0x56, 0xaa, 0x75, 0x27, 0xdb, 0xb6, 0xf5, 0x4d, 0xae, 0x19, 0x3b, 0x62, 0x5f, 0x0e, 0x7c, - 0x70, 0x9a, 0xc9, 0xb5, 0x80, 0xa4, 0x21, 0xd3, 0xe7, 0x6e, 0x8d, 0xba, 0x91, 0x8c, 0xdc, 0x01, 0x09, 0x44, 0x08, - 0x34, 0x18, 0x94, 0x75, 0xbb, 0x2f, 0xc6, 0xe1, 0xbc, 0xb3, 0x78, 0xff, 0x73, 0x41, 0x97, 0xe8, 0xb0, 0xae, 0xce, - 0x62, 0xc9, 0x7f, 0xbf, 0x53, 0x8c, 0x64, 0x7b, 0xb4, 0xbd, 0x7f, 0x31, 0x9a, 0xe2, 0x4a, 0xa6, 0xfd, 0x83, 0xbf, - 0xbf, 0xe8, 0x2d, 0xbc, 0xd9, 0xd1, 0xc1, 0xea, 0x3e, 0x4f, 0xb9, 0x79, 0xd2, 0x17, 0x33, 0xf9, 0xae, 0x8c, 0x4c, - 0x6e, 0x10, 0x18, 0x75, 0x67, 0x75, 0xa9, 0xcb, 0xc3, 0xc8, 0xc5, 0x7a, 0xb8, 0x19, 0x4e, 0xe1, 0x36, 0x13, 0x59, - 0xb5, 0x50, 0x05, 0xe0, 0x11, 0x3a, 0x29, 0x51, 0x24, 0x49, 0x62, 0x80, 0xe8, 0xde, 0xc6, 0x3c, 0x80, 0x17, 0x35, - 0x9f, 0x35, 0xd4, 0x76, 0x46, 0x36, 0xc7, 0x01, 0xad, 0xcd, 0x1c, 0xd3, 0xb2, 0x29, 0x41, 0xe7, 0xee, 0x74, 0x84, - 0x0e, 0xbd, 0xa5, 0xf5, 0x54, 0x97, 0x8a, 0x7d, 0xd3, 0xb3, 0xb6, 0x8e, 0xc8, 0x27, 0x71, 0x6b, 0x75, 0x90, 0xe6, - 0x2a, 0xa7, 0xe2, 0x66, 0xaa, 0x9e, 0xa3, 0x77, 0x16, 0x8e, 0x40, 0xdf, 0x5a, 0x1e, 0xac, 0x71, 0x11, 0x6c, 0x8a, - 0xee, 0x2c, 0x15, 0x55, 0xc5, 0x96, 0xfb, 0x9d, 0x4c, 0xa7, 0xed, 0x9d, 0x01, 0xb2, 0x4e, 0x98, 0xee, 0x1e, 0x12, - 0x48, 0x3c, 0x7a, 0x17, 0x60, 0xb0, 0x67, 0x52, 0x4c, 0xab, 0xea, 0xbc, 0x62, 0x82, 0x87, 0xa5, 0x3c, 0xf3, 0xbd, - 0xd9, 0xb3, 0xc3, 0xa8, 0x61, 0x3c, 0x76, 0xf8, 0x05, 0x25, 0x05, 0xb3, 0x37, 0xd1, 0xcd, 0xdf, 0xcb, 0xd7, 0xf5, - 0x09, 0xb7, 0x46, 0x0e, 0xb9, 0x75, 0x07, 0xd7, 0xef, 0xf4, 0x7d, 0xe6, 0x62, 0x56, 0xdf, 0x7e, 0x36, 0x2e, 0x66, - 0x46, 0xc9, 0x77, 0x6a, 0xa4, 0x3d, 0x24, 0xde, 0x6b, 0x00, 0xb6, 0x00, 0xca, 0x92, 0x09, 0xe8, 0x60, 0xb5, 0x2e, - 0x97, 0x4e, 0xd7, 0x29, 0x69, 0xaa, 0x3d, 0xf3, 0x6a, 0xbb, 0xb1, 0xcd, 0x85, 0x67, 0x4c, 0xb6, 0x58, 0x9b, 0x4e, - 0x5b, 0xc2, 0xe4, 0xb5, 0xae, 0xdf, 0xe8, 0xfa, 0x97, 0x15, 0x81, 0x9a, 0xa9, 0x5c, 0x71, 0x5f, 0x2b, 0x6b, 0x08, - 0x3e, 0x85, 0x45, 0x98, 0x0a, 0xf0, 0xac, 0x3a, 0x81, 0xaa, 0xf5, 0x43, 0xdb, 0xfb, 0x1b, 0x16, 0xdb, 0xb3, 0x9d, - 0x76, 0x01, 0x14, 0x9e, 0x25, 0xee, 0x9c, 0x2b, 0x77, 0x74, 0xe3, 0x74, 0x13, 0x53, 0xf6, 0xe3, 0x17, 0xa8, 0x93, - 0x83, 0x99, 0xdb, 0x0b, 0x1a, 0x4b, 0xc0, 0x93, 0x22, 0x13, 0xc4, 0xe4, 0xe7, 0x40, 0x08, 0x6d, 0xa4, 0xd2, 0x99, - 0x86, 0xc7, 0x68, 0xf7, 0x5a, 0x29, 0x0b, 0xa6, 0x76, 0xef, 0xc9, 0x6d, 0xd8, 0x74, 0x04, 0xaa, 0xb3, 0xef, 0xa4, - 0xdc, 0xa8, 0x97, 0x30, 0x02, 0x3a, 0xdd, 0x6b, 0xf5, 0xe3, 0x9f, 0x93, 0xb9, 0x86, 0x7d, 0x64, 0xc7, 0x1b, 0xd1, - 0x0d, 0x40, 0x0e, 0x87, 0x4b, 0x28, 0x65, 0xed, 0x93, 0xea, 0xdf, 0xfb, 0xb2, 0xd1, 0xf0, 0x09, 0xc3, 0x38, 0x49, - 0x54, 0x71, 0xda, 0xe6, 0xd6, 0x40, 0xe9, 0x4f, 0xee, 0x9d, 0x12, 0xa6, 0x20, 0x10, 0x4d, 0xb2, 0x72, 0x3e, 0x05, - 0x2c, 0x3c, 0xe5, 0x81, 0x4a, 0x98, 0x46, 0x2c, 0xd1, 0x0e, 0xad, 0x34, 0x1b, 0x5d, 0x82, 0x19, 0x06, 0x5c, 0xfb, - 0x0b, 0x8d, 0xd6, 0x9d, 0xec, 0xad, 0xa3, 0x06, 0x6f, 0xd0, 0xd2, 0xe8, 0x77, 0x43, 0x93, 0x00, 0x84, 0x9c, 0x1e, - 0xdc, 0xf7, 0x2c, 0x46, 0xc7, 0x15, 0x9d, 0x47, 0x0f, 0x64, 0x22, 0xd4, 0x94, 0xeb, 0x4e, 0x0e, 0x80, 0x39, 0xdd, - 0x72, 0x69, 0xa8, 0xc7, 0x60, 0x9a, 0x5e, 0x40, 0x54, 0xc0, 0x8a, 0x0e, 0xa0, 0xd3, 0xd8, 0x0d, 0x65, 0x79, 0xc3, - 0x0c, 0x05, 0x08, 0x82, 0xec, 0x1b, 0x84, 0xfd, 0xa9, 0x3a, 0xe6, 0x6a, 0x86, 0x5d, 0x86, 0x19, 0x1c, 0x87, 0x86, - 0xf6, 0x14, 0xfc, 0x04, 0x6c, 0xa2, 0x09, 0x08, 0x50, 0x6e, 0x12, 0x62, 0x0f, 0x6a, 0xfe, 0x2b, 0x0f, 0x49, 0x7b, - 0xdd, 0x34, 0xf5, 0x09, 0xa6, 0x38, 0x2c, 0x83, 0x75, 0x5b, 0xb6, 0x57, 0xb7, 0xaa, 0x8c, 0xe3, 0x1a, 0x60, 0x6c, - 0xe9, 0x1c, 0x47, 0x61, 0x1a, 0xe2, 0x7f, 0x0d, 0xa8, 0x0b, 0x73, 0xab, 0x76, 0x13, 0xfa, 0x66, 0x49, 0x53, 0x3e, - 0x9a, 0xdc, 0x1f, 0x1b, 0x9a, 0x13, 0xfd, 0xbe, 0xc0, 0x8c, 0x6b, 0x89, 0x4b, 0x16, 0x7a, 0xda, 0x06, 0x65, 0xa7, - 0x6b, 0x5c, 0x65, 0xfc, 0x6a, 0xf4, 0xcb, 0xb7, 0xab, 0x57, 0x31, 0xc4, 0x8c, 0x28, 0x60, 0x6b, 0xde, 0x59, 0xc7, - 0x27, 0xda, 0x7d, 0x37, 0xe6, 0x97, 0xa7, 0xa8, 0x71, 0xa3, 0x94, 0x58, 0x2c, 0x92, 0xf7, 0x15, 0x6e, 0x6b, 0x3e, - 0xd8, 0x5e, 0xf9, 0xf8, 0xeb, 0x79, 0x28, 0x04, 0x4f, 0xa8, 0x92, 0x20, 0xd1, 0x40, 0x37, 0xad, 0xd7, 0x82, 0x96, - 0xde, 0x97, 0x94, 0x66, 0x9e, 0xfb, 0xcb, 0xa6, 0x5d, 0x02, 0x42, 0x55, 0x03, 0x39, 0x3f, 0x45, 0x93, 0x2f, 0xa6, - 0xd3, 0x31, 0x82, 0x4f, 0x9a, 0x9c, 0x23, 0x0c, 0xa7, 0xdd, 0x7e, 0x97, 0x9f, 0xfe, 0x26, 0x47, 0x07, 0x9e, 0xfb, - 0x49, 0xea, 0xdb, 0xa1, 0xf0, 0xe9, 0x77, 0xbd, 0x18, 0x7d, 0xf5, 0x8d, 0x90, 0xbe, 0xed, 0xc4, 0xc6, 0x41, 0x90, - 0x37, 0xf2, 0x42, 0x84, 0x08, 0x76, 0x49, 0x20, 0xcc, 0x65, 0xfd, 0x46, 0xbc, 0x85, 0xaf, 0xe8, 0x2d, 0x35, 0x47, - 0x4f, 0xa3, 0x03, 0x3d, 0x9c, 0xb0, 0xbe, 0x8b, 0x0f, 0xa3, 0x2f, 0xb0, 0xe6, 0xe1, 0xb3, 0x00, 0x90, 0x8e, 0x61, - 0x15, 0xc0, 0xda, 0x60, 0xee, 0x18, 0x6e, 0xeb, 0xf4, 0xc4, 0x5a, 0xe6, 0x00, 0xbb, 0xdc, 0xc9, 0x71, 0x43, 0x77, - 0x0e, 0x41, 0xc1, 0xbc, 0x1d, 0x58, 0x23, 0xff, 0xcf, 0xb4, 0xa1, 0x3b, 0xab, 0x98, 0x58, 0x06, 0x22, 0xcd, 0x11, - 0x09, 0x0d, 0x5f, 0x77, 0x2f, 0x02, 0x07, 0xf0, 0x11, 0x83, 0xaf, 0x41, 0xc5, 0xf3, 0xdc, 0xe4, 0x57, 0xf5, 0xf3, - 0x4b, 0xc4, 0xde, 0x14, 0xaf, 0xeb, 0xa9, 0xbb, 0x2b, 0x0f, 0x7f, 0xa7, 0x14, 0x00, 0xbd, 0x54, 0x76, 0x15, 0x98, - 0xc9, 0xc1, 0x26, 0x32, 0xfc, 0x5c, 0x2f, 0xa1, 0x32, 0x6d, 0xf6, 0x84, 0x10, 0x2e, 0x48, 0x39, 0xb9, 0x1e, 0x2f, - 0x46, 0x7e, 0x02, 0x2a, 0x02, 0x6d, 0x02, 0xc8, 0xb2, 0x3f, 0x82, 0x45, 0xca, 0x01, 0x81, 0x78, 0x17, 0x17, 0x7d, - 0xea, 0x2d, 0x0d, 0x92, 0x98, 0xdd, 0x4b, 0x11, 0x30, 0x88, 0xcb, 0x84, 0x12, 0x34, 0x6c, 0x4d, 0xd9, 0xb7, 0x10, - 0xb9, 0x23, 0x74, 0xd8, 0x11, 0x66, 0x06, 0x5d, 0x25, 0xf2, 0x1f, 0x1d, 0x2d, 0xa9, 0x82, 0x87, 0xe9, 0xc9, 0xb3, - 0x15, 0xcd, 0x86, 0x93, 0x06, 0x12, 0x12, 0xf8, 0x50, 0x88, 0x03, 0x1b, 0x6f, 0x48, 0xa2, 0x60, 0x3d, 0x48, 0xfe, - 0x74, 0xd9, 0xf2, 0xba, 0xf3, 0x2c, 0x3b, 0xbe, 0x63, 0x68, 0x0e, 0x79, 0x8c, 0x44, 0x11, 0x94, 0xc2, 0xef, 0xa0, - 0xa4, 0x45, 0xa6, 0x12, 0x50, 0xae, 0x15, 0xd9, 0xe1, 0xa9, 0x2a, 0x31, 0x01, 0x5a, 0xa0, 0x07, 0xd1, 0xbd, 0xcb, - 0x42, 0xd3, 0x74, 0xf0, 0xda, 0xa1, 0x61, 0x2c, 0x83, 0xa9, 0x0e, 0x2e, 0x5b, 0xa1, 0x38, 0x32, 0xe9, 0x90, 0x51, - 0x70, 0x72, 0xeb, 0xac, 0xcb, 0x26, 0x37, 0xbf, 0xbd, 0x57, 0x74, 0x74, 0x0c, 0x64, 0x75, 0x9e, 0xde, 0x3c, 0xcf, - 0x66, 0x08, 0x06, 0xe9, 0xe3, 0x69, 0x57, 0xf1, 0x72, 0x9a, 0x81, 0x69, 0xd5, 0xf6, 0x6d, 0x19, 0x2e, 0x37, 0xb1, - 0xe4, 0xaa, 0xdb, 0x87, 0xb9, 0xcc, 0x67, 0xa7, 0x93, 0xec, 0xb7, 0xd6, 0xe0, 0xc8, 0x69, 0x66, 0xfd, 0x46, 0xf9, - 0x3c, 0x3f, 0x32, 0x95, 0x6f, 0x0e, 0x93, 0x44, 0x6a, 0xd7, 0x50, 0xad, 0xc2, 0x0c, 0x3f, 0x19, 0x44, 0xbd, 0x06, - 0x54, 0x1b, 0xad, 0x18, 0x6e, 0xe0, 0xc5, 0xe6, 0xc9, 0xd2, 0x75, 0xcc, 0x8c, 0xab, 0xc0, 0x2c, 0x26, 0x95, 0x18, - 0xdf, 0x8b, 0x04, 0x19, 0xb4, 0xa7, 0xfb, 0x54, 0x34, 0xd6, 0x97, 0x40, 0x27, 0x8b, 0x68, 0x03, 0x06, 0xe6, 0x10, - 0xea, 0x58, 0xa0, 0xb1, 0x71, 0x98, 0x45, 0x6d, 0x2b, 0x33, 0x9a, 0x2a, 0x83, 0x61, 0x0c, 0xb5, 0x01, 0x5c, 0xdd, - 0xaa, 0x85, 0x94, 0x8c, 0xb2, 0xee, 0x32, 0x1a, 0x28, 0xa1, 0x63, 0x19, 0x2b, 0x3c, 0x52, 0x32, 0x5c, 0x19, 0x71, - 0x1a, 0xe0, 0xcb, 0xd3, 0xff, 0xf7, 0x27, 0x32, 0x6a, 0xe9, 0xee, 0x48, 0xde, 0xb9, 0xec, 0xe8, 0x4a, 0x33, 0x9e, - 0xa7, 0xcb, 0xe9, 0x8b, 0xd4, 0x6d, 0xa9, 0x96, 0xf9, 0xc3, 0xe8, 0x18, 0x85, 0xaf, 0xed, 0xdb, 0x6d, 0x25, 0x1a, - 0xce, 0x30, 0x62, 0xae, 0x7c, 0xe3, 0x16, 0xf5, 0x5a, 0x8a, 0xee, 0xb7, 0x8c, 0x9c, 0x4a, 0x75, 0xc7, 0x1b, 0x97, - 0x1a, 0x5e, 0x29, 0xfd, 0x87, 0x79, 0x5e, 0xcc, 0xb1, 0xdd, 0xf6, 0x71, 0xb5, 0xb2, 0xcb, 0x89, 0xce, 0x9b, 0xe7, - 0x24, 0xe1, 0x6d, 0x8f, 0x63, 0x2b, 0x91, 0xe2, 0xb1, 0x34, 0x7f, 0x57, 0x4d, 0xb6, 0x9b, 0x5f, 0x7d, 0x2e, 0xc8, - 0x5a, 0x4c, 0xba, 0xd5, 0x56, 0xa5, 0x35, 0xf3, 0xe0, 0xdd, 0x9e, 0x39, 0xd2, 0x53, 0x12, 0x34, 0xdc, 0x2e, 0xe4, - 0xd3, 0x20, 0xd2, 0x5b, 0x66, 0x74, 0x58, 0x93, 0x57, 0xbe, 0xb1, 0x09, 0x0e, 0xe1, 0x78, 0x6b, 0x34, 0x0f, 0x93, - 0x9d, 0x4e, 0xea, 0xc5, 0xff, 0x33, 0x5b, 0xf8, 0x36, 0x75, 0xf2, 0x57, 0x5c, 0xe9, 0x20, 0xc5, 0xc5, 0x14, 0x1f, - 0x8e, 0x11, 0xcc, 0x97, 0xf4, 0x1d, 0x0a, 0x8f, 0xa6, 0x9c, 0x06, 0x06, 0x21, 0x66, 0xcf, 0xbe, 0x73, 0xf7, 0x76, - 0xbc, 0x25, 0xce, 0xa8, 0x2c, 0x6b, 0x8a, 0x25, 0x18, 0xa4, 0x79, 0x1d, 0x10, 0x00, 0x57, 0x2e, 0x88, 0x5d, 0x81, - 0xc8, 0x96, 0x17, 0xd1, 0xe2, 0xdd, 0x2f, 0x4b, 0xa3, 0xb8, 0x29, 0xf1, 0x99, 0xec, 0xf6, 0x44, 0x30, 0xc0, 0x2d, - 0xc8, 0x0e, 0xc7, 0x0e, 0x16, 0xc4, 0x1c, 0x09, 0xde, 0x15, 0x65, 0x58, 0x92, 0x3a, 0x50, 0x2c, 0x5a, 0xd4, 0x05, - 0x13, 0x13, 0x29, 0x64, 0x6b, 0xab, 0x84, 0x00, 0x69, 0xa2, 0xf6, 0x12, 0x58, 0xd4, 0x34, 0x7b, 0xa2, 0xea, 0x62, - 0x92, 0xbb, 0x21, 0xf7, 0x34, 0x1e, 0xfc, 0x3c, 0x94, 0xcc, 0xb1, 0x37, 0x89, 0x90, 0x7f, 0xbd, 0xd9, 0xfa, 0x05, - 0xf6, 0x0e, 0x7e, 0xd1, 0x10, 0xbe, 0x9a, 0xc2, 0x1a, 0x92, 0x30, 0xab, 0x5c, 0x78, 0xab, 0x24, 0x40, 0x81, 0xb2, - 0xac, 0x4f, 0x8b, 0x03, 0x46, 0x1f, 0x0a, 0xca, 0x16, 0xcb, 0x39, 0x89, 0xd9, 0x71, 0x11, 0x5b, 0x72, 0x2f, 0xfa, - 0xfc, 0xfc, 0x65, 0x1c, 0xef, 0x11, 0x81, 0xca, 0xad, 0xf2, 0xfe, 0x48, 0xc9, 0x01, 0x03, 0x33, 0xfd, 0x29, 0x8d, - 0xe8, 0xdc, 0x7f, 0x3f, 0xd5, 0x0f, 0x39, 0xf0, 0x4b, 0xe0, 0x38, 0xd0, 0xa7, 0x2c, 0x5a, 0xce, 0x06, 0xaa, 0x7b, - 0x9c, 0x53, 0xfb, 0x58, 0x5c, 0x22, 0xae, 0x4c, 0x42, 0xa0, 0xbb, 0x5c, 0x48, 0x82, 0xc5, 0xa7, 0x60, 0x48, 0x36, - 0x01, 0xd3, 0x58, 0x61, 0x73, 0xad, 0x79, 0x77, 0x80, 0x2e, 0x36, 0x58, 0xc1, 0x2b, 0x1c, 0x0c, 0xbd, 0xb6, 0x66, - 0x4e, 0x6b, 0x35, 0xbd, 0x13, 0x25, 0x89, 0x6c, 0xb2, 0xdb, 0x8f, 0x23, 0x7b, 0x43, 0x1a, 0x22, 0xfc, 0xb9, 0x51, - 0x5a, 0x14, 0x8a, 0xe6, 0x6a, 0x85, 0x88, 0x7d, 0xaf, 0x52, 0x4e, 0x32, 0xa9, 0x5a, 0x6a, 0x72, 0x5b, 0x29, 0x21, - 0xd6, 0x85, 0x7f, 0x14, 0xd4, 0xcd, 0xa8, 0x3f, 0x25, 0x37, 0xd0, 0x37, 0xe2, 0x4d, 0x02, 0x6f, 0xac, 0xf5, 0x21, - 0x28, 0x9a, 0x68, 0xfc, 0x08, 0x8a, 0xc5, 0xc1, 0x04, 0x4f, 0x3c, 0x97, 0x61, 0x09, 0x48, 0xa7, 0x78, 0xe8, 0xc5, - 0x84, 0x8b, 0x40, 0x0b, 0xce, 0x59, 0xbe, 0x7b, 0xa7, 0x79, 0xa0, 0xd3, 0x7a, 0x21, 0x89, 0xd9, 0x5e, 0x75, 0xba, - 0xf4, 0x8a, 0x81, 0xf3, 0xeb, 0x4c, 0x59, 0x22, 0xee, 0x49, 0x5e, 0x6e, 0x37, 0x96, 0xd1, 0xc6, 0x22, 0xe6, 0x74, - 0xa6, 0x82, 0x3f, 0x9b, 0x7a, 0x5b, 0x27, 0x16, 0xbf, 0x1d, 0x4f, 0xad, 0x8c, 0xd7, 0x01, 0xee, 0x12, 0x6f, 0xca, - 0xa0, 0x54, 0xc4, 0xeb, 0x41, 0x84, 0x48, 0x90, 0x12, 0x9d, 0x46, 0x86, 0xd2, 0x63, 0x3f, 0x48, 0xcc, 0x06, 0xd4, - 0x88, 0x1d, 0xd8, 0x39, 0xca, 0x6e, 0x85, 0x9f, 0xfb, 0xbb, 0xf5, 0xf0, 0x7b, 0x95, 0x3e, 0xe9, 0x2d, 0xcc, 0x4a, - 0xf3, 0xa4, 0x1a, 0x56, 0x60, 0xd9, 0x71, 0xfb, 0x97, 0x67, 0xae, 0xc2, 0xe0, 0xdc, 0x56, 0xc3, 0x9d, 0x3e, 0x97, - 0xef, 0x2f, 0xe0, 0xef, 0x4f, 0xbf, 0xaf, 0xf9, 0x02, 0xb3, 0x8e, 0x94, 0x50, 0xd7, 0x6e, 0x23, 0x22, 0xee, 0xc5, - 0xab, 0xab, 0x14, 0x5a, 0x80, 0x2c, 0xbf, 0x80, 0x67, 0xc7, 0xb7, 0x46, 0xba, 0x4f, 0x8e, 0x54, 0x20, 0x11, 0xf2, - 0x56, 0x41, 0x58, 0x09, 0x38, 0x52, 0xd8, 0x2c, 0xb2, 0xa0, 0x4f, 0x25, 0x74, 0x0d, 0x3f, 0x25, 0xbe, 0xbc, 0x9a, - 0x2b, 0x7e, 0x0c, 0xe9, 0x04, 0x74, 0xd8, 0x9d, 0x0f, 0x22, 0xb0, 0x41, 0xce, 0x7a, 0xc9, 0x68, 0xde, 0xc9, 0x67, - 0xa3, 0xc8, 0xb4, 0x63, 0xa5, 0xfd, 0xda, 0xa8, 0xdb, 0x3e, 0x1e, 0xdf, 0x29, 0x06, 0x3c, 0x38, 0x6c, 0x6e, 0x37, - 0x69, 0x20, 0x6f, 0xd5, 0xde, 0xfb, 0x7a, 0xc7, 0xb5, 0x5d, 0x90, 0x7c, 0xb2, 0xb4, 0x83, 0x28, 0xa4, 0xdb, 0x8d, - 0x9c, 0x2a, 0xea, 0x17, 0x45, 0xfb, 0x22, 0x2d, 0xef, 0xee, 0x12, 0x8f, 0x7b, 0xf5, 0x24, 0x4e, 0x2e, 0x8e, 0x73, - 0x4d, 0x25, 0xe2, 0xc8, 0x97, 0xa8, 0x2f, 0x4f, 0xd1, 0x66, 0x5a, 0x53, 0x83, 0xab, 0x5c, 0xab, 0xa6, 0x44, 0x5e, - 0x8a, 0x9e, 0xd9, 0xcd, 0xe2, 0xaf, 0xb8, 0xa6, 0x42, 0x33, 0xe0, 0xfc, 0x59, 0xfb, 0xe6, 0xcf, 0x04, 0x0f, 0x2e, - 0x8b, 0x7f, 0x94, 0xfe, 0x97, 0x53, 0x4f, 0x64, 0xf9, 0x05, 0xfe, 0x6a, 0xbc, 0x59, 0xbc, 0xf9, 0xef, 0x2e, 0x72, - 0x9f, 0x2f, 0xd8, 0x51, 0xb0, 0xde, 0x5e, 0x8e, 0x2f, 0x72, 0x7d, 0x39, 0x49, 0x7c, 0x83, 0x30, 0x80, 0xd3, 0x21, - 0x2d, 0xeb, 0x9d, 0xd6, 0x04, 0x9f, 0x81, 0x80, 0x90, 0x6c, 0xe7, 0xec, 0xc4, 0xd6, 0x1f, 0x49, 0xb4, 0x19, 0xcc, - 0xe4, 0x65, 0x90, 0xec, 0x6b, 0x24, 0x00, 0x72, 0x6a, 0x33, 0xd2, 0x71, 0x3e, 0x6d, 0x02, 0x6d, 0x32, 0x49, 0xdd, - 0x6e, 0x01, 0x5c, 0x80, 0x54, 0x94, 0xaf, 0xd6, 0x4d, 0x14, 0x35, 0xf3, 0x2a, 0x14, 0x5f, 0xed, 0xf5, 0x0b, 0xb4, - 0x53, 0x4d, 0x7b, 0x35, 0xd7, 0x81, 0xc0, 0x7a, 0xd5, 0x21, 0x42, 0x4b, 0xb6, 0x82, 0x1e, 0x7f, 0x4f, 0x7c, 0xb7, - 0xf9, 0x80, 0x7a, 0x83, 0xe5, 0x6e, 0xaf, 0x39, 0x9d, 0xda, 0xcd, 0x90, 0x1a, 0xf4, 0x19, 0x54, 0x51, 0xb0, 0x04, - 0xbc, 0xfd, 0xcc, 0xee, 0x66, 0x4b, 0x45, 0x36, 0xb7, 0xf8, 0xe2, 0x60, 0x5b, 0x24, 0x90, 0x8e, 0x23, 0x60, 0x4d, - 0x66, 0xa4, 0x24, 0xb3, 0x53, 0x9a, 0x32, 0x0a, 0x40, 0x06, 0xf0, 0x62, 0x12, 0xf6, 0x98, 0xf4, 0xdf, 0x87, 0x57, - 0x69, 0xfd, 0xd5, 0xfb, 0x62, 0xe4, 0x3d, 0xff, 0x68, 0x7a, 0xe0, 0xf4, 0x7b, 0x6b, 0x97, 0xb3, 0xd7, 0xa9, 0x47, - 0x8d, 0x25, 0xdf, 0x38, 0x80, 0xff, 0x84, 0xa7, 0xce, 0xea, 0x30, 0xdf, 0x8e, 0x9c, 0x52, 0xe5, 0xca, 0xbd, 0x0a, - 0xee, 0xf7, 0x07, 0xe1, 0xfe, 0xff, 0x55, 0xed, 0x66, 0xf8, 0xf9, 0xdf, 0xd6, 0xf0, 0x7f, 0xd9, 0x75, 0x58, 0x6b, - 0xee, 0x7f, 0x6b, 0x70, 0xe9, 0x77, 0x14, 0xd4, 0x75, 0xed, 0xdf, 0x79, 0x10, 0x68, 0x05, 0x5e, 0x17, 0x77, 0x26, - 0x2b, 0x3d, 0x4d, 0xe9, 0xc1, 0x20, 0x3a, 0xf8, 0xff, 0xb3, 0x6c, 0x8e, 0xbd, 0x38, 0x61, 0xb2, 0xb2, 0xfd, 0x7e, - 0x1a, 0x0b, 0xb0, 0x9c, 0x44, 0x69, 0xe3, 0x80, 0xf7, 0x95, 0x1f, 0xd7, 0xe8, 0xe7, 0x80, 0x4e, 0xac, 0x53, 0x40, - 0xdf, 0xd5, 0xf4, 0x09, 0xe2, 0xb1, 0x87, 0xd7, 0x90, 0x7b, 0x47, 0x70, 0x5f, 0x6b, 0x1c, 0x2e, 0x28, 0x3f, 0x14, - 0x77, 0x72, 0x31, 0x95, 0xfc, 0x52, 0xfa, 0xbd, 0x66, 0xf7, 0x45, 0x29, 0x37, 0xc4, 0x50, 0x93, 0x5b, 0x7f, 0x13, - 0x21, 0xdd, 0x2b, 0x92, 0xc8, 0x6a, 0x51, 0x77, 0xae, 0x92, 0x4e, 0x9c, 0xdd, 0xb3, 0xad, 0xca, 0x0c, 0xc0, 0x8b, - 0x2a, 0x97, 0x92, 0xb7, 0xeb, 0x40, 0x19, 0xd3, 0x7b, 0xec, 0x7c, 0x1d, 0x0d, 0xa8, 0xa5, 0xf4, 0x55, 0xde, 0xf0, - 0xef, 0xfc, 0x02, 0xf3, 0xae, 0xc6, 0xba, 0xf1, 0xc4, 0x3e, 0x1a, 0xda, 0x4d, 0xe3, 0x3e, 0x42, 0x40, 0x1d, 0x6e, - 0xc8, 0xa9, 0x46, 0xa2, 0x6b, 0x3e, 0xcb, 0xc3, 0x88, 0xb2, 0x91, 0xb7, 0xe0, 0x4c, 0x9c, 0xb3, 0x4e, 0x41, 0x86, - 0x99, 0xa1, 0x61, 0x05, 0x4d, 0x6f, 0x31, 0xc6, 0xf6, 0xf2, 0x8e, 0xef, 0x8e, 0xb2, 0xb5, 0xfd, 0xfa, 0xcb, 0x02, - 0x81, 0x74, 0x5c, 0x04, 0xef, 0x14, 0x5f, 0xe0, 0x91, 0x34, 0x32, 0xf5, 0x60, 0xdf, 0x5f, 0xd2, 0x8b, 0xb0, 0xff, - 0xd6, 0x9c, 0x26, 0x97, 0x2f, 0xe7, 0x4c, 0x69, 0x51, 0xe7, 0x6c, 0xf1, 0xe2, 0xb6, 0x71, 0xff, 0xd3, 0xe4, 0xda, - 0x98, 0xf5, 0xe7, 0x9c, 0x14, 0x15, 0x4e, 0xb4, 0xce, 0xe6, 0x62, 0xef, 0xbd, 0xe7, 0xbc, 0x1e, 0x2c, 0xbb, 0x07, - 0x67, 0x32, 0x62, 0xeb, 0xad, 0x2e, 0xbc, 0x64, 0xdf, 0x26, 0x6e, 0x9b, 0x0a, 0xae, 0x29, 0x1e, 0xbc, 0x7a, 0x99, - 0xde, 0x5d, 0x9d, 0x2c, 0x59, 0xac, 0x59, 0x7e, 0x34, 0xa0, 0x9b, 0x7d, 0x58, 0xb7, 0x8d, 0xc6, 0xf1, 0x9a, 0x4a, - 0x6c, 0x9b, 0x58, 0xc6, 0xac, 0xa6, 0x13, 0xc1, 0xfd, 0x5f, 0x36, 0xb8, 0x76, 0xa6, 0x0e, 0xc5, 0xb5, 0x19, 0x85, - 0x52, 0xf0, 0xa3, 0x04, 0x24, 0x5c, 0x32, 0x96, 0x0c, 0x9c, 0xe0, 0xdb, 0x39, 0x9d, 0x4c, 0x99, 0xa6, 0xe1, 0x74, - 0xf3, 0xc3, 0x69, 0x07, 0xdf, 0x76, 0x12, 0x23, 0x20, 0xb9, 0x1f, 0x19, 0xb9, 0xc1, 0x64, 0x49, 0xa8, 0x11, 0x77, - 0xeb, 0xe4, 0x17, 0x74, 0xcc, 0x64, 0x89, 0xa7, 0x52, 0x93, 0x87, 0xf3, 0xf1, 0x09, 0xfb, 0xf9, 0x67, 0xeb, 0x6f, - 0xd6, 0x37, 0xed, 0x2a, 0xdc, 0x28, 0xeb, 0xd4, 0x7e, 0x86, 0x3d, 0xab, 0x12, 0x42, 0xde, 0x94, 0xf7, 0xf6, 0x96, - 0xda, 0xa7, 0xdf, 0x37, 0x22, 0xb8, 0xfa, 0xce, 0xd0, 0xea, 0xcf, 0x29, 0x82, 0xe5, 0x6e, 0xd7, 0x4a, 0xa5, 0xc9, - 0xea, 0x89, 0xef, 0xfd, 0x1a, 0x6f, 0x45, 0xce, 0x83, 0x97, 0xec, 0x8d, 0x38, 0x7f, 0x2c, 0x8a, 0xef, 0x9e, 0x3f, - 0x22, 0x16, 0x97, 0x77, 0x73, 0x0c, 0xb1, 0xc9, 0xe5, 0x21, 0x95, 0xc4, 0x34, 0xf8, 0x04, 0x6a, 0x3b, 0xab, 0xa1, - 0x44, 0x57, 0x52, 0xab, 0x2b, 0xbe, 0x94, 0x02, 0x96, 0xca, 0xa8, 0x92, 0xa1, 0xae, 0x0e, 0xa7, 0x8e, 0xd2, 0xf2, - 0xc3, 0xab, 0x0a, 0x2e, 0x95, 0x79, 0x68, 0x69, 0x0c, 0xbf, 0xf4, 0xe9, 0x05, 0x63, 0x18, 0xd9, 0x66, 0x03, 0x97, - 0xa7, 0xe8, 0x44, 0xef, 0xe0, 0x0b, 0xa1, 0xe3, 0x43, 0x33, 0xf9, 0x02, 0x8d, 0xd7, 0x50, 0x92, 0xe1, 0x90, 0x73, - 0x55, 0xdc, 0xa2, 0x96, 0xb8, 0x7f, 0x45, 0xd6, 0x53, 0x4d, 0x69, 0xd1, 0x9e, 0x96, 0xa1, 0xa0, 0xb4, 0x4e, 0x72, - 0x5d, 0x61, 0x7a, 0x99, 0x74, 0x52, 0x4f, 0x6b, 0x70, 0x2b, 0x77, 0x8e, 0x44, 0x66, 0xf7, 0x4d, 0xd3, 0x91, 0x42, - 0x6e, 0x57, 0x40, 0xd2, 0xae, 0x3f, 0x8f, 0x55, 0xc8, 0x3e, 0x24, 0x4d, 0x2d, 0xe9, 0xfe, 0xcc, 0x0e, 0x84, 0x96, - 0xf7, 0xdd, 0xd8, 0xa9, 0x23, 0xdd, 0x83, 0x60, 0xec, 0xfc, 0x56, 0x69, 0x37, 0xcd, 0x49, 0xbc, 0x71, 0xf1, 0x6b, - 0x8f, 0x02, 0xb2, 0xa5, 0x19, 0x7c, 0x6d, 0x4d, 0x40, 0xbb, 0x7c, 0x03, 0x6b, 0x2d, 0x76, 0x30, 0xde, 0xe7, 0x6d, - 0xe8, 0xa1, 0x0f, 0x6c, 0x94, 0x6a, 0x1e, 0x7e, 0xa3, 0x54, 0xfd, 0x8e, 0x9c, 0x42, 0xd5, 0x73, 0xfe, 0xba, 0x24, - 0x8e, 0x8d, 0xac, 0xb6, 0x8b, 0x23, 0x06, 0x1b, 0x18, 0xe3, 0x50, 0x5f, 0x20, 0x62, 0xde, 0x31, 0x32, 0x40, 0x87, - 0xa4, 0x43, 0x59, 0x27, 0xd3, 0x44, 0x42, 0xcc, 0x03, 0x12, 0xec, 0x1d, 0x40, 0x3d, 0x80, 0xff, 0xc9, 0xa4, 0x45, - 0xfe, 0xa9, 0x5d, 0xe5, 0xdc, 0x31, 0xc7, 0x5f, 0x6a, 0x76, 0xcd, 0x46, 0x99, 0xd5, 0xd4, 0xe0, 0x7e, 0xd1, 0x14, - 0x11, 0xd5, 0xf2, 0x5a, 0x36, 0x4a, 0x67, 0x8e, 0xa4, 0xf8, 0x8b, 0xd9, 0xd2, 0x93, 0xfe, 0xf6, 0xbe, 0x94, 0x3e, - 0x7b, 0xf7, 0xfc, 0xce, 0x22, 0x67, 0xaa, 0xdd, 0xfd, 0x14, 0x87, 0x4f, 0x7d, 0xb2, 0xe4, 0x5f, 0x3f, 0xfe, 0x8b, - 0x7f, 0x41, 0x6f, 0xf8, 0x0b, 0xd6, 0xa5, 0x11, 0xac, 0x5d, 0xf6, 0xf5, 0x25, 0xd8, 0xf1, 0xa1, 0xd1, 0xa7, 0x0c, - 0x2c, 0x05, 0x77, 0x41, 0x4b, 0xf0, 0x10, 0x60, 0xb2, 0xd5, 0x6a, 0xfd, 0x40, 0xdc, 0x3f, 0xbb, 0xae, 0x2b, 0x5f, - 0x2c, 0xdc, 0x57, 0xdb, 0xf7, 0x45, 0xaa, 0xd5, 0xe7, 0xdd, 0x4e, 0x26, 0xc7, 0xbb, 0xff, 0x42, 0x0d, 0xfa, 0x46, - 0x84, 0xc2, 0x33, 0x3b, 0x3d, 0x5d, 0x0d, 0x8b, 0xd7, 0x55, 0xbf, 0x98, 0xaa, 0xb6, 0xb8, 0xa9, 0xa6, 0xc5, 0x8b, - 0xea, 0xf4, 0xe0, 0xff, 0xae, 0x78, 0xc9, 0x33, 0x61, 0x98, 0x0f, 0x34, 0xc8, 0xd9, 0xe3, 0x97, 0xa1, 0x96, 0x1b, - 0x1f, 0x28, 0x76, 0xab, 0xa2, 0x70, 0xda, 0xa5, 0xef, 0x7f, 0xdc, 0x67, 0xf0, 0x46, 0x0a, 0x7a, 0x19, 0xd3, 0x1e, - 0x2d, 0x69, 0xd0, 0x4c, 0x22, 0x48, 0xec, 0xeb, 0x4e, 0x7b, 0x27, 0x1d, 0x48, 0x18, 0xf4, 0xeb, 0x6d, 0xcb, 0x35, - 0x1e, 0x45, 0x98, 0x30, 0x79, 0xcb, 0x40, 0x1c, 0x0a, 0xbe, 0x42, 0x35, 0x22, 0xda, 0x3b, 0x61, 0x9b, 0x24, 0x82, - 0x20, 0x86, 0x2e, 0x75, 0x52, 0x07, 0xbb, 0x4c, 0x73, 0xeb, 0x3c, 0x96, 0x00, 0x69, 0xc5, 0x9a, 0xf2, 0x53, 0x5f, - 0xb4, 0x62, 0x92, 0x8a, 0xda, 0x5c, 0x98, 0x2a, 0xa1, 0x9b, 0x11, 0x62, 0x7d, 0xcb, 0x05, 0xdf, 0xe6, 0x75, 0x2d, - 0x02, 0x2d, 0x37, 0x6b, 0x06, 0xe4, 0x8c, 0x2e, 0xe4, 0x8d, 0xb9, 0x90, 0x2d, 0x96, 0x69, 0xbd, 0x30, 0x4e, 0x35, - 0x6d, 0xda, 0x88, 0xc8, 0x5e, 0xfd, 0xfa, 0x16, 0x88, 0xec, 0x43, 0x5f, 0xd4, 0x99, 0xde, 0xd4, 0xad, 0xb0, 0x89, - 0x41, 0xa6, 0xa1, 0x2a, 0x51, 0x1a, 0xa2, 0x11, 0x17, 0xf1, 0x68, 0x57, 0x89, 0xb0, 0xf1, 0x93, 0xfc, 0x9a, 0x49, - 0x0a, 0x3a, 0x80, 0x58, 0xa0, 0xe2, 0xba, 0xf6, 0x02, 0xe2, 0x90, 0x95, 0xde, 0x37, 0xfd, 0x53, 0x6e, 0xee, 0x8c, - 0x72, 0x33, 0xf4, 0x93, 0x26, 0x57, 0x70, 0x09, 0x31, 0xea, 0x21, 0x8a, 0xc8, 0x47, 0xb1, 0xef, 0x50, 0x27, 0x90, - 0x02, 0x4e, 0x14, 0x67, 0xd0, 0x38, 0x53, 0x81, 0x03, 0xf6, 0x81, 0x16, 0x71, 0x0c, 0x45, 0xf6, 0xa3, 0xae, 0x3a, - 0xd7, 0x23, 0x93, 0x54, 0x77, 0xd2, 0x6f, 0xfe, 0x73, 0x55, 0x65, 0xb0, 0x97, 0x57, 0xab, 0x6c, 0x3e, 0x28, 0xf9, - 0x07, 0xf6, 0x37, 0x73, 0x85, 0x8a, 0xb5, 0xf3, 0x36, 0x9c, 0xd1, 0xe6, 0x88, 0xb1, 0x85, 0xe5, 0x71, 0x6e, 0xa9, - 0x27, 0x70, 0xad, 0xbf, 0x03, 0xcf, 0x70, 0xf6, 0x2d, 0x61, 0x64, 0x5e, 0x4e, 0x26, 0x0b, 0xdd, 0xcc, 0x6e, 0x77, - 0x79, 0xe4, 0x6c, 0x98, 0xd4, 0x9e, 0x2c, 0xea, 0xd7, 0x00, 0xe3, 0x47, 0xbc, 0xe6, 0xc1, 0x9e, 0x81, 0xeb, 0x91, - 0x48, 0xc1, 0x66, 0x80, 0x77, 0x32, 0x76, 0xc4, 0xca, 0x89, 0xb3, 0x34, 0x06, 0x9d, 0x0b, 0x57, 0xa5, 0xe9, 0xef, - 0x2d, 0x51, 0x4a, 0x00, 0x98, 0x41, 0xe8, 0xfd, 0xdc, 0x36, 0xf7, 0xad, 0xa8, 0x4d, 0x49, 0x1a, 0xe2, 0x0c, 0xa2, - 0x72, 0xa0, 0x62, 0x17, 0x34, 0x1d, 0xed, 0x5b, 0x2a, 0xc7, 0xc9, 0x0c, 0x12, 0xa6, 0x5e, 0x19, 0x77, 0xc5, 0x5f, - 0xfa, 0xac, 0x56, 0xff, 0xfc, 0xc8, 0xf4, 0x54, 0xff, 0x88, 0xec, 0xf6, 0x55, 0xf1, 0x3c, 0x57, 0x7e, 0x62, 0x4a, - 0xcd, 0xd5, 0x76, 0x27, 0x43, 0xbc, 0xb1, 0xf4, 0x56, 0xcc, 0x1c, 0xb0, 0xde, 0x1a, 0x9e, 0xd7, 0xbb, 0x5e, 0xcc, - 0x1d, 0xf5, 0x4b, 0xba, 0xaf, 0xcf, 0xff, 0x26, 0x03, 0xfb, 0x0d, 0x93, 0x9c, 0xfb, 0x9a, 0xf6, 0x3c, 0xa1, 0xaf, - 0x16, 0xf3, 0x9a, 0x26, 0x36, 0xf6, 0x99, 0x67, 0xde, 0xd2, 0x34, 0xc8, 0x9e, 0xee, 0xeb, 0x95, 0xd6, 0x99, 0x39, - 0xc7, 0x87, 0x83, 0xe6, 0xf3, 0x27, 0xdd, 0x1b, 0x07, 0xdd, 0x15, 0x28, 0x2e, 0xdd, 0x27, 0xdf, 0x51, 0xf8, 0xc2, - 0x5c, 0x30, 0xed, 0x5c, 0x22, 0xe4, 0xc1, 0xcd, 0xf2, 0x04, 0xa4, 0xbc, 0xcc, 0x27, 0x90, 0x34, 0xcf, 0xcf, 0x97, - 0x98, 0x95, 0x22, 0xc1, 0xcd, 0x84, 0x59, 0x4f, 0x9b, 0x81, 0xe9, 0x66, 0x37, 0x9b, 0x77, 0xf5, 0xe4, 0x49, 0xe7, - 0xb5, 0x83, 0xa6, 0xce, 0xbf, 0xb2, 0xd9, 0xa7, 0x2f, 0x2a, 0xf5, 0x34, 0xad, 0xdc, 0xf5, 0x24, 0x7f, 0xaf, 0x44, - 0x99, 0x05, 0xf0, 0x5e, 0x4a, 0xe4, 0x29, 0x9e, 0xee, 0xe4, 0xa4, 0x89, 0x6a, 0x80, 0x14, 0xab, 0xbb, 0x13, 0xc3, - 0x46, 0xd5, 0x42, 0xa7, 0x1c, 0x3d, 0xe3, 0xd5, 0xcf, 0xd8, 0x23, 0xbe, 0x88, 0xd8, 0x89, 0x8d, 0xc2, 0x8f, 0x8a, - 0xa1, 0xc2, 0xfa, 0xb4, 0x2e, 0x83, 0x57, 0x86, 0xd5, 0x65, 0x2c, 0x06, 0xe4, 0xe7, 0xa6, 0x5c, 0x48, 0xa1, 0xe5, - 0xe7, 0xe8, 0x3e, 0x34, 0x35, 0x50, 0x6f, 0x7c, 0x1e, 0xef, 0xf2, 0xd9, 0xa4, 0xfe, 0x0d, 0x04, 0x7c, 0x90, 0x01, - 0xb5, 0x2c, 0x74, 0x5a, 0xcf, 0x9e, 0xe2, 0xc7, 0x4f, 0xfb, 0xdf, 0x58, 0xf2, 0xf1, 0xc7, 0xff, 0x14, 0xcf, 0x1e, - 0xfb, 0xa8, 0x52, 0x68, 0x12, 0xbc, 0xa7, 0xb0, 0x6f, 0xb3, 0xff, 0xef, 0x91, 0x67, 0xd1, 0xc4, 0x8b, 0xe1, 0x51, - 0x9d, 0x5d, 0x20, 0x4a, 0x6a, 0x7d, 0xe8, 0x8b, 0xd8, 0x71, 0xdf, 0xf7, 0x60, 0x59, 0xba, 0x47, 0xdc, 0xe4, 0xc1, - 0xf5, 0x49, 0xec, 0xf6, 0x95, 0x89, 0x54, 0x6a, 0xfc, 0x8c, 0x5c, 0xc0, 0x58, 0x27, 0xed, 0x31, 0x5c, 0xda, 0xd2, - 0x48, 0xc1, 0xa6, 0x14, 0x67, 0x52, 0x80, 0xfb, 0x4c, 0x94, 0xbe, 0xb3, 0x8f, 0x20, 0xa9, 0xf7, 0x2f, 0x4d, 0x60, - 0x49, 0x5e, 0x97, 0x05, 0x12, 0x9f, 0x8f, 0x2b, 0xf7, 0xf9, 0x24, 0x20, 0x2e, 0x8a, 0x0b, 0x68, 0x0b, 0xc4, 0x18, - 0x15, 0xb9, 0x14, 0x3d, 0x64, 0x69, 0x2a, 0x26, 0xaa, 0x43, 0x7a, 0xc1, 0x6e, 0xdf, 0x0d, 0x94, 0x32, 0x2d, 0x74, - 0xfc, 0xd5, 0x89, 0x18, 0x28, 0x5d, 0x9e, 0xed, 0x6d, 0x61, 0x40, 0x2e, 0x17, 0x13, 0x82, 0x34, 0xbe, 0x9e, 0xc2, - 0xb2, 0xf3, 0x11, 0x5d, 0x25, 0x00, 0x29, 0xbc, 0x5b, 0xc4, 0xcd, 0xa0, 0xa0, 0xa4, 0x81, 0xaa, 0xa9, 0x8d, 0x1e, - 0x08, 0xb1, 0xec, 0x4c, 0xa9, 0xdc, 0x8a, 0x0a, 0x04, 0x01, 0x22, 0x1b, 0x7b, 0x90, 0xc8, 0xe9, 0x61, 0x7a, 0xb8, - 0xa3, 0x2d, 0x4a, 0xa6, 0x68, 0x04, 0x35, 0xda, 0xf4, 0x90, 0xa4, 0x07, 0xaf, 0x9b, 0x89, 0xc1, 0x89, 0xb3, 0xe1, - 0x17, 0xbc, 0xd7, 0x93, 0x7b, 0xbb, 0x36, 0xb2, 0xcf, 0x25, 0xe9, 0x10, 0x73, 0x78, 0xd8, 0xd5, 0xd3, 0xcd, 0x71, - 0xbb, 0xa7, 0x9c, 0x7b, 0x89, 0x9d, 0x80, 0xf6, 0xf6, 0xd4, 0x7d, 0x67, 0x25, 0x6a, 0x5d, 0xf0, 0x08, 0x29, 0xd7, - 0x49, 0xd7, 0x93, 0xef, 0x2f, 0xef, 0x6a, 0x53, 0x2a, 0x9b, 0x88, 0x54, 0x34, 0x99, 0x2a, 0x10, 0x53, 0x82, 0x34, - 0x96, 0x51, 0x2f, 0xb7, 0x73, 0xc4, 0x9e, 0x0e, 0xa3, 0xb8, 0x85, 0x37, 0xb3, 0x55, 0xf6, 0x26, 0xad, 0xaf, 0xb0, - 0x82, 0x69, 0x8a, 0x6a, 0xfe, 0xfb, 0x59, 0x56, 0xb4, 0x33, 0x10, 0x41, 0xa8, 0xe7, 0xb6, 0x25, 0xbb, 0x80, 0x46, - 0x39, 0x7f, 0xdb, 0x40, 0x9b, 0x0e, 0xfb, 0x20, 0xe4, 0x39, 0x32, 0xef, 0xe5, 0x5b, 0x22, 0xc4, 0x40, 0x4a, 0x90, - 0x81, 0xaf, 0x5d, 0x44, 0xd4, 0x1c, 0x16, 0xcd, 0x6d, 0xe0, 0xf0, 0x21, 0x5c, 0x91, 0x99, 0x60, 0x32, 0xc5, 0xdd, - 0x85, 0x38, 0xed, 0xb8, 0xc3, 0x3d, 0x3b, 0xea, 0x59, 0x93, 0x3b, 0x65, 0x1a, 0x69, 0x26, 0x79, 0x7a, 0xb7, 0x4a, - 0xa3, 0x6c, 0xe9, 0xc8, 0xc5, 0x26, 0x92, 0x4b, 0xb9, 0x85, 0x88, 0xdb, 0xb2, 0x76, 0xfa, 0xf6, 0xfb, 0xb2, 0x79, - 0x74, 0x2f, 0xbe, 0xf5, 0x3e, 0xec, 0xdc, 0xaa, 0xb7, 0x35, 0xdb, 0xd6, 0x4f, 0x4b, 0x81, 0x52, 0x06, 0xdc, 0x99, - 0xae, 0x64, 0x26, 0x55, 0x57, 0xbe, 0x68, 0xdb, 0x21, 0x5f, 0x98, 0xc0, 0xf4, 0x34, 0xbc, 0xcd, 0x6a, 0x9d, 0x50, - 0x94, 0xd2, 0x07, 0x62, 0x91, 0xf8, 0x30, 0x00, 0xc6, 0x07, 0xaf, 0x89, 0x5c, 0xf0, 0x33, 0xfc, 0x5c, 0x2a, 0xfd, - 0xae, 0xc9, 0x52, 0x14, 0x80, 0x3e, 0x88, 0xf6, 0xec, 0x34, 0xaa, 0xf9, 0x2a, 0x9b, 0xe9, 0xe4, 0x20, 0xa6, 0x7f, - 0xfc, 0xff, 0x5c, 0x05, 0xea, 0xb7, 0x23, 0x3d, 0x84, 0xf7, 0x9b, 0x04, 0xae, 0xd5, 0xc2, 0x98, 0x9e, 0xc4, 0xa8, - 0x7b, 0x58, 0x12, 0xe1, 0x40, 0x00, 0x5f, 0x45, 0x4d, 0xdc, 0x48, 0xe3, 0xad, 0xa2, 0xa7, 0xa8, 0x6f, 0xc3, 0x5b, - 0x77, 0xb2, 0x4f, 0xc6, 0xe1, 0x7e, 0x8e, 0xb9, 0x17, 0x25, 0xcb, 0x32, 0x88, 0x86, 0x41, 0xd1, 0x2d, 0x0c, 0xac, - 0x90, 0x9f, 0x2f, 0xe0, 0xcb, 0x30, 0xe7, 0x33, 0xa3, 0x64, 0xb4, 0x42, 0xf4, 0x2a, 0xa4, 0x0e, 0x12, 0xef, 0x66, - 0x98, 0x0d, 0x7a, 0x06, 0xc5, 0xfe, 0x60, 0xea, 0x54, 0x2a, 0x68, 0xaf, 0xaa, 0xd2, 0x64, 0x57, 0x92, 0x5b, 0x7b, - 0x15, 0x1d, 0xfd, 0x14, 0x44, 0x8e, 0x97, 0xa2, 0xc5, 0x17, 0x1e, 0x0b, 0xfb, 0x5d, 0xdc, 0x1c, 0xc7, 0x00, 0x3c, - 0x7f, 0xfa, 0xa9, 0x17, 0xb7, 0x22, 0x3b, 0xfd, 0x5b, 0xe2, 0xd2, 0x77, 0x8f, 0xa6, 0xf1, 0xff, 0x29, 0x64, 0x7f, - 0xe0, 0xb7, 0x08, 0xac, 0x3f, 0xed, 0x31, 0x38, 0xb8, 0x84, 0xeb, 0x2d, 0x62, 0xf3, 0x05, 0x2c, 0xcb, 0xdb, 0xad, - 0x79, 0x20, 0x24, 0xee, 0x0b, 0x63, 0x66, 0x4f, 0xca, 0x6a, 0x94, 0x08, 0xff, 0xba, 0x8f, 0x61, 0xff, 0x35, 0x71, - 0x09, 0xc2, 0x70, 0x6e, 0x5c, 0xe8, 0xef, 0xb4, 0xce, 0x9e, 0xe2, 0xfb, 0xa7, 0xfe, 0x66, 0xc9, 0xfb, 0x1f, 0xff, - 0x53, 0x9c, 0x79, 0x67, 0x5c, 0xa3, 0xb7, 0x4f, 0x6f, 0x6e, 0x22, 0x46, 0x4d, 0x5e, 0xcb, 0x0a, 0x67, 0x3f, 0x8b, - 0x9c, 0xcd, 0x84, 0x57, 0x27, 0x72, 0x81, 0x86, 0x91, 0x8f, 0x7b, 0x5e, 0xa2, 0x17, 0xec, 0xba, 0xa3, 0x58, 0x9a, - 0x68, 0xcb, 0x22, 0x54, 0xe8, 0xa7, 0x06, 0x49, 0x82, 0xf9, 0xfe, 0x27, 0x31, 0x3b, 0x6a, 0xab, 0x61, 0x66, 0x15, - 0x0f, 0xf1, 0x5d, 0x5a, 0x99, 0xa4, 0x9c, 0x57, 0xf5, 0x4e, 0x25, 0xca, 0xe6, 0x47, 0x64, 0x87, 0xc5, 0x60, 0xcc, - 0x4a, 0x08, 0xfb, 0x9d, 0x21, 0x32, 0x72, 0xd4, 0x97, 0x38, 0x49, 0xfc, 0x56, 0x47, 0x48, 0xbc, 0xb3, 0x34, 0x48, - 0x5f, 0x4b, 0x80, 0x7c, 0x2d, 0xbb, 0x3e, 0xf6, 0x62, 0x4a, 0x27, 0x1c, 0xee, 0x24, 0x7d, 0xeb, 0x3d, 0xf2, 0x8d, - 0x30, 0x6f, 0x95, 0xc6, 0x31, 0x20, 0xec, 0x5c, 0x03, 0x46, 0x46, 0xec, 0x40, 0x0e, 0x31, 0x17, 0x3b, 0x40, 0x30, - 0xeb, 0x6a, 0x9c, 0x03, 0xbf, 0xf7, 0x0e, 0xa5, 0xf4, 0x62, 0x2d, 0xb5, 0x4f, 0x72, 0x7e, 0x90, 0xc3, 0x31, 0x27, - 0x30, 0xde, 0x93, 0x39, 0x1d, 0x68, 0x1e, 0x93, 0x52, 0x2b, 0x91, 0x8a, 0x06, 0xe4, 0x57, 0xc9, 0xe0, 0x9e, 0xed, - 0xc9, 0x88, 0x93, 0x7f, 0xa1, 0x94, 0xfc, 0xe1, 0xc6, 0x3d, 0x9a, 0x09, 0xce, 0xcb, 0x03, 0x76, 0xb1, 0x59, 0x94, - 0x40, 0x3b, 0x53, 0xcd, 0x93, 0xb3, 0x05, 0x73, 0x69, 0x49, 0x49, 0xcb, 0xc2, 0x27, 0x64, 0x46, 0x6e, 0xfe, 0xc5, - 0xeb, 0x9b, 0xfe, 0x91, 0x61, 0x05, 0xc1, 0x5e, 0xeb, 0xaf, 0xf5, 0x7e, 0x4f, 0xa7, 0x83, 0x43, 0xd0, 0x9d, 0x03, - 0xb4, 0x4a, 0xe3, 0x61, 0x7f, 0xc6, 0x26, 0x80, 0x4c, 0x10, 0x3f, 0xdc, 0xb0, 0xee, 0xee, 0x07, 0x04, 0x66, 0x3f, - 0xf1, 0x8b, 0xe3, 0x94, 0x11, 0xf0, 0xad, 0xdd, 0xa2, 0x12, 0x22, 0x87, 0xff, 0xe7, 0xbe, 0x92, 0xc5, 0xea, 0x36, - 0x39, 0xd2, 0xec, 0xd7, 0xad, 0x33, 0xc0, 0x38, 0xfa, 0xe5, 0x3a, 0xa1, 0x12, 0x46, 0x66, 0x87, 0xa6, 0xd8, 0x15, - 0xce, 0x1e, 0xe1, 0x64, 0xc6, 0x7e, 0x0a, 0x8d, 0x48, 0xe3, 0x60, 0x25, 0x47, 0x5a, 0x80, 0x8b, 0xe5, 0x70, 0x68, - 0x98, 0x84, 0x0e, 0xb0, 0xc5, 0x45, 0x8e, 0xfb, 0xe1, 0xf9, 0x4c, 0xb2, 0xc3, 0x4b, 0x02, 0xe8, 0xc0, 0xb9, 0x88, - 0x89, 0x20, 0x07, 0x2d, 0x06, 0xa1, 0x1b, 0x72, 0xb0, 0x16, 0xaa, 0x61, 0x72, 0x04, 0xcf, 0xbe, 0xfe, 0x31, 0xfa, - 0x49, 0xc3, 0xe0, 0x25, 0x24, 0xc3, 0x28, 0x01, 0xe4, 0x98, 0xac, 0xf4, 0x1b, 0xf7, 0x76, 0x7b, 0xeb, 0xae, 0x0b, - 0x89, 0x3b, 0xfb, 0x69, 0xd7, 0x72, 0x31, 0x91, 0x7a, 0xf5, 0xd1, 0xc0, 0xb0, 0x73, 0xc0, 0x16, 0x78, 0x15, 0x44, - 0x67, 0xa2, 0xc7, 0x3d, 0xdc, 0x9f, 0x42, 0xaf, 0x30, 0x47, 0x60, 0x02, 0x7c, 0x63, 0xb2, 0x3b, 0x79, 0x84, 0xab, - 0xdb, 0x7d, 0xb4, 0xe7, 0xb1, 0x35, 0x8e, 0x0a, 0xa1, 0x34, 0xe2, 0x2d, 0xd3, 0x9d, 0x64, 0xc2, 0x3a, 0xac, 0xfe, - 0xa1, 0x29, 0xae, 0xd2, 0x77, 0xd2, 0x34, 0x82, 0x13, 0xb1, 0xfb, 0x36, 0xdc, 0x6a, 0xe0, 0x04, 0x47, 0x2e, 0x7a, - 0xf8, 0x8e, 0x08, 0x43, 0x0b, 0x7c, 0xc0, 0x49, 0xc5, 0x6c, 0x3c, 0x26, 0x06, 0x4e, 0xe3, 0x24, 0x57, 0x66, 0x39, - 0x37, 0x39, 0x24, 0xae, 0x58, 0xae, 0xb0, 0x9e, 0x5e, 0xb3, 0x6c, 0x93, 0x09, 0xf0, 0xde, 0x4f, 0xdd, 0x7b, 0x26, - 0xa4, 0x5c, 0x35, 0x6a, 0xcb, 0xdd, 0x59, 0xf1, 0x69, 0xb4, 0x92, 0xc9, 0xc9, 0x26, 0xfe, 0x30, 0xc0, 0x9d, 0xdd, - 0x12, 0xdd, 0xa9, 0xee, 0x2e, 0xb9, 0x0b, 0x8d, 0x27, 0xcc, 0x9c, 0xc2, 0x3e, 0x58, 0x4b, 0x75, 0x1e, 0x86, 0xd6, - 0xe3, 0xef, 0x9a, 0x99, 0x80, 0xc8, 0x4e, 0xa6, 0xb3, 0xf8, 0xa1, 0x1b, 0x90, 0xd2, 0x12, 0x47, 0x17, 0x25, 0xab, - 0x3f, 0xad, 0x7b, 0x93, 0x2a, 0xee, 0xd0, 0xf6, 0xf5, 0x8d, 0x1c, 0xef, 0x24, 0x2b, 0xb1, 0x04, 0xd5, 0x48, 0x7e, - 0x91, 0xa4, 0x81, 0x1d, 0x90, 0x0e, 0xb9, 0x46, 0xc3, 0x4c, 0x3d, 0x9b, 0x07, 0xaf, 0x23, 0xdd, 0x06, 0xab, 0x74, - 0x66, 0xf7, 0xf2, 0x03, 0x69, 0x85, 0xa6, 0x8c, 0x49, 0x31, 0xc9, 0xc7, 0x8b, 0x2e, 0x4e, 0x9c, 0xa2, 0x96, 0x7c, - 0x72, 0xe5, 0xa4, 0xe7, 0xb5, 0x3a, 0xe4, 0xea, 0x65, 0x0f, 0x31, 0x90, 0x24, 0xb3, 0x78, 0xa1, 0x7a, 0x72, 0x43, - 0xbc, 0x46, 0x03, 0x9c, 0xb6, 0xc7, 0xee, 0x2e, 0x1e, 0xe5, 0xad, 0x7f, 0xaa, 0xb7, 0x15, 0x5a, 0xfe, 0x94, 0x97, - 0xf7, 0x6a, 0xfd, 0x6d, 0x14, 0x21, 0xbf, 0x8b, 0x1f, 0x76, 0xeb, 0x9f, 0xb6, 0xab, 0x52, 0xa1, 0x53, 0xf9, 0x35, - 0x69, 0x8b, 0x05, 0xc0, 0x9f, 0xd7, 0xa6, 0x29, 0x24, 0x23, 0x8c, 0xa8, 0x4f, 0x10, 0x61, 0xaa, 0x13, 0xc6, 0xb7, - 0xa2, 0x86, 0xbc, 0x15, 0xad, 0x48, 0xaf, 0x19, 0x4d, 0xe3, 0xec, 0xe7, 0x8e, 0x11, 0x76, 0x32, 0x1c, 0xb1, 0x5b, - 0x92, 0x72, 0xfd, 0x14, 0xe9, 0x29, 0x24, 0x8e, 0x45, 0x70, 0x99, 0x50, 0x69, 0x29, 0xc7, 0x04, 0xd0, 0xad, 0xb6, - 0x86, 0x2c, 0x86, 0xd4, 0xa0, 0x8c, 0x59, 0xdd, 0x3c, 0x22, 0x70, 0xd4, 0xa0, 0x87, 0x8e, 0xa4, 0x70, 0x42, 0xb3, - 0x9d, 0x3e, 0x3e, 0x7f, 0xc6, 0xb4, 0x55, 0xdb, 0x20, 0x12, 0x03, 0xd0, 0xed, 0xee, 0x48, 0x0c, 0x41, 0xda, 0xdf, - 0x11, 0xd9, 0x5a, 0x2d, 0xca, 0xe8, 0xc8, 0x86, 0x6e, 0xa7, 0xc8, 0xfc, 0x5a, 0xcd, 0x15, 0xd9, 0xd4, 0xed, 0x06, - 0x65, 0x14, 0x19, 0xa4, 0xbc, 0xa3, 0xb4, 0xe5, 0x62, 0x19, 0x1d, 0xdd, 0xa2, 0x88, 0x50, 0x71, 0x1b, 0x14, 0x69, - 0xfa, 0x83, 0x14, 0x39, 0x0d, 0x11, 0xa7, 0x00, 0xef, 0x4e, 0x2d, 0x89, 0xda, 0x2d, 0x15, 0x0d, 0x9e, 0x42, 0x2f, - 0x83, 0x79, 0x77, 0xe1, 0x40, 0x42, 0x68, 0x73, 0x83, 0x53, 0x10, 0x6d, 0x41, 0xa7, 0x22, 0xbc, 0x15, 0xe9, 0x41, - 0x6a, 0x20, 0x00, 0xaf, 0xce, 0x7d, 0x1c, 0x1c, 0x00, 0xf4, 0xc9, 0x2a, 0xe8, 0x7c, 0x7f, 0xb4, 0xc8, 0x21, 0x96, - 0x66, 0x47, 0xea, 0x29, 0xe2, 0x52, 0x9a, 0x4f, 0xc4, 0xed, 0x82, 0x1c, 0x44, 0x9a, 0x56, 0xfc, 0x87, 0x5c, 0xd8, - 0xa4, 0x9d, 0x0f, 0x33, 0x04, 0x5f, 0x6a, 0xe2, 0x89, 0xd4, 0x7d, 0x8e, 0xc5, 0x94, 0x91, 0xc9, 0xd7, 0xba, 0x0b, - 0xab, 0x1d, 0xcc, 0x01, 0x31, 0x9e, 0x54, 0xf2, 0xd3, 0x29, 0xb2, 0xb3, 0xc9, 0x62, 0xa5, 0xa1, 0x02, 0x5a, 0x3a, - 0xa4, 0xcb, 0x65, 0xab, 0xc7, 0x01, 0x77, 0xfc, 0xa8, 0x09, 0x1f, 0x0d, 0x71, 0x5d, 0xfa, 0xf4, 0x2a, 0x48, 0x43, - 0xf8, 0x60, 0x29, 0xa4, 0x65, 0xd5, 0xb8, 0xf7, 0x66, 0x92, 0xda, 0xbf, 0xdb, 0x2c, 0xad, 0x69, 0xb0, 0xc3, 0xb6, - 0xe8, 0x19, 0x44, 0xe1, 0xeb, 0xaf, 0xa7, 0xd5, 0x47, 0xe7, 0x36, 0x2d, 0x88, 0xd0, 0x57, 0x05, 0x4e, 0x2c, 0xa7, - 0xbf, 0x2c, 0xe9, 0xe6, 0x96, 0xd0, 0x47, 0x2c, 0x7f, 0x94, 0x29, 0xc7, 0x67, 0x86, 0x17, 0x6b, 0xe8, 0x7e, 0x07, - 0x5a, 0x44, 0x8d, 0xb3, 0x59, 0x16, 0x8d, 0x6c, 0x09, 0xaf, 0xa9, 0x98, 0x98, 0xab, 0x1f, 0x0d, 0x19, 0x6b, 0x64, - 0x82, 0xc8, 0xa2, 0x1f, 0x3f, 0xea, 0xd2, 0x11, 0xe7, 0x61, 0x10, 0x27, 0x20, 0xcd, 0xbc, 0x64, 0xe4, 0x4d, 0x14, - 0xfc, 0xd6, 0x73, 0x60, 0x9b, 0xf7, 0x5b, 0xfb, 0xcc, 0x6e, 0xc4, 0x47, 0xfa, 0xda, 0xeb, 0x1d, 0x08, 0x25, 0x21, - 0x46, 0xe5, 0x9e, 0x8f, 0x8b, 0x25, 0x7b, 0x1a, 0x78, 0x53, 0x96, 0x2b, 0x06, 0xb7, 0xf8, 0x0d, 0xe8, 0x41, 0x0d, - 0xef, 0x20, 0xb4, 0x8f, 0x9c, 0x76, 0x84, 0x07, 0x68, 0x54, 0x0f, 0xd7, 0x72, 0x44, 0x17, 0x10, 0x64, 0x4e, 0xd0, - 0xa3, 0x81, 0x32, 0x50, 0xf0, 0x95, 0x64, 0xd0, 0x55, 0x66, 0xbb, 0xcc, 0xd6, 0xd0, 0x8c, 0x09, 0x10, 0xa9, 0xce, - 0x9f, 0x46, 0x70, 0x09, 0x5d, 0xc2, 0xa5, 0xa2, 0x0e, 0x64, 0xd4, 0xca, 0x70, 0x30, 0x0a, 0x68, 0xfa, 0x54, 0x1a, - 0xbf, 0x1a, 0x5d, 0x0a, 0xc0, 0xb1, 0xc6, 0xe7, 0x49, 0x06, 0x9f, 0x6d, 0x5c, 0xb1, 0xb8, 0x0c, 0x9a, 0x03, 0x83, - 0x6b, 0x5f, 0xdb, 0xab, 0xae, 0xc1, 0xce, 0xeb, 0xef, 0xa2, 0x33, 0x86, 0x3d, 0xa3, 0x10, 0x2b, 0x80, 0x0e, 0x90, - 0x28, 0xd7, 0xc0, 0xd9, 0x7b, 0xee, 0x7c, 0x6c, 0x23, 0x57, 0xd0, 0x85, 0x0e, 0x08, 0xae, 0x35, 0xb8, 0xdc, 0x3f, - 0x02, 0x5c, 0x68, 0x68, 0xe7, 0x59, 0x60, 0x45, 0x2e, 0x33, 0x50, 0x23, 0xfe, 0x4d, 0xee, 0x60, 0x59, 0x8d, 0x74, - 0x11, 0x8f, 0x15, 0xb5, 0xed, 0x64, 0x81, 0x4e, 0xb0, 0x4d, 0x0d, 0xb1, 0x03, 0x14, 0xbc, 0x50, 0x7e, 0xc2, 0xa9, - 0x47, 0x4b, 0x37, 0xa8, 0x2c, 0xf8, 0x1c, 0x98, 0xc5, 0xef, 0xa4, 0xde, 0xc2, 0xfd, 0x27, 0x47, 0x76, 0x10, 0x40, - 0xbc, 0x35, 0x2b, 0x84, 0x30, 0xf1, 0x72, 0x6c, 0x13, 0x76, 0x94, 0x81, 0x60, 0x37, 0x9a, 0x08, 0xd9, 0x08, 0x73, - 0x1b, 0xef, 0xa2, 0xf5, 0x7a, 0x1f, 0xbb, 0xbf, 0x18, 0x85, 0xc1, 0x76, 0x81, 0x61, 0xfc, 0x58, 0x8c, 0x22, 0xd4, - 0xf6, 0xf2, 0x5b, 0x91, 0x8c, 0xe4, 0x67, 0x15, 0xcc, 0x45, 0xdc, 0x0e, 0x6c, 0x5d, 0x99, 0x5a, 0x3f, 0x20, 0x2a, - 0xef, 0xf7, 0x92, 0x41, 0xbb, 0x85, 0x8f, 0x6f, 0x46, 0x76, 0xe2, 0x2b, 0xc7, 0xf5, 0xac, 0xfa, 0xfc, 0xfe, 0x39, - 0x22, 0x6b, 0x1e, 0x6f, 0xdd, 0x19, 0x8d, 0xce, 0x6a, 0x2d, 0x87, 0x8d, 0xda, 0xc0, 0x30, 0x21, 0xac, 0xf0, 0xfd, - 0xbc, 0x5c, 0xfb, 0x80, 0x30, 0xfc, 0xba, 0xe1, 0x37, 0x9e, 0x2d, 0xb0, 0x97, 0x1e, 0x1c, 0x2d, 0x6f, 0x5d, 0x57, - 0xd7, 0xd3, 0x4a, 0x34, 0x1e, 0x61, 0x12, 0xe2, 0x75, 0xde, 0x30, 0xa5, 0x03, 0x99, 0xe5, 0xf0, 0xa4, 0x7f, 0x70, - 0x4d, 0x67, 0x3e, 0xc4, 0x88, 0xf6, 0x17, 0x80, 0x7c, 0xd1, 0x94, 0x8f, 0x48, 0x9e, 0xd1, 0xe4, 0x06, 0x9b, 0x46, - 0xfa, 0x85, 0x33, 0xa5, 0x3a, 0x10, 0xdc, 0x77, 0x00, 0x00, 0x03, 0x75, 0x58, 0xf0, 0xfb, 0xd8, 0x56, 0xe2, 0xba, - 0x06, 0x63, 0x30, 0x30, 0xfd, 0x47, 0xbf, 0xa8, 0xf3, 0xa3, 0xcf, 0x50, 0x4d, 0xac, 0xf9, 0x52, 0x23, 0x32, 0xbb, - 0x92, 0x15, 0xd9, 0x4d, 0x90, 0x1f, 0xe7, 0xcb, 0x82, 0xdc, 0x84, 0xb8, 0x09, 0x41, 0xc5, 0x3d, 0xf1, 0xb5, 0xa8, - 0x02, 0x7d, 0x03, 0x94, 0x7f, 0x38, 0xeb, 0x40, 0xd0, 0x5f, 0x04, 0xc6, 0x9a, 0x1c, 0x84, 0xf3, 0xcf, 0x2d, 0x33, - 0x91, 0x3f, 0x8f, 0xc2, 0x65, 0xc9, 0x5f, 0xdd, 0xb2, 0x8d, 0xcc, 0xb8, 0xf1, 0x6d, 0x54, 0x99, 0x14, 0xf2, 0xba, - 0xf6, 0xac, 0x33, 0xbe, 0x90, 0x6a, 0x15, 0xc8, 0xd5, 0x45, 0xcc, 0x8c, 0x69, 0x0b, 0x39, 0xfd, 0x59, 0xfb, 0x5a, - 0xe5, 0x7f, 0xc6, 0x1d, 0x1c, 0xb3, 0xf0, 0xcf, 0xc7, 0x6d, 0x63, 0x0a, 0x88, 0xca, 0x1e, 0xf6, 0x45, 0xe5, 0x59, - 0xa7, 0xcb, 0x02, 0xd8, 0x26, 0x88, 0x2b, 0x19, 0xa1, 0xcc, 0x61, 0xa3, 0xf6, 0xfe, 0xbb, 0x7d, 0x1d, 0xcf, 0xac, - 0xb6, 0x1f, 0xd1, 0x4f, 0xfc, 0x11, 0xf3, 0x6f, 0x17, 0xf6, 0x65, 0x62, 0x9c, 0xbe, 0xce, 0x33, 0xd5, 0x9f, 0xba, - 0x49, 0x5d, 0xf0, 0xac, 0x4e, 0x40, 0x05, 0x09, 0xab, 0xe0, 0x67, 0xb0, 0x67, 0xff, 0xa3, 0xc2, 0xd5, 0x90, 0x4f, - 0xcb, 0x67, 0x67, 0xf6, 0x1a, 0xba, 0x56, 0x50, 0x75, 0xb8, 0x01, 0x22, 0x87, 0xc5, 0xad, 0xea, 0x62, 0xc7, 0x99, - 0xf9, 0x2f, 0x0b, 0xd8, 0xf8, 0x5a, 0x08, 0x4e, 0xd7, 0x1f, 0x5d, 0xbe, 0x44, 0xdb, 0x93, 0xb3, 0x89, 0x19, 0xc6, - 0x97, 0x34, 0xc4, 0x83, 0x7b, 0x4a, 0x87, 0x1f, 0x19, 0x93, 0x4f, 0xf1, 0xc7, 0xa7, 0xfd, 0x64, 0x2e, 0xf9, 0xf1, - 0xe3, 0x5f, 0xfa, 0xab, 0x7e, 0xcd, 0x33, 0xbf, 0x50, 0x67, 0xb2, 0xc3, 0x7b, 0x45, 0xd1, 0xf3, 0x8b, 0xb9, 0x18, - 0x11, 0x5b, 0x31, 0xfe, 0x30, 0x0b, 0x7d, 0xfa, 0xed, 0xed, 0xc3, 0x3d, 0x78, 0x33, 0x28, 0x69, 0xc6, 0x41, 0x9d, - 0x9b, 0xeb, 0x1c, 0x5b, 0x69, 0xca, 0x60, 0x12, 0xec, 0x0d, 0x2c, 0x91, 0x41, 0xda, 0x9d, 0x08, 0x11, 0xa8, 0x0c, - 0x4a, 0x05, 0x43, 0x69, 0x8e, 0xa3, 0xae, 0x83, 0x70, 0x20, 0x28, 0x97, 0x11, 0x5d, 0xd5, 0x2a, 0xce, 0x46, 0x07, - 0x0b, 0x01, 0x67, 0x10, 0x61, 0x08, 0xf0, 0xbd, 0x9a, 0x28, 0x81, 0x29, 0x24, 0x0d, 0x21, 0xfe, 0x54, 0x3a, 0x8e, - 0xa2, 0xb8, 0xae, 0xc2, 0xd7, 0xfb, 0x17, 0xd9, 0x38, 0xf9, 0x28, 0x81, 0xa2, 0xbc, 0x03, 0x81, 0x9a, 0x22, 0x05, - 0x9b, 0x8b, 0xcc, 0xe5, 0x88, 0x85, 0xf3, 0xa1, 0xa0, 0x17, 0x12, 0x56, 0x4b, 0x07, 0x3a, 0x1d, 0x7b, 0x38, 0x24, - 0xb4, 0x2a, 0xd8, 0x84, 0xa1, 0xc9, 0x6d, 0x7e, 0xad, 0x02, 0xca, 0xc9, 0x64, 0x17, 0xb7, 0xc0, 0x37, 0xbf, 0x3e, - 0x47, 0x77, 0x2d, 0x74, 0x9e, 0xf9, 0x9e, 0xf1, 0xf7, 0xef, 0x6f, 0x8e, 0xbf, 0xc2, 0xa3, 0x19, 0xab, 0x26, 0xac, - 0xff, 0xf5, 0x4d, 0x48, 0x08, 0xa5, 0x40, 0x15, 0x01, 0x42, 0xa9, 0xac, 0x81, 0x75, 0x1d, 0x32, 0x73, 0x68, 0xe8, - 0xfa, 0x47, 0x06, 0x39, 0x82, 0x1d, 0x36, 0xf6, 0x6d, 0x58, 0xf8, 0x5a, 0xa9, 0x83, 0xbd, 0x81, 0xa9, 0xb5, 0xb0, - 0x4f, 0x5b, 0xa0, 0xce, 0xe6, 0x9c, 0x39, 0x82, 0xa0, 0x5b, 0x66, 0x66, 0xaa, 0xd2, 0x59, 0x9e, 0x69, 0x7e, 0x46, - 0x7b, 0xc5, 0x7e, 0x5d, 0x02, 0x17, 0x64, 0xe9, 0x6c, 0x6e, 0x6d, 0x45, 0x81, 0xbb, 0x92, 0x2a, 0x9f, 0xb1, 0x75, - 0x3c, 0x04, 0xc2, 0xcf, 0x13, 0x63, 0xbb, 0xc6, 0xe7, 0xc1, 0xd6, 0x27, 0xf9, 0x80, 0x96, 0xae, 0x0f, 0x5d, 0x72, - 0xad, 0x8f, 0x11, 0xcc, 0x88, 0xa9, 0xfc, 0xf0, 0x86, 0xfa, 0x6e, 0x38, 0x70, 0x74, 0x9f, 0xe2, 0xa7, 0x4f, 0x3b, - 0xe9, 0x92, 0x4f, 0x3f, 0xfe, 0x4b, 0x5e, 0xd9, 0x75, 0x08, 0x55, 0x2e, 0x53, 0xf0, 0x63, 0x9f, 0xaf, 0xab, 0xc9, - 0xf6, 0xa6, 0xe2, 0x38, 0x10, 0x3f, 0xaa, 0x34, 0x99, 0xe8, 0x17, 0x77, 0x99, 0xc1, 0xf1, 0x3c, 0x44, 0x56, 0x7b, - 0xe2, 0x5c, 0xd7, 0x93, 0x39, 0x97, 0x6d, 0x70, 0xa1, 0x11, 0x32, 0x75, 0x79, 0xe6, 0x65, 0x9f, 0x13, 0x58, 0xd4, - 0x4c, 0xca, 0xee, 0x6b, 0x8c, 0xc7, 0xd7, 0x96, 0xd3, 0xf4, 0x2c, 0xa4, 0x50, 0x37, 0x1d, 0x2e, 0x68, 0x08, 0x15, - 0xd0, 0xe0, 0xe7, 0x6f, 0x4a, 0xd9, 0x98, 0xb8, 0x19, 0xc9, 0x7f, 0xf0, 0xc8, 0xa4, 0x99, 0x32, 0x1e, 0xe9, 0xb3, - 0x5a, 0xb3, 0x3c, 0xe8, 0x88, 0x2d, 0x65, 0xd9, 0xc7, 0xf8, 0x3a, 0xb5, 0x90, 0xfd, 0x35, 0xfd, 0x3e, 0xdd, 0x62, - 0x94, 0x5a, 0xca, 0x93, 0x8e, 0x84, 0xda, 0xfa, 0xb2, 0x1f, 0x44, 0xa4, 0x2e, 0xf0, 0x50, 0x13, 0xb1, 0x5e, 0xc6, - 0x74, 0x30, 0x98, 0x2a, 0xda, 0x6f, 0x4f, 0x77, 0xab, 0xd3, 0x37, 0x9b, 0x6a, 0x11, 0xe2, 0xfa, 0x40, 0xea, 0x63, - 0x58, 0x4d, 0x94, 0x9d, 0x1d, 0x7a, 0x09, 0x07, 0xc1, 0x03, 0xa3, 0x73, 0x05, 0x37, 0x19, 0x2e, 0x62, 0xd6, 0x99, - 0x07, 0xdc, 0x25, 0xe5, 0x70, 0x82, 0x38, 0xaf, 0xd0, 0xd5, 0xba, 0x0b, 0xe3, 0x5a, 0xbe, 0xc9, 0xbb, 0xd3, 0xa9, - 0xa4, 0x4e, 0x79, 0xb8, 0x01, 0x6d, 0xa4, 0x36, 0xbd, 0x53, 0x6c, 0x1f, 0xb8, 0x55, 0xfb, 0xef, 0x37, 0x20, 0x93, - 0xbd, 0xcf, 0x1f, 0x38, 0xa0, 0x21, 0x08, 0xd9, 0xe3, 0xe5, 0xf8, 0x02, 0x9d, 0x45, 0xdd, 0x5a, 0x77, 0x75, 0xda, - 0x5d, 0x6f, 0xa1, 0x2a, 0xeb, 0x23, 0x2e, 0x98, 0x9c, 0xa1, 0xc3, 0xb6, 0x95, 0x42, 0x3f, 0x86, 0x9e, 0xc4, 0xbc, - 0xbc, 0xb2, 0x36, 0xac, 0x93, 0x13, 0x7b, 0xc1, 0xd5, 0xbe, 0xf9, 0x83, 0xf8, 0x0c, 0x30, 0x8b, 0xb9, 0xe9, 0xcd, - 0xb3, 0xea, 0x0b, 0x31, 0x44, 0xc6, 0x35, 0x1c, 0x61, 0x02, 0x3e, 0xa0, 0xfa, 0x0f, 0x0e, 0x2d, 0x86, 0xab, 0xe3, - 0x52, 0x83, 0xe9, 0xf8, 0xc1, 0x17, 0xd5, 0x11, 0x12, 0xd3, 0x8e, 0xc7, 0x06, 0xac, 0x31, 0xd4, 0xa0, 0x83, 0x5b, - 0xd3, 0x28, 0x40, 0x10, 0xdb, 0xd7, 0xcf, 0x0d, 0x6e, 0xbf, 0x7b, 0xd7, 0xbb, 0x22, 0xe1, 0xdc, 0xbe, 0x6c, 0x80, - 0x39, 0x61, 0x30, 0x3a, 0x9d, 0xad, 0x6b, 0xa2, 0x2f, 0xea, 0xf7, 0x85, 0x2d, 0xf4, 0x40, 0x6e, 0x4c, 0x78, 0x04, - 0x0b, 0x15, 0x77, 0x90, 0x33, 0xa8, 0x80, 0xfb, 0x0b, 0x7a, 0xc0, 0x82, 0x94, 0xf1, 0x89, 0x78, 0x87, 0xd6, 0x31, - 0x42, 0x0d, 0x2c, 0x38, 0x56, 0x1a, 0x86, 0x03, 0x06, 0xc1, 0xf1, 0x59, 0xd6, 0x88, 0xbc, 0x53, 0x23, 0xf8, 0x2b, - 0x1a, 0x45, 0xeb, 0x58, 0x4a, 0x0a, 0x15, 0xac, 0xe9, 0xec, 0x6b, 0x45, 0xc4, 0xab, 0x29, 0xe8, 0x04, 0x18, 0xd2, - 0x9e, 0x38, 0xfb, 0x74, 0x17, 0xc9, 0xa2, 0x7a, 0xcf, 0x2e, 0x12, 0xe1, 0x2e, 0x03, 0x52, 0xc4, 0x81, 0x4f, 0x87, - 0xd5, 0x74, 0x55, 0x6e, 0xee, 0xa1, 0xad, 0xbd, 0x8b, 0x1d, 0x9d, 0x06, 0xc1, 0xe4, 0xd8, 0xb3, 0xe1, 0x46, 0x2f, - 0x0a, 0x0e, 0x5b, 0x49, 0xd9, 0xaf, 0x22, 0x9c, 0x70, 0xeb, 0xb9, 0x16, 0x2a, 0x1d, 0x34, 0x17, 0x7f, 0xba, 0x42, - 0x2f, 0x21, 0xd4, 0xf6, 0x4c, 0x23, 0x4e, 0x2f, 0xc1, 0xd8, 0x6e, 0xfe, 0x53, 0xb7, 0x0c, 0xda, 0xdc, 0xda, 0xbb, - 0xf4, 0xd6, 0x86, 0xb3, 0x49, 0x65, 0x56, 0x0e, 0xba, 0x17, 0xa5, 0xbb, 0x1c, 0xe3, 0x0c, 0x46, 0xf1, 0x49, 0x3e, - 0xd3, 0xaa, 0xf4, 0xd8, 0xef, 0x36, 0x78, 0xc4, 0x3e, 0xda, 0xc6, 0xd8, 0x21, 0x16, 0x58, 0xe4, 0x78, 0x76, 0x02, - 0x35, 0x0e, 0x8d, 0x78, 0x4d, 0x11, 0x5a, 0x52, 0x7b, 0x87, 0x8f, 0x3e, 0xf6, 0xd6, 0xca, 0x77, 0xe4, 0xc5, 0x5a, - 0x04, 0x50, 0x83, 0x9a, 0x56, 0x09, 0xdd, 0xa5, 0x9b, 0x67, 0xbc, 0x06, 0x2c, 0x3a, 0x0a, 0x87, 0x28, 0xdf, 0x39, - 0x57, 0xd0, 0x8e, 0xb6, 0x48, 0x64, 0x1d, 0xa3, 0xa9, 0x14, 0xb9, 0xfe, 0xc3, 0x32, 0x0d, 0x9a, 0x1f, 0xdb, 0x57, - 0x90, 0xbd, 0x39, 0x1f, 0xf3, 0x3f, 0x9e, 0xb3, 0x2f, 0xd9, 0x5e, 0x17, 0x00, 0xae, 0x9f, 0xfd, 0x63, 0x62, 0xf2, - 0x67, 0x16, 0x86, 0xf9, 0x0f, 0xf5, 0x09, 0x6f, 0x02, 0xff, 0x04, 0xcf, 0x59, 0x62, 0xbc, 0x97, 0xe2, 0x3c, 0xc5, - 0x33, 0x97, 0x40, 0x6f, 0x4b, 0xbe, 0x6c, 0x01, 0xbc, 0xb8, 0xd4, 0xbc, 0x5d, 0x70, 0x36, 0x46, 0x14, 0xa8, 0xbe, - 0xd5, 0x93, 0x5c, 0x0e, 0xba, 0x51, 0x8a, 0xb8, 0xe9, 0xb7, 0x0a, 0x34, 0xa3, 0x55, 0x62, 0x76, 0x19, 0x7a, 0xb1, - 0x74, 0x9c, 0xab, 0xf0, 0x33, 0x19, 0x6c, 0x83, 0x7d, 0x22, 0xc2, 0x65, 0xd2, 0x63, 0x84, 0xbf, 0x4d, 0x91, 0x7c, - 0xab, 0xc3, 0xf5, 0x83, 0xc6, 0xbf, 0xee, 0xfd, 0xfa, 0xd5, 0xe3, 0x8b, 0x9b, 0x86, 0xb0, 0x7a, 0xa0, 0x7c, 0x72, - 0xb6, 0xde, 0xed, 0x4c, 0x0f, 0x03, 0xc5, 0x43, 0x61, 0x34, 0x3a, 0xc6, 0x49, 0x61, 0x36, 0x9b, 0x75, 0xfd, 0xd0, - 0xf5, 0x1f, 0x39, 0x1d, 0x48, 0xb0, 0x0c, 0xe5, 0xde, 0x9d, 0x81, 0x79, 0xbd, 0x35, 0x90, 0xb5, 0x5c, 0xe5, 0xc0, - 0x9d, 0x9e, 0xa9, 0xde, 0x8e, 0x14, 0x8e, 0x78, 0xa4, 0x15, 0xee, 0xcc, 0x5e, 0x66, 0x03, 0xba, 0x8b, 0x73, 0x45, - 0x77, 0xce, 0x29, 0x59, 0x44, 0x96, 0x9f, 0xf4, 0x8e, 0xde, 0xec, 0xf8, 0xd8, 0xbd, 0x2b, 0x09, 0x2c, 0xff, 0x6f, - 0xd4, 0xa1, 0x7a, 0x48, 0x8c, 0x14, 0x9e, 0x45, 0xb1, 0xb1, 0x2a, 0x86, 0xef, 0xf0, 0x5b, 0xc9, 0x53, 0xed, 0x15, - 0xc3, 0x02, 0xdf, 0x35, 0xcc, 0xdd, 0x3a, 0x12, 0xbc, 0x4c, 0xc7, 0x80, 0x47, 0x62, 0xc0, 0x6f, 0x36, 0x8f, 0x08, - 0x5d, 0x27, 0x7b, 0x1c, 0x3f, 0x05, 0xe1, 0xc6, 0x15, 0x94, 0x33, 0x23, 0x7c, 0x83, 0x91, 0x83, 0xa7, 0x98, 0x3f, - 0xde, 0xdc, 0x41, 0xf5, 0xf1, 0xc3, 0xbe, 0x58, 0x7b, 0xf0, 0xd7, 0x02, 0xac, 0x81, 0x3c, 0xda, 0x50, 0x3d, 0x4b, - 0xf5, 0xce, 0xfd, 0x35, 0x4f, 0x0b, 0x7e, 0x46, 0x6e, 0x74, 0x5b, 0x9c, 0x23, 0x5f, 0xe2, 0xed, 0xb6, 0x13, 0x6f, - 0x77, 0x7d, 0x6b, 0x7e, 0xd4, 0x08, 0x10, 0x36, 0xbf, 0x2d, 0xdb, 0xfa, 0xc3, 0xc5, 0xed, 0x97, 0xf6, 0xce, 0x60, - 0x07, 0xb8, 0xc4, 0x80, 0x8d, 0xae, 0x8b, 0xd8, 0xe6, 0x8c, 0x1b, 0x63, 0x17, 0x71, 0xd8, 0x00, 0xa4, 0x8a, 0x98, - 0x08, 0x4d, 0xe5, 0x28, 0x04, 0x83, 0xa1, 0x37, 0x7d, 0x1f, 0xef, 0x33, 0x0f, 0xb0, 0x01, 0x9b, 0x4c, 0x6d, 0x42, - 0xd8, 0x98, 0x54, 0xb7, 0x7e, 0x1d, 0xa1, 0x2c, 0xc6, 0xc6, 0xd2, 0x9a, 0x2b, 0x0b, 0x42, 0x9f, 0xb7, 0xfe, 0x56, - 0xc3, 0x36, 0xd7, 0xf8, 0xb7, 0x58, 0x44, 0xfc, 0x98, 0x72, 0xd8, 0x5f, 0xc2, 0xa7, 0x0b, 0xc7, 0xe8, 0xe8, 0x93, - 0xc6, 0x99, 0x71, 0xaa, 0xae, 0x95, 0xfe, 0x56, 0xc6, 0x43, 0x1f, 0xdf, 0xdd, 0x98, 0x2a, 0x3b, 0xf4, 0x12, 0x2c, - 0x3a, 0x0a, 0xe3, 0x21, 0x9e, 0x06, 0x75, 0x1d, 0x47, 0x32, 0x98, 0xba, 0xc7, 0x99, 0xbe, 0xda, 0xce, 0xa2, 0xb8, - 0x8c, 0xd8, 0x79, 0x5f, 0x5a, 0x2d, 0xe3, 0xa0, 0x5a, 0xb8, 0x88, 0x8e, 0x19, 0xd4, 0x22, 0xe2, 0x9d, 0x7a, 0xf1, - 0x24, 0xf9, 0x98, 0xd3, 0x71, 0xa0, 0x74, 0x2d, 0x69, 0x8f, 0x05, 0x34, 0x44, 0x66, 0x14, 0x5e, 0xfa, 0xa9, 0x9b, - 0xfd, 0xd3, 0xf8, 0x7f, 0x5d, 0x6e, 0xb6, 0xdb, 0x63, 0xbb, 0x12, 0x85, 0x39, 0x4d, 0x0e, 0x81, 0xb6, 0xe0, 0x3b, - 0x6e, 0xf5, 0x31, 0x47, 0xc6, 0x78, 0xad, 0x4b, 0xfa, 0xa5, 0xad, 0x3a, 0x8f, 0xda, 0x35, 0x5a, 0xbf, 0xc0, 0x51, - 0x21, 0xb4, 0xd3, 0x6c, 0xb4, 0x8b, 0x0f, 0x7c, 0xde, 0x3c, 0x98, 0x86, 0x26, 0x14, 0x53, 0x4b, 0xf5, 0x90, 0x39, - 0x2a, 0x9f, 0xe3, 0xf4, 0x1e, 0x80, 0x8a, 0x48, 0x7b, 0xf7, 0x7e, 0xa6, 0xde, 0x5f, 0x6b, 0x86, 0xee, 0xa3, 0x56, - 0xca, 0x48, 0xf8, 0x6d, 0x87, 0x90, 0xb0, 0x0a, 0x49, 0x18, 0x3b, 0x27, 0xca, 0x59, 0xd6, 0x36, 0x86, 0x96, 0xf7, - 0x87, 0x83, 0xe7, 0x89, 0x56, 0xcb, 0xb8, 0xc5, 0x23, 0x72, 0xbb, 0xf7, 0x99, 0x48, 0xf5, 0xa2, 0xea, 0xf2, 0x08, - 0x82, 0x45, 0x27, 0x32, 0xd2, 0x5f, 0x8c, 0xda, 0x71, 0x02, 0xfd, 0x7b, 0xf9, 0x53, 0x50, 0x52, 0xd4, 0x0a, 0xa7, - 0x8c, 0x75, 0x13, 0x9d, 0x68, 0x29, 0xc2, 0xc8, 0xa6, 0xaf, 0x82, 0xff, 0x04, 0x37, 0x58, 0x79, 0x77, 0xcf, 0x33, - 0xa2, 0x6a, 0xe1, 0x11, 0x45, 0x32, 0x26, 0xee, 0x7e, 0x0e, 0xb3, 0x84, 0x5e, 0x7a, 0x77, 0xad, 0x75, 0xea, 0x9c, - 0x2e, 0xde, 0x04, 0x51, 0x0a, 0xa2, 0xbb, 0xcf, 0xf1, 0x13, 0xe3, 0x00, 0xe9, 0x06, 0xf8, 0xe7, 0x04, 0xc9, 0x29, - 0x4f, 0x54, 0x5e, 0x06, 0xd3, 0x36, 0xa4, 0x60, 0xf8, 0x58, 0xef, 0xc1, 0x8d, 0x37, 0x7c, 0xb9, 0x9c, 0xfa, 0xe6, - 0xcd, 0x23, 0x57, 0x3d, 0xc4, 0xd3, 0x38, 0xef, 0x6c, 0x5a, 0x26, 0xf8, 0x48, 0x12, 0xff, 0xac, 0x4d, 0xec, 0xb6, - 0x6c, 0xd2, 0xf3, 0xa6, 0xdb, 0xc2, 0xd9, 0xbd, 0x65, 0x0e, 0xb2, 0xd8, 0xf4, 0x05, 0x20, 0xe5, 0x80, 0xd6, 0xc5, - 0x2e, 0x0a, 0x05, 0x71, 0x1a, 0xe0, 0x02, 0x30, 0x42, 0x4b, 0x2c, 0x56, 0xe0, 0x89, 0xc6, 0xd3, 0x2c, 0xa7, 0xc5, - 0x36, 0x78, 0x37, 0x82, 0x67, 0x89, 0x5c, 0x4a, 0xd3, 0x90, 0x36, 0xb5, 0x94, 0xf0, 0xcc, 0xa9, 0xf5, 0x6d, 0x9a, - 0x6e, 0x6a, 0x93, 0xd9, 0x7c, 0xec, 0x8a, 0x15, 0x6d, 0xe8, 0xe0, 0x0e, 0x26, 0xd1, 0x16, 0x42, 0x36, 0x6a, 0x20, - 0xdb, 0xec, 0x4f, 0x59, 0xb2, 0xd3, 0x54, 0xa1, 0x27, 0xb5, 0x6e, 0x89, 0x16, 0x47, 0xa6, 0xde, 0xcd, 0x02, 0x49, - 0xb0, 0x85, 0x86, 0x63, 0x61, 0x45, 0xd3, 0xe3, 0x7b, 0xee, 0xa3, 0x63, 0x60, 0xa1, 0x96, 0x9e, 0xc6, 0x1c, 0xbd, - 0x33, 0x88, 0x69, 0xd6, 0x32, 0xb2, 0x65, 0xe3, 0xf3, 0x1e}; +const uint8_t INDEX_BR[] PROGMEM = { + 0x5b, 0x05, 0x7b, 0x53, 0xc1, 0xb6, 0x69, 0x3d, 0x41, 0xeb, 0x04, 0x30, 0xf6, 0xd6, 0x77, 0x35, 0xdb, 0xa3, 0x08, + 0x36, 0x0e, 0x04, 0x80, 0x90, 0x4f, 0xf1, 0xb2, 0x21, 0xa4, 0x82, 0xee, 0x00, 0xaa, 0x20, 0x7f, 0x3b, 0xff, 0x00, + 0xaa, 0x9a, 0x73, 0x74, 0x8c, 0xe1, 0xa6, 0x1f, 0xa0, 0xa2, 0x59, 0xf5, 0xaa, 0x92, 0x79, 0x50, 0x43, 0x1f, 0xe8, + 0x3c, 0x52, 0x68, 0x2c, 0xf6, 0x36, 0x31, 0x90, 0x4e, 0x2b, 0x91, 0x69, 0x38, 0xb4, 0x61, 0xa7, 0xbb, 0x57, 0x79, + 0x3b, 0x6d, 0x62, 0x85, 0x9f, 0x24, 0x24, 0xda, 0x45, 0xc1, 0xe2, 0x85, 0x44, 0x50, 0x6c, 0x8c, 0x1e, 0xab, 0xbb, + 0x7b, 0x1e, 0x30, 0x98, 0x59, 0xe5, 0xb9, 0xd1, 0x34, 0x03, 0x0b, 0x79, 0x37, 0x8c, 0x4b, 0x68, 0x2c, 0xbe, 0x21, + 0xcc, 0x30, 0xc4, 0x9c, 0x4d, 0x34, 0xd6, 0x01, 0xc1, 0xe1, 0x66, 0x48, 0x3c, 0x94, 0xf4, 0x3c, 0x5b, 0x12, 0xfd, + 0x15, 0xb4, 0x05, 0xdb, 0xaf, 0xc1, 0x59, 0x5d, 0x9f, 0xc5, 0x8d, 0xaf, 0xf7, 0xc6, 0x5f, 0x58, 0xfb, 0xc3, 0x74, + 0x45, 0x97, 0x23, 0xac, 0x2e, 0x4a, 0xb9, 0xab, 0xd7, 0x5b, 0xe5, 0x85, 0x6f, 0x21, 0xba, 0x7e, 0xa3, 0x57, 0xa5, + 0xb3, 0x24, 0x7e, 0x60, 0x30, 0x0e, 0x15, 0x1f, 0x04, 0x5e, 0xba, 0x06, 0x04, 0xff, 0x02, 0x26, 0x2d, 0x03, 0x6c, + 0x97, 0x1b, 0x23, 0x84, 0x28, 0x25, 0xb4, 0xe2, 0xca, 0xbd, 0xc8, 0xc3, 0x96, 0x4a, 0xef, 0xed, 0xe7, 0x65, 0x4f, + 0xb2, 0x2d, 0x6c, 0x02, 0x91, 0x04, 0x76, 0xae, 0xba, 0x67, 0x9c, 0xe3, 0x38, 0xd6, 0x96, 0x61, 0x0c, 0xd3, 0x80, + 0xe4, 0x4a, 0x43, 0x2e, 0x92, 0xff, 0xa7, 0xdb, 0xd2, 0xec, 0xf5, 0xf5, 0x90, 0x4a, 0x22, 0x74, 0x92, 0x40, 0x80, + 0xed, 0x2b, 0xb5, 0xbc, 0xb6, 0xe0, 0x71, 0xda, 0x35, 0x3b, 0xe9, 0xec, 0xeb, 0xac, 0xbe, 0x6a, 0x2f, 0xf0, 0xde, + 0x96, 0xf7, 0x43, 0x51, 0x4d, 0xce, 0xbb, 0xd3, 0x70, 0xc2, 0x08, 0x2c, 0xf0, 0xc8, 0xac, 0x25, 0x82, 0x67, 0xeb, + 0xcd, 0xf5, 0x7d, 0x7d, 0xb7, 0x8b, 0xca, 0x2a, 0x15, 0x66, 0x68, 0xf3, 0x6e, 0xb6, 0xda, 0x5b, 0x81, 0x7b, 0x2e, + 0xe6, 0xc1, 0xdc, 0x5e, 0x0a, 0x7a, 0xda, 0x4a, 0x2c, 0x68, 0xa4, 0xd0, 0x92, 0xe7, 0xb2, 0xb3, 0xef, 0xab, 0xda, + 0xd7, 0xef, 0x89, 0xe7, 0x22, 0xd0, 0x93, 0xe5, 0x08, 0xcc, 0xea, 0x82, 0xdb, 0xde, 0x1a, 0xdd, 0x49, 0x47, 0x26, + 0x23, 0xc1, 0x16, 0x7b, 0x2a, 0x99, 0x23, 0xe9, 0x4c, 0xf8, 0x65, 0xaa, 0xfe, 0xe9, 0x6a, 0x41, 0x0d, 0x8f, 0xfb, + 0x48, 0x9a, 0x49, 0x4e, 0x0b, 0x2e, 0x91, 0xfc, 0x76, 0x6e, 0x55, 0xee, 0xb4, 0xbb, 0x3c, 0x63, 0xc1, 0x95, 0x5a, + 0xf8, 0x37, 0x35, 0xb1, 0xaa, 0xcd, 0xa9, 0x99, 0x2f, 0x3d, 0x52, 0xd0, 0x01, 0xd9, 0x06, 0x27, 0x41, 0x92, 0xdd, + 0x6d, 0x4a, 0xee, 0xad, 0xa6, 0x33, 0xe1, 0xff, 0xeb, 0x6b, 0x5a, 0xff, 0xfd, 0xf3, 0x05, 0x0a, 0xbb, 0xc4, 0x55, + 0x1d, 0x28, 0x69, 0x66, 0x7f, 0x4f, 0x49, 0xce, 0xb2, 0xec, 0x8a, 0x0a, 0x15, 0x2d, 0x63, 0x1b, 0x6f, 0x28, 0x00, + 0xf5, 0x44, 0x47, 0xd6, 0xf5, 0xb5, 0xd7, 0x6f, 0xff, 0xf5, 0x0b, 0x3d, 0xd9, 0x1d, 0x42, 0xe9, 0xb3, 0x32, 0xa5, + 0xf6, 0xa3, 0x1b, 0xc3, 0x58, 0x8a, 0x7c, 0x24, 0xc7, 0x03, 0xbc, 0x92, 0xd5, 0x4f, 0xbf, 0xfc, 0xfa, 0x3d, 0x3b, + 0x6d, 0x8c, 0xc5, 0xdb, 0x9d, 0x77, 0x97, 0xd2, 0x5e, 0x82, 0x85, 0x53, 0x9a, 0xaf, 0xb6, 0x09, 0x45, 0x51, 0x25, + 0x90, 0x44, 0x85, 0xa4, 0xc6, 0x23, 0x18, 0xfb, 0xbb, 0x56, 0xef, 0xe9, 0x3c, 0x13, 0x62, 0xf0, 0xaf, 0x6b, 0xe9, + 0xa2, 0xa7, 0x26, 0x32, 0xa1, 0x8d, 0x5d, 0x56, 0x22, 0xa0, 0xe8, 0xb8, 0x06, 0xc1, 0x07, 0xb4, 0x7e, 0xf5, 0xbe, + 0xae, 0xff, 0xfa, 0x95, 0xa7, 0xb8, 0xe2, 0x34, 0x6a, 0xc9, 0x3c, 0xee, 0xb3, 0x45, 0x67, 0xae, 0x93, 0x6c, 0x20, + 0xbc, 0x42, 0xc5, 0xe1, 0x91, 0x52, 0xb9, 0x8c, 0x62, 0x0d, 0x46, 0xbb, 0x46, 0x72, 0x69, 0x26, 0xe0, 0xac, 0x67, + 0xec, 0xb5, 0x51, 0x5f, 0x9d, 0xae, 0x5d, 0x25, 0x15, 0x72, 0xf0, 0xc2, 0x2f, 0x67, 0xe6, 0x68, 0x08, 0xac, 0xdd, + 0xcb, 0xd5, 0xe3, 0x03, 0x9d, 0x49, 0x4d, 0xa1, 0xcd, 0x81, 0xbf, 0x09, 0xd9, 0xdd, 0x2d, 0xe1, 0x7b, 0x7f, 0x9a, + 0x7d, 0xfd, 0x92, 0xd9, 0x26, 0x19, 0x8d, 0x9d, 0x2b, 0x6d, 0x38, 0x99, 0x2b, 0x2d, 0xb5, 0xfa, 0xed, 0xe3, 0xb0, + 0x11, 0x2c, 0x17, 0x79, 0xe0, 0x24, 0xb1, 0x86, 0x68, 0x15, 0xb3, 0x7f, 0x4b, 0xf3, 0xeb, 0xd7, 0xc3, 0xdd, 0x24, + 0x64, 0x69, 0x0f, 0x4e, 0xae, 0xdb, 0xc7, 0x71, 0x4e, 0xaa, 0x64, 0x18, 0xec, 0xd1, 0x7b, 0x59, 0xa8, 0x31, 0x05, + 0x80, 0x2b, 0xcb, 0x0c, 0x91, 0x02, 0x85, 0x0d, 0x16, 0x1d, 0x1b, 0xa6, 0x6e, 0x48, 0x13, 0x04, 0xa1, 0xc5, 0xff, + 0x33, 0xa7, 0xdf, 0xb4, 0x67, 0x98, 0xb0, 0x60, 0xdb, 0x52, 0xfa, 0xaf, 0x26, 0xbf, 0x98, 0xed, 0xc1, 0x7b, 0x18, + 0x2e, 0x02, 0x9e, 0x60, 0x3c, 0xef, 0xff, 0xef, 0xad, 0xb4, 0xdc, 0xfe, 0x88, 0x74, 0x45, 0x88, 0xec, 0x01, 0xc8, + 0x71, 0x86, 0x23, 0xeb, 0xf7, 0x9d, 0x99, 0x55, 0xe0, 0x34, 0x40, 0xb2, 0xc7, 0x99, 0x95, 0xb4, 0xd6, 0x62, 0x53, + 0x71, 0xef, 0x7d, 0xef, 0x32, 0xbf, 0x8b, 0xce, 0xf8, 0x61, 0x58, 0x61, 0x32, 0x1b, 0x69, 0x87, 0x65, 0xbb, 0xb2, + 0x9c, 0x06, 0x00, 0xc1, 0xfb, 0xde, 0xfb, 0x51, 0xf8, 0xff, 0x47, 0x16, 0xe7, 0x47, 0x64, 0x81, 0x8a, 0xcc, 0x2a, + 0xce, 0xc9, 0x2c, 0x60, 0x46, 0x55, 0x00, 0x67, 0x54, 0x00, 0xfb, 0xe8, 0x80, 0x1c, 0x0b, 0x82, 0x46, 0xd3, 0x64, + 0xb3, 0x8f, 0x86, 0x6c, 0x79, 0xbf, 0xd2, 0x62, 0x05, 0x51, 0xae, 0x67, 0x64, 0x5b, 0xf2, 0xbb, 0x1d, 0x28, 0x6b, + 0x96, 0xd2, 0x4a, 0x47, 0xff, 0xeb, 0x96, 0xeb, 0x89, 0x6e, 0x15, 0x9f, 0x68, 0xa7, 0xdc, 0x30, 0x8c, 0xfc, 0x9f, + 0xc0, 0x23, 0xe1, 0x0c, 0x4e, 0xc3, 0x69, 0xa8, 0xc2, 0xd5, 0xa0, 0xa6, 0x5b, 0xc7, 0x8e, 0xb3, 0xe9, 0x34, 0x54, + 0xfd, 0x31, 0xfc, 0x1e, 0x4a, 0x0b, 0x83, 0xa9, 0x33, 0x4d, 0xd3, 0x7f, 0x77, 0x8d, 0x72, 0x55, 0x71, 0x4d, 0x74, + 0xe3, 0xaa, 0x55, 0x19, 0xf8, 0x9b, 0xa6, 0x69, 0xf8, 0x87, 0xe4, 0x6e, 0x5f, 0xac, 0xf9, 0x3d, 0xda, 0x61, 0xa0, + 0x50, 0xca, 0x2e, 0x93, 0x38, 0x3e, 0xe4, 0x5b, 0x96, 0x25, 0xd9, 0xf0, 0x75, 0x2c, 0xad, 0x37, 0x37, 0x2a, 0x15, + 0x48, 0xe8, 0xbd, 0xf6, 0x19, 0xb5, 0x55, 0xf0, 0x10, 0x5d, 0x14, 0x49, 0xa4, 0xa7, 0xff, 0xa7, 0xaa, 0x7a, 0xbc, + 0x43, 0xa6, 0x66, 0xdd, 0xd8, 0xc4, 0x05, 0xf2, 0xe9, 0x1b, 0x34, 0x4f, 0x12, 0x1a, 0x1b, 0xac, 0xf3, 0x32, 0x32, + 0xad, 0x2b, 0xeb, 0xd8, 0x29, 0x4e, 0xfe, 0x6f, 0x80, 0xa1, 0x0d, 0xad, 0x4a, 0xc2, 0xb6, 0x83, 0xa0, 0xff, 0x50, + 0x68, 0x62, 0xad, 0x71, 0x00, 0x77, 0x5a, 0x66, 0xb4, 0x05, 0x9d, 0x01, 0x51, 0x9c, 0xbb, 0xf8, 0xd3, 0x64, 0x82, + 0x69, 0xb6, 0x87, 0x82, 0xef, 0x73, 0x14, 0x62, 0x0c, 0x22, 0xcb, 0x73, 0xdb, 0xb3, 0x0f, 0x4c, 0xfe, 0x77, 0x60, + 0x45, 0xff, 0x14, 0xea, 0x9f, 0xb0, 0x02, 0xa0, 0x6e, 0xbd, 0xe4, 0xd7, 0x0a, 0xbc, 0xe7, 0xf7, 0x00, 0x44, 0x26, + 0xc2, 0x96, 0xe5, 0x37, 0xda, 0x32, 0xca, 0xef, 0x42, 0xb4, 0xdd, 0xe2, 0x70, 0xf0, 0xe0, 0xc1, 0x93, 0xd4, 0xed, + 0x18, 0xbf, 0x54, 0xdd, 0xb0, 0x5b, 0x86, 0x3e, 0x46, 0x7f, 0xbe, 0x1b, 0x61, 0x83, 0x52, 0xe1, 0x6a, 0x62, 0x7a, + 0xa6, 0x7e, 0x6f, 0xc4, 0x5b, 0x63, 0x55, 0x1a, 0xb8, 0xa5, 0x87, 0xd9, 0x84, 0xd4, 0xcf, 0xd7, 0xd4, 0x62, 0x26, + 0x28, 0x13, 0x41, 0x44, 0xca, 0x76, 0xf6, 0x21, 0x63, 0x4b, 0xae, 0xaf, 0xe7, 0xc7, 0x4d, 0x79, 0x5d, 0x8f, 0xc3, + 0x04, 0x0d, 0xc9, 0x06, 0x39, 0xa0, 0xd8, 0xde, 0x87, 0xb3, 0xd7, 0x4b, 0x65, 0x4e, 0xa3, 0xa6, 0xff, 0x79, 0xab, + 0x0c, 0xed, 0xb4, 0x01, 0x69, 0xdc, 0xa6, 0x15, 0x98, 0x98, 0x82, 0xc8, 0x86, 0x4d, 0x06, 0x77, 0xbd, 0x39, 0x6c, + 0x73, 0x52, 0x73, 0x48, 0xd6, 0xe4, 0x8a, 0x4a, 0x83, 0x9a, 0xf5, 0xc9, 0xeb, 0xa5, 0x2e, 0xa9, 0x70, 0x4b, 0x9d, + 0xb9, 0xbe, 0xc8, 0xe4, 0x9e, 0x17, 0xf2, 0xca, 0x95, 0x97, 0xd3, 0x14, 0xd8, 0x9b, 0xef, 0xb1, 0xaf, 0x13, 0x9a, + 0xf5, 0x3d, 0x8f, 0x74, 0xe3, 0x5d, 0x47, 0x07, 0x26, 0xd2, 0x60, 0xa2, 0xef, 0x11, 0x34, 0x2f, 0x6e, 0xb3, 0xfe, + 0xf0, 0xa3, 0x42, 0x7d, 0xfb, 0x47, 0x5c, 0x75, 0x98, 0x0f, 0xe6, 0x0f, 0x4e, 0xe5, 0x67, 0x1a, 0x4f, 0x93, 0x47, + 0x5f, 0x04, 0x7c, 0xff, 0x7a, 0xf9, 0x79, 0x6b, 0x46, 0x47, 0xc6, 0x0c, 0xd5, 0x90, 0x30, 0xd5, 0xfc, 0xde, 0x8b, + 0xd5, 0x65, 0x7f, 0x87, 0x2d, 0x9a, 0xd5, 0xe4, 0x3f, 0xf9, 0xed, 0x07, 0xfb, 0xa2, 0xb6, 0xd3, 0xe1, 0xbf, 0xbd, + 0x40, 0x78, 0x7a, 0x67, 0x24, 0x0b, 0x6b, 0x0e, 0xed, 0xcf, 0x7a, 0x6a, 0x7c, 0xdb, 0x36, 0x61, 0x5b, 0xc3, 0x7c, + 0x5d, 0xfc, 0xf6, 0x1c, 0xc2, 0xa9, 0xba, 0x12, 0x15, 0x35, 0x11, 0x87, 0x41, 0x13, 0xa5, 0xe5, 0x73, 0x07, 0xfa, + 0xc6, 0xe3, 0x96, 0x43, 0x01, 0x76, 0x4b, 0x53, 0x28, 0xad, 0x09, 0x7e, 0x88, 0x0f, 0x26, 0x90, 0x04, 0xfd, 0x2a, + 0x0d, 0x4c, 0xcd, 0x5c, 0xbf, 0x10, 0xd6, 0x47, 0xaf, 0xe2, 0x12, 0x80, 0x00, 0x59, 0xaa, 0x9b, 0x18, 0x58, 0x96, + 0xc8, 0x80, 0x67, 0xc2, 0xb9, 0x4e, 0x5d, 0x86, 0x1e, 0x79, 0xf5, 0xcf, 0xb0, 0x81, 0x1f, 0x9e, 0x4f, 0x34, 0x1d, + 0x7c, 0xf2, 0x4a, 0x4d, 0xbd, 0x42, 0x06, 0xbc, 0x73, 0x56, 0xbc, 0x9d, 0x43, 0xa9, 0xe6, 0x44, 0x0c, 0xcd, 0xcd, + 0xe4, 0x4e, 0xde, 0xb3, 0x0e, 0x25, 0x35, 0xb6, 0xb6, 0xf6, 0xcc, 0xae, 0x6f, 0x91, 0x82, 0x59, 0xa1, 0xdc, 0x8b, + 0xaa, 0x4f, 0x64, 0x26, 0xd0, 0xa5, 0xe7, 0x38, 0xf3, 0xf5, 0xcd, 0x4f, 0x05, 0x62, 0x8c, 0x38, 0xc3, 0x96, 0x13, + 0x68, 0xb2, 0xe4, 0xd9, 0xcf, 0x4a, 0x5f, 0x44, 0x57, 0xf6, 0x49, 0x47, 0xae, 0x16, 0x81, 0xa1, 0xa7, 0x2d, 0xd8, + 0xb3, 0x35, 0x74, 0x6a, 0xc2, 0xbc, 0xc0, 0x7d, 0xae, 0xf0, 0x88, 0xe4, 0xd0, 0x28, 0x7c, 0x22, 0x98, 0x95, 0xa3, + 0x2a, 0x81, 0x16, 0x0b, 0xc7, 0x4a, 0xf3, 0x07, 0xb8, 0xa1, 0x56, 0xbf, 0xdf, 0x36, 0x6b, 0xa3, 0x84, 0x8b, 0xbf, + 0x24, 0x99, 0xc1, 0x09, 0x7e, 0xff, 0x99, 0x8c, 0x1c, 0xd1, 0x43, 0x7c, 0xb1, 0x46, 0x9d, 0x2e, 0x65, 0x92, 0xa9, + 0xa0, 0xd0, 0x45, 0x92, 0x47, 0x37, 0x9c, 0x3c, 0x5f, 0xf1, 0xf3, 0x0d, 0x1e, 0x37, 0xeb, 0x3d, 0xb6, 0x7c, 0x33, + 0x35, 0xaf, 0xf3, 0x08, 0x54, 0x33, 0x56, 0x02, 0x4f, 0x18, 0xda, 0xc0, 0xbb, 0xc5, 0x4a, 0x42, 0xd7, 0xef, 0xbd, + 0xa4, 0xec, 0x61, 0x77, 0x1b, 0xa2, 0x57, 0x47, 0x00, 0xee, 0x8b, 0xd3, 0x56, 0xd4, 0xbd, 0x01, 0xa2, 0x8f, 0xee, + 0xef, 0xb1, 0xac, 0xe1, 0x03, 0x87, 0x8d, 0x2b, 0x3c, 0x8e, 0x15, 0x84, 0x96, 0xb4, 0xfe, 0x56, 0xd5, 0x1e, 0xc0, + 0x83, 0x66, 0x79, 0x15, 0x8a, 0x60, 0xb7, 0x15, 0x21, 0x3b, 0xce, 0x44, 0x71, 0x9f, 0x6f, 0xe0, 0x70, 0xde, 0xb8, + 0x7a, 0x74, 0x43, 0xcd, 0x4d, 0x7a, 0x90, 0xd2, 0x4b, 0x9b, 0xc8, 0x6a, 0xa2, 0xc6, 0xdf, 0x1a, 0x55, 0x85, 0x34, + 0xdd, 0x1d, 0x1a, 0x7c, 0x88, 0x7c, 0x81, 0x3d, 0xd8, 0x12, 0xf4, 0x32, 0x8d, 0xc6, 0xc1, 0x56, 0x0d, 0xe5, 0x8d, + 0x75, 0x00, 0x03, 0x61, 0x93, 0xa0, 0x44, 0x06, 0x5b, 0x67, 0x8b, 0x58, 0xf5, 0x9c, 0xb0, 0x79, 0x7f, 0xbd, 0x2e, + 0xb1, 0x17, 0xb3, 0x85, 0xcd, 0x54, 0x5f, 0xc8, 0x38, 0xeb, 0xa7, 0xcd, 0xfe, 0xbf, 0x2d, 0x80, 0x83, 0x12, 0xc3, + 0x0b, 0x02, 0x41, 0x44, 0xd5, 0x07, 0xe5, 0xcd, 0xb0, 0x24, 0x2c, 0x0a, 0x6c, 0x1b, 0x1f, 0xb9, 0x7b, 0x48, 0x9e, + 0x55, 0x42, 0x7c, 0x2b, 0x63, 0xd3, 0xd1, 0x76, 0x18, 0x61, 0xa8, 0x86, 0x2d, 0x11, 0x5a, 0x41, 0x04, 0x6c, 0xea, + 0xcf, 0x34, 0xf6, 0x71, 0xe7, 0xda, 0x41, 0xba, 0x28, 0xcb, 0x2c, 0x1c, 0x47, 0x50, 0xa7, 0x83, 0x41, 0xad, 0x84, + 0x9e, 0xec, 0x1e, 0xfc, 0xc6, 0xc6, 0xb8, 0xa0, 0xb8, 0xa1, 0x70, 0xeb, 0x5a, 0x9f, 0x46, 0x06, 0xa6, 0xf8, 0x72, + 0xa5, 0xff, 0xfe, 0x80, 0x1e, 0x07, 0xbb, 0xd4, 0x48, 0xf9, 0x6c, 0xd6, 0x13, 0xf8, 0xee, 0x86, 0x06, 0x67, 0x78, + 0x38, 0xf2, 0x83, 0xc3, 0x3b, 0x25, 0xf0, 0xa0, 0x60, 0x56, 0xbe, 0x7d, 0x10, 0x8a, 0xd4, 0x17, 0x81, 0x0e, 0x17, + 0x5f, 0x53, 0xaf, 0x87, 0x23, 0xe4, 0x56, 0xe4, 0xb1, 0xc0, 0x9d, 0xc8, 0x38, 0x25, 0x47, 0x18, 0x18, 0x27, 0x57, + 0xdf, 0x84, 0xcd, 0x7c, 0xdb, 0x31, 0xff, 0xc2, 0xe5, 0x83, 0x03, 0xd1, 0xac, 0x9f, 0x2d, 0xd8, 0xa5, 0xa4, 0x40, + 0xe0, 0x1e, 0xe9, 0x18, 0x3d, 0x85, 0xa9, 0x1b, 0x9c, 0xa6, 0x14, 0x51, 0x9a, 0x88, 0xd9, 0x42, 0x70, 0x6c, 0x6b, + 0x63, 0x2e, 0xfd, 0x46, 0xd9, 0xd5, 0xb4, 0xd9, 0x8f, 0x2c, 0xf8, 0x42, 0xf9, 0xb6, 0x27, 0xb8, 0x69, 0xc5, 0xed, + 0x5c, 0xea, 0xff, 0x76, 0x9d, 0x49, 0x1b, 0x5a, 0xf7, 0xaa, 0x2d, 0x04, 0x36, 0x45, 0x05, 0xa8, 0x99, 0x5e, 0x24, + 0x53, 0x3b, 0x89, 0xd9, 0x0f, 0x4d, 0x24, 0x27, 0x44, 0x2b, 0xfb, 0x3b, 0xd9, 0x8b, 0x36, 0xe9, 0x18, 0x4a, 0x30, + 0xfc, 0xc8, 0xa5, 0xf4, 0xd5, 0xd5, 0x72, 0x2d, 0x3b, 0x5f, 0xc3, 0x4e, 0x98, 0x0c, 0x08, 0xb2, 0xff, 0x59, 0x68, + 0x6b, 0xc0, 0xe4, 0x52, 0x8f, 0x29, 0x78, 0x74, 0x6d, 0xbe, 0xfb, 0x13, 0x8a, 0x25, 0x0b, 0x31, 0xe5, 0xd0, 0xae, + 0xbf, 0x1e, 0x13, 0x23, 0xa0, 0x2c, 0x89, 0x10, 0x6e, 0xa5, 0x1c, 0xa8, 0x7f, 0xbf, 0x62, 0x52, 0xb0, 0xa5, 0xf6, + 0x46, 0x9c, 0x5d, 0xbd, 0xac, 0x81, 0xc4, 0x46, 0xf3, 0x61, 0x62, 0x47, 0x08, 0x27, 0x4d, 0xed, 0x4b, 0x45, 0x91, + 0x48, 0xcf, 0x52, 0xc4, 0x20, 0xe3, 0x8a, 0xe9, 0x12, 0x2d, 0xac, 0x99, 0xb0, 0x1c, 0x1b, 0x91, 0xa4, 0xcc, 0x6d, + 0x11, 0x3f, 0xbe, 0xe9, 0x06, 0x24, 0x40, 0x3d, 0x62, 0x90, 0x0f, 0xbe, 0x25, 0x20, 0xd7, 0x25, 0x09, 0xca, 0xb5, + 0xcf, 0x25, 0x64, 0x42, 0x3b, 0x19, 0x09, 0x13, 0xf3, 0x46, 0x90, 0x72, 0xf7, 0x74, 0x4a, 0xd7, 0x00, 0x4b, 0x39, + 0x59, 0xcd, 0x21, 0x62, 0xe4, 0x78, 0x5d, 0x75, 0xb5, 0x80, 0x58, 0x0a, 0xb7, 0xa3, 0xed, 0xc8, 0xe4, 0x5c, 0xdc, + 0xa1, 0xf3, 0xce, 0x99, 0x5f, 0x18, 0xa7, 0x1c, 0x9c, 0x1e, 0xe6, 0x2e, 0x20, 0x20, 0xa7, 0xae, 0xfa, 0xc1, 0x19, + 0x19, 0xa4, 0xb8, 0x9a, 0x77, 0x5a, 0x24, 0x9a, 0x11, 0xf9, 0xac, 0x18, 0xaa, 0xdb, 0x2a, 0x37, 0xc2, 0x62, 0xad, + 0x5c, 0x82, 0x29, 0x72, 0x72, 0x1b, 0x7c, 0xb3, 0x83, 0xc7, 0xcd, 0x13, 0x16, 0xc0, 0x59, 0x8f, 0xe5, 0x62, 0xc2, + 0xa1, 0xea, 0x36, 0x7e, 0x0d, 0x64, 0x0a, 0xbc, 0x72, 0xd4, 0x59, 0x92, 0xe3, 0x0b, 0x0d, 0xaa, 0x81, 0xbf, 0xf6, + 0x91, 0xe7, 0x41, 0x6e, 0x50, 0x35, 0xd5, 0x34, 0x8b, 0x42, 0x4f, 0x31, 0xcf, 0x84, 0xcc, 0x5f, 0x35, 0x8a, 0x4e, + 0xc2, 0x8c, 0xa7, 0xc9, 0x96, 0x3a, 0xdd, 0xa7, 0x32, 0xa1, 0x80, 0xd8, 0x43, 0xe0, 0x14, 0xb8, 0xf7, 0xa6, 0xc2, + 0x3c, 0x9d, 0x92, 0x49, 0x1c, 0x9e, 0xcc, 0xb3, 0x59, 0x03, 0x66, 0xc0, 0x8d, 0x12, 0xe8, 0xe6, 0x8c, 0xfc, 0x70, + 0x0b, 0xb7, 0x55, 0x71, 0x1e, 0x93, 0x15, 0x68, 0xc9, 0x4d, 0x04, 0xc9, 0xf0, 0xca, 0xb8, 0x80, 0xd2, 0x7b, 0x13, + 0x67, 0xc6, 0xdd, 0xe2, 0xab, 0x8a, 0x4f, 0xc0, 0x79, 0xdc, 0x97, 0xdb, 0x8a, 0x53, 0x9a, 0x2a, 0x1c, 0x80, 0x92, + 0x17, 0xc4, 0x63, 0xe1, 0x9b, 0xd3, 0x2b, 0x99, 0x61, 0xe0, 0x62, 0x46, 0x35, 0x15, 0xdd, 0x85, 0x74, 0xc2, 0x74, + 0x90, 0xf0, 0x96, 0x34, 0x06, 0x77, 0x40, 0xf1, 0xbe, 0x00, 0x14, 0x11, 0x8e, 0xc2, 0x77, 0x76, 0x4c, 0x47, 0xab, + 0x92, 0xf0, 0x68, 0x99, 0x2d, 0xda, 0x79, 0xf9, 0x46, 0x25, 0xab, 0x1c, 0x70, 0x34, 0x00, 0x6c, 0x5e, 0x7f, 0x48, + 0x7c, 0x06, 0x81, 0x1c, 0x1f, 0x27, 0x76, 0x3e, 0x34, 0x4d, 0x95, 0xc2, 0x9f, 0x8d, 0xf6, 0x26, 0x2c, 0x70, 0xc7, + 0x29, 0x13, 0x3a, 0x1e, 0x1b, 0xd1, 0x0d, 0x41, 0xe7, 0x5f, 0xb0, 0x03, 0xb6, 0xda, 0x66, 0x7b, 0xbf, 0x7a, 0xbd, + 0x2c, 0x4e, 0x0e, 0x98, 0xe4, 0x9d, 0xcd, 0xbd, 0xb7, 0xdb, 0xf9, 0x2f, 0x27, 0x1f, 0x99, 0xb0, 0x40, 0xe1, 0x55, + 0x4e, 0x59, 0x64, 0x24, 0x3a, 0xa0, 0xc4, 0x2b, 0x4d, 0xe7, 0x62, 0x74, 0x2d, 0x12, 0xaf, 0x4a, 0xb1, 0x2b, 0x24, + 0xa9, 0x61, 0xe6, 0x0d, 0x38, 0xc8, 0x66, 0x9d, 0xa6, 0x46, 0x41, 0x11, 0xb2, 0xcc, 0xc5, 0xc6, 0x2c, 0xb1, 0x58, + 0xf3, 0x96, 0x33, 0x6d, 0x4e, 0x61, 0x44, 0xe0, 0xe4, 0x80, 0xa8, 0xfe, 0xac, 0xd6, 0xd8, 0xe0, 0xd6, 0xf3, 0x6a, + 0x18, 0x61, 0xe0, 0xdd, 0x50, 0x92, 0xf2, 0xc4, 0x18, 0x2b, 0x21, 0xc9, 0xa9, 0x23, 0x8e, 0xfd, 0xc8, 0xf2, 0x15, + 0xdf, 0xef, 0xb9, 0xc6, 0x14, 0x97, 0x07, 0x13, 0x63, 0x16, 0x43, 0xa6, 0x76, 0x83, 0xca, 0x22, 0xa9, 0x9f, 0x8e, + 0x6a, 0x59, 0x39, 0x93, 0x7b, 0x0c, 0xf7, 0x51, 0xe7, 0x92, 0xbc, 0x7b, 0x74, 0x05, 0x01, 0xd9, 0x5d, 0x82, 0xd5, + 0x27, 0x87, 0x24, 0x8a, 0x9c, 0xb0, 0x9f, 0xeb, 0x5f, 0x56, 0x23, 0x7f, 0x95, 0x63, 0x29, 0x5f, 0x0f, 0x79, 0x4b, + 0x19, 0x62, 0x2a, 0xad, 0xe5, 0x9e, 0x53, 0x90, 0x71, 0xe9, 0xb2, 0x6c, 0xf1, 0x40, 0x2c, 0x21, 0x7c, 0xb0, 0x98, + 0x7d, 0xde, 0x2e, 0x1c, 0xc8, 0xa4, 0x50, 0x7e, 0xdf, 0xe5, 0xf2, 0xfc, 0xf0, 0xc9, 0x46, 0x2d, 0xc6, 0x18, 0xa7, + 0x54, 0x5b, 0xdf, 0x38, 0xa8, 0x15, 0xa3, 0x0d, 0x3c, 0xbf, 0xb0, 0xdd, 0x6e, 0x6f, 0xd9, 0x2b, 0x20, 0x2b, 0x6b, + 0x2b, 0x70, 0x53, 0x27, 0x9d, 0x1f, 0x36, 0xc2, 0x49, 0x13, 0xca, 0x40, 0x7a, 0x39, 0x43, 0x2d, 0x90, 0x44, 0x37, + 0x45, 0x2d, 0x8c, 0x16, 0xeb, 0x5b, 0x8e, 0xbe, 0xf5, 0x6b, 0x18, 0x32, 0x4a, 0xe9, 0x98, 0xa6, 0xc3, 0xbd, 0x2e, + 0xd9, 0x77, 0x11, 0x44, 0x8b, 0x6c, 0xd6, 0x6c, 0xc3, 0x2f, 0x52, 0xae, 0xbd, 0x10, 0xde, 0x92, 0xe6, 0x28, 0xf2, + 0xce, 0x11, 0xa9, 0xa5, 0x70, 0xaa, 0xa9, 0xc7, 0x24, 0x1c, 0xbb, 0x04, 0x5a, 0x29, 0x98, 0xee, 0xe1, 0xc5, 0x4e, + 0xb6, 0xa1, 0xc2, 0x22, 0x2d, 0x04, 0x69, 0x14, 0x73, 0xf8, 0xfd, 0x20, 0x11, 0x59, 0x06, 0xd8, 0x79, 0xd4, 0xe7, + 0xc0, 0x1e, 0x0e, 0xe3, 0xd1, 0x55, 0x11, 0xfb, 0xee, 0x0f, 0x13, 0x14, 0x62, 0x9d, 0xa6, 0x09, 0x0a, 0xf7, 0x7c, + 0x84, 0x8e, 0xcd, 0x31, 0x3f, 0x9d, 0x85, 0xb6, 0x7d, 0xb3, 0x7a, 0x28, 0xbc, 0xfc, 0x2e, 0xc5, 0x3e, 0xa5, 0xb7, + 0xf3, 0x2f, 0xd3, 0x39, 0xc5, 0x3c, 0xb3, 0xeb, 0x14, 0x53, 0xa1, 0x89, 0xcd, 0x85, 0x97, 0x28, 0x12, 0xe7, 0xc3, + 0x4b, 0x69, 0xe0, 0xcd, 0xd0, 0x29, 0x91, 0x60, 0xdc, 0x08, 0x4f, 0x62, 0x14, 0x61, 0x0d, 0x98, 0xee, 0xe6, 0x3d, + 0x3b, 0xa9, 0x38, 0x77, 0xca, 0x92, 0x2b, 0xba, 0xfb, 0xb9, 0x41, 0x00, 0x40, 0xc5, 0xce, 0xe3, 0x0b, 0x9f, 0x2c, + 0xaf, 0xe5, 0xf4, 0x1c, 0x57, 0x86, 0x10, 0x5a, 0x1a, 0x71, 0x42, 0xb0, 0x91, 0x4a, 0x60, 0x5b, 0x61, 0xb9, 0x53, + 0x25, 0x4a, 0x06, 0x7d, 0x60, 0x8c, 0xec, 0x0e, 0x8a, 0x13, 0xd9, 0x10, 0xda, 0x9a, 0x8e, 0x08, 0x62, 0xe6, 0x7f, + 0x1f, 0xe4, 0x0c, 0xf0, 0xf8, 0xad, 0x64, 0x48, 0x85, 0x00, 0xcd, 0x1c, 0x2f, 0x2f, 0x03, 0x76, 0x31, 0xc4, 0x95, + 0xb2, 0xb8, 0x00, 0xc4, 0x66, 0x1f, 0x9b, 0x71, 0x90, 0xfb, 0x41, 0xea, 0x1c, 0xd6, 0x65, 0x07, 0xa2, 0xd2, 0x0b, + 0xe1, 0xf9, 0x35, 0x0d, 0xd5, 0xa4, 0x09, 0xe3, 0x75, 0xcc, 0x20, 0x66, 0x45, 0x69, 0x23, 0x05, 0x61, 0x95, 0x82, + 0x43, 0x60, 0x8e, 0x34, 0xcb, 0x84, 0xfa, 0xd2, 0x22, 0xc1, 0x1b, 0x0a, 0x01, 0xdb, 0xf1, 0x5a, 0xa2, 0x01, 0x1c, + 0x18, 0xb3, 0x61, 0x87, 0xa4, 0x95, 0x05, 0xf5, 0x40, 0xf9, 0x59, 0x5f, 0x78, 0x3c, 0x23, 0x99, 0xf0, 0xc1, 0x03, + 0x47, 0xf3, 0xa5, 0xa8, 0xe7, 0xe6, 0x44, 0x4b, 0xda, 0xe4, 0x10, 0x7f, 0x22, 0xaa, 0xb5, 0x68, 0xc5, 0x03, 0x5f, + 0x01, 0xa8, 0x90, 0xa6, 0x82, 0x4a, 0x64, 0x49, 0x59, 0x54, 0x64, 0x1e, 0x4c, 0xcc, 0xb5, 0x05, 0x56, 0xd6, 0xa8, + 0x77, 0xfd, 0x08, 0x7e, 0xa6, 0x84, 0x4a, 0xad, 0x3b, 0xc4, 0x3f, 0x65, 0xfc, 0x35, 0x84, 0x14, 0x99, 0x9f, 0x19, + 0xb9, 0xcb, 0x63, 0x9e, 0x3d, 0xb2, 0xfa, 0xb7, 0x7d, 0x1b, 0x8e, 0x2e, 0xdc, 0x63, 0x92, 0xab, 0xf7, 0x48, 0x64, + 0x64, 0x33, 0x01, 0x41, 0xb7, 0xf1, 0x7a, 0x38, 0x4a, 0xc5, 0x1f, 0x9b, 0x14, 0x6f, 0xab, 0x0a, 0xa2, 0xf6, 0xa2, + 0x85, 0x54, 0x93, 0x9e, 0x03, 0x69, 0xd0, 0x9c, 0x34, 0x6a, 0xd0, 0xb5, 0x07, 0x2a, 0x54, 0x04, 0xe5, 0x8f, 0x0e, + 0x27, 0x26, 0xca, 0xf0, 0x14, 0xee, 0x2f, 0x4c, 0x46, 0x98, 0x39, 0x02, 0x55, 0xed, 0xa2, 0xe5, 0xf3, 0x16, 0x63, + 0x02, 0x79, 0xef, 0xfe, 0x0d, 0xf3, 0x23, 0xaa, 0x61, 0xb3, 0xe5, 0xbf, 0xf9, 0x33, 0x4f, 0x29, 0x2a, 0x27, 0xc2, + 0x54, 0x48, 0xa8, 0xb1, 0xb3, 0xa4, 0xd0, 0xa3, 0x8b, 0x58, 0xc3, 0x2c, 0x3f, 0x59, 0x73, 0x75, 0x63, 0x08, 0x19, + 0x23, 0x10, 0x9c, 0x21, 0xc4, 0xa8, 0xf2, 0x44, 0x39, 0x78, 0x9b, 0x00, 0xb8, 0x04, 0xf5, 0x18, 0x2c, 0x73, 0xfb, + 0x12, 0xa1, 0xb9, 0x9c, 0xf7, 0x1f, 0x71, 0x29, 0x19, 0x29, 0x7e, 0x96, 0x97, 0x59, 0xf7, 0x52, 0x79, 0x2a, 0x6e, + 0x5d, 0xd1, 0x56, 0xdc, 0x06, 0x0f, 0x98, 0x82, 0x3e, 0xca, 0x4a, 0x6d, 0xf4, 0x69, 0x16, 0x35, 0xbb, 0x64, 0xdb, + 0x63, 0x77, 0x63, 0x79, 0x52, 0x70, 0x27, 0xbc, 0x84, 0x69, 0x19, 0x4a, 0xdd, 0x36, 0x5e, 0x8a, 0x7d, 0x38, 0xc7, + 0x79, 0xf9, 0x2d, 0x53, 0xbc, 0xff, 0x8e, 0xe2, 0xd3, 0xd7, 0x2c, 0xcd, 0x30, 0x3f, 0x3a, 0x2a, 0x94, 0xc0, 0xcc, + 0x7a, 0xac, 0xe6, 0x51, 0x12, 0x6b, 0x28, 0x40, 0x87, 0x05, 0x43, 0x7b, 0xbc, 0xde, 0x82, 0x78, 0xa8, 0xb2, 0x1e, + 0x2c, 0xbc, 0x27, 0x33, 0x74, 0xb5, 0xa4, 0x0d, 0xad, 0x6b, 0xa2, 0xa2, 0x4f, 0xef, 0x91, 0xc5, 0xdd, 0xfa, 0x38, + 0x4e, 0x9a, 0x18, 0x15, 0x97, 0xa2, 0xdd, 0x59, 0x37, 0x5e, 0x84, 0x99, 0x78, 0x8c, 0x9c, 0x11, 0x19, 0x6b, 0xe9, + 0x8c, 0x96, 0xdc, 0xba, 0x04, 0x99, 0x4f, 0x06, 0x7a, 0x27, 0x50, 0x7e, 0xfe, 0x28, 0x07, 0x8e, 0x08, 0x00, 0xb5, + 0xdb, 0x16, 0x84, 0x2c, 0x38, 0xf0, 0xbf, 0x2c, 0xb7, 0x7d, 0x9f, 0xbc, 0xb3, 0x7c, 0x31, 0x2b, 0xe0, 0x7c, 0x63, + 0xa3, 0xbd, 0x11, 0xb7, 0xb3, 0x91, 0x0d, 0x1d, 0x4f, 0x24, 0xd4, 0x1f, 0x66, 0xe6, 0xd9, 0x31, 0x1f, 0x18, 0x09, + 0xce, 0x46, 0x47, 0x60, 0xbb, 0x2a, 0x73, 0xfd, 0x37, 0x49, 0xde, 0x59, 0x12, 0x23, 0x5c, 0x51, 0x81, 0xac, 0x1e, + 0x64, 0x41, 0xb0, 0xb8, 0xa5, 0x2b, 0xba, 0xde, 0xe7, 0x15, 0x89, 0x36, 0x91, 0x29, 0xb9, 0x1e, 0x20, 0xa0, 0xab, + 0x57, 0x3d, 0x3e, 0x55, 0xf7, 0xc6, 0xfb, 0x52, 0xe3, 0x85, 0xf6, 0xf7, 0xb5, 0x9d, 0x3a, 0xdc, 0xbc, 0xe7, 0x7a, + 0x9e, 0xf6, 0x8f, 0x12, 0x75, 0x7a, 0x2d, 0x32, 0x9e, 0x87, 0xfb, 0x35, 0x94, 0xd3, 0x48, 0x35, 0x69, 0x25, 0xa1, + 0xb8, 0x11, 0xcb, 0xbc, 0xe3, 0x96, 0x07, 0xbc, 0x0a, 0xbf, 0x69, 0xb5, 0x10, 0xef, 0x7d, 0x64, 0x58, 0x9e, 0x7a, + 0x77, 0x34, 0x61, 0xf6, 0x50, 0x2b, 0xbb, 0x29, 0x26, 0x60, 0x3f, 0xad, 0xfe, 0xc9, 0xae, 0xca, 0x17, 0x17, 0xf3, + 0x3f, 0xbb, 0xae, 0xb2, 0x91, 0xf1, 0x64, 0x9a, 0xf5, 0xdd, 0xf3, 0xf9, 0x87, 0x3f, 0x7b, 0x1a, 0x4e, 0xe6, 0xc3, + 0x2c, 0xda, 0x7f, 0x29, 0xbf, 0x2c, 0x1a, 0x42, 0x5b, 0xfe, 0xe8, 0x2c, 0xf5, 0xcc, 0x42, 0xef, 0x85, 0x00, 0x3e, + 0x2d, 0xda, 0x1d, 0x1c, 0xd5, 0x74, 0x3d, 0x8c, 0xf7, 0x41, 0x0b, 0xb7, 0x2e, 0x77, 0x20, 0xce, 0x6c, 0xc4, 0x49, + 0x7e, 0x51, 0xb3, 0xeb, 0x5d, 0x36, 0xb3, 0x69, 0x97, 0xbf, 0x23, 0x08, 0x9f, 0x4d, 0x90, 0xd1, 0x3a, 0xd6, 0x36, + 0xa9, 0xbc, 0x0a, 0x2c, 0xff, 0x8d, 0xe4, 0xd3, 0xb9, 0x36, 0x8c, 0x6e, 0x05, 0xfb, 0xf9, 0xa7, 0xf0, 0x6d, 0xd5, + 0x77, 0xc7, 0x9d, 0xff, 0x7c, 0xba, 0x5f, 0xfd, 0x3b, 0xd5, 0x93, 0xb9, 0x59, 0xf4, 0xde, 0xf3, 0xe0, 0xfb, 0xd3, + 0xa0, 0xdb, 0xb6, 0x7a, 0xb2, 0x7d, 0xfc, 0x7f, 0x17, 0xef, 0xff, 0x27, 0xfa, 0xfa, 0x15, 0x7b, 0x64, 0x3e, 0xf4, + 0xe2, 0xf8, 0x6c, 0xea, 0xe2, 0xf2, 0xff, 0x5c, 0xab, 0xdb, 0x3b, 0xcf, 0x6a, 0xb4, 0x6b, 0x6e, 0xb9, 0xee, 0xb5, + 0xed, 0x32, 0xaa, 0x9c, 0xc0, 0xad, 0xff, 0x7a, 0xea, 0x2b, 0x08, 0xf2, 0x79, 0xe3, 0xe5, 0x7e, 0xf6, 0xf0, 0xb6, + 0x26, 0xb3, 0xcf, 0x96, 0x7b, 0x27, 0x2f, 0xfc, 0xbc, 0x1e, 0x6c, 0xfb, 0x54, 0xea, 0x28, 0xd5, 0xac, 0xf8, 0x61, + 0x4d, 0x72, 0xb6, 0x2b, 0xe7, 0xfc, 0xd3, 0xf2, 0x79, 0xfd, 0xff, 0xc5, 0xf3, 0x6f, 0x76, 0xba, 0x47, 0xd8, 0x6f, + 0x61, 0x5f, 0x63, 0x3f, 0xab, 0x4f, 0xe6, 0x70, 0xf5, 0x29, 0xde, 0xba, 0xee, 0x6d, 0xb5, 0x6b, 0xee, 0xe2, 0x8a, + 0x39, 0x75, 0xc9, 0x95, 0xb3, 0x7a, 0x2a, 0x88, 0x0b, 0x62, 0x11, 0x48, 0x7c, 0x57, 0xff, 0x1d, 0xdc, 0xd5, 0x43, + 0xef, 0x95, 0x8b, 0x6d, 0xcf, 0x6d, 0x02, 0xec, 0x9a, 0xc8, 0x8c, 0x11, 0x2b, 0x62, 0x1b, 0xed, 0x28, 0xf0, 0x01, + 0x8b, 0xb6, 0xcf, 0x9f, 0x12, 0x9f, 0xf5, 0xbb, 0x90, 0x6b, 0x70, 0x7d, 0x7f, 0xfd, 0x5a, 0x3c, 0x93, 0x3f, 0x08, + 0x29, 0xfa, 0xf9, 0xc0, 0x53, 0xfd, 0x8b, 0x54, 0x98, 0x42, 0x54, 0x27, 0x2c, 0x6b, 0x86, 0x7d, 0x65, 0xdf, 0x47, + 0x8b, 0x43, 0x05, 0x6a, 0x3d, 0xde, 0x27, 0x44, 0x3e, 0xaa, 0x48, 0x77, 0xf9, 0x77, 0x2a, 0x82, 0xc0, 0x88, 0x28, + 0x30, 0x34, 0xc8, 0xa7, 0x9d, 0x03, 0x7f, 0x81, 0xe5, 0xfe, 0xa2, 0xb8, 0x7a, 0xb7, 0x4b, 0x4a, 0x35, 0x5c, 0xcd, + 0x1b, 0xeb, 0x14, 0x20, 0x25, 0x36, 0x11, 0x01, 0xc3, 0x7f, 0x72, 0xe5, 0x0b, 0xb6, 0x5e, 0xe7, 0x69, 0x3e, 0x19, + 0x55, 0xa7, 0x26, 0x37, 0x5b, 0x0b, 0x23, 0x5d, 0x0b, 0xaf, 0x3a, 0x3a, 0xe1, 0x94, 0x63, 0xcc, 0x57, 0x94, 0x36, + 0x1e, 0x71, 0xa7, 0xfe, 0xf6, 0x2a, 0xfe, 0xdc, 0x80, 0x84, 0x72, 0xcb, 0x6c, 0x70, 0x95, 0x99, 0xbd, 0x56, 0x70, + 0xa3, 0x24, 0x4c, 0xf1, 0x12, 0xf2, 0x8c, 0xdf, 0x73, 0xb1, 0x32, 0x05, 0xb6, 0x69, 0x7a, 0xff, 0xc7, 0xf6, 0x24, + 0x28, 0x04, 0x3a, 0xf1, 0x52, 0x80, 0x04, 0xaa, 0xbe, 0x21, 0xc8, 0x37, 0x3d, 0xe2, 0xa0, 0xad, 0xa9, 0x8d, 0xf3, + 0x4c, 0xb3, 0x68, 0x33, 0xde, 0xd5, 0x95, 0x92, 0xb9, 0x9e, 0x11, 0x8d, 0xbc, 0xb6, 0xed, 0xf5, 0x66, 0xcc, 0xec, + 0x7a, 0x38, 0x07, 0x74, 0xbe, 0x0e, 0xa0, 0x9f, 0x55, 0x07, 0x96, 0x2a, 0x4e, 0x7f, 0xb9, 0x03, 0x32, 0xb0, 0xa4, + 0x43, 0x0f, 0x53, 0x0a, 0x9f, 0xa5, 0xf4, 0x1f, 0x01, 0x3b, 0x05, 0x0e, 0x4b, 0x39, 0x16, 0xd9, 0x8d, 0xd9, 0xcc, + 0xda, 0xd6, 0xe4, 0xa4, 0x33, 0x17, 0x51, 0xf6, 0x7c, 0x6d, 0x57, 0xff, 0x5d, 0xac, 0xfa, 0x78, 0xc9, 0xc6, 0x29, + 0x20, 0x05, 0x05, 0x05, 0xb1, 0xc7, 0xf2, 0xf3, 0xd0, 0x2f, 0x19, 0x91, 0x41, 0x34, 0xbd, 0x1a, 0x74, 0xfa, 0x79, + 0xd2, 0x5f, 0x58, 0x94, 0xd4, 0x4a, 0xe8, 0x0f, 0xb8, 0xe1, 0x03, 0x77, 0xe7, 0x8c, 0x38, 0x37, 0x84, 0xfc, 0x34, + 0xf5, 0x23, 0xe6, 0x6e, 0xe6, 0x64, 0x16, 0x3f, 0x07, 0x72, 0xd2, 0x82, 0xd5, 0xf8, 0x13, 0x36, 0x82, 0xdc, 0x37, + 0x7e, 0x69, 0xa9, 0x02, 0x47, 0x97, 0x2b, 0xe9, 0xe9, 0xd2, 0xc9, 0x62, 0xa1, 0x75, 0x70, 0x77, 0x8a, 0xe0, 0x8b, + 0xb7, 0xc2, 0x3a, 0xff, 0xe5, 0x72, 0xeb, 0xae, 0x38, 0x1b, 0x36, 0xa2, 0x7e, 0x76, 0xbf, 0xba, 0x75, 0x1a, 0xbe, + 0x47, 0xbf, 0x8b, 0x57, 0xdf, 0xf5, 0x78, 0x21, 0x47, 0xb7, 0x6e, 0x0d, 0x56, 0xf3, 0x74, 0x05, 0x45, 0x33, 0xb0, + 0x2f, 0x5e, 0x94, 0x83, 0x2f, 0x60, 0x34, 0xee, 0x78, 0x11, 0x6c, 0x0a, 0xed, 0xf3, 0xce, 0xf3, 0xe5, 0x15, 0x55, + 0x93, 0x85, 0xf4, 0x76, 0xcd, 0xc6, 0xf0, 0xfe, 0xba, 0xbd, 0xf9, 0x59, 0xfc, 0x54, 0x7c, 0x85, 0x46, 0xe8, 0xb7, + 0x0b, 0x40, 0xba, 0xfa, 0x92, 0x15, 0x8f, 0x3e, 0x6f, 0x85, 0x58, 0xcd, 0xf7, 0x8e, 0x67, 0xb1, 0x42, 0xe0, 0xe3, + 0x5b, 0xd1, 0xeb, 0x25, 0x3c, 0xb3, 0x9a, 0x75, 0x82, 0xb9, 0x03, 0x88, 0x50, 0x44, 0x1a, 0x34, 0x11, 0xe4, 0xbb, + 0xb3, 0x61, 0x2e, 0x54, 0x67, 0x35, 0x2f, 0xff, 0xbc, 0xbc, 0xa2, 0x1e, 0x51, 0x2f, 0x7e, 0x73, 0xbd, 0x81, 0x44, + 0xab, 0xae, 0x85, 0x1a, 0xbf, 0x4b, 0x8d, 0x53, 0xe6, 0x41, 0x03, 0x24, 0x69, 0xe8, 0x50, 0xcb, 0xfb, 0x10, 0x8f, + 0x8b, 0xed, 0x6b, 0x34, 0x7e, 0xdf, 0xc4, 0x5a, 0x11, 0x15, 0x79, 0x4f, 0x81, 0x2c, 0x0e, 0x94, 0x50, 0x5a, 0x5e, + 0xc8, 0xdf, 0x15, 0xa6, 0x5a, 0x64, 0xce, 0xc3, 0xc4, 0x59, 0xa0, 0xfe, 0x7a, 0xf4, 0xd4, 0xfb, 0x52, 0x07, 0x7c, + 0x63, 0x61, 0x03, 0xd7, 0x73, 0x43, 0x8d, 0x60, 0x1c, 0xa4, 0x48, 0x35, 0xa8, 0x0d, 0x33, 0x6a, 0x30, 0x79, 0x4d, + 0xc7, 0x96, 0x62, 0xaf, 0xa4, 0x3f, 0xe4, 0x99, 0x2e, 0xac, 0xf2, 0x6d, 0xd5, 0x23, 0x3b, 0xbd, 0x8d, 0x0b, 0xfe, + 0x59, 0xf3, 0xde, 0x7c, 0x8d, 0x78, 0xba, 0x6c, 0x48, 0xb0, 0xa4, 0xe7, 0xc9, 0x71, 0xeb, 0xfc, 0xa2, 0xba, 0x9e, + 0xab, 0x78, 0x04, 0x99, 0x12, 0x2e, 0x09, 0xb9, 0x8e, 0x73, 0xbd, 0x97, 0x30, 0x10, 0x21, 0x5f, 0xd7, 0x67, 0x09, + 0x50, 0xda, 0xd6, 0x97, 0x50, 0x0b, 0x1f, 0x4a, 0xb3, 0xaf, 0xdd, 0x12, 0x8e, 0xcc, 0x7d, 0x57, 0xec, 0x5d, 0x1d, + 0x39, 0x5d, 0x38, 0x24, 0x15, 0xa0, 0xef, 0xb9, 0xe8, 0xf0, 0x8d, 0xb1, 0xb0, 0x48, 0xc4, 0xa6, 0x37, 0xd1, 0x8d, + 0x66, 0xdd, 0xe0, 0x57, 0x27, 0x9d, 0x06, 0x5f, 0x1b, 0x38, 0x15, 0xb1, 0xa6, 0x00, 0x3b, 0xf4, 0xb8, 0xb1, 0x3b, + 0x40, 0x88, 0x6f, 0x5a, 0xed, 0x84, 0xce, 0xd6, 0x8d, 0x23, 0x14, 0x04, 0x03, 0xea, 0x45, 0xd4, 0x8a, 0xf2, 0xfb, + 0x4a, 0x27, 0x11, 0xf5, 0x06, 0xbf, 0x7b, 0x7d, 0xef, 0xe5, 0xa9, 0x2a, 0xbe, 0xde, 0xab, 0x07, 0x7a, 0xb2, 0x1d, + 0x90, 0xd8, 0x34, 0x15, 0x6b, 0x40, 0x93, 0x67, 0x4e, 0x81, 0xb8, 0xc1, 0x3c, 0xf7, 0x61, 0x3f, 0xc6, 0x7c, 0x8d, + 0x40, 0x8d, 0x4e, 0x84, 0x6a, 0x49, 0xa4, 0x2f, 0x57, 0x2a, 0x63, 0xdd, 0x2c, 0xe2, 0xd9, 0x45, 0x93, 0xf7, 0x7f, + 0x6f, 0x1c, 0x5c, 0xd7, 0x54, 0x50, 0x6e, 0xda, 0x33, 0xff, 0xeb, 0x9a, 0xc2, 0x06, 0xc0, 0x03, 0x7c, 0x5d, 0x42, + 0x72, 0x65, 0xc0, 0x87, 0x6f, 0x0a, 0x75, 0x5e, 0xdd, 0xe6, 0x2d, 0x64, 0x65, 0x40, 0xe4, 0xb4, 0x7d, 0x80, 0xf0, + 0x36, 0x60, 0x0c, 0x23, 0x2e, 0x40, 0xf4, 0xd1, 0x31, 0xdd, 0xaa, 0x69, 0x20, 0xee, 0xca, 0x56, 0x1f, 0xcf, 0x04, + 0xbc, 0x12, 0x7e, 0x63, 0x18, 0xc7, 0x10, 0xe2, 0x33, 0x8e, 0xed, 0xf1, 0x25, 0x54, 0x0e, 0x34, 0xca, 0x83, 0x55, + 0x79, 0xfc, 0x39, 0xf7, 0xfe, 0xea, 0xab, 0x66, 0x64, 0xd3, 0x80, 0xb9, 0xd1, 0xb4, 0xa1, 0x63, 0xc5, 0xba, 0xe4, + 0x6f, 0xbd, 0x43, 0x63, 0xb0, 0x2f, 0x5b, 0x5f, 0xc2, 0xfd, 0x4e, 0x46, 0xc3, 0x0d, 0x8c, 0xc0, 0x18, 0x0c, 0x54, + 0x95, 0x52, 0x90, 0xfe, 0xe2, 0xa5, 0x9d, 0x8b, 0x92, 0xf7, 0xa4, 0x93, 0xbd, 0x11, 0xca, 0x83, 0x42, 0x8b, 0x81, + 0x8b, 0x7e, 0x53, 0x6b, 0x25, 0xee, 0x63, 0xf9, 0xae, 0x8f, 0xd6, 0x54, 0xba, 0x99, 0x11, 0xd9, 0x52, 0x87, 0x51, + 0xdc, 0x9c, 0x3b, 0x69, 0xe6, 0xa1, 0x53, 0x60, 0x91, 0xe6, 0x66, 0x79, 0x00, 0xe2, 0x5b, 0xd4, 0xc8, 0xe6, 0x94, + 0xff, 0x89, 0x47, 0xdd, 0x40, 0x88, 0xc8, 0xb2, 0xbe, 0x6b, 0x8a, 0x33, 0x28, 0x94, 0xe4, 0x06, 0x85, 0xf0, 0xde, + 0xc8, 0xa0, 0x40, 0xc9, 0x52, 0xd9, 0x48, 0xfa, 0xfd, 0x27, 0x1e, 0x54, 0xe8, 0xe9, 0xce, 0x91, 0x64, 0xeb, 0x36, + 0x0b, 0x6b, 0x28, 0x8d, 0x32, 0x31, 0xbb, 0xd9, 0xc9, 0xb7, 0x05, 0x05, 0x45, 0x49, 0x39, 0x51, 0xa4, 0x19, 0x0e, + 0x77, 0xfa, 0x5f, 0xee, 0x51, 0xde, 0xb1, 0x40, 0xb9, 0xcd, 0x9c, 0x96, 0x00, 0x01, 0x44, 0xfd, 0x5c, 0x40, 0x34, + 0x51, 0xa4, 0x14, 0x72, 0x79, 0x23, 0x2f, 0xf3, 0xd1, 0xad, 0x79, 0xca, 0x41, 0xfb, 0xca, 0xfe, 0x94, 0x30, 0xe7, + 0xb6, 0x92, 0x3e, 0x92, 0x31, 0x31, 0x52, 0x17, 0xdc, 0xd0, 0x81, 0x61, 0xbd, 0x77, 0xe8, 0xe5, 0x53, 0x63, 0xc2, + 0xef, 0x2f, 0x82, 0x22, 0x88, 0x42, 0x00, 0x80, 0x69, 0x59, 0xb6, 0xa4, 0xf0, 0x49, 0x12, 0x05, 0x90, 0xf5, 0xb8, + 0xf4, 0xc0, 0xb5, 0x24, 0x30, 0x3c, 0xaa, 0x09, 0x68, 0xde, 0x2e, 0x50, 0x38, 0xa0, 0x85, 0x95, 0xeb, 0xb0, 0x76, + 0x42, 0xaa, 0x26, 0x45, 0xab, 0x9b, 0xd5, 0x92, 0x94, 0x67, 0x06, 0x4c, 0x15, 0x91, 0xa7, 0xf5, 0x3f, 0x64, 0xbe, + 0xb4, 0x40, 0xf4, 0xc6, 0x7c, 0x16, 0x5c, 0x3f, 0x56, 0x3b, 0x8e, 0x5e, 0x37, 0x4c, 0x6b, 0x37, 0x48, 0x02, 0x44, + 0x3e, 0x95, 0xd5, 0xd5, 0x2b, 0x15, 0xa4, 0xa1, 0xc6, 0x8f, 0x7c, 0xaf, 0x14, 0xe4, 0x4a, 0xe9, 0xa5, 0xa0, 0x00, + 0xdd, 0x78, 0xe9, 0x88, 0xa7, 0x6c, 0xe9, 0xc5, 0xa6, 0xb0, 0x71, 0xc2, 0xd8, 0xeb, 0xd9, 0x8a, 0x93, 0x7a, 0xec, + 0xaa, 0x4e, 0xb2, 0x04, 0x5d, 0x5c, 0x4b, 0x5e, 0xc5, 0x91, 0xe9, 0xd2, 0xd4, 0x31, 0xf5, 0xef, 0x1a, 0xed, 0x89, + 0x15, 0xba, 0xff, 0x2d, 0x91, 0x3b, 0xaf, 0x4c, 0xd3, 0x02, 0x41, 0xd6, 0x82, 0x90, 0xe0, 0x7c, 0x27, 0x44, 0x1e, + 0x95, 0xc7, 0xa4, 0x65, 0xee, 0xf1, 0xb5, 0x6e, 0xc6, 0x53, 0xda, 0x03, 0x51, 0x3e, 0xcc, 0x71, 0x97, 0x12, 0xe6, + 0x9e, 0x3d, 0xb0, 0x32, 0x4c, 0x4c, 0xec, 0x83, 0x1e, 0x3d, 0xae, 0x59, 0x01, 0xc1, 0x30, 0xfd, 0xda, 0xa5, 0xdd, + 0xed, 0xfa, 0x61, 0x0b, 0xe0, 0x5d, 0x2e, 0x84, 0xfa, 0xb9, 0x3a, 0x71, 0x53, 0x78, 0x75, 0x83, 0xb6, 0x8a, 0xd5, + 0x1a, 0x54, 0xb4, 0xdc, 0xa1, 0x6d, 0xeb, 0xcf, 0x69, 0x06, 0x1f, 0x3b, 0x39, 0x10, 0x6a, 0x3a, 0x22, 0x98, 0x89, + 0x72, 0x24, 0x0d, 0x9e, 0xb8, 0xea, 0x6c, 0x91, 0xaa, 0x77, 0x73, 0x02, 0x64, 0x48, 0xea, 0x0b, 0x32, 0x84, 0x36, + 0x20, 0x74, 0xac, 0xa9, 0xf2, 0xb5, 0x41, 0xed, 0xd6, 0x13, 0x63, 0x6f, 0xdf, 0x84, 0x16, 0x45, 0x85, 0xbe, 0x2d, + 0x16, 0xbb, 0x64, 0x8c, 0xbe, 0xe0, 0x6f, 0xcc, 0x7e, 0x92, 0xd1, 0xc3, 0x67, 0xb5, 0xd1, 0x45, 0x3b, 0x88, 0x5d, + 0x60, 0xfc, 0xa3, 0xc9, 0xdb, 0x9a, 0x55, 0x5c, 0x7e, 0x75, 0x41, 0x55, 0xab, 0xd9, 0x62, 0xfa, 0xb3, 0xae, 0x70, + 0x89, 0x64, 0xa2, 0xc4, 0x0c, 0x7f, 0x10, 0x68, 0xf5, 0x7d, 0xe0, 0xdc, 0xe7, 0xb9, 0x9e, 0xa0, 0xff, 0xe2, 0xa5, + 0x77, 0x28, 0xa3, 0x42, 0xc8, 0xc7, 0x91, 0x2f, 0xa4, 0x88, 0x55, 0xba, 0x7b, 0xb4, 0xd1, 0x12, 0x39, 0x0b, 0x64, + 0xcd, 0x6a, 0xca, 0x34, 0xd4, 0x85, 0x63, 0x8b, 0x2e, 0xd3, 0x6c, 0x17, 0xd0, 0x32, 0xac, 0xa4, 0x63, 0xeb, 0x9d, + 0x05, 0x84, 0x22, 0x22, 0x80, 0x29, 0x69, 0xf0, 0x9f, 0x5d, 0x69, 0x8b, 0xc5, 0xdc, 0xb4, 0x94, 0x7d, 0x2e, 0x23, + 0x31, 0xd7, 0x13, 0xb2, 0x1b, 0xb8, 0x5e, 0xdc, 0x08, 0x4d, 0x6b, 0x84, 0xe4, 0x24, 0xd1, 0xa3, 0x5e, 0xa8, 0x37, + 0x55, 0x59, 0x34, 0x7a, 0x40, 0x54, 0x03, 0xd7, 0xbb, 0x69, 0x27, 0x52, 0x12, 0x2c, 0x15, 0xdd, 0x07, 0x6e, 0xdd, + 0xad, 0x75, 0x98, 0x70, 0x85, 0x9c, 0x97, 0x50, 0x23, 0x86, 0x84, 0xfb, 0x80, 0x3d, 0x94, 0x4c, 0x00, 0x98, 0x82, + 0x13, 0x68, 0x09, 0xb0, 0xed, 0x56, 0x50, 0x02, 0x06, 0xac, 0xcc, 0x34, 0xa2, 0x30, 0xf3, 0xd0, 0x15, 0x26, 0xe4, + 0x38, 0x37, 0x8f, 0x3a, 0x58, 0x90, 0x2a, 0x44, 0xdb, 0xef, 0x4d, 0x0f, 0xe6, 0x38, 0x33, 0xce, 0x91, 0x0b, 0x80, + 0xe3, 0x2d, 0x28, 0xd5, 0x30, 0x34, 0x6c, 0xff, 0xaa, 0xc9, 0x2a, 0x67, 0x44, 0x62, 0xd5, 0x2b, 0x9b, 0xfd, 0x2a, + 0xfe, 0x18, 0x0a, 0x2a, 0x69, 0x3a, 0xbe, 0x49, 0x4c, 0x3d, 0x5b, 0x5e, 0x7d, 0x65, 0x78, 0xd2, 0xb3, 0x7d, 0xc0, + 0x15, 0x8f, 0xc0, 0xba, 0x29, 0xf9, 0x51, 0x27, 0x83, 0x06, 0xe0, 0xa8, 0x45, 0x3b, 0x54, 0x9d, 0x62, 0x10, 0x30, + 0xe2, 0x74, 0x5a, 0x96, 0xfc, 0x25, 0x8a, 0x0d, 0x34, 0xf1, 0x18, 0x2f, 0x58, 0x3a, 0xb1, 0xbd, 0xa3, 0xf9, 0xaa, + 0x44, 0x23, 0xcb, 0xac, 0x0d, 0x92, 0xfc, 0x36, 0xbd, 0xd6, 0x2a, 0x23, 0xed, 0x6d, 0xd9, 0x21, 0xfe, 0x11, 0xc8, + 0x82, 0x31, 0xa3, 0x22, 0x51, 0x31, 0x45, 0xc6, 0xa5, 0xf1, 0x56, 0xb2, 0x00, 0x5d, 0xa6, 0x67, 0x6b, 0xf3, 0x8a, + 0xbc, 0x7d, 0x12, 0xcd, 0x7d, 0x30, 0x55, 0x61, 0xff, 0x72, 0x34, 0x5b, 0x1e, 0xab, 0xf0, 0x8f, 0x55, 0x75, 0x04, + 0x9a, 0xb6, 0xab, 0xa7, 0x40, 0x8e, 0x4e, 0xd5, 0xc5, 0x21, 0x39, 0xf6, 0xc2, 0x8c, 0x43, 0x12, 0x72, 0xb2, 0x78, + 0x1b, 0xac, 0x4f, 0x32, 0xb4, 0x46, 0x80, 0x2f, 0x17, 0x61, 0xd5, 0x2b, 0xcd, 0x1e, 0x65, 0xb2, 0x1a, 0x59, 0x2b, + 0x28, 0x4d, 0x10, 0x45, 0xf3, 0x14, 0x09, 0x03, 0xcf, 0x72, 0xa2, 0x30, 0x61, 0x38, 0x25, 0xec, 0x43, 0xa2, 0x8b, + 0xf6, 0x0f, 0x33, 0xcb, 0x87, 0x12, 0xb0, 0xa5, 0x79, 0x12, 0x20, 0x46, 0x80, 0x51, 0xa5, 0x58, 0xd1, 0x3f, 0x38, + 0x4f, 0x1c, 0x0f, 0x73, 0x49, 0x22, 0x3f, 0xe3, 0xfd, 0x91, 0x79, 0xd3, 0xcd, 0xcb, 0x23, 0xdb, 0x90, 0x26, 0x66, + 0xaa, 0xa7, 0x70, 0x8d, 0xd8, 0x6e, 0xbb, 0x80, 0x2d, 0x54, 0xba, 0x41, 0xb5, 0x2f, 0x8a, 0x20, 0xf4, 0x2f, 0x75, + 0x90, 0xd6, 0xfc, 0x37, 0x47, 0x1b, 0x4c, 0x8c, 0xde, 0x64, 0x07, 0x8c, 0xfb, 0x66, 0xaa, 0xba, 0x96, 0x40, 0xc7, + 0xa6, 0x2a, 0xfc, 0x76, 0x70, 0x09, 0x89, 0xb9, 0x32, 0x16, 0xbd, 0xd5, 0x19, 0x59, 0xe5, 0xfe, 0xdf, 0x36, 0x1d, + 0x41, 0xb7, 0x7f, 0x9d, 0x5d, 0xcd, 0xce, 0x03, 0x64, 0x91, 0x07, 0x8e, 0x88, 0xa5, 0x7a, 0x6a, 0xf3, 0x68, 0x58, + 0x58, 0xaa, 0x2b, 0xc7, 0xfb, 0xb8, 0x92, 0x36, 0x9f, 0x97, 0x86, 0x03, 0x22, 0x72, 0x30, 0xbd, 0x35, 0xf0, 0x5b, + 0x24, 0x32, 0xaf, 0x6a, 0x1c, 0xd1, 0xa9, 0x8b, 0x71, 0x31, 0xae, 0x15, 0x94, 0x46, 0x7e, 0xdc, 0x49, 0x3f, 0x46, + 0x47, 0x4b, 0x1f, 0x9f, 0x6e, 0xad, 0x8a, 0xee, 0xd5, 0x2f, 0x72, 0x28, 0xe6, 0x65, 0x19, 0x1d, 0x08, 0x19, 0x24, + 0x7b, 0x4f, 0xbe, 0xf3, 0x9e, 0xb8, 0xcc, 0x45, 0x4f, 0x8d, 0x0a, 0x0e, 0xbd, 0xbd, 0x8d, 0x2c, 0x53, 0x39, 0x72, + 0x07, 0xcc, 0xce, 0xf8, 0xda, 0xde, 0x40, 0x6c, 0xef, 0x85, 0xc8, 0xad, 0xf0, 0x48, 0x61, 0xfa, 0x71, 0x65, 0x84, + 0xab, 0x31, 0xe9, 0x50, 0x99, 0x4c, 0xf3, 0xc2, 0x2e, 0x57, 0x59, 0xd0, 0x61, 0x19, 0x54, 0x33, 0x99, 0x99, 0x66, + 0xb2, 0x69, 0xa4, 0xe1, 0x0a, 0xc5, 0x34, 0x06, 0x2e, 0x97, 0x2a, 0x52, 0xf6, 0xbc, 0x92, 0xa5, 0xe7, 0x38, 0x0b, + 0x1d, 0xa6, 0x4d, 0x07, 0xcf, 0x53, 0xe2, 0x92, 0x70, 0x84, 0x35, 0x13, 0x4c, 0x93, 0xac, 0xb4, 0x40, 0xb9, 0xa8, + 0xa4, 0x18, 0xba, 0x3e, 0xaf, 0x24, 0x65, 0xee, 0x68, 0x19, 0x4f, 0x69, 0xf4, 0x8c, 0xf2, 0x15, 0xb5, 0x66, 0xfe, + 0xc9, 0xf2, 0xef, 0x20, 0x85, 0xd6, 0x57, 0x40, 0x05, 0xa6, 0x14, 0xac, 0x04, 0xf9, 0xfb, 0xc5, 0x8d, 0x56, 0x11, + 0x97, 0x82, 0xf3, 0x2a, 0xe6, 0x65, 0x53, 0x0d, 0x69, 0xbe, 0xfe, 0xe4, 0x7f, 0xa6, 0x93, 0x83, 0x4a, 0x1c, 0x6e, + 0x00, 0x33, 0x86, 0x5c, 0x2c, 0xe8, 0x4f, 0xa5, 0x57, 0x5f, 0xa9, 0x97, 0xa2, 0x46, 0x5d, 0xe8, 0xee, 0x96, 0xdc, + 0x5a, 0xcf, 0x46, 0x9a, 0x68, 0x56, 0x2a, 0xdf, 0x0f, 0x92, 0x66, 0x86, 0x1a, 0xe1, 0x62, 0x2f, 0x36, 0x60, 0xdc, + 0x1a, 0xa7, 0x50, 0x7b, 0x2f, 0x59, 0xc2, 0x67, 0x8b, 0xcb, 0x41, 0x95, 0xc2, 0x18, 0xdf, 0x81, 0xb9, 0x21, 0xf7, + 0xc1, 0x93, 0xde, 0x7e, 0xbb, 0xf3, 0x53, 0xbc, 0x0c, 0xec, 0x12, 0x11, 0x0f, 0xa2, 0xdf, 0xdc, 0x2a, 0x6d, 0xaf, + 0x37, 0x16, 0x36, 0x57, 0xc5, 0x0f, 0x2a, 0x55, 0xe0, 0xce, 0x2b, 0x77, 0x61, 0x50, 0x1e, 0x41, 0x0e, 0xfa, 0x4d, + 0xe3, 0xe6, 0x7e, 0x27, 0x54, 0x61, 0xd8, 0xa5, 0x87, 0x49, 0x59, 0xe7, 0x4b, 0x7a, 0x18, 0x33, 0xc4, 0xce, 0xcc, + 0x32, 0xa9, 0xd0, 0xae, 0x65, 0x41, 0xe3, 0xa7, 0xe0, 0x8f, 0x28, 0xa3, 0x48, 0x2b, 0x26, 0xb0, 0x0f, 0x32, 0x01, + 0xc7, 0x07, 0xc1, 0xa8, 0x2e, 0xe2, 0x13, 0x9c, 0xee, 0xce, 0x0b, 0x0e, 0x54, 0x32, 0xb4, 0x48, 0xb0, 0xc4, 0x1e, + 0xf1, 0xb0, 0xa9, 0x1f, 0xec, 0x9d, 0xda, 0x55, 0x38, 0x6f, 0x16, 0xeb, 0x31, 0x48, 0xf5, 0xfc, 0xb6, 0xf9, 0x84, + 0x03, 0xfc, 0x51, 0x9d, 0xea, 0xf1, 0x4d, 0x1d, 0xaf, 0x71, 0x08, 0xab, 0x43, 0xe5, 0x16, 0x7f, 0x52, 0x90, 0xce, + 0xb8, 0xa0, 0x87, 0xfd, 0x2b, 0x69, 0xf1, 0x05, 0x65, 0x37, 0x01, 0x1b, 0xbd, 0xf5, 0xa0, 0x04, 0xa1, 0xf3, 0xfe, + 0xe1, 0xd1, 0x7d, 0x16, 0x14, 0x6b, 0x44, 0x1d, 0x35, 0xf1, 0x6e, 0xb4, 0x9b, 0x54, 0x5c, 0x10, 0xab, 0x36, 0x5b, + 0xed, 0xb0, 0x0c, 0xd1, 0xfb, 0x37, 0x19, 0x59, 0x80, 0xa2, 0xbd, 0xe9, 0x79, 0x19, 0xac, 0x56, 0x4f, 0x13, 0x12, + 0x86, 0x6f, 0x20, 0xab, 0x29, 0x6c, 0x33, 0xdd, 0xca, 0xe8, 0x73, 0x60, 0x8e, 0x9e, 0x74, 0xd6, 0xd4, 0x82, 0xb1, + 0x65, 0xd4, 0x9f, 0x29, 0x0b, 0x27, 0x1f, 0xcb, 0xe0, 0xe7, 0x85, 0x29, 0x75, 0x07, 0x0d, 0xc9, 0x62, 0xc4, 0xca, + 0x4d, 0x3c, 0x74, 0xe8, 0xaa, 0x04, 0x83, 0xf5, 0xdb, 0x7a, 0xe3, 0xac, 0xd7, 0x38, 0x20, 0xf4, 0xde, 0x0f, 0x5c, + 0x2d, 0xfc, 0x10, 0x89, 0x11, 0xde, 0x90, 0x36, 0x47, 0x9d, 0xf1, 0xe2, 0x37, 0xde, 0x1b, 0x43, 0xb9, 0xbd, 0xae, + 0xf8, 0xa3, 0x5f, 0xd7, 0x95, 0x2a, 0x74, 0x25, 0x71, 0x66, 0xee, 0x63, 0x49, 0xd1, 0x23, 0x53, 0xda, 0xc5, 0x3d, + 0x00, 0x58, 0x98, 0x8d, 0x8a, 0xd0, 0xa4, 0x91, 0xb8, 0xfc, 0x54, 0x61, 0xa5, 0x52, 0x9f, 0x50, 0x72, 0x22, 0x30, + 0x0c, 0xbe, 0xff, 0x28, 0xd2, 0x15, 0x47, 0x3f, 0xc0, 0x3f, 0x22, 0x20, 0x50, 0x6b, 0x16, 0x69, 0xa8, 0x1d, 0x90, + 0x8c, 0x9f, 0x0e, 0x17, 0xce, 0xce, 0xcc, 0x88, 0x20, 0x53, 0x77, 0x03, 0x02, 0x84, 0xe1, 0x1a, 0x81, 0x2e, 0xff, + 0x4a, 0x49, 0xdb, 0x96, 0x3b, 0xd4, 0x61, 0x90, 0x5d, 0xe8, 0x20, 0x5a, 0x2d, 0xfa, 0xa5, 0xca, 0xf8, 0x16, 0xd1, + 0xc9, 0xfa, 0xfa, 0xfd, 0xc7, 0xe5, 0x5e, 0xe4, 0x0f, 0x6e, 0x2d, 0x00, 0x98, 0x8d, 0x38, 0x1b, 0x27, 0xbc, 0x6a, + 0x1d, 0x8b, 0x8f, 0xd2, 0x35, 0x86, 0xed, 0x14, 0xb4, 0xe2, 0x41, 0xab, 0x46, 0x54, 0x8a, 0x75, 0x7e, 0xdc, 0x2b, + 0x0f, 0xed, 0x76, 0xef, 0x87, 0xc0, 0xb9, 0x60, 0x47, 0xcc, 0x13, 0xc0, 0xbc, 0xac, 0x5c, 0x15, 0x32, 0x4d, 0xb0, + 0x11, 0x07, 0x39, 0xc8, 0xb4, 0xeb, 0x1e, 0x98, 0xb2, 0x4d, 0x8b, 0xdd, 0x2d, 0x66, 0x61, 0x03, 0x19, 0x21, 0x05, + 0x9b, 0x84, 0x0f, 0xd9, 0x32, 0x09, 0xa5, 0x07, 0x0e, 0x33, 0xd0, 0xd6, 0x7a, 0x14, 0xfb, 0x35, 0x6d, 0x13, 0x5d, + 0xb2, 0xc0, 0xa5, 0x46, 0xb6, 0x6f, 0xfa, 0x84, 0x8e, 0xc5, 0xe4, 0x86, 0xef, 0x77, 0xe7, 0xd5, 0x18, 0x96, 0xed, + 0x63, 0x65, 0x2f, 0xeb, 0xaf, 0x57, 0x2b, 0x50, 0x33, 0x9d, 0xb9, 0x7c, 0xab, 0xe4, 0xdf, 0xf5, 0x61, 0xa0, 0xf6, + 0x42, 0xe1, 0xa3, 0x98, 0x40, 0x59, 0x4b, 0xea, 0x14, 0xbc, 0x35, 0xde, 0xaf, 0xda, 0x61, 0xdc, 0xbf, 0xb9, 0x0b, + 0x15, 0x37, 0xbe, 0xfe, 0xa7, 0x9b, 0xda, 0xe0, 0xe8, 0x8d, 0x70, 0x49, 0xe7, 0x7e, 0xbd, 0x82, 0x00, 0x51, 0x6f, + 0x61, 0xca, 0x3a, 0x6f, 0xaf, 0xae, 0x6b, 0xf2, 0x68, 0x4b, 0x3f, 0xee, 0x82, 0x0e, 0x9b, 0xac, 0x64, 0x6d, 0xa1, + 0x7b, 0x64, 0x35, 0xee, 0x7e, 0x46, 0x38, 0x74, 0xb0, 0x83, 0xd4, 0x2d, 0x3e, 0xf4, 0x0e, 0xd3, 0xeb, 0x94, 0x54, + 0x6f, 0xf5, 0x9b, 0xfa, 0xcb, 0x97, 0xc6, 0xb9, 0x1a, 0x35, 0xac, 0x5d, 0xd4, 0xa4, 0x64, 0x66, 0x0a, 0xa6, 0xdb, + 0x20, 0x85, 0xab, 0xbe, 0xfa, 0xca, 0xe0, 0xc8, 0xf7, 0x73, 0x42, 0x05, 0x9b, 0x10, 0xaa, 0xc7, 0x2f, 0x88, 0xae, + 0x64, 0xfe, 0x71, 0xbb, 0x32, 0x06, 0x49, 0xe8, 0xdb, 0x11, 0x6f, 0xa5, 0xa9, 0xb3, 0x43, 0x3e, 0xe6, 0x6c, 0x82, + 0x5f, 0xd2, 0x84, 0x40, 0xb3, 0xf0, 0x2f, 0x0d, 0xd8, 0xee, 0x70, 0x6c, 0x3d, 0xd0, 0xb8, 0xf8, 0x0f, 0x94, 0x1b, + 0xd1, 0x99, 0x85, 0x1d, 0xef, 0x66, 0xe6, 0x4b, 0x87, 0xc3, 0x9e, 0x61, 0x09, 0x54, 0x65, 0x18, 0xd0, 0x0f, 0xdd, + 0x90, 0xed, 0x52, 0x1d, 0x3b, 0x07, 0x09, 0xeb, 0x3d, 0x14, 0x62, 0x3f, 0x9a, 0xab, 0xcb, 0xeb, 0xe5, 0x81, 0xfb, + 0x0a, 0xc3, 0xe5, 0xc1, 0xb0, 0xf8, 0x98, 0x15, 0x52, 0x75, 0xe9, 0xba, 0x8e, 0x3d, 0xad, 0x31, 0x20, 0x1f, 0x33, + 0x0c, 0x7f, 0x0e, 0x06, 0x8b, 0x76, 0x64, 0xdb, 0x32, 0x38, 0xc3, 0xca, 0xdb, 0x32, 0x65, 0xa6, 0xec, 0x2e, 0xd8, + 0x9e, 0x1a, 0xf8, 0xb3, 0x93, 0x94, 0x11, 0x14, 0x2a, 0xd3, 0x11, 0x34, 0xfa, 0xc7, 0x57, 0x45, 0xad, 0xc8, 0x46, + 0xd6, 0xfc, 0xb6, 0x78, 0x67, 0x8c, 0x53, 0x5a, 0x97, 0xb3, 0x7a, 0x17, 0xab, 0x46, 0x1f, 0x9a, 0x44, 0x2b, 0x92, + 0xb5, 0xaf, 0xb1, 0xc7, 0xc0, 0x10, 0x46, 0xc8, 0x72, 0xb3, 0xd6, 0x66, 0x36, 0x38, 0x89, 0xe3, 0x51, 0x07, 0xd6, + 0xdb, 0x79, 0xe5, 0x15, 0x0c, 0x02, 0xe0, 0x5f, 0x43, 0xcc, 0xb3, 0x0d, 0xfd, 0xce, 0x74, 0x53, 0xd5, 0xcb, 0x25, + 0x14, 0x46, 0x7f, 0x8c, 0x49, 0x07, 0xe5, 0xa5, 0x6a, 0x2a, 0x0d, 0x92, 0x51, 0x3d, 0x16, 0xa4, 0xa3, 0xcb, 0x73, + 0xc6, 0x67, 0x1d, 0xec, 0x69, 0xd2, 0xcd, 0x00, 0x10, 0x69, 0x87, 0x32, 0x26, 0x2a, 0x84, 0x15, 0x1e, 0x19, 0xa1, + 0xca, 0x1d, 0xf8, 0x28, 0xe0, 0xf3, 0xee, 0xb4, 0x20, 0x98, 0xd5, 0x25, 0x87, 0xb7, 0x42, 0x54, 0x14, 0xb7, 0xb2, + 0x9f, 0x90, 0xcc, 0xc7, 0x66, 0x26, 0xda, 0x6b, 0xc6, 0x9a, 0x77, 0x7f, 0x0e, 0x41, 0xa3, 0x20, 0x74, 0x58, 0x44, + 0xf3, 0x43, 0x0e, 0x83, 0xe4, 0x95, 0xd5, 0xd5, 0xc9, 0xf0, 0x5b, 0x81, 0x64, 0x05, 0x42, 0x74, 0xe2, 0x12, 0x84, + 0xde, 0x7e, 0x32, 0xec, 0x82, 0x57, 0xa0, 0x70, 0x28, 0x1c, 0x2f, 0x81, 0xcd, 0x27, 0x46, 0xc7, 0x72, 0xec, 0x74, + 0xc8, 0x55, 0x85, 0x3a, 0x59, 0x45, 0xd0, 0xda, 0x92, 0x9f, 0xf4, 0x95, 0x82, 0x98, 0x64, 0xcb, 0x96, 0x20, 0xa6, + 0x26, 0xc7, 0xc7, 0x09, 0xbd, 0x3f, 0xb1, 0x48, 0x1a, 0x92, 0x84, 0xef, 0x3e, 0xc1, 0x19, 0x23, 0x18, 0x63, 0x95, + 0x12, 0x63, 0x43, 0x59, 0x66, 0x7f, 0x38, 0x7d, 0x33, 0xc1, 0x81, 0x5f, 0x42, 0x91, 0xf2, 0x2a, 0x39, 0xe1, 0x19, + 0xc3, 0x5c, 0xca, 0xf1, 0xac, 0xe8, 0x1b, 0x75, 0xf8, 0x4b, 0x84, 0x22, 0x90, 0xe0, 0x2e, 0x2f, 0x67, 0xea, 0x8b, + 0xca, 0x4c, 0x69, 0x11, 0x6e, 0xf1, 0x1c, 0x6a, 0x8f, 0x1b, 0xb2, 0x13, 0x6f, 0xba, 0xf7, 0x7a, 0x84, 0x0f, 0x54, + 0x8a, 0x70, 0xde, 0x15, 0x83, 0xe5, 0x6e, 0x2c, 0xcc, 0xa5, 0x2f, 0x26, 0x7f, 0xa0, 0xe7, 0xb3, 0xb4, 0x9c, 0xf8, + 0xaa, 0xf9, 0xe6, 0xa8, 0x8b, 0x3f, 0xe2, 0x69, 0x89, 0x6d, 0xb9, 0xbd, 0x49, 0x2f, 0x3e, 0xdf, 0xe7, 0x7f, 0xd7, + 0x78, 0xb0, 0x50, 0xfd, 0x6c, 0x96, 0xe2, 0x06, 0xab, 0x07, 0x3e, 0x04, 0xb9, 0xc8, 0x4c, 0xe5, 0xda, 0xa6, 0xc7, + 0x9a, 0x3c, 0x6c, 0xc6, 0x3a, 0xb1, 0x5a, 0xf9, 0x36, 0xd8, 0xf4, 0x6a, 0x5b, 0xab, 0x5b, 0x13, 0xfa, 0x43, 0xad, + 0x64, 0x5b, 0xfd, 0x82, 0x07, 0x75, 0x34, 0x5f, 0x76, 0x86, 0xd5, 0xc5, 0xfe, 0x94, 0xc3, 0xa4, 0xf8, 0xbb, 0xb4, + 0xa2, 0x88, 0xfb, 0x4b, 0x06, 0x35, 0x75, 0x7e, 0x8c, 0x5f, 0xac, 0x0e, 0xa1, 0xdf, 0xc7, 0x11, 0xb5, 0x54, 0xfe, + 0x87, 0x13, 0xa5, 0x93, 0x49, 0x1e, 0xed, 0xf9, 0x34, 0x2d, 0x5e, 0xd1, 0xa5, 0xdb, 0xf1, 0x32, 0xf9, 0x56, 0x14, + 0x79, 0xbc, 0x58, 0x65, 0xc0, 0xec, 0x75, 0x68, 0xfa, 0xc9, 0xd3, 0x28, 0x39, 0x16, 0x57, 0x0c, 0x6e, 0x3d, 0x0e, + 0x1e, 0x91, 0x37, 0x35, 0xec, 0x4d, 0x28, 0x58, 0xec, 0xc0, 0x20, 0x3f, 0x06, 0x87, 0xa1, 0x43, 0x3d, 0x22, 0xde, + 0x35, 0xbe, 0x9c, 0xd4, 0x47, 0x58, 0x30, 0xab, 0x89, 0xf0, 0xdb, 0x82, 0xbc, 0x47, 0x22, 0x5a, 0x9f, 0x24, 0x8d, + 0xad, 0x74, 0xe0, 0xed, 0x2b, 0x41, 0x36, 0x39, 0xd0, 0x93, 0xde, 0xc2, 0x6c, 0xe7, 0x7c, 0xb4, 0xf2, 0xf7, 0x22, + 0xf9, 0x29, 0x24, 0xd2, 0x55, 0x15, 0x34, 0x75, 0x64, 0x1f, 0x91, 0x61, 0xed, 0x4b, 0xdd, 0xaf, 0x7d, 0x2d, 0xa4, + 0x24, 0xf8, 0xff, 0x69, 0x14, 0x0b, 0xba, 0x0b, 0x98, 0x77, 0x57, 0xc3, 0x30, 0x91, 0x93, 0xd5, 0xc4, 0x65, 0x89, + 0x6e, 0xea, 0x40, 0x61, 0x0c, 0xfb, 0x6d, 0xb4, 0x38, 0x5c, 0xd8, 0x97, 0x3c, 0x50, 0xa9, 0x5b, 0x8e, 0xe7, 0xb7, + 0x32, 0xa7, 0xf2, 0x62, 0x39, 0xa8, 0x0c, 0x5b, 0x03, 0xf0, 0x0c, 0x61, 0x18, 0x10, 0x0d, 0x39, 0x2e, 0x49, 0xb8, + 0xc2, 0xb0, 0x46, 0xf6, 0x58, 0x34, 0x52, 0x86, 0xd5, 0xc5, 0x8d, 0x2c, 0x4e, 0xa6, 0x89, 0x18, 0xce, 0xa7, 0x69, + 0x71, 0x02, 0xfc, 0xc0, 0x84, 0x1b, 0xec, 0xfa, 0xaa, 0xb0, 0x1c, 0xd0, 0x6c, 0x13, 0x1a, 0x2d, 0x52, 0x93, 0x66, + 0x95, 0x74, 0x52, 0xfe, 0x2f, 0xc7, 0x34, 0xd6, 0x19, 0xe9, 0x9c, 0x30, 0x22, 0xf2, 0x0f, 0x8d, 0x52, 0x76, 0x10, + 0xb6, 0x3a, 0x3c, 0x9c, 0x4d, 0x7e, 0x40, 0xd5, 0x46, 0xf7, 0x83, 0xaf, 0x2e, 0x64, 0xb0, 0x07, 0xc1, 0x91, 0xeb, + 0xe6, 0xa8, 0x49, 0xfe, 0x97, 0xa3, 0xfc, 0x73, 0x2e, 0x4e, 0x3a, 0x92, 0xb4, 0xb1, 0x62, 0x44, 0x64, 0x63, 0xfa, + 0x83, 0x4d, 0x9b, 0xc2, 0xa1, 0x0f, 0x0b, 0x3a, 0x9e, 0xa2, 0x40, 0x1a, 0xbd, 0xaa, 0xe4, 0xa0, 0x30, 0x19, 0x20, + 0x7b, 0x25, 0x65, 0xde, 0x2f, 0xe4, 0x32, 0xc8, 0x7c, 0xab, 0x56, 0x66, 0xcb, 0x67, 0x0c, 0xc4, 0x08, 0x37, 0xc2, + 0xe0, 0x57, 0x8a, 0xa9, 0xaf, 0x59, 0xa6, 0xb8, 0x9f, 0x86, 0x90, 0x1d, 0x12, 0x86, 0x1f, 0x68, 0x53, 0x26, 0xa7, + 0x4d, 0x13, 0x2f, 0xf5, 0xd5, 0xd5, 0x30, 0x79, 0x81, 0x4c, 0xde, 0xe0, 0x3e, 0x72, 0xdd, 0x8f, 0xfa, 0x6d, 0x93, + 0x60, 0x7f, 0x95, 0xe0, 0xff, 0x0e, 0x21, 0x5b, 0x0b, 0x60, 0x06, 0x89, 0x8d, 0xbe, 0x5d, 0xf0, 0x71, 0x51, 0xe4, + 0xeb, 0x44, 0xac, 0xa8, 0x8c, 0x4b, 0xb2, 0x5b, 0x07, 0xbb, 0xd2, 0x36, 0x18, 0x6c, 0x90, 0x68, 0x68, 0x32, 0x0d, + 0x9d, 0x2c, 0xdf, 0x2c, 0x0d, 0xc9, 0xb7, 0x65, 0x9d, 0x3b, 0x14, 0x1a, 0x49, 0x0d, 0x6e, 0xd3, 0xf2, 0x39, 0xf5, + 0x94, 0x6c, 0x78, 0x6c, 0x2b, 0x79, 0x50, 0x61, 0x3a, 0x36, 0xb3, 0x79, 0xb4, 0x62, 0x3d, 0xaf, 0xd6, 0x39, 0x24, + 0x76, 0x5b, 0x56, 0x0e, 0x56, 0x64, 0x88, 0x4f, 0x5c, 0x8f, 0xd6, 0xf6, 0xa3, 0xef, 0x63, 0x44, 0x25, 0x39, 0x18, + 0x96, 0x2d, 0x95, 0xa7, 0x16, 0x27, 0x9e, 0xda, 0xfd, 0x60, 0xfa, 0xb2, 0x48, 0x85, 0x17, 0x4d, 0xe6, 0x8b, 0xd4, + 0xa4, 0x38, 0xcc, 0xc5, 0x48, 0xcc, 0x81, 0xa5, 0x7d, 0x84, 0x8c, 0xc1, 0x52, 0x7a, 0xa4, 0xb1, 0x8d, 0x91, 0xb7, + 0xe8, 0x50, 0x2e, 0x3a, 0xfb, 0x9f, 0x59, 0x7a, 0x0a, 0x43, 0xda, 0x2c, 0x8d, 0x6b, 0xb7, 0x54, 0x5e, 0xd9, 0x0f, + 0xae, 0xb2, 0x6d, 0x1b, 0x8b, 0xa0, 0x7e, 0xb2, 0xde, 0x92, 0x4c, 0x2a, 0x70, 0x30, 0x31, 0xd0, 0x43, 0x65, 0x37, + 0x5a, 0x6b, 0x7a, 0x3c, 0x4a, 0x5b, 0xa4, 0x89, 0xb0, 0x36, 0x32, 0xaa, 0xba, 0x8a, 0xb8, 0xbd, 0xd2, 0x97, 0x7c, + 0x05, 0x18, 0x6f, 0xbc, 0xb9, 0xb9, 0x46, 0xc3, 0x1d, 0x73, 0x12, 0xc5, 0x20, 0xde, 0xad, 0x8c, 0x27, 0x4d, 0xeb, + 0xf2, 0xdb, 0x72, 0xe2, 0x53, 0xbd, 0x6e, 0x31, 0x81, 0x0c, 0xce, 0x82, 0x78, 0x3d, 0x03, 0x58, 0x65, 0x57, 0x11, + 0xd5, 0x66, 0x41, 0x82, 0x60, 0x1a, 0x5c, 0xc7, 0x1a, 0xb7, 0x39, 0xca, 0x36, 0x09, 0x7a, 0xe6, 0x68, 0x2c, 0xff, + 0x9d, 0x59, 0xe0, 0xe5, 0x45, 0x81, 0xf6, 0xc4, 0x81, 0xef, 0xc6, 0x33, 0xf6, 0xfa, 0x9f, 0x8f, 0x8c, 0x01, 0x1c, + 0xd4, 0x98, 0x93, 0xd9, 0xe1, 0x55, 0xda, 0xaa, 0xf4, 0x2a, 0xa9, 0xb2, 0x3d, 0xc4, 0xab, 0xde, 0x8c, 0x24, 0x4a, + 0xa9, 0x0b, 0xb6, 0x6c, 0xe6, 0x41, 0xe2, 0x35, 0x47, 0x7f, 0xac, 0x25, 0x92, 0xbd, 0x82, 0x86, 0x91, 0x33, 0xf1, + 0x8b, 0x4f, 0x89, 0x79, 0xa7, 0x7d, 0xc5, 0xe4, 0x79, 0x83, 0x8f, 0xe0, 0x8b, 0x4a, 0x5d, 0x34, 0xde, 0x38, 0x84, + 0x3a, 0xd6, 0x30, 0x41, 0x0a, 0x30, 0x73, 0x80, 0x8b, 0x62, 0xe5, 0x93, 0x51, 0x52, 0xec, 0x9d, 0x16, 0xdb, 0xbc, + 0x16, 0x8a, 0x57, 0xfe, 0xa2, 0x94, 0xa6, 0xf6, 0xf2, 0xa9, 0x0d, 0xe5, 0xfb, 0x0d, 0x74, 0x1a, 0xeb, 0x8d, 0xe8, + 0xa3, 0x94, 0xf2, 0xec, 0x3a, 0xe1, 0x0c, 0x8f, 0xad, 0x8a, 0x59, 0x3c, 0x58, 0x35, 0xce, 0x3e, 0x84, 0x12, 0x1e, + 0x2d, 0x5b, 0x52, 0x76, 0xf3, 0x14, 0x86, 0xbf, 0xc3, 0x4a, 0xa1, 0xa8, 0x45, 0x80, 0xc0, 0xba, 0x8d, 0x7f, 0xdc, + 0x69, 0x3a, 0x6f, 0xa7, 0xb3, 0xc1, 0xc6, 0xe2, 0x68, 0xd8, 0x1e, 0xa9, 0x32, 0xf0, 0xcb, 0xc7, 0x33, 0x6b, 0x4d, + 0x0a, 0x17, 0x78, 0xa8, 0xc2, 0xf6, 0xaf, 0xa3, 0x3d, 0x69, 0xbe, 0x23, 0xe0, 0x8e, 0x41, 0x3a, 0x01, 0xdf, 0x79, + 0x08, 0x5d, 0x00, 0xed, 0x14, 0x62, 0x17, 0x21, 0x75, 0x09, 0x72, 0x97, 0xa1, 0xed, 0x5a, 0x78, 0x40, 0x4c, 0xf0, + 0xb0, 0x32, 0xc3, 0xa3, 0xca, 0x02, 0x8f, 0x2b, 0x7b, 0x78, 0x42, 0x1c, 0xe0, 0x69, 0x65, 0x85, 0x85, 0x8a, 0xea, + 0xb2, 0xba, 0xaa, 0xae, 0xab, 0xa5, 0x32, 0xf4, 0x2b, 0x96, 0x05, 0xc6, 0xbf, 0x20, 0x46, 0xb0, 0xf8, 0xa0, 0x31, + 0xe5, 0xf6, 0xc1, 0xc3, 0x47, 0x8f, 0x9f, 0x3c, 0x0d, 0x55, 0xa6, 0xf3, 0x75, 0xd6, 0xa4, 0xe6, 0x71, 0x6b, 0xa8, + 0x23, 0xef, 0x7c, 0x39, 0xf4, 0x2d, 0x03, 0xea, 0xec, 0x69, 0x1a, 0x86, 0x2f, 0x5d, 0x7e, 0x3d, 0xf1, 0x4b, 0xc5, + 0xeb, 0xc6, 0x37, 0x66, 0x7c, 0xef, 0x8b, 0x4f, 0x13, 0x5a, 0x3c, 0xbd, 0xe0, 0x82, 0x15, 0x3c, 0x98, 0x8f, 0xf3, + 0x2e, 0x0b, 0x11, 0x75, 0xaa, 0x87, 0xc2, 0x5a, 0xf1, 0xfc, 0x83, 0x0e, 0x63, 0x2f, 0x4e, 0x11, 0xd9, 0x37, 0xe7, + 0x86, 0x88, 0xa6, 0xc9, 0xbb, 0x88, 0xaa, 0x7f, 0xb0, 0x7b, 0x29, 0x1a, 0x5d, 0x52, 0xee, 0xd3, 0xdf, 0x9f, 0xad, + 0xc8, 0xbe, 0xa9, 0xe8, 0x41, 0x71, 0x70, 0x56, 0x2d, 0x4c, 0x66, 0x93, 0xa6, 0x4e, 0x36, 0xc0, 0xf7, 0x4c, 0x0a, + 0xaa, 0x92, 0xa1, 0xf6, 0xcf, 0x1e, 0xf8, 0x4f, 0x5a, 0x77, 0xe1, 0xa7, 0xb2, 0x7e, 0xbb, 0xff, 0x9a, 0xbe, 0x2d, + 0x4e, 0x58, 0x9c, 0x58, 0x94, 0x54, 0xd2, 0x99, 0xc9, 0xa2, 0x9e, 0xd7, 0x57, 0xce, 0xcd, 0x3c, 0xc9, 0x2c, 0x9f, + 0xf6, 0x56, 0xd0, 0xd9, 0x6d, 0x80, 0x98, 0x04, 0xea, 0x35, 0x3e, 0x65, 0xf5, 0x66, 0x3c, 0xfd, 0xc4, 0xb2, 0x29, + 0x85, 0x00, 0xa7, 0xc5, 0x17, 0xff, 0xdb, 0xf1, 0x2d, 0xf9, 0x6e, 0xa6, 0x97, 0xf9, 0xfd, 0x9a, 0x6c, 0xe6, 0x46, + 0x0a, 0xdc, 0xf1, 0xcb, 0xd9, 0xe8, 0xb9, 0xc6, 0x7f, 0x33, 0xc8, 0xdb, 0xe4, 0xe9, 0x52, 0xca, 0xd5, 0xc6, 0x0e, + 0x5d, 0x6e, 0xfe, 0xa9, 0x2a, 0x8f, 0x5d, 0x5b, 0xea, 0xf8, 0xe7, 0x5d, 0x1c, 0x8f, 0x5f, 0xf4, 0x3f, 0xde, 0xe7, + 0x1e, 0xef, 0xa2, 0x66, 0x51, 0xfe, 0x3d, 0x54, 0xa9, 0xe3, 0xf7, 0xa7, 0xf9, 0xd2, 0x51, 0x3e, 0x4e, 0x4d, 0xf3, + 0x41, 0x5e, 0x46, 0x2c, 0x8f, 0xd6, 0xf4, 0xf6, 0xcf, 0x9f, 0x12, 0xff, 0x6b, 0x73, 0x9d, 0xed, 0xe1, 0xfe, 0xa4, + 0x5e, 0x6c, 0x63, 0x31, 0x22, 0x66, 0x63, 0xc1, 0x0e, 0xa8, 0xde, 0x3f, 0x3d, 0x87, 0x94, 0x65, 0xfc, 0xa7, 0xc7, + 0x0b, 0x2c, 0x9d, 0xc0, 0x9a, 0xbf, 0xed, 0x98, 0x96, 0xbc, 0xcb, 0x96, 0x63, 0x0c, 0x7d, 0xc6, 0xff, 0xe6, 0x2e, + 0xee, 0xb0, 0x5a, 0x12, 0xa5, 0x18, 0x11, 0x16, 0xfa, 0xf9, 0x8d, 0x0f, 0x42, 0xf4, 0x61, 0xe5, 0xc1, 0x54, 0xaa, + 0x4d, 0xa6, 0x9c, 0x38, 0x81, 0x0f, 0xbe, 0x1d, 0x78, 0x5a, 0x81, 0x37, 0x7f, 0xdb, 0x37, 0x2d, 0xa3, 0x63, 0xf7, + 0x92, 0xbe, 0xbc, 0x9c, 0xb7, 0x4c, 0x4b, 0xbc, 0xd4, 0xc5, 0xed, 0x49, 0x13, 0x7a, 0x67, 0x41, 0x35, 0xc9, 0xc9, + 0xa7, 0xe5, 0x13, 0x0c, 0xfb, 0x45, 0x49, 0xac, 0x9a, 0x56, 0x43, 0x63, 0xd4, 0x9b, 0x26, 0x53, 0x37, 0xb8, 0xd6, + 0xa8, 0x57, 0x81, 0x7f, 0x66, 0x40, 0xee, 0x39, 0x13, 0x7b, 0xce, 0xa0, 0x97, 0x6f, 0x7e, 0x4c, 0x9a, 0xb7, 0xa6, + 0xde, 0x16, 0x5f, 0x2b, 0xa6, 0x52, 0xa2, 0xf0, 0x7d, 0x5d, 0xb8, 0x10, 0x1f, 0x0b, 0x97, 0xab, 0x07, 0xa6, 0xe9, + 0xd3, 0x29, 0xe4, 0xfb, 0x56, 0xe0, 0x9c, 0x91, 0xa3, 0xa0, 0x0d, 0x86, 0x3c, 0x62, 0xde, 0x0f, 0x21, 0xe5, 0x73, + 0x1f, 0xc6, 0xe8, 0xf7, 0xd8, 0x08, 0x28, 0x96, 0xa3, 0xec, 0x2e, 0xc4, 0x3c, 0xcf, 0x62, 0x76, 0xd0, 0xd5, 0x72, + 0xb3, 0xee, 0x98, 0x63, 0x26, 0x56, 0xfc, 0x8c, 0xeb, 0x58, 0xc1, 0x8c, 0x7c, 0xd0, 0xa2, 0x3b, 0xfa, 0x23, 0x2b, + 0xfb, 0x3a, 0x50, 0x77, 0x12, 0x5a, 0x3b, 0x4c, 0xdf, 0x66, 0x19, 0x0e, 0x28, 0x96, 0x08, 0x12, 0x85, 0xe6, 0xf7, + 0x2a, 0x31, 0xb1, 0x52, 0x9c, 0x1a, 0xcd, 0xf1, 0x86, 0xaa, 0xc4, 0x98, 0xa0, 0xe5, 0xed, 0xea, 0x8b, 0xcc, 0x74, + 0x09, 0x1b, 0x37, 0x58, 0xd2, 0x8a, 0xfd, 0xa2, 0xa4, 0x7e, 0xdf, 0x96, 0x85, 0x8a, 0x77, 0xd9, 0xd5, 0x51, 0x5d, + 0xda, 0xa1, 0xa3, 0x22, 0x66, 0xa0, 0xe8, 0xbb, 0x9d, 0x57, 0xce, 0x46, 0xf6, 0x81, 0xdb, 0x09, 0x9c, 0x05, 0x55, + 0x79, 0x9d, 0xea, 0x50, 0x0a, 0x97, 0x05, 0xe0, 0x16, 0x38, 0x3d, 0x3d, 0x09, 0xca, 0xb3, 0xae, 0xac, 0x44, 0x57, + 0x7a, 0x37, 0xe4, 0x3f, 0xd2, 0xe8, 0xc7, 0x39, 0xe2, 0xd9, 0xe7, 0xdd, 0xe9, 0x46, 0xa7, 0xf9, 0x5d, 0xfe, 0x26, + 0xe6, 0xef, 0x2a, 0xfa, 0xf2, 0x97, 0x2d, 0x6f, 0x5f, 0x07, 0xc2, 0x60, 0x69, 0x86, 0xf8, 0xda, 0x52, 0x79, 0x8d, + 0xb9, 0x0f, 0x31, 0x4d, 0xc2, 0xc6, 0xc8, 0xa3, 0x29, 0xe0, 0x3c, 0xdf, 0xbb, 0x51, 0x7a, 0x6d, 0x4b, 0x82, 0x50, + 0xe2, 0x3d, 0xdf, 0x4a, 0xbf, 0xe7, 0x71, 0x11, 0x0b, 0x32, 0xdb, 0xd0, 0xe9, 0xf9, 0xa8, 0x8e, 0x21, 0xe6, 0xc6, + 0xd3, 0x2e, 0xec, 0xa9, 0x5a, 0xab, 0x05, 0x49, 0xd2, 0xeb, 0x3c, 0xdf, 0xfd, 0xce, 0xa4, 0x8c, 0xe0, 0x51, 0x13, + 0x24, 0x7e, 0xee, 0xeb, 0x3f, 0xef, 0x4c, 0x0d, 0x7a, 0x0b, 0x78, 0x5a, 0x3a, 0xe8, 0xc9, 0x27, 0x8e, 0x5f, 0x0b, + 0xcb, 0x6d, 0xa3, 0x4b, 0xff, 0x7d, 0x9e, 0xad, 0x4b, 0x26, 0x4c, 0xba, 0x45, 0x32, 0x1f, 0x2b, 0x13, 0xe8, 0xa9, + 0x69, 0x9d, 0x90, 0xaa, 0xfe, 0x89, 0x15, 0xd4, 0x28, 0x70, 0xa0, 0x94, 0xba, 0x9a, 0xb0, 0x10, 0xdc, 0x09, 0x8f, + 0xcf, 0x4b, 0x01, 0xd1, 0xdc, 0xbd, 0x08, 0x92, 0x8b, 0xc4, 0x6b, 0x41, 0x25, 0x56, 0x65, 0xc5, 0x65, 0x55, 0x52, + 0x09, 0xa6, 0xf0, 0x03, 0x00, 0xcd, 0x7b, 0x05, 0x4c, 0x72, 0xa0, 0x47, 0x54, 0x3a, 0xf9, 0xa8, 0x11, 0x1f, 0xf0, + 0x64, 0x93, 0xfe, 0xa1, 0xa7, 0xb8, 0x24, 0x4e, 0x9c, 0xe9, 0x50, 0x82, 0xfb, 0x63, 0xd2, 0xfa, 0x53, 0xc5, 0x7c, + 0x8b, 0x89, 0x0e, 0xea, 0xf2, 0xf6, 0xfa, 0x3a, 0xab, 0x89, 0x84, 0x1a, 0x70, 0xc3, 0x9a, 0xd6, 0x94, 0xba, 0x6e, + 0x03, 0x4d, 0x1f, 0x40, 0xdf, 0xb3, 0xea, 0xf6, 0x47, 0x85, 0xe5, 0x40, 0x6e, 0x72, 0x5b, 0x8d, 0xa2, 0x8d, 0xff, + 0x38, 0xb8, 0xa9, 0xc6, 0x29, 0x70, 0x5d, 0xcd, 0x64, 0x99, 0x80, 0xab, 0x6a, 0x8a, 0x00, 0x2e, 0xab, 0xf9, 0x09, + 0xf5, 0xbd, 0xfa, 0x82, 0x8c, 0x5f, 0xea, 0xf0, 0xf5, 0x7e, 0xae, 0x20, 0xf4, 0xce, 0x4f, 0x59, 0x5b, 0x39, 0xf3, + 0x9c, 0xf7, 0xeb, 0x94, 0x83, 0xf7, 0x1b, 0xba, 0xee, 0xf0, 0xa9, 0xce, 0xbc, 0x0d, 0xd0, 0xfe, 0x29, 0x6f, 0xde, + 0xe9, 0x29, 0x62, 0xef, 0xe4, 0x6a, 0xee, 0xee, 0xff, 0x69, 0xe8, 0x79, 0x6f, 0x78, 0xaa, 0xb4, 0x95, 0xe3, 0x4a, + 0x8f, 0xde, 0xd1, 0xbf, 0x96, 0xda, 0x02, 0x87, 0xd5, 0x54, 0x0a, 0x1c, 0x54, 0x93, 0x2b, 0xb0, 0x5f, 0xcd, 0xab, + 0x5a, 0x3b, 0x00, 0x9b, 0xd5, 0x20, 0x03, 0x7b, 0xd5, 0x64, 0x0d, 0xd8, 0xaa, 0x46, 0xbb, 0x4f, 0x1e, 0xd8, 0xae, + 0x06, 0x6b, 0x60, 0x67, 0x64, 0x5e, 0xe7, 0xed, 0xfe, 0x33, 0xf1, 0xdc, 0xc0, 0x58, 0xaf, 0xdb, 0xcb, 0x3e, 0x33, + 0xef, 0x75, 0x0d, 0x4c, 0x97, 0x65, 0x5b, 0xac, 0x5a, 0x80, 0x0d, 0xfa, 0xef, 0x48, 0xd3, 0xd6, 0x0a, 0x7e, 0x23, + 0x90, 0xc0, 0xfc, 0xec, 0x2c, 0x60, 0x01, 0xa3, 0xff, 0xd0, 0xe3, 0xd8, 0xc0, 0x50, 0x4b, 0xac, 0x4f, 0xd6, 0x31, + 0x6d, 0xd3, 0xff, 0xc7, 0xc6, 0x66, 0x1b, 0x25, 0x48, 0xb7, 0xed, 0x35, 0xbe, 0xbc, 0x65, 0x47, 0x68, 0x3f, 0x52, + 0xd4, 0xa5, 0xb4, 0x0a, 0xf0, 0x57, 0x4b, 0x72, 0xf1, 0xe8, 0xb2, 0x3d, 0x13, 0xbe, 0x57, 0x67, 0xb1, 0xce, 0x94, + 0x90, 0x88, 0x1d, 0xe2, 0xea, 0xfe, 0xbf, 0x3c, 0xf5, 0x95, 0x42, 0x65, 0x11, 0x27, 0xc5, 0x47, 0x7c, 0x4c, 0xb6, + 0x0a, 0x4e, 0x11, 0xc7, 0x42, 0x8c, 0x43, 0x37, 0x4f, 0xce, 0x61, 0xd5, 0x24, 0x0a, 0xfb, 0x47, 0xc2, 0x6b, 0xd0, + 0xf0, 0x9b, 0x6c, 0x39, 0xc0, 0xce, 0x19, 0x86, 0xd0, 0x8c, 0xbe, 0xb3, 0x9c, 0x49, 0x95, 0x93, 0x57, 0x30, 0x8e, + 0xe8, 0x58, 0xfd, 0x8e, 0x94, 0x97, 0xf7, 0xa5, 0xca, 0x97, 0x43, 0x50, 0xb6, 0x1f, 0xe0, 0x1d, 0x14, 0xc5, 0xf4, + 0xb9, 0x0c, 0x16, 0xf8, 0x5e, 0x77, 0x0f, 0x0f, 0xf0, 0xc2, 0x26, 0xfc, 0x58, 0x2b, 0x4f, 0x78, 0x67, 0xab, 0x96, + 0xd1, 0x63, 0xe1, 0x60, 0x06, 0xeb, 0x20, 0xfe, 0xaf, 0x79, 0xdd, 0x32, 0x80, 0x12, 0x38, 0xe1, 0xe6, 0x4c, 0x7b, + 0x7d, 0x9d, 0x3e, 0xb0, 0x32, 0x69, 0x8a, 0x5d, 0xf4, 0x07, 0xe6, 0x2a, 0xca, 0x6b, 0xa8, 0xe2, 0x34, 0xbb, 0x91, + 0xba, 0xc0, 0xe3, 0x7b, 0xa5, 0x3b, 0x8f, 0x3b, 0x3c, 0x74, 0xed, 0x0a, 0x8a, 0x5c, 0x23, 0x06, 0xda, 0x5d, 0x00, + 0xdd, 0x2e, 0xc3, 0xc6, 0xbf, 0xea, 0x27, 0xa9, 0x08, 0x44, 0x34, 0xe3, 0x5e, 0xf8, 0x17, 0xb0, 0xad, 0x1b, 0xdd, + 0xa2, 0x94, 0x8e, 0x2e, 0x72, 0xeb, 0xad, 0xc4, 0x9d, 0xa9, 0x64, 0xa3, 0x41, 0x0d, 0x58, 0xce, 0xbd, 0xff, 0x3b, + 0xd5, 0x5c, 0x52, 0x7f, 0xc8, 0xc4, 0x41, 0xdf, 0x9a, 0xad, 0x90, 0x05, 0xff, 0x32, 0xc9, 0x7f, 0xc6, 0x3d, 0x3e, + 0xf1, 0xd5, 0xf3, 0x90, 0x36, 0xdd, 0xb4, 0x02, 0x76, 0xd7, 0x14, 0x59, 0x9f, 0xf2, 0x71, 0x11, 0x46, 0xce, 0x82, + 0x47, 0xf8, 0x03, 0x3a, 0x09, 0x71, 0xd9, 0xfb, 0xda, 0x8b, 0xbd, 0xe8, 0xb9, 0x4c, 0xfa, 0xc7, 0xac, 0x5e, 0xa4, + 0xe3, 0x3c, 0xd4, 0x66, 0xef, 0xa8, 0x5f, 0xa3, 0x0c, 0xd5, 0x9b, 0xb3, 0x9e, 0xc3, 0x36, 0x30, 0xea, 0x4e, 0x89, + 0x56, 0x11, 0xa5, 0x1b, 0x31, 0x68, 0x28, 0x39, 0x19, 0x5f, 0xe9, 0x6c, 0xbf, 0xbb, 0xbc, 0xe3, 0x56, 0x53, 0x0a, + 0x14, 0xfb, 0x46, 0xb9, 0xc6, 0x87, 0x9f, 0x25, 0x2b, 0x22, 0xcb, 0x7a, 0xc1, 0x78, 0xf6, 0x1a, 0xd6, 0x82, 0x69, + 0x8a, 0xb6, 0x50, 0x7b, 0x71, 0x71, 0xd1, 0x84, 0x6b, 0x9d, 0xdc, 0xb8, 0x3b, 0x1f, 0x16, 0x76, 0x41, 0x09, 0xb7, + 0xbd, 0xc2, 0xd3, 0xc1, 0xdc, 0x67, 0x46, 0x28, 0x89, 0x06, 0xe0, 0x04, 0x5c, 0x3a, 0x86, 0xeb, 0xc6, 0xf1, 0x5e, + 0x03, 0x78, 0x58, 0x2f, 0xe0, 0x0c, 0x50, 0xa9, 0x78, 0x45, 0x77, 0xab, 0x1e, 0xbf, 0x3b, 0x4b, 0xf3, 0xeb, 0x44, + 0x19, 0x14, 0x96, 0x83, 0x87, 0x96, 0x32, 0xcf, 0x34, 0xa8, 0x51, 0x83, 0x8d, 0x54, 0xac, 0x90, 0x17, 0xaf, 0x79, + 0xd0, 0xba, 0xea, 0x85, 0x3d, 0x41, 0x4a, 0x3f, 0x2f, 0x73, 0x0b, 0xaa, 0x81, 0x81, 0x84, 0x58, 0x36, 0x99, 0xd5, + 0x1f, 0x1d, 0x3c, 0x77, 0xaf, 0xa6, 0x9a, 0x6c, 0xb2, 0x05, 0x99, 0xf7, 0xeb, 0xef, 0xad, 0x24, 0x5b, 0x7e, 0x71, + 0x27, 0xdb, 0x5f, 0xef, 0x97, 0x42, 0xc9, 0xe8, 0xcd, 0x38, 0x3b, 0xd6, 0x83, 0x72, 0xd6, 0x81, 0x40, 0x44, 0x7e, + 0x5b, 0x1d, 0x0a, 0xc4, 0x62, 0x32, 0x82, 0x32, 0x85, 0x19, 0x61, 0xdf, 0xd0, 0x5d, 0x1e, 0xc6, 0xdb, 0xb7, 0x13, + 0xd8, 0x4c, 0x2c, 0x28, 0xc0, 0x58, 0xdc, 0x4e, 0x4e, 0x27, 0xd7, 0x50, 0x09, 0xe6, 0x31, 0xc7, 0x50, 0x09, 0xc9, + 0xbd, 0x72, 0x66, 0xfd, 0x58, 0x64, 0x15, 0x35, 0x06, 0xc9, 0x95, 0x55, 0xc0, 0xb2, 0xb7, 0x90, 0x90, 0x71, 0xc8, + 0x28, 0x72, 0x02, 0xfb, 0x45, 0x6a, 0x46, 0x41, 0x49, 0xfc, 0x6f, 0xe6, 0x9e, 0xb9, 0x99, 0x7b, 0xe5, 0xc3, 0xb2, + 0x34, 0x29, 0x49, 0x56, 0x69, 0x13, 0xf4, 0x13, 0x3a, 0x7e, 0x89, 0xd8, 0xe9, 0xb4, 0x21, 0x34, 0xb0, 0xc7, 0x38, + 0x32, 0x82, 0xa9, 0x30, 0xa1, 0x7a, 0x57, 0x6f, 0x40, 0x12, 0x78, 0x80, 0xd1, 0xce, 0x44, 0x4e, 0x68, 0xe0, 0xc3, + 0xd9, 0x68, 0xe3, 0xe6, 0xe1, 0x77, 0x4e, 0x54, 0xaa, 0x85, 0x9d, 0xe5, 0xdf, 0x5b, 0x70, 0xa5, 0xfd, 0x4d, 0x40, + 0x95, 0xc0, 0xc5, 0xe4, 0xdf, 0xbf, 0xfb, 0x71, 0xe8, 0xdf, 0x2a, 0xe7, 0x8f, 0x16, 0x8b, 0x6f, 0x23, 0x3b, 0x5c, + 0x82, 0xb5, 0xae, 0x23, 0xe7, 0x22, 0x3f, 0x33, 0x1b, 0xaa, 0xe1, 0xf7, 0x43, 0xdd, 0xee, 0xe4, 0x42, 0xa9, 0x5a, + 0x7c, 0x32, 0x72, 0xdc, 0x51, 0xc9, 0x13, 0x89, 0x86, 0x58, 0xd1, 0xf2, 0x2b, 0x26, 0x9e, 0xd1, 0xf8, 0x52, 0xf1, + 0x48, 0x16, 0xee, 0x1f, 0xb9, 0xf6, 0x92, 0xa9, 0x83, 0x6c, 0xc0, 0x64, 0x64, 0xae, 0xfc, 0x76, 0xdc, 0x68, 0x1f, + 0xe6, 0x0f, 0x7f, 0xaa, 0x4f, 0xc4, 0xde, 0xff, 0x9a, 0x51, 0x76, 0xbd, 0x2c, 0x24, 0x3f, 0x61, 0xe1, 0x09, 0x7f, + 0x1c, 0xa1, 0xe0, 0xe5, 0xf4, 0xf1, 0x82, 0x38, 0x8e, 0x9e, 0x2b, 0x76, 0x20, 0x4b, 0x25, 0x2a, 0xee, 0x22, 0x48, + 0x42, 0x14, 0xe1, 0x09, 0xa0, 0xe4, 0x7d, 0x5c, 0x89, 0x4a, 0xb3, 0x98, 0x98, 0x4f, 0x46, 0x2a, 0x00, 0x67, 0x07, + 0xd1, 0xd0, 0x4a, 0xa4, 0x27, 0xea, 0x24, 0xa2, 0x21, 0x47, 0x67, 0x83, 0xfe, 0x9d, 0x78, 0x16, 0x1b, 0xa2, 0x22, + 0x40, 0x4c, 0x41, 0xd4, 0xb1, 0x15, 0x6f, 0x94, 0x81, 0x2b, 0xea, 0xa1, 0x44, 0x82, 0xd6, 0xd4, 0x19, 0xa0, 0x29, + 0x21, 0x20, 0xc7, 0x5c, 0xee, 0xa1, 0x87, 0x19, 0xa6, 0x6d, 0xb7, 0xab, 0xa8, 0x30, 0xde, 0x1f, 0xce, 0xcb, 0xa8, + 0xd4, 0x76, 0x0a, 0x44, 0xa9, 0x7a, 0x1a, 0xc6, 0x38, 0x1d, 0x2b, 0x84, 0x77, 0x01, 0xc5, 0x25, 0xc9, 0x4a, 0x8c, + 0xfa, 0x6e, 0x34, 0x6d, 0xaa, 0x11, 0x12, 0x38, 0xe9, 0xdd, 0xf4, 0x3a, 0x40, 0x49, 0x0c, 0x26, 0x58, 0x1c, 0xc1, + 0x66, 0xf0, 0xc9, 0xb1, 0x46, 0x90, 0x94, 0x50, 0xa0, 0x54, 0x99, 0xf1, 0x47, 0xad, 0xa4, 0x20, 0xf1, 0x3e, 0xfc, + 0xfc, 0x53, 0x65, 0xf1, 0xc4, 0x0a, 0x1b, 0xef, 0x63, 0x7e, 0x5f, 0xc9, 0xab, 0x1e, 0x84, 0x89, 0x07, 0xc4, 0xb7, + 0x09, 0x1c, 0x61, 0x99, 0xca, 0x65, 0x83, 0x0c, 0x05, 0x28, 0x38, 0xd5, 0x9a, 0xb3, 0x38, 0x13, 0x9b, 0x46, 0xaa, + 0x30, 0xe8, 0x11, 0x4c, 0x91, 0xfc, 0x8b, 0xb8, 0x7f, 0xdf, 0x06, 0xc4, 0xf8, 0x14, 0x1f, 0xfa, 0xc9, 0xeb, 0x9d, + 0x85, 0x2a, 0x98, 0x10, 0xf5, 0xe2, 0x3f, 0x8f, 0x97, 0xc5, 0xae, 0x1f, 0x1f, 0x36, 0x15, 0x48, 0x20, 0xf6, 0x3c, + 0x0d, 0xce, 0x7f, 0xc6, 0x2c, 0x09, 0x03, 0x69, 0x71, 0x6f, 0x4a, 0xe0, 0x4d, 0x2f, 0x58, 0x9a, 0x4d, 0x80, 0x09, + 0x52, 0x9c, 0x8c, 0xb6, 0x16, 0xac, 0x53, 0x4f, 0x8d, 0xd2, 0x99, 0x13, 0x60, 0x52, 0xa9, 0x87, 0x90, 0x9e, 0x7a, + 0x81, 0xde, 0xb1, 0xa2, 0x1f, 0xe3, 0xad, 0x86, 0xb7, 0xec, 0x6a, 0x91, 0x0a, 0x8b, 0xd0, 0x19, 0x08, 0x2e, 0x0b, + 0x4e, 0xf1, 0xfb, 0x08, 0x75, 0x0d, 0xc3, 0x9a, 0xb1, 0xc9, 0x89, 0x1a, 0x2a, 0xc8, 0xb4, 0xee, 0xe9, 0x60, 0xef, + 0x65, 0xde, 0x24, 0xcc, 0x9c, 0x0f, 0x47, 0x94, 0x6c, 0x78, 0xe3, 0x1e, 0x4f, 0x3f, 0x20, 0xdd, 0x19, 0xa4, 0x4f, + 0x30, 0x3d, 0xa6, 0x50, 0x12, 0x41, 0x73, 0x5f, 0x5e, 0x92, 0xab, 0x47, 0x87, 0xbd, 0x52, 0x37, 0xdd, 0x2f, 0xf7, + 0x72, 0xd2, 0xb8, 0x85, 0xe9, 0x2a, 0xec, 0x76, 0xf7, 0xa3, 0x7e, 0x10, 0x38, 0xea, 0xa5, 0x9a, 0x66, 0xa0, 0x66, + 0x92, 0x62, 0x67, 0xf2, 0x56, 0xca, 0x37, 0x8e, 0xa5, 0x17, 0xd4, 0x79, 0x5a, 0x33, 0x6f, 0x72, 0x9f, 0x15, 0x8a, + 0xb2, 0xc2, 0xda, 0xa1, 0x8c, 0x61, 0xa8, 0x92, 0xe1, 0x71, 0x1a, 0xc1, 0xba, 0x28, 0x8a, 0xb6, 0xe8, 0x3a, 0x73, + 0x34, 0xa7, 0x5d, 0xa5, 0x4b, 0xb2, 0xc4, 0xf7, 0xe8, 0x47, 0x13, 0x95, 0xfd, 0x25, 0x7a, 0x91, 0x5a, 0x5a, 0x99, + 0xb6, 0x77, 0xd7, 0xd4, 0xf1, 0x3b, 0x1b, 0x85, 0x3f, 0xba, 0xc1, 0x67, 0xa6, 0xf3, 0xd1, 0x06, 0x37, 0x96, 0xee, + 0x08, 0x67, 0xb0, 0x6d, 0x63, 0x3f, 0xef, 0x03, 0x46, 0x85, 0x0f, 0xa8, 0xbe, 0x9e, 0xe6, 0x1d, 0xae, 0x1d, 0x71, + 0x53, 0x5b, 0xcf, 0xff, 0x66, 0x3a, 0x07, 0xe5, 0x07, 0x7c, 0xc0, 0xa6, 0xcf, 0xf6, 0x16, 0x2c, 0x95, 0xaf, 0x3b, + 0x01, 0x63, 0x49, 0xd5, 0x55, 0x98, 0xad, 0xd9, 0xd2, 0x60, 0x80, 0xe2, 0x22, 0x3f, 0x73, 0x2a, 0x88, 0x70, 0x89, + 0x4c, 0x70, 0x03, 0x45, 0x1f, 0x50, 0x42, 0x99, 0xb1, 0x08, 0x0f, 0xc6, 0x57, 0x98, 0x48, 0x8c, 0xf4, 0x27, 0x14, + 0xc6, 0xe3, 0x8e, 0x7f, 0x7e, 0xce, 0xcf, 0xbb, 0x66, 0xa7, 0xbd, 0xc2, 0xd8, 0xc4, 0xee, 0x59, 0xe8, 0xf8, 0x83, + 0x9c, 0xfd, 0xce, 0xfc, 0x45, 0x30, 0xad, 0xf3, 0x17, 0xc2, 0x7e, 0xe6, 0x44, 0x82, 0xbf, 0xf6, 0xd4, 0xf6, 0x06, + 0x62, 0x39, 0x11, 0xa5, 0x94, 0xfa, 0xc6, 0xbc, 0x55, 0x83, 0x05, 0x50, 0xc2, 0xec, 0xa7, 0x51, 0x7f, 0x6d, 0xe3, + 0x4f, 0x1d, 0x5a, 0x17, 0x4c, 0xbd, 0x16, 0x30, 0x64, 0x86, 0x11, 0x2d, 0x58, 0x13, 0x69, 0x64, 0xe6, 0x8f, 0x33, + 0xb9, 0x90, 0xf3, 0x3e, 0xf3, 0xe7, 0x19, 0x9f, 0x73, 0xea, 0xb7, 0xd5, 0xe6, 0x4a, 0xce, 0xd0, 0x5f, 0xc5, 0xb3, + 0xb7, 0x97, 0x18, 0xdd, 0xf5, 0x0e, 0x62, 0x24, 0xf6, 0x95, 0x3e, 0x9c, 0xfe, 0x73, 0x77, 0x8e, 0x5c, 0x06, 0x3c, + 0x9e, 0x8b, 0x15, 0x67, 0x7e, 0x80, 0xf2, 0x34, 0xb7, 0x36, 0x4e, 0x10, 0xa9, 0xc9, 0xd2, 0xb8, 0xf2, 0x7c, 0x2f, + 0xe3, 0x60, 0x41, 0x76, 0x6c, 0x87, 0x3d, 0xd3, 0xf7, 0xb4, 0xd7, 0xd7, 0x1e, 0xc8, 0x88, 0x3f, 0x97, 0xbe, 0x9f, + 0x40, 0x94, 0xab, 0xad, 0xa7, 0x2a, 0xaa, 0xe6, 0x5e, 0x1e, 0xac, 0xf3, 0x2b, 0x2f, 0xfd, 0xc9, 0xd3, 0x4e, 0xf7, + 0xe5, 0x6e, 0xdd, 0x80, 0x4c, 0x71, 0xa2, 0xf6, 0xc9, 0xc7, 0x0c, 0x11, 0xc5, 0xc5, 0x6f, 0x5c, 0x4f, 0xef, 0x30, + 0xe1, 0x7b, 0x5f, 0x20, 0xcb, 0x47, 0x2a, 0x71, 0xc1, 0x7a, 0x4e, 0x53, 0x63, 0xf9, 0x2e, 0xbb, 0xfd, 0xa2, 0x6d, + 0x83, 0x31, 0xe9, 0xd1, 0x8c, 0xcf, 0xfa, 0x6f, 0xac, 0x03, 0x00, 0x16, 0x7f, 0x97, 0xf8, 0x4a, 0xac, 0xe6, 0xb9, + 0xc9, 0x12, 0x5b, 0x71, 0xd5, 0x78, 0x5e, 0x27, 0x72, 0x90, 0xdd, 0x64, 0x87, 0x08, 0x4d, 0x11, 0x31, 0xed, 0xa5, + 0x7a, 0xbd, 0x41, 0xdf, 0x41, 0x84, 0x85, 0x8a, 0xea, 0x4c, 0x3f, 0x69, 0x0a, 0x33, 0x46, 0xa7, 0x23, 0x40, 0xe5, + 0x80, 0xa4, 0x2f, 0x0f, 0xbe, 0x7d, 0x25, 0x64, 0x05, 0xee, 0x72, 0xe7, 0xb2, 0x90, 0x02, 0xfb, 0xf0, 0xa1, 0xe9, + 0x6f, 0xcf, 0xf3, 0x3c, 0x57, 0x59, 0x3c, 0x8f, 0x7f, 0x92, 0x1c, 0x26, 0x26, 0xda, 0x1a, 0x45, 0x3c, 0xd1, 0x02, + 0x6f, 0x7f, 0xd1, 0x14, 0x09, 0x57, 0x95, 0xc2, 0x8a, 0x02, 0xbd, 0x6a, 0x74, 0x49, 0x50, 0xbc, 0x2b, 0x93, 0x40, + 0x67, 0x70, 0x5d, 0x32, 0x58, 0xb0, 0x3b, 0x15, 0xd2, 0x73, 0x6a, 0x2e, 0xd8, 0xce, 0x04, 0x78, 0x7d, 0x48, 0xe5, + 0xc6, 0x2c, 0x55, 0x48, 0x59, 0x54, 0xe7, 0x05, 0x78, 0xdb, 0xe3, 0xa0, 0xd5, 0x89, 0xc2, 0xde, 0x6b, 0x01, 0x68, + 0xc0, 0xf2, 0xb9, 0xcd, 0x03, 0x5b, 0x72, 0x80, 0xa6, 0x41, 0xd3, 0x31, 0x80, 0xec, 0xef, 0x7c, 0x95, 0xf4, 0xbf, + 0xbd, 0xe3, 0x4f, 0xeb, 0x16, 0x0c, 0x21, 0x63, 0x36, 0x4f, 0x9f, 0xb5, 0x68, 0x08, 0x14, 0x6f, 0xa3, 0x48, 0xc4, + 0x9f, 0x59, 0xab, 0x2a, 0x0d, 0x0c, 0xb9, 0x0e, 0x83, 0x5a, 0xa5, 0x9d, 0xcc, 0x99, 0x96, 0x59, 0xa9, 0x53, 0xb5, + 0xb0, 0x7b, 0x80, 0x0a, 0x3c, 0xa8, 0xde, 0x4b, 0x0d, 0x2a, 0x07, 0x18, 0x8a, 0xe2, 0xa2, 0x0c, 0x9d, 0x8a, 0x7b, + 0xfa, 0x3e, 0x4f, 0xb1, 0x09, 0xff, 0xf9, 0xb2, 0xf2, 0x8d, 0xe7, 0x20, 0x5e, 0x4a, 0xff, 0xb9, 0x53, 0x7e, 0x8c, + 0xbc, 0x86, 0xfa, 0xea, 0x2a, 0x0a, 0x73, 0x3c, 0x62, 0xbc, 0xb3, 0x31, 0x34, 0x02, 0xb1, 0x94, 0xe2, 0x09, 0xe1, + 0xc5, 0x36, 0x0a, 0x3e, 0x87, 0x0a, 0xb4, 0x19, 0x01, 0x58, 0xb8, 0xbb, 0x16, 0xfc, 0xcb, 0xd7, 0x90, 0xbf, 0x57, + 0x3c, 0xa2, 0xa9, 0x4c, 0x6e, 0x57, 0xa7, 0xec, 0x6b, 0xb8, 0xc2, 0xe8, 0xed, 0xfb, 0xbe, 0x5e, 0xe6, 0xeb, 0x4e, + 0xff, 0xe1, 0x13, 0x3e, 0x76, 0xa7, 0xfd, 0x65, 0xe7, 0x61, 0x9d, 0x91, 0x7f, 0x3c, 0x36, 0x79, 0x1b, 0xd6, 0xb4, + 0x1b, 0xec, 0x65, 0x8f, 0x89, 0xc3, 0x4c, 0x3c, 0x14, 0xe3, 0xdf, 0x16, 0x15, 0x38, 0xe6, 0x85, 0x06, 0x52, 0x6b, + 0x7f, 0x4e, 0x1f, 0xff, 0xaa, 0xb0, 0x43, 0xcc, 0x0e, 0xac, 0x04, 0x81, 0xee, 0x7b, 0x05, 0x6e, 0x29, 0x5d, 0x0a, + 0xb4, 0xf5, 0x68, 0x51, 0x2e, 0xae, 0xef, 0x01, 0x21, 0xb5, 0xb6, 0x59, 0xd5, 0x65, 0xa2, 0x71, 0x29, 0x8e, 0x57, + 0xa7, 0x3e, 0xfa, 0x03, 0x2d, 0xf6, 0xd9, 0xc5, 0x16, 0x9a, 0x5a, 0x88, 0x33, 0x71, 0x36, 0x2e, 0xb8, 0x19, 0x37, + 0xeb, 0xce, 0x81, 0x0a, 0x7d, 0x35, 0x18, 0xca, 0x92, 0xd0, 0xda, 0xc0, 0x10, 0x4c, 0x2b, 0x37, 0x58, 0x14, 0x25, + 0xdd, 0x51, 0x2f, 0x8e, 0xb6, 0x0d, 0x2e, 0xdc, 0x46, 0x8c, 0x72, 0xbc, 0x65, 0x7f, 0x81, 0x2a, 0x61, 0xfe, 0x92, + 0x59, 0xe4, 0x5d, 0x93, 0xce, 0x9f, 0x23, 0x3b, 0x10, 0xb3, 0xd4, 0x96, 0xb5, 0x5a, 0x85, 0x9b, 0x09, 0xf9, 0xa6, + 0x7b, 0x8a, 0x42, 0x81, 0x6d, 0x53, 0xb9, 0x8b, 0xff, 0x3d, 0xe2, 0x6a, 0xe7, 0x1c, 0xf9, 0x37, 0x9b, 0xe6, 0xb9, + 0x9e, 0x89, 0x03, 0xa2, 0x9c, 0xae, 0x82, 0x99, 0xc3, 0x70, 0xc7, 0x3d, 0xf5, 0xf3, 0x22, 0x3d, 0xcf, 0xa8, 0x63, + 0x25, 0xb7, 0xef, 0xd4, 0x2f, 0xfc, 0x52, 0x3c, 0xf6, 0x0b, 0x7d, 0x61, 0xcf, 0x95, 0x78, 0x4b, 0xb7, 0x4f, 0x4e, + 0xa2, 0xd5, 0xa8, 0x9c, 0x9a, 0x76, 0xe1, 0x25, 0xd4, 0x6c, 0x6f, 0x68, 0xaf, 0xcc, 0x2d, 0x7a, 0xd9, 0x1d, 0xee, + 0x57, 0x76, 0x8a, 0x22, 0x76, 0xa5, 0x60, 0x9f, 0x56, 0xd2, 0xe3, 0x4a, 0x9c, 0x73, 0xa1, 0xb3, 0x4e, 0x2d, 0xa0, + 0x28, 0xcb, 0x00, 0x2b, 0x2f, 0x15, 0xfa, 0x6d, 0xc5, 0x13, 0x94, 0x1c, 0xa6, 0x36, 0x1b, 0x47, 0xcf, 0x7a, 0x49, + 0x25, 0xf7, 0xd5, 0x06, 0xf7, 0x92, 0x91, 0xd6, 0xde, 0xe1, 0xf2, 0xce, 0x56, 0x1f, 0xf1, 0xb4, 0x87, 0xfb, 0x8f, + 0x59, 0x81, 0x7f, 0xab, 0xca, 0xfb, 0x86, 0x31, 0x14, 0x02, 0xb5, 0xce, 0xa5, 0xd4, 0xb4, 0x49, 0xb8, 0x64, 0x10, + 0xee, 0x1d, 0xac, 0x11, 0x51, 0x27, 0xb0, 0xbf, 0x46, 0xc9, 0x6a, 0x67, 0xc0, 0xe7, 0x49, 0x88, 0x98, 0x13, 0xe5, + 0xb1, 0x7f, 0x7e, 0xb7, 0xb2, 0xc9, 0x9f, 0x98, 0x43, 0x9d, 0xa1, 0x4a, 0x58, 0x87, 0xf8, 0x83, 0xb8, 0x79, 0xd5, + 0xeb, 0xdb, 0x25, 0xca, 0xdf, 0x4c, 0x4f, 0x2c, 0xcc, 0x0a, 0x2b, 0xf8, 0x4b, 0x29, 0x5f, 0xdf, 0xf1, 0x01, 0xd4, + 0x67, 0x93, 0x1f, 0xff, 0xbc, 0xa1, 0x7b, 0xd9, 0x95, 0xff, 0x72, 0x87, 0x40, 0x31, 0xed, 0xe2, 0x70, 0x8e, 0x4b, + 0x87, 0xc2, 0xec, 0x02, 0xc3, 0xe2, 0x45, 0x55, 0x1d, 0xf0, 0xe1, 0x39, 0xe2, 0xe3, 0xf3, 0x24, 0x2e, 0x88, 0x84, + 0xd2, 0xfc, 0x79, 0x50, 0x37, 0xa3, 0xe3, 0xc2, 0x86, 0x3e, 0x38, 0x9c, 0xd1, 0x00, 0x84, 0xac, 0xb1, 0xc9, 0xf6, + 0x63, 0x95, 0x52, 0x99, 0x59, 0x3a, 0x72, 0xc7, 0xc6, 0x76, 0xb5, 0xd4, 0xe3, 0xbb, 0x7e, 0x1f, 0xee, 0x64, 0xd3, + 0xb2, 0x7e, 0xca, 0x10, 0xfb, 0xa8, 0x0b, 0xc3, 0x05, 0xc8, 0xda, 0x0f, 0xb9, 0xf6, 0x62, 0x19, 0xdb, 0x80, 0x3e, + 0xc4, 0xfe, 0xa7, 0x5d, 0x9c, 0xd1, 0xad, 0xf0, 0xb1, 0x58, 0xb4, 0x3a, 0xdb, 0x90, 0x24, 0x07, 0x46, 0x73, 0x48, + 0x30, 0x6e, 0x44, 0x68, 0x1b, 0x94, 0xe0, 0xb1, 0x62, 0x0a, 0x37, 0xe2, 0xfe, 0x38, 0x58, 0x68, 0x55, 0xd1, 0x8d, + 0xb9, 0x8d, 0x6d, 0x14, 0x67, 0xf0, 0xf5, 0x80, 0x7f, 0x15, 0x65, 0x7c, 0x80, 0xbb, 0x41, 0xe4, 0xce, 0x9e, 0x97, + 0x92, 0x48, 0x2d, 0xa7, 0xdb, 0x56, 0x6c, 0xe7, 0x97, 0xd8, 0xed, 0x82, 0x3d, 0x5f, 0x16, 0xe6, 0xcc, 0xd6, 0x20, + 0x33, 0x8c, 0xbd, 0xb7, 0xc0, 0xbb, 0x6d, 0x29, 0x4c, 0x76, 0xe9, 0x1b, 0xa8, 0x84, 0x56, 0xe7, 0xa3, 0xc3, 0x78, + 0x53, 0x07, 0xae, 0xe2, 0x78, 0x1a, 0xdb, 0x56, 0x62, 0x34, 0x46, 0x1e, 0xc9, 0x30, 0x95, 0xe1, 0x10, 0x7f, 0x24, + 0xbb, 0x29, 0x37, 0xd3, 0xa5, 0xce, 0x2e, 0x0d, 0x10, 0x74, 0x85, 0x55, 0x0f, 0x85, 0x24, 0x11, 0x70, 0xcb, 0x15, + 0xc8, 0xc3, 0x70, 0x3f, 0xaf, 0xc6, 0xc8, 0xe1, 0x6c, 0x81, 0xd0, 0xf0, 0xb2, 0x51, 0x90, 0x39, 0xf1, 0xed, 0x21, + 0xf1, 0x72, 0x6a, 0x7a, 0xc7, 0x11, 0x0f, 0x86, 0xea, 0x36, 0x0f, 0x5e, 0x8e, 0xaa, 0x4e, 0x67, 0xe9, 0x91, 0x70, + 0x4b, 0xe7, 0x59, 0x49, 0xca, 0x51, 0x35, 0x05, 0x7a, 0x8e, 0x34, 0x1a, 0xca, 0x5b, 0x5b, 0x29, 0x81, 0x55, 0xd0, + 0x32, 0x39, 0xfd, 0xbc, 0x23, 0x12, 0x91, 0x70, 0x21, 0xa0, 0xf8, 0xcb, 0x28, 0xa4, 0xd1, 0x5b, 0xcd, 0xc7, 0x30, + 0xc8, 0x70, 0x9d, 0x57, 0x5c, 0x0a, 0xfe, 0xf8, 0xc5, 0x01, 0xf4, 0x50, 0x94, 0x0d, 0xdc, 0x2f, 0x16, 0xfe, 0xcc, + 0x2e, 0x46, 0xf4, 0x36, 0x9b, 0xa1, 0xba, 0xcd, 0xe8, 0x27, 0xd6, 0xe7, 0x95, 0x22, 0x76, 0xcf, 0x0e, 0xed, 0xc1, + 0x8e, 0x19, 0x59, 0x5d, 0x25, 0x1d, 0xcd, 0x04, 0x99, 0xf4, 0x32, 0xf2, 0x0c, 0x41, 0x84, 0x0e, 0xd8, 0x27, 0x87, + 0x36, 0x4a, 0x87, 0xf9, 0x0a, 0xc2, 0x3f, 0xc7, 0x7c, 0x0e, 0x89, 0x6e, 0x76, 0x09, 0x8e, 0x7a, 0x8d, 0x48, 0x3a, + 0x89, 0x50, 0x04, 0x5e, 0xc8, 0x7a, 0x22, 0x98, 0x78, 0x41, 0xef, 0x26, 0xb8, 0xe2, 0xc5, 0xd1, 0x4d, 0x07, 0xcb, + 0x61, 0x46, 0xfc, 0x1b, 0x13, 0x46, 0xee, 0x12, 0xe2, 0xdb, 0x03, 0x84, 0x3b, 0xd8, 0x29, 0x88, 0xea, 0xe5, 0x56, + 0x9b, 0x5e, 0x9c, 0xa2, 0x9e, 0xc7, 0xf9, 0x78, 0x36, 0xd6, 0x91, 0x17, 0x72, 0x38, 0x5b, 0xc4, 0x30, 0x40, 0x16, + 0xc2, 0x93, 0x9b, 0x76, 0x97, 0x10, 0x90, 0x9c, 0x4c, 0x65, 0x59, 0xde, 0xc4, 0x4d, 0x27, 0x60, 0x91, 0xe7, 0x76, + 0x69, 0x4a, 0xf1, 0xf4, 0x9f, 0xaa, 0xb1, 0x0d, 0x67, 0x8b, 0x8c, 0x63, 0xd0, 0x62, 0x95, 0xce, 0x20, 0x10, 0x17, + 0xe5, 0x46, 0x4b, 0x2f, 0x0e, 0x73, 0xb8, 0xf9, 0xf7, 0xb8, 0xbc, 0x8d, 0x09, 0x20, 0x1f, 0xbc, 0x49, 0x3a, 0x40, + 0xcf, 0xf2, 0x7c, 0xce, 0xd0, 0x4b, 0x6f, 0x73, 0x91, 0x4c, 0x84, 0xff, 0xd3, 0xc9, 0x47, 0xa2, 0x1c, 0xe9, 0x15, + 0x72, 0x9c, 0x50, 0x51, 0xb2, 0x9d, 0x10, 0xd5, 0xcb, 0xc3, 0x7f, 0x61, 0xd5, 0x11, 0x02, 0xe7, 0xdb, 0x84, 0x2f, + 0x5f, 0x6e, 0xf9, 0xc1, 0xd7, 0x97, 0xec, 0x44, 0x28, 0xe5, 0x1f, 0x18, 0x87, 0x98, 0x56, 0x32, 0xb1, 0x63, 0x40, + 0x64, 0x7a, 0x58, 0xc0, 0x72, 0xe0, 0x66, 0xe4, 0xf1, 0xe3, 0xd6, 0x38, 0xd3, 0x14, 0x9f, 0xab, 0xff, 0x9f, 0xd8, + 0x7a, 0x10, 0xd7, 0x6e, 0x2f, 0x8d, 0x48, 0x62, 0x1a, 0xa3, 0x01, 0xf3, 0x8a, 0x06, 0x68, 0x9c, 0x94, 0x01, 0xc3, + 0x5e, 0x59, 0xfa, 0x85, 0x1e, 0x63, 0x93, 0x47, 0xaa, 0x99, 0x98, 0x1f, 0x21, 0x64, 0xbb, 0x46, 0xc1, 0x44, 0x12, + 0x8c, 0xf6, 0x2d, 0x50, 0xd8, 0x81, 0x14, 0x53, 0xdd, 0x01, 0xf9, 0x9c, 0xcb, 0xc8, 0x6b, 0x20, 0x1b, 0x7d, 0xbe, + 0xb9, 0xd7, 0xaf, 0x13, 0xfb, 0xd2, 0xa3, 0x39, 0x84, 0x48, 0x23, 0xf2, 0xfb, 0xf0, 0xfe, 0x58, 0xaf, 0x99, 0x77, + 0xbd, 0xca, 0x14, 0x2f, 0xc1, 0xc8, 0x07, 0x37, 0x96, 0x90, 0x0f, 0x1a, 0xac, 0x02, 0x73, 0xf2, 0x35, 0xaa, 0xb5, + 0x43, 0xcc, 0xce, 0xf3, 0x26, 0x47, 0xde, 0x76, 0x75, 0x54, 0x51, 0x58, 0xad, 0xc0, 0xf9, 0x55, 0x03, 0xad, 0xc4, + 0x07, 0xf2, 0x2f, 0x43, 0xa2, 0x8a, 0x09, 0x61, 0x80, 0x1e, 0x19, 0xe7, 0x1f, 0x84, 0x28, 0xe8, 0x32, 0xa9, 0x5a, + 0x36, 0xfb, 0x97, 0x9a, 0xc3, 0x55, 0x60, 0x04, 0xec, 0x36, 0xa6, 0x31, 0x8d, 0xe7, 0xe3, 0x28, 0x66, 0xd6, 0xbc, + 0x2b, 0x89, 0xaf, 0x70, 0x2e, 0x08, 0x2a, 0xac, 0xe1, 0xbe, 0xcb, 0xff, 0xfd, 0x7c, 0xfc, 0x90, 0x97, 0x62, 0xe7, + 0xd7, 0xe5, 0x1a, 0xfa, 0x61, 0xff, 0x75, 0x29, 0x56, 0xbd, 0x49, 0x2d, 0x7a, 0x37, 0x9a, 0x36, 0x8e, 0xff, 0x7c, + 0x76, 0xb1, 0x91, 0x4e, 0xef, 0x78, 0xcb, 0x7b, 0xd0, 0x37, 0xa7, 0xe9, 0x69, 0x5c, 0xe0, 0xe7, 0x2c, 0x2f, 0x67, + 0xff, 0x95, 0xbb, 0x94, 0xc7, 0xf5, 0x7b, 0x76, 0xdd, 0xa1, 0x39, 0xad, 0xbd, 0xb1, 0xec, 0xdd, 0xb3, 0x2b, 0xfe, + 0x1e, 0x81, 0x2c, 0xbe, 0x08, 0xc9, 0xa4, 0x52, 0x09, 0x20, 0xd0, 0x5c, 0x0f, 0x7e, 0xf7, 0xc4, 0x28, 0xa5, 0x1e, + 0xef, 0x3f, 0x26, 0x5f, 0x95, 0x75, 0xb8, 0x3b, 0xb7, 0x40, 0xd6, 0x23, 0xfd, 0x3b, 0x4f, 0x37, 0xba, 0x5f, 0xd0, + 0xa8, 0x3a, 0x75, 0x90, 0x19, 0x8d, 0x33, 0x2d, 0x0d, 0xf9, 0xb7, 0x8d, 0xe6, 0x8c, 0xc2, 0xb7, 0x82, 0x46, 0x74, + 0x13, 0xe1, 0x1f, 0x57, 0x8d, 0x03, 0x4a, 0x0a, 0xf8, 0x61, 0x9b, 0xf6, 0x6d, 0xf7, 0x72, 0x2f, 0xa4, 0xa9, 0xf2, + 0xcb, 0x33, 0x16, 0x18, 0xb4, 0x0f, 0x74, 0x66, 0x47, 0xff, 0x7f, 0x0a, 0x68, 0xbd, 0x88, 0x51, 0xb2, 0x95, 0x3a, + 0x40, 0x5c, 0x6c, 0xe3, 0xe6, 0x0b, 0xbd, 0x71, 0x9a, 0x0b, 0x67, 0x1e, 0xf5, 0xe8, 0x24, 0xdd, 0x02, 0x18, 0xd5, + 0xfc, 0x7e, 0xc4, 0xab, 0x53, 0x57, 0x46, 0x7c, 0x54, 0xbc, 0xa3, 0xbb, 0x0b, 0xcc, 0xf6, 0xbf, 0xf2, 0x2e, 0x46, + 0x34, 0x7f, 0xf7, 0x11, 0xe8, 0x86, 0x1f, 0xb3, 0xd3, 0x37, 0x9f, 0xf9, 0xe3, 0x03, 0x3e, 0x0c, 0xed, 0x1e, 0xa3, + 0x79, 0x67, 0xdc, 0x9a, 0x27, 0x3c, 0x31, 0xc8, 0x0c, 0xe0, 0xb2, 0xcf, 0xde, 0x7b, 0x2c, 0xe3, 0xc0, 0x77, 0x20, + 0x56, 0x26, 0xf3, 0x16, 0x30, 0x29, 0x17, 0x23, 0xa4, 0x35, 0x32, 0xfa, 0x37, 0xe0, 0x45, 0xc9, 0xe8, 0x9f, 0xce, + 0x3d, 0x8a, 0x6e, 0x48, 0xf4, 0xc9, 0x93, 0x01, 0xcb, 0x3a, 0x28, 0x5a, 0x62, 0x52, 0x21, 0x3a, 0x84, 0x2c, 0x13, + 0xa0, 0xf4, 0x49, 0xa0, 0xa1, 0xf0, 0x77, 0x2d, 0x27, 0xbd, 0x9f, 0x7b, 0x66, 0x82, 0xa4, 0xc7, 0xe4, 0x28, 0x8d, + 0x4c, 0x18, 0xf9, 0x73, 0xcd, 0xcb, 0xeb, 0xeb, 0xa7, 0x76, 0x7b, 0xd0, 0x7c, 0x64, 0xbf, 0x95, 0xe6, 0xc4, 0xe4, + 0x6b, 0xad, 0x06, 0x2b, 0x79, 0x03, 0x28, 0x9b, 0x7d, 0x41, 0x2b, 0x60, 0xf1, 0x5b, 0x0d, 0x61, 0xe9, 0x99, 0x0c, + 0xb4, 0x06, 0x4e, 0xd2, 0x73, 0x36, 0xb8, 0x6e, 0x98, 0x1f, 0x91, 0x5e, 0xaf, 0x98, 0xa8, 0x32, 0xa7, 0x27, 0x7d, + 0xba, 0xb9, 0x1e, 0x7b, 0xb1, 0xd0, 0x87, 0xd4, 0x13, 0xfa, 0x93, 0x17, 0xe1, 0x6c, 0xf9, 0xb9, 0xec, 0x3f, 0x4d, + 0x20, 0x75, 0xd5, 0x18, 0x2d, 0x74, 0x7e, 0x3d, 0xbe, 0x9b, 0x35, 0x3e, 0x1a, 0xd9, 0xea, 0x6d, 0xbb, 0x73, 0x64, + 0xb9, 0x77, 0x8b, 0x59, 0x5f, 0x42, 0x3e, 0xa3, 0x58, 0x33, 0x99, 0x83, 0x9c, 0x23, 0xb4, 0xbf, 0xd6, 0x95, 0xe4, + 0xb8, 0xf6, 0x61, 0x4e, 0x41, 0x7a, 0x6c, 0x0d, 0xeb, 0x20, 0x6a, 0xbe, 0xad, 0x7d, 0x06, 0x2d, 0xbf, 0x9e, 0x7a, + 0x9d, 0x16, 0x4c, 0xf2, 0xa4, 0x73, 0x5f, 0xf7, 0x8f, 0x34, 0xe2, 0x5e, 0x7a, 0x59, 0x13, 0x45, 0xb7, 0x48, 0x40, + 0xd7, 0x2a, 0x2d, 0xf4, 0xb2, 0xe2, 0x3c, 0xad, 0xe8, 0x4f, 0x33, 0xe6, 0x51, 0xc9, 0xaa, 0x51, 0xa9, 0x9e, 0x5c, + 0x63, 0x9c, 0x29, 0xeb, 0x09, 0x20, 0x17, 0x45, 0x02, 0xc7, 0x59, 0x6f, 0xd7, 0xa7, 0x4b, 0x43, 0x07, 0xf1, 0xd1, + 0xdb, 0xb8, 0xe9, 0xbc, 0x83, 0x69, 0x2c, 0xdd, 0x9f, 0x48, 0x67, 0x19, 0xc3, 0x89, 0x2a, 0x4b, 0xf2, 0xb4, 0x1c, + 0x85, 0xba, 0xa3, 0xbb, 0x20, 0x29, 0x4b, 0xf6, 0x46, 0x3b, 0xfb, 0xe3, 0x7a, 0xf2, 0x28, 0xfb, 0x30, 0xec, 0xa1, + 0x0a, 0xdc, 0x43, 0xaa, 0xef, 0x72, 0xff, 0xba, 0xcc, 0x94, 0xa6, 0xc1, 0xfe, 0xc7, 0xd7, 0xa1, 0x03, 0x3f, 0x0e, + 0x6e, 0xc7, 0x11, 0x12, 0x28, 0xb7, 0x98, 0xa6, 0x0c, 0x5b, 0x4e, 0x30, 0xd9, 0xee, 0x0d, 0x37, 0xc5, 0xd5, 0x9e, + 0x4b, 0x14, 0x83, 0x25, 0xf7, 0xc0, 0xcb, 0x67, 0xb4, 0x7f, 0x62, 0xeb, 0x26, 0xe6, 0xa9, 0x6b, 0xe1, 0xa3, 0xd4, + 0xc2, 0x34, 0x74, 0x60, 0xb0, 0xc8, 0x59, 0x92, 0x8c, 0x04, 0xbb, 0xfc, 0xd2, 0x6a, 0x27, 0xcf, 0x72, 0x25, 0xd3, + 0xd7, 0x5e, 0x4f, 0x4c, 0x31, 0xd2, 0xb0, 0xa5, 0xed, 0x70, 0xd8, 0xc9, 0x79, 0x02, 0x22, 0x44, 0x54, 0xcf, 0x97, + 0xb8, 0xa6, 0xbe, 0x10, 0x67, 0xdd, 0xf9, 0x32, 0x56, 0xb4, 0xe7, 0x41, 0x01, 0x08, 0xad, 0x36, 0xad, 0x54, 0x17, + 0xdc, 0xd0, 0x23, 0x48, 0x77, 0xeb, 0xe5, 0x1d, 0x14, 0x55, 0xcd, 0xf4, 0x60, 0xd2, 0x8b, 0x1f, 0xe7, 0x5d, 0xe1, + 0x61, 0x16, 0x19, 0x2a, 0x80, 0x1b, 0xa3, 0xef, 0xe0, 0x72, 0x7d, 0xcf, 0x43, 0xb8, 0xb5, 0xe6, 0x4c, 0xcf, 0x4f, + 0x5b, 0x8f, 0x78, 0xf1, 0xe6, 0x61, 0x1c, 0xc2, 0x5d, 0x6c, 0x7d, 0xfa, 0x24, 0x5f, 0x3b, 0x6c, 0xe7, 0xd1, 0xa2, + 0xd0, 0xd6, 0xe5, 0xd4, 0xf6, 0xe2, 0xce, 0xe7, 0xf9, 0x27, 0xb5, 0x05, 0xca, 0x0a, 0xbc, 0xf6, 0xea, 0x3e, 0x22, + 0x14, 0x73, 0x07, 0xdf, 0xfe, 0x2f, 0x1d, 0xb5, 0xdd, 0x7c, 0xde, 0x0e, 0x67, 0x46, 0x0f, 0xcf, 0x48, 0x88, 0xba, + 0x3c, 0xd8, 0x24, 0xd7, 0xaf, 0xfe, 0xe9, 0x29, 0x7e, 0xa5, 0x9d, 0xe6, 0x5f, 0x73, 0xce, 0x0b, 0x63, 0x53, 0x3e, + 0xdb, 0x47, 0x9a, 0x30, 0xba, 0x46, 0x84, 0xcb, 0xef, 0xdb, 0xd0, 0x4a, 0x83, 0x8c, 0x48, 0x08, 0x79, 0xbd, 0x75, + 0x05, 0xb8, 0xef, 0x2f, 0xdb, 0x1d, 0xbc, 0xa5, 0x44, 0xe2, 0x8d, 0xea, 0x38, 0x6e, 0xcf, 0xc8, 0xc2, 0xf5, 0xfd, + 0x5b, 0x07, 0x82, 0x7d, 0xad, 0x7d, 0x25, 0xbf, 0xdc, 0x39, 0x7a, 0x01, 0x06, 0x94, 0x30, 0x84, 0x27, 0x51, 0xff, + 0x97, 0xd8, 0x88, 0xd4, 0x6d, 0xc6, 0x74, 0xc2, 0x84, 0xfd, 0x59, 0xd1, 0xaa, 0xad, 0xf4, 0x00, 0x28, 0xa6, 0x4e, + 0xae, 0x06, 0x51, 0x74, 0x87, 0x26, 0xe2, 0x8e, 0x39, 0x5a, 0xde, 0x13, 0x9a, 0xb5, 0x40, 0x15, 0x4e, 0x61, 0xcf, + 0xa3, 0x50, 0x9a, 0xe1, 0x19, 0xf4, 0x01, 0xf6, 0x52, 0x84, 0x9c, 0xb9, 0x24, 0x79, 0xe6, 0xc0, 0x6b, 0x13, 0x04, + 0x69, 0x74, 0xb7, 0x7a, 0x4a, 0xb1, 0x8b, 0x6c, 0xb7, 0x20, 0xe9, 0xcd, 0x22, 0x74, 0x2b, 0x56, 0x49, 0x8a, 0xbb, + 0x99, 0x8a, 0xad, 0x0e, 0x1e, 0x61, 0x8f, 0x48, 0xdf, 0x96, 0xfd, 0xbd, 0x75, 0xc0, 0x42, 0x17, 0x45, 0x4d, 0x4a, + 0xed, 0xbf, 0x29, 0x1d, 0x38, 0xa1, 0x86, 0x09, 0x05, 0x05, 0xfb, 0x6c, 0xdc, 0x62, 0xbc, 0x7b, 0x6b, 0x6d, 0x6f, + 0x21, 0xf0, 0x2a, 0x34, 0x37, 0xd5, 0x82, 0x5c, 0xe1, 0x0b, 0x64, 0xc9, 0xb5, 0x15, 0x42, 0xd7, 0x37, 0x2d, 0xbb, + 0xf0, 0xfc, 0xc2, 0xf4, 0xc7, 0x56, 0x29, 0xea, 0x52, 0x90, 0x4b, 0x38, 0xb5, 0xb2, 0x46, 0x57, 0x1f, 0xd8, 0x9a, + 0x8e, 0x51, 0xbb, 0x33, 0xce, 0x5e, 0x21, 0x90, 0xfc, 0x89, 0x4a, 0x9d, 0x53, 0x9a, 0x11, 0x18, 0x5e, 0x0f, 0x8a, + 0xd5, 0x2f, 0xb9, 0x16, 0x30, 0x0e, 0x0f, 0xf4, 0xc7, 0xa0, 0x48, 0x9e, 0x64, 0x62, 0x0e, 0x03, 0x4f, 0xe5, 0xb0, + 0x73, 0xcf, 0xe9, 0x4e, 0xe6, 0xf7, 0xbe, 0xb1, 0xb7, 0xc7, 0xae, 0xe3, 0x96, 0x31, 0x3f, 0x8c, 0x20, 0x6a, 0x25, + 0xc2, 0x48, 0x45, 0x1e, 0x31, 0x80, 0x12, 0x4e, 0xae, 0x1b, 0x70, 0xa8, 0xa9, 0x36, 0xdc, 0xa7, 0xe8, 0x08, 0xcc, + 0xa9, 0xcb, 0x34, 0xaa, 0x39, 0x55, 0x99, 0x20, 0x84, 0xcf, 0x8d, 0x5b, 0xe7, 0x78, 0x02, 0x33, 0xed, 0x80, 0xd5, + 0x26, 0xaf, 0x53, 0x1c, 0x84, 0xcc, 0xd4, 0x9d, 0x2d, 0x1a, 0x13, 0x49, 0x4d, 0xb5, 0x4b, 0xad, 0x05, 0xe3, 0x64, + 0xb3, 0x6b, 0xd4, 0x6e, 0x2b, 0x32, 0xb8, 0x88, 0x15, 0x0f, 0x64, 0x04, 0x38, 0xba, 0x96, 0x6b, 0x94, 0x27, 0x47, + 0x5a, 0x10, 0xe6, 0x26, 0x39, 0x8e, 0x98, 0xb6, 0x7f, 0xdc, 0x8d, 0xe8, 0x66, 0x9e, 0x99, 0x8a, 0xc3, 0x5f, 0xbd, + 0xe7, 0xb6, 0x5e, 0x59, 0x2a, 0xd6, 0xf3, 0x2c, 0x25, 0xeb, 0x95, 0xcf, 0x2c, 0xa5, 0x21, 0xb9, 0xb0, 0x16, 0xd8, + 0x6c, 0x9a, 0xa5, 0xd9, 0x72, 0x7a, 0xde, 0xb9, 0x45, 0x66, 0x5e, 0xf0, 0x08, 0x53, 0xde, 0xae, 0xbc, 0x44, 0x67, + 0x03, 0xf6, 0x3f, 0xfb, 0x7c, 0x09, 0x9a, 0x19, 0x2b, 0x34, 0xc7, 0xbb, 0xc2, 0x1c, 0x12, 0x59, 0x61, 0xd4, 0x8f, + 0x4b, 0xf9, 0xec, 0x5d, 0x70, 0xda, 0x6a, 0xe7, 0x46, 0x05, 0x85, 0xef, 0x4d, 0x52, 0x60, 0x22, 0x09, 0x6c, 0x72, + 0x34, 0xee, 0x83, 0xf3, 0xac, 0x9c, 0xe9, 0x97, 0x03, 0x04, 0xff, 0x89, 0x6d, 0xc6, 0x35, 0x27, 0x30, 0x77, 0x06, + 0x77, 0x4a, 0xa8, 0x6e, 0x88, 0xe1, 0xf5, 0xd9, 0x75, 0x4e, 0x56, 0x1c, 0x73, 0x4b, 0xb2, 0x10, 0xe0, 0xb5, 0x07, + 0xb7, 0xcf, 0x33, 0x6b, 0x71, 0xa7, 0xe2, 0x34, 0xd4, 0x66, 0x5f, 0xfa, 0xcc, 0xd7, 0x83, 0x5f, 0x8d, 0x1c, 0x65, + 0x5c, 0xe0, 0x66, 0xd7, 0x8b, 0x81, 0x21, 0x34, 0x9e, 0x05, 0xe8, 0x11, 0x4f, 0xe9, 0xbf, 0x80, 0x10, 0xbf, 0x1b, + 0xfc, 0x2a, 0x33, 0x83, 0xd5, 0xd7, 0x2a, 0x06, 0x89, 0x9e, 0x64, 0x42, 0x81, 0x91, 0x61, 0xe8, 0xba, 0x2a, 0x8b, + 0x84, 0x37, 0xbc, 0xd8, 0xcd, 0xee, 0xcd, 0x98, 0x3f, 0x60, 0xa8, 0x43, 0xf8, 0x25, 0xb1, 0x27, 0xe6, 0x39, 0x9c, + 0x6a, 0xe6, 0x65, 0x76, 0x56, 0x45, 0x63, 0xbd, 0x59, 0xe3, 0x89, 0x09, 0xd5, 0x87, 0x68, 0xdb, 0x37, 0xc5, 0xdc, + 0x6e, 0xf7, 0xd6, 0x87, 0xd3, 0x44, 0x8d, 0x98, 0x99, 0x9a, 0x8f, 0xfb, 0xc6, 0x0a, 0x69, 0x33, 0x52, 0x64, 0x12, + 0xaa, 0x0c, 0x56, 0xc2, 0xc8, 0x3d, 0xbd, 0x6d, 0x75, 0x74, 0x5a, 0x00, 0x4e, 0x34, 0xcb, 0xdb, 0x4a, 0x64, 0xa3, + 0xbd, 0xb6, 0x1b, 0x85, 0xa8, 0x17, 0x3d, 0x9e, 0x51, 0x28, 0x15, 0x37, 0x34, 0x70, 0x6e, 0x06, 0x02, 0x4b, 0x3f, + 0xc5, 0x4b, 0xd8, 0x8b, 0xae, 0x3d, 0x6b, 0xc2, 0xb5, 0x51, 0x7b, 0x87, 0xb4, 0xac, 0x54, 0x4b, 0xd9, 0x77, 0x8e, + 0x74, 0xe3, 0x85, 0xaa, 0x97, 0xb9, 0xd0, 0xb9, 0xda, 0x4f, 0x7c, 0x6c, 0x1b, 0x23, 0x4d, 0xed, 0x9a, 0xfe, 0x66, + 0xce, 0x36, 0xd7, 0x99, 0xac, 0x90, 0x1f, 0x2c, 0x43, 0xfe, 0x04, 0xe9, 0xb6, 0x91, 0x4d, 0xac, 0xc4, 0xfa, 0x85, + 0x1f, 0xf0, 0x0e, 0x3a, 0x67, 0x2d, 0x3b, 0xb0, 0x36, 0xdb, 0x2e, 0x58, 0x26, 0x3f, 0x58, 0xae, 0x5d, 0xe3, 0x37, + 0x7c, 0x08, 0x57, 0xb2, 0x3a, 0x97, 0x9d, 0xec, 0x3d, 0xfe, 0x45, 0xfd, 0xf2, 0xfb, 0x19, 0x3d, 0x8b, 0x0f, 0x96, + 0x35, 0xde, 0x4c, 0x9f, 0xb2, 0x32, 0xfb, 0xc5, 0xed, 0x5b, 0x8b, 0x8f, 0x37, 0x97, 0x36, 0x38, 0x8f, 0x61, 0x68, + 0xef, 0xc5, 0xdd, 0x83, 0xfa, 0xc3, 0x70, 0x56, 0x4e, 0xd0, 0x6a, 0x18, 0x19, 0xe0, 0xce, 0xd6, 0xf3, 0x05, 0xbd, + 0xc7, 0xc6, 0x4c, 0x1f, 0xee, 0xf9, 0xd0, 0xbb, 0xfc, 0xc7, 0xcb, 0x7e, 0x24, 0x9c, 0x3d, 0x3a, 0xbb, 0x40, 0xd0, + 0x5a, 0xd7, 0x56, 0x4a, 0xf5, 0x98, 0xd7, 0x2e, 0x8e, 0xd0, 0x92, 0x3d, 0x2f, 0x75, 0x34, 0xff, 0xd0, 0x2a, 0x87, + 0x0d, 0x1a, 0x63, 0xf5, 0xbe, 0xd5, 0x96, 0x46, 0x6f, 0x3f, 0x10, 0x16, 0xa6, 0xa1, 0x52, 0x81, 0x80, 0x4a, 0xff, + 0xcc, 0x26, 0x5c, 0x7b, 0x9b, 0xc9, 0x28, 0x7d, 0x8a, 0x30, 0x7b, 0xd4, 0x93, 0xc5, 0xfb, 0x8e, 0x9d, 0xac, 0xd5, + 0x1b, 0xca, 0x74, 0x58, 0x69, 0x33, 0x59, 0xa9, 0x11, 0x46, 0x0c, 0x33, 0x9b, 0xe1, 0x85, 0x2a, 0xa7, 0xc3, 0xae, + 0x04, 0x81, 0xa9, 0xda, 0x78, 0xe2, 0xdc, 0xc1, 0xf3, 0xdf, 0xb2, 0x3e, 0x40, 0x8c, 0xe9, 0xe1, 0xc4, 0xde, 0x81, + 0x2e, 0xb5, 0x8b, 0x27, 0xfc, 0xcb, 0xdf, 0x92, 0x43, 0xb0, 0x42, 0xb5, 0xf1, 0xfd, 0x00, 0xd6, 0xd7, 0x20, 0xe6, + 0x6d, 0x77, 0xe2, 0xe0, 0x8e, 0x5d, 0x59, 0x3e, 0xcd, 0xcb, 0x03, 0x88, 0x52, 0x36, 0x72, 0xb3, 0x61, 0xcd, 0xaf, + 0x66, 0x16, 0xbb, 0x36, 0x9e, 0x1f, 0xc8, 0xfa, 0xb0, 0xcd, 0x9f, 0x0b, 0x90, 0xa2, 0x28, 0xd0, 0x96, 0xbb, 0xda, + 0xa2, 0x8d, 0x80, 0x69, 0xd7, 0x3a, 0x6f, 0x6d, 0x21, 0xeb, 0xa4, 0x76, 0xca, 0xa0, 0x2b, 0x65, 0x8a, 0x9c, 0x9a, + 0x51, 0x23, 0x44, 0xc7, 0xf8, 0x41, 0x0e, 0xfd, 0x62, 0xf5, 0xdd, 0xf5, 0x3b, 0x5d, 0x80, 0xb8, 0xe2, 0x54, 0xe6, + 0x59, 0x49, 0xac, 0x0f, 0x37, 0x79, 0xcf, 0x1b, 0xf4, 0xbf, 0xd4, 0x95, 0xef, 0xcb, 0xda, 0x13, 0x24, 0x03, 0x41, + 0x3a, 0x0e, 0xfe, 0x18, 0xc0, 0xf0, 0xc7, 0x06, 0x46, 0x2f, 0x7a, 0x78, 0x1e, 0x54, 0xbf, 0x76, 0xc2, 0x77, 0x96, + 0x5f, 0xaa, 0xd0, 0xfb, 0x49, 0xf5, 0x0b, 0x58, 0x5f, 0x83, 0xa0, 0x8e, 0x44, 0xcd, 0xef, 0x69, 0x5b, 0xf7, 0x2b, + 0x8c, 0x78, 0x91, 0x0f, 0x15, 0xf9, 0xeb, 0xba, 0xfa, 0x3c, 0x87, 0x01, 0x39, 0xf6, 0x09, 0x06, 0x36, 0xfd, 0xb2, + 0x0f, 0x21, 0x78, 0x5f, 0x5f, 0xd5, 0x42, 0xe3, 0x97, 0x22, 0x4e, 0x50, 0xe1, 0x81, 0x2c, 0x74, 0x3c, 0xb5, 0x72, + 0x6b, 0x1d, 0x99, 0x68, 0x6c, 0x62, 0x14, 0x3a, 0x8b, 0x15, 0x6c, 0xcc, 0x27, 0xa3, 0xba, 0xf2, 0x86, 0x09, 0x86, + 0x5f, 0xad, 0x3f, 0x9d, 0xa5, 0x57, 0x5b, 0x85, 0xbd, 0xaa, 0xf0, 0x5f, 0x75, 0x13, 0xbe, 0xc9, 0x70, 0x58, 0x05, + 0x2f, 0x08, 0x15, 0xfc, 0x40, 0x27, 0x55, 0xa8, 0xa3, 0xd3, 0x10, 0xa1, 0x55, 0xb3, 0x82, 0x1c, 0x15, 0xda, 0xef, + 0xdb, 0xd4, 0xd6, 0x9b, 0xea, 0xec, 0xed, 0x58, 0xd5, 0x54, 0x98, 0x1f, 0x8f, 0x59, 0x4d, 0x33, 0x12, 0x95, 0x2c, + 0xbf, 0x83, 0xdd, 0x69, 0x0b, 0x6f, 0x9f, 0xc0, 0xfb, 0x9b, 0xfa, 0x31, 0xe3, 0xb3, 0x6c, 0xd2, 0x04, 0xba, 0x33, + 0xd7, 0x02, 0xb5, 0x4f, 0x4d, 0xdd, 0x91, 0xb9, 0x0e, 0xec, 0x5d, 0xcd, 0x97, 0xf8, 0x4c, 0x84, 0xbb, 0x5f, 0x93, + 0xa8, 0xcc, 0x69, 0x06, 0x6d, 0x2c, 0xa5, 0x89, 0xaa, 0xdb, 0x70, 0xca, 0xb0, 0xf7, 0x0c, 0xed, 0x02, 0x6a, 0xf4, + 0x44, 0x77, 0x62, 0x8c, 0x90, 0xc6, 0xfd, 0x22, 0xb4, 0x1f, 0xe9, 0x79, 0x2b, 0x90, 0x8e, 0xed, 0x18, 0xa6, 0x9b, + 0x06, 0xc8, 0x5a, 0xe8, 0xe3, 0x5f, 0x5f, 0xed, 0xc3, 0xd8, 0xe6, 0xfd, 0x06, 0x61, 0xa9, 0xde, 0x1e, 0x1d, 0x20, + 0xf9, 0x9e, 0x52, 0x58, 0x5c, 0xd1, 0x1a, 0xad, 0x86, 0x8d, 0x83, 0x5c, 0x61, 0x30, 0xca, 0x54, 0xe9, 0x3c, 0x62, + 0x38, 0x1a, 0xc2, 0x08, 0x85, 0x42, 0x5e, 0x7d, 0xc4, 0x9a, 0x79, 0xdc, 0x9e, 0x3d, 0x94, 0x56, 0x07, 0xbf, 0x7a, + 0xb2, 0x46, 0x7d, 0xe9, 0x5d, 0x6e, 0xc6, 0x52, 0x8b, 0x8f, 0x57, 0xbc, 0xd1, 0xeb, 0xcb, 0x84, 0x66, 0x6e, 0xd1, + 0xa0, 0x14, 0x1b, 0x12, 0xbb, 0x95, 0xdf, 0x13, 0xeb, 0xb1, 0x59, 0x21, 0x09, 0x99, 0x5f, 0x5e, 0x99, 0xca, 0x53, + 0x79, 0x7f, 0x65, 0x39, 0xc3, 0x51, 0x3c, 0x78, 0x07, 0x7e, 0xd1, 0xcb, 0x9f, 0xa4, 0xde, 0xaa, 0x6e, 0x4b, 0x1b, + 0x14, 0xb5, 0x73, 0xcb, 0x86, 0x73, 0xe1, 0x3a, 0x29, 0x54, 0xc1, 0x0d, 0x16, 0x49, 0x23, 0x6f, 0x1d, 0x2f, 0x3e, + 0xc5, 0x60, 0xca, 0xc2, 0x19, 0x94, 0xb5, 0xcc, 0x05, 0xd6, 0x68, 0x1f, 0x86, 0x67, 0x8b, 0xcc, 0x18, 0x33, 0x18, + 0xdb, 0x70, 0x6e, 0xf9, 0xac, 0xfb, 0xfa, 0x85, 0xe0, 0xfd, 0xc6, 0x48, 0x44, 0x2c, 0x1f, 0xa0, 0x0f, 0x06, 0xa4, + 0x7f, 0x59, 0x62, 0xe4, 0xc3, 0x73, 0x05, 0x7e, 0xd2, 0xb2, 0x70, 0x00, 0x36, 0x6b, 0xef, 0x30, 0x2e, 0x92, 0x79, + 0xab, 0xdb, 0x31, 0x3b, 0x04, 0x37, 0x6c, 0x8d, 0x22, 0x18, 0x15, 0xa3, 0x25, 0x18, 0xac, 0xa0, 0x21, 0xb8, 0x80, + 0xf3, 0x75, 0xc4, 0xaa, 0xc7, 0x29, 0x2e, 0x33, 0x75, 0x86, 0x7f, 0x76, 0x37, 0xcd, 0xb2, 0x1a, 0xc4, 0x07, 0xa1, + 0xc8, 0x16, 0xec, 0xc1, 0xc5, 0x63, 0xe1, 0xcf, 0x21, 0xdf, 0x45, 0x61, 0xe9, 0x1a, 0xff, 0xaf, 0x43, 0xaa, 0xf7, + 0x3d, 0xec, 0x9e, 0x60, 0x0f, 0x3a, 0xa9, 0x2d, 0x34, 0x7f, 0x85, 0x55, 0x15, 0x55, 0xf3, 0xcd, 0x08, 0x8f, 0x16, + 0x5c, 0xab, 0x23, 0xd0, 0x41, 0x20, 0xd4, 0x6a, 0x06, 0x03, 0xb4, 0xe3, 0x07, 0xf8, 0xd2, 0xf1, 0xf8, 0x25, 0x89, + 0x09, 0xcf, 0xef, 0x9b, 0x10, 0xc4, 0xe3, 0xe8, 0x71, 0xe7, 0xfa, 0x43, 0x95, 0x21, 0xb2, 0x48, 0xea, 0x7e, 0x84, + 0xb9, 0xfd, 0x34, 0x17, 0x2e, 0x16, 0x27, 0xe8, 0xb1, 0x5c, 0x71, 0xc7, 0x3d, 0xea, 0x6e, 0xda, 0x3d, 0x9f, 0xb2, + 0x27, 0x31, 0x96, 0x52, 0xc4, 0x1d, 0xad, 0xcd, 0xb8, 0x22, 0x45, 0xae, 0x36, 0x81, 0x5e, 0x8e, 0xf4, 0x1c, 0x8f, + 0x64, 0x29, 0x51, 0xc7, 0x12, 0x44, 0xad, 0xe2, 0x3b, 0x23, 0x05, 0xd5, 0x28, 0xef, 0x72, 0xf7, 0xad, 0xd3, 0xd4, + 0xdd, 0xcf, 0xee, 0xa7, 0xc1, 0xcb, 0x54, 0xe7, 0x8c, 0x77, 0x5e, 0xb4, 0x5a, 0xfb, 0x22, 0x46, 0xaf, 0x1f, 0x0b, + 0x32, 0x9c, 0xf6, 0x5d, 0x67, 0x01, 0x6a, 0x95, 0xe5, 0xbf, 0x41, 0x20, 0x53, 0x74, 0x97, 0x9e, 0x8e, 0x68, 0xae, + 0x74, 0xf9, 0x8e, 0x0e, 0x54, 0x26, 0x0a, 0x31, 0xd3, 0x68, 0xf6, 0x80, 0xce, 0x2d, 0xcf, 0x75, 0x19, 0xf5, 0x2e, + 0xa2, 0x0d, 0x0a, 0xb5, 0xcf, 0xd1, 0x5d, 0x2f, 0x3a, 0x87, 0xeb, 0x94, 0xdb, 0x47, 0xcb, 0x45, 0xe5, 0xb3, 0xf1, + 0x70, 0x61, 0x97, 0x48, 0x22, 0x1f, 0x78, 0x09, 0x31, 0x74, 0xdf, 0xce, 0x30, 0x83, 0xb3, 0xda, 0xbb, 0x5d, 0xaa, + 0x1b, 0x3e, 0x84, 0x1e, 0xc5, 0xc2, 0xb5, 0x59, 0xce, 0xff, 0x97, 0xde, 0x45, 0xf5, 0xb7, 0x3a, 0x25, 0xee, 0x17, + 0xfe, 0x5d, 0x24, 0x8a, 0x84, 0x1e, 0xd2, 0x90, 0xde, 0x9f, 0x95, 0x1d, 0x98, 0x0f, 0xed, 0xa1, 0x32, 0x35, 0x79, + 0x9e, 0x05, 0xa0, 0xf5, 0xaa, 0x50, 0x46, 0x0e, 0x46, 0x4f, 0xce, 0x3b, 0xa4, 0x10, 0x86, 0x90, 0xc3, 0x20, 0x11, + 0x73, 0x1d, 0x70, 0x73, 0xd5, 0xed, 0x2c, 0x45, 0x85, 0xee, 0x1a, 0x96, 0x12, 0xd0, 0x11, 0x1d, 0x92, 0xcc, 0x9c, + 0xd0, 0x10, 0x14, 0x28, 0xf2, 0x1e, 0x31, 0x18, 0x4d, 0xe0, 0x3f, 0x98, 0x7d, 0x14, 0xd2, 0x08, 0x08, 0xe3, 0x14, + 0xc5, 0x7b, 0x20, 0x0e, 0x94, 0xd6, 0x3d, 0x98, 0x56, 0xe1, 0xaa, 0x57, 0xda, 0xca, 0x18, 0xbe, 0xc6, 0xb9, 0x33, + 0xc8, 0x05, 0x9e, 0xea, 0x5e, 0xcc, 0x80, 0x28, 0x40, 0x29, 0x68, 0xc1, 0x49, 0x90, 0x7c, 0xa8, 0x15, 0x48, 0xc0, + 0x21, 0xae, 0x41, 0xa9, 0xb1, 0xe0, 0xd5, 0x78, 0xa3, 0x10, 0x96, 0x62, 0x24, 0x02, 0x21, 0xd9, 0x30, 0xac, 0x98, + 0x0a, 0xb4, 0xfb, 0xc5, 0xbe, 0xf7, 0xc2, 0xe3, 0x43, 0x7d, 0x23, 0xe6, 0x02, 0x09, 0xa3, 0xb3, 0x93, 0x7b, 0x81, + 0x24, 0x7f, 0xb5, 0xa7, 0x2b, 0xb3, 0xbc, 0xf0, 0x8d, 0x85, 0x73, 0xb5, 0x12, 0x10, 0xf6, 0x6f, 0x8c, 0x03, 0x01, + 0x30, 0x97, 0xce, 0x6a, 0x2d, 0x91, 0x95, 0x0b, 0x69, 0xd6, 0x63, 0x29, 0xd6, 0xdd, 0x3c, 0x54, 0x80, 0x29, 0xb5, + 0xb8, 0x20, 0x95, 0x15, 0xde, 0x68, 0x0e, 0xa6, 0xf0, 0xa6, 0x83, 0xae, 0xcd, 0x67, 0xff, 0x43, 0xee, 0x1e, 0x1e, + 0x87, 0x57, 0xaa, 0x5b, 0x82, 0x51, 0x67, 0x92, 0xc1, 0x89, 0x4c, 0xa5, 0x9e, 0x06, 0xb1, 0x93, 0xbe, 0x13, 0x20, + 0x90, 0xd0, 0x38, 0x25, 0x9d, 0x8d, 0x74, 0xe8, 0x03, 0xf7, 0x43, 0x59, 0x50, 0x7c, 0x1d, 0x75, 0x7c, 0x11, 0x45, + 0x58, 0x64, 0xa5, 0x67, 0x97, 0x57, 0x37, 0x8d, 0xce, 0xcc, 0x4b, 0xcb, 0x9c, 0xc6, 0x4f, 0x60, 0xc9, 0x0a, 0x51, + 0xf2, 0x92, 0xb4, 0xb0, 0x9c, 0xe0, 0x7a, 0xa0, 0xe9, 0xb0, 0x20, 0x73, 0xe3, 0xb8, 0xfe, 0x51, 0x31, 0x8e, 0xa9, + 0xc3, 0x9e, 0xd2, 0x9b, 0x0a, 0x3c, 0x75, 0x64, 0x15, 0x3a, 0x10, 0x9e, 0x61, 0xbc, 0xa6, 0x81, 0x37, 0xfb, 0xf5, + 0xfc, 0xdf, 0x01, 0x8d, 0xe3, 0xc3, 0x25, 0x6d, 0xb8, 0x0e, 0xab, 0x70, 0x21, 0x8e, 0xc9, 0x0f, 0x26, 0x93, 0xb8, + 0x26, 0x71, 0xe0, 0xf7, 0x61, 0x89, 0x54, 0x88, 0x0c, 0xea, 0x58, 0xb9, 0x1d, 0xfb, 0x0b, 0x40, 0x8f, 0x87, 0x4c, + 0xe7, 0x81, 0x2f, 0x58, 0xe0, 0x38, 0xa8, 0x66, 0x37, 0x87, 0x8a, 0x05, 0xc0, 0x85, 0x59, 0x29, 0x5c, 0x8c, 0xd6, + 0x28, 0xc5, 0xec, 0x19, 0x1f, 0xda, 0x55, 0x03, 0x96, 0x99, 0x77, 0x66, 0xe5, 0xdc, 0x5a, 0x48, 0xc1, 0xed, 0xfa, + 0x46, 0x4c, 0x70, 0xbb, 0x46, 0xb2, 0x2d, 0xea, 0xd7, 0xe0, 0x58, 0x5c, 0x5c, 0xd7, 0xf8, 0xac, 0xcc, 0xdc, 0x49, + 0xfb, 0xc4, 0x75, 0x94, 0x56, 0x20, 0x89, 0xe7, 0x79, 0x18, 0x89, 0x05, 0xd3, 0xe7, 0x84, 0xa8, 0xc4, 0xb0, 0xf4, + 0xb1, 0xec, 0x0c, 0x83, 0xc7, 0x1c, 0x1d, 0x79, 0x66, 0xe7, 0x1c, 0xfe, 0xc7, 0x05, 0x60, 0x59, 0x7c, 0x2a, 0xe3, + 0x5f, 0x1c, 0x8f, 0xb2, 0x27, 0xf2, 0xfe, 0x4a, 0xe2, 0x4e, 0xc5, 0x1c, 0x48, 0x23, 0x5b, 0xc6, 0xd2, 0x16, 0xc8, + 0x45, 0xc6, 0x33, 0xec, 0xfc, 0xd4, 0xfa, 0x98, 0xfd, 0xd8, 0xc7, 0xaa, 0xe1, 0xd7, 0x81, 0x6e, 0x93, 0x12, 0xf4, + 0xad, 0x94, 0xe9, 0xec, 0xbd, 0x99, 0xd2, 0xdc, 0x89, 0xab, 0x7a, 0x65, 0x6b, 0x1b, 0x6a, 0x9b, 0xc4, 0xf5, 0x5b, + 0xf3, 0x18, 0x98, 0xb6, 0x4e, 0x5c, 0x19, 0x0a, 0x6d, 0xb2, 0x3c, 0xd3, 0x20, 0x55, 0x31, 0x74, 0xf7, 0x8a, 0x0f, + 0x9d, 0xee, 0x70, 0x36, 0x5f, 0x9a, 0xf4, 0x30, 0x9e, 0xc5, 0xb5, 0x5c, 0x92, 0xc1, 0x07, 0x85, 0xc3, 0x21, 0x49, + 0xd1, 0x22, 0x97, 0x21, 0x80, 0xdc, 0xed, 0xe0, 0x6e, 0xb2, 0xdd, 0x94, 0x77, 0xcc, 0x5e, 0x9a, 0xa3, 0xcf, 0xdb, + 0x72, 0x31, 0xa1, 0x46, 0x4c, 0xd5, 0x79, 0x6b, 0xbb, 0x6e, 0x0a, 0x4a, 0x39, 0x0a, 0xa4, 0x53, 0x16, 0xa2, 0x82, + 0x9f, 0x98, 0xef, 0xff, 0xa0, 0x28, 0x37, 0x04, 0xdc, 0xf2, 0x3a, 0x7e, 0xdc, 0x69, 0x2d, 0x63, 0x58, 0x8e, 0x8c, + 0x0b, 0xd3, 0xbf, 0xa4, 0x59, 0xcd, 0x96, 0x65, 0xe2, 0x75, 0x9d, 0x3d, 0x28, 0x2e, 0xe1, 0x5c, 0xad, 0x65, 0xe1, + 0x3a, 0xd2, 0xd0, 0x84, 0xfe, 0x10, 0x0a, 0xdb, 0xa6, 0x32, 0x70, 0xa2, 0x94, 0x21, 0x3f, 0x97, 0x86, 0x29, 0x18, + 0x7e, 0x13, 0x60, 0x9d, 0x66, 0x18, 0x85, 0xb4, 0x00, 0xaa, 0x0f, 0x47, 0x93, 0x6e, 0x08, 0x3b, 0x07, 0x1d, 0x47, + 0xe9, 0xec, 0xc0, 0x7a, 0x40, 0xce, 0xf3, 0xd9, 0x5e, 0xef, 0xcd, 0xfa, 0xda, 0xf8, 0x07, 0x04, 0x3e, 0xf3, 0x2d, + 0xfa, 0xda, 0x06, 0xe2, 0x7c, 0x39, 0x23, 0xc6, 0xb6, 0x0c, 0xd8, 0x52, 0xe5, 0x10, 0xb6, 0x54, 0x0c, 0x13, 0x33, + 0x75, 0x62, 0x8a, 0x17, 0x65, 0xdd, 0x79, 0x3e, 0x04, 0x2c, 0x50, 0x7e, 0xc0, 0x91, 0x25, 0xc7, 0x74, 0x14, 0x29, + 0x3a, 0x0d, 0x14, 0x2c, 0x50, 0x7e, 0x7d, 0x5b, 0xfe, 0x61, 0x08, 0xb0, 0x1c, 0x69, 0x95, 0x81, 0x64, 0x6a, 0x63, + 0x39, 0xa9, 0xc5, 0xa9, 0x38, 0x8b, 0xca, 0x30, 0xfa, 0xdd, 0xb8, 0x78, 0xe9, 0xbd, 0x56, 0x6f, 0xe1, 0x29, 0x57, + 0xb0, 0x46, 0x13, 0xd3, 0x13, 0xb1, 0xbf, 0xe0, 0x7c, 0x30, 0xc8, 0x6f, 0x78, 0x77, 0x08, 0xa9, 0x8d, 0x62, 0x8f, + 0xda, 0x0f, 0x4c, 0x46, 0xa5, 0xd5, 0x25, 0x2f, 0xea, 0x45, 0xb6, 0x65, 0x17, 0xbb, 0x72, 0x8f, 0x81, 0xcb, 0x8b, + 0x11, 0xe8, 0xf1, 0xf6, 0x1a, 0x1c, 0x00, 0x1f, 0x2d, 0x8a, 0xab, 0x61, 0x5b, 0xa4, 0x40, 0xd8, 0xd6, 0x7b, 0xde, + 0xea, 0x53, 0x2b, 0xc8, 0x63, 0x10, 0x5a, 0x97, 0x13, 0xde, 0xb9, 0x75, 0xca, 0x90, 0x16, 0x31, 0xce, 0xb3, 0xa8, + 0xd0, 0x87, 0x49, 0x55, 0xc9, 0x86, 0x7f, 0xc0, 0x60, 0xe4, 0x16, 0x53, 0xe1, 0xdf, 0xe2, 0xaf, 0xcc, 0x0d, 0xf7, + 0x6a, 0x98, 0xce, 0xa9, 0x36, 0xef, 0xba, 0xed, 0xf0, 0xc3, 0xf0, 0xdd, 0x12, 0x7a, 0x54, 0x60, 0x9c, 0xe6, 0x89, + 0xd9, 0x1a, 0x7e, 0xa5, 0x80, 0x6f, 0x1f, 0xca, 0xb4, 0x0d, 0x37, 0xd3, 0xaa, 0xbd, 0xe9, 0xb6, 0x1b, 0x40, 0xe6, + 0xac, 0x66, 0xf9, 0xe6, 0x83, 0x3b, 0x09, 0x69, 0x11, 0xfe, 0x58, 0x26, 0xea, 0x11, 0xb6, 0x74, 0xe8, 0x04, 0x3c, + 0xd3, 0xd3, 0xaa, 0xc6, 0xf3, 0x75, 0x56, 0x22, 0x7f, 0xb4, 0x37, 0xfe, 0xe4, 0x83, 0xb7, 0xbe, 0x83, 0x1a, 0x79, + 0xa2, 0x47, 0x84, 0x0b, 0xd5, 0x25, 0xb4, 0xad, 0x1a, 0xb2, 0x28, 0x96, 0xdc, 0x06, 0xde, 0x13, 0x53, 0x84, 0xc3, + 0x4f, 0xed, 0xe9, 0x52, 0xd4, 0xfe, 0x98, 0x19, 0xfc, 0x07, 0x80, 0x44, 0xe5, 0xf2, 0xbf, 0xc3, 0xe3, 0x1d, 0x85, + 0x88, 0x78, 0x0b, 0xc9, 0x82, 0x05, 0x18, 0x79, 0xa8, 0xcc, 0x48, 0x4a, 0xca, 0xb5, 0x12, 0x80, 0xef, 0xc3, 0xd0, + 0x56, 0x5d, 0x83, 0x1c, 0x6c, 0xf0, 0xb7, 0x0c, 0xe2, 0x61, 0xd7, 0x23, 0xad, 0xf1, 0xf2, 0xf8, 0xd2, 0xa7, 0x9a, + 0xd0, 0xe2, 0xdb, 0x48, 0x59, 0xbc, 0x5c, 0x3d, 0x10, 0x1d, 0x49, 0x0c, 0x71, 0x23, 0x27, 0xc9, 0x9b, 0xc4, 0xfb, + 0x69, 0x63, 0x44, 0x72, 0x62, 0x9d, 0xbd, 0x20, 0xe5, 0x17, 0x62, 0xf3, 0xdd, 0xb8, 0x73, 0xb8, 0x73, 0xbd, 0xaf, + 0x94, 0x45, 0x5d, 0x8b, 0x7a, 0x68, 0x76, 0x1d, 0xfd, 0xd9, 0x94, 0x30, 0xa4, 0x43, 0xa2, 0x41, 0x21, 0x2d, 0x2a, + 0x0b, 0xa4, 0x81, 0x9e, 0x44, 0xf6, 0x71, 0x58, 0xcc, 0xde, 0xbd, 0x4a, 0x7d, 0x92, 0x48, 0x49, 0x6c, 0x0f, 0x58, + 0x9a, 0x4c, 0xbc, 0xb9, 0x30, 0xfb, 0xbf, 0xb2, 0xf3, 0xf2, 0x21, 0xd2, 0x98, 0xaa, 0x63, 0x64, 0xa1, 0x06, 0x4a, + 0x59, 0x0b, 0xa7, 0x2d, 0xbe, 0x14, 0x45, 0x5b, 0x85, 0x9e, 0x6a, 0x1e, 0x78, 0x5a, 0x58, 0x13, 0xc5, 0x16, 0xf4, + 0x74, 0x98, 0x96, 0x25, 0xb5, 0x09, 0x4f, 0x5f, 0x7a, 0x9e, 0xe5, 0x39, 0xdb, 0x5d, 0x9a, 0x7d, 0xeb, 0xa0, 0x5e, + 0x53, 0xcb, 0xf6, 0x53, 0x95, 0x69, 0xd0, 0x12, 0x04, 0xf5, 0x10, 0xe4, 0x56, 0x61, 0xe2, 0xc6, 0x38, 0x4f, 0x77, + 0xed, 0xd6, 0x9d, 0xf9, 0xa7, 0x5d, 0x10, 0x17, 0x12, 0x18, 0x34, 0x12, 0xad, 0x26, 0xf4, 0x63, 0xc3, 0x52, 0x18, + 0x72, 0xb6, 0x64, 0x96, 0xf3, 0x6a, 0x20, 0x3f, 0xd3, 0x56, 0x70, 0x40, 0xc2, 0xe8, 0x1c, 0x63, 0x66, 0xf0, 0x39, + 0x12, 0xc3, 0x57, 0x6d, 0xd2, 0x73, 0x24, 0xf7, 0x34, 0xc1, 0x54, 0x00, 0xf3, 0x4a, 0xc1, 0x74, 0xd6, 0x37, 0x8b, + 0x0a, 0x56, 0xfc, 0xf0, 0xe3, 0x2f, 0xa8, 0xde, 0x07, 0x05, 0x9d, 0x04, 0x57, 0xea, 0xb6, 0x9d, 0xf1, 0x6d, 0xf7, + 0x41, 0x01, 0x5e, 0xa8, 0x21, 0xf3, 0x12, 0xff, 0x57, 0x2f, 0xd6, 0xd4, 0x2f, 0xf2, 0xd9, 0x61, 0xa2, 0xef, 0x32, + 0x69, 0xe6, 0xf7, 0xa5, 0x01, 0x65, 0x7e, 0xc9, 0xe3, 0x8a, 0x69, 0xde, 0x23, 0xfe, 0xd3, 0x98, 0xdb, 0xc2, 0x84, + 0x76, 0x98, 0x3e, 0x4a, 0xd4, 0xdc, 0x3e, 0x13, 0x54, 0xfb, 0x86, 0x97, 0xea, 0x31, 0x17, 0xac, 0x63, 0x72, 0x4b, + 0x89, 0xf5, 0x95, 0xc0, 0x83, 0x2c, 0x92, 0x89, 0x7b, 0xa9, 0xf6, 0x8e, 0xf2, 0x7c, 0xa7, 0xf6, 0x34, 0x39, 0x61, + 0x5d, 0x5c, 0x5d, 0xc9, 0xd7, 0x31, 0xc2, 0x6e, 0xbd, 0x59, 0x5e, 0xab, 0x62, 0xcc, 0x28, 0xd9, 0xd4, 0x6e, 0xef, + 0x62, 0x31, 0xe3, 0x26, 0x0c, 0x45, 0xb6, 0x28, 0x97, 0x8f, 0x5c, 0x3c, 0xe4, 0xfb, 0x94, 0x5f, 0xfd, 0x67, 0x0b, + 0x71, 0xf3, 0xf9, 0xf9, 0x1b, 0x23, 0x2c, 0x08, 0x03, 0xdb, 0xad, 0x22, 0x3e, 0x9d, 0x09, 0x14, 0xc6, 0xc6, 0x04, + 0x9b, 0xd7, 0xba, 0x09, 0xbc, 0x48, 0x94, 0x91, 0x34, 0xcc, 0xcf, 0xf2, 0x10, 0xa8, 0x62, 0xe8, 0x49, 0x6b, 0x25, + 0x8a, 0xd6, 0xf7, 0x63, 0x9f, 0x01, 0x21, 0x55, 0xb2, 0xac, 0x88, 0x2b, 0x57, 0x28, 0x04, 0x22, 0x09, 0x07, 0x47, + 0x60, 0x9b, 0x26, 0x84, 0x4f, 0x0f, 0xe9, 0xa5, 0x2e, 0x73, 0xc9, 0xc5, 0x35, 0x38, 0x0a, 0x60, 0x69, 0x32, 0xe2, + 0xd7, 0xbb, 0x55, 0x5e, 0xfa, 0xa5, 0x9d, 0x6e, 0xfe, 0x9e, 0x03, 0x8e, 0x0b, 0xdd, 0x17, 0x05, 0x68, 0x0d, 0x58, + 0x56, 0x28, 0x6f, 0x1f, 0x83, 0x8b, 0xd2, 0x61, 0xf4, 0x72, 0x5c, 0x2d, 0xa2, 0xba, 0x42, 0x59, 0xbb, 0x5d, 0x11, + 0x95, 0xb7, 0xf3, 0xd7, 0x34, 0xa9, 0x45, 0x04, 0x71, 0xde, 0x47, 0x34, 0xcb, 0x44, 0x98, 0x5d, 0xdc, 0x75, 0xa8, + 0xc7, 0x90, 0xf4, 0xa1, 0x15, 0x17, 0x11, 0xf8, 0xb4, 0x02, 0x69, 0x63, 0x6e, 0x0f, 0xe9, 0xb7, 0xb6, 0xa3, 0x00, + 0xe8, 0x85, 0xb0, 0x90, 0xb9, 0x91, 0x14, 0x3c, 0x7b, 0x0f, 0x54, 0x92, 0xf4, 0xb9, 0x1a, 0xb3, 0xae, 0xc7, 0x17, + 0xaf, 0x95, 0xbe, 0x05, 0x79, 0x6f, 0x8a, 0xe0, 0xe9, 0xaa, 0xbd, 0x74, 0x21, 0xa0, 0xbd, 0xce, 0x74, 0xb6, 0x34, + 0x7b, 0x1f, 0xbc, 0x17, 0x1d, 0x78, 0x0d, 0xf5, 0x66, 0x89, 0x3c, 0x66, 0xf0, 0x65, 0x93, 0x90, 0xe4, 0xb5, 0x91, + 0x0a, 0xa2, 0xa0, 0x07, 0xae, 0x51, 0x91, 0x8c, 0x92, 0x8b, 0x6e, 0xfb, 0xb3, 0x19, 0xa4, 0x5c, 0x5e, 0x7d, 0xcd, + 0xdb, 0x9d, 0x83, 0x28, 0xa5, 0xf9, 0xeb, 0x85, 0x4f, 0xbb, 0x67, 0x74, 0xe5, 0x35, 0x81, 0x56, 0x33, 0x7a, 0x4b, + 0x8d, 0x6a, 0xa4, 0xa9, 0x48, 0x05, 0xb1, 0x77, 0x59, 0x83, 0xb5, 0xf1, 0x78, 0x30, 0x95, 0x1a, 0xbc, 0xcf, 0xf4, + 0xa4, 0x75, 0xfa, 0xf6, 0x69, 0x39, 0x84, 0x57, 0xdf, 0x6d, 0x37, 0x2a, 0xf5, 0x5c, 0x7c, 0x2f, 0xdb, 0x45, 0xa6, + 0x75, 0x9c, 0xe8, 0x64, 0xd2, 0x57, 0x69, 0xb7, 0x27, 0x55, 0x3d, 0x73, 0xe1, 0x00, 0xfd, 0xbb, 0x51, 0xa6, 0x94, + 0xa5, 0xf1, 0xda, 0x25, 0xac, 0xa7, 0xde, 0x08, 0xbb, 0x2e, 0x0a, 0xc0, 0x3f, 0x67, 0x1c, 0x68, 0xa2, 0x63, 0xc5, + 0x3a, 0xbe, 0x2e, 0x75, 0x3c, 0x94, 0xfc, 0x80, 0x6d, 0x66, 0x92, 0x77, 0x28, 0x6e, 0xde, 0x10, 0xd8, 0x9f, 0x29, + 0xd6, 0x76, 0xab, 0xc4, 0x19, 0xab, 0xd8, 0x2b, 0xb1, 0xd7, 0x1e, 0x6f, 0x98, 0x40, 0x99, 0xad, 0x2b, 0x4c, 0x99, + 0x33, 0xbf, 0xc9, 0x67, 0x2f, 0xaf, 0x6f, 0x9e, 0xfd, 0x65, 0xe7, 0x35, 0xc3, 0xb0, 0x92, 0x3d, 0x27, 0xcb, 0x21, + 0xac, 0xca, 0xf8, 0x99, 0x9e, 0x6a, 0x1e, 0xdf, 0x51, 0x6f, 0xdc, 0x9b, 0xa4, 0x07, 0xe3, 0xdb, 0x13, 0x92, 0x87, + 0x82, 0x09, 0x18, 0xe9, 0xcf, 0x47, 0xa3, 0x00, 0x9d, 0x5c, 0x76, 0x0d, 0x2e, 0x12, 0x93, 0x3f, 0xa6, 0x5e, 0xc6, + 0x22, 0x05, 0xa9, 0x3a, 0xc2, 0x5d, 0x83, 0x48, 0x8a, 0x2a, 0xe8, 0xc8, 0x8b, 0x02, 0x4c, 0x5a, 0x70, 0x60, 0x01, + 0x0a, 0x3a, 0x5f, 0x79, 0x89, 0x25, 0x7e, 0x88, 0xfe, 0xf1, 0x1f, 0x56, 0x27, 0xbd, 0x68, 0xfe, 0x31, 0xbd, 0xf0, + 0xff, 0x4f, 0xe7, 0xb3, 0x75, 0x8e, 0x0d, 0x08, 0xd6, 0xff, 0x05, 0x62, 0x17, 0x9a, 0xa0, 0x04, 0xe9, 0x01, 0x84, + 0x43, 0xfe, 0xf4, 0x4e, 0x33, 0x9c, 0xb0, 0x7c, 0xaa, 0x26, 0xc3, 0x78, 0x2a, 0xce, 0xc9, 0x84, 0xc6, 0x71, 0x1a, + 0x4d, 0x29, 0xc0, 0x33, 0x6e, 0xcc, 0x5e, 0xc3, 0xd0, 0x7b, 0xdd, 0xa3, 0x40, 0xe8, 0x24, 0xdd, 0xcc, 0x58, 0x8a, + 0x49, 0xb4, 0xac, 0x57, 0x6d, 0x9c, 0x1c, 0xf6, 0xe0, 0x0c, 0xb4, 0xcc, 0x32, 0x27, 0x5a, 0x0f, 0x16, 0x42, 0x73, + 0x6e, 0x6c, 0x30, 0xdd, 0x5b, 0x28, 0xdd, 0x11, 0x41, 0x63, 0xf7, 0x98, 0xca, 0x56, 0x44, 0x25, 0xa5, 0x88, 0x78, + 0x63, 0x20, 0x4c, 0x2f, 0x7e, 0x9d, 0x9a, 0x53, 0xdf, 0xf6, 0x51, 0xbe, 0x32, 0xa9, 0x94, 0xb5, 0x5c, 0x48, 0x83, + 0xf1, 0xea, 0xcb, 0x48, 0x08, 0xcd, 0x44, 0x48, 0x91, 0x17, 0xb2, 0x27, 0x60, 0x13, 0x23, 0xbe, 0xc7, 0xa5, 0x84, + 0x32, 0x39, 0x28, 0x55, 0x50, 0x3c, 0x3e, 0xd4, 0x8f, 0x18, 0x10, 0xba, 0x1a, 0x23, 0x89, 0xe5, 0x58, 0xe8, 0x27, + 0xd2, 0x17, 0x34, 0x71, 0x08, 0x5c, 0xe3, 0x07, 0xd1, 0x67, 0x4f, 0xc6, 0xcb, 0x5e, 0x81, 0xcd, 0x39, 0xb0, 0x89, + 0xdc, 0x1c, 0xb4, 0xee, 0x3f, 0x65, 0x02, 0x4d, 0x14, 0x59, 0xeb, 0x39, 0x4b, 0xf6, 0x2c, 0xc5, 0x3c, 0x54, 0x4b, + 0x96, 0x40, 0xa3, 0xa8, 0x88, 0xd0, 0x5f, 0x0e, 0xf6, 0x50, 0xcf, 0x2a, 0x23, 0xb6, 0x30, 0xaf, 0x4e, 0xa8, 0xb2, + 0xd5, 0x51, 0xd2, 0x2b, 0x0b, 0xf5, 0x70, 0x37, 0x80, 0xf1, 0xb5, 0x9f, 0x7b, 0xe3, 0xce, 0xfa, 0x84, 0x6d, 0xcf, + 0x37, 0xb9, 0x38, 0xfe, 0x7a, 0xe6, 0xe5, 0xe1, 0xe0, 0xb9, 0x67, 0xb1, 0xd9, 0x50, 0x09, 0x81, 0x48, 0x26, 0x82, + 0x4a, 0xf5, 0xdb, 0x6c, 0x38, 0x20, 0xb0, 0x83, 0x62, 0x2b, 0xe1, 0xa5, 0x52, 0x51, 0xee, 0xad, 0xd5, 0x58, 0x0e, + 0x1c, 0x8e, 0x21, 0x8d, 0x63, 0x97, 0x98, 0x82, 0x38, 0xd6, 0x67, 0xe9, 0x21, 0xf0, 0x6b, 0x6b, 0x34, 0x4f, 0xec, + 0x90, 0x21, 0xd3, 0xa4, 0x4a, 0x45, 0xfa, 0x39, 0x00, 0x6e, 0x23, 0xa7, 0x17, 0xe9, 0x1f, 0x50, 0x02, 0xfc, 0x2a, + 0x16, 0x7b, 0xa3, 0x32, 0x16, 0xc9, 0xaa, 0xac, 0x56, 0x0a, 0xa7, 0xe3, 0x03, 0xf0, 0xd2, 0xbf, 0x2c, 0x58, 0xa0, + 0x6a, 0xa4, 0x67, 0x0b, 0x77, 0x08, 0xd4, 0x4a, 0xdf, 0x8d, 0x10, 0xb5, 0xee, 0xd7, 0xf5, 0xa8, 0xba, 0xb9, 0x58, + 0x8c, 0x31, 0xb6, 0xdc, 0x9b, 0x4f, 0x2a, 0xb7, 0x6d, 0x31, 0xfa, 0x36, 0x77, 0x60, 0x6b, 0xd2, 0x3d, 0xfd, 0xb0, + 0x3d, 0xe5, 0xe1, 0x25, 0x7e, 0xf3, 0x6a, 0x22, 0x33, 0x79, 0x7c, 0x26, 0x74, 0xef, 0xa9, 0x42, 0xcd, 0x8d, 0x85, + 0x3c, 0xf5, 0xbb, 0xf7, 0x34, 0xe1, 0x87, 0xfa, 0xaf, 0xd4, 0x78, 0x52, 0x93, 0x93, 0xbf, 0x99, 0xc5, 0x64, 0x13, + 0x86, 0xfd, 0xb0, 0x81, 0x20, 0x10, 0x50, 0x25, 0x80, 0xed, 0x51, 0x94, 0xeb, 0x6f, 0x4a, 0xaa, 0x5b, 0xc0, 0x85, + 0xa2, 0x53, 0x51, 0xf7, 0x29, 0xe1, 0x59, 0xcf, 0xb6, 0x5b, 0x28, 0x22, 0x2e, 0xaa, 0x33, 0x71, 0xa2, 0xcd, 0xcf, + 0xd9, 0xfe, 0x51, 0x75, 0x40, 0x87, 0x2c, 0x3e, 0xea, 0x9a, 0x50, 0x10, 0x37, 0xbc, 0x50, 0x86, 0x09, 0x92, 0x08, + 0x65, 0xd3, 0x6c, 0xf7, 0x65, 0xba, 0xc6, 0xad, 0xfd, 0xb9, 0xd0, 0xf7, 0x76, 0x20, 0x12, 0x17, 0x33, 0xd1, 0xac, + 0x08, 0x57, 0xf2, 0xe0, 0x54, 0xd1, 0xe6, 0x2c, 0xc8, 0xd6, 0xec, 0xad, 0x9e, 0x93, 0x39, 0xe0, 0x28, 0xd2, 0x72, + 0xcc, 0x8f, 0x3c, 0x17, 0xcf, 0xd5, 0x67, 0x8b, 0xe4, 0x7e, 0xc9, 0xc6, 0xb6, 0xdd, 0x4b, 0xd2, 0x93, 0x1c, 0x60, + 0x22, 0x04, 0xdf, 0x94, 0x90, 0x0e, 0xc0, 0xfa, 0xda, 0xb2, 0x04, 0x99, 0xa9, 0x02, 0x22, 0x93, 0xe9, 0xfc, 0xda, + 0x93, 0x83, 0xa0, 0x33, 0x15, 0x70, 0xc0, 0x10, 0x7e, 0x06, 0x06, 0x1a, 0xdf, 0x73, 0x90, 0xa4, 0x44, 0x43, 0x32, + 0xc5, 0x3d, 0x20, 0x52, 0xc0, 0xcd, 0x37, 0x4f, 0xb6, 0x68, 0x5d, 0x19, 0xa7, 0x90, 0x5e, 0xd2, 0x7a, 0x30, 0x25, + 0x31, 0x9e, 0x49, 0xb8, 0xc5, 0xf1, 0xcf, 0x96, 0xc0, 0xeb, 0x44, 0x5e, 0xa5, 0x64, 0x4b, 0xce, 0x09, 0x0c, 0x2e, + 0x47, 0x2b, 0x36, 0xb0, 0xb8, 0x7e, 0x0a, 0x6b, 0x8c, 0x27, 0x59, 0x1e, 0x8b, 0x9b, 0xbf, 0xef, 0xbf, 0xf5, 0xc8, + 0xee, 0xb3, 0xdd, 0x9d, 0x4f, 0x4d, 0x6b, 0x05, 0x83, 0x18, 0x53, 0xb2, 0x01, 0x6a, 0x05, 0xcf, 0x6c, 0xc8, 0xd8, + 0x95, 0xe6, 0x59, 0xe0, 0xa8, 0x67, 0xbb, 0x6f, 0xcd, 0xee, 0x58, 0xbe, 0x41, 0xfc, 0x44, 0x43, 0xd8, 0x78, 0xa7, + 0x02, 0x9b, 0xa8, 0xe5, 0xf7, 0xe8, 0xb7, 0xbb, 0xb3, 0x1d, 0xda, 0xab, 0x78, 0x37, 0x8d, 0x67, 0x19, 0x53, 0x59, + 0x96, 0xbf, 0x8f, 0x9e, 0xf9, 0xd1, 0xd1, 0x12, 0x86, 0xfa, 0xc8, 0x39, 0x4e, 0xd3, 0x38, 0xe8, 0x08, 0xf5, 0x84, + 0x1d, 0x7f, 0x87, 0x88, 0xff, 0xef, 0x0c, 0xff, 0x77, 0xf0, 0xbf, 0xe7, 0xf8, 0x17, 0xa1, 0xae, 0xf3, 0xd5, 0xe6, + 0xaf, 0x28, 0xff, 0x8d, 0x8f, 0x64, 0x9d, 0xbf, 0xda, 0xdb, 0x7d, 0x61, 0x5a, 0x6f, 0x92, 0x47, 0x6b, 0x13, 0x05, + 0xf4, 0xd5, 0xe3, 0xce, 0xe9, 0x04, 0x51, 0xe6, 0xd4, 0xf9, 0x88, 0xf2, 0xfb, 0x6e, 0x70, 0xe1, 0x17, 0xe3, 0x06, + 0x96, 0xad, 0x2f, 0xff, 0xf4, 0xaf, 0x90, 0xcb, 0x7d, 0x7e, 0xce, 0xb5, 0xad, 0x68, 0x94, 0xfd, 0x7d, 0x68, 0xdc, + 0x11, 0x41, 0x0d, 0xe7, 0xfe, 0xf3, 0x44, 0xf3, 0x4f, 0x06, 0x97, 0x22, 0x47, 0x6b, 0x85, 0xc5, 0x67, 0xfe, 0x4c, + 0x2b, 0x7b, 0xdf, 0xb8, 0x75, 0x65, 0x1b, 0x5a, 0x8c, 0x36, 0xdd, 0x00, 0x83, 0x49, 0x05, 0xad, 0x95, 0xb5, 0xfb, + 0xfc, 0x97, 0x89, 0x31, 0x24, 0x3c, 0xfa, 0xd9, 0xaf, 0x81, 0xfa, 0x8d, 0x6b, 0xb3, 0x06, 0x12, 0x5b, 0xa6, 0xe7, + 0x2f, 0x05, 0xb3, 0x09, 0x0d, 0x0f, 0xf5, 0x74, 0xa9, 0xf7, 0xea, 0xb3, 0x48, 0x34, 0xc6, 0xed, 0x50, 0x5c, 0x5f, + 0x01, 0x0a, 0xa4, 0x38, 0x4f, 0x97, 0xff, 0xc7, 0x5f, 0x88, 0xa2, 0xeb, 0x12, 0x3a, 0x2a, 0xe7, 0xa4, 0xe6, 0xa7, + 0xd0, 0x4b, 0x54, 0x7a, 0xa9, 0x12, 0x75, 0x54, 0x4c, 0x3c, 0x0b, 0x82, 0xeb, 0x8a, 0x9c, 0x9b, 0x5c, 0x0d, 0xe7, + 0x64, 0xdc, 0xe7, 0xff, 0x38, 0xcb, 0x49, 0x82, 0x87, 0xc0, 0x8c, 0xef, 0xec, 0x0a, 0x61, 0x49, 0x5e, 0xc3, 0xc1, + 0xec, 0xc1, 0xb0, 0x7c, 0x52, 0x03, 0x0d, 0x86, 0xf9, 0xf3, 0x05, 0x24, 0xe0, 0x1b, 0xdf, 0x0c, 0x52, 0xa3, 0xe2, + 0xa9, 0x3d, 0xad, 0x43, 0x95, 0x36, 0x06, 0x86, 0x21, 0x11, 0xc1, 0x21, 0xe7, 0x00, 0xf1, 0x41, 0xa7, 0xb3, 0xa3, + 0x8d, 0x63, 0x26, 0xa7, 0xe4, 0xa0, 0x1f, 0xe7, 0x52, 0x15, 0xca, 0xa1, 0x96, 0xd4, 0x51, 0xd1, 0x90, 0xe3, 0x32, + 0x3a, 0xd0, 0xa2, 0xdb, 0x2b, 0xc8, 0xfa, 0x32, 0x17, 0xef, 0xb2, 0xd9, 0xea, 0x91, 0x08, 0x33, 0x89, 0x18, 0xa9, + 0xe0, 0xb4, 0x64, 0x55, 0x0c, 0xfd, 0xa2, 0x25, 0xa6, 0x36, 0xe2, 0xe9, 0xde, 0x5a, 0x3a, 0x75, 0x1e, 0x79, 0x70, + 0x65, 0x90, 0xbd, 0xe2, 0x35, 0xe0, 0xde, 0x24, 0x1b, 0x66, 0x19, 0x2b, 0xb8, 0xb0, 0xbe, 0xf6, 0x69, 0xf5, 0xc1, + 0xbe, 0x43, 0xc1, 0x79, 0xf6, 0x5e, 0x0c, 0xba, 0x19, 0xec, 0x82, 0xea, 0x8d, 0xfd, 0x3c, 0x0d, 0xf8, 0xdd, 0x03, + 0xa5, 0xa0, 0xc0, 0x31, 0xa8, 0xa7, 0x3b, 0x7f, 0x43, 0x04, 0x7e, 0x60, 0x37, 0x60, 0x9d, 0xb3, 0x6d, 0xc1, 0x95, + 0x84, 0x6a, 0x20, 0xd5, 0x65, 0x2c, 0x0b, 0x6c, 0xa7, 0x87, 0x2d, 0xae, 0x7b, 0x8e, 0x06, 0xa5, 0xba, 0x37, 0x13, + 0x94, 0x89, 0xa3, 0x65, 0xae, 0x03, 0x5b, 0x04, 0x30, 0xb2, 0x15, 0xe1, 0x36, 0x20, 0xaf, 0x2e, 0x66, 0x34, 0xf4, + 0x87, 0xb8, 0x9a, 0x04, 0x34, 0xa0, 0x91, 0x8b, 0x8e, 0x17, 0x5d, 0x2f, 0x9d, 0xd2, 0xdb, 0xe3, 0x09, 0x72, 0xb0, + 0x73, 0x16, 0x86, 0xf2, 0x7a, 0x92, 0xf3, 0xa5, 0xe7, 0xbf, 0x9e, 0x10, 0xed, 0xf5, 0x31, 0x86, 0xb3, 0x85, 0x94, + 0xc4, 0x06, 0x32, 0xb4, 0x3a, 0x8e, 0xe8, 0xe0, 0x21, 0x0f, 0xe8, 0x49, 0x69, 0x96, 0xd7, 0x4d, 0x88, 0x8a, 0xb9, + 0x08, 0x95, 0xc8, 0xcb, 0xe2, 0x9a, 0xad, 0x2b, 0x10, 0x98, 0xd9, 0x2e, 0xf8, 0x82, 0xa3, 0xd1, 0xca, 0xf0, 0x82, + 0x30, 0x27, 0x8c, 0x90, 0xc5, 0xfa, 0x27, 0xc0, 0x1b, 0xbe, 0x06, 0x45, 0xfe, 0x64, 0x5c, 0x12, 0x9e, 0x59, 0xe6, + 0x0e, 0xe1, 0x84, 0x9f, 0x2a, 0x61, 0x75, 0x17, 0x15, 0xfa, 0xc7, 0xf3, 0x52, 0x50, 0x1c, 0x35, 0x06, 0x0c, 0xfb, + 0x22, 0x04, 0x91, 0x8a, 0xd2, 0xec, 0x3b, 0x88, 0x54, 0xf7, 0x03, 0xb1, 0x1e, 0x8e, 0x68, 0xc3, 0x23, 0x11, 0xc8, + 0x1f, 0x49, 0xb5, 0xd0, 0xc6, 0x47, 0x22, 0x24, 0xbd, 0x7d, 0xf3, 0xc1, 0x79, 0xb9, 0x29, 0xaa, 0x2c, 0xa3, 0xd0, + 0x27, 0x3a, 0xa8, 0x9c, 0xea, 0xf1, 0x7a, 0x96, 0xda, 0x0a, 0xae, 0x5b, 0x2b, 0x81, 0x8a, 0x8d, 0xe9, 0x32, 0xf7, + 0x91, 0xed, 0xb5, 0x4b, 0x29, 0xf1, 0xe7, 0x79, 0xc6, 0x1a, 0x8e, 0x04, 0xec, 0xa6, 0x17, 0x86, 0xb1, 0x93, 0x1d, + 0x43, 0x47, 0x92, 0xf4, 0x82, 0xd2, 0x74, 0x8c, 0x91, 0x9b, 0xd7, 0xdd, 0x60, 0xbb, 0x8a, 0xc1, 0x57, 0x03, 0xc3, + 0x39, 0x34, 0x69, 0x38, 0x29, 0x38, 0xd7, 0xad, 0xfb, 0xeb, 0x78, 0xe9, 0x3c, 0x2f, 0xfb, 0x18, 0x96, 0x47, 0x3c, + 0xd7, 0xf6, 0x5f, 0x2f, 0xe4, 0xf9, 0x7a, 0x09, 0xcb, 0xd6, 0x5f, 0x93, 0xe4, 0x08, 0xd9, 0xcf, 0x2a, 0xa2, 0xe6, + 0x17, 0x8c, 0x43, 0xe4, 0x4f, 0x71, 0x4c, 0x15, 0xcd, 0x5c, 0x75, 0x7a, 0x54, 0x7a, 0x83, 0x5e, 0x3c, 0x3c, 0x20, + 0x38, 0x09, 0x90, 0x27, 0x4e, 0x42, 0x18, 0x30, 0xe4, 0x14, 0xd6, 0x7c, 0x05, 0x3c, 0xe6, 0x71, 0xc3, 0x53, 0x9a, + 0xab, 0x3f, 0x01, 0x34, 0xa0, 0x02, 0xea, 0xc3, 0x0e, 0x5f, 0x2c, 0xc1, 0x86, 0x46, 0x08, 0x21, 0x9f, 0x10, 0xfc, + 0x2e, 0xff, 0x4a, 0x50, 0x16, 0x3b, 0x05, 0x32, 0x5a, 0xa7, 0xda, 0x15, 0x93, 0x95, 0x36, 0xa8, 0xff, 0x96, 0x76, + 0x53, 0x5e, 0x10, 0x39, 0x88, 0x50, 0x01, 0x06, 0xb2, 0xda, 0x50, 0xe0, 0xd5, 0x65, 0x9b, 0x69, 0x06, 0x63, 0xf5, + 0x51, 0xd3, 0x66, 0xef, 0xf6, 0x4a, 0x4f, 0x65, 0x40, 0xd4, 0x06, 0x86, 0x77, 0xfd, 0xcf, 0xe1, 0x69, 0x19, 0xcf, + 0x8b, 0x41, 0x35, 0x4e, 0x87, 0xc8, 0x70, 0x9d, 0x3a, 0x56, 0xad, 0xfc, 0xcf, 0x94, 0x12, 0xe3, 0x53, 0x51, 0xe1, + 0xc0, 0xba, 0x6a, 0xa8, 0x5a, 0x10, 0xa6, 0x8d, 0xd2, 0x3f, 0x28, 0xca, 0xa0, 0x05, 0x58, 0x1a, 0xb5, 0xc6, 0x01, + 0x9b, 0x9a, 0x5e, 0x9e, 0x1b, 0xd3, 0x89, 0x57, 0xfb, 0x9b, 0xc0, 0x84, 0xbb, 0xfa, 0x31, 0x87, 0xba, 0xb6, 0x78, + 0xae, 0xef, 0xbb, 0x8f, 0xa7, 0x3c, 0x24, 0x7a, 0x26, 0x16, 0x9a, 0xb2, 0xb9, 0xce, 0xe7, 0x1d, 0x14, 0x1b, 0x5e, + 0xd8, 0xe7, 0x2b, 0x24, 0xc8, 0x01, 0x87, 0x4d, 0x71, 0x35, 0x0e, 0xcf, 0x38, 0x27, 0x2a, 0x7d, 0x2e, 0xf6, 0x83, + 0x03, 0x38, 0x3c, 0xe7, 0x4a, 0x59, 0xcb, 0xe7, 0x6f, 0xd3, 0x19, 0x4f, 0xfa, 0x12, 0x9c, 0x94, 0x32, 0xf1, 0x8a, + 0xc5, 0x9f, 0xf1, 0xf1, 0x11, 0x67, 0x78, 0x7f, 0xa3, 0xf3, 0x69, 0x7f, 0x9b, 0xae, 0x8d, 0x79, 0x0f, 0xf2, 0x46, + 0x05, 0xcf, 0x5d, 0x58, 0xc6, 0x36, 0x51, 0x40, 0xf0, 0x37, 0x3a, 0xa7, 0x69, 0x1e, 0x42, 0x3c, 0xac, 0xa2, 0x22, + 0xbf, 0xa3, 0x48, 0x96, 0x68, 0xe1, 0x6d, 0x4e, 0x8b, 0xf4, 0x6a, 0x7a, 0xde, 0xe6, 0x4b, 0x99, 0x0e, 0xed, 0xc6, + 0xd4, 0x49, 0xb4, 0xaa, 0x30, 0x21, 0x88, 0xc0, 0x9d, 0x2e, 0x91, 0x60, 0xb7, 0x26, 0xb3, 0x27, 0xfc, 0x6b, 0x4c, + 0xe3, 0x20, 0x6e, 0xd9, 0x73, 0x8b, 0x7d, 0x4f, 0xcd, 0x1c, 0xe8, 0x85, 0x9c, 0xa1, 0x38, 0x64, 0x0c, 0xfa, 0x42, + 0xae, 0x1f, 0x8f, 0xd0, 0x49, 0x05, 0xf8, 0x55, 0xc9, 0xaf, 0x7a, 0xbc, 0x96, 0x0a, 0x95, 0xea, 0x6c, 0x22, 0x47, + 0xfb, 0xb3, 0xb4, 0xf1, 0x18, 0xfb, 0xb1, 0x3c, 0x7d, 0xed, 0xe9, 0x04, 0x50, 0xb1, 0x25, 0xb7, 0xb2, 0x6f, 0xc8, + 0x43, 0x4b, 0xbf, 0x79, 0x9c, 0x61, 0xe6, 0x08, 0xd0, 0xd9, 0xaa, 0xe0, 0xa9, 0x8b, 0xd9, 0xdb, 0x5b, 0x6e, 0x2a, + 0xb6, 0xdf, 0xf0, 0x19, 0x60, 0x27, 0x89, 0x40, 0x34, 0xaa, 0xf6, 0x59, 0xa7, 0x31, 0x79, 0x2a, 0x9e, 0x42, 0x23, + 0x60, 0x29, 0x22, 0xd7, 0x94, 0x68, 0x5d, 0xcb, 0xd0, 0x45, 0x72, 0x7c, 0xbb, 0x1c, 0x82, 0x04, 0x77, 0x68, 0xd6, + 0x48, 0xe8, 0xa9, 0xba, 0x62, 0xae, 0x54, 0x95, 0x50, 0x77, 0x3a, 0x4c, 0x79, 0x6e, 0xac, 0xf0, 0x28, 0x73, 0xd1, + 0xb9, 0x8e, 0x55, 0x25, 0xc2, 0x22, 0x2e, 0xce, 0x02, 0x2f, 0x02, 0xfc, 0x08, 0x0c, 0xe2, 0x52, 0x95, 0x65, 0xa6, + 0x08, 0x49, 0x93, 0x6d, 0x81, 0xb1, 0xe2, 0xd0, 0x6f, 0xa2, 0x9a, 0x07, 0x5f, 0xff, 0x34, 0xa1, 0x28, 0xec, 0x04, + 0x44, 0xa3, 0x41, 0xb7, 0x16, 0xd0, 0xcc, 0xfc, 0x9b, 0x72, 0x78, 0x0c, 0x9d, 0x27, 0xf1, 0x86, 0x25, 0xc9, 0xa2, + 0x3f, 0xff, 0x97, 0x06, 0x61, 0xd2, 0x3b, 0x90, 0x52, 0x95, 0x40, 0xbb, 0x61, 0x6e, 0x3e, 0x89, 0x0c, 0xd9, 0xa2, + 0x5c, 0xec, 0x71, 0x94, 0x73, 0x1b, 0xd2, 0x0a, 0x66, 0x5e, 0x42, 0xc9, 0x3b, 0x5a, 0xaf, 0xbc, 0x0e, 0xab, 0x6e, + 0x79, 0x8d, 0x97, 0x38, 0xd1, 0x2f, 0x58, 0x74, 0x4d, 0xae, 0xc1, 0x6d, 0x42, 0x03, 0x32, 0x2b, 0x13, 0xd5, 0x1b, + 0x82, 0xa7, 0x90, 0xb2, 0x31, 0xe0, 0xe2, 0xdc, 0xe0, 0xd1, 0xf9, 0x9c, 0x90, 0xb2, 0x90, 0x0b, 0xcd, 0x00, 0x27, + 0x31, 0x92, 0xd1, 0xa6, 0x2e, 0xe5, 0x42, 0x9d, 0x7e, 0xe0, 0xad, 0x83, 0x1e, 0xd6, 0x66, 0x67, 0x2b, 0xf6, 0xb2, + 0x5e, 0x31, 0xb2, 0xa6, 0xea, 0x72, 0x52, 0x6e, 0x4a, 0x68, 0x68, 0x3f, 0xc6, 0x7b, 0x19, 0x87, 0x1c, 0x66, 0xd5, + 0x18, 0x89, 0x3d, 0x23, 0x39, 0xb3, 0x49, 0x92, 0x65, 0xd6, 0x65, 0x2d, 0xfd, 0x6c, 0xae, 0xfd, 0x8f, 0x6c, 0xe1, + 0x11, 0xcb, 0x38, 0xbf, 0xd2, 0x43, 0x49, 0xb8, 0xb2, 0x4e, 0x93, 0xe7, 0xe9, 0x07, 0xe1, 0xba, 0xdb, 0xd4, 0x8c, + 0x86, 0xf7, 0x80, 0x84, 0x2c, 0x6d, 0xd9, 0xaa, 0x36, 0x36, 0x36, 0xf8, 0xb9, 0xcf, 0x03, 0x0b, 0xf1, 0xa0, 0x21, + 0x19, 0xfd, 0x7c, 0x9b, 0xc7, 0x86, 0x85, 0xce, 0xe8, 0xa0, 0x94, 0x5e, 0x4b, 0x71, 0xee, 0x05, 0x4a, 0x2c, 0xf8, + 0x72, 0xaf, 0xa4, 0xc9, 0x99, 0x07, 0x7e, 0x10, 0x67, 0x46, 0x17, 0x99, 0x77, 0xbe, 0x46, 0xdd, 0x4a, 0xef, 0x95, + 0x96, 0xf4, 0x83, 0x5f, 0xfd, 0x6c, 0x95, 0x5e, 0xa7, 0x5f, 0x22, 0x73, 0xe6, 0x2b, 0x5c, 0xa2, 0x5d, 0x81, 0x1d, + 0xc6, 0x49, 0x5d, 0xf3, 0xeb, 0x0e, 0xd0, 0xfb, 0xc2, 0x9b, 0x81, 0x46, 0x89, 0xc0, 0x0b, 0x4d, 0x2e, 0x71, 0x25, + 0xbe, 0x10, 0xde, 0x14, 0x7b, 0x98, 0xc1, 0x44, 0x6c, 0x14, 0x41, 0x3c, 0x00, 0xff, 0x1c, 0xe2, 0x97, 0x31, 0xbf, + 0xd7, 0x41, 0x6d, 0xb4, 0x20, 0x34, 0x45, 0xe6, 0x26, 0x7b, 0x11, 0x5b, 0x90, 0xc3, 0x2b, 0x81, 0x04, 0xb9, 0x66, + 0x8d, 0x1d, 0xb0, 0x65, 0x5b, 0xa1, 0x6c, 0xde, 0x70, 0x6c, 0xb0, 0x3b, 0x7b, 0x69, 0x6d, 0x12, 0x84, 0xb8, 0x44, + 0xd4, 0x9a, 0x1e, 0x19, 0xe5, 0xab, 0x96, 0x82, 0x4a, 0xf4, 0xf3, 0xf4, 0xbf, 0xb1, 0x49, 0x6f, 0x17, 0x04, 0xfd, + 0x52, 0x64, 0xda, 0x2f, 0x34, 0x0c, 0x4a, 0x8b, 0xbc, 0xff, 0x43, 0x09, 0x7c, 0x1d, 0xac, 0xe9, 0x3a, 0xd5, 0x6e, + 0x7d, 0xf1, 0x4f, 0x50, 0x3d, 0x4f, 0xc6, 0x1d, 0x46, 0x2e, 0x0c, 0x68, 0x7e, 0x6f, 0x24, 0x9d, 0xb7, 0xfe, 0xcf, + 0x12, 0x02, 0x67, 0x90, 0x25, 0x14, 0xae, 0x11, 0xf9, 0x26, 0xff, 0xc0, 0x30, 0xed, 0x15, 0xc1, 0xce, 0x8e, 0xbc, + 0x37, 0xb6, 0x93, 0x75, 0x88, 0x36, 0xf4, 0x0d, 0x98, 0xb2, 0x7e, 0xa3, 0x59, 0x36, 0x40, 0x6d, 0x3b, 0xe3, 0xb6, + 0x2b, 0x69, 0x16, 0x92, 0xe1, 0x99, 0xc5, 0x20, 0xd5, 0x46, 0x7e, 0xe6, 0x95, 0x85, 0x33, 0x73, 0x77, 0xa1, 0x91, + 0x9f, 0x3d, 0x9d, 0xe1, 0xf0, 0xc6, 0x46, 0x7a, 0xe1, 0x09, 0x4b, 0x73, 0x43, 0x07, 0x00, 0x20, 0x5c, 0x2a, 0x3a, + 0xde, 0xb1, 0x54, 0xd9, 0x4a, 0xd4, 0x57, 0x82, 0xe6, 0xe0, 0x38, 0x1b, 0x5d, 0xc8, 0x27, 0x4c, 0x18, 0xe9, 0x79, + 0x0e, 0x11, 0x8f, 0x63, 0xe5, 0x32, 0x18, 0x32, 0x07, 0x3f, 0x91, 0x60, 0x47, 0xb3, 0xc3, 0x59, 0x0e, 0x57, 0xa9, + 0x5f, 0x27, 0xa8, 0x86, 0xd4, 0x58, 0x65, 0x6c, 0xc3, 0xfa, 0xff, 0x0a, 0x27, 0x2e, 0xeb, 0x79, 0x42, 0x2f, 0x3a, + 0x2c, 0xdc, 0xc2, 0x47, 0x2e, 0xf9, 0x87, 0x38, 0x38, 0xc4, 0xd9, 0x18, 0xe7, 0x19, 0x48, 0x9e, 0x36, 0x50, 0x18, + 0x79, 0x39, 0xbe, 0x54, 0x72, 0xb6, 0x1b, 0xaa, 0x4a, 0x4a, 0x97, 0x74, 0xab, 0xbd, 0x67, 0xb2, 0x1f, 0x91, 0x13, + 0x27, 0x45, 0x24, 0x99, 0x4c, 0x2a, 0xa8, 0x53, 0x1a, 0x6c, 0xfa, 0x15, 0xc0, 0x08, 0xe6, 0x5a, 0xd7, 0x34, 0x45, + 0x69, 0x99, 0x70, 0xf8, 0x1c, 0x2d, 0xd6, 0x99, 0x43, 0xd1, 0xc1, 0x60, 0x50, 0x48, 0x33, 0xb4, 0x0b, 0x3c, 0xc8, + 0xa8, 0xcc, 0x1e, 0x7f, 0x9e, 0x9f, 0x14, 0x4d, 0xd8, 0x64, 0xc5, 0x2a, 0x51, 0xa4, 0x96, 0xb1, 0xa4, 0x54, 0x75, + 0xfe, 0xed, 0x3e, 0x1a, 0x1a, 0x1d, 0x58, 0x03, 0x3a, 0x0a, 0x25, 0x9c, 0xf7, 0x50, 0x3c, 0xe9, 0x4e, 0xcd, 0xde, + 0x7e, 0xeb, 0x77, 0x57, 0x1f, 0xe2, 0x54, 0xdf, 0x25, 0x9d, 0xdc, 0x37, 0x9e, 0xb0, 0x9d, 0xb1, 0xd7, 0xba, 0xbe, + 0x63, 0x0b, 0x60, 0x97, 0x19, 0xcf, 0xc6, 0xf0, 0xbe, 0x8e, 0xbd, 0xf8, 0x82, 0x5c, 0x3d, 0x7f, 0xd6, 0x6a, 0xf9, + 0x8c, 0x5b, 0x0a, 0xa7, 0x1c, 0xa1, 0x85, 0x45, 0x40, 0x76, 0xc0, 0x28, 0x20, 0x2f, 0xa1, 0xcb, 0x18, 0xc2, 0x83, + 0x5f, 0x1b, 0x14, 0xad, 0x0e, 0x38, 0x13, 0xe8, 0x51, 0x3f, 0xe8, 0xc1, 0xdf, 0x74, 0x12, 0x6f, 0x4c, 0xbc, 0xb7, + 0x68, 0x4a, 0xbf, 0x5a, 0x69, 0x53, 0xa6, 0x3c, 0xbb, 0xe4, 0xc3, 0xd8, 0x0e, 0x55, 0x5b, 0xbb, 0x0d, 0x35, 0xc4, + 0x67, 0xb8, 0x9f, 0xb8, 0xa2, 0x78, 0x6c, 0x06, 0xea, 0x6f, 0xfc, 0xc8, 0x3b, 0x6d, 0x3f, 0x92, 0xfb, 0x10, 0x87, + 0x1e, 0xcb, 0xbe, 0x2a, 0xfb, 0x00, 0xae, 0xd9, 0xd9, 0x58, 0x89, 0x9f, 0x15, 0x61, 0xb8, 0x1c, 0x82, 0xf1, 0x67, + 0xb5, 0x0d, 0x3d, 0xa9, 0xa7, 0xac, 0x79, 0xfc, 0x3a, 0x32, 0xbf, 0xc9, 0xc2, 0xa4, 0x5e, 0xc8, 0x9a, 0x1e, 0xa7, + 0x33, 0x43, 0xe7, 0x4c, 0xe4, 0xee, 0xd9, 0xaf, 0xd1, 0xda, 0xc8, 0x48, 0xed, 0x6c, 0xd1, 0xc7, 0xdf, 0x35, 0x55, + 0x36, 0x9a, 0x34, 0xe3, 0xb1, 0x73, 0xad, 0x4b, 0x97, 0xba, 0x8c, 0x4c, 0xf7, 0x67, 0xbd, 0x58, 0x50, 0xa5, 0x55, + 0x66, 0xde, 0x27, 0x52, 0xc3, 0xb8, 0xd6, 0x6a, 0xe2, 0x9d, 0x5e, 0x7c, 0xcb, 0x8e, 0xdd, 0x74, 0x96, 0x64, 0x30, + 0x98, 0x8e, 0xdc, 0x0c, 0x1d, 0xc9, 0x08, 0x53, 0xbf, 0x7c, 0x32, 0x30, 0xeb, 0x3a, 0x7f, 0x6f, 0x5f, 0x33, 0x8e, + 0xe2, 0x5b, 0x8f, 0x15, 0xfd, 0xb6, 0x4e, 0xb7, 0xef, 0x09, 0x70, 0x6d, 0x53, 0xfb, 0xd4, 0x5b, 0xa5, 0x9c, 0x8d, + 0xe4, 0x58, 0x4e, 0xe2, 0x09, 0x14, 0x3c, 0x00, 0x49, 0x04, 0x4c, 0x9a, 0xf9, 0xc5, 0x52, 0xc9, 0x84, 0x8d, 0xba, + 0xdb, 0xef, 0x15, 0x17, 0x18, 0xb5, 0x7f, 0x94, 0xb9, 0x9a, 0x5e, 0x8e, 0x14, 0x25, 0xd3, 0x81, 0x81, 0xa6, 0x30, + 0x96, 0x3e, 0xff, 0x62, 0x5b, 0x89, 0x05, 0x95, 0x45, 0xb1, 0xb0, 0x66, 0x74, 0x00, 0x22, 0xe8, 0x63, 0x2a, 0xa8, + 0x1a, 0x7e, 0x43, 0xca, 0xe7, 0xf4, 0xeb, 0xd9, 0xf9, 0x88, 0x6d, 0xba, 0x29, 0x7d, 0x8c, 0x07, 0xab, 0xec, 0x75, + 0x7e, 0x1f, 0x9f, 0xfa, 0x42, 0x2f, 0x5e, 0x49, 0xde, 0x59, 0xf7, 0x4f, 0xf3, 0xcf, 0xe7, 0x6c, 0xfe, 0xf9, 0xac, + 0x19, 0x60, 0x98, 0xd9, 0x18, 0xf7, 0x2a, 0x7a, 0xb5, 0xc0, 0x35, 0x4e, 0x65, 0x78, 0x1a, 0xcb, 0x7f, 0x74, 0x16, + 0xae, 0x32, 0xc0, 0x99, 0x6c, 0xa0, 0x4d, 0x65, 0x10, 0x7c, 0x73, 0xc3, 0x82, 0xa6, 0x2b, 0x27, 0x26, 0x9a, 0x99, + 0x48, 0x5c, 0x82, 0x9c, 0xc4, 0x1c, 0xec, 0xc9, 0xa5, 0xea, 0x3a, 0x25, 0x33, 0x7e, 0x66, 0xda, 0x82, 0x9e, 0xa0, + 0xf0, 0x53, 0x90, 0xa9, 0x00, 0x41, 0x74, 0xbe, 0x4f, 0xf3, 0x38, 0x4a, 0x7f, 0xe7, 0x98, 0xa7, 0xfc, 0xff, 0x89, + 0x2c, 0x11, 0xa9, 0x6b, 0x19, 0xc2, 0x01, 0xbf, 0x46, 0x34, 0x75, 0xc6, 0xef, 0xc5, 0xa0, 0x7e, 0x91, 0xde, 0xaa, + 0x11, 0x38, 0x17, 0xa0, 0xae, 0x46, 0x98, 0x06, 0xf2, 0x8e, 0xf6, 0x1a, 0xf9, 0xc5, 0xa9, 0xd2, 0x2d, 0x7b, 0x5a, + 0xf2, 0x8a, 0x2c, 0x54, 0xfe, 0xc9, 0x98, 0x62, 0x56, 0x15, 0x1d, 0x47, 0x9a, 0xf7, 0x0d, 0x36, 0xc9, 0x17, 0x4e, + 0x12, 0x7a, 0xca, 0xa9, 0xb1, 0x30, 0x6e, 0x15, 0x5e, 0xda, 0x85, 0xd1, 0x07, 0x64, 0x0e, 0x34, 0x17, 0x65, 0x3f, + 0x0e, 0x71, 0x44, 0x7c, 0xa0, 0x9f, 0xf1, 0x2d, 0x64, 0xdc, 0xf6, 0x1c, 0x27, 0xc4, 0xa9, 0xfa, 0x62, 0x28, 0x5e, + 0xc6, 0x26, 0x15, 0x92, 0x1b, 0xb2, 0x18, 0xca, 0xaa, 0x85, 0xe7, 0x67, 0xf0, 0x7c, 0xe1, 0xf9, 0x69, 0x9e, 0x1b, + 0xbc, 0x25, 0x53, 0x51, 0x46, 0x62, 0xed, 0x0a, 0x3b, 0x6a, 0xf9, 0x4d, 0x5e, 0x61, 0xef, 0x73, 0x00, 0x12, 0xd5, + 0x5e, 0x15, 0x0b, 0x72, 0x01, 0x1b, 0xd0, 0xa0, 0x3a, 0x0c, 0xf2, 0x12, 0x5f, 0x0a, 0x20, 0x00, 0xa3, 0x07, 0xef, + 0xd9, 0x71, 0x3a, 0x6d, 0x0c, 0xe3, 0x2e, 0xc7, 0x04, 0x4a, 0xae, 0xd9, 0x54, 0x12, 0xf2, 0x26, 0x27, 0x9c, 0x7d, + 0x01, 0x2a, 0x84, 0x31, 0x80, 0x3c, 0x35, 0xb6, 0x58, 0xbd, 0x7e, 0x2b, 0xd5, 0xe7, 0x04, 0xa3, 0xb0, 0x97, 0x98, + 0x7d, 0xa1, 0x1b, 0x11, 0x11, 0x39, 0x8b, 0x39, 0xb9, 0xf4, 0xda, 0x55, 0xea, 0xfb, 0xd1, 0xd9, 0x27, 0x04, 0xf4, + 0xed, 0xe4, 0x5b, 0x73, 0x13, 0x0e, 0xc7, 0x97, 0x2c, 0x33, 0xd1, 0x7f, 0xae, 0x16, 0xc8, 0xb3, 0x32, 0xe7, 0x3d, + 0x57, 0xc7, 0xae, 0x89, 0x34, 0xc5, 0xa6, 0xa0, 0x53, 0x70, 0x4a, 0x10, 0x41, 0x5b, 0x70, 0x92, 0xe4, 0x90, 0x88, + 0xca, 0x40, 0x7f, 0x36, 0x15, 0x4b, 0x30, 0x5b, 0xc3, 0x52, 0x9d, 0xd7, 0x5a, 0xec, 0x9a, 0x1f, 0xb2, 0x27, 0x99, + 0x65, 0xc3, 0x45, 0xea, 0x4c, 0x27, 0xc5, 0x75, 0x64, 0x8d, 0xc2, 0xd4, 0xb8, 0x8b, 0xc5, 0xab, 0x84, 0x2b, 0xa9, + 0xdc, 0xa4, 0x4a, 0xbc, 0xd9, 0xa8, 0x19, 0x8f, 0xc6, 0xe5, 0x8f, 0x6d, 0x76, 0xf4, 0x46, 0x4a, 0xc0, 0xe3, 0x21, + 0xd5, 0xde, 0xa6, 0x33, 0x6e, 0x43, 0xfe, 0xed, 0xea, 0x69, 0x0b, 0x58, 0xfc, 0x4f, 0x7a, 0x18, 0xe2, 0xff, 0x8d, + 0x0a, 0x8a, 0xc8, 0x36, 0x42, 0x78, 0x14, 0x02, 0x84, 0xfb, 0xc2, 0xc9, 0x0b, 0x62, 0x51, 0xc4, 0xa3, 0xb0, 0xf7, + 0x3a, 0x6b, 0xe5, 0x12, 0x16, 0x7f, 0x1f, 0x74, 0xff, 0x8e, 0x42, 0xe7, 0x5c, 0xaf, 0xf1, 0xa7, 0xe2, 0xc7, 0x1f, + 0x39, 0x76, 0x74, 0x28, 0xc2, 0x4d, 0x0c, 0xad, 0x04, 0xcf, 0xb6, 0x44, 0x75, 0xac, 0x0a, 0x46, 0x84, 0x30, 0x07, + 0xa9, 0x6a, 0xc1, 0x04, 0xbb, 0xf0, 0x13, 0x2c, 0x3e, 0x10, 0x4e, 0xff, 0x1b, 0x1a, 0xb7, 0x09, 0x61, 0x36, 0x58, + 0xe5, 0xa8, 0x93, 0x27, 0xf1, 0x6a, 0x9f, 0xf9, 0x23, 0xe3, 0xd6, 0x57, 0x5f, 0xa4, 0xd5, 0x48, 0xf6, 0x14, 0x33, + 0x38, 0xbc, 0x76, 0x4b, 0x99, 0x19, 0x9f, 0x23, 0x26, 0x55, 0xf3, 0xe4, 0x45, 0x41, 0xa9, 0xa1, 0xae, 0x59, 0x9e, + 0x0b, 0xf3, 0x9c, 0xe1, 0xb9, 0xc0, 0x60, 0x2c, 0xeb, 0xb2, 0xc6, 0x19, 0x9f, 0xc6, 0x56, 0x0c, 0x8c, 0x3b, 0x1e, + 0x0e, 0x7a, 0x8e, 0x39, 0x54, 0x80, 0x5e, 0x04, 0x6e, 0x22, 0x3e, 0xe9, 0x25, 0x70, 0xea, 0xd4, 0x86, 0xf3, 0xcc, + 0x5c, 0x8e, 0x98, 0xf2, 0x0c, 0xa7, 0x98, 0xd2, 0xef, 0xdc, 0x26, 0xd2, 0x6e, 0x7d, 0xad, 0xc9, 0x47, 0xc1, 0xad, + 0x7a, 0x3a, 0xac, 0x23, 0x1a, 0x97, 0x16, 0xc1, 0x93, 0x45, 0x79, 0xd8, 0xbf, 0xf1, 0x9c, 0x5f, 0x89, 0xb4, 0x93, + 0x52, 0x25, 0x3a, 0x9f, 0x79, 0x0e, 0x42, 0x48, 0x93, 0x96, 0xd0, 0xf6, 0xb9, 0x20, 0x25, 0xc6, 0xb5, 0x53, 0x8a, + 0xd8, 0xcd, 0xc1, 0xfe, 0xd7, 0x53, 0xb9, 0x48, 0xab, 0xeb, 0x87, 0x77, 0x8b, 0x85, 0xd8, 0x4e, 0xc8, 0x79, 0x2e, + 0x64, 0xef, 0xfd, 0x6b, 0x12, 0x5e, 0x27, 0xe5, 0x97, 0xfd, 0x20, 0x71, 0xef, 0x5c, 0x6a, 0xfe, 0x5f, 0x38, 0x6c, + 0x7a, 0xf4, 0xca, 0xc1, 0x6c, 0x33, 0x01, 0x93, 0xa3, 0x52, 0x55, 0xdf, 0x8f, 0x80, 0xd4, 0xf0, 0x30, 0x40, 0x59, + 0xc5, 0xfe, 0x41, 0xbd, 0x25, 0x00, 0xb8, 0xd4, 0xb3, 0xfd, 0xb0, 0xb4, 0xbf, 0xfb, 0x61, 0xab, 0x13, 0x2e, 0x57, + 0x4a, 0xfe, 0x1b, 0x18, 0x42, 0x97, 0x86, 0xe4, 0xb0, 0x75, 0xd6, 0x03, 0x21, 0x77, 0x9e, 0x73, 0x8a, 0x51, 0x5c, + 0x4b, 0xbe, 0x60, 0xfb, 0x91, 0x94, 0xd7, 0xc9, 0x7c, 0x7e, 0xb1, 0xb5, 0x82, 0xcf, 0x3a, 0xa9, 0xbf, 0xa8, 0x44, + 0xff, 0x05, 0xec, 0x3b, 0x88, 0x0f, 0xcf, 0x13, 0xab, 0xca, 0x0f, 0x1d, 0x87, 0x23, 0xac, 0xb0, 0x95, 0x2a, 0x0d, + 0xcd, 0x23, 0xd3, 0xc7, 0x94, 0x9c, 0x9a, 0x80, 0x71, 0x48, 0x72, 0xb6, 0x56, 0x91, 0x4b, 0x92, 0x6a, 0x16, 0xee, + 0xeb, 0x06, 0x9f, 0x84, 0xe9, 0x92, 0x56, 0xe1, 0x1e, 0xa0, 0x7f, 0x0c, 0x7a, 0xfe, 0xcf, 0x4a, 0x71, 0xc0, 0xb0, + 0xea, 0x85, 0x4c, 0xfc, 0x64, 0x00, 0xd7, 0x66, 0x9e, 0xcc, 0x71, 0xe3, 0x6d, 0xd4, 0x47, 0x6d, 0x4b, 0xc0, 0x4f, + 0xa2, 0x27, 0x8f, 0x61, 0x4c, 0x16, 0x86, 0x02, 0x77, 0x2e, 0xf5, 0x73, 0xb0, 0x70, 0xc3, 0x31, 0xfa, 0x86, 0xe0, + 0x5b, 0xfe, 0xfc, 0x0e, 0xd4, 0x71, 0x8c, 0x86, 0x2e, 0x8d, 0xa4, 0xf4, 0x3b, 0x14, 0x44, 0x1a, 0x48, 0xa0, 0x79, + 0xee, 0x6b, 0x28, 0x9c, 0x4e, 0x22, 0x3b, 0xc9, 0x11, 0xd6, 0xce, 0x82, 0x59, 0xab, 0x9c, 0xbe, 0x9e, 0xed, 0x3b, + 0x0c, 0xd0, 0xb5, 0xcb, 0xe9, 0x7d, 0xae, 0xbf, 0x95, 0xa9, 0x9f, 0x2b, 0x84, 0x96, 0x65, 0x9b, 0x9d, 0x43, 0x20, + 0x94, 0x6b, 0xf5, 0x34, 0x44, 0xc1, 0x10, 0xef, 0x74, 0xac, 0x6e, 0xd0, 0x47, 0x2a, 0x5a, 0xe7, 0xab, 0x0d, 0xda, + 0x7c, 0xab, 0xf0, 0xd2, 0xf5, 0xca, 0xf6, 0x34, 0x24, 0xc5, 0x34, 0x62, 0x38, 0xf8, 0x66, 0x42, 0x67, 0xf2, 0x3e, + 0x6e, 0x90, 0x4d, 0x9c, 0x21, 0x79, 0xb8, 0x8e, 0x58, 0xba, 0x4a, 0x58, 0x54, 0xb6, 0xf0, 0x74, 0x4a, 0x3b, 0x5c, + 0xdd, 0x15, 0xe1, 0xe6, 0x4c, 0x06, 0x6a, 0xb9, 0x8e, 0xbe, 0x89, 0xc4, 0x20, 0x07, 0x6c, 0xb9, 0x4e, 0x1f, 0x1d, + 0x55, 0xaa, 0x90, 0xe9, 0x76, 0xde, 0xbc, 0x86, 0x05, 0x87, 0x2d, 0x63, 0x29, 0x0d, 0x30, 0xf0, 0x5d, 0x7b, 0x79, + 0x5f, 0x6d, 0x76, 0x2a, 0x3c, 0xe6, 0xbc, 0x4b, 0x69, 0x91, 0x57, 0xa9, 0x7f, 0x6e, 0xe3, 0xb5, 0xf9, 0xd5, 0xdc, + 0x46, 0x17, 0xbc, 0x39, 0x27, 0x3a, 0xad, 0x6f, 0x31, 0x5e, 0x76, 0x88, 0x68, 0xe7, 0x3e, 0x97, 0x79, 0xba, 0x85, + 0xd4, 0x62, 0xcc, 0x3a, 0xc7, 0x4c, 0x4f, 0x4b, 0x8b, 0xf2, 0x11, 0x63, 0x50, 0xc9, 0x9d, 0xf2, 0xcd, 0xc8, 0x53, + 0x8d, 0xc2, 0x10, 0xab, 0x6d, 0xe8, 0x19, 0xb4, 0xd2, 0x4f, 0x34, 0xfb, 0xf6, 0x76, 0xb3, 0x06, 0xa8, 0xc4, 0x62, + 0x1f, 0x99, 0x65, 0xe1, 0xe3, 0x72, 0x07, 0xa1, 0x83, 0x34, 0xb7, 0x93, 0xc3, 0x38, 0x3d, 0x46, 0x2b, 0xf4, 0xbb, + 0xf6, 0x14, 0xb3, 0x80, 0x48, 0x52, 0x2f, 0x90, 0xce, 0x01, 0x77, 0x49, 0xfd, 0x59, 0x9c, 0x19, 0x61, 0x3e, 0x7a, + 0x29, 0xa1, 0xdc, 0x25, 0x94, 0x1b, 0xaf, 0xf3, 0x24, 0x00, 0x3e, 0x89, 0xb6, 0xd7, 0x56, 0x9c, 0xe6, 0x9c, 0xd7, + 0xb6, 0x08, 0xc3, 0xae, 0xeb, 0xc5, 0x48, 0x72, 0x33, 0x7b, 0x61, 0xcc, 0x18, 0x04, 0x22, 0xa8, 0xe8, 0xe6, 0x80, + 0xc9, 0x98, 0x3a, 0xc2, 0x41, 0xe7, 0x4f, 0xb2, 0xa9, 0xa6, 0xb4, 0x98, 0xda, 0xd1, 0xff, 0xce, 0x1e, 0xfc, 0xf0, + 0x68, 0x7a, 0xfe, 0xeb, 0xdb, 0xfc, 0x1a, 0x34, 0x41, 0x0f, 0xe1, 0x7e, 0x77, 0x35, 0x53, 0xa1, 0xa0, 0xb0, 0xb2, + 0x7d, 0x69, 0x01, 0x50, 0x63, 0x2a, 0x4a, 0x57, 0xd7, 0xd6, 0xfd, 0x49, 0xc6, 0xa7, 0x35, 0x6d, 0x7c, 0x1d, 0xa8, + 0xf2, 0xb5, 0xa1, 0x6c, 0xdd, 0xe1, 0xf3, 0x50, 0xcd, 0x78, 0x0a, 0xda, 0x5c, 0x18, 0xfc, 0x0a, 0x39, 0x6e, 0x42, + 0x27, 0x43, 0x95, 0x4d, 0xb3, 0x13, 0x6f, 0x59, 0x25, 0x6b, 0x29, 0x39, 0x91, 0x12, 0xf6, 0xaa, 0xf2, 0x47, 0xdd, + 0x93, 0xd4, 0x96, 0x6a, 0x70, 0x83, 0x13, 0x94, 0x36, 0x3a, 0xfa, 0x2a, 0x6e, 0xe4, 0xa7, 0x1b, 0x40, 0x44, 0x4d, + 0x4f, 0x87, 0xf6, 0x76, 0xfe, 0x79, 0x6f, 0x8c, 0xb0, 0x49, 0x05, 0x3f, 0xca, 0xa8, 0xa9, 0x1a, 0x9e, 0xfb, 0x53, + 0xaf, 0x6c, 0x87, 0x67, 0xba, 0x9b, 0x2c, 0x6a, 0x8b, 0x3e, 0xe3, 0x02, 0xac, 0x9a, 0x68, 0xaa, 0x71, 0xa1, 0x08, + 0x63, 0x1a, 0x6f, 0xce, 0xda, 0x89, 0xb5, 0xea, 0xd5, 0xed, 0xac, 0x97, 0x1e, 0x6d, 0xb3, 0x05, 0x6a, 0xbc, 0x68, + 0xda, 0xf2, 0x2a, 0x7c, 0xb7, 0xa4, 0x9b, 0x95, 0x23, 0xc8, 0xdc, 0x04, 0xe8, 0x67, 0x3f, 0x64, 0x26, 0x7a, 0x07, + 0xe1, 0x4e, 0x2a, 0xd9, 0x93, 0x8a, 0xcd, 0x78, 0xbe, 0xbd, 0x72, 0x7e, 0x5a, 0x92, 0x6d, 0xc4, 0xda, 0x8d, 0x2a, + 0xe3, 0xb1, 0x35, 0xc8, 0xdb, 0x35, 0xd3, 0x59, 0xa2, 0xbf, 0x86, 0x7b, 0xfd, 0xf9, 0x76, 0x0d, 0x4d, 0xb7, 0xd5, + 0xdc, 0x79, 0xe9, 0xe4, 0x28, 0x29, 0x79, 0xf1, 0x03, 0x7b, 0x6c, 0xe1, 0x7c, 0xb0, 0xf1, 0x90, 0x60, 0x99, 0x8a, + 0x55, 0x9f, 0x55, 0xac, 0xf2, 0x2c, 0x11, 0x85, 0xd9, 0xd3, 0x2e, 0x80, 0xe3, 0x0f, 0x2b, 0xb9, 0x8a, 0x1f, 0x66, + 0x5a, 0xb5, 0x7c, 0x58, 0x08, 0xd5, 0xf4, 0x24, 0x10, 0x19, 0x98, 0x35, 0xf2, 0x70, 0x61, 0xea, 0x98, 0x4c, 0x4a, + 0x49, 0x20, 0x57, 0x58, 0x4d, 0xab, 0x6a, 0xd5, 0x87, 0x1f, 0x6e, 0xb8, 0x41, 0x5d, 0xf9, 0x67, 0x33, 0xae, 0xff, + 0xaf, 0x99, 0x89, 0xe5, 0xa0, 0xb1, 0xd0, 0xfa, 0x27, 0x2d, 0xcc, 0xfd, 0x08, 0xdd, 0x35, 0xc3, 0x95, 0xe9, 0x4b, + 0x14, 0xf3, 0x2b, 0x46, 0x66, 0x5b, 0xe4, 0xb5, 0x32, 0xd8, 0xc5, 0xde, 0x98, 0x49, 0x5b, 0x27, 0xc6, 0x34, 0x36, + 0x24, 0x56, 0x67, 0x51, 0x23, 0x43, 0x6a, 0x6b, 0xf3, 0x9d, 0xbe, 0xa2, 0xf9, 0x25, 0x2f, 0xf1, 0x97, 0x69, 0xc8, + 0xc2, 0x7c, 0xab, 0xe8, 0x8d, 0xf4, 0x1a, 0x7a, 0x09, 0xfe, 0x62, 0xe1, 0xde, 0x04, 0x78, 0x8e, 0x22, 0x2a, 0x46, + 0x63, 0xf0, 0x4d, 0x16, 0x07, 0x7a, 0xbf, 0xc2, 0xad, 0x20, 0xc3, 0x94, 0x15, 0xff, 0x5f, 0x03, 0xad, 0xf4, 0x2f, + 0x20, 0xf6, 0x0d, 0xbe, 0xc0, 0xca, 0x5b, 0x41, 0xb9, 0x87, 0xe9, 0xfe, 0x02, 0xdf, 0x0a, 0x71, 0x30, 0x68, 0x44, + 0x4d, 0x58, 0xeb, 0x3d, 0xc5, 0x38, 0x31, 0x3d, 0xf8, 0x67, 0x7a, 0x68, 0x25, 0x08, 0x87, 0x31, 0x86, 0x0e, 0x52, + 0x80, 0x60, 0xa5, 0x7c, 0xf5, 0xf5, 0xa1, 0x55, 0xa1, 0x64, 0xe4, 0xab, 0x66, 0x83, 0x4f, 0xb1, 0x9d, 0xa7, 0xd8, + 0x3e, 0x2b, 0x5f, 0x5a, 0x6b, 0x4d, 0x67, 0xab, 0x46, 0x7a, 0xbe, 0x0a, 0x2e, 0x30, 0xec, 0x60, 0xe0, 0xbc, 0x0a, + 0xbe, 0x22, 0x41, 0x93, 0x40, 0x58, 0x40, 0x83, 0xe7, 0x36, 0x94, 0x93, 0x0f, 0x09, 0x94, 0xba, 0x04, 0xbb, 0xcd, + 0x8f, 0x48, 0x6e, 0x03, 0x21, 0xb5, 0x18, 0x67, 0x0b, 0x7b, 0x70, 0x26, 0xcd, 0xfa, 0xb9, 0xb1, 0x1e, 0x5a, 0x89, + 0x92, 0x36, 0x5d, 0x9e, 0x0d, 0xae, 0x19, 0x29, 0xac, 0x93, 0xff, 0x2f, 0x39, 0x6e, 0xf3, 0xbd, 0x2a, 0x08, 0xae, + 0xc4, 0x49, 0x5f, 0xeb, 0x29, 0x28, 0x77, 0x3a, 0x74, 0xe0, 0x8e, 0x20, 0x4b, 0xd9, 0xf3, 0xcc, 0xd3, 0xe7, 0x76, + 0x81, 0x9d, 0x98, 0xed, 0x9e, 0x95, 0xce, 0x70, 0xc2, 0xf1, 0xfb, 0x05, 0xef, 0x2e, 0xfd, 0x39, 0xa4, 0xdc, 0xdf, + 0x57, 0x96, 0x8c, 0x77, 0xc7, 0xdd, 0xb3, 0x3f, 0xc7, 0x1b, 0xb7, 0xfc, 0x29, 0xf7, 0xc3, 0x6d, 0x24, 0xdd, 0xf0, + 0xaa, 0xf9, 0x17, 0x8f, 0x6c, 0xe5, 0x56, 0x42, 0xd0, 0x3a, 0x9d, 0xe3, 0x9b, 0xaf, 0x41, 0xa7, 0x12, 0xb6, 0xf8, + 0xf1, 0x5d, 0xf2, 0x5b, 0x63, 0x2f, 0x46, 0x61, 0x70, 0x68, 0xa6, 0x76, 0x95, 0x9c, 0xb7, 0xe0, 0xb6, 0x4c, 0xed, + 0x56, 0x81, 0x34, 0x7a, 0xe7, 0xb9, 0xfa, 0x3a, 0xfd, 0x54, 0x35, 0xff, 0x31, 0x73, 0x13, 0xf6, 0xb1, 0x42, 0x91, + 0xf8, 0x67, 0x6a, 0x74, 0x83, 0x91, 0xca, 0x03, 0x75, 0x81, 0x55, 0x4b, 0x82, 0xca, 0xdb, 0x11, 0x3f, 0xe5, 0xd7, + 0xdc, 0xe5, 0x48, 0x6c, 0x84, 0xfd, 0x69, 0x6c, 0x3e, 0x53, 0x9f, 0xed, 0x6c, 0x99, 0x65, 0xb7, 0x8f, 0x99, 0x87, + 0x47, 0xf9, 0x5b, 0xaa, 0x5b, 0xe4, 0x7b, 0xb3, 0x2c, 0x2f, 0xca, 0xec, 0xd4, 0x3e, 0x87, 0x4f, 0xb4, 0xd1, 0x24, + 0x7f, 0xe3, 0x71, 0x2f, 0xb1, 0x21, 0xd9, 0x34, 0x2f, 0x33, 0x87, 0xd8, 0xc3, 0xf3, 0x1b, 0x3f, 0x65, 0x53, 0x99, + 0x28, 0x38, 0x6d, 0x87, 0xa6, 0x03, 0xf7, 0x0d, 0xd4, 0x41, 0x15, 0x20, 0xa7, 0x2b, 0xb0, 0xd1, 0x80, 0x59, 0x6d, + 0xd4, 0x97, 0xa5, 0xb2, 0x8a, 0xb3, 0xae, 0xdb, 0x75, 0xfa, 0xc5, 0x46, 0x12, 0xb8, 0xb1, 0x47, 0x91, 0x3c, 0x9d, + 0x95, 0xae, 0xf1, 0x57, 0x79, 0xc9, 0x1c, 0xa5, 0x0f, 0xf8, 0xfc, 0x5b, 0xf0, 0x48, 0xfe, 0x52, 0x74, 0x8d, 0xab, + 0xdc, 0x66, 0x34, 0x6a, 0xcc, 0xc9, 0x78, 0x43, 0xd8, 0x58, 0x14, 0x55, 0x31, 0x35, 0xa7, 0xfc, 0x9c, 0x19, 0x8d, + 0xe4, 0x05, 0x09, 0xf2, 0xb9, 0xb7, 0xfb, 0x99, 0x5c, 0xf0, 0x2b, 0xd7, 0xa1, 0x57, 0x56, 0xa3, 0xe2, 0x4b, 0xe7, + 0xb8, 0xb7, 0xee, 0x50, 0x80, 0x0c, 0xd8, 0x43, 0x86, 0x9c, 0xf2, 0x56, 0xd3, 0xbe, 0x5e, 0x7e, 0xff, 0x82, 0x91, + 0x14, 0xab, 0xb2, 0xef, 0xc6, 0x26, 0x4c, 0x22, 0x3b, 0xc2, 0x5e, 0x35, 0xb2, 0x9b, 0x63, 0x11, 0x21, 0x21, 0x19, + 0xf7, 0x4c, 0xc8, 0xe8, 0xad, 0x5e, 0x26, 0x80, 0x73, 0xe2, 0x49, 0xe1, 0x4c, 0x4e, 0x9c, 0xc8, 0xb2, 0xb4, 0x15, + 0x5b, 0x62, 0xe1, 0x08, 0x5f, 0x6b, 0xca, 0x6c, 0xdb, 0x18, 0x2b, 0x68, 0x70, 0xcc, 0x01, 0xa2, 0x33, 0x9f, 0xf1, + 0x86, 0x09, 0xe7, 0xfc, 0xa9, 0xf2, 0xbe, 0x59, 0xa6, 0x41, 0x5f, 0x15, 0x2c, 0x6c, 0xb7, 0x9e, 0x20, 0xca, 0x34, + 0x03, 0x03, 0xf2, 0x6d, 0x85, 0x6a, 0xfe, 0x95, 0x97, 0x28, 0xf8, 0xee, 0x32, 0xf2, 0xf3, 0xe7, 0x70, 0x1b, 0x21, + 0x1a, 0x04, 0x8d, 0xed, 0x85, 0x51, 0xb2, 0xb3, 0xac, 0x86, 0x5c, 0x84, 0x93, 0xe0, 0x83, 0xdd, 0x53, 0xb8, 0x3e, + 0x87, 0xa4, 0x97, 0xe0, 0x29, 0x30, 0x4f, 0x68, 0xa4, 0xb4, 0xdf, 0xf7, 0x2e, 0x08, 0x84, 0xe6, 0x2d, 0x8f, 0x70, + 0x40, 0x32, 0x62, 0x36, 0x16, 0x1e, 0x37, 0x0b, 0x97, 0x56, 0xc1, 0xa9, 0xcb, 0x6a, 0xd4, 0x15, 0xc9, 0x25, 0x85, + 0x34, 0x63, 0xf0, 0x60, 0x74, 0x24, 0x24, 0x96, 0x85, 0x60, 0xcc, 0x86, 0xbf, 0xf5, 0x02, 0xeb, 0xa7, 0x39, 0x00, + 0xf6, 0x04, 0xf1, 0xd8, 0x84, 0xe9, 0x0d, 0x44, 0x78, 0xb6, 0x2d, 0x0f, 0x98, 0xf7, 0x05, 0x1a, 0xcf, 0xf9, 0x98, + 0x52, 0xe6, 0x7c, 0x01, 0x2e, 0x33, 0xf1, 0x62, 0x20, 0x87, 0x80, 0x7b, 0x88, 0x87, 0xfc, 0xe0, 0xb1, 0x97, 0x60, + 0x67, 0x64, 0xa5, 0xbb, 0x5d, 0xbb, 0x71, 0xcc, 0x2d, 0x45, 0x0e, 0xbc, 0xd8, 0x3f, 0x38, 0xac, 0x6b, 0xb1, 0x4a, + 0xbf, 0x69, 0xa0, 0xd2, 0xbf, 0xfa, 0xf7, 0xcd, 0xe7, 0xc8, 0xb7, 0xcf, 0xb5, 0xd4, 0xa2, 0xf8, 0x81, 0x77, 0x56, + 0x93, 0x82, 0x76, 0xff, 0xab, 0x26, 0x23, 0x5f, 0x52, 0x5a, 0x2d, 0x8b, 0x4f, 0xb5, 0x8b, 0x9e, 0xa2, 0x5a, 0x36, + 0x79, 0x6e, 0x0f, 0x49, 0x8e, 0x5e, 0x6b, 0x19, 0x56, 0xb5, 0x3d, 0xea, 0x83, 0xbd, 0xd7, 0x7b, 0x41, 0x1e, 0x52, + 0x0f, 0x89, 0xaa, 0x37, 0x5e, 0x40, 0xc5, 0x7f, 0xf5, 0x95, 0xea, 0x99, 0x5f, 0xd0, 0x90, 0x37, 0xca, 0x31, 0xbe, + 0x6c, 0x9c, 0xb6, 0xae, 0x1e, 0xc5, 0x14, 0xac, 0x14, 0x65, 0x65, 0x88, 0x1b, 0x59, 0xa1, 0x6b, 0x44, 0x5a, 0x03, + 0x7f, 0x63, 0xa3, 0x14, 0x5b, 0x4d, 0xb5, 0x91, 0xf4, 0xdf, 0x99, 0xfe, 0x0f, 0x89, 0xec, 0xff, 0x14, 0xc7, 0xfe, + 0x8f, 0x73, 0x84, 0x16, 0xf6, 0x8d, 0x65, 0x44, 0xc0, 0x15, 0x4d, 0x8a, 0xe3, 0x2b, 0x83, 0xb3, 0x44, 0x50, 0xa3, + 0x89, 0xc8, 0x6e, 0x3c, 0x51, 0x99, 0x11, 0x79, 0x6e, 0x15, 0x3c, 0x4b, 0x7b, 0x74, 0x4f, 0xee, 0xef, 0x9c, 0x40, + 0x94, 0x63, 0x52, 0x6d, 0x6f, 0xc6, 0x9c, 0xc3, 0x10, 0x17, 0x93, 0x8b, 0x0a, 0x60, 0x04, 0x37, 0x84, 0x6c, 0x2c, + 0x81, 0x8e, 0x92, 0xec, 0x47, 0x23, 0xe6, 0x00, 0x34, 0xc0, 0x7e, 0xd9, 0x20, 0xb0, 0x1c, 0xcc, 0x30, 0x43, 0x30, + 0x3a, 0xaf, 0x0e, 0xf0, 0x39, 0x66, 0x7b, 0xc7, 0x26, 0xf8, 0xb0, 0x32, 0x07, 0x3b, 0xd0, 0x20, 0x1c, 0x30, 0x8f, + 0x66, 0x79, 0x26, 0x28, 0x9a, 0xe0, 0x23, 0xea, 0x2c, 0xfb, 0x5c, 0xfc, 0x92, 0xa5, 0xad, 0xd7, 0x25, 0x87, 0x2e, + 0x16, 0x9f, 0x66, 0xd6, 0x50, 0xfa, 0x13, 0xf8, 0x5f, 0x83, 0x3b, 0xb0, 0xc7, 0x1d, 0x24, 0xc2, 0x56, 0x14, 0x4e, + 0xa5, 0xea, 0x9f, 0x5d, 0x06, 0x84, 0x92, 0x9e, 0x66, 0x48, 0xb9, 0x40, 0xeb, 0x1a, 0xe2, 0x1a, 0x6c, 0x0c, 0x86, + 0xed, 0x8c, 0x67, 0x9a, 0xdb, 0xd6, 0x33, 0xe3, 0xa4, 0x5e, 0xa3, 0x4d, 0x46, 0x87, 0x64, 0x5e, 0x44, 0x99, 0xbb, + 0xc8, 0x2f, 0x47, 0x4a, 0xad, 0xb9, 0x51, 0xac, 0x31, 0xe5, 0xa5, 0xd3, 0xec, 0xc3, 0x39, 0x82, 0xd3, 0x45, 0x50, + 0xf5, 0xc3, 0x1e, 0x9c, 0xb5, 0x31, 0xf8, 0x91, 0x62, 0x51, 0x20, 0xbd, 0x5d, 0x11, 0x52, 0xf3, 0x93, 0x1d, 0xab, + 0x59, 0x4c, 0x4b, 0x6f, 0x17, 0x1e, 0xce, 0xb7, 0xc5, 0x14, 0x64, 0x1c, 0x08, 0xf9, 0x23, 0x18, 0xd8, 0x14, 0x77, + 0x66, 0xa2, 0x2d, 0x82, 0xe0, 0x04, 0xa1, 0x8c, 0x48, 0x87, 0x6f, 0x45, 0x29, 0x2a, 0x02, 0x91, 0xfb, 0xd1, 0x7b, + 0x4c, 0xcb, 0x6a, 0x28, 0xad, 0x81, 0x3d, 0x2a, 0x2a, 0x06, 0xca, 0xeb, 0x4c, 0x68, 0x38, 0x45, 0x8b, 0x90, 0xa9, + 0x78, 0xb3, 0x00, 0x2b, 0x37, 0xf8, 0xa4, 0xc5, 0x4a, 0xcf, 0x58, 0xf6, 0x07, 0xf9, 0x4a, 0x59, 0xd1, 0x30, 0x81, + 0xee, 0x31, 0x55, 0xdb, 0x7f, 0xe3, 0xa2, 0x8d, 0xb9, 0x01, 0x7e, 0x06, 0x8c, 0xe2, 0x7a, 0x85, 0x09, 0xab, 0x15, + 0xdc, 0x01, 0x2c, 0xbd, 0x71, 0x6f, 0xd5, 0x4c, 0xc9, 0xa7, 0x5c, 0x49, 0x29, 0x13, 0xac, 0x77, 0x2a, 0x4b, 0x70, + 0xf2, 0x21, 0x04, 0x43, 0x7c, 0xf7, 0x65, 0xe6, 0xd7, 0x6b, 0x9e, 0xda, 0x84, 0x27, 0xd1, 0x9e, 0xf6, 0xd1, 0x37, + 0x6e, 0xc3, 0xab, 0x16, 0x43, 0x5c, 0x9d, 0x65, 0xe7, 0xe4, 0x4b, 0x64, 0x4d, 0xe5, 0x80, 0x1f, 0xb1, 0x1a, 0xea, + 0x12, 0xf8, 0x8c, 0x98, 0x37, 0x0c, 0xe6, 0x6f, 0x74, 0x8a, 0xc5, 0xbc, 0xa9, 0x22, 0xc5, 0xe1, 0xfe, 0x08, 0x8b, + 0x8c, 0x4b, 0x94, 0x23, 0x7d, 0xc8, 0xbf, 0x83, 0xc1, 0xa8, 0xd2, 0xcd, 0x8a, 0xfa, 0x9a, 0xf1, 0xbc, 0xed, 0x63, + 0x6b, 0x12, 0xb6, 0x00, 0x6b, 0x91, 0xf1, 0x04, 0xe8, 0xbe, 0x56, 0x6f, 0x0b, 0xd9, 0x9a, 0x68, 0x89, 0x86, 0xa6, + 0x50, 0xd4, 0x0d, 0x04, 0x13, 0x93, 0xd2, 0xee, 0xc0, 0x48, 0x82, 0xf1, 0xac, 0xa9, 0xfc, 0x82, 0xfc, 0xb8, 0x5e, + 0xc4, 0xd6, 0x98, 0x0b, 0x1d, 0x33, 0xa2, 0x49, 0x4d, 0x9a, 0x09, 0x88, 0x05, 0xf0, 0x72, 0x16, 0x3d, 0xac, 0xf3, + 0x84, 0x53, 0x73, 0xef, 0xd4, 0x11, 0x33, 0x30, 0x40, 0xa7, 0x10, 0xa9, 0x14, 0x3f, 0xbc, 0x0f, 0x52, 0x0a, 0x04, + 0xa0, 0xec, 0x98, 0x0d, 0x2d, 0x29, 0xa8, 0x4f, 0x6b, 0x97, 0x99, 0x5a, 0xdb, 0x1a, 0xfe, 0x94, 0xd9, 0xac, 0x45, + 0x55, 0xcc, 0x1f, 0x2e, 0xf3, 0x8b, 0x98, 0x71, 0xd1, 0xf0, 0x09, 0x43, 0xd5, 0x61, 0x05, 0x7a, 0x8f, 0x9b, 0xbc, + 0x5e, 0x99, 0xf0, 0x7e, 0x5e, 0xef, 0x9b, 0xfb, 0x22, 0x16, 0x5e, 0x14, 0x38, 0xf7, 0xa5, 0x82, 0x97, 0x86, 0x13, + 0xb8, 0xc4, 0x43, 0x99, 0xf9, 0x54, 0xb6, 0x95, 0x99, 0xa2, 0x04, 0x25, 0xb5, 0x88, 0x5c, 0x92, 0x0b, 0x82, 0x94, + 0x8a, 0x97, 0x81, 0x50, 0xdb, 0xb7, 0x0b, 0x90, 0xbd, 0xaf, 0x2d, 0xe3, 0xb5, 0x64, 0xe7, 0x22, 0x94, 0xcd, 0x66, + 0x5c, 0xdb, 0xcb, 0x69, 0xb7, 0xbf, 0x95, 0x41, 0x35, 0x00, 0x25, 0xb3, 0xe1, 0x32, 0xe2, 0x9b, 0x9b, 0x1d, 0x0a, + 0x08, 0xed, 0xfc, 0x6d, 0x57, 0xca, 0x2c, 0xcc, 0x41, 0xd7, 0xec, 0xa8, 0xf8, 0x17, 0xe5, 0xdd, 0x59, 0xed, 0x66, + 0x7c, 0xc9, 0x3b, 0x18, 0xdf, 0x14, 0xca, 0x01, 0x2e, 0x79, 0x21, 0xeb, 0x07, 0x6e, 0x94, 0xc8, 0x12, 0xc2, 0x32, + 0xbb, 0xa8, 0x15, 0x95, 0xc9, 0x98, 0x36, 0xbd, 0xad, 0xc8, 0x66, 0x23, 0x63, 0x5d, 0x96, 0x68, 0x79, 0x6e, 0xc5, + 0xc9, 0xab, 0x87, 0x3f, 0x52, 0xe5, 0xf4, 0x7d, 0x89, 0xfa, 0xd4, 0x07, 0x77, 0x6f, 0x2b, 0xb3, 0x84, 0x4f, 0x4d, + 0x13, 0x45, 0x70, 0x07, 0xcc, 0x55, 0xb6, 0x22, 0x6a, 0x61, 0x48, 0xfd, 0x17, 0x5e, 0xfa, 0xc6, 0x13, 0xaa, 0xb6, + 0x73, 0xd5, 0xab, 0x39, 0x24, 0x66, 0x8c, 0xe7, 0x68, 0xf5, 0x01, 0xb2, 0x81, 0xae, 0xcd, 0xbe, 0x02, 0x02, 0xaf, + 0x99, 0xfc, 0xf2, 0xdb, 0x61, 0xcc, 0xd9, 0x6d, 0x5e, 0x68, 0x64, 0x2b, 0x67, 0xd9, 0xc1, 0x8d, 0xb4, 0x55, 0x7b, + 0x3c, 0xd3, 0x4d, 0x5c, 0x69, 0x99, 0x8c, 0x31, 0xbf, 0xad, 0xea, 0xab, 0x05, 0xdf, 0x99, 0xdb, 0xce, 0x4a, 0xa6, + 0x91, 0xab, 0xd5, 0x57, 0x78, 0x5e, 0x34, 0x41, 0x27, 0x2e, 0x31, 0x53, 0xab, 0xea, 0xb7, 0xaa, 0x1c, 0x15, 0x15, + 0xcb, 0xb9, 0xd5, 0xa6, 0xca, 0x6b, 0xb7, 0xa8, 0xdf, 0x9f, 0x8d, 0x09, 0x41, 0x65, 0x8a, 0xcc, 0x58, 0xa2, 0x8b, + 0x78, 0xa1, 0x9f, 0xd3, 0xba, 0xa8, 0xe8, 0xf1, 0xca, 0xaa, 0x86, 0x18, 0x02, 0xb7, 0xda, 0xc9, 0x20, 0x65, 0x16, + 0x89, 0x79, 0x16, 0x31, 0xdb, 0xeb, 0xd1, 0xb6, 0x39, 0x1b, 0x06, 0x6e, 0xd2, 0x39, 0x81, 0x73, 0x12, 0xfe, 0xa6, + 0x32, 0xdb, 0xb0, 0xa2, 0x6e, 0x79, 0x8d, 0xae, 0xa2, 0x6e, 0xcd, 0x79, 0x3d, 0x55, 0xc5, 0x0f, 0xd4, 0x71, 0xb5, + 0x5e, 0xdd, 0x88, 0x0e, 0x41, 0x81, 0x0b, 0xeb, 0xe7, 0x00, 0xfe, 0x6f, 0xab, 0x18, 0x1e, 0xec, 0xaa, 0x0f, 0xd5, + 0xaa, 0x69, 0x63, 0xdf, 0x38, 0x20, 0xb0, 0x58, 0x15, 0x5c, 0xa0, 0x33, 0xac, 0x50, 0x2f, 0x33, 0x6d, 0x30, 0x4c, + 0x8c, 0x53, 0x4b, 0xcf, 0xa5, 0x13, 0x11, 0x7d, 0x1a, 0x5e, 0xcc, 0x34, 0x77, 0x68, 0xb3, 0x55, 0x6e, 0x11, 0x6a, + 0x47, 0xb0, 0x08, 0x26, 0xd3, 0x65, 0x1c, 0x8c, 0xfc, 0x36, 0xb4, 0xd1, 0x00, 0x13, 0x97, 0x36, 0x65, 0x21, 0xc4, + 0xca, 0x02, 0xaa, 0xf7, 0x5d, 0xb0, 0x40, 0x30, 0xb3, 0x27, 0xa4, 0x5f, 0x41, 0x54, 0xf9, 0x69, 0x7c, 0x3e, 0xa9, + 0xa4, 0xb6, 0x9d, 0xaf, 0x73, 0x0d, 0x1c, 0xaa, 0xa6, 0xa2, 0x2a, 0x57, 0xb6, 0x21, 0x72, 0x18, 0xe4, 0x3a, 0x52, + 0x42, 0x2d, 0x91, 0x2f, 0x33, 0x4a, 0x27, 0x13, 0x46, 0xcb, 0xb5, 0x29, 0x56, 0x81, 0xb4, 0xd6, 0x85, 0x77, 0xf9, + 0x1f, 0x86, 0xb2, 0x0d, 0x7a, 0x21, 0x4a, 0xe4, 0xa2, 0x67, 0xf1, 0xe7, 0x6b, 0x9d, 0x03, 0xa4, 0xfa, 0x5f, 0xad, + 0x5c, 0x2a, 0x63, 0x03, 0xa7, 0xd8, 0x95, 0x91, 0xf8, 0x20, 0xad, 0x25, 0xfa, 0x3e, 0xd7, 0x31, 0xea, 0xba, 0x07, + 0x8d, 0xeb, 0x55, 0xb1, 0x18, 0xfa, 0xc5, 0x7b, 0x12, 0xdd, 0xc2, 0x97, 0x05, 0x25, 0x1d, 0xf4, 0xd3, 0x5d, 0xdd, + 0x5f, 0xa7, 0x84, 0x44, 0x65, 0x31, 0x31, 0x84, 0x55, 0x7e, 0x66, 0x38, 0x6c, 0x15, 0xd1, 0xac, 0xb6, 0xeb, 0xec, + 0xfb, 0x03, 0x05, 0x13, 0x88, 0xa0, 0xb0, 0xf8, 0xff, 0x73, 0x1f, 0x14, 0x68, 0xe0, 0xa4, 0xce, 0x8d, 0x4a, 0x4e, + 0xfb, 0xb9, 0x76, 0x7d, 0xa8, 0xbc, 0x04, 0x40, 0x56, 0x8f, 0x37, 0xb2, 0xbe, 0xe5, 0x77, 0x2b, 0x8b, 0x17, 0x30, + 0x96, 0x3f, 0x85, 0x4d, 0x56, 0x23, 0xda, 0x8c, 0xae, 0xd5, 0x6c, 0x94, 0x4f, 0x99, 0x18, 0x8e, 0x36, 0xd0, 0xf5, + 0xbb, 0x6d, 0x6f, 0x50, 0x5d, 0x58, 0x4b, 0xec, 0x3e, 0x60, 0x5d, 0xa9, 0x80, 0xfd, 0xac, 0xa9, 0xa5, 0x3b, 0x15, + 0xfc, 0xfc, 0x56, 0x4f, 0xa5, 0x59, 0xd8, 0x3a, 0x02, 0x7a, 0xf6, 0xd5, 0x15, 0x07, 0xc0, 0x07, 0x74, 0x0d, 0x0b, + 0x3d, 0x76, 0x2c, 0xf5, 0x99, 0x45, 0x94, 0x7d, 0xe6, 0x1e, 0x5f, 0xdf, 0x0c, 0x85, 0x87, 0x9d, 0xdb, 0x6f, 0xbd, + 0x2a, 0xc6, 0xf1, 0xc2, 0xba, 0xba, 0xc8, 0x05, 0xc5, 0x13, 0x92, 0x9c, 0x5f, 0xce, 0x61, 0xa8, 0x5a, 0x49, 0xc4, + 0x5c, 0x85, 0x04, 0x41, 0xed, 0xbb, 0x22, 0xe0, 0x31, 0x39, 0x5e, 0x81, 0xbb, 0x07, 0xa6, 0xa8, 0x9b, 0x1d, 0x42, + 0xe8, 0x96, 0xb4, 0xba, 0x5b, 0x91, 0x00, 0xd0, 0x2e, 0xd8, 0xfe, 0xc6, 0x79, 0x89, 0x2d, 0x68, 0x19, 0xad, 0x17, + 0x21, 0x0c, 0x44, 0x22, 0x85, 0x31, 0x72, 0x7a, 0x38, 0x5b, 0xd7, 0xa0, 0x18, 0xfa, 0x53, 0x17, 0x38, 0xf5, 0xe3, + 0xd9, 0xb6, 0x4b, 0x85, 0x64, 0x22, 0xe8, 0xd0, 0x14, 0x58, 0x96, 0x9d, 0x38, 0xed, 0x91, 0xe4, 0xfd, 0x7d, 0x9e, + 0x92, 0x15, 0x15, 0x3f, 0x94, 0xc3, 0xcf, 0x27, 0x24, 0x38, 0xd4, 0x13, 0x30, 0x83, 0x0e, 0x78, 0xa6, 0xf7, 0xa9, + 0x13, 0x23, 0x95, 0xf5, 0x0e, 0x38, 0x8a, 0x88, 0x32, 0xd3, 0x25, 0xbb, 0xc7, 0xed, 0xf1, 0x14, 0x70, 0x23, 0x63, + 0xda, 0x65, 0x8e, 0x61, 0x26, 0x30, 0x8e, 0xf9, 0x6a, 0x7c, 0x3e, 0xa2, 0x1f, 0xc7, 0x7d, 0x44, 0xc9, 0x45, 0xa5, + 0x86, 0xc2, 0x36, 0x66, 0x8b, 0x5a, 0xf4, 0xd4, 0xbe, 0x91, 0x48, 0x47, 0xaf, 0x60, 0x2c, 0x17, 0x98, 0x06, 0x2b, + 0x9d, 0xf3, 0x8a, 0x82, 0x15, 0x4d, 0x80, 0xb8, 0x0a, 0xe3, 0x94, 0x51, 0x6b, 0xcc, 0x52, 0x18, 0x5c, 0x51, 0x93, + 0x11, 0xc1, 0xaf, 0x26, 0xf4, 0xec, 0x63, 0x31, 0xdc, 0x93, 0x97, 0xc3, 0xa1, 0x1e, 0xad, 0xa7, 0x75, 0xf7, 0x06, + 0x16, 0x63, 0xc1, 0xb2, 0xc0, 0x9c, 0x9d, 0x0e, 0x3a, 0xaf, 0xd8, 0xd6, 0xd4, 0x3a, 0x5d, 0xad, 0x1f, 0x5a, 0xd9, + 0x28, 0x96, 0xd3, 0x24, 0x92, 0x38, 0x6f, 0xa6, 0x51, 0x8c, 0x3f, 0x34, 0x5c, 0xea, 0x86, 0xfa, 0xc4, 0x6f, 0xcd, + 0x5f, 0x4b, 0xa5, 0xbf, 0xce, 0x3f, 0x8a, 0x85, 0x1d, 0x9b, 0xd8, 0x6f, 0xb4, 0x60, 0x65, 0xd1, 0xd8, 0x40, 0xa8, + 0xea, 0x0b, 0x9e, 0x25, 0x2b, 0x95, 0x27, 0xdf, 0x8d, 0xd8, 0xd2, 0x02, 0x3f, 0x8f, 0xf2, 0x6a, 0xea, 0xcd, 0x88, + 0x41, 0xb5, 0x7c, 0x8a, 0x6a, 0x77, 0x72, 0x20, 0x5c, 0x26, 0x37, 0x56, 0x95, 0x07, 0x88, 0x9e, 0x5f, 0x96, 0x1e, + 0x09, 0x73, 0xa9, 0x98, 0x92, 0x06, 0xcf, 0x89, 0xa0, 0xb7, 0x30, 0x85, 0x18, 0x1e, 0x49, 0xdf, 0xa0, 0xf2, 0xee, + 0x8f, 0xeb, 0x7d, 0xaf, 0x0b, 0xdc, 0x19, 0x3b, 0xdf, 0x74, 0x2f, 0x9d, 0x19, 0x34, 0x7a, 0xfd, 0x73, 0xa8, 0x5a, + 0x84, 0xc1, 0x4e, 0xd3, 0x85, 0xa0, 0x09, 0xea, 0x5f, 0x8c, 0x06, 0xd6, 0x32, 0x5d, 0xeb, 0xed, 0x20, 0x53, 0x21, + 0x31, 0xfe, 0x5f, 0x64, 0xbc, 0x0c, 0x98, 0x9c, 0x8c, 0xe2, 0x16, 0x3c, 0x00, 0x37, 0xd3, 0x90, 0x0b, 0x94, 0xd9, + 0xc3, 0x13, 0xa8, 0xc9, 0x58, 0x84, 0x67, 0x39, 0xe6, 0x3a, 0x75, 0x60, 0x3d, 0xb2, 0x79, 0x58, 0xa3, 0x70, 0xb5, + 0x9c, 0x4d, 0x4e, 0xc9, 0x8e, 0x59, 0x5d, 0xed, 0x63, 0x77, 0xc6, 0x25, 0x9e, 0x39, 0x1f, 0xf2, 0x6d, 0xec, 0x71, + 0xc0, 0x1c, 0x87, 0x07, 0x0e, 0x33, 0xe7, 0x21, 0x82, 0x5c, 0x37, 0xc0, 0x16, 0x60, 0xb2, 0x93, 0xb5, 0x6b, 0x94, + 0xb0, 0x78, 0x73, 0x03, 0x20, 0x8f, 0x64, 0x12, 0x42, 0x2e, 0x1b, 0x7e, 0x96, 0x5a, 0xaa, 0x8f, 0x80, 0x8f, 0xd5, + 0x87, 0x9a, 0x06, 0x42, 0xdc, 0x36, 0x42, 0x1e, 0x30, 0x26, 0xae, 0xcc, 0x2f, 0xaa, 0x09, 0x2e, 0xf8, 0x2f, 0x7b, + 0x1d, 0x7f, 0xdd, 0xac, 0xfb, 0x9e, 0x21, 0x22, 0x1d, 0xac, 0x45, 0x64, 0xbd, 0x22, 0x85, 0xff, 0x86, 0xdf, 0x03, + 0x45, 0x09, 0xc5, 0x52, 0xb1, 0xfc, 0x88, 0xea, 0x1e, 0xe3, 0x1e, 0xf2, 0xde, 0x4e, 0x7e, 0x1f, 0x09, 0x83, 0x1e, + 0x50, 0x63, 0x94, 0xa4, 0x38, 0x52, 0xab, 0x9e, 0x7b, 0x14, 0x2c, 0x85, 0xa5, 0x86, 0xe7, 0x88, 0xd2, 0xd5, 0xf7, + 0x0a, 0xb5, 0xf0, 0x1f, 0x2d, 0x6d, 0x9e, 0x86, 0x7d, 0x44, 0xf2, 0x4d, 0x46, 0xd7, 0xc8, 0x42, 0x25, 0x51, 0x78, + 0x23, 0x04, 0x9e, 0x73, 0xc6, 0x53, 0x7d, 0x84, 0x98, 0x07, 0xa2, 0xc9, 0xc8, 0xf5, 0x80, 0xde, 0xd1, 0xe6, 0xe8, + 0x45, 0x72, 0x4c, 0xdf, 0xb4, 0x0f, 0x83, 0xb0, 0x1d, 0x5b, 0x5c, 0x6a, 0x4c, 0x44, 0x6b, 0x5a, 0x75, 0xd9, 0x23, + 0x52, 0xef, 0x3c, 0x15, 0xa3, 0x04, 0x25, 0x72, 0x13, 0x15, 0xdd, 0x39, 0x4e, 0xed, 0xa2, 0xe8, 0xf6, 0x19, 0x4b, + 0xb8, 0x18, 0x55, 0x7c, 0x5f, 0x06, 0x9f, 0x44, 0x52, 0x34, 0x60, 0xd8, 0x80, 0xaf, 0xf7, 0xff, 0x60, 0xe8, 0x66, + 0x20, 0x97, 0xda, 0x30, 0x65, 0xf3, 0xe9, 0x9c, 0x66, 0x5b, 0xc3, 0x7d, 0x83, 0xd6, 0x97, 0x50, 0xcf, 0xb9, 0xcf, + 0x88, 0xdf, 0x4a, 0xcd, 0x2d, 0x26, 0xab, 0x36, 0x1b, 0x59, 0x4c, 0xd6, 0x61, 0xd5, 0x3d, 0x46, 0xa6, 0x10, 0xff, + 0x42, 0x93, 0x5c, 0x10, 0x1e, 0x55, 0xc9, 0x82, 0x7f, 0xd0, 0xcc, 0x60, 0xc3, 0x79, 0x9d, 0xfe, 0x8d, 0x32, 0x80, + 0xf7, 0x3b, 0xcb, 0x5a, 0x41, 0x35, 0x25, 0xb5, 0xe3, 0xba, 0x4b, 0xc7, 0x2f, 0x5d, 0xa0, 0x07, 0x99, 0x89, 0x67, + 0x47, 0x25, 0x21, 0x66, 0x81, 0x75, 0x2f, 0x91, 0xfa, 0xe6, 0x27, 0xe9, 0x91, 0x36, 0x78, 0xce, 0x42, 0xb4, 0xa0, + 0x17, 0x15, 0xfb, 0x7b, 0xa5, 0x34, 0xbd, 0x57, 0x76, 0x06, 0xaf, 0x8d, 0x99, 0xca, 0x41, 0xb0, 0xee, 0x12, 0x7a, + 0xb9, 0x3c, 0xc9, 0x8f, 0x6d, 0xe6, 0x92, 0xe6, 0x23, 0xe9, 0xef, 0xaa, 0x14, 0xeb, 0xc7, 0x80, 0xd6, 0xbf, 0xa6, + 0xcc, 0x92, 0x14, 0x68, 0x30, 0x58, 0x8d, 0x15, 0xdb, 0x04, 0x1c, 0x52, 0x68, 0x22, 0xa2, 0x89, 0x76, 0xdc, 0xee, + 0x68, 0x7c, 0x86, 0xd4, 0x47, 0xb8, 0x40, 0x32, 0xe0, 0x91, 0x43, 0x4c, 0x56, 0xc5, 0x2e, 0xc0, 0x17, 0xb8, 0x7d, + 0x3c, 0x83, 0x7e, 0xd8, 0x6e, 0xdd, 0x20, 0xe5, 0xa6, 0x1c, 0x17, 0x01, 0x6b, 0x08, 0x80, 0xa7, 0x5c, 0x13, 0xad, + 0x06, 0x52, 0x7d, 0x69, 0x04, 0xec, 0xbb, 0x83, 0xfa, 0x38, 0x9a, 0xa6, 0x8c, 0x65, 0xd3, 0xc4, 0x4b, 0xc9, 0x23, + 0xc4, 0x88, 0x7d, 0x85, 0x53, 0x8e, 0xc0, 0xbc, 0xc3, 0xef, 0xac, 0xd7, 0x42, 0x7a, 0x9b, 0xe8, 0x73, 0x93, 0x81, + 0x87, 0xe1, 0xc7, 0xd8, 0x7e, 0xd1, 0xd3, 0xce, 0xd6, 0x9c, 0xbf, 0x0a, 0x48, 0x46, 0x47, 0xe1, 0x5f, 0x85, 0x67, + 0xa5, 0x6d, 0x12, 0x42, 0xfc, 0x03, 0xd1, 0x75, 0x86, 0x33, 0x48, 0xb0, 0x48, 0x5f, 0x2e, 0x6a, 0x17, 0x39, 0x05, + 0x95, 0xf6, 0x99, 0xd5, 0xca, 0xb2, 0x7c, 0x7b, 0xfb, 0x8f, 0x73, 0x6b, 0x53, 0x8d, 0x0b, 0x1e, 0x72, 0x8d, 0xcb, + 0x56, 0x36, 0xfc, 0xa2, 0x8d, 0xf7, 0x96, 0x6c, 0x36, 0x72, 0xd5, 0x57, 0x2e, 0xe1, 0x9f, 0xf8, 0x51, 0x46, 0xb2, + 0xf1, 0x02, 0xac, 0x45, 0x6a, 0x79, 0xe4, 0xea, 0xa9, 0xed, 0x7b, 0xbd, 0x50, 0xac, 0x0c, 0xee, 0xfc, 0xe2, 0x38, + 0x41, 0x92, 0xca, 0x43, 0xfe, 0x9c, 0xaf, 0xe3, 0x1c, 0x3b, 0xab, 0xe9, 0x68, 0x45, 0xef, 0x48, 0x5d, 0x0e, 0x16, + 0x5b, 0x8e, 0x92, 0xf3, 0xc1, 0xb9, 0x6b, 0x86, 0x1e, 0x1c, 0x45, 0x3d, 0x9f, 0x29, 0x89, 0x05, 0x5c, 0x9a, 0xbb, + 0xa7, 0x08, 0x7a, 0x84, 0x48, 0x8c, 0xd0, 0xbb, 0xa0, 0x21, 0x18, 0xb6, 0x39, 0xdf, 0x64, 0x82, 0x36, 0x6b, 0xd1, + 0x2e, 0xe2, 0x17, 0xc3, 0xa2, 0xf0, 0xda, 0xbb, 0x7a, 0xe4, 0x8a, 0xe5, 0x12, 0x1a, 0x03, 0x59, 0x83, 0x62, 0xff, + 0x9a, 0x04, 0x3f, 0xe8, 0x9f, 0xad, 0x16, 0x1a, 0x2b, 0x53, 0xe6, 0xc7, 0x8c, 0x55, 0xea, 0x9c, 0xb5, 0xf4, 0x6e, + 0x6a, 0x17, 0x94, 0x9c, 0x6c, 0xe5, 0xfc, 0x5a, 0xcc, 0xbf, 0x1f, 0xaa, 0x9f, 0x66, 0xca, 0x3b, 0x84, 0x27, 0xcc, + 0xd7, 0x89, 0xa2, 0x80, 0xac, 0x9b, 0x25, 0xce, 0x52, 0x52, 0xc7, 0x2a, 0x89, 0x12, 0x63, 0x3b, 0x87, 0x47, 0x08, + 0x42, 0xd2, 0xd9, 0xac, 0xce, 0x8c, 0xc9, 0x95, 0x14, 0x6f, 0x87, 0x72, 0x25, 0x9c, 0xc5, 0x22, 0x4d, 0x50, 0x74, + 0xf9, 0x86, 0x5c, 0x2a, 0xf4, 0x54, 0x97, 0x76, 0x74, 0xa8, 0xf4, 0x80, 0x7f, 0x74, 0x73, 0x89, 0x99, 0x54, 0xa0, + 0x11, 0x9f, 0xc7, 0x96, 0x54, 0x22, 0x59, 0x15, 0x39, 0x0c, 0xd4, 0xca, 0xe4, 0x27, 0xdb, 0xe7, 0x32, 0x5a, 0x37, + 0x21, 0x70, 0xdd, 0xe6, 0x4a, 0xe2, 0xee, 0x5f, 0x26, 0xf3, 0x74, 0x00, 0xf6, 0xcb, 0x72, 0x9d, 0x37, 0x3a, 0xe1, + 0xf2, 0xe8, 0xec, 0x53, 0x13, 0xec, 0x78, 0x07, 0x2f, 0x26, 0x12, 0x04, 0x07, 0x89, 0x44, 0xa4, 0x82, 0x33, 0x90, + 0x78, 0x02, 0x03, 0x70, 0x72, 0xfe, 0x88, 0x9f, 0x17, 0x64, 0x79, 0x01, 0x5c, 0xe1, 0xa8, 0x02, 0x44, 0x82, 0x04, + 0x8d, 0x2e, 0xbc, 0x9b, 0x63, 0xf5, 0x9a, 0x2d, 0xb7, 0xab, 0xd2, 0x79, 0x50, 0x73, 0x24, 0x85, 0x92, 0x30, 0xe2, + 0x0c, 0x8b, 0x1f, 0x6c, 0x4a, 0x94, 0xaf, 0x1a, 0x81, 0x30, 0xb2, 0x58, 0xe2, 0x85, 0x46, 0x83, 0x00, 0x8f, 0x8f, + 0x90, 0x32, 0xd9, 0x36, 0xe3, 0x98, 0x7d, 0x4d, 0x89, 0x73, 0x86, 0xcc, 0x10, 0x4a, 0x06, 0xe6, 0x68, 0x09, 0x60, + 0x9d, 0xc5, 0x18, 0x4d, 0xa5, 0x29, 0x3e, 0x3f, 0x47, 0xad, 0xd6, 0x91, 0x57, 0x36, 0x43, 0xbd, 0x0d, 0x56, 0x4c, + 0x89, 0x00, 0xc7, 0x21, 0xa4, 0x97, 0xc0, 0x82, 0x32, 0xe6, 0xb6, 0x24, 0x97, 0x22, 0x3f, 0x24, 0x6b, 0x49, 0xd3, + 0x66, 0x60, 0x70, 0xae, 0x9b, 0x7c, 0x31, 0x1f, 0x8e, 0x92, 0x69, 0x15, 0x6c, 0x1a, 0xeb, 0xfe, 0x3d, 0x6c, 0x9a, + 0x6e, 0x72, 0xe5, 0xb6, 0x0a, 0xc6, 0x6a, 0xe6, 0x78, 0xc4, 0xe6, 0x6c, 0xc0, 0xcb, 0xef, 0x41, 0x1a, 0x2e, 0x1e, + 0x42, 0x64, 0xda, 0x4f, 0xfb, 0xa7, 0xd8, 0xbb, 0xe5, 0xf2, 0x64, 0x06, 0xce, 0xda, 0xd8, 0x1d, 0xa7, 0xa8, 0xae, + 0x25, 0x51, 0x2e, 0x09, 0x9b, 0x0f, 0x80, 0xa1, 0xd6, 0xf7, 0xa2, 0xec, 0xff, 0x2e, 0xe9, 0x89, 0xa2, 0xc2, 0x73, + 0x9d, 0xd7, 0x67, 0xa9, 0x3f, 0x80, 0x76, 0x1f, 0xc7, 0xe4, 0xce, 0x38, 0xcc, 0x11, 0x50, 0x99, 0xbd, 0x5f, 0xbf, + 0xa2, 0x83, 0xb6, 0x95, 0xea, 0x4f, 0x28, 0xce, 0x1f, 0x94, 0xd1, 0x3a, 0x5b, 0xe6, 0xfc, 0x6c, 0xc1, 0x40, 0x67, + 0x92, 0x96, 0x92, 0xca, 0x27, 0xfe, 0x87, 0xea, 0xf0, 0x31, 0xb5, 0x47, 0x8c, 0x4d, 0x24, 0x09, 0x7e, 0x92, 0x27, + 0x7c, 0x4c, 0x35, 0x13, 0xb5, 0x3d, 0x43, 0x8a, 0x7a, 0x63, 0x44, 0xa6, 0x52, 0x4d, 0x96, 0x15, 0x9b, 0x58, 0xe4, + 0x04, 0xbb, 0xba, 0xb0, 0x2e, 0x7d, 0xa2, 0x3e, 0xa5, 0xa6, 0xe6, 0x20, 0x5c, 0x18, 0x48, 0x77, 0xdb, 0x55, 0x8f, + 0x16, 0x4b, 0x5a, 0x28, 0x52, 0x12, 0x39, 0x89, 0xa4, 0x69, 0x1c, 0xa9, 0x0b, 0x60, 0x9e, 0xa3, 0xec, 0x56, 0xb2, + 0x06, 0x6b, 0x6b, 0x3c, 0x51, 0x27, 0xd5, 0x91, 0x9b, 0x3a, 0xcc, 0xac, 0xa7, 0x9a, 0xf9, 0x3f, 0x3b, 0x26, 0x52, + 0xd0, 0x71, 0xe5, 0x91, 0x27, 0x94, 0xe6, 0xcd, 0x54, 0xed, 0x64, 0x48, 0x8f, 0x3c, 0xd7, 0xdb, 0xa4, 0x63, 0x9f, + 0x0b, 0x25, 0x0e, 0xdd, 0x74, 0x39, 0xd5, 0x25, 0xf0, 0xf1, 0x55, 0xfc, 0x91, 0x50, 0x2c, 0x49, 0xa4, 0x61, 0xee, + 0x6c, 0x34, 0xb6, 0x34, 0x56, 0x97, 0x8a, 0x2c, 0x0e, 0x4b, 0x43, 0xd5, 0x55, 0x94, 0xc5, 0x1b, 0x35, 0xd8, 0x44, + 0xd4, 0x45, 0x7d, 0x0d, 0x3a, 0xa5, 0xf3, 0x1c, 0x68, 0xa9, 0xc5, 0xdd, 0x53, 0x41, 0xef, 0x27, 0x5c, 0x53, 0x01, + 0x0e, 0x26, 0x1e, 0xb9, 0x78, 0xc1, 0xe9, 0x32, 0xa7, 0x2b, 0xd8, 0x5f, 0x34, 0x52, 0x8d, 0x33, 0x0b, 0x55, 0x39, + 0x33, 0xaa, 0xda, 0x99, 0x75, 0xd3, 0x0d, 0x86, 0x39, 0x57, 0xbb, 0x1c, 0x59, 0x94, 0x25, 0x59, 0x1c, 0x57, 0xa6, + 0x89, 0xe7, 0xf6, 0xca, 0x65, 0xa4, 0xf3, 0x4a, 0xb6, 0x98, 0x8c, 0x89, 0x4b, 0x3d, 0x32, 0x3d, 0x31, 0xb2, 0x6c, + 0xa0, 0xb6, 0x4b, 0xaf, 0xa9, 0x3a, 0xf6, 0x8b, 0x3b, 0x16, 0x85, 0x97, 0xb9, 0xc6, 0xf4, 0x38, 0x09, 0x19, 0xf2, + 0xa5, 0x35, 0x50, 0x62, 0x1b, 0xfc, 0x58, 0x8e, 0xf6, 0xd3, 0x69, 0x09, 0xac, 0x49, 0x44, 0xa0, 0xea, 0xb5, 0x81, + 0xfc, 0xb8, 0x4d, 0x0f, 0xe9, 0xcb, 0x16, 0x2e, 0xca, 0x1f, 0xca, 0x61, 0x73, 0xe0, 0x10, 0x66, 0x02, 0xa3, 0x60, + 0xa1, 0xbc, 0x92, 0xc0, 0x26, 0xf0, 0x3b, 0x46, 0xcd, 0x76, 0xbb, 0xd2, 0xfb, 0x00, 0x32, 0x19, 0x37, 0x21, 0x3c, + 0x80, 0xc2, 0xeb, 0x29, 0x28, 0x57, 0x88, 0x03, 0xcd, 0x14, 0xa0, 0xc3, 0x0f, 0xe9, 0xc3, 0x13, 0x90, 0x1f, 0xd3, + 0xe1, 0x47, 0xb7, 0x72, 0x1b, 0x6d, 0x73, 0x2c, 0x4f, 0x95, 0x87, 0x6a, 0x1c, 0x21, 0x4a, 0x72, 0x61, 0xb1, 0xa8, + 0xdb, 0x2b, 0x57, 0xb4, 0xbd, 0xf1, 0x5e, 0xdf, 0xb0, 0x4d, 0x3a, 0xfe, 0x18, 0xe6, 0xb8, 0xc2, 0xa8, 0x46, 0x15, + 0x6c, 0xe9, 0x1d, 0xb0, 0xd5, 0x5d, 0x25, 0xb0, 0xc7, 0xa6, 0xb1, 0xb9, 0x00, 0x1d, 0x1a, 0xa2, 0x0c, 0xa4, 0x54, + 0x35, 0x0b, 0x64, 0x72, 0xf5, 0x29, 0xec, 0xb6, 0xa6, 0x31, 0x9b, 0x90, 0xf7, 0xbf, 0xa1, 0x79, 0x15, 0x96, 0x7c, + 0xc2, 0xfe, 0x10, 0xc9, 0x67, 0xf8, 0xd2, 0x47, 0x8d, 0xf8, 0x1e, 0xe0, 0x6a, 0x5f, 0x0a, 0x45, 0xa6, 0x38, 0xb6, + 0xc7, 0x6b, 0xd4, 0x26, 0xf3, 0xf0, 0x50, 0x47, 0x17, 0x36, 0xe4, 0x47, 0x38, 0x61, 0xfb, 0x31, 0xce, 0x93, 0x0b, + 0x8c, 0xe8, 0xbb, 0x18, 0x37, 0x07, 0xe8, 0xca, 0x00, 0xbc, 0x2d, 0xa3, 0x5e, 0x2a, 0x1f, 0xec, 0xf0, 0x16, 0x75, + 0xb3, 0xe3, 0x34, 0xd8, 0x66, 0xc4, 0xa1, 0x1c, 0x0a, 0x70, 0x97, 0xbd, 0xaa, 0x52, 0xe4, 0xf4, 0xd6, 0xf4, 0x8e, + 0xeb, 0x0d, 0xf2, 0x45, 0x13, 0x4d, 0x1d, 0x4c, 0x7a, 0x00, 0x13, 0x6a, 0x39, 0xa0, 0x31, 0x7a, 0xb5, 0x25, 0x5b, + 0x5c, 0x0b, 0x9e, 0xd9, 0x02, 0xd2, 0xbc, 0x22, 0xd5, 0x6e, 0x94, 0x46, 0x53, 0x32, 0x34, 0x69, 0x13, 0x8b, 0xd4, + 0x40, 0x32, 0xab, 0x57, 0x75, 0x22, 0x55, 0x83, 0x2d, 0x70, 0x60, 0xb3, 0x20, 0xc3, 0x37, 0xfb, 0x93, 0x01, 0x63, + 0x4b, 0xaf, 0x7d, 0x4f, 0x76, 0x1f, 0x35, 0xe4, 0x1a, 0x12, 0x19, 0xc7, 0x39, 0xd1, 0x6c, 0xaa, 0x88, 0xf8, 0x64, + 0x1d, 0x15, 0xb0, 0x36, 0x97, 0x6d, 0xe5, 0x83, 0x0a, 0x7a, 0x6c, 0xb0, 0xc9, 0x06, 0xd7, 0x8e, 0x19, 0xd6, 0x17, + 0x9b, 0x8a, 0xd4, 0x65, 0xfa, 0x40, 0xc4, 0x34, 0x83, 0x44, 0xa8, 0x3b, 0x36, 0xbe, 0xae, 0x4a, 0xbb, 0x20, 0xec, + 0xfb, 0xab, 0x74, 0xae, 0x61, 0xe3, 0x15, 0x90, 0xb9, 0xbe, 0xea, 0x00, 0x03, 0xbc, 0x02, 0x71, 0x31, 0xe0, 0xe3, + 0x2d, 0x90, 0x29, 0xf9, 0x77, 0xd4, 0x73, 0xa3, 0x94, 0x47, 0x2d, 0xef, 0x86, 0x19, 0xde, 0x6a, 0x2f, 0xf3, 0x0f, + 0x4b, 0x1f, 0xf2, 0x21, 0x41, 0x85, 0x2c, 0xa4, 0xe6, 0x49, 0xd4, 0xcd, 0x1d, 0x88, 0x6d, 0xdd, 0xe7, 0x22, 0xa3, + 0x58, 0xc4, 0xca, 0x23, 0xc0, 0x5d, 0x04, 0xbc, 0xdb, 0xac, 0x94, 0x88, 0x6f, 0x0e, 0xa5, 0x55, 0xde, 0x12, 0xcd, + 0x55, 0x60, 0xde, 0x45, 0x2b, 0x3b, 0x9c, 0xea, 0x90, 0x81, 0x9d, 0x4a, 0xed, 0x94, 0x44, 0xef, 0xb1, 0xc2, 0x5e, + 0xb3, 0x8d, 0xf5, 0x5b, 0x3b, 0xfa, 0x10, 0x56, 0x0b, 0x56, 0xdb, 0x73, 0x85, 0xe6, 0x66, 0xa2, 0x80, 0x58, 0x60, + 0xcd, 0xde, 0xbe, 0x49, 0x14, 0x42, 0xe1, 0x82, 0xcb, 0x52, 0x2a, 0x91, 0x62, 0xdb, 0x01, 0x83, 0x44, 0x13, 0x26, + 0xaa, 0x8a, 0x73, 0x23, 0xf6, 0x3c, 0x6b, 0xb0, 0xc0, 0xb3, 0x92, 0x0c, 0x8a, 0xd0, 0xba, 0xad, 0x76, 0xa9, 0x40, + 0xef, 0x67, 0x81, 0x95, 0x53, 0xe5, 0x04, 0x0e, 0xa9, 0x46, 0x1c, 0x9f, 0x05, 0x23, 0xb7, 0x4a, 0xf9, 0x16, 0x61, + 0xce, 0xe0, 0xcc, 0x7f, 0x5d, 0x7a, 0x6d, 0x7a, 0x52, 0x0a, 0x0e, 0xd3, 0xf7, 0xe7, 0x30, 0xe9, 0x15, 0x18, 0x9d, + 0xf7, 0x9e, 0xf7, 0x4a, 0x16, 0xf8, 0xeb, 0xb5, 0xbe, 0x14, 0x45, 0xb3, 0x29, 0x4f, 0x3d, 0xb9, 0x94, 0xf9, 0x96, + 0x06, 0xae, 0x53, 0x1c, 0xad, 0xee, 0xd9, 0x9b, 0x15, 0x30, 0x13, 0xcb, 0xf0, 0xef, 0xc1, 0xd6, 0xbe, 0x81, 0xf3, + 0x62, 0x09, 0x44, 0x7e, 0x63, 0x7c, 0x7d, 0xc8, 0xd3, 0xe2, 0x85, 0xcf, 0xcf, 0x08, 0x0b, 0x15, 0xe6, 0x8a, 0x84, + 0xc7, 0xa7, 0x4a, 0xab, 0x2c, 0x41, 0xc3, 0xb4, 0x7c, 0xa6, 0xc7, 0x8b, 0xfa, 0x56, 0x39, 0x7a, 0xeb, 0x2f, 0xb3, + 0xd2, 0x98, 0xcf, 0xcf, 0x93, 0x47, 0x4a, 0xbc, 0x7e, 0xf2, 0x89, 0xaa, 0xf2, 0x67, 0xc3, 0x6d, 0xbd, 0xef, 0xe1, + 0xd7, 0xde, 0x7e, 0x90, 0x65, 0x81, 0xc8, 0xaa, 0x71, 0x6d, 0xe3, 0xa8, 0xf2, 0x34, 0xed, 0x85, 0x58, 0x28, 0xc2, + 0x0f, 0x8a, 0x0e, 0x2c, 0x2f, 0x73, 0xa9, 0xe6, 0x5c, 0x85, 0x8a, 0x46, 0xec, 0x69, 0xfc, 0x7c, 0x08, 0x4b, 0x61, + 0x2a, 0x32, 0x08, 0xe3, 0x0e, 0xed, 0x92, 0x64, 0xec, 0x96, 0x32, 0x6d, 0xeb, 0x77, 0x0b, 0xe5, 0x35, 0x15, 0x13, + 0x30, 0x85, 0x77, 0x20, 0xb9, 0x99, 0x2d, 0xb6, 0xd6, 0x39, 0xa9, 0xa3, 0x02, 0xfb, 0x31, 0xa6, 0xc0, 0x61, 0xa7, + 0x9b, 0xe9, 0x73, 0x81, 0x1b, 0x9a, 0xf2, 0x50, 0x6f, 0x3a, 0xc3, 0x95, 0xd7, 0xf1, 0x43, 0x75, 0xe6, 0x6c, 0x81, + 0x96, 0x6b, 0xe4, 0x2b, 0xbe, 0xaa, 0x96, 0x20, 0xf2, 0x40, 0xf2, 0xb7, 0xaf, 0xbe, 0x7b, 0xab, 0x4b, 0x45, 0xd2, + 0x19, 0x6e, 0xd5, 0xb0, 0x3b, 0x58, 0xe8, 0x6e, 0x75, 0x26, 0x91, 0x04, 0x1a, 0x90, 0x5d, 0x1b, 0xde, 0x0b, 0x1b, + 0xe8, 0x4e, 0x3b, 0x5c, 0x3b, 0xa9, 0x22, 0x68, 0x9d, 0x5d, 0x65, 0x0c, 0x6d, 0xd9, 0x45, 0xc4, 0x2d, 0xbb, 0x0e, + 0x47, 0xd1, 0xcd, 0xb4, 0x10, 0xd6, 0xc6, 0xe3, 0x1e, 0x54, 0x6f, 0x33, 0x20, 0x25, 0x22, 0x12, 0x28, 0x17, 0xe2, + 0x6f, 0x5d, 0xa8, 0x59, 0xc6, 0xdd, 0xa6, 0x43, 0xec, 0x26, 0x89, 0xeb, 0x83, 0x66, 0xf0, 0xd6, 0xa5, 0x95, 0xd7, + 0x19, 0x52, 0xf8, 0x48, 0x45, 0x06, 0xce, 0x0d, 0x53, 0x7b, 0xda, 0x65, 0x1d, 0xc6, 0xbc, 0x54, 0xca, 0xaa, 0x08, + 0xb8, 0xd5, 0x00, 0xcf, 0xda, 0xb7, 0x70, 0x4c, 0x13, 0x1b, 0x9a, 0xa7, 0xbe, 0xcf, 0xd1, 0x76, 0x37, 0x5e, 0xb4, + 0xe2, 0xab, 0xd7, 0xd6, 0x71, 0xd9, 0x3c, 0xeb, 0x6e, 0x13, 0x56, 0xb1, 0x9f, 0x22, 0x29, 0x6c, 0x1a, 0x8b, 0xb9, + 0x26, 0x71, 0x4c, 0x02, 0xa3, 0x05, 0xb0, 0x37, 0xd1, 0x2c, 0xbb, 0x58, 0x22, 0xb5, 0x75, 0x58, 0x77, 0x73, 0xc0, + 0xe1, 0xdb, 0xce, 0x57, 0xaa, 0x76, 0x53, 0x83, 0x12, 0x39, 0xe7, 0xc3, 0xfe, 0xc2, 0xed, 0xfe, 0x50, 0xe2, 0x4d, + 0xdd, 0xc6, 0xe2, 0x48, 0x34, 0x16, 0x10, 0x5c, 0x66, 0x8c, 0xda, 0x2c, 0x8b, 0x10, 0x9d, 0x5a, 0x59, 0xff, 0x40, + 0x05, 0x48, 0x25, 0xd4, 0x6a, 0x71, 0x33, 0x81, 0x05, 0xc7, 0xa4, 0xd4, 0xc6, 0xe1, 0xc7, 0x3f, 0x89, 0xa7, 0x54, + 0xb4, 0x69, 0xd4, 0x13, 0xcd, 0x05, 0xfb, 0x72, 0x88, 0x46, 0x20, 0x77, 0x1b, 0xd6, 0x38, 0xf5, 0xa2, 0xb3, 0xb9, + 0x51, 0xe8, 0xb0, 0x32, 0x55, 0x60, 0xfc, 0xad, 0xc2, 0x6c, 0x20, 0xe7, 0x2a, 0x89, 0x95, 0xdb, 0x19, 0x78, 0x61, + 0x84, 0x0e, 0x62, 0x00, 0xbd, 0x9d, 0xfc, 0x54, 0x7f, 0x5a, 0x5d, 0x94, 0x71, 0x22, 0x4c, 0x4e, 0xdf, 0xdb, 0xc1, + 0x83, 0xda, 0x6c, 0xe7, 0x52, 0xbc, 0xe6, 0x39, 0x81, 0xf6, 0x95, 0x9f, 0xfd, 0x5e, 0xbf, 0x3f, 0x71, 0x13, 0x5a, + 0x56, 0x20, 0x75, 0x8a, 0x7f, 0xd5, 0x9d, 0x51, 0xee, 0x76, 0xce, 0xb3, 0x09, 0xcb, 0x63, 0x92, 0xec, 0x1b, 0x7f, + 0x5b, 0xb8, 0xe4, 0x68, 0xc9, 0x9f, 0x38, 0x0f, 0x8c, 0x62, 0x31, 0x4d, 0x16, 0xa6, 0x7e, 0x4d, 0xd2, 0xde, 0xc4, + 0x75, 0x6c, 0xc4, 0xf1, 0x9f, 0xe3, 0x10, 0x3d, 0x49, 0x84, 0xd4, 0x7a, 0x4b, 0x51, 0x3d, 0xaa, 0x7b, 0x27, 0xea, + 0x42, 0x36, 0x0f, 0x39, 0x5a, 0x93, 0x31, 0x9d, 0xd4, 0x5d, 0xaa, 0x91, 0x68, 0x04, 0x4b, 0xb7, 0x76, 0x3b, 0x39, + 0x3c, 0xc4, 0x4b, 0xfb, 0x28, 0x12, 0x15, 0xbd, 0x0b, 0x4d, 0x71, 0x68, 0xa3, 0x58, 0x5f, 0x59, 0x89, 0x05, 0xd6, + 0x5e, 0x2a, 0xad, 0xe6, 0x82, 0xae, 0xbc, 0x1c, 0x15, 0x3a, 0x27, 0x00, 0x5b, 0xcb, 0x39, 0x91, 0x01, 0x74, 0x62, + 0xb1, 0x70, 0x1d, 0x72, 0x03, 0xca, 0x10, 0xa1, 0x72, 0x4b, 0x8f, 0x53, 0x69, 0xd5, 0x28, 0x96, 0x80, 0xc4, 0x70, + 0xc6, 0x7c, 0xeb, 0x63, 0x36, 0x6e, 0x64, 0x0c, 0xae, 0x5a, 0x76, 0x6d, 0x19, 0x6b, 0x6b, 0x85, 0xb4, 0x0e, 0x98, + 0x5a, 0x65, 0x3f, 0x35, 0xbe, 0xf3, 0xe7, 0xbd, 0x23, 0x4d, 0x6f, 0x71, 0x24, 0x11, 0x06, 0x6d, 0xaf, 0x18, 0x0b, + 0x53, 0x84, 0xdb, 0xec, 0xf6, 0x8a, 0xd0, 0xdd, 0x5f, 0x0a, 0x7c, 0x5b, 0xb8, 0x31, 0x15, 0x37, 0x8e, 0x1e, 0x5f, + 0x14, 0x4c, 0x04, 0xe3, 0xd0, 0x54, 0x95, 0xf0, 0x6e, 0xba, 0x0a, 0x0a, 0x72, 0x2a, 0x2a, 0x1c, 0x78, 0xb0, 0x5e, + 0x66, 0xf4, 0x28, 0x12, 0x8a, 0x2d, 0xae, 0x6a, 0x4d, 0x14, 0x77, 0x19, 0x17, 0xa4, 0x2f, 0x87, 0xf9, 0xb7, 0xaa, + 0x1b, 0xba, 0x66, 0x55, 0xba, 0x45, 0xe2, 0x6b, 0x53, 0x8d, 0x46, 0x44, 0xe5, 0x7b, 0xe9, 0x03, 0xf3, 0x58, 0x4b, + 0x77, 0x3e, 0xed, 0x13, 0xae, 0x0c, 0x1c, 0x18, 0xfa, 0x48, 0xf7, 0x57, 0xeb, 0xea, 0x24, 0x1f, 0x57, 0x9f, 0x7c, + 0x0d, 0xcf, 0x1f, 0x8c, 0x9d, 0x76, 0x70, 0xcb, 0x21, 0x7d, 0xcc, 0xf9, 0x35, 0x33, 0xbd, 0x45, 0xab, 0xbd, 0x51, + 0xa3, 0x2e, 0xb0, 0x99, 0x4c, 0xd3, 0x63, 0xfe, 0xe9, 0xad, 0xe8, 0xc1, 0x05, 0x27, 0x89, 0x2f, 0x20, 0xe1, 0x86, + 0xed, 0xd5, 0xc7, 0x47, 0xaa, 0xeb, 0xd6, 0x09, 0x25, 0x76, 0x23, 0x95, 0xed, 0xa0, 0x42, 0xc8, 0x0e, 0xf7, 0xc4, + 0xd5, 0x1b, 0xec, 0x73, 0x88, 0xd3, 0xd1, 0x00, 0x29, 0x93, 0x26, 0xf6, 0x25, 0x8c, 0xf7, 0xc5, 0x1c, 0x36, 0x2b, + 0x48, 0xbc, 0xea, 0xc2, 0x29, 0x94, 0x58, 0xd9, 0xf3, 0xea, 0x78, 0x1d, 0xdd, 0xe4, 0x23, 0x9a, 0x32, 0xd0, 0xdc, + 0xbd, 0xe5, 0x2e, 0x17, 0x18, 0xdd, 0x6b, 0xf3, 0xf1, 0xdd, 0xce, 0x68, 0x4d, 0x12, 0xf9, 0xc6, 0xb7, 0xf9, 0x70, + 0xf3, 0xf8, 0x85, 0x86, 0xe2, 0x00, 0xd7, 0x71, 0xb8, 0xfd, 0xe1, 0xb2, 0x2a, 0xf7, 0xaa, 0x1f, 0xd4, 0xc0, 0x91, + 0x52, 0x4f, 0x0d, 0x67, 0x61, 0x4f, 0x30, 0xe1, 0xd4, 0x81, 0xb3, 0xe6, 0x83, 0x90, 0x73, 0xf9, 0xd7, 0x2e, 0x94, + 0x73, 0x37, 0x6e, 0x16, 0x9e, 0x06, 0x36, 0x76, 0x86, 0x3a, 0x5c, 0xea, 0xce, 0x6c, 0x49, 0x3c, 0xc3, 0x8f, 0xbb, + 0x9a, 0x08, 0x4b, 0xed, 0x81, 0xaf, 0x57, 0xbc, 0x9c, 0xfb, 0xdb, 0xe1, 0x8d, 0xe2, 0x82, 0x30, 0x33, 0xbe, 0x88, + 0x4a, 0x93, 0x2c, 0x69, 0xf8, 0x36, 0xb2, 0x19, 0x75, 0xed, 0xbb, 0x68, 0x45, 0x50, 0x32, 0x22, 0x54, 0x71, 0x68, + 0xc6, 0x50, 0x06, 0xe8, 0x58, 0x45, 0x69, 0xcf, 0xd7, 0x06, 0xb3, 0x4e, 0x36, 0x73, 0x04, 0x74, 0x44, 0xdf, 0x2f, + 0x5f, 0xd4, 0xb3, 0x7f, 0xdd, 0x1f, 0x1e, 0xbe, 0xc8, 0x55, 0x7d, 0xd4, 0x00, 0xfc, 0x8e, 0x54, 0xf5, 0xe8, 0x8d, + 0xe5, 0x57, 0x5a, 0x82, 0xad, 0x66, 0x07, 0x46, 0x9d, 0xa4, 0x8d, 0xd4, 0x86, 0xcc, 0x32, 0x67, 0x52, 0x28, 0x04, + 0x2d, 0x3d, 0x9e, 0x63, 0x31, 0x05, 0x50, 0xd2, 0xe5, 0x8a, 0x88, 0x0b, 0x06, 0x61, 0x15, 0x87, 0x31, 0x2c, 0xa4, + 0x69, 0x3d, 0xdb, 0xce, 0xa2, 0x51, 0x83, 0xd0, 0x35, 0x86, 0x44, 0x85, 0x99, 0xf5, 0x8c, 0x83, 0x5c, 0x6a, 0xbb, + 0x20, 0x4f, 0x7e, 0x73, 0x15, 0x03, 0xd0, 0x63, 0x22, 0x79, 0x5a, 0xd5, 0x44, 0x96, 0x90, 0xcf, 0xa4, 0x61, 0xd3, + 0xfb, 0xc3, 0x37, 0x31, 0x3d, 0xfa, 0xd8, 0xd5, 0xd6, 0x1f, 0xa2, 0xe4, 0xb9, 0x17, 0x29, 0x5f, 0xeb, 0xb4, 0x65, + 0x77, 0xa2, 0x4d, 0xd0, 0x44, 0xdb, 0x82, 0xb0, 0x05, 0x3a, 0xd0, 0x67, 0x3c, 0x1a, 0x2e, 0x1b, 0x51, 0x16, 0x8b, + 0x94, 0x28, 0x87, 0xfc, 0xe6, 0xec, 0x11, 0xe2, 0xb4, 0x16, 0x46, 0x03, 0xcb, 0xbd, 0x60, 0x18, 0x45, 0x17, 0xec, + 0x81, 0x4f, 0x2b, 0x52, 0x5c, 0x7d, 0xbb, 0x58, 0xf3, 0xba, 0x32, 0xd1, 0x36, 0x98, 0xf0, 0x0e, 0x1a, 0x1e, 0x61, + 0xaf, 0x71, 0x4e, 0x83, 0xae, 0xeb, 0xc9, 0xd3, 0x8a, 0x8c, 0x4d, 0x65, 0xa5, 0x1e, 0x01, 0xb7, 0xd8, 0xdb, 0x7e, + 0xd4, 0x36, 0x07, 0x7b, 0x36, 0xb6, 0xea, 0xc6, 0xb6, 0xc7, 0x10, 0xdc, 0x3a, 0x41, 0x3e, 0xdd, 0x29, 0x7d, 0x9e, + 0xe7, 0x4b, 0x6b, 0x13, 0xe8, 0xf0, 0xba, 0x35, 0xed, 0x71, 0x84, 0x11, 0x11, 0xb7, 0x99, 0x2e, 0x58, 0x58, 0x4a, + 0x6f, 0xa9, 0xae, 0x88, 0xe1, 0xd9, 0x0e, 0x59, 0x0d, 0x40, 0x2f, 0xb0, 0x3f, 0x94, 0xe7, 0x25, 0x5c, 0xe8, 0xf9, + 0xf0, 0xd1, 0x45, 0x95, 0x97, 0xa5, 0x1d, 0x66, 0x7b, 0xd6, 0xdd, 0xa0, 0xc2, 0xf5, 0xa9, 0x5a, 0x9d, 0x50, 0x20, + 0x96, 0x9e, 0xa3, 0xbf, 0xef, 0x53, 0x47, 0x3c, 0xcf, 0x08, 0x61, 0x27, 0x36, 0x9b, 0x07, 0x20, 0xf6, 0x41, 0xc7, + 0x04, 0x01, 0x42, 0xd0, 0x10, 0xab, 0x3d, 0xa0, 0x1e, 0xbf, 0x33, 0xf4, 0x7d, 0x44, 0x7a, 0x13, 0xa0, 0x32, 0x05, + 0xc5, 0x89, 0xda, 0xa7, 0x24, 0x22, 0x27, 0x3f, 0xc9, 0x2e, 0x9b, 0xb1, 0xa8, 0x93, 0xc0, 0xf9, 0x88, 0x53, 0xb0, + 0x14, 0x0a, 0xe7, 0xc5, 0x33, 0x01, 0x7c, 0x3a, 0x67, 0x8b, 0x69, 0xe1, 0x8b, 0x1c, 0x94, 0xcd, 0xa4, 0xc7, 0xf5, + 0x38, 0xb7, 0x7d, 0x4c, 0x38, 0x2a, 0xca, 0xd8, 0xd8, 0x5b, 0x75, 0x66, 0x8c, 0xf0, 0xd5, 0x44, 0xa8, 0xf7, 0x63, + 0x9c, 0xb7, 0xf7, 0x3d, 0x3e, 0xe2, 0x52, 0x6c, 0x2a, 0x84, 0xc2, 0x82, 0x9a, 0xa7, 0xf4, 0x47, 0x59, 0xe7, 0xd4, + 0xc8, 0x82, 0xf2, 0xb8, 0x82, 0x91, 0xa2, 0x4c, 0xfb, 0xec, 0xc9, 0x9e, 0x32, 0x48, 0x6c, 0xe7, 0x65, 0xa2, 0x2b, + 0x05, 0x0c, 0xa2, 0x54, 0x0a, 0x76, 0xb5, 0x2d, 0x14, 0xc9, 0x20, 0x1c, 0x43, 0xbb, 0x11, 0x47, 0x55, 0xe6, 0x90, + 0x84, 0x7c, 0xcd, 0xd7, 0x38, 0xb3, 0xdd, 0x1c, 0x52, 0x18, 0x6c, 0x51, 0x4d, 0x46, 0x81, 0xd0, 0x6e, 0x41, 0x40, + 0xe8, 0xd2, 0x85, 0xdf, 0xe0, 0x97, 0x47, 0xa9, 0x6c, 0x26, 0x38, 0x4f, 0x17, 0x6e, 0xe1, 0x97, 0x1e, 0xb5, 0x62, + 0xc7, 0x5b, 0x6b, 0xe3, 0x12, 0xe5, 0xa2, 0x65, 0xfe, 0x23, 0xf6, 0xb8, 0x80, 0x03, 0x5b, 0x60, 0x6d, 0xe8, 0x0e, + 0x95, 0x61, 0x34, 0x70, 0xe2, 0x01, 0x24, 0xb5, 0xbb, 0x61, 0x49, 0x5b, 0xd4, 0x7f, 0x32, 0xd7, 0xea, 0x1a, 0x34, + 0x81, 0x59, 0xab, 0xc5, 0x36, 0x4d, 0x85, 0x1c, 0x32, 0xaa, 0x1a, 0xb0, 0x52, 0x6d, 0x87, 0x34, 0x59, 0x22, 0x87, + 0x24, 0x71, 0x27, 0x73, 0x06, 0x55, 0x56, 0x7a, 0xd1, 0xff, 0xa8, 0x44, 0xe4, 0x43, 0x5e, 0xff, 0xa4, 0x8a, 0x67, + 0x99, 0xd4, 0x8f, 0xc2, 0xa1, 0x4a, 0x63, 0x93, 0x0d, 0x6d, 0x00, 0xa3, 0x0f, 0x73, 0xa8, 0x2c, 0x74, 0x6c, 0x95, + 0xfb, 0x6e, 0x5a, 0xc9, 0xb1, 0x21, 0x9f, 0xcc, 0x18, 0x30, 0xdf, 0x7e, 0x0b, 0x62, 0x8f, 0x5b, 0xcc, 0x38, 0xdc, + 0x4b, 0x7e, 0xbe, 0x4c, 0x44, 0xc1, 0x1f, 0x4e, 0xc3, 0x0e, 0x7c, 0xd3, 0x21, 0xa6, 0xcd, 0x15, 0x33, 0x64, 0x06, + 0xa5, 0x6d, 0x09, 0x31, 0x2d, 0x78, 0x4a, 0xa4, 0xfb, 0xf7, 0xfe, 0xc4, 0xde, 0xd3, 0x7c, 0x21, 0x3f, 0x59, 0x9d, + 0x83, 0xbe, 0x55, 0x97, 0x3e, 0x86, 0x17, 0xc6, 0x7d, 0x00, 0x50, 0xb9, 0xbd, 0x6d, 0xc5, 0x71, 0x7b, 0x5f, 0x85, + 0xf8, 0x83, 0x19, 0x66, 0x1c, 0xaf, 0x52, 0x64, 0x63, 0x81, 0xe9, 0x94, 0x59, 0xa9, 0x5b, 0x35, 0x2b, 0xfb, 0xc7, + 0xf4, 0x5f, 0x1a, 0x0d, 0xb0, 0x2f, 0x17, 0xf9, 0xf9, 0x76, 0x99, 0x28, 0xb0, 0xc2, 0x22, 0xd1, 0x7b, 0x17, 0x40, + 0xba, 0x83, 0x48, 0xc6, 0x9f, 0xf7, 0x70, 0xd1, 0xf0, 0xf0, 0x17, 0x39, 0x68, 0xd9, 0x79, 0xe5, 0x44, 0x69, 0x3e, + 0xaf, 0xd8, 0x09, 0x44, 0x9e, 0x3a, 0x45, 0x98, 0xfe, 0x7d, 0x72, 0xe5, 0xb5, 0x47, 0x4e, 0xce, 0x5e, 0x40, 0xbf, + 0x26, 0x6e, 0x9f, 0x9f, 0x65, 0x1d, 0xfe, 0xb1, 0x44, 0x85, 0xb0, 0x52, 0xe8, 0x41, 0x55, 0x08, 0x5f, 0x70, 0xe2, + 0x00, 0x4e, 0x3d, 0x7c, 0x78, 0xc6, 0xdf, 0x0e, 0x43, 0xfb, 0xf8, 0x99, 0xb3, 0x69, 0x89, 0xf1, 0x12, 0x83, 0x45, + 0xb5, 0xd4, 0x78, 0x7e, 0xff, 0x34, 0xeb, 0xe9, 0x9e, 0xb1, 0x4f, 0x8b, 0x9e, 0xac, 0x6a, 0x9a, 0x37, 0x24, 0xce, + 0x7f, 0xd8, 0xfc, 0x5a, 0x1b, 0x1f, 0xec, 0xdc, 0x56, 0x25, 0x47, 0xd6, 0x05, 0xae, 0xcb, 0xaa, 0x55, 0xd5, 0x37, + 0x03, 0xce, 0x49, 0x8f, 0xc5, 0x4b, 0x9d, 0xdd, 0x2f, 0xe8, 0x8f, 0x66, 0x3a, 0x5a, 0x1f, 0x7d, 0x70, 0x2d, 0x42, + 0xd5, 0xa4, 0x33, 0xba, 0x37, 0xbf, 0xc3, 0x39, 0xe5, 0x33, 0xd7, 0xf1, 0xb9, 0x5b, 0x0b, 0xe5, 0x09, 0x8f, 0x16, + 0x1a, 0x85, 0xa1, 0x3b, 0x77, 0x8f, 0xe0, 0x5a, 0x24, 0xcd, 0xc8, 0xde, 0xc2, 0x39, 0xd3, 0xf8, 0x4c, 0x7f, 0x36, + 0x0b, 0xf5, 0xa7, 0x3e, 0x14, 0x14, 0x11, 0xf3, 0x2b, 0xa6, 0x62, 0x28, 0x25, 0xc1, 0x43, 0x44, 0x04, 0x5a, 0x47, + 0x51, 0x3e, 0x55, 0x57, 0x57, 0xca, 0xea, 0x97, 0xb3, 0x2c, 0x28, 0x92, 0xd9, 0x14, 0x59, 0xb9, 0xe2, 0x8f, 0x4e, + 0x72, 0x96, 0xeb, 0x42, 0x40, 0xce, 0x3e, 0xc0, 0x89, 0xfd, 0x9b, 0x41, 0xe0, 0xb6, 0xb6, 0xd6, 0xfe, 0x48, 0x50, + 0x63, 0x14, 0x7c, 0x8b, 0x00, 0x8c, 0xc4, 0xd0, 0x46, 0xf9, 0xe4, 0x56, 0xba, 0x50, 0x51, 0xbd, 0x3f, 0x71, 0xf7, + 0x3f, 0xbf, 0xbb, 0xc9, 0x0d, 0x1b, 0x77, 0x69, 0x0e, 0x4d, 0xe6, 0xc9, 0x39, 0xda, 0xc8, 0xee, 0xba, 0x5f, 0x06, + 0xf9, 0x2d, 0x5f, 0x92, 0xf4, 0x74, 0x44, 0x60, 0x4b, 0xcb, 0x8f, 0x48, 0x45, 0x49, 0x22, 0x90, 0x63, 0xad, 0x00, + 0x50, 0x33, 0x21, 0x95, 0x8a, 0x1c, 0x45, 0x9e, 0x8c, 0x7a, 0x33, 0xa7, 0x24, 0x2d, 0x69, 0x37, 0xa8, 0x31, 0x2c, + 0x87, 0xaf, 0xb9, 0x36, 0x4b, 0x7d, 0xad, 0xa4, 0xec, 0xc4, 0xf6, 0x82, 0x05, 0x94, 0x38, 0xa6, 0xe0, 0x82, 0xd5, + 0x58, 0x9a, 0x36, 0xaf, 0x27, 0x18, 0xd0, 0x32, 0x97, 0x76, 0xd9, 0x12, 0xf2, 0x95, 0xfa, 0x7d, 0x58, 0x8c, 0x90, + 0x7c, 0x63, 0xa1, 0x58, 0xda, 0xaa, 0x55, 0xb9, 0xf3, 0x1c, 0x3f, 0xd0, 0xa4, 0x48, 0x1d, 0xed, 0x61, 0xfa, 0x16, + 0x8e, 0xc4, 0xe0, 0x66, 0x4e, 0xb9, 0xa4, 0x4c, 0xe3, 0xd2, 0x9f, 0xa4, 0xff, 0xaa, 0x2f, 0x43, 0x3e, 0xc1, 0x51, + 0xac, 0xfe, 0x83, 0x6a, 0xcc, 0x40, 0x40, 0xea, 0x4b, 0x10, 0x15, 0xc3, 0x68, 0xe6, 0x10, 0xdd, 0xa0, 0xf5, 0x99, + 0x3a, 0x91, 0xce, 0x5e, 0x6c, 0x70, 0xd2, 0x97, 0x73, 0xa2, 0x79, 0xe1, 0x3b, 0x8c, 0xf7, 0x81, 0x01, 0x0c, 0x0a, + 0xf3, 0x60, 0x0c, 0xec, 0xb2, 0x26, 0x6d, 0x29, 0xb8, 0x41, 0x0d, 0x34, 0x81, 0x07, 0x78, 0x3a, 0x89, 0x90, 0x8b, + 0x7c, 0x66, 0x71, 0x27, 0xbb, 0x98, 0x52, 0x6b, 0x7e, 0x2c, 0x84, 0x85, 0xfe, 0xdd, 0x62, 0x2b, 0xcb, 0x1d, 0x3c, + 0x13, 0x11, 0x54, 0x05, 0x0a, 0xbc, 0x72, 0x79, 0x43, 0xa7, 0x25, 0x70, 0xf0, 0x3e, 0xb5, 0xe2, 0x06, 0x07, 0xbe, + 0x0d, 0x2a, 0x5d, 0xb0, 0x1f, 0xb4, 0xeb, 0xdf, 0x7b, 0xae, 0xc2, 0x22, 0xea, 0x21, 0xde, 0x6a, 0xae, 0x57, 0x77, + 0xf7, 0xbe, 0xd7, 0xf1, 0x59, 0x53, 0xcb, 0x1e, 0x7f, 0xc6, 0x10, 0x0a, 0x4e, 0xd0, 0x2a, 0x15, 0x12, 0x30, 0xf0, + 0xc7, 0x2d, 0x6c, 0xfc, 0x92, 0xa5, 0xdb, 0x11, 0x4b, 0x7f, 0xfd, 0xba, 0xa2, 0xc9, 0xae, 0xba, 0xa9, 0x27, 0xa0, + 0x88, 0xbd, 0xa3, 0x55, 0x76, 0xb8, 0x4a, 0xcd, 0x7b, 0xc5, 0xbb, 0x1e, 0xf8, 0x94, 0x0e, 0xcc, 0x28, 0xb0, 0x17, + 0xc4, 0x1c, 0x18, 0xeb, 0xc7, 0x46, 0x79, 0xd7, 0x4f, 0xbe, 0x4b, 0xd1, 0x46, 0xad, 0xaf, 0xfc, 0x41, 0x10, 0xdf, + 0x67, 0x46, 0xac, 0xbd, 0x04, 0x66, 0x30, 0xba, 0xd3, 0x36, 0x1d, 0x76, 0xe5, 0x3e, 0x9e, 0x1f, 0xb2, 0xde, 0x41, + 0x40, 0xa5, 0xe8, 0x47, 0x81, 0x4b, 0x26, 0x30, 0x98, 0x83, 0x23, 0xdb, 0x8b, 0x3d, 0xf9, 0x44, 0xcc, 0x85, 0x28, + 0x45, 0x33, 0x46, 0x01, 0xc1, 0xc8, 0x61, 0x85, 0xed, 0x3f, 0xc2, 0x76, 0x01, 0x70, 0x8b, 0x87, 0x0c, 0x7b, 0x5e, + 0xe3, 0x4d, 0xbc, 0x1d, 0x35, 0xcc, 0x99, 0xd4, 0x5b, 0xd0, 0x4e, 0x8f, 0x21, 0xf9, 0x7d, 0x1a, 0x24, 0xa3, 0x22, + 0xf7, 0x28, 0x12, 0x84, 0xd7, 0x45, 0x4e, 0x5a, 0x80, 0x75, 0x77, 0xe8, 0xa6, 0x5f, 0x01, 0x62, 0xfa, 0x5e, 0x02, + 0xfe, 0x44, 0x6e, 0x22, 0x16, 0xbc, 0xdd, 0x34, 0xc4, 0x1d, 0x4c, 0x80, 0xa1, 0x11, 0x9e, 0x41, 0xd0, 0x08, 0x92, + 0x11, 0xdd, 0x6d, 0xee, 0xa7, 0xcc, 0x7f, 0x56, 0xe4, 0xc7, 0xb2, 0x31, 0xae, 0x79, 0xd3, 0xde, 0xc5, 0x6f, 0x11, + 0xa9, 0x00, 0x62, 0x67, 0xca, 0x2c, 0x54, 0x89, 0xc9, 0xd7, 0x85, 0x8d, 0x7d, 0x6e, 0x94, 0x25, 0xdb, 0xe7, 0xf5, + 0xd7, 0x66, 0xd8, 0x92, 0x66, 0xb6, 0xb7, 0x39, 0xe3, 0xb3, 0x8a, 0x89, 0x85, 0x17, 0x05, 0xce, 0xfd, 0xed, 0xd7, + 0xfd, 0xf9, 0x70, 0x95, 0x2d, 0xdb, 0x29, 0x53, 0x8f, 0x23, 0x25, 0xcb, 0x5a, 0x7f, 0xbb, 0x32, 0x93, 0xb7, 0x6e, + 0xd1, 0x13, 0xec, 0xa8, 0x35, 0xf3, 0x25, 0x47, 0xda, 0xba, 0x87, 0x93, 0xec, 0xba, 0xc0, 0x2e, 0xef, 0x04, 0xd0, + 0xb4, 0x74, 0x42, 0xf1, 0x73, 0x25, 0xb4, 0xac, 0x1d, 0xe0, 0x24, 0x7e, 0xfa, 0x62, 0xe2, 0xa5, 0x98, 0xad, 0xc1, + 0x36, 0xbf, 0x62, 0x5e, 0xc4, 0x60, 0xcf, 0x8d, 0x0a, 0xe1, 0x8c, 0xf3, 0xbe, 0x05, 0xb3, 0xf4, 0x1b, 0xaf, 0xdc, + 0xe6, 0x73, 0x82, 0xfd, 0x96, 0x16, 0xc1, 0xc0, 0xc4, 0x5d, 0xf5, 0x5a, 0xe3, 0x2c, 0x84, 0xa8, 0x6b, 0xb9, 0x2f, + 0x62, 0xe6, 0x36, 0xa7, 0xe9, 0x5d, 0xad, 0xc9, 0x8c, 0xfd, 0xe2, 0x4a, 0x33, 0xeb, 0xbb, 0xef, 0x20, 0x6b, 0xad, + 0x2a, 0xf4, 0x2b, 0x52, 0xcf, 0x64, 0xfd, 0x27, 0xb0, 0x19, 0x8b, 0x1d, 0x16, 0x4b, 0x2b, 0x75, 0xe7, 0xaa, 0xf4, + 0x03, 0x9e, 0x54, 0x00, 0x72, 0x11, 0xd0, 0x99, 0x6e, 0x3d, 0x77, 0x8b, 0x45, 0x3d, 0xea, 0xc1, 0xad, 0xbf, 0xb7, + 0x1a, 0x06, 0x41, 0xac, 0x63, 0xbf, 0x22, 0x78, 0x3c, 0x5e, 0x89, 0xdf, 0x0b, 0xaf, 0xc8, 0x0f, 0x5b, 0x1e, 0xff, + 0x7c, 0x01, 0x65, 0xfa, 0x49, 0x34, 0xed, 0xfc, 0x6c, 0xc3, 0xc2, 0xa4, 0x7c, 0x3a, 0x8f, 0xfc, 0x1e, 0xcd, 0xcd, + 0x15, 0xb4, 0xdc, 0xf3, 0x03, 0x97, 0xf2, 0x7f, 0x16, 0x29, 0x4b, 0x6a, 0x85, 0x66, 0xd9, 0x36, 0xc1, 0xd1, 0xdd, + 0x9e, 0xe2, 0xc1, 0x73, 0x9c, 0x50, 0x68, 0x6f, 0x4a, 0xbd, 0x55, 0x85, 0x9a, 0xa8, 0xb5, 0x85, 0x02, 0x65, 0xfd, + 0x88, 0xf6, 0x51, 0x71, 0xc4, 0x0d, 0x23, 0x3d, 0xea, 0x6f, 0x6a, 0x6d, 0x91, 0x5d, 0x47, 0xed, 0x97, 0xb5, 0xfb, + 0x7d, 0x12, 0x24, 0xff, 0x6d, 0x05, 0xc8, 0xac, 0x0d, 0xd5, 0x9b, 0x80, 0x69, 0x44, 0x31, 0x47, 0xc1, 0x8f, 0xd1, + 0x92, 0x42, 0xa3, 0x0c, 0x2e, 0x1c, 0x11, 0x66, 0x2d, 0xb5, 0xe4, 0x19, 0x43, 0xf0, 0xbc, 0xd1, 0xd0, 0x91, 0xf0, + 0xb5, 0xe9, 0x5d, 0x76, 0x66, 0x36, 0x4c, 0xce, 0x3d, 0xa2, 0x21, 0x9b, 0x7a, 0xaa, 0x28, 0x01, 0xf7, 0xcd, 0x72, + 0x7c, 0x75, 0x50, 0xb2, 0x26, 0xb5, 0x57, 0xc1, 0x6e, 0x1f, 0x72, 0x73, 0x19, 0xbd, 0x35, 0x54, 0x6b, 0xf8, 0xde, + 0x48, 0xd6, 0xb0, 0xca, 0x35, 0x90, 0xd8, 0xce, 0x8f, 0x30, 0x49, 0x45, 0x77, 0xf9, 0x16, 0x34, 0xde, 0x51, 0x95, + 0xcb, 0x4e, 0xeb, 0xda, 0xbb, 0x03, 0x37, 0x61, 0xd8, 0xfa, 0xd4, 0x8d, 0x8e, 0xf4, 0xfd, 0x80, 0x0d, 0x9a, 0x95, + 0x4a, 0x03, 0x4e, 0xf9, 0x05, 0x15, 0xad, 0xf3, 0xbb, 0x25, 0x5f, 0xec, 0x19, 0xee, 0x83, 0x11, 0x32, 0x26, 0x8e, + 0xc0, 0x8e, 0x1a, 0xe0, 0x29, 0x61, 0xc6, 0xe1, 0xc7, 0x9e, 0xdb, 0xd7, 0xc6, 0xa0, 0x7f, 0xa5, 0xd9, 0x50, 0x40, + 0x8d, 0xf6, 0xb8, 0x92, 0x54, 0x3a, 0x86, 0x19, 0x93, 0xc2, 0x87, 0x54, 0x28, 0x73, 0xfc, 0xbb, 0x73, 0x4d, 0xb1, + 0x66, 0x38, 0x57, 0x23, 0xd3, 0x86, 0xf3, 0xbf, 0x1a, 0xf3, 0x5b, 0x8e, 0xef, 0x20, 0xaa, 0x9e, 0x8e, 0x41, 0x87, + 0x50, 0x4a, 0x50, 0x76, 0x65, 0x42, 0x55, 0x03, 0xfd, 0xa2, 0x19, 0x6d, 0x9a, 0xd6, 0x8f, 0x91, 0xf3, 0xbf, 0x6e, + 0xbf, 0xb6, 0x93, 0x8b, 0xd6, 0x0a, 0xeb, 0xe2, 0x07, 0x3f, 0x30, 0xe4, 0xb5, 0x7b, 0x7e, 0x76, 0xab, 0x5c, 0xd9, + 0x53, 0x5b, 0x3c, 0x75, 0xc1, 0x97, 0xe9, 0xfa, 0x18, 0xbc, 0x2c, 0x20, 0x35, 0x8d, 0xaa, 0x75, 0xec, 0x13, 0x16, + 0x5a, 0xec, 0x3a, 0x6f, 0xd7, 0x2f, 0x4f, 0xaa, 0x89, 0x57, 0x2c, 0x03, 0x3a, 0x3f, 0xb3, 0x29, 0xf6, 0x91, 0x16, + 0x97, 0x0d, 0xff, 0x32, 0xa0, 0xe7, 0xd9, 0x40, 0x9e, 0xf9, 0x59, 0x7f, 0xae, 0x3f, 0xbe, 0xe5, 0x21, 0xa1, 0x14, + 0xb7, 0x35, 0x4e, 0xef, 0x1a, 0xdb, 0xcc, 0x3b, 0xb3, 0xb4, 0x8f, 0x9d, 0x66, 0x3e, 0xa2, 0x22, 0x5d, 0x70, 0x12, + 0xb6, 0xa7, 0x43, 0xba, 0x92, 0x6d, 0x16, 0x9a, 0x39, 0xf5, 0xa5, 0x71, 0x59, 0x9c, 0xd7, 0x69, 0x73, 0x31, 0xf7, + 0x82, 0xae, 0x03, 0x38, 0xd7, 0x29, 0x47, 0x70, 0x55, 0x11, 0x28, 0x9a, 0x9a, 0xb9, 0xa2, 0x78, 0x68, 0x2d, 0x76, + 0x73, 0x6b, 0xf7, 0x53, 0x8c, 0xb8, 0xd4, 0xa5, 0x2a, 0x51, 0x92, 0x6c, 0x59, 0x29, 0x90, 0xc9, 0x82, 0xac, 0x39, + 0x49, 0x15, 0x0e, 0xfa, 0x37, 0x87, 0x74, 0xf6, 0x62, 0xd8, 0x87, 0x70, 0xe6, 0x6b, 0xa9, 0x0a, 0xa3, 0x59, 0xac, + 0xe6, 0x39, 0x88, 0x54, 0x6d, 0x1f, 0x28, 0xa7, 0xca, 0x7d, 0x5b, 0x40, 0xe6, 0x46, 0xca, 0x4b, 0x51, 0x47, 0x6e, + 0x78, 0x4a, 0xbf, 0x36, 0x3d, 0x10, 0xa3, 0xd5, 0xb0, 0xa3, 0x8d, 0x66, 0xb3, 0x59, 0x14, 0x53, 0x8f, 0x43, 0x9b, + 0xd4, 0x7c, 0x1b, 0x51, 0xaf, 0x50, 0x35, 0xb3, 0x6f, 0x4c, 0x3d, 0x62, 0xc9, 0x9c, 0xe2, 0x35, 0x14, 0x26, 0xc9, + 0x3d, 0x8b, 0x2d, 0xea, 0x16, 0x6d, 0x6e, 0xce, 0x1c, 0x1b, 0x92, 0xb8, 0x8a, 0x4b, 0x99, 0xae, 0x34, 0x1e, 0x05, + 0xc2, 0x61, 0x25, 0xda, 0x4e, 0x30, 0x1b, 0xf3, 0xf4, 0x83, 0x9f, 0x92, 0x9f, 0x7b, 0x04, 0x4c, 0xb3, 0x2f, 0x60, + 0x2b, 0xe9, 0xce, 0x8c, 0xf8, 0x48, 0x41, 0xce, 0xe1, 0x2b, 0x86, 0xe9, 0x7b, 0x9b, 0xc8, 0x72, 0x1f, 0xe7, 0x53, + 0x82, 0x32, 0x39, 0xa9, 0x76, 0x68, 0xbc, 0x81, 0xd8, 0x1b, 0x20, 0x9e, 0x86, 0xb0, 0x04, 0x4f, 0x23, 0x60, 0x90, + 0xf8, 0xbc, 0x5c, 0xc5, 0x43, 0x2d, 0x6e, 0x7c, 0x97, 0x79, 0x08, 0x70, 0xb6, 0x0c, 0x43, 0x6d, 0x62, 0x92, 0xfb, + 0xb3, 0x06, 0x74, 0x27, 0xa2, 0x62, 0x49, 0x66, 0x97, 0x75, 0x15, 0x85, 0xf9, 0x77, 0x5e, 0x2f, 0x53, 0x27, 0x9c, + 0x2d, 0xde, 0xb8, 0x0d, 0x80, 0xe9, 0x42, 0x7b, 0xaa, 0x93, 0x13, 0x93, 0xe2, 0xd7, 0x50, 0x1a, 0xd6, 0x09, 0x0d, + 0x14, 0x89, 0xfa, 0x79, 0xb4, 0x9e, 0x98, 0xa2, 0x38, 0xff, 0x11, 0x91, 0x0c, 0x4c, 0x12, 0xc8, 0x60, 0xb4, 0x7b, + 0xc5, 0x9a, 0x50, 0xac, 0xfd, 0xa4, 0x65, 0xd3, 0x99, 0xfb, 0x36, 0x83, 0x98, 0xbd, 0x1f, 0x04, 0x0f, 0x04, 0xff, + 0xe5, 0x56, 0x27, 0x8c, 0xa0, 0x04, 0xc3, 0xec, 0x30, 0xff, 0x89, 0xac, 0xba, 0x2d, 0xe8, 0xa8, 0x57, 0xe4, 0xaa, + 0x77, 0xe2, 0x52, 0x67, 0x12, 0x55, 0x4f, 0x7e, 0x9e, 0xb8, 0xfb, 0x56, 0x36, 0x46, 0x0b, 0xdc, 0xe7, 0xc8, 0x27, + 0x57, 0x6e, 0x66, 0xdb, 0xc8, 0xa8, 0xa6, 0x28, 0x12, 0x8b, 0x98, 0xca, 0xbc, 0x79, 0x8e, 0xfb, 0xf0, 0xaa, 0xb9, + 0x83, 0xef, 0xb7, 0x39, 0xd8, 0xca, 0xac, 0xdc, 0xe5, 0xf2, 0x6d, 0x7a, 0x68, 0xd0, 0xfd, 0xae, 0x73, 0xa4, 0x4b, + 0xef, 0xa6, 0x72, 0x5b, 0xb7, 0x3b, 0x53, 0xe7, 0x23, 0xf5, 0xd4, 0xf1, 0xf9, 0x99, 0x74, 0x63, 0xb7, 0xfe, 0x53, + 0x35, 0xc1, 0x4f, 0xf9, 0x02, 0xb4, 0x34, 0x55, 0x7c, 0x2c, 0x28, 0xa3, 0x16, 0xdd, 0xc1, 0x57, 0x6e, 0xb9, 0x15, + 0xf4, 0x2b, 0x9f, 0xab, 0xbc, 0x70, 0x8d, 0x5c, 0x3a, 0x7b, 0xe1, 0x04, 0x36, 0xf5, 0xe0, 0x9d, 0xf1, 0x57, 0xc1, + 0x25, 0xe0, 0xda, 0x10, 0x07, 0x23, 0x25, 0x89, 0xf7, 0xd5, 0xc0, 0x1b, 0x11, 0xf1, 0x8f, 0x82, 0xa1, 0x51, 0xfa, + 0x96, 0xfa, 0x18, 0x5b, 0x0c, 0xb3, 0x3e, 0xaa, 0x94, 0xab, 0xb3, 0xb9, 0xe0, 0xd4, 0x39, 0xfa, 0x13, 0x46, 0xdc, + 0xf3, 0x00, 0xe7, 0xac, 0xae, 0x7f, 0x06, 0xe7, 0xb5, 0xfd, 0xcc, 0x38, 0x1f, 0x8a, 0xa6, 0x44, 0xeb, 0xdd, 0x78, + 0xa7, 0x3c, 0x9b, 0x2d, 0xe7, 0x67, 0x15, 0x5e, 0x0d, 0xf7, 0x19, 0x9f, 0x5e, 0xfa, 0x1d, 0x98, 0x3c, 0x0e, 0xba, + 0xf4, 0xbe, 0x72, 0x78, 0xef, 0x4e, 0xc5, 0x4a, 0x15, 0x35, 0xe2, 0xd8, 0xa1, 0x7b, 0x8d, 0xc7, 0xbd, 0x0b, 0xcc, + 0x1a, 0xab, 0x93, 0xc3, 0x43, 0x6b, 0xab, 0x7c, 0x5d, 0x65, 0x84, 0x9d, 0xc4, 0x37, 0xcb, 0xc6, 0x94, 0x48, 0xb0, + 0xbf, 0x0d, 0x94, 0x63, 0x38, 0x10, 0xe1, 0x61, 0xc2, 0x9b, 0xac, 0xc2, 0xbc, 0x96, 0x8a, 0x32, 0xab, 0x1d, 0xfe, + 0x5a, 0x71, 0x8d, 0x68, 0x07, 0x4b, 0xae, 0xa4, 0x0d, 0x66, 0xd1, 0xa5, 0xa4, 0x61, 0x75, 0xc3, 0xa1, 0x5e, 0xdf, + 0x15, 0x5c, 0xd5, 0xb6, 0x66, 0x91, 0xfc, 0x45, 0xd9, 0x8e, 0x95, 0x08, 0x9b, 0x29, 0xf3, 0x3c, 0xfb, 0x3f, 0xa2, + 0x4a, 0x87, 0xdc, 0x02, 0xa6, 0xf6, 0x43, 0xba, 0x42, 0x52, 0x8c, 0x0d, 0xda, 0x4f, 0x4a, 0x57, 0x32, 0xef, 0xf8, + 0x8d, 0xc5, 0x75, 0xcb, 0x50, 0xe4, 0x62, 0x33, 0x56, 0x17, 0x1b, 0xc0, 0xc2, 0x2a, 0x07, 0xcc, 0x46, 0x14, 0xcd, + 0xa2, 0x6c, 0xca, 0xa3, 0xed, 0x16, 0xaf, 0x5b, 0xb4, 0xfa, 0xfb, 0x33, 0xd1, 0x33, 0x5b, 0x27, 0x55, 0x9d, 0x65, + 0xbd, 0x7f, 0x65, 0xc5, 0x5c, 0xe1, 0xe1, 0x47, 0x7b, 0x6e, 0xe7, 0x88, 0xce, 0xfb, 0xbb, 0x9b, 0xfe, 0x85, 0xdd, + 0xfc, 0x7f, 0x49, 0x37, 0x61, 0x86, 0xf5, 0xe4, 0xf6, 0xd3, 0x4b, 0xac, 0x09, 0xe7, 0x3f, 0xb2, 0x89, 0x61, 0xdb, + 0x15, 0xd4, 0x16, 0x35, 0x66, 0x9c, 0x12, 0x3c, 0xf6, 0x81, 0x0a, 0xed, 0x61, 0xe2, 0x0a, 0x61, 0x54, 0x79, 0xaa, + 0x44, 0xfa, 0x5c, 0xfc, 0xb2, 0x4d, 0x64, 0xd0, 0x19, 0x87, 0xb2, 0x81, 0x9d, 0xdb, 0xb5, 0xca, 0xcc, 0xd6, 0xd2, + 0xfa, 0x8f, 0x99, 0x62, 0xf3, 0x7f, 0xc0, 0x12, 0xf5, 0x90, 0x47, 0x7e, 0x59, 0xb5, 0x08, 0xef, 0x0d, 0xe5, 0xe6, + 0x21, 0xc8, 0x2d, 0x8b, 0x0e, 0x7f, 0x60, 0x3e, 0x40, 0x8e, 0x60, 0x8c, 0x1a, 0xb0, 0x52, 0x4e, 0x21, 0x97, 0xf9, + 0x71, 0xaa, 0xc9, 0x50, 0xcb, 0x72, 0x9d, 0xb1, 0x4a, 0x23, 0xaf, 0x59, 0x99, 0xa7, 0x59, 0x91, 0x6b, 0x94, 0x0d, + 0x15, 0xd7, 0x9f, 0x91, 0xa3, 0x51, 0x1b, 0xd0, 0x10, 0xbb, 0xe3, 0x9c, 0xd8, 0x28, 0x73, 0xd4, 0x71, 0x72, 0x4b, + 0x9e, 0x59, 0x57, 0x33, 0x5b, 0x89, 0x93, 0x8b, 0x77, 0x9b, 0xb1, 0x6d, 0x77, 0x34, 0x2e, 0x99, 0x27, 0x8e, 0x73, + 0x74, 0x7d, 0xa3, 0xcd, 0x9e, 0x97, 0xec, 0xb8, 0xf8, 0x3f, 0x48, 0x0e, 0xdd, 0x3c, 0x1a, 0x11, 0xcc, 0xc5, 0x25, + 0x45, 0xa9, 0xe9, 0xe6, 0x48, 0x02, 0x1b, 0x1e, 0xff, 0xb9, 0x89, 0xae, 0xf8, 0x78, 0x6e, 0x56, 0x46, 0x14, 0x5b, + 0x9c, 0xd8, 0x9f, 0xed, 0x61, 0xd5, 0x7a, 0x44, 0xc2, 0x81, 0xb3, 0xce, 0xfa, 0x60, 0x9f, 0xeb, 0xd2, 0xff, 0xe0, + 0x07, 0x36, 0x12, 0x82, 0x8d, 0x61, 0xf5, 0xce, 0xfe, 0xa7, 0x66, 0xc5, 0x85, 0xae, 0x35, 0x3b, 0x5e, 0xf8, 0x57, + 0x5c, 0xe1, 0x2d, 0x49, 0x65, 0x25, 0x37, 0x2e, 0x77, 0x2a, 0xe3, 0x05, 0x55, 0x3a, 0x66, 0x61, 0xe8, 0x58, 0x4c, + 0xaf, 0x0e, 0x4a, 0xaf, 0x08, 0x68, 0xa8, 0xce, 0xb9, 0xab, 0x95, 0xd9, 0x04, 0x97, 0x11, 0x92, 0x4a, 0x81, 0xbb, + 0xc2, 0x90, 0xe9, 0x9d, 0x6f, 0x86, 0x7e, 0x30, 0x14, 0x66, 0x6e, 0x40, 0xd8, 0x32, 0x41, 0xa5, 0xc3, 0x9a, 0x15, + 0x7b, 0x41, 0x9b, 0x0c, 0xe6, 0x3c, 0xa2, 0xde, 0x6b, 0xa4, 0xbf, 0x73, 0xc2, 0x05, 0x38, 0x4a, 0x81, 0xc2, 0x80, + 0x2e, 0x6f, 0x3c, 0x40, 0x72, 0x89, 0x10, 0x63, 0x0d, 0x85, 0xd4, 0x26, 0x7e, 0x39, 0xbf, 0xe2, 0x9e, 0xf7, 0xb3, + 0xe3, 0xac, 0xeb, 0x5b, 0x03, 0x79, 0x98, 0x5f, 0xbf, 0xbd, 0xce, 0x7a, 0x90, 0xb3, 0x21, 0x71, 0xb1, 0xb2, 0xf3, + 0x8a, 0x76, 0x76, 0x45, 0x5b, 0xea, 0x6a, 0x54, 0xe1, 0xb6, 0x86, 0x29, 0x52, 0x54, 0xb1, 0xe1, 0x7a, 0x1b, 0xba, + 0x20, 0xe9, 0x8b, 0x35, 0x85, 0x84, 0x19, 0xbb, 0xa6, 0x30, 0x95, 0x3b, 0xa1, 0x47, 0x67, 0xc3, 0x40, 0x5f, 0x6c, + 0xfd, 0x02, 0xf4, 0xa7, 0x8d, 0x8d, 0x36, 0x7d, 0x4f, 0x54, 0x46, 0xcc, 0x29, 0xfa, 0xbc, 0xc3, 0xec, 0xd3, 0xfe, + 0x44, 0x77, 0xb0, 0x5a, 0x5f, 0xc6, 0x5f, 0x56, 0x6c, 0xd4, 0xc7, 0xd6, 0x33, 0x26, 0x89, 0x53, 0xc9, 0xed, 0x41, + 0x49, 0x41, 0x66, 0xde, 0x44, 0x0d, 0x19, 0x29, 0xad, 0x39, 0x8f, 0x20, 0xfe, 0x77, 0xae, 0x98, 0x99, 0x98, 0xf6, + 0x63, 0x5c, 0x52, 0x1f, 0x7f, 0xf7, 0xc4, 0x5b, 0xbb, 0x77, 0x9a, 0xa1, 0x63, 0xf6, 0x00, 0x81, 0x9c, 0x57, 0x5e, + 0xba, 0x60, 0x68, 0x6e, 0xad, 0x54, 0xb3, 0xa6, 0x51, 0xfe, 0xb3, 0xbb, 0x32, 0x05, 0x03, 0xfb, 0x44, 0xad, 0x3f, + 0xdb, 0xe5, 0x66, 0xea, 0x1b, 0xb3, 0x57, 0x03, 0x4e, 0x04, 0x66, 0x36, 0xdd, 0x54, 0xfa, 0xaf, 0xfb, 0xfe, 0x3b, + 0x16, 0xa0, 0xd8, 0xd9, 0xc8, 0x1f, 0x9a, 0x8a, 0xe0, 0xc6, 0x77, 0x67, 0x2f, 0x86, 0x2d, 0x0a, 0x05, 0x5f, 0x46, + 0x99, 0xee, 0x32, 0xf2, 0x07, 0x0d, 0x6d, 0xf0, 0x4b, 0x7a, 0x63, 0x1b, 0x97, 0x61, 0x1f, 0xed, 0x61, 0x12, 0xbb, + 0x60, 0x68, 0x6b, 0x62, 0x41, 0x50, 0x35, 0x75, 0xde, 0x30, 0x22, 0xa1, 0x6f, 0xad, 0x95, 0xcf, 0xeb, 0xd8, 0x33, + 0xde, 0x71, 0x3e, 0x64, 0x62, 0x04, 0x7e, 0x8b, 0xb6, 0x5b, 0x12, 0xca, 0xb8, 0x74, 0x0c, 0x32, 0xb5, 0x47, 0x6d, + 0xc7, 0xc9, 0xb4, 0xed, 0x76, 0xd4, 0xee, 0xd1, 0xdd, 0xcd, 0x6f, 0x06, 0xa5, 0xed, 0x8e, 0xf0, 0x2d, 0xbc, 0x3a, + 0x73, 0xe4, 0x7e, 0xeb, 0xee, 0x24, 0x5b, 0xa0, 0x37, 0x33, 0x15, 0x14, 0x75, 0xc2, 0xc9, 0x33, 0xd6, 0xf8, 0xbf, + 0xd0, 0x54, 0xc1, 0x10, 0x98, 0xcc, 0x44, 0xb2, 0xdb, 0x82, 0x7c, 0x16, 0xfa, 0xfb, 0x14, 0x6e, 0x15, 0xb2, 0xb4, + 0x2d, 0x66, 0x08, 0xa7, 0x7a, 0xd0, 0x0c, 0x5e, 0x42, 0x81, 0x28, 0xed, 0x9d, 0xa1, 0x32, 0xe8, 0x41, 0xa5, 0x03, + 0x99, 0x28, 0x06, 0x35, 0x4b, 0x61, 0xca, 0x9b, 0x90, 0x7a, 0xf7, 0x7b, 0xbd, 0xf5, 0x77, 0xf9, 0xde, 0x8c, 0x22, + 0x1e, 0xf5, 0xd6, 0x49, 0x02, 0x82, 0x5f, 0x71, 0x20, 0x13, 0xe5, 0xf5, 0x92, 0x18, 0xb1, 0x8e, 0xc7, 0x49, 0xae, + 0x16, 0x1d, 0xaf, 0xc4, 0x39, 0x25, 0x15, 0x42, 0xce, 0x01, 0x0c, 0x13, 0x05, 0xee, 0xe5, 0x38, 0x82, 0xf5, 0x80, + 0x67, 0x72, 0x45, 0x3d, 0x1b, 0x8b, 0xbb, 0xfd, 0xef, 0xe5, 0xd5, 0xed, 0x9a, 0xf6, 0x36, 0x49, 0x01, 0x56, 0x5d, + 0x54, 0x82, 0xef, 0xfe, 0xfc, 0x29, 0xe4, 0xb1, 0x64, 0x87, 0x5a, 0x2a, 0x73, 0x30, 0x5b, 0x74, 0x1d, 0x72, 0xd6, + 0xa7, 0xaa, 0x3a, 0x36, 0x39, 0xa0, 0x86, 0xd3, 0xb4, 0x73, 0xc1, 0x78, 0x9c, 0xb0, 0x86, 0x73, 0xc2, 0x1a, 0x76, + 0xa8, 0x68, 0x23, 0x8c, 0x6e, 0x68, 0x31, 0x96, 0xb4, 0xc6, 0x7c, 0x3b, 0x20, 0x24, 0xf8, 0x7a, 0xa1, 0x95, 0x8b, + 0x8c, 0xe3, 0x8f, 0x2d, 0x06, 0x13, 0xec, 0x12, 0x2b, 0xdd, 0x84, 0x7f, 0x0d, 0xcf, 0x95, 0xbe, 0x95, 0x27, 0x71, + 0x73, 0x6f, 0xce, 0xe1, 0x44, 0xe3, 0x51, 0x93, 0x8c, 0xfc, 0x94, 0xf5, 0xa8, 0x94, 0xe4, 0x3f, 0x37, 0x8f, 0x81, + 0x33, 0x73, 0x8b, 0x7d, 0x25, 0x30, 0x26, 0x54, 0x3a, 0x96, 0xf1, 0x2f, 0x11, 0xf5, 0xd9, 0x68, 0xc4, 0x0c, 0x0a, + 0xe3, 0x5c, 0x25, 0x56, 0xe2, 0x3e, 0xdb, 0xa2, 0x97, 0xf2, 0xae, 0x31, 0x46, 0x25, 0x4c, 0xc5, 0x2f, 0x46, 0xf6, + 0x18, 0xa9, 0xb7, 0x73, 0xb6, 0xfd, 0x5c, 0x13, 0xdd, 0x73, 0x3a, 0x90, 0x04, 0x8d, 0x4b, 0x66, 0x0a, 0x90, 0xc4, + 0x04, 0x63, 0x72, 0x07, 0x2c, 0xda, 0xa6, 0x75, 0x9e, 0xc2, 0xab, 0x56, 0xe3, 0x49, 0x65, 0x7b, 0xdf, 0x65, 0x65, + 0x2e, 0xdb, 0x8e, 0x4e, 0x5b, 0x12, 0x24, 0x8d, 0x1a, 0xa7, 0x48, 0x48, 0xd5, 0xd3, 0xac, 0x0c, 0x0b, 0x84, 0xb5, + 0xe2, 0x9c, 0xbe, 0xb9, 0x35, 0x99, 0x9d, 0x17, 0xb1, 0x57, 0x78, 0x15, 0x85, 0x08, 0x6e, 0x67, 0x13, 0x89, 0x0f, + 0x63, 0xcb, 0x3a, 0x59, 0xc8, 0xd2, 0xb7, 0x6e, 0xad, 0x4b, 0xc0, 0x0f, 0xde, 0xea, 0xb7, 0xfb, 0xf1, 0x38, 0xb4, + 0x30, 0xd6, 0x47, 0xb8, 0xf8, 0xa8, 0x17, 0x2c, 0xad, 0x7c, 0x89, 0x08, 0x4a, 0x9b, 0xa5, 0xd7, 0xbf, 0x60, 0xb1, + 0x29, 0x2f, 0x57, 0x2c, 0x34, 0x36, 0x74, 0x33, 0x0d, 0xd5, 0x32, 0x31, 0x27, 0x15, 0x55, 0x31, 0xc7, 0x00, 0x3d, + 0xee, 0x20, 0x73, 0xcb, 0x22, 0x6b, 0xd2, 0xc3, 0x59, 0x09, 0xcc, 0xd7, 0x60, 0xe7, 0x38, 0x03, 0xea, 0xd8, 0xa4, + 0xea, 0x17, 0x0b, 0xa0, 0x24, 0x6e, 0xe0, 0x5b, 0x21, 0x77, 0xa1, 0xca, 0x1e, 0x29, 0xa4, 0xb0, 0x0e, 0x2c, 0xe1, + 0xac, 0x60, 0xc5, 0xd8, 0x3e, 0x6c, 0xe6, 0x8f, 0x51, 0x6f, 0x01, 0xd3, 0x43, 0x08, 0xf3, 0xdd, 0x1d, 0xb8, 0x11, + 0x1d, 0xad, 0xc9, 0xe4, 0x1e, 0x27, 0xc8, 0xa2, 0x9f, 0xfb, 0x25, 0x31, 0x14, 0x4f, 0xc8, 0xcb, 0x51, 0x33, 0x16, + 0xb5, 0x60, 0x5a, 0xa6, 0xcd, 0x2d, 0xdf, 0x7d, 0x6d, 0x23, 0xaa, 0x47, 0xc4, 0xa5, 0x42, 0x48, 0x1d, 0x14, 0xe8, + 0x0e, 0x73, 0xa9, 0xeb, 0xc9, 0xb3, 0x45, 0xf1, 0x2c, 0x9b, 0xae, 0x12, 0xfc, 0xe9, 0xe3, 0x0d, 0xb5, 0xbd, 0x09, + 0xa8, 0xf4, 0x5e, 0x77, 0x9c, 0x93, 0xde, 0x51, 0x89, 0x88, 0x26, 0x19, 0x7f, 0xfb, 0xc8, 0xbc, 0x05, 0x91, 0x58, + 0xeb, 0xe1, 0xd2, 0xeb, 0xb7, 0xaf, 0x51, 0xb0, 0x6a, 0x22, 0x9c, 0xbd, 0xa5, 0x49, 0x1c, 0xbc, 0x14, 0x21, 0x19, + 0x8a, 0x60, 0xe4, 0xa3, 0x82, 0xd8, 0x8a, 0xad, 0x12, 0x75, 0xb5, 0x86, 0x40, 0xc4, 0x39, 0xd8, 0x20, 0xb3, 0x8c, + 0xce, 0x99, 0xd7, 0xbe, 0x3c, 0x44, 0xf1, 0xd2, 0x14, 0xf5, 0xbf, 0x5a, 0x16, 0x7e, 0xf4, 0x70, 0xe0, 0x75, 0x64, + 0xe5, 0xac, 0x77, 0xbd, 0x54, 0x6e, 0xcb, 0x3a, 0x6e, 0xad, 0x7a, 0x4f, 0x9e, 0x20, 0xa7, 0xd1, 0xa6, 0x97, 0xe2, + 0xd6, 0x21, 0xa9, 0x31, 0xbc, 0x56, 0xb5, 0xa8, 0x8f, 0x0b, 0x77, 0xd8, 0x8b, 0x5a, 0xa9, 0x77, 0x30, 0x11, 0x5d, + 0xf7, 0xed, 0x9f, 0x88, 0x6a, 0xc8, 0x98, 0x8e, 0x35, 0xe4, 0x0e, 0x6c, 0xc1, 0xf4, 0x54, 0xd2, 0x77, 0x02, 0xf1, + 0xf8, 0x48, 0xb2, 0xab, 0xff, 0x94, 0xd1, 0xfd, 0x85, 0x8c, 0x81, 0x91, 0xd1, 0x1d, 0x61, 0x2d, 0xc2, 0xbd, 0x34, + 0xe8, 0x18, 0x23, 0x94, 0x4f, 0x89, 0x66, 0x66, 0xd9, 0x6d, 0x5e, 0x90, 0xd8, 0xe7, 0x5a, 0xcd, 0xde, 0x72, 0x9d, + 0x48, 0xd0, 0xa2, 0x04, 0xe2, 0xe5, 0x96, 0x19, 0x17, 0x80, 0xae, 0x8d, 0x9b, 0x14, 0x71, 0xb8, 0xb1, 0xd9, 0xdb, + 0x00, 0xa0, 0x7d, 0xfe, 0xfd, 0x4c, 0xe9, 0xe2, 0x76, 0x41, 0x09, 0x9b, 0x1f, 0x2c, 0x26, 0x8b, 0x5b, 0x19, 0x14, + 0x62, 0x23, 0x04, 0x0f, 0x64, 0x13, 0x8d, 0xdd, 0x7a, 0x8a, 0xd8, 0x3c, 0x5f, 0x20, 0x6d, 0x51, 0x78, 0x26, 0x67, + 0x93, 0xfd, 0x8b, 0x76, 0xb0, 0x81, 0xb1, 0x6e, 0x52, 0x94, 0xdf, 0x95, 0xa6, 0xa3, 0x8c, 0xda, 0xc7, 0x2f, 0x37, + 0x5c, 0x94, 0xa5, 0x26, 0x30, 0x9a, 0x46, 0xdd, 0xf2, 0xf7, 0x89, 0x13, 0x0c, 0x5d, 0x19, 0x01, 0xca, 0xb9, 0x94, + 0x09, 0x9f, 0xb3, 0x6f, 0x90, 0x16, 0x00, 0xf2, 0x9b, 0x1f, 0xb5, 0xe3, 0x63, 0x73, 0xbd, 0xfc, 0xd2, 0xb6, 0xa5, + 0x44, 0xf4, 0x5f, 0xda, 0x2a, 0xdb, 0xb1, 0x0f, 0x54, 0xf1, 0x30, 0x6a, 0x44, 0xcb, 0x9a, 0x0f, 0x59, 0xfb, 0x14, + 0x0f, 0x9b, 0x7b, 0x6f, 0x76, 0xa6, 0xc8, 0x86, 0xda, 0x25, 0xfb, 0xcb, 0x4b, 0x3a, 0x2f, 0xaf, 0xd6, 0x0c, 0x5e, + 0xed, 0x11, 0xea, 0x2a, 0x02, 0x05, 0x8f, 0xc1, 0x01, 0xbe, 0x36, 0xfb, 0x9e, 0x2d, 0x28, 0xf0, 0xcf, 0x8e, 0x9d, + 0xbf, 0x3c, 0x9f, 0x43, 0x02, 0x59, 0x9f, 0x35, 0x49, 0x04, 0x44, 0x24, 0x74, 0x3a, 0xdb, 0x1a, 0x82, 0x3c, 0x8c, + 0x2c, 0x1e, 0xb1, 0x59, 0xc6, 0x7f, 0xb1, 0x98, 0x8b, 0xcb, 0x7b, 0x36, 0xb9, 0x9f, 0x9b, 0xb7, 0xce, 0x00, 0xa9, + 0x6d, 0x9a, 0xc9, 0x48, 0x75, 0x64, 0x1a, 0x40, 0x05, 0xed, 0x85, 0x52, 0x4a, 0x46, 0xa9, 0x1c, 0x23, 0xb6, 0x6b, + 0x23, 0xe3, 0xe2, 0x64, 0x49, 0xc3, 0xb0, 0x24, 0xf8, 0x35, 0x11, 0x04, 0xbd, 0x54, 0x44, 0xf5, 0x70, 0x51, 0xca, + 0xdb, 0x21, 0x8f, 0x06, 0xd0, 0x52, 0xe3, 0x6d, 0x92, 0xa7, 0xdd, 0x8b, 0x73, 0x17, 0x59, 0x71, 0xf3, 0xa7, 0xc4, + 0x0f, 0x95, 0x63, 0x3c, 0x29, 0x90, 0x18, 0xe7, 0x5d, 0xb9, 0xf3, 0xa0, 0x0e, 0xc4, 0x1c, 0x13, 0x3c, 0xd2, 0xb3, + 0xaa, 0x3d, 0x98, 0x19, 0x68, 0x53, 0x1a, 0x4d, 0x15, 0xb5, 0x01, 0xe5, 0xff, 0x80, 0xbe, 0xca, 0xa7, 0xe5, 0x91, + 0x6b, 0x10, 0x86, 0xd2, 0x7a, 0x4b, 0xc3, 0x4b, 0x42, 0x68, 0x71, 0xae, 0x4c, 0x32, 0x08, 0xbc, 0xf1, 0xa1, 0xd7, + 0x35, 0x7e, 0x10, 0x25, 0x40, 0x73, 0xe6, 0x27, 0x1f, 0x3e, 0x9e, 0x03, 0x14, 0xce, 0x5a, 0x32, 0xfa, 0xb3, 0xab, + 0x09, 0x4b, 0xba, 0x5d, 0x34, 0xbb, 0x11, 0xca, 0x57, 0x29, 0x58, 0x5a, 0x58, 0x8a, 0xde, 0xa2, 0x3c, 0x30, 0x6c, + 0xb7, 0xb2, 0x7d, 0xfb, 0x5f, 0x1e, 0xde, 0x2b, 0x74, 0x91, 0xb0, 0x1d, 0xe2, 0xa7, 0xa8, 0xe9, 0x2f, 0x3e, 0x9c, + 0x9e, 0x8c, 0x61, 0xbb, 0x2b, 0x61, 0xee, 0x30, 0xcf, 0xb1, 0xbf, 0x74, 0xe4, 0x86, 0xb6, 0x12, 0x31, 0xf9, 0x5a, + 0x36, 0x61, 0x11, 0x07, 0x0c, 0x64, 0xae, 0x06, 0xb9, 0x83, 0x23, 0x04, 0xa6, 0xd6, 0x7c, 0xf2, 0xff, 0x54, 0x2d, + 0x1e, 0x9f, 0x2d, 0x8b, 0x4a, 0x82, 0x7c, 0x2b, 0xed, 0xf3, 0xd8, 0x87, 0xa4, 0x1d, 0xd8, 0xf7, 0x08, 0x16, 0xbd, + 0xdd, 0x61, 0x51, 0x68, 0xa1, 0x83, 0xb8, 0xa4, 0xce, 0xa7, 0xf0, 0xea, 0xe5, 0x32, 0x85, 0xd0, 0x29, 0x0b, 0x3c, + 0x5f, 0x45, 0x38, 0xa6, 0xf7, 0xc7, 0x03, 0x95, 0x05, 0xa5, 0x5c, 0x4e, 0xf0, 0x29, 0x6f, 0xea, 0x70, 0x06, 0xd4, + 0x90, 0xf6, 0xa9, 0x70, 0xc5, 0x3f, 0x4a, 0x59, 0x17, 0x3a, 0xb3, 0x90, 0xaa, 0x30, 0xd9, 0x91, 0xf0, 0xbf, 0x54, + 0xcc, 0x90, 0xe1, 0x85, 0x50, 0xa5, 0x0d, 0x7c, 0x6d, 0x8b, 0xae, 0x94, 0x17, 0x6d, 0x0b, 0x7d, 0x2c, 0x76, 0x65, + 0x4e, 0x00, 0xba, 0x01, 0x5a, 0x7b, 0xed, 0x82, 0xbb, 0x1b, 0xee, 0x65, 0x9f, 0x15, 0xf7, 0x6e, 0xda, 0x00, 0x07, + 0x5f, 0x20, 0xa7, 0xbe, 0x7f, 0x45, 0x71, 0xfe, 0x69, 0x2b, 0x1e, 0x2d, 0xc4, 0x94, 0x80, 0x09, 0x24, 0xe4, 0x1b, + 0x3e, 0xb6, 0x66, 0xc4, 0x3e, 0x7e, 0x08, 0x37, 0x4a, 0x09, 0x2b, 0x8d, 0x3c, 0x38, 0xca, 0xed, 0x37, 0x55, 0x86, + 0xe4, 0xb6, 0x9c, 0x83, 0xc2, 0x10, 0x0b, 0x07, 0xdc, 0x65, 0xae, 0x6c, 0x7f, 0xbc, 0x4a, 0x8f, 0xc2, 0x9e, 0xb8, + 0x50, 0xb1, 0x18, 0x6a, 0x64, 0xc4, 0x2b, 0x1e, 0xaa, 0xb3, 0xd2, 0xc4, 0x00, 0x19, 0x61, 0x80, 0x8e, 0x29, 0x6d, + 0x84, 0x40, 0x09, 0x01, 0x5b, 0x7e, 0xa8, 0xa3, 0x42, 0x13, 0xa1, 0x08, 0xa1, 0x25, 0xd2, 0x1c, 0x1d, 0x64, 0x65, + 0x86, 0xa4, 0xd2, 0x63, 0x76, 0x4c, 0x07, 0x96, 0x05, 0x58, 0x52, 0x29, 0x0a, 0x20, 0x9f, 0x8c, 0x51, 0xab, 0x88, + 0x50, 0xe2, 0xae, 0xbc, 0x4c, 0x1a, 0x0e, 0x58, 0xc3, 0x5c, 0x34, 0x17, 0x4b, 0xd6, 0x75, 0x38, 0x94, 0x21, 0x4d, + 0xae, 0x5a, 0x05, 0x79, 0xa7, 0x3f, 0x4f, 0x63, 0xce, 0x57, 0x04, 0x42, 0x9b, 0xfb, 0x91, 0xcb, 0x05, 0xc2, 0x8f, + 0x74, 0x6c, 0x8c, 0x91, 0x91, 0xb4, 0x76, 0x20, 0x75, 0x51, 0x22, 0x24, 0xc4, 0x95, 0x74, 0x41, 0x73, 0x3e, 0x14, + 0x22, 0x3e, 0x3b, 0x61, 0xae, 0x0f, 0x12, 0xb3, 0x44, 0xe5, 0xdf, 0x37, 0xcb, 0x61, 0xf5, 0x42, 0xf0, 0xb0, 0xd8, + 0xae, 0xaa, 0x1c, 0x28, 0x24, 0x12, 0xd6, 0xa8, 0x13, 0xe6, 0xce, 0x1b, 0xcb, 0xdf, 0x14, 0xc1, 0x9e, 0x27, 0x64, + 0x26, 0x18, 0xa5, 0x57, 0x51, 0xae, 0x54, 0xef, 0x94, 0x39, 0x8c, 0xdc, 0xf0, 0xee, 0xa6, 0xf8, 0xc1, 0x81, 0xbc, + 0x67, 0x53, 0x7a, 0xc4, 0xdb, 0xfd, 0x50, 0x4b, 0x9c, 0x53, 0x24, 0x39, 0x41, 0x29, 0xe8, 0xfe, 0xc3, 0x6b, 0x47, + 0x25, 0x31, 0xfe, 0xd0, 0xa2, 0xf4, 0x5b, 0x8b, 0xa7, 0xb9, 0x96, 0x33, 0x6d, 0xd2, 0xcc, 0x9c, 0x6f, 0x46, 0x15, + 0x9b, 0x2b, 0x63, 0x68, 0x5d, 0x70, 0x20, 0x00, 0x37, 0x83, 0x75, 0x2a, 0xad, 0xcf, 0xf5, 0x07, 0x08, 0x7d, 0xe3, + 0x3e, 0x28, 0xb3, 0x1d, 0x3c, 0x1a, 0x63, 0xc8, 0xeb, 0x67, 0x57, 0x75, 0xd0, 0x65, 0x44, 0x82, 0x00, 0x16, 0x7a, + 0xc8, 0xe1, 0x95, 0xba, 0x9c, 0xd9, 0xca, 0xec, 0xd1, 0xe6, 0xb5, 0x1c, 0x6f, 0x1d, 0x69, 0x38, 0x2e, 0x8e, 0x67, + 0x1f, 0x2c, 0x9d, 0x47, 0xe8, 0x48, 0xca, 0x8d, 0xf7, 0x4a, 0x20, 0xdf, 0x10, 0x19, 0xa8, 0xe7, 0xa2, 0x02, 0xb0, + 0x2b, 0x8b, 0xaa, 0xe4, 0x75, 0x78, 0xe8, 0xf9, 0x38, 0x32, 0x8f, 0x18, 0xe3, 0x10, 0x55, 0x46, 0x1e, 0x9d, 0xdc, + 0x2e, 0x2d, 0x32, 0x6a, 0x2e, 0x98, 0x5a, 0xcd, 0xbb, 0xea, 0x94, 0x07, 0xb2, 0xc9, 0xd7, 0x2b, 0x2d, 0xb4, 0x1e, + 0x89, 0x15, 0xdd, 0xac, 0x8a, 0x7a, 0x58, 0x20, 0x62, 0xbd, 0xff, 0x04, 0x91, 0x47, 0x2c, 0x1f, 0x64, 0xd4, 0x22, + 0x6d, 0xae, 0xc4, 0x4a, 0x29, 0x60, 0x76, 0x81, 0x42, 0x2b, 0xef, 0x10, 0x5c, 0xf9, 0x4d, 0x85, 0x44, 0xda, 0xc5, + 0x5d, 0x07, 0xea, 0x11, 0xbf, 0x35, 0xb2, 0x59, 0x1f, 0xa8, 0xe5, 0x7c, 0x2b, 0x2a, 0x1a, 0x22, 0x23, 0xd2, 0xd1, + 0x6f, 0x38, 0x01, 0xc3, 0x82, 0x0c, 0xe9, 0xf4, 0x3c, 0xf5, 0x58, 0xa0, 0xc5, 0x50, 0xe5, 0x54, 0x8c, 0x65, 0x52, + 0xdd, 0x0a, 0x96, 0x29, 0xb3, 0x50, 0x12, 0x5d, 0x41, 0xcb, 0xec, 0x35, 0xd8, 0x7c, 0xcf, 0x6a, 0x5b, 0x64, 0x44, + 0xc2, 0x35, 0xc2, 0x1f, 0x86, 0x31, 0x00, 0xaf, 0x12, 0xa5, 0xf3, 0xc0, 0x68, 0xc5, 0x24, 0xe6, 0x71, 0x0a, 0xaf, + 0x9b, 0x2a, 0x79, 0x81, 0x5b, 0xf3, 0xd4, 0xd4, 0x58, 0x7e, 0xff, 0xfa, 0xfb, 0x41, 0x53, 0x65, 0xad, 0x40, 0x7e, + 0xb2, 0x6e, 0xfd, 0xfb, 0x2e, 0xf7, 0x20, 0x6f, 0xd3, 0xfb, 0x7e, 0x1c, 0xf2, 0x0d, 0x04, 0x82, 0x51, 0x0a, 0xd3, + 0xc5, 0xfa, 0xb4, 0xc2, 0xe8, 0x7a, 0x49, 0xbb, 0x32, 0x7d, 0x40, 0xc2, 0xfb, 0x7a, 0xfb, 0x19, 0xa1, 0xcc, 0x12, + 0xfb, 0x50, 0x10, 0xc5, 0x4a, 0x94, 0x47, 0xe6, 0x67, 0x73, 0x6f, 0x45, 0x5c, 0x30, 0x53, 0xfd, 0x62, 0xf2, 0x30, + 0x24, 0x1c, 0x98, 0x99, 0x08, 0x07, 0xd6, 0xb4, 0xf0, 0xec, 0x6a, 0xc1, 0x9f, 0x96, 0x12, 0xe0, 0x11, 0xeb, 0x2a, + 0xfd, 0xbd, 0x8c, 0xa5, 0x18, 0xb1, 0xbd, 0x9d, 0xa1, 0xf4, 0x9c, 0x59, 0x07, 0x1d, 0x5e, 0x89, 0x82, 0x2d, 0xce, + 0xf4, 0x03, 0x33, 0x39, 0x8a, 0x0b, 0xaa, 0x25, 0xfc, 0xed, 0xad, 0xe2, 0xbe, 0x54, 0x3c, 0xa7, 0xb0, 0xef, 0x49, + 0xbe, 0xf8, 0x20, 0xed, 0xbd, 0xa8, 0x82, 0x56, 0x38, 0xb5, 0xc1, 0x0d, 0xf1, 0xd1, 0x9d, 0xf2, 0x50, 0x29, 0x42, + 0x08, 0xa3, 0xe8, 0x17, 0xf5, 0x08, 0x79, 0x81, 0xd7, 0xcb, 0xb7, 0x75, 0x7d, 0x48, 0x98, 0x82, 0xb8, 0xbe, 0xdb, + 0xc2, 0x19, 0x7d, 0x6a, 0x46, 0xcd, 0xdd, 0x71, 0xf5, 0x17, 0xea, 0x16, 0xef, 0x40, 0xb4, 0xc3, 0x74, 0x0f, 0x8f, + 0xeb, 0xfa, 0x68, 0x72, 0xd4, 0x85, 0x26, 0x6e, 0x35, 0xdb, 0xaf, 0xb4, 0x64, 0x9e, 0x06, 0x30, 0x68, 0x8c, 0xfa, + 0x7d, 0xc9, 0x1e, 0x8d, 0xef, 0x78, 0x4b, 0x64, 0x43, 0xb8, 0x0d, 0xe8, 0x70, 0x70, 0xa1, 0xa7, 0xfe, 0x46, 0xf6, + 0x6b, 0xb9, 0x04, 0xc7, 0x4b, 0xf1, 0x63, 0xdd, 0xf0, 0x47, 0x99, 0xd0, 0xec, 0x24, 0xa6, 0xc1, 0xfd, 0xb6, 0xb8, + 0xb2, 0xc2, 0x65, 0x12, 0x0a, 0x0b, 0x68, 0x40, 0x60, 0x01, 0x45, 0xd0, 0xe7, 0x30, 0x49, 0x94, 0x3d, 0x9c, 0xb3, + 0xdd, 0xdb, 0x7b, 0x71, 0x4c, 0x24, 0x9d, 0xd2, 0xe4, 0xe8, 0x07, 0x5e, 0x4c, 0x67, 0x51, 0x93, 0x68, 0xa4, 0x5f, + 0x31, 0x27, 0x2f, 0x1b, 0xe9, 0xc8, 0x00, 0x31, 0x67, 0x15, 0xa2, 0x0b, 0x69, 0xdf, 0x3f, 0x23, 0x32, 0xa0, 0x62, + 0x50, 0x37, 0xc3, 0x0e, 0xb1, 0x29, 0xe7, 0xb5, 0xeb, 0x07, 0x46, 0x4b, 0x7c, 0xb1, 0x3c, 0xca, 0xb0, 0xfc, 0x31, + 0x1f, 0xa3, 0xef, 0xbc, 0x95, 0x65, 0xe8, 0xc2, 0x72, 0x7e, 0x17, 0x93, 0xa5, 0x3a, 0x5c, 0x3d, 0x09, 0xe9, 0x7e, + 0x6a, 0xcd, 0x4b, 0xff, 0xb3, 0xe9, 0x82, 0xb4, 0xcf, 0x3c, 0xa4, 0x7e, 0x92, 0xc7, 0x68, 0xf7, 0x55, 0x2f, 0xac, + 0xd3, 0x85, 0x11, 0x66, 0xfa, 0xa8, 0x9a, 0x85, 0x0f, 0x55, 0x66, 0xcb, 0xc6, 0xd3, 0x52, 0xcc, 0x1f, 0x1d, 0xc1, + 0x1a, 0x82, 0x26, 0x24, 0x1b, 0xf7, 0x25, 0xf1, 0x83, 0xea, 0x82, 0xf1, 0x60, 0x87, 0xe5, 0xf5, 0xfc, 0x66, 0xd7, + 0xbe, 0xd3, 0xf2, 0xa9, 0xe0, 0x1f, 0x3c, 0xc4, 0xca, 0x9f, 0x0a, 0x87, 0x25, 0x2e, 0xbe, 0xb0, 0x52, 0x67, 0xbd, + 0x28, 0xa4, 0xad, 0xd5, 0xac, 0xa8, 0x65, 0x37, 0x64, 0xe5, 0x55, 0xde, 0xf2, 0x52, 0x0a, 0x7e, 0x4d, 0x45, 0x4e, + 0x72, 0x9d, 0x72, 0x31, 0x18, 0x78, 0x33, 0x27, 0xfd, 0x7a, 0x42, 0x1b, 0xb9, 0x81, 0x71, 0xfa, 0x3a, 0xb6, 0x2e, + 0x90, 0x04, 0x76, 0xf5, 0x91, 0x0b, 0x4f, 0x20, 0x91, 0xd5, 0xc7, 0x73, 0x36, 0xd6, 0xcd, 0x4e, 0xbe, 0x97, 0x77, + 0x19, 0x47, 0xf0, 0x4c, 0x21, 0xde, 0x9c, 0xd7, 0x06, 0xfc, 0x23, 0xef, 0x70, 0x6e, 0xe5, 0x7d, 0x50, 0x8d, 0xa1, + 0x35, 0x6c, 0x69, 0xe0, 0xab, 0x0b, 0x8c, 0x61, 0x02, 0xd5, 0x24, 0x08, 0x8e, 0xd6, 0x62, 0xbb, 0x20, 0x38, 0x96, + 0x51, 0x78, 0xb1, 0x3a, 0xe5, 0x17, 0x66, 0x53, 0x11, 0x45, 0x26, 0xfc, 0xc2, 0x0e, 0xd5, 0x66, 0x84, 0x43, 0xfc, + 0x58, 0x11, 0xe5, 0x43, 0x8d, 0x07, 0x20, 0x0e, 0x60, 0x7a, 0xd3, 0x88, 0x68, 0x7f, 0x8d, 0x1a, 0x05, 0x35, 0x3c, + 0x73, 0x97, 0xde, 0x59, 0x23, 0x2e, 0xeb, 0x6f, 0x0a, 0xcc, 0x2b, 0xb1, 0x6c, 0xaf, 0xac, 0xcb, 0x92, 0xf3, 0x3d, + 0x68, 0xe2, 0xb8, 0xd3, 0xce, 0x92, 0xb3, 0xe4, 0x00, 0x6d, 0xbb, 0xa7, 0xcd, 0x7c, 0x42, 0x72, 0x71, 0xb5, 0xeb, + 0x14, 0xa4, 0x32, 0xaf, 0x62, 0x23, 0x95, 0xe2, 0xbc, 0x73, 0x09, 0x38, 0xdc, 0x4f, 0xf1, 0xff, 0x55, 0xa2, 0xbe, + 0x9b, 0x5f, 0x10, 0xc6, 0x76, 0x52, 0xbf, 0x1c, 0x36, 0x6d, 0x61, 0x76, 0x70, 0xdd, 0xe2, 0xa6, 0xdd, 0x10, 0x55, + 0xd9, 0x5b, 0x6b, 0xbe, 0x34, 0x0f, 0x79, 0x61, 0x39, 0xb3, 0xd0, 0xea, 0xf3, 0xb8, 0x65, 0x44, 0xf9, 0xd4, 0x85, + 0xc3, 0xb1, 0xd0, 0x90, 0xcb, 0x9b, 0x43, 0x5d, 0xe4, 0xc7, 0xa4, 0xed, 0x01, 0x03, 0x49, 0x3d, 0xd1, 0xc6, 0x87, + 0x6e, 0xe6, 0xb6, 0x3b, 0x03, 0xe9, 0x7a, 0x39, 0x0d, 0x25, 0xb3, 0x18, 0xb8, 0x70, 0x34, 0xe6, 0xa9, 0x43, 0xa7, + 0x5d, 0xb1, 0x11, 0xd6, 0x1d, 0x0c, 0x57, 0x62, 0x54, 0x75, 0x18, 0xbb, 0xa6, 0xd5, 0x39, 0x56, 0xaa, 0xc7, 0xde, + 0x67, 0x1d, 0x91, 0xe0, 0x09, 0x05, 0x47, 0x1e, 0x78, 0x86, 0xcf, 0xea, 0xa0, 0xc3, 0xa3, 0x8e, 0xc0, 0xa1, 0xba, + 0x40, 0x5f, 0x1d, 0xc6, 0x40, 0x39, 0x82, 0x50, 0x44, 0xbe, 0x7b, 0xa0, 0x0e, 0xe1, 0x6b, 0x7e, 0x82, 0x99, 0x52, + 0x7a, 0x3e, 0x66, 0x7b, 0xf0, 0xe5, 0x80, 0xfb, 0xfd, 0x17, 0x9e, 0xd2, 0x35, 0x8c, 0xc3, 0x0f, 0xbf, 0xd5, 0x62, + 0xf9, 0xfd, 0x00, 0xf3, 0xed, 0x20, 0xd5, 0x25, 0x1c, 0xe5, 0x2a, 0xc0, 0x1f, 0x6d, 0x19, 0x77, 0x0d, 0x86, 0xf5, + 0x11, 0x14, 0x11, 0x1e, 0x71, 0x30, 0xdc, 0x2c, 0x05, 0x80, 0xe2, 0x3c, 0xac, 0x80, 0xc8, 0x42, 0x34, 0x3f, 0x2f, + 0x97, 0x58, 0x57, 0x65, 0x68, 0x4b, 0x4b, 0x36, 0x8f, 0x13, 0x31, 0x6c, 0x26, 0x49, 0x25, 0x44, 0xaf, 0x88, 0x18, + 0x11, 0x33, 0x43, 0xeb, 0xa5, 0xfd, 0x9e, 0xba, 0x2a, 0x08, 0xa3, 0xd6, 0x6d, 0xb8, 0xd7, 0xf5, 0x55, 0x2f, 0x6a, + 0xb5, 0x5f, 0x6b, 0x65, 0x00, 0xfb, 0x96, 0x7c, 0x80, 0x22, 0x09, 0x5b, 0xda, 0xf1, 0x6f, 0x07, 0x72, 0xd1, 0x3f, + 0x84, 0xb0, 0x89, 0x4d, 0x90, 0x73, 0x78, 0xa9, 0x75, 0xf6, 0x36, 0x10, 0xc2, 0x24, 0xd6, 0x6a, 0x3d, 0x82, 0x17, + 0x4d, 0x00, 0xa9, 0xd0, 0x3e, 0x63, 0xf9, 0x88, 0x54, 0x9c, 0x3f, 0x1f, 0x5a, 0x36, 0xb7, 0x3f, 0xe5, 0x13, 0x2b, + 0x47, 0x9c, 0xad, 0x5f, 0x2c, 0x49, 0x56, 0xf0, 0x5d, 0x22, 0xc1, 0x37, 0x96, 0xa1, 0xfa, 0xb4, 0x0f, 0xa0, 0x49, + 0x21, 0xd0, 0xc1, 0xe5, 0x5d, 0x8d, 0x0c, 0xb5, 0x58, 0x46, 0x75, 0xb4, 0xc7, 0x22, 0xd3, 0x17, 0x2f, 0xca, 0xea, + 0xb3, 0x08, 0x0d, 0x27, 0x16, 0xc3, 0x28, 0x95, 0x5e, 0x6c, 0xd1, 0xc6, 0x9f, 0xf4, 0x3f, 0xe6, 0x06, 0xa5, 0xea, + 0x78, 0x85, 0x5b, 0x35, 0x54, 0x87, 0xae, 0xd0, 0x1b, 0xd9, 0xca, 0xb1, 0x7f, 0x79, 0x67, 0x51, 0xc7, 0x9a, 0x36, + 0x08, 0x5e, 0x07, 0xfd, 0xcd, 0x14, 0x9c, 0xec, 0xfc, 0x9a, 0xe8, 0x14, 0x06, 0x08, 0x0a, 0x66, 0x08, 0xf6, 0x19, + 0xcd, 0xa6, 0xa5, 0x74, 0x67, 0xcd, 0x89, 0x3a, 0x36, 0xce, 0x8c, 0xb2, 0x76, 0x29, 0x9e, 0xda, 0x78, 0xeb, 0x05, + 0x3d, 0xf8, 0x5a, 0xbc, 0x58, 0x71, 0x52, 0x5b, 0x46, 0xc4, 0x0b, 0x8e, 0x87, 0xeb, 0x98, 0x43, 0xb5, 0x71, 0x6b, + 0xc1, 0x84, 0x09, 0xad, 0x86, 0xcd, 0xce, 0x5a, 0x4e, 0xf9, 0x5a, 0x1e, 0x27, 0x95, 0x4b, 0x7f, 0xac, 0x90, 0x00, + 0x08, 0x1d, 0x2d, 0x22, 0x09, 0x7c, 0x56, 0x18, 0xc6, 0x1c, 0x0f, 0x92, 0x25, 0xf3, 0x63, 0x25, 0x8f, 0x00, 0x33, + 0x31, 0x5c, 0xbc, 0x0d, 0xbd, 0x7a, 0x82, 0x2e, 0xd9, 0xc1, 0x46, 0xdd, 0x20, 0x08, 0x12, 0xec, 0x00, 0x7f, 0xe1, + 0x7d, 0x17, 0x26, 0xe7, 0xfc, 0x66, 0xeb, 0xf0, 0xff, 0x04, 0x4f, 0xe6, 0x61, 0x6d, 0xbb, 0x1f, 0x6c, 0xd4, 0x97, + 0xff, 0x3f, 0xd5, 0x35, 0xb4, 0x0e, 0x7c, 0xf8, 0xc0, 0x85, 0xc7, 0xab, 0x55, 0x3d, 0x5a, 0x6d, 0xed, 0x80, 0x21, + 0x99, 0x38, 0x51, 0x56, 0xec, 0xa8, 0xde, 0xa1, 0xee, 0x1f, 0x1c, 0xee, 0x8f, 0x1c, 0xa0, 0xfe, 0xc1, 0xc4, 0xdb, + 0x48, 0x23, 0xdd, 0xfd, 0x22, 0x64, 0x62, 0x3d, 0xea, 0x20, 0x57, 0x29, 0xfd, 0xfc, 0xdc, 0xb3, 0x75, 0x84, 0xa5, + 0xab, 0x74, 0x70, 0x7f, 0xd1, 0x59, 0x7b, 0xb0, 0xc1, 0xe5, 0x8b, 0xec, 0x56, 0xad, 0x7d, 0x52, 0xba, 0xca, 0x1a, + 0x6f, 0x02, 0x10, 0x60, 0xab, 0xcc, 0x64, 0xe5, 0xe9, 0x9e, 0x52, 0xc2, 0xbb, 0xd6, 0xe6, 0xec, 0xaf, 0xd7, 0xc1, + 0x29, 0x63, 0x6d, 0x77, 0x71, 0x6b, 0xbd, 0x00, 0x41, 0x39, 0xf7, 0x1a, 0xca, 0x29, 0x84, 0x78, 0x49, 0x0d, 0x2e, + 0x87, 0xb1, 0xf1, 0x10, 0x39, 0x72, 0x88, 0x6e, 0x23, 0x82, 0x75, 0x95, 0xb6, 0x2a, 0x8e, 0xbd, 0x96, 0x27, 0x66, + 0x0b, 0xe3, 0x26, 0xa6, 0x14, 0x16, 0x15, 0x18, 0x79, 0x1a, 0x76, 0x38, 0xdb, 0x11, 0x7a, 0x34, 0x6b, 0x5b, 0x90, + 0x26, 0xec, 0x97, 0xfa, 0x7d, 0x58, 0x82, 0xb1, 0xf9, 0xaa, 0x85, 0xd0, 0x4b, 0xe0, 0x34, 0x79, 0x6f, 0xc8, 0xaf, + 0x2e, 0xf4, 0x8c, 0x70, 0x59, 0x24, 0xf7, 0x58, 0x08, 0x42, 0x65, 0x6b, 0xbb, 0x4c, 0xba, 0x99, 0x63, 0x88, 0xe7, + 0x19, 0x63, 0x68, 0xe1, 0x05, 0x81, 0x4c, 0x13, 0x94, 0x32, 0xfc, 0x16, 0xe1, 0x71, 0x86, 0xb3, 0x43, 0x6e, 0xa6, + 0xc3, 0x48, 0xb8, 0xc2, 0xed, 0x04, 0x99, 0xa5, 0xf9, 0x44, 0xe9, 0xc7, 0x54, 0x75, 0xd8, 0x67, 0x26, 0x21, 0x6a, + 0x8f, 0x58, 0x8f, 0xa7, 0x34, 0x6c, 0x67, 0xfc, 0x9c, 0xe7, 0x52, 0x6c, 0x20, 0xee, 0xae, 0xdc, 0x25, 0xd7, 0xc4, + 0x29, 0xb1, 0xb4, 0xca, 0x38, 0xa4, 0xd0, 0x8e, 0x85, 0xb6, 0xf1, 0x90, 0x1e, 0x44, 0xda, 0xae, 0x0f, 0x49, 0x95, + 0x4e, 0x1e, 0xf3, 0x23, 0x62, 0xc8, 0x4c, 0xbf, 0xc0, 0xda, 0xfe, 0x72, 0xf3, 0x21, 0x04, 0x2a, 0x12, 0x3b, 0x77, + 0x04, 0x3e, 0x0d, 0xb0, 0x79, 0x29, 0x2d, 0x85, 0x56, 0x85, 0xce, 0x55, 0x5b, 0xbd, 0x34, 0x94, 0x0d, 0x51, 0x04, + 0x92, 0x59, 0x96, 0xf0, 0x51, 0xd6, 0x30, 0xc8, 0xa9, 0xdf, 0x35, 0x20, 0xdb, 0x1e, 0x06, 0xeb, 0x7b, 0xaa, 0x2c, + 0xf5, 0xfd, 0xd9, 0x4f, 0x9f, 0xf0, 0xb1, 0x0e, 0x61, 0x95, 0x01, 0xd7, 0x6c, 0x5e, 0xe3, 0xa1, 0x77, 0x9f, 0xcc, + 0xa0, 0x7e, 0xc2, 0x91, 0xbe, 0xc1, 0xd7, 0xc8, 0xfd, 0xcc, 0xcb, 0xf2, 0xca, 0x7b, 0x49, 0x9e, 0x6d, 0x53, 0xea, + 0x27, 0x2d, 0x56, 0xbc, 0x81, 0x3f, 0x75, 0x7e, 0x68, 0xc5, 0xf7, 0x65, 0x75, 0x67, 0xdb, 0x99, 0x33, 0x2c, 0x33, + 0xd8, 0x83, 0x19, 0xba, 0xeb, 0xa3, 0x56, 0x2a, 0xa4, 0x5e, 0xe9, 0xcb, 0x07, 0xef, 0x53, 0xef, 0x53, 0x26, 0x4d, + 0x74, 0x13, 0x98, 0xa2, 0xd2, 0xd7, 0x21, 0xca, 0x0e, 0x69, 0x62, 0xda, 0xa1, 0x4a, 0x14, 0x1d, 0x5e, 0x98, 0x65, + 0x29, 0xc0, 0xf0, 0x8d, 0xe5, 0x53, 0x86, 0x6b, 0x25, 0xa9, 0x20, 0xd4, 0x1a, 0xc4, 0x67, 0x93, 0xe9, 0x7d, 0x99, + 0x1b, 0x0a, 0x58, 0xb0, 0xf8, 0x3a, 0x86, 0x5d, 0xa4, 0x7f, 0x38, 0x7e, 0x27, 0xc1, 0x39, 0xe1, 0x70, 0x64, 0x03, + 0x01, 0x94, 0x69, 0xbb, 0xe0, 0xe2, 0x7e, 0x83, 0x3d, 0xcc, 0xd2, 0x7f, 0x42, 0x6a, 0xc1, 0x69, 0xa0, 0x97, 0xe8, + 0xff, 0xba, 0x33, 0x7f, 0x2a, 0x5f, 0x2f, 0x2c, 0xe6, 0x44, 0xb8, 0xc5, 0xd9, 0x57, 0x96, 0x59, 0xe5, 0x8a, 0xfb, + 0x03, 0x23, 0x13, 0xad, 0x5d, 0x9f, 0x1f, 0xac, 0x56, 0xd4, 0x2a, 0xd4, 0xd0, 0x57, 0xee, 0x7f, 0xa6, 0x7b, 0xb9, + 0x67, 0xc6, 0x3c, 0x14, 0x73, 0x87, 0x75, 0xd1, 0xd0, 0xf8, 0x0c, 0xd1, 0x10, 0xa5, 0xc6, 0x6a, 0xc0, 0x66, 0x4c, + 0xea, 0xd7, 0x83, 0x08, 0x4b, 0xe9, 0x9c, 0x18, 0x55, 0x6a, 0x91, 0x41, 0x82, 0xc9, 0xf1, 0x5c, 0xda, 0x1c, 0x0a, + 0x44, 0xd0, 0xcc, 0x6b, 0x68, 0xf4, 0x35, 0x1e, 0x56, 0xb8, 0xd1, 0xcd, 0x1e, 0x31, 0x64, 0x04, 0x41, 0x65, 0xd9, + 0x18, 0xd9, 0x2e, 0x46, 0x51, 0x38, 0xf5, 0xb3, 0x43, 0x41, 0xf1, 0xcb, 0x99, 0x2f, 0x4d, 0x76, 0xdc, 0x3d, 0x1a, + 0x80, 0xa2, 0x58, 0x97, 0x78, 0xd9, 0x66, 0x22, 0x37, 0xb9, 0xc1, 0x94, 0x20, 0x88, 0x39, 0xfc, 0x09, 0xb2, 0xa4, + 0x88, 0xe9, 0x22, 0x6e, 0x2e, 0xcd, 0xc5, 0xa8, 0x4c, 0x76, 0xf5, 0xc0, 0x6d, 0x68, 0x54, 0xab, 0x89, 0x5e, 0x6b, + 0xdd, 0x0f, 0x0a, 0xd1, 0x09, 0x8b, 0x27, 0xf2, 0x8a, 0x85, 0x48, 0x82, 0x81, 0x00, 0x45, 0xdb, 0xc2, 0x28, 0x0a, + 0xbd, 0x16, 0xd3, 0xd9, 0x72, 0x7e, 0x2e, 0xd3, 0x50, 0x34, 0x3a, 0xe3, 0x96, 0x81, 0x55, 0x3f, 0x6d, 0x96, 0x1f, + 0x78, 0xfc, 0x4f, 0x62, 0xc2, 0xdb, 0x1e, 0x7a, 0x06, 0xe2, 0x53, 0x0f, 0x28, 0xd3, 0x5d, 0x02, 0x85, 0xe9, 0xb9, + 0x8b, 0x50, 0x22, 0x29, 0xea, 0xc6, 0x9c, 0x58, 0x72, 0x1f, 0x95, 0xf8, 0xbe, 0x1a, 0x1f, 0xc2, 0x11, 0xd5, 0x87, + 0xc4, 0x15, 0x05, 0x2c, 0xf2, 0xac, 0x64, 0xf3, 0x39, 0xcb, 0xb8, 0x0e, 0x8b, 0x35, 0x73, 0xce, 0x1b, 0x5e, 0x96, + 0xfa, 0xa0, 0x64, 0x04, 0xef, 0x07, 0x73, 0x08, 0x55, 0xae, 0xc8, 0x0c, 0xf9, 0x55, 0x89, 0xda, 0x0a, 0x58, 0xe3, + 0x0a, 0xc2, 0x6c, 0xed, 0x15, 0xaf, 0x6f, 0x8b, 0x95, 0xfe, 0x40, 0xae, 0x11, 0xf7, 0x70, 0x08, 0x00, 0x0c, 0xfb, + 0xdd, 0x09, 0x8a, 0x91, 0x0a, 0x17, 0xe6, 0x62, 0x68, 0x40, 0xc2, 0x36, 0x48, 0x99, 0xed, 0xc7, 0xf9, 0xf0, 0xee, + 0x9f, 0xd6, 0x9c, 0x1d, 0xac, 0x95, 0x70, 0xed, 0xe8, 0x2a, 0x13, 0xe4, 0xe5, 0x43, 0x94, 0x9d, 0xb9, 0x6d, 0xae, + 0x96, 0x05, 0x51, 0x69, 0x31, 0x9e, 0xad, 0xc4, 0xfd, 0x32, 0x85, 0xc7, 0xce, 0x22, 0xd8, 0x99, 0x97, 0xe0, 0x12, + 0x10, 0x7d, 0x90, 0xf1, 0x91, 0x0d, 0x12, 0xbd, 0xf2, 0x6c, 0xfc, 0x59, 0x75, 0xef, 0x51, 0x9b, 0x2a, 0x52, 0xbb, + 0x1e, 0xb8, 0x3b, 0x94, 0xa4, 0x82, 0x69, 0x77, 0x03, 0xd6, 0xcc, 0xeb, 0x89, 0xc9, 0x37, 0x0a, 0xe2, 0x06, 0x38, + 0xfb, 0x6e, 0x1c, 0x68, 0x1a, 0x58, 0x6f, 0x3e, 0xa2, 0x68, 0x10, 0x6a, 0x44, 0x9c, 0x9b, 0xf5, 0xfa, 0xa5, 0xdf, + 0x81, 0x00, 0xa4, 0x60, 0x56, 0x12, 0xbc, 0x77, 0xe5, 0xa4, 0x10, 0x84, 0xd8, 0x02, 0x88, 0xe9, 0x06, 0x12, 0xc7, + 0x11, 0xe5, 0x1a, 0xcf, 0xbe, 0x59, 0x7a, 0xf4, 0xa2, 0x23, 0x76, 0x7f, 0x09, 0xac, 0xe9, 0x65, 0x07, 0xdb, 0xb9, + 0x09, 0x49, 0x85, 0x32, 0xf4, 0xaa, 0x9e, 0xdd, 0xb0, 0xb9, 0x75, 0x2c, 0x0b, 0x3d, 0x7a, 0x08, 0x72, 0xc9, 0xbc, + 0xb7, 0xd5, 0x18, 0x08, 0xa5, 0x9b, 0x5f, 0x08, 0x14, 0x47, 0xeb, 0x5f, 0x68, 0x93, 0x0c, 0x6d, 0x9c, 0xdb, 0xb4, + 0xb3, 0x66, 0xb7, 0x29, 0xac, 0x5c, 0x72, 0x73, 0xbd, 0x38, 0xa6, 0xa8, 0xa7, 0xf2, 0xbd, 0xd6, 0xb2, 0x67, 0x63, + 0xa0, 0x86, 0xf6, 0xc8, 0x27, 0x85, 0xd0, 0x1b, 0xb6, 0x62, 0x69, 0x24, 0x93, 0xe6, 0xce, 0x3b, 0x27, 0xd4, 0xe6, + 0x61, 0x87, 0xc4, 0x09, 0x73, 0xeb, 0xbf, 0xcf, 0x23, 0x29, 0x8b, 0xf7, 0x29, 0x14, 0x6e, 0x86, 0xea, 0x86, 0xb1, + 0xe8, 0x38, 0x01, 0xb7, 0x95, 0xf5, 0xd3, 0x8c, 0x44, 0xac, 0x16, 0x16, 0xc6, 0x33, 0x80, 0xa9, 0x98, 0x22, 0x6e, + 0x55, 0x30, 0xd4, 0x20, 0x39, 0x57, 0x83, 0x60, 0xa6, 0xd7, 0x8c, 0x9d, 0x79, 0x99, 0xb7, 0xd0, 0xd6, 0xc6, 0x2c, + 0x2c, 0xd4, 0x6c, 0x4c, 0xcd, 0x93, 0x49, 0x01, 0x4b, 0x23, 0xe8, 0xf6, 0x98, 0x1e, 0xee, 0xae, 0x91, 0xef, 0x96, + 0x23, 0x67, 0x17, 0x83, 0xf9, 0xd8, 0xcb, 0xec, 0x71, 0xea, 0xc1, 0xcb, 0x04, 0x33, 0x42, 0x85, 0xad, 0xe2, 0x02, + 0xda, 0xb3, 0xa6, 0xff, 0xc0, 0x37, 0xf1, 0x31, 0x07, 0x37, 0x66, 0xec, 0xad, 0x59, 0xba, 0xe2, 0x3d, 0x1d, 0x23, + 0x64, 0x11, 0x23, 0xf2, 0x9c, 0x35, 0xc5, 0xdc, 0x4a, 0x15, 0xe3, 0x1e, 0x14, 0x82, 0xe5, 0x2b, 0x4c, 0x05, 0x10, + 0x0e, 0x66, 0x37, 0x1a, 0x1c, 0x62, 0x7d, 0xdc, 0xba, 0x5b, 0x20, 0x04, 0x06, 0x50, 0x5d, 0x9c, 0x73, 0x34, 0xd1, + 0x01, 0x90, 0xfc, 0x3e, 0x12, 0x00, 0x49, 0x60, 0x86, 0x22, 0x01, 0x46, 0xaf, 0x5a, 0xfa, 0x9a, 0x17, 0x6b, 0x8c, + 0x8c, 0xd8, 0x23, 0x08, 0xb6, 0x72, 0x8f, 0x2c, 0x90, 0x66, 0x73, 0xaf, 0x75, 0xc2, 0xb7, 0x67, 0x45, 0x25, 0x0e, + 0x2e, 0xbf, 0x2a, 0x23, 0xe9, 0x9f, 0x0c, 0x6a, 0xac, 0x63, 0xe5, 0x29, 0xfd, 0x98, 0xa9, 0xa3, 0x47, 0x77, 0x69, + 0x9a, 0x4e, 0xe6, 0xa0, 0xd8, 0x43, 0x36, 0x28, 0xab, 0x64, 0xec, 0xc4, 0x39, 0x74, 0x22, 0xa9, 0x7f, 0x1c, 0xbd, + 0xbc, 0x53, 0x8f, 0xa2, 0x34, 0xb7, 0xeb, 0x09, 0xb5, 0x72, 0xaa, 0xdd, 0x08, 0xbc, 0x49, 0x79, 0x26, 0x75, 0xc6, + 0x96, 0xfa, 0xa5, 0x42, 0x2a, 0x3b, 0x35, 0x26, 0xb1, 0x93, 0xf3, 0x32, 0xe7, 0xe8, 0x29, 0xbf, 0x10, 0xc6, 0x81, + 0xb1, 0x3f, 0x9d, 0xb6, 0xde, 0xaf, 0xd9, 0x19, 0xe2, 0xf1, 0x6a, 0xaa, 0xf6, 0x21, 0x5d, 0xab, 0x26, 0xa6, 0x40, + 0xd3, 0x9e, 0xa6, 0xff, 0xc9, 0x80, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0x27, 0x21, 0xd5, 0x0e, 0xa2, 0xfd, 0x41, 0x0b, + 0xaf, 0xf0, 0x35, 0x4a, 0xa8, 0xfe, 0xce, 0x09, 0xd0, 0xf1, 0xbd, 0x6e, 0x10, 0x5b, 0x92, 0xb8, 0x98, 0x8b, 0x54, + 0x76, 0x8e, 0x19, 0xd5, 0x40, 0x2e, 0x88, 0x14, 0xcf, 0x75, 0x1a, 0x95, 0x85, 0x2c, 0x79, 0x83, 0x1b, 0x3f, 0xfb, + 0x35, 0x53, 0x28, 0xfc, 0x6a, 0x38, 0x08, 0x58, 0x06, 0x90, 0x30, 0xef, 0x5e, 0x69, 0xce, 0x99, 0x9d, 0x8d, 0x18, + 0xb2, 0x00, 0x2e, 0x75, 0xec, 0x23, 0x74, 0x12, 0x00, 0x10, 0x1d, 0x13, 0x63, 0x20, 0xaf, 0x76, 0x54, 0xff, 0x03, + 0x1c, 0x7a, 0x27, 0xbd, 0x5a, 0x73, 0x37, 0x81, 0x28, 0x42, 0x40, 0x80, 0xc4, 0xde, 0x50, 0x10, 0x45, 0xcb, 0x41, + 0x24, 0x55, 0x62, 0x27, 0xb4, 0x15, 0x9a, 0x05, 0x37, 0xb2, 0x11, 0x69, 0x04, 0xd0, 0x2b, 0xb8, 0x10, 0x33, 0x02, + 0x65, 0x1e, 0x47, 0x1a, 0xbf, 0xa0, 0xc4, 0xcc, 0x4b, 0xc5, 0xe8, 0x73, 0x8a, 0x7a, 0xef, 0x41, 0x74, 0xcf, 0xcd, + 0xa3, 0xd6, 0xc7, 0x84, 0x10, 0x3d, 0x01, 0x6b, 0x28, 0xab, 0x9f, 0xa2, 0x0c, 0x30, 0x1a, 0xa0, 0x6c, 0xef, 0x70, + 0x1e, 0xb0, 0x7c, 0xa9, 0x79, 0x52, 0x0f, 0x1d, 0xf3, 0x88, 0x5c, 0x3a, 0x9f, 0xf7, 0xeb, 0xb8, 0x5e, 0xd4, 0x0e, + 0x2a, 0x04, 0x3c, 0x56, 0x2f, 0xd5, 0x8d, 0x20, 0x37, 0x14, 0xff, 0x45, 0xc5, 0xd4, 0x18, 0xf6, 0x95, 0x5f, 0x4c, + 0x27, 0x1d, 0x6a, 0x87, 0xf5, 0x2e, 0x72, 0x75, 0x2a, 0x4a, 0x00, 0x4e, 0xbb, 0x1d, 0xda, 0x39, 0xb3, 0xe3, 0x6f, + 0x77, 0xfd, 0x68, 0x95, 0x95, 0x6a, 0x51, 0xe7, 0x59, 0x43, 0x41, 0x79, 0x39, 0x1d, 0xff, 0x0b, 0x4f, 0xf6, 0xf2, + 0x64, 0x30, 0xa7, 0x16, 0x61, 0x9c, 0xba, 0xf3, 0xec, 0xfd, 0xc1, 0xdb, 0x61, 0x4b, 0x88, 0x9d, 0xea, 0xe6, 0xd7, + 0x7a, 0xe4, 0x8b, 0xa9, 0xdb, 0x7a, 0x82, 0x1b, 0x37, 0xb7, 0xce, 0xd8, 0xab, 0xc7, 0xd0, 0x31, 0x01, 0xe0, 0xad, + 0x25, 0x8a, 0xa2, 0x22, 0xfc, 0xfb, 0xe3, 0xe9, 0xa1, 0xe6, 0xf4, 0xa0, 0x6f, 0xe3, 0x9d, 0x18, 0x34, 0x05, 0x26, + 0x58, 0x07, 0x0c, 0xf3, 0x01, 0xfd, 0xae, 0xa4, 0x1a, 0xdf, 0xf7, 0xa2, 0xc8, 0x41, 0x4c, 0x66, 0xf0, 0xa5, 0xf1, + 0x4d, 0x56, 0x64, 0x7c, 0x51, 0x01, 0xda, 0xbc, 0x4c, 0xb6, 0x0b, 0xc7, 0x30, 0x85, 0x69, 0xb7, 0xee, 0x7b, 0xec, + 0xa0, 0x5c, 0xdc, 0x1a, 0xc6, 0x6f, 0x24, 0x82, 0xec, 0x8d, 0x65, 0xe6, 0x03, 0xc5, 0xaf, 0x9f, 0x3a, 0x26, 0xb9, + 0xe7, 0x0d, 0xf7, 0x87, 0xa8, 0xa9, 0xd0, 0xf9, 0x5c, 0x79, 0xb7, 0x9c, 0xdf, 0x85, 0xd7, 0xaa, 0x50, 0x77, 0x64, + 0xc9, 0x48, 0xd3, 0x69, 0xe8, 0xe9, 0x5a, 0x2d, 0xda, 0x9c, 0xb8, 0x0e, 0xda, 0xb6, 0xd8, 0x04, 0x12, 0x4b, 0xce, + 0x60, 0xa6, 0xe4, 0x4d, 0x2c, 0x08, 0x0e, 0x1d, 0x41, 0xa1, 0xbb, 0x28, 0x0e, 0x91, 0x30, 0x60, 0xb3, 0x43, 0x85, + 0xdd, 0x47, 0xb0, 0xf1, 0xd5, 0xbc, 0x50, 0xe4, 0x19, 0xab, 0xc2, 0x9c, 0xa9, 0x66, 0x56, 0xb5, 0x5f, 0x0c, 0x70, + 0xf6, 0x4f, 0xb8, 0x97, 0x4e, 0x0c, 0xd6, 0x83, 0x4c, 0x49, 0x0d, 0x9d, 0x72, 0x8a, 0x6a, 0x1e, 0xb1, 0x8d, 0x35, + 0x14, 0x50, 0x3b, 0x3c, 0x32, 0x1c, 0x6e, 0x7a, 0x6f, 0x7c, 0x3e, 0xf0, 0x2c, 0x36, 0xf0, 0xce, 0x21, 0xb0, 0x10, + 0xfb, 0x51, 0xbb, 0x38, 0xb4, 0x35, 0x9b, 0xa1, 0xb0, 0x8d, 0x86, 0x42, 0x3a, 0xb3, 0x17, 0x24, 0xd3, 0x41, 0x1a, + 0x48, 0x5b, 0x87, 0x89, 0xc6, 0xde, 0x57, 0x07, 0xba, 0x03, 0x8d, 0x37, 0x3d, 0xa2, 0xd0, 0xc6, 0xae, 0x1a, 0xc0, + 0x2b, 0x3a, 0x97, 0xe8, 0x66, 0xdf, 0x12, 0xd8, 0xaf, 0x36, 0x83, 0x1b, 0xcb, 0x97, 0x00, 0xa6, 0x14, 0x90, 0x6d, + 0xd9, 0xf8, 0xdc, 0x73, 0x3e, 0x90, 0x4d, 0x98, 0x1c, 0x8d, 0xa3, 0x76, 0x11, 0x76, 0x69, 0x8d, 0xb7, 0x1c, 0x4d, + 0xf5, 0xcf, 0xa5, 0x08, 0x0b, 0xac, 0x2e, 0x98, 0xb6, 0xc5, 0x47, 0x23, 0xac, 0x49, 0x51, 0xb7, 0x87, 0x05, 0xd4, + 0xd8, 0x61, 0x21, 0x95, 0xd6, 0x6b, 0x89, 0x8b, 0x95, 0x18, 0x5f, 0xd3, 0x53, 0x28, 0x0e, 0x2c, 0x3b, 0x47, 0x40, + 0xe3, 0xc1, 0x3a, 0xdf, 0x0a, 0x5f, 0x2a, 0xdc, 0x2f, 0xe5, 0xa8, 0xe4, 0x99, 0x26, 0xce, 0xf5, 0x02, 0xce, 0x08, + 0x89, 0xcb, 0x0f, 0xd8, 0x38, 0x96, 0x00, 0x5b, 0xa6, 0xf7, 0xff, 0x50, 0x87, 0x05, 0xdb, 0x21, 0xc0, 0x2b, 0xee, + 0xa2, 0x7d, 0xa0, 0x5c, 0x01, 0xa4, 0xc9, 0x51, 0x86, 0x5c, 0x7e, 0xd6, 0xbd, 0x42, 0xc6, 0xb4, 0x4f, 0xa2, 0x63, + 0xcb, 0x76, 0x76, 0x20, 0x99, 0x23, 0x43, 0xc2, 0xb8, 0xf5, 0x2a, 0x65, 0xe1, 0x6e, 0x80, 0x63, 0x44, 0xb1, 0xb4, + 0x62, 0x7e, 0x99, 0x29, 0xf2, 0x0a, 0xd8, 0x8d, 0x93, 0xa6, 0xbd, 0x36, 0xa6, 0x4b, 0x64, 0x63, 0x30, 0x76, 0xaa, + 0x08, 0x2c, 0x8c, 0x41, 0x55, 0xb2, 0x20, 0xfc, 0x63, 0x4f, 0x38, 0xe0, 0x7f, 0x72, 0x26, 0x28, 0x3f, 0x1e, 0xcb, + 0x79, 0x52, 0x61, 0x1e, 0xc8, 0x14, 0xf6, 0x70, 0xfa, 0xd2, 0xbf, 0xa2, 0x4f, 0xa9, 0xc0, 0x9a, 0x24, 0xc2, 0xb8, + 0x01, 0x06, 0x55, 0x1b, 0x00, 0x74, 0x83, 0x14, 0x6f, 0x12, 0xd0, 0xe4, 0x26, 0xb4, 0x0a, 0xe5, 0x28, 0x1d, 0xb0, + 0x52, 0xe4, 0x66, 0xd4, 0x14, 0xc1, 0x46, 0xda, 0x41, 0x8a, 0x12, 0x0c, 0x3b, 0xa9, 0x06, 0x69, 0x95, 0x7a, 0x38, + 0x08, 0x7f, 0x4d, 0x80, 0x0e, 0x40, 0x1e, 0x96, 0xff, 0x65, 0x32, 0xc2, 0xcb, 0x23, 0x2b, 0xb9, 0x47, 0xcd, 0x51, + 0x63, 0x62, 0x1a, 0x3a, 0xb9, 0xa5, 0x13, 0xde, 0xd5, 0x1c, 0x71, 0x36, 0x0e, 0x6a, 0xab, 0x9a, 0x0f, 0x06, 0x43, + 0xa7, 0x4e, 0x3a, 0x82, 0xc2, 0x47, 0x39, 0x06, 0x13, 0xda, 0xcd, 0x14, 0x3d, 0x0f, 0x7b, 0x99, 0x97, 0x93, 0x6e, + 0x36, 0x53, 0x00, 0xb6, 0x5a, 0x0a, 0x5b, 0x86, 0x81, 0x31, 0x8c, 0x3f, 0x02, 0x72, 0xc3, 0xa7, 0xcf, 0x4b, 0x23, + 0x8f, 0x4a, 0x2f, 0x6f, 0x7e, 0xf8, 0xf8, 0x31, 0x80, 0xc1, 0x50, 0xd1, 0xe0, 0xc3, 0x67, 0x7d, 0x35, 0xbe, 0x93, + 0xc9, 0xc6, 0x1a, 0xe3, 0x1c, 0x44, 0x79, 0x16, 0xda, 0x91, 0x9f, 0x95, 0x75, 0x51, 0x0c, 0xdb, 0xd7, 0x17, 0x95, + 0xd7, 0x97, 0x22, 0xa4, 0x6a, 0x41, 0x2d, 0x6f, 0x71, 0x8a, 0x8d, 0x43, 0x28, 0x34, 0xe6, 0xa0, 0x08, 0x19, 0x8e, + 0x22, 0x6e, 0xee, 0x34, 0x14, 0x03, 0x52, 0x22, 0x12, 0xe6, 0xd4, 0x69, 0xed, 0x6d, 0x4e, 0xb0, 0xd9, 0x3b, 0x53, + 0x4b, 0x0d, 0xaf, 0x31, 0x61, 0x65, 0xe7, 0x48, 0x91, 0xc0, 0xa5, 0x2d, 0xbb, 0xb5, 0xc8, 0x54, 0xdd, 0x8d, 0x86, + 0xcc, 0x99, 0x15, 0x14, 0xab, 0x97, 0xcf, 0x0a, 0x87, 0x56, 0x50, 0xfc, 0xa6, 0xc1, 0x06, 0xc4, 0x38, 0x76, 0x7f, + 0x7c, 0xae, 0x82, 0x7f, 0xf8, 0x39, 0xdc, 0x93, 0x3f, 0xa6, 0x17, 0xac, 0x52, 0xcc, 0x06, 0x35, 0xdf, 0x25, 0xca, + 0xf4, 0xda, 0x9c, 0x09, 0x4c, 0x1c, 0xf3, 0x82, 0x1e, 0x3e, 0x4b, 0x5f, 0x14, 0xa3, 0xcd, 0xa0, 0x22, 0x65, 0x52, + 0x01, 0x44, 0x34, 0xe9, 0x0e, 0xa9, 0xd7, 0x58, 0xa4, 0x2c, 0x9b, 0x62, 0x9b, 0xe6, 0x4a, 0x6d, 0x1f, 0x3b, 0x6a, + 0x6a, 0xdd, 0x90, 0x48, 0x3c, 0xa4, 0xf9, 0x0f, 0xc5, 0xd7, 0x31, 0x16, 0x84, 0x48, 0x21, 0xad, 0x2f, 0xce, 0x74, + 0x7a, 0x7c, 0x36, 0x8a, 0x3b, 0x1a, 0xc7, 0xa3, 0xfc, 0x6b, 0x8d, 0xe9, 0x54, 0xb0, 0x1f, 0xd2, 0x19, 0x12, 0x47, + 0x9e, 0x1b, 0x17, 0xdd, 0xad, 0xcf, 0x3a, 0x7c, 0x10, 0xb5, 0xbc, 0xe4, 0x1d, 0xbb, 0x21, 0x54, 0x32, 0x98, 0xeb, + 0x94, 0x40, 0x6b, 0xea, 0x0f, 0xfc, 0x0d, 0x5f, 0xb8, 0x51, 0x5d, 0x3a, 0x24, 0x5a, 0xd8, 0x90, 0x60, 0x3a, 0x49, + 0x8a, 0xb4, 0xe0, 0x12, 0x26, 0x9b, 0xa0, 0x58, 0xbb, 0xb0, 0xe0, 0xb3, 0xb0, 0x38, 0x64, 0xf3, 0x8b, 0x2e, 0xc4, + 0x78, 0x07, 0xd6, 0x19, 0xaa, 0x3c, 0x73, 0x8e, 0x41, 0x33, 0x78, 0x61, 0x61, 0xaf, 0x0a, 0x72, 0x88, 0x0b, 0xeb, + 0x80, 0xca, 0xc7, 0xa8, 0x91, 0xf1, 0xe8, 0xad, 0x4d, 0x21, 0x3d, 0xd0, 0x7d, 0xff, 0xce, 0x6f, 0xaf, 0x22, 0x67, + 0x60, 0x88, 0x0b, 0x91, 0x17, 0x1e, 0xcd, 0x6c, 0x70, 0x81, 0x71, 0xe1, 0x6d, 0x8e, 0x7a, 0xf1, 0x1c, 0xce, 0xdb, + 0x37, 0x54, 0x6a, 0x78, 0xc5, 0x94, 0x8e, 0x13, 0x14, 0xdc, 0xa4, 0x97, 0x15, 0xc8, 0xbb, 0x93, 0xb4, 0xd8, 0x35, + 0xbb, 0x14, 0xb2, 0x21, 0x45, 0x67, 0xed, 0x6e, 0x70, 0xca, 0xdc, 0x9e, 0x49, 0x87, 0x65, 0x4e, 0xd1, 0xcc, 0x24, + 0x0a, 0x3d, 0x87, 0x28, 0xc6, 0x8c, 0xa1, 0x49, 0xb5, 0x65, 0x5e, 0xd4, 0x92, 0x0d, 0x95, 0xba, 0x46, 0x50, 0xd6, + 0xcd, 0x91, 0xfd, 0x73, 0xe6, 0xe3, 0xdc, 0xb9, 0xc1, 0xd0, 0x03, 0x79, 0x18, 0x90, 0xb1, 0x3a, 0xc7, 0xfd, 0x85, + 0x8f, 0x69, 0xde, 0xed, 0x5e, 0x73, 0xfc, 0x6b, 0x04, 0x6d, 0x99, 0x7c, 0xd3, 0xfa, 0x07, 0xd5, 0xff, 0x65, 0x03, + 0x46, 0xd6, 0x3a, 0x3e, 0x2c, 0x36, 0xad, 0x37, 0x55, 0x4d, 0x76, 0xd0, 0xd8, 0x99, 0x26, 0x6d, 0x2c, 0x1c, 0xd4, + 0xdd, 0xdb, 0xc8, 0x24, 0x38, 0x6c, 0xde, 0x1c, 0xc2, 0x40, 0x56, 0xc6, 0x77, 0x1b, 0xa1, 0x75, 0xeb, 0xb4, 0xa9, + 0xc3, 0x2f, 0x43, 0x13, 0x0f, 0x7b, 0x8d, 0x56, 0x0c, 0x61, 0x9e, 0x4d, 0x19, 0xbb, 0x02, 0xde, 0x14, 0x41, 0x11, + 0x4f, 0xe3, 0x9a, 0x23, 0x98, 0xd2, 0x6a, 0x60, 0xc8, 0xaa, 0x11, 0xcf, 0x51, 0xa5, 0x6b, 0xd4, 0x73, 0xfb, 0xa6, + 0x07, 0x0c, 0xb9, 0x90, 0xb3, 0x5f, 0x4e, 0x6b, 0x42, 0x67, 0xeb, 0x76, 0xf4, 0x18, 0xf0, 0x0a, 0x91, 0xe8, 0xf3, + 0x41, 0x96, 0x01, 0xb1, 0xc5, 0x64, 0xa5, 0x43, 0x21, 0xc1, 0xe3, 0x76, 0xf8, 0x8c, 0xd5, 0xa7, 0xbc, 0xa7, 0x4f, + 0x59, 0xec, 0x86, 0xa6, 0xd6, 0xc1, 0xdf, 0xa9, 0x12, 0x22, 0x05, 0xae, 0xa5, 0xba, 0xcb, 0x90, 0x55, 0xa5, 0x1e, + 0xfd, 0x41, 0x91, 0x96, 0x46, 0xac, 0xc4, 0x52, 0xa9, 0x1a, 0xd3, 0xfa, 0xef, 0xb4, 0x15, 0x9d, 0xa8, 0xff, 0xb4, + 0xb0, 0x5a, 0x7d, 0x45, 0xac, 0xab, 0x84, 0xe3, 0x45, 0xaf, 0xb6, 0xe8, 0xc8, 0x10, 0x41, 0x02, 0x9f, 0x75, 0xad, + 0x37, 0x1b, 0x3a, 0x0d, 0x68, 0xbc, 0xcd, 0xe3, 0xbf, 0xea, 0xf9, 0x8c, 0xec, 0x05, 0x9a, 0xae, 0x52, 0x9b, 0x02, + 0x5d, 0x41, 0xe0, 0x9c, 0x25, 0xd6, 0x5c, 0xa5, 0x81, 0x09, 0xa6, 0x79, 0xb1, 0xe5, 0x0b, 0xa8, 0x4e, 0xb7, 0x40, + 0x1a, 0x08, 0xd2, 0x50, 0x7a, 0xa9, 0x55, 0xea, 0x36, 0xa6, 0xd3, 0x41, 0x79, 0xd6, 0x06, 0xa5, 0xf8, 0x01, 0xe5, + 0xc0, 0x95, 0x5b, 0xbd, 0x44, 0x09, 0xb2, 0xea, 0xd0, 0xbc, 0x95, 0xbd, 0x66, 0x1e, 0x52, 0xb8, 0x61, 0x4d, 0xc6, + 0x05, 0x6f, 0xfb, 0xdb, 0xdd, 0x40, 0xe6, 0x28, 0x5a, 0x47, 0x4d, 0xc9, 0x42, 0xf7, 0x88, 0xab, 0x08, 0xe9, 0xe7, + 0x85, 0xd2, 0x2d, 0xb0, 0xd3, 0xed, 0xef, 0xd0, 0xba, 0x5b, 0xd4, 0xc5, 0xf2, 0xd0, 0xc3, 0xce, 0x91, 0x7f, 0x04, + 0xef, 0x8f, 0x02, 0x16, 0xc3, 0x4f, 0x13, 0x65, 0x07, 0x6d, 0xf6, 0xfa, 0x7e, 0xb0, 0x4d, 0xbf, 0xa9, 0xb1, 0x1b, + 0xbc, 0x1d, 0xf0, 0x57, 0x1d, 0xac, 0xc2, 0x61, 0x0f, 0xcc, 0x27, 0x16, 0xbf, 0x3f, 0xbf, 0xd8, 0xd7, 0xe0, 0xf8, + 0x44, 0xb8, 0xcd, 0x54, 0x3e, 0xc0, 0xdb, 0x24, 0xb7, 0x74, 0xbd, 0x30, 0xf2, 0xea, 0x02, 0x52, 0x56, 0xce, 0x89, + 0xeb, 0x8b, 0x02, 0x57, 0xa0, 0x85, 0x12, 0x8f, 0x6e, 0x0b, 0xde, 0xb3, 0x86, 0xb4, 0x77, 0x1b, 0x63, 0xd3, 0x69, + 0xef, 0x38, 0x15, 0x87, 0x99, 0x2c, 0xcd, 0x26, 0xe4, 0xdf, 0xae, 0xa8, 0x53, 0x35, 0x70, 0x9f, 0x5f, 0xd7, 0x57, + 0xb3, 0x74, 0x37, 0xee, 0xe7, 0x4f, 0xd9, 0x9e, 0xb0, 0x95, 0x0d, 0x63, 0x76, 0xc8, 0xef, 0x0b, 0x0d, 0xe8, 0x7a, + 0x34, 0x8b, 0x90, 0xfd, 0x40, 0x80, 0xc1, 0x57, 0xe7, 0x8c, 0xa5, 0x59, 0xd9, 0x37, 0xfd, 0xc2, 0x43, 0x49, 0x41, + 0x8c, 0xca, 0x5e, 0x03, 0x64, 0xb4, 0x24, 0x5b, 0xa7, 0xc5, 0x7b, 0x61, 0x01, 0xa3, 0xef, 0x53, 0xe8, 0x54, 0x9f, + 0x64, 0x08, 0xab, 0x2e, 0xd1, 0x78, 0x4e, 0x7b, 0xc4, 0xd7, 0x79, 0x3e, 0x7c, 0x1d, 0x8b, 0x3d, 0x57, 0x58, 0x66, + 0xd8, 0x4c, 0x8c, 0x43, 0x03, 0xf1, 0xc4, 0xa1, 0xfb, 0x91, 0xd1, 0x62, 0xdc, 0x5a, 0x50, 0xc3, 0xe3, 0x2f, 0xe7, + 0x89, 0xa5, 0xeb, 0xdb, 0x80, 0x0c, 0x53, 0x44, 0x9c, 0x59, 0xd4, 0xeb, 0x8b, 0x6a, 0xd8, 0xbd, 0x2e, 0x2a, 0x08, + 0x9a, 0xcd, 0xb7, 0xf5, 0xe0, 0x06, 0xdb, 0x45, 0xed, 0x87, 0x2c, 0xd1, 0xda, 0xba, 0xdf, 0xb4, 0x1b, 0x64, 0x9f, + 0x33, 0x0e, 0xda, 0x40, 0xc0, 0xf8, 0xde, 0xbf, 0xb4, 0xd5, 0xd9, 0xde, 0x3a, 0xcd, 0x5f, 0x88, 0x8b, 0xbf, 0x93, + 0xb0, 0xbc, 0x9b, 0xe1, 0xa0, 0x94, 0x90, 0xe1, 0x04, 0x11, 0xa6, 0xc2, 0xba, 0xb8, 0xe4, 0xb3, 0x0b, 0x13, 0x2b, + 0x23, 0xac, 0x88, 0x76, 0xc4, 0x37, 0x7c, 0x9f, 0xb7, 0x05, 0x04, 0xb1, 0xb3, 0x65, 0xcc, 0x78, 0x46, 0x44, 0x89, + 0x8c, 0xec, 0x30, 0xb1, 0x69, 0x36, 0x61, 0xea, 0xd8, 0x6d, 0x32, 0x68, 0xea, 0x60, 0x9c, 0xc2, 0x06, 0xba, 0x1b, + 0xaa, 0xad, 0xb6, 0x92, 0x8c, 0xf9, 0x85, 0xac, 0x9d, 0xed, 0x8f, 0xea, 0x65, 0x5e, 0x6c, 0x3f, 0xdb, 0x86, 0xe6, + 0x55, 0x31, 0x86, 0x76, 0x20, 0xb3, 0x23, 0xdc, 0x65, 0xea, 0x1e, 0xd2, 0x78, 0xa2, 0x50, 0x6d, 0x25, 0x08, 0x07, + 0xa0, 0x90, 0xa6, 0x39, 0xe3, 0x02, 0xb3, 0xe8, 0xa3, 0x2a, 0xbc, 0xd3, 0x41, 0x59, 0x2d, 0x0d, 0xa0, 0x04, 0x88, + 0xe3, 0x4e, 0x3a, 0x8c, 0xe4, 0xc1, 0x5e, 0xa9, 0x3b, 0xb5, 0x6a, 0x9d, 0xb9, 0x58, 0xec, 0x47, 0x97, 0x5a, 0xec, + 0x11, 0xa6, 0x59, 0x52, 0xeb, 0x07, 0x5a, 0x68, 0xcf, 0x37, 0x5b, 0x13, 0xf3, 0x94, 0x71, 0x48, 0x7b, 0x68, 0x3f, + 0x14, 0xd4, 0x7a, 0x09, 0x8f, 0x95, 0x41, 0x89, 0x64, 0xa7, 0xa6, 0x9d, 0x95, 0x69, 0x0a, 0xde, 0x65, 0x99, 0x78, + 0x15, 0xfa, 0x1d, 0xe1, 0xdd, 0x96, 0x59, 0xdb, 0xc8, 0xc4, 0xcd, 0x49, 0xa4, 0x58, 0x0e, 0xce, 0x35, 0xbc, 0x2d, + 0x3a, 0x76, 0xf1, 0xdb, 0x4c, 0xad, 0x5f, 0xb0, 0x8c, 0x38, 0x5a, 0xc2, 0xcd, 0x0f, 0xe9, 0x91, 0x88, 0x02, 0xa3, + 0xfe, 0x4d, 0x5e, 0xc5, 0x89, 0x1b, 0x66, 0xfc, 0x8e, 0x86, 0x38, 0x54, 0x87, 0xba, 0x67, 0x89, 0x15, 0x88, 0x85, + 0xf3, 0xf7, 0xd5, 0x4d, 0x20, 0x97, 0xde, 0x56, 0xab, 0xe6, 0x61, 0xd4, 0x65, 0xdf, 0x83, 0x3f, 0x85, 0x31, 0x35, + 0x9f, 0xbc, 0x2a, 0xdf, 0x70, 0xcf, 0x64, 0x29, 0x97, 0xf0, 0xba, 0xde, 0x52, 0x73, 0x9c, 0xcb, 0x37, 0x5c, 0x56, + 0x59, 0x6a, 0x33, 0x82, 0x49, 0xdf, 0x5a, 0xe1, 0x38, 0x82, 0x35, 0x14, 0x91, 0xb8, 0x39, 0xa8, 0x83, 0xcd, 0xb1, + 0x0e, 0xe5, 0x36, 0x13, 0xac, 0x43, 0x36, 0xd8, 0x03, 0xc2, 0xa9, 0xc5, 0x66, 0x8e, 0x7d, 0x6c, 0x08, 0x07, 0x74, + 0xdc, 0x9a, 0x22, 0x4a, 0x4e, 0xdd, 0x89, 0xa7, 0x96, 0xe5, 0xbb, 0x19, 0xd3, 0x74, 0x7c, 0x87, 0x18, 0x59, 0x12, + 0x8b, 0xdc, 0xcb, 0x10, 0x97, 0x43, 0x8b, 0xf6, 0x2c, 0x55, 0x21, 0x37, 0xe8, 0xd6, 0x2d, 0x37, 0x1c, 0x3e, 0x7d, + 0x38, 0x2e, 0x7a, 0x22, 0xda, 0x4a, 0x7c, 0x39, 0x85, 0x54, 0x4a, 0xf6, 0x93, 0x0a, 0x2f, 0x54, 0x48, 0xa7, 0xcf, + 0x8a, 0x04, 0xdc, 0xdc, 0x99, 0x0b, 0x57, 0x53, 0xae, 0x65, 0x8d, 0x76, 0x93, 0x9c, 0x08, 0x15, 0x96, 0xcc, 0x18, + 0xbd, 0x78, 0x3f, 0x8b, 0x16, 0xdb, 0x39, 0xc5, 0x76, 0xd8, 0xd4, 0xd4, 0x79, 0x87, 0x22, 0xa7, 0xa1, 0xb2, 0x8c, + 0xc4, 0x2c, 0xca, 0x21, 0x3e, 0x3a, 0x75, 0xbe, 0xc7, 0x28, 0x75, 0x05, 0x73, 0xaa, 0x4d, 0xc9, 0x83, 0xd3, 0xc5, + 0xf9, 0x17, 0x6e, 0x37, 0xfd, 0xe3, 0x28, 0xe8, 0xb7, 0x5a, 0x35, 0x51, 0xad, 0x1c, 0x9a, 0x5d, 0xd7, 0x6e, 0x34, + 0x26, 0xc7, 0x0e, 0x70, 0x67, 0x13, 0xc4, 0x18, 0x3e, 0x2e, 0xc3, 0x2c, 0x9a, 0xe7, 0x53, 0x7d, 0xfd, 0xf3, 0x20, + 0xca, 0xb6, 0x4c, 0xdd, 0x0a, 0xa3, 0xc0, 0xa5, 0x81, 0x07, 0xe3, 0xa8, 0x21, 0x86, 0xcd, 0xa3, 0xa3, 0x6d, 0x22, + 0x93, 0x74, 0xcf, 0x6a, 0xae, 0x00, 0x4d, 0xa7, 0x20, 0xf2, 0xef, 0x71, 0x9b, 0x2f, 0x38, 0x8e, 0x4e, 0xe9, 0xf9, + 0x17, 0xa5, 0x1f, 0x69, 0xf0, 0xc6, 0x78, 0x75, 0xc1, 0xaa, 0x27, 0x23, 0xef, 0x88, 0x87, 0x39, 0x0a, 0xe4, 0x07, + 0xf0, 0x4d, 0xe9, 0x82, 0x73, 0x63, 0xf7, 0x3d, 0xc8, 0x96, 0xed, 0x68, 0x55, 0xc4, 0x1a, 0xf9, 0x1a, 0x27, 0x2e, + 0xb5, 0xfe, 0x92, 0xcc, 0x3a, 0x09, 0xf6, 0x35, 0xf2, 0x78, 0x47, 0xbc, 0xcf, 0xf6, 0x76, 0x68, 0xe3, 0x35, 0x92, + 0xb0, 0xef, 0xb6, 0xeb, 0xaa, 0x2b, 0x4e, 0x7f, 0x62, 0x01, 0xe1, 0x02, 0x36, 0x16, 0xe8, 0x9b, 0xdb, 0x86, 0xd2, + 0xb9, 0xee, 0x1a, 0x7c, 0xaf, 0xf1, 0xce, 0x3b, 0x3b, 0xb8, 0x8a, 0x93, 0x44, 0x60, 0x76, 0xa1, 0x34, 0x26, 0xea, + 0x17, 0x86, 0xe7, 0x6a, 0xcf, 0xb7, 0xea, 0x65, 0x54, 0xb3, 0x67, 0x02, 0xfe, 0x3a, 0x1a, 0x1f, 0x95, 0x79, 0x83, + 0x85, 0xdb, 0x3a, 0x85, 0xee, 0xfc, 0x7d, 0x00, 0xaf, 0xbc, 0x6b, 0xb6, 0x2c, 0xe6, 0x3b, 0xce, 0x82, 0xa5, 0xcd, + 0xdb, 0x5d, 0x99, 0xe0, 0x97, 0x1f, 0x70, 0xaf, 0xf8, 0xd2, 0xc1, 0x93, 0x7e, 0xdd, 0x52, 0xc0, 0x5d, 0xc0, 0xe4, + 0xa5, 0x1b, 0x6e, 0x42, 0xdc, 0xac, 0x72, 0xd3, 0xb7, 0x48, 0xf9, 0xe9, 0xe9, 0xac, 0x79, 0xea, 0x10, 0xde, 0x78, + 0x54, 0x87, 0xca, 0x68, 0x6e, 0xdd, 0xc2, 0x95, 0xfe, 0x05, 0xb7, 0x35, 0x77, 0x30, 0xc3, 0x89, 0x2c, 0x89, 0xc7, + 0x93, 0xee, 0x1e, 0xa0, 0xcc, 0x03, 0x2f, 0x22, 0x29, 0xa2, 0x6d, 0x60, 0x07, 0x2d, 0x28, 0x6b, 0x0d, 0xa2, 0xd6, + 0xde, 0x23, 0x66, 0x7b, 0x69, 0xf7, 0x43, 0xf7, 0xe1, 0x03, 0x0f, 0xd6, 0x44, 0xc8, 0x19, 0x4c, 0x28, 0x7e, 0x97, + 0xf6, 0x73, 0xd8, 0x33, 0xc2, 0x95, 0x56, 0x28, 0x78, 0x8e, 0x1b, 0x54, 0x7d, 0x9f, 0x2d, 0x20, 0x5a, 0x24, 0x20, + 0x67, 0xbb, 0x16, 0xd6, 0xa8, 0x18, 0xf9, 0x49, 0x0c, 0x95, 0xd4, 0xb6, 0x7c, 0x83, 0xa5, 0xbf, 0xae, 0x02, 0x49, + 0x20, 0x31, 0x27, 0xf5, 0xbc, 0xb6, 0x2d, 0x16, 0x37, 0x4b, 0x36, 0x0f, 0x15, 0xa5, 0xe7, 0xe5, 0xd8, 0x79, 0x07, + 0xf5, 0x28, 0xb8, 0x2c, 0xdb, 0xeb, 0xdc, 0x2e, 0xed, 0xae, 0x75, 0x18, 0x4e, 0x52, 0x4e, 0x31, 0xe0, 0xa1, 0x6d, + 0x3d, 0x72, 0xb1, 0xf8, 0xf7, 0x40, 0x1a, 0xde, 0x5d, 0x7e, 0x78, 0x73, 0x4b, 0x6b, 0x4c, 0xd4, 0x66, 0x2a, 0x12, + 0xe1, 0xe4, 0x86, 0x15, 0xc9, 0x90, 0x26, 0x12, 0x3d, 0x7c, 0x91, 0x6f, 0xae, 0x35, 0xac, 0x12, 0xd9, 0x90, 0x61, + 0xb3, 0xd9, 0xcd, 0x64, 0xdc, 0xca, 0x76, 0x7f, 0x3a, 0xf4, 0x26, 0xc8, 0xd6, 0x89, 0xd2, 0x3c, 0x77, 0xd8, 0x62, + 0xed, 0x9e, 0xb1, 0xa8, 0x9f, 0x1c, 0x65, 0x8e, 0x78, 0x43, 0x4c, 0xb4, 0x4d, 0xb9, 0xb6, 0x66, 0x1e, 0x21, 0x70, + 0x6f, 0xed, 0x3f, 0x2b, 0xf6, 0xf1, 0x1a, 0xd3, 0xc7, 0xf4, 0x0b, 0xee, 0xf9, 0xc4, 0xc3, 0xcf, 0x34, 0xc9, 0x0d, + 0xb1, 0xdd, 0x45, 0x3c, 0xe4, 0xa3, 0xbb, 0x61, 0xca, 0x84, 0x65, 0xda, 0x5c, 0xb5, 0x9c, 0x1b, 0x83, 0x54, 0xa1, + 0x48, 0xe5, 0x3e, 0xa2, 0xeb, 0xb0, 0x1f, 0xce, 0xf2, 0x3d, 0x48, 0x64, 0xbe, 0x4f, 0x22, 0x10, 0xeb, 0x1f, 0x05, + 0x71, 0x8e, 0x25, 0x2f, 0x8d, 0x22, 0x8d, 0xfe, 0x84, 0x52, 0x96, 0x72, 0x08, 0xf8, 0xe6, 0x80, 0x73, 0x15, 0x5b, + 0xff, 0x7d, 0xf6, 0x7d, 0x98, 0x76, 0x7d, 0xce, 0xee, 0x16, 0x12, 0xde, 0x72, 0xe2, 0x4e, 0xe5, 0xf1, 0xa9, 0x90, + 0x7b, 0x0f, 0x72, 0x2d, 0xbf, 0xed, 0x86, 0x86, 0xce, 0xf1, 0xb2, 0x45, 0x71, 0x55, 0xa7, 0xf2, 0x47, 0x51, 0xab, + 0xaa, 0xa4, 0x2a, 0x7b, 0x1e, 0x39, 0x93, 0x93, 0xb3, 0xdc, 0xab, 0x0a, 0x43, 0x03, 0x8f, 0x38, 0x5b, 0x62, 0x9d, + 0xbd, 0xc7, 0xcc, 0xe2, 0x84, 0x8f, 0x42, 0x03, 0xc1, 0x2a, 0xe9, 0x13, 0x8e, 0xe8, 0x0b, 0xb4, 0xd3, 0xfa, 0xd2, + 0xcd, 0xed, 0x47, 0x96, 0xb2, 0x83, 0xa3, 0xf1, 0xca, 0x8a, 0x7c, 0x9d, 0x02, 0x31, 0x53, 0xac, 0x62, 0xe4, 0xf3, + 0x1b, 0x84, 0x44, 0xf3, 0x8b, 0xe9, 0x82, 0xf6, 0x2e, 0x6b, 0x51, 0x1d, 0x3e, 0x6b, 0xf6, 0xca, 0x0b, 0x40, 0x1e, + 0x57, 0x15, 0x87, 0x48, 0x7b, 0x83, 0x38, 0x4d, 0x88, 0x7a, 0x76, 0xdb, 0xb3, 0x85, 0x38, 0x31, 0xcb, 0xd1, 0x62, + 0xd2, 0xab, 0x12, 0xd9, 0x6f, 0x4f, 0x4f, 0x20, 0x25, 0x3a, 0x63, 0x2c, 0x19, 0x69, 0x4b, 0xf9, 0x86, 0xe6, 0xf4, + 0x1e, 0xd6, 0x31, 0x11, 0xb1, 0x5a, 0x00, 0x52, 0x0d, 0x29, 0xf4, 0x35, 0x6a, 0x47, 0x44, 0xf8, 0xf9, 0xc8, 0x52, + 0x29, 0x32, 0xc6, 0x37, 0xf6, 0x23, 0x6d, 0x31, 0xab, 0x88, 0x53, 0xc4, 0xac, 0x61, 0x18, 0xd9, 0xb8, 0xc4, 0x28, + 0xa8, 0x56, 0x8f, 0x37, 0xf8, 0x04, 0x83, 0x94, 0x62, 0xab, 0xeb, 0x71, 0xdc, 0xc4, 0x4f, 0x47, 0x22, 0xc2, 0x7d, + 0x82, 0x38, 0x29, 0x81, 0x8d, 0xe7, 0x6f, 0xce, 0x54, 0xe7, 0x7c, 0x69, 0xb0, 0xe7, 0x48, 0xf5, 0x38, 0xc3, 0x40, + 0x13, 0x7c, 0xf5, 0xa3, 0xd9, 0x1f, 0xcb, 0x37, 0x32, 0x8b, 0x3b, 0x09, 0xa9, 0x95, 0xd3, 0xad, 0x36, 0x5b, 0x22, + 0x7e, 0x80, 0x0b, 0x67, 0xbc, 0x47, 0x35, 0x77, 0xf9, 0xa5, 0x26, 0x06, 0x56, 0x98, 0x13, 0x72, 0x37, 0x47, 0x92, + 0xbf, 0x00, 0xe3, 0x66, 0x0d, 0x6d, 0x2e, 0xc7, 0x2e, 0x38, 0x09, 0xe4, 0xea, 0xa6, 0xf4, 0x9b, 0xcf, 0x9e, 0x81, + 0xe9, 0x61, 0xf9, 0xd1, 0xef, 0x32, 0xff, 0x65, 0xac, 0x53, 0x13, 0xfc, 0xc8, 0x51, 0x59, 0x9f, 0xcb, 0xe3, 0xdf, + 0x01, 0xb7, 0x67, 0x7d, 0xe3, 0xcf, 0x93, 0x20, 0xce, 0x35, 0x7f, 0x66, 0xc1, 0x44, 0x36, 0xbb, 0xd6, 0xde, 0xcf, + 0x9f, 0xc1, 0x9b, 0xfb, 0x3d, 0x42, 0xe5, 0x6b, 0xe7, 0x14, 0xea, 0xa6, 0x01, 0x16, 0x7a, 0xf5, 0x60, 0x1f, 0x90, + 0x9b, 0x7d, 0x88, 0x0a, 0x14, 0x39, 0xa2, 0xd2, 0x35, 0xe3, 0x1a, 0xb6, 0x55, 0x3b, 0x94, 0x3d, 0xc3, 0x79, 0x4f, + 0x9b, 0x5d, 0xb5, 0xad, 0x57, 0x69, 0x13, 0x52, 0x98, 0xc2, 0x1a, 0xe8, 0x56, 0x37, 0xec, 0xdc, 0x1c, 0x2c, 0xdf, + 0x5a, 0x06, 0x1e, 0x16, 0xae, 0x98, 0xbd, 0x98, 0xf9, 0x8e, 0x0e, 0xfc, 0x59, 0xca, 0x2b, 0x0a, 0xb5, 0xfa, 0x8d, + 0xe3, 0xa8, 0x87, 0x36, 0xe0, 0x0d, 0x7f, 0x9d, 0x50, 0x9d, 0x3d, 0x67, 0x0b, 0x23, 0x17, 0xe2, 0xd7, 0xba, 0xc1, + 0x27, 0x74, 0xa9, 0x0a, 0x26, 0xe0, 0x4b, 0x9b, 0x1d, 0x0f, 0x5e, 0x87, 0x96, 0xa4, 0xf2, 0xb8, 0x79, 0x8c, 0xe5, + 0xdc, 0x4e, 0xb9, 0x54, 0x2b, 0xf3, 0xa3, 0x1b, 0x25, 0xd8, 0x20, 0x90, 0x24, 0x58, 0x84, 0xf0, 0x4f, 0x30, 0x67, + 0x1c, 0x89, 0x21, 0x7b, 0xa3, 0x62, 0x8a, 0x16, 0x14, 0x0c, 0x91, 0x52, 0xb6, 0x58, 0x60, 0xb3, 0xf7, 0x08, 0x7f, + 0x7f, 0xdf, 0x3a, 0xf5, 0xb4, 0xef, 0x9d, 0x02, 0xed, 0xdb, 0x27, 0xa5, 0xb4, 0xef, 0x9f, 0x14, 0x9d, 0xa5, 0x56, + 0x19, 0xfb, 0x6a, 0xc9, 0xbe, 0x1a, 0xd9, 0x63, 0x3b, 0xdb, 0x43, 0x2b, 0x8e, 0x6b, 0xd0, 0x8e, 0xc5, 0x82, 0x6f, + 0xc9, 0x01, 0xc7, 0x11, 0xcb, 0x92, 0xf5, 0x63, 0xa1, 0xb9, 0x77, 0xb5, 0x1e, 0xf9, 0xf6, 0x00, 0x15, 0x85, 0xb7, + 0xc3, 0xda, 0x8d, 0xac, 0x2c, 0xcf, 0x34, 0xb7, 0x4e, 0xe2, 0xd4, 0xd9, 0xde, 0x42, 0xf1, 0x74, 0xea, 0x08, 0x7e, + 0xd6, 0xe4, 0x70, 0x86, 0x4a, 0x13, 0xf8, 0x8f, 0x1c, 0x3f, 0x36, 0x5a, 0x5b, 0xd1, 0x78, 0xf3, 0xbd, 0x27, 0x1a, + 0x9a, 0xbf, 0xb8, 0x8a, 0x58, 0x98, 0x96, 0x93, 0x7b, 0x07, 0x53, 0x1f, 0x4a, 0xd6, 0xe2, 0x76, 0x07, 0x64, 0xb6, + 0x94, 0x97, 0x39, 0x41, 0x08, 0xa7, 0xba, 0x85, 0xff, 0x2c, 0xa0, 0x31, 0x27, 0x2d, 0x17, 0x53, 0xf9, 0xbc, 0xd5, + 0x86, 0xda, 0xce, 0x59, 0x83, 0x78, 0xec, 0xca, 0x74, 0x57, 0x82, 0x29, 0x3c, 0x9f, 0xc5, 0x15, 0x90, 0xfa, 0x85, + 0x35, 0xff, 0x46, 0xd9, 0xc3, 0xe5, 0x4e, 0x4c, 0x83, 0xff, 0xf7, 0x64, 0x3b, 0x08, 0x27, 0x74, 0x35, 0x4e, 0xb9, + 0x24, 0xe2, 0x7e, 0x6e, 0xa5, 0xed, 0x99, 0x37, 0x72, 0x7d, 0xfb, 0x8c, 0x61, 0x7a, 0xae, 0x42, 0x20, 0x53, 0x68, + 0x3f, 0xdd, 0x3f, 0x8f, 0x71, 0x48, 0xb0, 0x62, 0xcf, 0x09, 0x56, 0x19, 0x98, 0x92, 0x77, 0x33, 0x99, 0x9e, 0xbf, + 0xfc, 0x5f, 0xe6, 0xf7, 0x92, 0xf5, 0xa0, 0x37, 0x7b, 0xcb, 0x36, 0xb7, 0x85, 0x75, 0x69, 0x31, 0xb3, 0x7e, 0x34, + 0xd3, 0xfa, 0x5f, 0x71, 0x80, 0xb7, 0xdb, 0xe9, 0x8a, 0x3f, 0xaa, 0x0c, 0xd2, 0x38, 0x64, 0xdd, 0x6f, 0x4b, 0xd6, + 0x03, 0xde, 0xed, 0xed, 0xf3, 0x5b, 0x1c, 0xfe, 0xb1, 0x09, 0x7c, 0x4b, 0x17, 0x00, 0x02, 0xf8, 0x91, 0xbb, 0x1c, + 0xbb, 0x9c, 0xc9, 0x9d, 0xeb, 0x0a, 0x0b, 0xaa, 0x96, 0x43, 0x16, 0x2e, 0x96, 0x6c, 0x41, 0x3e, 0x5e, 0x13, 0xd9, + 0xe8, 0xc0, 0xec, 0x12, 0xb1, 0xf7, 0x44, 0x09, 0xde, 0x6c, 0xf3, 0x29, 0xd1, 0xf3, 0xe7, 0x04, 0xda, 0x66, 0xd7, + 0x89, 0x0d, 0xb9, 0xb6, 0x38, 0x15, 0x27, 0x00, 0xec, 0x7b, 0xe6, 0xc2, 0xe0, 0x6c, 0xa4, 0xf6, 0x41, 0x0b, 0xa7, + 0xb7, 0x04, 0xfa, 0xd9, 0x38, 0x45, 0xde, 0xef, 0x00, 0x95, 0x52, 0x78, 0xe2, 0x10, 0x13, 0x2a, 0x76, 0xf8, 0x9c, + 0x52, 0x9f, 0x43, 0x8e, 0x9c, 0x77, 0xc0, 0x89, 0x5b, 0xda, 0x74, 0x63, 0x79, 0xbb, 0xe1, 0xbb, 0x8a, 0x74, 0x65, + 0xf5, 0xb0, 0x84, 0xb6, 0xcd, 0xd1, 0x19, 0x67, 0x94, 0xa0, 0xc4, 0x08, 0x29, 0xc3, 0xf3, 0x63, 0x30, 0x48, 0x26, + 0xe3, 0x30, 0xd1, 0x6b, 0xce, 0x0a, 0xa3, 0xff, 0x44, 0x08, 0x23, 0x84, 0x9f, 0x5f, 0x67, 0x6c, 0xdf, 0xa3, 0xbb, + 0xb6, 0xe9, 0x5e, 0x6f, 0x15, 0xb1, 0xcf, 0x2b, 0x0d, 0x4a, 0xa5, 0xd2, 0x58, 0xdb, 0x8c, 0x7f, 0x75, 0x52, 0x9d, + 0x46, 0x1d, 0x8e, 0x70, 0x76, 0xa6, 0xcd, 0xf9, 0xbf, 0x9e, 0x99, 0xb9, 0xd7, 0xce, 0xcb, 0x9e, 0x19, 0xeb, 0xc6, + 0x25, 0x91, 0x16, 0xe4, 0x26, 0x0d, 0x25, 0xeb, 0xb1, 0xd1, 0xde, 0x94, 0x4c, 0xfd, 0xc8, 0xa4, 0x80, 0x8d, 0xb1, + 0xda, 0xe1, 0x32, 0x3e, 0xcd, 0xfb, 0xa8, 0x24, 0x0b, 0x2e, 0xc4, 0xf9, 0x70, 0xbb, 0xb1, 0x22, 0x85, 0x1d, 0x68, + 0xf2, 0xeb, 0x4f, 0xc9, 0xae, 0xf0, 0x9d, 0xdf, 0x81, 0x64, 0xd6, 0x17, 0x2b, 0xc5, 0x06, 0x75, 0x05, 0x7a, 0x03, + 0x2e, 0xdf, 0xd1, 0x1b, 0x65, 0x34, 0x7c, 0x5d, 0x4a, 0x54, 0x92, 0x50, 0x63, 0xa5, 0x60, 0xb7, 0x58, 0x97, 0x51, + 0x14, 0x17, 0x6b, 0xa2, 0x5f, 0x15, 0x4c, 0x16, 0xdb, 0x6e, 0xe0, 0xdc, 0x04, 0xea, 0x51, 0x07, 0x27, 0xfa, 0x3b, + 0x44, 0xde, 0x16, 0xc5, 0x86, 0x6c, 0x1b, 0x88, 0xa1, 0x15, 0xd7, 0x75, 0xaa, 0xb1, 0x3e, 0xf2, 0x80, 0x1e, 0xf7, + 0xaf, 0x8e, 0x21, 0x78, 0xf8, 0x69, 0xc0, 0x52, 0x57, 0x9d, 0x3a, 0xbc, 0x7d, 0x74, 0x63, 0x49, 0xea, 0x71, 0x7e, + 0xf3, 0x4f, 0xc5, 0x36, 0x2d, 0x4f, 0xb7, 0xc8, 0x0d, 0x89, 0xe0, 0x5d, 0x41, 0xf6, 0xd3, 0xc1, 0x6d, 0x7b, 0xa6, + 0x28, 0x0a, 0x2f, 0x94, 0xb4, 0xbc, 0x29, 0x3c, 0x8c, 0xe3, 0x46, 0x8a, 0x1a, 0x2a, 0x32, 0xfe, 0x31, 0x36, 0x2c, + 0x92, 0x9d, 0xad, 0x0f, 0x63, 0xe7, 0x95, 0x90, 0x8f, 0x2b, 0xd7, 0xea, 0x46, 0xa6, 0x63, 0x5b, 0x14, 0x39, 0x7a, + 0x9d, 0x07, 0xa0, 0xf2, 0x47, 0x61, 0x12, 0x50, 0x1c, 0x89, 0x00, 0xa2, 0x3a, 0xf1, 0xa2, 0xe8, 0x81, 0xcd, 0xa1, + 0x19, 0x56, 0x83, 0x7c, 0x2a, 0xfa, 0x1b, 0x15, 0x9d, 0xd9, 0xf1, 0x50, 0xd8, 0x8b, 0x4c, 0xac, 0x3e, 0xe6, 0xae, + 0x43, 0x91, 0x36, 0x72, 0xea, 0x3b, 0xd7, 0x98, 0x37, 0xd0, 0xc3, 0x22, 0x14, 0x7f, 0x61, 0x83, 0x98, 0xb2, 0x01, + 0x2b, 0x64, 0xec, 0x79, 0x04, 0x30, 0x4b, 0xf2, 0x45, 0x12, 0x78, 0xd3, 0xaf, 0x40, 0xbf, 0xc1, 0x7d, 0xb5, 0x6f, + 0x26, 0x4d, 0xb3, 0x70, 0x77, 0x63, 0xbf, 0x2f, 0x36, 0x72, 0x4f, 0x08, 0x22, 0x58, 0x92, 0xd9, 0xb2, 0x96, 0xde, + 0x03, 0x7e, 0xa5, 0xe0, 0x19, 0x78, 0xc8, 0x00, 0x8e, 0x98, 0x96, 0xb8, 0xae, 0xd6, 0x93, 0xab, 0xc3, 0x56, 0x6e, + 0x0b, 0x25, 0x38, 0x44, 0xd2, 0xe8, 0x96, 0x4a, 0xb3, 0x94, 0xee, 0x99, 0xad, 0xbb, 0x91, 0x71, 0xfc, 0x65, 0x50, + 0x9e, 0x58, 0x8f, 0x5f, 0x93, 0xc8, 0x96, 0x44, 0x14, 0x97, 0x5f, 0xe7, 0x90, 0xdd, 0x58, 0xa9, 0x98, 0x0c, 0x54, + 0x3d, 0x79, 0xaa, 0x09, 0xde, 0x60, 0x71, 0x17, 0x1c, 0xe9, 0x03, 0xed, 0x09, 0xc5, 0x49, 0xaa, 0x4a, 0xa9, 0x87, + 0x7e, 0xc2, 0x57, 0xd8, 0xe7, 0xa2, 0xb7, 0xd9, 0x31, 0xcd, 0xd8, 0x67, 0x34, 0xc1, 0x80, 0x3a, 0x09, 0x4e, 0xbb, + 0x66, 0x78, 0xe4, 0x48, 0x6c, 0x3a, 0x90, 0x8f, 0xf1, 0x54, 0xe8, 0x36, 0x6e, 0x56, 0x21, 0x8e, 0x86, 0xd0, 0xfd, + 0x30, 0x14, 0x46, 0x3f, 0xa3, 0x74, 0xac, 0x3e, 0xed, 0xd7, 0x5c, 0xd4, 0xce, 0x98, 0xa2, 0x29, 0x2f, 0xbb, 0xa6, + 0x00, 0x6f, 0xa4, 0xbc, 0xc1, 0x0a, 0xf8, 0xfe, 0xb2, 0x59, 0x57, 0x8f, 0x17, 0x36, 0xf9, 0x0f, 0xae, 0x83, 0x8d, + 0x84, 0x09, 0xfc, 0x11, 0x92, 0x99, 0x2d, 0x82, 0x35, 0x8c, 0xf3, 0x92, 0x58, 0x38, 0x7a, 0x9c, 0xef, 0x07, 0xd9, + 0x1f, 0x57, 0x8d, 0x07, 0x61, 0x0b, 0x0f, 0xad, 0xe4, 0x9c, 0xa8, 0xd7, 0xd4, 0xa9, 0x91, 0x0f, 0x22, 0x93, 0xc0, + 0x84, 0xf2, 0x3c, 0xc1, 0x34, 0xab, 0xb3, 0x59, 0x50, 0xdb, 0x44, 0xc5, 0xa0, 0xd0, 0x8d, 0xdb, 0x99, 0x20, 0xc9, + 0x86, 0xe0, 0x94, 0x97, 0x65, 0xc3, 0xed, 0x75, 0x6b, 0xe6, 0x45, 0xf3, 0xd7, 0x64, 0x87, 0xa5, 0xdf, 0x05, 0x1d, + 0x6b, 0xe3, 0xf5, 0x60, 0x7b, 0xd0, 0x79, 0x58, 0xbc, 0x50, 0x3a, 0x8d, 0xaa, 0x9b, 0x7a, 0x11, 0x37, 0xfb, 0x39, + 0x75, 0x35, 0xd1, 0x6c, 0x09, 0x48, 0x67, 0xa3, 0x7c, 0x8f, 0x9d, 0xb8, 0x46, 0x51, 0x5a, 0x4b, 0xab, 0x5b, 0xe6, + 0x1d, 0x8b, 0x91, 0xbb, 0x81, 0x51, 0x62, 0xed, 0x22, 0x86, 0x9a, 0x9f, 0xc3, 0xdc, 0x9e, 0x98, 0x40, 0x45, 0xff, + 0x3a, 0x9f, 0xcc, 0xec, 0x62, 0x9a, 0x4a, 0x32, 0xcc, 0x07, 0xa5, 0x6f, 0x89, 0xe6, 0xee, 0xf1, 0x9c, 0x93, 0xd2, + 0xb6, 0xad, 0x62, 0x1d, 0xba, 0x85, 0x81, 0x0f, 0x5c, 0x4d, 0x03, 0xc1, 0x15, 0xbd, 0xc5, 0x98, 0x67, 0xf0, 0x7c, + 0xc0, 0xec, 0x1b, 0x79, 0x3e, 0x2f, 0x45, 0xde, 0x3e, 0x91, 0x19, 0xbe, 0x50, 0xa0, 0x98, 0xde, 0xe9, 0x7c, 0x6f, + 0x9f, 0x87, 0x1b, 0x77, 0x59, 0xe0, 0xbe, 0x24, 0x0e, 0x19, 0xfe, 0x75, 0x17, 0x5b, 0xc6, 0x1a, 0xcf, 0x9c, 0xfb, + 0x2d, 0x89, 0x09, 0xa5, 0xda, 0xae, 0x1f, 0xa2, 0xbc, 0x16, 0x61, 0x52, 0x85, 0xa5, 0xdb, 0x2a, 0xa4, 0x32, 0xf4, + 0x45, 0xa4, 0x8a, 0xc7, 0x99, 0x9b, 0x9d, 0xa1, 0x34, 0x82, 0x0c, 0x05, 0x13, 0x64, 0xb5, 0x4f, 0xa2, 0x79, 0x56, + 0xf2, 0xa0, 0x4d, 0x13, 0xf9, 0xf0, 0xba, 0x2a, 0x63, 0xe1, 0x71, 0xe6, 0xde, 0x76, 0xc4, 0xdc, 0xba, 0x8e, 0xf3, + 0xea, 0x03, 0x75, 0x2b, 0x47, 0xe6, 0xb9, 0x62, 0x6c, 0xc5, 0xd8, 0x3d, 0xa8, 0x45, 0xa0, 0x0c, 0x45, 0x12, 0x0e, + 0x6c, 0x31, 0x7a, 0x7b, 0xa1, 0xce, 0x06, 0xc2, 0xad, 0xb2, 0x3e, 0xaa, 0xc5, 0x6b, 0xda, 0xb6, 0x52, 0x0a, 0x4e, + 0xa0, 0x10, 0x4e, 0x34, 0xf6, 0x9c, 0x0f, 0xff, 0xfc, 0x5c, 0xa7, 0x1e, 0xff, 0x99, 0x10, 0x9b, 0xfd, 0x97, 0xf7, + 0xaf, 0x63, 0x0a, 0xd0, 0xeb, 0x9e, 0x75, 0x45, 0x7a, 0xad, 0xeb, 0x35, 0xd2, 0xab, 0xaf, 0x57, 0xb5, 0x39, 0xe1, + 0x59, 0x2d, 0xb5, 0x51, 0x1b, 0x77, 0x6e, 0x14, 0xeb, 0x30, 0x94, 0x94, 0xd4, 0x7e, 0x4f, 0x4f, 0x3f, 0x8d, 0x55, + 0x99, 0x6f, 0xcd, 0xa4, 0x98, 0x4d, 0x5f, 0x1c, 0xab, 0xf5, 0xb8, 0x8c, 0x10, 0xbb, 0x17, 0x43, 0x6d, 0xa5, 0x3a, + 0x35, 0x75, 0x9b, 0xcf, 0x2f, 0xc6, 0xc5, 0xe5, 0xcb, 0xbf, 0x22, 0xc4, 0xf3, 0x11, 0xe3, 0xa1, 0x8d, 0x76, 0xde, + 0x37, 0x48, 0x8d, 0xcb, 0xcd, 0x11, 0xe7, 0x68, 0x56, 0x15, 0xc6, 0x88, 0xa7, 0x55, 0xe7, 0x2e, 0xb8, 0xf6, 0x20, + 0xf0, 0x73, 0x71, 0x55, 0xa9, 0x48, 0x52, 0xdf, 0x36, 0xb0, 0x2e, 0x37, 0x4b, 0x93, 0xc3, 0xdf, 0x0b, 0x2c, 0xe8, + 0x63, 0x53, 0x95, 0xeb, 0x07, 0x25, 0xc4, 0xd8, 0x29, 0x62, 0xca, 0xa9, 0xf4, 0x2e, 0xac, 0x7c, 0x51, 0x5f, 0x4f, + 0x99, 0xfa, 0x36, 0x88, 0x31, 0x8b, 0x31, 0x27, 0x4b, 0x31, 0x27, 0x79, 0x60, 0xfb, 0x3c, 0x06, 0xc6, 0xc5, 0x24, + 0x10, 0xf9, 0x70, 0xe3, 0xca, 0x58, 0xbe, 0x08, 0x18, 0xac, 0xa2, 0x0d, 0x04, 0xd3, 0x3b, 0x33, 0xed, 0xe2, 0x1f, + 0xf3, 0xcb, 0x41, 0x64, 0x5c, 0x89, 0x21, 0xac, 0x8e, 0xf8, 0xad, 0xd3, 0x0b, 0x64, 0x62, 0xe5, 0x8c, 0x66, 0x09, + 0x98, 0x75, 0xd3, 0x34, 0x38, 0x56, 0x4d, 0x83, 0x4a, 0x33, 0xaf, 0xb0, 0x0c, 0x24, 0x31, 0x30, 0x95, 0x6a, 0xf8, + 0x95, 0x16, 0x09, 0xce, 0xf9, 0xfb, 0xae, 0xcf, 0x29, 0x90, 0xc6, 0x99, 0x46, 0xa5, 0xc0, 0xe7, 0x0e, 0xf8, 0x05, + 0xfa, 0x15, 0x9f, 0x88, 0xe3, 0x34, 0xe9, 0x91, 0xc9, 0xe8, 0x81, 0xda, 0x81, 0x90, 0x59, 0x4b, 0x46, 0x61, 0x1a, + 0x42, 0x28, 0x05, 0x44, 0x7c, 0x3f, 0xcc, 0x45, 0x95, 0x35, 0xaf, 0xc6, 0x02, 0xb7, 0x10, 0x31, 0x23, 0xaa, 0x06, + 0x22, 0xc9, 0x48, 0xae, 0x1b, 0x32, 0x2c, 0x96, 0x26, 0x2d, 0xc6, 0xe0, 0x04, 0xc9, 0x3c, 0x9d, 0x08, 0xfe, 0x65, + 0x48, 0xc6, 0x05, 0x6f, 0x7a, 0xaf, 0x80, 0xbe, 0xc6, 0xc5, 0x64, 0xe7, 0xcd, 0xbc, 0xe8, 0x89, 0xb4, 0x5c, 0xe5, + 0x43, 0xa2, 0xd0, 0xdf, 0xd7, 0x7d, 0xef, 0x58, 0x3d, 0x48, 0xc1, 0xbc, 0x2d, 0x6a, 0x8b, 0x96, 0xed, 0xb5, 0x95, + 0xc7, 0x72, 0xdc, 0x3d, 0x4a, 0x50, 0x00, 0xdd, 0x66, 0xd1, 0x0a, 0x3c, 0x49, 0xd6, 0xd8, 0xa9, 0x4f, 0x44, 0x74, + 0x74, 0x1b, 0x25, 0xb3, 0x23, 0x5b, 0x17, 0x3f, 0x90, 0x91, 0xe4, 0xcc, 0x5c, 0x89, 0xce, 0xff, 0x59, 0xf2, 0x26, + 0x17, 0x33, 0x5b, 0x75, 0xc8, 0x01, 0x6e, 0x3a, 0x13, 0x61, 0x8a, 0xf6, 0x56, 0x66, 0x23, 0x44, 0x86, 0x93, 0x49, + 0x16, 0x64, 0xea, 0xc5, 0x5f, 0x8c, 0x14, 0xfc, 0x47, 0xc4, 0x86, 0x96, 0x3c, 0xd2, 0xff, 0x70, 0x0d, 0xe1, 0x5b, + 0x39, 0x1c, 0x24, 0xd5, 0x7b, 0x2d, 0xb8, 0x2d, 0x2d, 0x1b, 0x66, 0x83, 0x24, 0x3c, 0x3e, 0xbb, 0x7c, 0xe6, 0xdb, + 0x83, 0xfc, 0x43, 0x44, 0x08, 0x84, 0xfa, 0xdf, 0xf4, 0xaa, 0x76, 0xf1, 0x32, 0x2a, 0x4e, 0x83, 0xf2, 0xf5, 0xf8, + 0x8c, 0xc3, 0x1b, 0x2a, 0x2f, 0xe0, 0xb7, 0x6f, 0xe7, 0x1c, 0x98, 0x81, 0x2f, 0x63, 0xab, 0xb1, 0x80, 0xbd, 0x70, + 0xd8, 0x63, 0x28, 0x59, 0xc4, 0xa1, 0xed, 0x6c, 0x84, 0xdb, 0xd0, 0xf5, 0x36, 0xdb, 0xaf, 0x94, 0x72, 0x75, 0xc6, + 0xf9, 0x3b, 0xdb, 0xaa, 0xe6, 0x66, 0xd7, 0x0d, 0x5b, 0x2a, 0xe9, 0x69, 0x5f, 0x6e, 0x30, 0x75, 0x43, 0xf6, 0x36, + 0xd4, 0x5a, 0xbe, 0x19, 0xd6, 0x95, 0x37, 0x0b, 0x83, 0x42, 0xc0, 0x98, 0x61, 0xcd, 0x15, 0xb9, 0xd6, 0xca, 0x7e, + 0x30, 0xc5, 0xfe, 0x30, 0x08, 0x89, 0xa8, 0x8a, 0x24, 0x67, 0x83, 0x1e, 0xe7, 0x6a, 0xed, 0x59, 0x3d, 0x02, 0x4b, + 0x27, 0x96, 0x63, 0xcd, 0x0a, 0x06, 0x43, 0xa9, 0xaa, 0xd5, 0x52, 0x77, 0xb8, 0x4a, 0x9f, 0x6a, 0x79, 0xc5, 0x0b, + 0x12, 0xf6, 0x0b, 0x88, 0x4e, 0x7c, 0x77, 0xf7, 0x24, 0xf2, 0x9d, 0x59, 0x7d, 0x63, 0x2a, 0x4d, 0x94, 0x67, 0xc5, + 0x0a, 0x4a, 0xd6, 0x3b, 0xc0, 0x50, 0x51, 0x63, 0x6c, 0xe8, 0x0e, 0x0d, 0xd6, 0xe6, 0x38, 0xdc, 0x17, 0xf6, 0xdb, + 0x82, 0xec, 0x47, 0xfd, 0x9c, 0x93, 0xfb, 0x68, 0xb3, 0xa8, 0x97, 0xf5, 0x56, 0x63, 0xe4, 0x08, 0xaf, 0x37, 0x27, + 0x55, 0xb6, 0xa0, 0xc9, 0x5e, 0x83, 0xd3, 0x4b, 0x33, 0x75, 0xa1, 0xec, 0xc4, 0x8c, 0x28, 0xd3, 0x41, 0x24, 0x09, + 0xba, 0xb3, 0x1e, 0x04, 0xd7, 0x2c, 0x0b, 0x6b, 0x93, 0x91, 0x7b, 0x30, 0x9c, 0x23, 0x15, 0xd1, 0x25, 0x14, 0xc5, + 0x39, 0x9b, 0xd7, 0x3f, 0x30, 0xe4, 0x28, 0x8f, 0xc5, 0xb2, 0x64, 0x41, 0xbd, 0x6f, 0x61, 0xa4, 0x26, 0xfb, 0x74, + 0x2c, 0xa5, 0x90, 0x1d, 0xc0, 0xc6, 0x8e, 0xb6, 0x73, 0xc1, 0x9c, 0xda, 0xba, 0x04, 0x3b, 0xd9, 0xa9, 0xb9, 0x5b, + 0x91, 0x01, 0x91, 0x07, 0x42, 0x14, 0x06, 0x7c, 0x7f, 0x5e, 0x11, 0xc0, 0x9a, 0xe3, 0x14, 0x89, 0x3f, 0x08, 0xe3, + 0x97, 0x1f, 0x14, 0x83, 0x84, 0xe5, 0xae, 0xe7, 0x70, 0xfa, 0x3a, 0x80, 0x56, 0xea, 0xc5, 0xe6, 0x7b, 0x26, 0xca, + 0x46, 0xfe, 0x2a, 0xd6, 0x3a, 0x62, 0x88, 0x70, 0xe0, 0xcb, 0x66, 0x43, 0xd2, 0x78, 0xb3, 0x5c, 0x9c, 0x8d, 0x9a, + 0xae, 0xab, 0x23, 0xee, 0x23, 0x15, 0x84, 0xb1, 0xd9, 0xf0, 0xd0, 0x8d, 0xf3, 0x43, 0xd6, 0x6e, 0x06, 0x87, 0x41, + 0x04, 0x7e, 0x6d, 0xba, 0x56, 0x97, 0x70, 0xb5, 0xa6, 0x99, 0x78, 0x2f, 0xce, 0xa6, 0xfb, 0xba, 0xd7, 0x35, 0xc2, + 0xbf, 0x5c, 0xe2, 0x80, 0xf9, 0x37, 0x52, 0xc5, 0x41, 0x8b, 0x39, 0x7a, 0x8d, 0x4b, 0x9a, 0xe9, 0xa9, 0x21, 0x77, + 0x57, 0xca, 0x7b, 0x28, 0x07, 0xaa, 0x63, 0x3c, 0x3d, 0x64, 0x37, 0x87, 0x5b, 0x80, 0xda, 0x8e, 0x10, 0x57, 0x06, + 0xea, 0x09, 0x80, 0x2b, 0x09, 0x84, 0x65, 0x1e, 0xcf, 0x90, 0xbe, 0x67, 0xd2, 0x09, 0x68, 0xe8, 0x40, 0xb9, 0xe9, + 0x49, 0x99, 0x43, 0xea, 0xa1, 0x0e, 0x52, 0x4c, 0x78, 0xd0, 0xcb, 0xae, 0x96, 0xea, 0x3a, 0x1a, 0x21, 0x69, 0x42, + 0x41, 0xfc, 0x82, 0xa0, 0xe8, 0xab, 0x21, 0xf2, 0x97, 0x89, 0xca, 0xba, 0xaa, 0xf0, 0x06, 0xff, 0x12, 0x2d, 0xb2, + 0xfa, 0xce, 0xcc, 0xec, 0x48, 0x5d, 0x56, 0xa2, 0xf6, 0x02, 0xb0, 0x0e, 0x87, 0xe0, 0x40, 0x22, 0x62, 0x9e, 0x44, + 0x13, 0xd9, 0x54, 0x28, 0x7f, 0xe6, 0xd0, 0x28, 0x80, 0xcb, 0x79, 0x24, 0x68, 0x22, 0xf0, 0xb1, 0x13, 0xe0, 0xcc, + 0x0c, 0x3c, 0x9c, 0xad, 0x26, 0x8d, 0xc0, 0x98, 0x6b, 0xe5, 0xa5, 0x66, 0x1f, 0x33, 0xa2, 0x1c, 0x17, 0x73, 0x23, + 0xbb, 0x6b, 0xf2, 0x70, 0x88, 0x79, 0x62, 0x63, 0x0e, 0xdf, 0xd7, 0x9e, 0x19, 0xd3, 0xbf, 0xcc, 0xc0, 0x27, 0x25, + 0xea, 0x4e, 0x0d, 0x8a, 0xd7, 0xed, 0x9d, 0xd7, 0x56, 0xbb, 0x86, 0x5c, 0x16, 0x1d, 0x06, 0xab, 0xb5, 0xff, 0xd7, + 0x7f, 0x8a, 0xe3, 0xbe, 0x72, 0x3e, 0x06, 0x57, 0x3c, 0x04, 0x87, 0x35, 0x43, 0xcd, 0xaf, 0xeb, 0xe2, 0x39, 0x3e, + 0x6d, 0x1f, 0xe6, 0xc6, 0xd3, 0xdd, 0x81, 0x97, 0xb9, 0x90, 0xfa, 0xcc, 0x12, 0xa2, 0x0f, 0x43, 0x8b, 0x67, 0x63, + 0x54, 0x89, 0xc6, 0x97, 0x0e, 0x29, 0x96, 0x2d, 0x9e, 0xee, 0x04, 0xe2, 0xe5, 0x70, 0x77, 0xb6, 0x40, 0xac, 0x28, + 0x11, 0xe6, 0x74, 0x22, 0xd2, 0x38, 0x02, 0xc6, 0x2b, 0xf1, 0xc0, 0x10, 0x18, 0x69, 0x94, 0x59, 0xd3, 0xfe, 0xb0, + 0x11, 0xd9, 0xe7, 0x90, 0x68, 0x32, 0x6c, 0xca, 0x3b, 0x9b, 0x51, 0x7b, 0x25, 0x12, 0x8a, 0x86, 0x75, 0xdf, 0x4d, + 0x33, 0x2a, 0xef, 0xc5, 0x38, 0x24, 0x0e, 0xe1, 0xa4, 0x77, 0x3f, 0x6d, 0x1f, 0x4a, 0x1e, 0x7e, 0x0e, 0xfb, 0xc3, + 0x0f, 0xfe, 0xe1, 0xe7, 0x70, 0x77, 0x7e, 0xf0, 0x9d, 0x9f, 0x43, 0xde, 0xf9, 0x41, 0xbc, 0x54, 0x9a, 0xbe, 0xd2, + 0x9e, 0x07, 0x63, 0xc5, 0x50, 0x2e, 0xcb, 0xc8, 0x56, 0xaa, 0xe0, 0x17, 0x3f, 0x24, 0xdc, 0xe7, 0x12, 0x29, 0x39, + 0x25, 0x2e, 0x58, 0x89, 0x4a, 0x56, 0x86, 0x4e, 0x81, 0x7d, 0x1a, 0xd0, 0xc3, 0xea, 0xed, 0xe7, 0xfc, 0xcb, 0x6d, + 0x50, 0x74, 0x26, 0xe2, 0x01, 0x24, 0x43, 0xb9, 0x33, 0x07, 0x2f, 0x4c, 0x49, 0x18, 0x65, 0x39, 0x62, 0xb4, 0xa2, + 0xd2, 0x8e, 0xb3, 0x44, 0xef, 0xdc, 0x01, 0x16, 0x82, 0xbe, 0x5d, 0xe8, 0xb5, 0x72, 0x58, 0x7f, 0xff, 0x05, 0x28, + 0x55, 0x57, 0x0c, 0x78, 0x60, 0x1f, 0x7b, 0x71, 0x1f, 0x69, 0xe5, 0xd5, 0xa4, 0x8a, 0x1a, 0x5c, 0x93, 0x83, 0x31, + 0x46, 0x48, 0xdc, 0xd3, 0xbf, 0x64, 0x4d, 0x72, 0xe6, 0xe6, 0xad, 0x66, 0xe1, 0x1e, 0xa3, 0xe7, 0x80, 0xe6, 0xc4, + 0xa8, 0x9a, 0x19, 0xb6, 0x88, 0x5a, 0xb3, 0x9a, 0x33, 0x8b, 0x38, 0x59, 0x8a, 0xad, 0xab, 0xb0, 0xe7, 0x3d, 0x7e, + 0xca, 0xef, 0xe0, 0x2a, 0x37, 0x43, 0x1a, 0xec, 0x8b, 0x0c, 0xec, 0x83, 0x2b, 0x6c, 0x6b, 0x0d, 0xa6, 0x27, 0x9c, + 0xad, 0xc5, 0xf5, 0xd5, 0x14, 0xbe, 0x20, 0xad, 0xa1, 0x2d, 0x45, 0x34, 0xba, 0x4b, 0x26, 0x36, 0x52, 0xda, 0xfa, + 0xe1, 0x6b, 0x0b, 0x8d, 0x36, 0x2b, 0x96, 0x60, 0xc9, 0xee, 0x37, 0x2f, 0xb9, 0x0f, 0x4d, 0xe6, 0x2c, 0xc8, 0x44, + 0xd5, 0x4d, 0x90, 0x36, 0x05, 0xbe, 0x38, 0x59, 0x61, 0x3c, 0x02, 0x59, 0xe4, 0x36, 0x17, 0x87, 0x53, 0x47, 0x2d, + 0xa3, 0xaa, 0x84, 0x48, 0x7d, 0x56, 0xae, 0x93, 0x4b, 0xd0, 0xf1, 0xe2, 0x40, 0x04, 0x97, 0xc3, 0x84, 0x54, 0x6a, + 0x3a, 0x6d, 0xd7, 0x68, 0x6f, 0x21, 0xcf, 0xa1, 0x4e, 0x3f, 0x0d, 0x36, 0x84, 0x21, 0xaa, 0x31, 0xf8, 0x32, 0xf3, + 0xf4, 0x9a, 0x2e, 0x4d, 0xdb, 0xc7, 0x01, 0x04, 0x7a, 0xb1, 0x3d, 0x95, 0xce, 0x5d, 0x9f, 0x92, 0x48, 0x20, 0x91, + 0xf8, 0x02, 0xe0, 0x03, 0x80, 0xaf, 0x7a, 0x89, 0xaa, 0x45, 0x26, 0xbd, 0x54, 0x81, 0x9e, 0x29, 0xb8, 0x03, 0x32, + 0x43, 0x2b, 0x40, 0xe5, 0x8f, 0x48, 0xf1, 0xb5, 0x43, 0xb2, 0x98, 0xf0, 0xd2, 0x50, 0xbc, 0x8e, 0x09, 0xed, 0x7c, + 0x98, 0x9a, 0x5e, 0x22, 0x77, 0x81, 0x94, 0x8e, 0xd8, 0xa2, 0x9f, 0xbe, 0x3c, 0xbb, 0x69, 0xe1, 0x24, 0x8f, 0x2c, + 0xbf, 0xd6, 0xfe, 0x2d, 0x6b, 0xdb, 0x55, 0xf5, 0x47, 0xa6, 0xa4, 0x0e, 0xb4, 0x21, 0x94, 0xeb, 0x99, 0xb2, 0xa7, + 0xf4, 0x15, 0xec, 0x2c, 0x86, 0x45, 0xaf, 0x2d, 0xb3, 0xdd, 0x1c, 0x3e, 0x74, 0xd1, 0x03, 0xd1, 0x84, 0xdb, 0xd7, + 0x48, 0xa0, 0xb9, 0x44, 0xb0, 0x18, 0x9e, 0xe1, 0xd2, 0x6e, 0xfc, 0x92, 0x53, 0x14, 0xc4, 0x2a, 0xf0, 0x21, 0x7d, + 0xff, 0x01, 0x43, 0x86, 0xb2, 0xdd, 0x86, 0xc3, 0x55, 0x0d, 0x34, 0x5f, 0xf7, 0x71, 0xd8, 0xab, 0x13, 0xb0, 0xb6, + 0x64, 0xbe, 0xda, 0xb4, 0x51, 0xec, 0x35, 0x97, 0xa7, 0xbd, 0xb6, 0x52, 0xe0, 0xcf, 0xc5, 0x47, 0x7f, 0x7b, 0x5e, + 0xd4, 0x2c, 0xcb, 0x8b, 0xd2, 0x7b, 0x5b, 0xd5, 0xec, 0xb4, 0x06, 0xa3, 0x3f, 0x4e, 0x85, 0x90, 0x58, 0x0e, 0x93, + 0xd2, 0xf3, 0xd1, 0xa8, 0x16, 0xbb, 0xd7, 0x64, 0x1e, 0x1f, 0x26, 0xa1, 0x9a, 0x4d, 0x8d, 0x3c, 0xb8, 0xd7, 0x9b, + 0x0b, 0x7d, 0x8f, 0x02, 0xd5, 0xbd, 0x16, 0x4e, 0xd5, 0x55, 0x29, 0x41, 0x4c, 0x46, 0x46, 0x33, 0xcd, 0xc6, 0xbc, + 0x0c, 0xdc, 0x9a, 0xa9, 0x7e, 0x41, 0x9f, 0x48, 0xc9, 0x61, 0xd8, 0x59, 0x59, 0x94, 0x8a, 0x49, 0x4a, 0x00, 0x8b, + 0xed, 0x67, 0x71, 0x72, 0x60, 0x50, 0xb5, 0xea, 0x3c, 0x60, 0x24, 0x8e, 0xc5, 0xe2, 0x23, 0x50, 0xf1, 0x5b, 0x07, + 0xa8, 0x12, 0x4e, 0x8f, 0x55, 0x71, 0x1e, 0x7e, 0x10, 0xa5, 0x52, 0x4f, 0x40, 0xa0, 0xa6, 0x4e, 0x5e, 0xe6, 0x5e, + 0xb0, 0x7c, 0x33, 0xa7, 0x8d, 0xbd, 0x30, 0x2b, 0x1d, 0x90, 0x6b, 0xd3, 0x48, 0x0c, 0x45, 0xfc, 0x93, 0x63, 0xe3, + 0x36, 0xba, 0xb0, 0xea, 0x85, 0xe5, 0x5e, 0x54, 0x07, 0xa1, 0x41, 0xe8, 0x90, 0xa7, 0xca, 0x6d, 0x19, 0xd6, 0xe7, + 0x81, 0x97, 0x27, 0xfd, 0x0b, 0x4f, 0x0f, 0x36, 0x3d, 0xfc, 0x80, 0x45, 0x2b, 0x89, 0x34, 0x54, 0xb1, 0x49, 0xe1, + 0x8e, 0x48, 0x95, 0xe5, 0xce, 0xd3, 0x1e, 0xdd, 0x6b, 0x33, 0x0f, 0xd2, 0xd5, 0x47, 0x05, 0x45, 0x6b, 0x68, 0x09, + 0xa5, 0x2e, 0x60, 0x0a, 0xa3, 0x2c, 0xde, 0xe9, 0xab, 0xf5, 0x93, 0x5d, 0x4a, 0xc2, 0x01, 0x1f, 0xc3, 0x60, 0x26, + 0xf0, 0xef, 0x87, 0x48, 0x07, 0x37, 0xb5, 0x6e, 0x85, 0x32, 0x86, 0xb4, 0x42, 0x30, 0x1f, 0x49, 0x74, 0x98, 0xe0, + 0xfb, 0xc1, 0xa4, 0xc8, 0x49, 0xc1, 0x46, 0xe3, 0x37, 0xe3, 0x1a, 0x43, 0xc7, 0x99, 0xf1, 0x9d, 0x9f, 0xae, 0xd8, + 0xdb, 0x72, 0x5c, 0x1d, 0x42, 0xc0, 0xe5, 0x58, 0xee, 0x75, 0x5d, 0x90, 0x75, 0x8c, 0x76, 0x14, 0x3e, 0x23, 0x71, + 0xa9, 0x0b, 0x3d, 0xa5, 0xda, 0x91, 0x33, 0x86, 0x25, 0x38, 0x5d, 0xcd, 0x9f, 0xd8, 0xc6, 0x15, 0xb4, 0xed, 0xec, + 0x34, 0x50, 0xb7, 0x57, 0xc0, 0x83, 0x5d, 0x63, 0x4a, 0x94, 0x25, 0x56, 0x05, 0x34, 0x18, 0x01, 0x6d, 0x59, 0x60, + 0x54, 0x13, 0x31, 0xd1, 0x28, 0x8c, 0x12, 0xa9, 0xa5, 0x94, 0x1d, 0xcb, 0x1f, 0x75, 0x92, 0x4c, 0x92, 0x75, 0x28, + 0x4e, 0x7a, 0x62, 0x92, 0xd4, 0x6a, 0x5d, 0xb6, 0x78, 0x79, 0x21, 0xf6, 0x8b, 0x54, 0x7a, 0x62, 0xef, 0xa0, 0x05, + 0x72, 0xb3, 0xef, 0x69, 0x48, 0x0d, 0x8d, 0xce, 0xf6, 0x46, 0xe7, 0xe5, 0xa9, 0x6c, 0xbe, 0xd5, 0x51, 0xcb, 0xf8, + 0xc6, 0x98, 0xa2, 0x0a, 0xa8, 0x3f, 0xd6, 0x82, 0xf4, 0xfd, 0x4b, 0xb1, 0xce, 0x50, 0x34, 0x4c, 0x5d, 0xf6, 0x58, + 0x8c, 0x74, 0x9d, 0xe6, 0x89, 0x90, 0xe0, 0xde, 0x1d, 0x18, 0x78, 0x44, 0x99, 0x3b, 0x19, 0xd3, 0x09, 0xc2, 0x10, + 0x91, 0x75, 0xb2, 0xe6, 0x7d, 0x6e, 0xfd, 0x7c, 0x14, 0x87, 0x61, 0x0c, 0x1b, 0x4c, 0xae, 0xf6, 0x53, 0x7a, 0xef, + 0xc7, 0x62, 0xa4, 0x76, 0x9d, 0xd9, 0xcd, 0x78, 0x61, 0xa9, 0x3d, 0x16, 0xf6, 0x3f, 0x64, 0x3e, 0xf5, 0x58, 0xe9, + 0xbd, 0xb4, 0x86, 0x34, 0x9e, 0x59, 0x63, 0xd5, 0x5f, 0x82, 0x76, 0xe4, 0x12, 0xed, 0xc4, 0x4e, 0xa9, 0x2a, 0x48, + 0x28, 0x48, 0x8c, 0xa9, 0xed, 0x1c, 0x0c, 0x34, 0x63, 0x9d, 0xb9, 0x63, 0x8b, 0xbe, 0x3d, 0xe5, 0xa4, 0x1c, 0xa0, + 0xbc, 0x14, 0xfe, 0xd9, 0x76, 0x50, 0x62, 0x1f, 0xc7, 0x18, 0x5b, 0x81, 0x7d, 0x48, 0x20, 0x55, 0xc1, 0x84, 0x56, + 0x93, 0x07, 0x74, 0x71, 0x4a, 0xc7, 0x9f, 0x19, 0xe6, 0x4f, 0xb0, 0xfa, 0x9a, 0x27, 0xb6, 0xd9, 0x85, 0x63, 0x4c, + 0xa9, 0xd7, 0xd9, 0x11, 0xeb, 0xa7, 0x74, 0x61, 0x8b, 0xb5, 0x31, 0xa4, 0x6c, 0xc9, 0xd6, 0xb5, 0x45, 0xc8, 0x84, + 0x21, 0xeb, 0x3a, 0x52, 0x71, 0x03, 0xe7, 0x37, 0xe4, 0x02, 0x5e, 0xef, 0xe7, 0x5c, 0xa9, 0x67, 0x11, 0xcd, 0x32, + 0x41, 0xbb, 0x04, 0x72, 0xa4, 0xf3, 0xa2, 0xfe, 0xbf, 0x95, 0x10, 0xa2, 0x4b, 0x6b, 0xba, 0x2d, 0xa1, 0x4e, 0xf2, + 0xd9, 0x59, 0xb4, 0x80, 0xc7, 0x6e, 0x94, 0x1b, 0xe7, 0xb1, 0xb4, 0x09, 0x9e, 0x0d, 0x22, 0x81, 0x0d, 0xcb, 0x29, + 0x51, 0x0d, 0xab, 0xad, 0xee, 0x7a, 0x18, 0x9f, 0xdd, 0xde, 0x28, 0xc4, 0x50, 0x61, 0xf6, 0x77, 0xa0, 0xa4, 0xe2, + 0x5e, 0x97, 0xd4, 0x3a, 0x2a, 0xff, 0x1b, 0xa5, 0x0d, 0x41, 0xe1, 0x8b, 0x9b, 0x82, 0x1d, 0xdc, 0xeb, 0x9e, 0x1a, + 0x8a, 0xfd, 0xfd, 0x42, 0x85, 0x69, 0xa7, 0x0f, 0xca, 0x04, 0x4d, 0x78, 0x0b, 0x72, 0x39, 0xf2, 0xfd, 0x6c, 0x2a, + 0xbf, 0xc8, 0x2f, 0x7d, 0x7b, 0x6d, 0x08, 0x5b, 0xd1, 0x4a, 0x2b, 0x56, 0x47, 0xf9, 0x61, 0x78, 0x13, 0xb7, 0x45, + 0x06, 0x45, 0x7d, 0x5e, 0x63, 0xef, 0x90, 0xaa, 0xc4, 0x6e, 0x7b, 0xe2, 0x06, 0x61, 0x39, 0xe9, 0x12, 0x5c, 0x58, + 0x23, 0x11, 0xa3, 0xd4, 0x9c, 0xe1, 0x54, 0x8b, 0xda, 0xc2, 0x72, 0xae, 0xd5, 0x11, 0x15, 0x10, 0xaa, 0xef, 0xa9, + 0x52, 0xd6, 0xc0, 0xb0, 0x77, 0x9e, 0x86, 0xc1, 0xcb, 0xb1, 0xab, 0x6b, 0xe5, 0xe8, 0x34, 0x5d, 0xf7, 0xb4, 0x40, + 0x81, 0x36, 0xa5, 0xb7, 0x76, 0x59, 0x8e, 0xb7, 0xea, 0x02, 0x17, 0x43, 0x0b, 0x9e, 0x3b, 0xef, 0x00, 0xbe, 0x4a, + 0x1e, 0x29, 0x3c, 0x58, 0xba, 0x76, 0x05, 0xb4, 0x30, 0x99, 0x04, 0x1e, 0x9c, 0xc5, 0x5a, 0x25, 0x6b, 0x51, 0xe1, + 0x35, 0x21, 0x0c, 0xc8, 0x59, 0x1f, 0x6c, 0xbb, 0x31, 0x72, 0x89, 0xda, 0xeb, 0x47, 0x1a, 0x5a, 0x64, 0xfd, 0xa0, + 0x49, 0xcf, 0x03, 0x45, 0xe5, 0xa8, 0x7a, 0x77, 0xa7, 0x8c, 0xbe, 0xc4, 0x3c, 0x61, 0xd4, 0x27, 0x06, 0x8d, 0xf4, + 0x85, 0x3a, 0x22, 0xe4, 0xfc, 0xc4, 0x66, 0xcd, 0x57, 0xfb, 0xf0, 0x9e, 0x10, 0xc6, 0x6a, 0xd3, 0x91, 0xcf, 0x13, + 0x68, 0xcf, 0x96, 0xae, 0x5f, 0xd4, 0x90, 0xe1, 0xb5, 0xe9, 0x72, 0x48, 0xc6, 0x82, 0xa7, 0x66, 0x08, 0x83, 0x5a, + 0xc9, 0x38, 0x4d, 0xec, 0x73, 0x16, 0x26, 0xd2, 0x55, 0xb9, 0x86, 0x00, 0xd7, 0x2f, 0x9c, 0x49, 0xb3, 0xd8, 0x72, + 0x8b, 0x92, 0xd1, 0xa5, 0x26, 0xc4, 0x16, 0x4d, 0x44, 0x06, 0x00, 0xbd, 0x1c, 0xf6, 0x11, 0x90, 0xf0, 0x6d, 0xc2, + 0xb9, 0x79, 0x62, 0x4b, 0x1b, 0xd7, 0x5c, 0x50, 0x18, 0xee, 0xe8, 0xc9, 0x5e, 0x6c, 0x2a, 0x62, 0xcf, 0x60, 0x1e, + 0x9a, 0x8d, 0x65, 0x36, 0x7f, 0xe4, 0xa7, 0xe3, 0x50, 0x0c, 0xa4, 0xff, 0xc0, 0x82, 0xf8, 0x9f, 0xa1, 0x42, 0x5c, + 0x71, 0x41, 0xfe, 0x80, 0x2b, 0x69, 0xf8, 0x82, 0x74, 0x3b, 0x9d, 0xf9, 0xd9, 0xf4, 0xa9, 0x5a, 0x40, 0x50, 0x1e, + 0x08, 0x85, 0x34, 0x17, 0x90, 0xc6, 0x0b, 0x1c, 0x58, 0x2f, 0xec, 0x90, 0x04, 0xb6, 0x9e, 0x8e, 0x64, 0xd2, 0x48, + 0xa7, 0x78, 0xe0, 0x53, 0xbd, 0xb6, 0x3f, 0xd5, 0x31, 0xa5, 0x37, 0xe5, 0x69, 0xd3, 0x3c, 0x15, 0x0f, 0x3d, 0x6b, + 0xab, 0x08, 0x13, 0x06, 0x4f, 0x85, 0x13, 0x5e, 0xef, 0xe9, 0x5a, 0xbb, 0x86, 0xaf, 0xe0, 0x8b, 0x9e, 0x0d, 0xe6, + 0xc2, 0xe6, 0x5a, 0x24, 0xe8, 0x20, 0x4c, 0x17, 0x3e, 0x3e, 0xc2, 0xc8, 0x74, 0x29, 0xbd, 0xa2, 0x1f, 0x0d, 0x0a, + 0xc5, 0xdb, 0xf5, 0x87, 0xf6, 0x2e, 0x82, 0x83, 0xb3, 0x05, 0xd9, 0x98, 0x76, 0x07, 0x20, 0x0f, 0x69, 0x51, 0xd5, + 0x18, 0x23, 0xa4, 0x42, 0x1c, 0x43, 0xc4, 0xe9, 0xf6, 0x55, 0x5b, 0x1e, 0xba, 0xe5, 0x97, 0x3c, 0x23, 0xff, 0x5e, + 0xfc, 0x99, 0xf9, 0xae, 0x6f, 0xd0, 0x15, 0xd7, 0x79, 0x0e, 0xf1, 0xbd, 0xdf, 0xb5, 0x46, 0x42, 0x94, 0x84, 0x7f, + 0x0c, 0x1e, 0x20, 0x66, 0x3c, 0x58, 0x03, 0xf6, 0xbc, 0xba, 0x91, 0x93, 0xe0, 0xbe, 0x60, 0xe8, 0x6d, 0xf3, 0xb5, + 0x7e, 0x3c, 0x26, 0xf1, 0x16, 0x6d, 0x11, 0xbb, 0x52, 0x07, 0x33, 0x76, 0xe2, 0x9c, 0x0f, 0x93, 0xd9, 0x7f, 0x8c, + 0xb0, 0xc0, 0x11, 0x0a, 0x6a, 0x2d, 0xfc, 0xb2, 0x15, 0xc0, 0xad, 0xfe, 0x83, 0x91, 0x02, 0x37, 0xd1, 0x13, 0x3f, + 0xdb, 0x3d, 0xc5, 0x26, 0x38, 0x11, 0x7b, 0x45, 0x6c, 0xcf, 0x81, 0x5a, 0xad, 0x6a, 0x0a, 0xd5, 0xad, 0xd3, 0x41, + 0xe8, 0x62, 0x51, 0x98, 0xeb, 0x75, 0x14, 0xf8, 0xac, 0x5a, 0x56, 0x1d, 0x86, 0x6c, 0x57, 0xa1, 0xf6, 0x24, 0x1b, + 0x16, 0x25, 0x2a, 0x72, 0xe3, 0x78, 0x53, 0xac, 0x03, 0xea, 0xb7, 0x7a, 0x6d, 0x82, 0x5b, 0x2f, 0x78, 0x74, 0x2c, + 0xc8, 0xb5, 0x14, 0x31, 0x78, 0x82, 0xc8, 0xe0, 0x55, 0xb9, 0x40, 0x07, 0xbd, 0x74, 0x5f, 0x37, 0x1f, 0x5a, 0xe3, + 0xe9, 0x6e, 0x1a, 0x3e, 0xfb, 0xb9, 0xf7, 0xd6, 0x88, 0xed, 0x9a, 0x31, 0x32, 0x2e, 0x92, 0x16, 0x3d, 0x75, 0x8d, + 0xcb, 0x35, 0x98, 0x3d, 0xb4, 0x3a, 0x66, 0x98, 0xbf, 0x5c, 0x69, 0x31, 0xc6, 0xef, 0x44, 0x31, 0xed, 0x41, 0x37, + 0x2b, 0xc4, 0x3d, 0xbd, 0x60, 0xc0, 0x5a, 0x4b, 0xbc, 0x69, 0xf5, 0x56, 0x5b, 0x9f, 0x2d, 0xcb, 0x20, 0xfa, 0x46, + 0x53, 0xbe, 0x9b, 0x85, 0x2c, 0x97, 0x29, 0xd6, 0x68, 0x13, 0xf6, 0xe5, 0x72, 0x6f, 0x37, 0xb6, 0x95, 0xf1, 0x6f, + 0x51, 0xf5, 0x64, 0x48, 0x24, 0x2d, 0x51, 0x2a, 0x15, 0x38, 0xe9, 0xc2, 0x10, 0x6b, 0x3a, 0x6a, 0xb9, 0x4e, 0x82, + 0xf9, 0xbe, 0x3b, 0x75, 0x58, 0xfe, 0xf8, 0x9c, 0x17, 0x69, 0xe5, 0x93, 0x22, 0xf6, 0xd9, 0xe1, 0x62, 0x42, 0x39, + 0x85, 0x33, 0xb2, 0xfb, 0x6f, 0x78, 0xb5, 0x2b, 0x80, 0x9a, 0x60, 0xf4, 0x72, 0xc9, 0xd5, 0x50, 0x94, 0x7e, 0x3a, + 0x19, 0xa6, 0x20, 0xac, 0xaf, 0xd6, 0xc2, 0x6b, 0xaf, 0x48, 0x74, 0x89, 0xbf, 0x92, 0x5e, 0x1a, 0x82, 0xa4, 0xed, + 0x50, 0x5f, 0xd5, 0x25, 0x08, 0x74, 0x88, 0x57, 0x12, 0xe0, 0x66, 0xde, 0x82, 0x26, 0x13, 0x19, 0x17, 0x6f, 0x5c, + 0x00, 0x17, 0xc6, 0xdb, 0xa7, 0x1b, 0x48, 0xd6, 0x5a, 0x62, 0x27, 0xa1, 0x9b, 0x5e, 0x1a, 0x9c, 0x00, 0x09, 0x76, + 0x3c, 0x81, 0x26, 0xef, 0x84, 0xcf, 0x5c, 0xaf, 0x26, 0xa6, 0x20, 0x88, 0xe8, 0xde, 0x73, 0xb0, 0x9b, 0xeb, 0x59, + 0x56, 0xd8, 0x84, 0xd8, 0xec, 0xa8, 0xfa, 0x7e, 0xaa, 0xc0, 0xeb, 0xa5, 0x49, 0xc5, 0x46, 0xa1, 0xeb, 0xe4, 0x0e, + 0xc7, 0x01, 0xa6, 0xb3, 0xe4, 0x50, 0xc3, 0x95, 0x8f, 0x65, 0x39, 0x49, 0x09, 0x2d, 0x85, 0x03, 0xce, 0x40, 0x72, + 0xf0, 0x3f, 0x96, 0x74, 0x90, 0x75, 0xf8, 0x89, 0x69, 0x0b, 0xfe, 0x4c, 0x5a, 0xd3, 0xb4, 0x88, 0x56, 0x7b, 0x1d, + 0x6b, 0xd0, 0xbc, 0x4a, 0x9e, 0x4f, 0x0c, 0x60, 0xb3, 0x5a, 0xc8, 0xea, 0xc7, 0x5e, 0x5b, 0xfe, 0x48, 0xf9, 0x29, + 0x0b, 0xb5, 0xa7, 0x7a, 0x6c, 0x85, 0x64, 0xa7, 0x69, 0x51, 0x11, 0xc5, 0xf5, 0x64, 0xbb, 0x21, 0x7e, 0xf8, 0x22, + 0x11, 0x94, 0x4b, 0x05, 0xc4, 0x90, 0x00, 0x04, 0x83, 0x19, 0xd4, 0x90, 0xd0, 0x51, 0x5f, 0x6f, 0x9e, 0x8e, 0x7b, + 0x08, 0x34, 0x4f, 0x85, 0x02, 0x62, 0xba, 0x62, 0x76, 0xbe, 0x0b, 0xa8, 0xe2, 0xfd, 0x1b, 0x6c, 0x9b, 0x56, 0xdf, + 0xd6, 0xb4, 0xca, 0x4f, 0xd6, 0x7f, 0xd4, 0xb9, 0x29, 0xb0, 0x21, 0x36, 0xa8, 0x52, 0x24, 0xac, 0x32, 0x06, 0x88, + 0x46, 0xcf, 0xdc, 0x64, 0x9a, 0xc2, 0xfe, 0xee, 0x3c, 0x5d, 0xf5, 0x75, 0x6a, 0xf3, 0x5d, 0xcf, 0xa5, 0xc4, 0x12, + 0x2e, 0xb3, 0xd0, 0xc7, 0x72, 0x00, 0x64, 0xa6, 0x87, 0xa5, 0x83, 0x06, 0x5f, 0x83, 0x57, 0x57, 0x2c, 0x55, 0xd7, + 0xec, 0x7e, 0xc8, 0xf8, 0xeb, 0x9b, 0xf4, 0x8a, 0xde, 0xc9, 0xc8, 0x7c, 0x73, 0xaf, 0x77, 0xd7, 0xea, 0xfa, 0x85, + 0xf5, 0x8c, 0xba, 0x54, 0x2d, 0x4f, 0x7f, 0x6f, 0xf7, 0x7d, 0x71, 0x67, 0xed, 0x4f, 0x41, 0x19, 0xdb, 0x93, 0x7c, + 0xa0, 0x9a, 0x1b, 0xff, 0x02, 0xcd, 0x9b, 0x82, 0x5a, 0x46, 0xa6, 0xbc, 0xad, 0xfd, 0x92, 0x1b, 0xf2, 0xf6, 0x44, + 0xc6, 0x11, 0xe7, 0x8e, 0x21, 0xef, 0x4b, 0xdb, 0xf8, 0xdc, 0xeb, 0x08, 0x14, 0x7e, 0x79, 0x3a, 0xa5, 0x80, 0xd6, + 0x84, 0x4b, 0xc4, 0x11, 0x5a, 0x5e, 0x97, 0x2e, 0x8a, 0x41, 0xe4, 0xe8, 0x03, 0xd8, 0xd2, 0x86, 0xe0, 0xd3, 0x22, + 0xfc, 0x6c, 0x26, 0xd4, 0x93, 0xad, 0x40, 0xad, 0x88, 0x2a, 0x7b, 0x48, 0x56, 0x02, 0xcb, 0x89, 0xe4, 0xa4, 0x27, + 0x75, 0x26, 0x90, 0x60, 0xea, 0x15, 0xef, 0xbb, 0x60, 0xc8, 0x62, 0x97, 0x2b, 0x0c, 0x2c, 0xe2, 0x64, 0xa1, 0x7e, + 0xbd, 0x3c, 0x95, 0x46, 0x0b, 0x0c, 0x01, 0x4c, 0x73, 0x2f, 0x2f, 0x1b, 0x23, 0x9e, 0xfe, 0xee, 0x86, 0x4c, 0x17, + 0x78, 0xf0, 0xcd, 0x8b, 0x9b, 0xd4, 0x52, 0x80, 0x9e, 0x9b, 0xfc, 0x6e, 0xa4, 0x9d, 0xc8, 0x09, 0xa9, 0xcd, 0x19, + 0x0e, 0x01, 0xaa, 0x9a, 0x3d, 0xc4, 0x5c, 0x2a, 0x65, 0x27, 0xae, 0x81, 0x2c, 0xbf, 0x89, 0xc0, 0x97, 0x6f, 0xe7, + 0xd8, 0x3b, 0x15, 0x95, 0xad, 0xd0, 0x0e, 0xa1, 0xa2, 0x36, 0xac, 0xee, 0xe6, 0xe1, 0x31, 0x47, 0xb0, 0xf3, 0x87, + 0x79, 0xdc, 0xd7, 0x0d, 0x8f, 0x10, 0x60, 0x05, 0xc2, 0x27, 0x04, 0x1f, 0x60, 0x88, 0x66, 0xba, 0xb5, 0xef, 0xef, + 0x55, 0x52, 0x55, 0x3c, 0x05, 0x38, 0x3e, 0xc0, 0xf0, 0xce, 0xd4, 0x63, 0xb3, 0x04, 0x9b, 0x79, 0x04, 0x86, 0x90, + 0x9b, 0xe6, 0x54, 0x53, 0x6e, 0x80, 0xf9, 0x2e, 0x62, 0x98, 0xe2, 0x91, 0xee, 0xd1, 0xf0, 0x01, 0xed, 0xc6, 0x9b, + 0x3b, 0x2f, 0xf0, 0xd3, 0x2c, 0x62, 0xd9, 0xf3, 0x64, 0x94, 0xc1, 0x27, 0x22, 0xdf, 0x22, 0x85, 0xcc, 0xfd, 0xc4, + 0x29, 0xac, 0xb6, 0x69, 0x7d, 0x51, 0x88, 0xdc, 0x5c, 0xdd, 0x98, 0x68, 0x0d, 0x5c, 0xa8, 0x4d, 0x54, 0x27, 0xd0, + 0xda, 0x66, 0x7b, 0xb8, 0xea, 0x4c, 0x24, 0x83, 0x27, 0xc2, 0xfc, 0x1b, 0xaf, 0xee, 0x64, 0xeb, 0x90, 0x8b, 0xd3, + 0xa3, 0x30, 0x57, 0x7b, 0x6b, 0xcf, 0x5b, 0xf7, 0x2d, 0x77, 0xd5, 0x9a, 0x3c, 0xa7, 0x45, 0x28, 0xb1, 0x93, 0x0c, + 0xa0, 0x08, 0xee, 0x9b, 0x41, 0xef, 0x3d, 0xd4, 0x89, 0x0c, 0x2e, 0x54, 0x31, 0xe3, 0xcc, 0x38, 0xca, 0xf3, 0x2b, + 0xae, 0x39, 0xb8, 0xfd, 0xbc, 0x71, 0x31, 0x10, 0xa0, 0xd0, 0x01, 0x99, 0xfa, 0x51, 0x99, 0xda, 0x9a, 0x26, 0xc7, + 0x7c, 0x05, 0x0b, 0x44, 0x86, 0x20, 0x00, 0x59, 0x78, 0xda, 0x56, 0xe9, 0x3e, 0x9e, 0x0c, 0x07, 0xca, 0x1b, 0x81, + 0x19, 0x19, 0x74, 0x10, 0xcd, 0x58, 0xdb, 0x99, 0x44, 0x44, 0x98, 0x84, 0x1b, 0x8b, 0x1a, 0xfe, 0xc5, 0x53, 0x52, + 0x3e, 0xe6, 0xa1, 0x87, 0x11, 0xd3, 0x62, 0x5e, 0x51, 0x7c, 0x49, 0x41, 0x3a, 0x97, 0x56, 0xdf, 0xb2, 0x4c, 0xce, + 0xa9, 0x97, 0xa1, 0xd0, 0x45, 0xc2, 0xa8, 0xb0, 0x49, 0x3d, 0x91, 0x01, 0x24, 0x63, 0x95, 0x19, 0xca, 0x15, 0x5e, + 0x8f, 0x2a, 0x79, 0x5c, 0xf2, 0x6f, 0xcc, 0xca, 0xb8, 0x1c, 0x5b, 0xd6, 0x0d, 0xeb, 0x1c, 0x1c, 0xaf, 0x54, 0xcb, + 0xe4, 0x9b, 0xa2, 0x38, 0xf1, 0xe2, 0x23, 0x06, 0xe2, 0xfd, 0xac, 0xde, 0x66, 0x9e, 0x7d, 0x58, 0xee, 0xda, 0xc2, + 0x95, 0x49, 0xc5, 0x20, 0x96, 0x30, 0x11, 0xb4, 0x28, 0x8d, 0xdf, 0x71, 0x30, 0xc5, 0x29, 0x40, 0x1b, 0x0b, 0xdf, + 0x1b, 0x49, 0x55, 0xe5, 0xb0, 0x5c, 0x46, 0x6f, 0xa5, 0xa8, 0xb3, 0x59, 0x5e, 0x46, 0x9b, 0x79, 0x12, 0x10, 0xe0, + 0xea, 0x4c, 0x59, 0xcd, 0x6e, 0x0e, 0x1d, 0x86, 0x33, 0xac, 0x2c, 0xe5, 0x84, 0x29, 0x9a, 0x35, 0x96, 0x12, 0x61, + 0xdc, 0x66, 0xb7, 0x2f, 0x8e, 0xdf, 0xd5, 0x72, 0x67, 0xfa, 0x0d, 0xdc, 0xe5, 0xae, 0x59, 0x40, 0x78, 0xe0, 0x11, + 0x9d, 0x93, 0xcb, 0x80, 0xaf, 0x8c, 0xea, 0x0d, 0x1a, 0xb0, 0x25, 0xeb, 0xa5, 0xf9, 0x58, 0x95, 0x87, 0xbe, 0x8a, + 0x5d, 0xbc, 0xd4, 0x25, 0xb4, 0x3a, 0xd4, 0xfa, 0xb0, 0xb7, 0xff, 0xb4, 0x57, 0xed, 0x34, 0xa0, 0x03, 0x62, 0x5f, + 0xeb, 0xf1, 0x65, 0x97, 0xff, 0xd5, 0x1f, 0xb7, 0x45, 0xa2, 0xed, 0x94, 0xba, 0x81, 0x0a, 0x41, 0xee, 0x40, 0xb0, + 0x95, 0xce, 0x67, 0xe5, 0x38, 0xe8, 0x85, 0x25, 0xa1, 0x16, 0x5e, 0x97, 0x97, 0x4a, 0xf0, 0x60, 0x4a, 0x49, 0xac, + 0x71, 0xaf, 0x37, 0x87, 0x01, 0x7d, 0xb8, 0xc5, 0x5a, 0x4d, 0x4c, 0x7f, 0x42, 0x54, 0x99, 0x48, 0x0f, 0x6c, 0x2f, + 0x9a, 0x98, 0xf0, 0xb0, 0x1f, 0x54, 0xa4, 0x84, 0xea, 0x40, 0xd0, 0x06, 0xca, 0xc4, 0x1c, 0x5f, 0x76, 0x28, 0x79, + 0x2e, 0xb4, 0xc0, 0x27, 0x06, 0xfb, 0x8e, 0xab, 0xb1, 0x50, 0xb1, 0x03, 0xc9, 0x31, 0x65, 0x0e, 0x37, 0xd8, 0x22, + 0xf6, 0x27, 0xd5, 0x40, 0xe9, 0xaf, 0xc6, 0x75, 0xdf, 0x56, 0x01, 0x94, 0xba, 0xe6, 0xc7, 0x7d, 0x8d, 0x42, 0x0f, + 0x16, 0xf1, 0x76, 0x08, 0xcf, 0x64, 0xbb, 0xa6, 0x22, 0xd6, 0x7c, 0x96, 0xec, 0xb9, 0x61, 0xc3, 0xdf, 0x57, 0x04, + 0x32, 0x46, 0x9a, 0x0e, 0x65, 0x6c, 0xc6, 0x2f, 0x65, 0x14, 0x53, 0x84, 0x7d, 0xe1, 0x77, 0x92, 0x10, 0x21, 0x42, + 0xc6, 0x30, 0xcd, 0x11, 0xb4, 0x33, 0x9f, 0x27, 0xb5, 0x40, 0x75, 0x4d, 0x42, 0xdf, 0xd3, 0xc3, 0x8a, 0x78, 0x90, + 0xa3, 0x47, 0x25, 0x00, 0xea, 0xbf, 0xc5, 0xbd, 0x27, 0x59, 0x31, 0x82, 0xb4, 0xe2, 0x44, 0x1a, 0x57, 0xe0, 0x38, + 0xc7, 0x27, 0x2d, 0x24, 0x88, 0x97, 0xea, 0x4e, 0x42, 0x5f, 0xb4, 0x71, 0x6a, 0xf0, 0x02, 0xb9, 0x28, 0x56, 0x2a, + 0x00, 0xb5, 0x5b, 0xf0, 0x66, 0x09, 0x33, 0x66, 0x48, 0x8f, 0xbc, 0x07, 0x6b, 0x1e, 0xf2, 0x52, 0x2e, 0x8f, 0x39, + 0x39, 0x87, 0xa8, 0xb9, 0x28, 0x92, 0x1a, 0x73, 0x05, 0x7d, 0x0d, 0x8a, 0x53, 0xe8, 0x63, 0x4c, 0xac, 0x36, 0x4f, + 0x7d, 0xaa, 0x86, 0xa2, 0xf4, 0x6c, 0x56, 0x17, 0xeb, 0x88, 0x2d, 0xb0, 0x0b, 0xcd, 0x18, 0x82, 0x5f, 0xc9, 0x24, + 0x87, 0x83, 0xb4, 0x4c, 0x04, 0x1d, 0x95, 0x17, 0x43, 0x27, 0x33, 0xda, 0xbb, 0xf4, 0x84, 0x3b, 0x7a, 0x28, 0x39, + 0x7d, 0x81, 0xd2, 0x43, 0x08, 0xd0, 0x5f, 0x8d, 0x68, 0xdc, 0xfe, 0x0a, 0x27, 0xc5, 0x8b, 0x09, 0x1f, 0x24, 0x51, + 0x84, 0x87, 0x70, 0x46, 0x14, 0x32, 0x12, 0xed, 0x43, 0xc1, 0xcc, 0x3b, 0xdb, 0xd6, 0x94, 0xf7, 0x45, 0x9d, 0x3a, + 0xcd, 0xc1, 0xcb, 0xf7, 0xe2, 0xb5, 0x5c, 0x4e, 0x3d, 0x7a, 0xec, 0xcb, 0x96, 0x90, 0x9d, 0x07, 0x00, 0x02, 0xe4, + 0x8b, 0x1d, 0x32, 0x26, 0x68, 0xc3, 0x9a, 0x96, 0x64, 0x4d, 0x3f, 0x5a, 0x84, 0x7e, 0x54, 0x7d, 0x9c, 0x66, 0x99, + 0x90, 0x6a, 0x0b, 0x63, 0x40, 0x84, 0x9e, 0x2a, 0x94, 0x60, 0x45, 0xee, 0x83, 0x97, 0xb8, 0x9a, 0x00, 0xdb, 0xb6, + 0x18, 0x9e, 0xb4, 0x37, 0x43, 0x60, 0x3b, 0x22, 0xa0, 0xd3, 0x0c, 0x89, 0x42, 0x6c, 0xb8, 0x8f, 0xd1, 0x4c, 0x52, + 0xc1, 0x98, 0x26, 0x2a, 0x1f, 0xfc, 0x07, 0xb5, 0x11, 0x37, 0x69, 0xaf, 0xe2, 0x79, 0x84, 0x3d, 0xc7, 0xa1, 0xeb, + 0xc2, 0x65, 0x40, 0x54, 0xd9, 0x72, 0x59, 0x73, 0x3d, 0x5a, 0x9e, 0x91, 0x41, 0x95, 0x48, 0xfd, 0x85, 0x5b, 0x07, + 0x95, 0x06, 0xd4, 0xb3, 0xf8, 0x64, 0xe0, 0xb9, 0x25, 0xb4, 0xdc, 0x9f, 0x23, 0x89, 0x07, 0xe0, 0xd4, 0xa3, 0x39, + 0xc2, 0x4b, 0x77, 0x87, 0x00, 0xf7, 0x56, 0x75, 0xbb, 0x69, 0x09, 0x28, 0x63, 0x27, 0xe1, 0xaa, 0xad, 0x52, 0x92, + 0x5a, 0x83, 0x12, 0xf3, 0xef, 0xf2, 0x4b, 0x3d, 0x76, 0x15, 0x1b, 0x96, 0x21, 0xd0, 0xb5, 0x42, 0xfd, 0xe5, 0x13, + 0xda, 0x49, 0xe1, 0xc6, 0xe1, 0x0d, 0xb2, 0x68, 0xf3, 0x11, 0xb5, 0x60, 0x2e, 0x50, 0x77, 0x5c, 0xd4, 0xbd, 0xf9, + 0x1b, 0xc1, 0x4d, 0x51, 0x53, 0xe8, 0x42, 0xc9, 0x46, 0x8f, 0x37, 0x12, 0x33, 0x40, 0x73, 0xb9, 0xd2, 0x0a, 0xcf, + 0xaa, 0x07, 0x6a, 0xbf, 0x21, 0x71, 0x6b, 0xbd, 0xbe, 0x0d, 0x1b, 0x3d, 0x44, 0xab, 0xc9, 0x82, 0x36, 0x46, 0x92, + 0xc7, 0xcc, 0xa1, 0xb5, 0x22, 0xd3, 0x35, 0x49, 0xb0, 0x2c, 0xa9, 0xf5, 0x6a, 0xd7, 0xf0, 0xf3, 0xb7, 0x3e, 0x40, + 0x58, 0x30, 0xb0, 0x5a, 0x49, 0xef, 0xb0, 0xdd, 0xca, 0xa5, 0x85, 0xab, 0x4d, 0xfe, 0x2c, 0x95, 0x43, 0x40, 0x9b, + 0x2c, 0xbf, 0xc4, 0xa5, 0xa7, 0x28, 0x88, 0xd4, 0x69, 0xab, 0xab, 0x84, 0x84, 0x60, 0xa5, 0x52, 0x3f, 0x1d, 0x98, + 0x90, 0x23, 0x2a, 0x47, 0x64, 0xf7, 0xba, 0x9c, 0xf3, 0x53, 0x03, 0xd2, 0xdd, 0x88, 0x48, 0xc8, 0xe9, 0x8d, 0x01, + 0x5c, 0x16, 0x1a, 0xfb, 0xdb, 0x80, 0x2b, 0x7c, 0x88, 0xe0, 0xb4, 0xef, 0x4a, 0xb9, 0x2e, 0x82, 0xfb, 0xbe, 0x40, + 0x8a, 0xaa, 0x22, 0x82, 0x05, 0xd5, 0x8e, 0x6c, 0xce, 0x8e, 0xfc, 0xc6, 0x8c, 0x02, 0xe7, 0xe6, 0x78, 0xd7, 0x28, + 0x42, 0xe9, 0x62, 0xe7, 0xbe, 0x62, 0x20, 0x4a, 0x12, 0x3e, 0x3b, 0x46, 0x68, 0xad, 0x75, 0x3e, 0xf1, 0x7e, 0xc0, + 0xb3, 0x24, 0x9c, 0x7f, 0x60, 0x93, 0xf7, 0xa5, 0x38, 0x2f, 0xaf, 0x36, 0x75, 0x5b, 0x30, 0x02, 0x50, 0x5f, 0x78, + 0xde, 0x56, 0x1e, 0xdc, 0x60, 0x64, 0x90, 0x27, 0x73, 0x81, 0xf1, 0xcc, 0xd5, 0x60, 0x9e, 0x1f, 0x3b, 0x2a, 0x04, + 0x2c, 0x04, 0xf2, 0x54, 0x53, 0x9b, 0xd6, 0x4a, 0x6c, 0xd1, 0x8e, 0xd9, 0x6f, 0xd9, 0x00, 0x27, 0xc0, 0xe9, 0x70, + 0xbc, 0xb4, 0x0d, 0xde, 0x90, 0x4b, 0x7a, 0x6b, 0x19, 0x05, 0xd9, 0x85, 0x7f, 0x1b, 0xb4, 0x06, 0xe5, 0x15, 0x08, + 0x15, 0x49, 0x1d, 0x1b, 0x25, 0xa5, 0x48, 0x1a, 0xa1, 0x65, 0xb6, 0x05, 0x59, 0x71, 0xb6, 0x47, 0x7c, 0xd5, 0xcc, + 0xe1, 0xa6, 0xc8, 0x6d, 0x91, 0xce, 0x1a, 0xee, 0x8b, 0x40, 0xc5, 0xa6, 0x90, 0x66, 0x5a, 0x23, 0xdb, 0xb8, 0x27, + 0xab, 0xb4, 0x77, 0x1b, 0x51, 0x33, 0x68, 0x44, 0xdf, 0xd2, 0x54, 0xf9, 0x7d, 0x2d, 0xaa, 0x97, 0x62, 0xa0, 0xcc, + 0x21, 0xa6, 0x6b, 0x5a, 0xc1, 0xa4, 0x4a, 0x2d, 0x8e, 0xf3, 0x36, 0x9f, 0x3e, 0x5c, 0x28, 0x87, 0xe4, 0xc0, 0x09, + 0x25, 0x47, 0x0c, 0xd9, 0x0a, 0x43, 0x70, 0x2b, 0x67, 0x13, 0xc9, 0x72, 0x23, 0x72, 0x99, 0x35, 0x46, 0x77, 0xfc, + 0x83, 0x05, 0xa0, 0xd0, 0x17, 0x1b, 0x14, 0xf4, 0x63, 0xad, 0xf5, 0x89, 0x3a, 0x52, 0x6a, 0x52, 0x7c, 0xba, 0x70, + 0x13, 0x95, 0x43, 0xcd, 0xd5, 0xab, 0xa2, 0x01, 0xb5, 0x26, 0x74, 0xc0, 0xf5, 0x08, 0x83, 0x0d, 0x84, 0xd1, 0x1f, + 0x4d, 0x21, 0x2c, 0xf7, 0x55, 0xdc, 0xb4, 0x9b, 0xbc, 0x7b, 0x3a, 0xdb, 0x63, 0xa4, 0x06, 0x15, 0x69, 0x59, 0x71, + 0x0c, 0xa7, 0x07, 0x9c, 0x83, 0xc7, 0x8e, 0x19, 0x36, 0x1b, 0xa7, 0xc7, 0x18, 0x03, 0x2c, 0x59, 0x61, 0xb1, 0x4d, + 0xa5, 0xb5, 0x22, 0x42, 0x6a, 0x9b, 0xd5, 0x4b, 0x9b, 0x3b, 0x45, 0x7e, 0xfb, 0x33, 0x00, 0xcc, 0xab, 0x26, 0xd3, + 0x3a, 0x8a, 0x29, 0x62, 0x94, 0xb4, 0x59, 0x1c, 0x2f, 0xc4, 0xca, 0x8b, 0x8f, 0x05, 0xee, 0x8f, 0x54, 0xb9, 0xb2, + 0xec, 0xb8, 0x3a, 0x93, 0xfb, 0xe1, 0xe6, 0x87, 0xcc, 0x49, 0xc4, 0x03, 0x16, 0xfa, 0x8c, 0xd9, 0x70, 0x75, 0xe2, + 0x1d, 0xa9, 0xc3, 0x2c, 0x26, 0xf7, 0xba, 0x78, 0xcb, 0xc7, 0xb9, 0x0b, 0xa8, 0xec, 0x41, 0xec, 0xb6, 0x2a, 0x63, + 0xbd, 0xce, 0xc8, 0x20, 0xe1, 0x5b, 0x2a, 0xf6, 0x4a, 0xc6, 0x4e, 0x7c, 0x06, 0x99, 0x1e, 0x2c, 0xc3, 0xc2, 0x53, + 0x46, 0x72, 0xfb, 0x4c, 0x15, 0xb5, 0xeb, 0x29, 0x95, 0xeb, 0xa2, 0x3b, 0xaf, 0xb9, 0xb7, 0x15, 0xee, 0xd4, 0xcc, + 0xa4, 0x13, 0xaf, 0x0b, 0x50, 0xe7, 0x83, 0x97, 0x16, 0xe9, 0x9c, 0x37, 0xb0, 0x6a, 0x85, 0xc2, 0x75, 0xa9, 0x46, + 0x9f, 0x5d, 0xee, 0xa3, 0x2d, 0x8e, 0x4d, 0x77, 0x7e, 0x5d, 0xf6, 0x68, 0xf2, 0x59, 0x87, 0x40, 0xec, 0x29, 0x22, + 0x3e, 0xa5, 0xc1, 0xad, 0x75, 0x98, 0x69, 0xab, 0xad, 0x0c, 0x54, 0x9b, 0xa4, 0x16, 0xf8, 0x49, 0x9b, 0xd2, 0xec, + 0x70, 0x6a, 0x79, 0xd7, 0x20, 0x96, 0xf8, 0x05, 0x0e, 0xab, 0x62, 0xf5, 0xec, 0xf1, 0x2d, 0xae, 0xac, 0x0c, 0x73, + 0xbf, 0x1e, 0x55, 0x0e, 0xb3, 0xb9, 0xe2, 0x78, 0x53, 0x1d, 0x91, 0x48, 0x6d, 0x3f, 0xf7, 0xf3, 0x27, 0x43, 0x45, + 0x8f, 0x83, 0x81, 0x38, 0x50, 0x55, 0xe4, 0x4c, 0x89, 0xb0, 0x0a, 0xa7, 0x25, 0x9a, 0x86, 0xc6, 0x3a, 0x14, 0x04, + 0x64, 0xd4, 0xff, 0x81, 0x70, 0x10, 0x99, 0xb7, 0x4e, 0x48, 0xaa, 0x2a, 0x35, 0x2c, 0xd1, 0x5e, 0xec, 0x7b, 0x48, + 0xe1, 0x21, 0x4f, 0xb6, 0x3e, 0x6f, 0xbf, 0xce, 0x91, 0x05, 0x0f, 0x04, 0xa3, 0x4c, 0x12, 0x03, 0x5b, 0x47, 0x97, + 0x7a, 0xd9, 0x8b, 0xbb, 0x4c, 0x40, 0x4f, 0x77, 0x1e, 0x7f, 0x84, 0x43, 0x51, 0xda, 0x9c, 0xbf, 0x6a, 0x49, 0x36, + 0xf3, 0xe8, 0xb6, 0x6a, 0xac, 0x43, 0x24, 0x36, 0x97, 0x1c, 0x2d, 0xe7, 0x45, 0x9e, 0x72, 0x74, 0xf9, 0x00, 0x8c, + 0x85, 0x77, 0xe7, 0x5c, 0x35, 0x17, 0x52, 0x4d, 0x5f, 0x1c, 0x13, 0xb8, 0x0e, 0x8f, 0xd8, 0x4a, 0xdb, 0x06, 0xeb, + 0xc1, 0x72, 0x88, 0xe7, 0xdc, 0x50, 0xae, 0x3f, 0xd4, 0x92, 0x6a, 0x52, 0xcf, 0x60, 0x1a, 0x2b, 0x75, 0x82, 0x26, + 0x65, 0xce, 0x2b, 0x9e, 0x3a, 0x98, 0x3a, 0x74, 0x93, 0x44, 0xf4, 0xd7, 0x91, 0x39, 0x91, 0xa4, 0x49, 0x3f, 0xb6, + 0x8d, 0x0a, 0x08, 0x80, 0x8e, 0x56, 0x08, 0x68, 0xf7, 0xbd, 0x5b, 0x7d, 0x26, 0xc9, 0x87, 0x67, 0x3d, 0x8a, 0xb9, + 0xd6, 0xd1, 0x56, 0xd7, 0xb0, 0x7c, 0x7b, 0x45, 0x18, 0xcd, 0xdb, 0x03, 0xb3, 0xc2, 0xd9, 0x88, 0x14, 0x63, 0xe7, + 0x2d, 0x20, 0x61, 0x1e, 0x22, 0xc7, 0xbb, 0x2e, 0x6a, 0xdc, 0x4a, 0xe7, 0xe8, 0xbc, 0x08, 0x4f, 0x9b, 0x2b, 0x16, + 0x4a, 0xd1, 0x4b, 0xe5, 0xd8, 0x6f, 0xde, 0x99, 0x51, 0x43, 0x5e, 0xf2, 0xb0, 0xf1, 0x7e, 0x94, 0xa7, 0xa7, 0x30, + 0x3a, 0x3f, 0xc4, 0x61, 0xee, 0x48, 0x5f, 0xa9, 0x03, 0xf4, 0x7a, 0x4f, 0x0e, 0xdf, 0xae, 0xef, 0x65, 0x27, 0x38, + 0x5c, 0x18, 0x2e, 0x8a, 0xf3, 0x05, 0xa9, 0x24, 0xe6, 0x28, 0xf5, 0x78, 0x51, 0x4f, 0xf1, 0x81, 0x78, 0xe5, 0x04, + 0xdb, 0x5e, 0xf6, 0xfd, 0xdf, 0x85, 0x33, 0x29, 0xbf, 0xef, 0x2e, 0x81, 0xaf, 0x07, 0x7f, 0xe8, 0xf6, 0x0b, 0x1c, + 0x89, 0xc8, 0x61, 0x1c, 0xee, 0xd8, 0x56, 0x71, 0xbd, 0x6f, 0xc3, 0x16, 0xa9, 0xd7, 0x1f, 0x27, 0x84, 0x5c, 0x37, + 0xe4, 0xa8, 0x3b, 0x28, 0xe2, 0x65, 0x09, 0x4c, 0xdc, 0x14, 0x42, 0x14, 0xe3, 0xbf, 0x5c, 0xcd, 0x53, 0x84, 0x5f, + 0x35, 0xa2, 0xb0, 0x55, 0x53, 0x53, 0x70, 0x57, 0x60, 0x00, 0x56, 0xf2, 0x04, 0x77, 0xa0, 0xe5, 0x43, 0x59, 0x78, + 0x85, 0x8e, 0xd5, 0xa2, 0xac, 0x04, 0x6a, 0x99, 0x21, 0x8f, 0x08, 0x4e, 0xd0, 0x5e, 0x84, 0x59, 0xd7, 0x30, 0x29, + 0xf7, 0x60, 0xf2, 0xb6, 0x6e, 0xe1, 0x75, 0xb7, 0xa9, 0xd3, 0xc3, 0xfb, 0x55, 0x69, 0xb1, 0xab, 0xb2, 0xc7, 0x03, + 0xe4, 0x28, 0x39, 0xbd, 0x03, 0x30, 0xe7, 0x61, 0x12, 0xd8, 0xea, 0xd2, 0x1c, 0xb6, 0x76, 0x97, 0xd0, 0x6f, 0x33, + 0x7c, 0xba, 0x43, 0x66, 0xa3, 0xa4, 0x9d, 0x7d, 0xfe, 0x53, 0x05, 0x8b, 0xa1, 0x37, 0x00, 0x9e, 0xb0, 0xee, 0x64, + 0xb5, 0xb0, 0x81, 0x7b, 0xfc, 0xd3, 0x87, 0xa6, 0x28, 0xa4, 0x25, 0xa6, 0xb1, 0x8b, 0xa3, 0x9a, 0x6c, 0xad, 0xf6, + 0x1a, 0x39, 0xbb, 0x21, 0x71, 0x55, 0x4a, 0x88, 0x2e, 0x47, 0xb4, 0x42, 0xb2, 0x47, 0x14, 0xc1, 0x6a, 0xef, 0x2c, + 0xdd, 0x46, 0x5f, 0xc3, 0x74, 0x05, 0x18, 0x4d, 0xc0, 0xb0, 0x41, 0xa5, 0xbd, 0x13, 0x00, 0x18, 0xa5, 0x55, 0x53, + 0xa7, 0xf4, 0x2e, 0x76, 0xd9, 0xe5, 0xe6, 0x41, 0xa6, 0xd4, 0x13, 0x35, 0x93, 0xdb, 0x03, 0x2a, 0x6b, 0x2d, 0x54, + 0xb2, 0x5f, 0x72, 0xc5, 0xa7, 0x51, 0x89, 0x56, 0xe8, 0x6a, 0x46, 0x07, 0xdd, 0x4c, 0xd1, 0x51, 0x22, 0xb6, 0x4c, + 0x4c, 0xbb, 0xf2, 0x66, 0x98, 0x78, 0xa9, 0xd8, 0x2a, 0x33, 0x22, 0x4d, 0xd9, 0xa2, 0x96, 0x23, 0xe2, 0xfc, 0xa8, + 0xbd, 0x66, 0x55, 0xa7, 0x36, 0xd6, 0x5a, 0x78, 0xba, 0x38, 0xc4, 0xe4, 0xea, 0x43, 0xb4, 0xdd, 0x07, 0x29, 0x38, + 0xd3, 0xa6, 0x8d, 0x2b, 0xb5, 0xcd, 0xbe, 0x88, 0x32, 0x5e, 0x91, 0x71, 0x11, 0xb3, 0xd9, 0xed, 0x93, 0xa5, 0x1d, + 0x26, 0xca, 0xe3, 0x98, 0x4c, 0x46, 0x0e, 0x54, 0xd2, 0x06, 0xaa, 0x25, 0xf7, 0x92, 0x15, 0x17, 0x71, 0xf1, 0xdf, + 0x68, 0xd9, 0xe6, 0xd9, 0xc2, 0xc0, 0x82, 0x16, 0x66, 0x89, 0x02, 0xb3, 0x54, 0x4a, 0x07, 0x25, 0x1c, 0x45, 0x64, + 0x27, 0x09, 0xd3, 0xcb, 0x92, 0x36, 0xf8, 0xa0, 0x91, 0xee, 0x4e, 0x26, 0x0d, 0x09, 0x97, 0x6b, 0x9c, 0xb5, 0x2d, + 0x26, 0x32, 0xe2, 0xa9, 0x6f, 0x4a, 0x26, 0xc2, 0x48, 0x3c, 0xc8, 0x94, 0x98, 0x0b, 0xcf, 0x06, 0x52, 0xe2, 0x8b, + 0x9c, 0x7e, 0xae, 0x17, 0xb3, 0xd1, 0x22, 0x8d, 0xfe, 0xa1, 0xe7, 0x97, 0xc5, 0xce, 0x6e, 0x47, 0x8c, 0x7a, 0x7b, + 0x5c, 0x79, 0x56, 0x53, 0x6b, 0xd7, 0x8c, 0x1c, 0x33, 0x06, 0x34, 0x52, 0x08, 0x14, 0xd2, 0x27, 0x23, 0x9c, 0x16, + 0x97, 0x03, 0x1b, 0x36, 0xbe, 0x53, 0x8e, 0x67, 0x0a, 0xb7, 0x17, 0x43, 0xc3, 0x73, 0x87, 0x44, 0x10, 0xa1, 0xf1, + 0x06, 0x67, 0xce, 0x50, 0xff, 0xe1, 0xe9, 0xbc, 0x35, 0xd7, 0xfd, 0x0f, 0x8a, 0x9e, 0xa5, 0x45, 0x44, 0x80, 0xf3, + 0x45, 0x45, 0x9a, 0xdb, 0x7b, 0x27, 0x8b, 0xac, 0xc7, 0x37, 0xcd, 0xfa, 0x15, 0x01, 0xdc, 0x49, 0x02, 0x42, 0x80, + 0x86, 0xd7, 0xf5, 0x7c, 0x38, 0x4b, 0x58, 0x1e, 0x60, 0xba, 0xab, 0xe0, 0xef, 0xc4, 0x4d, 0xce, 0x4b, 0x13, 0xfa, + 0xb1, 0xa8, 0xe0, 0x83, 0x9d, 0x2c, 0x10, 0x6e, 0x01, 0x96, 0x10, 0x04, 0x82, 0x92, 0x99, 0x29, 0xa6, 0x12, 0xfa, + 0x0b, 0x29, 0x21, 0x43, 0x02, 0x4c, 0x47, 0xe3, 0x82, 0x1b, 0x24, 0xd5, 0x46, 0x3c, 0xad, 0x62, 0x36, 0x9c, 0x34, + 0x0c, 0x88, 0xf5, 0xc7, 0x30, 0xd8, 0x2a, 0x26, 0xc9, 0xb0, 0xbf, 0xb3, 0x37, 0x9e, 0x0c, 0xa7, 0x4b, 0x14, 0x72, + 0xb1, 0xcf, 0x98, 0x3c, 0xa1, 0xe9, 0x17, 0x85, 0xa8, 0x2f, 0xeb, 0x82, 0xd3, 0x7b, 0x76, 0x74, 0x07, 0x4f, 0xae, + 0x32, 0xd2, 0xdb, 0x38, 0xb0, 0xdc, 0x42, 0x22, 0xc0, 0xbc, 0xdf, 0x03, 0xcd, 0x48, 0x32, 0x64, 0x28, 0x03, 0xcc, + 0x35, 0x66, 0x4f, 0x0d, 0x4d, 0x0f, 0x65, 0x47, 0x72, 0x6d, 0x12, 0xac, 0x1e, 0xe6, 0xbe, 0xbc, 0xb2, 0x6e, 0x73, + 0xbd, 0x03, 0xb9, 0x6e, 0x6b, 0x08, 0xd8, 0xe5, 0x88, 0x34, 0xa7, 0x26, 0xb7, 0x09, 0xd5, 0x03, 0x14, 0x48, 0x35, + 0xd5, 0xb4, 0x0e, 0x0e, 0x37, 0x7c, 0xd0, 0x01, 0xe1, 0x26, 0xc4, 0x46, 0xe5, 0x11, 0x7a, 0xad, 0xc6, 0x3e, 0xd1, + 0xd7, 0x92, 0x6b, 0x9a, 0x6f, 0x90, 0x3a, 0x72, 0xa9, 0xea, 0x3c, 0x4e, 0xd4, 0xb5, 0xb6, 0xda, 0x82, 0x2d, 0xc2, + 0x00, 0x8b, 0x55, 0x0c, 0x87, 0xe8, 0x54, 0xa8, 0x68, 0x89, 0x7b, 0x1b, 0x73, 0xd5, 0xcb, 0x9d, 0xb7, 0x55, 0x97, + 0x7a, 0xa7, 0x06, 0x8d, 0xc8, 0xf4, 0x50, 0x01, 0x0e, 0x84, 0x8c, 0xb5, 0x7d, 0xb0, 0x8c, 0xe3, 0x8c, 0x54, 0x65, + 0xd8, 0x08, 0x46, 0xd3, 0x01, 0xca, 0x5a, 0xf5, 0x38, 0x9c, 0x03, 0x62, 0x79, 0x48, 0x6e, 0x9a, 0xcc, 0x10, 0xd9, + 0x22, 0x9b, 0x5f, 0x6a, 0xf2, 0xe4, 0x0a, 0x1d, 0x0d, 0xfb, 0x1e, 0xd0, 0xae, 0xee, 0xc0, 0x40, 0x76, 0xf8, 0xaa, + 0x93, 0xce, 0x72, 0x09, 0xb4, 0x39, 0x86, 0xce, 0x85, 0xc5, 0x29, 0x9f, 0xa7, 0x23, 0x1b, 0xee, 0x1d, 0xe0, 0x45, + 0x47, 0xd7, 0x0b, 0xf0, 0xdb, 0xc1, 0xc5, 0x1d, 0x63, 0x0f, 0x6e, 0xca, 0xa3, 0x2c, 0x3b, 0x95, 0x30, 0x95, 0x47, + 0x13, 0x17, 0xeb, 0x9c, 0x0b, 0x5d, 0xce, 0xe6, 0x75, 0xba, 0xf5, 0x27, 0x2a, 0x86, 0x9b, 0xb5, 0x73, 0x06, 0xcf, + 0x55, 0x4e, 0x87, 0x24, 0x62, 0x49, 0x8b, 0x73, 0xf4, 0x85, 0x44, 0x9e, 0xd6, 0xf9, 0xfd, 0x42, 0x81, 0xce, 0xa9, + 0x83, 0x6a, 0x1d, 0xe3, 0xcc, 0x4e, 0x0f, 0x3a, 0xef, 0x95, 0xc6, 0xa2, 0xb1, 0x4a, 0x59, 0xf1, 0x1f, 0x38, 0xb7, + 0xf4, 0xf6, 0x84, 0xb0, 0x49, 0x2a, 0xa4, 0x50, 0x96, 0x09, 0xb7, 0x3d, 0x0e, 0x34, 0x6d, 0xe7, 0x44, 0x76, 0x5b, + 0xdf, 0xbe, 0x93, 0x24, 0x22, 0x71, 0xdb, 0x0b, 0xa2, 0xf0, 0x0c, 0xd0, 0x18, 0x92, 0xb3, 0xe7, 0x9d, 0x75, 0xf9, + 0xd2, 0xcb, 0x72, 0xbc, 0xc2, 0xde, 0x15, 0x83, 0xb1, 0xb0, 0x42, 0x0b, 0x0b, 0x37, 0x0d, 0xd4, 0xb1, 0x93, 0x24, + 0x76, 0x59, 0x12, 0x3f, 0xb6, 0xfc, 0x33, 0x69, 0x6e, 0x44, 0x9e, 0x8a, 0x8e, 0x75, 0xc8, 0x3e, 0x73, 0xaa, 0x54, + 0xf7, 0x5a, 0xe5, 0x41, 0x39, 0xe6, 0xa9, 0x1a, 0x31, 0x67, 0x6e, 0x33, 0x45, 0x3e, 0x92, 0x3e, 0x6f, 0xae, 0x67, + 0x94, 0x28, 0x10, 0xa9, 0x0b, 0xbd, 0xca, 0x9c, 0xab, 0xd0, 0x91, 0x42, 0x4a, 0xb7, 0x46, 0xb3, 0x89, 0x39, 0x0e, + 0x67, 0x3f, 0x55, 0xd9, 0x13, 0x7c, 0xed, 0x3d, 0x6f, 0xed, 0xc3, 0x66, 0x83, 0xeb, 0x50, 0xf3, 0x21, 0x3d, 0x60, + 0xa6, 0x99, 0x3b, 0x53, 0x20, 0x0b, 0xdb, 0xaf, 0xec, 0x48, 0x94, 0x32, 0xfd, 0x63, 0xa3, 0x75, 0x7d, 0xd9, 0x47, + 0x75, 0x4c, 0xfe, 0xfd, 0x2d, 0x5d, 0xc3, 0x55, 0x07, 0x45, 0x8e, 0xe1, 0x58, 0xd1, 0x6e, 0xa5, 0x3b, 0x00, 0xe1, + 0x35, 0x3b, 0x8c, 0xdc, 0x72, 0x36, 0x45, 0xbd, 0x55, 0x57, 0xc1, 0x02, 0x6a, 0xd4, 0x91, 0x94, 0xbd, 0x51, 0x58, + 0x44, 0xfd, 0x9a, 0x5d, 0x8b, 0x2b, 0x8a, 0x6e, 0x59, 0xe3, 0x7e, 0xc8, 0xec, 0xa8, 0x3f, 0xe2, 0x5a, 0xb9, 0xc3, + 0x0c, 0xd9, 0xe1, 0x1a, 0x53, 0x48, 0xea, 0x8d, 0xc6, 0xcd, 0xb6, 0xd5, 0xf3, 0x4c, 0xc3, 0xb8, 0x6d, 0xcd, 0xd2, + 0x26, 0x76, 0x50, 0x0d, 0xb7, 0x75, 0xc1, 0x54, 0xb5, 0x5d, 0xf8, 0xfa, 0xd5, 0x6e, 0x25, 0xb2, 0x26, 0xb4, 0xe1, + 0x68, 0x6b, 0x60, 0x9a, 0x16, 0xf9, 0x5c, 0xf4, 0xec, 0x6a, 0xb0, 0xc5, 0xbe, 0x0b, 0xd9, 0xbc, 0xfb, 0x6b, 0x95, + 0x84, 0x2a, 0xb9, 0x72, 0x1f, 0x97, 0xe4, 0x27, 0x9d, 0xac, 0xc2, 0x33, 0xb5, 0x8d, 0xfc, 0x0e, 0x27, 0xda, 0x87, + 0x95, 0xe6, 0x69, 0x25, 0xb3, 0x10, 0x10, 0x85, 0xae, 0xf0, 0x2a, 0x04, 0xba, 0x05, 0x0b, 0xff, 0x07, 0x3a, 0x76, + 0x65, 0x5c, 0x0a, 0xe9, 0x8d, 0xca, 0x39, 0x74, 0x43, 0x42, 0x3e, 0xb4, 0xb0, 0x9c, 0x9c, 0x97, 0x1a, 0x74, 0xb5, + 0x35, 0x74, 0x64, 0x79, 0x20, 0x02, 0xfc, 0x44, 0x0e, 0x79, 0xa6, 0x26, 0xd8, 0xfd, 0x24, 0x70, 0x96, 0x66, 0xb3, + 0x08, 0xbf, 0x18, 0x70, 0x86, 0xa4, 0xf6, 0x9e, 0x7e, 0xf9, 0x14, 0x68, 0x0f, 0xbf, 0x5c, 0x68, 0x7d, 0x72, 0x66, + 0xce, 0x51, 0x4b, 0xc7, 0x0d, 0x1c, 0xc2, 0x45, 0x69, 0xc0, 0xf7, 0x48, 0x35, 0x86, 0x29, 0x22, 0x4c, 0x0e, 0xe0, + 0x5c, 0xb9, 0x6d, 0x78, 0x56, 0x6e, 0x02, 0x33, 0x1d, 0x29, 0xa5, 0x15, 0xc7, 0xa8, 0xfb, 0xb6, 0xf7, 0xa3, 0x24, + 0xe9, 0xcd, 0xc7, 0xcb, 0xac, 0x50, 0xfa, 0x9e, 0x99, 0x85, 0xae, 0xe2, 0x77, 0x26, 0xb9, 0xab, 0x4b, 0xe8, 0xa4, + 0x5a, 0xce, 0x80, 0x51, 0xae, 0x56, 0x58, 0xee, 0x84, 0x40, 0x0e, 0x9b, 0xfb, 0xe9, 0x66, 0x90, 0x26, 0x5b, 0x51, + 0x95, 0x18, 0x23, 0x52, 0x68, 0xbf, 0xd9, 0x9d, 0xfb, 0xa3, 0xd5, 0x0c, 0x3a, 0xea, 0x3b, 0x66, 0x5c, 0xcd, 0xb7, + 0x62, 0xbb, 0xd8, 0xb0, 0x83, 0x69, 0x14, 0x75, 0x98, 0xe6, 0x01, 0x42, 0xf7, 0x2c, 0x1d, 0xa8, 0x5f, 0x10, 0x9f, + 0xf2, 0x76, 0x55, 0x6d, 0x1d, 0xe4, 0x62, 0xa6, 0xa2, 0x7c, 0x8a, 0x1a, 0x14, 0xb0, 0x68, 0xdd, 0x2e, 0x4d, 0xc0, + 0x14, 0x59, 0x48, 0xb7, 0x90, 0x82, 0x28, 0x59, 0x08, 0x66, 0x50, 0xf1, 0x99, 0xbf, 0x4c, 0x7c, 0xad, 0x8f, 0x16, + 0x3c, 0xa5, 0x27, 0x6c, 0x15, 0x72, 0x75, 0xc7, 0x68, 0x31, 0xab, 0x4e, 0x3b, 0x4e, 0x13, 0x87, 0x0e, 0x35, 0xea, + 0x88, 0xd8, 0x75, 0x7c, 0xf0, 0x54, 0x32, 0x79, 0x83, 0xec, 0x2f, 0x27, 0x01, 0x3f, 0xd6, 0xb3, 0x5f, 0x32, 0x7b, + 0x88, 0x55, 0x69, 0xc6, 0xe3, 0x85, 0xb2, 0x47, 0xe5, 0xa8, 0xa8, 0x35, 0xf6, 0x73, 0x17, 0xa7, 0xb5, 0x51, 0x49, + 0x21, 0x77, 0x1e, 0x2e, 0xe4, 0x2b, 0xa7, 0x70, 0xee, 0x46, 0x25, 0xa2, 0x3c, 0x80, 0x99, 0xb0, 0x39, 0x71, 0xa3, + 0xe2, 0x16, 0x50, 0x39, 0xd3, 0x93, 0x26, 0x31, 0x9d, 0x95, 0x88, 0x31, 0xa3, 0x4b, 0xb8, 0x1e, 0x87, 0x68, 0x0c, + 0xcd, 0x30, 0xa7, 0xf7, 0x31, 0x7a, 0x82, 0x1c, 0x50, 0x0f, 0xed, 0x5a, 0x43, 0x88, 0x99, 0x54, 0xf8, 0x56, 0xad, + 0x88, 0x2d, 0xb3, 0x4f, 0x04, 0xb5, 0x6d, 0x2e, 0xf3, 0x88, 0x28, 0x6f, 0x29, 0x7c, 0x9f, 0xfb, 0xcb, 0x77, 0x8c, + 0x57, 0x72, 0xe8, 0x9d, 0x8e, 0x92, 0x9f, 0xc3, 0xfc, 0xec, 0x37, 0x0e, 0x60, 0x01, 0x11, 0xe7, 0x92, 0x9c, 0x7a, + 0x4a, 0x96, 0xe6, 0x3a, 0xeb, 0x75, 0x13, 0xc1, 0x2c, 0x99, 0x06, 0x4c, 0xac, 0x65, 0x16, 0x40, 0x07, 0x52, 0x09, + 0x9c, 0x15, 0x95, 0x75, 0x34, 0x93, 0x47, 0x0b, 0xbd, 0x37, 0xf1, 0xf0, 0x45, 0x29, 0xc6, 0x02, 0xfc, 0xb1, 0xa5, + 0xc6, 0xa2, 0x4c, 0xdf, 0xbc, 0x08, 0x54, 0xcd, 0x5a, 0x1e, 0x87, 0x74, 0xe9, 0xf5, 0x8a, 0x9a, 0x55, 0x49, 0x6b, + 0xa9, 0x2e, 0xd0, 0x76, 0x40, 0x8e, 0x51, 0x8b, 0xea, 0x0c, 0xd2, 0x50, 0xb4, 0x07, 0x4a, 0x5f, 0xc3, 0x84, 0x1e, + 0xf0, 0x4b, 0x35, 0x90, 0xd9, 0xe0, 0x9d, 0x4d, 0xb7, 0xb8, 0x98, 0x1c, 0x39, 0xeb, 0x06, 0x10, 0x70, 0xbb, 0xde, + 0x96, 0x9a, 0x08, 0xa9, 0x70, 0x83, 0x71, 0x59, 0x24, 0xea, 0x2f, 0x9a, 0xc3, 0xda, 0x15, 0x92, 0x3a, 0xc4, 0x3a, + 0xb4, 0x30, 0x01, 0xad, 0x19, 0x17, 0x1b, 0x5a, 0x94, 0x9d, 0xc8, 0x81, 0xb5, 0x59, 0x24, 0x19, 0x87, 0x3d, 0x9a, + 0x69, 0x33, 0x91, 0x6b, 0x09, 0x9e, 0x4a, 0x44, 0x6f, 0xd1, 0xf4, 0xeb, 0x07, 0x15, 0x36, 0x37, 0x99, 0x54, 0xca, + 0x4c, 0x8f, 0x86, 0x40, 0xbb, 0x76, 0x07, 0x7c, 0x87, 0x0a, 0xfe, 0x12, 0x3e, 0x18, 0x45, 0xf7, 0xfb, 0xec, 0x69, + 0x07, 0x7c, 0x08, 0xa7, 0x4e, 0xfb, 0x45, 0x80, 0x75, 0x0e, 0x94, 0x62, 0x5d, 0x98, 0xe3, 0x8c, 0xa3, 0x76, 0x35, + 0xa3, 0x8d, 0xfd, 0xc4, 0x18, 0x02, 0x85, 0xc3, 0xb7, 0x3d, 0x5a, 0x79, 0xd5, 0xcc, 0xd6, 0x4c, 0x2f, 0x69, 0x47, + 0x3e, 0xa2, 0x46, 0x30, 0x09, 0x22, 0x69, 0x99, 0x40, 0x68, 0xc6, 0xe8, 0x2d, 0x5c, 0xc1, 0xda, 0x9c, 0x01, 0x2d, + 0x75, 0xbd, 0x50, 0xe8, 0x81, 0xa7, 0xe7, 0x4c, 0x4c, 0x0a, 0xf3, 0x01, 0x2e, 0x69, 0xff, 0xde, 0x1c, 0x66, 0x0d, + 0xd5, 0x6a, 0x6d, 0xb7, 0x65, 0x7d, 0x97, 0x28, 0x10, 0xb6, 0x1f, 0xda, 0x45, 0xf7, 0x23, 0x3f, 0xbb, 0x16, 0xa0, + 0xce, 0x62, 0xdb, 0x35, 0x9e, 0xf4, 0xd7, 0x5e, 0xb7, 0x04, 0x1f, 0xfb, 0x2b, 0x0d, 0x9f, 0x54, 0x0c, 0xcb, 0x92, + 0x09, 0xd3, 0x95, 0xe5, 0x18, 0x67, 0xa5, 0xb8, 0xcf, 0xcb, 0x98, 0x74, 0x77, 0x28, 0x31, 0x89, 0xaf, 0x3b, 0x1b, + 0xd0, 0xb7, 0x8c, 0xe8, 0x65, 0xfd, 0x56, 0xcf, 0xb0, 0xd7, 0x25, 0x80, 0x98, 0x7a, 0x45, 0xc5, 0x78, 0x98, 0xe8, + 0x8b, 0x87, 0xa0, 0x30, 0x7e, 0x94, 0xb9, 0x18, 0x7c, 0x52, 0x6f, 0x5b, 0x48, 0x84, 0x9f, 0xc6, 0xa3, 0xb8, 0x98, + 0xb5, 0x68, 0xd8, 0x76, 0x3d, 0x29, 0x0e, 0x84, 0x84, 0xd6, 0xcc, 0xa7, 0x49, 0x5a, 0x73, 0x29, 0x0c, 0xbf, 0x59, + 0x88, 0x8d, 0x66, 0xe3, 0x28, 0x5a, 0x0b, 0x60, 0x74, 0x55, 0x73, 0xc5, 0x62, 0xe0, 0x61, 0xc1, 0x43, 0xf9, 0xd2, + 0x12, 0x96, 0x3d, 0x7f, 0x9f, 0x4e, 0xe4, 0x9b, 0xbb, 0x9c, 0x6e, 0xbf, 0x77, 0x9d, 0xbd, 0xb9, 0x4b, 0x27, 0xca, + 0xea, 0x17, 0x1d, 0x95, 0xa8, 0xc6, 0xfa, 0xd8, 0xfa, 0x70, 0x97, 0x5b, 0xfd, 0x44, 0x72, 0xda, 0xd9, 0x0e, 0x18, + 0xb7, 0x14, 0xb0, 0x65, 0xda, 0x1e, 0x36, 0xe5, 0x3f, 0xde, 0xba, 0x38, 0xd2, 0x28, 0x48, 0x7c, 0xc2, 0x9c, 0x21, + 0x49, 0xf1, 0xd8, 0x64, 0x00, 0xa3, 0x96, 0x01, 0xf5, 0x08, 0xf6, 0x75, 0x63, 0x47, 0xbe, 0xb9, 0x8c, 0x71, 0xa9, + 0x4e, 0xbb, 0x0e, 0x64, 0xda, 0xe5, 0x21, 0xb0, 0x71, 0x9b, 0xbb, 0x1c, 0x28, 0x12, 0x07, 0x2a, 0x62, 0xa6, 0xfd, + 0x22, 0xf5, 0xa7, 0x1b, 0xc4, 0x46, 0xed, 0xc0, 0xf5, 0x39, 0xd8, 0x14, 0xfb, 0x64, 0xb1, 0xdf, 0xca, 0x3b, 0x3b, + 0xec, 0x8d, 0xf4, 0x47, 0x9c, 0x9b, 0xcf, 0x38, 0x30, 0xa2, 0x4a, 0x73, 0x31, 0x2b, 0x91, 0x2a, 0xb2, 0xa7, 0x95, + 0xef, 0x2f, 0xa4, 0x49, 0xe0, 0xdc, 0xad, 0x0f, 0x3d, 0x9c, 0xcc, 0x9e, 0x08, 0x93, 0x39, 0xe4, 0x3b, 0x78, 0x49, + 0x89, 0xa6, 0x0b, 0x8d, 0xb6, 0x5b, 0x07, 0x04, 0x76, 0x02, 0xe6, 0x69, 0x89, 0xbc, 0x4e, 0xc9, 0x4b, 0x7e, 0xff, + 0xf6, 0xcf, 0xd2, 0xb2, 0x80, 0xe1, 0xc8, 0x53, 0x5c, 0xa5, 0x00, 0x11, 0xc7, 0x71, 0xfe, 0x6a, 0xdd, 0x67, 0x24, + 0xc6, 0xfa, 0xf3, 0xbb, 0x1f, 0xec, 0x3e, 0x41, 0xae, 0xa4, 0xa1, 0xf0, 0xcc, 0xcd, 0x91, 0x9d, 0x83, 0xec, 0xca, + 0xb8, 0x62, 0xb7, 0x41, 0x3f, 0x89, 0x2c, 0x2a, 0xd1, 0x4c, 0xeb, 0xcd, 0x29, 0x16, 0x49, 0x49, 0x2b, 0x2c, 0x6a, + 0xc9, 0x17, 0x0c, 0xe5, 0x30, 0x59, 0x96, 0xb6, 0x9d, 0x39, 0x0e, 0xc5, 0x5a, 0x96, 0x00, 0xd9, 0xc5, 0x12, 0x9c, + 0x2b, 0x8a, 0x5c, 0x86, 0x95, 0x35, 0xb7, 0x02, 0xe3, 0xc0, 0x14, 0x7e, 0xf2, 0x8f, 0x12, 0xed, 0xef, 0x64, 0x58, + 0xb2, 0x8b, 0x3f, 0xa7, 0x2b, 0xf4, 0xda, 0xb9, 0x17, 0xcc, 0x60, 0x32, 0x44, 0xef, 0xb1, 0x84, 0x79, 0xb9, 0x13, + 0xaf, 0x4a, 0x96, 0xa5, 0x71, 0xe0, 0xa0, 0x59, 0x37, 0x6b, 0x75, 0xdf, 0x22, 0x48, 0xcb, 0x86, 0xab, 0x46, 0xac, + 0xb4, 0x12, 0x2a, 0xa1, 0x29, 0xe8, 0x88, 0x92, 0xbc, 0x44, 0x98, 0x19, 0x80, 0xb2, 0x93, 0x88, 0xca, 0x08, 0x82, + 0x63, 0x58, 0xb1, 0x98, 0x69, 0x5c, 0xd9, 0x80, 0xd5, 0xa9, 0xf1, 0x51, 0x1e, 0xba, 0x5e, 0xc0, 0x50, 0x7d, 0xed, + 0x4d, 0xc6, 0x39, 0xc6, 0xbc, 0xd6, 0x4c, 0x73, 0x72, 0xec, 0x7f, 0xdc, 0x55, 0x13, 0x24, 0x2d, 0xbe, 0x1f, 0xed, + 0x67, 0x0c, 0x4d, 0x33, 0x20, 0x96, 0x3d, 0x7c, 0xc2, 0x56, 0x07, 0x6c, 0xd2, 0x75, 0xd8, 0x48, 0x12, 0x25, 0xe8, + 0x4d, 0x9c, 0x66, 0xfb, 0x26, 0x80, 0xa1, 0xba, 0x34, 0xc8, 0x9e, 0x47, 0x46, 0xbc, 0x35, 0x96, 0x03, 0x4b, 0xbe, + 0x02, 0xba, 0xa0, 0x3c, 0xf3, 0x08, 0xce, 0xb6, 0x73, 0x20, 0x0a, 0x63, 0x2d, 0x8a, 0x4b, 0x9c, 0xf0, 0x3b, 0x92, + 0x43, 0x59, 0x32, 0x43, 0x61, 0xca, 0xe7, 0xe0, 0x5c, 0x99, 0x0f, 0x1f, 0xfe, 0x90, 0x7f, 0xfc, 0x4c, 0x57, 0x97, + 0x22, 0xf6, 0xf9, 0x71, 0x8e, 0xcf, 0xbf, 0x4d, 0x7a, 0xca, 0xed, 0x2c, 0xfc, 0x06, 0xde, 0x59, 0x42, 0xce, 0xbb, + 0x1f, 0x7e, 0xd4, 0x2d, 0x0e, 0x8a, 0x85, 0xce, 0x62, 0x8b, 0x5a, 0x70, 0xfe, 0xf9, 0x79, 0x31, 0x17, 0x55, 0x1e, + 0x13, 0x38, 0x53, 0x49, 0x59, 0xfd, 0xa6, 0x48, 0x81, 0xb4, 0x8d, 0x4a, 0xc2, 0xc6, 0xff, 0x18, 0x45, 0xf1, 0xff, + 0xa3, 0x0c, 0x85, 0x86, 0xac, 0xfd, 0xf1, 0x96, 0x45, 0x65, 0x83, 0xcd, 0xff, 0x98, 0x68, 0xad, 0x56, 0x9f, 0x09, + 0x50, 0x49, 0x5b, 0x49, 0xa5, 0x0f, 0x0a, 0x3c, 0xd3, 0xd1, 0xe4, 0x0c, 0x35, 0xc8, 0x88, 0x27, 0x8c, 0x33, 0x60, + 0x68, 0x9b, 0x75, 0xc9, 0xbb, 0x6d, 0x13, 0x7f, 0x8d, 0xc2, 0x9b, 0x32, 0xb5, 0xd1, 0x18, 0x24, 0xa7, 0x0a, 0x90, + 0xe6, 0x38, 0x5b, 0x85, 0xae, 0x68, 0xc3, 0x39, 0x37, 0x6b, 0x2d, 0x38, 0x1b, 0xc6, 0x56, 0xc3, 0x97, 0xbf, 0x20, + 0x41, 0x60, 0xd7, 0xa4, 0x0e, 0xaa, 0xb2, 0xe6, 0xc5, 0x4d, 0xf8, 0x27, 0x6c, 0x2f, 0x31, 0x98, 0xc9, 0x4b, 0x9a, + 0x77, 0xa6, 0x23, 0xa4, 0x79, 0x84, 0x9c, 0xd9, 0xfc, 0x8f, 0x62, 0x26, 0xcb, 0x43, 0x19, 0xcd, 0x7c, 0x98, 0x18, + 0xff, 0xe6, 0x49, 0x02, 0xfb, 0x99, 0xf3, 0x61, 0x14, 0x99, 0x58, 0x1e, 0xdb, 0xc6, 0x0b, 0x72, 0x1f, 0x43, 0x37, + 0x5a, 0xac, 0xb2, 0x2c, 0x63, 0x5f, 0x29, 0xb3, 0xb4, 0xb4, 0xe0, 0xf0, 0x74, 0x03, 0xa2, 0x0a, 0x9d, 0x0d, 0x21, + 0xcf, 0xa5, 0x7f, 0x59, 0xa5, 0xc2, 0xf4, 0xa1, 0xcc, 0x58, 0xeb, 0x2d, 0x10, 0x7b, 0x3d, 0x51, 0x7f, 0x72, 0x87, + 0x44, 0x9b, 0xdc, 0x68, 0xf9, 0xe0, 0x14, 0x56, 0x93, 0x74, 0x1e, 0x99, 0x88, 0x47, 0xf8, 0xce, 0xb6, 0x9f, 0xb7, + 0x4a, 0x7a, 0x7e, 0xf0, 0x11, 0x76, 0xbb, 0x34, 0xf6, 0x5e, 0xf2, 0x3b, 0xf9, 0x39, 0xfa, 0x30, 0xb8, 0x23, 0x27, + 0x25, 0xb5, 0xfd, 0xa1, 0x8f, 0x71, 0x1d, 0x28, 0xbb, 0xff, 0x41, 0xe3, 0x39, 0x64, 0x51, 0xf1, 0x68, 0x92, 0xce, + 0x30, 0x07, 0x4b, 0xfd, 0x30, 0x73, 0xe1, 0x6f, 0xd2, 0x04, 0x67, 0xd1, 0x8d, 0x5e, 0x1e, 0x4c, 0xeb, 0xc9, 0x3f, + 0x22, 0x2b, 0x7f, 0x9a, 0x65, 0x93, 0xc3, 0x69, 0xb8, 0xe0, 0x47, 0x32, 0xfa, 0xed, 0xac, 0x6e, 0x4f, 0x96, 0xad, + 0x5b, 0xed, 0x21, 0x60, 0xfa, 0x91, 0x86, 0x48, 0xde, 0x2c, 0x53, 0x85, 0x81, 0xa8, 0x18, 0x5c, 0xd0, 0x1a, 0x74, + 0x29, 0x35, 0xb5, 0x55, 0xe0, 0x8c, 0x4e, 0x04, 0x1d, 0x54, 0x70, 0xb4, 0x5c, 0xf9, 0xea, 0x07, 0x0d, 0x8b, 0x93, + 0x8a, 0xed, 0xb6, 0x28, 0x92, 0x3d, 0x83, 0xe3, 0x68, 0x11, 0x15, 0x99, 0xf1, 0xef, 0x32, 0x5a, 0x29, 0xa5, 0x54, + 0x82, 0xe0, 0x8e, 0xbe, 0xd0, 0xc5, 0x65, 0x94, 0x86, 0xfc, 0x90, 0x4a, 0xb6, 0xd0, 0x80, 0x4a, 0x29, 0x0e, 0x54, + 0x8d, 0xcb, 0x44, 0xb8, 0x32, 0x36, 0x17, 0x8d, 0x2b, 0x5a, 0x15, 0xaf, 0x62, 0x87, 0xf4, 0xfa, 0x3a, 0x51, 0x85, + 0x21, 0xcb, 0xc0, 0xe1, 0xe1, 0x1c, 0x65, 0xcd, 0x93, 0x6d, 0x28, 0xc9, 0x33, 0x15, 0x73, 0x30, 0xe3, 0x5c, 0x3d, + 0xa9, 0xc6, 0x06, 0x34, 0x54, 0x54, 0x67, 0x31, 0x9d, 0xad, 0x0e, 0xa8, 0x53, 0x02, 0x02, 0xb3, 0xf0, 0x18, 0x8f, + 0xa3, 0x10, 0x77, 0xa5, 0x0c, 0xbb, 0x70, 0x9b, 0x25, 0x58, 0x8f, 0x93, 0xe1, 0x70, 0x47, 0x1b, 0x3b, 0x17, 0x75, + 0xf8, 0x51, 0xb0, 0x4b, 0x31, 0x68, 0x48, 0x23, 0x24, 0xb9, 0xd8, 0x39, 0x75, 0x2c, 0x9a, 0x70, 0x27, 0x0b, 0x02, + 0x20, 0xf2, 0x30, 0xf5, 0xc1, 0xe2, 0xf2, 0xa8, 0xb3, 0x80, 0x89, 0x79, 0xae, 0xec, 0xa2, 0xbc, 0x81, 0xaf, 0xd6, + 0xa1, 0x1c, 0x62, 0x99, 0xc4, 0x58, 0x69, 0x33, 0xfe, 0x77, 0x59, 0x1e, 0xa5, 0x37, 0x96, 0xd5, 0xb4, 0x45, 0xf5, + 0xa0, 0xd1, 0x1d, 0xae, 0x1d, 0x31, 0x36, 0x96, 0x59, 0x27, 0x86, 0x05, 0xf3, 0xdf, 0x67, 0x86, 0xc5, 0x46, 0x55, + 0xcb, 0x37, 0x39, 0xdf, 0xbd, 0xe3, 0x53, 0x08, 0x66, 0x51, 0xc3, 0x03, 0xee, 0x1e, 0x96, 0x31, 0x2c, 0x16, 0x04, + 0xb3, 0xec, 0xc1, 0xc3, 0xba, 0x1a, 0x82, 0x34, 0xe3, 0x51, 0x92, 0x40, 0x37, 0x62, 0x28, 0x46, 0x72, 0x46, 0xc0, + 0x26, 0x29, 0xc4, 0xe0, 0x15, 0xb0, 0x3f, 0xd6, 0x25, 0xa4, 0x82, 0x23, 0x3c, 0x20, 0x1c, 0xaa, 0xb8, 0xfc, 0xb0, + 0x88, 0x61, 0x20, 0x86, 0x54, 0xbc, 0x98, 0x95, 0x4f, 0x0b, 0x80, 0x91, 0x35, 0xaa, 0x78, 0x48, 0x86, 0xc8, 0xc8, + 0x9b, 0x16, 0x19, 0x75, 0xf0, 0xc6, 0xf0, 0x1b, 0x11, 0x03, 0x5e, 0xc9, 0x19, 0xe4, 0x31, 0x27, 0x2b, 0x73, 0xf9, + 0x32, 0x77, 0xe9, 0xb7, 0xe3, 0xa9, 0x1c, 0xb7, 0xcd, 0x3c, 0xb4, 0xe9, 0x65, 0xac, 0x73, 0x51, 0x71, 0x70, 0xbd, + 0xcc, 0x31, 0xa2, 0xa7, 0xf9, 0xc2, 0xb5, 0x45, 0xf6, 0xb4, 0x86, 0xeb, 0xd7, 0x2a, 0xfb, 0xf0, 0x09, 0x19, 0x5b, + 0x40, 0x86, 0x87, 0x9d, 0xba, 0x6d, 0x64, 0x0c, 0x23, 0xf0, 0xdf, 0xc6, 0xf7, 0x13, 0xe7, 0x98, 0x2e, 0x73, 0x41, + 0x72, 0x98, 0x17, 0xf8, 0xb6, 0x30, 0xfe, 0x92, 0x73, 0x1c, 0x8d, 0xc9, 0xba, 0x87, 0x1a, 0xdd, 0xbd, 0xb4, 0xe1, + 0x0b, 0x26, 0xe8, 0xfc, 0x12, 0x1d, 0x5f, 0x93, 0x06, 0xcb, 0x7d, 0xde, 0xd7, 0x33, 0x64, 0x1a, 0x0f, 0x63, 0x4c, + 0xc8, 0x35, 0x9e, 0x33, 0xd1, 0x8d, 0x7a, 0xcf, 0x96, 0xb1, 0x96, 0xd8, 0x2a, 0xa2, 0xcd, 0x36, 0x58, 0x39, 0xaa, + 0xfe, 0xf5, 0x5d, 0x24, 0x82, 0x11, 0x35, 0xed, 0xd3, 0x5a, 0xdf, 0xa8, 0x3c, 0xf3, 0xdb, 0x99, 0x77, 0x1b, 0x56, + 0x86, 0x19, 0x04, 0x33, 0xbe, 0x62, 0xce, 0xd0, 0xcf, 0x23, 0x73, 0x0f, 0xbc, 0xdd, 0x4b, 0xef, 0xc6, 0x9a, 0x35, + 0xfa, 0x61, 0xba, 0x53, 0x92, 0x59, 0x60, 0x3b, 0xfe, 0x4d, 0xd0, 0x53, 0x21, 0xf5, 0xa3, 0x3a, 0xb0, 0xf8, 0x9a, + 0x93, 0x98, 0x90, 0x0c, 0x39, 0x58, 0x90, 0xab, 0xe6, 0xbd, 0xa7, 0xdb, 0xb6, 0x8c, 0x0a, 0x71, 0xe9, 0x74, 0xf5, + 0xe5, 0xf5, 0xda, 0x0b, 0xb4, 0xa3, 0xfa, 0xd1, 0xc6, 0xcb, 0x78, 0xf1, 0x78, 0x03, 0x77, 0x22, 0x7e, 0x43, 0x6e, + 0x68, 0x8c, 0xaf, 0xc2, 0xa3, 0xb5, 0xda, 0x2b, 0xae, 0xbd, 0x69, 0xee, 0xf1, 0x8b, 0xb9, 0x56, 0x67, 0x4e, 0xb5, + 0x57, 0x66, 0x5c, 0x99, 0xb8, 0x58, 0x51, 0x92, 0x0f, 0x5f, 0x10, 0x5c, 0xc7, 0xcf, 0xd6, 0x41, 0xb8, 0xeb, 0xf1, + 0x9d, 0x1c, 0x2c, 0xc5, 0xc0, 0x74, 0x03, 0xd7, 0x81, 0x18, 0xc3, 0xd8, 0x22, 0x01, 0x92, 0xfa, 0xa1, 0x6c, 0xc5, + 0x28, 0x18, 0xbf, 0x3e, 0x5e, 0xb6, 0xea, 0x1d, 0xff, 0x61, 0x09, 0xe0, 0xd8, 0x46, 0x38, 0x02, 0xcd, 0xac, 0x38, + 0xe5, 0x52, 0x5c, 0xe8, 0x23, 0x38, 0xb3, 0x29, 0xfb, 0x80, 0xe3, 0x90, 0x4d, 0x30, 0xed, 0x8f, 0x86, 0xca, 0xef, + 0x9f, 0xc8, 0x8f, 0x6b, 0x77, 0xbf, 0x57, 0xd7, 0x16, 0x9e, 0xfe, 0x73, 0x87, 0xde, 0x49, 0x47, 0x5e, 0x3b, 0x1d, + 0x75, 0xb8, 0x02, 0xd9, 0x71, 0x53, 0x7b, 0x56, 0x57, 0x5f, 0xc9, 0xf1, 0x63, 0x7f, 0x5b, 0x95, 0x6e, 0xe3, 0xfd, + 0xf3, 0xb2, 0xaa, 0xec, 0x6c, 0xaf, 0x5c, 0x17, 0x7f, 0x59, 0x69, 0xf2, 0xda, 0x7f, 0xd1, 0x6f, 0xe7, 0xa4, 0x1f, + 0xe6, 0x9b, 0x45, 0x8b, 0xbc, 0x61, 0x9b, 0x01, 0xfe, 0xf1, 0xa0, 0xf1, 0x50, 0x44, 0xd8, 0xdc, 0x75, 0xbf, 0xb5, + 0xa1, 0x41, 0x31, 0x27, 0xef, 0x04, 0x29, 0x0a, 0x20, 0x71, 0xc7, 0x5a, 0x01, 0x38, 0x06, 0x86, 0xc1, 0x73, 0xef, + 0x53, 0xeb, 0xc6, 0x14, 0x75, 0xb9, 0x7a, 0xa2, 0xb1, 0x9b, 0xad, 0x37, 0xf4, 0x35, 0x6e, 0xf4, 0x1f, 0x91, 0x0b, + 0x11, 0x18, 0x3c, 0x3f, 0x80, 0xfb, 0xc7, 0x29, 0x7b, 0xd1, 0x62, 0x52, 0x79, 0xc3, 0xe7, 0xf6, 0xf5, 0xe1, 0xb5, + 0x7c, 0x9a, 0xcd, 0x05, 0x12, 0xbd, 0x3e, 0x36, 0xb5, 0xd0, 0x14, 0xb9, 0x96, 0x3b, 0xd9, 0xc5, 0xd1, 0x34, 0xc4, + 0x68, 0x01, 0x50, 0x28, 0x03, 0xc6, 0x4f, 0xb0, 0x86, 0x3a, 0xe3, 0x9f, 0xcf, 0xa7, 0x3c, 0xa7, 0xfb, 0xcd, 0x5b, + 0x33, 0xbd, 0xa5, 0x39, 0xe0, 0xdb, 0x90, 0xff, 0xdb, 0x3f, 0xd1, 0xad, 0x63, 0xac, 0xf6, 0x98, 0x1d, 0x5c, 0x9b, + 0x6b, 0x59, 0xf4, 0x6f, 0x6b, 0xe2, 0xca, 0xeb, 0xd1, 0x0f, 0xf8, 0x75, 0xee, 0x0b, 0x81, 0xd1, 0x14, 0x9e, 0xb1, + 0x98, 0xb4, 0x55, 0xae, 0xef, 0x7a, 0xc2, 0x6c, 0x1b, 0x9d, 0x22, 0x35, 0x04, 0xd7, 0xfb, 0x18, 0x57, 0x1b, 0x4f, + 0xa2, 0xb2, 0xda, 0xbe, 0x79, 0x2a, 0xc0, 0x85, 0xc6, 0xf2, 0x4f, 0xd4, 0x79, 0xbb, 0x47, 0x6d, 0x72, 0xda, 0x3f, + 0x69, 0xed, 0x9e, 0x4b, 0x0f, 0x1d, 0xe9, 0xb1, 0xe9, 0x53, 0x6b, 0xde, 0x10, 0xec, 0x5b, 0xd2, 0x62, 0x2f, 0x00, + 0xdc, 0x01, 0x9e, 0xa8, 0x36, 0xd1, 0xb3, 0xaa, 0x7f, 0xec, 0x01, 0x69, 0x8c, 0xef, 0x31, 0x49, 0x95, 0x1b, 0xb9, + 0x50, 0xb3, 0x48, 0x50, 0x74, 0x1c, 0x1f, 0xdf, 0x31, 0xad, 0xd6, 0xc3, 0xf3, 0x62, 0x55, 0x0a, 0x63, 0xcb, 0xdc, + 0x9b, 0x79, 0x90, 0xd3, 0x54, 0x1f, 0xbc, 0x16, 0xee, 0x1b, 0xba, 0x14, 0x3e, 0x16, 0x8f, 0x5a, 0xed, 0x80, 0x9c, + 0x6c, 0x41, 0x08, 0x47, 0x74, 0xfe, 0x52, 0x32, 0x53, 0x80, 0xd7, 0x81, 0xbb, 0xe2, 0x18, 0x3d, 0xb6, 0xe3, 0x6e, + 0x54, 0xdc, 0xc2, 0x9f, 0x1d, 0x44, 0x11, 0xd6, 0x55, 0xbb, 0x35, 0x61, 0xce, 0xcb, 0x14, 0x46, 0xa9, 0x90, 0x80, + 0x70, 0xb8, 0xcc, 0x0d, 0x50, 0x42, 0x49, 0x40, 0x5b, 0x15, 0xd5, 0x1f, 0xca, 0xdc, 0x76, 0xbb, 0x51, 0x73, 0x1e, + 0x89, 0x87, 0x81, 0x8a, 0xf5, 0x98, 0xd6, 0x5a, 0x92, 0x03, 0x0a, 0x51, 0xb3, 0xc9, 0xf3, 0xf2, 0x8f, 0xf5, 0x48, + 0x2e, 0x05, 0x8f, 0x44, 0x2c, 0xde, 0x96, 0xe4, 0x9b, 0xfc, 0xf1, 0x0c, 0x99, 0xbd, 0xe5, 0xe4, 0x87, 0x39, 0x4c, + 0x27, 0x76, 0x19, 0xf0, 0x04, 0x05, 0xac, 0x51, 0x8f, 0xb6, 0xa2, 0xa7, 0x80, 0x74, 0x98, 0x15, 0x0c, 0x08, 0x4e, + 0xa9, 0x5f, 0xe6, 0x47, 0xbe, 0xd9, 0x96, 0x42, 0x55, 0x89, 0x68, 0x29, 0x0b, 0xbe, 0xdc, 0x9e, 0x6f, 0x26, 0x94, + 0xac, 0xb8, 0xa6, 0xb6, 0x99, 0xad, 0xa2, 0x45, 0x2b, 0x08, 0x7f, 0x5c, 0xcd, 0x8c, 0xa8, 0xbf, 0x90, 0x6e, 0xd6, + 0xb4, 0x7d, 0x80, 0xb4, 0x9a, 0x53, 0x3b, 0x3b, 0x47, 0x73, 0x41, 0x03, 0xf5, 0x18, 0xc1, 0xc6, 0xe2, 0x52, 0x93, + 0x72, 0xd6, 0x39, 0xaf, 0xc6, 0x1b, 0x86, 0xdb, 0x4d, 0x52, 0x2f, 0x8a, 0x1b, 0x57, 0x37, 0x3a, 0xe9, 0x4b, 0xd0, + 0xc1, 0xa0, 0x03, 0x86, 0x94, 0x5a, 0x85, 0x8a, 0xec, 0xce, 0x62, 0x5d, 0x38, 0x4d, 0x48, 0x3a, 0x5d, 0xf1, 0x72, + 0x52, 0xbc, 0x67, 0x84, 0x38, 0xfa, 0x01, 0x29, 0x93, 0x47, 0xa8, 0x49, 0x5e, 0xfb, 0x80, 0x21, 0xf3, 0x67, 0xd4, + 0xe2, 0xb0, 0xa1, 0x0d, 0xa2, 0x7f, 0x10, 0x38, 0x1e, 0x47, 0x90, 0x0a, 0xd6, 0x53, 0x32, 0xba, 0x04, 0x48, 0x7a, + 0x09, 0x9f, 0x1e, 0xb1, 0x60, 0x6a, 0xee, 0x94, 0x82, 0xe2, 0xc9, 0x00, 0x43, 0x5b, 0x69, 0x54, 0x96, 0x54, 0x4e, + 0xf4, 0x40, 0x03, 0xef, 0x29, 0x14, 0x10, 0x46, 0x9c, 0x3d, 0xf6, 0x39, 0x2f, 0x62, 0x50, 0xec, 0xad, 0x41, 0xe8, + 0x3e, 0x03, 0xd8, 0xc8, 0x33, 0x0c, 0x16, 0x79, 0x5e, 0x21, 0x47, 0x65, 0x2f, 0xab, 0xb9, 0xff, 0x72, 0x46, 0xd9, + 0xc0, 0xe0, 0x51, 0x3d, 0xe9, 0xe4, 0x5a, 0xbf, 0x0e, 0x27, 0xc8, 0x59, 0xfa, 0x94, 0xd5, 0xa3, 0x76, 0x6e, 0xca, + 0x98, 0xac, 0x2b, 0xf5, 0x67, 0xee, 0x61, 0x24, 0xdf, 0xca, 0x99, 0x51, 0x16, 0xa9, 0x88, 0x17, 0x7e, 0x00, 0xa5, + 0x9f, 0x67, 0x1d, 0x83, 0xc2, 0x13, 0x0b, 0x0d, 0x81, 0x38, 0xc4, 0x35, 0x36, 0xb8, 0x71, 0x20, 0x18, 0x35, 0x68, + 0x4c, 0x6e, 0x51, 0xad, 0x29, 0x72, 0x2d, 0xd4, 0xa7, 0x06, 0x43, 0x6d, 0x9c, 0x79, 0x66, 0x25, 0x98, 0xd0, 0xf0, + 0x92, 0x4f, 0x95, 0xac, 0xa3, 0xb8, 0xc2, 0x2f, 0x57, 0x80, 0xd9, 0xc0, 0x34, 0x77, 0x1d, 0x60, 0xb0, 0xd2, 0x9c, + 0x9a, 0x91, 0x67, 0xe7, 0x0e, 0xa1, 0xd4, 0x8d, 0x5e, 0xc0, 0x04, 0x30, 0x1c, 0x02, 0xda, 0xa0, 0x97, 0x17, 0x3e, + 0x5c, 0x90, 0xaa, 0x1d, 0x19, 0x70, 0xb4, 0xc8, 0x89, 0xb2, 0x75, 0x88, 0xff, 0x99, 0x48, 0x48, 0xda, 0xec, 0x40, + 0xbc, 0x39, 0x76, 0x53, 0xc7, 0xaa, 0xe7, 0x20, 0xbf, 0xba, 0xc1, 0x5e, 0x2b, 0xae, 0x4c, 0x93, 0x1a, 0x7a, 0x35, + 0x1a, 0x87, 0x82, 0xb4, 0xbc, 0x98, 0xdd, 0x78, 0xd2, 0x24, 0xba, 0x2c, 0xdd, 0x34, 0xe8, 0x21, 0xbc, 0x33, 0x0f, + 0xf9, 0x1d, 0xef, 0xeb, 0xc9, 0xfe, 0x81, 0xa2, 0x43, 0x60, 0xb7, 0x21, 0xd7, 0x15, 0x5f, 0x3f, 0x21, 0xc5, 0x32, + 0xda, 0xa7, 0x5d, 0x5b, 0xf3, 0xd4, 0xe2, 0x04, 0x86, 0xbd, 0x9c, 0x8d, 0x0b, 0x6e, 0x55, 0x84, 0x61, 0x6a, 0x28, + 0x25, 0x97, 0x9d, 0x6e, 0x49, 0x4e, 0xae, 0x05, 0x1a, 0x83, 0x40, 0x71, 0x1e, 0xf7, 0x9f, 0xd3, 0x97, 0x12, 0x6c, + 0xc7, 0x0e, 0x46, 0x27, 0xe9, 0x3d, 0xad, 0x93, 0xa3, 0xa2, 0xb0, 0xdd, 0x29, 0xdd, 0x38, 0xf0, 0xc7, 0x89, 0x2a, + 0xb5, 0x25, 0x06, 0x9e, 0xef, 0xae, 0x4d, 0x42, 0x5b, 0x73, 0x0e, 0xb0, 0x12, 0x00, 0x28, 0x2f, 0x06, 0x55, 0xbb, + 0x78, 0xe0, 0xa6, 0xa5, 0x6d, 0x70, 0xd3, 0x40, 0x8d, 0x44, 0x04, 0x51, 0x40, 0xc2, 0xd4, 0x3f, 0x37, 0x25, 0x3b, + 0xf1, 0x8e, 0x79, 0x27, 0x0a, 0x15, 0x92, 0x06, 0xda, 0x79, 0xf5, 0xf0, 0xe8, 0x63, 0x42, 0x58, 0x63, 0x9c, 0x18, + 0xdb, 0x80, 0x7d, 0xd7, 0x5a, 0xd1, 0x5c, 0x17, 0xf4, 0xb6, 0xee, 0x14, 0xd5, 0x1c, 0xa0, 0x93, 0x70, 0x3b, 0x0f, + 0x3e, 0x42, 0x46, 0x95, 0xbc, 0xdf, 0xe5, 0xc8, 0xe3, 0x12, 0x04, 0x39, 0xdf, 0x36, 0x54, 0x1e, 0x83, 0x07, 0x51, + 0xe0, 0x83, 0x2a, 0x06, 0x53, 0xe5, 0xc4, 0x4d, 0x20, 0xd5, 0x08, 0x32, 0xaf, 0x22, 0xc4, 0x2b, 0x3a, 0xf9, 0x7d, + 0x8f, 0x40, 0xac, 0x12, 0x9e, 0x24, 0xf3, 0x2a, 0xf9, 0x34, 0x23, 0xae, 0x6a, 0x83, 0x61, 0x66, 0xd8, 0x6e, 0xc9, + 0x69, 0x8c, 0x88, 0xa7, 0xe3, 0x66, 0x6f, 0xe2, 0xb9, 0x11, 0xd8, 0xc2, 0x51, 0xc4, 0xf2, 0x35, 0xd1, 0x19, 0x98, + 0x21, 0x55, 0x85, 0xd8, 0x5c, 0xf2, 0x19, 0x91, 0x8d, 0xc2, 0xf5, 0xb5, 0xf8, 0xbb, 0x4a, 0x29, 0x11, 0x19, 0xea, + 0x6b, 0xf5, 0x4f, 0xd1, 0x08, 0xdb, 0xa9, 0x62, 0xf4, 0xfa, 0xf1, 0x81, 0x7a, 0x04, 0x3a, 0x53, 0xaa, 0x8d, 0x52, + 0x2d, 0x98, 0xd7, 0x5a, 0xa1, 0x61, 0xa1, 0x75, 0xd4, 0xa7, 0x26, 0xc3, 0xe2, 0xc7, 0xab, 0xdc, 0x2e, 0x06, 0x80, + 0x4e, 0x02, 0x94, 0xec, 0x0f, 0x2d, 0xf5, 0xcd, 0x2a, 0x4b, 0x12, 0x80, 0xcc, 0x01, 0xd8, 0xe3, 0x38, 0xa9, 0x59, + 0x91, 0x1d, 0xfd, 0x42, 0x54, 0x4e, 0xd8, 0xe1, 0x8b, 0xa5, 0x69, 0xfe, 0x00, 0x57, 0x09, 0xcc, 0x08, 0x31, 0x19, + 0xd7, 0x51, 0x67, 0x83, 0xd0, 0x02, 0xa0, 0x5b, 0xda, 0xa9, 0x87, 0xe4, 0xed, 0x7a, 0x0c, 0x52, 0xf6, 0x01, 0xea, + 0xbc, 0xab, 0xe1, 0xfa, 0x08, 0xc3, 0x9c, 0x1d, 0x24, 0xa0, 0x9d, 0xaa, 0xd0, 0x9f, 0x9a, 0xa6, 0x72, 0x44, 0xaf, + 0xa7, 0x4d, 0x07, 0x95, 0xbb, 0x89, 0x2a, 0x13, 0xe0, 0xe0, 0x4d, 0x3c, 0x49, 0xe2, 0xb0, 0xdb, 0x4b, 0x4d, 0x53, + 0x3f, 0x99, 0xb8, 0xb2, 0x5a, 0x4d, 0xf9, 0x76, 0x6e, 0x95, 0xd0, 0xd2, 0xe3, 0x42, 0x88, 0x79, 0xbc, 0xe7, 0x81, + 0xeb, 0x65, 0xdf, 0xc8, 0x1a, 0x2c, 0xee, 0x9b, 0x95, 0x51, 0x95, 0xd3, 0x11, 0x1a, 0x93, 0x62, 0x9e, 0xfc, 0x05, + 0x88, 0xd1, 0xdd, 0x4e, 0xd3, 0xeb, 0x10, 0x42, 0x44, 0x37, 0xfb, 0xf6, 0x9e, 0x1a, 0xeb, 0x88, 0x3d, 0x21, 0x2c, + 0x73, 0xc3, 0x4b, 0x74, 0x0c, 0xdc, 0xf6, 0xae, 0x2c, 0xc9, 0x74, 0xf9, 0xdc, 0x17, 0x20, 0x7c, 0x1d, 0x30, 0x43, + 0x0a, 0x54, 0x4a, 0xec, 0x83, 0xcd, 0xf7, 0x91, 0xd0, 0x3c, 0x3d, 0x17, 0xb6, 0x11, 0x7a, 0xbe, 0xec, 0xb3, 0xf5, + 0x5b, 0x38, 0x62, 0x6b, 0xab, 0x60, 0x0f, 0x7b, 0xb9, 0x6e, 0x91, 0xd1, 0x3c, 0xf8, 0x85, 0xe9, 0x2c, 0x0b, 0x89, + 0x57, 0x1b, 0xf5, 0x0d, 0xeb, 0x1d, 0x5b, 0xfa, 0x4c, 0x66, 0x4d, 0x3c, 0x4c, 0xd6, 0xd3, 0xc8, 0xc3, 0xc9, 0xa9, + 0x3c, 0xc7, 0xe6, 0xa9, 0xb0, 0xc0, 0x1b, 0xba, 0x7a, 0x7a, 0xcb, 0xb8, 0xf7, 0xa6, 0x21, 0x79, 0x89, 0xcf, 0xce, + 0xa2, 0x05, 0xa0, 0x98, 0xa8, 0x9c, 0x5e, 0xbb, 0xc0, 0x09, 0xf6, 0x7a, 0x51, 0x41, 0x83, 0x63, 0xe4, 0xd8, 0x96, + 0xe0, 0xe9, 0x70, 0x26, 0x67, 0x9d, 0x0b, 0x48, 0x5f, 0x33, 0xa9, 0x39, 0x0b, 0x73, 0x4e, 0x4a, 0x11, 0xf8, 0xe8, + 0x51, 0x9c, 0xa3, 0x79, 0xba, 0x01, 0x04, 0x86, 0x8a, 0xf7, 0x5d, 0x60, 0x8f, 0x37, 0x1c, 0xa9, 0x8b, 0x1c, 0xac, + 0xe4, 0x3d, 0x31, 0xcc, 0x0a, 0xfd, 0xeb, 0xe7, 0x07, 0x2b, 0x85, 0x8a, 0x5c, 0x8e, 0x51, 0x88, 0x62, 0xf7, 0x8c, + 0x08, 0xcc, 0x4d, 0xa5, 0x3a, 0x08, 0xd4, 0xf2, 0x0f, 0xb6, 0x5f, 0x08, 0x57, 0x4a, 0x70, 0xeb, 0x41, 0x5d, 0x5a, + 0x42, 0xc6, 0x1e, 0xce, 0xea, 0x2d, 0xd2, 0x58, 0x40, 0xb0, 0xc7, 0x5c, 0x6b, 0x7a, 0x98, 0x03, 0xc9, 0xac, 0x06, + 0x18, 0x6d, 0x89, 0x20, 0xf5, 0x82, 0xc1, 0x2e, 0x15, 0xdd, 0xd7, 0x05, 0x45, 0xba, 0xcb, 0xa8, 0x31, 0x95, 0x56, + 0x72, 0x7c, 0x1e, 0x62, 0x7f, 0xad, 0xa9, 0x5a, 0xea, 0xab, 0xec, 0x6b, 0x72, 0xba, 0xbb, 0x5f, 0x6c, 0xfc, 0x48, + 0xf8, 0x79, 0xae, 0x98, 0x41, 0x95, 0x8c, 0xa3, 0x5d, 0xc2, 0xa4, 0xa1, 0x7a, 0xa5, 0x38, 0x6e, 0x2c, 0x37, 0x9e, + 0x6e, 0x5f, 0x74, 0xc6, 0x56, 0xe9, 0xbf, 0xbb, 0x05, 0x3e, 0x27, 0xdd, 0x6b, 0x32, 0x4f, 0x49, 0x6c, 0xf0, 0x43, + 0xf7, 0x20, 0x9d, 0x28, 0xcf, 0xfd, 0xcb, 0xf3, 0xe3, 0x39, 0x29, 0x62, 0xdb, 0x56, 0xe4, 0x95, 0x15, 0xa0, 0x1c, + 0xd2, 0x6e, 0x02, 0xea, 0x4b, 0x37, 0xea, 0x4d, 0xe4, 0x8d, 0x0d, 0xbc, 0x84, 0xd4, 0x1a, 0x28, 0x76, 0x61, 0xec, + 0xab, 0xd3, 0x51, 0x48, 0x93, 0x33, 0xd9, 0x43, 0x42, 0x31, 0x61, 0x80, 0xfe, 0x69, 0x71, 0x34, 0xa3, 0x82, 0xd6, + 0xfb, 0xbd, 0xa8, 0x8e, 0x65, 0xe7, 0x1a, 0x08, 0x99, 0xd9, 0x68, 0x96, 0xbf, 0xc8, 0xf0, 0xc6, 0x21, 0xf2, 0x55, + 0x66, 0x3a, 0x3a, 0xf0, 0xd7, 0x94, 0x3b, 0xa9, 0xc3, 0xc6, 0x55, 0x76, 0x24, 0x81, 0xff, 0x2e, 0x73, 0x22, 0x14, + 0xbe, 0x99, 0x2d, 0x0f, 0xe4, 0x6b, 0x5d, 0xf9, 0x5f, 0x33, 0xea, 0xb3, 0xc2, 0x1d, 0x6d, 0xcb, 0xd5, 0x8c, 0xc3, + 0xd9, 0x70, 0x20, 0xf3, 0xf1, 0x81, 0x0b, 0x5e, 0x79, 0xaa, 0xca, 0x7e, 0x13, 0x0e, 0xc9, 0x03, 0x7b, 0x36, 0x39, + 0x4a, 0x4b, 0x47, 0xed, 0x7f, 0xe5, 0xb4, 0xe8, 0x50, 0x34, 0x2c, 0x5a, 0x17, 0x05, 0xa2, 0x56, 0x1b, 0xcb, 0xcb, + 0x3c, 0x22, 0x41, 0xed, 0x8b, 0xc5, 0x43, 0x7b, 0xe0, 0xa3, 0x29, 0x46, 0xbe, 0xcf, 0x58, 0x07, 0x12, 0x7d, 0x7f, + 0x44, 0x90, 0x32, 0x50, 0x3a, 0x74, 0x06, 0xa5, 0x89, 0x29, 0x1e, 0x93, 0x3c, 0x67, 0xb1, 0xc2, 0x5e, 0xf2, 0x3a, + 0x2a, 0x07, 0x2b, 0x92, 0x7f, 0x8e, 0x08, 0x70, 0x14, 0x0c, 0x1e, 0x45, 0x9e, 0xfa, 0x25, 0xb8, 0xe5, 0xbe, 0x3f, + 0x60, 0x44, 0x56, 0xd2, 0x58, 0x5b, 0x8c, 0x5e, 0x88, 0x91, 0xf9, 0x08, 0x8e, 0xc7, 0xef, 0x9b, 0xa3, 0x14, 0x94, + 0xbe, 0xb4, 0x2b, 0x50, 0xdc, 0x04, 0xba, 0xb4, 0x9b, 0x1a, 0xa7, 0x81, 0x9c, 0xc8, 0xb4, 0xb5, 0x1d, 0xf7, 0xdd, + 0xd9, 0xb1, 0xa0, 0x2d, 0x41, 0xc6, 0x74, 0x17, 0x9a, 0x39, 0x0a, 0x0c, 0xef, 0xb7, 0x1a, 0x47, 0xc0, 0x80, 0x5d, + 0x63, 0x3d, 0xfc, 0x52, 0x4c, 0xfb, 0x54, 0xe9, 0x87, 0x2b, 0x9c, 0xb3, 0x4b, 0x3a, 0xbd, 0xf9, 0xfd, 0x40, 0x06, + 0xc4, 0xc5, 0x1b, 0x31, 0xf5, 0x05, 0xcc, 0x2f, 0x83, 0x02, 0x10, 0xa6, 0x12, 0x58, 0xfa, 0xbf, 0x98, 0x0b, 0xbc, + 0x13, 0x87, 0x35, 0x83, 0x03, 0x83, 0x88, 0x8f, 0x3b, 0xb8, 0xc5, 0x5f, 0x87, 0xff, 0x68, 0x80, 0xba, 0x72, 0xf7, + 0x19, 0x65, 0xcd, 0xf7, 0x49, 0x29, 0x32, 0x7d, 0xf9, 0xee, 0x65, 0x2b, 0xd4, 0x41, 0x8e, 0x6d, 0x6e, 0x55, 0xf3, + 0xda, 0xe2, 0xf7, 0xd3, 0x58, 0xcd, 0x4d, 0x7e, 0xd3, 0xdb, 0x55, 0x57, 0x4f, 0x8d, 0x1a, 0xf5, 0x84, 0x60, 0xf4, + 0xe6, 0x66, 0xd8, 0xad, 0xf1, 0xcb, 0x59, 0x09, 0x68, 0x64, 0xb3, 0x57, 0xbf, 0x47, 0x41, 0xae, 0xaf, 0xf5, 0xf3, + 0xbc, 0xac, 0x32, 0x2e, 0xbe, 0x09, 0xc0, 0x53, 0xe3, 0x43, 0xa2, 0x4a, 0xb5, 0x2c, 0x0d, 0x51, 0x93, 0x00, 0x82, + 0xc3, 0x1f, 0x74, 0x0b, 0x2e, 0xed, 0x57, 0x72, 0x9b, 0x55, 0x79, 0x6d, 0x45, 0xd0, 0x81, 0x45, 0x9f, 0xae, 0x0c, + 0x76, 0x30, 0xe0, 0xe1, 0x14, 0xfd, 0x43, 0xf1, 0x87, 0x89, 0xed, 0x9d, 0x6d, 0x4a, 0x28, 0x1f, 0x9a, 0xb9, 0x17, + 0x77, 0xf6, 0x4c, 0x11, 0xcd, 0x22, 0xd4, 0xac, 0x82, 0x19, 0x2c, 0x1b, 0x6a, 0xe7, 0x1a, 0x12, 0xf6, 0x08, 0x52, + 0x4c, 0xc1, 0xb8, 0xd1, 0xde, 0x90, 0xee, 0x88, 0x6b, 0x06, 0xe5, 0xb0, 0x50, 0x94, 0xf9, 0xcd, 0x08, 0xc9, 0x38, + 0xa7, 0xe9, 0x0d, 0x0a, 0xfc, 0x30, 0xe0, 0x73, 0x79, 0xb0, 0x20, 0xcf, 0x1f, 0x55, 0xe9, 0x74, 0x14, 0xfb, 0x56, + 0x12, 0x31, 0x63, 0xda, 0x81, 0x2d, 0xaf, 0xf7, 0xca, 0x74, 0x61, 0xb3, 0x4f, 0x3a, 0xd6, 0x1d, 0xe2, 0xed, 0x29, + 0x71, 0x1d, 0xc4, 0x9e, 0xa6, 0x1c, 0x36, 0x79, 0x3d, 0x99, 0xe3, 0x68, 0x51, 0x76, 0x5d, 0xac, 0xa6, 0x33, 0x14, + 0x7a, 0x0b, 0xc9, 0x46, 0x1b, 0x7a, 0xfe, 0xa4, 0x72, 0x8c, 0xf3, 0xc3, 0xe5, 0x24, 0x86, 0xe9, 0x4b, 0xa9, 0x21, + 0x5a, 0xb7, 0x94, 0xee, 0xb1, 0xbe, 0x63, 0x05, 0x5b, 0xb3, 0xf7, 0x8f, 0x44, 0x96, 0x26, 0x96, 0xa9, 0xd4, 0x96, + 0x9d, 0xba, 0x71, 0xef, 0x59, 0x7b, 0xfc, 0xde, 0x62, 0xb1, 0x46, 0xea, 0x6c, 0xb4, 0x31, 0xcd, 0x40, 0x3e, 0x1a, + 0xda, 0x07, 0x5f, 0x30, 0x65, 0x0b, 0xfd, 0x70, 0xde, 0x6d, 0xd0, 0x16, 0xe3, 0x33, 0x86, 0xa6, 0xd9, 0x9d, 0x0f, + 0xbc, 0xfa, 0x2c, 0x8b, 0x2e, 0x17, 0x1d, 0x4f, 0x73, 0x8c, 0x18, 0x75, 0xff, 0x5f, 0x1e, 0xf6, 0x52, 0x86, 0xbb, + 0x3c, 0x21, 0xc3, 0x4e, 0xee, 0xdb, 0x29, 0xab, 0x80, 0x7c, 0x8c, 0xad, 0xf4, 0xbc, 0x72, 0x30, 0xa2, 0xd2, 0x51, + 0x9c, 0xe9, 0x3f, 0x7c, 0xe5, 0xd7, 0x32, 0x8d, 0xda, 0xf4, 0xa3, 0xcb, 0x92, 0xbf, 0xb2, 0x1a, 0x88, 0x36, 0x4f, + 0x88, 0x4c, 0xfe, 0x4f, 0x24, 0x25, 0x47, 0x06, 0xe2, 0xd1, 0x01, 0x14, 0x30, 0x53, 0x27, 0x93, 0xd3, 0x62, 0x70, + 0x02, 0x22, 0x4b, 0x34, 0x87, 0x73, 0x00, 0x93, 0xb4, 0x04, 0x13, 0x1e, 0xd7, 0x6a, 0xdf, 0x63, 0xc6, 0x01, 0x7f, + 0x99, 0x47, 0x73, 0x70, 0xf7, 0x01, 0x2d, 0x9a, 0x80, 0x64, 0x24, 0x61, 0x58, 0x6b, 0xdb, 0x79, 0x38, 0xd9, 0x4e, + 0xf0, 0xac, 0x7a, 0x7d, 0xc0, 0x8f, 0xb5, 0x82, 0xcb, 0x9d, 0x28, 0x45, 0x75, 0x1f, 0x7c, 0xd9, 0xea, 0xcd, 0x21, + 0xd4, 0x59, 0x0f, 0xf5, 0xcc, 0x40, 0x71, 0xdb, 0xce, 0x66, 0x54, 0xf3, 0x05, 0xff, 0xf8, 0xcb, 0xc2, 0x50, 0x2c, + 0x9a, 0x35, 0x64, 0xc0, 0x00, 0xdc, 0xc6, 0x9c, 0xef, 0x75, 0xfc, 0x97, 0x4f, 0x90, 0xb0, 0x17, 0x11, 0xf6, 0x26, + 0xc5, 0x28, 0xe1, 0x97, 0x13, 0x06, 0x04, 0xf1, 0xda, 0x13, 0x25, 0x88, 0xf4, 0xa0, 0x3e, 0x99, 0x66, 0x5c, 0x66, + 0x33, 0x48, 0xd6, 0xb0, 0x08, 0xba, 0xdd, 0x35, 0xeb, 0x32, 0xe3, 0x4f, 0x7e, 0xc8, 0x70, 0x0d, 0xf4, 0x4f, 0x26, + 0x4a, 0x3a, 0x37, 0x24, 0xa8, 0xf8, 0x20, 0x5e, 0xe6, 0x50, 0x79, 0xde, 0x33, 0xe4, 0xe9, 0xf9, 0x47, 0x7f, 0xdf, + 0xcc, 0x1c, 0xca, 0x53, 0xd6, 0xe4, 0xef, 0x9e, 0xea, 0xfe, 0xa7, 0xc8, 0x2b, 0x3a, 0xf3, 0xd5, 0xac, 0xb3, 0xe2, + 0x3a, 0xe3, 0xec, 0x88, 0x54, 0x70, 0x6a, 0x45, 0xeb, 0x1d, 0x0f, 0xb1, 0x69, 0xfc, 0xb5, 0x40, 0xea, 0xec, 0x91, + 0xb9, 0x67, 0x07, 0x15, 0xa3, 0x25, 0x14, 0x58, 0x2f, 0xa2, 0x06, 0xbe, 0x1d, 0xb5, 0x19, 0x33, 0x7d, 0x4e, 0x0a, + 0xb4, 0x68, 0x09, 0x36, 0x6d, 0x17, 0xa3, 0x26, 0x5e, 0x96, 0xcc, 0x15, 0x27, 0xfc, 0xe9, 0x32, 0x53, 0xec, 0x87, + 0x8c, 0xd4, 0xc1, 0x9e, 0x17, 0x2b, 0x96, 0x2c, 0x97, 0x4f, 0xd7, 0x0f, 0xc9, 0x2e, 0xf7, 0x1e, 0x11, 0x33, 0x5e, + 0x3f, 0x5e, 0xb2, 0x4b, 0x09, 0x28, 0x91, 0x91, 0x0d, 0xe3, 0x36, 0x12, 0x6a, 0x14, 0x15, 0xa3, 0x2b, 0x50, 0x72, + 0xac, 0x53, 0x11, 0x00, 0xf0, 0xc7, 0xf4, 0x52, 0xd8, 0xc0, 0x83, 0xd3, 0x89, 0x02, 0x94, 0x91, 0xa7, 0xef, 0x4c, + 0xc6, 0x82, 0xe8, 0xa8, 0x99, 0xc3, 0xef, 0x84, 0xb1, 0x7a, 0xe6, 0xde, 0xeb, 0xa3, 0x48, 0xb0, 0x7b, 0xd9, 0x08, + 0x03, 0x89, 0x65, 0xd9, 0x64, 0x1c, 0xb6, 0x6e, 0x2b, 0xfc, 0xb4, 0x58, 0x81, 0x34, 0x05, 0x68, 0xde, 0xd3, 0x46, + 0xc0, 0x69, 0x18, 0xb3, 0x2f, 0x13, 0x48, 0xa9, 0x82, 0xb1, 0xfc, 0xa4, 0x64, 0xc3, 0xb3, 0x49, 0xde, 0xfd, 0xc4, + 0xd3, 0x5c, 0x20, 0xe4, 0xc5, 0x02, 0xdb, 0x9a, 0xa9, 0x13, 0xbf, 0x19, 0xe5, 0x66, 0x3f, 0x56, 0xcd, 0xa2, 0x0d, + 0x47, 0x1e, 0x95, 0xe5, 0xa6, 0x1b, 0xdd, 0xda, 0x2d, 0x58, 0xb5, 0x10, 0xa9, 0xe6, 0x78, 0x19, 0x80, 0x8d, 0xe8, + 0x97, 0x94, 0x61, 0xf5, 0x83, 0x4e, 0x81, 0x64, 0x61, 0xc8, 0xb6, 0xcd, 0x92, 0x32, 0x18, 0x82, 0xf2, 0xa8, 0x9a, + 0x02, 0xac, 0x91, 0xf2, 0x4d, 0x0a, 0xa3, 0xc9, 0xbf, 0x6a, 0x8b, 0xfe, 0x93, 0xff, 0x29, 0xd6, 0x7b, 0x26, 0x88, + 0x64, 0x7b, 0x38, 0x9f, 0x9d, 0xa6, 0x05, 0x33, 0x68, 0x14, 0x84, 0xf6, 0x60, 0x4a, 0xcd, 0x49, 0x24, 0x06, 0x25, + 0x17, 0x22, 0xfb, 0x93, 0xea, 0x2d, 0xc7, 0x47, 0x1e, 0xb2, 0xaf, 0x6f, 0x92, 0x26, 0x9d, 0x56, 0xa7, 0xca, 0x08, + 0xee, 0x0a, 0x9c, 0xa0, 0x04, 0xb3, 0x01, 0xfd, 0x93, 0x9f, 0x3f, 0x85, 0x24, 0xfa, 0xd0, 0x05, 0x84, 0x52, 0x67, + 0xcf, 0x88, 0xdc, 0x2c, 0x3c, 0xa2, 0x55, 0x88, 0x62, 0x5c, 0x20, 0x07, 0xc8, 0xfc, 0xb7, 0x91, 0x05, 0xbd, 0x86, + 0xfd, 0x42, 0x37, 0xa2, 0x7d, 0x08, 0x8b, 0x11, 0x9b, 0x2b, 0xde, 0xe8, 0x3d, 0x90, 0x67, 0x88, 0x1b, 0xf7, 0x34, + 0x2e, 0x68, 0xe9, 0x2a, 0x9b, 0x95, 0x02, 0xdd, 0xc4, 0xa3, 0x3e, 0x09, 0x1d, 0xb5, 0x5a, 0xde, 0x0c, 0xd1, 0x3b, + 0xd0, 0xf3, 0x7a, 0xff, 0x04, 0xdf, 0x0e, 0x08, 0x10, 0x51, 0xb8, 0xa3, 0x33, 0xf9, 0xc1, 0xe1, 0x77, 0xae, 0x3c, + 0xff, 0xc8, 0x44, 0x3d, 0x52, 0x99, 0xef, 0x96, 0xf8, 0xbb, 0x5b, 0xde, 0xff, 0xa1, 0x29, 0x53, 0x82, 0xf2, 0x83, + 0x60, 0x60, 0x64, 0x85, 0x0f, 0xae, 0x9d, 0x0e, 0xbf, 0x91, 0x79, 0x89, 0xe2, 0x85, 0xe4, 0xb2, 0x85, 0xdb, 0x2b, + 0xc6, 0x55, 0xac, 0xe2, 0xca, 0x0e, 0xda, 0x67, 0xdc, 0x7a, 0xfc, 0x10, 0x35, 0xc6, 0x1a, 0x47, 0xe7, 0x1c, 0x94, + 0x06, 0x84, 0x04, 0xd3, 0xc0, 0x26, 0x3d, 0x5a, 0x60, 0x99, 0x16, 0x48, 0x09, 0x42, 0x48, 0x2a, 0xba, 0x1f, 0x43, + 0x53, 0x89, 0xcd, 0x8c, 0x20, 0xad, 0x2a, 0x76, 0xa8, 0xc4, 0x29, 0x67, 0x1f, 0xa6, 0x58, 0x23, 0x7c, 0xaa, 0xe9, + 0x3b, 0x88, 0x92, 0xc8, 0x7b, 0x4e, 0x2e, 0x2e, 0x1d, 0x68, 0x45, 0xa6, 0x4a, 0x49, 0xdf, 0x79, 0xc1, 0xad, 0xbf, + 0xd6, 0x3e, 0x20, 0xd6, 0x41, 0x15, 0xf4, 0xac, 0x8a, 0xbf, 0xdc, 0x62, 0xae, 0xa4, 0x25, 0x56, 0xb1, 0xa7, 0x2c, + 0xf6, 0x73, 0x5a, 0x71, 0x1e, 0xce, 0x69, 0xe8, 0x2e, 0x39, 0x77, 0xa9, 0xb8, 0x27, 0x9d, 0xf5, 0x12, 0xe1, 0xbe, + 0x65, 0x07, 0xd3, 0x67, 0x25, 0xfc, 0xf8, 0xbb, 0x39, 0x29, 0x29, 0xd7, 0x81, 0x46, 0xcf, 0x61, 0xe0, 0x65, 0xd0, + 0xa2, 0xee, 0xd4, 0xc0, 0x3d, 0x91, 0xe8, 0x5b, 0x7f, 0x60, 0xc6, 0x66, 0xd9, 0x12, 0x19, 0x14, 0xcf, 0xd4, 0xff, + 0x24, 0x48, 0xe2, 0xb1, 0xfc, 0x23, 0x2f, 0x0e, 0x49, 0x22, 0xa9, 0x3e, 0x80, 0x3e, 0x09, 0x9e, 0x58, 0x80, 0x57, + 0x7f, 0xa0, 0x80, 0xa1, 0x28, 0x57, 0x39, 0x32, 0x77, 0xc2, 0x1c, 0xf2, 0x74, 0x57, 0xbd, 0x53, 0x07, 0x38, 0x7d, + 0xb5, 0x9e, 0x4d, 0x40, 0xa7, 0x85, 0x1e, 0xa0, 0xc4, 0x99, 0x11, 0xa5, 0x19, 0x07, 0xa7, 0x86, 0x39, 0xfc, 0xaf, + 0x57, 0x12, 0x61, 0xec, 0xc1, 0xc3, 0x41, 0xe3, 0x41, 0x05, 0xf9, 0xd9, 0x8e, 0xa6, 0x34, 0x0c, 0x48, 0xc2, 0xb9, + 0x16, 0xab, 0x64, 0x19, 0x5e, 0x3c, 0xf2, 0xca, 0x0c, 0xe1, 0x04, 0xd6, 0x9d, 0x3e, 0x95, 0x0e, 0x82, 0x71, 0x09, + 0x17, 0x2a, 0xaf, 0x39, 0x35, 0x1c, 0x69, 0xb9, 0x40, 0xf1, 0x57, 0x9a, 0xa8, 0x6b, 0x11, 0x4f, 0xe6, 0x47, 0x5c, + 0x35, 0x10, 0x61, 0xda, 0x05, 0x01, 0x96, 0x97, 0xc8, 0xad, 0x85, 0x72, 0xed, 0xb7, 0x1e, 0x36, 0x30, 0x06, 0xeb, + 0xe6, 0xd7, 0x4b, 0x7e, 0x7d, 0xd3, 0xb4, 0xf6, 0xe2, 0x2d, 0x2a, 0x34, 0x9d, 0xe8, 0xe9, 0x90, 0x22, 0x3c, 0x1d, + 0x77, 0x11, 0x19, 0x46, 0x03, 0x4c, 0xdf, 0x56, 0xd5, 0x62, 0x26, 0xed, 0x00, 0xfa, 0xb9, 0x20, 0xcd, 0x01, 0xa0, + 0x29, 0x42, 0xd9, 0x01, 0x70, 0x15, 0xaa, 0xf5, 0xba, 0x5f, 0x69, 0x63, 0x63, 0x3c, 0xe0, 0x11, 0x81, 0x59, 0xf1, + 0x94, 0x42, 0xc9, 0x79, 0x02, 0x79, 0xb1, 0x4d, 0x55, 0xba, 0x99, 0x96, 0xcd, 0xfa, 0xdd, 0xfa, 0x47, 0x96, 0x00, + 0xa2, 0x26, 0x79, 0x64, 0x32, 0x81, 0x0d, 0x15, 0xd2, 0x14, 0xc7, 0xa4, 0x56, 0x02, 0xae, 0xf9, 0xb0, 0x8f, 0x6c, + 0x09, 0x38, 0x3b, 0x70, 0x2d, 0x88, 0xc3, 0x59, 0x33, 0x64, 0xb2, 0x3c, 0xa7, 0xad, 0xd1, 0x3f, 0x5b, 0xad, 0xb1, + 0xb5, 0xff, 0x43, 0x4b, 0x71, 0x3f, 0x19, 0x0b, 0x4d, 0x0c, 0x48, 0x6d, 0x8f, 0xbf, 0xbb, 0x95, 0x74, 0xe6, 0x6d, + 0xc1, 0x49, 0xff, 0x37, 0xd3, 0xe6, 0x74, 0x9e, 0x3d, 0x39, 0x8c, 0x7c, 0xc0, 0x98, 0x0a, 0x61, 0x8c, 0x93, 0xf0, + 0x62, 0x3b, 0xbc, 0x68, 0x0c, 0x6a, 0xff, 0xe5, 0x0e, 0x86, 0x9c, 0xea, 0xd8, 0x7b, 0x1f, 0x44, 0xc9, 0xbe, 0x98, + 0x5b, 0x34, 0x56, 0x87, 0xb4, 0x28, 0x6e, 0xfb, 0x00, 0x32, 0xf0, 0xd2, 0xfd, 0xff, 0xb8, 0x75, 0x88, 0x63, 0xb0, + 0x09, 0x79, 0x89, 0x4b, 0x12, 0xb3, 0x4d, 0x1f, 0x05, 0xf5, 0xfa, 0xb4, 0x11, 0x2e, 0xd1, 0x5c, 0xe9, 0xfe, 0x07, + 0x2f, 0x5b, 0x54, 0x77, 0x29, 0x0f, 0xf7, 0x0e, 0x8c, 0x69, 0x7c, 0x73, 0xf3, 0x3d, 0x0d, 0xa6, 0x14, 0xba, 0x19, + 0xef, 0x60, 0x13, 0xbb, 0xde, 0x56, 0x56, 0x6c, 0x17, 0x99, 0xa2, 0xa2, 0xa9, 0xd1, 0x47, 0x33, 0xd8, 0xec, 0xd0, + 0x80, 0xf6, 0x6f, 0x31, 0xc9, 0x60, 0xf1, 0x70, 0x6b, 0x2e, 0x44, 0xcb, 0xeb, 0x9c, 0xed, 0x28, 0x38, 0x27, 0x23, + 0x8e, 0x24, 0x48, 0x93, 0xee, 0x3b, 0x8e, 0x1e, 0xd4, 0x41, 0xd5, 0x88, 0x3b, 0x6d, 0xc9, 0x7e, 0x45, 0x7d, 0x97, + 0x3e, 0xae, 0x0b, 0x79, 0xe5, 0x1c, 0x48, 0xc4, 0x67, 0x85, 0x37, 0x27, 0x44, 0x46, 0x6d, 0x1b, 0xa9, 0x15, 0x59, + 0x91, 0x5f, 0x21, 0x25, 0xea, 0x5f, 0x51, 0x2b, 0xc8, 0x62, 0x0e, 0xc0, 0xc0, 0x36, 0x00, 0xab, 0xdf, 0xac, 0x18, + 0xb2, 0xa5, 0x80, 0xc6, 0x2f, 0x67, 0xdb, 0x7c, 0xe2, 0x96, 0xec, 0xe8, 0x17, 0x44, 0x6d, 0x6b, 0x45, 0x13, 0x9c, + 0x77, 0x2f, 0xac, 0x9e, 0x89, 0xdf, 0x53, 0xcf, 0xb7, 0xc0, 0x36, 0x90, 0x4f, 0xd2, 0xfd, 0xce, 0x99, 0x3e, 0x60, + 0x0f, 0xc6, 0x58, 0xc7, 0x60, 0x57, 0xd8, 0x63, 0xa3, 0x37, 0x55, 0xe5, 0x39, 0x68, 0x57, 0xb7, 0x1c, 0x15, 0xf1, + 0xf8, 0x2d, 0xcb, 0x3a, 0x18, 0x66, 0x18, 0x3d, 0xf3, 0x05, 0x94, 0x2d, 0xda, 0x11, 0x99, 0x93, 0x5c, 0x46, 0xdb, + 0x54, 0x0e, 0x28, 0x81, 0x05, 0x31, 0xa9, 0x71, 0x4a, 0xdd, 0x2d, 0x9b, 0x97, 0xae, 0xa3, 0x09, 0xf1, 0xd6, 0x5f, + 0x67, 0x3e, 0xd7, 0x83, 0xa3, 0xf2, 0x3c, 0x44, 0x60, 0x1a, 0xc8, 0xc3, 0x02, 0x0e, 0x23, 0x79, 0x5e, 0x8a, 0x40, + 0x01, 0xef, 0x06, 0x7d, 0xb6, 0x19, 0x28, 0x72, 0x0a, 0x91, 0x77, 0x9e, 0x83, 0x05, 0xba, 0xc1, 0x53, 0x44, 0x19, + 0x87, 0x87, 0xff, 0x2e, 0x70, 0x19, 0x1e, 0x92, 0x25, 0x8c, 0xef, 0x1d, 0x4e, 0x24, 0x27, 0xa9, 0x8b, 0xa4, 0xf5, + 0x4b, 0x78, 0xa6, 0xb6, 0x71, 0x6b, 0xfe, 0x22, 0xfb, 0x24, 0x76, 0xaf, 0xbc, 0x80, 0xf9, 0x18, 0x35, 0xd9, 0x65, + 0xfe, 0xc2, 0x3c, 0x26, 0x3d, 0x33, 0xaf, 0xd1, 0x6a, 0x0d, 0x78, 0x20, 0x69, 0x45, 0x58, 0xca, 0x2c, 0x99, 0x73, + 0x19, 0x00, 0xe8, 0xda, 0x78, 0xd8, 0x1a, 0x42, 0x7c, 0x22, 0xd7, 0x77, 0x45, 0x42, 0x65, 0xaa, 0x59, 0x96, 0x23, + 0xf7, 0xc9, 0x4d, 0x08, 0x4b, 0xb5, 0xcb, 0x12, 0xb7, 0x99, 0xe6, 0xb6, 0x36, 0x3c, 0xf7, 0xca, 0xf2, 0xbd, 0xc0, + 0x14, 0xf5, 0xa0, 0xbf, 0xb3, 0x8d, 0x38, 0x45, 0x10, 0x22, 0x66, 0x70, 0x87, 0xa3, 0x11, 0x64, 0x53, 0x4e, 0xf4, + 0x67, 0xbb, 0xc4, 0xe6, 0xa7, 0x97, 0xa9, 0xaa, 0x70, 0x39, 0x62, 0x32, 0xb1, 0x39, 0x1b, 0xb0, 0x98, 0x83, 0x57, + 0x7b, 0x72, 0x9b, 0xdb, 0xb2, 0xec, 0x8d, 0x08, 0x56, 0x83, 0x16, 0xce, 0x1d, 0x2c, 0x15, 0xfa, 0x4e, 0x66, 0xbd, + 0xab, 0x83, 0x9b, 0xd9, 0x6f, 0xd2, 0xee, 0x8f, 0x1c, 0x7d, 0x55, 0x69, 0xdc, 0x81, 0x6d, 0x2c, 0x81, 0x0d, 0x8f, + 0x11, 0x29, 0x87, 0x44, 0xf5, 0xa9, 0x4f, 0x6c, 0x1e, 0xd5, 0x98, 0xe4, 0x38, 0xc8, 0x1d, 0x26, 0xae, 0x68, 0x3a, + 0x9b, 0xb4, 0x10, 0xbb, 0x52, 0x21, 0x3d, 0x9d, 0x85, 0xfc, 0x16, 0x73, 0xd3, 0x75, 0x92, 0xc8, 0xd6, 0xb5, 0x0f, + 0xf9, 0xa2, 0x25, 0x75, 0x68, 0x60, 0xa7, 0xc7, 0x1c, 0xfd, 0x78, 0xb5, 0x95, 0xaf, 0xd4, 0xd6, 0x71, 0x4e, 0x92, + 0x8f, 0x71, 0xbc, 0x68, 0xf8, 0xe7, 0xa2, 0xa2, 0xd1, 0xc2, 0x93, 0xd8, 0xfa, 0x61, 0x27, 0xaf, 0x5f, 0xd1, 0x62, + 0x36, 0x1c, 0xb5, 0x5e, 0x96, 0x57, 0x1c, 0xee, 0xdd, 0xb6, 0x14, 0x4b, 0x58, 0x1f, 0xe3, 0x72, 0xc9, 0xd3, 0xa8, + 0x5a, 0x3a, 0xfa, 0xcb, 0x1b, 0xb8, 0x25, 0xef, 0x04, 0xc0, 0x44, 0x52, 0x1f, 0x61, 0x41, 0x7b, 0x19, 0x31, 0x42, + 0xec, 0x05, 0xd9, 0x27, 0x68, 0x7b, 0xb1, 0xaf, 0x76, 0x3d, 0x0c, 0xd9, 0x92, 0x64, 0x77, 0x6f, 0x46, 0xf8, 0x42, + 0xdd, 0x3d, 0xb2, 0x1a, 0x87, 0x6b, 0xf2, 0xe2, 0x32, 0x44, 0xb1, 0x97, 0x70, 0xc3, 0xa8, 0x2d, 0xc5, 0xdc, 0x82, + 0x1b, 0x49, 0x8b, 0x89, 0xad, 0x51, 0x46, 0x0d, 0x9b, 0x43, 0x9b, 0x43, 0x69, 0xef, 0x15, 0xdf, 0xf0, 0xdf, 0x10, + 0xef, 0x7c, 0x69, 0x4b, 0x12, 0x75, 0xef, 0x42, 0x5a, 0xe6, 0x45, 0xba, 0x92, 0xf5, 0xf3, 0x76, 0x62, 0x43, 0x71, + 0x37, 0xc7, 0x80, 0xf5, 0xc4, 0x41, 0x76, 0x69, 0xf2, 0x81, 0xc4, 0x26, 0x4a, 0x56, 0x5a, 0xfd, 0xcf, 0xee, 0xfa, + 0xb0, 0xe0, 0xa1, 0x89, 0x46, 0xc7, 0xb6, 0x43, 0x37, 0x62, 0x1e, 0x7e, 0x8d, 0x67, 0xaa, 0x16, 0x90, 0x1c, 0xe6, + 0x26, 0x51, 0xea, 0x66, 0x84, 0xea, 0xc4, 0x8d, 0x17, 0x88, 0x7a, 0xda, 0xf5, 0x4c, 0xc7, 0xd2, 0xfb, 0xbb, 0x0c, + 0xa1, 0xa9, 0x21, 0x04, 0x0f, 0x21, 0x39, 0x3f, 0x09, 0x6f, 0x46, 0x27, 0xe2, 0x1b, 0xa6, 0xcb, 0x19, 0x72, 0x0f, + 0x5f, 0xa0, 0x75, 0x27, 0xc1, 0xc2, 0xe1, 0x86, 0x90, 0x22, 0x15, 0x04, 0xc8, 0xf6, 0x31, 0x80, 0x85, 0x46, 0xf6, + 0xa2, 0xc9, 0xd4, 0x80, 0xc8, 0x66, 0x6d, 0x4b, 0x98, 0x63, 0x33, 0x35, 0x68, 0xc1, 0xd6, 0xfc, 0x12, 0x28, 0x1b, + 0xda, 0xe2, 0x2d, 0xfd, 0x4f, 0x5e, 0x13, 0x41, 0x8c, 0x69, 0x6a, 0xd3, 0xcc, 0x7a, 0xe5, 0xda, 0xde, 0xf5, 0x29, + 0x16, 0x0b, 0xe4, 0xc0, 0x75, 0x43, 0x69, 0x6c, 0x8d, 0xd5, 0x25, 0x0d, 0x68, 0xb9, 0xa8, 0x2e, 0x08, 0x84, 0xc4, + 0x10, 0xf3, 0xaa, 0xa1, 0x90, 0x92, 0x84, 0x6a, 0x6e, 0xdd, 0x89, 0x6d, 0x82, 0xc2, 0xec, 0xb8, 0x33, 0x79, 0xe8, + 0xe7, 0x70, 0xfe, 0xfe, 0xc6, 0x2c, 0x40, 0x51, 0xb8, 0xe2, 0xa5, 0x8c, 0x06, 0x89, 0x7e, 0xb3, 0x1e, 0x7a, 0xfe, + 0x83, 0x03, 0xda, 0x9d, 0xca, 0x32, 0xa3, 0xd4, 0xa9, 0x9e, 0x09, 0x4e, 0x6f, 0x0d, 0xd0, 0x88, 0x48, 0x80, 0x09, + 0xfc, 0xa8, 0x3f, 0x0a, 0x15, 0x0b, 0x98, 0xb5, 0x95, 0x53, 0xaf, 0xef, 0x31, 0x10, 0x29, 0xec, 0xb0, 0x71, 0xce, + 0xa2, 0x55, 0x8d, 0x78, 0x42, 0x82, 0x3e, 0x48, 0xc8, 0xce, 0x59, 0xf5, 0x8c, 0xaf, 0x93, 0x0b, 0xbe, 0x60, 0x77, + 0xfc, 0xb5, 0x06, 0x50, 0x8e, 0x7f, 0xb1, 0xf7, 0x86, 0xd7, 0xc3, 0x16, 0xd7, 0x23, 0xe6, 0x8b, 0x32, 0x2f, 0x7f, + 0x78, 0xd0, 0x72, 0xfa, 0xf7, 0xe7, 0x69, 0x80, 0x2a, 0x7f, 0xb1, 0x84, 0x01, 0xa9, 0x3c, 0xbc, 0xf5, 0x46, 0xe4, + 0x4a, 0x66, 0x14, 0x8d, 0x59, 0x3b, 0x6e, 0x09, 0x3b, 0xb8, 0x28, 0x8e, 0x20, 0x54, 0xfc, 0xf3, 0x16, 0x40, 0x62, + 0x2b, 0x68, 0x99, 0xd1, 0xa0, 0x11, 0xed, 0x81, 0x3a, 0x2b, 0x6c, 0xcc, 0x0b, 0xb6, 0x2e, 0x5f, 0xde, 0xad, 0xe0, + 0x20, 0x4b, 0x48, 0x82, 0x87, 0xf5, 0xf6, 0xcd, 0x26, 0xd3, 0xa5, 0x87, 0xa9, 0xd7, 0x1d, 0xbf, 0x67, 0x56, 0x20, + 0xa4, 0xd9, 0x43, 0x64, 0x6d, 0x37, 0x12, 0xd3, 0x1b, 0x4f, 0x6d, 0x3b, 0x62, 0x5e, 0xb7, 0x13, 0x91, 0x2b, 0x75, + 0x6c, 0x9b, 0x87, 0xc8, 0x08, 0x2b, 0x8c, 0x24, 0xb8, 0xfc, 0x32, 0x20, 0x36, 0x51, 0xd0, 0xd8, 0xc7, 0xe2, 0x52, + 0x16, 0x93, 0xec, 0xa3, 0xf8, 0x4b, 0x59, 0xeb, 0x5f, 0x22, 0xd5, 0xd9, 0x13, 0xf8, 0x15, 0x43, 0x7b, 0x0f, 0xa1, + 0xb1, 0x4e, 0x83, 0xbb, 0x16, 0x3c, 0xb2, 0x80, 0x72, 0x1f, 0x1a, 0x12, 0x42, 0x71, 0xba, 0x1d, 0x16, 0xd9, 0xae, + 0x25, 0x46, 0x80, 0x8f, 0x92, 0x5e, 0xa9, 0x4d, 0xc6, 0x70, 0x05, 0x05, 0x70, 0x79, 0xae, 0xc7, 0xf3, 0xd1, 0xcd, + 0xf6, 0x4a, 0x23, 0x09, 0x7d, 0x37, 0xac, 0x78, 0xb9, 0xb9, 0xee, 0x2a, 0x8b, 0x36, 0x9f, 0x62, 0x1c, 0xeb, 0x02, + 0x91, 0x19, 0x21, 0x62, 0x6e, 0xd9, 0xa0, 0x20, 0x1d, 0x6c, 0x17, 0x03, 0xf4, 0xb1, 0x81, 0xe1, 0x0c, 0x56, 0xba, + 0xaa, 0xad, 0x9d, 0xa7, 0xc8, 0xf4, 0x6f, 0xb6, 0x98, 0xc0, 0xcf, 0x17, 0x17, 0x24, 0x04, 0x24, 0x2c, 0xf4, 0xcc, + 0x83, 0x59, 0x0f, 0x27, 0x79, 0xf6, 0x12, 0x13, 0x2e, 0x64, 0xa8, 0x70, 0xfc, 0xa0, 0xad, 0xe6, 0x82, 0xe6, 0xf8, + 0xf5, 0x4c, 0x5b, 0x95, 0xaf, 0x95, 0x34, 0xc9, 0x82, 0x43, 0x5e, 0x38, 0x5d, 0xde, 0x32, 0x44, 0xf1, 0xa9, 0x76, + 0xdd, 0x77, 0xb8, 0xf9, 0x4c, 0x8a, 0x9c, 0x54, 0xda, 0x89, 0x40, 0xa5, 0x21, 0x93, 0xb7, 0x7b, 0x01, 0xb0, 0x6d, + 0x88, 0xbe, 0x68, 0x36, 0x32, 0x53, 0x99, 0x8e, 0xae, 0x96, 0x87, 0x70, 0x6c, 0x0f, 0x6f, 0x06, 0xc3, 0x10, 0xf0, + 0xfa, 0xb4, 0x66, 0xff, 0xba, 0x67, 0x25, 0x55, 0x15, 0x4d, 0x8c, 0x8a, 0xb8, 0xb9, 0x60, 0x72, 0x0f, 0x2a, 0xa6, + 0xc1, 0x43, 0x38, 0x69, 0xc0, 0xe9, 0x38, 0x53, 0xd9, 0x20, 0x79, 0x81, 0x49, 0x10, 0x7b, 0x02, 0x2d, 0x4d, 0xc0, + 0xbc, 0xa2, 0xec, 0x38, 0xda, 0x8c, 0xed, 0x88, 0x50, 0xce, 0x9c, 0x44, 0x45, 0xfc, 0x98, 0x7b, 0xd2, 0x0a, 0xb0, + 0xcf, 0x40, 0x77, 0xbd, 0xc6, 0x4f, 0x6a, 0x41, 0xd1, 0xb7, 0xb6, 0xff, 0x5f, 0x86, 0x41, 0xd8, 0x9e, 0xb6, 0x73, + 0x00, 0x0a, 0xb2, 0x84, 0x00, 0xfe, 0xf2, 0x82, 0xbe, 0x04, 0x59, 0x92, 0x0a, 0x3f, 0x90, 0x57, 0x8f, 0xad, 0x3e, + 0x55, 0xce, 0xbe, 0x3a, 0xfb, 0xf5, 0xb7, 0xec, 0x97, 0xf4, 0xc1, 0x25, 0x27, 0x77, 0xfb, 0x54, 0x62, 0x73, 0xbd, + 0x53, 0x2a, 0x1c, 0x9d, 0x63, 0xb9, 0xac, 0xaf, 0xc4, 0x70, 0x39, 0x95, 0x10, 0xfc, 0x87, 0x0f, 0xc4, 0xef, 0xb3, + 0x72, 0xb7, 0x4f, 0x21, 0xbf, 0x9f, 0x4b, 0x2b, 0xf9, 0xc9, 0xe1, 0x46, 0xba, 0x4f, 0xab, 0x28, 0xc7, 0x5c, 0x7f, + 0x5b, 0x4a, 0xe5, 0xac, 0xb3, 0xb3, 0x4b, 0xb9, 0xb9, 0x9c, 0x73, 0x78, 0xaa, 0xcb, 0xbd, 0x5e, 0xb5, 0xd6, 0xff, + 0x57, 0x66, 0x0d, 0x65, 0xb9, 0x19, 0x94, 0xcc, 0x7b, 0x28, 0x08, 0x72, 0x37, 0xb1, 0x4e, 0x2f, 0x72, 0xe7, 0xb8, + 0x43, 0x39, 0x96, 0xb6, 0xbe, 0x2a, 0x53, 0x8f, 0xcc, 0x45, 0x8c, 0xf3, 0x15, 0xf1, 0xb2, 0x9a, 0xbc, 0x6d, 0xd0, + 0x6f, 0x4f, 0xc8, 0xfc, 0xe7, 0xd7, 0x90, 0x64, 0x3f, 0xc6, 0x2f, 0xea, 0xfe, 0x02, 0x5c, 0xc3, 0x9b, 0x72, 0xe4, + 0x05, 0x3b, 0xae, 0xab, 0xa7, 0x6d, 0xb2, 0xae, 0x85, 0x63, 0xdb, 0xe5, 0xc0, 0x6b, 0x8b, 0x38, 0x04, 0x44, 0x69, + 0x65, 0xdc, 0x73, 0x7a, 0xd7, 0xe9, 0x77, 0xa6, 0x3a, 0x86, 0xdd, 0x80, 0x20, 0x11, 0x0c, 0x28, 0x30, 0x1f, 0x83, + 0xba, 0x93, 0x51, 0xe5, 0xc4, 0x9e, 0x35, 0x10, 0x4a, 0x60, 0x45, 0xf3, 0x35, 0x12, 0x80, 0x96, 0x76, 0xe0, 0x65, + 0xad, 0xa2, 0x93, 0x25, 0x6b, 0x10, 0x1c, 0xf4, 0xff, 0x88, 0xc1, 0x11, 0x07, 0xdf, 0x24, 0x21, 0xce, 0x0a, 0x45, + 0x62, 0x4e, 0xb3, 0x47, 0x1f, 0xb3, 0x8f, 0x72, 0x09, 0xd2, 0xec, 0x47, 0x60, 0x80, 0x60, 0x19, 0x8e, 0x63, 0x91, + 0xa0, 0x64, 0xbe, 0x2a, 0xc8, 0x92, 0x9a, 0xf7, 0x9f, 0x60, 0x6c, 0xff, 0x46, 0xb7, 0x8d, 0xec, 0xef, 0x9a, 0x4a, + 0x6e, 0x7f, 0xe5, 0xdd, 0xf2, 0xeb, 0xfa, 0x7a, 0x79, 0xa1, 0xfe, 0xfc, 0xba, 0x69, 0x81, 0x77, 0x72, 0xf7, 0x52, + 0x0e, 0x35, 0x3f, 0x5f, 0x67, 0xc4, 0x58, 0x30, 0x40, 0xec, 0x53, 0xc7, 0x87, 0x92, 0xee, 0xb7, 0x9e, 0x0d, 0xac, + 0x89, 0xfd, 0x1a, 0xb7, 0xa8, 0x5e, 0xce, 0x0b, 0x6c, 0x56, 0xe3, 0x1a, 0xba, 0xe7, 0x85, 0xd6, 0x3c, 0x17, 0x66, + 0xa9, 0xa0, 0x14, 0x5b, 0x53, 0xc0, 0x27, 0xb8, 0xeb, 0xca, 0x4d, 0x6a, 0xa2, 0xea, 0x4d, 0x78, 0x92, 0xa0, 0xa2, + 0x03, 0x17, 0x4d, 0x5f, 0x3d, 0xb5, 0x2d, 0x36, 0x86, 0x3f, 0x13, 0x74, 0x35, 0x86, 0xac, 0x46, 0x39, 0x66, 0x2d, + 0x56, 0x7a, 0xa1, 0xb5, 0xbc, 0x5a, 0xea, 0x6e, 0x5f, 0x03, 0xbd, 0xf2, 0x82, 0x32, 0xe0, 0x1e, 0x80, 0xac, 0x57, + 0xf4, 0x94, 0x56, 0x91, 0x2d, 0xd9, 0x27, 0x14, 0xdc, 0x3c, 0x9e, 0xe0, 0xb0, 0xf4, 0x51, 0xdd, 0x23, 0x4d, 0x62, + 0x2b, 0x5c, 0xc3, 0xde, 0x64, 0x55, 0xe9, 0x65, 0xf3, 0x84, 0x07, 0x98, 0xd3, 0x82, 0xfd, 0x1b, 0xdb, 0x62, 0xf9, + 0x71, 0x12, 0x68, 0xbb, 0x68, 0x14, 0x37, 0xca, 0x00, 0x88, 0xd2, 0x3d, 0xbd, 0x01, 0x07, 0xa2, 0x5d, 0xd7, 0x42, + 0x7d, 0x9b, 0xd8, 0x0e, 0xe7, 0x26, 0x13, 0x6a, 0xe1, 0xc2, 0x1a, 0xcd, 0xa6, 0x0b, 0x27, 0x6a, 0xef, 0xd2, 0x9e, + 0x27, 0x83, 0x8c, 0xff, 0x2a, 0x83, 0x98, 0xf4, 0xbd, 0xc0, 0xa3, 0x83, 0x70, 0x0f, 0xa1, 0x27, 0x61, 0x91, 0x8a, + 0xd6, 0x14, 0x6c, 0x83, 0x15, 0xa5, 0x71, 0x00, 0xa0, 0xbd, 0x8b, 0xb8, 0x01, 0x07, 0x37, 0x6c, 0x0c, 0x1d, 0x1b, + 0xb7, 0xe4, 0x95, 0x64, 0x82, 0xa0, 0xf2, 0x66, 0x89, 0xcd, 0x78, 0xb2, 0x13, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, + 0x14, 0xf6, 0x3b, 0x9d, 0x11, 0x4c, 0x59, 0x59, 0xed, 0xd0, 0x37, 0x23, 0x5e, 0x70, 0x98, 0x43, 0xb2, 0x20, 0x22, + 0x19, 0xb1, 0xaa, 0x1b, 0xbf, 0xf3, 0x7e, 0x94, 0x9b, 0x89, 0x6d, 0xb1, 0x5e, 0xf1, 0x8c, 0x60, 0xbd, 0x83, 0xa3, + 0x73, 0xf2, 0xdc, 0xcd, 0xc8, 0x5c, 0xe1, 0x3f, 0x86, 0xc9, 0xed, 0x66, 0x7e, 0x30, 0x8c, 0xa8, 0x2f, 0xff, 0x93, + 0x8c, 0x59, 0x55, 0x4e, 0xa3, 0x31, 0x24, 0x44, 0x32, 0xbc, 0x09, 0x40, 0x3c, 0xcf, 0x9a, 0x8c, 0xd1, 0x4c, 0xac, + 0xb6, 0xad, 0xd3, 0x34, 0xfb, 0xf6, 0x92, 0xd3, 0xef, 0x45, 0x85, 0x17, 0x78, 0x5c, 0x75, 0x6e, 0x64, 0xd7, 0x0f, + 0x74, 0x31, 0x87, 0xbe, 0x55, 0xe9, 0xaa, 0xbe, 0x91, 0x1f, 0x6a, 0xf8, 0x4a, 0x0c, 0xea, 0x6e, 0x50, 0xf2, 0x00, + 0x00, 0xfd, 0x71, 0x5e, 0x5e, 0xfd, 0x5f, 0xa3, 0xb9, 0x93, 0x4e, 0xb0, 0xb1, 0x62, 0x69, 0x8e, 0xe3, 0xe5, 0xd0, + 0x5f, 0xa8, 0xe8, 0x39, 0xa1, 0xdf, 0x8d, 0x48, 0xba, 0x44, 0x67, 0x18, 0x4f, 0xcc, 0xd2, 0xe0, 0xb0, 0x86, 0x12, + 0xfa, 0x9b, 0xd1, 0x6f, 0xd7, 0xde, 0x37, 0x90, 0xe2, 0xdf, 0xb8, 0xad, 0x8e, 0x67, 0x47, 0x95, 0x99, 0xd4, 0x32, + 0x0f, 0xdc, 0x16, 0x57, 0x75, 0xd5, 0xcc, 0xa7, 0xed, 0x92, 0x69, 0xda, 0x79, 0xcc, 0x2e, 0xe3, 0x57, 0x38, 0x91, + 0x44, 0x7d, 0xb7, 0x0e, 0x03, 0x34, 0x30, 0xd0, 0x5e, 0x12, 0xa7, 0x17, 0x99, 0xae, 0xde, 0x6a, 0x06, 0x43, 0x73, + 0xa5, 0x4e, 0x3f, 0xb0, 0x7a, 0x41, 0xcb, 0xb0, 0xb3, 0x66, 0xf2, 0xc8, 0x09, 0xb1, 0x8b, 0x9c, 0x9f, 0x98, 0x0f, + 0x39, 0xa1, 0xa6, 0x01, 0xbd, 0x9d, 0x97, 0x57, 0xae, 0x54, 0x91, 0x81, 0x9a, 0x09, 0x29, 0x20, 0xbb, 0xa1, 0xf5, + 0xb1, 0x26, 0xc6, 0x1e, 0xa4, 0x0b, 0xb3, 0xd6, 0x3c, 0x0d, 0x42, 0x53, 0x28, 0x0b, 0x57, 0x66, 0x64, 0xa3, 0xf0, + 0x3d, 0x39, 0x85, 0x86, 0x0b, 0x5a, 0x42, 0x7b, 0xf7, 0x3e, 0x24, 0x74, 0xf7, 0x98, 0x44, 0xd5, 0x74, 0x96, 0x16, + 0xca, 0xcd, 0x42, 0x79, 0x8e, 0xc0, 0x0b, 0x16, 0xb9, 0xe7, 0x55, 0x39, 0x12, 0xb7, 0xee, 0xd6, 0xe9, 0xeb, 0x6e, + 0xb5, 0x86, 0x7d, 0x4c, 0x79, 0x24, 0xbc, 0xa3, 0x85, 0xf9, 0x57, 0xb2, 0xe4, 0x48, 0x87, 0x8d, 0x9a, 0x66, 0xf2, + 0x15, 0x3e, 0xff, 0x47, 0x75, 0x6f, 0xe2, 0x7d, 0xe2, 0x59, 0x81, 0x70, 0x57, 0x14, 0x3a, 0xe3, 0x8e, 0x59, 0x47, + 0xeb, 0x70, 0x4e, 0x9d, 0x98, 0xf1, 0xf0, 0xb8, 0x40, 0x31, 0xfc, 0xf6, 0x8c, 0x06, 0xdc, 0xfd, 0xe9, 0x98, 0xd8, + 0xbd, 0x0e, 0x52, 0xec, 0x32, 0x8b, 0x74, 0x7f, 0xd5, 0x68, 0xaa, 0x0b, 0xb1, 0x6e, 0x95, 0xb9, 0x27, 0xa6, 0xec, + 0x30, 0x9c, 0x51, 0xed, 0xb3, 0x85, 0x9b, 0xbd, 0x91, 0xbb, 0x51, 0xd5, 0x53, 0x6c, 0xe9, 0x92, 0xc3, 0x13, 0x38, + 0x64, 0xd3, 0xa8, 0xdc, 0xfd, 0x5a, 0xcb, 0x57, 0xfb, 0x6a, 0xd1, 0x97, 0x58, 0xc4, 0xf7, 0xf3, 0x21, 0x85, 0x2d, + 0x4f, 0x44, 0xb6, 0x3a, 0x8c, 0x75, 0x80, 0xf1, 0x50, 0xeb, 0xdb, 0xcd, 0x4e, 0x6a, 0x3f, 0xa0, 0xbb, 0x75, 0x96, + 0x96, 0x6f, 0x16, 0xbf, 0xad, 0xff, 0x3c, 0x70, 0x2f, 0x39, 0x14, 0xbf, 0x56, 0x5f, 0x45, 0xa2, 0xc1, 0xfd, 0xb2, + 0x5c, 0x93, 0x49, 0x71, 0xfc, 0x04, 0xc7, 0x54, 0xa6, 0x28, 0x47, 0xd5, 0x6d, 0x37, 0x84, 0x8a, 0x8d, 0x8e, 0xcd, + 0x79, 0xb2, 0x33, 0x75, 0xf5, 0x00, 0x8f, 0x0c, 0x51, 0xfb, 0x9f, 0xca, 0x8b, 0xd3, 0x1e, 0xd9, 0x07, 0xfb, 0xb7, + 0xcc, 0x21, 0xb6, 0x4e, 0xbb, 0x4e, 0xad, 0x9a, 0x70, 0xe0, 0xc3, 0xea, 0x1a, 0xff, 0x17, 0x2f, 0xb8, 0xd1, 0x44, + 0xf4, 0x56, 0xb5, 0x65, 0xa5, 0x04, 0xb6, 0xab, 0x5d, 0x2a, 0x35, 0xbd, 0xd5, 0x4d, 0x8c, 0xcb, 0x9c, 0xd7, 0xd5, + 0xde, 0x90, 0xf5, 0x93, 0xa0, 0x0d, 0xb9, 0x7f, 0xfa, 0x30, 0xe2, 0x10, 0x23, 0x29, 0x6b, 0x17, 0x63, 0xae, 0x0d, + 0xa1, 0x93, 0x2d, 0xca, 0x98, 0xdc, 0xf5, 0x4f, 0x24, 0xaa, 0x2e, 0x9a, 0x20, 0x10, 0xe7, 0xed, 0xb1, 0xf5, 0xea, + 0x6e, 0x71, 0xc9, 0xcd, 0x70, 0x65, 0xaa, 0x12, 0xf0, 0x79, 0x62, 0xf0, 0xc5, 0x82, 0x44, 0x09, 0x3c, 0x0b, 0xd5, + 0x64, 0xdc, 0x35, 0x44, 0x1f, 0x6c, 0xbc, 0xfc, 0xc3, 0xc8, 0x31, 0x3f, 0xf3, 0x4f, 0xca, 0x1b, 0x44, 0x27, 0xc0, + 0x99, 0x00, 0x3c, 0x9e, 0xa5, 0x25, 0xd5, 0x37, 0xa7, 0x7f, 0x6d, 0x92, 0xff, 0x77, 0x6c, 0xf8, 0x56, 0xfb, 0x15, + 0xd0, 0x68, 0x61, 0xd8, 0x21, 0xd0, 0x1a, 0xd4, 0x39, 0x85, 0x71, 0x0f, 0x01, 0xb5, 0xe2, 0x1a, 0xd7, 0x77, 0x69, + 0x84, 0x30, 0x08, 0x49, 0x50, 0xd9, 0x1d, 0x76, 0xb8, 0xb7, 0xbe, 0x2f, 0x90, 0x01, 0xc2, 0x43, 0x19, 0x41, 0x8b, + 0x8c, 0x07, 0xf7, 0x06, 0x7b, 0x10, 0xd6, 0xb9, 0x94, 0x53, 0xae, 0x92, 0xae, 0x43, 0xf6, 0x71, 0xd3, 0xf4, 0x1a, + 0x27, 0xe4, 0x08, 0x52, 0xa9, 0x67, 0x40, 0xd3, 0x74, 0x91, 0x5e, 0xae, 0xb7, 0x74, 0xca, 0xb7, 0x06, 0x62, 0xeb, + 0x5a, 0x58, 0x74, 0x9f, 0x5d, 0xca, 0x43, 0x0f, 0x52, 0x08, 0x0e, 0x89, 0xe5, 0x14, 0xd4, 0x0f, 0x60, 0x52, 0x2e, + 0xff, 0xc3, 0x24, 0x5e, 0xe5, 0xee, 0xfe, 0xd7, 0x6a, 0xb1, 0xaa, 0x1e, 0xcc, 0x6c, 0xfc, 0x40, 0xf7, 0x97, 0xf0, + 0x51, 0xad, 0x3d, 0x5f, 0x39, 0x66, 0x05, 0xa6, 0x0c, 0xfe, 0x93, 0x7f, 0xd0, 0x86, 0x3a, 0x97, 0xf9, 0x6f, 0x71, + 0x25, 0xae, 0x81, 0x14, 0xe7, 0x3d, 0xd4, 0x88, 0x26, 0x69, 0xbc, 0x4c, 0x59, 0x6d, 0x1a, 0x9f, 0x66, 0x8a, 0x40, + 0x88, 0x3a, 0x7a, 0x1d, 0x2e, 0x39, 0x70, 0x91, 0xc3, 0xaa, 0x05, 0xf8, 0x67, 0xc1, 0x0a, 0xe8, 0xf6, 0xb7, 0xe4, + 0x68, 0xcd, 0xfc, 0xed, 0x8e, 0xc6, 0x95, 0x0b, 0x39, 0x34, 0xb1, 0xf6, 0xd5, 0x76, 0x4c, 0xce, 0xd4, 0x9d, 0xa7, + 0x15, 0x8a, 0xae, 0xab, 0x9b, 0x89, 0x2b, 0x02, 0x8e, 0x53, 0xcf, 0x0f, 0x02, 0x0c, 0x66, 0x85, 0x2f, 0xfb, 0x42, + 0x4d, 0xbf, 0xc6, 0x60, 0x0a, 0x32, 0x96, 0x3d, 0x8b, 0x62, 0x78, 0x17, 0xf2, 0x2a, 0x62, 0x2c, 0x97, 0x22, 0x56, + 0x08, 0x65, 0x01, 0x5b, 0x56, 0xae, 0x47, 0xa1, 0x78, 0x78, 0x9c, 0xe2, 0xdd, 0xcc, 0x39, 0x52, 0xee, 0x12, 0xcc, + 0xee, 0x90, 0xf9, 0x49, 0x22, 0xf5, 0xda, 0xb5, 0x60, 0x93, 0x62, 0x8a, 0x5d, 0x51, 0xe4, 0x06, 0x87, 0x30, 0xe1, + 0xa8, 0x7b, 0x7b, 0xa3, 0x69, 0x22, 0xa1, 0x91, 0x28, 0x30, 0x23, 0xa4, 0xbb, 0xfe, 0xe3, 0xee, 0x4d, 0x3f, 0x99, + 0x32, 0x06, 0x11, 0xd0, 0x28, 0x7a, 0x06, 0x10, 0x7a, 0xbe, 0x4a, 0xb9, 0x64, 0x3a, 0xae, 0x60, 0xc4, 0x7d, 0x05, + 0x24, 0x5c, 0x34, 0x6e, 0xcd, 0x2f, 0xd1, 0x49, 0xa6, 0x78, 0x9a, 0x00, 0x45, 0xa3, 0xad, 0xf2, 0x6c, 0x28, 0x1f, + 0x79, 0x16, 0xac, 0x44, 0x3d, 0x69, 0x70, 0x14, 0x0c, 0xba, 0xd9, 0x48, 0xc2, 0x21, 0x35, 0x19, 0xc6, 0xc8, 0x30, + 0x38, 0xfa, 0x97, 0xb6, 0xca, 0x43, 0x6a, 0x5d, 0x2d, 0x14, 0x32, 0xa3, 0x07, 0x33, 0x3f, 0x98, 0xa8, 0x61, 0x55, + 0x0b, 0xf3, 0x41, 0xba, 0x76, 0x5a, 0x65, 0x94, 0x25, 0xc6, 0x69, 0xb0, 0x30, 0x86, 0x1c, 0x6a, 0x1c, 0xb0, 0xd9, + 0x40, 0xee, 0x6a, 0xce, 0xe6, 0x51, 0x33, 0x6e, 0xaf, 0x6b, 0x46, 0x9f, 0xfa, 0xe2, 0x56, 0x7f, 0x2e, 0xd3, 0x0d, + 0x3b, 0x56, 0xf9, 0x4b, 0xbf, 0xa8, 0xa6, 0x0f, 0x3d, 0xe6, 0x4d, 0x39, 0x18, 0x66, 0x78, 0xf5, 0x59, 0x58, 0x3c, + 0x48, 0x1a, 0x94, 0xf9, 0x52, 0xad, 0x1d, 0x6e, 0x7f, 0x3f, 0x30, 0xf4, 0x66, 0x37, 0x31, 0x49, 0x1a, 0x02, 0xe5, + 0x08, 0x89, 0x08, 0x8e, 0x59, 0xf1, 0x1f, 0x57, 0x95, 0xff, 0xbd, 0x53, 0x5f, 0xd0, 0x83, 0xf0, 0xd1, 0x5e, 0xf7, + 0x34, 0x0a, 0x98, 0xb3, 0x96, 0xed, 0xea, 0xd3, 0x84, 0x1a, 0xd2, 0x5f, 0x11, 0x32, 0x6e, 0x1c, 0xab, 0x7f, 0x74, + 0x53, 0xf2, 0x3b, 0x5d, 0x25, 0xf6, 0xd1, 0x5c, 0x9f, 0xd8, 0xa2, 0x4a, 0x3a, 0x3a, 0x36, 0xa7, 0x2d, 0x29, 0xcd, + 0x49, 0xf9, 0x56, 0x7b, 0x78, 0xda, 0x4a, 0x71, 0xc9, 0xe6, 0x3d, 0xb9, 0x9a, 0x27, 0x59, 0x6d, 0xcb, 0x71, 0x84, + 0x3b, 0xc8, 0xd7, 0xe7, 0x8c, 0xd2, 0xd1, 0x07, 0xab, 0x1f, 0xf7, 0x26, 0x81, 0xcc, 0xd3, 0x13, 0x70, 0xa3, 0x6b, + 0x57, 0x7a, 0x7c, 0x2b, 0x4e, 0xcc, 0x93, 0xf7, 0x43, 0xf6, 0x6b, 0x5c, 0xc9, 0x82, 0x8e, 0x7b, 0x5f, 0x35, 0x4c, + 0xb7, 0x19, 0xd3, 0x7e, 0xa4, 0x18, 0x8c, 0xe6, 0xab, 0x2c, 0x89, 0x0a, 0x62, 0xc1, 0x6b, 0xe2, 0x83, 0xd8, 0x00, + 0x40, 0xce, 0x68, 0x8b, 0x5a, 0x7a, 0x8c, 0x25, 0x51, 0xbc, 0xad, 0x40, 0xcd, 0x79, 0x76, 0x96, 0xd1, 0xaa, 0x3a, + 0xd1, 0xab, 0x53, 0xae, 0xd2, 0xec, 0x22, 0x74, 0x3d, 0x7c, 0x65, 0x29, 0x2a, 0x59, 0x56, 0xbd, 0x0b, 0xd3, 0x57, + 0xec, 0x95, 0x17, 0x48, 0x79, 0x57, 0x4a, 0x4d, 0x21, 0x23, 0x1b, 0x83, 0xc6, 0xd6, 0xd9, 0x4b, 0x2c, 0x6e, 0xb2, + 0x3c, 0x4a, 0x28, 0x7c, 0x31, 0xf7, 0x71, 0x7b, 0x2c, 0x55, 0xc5, 0x9c, 0x43, 0x98, 0x93, 0x2a, 0x9d, 0x74, 0x95, + 0x03, 0xf8, 0xd5, 0x65, 0x10, 0xd6, 0x48, 0xa1, 0x3a, 0xc7, 0x3d, 0x6c, 0x49, 0xa6, 0x63, 0x06, 0x19, 0x8b, 0xee, + 0xfa, 0x3b, 0x2a, 0x9d, 0xc7, 0x41, 0x74, 0x1f, 0xba, 0x5a, 0x21, 0xc2, 0x60, 0x7b, 0xd6, 0x92, 0x2b, 0x9e, 0x2b, + 0x8e, 0xb2, 0x2b, 0x31, 0xb5, 0x3c, 0x1b, 0xb2, 0x6d, 0xd1, 0x15, 0x4b, 0x65, 0x4d, 0x77, 0x57, 0x13, 0xa9, 0xe0, + 0xb1, 0x1f, 0x7f, 0xe0, 0xcb, 0x92, 0x91, 0x53, 0x99, 0xc4, 0xb2, 0x0c, 0x61, 0x6e, 0xdc, 0x10, 0x3c, 0xc1, 0x68, + 0xde, 0x92, 0x79, 0xca, 0x29, 0x85, 0xd2, 0xfb, 0x9f, 0x1b, 0x8f, 0x50, 0x36, 0xdb, 0x30, 0xbd, 0x65, 0xea, 0xbb, + 0xc4, 0xf5, 0xfc, 0x87, 0xe8, 0x94, 0x44, 0x0b, 0xde, 0x9f, 0x27, 0x32, 0xda, 0x54, 0xa8, 0xb0, 0x6e, 0x66, 0xbb, + 0xcf, 0xc1, 0xc6, 0x7f, 0x51, 0x49, 0x86, 0x1c, 0x54, 0x98, 0x5e, 0xb5, 0x63, 0xa1, 0x73, 0xc8, 0x5d, 0x6f, 0x28, + 0x88, 0x8d, 0xc0, 0x6e, 0x68, 0x05, 0x89, 0x34, 0x59, 0x88, 0x7d, 0x36, 0xaa, 0xba, 0x9b, 0x55, 0xa1, 0x06, 0xfc, + 0xf2, 0x17, 0xb1, 0x38, 0xbf, 0x40, 0x52, 0x7d, 0xc1, 0x21, 0x21, 0xf4, 0xc9, 0x6f, 0xc4, 0x5e, 0x0d, 0xbe, 0x88, + 0x95, 0x66, 0xdb, 0x31, 0xfa, 0x99, 0x9f, 0x8f, 0xbb, 0xee, 0x0c, 0x53, 0x74, 0xb8, 0x01, 0x8b, 0x11, 0x43, 0x4e, + 0x52, 0x37, 0xf9, 0x4b, 0x4a, 0x7e, 0x32, 0xbd, 0xf9, 0x06, 0xdf, 0x69, 0x6d, 0x6f, 0xa0, 0x50, 0x88, 0x59, 0x67, + 0x68, 0x70, 0xc3, 0x1e, 0x9e, 0xea, 0x98, 0x59, 0x98, 0xe3, 0x90, 0x24, 0xa2, 0x45, 0x0e, 0x67, 0x88, 0xdf, 0x00, + 0x98, 0x40, 0x93, 0x95, 0x08, 0x19, 0x25, 0xb0, 0x47, 0xf0, 0x82, 0x9b, 0x6d, 0xde, 0xef, 0x79, 0x1e, 0x2e, 0xa4, + 0x56, 0xae, 0xe0, 0x0a, 0x30, 0xd5, 0xb3, 0x6b, 0x49, 0xf1, 0xe1, 0x51, 0xb4, 0x86, 0x78, 0xae, 0x25, 0x94, 0xc5, + 0xce, 0x83, 0x60, 0x55, 0x65, 0x57, 0xd9, 0x19, 0xcc, 0x52, 0x0f, 0x0e, 0x54, 0x71, 0x81, 0x24, 0xdd, 0x18, 0x6f, + 0x53, 0xcc, 0xb2, 0x16, 0x7e, 0xaa, 0x62, 0xde, 0xb0, 0xa9, 0xe0, 0xf0, 0x5a, 0x9d, 0x7f, 0x31, 0xd6, 0x30, 0xa1, + 0x4d, 0x0d, 0xac, 0x04, 0x31, 0x69, 0x58, 0xc2, 0xc6, 0xc1, 0x67, 0xd0, 0x9f, 0x07, 0x4c, 0x33, 0x9c, 0xde, 0x8f, + 0x51, 0xbd, 0x65, 0xf5, 0xc9, 0xf7, 0xd2, 0xf3, 0x46, 0x1d, 0x3d, 0xa8, 0xc4, 0xaa, 0xe5, 0xeb, 0x0c, 0x11, 0xdd, + 0xde, 0xfa, 0x8c, 0xe7, 0xd4, 0x32, 0x04, 0x40, 0xe2, 0x49, 0x9d, 0x19, 0x7b, 0x7c, 0xdc, 0x30, 0x46, 0x52, 0xa5, + 0xb7, 0x2c, 0x42, 0xa6, 0x9f, 0x94, 0x55, 0x0d, 0x87, 0x27, 0x9b, 0x76, 0x25, 0x54, 0x0c, 0xd7, 0x6f, 0x96, 0x17, + 0x50, 0x85, 0xc5, 0x0c, 0xc5, 0x1c, 0x9b, 0xca, 0xd9, 0x78, 0x83, 0x79, 0x06, 0xe3, 0x3c, 0xa5, 0x31, 0x37, 0x54, + 0xa0, 0x5f, 0x2a, 0x47, 0x53, 0x67, 0x62, 0xce, 0x58, 0x9e, 0xfb, 0xb0, 0xe3, 0x73, 0xc7, 0x98, 0x5d, 0x78, 0xee, + 0xee, 0xa9, 0xc3, 0xf6, 0x59, 0x74, 0x19, 0xee, 0x6e, 0x61, 0xc9, 0x9e, 0x92, 0x49, 0x8c, 0x03, 0x58, 0xe7, 0xd1, + 0x95, 0xad, 0xf1, 0x52, 0x06, 0xbb, 0x3f, 0x41, 0x0c, 0xe0, 0x68, 0xc1, 0x60, 0x04, 0xec, 0x5a, 0x7e, 0xed, 0x6a, + 0xac, 0x74, 0xf3, 0x71, 0x60, 0x85, 0x17, 0x99, 0xc0, 0xe5, 0x23, 0x26, 0xd2, 0xe0, 0x7f, 0xde, 0xc7, 0xc9, 0x57, + 0x9b, 0x8e, 0x26, 0xb2, 0xd7, 0x42, 0xbe, 0xf0, 0xaf, 0xe1, 0x6e, 0x1e, 0x98, 0xf2, 0xc5, 0x1e, 0x4f, 0x11, 0x05, + 0x4d, 0x62, 0x6c, 0xf5, 0x8c, 0x8b, 0x3d, 0x73, 0xb5, 0xe1, 0x17, 0x22, 0x8a, 0xbb, 0xbb, 0xb8, 0x2c, 0x04, 0x2c, + 0x99, 0xf0, 0x53, 0xce, 0xd4, 0xc8, 0x94, 0x3d, 0xc4, 0xb7, 0xcb, 0xf0, 0xe1, 0x71, 0x23, 0xf6, 0xc9, 0x5d, 0x11, + 0x9e, 0x48, 0xaa, 0xb0, 0xcf, 0xfc, 0xef, 0x90, 0x31, 0x27, 0xa2, 0xe8, 0xb1, 0x4c, 0x37, 0x06, 0xcf, 0x7f, 0x56, + 0xf1, 0x64, 0x55, 0x00, 0xb6, 0x46, 0x05, 0xe9, 0x97, 0x80, 0x73, 0x0a, 0x40, 0x3d, 0x0c, 0x63, 0x20, 0xc5, 0x9c, + 0x42, 0xe0, 0xe8, 0x92, 0x76, 0xc0, 0xf0, 0xb3, 0x59, 0xd0, 0x63, 0xf1, 0x5b, 0xba, 0xdc, 0x6d, 0xce, 0xcf, 0xd6, + 0x28, 0x6f, 0x2a, 0x77, 0xd5, 0xb0, 0xca, 0x4c, 0x4d, 0x61, 0xb2, 0xd8, 0x9b, 0xb9, 0x0e, 0xdf, 0xf9, 0x63, 0x5f, + 0xbb, 0xc4, 0xc1, 0xc8, 0x8d, 0xcc, 0x15, 0x2e, 0x3c, 0x98, 0x76, 0xf2, 0x0a, 0x88, 0x9a, 0xad, 0x04, 0x57, 0x02, + 0xad, 0x07, 0xa7, 0x0e, 0x77, 0x0a, 0x58, 0x41, 0x20, 0xf3, 0xfa, 0xab, 0x3e, 0x39, 0x90, 0xd1, 0xe6, 0x0a, 0x19, + 0xf4, 0xdc, 0xea, 0x05, 0x5a, 0xe5, 0x7d, 0xab, 0xfb, 0x39, 0x79, 0x63, 0xde, 0x75, 0x1f, 0x81, 0xc9, 0xf7, 0x8c, + 0xc4, 0x86, 0x2c, 0xaf, 0x85, 0xc2, 0x24, 0x01, 0x3d, 0x0e, 0xaa, 0x0a, 0x89, 0xd4, 0xa1, 0x6c, 0xd4, 0x0c, 0x15, + 0xc2, 0xf4, 0xfa, 0x07, 0x80, 0x80, 0xa3, 0x94, 0x42, 0x79, 0x22, 0xaa, 0x32, 0x02, 0x08, 0x8c, 0x0d, 0xd0, 0xb0, + 0x0c, 0x4c, 0x61, 0x9b, 0x51, 0xb4, 0xe5, 0x74, 0xe9, 0x6e, 0xbc, 0x2f, 0x47, 0xe6, 0xbc, 0x1b, 0x3c, 0x4b, 0xd0, + 0x6e, 0xec, 0xeb, 0x38, 0x86, 0x7e, 0x2a, 0xfa, 0x47, 0xb0, 0x83, 0x73, 0x58, 0x82, 0x82, 0x53, 0x42, 0x9f, 0x33, + 0xff, 0x83, 0xaf, 0xc4, 0xeb, 0x9e, 0xb6, 0xb8, 0xb7, 0x63, 0xc7, 0xcc, 0xca, 0x8f, 0x4d, 0x96, 0x5c, 0xcb, 0x90, + 0x44, 0x79, 0xcd, 0xa5, 0x63, 0xd0, 0x94, 0xc8, 0xcd, 0x07, 0x81, 0xa4, 0xbd, 0x41, 0xe5, 0x47, 0x9b, 0xd3, 0xfe, + 0x48, 0x08, 0xb1, 0x5a, 0x6a, 0xe6, 0xe2, 0x4b, 0x8a, 0x05, 0x50, 0x70, 0x1f, 0xc0, 0xf9, 0x3b, 0x71, 0xfd, 0xbb, + 0xe8, 0xc0, 0xa1, 0x8f, 0x00, 0x06, 0xe0, 0xad, 0x54, 0x5b, 0x79, 0x13, 0x50, 0x5a, 0x01, 0x90, 0x6b, 0x53, 0x19, + 0xe0, 0x0d, 0xf9, 0x0b, 0x1e, 0xd9, 0x97, 0x29, 0x50, 0x8c, 0xe2, 0xc6, 0xbb, 0x4c, 0xe5, 0xe5, 0xdd, 0x31, 0xe7, + 0x82, 0xdd, 0xdc, 0x30, 0xaf, 0xda, 0x44, 0x99, 0xb4, 0x5d, 0x0c, 0x62, 0x9c, 0x12, 0xa4, 0x28, 0x01, 0xd2, 0xf7, + 0x0f, 0x66, 0x21, 0x7a, 0xfe, 0xee, 0xd1, 0x5d, 0x65, 0xae, 0xc3, 0x30, 0x9a, 0xec, 0x1d, 0x51, 0x0b, 0x72, 0xba, + 0x3a, 0x26, 0xdf, 0x27, 0x07, 0xe1, 0x5f, 0x12, 0xf5, 0x33, 0x55, 0x82, 0xa6, 0xfa, 0xa6, 0x8a, 0x38, 0xa8, 0xf1, + 0x09, 0x88, 0xda, 0x32, 0xa9, 0x09, 0x73, 0x25, 0xea, 0x93, 0xeb, 0x44, 0x60, 0x5a, 0xfd, 0xb3, 0x1f, 0x5f, 0xfd, + 0xc0, 0x60, 0x7b, 0xbf, 0x77, 0xf1, 0x75, 0xa6, 0x0f, 0xe7, 0xef, 0xd8, 0xbd, 0xfb, 0x3c, 0xb8, 0x71, 0x5c, 0x5d, + 0xd6, 0x23, 0x49, 0x23, 0x33, 0xc0, 0x7b, 0xca, 0xa0, 0x61, 0x2f, 0xca, 0xe6, 0xcc, 0x35, 0xbb, 0xcf, 0xba, 0xca, + 0x5d, 0x40, 0x3f, 0x22, 0x69, 0x35, 0xa1, 0xb8, 0x00, 0x6e, 0xe7, 0x31, 0xcd, 0x0a, 0xff, 0x83, 0x42, 0xcd, 0x04, + 0x7b, 0x19, 0x19, 0xa5, 0x2f, 0x23, 0x98, 0xaf, 0x16, 0x41, 0xb6, 0xac, 0x42, 0xbb, 0x8f, 0x1a, 0x6c, 0xd6, 0xae, + 0xcd, 0xb3, 0x7b, 0xcb, 0x01, 0x39, 0x13, 0x44, 0xe3, 0x45, 0xad, 0xe4, 0x59, 0x4f, 0xf5, 0xaa, 0xe7, 0x88, 0x9b, + 0xae, 0xdc, 0xab, 0x47, 0xa6, 0xf9, 0x78, 0x85, 0x77, 0x3f, 0xea, 0x22, 0xda, 0x95, 0xb5, 0x00, 0x56, 0x16, 0x43, + 0xf2, 0x3d, 0xeb, 0xef, 0x99, 0x74, 0x09, 0x16, 0x90, 0xfa, 0xc4, 0xeb, 0xda, 0x75, 0xd0, 0x91, 0x93, 0xba, 0xfd, + 0x28, 0x0a, 0x66, 0xa5, 0x45, 0x26, 0xe5, 0x89, 0xa4, 0x2b, 0xc9, 0x47, 0xae, 0x62, 0x66, 0x98, 0xe6, 0xd2, 0x19, + 0xf6, 0xa4, 0xbf, 0xaa, 0xda, 0xab, 0xed, 0x39, 0x04, 0xc0, 0x35, 0x4f, 0x21, 0x56, 0xef, 0x56, 0xd1, 0xc2, 0x9d, + 0xf1, 0x6e, 0xff, 0xa2, 0xb5, 0xe3, 0x61, 0xec, 0xd6, 0x30, 0x0e, 0x32, 0x67, 0x05, 0xf3, 0x9b, 0x1c, 0xd3, 0x70, + 0xcd, 0x68, 0xa3, 0x0b, 0x3e, 0xc5, 0xbe, 0x5c, 0xbd, 0x8f, 0xba, 0x87, 0x0a, 0x91, 0xde, 0x83, 0x67, 0x84, 0xaf, + 0x37, 0x7f, 0x6d, 0xd4, 0x6b, 0xdf, 0xd1, 0x58, 0xde, 0x58, 0x3a, 0x82, 0xc6, 0x3f, 0x26, 0x38, 0xa3, 0x30, 0xdf, + 0xc0, 0x9b, 0x26, 0x93, 0xb5, 0x09, 0xc7, 0x8e, 0xdc, 0x26, 0xc5, 0xa6, 0xa5, 0x2a, 0xdf, 0x25, 0xb0, 0x92, 0x5b, + 0xa6, 0xfd, 0xe2, 0x9e, 0x52, 0x8f, 0x0d, 0xc1, 0x42, 0xc5, 0x82, 0x00, 0x35, 0xe6, 0xaa, 0xbd, 0x99, 0x48, 0x06, + 0xa7, 0xb4, 0xfd, 0x6c, 0xfb, 0x0c, 0xb3, 0xb3, 0x26, 0x12, 0x82, 0xb3, 0x42, 0xcb, 0xa6, 0x9b, 0xb4, 0x91, 0x00, + 0x8d, 0x54, 0xa3, 0xd0, 0x64, 0x82, 0xc6, 0xce, 0x7b, 0x41, 0xee, 0x86, 0x0e, 0xa9, 0xeb, 0x0c, 0x4a, 0xf0, 0x85, + 0x23, 0x31, 0x4b, 0x77, 0x41, 0x69, 0x0d, 0xdd, 0x63, 0xa8, 0x5e, 0x6f, 0xbf, 0x0b, 0x9e, 0x91, 0xf1, 0xa6, 0x11, + 0x68, 0x8e, 0x41, 0x20, 0xdf, 0x04, 0x63, 0x02, 0x0e, 0xbc, 0xf3, 0xf2, 0xfb, 0x48, 0x44, 0xca, 0x0f, 0xb0, 0x01, + 0xbd, 0xcc, 0x37, 0x5e, 0x04, 0x2b, 0x52, 0xa5, 0x19, 0x16, 0x66, 0x8f, 0xc1, 0xbc, 0xed, 0x8e, 0x7e, 0x32, 0xfc, + 0xcc, 0x04, 0x2f, 0xf9, 0x93, 0xe7, 0xf4, 0xce, 0x10, 0x1e, 0x62, 0xf0, 0xc1, 0x58, 0xf5, 0xc0, 0xe3, 0x94, 0xc6, + 0x0d, 0x01, 0x2e, 0x9f, 0xfd, 0xc8, 0x7c, 0x10, 0xbb, 0x11, 0x80, 0x67, 0x17, 0x57, 0x19, 0x78, 0x9b, 0xa9, 0xab, + 0x93, 0x96, 0x4e, 0xee, 0x17, 0x62, 0xc4, 0x0c, 0x52, 0x31, 0x9f, 0xde, 0xc8, 0x28, 0x0d, 0xe2, 0xa2, 0x64, 0xc6, + 0x25, 0xc5, 0xae, 0x39, 0x6f, 0xe8, 0x96, 0x9f, 0x33, 0x45, 0xed, 0x9d, 0x1d, 0xeb, 0x78, 0xff, 0x8f, 0xf3, 0x27, + 0x5d, 0x30, 0xba, 0xbc, 0xb5, 0xae, 0x66, 0xdb, 0xf3, 0x4d, 0x4d, 0xa9, 0x81, 0xe1, 0x37, 0x26, 0xfc, 0xd1, 0xc7, + 0x4b, 0x4d, 0x41, 0x0d, 0x8d, 0x5d, 0x64, 0x53, 0x8a, 0xac, 0xf0, 0xc6, 0x31, 0x65, 0xbe, 0x04, 0x52, 0x2c, 0xc6, + 0x4f, 0x3e, 0x37, 0x1a, 0x5c, 0x33, 0x52, 0x1c, 0x0e, 0x09, 0xea, 0x45, 0x91, 0xb7, 0x9f, 0x3a, 0xf9, 0x1c, 0x57, + 0x6f, 0xe6, 0x37, 0x43, 0x60, 0xa6, 0xdb, 0x56, 0x5a, 0xac, 0x9b, 0xb6, 0xe2, 0xab, 0xb5, 0x4a, 0xe3, 0x29, 0x5a, + 0xe3, 0xbb, 0x1a, 0x87, 0xe9, 0x95, 0xea, 0xab, 0xe1, 0xd7, 0xbd, 0x8d, 0xcd, 0x8c, 0x66, 0x2e, 0x74, 0x90, 0x38, + 0x24, 0xbd, 0x54, 0x4d, 0xab, 0xa8, 0xc6, 0x9e, 0x1e, 0xab, 0x1a, 0x94, 0x88, 0x77, 0xe8, 0x24, 0xd6, 0x30, 0x5b, + 0x8f, 0x72, 0xfa, 0x61, 0xb5, 0x85, 0x5a, 0xb1, 0x33, 0x31, 0xa9, 0xa9, 0x37, 0x96, 0xe5, 0xd5, 0x56, 0xd5, 0xe7, + 0x74, 0xa0, 0xc3, 0x9f, 0xc3, 0x1d, 0x84, 0xef, 0x6e, 0x05, 0x9a, 0x10, 0x64, 0x2e, 0xb6, 0xf2, 0x75, 0x88, 0xef, + 0xd2, 0x1e, 0x8f, 0x25, 0x56, 0x2b, 0x12, 0x8d, 0x6f, 0xaa, 0x2a, 0x0c, 0xc8, 0x65, 0x46, 0x15, 0x4b, 0x7b, 0x65, + 0x54, 0xc8, 0x8a, 0xec, 0x8d, 0x04, 0x69, 0x55, 0xf0, 0x42, 0x90, 0xd2, 0x2c, 0x9a, 0x98, 0xcc, 0x5f, 0x0d, 0xef, + 0x92, 0x92, 0xcc, 0x26, 0xf8, 0xd3, 0x26, 0xce, 0x66, 0x14, 0x70, 0xb7, 0x52, 0x37, 0xb8, 0xd8, 0x9a, 0x71, 0x55, + 0xae, 0x20, 0xb7, 0x05, 0x07, 0x21, 0xab, 0xa2, 0x68, 0x61, 0x9f, 0xdf, 0xf9, 0xa7, 0x48, 0x53, 0x00, 0x40, 0xe4, + 0x35, 0xa1, 0x29, 0xbb, 0x69, 0x41, 0x66, 0xe9, 0x60, 0x19, 0xc0, 0x07, 0x2c, 0x85, 0xf2, 0xd0, 0x79, 0x1e, 0xd7, + 0xdd, 0xcb, 0x4c, 0x2d, 0x2a, 0x2d, 0x1b, 0x74, 0x4f, 0x07, 0x87, 0x5c, 0xaf, 0x74, 0xfa, 0xef, 0x8f, 0xd4, 0x56, + 0x5e, 0xa0, 0x0e, 0x2a, 0x7c, 0xf7, 0x51, 0xb5, 0x10, 0xe3, 0x50, 0xab, 0xff, 0xad, 0x28, 0xd1, 0x39, 0x05, 0x4f, + 0xa2, 0xd7, 0x3f, 0x56, 0xac, 0xd3, 0x2b, 0x26, 0x91, 0xf8, 0x72, 0x22, 0xa8, 0xdb, 0x66, 0x57, 0x5d, 0x72, 0xda, + 0x5c, 0x65, 0xf6, 0x9f, 0x0e, 0x78, 0xf6, 0xad, 0x77, 0x7e, 0xa7, 0x17, 0x77, 0x90, 0x44, 0xa1, 0xd0, 0xf3, 0x21, + 0x3e, 0xa0, 0xa3, 0xb2, 0xfa, 0x13, 0x91, 0x14, 0xab, 0x96, 0xcb, 0xd0, 0x80, 0x22, 0xc5, 0x4d, 0x19, 0xd5, 0x78, + 0xd0, 0xeb, 0x19, 0xbb, 0x04, 0x69, 0x74, 0xb3, 0x57, 0x2a, 0xc1, 0x49, 0x2b, 0xad, 0x3e, 0x2f, 0x41, 0x90, 0x6d, + 0xbc, 0x93, 0x5c, 0xa5, 0x58, 0x61, 0xf6, 0x58, 0x96, 0x95, 0x51, 0xd7, 0x50, 0x8c, 0xce, 0xb2, 0x8a, 0x5a, 0xe7, + 0xda, 0x6a, 0xa7, 0x08, 0xc5, 0x3a, 0x16, 0x00, 0x9b, 0x16, 0x4e, 0x07, 0x69, 0x61, 0x0b, 0x7a, 0x3a, 0xa9, 0xc6, + 0x25, 0xcd, 0x8e, 0xb1, 0xc8, 0xbb, 0x85, 0x41, 0xd0, 0x44, 0x5f, 0x77, 0x70, 0x53, 0x1e, 0x2b, 0x8a, 0x3a, 0xb8, + 0xde, 0xb1, 0x0c, 0xaf, 0x0e, 0xcd, 0x78, 0x81, 0xae, 0xa4, 0xec, 0x08, 0x95, 0x2b, 0x61, 0x27, 0x6d, 0x4a, 0x07, + 0x6d, 0x15, 0x7e, 0x68, 0x9e, 0x94, 0x8e, 0x05, 0xaf, 0x58, 0xa1, 0xc4, 0x35, 0xdd, 0x79, 0x0f, 0xeb, 0xa5, 0x9b, + 0x18, 0x23, 0xe4, 0x2b, 0xe2, 0xaf, 0x91, 0x22, 0xcc, 0x0d, 0x64, 0x0d, 0x2c, 0x64, 0x34, 0xc5, 0x24, 0x4c, 0x90, + 0xb1, 0xc7, 0x98, 0x78, 0xd1, 0x2d, 0x2e, 0xfd, 0x19, 0xb4, 0x41, 0x9b, 0x4d, 0x2b, 0xe9, 0x3e, 0x70, 0x85, 0xf6, + 0x7b, 0x3c, 0xe9, 0x90, 0x8f, 0x2d, 0x44, 0xcf, 0x14, 0x5c, 0xbe, 0x2e, 0xa1, 0x51, 0x7f, 0x00, 0x65, 0xed, 0x58, + 0x8a, 0xcd, 0x9a, 0x84, 0x1d, 0x98, 0x4e, 0x94, 0xf6, 0x7d, 0xf0, 0xa9, 0xc7, 0x5f, 0xbe, 0xde, 0x23, 0xf8, 0x0c, + 0x65, 0x4f, 0x14, 0x9b, 0x29, 0x86, 0x8a, 0xa6, 0xa6, 0xa0, 0x99, 0x0f, 0xce, 0xc2, 0x6d, 0xf1, 0x7a, 0x42, 0x2f, + 0x57, 0xbb, 0x75, 0x4f, 0xe5, 0xe3, 0x8a, 0x34, 0x40, 0xab, 0xcf, 0x1a, 0x95, 0x0f, 0xe6, 0xc9, 0x3d, 0xaf, 0x74, + 0xcf, 0x0f, 0xe8, 0xa1, 0xd1, 0xee, 0xe2, 0x4d, 0xd7, 0x32, 0x3e, 0xb4, 0x9b, 0xac, 0xcf, 0x60, 0x11, 0x7a, 0xa8, + 0xa1, 0x14, 0xcd, 0xe1, 0x26, 0xab, 0xd9, 0xf0, 0xee, 0x8d, 0x66, 0x6d, 0x29, 0x25, 0x7f, 0x6f, 0xe7, 0xc5, 0x36, + 0x9b, 0x2a, 0x9c, 0xde, 0x0f, 0xa4, 0x77, 0x09, 0x14, 0xcd, 0x91, 0xef, 0x4e, 0xa7, 0xa9, 0x7c, 0xd0, 0x4f, 0x80, + 0xa1, 0x82, 0xef, 0x1e, 0x69, 0x74, 0x67, 0x4d, 0x71, 0xbe, 0x82, 0x4b, 0x0f, 0xa3, 0xc6, 0xb6, 0x4b, 0x4f, 0x04, + 0xe1, 0xd4, 0xe2, 0x1e, 0xe9, 0x25, 0x45, 0x4b, 0x1d, 0x29, 0xe1, 0x9f, 0x22, 0x9c, 0x53, 0x8d, 0x1d, 0xed, 0x6c, + 0x1a, 0x6f, 0xa8, 0x20, 0x3d, 0x6a, 0x72, 0x4d, 0xfb, 0x12, 0x0a, 0xe0, 0xbf, 0x9d, 0xba, 0x12, 0xe1, 0x32, 0x21, + 0x37, 0xab, 0x8a, 0x4a, 0x83, 0x32, 0x00, 0x28, 0xbf, 0x5d, 0xcb, 0xe8, 0x1a, 0x3f, 0x5a, 0xa8, 0xcb, 0x12, 0x73, + 0xa0, 0x83, 0x16, 0x67, 0x77, 0x03, 0x2d, 0x92, 0x6d, 0x73, 0xea, 0xae, 0x51, 0xb5, 0xc5, 0x93, 0xc0, 0x4b, 0x44, + 0x63, 0x39, 0xeb, 0xe7, 0xf0, 0x6d, 0xda, 0x5e, 0x0f, 0xce, 0x3a, 0x40, 0xb7, 0xb0, 0xa0, 0xda, 0x9a, 0xb5, 0xfc, + 0x5e, 0x81, 0x0f, 0xf8, 0x51, 0x6c, 0xec, 0x3c, 0x76, 0x42, 0xcd, 0xc9, 0x0e, 0xbd, 0xb9, 0x49, 0x39, 0x27, 0xca, + 0x9c, 0x4e, 0x62, 0x1c, 0x85, 0x26, 0xea, 0x8c, 0x30, 0xf9, 0xd4, 0x6b, 0x32, 0x03, 0x1e, 0x7d, 0xe3, 0x22, 0x61, + 0x1f, 0x9c, 0x53, 0xc9, 0x96, 0xb5, 0x66, 0x93, 0x9b, 0x9f, 0x49, 0xb1, 0xc9, 0x98, 0x60, 0xbd, 0xa0, 0xf7, 0x37, + 0x44, 0x42, 0x18, 0x37, 0x84, 0x62, 0x68, 0x6a, 0x52, 0xe3, 0x69, 0x73, 0xa5, 0x64, 0x46, 0x97, 0x54, 0xbf, 0xac, + 0x2e, 0x28, 0x24, 0x5a, 0x51, 0xf0, 0xcb, 0x16, 0x8e, 0xbd, 0x46, 0x10, 0x7b, 0x06, 0xa0, 0x0f, 0x1d, 0xa0, 0x89, + 0x91, 0xdc, 0x6a, 0x6a, 0x4f, 0xb6, 0x04, 0x5e, 0x79, 0x19, 0xe2, 0x0d, 0xfb, 0x4d, 0x50, 0x09, 0x3c, 0xc7, 0xbe, + 0x59, 0x0e, 0x79, 0x0e, 0x97, 0xd5, 0x5d, 0x7d, 0x8a, 0xe8, 0xdc, 0xf9, 0xc2, 0xc3, 0x14, 0xbd, 0x43, 0xb5, 0x8f, + 0xd0, 0xd5, 0x2b, 0x79, 0x15, 0x73, 0x90, 0x83, 0x45, 0xdd, 0x98, 0x6c, 0x62, 0x57, 0xa7, 0x9e, 0x6b, 0x40, 0xb7, + 0xc7, 0x6e, 0x06, 0x62, 0x0f, 0x23, 0x26, 0xeb, 0x78, 0x4a, 0x6f, 0x11, 0x31, 0xda, 0x62, 0x22, 0xa9, 0x99, 0x34, + 0x6b, 0x52, 0x03, 0x39, 0x2d, 0xc2, 0x1c, 0xd4, 0x4f, 0x74, 0x8e, 0x3d, 0x12, 0xba, 0xb3, 0x6c, 0xbb, 0x46, 0x25, + 0x93, 0xdc, 0x61, 0xae, 0x50, 0x49, 0x08, 0xa9, 0x28, 0xbb, 0x92, 0x29, 0x30, 0xa5, 0x31, 0xe1, 0x1a, 0x57, 0x83, + 0x22, 0x43, 0x97, 0x0a, 0x6a, 0x6f, 0x9d, 0xa4, 0xd4, 0x39, 0x67, 0x0e, 0xf0, 0x29, 0x94, 0x3f, 0xab, 0x9a, 0x87, + 0x4d, 0xad, 0x40, 0xc5, 0xbd, 0x7a, 0xb9, 0x58, 0xf0, 0x66, 0xfe, 0x04, 0x85, 0x41, 0xa1, 0xaf, 0xa8, 0x29, 0xcf, + 0xe5, 0xa1, 0xac, 0x44, 0xdf, 0x8c, 0xbf, 0xa7, 0xf2, 0x8a, 0xc0, 0x65, 0xbe, 0x42, 0x24, 0xa6, 0xdf, 0x78, 0xa8, + 0x3f, 0x2d, 0x0b, 0x9b, 0x2a, 0x62, 0xfa, 0xf7, 0x93, 0xbf, 0x36, 0x90, 0xe0, 0x87, 0x9e, 0xd8, 0x2f, 0x5a, 0x08, + 0x3a, 0x16, 0x19, 0x54, 0x98, 0x23, 0x72, 0xa2, 0x00, 0x4e, 0xb2, 0x47, 0xd7, 0xcb, 0x4b, 0x6a, 0xe8, 0x08, 0x6e, + 0x81, 0x9c, 0xb0, 0xa2, 0x6a, 0x24, 0xcf, 0xfe, 0xde, 0x76, 0x4b, 0xaa, 0x53, 0x0e, 0x03, 0x36, 0x7f, 0xed, 0x69, + 0x19, 0xba, 0x7b, 0x1b, 0x04, 0xb0, 0x05, 0xbd, 0x1e, 0x87, 0x1f, 0x01, 0x34, 0x82, 0xc9, 0x0c, 0x19, 0xcb, 0xa7, + 0x86, 0xea, 0x55, 0x5f, 0x9e, 0x1c, 0x85, 0xb8, 0x75, 0x8a, 0x8c, 0x28, 0x5b, 0x41, 0x2e, 0x63, 0xcb, 0x6e, 0xbf, + 0x95, 0xfe, 0xc0, 0x0a, 0xaa, 0x6b, 0x15, 0x82, 0x1c, 0x8c, 0x49, 0x78, 0x23, 0x51, 0x61, 0xcd, 0xdf, 0x74, 0x06, + 0x78, 0xcb, 0x53, 0x38, 0x84, 0x7b, 0xbb, 0x03, 0x6a, 0x7d, 0x0d, 0x41, 0xa1, 0xa9, 0x1c, 0x38, 0x9b, 0xa2, 0x35, + 0x47, 0x1c, 0x9c, 0xcf, 0x61, 0xc3, 0xf2, 0x1e, 0x77, 0x33, 0xc4, 0x32, 0x88, 0x14, 0xd1, 0xe1, 0x90, 0xa5, 0xc5, + 0xa2, 0xc6, 0x16, 0xab, 0x90, 0xf6, 0x29, 0xce, 0x08, 0x37, 0x31, 0xa5, 0x38, 0xd1, 0x87, 0x97, 0x60, 0x78, 0x06, + 0xce, 0x30, 0x6b, 0xd7, 0x1e, 0x88, 0xdb, 0x1b, 0x3a, 0x5f, 0x7e, 0xc9, 0x8e, 0x32, 0x7f, 0xae, 0x3a, 0x03, 0x96, + 0xcf, 0x7e, 0xde, 0x13, 0xa0, 0xa7, 0x9f, 0x38, 0x6c, 0xab, 0x9f, 0x4a, 0xcf, 0x46, 0x6a, 0xac, 0xee, 0x99, 0x4e, + 0xce, 0xdc, 0xd6, 0x1b, 0x1e, 0xe8, 0xcc, 0x82, 0x84, 0x0f, 0x5e, 0x63, 0x1f, 0xb4, 0x91, 0x4a, 0xd2, 0x57, 0x3c, + 0x51, 0xde, 0x5b, 0xe0, 0x57, 0x0f, 0x64, 0xe5, 0x91, 0x0b, 0x12, 0xf4, 0x12, 0xda, 0xf3, 0x79, 0x74, 0xc6, 0xc1, + 0xb0, 0x37, 0xca, 0x27, 0xc6, 0x4b, 0x4c, 0xfa, 0xe6, 0xdb, 0x61, 0x88, 0x48, 0xdf, 0x47, 0x54, 0xe0, 0x84, 0x2c, + 0xa9, 0xf2, 0xf0, 0x46, 0xc2, 0x21, 0x9e, 0x4a, 0x74, 0xa6, 0x4e, 0xcd, 0x59, 0xb5, 0x58, 0x6c, 0x59, 0x79, 0xf5, + 0xda, 0x51, 0xe4, 0x77, 0x6c, 0xd5, 0x92, 0x76, 0xb2, 0xaf, 0x94, 0xa1, 0x55, 0x89, 0x33, 0x18, 0x19, 0x5d, 0xb0, + 0xd5, 0x8b, 0x24, 0x1f, 0xb2, 0x26, 0x2a, 0x1a, 0xd6, 0x95, 0xc9, 0xcc, 0x28, 0xb9, 0x95, 0xf5, 0x45, 0xda, 0xb1, + 0x5d, 0x78, 0xad, 0x42, 0x25, 0xe9, 0xa0, 0x9e, 0x93, 0xe4, 0xfc, 0x40, 0xda, 0x3a, 0xca, 0xdf, 0xb6, 0xaa, 0x93, + 0xeb, 0xa1, 0xa7, 0xec, 0x2a, 0x1d, 0x08, 0xf3, 0x55, 0xb2, 0x06, 0x81, 0x81, 0x7c, 0x77, 0x60, 0x3b, 0x06, 0x9d, + 0xbe, 0xdd, 0x1e, 0x99, 0x74, 0x3c, 0x05, 0x3a, 0x65, 0x14, 0x30, 0x4d, 0x14, 0x46, 0x57, 0x6b, 0xcc, 0xfe, 0xee, + 0x78, 0xe9, 0xbb, 0x82, 0x03, 0x55, 0xdc, 0xe9, 0x41, 0xf8, 0xe1, 0xde, 0xd5, 0xc6, 0xe0, 0x87, 0x53, 0x47, 0x4f, + 0xcc, 0xcc, 0x52, 0xcb, 0xbf, 0xaf, 0x77, 0x87, 0xdf, 0xc7, 0x29, 0xc4, 0xf0, 0x81, 0x5e, 0xc7, 0xee, 0x8b, 0xfa, + 0x57, 0xf1, 0x32, 0x97, 0xf8, 0xc2, 0x0f, 0x5d, 0xd1, 0xec, 0xa6, 0xde, 0x95, 0x35, 0xcc, 0xa1, 0x6f, 0xc5, 0xda, + 0x48, 0xe5, 0x16, 0x58, 0xf7, 0x66, 0xb9, 0x13, 0x07, 0x0c, 0x38, 0xd7, 0x73, 0xc4, 0xc4, 0x67, 0xa6, 0x5a, 0xcf, + 0x24, 0x9d, 0x9a, 0xe9, 0x48, 0x86, 0x51, 0x0b, 0x58, 0x89, 0x89, 0x50, 0xdc, 0xe1, 0x01, 0x51, 0xd9, 0x21, 0x8f, + 0x7e, 0x2c, 0xf4, 0x6d, 0x4a, 0x28, 0x1b, 0xbe, 0x82, 0x97, 0x27, 0x97, 0x3f, 0x18, 0xd9, 0x78, 0xdc, 0xfc, 0xb1, + 0xd2, 0x8b, 0x97, 0x33, 0x4f, 0xde, 0xa0, 0x88, 0xd5, 0x79, 0x82, 0xe5, 0x01, 0x12, 0x9b, 0x33, 0x37, 0xd0, 0x49, + 0x30, 0xf5, 0x46, 0x37, 0xbf, 0x14, 0xc1, 0xad, 0xb6, 0x0d, 0x4e, 0xf9, 0x99, 0xe9, 0x1e, 0x85, 0xe2, 0x27, 0x3f, + 0xc3, 0x92, 0x59, 0x36, 0x4e, 0xc0, 0xc9, 0xb2, 0x2b, 0x0f, 0x3e, 0x6d, 0x7d, 0xf1, 0x61, 0x7d, 0x61, 0xfd, 0x79, + 0x8a, 0xb1, 0xfe, 0xfc, 0x92, 0xde, 0xdc, 0x3b, 0xac, 0x83, 0xd8, 0x7a, 0x74, 0x83, 0xfe, 0x29, 0x35, 0xec, 0xfc, + 0xb1, 0x33, 0x84, 0x0a, 0x11, 0x6a, 0xbc, 0x41, 0x58, 0x70, 0xee, 0x8e, 0x94, 0xac, 0x9a, 0x31, 0x4e, 0x2b, 0x49, + 0x6b, 0xc0, 0xbe, 0xd2, 0xa4, 0xb4, 0xca, 0xed, 0x6a, 0x45, 0xcc, 0x2e, 0x4d, 0xed, 0x50, 0x60, 0xd1, 0x77, 0x52, + 0x92, 0x24, 0xe7, 0xa5, 0x67, 0xf7, 0x48, 0x6d, 0x47, 0x40, 0xe5, 0xea, 0xbe, 0x40, 0x30, 0x6e, 0x6f, 0x77, 0x07, + 0xaa, 0xa6, 0xbe, 0x83, 0x41, 0x3d, 0xf2, 0x52, 0xff, 0x1f, 0x6c, 0x44, 0x6f, 0x5e, 0xfa, 0x0e, 0x50, 0x36, 0xa2, + 0xd9, 0xbb, 0x20, 0xd7, 0x01, 0xf2, 0x8e, 0x19, 0x4e, 0x55, 0xe5, 0x30, 0xca, 0xe8, 0xaf, 0x3e, 0x4b, 0xe1, 0xd2, + 0x7b, 0x87, 0x79, 0xb2, 0x49, 0x07, 0x9e, 0x05, 0x5b, 0xba, 0xf7, 0x12, 0x49, 0x60, 0xd9, 0x21, 0x21, 0x57, 0x69, + 0xdf, 0xa0, 0xcb, 0xaa, 0x53, 0x6d, 0x52, 0xf4, 0xd3, 0x8e, 0x1b, 0x3c, 0x62, 0x5a, 0x70, 0x8b, 0x74, 0x84, 0x5a, + 0x25, 0x84, 0x31, 0xe3, 0x29, 0x92, 0xd5, 0x80, 0xf0, 0x5a, 0x4d, 0x3c, 0x25, 0x8d, 0x28, 0x0f, 0x0c, 0x13, 0xdb, + 0x7b, 0xb1, 0xf3, 0x63, 0x17, 0xc4, 0x98, 0xdd, 0xba, 0xfb, 0x55, 0xf1, 0xa1, 0x3f, 0xc3, 0xb3, 0xb6, 0x3b, 0x99, + 0x4b, 0x48, 0x90, 0x38, 0xf4, 0x2f, 0x54, 0xfc, 0x5a, 0x23, 0x3c, 0x01, 0x0d, 0x56, 0x09, 0x86, 0xa9, 0x05, 0xbe, + 0x9d, 0xde, 0x67, 0xbd, 0x5e, 0x3d, 0x61, 0xe2, 0xe8, 0x16, 0xe0, 0xda, 0x23, 0x15, 0x97, 0xd3, 0x87, 0x03, 0xbb, + 0xaf, 0x96, 0x1a, 0xe8, 0xaa, 0x64, 0xb2, 0xf8, 0x4b, 0x6f, 0x9c, 0xff, 0x4d, 0x43, 0xd1, 0x81, 0x25, 0xfa, 0x39, + 0xbd, 0x16, 0x3b, 0xf7, 0xc9, 0x27, 0x12, 0x30, 0x1a, 0xc3, 0x93, 0x9c, 0x1e, 0x58, 0x15, 0x6d, 0x26, 0xdc, 0x9f, + 0x17, 0xa7, 0x09, 0x34, 0x76, 0xa0, 0x17, 0x37, 0x99, 0x6f, 0xe3, 0xdd, 0x95, 0xad, 0xee, 0xfd, 0x61, 0x59, 0xd5, + 0xbc, 0x4d, 0xea, 0xa8, 0xbb, 0x48, 0x10, 0xe2, 0x52, 0x47, 0xa8, 0x49, 0x57, 0x6c, 0xad, 0x39, 0x73, 0x71, 0x9a, + 0xc7, 0x56, 0x7e, 0x96, 0x6d, 0x79, 0x20, 0xeb, 0x85, 0x58, 0xd5, 0x6c, 0xd4, 0x04, 0x29, 0x43, 0x5d, 0x88, 0xee, + 0x32, 0xa2, 0x82, 0x16, 0xab, 0x5f, 0xd7, 0xb1, 0x32, 0xdd, 0xa7, 0xe9, 0x68, 0x42, 0xe0, 0xf7, 0x71, 0x00, 0x46, + 0xb4, 0xfe, 0x15, 0x83, 0xa6, 0xd6, 0x28, 0x39, 0x8d, 0x4e, 0x84, 0x88, 0x43, 0xdb, 0x34, 0x06, 0xbc, 0x74, 0xf9, + 0x99, 0x1c, 0x62, 0xaa, 0x05, 0xc6, 0x1b, 0x18, 0xaf, 0x87, 0xb6, 0x98, 0x72, 0xe7, 0xe9, 0x55, 0x65, 0x94, 0xf1, + 0xc9, 0xfd, 0xcc, 0xeb, 0x9e, 0x7d, 0x40, 0xc9, 0x00, 0x84, 0x0a, 0xaf, 0x55, 0x18, 0x32, 0x58, 0x25, 0xf8, 0xf3, + 0x16, 0x63, 0xf0, 0x73, 0xdb, 0x9c, 0x87, 0xf9, 0x10, 0x2b, 0xec, 0x30, 0x0b, 0xf4, 0xe9, 0x79, 0x01, 0xd3, 0x03, + 0xd9, 0xd9, 0xb3, 0xd2, 0x96, 0x51, 0x5a, 0x22, 0x60, 0x57, 0xb8, 0x4e, 0x01, 0x2c, 0x2a, 0x81, 0xc7, 0x41, 0x94, + 0x40, 0x1b, 0xfb, 0x81, 0x68, 0xca, 0x12, 0x52, 0x00, 0xab, 0xd5, 0x9f, 0x01, 0xec, 0xa0, 0x24, 0xdb, 0xce, 0xae, + 0x09, 0x91, 0xd9, 0x7a, 0xa4, 0xbd, 0xea, 0x30, 0x2d, 0xfa, 0xe7, 0xc4, 0x59, 0x9a, 0x18, 0xfb, 0x28, 0x89, 0x8f, + 0x1a, 0x37, 0x30, 0x25, 0x12, 0x74, 0x23, 0x0f, 0xc0, 0xcd, 0x2d, 0xf7, 0xe4, 0xbc, 0xd3, 0x9b, 0x03, 0x18, 0xe4, + 0xf4, 0x4c, 0x7f, 0xed, 0xc0, 0x82, 0xfb, 0xcf, 0x25, 0xea, 0xe8, 0x69, 0x09, 0xc9, 0x04, 0x2e, 0x7e, 0x34, 0xee, + 0x99, 0x06, 0x02, 0x62, 0xcf, 0x85, 0x51, 0x0b, 0x18, 0xfc, 0xd4, 0x11, 0xaf, 0x69, 0xcf, 0x92, 0x0e, 0xa3, 0xd2, + 0xf7, 0x44, 0x3a, 0xd5, 0xb6, 0x47, 0xa6, 0x7b, 0x9d, 0xc6, 0xd4, 0x87, 0xc8, 0x07, 0x53, 0xb8, 0x52, 0x04, 0xc0, + 0xf9, 0x1f, 0x0f, 0x58, 0xee, 0xdf, 0xc4, 0x8a, 0xae, 0x90, 0xe7, 0xda, 0x98, 0x72, 0x58, 0x25, 0x03, 0x5b, 0xc6, + 0x65, 0x47, 0x4c, 0x98, 0x8d, 0x99, 0x56, 0x46, 0x6f, 0xe6, 0xc8, 0x19, 0xa4, 0xb2, 0x31, 0x5c, 0x44, 0x39, 0xb5, + 0x25, 0x20, 0x21, 0xaa, 0x02, 0x18, 0x3c, 0xbd, 0x85, 0x8d, 0x95, 0xd4, 0xa6, 0x74, 0x26, 0x18, 0xaa, 0x21, 0xca, + 0x57, 0x39, 0xb1, 0x9d, 0xca, 0xd1, 0x7c, 0xa0, 0xc9, 0xea, 0x6f, 0x9f, 0x16, 0xee, 0x63, 0x87, 0xe7, 0xbd, 0x4e, + 0x9e, 0x99, 0x14, 0xe8, 0xd3, 0x96, 0xb9, 0x73, 0xe9, 0xc4, 0x65, 0xf1, 0xd2, 0x74, 0xb1, 0x5f, 0x9c, 0xf5, 0x4d, + 0x0a, 0xb2, 0x6c, 0xed, 0xd7, 0x83, 0xb9, 0xc3, 0xb6, 0x98, 0x3a, 0x8f, 0x45, 0x80, 0xcb, 0x12, 0x51, 0xba, 0x96, + 0x09, 0x81, 0x4d, 0xcb, 0xbc, 0x30, 0x9b, 0xd1, 0xe6, 0x0a, 0x2f, 0xcf, 0x47, 0x35, 0xad, 0xc9, 0x15, 0x7a, 0xdd, + 0xa7, 0xd3, 0x77, 0x42, 0xfe, 0x79, 0x39, 0xea, 0x9e, 0x59, 0xca, 0x40, 0x54, 0xed, 0x94, 0x0e, 0x3c, 0xe9, 0xc0, + 0xce, 0xb6, 0xa6, 0x6f, 0xdf, 0x2f, 0xfe, 0xd1, 0x3e, 0x99, 0x3a, 0xb7, 0xa1, 0xb5, 0xe8, 0xf8, 0xfd, 0x1e, 0x51, + 0xbb, 0x64, 0x85, 0x23, 0x04, 0x2a, 0xcf, 0x18, 0xd8, 0xa4, 0xde, 0xcc, 0x59, 0xdc, 0xf8, 0x48, 0xb5, 0xe8, 0x16, + 0x0e, 0xf0, 0x58, 0xdf, 0xfd, 0xea, 0x4f, 0xaa, 0x6e, 0xcf, 0xff, 0xda, 0x9e, 0x84, 0x76, 0x93, 0x7f, 0x6d, 0x6c, + 0xfd, 0xc7, 0xce, 0xc8, 0x72, 0x05, 0xd1, 0xf3, 0xda, 0x7a, 0xb5, 0x24, 0x1c, 0xbc, 0xc3, 0xdc, 0x3d, 0x01, 0xdf, + 0x8e, 0xbf, 0x35, 0xcd, 0x80, 0xa4, 0x65, 0x16, 0xad, 0x8d, 0x5c, 0xe3, 0x25, 0x0c, 0x28, 0x43, 0xd9, 0x15, 0xce, + 0x54, 0x1b, 0x43, 0xf3, 0xeb, 0x1d, 0xb7, 0x6f, 0x1b, 0x6c, 0xdc, 0xa2, 0x5b, 0x45, 0x37, 0x71, 0x61, 0x75, 0x78, + 0xa6, 0xb8, 0xca, 0xe6, 0x54, 0xb9, 0xca, 0xf8, 0xbb, 0x60, 0xa8, 0x0e, 0x03, 0x6e, 0x33, 0xec, 0xc2, 0x75, 0xe7, + 0xa1, 0x0b, 0x15, 0x45, 0x30, 0x2c, 0x48, 0xa5, 0xe5, 0x04, 0x8a, 0x32, 0xb6, 0xc5, 0x86, 0xa2, 0x04, 0xff, 0xfa, + 0xf7, 0x8c, 0x97, 0x51, 0xc0, 0x37, 0x36, 0xb4, 0xc8, 0x6c, 0x85, 0xa6, 0x29, 0xe1, 0x67, 0x98, 0x92, 0xe3, 0xca, + 0x27, 0x8d, 0xdb, 0xe1, 0x7f, 0x15, 0x4d, 0x04, 0x0a, 0xa8, 0x13, 0x0b, 0x09, 0x99, 0x69, 0x33, 0x45, 0xaf, 0x30, + 0x84, 0xae, 0x48, 0xca, 0x07, 0x97, 0x39, 0xf8, 0xae, 0xcd, 0x3d, 0x77, 0x2d, 0x6f, 0x1e, 0x04, 0x5b, 0x92, 0x4d, + 0x90, 0x96, 0x14, 0x32, 0xc9, 0xc2, 0xda, 0x91, 0x81, 0xeb, 0x6b, 0x7b, 0xa1, 0xa0, 0x24, 0x59, 0x10, 0x89, 0xe7, + 0x6b, 0x6d, 0x91, 0x3a, 0x16, 0x7f, 0x61, 0xba, 0x9f, 0xe2, 0xd3, 0x5e, 0xf8, 0x81, 0xfa, 0x3a, 0xdc, 0x75, 0x5f, + 0x45, 0xb3, 0x21, 0x6e, 0xd5, 0x8f, 0x19, 0x15, 0xd7, 0x23, 0xa5, 0xfd, 0x58, 0xfe, 0xf9, 0xfb, 0x0d, 0x8b, 0xc9, + 0x00, 0xc7, 0xc3, 0x9b, 0x9b, 0xa9, 0xc3, 0x8c, 0xbd, 0x86, 0x54, 0x6d, 0xac, 0xbe, 0x01, 0xb9, 0x45, 0x0e, 0xd5, + 0xae, 0x89, 0xa2, 0x0e, 0xb8, 0x99, 0x08, 0x0e, 0xc4, 0x7d, 0x64, 0xce, 0xa8, 0xd7, 0x9e, 0x54, 0x73, 0xba, 0x94, + 0x69, 0xd1, 0x52, 0xcd, 0x18, 0x66, 0xca, 0x74, 0x54, 0x31, 0xa0, 0xae, 0xdc, 0xd9, 0x95, 0xe7, 0x7b, 0x7e, 0x2a, + 0xcb, 0x8b, 0x0d, 0x9b, 0xa1, 0x4b, 0x2d, 0xcc, 0x51, 0xbe, 0xea, 0xf3, 0xb8, 0xbf, 0xf1, 0x8a, 0xf7, 0x7a, 0x87, + 0xa1, 0x43, 0x2d, 0x3d, 0x66, 0x6f, 0xd6, 0xfa, 0x7a, 0x3e, 0xe3, 0x20, 0x2d, 0x6a, 0xa7, 0x82, 0x71, 0xce, 0xa2, + 0x80, 0x05, 0x78, 0x85, 0xdd, 0x11, 0x8c, 0x8f, 0x67, 0xf1, 0x88, 0x7e, 0x64, 0xec, 0xe6, 0x6d, 0xf3, 0x1a, 0x10, + 0xa4, 0xca, 0x8e, 0x7b, 0x4b, 0x17, 0x2a, 0x16, 0xd1, 0xd2, 0x3b, 0xd3, 0x29, 0x94, 0x8f, 0x15, 0x00, 0xa4, 0xee, + 0xf4, 0x66, 0x30, 0x1e, 0xca, 0xcd, 0x01, 0xc2, 0x8d, 0x9c, 0x19, 0x37, 0x26, 0x0a, 0x47, 0x37, 0x06, 0x84, 0x08, + 0x71, 0x35, 0xf0, 0x95, 0x97, 0xb4, 0x4f, 0x54, 0x2c, 0x0d, 0xf1, 0xbd, 0xf2, 0xe0, 0x3e, 0xdc, 0xda, 0x6f, 0x2f, + 0x4e, 0x55, 0xc9, 0xc1, 0x22, 0x14, 0x1b, 0xc5, 0x7b, 0xe3, 0x77, 0x2f, 0xec, 0x7e, 0x62, 0x31, 0xd7, 0x12, 0x01, + 0xe5, 0x96, 0xef, 0x97, 0x56, 0x4d, 0x9c, 0x3f, 0xfd, 0x87, 0x0f, 0xe5, 0x12, 0x72, 0xe4, 0xab, 0x58, 0x76, 0x40, + 0x66, 0xbe, 0xa2, 0x9f, 0x45, 0x59, 0x4d, 0xe1, 0x53, 0xde, 0xc2, 0xdd, 0x75, 0xc7, 0xb5, 0x0e, 0x00, 0x59, 0x38, + 0x44, 0xb3, 0xd1, 0xd3, 0x2e, 0x89, 0xd0, 0x36, 0xda, 0xf8, 0x96, 0x47, 0x9a, 0x51, 0x51, 0x51, 0x34, 0x2e, 0xcd, + 0x46, 0x3d, 0xa4, 0x20, 0x9e, 0xa0, 0x17, 0xdf, 0x86, 0x80, 0x7c, 0x74, 0xed, 0x9d, 0x12, 0xb3, 0x74, 0x6b, 0xe1, + 0xfb, 0xfe, 0x46, 0xa2, 0x9e, 0x02, 0xfd, 0x28, 0xc2, 0x64, 0x41, 0x33, 0xf2, 0x95, 0xff, 0x2e, 0x00, 0x6f, 0x62, + 0xd1, 0x3f, 0xb1, 0xf0, 0x67, 0xb2, 0xee, 0xe1, 0xab, 0x9d, 0xb8, 0xde, 0x2e, 0x9a, 0xd3, 0x41, 0xfb, 0x10, 0x94, + 0xaa, 0xbe, 0xc7, 0xe1, 0x4d, 0xa1, 0xf5, 0xcb, 0x8e, 0x5d, 0xb0, 0x15, 0x05, 0x03, 0x9e, 0x75, 0x4a, 0x34, 0x31, + 0x5d, 0x97, 0x15, 0x01, 0xc6, 0x12, 0x27, 0x90, 0x5b, 0x9d, 0xb3, 0x74, 0x94, 0x9b, 0xb3, 0x9b, 0x3c, 0x6b, 0x27, + 0xd7, 0xd1, 0xbe, 0x9d, 0xcc, 0x4a, 0x59, 0xe5, 0xba, 0x21, 0x34, 0x7b, 0xf9, 0xe4, 0x2c, 0x94, 0x8b, 0x42, 0x1d, + 0x15, 0x37, 0xb4, 0x6a, 0x4d, 0x56, 0xc0, 0xca, 0xc9, 0x45, 0xab, 0xf2, 0xe6, 0xe9, 0xd0, 0xb8, 0xc9, 0x36, 0xfd, + 0x58, 0xd1, 0x76, 0x07, 0x0a, 0xaf, 0x14, 0xd6, 0xf6, 0x1c, 0x6c, 0xc3, 0x89, 0x06, 0xc7, 0x7d, 0xbb, 0x6d, 0x40, + 0x54, 0x20, 0xbb, 0x98, 0x50, 0x62, 0x6b, 0xf9, 0x2f, 0x0e, 0x28, 0xbe, 0xbd, 0x9a, 0x5e, 0x47, 0x31, 0x32, 0x7c, + 0x53, 0xff, 0x1e, 0x08, 0xf0, 0x2f, 0x7c, 0x60, 0x6a, 0x67, 0x54, 0x45, 0x28, 0x6b, 0x37, 0xab, 0xd4, 0x1f, 0x15, + 0xb9, 0xa5, 0x49, 0x6a, 0xb6, 0x79, 0x7a, 0x82, 0x29, 0x2b, 0xda, 0x9b, 0xc3, 0x06, 0x7b, 0x74, 0x6d, 0x44, 0xd8, + 0x60, 0x42, 0x1c, 0xff, 0x83, 0x9d, 0x00, 0x9d, 0x48, 0xf1, 0x82, 0xcb, 0x71, 0x65, 0x29, 0x9a, 0x12, 0xcd, 0x4b, + 0x51, 0xfb, 0x14, 0xe6, 0x3d, 0xaf, 0x02, 0xae, 0x9b, 0x93, 0xa3, 0x6e, 0x87, 0x3e, 0x71, 0x8a, 0x38, 0xcf, 0x81, + 0x08, 0x7b, 0x2a, 0xa7, 0x5d, 0xbd, 0x59, 0x5b, 0x86, 0xb8, 0x6e, 0x56, 0x88, 0x90, 0x7c, 0x18, 0xa7, 0x62, 0x88, + 0xb1, 0x7d, 0x23, 0x73, 0xde, 0xe3, 0xab, 0x85, 0x28, 0xac, 0x70, 0x11, 0x8f, 0x91, 0xa5, 0x3f, 0x79, 0x05, 0xd1, + 0x9d, 0x51, 0x93, 0x60, 0xd6, 0xea, 0x64, 0xa4, 0x38, 0x53, 0xff, 0x02, 0x02, 0x43, 0x6d, 0x90, 0xd1, 0x41, 0x4d, + 0x95, 0xf9, 0xd1, 0xd3, 0x88, 0x1b, 0x1f, 0x38, 0xaa, 0x46, 0x9b, 0x90, 0x71, 0xa6, 0xd8, 0x16, 0xbf, 0x35, 0x77, + 0x4b, 0x00, 0x66, 0x77, 0x1d, 0xa8, 0x15, 0x71, 0x24, 0xa1, 0x55, 0x7f, 0xae, 0xfe, 0x7a, 0x89, 0x44, 0x71, 0x4e, + 0xe8, 0x63, 0xa0, 0xc5, 0x47, 0x98, 0xae, 0xe6, 0x62, 0x1b, 0x87, 0x1d, 0x47, 0xa2, 0x8a, 0xd3, 0xbb, 0xe8, 0x72, + 0x3f, 0x93, 0x60, 0xf7, 0x13, 0x22, 0x9e, 0xef, 0xad, 0x0b, 0x35, 0x0b, 0x47, 0x1f, 0xb6, 0x3f, 0x09, 0x12, 0x32, + 0xd4, 0x5f, 0x0b, 0x37, 0x47, 0xed, 0xd5, 0x4b, 0x2d, 0xa3, 0x0d, 0x3f, 0x2d, 0xa2, 0xc5, 0xa9, 0x00, 0x94, 0x0c, + 0xa3, 0xf3, 0xcf, 0x5f, 0xec, 0xb0, 0x9f, 0x83, 0x73, 0x0c, 0x26, 0x0f, 0x78, 0xc0, 0xcd, 0xdd, 0x4f, 0xe8, 0xda, + 0x52, 0xce, 0x08, 0x67, 0xac, 0x0d, 0x09, 0x56, 0xc6, 0xb9, 0x66, 0x6b, 0xe3, 0x45, 0xc3, 0x09, 0xe1, 0x08, 0x3a, + 0x68, 0x8c, 0x7a, 0x9e, 0x33, 0x9a, 0xa7, 0x58, 0xfd, 0xca, 0x19, 0x6f, 0xf9, 0x81, 0x9d, 0xcb, 0x15, 0x04, 0x55, + 0x17, 0x55, 0x62, 0x4d, 0x27, 0xda, 0x81, 0xcb, 0xfd, 0x25, 0x7b, 0xca, 0x22, 0x7f, 0xbb, 0xc0, 0xa4, 0xa6, 0x42, + 0x21, 0x67, 0x85, 0x1b, 0xda, 0x15, 0x82, 0xd5, 0x6a, 0xdc, 0xf0, 0x3b, 0x6f, 0x59, 0x96, 0xaa, 0x4e, 0xed, 0x46, + 0x35, 0x34, 0xc3, 0x84, 0x29, 0x6e, 0x68, 0x19, 0xdf, 0x91, 0x94, 0xd8, 0x59, 0xd7, 0x06, 0x73, 0xfa, 0x1f, 0x52, + 0x9c, 0x0a, 0x2d, 0x44, 0xa9, 0xfa, 0x1c, 0x34, 0x3a, 0x31, 0x4d, 0xd5, 0x79, 0x23, 0x77, 0x26, 0x07, 0x83, 0x9a, + 0xb2, 0xb1, 0x53, 0xf3, 0x8e, 0xe9, 0xc8, 0x0c, 0xfe, 0x8e, 0xfc, 0xe4, 0x21, 0xab, 0x65, 0x72, 0x99, 0xe8, 0x67, + 0xbd, 0xf1, 0xaa, 0x00, 0x14, 0xd6, 0x31, 0xa8, 0xd0, 0x3c, 0x6b, 0x2a, 0x6b, 0x55, 0x65, 0xba, 0x09, 0x5f, 0x75, + 0x4b, 0x03, 0x05, 0xcf, 0x94, 0xa7, 0x10, 0x51, 0x53, 0xe2, 0xa4, 0xd5, 0x43, 0xa8, 0x01, 0xe5, 0xe8, 0xbc, 0x88, + 0xb9, 0x8e, 0x95, 0x2a, 0x1b, 0xff, 0x72, 0xef, 0xa3, 0x75, 0xeb, 0x20, 0xef, 0x67, 0x36, 0xea, 0xfd, 0x22, 0x55, + 0x4e, 0xa1, 0xcf, 0x8f, 0x34, 0x8d, 0x35, 0x09, 0xb6, 0x71, 0x32, 0x90, 0x0a, 0x3a, 0xa9, 0xc0, 0xff, 0xcb, 0x94, + 0x33, 0x56, 0x4c, 0x2a, 0x40, 0xc5, 0x62, 0xed, 0x9a, 0x7f, 0xdd, 0x87, 0x05, 0x93, 0xa0, 0xee, 0x1f, 0x80, 0x5e, + 0x0b, 0xb9, 0x90, 0x5f, 0xad, 0xb7, 0xa1, 0xec, 0x7c, 0xc3, 0x49, 0xeb, 0x73, 0xf5, 0x93, 0x23, 0x17, 0xb1, 0x9a, + 0xa2, 0xcd, 0xb4, 0x9e, 0x3a, 0x4b, 0x98, 0xf0, 0xd3, 0x72, 0x6e, 0xba, 0x41, 0xe6, 0x1a, 0x47, 0xca, 0xcb, 0xec, + 0xe3, 0xa8, 0x55, 0x66, 0xe9, 0xd8, 0x86, 0x2a, 0x6a, 0x73, 0x3c, 0x73, 0xc6, 0xf8, 0x62, 0x4f, 0x9a, 0x6a, 0x57, + 0x56, 0xf2, 0xcd, 0xb5, 0x98, 0x37, 0x87, 0x32, 0x45, 0x3d, 0x32, 0xad, 0x93, 0x8b, 0x13, 0x2a, 0xb3, 0x02, 0xc0, + 0xdb, 0x90, 0x6c, 0x84, 0xc0, 0x2e, 0xd8, 0x8f, 0xb5, 0x78, 0xe9, 0x2e, 0xa5, 0x51, 0x82, 0x97, 0x10, 0x2b, 0xfa, + 0x87, 0xd2, 0x42, 0x83, 0x54, 0x57, 0x94, 0x2c, 0x0d, 0xf5, 0xdf, 0x4a, 0x0f, 0x27, 0x39, 0x6a, 0x70, 0x0e, 0xb5, + 0xc7, 0xc2, 0xa8, 0xf7, 0x63, 0xd2, 0xa3, 0x3c, 0xd6, 0x4b, 0x81, 0x4d, 0x96, 0xc0, 0xca, 0x09, 0x76, 0x17, 0x20, + 0xe5, 0xb5, 0x87, 0xbe, 0x56, 0x64, 0xc2, 0xe3, 0xf3, 0xe4, 0xd6, 0xa5, 0x65, 0xe0, 0x15, 0xf4, 0xae, 0xbd, 0xa1, + 0xd2, 0x02, 0x77, 0xbf, 0xc8, 0x95, 0xff, 0xe8, 0x50, 0x24, 0x1d, 0xf1, 0x54, 0x12, 0x78, 0x2b, 0xa9, 0xc1, 0xc0, + 0xad, 0x65, 0xc3, 0xb5, 0x69, 0x1b, 0x7d, 0xa8, 0x8f, 0xe3, 0x3b, 0x46, 0xab, 0xe0, 0x3f, 0x9f, 0x7e, 0xc3, 0x38, + 0xb4, 0xe0, 0xd9, 0xaa, 0x54, 0x59, 0xd7, 0x53, 0x47, 0xb2, 0xfd, 0xd5, 0xce, 0x5b, 0x04, 0xb3, 0x70, 0x25, 0x0b, + 0x4d, 0x02, 0x3a, 0xb6, 0x49, 0x16, 0xb8, 0x4d, 0x81, 0x99, 0x47, 0x3f, 0x45, 0x6f, 0x23, 0xd5, 0x38, 0x52, 0xb5, + 0x68, 0x12, 0xe3, 0x70, 0x41, 0x34, 0x79, 0x73, 0xb7, 0x2a, 0x02, 0x19, 0x1c, 0xc0, 0x2d, 0xbf, 0x33, 0xce, 0x3d, + 0xf5, 0x91, 0xd6, 0x3a, 0xf0, 0xbb, 0x6e, 0xb2, 0x5d, 0xda, 0xa1, 0x51, 0x4b, 0xf4, 0xb6, 0x1d, 0x35, 0x1a, 0x64, + 0xd8, 0x23, 0xc5, 0xd8, 0xbd, 0x8f, 0xcf, 0xea, 0x31, 0x83, 0x2c, 0xd1, 0x01, 0x5f, 0x77, 0x0d, 0x54, 0x2c, 0x32, + 0x90, 0xbb, 0x0b, 0x21, 0x51, 0x87, 0x6d, 0xb4, 0x00, 0x50, 0xfa, 0x04, 0xab, 0xef, 0xc4, 0x2d, 0xf5, 0x06, 0x94, + 0xf9, 0x3e, 0xa4, 0x94, 0x42, 0x7d, 0x51, 0x91, 0x29, 0x67, 0x8b, 0xc5, 0x8c, 0x22, 0x8c, 0x3c, 0x11, 0x19, 0x6a, + 0x13, 0xc4, 0x08, 0x9c, 0xde, 0x32, 0xaa, 0x7e, 0x6c, 0x2f, 0x03, 0x2d, 0xed, 0xb5, 0x88, 0xa9, 0xca, 0x19, 0xcf, + 0x01, 0x94, 0x80, 0xc1, 0x55, 0x00, 0x67, 0xa6, 0x7a, 0x57, 0xc6, 0x9c, 0x58, 0x66, 0x05, 0x0f, 0x94, 0x4e, 0x2e, + 0xc6, 0xd7, 0xc0, 0xf9, 0x8f, 0xad, 0x89, 0xab, 0xf8, 0xeb, 0xd7, 0x2d, 0x9f, 0x67, 0xff, 0x97, 0x89, 0x76, 0x75, + 0x06, 0xac, 0x9c, 0xb0, 0xcf, 0x13, 0xc4, 0xeb, 0x06, 0xdb, 0xcb, 0xd6, 0x62, 0xc5, 0x93, 0x5e, 0x7f, 0x6c, 0xb5, + 0xa4, 0x2c, 0xab, 0xe4, 0x57, 0x1b, 0x08, 0xa4, 0xf1, 0x9d, 0x49, 0x64, 0x90, 0x0a, 0x92, 0x62, 0xba, 0x11, 0xfc, + 0xee, 0x5b, 0xef, 0x61, 0x47, 0x1a, 0x78, 0xd9, 0xea, 0xc2, 0xf0, 0x99, 0xba, 0x5d, 0xd3, 0x49, 0xce, 0xe0, 0xcc, + 0x9b, 0x09, 0x47, 0x5b, 0xef, 0xf2, 0xe5, 0x0a, 0x2d, 0xfa, 0x3c, 0xf4, 0x2b, 0xba, 0x4d, 0x5a, 0x96, 0xc7, 0x3d, + 0xcc, 0xa0, 0xfe, 0xaf, 0x62, 0xcd, 0x69, 0xf4, 0x55, 0x51, 0x5f, 0x7a, 0x41, 0x2b, 0xcd, 0x6d, 0xad, 0x2d, 0xe4, + 0x74, 0x6e, 0x91, 0x7b, 0x30, 0x34, 0xed, 0xfb, 0x8f, 0x2a, 0xc2, 0x92, 0x3d, 0xa5, 0xad, 0xf7, 0xc9, 0x45, 0x2f, + 0xd5, 0xb9, 0x11, 0xff, 0x96, 0x53, 0x79, 0xd3, 0x3a, 0x6a, 0x64, 0x27, 0xfe, 0x0f, 0xd6, 0x4d, 0x94, 0x71, 0xb9, + 0x4e, 0xee, 0xb4, 0x83, 0xe2, 0xa8, 0x4b, 0x8e, 0x87, 0x38, 0xd7, 0x8c, 0x46, 0x7a, 0x25, 0xcc, 0x33, 0xa7, 0x55, + 0x85, 0x1e, 0x8b, 0x06, 0xc9, 0x1a, 0x1a, 0x90, 0x04, 0xa8, 0xc9, 0x09, 0x71, 0xea, 0x4e, 0x70, 0x6b, 0x40, 0x72, + 0x72, 0x89, 0x90, 0x9c, 0x16, 0xde, 0xe5, 0xe7, 0x0d, 0x19, 0xa2, 0x9c, 0xd7, 0x37, 0xb1, 0x23, 0x2a, 0x3e, 0x8b, + 0x6e, 0xb9, 0x6f, 0x11, 0x1a, 0x6d, 0x1f, 0x34, 0x9a, 0x8e, 0x39, 0xb0, 0xcb, 0x9b, 0x35, 0x68, 0x39, 0x33, 0x08, + 0xf9, 0xe9, 0x19, 0x34, 0x61, 0xc0, 0x6c, 0x85, 0x10, 0x73, 0x94, 0x6c, 0x95, 0x9a, 0x34, 0x06, 0xf5, 0xc4, 0x4e, + 0x1c, 0xa8, 0xcf, 0xcf, 0xba, 0x59, 0x29, 0xd9, 0x9c, 0x9a, 0xda, 0xf4, 0x03, 0xd8, 0xe2, 0x89, 0x36, 0x1f, 0x28, + 0xc3, 0x20, 0x0d, 0x57, 0x25, 0xc2, 0xdf, 0xa8, 0xa8, 0xf3, 0x65, 0x3e, 0x6f, 0xd8, 0x46, 0xd0, 0x88, 0x21, 0x03, + 0xb3, 0x13, 0xac, 0x81, 0x20, 0x58, 0x16, 0x67, 0x72, 0x96, 0xd2, 0xd9, 0x38, 0x96, 0x58, 0x0b, 0x05, 0xb4, 0xbc, + 0x4d, 0xce, 0x1d, 0x04, 0x50, 0x46, 0xa2, 0xc4, 0xb2, 0x8d, 0x88, 0x3e, 0x30, 0x09, 0xde, 0x10, 0x2b, 0xf8, 0x05, + 0xdf, 0x50, 0x3a, 0xe9, 0x40, 0x6d, 0x92, 0x3b, 0x85, 0xaa, 0x0c, 0x0e, 0xc6, 0xe1, 0x15, 0xff, 0xed, 0xca, 0x0f, + 0x0e, 0x6d, 0x22, 0xee, 0x2a, 0xe0, 0x92, 0xd1, 0x73, 0x08, 0xea, 0xe4, 0xda, 0xb2, 0x89, 0xef, 0x34, 0xda, 0xde, + 0x55, 0xb5, 0x2b, 0x8e, 0xf8, 0xfc, 0x51, 0x60, 0x14, 0xa4, 0x5c, 0xce, 0xfe, 0x8d, 0x27, 0x69, 0xa2, 0x39, 0xb7, + 0xef, 0x1d, 0x2e, 0x16, 0x69, 0xa6, 0x3a, 0x35, 0xbd, 0xb9, 0x58, 0xb5, 0x0f, 0x46, 0xae, 0xb5, 0x67, 0x67, 0x1c, + 0x47, 0x20, 0x05, 0xe5, 0x03, 0xfe, 0x0b, 0xa9, 0x1a, 0xf2, 0xf9, 0xd0, 0xcf, 0x01, 0xd5, 0x4c, 0xf1, 0x69, 0xd5, + 0xd6, 0xbe, 0x49, 0xb5, 0xe0, 0x7f, 0x73, 0x58, 0xa4, 0x75, 0xfd, 0xfd, 0xf9, 0x8b, 0xde, 0x36, 0xd2, 0xf1, 0x23, + 0xb1, 0x92, 0xfa, 0x13, 0xa0, 0xac, 0xbd, 0x19, 0xb3, 0x76, 0x10, 0x4e, 0x63, 0x4a, 0x21, 0xfa, 0x4f, 0xe2, 0x53, + 0x4f, 0x65, 0xf0, 0x0d, 0xd4, 0x0f, 0xde, 0xc4, 0x68, 0xe9, 0x67, 0x6d, 0x6a, 0x21, 0xfc, 0x2d, 0xe6, 0x8b, 0x5a, + 0x3d, 0xe8, 0x38, 0x0f, 0xb9, 0x78, 0xc5, 0xea, 0x3f, 0x7d, 0xf1, 0xe5, 0x6c, 0x61, 0xb0, 0x96, 0xb5, 0x07, 0xff, + 0x8f, 0xf3, 0x00, 0x20, 0x5b, 0x61, 0x58, 0x4b, 0x34, 0xd3, 0x2f, 0xad, 0xa7, 0x00, 0xdf, 0x9d, 0xa7, 0x52, 0x6c, + 0x4d, 0x8b, 0x95, 0xa9, 0x67, 0x3a, 0xa8, 0x57, 0x6a, 0x6b, 0xdc, 0x37, 0xd6, 0x87, 0xd1, 0xd0, 0x77, 0x30, 0x57, + 0xbc, 0x7e, 0x8c, 0xe9, 0xee, 0x9f, 0x26, 0x26, 0x86, 0xfd, 0x4e, 0x75, 0x4a, 0x9a, 0xa5, 0xbe, 0x6d, 0xc8, 0x99, + 0xdd, 0x26, 0x70, 0xdf, 0x64, 0x8a, 0x90, 0xe3, 0x7d, 0x72, 0x94, 0xa2, 0xa6, 0x7d, 0x4b, 0x25, 0xab, 0x5b, 0x0f, + 0x69, 0xc4, 0x2c, 0x35, 0xd0, 0xfb, 0xe2, 0x55, 0x81, 0x81, 0x87, 0xea, 0xbc, 0x7e, 0x8b, 0x02, 0x9e, 0xc1, 0x47, + 0xcb, 0xac, 0xda, 0xba, 0x04, 0x8e, 0x51, 0xeb, 0xc0, 0xfd, 0xf2, 0xc0, 0x1f, 0x29, 0xfa, 0xe2, 0x6d, 0xe4, 0x60, + 0x83, 0xd7, 0x53, 0x83, 0x53, 0x1e, 0x9e, 0x8d, 0xf5, 0x31, 0xe3, 0xa7, 0x95, 0xa3, 0xb0, 0x67, 0x5c, 0x3c, 0x99, + 0x5d, 0x8c, 0xc3, 0xa6, 0xdb, 0x2a, 0x27, 0x4a, 0xa6, 0x4c, 0xd7, 0x64, 0x7e, 0xc6, 0x85, 0x9e, 0x37, 0x6b, 0xb5, + 0x84, 0xcd, 0x0f, 0xfe, 0x70, 0x53, 0x5c, 0x19, 0x27, 0xa3, 0xd0, 0xfe, 0x1f, 0xd9, 0x99, 0xa6, 0x77, 0xa1, 0x46, + 0xe0, 0x52, 0x70, 0xb5, 0x54, 0x96, 0x46, 0xda, 0xcf, 0xf6, 0xe9, 0xfb, 0x24, 0x5f, 0x41, 0x9e, 0xfe, 0x92, 0x15, + 0x1b, 0x73, 0x92, 0x64, 0xff, 0xa8, 0x14, 0x32, 0x87, 0xaa, 0x45, 0x3b, 0x46, 0x5b, 0xf9, 0x09, 0x41, 0x7d, 0xd9, + 0x21, 0xea, 0x00, 0xcc, 0xb6, 0x4a, 0x79, 0xbf, 0x18, 0x68, 0x46, 0x51, 0xb6, 0x1c, 0xf4, 0xb5, 0x61, 0x06, 0x07, + 0xaf, 0x1a, 0xd6, 0xef, 0xbd, 0xac, 0x55, 0x32, 0x52, 0x69, 0xb3, 0xcc, 0x51, 0x6a, 0xf2, 0x74, 0xbf, 0xd4, 0xb9, + 0xe8, 0x9a, 0x38, 0xf8, 0xd9, 0xda, 0xf7, 0x60, 0xd7, 0x4e, 0xcb, 0xae, 0x14, 0xe6, 0x06, 0xc7, 0x79, 0xcc, 0x71, + 0x65, 0x03, 0x11, 0x6b, 0x16, 0x5a, 0xde, 0x14, 0x2d, 0x52, 0x77, 0xea, 0xbb, 0xb3, 0xec, 0x26, 0x80, 0xad, 0x62, + 0xef, 0xa1, 0xe5, 0xdb, 0x67, 0xe9, 0x8d, 0x0e, 0x6c, 0x6b, 0xe3, 0x5e, 0xc7, 0x37, 0x16, 0x84, 0x9e, 0x2c, 0xaf, + 0xce, 0xa8, 0x8e, 0x3b, 0xa7, 0xf9, 0xfc, 0x50, 0x31, 0x96, 0x6e, 0x93, 0xe8, 0x9c, 0x8f, 0xe4, 0x09, 0xb2, 0x0c, + 0x15, 0xcb, 0x69, 0x60, 0x2d, 0x23, 0x68, 0xec, 0x24, 0x7d, 0xe5, 0x91, 0xac, 0xc6, 0x8a, 0xf9, 0x47, 0xa0, 0x76, + 0xae, 0xec, 0xb8, 0x6d, 0x86, 0xa4, 0x5a, 0xae, 0xb4, 0x46, 0x30, 0x0c, 0x8d, 0x7f, 0x2d, 0x44, 0xa2, 0xda, 0x4a, + 0x40, 0x02, 0x0e, 0x67, 0x29, 0xa8, 0xdd, 0x6d, 0x79, 0xf3, 0x6e, 0x94, 0x1e, 0x51, 0xa4, 0xa2, 0x56, 0x54, 0x4e, + 0xf1, 0x86, 0xb2, 0xf5, 0x4c, 0x34, 0x01, 0x13, 0x8d, 0x62, 0x23, 0x33, 0x28, 0x6f, 0xb7, 0x2a, 0xe4, 0x5e, 0xae, + 0xfb, 0xb7, 0x57, 0xef, 0x28, 0x0d, 0x9b, 0xbe, 0x12, 0x92, 0x06, 0xad, 0x50, 0x44, 0x7c, 0xc0, 0x8e, 0x31, 0x8e, + 0xae, 0xc9, 0xf4, 0x99, 0x3a, 0x30, 0x46, 0x75, 0x89, 0x94, 0x2f, 0xcd, 0x9f, 0xbd, 0xf1, 0xea, 0x25, 0xb0, 0xf5, + 0x3b, 0x5d, 0x6b, 0x4d, 0x66, 0xde, 0x96, 0x52, 0x2b, 0x91, 0x6e, 0x32, 0x22, 0x8d, 0xff, 0x4c, 0xb3, 0x6f, 0x26, + 0xf2, 0x87, 0x1d, 0xed, 0xc0, 0x40, 0x86, 0xf4, 0x66, 0xb3, 0x39, 0xa7, 0x6a, 0x16, 0x00, 0x0a, 0xff, 0xd5, 0xba, + 0x0f, 0x66, 0x6b, 0xa6, 0xa9, 0x88, 0xe0, 0xb3, 0x30, 0x34, 0x6f, 0xe1, 0x90, 0xd5, 0x69, 0x04, 0xe0, 0x20, 0x09, + 0x81, 0xcc, 0xd9, 0x5c, 0x6f, 0x08, 0xaa, 0xd8, 0xdb, 0xb0, 0x46, 0x9f, 0x42, 0xe8, 0x7f, 0xe4, 0xd3, 0xcf, 0xf9, + 0x5e, 0x45, 0x51, 0x0c, 0x5d, 0x1d, 0x0a, 0x87, 0xd6, 0xdf, 0x64, 0xd2, 0x78, 0x97, 0x2c, 0x14, 0x83, 0xfa, 0x8b, + 0xbd, 0x43, 0xcb, 0xdc, 0x74, 0x67, 0x03, 0x0b, 0x97, 0x0a, 0x06, 0x52, 0x2c, 0x42, 0x48, 0x73, 0x83, 0xb3, 0x7e, + 0xeb, 0xb1, 0x7c, 0xe9, 0x02, 0x4d, 0xdf, 0xca, 0xe3, 0x31, 0x3e, 0xfb, 0x76, 0xbc, 0xe3, 0x13, 0x66, 0x5a, 0x66, + 0x89, 0x4a, 0x0a, 0xe9, 0x93, 0xff, 0x0e, 0xa3, 0x96, 0xc7, 0x84, 0x05, 0xd3, 0xea, 0xee, 0xa9, 0x14, 0xc5, 0xce, + 0x73, 0x58, 0x53, 0x2f, 0xa0, 0x0e, 0x85, 0x9b, 0xea, 0x03, 0xbb, 0x12, 0x41, 0x6a, 0x53, 0x00, 0x30, 0xfe, 0x08, + 0x80, 0x88, 0x07, 0x99, 0x57, 0xaa, 0x25, 0x64, 0xb8, 0x59, 0x4e, 0xa4, 0xbb, 0x8b, 0x51, 0xe2, 0x9b, 0x23, 0x02, + 0xb4, 0xa5, 0x66, 0x18, 0x9e, 0xc9, 0x6f, 0x73, 0x79, 0x13, 0x2e, 0x81, 0xed, 0x1a, 0xc1, 0x1b, 0x21, 0x6d, 0xd6, + 0x7e, 0x38, 0x02, 0xaa, 0xb6, 0x01, 0x51, 0xfa, 0x4d, 0x79, 0x63, 0xde, 0x88, 0x14, 0xaa, 0xd5, 0xce, 0xee, 0x4d, + 0x5a, 0xa7, 0x0d, 0xab, 0xe1, 0x29, 0xdc, 0x54, 0xa9, 0x6d, 0x23, 0xd7, 0xf6, 0x7f, 0x92, 0x82, 0x9c, 0x4d, 0xdd, + 0xd5, 0x6d, 0xf7, 0xfb, 0xa7, 0x09, 0x38, 0xfc, 0x24, 0x31, 0xbe, 0xfb, 0xd5, 0x32, 0xfb, 0x3f, 0xb6, 0xf2, 0xa0, + 0x04, 0x0f, 0xa7, 0x20, 0x9f, 0x62, 0x0d, 0xd7, 0x90, 0x7a, 0xf2, 0xae, 0xaf, 0xbb, 0x80, 0xc0, 0xfa, 0x2d, 0xb9, + 0x13, 0xef, 0x32, 0x82, 0x53, 0x00, 0xdb, 0xd6, 0x11, 0x58, 0xeb, 0xe6, 0x3b, 0x90, 0x82, 0x18, 0xf9, 0x2d, 0x92, + 0xff, 0xb3, 0x32, 0x37, 0xfc, 0x48, 0x51, 0xdc, 0x9c, 0x4b, 0x17, 0xd1, 0x93, 0x55, 0xd8, 0x0e, 0x1b, 0x55, 0x80, + 0x23, 0xb0, 0xf0, 0x7e, 0x6e, 0x26, 0xff, 0x0c, 0xa1, 0x9d, 0xab, 0x33, 0xc5, 0xa1, 0x18, 0xd5, 0x4f, 0x75, 0x01, + 0xca, 0xc3, 0x64, 0xc4, 0xa6, 0x26, 0xb4, 0x18, 0x0b, 0x4b, 0x97, 0x24, 0x80, 0x40, 0x7b, 0xa8, 0x25, 0x32, 0x97, + 0x6b, 0x91, 0x5d, 0x32, 0xee, 0xd9, 0x56, 0x2c, 0x5d, 0xfb, 0x98, 0xd7, 0xd9, 0x33, 0x70, 0xe3, 0x3c, 0x06, 0x5f, + 0xdc, 0xd9, 0x52, 0x58, 0xe9, 0x19, 0xb2, 0x3a, 0x3b, 0x57, 0xe2, 0xb0, 0x4d, 0xb6, 0x1f, 0x15, 0xec, 0xee, 0xdb, + 0x5b, 0x22, 0x0b, 0xc4, 0xe0, 0x3f, 0xad, 0x35, 0x59, 0xeb, 0x6f, 0xe4, 0x00, 0xbe, 0x85, 0x95, 0x7c, 0x41, 0x33, + 0xe0, 0x72, 0x77, 0x73, 0x40, 0xea, 0x81, 0x4f, 0x26, 0xac, 0xaa, 0x72, 0xcd, 0xcd, 0x46, 0xa6, 0x09, 0x9a, 0x10, + 0xff, 0xbf, 0xb2, 0xd5, 0x10, 0x1b, 0x80, 0x27, 0x63, 0xdf, 0x7c, 0xd9, 0x85, 0xc1, 0x66, 0xa1, 0xc5, 0x16, 0xf6, + 0xe1, 0x2d, 0xa7, 0xe2, 0x75, 0x73, 0x03, 0x35, 0xfc, 0x20, 0x81, 0x95, 0xef, 0x12, 0xaa, 0xf9, 0x9e, 0x38, 0xf6, + 0xbd, 0x57, 0xbe, 0x7a, 0x4e, 0x8f, 0x40, 0xd3, 0xe8, 0xac, 0x99, 0xf4, 0xe4, 0x70, 0x6e, 0x0c, 0x55, 0x23, 0xaf, + 0x95, 0xb7, 0x07, 0x57, 0xab, 0xbf, 0x3e, 0x9b, 0xf3, 0x36, 0x3f, 0xa2, 0x1f, 0x5d, 0x63, 0x23, 0x66, 0x71, 0xc2, + 0x57, 0xd7, 0x47, 0x91, 0x50, 0x51, 0xc4, 0xc5, 0x87, 0x75, 0x9f, 0x36, 0xae, 0xb7, 0x8e, 0x6e, 0xf1, 0x2e, 0xc0, + 0x9c, 0x92, 0x54, 0x9d, 0x6d, 0x67, 0xe8, 0x0a, 0xbe, 0x97, 0xb5, 0xc5, 0xf1, 0xa5, 0xb5, 0x6e, 0xcb, 0xcb, 0xae, + 0xbc, 0x37, 0x46, 0x5d, 0xb4, 0x60, 0xd7, 0x77, 0x9c, 0xbc, 0xd5, 0xc8, 0xfd, 0xea, 0xa9, 0x2d, 0x96, 0x50, 0x40, + 0x1b, 0x5a, 0xbe, 0x20, 0x3b, 0xc6, 0x9e, 0x8d, 0x4e, 0xa5, 0xc9, 0x53, 0xf4, 0xba, 0xfb, 0xcc, 0x23, 0x1e, 0xd6, + 0x81, 0xae, 0x9c, 0x06, 0x1d, 0xff, 0xc2, 0x7f, 0x79, 0x59, 0xaa, 0xb7, 0x2a, 0xae, 0xbd, 0x12, 0x00, 0x93, 0x2a, + 0x9f, 0xf4, 0xf2, 0xf7, 0x41, 0x10, 0x19, 0xd9, 0x08, 0xf1, 0x4c, 0x54, 0x96, 0x00, 0x3a, 0xae, 0x72, 0xf1, 0xce, + 0x74, 0xd0, 0x2f, 0x67, 0x22, 0x11, 0x39, 0x03, 0x6d, 0x1b, 0x14, 0x0a, 0x91, 0x7a, 0xbb, 0x08, 0xe2, 0x1e, 0x45, + 0x4c, 0x34, 0xd7, 0x5d, 0xdf, 0xaf, 0xd1, 0x71, 0x34, 0x36, 0xa3, 0x76, 0xfb, 0x5b, 0xc1, 0x14, 0x48, 0x89, 0x83, + 0x81, 0xba, 0xa2, 0x22, 0x1e, 0xff, 0xf1, 0x40, 0xfb, 0x25, 0x35, 0x9c, 0xb2, 0xc3, 0x78, 0x15, 0x5f, 0x59, 0x55, + 0xb5, 0xe2, 0x97, 0x88, 0x99, 0x21, 0x88, 0x37, 0x1a, 0xe9, 0x95, 0xcd, 0x5e, 0xcd, 0x64, 0xa2, 0x38, 0x29, 0x2c, + 0x8f, 0x6b, 0xd7, 0x84, 0x75, 0x00, 0x6b, 0xf5, 0xd1, 0xa1, 0xa5, 0xf8, 0xfb, 0xec, 0x8f, 0x4b, 0x8e, 0x99, 0xe7, + 0xcf, 0xf0, 0xbf, 0xcd, 0x2e, 0x97, 0xfc, 0xd1, 0x3d, 0xc9, 0xf6, 0x3d, 0x76, 0x00, 0xcd, 0x32, 0xa5, 0x8e, 0x32, + 0x86, 0x00, 0xc0, 0x41, 0xe2, 0x7b, 0x8b, 0xdb, 0xff, 0xee, 0x18, 0x44, 0xce, 0xf2, 0xa6, 0xc5, 0x83, 0xff, 0x18, + 0x51, 0x5a, 0x1a, 0x6b, 0xe1, 0x08, 0x82, 0x71, 0x6d, 0xac, 0x1b, 0xc9, 0x3c, 0xd0, 0x75, 0x04, 0xb2, 0x96, 0x9c, + 0x60, 0xa2, 0x44, 0xee, 0x55, 0xcd, 0xeb, 0x10, 0x6a, 0x25, 0x96, 0xa9, 0xcd, 0x23, 0xea, 0xa8, 0xb1, 0xef, 0x40, + 0xf0, 0x32, 0x3b, 0x44, 0x6d, 0xfe, 0x63, 0x4b, 0x81, 0x5f, 0x4a, 0x79, 0x32, 0x70, 0x78, 0x23, 0x14, 0x15, 0x1f, + 0x05, 0x30, 0x9c, 0x11, 0xbc, 0xa8, 0xd5, 0x57, 0x8e, 0x63, 0xa0, 0x1f, 0x4a, 0x2a, 0x5e, 0xec, 0x3e, 0x6f, 0xbc, + 0x01, 0x77, 0xa1, 0xfc, 0x03, 0xe5, 0x3a, 0x52, 0x2d, 0x7b, 0xf9, 0xc8, 0x4e, 0x6d, 0xc7, 0xd9, 0x50, 0x15, 0x54, + 0x45, 0xef, 0xd0, 0x2f, 0x85, 0x70, 0x60, 0x79, 0xb2, 0xda, 0x1b, 0xee, 0x0c, 0x7c, 0x6c, 0xc4, 0x47, 0x7d, 0x25, + 0x7b, 0x43, 0xa2, 0x8c, 0x85, 0xe4, 0x38, 0x2a, 0x40, 0xf4, 0xe4, 0xd3, 0x75, 0x36, 0x0d, 0x7b, 0x75, 0xb6, 0x14, + 0x48, 0x23, 0x46, 0x3a, 0x97, 0x4a, 0x67, 0xf6, 0xf4, 0x48, 0x19, 0x3f, 0xef, 0xfc, 0x6a, 0xd9, 0xa0, 0xcc, 0x36, + 0xa4, 0xf2, 0xa7, 0xbc, 0x2f, 0x25, 0x65, 0xb2, 0xad, 0xd8, 0xf4, 0xc6, 0xe6, 0x14, 0xc0, 0x64, 0x05, 0x61, 0xee, + 0xbe, 0x41, 0x39, 0x18, 0x63, 0x5d, 0xa9, 0x22, 0xdf, 0xf8, 0x3c, 0x76, 0x7a, 0x7a, 0xc1, 0x33, 0x8a, 0x2c, 0xfa, + 0x53, 0x04, 0x36, 0xcb, 0x6b, 0x85, 0x09, 0xdf, 0xe7, 0xb8, 0x46, 0xbf, 0xd0, 0x14, 0x4d, 0x42, 0xf4, 0xe3, 0x8d, + 0x48, 0x35, 0x2b, 0xe0, 0xcd, 0xfb, 0xa6, 0x1b, 0xc1, 0xb3, 0x32, 0xda, 0x48, 0x24, 0xda, 0xba, 0x29, 0xf0, 0xef, + 0x11, 0x7d, 0x23, 0x66, 0xfa, 0x83, 0x34, 0x5f, 0xfd, 0x20, 0xcc, 0x37, 0xdb, 0x03, 0xaa, 0xda, 0x87, 0xdc, 0xf8, + 0xe4, 0x42, 0x01, 0x16, 0x10, 0x46, 0x2f, 0x95, 0x36, 0xd6, 0x04, 0xa5, 0x84, 0x4b, 0x51, 0x93, 0x51, 0x5e, 0x4f, + 0xf5, 0x09, 0xad, 0xeb, 0x25, 0x19, 0x60, 0x12, 0xba, 0xb1, 0x8d, 0xbe, 0x8d, 0xb9, 0x4d, 0x97, 0xfd, 0x87, 0x0a, + 0xed, 0x81, 0x2b, 0x1b, 0x2c, 0xe0, 0x73, 0xb5, 0xe7, 0xce, 0x45, 0x04, 0x5a, 0x83, 0xf8, 0x8f, 0xe3, 0x7a, 0xb1, + 0x77, 0x4b, 0x25, 0x25, 0x56, 0x59, 0x08, 0x19, 0x2a, 0x17, 0x76, 0x73, 0xc3, 0x3c, 0xeb, 0x71, 0xf0, 0x8c, 0x04, + 0x01, 0xc1, 0xa9, 0x82, 0x49, 0x5c, 0x4d, 0x69, 0x58, 0xd9, 0x73, 0x74, 0xc3, 0x69, 0xf9, 0x35, 0x53, 0x65, 0xbb, + 0x40, 0xa7, 0x6f, 0x5c, 0x31, 0x98, 0x9f, 0xd8, 0x17, 0x8e, 0x1e, 0x5a, 0x46, 0xd7, 0x67, 0x07, 0x46, 0x80, 0x1c, + 0x56, 0x96, 0x81, 0x84, 0x2d, 0x49, 0xab, 0x37, 0x79, 0x78, 0xcf, 0x14, 0x22, 0xc9, 0x02, 0x55, 0x8e, 0x5f, 0x60, + 0x6b, 0x69, 0x49, 0x39, 0x2b, 0xd1, 0x5a, 0x85, 0x32, 0x44, 0x6b, 0xbd, 0x6f, 0x57, 0x9d, 0xde, 0x7b, 0x5f, 0xd0, + 0x79, 0x69, 0x24, 0x87, 0x18, 0x02, 0x43, 0x2c, 0x8d, 0xef, 0x14, 0x36, 0x5a, 0x6f, 0x96, 0xd9, 0x7d, 0x35, 0xb6, + 0x5f, 0xc3, 0x75, 0x3d, 0xf1, 0xa6, 0xfc, 0xb6, 0xce, 0x1e, 0xe6, 0xbc, 0x72, 0xa2, 0x1b, 0xba, 0x86, 0xcd, 0xda, + 0x4e, 0x7f, 0x55, 0xdf, 0x32, 0x19, 0x16, 0x1f, 0x7b, 0x08, 0x21, 0x17, 0xaa, 0x54, 0x88, 0xf4, 0x76, 0x27, 0x90, + 0x2a, 0xf7, 0x94, 0x2b, 0x9d, 0xe3, 0x44, 0xd6, 0xb1, 0x9d, 0x1c, 0x2e, 0x4d, 0x2a, 0x88, 0x63, 0x7b, 0xf7, 0x9d, + 0x58, 0xf0, 0xc9, 0x17, 0xd2, 0x9c, 0xa7, 0xeb, 0x97, 0x7e, 0x78, 0x65, 0xac, 0x94, 0x9c, 0x6e, 0x66, 0x51, 0xd3, + 0xdd, 0x2c, 0xb2, 0xf3, 0xaf, 0x71, 0xeb, 0x92, 0xf0, 0x3a, 0x69, 0xff, 0x6a, 0x84, 0x97, 0x5c, 0xeb, 0x52, 0x44, + 0x53, 0x94, 0xba, 0x7f, 0x9d, 0xa0, 0x20, 0x12, 0xfc, 0xb9, 0x68, 0x18, 0x6b, 0x9f, 0x56, 0xcd, 0x47, 0x63, 0xc5, + 0xd6, 0xde, 0xb7, 0x92, 0x1a, 0x17, 0x05, 0xd7, 0x8c, 0x5c, 0x69, 0xa5, 0xc4, 0xe0, 0x38, 0xd0, 0x94, 0x3f, 0x50, + 0xe5, 0x0f, 0x53, 0xd2, 0x79, 0x8b, 0xd9, 0xea, 0xfb, 0xd4, 0x6e, 0x1d, 0x53, 0x45, 0x23, 0x9d, 0x19, 0xb3, 0x51, + 0x2b, 0x38, 0xda, 0xe3, 0x7a, 0x59, 0x48, 0xe7, 0xb4, 0xcd, 0xe0, 0x93, 0xf4, 0xf1, 0xad, 0x7c, 0xb6, 0xca, 0x5f, + 0xea, 0xfd, 0x5e, 0xda, 0xdb, 0xe4, 0xc5, 0x06, 0xde, 0x0a, 0x13, 0x60, 0x20, 0xa2, 0x52, 0x05, 0xb5, 0x84, 0x24, + 0xec, 0xb4, 0xd3, 0x39, 0x43, 0x55, 0x5a, 0x4c, 0x81, 0x1f, 0x97, 0xf5, 0xf1, 0xf8, 0x5a, 0x34, 0xa6, 0xd6, 0x51, + 0x23, 0x3e, 0x2e, 0xe7, 0x19, 0x20, 0x2f, 0x54, 0x3c, 0x53, 0x11, 0x7d, 0x46, 0xce, 0xf0, 0xa0, 0xcc, 0x82, 0x91, + 0x76, 0x18, 0x8a, 0x2d, 0x37, 0xa6, 0x3a, 0x03, 0xba, 0xf0, 0x67, 0x8d, 0x94, 0x69, 0x84, 0x52, 0xc8, 0xb5, 0x49, + 0xbb, 0xcc, 0x37, 0x08, 0xd3, 0x0b, 0x1a, 0x7f, 0x3d, 0xf9, 0x5e, 0x0a, 0x99, 0x02, 0xee, 0x23, 0x85, 0xd7, 0xf4, + 0x42, 0x66, 0xc0, 0x9b, 0x1a, 0x20, 0x09, 0x40, 0x9a, 0x55, 0x27, 0xbc, 0x0d, 0x0f, 0x49, 0xb3, 0xdf, 0xca, 0x52, + 0xb9, 0x27, 0x57, 0x5a, 0xf2, 0xad, 0x6e, 0x2b, 0xe6, 0x4b, 0xd6, 0x36, 0xad, 0x9d, 0x9d, 0xd0, 0xeb, 0x34, 0x4d, + 0xba, 0x44, 0x38, 0xa8, 0x24, 0xbd, 0xdf, 0x01, 0x06, 0x53, 0x5f, 0xbe, 0x45, 0xcd, 0xfc, 0x5e, 0x82, 0x9d, 0x0c, + 0xd8, 0x50, 0x65, 0xe5, 0x32, 0x0b, 0x00, 0x01, 0xba, 0x6d, 0xa3, 0x9b, 0x26, 0x8b, 0x37, 0x22, 0xf7, 0x80, 0xce, + 0x05, 0x77, 0x64, 0x6f, 0x29, 0xdd, 0x99, 0x8e, 0x95, 0x6c, 0xbc, 0x2b, 0x6b, 0xb2, 0x0b, 0x95, 0xf8, 0x26, 0x06, + 0x66, 0x3b, 0x2b, 0x09, 0x80, 0xeb, 0xc6, 0x2e, 0xf3, 0x42, 0x9d, 0xc9, 0x6c, 0xcd, 0xaa, 0x3c, 0x55, 0xc3, 0x54, + 0x3a, 0x74, 0xd5, 0x44, 0x0d, 0x41, 0x36, 0x20, 0x6c, 0x5e, 0xdb, 0x5c, 0xc7, 0x67, 0x01, 0x20, 0xe8, 0x41, 0x29, + 0x4b, 0xc6, 0x8e, 0x1b, 0x69, 0x77, 0xbd, 0xac, 0x00, 0x61, 0xbc, 0xb3, 0x26, 0x39, 0x39, 0x2d, 0xfd, 0xc9, 0x78, + 0xdb, 0x6a, 0xa6, 0xdd, 0xf1, 0x43, 0x42, 0xdb, 0xe2, 0xd0, 0x82, 0x1f, 0xa9, 0xdd, 0xb9, 0x5a, 0xc4, 0xaa, 0xbd, + 0x2c, 0x60, 0xb0, 0x8d, 0xd6, 0xba, 0x6d, 0xee, 0xe6, 0x98, 0x08, 0x27, 0xcb, 0xc6, 0x74, 0x27, 0x96, 0x17, 0x89, + 0x35, 0x06, 0x6a, 0x6b, 0xde, 0xf8, 0xa5, 0xc0, 0xd4, 0x04, 0xdf, 0xa8, 0x5c, 0x2c, 0x8d, 0xfe, 0xf4, 0x03, 0x11, + 0xa1, 0x59, 0x6c, 0xae, 0xd6, 0x4d, 0x68, 0xbc, 0xc6, 0xf5, 0x06, 0xdc, 0x0d, 0x2c, 0x1a, 0x4e, 0xf4, 0x60, 0xce, + 0xee, 0x48, 0xcd, 0x8a, 0x65, 0xf8, 0xd1, 0xa3, 0xa3, 0x02, 0xbb, 0xb3, 0x39, 0x96, 0x14, 0x48, 0x30, 0xe2, 0xd7, + 0xd7, 0x58, 0x2c, 0x6a, 0x97, 0x46, 0x07, 0x63, 0x2e, 0xf9, 0x0f, 0xaa, 0x9b, 0x69, 0x5f, 0x01, 0x9b, 0x7b, 0x86, + 0x23, 0x49, 0x99, 0x19, 0xbd, 0xbc, 0x36, 0x0d, 0xec, 0x55, 0x1e, 0x75, 0x1c, 0x46, 0x4f, 0x4a, 0xc2, 0xde, 0x6c, + 0x4d, 0xa9, 0x5c, 0x8a, 0x51, 0xe8, 0x79, 0x83, 0x58, 0xf4, 0xb8, 0x07, 0x38, 0xc9, 0x98, 0x22, 0x7d, 0xb5, 0x51, + 0x90, 0xb7, 0xda, 0x5f, 0xba, 0xec, 0xa0, 0x49, 0x87, 0xce, 0x02, 0x9c, 0x8c, 0x92, 0x82, 0x10, 0xa0, 0x0d, 0xa1, + 0xd7, 0x06, 0x2f, 0xa5, 0x08, 0x4d, 0x4d, 0x66, 0xd4, 0x85, 0xf9, 0x9c, 0x71, 0x46, 0xa1, 0xa0, 0xa7, 0x5d, 0x9a, + 0x77, 0xab, 0xdb, 0x5c, 0x38, 0xde, 0x5d, 0x54, 0x2b, 0x02, 0x29, 0x5a, 0xf1, 0xd3, 0x43, 0xe1, 0x22, 0xb7, 0x20, + 0xa2, 0xd6, 0x1c, 0xde, 0x1a, 0x9c, 0x5c, 0x4c, 0x68, 0x95, 0xea, 0xae, 0xf7, 0xf0, 0x85, 0x88, 0xef, 0xda, 0x3c, + 0x21, 0x8e, 0x5a, 0x6f, 0xe8, 0xe6, 0x2c, 0xcd, 0x53, 0x09, 0xf5, 0xcc, 0x16, 0x02, 0x97, 0x8d, 0x8c, 0x2a, 0x7c, + 0x33, 0x3e, 0xc7, 0xc8, 0x92, 0x80, 0x32, 0x38, 0x9d, 0xc5, 0x08, 0x2c, 0x32, 0xe6, 0xe3, 0xd8, 0x1f, 0xcf, 0x6c, + 0x82, 0x7c, 0xd7, 0x98, 0x11, 0x89, 0xb7, 0xbd, 0x37, 0xd4, 0x28, 0x94, 0x8c, 0x44, 0x5c, 0x1e, 0x39, 0xb4, 0x7b, + 0x50, 0x7d, 0x37, 0x20, 0x36, 0x8c, 0x29, 0xd3, 0x09, 0xa1, 0x4f, 0x1e, 0xc4, 0x9a, 0x5c, 0x98, 0xb0, 0xd2, 0x49, + 0x0c, 0xc4, 0xe8, 0x6c, 0x40, 0xf5, 0x8d, 0xd0, 0x22, 0x91, 0x05, 0x25, 0x12, 0xf9, 0x6c, 0x4e, 0x88, 0xc3, 0x56, + 0x64, 0xfc, 0x60, 0xb5, 0x77, 0x11, 0x95, 0x3e, 0xe3, 0xa4, 0xb0, 0x2e, 0x0b, 0xa3, 0x3f, 0x46, 0x09, 0x61, 0xc0, + 0xd9, 0xed, 0x49, 0x51, 0xde, 0x0d, 0x8b, 0x47, 0x17, 0xa8, 0xca, 0xb7, 0x5c, 0x01, 0xec, 0xd1, 0x42, 0x0d, 0x54, + 0x59, 0xb2, 0x9c, 0xeb, 0x47, 0x21, 0xc2, 0x53, 0x66, 0x8e, 0xaa, 0x10, 0x06, 0x84, 0x88, 0x4a, 0xed, 0xc2, 0xae, + 0x95, 0x02, 0x74, 0x30, 0xa6, 0x8d, 0x46, 0x88, 0x0b, 0x78, 0x9e, 0xb7, 0x3f, 0x0e, 0x98, 0xe6, 0x89, 0x3f, 0xa8, + 0x06, 0xfd, 0x77, 0x24, 0x9b, 0xac, 0x9f, 0xdc, 0xf7, 0xc3, 0x27, 0x7d, 0x87, 0xce, 0xde, 0xef, 0xab, 0xbf, 0x7f, + 0xec, 0xd1, 0x40, 0x16, 0xf2, 0x0b, 0xdd, 0x84, 0x56, 0xcf, 0xde, 0x18, 0xee, 0x88, 0x56, 0xcf, 0x4e, 0x2f, 0x0a, + 0xd4, 0x3b, 0xd7, 0x4e, 0x6d, 0x1b, 0x36, 0x32, 0x89, 0xc7, 0x9a, 0x27, 0x63, 0xb0, 0x22, 0x83, 0x6a, 0x05, 0x2b, + 0x9b, 0x2c, 0xd1, 0x5d, 0x9f, 0x99, 0x83, 0x7b, 0xe2, 0x46, 0xbe, 0x93, 0x67, 0x1f, 0x80, 0x9b, 0x10, 0xf9, 0x4b, + 0x0e, 0xab, 0xfa, 0x1d, 0xd5, 0xa6, 0x3b, 0x28, 0x18, 0x4a, 0x2d, 0x31, 0x5b, 0x15, 0x8d, 0x25, 0xd8, 0x1b, 0x04, + 0x5a, 0x53, 0xab, 0x0f, 0xeb, 0x70, 0xc8, 0x1f, 0x5b, 0xfb, 0x07, 0x95, 0x89, 0xba, 0x68, 0x40, 0x9e, 0x86, 0x5f, + 0xba, 0x44, 0xb8, 0x6c, 0x53, 0xff, 0xaf, 0x6e, 0x2f, 0x76, 0x46, 0xc1, 0x24, 0xe4, 0x6d, 0xc8, 0xc3, 0xdd, 0xc1, + 0x00, 0x05, 0x4a, 0xe7, 0x1b, 0x6d, 0x78, 0x12, 0x3d, 0xc9, 0xc3, 0xf6, 0x79, 0x69, 0xaf, 0x46, 0x7d, 0xae, 0x63, + 0x9b, 0xda, 0xb6, 0x49, 0x4d, 0x49, 0x73, 0x70, 0x05, 0x96, 0x18, 0x17, 0x34, 0xad, 0xe4, 0x11, 0x4b, 0x2c, 0xc7, + 0xa4, 0xca, 0xad, 0xe4, 0x29, 0xa7, 0x8c, 0xff, 0x10, 0xb4, 0x97, 0x59, 0x1e, 0x0d, 0x97, 0xe5, 0xd1, 0x65, 0xb0, + 0x36, 0xa1, 0xba, 0xb7, 0xa1, 0xfa, 0x62, 0xd6, 0xb4, 0xd4, 0x6a, 0x93, 0x24, 0x91, 0xc6, 0x7b, 0xba, 0x58, 0xd7, + 0x03, 0xe8, 0xce, 0xd4, 0x2e, 0x65, 0x12, 0xc7, 0x38, 0xd9, 0x86, 0xb9, 0xfa, 0xd8, 0x2a, 0xad, 0xcf, 0x5f, 0xd0, + 0xf8, 0xdc, 0x7d, 0x2b, 0x8f, 0x18, 0xb5, 0x18, 0x78, 0x7f, 0x78, 0x2a, 0xc1, 0xc5, 0xa1, 0xb1, 0xb3, 0x3d, 0x4c, + 0x1c, 0x76, 0xec, 0xec, 0xd7, 0x14, 0x4c, 0xcf, 0x81, 0x36, 0xf4, 0xd5, 0xe0, 0xf8, 0xda, 0x3d, 0x77, 0xf0, 0x62, + 0x40, 0x4b, 0xa4, 0xbc, 0x53, 0xe4, 0x88, 0x01, 0x26, 0x5a, 0xf9, 0x9b, 0x5f, 0xe7, 0xf5, 0x87, 0xf8, 0x7a, 0x3c, + 0x10, 0x3b, 0x51, 0x1e, 0x3d, 0x2b, 0x14, 0xa5, 0x44, 0x45, 0x4f, 0xe1, 0x2f, 0x6e, 0xa1, 0x0c, 0xa7, 0x89, 0x4e, + 0x47, 0x45, 0xb7, 0x77, 0x4f, 0x7c, 0x67, 0xff, 0xa6, 0x3a, 0x97, 0xf3, 0x0a, 0x03, 0x5d, 0x08, 0x6c, 0xa0, 0x8c, + 0x8c, 0x05, 0x4a, 0xf1, 0x63, 0xcc, 0x2e, 0x43, 0x94, 0xdc, 0xea, 0x13, 0x3e, 0x70, 0x11, 0x98, 0x3b, 0xa4, 0x49, + 0xc2, 0xe8, 0x51, 0x7b, 0x6e, 0x5a, 0x9e, 0x84, 0x99, 0x9d, 0x27, 0x99, 0x9d, 0x53, 0xc5, 0x85, 0x09, 0x53, 0x35, + 0x28, 0x16, 0x8f, 0xe5, 0xa4, 0xb6, 0x5a, 0x4d, 0x33, 0x27, 0x9a, 0xe9, 0x91, 0x3b, 0x0c, 0x81, 0x6e, 0xd2, 0x0d, + 0x35, 0xfa, 0x4d, 0x54, 0xf1, 0xd1, 0x7a, 0x11, 0x0c, 0xd1, 0xfa, 0x74, 0xd6, 0x46, 0xb9, 0x63, 0x14, 0x25, 0xdf, + 0x17, 0x80, 0xb8, 0xb7, 0xae, 0x28, 0x5d, 0x7d, 0xf2, 0xc7, 0x3f, 0x4c, 0xb5, 0x9e, 0x07, 0x10, 0x23, 0xbe, 0x66, + 0x93, 0x33, 0xa3, 0xf2, 0x48, 0xfc, 0x43, 0x98, 0xb4, 0x80, 0x3b, 0x42, 0x58, 0xb5, 0x71, 0x30, 0x49, 0x4e, 0xe7, + 0x62, 0xa8, 0xef, 0xa2, 0x91, 0x24, 0x94, 0x49, 0x7d, 0x0a, 0x9e, 0x4d, 0x4e, 0xad, 0x8f, 0x0e, 0x09, 0x77, 0xeb, + 0x20, 0x14, 0x62, 0xa6, 0x5a, 0x03, 0xdc, 0x3d, 0xa5, 0xfb, 0x7a, 0xed, 0x8d, 0xd7, 0x2a, 0xe2, 0xfe, 0xfb, 0xc5, + 0xc1, 0xfb, 0xef, 0x78, 0xa9, 0xa9, 0xdf, 0x6f, 0x9c, 0x0d, 0xdb, 0xb7, 0x3c, 0x00, 0x2f, 0x06, 0x78, 0x08, 0x70, + 0x11, 0xf5, 0x56, 0xa7, 0xfd, 0x61, 0x74, 0xe3, 0xeb, 0xca, 0xec, 0x59, 0xd1, 0xf5, 0x3b, 0x3f, 0x78, 0xb7, 0x6f, + 0x21, 0x60, 0x17, 0xdd, 0xff, 0x1f, 0x81, 0x0a, 0x08, 0x86, 0x82, 0xbf, 0x3f, 0x6e, 0x87, 0xb3, 0x23, 0x78, 0x0e, + 0xbd, 0x3e, 0x8e, 0x62, 0xa5, 0x7b, 0x27, 0x4d, 0xb1, 0x57, 0x11, 0x54, 0x99, 0x57, 0xc4, 0xa6, 0x8c, 0xcd, 0x2e, + 0xeb, 0x52, 0xaf, 0xcd, 0x37, 0x18, 0xd0, 0x97, 0x00, 0xc8, 0x48, 0xf5, 0xa6, 0x0c, 0x20, 0xfc, 0xfa, 0x52, 0x2c, + 0x46, 0xf3, 0x7c, 0xa7, 0xb5, 0x6b, 0xf7, 0x29, 0xf4, 0xc3, 0x76, 0x1d, 0x1e, 0x0c, 0xed, 0x09, 0x79, 0x9e, 0x37, + 0xbc, 0xcb, 0xf0, 0x6d, 0x5e, 0x14, 0x9c, 0x06, 0x2f, 0xa3, 0x5a, 0x1a, 0xf2, 0x49, 0x34, 0x06, 0xfa, 0xb4, 0x6f, + 0x29, 0x01, 0xb7, 0x21, 0x31, 0xd8, 0x41, 0x56, 0x7a, 0x7d, 0x24, 0xed, 0x9d, 0xeb, 0x31, 0xbc, 0xd9, 0x6e, 0x71, + 0x91, 0x32, 0x22, 0xb1, 0x63, 0xa0, 0xc9, 0x8d, 0x50, 0xed, 0xed, 0xce, 0x9e, 0x0f, 0xdf, 0xdc, 0x5c, 0xde, 0xdc, + 0xae, 0x8f, 0x43, 0xaa, 0xb1, 0x4e, 0xa7, 0xd6, 0x6a, 0x6c, 0x27, 0x6d, 0x91, 0xef, 0x2d, 0x0b, 0x9b, 0x84, 0x16, + 0xe9, 0x06, 0x96, 0x96, 0x0f, 0x93, 0xaa, 0x55, 0x06, 0x38, 0x91, 0x9a, 0xba, 0x9f, 0x9e, 0x9e, 0x33, 0x25, 0xcb, + 0x03, 0x7a, 0x71, 0xd0, 0x55, 0x21, 0xb6, 0x4b, 0xd7, 0x6f, 0x2f, 0x97, 0x9e, 0xeb, 0x06, 0x60, 0x13, 0x39, 0x30, + 0x90, 0xf2, 0x7f, 0xc7, 0xa2, 0x5e, 0x0e, 0xcb, 0x93, 0x25, 0x82, 0xc2, 0x25, 0xde, 0x75, 0x49, 0x4a, 0xb4, 0x29, + 0x45, 0x68, 0x2e, 0x5e, 0x1f, 0x15, 0xe3, 0x49, 0xdd, 0x59, 0xf3, 0xec, 0x20, 0x12, 0x19, 0x5b, 0x19, 0x1b, 0xcc, + 0x4d, 0x5a, 0x86, 0x00, 0x07, 0x85, 0x64, 0xcb, 0xf5, 0xa6, 0x0b, 0xb0, 0x5d, 0xf2, 0x57, 0xa3, 0x71, 0x9e, 0x2c, + 0xd1, 0x1d, 0x1a, 0xf6, 0xe5, 0x40, 0xc1, 0xe4, 0xe6, 0xca, 0xe9, 0x91, 0x1f, 0xc5, 0x6c, 0xb1, 0x46, 0x86, 0xc1, + 0x82, 0xe9, 0x04, 0x1c, 0x08, 0xb9, 0x57, 0x0e, 0x10, 0x5b, 0x16, 0xf8, 0x30, 0x98, 0x5b, 0x22, 0x9b, 0x3c, 0xda, + 0xd9, 0x3d, 0x55, 0x28, 0xf8, 0xe4, 0xd6, 0x6d, 0x59, 0xf2, 0xca, 0x0f, 0x82, 0x5e, 0xc5, 0xe5, 0x69, 0xbb, 0x68, + 0x8e, 0xc9, 0xd1, 0x77, 0xd9, 0x94, 0xfd, 0x30, 0x8d, 0xc8, 0xc3, 0x43, 0x92, 0xe1, 0x30, 0x0b, 0x82, 0xc5, 0x4e, + 0x78, 0x29, 0x6c, 0x60, 0xac, 0x6d, 0xc2, 0x8e, 0xd4, 0x10, 0xde, 0x21, 0x26, 0xac, 0x99, 0xb3, 0x16, 0x2c, 0x10, + 0x71, 0x39, 0xe8, 0x3e, 0x72, 0xa0, 0x5f, 0xd9, 0x0a, 0x1d, 0xed, 0xe2, 0x6e, 0xf6, 0x23, 0x16, 0xc8, 0xd8, 0x92, + 0x39, 0xe9, 0x1a, 0x7e, 0xcb, 0x50, 0xad, 0xad, 0x67, 0xa3, 0xb3, 0x7b, 0xc3, 0x34, 0xd1, 0x96, 0x25, 0x3b, 0xa2, + 0x64, 0xfd, 0x42, 0x02, 0xb7, 0x50, 0xe5, 0x46, 0xee, 0xad, 0x44, 0x11, 0xc4, 0x14, 0xba, 0x78, 0xdd, 0x2d, 0x8c, + 0x88, 0x37, 0xd3, 0xa9, 0x39, 0x2a, 0x7c, 0x22, 0x63, 0x50, 0x52, 0x92, 0x82, 0xff, 0x67, 0xbd, 0x5f, 0x80, 0x82, + 0xf8, 0xc4, 0xaf, 0x7f, 0x17, 0x44, 0x38, 0xb0, 0xdb, 0x5e, 0xb6, 0xaa, 0x1d, 0x4b, 0x50, 0x1e, 0x15, 0xe6, 0xdc, + 0x40, 0x6a, 0xfd, 0x7b, 0x6e, 0xe3, 0xcd, 0x9f, 0xbf, 0xcb, 0x5c, 0xad, 0xdb, 0xe5, 0xe6, 0xb5, 0x3b, 0xe4, 0x9a, + 0xb1, 0x03, 0xf6, 0xe5, 0xe0, 0xc3, 0x6a, 0x26, 0xdd, 0x02, 0x92, 0x86, 0x4c, 0x2f, 0xdc, 0xae, 0xe8, 0x86, 0x13, + 0x72, 0x07, 0xe4, 0x10, 0x20, 0xd0, 0x66, 0x50, 0xd6, 0xe8, 0x58, 0xef, 0xc3, 0x79, 0x7b, 0x7d, 0xf9, 0xf7, 0xba, + 0x5e, 0xa2, 0x43, 0x9a, 0x9d, 0xc5, 0xa0, 0xff, 0x7e, 0x2b, 0x19, 0xc9, 0xf6, 0xcd, 0xf6, 0xfe, 0x5d, 0x0b, 0x8a, + 0x6b, 0x9a, 0xf6, 0x0f, 0x7e, 0xf9, 0xa2, 0xb7, 0xf0, 0x7a, 0xe7, 0x23, 0xa9, 0x49, 0x53, 0x6e, 0xf8, 0x71, 0xb5, + 0x95, 0xef, 0x4a, 0x66, 0x7c, 0x40, 0x60, 0xc4, 0xc9, 0xea, 0xe2, 0xe9, 0x61, 0xc4, 0x64, 0x3d, 0x6a, 0x18, 0x4e, + 0x6e, 0x6d, 0xc6, 0xb4, 0x6a, 0x21, 0x32, 0xc0, 0x25, 0x1a, 0x95, 0x28, 0x12, 0x25, 0x31, 0x40, 0x70, 0x6f, 0x7d, + 0x9e, 0xa0, 0x2d, 0x6a, 0xd6, 0x0e, 0xd4, 0x76, 0x56, 0x36, 0x27, 0x01, 0xa3, 0xcd, 0x1c, 0xd3, 0x6a, 0x2e, 0x42, + 0xe7, 0xee, 0x34, 0x88, 0x0e, 0xbd, 0x25, 0xba, 0x94, 0xb9, 0x62, 0xdf, 0xb4, 0xac, 0x2d, 0x03, 0xf2, 0x49, 0xd4, + 0x46, 0x1d, 0x24, 0x58, 0xe5, 0x54, 0x6c, 0x26, 0xf6, 0x8d, 0xa1, 0x2d, 0xdc, 0x81, 0xbe, 0x81, 0x1e, 0xac, 0xf1, + 0x92, 0xdd, 0xe4, 0xed, 0x53, 0xca, 0x0b, 0x8b, 0x49, 0xf7, 0x3b, 0xa9, 0x1e, 0xdb, 0x5b, 0x03, 0xa2, 0x50, 0x8c, + 0x77, 0x0f, 0x09, 0x56, 0x1e, 0xbd, 0x0d, 0x38, 0xb6, 0x4a, 0xaf, 0x71, 0x55, 0x3d, 0x31, 0x26, 0x78, 0x58, 0xca, + 0x27, 0xdf, 0x3f, 0x79, 0x35, 0xee, 0x1a, 0xc6, 0x4b, 0x8b, 0x5b, 0x10, 0x54, 0x30, 0x7b, 0x8b, 0x59, 0xfc, 0xd2, + 0xfc, 0xbe, 0x7b, 0xe0, 0xc6, 0xce, 0x21, 0x37, 0x6f, 0x70, 0xf7, 0x5a, 0xdc, 0xa7, 0xce, 0x67, 0xf5, 0xec, 0xd3, + 0xe9, 0x6a, 0x6b, 0x14, 0x7d, 0x3b, 0x03, 0xed, 0x11, 0xe9, 0xac, 0x01, 0x98, 0x04, 0x28, 0x4b, 0x32, 0xa0, 0x86, + 0x05, 0x5e, 0x2e, 0xad, 0xba, 0x13, 0xd4, 0x54, 0x7b, 0xb6, 0x29, 0x9f, 0x0b, 0x6b, 0x2c, 0xbe, 0x58, 0xba, 0x4e, + 0x53, 0xc3, 0x14, 0xb5, 0xae, 0x5d, 0xf3, 0xf7, 0x6f, 0x65, 0x09, 0x34, 0x4c, 0xe5, 0x8a, 0xfd, 0x1a, 0x55, 0x43, + 0xf0, 0x29, 0x2c, 0xa2, 0x84, 0x00, 0xcf, 0x62, 0x12, 0xa8, 0x5a, 0x3f, 0xb4, 0xbd, 0xdf, 0xbb, 0x63, 0xeb, 0x64, + 0x3a, 0xb8, 0x6b, 0x40, 0x96, 0x99, 0xf3, 0xce, 0x99, 0x96, 0xa1, 0x9b, 0xc6, 0x45, 0x48, 0xd9, 0x4f, 0x5f, 0xa0, + 0x4e, 0x96, 0xdb, 0xec, 0x51, 0xd0, 0x58, 0x0e, 0x91, 0x14, 0xb9, 0x20, 0xc5, 0xbf, 0x0b, 0x47, 0x3c, 0x46, 0x6a, + 0x9d, 0xa9, 0x65, 0x8c, 0xa6, 0xff, 0x16, 0xd6, 0x82, 0xa5, 0xdd, 0x7b, 0x96, 0xc1, 0x8f, 0x93, 0x01, 0xd5, 0x3a, + 0x77, 0x52, 0x26, 0x9b, 0x25, 0x8c, 0x0c, 0xed, 0x8e, 0x5a, 0xfd, 0xf4, 0x6b, 0xbd, 0x5d, 0x9a, 0xbd, 0x34, 0xcd, + 0x2f, 0xa2, 0x85, 0x81, 0x2c, 0x01, 0x17, 0x0b, 0x4a, 0x3b, 0x27, 0xd5, 0xbf, 0xf7, 0xcd, 0xf7, 0xc4, 0xf7, 0xc2, + 0x5f, 0x66, 0x3e, 0x8f, 0x7c, 0xca, 0x2b, 0x3f, 0x40, 0x9e, 0x4f, 0xee, 0xad, 0x16, 0x0c, 0x23, 0x98, 0x88, 0xac, + 0x5c, 0x81, 0x80, 0x45, 0x91, 0x3c, 0x50, 0x01, 0x89, 0x88, 0x2b, 0xdb, 0x21, 0xad, 0x66, 0xbd, 0x9b, 0x01, 0x85, + 0x01, 0xd7, 0xfe, 0x42, 0xe3, 0x9c, 0x2e, 0xf6, 0xd6, 0x51, 0x51, 0xe9, 0x58, 0x1a, 0xfd, 0x11, 0x98, 0x18, 0x51, + 0xc9, 0xe9, 0xa8, 0x38, 0xb3, 0x18, 0xed, 0x2b, 0x3a, 0x8b, 0x19, 0xc8, 0x58, 0xa9, 0x29, 0x5b, 0xf9, 0x0d, 0x30, + 0xbb, 0x3d, 0x97, 0x34, 0xf5, 0x18, 0x0e, 0xe4, 0x05, 0x44, 0x0d, 0xac, 0x68, 0x03, 0x9d, 0xda, 0x6f, 0x08, 0xcf, + 0x1b, 0x96, 0x47, 0x80, 0x20, 0x28, 0xdf, 0x41, 0xd8, 0x9f, 0xd8, 0xbe, 0x72, 0x35, 0xc3, 0x29, 0xc3, 0xf4, 0x19, + 0x87, 0x86, 0xfa, 0x14, 0xfc, 0x04, 0x6c, 0xa2, 0xab, 0x11, 0x20, 0xdf, 0x24, 0x84, 0x1e, 0x04, 0xfd, 0x2b, 0x8f, + 0x48, 0x7f, 0xdd, 0xd4, 0xea, 0x2b, 0x98, 0xe2, 0xa8, 0x4c, 0xd6, 0x6d, 0x6a, 0x5b, 0xbd, 0xb2, 0x65, 0x5c, 0xd7, + 0x80, 0x3a, 0x2d, 0x9d, 0xe3, 0x0c, 0x27, 0x0d, 0xf1, 0xbf, 0x06, 0x86, 0x3f, 0xa8, 0xdd, 0x0e, 0xa3, 0x0f, 0xfd, + 0xc6, 0x8c, 0x79, 0x87, 0x70, 0x78, 0x3c, 0x31, 0x8d, 0xdc, 0x9f, 0x0b, 0x4c, 0x87, 0x96, 0xf8, 0x23, 0x8d, 0x38, + 0xe9, 0x83, 0xd2, 0x8b, 0xd5, 0xa1, 0x32, 0xfe, 0xdb, 0xb8, 0x1f, 0xbe, 0x6d, 0xb3, 0x8a, 0xe1, 0xc9, 0x88, 0x02, + 0xb6, 0x1a, 0xb3, 0x8e, 0x4f, 0x8e, 0xd6, 0xe3, 0x98, 0xdb, 0x80, 0xa8, 0x71, 0xbd, 0xa9, 0xda, 0x2c, 0x52, 0xb1, + 0xe5, 0x96, 0x3d, 0x1f, 0xcc, 0xa8, 0x7c, 0xfc, 0xf3, 0x32, 0x15, 0x82, 0x00, 0x55, 0xe2, 0x43, 0x34, 0xd0, 0xc5, + 0x6e, 0x27, 0x68, 0xe1, 0xb7, 0x96, 0xd2, 0x4a, 0xe6, 0xc1, 0x6a, 0xee, 0x90, 0x80, 0x8e, 0xaa, 0x01, 0xc3, 0xa7, + 0x68, 0xb2, 0xab, 0xc9, 0x31, 0x42, 0x01, 0x4d, 0xce, 0x92, 0x86, 0x93, 0x61, 0xbf, 0x2d, 0x4e, 0x7f, 0x9d, 0xf3, + 0x51, 0xb3, 0x21, 0x52, 0xdf, 0x8e, 0x89, 0x98, 0x7e, 0xc7, 0x57, 0x59, 0x19, 0x1b, 0xa1, 0x78, 0x33, 0x88, 0x8d, + 0x21, 0xc9, 0x1b, 0x05, 0x25, 0x42, 0x24, 0xbb, 0x38, 0x11, 0x66, 0xf3, 0x7e, 0xa5, 0xf0, 0xf4, 0x15, 0xa1, 0xd4, + 0x1c, 0x23, 0x8d, 0x0e, 0xb6, 0x74, 0xc2, 0xda, 0xb4, 0x7d, 0x5c, 0x7d, 0x81, 0x41, 0x87, 0xcf, 0x1c, 0xf0, 0x02, + 0xe0, 0xc6, 0xb0, 0x0a, 0x60, 0xad, 0x31, 0x77, 0x0c, 0xb7, 0x65, 0x7c, 0x62, 0x2d, 0x73, 0x40, 0xff, 0x98, 0xc8, + 0x72, 0x43, 0x7b, 0x0e, 0x41, 0xc1, 0xb4, 0x1d, 0x58, 0xa2, 0xf2, 0xef, 0xb4, 0x29, 0x76, 0x55, 0x31, 0x31, 0x0f, + 0x84, 0xcb, 0x12, 0x09, 0x95, 0xaf, 0x7b, 0xd7, 0x63, 0x06, 0xf8, 0x88, 0xa8, 0x19, 0x54, 0xbc, 0xce, 0x4d, 0x7e, + 0x55, 0x3f, 0xbf, 0x04, 0xec, 0x75, 0xf6, 0xba, 0xfe, 0xf0, 0xba, 0x7a, 0xfa, 0x93, 0x52, 0x00, 0xf4, 0x5c, 0xd8, + 0x95, 0x61, 0x26, 0x0b, 0x9b, 0xc8, 0xf0, 0x73, 0xbd, 0x84, 0xf2, 0xb4, 0x99, 0x03, 0x42, 0x38, 0xc7, 0xf9, 0xe4, + 0xfa, 0x74, 0x95, 0xb9, 0x09, 0xa4, 0x08, 0xb8, 0x09, 0x20, 0xf3, 0xfe, 0x08, 0x67, 0xce, 0x07, 0x04, 0xe2, 0x5d, + 0x5c, 0x9b, 0x1c, 0x3d, 0x0e, 0x92, 0x98, 0xdd, 0x4f, 0x3d, 0x2a, 0x88, 0xcb, 0x68, 0x01, 0x0d, 0x5b, 0x53, 0x76, + 0x2d, 0x58, 0xee, 0x08, 0x1d, 0x36, 0x84, 0x99, 0x42, 0x57, 0x89, 0xfc, 0x87, 0x47, 0x4b, 0xaa, 0xe8, 0xb1, 0x3b, + 0x7a, 0xb6, 0x22, 0xca, 0x70, 0x52, 0x47, 0x42, 0x82, 0xf0, 0x85, 0xa8, 0x81, 0x7e, 0xc0, 0xc6, 0xa8, 0x52, 0xe2, + 0x12, 0xdb, 0x12, 0xe8, 0x3b, 0x09, 0xc2, 0xb2, 0x53, 0x1a, 0x86, 0xe6, 0x90, 0xc3, 0x48, 0x14, 0x41, 0x29, 0xfc, + 0x02, 0x25, 0xcf, 0x34, 0x94, 0x80, 0x32, 0x75, 0x60, 0x47, 0x0d, 0x55, 0x89, 0x09, 0x75, 0x7a, 0x7a, 0x10, 0xdd, + 0xbb, 0x0c, 0x34, 0x4d, 0x07, 0xa7, 0x1d, 0x2a, 0xc6, 0xd2, 0x98, 0xea, 0x60, 0x3b, 0x2a, 0x04, 0x47, 0x3a, 0x1e, + 0x32, 0x0a, 0x4e, 0x6e, 0xdf, 0xe1, 0xb2, 0xe1, 0xd3, 0xed, 0xa7, 0x4a, 0x8c, 0x8e, 0x9e, 0xac, 0xce, 0xa5, 0xd5, + 0xf3, 0x6c, 0xcc, 0x24, 0x48, 0x9f, 0xc0, 0xa1, 0x52, 0xf8, 0x32, 0x03, 0xd3, 0x22, 0x8f, 0xb7, 0x65, 0xb4, 0x38, + 0x85, 0x92, 0xab, 0x6e, 0x1f, 0xe9, 0x36, 0xdf, 0xce, 0xa4, 0xdb, 0x6f, 0xa7, 0xc1, 0x51, 0xd6, 0xcc, 0xfa, 0x42, + 0xf9, 0xbc, 0x52, 0xaa, 0xed, 0x5b, 0xf9, 0x49, 0xa2, 0x83, 0x63, 0x0d, 0xd5, 0x2a, 0x2c, 0xf1, 0x93, 0x81, 0xd5, + 0x6b, 0x48, 0xb5, 0x91, 0x8a, 0x61, 0x07, 0x9e, 0x8f, 0x3c, 0x9e, 0xbb, 0xae, 0x34, 0xe3, 0xca, 0x30, 0xb3, 0x49, + 0x25, 0xc6, 0xf7, 0xc3, 0x63, 0x0f, 0xed, 0x99, 0xf6, 0xf9, 0x74, 0xf8, 0x12, 0xe8, 0x74, 0x20, 0x9a, 0x80, 0x81, + 0x39, 0x84, 0x32, 0x16, 0x68, 0x6c, 0x2c, 0x66, 0x51, 0x1e, 0x95, 0x29, 0x4d, 0x95, 0xc6, 0x30, 0x86, 0xda, 0x00, + 0xae, 0x6e, 0xd7, 0x4c, 0x4a, 0x46, 0x49, 0x77, 0x29, 0x0d, 0x14, 0xd3, 0x31, 0x8c, 0x15, 0x9e, 0x29, 0x19, 0x2e, + 0x0a, 0x71, 0x1a, 0xe0, 0xcb, 0x8b, 0xff, 0xf7, 0xaf, 0xc0, 0xa8, 0xb9, 0xed, 0x91, 0xac, 0xd9, 0xec, 0x68, 0x4b, + 0x2b, 0x3c, 0x4f, 0xe7, 0xcb, 0x17, 0x29, 0xeb, 0x52, 0x2d, 0x8a, 0xd3, 0xe8, 0x28, 0x23, 0x4a, 0xfb, 0x76, 0xf7, + 0x97, 0xba, 0x33, 0x8c, 0x98, 0x2b, 0xdf, 0xf8, 0x3d, 0xe5, 0x5a, 0xf2, 0x6e, 0xb7, 0x8c, 0xac, 0x4a, 0x31, 0xe1, + 0x43, 0xe5, 0x1a, 0x5e, 0x69, 0xfd, 0x07, 0xf9, 0x4f, 0xb9, 0xaa, 0x6d, 0x7f, 0x0c, 0xeb, 0x95, 0x6c, 0x4e, 0xb4, + 0xde, 0x3c, 0xe3, 0x88, 0xb7, 0x3d, 0xc6, 0xfd, 0x25, 0x85, 0x63, 0x69, 0xfc, 0xae, 0xea, 0x64, 0x37, 0x3f, 0xb9, + 0x5c, 0x90, 0xb4, 0x98, 0x74, 0xeb, 0xad, 0xca, 0x7e, 0xe6, 0xab, 0xf7, 0xfb, 0xb3, 0x87, 0x3b, 0x26, 0x41, 0xc2, + 0x6d, 0x43, 0x3e, 0x0d, 0x22, 0xbd, 0x6d, 0x46, 0x47, 0x69, 0xf2, 0xca, 0x99, 0x4d, 0x08, 0x84, 0xe3, 0x8d, 0xe9, + 0x01, 0x26, 0x3b, 0x93, 0xd2, 0xcb, 0xfe, 0x67, 0x76, 0xe5, 0xda, 0xd4, 0xc5, 0x5d, 0xb1, 0xc5, 0x83, 0xe4, 0xd7, + 0x43, 0x7c, 0x38, 0x86, 0x37, 0x9f, 0xe3, 0x77, 0xc8, 0x3f, 0xea, 0xb8, 0x0c, 0x0c, 0x4c, 0xac, 0x1c, 0xfb, 0x4e, + 0x78, 0xd9, 0xdf, 0x12, 0x6b, 0x50, 0x56, 0x69, 0x8a, 0x21, 0x18, 0xc4, 0x79, 0x1d, 0x00, 0xc8, 0x95, 0x0d, 0x62, + 0x9b, 0x27, 0xb2, 0xe5, 0xab, 0x60, 0xf1, 0xce, 0xf1, 0xd1, 0x0b, 0x6e, 0x4a, 0x7c, 0xaa, 0xbc, 0x3d, 0x63, 0x0c, + 0x70, 0x0b, 0xca, 0xd3, 0xb1, 0x83, 0x19, 0x31, 0x47, 0x42, 0xed, 0x8a, 0x4a, 0x2c, 0x49, 0x1d, 0x2a, 0x14, 0xcd, + 0xea, 0x82, 0x91, 0x89, 0xe4, 0xb3, 0x35, 0x55, 0x82, 0x81, 0xd4, 0x41, 0x7b, 0xf6, 0x2c, 0x4a, 0x9a, 0x7d, 0x1e, + 0x9a, 0x6c, 0x92, 0x3b, 0x7e, 0x09, 0xa6, 0x3f, 0xf8, 0x59, 0x28, 0xe9, 0x73, 0x6f, 0x62, 0x21, 0x7f, 0xb7, 0x95, + 0xf5, 0x27, 0xec, 0x1d, 0xfe, 0x26, 0x21, 0x7c, 0x39, 0x85, 0xd5, 0x24, 0x61, 0x59, 0xb8, 0xf0, 0x76, 0x49, 0x80, + 0x3c, 0x65, 0x69, 0x57, 0x83, 0x03, 0x85, 0x3e, 0x14, 0x94, 0x2c, 0x96, 0xb1, 0x12, 0x33, 0xc3, 0x22, 0xa6, 0xe4, + 0x5e, 0xf4, 0x35, 0xf3, 0xbe, 0xf9, 0x3a, 0x85, 0x47, 0x06, 0x4f, 0xe5, 0xa6, 0x6d, 0x5b, 0x88, 0x0e, 0x18, 0x9a, + 0xe9, 0x4f, 0x70, 0x40, 0xbb, 0x7f, 0xdd, 0xa5, 0xa7, 0x1c, 0xf8, 0xec, 0x39, 0x0e, 0xd6, 0x56, 0x9e, 0xa5, 0x9c, + 0x35, 0x54, 0xf7, 0x39, 0x05, 0x3f, 0x17, 0xef, 0x10, 0x57, 0x26, 0xc1, 0xd3, 0x5d, 0x4c, 0x12, 0x54, 0x9f, 0x82, + 0x21, 0xe9, 0x04, 0x74, 0xb1, 0xc2, 0xea, 0x5a, 0xb3, 0xe5, 0x09, 0xba, 0x98, 0x60, 0x05, 0x63, 0x38, 0x14, 0xf4, + 0xf2, 0x30, 0xb3, 0x1e, 0x56, 0xd3, 0xd3, 0x22, 0x48, 0x22, 0x9d, 0xec, 0xf6, 0x53, 0x92, 0xbd, 0x26, 0x12, 0x40, + 0x3f, 0x37, 0x2b, 0x69, 0x03, 0xe0, 0x41, 0xad, 0x10, 0xb1, 0xef, 0x45, 0xcc, 0x49, 0x2a, 0x55, 0x73, 0x46, 0xb7, + 0x15, 0x02, 0x62, 0x5d, 0xf8, 0x5b, 0x5e, 0xdd, 0x94, 0xfa, 0x53, 0xb0, 0x80, 0xbe, 0xe1, 0x42, 0x02, 0xaf, 0x8d, + 0x8d, 0xf7, 0x8a, 0xc6, 0x1a, 0x5f, 0x02, 0x58, 0x1c, 0x0c, 0xf0, 0xa4, 0xc6, 0x32, 0x2c, 0x01, 0x69, 0x15, 0x0f, + 0x9d, 0x98, 0xb0, 0xf2, 0xb4, 0xe0, 0x98, 0xe5, 0xbb, 0x7f, 0x98, 0xdf, 0xe9, 0xb4, 0x4e, 0x20, 0x31, 0xd3, 0xa9, + 0x76, 0x4b, 0x2f, 0x1f, 0x58, 0xbf, 0xd6, 0x98, 0x25, 0xe2, 0x9e, 0xe4, 0x65, 0xb7, 0x63, 0x15, 0xda, 0x58, 0xc4, + 0x32, 0x9e, 0x29, 0x87, 0x57, 0x53, 0x6f, 0xf3, 0xf0, 0x00, 0x0e, 0xcf, 0xa7, 0x96, 0xfb, 0xeb, 0x00, 0x13, 0x87, + 0x9b, 0x52, 0x28, 0x15, 0xf1, 0x7a, 0x10, 0x20, 0x12, 0xc4, 0x44, 0xbb, 0xc8, 0x50, 0x7a, 0xca, 0x0d, 0x62, 0xb3, + 0x01, 0x25, 0x62, 0x87, 0xb6, 0x8e, 0xd2, 0x1f, 0xc2, 0x57, 0x47, 0xf9, 0x54, 0x99, 0xea, 0xa4, 0xb7, 0x30, 0xcb, + 0xe5, 0x48, 0x35, 0x34, 0x60, 0xd9, 0x71, 0xfb, 0xc9, 0x63, 0x5b, 0x61, 0x78, 0x6e, 0xab, 0xfe, 0x6e, 0x1b, 0xfe, + 0xfe, 0x02, 0x5e, 0x3c, 0xfd, 0xbe, 0xee, 0x6b, 0x6e, 0xd9, 0x90, 0x43, 0x5d, 0xda, 0x8d, 0x88, 0xb8, 0x17, 0x2f, + 0xaf, 0x52, 0x48, 0x01, 0xd2, 0xfc, 0x01, 0x3c, 0x3b, 0xbe, 0x3d, 0xd2, 0x7d, 0x2a, 0x32, 0x41, 0x24, 0xe4, 0xed, + 0x82, 0xb0, 0xe2, 0xb1, 0xa7, 0xb0, 0x69, 0x64, 0x41, 0x9f, 0x4a, 0xe8, 0x12, 0x7e, 0x8a, 0x7c, 0x79, 0x39, 0x17, + 0xfc, 0x18, 0xd2, 0x09, 0x68, 0xb0, 0x3b, 0xeb, 0x45, 0x50, 0x06, 0x39, 0xed, 0x2d, 0xa5, 0x79, 0x27, 0x97, 0x8d, + 0x02, 0xd3, 0x96, 0x85, 0xf6, 0x4b, 0xa3, 0x6e, 0xba, 0x78, 0x6a, 0xa2, 0x10, 0xf0, 0xf0, 0xb0, 0xd9, 0xed, 0xa4, + 0xa1, 0x9c, 0x55, 0x73, 0xef, 0xab, 0x55, 0xe3, 0x8a, 0xe4, 0xe3, 0x61, 0x86, 0x20, 0xa4, 0xdd, 0x8e, 0x9c, 0x1a, + 0xc3, 0x51, 0xd1, 0xbe, 0x48, 0xd6, 0x79, 0xe2, 0x70, 0xdc, 0xcb, 0x27, 0x71, 0xb2, 0x71, 0xac, 0x8b, 0x93, 0x48, + 0x05, 0xbe, 0x58, 0x7d, 0xd5, 0x10, 0x6d, 0xa6, 0xc5, 0xe9, 0x5d, 0x55, 0xa5, 0x6a, 0x0a, 0xb4, 0x93, 0x22, 0x47, + 0x76, 0x33, 0xbb, 0x2b, 0xb6, 0xa1, 0xd0, 0x0c, 0x38, 0x7f, 0xd6, 0x5e, 0xac, 0x47, 0x78, 0xa8, 0xbc, 0xf8, 0x47, + 0xd1, 0x3f, 0x56, 0x3d, 0x91, 0x65, 0x2b, 0xfc, 0xd5, 0x78, 0xbd, 0xb4, 0xf8, 0x37, 0x0f, 0xdc, 0x67, 0xd7, 0xd9, + 0x91, 0xb7, 0xde, 0x9c, 0x8f, 0x57, 0x15, 0x4f, 0x17, 0x89, 0x6f, 0x18, 0x06, 0x70, 0x39, 0xa4, 0x79, 0xb9, 0xdb, + 0x7b, 0x0c, 0x9f, 0x86, 0x80, 0x90, 0x6c, 0xe7, 0xdc, 0x3e, 0x9f, 0x3f, 0x1c, 0x69, 0x33, 0x9c, 0xc9, 0x4b, 0x21, + 0xd9, 0x57, 0x08, 0x00, 0x64, 0xd5, 0x66, 0xa4, 0x63, 0x5d, 0x4d, 0x02, 0x69, 0x32, 0x49, 0xdd, 0x6e, 0x03, 0x5c, + 0x80, 0x54, 0x94, 0x2f, 0xd7, 0x83, 0x15, 0x35, 0xf5, 0xc2, 0x14, 0x5f, 0xee, 0xe5, 0x0b, 0x34, 0xad, 0x69, 0xda, + 0xcb, 0xb9, 0x0c, 0x05, 0xd6, 0xcb, 0x0e, 0x11, 0x1e, 0x64, 0x2b, 0xc6, 0xe3, 0x71, 0xe4, 0xbb, 0xc9, 0x07, 0x94, + 0x1b, 0x2c, 0x2e, 0xf7, 0xea, 0xcb, 0xa9, 0xdd, 0x14, 0xb6, 0x42, 0x9f, 0x61, 0x15, 0x05, 0x73, 0xc0, 0x9b, 0x6b, + 0x7a, 0x3b, 0x9b, 0x0b, 0xb2, 0xd9, 0xc5, 0x67, 0x0b, 0xdb, 0x20, 0x81, 0x78, 0x1c, 0x06, 0x6b, 0x72, 0x88, 0x94, + 0x78, 0x74, 0x4a, 0x53, 0x42, 0x01, 0xc8, 0x00, 0x5e, 0x4c, 0xe2, 0x2d, 0x24, 0xfd, 0xf7, 0xe0, 0x13, 0xec, 0xad, + 0x71, 0xc5, 0xc8, 0x79, 0xfe, 0xe1, 0x74, 0xc0, 0xe9, 0xcf, 0xed, 0x9d, 0xcf, 0x3d, 0x23, 0xa0, 0x46, 0xa9, 0x0f, + 0xe5, 0xc1, 0x7f, 0xd2, 0x15, 0x9d, 0xd6, 0x62, 0xbe, 0x13, 0xb1, 0x4a, 0x85, 0x2d, 0xf7, 0x32, 0xd8, 0xdf, 0xef, + 0x87, 0xe9, 0xff, 0xab, 0x6b, 0x43, 0x55, 0x7e, 0xfe, 0xb7, 0x35, 0xfc, 0x27, 0xbd, 0x0e, 0x4b, 0xcd, 0xfd, 0x6f, + 0x0d, 0x36, 0xfd, 0xf6, 0x1a, 0xea, 0xa1, 0x6d, 0xff, 0xd6, 0x03, 0x88, 0x3a, 0x28, 0x72, 0xb3, 0x27, 0xb2, 0xd2, + 0xaa, 0x73, 0x0f, 0x06, 0xda, 0xc2, 0xff, 0x9f, 0xe5, 0x3d, 0xcb, 0x9e, 0xad, 0x30, 0xb5, 0xf0, 0xf1, 0xfd, 0x8c, + 0x49, 0x00, 0xcb, 0x49, 0x84, 0x36, 0x0e, 0x39, 0xad, 0xfc, 0xb4, 0x46, 0xae, 0x43, 0x5a, 0xb1, 0x56, 0x01, 0xfd, + 0xb2, 0xa4, 0x4f, 0x10, 0xcf, 0x3d, 0x8c, 0xbd, 0x86, 0x92, 0xe0, 0x81, 0x7a, 0xbe, 0x75, 0x94, 0x1f, 0x49, 0xd3, + 0x62, 0x57, 0x4a, 0x7e, 0xe9, 0x9f, 0x3f, 0x66, 0xd9, 0x57, 0x96, 0x1f, 0x88, 0xa1, 0x26, 0xb7, 0xff, 0xc1, 0x42, + 0xda, 0x17, 0x24, 0x31, 0x16, 0xa6, 0x6e, 0x5d, 0x38, 0x9e, 0x38, 0xbd, 0x63, 0x5b, 0xb5, 0x19, 0x84, 0x17, 0x55, + 0x2d, 0x14, 0x67, 0xd7, 0x82, 0x32, 0xa6, 0xf7, 0xe9, 0x4c, 0x13, 0x0c, 0xa8, 0xa5, 0xe4, 0x9d, 0xdf, 0xf0, 0xef, + 0x6c, 0x85, 0x79, 0x57, 0x63, 0xee, 0xde, 0xc0, 0x3e, 0x1a, 0x39, 0x8c, 0xe3, 0x3e, 0x42, 0xa1, 0x6e, 0x70, 0x83, + 0x2f, 0x35, 0x12, 0xdd, 0xb3, 0x65, 0x1a, 0x46, 0x54, 0xf6, 0xbc, 0x05, 0x47, 0xe2, 0x9c, 0x71, 0x09, 0x32, 0xf4, + 0x08, 0x0d, 0xcb, 0x69, 0x78, 0x8b, 0x29, 0x6c, 0x2f, 0xef, 0x18, 0x77, 0x96, 0xad, 0xed, 0x55, 0x9a, 0x21, 0x90, + 0xce, 0x8b, 0xe0, 0xad, 0xe2, 0x49, 0xb8, 0x31, 0x6d, 0xcf, 0xd4, 0x83, 0x5d, 0x7b, 0x49, 0x2f, 0x6a, 0xf3, 0x37, + 0xb2, 0xdb, 0x7b, 0xe9, 0x98, 0x29, 0xcd, 0xeb, 0x9a, 0x2d, 0x5e, 0xbc, 0x20, 0x13, 0x7e, 0x1c, 0x5c, 0x1b, 0xb3, + 0x6e, 0xb7, 0x12, 0x80, 0xcc, 0x89, 0xc6, 0xd5, 0x5c, 0xec, 0x7f, 0xda, 0x1f, 0xa4, 0xf5, 0x60, 0xde, 0x3d, 0xb8, + 0x92, 0x11, 0x9b, 0xbf, 0x33, 0x37, 0x92, 0x7d, 0x93, 0x49, 0x0e, 0xb5, 0xa8, 0xaa, 0xe2, 0xc1, 0xbb, 0x17, 0xc9, + 0xdd, 0xd5, 0xa5, 0x25, 0xa3, 0xde, 0x20, 0x9f, 0xef, 0xd0, 0xcd, 0x3e, 0xac, 0xdb, 0x5a, 0xe3, 0xd4, 0xe2, 0x24, + 0x36, 0x4d, 0xac, 0xc2, 0xac, 0xa6, 0x13, 0xc1, 0xf6, 0xbf, 0xd6, 0xe0, 0x9a, 0x89, 0x3a, 0x14, 0xd6, 0x56, 0x28, + 0x94, 0x82, 0x1f, 0x25, 0x20, 0x61, 0xc6, 0x98, 0x13, 0x70, 0x82, 0x64, 0x4c, 0x27, 0x53, 0xa2, 0x69, 0x28, 0x37, + 0x3f, 0x88, 0x19, 0xbe, 0xcd, 0x28, 0x46, 0x40, 0x72, 0x3f, 0x32, 0x72, 0xc3, 0xc9, 0x92, 0x50, 0x23, 0xee, 0xf6, + 0xc9, 0x2f, 0x70, 0xcc, 0x78, 0x8e, 0xa5, 0xd4, 0xf8, 0x69, 0x7d, 0x7e, 0xcc, 0x7a, 0x3f, 0x5d, 0xff, 0xb0, 0xba, + 0xe7, 0xce, 0x3f, 0x28, 0xe9, 0xd4, 0x5c, 0x43, 0x66, 0x55, 0x00, 0xc8, 0x9b, 0xf2, 0xce, 0xb8, 0x8e, 0xd3, 0x7b, + 0xab, 0x44, 0x04, 0x2e, 0x55, 0xb4, 0xaa, 0x31, 0x82, 0xf9, 0x5e, 0x88, 0x18, 0x27, 0x2b, 0x07, 0xbe, 0xf7, 0x2b, + 0x54, 0x24, 0xe7, 0xe1, 0x73, 0xf6, 0x46, 0x9a, 0x3e, 0x16, 0x4d, 0x26, 0xcf, 0x1d, 0xf1, 0x55, 0x7c, 0x7e, 0x37, + 0x4b, 0x17, 0x9b, 0x6c, 0x0e, 0x52, 0xc1, 0x92, 0x86, 0xba, 0x80, 0xda, 0xd6, 0x62, 0x28, 0xd1, 0x8e, 0xd4, 0xea, + 0x84, 0x2f, 0xa5, 0x80, 0xa5, 0x32, 0x22, 0x67, 0xa8, 0xad, 0xc1, 0xa9, 0xa3, 0x34, 0x71, 0xdd, 0xab, 0x0a, 0xbe, + 0x28, 0xf3, 0xc8, 0x9d, 0x31, 0xfc, 0xd2, 0xc7, 0xeb, 0x90, 0x8c, 0x91, 0x69, 0x36, 0x70, 0x7e, 0x9a, 0x15, 0xeb, + 0x1d, 0x7c, 0x21, 0x74, 0xea, 0xd4, 0x4c, 0xbe, 0x40, 0xdd, 0x0a, 0x4a, 0x32, 0x1c, 0x7c, 0xad, 0x8a, 0x5b, 0xb4, + 0x12, 0xf7, 0x1f, 0x90, 0xf5, 0x49, 0x2b, 0x69, 0xd1, 0x9e, 0x56, 0x56, 0x04, 0xa5, 0x65, 0x52, 0xb5, 0x29, 0x4c, + 0xbf, 0x14, 0x1d, 0xd5, 0xd3, 0xba, 0x7b, 0x3f, 0xe4, 0x76, 0xc9, 0x25, 0xdb, 0x7d, 0x8b, 0x34, 0x34, 0xba, 0xda, + 0x15, 0x80, 0xb4, 0xeb, 0x4d, 0x5f, 0x85, 0xcc, 0x53, 0xd2, 0x94, 0x92, 0x1e, 0x1c, 0xb2, 0x23, 0x34, 0xbf, 0xef, + 0xc6, 0x56, 0x1d, 0xe9, 0x4e, 0x05, 0xfb, 0xce, 0x2f, 0x73, 0xbb, 0x19, 0x9c, 0xc4, 0xe7, 0x36, 0x7e, 0xed, 0x11, + 0x40, 0xb6, 0xa8, 0x84, 0xaf, 0x4d, 0x39, 0x68, 0x97, 0x5f, 0xe2, 0x99, 0x9a, 0x1d, 0x0a, 0xef, 0xf3, 0xd6, 0x69, + 0xba, 0x75, 0x6c, 0x94, 0x4a, 0x1e, 0x7e, 0xa3, 0x42, 0xb6, 0x62, 0x77, 0x56, 0xb8, 0x00, 0x73, 0xfe, 0xaa, 0x20, + 0xea, 0x4a, 0x56, 0xdb, 0x45, 0x8d, 0xc1, 0x06, 0xda, 0x38, 0xd4, 0x2b, 0x44, 0xcc, 0x3b, 0x46, 0x39, 0x42, 0x87, + 0xa4, 0x43, 0x49, 0x27, 0xd3, 0x40, 0x4e, 0xac, 0x3a, 0x24, 0xd8, 0x9f, 0x8e, 0x94, 0x03, 0xf8, 0x9f, 0x4c, 0x91, + 0xe5, 0x9f, 0xea, 0x55, 0xce, 0xd4, 0x29, 0xfe, 0x5c, 0xb2, 0x6b, 0x76, 0x94, 0x5a, 0x4d, 0x35, 0xee, 0x17, 0x4d, + 0x01, 0xa3, 0x52, 0x5e, 0xcb, 0x8e, 0xdc, 0xcc, 0x91, 0x14, 0xff, 0x60, 0xb2, 0xf4, 0xa4, 0x7f, 0x7c, 0xc8, 0xa5, + 0xaf, 0x9c, 0x7b, 0xf5, 0xce, 0x22, 0xa7, 0x2a, 0xdd, 0xfd, 0x34, 0x77, 0x9e, 0xfe, 0xfe, 0x92, 0x9d, 0x1f, 0xfd, + 0xc5, 0x43, 0x74, 0x86, 0xbf, 0x60, 0x43, 0xec, 0xc1, 0xda, 0x65, 0xe1, 0xc9, 0xeb, 0xf3, 0x43, 0xa3, 0x4f, 0x19, + 0x58, 0xf2, 0xee, 0x82, 0x96, 0x40, 0x99, 0xd7, 0x94, 0xa5, 0x5a, 0xdf, 0x17, 0xd3, 0xa7, 0x2b, 0x76, 0xbe, 0x98, + 0x55, 0x5b, 0x6d, 0xdf, 0x97, 0xd5, 0x6d, 0x75, 0xff, 0x72, 0xf6, 0xe1, 0xaf, 0xdb, 0x7b, 0x3e, 0x31, 0x01, 0x08, + 0xec, 0xf4, 0x50, 0xf5, 0x8b, 0x9f, 0xab, 0xb2, 0x98, 0xaa, 0xba, 0x38, 0xab, 0xc6, 0xc5, 0x79, 0x35, 0x3d, 0xfc, + 0x74, 0xc4, 0x0f, 0x3c, 0x12, 0x86, 0xd5, 0x89, 0x06, 0x59, 0x5b, 0xfc, 0xd2, 0xd4, 0x32, 0xcb, 0x27, 0x8a, 0xdd, + 0x4a, 0xad, 0x3f, 0xed, 0xd2, 0xf8, 0xd3, 0x64, 0x79, 0x23, 0x05, 0xbd, 0x52, 0xd1, 0x2e, 0x27, 0xb6, 0xd3, 0x4c, + 0x2c, 0x48, 0x2c, 0x65, 0xa7, 0xbd, 0xb5, 0x0e, 0x39, 0x83, 0x41, 0x6f, 0xbf, 0xe4, 0x1a, 0xcf, 0x22, 0x8c, 0x99, + 0xbc, 0xa1, 0xb7, 0x4c, 0x05, 0x5f, 0xa1, 0x1a, 0x33, 0xeb, 0x3b, 0x51, 0x47, 0x12, 0x0b, 0x82, 0x18, 0xba, 0xd4, + 0x49, 0xed, 0xed, 0xd2, 0xd5, 0xad, 0xab, 0xbe, 0x04, 0x70, 0x2d, 0xd6, 0x94, 0x9e, 0xfa, 0xa2, 0x46, 0x31, 0x3a, + 0x2a, 0x4b, 0x66, 0xaa, 0x84, 0x8a, 0x1e, 0x62, 0x7d, 0xcb, 0xbc, 0xce, 0xca, 0x73, 0x33, 0x4c, 0xd3, 0x2d, 0xcd, + 0x00, 0x5f, 0xd1, 0x85, 0xac, 0xcc, 0x05, 0x6f, 0x29, 0x99, 0xd6, 0x23, 0xe3, 0x54, 0xd3, 0xba, 0x7a, 0x44, 0xf6, + 0xf2, 0x97, 0xb7, 0x40, 0x64, 0x1f, 0xfa, 0xa2, 0xf6, 0x59, 0x94, 0xad, 0x30, 0x89, 0x41, 0xa6, 0x21, 0xe4, 0x28, + 0x0d, 0xd1, 0x88, 0xb3, 0x78, 0xb4, 0xab, 0x20, 0xb1, 0xf1, 0x59, 0x7e, 0xcd, 0x8c, 0xbd, 0x0e, 0x20, 0x16, 0xa8, + 0xb8, 0x2c, 0xbd, 0xe0, 0xff, 0x41, 0x0d, 0xe5, 0xbe, 0xe9, 0x7f, 0xa0, 0x98, 0x14, 0xca, 0xcd, 0xd0, 0x8f, 0x4b, + 0xae, 0x60, 0x13, 0x62, 0xd0, 0x83, 0x15, 0x51, 0x9d, 0xc5, 0xbe, 0x45, 0x9d, 0x40, 0x0a, 0x38, 0x50, 0x9c, 0x41, + 0xe3, 0x44, 0x01, 0x8e, 0x06, 0xad, 0xb5, 0x48, 0x85, 0x50, 0x78, 0x3f, 0xea, 0xaa, 0x75, 0x39, 0xd2, 0xd0, 0x4d, + 0xa4, 0xdf, 0xea, 0xd7, 0x56, 0x94, 0xc1, 0x9c, 0x5f, 0xae, 0xbc, 0xf9, 0xa0, 0xe4, 0xef, 0xdb, 0x3f, 0xa9, 0x0b, + 0x54, 0xf4, 0x0e, 0x1c, 0x46, 0xb4, 0x39, 0x62, 0x6c, 0x61, 0x71, 0x18, 0x5b, 0xea, 0x09, 0xb1, 0xfe, 0x0e, 0x3d, + 0xc2, 0xd9, 0x37, 0x49, 0xad, 0x79, 0x39, 0x99, 0xe5, 0x76, 0x3b, 0xba, 0xdd, 0xf9, 0x99, 0x29, 0xfc, 0xa4, 0xe6, + 0x60, 0x51, 0xef, 0x49, 0xa4, 0x01, 0xba, 0x5e, 0x38, 0x8f, 0xc0, 0xf5, 0x28, 0x49, 0xc1, 0x64, 0x40, 0x13, 0x1a, + 0x3b, 0x62, 0x65, 0xc5, 0x59, 0x1a, 0x8d, 0xce, 0x85, 0xab, 0xa2, 0xfa, 0xfb, 0xcb, 0x62, 0x2e, 0x00, 0x8c, 0x20, + 0xf4, 0xc1, 0x1b, 0xbb, 0x9d, 0x36, 0xbd, 0xda, 0x96, 0x34, 0xc4, 0x11, 0x44, 0x65, 0x41, 0xc5, 0x2e, 0xa8, 0x3a, + 0xda, 0x2f, 0xa8, 0x1c, 0x27, 0xd5, 0x90, 0x9f, 0x7a, 0x65, 0xb9, 0x0b, 0xfe, 0xdc, 0xa3, 0x5a, 0xfd, 0xf3, 0x43, + 0xc3, 0x53, 0xfd, 0x43, 0x98, 0xf7, 0x95, 0xf2, 0x3c, 0x97, 0x7c, 0x6c, 0x12, 0xc9, 0xd5, 0x56, 0x05, 0x1f, 0x1e, + 0x4a, 0x7a, 0x2b, 0x6a, 0x16, 0x58, 0x6f, 0x0f, 0xcf, 0x6b, 0xcf, 0x61, 0xc6, 0x8e, 0xfa, 0x25, 0x51, 0x37, 0x67, + 0xff, 0x0d, 0x06, 0xf6, 0x9b, 0x56, 0x72, 0xae, 0x9b, 0xf5, 0x9e, 0x27, 0xc5, 0x7a, 0x3d, 0xbf, 0xa2, 0x81, 0x8d, + 0x7d, 0xf6, 0x99, 0x3f, 0xa0, 0x61, 0x90, 0x3d, 0x5d, 0x37, 0xe7, 0xb4, 0xce, 0xce, 0xb9, 0x72, 0xd8, 0x69, 0x33, + 0x7e, 0xd2, 0xbd, 0xe5, 0xa0, 0xda, 0x02, 0xf9, 0x9d, 0xfd, 0x84, 0x38, 0x69, 0xf9, 0xf9, 0x69, 0xb4, 0x33, 0x0b, + 0x21, 0x0f, 0xce, 0x76, 0x2b, 0x20, 0xe5, 0x65, 0x76, 0x01, 0x49, 0x73, 0xa1, 0xe7, 0x38, 0x2a, 0x45, 0x82, 0x2f, + 0x03, 0x66, 0xdd, 0x35, 0x02, 0xd3, 0xf5, 0x6e, 0x65, 0xde, 0xc5, 0xaa, 0x06, 0x9d, 0xd7, 0x36, 0x6d, 0xdf, 0x7c, + 0xa5, 0x3b, 0x9e, 0xbe, 0x28, 0x16, 0x3b, 0xac, 0xdc, 0xe5, 0x20, 0x7f, 0xaf, 0x04, 0x1e, 0x05, 0xf0, 0x5e, 0x4c, + 0xd2, 0x4f, 0xf0, 0x74, 0x27, 0x13, 0x98, 0xa8, 0x86, 0xa4, 0x6c, 0x75, 0x77, 0x23, 0x9b, 0x51, 0x35, 0xd0, 0x29, + 0x47, 0x8e, 0x78, 0xf5, 0xb3, 0xf6, 0x98, 0x07, 0x3b, 0xf7, 0xad, 0x17, 0x7e, 0x94, 0x0d, 0x15, 0x96, 0x67, 0x0c, + 0x0d, 0x38, 0x65, 0x58, 0x5c, 0xc6, 0x60, 0x40, 0x6e, 0xae, 0xe3, 0x46, 0x0a, 0xcd, 0x3f, 0x47, 0x3f, 0xa6, 0xa0, + 0x06, 0xea, 0x8d, 0xeb, 0xf1, 0xa1, 0x19, 0xec, 0x97, 0xbf, 0x01, 0x8f, 0x0f, 0x32, 0xa0, 0x9a, 0x85, 0xce, 0x68, + 0xe3, 0x69, 0x9e, 0x7f, 0xd2, 0xb7, 0xb9, 0xe4, 0xfc, 0x47, 0xff, 0x34, 0x1b, 0xa7, 0xce, 0xc9, 0x99, 0x26, 0xc1, + 0x79, 0x0a, 0x5d, 0x9d, 0xfd, 0x7f, 0x97, 0x6c, 0x64, 0x15, 0x2f, 0x9a, 0x47, 0x71, 0x75, 0x81, 0x28, 0xaa, 0xf5, + 0x91, 0x67, 0xed, 0xce, 0x5e, 0xec, 0x7b, 0x38, 0x0c, 0x7a, 0x83, 0x0f, 0x7e, 0xaa, 0xf2, 0x24, 0x66, 0xfd, 0xca, + 0x44, 0xca, 0x25, 0x7e, 0x4a, 0x5d, 0xd9, 0xd7, 0x49, 0xb3, 0x0f, 0x97, 0xa6, 0x34, 0x1c, 0xd8, 0x94, 0x62, 0x8d, + 0x0a, 0xb0, 0x5f, 0x89, 0xd2, 0xb7, 0x76, 0xce, 0xd0, 0x07, 0xff, 0xac, 0x0a, 0x2c, 0x4e, 0xeb, 0x32, 0x40, 0x52, + 0xd7, 0xe3, 0xca, 0x7e, 0x3d, 0x09, 0x88, 0x8b, 0x7c, 0x85, 0x36, 0x47, 0x8c, 0x51, 0x91, 0x0b, 0xd1, 0x41, 0xe6, + 0xaa, 0x62, 0xa2, 0xd6, 0xa7, 0x17, 0xb4, 0xfb, 0x6e, 0x22, 0x2e, 0xd4, 0xd0, 0xf9, 0x57, 0x27, 0x16, 0x94, 0x36, + 0xc7, 0xf6, 0x8e, 0xd0, 0x23, 0x97, 0xf1, 0x11, 0x41, 0x12, 0x5f, 0x4f, 0x61, 0xde, 0x7e, 0xc7, 0x8f, 0xab, 0x08, + 0x20, 0x81, 0x77, 0x8b, 0xb8, 0x19, 0x18, 0x4a, 0x12, 0xa8, 0x9a, 0x5a, 0xeb, 0x01, 0x13, 0xf3, 0x4e, 0x47, 0xe1, + 0x56, 0x54, 0x20, 0xf0, 0x10, 0x99, 0xd8, 0x83, 0x44, 0x56, 0x8f, 0xa2, 0x87, 0x3b, 0xda, 0xe9, 0x4a, 0xa6, 0x68, + 0x04, 0x25, 0xda, 0xf4, 0x90, 0xa4, 0x87, 0x2f, 0x9b, 0x89, 0xde, 0x89, 0x73, 0xd3, 0x1f, 0xf5, 0x5e, 0xcb, 0xfe, + 0x77, 0x5d, 0x47, 0xf6, 0x2e, 0x63, 0x44, 0xcc, 0xe1, 0x51, 0xb6, 0x9e, 0xac, 0x8e, 0xdb, 0x3e, 0xe4, 0xdc, 0x0b, + 0x8a, 0x01, 0x68, 0x6f, 0x0e, 0xdd, 0x77, 0xa5, 0x44, 0xad, 0xeb, 0xd6, 0x43, 0xca, 0x35, 0x12, 0xfd, 0xc5, 0xf7, + 0xe7, 0x77, 0xb5, 0xc9, 0xc9, 0x26, 0x0a, 0x15, 0x4d, 0xf2, 0x18, 0x44, 0x87, 0x97, 0xc6, 0x30, 0xea, 0xc5, 0xc5, + 0x18, 0xb1, 0xa7, 0xd3, 0x28, 0x6e, 0x61, 0x31, 0x5a, 0x65, 0x6f, 0x11, 0x62, 0x5d, 0x3a, 0x35, 0x4c, 0x51, 0xf5, + 0xdf, 0x9f, 0x46, 0xb5, 0x3b, 0x05, 0x11, 0xf8, 0x7a, 0xee, 0x58, 0xb2, 0x0b, 0xa8, 0x97, 0xf3, 0x77, 0xac, 0x68, + 0xd3, 0x69, 0x1f, 0x84, 0x71, 0x8c, 0xcc, 0x7b, 0xf9, 0xb6, 0x08, 0x31, 0x94, 0x12, 0xa4, 0xe0, 0x6b, 0xc7, 0x30, + 0x08, 0x0e, 0xf3, 0xf2, 0x31, 0xb4, 0xff, 0x10, 0xee, 0xc8, 0x8c, 0x31, 0x99, 0xe2, 0xde, 0x00, 0xeb, 0x0d, 0x77, + 0xd8, 0x47, 0x47, 0xbd, 0xd2, 0xe4, 0x4e, 0x12, 0x7b, 0x9a, 0x49, 0x8e, 0xde, 0xed, 0xd2, 0x28, 0x53, 0x3a, 0x7c, + 0x33, 0x89, 0xf8, 0x56, 0x9c, 0x10, 0xa9, 0xba, 0xac, 0xad, 0xae, 0xfd, 0xbe, 0x74, 0x1c, 0xdd, 0xb3, 0x6b, 0xbd, + 0x8f, 0x62, 0x6c, 0xd5, 0x9b, 0x9a, 0x6d, 0xea, 0xa7, 0xa1, 0x40, 0x8e, 0x0e, 0x77, 0xba, 0x95, 0x4c, 0xc7, 0xea, + 0xf2, 0x17, 0x6d, 0x5b, 0xe4, 0x0b, 0x03, 0x98, 0x9e, 0xba, 0xb7, 0x59, 0xed, 0x27, 0x44, 0x89, 0xf4, 0x81, 0x98, + 0x25, 0x3e, 0x4a, 0x01, 0xe3, 0x2b, 0xa7, 0x89, 0x6c, 0xf0, 0xb3, 0xfc, 0x5c, 0xc4, 0xed, 0xae, 0xf1, 0x9c, 0x4f, + 0x00, 0xbd, 0x1f, 0x8f, 0xb3, 0x33, 0x68, 0xe7, 0xdb, 0x74, 0xa6, 0x53, 0x79, 0x31, 0xfd, 0xb3, 0xff, 0xcf, 0xf4, + 0x40, 0xfd, 0x01, 0x24, 0x1a, 0xff, 0xf7, 0x22, 0x93, 0xd7, 0x6a, 0x24, 0x26, 0x07, 0x31, 0xea, 0x1e, 0x14, 0x8b, + 0x68, 0x08, 0xe0, 0x2b, 0x2f, 0x88, 0x1b, 0x1c, 0x1e, 0x15, 0x3e, 0x4d, 0xef, 0x0e, 0xe4, 0x70, 0xa7, 0xe3, 0x49, + 0x5b, 0xdc, 0x57, 0xc9, 0xcd, 0x8c, 0xfd, 0x3e, 0x83, 0x68, 0x18, 0x14, 0x7d, 0x81, 0x41, 0x29, 0xe4, 0xe7, 0x4b, + 0xf1, 0xa5, 0x99, 0xab, 0x2b, 0xa3, 0xa4, 0xb5, 0x82, 0xf5, 0x2a, 0xa4, 0x06, 0x12, 0xef, 0xa5, 0xf0, 0x19, 0xf4, + 0x14, 0x8a, 0xfd, 0xfe, 0xd4, 0x29, 0x27, 0x68, 0x2f, 0xab, 0xd2, 0xa4, 0x57, 0x92, 0xdb, 0x7b, 0x67, 0x1d, 0xfd, + 0x04, 0x28, 0xc7, 0x0f, 0xa2, 0xc5, 0xd7, 0x0e, 0x8b, 0x72, 0xbb, 0x54, 0x75, 0x1c, 0x43, 0xf0, 0xfc, 0xc9, 0xb3, + 0xb0, 0x5d, 0x91, 0x9e, 0xfe, 0x6d, 0xb1, 0xe9, 0xbb, 0x73, 0xab, 0xe1, 0xff, 0xe4, 0xb3, 0x3f, 0xf0, 0x36, 0x3d, + 0xeb, 0xcf, 0xd8, 0x48, 0xe5, 0x5d, 0xc2, 0xe5, 0x36, 0xb1, 0xf9, 0x02, 0x86, 0xe1, 0x71, 0x7b, 0x9e, 0x08, 0x89, + 0xfd, 0xa6, 0x30, 0xb3, 0xc7, 0xb1, 0x68, 0x25, 0xc2, 0xdf, 0xee, 0x46, 0xde, 0xf9, 0x4f, 0x87, 0x25, 0x08, 0xc3, + 0xb9, 0x71, 0xa6, 0xdf, 0x33, 0xda, 0x7f, 0x9a, 0xa7, 0x4f, 0x7f, 0x77, 0xc9, 0xe9, 0x8f, 0xfe, 0x69, 0xf6, 0xbd, + 0x7d, 0x55, 0xa2, 0x77, 0xc0, 0x66, 0xdf, 0x44, 0x8c, 0x9a, 0xbc, 0x9e, 0x53, 0x0e, 0x7a, 0x44, 0x57, 0x33, 0xe1, + 0xe5, 0x09, 0x5c, 0xa0, 0x61, 0x54, 0xe7, 0x3d, 0xcf, 0xc1, 0x0b, 0x65, 0xbb, 0xa3, 0x58, 0x92, 0x68, 0xb3, 0x90, + 0x3b, 0xf4, 0x53, 0x83, 0x28, 0xc1, 0xac, 0xfb, 0x49, 0xb2, 0x47, 0x6d, 0x35, 0x4c, 0xac, 0x52, 0x5d, 0x7c, 0xe7, + 0x5a, 0x26, 0x29, 0xe5, 0x55, 0xbc, 0x53, 0x89, 0xbc, 0xf9, 0x21, 0xcc, 0x98, 0x0d, 0x46, 0x2f, 0x84, 0xb0, 0xdf, + 0x29, 0x02, 0x23, 0x47, 0x15, 0x2c, 0x24, 0x7e, 0xbb, 0x03, 0x24, 0xde, 0xbe, 0x0b, 0xd2, 0x57, 0x12, 0x20, 0x5f, + 0xcb, 0x96, 0x53, 0x9b, 0x9d, 0x1b, 0xe1, 0xb0, 0x47, 0xe9, 0x1b, 0xef, 0x91, 0x6f, 0x64, 0xd2, 0x56, 0xa9, 0x1f, + 0x03, 0xcc, 0xce, 0xd6, 0x61, 0x64, 0xc4, 0x0e, 0xe4, 0x10, 0x53, 0xb1, 0x03, 0x04, 0xb3, 0x0e, 0xfd, 0x1c, 0xf8, + 0x63, 0xd7, 0x0d, 0x40, 0x34, 0x6b, 0x2e, 0x7d, 0x92, 0xb1, 0x9d, 0x1c, 0x8e, 0x4d, 0x04, 0xe3, 0x7d, 0xa9, 0xfb, + 0xac, 0x79, 0x8a, 0x94, 0x6a, 0x89, 0x14, 0x34, 0x20, 0xbd, 0x8a, 0x3b, 0xf7, 0x6c, 0x0e, 0x46, 0x9c, 0xec, 0xef, + 0x4a, 0xa9, 0x3e, 0xdc, 0xb8, 0xcb, 0xa1, 0x71, 0x5e, 0x1e, 0xb0, 0x8b, 0xcd, 0xa0, 0x04, 0xda, 0xe9, 0x34, 0x4f, + 0xd6, 0x1a, 0xcc, 0xb9, 0x26, 0x25, 0x29, 0x0b, 0x9f, 0x90, 0x19, 0xb9, 0xf9, 0xbe, 0xbc, 0xbe, 0xe5, 0xc3, 0x68, + 0x4e, 0x29, 0xd8, 0x2b, 0x7d, 0xd3, 0xa7, 0xfb, 0xba, 0xfc, 0xdc, 0x05, 0xdd, 0xda, 0x41, 0x2b, 0x17, 0x0f, 0xfb, + 0x93, 0x47, 0x02, 0xc8, 0x04, 0xf1, 0xc3, 0x0d, 0xcb, 0xee, 0xbe, 0x4f, 0x60, 0xf6, 0x8d, 0x5f, 0xec, 0xa7, 0x0c, + 0x83, 0x6f, 0xec, 0x66, 0x95, 0x60, 0x39, 0xfc, 0x3f, 0xf7, 0xcf, 0xb6, 0x5e, 0xec, 0x26, 0x87, 0xab, 0xfd, 0xba, + 0x7d, 0x06, 0x18, 0x7b, 0xbf, 0x5c, 0x27, 0x54, 0xc2, 0x48, 0x6d, 0xd1, 0xe4, 0xab, 0xc2, 0x99, 0x3d, 0x9c, 0x4c, + 0xd9, 0x4e, 0xa1, 0x16, 0x69, 0x1c, 0xd7, 0x39, 0x47, 0x5a, 0xa0, 0x8d, 0x65, 0xb1, 0x68, 0x14, 0x09, 0x9d, 0x60, + 0x8b, 0x8d, 0x1c, 0xf7, 0xc3, 0xfa, 0x6c, 0x98, 0xf1, 0x96, 0x28, 0xb4, 0xe0, 0x6c, 0xc4, 0x44, 0x90, 0x51, 0x35, + 0x06, 0xa1, 0x1d, 0x72, 0xb0, 0x00, 0xd5, 0xd0, 0x29, 0x82, 0xe7, 0xc6, 0x9f, 0x16, 0x3f, 0x2e, 0x0c, 0x5e, 0x42, + 0x32, 0x0c, 0x12, 0x40, 0x8a, 0xc9, 0x4a, 0xba, 0x71, 0x6f, 0xb7, 0x70, 0xbc, 0x2f, 0x98, 0x6a, 0xec, 0xa7, 0xdd, + 0xa3, 0x9b, 0x0e, 0xd4, 0x8b, 0x8f, 0x06, 0x86, 0xed, 0x8e, 0x21, 0xf3, 0xca, 0x88, 0xce, 0x44, 0xcf, 0xfb, 0x38, + 0xe9, 0xb1, 0x55, 0x98, 0x23, 0xcc, 0x08, 0xbe, 0x31, 0x99, 0x8d, 0x3c, 0xc2, 0xdd, 0x6e, 0x3f, 0x9a, 0xe3, 0xd8, + 0x1a, 0x7b, 0x85, 0x50, 0xa8, 0x78, 0xcb, 0x74, 0x37, 0xa1, 0x59, 0x87, 0xcd, 0x3d, 0xd4, 0xd9, 0x55, 0x06, 0xfa, + 0x2c, 0xab, 0x04, 0x27, 0xf2, 0xf6, 0xdb, 0xe8, 0x42, 0x03, 0x27, 0x68, 0x6b, 0xa3, 0x87, 0x7f, 0x88, 0xd0, 0xb7, + 0xa0, 0x4e, 0x38, 0x29, 0xdf, 0x19, 0x8f, 0x89, 0x41, 0xd4, 0x38, 0x4e, 0x95, 0x59, 0x4e, 0x4f, 0x76, 0x23, 0x57, + 0x4a, 0xae, 0xb0, 0x9c, 0x59, 0x5a, 0x36, 0x4b, 0x05, 0x78, 0xff, 0x51, 0x17, 0xc7, 0x84, 0x94, 0xab, 0x46, 0x6d, + 0xea, 0x81, 0x86, 0x4f, 0xa3, 0x95, 0x54, 0x56, 0x36, 0xf1, 0x87, 0x1e, 0xee, 0xf4, 0x07, 0xd1, 0xdd, 0x8a, 0x6a, + 0x93, 0xdb, 0xd0, 0x78, 0x42, 0x8f, 0x29, 0xec, 0x83, 0x45, 0xa0, 0xce, 0xa3, 0xf0, 0xf0, 0xf8, 0x3b, 0x26, 0x6f, + 0x24, 0xd1, 0xad, 0xc0, 0xcd, 0xe2, 0x07, 0x2e, 0x58, 0x24, 0x39, 0x5a, 0xc5, 0xd2, 0xbb, 0xd3, 0xb2, 0x35, 0xa9, + 0xfc, 0x84, 0xb6, 0xaf, 0xaf, 0xe5, 0x55, 0x0b, 0xac, 0xc4, 0xec, 0x55, 0x23, 0xf9, 0x45, 0x29, 0x0e, 0xec, 0x80, + 0x69, 0x91, 0x6b, 0x34, 0xcc, 0xd4, 0xb2, 0x79, 0x30, 0xee, 0xe9, 0x36, 0x1c, 0x4a, 0x67, 0x77, 0x7f, 0xa1, 0x09, + 0x0e, 0xa1, 0x29, 0xa9, 0x09, 0x93, 0x7c, 0x3c, 0xb5, 0x71, 0x62, 0x15, 0xb5, 0x60, 0xb2, 0xe5, 0xb8, 0xe5, 0xb5, + 0x3a, 0xa6, 0xea, 0xa5, 0xf7, 0x31, 0x90, 0x24, 0xd3, 0x38, 0xa1, 0x72, 0x70, 0x43, 0xbc, 0x42, 0xc1, 0x69, 0x7b, + 0x1a, 0x27, 0x76, 0x28, 0x6f, 0xff, 0x2a, 0xde, 0x56, 0x68, 0xfe, 0x15, 0x4e, 0xde, 0xcb, 0xf5, 0xbb, 0x6e, 0xb8, + 0x99, 0xd8, 0x0d, 0xbb, 0xfd, 0xab, 0x69, 0xab, 0x54, 0xec, 0xe9, 0xa4, 0xe7, 0x23, 0x1f, 0x00, 0xf8, 0xf3, 0xca, + 0x04, 0xf9, 0x64, 0x98, 0x11, 0xb5, 0x09, 0xc2, 0x4c, 0x65, 0xc4, 0xf8, 0xa6, 0x2a, 0x37, 0xb5, 0x68, 0x45, 0x62, + 0x49, 0x69, 0x1a, 0x67, 0xe7, 0x8e, 0x34, 0x3b, 0xee, 0x8e, 0xd8, 0x6d, 0x89, 0xb9, 0x7e, 0x9a, 0xf4, 0x34, 0x58, + 0x85, 0x22, 0x54, 0x9e, 0x50, 0xae, 0x29, 0x47, 0x7b, 0xd0, 0x8d, 0xba, 0x86, 0x0c, 0x86, 0x54, 0xa1, 0x8c, 0x5e, + 0xec, 0x3c, 0x22, 0x70, 0x54, 0xa1, 0x87, 0x0c, 0xa4, 0xa8, 0x88, 0x66, 0x33, 0x7e, 0x7c, 0xfe, 0x95, 0xa2, 0x2d, + 0xea, 0x06, 0xe1, 0x10, 0x80, 0xac, 0x77, 0x87, 0x43, 0x08, 0x5c, 0xff, 0x0e, 0xcb, 0xd6, 0xa8, 0x51, 0x46, 0x06, + 0x36, 0x64, 0x3d, 0x45, 0xfa, 0x8f, 0x51, 0x5d, 0x91, 0x49, 0xdd, 0xac, 0x50, 0x46, 0x90, 0x41, 0xcc, 0x3b, 0x4a, + 0x9b, 0x6f, 0x86, 0xd1, 0x91, 0x35, 0x8a, 0x30, 0x15, 0xbb, 0x41, 0xe1, 0xaa, 0x3f, 0x48, 0x91, 0x5d, 0x88, 0x38, + 0x05, 0x78, 0x77, 0x6a, 0x48, 0xd4, 0xac, 0xa9, 0x68, 0xf8, 0x18, 0x7a, 0xee, 0xcc, 0xbb, 0x0d, 0x07, 0x12, 0xc2, + 0x22, 0x35, 0xd8, 0x81, 0x68, 0x0b, 0x32, 0x16, 0xe1, 0x8d, 0x48, 0x34, 0xd4, 0x7b, 0x02, 0xf0, 0x6e, 0xdd, 0xa7, + 0xbc, 0x03, 0x80, 0x3e, 0x59, 0x39, 0x91, 0xee, 0x8f, 0x07, 0x72, 0x88, 0xb9, 0xd9, 0x91, 0xba, 0x43, 0x5c, 0x8a, + 0xf3, 0x89, 0x62, 0xbd, 0x20, 0x07, 0x91, 0xa0, 0x15, 0xaf, 0xc9, 0x45, 0x99, 0xb4, 0xf3, 0xae, 0x33, 0xd7, 0xb9, + 0x26, 0x9e, 0xe4, 0xa8, 0x33, 0x51, 0x4c, 0xee, 0x99, 0x7c, 0xad, 0xdb, 0xb0, 0xda, 0x41, 0x9f, 0x10, 0xe3, 0xc9, + 0x58, 0xa6, 0x1e, 0xd9, 0xd9, 0x78, 0x36, 0xe2, 0x50, 0x01, 0x2d, 0x1d, 0xdc, 0x72, 0xd9, 0xac, 0xf9, 0x19, 0x77, + 0xfc, 0xb0, 0x09, 0x1f, 0xad, 0xe2, 0xda, 0xf4, 0xe9, 0x65, 0x90, 0x06, 0xf3, 0xa1, 0xa4, 0xe0, 0x4a, 0xaa, 0xb1, + 0xef, 0x4d, 0x25, 0xb5, 0x7f, 0xb7, 0x99, 0x9a, 0xb5, 0x58, 0xf1, 0x64, 0x5c, 0x04, 0x91, 0xf9, 0xfa, 0xdd, 0xd4, + 0x8c, 0xa3, 0xdd, 0xb4, 0x20, 0x42, 0x5f, 0xe5, 0x62, 0x64, 0x39, 0xfd, 0xa6, 0x89, 0x37, 0x37, 0x84, 0x3e, 0x62, + 0xfa, 0xb3, 0x8d, 0x39, 0x3e, 0x3b, 0xbc, 0x50, 0x43, 0x0f, 0xda, 0x20, 0x22, 0x35, 0x4e, 0x77, 0xb0, 0x48, 0x64, + 0x4b, 0x78, 0x45, 0xd1, 0x8a, 0xb9, 0xfa, 0xe1, 0x90, 0xb1, 0x44, 0x26, 0x88, 0x34, 0xfa, 0xf1, 0xc3, 0x2e, 0x1d, + 0xb6, 0x1e, 0x86, 0xb1, 0x02, 0x5c, 0xe6, 0x25, 0x25, 0x6f, 0xac, 0xe0, 0xb7, 0x9f, 0x03, 0xd3, 0xbc, 0xdf, 0xde, + 0x35, 0xbd, 0x11, 0x2f, 0xd5, 0x8d, 0xd3, 0x3b, 0x14, 0x4a, 0x42, 0x94, 0xd3, 0xc6, 0xc5, 0xc5, 0x9c, 0x3d, 0x0d, + 0x2c, 0xf2, 0x72, 0xc5, 0xd2, 0x2e, 0x7e, 0x0d, 0xa2, 0x61, 0xc5, 0x3b, 0x08, 0xe9, 0x22, 0xbb, 0xce, 0xf0, 0x00, + 0x8d, 0xea, 0xe1, 0x1e, 0x6d, 0xd1, 0x05, 0x04, 0x99, 0x63, 0xf4, 0x68, 0xa0, 0x04, 0x14, 0x7c, 0xc5, 0x09, 0x74, + 0x95, 0xd6, 0xcc, 0xb3, 0x35, 0x32, 0x63, 0x02, 0x84, 0xd3, 0xfa, 0x93, 0x08, 0x2e, 0x21, 0x73, 0xb8, 0x54, 0xd8, + 0x82, 0x8c, 0x5a, 0x29, 0x4e, 0x46, 0x01, 0x4d, 0x9f, 0x88, 0xe3, 0x17, 0xbd, 0x4b, 0x01, 0x38, 0x7a, 0x2c, 0xac, + 0x24, 0xf0, 0x99, 0xc6, 0x15, 0xb3, 0xcb, 0xa0, 0x39, 0xd0, 0xb8, 0xf6, 0xb5, 0xd5, 0x18, 0x8b, 0x8d, 0xd7, 0xdf, + 0x43, 0x84, 0x0d, 0xf6, 0x94, 0x42, 0xac, 0x48, 0x74, 0x80, 0xac, 0x5c, 0x43, 0x27, 0xef, 0xd9, 0xd3, 0xb1, 0xb5, + 0x5c, 0x41, 0x17, 0x3a, 0x92, 0x70, 0xad, 0xc1, 0x66, 0xff, 0x11, 0xe0, 0x4c, 0x43, 0x5a, 0xcf, 0x0c, 0x2b, 0x72, + 0x99, 0x82, 0x1a, 0xf1, 0xaf, 0x53, 0x07, 0x8b, 0x7a, 0x48, 0x17, 0x71, 0x2a, 0xea, 0x99, 0x56, 0x16, 0xe8, 0x84, + 0x3a, 0x52, 0x43, 0x6c, 0x00, 0x05, 0x6f, 0x94, 0x9e, 0x70, 0xfa, 0xdd, 0xa5, 0xe7, 0xa8, 0x2c, 0xb8, 0x0e, 0xcd, + 0xe2, 0x0f, 0x51, 0x6d, 0x3c, 0xfd, 0xf8, 0x60, 0x06, 0x0f, 0xe2, 0xed, 0x59, 0xc0, 0x87, 0x89, 0xb7, 0x63, 0xe7, + 0x79, 0x67, 0x37, 0x01, 0xc1, 0xac, 0x34, 0x11, 0x92, 0x11, 0xe6, 0xce, 0xbd, 0xc3, 0xd6, 0xf8, 0x2b, 0x76, 0x7f, + 0x29, 0x14, 0x06, 0xdb, 0x91, 0x08, 0xf3, 0xb1, 0x18, 0x45, 0xa8, 0xed, 0xe5, 0xd7, 0x2c, 0x19, 0xc9, 0xef, 0xce, + 0x9b, 0x8b, 0xb8, 0x1d, 0xd8, 0xaa, 0x54, 0xa9, 0x1f, 0x10, 0x55, 0xed, 0xf7, 0xb2, 0x61, 0x9b, 0x85, 0x8f, 0x17, + 0x3d, 0x3b, 0xf1, 0xc1, 0x72, 0x3d, 0xc7, 0x92, 0xdf, 0x3f, 0x43, 0x40, 0xcd, 0x66, 0xfb, 0xd5, 0xe2, 0xa0, 0xcf, + 0xb5, 0xf5, 0x1b, 0xb5, 0x81, 0x7e, 0x42, 0x58, 0xe0, 0xfb, 0x79, 0x8d, 0x5c, 0x3c, 0xca, 0xe6, 0xfa, 0x81, 0xdf, + 0x78, 0xb5, 0xc0, 0x3e, 0xbb, 0x33, 0x37, 0x9c, 0x1b, 0xc2, 0xd0, 0xf6, 0x44, 0xe3, 0xfe, 0x89, 0x49, 0x08, 0xaf, + 0xb3, 0x8a, 0x29, 0x9d, 0xc8, 0xac, 0xf2, 0x4f, 0xfa, 0x9d, 0xbb, 0x9b, 0xf9, 0x08, 0x25, 0xda, 0xdf, 0x80, 0xf3, + 0x72, 0xd5, 0x7e, 0x4d, 0xf2, 0x8c, 0x96, 0x1e, 0xb0, 0xa9, 0xa5, 0x9f, 0xeb, 0x95, 0xea, 0x40, 0xe9, 0xbe, 0x03, + 0x09, 0x30, 0x50, 0x87, 0x19, 0xbf, 0x8f, 0xcd, 0x10, 0x6e, 0x4a, 0x30, 0x06, 0x9e, 0xe9, 0x3f, 0x7c, 0x81, 0x83, + 0xb3, 0x92, 0x81, 0x39, 0xa2, 0xe6, 0x15, 0x41, 0xc0, 0xe7, 0x12, 0x54, 0xc8, 0x6e, 0x05, 0xf2, 0xf3, 0xbc, 0x72, + 0xe4, 0x06, 0x90, 0x5b, 0x21, 0xa8, 0xb8, 0x27, 0xcf, 0x5c, 0x1a, 0xd0, 0x03, 0x50, 0xfe, 0xe1, 0x9c, 0x93, 0x84, + 0xfe, 0x26, 0xa0, 0xa8, 0xd1, 0x49, 0x7f, 0xfe, 0xb5, 0x66, 0x64, 0xf2, 0xe7, 0xb1, 0x5f, 0x79, 0xbc, 0xec, 0xe6, + 0x2d, 0xc8, 0x48, 0x1b, 0xdf, 0x86, 0x19, 0x99, 0x81, 0x8e, 0x55, 0x50, 0x5b, 0xf8, 0x42, 0xaa, 0x55, 0x40, 0xae, + 0x2e, 0x42, 0x8b, 0x14, 0xb7, 0x90, 0xd3, 0x9f, 0xb6, 0xb3, 0x90, 0x7f, 0x9a, 0x01, 0x8e, 0x59, 0xf9, 0xcf, 0xc6, + 0x15, 0x45, 0xf6, 0x10, 0x18, 0xcd, 0x8f, 0x2e, 0x15, 0xd4, 0xb4, 0x72, 0x12, 0x7f, 0x02, 0x72, 0x09, 0x12, 0x30, + 0x3e, 0xbf, 0x51, 0x7b, 0xff, 0x9d, 0xce, 0x52, 0x8b, 0xaa, 0x63, 0xa4, 0x9f, 0xfc, 0x1a, 0xf2, 0x1f, 0xe0, 0x47, + 0x1f, 0x91, 0xd2, 0xd9, 0x3c, 0x5b, 0xfd, 0x89, 0xab, 0xd8, 0x65, 0x41, 0x75, 0x02, 0x2a, 0x48, 0x58, 0x05, 0xb5, + 0x06, 0x23, 0xfb, 0x1f, 0x16, 0xae, 0x46, 0x4c, 0xf3, 0xa7, 0x5b, 0xb4, 0x1a, 0xba, 0x57, 0xa0, 0xea, 0x70, 0x03, + 0x44, 0x0e, 0xdd, 0xa3, 0xea, 0x62, 0xc7, 0x99, 0xfe, 0x5b, 0x09, 0xd8, 0x38, 0x73, 0x82, 0xd3, 0xfd, 0x87, 0x97, + 0x2f, 0xd6, 0xf6, 0xa4, 0x5f, 0x32, 0xc3, 0xf8, 0x92, 0xba, 0x78, 0x70, 0x5f, 0xd3, 0xe2, 0x5b, 0xc2, 0xe4, 0xd3, + 0xfc, 0xf3, 0x49, 0xff, 0xea, 0x4b, 0xfe, 0xfc, 0xe8, 0x17, 0xbe, 0x95, 0xaf, 0x79, 0xf6, 0x4d, 0x5a, 0xa3, 0x1d, + 0xf6, 0x7a, 0x88, 0xbb, 0x37, 0xfd, 0xa1, 0x0e, 0xf9, 0x5a, 0xc5, 0xf8, 0xaf, 0x9e, 0xe9, 0xd3, 0x1f, 0x1e, 0x1f, + 0xdc, 0xa4, 0x77, 0x09, 0x39, 0xcd, 0x94, 0x57, 0xe7, 0xd6, 0xbe, 0xc1, 0x12, 0xb6, 0xf5, 0x26, 0xc1, 0xde, 0xa0, + 0x20, 0xd2, 0x48, 0xbb, 0x13, 0x21, 0x02, 0x95, 0x41, 0xae, 0x60, 0xc8, 0xcd, 0x71, 0xd4, 0xf0, 0x3f, 0x71, 0xc0, + 0x28, 0x97, 0x11, 0x55, 0xa5, 0x8a, 0xd3, 0xd1, 0xc1, 0x4c, 0xc0, 0x29, 0x44, 0x18, 0x21, 0xf9, 0x5e, 0xcd, 0x62, + 0x81, 0xce, 0x24, 0x0d, 0x3e, 0x7e, 0x27, 0x1d, 0x4b, 0x56, 0x5c, 0x5b, 0xe6, 0xeb, 0xfd, 0x27, 0xd9, 0x58, 0xf9, + 0x28, 0x90, 0x59, 0x79, 0x87, 0x02, 0xd5, 0x21, 0x05, 0x93, 0x8b, 0xd4, 0xf9, 0x88, 0x99, 0xf3, 0x91, 0x4a, 0x2f, + 0xd8, 0xaf, 0xe6, 0x06, 0xda, 0x8d, 0x3d, 0x1c, 0xec, 0x5b, 0x65, 0x6c, 0xc2, 0x90, 0xe4, 0x26, 0xbf, 0x46, 0x06, + 0xe5, 0xe4, 0xa6, 0x0d, 0x5b, 0xe0, 0x9b, 0x5f, 0x9f, 0xa1, 0x49, 0x0a, 0x9d, 0x8d, 0x7c, 0xcf, 0xc8, 0x83, 0xeb, + 0xfb, 0xb3, 0xd7, 0xfe, 0xd1, 0x94, 0x45, 0x13, 0xd6, 0x6e, 0xa9, 0x7d, 0x42, 0x28, 0x05, 0x2a, 0x08, 0x10, 0xa6, + 0xc2, 0x1a, 0x58, 0xd6, 0x21, 0x35, 0x87, 0x9a, 0xae, 0x3f, 0x67, 0x90, 0x23, 0xb5, 0xc3, 0xc4, 0xbe, 0x0d, 0x03, + 0x5f, 0x2b, 0xa5, 0xb7, 0x37, 0x50, 0xa5, 0x16, 0xf6, 0x59, 0x64, 0xa8, 0x33, 0x39, 0x57, 0x1c, 0x81, 0xd7, 0x2d, + 0x35, 0x33, 0x51, 0xe8, 0x2c, 0x1b, 0x69, 0x7e, 0x4a, 0x78, 0x45, 0x7f, 0x55, 0x04, 0x4c, 0x74, 0xd0, 0x99, 0xdc, + 0x9a, 0x8a, 0x02, 0x93, 0x90, 0xaa, 0xba, 0x62, 0xeb, 0x78, 0x0a, 0x84, 0x9f, 0xa7, 0x88, 0xed, 0x1a, 0x9f, 0x87, + 0xa2, 0x3c, 0xc9, 0xfb, 0x34, 0x77, 0x7d, 0xe8, 0x9c, 0x6b, 0x03, 0x91, 0x6c, 0x46, 0x74, 0xe1, 0x87, 0xd7, 0x54, + 0xa7, 0xc5, 0x6d, 0x4b, 0xf7, 0x69, 0x5e, 0x7c, 0xd2, 0xac, 0x4b, 0x2e, 0x7e, 0xf4, 0x97, 0x6c, 0x9b, 0x65, 0x08, + 0x45, 0x2a, 0x53, 0xf0, 0x6a, 0x9f, 0x2f, 0x8a, 0xc9, 0xf6, 0x7b, 0x58, 0xf2, 0xc4, 0x97, 0x41, 0x83, 0x89, 0x7e, + 0x71, 0xe7, 0x11, 0x1c, 0xaf, 0xba, 0xc8, 0x6a, 0x0e, 0x9c, 0xeb, 0x7a, 0x36, 0xe6, 0xb2, 0x35, 0x2e, 0x34, 0x42, + 0xa2, 0xae, 0x1a, 0x79, 0xd9, 0xbb, 0x80, 0x0c, 0x23, 0x29, 0x7b, 0x20, 0xc0, 0x9c, 0x5f, 0x5b, 0x46, 0xc3, 0xb3, + 0x90, 0x7c, 0xdd, 0x74, 0xba, 0xa0, 0x21, 0x54, 0x40, 0x83, 0x9f, 0xbf, 0x97, 0xd0, 0x9e, 0x0a, 0x7b, 0x7d, 0xfa, + 0x0b, 0xcf, 0x4c, 0x5a, 0x51, 0xc6, 0x33, 0x7d, 0x16, 0x4b, 0x9a, 0x27, 0x9d, 0xb1, 0x25, 0xcf, 0xfb, 0x58, 0xbe, + 0x4f, 0xe5, 0x58, 0xee, 0xee, 0x69, 0xba, 0xe4, 0x24, 0x35, 0xc7, 0x4a, 0x67, 0x42, 0x6d, 0x7c, 0x99, 0x4f, 0x22, + 0x12, 0x37, 0x78, 0x8a, 0x81, 0x58, 0xcf, 0x7d, 0x3a, 0x18, 0x4e, 0x15, 0xcd, 0xb7, 0xa7, 0xbb, 0x55, 0xe9, 0x9b, + 0x4d, 0xb5, 0x08, 0x71, 0x79, 0xc8, 0x62, 0xe2, 0xc3, 0x40, 0xd9, 0xd9, 0xa6, 0x8d, 0x9b, 0x04, 0x0f, 0xa4, 0xce, + 0xe5, 0xf4, 0x60, 0xb8, 0x88, 0xbd, 0xce, 0x3c, 0xa4, 0x57, 0x5c, 0xdc, 0x05, 0xe2, 0xbc, 0x42, 0x38, 0xa8, 0x57, + 0x8c, 0x6b, 0xf9, 0xa6, 0xd9, 0xbf, 0x9c, 0x4a, 0xe2, 0x92, 0x87, 0x6b, 0xd0, 0x4a, 0x35, 0x6b, 0x9d, 0x62, 0xab, + 0xa3, 0xf5, 0xf0, 0xdf, 0x37, 0x88, 0xac, 0xd8, 0x7c, 0xe1, 0x5b, 0xf9, 0xca, 0x76, 0x41, 0xc8, 0xec, 0x2f, 0xc7, + 0x17, 0x68, 0x3f, 0xcb, 0xd6, 0xda, 0x8b, 0xd3, 0xee, 0x74, 0xe3, 0x2e, 0xaf, 0x0f, 0xdb, 0x60, 0x7c, 0x85, 0x0e, + 0xdb, 0x05, 0x99, 0x7e, 0x62, 0xbd, 0xbe, 0xa7, 0x12, 0xfe, 0xe1, 0xfa, 0x87, 0xdf, 0xf4, 0xb9, 0x3f, 0xe6, 0x2a, + 0xe2, 0x00, 0x99, 0x97, 0xd4, 0x86, 0x71, 0xcd, 0x62, 0x2f, 0xe8, 0x56, 0x42, 0x7d, 0x6e, 0x9f, 0x01, 0x07, 0x37, + 0x37, 0xbd, 0xa7, 0x56, 0x03, 0x80, 0x45, 0x1c, 0x5d, 0xc3, 0x8e, 0x27, 0xe0, 0x13, 0x4a, 0x05, 0x61, 0x8f, 0x63, + 0x54, 0x29, 0x5d, 0xaa, 0x47, 0x1d, 0x3f, 0x0f, 0xa3, 0x3a, 0x10, 0x20, 0xe0, 0xf1, 0x98, 0xc7, 0x82, 0x44, 0x0d, + 0xea, 0x3c, 0x9a, 0xf2, 0x0a, 0x3e, 0x44, 0x02, 0xf6, 0x5d, 0xaf, 0xef, 0xc6, 0x37, 0xc3, 0x2b, 0x02, 0x5b, 0xf8, + 0x25, 0x8d, 0x6c, 0x23, 0x34, 0x8a, 0x47, 0xb9, 0x75, 0x4d, 0xf4, 0x45, 0x6d, 0xc7, 0xcc, 0x0b, 0x41, 0x56, 0x4f, + 0x78, 0x06, 0x0b, 0xe5, 0x82, 0xe0, 0x0b, 0xab, 0x80, 0xfb, 0x73, 0xa2, 0x1f, 0x83, 0x94, 0x1e, 0x8a, 0xe8, 0x88, + 0xd6, 0x91, 0xa9, 0xc1, 0x71, 0x8f, 0x65, 0x89, 0xe1, 0x3c, 0x42, 0xb0, 0xdb, 0x96, 0x35, 0x22, 0xab, 0xd5, 0x08, + 0x7e, 0xf3, 0x52, 0xd1, 0x3a, 0xa4, 0x24, 0x85, 0x0a, 0xd6, 0xd4, 0xf4, 0x5a, 0x10, 0xa9, 0x45, 0xe7, 0x7f, 0x02, + 0xc4, 0x69, 0x4f, 0x34, 0xad, 0xf6, 0x9c, 0x5a, 0x54, 0x1c, 0xda, 0x46, 0xc2, 0xdc, 0xa5, 0xc0, 0x95, 0x38, 0x70, + 0x00, 0xb1, 0xf4, 0xae, 0x48, 0xe4, 0x3d, 0xb4, 0x3f, 0xb8, 0x42, 0x9a, 0x4e, 0x8d, 0x77, 0x72, 0xca, 0x0d, 0x52, + 0x75, 0x61, 0xe4, 0x34, 0x12, 0x93, 0x2a, 0x27, 0x8c, 0x50, 0xc5, 0xed, 0x5a, 0x2d, 0xe1, 0xd4, 0x1b, 0xb7, 0x03, + 0x4f, 0x01, 0xef, 0x92, 0x21, 0x6c, 0xaf, 0x35, 0xe2, 0xcc, 0x18, 0xba, 0x7c, 0xf3, 0x9f, 0xba, 0x9d, 0x53, 0xfb, + 0x65, 0x70, 0x45, 0x87, 0x81, 0xaf, 0xc6, 0xab, 0x30, 0x79, 0x4a, 0x61, 0x5a, 0xfd, 0xa5, 0xeb, 0x33, 0x18, 0xf2, + 0x27, 0xf9, 0x4c, 0x43, 0x22, 0x48, 0xf1, 0x36, 0x7c, 0x78, 0x3f, 0xda, 0x06, 0xe4, 0x21, 0x70, 0x98, 0x8f, 0xc1, + 0xef, 0x44, 0xf6, 0x41, 0x6b, 0x44, 0x77, 0x8a, 0xb0, 0x20, 0x35, 0x77, 0xf8, 0xe8, 0x90, 0x6f, 0x1e, 0xea, 0x91, + 0x5c, 0x5e, 0x83, 0x00, 0x0a, 0x56, 0xd3, 0xc2, 0x9e, 0x3e, 0xb7, 0x79, 0xc6, 0x7b, 0xd0, 0x44, 0x47, 0xe1, 0x10, + 0x13, 0x3c, 0xe7, 0x0c, 0xed, 0x68, 0x27, 0x87, 0xe1, 0x31, 0xf4, 0x4a, 0x61, 0xee, 0x3f, 0x23, 0x72, 0xc3, 0xf9, + 0xb9, 0x9e, 0x31, 0x8d, 0x72, 0x9e, 0xb2, 0xaf, 0x57, 0x8d, 0x1e, 0xff, 0xb1, 0x03, 0x70, 0xff, 0xf4, 0xd7, 0x84, + 0xe4, 0x4f, 0x75, 0x0a, 0xdf, 0x57, 0x96, 0x84, 0xb7, 0x02, 0xff, 0x06, 0xaf, 0x59, 0x62, 0x70, 0x98, 0x82, 0x42, + 0xf9, 0x6b, 0x0b, 0x42, 0x6e, 0x73, 0x72, 0x6d, 0x0e, 0x97, 0xcf, 0x99, 0xe4, 0x0b, 0xb6, 0x09, 0xb5, 0x3e, 0x2b, + 0x70, 0xf0, 0xa6, 0xc9, 0x72, 0x3a, 0x8e, 0x9c, 0xf9, 0xad, 0xd8, 0x5c, 0x37, 0x26, 0x79, 0x14, 0x29, 0xfa, 0xcd, + 0xf4, 0x46, 0xde, 0x78, 0xb3, 0x10, 0x6d, 0x87, 0x5e, 0x9a, 0xd6, 0x8f, 0x2f, 0x08, 0x3f, 0x0d, 0xcb, 0x89, 0xd9, + 0x1f, 0x7c, 0x2f, 0xb0, 0xba, 0xc4, 0xc5, 0x80, 0x0c, 0xc3, 0xee, 0x58, 0xb0, 0x0e, 0x57, 0xd7, 0x68, 0xca, 0xb8, + 0x1c, 0xa4, 0x8a, 0x96, 0xee, 0x08, 0xa1, 0x9b, 0xb8, 0x28, 0xed, 0x4c, 0xd9, 0x7b, 0xf9, 0x3b, 0xb4, 0xfa, 0xb5, + 0x2a, 0xde, 0x5d, 0x12, 0x3e, 0xf8, 0xee, 0x5d, 0xd0, 0xdf, 0x74, 0xc8, 0xc6, 0xba, 0x5f, 0x3e, 0xbe, 0x54, 0x4d, + 0x16, 0x46, 0x83, 0x99, 0x4f, 0x79, 0x73, 0x76, 0x57, 0x65, 0x94, 0xd4, 0x35, 0x14, 0x46, 0x62, 0x8f, 0x1c, 0xe7, + 0xbd, 0x33, 0x59, 0xd7, 0xbb, 0x8e, 0x55, 0xe9, 0xf2, 0xb3, 0x04, 0x8b, 0xd6, 0x72, 0xef, 0xfe, 0x2c, 0xd5, 0xa7, + 0x50, 0x03, 0x69, 0xb3, 0x81, 0x0e, 0xdd, 0x46, 0x9b, 0x68, 0x9c, 0x49, 0xa0, 0xb4, 0x87, 0x2b, 0x2f, 0x6a, 0xfa, + 0x2c, 0x26, 0xd0, 0xba, 0x9d, 0x2d, 0x74, 0xb6, 0x0b, 0x4a, 0x83, 0xdb, 0x3f, 0xee, 0x76, 0xe9, 0xcc, 0xe0, 0xe3, + 0xfd, 0x83, 0x0c, 0xcb, 0xff, 0x1b, 0x55, 0xec, 0x9e, 0x1c, 0x80, 0x86, 0x35, 0x6f, 0x9b, 0x44, 0x44, 0x48, 0x58, + 0xdc, 0x7c, 0x72, 0xec, 0xfb, 0xc6, 0x97, 0xe8, 0xb9, 0xa1, 0x27, 0xe3, 0xc4, 0xf5, 0x52, 0x9d, 0xb2, 0x1e, 0x89, + 0x01, 0x7f, 0xd2, 0x39, 0x90, 0x68, 0x6b, 0x9a, 0xdd, 0x0e, 0xca, 0x81, 0xdd, 0x9b, 0x03, 0xeb, 0x8f, 0xf9, 0x06, + 0x23, 0x07, 0x2b, 0x9b, 0x3f, 0xb5, 0xb9, 0xed, 0xb4, 0x0e, 0x9f, 0x4d, 0xc6, 0xd2, 0xe3, 0xe1, 0x2b, 0xab, 0x23, + 0xb4, 0x35, 0x92, 0x15, 0x83, 0x6a, 0x6f, 0xf7, 0x63, 0x0f, 0x22, 0x7e, 0xa6, 0xee, 0xde, 0x45, 0xdd, 0xa1, 0xa5, + 0x67, 0xf6, 0xf6, 0xe0, 0xb1, 0x7f, 0x60, 0x8d, 0x43, 0xdd, 0xcb, 0x05, 0x08, 0x4b, 0xdc, 0x51, 0xd6, 0x56, 0x71, + 0x71, 0xfb, 0xe7, 0xd7, 0x0f, 0x9a, 0x83, 0x40, 0xe5, 0x70, 0x30, 0xd1, 0x8b, 0x11, 0xeb, 0xc8, 0xb1, 0x63, 0x18, + 0x23, 0x76, 0x73, 0x80, 0x94, 0x11, 0x23, 0xcd, 0x29, 0xdf, 0x07, 0x63, 0x5c, 0xf4, 0x46, 0xed, 0xc2, 0x86, 0x79, + 0x80, 0x15, 0xee, 0xa4, 0xaa, 0xc3, 0xc2, 0xc4, 0xfc, 0xba, 0xb5, 0x49, 0x72, 0xde, 0x91, 0xf5, 0xa9, 0xd9, 0xbb, + 0x12, 0x84, 0x3e, 0x1f, 0xfe, 0x8d, 0x8a, 0x78, 0xae, 0xb3, 0x87, 0x60, 0x02, 0x7e, 0xac, 0x3a, 0xec, 0x6f, 0xc1, + 0xa7, 0x0d, 0x27, 0xea, 0xe8, 0x93, 0xd1, 0x59, 0xe1, 0x80, 0x5d, 0x6b, 0xfa, 0x50, 0xc6, 0x43, 0x8f, 0x59, 0x18, + 0x2b, 0xd3, 0x5b, 0x15, 0x94, 0x0d, 0x9b, 0xa9, 0x2e, 0xa9, 0x06, 0xaa, 0x4c, 0x26, 0x99, 0x4c, 0xd9, 0x42, 0xce, + 0x00, 0xb6, 0xf7, 0x41, 0x72, 0x85, 0x88, 0x7a, 0x5f, 0x5a, 0x8f, 0xcc, 0x22, 0xae, 0x91, 0x23, 0xda, 0x63, 0x50, + 0x8b, 0x88, 0x77, 0x6a, 0x75, 0x94, 0xe4, 0xa3, 0x2f, 0x1f, 0x82, 0xd0, 0xb5, 0xa4, 0x3f, 0x9b, 0xa1, 0x84, 0x65, + 0x46, 0x2e, 0xdb, 0x4f, 0xdc, 0xbd, 0x3f, 0x8d, 0x7f, 0x9a, 0x08, 0x6d, 0x97, 0x67, 0xeb, 0xc1, 0xc8, 0xb5, 0x34, + 0x95, 0xd7, 0xb8, 0xa5, 0xc6, 0xb8, 0xe0, 0xa7, 0x38, 0xd2, 0xe6, 0x6b, 0xcd, 0xd3, 0x43, 0xdd, 0x7a, 0x1e, 0xb5, + 0x0f, 0xb2, 0xb6, 0x0e, 0xec, 0xc5, 0x42, 0x7b, 0x0a, 0x7b, 0xe7, 0xf8, 0xd0, 0xfd, 0xc5, 0xad, 0xcb, 0x4d, 0x95, + 0x8f, 0xce, 0x5c, 0x48, 0x64, 0x8e, 0x8a, 0xb7, 0x38, 0xc8, 0x07, 0xa0, 0x22, 0x92, 0xe1, 0xbd, 0x5b, 0x1e, 0x36, + 0xcf, 0xba, 0x47, 0x3d, 0xf6, 0xa0, 0x8c, 0x84, 0x8f, 0x77, 0x08, 0x89, 0x52, 0x21, 0xf6, 0xfc, 0x67, 0x92, 0x72, + 0x16, 0x0d, 0x95, 0xb7, 0x65, 0xe5, 0xf4, 0xf5, 0x3c, 0x92, 0x6a, 0x19, 0x0f, 0x78, 0x4f, 0x6e, 0xb6, 0x96, 0x13, + 0xc5, 0xad, 0xbe, 0xda, 0x5c, 0x82, 0xa0, 0x6c, 0xf4, 0x86, 0xdb, 0xb7, 0x11, 0x3b, 0x4e, 0xa0, 0x6d, 0xdb, 0x9f, + 0x5c, 0x2c, 0x45, 0xa9, 0x70, 0xc2, 0x58, 0x37, 0x39, 0x8a, 0xe6, 0x10, 0x86, 0x37, 0x6b, 0xab, 0x09, 0x1f, 0x70, + 0xc3, 0x31, 0x6f, 0x6f, 0x29, 0x87, 0x55, 0x2d, 0x9c, 0xa3, 0x48, 0xc6, 0xc4, 0xde, 0x2e, 0xa3, 0xdb, 0x5b, 0x85, + 0xfe, 0x13, 0xb2, 0xeb, 0xac, 0x56, 0xde, 0x04, 0x5f, 0x29, 0x88, 0x6c, 0xee, 0xc7, 0x67, 0xc6, 0x01, 0xd2, 0x0d, + 0xf0, 0xd7, 0x0a, 0x92, 0x55, 0x9e, 0xa8, 0xbc, 0x0a, 0x4c, 0xd3, 0x90, 0x82, 0xe1, 0x53, 0x7a, 0x0f, 0x96, 0xbc, + 0xe6, 0xcb, 0x66, 0xd7, 0x37, 0x17, 0x3f, 0xac, 0xf5, 0x10, 0x2f, 0x3b, 0xbd, 0xb5, 0x2a, 0x9c, 0xe0, 0x31, 0x49, + 0xfc, 0xba, 0xf4, 0xb3, 0xfd, 0x60, 0xe3, 0x96, 0x42, 0xed, 0x07, 0x9c, 0xd9, 0xba, 0xe7, 0x30, 0xb3, 0x49, 0x9f, + 0x01, 0x12, 0x16, 0x68, 0xdd, 0xc7, 0x22, 0x53, 0x60, 0xab, 0x01, 0x6e, 0x00, 0x23, 0xb6, 0x7d, 0xc8, 0x1e, 0xbd, + 0x29, 0x92, 0x2d, 0xe4, 0x7b, 0x3a, 0x72, 0xfb, 0x53, 0x4c, 0xef, 0x17, 0x75, 0x20, 0x9a, 0xaf, 0x03, 0x6e, 0xeb, + 0x81, 0x77, 0x1c, 0xa4, 0x48, 0x5c, 0x21, 0xa6, 0x49, 0xf7, 0x15, 0x5a, 0xb5, 0xba, 0x9b, 0x5c, 0xf6, 0xe7, 0x8e, + 0x93, 0xb5, 0xde, 0x86, 0xbb, 0xd8, 0xcf, 0xaa, 0x1d, 0xd2, 0x51, 0x03, 0xf8, 0xd2, 0xaf, 0x0c, 0x74, 0x7a, 0x9a, + 0xc2, 0x77, 0x25, 0x96, 0x4d, 0x08, 0x98, 0x3b, 0x28, 0xec, 0x2c, 0x90, 0x04, 0x2b, 0x9c, 0x38, 0x96, 0x77, 0x58, + 0x93, 0x17, 0xfa, 0x7a, 0x1c, 0x19, 0x18, 0x98, 0xb2, 0x27, 0x11, 0x61, 0xef, 0x2c, 0x52, 0x34, 0x6b, 0x19, 0xde, + 0x32, 0xd1, 0x93, 0x0f}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR From 37a0cec53dc0e4e9d9c0c5848e220b6d8480964d Mon Sep 17 00:00:00 2001 From: Szpadel Date: Wed, 25 Feb 2026 17:12:03 +0100 Subject: [PATCH 0195/2056] [ac_dimmer] Use a shared ESP32 GPTimer for multiple dimmers (#13523) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 1e850a18fe1..f731a8c7537 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -199,12 +199,19 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); - timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); - // For ESP32, we can't use dynamic interval calculation because the timerX functions - // are not callable from ISR (placed in flash storage). - // Here we just use an interrupt firing every 50 µs. - timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + if (dimmer_timer == nullptr) { + dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); + if (dimmer_timer == nullptr) { + ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer"); + this->mark_failed(); + return; + } + timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); + // For ESP32, we can't use dynamic interval calculation because the timerX functions + // are not callable from ISR (placed in flash storage). + // Here we just use an interrupt firing every 50 µs. + timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + } #endif } From ede8235aaee8467be33dd33fd8c25c39957063e3 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 25 Feb 2026 17:46:28 +0100 Subject: [PATCH 0196/2056] [core] more accurate check for leap year and valid day_of_month (#14197) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/time.cpp | 2 +- esphome/core/time.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 554431c631a..1aea18ac8d3 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -8,7 +8,7 @@ namespace esphome { uint8_t days_in_month(uint8_t month, uint16_t year) { static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2 && (year % 4 == 0)) + if (month == 2 && (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)) return 29; return DAYS_IN_MONTH[month]; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 87ebb5c2213..d9ce86131c3 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -70,14 +70,14 @@ struct ESPTime { /// @copydoc strftime(const std::string &format) std::string strftime(const char *format); - /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } /// Check if all time fields of this ESPTime are in range. bool fields_in_range() const { return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && - this->day_of_year < 367 && this->month > 0 && this->month < 13; + this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && + this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); } /** Convert a string to ESPTime struct as specified by the format argument. From 8bb577de644243c3822305d3720ddaec8b9864c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 12:23:13 -0600 Subject: [PATCH 0197/2056] [api] Split ProtoVarInt::parse into 32-bit and 64-bit phases (#14039) --- esphome/components/api/api_pb2.h | 1 - esphome/components/api/api_pb2_defines.h | 12 ++ esphome/components/api/proto.cpp | 17 +++ esphome/components/api/proto.h | 70 ++++++---- esphome/core/defines.h | 1 + script/api_protobuf/api_protobuf.py | 65 +++++++++- .../fixtures/varint_five_byte_device_id.yaml | 47 +++++++ .../test_varint_five_byte_device_id.py | 120 ++++++++++++++++++ 8 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 esphome/components/api/api_pb2_defines.h create mode 100644 tests/integration/fixtures/varint_five_byte_device_id.yaml create mode 100644 tests/integration/test_varint_five_byte_device_id.py diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c90873d9931..c2675cefe46 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2,7 +2,6 @@ // See script/api_protobuf/api_protobuf.py #pragma once -#include "esphome/core/defines.h" #include "esphome/core/string_ref.h" #include "proto.h" diff --git a/esphome/components/api/api_pb2_defines.h b/esphome/components/api/api_pb2_defines.h new file mode 100644 index 00000000000..8ebd60fb5d1 --- /dev/null +++ b/esphome/components/api/api_pb2_defines.h @@ -0,0 +1,12 @@ +// This file was automatically generated with a tool. +// See script/api_protobuf/api_protobuf.py +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BLUETOOTH_PROXY +#ifndef USE_API_VARINT64 +#define USE_API_VARINT64 +#endif +#endif + +namespace esphome::api {} // namespace esphome::api diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 73a3bab12a2..a252907fd7a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -7,6 +7,23 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +#ifdef USE_API_VARINT64 +optional ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, + uint32_t result32) { + uint64_t result64 = result32; + uint32_t limit = std::min(len, uint32_t(10)); + for (uint32_t i = 4; i < limit; i++) { + uint8_t val = buffer[i]; + result64 |= uint64_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + *consumed = i + 1; + return ProtoVarInt(result64); + } + } + return {}; +} +#endif + uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) { uint32_t count = 0; const uint8_t *ptr = buffer; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 4522fc96655..c34f7744e6b 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,5 +1,6 @@ #pragma once +#include "api_pb2_defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -110,59 +111,78 @@ class ProtoVarInt { #endif if (len == 0) return {}; - - // Most common case: single-byte varint (values 0-127) + // Fast path: single-byte varints (0-127) are the most common case + // (booleans, small enums, field tags). Avoid loop overhead entirely. if ((buffer[0] & 0x80) == 0) { *consumed = 1; return ProtoVarInt(buffer[0]); } - - // General case for multi-byte varints - // Since we know buffer[0]'s high bit is set, initialize with its value - uint64_t result = buffer[0] & 0x7F; - uint8_t bitpos = 7; - - // A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings - // to avoid undefined behavior from shifting uint64_t by >= 64 bits. - uint32_t max_len = std::min(len, uint32_t(10)); - - // Start from the second byte since we've already processed the first - for (uint32_t i = 1; i < max_len; i++) { + // 32-bit phase: process remaining bytes with native 32-bit shifts. + // Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t + // shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. + // With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles + // byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). + uint32_t result32 = buffer[0] & 0x7F; +#ifdef USE_API_VARINT64 + uint32_t limit = std::min(len, uint32_t(4)); +#else + uint32_t limit = std::min(len, uint32_t(5)); +#endif + for (uint32_t i = 1; i < limit; i++) { uint8_t val = buffer[i]; - result |= uint64_t(val & 0x7F) << uint64_t(bitpos); - bitpos += 7; + result32 |= uint32_t(val & 0x7F) << (i * 7); if ((val & 0x80) == 0) { *consumed = i + 1; - return ProtoVarInt(result); + return ProtoVarInt(result32); } } - - return {}; // Incomplete or invalid varint + // 64-bit phase for remaining bytes (BLE addresses etc.) +#ifdef USE_API_VARINT64 + return parse_wide(buffer, len, consumed, result32); +#else + return {}; +#endif } +#ifdef USE_API_VARINT64 + protected: + /// Continue parsing varint bytes 4-9 with 64-bit arithmetic. + /// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path. + static optional parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) + __attribute__((noinline)); + + public: +#endif + constexpr uint16_t as_uint16() const { return this->value_; } constexpr uint32_t as_uint32() const { return this->value_; } - constexpr uint64_t as_uint64() const { return this->value_; } constexpr bool as_bool() const { return this->value_; } constexpr int32_t as_int32() const { // Not ZigZag encoded - return static_cast(this->as_int64()); - } - constexpr int64_t as_int64() const { - // Not ZigZag encoded - return static_cast(this->value_); + return static_cast(this->value_); } constexpr int32_t as_sint32() const { // with ZigZag encoding return decode_zigzag32(static_cast(this->value_)); } +#ifdef USE_API_VARINT64 + constexpr uint64_t as_uint64() const { return this->value_; } + constexpr int64_t as_int64() const { + // Not ZigZag encoded + return static_cast(this->value_); + } constexpr int64_t as_sint64() const { // with ZigZag encoding return decode_zigzag64(this->value_); } +#endif protected: +#ifdef USE_API_VARINT64 uint64_t value_; +#else + uint32_t value_; +#endif }; // Forward declarations for decode_to_message, encode_message and encode_packed_sint32 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a1e3d1707fb..673aa246fe4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -144,6 +144,7 @@ #define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE +#define USE_API_VARINT64 #define USE_API_PLAINTEXT #define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index cc881caa5cf..350947a8d69 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1913,6 +1913,37 @@ def build_type_usage_map( ) +def get_varint64_ifdef( + file_desc: descriptor.FileDescriptorProto, + message_ifdef_map: dict[str, str | None], +) -> tuple[bool, str | None]: + """Check if 64-bit varint fields exist and get their common ifdef guard. + + Returns: + (has_varint64, ifdef_guard) - has_varint64 is True if any fields exist, + ifdef_guard is the common guard or None if unconditional. + """ + varint64_types = { + FieldDescriptorProto.TYPE_INT64, + FieldDescriptorProto.TYPE_UINT64, + FieldDescriptorProto.TYPE_SINT64, + } + ifdefs: set[str | None] = { + message_ifdef_map.get(msg.name) + for msg in file_desc.message_type + if not msg.options.deprecated + for field in msg.field + if not field.options.deprecated and field.type in varint64_types + } + if not ifdefs: + return False, None + if None in ifdefs: + # At least one 64-bit varint field is unconditional, so the guard must be unconditional. + return True, None + ifdefs.discard(None) + return True, ifdefs.pop() if len(ifdefs) == 1 else None + + def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: """Builds the enum type. @@ -2567,11 +2598,38 @@ def main() -> None: file = d.file[0] + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes + enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( + build_type_usage_map(file) + ) + + # Find the ifdef guard for 64-bit varint fields (int64/uint64/sint64). + # Generated into api_pb2_defines.h so proto.h can include it, ensuring + # consistent ProtoVarInt layout across all translation units. + has_varint64, varint64_guard = get_varint64_ifdef(file, message_ifdef_map) + + # Generate api_pb2_defines.h — included by proto.h to ensure all translation + # units see USE_API_VARINT64 consistently (avoids ODR violations in ProtoVarInt). + defines_content = FILE_HEADER + defines_content += "#pragma once\n\n" + defines_content += '#include "esphome/core/defines.h"\n' + if has_varint64: + lines = [ + "#ifndef USE_API_VARINT64", + "#define USE_API_VARINT64", + "#endif", + ] + defines_content += "\n".join(wrap_with_ifdef(lines, varint64_guard)) + defines_content += "\n" + defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n" + + with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f: + f.write(defines_content) + content = FILE_HEADER content += """\ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/string_ref.h" #include "proto.h" @@ -2702,11 +2760,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint content += "namespace enums {\n\n" - # Build dynamic ifdef mappings for both enums and messages - enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( - build_type_usage_map(file) - ) - # Simple grouping of enums by ifdef current_ifdef = None diff --git a/tests/integration/fixtures/varint_five_byte_device_id.yaml b/tests/integration/fixtures/varint_five_byte_device_id.yaml new file mode 100644 index 00000000000..08259869ca7 --- /dev/null +++ b/tests/integration/fixtures/varint_five_byte_device_id.yaml @@ -0,0 +1,47 @@ +esphome: + name: varint-5byte-test + # Define areas and devices - device_ids will be FNV hashes > 2^28, + # requiring 5-byte varint encoding that exercises the 32-bit parse boundary. + areas: + - id: test_area + name: Test Area + devices: + - id: sub_device_one + name: Sub Device One + area_id: test_area + - id: sub_device_two + name: Sub Device Two + area_id: test_area + +host: +api: +logger: + +# Switches on sub-devices so we can send commands with large device_id varints +switch: + - platform: template + name: Device Switch + device_id: sub_device_one + id: device_switch_one + optimistic: true + turn_on_action: + - logger.log: "Switch one on" + turn_off_action: + - logger.log: "Switch one off" + + - platform: template + name: Device Switch + device_id: sub_device_two + id: device_switch_two + optimistic: true + turn_on_action: + - logger.log: "Switch two on" + turn_off_action: + - logger.log: "Switch two off" + +sensor: + - platform: template + name: Device Sensor + device_id: sub_device_one + lambda: return 42.0; + update_interval: 0.1s diff --git a/tests/integration/test_varint_five_byte_device_id.py b/tests/integration/test_varint_five_byte_device_id.py new file mode 100644 index 00000000000..d34c2f03d6c --- /dev/null +++ b/tests/integration/test_varint_five_byte_device_id.py @@ -0,0 +1,120 @@ +"""Integration test for 5-byte varint parsing of device_id fields. + +Device IDs are FNV hashes (uint32) that frequently exceed 2^28 (268435456), +requiring 5 varint bytes. This test verifies that: +1. The firmware correctly decodes 5-byte varint device_id in incoming commands +2. The firmware correctly encodes large device_id values in state responses +3. Switch commands with large device_id reach the correct entity +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SwitchInfo, SwitchState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_varint_five_byte_device_id( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that device_id values requiring 5-byte varints parse correctly.""" + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + devices = device_info.devices + assert len(devices) >= 2, f"Expected at least 2 devices, got {len(devices)}" + + # Verify at least one device_id exceeds the 4-byte varint boundary (2^28) + large_ids = [d for d in devices if d.device_id >= (1 << 28)] + assert len(large_ids) > 0, ( + "Expected at least one device_id >= 2^28 to exercise 5-byte varint path. " + f"Got device_ids: {[d.device_id for d in devices]}" + ) + + # Get entities + all_entities, _ = await client.list_entities_services() + switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)] + + # Find switches named "Device Switch" — one per sub-device + device_switches = [e for e in switch_entities if e.name == "Device Switch"] + assert len(device_switches) == 2, ( + f"Expected 2 'Device Switch' entities, got {len(device_switches)}" + ) + + # Verify switches have different device_ids matching the sub-devices + switch_device_ids = {s.device_id for s in device_switches} + assert len(switch_device_ids) == 2, "Switches should have different device_ids" + + # Subscribe to states and wait for initial states + loop = asyncio.get_running_loop() + states: dict[tuple[int, int], EntityState] = {} + switch_futures: dict[tuple[int, int], asyncio.Future[EntityState]] = {} + initial_done: asyncio.Future[bool] = loop.create_future() + + def on_state(state: EntityState) -> None: + key = (state.device_id, state.key) + states[key] = state + + if len(states) >= 3 and not initial_done.done(): + initial_done.set_result(True) + + if initial_done.done() and key in switch_futures: + fut = switch_futures[key] + if not fut.done() and isinstance(state, SwitchState): + fut.set_result(state) + + client.subscribe_states(on_state) + + try: + await asyncio.wait_for(initial_done, timeout=10.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for initial states. Got {len(states)} states" + ) + + # Verify state responses contain correct large device_id values + for device in devices: + device_states = [ + s for (did, _), s in states.items() if did == device.device_id + ] + assert len(device_states) > 0, ( + f"No states received for device '{device.name}' " + f"(device_id={device.device_id})" + ) + + # Test switch commands with large device_id varints — + # this is the critical path: the client encodes device_id as a varint + # in the SwitchCommandRequest, and the firmware must decode it correctly. + for switch in device_switches: + state_key = (switch.device_id, switch.key) + + # Turn on + switch_futures[state_key] = loop.create_future() + client.switch_command(switch.key, True, device_id=switch.device_id) + try: + await asyncio.wait_for(switch_futures[state_key], timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for switch ON state " + f"(device_id={switch.device_id}, key={switch.key}). " + f"This likely means the firmware failed to decode the " + f"5-byte varint device_id in SwitchCommandRequest." + ) + assert states[state_key].state is True + + # Turn off + switch_futures[state_key] = loop.create_future() + client.switch_command(switch.key, False, device_id=switch.device_id) + try: + await asyncio.wait_for(switch_futures[state_key], timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timed out waiting for switch OFF state " + f"(device_id={switch.device_id}, key={switch.key})" + ) + assert states[state_key].state is False From 62da60df477d3efcf9fcb80de6978b7ff44749f9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:19:20 -0500 Subject: [PATCH 0198/2056] [ld2420] Fix sizeof vs value bug in register memcpy (#14286) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index cf78a1a4609..b653f4ae88e 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -590,7 +590,7 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); index += CMD_REG_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE); byteswap(this->cmd_reply_.data[reg_element]); reg_element++; } @@ -729,9 +729,9 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { cmd_frame.data_length = 0; cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_WRITE_REGISTER; - memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], ®, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; - memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); From e601162cdd0500581cb838b8fd588fca3e3bba8e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:21:00 -0500 Subject: [PATCH 0199/2056] [lcd_base] Fix millis() truncation to uint8_t (#14289) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/lcd_base/lcd_display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index d3434cce101..cd08a739eb6 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -45,7 +45,7 @@ void LCDDisplay::setup() { // TODO dotsize // Commands can only be sent 40ms after boot-up, so let's wait if we're close - const uint8_t now = millis(); + const uint32_t now = millis(); if (now < 40) delay(40u - now); From df77213f2cb21a31e62ff0a9570d7af73e8f431b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:27:00 -0500 Subject: [PATCH 0200/2056] [shelly_dimmer] Fix millis overflow in ACK timeout check (#14288) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/shelly_dimmer/stm32flash.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index b052c0cee9f..a1a933bcab1 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -149,7 +149,7 @@ stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) do { yield(); if (!stream->available()) { - if (millis() < start_time + timeout) + if (millis() - start_time < timeout) continue; ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout); return STM32_ERR_UNKNOWN; @@ -212,7 +212,7 @@ stm32_err_t stm32_resync(const stm32_unique_ptr &stm) { static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); uint8_t ack; - while (t1 < t0 + STM32_RESYNC_TIMEOUT) { + while (t1 - t0 < STM32_RESYNC_TIMEOUT) { stream->write_array(buf, BUFFER_SIZE); stream->flush(); if (!stream->read_array(&ack, 1)) { From 3f558f63d8fec863d51e05e44a8dcee406d278c1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:28:47 -0500 Subject: [PATCH 0201/2056] [bl0942] Fix millis overflow in packet timeout check (#14285) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/bl0942/bl0942.cpp | 8 ++++---- esphome/components/bl0942/bl0942.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index b408c5549c1..16ad33141d1 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -52,12 +52,12 @@ void BL0942::loop() { return; } if (avail < sizeof(buffer)) { - if (!this->rx_start_) { + if (!this->rx_start_.has_value()) { this->rx_start_ = millis(); - } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { + } else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) { ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail); this->read_array((uint8_t *) &buffer, avail); - this->rx_start_ = 0; + this->rx_start_.reset(); } return; } @@ -67,7 +67,7 @@ void BL0942::loop() { this->received_package_(&buffer); } } - this->rx_start_ = 0; + this->rx_start_.reset(); } bool BL0942::validate_checksum_(DataPacket *data) { diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 10b29a72c61..3c013f86e7d 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { uint8_t address_ = 0; bool reset_ = false; LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; - uint32_t rx_start_ = 0; + optional rx_start_{}; uint32_t prev_cf_cnt_ = 0; bool validate_checksum_(DataPacket *data); From d1a636a5c3227d08eed01f704e26f172394eba64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:34:38 -0500 Subject: [PATCH 0202/2056] [rtttl] Fix speaker playback bugs (#14280) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 21 +++++++++++---------- esphome/components/rtttl/rtttl.h | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 6e86405b740..ab95067f458 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -139,9 +139,10 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 2); - if (send != x * 4) { - this->samples_sent_ -= (x - (send / 2)); + size_t bytes_to_send = x * sizeof(SpeakerSample); + size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); + if (send != bytes_to_send) { + this->samples_sent_ -= (x - (send / sizeof(SpeakerSample))); } return; } @@ -201,9 +202,9 @@ void Rtttl::loop() { bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { + if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) { ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, - (int) sizeof(NOTES)); + (int) (sizeof(NOTES) / sizeof(NOTES[0]))); this->finish_(); return; } @@ -221,7 +222,7 @@ void Rtttl::loop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { - if (need_note_gap) { + if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) { this->output_->set_level(0.0); delay(DOUBLE_NOTE_GAP_MS); this->note_duration_ -= DOUBLE_NOTE_GAP_MS; @@ -240,9 +241,9 @@ void Rtttl::loop() { this->samples_sent_ = 0; this->samples_gap_ = 0; this->samples_per_wave_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); + this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); + this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; } if (this->output_freq_ != 0) { // make sure there is enough samples to add a full last sinus. @@ -279,7 +280,7 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 0; int bpm = 63; - uint8_t num; + uint16_t num; // Get name this->position_ = this->rtttl_.find(':'); @@ -395,7 +396,7 @@ void Rtttl::finish_() { sample[0].right = 0; sample[1].left = 0; sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 6f5df07766b..4d4a652c517 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -46,8 +46,8 @@ class Rtttl : public Component { } protected: - inline uint8_t get_integer_() { - uint8_t ret = 0; + inline uint16_t get_integer_() { + uint16_t ret = 0; while (isdigit(this->rtttl_[this->position_])) { ret = (ret * 10) + (this->rtttl_[this->position_++] - '0'); } @@ -87,7 +87,7 @@ class Rtttl : public Component { #ifdef USE_OUTPUT /// The output to write the sound to. - output::FloatOutput *output_; + output::FloatOutput *output_{nullptr}; #endif // USE_OUTPUT #ifdef USE_SPEAKER From 5dffceda59542cf5abca89415bc6944b94b97793 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:35:27 -0500 Subject: [PATCH 0203/2056] [hmc5883l] Fix wrong gain for 88uT range (#14281) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/hmc5883l/hmc5883l.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index b62381a287f..bee52821255 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -95,7 +95,7 @@ void HMC5883LComponent::update() { float mg_per_bit; switch (this->range_) { case HMC5883L_RANGE_88_UT: - mg_per_bit = 0.073f; + mg_per_bit = 0.73f; break; case HMC5883L_RANGE_130_UT: mg_per_bit = 0.92f; From d61e2f9c29543e19c79b6accaf94a47b11cf87ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:06:13 -0500 Subject: [PATCH 0204/2056] [light] Fix millis overflow in transition progress and flash timing (#14292) Co-authored-by: J. Nick Koston Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/light/light_transformer.h | 7 +++---- esphome/components/light/transformers.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 079c2d2ae01..b9535c834c5 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -44,12 +44,11 @@ class LightTransformer { /// The progress of this transition, on a scale of 0 to 1. float get_progress_() { uint32_t now = esphome::millis(); - if (now < this->start_time_) - return 0.0f; - if (now >= this->start_time_ + this->length_) + uint32_t elapsed = now - this->start_time_; + if (elapsed >= this->length_) return 1.0f; - return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + return clamp(elapsed / float(this->length_), 0.0f, 1.0f); } uint32_t start_time_; diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index a26713b7238..b6e5e08f2bb 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -78,7 +78,7 @@ class LightFlashTransformer : public LightTransformer { optional apply() override { optional result = {}; - if (this->transformer_ == nullptr && millis() > this->start_time_ + this->length_ - this->transition_length_) { + if (this->transformer_ == nullptr && millis() - this->start_time_ > this->length_ - this->transition_length_) { // second transition back to start value this->transformer_ = this->state_.get_output()->create_default_transition(); this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); From 3dcc9ab76537dc884dbc93924e5190fd12c14bef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:08:04 -0500 Subject: [PATCH 0205/2056] [ble_presence] Fix millis overflow in presence timeout check (#14293) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ble_presence/ble_presence_device.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 70ecc67c325..f2f0a3ed191 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -101,7 +101,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } void loop() override { - if (this->found_ && this->last_seen_ + this->timeout_ < millis()) + if (this->found_ && millis() - this->last_seen_ > this->timeout_) this->set_found_(false); } void dump_config() override; From a60e5c5c4f5b3871ac30633ed007f21bbbc24333 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:11:52 -0500 Subject: [PATCH 0206/2056] [lightwaverf] Fix millis overflow in send timeout check (#14294) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/lightwaverf/lightwaverf.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp index 2b44195c974..2c6a1ecf5b5 100644 --- a/esphome/components/lightwaverf/lightwaverf.cpp +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -31,13 +31,12 @@ void LightWaveRF::read_tx() { void LightWaveRF::send_rx(const std::vector &msg, uint8_t repeats, bool inverted, int u_sec) { this->lwtx_.lwtx_setup(pin_tx_, repeats, inverted, u_sec); - uint32_t timeout = 0; + uint32_t timeout = millis(); if (this->lwtx_.lwtx_free()) { this->lwtx_.lwtx_send(msg); - timeout = millis(); ESP_LOGD(TAG, "[%i] msg start", timeout); } - while (!this->lwtx_.lwtx_free() && millis() < (timeout + 1000)) { + while (!this->lwtx_.lwtx_free() && millis() - timeout < 1000) { delay(10); } timeout = millis() - timeout; From 2e167835ead5cc85d3507d0f262695ed112d5729 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:15:49 -0500 Subject: [PATCH 0207/2056] [pn532] Replace millis zero sentinel with optional (#14295) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/pn532/pn532.cpp | 8 ++++---- esphome/components/pn532/pn532.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 1ab0da3df78..199a44dacc1 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -308,13 +308,13 @@ void PN532::send_nack_() { enum PN532ReadReady PN532::read_ready_(bool block) { if (this->rd_ready_ == READY) { if (block) { - this->rd_start_time_ = 0; + this->rd_start_time_.reset(); this->rd_ready_ = WOULDBLOCK; } return READY; } - if (!this->rd_start_time_) { + if (!this->rd_start_time_.has_value()) { this->rd_start_time_ = millis(); } @@ -324,7 +324,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { break; } - if (millis() - this->rd_start_time_ > 100) { + if (millis() - *this->rd_start_time_ > 100) { ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); this->rd_ready_ = TIMEOUT; break; @@ -340,7 +340,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { auto rdy = this->rd_ready_; if (block || rdy == TIMEOUT) { - this->rd_start_time_ = 0; + this->rd_start_time_.reset(); this->rd_ready_ = WOULDBLOCK; } return rdy; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index f98c0f93223..e57ecd81046 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -99,7 +99,7 @@ class PN532 : public PollingComponent { std::vector triggers_ontagremoved_; nfc::NfcTagUid current_uid_; nfc::NdefMessage *next_task_message_to_write_; - uint32_t rd_start_time_{0}; + optional rd_start_time_{}; enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum NfcTask { READ = 0, From 24fb74f78bce8d618f1a4e134c61aeded194956d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:21:33 -0500 Subject: [PATCH 0208/2056] [ld2420] Fix buffer overflows in command response parsing (#14297) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 39 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b653f4ae88e..f14400d15a4 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -560,8 +560,6 @@ void LD2420Component::read_batch_(std::span buffer) { void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; - uint8_t reg_element = 0; - uint8_t data_element = 0; uint16_t data_pos = 0; if (this->cmd_reply_.length > CMD_MAX_BYTES) { ESP_LOGW(TAG, "Reply frame too long"); @@ -583,43 +581,44 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { case (CMD_DISABLE_CONF): ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); break; - case (CMD_READ_REGISTER): + case (CMD_READ_REGISTER): { ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result); // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file data_pos = 0x0A; - for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT - ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); - index += CMD_REG_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE); - byteswap(this->cmd_reply_.data[reg_element]); - reg_element++; + uint16_t reg_count = std::min((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE, + sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0])); + for (uint16_t i = 0; i < reg_count; i++) { + memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_REG_DATA_REPLY_SIZE], CMD_REG_DATA_REPLY_SIZE); } break; + } case (CMD_WRITE_REGISTER): ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); break; case (CMD_WRITE_ABD_PARAM): ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); break; - case (CMD_READ_ABD_PARAM): + case (CMD_READ_ABD_PARAM): { ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); data_pos = CMD_ABD_DATA_REPLY_START; - for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT - ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); - index += CMD_ABD_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index], - sizeof(this->cmd_reply_.data[data_element])); - byteswap(this->cmd_reply_.data[data_element]); - data_element++; + uint16_t abd_count = std::min((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE, + sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0])); + for (uint16_t i = 0; i < abd_count; i++) { + memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_ABD_DATA_REPLY_SIZE], + sizeof(this->cmd_reply_.data[i])); } break; + } case (CMD_WRITE_SYS_PARAM): ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; - case (CMD_READ_VERSION): - memcpy(this->firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGV(TAG, "Firmware version: %7s %s", this->firmware_ver_, result); + case (CMD_READ_VERSION): { + uint8_t ver_len = std::min(buffer[10], sizeof(this->firmware_ver_) - 1); + memcpy(this->firmware_ver_, &buffer[12], ver_len); + this->firmware_ver_[ver_len] = '\0'; + ESP_LOGV(TAG, "Firmware version: %s %s", this->firmware_ver_, result); break; + } default: break; } From 23ef233b60de8bef361eb062cfa732321c9a4ceb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:21:50 -0500 Subject: [PATCH 0209/2056] [gp8403] Fix enum size mismatch in voltage register write (#14296) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/gp8403/gp8403.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 972f2ce60c3..a19df15515d 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -6,12 +6,12 @@ namespace esphome { namespace gp8403 { -enum GP8403Voltage { +enum GP8403Voltage : uint8_t { GP8403_VOLTAGE_5V = 0x00, GP8403_VOLTAGE_10V = 0x11, }; -enum GP8403Model { +enum GP8403Model : uint8_t { GP8403, GP8413, }; From 0a81a7a50bc4b73f4075a20fb8c5a74b4ddc82f4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:01:32 -0500 Subject: [PATCH 0210/2056] [mcp2515] Fix millis overflow in set_mode_ timeout (#14298) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/mcp2515/mcp2515.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 1a17715315b..77bfaf92246 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -133,8 +133,8 @@ uint8_t MCP2515::get_status_() { canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); - uint32_t end_time = millis() + 10; - while (millis() < end_time) { + uint32_t start_time = millis(); + while (millis() - start_time < 10) { if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) return canbus::ERROR_OK; } From 534857db9c16b7ebfc62693291d4b7a56b91eb36 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:01:49 -0500 Subject: [PATCH 0211/2056] [wled] Fix millis overflow in blank timeout (#14300) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/wled/wled_light_effect.cpp | 21 +++++++++++-------- esphome/components/wled/wled_light_effect.h | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index d26b7a17503..87bae5b1da4 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -24,7 +24,7 @@ namespace wled { // https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; -const int DEFAULT_BLANK_TIME = 1000; +constexpr uint32_t DEFAULT_BLANK_TIME = 1000; static const char *const TAG = "wled_light_effect"; @@ -34,9 +34,10 @@ void WLEDLightEffect::start() { AddressableLightEffect::start(); if (this->blank_on_start_) { - this->blank_at_ = 0; + this->blank_start_ = millis(); + this->blank_timeout_ = 0; } else { - this->blank_at_ = UINT32_MAX; + this->blank_start_.reset(); } } @@ -81,10 +82,10 @@ void WLEDLightEffect::apply(light::AddressableLight &it, const Color ¤t_co } } - // FIXME: Use roll-over safe arithmetic - if (blank_at_ < millis()) { + if (this->blank_start_.has_value() && millis() - *this->blank_start_ >= this->blank_timeout_) { blank_all_leds_(it); - blank_at_ = millis() + DEFAULT_BLANK_TIME; + this->blank_start_ = millis(); + this->blank_timeout_ = DEFAULT_BLANK_TIME; } } @@ -142,11 +143,13 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p } if (timeout == UINT8_MAX) { - blank_at_ = UINT32_MAX; + this->blank_start_.reset(); } else if (timeout > 0) { - blank_at_ = millis() + timeout * 1000; + this->blank_start_ = millis(); + this->blank_timeout_ = timeout * 1000; } else { - blank_at_ = millis() + DEFAULT_BLANK_TIME; + this->blank_start_ = millis(); + this->blank_timeout_ = DEFAULT_BLANK_TIME; } it.schedule_show(); diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 6da5f4e9f9c..3f3b7106119 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -35,7 +35,8 @@ class WLEDLightEffect : public light::AddressableLightEffect { uint16_t port_{0}; std::unique_ptr udp_; - uint32_t blank_at_{0}; + optional blank_start_{}; + uint32_t blank_timeout_{0}; uint32_t dropped_{0}; uint8_t sync_group_mask_{0}; bool blank_on_start_{true}; From 0d5b7df77d11b48ad58029adea4dfb88743e2932 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:32:02 -0500 Subject: [PATCH 0212/2056] [sensor] Fix delta filter percentage mode regression (#14302) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/sensor/__init__.py | 2 +- .../fixtures/sensor_filters_delta.yaml | 40 +++++++++++++++++++ .../integration/test_sensor_filters_delta.py | 27 ++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index b0e0c28bda6..770c044efb9 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -603,7 +603,7 @@ DELTA_SCHEMA = cv.Any( def _get_delta(value): if isinstance(value, str): assert value.endswith("%") - return 0.0, float(value[:-1]) + return 0.0, float(value[:-1]) / 100.0 return value, 0.0 diff --git a/tests/integration/fixtures/sensor_filters_delta.yaml b/tests/integration/fixtures/sensor_filters_delta.yaml index 19bd2d5ca48..2494a430dac 100644 --- a/tests/integration/fixtures/sensor_filters_delta.yaml +++ b/tests/integration/fixtures/sensor_filters_delta.yaml @@ -28,6 +28,11 @@ sensor: id: source_sensor_4 accuracy_decimals: 1 + - platform: template + name: "Source Sensor 5" + id: source_sensor_5 + accuracy_decimals: 1 + - platform: copy source_id: source_sensor_1 name: "Filter Min" @@ -69,6 +74,13 @@ sensor: filters: - delta: 0 + - platform: copy + source_id: source_sensor_5 + name: "Filter Percentage" + id: filter_percentage + filters: + - delta: 50% + script: - id: test_filter_min then: @@ -154,6 +166,28 @@ script: id: source_sensor_4 state: 2.0 + - id: test_filter_percentage + then: + - sensor.template.publish: + id: source_sensor_5 + state: 100.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 120.0 # Filtered out (delta=20, need >50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 160.0 # Passes (delta=60 > 50% of 100=50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 200.0 # Filtered out (delta=40, need >50% of 160=80) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 250.0 # Passes (delta=90 > 80) + button: - platform: template name: "Test Filter Min" @@ -178,3 +212,9 @@ button: id: btn_filter_zero_delta on_press: - script.execute: test_filter_zero_delta + + - platform: template + name: "Test Filter Percentage" + id: btn_filter_percentage + on_press: + - script.execute: test_filter_percentage diff --git a/tests/integration/test_sensor_filters_delta.py b/tests/integration/test_sensor_filters_delta.py index c7a26bf9d12..9d0114e0c41 100644 --- a/tests/integration/test_sensor_filters_delta.py +++ b/tests/integration/test_sensor_filters_delta.py @@ -24,12 +24,14 @@ async def test_sensor_filters_delta( "filter_max": [], "filter_baseline_max": [], "filter_zero_delta": [], + "filter_percentage": [], } filter_min_done = loop.create_future() filter_max_done = loop.create_future() filter_baseline_max_done = loop.create_future() filter_zero_delta_done = loop.create_future() + filter_percentage_done = loop.create_future() def on_state(state: EntityState) -> None: if not isinstance(state, SensorState) or state.missing_state: @@ -66,6 +68,12 @@ async def test_sensor_filters_delta( and not filter_zero_delta_done.done() ): filter_zero_delta_done.set_result(True) + elif ( + sensor_name == "filter_percentage" + and len(sensor_values[sensor_name]) == 3 + and not filter_percentage_done.done() + ): + filter_percentage_done.set_result(True) async with ( run_compiled(yaml_config), @@ -80,6 +88,7 @@ async def test_sensor_filters_delta( "filter_max": "Filter Max", "filter_baseline_max": "Filter Baseline Max", "filter_zero_delta": "Filter Zero Delta", + "filter_percentage": "Filter Percentage", }, ) @@ -98,13 +107,14 @@ async def test_sensor_filters_delta( "Test Filter Max": "filter_max", "Test Filter Baseline Max": "filter_baseline_max", "Test Filter Zero Delta": "filter_zero_delta", + "Test Filter Percentage": "filter_percentage", } buttons = {} for entity in entities: if isinstance(entity, ButtonInfo) and entity.name in button_name_map: buttons[button_name_map[entity.name]] = entity.key - assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}" + assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}" # Test 1: Min sensor_values["filter_min"].clear() @@ -161,3 +171,18 @@ async def test_sensor_filters_delta( assert sensor_values["filter_zero_delta"] == pytest.approx(expected), ( f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}" ) + + # Test 5: Percentage (delta: 50%) + sensor_values["filter_percentage"].clear() + client.button_command(buttons["filter_percentage"]) + try: + await asyncio.wait_for(filter_percentage_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 5 timed out. Values: {sensor_values['filter_percentage']}" + ) + + expected = [100.0, 160.0, 250.0] + assert sensor_values["filter_percentage"] == pytest.approx(expected), ( + f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}" + ) From 19f4845185c5669d88c395e323f892fd6a0dbb2c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:06:46 -0500 Subject: [PATCH 0213/2056] [max7219digit] Fix typo in action names (#14162) Co-authored-by: Claude Opus 4.6 --- esphome/components/max7219digit/display.py | 22 +++++++++++----------- tests/components/max7219digit/common.yaml | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index e6d53efc5dd..a251eaccea5 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,12 +133,12 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_invert_to_code(config, action_id, template_arg, args): +async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -146,12 +146,12 @@ async def max7129digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_visible_to_code(config, action_id, template_arg, args): +async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -159,12 +159,12 @@ async def max7129digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA ) @automation.register_action( - "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA ) -async def max7129digit_reverse_to_code(config, action_id, template_arg, args): +async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_state(config[CONF_STATE])) @@ -183,9 +183,9 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA ) -async def max7129digit_intensity_to_code(config, action_id, template_arg, args): +async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8) diff --git a/tests/components/max7219digit/common.yaml b/tests/components/max7219digit/common.yaml index 525b7b8d3e3..4d2dbb781d8 100644 --- a/tests/components/max7219digit/common.yaml +++ b/tests/components/max7219digit/common.yaml @@ -13,10 +13,10 @@ esphome: on_boot: - priority: 100 then: - - max7129digit.invert_off: - - max7129digit.invert_on: - - max7129digit.turn_on: - - max7129digit.turn_off: - - max7129digit.reverse_on: - - max7129digit.reverse_off: - - max7129digit.intensity: 10 + - max7219digit.invert_off: + - max7219digit.invert_on: + - max7219digit.turn_on: + - max7219digit.turn_off: + - max7219digit.reverse_on: + - max7219digit.reverse_off: + - max7219digit.intensity: 10 From 0975755a9d13de364cb780abdc6bd854d7419b64 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:51:13 -1000 Subject: [PATCH 0214/2056] [mipi_dsi] Disallow swap_xy (#14124) --- esphome/components/mipi_dsi/display.py | 26 +++++-------------- .../mipi_dsi/fixtures/mipi_dsi.yaml | 7 ++++- .../mipi_dsi/test_mipi_dsi_config.py | 6 +++-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index c288b33cd2f..de3791b3a47 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -87,38 +87,24 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] + model.defaults[CONF_SWAP_XY] = cv.UNDEFINED transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), } ) - if model.get_default(CONF_SWAP_XY) != cv.UNDEFINED: - transform = transform.extend( - { - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by this model" - ) - } - ) - else: - transform = transform.extend( - { - cv.Required(CONF_SWAP_XY): cv.boolean, - } - ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None else cv.Optional(CONF_INIT_SEQUENCE) ) - swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) - - # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden - cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required - ) + # Dimensions are optional if the model has a default width + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") schema = display.FULL_DISPLAY_SCHEMA.extend( { diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 7d1fc841215..6de2bd5a770 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -15,8 +15,13 @@ esp_ldo: display: - platform: mipi_dsi + id: p4_nano model: WAVESHARE-P4-NANO-10.1 - + rotation: 90 + - platform: mipi_dsi + id: p4_86 + model: "WAVESHARE-P4-86-PANEL" + rotation: 180 i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index f8a9af02796..d465a8c81bf 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,9 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 1f5a35a99f599045a397d8df7316e4f1626fed74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:08:13 -0600 Subject: [PATCH 0215/2056] [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) --- esphome/components/dsmr/dsmr.h | 3 +++ tests/components/dsmr/common.yaml | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index fafcf62b876..dc81ba9b2af 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index 038bf2806b8..d11ce37d598 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -1,4 +1,13 @@ +esphome: + on_boot: + then: + - lambda: |- + // Test deprecated std::string overload still compiles + std::string key = "00112233445566778899aabbccddeeff"; + id(dsmr_instance).set_decryption_key(key); + dsmr: + id: dsmr_instance decryption_key: 00112233445566778899aabbccddeeff max_telegram_length: 1000 request_pin: ${request_pin} From 15e2a778d406ccdfdfcc8b6a39709d9ca84cadc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:20 -0600 Subject: [PATCH 0216/2056] [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) --- esphome/components/api/homeassistant_service.h | 2 ++ tests/components/homeassistant/common.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 2322d96eefc..340699e1a6d 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -36,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 9c6cb71b8b3..60e3defd496 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -90,6 +90,19 @@ text_sensor: id: ha_hello_world_text2 attribute: some_attribute +event: + - platform: template + name: Test Event + id: test_event + event_types: + - test_event_type + on_event: + - homeassistant.event: + event: esphome.test_event + data: + event_name: !lambda |- + return event_type; + time: - platform: homeassistant on_time: From c5c6ce6b0e71a932621fbb5848cc3909fbe9634a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 13:54:43 -0600 Subject: [PATCH 0217/2056] [haier] Fix uninitialized HonSettings causing API connection failures (#14188) --- esphome/components/haier/hon_climate.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89c..608d5e7f210 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -29,10 +29,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase { int big_data_sensors_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; From 27fe866d5e5776317a3700cd98b00adf7e2fc52e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 23:22:59 -0600 Subject: [PATCH 0218/2056] [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) --- esphome/components/bme68x_bsec2/__init__.py | 5 ++++- tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6a..4200b2f0b85 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,8 +178,11 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 00000000000..7c503b0ccb6 --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml From 997f825cd3c03ee1a7a07c07306b37e1d37e4fe2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:56:09 -0600 Subject: [PATCH 0219/2056] [network] Improve IPAddress::str() deprecation warning with usage example (#14195) --- esphome/components/network/ip_address.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d0ac8164af2..b2a2c563e28 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -61,7 +61,9 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -150,7 +152,9 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } // Remove before 2026.8.0 - ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") + ESPDEPRECATED( + "str() is deprecated: use 'char buf[IP_ADDRESS_BUFFER_SIZE]; ip.str_to(buf);' instead. Removed in 2026.8.0", + "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From 4b57ac323666c8278e93bf20af20268077cab2c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 17:07:56 -0600 Subject: [PATCH 0220/2056] [water_heater] Fix device_id missing from state responses (#14212) --- esphome/components/api/api_connection.cpp | 3 +- .../fixtures/device_id_in_state.yaml | 115 ++++++++++++ tests/integration/test_device_id_in_state.py | 173 ++++++++++++------ 3 files changed, 232 insertions(+), 59 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4bc3c9b3074..b388bf59713 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1334,9 +1334,8 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index c8548617b88..a5dfb7ee45c 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -46,6 +46,7 @@ sensor: binary_sensor: - platform: template + id: motion_detected name: Motion Detected device_id: motion_sensor lambda: return true; @@ -82,3 +83,117 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); + +cover: + - platform: template + name: Garage Door + device_id: motion_sensor + optimistic: true + +fan: + - platform: template + name: Ceiling Fan + device_id: humidity_monitor + speed_count: 3 + has_oscillating: false + has_direction: false + +lock: + - platform: template + name: Front Door Lock + device_id: motion_sensor + optimistic: true + +number: + - platform: template + name: Target Temperature + device_id: temperature_monitor + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: Mode Select + device_id: humidity_monitor + optimistic: true + options: + - "Auto" + - "Manual" + +text: + - platform: template + name: Device Label + device_id: temperature_monitor + optimistic: true + mode: text + +valve: + - platform: template + name: Water Valve + device_id: humidity_monitor + optimistic: true + +globals: + - id: global_away + type: bool + initial_value: "false" + - id: global_is_on + type: bool + initial_value: "true" + +water_heater: + - platform: template + name: Test Boiler + device_id: temperature_monitor + optimistic: true + current_temperature: !lambda "return 45.0f;" + target_temperature: !lambda "return 60.0f;" + away: !lambda "return id(global_away);" + is_on: !lambda "return id(global_is_on);" + supported_modes: + - "off" + - electric + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 + set_action: + - lambda: |- + ESP_LOGD("test", "Water heater set"); + +alarm_control_panel: + - platform: template + name: House Alarm + device_id: motion_sensor + codes: + - "1234" + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: motion_detected + +datetime: + - platform: template + name: Schedule Date + device_id: temperature_monitor + type: date + optimistic: true + - platform: template + name: Schedule Time + device_id: humidity_monitor + type: time + optimistic: true + - platform: template + name: Schedule DateTime + device_id: motion_sensor + type: datetime + optimistic: true + +event: + - platform: template + name: Doorbell + device_id: motion_sensor + event_types: + - "press" + - "double_press" diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index 51088bcbf79..48de94a85a8 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -4,11 +4,80 @@ from __future__ import annotations import asyncio -from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState +from aioesphomeapi import ( + AlarmControlPanelEntityState, + BinarySensorState, + CoverState, + DateState, + DateTimeState, + EntityState, + FanState, + LightState, + LockEntityState, + NumberState, + SelectState, + SensorState, + SwitchState, + TextSensorState, + TextState, + TimeState, + ValveState, + WaterHeaterState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction +# Mapping of entity name to device name for all entities with device_id +ENTITY_TO_DEVICE = { + # Original entities + "Temperature": "Temperature Monitor", + "Humidity": "Humidity Monitor", + "Motion Detected": "Motion Sensor", + "Temperature Monitor Power": "Temperature Monitor", + "Temperature Status": "Temperature Monitor", + "Motion Light": "Motion Sensor", + # New entity types + "Garage Door": "Motion Sensor", + "Ceiling Fan": "Humidity Monitor", + "Front Door Lock": "Motion Sensor", + "Target Temperature": "Temperature Monitor", + "Mode Select": "Humidity Monitor", + "Device Label": "Temperature Monitor", + "Water Valve": "Humidity Monitor", + "Test Boiler": "Temperature Monitor", + "House Alarm": "Motion Sensor", + "Schedule Date": "Temperature Monitor", + "Schedule Time": "Humidity Monitor", + "Schedule DateTime": "Motion Sensor", + "Doorbell": "Motion Sensor", +} + +# Entities without device_id (should have device_id 0) +NO_DEVICE_ENTITIES = {"No Device Sensor"} + +# State types that should have non-zero device_id, mapped by their aioesphomeapi class +EXPECTED_STATE_TYPES = [ + (SensorState, "sensor"), + (BinarySensorState, "binary_sensor"), + (SwitchState, "switch"), + (TextSensorState, "text_sensor"), + (LightState, "light"), + (CoverState, "cover"), + (FanState, "fan"), + (LockEntityState, "lock"), + (NumberState, "number"), + (SelectState, "select"), + (TextState, "text"), + (ValveState, "valve"), + (WaterHeaterState, "water_heater"), + (AlarmControlPanelEntityState, "alarm_control_panel"), + (DateState, "date"), + (TimeState, "time"), + (DateTimeState, "datetime"), + # Event is stateless (no initial state sent on subscribe) +] + @pytest.mark.asyncio async def test_device_id_in_state( @@ -40,34 +109,35 @@ async def test_device_id_in_state( entity_device_mapping: dict[int, int] = {} for entity in all_entities: - # All entities have name and key attributes - if entity.name == "Temperature": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Humidity": - entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] - elif entity.name == "Motion Detected": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Motion Light": - entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "No Device Sensor": - # Entity without device_id should have device_id 0 + if entity.name in ENTITY_TO_DEVICE: + expected_device = ENTITY_TO_DEVICE[entity.name] + entity_device_mapping[entity.key] = device_ids[expected_device] + elif entity.name in NO_DEVICE_ENTITIES: entity_device_mapping[entity.key] = 0 - assert len(entity_device_mapping) >= 6, ( - f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}" + expected_count = len(ENTITY_TO_DEVICE) + len(NO_DEVICE_ENTITIES) + assert len(entity_device_mapping) >= expected_count, ( + f"Expected at least {expected_count} mapped entities, " + f"got {len(entity_device_mapping)}. " + f"Missing: {set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES - {e.name for e in all_entities}}" + ) + + # Subscribe to states and wait for all mapped entities + # Event entities are stateless (no initial state on subscribe), + # so exclude them from the expected count + stateless_keys = {e.key for e in all_entities if e.name == "Doorbell"} + stateful_count = len(entity_device_mapping) - len( + stateless_keys & entity_device_mapping.keys() ) - # Subscribe to states loop = asyncio.get_running_loop() states: dict[int, EntityState] = {} states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have states for all mapped entities - if len(states) >= len(entity_device_mapping) and not states_future.done(): + if state.key in entity_device_mapping: + states[state.key] = state + if len(states) >= stateful_count and not states_future.done(): states_future.set_result(True) client.subscribe_states(on_state) @@ -76,9 +146,16 @@ async def test_device_id_in_state( try: await asyncio.wait_for(states_future, timeout=10.0) except TimeoutError: + received_names = {e.name for e in all_entities if e.key in states} + missing_names = ( + (set(ENTITY_TO_DEVICE) | NO_DEVICE_ENTITIES) + - received_names + - {"Doorbell"} + ) pytest.fail( f"Did not receive all entity states within 10 seconds. " - f"Received {len(states)} states, expected {len(entity_device_mapping)}" + f"Received {len(states)} states. " + f"Missing: {missing_names}" ) # Verify each state has the correct device_id @@ -86,51 +163,33 @@ async def test_device_id_in_state( for key, expected_device_id in entity_device_mapping.items(): if key in states: state = states[key] + entity_name = next( + (e.name for e in all_entities if e.key == key), f"key={key}" + ) assert state.device_id == expected_device_id, ( - f"State for key {key} has device_id {state.device_id}, " - f"expected {expected_device_id}" + f"State for '{entity_name}' (type={type(state).__name__}) " + f"has device_id {state.device_id}, expected {expected_device_id}" ) verified_count += 1 - assert verified_count >= 6, ( - f"Only verified {verified_count} states, expected at least 6" + # All stateful entities should be verified (everything except Doorbell event) + expected_verified = expected_count - 1 # exclude Doorbell + assert verified_count >= expected_verified, ( + f"Only verified {verified_count} states, expected at least {expected_verified}" ) - # Test specific state types to ensure device_id is present - # Find a sensor state with device_id - sensor_state = next( - ( + # Verify each expected state type has at least one instance with non-zero device_id + for state_type, type_name in EXPECTED_STATE_TYPES: + matching = [ s for s in states.values() - if isinstance(s, SensorState) - and isinstance(s.state, float) - and s.device_id != 0 - ), - None, - ) - assert sensor_state is not None, "No sensor state with device_id found" - assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id" - - # Find a binary sensor state - binary_sensor_state = next( - (s for s in states.values() if isinstance(s, BinarySensorState)), - None, - ) - assert binary_sensor_state is not None, "No binary sensor state found" - assert binary_sensor_state.device_id > 0, ( - "Binary sensor state should have non-zero device_id" - ) - - # Find a text sensor state - text_sensor_state = next( - (s for s in states.values() if isinstance(s, TextSensorState)), - None, - ) - assert text_sensor_state is not None, "No text sensor state found" - assert text_sensor_state.device_id > 0, ( - "Text sensor state should have non-zero device_id" - ) + if isinstance(s, state_type) and s.device_id != 0 + ] + assert matching, ( + f"No {type_name} state (type={state_type.__name__}) " + f"with non-zero device_id found" + ) # Verify the "No Device Sensor" has device_id = 0 no_device_key = next( From efa39ae591ea3aedc5ea7b41d048df33ac15d0d7 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:33:33 +1100 Subject: [PATCH 0221/2056] [mipi_dsi] Allow transform disable; fix warnings (#14216) --- esphome/components/mipi_dsi/display.py | 24 +++++++++++-------- esphome/components/mipi_dsi/mipi_dsi.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.h | 10 ++++---- .../mipi_dsi/fixtures/mipi_dsi.yaml | 17 +++++++++++++ .../mipi_dsi/test_mipi_dsi_config.py | 3 ++- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index de3791b3a47..85bfad7f1a8 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -39,6 +39,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_COLOR_ORDER, CONF_DIMENSIONS, + CONF_DISABLED, CONF_ENABLE_PIN, CONF_ID, CONF_INIT_SEQUENCE, @@ -88,14 +89,17 @@ COLOR_DEPTHS = { def model_schema(config): model = MODELS[config[CONF_MODEL].upper()] model.defaults[CONF_SWAP_XY] = cv.UNDEFINED - transform = cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by DSI displays" - ), - } + transform = cv.Any( + cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by DSI displays" + ), + } + ), + cv.one_of(CONF_DISABLED, lower=True), ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( @@ -199,9 +203,9 @@ async def to_code(config): cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) - cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6))) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6)) cg.add(var.set_lanes(int(config[CONF_LANES]))) - cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6))) + cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6)) if reset_pin := config.get(CONF_RESET_PIN): reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 18cafab6844..4d45cfb7990 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -374,7 +374,7 @@ void MIPI_DSI::dump_config() { "\n Swap X/Y: %s" "\n Rotation: %d degrees" "\n DSI Lanes: %u" - "\n Lane Bit Rate: %uMbps" + "\n Lane Bit Rate: %.0fMbps" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -385,7 +385,7 @@ void MIPI_DSI::dump_config() { "\n Display Pixel Mode: %d bit" "\n Color Order: %s" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz", + "\n Pixel Clock: %.1fMHz", this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 1cffe3b1781..6e27912aa51 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -47,7 +47,7 @@ class MIPI_DSI : public display::Display { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } - void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_frequency(float pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } @@ -58,7 +58,7 @@ class MIPI_DSI : public display::Display { void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } void set_model(const char *model) { this->model_ = model; } - void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } + void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } @@ -95,9 +95,9 @@ class MIPI_DSI : public display::Display { uint16_t vsync_front_porch_ = 10; const char *model_{"Unknown"}; std::vector init_sequence_{}; - uint16_t pclk_frequency_ = 16; // in MHz - uint16_t lane_bit_rate_{1500}; // in Mbps - uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes + float pclk_frequency_ = 16; // in MHz + float lane_bit_rate_{1500}; // in Mbps + uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes bool invert_colors_{}; display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml index 6de2bd5a770..6f76dcb1d1a 100644 --- a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -22,6 +22,23 @@ display: id: p4_86 model: "WAVESHARE-P4-86-PANEL" rotation: 180 + - platform: mipi_dsi + model: custom + id: custom_id + dimensions: + width: 400 + height: 1280 + hsync_back_porch: 40 + hsync_pulse_width: 30 + hsync_front_porch: 40 + vsync_back_porch: 20 + vsync_pulse_width: 10 + vsync_front_porch: 20 + pclk_frequency: 48Mhz + lane_bit_rate: 1.2Gbps + rotation: 180 + transform: disabled + init_sequence: i2c: sda: GPIO7 scl: GPIO8 diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index d465a8c81bf..92f56b5451a 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -123,7 +123,8 @@ def test_code_generation( in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp - assert "p4_nano->set_lane_bit_rate(1500);" in main_cpp + assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp + assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 29d890bb0f89af6257a471e9256aa1bc042f381b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:15:22 -0500 Subject: [PATCH 0222/2056] [http_request.ota] Percent-encode credentials in URL (#14257) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http_request/ota/ota_http_request.cpp | 22 +++++++++++++++++++ .../http_request/ota/ota_http_request.h | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 882def4d7f0..bbe128aad97 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,5 +1,7 @@ #include "ota_http_request.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -210,6 +212,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() { return ota::OTA_RESPONSE_OK; } +// URL-encode characters that are not unreserved per RFC 3986 section 2.3. +// This is needed for embedding userinfo (username/password) in URLs safely. +static std::string url_encode(const std::string &str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + result += '%'; + result += format_hex_pretty_char((static_cast(c) >> 4) & 0x0F); + result += format_hex_pretty_char(static_cast(c) & 0x0F); + } + } + return result; +} + +void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); } +void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); } + std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { if (this->username_.empty() || this->password_.empty()) { return url; diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 8735189e993..e3f1a4aa90e 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -29,9 +29,9 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parentedmd5_expected_ = md5; } - void set_password(const std::string &password) { this->password_ = password; } + void set_password(const std::string &password); void set_url(const std::string &url); - void set_username(const std::string &username) { this->username_ = username; } + void set_username(const std::string &username); std::string md5_computed() { return this->md5_computed_; } std::string md5_expected() { return this->md5_expected_; } From 2c11c65faff2edec0224ca0bbe7624a9d3adc147 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:28:19 +1300 Subject: [PATCH 0223/2056] Don't get stuck forever on a failed component can_proceed (#14267) --- esphome/core/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 449acc64cf4..2b84b1c1e47 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -132,7 +132,7 @@ void Application::setup() { this->after_loop_tasks_(); this->app_state_ = new_app_state; yield(); - } while (!component->can_proceed()); + } while (!component->can_proceed() && !component->is_failed()); } ESP_LOGI(TAG, "setup() finished successfully!"); From af296eb600262ca2a3a4f9e47aab3e65bbc8826d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:26:00 -0500 Subject: [PATCH 0224/2056] [pid] Fix deadband threshold conversion for Fahrenheit (#14268) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/pid/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 5919d2cac87..5fa3166f9df 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -50,8 +50,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_DEADBAND_PARAMETERS): cv.Schema( { - cv.Required(CONF_THRESHOLD_HIGH): cv.temperature, - cv.Required(CONF_THRESHOLD_LOW): cv.temperature, + cv.Required(CONF_THRESHOLD_HIGH): cv.temperature_delta, + cv.Required(CONF_THRESHOLD_LOW): cv.temperature_delta, cv.Optional(CONF_KP_MULTIPLIER, default=0.1): cv.float_, cv.Optional(CONF_KI_MULTIPLIER, default=0.0): cv.float_, cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, From da930310b1e2e1175118126cd25680fa3ca2eeb2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:19:20 -0500 Subject: [PATCH 0225/2056] [ld2420] Fix sizeof vs value bug in register memcpy (#14286) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index cf78a1a4609..b653f4ae88e 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -590,7 +590,7 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); index += CMD_REG_DATA_REPLY_SIZE) { - memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], CMD_REG_DATA_REPLY_SIZE); byteswap(this->cmd_reply_.data[reg_element]); reg_element++; } @@ -729,9 +729,9 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { cmd_frame.data_length = 0; cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_WRITE_REGISTER; - memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], ®, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; - memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); + memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); From a39be5a4619817d0b3a244b04cbac5150cb4b540 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:34:38 -0500 Subject: [PATCH 0226/2056] [rtttl] Fix speaker playback bugs (#14280) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 21 +++++++++++---------- esphome/components/rtttl/rtttl.h | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 6e86405b740..ab95067f458 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -139,9 +139,10 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 2); - if (send != x * 4) { - this->samples_sent_ -= (x - (send / 2)); + size_t bytes_to_send = x * sizeof(SpeakerSample); + size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); + if (send != bytes_to_send) { + this->samples_sent_ -= (x - (send / sizeof(SpeakerSample))); } return; } @@ -201,9 +202,9 @@ void Rtttl::loop() { bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { + if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) { ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, - (int) sizeof(NOTES)); + (int) (sizeof(NOTES) / sizeof(NOTES[0]))); this->finish_(); return; } @@ -221,7 +222,7 @@ void Rtttl::loop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { - if (need_note_gap) { + if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) { this->output_->set_level(0.0); delay(DOUBLE_NOTE_GAP_MS); this->note_duration_ -= DOUBLE_NOTE_GAP_MS; @@ -240,9 +241,9 @@ void Rtttl::loop() { this->samples_sent_ = 0; this->samples_gap_ = 0; this->samples_per_wave_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); + this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); + this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; } if (this->output_freq_ != 0) { // make sure there is enough samples to add a full last sinus. @@ -279,7 +280,7 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 0; int bpm = 63; - uint8_t num; + uint16_t num; // Get name this->position_ = this->rtttl_.find(':'); @@ -395,7 +396,7 @@ void Rtttl::finish_() { sample[0].right = 0; sample[1].left = 0; sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 6f5df07766b..4d4a652c517 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -46,8 +46,8 @@ class Rtttl : public Component { } protected: - inline uint8_t get_integer_() { - uint8_t ret = 0; + inline uint16_t get_integer_() { + uint16_t ret = 0; while (isdigit(this->rtttl_[this->position_])) { ret = (ret * 10) + (this->rtttl_[this->position_++] - '0'); } @@ -87,7 +87,7 @@ class Rtttl : public Component { #ifdef USE_OUTPUT /// The output to write the sound to. - output::FloatOutput *output_; + output::FloatOutput *output_{nullptr}; #endif // USE_OUTPUT #ifdef USE_SPEAKER From 5a1d6428b20163de46cc428246002ac1dd845b4e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:35:27 -0500 Subject: [PATCH 0227/2056] [hmc5883l] Fix wrong gain for 88uT range (#14281) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/hmc5883l/hmc5883l.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index b62381a287f..bee52821255 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -95,7 +95,7 @@ void HMC5883LComponent::update() { float mg_per_bit; switch (this->range_) { case HMC5883L_RANGE_88_UT: - mg_per_bit = 0.073f; + mg_per_bit = 0.73f; break; case HMC5883L_RANGE_130_UT: mg_per_bit = 0.92f; From 8479664df104b85ab3bce2e9c6ca3377ed02eb24 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:32:02 -0500 Subject: [PATCH 0228/2056] [sensor] Fix delta filter percentage mode regression (#14302) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/sensor/__init__.py | 2 +- .../fixtures/sensor_filters_delta.yaml | 40 +++++++++++++++++++ .../integration/test_sensor_filters_delta.py | 27 ++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ebbe0fbccca..03784ba76b3 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -603,7 +603,7 @@ DELTA_SCHEMA = cv.Any( def _get_delta(value): if isinstance(value, str): assert value.endswith("%") - return 0.0, float(value[:-1]) + return 0.0, float(value[:-1]) / 100.0 return value, 0.0 diff --git a/tests/integration/fixtures/sensor_filters_delta.yaml b/tests/integration/fixtures/sensor_filters_delta.yaml index 19bd2d5ca48..2494a430dac 100644 --- a/tests/integration/fixtures/sensor_filters_delta.yaml +++ b/tests/integration/fixtures/sensor_filters_delta.yaml @@ -28,6 +28,11 @@ sensor: id: source_sensor_4 accuracy_decimals: 1 + - platform: template + name: "Source Sensor 5" + id: source_sensor_5 + accuracy_decimals: 1 + - platform: copy source_id: source_sensor_1 name: "Filter Min" @@ -69,6 +74,13 @@ sensor: filters: - delta: 0 + - platform: copy + source_id: source_sensor_5 + name: "Filter Percentage" + id: filter_percentage + filters: + - delta: 50% + script: - id: test_filter_min then: @@ -154,6 +166,28 @@ script: id: source_sensor_4 state: 2.0 + - id: test_filter_percentage + then: + - sensor.template.publish: + id: source_sensor_5 + state: 100.0 + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 120.0 # Filtered out (delta=20, need >50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 160.0 # Passes (delta=60 > 50% of 100=50) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 200.0 # Filtered out (delta=40, need >50% of 160=80) + - delay: 20ms + - sensor.template.publish: + id: source_sensor_5 + state: 250.0 # Passes (delta=90 > 80) + button: - platform: template name: "Test Filter Min" @@ -178,3 +212,9 @@ button: id: btn_filter_zero_delta on_press: - script.execute: test_filter_zero_delta + + - platform: template + name: "Test Filter Percentage" + id: btn_filter_percentage + on_press: + - script.execute: test_filter_percentage diff --git a/tests/integration/test_sensor_filters_delta.py b/tests/integration/test_sensor_filters_delta.py index c7a26bf9d12..9d0114e0c41 100644 --- a/tests/integration/test_sensor_filters_delta.py +++ b/tests/integration/test_sensor_filters_delta.py @@ -24,12 +24,14 @@ async def test_sensor_filters_delta( "filter_max": [], "filter_baseline_max": [], "filter_zero_delta": [], + "filter_percentage": [], } filter_min_done = loop.create_future() filter_max_done = loop.create_future() filter_baseline_max_done = loop.create_future() filter_zero_delta_done = loop.create_future() + filter_percentage_done = loop.create_future() def on_state(state: EntityState) -> None: if not isinstance(state, SensorState) or state.missing_state: @@ -66,6 +68,12 @@ async def test_sensor_filters_delta( and not filter_zero_delta_done.done() ): filter_zero_delta_done.set_result(True) + elif ( + sensor_name == "filter_percentage" + and len(sensor_values[sensor_name]) == 3 + and not filter_percentage_done.done() + ): + filter_percentage_done.set_result(True) async with ( run_compiled(yaml_config), @@ -80,6 +88,7 @@ async def test_sensor_filters_delta( "filter_max": "Filter Max", "filter_baseline_max": "Filter Baseline Max", "filter_zero_delta": "Filter Zero Delta", + "filter_percentage": "Filter Percentage", }, ) @@ -98,13 +107,14 @@ async def test_sensor_filters_delta( "Test Filter Max": "filter_max", "Test Filter Baseline Max": "filter_baseline_max", "Test Filter Zero Delta": "filter_zero_delta", + "Test Filter Percentage": "filter_percentage", } buttons = {} for entity in entities: if isinstance(entity, ButtonInfo) and entity.name in button_name_map: buttons[button_name_map[entity.name]] = entity.key - assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}" + assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}" # Test 1: Min sensor_values["filter_min"].clear() @@ -161,3 +171,18 @@ async def test_sensor_filters_delta( assert sensor_values["filter_zero_delta"] == pytest.approx(expected), ( f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}" ) + + # Test 5: Percentage (delta: 50%) + sensor_values["filter_percentage"].clear() + client.button_command(buttons["filter_percentage"]) + try: + await asyncio.wait_for(filter_percentage_done, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Test 5 timed out. Values: {sensor_values['filter_percentage']}" + ) + + expected = [100.0, 160.0, 250.0] + assert sensor_values["filter_percentage"] == pytest.approx(expected), ( + f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}" + ) From 2c749e9dbefa3a3b0caf3b2275d2cadb46ee5165 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:45:13 +1300 Subject: [PATCH 0229/2056] Bump version to 2026.2.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index d41a79b0dc3..2de0460ef1e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.1 +PROJECT_NUMBER = 2026.2.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index b3c15b1e27c..7d15964eab3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.1" +__version__ = "2026.2.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 789da5fdf822a7332adb7e670ff2de9353907bc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:13:44 -0700 Subject: [PATCH 0230/2056] [logger] Mark Logger and LoggerMessageTrigger as final (#14291) --- esphome/components/logger/logger.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 90722ee79c7..263d12b4441 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -141,7 +141,7 @@ enum UARTSelection : uint8_t { * 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables * 3. Avoids the limitations of the fixed FreeRTOS task local storage slots */ -class Logger : public Component { +class Logger final : public Component { public: explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -481,7 +481,7 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger final : public Trigger { public: explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_callback(this, From 478a876b0156763e27605ed22e034fb2ed442a3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:13:51 -0700 Subject: [PATCH 0231/2056] [mdns] Mark MDNSComponent as final (#14290) --- esphome/components/mdns/mdns_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 32f8f16ec1e..13c8ccf2884 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -40,7 +40,7 @@ struct MDNSService { FixedVector txt_records; }; -class MDNSComponent : public Component { +class MDNSComponent final : public Component { public: void setup() override; void dump_config() override; From cced0a82b55d17ae402425c86dfd9654ed4f7d86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:04 -0700 Subject: [PATCH 0232/2056] [ota] Mark OTA backend and component leaf classes as final (#14287) --- esphome/components/esphome/ota/ota_esphome.h | 2 +- esphome/components/http_request/ota/ota_http_request.h | 2 +- esphome/components/ota/ota_backend_arduino_libretiny.h | 2 +- esphome/components/ota/ota_backend_arduino_rp2040.h | 2 +- esphome/components/ota/ota_backend_esp8266.h | 2 +- esphome/components/ota/ota_backend_esp_idf.h | 2 +- esphome/components/ota/ota_backend_host.h | 2 +- esphome/components/web_server/ota/ota_web_server.h | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c9e89c82ba0..53715cfe6a3 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -12,7 +12,7 @@ namespace esphome { /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. -class ESPHomeOTAComponent : public ota::OTAComponent { +class ESPHomeOTAComponent final : public ota::OTAComponent { public: enum class OTAState : uint8_t { IDLE, diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index e3f1a4aa90e..6d39b0d466c 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -22,7 +22,7 @@ enum OtaHttpRequestError : uint8_t { OTA_CONNECTION_ERROR = 0x12, }; -class OtaHttpRequestComponent : public ota::OTAComponent, public Parented { +class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 6d9b7a96d59..8f9d268eec6 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -7,7 +7,7 @@ namespace esphome { namespace ota { -class ArduinoLibreTinyOTABackend : public OTABackend { +class ArduinoLibreTinyOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index b9e10d506cc..6a708f9c574 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -9,7 +9,7 @@ namespace esphome { namespace ota { -class ArduinoRP2040OTABackend : public OTABackend { +class ArduinoRP2040OTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index a9d6dd2ccc5..52f657f0065 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -12,7 +12,7 @@ namespace esphome::ota { /// OTA backend for ESP8266 using native SDK functions. /// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM /// by not having a global Update object in .bss. -class ESP8266OTABackend : public OTABackend { +class ESP8266OTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 764010e6142..7f7f6115c50 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -10,7 +10,7 @@ namespace esphome { namespace ota { -class IDFOTABackend : public OTABackend { +class IDFOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index ae7d0cb0b30..5a2dcfcf39b 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -7,7 +7,7 @@ namespace esphome::ota { /// Stub OTA backend for host platform - allows compilation but does not implement OTA. /// All operations return error codes immediately. This enables configurations with /// OTA triggers to compile for host platform during development. -class HostOTABackend : public OTABackend { +class HostOTABackend final : public OTABackend { public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h index 53ff99899cd..0857c31c5d9 100644 --- a/esphome/components/web_server/ota/ota_web_server.h +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -9,7 +9,7 @@ namespace esphome::web_server { -class WebServerOTAComponent : public ota::OTAComponent { +class WebServerOTAComponent final : public ota::OTAComponent { public: void setup() override; void dump_config() override; From ee4d67930f11a4af1997190f9dc576db8a0d05c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:16 -0700 Subject: [PATCH 0233/2056] [api] Mark ListEntitiesIterator and InitialStateIterator as final (#14284) --- esphome/components/api/list_entities.h | 2 +- esphome/components/api/subscribe_state.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 90769f9a81e..7d0eb5bb136 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -15,7 +15,7 @@ class APIConnection; return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \ } -class ListEntitiesIterator : public ComponentIterator { +class ListEntitiesIterator final : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 6f8577ca7be..9edf0f0f0cf 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -16,7 +16,7 @@ class APIConnection; return this->client_->send_##entity_type##_state(entity); \ } -class InitialStateIterator : public ComponentIterator { +class InitialStateIterator final : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR From d52f8c9c6f250024f8ce4a353100919cb2b25215 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Feb 2026 20:14:33 -0700 Subject: [PATCH 0234/2056] [web_server] Mark classes as final (#14283) --- esphome/components/web_server/list_entities.cpp | 2 -- esphome/components/web_server/list_entities.h | 3 +-- esphome/components/web_server/web_server.h | 6 +++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 84582980620..ebe7bf44504 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -14,8 +14,6 @@ ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) : web_server_(ws), events_(es) {} #endif -ListEntitiesIterator::~ListEntitiesIterator() {} - #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) { this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator); diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index d0a4fa27256..6a840661097 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -17,14 +17,13 @@ class DeferredUpdateEventSource; #endif class WebServer; -class ListEntitiesIterator : public ComponentIterator { +class ListEntitiesIterator final : public ComponentIterator { public: #ifdef USE_ESP32 ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); #elif defined(USE_ARDUINO) ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif - virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *obj) override; #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 76c1c8b0bd5..64c492f82b7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -107,7 +107,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; using message_generator_t = json::SerializationBuffer<>(WebServer *, void *); class DeferredUpdateEventSourceList; -class DeferredUpdateEventSource : public AsyncEventSource { +class DeferredUpdateEventSource final : public AsyncEventSource { friend class DeferredUpdateEventSourceList; /* @@ -163,7 +163,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); }; -class DeferredUpdateEventSourceList : public std::list { +class DeferredUpdateEventSourceList final : public std::list { protected: void on_client_connect_(DeferredUpdateEventSource *source); void on_client_disconnect_(DeferredUpdateEventSource *source); @@ -187,7 +187,7 @@ class DeferredUpdateEventSourceList : public std::list Date: Wed, 25 Feb 2026 20:14:53 -0700 Subject: [PATCH 0235/2056] [safe_mode] Mark SafeModeComponent and SafeModeTrigger as final (#14282) --- esphome/components/safe_mode/automation.h | 2 +- esphome/components/safe_mode/safe_mode.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 952ed4da331..1d82ac45f14 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -8,7 +8,7 @@ namespace esphome::safe_mode { -class SafeModeTrigger : public Trigger<> { +class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index a4d27c15da7..902b8c415de 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -15,7 +15,7 @@ namespace esphome::safe_mode { constexpr uint32_t RTC_KEY = 233825507UL; /// SafeModeComponent provides a safe way to recover from repeated boot failures -class SafeModeComponent : public Component { +class SafeModeComponent final : public Component { public: bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after); From 6c253f0c71958a182cf1d9c4ad64a10b2b62957f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:40:43 -0500 Subject: [PATCH 0236/2056] [sprinkler] Fix millis overflow and underflow bugs (#14299) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/sprinkler/sprinkler.cpp | 81 +++++++++++----------- esphome/components/sprinkler/sprinkler.h | 4 +- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 9e423c17609..d82d7baaf67 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { + // Use wrapping subtraction so 32-bit millis() rollover is handled correctly: + // (now - start) yields the true elapsed time even across the 49.7-day boundary. uint32_t now = App.get_loop_component_start_time(); - if (now >= this->start_millis_) { // dummy check - switch (this->state_) { - case STARTING: - if (now > (this->start_millis_ + this->start_delay_)) { - this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state - } - break; + switch (this->state_) { + case STARTING: + if ((now - *this->start_millis_) > this->start_delay_) { + this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state + } + break; - case ACTIVE: - if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { - this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down - } - break; + case ACTIVE: + if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) { + this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down + } + break; - case STOPPING: - if (now > (this->stop_millis_ + this->stop_delay_)) { - this->kill_(); // stop_delay_has been exceeded, ensure all valves are off - } - break; + case STOPPING: + if ((now - *this->stop_millis_) > this->stop_delay_) { + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + } + break; - default: - break; - } - } else { // perhaps millis() rolled over...or something else is horribly wrong! - this->stop(); // bail out (TODO: handle this highly unlikely situation better...) + default: + break; } } @@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (this->state_ != IDLE) { // Only kill if not already idle this->kill_(); // ensure everything is off before we let go! } - this->state_ = IDLE; // reset state - this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it - this->start_millis_ = 0; // reset because (new) valve has not been started yet - this->stop_millis_ = 0; // reset because (new) valve has not been started yet - this->valve_ = valve; // finally, set the pointer to the new valve + this->state_ = IDLE; // reset state + this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it + this->start_millis_.reset(); // reset because (new) valve has not been started yet + this->stop_millis_.reset(); // reset because (new) valve has not been started yet + this->valve_ = valve; // finally, set the pointer to the new valve } } @@ -162,7 +160,7 @@ void SprinklerValveOperator::start() { } else { this->run_(); // there is no start_delay_, so just start the pump and valve } - this->stop_millis_ = 0; + this->stop_millis_.reset(); this->start_millis_ = millis(); // save the time the start request was made } @@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() { uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; } uint32_t SprinklerValveOperator::time_remaining() { - if (this->start_millis_ == 0) { + if (!this->start_millis_.has_value()) { return this->run_duration(); // hasn't been started yet } - if (this->stop_millis_) { - if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) { + if (this->stop_millis_.has_value()) { + uint32_t elapsed = *this->stop_millis_ - *this->start_millis_; + if (elapsed >= this->start_delay_ + this->run_duration_) { return 0; // valve was active for more than its configured duration, so we are done - } else { - // we're stopped; return time remaining - return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000; } + if (elapsed <= this->start_delay_) { + return this->run_duration_ / 1000; // stopped during start delay, full run duration remains + } + return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000; } - auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_; - if (completed_millis > millis()) { - return (completed_millis - millis()) / 1000; // running now + uint32_t elapsed = millis() - *this->start_millis_; + uint32_t total_duration = this->start_delay_ + this->run_duration_; + if (elapsed < total_duration) { + return (total_duration - elapsed) / 1000; // running now } return 0; // run completed } @@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional repeat) { if (this->repeat_number_ == nullptr) { return; } - if (this->repeat_number_->state == repeat.value()) { + if (this->repeat_number_->state == repeat.value_or(0)) { return; } auto call = this->repeat_number_->make_call(); @@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional valve_number, optional void Sprinkler::queue_valve(optional valve_number, optional run_duration) { if (valve_number.has_value()) { if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { - SprinklerQueueItem item{valve_number.value(), run_duration.value()}; + SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)}; this->queued_valves_.insert(this->queued_valves_.begin(), item); ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0), run_duration.value_or(0)); @@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { } } - if (incomplete_valve_count >= enabled_valve_count) { + if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) { incomplete_valve_count--; } if (incomplete_valve_count) { diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index a3cdef5b1a0..2598a5606a7 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -141,8 +141,8 @@ class SprinklerValveOperator { uint32_t start_delay_{0}; uint32_t stop_delay_{0}; uint32_t run_duration_{0}; - uint64_t start_millis_{0}; - uint64_t stop_millis_{0}; + optional start_millis_{}; + optional stop_millis_{}; Sprinkler *controller_{nullptr}; SprinklerValve *valve_{nullptr}; SprinklerState state_{IDLE}; From a05d0202e696b5498787b3b895acd71915844ac5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 09:21:27 -0700 Subject: [PATCH 0237/2056] [core] ESP32: massively reduce main loop socket polling overhead by replacing select() (#14249) --- esphome/components/socket/__init__.py | 8 +- esphome/components/socket/socket.h | 2 +- esphome/core/application.cpp | 102 ++++++--- esphome/core/application.h | 47 ++-- esphome/core/lwip_fast_select.c | 216 ++++++++++++++++++ esphome/core/lwip_fast_select.h | 33 +++ .../socket/test_wake_loop_threadsafe.py | 39 ++++ 7 files changed, 397 insertions(+), 50 deletions(-) create mode 100644 esphome/core/lwip_fast_select.c create mode 100644 esphome/core/lwip_fast_select.h diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index d82f0c7abad..5f4d04eb441 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -134,6 +134,8 @@ def require_wake_loop_threadsafe() -> None: IMPORTANT: This is for background thread context only, NOT ISR context. Socket operations are not safe to call from ISR handlers. + On ESP32, FreeRTOS task notifications are used instead (no socket needed). + Example: from esphome.components import socket @@ -147,8 +149,10 @@ def require_wake_loop_threadsafe() -> None: ): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") - # Consume 1 socket for the shared wake notification socket - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) + if not CORE.is_esp32: + # Only non-ESP32 platforms need a UDP socket for wake notifications. + # ESP32 uses FreeRTOS task notifications instead (no socket needed). + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index c0098d689a1..a771e2fe1a9 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -71,7 +71,7 @@ class Socket { int get_fd() const { return -1; } #endif - /// Check if socket has data ready to read + /// Check if socket has data ready to read. Must only be called from the main loop thread. /// For select()-based sockets: non-virtual, checks Application's select() results /// For LWIP raw TCP sockets: virtual, checks internal buffer state #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 6a7683a987d..fd6a14b50fa 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,6 +9,9 @@ #endif #ifdef USE_ESP32 #include +#include "esphome/core/lwip_fast_select.h" +#include +#include #endif #include "esphome/core/version.h" #include "esphome/core/hal.h" @@ -144,8 +147,14 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - // Set up wake socket for waking main loop from tasks +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) + // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. + // Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used + // unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled. + esphome_lwip_fast_select_init(); +#endif +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) + // Set up wake socket for waking main loop from tasks (non-ESP32 only) this->setup_wake_loop_threadsafe_(); #endif @@ -523,7 +532,7 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -576,11 +585,15 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); +#ifdef USE_ESP32 + // Hook the socket's netconn callback for instant wake on receive events + esphome_lwip_hook_socket(fd); +#else this->socket_fds_changed_ = true; - if (fd > this->max_fd_) { this->max_fd_ = fd; } +#endif return true; } @@ -595,12 +608,14 @@ void Application::unregister_socket_fd(int fd) { if (this->socket_fds_[i] != fd) continue; - // Swap with last element and pop - O(1) removal since order doesn't matter + // Swap with last element and pop - O(1) removal since order doesn't matter. + // No need to unhook the netconn callback on ESP32 — all LwIP sockets share + // the same static event_callback, and the socket will be closed by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); +#ifndef USE_ESP32 this->socket_fds_changed_ = true; - // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { this->max_fd_ = -1; @@ -609,6 +624,7 @@ void Application::unregister_socket_fd(int fd) { this->max_fd_ = sock_fd; } } +#endif return; } } @@ -616,16 +632,41 @@ void Application::unregister_socket_fd(int fd) { #endif void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run - // since select() with 0 timeout only polls without yielding. -#ifdef USE_SOCKET_SELECT_SUPPORT - if (!this->socket_fds_.empty()) { + // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) + // ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket). + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (int fd : this->socket_fds_) { + if (esphome_lwip_socket_has_data(fd)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); + +#elif defined(USE_SOCKET_SELECT_SUPPORT) + // Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform). + // ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32 + // use LwIP under the hood, so the fast path handles all ESP32 socket implementations. + if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed - if (this->socket_fds_changed_) { + if (this->socket_fds_changed_) [[unlikely]] { FD_ZERO(&this->base_read_fds_); - // fd bounds are already validated in register_socket_fd() or guaranteed by platform design: - // - ESP32: LwIP guarantees fd < FD_SETSIZE by design (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS) - // - Other platforms: register_socket_fd() validates fd < FD_SETSIZE + // fd bounds are validated in register_socket_fd() for (int fd : this->socket_fds_) { FD_SET(fd, &this->base_read_fds_); } @@ -641,7 +682,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; // Call select with timeout -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || (defined(USE_ESP32) && defined(USE_SOCKET_IMPL_BSD_SOCKETS)) +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); #else int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); @@ -651,19 +692,18 @@ void Application::yield_with_select_(uint32_t delay_ms) { // ret < 0: error (except EINTR which is normal) // ret > 0: socket(s) have data ready - normal and expected // ret == 0: timeout occurred - normal and expected - if (ret < 0 && errno != EINTR) { - // Actual error - log and fall back to delay - ESP_LOGW(TAG, "select() failed with errno %d", errno); - delay(delay_ms); + if (ret >= 0 || errno == EINTR) [[likely]] { + // Yield if zero timeout since select(0) only polls without yielding + if (delay_ms == 0) [[unlikely]] { + yield(); + } + return; } - // When delay_ms is 0, we need to yield since select(0) doesn't yield - if (delay_ms == 0) { - yield(); - } - } else { - // No sockets registered, use regular delay - delay(delay_ms); + // select() error - log and fall through to delay() + ESP_LOGW(TAG, "select() failed with errno %d", errno); } + // No sockets registered or select() failed - use regular delay + delay(delay_ms); #elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) // No select support but can wake on socket activity via esp_schedule() socket::socket_delay(delay_ms); @@ -676,6 +716,14 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + +#ifdef USE_ESP32 +void Application::wake_loop_threadsafe() { + // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) + esphome_lwip_wake_main_loop(); +} +#else // !USE_ESP32 + void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); @@ -742,6 +790,8 @@ void Application::wake_loop_threadsafe() { lwip_send(this->wake_socket_fd_, &dummy, 1, 0); } } +#endif // USE_ESP32 + #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) void Application::get_build_time_string(std::span buffer) { diff --git a/esphome/core/application.h b/esphome/core/application.h index cd275bb97fc..f5df5e7bdf7 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,10 +24,14 @@ #endif #ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_ESP32 +#include "esphome/core/lwip_fast_select.h" +#else #include #ifdef USE_WAKE_LOOP_THREADSAFE #include #endif +#endif #endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR @@ -491,15 +495,12 @@ class Application { /// @return true if registration was successful, false if fd exceeds limits bool register_socket_fd(int fd); void unregister_socket_fd(int fd); - /// Check if there's data available on a socket without blocking - /// This function is thread-safe for reading, but should be called after select() has run - /// The read_fds_ is only modified by select() in the main loop - bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); } #ifdef USE_WAKE_LOOP_THREADSAFE - /// Wake the main event loop from a FreeRTOS task - /// Thread-safe, can be called from task context to immediately wake select() - /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + /// Wake the main event loop from another FreeRTOS task. + /// Thread-safe, but must only be called from task context (NOT ISR-safe). + /// On ESP32: uses xTaskNotifyGive (<1 us) + /// On other platforms: uses UDP loopback socket void wake_loop_threadsafe(); #endif #endif @@ -510,10 +511,14 @@ class Application { #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Safe because: fd was validated in register_socket_fd() at registration time, - /// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded). - /// FD_ISSET may include its own upper bounds check depending on platform. + /// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket() + /// which has no refcount; safe only because the main loop owns socket lifetime + /// (creates, reads, and closes sockets on the same thread). +#ifdef USE_ESP32 + bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } +#else bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } +#endif #endif void register_component_(Component *comp); @@ -541,7 +546,7 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -571,7 +576,7 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif #endif @@ -584,7 +589,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -600,14 +605,14 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - // Variable-sized members +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) + // Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly) + fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes - fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ #endif // StaticVectors (largest members - contain actual array data inline) @@ -694,7 +699,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -704,8 +709,8 @@ static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; inline void Application::drain_wake_notifications_() { // Called from main loop to drain any pending wake notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + // Must check is_socket_ready_() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready_(this->wake_socket_fd_)) { char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; // Drain all pending notifications with non-blocking reads // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK @@ -716,6 +721,6 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) } // namespace esphome diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c new file mode 100644 index 00000000000..70a6482d480 --- /dev/null +++ b/esphome/core/lwip_fast_select.c @@ -0,0 +1,216 @@ +// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. +// +// This must be a .c file (not .cpp) because: +// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers +// 2. The netconn callback is a C function pointer +// +// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc. +// +// Thread safety analysis +// ====================== +// Three threads interact with this code: +// 1. Main loop task — calls init, has_data, hook +// 2. LwIP TCP/IP task — calls event_callback (reads s_original_callback; writes rcvevent +// via the original callback under SYS_ARCH_PROTECT/UNPROTECT mutex) +// 3. Background tasks — call wake_main_loop +// +// LwIP source references (ESP-IDF v5.5.2, commit 30aaf64524): +// sockets.c: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/api/sockets.c +// - event_callback (static, same for all sockets): L327 +// - DEFAULT_SOCKET_EVENTCB = event_callback: L328 +// - tryget_socket_unconn_nouse (direct array lookup): L450 +// - lwip_socket_dbg_get_socket (thin wrapper): L461 +// - All socket types use DEFAULT_SOCKET_EVENTCB: L1741, L1748, L1759 +// - event_callback definition: L2538 +// - SYS_ARCH_PROTECT before rcvevent switch: L2578 +// - sock->rcvevent++ (NETCONN_EVT_RCVPLUS case): L2582 +// - SYS_ARCH_UNPROTECT after switch: L2615 +// sys.h: https://github.com/espressif/esp-idf/blob/30aaf64524/components/lwip/lwip/src/include/lwip/sys.h +// - SYS_ARCH_PROTECT calls sys_arch_protect(): L495 +// - SYS_ARCH_UNPROTECT calls sys_arch_unprotect(): L506 +// (ESP-IDF implements sys_arch_protect/unprotect as FreeRTOS mutex lock/unlock) +// +// Socket slot lifetime +// ==================== +// This code reads struct lwip_sock fields without SYS_ARCH_PROTECT. The safety +// argument requires that the slot cannot be freed while we read it. +// +// In LwIP, the socket table is a static array and slots are only freed via: +// lwip_close() -> lwip_close_internal() -> free_socket_free_elements() -> free_socket() +// The TCP/IP thread does NOT call free_socket(). On link loss, RST, or timeout +// it frees the TCP PCB and signals the netconn (rcvevent++ to indicate EOF), but +// the netconn and lwip_sock slot remain allocated until the application calls +// lwip_close(). ESPHome removes the fd from the monitored set before calling +// lwip_close(). +// +// Therefore lwip_socket_dbg_get_socket(fd) plus a volatile read of rcvevent +// (to prevent compiler reordering or caching) is safe as long as the application +// is single-writer for close. ESPHome guarantees this by design: all socket +// create/read/close happens on the main loop. fd numbers are not reused while +// the slot remains allocated, and the slot remains allocated until lwip_close(). +// Any change in LwIP that allows free_socket() to be called outside lwip_close() +// would invalidate this assumption. +// +// LwIP source references for slot lifetime: +// sockets.c (same commit as above): +// - alloc_socket (slot allocation): L419 +// - free_socket (slot deallocation): L384 +// - free_socket_free_elements (called from lwip_close_internal): L393 +// - lwip_close_internal (only caller of free_socket_free_elements): L2355 +// - lwip_close (only caller of lwip_close_internal): L2450 +// +// Shared state and safety rationale: +// +// s_main_loop_task (TaskHandle_t, 4 bytes): +// Written once by main loop in init(). Read by TCP/IP thread (in callback) +// and background tasks (in wake). +// Safe: write-once-then-read pattern. Socket hooks may run before init(), +// but the NULL check on s_main_loop_task in the callback provides correct +// degraded behavior — notifications are simply skipped until init() completes. +// +// s_original_callback (netconn_callback, 4-byte function pointer): +// Written by main loop in hook_socket() (only when NULL — set once). +// Read by TCP/IP thread in esphome_socket_event_callback(). +// Safe: set-once pattern. The first hook_socket() captures the original callback. +// All subsequent hooks see it already set and skip the write. The TCP/IP thread +// only reads this after the callback pointer has been swapped (which happens after +// the write), so it always sees the initialized value. +// +// sock->conn->callback (netconn_callback, 4-byte function pointer): +// Written by main loop in hook_socket(). Never restored — all LwIP sockets share +// the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently. +// Read by TCP/IP thread when invoking the callback. +// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32). +// The TCP/IP thread will see either the old or new pointer atomically — never a +// torn value. Both the wrapper and original callbacks are valid at all times +// (the wrapper itself calls the original), so either value is correct. +// +// sock->rcvevent (s16_t, 2 bytes): +// Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT. +// Read by main loop in has_data() via volatile cast. +// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally +// uses a critical section with memory barrier (rsync on dual-core Xtensa; on +// single-core builds the spinlock is compiled out, but cross-core visibility is +// not an issue). The volatile cast prevents the compiler +// from caching the read. Aligned 16-bit reads are single-instruction loads on +// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values. +// +// FreeRTOS task notification value: +// Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks +// (xTaskNotifyGive in wake_main_loop). Read by main loop (ulTaskNotifyTake). +// Safe: FreeRTOS notification APIs are thread-safe by design (use internal +// critical sections). Multiple concurrent xTaskNotifyGive calls are safe — +// the notification count simply increments. + +#ifdef USE_ESP32 + +// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. +#include +#include +#include +#include + +#include "esphome/core/lwip_fast_select.h" + +#include + +// Compile-time verification of thread safety assumptions. +// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic. +// These asserts ensure our cross-thread shared state meets those requirements. + +// Pointer types must fit in a single 32-bit store (atomic write) +_Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access"); +_Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access"); + +// rcvevent must fit in a single atomic read +_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); + +// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V. +// Misaligned access would not be atomic even if the size is <= 4 bytes. +_Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0, + "netconn.callback must be naturally aligned for atomic access"); +_Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0, + "lwip_sock.rcvevent must be naturally aligned for atomic access"); + +// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. +static TaskHandle_t s_main_loop_task = NULL; + +// Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. +static netconn_callback s_original_callback = NULL; + +// Wrapper callback: calls original event_callback + notifies main loop task. +// Called from LwIP's TCP/IP thread when socket events occur (task context, not ISR). +static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt evt, u16_t len) { + // Call original LwIP event_callback first — updates rcvevent/sendevent/errevent, + // signals any select() waiters. This preserves all LwIP behavior. + // s_original_callback is always valid here: hook_socket() sets it before swapping + // the callback pointer, so this wrapper cannot run until it's initialized. + s_original_callback(conn, evt, len); + // Wake the main loop task if sleeping in ulTaskNotifyTake(). + // Only notify on receive events to avoid spurious wakeups from send-ready events. + // NETCONN_EVT_ERROR is deliberately omitted: LwIP signals errors via RCVPLUS + // (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions + // already wake the main loop through the RCVPLUS path. + if (evt == NETCONN_EVT_RCVPLUS) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + xTaskNotifyGive(task); + } + } +} + +void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); } + +// lwip_socket_dbg_get_socket() is a thin wrapper around the static +// tryget_socket_unconn_nouse() — a direct array lookup without the refcount +// that get_socket()/done_socket() uses. This is safe because: +// 1. The only path to free_socket() is lwip_close(), called exclusively from the main loop +// 2. The TCP/IP thread never frees socket slots (see "Socket slot lifetime" above) +// 3. Both has_data() reads and lwip_close() run on the main loop — no concurrent free +// If lwip_socket_dbg_get_socket() were ever removed, we could fall back to lwip_select(). +// Returns the sock only if both the sock and its netconn are valid, NULL otherwise. +static inline struct lwip_sock *get_sock(int fd) { + struct lwip_sock *sock = lwip_socket_dbg_get_socket(fd); + if (sock == NULL || sock->conn == NULL) + return NULL; + return sock; +} + +bool esphome_lwip_socket_has_data(int fd) { + struct lwip_sock *sock = get_sock(fd); + if (sock == NULL) + return false; + // volatile prevents the compiler from caching/reordering this cross-thread read. + // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a + // FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is + // visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on + // Xtensa/RISC-V and cannot produce torn values. + return *(volatile s16_t *) &sock->rcvevent > 0; +} + +void esphome_lwip_hook_socket(int fd) { + struct lwip_sock *sock = get_sock(fd); + if (sock == NULL) + return; + + // Save original callback once — all LwIP sockets share the same static event_callback + // (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM). + if (s_original_callback == NULL) { + s_original_callback = sock->conn->callback; + } + + // Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write). + // TCP/IP thread sees either old or new pointer — both are valid. + sock->conn->callback = esphome_socket_event_callback; +} + +// Wake the main loop from another FreeRTOS task. NOT ISR-safe. +void esphome_lwip_wake_main_loop(void) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +#endif // USE_ESP32 diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h new file mode 100644 index 00000000000..73a89fdc3db --- /dev/null +++ b/esphome/core/lwip_fast_select.h @@ -0,0 +1,33 @@ +#pragma once + +// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Initialize fast select — must be called from the main loop task during setup(). +/// Saves the current task handle for xTaskNotifyGive() wake notifications. +void esphome_lwip_fast_select_init(void); + +/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket). +/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that +/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime: +/// both has_data reads and socket close/unregister happen on the main loop thread. +bool esphome_lwip_socket_has_data(int fd); + +/// Hook a socket's netconn callback to notify the main loop task on receive events. +/// Wraps the original event_callback with one that also calls xTaskNotifyGive(). +/// Must be called from the main loop after socket creation. +void esphome_lwip_hook_socket(int fd); + +/// Wake the main loop task from another FreeRTOS task — costs <1 us. +/// NOT ISR-safe — must only be called from task context. +void esphome_lwip_wake_main_loop(void); + +#ifdef __cplusplus +} +#endif diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index a40b6068a8a..28b4ee564fe 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -1,9 +1,21 @@ from esphome.components import socket +from esphome.const import ( + KEY_CORE, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) from esphome.core import CORE +def _setup_platform(platform=PLATFORM_ESP8266) -> None: + """Set up CORE.data with a platform for testing.""" + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} + + def test_require_wake_loop_threadsafe__first_call() -> None: """Test that first call sets up define and consumes socket.""" + _setup_platform() CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() @@ -32,6 +44,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None: def test_require_wake_loop_threadsafe__multiple_calls() -> None: """Test that multiple calls only set up once.""" + _setup_platform() # Call three times CORE.config = {"openthread": True} socket.require_wake_loop_threadsafe() @@ -75,3 +88,29 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) assert "socket.wake_loop_threadsafe" not in udp_consumers assert udp_consumers == initial_udp + + +def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: + """Test that ESP32 uses task notifications instead of UDP socket.""" + _setup_platform(PLATFORM_ESP32) + CORE.config = {"wifi": True} + socket.require_wake_loop_threadsafe() + + # Verify the define was added + assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + # Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications) + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert "socket.wake_loop_threadsafe" not in udp_consumers + + +def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None: + """Test that non-ESP32 platforms consume a UDP socket for wake notifications.""" + _setup_platform(PLATFORM_ESP8266) + CORE.config = {"wifi": True} + socket.require_wake_loop_threadsafe() + + # Verify UDP socket was consumed + udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) + assert udp_consumers.get("socket.wake_loop_threadsafe") == 1 From be000eab4e0cb4f9eaa3807b069c143c4eeee5b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 11:02:52 -0700 Subject: [PATCH 0238/2056] [ci] Add undocumented C++ API change checkbox and auto-label (#14317) --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++-- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1ef3bd8225..965b186c31e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,8 +6,9 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Developer breaking change (an API change that could break external components) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change) +- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api) +- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations) - [ ] Code quality improvements to existing code or addition of tests - [ ] Other diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index bd60d8c7668..8c3a62cf194 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -27,6 +27,7 @@ module.exports = { 'new-feature', 'breaking-change', 'developer-breaking-change', + 'undocumented-api-change', 'code-quality', 'deprecated-component' ], diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index f502a856664..a45a84f2196 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -238,6 +238,7 @@ async function detectPRTemplateCheckboxes(context) { { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, + { pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; From ae16c3bae7450db7bba0b1c06755022e5b2696e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 11:25:36 -0700 Subject: [PATCH 0239/2056] Add socket compile tests for libretiny platforms (#14314) --- tests/components/socket/test.bk72xx-ard.yaml | 1 + tests/components/socket/test.ln882x-ard.yaml | 1 + tests/components/socket/test.rtl87xx-ard.yaml | 1 + 3 files changed, 3 insertions(+) create mode 100644 tests/components/socket/test.bk72xx-ard.yaml create mode 100644 tests/components/socket/test.ln882x-ard.yaml create mode 100644 tests/components/socket/test.rtl87xx-ard.yaml diff --git a/tests/components/socket/test.bk72xx-ard.yaml b/tests/components/socket/test.bk72xx-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/socket/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.ln882x-ard.yaml b/tests/components/socket/test.ln882x-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/socket/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/socket/test.rtl87xx-ard.yaml b/tests/components/socket/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/socket/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 1912dcf03da6f0d1590f3708cf8ddad9b404ef04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 12:07:42 -0700 Subject: [PATCH 0240/2056] [core] Use placement new for global Application instance (#14052) --- esphome/core/application.cpp | 10 +++++++++- esphome/core/config.py | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index fd6a14b50fa..a9753da1b5f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -713,7 +713,15 @@ void Application::yield_with_select_(uint32_t delay_ms) { #endif } -Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// App storage — asm label shares the linker symbol with "extern Application App". +// char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. +// Constructed via placement new in the generated setup(). +#ifndef __GXX_ABI_VERSION +#error "Application placement new requires Itanium C++ ABI (GCC/Clang)" +#endif +static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/core/config.py b/esphome/core/config.py index 21ed8ced1a1..215432835a0 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -512,6 +512,9 @@ async def to_code(config: ConfigType) -> None: cg.add_global(cg.RawExpression("using std::min")) cg.add_global(cg.RawExpression("using std::max")) + # Construct App via placement new — see application.cpp for storage details + cg.add_global(cg.RawStatement("#include ")) + cg.add(cg.RawExpression("new (&App) Application()")) cg.add( cg.App.pre_setup( config[CONF_NAME], From 4c3bb1596e876259f496cafc682dfb75e34d3089 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 12:14:46 -0700 Subject: [PATCH 0241/2056] [wifi] Use memcpy-based insertion sort for scan results (#13960) --- esphome/components/wifi/wifi_component.cpp | 50 ++++++++++++++++++++-- esphome/components/wifi/wifi_component.h | 9 ++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d5d04193957..1e6961b8bde 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -1334,20 +1335,61 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) +// +// Uses raw memcpy instead of copy assignment to avoid CompactString's +// destructor/constructor overhead (heap delete[]/new[] for long SSIDs). +// Copy assignment calls ~CompactString() then placement-new for every shift, +// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+ +// networks (e.g., captive portal showing full scan results), this caused +// event loop blocking from hundreds of heap operations in a tight loop. +// +// This is safe because we're permuting elements within the same array — +// each slot is overwritten exactly once, so no ownership duplication occurs. +// All members of WiFiScanResult are either trivially copyable (bssid, channel, +// rssi, priority, flags) or CompactString, which stores either inline data or +// a heap pointer — never a self-referential pointer (unlike std::string's SSO +// on some implementations). This was not possible before PR#13472 replaced +// std::string with CompactString, since std::string's internal layout is +// implementation-defined and may use self-referential pointers. +// +// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for +// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee. template static void insertion_sort_scan_results(VectorType &results) { + // memcpy-based sort requires no self-referential pointers or virtual dispatch. + // These static_asserts guard the assumptions. If any fire, the memcpy sort + // must be reviewed for safety before updating the expected values. + // + // No vtable pointers (memcpy would corrupt vptr) + static_assert(!std::is_polymorphic::value, "WiFiScanResult must not have vtable"); + static_assert(!std::is_polymorphic::value, "CompactString must not have vtable"); + // Standard layout ensures predictable memory layout with no virtual bases + // and no mixed-access-specifier reordering + static_assert(std::is_standard_layout::value, "WiFiScanResult must be standard layout"); + static_assert(std::is_standard_layout::value, "CompactString must be standard layout"); + // Size checks catch added/removed fields that may need safety review + static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe"); + static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe"); + // Alignment must match for reinterpret_cast of key_buf to be valid + static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t"); const size_t size = results.size(); + constexpr size_t elem_size = sizeof(WiFiScanResult); + // Suppress warnings for intentional memcpy on non-trivially-copyable type. + // Safety is guaranteed by the static_asserts above and the permutation invariant. + // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation) + auto *memcpy_fn = &memcpy; for (size_t i = 1; i < size; i++) { - // Make a copy to avoid issues with move semantics during comparison - WiFiScanResult key = results[i]; + alignas(WiFiScanResult) uint8_t key_buf[elem_size]; + memcpy_fn(key_buf, &results[i], elem_size); + const auto &key = *reinterpret_cast(key_buf); int32_t j = i - 1; // Move elements that are worse than key to the right // For stability, we only move if key is strictly better than results[j] while (j >= 0 && wifi_scan_result_is_better(key, results[j])) { - results[j + 1] = results[j]; + memcpy_fn(&results[j + 1], &results[j], elem_size); j--; } - results[j + 1] = key; + memcpy_fn(&results[j + 1], key_buf, elem_size); } } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 984930c80cd..63c7039f21a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -10,6 +10,7 @@ #include #include +#include #include #ifdef USE_LIBRETINY @@ -223,6 +224,14 @@ class CompactString { }; static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); +// CompactString is not trivially copyable (non-trivial destructor/copy for heap case). +// However, its layout has no self-referential pointers: storage_[] contains either inline +// data or an external heap pointer — never a pointer to itself. This is unlike libstdc++ +// std::string SSO where _M_p points to _M_local_buf within the same object. +// This property allows memcpy-based permutation sorting where each element ends up in +// exactly one slot (no ownership duplication). These asserts document that layout property. +static_assert(std::is_standard_layout::value, "CompactString must be standard layout"); +static_assert(!std::is_polymorphic::value, "CompactString must not have vtable"); class WiFiAP { friend class WiFiComponent; From c149be20fcf8d2eecc8a4c518673f2bb8cc1bc42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:31:47 +0000 Subject: [PATCH 0242/2056] Bump aioesphomeapi from 44.1.0 to 44.2.0 (#14324) 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 d22097b3caa..95e3710f9ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.1.7 esphome-dashboard==20260210.0 -aioesphomeapi==44.1.0 +aioesphomeapi==44.2.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 8da1e3ce21de2e720294c1ff05d3021a07b0ddb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:32:53 +0000 Subject: [PATCH 0243/2056] Bump ruff from 0.15.2 to 0.15.3 (#14323) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07d02e0e3c9..d70dd9d0e14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.2 + rev: v0.15.3 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 3e5dc8a90ca..88a38ffa995 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.2 # also change in .pre-commit-config.yaml when updating +ruff==0.15.3 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d325890148091a682c31185ccf1c5d3864e8c303 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:48:05 -0500 Subject: [PATCH 0244/2056] [cc1101] Transition through IDLE in begin_tx/begin_rx for reliable state changes (#14321) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/cc1101/cc1101.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index b6973da78d1..51aa88b8f72 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -242,6 +242,9 @@ void CC1101Component::begin_tx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } + // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can + // block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration + this->enter_idle_(); if (!this->enter_tx_()) { ESP_LOGW(TAG, "Failed to enter TX state!"); } @@ -252,6 +255,8 @@ void CC1101Component::begin_rx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); } + // Transition through IDLE to ensure FS_AUTOCAL calibration occurs + this->enter_idle_(); if (!this->enter_rx_()) { ESP_LOGW(TAG, "Failed to enter RX state!"); } From e8b45e53fd4020ecc65500e9df8c0c7145b4f1fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 13:02:25 -0700 Subject: [PATCH 0245/2056] [libretiny] Use -Os optimization for ESPHome source on BK72xx (SDK remains at -O1) (#14322) --- esphome/components/libretiny/__init__.py | 10 +++++++++- esphome/components/libretiny/lt_component.cpp | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 2291114d9a0..8f991246040 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -27,6 +27,7 @@ from esphome.storage_json import StorageJSON from . import gpio # noqa from .const import ( + COMPONENT_BK72XX, CONF_GPIO_RECOVER, CONF_LOGLEVEL, CONF_SDK_SILENT, @@ -453,7 +454,14 @@ async def component_to_code(config): cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "soft") # include in every file - cg.add_platformio_option("build_src_flags", "-include Arduino.h") + build_src_flags = "-include Arduino.h" + if FAMILY_COMPONENT[config[CONF_FAMILY]] == COMPONENT_BK72XX: + # LibreTiny forces -O1 globally for BK72xx because the Beken SDK + # has issues with higher optimization levels. However, ESPHome code + # works fine with -Os (used on every other platform), so override + # it for project source files only. GCC uses the last -O flag. + build_src_flags += " -Os" + cg.add_platformio_option("build_src_flags", build_src_flags) # dummy version code cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)")) # decrease web server stack size (16k words -> 4k words) diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ffccd0ad7a2..834245c147b 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -15,6 +15,9 @@ void LTComponent::dump_config() { " Version: %s\n" " Loglevel: %u", LT_BANNER_STR + 10, LT_LOGLEVEL); +#if defined(__OPTIMIZE_SIZE__) && __OPTIMIZE_LEVEL__ > 0 && __OPTIMIZE_LEVEL__ <= 3 + ESP_LOGCONFIG(TAG, " Optimization: -Os, SDK: -O" STRINGIFY_MACRO(__OPTIMIZE_LEVEL__)); +#endif #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { From 08035261b85617b6b978336c8ca56629a86bb5cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 13:02:36 -0700 Subject: [PATCH 0246/2056] [libretiny] Use C++17 nested namespace syntax (#14325) --- esphome/components/libretiny/core.h | 4 +--- esphome/components/libretiny/gpio_arduino.cpp | 7 ++++--- esphome/components/libretiny/gpio_arduino.h | 6 ++---- esphome/components/libretiny/lt_component.cpp | 6 ++---- esphome/components/libretiny/lt_component.h | 6 ++---- esphome/components/libretiny/preferences.cpp | 7 ++++--- esphome/components/libretiny/preferences.h | 6 ++---- 7 files changed, 17 insertions(+), 25 deletions(-) diff --git a/esphome/components/libretiny/core.h b/esphome/components/libretiny/core.h index 9458df1f169..f909db4f0f8 100644 --- a/esphome/components/libretiny/core.h +++ b/esphome/components/libretiny/core.h @@ -4,8 +4,6 @@ #include -namespace esphome { -namespace libretiny {} // namespace libretiny -} // namespace esphome +namespace esphome::libretiny {} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index 0b14c77cf27..1af0dce16d4 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -3,8 +3,7 @@ #include "gpio_arduino.h" #include "esphome/core/log.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.gpio"; @@ -77,7 +76,9 @@ void ArduinoInternalGPIOPin::detach_interrupt() const { detachInterrupt(pin_); // NOLINT } -} // namespace libretiny +} // namespace esphome::libretiny + +namespace esphome { using namespace libretiny; diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 30c7c338697..5f1fa3fec76 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -3,8 +3,7 @@ #ifdef USE_LIBRETINY #include "esphome/core/hal.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { class ArduinoInternalGPIOPin : public InternalGPIOPin { public: @@ -31,7 +30,6 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index 834245c147b..c01661b3a68 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.component"; @@ -28,7 +27,6 @@ void LTComponent::dump_config() { float LTComponent::get_setup_priority() const { return setup_priority::LATE; } -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.h b/esphome/components/libretiny/lt_component.h index 3d4483ab5d3..896f1901e3c 100644 --- a/esphome/components/libretiny/lt_component.h +++ b/esphome/components/libretiny/lt_component.h @@ -12,8 +12,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { class LTComponent : public Component { public: @@ -30,7 +29,6 @@ class LTComponent : public Component { #endif // USE_TEXT_SENSOR }; -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 8549631e460..740c1a233a3 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -8,8 +8,7 @@ #include #include -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { static const char *const TAG = "lt.preferences"; @@ -194,7 +193,9 @@ void setup_preferences() { global_preferences = &s_preferences; } -} // namespace libretiny +} // namespace esphome::libretiny + +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/libretiny/preferences.h b/esphome/components/libretiny/preferences.h index 8ec3cd31b1c..68f377bd3ee 100644 --- a/esphome/components/libretiny/preferences.h +++ b/esphome/components/libretiny/preferences.h @@ -2,12 +2,10 @@ #ifdef USE_LIBRETINY -namespace esphome { -namespace libretiny { +namespace esphome::libretiny { void setup_preferences(); -} // namespace libretiny -} // namespace esphome +} // namespace esphome::libretiny #endif // USE_LIBRETINY From 54edc46c7f95ee35ec3c681276aa6a789fdeafdc Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Thu, 26 Feb 2026 21:12:52 +0100 Subject: [PATCH 0247/2056] [esp_ldo] Add channels 1&2 support and passthrough mode (#14177) --- esphome/components/esp_ldo/__init__.py | 67 ++++++++++++++++--- esphome/components/esp_ldo/esp_ldo.cpp | 20 +++--- esphome/components/esp_ldo/esp_ldo.h | 4 +- .../components/esp_ldo/test.esp32-p4-idf.yaml | 9 ++- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index f136dd149bb..5235a9411ee 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -13,22 +13,63 @@ esp_ldo_ns = cg.esphome_ns.namespace("esp_ldo") EspLdo = esp_ldo_ns.class_("EspLdo", cg.Component) AdjustAction = esp_ldo_ns.class_("AdjustAction", Action) -CHANNELS = (3, 4) +CHANNELS = (1, 2, 3, 4) +CHANNELS_INTERNAL = (1, 2) CONF_ADJUSTABLE = "adjustable" +CONF_ALLOW_INTERNAL_CHANNEL = "allow_internal_channel" +CONF_PASSTHROUGH = "passthrough" adjusted_ids = set() + +def validate_ldo_voltage(value): + if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH: + return CONF_PASSTHROUGH + value = cv.voltage(value) + if 0.5 <= value <= 2.7: + return value + raise cv.Invalid( + f"LDO voltage must be in range 0.5V-2.7V or 'passthrough' (bypass mode), got {value}V" + ) + + +def validate_ldo_config(config): + channel = config[CONF_CHANNEL] + allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL] + if allow_internal and channel not in CHANNELS_INTERNAL: + raise cv.Invalid( + f"'{CONF_ALLOW_INTERNAL_CHANNEL}' is only valid for internal channels (1, 2). " + f"Channel {channel} is a user-configurable channel — its usage depends on your board schematic.", + path=[CONF_ALLOW_INTERNAL_CHANNEL], + ) + if channel in CHANNELS_INTERNAL and not allow_internal: + raise cv.Invalid( + f"LDO channel {channel} is normally used internally by the chip (flash/PSRAM). " + f"Set '{CONF_ALLOW_INTERNAL_CHANNEL}: true' to confirm you know what you are doing.", + path=[CONF_CHANNEL], + ) + if config[CONF_VOLTAGE] == CONF_PASSTHROUGH and config[CONF_ADJUSTABLE]: + raise cv.Invalid( + "Passthrough mode passes the supply voltage directly to the output and does not support " + "runtime voltage adjustment.", + path=[CONF_ADJUSTABLE], + ) + return config + + CONFIG_SCHEMA = cv.All( cv.ensure_list( - cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EspLdo), - cv.Required(CONF_VOLTAGE): cv.All( - cv.voltage, cv.float_range(min=0.5, max=2.7) - ), - cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), - cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, - } + cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EspLdo), + cv.Required(CONF_VOLTAGE): validate_ldo_voltage, + cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), + cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, + cv.Optional(CONF_ALLOW_INTERNAL_CHANNEL, default=False): cv.boolean, + } + ), + validate_ldo_config, ) ), cv.only_on_esp32, @@ -40,7 +81,11 @@ async def to_code(configs): for config in configs: var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL]) await cg.register_component(var, config) - cg.add(var.set_voltage(config[CONF_VOLTAGE])) + voltage = config[CONF_VOLTAGE] + if voltage == CONF_PASSTHROUGH: + cg.add(var.set_voltage(3300)) + else: + cg.add(var.set_voltage(int(round(voltage * 1000)))) cg.add(var.set_adjustable(config[CONF_ADJUSTABLE])) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 2eee855b469..f8ebec19035 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -10,32 +10,34 @@ static const char *const TAG = "esp_ldo"; void EspLdo::setup() { esp_ldo_channel_config_t config{}; config.chan_id = this->channel_; - config.voltage_mv = (int) (this->voltage_ * 1000.0f); + config.voltage_mv = this->voltage_mv_; config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { - ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); } } void EspLdo::dump_config() { ESP_LOGCONFIG(TAG, "ESP LDO Channel %d:\n" - " Voltage: %fV\n" + " Voltage: %dmV\n" " Adjustable: %s", - this->channel_, this->voltage_, YESNO(this->adjustable_)); + this->channel_, this->voltage_mv_, YESNO(this->adjustable_)); } void EspLdo::adjust_voltage(float voltage) { if (!std::isfinite(voltage) || voltage < 0.5f || voltage > 2.7f) { - ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d", voltage, this->channel_); + ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d (must be 0.5V-2.7V)", voltage, this->channel_); return; } - auto erro = esp_ldo_channel_adjust_voltage(this->handle_, (int) (voltage * 1000.0f)); - if (erro != ESP_OK) { - ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %fV: %s", this->channel_, voltage, esp_err_to_name(erro)); + int voltage_mv = (int) roundf(voltage * 1000.0f); + auto err = esp_ldo_channel_adjust_voltage(this->handle_, voltage_mv); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %dmV: %s", this->channel_, voltage_mv, + esp_err_to_name(err)); } } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index 9edd303e164..1a20f1d08ad 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -15,7 +15,7 @@ class EspLdo : public Component { void dump_config() override; void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } - void set_voltage(float voltage) { this->voltage_ = voltage; } + void set_voltage(int voltage_mv) { this->voltage_mv_ = voltage_mv; } void adjust_voltage(float voltage); float get_setup_priority() const override { return setup_priority::BUS; // LDO setup should be done early @@ -23,7 +23,7 @@ class EspLdo : public Component { protected: int channel_; - float voltage_{2.7}; + int voltage_mv_{2700}; bool adjustable_{false}; esp_ldo_channel_handle_t handle_{}; }; diff --git a/tests/components/esp_ldo/test.esp32-p4-idf.yaml b/tests/components/esp_ldo/test.esp32-p4-idf.yaml index 38bd6ac411a..31d2b8cf7a4 100644 --- a/tests/components/esp_ldo/test.esp32-p4-idf.yaml +++ b/tests/components/esp_ldo/test.esp32-p4-idf.yaml @@ -3,10 +3,13 @@ esp_ldo: channel: 3 voltage: 2.5V adjustable: true - - id: ldo_4 + - id: ldo_4_passthrough channel: 4 - voltage: 2.0V - setup_priority: 900 + voltage: passthrough + - id: ldo_1_internal + channel: 1 + voltage: 1.8V + allow_internal_channel: true esphome: on_boot: From 8bd474fd017d31f4f8504f4c96db5a8e1a7f2f80 Mon Sep 17 00:00:00 2001 From: lyubomirtraykov Date: Thu, 26 Feb 2026 22:27:18 +0200 Subject: [PATCH 0248/2056] [api] Add DEFROSTING to ClimateAction (#13976) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.h | 1 + esphome/components/api/api_pb2_dump.cpp | 2 ++ esphome/components/climate/climate_mode.cpp | 6 ++++-- esphome/components/climate/climate_mode.h | 2 ++ esphome/components/mqtt/mqtt_climate.cpp | 5 +++-- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 18dac6a2d17..d7f32cd8d14 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -989,6 +989,7 @@ enum ClimateAction { CLIMATE_ACTION_IDLE = 4; CLIMATE_ACTION_DRYING = 5; CLIMATE_ACTION_FAN = 6; + CLIMATE_ACTION_DEFROSTING = 7; } enum ClimatePreset { CLIMATE_PRESET_NONE = 0; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c2675cefe46..22dc3de995d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -116,6 +116,7 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_IDLE = 4, CLIMATE_ACTION_DRYING = 5, CLIMATE_ACTION_FAN = 6, + CLIMATE_ACTION_DEFROSTING = 7, }; enum ClimatePreset : uint32_t { CLIMATE_PRESET_NONE = 0, diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 73690610ede..52d24864107 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -321,6 +321,8 @@ template<> const char *proto_enum_to_string(enums::Climate return "CLIMATE_ACTION_DRYING"; case enums::CLIMATE_ACTION_FAN: return "CLIMATE_ACTION_FAN"; + case enums::CLIMATE_ACTION_DEFROSTING: + return "CLIMATE_ACTION_DEFROSTING"; default: return "UNKNOWN"; } diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index c4dd19d503a..8e443f41468 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -10,8 +10,10 @@ const LogString *climate_mode_to_string(ClimateMode mode) { return ClimateModeStrings::get_log_str(static_cast(mode), ClimateModeStrings::LAST_INDEX); } -// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN -PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN"); +// Climate action strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN, +// DEFROSTING +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", + "DEFROSTING", "UNKNOWN"); const LogString *climate_action_to_string(ClimateAction action) { return ClimateActionStrings::get_log_str(static_cast(action), ClimateActionStrings::LAST_INDEX); diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index c961c44248d..014b1a9e64f 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -41,6 +41,8 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_DRYING = 5, /// The climate device is in fan only mode CLIMATE_ACTION_FAN = 6, + /// The climate device is defrosting + CLIMATE_ACTION_DEFROSTING = 7, }; /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 81b2e0e8dbc..443c983efe5 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -20,9 +20,10 @@ static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { return ClimateMqttModeStrings::get_progmem_str(static_cast(mode), ClimateMqttModeStrings::LAST_INDEX); } -// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +// Climate action MQTT strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN, +// DEFROSTING PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan", - "unknown"); + "defrosting", "unknown"); static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { return ClimateMqttActionStrings::get_progmem_str(static_cast(action), ClimateMqttActionStrings::LAST_INDEX); From 67ba68a1a09adb4cf3cb8178d625645a59a33c29 Mon Sep 17 00:00:00 2001 From: esphomebot Date: Fri, 27 Feb 2026 11:21:40 +1300 Subject: [PATCH 0249/2056] Update webserver local assets to 20260226-220330 (#14330) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/captive_portal/captive_index.h | 4 ++-- esphome/components/web_server/server_index_v2.h | 4 ++-- esphome/components/web_server/server_index_v3.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f4..a81edc19005 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index ffa9c87b3a9..ac2195f3870 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xd9, 0x72, 0xdb, 0x48, 0xb6, 0xe0, 0xf3, 0xd4, 0x57, 0x40, 0x28, 0xb5, 0x8c, 0x2c, 0x26, 0xc1, 0x45, 0x92, 0x2d, 0x83, 0x4a, 0xb2, 0x65, 0xd9, 0xd5, 0x76, 0x97, 0xb7, 0xb6, 0xec, 0xda, 0x58, 0x6c, 0x09, 0x02, 0x92, 0x44, 0x96, 0x41, 0x80, 0x05, 0x24, 0xb5, 0x14, 0x89, @@ -698,7 +698,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x01, 0x65, 0x21, 0x07, 0x4b, 0xe3, 0x97, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xe2, 0x97, 0xa3, 0x90, 0xa2, 0x95, 0x55, 0x51, 0x04, 0x1b, 0x07, 0x80, 0x20, 0x79, 0x0e, 0x50, 0xab, 0x02, 0xdb, 0x98, 0x16, 0xf4, 0x7b, 0x22, 0xa3, 0x4d, 0xd3, 0x86, 0xc1, 0x26, 0x48, 0x49, 0x60, 0xbe, 0xb3, 0xc9, 0xa1, 0x8c, 0x96, 0x10, 0x1b, 0x21, 0xcf, 0x48, 0x68, 0xce, 0x10, 0x34, 0x32, 0x7c, 0xbf, 0x71, 0x7b, 0x03, 0x8f, 0xdd, diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index b7c15df32ba..a1cafe8707b 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -9,7 +9,7 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x7b, 0x7f, 0x1a, 0xb9, 0xb2, 0x28, 0xfa, 0xf7, 0x3d, 0x9f, 0xc2, 0xee, 0x9d, 0xf1, 0xb4, 0x8c, 0x68, 0x03, 0x36, 0x8e, 0xd3, 0x58, 0xe6, 0xe4, 0x39, 0xc9, 0x3c, 0x92, 0x4c, 0x9c, 0x64, 0x26, 0xc3, 0xb0, 0x33, 0xa2, 0x11, 0xa0, 0xa4, 0x91, 0x98, 0x96, 0x88, 0xed, 0x01, @@ -4107,7 +4107,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xe8, 0xcd, 0xfe, 0x2c, 0x9d, 0x07, 0xfd, 0xff, 0x05, 0x64, 0x23, 0xa6, 0xdb, 0x06, 0x7b, 0x03, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x5b, 0x05, 0x7b, 0x53, 0xc1, 0xb6, 0x69, 0x3d, 0x41, 0xeb, 0x04, 0x30, 0xf6, 0xd6, 0x77, 0x35, 0xdb, 0xa3, 0x08, 0x36, 0x0e, 0x04, 0x80, 0x90, 0x4f, 0xf1, 0xb2, 0x21, 0xa4, 0x82, 0xee, 0x00, 0xaa, 0x20, 0x7f, 0x3b, 0xff, 0x00, 0xaa, 0x9a, 0x73, 0x74, 0x8c, 0xe1, 0xa6, 0x1f, 0xa0, 0xa2, 0x59, 0xf5, 0xaa, 0x92, 0x79, 0x50, 0x43, 0x1f, 0xe8, From 527d4964f61b77439c469d41207527c5677b813b Mon Sep 17 00:00:00 2001 From: George Joseph Date: Thu, 26 Feb 2026 17:38:07 -0700 Subject: [PATCH 0250/2056] [mipi_dsi] Add more Waveshare panels and comments (#14023) --- .../components/mipi_dsi/models/waveshare.py | 139 +++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index bf4f9063bb8..69414065f13 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -2,7 +2,11 @@ from esphome.components.mipi import DriverChip import esphome.config_validation as cv # fmt: off -DriverChip( + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage +JD9365_10_1_DSI_TOUCH_A = DriverChip( "WAVESHARE-P4-NANO-10.1", height=1280, width=800, @@ -52,6 +56,15 @@ DriverChip( ], ) +# Standalone display +# Product page: https://www.waveshare.com/wiki/10.1-DSI-TOUCH-A +JD9365_10_1_DSI_TOUCH_A.extend( + "WAVESHARE-10.1-DSI-TOUCH-A", +) + +# Source for parameters and initsequence: +# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO DriverChip( "WAVESHARE-P4-86-PANEL", height=720, @@ -95,6 +108,9 @@ DriverChip( ], ) +# Source for parameters and initsequence: +# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", height=600, @@ -121,7 +137,10 @@ DriverChip( ], ) -DriverChip( +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C +JD9365_3_4_DSI_TOUCH_C = DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C", height=800, width=800, @@ -170,7 +189,16 @@ DriverChip( ], ) -DriverChip( +# Standalone display +# Product page: https://www.waveshare.com/wiki/3.4-DSI-TOUCH-C +JD9365_3_4_DSI_TOUCH_C.extend( + "WAVESHARE-3.4-DSI-TOUCH-C", +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C +JD9365_4_DSI_TOUCH_C = DriverChip( "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C", height=720, width=720, @@ -218,3 +246,108 @@ DriverChip( (0xE0, 0x00), # select userpage ] ) + +# Standalone display +# Product page: https://www.waveshare.com/wiki/4-DSI-TOUCH-C +JD9365_4_DSI_TOUCH_C.extend( + "WAVESHARE-4-DSI-TOUCH-C", +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365 +# Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A +DriverChip( + "WAVESHARE-8-DSI-TOUCH-A", + height=1280, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x4E), (0x03, 0x00), (0x04, 0x65), (0x0C, 0x74), (0x17, 0x00), (0x18, 0xB7), (0x19, 0x00), + (0x1A, 0x00), (0x1B, 0xB7), (0x1C, 0x00), (0x24, 0xFE), (0x37, 0x19), (0x38, 0x05), (0x39, 0x00), (0x3A, 0x01), + (0x3B, 0x01), (0x3C, 0x70), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x06), (0x41, 0xA0), (0x43, 0x1E), + (0x44, 0x0F), (0x45, 0x28), (0x4B, 0x04), (0x55, 0x02), (0x56, 0x01), (0x57, 0xA9), (0x58, 0x0A), (0x59, 0x0A), + (0x5A, 0x37), (0x5B, 0x19), (0x5D, 0x78), (0x5E, 0x63), (0x5F, 0x54), (0x60, 0x49), (0x61, 0x45), (0x62, 0x38), + (0x63, 0x3D), (0x64, 0x28), (0x65, 0x43), (0x66, 0x41), (0x67, 0x43), (0x68, 0x62), (0x69, 0x50), (0x6A, 0x57), + (0x6B, 0x49), (0x6C, 0x44), (0x6D, 0x37), (0x6E, 0x23), (0x6F, 0x10), (0x70, 0x78), (0x71, 0x63), (0x72, 0x54), + (0x73, 0x49), (0x74, 0x45), (0x75, 0x38), (0x76, 0x3D), (0x77, 0x28), (0x78, 0x43), (0x79, 0x41), (0x7A, 0x43), + (0x7B, 0x62), (0x7C, 0x50), (0x7D, 0x57), (0x7E, 0x49), (0x7F, 0x44), (0x80, 0x37), (0x81, 0x23), (0x82, 0x10), + (0xE0, 0x02), # select page 2 + (0x00, 0x47), (0x01, 0x47), (0x02, 0x45), (0x03, 0x45), (0x04, 0x4B), (0x05, 0x4B), (0x06, 0x49), (0x07, 0x49), + (0x08, 0x41), (0x09, 0x1F), (0x0A, 0x1F), (0x0B, 0x1F), (0x0C, 0x1F), (0x0D, 0x1F), (0x0E, 0x1F), (0x0F, 0x5F), + (0x10, 0x5F), (0x11, 0x57), (0x12, 0x77), (0x13, 0x35), (0x14, 0x1F), (0x15, 0x1F), (0x16, 0x46), (0x17, 0x46), + (0x18, 0x44), (0x19, 0x44), (0x1A, 0x4A), (0x1B, 0x4A), (0x1C, 0x48), (0x1D, 0x48), (0x1E, 0x40), (0x1F, 0x1F), + (0x20, 0x1F), (0x21, 0x1F), (0x22, 0x1F), (0x23, 0x1F), (0x24, 0x1F), (0x25, 0x5F), (0x26, 0x5F), (0x27, 0x57), + (0x28, 0x77), (0x29, 0x35), (0x2A, 0x1F), (0x2B, 0x1F), (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x10), + (0x5C, 0x06), (0x5D, 0x40), (0x5E, 0x01), (0x5F, 0x02), (0x60, 0x30), (0x61, 0x01), (0x62, 0x02), (0x63, 0x03), + (0x64, 0x6B), (0x65, 0x05), (0x66, 0x0C), (0x67, 0x73), (0x68, 0x09), (0x69, 0x03), (0x6A, 0x56), (0x6B, 0x08), + (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), (0x70, 0x00), (0x71, 0x00), (0x72, 0x06), (0x73, 0x7B), + (0x74, 0x00), (0x75, 0xF8), (0x76, 0x00), (0x77, 0xD5), (0x78, 0x2E), (0x79, 0x12), (0x7A, 0x03), (0x7B, 0x00), + (0x7C, 0x00), (0x7D, 0x03), (0x7E, 0x7B), + (0xE0, 0x04), # select page 4 + (0x00, 0x0E), (0x02, 0xB3), (0x09, 0x60), (0x0E, 0x2A), (0x36, 0x59), (0x37, 0x58), (0x2B, 0x0F), + (0xE0, 0x00), # select userpage + ] +) + +# Source for parameters and initsequence: +# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c +# Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A +DriverChip( + "WAVESHARE-7-DSI-TOUCH-A", + height=1280, + width=720, + hsync_back_porch=239, + hsync_pulse_width=50, + hsync_front_porch=33, + vsync_back_porch=20, + vsync_pulse_width=30, + vsync_front_porch=2, + pclk_frequency="80MHz", + lane_bit_rate="1000Mbps", + no_transform=True, + color_order="RGB", + initsequence=[ + (0xFF, 0x98, 0x81, 0x03), + (0x01, 0x00), (0x02, 0x00), (0x03, 0x73), (0x04, 0x00), (0x05, 0x00), (0x06, 0x0A), (0x07, 0x00), (0x08, 0x00), + (0x09, 0x61), (0x0A, 0x00), (0x0B, 0x00), (0x0C, 0x01), (0x0D, 0x00), (0x0E, 0x00), (0x0F, 0x61), (0x10, 0x61), + (0x11, 0x00), (0x12, 0x00), (0x13, 0x00), (0x14, 0x00), (0x15, 0x00), (0x16, 0x00), (0x17, 0x00), (0x18, 0x00), + (0x19, 0x00), (0x1A, 0x00), (0x1B, 0x00), (0x1C, 0x00), (0x1D, 0x00), (0x1E, 0x40), (0x1F, 0x80), (0x20, 0x06), + (0x21, 0x01), (0x22, 0x00), (0x23, 0x00), (0x24, 0x00), (0x25, 0x00), (0x26, 0x00), (0x27, 0x00), (0x28, 0x33), + (0x29, 0x03), (0x2A, 0x00), (0x2B, 0x00), (0x2C, 0x00), (0x2D, 0x00), (0x2E, 0x00), (0x2F, 0x00), (0x30, 0x00), + (0x31, 0x00), (0x32, 0x00), (0x33, 0x00), (0x34, 0x04), (0x35, 0x00), (0x36, 0x00), (0x37, 0x00), (0x38, 0x3C), + (0x39, 0x00), (0x3A, 0x00), (0x3B, 0x00), (0x3C, 0x00), (0x3D, 0x00), (0x3E, 0x00), (0x3F, 0x00), (0x40, 0x00), + (0x41, 0x00), (0x42, 0x00), (0x43, 0x00), (0x44, 0x00), (0x50, 0x10), (0x51, 0x32), (0x52, 0x54), (0x53, 0x76), + (0x54, 0x98), (0x55, 0xBA), (0x56, 0x10), (0x57, 0x32), (0x58, 0x54), (0x59, 0x76), (0x5A, 0x98), (0x5B, 0xBA), + (0x5C, 0xDC), (0x5D, 0xFE), (0x5E, 0x00), (0x5F, 0x0E), (0x60, 0x0F), (0x61, 0x0C), (0x62, 0x0D), (0x63, 0x06), + (0x64, 0x07), (0x65, 0x02), (0x66, 0x02), (0x67, 0x02), (0x68, 0x02), (0x69, 0x01), (0x6A, 0x00), (0x6B, 0x02), + (0x6C, 0x15), (0x6D, 0x14), (0x6E, 0x02), (0x6F, 0x02), (0x70, 0x02), (0x71, 0x02), (0x72, 0x02), (0x73, 0x02), + (0x74, 0x02), (0x75, 0x0E), (0x76, 0x0F), (0x77, 0x0C), (0x78, 0x0D), (0x79, 0x06), (0x7A, 0x07), (0x7B, 0x02), + (0x7C, 0x02), (0x7D, 0x02), (0x7E, 0x02), (0x7F, 0x01), (0x80, 0x00), (0x81, 0x02), (0x82, 0x14), (0x83, 0x15), + (0x84, 0x02), (0x85, 0x02), (0x86, 0x02), (0x87, 0x02), (0x88, 0x02), (0x89, 0x02), (0x8A, 0x02), + (0xFF, 0x98, 0x81, 0x04), + (0x38, 0x01), (0x39, 0x00), (0x6C, 0x15), (0x6E, 0x2A), (0x6F, 0x33), (0x3A, 0x94), (0x8D, 0x14), (0x87, 0xBA), + (0x26, 0x76), (0xB2, 0xD1), (0xB5, 0x06), (0x3B, 0x98), + (0xFF, 0x98, 0x81, 0x01), + (0x22, 0x0A), (0x31, 0x00), (0x53, 0x71), (0x55, 0x8F), (0x40, 0x33), (0x50, 0x96), (0x51, 0x96), (0x60, 0x23), + (0xA0, 0x08), (0xA1, 0x1D), (0xA2, 0x2A), (0xA3, 0x10), (0xA4, 0x15), (0xA5, 0x28), (0xA6, 0x1C), (0xA7, 0x1D), + (0xA8, 0x7E), (0xA9, 0x1D), (0xAA, 0x29), (0xAB, 0x6B), (0xAC, 0x1A), (0xAD, 0x18), (0xAE, 0x4B), (0xAF, 0x20), + (0xB0, 0x27), (0xB1, 0x50), (0xB2, 0x64), (0xB3, 0x39), (0xC0, 0x08), (0xC1, 0x1D), (0xC2, 0x2A), (0xC3, 0x10), + (0xC4, 0x15), (0xC5, 0x28), (0xC6, 0x1C), (0xC7, 0x1D), (0xC8, 0x7E), (0xC9, 0x1D), (0xCA, 0x29), (0xCB, 0x6B), + (0xCC, 0x1A), (0xCD, 0x18), (0xCE, 0x4B), (0xCF, 0x20), (0xD0, 0x27), (0xD1, 0x50), (0xD2, 0x64), (0xD3, 0x39), + (0xFF, 0x98, 0x81, 0x00), + (0x3A, 0x77), (0x36, 0x00), (0x35, 0x00), (0x35, 0x00), + ], +) From 1ccfcfc8d85593f81e96ea40cd4e8a162392a678 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:12:44 -0700 Subject: [PATCH 0251/2056] [time] Eliminate libc timezone bloat (~9.5KB flash ESP32, ~2% RAM on ESP8266) (#13635) Co-authored-by: Claude Opus 4.6 --- esphome/components/time/posix_tz.cpp | 488 +++++++ esphome/components/time/posix_tz.h | 144 +++ esphome/components/time/real_time_clock.cpp | 61 +- esphome/components/time/real_time_clock.h | 35 +- esphome/core/time.cpp | 232 +++- esphome/core/time.h | 18 +- script/cpp_unit_test.py | 1 + tests/components/time/posix_tz_parser.cpp | 1275 +++++++++++++++++++ 8 files changed, 2163 insertions(+), 91 deletions(-) create mode 100644 esphome/components/time/posix_tz.cpp create mode 100644 esphome/components/time/posix_tz.h create mode 100644 tests/components/time/posix_tz_parser.cpp diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp new file mode 100644 index 00000000000..4d1f0c74c2c --- /dev/null +++ b/esphome/components/time/posix_tz.cpp @@ -0,0 +1,488 @@ +#include "esphome/core/defines.h" + +#ifdef USE_TIME_TIMEZONE + +#include "posix_tz.h" +#include + +namespace esphome::time { + +// Global timezone - set once at startup, rarely changes +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state +static ParsedTimezone global_tz_{}; + +void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; } + +const ParsedTimezone &get_global_tz() { return global_tz_; } + +namespace internal { + +// Remove before 2026.9.0: parse_uint, skip_tz_name, parse_offset, parse_dst_rule, +// and parse_transition_time are only used by parse_posix_tz() (bridge code). +static uint32_t parse_uint(const char *&p) { + uint32_t value = 0; + while (std::isdigit(static_cast(*p))) { + value = value * 10 + (*p - '0'); + p++; + } + return value; +} + +bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } + +// Get days in year (avoids duplicate is_leap_year calls) +static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; } + +// Convert days since epoch to year, updating days to remainder +static int __attribute__((noinline)) days_to_year(int64_t &days) { + int year = 1970; + int diy; + while (days >= (diy = days_in_year(year)) && year < 2200) { + days -= diy; + year++; + } + while (days < 0 && year > 1900) { + year--; + days += days_in_year(year); + } + return year; +} + +// Extract just the year from a UTC epoch +static int epoch_to_year(time_t epoch) { + int64_t days = epoch / 86400; + if (epoch < 0 && epoch % 86400 != 0) + days--; + return days_to_year(days); +} + +int days_in_month(int year, int month) { + switch (month) { + case 2: + return is_leap_year(year) ? 29 : 28; + case 4: + case 6: + case 9: + case 11: + return 30; + default: + return 31; + } +} + +// Zeller-like algorithm for day of week (0 = Sunday) +int __attribute__((noinline)) day_of_week(int year, int month, int day) { + // Adjust for January/February + if (month < 3) { + month += 12; + year--; + } + int k = year % 100; + int j = year / 100; + int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7; + // Convert from Zeller (0=Sat) to standard (0=Sun) + return ((h + 6) % 7); +} + +void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) { + // Days since epoch + int64_t days = epoch / 86400; + int32_t remaining_secs = epoch % 86400; + if (remaining_secs < 0) { + days--; + remaining_secs += 86400; + } + + out_tm->tm_sec = remaining_secs % 60; + remaining_secs /= 60; + out_tm->tm_min = remaining_secs % 60; + out_tm->tm_hour = remaining_secs / 60; + + // Day of week (Jan 1, 1970 was Thursday = 4) + out_tm->tm_wday = static_cast((days + 4) % 7); + if (out_tm->tm_wday < 0) + out_tm->tm_wday += 7; + + // Calculate year (updates days to day-of-year) + int year = days_to_year(days); + out_tm->tm_year = year - 1900; + out_tm->tm_yday = static_cast(days); + + // Calculate month and day + int month = 1; + int dim; + while (days >= (dim = days_in_month(year, month))) { + days -= dim; + month++; + } + + out_tm->tm_mon = month - 1; + out_tm->tm_mday = static_cast(days) + 1; + out_tm->tm_isdst = 0; +} + +bool skip_tz_name(const char *&p) { + if (*p == '<') { + // Angle-bracket quoted name: <+07>, <-03>, + p++; // skip '<' + while (*p && *p != '>') { + p++; + } + if (*p == '>') { + p++; // skip '>' + return true; + } + return false; // Unterminated + } + + // Standard name: 3+ letters + const char *start = p; + while (*p && std::isalpha(static_cast(*p))) { + p++; + } + return (p - start) >= 3; +} + +int32_t __attribute__((noinline)) parse_offset(const char *&p) { + int sign = 1; + if (*p == '-') { + sign = -1; + p++; + } else if (*p == '+') { + p++; + } + + int hours = parse_uint(p); + int minutes = 0; + int seconds = 0; + + if (*p == ':') { + p++; + minutes = parse_uint(p); + if (*p == ':') { + p++; + seconds = parse_uint(p); + } + } + + return sign * (hours * 3600 + minutes * 60 + seconds); +} + +// Helper to parse the optional /time suffix (reuses parse_offset logic) +static void parse_transition_time(const char *&p, DSTRule &rule) { + rule.time_seconds = 2 * 3600; // Default 02:00 + if (*p == '/') { + p++; + rule.time_seconds = parse_offset(p); + } +} + +void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) { + // J format: day 1-365, Feb 29 is NOT counted even in leap years + // So day 60 is always March 1 + // Iterate forward through months (no array needed) + int remaining = julian_day; + out_month = 1; + while (out_month <= 12) { + // Days in month for non-leap year (J format ignores leap years) + int dim = days_in_month(2001, out_month); // 2001 is non-leap year + if (remaining <= dim) { + out_day = remaining; + return; + } + remaining -= dim; + out_month++; + } + out_day = remaining; +} + +void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) { + // Plain format: day 0-365, Feb 29 IS counted in leap years + // Day 0 = Jan 1 + int remaining = day_of_year; + out_month = 1; + + while (out_month <= 12) { + int days_this_month = days_in_month(year, out_month); + if (remaining < days_this_month) { + out_day = remaining + 1; + return; + } + remaining -= days_this_month; + out_month++; + } + + // Shouldn't reach here with valid input + out_month = 12; + out_day = 31; +} + +bool parse_dst_rule(const char *&p, DSTRule &rule) { + rule = {}; // Zero initialize + + if (*p == 'M' || *p == 'm') { + // M format: Mm.w.d (month.week.day) + rule.type = DSTRuleType::MONTH_WEEK_DAY; + p++; + + rule.month = parse_uint(p); + if (rule.month < 1 || rule.month > 12) + return false; + + if (*p++ != '.') + return false; + + rule.week = parse_uint(p); + if (rule.week < 1 || rule.week > 5) + return false; + + if (*p++ != '.') + return false; + + rule.day_of_week = parse_uint(p); + if (rule.day_of_week > 6) + return false; + + } else if (*p == 'J' || *p == 'j') { + // J format: Jn (Julian day 1-365, not counting Feb 29) + rule.type = DSTRuleType::JULIAN_NO_LEAP; + p++; + + rule.day = parse_uint(p); + if (rule.day < 1 || rule.day > 365) + return false; + + } else if (std::isdigit(static_cast(*p))) { + // Plain number format: n (day 0-365, counting Feb 29) + rule.type = DSTRuleType::DAY_OF_YEAR; + + rule.day = parse_uint(p); + if (rule.day > 365) + return false; + + } else { + return false; + } + + // Parse optional /time suffix + parse_transition_time(p, rule); + + return true; +} + +// Calculate days from Jan 1 of given year to given month/day +static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) { + int days = day - 1; + for (int m = 1; m < month; m++) { + days += days_in_month(year, m); + } + return days; +} + +// Calculate days from epoch to Jan 1 of given year (for DST transition calculations) +// Only supports years >= 1970. Timezone is either compiled in from YAML or set by +// Home Assistant, so pre-1970 dates are not a concern. +static int64_t __attribute__((noinline)) days_to_year_start(int year) { + int64_t days = 0; + for (int y = 1970; y < year; y++) { + days += days_in_year(y); + } + return days; +} + +time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) { + int month, day; + + switch (rule.type) { + case DSTRuleType::MONTH_WEEK_DAY: { + // Find the nth occurrence of day_of_week in the given month + int first_dow = day_of_week(year, rule.month, 1); + + // Days until first occurrence of target day + int days_until_first = (rule.day_of_week - first_dow + 7) % 7; + int first_occurrence = 1 + days_until_first; + + if (rule.week == 5) { + // "Last" occurrence - find the last one in the month + int dim = days_in_month(year, rule.month); + day = first_occurrence; + while (day + 7 <= dim) { + day += 7; + } + } else { + // nth occurrence + day = first_occurrence + (rule.week - 1) * 7; + } + month = rule.month; + break; + } + + case DSTRuleType::JULIAN_NO_LEAP: + // J format: day 1-365, Feb 29 not counted + julian_to_month_day(rule.day, month, day); + break; + + case DSTRuleType::DAY_OF_YEAR: + // Plain format: day 0-365, Feb 29 counted + day_of_year_to_month_day(rule.day, year, month, day); + break; + + case DSTRuleType::NONE: + // Should never be called with NONE, but handle it gracefully + month = 1; + day = 1; + break; + } + + // Calculate days from epoch to this date + int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day); + + // Convert to epoch and add transition time and base offset + return days * 86400 + rule.time_seconds + base_offset_seconds; +} + +} // namespace internal + +bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) { + if (!tz.has_dst()) { + return false; + } + + int year = internal::epoch_to_year(utc_epoch); + + // Calculate DST start and end for this year + // DST start transition happens in standard time + time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds); + // DST end transition happens in daylight time + time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds); + + if (dst_start < dst_end) { + // Northern hemisphere: DST is between start and end + return (utc_epoch >= dst_start && utc_epoch < dst_end); + } else { + // Southern hemisphere: DST is outside the range (wraps around year) + return (utc_epoch >= dst_start || utc_epoch < dst_end); + } +} + +// Remove before 2026.9.0: This parser is bridge code for backward compatibility with +// older Home Assistant clients that send the timezone as a POSIX TZ string instead of +// the pre-parsed ParsedTimezone protobuf struct. Once all clients send the struct +// directly, this function and the parsing helpers above (skip_tz_name, parse_offset, +// parse_dst_rule, parse_transition_time) can be removed. +// See https://github.com/esphome/backlog/issues/91 +bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) { + if (!tz_string || !*tz_string) { + return false; + } + + const char *p = tz_string; + + // Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false) + result.std_offset_seconds = 0; + result.dst_offset_seconds = 0; + result.dst_start = {}; + result.dst_end = {}; + + // Skip standard timezone name + if (!internal::skip_tz_name(p)) { + return false; + } + + // Parse standard offset (required) + if (!*p || (!std::isdigit(static_cast(*p)) && *p != '+' && *p != '-')) { + return false; + } + result.std_offset_seconds = internal::parse_offset(p); + + // Check for DST name + if (!*p) { + return true; // No DST + } + + // If next char is comma, there's no DST name but there are rules (invalid) + if (*p == ',') { + return false; + } + + // Check if there's something that looks like a DST name start + // (letter or angle bracket). If not, treat as trailing garbage and return success. + if (!std::isalpha(static_cast(*p)) && *p != '<') { + return true; // No DST, trailing characters ignored + } + + if (!internal::skip_tz_name(p)) { + return false; // Invalid DST name (started but malformed) + } + + // Optional DST offset (default is std - 1 hour) + if (*p && *p != ',' && (std::isdigit(static_cast(*p)) || *p == '+' || *p == '-')) { + result.dst_offset_seconds = internal::parse_offset(p); + } else { + result.dst_offset_seconds = result.std_offset_seconds - 3600; + } + + // Parse DST rules (required when DST name is present) + if (*p != ',') { + // DST name without rules - treat as no DST since we can't determine transitions + return true; + } + + p++; + if (!internal::parse_dst_rule(p, result.dst_start)) { + return false; + } + + // Second rule is required per POSIX + if (*p != ',') { + return false; + } + p++; + // has_dst() now returns true since dst_start.type was set by parse_dst_rule + return internal::parse_dst_rule(p, result.dst_end); +} + +bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) { + if (!out_tm) { + return false; + } + + // Determine DST status once (avoids duplicate is_in_dst calculation) + bool in_dst = is_in_dst(utc_epoch, tz); + int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds; + + // Apply offset (POSIX offset is positive west, so subtract to get local) + time_t local_epoch = utc_epoch - offset; + + internal::epoch_to_tm_utc(local_epoch, out_tm); + out_tm->tm_isdst = in_dst ? 1 : 0; + + return true; +} + +} // namespace esphome::time + +#ifndef USE_HOST +// Override libc's localtime functions to use our timezone on embedded platforms. +// This allows user lambdas calling ::localtime() to get correct local time +// without needing the TZ environment variable (which pulls in scanf bloat). +// On host, we use the normal TZ mechanism since there's no memory constraint. + +// Thread-safe version +extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) { + if (timer == nullptr || result == nullptr) { + return nullptr; + } + esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result); + return result; +} + +// Non-thread-safe version (uses static buffer, standard libc behavior) +extern "C" struct tm *localtime(const time_t *timer) { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static struct tm localtime_buf; + return localtime_r(timer, &localtime_buf); +} +#endif // !USE_HOST + +#endif // USE_TIME_TIMEZONE diff --git a/esphome/components/time/posix_tz.h b/esphome/components/time/posix_tz.h new file mode 100644 index 00000000000..c71ba15cd14 --- /dev/null +++ b/esphome/components/time/posix_tz.h @@ -0,0 +1,144 @@ +#pragma once + +#ifdef USE_TIME_TIMEZONE + +#include +#include + +namespace esphome::time { + +/// Type of DST transition rule +enum class DSTRuleType : uint8_t { + NONE = 0, ///< No DST rule (used to indicate no DST) + MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March) + JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted) + DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years) +}; + +/// Rule for DST transition (packed for 32-bit: 12 bytes) +struct DSTRule { + int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM) + uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR) + DSTRuleType type; ///< Type of rule + uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY) + uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY) + uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY) +}; + +/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes) +struct ParsedTimezone { + int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west) + int32_t dst_offset_seconds; ///< DST offset from UTC in seconds + DSTRule dst_start; ///< When DST starts + DSTRule dst_end; ///< When DST ends + + /// Check if this timezone has DST rules + bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; } +}; + +/// Parse a POSIX TZ string into a ParsedTimezone struct. +/// +/// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility). +/// This parser only exists so that older Home Assistant clients that send the timezone +/// as a string (instead of the pre-parsed ParsedTimezone protobuf struct) can still +/// set the timezone on the device. Once all clients are updated to send the struct +/// directly, this function and all internal parsing helpers will be removed. +/// See https://github.com/esphome/backlog/issues/91 +/// +/// Supports formats like: +/// - "EST5" (simple offset, no DST) +/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules) +/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times) +/// - "<+07>-7" (angle-bracket notation for special names) +/// - "IST-5:30" (half-hour offsets) +/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day) +/// - "EST5EDT,60,300" (plain day number: day of year with leap day) +/// @param tz_string The POSIX TZ string to parse +/// @param result Output: the parsed timezone data +/// @return true if parsing succeeded, false on error +bool parse_posix_tz(const char *tz_string, ParsedTimezone &result); + +/// Convert a UTC epoch to local time using the parsed timezone. +/// This replaces libc's localtime() to avoid scanf dependency. +/// @param utc_epoch Unix timestamp in UTC +/// @param tz The parsed timezone +/// @param[out] out_tm Output tm struct with local time +/// @return true on success +bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm); + +/// Set the global timezone used by epoch_to_local_tm() when called without a timezone. +/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local() +/// to work without libc's localtime(). +void set_global_tz(const ParsedTimezone &tz); + +/// Get the global timezone. +const ParsedTimezone &get_global_tz(); + +/// Check if a given UTC epoch falls within DST for the parsed timezone. +/// @param utc_epoch Unix timestamp in UTC +/// @param tz The parsed timezone +/// @return true if DST is in effect at the given time +bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz); + +// Internal helper functions exposed for testing. +// Remove before 2026.9.0: skip_tz_name, parse_offset, parse_dst_rule are only +// used by parse_posix_tz() which is bridge code for backward compatibility. +// The remaining helpers (epoch_to_tm_utc, day_of_week, days_in_month, etc.) +// are used by the conversion functions and will stay. + +namespace internal { + +/// Skip a timezone name (letters or <...> quoted format) +/// @param p Pointer to current position, updated on return +/// @return true if a valid name was found +bool skip_tz_name(const char *&p); + +/// Parse an offset in format [-]hh[:mm[:ss]] +/// @param p Pointer to current position, updated on return +/// @return Offset in seconds +int32_t parse_offset(const char *&p); + +/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time] +/// @param p Pointer to current position, updated on return +/// @param rule Output: the parsed rule +/// @return true if parsing succeeded +bool parse_dst_rule(const char *&p, DSTRule &rule); + +/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day +/// @param julian_day Day number 1-365 +/// @param[out] month Output: month 1-12 +/// @param[out] day Output: day of month +void julian_to_month_day(int julian_day, int &month, int &day); + +/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day +/// @param day_of_year Day number 0-365 +/// @param year The year (for leap year calculation) +/// @param[out] month Output: month 1-12 +/// @param[out] day Output: day of month +void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day); + +/// Calculate day of week for any date (0 = Sunday) +/// Uses a simplified algorithm that works for years 1970-2099 +int day_of_week(int year, int month, int day); + +/// Get the number of days in a month +int days_in_month(int year, int month); + +/// Check if a year is a leap year +bool is_leap_year(int year); + +/// Convert epoch to year/month/day/hour/min/sec (UTC) +void epoch_to_tm_utc(time_t epoch, struct tm *out_tm); + +/// Calculate the epoch timestamp for a DST transition in a given year. +/// @param year The year (e.g., 2026) +/// @param rule The DST rule (month, week, day_of_week, time) +/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context) +/// @return Unix epoch timestamp of the transition +time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds); + +} // namespace internal + +} // namespace esphome::time + +#endif // USE_TIME_TIMEZONE diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 8a78186178a..2e758ad8e7a 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -14,8 +14,8 @@ #include #endif #include - #include +#include namespace esphome::time { @@ -23,9 +23,33 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; +ESPTime __attribute__((noinline)) RealTimeClock::now() { +#ifdef USE_TIME_TIMEZONE + time_t epoch = this->timestamp_now(); + struct tm local_tm; + if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) { + return ESPTime::from_c_tm(&local_tm, epoch); + } + // Fallback to UTC if parsing failed + return ESPTime::from_epoch_utc(epoch); +#else + return ESPTime::from_epoch_local(this->timestamp_now()); +#endif +} + void RealTimeClock::dump_config() { #ifdef USE_TIME_TIMEZONE - ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); + const auto &tz = get_global_tz(); + // POSIX offset is positive west, negate for conventional UTC+X display + int std_h = -tz.std_offset_seconds / 3600; + int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60; + if (tz.has_dst()) { + int dst_h = -tz.dst_offset_seconds / 3600; + int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60; + ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m); + } else { + ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m); + } #endif auto time = this->now(); ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, @@ -72,11 +96,6 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } -#ifdef USE_TIME_TIMEZONE - // Move timezone back to local timezone. - this->apply_timezone_(); -#endif - if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } @@ -89,9 +108,33 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { } #ifdef USE_TIME_TIMEZONE -void RealTimeClock::apply_timezone_() { - setenv("TZ", this->timezone_.c_str(), 1); +void RealTimeClock::apply_timezone_(const char *tz) { + ParsedTimezone parsed{}; + + // Handle null or empty input - use UTC + if (tz == nullptr || *tz == '\0') { + // Skip if already UTC + if (!get_global_tz().has_dst() && get_global_tz().std_offset_seconds == 0) { + return; + } + set_global_tz(parsed); + return; + } + +#ifdef USE_HOST + // On host platform, also set TZ environment variable for libc compatibility + setenv("TZ", tz, 1); tzset(); +#endif + + // Parse the POSIX TZ string using our custom parser + if (!parse_posix_tz(tz, parsed)) { + ESP_LOGW(TAG, "Failed to parse timezone: %s", tz); + return; + } + + // Set global timezone for all time conversions + set_global_tz(parsed); } #endif diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 19aa1a4f4a5..f9de5f5614c 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -6,6 +6,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/time.h" +#ifdef USE_TIME_TIMEZONE +#include "posix_tz.h" +#endif namespace esphome::time { @@ -20,26 +23,31 @@ class RealTimeClock : public PollingComponent { explicit RealTimeClock(); #ifdef USE_TIME_TIMEZONE - /// Set the time zone. - void set_timezone(const std::string &tz) { - this->timezone_ = tz; - this->apply_timezone_(); - } + /// Set the time zone from a POSIX TZ string. + void set_timezone(const char *tz) { this->apply_timezone_(tz); } - /// Set the time zone from raw buffer, only if it differs from the current one. + /// Set the time zone from a character buffer with known length. + /// The buffer does not need to be null-terminated. void set_timezone(const char *tz, size_t len) { - if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) { - this->timezone_.assign(tz, len); - this->apply_timezone_(); + if (tz == nullptr) { + this->apply_timezone_(nullptr); + return; } + // Stack buffer - TZ strings from tzdata are typically short (< 50 chars) + char buf[128]; + if (len >= sizeof(buf)) + len = sizeof(buf) - 1; + memcpy(buf, tz, len); + buf[len] = '\0'; + this->apply_timezone_(buf); } - /// Get the time zone currently in use. - std::string get_timezone() { return this->timezone_; } + /// Set the time zone from a std::string. + void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); } #endif /// Get the time in the currently defined timezone. - ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); } + ESPTime now(); /// Get the time without any time zone or DST corrections. ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); } @@ -58,8 +66,7 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); #ifdef USE_TIME_TIMEZONE - std::string timezone_{}; - void apply_timezone_(); + void apply_timezone_(const char *tz); #endif LazyCallbackManager time_sync_callback_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 1aea18ac8d3..73ba0a9be7b 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -2,7 +2,6 @@ #include "helpers.h" #include -#include namespace esphome { @@ -67,58 +66,123 @@ std::string ESPTime::strftime(const char *format) { std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } -bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) { - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; - int num; - const int ilen = static_cast(len); - - if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT - &hour, // NOLINT - &minute, // NOLINT - &second, &num) == 6 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = second; - } else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT - &hour, // NOLINT - &minute, &num) == 5 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = 0; - } else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT - num == ilen) { - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = second; - } else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT - num == ilen) { - esp_time.hour = hour; - esp_time.minute = minute; - esp_time.second = 0; - } else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT - num == ilen) { - esp_time.year = year; - esp_time.month = month; - esp_time.day_of_month = day; - } else { - return false; +// Helper to parse exactly N digits, returns false if not enough digits +static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) { + value = 0; + for (int i = 0; i < count; i++) { + if (p >= end || *p < '0' || *p > '9') + return false; + value = value * 10 + (*p - '0'); + p++; } return true; } +// Helper to check for expected character +static bool expect_char(const char *&p, const char *end, char expected) { + if (p >= end || *p != expected) + return false; + p++; + return true; +} + +bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) { + // Supported formats: + // YYYY-MM-DD HH:MM:SS (19 chars) + // YYYY-MM-DD HH:MM (16 chars) + // YYYY-MM-DD (10 chars) + // HH:MM:SS (8 chars) + // HH:MM (5 chars) + + if (time_to_parse == nullptr || len == 0) + return false; + + const char *p = time_to_parse; + const char *end = time_to_parse + len; + uint16_t v1, v2, v3, v4, v5, v6; + + // Try date formats first (start with 4-digit year) + if (len >= 10 && time_to_parse[4] == '-') { + // YYYY-MM-DD... + if (!parse_digits(p, end, 4, v1)) + return false; + if (!expect_char(p, end, '-')) + return false; + if (!parse_digits(p, end, 2, v2)) + return false; + if (!expect_char(p, end, '-')) + return false; + if (!parse_digits(p, end, 2, v3)) + return false; + + esp_time.year = v1; + esp_time.month = v2; + esp_time.day_of_month = v3; + + if (p == end) { + // YYYY-MM-DD (date only) + return true; + } + + if (!expect_char(p, end, ' ')) + return false; + + // Continue with time part: HH:MM[:SS] + if (!parse_digits(p, end, 2, v4)) + return false; + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v5)) + return false; + + esp_time.hour = v4; + esp_time.minute = v5; + + if (p == end) { + // YYYY-MM-DD HH:MM + esp_time.second = 0; + return true; + } + + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v6)) + return false; + + esp_time.second = v6; + return p == end; // YYYY-MM-DD HH:MM:SS + } + + // Try time-only formats (HH:MM[:SS]) + if (len >= 5 && time_to_parse[2] == ':') { + if (!parse_digits(p, end, 2, v1)) + return false; + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v2)) + return false; + + esp_time.hour = v1; + esp_time.minute = v2; + + if (p == end) { + // HH:MM + esp_time.second = 0; + return true; + } + + if (!expect_char(p, end, ':')) + return false; + if (!parse_digits(p, end, 2, v3)) + return false; + + esp_time.second = v3; + return p == end; // HH:MM:SS + } + + return false; +} + void ESPTime::increment_second() { this->timestamp++; if (!increment_time_value(this->second, 0, 60)) @@ -193,27 +257,67 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { } void ESPTime::recalc_timestamp_local() { - struct tm tm; +#ifdef USE_TIME_TIMEZONE + // Calculate timestamp as if fields were UTC + this->recalc_timestamp_utc(false); + if (this->timestamp == -1) { + return; // Invalid time + } - tm.tm_year = this->year - 1900; - tm.tm_mon = this->month - 1; - tm.tm_mday = this->day_of_month; - tm.tm_hour = this->hour; - tm.tm_min = this->minute; - tm.tm_sec = this->second; - tm.tm_isdst = -1; + // Now convert from local to UTC by adding the offset + // POSIX: local = utc - offset, so utc = local + offset + const auto &tz = time::get_global_tz(); - this->timestamp = mktime(&tm); + if (!tz.has_dst()) { + // No DST - just apply standard offset + this->timestamp += tz.std_offset_seconds; + return; + } + + // Try both interpretations to match libc mktime() with tm_isdst=-1 + // For ambiguous times (fall-back repeated hour), prefer standard time + // For invalid times (spring-forward skipped hour), libc normalizes forward + time_t utc_if_dst = this->timestamp + tz.dst_offset_seconds; + time_t utc_if_std = this->timestamp + tz.std_offset_seconds; + + bool dst_valid = time::is_in_dst(utc_if_dst, tz); + bool std_valid = !time::is_in_dst(utc_if_std, tz); + + if (dst_valid && std_valid) { + // Ambiguous time (repeated hour during fall-back) - prefer standard time + this->timestamp = utc_if_std; + } else if (dst_valid) { + // Only DST interpretation is valid + this->timestamp = utc_if_dst; + } else if (std_valid) { + // Only standard interpretation is valid + this->timestamp = utc_if_std; + } else { + // Invalid time (skipped hour during spring-forward) + // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT + // Using std offset achieves this since the UTC result falls during DST + this->timestamp = utc_if_std; + } +#else + // No timezone support - treat as UTC + this->recalc_timestamp_utc(false); +#endif } int32_t ESPTime::timezone_offset() { +#ifdef USE_TIME_TIMEZONE time_t now = ::time(nullptr); - struct tm local_tm = *::localtime(&now); - local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset. - time_t local_time = mktime(&local_tm); - struct tm utc_tm = *::gmtime(&now); - time_t utc_time = mktime(&utc_tm); - return static_cast(local_time - utc_time); + const auto &tz = time::get_global_tz(); + // POSIX offset is positive west, but we return offset to add to UTC to get local + // So we negate the POSIX offset + if (time::is_in_dst(now, tz)) { + return -tz.dst_offset_seconds; + } + return -tz.std_offset_seconds; +#else + // No timezone support - no offset + return 0; +#endif } bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; } diff --git a/esphome/core/time.h b/esphome/core/time.h index d9ce86131c3..874f0db4b4a 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -7,6 +7,10 @@ #include #include +#ifdef USE_TIME_TIMEZONE +#include "esphome/components/time/posix_tz.h" +#endif + namespace esphome { template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); @@ -105,11 +109,17 @@ struct ESPTime { * @return The generated ESPTime */ static ESPTime from_epoch_local(time_t epoch) { - struct tm *c_tm = ::localtime(&epoch); - if (c_tm == nullptr) { - return ESPTime{}; // Return an invalid ESPTime +#ifdef USE_TIME_TIMEZONE + struct tm local_tm; + if (time::epoch_to_local_tm(epoch, time::get_global_tz(), &local_tm)) { + return ESPTime::from_c_tm(&local_tm, epoch); } - return ESPTime::from_c_tm(c_tm, epoch); + // Fallback to UTC if conversion failed + return ESPTime::from_epoch_utc(epoch); +#else + // No timezone support - return UTC (no TZ configured, localtime would return UTC anyway) + return ESPTime::from_epoch_utc(epoch); +#endif } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. * diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index e97b5bd7b00..78b65092ae4 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -66,6 +66,7 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: ], "build_flags": [ "-Og", # optimize for debug + "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp new file mode 100644 index 00000000000..d1747ef5b19 --- /dev/null +++ b/tests/components/time/posix_tz_parser.cpp @@ -0,0 +1,1275 @@ +// Tests for the POSIX TZ parser, time conversion functions, and ESPTime::strptime. +// +// Most tests here cover the C++ POSIX TZ string parser (parse_posix_tz), which is +// bridge code for backward compatibility — it will be removed before ESPHome 2026.9.0. +// After https://github.com/esphome/esphome/pull/14233 merges, the parser is solely +// used to handle timezone strings from Home Assistant clients older than 2026.3.0 +// that haven't been updated to send the pre-parsed ParsedTimezone protobuf struct. +// See https://github.com/esphome/backlog/issues/91 +// +// The epoch_to_local_tm, is_in_dst, and ESPTime::strptime tests cover conversion +// functions that will remain permanently. + +// Enable USE_TIME_TIMEZONE for tests +#define USE_TIME_TIMEZONE + +#include +#include +#include +#include "esphome/components/time/posix_tz.h" +#include "esphome/core/time.h" + +namespace esphome::time::testing { + +// Helper to create UTC epoch from date/time components (for test readability) +static time_t make_utc(int year, int month, int day, int hour = 0, int min = 0, int sec = 0) { + int64_t days = 0; + for (int y = 1970; y < year; y++) { + days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; + } + static const int DAYS_BEFORE[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + days += DAYS_BEFORE[month - 1]; + if (month > 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) + days++; // Leap year adjustment + days += day - 1; + return days * 86400 + hour * 3600 + min * 60 + sec; +} + +// ============================================================================ +// Basic TZ string parsing tests +// ============================================================================ + +TEST(PosixTzParser, ParseSimpleOffsetEST5) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); // +5 hours (west of UTC) + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseNegativeOffsetCET) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CET-1", tz)); + EXPECT_EQ(tz.std_offset_seconds, -1 * 3600); // -1 hour (east of UTC) + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseExplicitPositiveOffset) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("TEST+5", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseZeroOffset) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 0); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseUSEasternWithDST) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0,M11.1.0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_start.week, 2); + EXPECT_EQ(tz.dst_start.day_of_week, 0); // Sunday + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // Default 2:00 AM + EXPECT_EQ(tz.dst_end.month, 11); + EXPECT_EQ(tz.dst_end.week, 1); + EXPECT_EQ(tz.dst_end.day_of_week, 0); +} + +TEST(PosixTzParser, ParseUSCentralWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CST6CDT,M3.2.0/2,M11.1.0/2", tz)); + EXPECT_EQ(tz.std_offset_seconds, 6 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); // 2:00 AM + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, ParseEuropeBerlin) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -1 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -2 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_start.week, 5); // Last week + EXPECT_EQ(tz.dst_end.month, 10); + EXPECT_EQ(tz.dst_end.week, 5); // Last week + EXPECT_EQ(tz.dst_end.time_seconds, 3 * 3600); // 3:00 AM +} + +TEST(PosixTzParser, ParseNewZealand) { + ParsedTimezone tz; + // Southern hemisphere - DST starts in Sept, ends in April + ASSERT_TRUE(parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -12 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -13 * 3600); // Default: STD - 1hr + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 9); // September + EXPECT_EQ(tz.dst_end.month, 4); // April +} + +TEST(PosixTzParser, ParseExplicitDstOffset) { + ParsedTimezone tz; + // Some places have non-standard DST offsets + ASSERT_TRUE(parse_posix_tz("TEST5DST4,M3.2.0,M11.1.0", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, 4 * 3600); + EXPECT_TRUE(tz.has_dst()); +} + +// ============================================================================ +// Angle-bracket notation tests (espressif/newlib-esp32#8) +// ============================================================================ + +TEST(PosixTzParser, ParseAngleBracketPositive) { + // Format: <+07>-7 means UTC+7 (name is "+07", offset is -7 hours east) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+07>-7", tz)); + EXPECT_EQ(tz.std_offset_seconds, -7 * 3600); // -7 = 7 hours east of UTC + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketNegative) { + // <-03>3 means UTC-3 (name is "-03", offset is 3 hours west) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<-03>3", tz)); + EXPECT_EQ(tz.std_offset_seconds, 3 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketWithDST) { + // <+10>-10<+11>,M10.1.0,M4.1.0/3 (Australia/Sydney style) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+10>-10<+11>,M10.1.0,M4.1.0/3", tz)); + EXPECT_EQ(tz.std_offset_seconds, -10 * 3600); + EXPECT_EQ(tz.dst_offset_seconds, -11 * 3600); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 10); + EXPECT_EQ(tz.dst_end.month, 4); +} + +TEST(PosixTzParser, ParseAngleBracketNamed) { + // -10 (Australian Eastern Standard Time) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("-10", tz)); + EXPECT_EQ(tz.std_offset_seconds, -10 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseAngleBracketWithMinutes) { + // <+0545>-5:45 (Nepal) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+0545>-5:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +// ============================================================================ +// Half-hour and unusual offset tests +// ============================================================================ + +TEST(PosixTzParser, ParseOffsetWithMinutesIndia) { + ParsedTimezone tz; + // India: UTC+5:30 + ASSERT_TRUE(parse_posix_tz("IST-5:30", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 30 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseOffsetWithMinutesNepal) { + ParsedTimezone tz; + // Nepal: UTC+5:45 + ASSERT_TRUE(parse_posix_tz("NPT-5:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(5 * 3600 + 45 * 60)); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, ParseOffsetWithSeconds) { + ParsedTimezone tz; + // Unusual but valid: offset with seconds + ASSERT_TRUE(parse_posix_tz("TEST-1:30:30", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(1 * 3600 + 30 * 60 + 30)); +} + +TEST(PosixTzParser, ParseChathamIslands) { + // Chatham Islands: UTC+12:45 with DST + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45", tz)); + EXPECT_EQ(tz.std_offset_seconds, -(12 * 3600 + 45 * 60)); + EXPECT_EQ(tz.dst_offset_seconds, -(13 * 3600 + 45 * 60)); + EXPECT_TRUE(tz.has_dst()); +} + +// ============================================================================ +// Invalid input tests +// ============================================================================ + +TEST(PosixTzParser, ParseEmptyStringFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("", tz)); +} + +TEST(PosixTzParser, ParseNullFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz(nullptr, tz)); +} + +TEST(PosixTzParser, ParseShortNameFails) { + ParsedTimezone tz; + // TZ name must be at least 3 characters + EXPECT_FALSE(parse_posix_tz("AB5", tz)); +} + +TEST(PosixTzParser, ParseMissingOffsetFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("EST", tz)); +} + +TEST(PosixTzParser, ParseUnterminatedBracketFails) { + ParsedTimezone tz; + EXPECT_FALSE(parse_posix_tz("<+07-7", tz)); // Missing closing > +} + +// ============================================================================ +// J-format and plain day number tests +// ============================================================================ + +TEST(PosixTzParser, ParseJFormatBasic) { + ParsedTimezone tz; + // J format: Julian day 1-365, not counting Feb 29 + ASSERT_TRUE(parse_posix_tz("EST5EDT,J60,J305", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_start.day, 60); // March 1 + EXPECT_EQ(tz.dst_end.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_end.day, 305); // November 1 +} + +TEST(PosixTzParser, ParseJFormatWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,J60/2,J305/2", tz)); + EXPECT_EQ(tz.dst_start.day, 60); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600); + EXPECT_EQ(tz.dst_end.day, 305); + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, ParsePlainDayNumber) { + ParsedTimezone tz; + // Plain format: day 0-365, counting Feb 29 in leap years + ASSERT_TRUE(parse_posix_tz("EST5EDT,59,304", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::DAY_OF_YEAR); + EXPECT_EQ(tz.dst_start.day, 59); + EXPECT_EQ(tz.dst_end.type, DSTRuleType::DAY_OF_YEAR); + EXPECT_EQ(tz.dst_end.day, 304); +} + +TEST(PosixTzParser, JFormatInvalidDayZero) { + ParsedTimezone tz; + // J format day must be 1-365, not 0 + EXPECT_FALSE(parse_posix_tz("EST5EDT,J0,J305", tz)); +} + +TEST(PosixTzParser, JFormatInvalidDay366) { + ParsedTimezone tz; + // J format day must be 1-365 + EXPECT_FALSE(parse_posix_tz("EST5EDT,J366,J305", tz)); +} + +TEST(PosixTzParser, ParsePlainDayNumberWithTime) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,59/3,304/1:30", tz)); + EXPECT_EQ(tz.dst_start.day, 59); + EXPECT_EQ(tz.dst_start.time_seconds, 3 * 3600); + EXPECT_EQ(tz.dst_end.day, 304); + EXPECT_EQ(tz.dst_end.time_seconds, 1 * 3600 + 30 * 60); +} + +TEST(PosixTzParser, PlainDayInvalidDay366) { + ParsedTimezone tz; + // Plain format day must be 0-365 + EXPECT_FALSE(parse_posix_tz("EST5EDT,366,304", tz)); +} + +// ============================================================================ +// Transition time edge cases (POSIX V3 allows -167 to +167 hours) +// ============================================================================ + +TEST(PosixTzParser, NegativeTransitionTime) { + ParsedTimezone tz; + // Negative transition time: /-1 means 11 PM (23:00) the previous day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/-1,M11.1.0/2", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, -1 * 3600); // -1 hour = 11 PM previous day + EXPECT_EQ(tz.dst_end.time_seconds, 2 * 3600); +} + +TEST(PosixTzParser, NegativeTransitionTimeWithMinutes) { + ParsedTimezone tz; + // /-1:30 means 10:30 PM the previous day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/-1:30,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, -(1 * 3600 + 30 * 60)); +} + +TEST(PosixTzParser, LargeTransitionTime) { + ParsedTimezone tz; + // POSIX V3 allows transition times from -167 to +167 hours + // /25 means 1:00 AM the next day + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/25,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 25 * 3600); +} + +TEST(PosixTzParser, MaxTransitionTime167Hours) { + ParsedTimezone tz; + // Maximum allowed transition time per POSIX V3 + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/167,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 167 * 3600); +} + +TEST(PosixTzParser, TransitionTimeWithHoursMinutesSeconds) { + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2:30:45,M11.1.0", tz)); + EXPECT_EQ(tz.dst_start.time_seconds, 2 * 3600 + 30 * 60 + 45); +} + +// ============================================================================ +// Invalid M format tests +// ============================================================================ + +TEST(PosixTzParser, MFormatInvalidMonth13) { + ParsedTimezone tz; + // Month must be 1-12 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M13.1.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidMonth0) { + ParsedTimezone tz; + // Month must be 1-12 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M0.1.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidWeek6) { + ParsedTimezone tz; + // Week must be 1-5 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.6.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidWeek0) { + ParsedTimezone tz; + // Week must be 1-5 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.0.0,M11.1.0", tz)); +} + +TEST(PosixTzParser, MFormatInvalidDayOfWeek7) { + ParsedTimezone tz; + // Day of week must be 0-6 + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.2.7,M11.1.0", tz)); +} + +TEST(PosixTzParser, MissingEndRule) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,M3.2.0", tz)); +} + +TEST(PosixTzParser, MissingEndRuleJFormat) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,J60", tz)); +} + +TEST(PosixTzParser, MissingEndRulePlainDay) { + ParsedTimezone tz; + // POSIX requires both start and end rules if any rules are specified + EXPECT_FALSE(parse_posix_tz("EST5EDT,60", tz)); +} + +TEST(PosixTzParser, LowercaseMFormat) { + ParsedTimezone tz; + // Lowercase 'm' should be accepted + ASSERT_TRUE(parse_posix_tz("EST5EDT,m3.2.0,m11.1.0", tz)); + EXPECT_TRUE(tz.has_dst()); + EXPECT_EQ(tz.dst_start.month, 3); + EXPECT_EQ(tz.dst_end.month, 11); +} + +TEST(PosixTzParser, LowercaseJFormat) { + ParsedTimezone tz; + // Lowercase 'j' should be accepted + ASSERT_TRUE(parse_posix_tz("EST5EDT,j60,j305", tz)); + EXPECT_EQ(tz.dst_start.type, DSTRuleType::JULIAN_NO_LEAP); + EXPECT_EQ(tz.dst_start.day, 60); +} + +TEST(PosixTzParser, DstNameWithoutRules) { + ParsedTimezone tz; + // DST name present but no rules - treat as no DST since we can't determine transitions + ASSERT_TRUE(parse_posix_tz("EST5EDT", tz)); + EXPECT_FALSE(tz.has_dst()); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); +} + +TEST(PosixTzParser, TrailingCharactersIgnored) { + ParsedTimezone tz; + // Trailing characters after valid TZ should be ignored (parser stops at end of valid input) + // This matches libc behavior + ASSERT_TRUE(parse_posix_tz("EST5 extra garbage here", tz)); + EXPECT_EQ(tz.std_offset_seconds, 5 * 3600); + EXPECT_FALSE(tz.has_dst()); +} + +TEST(PosixTzParser, PlainDay365LeapYear) { + // Day 365 in leap year is Dec 31 + int month, day; + internal::day_of_year_to_month_day(365, 2024, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, PlainDay364NonLeapYear) { + // Day 364 (0-indexed) is Dec 31 in non-leap year (last valid day) + int month, day; + internal::day_of_year_to_month_day(364, 2025, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +// ============================================================================ +// Large offset tests +// ============================================================================ + +TEST(PosixTzParser, MaxOffset14Hours) { + ParsedTimezone tz; + // Line Islands (Kiribati) is UTC+14, the maximum offset + ASSERT_TRUE(parse_posix_tz("<+14>-14", tz)); + EXPECT_EQ(tz.std_offset_seconds, -14 * 3600); +} + +TEST(PosixTzParser, MaxNegativeOffset12Hours) { + ParsedTimezone tz; + // Baker Island is UTC-12 + ASSERT_TRUE(parse_posix_tz("<-12>12", tz)); + EXPECT_EQ(tz.std_offset_seconds, 12 * 3600); +} + +// ============================================================================ +// Helper function tests +// ============================================================================ + +TEST(PosixTzParser, JulianDay60IsMarch1) { + // J60 is always March 1 (J format ignores leap years by design) + int month, day; + internal::julian_to_month_day(60, month, day); + EXPECT_EQ(month, 3); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DayOfYear59DiffersByLeap) { + int month, day; + // Day 59 in leap year is Feb 29 + internal::day_of_year_to_month_day(59, 2024, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 29); + // Day 59 in non-leap year is March 1 + internal::day_of_year_to_month_day(59, 2025, month, day); + EXPECT_EQ(month, 3); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DayOfWeekKnownDates) { + // January 1, 1970 was Thursday (4) + EXPECT_EQ(internal::day_of_week(1970, 1, 1), 4); + // January 1, 2000 was Saturday (6) + EXPECT_EQ(internal::day_of_week(2000, 1, 1), 6); + // March 8, 2026 is Sunday (0) - US DST start + EXPECT_EQ(internal::day_of_week(2026, 3, 8), 0); +} + +TEST(PosixTzParser, LeapYearDetection) { + EXPECT_FALSE(internal::is_leap_year(1900)); // Divisible by 100 but not 400 + EXPECT_TRUE(internal::is_leap_year(2000)); // Divisible by 400 + EXPECT_TRUE(internal::is_leap_year(2024)); // Divisible by 4 + EXPECT_FALSE(internal::is_leap_year(2025)); // Not divisible by 4 +} + +TEST(PosixTzParser, JulianDay1IsJan1) { + int month, day; + internal::julian_to_month_day(1, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, JulianDay31IsJan31) { + int month, day; + internal::julian_to_month_day(31, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, JulianDay32IsFeb1) { + int month, day; + internal::julian_to_month_day(32, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, JulianDay59IsFeb28) { + int month, day; + internal::julian_to_month_day(59, month, day); + EXPECT_EQ(month, 2); + EXPECT_EQ(day, 28); +} + +TEST(PosixTzParser, JulianDay365IsDec31) { + int month, day; + internal::julian_to_month_day(365, month, day); + EXPECT_EQ(month, 12); + EXPECT_EQ(day, 31); +} + +TEST(PosixTzParser, DayOfYear0IsJan1) { + int month, day; + internal::day_of_year_to_month_day(0, 2025, month, day); + EXPECT_EQ(month, 1); + EXPECT_EQ(day, 1); +} + +TEST(PosixTzParser, DaysInMonthRegular) { + // Test all 12 months to ensure switch coverage + EXPECT_EQ(internal::days_in_month(2025, 1), 31); // Jan - default case + EXPECT_EQ(internal::days_in_month(2025, 2), 28); // Feb - case 2 + EXPECT_EQ(internal::days_in_month(2025, 3), 31); // Mar - default case + EXPECT_EQ(internal::days_in_month(2025, 4), 30); // Apr - case 4 + EXPECT_EQ(internal::days_in_month(2025, 5), 31); // May - default case + EXPECT_EQ(internal::days_in_month(2025, 6), 30); // Jun - case 6 + EXPECT_EQ(internal::days_in_month(2025, 7), 31); // Jul - default case + EXPECT_EQ(internal::days_in_month(2025, 8), 31); // Aug - default case + EXPECT_EQ(internal::days_in_month(2025, 9), 30); // Sep - case 9 + EXPECT_EQ(internal::days_in_month(2025, 10), 31); // Oct - default case + EXPECT_EQ(internal::days_in_month(2025, 11), 30); // Nov - case 11 + EXPECT_EQ(internal::days_in_month(2025, 12), 31); // Dec - default case +} + +TEST(PosixTzParser, DaysInMonthLeapYear) { + EXPECT_EQ(internal::days_in_month(2024, 2), 29); + EXPECT_EQ(internal::days_in_month(2025, 2), 28); +} + +// ============================================================================ +// DST transition calculation tests +// ============================================================================ + +TEST(PosixTzParser, DstStartUSEastern2026) { + // March 8, 2026 is 2nd Sunday of March + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + time_t dst_start = internal::calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds); + struct tm tm; + internal::epoch_to_tm_utc(dst_start, &tm); + + // At 2:00 AM EST (UTC-5), so 7:00 AM UTC + EXPECT_EQ(tm.tm_year + 1900, 2026); + EXPECT_EQ(tm.tm_mon + 1, 3); // March + EXPECT_EQ(tm.tm_mday, 8); // 8th + EXPECT_EQ(tm.tm_hour, 7); // 7:00 UTC = 2:00 EST +} + +TEST(PosixTzParser, DstEndUSEastern2026) { + // November 1, 2026 is 1st Sunday of November + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + time_t dst_end = internal::calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds); + struct tm tm; + internal::epoch_to_tm_utc(dst_end, &tm); + + // At 2:00 AM EDT (UTC-4), so 6:00 AM UTC + EXPECT_EQ(tm.tm_year + 1900, 2026); + EXPECT_EQ(tm.tm_mon + 1, 11); // November + EXPECT_EQ(tm.tm_mday, 1); // 1st + EXPECT_EQ(tm.tm_hour, 6); // 6:00 UTC = 2:00 EDT +} + +TEST(PosixTzParser, LastSundayOfMarch2026) { + // Europe: M3.5.0 = last Sunday of March = March 29, 2026 + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 3; + rule.week = 5; + rule.day_of_week = 0; + rule.time_seconds = 2 * 3600; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 29); + EXPECT_EQ(tm.tm_wday, 0); // Sunday +} + +TEST(PosixTzParser, LastSundayOfOctober2026) { + // Europe: M10.5.0 = last Sunday of October = October 25, 2026 + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 10; + rule.week = 5; + rule.day_of_week = 0; + rule.time_seconds = 3 * 3600; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 25); + EXPECT_EQ(tm.tm_wday, 0); // Sunday +} + +TEST(PosixTzParser, FirstSundayOfApril2026) { + // April 5, 2026 is 1st Sunday + DSTRule rule{}; + rule.type = DSTRuleType::MONTH_WEEK_DAY; + rule.month = 4; + rule.week = 1; + rule.day_of_week = 0; + rule.time_seconds = 0; + time_t transition = internal::calculate_dst_transition(2026, rule, 0); + struct tm tm; + internal::epoch_to_tm_utc(transition, &tm); + EXPECT_EQ(tm.tm_mday, 5); + EXPECT_EQ(tm.tm_wday, 0); +} + +// ============================================================================ +// DST detection tests +// ============================================================================ + +TEST(PosixTzParser, IsInDstUSEasternSummer) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // July 4, 2026 12:00 UTC - definitely in DST + time_t summer = make_utc(2026, 7, 4, 12); + EXPECT_TRUE(is_in_dst(summer, tz)); +} + +TEST(PosixTzParser, IsInDstUSEasternWinter) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // January 15, 2026 12:00 UTC - definitely not in DST + time_t winter = make_utc(2026, 1, 15, 12); + EXPECT_FALSE(is_in_dst(winter, tz)); +} + +TEST(PosixTzParser, IsInDstNoDstTimezone) { + ParsedTimezone tz; + parse_posix_tz("IST-5:30", tz); + + // July 15, 2026 12:00 UTC + time_t epoch = make_utc(2026, 7, 15, 12); + EXPECT_FALSE(is_in_dst(epoch, tz)); +} + +TEST(PosixTzParser, SouthernHemisphereDstSummer) { + ParsedTimezone tz; + parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz); + + // December 15, 2025 12:00 UTC - summer in NZ, should be in DST + time_t nz_summer = make_utc(2025, 12, 15, 12); + EXPECT_TRUE(is_in_dst(nz_summer, tz)); +} + +TEST(PosixTzParser, SouthernHemisphereDstWinter) { + ParsedTimezone tz; + parse_posix_tz("NZST-12NZDT,M9.5.0,M4.1.0/3", tz); + + // July 15, 2026 12:00 UTC - winter in NZ, should NOT be in DST + time_t nz_winter = make_utc(2026, 7, 15, 12); + EXPECT_FALSE(is_in_dst(nz_winter, tz)); +} + +// ============================================================================ +// epoch_to_local_tm tests +// ============================================================================ + +TEST(PosixTzParser, EpochToLocalBasic) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + + time_t epoch = 0; // Jan 1, 1970 00:00:00 UTC + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 70); + EXPECT_EQ(local.tm_mon, 0); + EXPECT_EQ(local.tm_mday, 1); + EXPECT_EQ(local.tm_hour, 0); +} + +TEST(PosixTzParser, EpochToLocalNegativeEpoch) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + + // Dec 31, 1969 23:59:59 UTC (1 second before epoch) + time_t epoch = -1; + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 69); // 1969 + EXPECT_EQ(local.tm_mon, 11); // December + EXPECT_EQ(local.tm_mday, 31); + EXPECT_EQ(local.tm_hour, 23); + EXPECT_EQ(local.tm_min, 59); + EXPECT_EQ(local.tm_sec, 59); +} + +TEST(PosixTzParser, EpochToLocalNullTmFails) { + ParsedTimezone tz; + parse_posix_tz("UTC0", tz); + EXPECT_FALSE(epoch_to_local_tm(0, tz, nullptr)); +} + +TEST(PosixTzParser, EpochToLocalWithOffset) { + ParsedTimezone tz; + parse_posix_tz("EST5", tz); // UTC-5 + + // Jan 1, 2026 05:00:00 UTC should be Jan 1, 2026 00:00:00 EST + time_t utc_epoch = make_utc(2026, 1, 1, 5); + + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 0); // Midnight EST + EXPECT_EQ(local.tm_mday, 1); + EXPECT_EQ(local.tm_isdst, 0); +} + +TEST(PosixTzParser, EpochToLocalDstTransition) { + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // July 4, 2026 16:00 UTC = 12:00 EDT (noon) + time_t utc_epoch = make_utc(2026, 7, 4, 16); + + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(utc_epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 12); // Noon EDT + EXPECT_EQ(local.tm_isdst, 1); +} + +// ============================================================================ +// Verification against libc +// ============================================================================ + +class LibcVerificationTest : public ::testing::TestWithParam> { + protected: + // NOLINTNEXTLINE(readability-identifier-naming) - Google Test requires this name + void SetUp() override { + // Save current TZ + const char *current_tz = getenv("TZ"); + saved_tz_ = current_tz ? current_tz : ""; + had_tz_ = current_tz != nullptr; + } + + // NOLINTNEXTLINE(readability-identifier-naming) - Google Test requires this name + void TearDown() override { + // Restore TZ + if (had_tz_) { + setenv("TZ", saved_tz_.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); + } + + private: + std::string saved_tz_; + bool had_tz_{false}; +}; + +TEST_P(LibcVerificationTest, MatchesLibc) { + auto [tz_str, epoch] = GetParam(); + + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + + // Our implementation + struct tm our_tm {}; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &our_tm)); + + // libc implementation + setenv("TZ", tz_str, 1); + tzset(); + struct tm *libc_tm = localtime(&epoch); + ASSERT_NE(libc_tm, nullptr); + + EXPECT_EQ(our_tm.tm_year, libc_tm->tm_year); + EXPECT_EQ(our_tm.tm_mon, libc_tm->tm_mon); + EXPECT_EQ(our_tm.tm_mday, libc_tm->tm_mday); + EXPECT_EQ(our_tm.tm_hour, libc_tm->tm_hour); + EXPECT_EQ(our_tm.tm_min, libc_tm->tm_min); + EXPECT_EQ(our_tm.tm_sec, libc_tm->tm_sec); + EXPECT_EQ(our_tm.tm_isdst, libc_tm->tm_isdst); +} + +INSTANTIATE_TEST_SUITE_P(USEastern, LibcVerificationTest, + ::testing::Values(std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1704067200), + std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1720000000), + std::make_tuple("EST5EDT,M3.2.0/2,M11.1.0/2", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(AngleBracket, LibcVerificationTest, + ::testing::Values(std::make_tuple("<+07>-7", 1704067200), + std::make_tuple("<+07>-7", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(India, LibcVerificationTest, + ::testing::Values(std::make_tuple("IST-5:30", 1704067200), + std::make_tuple("IST-5:30", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(NewZealand, LibcVerificationTest, + ::testing::Values(std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1704067200), + std::make_tuple("NZST-12NZDT,M9.5.0,M4.1.0/3", 1720000000))); + +INSTANTIATE_TEST_SUITE_P(USCentral, LibcVerificationTest, + ::testing::Values(std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1704067200), + std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1720000000), + std::make_tuple("CST6CDT,M3.2.0/2,M11.1.0/2", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(EuropeBerlin, LibcVerificationTest, + ::testing::Values(std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1704067200), + std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1720000000), + std::make_tuple("CET-1CEST,M3.5.0,M10.5.0/3", 1735689600))); + +INSTANTIATE_TEST_SUITE_P(AustraliaSydney, LibcVerificationTest, + ::testing::Values(std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1704067200), + std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1720000000), + std::make_tuple("AEST-10AEDT,M10.1.0,M4.1.0/3", 1735689600))); + +// ============================================================================ +// DST boundary edge cases +// ============================================================================ + +TEST(PosixTzParser, DstBoundaryJustBeforeSpringForward) { + // Test 1 second before DST starts + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // March 8, 2026 06:59:59 UTC = 01:59:59 EST (1 second before spring forward) + time_t before_epoch = make_utc(2026, 3, 8, 6, 59, 59); + EXPECT_FALSE(is_in_dst(before_epoch, tz)); + + // March 8, 2026 07:00:00 UTC = 02:00:00 EST -> 03:00:00 EDT (DST started) + time_t after_epoch = make_utc(2026, 3, 8, 7); + EXPECT_TRUE(is_in_dst(after_epoch, tz)); +} + +TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) { + // Test 1 second before DST ends + ParsedTimezone tz; + parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz); + + // November 1, 2026 05:59:59 UTC = 01:59:59 EDT (1 second before fall back) + time_t before_epoch = make_utc(2026, 11, 1, 5, 59, 59); + EXPECT_TRUE(is_in_dst(before_epoch, tz)); + + // November 1, 2026 06:00:00 UTC = 02:00:00 EDT -> 01:00:00 EST (DST ended) + time_t after_epoch = make_utc(2026, 11, 1, 6); + EXPECT_FALSE(is_in_dst(after_epoch, tz)); +} + +} // namespace esphome::time::testing + +// ============================================================================ +// ESPTime::strptime tests (replaces sscanf-based parsing) +// ============================================================================ + +namespace esphome::testing { + +TEST(ESPTimeStrptime, FullDateTime) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30:45", 19, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 45); +} + +TEST(ESPTimeStrptime, DateTimeNoSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30", 16, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, DateOnly) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-03-15", 10, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 3); + EXPECT_EQ(t.day_of_month, 15); +} + +TEST(ESPTimeStrptime, TimeWithSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("14:30:45", 8, t)); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 45); +} + +TEST(ESPTimeStrptime, TimeNoSeconds) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("14:30", 5, t)); + EXPECT_EQ(t.hour, 14); + EXPECT_EQ(t.minute, 30); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, Midnight) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("00:00:00", 8, t)); + EXPECT_EQ(t.hour, 0); + EXPECT_EQ(t.minute, 0); + EXPECT_EQ(t.second, 0); +} + +TEST(ESPTimeStrptime, EndOfDay) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("23:59:59", 8, t)); + EXPECT_EQ(t.hour, 23); + EXPECT_EQ(t.minute, 59); + EXPECT_EQ(t.second, 59); +} + +TEST(ESPTimeStrptime, LeapYearDate) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2024-02-29", 10, t)); + EXPECT_EQ(t.year, 2024); + EXPECT_EQ(t.month, 2); + EXPECT_EQ(t.day_of_month, 29); +} + +TEST(ESPTimeStrptime, NewYearsEve) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("2026-12-31 23:59:59", 19, t)); + EXPECT_EQ(t.year, 2026); + EXPECT_EQ(t.month, 12); + EXPECT_EQ(t.day_of_month, 31); + EXPECT_EQ(t.hour, 23); + EXPECT_EQ(t.minute, 59); + EXPECT_EQ(t.second, 59); +} + +TEST(ESPTimeStrptime, EmptyStringFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("", 0, t)); +} + +TEST(ESPTimeStrptime, NullInputFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime(nullptr, 0, t)); +} + +TEST(ESPTimeStrptime, InvalidFormatFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("not-a-date", 10, t)); +} + +TEST(ESPTimeStrptime, PartialDateFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("2026-03", 7, t)); +} + +TEST(ESPTimeStrptime, PartialTimeFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("14:", 3, t)); +} + +TEST(ESPTimeStrptime, ExtraCharactersFails) { + ESPTime t{}; + // Full datetime with extra characters should fail + EXPECT_FALSE(ESPTime::strptime("2026-03-15 14:30:45x", 20, t)); +} + +TEST(ESPTimeStrptime, WrongSeparatorFails) { + ESPTime t{}; + EXPECT_FALSE(ESPTime::strptime("2026/03/15", 10, t)); +} + +TEST(ESPTimeStrptime, LeadingZeroTime) { + ESPTime t{}; + ASSERT_TRUE(ESPTime::strptime("01:05:09", 8, t)); + EXPECT_EQ(t.hour, 1); + EXPECT_EQ(t.minute, 5); + EXPECT_EQ(t.second, 9); +} + +// ============================================================================ +// recalc_timestamp_local() tests - verify behavior matches libc mktime() +// ============================================================================ + +// Helper to call libc mktime with same fields +static time_t libc_mktime(int year, int month, int day, int hour, int min, int sec) { + struct tm tm {}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + tm.tm_isdst = -1; // Let libc determine DST + return mktime(&tm); +} + +// Helper to create ESPTime and call recalc_timestamp_local +static time_t esptime_recalc_local(int year, int month, int day, int hour, int min, int sec) { + ESPTime t{}; + t.year = year; + t.month = month; + t.day_of_month = day; + t.hour = hour; + t.minute = min; + t.second = sec; + t.day_of_week = 1; // Placeholder for fields_in_range() + t.day_of_year = 1; + t.recalc_timestamp_local(); + return t.timestamp; +} + +TEST(RecalcTimestampLocal, NormalTimeMatchesLibc) { + // Set timezone to US Central (CST6CDT) + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test a normal time in winter (no DST) + // January 15, 2026 at 10:30:00 CST + time_t libc_result = libc_mktime(2026, 1, 15, 10, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 1, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test a normal time in summer (DST active) + // July 15, 2026 at 10:30:00 CDT + libc_result = libc_mktime(2026, 7, 15, 10, 30, 0); + esp_result = esptime_recalc_local(2026, 7, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, SpringForwardSkippedHour) { + // Set timezone to US Central (CST6CDT) + // DST starts March 8, 2026 at 2:00 AM -> clocks jump to 3:00 AM + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test time before the transition (1:30 AM CST exists) + time_t libc_result = libc_mktime(2026, 3, 8, 1, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 1, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test time after the transition (3:30 AM CDT exists) + libc_result = libc_mktime(2026, 3, 8, 3, 30, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 3, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test the skipped hour (2:30 AM doesn't exist - gets normalized) + // Both implementations should produce the same result + libc_result = libc_mktime(2026, 3, 8, 2, 30, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 2, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, FallBackRepeatedHour) { + // Set timezone to US Central (CST6CDT) + // DST ends November 1, 2026 at 2:00 AM -> clocks fall back to 1:00 AM + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test time before the transition (midnight CDT) + time_t libc_result = libc_mktime(2026, 11, 1, 0, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 11, 1, 0, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test time well after the transition (3:00 AM CST) + libc_result = libc_mktime(2026, 11, 1, 3, 0, 0); + esp_result = esptime_recalc_local(2026, 11, 1, 3, 0, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test the repeated hour (1:30 AM occurs twice) + // libc behavior varies by platform for this edge case, so we verify our + // consistent behavior: prefer standard time (later UTC timestamp) + esp_result = esptime_recalc_local(2026, 11, 1, 1, 30, 0); + time_t std_interpretation = esptime_recalc_local(2026, 11, 1, 2, 30, 0) - 3600; // 2:30 CST - 1 hour + EXPECT_EQ(esp_result, std_interpretation); +} + +TEST(RecalcTimestampLocal, SouthernHemisphereDST) { + // Set timezone to Australia/Sydney (AEST-10AEDT,M10.1.0,M4.1.0) + // DST starts first Sunday of October, ends first Sunday of April + const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Test winter time (July - no DST in southern hemisphere) + time_t libc_result = libc_mktime(2026, 7, 15, 10, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 7, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Test summer time (January - DST active in southern hemisphere) + libc_result = libc_mktime(2026, 1, 15, 10, 30, 0); + esp_result = esptime_recalc_local(2026, 1, 15, 10, 30, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, ExactTransitionBoundary) { + // Test exact boundary of spring forward transition + // Mar 8, 2026 at 2:00 AM CST -> 3:00 AM CDT (clocks skip forward) + const char *tz_str = "CST6CDT,M3.2.0,M11.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // 1:59:59 AM CST - last second before transition (still standard time) + time_t libc_result = libc_mktime(2026, 3, 8, 1, 59, 59); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 1, 59, 59); + EXPECT_EQ(esp_result, libc_result); + + // 3:00:00 AM CDT - first second after transition (now DST) + libc_result = libc_mktime(2026, 3, 8, 3, 0, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 3, 0, 0); + EXPECT_EQ(esp_result, libc_result); + + // Verify the gap: 3:00 AM CDT should be exactly 1 second after 1:59:59 AM CST + time_t before_transition = esptime_recalc_local(2026, 3, 8, 1, 59, 59); + time_t after_transition = esptime_recalc_local(2026, 3, 8, 3, 0, 0); + EXPECT_EQ(after_transition - before_transition, 1); +} + +TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { + // Test DST transition at 3:00 AM instead of default 2:00 AM + // Using custom transition time: CST6CDT,M3.2.0/3,M11.1.0/3 + const char *tz_str = "CST6CDT,M3.2.0/3,M11.1.0/3"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // 2:30 AM should still be standard time (transition at 3:00 AM) + time_t libc_result = libc_mktime(2026, 3, 8, 2, 30, 0); + time_t esp_result = esptime_recalc_local(2026, 3, 8, 2, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // 4:00 AM should be DST (after 3:00 AM transition) + libc_result = libc_mktime(2026, 3, 8, 4, 0, 0); + esp_result = esptime_recalc_local(2026, 3, 8, 4, 0, 0); + EXPECT_EQ(esp_result, libc_result); +} + +TEST(RecalcTimestampLocal, YearBoundaryDST) { + // Test southern hemisphere DST across year boundary + // Australia/Sydney: DST active from October to April (spans Jan 1) + const char *tz_str = "AEST-10AEDT,M10.1.0,M4.1.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Dec 31, 2025 at 23:30 - DST should be active + time_t libc_result = libc_mktime(2025, 12, 31, 23, 30, 0); + time_t esp_result = esptime_recalc_local(2025, 12, 31, 23, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Jan 1, 2026 at 00:30 - DST should still be active + libc_result = libc_mktime(2026, 1, 1, 0, 30, 0); + esp_result = esptime_recalc_local(2026, 1, 1, 0, 30, 0); + EXPECT_EQ(esp_result, libc_result); + + // Verify both are in DST (11 hour offset from UTC, not 10) + // The timestamps should be 1 hour apart + time_t dec31 = esptime_recalc_local(2025, 12, 31, 23, 30, 0); + time_t jan1 = esptime_recalc_local(2026, 1, 1, 0, 30, 0); + EXPECT_EQ(jan1 - dec31, 3600); // 1 hour difference +} + +// ============================================================================ +// ESPTime::timezone_offset() tests +// ============================================================================ + +TEST(TimezoneOffset, NoTimezone) { + // When no timezone is set, offset should be 0 + time::ParsedTimezone tz{}; + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + EXPECT_EQ(offset, 0); +} + +TEST(TimezoneOffset, FixedOffsetPositive) { + // India: UTC+5:30 (no DST) + const char *tz_str = "IST-5:30"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + // Offset should be +5:30 = 19800 seconds (to add to UTC to get local) + EXPECT_EQ(offset, 5 * 3600 + 30 * 60); +} + +TEST(TimezoneOffset, FixedOffsetNegative) { + // US Eastern Standard Time: UTC-5 (testing without DST rules) + const char *tz_str = "EST5"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + int32_t offset = ESPTime::timezone_offset(); + // Offset should be -5 hours = -18000 seconds + EXPECT_EQ(offset, -5 * 3600); +} + +TEST(TimezoneOffset, WithDstReturnsCorrectOffsetBasedOnCurrentTime) { + // US Eastern with DST + const char *tz_str = "EST5EDT,M3.2.0,M11.1.0"; + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Get current time and check offset matches expected based on DST status + time_t now = ::time(nullptr); + int32_t offset = ESPTime::timezone_offset(); + + // Verify offset matches what is_in_dst says + if (time::is_in_dst(now, tz)) { + // During DST, offset should be -4 hours (EDT) + EXPECT_EQ(offset, -4 * 3600); + } else { + // During standard time, offset should be -5 hours (EST) + EXPECT_EQ(offset, -5 * 3600); + } +} + +} // namespace esphome::testing From 50e7571f4c603f460a2dffa2b78eacc0a6dd5014 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:17:25 -0700 Subject: [PATCH 0252/2056] [web_server_idf] Prefer make_unique_for_overwrite for noninit recv buffer (#14279) --- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 4034a22586b..f18570965b7 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -921,7 +921,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c }); // Use heap buffer - 1460 bytes is too large for the httpd task stack - auto buffer = std::make_unique(MULTIPART_CHUNK_SIZE); + auto buffer = std::make_unique_for_overwrite(MULTIPART_CHUNK_SIZE); size_t bytes_since_yield = 0; for (size_t remaining = r->content_len; remaining > 0;) { From 15846137a6c64bbbc44345f29ff17d40ac92f1bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 18:17:52 -0700 Subject: [PATCH 0253/2056] [rp2040] Update arduino-pico framework from 3.9.4 to 5.5.0 (#14328) --- .clang-tidy.hash | 2 +- esphome/components/rp2040/__init__.py | 8 +-- esphome/components/rp2040/gpio.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 4 +- .../components/wifi/wifi_component_pico_w.cpp | 53 +++++++++++++------ platformio.ini | 4 +- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 777c8463715..767da3f33ec 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -5eb1e5852765114ad06533220d3160b6c23f5ccefc4de41828699de5dfff5ad6 +b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052 diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 3a1ea16fa3c..23f12e651f3 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -91,18 +91,18 @@ def _parse_platform_version(value): # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 9, 4) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 0) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags -RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.2.0-gcc12" +RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 9, 4), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(3, 9, 4), None), + "dev": (cv.Version(5, 5, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(5, 5, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp index 2b1699f888a..4b3c98104c5 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -106,7 +106,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { sio_hw->gpio_oe_set = arg->mask; } else if (flags & gpio::FLAG_INPUT) { sio_hw->gpio_oe_clr = arg->mask; - hw_write_masked(&padsbank0_hw->io[arg->pin], + hw_write_masked(&pads_bank0_hw->io[arg->pin], (bool_to_bit(flags & gpio::FLAG_PULLUP) << PADS_BANK0_GPIO0_PUE_LSB) | (bool_to_bit(flags & gpio::FLAG_PULLDOWN) << PADS_BANK0_GPIO0_PDE_LSB), PADS_BANK0_GPIO0_PUE_BITS | PADS_BANK0_GPIO0_PDE_BITS); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 0c6834055c3..faf8f4d90f1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -115,8 +115,8 @@ void RP2040UartComponent::setup() { if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { ESP_LOGV(TAG, "Using SerialPIO"); - pin_size_t tx = this->tx_pin_ == nullptr ? SerialPIO::NOPIN : this->tx_pin_->get_pin(); - pin_size_t rx = this->rx_pin_ == nullptr ? SerialPIO::NOPIN : this->rx_pin_->get_pin(); + pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin(); + pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin(); auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory) serial->begin(this->baud_rate_, config); if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1baf21e2b2c..9b2c077dc5e 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -78,8 +78,13 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; #endif - auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); - if (ret != WL_CONNECTED) + // Use beginNoBlock to avoid WiFi.begin()'s additional 2x timeout wait loop on top of + // CYW43::begin()'s internal blocking join. CYW43::begin() blocks for up to 10 seconds + // (default timeout) to complete the join - this is required because the LwipIntfDev netif + // setup depends on begin() succeeding. beginNoBlock() skips the outer wait loop, saving + // up to 20 additional seconds of blocking per attempt. + auto ret = WiFi.beginNoBlock(ap.ssid_.c_str(), ap.password_.c_str()); + if (ret == WL_IDLE_STATUS) return false; return true; @@ -116,13 +121,19 @@ const char *get_disconnect_reason_str(uint8_t reason) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino + // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif + // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's + // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. + // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. + int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: - case CYW43_LINK_NOIP: + // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class + if (WiFi.status() == WL_CONNECTED) { + return WiFiSTAConnectStatus::CONNECTED; + } return WiFiSTAConnectStatus::CONNECTING; - case CYW43_LINK_UP: - return WiFiSTAConnectStatus::CONNECTED; case CYW43_LINK_FAIL: case CYW43_LINK_BADAUTH: return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; @@ -139,18 +150,24 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { s_scan_result_count++; - const char *ssid_cstr = reinterpret_cast(result->ssid); + + // CYW43 scan results have ssid as a 32-byte buffer that is NOT null-terminated. + // Use ssid_len to create a properly terminated copy for string operations. + uint8_t len = std::min(result->ssid_len, static_cast(sizeof(result->ssid))); + char ssid_buf[33]; // 32 max + null terminator + memcpy(ssid_buf, result->ssid, len); + ssid_buf[len] = '\0'; // Skip networks that don't match any configured network (unless full results needed) - if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) { - this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel); + if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_buf, result->bssid)) { + this->log_discarded_scan_result_(ssid_buf, result->bssid, result->rssi, result->channel); return; } bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, - result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); + WiFiScanResult res(bssid, ssid_buf, len, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, + len == 0); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); } @@ -167,7 +184,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { ESP_LOGV(TAG, "cyw43_wifi_scan failed"); } return err == 0; - return true; } #ifdef USE_WIFI_AP @@ -212,8 +228,10 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t * #endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { - int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); - return err == 0; + // Use Arduino WiFi.disconnect() instead of raw cyw43_wifi_leave() to properly + // clean up the lwIP netif, DHCP client, and internal Arduino state. + WiFi.disconnect(); + return true; } bssid_t WiFiComponent::wifi_bssid() { @@ -269,9 +287,10 @@ void WiFiComponent::wifi_loop_() { // Poll for connection state changes // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, - // so we need to poll the link status to detect state changes - auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); - bool is_connected = (status == CYW43_LINK_UP); + // so we need to poll the link status to detect state changes. + // Use WiFi.connected() which checks both the WiFi link and IP address via the + // Arduino framework's own netif (not the SDK's uninitialized one). + bool is_connected = WiFi.connected(); // Detect connection state change if (is_connected && !s_sta_was_connected) { diff --git a/platformio.ini b/platformio.ini index e35dce2228b..16a1b18211c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -193,10 +193,10 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:arduino board_build.filesystem_size = 0.5m -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.2.0-gcc12 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.9.4/rp2040-3.9.4.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.0/rp2040-5.5.0.zip framework = arduino lib_deps = From 04db37a34ac43d0a9c5fb8c807b56ed58a551bce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Feb 2026 20:38:38 -0700 Subject: [PATCH 0254/2056] [esp8266] Remove forced scanf linkage to save ~8KB flash (#13678) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp8266/__init__.py | 6 +++ .../esp8266/remove_float_scanf.py.script | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 esphome/components/esp8266/remove_float_scanf.py.script diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c7b5d5c130d..927a59fd616 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -205,6 +205,7 @@ async def to_code(config): "pre:testing_mode.py", "pre:exclude_updater.py", "pre:exclude_waveform.py", + "pre:remove_float_scanf.py", "post:post_build.py", ], ) @@ -342,3 +343,8 @@ def copy_files() -> None: exclude_waveform_file, CORE.relative_build_path("exclude_waveform.py"), ) + remove_float_scanf_file = dir / "remove_float_scanf.py.script" + copy_file_if_changed( + remove_float_scanf_file, + CORE.relative_build_path("remove_float_scanf.py"), + ) diff --git a/esphome/components/esp8266/remove_float_scanf.py.script b/esphome/components/esp8266/remove_float_scanf.py.script new file mode 100644 index 00000000000..b1d03a4d46e --- /dev/null +++ b/esphome/components/esp8266/remove_float_scanf.py.script @@ -0,0 +1,46 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Remove forced scanf linkage to allow garbage collection of unused code +# +# The ESP8266 Arduino framework unconditionally adds: +# -u _printf_float -u _scanf_float +# +# The -u flag forces symbols to be linked even if unreferenced, which pulls +# in the entire scanf family (~7-8KB). ESPHome doesn't use scanf at all +# (verified by CI check in PR #13657), so this is pure dead weight. +# +# By removing -u _scanf_float, --gc-sections can eliminate: +# - scanf family functions (~7KB) +# - _strtod_l (~3.7KB) +# - Related parsing infrastructure +# +# We keep -u _printf_float because components still use %f in logging. + + +def remove_scanf_float_flag(source, target, env): + """Remove -u _scanf_float from linker flags. + + This is called as a pre-action before the link step, after the + Arduino framework has added its default flags. + """ + linkflags = env.get("LINKFLAGS", []) + new_linkflags = [] + i = 0 + + while i < len(linkflags): + flag = linkflags[i] + if flag == "-u" and i + 1 < len(linkflags): + next_flag = linkflags[i + 1] + if next_flag == "_scanf_float": + print("ESPHome: Removing _scanf_float (saves ~8KB flash)") + i += 2 # Skip both -u and the symbol + continue + new_linkflags.append(flag) + i += 1 + + env.Replace(LINKFLAGS=new_linkflags) + + +# Register the callback to run before the link step +env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_scanf_float_flag) From 656389f2158b0723630839c385aa26648c6f53b1 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 26 Feb 2026 23:41:35 -0600 Subject: [PATCH 0255/2056] [usb_uart] Performance, correctness and reliability improvements (#14333) --- .../components/usb_host/usb_host_client.cpp | 19 +- esphome/components/usb_uart/ch34x.cpp | 9 +- esphome/components/usb_uart/cp210x.cpp | 9 +- esphome/components/usb_uart/usb_uart.cpp | 205 ++++++++++++++---- esphome/components/usb_uart/usb_uart.h | 45 +++- 5 files changed, 224 insertions(+), 63 deletions(-) diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 5b0aed4c598..c77d738ace7 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -424,9 +424,9 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, if (trq == nullptr) return false; auto length = data.size(); - if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) { + if (length > trq->transfer->data_buffer_size - SETUP_PACKET_SIZE) { ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length, - sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t)); + trq->transfer->data_buffer_size - SETUP_PACKET_SIZE); this->release_trq(trq); return false; } @@ -507,9 +507,13 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u /** * Performs an output transfer operation. - * THREAD CONTEXT: Called from main loop thread only - * - USB UART output uses defer() to ensure main loop context - * - Modbus and other components call from loop() + * THREAD CONTEXT: Called from both USB task and main loop threads. + * - USB task: output transfer callback restarts output directly (no defer) + * - Main loop: initial output trigger from write_array() and loop() + * Thread safety is ensured by: + * - get_trq_() uses atomic CAS (multi-consumer safe) + * - claimed trq slot is exclusively owned until submission + * - usb_host_transfer_submit() is safe to call from any task context * * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. @@ -524,6 +528,11 @@ bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, ESP_LOGE(TAG, "Too many requests queued"); return false; } + if (length > trq->transfer->data_buffer_size) { + ESP_LOGE(TAG, "transfer_out: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size); + this->release_trq(trq); + return false; + } trq->callback = callback; trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT; diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index caa4b656575..7fa964c0cb6 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -5,8 +5,7 @@ #include "esphome/components/bytebuffer/bytebuffer.h" -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { using namespace bytebuffer; /** @@ -74,8 +73,8 @@ void USBUartTypeCH34X::enable_channels() { this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); } - USBUartTypeCdcAcm::enable_channels(); + this->start_channels(); } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index fa8c2331b2c..ae9170c5fbd 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -5,8 +5,7 @@ #include "esphome/components/bytebuffer/bytebuffer.h" -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { using namespace bytebuffer; /** @@ -119,8 +118,8 @@ void USBUartTypeCP210X::enable_channels() { this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback, baud.get_data()); } - USBUartTypeCdcAcm::enable_channels(); + this->start_channels(); } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 5c2806c4560..de81bfc587b 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -3,12 +3,10 @@ #include "usb_uart.h" #include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/components/uart/uart_debugger.h" #include -namespace esphome { -namespace usb_uart { +namespace esphome::usb_uart { /** * @@ -135,16 +133,51 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { ESP_LOGV(TAG, "Channel not initialised - write ignored"); return; } - while (this->output_buffer_.get_free_space() != 0 && len-- != 0) { - this->output_buffer_.push(*data++); +#ifdef USE_UART_DEBUGGER + if (this->debug_) { + constexpr size_t BATCH = 16; + char buf[4 + format_hex_pretty_size(BATCH)]; // ">>> " + "XX,XX,...,XX\0" + for (size_t off = 0; off < len; off += BATCH) { + size_t n = std::min(len - off, BATCH); + memcpy(buf, ">>> ", 4); + format_hex_pretty_to(buf + 4, sizeof(buf) - 4, data + off, n, ','); + ESP_LOGD(TAG, "%s", buf); + } } - len++; - if (len > 0) { - ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len); +#endif + while (len > 0) { + UsbOutputChunk *chunk = this->output_pool_.allocate(); + if (chunk == nullptr) { + ESP_LOGE(TAG, "Output pool full - lost %zu bytes", len); + break; + } + size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); + memcpy(chunk->data, data, chunk_len); + chunk->length = static_cast(chunk_len); + if (!this->output_queue_.push(chunk)) { + this->output_pool_.release(chunk); + ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len); + break; + } + data += chunk_len; + len -= chunk_len; } this->parent_->start_output(this); } +void USBUartChannel::flush() { + // Spin until the output queue is drained and the last USB transfer completes. + // Safe to call from the main loop only. + // The 100 ms timeout guards against a device that stops responding mid-flush; + // in that case the main loop is blocked for the full duration. + uint32_t deadline = millis() + 100; // 100 ms safety timeout + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() < deadline) { + // Kick start_output() in case data arrived but no transfer is in flight yet. + this->parent_->start_output(this); + yield(); + } +} + bool USBUartChannel::peek_byte(uint8_t *data) { if (this->input_buffer_.is_empty()) { return false; @@ -182,8 +215,10 @@ void USBUartComponent::loop() { #ifdef USE_UART_DEBUGGER if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector(chunk->data, chunk->data + chunk->length), - ','); // NOLINT() + char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex + memcpy(buf, "<<< ", 4); + format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); + ESP_LOGD(TAG, "%s", buf); } #endif @@ -192,6 +227,13 @@ void USBUartComponent::loop() { // Return chunk to pool for reuse this->chunk_pool_.release(chunk); + + // Invoke the RX callback (if registered) immediately after data lands in the + // ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes + // in the same loop iteration they are delivered, avoiding an extra wakeup cycle. + if (channel->rx_callback_) { + channel->rx_callback_(); + } } // Log dropped USB data periodically @@ -234,10 +276,10 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe - // if already started, don't restart. A spurious failure in compare_exchange_weak - // is not a problem, as it will be retried on the next read_array() + // Use compare_exchange_strong to avoid spurious failures: a missed submit here is + // never retried by read_array() because no data will ever arrive to trigger it. auto started = false; - if (!channel->input_started_.compare_exchange_weak(started, true)) + if (!channel->input_started_.compare_exchange_strong(started, true)) return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback @@ -285,37 +327,56 @@ void USBUartComponent::start_input(USBUartChannel *channel) { this->start_input(channel); }; if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + ESP_LOGE(TAG, "IN transfer submission failed for ep=0x%02X", ep->bEndpointAddress); channel->input_started_.store(false); } } void USBUartComponent::start_output(USBUartChannel *channel) { - // IMPORTANT: This function must only be called from the main loop! - // The output_buffer_ is not thread-safe and can only be accessed from main loop. - // USB callbacks use defer() to ensure this function runs in the correct context. - if (channel->output_started_.load()) - return; - if (channel->output_buffer_.is_empty()) { + // THREAD CONTEXT: Called from both main loop and USB task threads. + // The output_queue_ is a lock-free SPSC queue, so pop() is safe from either thread. + // The output_started_ atomic flag is claimed via compare_exchange to guarantee that + // only one thread starts a transfer at a time. + + // Atomically claim the "output in progress" flag. If already set, another thread + // is handling the transfer; return immediately. + bool expected = false; + if (!channel->output_started_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { return; } + + UsbOutputChunk *chunk = channel->output_queue_.pop(); + if (chunk == nullptr) { + // Nothing to send — release the flag and return. + channel->output_started_.store(false, std::memory_order_release); + return; + } + const auto *ep = channel->cdc_dev_.out_ep; - // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback - auto callback = [this, channel](const usb_host::TransferStatus &status) { - ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); - channel->output_started_.store(false); - // Defer restart to main loop (defer is thread-safe) - this->defer([this, channel] { this->start_output(channel); }); + // CALLBACK CONTEXT: This lambda is executed in the USB task via transfer_callback. + // It releases the chunk, clears the flag, and directly restarts output without + // going through defer() — eliminating one full main-loop-wakeup cycle of latency. + auto callback = [this, channel, chunk](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "Output transfer failed: status %X", status.error_code); + } else { + ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); + } + channel->output_pool_.release(chunk); + channel->output_started_.store(false, std::memory_order_release); + // Restart directly from USB task — safe because output_queue_ is lock-free + // and transfer_out() uses thread-safe atomic slot allocation. + this->start_output(channel); }; - channel->output_started_.store(true); - uint8_t data[ep->wMaxPacketSize]; - auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize); - this->transfer_out(ep->bEndpointAddress, callback, data, len); -#ifdef USE_UART_DEBUGGER - if (channel->debug_) { - uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector(data, data + len), ','); // NOLINT() + + const uint8_t len = chunk->length; + if (!this->transfer_out(ep->bEndpointAddress, callback, chunk->data, len)) { + // Transfer submission failed — return chunk and release flag so callers can retry. + channel->output_pool_.release(chunk); + channel->output_started_.store(false, std::memory_order_release); + return; } -#endif - ESP_LOGV(TAG, "Output %d bytes started", len); + ESP_LOGV(TAG, "Output %u bytes started", len); } /** @@ -351,6 +412,20 @@ void USBUartTypeCdcAcm::on_connected() { fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); channel->initialised_.store(true); + // Claim the communication (interrupt) interface so CDC class requests are accepted + // by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before + // they enable data flow on the bulk endpoints. + if (channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, + channel->cdc_dev_.interrupt_interface_number, 0); + if (err_comm != ESP_OK) { + ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, + esp_err_to_name(err_comm)); + } else { + channel->cdc_dev_.comm_interface_claimed = true; + ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); + } + } auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { @@ -378,18 +453,74 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } + if (channel->cdc_dev_.comm_interface_claimed) { + usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); + channel->cdc_dev_.comm_interface_claimed = false; + } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts channel->input_started_.store(true); channel->output_started_.store(true); channel->input_buffer_.clear(); - channel->output_buffer_.clear(); + // Drain any pending output chunks and return them to the pool + { + UsbOutputChunk *chunk; + while ((chunk = channel->output_queue_.pop()) != nullptr) { + channel->output_pool_.release(chunk); + } + } channel->initialised_.store(false); } USBClient::on_disconnected(); } void USBUartTypeCdcAcm::enable_channels() { + static constexpr uint8_t CDC_REQUEST_TYPE = usb_host::USB_TYPE_CLASS | usb_host::USB_RECIP_INTERFACE; + static constexpr uint8_t CDC_SET_LINE_CODING = 0x20; + static constexpr uint8_t CDC_SET_CONTROL_LINE_STATE = 0x22; + static constexpr uint16_t CDC_DTR_RTS = 0x0003; // D0=DTR, D1=RTS + + for (auto *channel : this->channels_) { + if (!channel->initialised_.load()) + continue; + // Configure the bridge's UART parameters. A USB-UART bridge will not forward data + // at the correct speed until SET_LINE_CODING is sent; without it the UART may run + // at an indeterminate default rate so the NCP receives garbled bytes and never + // sends RSTACK. + uint32_t baud = channel->baud_rate_; + std::vector line_coding = { + static_cast(baud & 0xFF), static_cast((baud >> 8) & 0xFF), + static_cast((baud >> 16) & 0xFF), static_cast((baud >> 24) & 0xFF), + static_cast(channel->stop_bits_), // bCharFormat: 0=1stop, 1=1.5stop, 2=2stop + static_cast(channel->parity_), // bParityType: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space + static_cast(channel->data_bits_), // bDataBits + }; + ESP_LOGD(TAG, "SET_LINE_CODING: baud=%u stop=%u parity=%u data=%u", (unsigned) baud, channel->stop_bits_, + (unsigned) channel->parity_, channel->data_bits_); + this->control_transfer( + CDC_REQUEST_TYPE, CDC_SET_LINE_CODING, 0, channel->cdc_dev_.interrupt_interface_number, + [](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "SET_LINE_CODING failed: %X", status.error_code); + } else { + ESP_LOGD(TAG, "SET_LINE_CODING OK"); + } + }, + line_coding); + // Assert DTR+RTS to signal DTE is present. + this->control_transfer(CDC_REQUEST_TYPE, CDC_SET_CONTROL_LINE_STATE, CDC_DTR_RTS, + channel->cdc_dev_.interrupt_interface_number, [](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGW(TAG, "SET_CONTROL_LINE_STATE failed: %X", status.error_code); + } else { + ESP_LOGD(TAG, "SET_CONTROL_LINE_STATE (DTR+RTS) OK"); + } + }); + } + this->start_channels(); +} + +void USBUartTypeCdcAcm::start_channels() { for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; @@ -399,6 +530,6 @@ void USBUartTypeCdcAcm::enable_channels() { } } -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart + #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 94e61204575..9a9fe1c2ca0 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -8,9 +8,10 @@ #include "esphome/core/lock_free_queue.h" #include "esphome/core/event_pool.h" #include +#include + +namespace esphome::usb_uart { -namespace esphome { -namespace usb_uart { class USBUartTypeCdcAcm; class USBUartComponent; class USBUartChannel; @@ -31,6 +32,7 @@ struct CdcEps { const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; uint8_t interrupt_interface_number; + bool comm_interface_claimed{false}; }; enum UARTParityOptions { @@ -83,6 +85,16 @@ struct UsbDataChunk { void release() {} }; +// Structure for queuing outgoing USB data chunks (one per USB FS packet) +struct UsbOutputChunk { + static constexpr size_t MAX_CHUNK_SIZE = 64; // USB FS MPS + uint8_t data[MAX_CHUNK_SIZE]; + uint8_t length; + + // Required for EventPool - no cleanup needed for POD types + void release() {} +}; + class USBUartChannel : public uart::UARTComponent, public Parented { friend class USBUartComponent; friend class USBUartTypeCdcAcm; @@ -90,24 +102,32 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - void flush() override {} + void flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + /// Register a callback invoked immediately after data is pushed to the input ring buffer. + /// Called from USBUartComponent::loop() in the main loop context. + /// Allows consumers (e.g. ZigbeeProxy) to process bytes in the same loop iteration + /// they arrive, eliminating one full main-loop-wakeup cycle of latency. + void set_rx_callback(std::function cb) { this->rx_callback_ = std::move(cb); } + protected: // Larger structures first for better alignment RingBuffer input_buffer_; - RingBuffer output_buffer_; + LockFreeQueue output_queue_; + EventPool output_pool_; + std::function rx_callback_{}; CdcEps cdc_dev_{}; // Enum (likely 4 bytes) UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; @@ -150,8 +170,12 @@ class USBUartTypeCdcAcm : public USBUartComponent { protected: virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; - virtual void enable_channels(); void on_disconnected() override; + virtual void enable_channels(); + /// Resets per-channel transfer flags and posts the first bulk IN transfer. + /// Called by enable_channels() and by vendor-specific subclass overrides that + /// handle their own line-coding setup before starting data flow. + void start_channels(); }; class USBUartTypeCP210X : public USBUartTypeCdcAcm { @@ -170,7 +194,6 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { void enable_channels() override; }; -} // namespace usb_uart -} // namespace esphome +} // namespace esphome::usb_uart #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 From 4044520ccc3a17b1162a6b7aec970f5658f1f641 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:38:36 -0500 Subject: [PATCH 0256/2056] [esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/esp32_touch/__init__.py | 319 ++++++----- .../components/esp32_touch/esp32_touch.cpp | 500 ++++++++++++++++++ esphome/components/esp32_touch/esp32_touch.h | 284 ++++------ .../esp32_touch/esp32_touch_common.cpp | 173 ------ .../components/esp32_touch/esp32_touch_v1.cpp | 244 --------- .../components/esp32_touch/esp32_touch_v2.cpp | 402 -------------- .../esp32_touch/common-variants.yaml | 1 - .../esp32_touch/test.esp32-p4-idf.yaml | 5 + 8 files changed, 800 insertions(+), 1128 deletions(-) create mode 100644 esphome/components/esp32_touch/esp32_touch.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_common.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_v1.cpp delete mode 100644 esphome/components/esp32_touch/esp32_touch_v2.cpp create mode 100644 tests/components/esp32_touch/test.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index a02370a3434..10ad339b127 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,7 +1,10 @@ +import logging + import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( VARIANT_ESP32, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, @@ -21,6 +24,8 @@ from esphome.const import ( ) from esphome.core import TimePeriod +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] @@ -37,135 +42,161 @@ CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver" esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) +# Channel ID mappings: GPIO pin number -> integer channel ID +# These are plain integers - the new unified API uses int chan_id directly. TOUCH_PADS = { VARIANT_ESP32: { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, + 4: 0, + 0: 1, + 2: 2, + 15: 3, + 13: 4, + 12: 5, + 14: 6, + 27: 7, + 33: 8, + 32: 9, }, VARIANT_ESP32S2: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, }, VARIANT_ESP32S3: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + }, + VARIANT_ESP32P4: { + 2: 1, + 3: 2, + 4: 3, + 5: 4, + 6: 5, + 7: 6, + 8: 7, + 9: 8, + 10: 9, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, }, } TOUCH_PAD_DENOISE_GRADE = { - "BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12, - "BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10, - "BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8, - "BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4, + "BIT12": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT12, + "BIT10": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT10, + "BIT8": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT8, + "BIT4": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT4, } TOUCH_PAD_DENOISE_CAP_LEVEL = { - "L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0, - "L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1, - "L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2, - "L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3, - "L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4, - "L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5, - "L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6, - "L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7, + "L0": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_5PF, + "L1": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_6PF, + "L2": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_7PF, + "L3": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_9PF, + "L4": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_10PF, + "L5": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_12PF, + "L6": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_13PF, + "L7": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_14PF, } TOUCH_PAD_FILTER_MODE = { - "IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8, - "IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16, - "IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32, - "IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64, - "IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128, - "IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256, - "JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER, + "IIR_4": cg.global_ns.TOUCH_BM_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_BM_IIR_FILTER_8, + "IIR_16": cg.global_ns.TOUCH_BM_IIR_FILTER_16, + "IIR_32": cg.global_ns.TOUCH_BM_IIR_FILTER_32, + "IIR_64": cg.global_ns.TOUCH_BM_IIR_FILTER_64, + "IIR_128": cg.global_ns.TOUCH_BM_IIR_FILTER_128, + "IIR_256": cg.global_ns.TOUCH_BM_IIR_FILTER_256, + "JITTER": cg.global_ns.TOUCH_BM_JITTER_FILTER, } TOUCH_PAD_SMOOTH_MODE = { - "OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF, - "IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2, - "IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8, + "OFF": cg.global_ns.TOUCH_SMOOTH_NO_FILTER, + "IIR_2": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_2, + "IIR_4": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_8, } LOW_VOLTAGE_REFERENCE = { - "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, - "0.6V": cg.global_ns.TOUCH_LVOLT_0V6, - "0.7V": cg.global_ns.TOUCH_LVOLT_0V7, - "0.8V": cg.global_ns.TOUCH_LVOLT_0V8, + "0.5V": cg.global_ns.TOUCH_VOLT_LIM_L_0V5, + "0.6V": cg.global_ns.TOUCH_VOLT_LIM_L_0V6, + "0.7V": cg.global_ns.TOUCH_VOLT_LIM_L_0V7, + "0.8V": cg.global_ns.TOUCH_VOLT_LIM_L_0V8, } HIGH_VOLTAGE_REFERENCE = { - "2.4V": cg.global_ns.TOUCH_HVOLT_2V4, - "2.5V": cg.global_ns.TOUCH_HVOLT_2V5, - "2.6V": cg.global_ns.TOUCH_HVOLT_2V6, - "2.7V": cg.global_ns.TOUCH_HVOLT_2V7, + "2.4V": cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + "2.5V": cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + "2.6V": cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + "2.7V": cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } -VOLTAGE_ATTENUATION = { - "1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5, - "1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V, - "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, - "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, -} -TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = { - "L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0, - "L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1, - "L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2, - "L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3, - "L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4, - "L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5, - "L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6, - "L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7, +VOLTAGE_ATTENUATION = {"1.5V", "1V", "0.5V", "0V"} + +# ESP32 V1: The new API's touch_volt_lim_h_t combines the old high_voltage_reference +# and voltage_attenuation into a single enum representing the effective upper voltage. +# Effective voltage = high_voltage_reference - voltage_attenuation +EFFECTIVE_HIGH_VOLTAGE = { + ("2.4V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_0V9, + ("2.5V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V0, + ("2.6V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V1, + ("2.7V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V2, + ("2.4V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V4, + ("2.5V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V5, + ("2.6V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V6, + ("2.7V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V7, + ("2.4V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V9, + ("2.5V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V0, + ("2.6V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V1, + ("2.7V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V2, + ("2.4V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + ("2.5V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + ("2.6V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + ("2.7V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } def validate_touch_pad(value): value = gpio.gpio_pin_number_validator(value) variant = get_esp32_variant() - if variant not in TOUCH_PADS: + pads = TOUCH_PADS.get(variant) + if pads is None: raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") - - pads = TOUCH_PADS[variant] if value not in pads: raise cv.Invalid(f"Pin {value} does not support touch pads.") - return cv.enum(pads)(value) + return pads[value] # Return integer channel ID def validate_variant_vars(config): - if get_esp32_variant() == VARIANT_ESP32: - variant_vars = { + variant = get_esp32_variant() + invalid_vars = set() + if variant == VARIANT_ESP32: + invalid_vars = { CONF_DEBOUNCE_COUNT, CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL, @@ -176,15 +207,14 @@ def validate_variant_vars(config): CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER, } - for vvar in variant_vars: - if vvar in config: - raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}") - elif ( - get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3 - ) and CONF_IIR_FILTER in config: - raise cv.Invalid( - f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}" - ) + elif variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): + invalid_vars = {CONF_IIR_FILTER} + if variant == VARIANT_ESP32P4: + invalid_vars |= {CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL} + unsupported = invalid_vars.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid(f"{keys} not valid on {variant}") return config @@ -219,12 +249,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( HIGH_VOLTAGE_REFERENCE ), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( - VOLTAGE_ATTENUATION - ), + # ESP32 V1 only: attenuates the high voltage reference + cv.SplitDefault( + CONF_VOLTAGE_ATTENUATION, + esp32="0V", + esp32_s2=cv.UNDEFINED, + esp32_s3=cv.UNDEFINED, + esp32_p4=cv.UNDEFINED, + ): validate_voltage(VOLTAGE_ATTENUATION), # ESP32 only cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds, - # ESP32-S2/S3 only + # ESP32-S2/S3/P4 only cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7), cv.Optional(CONF_FILTER_MODE): cv.enum( TOUCH_PAD_FILTER_MODE, upper=True, space="_" @@ -241,9 +276,7 @@ CONFIG_SCHEMA = cv.All( TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_" ), cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad, - cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum( - TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_" - ), + cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7), } ).extend(cv.COMPONENT_SCHEMA), cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL), @@ -260,6 +293,7 @@ CONFIG_SCHEMA = cv.All( esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2, esp32.VARIANT_ESP32S3, + esp32.VARIANT_ESP32P4, ] ), validate_variant_vars, @@ -267,44 +301,67 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) + # New unified touch sensor driver include_builtin_idf_component("esp_driver_touch_sens") - # Legacy driver component provides driver/touch_sensor.h header - include_builtin_idf_component("driver") touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) - sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) - cg.add(touch.set_sleep_duration(sleep_duration)) + # sleep_duration -> meas_interval_us (pass microseconds directly) + cg.add(touch.set_meas_interval_us(config[CONF_SLEEP_DURATION].total_microseconds)) - measurement_duration = int( - round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793) - ) - cg.add(touch.set_measurement_duration(measurement_duration)) + variant = get_esp32_variant() - cg.add( - touch.set_low_voltage_reference( - LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + # measurement_duration handling differs per variant + if variant == VARIANT_ESP32: + # V1: charge_duration_ms (convert from microseconds to milliseconds) + charge_duration_ms = ( + config[CONF_MEASUREMENT_DURATION].total_microseconds / 1000.0 ) - ) - cg.add( - touch.set_high_voltage_reference( - HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + cg.add(touch.set_charge_duration_ms(charge_duration_ms)) + else: + # V2/V3: charge_times (approximate conversion from duration) + # The old API used clock cycles; the new API uses charge_times count. + # Default is 500 for V2/V3. Use measurement_duration as a rough scaling factor. + # 65535 / 8192 ≈ 7.9999 maps the microsecond duration to charge_times. + charge_times = int( + round(config[CONF_MEASUREMENT_DURATION].total_microseconds * (65535 / 8192)) ) - ) - cg.add( - touch.set_voltage_attenuation( - VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] - ) - ) + charge_times = max(charge_times, 1) + cg.add(touch.set_charge_times(charge_times)) - if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config: + # Voltage references (not applicable to P4) + if variant != VARIANT_ESP32P4: + if CONF_LOW_VOLTAGE_REFERENCE in config: + cg.add( + touch.set_low_voltage_reference( + LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + ) + ) + if CONF_HIGH_VOLTAGE_REFERENCE in config: + if variant == VARIANT_ESP32: + # V1: combine high_voltage_reference with voltage_attenuation + high_ref = config[CONF_HIGH_VOLTAGE_REFERENCE] + atten = config[CONF_VOLTAGE_ATTENUATION] + cg.add( + touch.set_high_voltage_reference( + EFFECTIVE_HIGH_VOLTAGE[(high_ref, atten)] + ) + ) + else: + # V2/V3: no attenuation concept, use directly + cg.add( + touch.set_high_voltage_reference( + HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + ) + ) + + if variant == VARIANT_ESP32 and CONF_IIR_FILTER in config: cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) - if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: + if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): if CONF_FILTER_MODE in config: cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE])) if CONF_DEBOUNCE_COUNT in config: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp new file mode 100644 index 00000000000..e7124ce92f9 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -0,0 +1,500 @@ +#ifdef USE_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome::esp32_touch { + +template static const char *lookup_str(const char *const (&table)[N], size_t index) { + return (index < N) ? table[index] : "UNKNOWN"; +} + +static const char *const TAG = "esp32_touch"; + +static constexpr uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +static constexpr uint32_t INITIAL_STATE_DELAY_MS = 1500; +static constexpr uint32_t ONESHOT_SCAN_COUNT = 3; +static constexpr uint32_t ONESHOT_SCAN_TIMEOUT_MS = 2000; + +// V1: called from esp_timer context (software filter) +// V2/V3: called from ISR context +// xQueueSendFromISR is safe from both contexts. + +bool IRAM_ATTR ESP32TouchComponent::on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, + void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, true}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +bool IRAM_ATTR ESP32TouchComponent::on_inactive_cb(touch_sensor_handle_t handle, + const touch_inactive_event_data_t *event, void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, false}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +void ESP32TouchComponent::setup() { + if (!this->create_touch_queue_()) { + return; + } + + // Create sample config - differs per hardware version +#ifdef USE_ESP32_VARIANT_ESP32 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG( + this->charge_duration_ms_, this->low_voltage_reference_, this->high_voltage_reference_); +#elif defined(USE_ESP32_VARIANT_ESP32P4) + // div_num=8 (data scaling divisor), coarse_freq_tune=2, fine_freq_tune=2 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG(8, 2, 2); + sample_cfg.charge_times = this->charge_times_; +#else + // ESP32-S2/S3 (V2) + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG( + this->charge_times_, this->low_voltage_reference_, this->high_voltage_reference_); +#endif + + // Create controller + touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(1, &sample_cfg); + sens_cfg.meas_interval_us = this->meas_interval_us_; +#ifndef USE_ESP32_VARIANT_ESP32 + sens_cfg.max_meas_time_us = 0; // Disable measurement timeout (V2/V3 only) +#endif + + esp_err_t err = touch_sensor_new_controller(&sens_cfg, &this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch controller: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Create channels for all children + for (auto *child : this->children_) { + touch_channel_config_t chan_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32 + chan_cfg.abs_active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + chan_cfg.group = TOUCH_CHAN_TRIG_GROUP_BOTH; +#elif defined(USE_ESP32_VARIANT_ESP32P4) + chan_cfg.active_thresh[0] = child->get_threshold(); +#else + // ESP32-S2/S3 (V2) + chan_cfg.active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; +#endif + + err = touch_sensor_new_channel(this->sens_handle_, child->get_channel_id(), &chan_cfg, &child->chan_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch channel %d: %s", child->get_channel_id(), esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } + + // Configure filter +#ifdef USE_ESP32_VARIANT_ESP32 + // Software filter is REQUIRED for V1 on_active/on_inactive callbacks + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->iir_filter_enabled_()) { + filter_cfg.interval_ms = this->iir_filter_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } +#else + // V2/V3: Hardware benchmark filter + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->filter_configured_) { + filter_cfg.benchmark.filter_mode = this->filter_mode_; + filter_cfg.benchmark.jitter_step = this->jitter_step_; + filter_cfg.benchmark.denoise_lvl = this->noise_threshold_; + filter_cfg.data.smooth_filter = this->smooth_level_; + filter_cfg.data.debounce_cnt = this->debounce_count_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + touch_denoise_chan_config_t denoise_cfg = {}; + denoise_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + denoise_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + denoise_cfg.ref_cap = this->denoise_cap_level_; + denoise_cfg.resolution = this->denoise_grade_; + err = touch_sensor_config_denoise_channel(this->sens_handle_, &denoise_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure denoise: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_WATERPROOF + if (this->waterproof_configured_) { + touch_channel_handle_t guard_chan = nullptr; + for (auto *child : this->children_) { + if (child->get_channel_id() == this->waterproof_guard_ring_pad_) { + guard_chan = child->chan_handle_; + break; + } + } + + touch_channel_handle_t shield_chan = nullptr; + touch_channel_config_t shield_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32P4 + shield_cfg.active_thresh[0] = 0; + err = touch_sensor_new_channel(this->sens_handle_, SOC_TOUCH_MAX_CHAN_ID, &shield_cfg, &shield_chan); +#else + shield_cfg.active_thresh[0] = 0; + shield_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + shield_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + err = touch_sensor_new_channel(this->sens_handle_, TOUCH_SHIELD_CHAN_ID, &shield_cfg, &shield_chan); +#endif + if (err == ESP_OK) { + touch_waterproof_config_t wp_cfg = {}; + wp_cfg.guard_chan = guard_chan; + wp_cfg.shield_chan = shield_chan; + wp_cfg.shield_drv = this->waterproof_shield_driver_; + wp_cfg.flags.immersion_proof = 1; + err = touch_sensor_config_waterproof(this->sens_handle_, &wp_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure waterproof: %s", esp_err_to_name(err)); + } + } else { + ESP_LOGW(TAG, "Failed to create shield channel: %s", esp_err_to_name(err)); + } + } +#endif + + // Configure wakeup pads before enabling (must be done in INIT state) + this->configure_wakeup_pads_(); + + // Register callbacks + touch_event_callbacks_t cbs = {}; + cbs.on_active = on_active_cb; + cbs.on_inactive = on_inactive_cb; + err = touch_sensor_register_callbacks(this->sens_handle_, &cbs, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register callbacks: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Enable and start scanning + err = touch_sensor_enable(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable touch sensor: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Do initial oneshot scans to populate baseline values + for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { + err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Oneshot scan %d failed: %s", i, esp_err_to_name(err)); + } + } + + err = touch_sensor_start_continuous_scanning(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start continuous scanning: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } +} + +void ESP32TouchComponent::dump_config() { +#if !defined(USE_ESP32_VARIANT_ESP32P4) + static constexpr const char *LV_STRS[] = {"0.5V", "0.6V", "0.7V", "0.8V"}; + static constexpr const char *HV_STRS[] = {"0.9V", "1.0V", "1.1V", "1.2V", "1.4V", "1.5V", "1.6V", "1.7V", + "1.9V", "2.0V", "2.1V", "2.2V", "2.4V", "2.5V", "2.6V", "2.7V"}; + const char *lv_s = lookup_str(LV_STRS, this->low_voltage_reference_); + const char *hv_s = lookup_str(HV_STRS, this->high_voltage_reference_); + + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus\n" + " Low Voltage Reference: %s\n" + " High Voltage Reference: %s", + this->meas_interval_us_, lv_s, hv_s); +#else + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus", + this->meas_interval_us_); +#endif + +#ifdef USE_ESP32_VARIANT_ESP32 + if (this->iir_filter_enabled_()) { + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); + } else { + ESP_LOGCONFIG(TAG, " IIR Filter: 10ms (default)"); + } +#else + if (this->filter_configured_) { + // TOUCH_BM_IIR_FILTER_256 only exists on V2, shifting JITTER's position + static constexpr const char *FILTER_STRS[] = { + "IIR_4", + "IIR_8", + "IIR_16", + "IIR_32", + "IIR_64", + "IIR_128", +#if SOC_TOUCH_SENSOR_VERSION == 2 + "IIR_256", +#endif + "JITTER", + }; + static constexpr const char *SMOOTH_STRS[] = {"OFF", "IIR_2", "IIR_4", "IIR_8"}; + const char *filter_s = lookup_str(FILTER_STRS, this->filter_mode_); + const char *smooth_s = lookup_str(SMOOTH_STRS, this->smooth_level_); + ESP_LOGCONFIG(TAG, + " Filter mode: %s\n" + " Debounce count: %" PRIu32 "\n" + " Noise threshold coefficient: %" PRIu32 "\n" + " Jitter filter step size: %" PRIu32 "\n" + " Smooth level: %s", + filter_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_, smooth_s); + } + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + static constexpr const char *GRADE_STRS[] = {"BIT12", "BIT10", "BIT8", "BIT4"}; + static constexpr const char *CAP_STRS[] = {"5pF", "6.4pF", "7.8pF", "9.2pF", "10.6pF", "12pF", "13.4pF", "14.8pF"}; + const char *grade_s = lookup_str(GRADE_STRS, this->denoise_grade_); + const char *cap_s = lookup_str(CAP_STRS, this->denoise_cap_level_); + ESP_LOGCONFIG(TAG, + " Denoise grade: %s\n" + " Denoise capacitance level: %s", + grade_s, cap_s); + } +#endif +#endif // !USE_ESP32_VARIANT_ESP32 + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, + " Channel: %d\n" + " Threshold: %" PRIu32 "\n" + " Benchmark: %" PRIu32, + child->channel_id_, child->threshold_, child->benchmark_); + } +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); + + // Process queued touch events from callbacks + TouchEvent event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + for (auto *child : this->children_) { + if (child->get_channel_id() != event.chan_id) { + continue; + } + + // Read current smooth value + uint32_t value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &value); + child->value_ = value; + +#ifndef USE_ESP32_VARIANT_ESP32 + // V2/V3: also read benchmark + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; +#endif + + bool new_state = event.is_active; + + if (new_state != child->last_state_) { + child->initial_state_published_ = true; + child->last_state_ = new_state; + child->publish_state(new_state); +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), value, child->get_threshold()); +#else + if (new_state) { + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 ", benchmark: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), value, benchmark, child->get_threshold()); + } else { + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); + } +#endif + } + break; + } + } + + // Publish initial OFF state for sensors that haven't received events yet + for (auto *child : this->children_) { + this->publish_initial_state_if_needed_(child, now); + } + + if (!this->setup_mode_) { + this->disable_loop(); + } +} + +void ESP32TouchComponent::on_shutdown() { + if (this->sens_handle_ == nullptr) + return; + + touch_sensor_stop_continuous_scanning(this->sens_handle_); + touch_sensor_disable(this->sens_handle_); + + for (auto *child : this->children_) { + if (child->chan_handle_ != nullptr) { + touch_sensor_del_channel(child->chan_handle_); + child->chan_handle_ = nullptr; + } + } + + touch_sensor_del_controller(this->sens_handle_); + this->sens_handle_ = nullptr; + + this->cleanup_touch_queue_(); +} + +bool ESP32TouchComponent::create_touch_queue_() { + size_t queue_size = this->children_.size() * 4; + if (queue_size < 8) + queue_size = 8; + + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchEvent)); + + if (this->touch_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); + this->mark_failed(); + return false; + } + return true; +} + +void ESP32TouchComponent::cleanup_touch_queue_() { + if (this->touch_queue_) { + vQueueDelete(this->touch_queue_); + this->touch_queue_ = nullptr; + } +} + +void ESP32TouchComponent::configure_wakeup_pads_() { +#if SOC_TOUCH_SUPPORT_SLEEP_WAKEUP + bool has_wakeup = false; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + has_wakeup = true; + break; + } + } + + if (!has_wakeup) + return; + +#ifdef USE_ESP32_VARIANT_ESP32 + // V1: Simple sleep config - threshold is set via channel config's abs_active_thresh + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } +#else + // V2/V3: Need to specify a deep sleep channel and threshold + touch_channel_handle_t wakeup_chan = nullptr; + uint32_t wakeup_thresh = 0; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + wakeup_chan = child->chan_handle_; + wakeup_thresh = child->get_wakeup_threshold(); + break; // Only one deep sleep wakeup channel is supported + } + } + + if (wakeup_chan != nullptr) { + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_chan = wakeup_chan; + sleep_cfg.deep_slp_thresh[0] = wakeup_thresh; + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } + } +#endif +#endif // SOC_TOUCH_SUPPORT_SLEEP_WAKEUP +} + +void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { + if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { + for (auto *child : this->children_) { + if (child->chan_handle_ == nullptr) + continue; + + uint32_t smooth_value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &smooth_value); + child->value_ = smooth_value; + +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGD(TAG, "Touch Pad '%s' (Ch%d): %" PRIu32, child->get_name().c_str(), child->channel_id_, smooth_value); +#else + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; + int32_t difference = static_cast(smooth_value) - static_cast(benchmark); + ESP_LOGD(TAG, + "Touch Pad '%s' (Ch%d): value=%" PRIu32 ", benchmark=%" PRIu32 ", difference=%" PRId32 + " (set threshold < %" PRId32 " to detect touch)", + child->get_name().c_str(), child->channel_id_, smooth_value, benchmark, difference, difference); +#endif + } + this->setup_mode_last_log_print_ = now; + } +} + +void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { + if (!child->initial_state_published_) { + if (now > INITIAL_STATE_DELAY_MS) { + child->publish_initial_state(false); + child->initial_state_published_ = true; + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); + } + } +} + +} // namespace esphome::esp32_touch + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 7f45f2ccb45..d51b2d49225 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -4,49 +4,49 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include #include -#include +#include #include #include -namespace esphome { -namespace esp32_touch { +namespace esphome::esp32_touch { // IMPORTANT: Touch detection logic differs between ESP32 variants: -// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease) -// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase) -// This inversion is due to different hardware implementations between chip generations. +// - ESP32 v1 (original): Touch detected when value < threshold (absolute threshold, capacitance increase causes +// value decrease) +// - ESP32-S2/S3 v2, ESP32-P4 v3: Touch detected when (smooth - benchmark) > threshold (relative threshold) // -// INTERRUPT BEHAVIOR: -// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched. -// Releases are detected by timeout since hardware doesn't generate release interrupts. -// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release -// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts -// and detect releases via timeout, similar to v1. - -static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +// CALLBACK BEHAVIOR: +// - ESP32 v1: on_active/on_inactive fire from a software filter timer (esp_timer context). +// The software filter MUST be configured for these callbacks to fire. +// - ESP32-S2/S3 v2, ESP32-P4 v3: on_active/on_inactive fire from hardware ISR context. +// Release detection via on_inactive is used, with timeout as safety fallback. class ESP32TouchBinarySensor; -class ESP32TouchComponent : public Component { +class ESP32TouchComponent final : public Component { public: void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } - void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } - void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } - void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { + void set_meas_interval_us(float meas_interval_us) { this->meas_interval_us_ = meas_interval_us; } + +#ifdef USE_ESP32_VARIANT_ESP32 + void set_charge_duration_ms(float charge_duration_ms) { this->charge_duration_ms_ = charge_duration_ms; } +#else + void set_charge_times(uint32_t charge_times) { this->charge_times_ = charge_times; } +#endif + +#if !defined(USE_ESP32_VARIANT_ESP32P4) + void set_low_voltage_reference(touch_volt_lim_l_t low_voltage_reference) { this->low_voltage_reference_ = low_voltage_reference; } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { + void set_high_voltage_reference(touch_volt_lim_h_t high_voltage_reference) { this->high_voltage_reference_ = high_voltage_reference; } - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { - this->voltage_attenuation_ = voltage_attenuation; - } +#endif void setup() override; void dump_config() override; @@ -54,183 +54,130 @@ class ESP32TouchComponent : public Component { void on_shutdown() override; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } - void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } - void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; } - void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; } - void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; } - void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; } - void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; } - void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; } - void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + void set_filter_mode(touch_benchmark_filter_mode_t filter_mode) { + this->filter_mode_ = filter_mode; + this->filter_configured_ = true; + } + void set_debounce_count(uint32_t debounce_count) { + this->debounce_count_ = debounce_count; + this->filter_configured_ = true; + } + void set_noise_threshold(uint32_t noise_threshold) { + this->noise_threshold_ = noise_threshold; + this->filter_configured_ = true; + } + void set_jitter_step(uint32_t jitter_step) { + this->jitter_step_ = jitter_step; + this->filter_configured_ = true; + } + void set_smooth_level(touch_smooth_filter_mode_t smooth_level) { + this->smooth_level_ = smooth_level; + this->filter_configured_ = true; + } +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + void set_denoise_grade(touch_denoise_chan_resolution_t denoise_grade) { + this->denoise_grade_ = denoise_grade; + this->denoise_configured_ = true; + } + void set_denoise_cap(touch_denoise_chan_cap_t cap_level) { + this->denoise_cap_level_ = cap_level; + this->denoise_configured_ = true; + } +#endif + void set_waterproof_guard_ring_pad(int channel_id) { + this->waterproof_guard_ring_pad_ = channel_id; + this->waterproof_configured_ = true; + } + void set_waterproof_shield_driver(uint32_t drive_capability) { this->waterproof_shield_driver_ = drive_capability; + this->waterproof_configured_ = true; } #else void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif protected: + // Unified touch event for queue communication + struct TouchEvent { + int chan_id; + bool is_active; + }; + // Common helper methods - void dump_config_base_(); - void dump_config_sensors_(); bool create_touch_queue_(); void cleanup_touch_queue_(); void configure_wakeup_pads_(); // Helper methods for loop() logic void process_setup_mode_logging_(uint32_t now); - bool should_check_for_releases_(uint32_t now); void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now); - void check_and_disable_loop_if_all_released_(size_t pads_off); - void calculate_release_timeout_(); + + // Unified callbacks for new API + static bool on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, void *ctx); + static bool on_inactive_cb(touch_sensor_handle_t handle, const touch_inactive_event_data_t *event, void *ctx); // Common members std::vector children_; bool setup_mode_{false}; uint32_t setup_mode_last_log_print_{0}; - uint32_t last_release_check_{0}; - uint32_t release_timeout_ms_{1500}; - uint32_t release_check_interval_ms_{50}; + + // Controller handle (new API) + touch_sensor_handle_t sens_handle_{nullptr}; + QueueHandle_t touch_queue_{nullptr}; // Common configuration parameters - uint16_t sleep_cycle_{4095}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; - touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; - touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; + float meas_interval_us_{320.0f}; - // Common constants - static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; +#ifdef USE_ESP32_VARIANT_ESP32 + float charge_duration_ms_{1.0f}; +#else + uint32_t charge_times_{500}; +#endif - // ==================== PLATFORM SPECIFIC ==================== +#if !defined(USE_ESP32_VARIANT_ESP32P4) + touch_volt_lim_l_t low_voltage_reference_{TOUCH_VOLT_LIM_L_0V5}; + touch_volt_lim_h_t high_voltage_reference_{TOUCH_VOLT_LIM_H_2V7}; +#endif #ifdef USE_ESP32_VARIANT_ESP32 // ESP32 v1 specific - - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; - - private: - // Touch event structure for ESP32 v1 - // Contains touch pad info, value, and touch state for queue communication - struct TouchPadEventV1 { - touch_pad_t pad; - uint32_t value; - bool is_touched; - }; - - protected: uint32_t iir_filter_{0}; bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } -#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - // ESP32-S2/S3 v2 specific - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + // ESP32-S2/S3/P4 v2/v3 specific - private: - // Touch event structure for ESP32 v2 (S2/S3) - // Contains touch pad and interrupt mask for queue communication - struct TouchPadEventV2 { - touch_pad_t pad; - uint32_t intr_mask; - }; - - protected: - // Filter configuration - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + // Filter configuration - use sentinel values to detect "not configured" + touch_benchmark_filter_mode_t filter_mode_{TOUCH_BM_JITTER_FILTER}; uint32_t debounce_count_{0}; uint32_t noise_threshold_{0}; uint32_t jitter_step_{0}; - touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + touch_smooth_filter_mode_t smooth_level_{TOUCH_SMOOTH_NO_FILTER}; + bool filter_configured_{false}; +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN // Denoise configuration - touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - - // Waterproof configuration - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; - - bool filter_configured_() const { - return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); - } - bool denoise_configured_() const { - return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX); - } - bool waterproof_configured_() const { - return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && - (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); - } - - // Helper method to read touch values - non-blocking operation - // Returns the current touch pad value using either filtered or raw reading - // based on the filter configuration - uint32_t read_touch_value(touch_pad_t pad) const; - - // Helper to update touch state with a known state and value - void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value); - - // Helper to read touch value and update state for a given child - bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); + touch_denoise_chan_resolution_t denoise_grade_{TOUCH_DENOISE_CHAN_RESOLUTION_BIT12}; + touch_denoise_chan_cap_t denoise_cap_level_{TOUCH_DENOISE_CHAN_CAP_5PF}; + bool denoise_configured_{false}; #endif - // Helper functions for dump_config - common to both implementations - static const char *get_low_voltage_reference_str(touch_low_volt_t ref) { - switch (ref) { - case TOUCH_LVOLT_0V5: - return "0.5V"; - case TOUCH_LVOLT_0V6: - return "0.6V"; - case TOUCH_LVOLT_0V7: - return "0.7V"; - case TOUCH_LVOLT_0V8: - return "0.8V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_high_voltage_reference_str(touch_high_volt_t ref) { - switch (ref) { - case TOUCH_HVOLT_2V4: - return "2.4V"; - case TOUCH_HVOLT_2V5: - return "2.5V"; - case TOUCH_HVOLT_2V6: - return "2.6V"; - case TOUCH_HVOLT_2V7: - return "2.7V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) { - switch (atten) { - case TOUCH_HVOLT_ATTEN_1V5: - return "1.5V"; - case TOUCH_HVOLT_ATTEN_1V: - return "1V"; - case TOUCH_HVOLT_ATTEN_0V5: - return "0.5V"; - case TOUCH_HVOLT_ATTEN_0V: - return "0V"; - default: - return "UNKNOWN"; - } - } + // Waterproof configuration + int waterproof_guard_ring_pad_{-1}; + uint32_t waterproof_shield_driver_{0}; + bool waterproof_configured_{false}; +#endif }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} + ESP32TouchBinarySensor(int channel_id, uint32_t threshold, uint32_t wakeup_threshold) + : channel_id_(channel_id), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - touch_pad_t get_touch_pad() const { return this->touch_pad_; } + int get_channel_id() const { return this->channel_id_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } @@ -242,39 +189,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - /// Ensure benchmark value is read (v2 touch hardware only). - /// Called from multiple places - kept as helper to document shared usage. - void ensure_benchmark_read() { - if (this->benchmark_ == 0) { - touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_); - } - } -#endif - protected: friend ESP32TouchComponent; - touch_pad_t touch_pad_{TOUCH_PAD_MAX}; + int channel_id_; + touch_channel_handle_t chan_handle_{nullptr}; uint32_t threshold_{0}; - uint32_t benchmark_{}; + uint32_t benchmark_{0}; /// Stores the last raw touch measurement value. uint32_t value_{0}; bool last_state_{false}; const uint32_t wakeup_threshold_{0}; // Track last touch time for timeout-based release detection - // Design note: last_touch_time_ does not require synchronization primitives because: - // 1. ESP32 guarantees atomic 32-bit aligned reads/writes - // 2. ISR only writes timestamps, main loop only reads - // 3. Timing tolerance allows for occasional stale reads (50ms check interval) - // 4. Queue operations provide implicit memory barriers - // Using atomic/critical sections would add overhead without meaningful benefit - uint32_t last_touch_time_{}; - bool initial_state_published_{}; + bool initial_state_published_{false}; }; -} // namespace esp32_touch -} // namespace esphome +} // namespace esphome::esp32_touch #endif diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp deleted file mode 100644 index 429b5173be8..00000000000 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/log.h" -#include - -#include "soc/rtc.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::dump_config_base_() { - const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_); - const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_); - const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_); - - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms\n" - " Low Voltage Reference: %s\n" - " High Voltage Reference: %s\n" - " Voltage Attenuation: %s\n" - " Release Timeout: %" PRIu32 "ms\n", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, - atten_s, this->release_timeout_ms_); -} - -void ESP32TouchComponent::dump_config_sensors_() { - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, - " Pad: T%u\n" - " Threshold: %" PRIu32 "\n" - " Benchmark: %" PRIu32, - (unsigned) child->touch_pad_, child->threshold_, child->benchmark_); - } -} - -bool ESP32TouchComponent::create_touch_queue_() { - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. - size_t queue_size = this->children_.size() * 4; - if (queue_size < 8) - queue_size = 8; - -#ifdef USE_ESP32_VARIANT_ESP32 - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1)); -#else - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2)); -#endif - - if (this->touch_queue_ == nullptr) { - ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); - this->mark_failed(); - return false; - } - return true; -} - -void ESP32TouchComponent::cleanup_touch_queue_() { - if (this->touch_queue_) { - vQueueDelete(this->touch_queue_); - this->touch_queue_ = nullptr; - } -} - -void ESP32TouchComponent::configure_wakeup_pads_() { - bool is_wakeup_source = false; - - // Check if any pad is configured for wakeup - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - is_wakeup_source = true; - -#ifdef USE_ESP32_VARIANT_ESP32 - // ESP32 v1: No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#else - // ESP32-S2/S3 v2: Set threshold for wakeup - touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - // If no pad is configured for wakeup, deinitialize touch pad - touch_pad_deinit(); - } -} - -void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { - if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { - for (auto *child : this->children_) { -#ifdef USE_ESP32_VARIANT_ESP32 - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); -#else - // Read the value being used for touch detection - uint32_t value = this->read_touch_value(child->get_touch_pad()); - // Store the value for get_value() access in lambdas - child->value_ = value; - // Read benchmark if not already read - child->ensure_benchmark_read(); - // Calculate difference to help user set threshold - // For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold - // So threshold should be < (value - benchmark) when touched - int32_t difference = static_cast(value) - static_cast(child->benchmark_); - ESP_LOGD(TAG, - "Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32 - " to detect touch)", - child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference); -#endif - } - this->setup_mode_last_log_print_ = now; - } -} - -bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { - if (now - this->last_release_check_ < this->release_check_interval_ms_) { - return false; - } - this->last_release_check_ = now; - return true; -} - -void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { - if (!child->initial_state_published_) { - // Check if enough time has passed since startup - if (now > this->release_timeout_ms_) { - child->publish_initial_state(false); - child->initial_state_published_ = true; - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); - } - } -} - -void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) { - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - if (pads_off == this->children_.size() && !this->setup_mode_) { - this->disable_loop(); - } -} - -void ESP32TouchComponent::calculate_release_timeout_() { - // Calculate release timeout based on sleep cycle - // Design note: Hardware limitation - interrupts only fire reliably on touch (not release) - // We must use timeout-based detection for release events - // Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum - // Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX - - uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); - - // Calculate timeout as 3 sleep cycles - this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq; - - if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) { - this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS; - } - - // Check for releases at 1/4 the timeout interval - // Since hardware doesn't generate reliable release interrupts, we must poll - // for releases in the main loop. Checking at 1/4 the timeout interval provides - // a good balance between responsiveness and efficiency. - this->release_check_interval_ms_ = this->release_timeout_ms_ / 4; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp deleted file mode 100644 index ffb805e0080..00000000000 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#ifdef USE_ESP32_VARIANT_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -#include -#include - -// Include HAL for ISR-safe touch reading -#include "hal/touch_sensor_ll.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; - -void ESP32TouchComponent::setup() { - // Create queue for touch events - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. This is important because - // ESP32 v1 scans all pads on each interrupt, potentially sending multiple events. - if (!this->create_touch_queue_()) { - return; - } - - touch_pad_init(); - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Set up IIR filter if enabled - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } - - // Configure measurement parameters -#if ESP_IDF_VERSION_MAJOR >= 5 - touch_pad_set_measurement_clock_cycles(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); -#else - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); -#endif - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - - // Configure each touch pad - for (auto *child : this->children_) { - if (this->setup_mode_) { - touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); - } else { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); - } - } - - // Register ISR handler - esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); - - // Enable touch pad interrupt - touch_pad_intr_enable(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // Print debug info for all pads in setup mode - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - // Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0) - // This is more efficient than sending all pad states every interrupt - TouchPadEventV1 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - // Find the corresponding sensor - O(n) search is acceptable since events are infrequent - for (auto *child : this->children_) { - if (child->get_touch_pad() != event.pad) { - continue; - } - - // Found matching pad - process it - child->value_ = event.value; - - // The interrupt gives us the touch state directly - bool new_state = event.is_touched; - - // Track when we last saw this pad as touched - if (new_state) { - child->last_touch_time_ = now; - } - - // Only publish if state changed - this filters out repeated events - if (new_state != child->last_state_) { - child->initial_state_published_ = true; - child->last_state_ = new_state; - child->publish_state(new_state); - // Original ESP32: ISR only fires when touched, release is detected by timeout - // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); - } - break; // Exit inner loop after processing matching pad - } - } - - // Check for released pads periodically - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently, assume it's released - child->last_state_ = false; - child->publish_state(false); - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str()); - pads_off++; - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - // The loop will be re-enabled by the ISR when any touch pad is touched. - // v1 hardware limitations require us to check all pads are off because: - // - v1 only generates interrupts on touch events (not releases) - // - We must poll for release timeouts in the main loop - // - We can only safely disable when no pads need timeout monitoring - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - touch_pad_intr_disable(); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - - uint32_t mask = 0; - touch_ll_read_trigger_status_mask(&mask); - touch_ll_clear_trigger_status_mask(); - touch_pad_clear_status(); - - // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured - // touch pad detects a touch (value goes below threshold). The hardware does NOT - // generate interrupts on release - only on touch events. - // The interrupt will continue to fire periodically (based on sleep_cycle) as long - // as any pad remains touched. This allows us to detect both new touches and - // continued touches, but releases must be detected by timeout in the main loop. - - // Process all configured pads to check their current state - // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, - // so we must scan all configured pads to find which ones were touched - for (auto *child : component->children_) { - touch_pad_t pad = child->get_touch_pad(); - - // Read current value using ISR-safe API - // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe - // In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes: - // "assert failed: xQueueSemaphoreTake queue.c:1718" - // We must use raw values even when filter is enabled as a workaround. - // Users should adjust thresholds to compensate for the lack of IIR filtering. - // See: https://github.com/espressif/esp-idf/issues/17045 - uint32_t value = touch_ll_read_raw_data(pad); - - // Skip pads that aren’t in the trigger mask - if (((mask >> pad) & 1) == 0) { - continue; - } - - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - bool is_touched = value < child->get_threshold(); - - // Always send the current state - the main loop will filter for changes - // We send both touched and untouched states because the ISR doesn't - // track previous state (to keep ISR fast and simple) - TouchPadEventV1 event; - event.pad = pad; - event.value = value; - event.is_touched = is_touched; - - // Send to queue from ISR - non-blocking, drops if queue full - BaseType_t x_higher_priority_task_woken = pdFALSE; - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } - } -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp deleted file mode 100644 index b34ca1abd35..00000000000 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ /dev/null @@ -1,402 +0,0 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -// Helper to update touch state with a known state and value -void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) { - // Store the value for get_value() access in lambdas - child->value_ = value; - - // Always update timer when touched - if (is_touched) { - child->last_touch_time_ = App.get_loop_component_start_time(); - } - - if (child->last_state_ != is_touched) { - child->last_state_ = is_touched; - child->publish_state(is_touched); - if (is_touched) { - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - value, child->threshold_ + child->benchmark_); - } else { - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); - } - } -} - -// Helper to read touch value and update state for a given child (used for timeout events) -bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { - // Read current touch value - uint32_t value = this->read_touch_value(child->touch_pad_); - - // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark - ESP_LOGV(TAG, - "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32, - child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); - bool is_touched = value > child->benchmark_ + child->threshold_; - - this->update_touch_state_(child, is_touched, value); - return is_touched; -} - -void ESP32TouchComponent::setup() { - // Create queue for touch events first - if (!this->create_touch_queue_()) { - return; - } - - // Initialize touch pad peripheral - esp_err_t init_err = touch_pad_init(); - if (init_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err)); - this->mark_failed(); - return; - } - - // Configure each touch pad first - for (auto *child : this->children_) { - esp_err_t config_err = touch_pad_config(child->touch_pad_); - if (config_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err)); - } - } - - // Set up filtering if configured - if (this->filter_configured_()) { - touch_filter_config_t filter_info = { - .mode = this->filter_mode_, - .debounce_cnt = this->debounce_count_, - .noise_thr = this->noise_threshold_, - .jitter_step = this->jitter_step_, - .smh_lvl = this->smooth_level_, - }; - touch_pad_filter_set_config(&filter_info); - touch_pad_filter_enable(); - } - - if (this->denoise_configured_()) { - touch_pad_denoise_t denoise = { - .grade = this->grade_, - .cap_level = this->cap_level_, - }; - touch_pad_denoise_set_config(&denoise); - touch_pad_denoise_enable(); - } - - if (this->waterproof_configured_()) { - touch_pad_waterproof_t waterproof = { - .guard_ring_pad = this->waterproof_guard_ring_pad_, - .shield_driver = this->waterproof_shield_driver_, - }; - touch_pad_waterproof_set_config(&waterproof); - touch_pad_waterproof_enable(); - } - - // Configure measurement parameters - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - touch_pad_set_charge_discharge_times(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); - - // Disable hardware timeout - it causes continuous interrupts with high-capacitance - // setups (e.g., pressure sensors under cushions). The periodic release check in - // loop() handles state detection reliably without needing hardware timeout. - touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX); - - // Register ISR handler with interrupt mask - esp_err_t err = - touch_pad_isr_register(touch_isr_handler, this, static_cast(TOUCH_PAD_INTR_MASK_ALL)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Set thresholds for each pad BEFORE starting FSM - for (auto *child : this->children_) { - if (child->threshold_ != 0) { - touch_pad_set_thresh(child->touch_pad_, child->threshold_); - } - } - - // Enable interrupts - only ACTIVE and TIMEOUT - // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable - // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based - // release detection with the ability to verify the actual state. - touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); - - // Set FSM mode before starting - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Start FSM - touch_pad_fsm_start(); - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->filter_configured_()) { - const char *filter_mode_s; - switch (this->filter_mode_) { - case TOUCH_PAD_FILTER_IIR_4: - filter_mode_s = "IIR_4"; - break; - case TOUCH_PAD_FILTER_IIR_8: - filter_mode_s = "IIR_8"; - break; - case TOUCH_PAD_FILTER_IIR_16: - filter_mode_s = "IIR_16"; - break; - case TOUCH_PAD_FILTER_IIR_32: - filter_mode_s = "IIR_32"; - break; - case TOUCH_PAD_FILTER_IIR_64: - filter_mode_s = "IIR_64"; - break; - case TOUCH_PAD_FILTER_IIR_128: - filter_mode_s = "IIR_128"; - break; - case TOUCH_PAD_FILTER_IIR_256: - filter_mode_s = "IIR_256"; - break; - case TOUCH_PAD_FILTER_JITTER: - filter_mode_s = "JITTER"; - break; - default: - filter_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, - " Filter mode: %s\n" - " Debounce count: %" PRIu32 "\n" - " Noise threshold coefficient: %" PRIu32 "\n" - " Jitter filter step size: %" PRIu32, - filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); - const char *smooth_level_s; - switch (this->smooth_level_) { - case TOUCH_PAD_SMOOTH_OFF: - smooth_level_s = "OFF"; - break; - case TOUCH_PAD_SMOOTH_IIR_2: - smooth_level_s = "IIR_2"; - break; - case TOUCH_PAD_SMOOTH_IIR_4: - smooth_level_s = "IIR_4"; - break; - case TOUCH_PAD_SMOOTH_IIR_8: - smooth_level_s = "IIR_8"; - break; - default: - smooth_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); - } - - if (this->denoise_configured_()) { - const char *grade_s; - switch (this->grade_) { - case TOUCH_PAD_DENOISE_BIT12: - grade_s = "BIT12"; - break; - case TOUCH_PAD_DENOISE_BIT10: - grade_s = "BIT10"; - break; - case TOUCH_PAD_DENOISE_BIT8: - grade_s = "BIT8"; - break; - case TOUCH_PAD_DENOISE_BIT4: - grade_s = "BIT4"; - break; - default: - grade_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); - - const char *cap_level_s; - switch (this->cap_level_) { - case TOUCH_PAD_DENOISE_CAP_L0: - cap_level_s = "L0"; - break; - case TOUCH_PAD_DENOISE_CAP_L1: - cap_level_s = "L1"; - break; - case TOUCH_PAD_DENOISE_CAP_L2: - cap_level_s = "L2"; - break; - case TOUCH_PAD_DENOISE_CAP_L3: - cap_level_s = "L3"; - break; - case TOUCH_PAD_DENOISE_CAP_L4: - cap_level_s = "L4"; - break; - case TOUCH_PAD_DENOISE_CAP_L5: - cap_level_s = "L5"; - break; - case TOUCH_PAD_DENOISE_CAP_L6: - cap_level_s = "L6"; - break; - case TOUCH_PAD_DENOISE_CAP_L7: - cap_level_s = "L7"; - break; - default: - cap_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // V2 TOUCH HANDLING: - // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach: - // 1. Process ACTIVE interrupts when pads are touched - // 2. Use timeout-based release detection (like v1) - // 3. But smarter than v1: verify actual state before releasing on timeout - // This prevents false releases if we missed interrupts - - // In setup mode, periodically log all pad values - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - TouchPadEventV2 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad); - // Handle timeout events - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // Resume measurement after timeout - touch_pad_timeout_resume(); - // For timeout events, always check the current state - } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { - // Skip if not an active/timeout event - continue; - } - - // Find the child for the pad that triggered the interrupt - for (auto *child : this->children_) { - if (child->touch_pad_ == event.pad) { - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // For timeout events, we need to read the value to determine state - this->check_and_update_touch_state_(child); - } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { - // We only get ACTIVE interrupts now, releases are detected by timeout - // Read the current value - uint32_t value = this->read_touch_value(child->touch_pad_); - this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts - } - break; - } - } - } - - // Check for released pads periodically (like v1) - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - child->ensure_benchmark_read(); - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently - verify actual state - // Unlike v1, v2 hardware allows us to read the current state anytime - // This makes v2 smarter: we can verify if it's actually released before - // declaring a timeout, preventing false releases if interrupts were missed - bool still_touched = this->check_and_update_touch_state_(child); - - if (still_touched) { - // Still touched! Timer was reset in update_touch_state_ - ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer", - child->get_name().c_str(), this->release_timeout_ms_); - } else { - // Actually released - already handled by check_and_update_touch_state_ - pads_off++; - } - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop when all pads are off and not in setup mode (like v1) - // We need to keep checking for timeouts, so only disable when all pads are confirmed off - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - // Disable interrupts - touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - BaseType_t x_higher_priority_task_woken = pdFALSE; - - // Read interrupt status - TouchPadEventV2 event; - event.intr_mask = touch_pad_read_intr_status_mask(); - event.pad = touch_pad_get_current_meas_channel(); - - // Send event to queue for processing in main loop - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } -} - -uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const { - // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations. - // The hardware continuously samples in the background and we can read the - // latest value at any time without waiting. - uint32_t value = 0; - if (this->filter_configured_()) { - // Read filtered/smoothed value when filter is enabled - touch_pad_filter_read_smooth(pad, &value); - } else { - // Read raw value when filter is not configured - touch_pad_read_raw_data(pad, &value); - } - return value; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/tests/components/esp32_touch/common-variants.yaml b/tests/components/esp32_touch/common-variants.yaml index 69a3dbd9691..5d6d0bbd195 100644 --- a/tests/components/esp32_touch/common-variants.yaml +++ b/tests/components/esp32_touch/common-variants.yaml @@ -4,7 +4,6 @@ esp32_touch: measurement_duration: 8ms low_voltage_reference: 0.5V high_voltage_reference: 2.7V - voltage_attenuation: 1.5V binary_sensor: - platform: esp32_touch diff --git a/tests/components/esp32_touch/test.esp32-p4-idf.yaml b/tests/components/esp32_touch/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..ab5484af449 --- /dev/null +++ b/tests/components/esp32_touch/test.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + pin: GPIO5 + +<<: !include common-variants.yaml +<<: !include common-get-value.yaml From 07406c96e136b61a4521ba86c00325b96f65fc66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:35:15 -1000 Subject: [PATCH 0257/2056] Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#14326) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 0328611f5c2..6d200956e9d 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8718772f537..c2a93d37fdb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -822,7 +822,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-target path: memory-analysis-target.json @@ -886,7 +886,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-pr path: memory-analysis-pr.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 479b01ee37d..0cca4dcaf6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests From bd3f8e006c40dfc5e64dc37f3f90e9bc2c4ec5ac Mon Sep 17 00:00:00 2001 From: whitty Date: Sat, 28 Feb 2026 03:02:29 +1100 Subject: [PATCH 0258/2056] [esp32_ble] allow setting of min/max key_size and auth_req_mode (#7138) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/esp32_ble/__init__.py | 52 +++++++++++++ esphome/components/esp32_ble/ble.cpp | 74 ++++++++++++++++++- esphome/components/esp32_ble/ble.h | 26 +++++++ esphome/core/defines.h | 1 + ...nded-auth-req-params-single.esp32-idf.yaml | 6 ++ ...st-extended-auth-req-params.esp32-idf.yaml | 5 ++ 6 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml create mode 100644 tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index c0e2f78bde3..8b368afc2e2 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -21,6 +21,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] @@ -188,6 +189,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None: CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_AUTH_REQ_MODE = "auth_req_mode" +CONF_MAX_KEY_SIZE = "max_key_size" +CONF_MIN_KEY_SIZE = "min_key_size" CONF_ADVERTISING = "advertising" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" @@ -238,6 +242,18 @@ IO_CAPABILITY = { "display_yes_no": IoCapability.IO_CAP_IO, } +AuthReqMode = esp32_ble_ns.enum("AuthReqMode") +AUTH_REQ_MODE = { + "no_bond": AuthReqMode.AUTH_REQ_NO_BOND, + "bond": AuthReqMode.AUTH_REQ_BOND, + "mitm": AuthReqMode.AUTH_REQ_MITM, + "bond_mitm": AuthReqMode.AUTH_REQ_BOND_MITM, + "sc_only": AuthReqMode.AUTH_REQ_SC_ONLY, + "sc_bond": AuthReqMode.AUTH_REQ_SC_BOND, + "sc_mitm": AuthReqMode.AUTH_REQ_SC_MITM, + "sc_mitm_bond": AuthReqMode.AUTH_REQ_SC_MITM_BOND, +} + esp_power_level_t = cg.global_ns.enum("esp_power_level_t") TX_POWER_LEVELS = { @@ -258,6 +274,10 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( IO_CAPABILITY, lower=True ), + # note: no defaults so we can action them not being present + cv.Optional(CONF_AUTH_REQ_MODE): cv.enum(AUTH_REQ_MODE, lower=True), + cv.Optional(CONF_MAX_KEY_SIZE): cv.int_range(min=7, max=16), + cv.Optional(CONF_MIN_KEY_SIZE): cv.int_range(min=7, max=16), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( @@ -279,6 +299,23 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def _validate_key_sizes(config: ConfigType) -> ConfigType: + if ( + CONF_MIN_KEY_SIZE in config + and CONF_MAX_KEY_SIZE in config + and config[CONF_MIN_KEY_SIZE] > config[CONF_MAX_KEY_SIZE] + ): + raise cv.Invalid( + f"min_key_size ({config[CONF_MIN_KEY_SIZE]}) must be " + f"less than or equal to " + f"max_key_size ({config[CONF_MAX_KEY_SIZE]})" + ) + return config + + +CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) + + bt_uuid16_format = "XXXX" bt_uuid32_format = "XXXXXXXX" bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" @@ -487,6 +524,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) + + if ( + CONF_AUTH_REQ_MODE in config + or CONF_MAX_KEY_SIZE in config + or CONF_MIN_KEY_SIZE in config + ): + cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None) + + if CONF_AUTH_REQ_MODE in config: + cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE])) + if CONF_MAX_KEY_SIZE in config: + cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE])) + if CONF_MIN_KEY_SIZE in config: + cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE])) + cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) if (name := config.get(CONF_NAME)) is not None: cg.add(var.set_name(name)) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index acbe9d88fcc..9d260188003 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -296,12 +296,39 @@ bool ESP32BLE::ble_setup_() { return false; } - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(esp_ble_io_cap_t)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + ESP_LOGE(TAG, "esp_ble_gap_set_security_param iocap_mode failed: %d", err); return false; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + if (this->max_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &(this->max_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param max_key_size failed: %d", err); + return false; + } + } + + if (this->min_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MIN_KEY_SIZE, &(this->min_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param min_key_size failed: %d", err); + return false; + } + } + + if (this->auth_req_mode_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &(this->auth_req_mode_.value()), + sizeof(esp_ble_auth_req_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param authen_req_mode failed: %d", err); + return false; + } + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT @@ -645,6 +672,7 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, @@ -652,6 +680,48 @@ void ESP32BLE::dump_config() { " MAC address: %s\n" " IO Capability: %s", mac_s, io_capability_s); + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + const char *auth_req_mode_s = ""; + if (this->auth_req_mode_) { + switch (this->auth_req_mode_.value()) { + case AUTH_REQ_NO_BOND: + auth_req_mode_s = "no_bond"; + break; + case AUTH_REQ_BOND: + auth_req_mode_s = "bond"; + break; + case AUTH_REQ_MITM: + auth_req_mode_s = "mitm"; + break; + case AUTH_REQ_BOND_MITM: + auth_req_mode_s = "bond_mitm"; + break; + case AUTH_REQ_SC_ONLY: + auth_req_mode_s = "sc_only"; + break; + case AUTH_REQ_SC_BOND: + auth_req_mode_s = "sc_bond"; + break; + case AUTH_REQ_SC_MITM: + auth_req_mode_s = "sc_mitm"; + break; + case AUTH_REQ_SC_MITM_BOND: + auth_req_mode_s = "sc_mitm_bond"; + break; + } + } + + ESP_LOGCONFIG(TAG, " Auth Req Mode: %s", auth_req_mode_s); + if (this->max_key_size_ && this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - %u", this->min_key_size_, this->max_key_size_); + } else if (this->max_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: - %u", this->max_key_size_); + } else if (this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - ", this->min_key_size_); + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index f1ab81b6dc6..2ce17e97be0 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -52,6 +52,19 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS +enum AuthReqMode { + AUTH_REQ_NO_BOND = ESP_LE_AUTH_NO_BOND, + AUTH_REQ_BOND = ESP_LE_AUTH_BOND, + AUTH_REQ_MITM = ESP_LE_AUTH_REQ_MITM, + AUTH_REQ_BOND_MITM = ESP_LE_AUTH_REQ_BOND_MITM, + AUTH_REQ_SC_ONLY = ESP_LE_AUTH_REQ_SC_ONLY, + AUTH_REQ_SC_BOND = ESP_LE_AUTH_REQ_SC_BOND, + AUTH_REQ_SC_MITM = ESP_LE_AUTH_REQ_SC_MITM, + AUTH_REQ_SC_MITM_BOND = ESP_LE_AUTH_REQ_SC_MITM_BOND, +}; +#endif + enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, @@ -100,6 +113,12 @@ class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + void set_max_key_size(uint8_t key_size) { this->max_key_size_ = key_size; } + void set_min_key_size(uint8_t key_size) { this->min_key_size_ = key_size; } + void set_auth_req(AuthReqMode req) { this->auth_req_mode_ = (esp_ble_auth_req_t) req; } +#endif + void set_advertising_cycle_time(uint32_t advertising_cycle_time) { this->advertising_cycle_time_ = advertising_cycle_time; } @@ -209,6 +228,13 @@ class ESP32BLE : public Component { // 1-byte aligned members (grouped together to minimize padding) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + optional auth_req_mode_; + + uint8_t max_key_size_{0}; // range is 7..16, 0 is unset + uint8_t min_key_size_{0}; // range is 7..16, 0 is unset +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 673aa246fe4..9b55ae93b1c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -209,6 +209,7 @@ #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 +#define ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS #define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #define USE_ESP32_CAMERA_JPEG_ENCODER #define USE_HTTP_REQUEST_RESPONSE diff --git a/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml b/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml new file mode 100644 index 00000000000..6e191c132f9 --- /dev/null +++ b/tests/components/esp32_ble/test-extended-auth-req-params-single.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32_ble: + io_capability: keyboard_display + # Explicitly not setting some parameters to test ifdef selection + # max_key_size: 16 + # min_key_size: 7 + auth_req_mode: sc_mitm_bond diff --git a/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml b/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml new file mode 100644 index 00000000000..f05b9bac968 --- /dev/null +++ b/tests/components/esp32_ble/test-extended-auth-req-params.esp32-idf.yaml @@ -0,0 +1,5 @@ +esp32_ble: + io_capability: keyboard_display + max_key_size: 16 + min_key_size: 7 + auth_req_mode: sc_mitm_bond From 0f7ac1726d9fea8cdb7ff481fbfcaa8b4b49620e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 09:03:37 -0700 Subject: [PATCH 0259/2056] [core] Extend fast select optimization to LibreTiny platforms (#14254) Co-authored-by: Claude Opus 4.6 --- esphome/components/socket/__init__.py | 11 ++-- esphome/core/application.cpp | 43 ++++++++------- esphome/core/application.h | 27 +++++----- esphome/core/defines.h | 2 + esphome/core/lwip_fast_select.c | 54 +++++++++++-------- esphome/core/lwip_fast_select.h | 2 +- .../socket/test_wake_loop_threadsafe.py | 23 +++++--- 7 files changed, 100 insertions(+), 62 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 5f4d04eb441..a83648979c7 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -149,9 +149,10 @@ def require_wake_loop_threadsafe() -> None: ): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") - if not CORE.is_esp32: - # Only non-ESP32 platforms need a UDP socket for wake notifications. - # ESP32 uses FreeRTOS task notifications instead (no socket needed). + if not CORE.is_esp32 and not CORE.is_libretiny: + # Only platforms without fast select need a UDP socket for wake + # notifications. ESP32 and LibreTiny use FreeRTOS task notifications + # instead (no socket needed). consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) @@ -187,6 +188,10 @@ async def to_code(config): elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") + # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() + # and FreeRTOS task notifications — enable fast select to bypass lwip_select() + if CORE.is_esp32 or CORE.is_libretiny: + cg.add_define("USE_LWIP_FAST_SELECT") def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index a9753da1b5f..3bd4c1c6703 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,10 +9,17 @@ #endif #ifdef USE_ESP32 #include +#endif +#ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 #include #include +#else +#include +#include #endif +#endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include @@ -147,14 +154,14 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. - // Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used - // unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled. + // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally + // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). esphome_lwip_fast_select_init(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) - // Set up wake socket for waking main loop from tasks (non-ESP32 only) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -532,7 +539,7 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -585,7 +592,7 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT // Hook the socket's netconn callback for instant wake on receive events esphome_lwip_hook_socket(fd); #else @@ -609,12 +616,13 @@ void Application::unregister_socket_fd(int fd) { continue; // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback on ESP32 — all LwIP sockets share - // the same static event_callback, and the socket will be closed by the caller. + // No need to unhook the netconn callback on fast select platforms — all LwIP + // sockets share the same static event_callback, and the socket will be closed + // by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); -#ifndef USE_ESP32 +#ifndef USE_LWIP_FAST_SELECT this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { @@ -633,8 +641,8 @@ void Application::unregister_socket_fd(int fd) { void Application::yield_with_select_(uint32_t delay_ms) { // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32) - // ESP32 fast path: reads rcvevent directly via lwip_socket_dbg_get_socket() (~215 ns per socket). +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket(). // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { yield(); @@ -659,9 +667,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); #elif defined(USE_SOCKET_SELECT_SUPPORT) - // Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform). - // ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32 - // use LwIP under the hood, so the fast path handles all ESP32 socket implementations. + // Fallback select() path (host platform and any future platforms without fast select). + // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -725,12 +732,12 @@ alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT void Application::wake_loop_threadsafe() { // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) esphome_lwip_wake_main_loop(); } -#else // !USE_ESP32 +#else // !USE_LWIP_FAST_SELECT void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications @@ -798,7 +805,7 @@ void Application::wake_loop_threadsafe() { lwip_send(this->wake_socket_fd_, &dummy, 1, 0); } } -#endif // USE_ESP32 +#endif // USE_LWIP_FAST_SELECT #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/core/application.h b/esphome/core/application.h index f5df5e7bdf7..0cc29af8e7f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,7 +24,7 @@ #endif #ifdef USE_SOCKET_SELECT_SUPPORT -#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" #else #include @@ -511,10 +511,11 @@ class Application { #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket() - /// which has no refcount; safe only because the main loop owns socket lifetime - /// (creates, reads, and closes sockets on the same thread). -#ifdef USE_ESP32 + /// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via + /// lwip_socket_dbg_get_socket(), which has no refcount; safe only because + /// the main loop owns socket lifetime (creates, reads, and closes sockets + /// on the same thread). +#ifdef USE_LWIP_FAST_SELECT bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } #else bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } @@ -546,7 +547,7 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -576,7 +577,7 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif #endif @@ -589,7 +590,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -605,12 +606,12 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_ESP32) - // Variable-sized members (not needed on ESP32 — is_socket_ready_ reads rcvevent directly) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes #endif @@ -699,7 +700,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -721,6 +722,6 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9b55ae93b1c..5c982c94b13 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -222,6 +222,7 @@ #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_LWIP_FAST_SELECT #define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI @@ -330,6 +331,7 @@ #define USE_CAPTIVE_PORTAL #define USE_SOCKET_IMPL_LWIP_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 70a6482d480..88cf23b67e8 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -1,11 +1,12 @@ -// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3) // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. // // This must be a .c file (not .cpp) because: -// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units that include bootloader headers +// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units // 2. The netconn callback is a C function pointer // -// defines.h is force-included by the build system (-include flag), providing USE_ESP32 etc. +// USE_ESP32 and USE_LIBRETINY platform flags (-D) control compilation of this file. +// See the guard at the bottom of the header comment for details. // // Thread safety analysis // ====================== @@ -81,20 +82,21 @@ // Written by main loop in hook_socket(). Never restored — all LwIP sockets share // the same static event_callback (DEFAULT_SOCKET_EVENTCB), so the wrapper stays permanently. // Read by TCP/IP thread when invoking the callback. -// Safe: 32-bit aligned pointer writes are atomic on Xtensa and RISC-V (ESP32). -// The TCP/IP thread will see either the old or new pointer atomically — never a -// torn value. Both the wrapper and original callbacks are valid at all times -// (the wrapper itself calls the original), so either value is correct. +// Safe: 32-bit aligned pointer writes are atomic on Xtensa, RISC-V (ESP32), +// and ARM Cortex-M (LibreTiny). The TCP/IP thread will see either the old or +// new pointer atomically — never a torn value. Both the wrapper and original +// callbacks are valid at all times (the wrapper itself calls the original), +// so either value is correct. // // sock->rcvevent (s16_t, 2 bytes): // Written by TCP/IP thread in event_callback under SYS_ARCH_PROTECT. // Read by main loop in has_data() via volatile cast. -// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex, which internally -// uses a critical section with memory barrier (rsync on dual-core Xtensa; on -// single-core builds the spinlock is compiled out, but cross-core visibility is -// not an issue). The volatile cast prevents the compiler -// from caching the read. Aligned 16-bit reads are single-instruction loads on -// Xtensa (L16SI) and RISC-V (LH), which cannot produce torn values. +// Safe: SYS_ARCH_UNPROTECT releases a FreeRTOS mutex (ESP32) or resumes the +// scheduler (LibreTiny), both providing a memory barrier. The volatile cast +// prevents the compiler from caching the read. Aligned 16-bit reads are +// single-instruction loads on Xtensa (L16SI), RISC-V (LH), and ARM Cortex-M +// (LDRH), which cannot produce torn values. On single-core chips (LibreTiny, +// ESP32-C3/C6/H2) cross-core visibility is not an issue. // // FreeRTOS task notification value: // Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks @@ -103,20 +105,30 @@ // critical sections). Multiple concurrent xTaskNotifyGive calls are safe — // the notification count simply increments. -#ifdef USE_ESP32 +// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file. +// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here, +// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT. +#if defined(USE_ESP32) || defined(USE_LIBRETINY) // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include #include +// FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not +#ifdef USE_ESP32 #include #include +#else +#include +#include +#endif #include "esphome/core/lwip_fast_select.h" #include // Compile-time verification of thread safety assumptions. -// On ESP32 (Xtensa/RISC-V), naturally-aligned reads/writes up to 32 bits are atomic. +// On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned +// reads/writes up to 32 bits are atomic. // These asserts ensure our cross-thread shared state meets those requirements. // Pointer types must fit in a single 32-bit store (atomic write) @@ -126,7 +138,7 @@ _Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 byt // rcvevent must fit in a single atomic read _Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); -// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V. +// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM. // Misaligned access would not be atomic even if the size is <= 4 bytes. _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == 0, "netconn.callback must be naturally aligned for atomic access"); @@ -183,9 +195,9 @@ bool esphome_lwip_socket_has_data(int fd) { return false; // volatile prevents the compiler from caching/reordering this cross-thread read. // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex with a memory barrier (rsync on Xtensa), ensuring the value is - // visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH) on - // Xtensa/RISC-V and cannot produce torn values. + // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value + // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on + // Xtensa/RISC-V/ARM and cannot produce torn values. return *(volatile s16_t *) &sock->rcvevent > 0; } @@ -200,7 +212,7 @@ void esphome_lwip_hook_socket(int fd) { s_original_callback = sock->conn->callback; } - // Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write). + // Replace with our wrapper. Atomic on all supported platforms (32-bit aligned pointer write). // TCP/IP thread sees either old or new pointer — both are valid. sock->conn->callback = esphome_socket_event_callback; } @@ -213,4 +225,4 @@ void esphome_lwip_wake_main_loop(void) { } } -#endif // USE_ESP32 +#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 73a89fdc3db..b08c9462126 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -1,6 +1,6 @@ #pragma once -// Fast socket monitoring for ESP32 (ESP-IDF LwIP) +// Fast socket monitoring for ESP32 and LibreTiny (LwIP >= 2.1.3) // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. #include diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index 28b4ee564fe..0434b3e1b54 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -1,9 +1,14 @@ +import pytest + from esphome.components import socket from esphome.const import ( KEY_CORE, KEY_TARGET_PLATFORM, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, ) from esphome.core import CORE @@ -90,9 +95,15 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() - assert udp_consumers == initial_udp -def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: - """Test that ESP32 uses task notifications instead of UDP socket.""" - _setup_platform(PLATFORM_ESP32) +@pytest.mark.parametrize( + "platform", + [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X], +) +def test_require_wake_loop_threadsafe__fast_select_no_udp_socket( + platform: str, +) -> None: + """Test that fast select platforms use task notifications instead of UDP socket.""" + _setup_platform(platform) CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() @@ -100,13 +111,13 @@ def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None: assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - # Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications) + # Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications) udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) assert "socket.wake_loop_threadsafe" not in udp_consumers -def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None: - """Test that non-ESP32 platforms consume a UDP socket for wake notifications.""" +def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None: + """Test that platforms without fast select consume a UDP socket for wake notifications.""" _setup_platform(PLATFORM_ESP8266) CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() From ef9fc87351a288293c0ad3e9ab59c7788af8db09 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:17:04 -0500 Subject: [PATCH 0260/2056] [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 3 ++- esphome/components/zigbee/zigbee_zephyr.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 7e917a9d704..a327cc29886 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType from .const_zephyr import ( @@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_define("USE_ZIGBEE") if CORE.using_zephyr: diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 0b6daa9476a..a1e6ad3097e 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None: "USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF) ) cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT") + + # Generate attribute lists before any await that could yield (e.g., build_automation + # waiting for variables from other components). If the hub's priority decays while + # yielding, deferred entity jobs may add cluster list globals that reference these + # attribute lists before they're declared. + await _attr_to_code(config) + var = cg.new_Pvariable(config[CONF_ID]) if on_join_config := config.get(CONF_ON_JOIN): @@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None: await cg.register_component(var, config) - await _attr_to_code(config) CORE.add_job(_ctx_to_code, config) From 017d1b2872b9ad85bf5b8a2fb563d14ff517975c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Fri, 27 Feb 2026 11:12:50 -0600 Subject: [PATCH 0261/2056] [audio] Bump microOpus to v0.3.4 (#14346) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index d8d426ec63a..d95fcf66d7b 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.3") + add_idf_component(name="esphome/micro-opus", ref="0.3.4") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 83b2d9d95c8..37bda65afdd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.3 + version: 0.3.4 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From 20314b4d63ee07d97840c821cf1ca0003393a339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:20:08 -0700 Subject: [PATCH 0262/2056] [mdns] Update espressif/mdns to v1.10.0 (#14338) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 420e6a60e30..0d535d69709 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -163,7 +163,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.9.1") + add_idf_component(name="espressif/mdns", ref="1.10.0") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 37bda65afdd..550e7b9af79 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/esp32-camera: version: 2.1.1 espressif/mdns: - version: 1.9.1 + version: 1.10.0 espressif/esp_wifi_remote: version: 1.3.2 rules: From 72ca514cc2650e641faa774a7d43a81dcdf6fb98 Mon Sep 17 00:00:00 2001 From: deirdreobyrne Date: Fri, 27 Feb 2026 17:25:53 +0000 Subject: [PATCH 0263/2056] [esp32_hosted] Add configurable SDIO clock frequency (#14319) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Deirdre Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_hosted/__init__.py | 8 ++++++++ .../test-sdio-speed.esp32-p4-idf.yaml | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 287c780769d..720d81acc4a 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -23,6 +23,7 @@ CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" CONF_SLOT = "slot" +CONF_SDIO_FREQUENCY = "sdio_frequency" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), } ), ) @@ -91,6 +95,10 @@ async def to_code(config): config[CONF_D3_PIN], ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", + int(config[CONF_SDIO_FREQUENCY] // 1000), + ) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" diff --git a/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml new file mode 100644 index 00000000000..9268c9ae415 --- /dev/null +++ b/tests/components/esp32_hosted/test-sdio-speed.esp32-p4-idf.yaml @@ -0,0 +1,16 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 + sdio_frequency: 8MHz + +wifi: + ssid: MySSID + password: password1 From 1c7f769ec7f54775c3d31f426dcfbb14f66a911f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:48:21 -0700 Subject: [PATCH 0264/2056] [core] Add millis_64() HAL function with native ESP32 implementation (#14339) --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 2 ++ esphome/components/host/core.cpp | 2 ++ esphome/components/libretiny/core.cpp | 2 ++ esphome/components/rp2040/core.cpp | 2 ++ .../uptime/sensor/uptime_seconds_sensor.cpp | 4 +-- .../uptime/text_sensor/uptime_text_sensor.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 4 +-- esphome/components/zephyr/core.cpp | 2 ++ esphome/core/hal.h | 1 + esphome/core/scheduler.cpp | 34 ++++++++++-------- esphome/core/scheduler.h | 35 +++++++++++++++---- 12 files changed, 66 insertions(+), 27 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 202d929ab93..b9ae871abf9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -23,6 +23,7 @@ namespace esphome { void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } +uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 236d3022be3..497e99b61f1 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -16,6 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index c20a33fa378..5c08717ac99 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_HOST +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -19,6 +20,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 4dda7c38563..6cbc81938d9 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -13,6 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 37378d88bb1..52c6f1185c4 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "hardware/watchdog.h" @@ -11,6 +12,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 20e8ed8fdae..552e4e5da42 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::uptime { @@ -8,7 +8,7 @@ namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t uptime = millis_64(); const uint64_t seconds_int = uptime / 1000ULL; const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 88ae53fbfcd..c89d23672e9 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_text_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -19,7 +19,7 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); + uint32_t uptime = static_cast(millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e2f9c213314..16674321c9d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,7 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; - root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); + root[ESPHOME_F("uptime")] = static_cast(millis_64() / 1000); return builder.serialize(); } @@ -414,7 +414,7 @@ void WebServer::setup() { // getting a lot of events this->set_interval(10000, [this]() { char buf[32]; - auto uptime = static_cast(App.scheduler.millis_64() / 1000); + auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7027b33f57..7f5d6d44faa 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,6 +18,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 1a4230e4215..fa5ca646f2e 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -32,6 +32,7 @@ namespace esphome { void yield(); uint32_t millis(); +uint64_t millis_64(); uint32_t micros(); void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d9c66b2000b..06c0bb8b1bf 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,8 +28,10 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; +#ifndef USE_ESP32 // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; +#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -150,8 +152,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself - const uint64_t now = this->millis_64_(millis()); + // Get fresh 64-bit timestamp BEFORE taking lock + const uint64_t now_64 = millis_64(); // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -184,7 +186,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->interval = delay; // first execution happens immediately after a random smallish offset uint32_t offset = this->calculate_interval_offset_(delay); - item->set_next_execution(now + offset); + item->set_next_execution(now_64 + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE SchedulerNameLog name_log; ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", @@ -192,11 +194,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif } else { item->interval = 0; - item->set_next_execution(now + delay); + item->set_next_execution(now_64 + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -399,8 +401,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return {}; auto &item = this->items_[0]; - // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller + const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) return 0; @@ -461,8 +462,8 @@ void HOT Scheduler::call(uint32_t now) { this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ - // Convert the fresh timestamp from main loop to 64-bit for scheduler operations - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() + // Extend the caller's 32-bit timestamp to 64-bit for scheduler operations + const auto now_64 = this->millis_64_from_(now); this->process_to_add(); // Track if any items were added to to_add_ during this call (intervals or from callbacks) @@ -474,15 +475,18 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ +#elif !defined(USE_ESP32) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#else + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), + now_64); +#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -710,9 +714,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } - -uint64_t Scheduler::millis_64_(uint32_t now) { +#ifndef USE_ESP32 +uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) @@ -869,6 +872,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } +#endif // not USE_ESP32 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 840ee7159a7..c6053258103 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,6 +10,7 @@ #endif #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -117,12 +118,12 @@ class Scheduler { bool cancel_retry(Component *component, uint32_t id); /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) - uint64_t millis_64(); + uint64_t millis_64() { return esphome::millis_64(); } - // Calculate when the next scheduled item should run - // @param now Fresh timestamp from millis() - must not be stale/cached - // Returns the time in milliseconds until the next scheduled item, or nullopt if no items - // This method performs cleanup of removed items before checking the schedule + // Calculate when the next scheduled item should run. + // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover. + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items. + // This method performs cleanup of removed items before checking the schedule. // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint32_t now); @@ -282,7 +283,25 @@ class Scheduler { // Common implementation for cancel_retry bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); - uint64_t millis_64_(uint32_t now); + // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. + // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). + // On non-ESP32, extends now to 64-bit using rollover tracking. + uint64_t millis_64_from_(uint32_t now) { +#ifdef USE_ESP32 + (void) now; + return millis_64(); +#else + return this->millis_64_impl_(now); +#endif + } + +#ifndef USE_ESP32 + // On non-ESP32 platforms, millis_64() HAL function delegates to this method + // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. + // On ESP32, millis_64() uses esp_timer_get_time() directly. + friend uint64_t millis_64(); + uint64_t millis_64_impl_(uint32_t now); +#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -549,6 +568,9 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; +#ifndef USE_ESP32 + // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. + // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates @@ -577,6 +599,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#endif /* not USE_ESP32 */ }; } // namespace esphome From 4fe173b64487b90d17fbca26727e0ad19bdd0d20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 10:56:57 -0700 Subject: [PATCH 0265/2056] [wifi] Remove stale TODO comment for ESP8266 callback deferral (#14347) --- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cbf7d7d80f8..8911bf15e07 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -470,10 +470,6 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { return LOG_STR("Unspecified"); } -// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). -// All listener notifications should be deferred to wifi_loop_() via pending_ flags -// to avoid stack overflow. Currently only connect_state is deferred; disconnect, -// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { From c3a0eeceec21b6f7e97a2044078c611c843679e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:17:17 -0700 Subject: [PATCH 0266/2056] [wifi] Use direct SDK APIs for LibreTiny SSID retrieval (#14349) --- .../wifi/wifi_component_libretiny.cpp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2cc05928afd..1c5490a3ac3 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -13,6 +13,19 @@ #include #include +#ifdef USE_BK72XX +extern "C" { +#include +} +#endif + +#ifdef USE_RTL87XX +extern "C" { +#include +#include +} +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -760,10 +773,22 @@ bssid_t WiFiComponent::wifi_bssid() { } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } const char *WiFiComponent::wifi_ssid_to(std::span buffer) { - // TODO: Find direct LibreTiny API to avoid Arduino String allocation +#ifdef USE_BK72XX + LinkStatusTypeDef link_status{}; + bk_wlan_get_link_status(&link_status); + size_t len = strnlen(reinterpret_cast(link_status.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), link_status.ssid, len); +#elif defined(USE_RTL87XX) + rtw_wifi_setting_t setting{}; + wifi_get_setting("wlan0", &setting); + size_t len = strnlen(reinterpret_cast(setting.ssid), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), setting.ssid, len); +#else + // LN882X: wifi_get_sta_conn_info() provides direct pointer access String ssid = WiFi.SSID(); size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); memcpy(buffer.data(), ssid.c_str(), len); +#endif buffer[len] = '\0'; return buffer.data(); } From 4ae7633418716d4f606e189ad1eb532d7ec00cb5 Mon Sep 17 00:00:00 2001 From: Michael Cassaniti Date: Sat, 28 Feb 2026 05:23:02 +1100 Subject: [PATCH 0267/2056] [safe_mode] Add feature to explicitly mark a boot as successful (#14306) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/safe_mode/__init__.py | 17 ++++++++++++++++ esphome/components/safe_mode/automation.h | 14 +++++++------ esphome/components/safe_mode/safe_mode.cpp | 20 +++++++++++-------- esphome/components/safe_mode/safe_mode.h | 2 ++ .../components/safe_mode/common-enabled.yaml | 4 ++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index d1754aaad72..f54151b7460 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -21,6 +21,7 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) +MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) def _remove_id_if_disabled(value): @@ -53,6 +54,22 @@ CONFIG_SCHEMA = cv.All( ) +@automation.register_action( + "safe_mode.mark_successful", + MarkSuccessfulAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(SafeModeComponent), + } + ), +) +async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(parent)) + return var + + @coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index 1d82ac45f14..dee02c64a07 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,20 +1,22 @@ #pragma once #include "esphome/core/defines.h" - -#ifdef USE_SAFE_MODE_CALLBACK -#include "safe_mode.h" - #include "esphome/core/automation.h" +#include "safe_mode.h" namespace esphome::safe_mode { +#ifdef USE_SAFE_MODE_CALLBACK class SafeModeTrigger final : public Trigger<> { public: explicit SafeModeTrigger(SafeModeComponent *parent) { parent->add_on_safe_mode_callback([this]() { trigger(); }); } }; +#endif // USE_SAFE_MODE_CALLBACK + +template class MarkSuccessfulAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->mark_successful(); } +}; } // namespace esphome::safe_mode - -#endif // USE_SAFE_MODE_CALLBACK diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index aa4fdb8291c..fe2acd96122 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -63,18 +63,22 @@ void SafeModeComponent::dump_config() { float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } +void SafeModeComponent::mark_successful() { + this->clean_rtc(); + this->boot_successful_ = true; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif + // Disable loop since we no longer need to check + this->disable_loop(); +} + void SafeModeComponent::loop() { if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { // successful boot, reset counter ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); - this->clean_rtc(); - this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) - // Mark OTA partition as valid to prevent rollback - esp_ota_mark_app_valid_cancel_rollback(); -#endif - // Disable loop since we no longer need to check - this->disable_loop(); + this->mark_successful(); } } diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 902b8c415de..1b28ea28f20 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -31,6 +31,8 @@ class SafeModeComponent final : public Component { void on_safe_shutdown() override; + void mark_successful(); + #ifdef USE_SAFE_MODE_CALLBACK void add_on_safe_mode_callback(std::function &&callback) { this->safe_mode_callback_.add(std::move(callback)); diff --git a/tests/components/safe_mode/common-enabled.yaml b/tests/components/safe_mode/common-enabled.yaml index c24f49e6b69..43025c60dbd 100644 --- a/tests/components/safe_mode/common-enabled.yaml +++ b/tests/components/safe_mode/common-enabled.yaml @@ -16,3 +16,7 @@ button: switch: - platform: safe_mode name: Safe Mode Switch + +esphome: + on_boot: + - safe_mode.mark_successful From 317dd5b2da73d39839695abb066cb976443dba05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:42:08 -0700 Subject: [PATCH 0268/2056] [ci] Skip memory impact target branch build when tests don't exist (#14316) --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a93d37fdb..b752701920a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -686,7 +686,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} - skip: ${{ steps.check-script.outputs.skip }} + skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -705,10 +705,39 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # All remaining steps only run if script exists + # Check if test files exist on the target branch for the requested + # components and platform. When a PR adds new test files for a platform, + # the target branch won't have them yet, so skip instead of failing. + # This check must be done here (not in determine-jobs.py) because + # determine-jobs runs on the PR branch and cannot see what the target + # branch has. + - name: Check for test files on target branch + id: check-tests + if: steps.check-script.outputs.skip != 'true' + run: | + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + found=false + for component in $(echo "$components" | jq -r '.[]'); do + # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) + for f in tests/components/${component}/test*.${platform}.yaml; do + if [ -f "$f" ]; then + found=true + break 2 + fi + done + done + if [ "$found" = false ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + # All remaining steps only run if script and tests exist - name: Generate cache key id: cache-key - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | # Get the commit SHA of the target branch target_sha=$(git rev-parse HEAD) @@ -735,14 +764,14 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Cache status - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then echo "✓ Cache hit! Using cached memory analysis results." @@ -752,21 +781,21 @@ jobs: fi - name: Restore Python - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} - name: Build, compile, and analyze memory - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' id: build run: | . venv/bin/activate @@ -800,7 +829,7 @@ jobs: --platform "$platform" - name: Save memory analysis to cache - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json @@ -808,7 +837,7 @@ jobs: - name: Extract memory usage for outputs id: extract - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ -f memory-analysis-target.json ]; then ram=$(jq -r '.ram_bytes' memory-analysis-target.json) From 29e1e8bdfd9ce8068cb97f8e0b1c8e8f64af50c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:45:20 -0700 Subject: [PATCH 0269/2056] [wifi] Add LibreTiny component test configs (#14351) --- tests/components/wifi/test.bk72xx-ard.yaml | 1 + tests/components/wifi/test.ln882x-ard.yaml | 1 + tests/components/wifi/test.rtl87xx-ard.yaml | 1 + 3 files changed, 3 insertions(+) create mode 100644 tests/components/wifi/test.bk72xx-ard.yaml create mode 100644 tests/components/wifi/test.ln882x-ard.yaml create mode 100644 tests/components/wifi/test.rtl87xx-ard.yaml diff --git a/tests/components/wifi/test.bk72xx-ard.yaml b/tests/components/wifi/test.bk72xx-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/wifi/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.ln882x-ard.yaml b/tests/components/wifi/test.ln882x-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/wifi/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/wifi/test.rtl87xx-ard.yaml b/tests/components/wifi/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/wifi/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 3411ce21505cd8fdcf74885199ba7627f5ac105b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:49:57 -0700 Subject: [PATCH 0270/2056] [core] Fix Application asm label for Mach-O using __USER_LABEL_PREFIX__ (#14334) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/application.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3bd4c1c6703..e27c2cdfc6f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -727,8 +727,17 @@ void Application::yield_with_select_(uint32_t delay_ms) { #error "Application placement new requires Itanium C++ ABI (GCC/Clang)" #endif static_assert(std::is_default_constructible::value, "Application must be default-constructible"); +// __USER_LABEL_PREFIX__ is "_" on Mach-O (macOS) and empty on ELF (embedded targets). +// String literal concatenation produces the correct platform-specific mangled symbol. +// Two-level macro needed: # stringifies before expansion, so the +// indirection forces __USER_LABEL_PREFIX__ to expand first. +#define ESPHOME_STRINGIFY_IMPL_(x) #x +#define ESPHOME_STRINGIFY_(x) ESPHOME_STRINGIFY_IMPL_(x) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -alignas(Application) char app_storage[sizeof(Application)] asm("_ZN7esphome3AppE"); +alignas(Application) char app_storage[sizeof(Application)] asm( + ESPHOME_STRINGIFY_(__USER_LABEL_PREFIX__) "_ZN7esphome3AppE"); +#undef ESPHOME_STRINGIFY_ +#undef ESPHOME_STRINGIFY_IMPL_ #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) From 8698b01bc7314443867327e11f8431a7f14bf7e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 11:54:13 -0700 Subject: [PATCH 0271/2056] [host] Use native clock_gettime for millis_64() (#14340) Co-authored-by: Claude Opus 4.6 --- esphome/components/host/core.cpp | 6 +++++- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 15 +++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 5c08717ac99..9af85bec58a 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -20,7 +20,11 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint64_t millis_64() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast(spec.tv_sec) * 1000ULL + static_cast(spec.tv_nsec) / 1000000ULL; +} void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 06c0bb8b1bf..97735c48760 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,12 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) +#elif !defined(USE_ESP32) && !defined(USE_HOST) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +714,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#ifndef USE_ESP32 +#if !defined(USE_ESP32) && !defined(USE_HOST) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +872,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // not USE_ESP32 +#endif // !USE_ESP32 && !USE_HOST bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c6053258103..9a62ac16347 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -287,7 +287,7 @@ class Scheduler { // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). // On non-ESP32, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) (void) now; return millis_64(); #else @@ -295,10 +295,9 @@ class Scheduler { #endif } -#ifndef USE_ESP32 - // On non-ESP32 platforms, millis_64() HAL function delegates to this method - // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. - // On ESP32, millis_64() uses esp_timer_get_time() directly. +#if !defined(USE_ESP32) && !defined(USE_HOST) + // On platforms without native 64-bit time, millis_64() HAL function delegates to this + // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); uint64_t millis_64_impl_(uint32_t now); #endif @@ -568,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#ifndef USE_ESP32 - // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) + // On platforms with native 64-bit time (ESP32, Host), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -599,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* not USE_ESP32 */ +#endif /* !USE_ESP32 && !USE_HOST */ }; } // namespace esphome From 9c1d1a0d9fba57b92e083437d422a3e7ca3137ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 12:25:13 -0700 Subject: [PATCH 0272/2056] [color] Use integer math in Color::gradient to reduce code size (#14354) --- esphome/core/color.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp index 14c41c2b0d1..edbc7714720 100644 --- a/esphome/core/color.cpp +++ b/esphome/core/color.cpp @@ -7,12 +7,12 @@ constinit const Color Color::BLACK(0, 0, 0, 0); constinit const Color Color::WHITE(255, 255, 255, 255); Color Color::gradient(const Color &to_color, uint8_t amnt) { + uint8_t inv = 255 - amnt; Color new_color; - float amnt_f = float(amnt) / 255.0f; - new_color.r = amnt_f * (to_color.r - this->r) + this->r; - new_color.g = amnt_f * (to_color.g - this->g) + this->g; - new_color.b = amnt_f * (to_color.b - this->b) + this->b; - new_color.w = amnt_f * (to_color.w - this->w) + this->w; + new_color.r = (uint16_t(this->r) * inv + uint16_t(to_color.r) * amnt) / 255; + new_color.g = (uint16_t(this->g) * inv + uint16_t(to_color.g) * amnt) / 255; + new_color.b = (uint16_t(this->b) * inv + uint16_t(to_color.b) * amnt) / 255; + new_color.w = (uint16_t(this->w) * inv + uint16_t(to_color.w) * amnt) / 255; return new_color; } From 2255c68377304c81ab75da9fda8b79cbcf5c756c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:40:55 +1100 Subject: [PATCH 0273/2056] [esp32] Enable `execute_from_psram` for P4 (#14329) --- esphome/components/esp32/__init__.py | 13 +++-- .../config/execute_from_psram_disabled.yaml | 10 ++++ .../esp32/config/execute_from_psram_p4.yaml | 11 ++++ .../esp32/config/execute_from_psram_s3.yaml | 12 ++++ tests/component_tests/esp32/test_esp32.py | 55 ++++++++++++++++++- 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/esp32/config/execute_from_psram_disabled.yaml create mode 100644 tests/component_tests/esp32/config/execute_from_psram_p4.yaml create mode 100644 tests/component_tests/esp32/config/execute_from_psram_s3.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a34747a183a..998913ecec5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -890,10 +890,10 @@ def final_validate(config): ) ) if advanced[CONF_EXECUTE_FROM_PSRAM]: - if config[CONF_VARIANT] != VARIANT_ESP32S3: + if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}: errs.append( cv.Invalid( - f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant", + f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], ) ) @@ -1627,8 +1627,13 @@ async def to_code(config): _configure_lwip_max_sockets(conf) if advanced[CONF_EXECUTE_FROM_PSRAM]: - add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + if variant == VARIANT_ESP32S3: + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + elif variant == VARIANT_ESP32P4: + add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True) + else: + raise ValueError("Unhandled ESP32 variant") # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides diff --git a/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml b/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml new file mode 100644 index 00000000000..b52255c5adb --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_disabled.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal diff --git a/tests/component_tests/esp32/config/execute_from_psram_p4.yaml b/tests/component_tests/esp32/config/execute_from_psram_p4.yaml new file mode 100644 index 00000000000..e9070a5ae18 --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_p4.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + variant: esp32p4 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: diff --git a/tests/component_tests/esp32/config/execute_from_psram_s3.yaml b/tests/component_tests/esp32/config/execute_from_psram_s3.yaml new file mode 100644 index 00000000000..aec020ddffb --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_s3.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: + mode: octal diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 68bd3a5965b..bd4f9828ce5 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -2,13 +2,17 @@ Test ESP32 configuration """ +from collections.abc import Callable +from pathlib import Path from typing import Any import pytest from esphome.components.esp32 import VARIANTS +from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS import esphome.config_validation as cv from esphome.const import CONF_ESPHOME, PlatformFramework +from esphome.core import CORE from tests.component_tests.types import SetCoreConfigCallable @@ -70,7 +74,7 @@ def test_esp32_config( "advanced": {"execute_from_psram": True}, }, }, - r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + r"'execute_from_psram' is not available on this esp32 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", id="execute_from_psram_invalid_for_variant_config", ), pytest.param( @@ -82,7 +86,18 @@ def test_esp32_config( }, }, r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", - id="execute_from_psram_requires_psram_config", + id="execute_from_psram_requires_psram_s3_config", + ), + pytest.param( + { + "variant": "esp32p4", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_requires_psram_p4_config", ), pytest.param( { @@ -108,3 +123,39 @@ def test_esp32_configuration_errors( with pytest.raises(cv.Invalid, match=error_match): FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +def test_execute_from_psram_s3_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig options.""" + generate_main(component_config_path("execute_from_psram_s3.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_SPIRAM_FETCH_INSTRUCTIONS") is True + assert sdkconfig.get("CONFIG_SPIRAM_RODATA") is True + assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig + + +def test_execute_from_psram_p4_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that execute_from_psram on ESP32-P4 sets the correct sdkconfig options.""" + generate_main(component_config_path("execute_from_psram_p4.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True + assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig + assert "CONFIG_SPIRAM_RODATA" not in sdkconfig + + +def test_execute_from_psram_disabled_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that without execute_from_psram, no XIP sdkconfig options are set.""" + generate_main(component_config_path("execute_from_psram_disabled.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig + assert "CONFIG_SPIRAM_RODATA" not in sdkconfig + assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig From 32133e2f464ec93c968d44cf97e9e689ac81f1ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:42:20 -0500 Subject: [PATCH 0274/2056] Bump ruff from 0.15.3 to 0.15.4 (#14357) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 88a38ffa995..6b2617b6563 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.3 # also change in .pre-commit-config.yaml when updating +ruff==0.15.4 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From edd63e3d2d82bfc6585c391ebdae25b0313c91e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:43:10 -0500 Subject: [PATCH 0275/2056] Bump actions/download-artifact from 7.0.0 to 8.0.0 (#14327) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b752701920a..4c9e8c58bcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -945,13 +945,13 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: memory-analysis-pr path: ./memory-analysis diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cca4dcaf6b..17a2616dffc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: pattern: digests-* path: /tmp/digests From 63e757807ee2a522f493bc65dd38ced7f3d601dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 13:01:09 -0700 Subject: [PATCH 0276/2056] [zephyr] Use native k_uptime_get() for millis_64() (#14350) Co-authored-by: Claude Opus 4.6 --- esphome/components/zephyr/core.cpp | 5 ++--- esphome/core/scheduler.cpp | 10 +++++----- esphome/core/scheduler.h | 10 +++++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 7f5d6d44faa..f0772a4422f 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,8 +16,8 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); #endif void yield() { ::k_yield(); } -uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } +uint32_t millis() { return static_cast(millis_64()); } +uint64_t millis_64() { return static_cast(k_uptime_get()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 97735c48760..73b6a6c9b3d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,12 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) +#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +714,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +872,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // !USE_ESP32 && !USE_HOST +#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 9a62ac16347..282f4c66ef8 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -287,7 +287,7 @@ class Scheduler { // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). // On non-ESP32, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) (void) now; return millis_64(); #else @@ -295,7 +295,7 @@ class Scheduler { #endif } -#if !defined(USE_ESP32) && !defined(USE_HOST) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) // On platforms without native 64-bit time, millis_64() HAL function delegates to this // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); @@ -567,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#if !defined(USE_ESP32) && !defined(USE_HOST) - // On platforms with native 64-bit time (ESP32, Host), no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) + // On platforms with native 64-bit time (ESP32, Host, Zephyr), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -598,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST */ +#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR */ }; } // namespace esphome From 52af4bced0fdbe0c89a21aa8bb826422467f1936 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 13:01:23 -0700 Subject: [PATCH 0277/2056] [component] Devirtualize call_dump_config (#14355) --- esphome/components/mqtt/mqtt_component.cpp | 6 ------ esphome/components/mqtt/mqtt_component.h | 2 -- esphome/core/application.cpp | 2 +- esphome/core/component.cpp | 2 +- esphome/core/component.h | 2 +- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 09570106df9..f49069960b3 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -405,12 +405,6 @@ void MQTTComponent::process_resend() { this->schedule_resend_state(); } } -void MQTTComponent::call_dump_config() { - if (this->is_internal()) - return; - - this->dump_config(); -} void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 853712940ad..0ffe6341d37 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -98,8 +98,6 @@ class MQTTComponent : public Component { /// Override setup_ so that we can call send_discovery() when needed. void call_setup() override; - void call_dump_config() override; - /// Send discovery info the Home Assistant, override this. virtual void send_discovery(JsonObject root, SendDiscoveryConfig &config) = 0; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e27c2cdfc6f..b1ece867017 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -256,7 +256,7 @@ void Application::process_dump_config_() { #endif } - this->components_[this->dump_config_at_]->call_dump_config(); + this->components_[this->dump_config_at_]->call_dump_config_(); this->dump_config_at_++; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b4a19a07765..e14fae5e08c 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -211,7 +211,7 @@ bool Component::cancel_retry(uint32_t id) { void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } -void Component::call_dump_config() { +void Component::call_dump_config_() { this->dump_config(); if (this->is_failed()) { // Look up error message from global vector diff --git a/esphome/core/component.h b/esphome/core/component.h index 7ea9fdf3b36..2620e8eb2aa 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -291,7 +291,7 @@ class Component { void call_loop_(); virtual void call_setup(); - virtual void call_dump_config(); + void call_dump_config_(); /// Helper to set component state (clears state bits and sets new state) void set_component_state_(uint8_t state); From f6755aabae2fad7ea5c082571502baf281c6d8b8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:18:07 -0500 Subject: [PATCH 0278/2056] [ci] Add PR title format check (#14345) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/auto-label-pr/detectors.js | 42 +++++------- .github/scripts/detect-tags.js | 66 +++++++++++++++++++ .github/workflows/pr-title-check.yml | 76 ++++++++++++++++++++++ 3 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 .github/scripts/detect-tags.js create mode 100644 .github/workflows/pr-title-check.yml diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index a45a84f2196..80d8847bc1f 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -1,5 +1,12 @@ const fs = require('fs'); const { DOCS_PR_PATTERNS } = require('./constants'); +const { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +} = require('../detect-tags'); // Strategy: Merge branch detection async function detectMergeBranch(context) { @@ -20,15 +27,13 @@ async function detectMergeBranch(context) { // Strategy: Component and platform labeling async function detectComponentPlatforms(changedFiles, apiData) { const labels = new Set(); - const componentRegex = /^esphome\/components\/([^\/]+)\//; const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); - for (const file of changedFiles) { - const componentMatch = file.match(componentRegex); - if (componentMatch) { - labels.add(`component: ${componentMatch[1]}`); - } + for (const comp of detectComponents(changedFiles)) { + labels.add(`component: ${comp}`); + } + for (const file of changedFiles) { const platformMatch = file.match(targetPlatformRegex); if (platformMatch) { labels.add(`platform: ${platformMatch[1]}`); @@ -90,15 +95,9 @@ async function detectNewPlatforms(prFiles, apiData) { // Strategy: Core files detection async function detectCoreChanges(changedFiles) { const labels = new Set(); - const coreFiles = changedFiles.filter(file => - file.startsWith('esphome/core/') || - (file.startsWith('esphome/') && file.split('/').length === 2) - ); - - if (coreFiles.length > 0) { + if (hasCoreChanges(changedFiles)) { labels.add('core'); } - return labels; } @@ -131,29 +130,18 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange // Strategy: Dashboard changes async function detectDashboardChanges(changedFiles) { const labels = new Set(); - const dashboardFiles = changedFiles.filter(file => - file.startsWith('esphome/dashboard/') || - file.startsWith('esphome/components/dashboard_import/') - ); - - if (dashboardFiles.length > 0) { + if (hasDashboardChanges(changedFiles)) { labels.add('dashboard'); } - return labels; } // Strategy: GitHub Actions changes async function detectGitHubActionsChanges(changedFiles) { const labels = new Set(); - const githubActionsFiles = changedFiles.filter(file => - file.startsWith('.github/workflows/') - ); - - if (githubActionsFiles.length > 0) { + if (hasGitHubActionsChanges(changedFiles)) { labels.add('github-actions'); } - return labels; } @@ -259,7 +247,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) { const { owner, repo } = context.repo; // Compile regex once for better performance - const componentFileRegex = /^esphome\/components\/([^\/]+)\//; + const componentFileRegex = COMPONENT_REGEX; // Get files that are modified or added in components directory const componentFiles = changedFiles.filter(file => componentFileRegex.test(file)); diff --git a/.github/scripts/detect-tags.js b/.github/scripts/detect-tags.js new file mode 100644 index 00000000000..3933776c616 --- /dev/null +++ b/.github/scripts/detect-tags.js @@ -0,0 +1,66 @@ +/** + * Shared tag detection from changed file paths. + * Used by pr-title-check and auto-label-pr workflows. + */ + +const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//; + +/** + * Detect component names from changed files. + * @param {string[]} changedFiles - List of changed file paths + * @returns {Set} Set of component names + */ +function detectComponents(changedFiles) { + const components = new Set(); + for (const file of changedFiles) { + const match = file.match(COMPONENT_REGEX); + if (match) { + components.add(match[1]); + } + } + return components; +} + +/** + * Detect if core files were changed. + * Core files are in esphome/core/ or top-level esphome/ directory. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasCoreChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); +} + +/** + * Detect if dashboard files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasDashboardChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); +} + +/** + * Detect if GitHub Actions files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasGitHubActionsChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('.github/workflows/') + ); +} + +module.exports = { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +}; diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 00000000000..f23c2c870ee --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,76 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + check: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, + } = require('./.github/scripts/detect-tags.js'); + + const title = context.payload.pull_request.title; + + // Block titles starting with "word:" or "word(scope):" patterns + const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; + if (commitStylePattern.test(title)) { + core.setFailed( + `PR title should not start with a "prefix:" style format.\n` + + `Please use the format: [component] Brief description\n` + + `Example: [pn532] Add health checking and auto-reset` + ); + return; + } + + // Get changed files to detect tags + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const filenames = files.map(f => f.filename); + + // Detect tags from changed files using shared logic + const tags = new Set(); + + for (const comp of detectComponents(filenames)) { + tags.add(comp); + } + if (hasCoreChanges(filenames)) tags.add('core'); + if (hasDashboardChanges(filenames)) tags.add('dashboard'); + if (hasGitHubActionsChanges(filenames)) tags.add('ci'); + + if (tags.size === 0) { + return; + } + + // Check title starts with [tag] prefix + const bracketPattern = /^\[\w+\]/; + if (!bracketPattern.test(title)) { + const suggestion = [...tags].map(c => `[${c}]`).join(''); + // Skip if the suggested prefix would be too long for a readable title + if (suggestion.length > 40) { + return; + } + core.setFailed( + `PR modifies: ${[...tags].join(', ')}\n` + + `Title must start with a [tag] prefix.\n` + + `Suggested: ${suggestion} ` + ); + } From 280f874edc36ce5127d005a526c5f4537d453f70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 14:18:02 -0700 Subject: [PATCH 0279/2056] [rp2040] Use native time_us_64() for millis_64() (#14356) Co-authored-by: Claude Opus 4.6 --- esphome/components/rp2040/core.cpp | 12 ++++++------ esphome/core/scheduler.cpp | 11 ++++++----- esphome/core/scheduler.h | 14 +++++++------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 52c6f1185c4..8b86de4be11 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,19 +3,19 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "hardware/timer.h" #include "hardware/watchdog.h" namespace esphome { void HOT yield() { ::yield(); } -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return time_us_64() / 1000ULL; } +uint32_t HOT millis() { return static_cast(millis_64()); } void HOT delay(uint32_t ms) { ::delay(ms); } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +uint32_t HOT micros() { return ::micros(); } +void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { watchdog_reboot(0, 0, 10); while (1) { @@ -34,7 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } -uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } +uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } } // namespace esphome diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 73b6a6c9b3d..2c10e7e2dad 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,7 +28,7 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; #endif @@ -475,12 +475,13 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \ + defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #else @@ -714,7 +715,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags @@ -872,7 +873,7 @@ uint64_t Scheduler::millis_64_impl_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } -#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR +#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 282f4c66ef8..d52cf5147d9 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,10 +284,10 @@ class Scheduler { bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. - // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). - // On non-ESP32, extends now to 64-bit using rollover tracking. + // On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64(). + // On other platforms, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) (void) now; return millis_64(); #else @@ -295,7 +295,7 @@ class Scheduler { #endif } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) // On platforms without native 64-bit time, millis_64() HAL function delegates to this // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. friend uint64_t millis_64(); @@ -567,8 +567,8 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) - // On platforms with native 64-bit time (ESP32, Host, Zephyr), no rollover tracking needed. +#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) + // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed. // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* @@ -598,7 +598,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR */ +#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */ }; } // namespace esphome From bb567827a1e2f67a362d0a385868e438df8b71aa Mon Sep 17 00:00:00 2001 From: Laura Wratten Date: Sat, 28 Feb 2026 08:23:32 +1100 Subject: [PATCH 0280/2056] [sht3xd] Allow sensors that don't support serial number read (#14224) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/sht3xd/sht3xd.cpp | 43 ++++++++-------------------- esphome/components/sht3xd/sht3xd.h | 9 ------ 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index bd3dec6fb85..8050a2d5f97 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -12,6 +12,8 @@ static const char *const TAG = "sht3xd"; // To ensure compatibility, reading serial number using the register with clock stretching register enabled // (used originally in this component) is tried first and if that fails the alternate register address // with clock stretching disabled is read. +// If both fail (e.g. some clones don't support the command), we continue so temp/humidity still work. +// Second attempt uses 10ms delay for boards that need more time before read (max permitted by ESPHome guidelines). static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING = 0x3780; static const uint16_t SHT3XD_COMMAND_READ_SERIAL_NUMBER = 0x3682; @@ -25,49 +27,28 @@ static const uint16_t SHT3XD_COMMAND_POLLING_H = 0x2400; static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { - uint16_t raw_serial_number[2]; + uint16_t raw_serial_number[2]{0}; if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING, raw_serial_number, 2)) { - this->error_code_ = READ_SERIAL_STRETCHED_FAILED; - if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { - this->error_code_ = READ_SERIAL_FAILED; - this->mark_failed(); - return; + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2, 10)) { + ESP_LOGW(TAG, "Serial number read failed, continuing without it (clone or non-standard sensor)"); } } - this->serial_number_ = (uint32_t(raw_serial_number[0]) << 16) | uint32_t(raw_serial_number[1]); - if (!this->write_command(heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) { - this->error_code_ = WRITE_HEATER_MODE_FAILED; - this->mark_failed(); + if (!this->write_command(this->heater_enabled_ ? SHT3XD_COMMAND_HEATER_ENABLE : SHT3XD_COMMAND_HEATER_DISABLE)) { + this->mark_failed(LOG_STR("Failed to set heater mode")); return; } } void SHT3XDComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHT3xD:"); - switch (this->error_code_) { - case READ_SERIAL_FAILED: - ESP_LOGD(TAG, " Error reading serial number"); - break; - case WRITE_HEATER_MODE_FAILED: - ESP_LOGD(TAG, " Error writing heater mode"); - break; - default: - break; - } - if (this->is_failed()) { - ESP_LOGE(TAG, " Communication with SHT3xD failed!"); - return; - } - ESP_LOGD(TAG, - " Serial Number: 0x%08" PRIX32 "\n" - " Heater Enabled: %s", - this->serial_number_, TRUEFALSE(this->heater_enabled_)); - + ESP_LOGCONFIG(TAG, + "SHT3xD:\n" + " Serial Number: 0x%08" PRIX32 "\n" + " Heater Enabled: %s", + this->serial_number_, TRUEFALSE(this->heater_enabled_)); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 43f1a4d8e24..54514d6de73 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -4,8 +4,6 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -#include - namespace esphome { namespace sht3xd { @@ -21,13 +19,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } protected: - enum ErrorCode { - NONE = 0, - READ_SERIAL_STRETCHED_FAILED, - READ_SERIAL_FAILED, - WRITE_HEATER_MODE_FAILED, - } error_code_{NONE}; - sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; bool heater_enabled_{true}; From 5e3857abf7dded7d731cdd5663b23ca0edb6491f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:25:36 -0500 Subject: [PATCH 0281/2056] Bump click from 8.1.7 to 8.3.1 (#11955) Signed-off-by: dependabot[bot] 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 95e3710f9ec..f111e05a9df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 -click==8.1.7 +click==8.3.1 esphome-dashboard==20260210.0 aioesphomeapi==44.2.0 zeroconf==0.148.0 From b9d70dcda2d5ff6bad34c6c72ae12f5c9b9cd6e6 Mon Sep 17 00:00:00 2001 From: Martin Ebner <185941678+mebner86@users.noreply.github.com> Date: Sat, 28 Feb 2026 03:11:28 +0530 Subject: [PATCH 0282/2056] [sen6x] Add SEN6x sensor support (#12553) Co-authored-by: Martin Ebner Co-authored-by: Tobias Stanzel Co-authored-by: Big Mike Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sen6x/__init__.py | 0 esphome/components/sen6x/sen6x.cpp | 376 +++++++++++++++++++ esphome/components/sen6x/sen6x.h | 43 +++ esphome/components/sen6x/sensor.py | 149 ++++++++ tests/components/sen6x/common.yaml | 37 ++ tests/components/sen6x/test.esp32-idf.yaml | 4 + tests/components/sen6x/test.esp8266-ard.yaml | 4 + tests/components/sen6x/test.rp2040-ard.yaml | 4 + 9 files changed, 618 insertions(+) create mode 100644 esphome/components/sen6x/__init__.py create mode 100644 esphome/components/sen6x/sen6x.cpp create mode 100644 esphome/components/sen6x/sen6x.h create mode 100644 esphome/components/sen6x/sensor.py create mode 100644 tests/components/sen6x/common.yaml create mode 100644 tests/components/sen6x/test.esp32-idf.yaml create mode 100644 tests/components/sen6x/test.esp8266-ard.yaml create mode 100644 tests/components/sen6x/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 6728e76bbad..4c97b7f99d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -429,6 +429,7 @@ esphome/components/select/* @esphome/core esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras +esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sfa30/* @ghsensdev diff --git a/esphome/components/sen6x/__init__.py b/esphome/components/sen6x/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp new file mode 100644 index 00000000000..baaadd64631 --- /dev/null +++ b/esphome/components/sen6x/sen6x.cpp @@ -0,0 +1,376 @@ +#include "sen6x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include +#include +#include + +namespace esphome::sen6x { + +static const char *const TAG = "sen6x"; + +static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202; +static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100; +static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014; +static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033; + +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only! +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467; +static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5; + +static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021; +static constexpr uint16_t SEN6X_CMD_RESET = 0xD304; + +static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) { + read_cmd = SEN6X_CMD_READ_MEASUREMENT; + read_words = 9; + switch (type) { + case SEN6XComponent::SEN62: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62; + read_words = 6; + break; + case SEN6XComponent::SEN63C: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C; + read_words = 7; + break; + case SEN6XComponent::SEN65: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65; + read_words = 8; + break; + case SEN6XComponent::SEN66: + read_cmd = SEN6X_CMD_READ_MEASUREMENT; + read_words = 9; + break; + case SEN6XComponent::SEN68: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68; + read_words = 9; + break; + case SEN6XComponent::SEN69C: + read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C; + read_words = 10; + break; + default: + break; + } +} + +void SEN6XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sen6x..."); + + // the sensor needs 100 ms to enter the idle state + this->set_timeout(100, [this]() { + // Reset the sensor to ensure a clean state regardless of prior commands or power issues + if (!this->write_command(SEN6X_CMD_RESET)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // After reset the sensor needs 100 ms to become ready + this->set_timeout(100, [this]() { + // Step 1: Read serial number (~25ms with I2C delay) + uint16_t raw_serial_number[16]; + if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + this->serial_number_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16); + ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str()); + + // Step 2: Read product name in next loop iteration + this->set_timeout(0, [this]() { + uint16_t raw_product_name[16]; + if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + this->product_name_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16); + + Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_); + if (this->sen6x_type_ == UNKNOWN) { + this->sen6x_type_ = inferred_type; + if (inferred_type == UNKNOWN) { + ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str()); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str()); + } else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) { + ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str()); + } + ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str()); + + // Validate configured sensors against detected type and disable unsupported ones + const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 || + this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C); + const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C); + const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C); + if (this->voc_sensor_ && !has_voc_nox) { + ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C"); + this->voc_sensor_ = nullptr; + } + if (this->nox_sensor_ && !has_voc_nox) { + ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C"); + this->nox_sensor_ = nullptr; + } + if (this->co2_sensor_ && !has_co2) { + ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C"); + this->co2_sensor_ = nullptr; + } + if (this->hcho_sensor_ && !has_hcho) { + ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C"); + this->hcho_sensor_ = nullptr; + } + + // Step 3: Read firmware version and start measurements in next loop iteration + this->set_timeout(0, [this]() { + uint16_t raw_firmware_version = 0; + if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF; + this->firmware_version_minor_ = raw_firmware_version & 0xFF; + ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_); + + if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + this->set_timeout(60000, [this]() { this->startup_complete_ = true; }); + this->initialized_ = true; + ESP_LOGD(TAG, "Initialized"); + }); + }); + }); + }); +} + +void SEN6XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "sen6x:\n" + " Product: %s\n" + " Serial: %s\n" + " Firmware: %u.%u\n" + " Address: 0x%02X", + this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_, + this->firmware_version_minor_, this->address_); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "VOC", this->voc_sensor_); + LOG_SENSOR(" ", "NOx", this->nox_sensor_); + LOG_SENSOR(" ", "HCHO", this->hcho_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); +} + +void SEN6XComponent::update() { + if (!this->initialized_) { + return; + } + + uint16_t read_cmd; + uint8_t read_words; + set_read_command_and_words(this->sen6x_type_, read_cmd, read_words); + + const uint8_t poll_retries = 24; + auto poll_ready = std::make_shared>(); + *poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) { + const uint8_t attempt = static_cast(poll_retries - retries_left + 1); + ESP_LOGV(TAG, "Data ready polling attempt %u", attempt); + + if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + return; + } + + this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() { + uint16_t raw_read_status; + if (!this->read_data(&raw_read_status, 1)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); + return; + } + + if ((raw_read_status & 0x0001) == 0) { + if (retries_left == 0) { + this->status_set_warning(); + ESP_LOGD(TAG, "Data not ready"); + return; + } + this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); }); + return; + } + + if (!this->write_command(read_cmd)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); + return; + } + + this->set_timeout(20, [this, read_words]() { + uint16_t measurements[10]; + + if (!this->read_data(measurements, read_words)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); + return; + } + int8_t voc_index = -1; + int8_t nox_index = -1; + int8_t hcho_index = -1; + int8_t co2_index = -1; + bool co2_uint16 = false; + switch (this->sen6x_type_) { + case SEN62: + break; + case SEN63C: + co2_index = 6; + break; + case SEN65: + voc_index = 6; + nox_index = 7; + break; + case SEN66: + voc_index = 6; + nox_index = 7; + co2_index = 8; + co2_uint16 = true; + break; + case SEN68: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + break; + case SEN69C: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + co2_index = 9; + break; + default: + break; + } + + float pm_1_0 = measurements[0] / 10.0f; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0f; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0f; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0f; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = static_cast(measurements[4]) / 100.0f; + if (measurements[4] == 0x7FFF) + humidity = NAN; + float temperature = static_cast(measurements[5]) / 200.0f; + if (measurements[5] == 0x7FFF) + temperature = NAN; + + float voc = NAN; + float nox = NAN; + float hcho = NAN; + float co2 = NAN; + + if (voc_index >= 0) { + voc = static_cast(measurements[voc_index]) / 10.0f; + if (measurements[voc_index] == 0x7FFF) + voc = NAN; + } + if (nox_index >= 0) { + nox = static_cast(measurements[nox_index]) / 10.0f; + if (measurements[nox_index] == 0x7FFF) + nox = NAN; + } + + if (hcho_index >= 0) { + const uint16_t hcho_raw = measurements[hcho_index]; + hcho = hcho_raw / 10.0f; + if (hcho_raw == 0xFFFF) + hcho = NAN; + } + + if (co2_index >= 0) { + if (co2_uint16) { + const uint16_t co2_raw = measurements[co2_index]; + co2 = static_cast(co2_raw); + if (co2_raw == 0xFFFF) + co2 = NAN; + } else { + const int16_t co2_raw = static_cast(measurements[co2_index]); + co2 = static_cast(co2_raw); + if (co2_raw == 0x7FFF) + co2 = NAN; + } + } + + if (!this->startup_complete_) { + ESP_LOGD(TAG, "Startup delay, ignoring values"); + this->status_clear_warning(); + return; + } + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + if (this->hcho_sensor_ != nullptr) + this->hcho_sensor_->publish_state(hcho); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + + this->status_clear_warning(); + }); + }); + }; + + (*poll_ready)(poll_retries); +} + +SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) { + if (product_name == "SEN62") + return SEN62; + if (product_name == "SEN63C") + return SEN63C; + if (product_name == "SEN65") + return SEN65; + if (product_name == "SEN66") + return SEN66; + if (product_name == "SEN68") + return SEN68; + if (product_name == "SEN69C") + return SEN69C; + return UNKNOWN; +} + +} // namespace esphome::sen6x diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h new file mode 100644 index 00000000000..01e89dce1bc --- /dev/null +++ b/esphome/components/sen6x/sen6x.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" + +namespace esphome::sen6x { + +class SEN6XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { + SUB_SENSOR(pm_1_0) + SUB_SENSOR(pm_2_5) + SUB_SENSOR(pm_4_0) + SUB_SENSOR(pm_10_0) + SUB_SENSOR(temperature) + SUB_SENSOR(humidity) + SUB_SENSOR(voc) + SUB_SENSOR(nox) + SUB_SENSOR(co2) + SUB_SENSOR(hcho) + + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN }; + + void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); } + + protected: + Sen6xType infer_type_from_product_name_(const std::string &product_name); + + bool initialized_{false}; + std::string product_name_; + Sen6xType sen6x_type_{UNKNOWN}; + std::string serial_number_; + uint8_t firmware_version_major_{0}; + uint8_t firmware_version_minor_{0}; + bool startup_complete_{false}; +}; + +} // namespace esphome::sen6x diff --git a/esphome/components/sen6x/sensor.py b/esphome/components/sen6x/sensor.py new file mode 100644 index 00000000000..071478e719a --- /dev/null +++ b/esphome/components/sen6x/sensor.py @@ -0,0 +1,149 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensirion_common, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_CO2, + CONF_FORMALDEHYDE, + CONF_HUMIDITY, + CONF_ID, + CONF_NOX, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_4_0, + CONF_PM_10_0, + CONF_TEMPERATURE, + CONF_TYPE, + CONF_VOC, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_TEMPERATURE, + ICON_CHEMICAL_WEAPON, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, +) + +CODEOWNERS = ["@martgras", "@mebner86", "@mikelawrence", "@tuct"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] + +sen6x_ns = cg.esphome_ns.namespace("sen6x") +SEN6XComponent = sen6x_ns.class_( + "SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SEN6XComponent), + cv.Optional(CONF_TYPE): cv.one_of( + "SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", upper=True + ), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOC): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_NOX): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( + unit_of_measurement="ppb", + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x6B)) +) + +SENSOR_MAP = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_4_0: "set_pm_4_0_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_VOC: "set_voc_sensor", + CONF_NOX: "set_nox_sensor", + CONF_CO2: "set_co2_sensor", + CONF_FORMALDEHYDE: "set_hcho_sensor", +} + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TYPE in config: + cg.add(var.set_type(config[CONF_TYPE])) + + for key, func_name in SENSOR_MAP.items(): + if cfg := config.get(key): + sens = await sensor.new_sensor(cfg) + cg.add(getattr(var, func_name)(sens)) diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml new file mode 100644 index 00000000000..fdb8a485e29 --- /dev/null +++ b/tests/components/sen6x/common.yaml @@ -0,0 +1,37 @@ +sensor: + # Test with explicit type parameter + - platform: sen6x + id: sen6x_sensor + type: SEN69C + i2c_id: i2c_bus + temperature: + name: Temperature + accuracy_decimals: 1 + humidity: + name: Humidity + accuracy_decimals: 0 + pm_1_0: + name: PM <1µm Weight concentration + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: PM <2.5µm Weight concentration + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: PM <4µm Weight concentration + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: PM <10µm Weight concentration + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: NOx + voc: + name: VOC + co2: + name: Carbon Dioxide + formaldehyde: + name: Formaldehyde + address: 0x6B diff --git a/tests/components/sen6x/test.esp32-idf.yaml b/tests/components/sen6x/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/sen6x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/sen6x/test.esp8266-ard.yaml b/tests/components/sen6x/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/sen6x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/sen6x/test.rp2040-ard.yaml b/tests/components/sen6x/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/sen6x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 5c56b99742a12cfde9cbeb0f91641db96cd132d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 16:19:11 -0700 Subject: [PATCH 0283/2056] [ci] Fix C++ unit tests missing time component dependency (#14364) --- script/cpp_unit_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 78b65092ae4..02b133060aa 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -98,8 +98,12 @@ def run_tests(selected_components: list[str]) -> int: components = sorted(components) - # Obtain possible dependencies for the requested components: - components_with_dependencies = sorted(get_all_dependencies(set(components))) + # Obtain possible dependencies for the requested components. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies = sorted( + get_all_dependencies(set(components) | {"time"}) + ) # Build a list of include folders, one folder per component containing tests. # A special replacement main.cpp is located in /tests/components/main.cpp From 298ee7b92e4d0a1ad17fcd3995a689deadcb890f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:08:42 -0500 Subject: [PATCH 0284/2056] [gps] Fix codegen deadlock when automations reference sibling sensors (#14365) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/gps/__init__.py | 44 ++++++++++++------------------ tests/components/gps/common.yaml | 7 +++++ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 2135189bd53..045a5a6c847 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -98,33 +98,25 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if latitude_config := config.get(CONF_LATITUDE): - sens = await sensor.new_sensor(latitude_config) - cg.add(var.set_latitude_sensor(sens)) + # Pre-create all sensor variables so automations that reference + # sibling sensors don't deadlock waiting for unregistered IDs. + sensors = [ + (cg.new_Pvariable(conf[CONF_ID]), conf, setter) + for key, setter in ( + (CONF_LATITUDE, "set_latitude_sensor"), + (CONF_LONGITUDE, "set_longitude_sensor"), + (CONF_SPEED, "set_speed_sensor"), + (CONF_COURSE, "set_course_sensor"), + (CONF_ALTITUDE, "set_altitude_sensor"), + (CONF_SATELLITES, "set_satellites_sensor"), + (CONF_HDOP, "set_hdop_sensor"), + ) + if (conf := config.get(key)) + ] - if longitude_config := config.get(CONF_LONGITUDE): - sens = await sensor.new_sensor(longitude_config) - cg.add(var.set_longitude_sensor(sens)) - - if speed_config := config.get(CONF_SPEED): - sens = await sensor.new_sensor(speed_config) - cg.add(var.set_speed_sensor(sens)) - - if course_config := config.get(CONF_COURSE): - sens = await sensor.new_sensor(course_config) - cg.add(var.set_course_sensor(sens)) - - if altitude_config := config.get(CONF_ALTITUDE): - sens = await sensor.new_sensor(altitude_config) - cg.add(var.set_altitude_sensor(sens)) - - if satellites_config := config.get(CONF_SATELLITES): - sens = await sensor.new_sensor(satellites_config) - cg.add(var.set_satellites_sensor(sens)) - - if hdop_config := config.get(CONF_HDOP): - sens = await sensor.new_sensor(hdop_config) - cg.add(var.set_hdop_sensor(sens)) + for sens, conf, setter in sensors: + await sensor.register_sensor(sens, conf) + cg.add(getattr(var, setter)(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus # Using fork of TinyGPSPlus patched to build on ESP-IDF diff --git a/tests/components/gps/common.yaml b/tests/components/gps/common.yaml index a99e3ef7e0d..568f2cc7c78 100644 --- a/tests/components/gps/common.yaml +++ b/tests/components/gps/common.yaml @@ -1,8 +1,15 @@ gps: latitude: name: "Latitude" + id: gps_lat + on_value: + then: + - logger.log: + format: "%.6f, %.6f" + args: [id(gps_lat).state, id(gps_long).state] longitude: name: "Longitude" + id: gps_long altitude: name: "Altitude" speed: From d1b481319773a38d4a24cae0a4e7c86ea0ab1908 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner <8441200+rwagoner@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:20:13 -0500 Subject: [PATCH 0285/2056] [web_server] Add climate preset, fan mode, and humidity support (#14061) --- esphome/components/web_server/web_server.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 16674321c9d..4824e33dcd1 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1490,6 +1490,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode); parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode); parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); + parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset); // Parse temperature parameters // static_cast needed to disambiguate overloaded setters (float vs optional) @@ -1530,7 +1531,7 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json JsonArray opt = root[ESPHOME_F("modes")].to(); for (climate::ClimateMode m : traits.get_supported_modes()) opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); - if (!traits.get_supported_custom_fan_modes().empty()) { + if (traits.get_supports_fan_modes()) { JsonArray opt = root[ESPHOME_F("fan_modes")].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); @@ -1546,12 +1547,12 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json for (auto swing_mode : traits.get_supported_swing_modes()) opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } - if (traits.get_supports_presets() && obj->preset.has_value()) { + if (traits.get_supports_presets()) { JsonArray opt = root[ESPHOME_F("presets")].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } - if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { + if (!traits.get_supported_custom_presets().empty()) { JsonArray opt = root[ESPHOME_F("custom_presets")].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); @@ -1592,6 +1593,11 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json ? "NA" : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf); } + if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { + root[ESPHOME_F("current_humidity")] = std::isnan(obj->current_humidity) + ? "NA" + : (value_accuracy_to_buf(temp_buf, obj->current_humidity, 0), temp_buf); + } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { root[ESPHOME_F("target_temperature_low")] = From e7d4f2608b20d4a7f1c2ea0bd99c8c25cd04e6a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 16:01:17 -1000 Subject: [PATCH 0286/2056] [sen6x] Fix test sensor ID collisions with sen5x (#14367) --- tests/components/sen6x/common.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml index fdb8a485e29..61ff9f1e0c7 100644 --- a/tests/components/sen6x/common.yaml +++ b/tests/components/sen6x/common.yaml @@ -12,19 +12,19 @@ sensor: accuracy_decimals: 0 pm_1_0: name: PM <1µm Weight concentration - id: pm_1_0 + id: sen6x_pm_1_0 accuracy_decimals: 1 pm_2_5: name: PM <2.5µm Weight concentration - id: pm_2_5 + id: sen6x_pm_2_5 accuracy_decimals: 1 pm_4_0: name: PM <4µm Weight concentration - id: pm_4_0 + id: sen6x_pm_4_0 accuracy_decimals: 1 pm_10_0: name: PM <10µm Weight concentration - id: pm_10_0 + id: sen6x_pm_10_0 accuracy_decimals: 1 nox: name: NOx From 8480e8df9f0b834ebeb2694b30e15eede8b716a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 17:27:51 -1000 Subject: [PATCH 0287/2056] [uart] Revert UART0 default pin workarounds (fixed in ESP-IDF 5.5.2) (#14363) --- .../uart/uart_component_esp_idf.cpp | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index ea7a09fee60..8699d37d7ad 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,7 +9,6 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" -#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -19,13 +18,6 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; -/// Check if a pin number matches one of the default UART0 GPIO pins. -/// These pins may have residual state from the boot console that requires -/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). -static constexpr bool is_default_uart0_pin(int8_t pin_num) { - return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; -} - uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -149,34 +141,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - - // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 - // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks - // UART on default UART0 pins that may have residual state from boot console. - // Reset these pins before configuring UART to ensure they're in a clean state. - if (is_default_uart0_pin(tx)) { - gpio_reset_pin(static_cast(tx)); - } - if (is_default_uart0_pin(rx)) { - gpio_reset_pin(static_cast(rx)); - } - - // Setup pins after reset to configure GPIO direction and pull resistors. - // For UART0 default pins, setup() must always be called because gpio_reset_pin() - // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), - // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for - // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). - // For other pins, only call setup() if pull or open-drain flags are set to avoid - // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; @@ -186,6 +156,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 49cc389bf0e0e4853b4bbc5de38f89b6224ee28f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 17:28:05 -1000 Subject: [PATCH 0288/2056] [esp32] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~11 KB flash) (#14362) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 10 +++ esphome/components/esp32/printf_stubs.cpp | 85 ++++++++++++++++++++++ tests/components/esp32/test.esp32-idf.yaml | 1 + 3 files changed, 96 insertions(+) create mode 100644 esphome/components/esp32/printf_stubs.cpp diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 998913ecec5..dd9e394fd2a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -952,6 +952,7 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" @@ -1126,6 +1127,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] ): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, cv.Optional( @@ -1469,6 +1471,14 @@ async def to_code(config): "_ZSt25__throw_bad_function_callv", ]: cg.add_build_flag(f"-Wl,--wrap={mangled}") + + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~11 KB). See printf_stubs.cpp for implementation. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp new file mode 100644 index 00000000000..c6f03bc363d --- /dev/null +++ b/esphome/components/esp32/printf_stubs.cpp @@ -0,0 +1,85 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference + * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r + * (~11 KB). This is a separate implementation from _svfprintf_r (used by + * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and + * locking. + * + * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), + * so the SDK's vprintf() path is dead code at runtime. The fprintf() + * and printf() calls in SDK components are only in debug/assert paths + * (gpio_dump_io_configuration, ringbuf diagnostics) that are either + * GC'd or never called. Crash backtraces and panic output are + * unaffected — they use esp_rom_printf() which is a ROM function + * and does not go through libc. + * + * These stubs redirect through vsnprintf() (which uses _svfprintf_r + * already in the binary) and fwrite(), allowing the linker to + * dead-code eliminate _vfprintf_r. + * + * Saves ~11 KB of flash. + * + * To disable these wraps, set enable_full_printf: true in the esp32 + * advanced config section. + */ + +#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) +#include +#include + +#include "esp_system.h" + +namespace esphome::esp32 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome replaces the +// ESP-IDF log handler, and the SDK's printf/fprintf calls only exist in +// debug/assert paths that are never reached in normal operation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + esp_system_abort("printf buffer overflow; set enable_full_printf: true in esp32 framework advanced config"); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP_IDF && !USE_FULL_PRINTF diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index f80c854de55..da85aa3b0f2 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -10,6 +10,7 @@ esp32: use_full_certificate_bundle: false # Test CMN bundle (default) include_builtin_idf_components: - freertos # Test escape hatch (freertos is always included anyway) + enable_full_printf: false disable_debug_stubs: true disable_ocd_aware: true disable_usb_serial_jtag_secondary: true From 6c0998f220fcd2d6ba1a3a12a6ebcfeef1166ace Mon Sep 17 00:00:00 2001 From: Raymond Richmond Date: Sat, 28 Feb 2026 00:26:06 -0700 Subject: [PATCH 0289/2056] [gt911] Support for interrupt signal via IO Expander (#14358) --- .../components/gt911/touchscreen/__init__.py | 2 +- .../gt911/touchscreen/gt911_touchscreen.cpp | 29 ++++++++++++++----- .../gt911/touchscreen/gt911_touchscreen.h | 6 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 6c80ff280f3..b850eeea8bc 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -16,7 +16,7 @@ GT911Touchscreen = gt911_ns.class_( CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ).extend(i2c.i2c_device_schema(0x5D)) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index b11880a0421..17bfa82cb45 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -2,6 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/gpio.h" namespace esphome { namespace gt911 { @@ -26,15 +27,17 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned void GT911Touchscreen::setup() { if (this->reset_pin_ != nullptr) { + // temporarily set the interrupt pin to output to control address selection this->reset_pin_->setup(); this->reset_pin_->digital_write(false); if (this->interrupt_pin_ != nullptr) { - // temporarily set the interrupt pin to output to control address selection + this->interrupt_pin_->setup(); this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->reset_pin_->digital_write(true); + // wait at least T3+T4 ms as per the datasheet this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); return; } @@ -43,11 +46,10 @@ void GT911Touchscreen::setup() { void GT911Touchscreen::setup_internal_() { if (this->interrupt_pin_ != nullptr) { - // set pre-configured input mode - this->interrupt_pin_->setup(); + if (this->interrupt_pin_->is_internal()) + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); } - // check the configuration of the int line. uint8_t data[4]; i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { @@ -58,12 +60,25 @@ void GT911Touchscreen::setup_internal_() { err = this->read(data, 1); if (err == i2c::ERROR_OK) { ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]); + + // data[0] & 1 == 1 => controller uses falling edge => active-low + // data[0] & 1 == 0 => controller uses rising edge => active-high + bool active_high = !(data[0] & 1); + if (this->interrupt_pin_ != nullptr) { - this->attach_interrupt_(this->interrupt_pin_, - (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + if (this->interrupt_pin_->is_internal()) { + // Direct MCU pin: attach a hardware interrupt, no polling needed. + this->attach_interrupt_(static_cast(this->interrupt_pin_), + active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE); + ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW"); + } else { + // IO expander pin: leave as output for configuration only. + ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW"); + } } } } + if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { // no calibration? Attempt to read the max values from the touchscreen. if (err == i2c::ERROR_OK) { diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 85025b5522b..a6577b5879d 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -30,7 +30,9 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void dump_config() override; bool can_proceed() override { return this->setup_done_; } - void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + /// Set a interrupt pin (supports hardware interrupts or expander connected). + void set_interrupt_pin(GPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } @@ -49,7 +51,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice /// @brief True if the touchscreen setup has completed successfully. bool setup_done_{false}; - InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *interrupt_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. From 089d1e55e729c25755ab76196917db1f090ae5f2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:37:04 +1100 Subject: [PATCH 0290/2056] [mipi_dsi] Fix Waveshare P4 7B board config (#14372) --- esphome/components/mipi_dsi/models/waveshare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index 69414065f13..c97a0bbd021 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -103,8 +103,6 @@ DriverChip( (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00), (0xEF, 0xFF, 0xFF, 0x01), - (0x11, 0x00), - (0x29, 0x00), ], ) @@ -125,6 +123,7 @@ DriverChip( lane_bit_rate="900Mbps", no_transform=True, color_order="RGB", + reset_pin=33, initsequence=[ (0x80, 0x8B), (0x81, 0x78), From 067d773aac5e1ca7911e2260b222b10b9e08444f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:19:55 -1000 Subject: [PATCH 0291/2056] [core] Make register_component protected, remove runtime checks (#14371) --- esphome/core/application.cpp | 19 +------------------ esphome/core/application.h | 13 ++++++------- esphome/cpp_helpers.py | 2 +- .../deep_sleep/test_deep_sleep.py | 2 +- .../ota/test_web_server_ota.py | 2 +- tests/dummy_main.cpp | 4 ++-- tests/unit_tests/test_cpp_helpers.py | 8 ++++---- 7 files changed, 16 insertions(+), 34 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b1ece867017..f963afa597c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -79,24 +79,7 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) { } } -void Application::register_component_(Component *comp) { - if (comp == nullptr) { - ESP_LOGW(TAG, "Tried to register null component!"); - return; - } - - for (auto *c : this->components_) { - if (comp == c) { - ESP_LOGW(TAG, "Component %s already registered! (%p)", LOG_STR_ARG(c->get_component_log_str()), c); - return; - } - } - if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { - ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); - return; - } - this->components_.push_back(comp); -} +void Application::register_component_(Component *comp) { this->components_.push_back(comp); } void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 0cc29af8e7f..ba3ce13291e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -108,6 +108,10 @@ namespace esphome::socket { class Socket; } // namespace esphome::socket +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Teardown timeout constant (in milliseconds) @@ -247,13 +251,6 @@ class Application { /// Reserve space for components to avoid memory fragmentation - /// Register the component in this Application instance. - template C *register_component(C *c) { - static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); - this->register_component_((Component *) c); - return c; - } - /// Set up all the registered components. Call this at the end of your setup() function. void setup(); @@ -508,6 +505,8 @@ class Application { protected: friend Component; friend class socket::Socket; + friend void ::setup(); + friend void ::original_setup(); #ifdef USE_SOCKET_SELECT_SUPPORT /// Fast path for Socket::ready() via friendship - skips negative fd check. diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 954a28d3d1d..b673eaa7e1f 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -79,7 +79,7 @@ async def register_component(var, config): if name is not None: add(var.set_component_source(LogStringLiteral(name))) - add(App.register_component(var)) + add(App.register_component_(var)) return var diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 11f1bcb58e3..41ddd72febd 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main): main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp - assert "App.register_component(deepsleep);" in main_cpp + assert "App.register_component_(deepsleep);" in main_cpp def test_deep_sleep_sleep_duration(generate_main): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 794eaac9be8..4b3a4c705c1 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -27,7 +27,7 @@ def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: assert "global_web_server_base" in main_cpp # Check component is registered - assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp + assert "App.register_component_(web_server_webserverotacomponent_id)" in main_cpp def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None: diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 52f1fbd319c..3ccf35e04d2 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -16,10 +16,10 @@ void setup() { auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); - App.register_component(log); + App.register_component_(log); auto *wifi = new wifi::WiFiComponent(); // NOLINT - App.register_component(wifi); + App.register_component_(wifi); wifi::WiFiAP ap; ap.set_ssid("Test SSID"); ap.set_password("password1"); diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 2618803fecf..82ded409c7c 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -16,7 +16,7 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): async def test_register_component(monkeypatch): var = Mock(base="foo.bar") - app_mock = Mock(register_component=Mock(return_value=var)) + app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) core_mock = Mock(component_ids=["foo.bar"]) @@ -29,7 +29,7 @@ async def test_register_component(monkeypatch): assert actual is var assert add_mock.call_count == 2 - app_mock.register_component.assert_called_with(var) + app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] @@ -48,7 +48,7 @@ async def test_register_component__no_component_id(monkeypatch): async def test_register_component__with_setup_priority(monkeypatch): var = Mock(base="foo.bar") - app_mock = Mock(register_component=Mock(return_value=var)) + app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) core_mock = Mock(component_ids=["foo.bar"]) @@ -68,5 +68,5 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() assert add_mock.call_count == 4 - app_mock.register_component.assert_called_with(var) + app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] From 7d52a9587f59c01a47c72bacb8c3d7e9f4d40e30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:20:20 -1000 Subject: [PATCH 0292/2056] [api] Outline keepalive ping logic from APIConnection::loop() (#14374) --- esphome/components/api/api_connection.cpp | 46 ++++++++++++++--------- esphome/components/api/api_connection.h | 4 ++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bc9c45c057..8b2efdde518 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -242,24 +242,11 @@ void APIConnection::loop() { return; } - if (this->flags_.sent_ping) { - // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { - on_fatal_error(); - this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); - } - } 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"); - PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); - if (!this->flags_.sent_ping) { - // If we can't send the ping request directly (tx_buffer full), - // schedule it at the front of the batch so it will be sent with priority - ESP_LOGW(TAG, "Buffer full, ping queued"); - this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); - this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings - } + // Keepalive: only call into the cold path when enough time has elapsed. + // When sent_ping is true, last_traffic_ hasn't been updated so this + // condition is already satisfied — covers both send-ping and disconnect cases. + if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + this->check_keepalive_(now); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -275,6 +262,29 @@ void APIConnection::loop() { #endif } +void APIConnection::check_keepalive_(uint32_t now) { + // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS + if (this->flags_.sent_ping) { + // Disconnect if not responded within 2.5*keepalive + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { + on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); + } + } else if (!this->flags_.remove) { + // Only send ping if we're not disconnecting + ESP_LOGVV(TAG, "Sending keepalive PING"); + PingRequest req; + this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + if (!this->flags_.sent_ping) { + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings + } + } +} + void APIConnection::process_active_iterator_() { // Caller ensures active_iterator_ != NONE if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e34bed8ada0..37855b2482a 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -370,6 +370,10 @@ class APIConnection final : public APIServerConnectionBase { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } + // Send keepalive ping or disconnect unresponsive client. + // Cold path — extracted from loop() to reduce instruction cache pressure. + void __attribute__((noinline)) check_keepalive_(uint32_t now); + // Process active iterator (list_entities/initial_state) during connection setup. // Extracted from loop() — only runs during initial handshake, NONE in steady state. void __attribute__((noinline)) process_active_iterator_(); From 757e8d90e6a32fb77cad14efb9e00e34e584fee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 07:20:34 -1000 Subject: [PATCH 0293/2056] [core] Inline set_component_state_ and use it in Application (#14369) --- esphome/core/application.cpp | 3 +-- esphome/core/component.cpp | 4 ---- esphome/core/component.h | 5 ++++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f963afa597c..c977fd66b3a 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -508,8 +508,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; ESP_LOGVV(TAG, "%s loop enabled from ISR", LOG_STR_ARG(component->get_component_log_str())); - component->component_state_ &= ~COMPONENT_STATE_MASK; - component->component_state_ |= COMPONENT_STATE_LOOP; + component->set_component_state_(COMPONENT_STATE_LOOP); // Move to active section this->activate_looping_component_(i); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e14fae5e08c..1fd621ea83a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -297,10 +297,6 @@ void Component::mark_failed() { // Also remove from loop since failed components shouldn't loop App.disable_component_loop_(this); } -void Component::set_component_state_(uint8_t state) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= state; -} void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str())); diff --git a/esphome/core/component.h b/esphome/core/component.h index 2620e8eb2aa..0b0b6345c74 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,10 @@ class Component { void call_dump_config_(); /// Helper to set component state (clears state bits and sets new state) - void set_component_state_(uint8_t state); + inline void set_component_state_(uint8_t state) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= state; + } /** Set an interval function with a unique name. Empty name means no cancelling possible. * From b7d651dd179ad165e9921bd9c7840de58da3987a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:10:53 -0500 Subject: [PATCH 0294/2056] [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/binary_sensor/__init__.py | 37 +++++++++++--------- esphome/components/number/__init__.py | 31 +++++++++------- esphome/components/sensor/__init__.py | 37 +++++++++++--------- esphome/components/switch/__init__.py | 16 ++++++--- esphome/components/text_sensor/__init__.py | 19 ++++++---- 5 files changed, 83 insertions(+), 57 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4500168cb1d..036d78da736 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -550,22 +550,8 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( - CONF_PUBLISH_INITIAL_STATE, False - ) - cg.add(var.set_trigger_on_initial_state(trigger)) - if inverted := config.get(CONF_INVERTED): - cg.add(var.set_inverted(inverted)) - if filters_config := config.get(CONF_FILTERS): - cg.add_define("USE_BINARY_SENSOR_FILTER") - filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) - cg.add(var.add_filters(filters)) - +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_binary_sensor_automations(var, config): for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -617,6 +603,25 @@ async def setup_binary_sensor_core_(var, config): conf, ) + +async def setup_binary_sensor_core_(var, config): + await setup_entity(var, config, "binary_sensor") + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) + cg.add(var.add_filters(filters)) + + CORE.add_job(_build_binary_sensor_automations, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index b23da7799f1..d12ec7463b5 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -240,6 +240,23 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_number_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): @@ -254,19 +271,7 @@ async def setup_number_core_( if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: cg.add(var.traits.set_mode(config[CONF_MODE])) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_number_automations, var, config) if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 770c044efb9..338aaae0b53 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -888,6 +888,26 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if (above := conf.get(CONF_ABOVE)) is not None: + template_ = await cg.templatable(above, [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if (below := conf.get(CONF_BELOW)) is not None: + template_ = await cg.templatable(below, [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_sensor_core_(var, config): await setup_entity(var, config, "sensor") @@ -907,22 +927,7 @@ async def setup_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if (above := conf.get(CONF_ABOVE)) is not None: - template_ = await cg.templatable(above, [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if (below := conf.get(CONF_BELOW)) is not None: - template_ = await cg.templatable(below, [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 7424d7c92fe..cfc5e2b6e82 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -141,11 +141,8 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - - if (inverted := config.get(CONF_INVERTED)) is not None: - cg.add(var.set_inverted(inverted)) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_switch_automations(var, config): for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) @@ -156,6 +153,15 @@ async def setup_switch_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + +async def setup_switch_core_(var, config): + await setup_entity(var, config, "switch") + + if (inverted := config.get(CONF_INVERTED)) is not None: + cg.add(var.set_inverted(inverted)) + + CORE.add_job(_build_switch_automations, var, config) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 2e8edb43c90..2edf202cd23 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -197,6 +197,17 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_text_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + async def setup_text_sensor_core_(var, config): await setup_entity(var, config, "text_sensor") @@ -208,13 +219,7 @@ async def setup_text_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + CORE.add_job(_build_text_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) From b679b04d140663770f4a5c4be16e1c9ed93de5c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 13:27:33 -1000 Subject: [PATCH 0295/2056] [core] Move CONF_STOP_BITS, CONF_DATA_BITS, CONF_PARITY to const.py (#14379) --- esphome/components/uart/__init__.py | 6 +++--- esphome/components/usb_uart/__init__.py | 10 ++++------ esphome/components/weikai/__init__.py | 6 +++--- esphome/const.py | 3 +++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 31e37a06e02..a6f4cf5d5fa 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BYTES, CONF_DATA, + CONF_DATA_BITS, CONF_DEBUG, CONF_DELIMITER, CONF_DIRECTION, @@ -21,10 +22,12 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_NUMBER, + CONF_PARITY, CONF_PORT, CONF_RX_BUFFER_SIZE, CONF_RX_PIN, CONF_SEQUENCE, + CONF_STOP_BITS, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TX_PIN, @@ -215,9 +218,6 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } -CONF_STOP_BITS = "stop_bits" -CONF_DATA_BITS = "data_bits" -CONF_PARITY = "parity" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index d9bb58ae3ab..cc69c0cb5a7 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,20 +1,18 @@ import esphome.codegen as cg from esphome.components import socket -from esphome.components.uart import ( - CONF_DATA_BITS, - CONF_PARITY, - CONF_STOP_BITS, - UARTComponent, -) +from esphome.components.uart import UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_BUFFER_SIZE, CONF_CHANNELS, + CONF_DATA_BITS, CONF_DEBUG, CONF_DUMMY_RECEIVER, CONF_ID, + CONF_PARITY, + CONF_STOP_BITS, ) from esphome.cpp_types import Component diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index 796423438e5..66cd4ce12aa 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -4,21 +4,21 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_CHANNEL, + CONF_DATA_BITS, CONF_ID, CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OUTPUT, + CONF_PARITY, + CONF_STOP_BITS, ) CODEOWNERS = ["@DrCoolZic"] AUTO_LOAD = ["uart"] MULTI_CONF = True -CONF_DATA_BITS = "data_bits" -CONF_STOP_BITS = "stop_bits" -CONF_PARITY = "parity" CONF_CRYSTAL = "crystal" CONF_UART = "uart" CONF_TEST_MODE = "test_mode" diff --git a/esphome/const.py b/esphome/const.py index 1611aeb1019..7262a106d8e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -280,6 +280,7 @@ CONF_CUSTOM_PRESETS = "custom_presets" CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" +CONF_DATA_BITS = "data_bits" CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" @@ -759,6 +760,7 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PARAMETERS = "parameters" +CONF_PARITY = "parity" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PATTERN = "pattern" @@ -961,6 +963,7 @@ CONF_STEP_PIN = "step_pin" CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" +CONF_STOP_BITS = "stop_bits" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSCRIBE_QOS = "subscribe_qos" From 28424d6acdb5a406f650d3f6133eb67f19304f2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 13:50:12 -1000 Subject: [PATCH 0296/2056] [ld2410][ld2412] Fix signed char causing incorrect distance values (#14380) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ld2410/ld2410.cpp | 21 ++++++++------------- esphome/components/ld2412/ld2412.cpp | 23 +++++++++-------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index dd1d53857d0..f10e7ec0aae 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -173,8 +173,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -361,17 +359,14 @@ void LD2410Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR( - this->moving_target_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) - SAFE_PUBLISH_SENSOR( - this->still_target_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])); + SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, + encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])); SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); - SAFE_PUBLISH_SENSOR( - this->detection_distance_sensor_, - ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH])); + SAFE_PUBLISH_SENSOR(this->detection_distance_sensor_, + encode_uint16(this->buffer_data_[DETECT_DISTANCE_HIGH], this->buffer_data_[DETECT_DISTANCE_LOW])); if (engineering_mode) { /* @@ -578,8 +573,8 @@ bool LD2410Component::handle_ack_data_() { /* None Duration: 33~34th bytes */ - updates.push_back(set_number_value(this->timeout_number_, - ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33]))); + updates.push_back( + set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]))); for (auto &update : updates) { update(); } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 484d5bd281c..ef0915d0bc0 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -192,8 +192,6 @@ static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0x // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -398,22 +396,19 @@ void LD2412Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR( - this->moving_target_distance_sensor_, - ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) - SAFE_PUBLISH_SENSOR( - this->still_target_distance_sensor_, - ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, + encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])) SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) if (this->detection_distance_sensor_ != nullptr) { int new_detect_distance = 0; if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { new_detect_distance = - ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]); } else if (target_state != 0x00) { - new_detect_distance = - ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]); } this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); } @@ -637,9 +632,9 @@ bool LD2412Component::handle_ack_data_() { /* None Duration: 11~12th bytes */ - updates.push_back(set_number_value(this->timeout_number_, - ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); - ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + updates.push_back( + set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]))); + ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12])); /* Output pin configuration: 13th bytes */ From fdbfac15db79a329629173c1d6ba967f12b43e46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:21:08 -1000 Subject: [PATCH 0297/2056] [uart] Replace wake-on-RX task+queue with direct ISR callback (#14382) --- .../uart/uart_component_esp_idf.cpp | 101 +++--------------- .../components/uart/uart_component_esp_idf.h | 21 ++-- esphome/core/application.h | 10 ++ esphome/core/lwip_fast_select.c | 14 +++ esphome/core/lwip_fast_select.h | 5 + 5 files changed, 52 insertions(+), 99 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8699d37d7ad..631db1e5c73 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -2,7 +2,6 @@ #include "uart_component_esp_idf.h" #include -#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -10,6 +9,10 @@ #include "driver/gpio.h" #include "soc/gpio_num.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include "esphome/core/application.h" +#endif + #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -107,12 +110,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { -#ifdef USE_UART_WAKE_LOOP_ON_RX - if (this->rx_event_task_handle_ != nullptr) { - vTaskDelete(this->rx_event_task_handle_); - this->rx_event_task_handle_ = nullptr; - } -#endif err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -120,20 +117,13 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } -#ifdef USE_UART_WAKE_LOOP_ON_RX - constexpr int event_queue_size = 20; - QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; -#else - constexpr int event_queue_size = 0; - QueueHandle_t *event_queue_ptr = nullptr; -#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will // block task until all data has been sent out - event_queue_size, // event queue size/depth - event_queue_ptr, // event queue - 0 // Flags used to allocate the interrupt + 0, // event queue size/depth + nullptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -213,8 +203,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { } #ifdef USE_UART_WAKE_LOOP_ON_RX - // Start the RX event task to enable low-latency data notifications - this->start_rx_event_task_(); + // Register ISR callback to wake the main loop when UART data arrives. + // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to + // wake the main loop task directly — no queue or FreeRTOS task needed. + uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback); #endif // USE_UART_WAKE_LOOP_ON_RX if (dump_config) { @@ -345,71 +337,12 @@ void IDFUARTComponent::flush() { void IDFUARTComponent::check_logger_conflict() {} #ifdef USE_UART_WAKE_LOOP_ON_RX -void IDFUARTComponent::start_rx_event_task_() { - // Create FreeRTOS task to monitor UART events - BaseType_t result = xTaskCreate(rx_event_task_func, // Task function - "uart_rx_evt", // Task name (max 16 chars) - 2240, // Stack size in bytes (~2.2KB); increase if needed for logging - this, // Task parameter (this pointer) - tskIDLE_PRIORITY + 1, // Priority (low, just above idle) - &this->rx_event_task_handle_ // Task handle - ); - - if (result != pdPASS) { - ESP_LOGE(TAG, "Failed to create RX event task"); - return; - } - - ESP_LOGV(TAG, "RX event task started"); -} - -// FreeRTOS task that relays UART ISR events to the main loop. -// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a -// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. -// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, -// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading -// is done by the main loop. This task only reads from the event queue and -// calls App.wake_loop_threadsafe(). -void IDFUARTComponent::rx_event_task_func(void *param) { - auto *self = static_cast(param); - uart_event_t event; - - ESP_LOGV(TAG, "RX event task running"); - - // Run forever - task lifecycle matches component lifecycle - while (true) { - // Wait for UART events (blocks efficiently) - if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) { - switch (event.type) { - case UART_DATA: - // Data available in UART RX buffer - wake the main loop - ESP_LOGVV(TAG, "Data event: %d bytes", event.size); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); -#endif - break; - - case UART_FIFO_OVF: - case UART_BUFFER_FULL: - // Don't call uart_flush_input() here — this task does not own the read side. - // ESP-IDF examples flush on overflow because the same task handles both events - // and reads, so flush and read are serialized. Here, reads happen on the main - // loop, so flushing from this task races with read_array() and can destroy data - // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() - // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes - // into the ring buffer and re-enables RX interrupts once space is freed. - ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); -#endif - break; - - default: - // Ignore other event types - ESP_LOGVV(TAG, "Event type: %d", event.type); - break; - } - } +// ISR callback invoked by the ESP-IDF UART driver when data arrives. +// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed. +void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, + BaseType_t *task_woken) { + if (uart_select_notif == UART_SELECT_READ_NOTIF) { + Application::wake_loop_isrsafe(task_woken); } } #endif // USE_UART_WAKE_LOOP_ON_RX diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1517eab5098..631bf54cd56 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -5,6 +5,9 @@ #include #include "esphome/core/component.h" #include "uart_component.h" +#ifdef USE_UART_WAKE_LOOP_ON_RX +#include +#endif namespace esphome::uart { @@ -12,9 +15,7 @@ namespace esphome::uart { /// /// Thread safety: All public methods must only be called from the main loop. /// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's -/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task -/// (when enabled) must not call any of these methods — it communicates with the -/// main loop exclusively via App.wake_loop_threadsafe(). +/// peek byte state (has_peek_/peek_byte_) is not synchronized. class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -33,9 +34,6 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } -#ifdef USE_UART_WAKE_LOOP_ON_RX - QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } -#endif /** * Load the UART with the current settings. @@ -61,15 +59,8 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support — runs on a separate FreeRTOS task. - // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, - // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the - // event queue and call App.wake_loop_threadsafe(). - void start_rx_event_task_(); - static void rx_event_task_func(void *param); - - QueueHandle_t uart_event_queue_; - TaskHandle_t rx_event_task_handle_{nullptr}; + // ISR callback for UART RX data notification — wakes the main loop directly. + static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken); #endif // USE_UART_WAKE_LOOP_ON_RX }; diff --git a/esphome/core/application.h b/esphome/core/application.h index ba3ce13291e..13e0f638856 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -500,6 +500,16 @@ class Application { /// On other platforms: uses UDP loopback socket void wake_loop_threadsafe(); #endif + +#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) + /// Wake the main event loop from an ISR. + /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. + /// Only available on platforms with fast select (ESP32, LibreTiny). + /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. + static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { + esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); + } +#endif #endif protected: diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 88cf23b67e8..da0f1f337a4 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -126,6 +126,12 @@ #include +// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. +// On LibreTiny it's not defined — provide a no-op fallback. +#ifndef IRAM_ATTR +#define IRAM_ATTR +#endif + // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -225,4 +231,12 @@ void esphome_lwip_wake_main_loop(void) { } } +// Wake the main loop from an ISR. ISR-safe variant. +void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { + TaskHandle_t task = s_main_loop_task; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + } +} + #endif // defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index b08c9462126..b7a70a8f9f2 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -28,6 +28,11 @@ void esphome_lwip_hook_socket(int fd); /// NOT ISR-safe — must only be called from task context. void esphome_lwip_wake_main_loop(void); +/// Wake the main loop task from an ISR — costs <1 us. +/// ISR-safe variant using vTaskNotifyGiveFromISR(). +/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. +void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); + #ifdef __cplusplus } #endif From b7cb65ec4996ba6c1b768908555f53586eb37d4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:23:20 -1000 Subject: [PATCH 0298/2056] [ci] Fix TypeError in ci-custom.py when POST lint checks fail (#14378) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 85a446ba0dd..f428eb0821b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -959,7 +959,7 @@ def main(): continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) - run_checks(LINT_POST_CHECKS, "POST") + run_checks(LINT_POST_CHECKS, Path("POST")) for f, errs in sorted(errors.items()): bold = functools.partial(styled, colorama.Style.BRIGHT) From c0781d3680f6c816510f7417eeb4523964e08575 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:08 -1000 Subject: [PATCH 0299/2056] [ld2450] Use atan2f for angle calculation (#14388) --- esphome/components/ld2450/ld2450.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index d30c1647695..5ec3ce47d70 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -168,15 +168,6 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { return dec_val; } -static inline float calculate_angle(float base, float hypotenuse) { - if (base < 0.0f || hypotenuse <= 0.0f) { - return 0.0f; - } - float angle_radians = acosf(base / hypotenuse); - float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v); - return angle_degrees; -} - static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } @@ -510,11 +501,8 @@ void LD2450Component::handle_periodic_data_() { } #ifdef USE_SENSOR SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); - // ANGLE - angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); - if (tx > 0) { - angle = angle * -1; - } + // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed + angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); #endif #ifdef USE_TEXT_SENSOR From 80e0761bf1c1085296aa785405e57e2c16cc6e38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:31 -1000 Subject: [PATCH 0300/2056] [ld2450] Use integer dedup for direction text sensor updates (#14386) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- esphome/components/ld2450/ld2450.h | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 5ec3ce47d70..ca64836bf86 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -516,10 +516,11 @@ void LD2450Component::handle_periodic_data_() { } else { direction = DIRECTION_STATIONARY; } - text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; - const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction); - if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) { - tsd->publish_state(dir_str); + if (this->direction_dedup_[index].next(direction)) { + text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; + if (tsd != nullptr) { + tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); + } } #endif diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 30f96c0a9c1..518b320b0af 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -194,7 +194,8 @@ class LD2450Component : public Component, public uart::UARTDevice { std::array *, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::array direction_text_sensors_{}; + std::array direction_text_sensors_{}; + std::array, MAX_TARGETS> direction_dedup_{}; #endif LazyCallbackManager data_callback_; From 82e620bcf5239bf1ad540986a54c50b2e3689326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:26:55 -1000 Subject: [PATCH 0301/2056] [ld2450] Single-pass zone target counting (#14387) --- esphome/components/ld2450/ld2450.cpp | 22 ++++++++++++---------- esphome/components/ld2450/ld2450.h | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index ca64836bf86..583918e5f53 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -283,16 +283,19 @@ void LD2450Component::loop() { } } -// Count targets in zone -uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) { - uint8_t count = 0; - for (auto &index : this->target_info_) { - if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 && - index.is_moving == is_moving) { - count++; +// Count targets in zone (single pass for both still and moving) +void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) { + still = 0; + moving = 0; + for (auto &target : this->target_info_) { + if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) { + if (target.is_moving) { + moving++; + } else { + still++; + } } } - return count; } // Service reset_radar_zone @@ -540,8 +543,7 @@ void LD2450Component::handle_periodic_data_() { uint8_t zone_moving_targets = 0; uint8_t zone_all_targets = 0; for (index = 0; index < MAX_ZONES; index++) { - zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false); - zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true); + this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets); zone_all_targets = zone_still_targets + zone_moving_targets; // Publish Still Target Count in Zones diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 518b320b0af..39b0ebd9da0 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -163,7 +163,7 @@ class LD2450Component : public Component, public uart::UARTDevice { void save_to_flash_(float value); float restore_from_flash_(); bool get_timeout_status_(uint32_t check_millis); - uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); + void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving); uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; From 3f97b3b7066c52bb7df8a9e5e686d290d7e88e23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 14:27:06 -1000 Subject: [PATCH 0302/2056] [core] Extract set_status_flag_ helper to deduplicate status_set methods (#14384) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/component.cpp | 25 +++++++++++-------------- esphome/core/component.h | 6 ++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 1fd621ea83a..fd4e0d29840 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -383,31 +383,30 @@ bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STA bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } +bool Component::set_status_flag_(uint8_t flag) { + if ((this->component_state_ & flag) != 0) + return false; + this->component_state_ |= flag; + App.app_state_ |= flag; + return true; +} void Component::status_set_warning(const char *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); } void Component::status_set_warning(const LogString *message) { - // Don't spam the log. This risks missing different warning messages though. - if ((this->component_state_ & STATUS_LED_WARNING) != 0) + if (!this->set_status_flag_(STATUS_LED_WARNING)) return; - this->component_state_ |= STATUS_LED_WARNING; - App.app_state_ |= STATUS_LED_WARNING; ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { @@ -415,10 +414,8 @@ void Component::status_set_error(const char *message) { } } void Component::status_set_error(const LogString *message) { - if ((this->component_state_ & STATUS_LED_ERROR) != 0) + if (!this->set_status_flag_(STATUS_LED_ERROR)) return; - this->component_state_ |= STATUS_LED_ERROR; - App.app_state_ |= STATUS_LED_ERROR; ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 0b0b6345c74..6b920da2901 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -299,6 +299,12 @@ class Component { this->component_state_ |= state; } + /// Helper to set a status LED flag on both this component and the app. + /// Returns true if the flag was newly set, false if it was already set. + /// Note: Callers often use the return value to decide whether to log a warning/error, + /// so once a flag is set, subsequent (potentially different) messages may be suppressed. + bool set_status_flag_(uint8_t flag); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). From 19bbd39e334332d5ebc3f33a2a435e96ab54555f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 17:06:46 -1000 Subject: [PATCH 0303/2056] [uart] Enable wake-on-RX by default on ESP32 (#14391) --- esphome/components/uart/__init__.py | 42 +++++----------------- esphome/components/zwave_proxy/__init__.py | 3 -- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a6f4cf5d5fa..69db4b44aae 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from logging import getLogger import math import re @@ -117,38 +116,6 @@ MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True -@dataclass -class UARTData: - """State data for UART component configuration generation.""" - - wake_loop_on_rx: bool = False - - -def _get_data() -> UARTData: - """Get UART component data from CORE.data.""" - if DOMAIN not in CORE.data: - CORE.data[DOMAIN] = UARTData() - return CORE.data[DOMAIN] - - -def request_wake_loop_on_rx() -> None: - """Request that the UART wake the main loop when data is received. - - Components that need low-latency notification of incoming UART data - should call this function during their code generation. - This enables the RX event task which wakes the main loop when data arrives. - """ - data = _get_data() - if not data.wake_loop_on_rx: - data.wake_loop_on_rx = True - - # UART RX event task uses wake_loop_threadsafe() to notify the main loop - # Automatically enable the socket wake infrastructure when RX wake is requested - from esphome.components import socket - - socket.require_wake_loop_threadsafe() - - def validate_raw_data(value): if isinstance(value, str): return value.encode("utf-8") @@ -542,7 +509,14 @@ async def uart_write_to_code(config, action_id, template_arg, args): @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): """Final code generation step to configure optional UART features.""" - if _get_data().wake_loop_on_rx: + if CORE.is_esp32 and CORE.has_networking: + # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer + # registration) — enable by default to reduce RX buffer overflow risk + # by waking the main loop immediately when data arrives. + # Requires networking for the wake_loop_isrsafe() infrastructure. + from esphome.components import socket + + socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index 5be05bb4646..d88f9f7041e 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -41,6 +41,3 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add_define("USE_ZWAVE_PROXY") - - # Request UART to wake the main loop when data arrives for low-latency processing - uart.request_wake_loop_on_rx() From a1760a198047d92a36fe9c8af01ca5c86f736d80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 10:23:10 -1000 Subject: [PATCH 0304/2056] [improv_serial] Add missing USE_IMPROV_SERIAL define to fix WiFi scan filtering (#14359) --- esphome/components/improv_serial/__init__.py | 1 + esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- esphome/components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 2 +- esphome/core/defines.h | 1 + 8 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 9a2ac2f40f2..4266f5b78be 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -43,3 +43,4 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await improv_base.setup_improv_core(var, config, "improv_serial") + cg.add_define("USE_IMPROV_SERIAL") diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1e6961b8bde..7d5d0133c1e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2121,7 +2121,7 @@ bool WiFiComponent::can_proceed() { #endif void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected() { +bool WiFiComponent::is_connected() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 63c7039f21a..a6f03a08d9d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -443,7 +443,7 @@ class WiFiComponent : public Component { void set_reboot_timeout(uint32_t reboot_timeout); - bool is_connected(); + bool is_connected() const; void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } @@ -677,7 +677,7 @@ class WiFiComponent : public Component { bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); - WiFiSTAConnectStatus wifi_sta_connect_status_(); + WiFiSTAConnectStatus wifi_sta_connect_status_() const; bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 8911bf15e07..bd6a18a99b2 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -622,7 +622,7 @@ void WiFiComponent::wifi_pre_setup_() { this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { station_status_t status = wifi_station_get_connect_status(); if (status == STATION_GOT_IP) return WiFiSTAConnectStatus::CONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 57bbceb1b85..734d1862052 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -921,7 +921,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { if (s_sta_connected && this->got_ipv4_address_) { #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 1c5490a3ac3..4c4150e44d4 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -634,7 +634,7 @@ void WiFiComponent::wifi_pre_setup_() { // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use state machine instead of querying WiFi.status() directly // State is updated in main loop from queued events, ensuring thread safety switch (s_sta_state) { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 9b2c077dc5e..270425d8c21 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -120,7 +120,7 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "UNKNOWN"; } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5c982c94b13..181425c1625 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -55,6 +55,7 @@ #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_IMAGE +#define USE_IMPROV_SERIAL #define USE_IMPROV_SERIAL_NEXT_URL #define USE_INFRARED #define USE_IR_RF From 48b5cae6c4692c6f563b1c250aa266e31b0b1b98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 11:32:44 -1000 Subject: [PATCH 0305/2056] [api] Use StringRef for user service string arguments (#13974) --- esphome/automation.py | 57 +++++- esphome/components/api/__init__.py | 15 +- esphome/components/api/api_connection.cpp | 43 +++-- esphome/components/api/api_frame_helper.h | 4 + .../components/api/api_frame_helper_noise.cpp | 30 ++- .../api/api_frame_helper_plaintext.cpp | 7 +- esphome/components/api/user_services.cpp | 5 + esphome/components/button/__init__.py | 4 +- esphome/components/cover/__init__.py | 20 +- esphome/components/fan/__init__.py | 9 +- esphome/components/globals/__init__.py | 1 + esphome/components/light/automation.py | 12 +- esphome/components/logger/__init__.py | 4 +- esphome/components/mqtt/__init__.py | 7 +- esphome/components/number/__init__.py | 6 + esphome/components/output/__init__.py | 7 +- esphome/components/script/__init__.py | 2 + esphome/components/select/__init__.py | 7 + esphome/components/switch/__init__.py | 14 +- esphome/components/text/__init__.py | 1 + esphome/core/string_ref.h | 14 ++ esphome/util.py | 16 +- tests/unit_tests/test_automation.py | 177 ++++++++++++++++++ 23 files changed, 406 insertions(+), 56 deletions(-) create mode 100644 tests/unit_tests/test_automation.py diff --git a/esphome/automation.py b/esphome/automation.py index 2439b1ddc4b..d9b8b2ec574 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -57,8 +57,23 @@ def maybe_conf(conf, *validators): return validate -def register_action(name: str, action_type: MockObjClass, schema: cv.Schema): - return ACTION_REGISTRY.register(name, action_type, schema) +def register_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool = False, +): + """Register an action type. + + Actions default to ``synchronous=False`` (safe default), meaning string + arguments use owning std::string to prevent dangling references. + + Set ``synchronous=True`` only for actions that complete synchronously + and never store trigger arguments for later execution. This allows + the code generator to use non-owning StringRef for zero-copy access. + """ + return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema): @@ -335,7 +350,9 @@ async def component_is_idle_condition_to_code( @register_action( - "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) + "delay", + DelayAction, + cv.templatable(cv.positive_time_period_milliseconds), ) async def delay_action_to_code( config: ConfigType, @@ -366,6 +383,7 @@ async def delay_action_to_code( cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), ), + synchronous=True, ) async def if_action_to_code( config: ConfigType, @@ -394,6 +412,7 @@ async def if_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def while_action_to_code( config: ConfigType, @@ -417,6 +436,7 @@ async def while_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def repeat_action_to_code( config: ConfigType, @@ -461,7 +481,12 @@ async def wait_until_action_to_code( return var -@register_action("lambda", LambdaAction, cv.lambda_) +# Lambda executes user C++ inline and returns — synchronous by execution model. +# User code could theoretically store the StringRef for deferred use, but StringRef +# is a view type and storing views beyond their scope is always unsafe regardless +# of this optimization. Marking non-synchronous would disable StringRef for nearly +# all user services since most use lambda. +@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True) async def lambda_action_to_code( config: ConfigType, action_id: ID, @@ -480,6 +505,7 @@ async def lambda_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_update_action_to_code( config: ConfigType, @@ -499,6 +525,7 @@ async def component_update_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_suspend_action_to_code( config: ConfigType, @@ -521,6 +548,7 @@ async def component_suspend_action_to_code( ), } ), + synchronous=True, ) async def component_resume_action_to_code( config: ConfigType, @@ -578,6 +606,27 @@ async def build_condition_list( return conditions +def has_non_synchronous_actions(actions: ConfigType) -> bool: + """Check if a validated action list contains any non-synchronous actions. + + Non-synchronous actions (delay, wait_until, script.wait, etc.) store + trigger args for later execution, making non-owning types like StringRef + unsafe. Actions that haven't been audited default to non-synchronous. + """ + if isinstance(actions, list): + return any(has_non_synchronous_actions(item) for item in actions) + if isinstance(actions, dict): + for key in actions: + if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous: + return True + return any( + has_non_synchronous_actions(v) + for v in actions.values() + if isinstance(v, (list, dict)) + ) + return False + + async def build_automation( trigger: MockObj, args: TemplateArgsType, config: ConfigType ) -> MockObj: diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 3f7cafb4851..d7b6bec3573 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { "bool": cg.bool_, "int": cg.int32, "float": cg.float_, - "string": cg.std_string, + "string": cg.StringRef, "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), @@ -380,9 +380,18 @@ async def to_code(config: ConfigType) -> None: if is_optional: func_args.append((cg.bool_, "return_response")) + # Check if action chain has non-synchronous actions that would make + # non-owning StringRef dangle (rx_buf_ reused after delay) + has_non_synchronous = automation.has_non_synchronous_actions( + conf.get(CONF_THEN, []) + ) + service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): native = SERVICE_ARG_NATIVE_TYPES[var_] + # Fall back to std::string for string args if non-synchronous actions exist + if has_non_synchronous and native is cg.StringRef: + native = cg.std_string service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) @@ -509,11 +518,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( "homeassistant.action", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_service_to_code( config: ConfigType, @@ -604,6 +615,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( "homeassistant.event", HomeAssistantServiceCallAction, HOMEASSISTANT_EVENT_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_event_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") @@ -644,6 +656,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( "homeassistant.tag_scanned", HomeAssistantServiceCallAction, HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8b2efdde518..215af611dbd 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1711,37 +1711,42 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes return; } + // Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks). + // Safe: decode is complete, byte after string data was already consumed during parse, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + if (!msg.state.empty()) { + const_cast(msg.state.c_str())[msg.state.size()] = '\0'; + } + for (auto &it : this->parent_->get_state_subs()) { - // Compare entity_id: check length matches and content matches - size_t entity_id_len = strlen(it.entity_id); - if (entity_id_len != msg.entity_id.size() || - memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) { + if (msg.entity_id != it.entity_id) { continue; } - // Compare attribute: either both have matching attribute, or both have none - size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; - if (sub_attr_len != msg.attribute.size() || - (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) { + // If subscriber has attribute filter (non-null), message attribute must match it; + // if subscriber has no filter (nullptr), message must have no attribute. + if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) { continue; } - // Create null-terminated state for callback (parse_number needs null-termination) - // HA state max length is 255 characters, but attributes can be much longer - // Use stack buffer for common case (states), heap fallback for large attributes - size_t state_len = msg.state.size(); - SmallBufferWithHeapFallback state_buf_alloc(state_len + 1); - char *state_buf = reinterpret_cast(state_buf_alloc.get()); - if (state_len > 0) { - memcpy(state_buf, msg.state.c_str(), state_len); - } - state_buf[state_len] = '\0'; - it.callback(StringRef(state_buf, state_len)); + it.callback(msg.state); } } #endif #ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { + // Null-terminate string args in-place for safe c_str() usage in YAML service triggers. + // Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + for (auto &arg : const_cast(msg).args) { + if (!arg.string_.empty()) { + const_cast(arg.string_.c_str())[arg.string_.size()] = '\0'; + } + } bool found = false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Register the call and get a unique server-generated action_call_id diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 03f3814bb9b..2b4e9ea3cdb 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -29,6 +29,10 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif +// Extra byte reserved in rx_buf_ beyond the message size so protobuf +// StringRef fields can be null-terminated in-place after decode. +static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; + // Maximum number of messages to batch in a single write operation // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 2aad732f7fa..3ae35e9be81 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -194,16 +194,21 @@ APIError APINoiseFrameHelper::try_read_frame_() { uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data - uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; + bool is_data = (state_ == State::DATA); + uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; if (msg_size > limit) { state_ = State::FAILED; HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); - return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; + return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; } - // Reserve space for body - if (this->rx_buf_.size() != msg_size) { - this->rx_buf_.resize(msg_size); + // Reserve space for body (+ null terminator in DATA state so protobuf + // StringRef fields can be safely null-terminated in-place after decode. + // During handshake, rx_buf_.size() is used in prologue construction, so + // the buffer must be exactly msg_size to avoid prologue mismatch.) + uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); + if (this->rx_buf_.size() != alloc_size) { + this->rx_buf_.resize(alloc_size); } if (rx_buf_len_ < msg_size) { @@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); + // read_packet() must only be called in DATA state; the extra + // RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state + // (see try_read_frame_), so calling this during handshake would + // underflow the size calculation below. +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); +#endif + // rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination + // (only added in DATA state — see try_read_frame_), so subtract it + // to get the actual encrypted data size for decryption. + size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR; + noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size); int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); APIError decrypt_err = handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); @@ -574,7 +590,9 @@ APIError APINoiseFrameHelper::init_handshake_() { } APIError APINoiseFrameHelper::check_handshake_finished_() { +#ifdef ESPHOME_DEBUG_API assert(state_ == State::HANDSHAKE); +#endif int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 5069dbf68b5..e2bb56e0acf 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -163,9 +163,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } // header reading done - // Reserve space for body - if (this->rx_buf_.size() != this->rx_header_parsed_len_) { - this->rx_buf_.resize(this->rx_header_parsed_len_); + // Reserve space for body (+ null terminator so protobuf StringRef fields + // can be safely null-terminated in-place after decode) + if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); } if (rx_buf_len_ < rx_header_parsed_len_) { diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 9c2b4aa79a4..28a43c656c3 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,5 +1,6 @@ #include "user_services.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome::api { @@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } +// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode) +template<> StringRef get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { @@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type() { return enums::SER template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } +// Zero-copy StringRef version for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } // Legacy std::vector versions for external components using custom_api_device.h template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index d2f143b97ef..94816a09748 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -123,7 +123,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id( ) -@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA) +@automation.register_action( + "button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True +) async def button_press_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 648fe7decff..17095f41f65 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -248,25 +248,33 @@ COVER_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_open_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_close_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_stop_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -283,7 +291,9 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA) +@automation.register_action( + "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True +) async def cover_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 6010aa8ed46..e839df6aee6 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -311,13 +311,17 @@ FAN_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_turn_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -336,6 +340,7 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fc400c5dd19..fe11a93a4bb 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -102,6 +102,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index e5aa8fa0e95..89b2fc0fb21 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -51,6 +51,7 @@ from .types import ( ), } ), + synchronous=True, ) async def light_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -111,13 +112,13 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA + "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA + "light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA + "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True ) async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -193,7 +194,10 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA + "light.dim_relative", + DimRelativeAction, + LIGHT_DIM_RELATIVE_ACTION_SCHEMA, + synchronous=True, ) async def light_dim_relative_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 264197c175d..026b8aaf246 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -519,7 +519,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All( ) -@automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) +@automation.register_action( + CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA, synchronous=True +) async def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 44e88364873..c25c4720387 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -492,7 +492,7 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA + "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA, synchronous=True ) async def mqtt_publish_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -521,7 +521,10 @@ MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA + "mqtt.publish_json", + MQTTPublishJsonAction, + MQTT_PUBLISH_JSON_ACTION_SCHEMA, + synchronous=True, ) async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index d12ec7463b5..2238f2c0375 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -352,6 +352,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def number_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -374,6 +375,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.decrement", @@ -388,6 +390,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.to_min", @@ -401,6 +404,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.to_max", @@ -414,6 +418,7 @@ async def number_set_to_code(config, action_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "number.operation", @@ -426,6 +431,7 @@ async def number_set_to_code(config, action_id, template_arg, args): cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def number_to_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index bde106b0851..a4c960927ba 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -74,14 +74,16 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +@automation.register_action( + "output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True +) async def output_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA + "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True ) async def output_turn_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -97,6 +99,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): cv.Required(CONF_LEVEL): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 8d69981db0c..369cefad91a 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -160,6 +160,7 @@ async def to_code(config): cv.Optional(validate_parameter_name): cv.templatable(cv.valid), }, ), + synchronous=True, ) async def script_execute_action_to_code(config, action_id, template_arg, args): def convert(type: str): @@ -208,6 +209,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): "script.stop", ScriptStopAction, maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), + synchronous=True, ) async def script_stop_action_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 84ad591ba13..c114b140a9a 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -145,6 +145,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def select_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -162,6 +163,7 @@ async def select_set_to_code(config, action_id, template_arg, args): cv.Required(CONF_INDEX): cv.templatable(cv.positive_int), } ), + synchronous=True, ) async def select_set_index_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -217,6 +219,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), } ), + synchronous=True, ) @automation.register_action( "select.next", @@ -229,6 +232,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.previous", @@ -243,6 +247,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.first", @@ -254,6 +259,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) @automation.register_action( "select.last", @@ -265,6 +271,7 @@ async def select_is_to_code(config, condition_id, template_arg, args): } ) ), + synchronous=True, ) async def select_operation_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index cfc5e2b6e82..6f1be7d53d5 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -204,7 +204,7 @@ SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA + "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA, synchronous=True ) async def switch_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -214,9 +214,15 @@ async def switch_control_to_code(config, action_id, template_arg, args): return var -@automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) -@automation.register_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA) -@automation.register_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA) +@automation.register_action( + "switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA, synchronous=True +) async def switch_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 9ceea0dfdfc..61f7119cadb 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -164,6 +164,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def text_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index d502c4d27fa..60472027530 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -81,6 +81,20 @@ class StringRef { operator std::string() const { return str(); } + /// Compare (compatible with std::string::compare) + int compare(const StringRef &other) const { + int result = std::memcmp(base_, other.base_, std::min(len_, other.len_)); + if (result != 0) + return result; + if (len_ < other.len_) + return -1; + if (len_ > other.len_) + return 1; + return 0; + } + int compare(const char *s) const { return compare(StringRef(s)); } + int compare(const std::string &s) const { return compare(StringRef(s)); } + /// Find first occurrence of substring, returns std::string::npos if not found. /// Note: Requires the underlying string to be null-terminated. size_type find(const char *s, size_type pos = 0) const { diff --git a/esphome/util.py b/esphome/util.py index 7b896de27eb..686aa74306a 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -24,11 +24,14 @@ class RegistryEntry: fun: Callable[..., Any], type_id: "MockObjClass", schema: "Schema", + *, + synchronous: bool = False, ): self.name = name self.fun = fun self.type_id = type_id self.raw_schema = schema + self.synchronous = synchronous @property def coroutine_fun(self): @@ -49,9 +52,18 @@ class Registry(dict[str, RegistryEntry]): self.base_schema = base_schema or {} self.type_id_key = type_id_key - def register(self, name: str, type_id: "MockObjClass", schema: "Schema"): + def register( + self, + name: str, + type_id: "MockObjClass", + schema: "Schema", + *, + synchronous: bool = False, + ): def decorator(fun: Callable[..., Any]): - self[name] = RegistryEntry(name, fun, type_id, schema) + self[name] = RegistryEntry( + name, fun, type_id, schema, synchronous=synchronous + ) return fun return decorator diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py new file mode 100644 index 00000000000..61fef8201d3 --- /dev/null +++ b/tests/unit_tests/test_automation.py @@ -0,0 +1,177 @@ +"""Tests for esphome.automation module.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from esphome.automation import has_non_synchronous_actions +from esphome.util import RegistryEntry + + +def _make_registry(non_synchronous_actions: set[str]) -> dict[str, RegistryEntry]: + """Create a mock ACTION_REGISTRY with specified non-synchronous actions. + + Uses the default synchronous=False, matching the real registry behavior. + """ + registry: dict[str, RegistryEntry] = {} + for name in non_synchronous_actions: + registry[name] = RegistryEntry(name, lambda: None, None, None) + return registry + + +@pytest.fixture +def mock_registry() -> Generator[dict[str, RegistryEntry]]: + """Fixture that patches ACTION_REGISTRY with delay, wait_until, script.wait as non-synchronous.""" + registry: dict[str, RegistryEntry] = _make_registry( + {"delay", "wait_until", "script.wait"} + ) + registry["logger.log"] = RegistryEntry( + "logger.log", lambda: None, None, None, synchronous=True + ) + with patch("esphome.automation.ACTION_REGISTRY", registry): + yield registry + + +def test_has_non_synchronous_actions_empty_list( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([]) is False + + +def test_has_non_synchronous_actions_empty_dict( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions({}) is False + + +def test_has_non_synchronous_actions_non_dict_non_list( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions("string") is False + assert has_non_synchronous_actions(42) is False + assert has_non_synchronous_actions(None) is False + + +def test_has_non_synchronous_actions_delay( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"delay": "1s"}]) is True + + +def test_has_non_synchronous_actions_wait_until( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"wait_until": {"condition": {}}}]) is True + + +def test_has_non_synchronous_actions_script_wait( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"script.wait": "script_id"}]) is True + + +def test_has_non_synchronous_actions_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"logger.log": "hello"}]) is False + + +def test_has_non_synchronous_actions_unknown_not_in_registry( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Unknown actions not in registry are not flagged (only registered actions count).""" + assert has_non_synchronous_actions([{"unknown.action": "value"}]) is False + + +def test_has_non_synchronous_actions_default_non_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Actions registered without explicit synchronous=True default to non-synchronous.""" + mock_registry["some.action"] = RegistryEntry( + "some.action", lambda: None, None, None + ) + assert has_non_synchronous_actions([{"some.action": "value"}]) is True + + +def test_has_non_synchronous_actions_nested_in_then( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Non-synchronous action nested inside a synchronous action's then block.""" + actions: list[dict[str, object]] = [ + { + "logger.log": "first", + "then": [{"delay": "1s"}], + } + ] + assert has_non_synchronous_actions(actions) is True + + +def test_has_non_synchronous_actions_deeply_nested( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Non-synchronous action deeply nested in action structure.""" + actions: list[dict[str, object]] = [ + { + "if": { + "then": [ + {"logger.log": "hello"}, + {"delay": "500ms"}, + ] + } + } + ] + assert has_non_synchronous_actions(actions) is True + + +def test_has_non_synchronous_actions_none_in_nested( + mock_registry: dict[str, RegistryEntry], +) -> None: + """No non-synchronous actions even with nesting.""" + actions: list[dict[str, object]] = [ + { + "if": { + "then": [ + {"logger.log": "hello"}, + ] + } + } + ] + assert has_non_synchronous_actions(actions) is False + + +def test_has_non_synchronous_actions_multiple_one_non_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert ( + has_non_synchronous_actions( + [ + {"logger.log": "first"}, + {"delay": "1s"}, + {"logger.log": "second"}, + ] + ) + is True + ) + + +def test_has_non_synchronous_actions_multiple_all_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert ( + has_non_synchronous_actions( + [ + {"logger.log": "first"}, + {"logger.log": "second"}, + ] + ) + is False + ) + + +def test_has_non_synchronous_actions_dict_input( + mock_registry: dict[str, RegistryEntry], +) -> None: + """Direct dict input (single action).""" + assert has_non_synchronous_actions({"delay": "1s"}) is True + assert has_non_synchronous_actions({"logger.log": "hello"}) is False From 3e7424b307b5763bba217d5f0d5d18130b67be4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 15:22:55 -1000 Subject: [PATCH 0306/2056] [preferences] Reduce heap churn with small inline buffer optimization (#13259) --- esphome/components/esp32/preferences.cpp | 33 ++++----- esphome/components/libretiny/preferences.cpp | 33 ++++----- esphome/core/helpers.h | 72 ++++++++++++++++++++ 3 files changed, 96 insertions(+), 42 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8d6fdc86f60..a3ef10b21f9 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -133,10 +124,10 @@ class ESP32Preferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -144,7 +135,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } @@ -176,7 +167,7 @@ class ESP32Preferences : public ESPPreferences { return true; } // Check size first before allocating memory - if (actual_len != to_save.len) { + if (actual_len != to_save.data.size()) { return true; } // Most preferences are small, use stack buffer with heap fallback for large ones @@ -186,7 +177,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } - return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool reset() override { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 740c1a233a3..1c101136e16 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -17,16 +17,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -57,11 +48,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -122,11 +113,11 @@ class LibreTinyPreferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); - fdb_blob_make(&this->blob, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + fdb_blob_make(&this->blob, save.data.data(), save.data.size()); fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); failed++; last_err = err; last_key = save.key; @@ -134,7 +125,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } @@ -159,7 +150,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Check size first - if different, data has changed - if (kv.value_len != to_save.len) { + if (kv.value_len != to_save.data.size()) { return true; } @@ -173,7 +164,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Compare the actual data - return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; } bool reset() override { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b606e68df31..65d590a5e67 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -133,6 +133,78 @@ template class ConstVector { size_t size_; }; +/// Small buffer optimization - stores data inline when small, heap-allocates for large data +/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes. +/// Memory management is encapsulated - callers just use set() and data(). +template class SmallInlineBuffer { + public: + SmallInlineBuffer() = default; + ~SmallInlineBuffer() { + if (!this->is_inline_()) + delete[] this->heap_; + } + + // Move constructor + SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) { + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + + // Move assignment + SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept { + if (this != &other) { + if (!this->is_inline_()) + delete[] this->heap_; + this->len_ = other.len_; + if (other.is_inline_()) { + memcpy(this->inline_, other.inline_, this->len_); + } else { + this->heap_ = other.heap_; + other.heap_ = nullptr; + } + other.len_ = 0; + } + return *this; + } + + // Disable copy (would need deep copy of heap data) + SmallInlineBuffer(const SmallInlineBuffer &) = delete; + SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete; + + /// Set buffer contents, allocating heap if needed + void set(const uint8_t *src, size_t size) { + // Free existing heap allocation if switching from heap to inline or different heap size + if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) { + delete[] this->heap_; + this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes + } + // Allocate new heap buffer if needed + if (size > InlineSize && (this->is_inline_() || size != this->len_)) { + this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory) + } + this->len_ = size; + memcpy(this->data(), src, size); + } + + uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; } + const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; } + size_t size() const { return this->len_; } + + protected: + bool is_inline_() const { return this->len_ <= InlineSize; } + + size_t len_{0}; + union { + uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state + uint8_t *heap_; + }; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: From 0e18e4461e8d82df75b3acbb61c7dcc8c9993311 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 16:52:53 -1000 Subject: [PATCH 0307/2056] [time,api] Send pre-parsed timezone struct over protobuf (#14233) Co-authored-by: Claude Opus 4.6 --- esphome/components/api/api.proto | 28 ++++++++++ esphome/components/api/api_connection.cpp | 25 ++++++++- esphome/components/api/api_pb2.cpp | 54 ++++++++++++++++++ esphome/components/api/api_pb2.h | 38 ++++++++++++- esphome/components/api/api_pb2_dump.cpp | 39 +++++++++++++ esphome/components/time/__init__.py | 68 +++++++++++++++++++++-- 6 files changed, 245 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d7f32cd8d14..802e3e3ae21 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -834,6 +834,33 @@ message GetTimeRequest { option (source) = SOURCE_SERVER; } +enum DSTRuleType { + DST_RULE_TYPE_NONE = 0; + DST_RULE_TYPE_MONTH_WEEK_DAY = 1; + DST_RULE_TYPE_JULIAN_NO_LEAP = 2; + DST_RULE_TYPE_DAY_OF_YEAR = 3; +} + +message DSTRule { + option (source) = SOURCE_CLIENT; + + sint32 time_seconds = 1; + uint32 day = 2; + DSTRuleType type = 3; + uint32 month = 4; + uint32 week = 5; + uint32 day_of_week = 6; +} + +message ParsedTimezone { + option (source) = SOURCE_CLIENT; + + sint32 std_offset_seconds = 1; + sint32 dst_offset_seconds = 2; + DSTRule dst_start = 3; + DSTRule dst_end = 4; +} + message GetTimeResponse { option (id) = 37; option (source) = SOURCE_CLIENT; @@ -841,6 +868,7 @@ message GetTimeResponse { fixed32 epoch_seconds = 1; string timezone = 2; + ParsedTimezone parsed_timezone = 3; } // ==================== USER-DEFINES SERVICES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 215af611dbd..90287ec2dda 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1123,7 +1123,30 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE if (!value.timezone.empty()) { - homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + // Check if the sender provided pre-parsed timezone data. + // If std_offset is non-zero or DST rules are present, the parsed data was populated. + // For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent. + const auto &pt = value.parsed_timezone; + if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) { + time::ParsedTimezone tz{}; + tz.std_offset_seconds = pt.std_offset_seconds; + tz.dst_offset_seconds = pt.dst_offset_seconds; + tz.dst_start.time_seconds = pt.dst_start.time_seconds; + tz.dst_start.day = static_cast(pt.dst_start.day); + tz.dst_start.type = static_cast(pt.dst_start.type); + tz.dst_start.month = static_cast(pt.dst_start.month); + tz.dst_start.week = static_cast(pt.dst_start.week); + tz.dst_start.day_of_week = static_cast(pt.dst_start.day_of_week); + tz.dst_end.time_seconds = pt.dst_end.time_seconds; + tz.dst_end.day = static_cast(pt.dst_end.day); + tz.dst_end.type = static_cast(pt.dst_end.type); + tz.dst_end.month = static_cast(pt.dst_end.month); + tz.dst_end.week = static_cast(pt.dst_end.week); + tz.dst_end.day_of_week = static_cast(pt.dst_end.day_of_week); + time::set_global_tz(tz); + } else { + homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + } } #endif } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5c50a8aa5b9..9e74d5ddc7b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -954,12 +954,66 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->time_seconds = value.as_sint32(); + break; + case 2: + this->day = value.as_uint32(); + break; + case 3: + this->type = static_cast(value.as_uint32()); + break; + case 4: + this->month = value.as_uint32(); + break; + case 5: + this->week = value.as_uint32(); + break; + case 6: + this->day_of_week = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->std_offset_seconds = value.as_sint32(); + break; + case 2: + this->dst_offset_seconds = value.as_sint32(); + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: + value.decode_to_message(this->dst_start); + break; + case 4: + value.decode_to_message(this->dst_end); + break; + default: + return false; + } + return true; +} bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); break; } + case 3: + value.decode_to_message(this->parsed_timezone); + break; default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 22dc3de995d..033b789c18d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -63,6 +63,12 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; +enum DSTRuleType : uint32_t { + DST_RULE_TYPE_NONE = 0, + DST_RULE_TYPE_MONTH_WEEK_DAY = 1, + DST_RULE_TYPE_JULIAN_NO_LEAP = 2, + DST_RULE_TYPE_DAY_OF_YEAR = 3, +}; #ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, @@ -1116,15 +1122,45 @@ class GetTimeRequest final : public ProtoMessage { protected: }; +class DSTRule final : public ProtoDecodableMessage { + public: + int32_t time_seconds{0}; + uint32_t day{0}; + enums::DSTRuleType type{}; + uint32_t month{0}; + uint32_t week{0}; + uint32_t day_of_week{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ParsedTimezone final : public ProtoDecodableMessage { + public: + int32_t std_offset_seconds{0}; + int32_t dst_offset_seconds{0}; + DSTRule dst_start{}; + DSTRule dst_end{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 14; + static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; + ParsedTimezone parsed_timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 52d24864107..4eec42e936c 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -208,6 +208,20 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::DSTRuleType value) { + switch (value) { + case enums::DST_RULE_TYPE_NONE: + return "DST_RULE_TYPE_NONE"; + case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: + return "DST_RULE_TYPE_MONTH_WEEK_DAY"; + case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: + return "DST_RULE_TYPE_JULIAN_NO_LEAP"; + case enums::DST_RULE_TYPE_DAY_OF_YEAR: + return "DST_RULE_TYPE_DAY_OF_YEAR"; + default: + return "UNKNOWN"; + } +} #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { @@ -1254,10 +1268,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const { out.append("GetTimeRequest {}"); return out.c_str(); } +const char *DSTRule::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "DSTRule"); + dump_field(out, "time_seconds", this->time_seconds); + dump_field(out, "day", this->day); + dump_field(out, "type", static_cast(this->type)); + dump_field(out, "month", this->month); + dump_field(out, "week", this->week); + dump_field(out, "day_of_week", this->day_of_week); + return out.c_str(); +} +const char *ParsedTimezone::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "ParsedTimezone"); + dump_field(out, "std_offset_seconds", this->std_offset_seconds); + dump_field(out, "dst_offset_seconds", this->dst_offset_seconds); + out.append(" dst_start: "); + this->dst_start.dump_to(out); + out.append("\n"); + out.append(" dst_end: "); + this->dst_end.dump_to(out); + out.append("\n"); + return out.c_str(); +} const char *GetTimeResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); dump_field(out, "timezone", this->timezone); + out.append(" parsed_timezone: "); + this->parsed_timezone.dump_to(out); + out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index a20d79b8578..7ffa408db97 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,6 +1,10 @@ from importlib import resources import logging +from aioesphomeapi.posix_tz import ( + DSTRuleType as PyDSTRuleType, + parse_posix_tz as parse_posix_tz_python, +) import tzlocal from esphome import automation @@ -39,6 +43,19 @@ CronTrigger = time_ns.class_("CronTrigger", automation.Trigger.template(), cg.Co SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Component) TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition) +# C++ types for pre-parsed timezone struct generation +DSTRuleType_cpp = time_ns.enum("DSTRuleType", is_class=True) +DSTRule_cpp = time_ns.struct("DSTRule") +ParsedTimezone_cpp = time_ns.struct("ParsedTimezone") + +# Map Python DSTRuleType enum values to C++ enum expressions +_DST_RULE_TYPE_MAP = { + PyDSTRuleType.NONE: DSTRuleType_cpp.NONE, + PyDSTRuleType.MONTH_WEEK_DAY: DSTRuleType_cpp.MONTH_WEEK_DAY, + PyDSTRuleType.JULIAN_NO_LEAP: DSTRuleType_cpp.JULIAN_NO_LEAP, + PyDSTRuleType.DAY_OF_YEAR: DSTRuleType_cpp.DAY_OF_YEAR, +} + def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples @@ -260,11 +277,17 @@ def validate_tz(value: str) -> str: value = cv.string_strict(value) tzfile = _load_tzdata(value) - if tzfile is None: - # Not a IANA key, probably a TZ string - return value + if tzfile is not None: + value = _extract_tz_string(tzfile) - return _extract_tz_string(tzfile) + # Validate that the POSIX TZ string is parseable (skip empty strings) + if value: + try: + parse_posix_tz_python(value) + except ValueError as e: + raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + + return value TIME_SCHEMA = cv.Schema( @@ -305,11 +328,46 @@ TIME_SCHEMA = cv.Schema( ).extend(cv.polling_component_schema("15min")) +def _emit_dst_rule_fields(prefix, rule): + """Emit field-by-field assignments for a DSTRule to avoid rodata struct blob.""" + cg.add(cg.RawExpression(f"{prefix}.time_seconds = {rule.time_seconds}")) + cg.add(cg.RawExpression(f"{prefix}.day = {rule.day}")) + cg.add(cg.RawExpression(f"{prefix}.type = {_DST_RULE_TYPE_MAP[rule.type]}")) + cg.add(cg.RawExpression(f"{prefix}.month = {rule.month}")) + cg.add(cg.RawExpression(f"{prefix}.week = {rule.week}")) + cg.add(cg.RawExpression(f"{prefix}.day_of_week = {rule.day_of_week}")) + + +def _emit_parsed_timezone_fields(parsed): + """Emit field-by-field assignments for a local ParsedTimezone, then set_global_tz(). + + Uses individual assignments on a stack variable instead of a struct initializer + to keep constants as immediate operands in instructions (.irom0.text/flash) + rather than a const blob in .rodata (which maps to RAM on ESP8266). + Wrapped in a scope block to allow multiple time platforms in the same build. + """ + cg.add(cg.RawStatement("{")) + cg.add(cg.RawExpression("time::ParsedTimezone tz{}")) + cg.add(cg.RawExpression(f"tz.std_offset_seconds = {parsed.std_offset_seconds}")) + cg.add(cg.RawExpression(f"tz.dst_offset_seconds = {parsed.dst_offset_seconds}")) + _emit_dst_rule_fields("tz.dst_start", parsed.dst_start) + _emit_dst_rule_fields("tz.dst_end", parsed.dst_end) + cg.add(time_ns.set_global_tz(cg.RawExpression("tz"))) + cg.add(cg.RawStatement("}")) + + async def setup_time_core_(time_var, config): if timezone := config.get(CONF_TIMEZONE): - cg.add(time_var.set_timezone(timezone)) cg.add_define("USE_TIME_TIMEZONE") + if CORE.is_host: + # Host platform needs setenv("TZ")/tzset() for libc compatibility + cg.add(time_var.set_timezone(timezone)) + else: + # Embedded: pre-parse at codegen time, emit struct directly + parsed = parse_posix_tz_python(timezone) + _emit_parsed_timezone_fields(parsed) + for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) From 073ca63f60bb23a7c84106eb57a5ce06388b1295 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 2 Mar 2026 04:44:02 +0100 Subject: [PATCH 0308/2056] [rtttl] improve comments Part 2 (#13971) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 189 ++++++++++++++++------------- esphome/components/rtttl/rtttl.h | 31 ++--- 2 files changed, 119 insertions(+), 101 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index ab95067f458..4ccfc539eac 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -8,18 +8,26 @@ namespace esphome::rtttl { static const char *const TAG = "rtttl"; -// These values can also be found as constants in the Tone library (Tone.h) -static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, - 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, - 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, - 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; +static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64; +static constexpr uint8_t SEMITONES_IN_OCTAVE = 12; -#if defined(USE_OUTPUT) || defined(USE_SPEAKER) -static const uint32_t DOUBLE_NOTE_GAP_MS = 10; -#endif // USE_OUTPUT || USE_SPEAKER +static constexpr uint8_t MIN_OCTAVE = 4; +static constexpr uint8_t MAX_OCTAVE = 7; + +static constexpr uint8_t DEFAULT_BPM = 63; // Default beats per minute + +// These values can also be found as constants in the Tone library (Tone.h) +static constexpr uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; +static constexpr uint8_t NOTES_COUNT = static_cast(sizeof(NOTES) / sizeof(NOTES[0])); + +static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10; #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 2048; +static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048; +static constexpr uint16_t SAMPLE_RATE = 16000; struct SpeakerSample { int8_t left{0}; @@ -27,7 +35,7 @@ struct SpeakerSample { }; inline double deg2rad(double degrees) { - static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0; + static constexpr double PI_ON_180 = M_PI / 180.0; return degrees * PI_ON_180; } #endif // USE_SPEAKER @@ -85,7 +93,7 @@ void Rtttl::loop() { } #ifdef USE_OUTPUT - if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) { + if (this->output_ != nullptr && millis() - this->last_note_start_time_ < this->note_duration_) { return; } #endif // USE_OUTPUT @@ -113,36 +121,34 @@ void Rtttl::loop() { } if (this->samples_sent_ != this->samples_count_) { SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; - int x = 0; + uint16_t sample_index = 0; double rem = 0.0; while (true) { - // Try and send out the remainder of the existing note, one per loop() - - if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// + // Try and send out the remainder of the existing note, one per `loop()` + if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 - - sample[x].left = val; - sample[x].right = val; + int8_t val = (127 * this->gain_) * sin(deg2rad(rem)); + sample[sample_index].left = val; + sample[sample_index].right = val; } else { - sample[x].left = 0; - sample[x].right = 0; + sample[sample_index].left = 0; + sample[sample_index].right = 0; } - if (static_cast(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { + if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { break; } this->samples_sent_++; - x++; + sample_index++; } - if (x > 0) { - size_t bytes_to_send = x * sizeof(SpeakerSample); + if (sample_index > 0) { + size_t bytes_to_send = sample_index * sizeof(SpeakerSample); size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); if (send != bytes_to_send) { - this->samples_sent_ -= (x - (send / sizeof(SpeakerSample))); + this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample))); } return; } @@ -155,83 +161,84 @@ void Rtttl::loop() { return; } - // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { this->position_++; } - // first, get note duration, if available - uint8_t num = this->get_integer_(); + // First, get note duration, if available + uint8_t note_denominator = this->get_integer_(); - if (num) { - this->note_duration_ = this->wholenote_ / num; + if (note_denominator) { + this->note_duration_ = this->wholenote_duration_ / note_denominator; } else { - this->note_duration_ = - this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after + // We will need to check if we are a dotted note after + this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_; } - uint8_t note = note_index_from_char(this->rtttl_[this->position_]); + uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]); this->position_++; - // now, get optional '#' sharp + // Now, get optional '#' sharp if (this->rtttl_[this->position_] == '#') { - note++; + note_index_in_octave++; this->position_++; } - // now, get scale + // Now, get scale uint8_t scale = this->get_integer_(); if (scale == 0) { scale = this->default_octave_; } - if (scale < 4 || scale > 7) { - ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale); + if (scale < MIN_OCTAVE || scale > MAX_OCTAVE) { + ESP_LOGE(TAG, "Octave must be between %d and %d (it is %d)", MIN_OCTAVE, MAX_OCTAVE, scale); this->finish_(); return; } - // now, get optional '.' dotted note + // Now, get optional '.' dotted note if (this->rtttl_[this->position_] == '.') { - this->note_duration_ += this->note_duration_ / 2; + this->note_duration_ += this->note_duration_ / 2; // Duration +50% this->position_++; } - // Now play the note bool need_note_gap = false; - if (note) { - auto note_index = (scale - 4) * 12 + note; - if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) { - ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, - (int) (sizeof(NOTES) / sizeof(NOTES[0]))); + + // Now play the note + if (note_index_in_octave == 0) { + this->output_freq_ = 0; + ESP_LOGVV(TAG, "Waiting: %dms", this->note_duration_); + } else { + uint8_t note_index = (scale - MIN_OCTAVE) * SEMITONES_IN_OCTAVE + note_index_in_octave; + if (note_index >= NOTES_COUNT) { + ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note_index_in_octave, scale, + note_index, NOTES_COUNT); this->finish_(); return; } - auto freq = NOTES[note_index]; + uint16_t freq = NOTES[note_index]; need_note_gap = freq == this->output_freq_; // Add small silence gap between same note this->output_freq_ = freq; - ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_); - } else { - ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_); - this->output_freq_ = 0; + ESP_LOGVV(TAG, "Playing note: %d for %dms", note_index_in_octave, this->note_duration_); } #ifdef USE_OUTPUT if (this->output_ != nullptr) { - if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) { + if (this->output_freq_ == 0) { this->output_->set_level(0.0); - delay(DOUBLE_NOTE_GAP_MS); - this->note_duration_ -= DOUBLE_NOTE_GAP_MS; - } - if (this->output_freq_ != 0) { + } else { + if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) { + this->output_->set_level(0.0); + delay(REPEATING_NOTE_GAP_MS); + this->note_duration_ -= REPEATING_NOTE_GAP_MS; + } this->output_->update_frequency(this->output_freq_); this->output_->set_level(this->gain_); - } else { - this->output_->set_level(0.0); } } #endif // USE_OUTPUT @@ -241,28 +248,26 @@ void Rtttl::loop() { this->samples_sent_ = 0; this->samples_gap_ = 0; this->samples_per_wave_ = 0; - this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; + this->samples_count_ = (SAMPLE_RATE * this->note_duration_) / 1000; if (need_note_gap) { - this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; + this->samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000; } if (this->output_freq_ != 0) { - // make sure there is enough samples to add a full last sinus. - - uint16_t samples_wish = this->samples_count_; - this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; + // Make sure there is enough samples to add a full last sinus. + uint32_t samples_wish = this->samples_count_; + this->samples_per_wave_ = (SAMPLE_RATE << 10) / this->output_freq_; uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; - this->samples_count_ = (division * this->samples_per_wave_); - this->samples_count_ = this->samples_count_ >> 10; - ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, - division, this->samples_per_wave_); + this->samples_count_ = (division * this->samples_per_wave_) >> 10; + ESP_LOGVV(TAG, "Calc play time: wish: %" PRIu32 " gets: %" PRIu32 " (div: %d spw: %" PRIu32 ")", samples_wish, + this->samples_count_, division, this->samples_per_wave_); } // Convert from frequency in Hz to high and low samples in fixed point } #endif // USE_SPEAKER - this->last_note_ = millis(); + this->last_note_start_time_ = millis(); } void Rtttl::play(std::string rtttl) { @@ -275,25 +280,28 @@ void Rtttl::play(std::string rtttl) { this->rtttl_ = std::move(rtttl); - this->default_duration_ = 4; - this->default_octave_ = 6; + this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR; + this->default_octave_ = DEFAULT_OCTAVE; this->note_duration_ = 0; - int bpm = 63; - uint16_t num; + uint16_t bpm = DEFAULT_BPM; + uint16_t num; // Used for: default note-denominator, default octave, BPM // Get name this->position_ = this->rtttl_.find(':'); - // it's somewhat documented to be up to 10 characters but let's be a bit flexible here - if (this->position_ == std::string::npos || this->position_ > 15) { + if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Unable to determine name; missing ':'"); return; } - + if (this->position_ >= SONG_NAME_LENGTH_LIMIT) { + ESP_LOGE(TAG, "Name is too long: length=%u, limit=%u", static_cast(this->position_), + static_cast(SONG_NAME_LENGTH_LIMIT)); + return; + } ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); - // get default duration + // Get default duration this->position_ = this->rtttl_.find("d=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'd='"); @@ -301,11 +309,14 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num > 0) { - this->default_duration_ = num; + if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) { + this->default_note_denominator_ = num; + } else { + ESP_LOGE(TAG, "Invalid default duration: %d", num); + return; } - // get default octave + // Get default octave this->position_ = this->rtttl_.find("o=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing 'o="); @@ -313,11 +324,14 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num >= 3 && num <= 7) { + if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) { this->default_octave_ = num; + } else { + ESP_LOGE(TAG, "Invalid default octave: %d", num); + return; } - // get BPM + // Get BPM this->position_ = this->rtttl_.find("b=", this->position_); if (this->position_ == std::string::npos) { ESP_LOGE(TAG, "Missing b="); @@ -325,8 +339,11 @@ void Rtttl::play(std::string rtttl) { } this->position_ += 2; num = this->get_integer_(); - if (num != 0) { + if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow bpm = num; + } else { + ESP_LOGE(TAG, "Invalid BPM: %d", num); + return; } this->position_ = this->rtttl_.find(':', this->position_); @@ -337,10 +354,10 @@ void Rtttl::play(std::string rtttl) { this->position_++; // BPM usually expresses the number of quarter notes per minute - this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds) this->output_freq_ = 0; - this->last_note_ = millis(); + this->last_note_start_time_ = millis(); this->note_duration_ = 1; #ifdef USE_OUTPUT diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 4d4a652c517..e37cccae9ef 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -13,6 +13,10 @@ namespace esphome::rtttl { +inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note) +inline constexpr uint8_t DEFAULT_OCTAVE = + 6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`) + enum class State : uint8_t { STOPPED = 0, INIT, @@ -67,19 +71,18 @@ class Rtttl : public Component { std::string rtttl_{""}; /// The current position in the RTTTL string. size_t position_{0}; - /// The duration of a whole note in milliseconds. - uint16_t wholenote_; /// The default duration of a note (e.g. 4 for a quarter note). - uint16_t default_duration_; + uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR}; /// The default octave for a note. - uint16_t default_octave_; - /// The time the last note was started. - uint32_t last_note_; + uint8_t default_octave_{DEFAULT_OCTAVE}; /// The duration of the current note in milliseconds. - uint16_t note_duration_; - + uint16_t note_duration_{0}; + /// The duration of a whole note in milliseconds. + uint16_t wholenote_duration_; + /// The time in milliseconds since microcontroller boot when the last note was started. + uint32_t last_note_start_time_; /// The frequency of the current note in Hz. - uint32_t output_freq_; + uint32_t output_freq_{0}; /// The gain of the output. float gain_{0.6f}; /// The current state of the RTTTL player. @@ -93,16 +96,14 @@ class Rtttl : public Component { #ifdef USE_SPEAKER /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; - /// The sample rate of the speaker. - int sample_rate_{16000}; /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. - int samples_per_wave_{0}; + uint32_t samples_per_wave_{0}; /// The number of samples sent. - int samples_sent_{0}; + uint32_t samples_sent_{0}; /// The total number of samples to send. - int samples_count_{0}; + uint32_t samples_count_{0}; /// The number of samples for the gap between notes. - int samples_gap_{0}; + uint32_t samples_gap_{0}; #endif // USE_SPEAKER /// The callback to call when playback is finished. From 6d3d8970a67dfeed99e48e36388b319b40f3ad79 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:47:02 +0100 Subject: [PATCH 0309/2056] [openthread] Disable default enabled OT diag code (#14399) --- esphome/components/openthread/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 89d335c574e..dde4987181c 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -53,6 +53,9 @@ def set_sdkconfig_options(config): # There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli" add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) + # Diag unused, if needed for lab/cert/etc tests then enable separately + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False) + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) if tlv := config.get(CONF_TLV): From f68a3ed15d441a90cd96415af05cc69ad2ab5411 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:09:00 -1000 Subject: [PATCH 0310/2056] [api] Remove virtual destructor from ProtoMessage (#14393) --- esphome/components/api/api_pb2.h | 6 +++--- esphome/components/api/proto.h | 7 ++++++- script/api_protobuf/api_protobuf.py | 5 ++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 033b789c18d..a97f6c0a762 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -322,7 +322,6 @@ enum ZWaveProxyRequestType : uint32_t { class InfoResponseProtoMessage : public ProtoMessage { public: - ~InfoResponseProtoMessage() override = default; StringRef object_id{}; uint32_t key{0}; StringRef name{}; @@ -336,28 +335,29 @@ class InfoResponseProtoMessage : public ProtoMessage { #endif protected: + ~InfoResponseProtoMessage() = default; }; class StateResponseProtoMessage : public ProtoMessage { public: - ~StateResponseProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~StateResponseProtoMessage() = default; }; class CommandProtoMessage : public ProtoDecodableMessage { public: - ~CommandProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~CommandProtoMessage() = default; }; class HelloRequest final : public ProtoDecodableMessage { public: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index c34f7744e6b..750fff08102 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -452,7 +452,6 @@ class DumpBuffer { class ProtoMessage { public: - virtual ~ProtoMessage() = default; // Default implementation for messages with no fields virtual void encode(ProtoWriteBuffer &buffer) const {} // Default implementation for messages with no fields @@ -463,6 +462,11 @@ class ProtoMessage { virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif + + protected: + // Non-virtual: messages are never deleted polymorphically. + // Protected prevents accidental `delete base_ptr` (compile error). + ~ProtoMessage() = default; }; // Base class for messages that support decoding @@ -482,6 +486,7 @@ class ProtoDecodableMessage : public ProtoMessage { static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); protected: + ~ProtoDecodableMessage() = default; 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; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 350947a8d69..9c9cda4d36e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2473,9 +2473,6 @@ def build_base_class( out = f"class {base_class_name} : public {parent_class} {{\n" out += " public:\n" - # Add destructor with override - public_content.insert(0, f"~{base_class_name}() override = default;") - # Base classes don't implement encode/decode/calculate_size # Derived classes handle these with their specific field numbers cpp = "" @@ -2483,6 +2480,8 @@ def build_base_class( out += indent("\n".join(public_content)) + "\n" out += "\n" out += " protected:\n" + # Non-virtual protected destructor prevents accidental polymorphic deletion + protected_content.insert(0, f"~{base_class_name}() = default;") out += indent("\n".join(protected_content)) if protected_content: out += "\n" From 80a2acca4f576b5c43fa0a87b5752a68e1d85b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:19:32 -1000 Subject: [PATCH 0311/2056] [ld2410] Add UART mock integration test for LD2410 component (#14377) --- .../external_components/uart_mock/__init__.py | 86 ++++ .../uart_mock/uart_mock.cpp | 159 +++++++ .../external_components/uart_mock/uart_mock.h | 78 ++++ .../fixtures/uart_mock_ld2410.yaml | 145 +++++++ .../uart_mock_ld2410_engineering.yaml | 156 +++++++ tests/integration/test_uart_mock_ld2410.py | 392 ++++++++++++++++++ 6 files changed, 1016 insertions(+) create mode 100644 tests/integration/fixtures/external_components/uart_mock/__init__.py create mode 100644 tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp create mode 100644 tests/integration/fixtures/external_components/uart_mock/uart_mock.h create mode 100644 tests/integration/fixtures/uart_mock_ld2410.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2410_engineering.yaml create mode 100644 tests/integration/test_uart_mock_ld2410.py diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py new file mode 100644 index 00000000000..5bd879ab12b --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -0,0 +1,86 @@ +import esphome.codegen as cg +from esphome.components import uart +from esphome.components.uart import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS +import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, CONF_DATA, CONF_DELAY, CONF_ID, CONF_INTERVAL + +CODEOWNERS = ["@esphome/tests"] +MULTI_CONF = True + +uart_mock_ns = cg.esphome_ns.namespace("uart_mock") +MockUartComponent = uart_mock_ns.class_( + "MockUartComponent", uart.UARTComponent, cg.Component +) + +CONF_INJECTIONS = "injections" +CONF_RESPONSES = "responses" +CONF_INJECT_RX = "inject_rx" +CONF_EXPECT_TX = "expect_tx" +CONF_PERIODIC_RX = "periodic_rx" + +UART_PARITY_OPTIONS = { + "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, + "EVEN": uart.UARTParityOptions.UART_CONFIG_PARITY_EVEN, + "ODD": uart.UARTParityOptions.UART_CONFIG_PARITY_ODD, +} + +INJECTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds, + } +) + +RESPONSE_SCHEMA = cv.Schema( + { + cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], + cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + } +) + +PERIODIC_RX_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): [cv.hex_uint8_t], + cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MockUartComponent), + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), + cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum( + UART_PARITY_OPTIONS, upper=True + ), + cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), + cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), + cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) + cg.add(var.set_data_bits(config[CONF_DATA_BITS])) + cg.add(var.set_parity(config[CONF_PARITY])) + + for injection in config[CONF_INJECTIONS]: + rx_data = injection[CONF_INJECT_RX] + delay_ms = injection[CONF_DELAY] + cg.add(var.add_injection(rx_data, delay_ms)) + + for response in config[CONF_RESPONSES]: + tx_data = response[CONF_EXPECT_TX] + rx_data = response[CONF_INJECT_RX] + cg.add(var.add_response(tx_data, rx_data)) + + for periodic in config[CONF_PERIODIC_RX]: + data = periodic[CONF_DATA] + interval = periodic[CONF_INTERVAL] + cg.add(var.add_periodic_rx(data, interval)) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp new file mode 100644 index 00000000000..a49d51ba10c --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -0,0 +1,159 @@ +// Host-only test component — do not copy to production code. +// See uart_mock.h for details. + +#include "uart_mock.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::uart_mock { + +static const char *const TAG = "uart_mock"; + +void MockUartComponent::setup() { + ESP_LOGI(TAG, "Mock UART initialized with %zu injections, %zu responses, %zu periodic", this->injections_.size(), + this->responses_.size(), this->periodic_rx_.size()); +} + +void MockUartComponent::loop() { + uint32_t now = App.get_loop_component_start_time(); + + // Initialize scenario start time on first loop() call, after all components have + // finished setup(). This prevents injection delays from being consumed during setup. + if (!this->loop_started_) { + this->loop_started_ = true; + this->scenario_start_ms_ = now; + this->cumulative_delay_ms_ = 0; + ESP_LOGD(TAG, "Scenario started at %u ms", now); + } + + // Process at most ONE timed injection per loop iteration. + // This ensures each injection is in a separate loop cycle, giving the consuming + // component (e.g., LD2410) a chance to process each batch independently. + if (this->injection_index_ < this->injections_.size()) { + auto &injection = this->injections_[this->injection_index_]; + uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; + if (now >= target_time) { + ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); + this->inject_to_rx_buffer_(injection.rx_data); + this->cumulative_delay_ms_ += injection.delay_ms; + this->injection_index_++; + } + } + + // Process periodic RX + for (auto &periodic : this->periodic_rx_) { + if (now - periodic.last_inject_ms >= periodic.interval_ms) { + this->inject_to_rx_buffer_(periodic.data); + periodic.last_inject_ms = now; + } + } +} + +void MockUartComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Mock UART Component:\n" + " Baud Rate: %u\n" + " Injections: %zu\n" + " Responses: %zu\n" + " Periodic RX: %zu", + this->baud_rate_, this->injections_.size(), this->responses_.size(), this->periodic_rx_.size()); +} + +void MockUartComponent::write_array(const uint8_t *data, size_t len) { + this->tx_count_ += len; + this->tx_buffer_.insert(this->tx_buffer_.end(), data, data + len); + + // Log all TX data so tests can verify what the component sends + if (len > 0 && len <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "TX %zu bytes: %s", len, format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, len)); + } else if (len > 64) { + ESP_LOGD(TAG, "TX %zu bytes (too large to log)", len); + } + +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif + + this->try_match_response_(); +} + +bool MockUartComponent::peek_byte(uint8_t *data) { + if (this->rx_buffer_.empty()) { + return false; + } + *data = this->rx_buffer_.front(); + return true; +} + +bool MockUartComponent::read_array(uint8_t *data, size_t len) { + if (this->rx_buffer_.size() < len) { + return false; + } + for (size_t i = 0; i < len; i++) { + data[i] = this->rx_buffer_.front(); + this->rx_buffer_.pop_front(); + } + this->rx_count_ += len; + +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + + return true; +} + +size_t MockUartComponent::available() { return this->rx_buffer_.size(); } + +void MockUartComponent::flush() { + // Nothing to flush in mock +} + +void MockUartComponent::add_injection(const std::vector &rx_data, uint32_t delay_ms) { + this->injections_.push_back({rx_data, delay_ms}); +} + +void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx) { + this->responses_.push_back({expect_tx, inject_rx}); +} + +void MockUartComponent::add_periodic_rx(const std::vector &data, uint32_t interval_ms) { + this->periodic_rx_.push_back({data, interval_ms, 0}); +} + +void MockUartComponent::try_match_response_() { + for (auto &response : this->responses_) { + if (this->tx_buffer_.size() < response.expect_tx.size()) { + continue; + } + // Check if tx_buffer_ ends with expect_tx + size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); + if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { + ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); + this->inject_to_rx_buffer_(response.inject_rx); + this->tx_buffer_.clear(); + return; + } + } +} + +void MockUartComponent::inject_to_rx_buffer_(const std::vector &data) { + // Log injected RX data so tests can see what's being fed to the component + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "RX inject %zu bytes: %s", data.size(), + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "RX inject %zu bytes (too large to log inline)", data.size()); + } + for (uint8_t byte : data) { + this->rx_buffer_.push_back(byte); + } +} + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h new file mode 100644 index 00000000000..00f4bf2e2bf --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -0,0 +1,78 @@ +#pragma once + +// ============================================================================ +// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE +// +// This component runs exclusively on the host platform for integration testing. +// It intentionally uses std::vector, std::deque, and dynamic allocation which +// would be inappropriate for production embedded components. Do not use this +// code as a reference for writing ESPHome components targeting real hardware. +// ============================================================================ + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" +#include +#include + +namespace esphome::uart_mock { + +class MockUartComponent : public uart::UARTComponent, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + // UARTComponent interface + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + void flush() override; + + // Scenario configuration - called from generated code + void add_injection(const std::vector &rx_data, uint32_t delay_ms); + void add_response(const std::vector &expect_tx, const std::vector &inject_rx); + void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + + protected: + void check_logger_conflict() override {} + void try_match_response_(); + void inject_to_rx_buffer_(const std::vector &data); + + // Timed injections + struct Injection { + std::vector rx_data; + uint32_t delay_ms; + }; + std::vector injections_; + uint32_t injection_index_{0}; + uint32_t scenario_start_ms_{0}; + uint32_t cumulative_delay_ms_{0}; + bool loop_started_{false}; + + // TX-triggered responses + struct Response { + std::vector expect_tx; + std::vector inject_rx; + }; + std::vector responses_; + std::vector tx_buffer_; + + // RX buffer + std::deque rx_buffer_; + + // Periodic RX + struct PeriodicRx { + std::vector data; + uint32_t interval_ms; + uint32_t last_inject_ms{0}; + }; + std::vector periodic_rx_; + + // Observability + uint32_t tx_count_{0}; + uint32_t rx_count_{0}; +}; + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml new file mode 100644 index 00000000000..9a814682630 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -0,0 +1,145 @@ +esphome: + name: uart-mock-ld2410-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2410 normal mode data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # Moving target: 100cm, energy 50 + # Still target: 120cm, energy 25 + # Detection distance: 300cm + # Target state: 0x03 (moving + still) + # + # Note: LD2410's two_byte_to_int() uses signed char, so low bytes must be + # <=127 to produce correct values. Values >127 wrap negative. + # + # Frame layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0D 00 = length 13 + # [6] 02 = data type (normal) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 64 00 = moving distance 100 (0x0064) + # [11] 32 = moving energy 50 + # [12-13] 78 00 = still distance 120 (0x0078) + # [14] 19 = still energy 25 + # [15-16] 2C 01 = detection distance 300 (0x012C) + # [17] 00 = padding + # [18] 55 = data footer marker + # [19] 00 = CRC/check + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x64, 0x00, + 0x32, + 0x78, 0x00, + 0x19, + 0x2C, 0x01, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Garbage bytes - corrupt the buffer + # These random bytes will accumulate in the LD2410's internal buffer. + # No footer will be found, so the buffer just grows. + - delay: 100ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=300ms): Truncated frame (header + partial data, no footer) + # More bytes accumulating in the buffer without a footer match. + - delay: 100ms + inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA] + + # Phase 4 (t=500ms): Overflow - inject 85 bytes of 0xFF (MAX_LINE_LENGTH=50) + # Buffer has 15 bytes from phases 2+3. + # Overflow math: need 35 bytes to trigger first overflow (pos 15->49), + # then 50 more to trigger second overflow (pos 0->49). Total = 85 bytes. + # After two overflows, buffer_pos_ = 0 with a completely clean buffer. + # The LD2410 logs "Max command length exceeded" at each overflow. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=600ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Moving target: 50cm, energy 100 + # Still target: 75cm, energy 80 + # Detection distance: 127cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x32, 0x00, + 0x64, + 0x4B, 0x00, + 0x50, + 0x7F, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2410: + id: ld2410_dev + uart_id: mock_uart + +sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + moving_distance: + name: "Moving Distance" + still_distance: + name: "Still Distance" + moving_energy: + name: "Moving Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Detection Distance" + +binary_sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + has_target: + name: "Has Target" + has_moving_target: + name: "Has Moving Target" + has_still_target: + name: "Has Still Target" diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml new file mode 100644 index 00000000000..3b730fc1f8b --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -0,0 +1,156 @@ +esphome: + name: uart-mock-ld2410-eng-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2410's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2410 engineering mode data frame + # Captured from a real Screek Human Presence Sensor 1U with LD2410 firmware 2.4.x + # + # Engineering mode frame layout (45 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 23 00 = length 35 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 (0x001E) + # [11] 64 = moving energy 100 + # [12-13] 1E 00 = still distance 30 (0x001E) + # [14] 64 = still energy 100 + # [15-16] 00 00 = detection distance 0 + # [17] 08 = max moving distance gate + # [18] 08 = max still distance gate + # [19-27] gate moving energies (gates 0-8) + # [28-36] gate still energies (gates 0-8) + # [37] 57 = light sensor value 87 + # [38] 01 = out pin presence (HIGH) + # [39] 55 = data footer marker + # [40] 00 = check + # [41-44] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x08, 0x08, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Second engineering mode frame with different values + # Real capture: moving at 73cm, still at 30cm, detection at 33cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x49, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x21, 0x00, + 0x08, 0x08, + 0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance) + # This tests the two_byte_to_int function with high byte > 0 + # Note: low byte 0x2F < 0x80 so it avoids the signed char bug + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, 0xAA, + 0x03, + 0x2F, 0x00, + 0x36, + 0x23, 0x01, + 0x64, + 0x21, 0x00, + 0x08, 0x08, + 0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D, + 0x57, 0x01, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2410: + id: ld2410_dev + uart_id: mock_uart + +sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + moving_distance: + name: "Moving Distance" + still_distance: + name: "Still Distance" + moving_energy: + name: "Moving Energy" + still_energy: + name: "Still Energy" + detection_distance: + name: "Detection Distance" + light: + name: "Light" + g0: + move_energy: + name: "Gate 0 Move Energy" + still_energy: + name: "Gate 0 Still Energy" + g1: + move_energy: + name: "Gate 1 Move Energy" + still_energy: + name: "Gate 1 Still Energy" + g2: + move_energy: + name: "Gate 2 Move Energy" + still_energy: + name: "Gate 2 Still Energy" + +binary_sensor: + - platform: ld2410 + ld2410_id: ld2410_dev + has_target: + name: "Has Target" + has_moving_target: + name: "Has Moving Target" + has_still_target: + name: "Has Still Target" + out_pin_presence_status: + name: "Out Pin Presence" diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py new file mode 100644 index 00000000000..e01d6ff8e82 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2410.py @@ -0,0 +1,392 @@ +"""Integration test for LD2410 component with mock UART. + +Tests: +test_uart_mock_ld2410 (normal mode): + 1. Happy path - valid data frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated frame handling - partial frame doesn't corrupt state + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2410 sends expected setup commands + +test_uart_mock_ld2410_engineering (engineering mode): + 1. Engineering mode frames with per-gate energy data and light sensor + 2. Multi-byte still distance (291cm) using high byte > 0 + 3. Out pin presence binary sensor + 4. Gate energy sensor values from real device captures +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ( + BinarySensorInfo, + BinarySensorState, + EntityState, + SensorInfo, + SensorState, +) +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2410( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2410 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see recovery frame values + recovery_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is the recovery frame (moving_distance = 50) + if ( + sensor_name == "moving_distance" + and state.state == pytest.approx(50.0) + and not recovery_received.done() + ): + recovery_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 values are in the initial states (swallowed by InitialStateHelper). + # Verify them via initial_states dict. + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(100.0), ( + f"Initial moving distance should be 100, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(120.0), ( + f"Initial still distance should be 120, got {initial_still.state}" + ) + + moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) + assert moving_energy_entity is not None + initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) + assert initial_me is not None and isinstance(initial_me, SensorState) + assert initial_me.state == pytest.approx(50.0), ( + f"Initial moving energy should be 50, got {initial_me.state}" + ) + + still_energy_entity = find_entity(entities, "still_energy", SensorInfo) + assert still_energy_entity is not None + initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) + assert initial_se is not None and isinstance(initial_se, SensorState) + assert initial_se.state == pytest.approx(25.0), ( + f"Initial still energy should be 25, got {initial_se.state}" + ) + + detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) + assert detect_dist_entity is not None + initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) + assert initial_dd is not None and isinstance(initial_dd, SensorState) + assert initial_dd.state == pytest.approx(300.0), ( + f"Initial detection distance should be 300, got {initial_dd.state}" + ) + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received sensor states:\n" + f" moving_distance: {sensor_states['moving_distance']}\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_energy: {sensor_states['moving_energy']}\n" + f" still_energy: {sensor_states['still_energy']}\n" + f" detection_distance: {sensor_states['detection_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2410 sent setup commands (TX logging) + # LD2410 sends 7 commands during setup: FF (config on), A0 (version), + # A5 (MAC), AB (distance res), AE (light), 61 (params), FE (config off) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2410 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2410 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + assert len(sensor_states["moving_distance"]) >= 1, ( + f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" + ) + # Find the recovery value (moving_distance = 50) + recovery_values = [ + v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" + ) + + # Recovery frame: moving=50, still=75, energy=100/80, detect=127 + recovery_idx = next( + i + for i, v in enumerate(sensor_states["moving_distance"]) + if v == pytest.approx(50.0) + ) + assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( + f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + ) + assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( + f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + ) + assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( + f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" + ) + assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( + 127.0 + ), ( + f"Recovery detection distance should be 127, got {sensor_states['detection_distance'][recovery_idx]}" + ) + + # Verify binary sensors detected targets + # Binary sensors could be in initial states or forwarded states + has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) + assert has_target_entity is not None + initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) + assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) + assert initial_ht.state is True, "Has target should be True" + + has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) + assert has_moving_entity is not None + initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) + assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) + assert initial_hm.state is True, "Has moving target should be True" + + has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) + assert has_still_entity is not None + initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) + assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) + assert initial_hs.state is True, "Has still target should be True" + + +@pytest.mark.asyncio +async def test_uart_mock_ld2410_engineering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2410 engineering mode with per-gate energy, light, and multi-byte distance.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + "light": [], + "gate_0_move_energy": [], + "gate_1_move_energy": [], + "gate_2_move_energy": [], + "gate_0_still_energy": [], + "gate_1_still_energy": [], + "gate_2_still_energy": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + "out_pin_presence": [], + } + + # Signal when we see Phase 3 frame (still_distance = 291) + phase3_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "still_distance" + and state.state == pytest.approx(291.0) + and not phase3_received.done() + ): + phase3_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 initial values (engineering mode frame): + # moving=30, energy=100, still=30, energy=100, detect=0 + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(30.0), ( + f"Initial moving distance should be 30, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(30.0), ( + f"Initial still distance should be 30, got {initial_still.state}" + ) + + # Verify engineering mode sensors from initial state + # Gate 0 moving energy = 0x64 = 100 + gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) + assert gate0_move_entity is not None + initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) + assert initial_g0m is not None and isinstance(initial_g0m, SensorState) + assert initial_g0m.state == pytest.approx(100.0), ( + f"Gate 0 move energy should be 100, got {initial_g0m.state}" + ) + + # Gate 1 moving energy = 0x41 = 65 + gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) + assert gate1_move_entity is not None + initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) + assert initial_g1m is not None and isinstance(initial_g1m, SensorState) + assert initial_g1m.state == pytest.approx(65.0), ( + f"Gate 1 move energy should be 65, got {initial_g1m.state}" + ) + + # Light sensor = 0x57 = 87 + light_entity = find_entity(entities, "light", SensorInfo) + assert light_entity is not None + initial_light = initial_state_helper.initial_states.get(light_entity.key) + assert initial_light is not None and isinstance(initial_light, SensorState) + assert initial_light.state == pytest.approx(87.0), ( + f"Light sensor should be 87, got {initial_light.state}" + ) + + # Out pin presence = 0x01 = True + out_pin_entity = find_entity(entities, "out_pin_presence", BinarySensorInfo) + assert out_pin_entity is not None + initial_out = initial_state_helper.initial_states.get(out_pin_entity.key) + assert initial_out is not None and isinstance(initial_out, BinarySensorState) + assert initial_out.state is True, "Out pin presence should be True" + + # Wait for Phase 3 frame (still_distance = 291cm, multi-byte) + try: + await asyncio.wait_for(phase3_received, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 3 frame. Received sensor states:\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_distance: {sensor_states['moving_distance']}" + ) + + # Phase 3: still distance = 0x0123 = 291cm (multi-byte distance test) + phase3_still = [ + v for v in sensor_states["still_distance"] if v == pytest.approx(291.0) + ] + assert len(phase3_still) >= 1, ( + f"Expected still_distance=291, got: {sensor_states['still_distance']}" + ) From 9d4357c619ee94ae9a934276525e1bfa0ac99b8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 18:20:14 -1000 Subject: [PATCH 0312/2056] [core] Wake main loop from ISR in enable_loop_soon_any_context() (#14383) --- esphome/components/socket/__init__.py | 7 ++++--- esphome/core/application.h | 8 +++++++- esphome/core/component.cpp | 8 +++++++- esphome/core/lwip_fast_select.c | 24 +++++++++++++++++++----- esphome/core/lwip_fast_select.h | 7 +++++++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index a83648979c7..08cf3ea33c3 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -189,9 +189,10 @@ async def to_code(config): cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() - # and FreeRTOS task notifications — enable fast select to bypass lwip_select() - if CORE.is_esp32 or CORE.is_libretiny: - cg.add_define("USE_LWIP_FAST_SELECT") + # and FreeRTOS task notifications — enable fast select to bypass lwip_select(). + # Only when not using lwip_tcp, which does not provide select() support. + if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: + cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/core/application.h b/esphome/core/application.h index 13e0f638856..d2345e2b0b6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -501,7 +501,7 @@ class Application { void wake_loop_threadsafe(); #endif -#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT /// Wake the main event loop from an ISR. /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. /// Only available on platforms with fast select (ESP32, LibreTiny). @@ -509,6 +509,12 @@ class Application { static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); } + +#ifdef USE_ESP32 + /// Wake the main event loop from any context (ISR, thread, or main loop). + /// Detects the calling context and uses the appropriate FreeRTOS API. + static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } +#endif #endif #endif diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index fd4e0d29840..5afd901da23 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -315,7 +315,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted - // 3. No memory allocation, object construction, or function calls + // 3. No memory allocation or object construction; on ESP32 the only call (wake_loop_any_context) is ISR-safe // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) // 5. Components are never destroyed, so no use-after-free concerns // 6. App is guaranteed to be initialized before any ISR could fire @@ -323,6 +323,12 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; +#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) + // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, + // the main loop would not wake until the select timeout expires (~16ms). + // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. + Application::wake_loop_any_context(); +#endif } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index da0f1f337a4..989f66e9be9 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -105,10 +105,9 @@ // critical sections). Multiple concurrent xTaskNotifyGive calls are safe — // the notification count simply increments. -// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file. -// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here, -// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT. -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// USE_LWIP_FAST_SELECT is set via -D build flag (not cg.add_define) so it is +// visible in both .c and .cpp translation units. +#ifdef USE_LWIP_FAST_SELECT // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include @@ -239,4 +238,19 @@ void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task } } -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +// Wake the main loop from any context (ISR, thread, or main loop). +// ESP32-only: uses xPortInIsrContext() to detect ISR context. +// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. +#ifdef USE_ESP32 +void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_lwip_wake_main_loop(); + } +} +#endif + +#endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index b7a70a8f9f2..6fce34fd76d 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -33,6 +33,13 @@ void esphome_lwip_wake_main_loop(void); /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); +/// Wake the main loop task from any context (ISR, thread, or main loop). +/// ESP32-only: uses xPortInIsrContext() to detect ISR context. +/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. +#ifdef USE_ESP32 +void esphome_lwip_wake_main_loop_any_context(void); +#endif + #ifdef __cplusplus } #endif From 590ee81f7a13356c75d5cafa2436771fe4b80f53 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 2 Mar 2026 07:16:36 +0100 Subject: [PATCH 0313/2056] [openthread] Disable default enabled OT console build (#14390) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/openthread/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index dde4987181c..5861c3db3f6 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -52,6 +52,8 @@ def set_sdkconfig_options(config): # There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli" add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) + # Console is the transport layer for CLI; disable it too since CLI is disabled + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CONSOLE_ENABLE", False) # Diag unused, if needed for lab/cert/etc tests then enable separately add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DIAG", False) From 3160457ca620adfbe48980af20d89a5c062462ee Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Mon, 2 Mar 2026 00:51:27 -0800 Subject: [PATCH 0314/2056] Create integration tests for modbus (#14395) Co-authored-by: J. Nick Koston --- .../external_components/uart_mock/__init__.py | 83 +++++++++- .../uart_mock/automation.h | 51 ++++++ .../uart_mock/uart_mock.cpp | 25 ++- .../external_components/uart_mock/uart_mock.h | 10 +- .../fixtures/uart_mock_modbus.yaml | 40 +++++ .../fixtures/uart_mock_modbus_timing.yaml | 54 +++++++ tests/integration/test_uart_mock_modbus.py | 153 ++++++++++++++++++ 7 files changed, 409 insertions(+), 7 deletions(-) create mode 100644 tests/integration/fixtures/external_components/uart_mock/automation.h create mode 100644 tests/integration/fixtures/uart_mock_modbus.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_timing.yaml create mode 100644 tests/integration/test_uart_mock_modbus.py diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index 5bd879ab12b..dea8c385513 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -1,8 +1,28 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import uart -from esphome.components.uart import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS +from esphome.components.uart import ( + CONF_RX_FULL_THRESHOLD, + CONF_RX_TIMEOUT, + debug_to_code, + maybe_empty_debug, + validate_raw_data, +) import esphome.config_validation as cv -from esphome.const import CONF_BAUD_RATE, CONF_DATA, CONF_DELAY, CONF_ID, CONF_INTERVAL +from esphome.const import ( + CONF_BAUD_RATE, + CONF_DATA, + CONF_DATA_BITS, + CONF_DEBUG, + CONF_DELAY, + CONF_ID, + CONF_INTERVAL, + CONF_PARITY, + CONF_RX_BUFFER_SIZE, + CONF_STOP_BITS, + CONF_TRIGGER_ID, +) +from esphome.core import ID CODEOWNERS = ["@esphome/tests"] MULTI_CONF = True @@ -11,12 +31,20 @@ uart_mock_ns = cg.esphome_ns.namespace("uart_mock") MockUartComponent = uart_mock_ns.class_( "MockUartComponent", uart.UARTComponent, cg.Component ) +MockUartInjectRXAction = uart_mock_ns.class_( + "MockUartInjectRXAction", automation.Action +) +MockUartTXTrigger = uart_mock_ns.class_( + "MockUartTXTrigger", + automation.Trigger.template(cg.std_vector.template(cg.uint8)), +) CONF_INJECTIONS = "injections" CONF_RESPONSES = "responses" CONF_INJECT_RX = "inject_rx" CONF_EXPECT_TX = "expect_tx" CONF_PERIODIC_RX = "periodic_rx" +CONF_ON_TX = "on_tx" UART_PARITY_OPTIONS = { "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, @@ -31,6 +59,15 @@ INJECTION_SCHEMA = cv.Schema( } ) +CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MockUartComponent), + cv.Required("data"): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + RESPONSE_SCHEMA = cv.Schema( { cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], @@ -49,6 +86,9 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(MockUartComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, + cv.Optional(CONF_RX_FULL_THRESHOLD, default=10): cv.int_range(min=1, max=120), + cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -57,15 +97,45 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + cv.Optional(CONF_ON_TX): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MockUartTXTrigger), + } + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA) +@automation.register_action( + "uart_mock.inject_rx", MockUartInjectRXAction, CONFIG_INJECT_RX_SCHEMA +) +async def inject_rx_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) + cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) @@ -84,3 +154,12 @@ async def to_code(config): data = periodic[CONF_DATA] interval = periodic[CONF_INTERVAL] cg.add(var.add_periodic_rx(data, interval)) + + for conf in config.get(CONF_ON_TX, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.std_vector.template(cg.uint8), "data")], conf + ) + + if CONF_DEBUG in config: + await debug_to_code(config[CONF_DEBUG], var) diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h new file mode 100644 index 00000000000..83a057d3a05 --- /dev/null +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "uart_mock.h" + +namespace esphome::uart_mock { + +// This pattern is similar to UARTWriteAction but calls inject_rx instead of write_array, and is parented to VirtualUART +// instead of UARTComponent +template class MockUartInjectRXAction : public Action, public Parented { + public: + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + } else { + // Template mode: call function + auto val = this->code_.func(x...); + this->parent_->inject_to_rx_buffer(val); + } + } + + protected: + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } code_; +}; + +class MockUartTXTrigger : public Trigger> { + public: + explicit MockUartTXTrigger(MockUartComponent *parent) { + parent->set_tx_hook([this](std::vector data) { this->trigger(data); }); + } +}; + +} // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index a49d51ba10c..a4a1c41234c 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -35,7 +35,7 @@ void MockUartComponent::loop() { uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; if (now >= target_time) { ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); - this->inject_to_rx_buffer_(injection.rx_data); + this->inject_to_rx_buffer(injection.rx_data); this->cumulative_delay_ms_ += injection.delay_ms; this->injection_index_++; } @@ -44,7 +44,7 @@ void MockUartComponent::loop() { // Process periodic RX for (auto &periodic : this->periodic_rx_) { if (now - periodic.last_inject_ms >= periodic.interval_ms) { - this->inject_to_rx_buffer_(periodic.data); + this->inject_to_rx_buffer(periodic.data); periodic.last_inject_ms = now; } } @@ -79,6 +79,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { #endif this->try_match_response_(); + + // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. + if (this->tx_hook_) { + std::vector buf(data, data + len); + this->tx_hook_(buf); + } } bool MockUartComponent::peek_byte(uint8_t *data) { @@ -114,6 +120,12 @@ void MockUartComponent::flush() { // Nothing to flush in mock } +void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { + this->rx_full_threshold_ = rx_full_threshold; +} + +void MockUartComponent::set_rx_timeout(size_t rx_timeout) { this->rx_timeout_ = rx_timeout; } + void MockUartComponent::add_injection(const std::vector &rx_data, uint32_t delay_ms) { this->injections_.push_back({rx_data, delay_ms}); } @@ -135,14 +147,19 @@ void MockUartComponent::try_match_response_() { size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); - this->inject_to_rx_buffer_(response.inject_rx); + this->inject_to_rx_buffer(response.inject_rx); this->tx_buffer_.clear(); return; } } } -void MockUartComponent::inject_to_rx_buffer_(const std::vector &data) { +void MockUartComponent::inject_to_rx_buffer(const uint8_t *data, size_t len) { + std::vector vec(data, data + len); + this->inject_to_rx_buffer(vec); +} + +void MockUartComponent::inject_to_rx_buffer(const std::vector &data) { // Log injected RX data so tests can see what's being fed to the component if (!data.empty() && data.size() <= 64) { char hex_buf[format_hex_pretty_size(64)]; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 00f4bf2e2bf..5bbc3c1bf60 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -29,16 +29,21 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; void flush() override; + void set_rx_full_threshold(size_t rx_full_threshold) override; + void set_rx_timeout(size_t rx_timeout) override; // Scenario configuration - called from generated code void add_injection(const std::vector &rx_data, uint32_t delay_ms); void add_response(const std::vector &expect_tx, const std::vector &inject_rx); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } + void inject_to_rx_buffer(const std::vector &data); + void inject_to_rx_buffer(const uint8_t *data, size_t len); + protected: void check_logger_conflict() override {} void try_match_response_(); - void inject_to_rx_buffer_(const std::vector &data); // Timed injections struct Injection { @@ -73,6 +78,9 @@ class MockUartComponent : public uart::UARTComponent, public Component { // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; + + // Direct TX hook for tests that want to bypass the response-matching logic + std::function &)> tx_hook_; }; } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml new file mode 100644 index 00000000000..89b9b91861f --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -0,0 +1,40 @@ +esphome: + name: uart-mock-modbus-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + debug: + responses: + - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 + inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec) + +modbus: + uart_id: virtual_uart_dev + +modbus_controller: + address: 1 + +sensor: + - platform: modbus_controller + name: "basic_register" + address: 0x03 + register_type: holding diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml new file mode 100644 index 00000000000..cd485ca3944 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -0,0 +1,54 @@ +esphome: + name: uart-mock-modbus-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + debug: + on_tx: + - then: + - if: + condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) + lambda: "return data == std::vector({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" + then: + - uart_mock.inject_rx: # Good response from SDM meter with CRC. Voltage is 243V + !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + - delay: 120ms # Simulate some delay for UART buffer to fill before next part of response (1ms = 1 byte at 9600 baud) + - uart_mock.inject_rx: # Rest of response (including CRC) + !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + +modbus: + uart_id: virtual_uart_dev + +sensor: + - platform: sdm_meter + address: 2 + phase_a: + voltage: + name: sdm_voltage diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py new file mode 100644 index 00000000000..309cb56dc99 --- /dev/null +++ b/tests/integration/test_uart_mock_modbus.py @@ -0,0 +1,153 @@ +"""Integration test for modbus component with virtual UART. + +Tests: +test_uart_mock_modbus : + 1. Read a single register and parse successfully (basic_register) + 2. Read multiple registers from SDM meter and parse successfully (sdm_voltage), with some intermediate delay to simulate UART buffer time. + +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_modbus( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test basic modbus data parsing.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "basic_register": [], + } + + basic_register_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "basic_register" + and state.state == 259.0 + and not basic_register_changed.done() + ): + basic_register_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Wait for basic register to be updated with successful parse + try: + await asyncio.wait_for(basic_register_changed, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Basic Register change. Received sensor states:\n" + f" basic_register: {sensor_states['basic_register']}\n" + ) + + +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="There is a bug in UART which will timeout for long responses." +) +async def test_uart_mock_modbus_timing( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test basic modbus data parsing.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "sdm_voltage": [], + } + + voltage_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is a good voltage reading (243V) + if ( + sensor_name == "sdm_voltage" + and state.state > 200.0 + and not voltage_changed.done() + ): + voltage_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Wait for voltage to be updated with successful parse + try: + await asyncio.wait_for(voltage_changed, timeout=15.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for SDM voltage change. Received sensor states:\n" + f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + ) From 77a7cbcffd4b8ec1fa3bc77b32f9ef06dc96761d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:41:20 -0500 Subject: [PATCH 0315/2056] Use cached files on network errors in external_files (#14055) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com> --- esphome/external_files.py | 15 +++++++-- tests/unit_tests/test_external_files.py | 44 +++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/esphome/external_files.py b/esphome/external_files.py index 80b54ebb2f1..72a3f33fdc8 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -55,10 +55,12 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: _LOGGER.debug("has_remote_file_changed: File modified") return True except requests.exceptions.RequestException as e: - raise cv.Invalid( - f"Could not check if {url} has changed, please check if file exists " - f"({e})" + _LOGGER.warning( + "Could not check if %s has changed due to network error (%s), using cached file", + url, + e, ) + return False _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) return True @@ -98,6 +100,13 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: ) req.raise_for_status() except requests.exceptions.RequestException as e: + if path.exists(): + _LOGGER.warning( + "Could not download from %s due to network error (%s), using cached file", + url, + e, + ) + return path.read_bytes() raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index 05e0bd35239..a319fae83d4 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -144,16 +144,16 @@ def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: def test_has_remote_file_changed_network_error( mock_head: MagicMock, setup_core: Path ) -> None: - """Test has_remote_file_changed handles network errors gracefully.""" + """Test has_remote_file_changed returns False on network error when file is cached.""" test_file = setup_core / "cached.txt" test_file.write_text("cached content") mock_head.side_effect = requests.exceptions.RequestException("Network error") url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) - with pytest.raises(Invalid, match="Could not check if.*Network error"): - external_files.has_remote_file_changed(url, test_file) + assert result is False @patch("esphome.external_files.requests.head") @@ -198,3 +198,41 @@ def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: result = external_files.is_file_recent(test_file, refresh) assert result is True + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_with_network_error_uses_cache( + mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path +) -> None: + """Test download_content uses cached file when network fails.""" + test_file = setup_core / "cached.txt" + cached_content = b"cached content" + test_file.write_bytes(cached_content) + + # Simulate file has changed, so it tries to download + mock_has_changed.return_value = True + mock_get.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + result = external_files.download_content(url, test_file) + + assert result == cached_content + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_with_network_error_no_cache_fails( + mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path +) -> None: + """Test download_content raises error when network fails and no cache exists.""" + test_file = setup_core / "nonexistent.txt" + + # Simulate file has changed (doesn't exist), so it tries to download + mock_has_changed.return_value = True + mock_get.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + + with pytest.raises(Invalid, match="Could not download from.*Network error"): + external_files.download_content(url, test_file) From 1c5fd8bbd46fca739ec2bf7743f87ea433c98ad6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:56:51 -1000 Subject: [PATCH 0316/2056] [core] Move millis_64 rollover tracking out of Scheduler (#14360) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 1 + esphome/components/esp8266/core.cpp | 4 +- esphome/components/host/__init__.py | 1 + esphome/components/libretiny/core.cpp | 4 +- esphome/components/rp2040/__init__.py | 1 + esphome/components/zephyr/__init__.py | 1 + esphome/core/config.py | 6 + esphome/core/defines.h | 5 + esphome/core/scheduler.cpp | 182 +--------------------- esphome/core/scheduler.h | 47 +----- esphome/core/time_64.cpp | 207 ++++++++++++++++++++++++++ esphome/core/time_64.h | 24 +++ 12 files changed, 257 insertions(+), 226 deletions(-) create mode 100644 esphome/core/time_64.cpp create mode 100644 esphome/core/time_64.h diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index dd9e394fd2a..a14d3af69ef 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1438,6 +1438,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 497e99b61f1..7136cf41cb3 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,7 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" +#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -17,7 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return Millis64Impl::compute(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index ba05e497c8b..8adbfb02ec5 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 6cbc81938d9..88952692a73 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,7 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/application.h" +#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -14,7 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } +uint64_t millis_64() { return Millis64Impl::compute(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 23f12e651f3..ea269a47c58 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -169,6 +169,7 @@ async def to_code(config): cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 43d5cebebb2..4cc71bddca8 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -112,6 +112,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: def zephyr_to_code(config): cg.add_build_flag("-DUSE_ZEPHYR") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") # build is done by west so bypass board checking in platformio cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) diff --git a/esphome/core/config.py b/esphome/core/config.py index 215432835a0..593b4022303 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -650,6 +650,12 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "time_64.cpp": { + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered # as they are only included when needed by the preprocessor } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 181425c1625..1f2597dd984 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -178,6 +178,11 @@ #define USE_I2S_LEGACY #endif +// Platforms with native 64-bit time sources (no rollover tracking needed) +#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) +#define USE_NATIVE_64BIT_TIME +#endif + // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_MQTT_IDF_ENQUEUE diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 2c10e7e2dad..ca560e8250a 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace esphome { @@ -28,10 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) -// Half the 32-bit range - used to detect rollovers vs normal time progression -static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; -#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -152,9 +147,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh 64-bit timestamp BEFORE taking lock - const uint64_t now_64 = millis_64(); - // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -181,6 +173,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } else #endif /* not ESPHOME_THREAD_SINGLE */ { + // Only non-defer items need a timestamp for scheduling + const uint64_t now_64 = millis_64(); + // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; @@ -475,19 +470,8 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \ - defined(ESPHOME_THREAD_MULTI_ATOMICS) - const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); - const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); - ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), - this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), - this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#else ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), now_64); -#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -715,166 +699,6 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) -uint64_t Scheduler::millis_64_impl_(uint32_t now) { - // THREAD SAFETY NOTE: - // This function has three implementations, based on the precompiler flags - // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) - // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx) - // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (ESP32, HOST, LibreTiny - // RTL87xx/LN882x, etc.) - // - // Make sure all changes are synchronized if you edit this function. - // - // IMPORTANT: Always pass fresh millis() values to this function. The implementation - // handles out-of-order timestamps between threads, but minimizing time differences - // helps maintain accuracy. - // - -#ifdef ESPHOME_THREAD_SINGLE - // This is the single core implementation. - // - // Single-core platforms have no concurrency, so this is a simple implementation - // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. - - uint16_t major = this->millis_major_; - uint32_t last = this->last_millis_; - - // Check for rollover - if (now < last && (last - now) > HALF_MAX_UINT32) { - this->millis_major_++; - major++; - this->last_millis_ = now; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } else if (now > last) { - // Only update if time moved forward - this->last_millis_ = now; - } - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(major) << 32); - -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - // This is the multi core no atomics implementation. - // - // Without atomics, this implementation uses locks more aggressively: - // 1. Always locks when near the rollover boundary (within 10 seconds) - // 2. Always locks when detecting a large backwards jump - // 3. Updates without lock in normal forward progression (accepting minor races) - // This is less efficient but necessary without atomic operations. - uint16_t major = this->millis_major_; - uint32_t last = this->last_millis_; - - // Define a safe window around the rollover point (10 seconds) - // This covers any reasonable scheduler delays or thread preemption - static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds - - // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) - bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); - - if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { - // Near rollover or detected a rollover - need lock for safety - LockGuard guard{this->lock_}; - // Re-read with lock held - last = this->last_millis_; - - if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_++; - major++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } - // Update last_millis_ while holding lock - this->last_millis_ = now; - } else if (now > last) { - // Normal case: Not near rollover and time moved forward - // Update without lock. While this may cause minor races (microseconds of - // backwards time movement), they're acceptable because: - // 1. The scheduler operates at millisecond resolution, not microsecond - // 2. We've already prevented the critical rollover race condition - // 3. Any backwards movement is orders of magnitude smaller than scheduler delays - this->last_millis_ = now; - } - // If now <= last and we're not near rollover, don't update - // This minimizes backwards time movement - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(major) << 32); - -#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) - // This is the multi core with atomics implementation. - // - // Uses atomic operations with acquire/release semantics to ensure coherent - // reads of millis_major_ and last_millis_ across cores. Features: - // 1. Epoch-coherency retry loop to handle concurrent updates - // 2. Lock only taken for actual rollover detection and update - // 3. Lock-free CAS updates for normal forward time progression - // 4. Memory ordering ensures cores see consistent time values - - for (;;) { - uint16_t major = this->millis_major_.load(std::memory_order_acquire); - - /* - * Acquire so that if we later decide **not** to take the lock we still - * observe a `millis_major_` value coherent with the loaded `last_millis_`. - * The acquire load ensures any later read of `millis_major_` sees its - * corresponding increment. - */ - uint32_t last = this->last_millis_.load(std::memory_order_acquire); - - // If we might be near a rollover (large backwards jump), take the lock for the entire operation - // This ensures rollover detection and last_millis_ update are atomic together - if (now < last && (last - now) > HALF_MAX_UINT32) { - // Potential rollover - need lock for atomic rollover detection + update - LockGuard guard{this->lock_}; - // Re-read with lock held; mutex already provides ordering - last = this->last_millis_.load(std::memory_order_relaxed); - - if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_.fetch_add(1, std::memory_order_relaxed); - major++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } - /* - * Update last_millis_ while holding the lock to prevent races - * Publish the new low-word *after* bumping `millis_major_` (done above) - * so readers never see a mismatched pair. - */ - this->last_millis_.store(now, std::memory_order_release); - } else { - // Normal case: Try lock-free update, but only allow forward movement within same epoch - // This prevents accidentally moving backwards across a rollover boundary - while (now > last && (now - last) < HALF_MAX_UINT32) { - if (this->last_millis_.compare_exchange_weak(last, now, - std::memory_order_release, // success - std::memory_order_relaxed)) { // failure - break; - } - // CAS failure means no data was published; relaxed is fine - // last is automatically updated by compare_exchange_weak if it fails - } - } - uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); - if (major_end == major) - return now + (static_cast(major) << 32); - } - // Unreachable - the loop always returns when major_end == major - __builtin_unreachable(); - -#else -#error \ - "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." -#endif -} -#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 - bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index d52cf5147d9..cefbdd1b223 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -12,6 +12,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/time_64.h" namespace esphome { @@ -284,23 +285,16 @@ class Scheduler { bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. - // On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64(). + // On platforms with native 64-bit time, ignores now and uses millis_64() directly. // On other platforms, extends now to 64-bit using rollover tracking. uint64_t millis_64_from_(uint32_t now) { -#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040) +#ifdef USE_NATIVE_64BIT_TIME (void) now; return millis_64(); #else - return this->millis_64_impl_(now); + return Millis64Impl::compute(now); #endif } - -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - // On platforms without native 64-bit time, millis_64() HAL function delegates to this - // method which tracks 32-bit millis() rollover using millis_major_ and last_millis_. - friend uint64_t millis_64(); - uint64_t millis_64_impl_(uint32_t now); -#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -566,39 +560,6 @@ class Scheduler { // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; - -#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) - // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed. - // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - /* - * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates - * - * MEMORY-ORDERING NOTE - * -------------------- - * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half. - * Writers publish `last_millis_` with memory_order_release and readers use - * memory_order_acquire. This ensures that once a reader sees the new low word, - * it also observes the corresponding increment of `millis_major_`. - */ - std::atomic last_millis_{0}; -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - // Platforms without atomic support or single-threaded platforms - uint32_t last_millis_{0}; -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ - - /* - * Upper 16 bits of the 64-bit millis counter. Incremented only while holding - * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race. - * Ordering relative to `last_millis_` is provided by its release store and the - * corresponding acquire loads. - */ -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - std::atomic millis_major_{0}; -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - uint16_t millis_major_{0}; -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ -#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */ }; } // namespace esphome diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp new file mode 100644 index 00000000000..db5df25eb93 --- /dev/null +++ b/esphome/core/time_64.cpp @@ -0,0 +1,207 @@ +#include "esphome/core/defines.h" + +#ifndef USE_NATIVE_64BIT_TIME + +#include "time_64.h" + +#include "esphome/core/helpers.h" +#ifdef ESPHOME_DEBUG_SCHEDULER +#include "esphome/core/log.h" +#include +#endif +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#include +#endif +#include + +namespace esphome { + +#ifdef ESPHOME_DEBUG_SCHEDULER +static const char *const TAG = "time_64"; +#endif + +uint64_t Millis64Impl::compute(uint32_t now) { + // Half the 32-bit range - used to detect rollovers vs normal time progression + static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; + + // State variables for rollover tracking - static to persist across calls +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Mutex for rollover serialization (taken only every ~49.7 days). + // A spinlock would be smaller (~1 byte vs ~80-100 bytes) but is unsafe on + // preemptive single-core RTOS platforms due to priority inversion: a high-priority + // task spinning would prevent the lock holder from running to release it. + static Mutex lock; + /* + * Multi-threaded platforms with atomic support: last_millis needs atomic for lock-free updates. + * Writers publish last_millis with memory_order_release and readers use memory_order_acquire. + * This ensures that once a reader sees the new low word, it also observes the corresponding + * increment of millis_major. + */ + static std::atomic last_millis{0}; + /* + * Upper 16 bits of the 64-bit millis counter. Incremented only while holding lock; + * read concurrently. Atomic (relaxed) avoids a formal data race. Ordering relative + * to last_millis is provided by its release store and the corresponding acquire loads. + */ + static std::atomic millis_major{0}; +#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */ + static Mutex lock; + static uint32_t last_millis{0}; + static uint16_t millis_major{0}; +#else /* ESPHOME_THREAD_SINGLE */ + static uint32_t last_millis{0}; + static uint16_t millis_major{0}; +#endif + + // THREAD SAFETY NOTE: + // This function has three implementations, based on the precompiler flags + // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.) + // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx) + // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.) + // + // Make sure all changes are synchronized if you edit this function. + // + // IMPORTANT: Always pass fresh millis() values to this function. The implementation + // handles out-of-order timestamps between threads, but minimizing time differences + // helps maintain accuracy. + +#ifdef ESPHOME_THREAD_SINGLE + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. + + uint16_t major = millis_major; + uint32_t last = last_millis; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { + millis_major++; + major++; + last_millis = now; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } else if (now > last) { + // Only update if time moved forward + last_millis = now; + } + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + // Without atomics, this implementation uses locks more aggressively: + // 1. Always locks when near the rollover boundary (within 10 seconds) + // 2. Always locks when detecting a large backwards jump + // 3. Updates without lock in normal forward progression (accepting minor races) + // This is less efficient but necessary without atomic operations. + uint16_t major = millis_major; + uint32_t last = last_millis; + + // Define a safe window around the rollover point (10 seconds) + // This covers any reasonable scheduler delays or thread preemption + static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + + // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) + bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); + + if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { + // Near rollover or detected a rollover - need lock for safety + LockGuard guard{lock}; + // Re-read with lock held + last = last_millis; + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + millis_major++; + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + // Update last_millis while holding lock + last_millis = now; + } else if (now > last) { + // Normal case: Not near rollover and time moved forward + // Update without lock. While this may cause minor races (microseconds of + // backwards time movement), they're acceptable because: + // 1. The scheduler operates at millisecond resolution, not microsecond + // 2. We've already prevented the critical rollover race condition + // 3. Any backwards movement is orders of magnitude smaller than scheduler delays + last_millis = now; + } + // If now <= last and we're not near rollover, don't update + // This minimizes backwards time movement + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + // Uses atomic operations with acquire/release semantics to ensure coherent + // reads of millis_major and last_millis across cores. Features: + // 1. Epoch-coherency retry loop to handle concurrent updates + // 2. Lock only taken for actual rollover detection and update + // 3. Lock-free CAS updates for normal forward time progression + // 4. Memory ordering ensures cores see consistent time values + + for (;;) { + uint16_t major = millis_major.load(std::memory_order_acquire); + + /* + * Acquire so that if we later decide **not** to take the lock we still + * observe a millis_major value coherent with the loaded last_millis. + * The acquire load ensures any later read of millis_major sees its + * corresponding increment. + */ + uint32_t last = last_millis.load(std::memory_order_acquire); + + // If we might be near a rollover (large backwards jump), take the lock + // This ensures rollover detection and last_millis update are atomic together + if (now < last && (last - now) > HALF_MAX_UINT32) { + // Potential rollover - need lock for atomic rollover detection + update + LockGuard guard{lock}; + // Re-read with lock held; mutex already provides ordering + last = last_millis.load(std::memory_order_relaxed); + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + millis_major.fetch_add(1, std::memory_order_relaxed); + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + /* + * Update last_millis while holding the lock to prevent races. + * Publish the new low-word *after* bumping millis_major (done above) + * so readers never see a mismatched pair. + */ + last_millis.store(now, std::memory_order_release); + } else { + // Normal case: Try lock-free update, but only allow forward movement within same epoch + // This prevents accidentally moving backwards across a rollover boundary + while (now > last && (now - last) < HALF_MAX_UINT32) { + if (last_millis.compare_exchange_weak(last, now, + std::memory_order_release, // success + std::memory_order_relaxed)) { // failure + break; + } + // CAS failure means no data was published; relaxed is fine + // last is automatically updated by compare_exchange_weak if it fails + } + } + uint16_t major_end = millis_major.load(std::memory_order_relaxed); + if (major_end == major) + return now + (static_cast(major) << 32); + } + // Unreachable - the loop always returns when major_end == major + __builtin_unreachable(); + +#else +#error \ + "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." +#endif +} + +} // namespace esphome + +#endif // !USE_NATIVE_64BIT_TIME diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h new file mode 100644 index 00000000000..42d4b041e51 --- /dev/null +++ b/esphome/core/time_64.h @@ -0,0 +1,24 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifndef USE_NATIVE_64BIT_TIME + +#include + +namespace esphome { + +class Scheduler; + +/// Extends 32-bit millis() to 64-bit using rollover tracking. +/// Access restricted to platform HAL (millis_64()) and Scheduler. +/// All other code should call millis_64() from hal.h instead. +class Millis64Impl { + friend uint64_t millis_64(); + friend class Scheduler; + + static uint64_t compute(uint32_t now); +}; + +} // namespace esphome + +#endif // !USE_NATIVE_64BIT_TIME From 82da4935b603624bf90a1429309e3d23d749bd46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:08 -1000 Subject: [PATCH 0317/2056] [core] Auto-wrap static strings in PROGMEM on ESP8266 via TemplatableValue (#13885) --- esphome/codegen.py | 1 + esphome/components/api/__init__.py | 34 +++++--- .../components/api/homeassistant_service.h | 46 ++++++++++- esphome/core/automation.h | 56 +++++++++++-- esphome/cpp_generator.py | 26 +++++++ tests/unit_tests/test_cpp_generator.py | 78 +++++++++++++++++++ 6 files changed, 223 insertions(+), 18 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index c5283f4967c..30e31353607 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -11,6 +11,7 @@ from esphome.cpp_generator import ( # noqa: F401 ArrayInitializer, Expression, + FlashStringLiteral, LineComment, LogStringLiteral, MockObj, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index d7b6bec3573..dd99862cc27 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -535,24 +535,31 @@ async def homeassistant_service_to_code( cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_ACTION], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) if on_error := config.get(CONF_ON_ERROR): cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") @@ -621,24 +628,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - templ = await cg.templatable(config[CONF_EVENT], args, None) + templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) return var @@ -662,11 +676,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - cg.add(var.set_service("esphome.tag_scanned")) + cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned"))) # Initialize FixedVector with exact size (1 data field) cg.add(var.init_data(1)) templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) - cg.add(var.add_data("tag_id", templ)) + cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ)) return var diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 340699e1a6d..9d14061d072 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -130,6 +130,20 @@ template class HomeAssistantServiceCallAction : public Actionadd_kv_(this->variables_, key, std::forward(value)); } +#ifdef USE_ESP8266 + // On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer). + // Store as const char* — populate_service_map copies from PROGMEM at play() time. + template void add_data(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_, reinterpret_cast(key), std::forward(value)); + } + template void add_data_template(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_template_, reinterpret_cast(key), std::forward(value)); + } + template void add_variable(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->variables_, reinterpret_cast(key), std::forward(value)); + } +#endif + #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES template void set_response_template(T response_template) { this->response_template_ = response_template; @@ -221,7 +235,32 @@ template class HomeAssistantServiceCallAction : public Action class HomeAssistantServiceCallAction : public Action #include @@ -56,6 +57,16 @@ template class TemplatableValue { this->static_str_ = str; } +#ifdef USE_ESP8266 + // On ESP8266, __FlashStringHelper* is a distinct type from const char*. + // ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM. + // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions + // to access the PROGMEM pointer safely. + TemplatableValue(const __FlashStringHelper *str) requires std::same_as : type_(FLASH_STRING) { + this->static_str_ = reinterpret_cast(str); + } +#endif + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { if constexpr (USE_HEAP_STORAGE) { this->value_ = new T(std::move(value)); @@ -89,7 +100,7 @@ template class TemplatableValue { this->f_ = new std::function(*other.f_); } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; - } else if (this->type_ == STATIC_STRING) { + } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { this->static_str_ = other.static_str_; } } @@ -108,7 +119,7 @@ template class TemplatableValue { other.f_ = nullptr; } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; - } else if (this->type_ == STATIC_STRING) { + } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { this->static_str_ = other.static_str_; } other.type_ = NONE; @@ -141,7 +152,7 @@ template class TemplatableValue { } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) + // STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() const { return this->type_ != NONE; } @@ -165,6 +176,17 @@ template class TemplatableValue { return std::string(this->static_str_); } __builtin_unreachable(); +#ifdef USE_ESP8266 + case FLASH_STRING: + // PROGMEM pointer — must use _P functions to access on ESP8266 + if constexpr (std::same_as) { + size_t len = strlen_P(this->static_str_); + std::string result(len, '\0'); + memcpy_P(result.data(), this->static_str_, len); + return result; + } + __builtin_unreachable(); +#endif case NONE: default: return T{}; @@ -186,9 +208,12 @@ template class TemplatableValue { } /// Check if this holds a static string (const char* stored without allocation) + /// The pointer is always directly readable (RAM or flash-mapped). + /// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions). bool is_static_string() const { return this->type_ == STATIC_STRING; } /// Get the static string pointer (only valid if is_static_string() returns true) + /// The pointer is always directly readable — FLASH_STRING uses a separate type. const char *get_static_string() const { return this->static_str_; } /// Check if the string value is empty without allocating (for std::string specialization). @@ -200,6 +225,12 @@ template class TemplatableValue { return true; case STATIC_STRING: return this->static_str_ == nullptr || this->static_str_[0] == '\0'; +#ifdef USE_ESP8266 + case FLASH_STRING: + // PROGMEM pointer — must use progmem_read_byte on ESP8266 + return this->static_str_ == nullptr || + progmem_read_byte(reinterpret_cast(this->static_str_)) == '\0'; +#endif case VALUE: return this->value_->empty(); default: // LAMBDA/STATELESS_LAMBDA - must call value() @@ -209,8 +240,9 @@ template class TemplatableValue { /// Get a StringRef to the string value without heap allocation when possible. /// For STATIC_STRING/VALUE, returns reference to existing data (no allocation). + /// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions. /// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer. - /// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used). + /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used). /// @param lambda_buf_size Size of the buffer. /// @return StringRef pointing to the string data. StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as { @@ -221,6 +253,19 @@ template class TemplatableValue { if (this->static_str_ == nullptr) return StringRef(); return StringRef(this->static_str_, strlen(this->static_str_)); +#ifdef USE_ESP8266 + case FLASH_STRING: + if (this->static_str_ == nullptr) + return StringRef(); + { + // PROGMEM pointer — copy to buffer via _P functions + size_t len = strlen_P(this->static_str_); + size_t copy_len = std::min(len, lambda_buf_size - 1); + memcpy_P(lambda_buf, this->static_str_, copy_len); + lambda_buf[copy_len] = '\0'; + return StringRef(lambda_buf, copy_len); + } +#endif case VALUE: return StringRef(this->value_->data(), this->value_->size()); default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy @@ -239,6 +284,7 @@ template class TemplatableValue { LAMBDA, STATELESS_LAMBDA, STATIC_STRING, // For const char* when T is std::string - avoids heap allocation + FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms } type_; // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). // For other types, store value inline as before. @@ -247,7 +293,7 @@ template class TemplatableValue { ValueStorage value_; // T for inline storage, T* for heap storage std::function *f_; T (*stateless_f_)(X...); - const char *static_str_; // For STATIC_STRING type + const char *static_str_; // For STATIC_STRING and FLASH_STRING types }; }; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index fe666bdd6e1..5457485d254 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -247,6 +247,23 @@ class LogStringLiteral(Literal): return f"LOG_STR({cpp_string_escape(self.string)})" +class FlashStringLiteral(Literal): + """A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266. + + On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM). + On other platforms, ESPHOME_F(s) expands to plain s (no-op). + """ + + __slots__ = ("string",) + + def __init__(self, string: str) -> None: + super().__init__() + self.string = string + + def __str__(self) -> str: + return f"ESPHOME_F({cpp_string_escape(self.string)})" + + class IntLiteral(Literal): __slots__ = ("i",) @@ -761,6 +778,15 @@ async def templatable( if is_template(value): return await process_lambda(value, args, return_type=output_type) if to_exp is None: + # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. + # On other platforms ESPHOME_F() is a no-op returning const char*. + # Lazy import to avoid circular dependency (cpp_generator <-> cpp_types). + # Identity check (is) avoids brittle string comparison. + if isinstance(value, str) and output_type is not None: + from esphome.cpp_types import std_string + + if output_type is std_string: + return FlashStringLiteral(value) return value if isinstance(to_exp, dict): return to_exp[value] diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 049d21027fa..bdc31cdef8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -248,6 +248,12 @@ class TestLiterals: (cg.FloatLiteral(4.2), "4.2f"), (cg.FloatLiteral(1.23456789), "1.23456789f"), (cg.FloatLiteral(math.nan), "NAN"), + (cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'), + (cg.FlashStringLiteral(""), 'ESPHOME_F("")'), + ( + cg.FlashStringLiteral('quote"here'), + 'ESPHOME_F("quote\\042here")', + ), ), ) def test_str__simple(self, target: cg.Literal, expected: str): @@ -624,3 +630,75 @@ class TestProcessLambda: # Test invalid tuple format (single element) with pytest.raises(AssertionError): await cg.process_lambda(lambda_obj, [(int,)]) + + +@pytest.mark.asyncio +async def test_templatable__string_with_std_string_returns_flash_literal() -> None: + """Static string with std::string output_type returns FlashStringLiteral.""" + result = await cg.templatable("hello", [], ct.std_string) + + assert isinstance(result, cg.FlashStringLiteral) + assert str(result) == 'ESPHOME_F("hello")' + + +@pytest.mark.asyncio +async def test_templatable__empty_string_with_std_string() -> None: + """Empty static string with std::string output_type returns FlashStringLiteral.""" + result = await cg.templatable("", [], ct.std_string) + + assert isinstance(result, cg.FlashStringLiteral) + assert str(result) == 'ESPHOME_F("")' + + +@pytest.mark.asyncio +async def test_templatable__string_with_none_output_type() -> None: + """Static string with output_type=None returns raw string (no wrapping).""" + result = await cg.templatable("hello", [], None) + + assert isinstance(result, str) + assert result == "hello" + + +@pytest.mark.asyncio +async def test_templatable__int_with_std_string() -> None: + """Non-string value with std::string output_type returns raw value.""" + result = await cg.templatable(42, [], ct.std_string) + + assert result == 42 + + +@pytest.mark.asyncio +async def test_templatable__string_with_non_string_output_type() -> None: + """Static string with non-std::string output_type returns raw string.""" + result = await cg.templatable("hello", [], ct.bool_) + + assert isinstance(result, str) + assert result == "hello" + + +@pytest.mark.asyncio +async def test_templatable__with_to_exp_callable() -> None: + """When to_exp is provided, it is applied to non-template values.""" + result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2) + + assert result == 84 + + +@pytest.mark.asyncio +async def test_templatable__with_to_exp_dict() -> None: + """When to_exp is a dict, value is looked up.""" + mapping: dict[str, int] = {"on": 1, "off": 0} + result = await cg.templatable("on", [], None, to_exp=mapping) + + assert result == 1 + + +@pytest.mark.asyncio +async def test_templatable__lambda_with_std_string() -> None: + """Lambda value returns LambdaExpression, not FlashStringLiteral.""" + from esphome.core import Lambda + + lambda_obj = Lambda('return "hello";') + result = await cg.templatable(lambda_obj, [], ct.std_string) + + assert isinstance(result, cg.LambdaExpression) From 00242443e1e67366712840b9fe1b47831d2e9fea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:23 -1000 Subject: [PATCH 0318/2056] [light] Replace powf gamma with pre-computed lookup tables (LUT) (#14123) Co-authored-by: Claude Opus 4.6 --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 3 + esphome/components/host/core.cpp | 1 + esphome/components/libretiny/core.cpp | 1 + esphome/components/light/__init__.py | 40 +++++++++- esphome/components/light/addressable_light.h | 4 +- .../light/addressable_light_wrapper.h | 9 +-- .../components/light/esp_color_correction.cpp | 36 ++++----- .../components/light/esp_color_correction.h | 45 +++++++---- esphome/components/light/light_call.cpp | 5 +- esphome/components/light/light_color_values.h | 48 +++++------- esphome/components/light/light_state.cpp | 78 ++++++++++++++++--- esphome/components/light/light_state.h | 26 ++++++- esphome/components/lvgl/light/lvgl_light.h | 2 +- esphome/components/rgb/rgb_light_output.h | 2 +- esphome/components/rgbw/rgbw_light_output.h | 2 +- esphome/components/rp2040/core.cpp | 1 + esphome/components/zephyr/core.cpp | 1 + esphome/core/defines.h | 1 + esphome/core/hal.h | 1 + esphome/core/helpers.h | 4 + 21 files changed, 228 insertions(+), 83 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index b9ae871abf9..59b791da403 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -48,6 +48,7 @@ void arch_init() { void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 7136cf41cb3..b665124d66f 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 9af85bec58a..cb2b2e19d7d 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -59,6 +59,7 @@ void HOT arch_feed_wdt() { } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 88952692a73..74b33a30a02 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index f1089ad64f2..40382bbda73 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import enum import esphome.automation as auto @@ -37,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -66,6 +67,40 @@ from .types import ( # noqa CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +DOMAIN = "light" + + +@dataclass +class LightData: + gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr + + +def _get_data() -> LightData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = LightData() + return CORE.data[DOMAIN] + + +def _get_or_create_gamma_table(gamma_correct): + data = _get_data() + if gamma_correct in data.gamma_tables: + return data.gamma_tables[gamma_correct] + + if gamma_correct > 0: + forward = [ + HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + for i in range(256) + ] + else: + forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + gamma_str = f"{gamma_correct}".replace(".", "_") + fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) + fwd_arr = cg.progmem_array(fwd_id, forward) + data.gamma_tables[gamma_correct] = fwd_arr + return fwd_arr + + LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, @@ -239,6 +274,9 @@ async def setup_light_core_(light_var, output_var, config): cg.add(light_var.set_flash_transition_length(flash_transition_length)) if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None: cg.add(light_var.set_gamma_correct(gamma_correct)) + fwd_arr = _get_or_create_gamma_table(gamma_correct) + cg.add(light_var.set_gamma_table(fwd_arr)) + cg.add_define("USE_LIGHT_GAMMA_LUT") effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index fcaf07f578f..17cdb7d6f6c 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -66,7 +66,9 @@ class AddressableLight : public LightOutput, public Component { Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { - this->correction_.calculate_gamma_table(state->get_gamma_correct()); +#ifdef USE_LIGHT_GAMMA_LUT + this->correction_.set_gamma_table(state->get_gamma_table()); +#endif this->state_parent_ = state; } void update_state(LightState *state) override; diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index cd83482248e..cc6aa57905f 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight { return; } - float gamma = this->light_state_->get_gamma_correct(); - float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma); - float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma); - float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma); - float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma); + float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f); + float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f); + float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f); + float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f); auto call = this->light_state_->make_call(); diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 1b511a94b27..9d731a2bd56 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -1,25 +1,25 @@ #include "esp_color_correction.h" -#include "light_color_values.h" -#include "esphome/core/log.h" namespace esphome::light { -void ESPColorCorrection::calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); - this->gamma_reverse_table_[i] = uncorrected; - } +uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + return static_cast((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257); +} + +uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + if (value == 0) + return 0; + uint16_t target = value * 257; // Scale 0-255 to 0-65535 + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + if (lo >= 255) + return 255; + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + return (target - a <= b - target) ? lo : lo + 1; } } // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index d275e045b7c..48ecc463648 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -1,15 +1,30 @@ #pragma once #include "esphome/core/color.h" +#include "esphome/core/hal.h" namespace esphome::light { +/// Binary search a monotonically increasing uint16[256] PROGMEM table. +/// Returns the largest index where table[index] <= target. +inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) { + uint8_t lo = 0, hi = 255; + while (lo < hi) { + uint8_t mid = (lo + hi + 1) / 2; + if (progmem_read_uint16(&table[mid]) <= target) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; +} + class ESPColorCorrection { public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma); + void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; } inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -17,19 +32,19 @@ class ESPColorCorrection { } inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) @@ -39,36 +54,40 @@ class ESPColorCorrection { inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } protected: - uint8_t gamma_table_[256]; - uint8_t gamma_reverse_table_[256]; - Color max_brightness_; + /// Forward gamma: read uint16 PROGMEM table, convert to uint8 + uint8_t gamma_correct_(uint8_t value) const; + /// Reverse gamma: binary search the forward PROGMEM table + uint8_t gamma_uncorrect_(uint8_t value) const; + + const uint16_t *gamma_table_{nullptr}; + Color max_brightness_{255, 255, 255, 255}; uint8_t local_brightness_{255}; }; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0291b2c3c6a..14cd0e92f69 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -389,9 +389,8 @@ void LightCall::transform_parameters_() { const float ww_fraction = (color_temp - min_mireds) / range; const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); - const float gamma = this->parent_->get_gamma_correct(); - this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); - this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); + this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww); + this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww); this->set_flag_(FLAG_HAS_COLD_WHITE); this->set_flag_(FLAG_HAS_WARM_WHITE); } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index dc232633124..3a9ca8c8c23 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -111,60 +111,54 @@ class LightColorValues { } } - // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters - // are always used or necessary. Methods will be deprecated later. - /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness, float gamma = 0) const { - *brightness = gamma_correct(this->state_ * this->brightness_, gamma); - } + void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + void as_rgb(float *red, float *green, float *blue) const { if (this->color_mode_ & ColorCapability::RGB) { float brightness = this->state_ * this->brightness_ * this->color_brightness_; - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); + *red = brightness * this->red_; + *green = brightness * this->green_; + *blue = brightness * this->blue_; } else { *red = *green = *blue = 0; } } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, - bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma); + void as_rgbw(float *red, float *green, float *blue, float *white) const { + this->as_rgb(red, green, blue); if (this->color_mode_ & ColorCapability::WHITE) { - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *white = this->state_ * this->brightness_ * this->white_; } else { *white = 0; } } /// Convert these light color values to an RGBWW representation with the given parameters. - void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, + void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness = false) const { - this->as_rgb(red, green, blue, gamma); - this->as_cwww(cold_white, warm_white, gamma, constant_brightness); + this->as_rgb(red, green, blue); + this->as_cwww(cold_white, warm_white, constant_brightness); } /// Convert these light color values to an RGB+CT+BR representation with the given parameters. void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *color_temperature, float *white_brightness, float gamma = 0) const { - this->as_rgb(red, green, blue, gamma); - this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma); + float *color_temperature, float *white_brightness) const { + this->as_rgb(red, green, blue); + this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness); } /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { - const float cw_level = gamma_correct(this->cold_white_, gamma); - const float ww_level = gamma_correct(this->warm_white_, gamma); - const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + const float cw_level = this->cold_white_; + const float ww_level = this->warm_white_; + const float white_level = this->state_ * this->brightness_; if (!constant_brightness) { *cold_white = white_level * cw_level; *warm_white = white_level * ww_level; @@ -184,13 +178,13 @@ class LightColorValues { } /// Convert these light color values to a CT+BR representation with the given parameters. - void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness, - float gamma = 0) const { + void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, + float *white_brightness) const { const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1; if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) { *color_temperature = (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); + *white_brightness = this->state_ * this->brightness_ * white_level; } else { // Probably won't get here but put this here anyway. *white_brightness = 0; } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index ed86bf58dab..161092532ac 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,4 +1,5 @@ #include "light_state.h" +#include "esp_color_correction.h" #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -204,33 +205,90 @@ void LightState::add_effects(const std::initializer_list &effects void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness); + *brightness = this->gamma_correct_lut(*brightness); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); +void LightState::current_values_as_rgb(float *red, float *green, float *blue) { + this->current_values.as_rgb(red, green, blue); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); } -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { + this->current_values.as_rgbw(red, green, blue, white); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white = this->gamma_correct_lut(*white); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature, - white_brightness, this->gamma_correct_); + white_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_cwww(cold_white, warm_white, constant_brightness); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); - this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness, - this->gamma_correct_); + this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness); + *white_brightness = this->gamma_correct_lut(*white_brightness); } +#ifdef USE_LIGHT_GAMMA_LUT +float LightState::gamma_correct_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + float scaled = value * 255.0f; + auto idx = static_cast(scaled); + if (idx >= 255) + return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f; + float frac = scaled - idx; + float a = progmem_read_uint16(&this->gamma_table_[idx]); + float b = progmem_read_uint16(&this->gamma_table_[idx + 1]); + return (a + frac * (b - a)) / 65535.0f; +} +float LightState::gamma_uncorrect_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + uint16_t target = static_cast(value * 65535.0f); + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + if (lo >= 255) + return 1.0f; + // Interpolate between lo and lo+1 + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + if (b == a) + return lo / 255.0f; + float frac = static_cast(target - a) / static_cast(b - a); + return (lo + frac) / 255.0f; +} +#endif // USE_LIGHT_GAMMA_LUT + bool LightState::is_transformer_active() { return this->is_transformer_active_; } void LightState::start_effect_(uint32_t effect_index) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 83b9226d039..528f5bd9117 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -11,6 +11,7 @@ #include "light_traits.h" #include "light_transformer.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include #include @@ -166,6 +167,23 @@ class LightState : public EntityBase, public Component { void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } +#ifdef USE_LIGHT_GAMMA_LUT + /// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array) + void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; } + + /// Get the forward gamma lookup table + const uint16_t *get_gamma_table() const { return this->gamma_table_; } + + /// Apply gamma correction using the pre-computed forward LUT + float gamma_correct_lut(float value) const; + /// Reverse gamma correction by binary-searching the forward LUT + float gamma_uncorrect_lut(float value) const; +#else + /// No gamma LUT — passthrough + float gamma_correct_lut(float value) const { return value; } + float gamma_uncorrect_lut(float value) const { return value; } +#endif // USE_LIGHT_GAMMA_LUT + /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); @@ -224,9 +242,9 @@ class LightState : public EntityBase, public Component { void current_values_as_brightness(float *brightness); - void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); + void current_values_as_rgb(float *red, float *green, float *blue); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); + void current_values_as_rgbw(float *red, float *green, float *blue, float *white); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness = false); @@ -297,6 +315,10 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; +#ifdef USE_LIGHT_GAMMA_LUT + const uint16_t *gamma_table_{nullptr}; +#endif // USE_LIGHT_GAMMA_LUT + /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 50ae4c53276..569f9a03c0b 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -16,7 +16,7 @@ class LVLight : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue, false); + state->current_values_as_rgb(&red, &green, &blue); auto color = lv_color_make(red * 255, green * 255, blue * 255); if (this->obj_ != nullptr) { this->set_value_(color); diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index ef53c8042d0..783187667a0 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -20,7 +20,7 @@ class RGBLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue, false); + state->current_values_as_rgb(&red, &green, &blue); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index a2ab17b75dc..140726a43c8 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -25,7 +25,7 @@ class RGBWLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); + state->current_values_as_rgbw(&red, &green, &blue, &white); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 8b86de4be11..6386d53292f 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -34,6 +34,7 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index f0772a4422f..cf3ea70245a 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } Mutex::Mutex() { auto *mutex = new k_mutex(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1f2597dd984..8c78afa7d49 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -61,6 +61,7 @@ #define USE_IR_RF #define USE_JSON #define USE_LIGHT +#define USE_LIGHT_GAMMA_LUT #define USE_LOCK #define USE_LOGGER #define USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/core/hal.h b/esphome/core/hal.h index fa5ca646f2e..ef45be629d3 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -42,5 +42,6 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); +uint16_t progmem_read_uint16(const uint16_t *addr); } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 65d590a5e67..c68cb549bb4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1437,8 +1437,12 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector ///@{ /// Applies gamma correction of \p gamma to \p value. +// Remove before 2026.9.0 +ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.9.0.", "2026.3.0") float gamma_correct(float value, float gamma); /// Reverts gamma correction of \p gamma to \p value. +// Remove before 2026.9.0 +ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.9.0.", "2026.3.0") float gamma_uncorrect(float value, float gamma); /// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1). From 54f410901fa09bd025bc370be3753346c2430b9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:39 -1000 Subject: [PATCH 0319/2056] [web_server] Avoid temporary std::string allocations in request parameter parsing (#14366) Co-authored-by: Claude Opus 4.6 --- .../alarm_control_panel_call.cpp | 9 ++++- .../alarm_control_panel_call.h | 3 +- esphome/components/api/api_connection.cpp | 4 +- esphome/components/climate/climate.cpp | 18 ++++++--- esphome/components/climate/climate.h | 4 ++ esphome/components/text/text_call.cpp | 5 +++ esphome/components/text/text_call.h | 1 + .../components/water_heater/water_heater.cpp | 21 +++++----- .../components/water_heater/water_heater.h | 3 +- esphome/components/web_server/web_server.cpp | 38 ++++++++++++++----- esphome/components/web_server/web_server.h | 8 ++-- 11 files changed, 82 insertions(+), 32 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index ba58ee3904a..0c43cd555a3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) { if (code != nullptr) { - this->code_ = std::string(code); + return this->set_code(code, strlen(code)); + } + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) { + if (code != nullptr) { + this->code_ = std::string(code, len); } return *this; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index 58764ea166c..6e39a0a4137 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -15,7 +15,8 @@ class AlarmControlPanelCall { AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall &set_code(const char *code); - AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); } + AlarmControlPanelCall &set_code(const char *code, size_t len); + AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); } AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_night(); diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 90287ec2dda..738dd1ef054 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -889,7 +889,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co } void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) - call.set_value(msg.state); + call.set_value(msg.state.c_str(), msg.state.size()); call.perform(); } #endif @@ -1360,7 +1360,7 @@ void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPan call.pending(); break; } - call.set_code(msg.code); + call.set_code(msg.code.c_str(), msg.code.size()); call.perform(); } #endif diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ba6de4ff615..43d25effa33 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) { return *this; } -ClimateCall &ClimateCall::set_mode(const std::string &mode) { +ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); } + +ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) { + StringRef mode_ref(mode, len); for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { - if (str_equals_case_insensitive(mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); return *this; } @@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { } ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + return this->set_swing_mode(swing_mode.c_str(), swing_mode.size()); +} + +ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) { + StringRef mode_ref(swing_mode, len); for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) { - if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_swing_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode); return *this; } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 6fac254502e..aa9ca91bc2c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -41,6 +41,8 @@ class ClimateCall { ClimateCall &set_mode(optional mode); /// Set the mode of the climate device based on a string. ClimateCall &set_mode(const std::string &mode); + /// Set the mode of the climate device based on a C string. + ClimateCall &set_mode(const char *mode, size_t len); /// Set the target temperature of the climate device. ClimateCall &set_target_temperature(float target_temperature); /// Set the target temperature of the climate device. @@ -87,6 +89,8 @@ class ClimateCall { ClimateCall &set_swing_mode(optional swing_mode); /// Set the swing mode of the climate device based on a string. ClimateCall &set_swing_mode(const std::string &swing_mode); + /// Set the swing mode of the climate device based on a C string. + ClimateCall &set_swing_mode(const char *swing_mode, size_t len); /// Set the preset of the climate device. ClimateCall &set_preset(ClimatePreset preset); /// Set the preset of the climate device. diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index 8a1630c5ca0..b7aed098c77 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -11,6 +11,11 @@ TextCall &TextCall::set_value(const std::string &value) { return *this; } +TextCall &TextCall::set_value(const char *value, size_t len) { + this->value_ = std::string(value, len); + return *this; +} + void TextCall::validate_() { const auto *name = this->parent_->get_name().c_str(); diff --git a/esphome/components/text/text_call.h b/esphome/components/text/text_call.h index 532fae34b2b..5a2b257ab2a 100644 --- a/esphome/components/text/text_call.h +++ b/esphome/components/text/text_call.h @@ -13,6 +13,7 @@ class TextCall { void perform(); TextCall &set_value(const std::string &value); + TextCall &set_value(const char *value, size_t len); protected: Text *const parent_; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9d7ae0cbc0d..3989230d2d9 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -5,6 +5,7 @@ #include "esphome/core/progmem.h" #include +#include namespace esphome::water_heater { @@ -23,23 +24,25 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { return *this; } -WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { - if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) { +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { return this->set_mode(mode, strlen(mode)); } + +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode, size_t len) { + if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("OFF"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) { + } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ECO"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) { + } else if (len == 8 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ELECTRIC"), 8) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) { + } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) { + } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) { + } else if (len == 9 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP"), 9) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); - } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) { + } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("GAS"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); } return *this; } diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 070ae995750..a1e1ca10a6c 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -76,7 +76,8 @@ class WaterHeaterCall { WaterHeaterCall &set_mode(WaterHeaterMode mode); WaterHeaterCall &set_mode(const char *mode); - WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); } + WaterHeaterCall &set_mode(const char *mode, size_t len); + WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); } WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4824e33dcd1..47e427c0d12 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -969,7 +969,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000); if (is_on) { - parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); + parse_cstr_param_( + request, ESPHOME_F("effect"), call, + static_cast(&decltype(call)::set_effect)); } DEFER_ACTION(call, call.perform()); @@ -1368,7 +1370,9 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); + parse_cstr_param_( + request, ESPHOME_F("value"), call, + static_cast(&decltype(call)::set_value)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1426,7 +1430,9 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); + parse_cstr_param_( + request, ESPHOME_F("option"), call, + static_cast(&decltype(call)::set_option)); DEFER_ACTION(call, call.perform()); request->send(200); @@ -1487,10 +1493,18 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); // Parse string mode parameters - parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode); - parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode); - parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); - parse_string_param_(request, ESPHOME_F("preset"), call, &decltype(call)::set_preset); + parse_cstr_param_( + request, ESPHOME_F("mode"), call, + static_cast(&decltype(call)::set_mode)); + parse_cstr_param_(request, ESPHOME_F("fan_mode"), call, + static_cast( + &decltype(call)::set_fan_mode)); + parse_cstr_param_(request, ESPHOME_F("swing_mode"), call, + static_cast( + &decltype(call)::set_swing_mode)); + parse_cstr_param_(request, ESPHOME_F("preset"), call, + static_cast( + &decltype(call)::set_preset)); // Parse temperature parameters // static_cast needed to disambiguate overloaded setters (float vs optional) @@ -1804,7 +1818,10 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code); + parse_cstr_param_( + request, ESPHOME_F("code"), call, + static_cast(&decltype(call)::set_code)); // Lookup table for alarm control panel methods static const struct { @@ -1892,7 +1909,10 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons water_heater::WaterHeaterCall &base_call = call; // Parse mode parameter - parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); + parse_cstr_param_( + request, ESPHOME_F("mode"), base_call, + static_cast( + &water_heater::WaterHeaterCall::set_mode)); // Parse temperature parameters parse_num_param_(request, ESPHOME_F("target_temperature"), base_call, diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 64c492f82b7..6152dfbfd32 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -533,13 +533,13 @@ class WebServer final : public Controller, public Component, public AsyncWebHand } } - // Generic helper to parse and apply a string parameter + // Generic helper to parse and apply a string parameter using const char* setter (avoids std::string allocation) template - void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, - Ret (T::*setter)(const std::string &)) { + void parse_cstr_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, + Ret (T::*setter)(const char *, size_t)) { if (request->hasArg(param_name)) { const auto &value = request->arg(param_name); - (call.*setter)(std::string(value.c_str(), value.length())); + (call.*setter)(value.c_str(), value.length()); } } From f278250740330b6b4e990ae19b0bc91d846ec924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:57:59 -1000 Subject: [PATCH 0320/2056] [core] Deduplicate ControllerRegistry notify dispatch loop (#14394) --- esphome/core/controller_registry.cpp | 21 +++++++++++++-------- esphome/core/controller_registry.h | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 13b505e8e95..255efa86ba8 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,21 +10,26 @@ StaticVector ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } +void ControllerRegistry::notify(void *obj, DispatchFunc dispatch) { + for (auto *controller : controllers) { + dispatch(controller, obj); + } +} + // Macro for standard registry notification dispatch - calls on__update() +// Each wrapper passes a small trampoline lambda that calls the correct virtual method. +// NOLINTBEGIN(bugprone-macro-parentheses) #define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ - for (auto *controller : controllers) { \ - controller->on_##entity_name##_update(obj); \ - } \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ + notify(obj, [](Controller *c, void *o) { c->on_##entity_name##_update(static_cast(o)); }); \ } // Macro for entities where controller method has no "_update" suffix (Event, Update) #define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ - for (auto *controller : controllers) { \ - controller->on_##entity_name(obj); \ - } \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ + notify(obj, [](Controller *c, void *o) { c->on_##entity_name(static_cast(o)); }); \ } +// NOLINTEND(bugprone-macro-parentheses) #ifdef USE_BINARY_SENSOR CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index d6452d8827e..15e3b4ba835 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -247,6 +247,21 @@ class ControllerRegistry { #endif protected: + /** Type-erased dispatch function pointer. + * + * Each notify method passes a small trampoline that calls the + * correct virtual method on Controller. The shared notify() loop + * iterates controllers once, calling the trampoline for each. + */ + using DispatchFunc = void (*)(Controller *, void *); + + /** Shared dispatch loop - iterates controllers and calls dispatch for each. + * + * Marked noinline to ensure only one copy of the loop exists in flash, + * rather than being duplicated into each notify_*_update wrapper. + */ + static void __attribute__((noinline)) notify(void *obj, DispatchFunc dispatch); + static StaticVector controllers; }; From 39572d9628d80ecf5f74f99f54c536ca26b357c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:58:15 -1000 Subject: [PATCH 0321/2056] [light] Resolve effect names to indices at codegen time (#14265) --- esphome/components/light/automation.h | 2 +- esphome/components/light/automation.py | 50 ++++++++++++++++++++++++-- esphome/components/light/light_state.h | 15 ++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index c90d71c5df8..2854bc62d9c 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -41,7 +41,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, color_temperature) TEMPLATABLE_VALUE(float, cold_white) TEMPLATABLE_VALUE(float, warm_white) - TEMPLATABLE_VALUE(std::string, effect) + TEMPLATABLE_VALUE(uint32_t, effect) void play(const Ts &...x) override { auto call = this->parent_->make_call(); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 89b2fc0fb21..08fd26a9379 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -10,12 +10,14 @@ from esphome.const import ( CONF_COLOR_MODE, CONF_COLOR_TEMPERATURE, CONF_EFFECT, + CONF_EFFECTS, CONF_FLASH_LENGTH, CONF_GREEN, CONF_ID, CONF_LIMIT_MODE, CONF_MAX_BRIGHTNESS, CONF_MIN_BRIGHTNESS, + CONF_NAME, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_RED, @@ -24,6 +26,9 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_WHITE, ) +from esphome.core import CORE, Lambda +from esphome.cpp_generator import LambdaExpression +from esphome.types import ConfigType from .types import ( COLOR_MODES, @@ -111,6 +116,26 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) +def _resolve_effect_index(config: ConfigType) -> int: + """Resolve a static effect name to its 1-based index at codegen time. + + Effect index 0 means "None" (no effect). Effects are 1-indexed matching + the C++ convention in LightState. + """ + original_name = config[CONF_EFFECT] + effect_name = original_name.lower() + if effect_name == "none": + return 0 + light_id = config[CONF_ID] + light_path = CORE.config.get_path_for_id(light_id)[:-1] + light_config = CORE.config.get_config_for_path(light_path) + for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])): + key = next(iter(effect_conf)) + if effect_conf[key][CONF_NAME].lower() == effect_name: + return i + 1 + raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'") + + @automation.register_action( "light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True ) @@ -165,8 +190,29 @@ async def light_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: - template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) - cg.add(var.set_effect(template_)) + if isinstance(config[CONF_EFFECT], Lambda): + # Lambda returns a string — wrap in a C++ lambda that resolves + # the effect name to its uint32_t index at runtime + inner_lambda = await cg.process_lambda( + config[CONF_EFFECT], args, return_type=cg.std_string + ) + fwd_args = ", ".join(n for _, n in args) + # capture="" is correct: paren is a global variable name + # string-interpolated into the body at codegen time, not a + # C++ runtime capture. + wrapper = LambdaExpression( + f"auto __effect_s = ({inner_lambda})({fwd_args});\n" + f"return {paren}->get_effect_index(" + f"__effect_s.c_str(), __effect_s.size());", + args, + capture="", + return_type=cg.uint32, + ) + cg.add(var.set_effect(wrapper)) + else: + # Static string — resolve effect name to index at codegen time + effect_index = _resolve_effect_index(config) + cg.add(var.set_effect(effect_index)) return var diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 528f5bd9117..b8d72cc8325 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -13,6 +13,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include #include @@ -218,6 +219,20 @@ class LightState : public EntityBase, public Component { return 0; // Effect not found } + /// Get effect index by name (const char* overload, avoids std::string construction). + uint32_t get_effect_index(const char *name, size_t len) const { + if (len == 4 && ESPHOME_strncasecmp_P(name, ESPHOME_PSTR("none"), 4) == 0) { + return 0; + } + StringRef ref(name, len); + for (size_t i = 0; i < this->effects_.size(); i++) { + if (str_equals_case_insensitive(ref, this->effects_[i]->get_name())) { + return i + 1; + } + } + return 0; + } + /// Get effect by index. Returns nullptr if index is invalid. LightEffect *get_effect_by_index(uint32_t index) const { if (index == 0 || index > this->effects_.size()) { From 585e195044af6a7415bf56108d4073c60a4a3e3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:58:31 -1000 Subject: [PATCH 0322/2056] [socket] Devirtualize socket abstraction layer (#14398) --- esphome/components/api/api_server.h | 2 +- .../captive_portal/dns_server_esp32_idf.h | 2 +- esphome/components/esphome/ota/ota_esphome.h | 2 +- .../components/socket/bsd_sockets_impl.cpp | 172 +-- esphome/components/socket/bsd_sockets_impl.h | 114 ++ esphome/components/socket/headers.h | 17 + .../components/socket/lwip_raw_tcp_impl.cpp | 1150 ++++++++--------- esphome/components/socket/lwip_raw_tcp_impl.h | 200 +++ .../components/socket/lwip_sockets_impl.cpp | 139 +- esphome/components/socket/lwip_sockets_impl.h | 80 ++ esphome/components/socket/socket.cpp | 32 +- esphome/components/socket/socket.h | 110 +- esphome/core/application.h | 9 +- 13 files changed, 1153 insertions(+), 876 deletions(-) create mode 100644 esphome/components/socket/bsd_sockets_impl.h create mode 100644 esphome/components/socket/lwip_raw_tcp_impl.h create mode 100644 esphome/components/socket/lwip_sockets_impl.h diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3abf68358c0..6eff2005f8a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -257,7 +257,7 @@ class APIServer : public Component, } void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - socket::Socket *socket_{nullptr}; + socket::ListenSocket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger client_connected_trigger_; #endif diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index f8e4cfec84e..b30856c2041 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -22,7 +22,7 @@ class DNSServer { } static constexpr size_t DNS_BUFFER_SIZE = 192; - socket::Socket *socket_{nullptr}; + socket::ListenSocket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 53715cfe6a3..08edacad92f 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -84,7 +84,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - socket::Socket *server_{nullptr}; + socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; std::unique_ptr backend_; diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index c96713f3766..92ecfc692b8 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -1,135 +1,81 @@ -#include "socket.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "socket.h" #ifdef USE_SOCKET_IMPL_BSD_SOCKETS #include #include "esphome/core/application.h" -#ifdef USE_ESP32 -#include -#include -#endif - namespace esphome::socket { -class BSDSocketImpl final : public Socket { - public: - BSDSocketImpl(int fd, bool monitor_loop = false) { - this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } - } - ~BSDSocketImpl() override { - if (!this->closed_) { - this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - } - } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); } - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = ::accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = ::accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, true); +BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { + this->fd_ = fd; + // Register new socket with the application for select() if monitoring requested + if (monitor_loop && this->fd_ >= 0) { + // Only set loop_monitored_ to true if registration succeeds + this->loop_monitored_ = App.register_socket_fd(this->fd_); } +} - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } - int close() override { - if (!this->closed_) { - // Unregister from select() before closing if monitored - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } - int ret = ::close(this->fd_); - this->closed_ = true; - return ret; +BSDSocketImpl::~BSDSocketImpl() { + if (!this->closed_) { + this->close(); + } +} + +int BSDSocketImpl::close() { + if (!this->closed_) { + // Unregister from select() before closing if monitored + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } + int ret = ::close(this->fd_); + this->closed_ = true; + return ret; + } + return 0; +} + +int BSDSocketImpl::setblocking(bool blocking) { + int fl = ::fcntl(this->fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + ::fcntl(this->fd_, F_SETFL, fl); + return 0; +} + +bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } + +size_t BSDSocketImpl::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } - int shutdown(int how) override { return ::shutdown(this->fd_, how); } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { - return ::getpeername(this->fd_, addr, addrlen); - } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { - return ::getsockname(this->fd_, addr, addrlen); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return ::getsockopt(this->fd_, level, optname, optval, optlen); - } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return ::setsockopt(this->fd_, level, optname, optval, optlen); - } - int listen(int backlog) override { return ::listen(this->fd_, backlog); } - ssize_t read(void *buf, size_t len) override { -#ifdef USE_ESP32 - return ::lwip_read(this->fd_, buf, len); -#else - return ::read(this->fd_, buf, len); -#endif - } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { -#if defined(USE_ESP32) || defined(USE_HOST) - return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); -#else - return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); -#endif - } - ssize_t readv(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) - return ::lwip_readv(this->fd_, iov, iovcnt); -#else - return ::readv(this->fd_, iov, iovcnt); -#endif - } - ssize_t write(const void *buf, size_t len) override { -#ifdef USE_ESP32 - return ::lwip_write(this->fd_, buf, len); -#else - return ::write(this->fd_, buf, len); -#endif - } - ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { -#if defined(USE_ESP32) - return ::lwip_writev(this->fd_, iov, iovcnt); -#else - return ::writev(this->fd_, iov, iovcnt); -#endif - } - - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) - } - - int setblocking(bool blocking) override { - int fl = ::fcntl(this->fd_, F_GETFL, 0); - if (blocking) { - fl &= ~O_NONBLOCK; - } else { - fl |= O_NONBLOCK; - } - ::fcntl(this->fd_, F_SETFL, fl); +size_t BSDSocketImpl::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } -}; + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} // Helper to create a socket with optional monitoring -static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { +static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { int ret = ::socket(domain, type, protocol); if (ret == -1) return nullptr; - return std::unique_ptr{new BSDSocketImpl(ret, loop_monitored)}; + return make_unique(ret, loop_monitored); } std::unique_ptr socket(int domain, int type, int protocol) { @@ -140,6 +86,14 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, false); +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, true); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h new file mode 100644 index 00000000000..edee39f3154 --- /dev/null +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -0,0 +1,114 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome::socket { + +class BSDSocketImpl { + public: + BSDSocketImpl(int fd, bool monitor_loop = false); + ~BSDSocketImpl(); + BSDSocketImpl(const BSDSocketImpl &) = delete; + BSDSocketImpl &operator=(const BSDSocketImpl &) = delete; + + int connect(const struct sockaddr *addr, socklen_t addrlen) { return ::connect(this->fd_, addr, addrlen); } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); + } + + int bind(const struct sockaddr *addr, socklen_t addrlen) { return ::bind(this->fd_, addr, addrlen); } + int close(); + int shutdown(int how) { return ::shutdown(this->fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return ::getpeername(this->fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return ::getsockname(this->fd_, addr, addrlen); } + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + return ::getsockopt(this->fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + return ::setsockopt(this->fd_, level, optname, optval, optlen); + } + int listen(int backlog) { return ::listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) { +#ifdef USE_ESP32 + return ::lwip_read(this->fd_, buf, len); +#else + return ::read(this->fd_, buf, len); +#endif + } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) { +#if defined(USE_ESP32) || defined(USE_HOST) + return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#else + return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); +#endif + } + ssize_t readv(const struct iovec *iov, int iovcnt) { +#if defined(USE_ESP32) + return ::lwip_readv(this->fd_, iov, iovcnt); +#else + return ::readv(this->fd_, iov, iovcnt); +#endif + } + ssize_t write(const void *buf, size_t len) { +#ifdef USE_ESP32 + return ::lwip_write(this->fd_, buf, len); +#else + return ::write(this->fd_, buf, len); +#endif + } + ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) { +#if defined(USE_ESP32) + return ::lwip_writev(this->fd_, iov, iovcnt); +#else + return ::writev(this->fd_, iov, iovcnt); +#endif + } + + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { + return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) + } + + int setblocking(bool blocking); + int loop() { return 0; } + + bool ready() const; + + int get_fd() const { return this->fd_; } + + protected: + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 032892072d9..16e4d23d3ba 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -183,3 +183,20 @@ using socklen_t = uint32_t; #endif #endif // USE_SOCKET_IMPL_BSD_SOCKETS + +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) + +namespace esphome::socket { + +// Maximum length for formatted socket address string (IP address without port) +// IPv4: "255.255.255.255" = 15 chars + null = 16 +// IPv6: full address = 45 chars + null = 46 +#if USE_NETWORK_IPV6 +static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN +#else +static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +#endif + +} // namespace esphome::socket + +#endif diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 6e95f5bc7af..6556979fe01 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -3,13 +3,8 @@ #ifdef USE_SOCKET_IMPL_LWIP_TCP -#include "lwip/ip.h" -#include "lwip/netif.h" -#include "lwip/opt.h" -#include "lwip/tcp.h" #include #include -#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -56,628 +51,596 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif -class LWIPRawImpl : public Socket { - public: - LWIPRawImpl(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {} - ~LWIPRawImpl() override { - if (pcb_ != nullptr) { - LWIP_LOG("tcp_abort(%p)", pcb_); - tcp_abort(pcb_); - pcb_ = nullptr; - } - } +// ---- LWIPRawCommon methods ---- - void init() { - LWIP_LOG("init(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_recv(pcb_, LWIPRawImpl::s_recv_fn); - tcp_err(pcb_, LWIPRawImpl::s_err_fn); +LWIPRawCommon::~LWIPRawCommon() { + if (this->pcb_ != nullptr) { + LWIP_LOG("tcp_abort(%p)", this->pcb_); + tcp_abort(this->pcb_); + this->pcb_ = nullptr; } +} - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - // Non-listening sockets return error +int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { errno = EINVAL; - return nullptr; + return 0; } - int bind(const struct sockaddr *name, socklen_t addrlen) final { - if (pcb_ == nullptr) { - errno = EBADF; - return -1; - } - if (name == nullptr) { - errno = EINVAL; - return 0; - } - ip_addr_t ip; - in_port_t port; + ip_addr_t ip; + in_port_t port; #if LWIP_IPV6 - if (family_ == AF_INET) { - if (addrlen < sizeof(sockaddr_in)) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.type = IPADDR_TYPE_V4; - ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); - } else if (family_ == AF_INET6) { - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return -1; - } - auto *addr6 = reinterpret_cast(name); - port = ntohs(addr6->sin6_port); - ip.type = IPADDR_TYPE_ANY; - memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); - } else { - errno = EINVAL; - return -1; - } -#else - if (family_ != AF_INET) { + if (this->family_ == AF_INET) { + if (addrlen < sizeof(sockaddr_in)) { errno = EINVAL; return -1; } auto *addr4 = reinterpret_cast(name); port = ntohs(addr4->sin_port); - ip.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port); + ip.type = IPADDR_TYPE_V4; + ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; + LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); + } else if (this->family_ == AF_INET6) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr6 = reinterpret_cast(name); + port = ntohs(addr6->sin6_port); + ip.type = IPADDR_TYPE_ANY; + memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); + } else { + errno = EINVAL; + return -1; + } +#else + if (this->family_ != AF_INET) { + errno = EINVAL; + return -1; + } + auto *addr4 = reinterpret_cast(name); + port = ntohs(addr4->sin_port); + ip.addr = addr4->sin_addr.s_addr; + LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port); #endif - err_t err = tcp_bind(pcb_, &ip, port); - if (err == ERR_USE) { - LWIP_LOG(" -> err ERR_USE"); - errno = EADDRINUSE; - return -1; - } - if (err == ERR_VAL) { - LWIP_LOG(" -> err ERR_VAL"); + err_t err = tcp_bind(this->pcb_, &ip, port); + if (err == ERR_USE) { + LWIP_LOG(" -> err ERR_USE"); + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + LWIP_LOG(" -> err ERR_VAL"); + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = EIO; + return -1; + } + return 0; +} + +int LWIPRawCommon::close() { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + LWIP_LOG("tcp_close(%p)", this->pcb_); + err_t err = tcp_close(this->pcb_); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + tcp_abort(this->pcb_); + this->pcb_ = nullptr; + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + this->pcb_ = nullptr; + return 0; +} + +int LWIPRawCommon::shutdown(int how) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + bool shut_rx = false, shut_tx = false; + if (how == SHUT_RD) { + shut_rx = true; + } else if (how == SHUT_WR) { + shut_tx = true; + } else if (how == SHUT_RDWR) { + shut_rx = shut_tx = true; + } else { + errno = EINVAL; + return -1; + } + LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", this->pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); + err_t err = tcp_shutdown(this->pcb_, shut_rx, shut_tx); + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return 0; +} + +int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + return this->ip2sockaddr_(&this->pcb_->remote_ip, this->pcb_->remote_port, name, addrlen); +} + +int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + return this->ip2sockaddr_(&this->pcb_->local_ip, this->pcb_->local_port, name, addrlen); +} + +size_t LWIPRawCommon::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} + +size_t LWIPRawCommon::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} + +int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (optlen == nullptr || optval == nullptr) { + errno = EINVAL; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (*optlen < 4) { errno = EINVAL; return -1; } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = EIO; - return -1; - } + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + *reinterpret_cast(optval) = 1; + *optlen = 4; return 0; } - int close() final { - if (pcb_ == nullptr) { - errno = ECONNRESET; + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (*optlen < 4) { + errno = EINVAL; return -1; } - LWIP_LOG("tcp_close(%p)", pcb_); - err_t err = tcp_close(pcb_); - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - tcp_abort(pcb_); - pcb_ = nullptr; - errno = err == ERR_MEM ? ENOMEM : EIO; - return -1; - } - pcb_ = nullptr; + *reinterpret_cast(optval) = this->nodelay_; + *optlen = 4; return 0; } - int shutdown(int how) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; + + errno = EINVAL; + return -1; +} + +int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optlen != 4) { + errno = EINVAL; return -1; } - bool shut_rx = false, shut_tx = false; - if (how == SHUT_RD) { - shut_rx = true; - } else if (how == SHUT_WR) { - shut_tx = true; - } else if (how == SHUT_RDWR) { - shut_rx = shut_tx = true; + // lwip doesn't seem to have this feature. Don't send an error + // to prevent warnings + return 0; + } + if (level == IPPROTO_TCP && optname == TCP_NODELAY) { + if (optlen != 4) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + this->nodelay_ = val; + return 0; + } + + errno = EINVAL; + return -1; +} + +int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { + if (this->family_ == AF_INET) { + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + + struct sockaddr_in *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = port; + inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); + return 0; + } +#if LWIP_IPV6 + else if (this->family_ == AF_INET6) { + if (*addrlen < sizeof(struct sockaddr_in6)) { + errno = EINVAL; + return -1; + } + + struct sockaddr_in6 *addr = reinterpret_cast(name); + addr->sin6_family = AF_INET6; + *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); + addr->sin6_port = port; + + // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6. + if (IP_IS_V4(ip)) { + ip_addr_t mapped; + ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); } else { - errno = EINVAL; - return -1; - } - LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0); - err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx); - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = err == ERR_MEM ? ENOMEM : EIO; - return -1; + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); } return 0; } +#endif + return -1; +} - int getpeername(struct sockaddr *name, socklen_t *addrlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (name == nullptr || addrlen == nullptr) { - errno = EINVAL; - return -1; - } - return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); +// ---- LWIPRawImpl methods ---- + +LWIPRawImpl::~LWIPRawImpl() { + // Base class destructor handles pcb_ cleanup via tcp_abort +} + +void LWIPRawImpl::init() { + LWIP_LOG("init(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); + tcp_err(this->pcb_, LWIPRawImpl::s_err_fn); +} + +void LWIPRawImpl::s_err_fn(void *arg, err_t err) { + // "If a connection is aborted because of an error, the application is alerted of this event by + // the err callback." + // pcb is already freed when this callback is called + // ERR_RST: connection was reset by remote host + // ERR_ABRT: aborted through tcp_abort or TCP timer + auto *arg_this = reinterpret_cast(arg); + ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); + arg_this->pcb_ = nullptr; +} + +err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + auto *arg_this = reinterpret_cast(arg); + return arg_this->recv_fn(pb, err); +} + +err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { + LWIP_LOG("recv(pb=%p err=%d)", pb, err); + if (err != 0) { + // "An error code if there has been an error receiving Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + this->rx_closed_ = true; + return ERR_OK; } - int getsockname(struct sockaddr *name, socklen_t *addrlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (name == nullptr || addrlen == nullptr) { - errno = EINVAL; - return -1; - } - return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); + if (pb == nullptr) { + this->rx_closed_ = true; + return ERR_OK; } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (optlen == nullptr || optval == nullptr) { - errno = EINVAL; - return -1; - } - if (level == SOL_SOCKET && optname == SO_REUSEADDR) { - if (*optlen < 4) { - errno = EINVAL; - return -1; - } + if (this->rx_buf_ == nullptr) { + // no need to copy because lwIP gave control of it to us + this->rx_buf_ = pb; + this->rx_buf_offset_ = 0; + } else { + pbuf_cat(this->rx_buf_, pb); + } +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can process the received data. + socket_wake(); +#endif + return ERR_OK; +} - // lwip doesn't seem to have this feature. Don't send an error - // to prevent warnings - *reinterpret_cast(optval) = 1; - *optlen = 4; - return 0; - } - if (level == IPPROTO_TCP && optname == TCP_NODELAY) { - if (*optlen < 4) { - errno = EINVAL; - return -1; - } - *reinterpret_cast(optval) = nodelay_; - *optlen = 4; - return 0; - } - - errno = EINVAL; +ssize_t LWIPRawImpl::read(void *buf, size_t len) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; return -1; } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (level == SOL_SOCKET && optname == SO_REUSEADDR) { - if (optlen != 4) { - errno = EINVAL; - return -1; - } - - // lwip doesn't seem to have this feature. Don't send an error - // to prevent warnings - return 0; - } - if (level == IPPROTO_TCP && optname == TCP_NODELAY) { - if (optlen != 4) { - errno = EINVAL; - return -1; - } - int val = *reinterpret_cast(optval); - nodelay_ = val; - return 0; - } - - errno = EINVAL; + if (this->rx_closed_ && this->rx_buf_ == nullptr) { + return 0; + } + if (len == 0) { + return 0; + } + if (this->rx_buf_ == nullptr) { + errno = EWOULDBLOCK; return -1; } - int listen(int backlog) override { - // Regular sockets can't be converted to listening - this shouldn't happen - // as listen() should only be called on sockets created for listening + + size_t read = 0; + uint8_t *buf8 = reinterpret_cast(buf); + while (len && this->rx_buf_ != nullptr) { + size_t pb_len = this->rx_buf_->len; + size_t pb_left = pb_len - this->rx_buf_offset_; + if (pb_left == 0) + break; + size_t copysize = std::min(len, pb_left); + memcpy(buf8, reinterpret_cast(this->rx_buf_->payload) + this->rx_buf_offset_, copysize); + + if (pb_left == copysize) { + // full pb copied, free it + if (this->rx_buf_->next == nullptr) { + // last buffer in chain + pbuf_free(this->rx_buf_); + this->rx_buf_ = nullptr; + this->rx_buf_offset_ = 0; + } else { + auto *old_buf = this->rx_buf_; + this->rx_buf_ = this->rx_buf_->next; + pbuf_ref(this->rx_buf_); + pbuf_free(old_buf); + this->rx_buf_offset_ = 0; + } + } else { + this->rx_buf_offset_ += copysize; + } + LWIP_LOG("tcp_recved(%p %u)", this->pcb_, copysize); + tcp_recved(this->pcb_, copysize); + + buf8 += copysize; + len -= copysize; + read += copysize; + } + + if (read == 0) { + errno = EWOULDBLOCK; + return -1; + } + + return read; +} + +ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + ssize_t ret = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = this->read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (ret != 0) { + // if we already read some don't return an error + break; + } + return err; + } + ret += err; + if ((size_t) err != iov[i].iov_len) + break; + } + return ret; +} + +ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (len == 0) + return 0; + if (buf == nullptr) { + errno = EINVAL; + return 0; + } + auto space = tcp_sndbuf(this->pcb_); + if (space == 0) { + errno = EWOULDBLOCK; + return -1; + } + size_t to_send = std::min((size_t) space, len); + LWIP_LOG("tcp_write(%p buf=%p %u)", this->pcb_, buf, to_send); + err_t err = tcp_write(this->pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); + if (err == ERR_MEM) { + LWIP_LOG(" -> err ERR_MEM"); + errno = EWOULDBLOCK; + return -1; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return to_send; +} + +int LWIPRawImpl::internal_output_() { + LWIP_LOG("tcp_output(%p)", this->pcb_); + err_t err = tcp_output(this->pcb_); + if (err == ERR_ABRT) { + // sometimes lwip returns ERR_ABRT for no apparent reason + // the connection works fine afterwards, and back with ESPAsyncTCP we + // indirectly also ignored this error + // FIXME: figure out where this is returned and what it means in this context + LWIP_LOG(" -> err ERR_ABRT"); + return 0; + } + if (err != ERR_OK) { + LWIP_LOG(" -> err %d", err); + errno = ECONNRESET; + return -1; + } + return 0; +} + +ssize_t LWIPRawImpl::write(const void *buf, size_t len) { + ssize_t written = this->internal_write_(buf, len); + if (written == -1) + return -1; + if (written == 0) { + // no need to output if nothing written + return 0; + } + if (this->nodelay_) { + int err = this->internal_output_(); + if (err == -1) + return -1; + } + return written; +} + +ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { + ssize_t written = 0; + for (int i = 0; i < iovcnt; i++) { + ssize_t err = this->internal_write_(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); + if (err == -1) { + if (written != 0) { + // if we already read some don't return an error + break; + } + return err; + } + written += err; + if ((size_t) err != iov[i].iov_len) + break; + } + if (written == 0) { + // no need to output if nothing written + return 0; + } + if (this->nodelay_) { + int err = this->internal_output_(); + if (err == -1) + return -1; + } + return written; +} + +// ---- LWIPRawListenImpl methods ---- + +LWIPRawListenImpl::~LWIPRawListenImpl() { + // Base class destructor handles pcb_ cleanup via tcp_abort +} + +void LWIPRawListenImpl::init() { + LWIP_LOG("init(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + tcp_err(this->pcb_, LWIPRawListenImpl::s_err_fn); +} + +void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { + auto *arg_this = reinterpret_cast(arg); + ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); + arg_this->pcb_ = nullptr; +} + +err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { + auto *arg_this = reinterpret_cast(arg); + return arg_this->accept_fn_(newpcb, err); +} + +std::unique_ptr LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return nullptr; + } + if (this->accepted_socket_count_ == 0) { + errno = EWOULDBLOCK; + return nullptr; + } + // Take from front for FIFO ordering + std::unique_ptr sock = std::move(this->accepted_sockets_[0]); + // Shift remaining sockets forward + for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { + this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]); + } + this->accepted_socket_count_--; + LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return sock; +} + +int LWIPRawListenImpl::listen(int backlog) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", this->pcb_, backlog); + struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(this->pcb_, backlog); + if (listen_pcb == nullptr) { + tcp_abort(this->pcb_); + this->pcb_ = nullptr; errno = EOPNOTSUPP; return -1; } - ssize_t read(void *buf, size_t len) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (rx_closed_ && rx_buf_ == nullptr) { - return 0; - } - if (len == 0) { - return 0; - } - if (rx_buf_ == nullptr) { - errno = EWOULDBLOCK; - return -1; - } + // tcp_listen reallocates the pcb, replace ours + this->pcb_ = listen_pcb; + // set callbacks on new pcb + LWIP_LOG("tcp_arg(%p)", this->pcb_); + tcp_arg(this->pcb_, this); + tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + return 0; +} - size_t read = 0; - uint8_t *buf8 = reinterpret_cast(buf); - while (len && rx_buf_ != nullptr) { - size_t pb_len = rx_buf_->len; - size_t pb_left = pb_len - rx_buf_offset_; - if (pb_left == 0) - break; - size_t copysize = std::min(len, pb_left); - memcpy(buf8, reinterpret_cast(rx_buf_->payload) + rx_buf_offset_, copysize); - - if (pb_left == copysize) { - // full pb copied, free it - if (rx_buf_->next == nullptr) { - // last buffer in chain - pbuf_free(rx_buf_); - rx_buf_ = nullptr; - rx_buf_offset_ = 0; - } else { - auto *old_buf = rx_buf_; - rx_buf_ = rx_buf_->next; - pbuf_ref(rx_buf_); - pbuf_free(old_buf); - rx_buf_offset_ = 0; - } - } else { - rx_buf_offset_ += copysize; - } - LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize); - tcp_recved(pcb_, copysize); - - buf8 += copysize; - len -= copysize; - read += copysize; - } - - if (read == 0) { - errno = EWOULDBLOCK; - return -1; - } - - return read; - } - ssize_t readv(const struct iovec *iov, int iovcnt) final { - ssize_t ret = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - if (err == -1) { - if (ret != 0) { - // if we already read some don't return an error - break; - } - return err; - } - ret += err; - if ((size_t) err != iov[i].iov_len) - break; - } - return ret; - } - - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) final { - errno = ENOTSUP; - return -1; - } - - ssize_t internal_write(const void *buf, size_t len) { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (len == 0) - return 0; - if (buf == nullptr) { - errno = EINVAL; - return 0; - } - auto space = tcp_sndbuf(pcb_); - if (space == 0) { - errno = EWOULDBLOCK; - return -1; - } - size_t to_send = std::min((size_t) space, len); - LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send); - err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY); - if (err == ERR_MEM) { - LWIP_LOG(" -> err ERR_MEM"); - errno = EWOULDBLOCK; - return -1; - } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = ECONNRESET; - return -1; - } - return to_send; - } - int internal_output() { - LWIP_LOG("tcp_output(%p)", pcb_); - err_t err = tcp_output(pcb_); - if (err == ERR_ABRT) { - LWIP_LOG(" -> err ERR_ABRT"); - // sometimes lwip returns ERR_ABRT for no apparent reason - // the connection works fine afterwards, and back with ESPAsyncTCP we - // indirectly also ignored this error - // FIXME: figure out where this is returned and what it means in this context - return 0; - } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = ECONNRESET; - return -1; - } - return 0; - } - ssize_t write(const void *buf, size_t len) final { - ssize_t written = internal_write(buf, len); - if (written == -1) - return -1; - if (written == 0) { - // no need to output if nothing written - return 0; - } - if (nodelay_) { - int err = internal_output(); - if (err == -1) - return -1; - } - return written; - } - ssize_t writev(const struct iovec *iov, int iovcnt) final { - ssize_t written = 0; - for (int i = 0; i < iovcnt; i++) { - ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); - if (err == -1) { - if (written != 0) { - // if we already read some don't return an error - break; - } - return err; - } - written += err; - if ((size_t) err != iov[i].iov_len) - break; - } - if (written == 0) { - // no need to output if nothing written - return 0; - } - if (nodelay_) { - int err = internal_output(); - if (err == -1) - return -1; - } - return written; - } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) final { - // return ::sendto(fd_, buf, len, flags, to, tolen); - errno = ENOSYS; - return -1; - } - bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } - - int setblocking(bool blocking) final { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } - return 0; - } - - void err_fn(err_t err) { - LWIP_LOG("err(err=%d)", err); - // "If a connection is aborted because of an error, the application is alerted of this event by - // the err callback." - // pcb is already freed when this callback is called - // ERR_RST: connection was reset by remote host - // ERR_ABRT: aborted through tcp_abort or TCP timer - pcb_ = nullptr; - } - err_t recv_fn(struct pbuf *pb, err_t err) { - LWIP_LOG("recv(pb=%p err=%d)", pb, err); - if (err != 0) { - // "An error code if there has been an error receiving Only return ERR_ABRT if you have - // called tcp_abort from within the callback function!" - rx_closed_ = true; - return ERR_OK; - } - if (pb == nullptr) { - rx_closed_ = true; - return ERR_OK; - } - if (rx_buf_ == nullptr) { - // no need to copy because lwIP gave control of it to us - rx_buf_ = pb; - rx_buf_offset_ = 0; - } else { - pbuf_cat(rx_buf_, pb); - } -#ifdef USE_ESP8266 - // Wake the main loop immediately so it can process the received data. - socket_wake(); -#endif +err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { + LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); + if (err != ERR_OK || newpcb == nullptr) { + // "An error code if there has been an error accepting. Only return ERR_ABRT if you have + // called tcp_abort from within the callback function!" + // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d + // nothing to do here, we just don't push it to the queue return ERR_OK; } - - static void s_err_fn(void *arg, err_t err) { - LWIPRawImpl *arg_this = reinterpret_cast(arg); - arg_this->err_fn(err); + // Check if we've reached the maximum accept queue size + if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) { + LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_); + // Abort the connection when queue is full + tcp_abort(newpcb); + // Must return ERR_ABRT since we called tcp_abort() + return ERR_ABRT; } - - static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { - LWIPRawImpl *arg_this = reinterpret_cast(arg); - return arg_this->recv_fn(pb, err); - } - - protected: - int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { - if (family_ == AF_INET) { - if (*addrlen < sizeof(struct sockaddr_in)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in *addr = reinterpret_cast(name); - addr->sin_family = AF_INET; - *addrlen = addr->sin_len = sizeof(struct sockaddr_in); - addr->sin_port = port; - inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); - return 0; - } -#if LWIP_IPV6 - else if (family_ == AF_INET6) { - if (*addrlen < sizeof(struct sockaddr_in6)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in6 *addr = reinterpret_cast(name); - addr->sin6_family = AF_INET6; - *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); - addr->sin6_port = port; - - // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6. - if (IP_IS_V4(ip)) { - ip_addr_t mapped; - ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); - } else { - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); - } - return 0; - } -#endif - return -1; - } - - // Member ordering optimized to minimize padding on 32-bit systems - // Largest members first (4 bytes), then smaller members (1 byte each) - struct tcp_pcb *pcb_; - pbuf *rx_buf_ = nullptr; - size_t rx_buf_offset_ = 0; - bool rx_closed_ = false; - // don't use lwip nodelay flag, it sometimes causes reconnect - // instead use it for determining whether to call lwip_output - bool nodelay_ = false; - sa_family_t family_ = 0; -}; - -// Listening socket class - only allocates accept queue when needed (for bind+listen sockets) -// This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 -class LWIPRawListenImpl final : public LWIPRawImpl { - public: - LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} - - void init() { - LWIP_LOG("init(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); - tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler - } - - bool ready() const override { return this->accepted_socket_count_ > 0; } - - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - if (pcb_ == nullptr) { - errno = EBADF; - return nullptr; - } - if (accepted_socket_count_ == 0) { - errno = EWOULDBLOCK; - return nullptr; - } - // Take from front for FIFO ordering - std::unique_ptr sock = std::move(accepted_sockets_[0]); - // Shift remaining sockets forward - for (uint8_t i = 1; i < accepted_socket_count_; i++) { - accepted_sockets_[i - 1] = std::move(accepted_sockets_[i]); - } - accepted_socket_count_--; - LWIP_LOG("Connection accepted by application, queue size: %d", accepted_socket_count_); - if (addr != nullptr) { - sock->getpeername(addr, addrlen); - } - LWIP_LOG("accept(%p)", sock.get()); - return std::unique_ptr(std::move(sock)); - } - - int listen(int backlog) override { - if (pcb_ == nullptr) { - errno = EBADF; - return -1; - } - LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog); - struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog); - if (listen_pcb == nullptr) { - tcp_abort(pcb_); - pcb_ = nullptr; - errno = EOPNOTSUPP; - return -1; - } - // tcp_listen reallocates the pcb, replace ours - pcb_ = listen_pcb; - // set callbacks on new pcb - LWIP_LOG("tcp_arg(%p)", pcb_); - tcp_arg(pcb_, this); - tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn); - return 0; - } - - private: - err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) { - LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); - if (err != ERR_OK || newpcb == nullptr) { - // "An error code if there has been an error accepting. Only return ERR_ABRT if you have - // called tcp_abort from within the callback function!" - // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d - // nothing to do here, we just don't push it to the queue - return ERR_OK; - } - // Check if we've reached the maximum accept queue size - if (accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) { - LWIP_LOG("Rejecting connection, queue full (%d)", accepted_socket_count_); - // Abort the connection when queue is full - tcp_abort(newpcb); - // Must return ERR_ABRT since we called tcp_abort() - return ERR_ABRT; - } - auto sock = make_unique(family_, newpcb); - sock->init(); - accepted_sockets_[accepted_socket_count_++] = std::move(sock); - LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); + auto sock = make_unique(this->family_, newpcb); + sock->init(); + this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); + LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); #ifdef USE_ESP8266 - // Wake the main loop immediately so it can accept the new connection. - socket_wake(); + // Wake the main loop immediately so it can accept the new connection. + socket_wake(); #endif - return ERR_OK; - } + return ERR_OK; +} - static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { - LWIPRawListenImpl *arg_this = reinterpret_cast(arg); - return arg_this->accept_fn_(newpcb, err); - } - - // Accept queue - holds incoming connections briefly until the event loop calls accept() - // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop - // 3 slots is plenty since connections are pulled out quickly by the event loop - // - // Memory analysis: std::array<3> vs original std::queue implementation: - // - std::queue uses std::deque internally which on 32-bit systems needs: - // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations - // Total: ~56+ bytes minimum, plus heap fragmentation - // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) - // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations - // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) - // - // By using a separate listening socket class, regular connected sockets save - // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems - static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; - std::array, MAX_ACCEPTED_SOCKETS> accepted_sockets_; - uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue -}; +// ---- Factory functions ---- std::unique_ptr socket(int domain, int type, int protocol) { if (type != SOCK_STREAM) { @@ -688,9 +651,7 @@ std::unique_ptr socket(int domain, int type, int protocol) { auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; - // Create listening socket implementation since user sockets typically bind+listen - // Accepted connections are created directly as LWIPRawImpl in the accept callback - auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) + auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) sock->init(); return std::unique_ptr{sock}; } @@ -700,6 +661,25 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return socket(domain, type, protocol); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + if (type != SOCK_STREAM) { + ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + errno = EPROTOTYPE; + return nullptr; + } + auto *pcb = tcp_new(); + if (pcb == nullptr) + return nullptr; + auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory) + sock->init(); + return std::unique_ptr{sock}; +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + // LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable + return socket_listen(domain, type, protocol); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h new file mode 100644 index 00000000000..c171e0537f3 --- /dev/null +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -0,0 +1,200 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_TCP + +#include +#include +#include +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/opt.h" +#include "lwip/tcp.h" + +namespace esphome::socket { + +// Forward declaration +class LWIPRawImpl; + +/// Non-virtual common base for LWIP raw TCP sockets. +/// Provides shared fields and methods for both connected and listening sockets. +/// No virtual methods — pure code sharing. +class LWIPRawCommon { + public: + LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {} + ~LWIPRawCommon(); + LWIPRawCommon(const LWIPRawCommon &) = delete; + LWIPRawCommon &operator=(const LWIPRawCommon &) = delete; + + int bind(const struct sockaddr *name, socklen_t addrlen); + int close(); + int shutdown(int how); + + int getpeername(struct sockaddr *name, socklen_t *addrlen); + int getsockname(struct sockaddr *name, socklen_t *addrlen); + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen); + int setsockopt(int level, int optname, const void *optval, socklen_t optlen); + + int get_fd() const { return -1; } + + protected: + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen); + + // Member ordering optimized to minimize padding on 32-bit systems + struct tcp_pcb *pcb_; + // don't use lwip nodelay flag, it sometimes causes reconnect + // instead use it for determining whether to call lwip_output + bool nodelay_ = false; + sa_family_t family_ = 0; +}; + +/// Connected socket implementation for LWIP raw TCP. +/// No virtual methods — callers always use the concrete type. +class LWIPRawImpl : public LWIPRawCommon { + public: + using LWIPRawCommon::LWIPRawCommon; + ~LWIPRawImpl(); + + void init(); + + // Non-listening sockets return error + std::unique_ptr accept(struct sockaddr *, socklen_t *) { + errno = EINVAL; + return nullptr; + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + return this->accept(addr, addrlen); + } + // Regular sockets can't be converted to listening - this shouldn't happen + // as listen() should only be called on sockets created for listening + int listen(int) { + errno = EOPNOTSUPP; + return -1; + } + ssize_t read(void *buf, size_t len); + ssize_t readv(const struct iovec *iov, int iovcnt); + ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) { + errno = ENOTSUP; + return -1; + } + ssize_t write(const void *buf, size_t len); + ssize_t writev(const struct iovec *iov, int iovcnt); + ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) { + // return ::sendto(fd_, buf, len, flags, to, tolen); + errno = ENOSYS; + return -1; + } + bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + + int setblocking(bool blocking) { + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (blocking) { + // blocking operation not supported + errno = EINVAL; + return -1; + } + return 0; + } + int loop() { return 0; } + + err_t recv_fn(struct pbuf *pb, err_t err); + + static void s_err_fn(void *arg, err_t err); + static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); + + protected: + ssize_t internal_write_(const void *buf, size_t len); + int internal_output_(); + + pbuf *rx_buf_ = nullptr; + size_t rx_buf_offset_ = 0; + bool rx_closed_ = false; +}; + +/// Listening socket implementation for LWIP raw TCP. +/// Separate from LWIPRawImpl — no virtual dispatch needed. +class LWIPRawListenImpl : public LWIPRawCommon { + public: + using LWIPRawCommon::LWIPRawCommon; + ~LWIPRawListenImpl(); + + void init(); + + bool ready() const { return this->accepted_socket_count_ > 0; } + + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen); + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + return this->accept(addr, addrlen); + } + int listen(int backlog); + + // Listening sockets don't do I/O + ssize_t read(void *, size_t) { + errno = ENOTSUP; + return -1; + } + ssize_t write(const void *, size_t) { + errno = ENOTSUP; + return -1; + } + ssize_t readv(const struct iovec *, int) { + errno = ENOTSUP; + return -1; + } + ssize_t writev(const struct iovec *, int) { + errno = ENOTSUP; + return -1; + } + ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) { + errno = ENOTSUP; + return -1; + } + ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) { + errno = ENOTSUP; + return -1; + } + int setblocking(bool) { return 0; } + int loop() { return 0; } + + static void s_err_fn(void *arg, err_t err); + + private: + err_t accept_fn_(struct tcp_pcb *newpcb, err_t err); + static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err); + + // Accept queue - holds incoming connections briefly until the event loop calls accept() + // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop + // 3 slots is plenty since connections are pulled out quickly by the event loop + // + // Memory analysis: std::array<3> vs original std::queue implementation: + // - std::queue uses std::deque internally which on 32-bit systems needs: + // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations + // Total: ~56+ bytes minimum, plus heap fragmentation + // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) + // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations + // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) + // + // By using a separate listening socket class, regular connected sockets save + // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems + static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; + std::array, MAX_ACCEPTED_SOCKETS> accepted_sockets_; + uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 79d68e085ae..0322820ef43 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -1,6 +1,6 @@ -#include "socket.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "socket.h" #ifdef USE_SOCKET_IMPL_LWIP_SOCKETS @@ -9,94 +9,73 @@ namespace esphome::socket { -class LwIPSocketImpl final : public Socket { - public: - LwIPSocketImpl(int fd, bool monitor_loop = false) { - this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } - } - ~LwIPSocketImpl() override { - if (!this->closed_) { - this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) - } - } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { - return lwip_connect(this->fd_, addr, addrlen); - } - std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = lwip_accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - int fd = lwip_accept(this->fd_, addr, addrlen); - if (fd == -1) - return {}; - return make_unique(fd, true); +LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { + this->fd_ = fd; + // Register new socket with the application for select() if monitoring requested + if (monitor_loop && this->fd_ >= 0) { + // Only set loop_monitored_ to true if registration succeeds + this->loop_monitored_ = App.register_socket_fd(this->fd_); } +} - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } - int close() override { - if (!this->closed_) { - // Unregister from select() before closing if monitored - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } - int ret = lwip_close(this->fd_); - this->closed_ = true; - return ret; +LwIPSocketImpl::~LwIPSocketImpl() { + if (!this->closed_) { + this->close(); + } +} + +int LwIPSocketImpl::close() { + if (!this->closed_) { + // Unregister from select() before closing if monitored + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } + int ret = lwip_close(this->fd_); + this->closed_ = true; + return ret; + } + return 0; +} + +int LwIPSocketImpl::setblocking(bool blocking) { + int fl = lwip_fcntl(this->fd_, F_GETFL, 0); + if (blocking) { + fl &= ~O_NONBLOCK; + } else { + fl |= O_NONBLOCK; + } + lwip_fcntl(this->fd_, F_SETFL, fl); + return 0; +} + +bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } + +size_t LwIPSocketImpl::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } - int shutdown(int how) override { return lwip_shutdown(this->fd_, how); } + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { - return lwip_getpeername(this->fd_, addr, addrlen); - } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { - return lwip_getsockname(this->fd_, addr, addrlen); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return lwip_getsockopt(this->fd_, level, optname, optval, optlen); - } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return lwip_setsockopt(this->fd_, level, optname, optval, optlen); - } - int listen(int backlog) override { return lwip_listen(this->fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); } - ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { - return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); - } - ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); } - ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return lwip_sendto(this->fd_, buf, len, flags, to, tolen); - } - int setblocking(bool blocking) override { - int fl = lwip_fcntl(this->fd_, F_GETFL, 0); - if (blocking) { - fl &= ~O_NONBLOCK; - } else { - fl |= O_NONBLOCK; - } - lwip_fcntl(this->fd_, F_SETFL, fl); +size_t LwIPSocketImpl::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; return 0; } -}; + return format_sockaddr_to(reinterpret_cast(&storage), len, buf); +} // Helper to create a socket with optional monitoring -static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { +static std::unique_ptr create_socket(int domain, int type, int protocol, bool loop_monitored = false) { int ret = lwip_socket(domain, type, protocol); if (ret == -1) return nullptr; - return std::unique_ptr{new LwIPSocketImpl(ret, loop_monitored)}; + return make_unique(ret, loop_monitored); } std::unique_ptr socket(int domain, int type, int protocol) { @@ -107,6 +86,14 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } +std::unique_ptr socket_listen(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, false); +} + +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return create_socket(domain, type, protocol, true); +} + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h new file mode 100644 index 00000000000..2e319fcc4df --- /dev/null +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -0,0 +1,80 @@ +#pragma once +#include "esphome/core/defines.h" + +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS + +#include +#include + +#include "esphome/core/helpers.h" +#include "headers.h" + +namespace esphome::socket { + +class LwIPSocketImpl { + public: + LwIPSocketImpl(int fd, bool monitor_loop = false); + ~LwIPSocketImpl(); + LwIPSocketImpl(const LwIPSocketImpl &) = delete; + LwIPSocketImpl &operator=(const LwIPSocketImpl &) = delete; + + int connect(const struct sockaddr *addr, socklen_t addrlen) { return lwip_connect(this->fd_, addr, addrlen); } + std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); + } + + int bind(const struct sockaddr *addr, socklen_t addrlen) { return lwip_bind(this->fd_, addr, addrlen); } + int close(); + int shutdown(int how) { return lwip_shutdown(this->fd_, how); } + + int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getpeername(this->fd_, addr, addrlen); } + int getsockname(struct sockaddr *addr, socklen_t *addrlen) { return lwip_getsockname(this->fd_, addr, addrlen); } + + /// Format peer address into a fixed-size buffer (no heap allocation) + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + size_t getsockname_to(std::span buf); + + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + return lwip_getsockopt(this->fd_, level, optname, optval, optlen); + } + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + return lwip_setsockopt(this->fd_, level, optname, optval, optlen); + } + int listen(int backlog) { return lwip_listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) { return lwip_read(this->fd_, buf, len); } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) { + return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); + } + ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); } + ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); } + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { + return lwip_sendto(this->fd_, buf, len, flags, to, tolen); + } + int setblocking(bool blocking); + int loop() { return 0; } + + bool ready() const; + + int get_fd() const { return this->fd_; } + + protected: + int fd_{-1}; + bool closed_{false}; + bool loop_monitored_{false}; +}; + +} // namespace esphome::socket + +#endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 6154c497e0c..c04671c7ee8 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,10 +8,10 @@ namespace esphome::socket { -Socket::~Socket() {} - #ifdef USE_SOCKET_SELECT_SUPPORT -bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); } +// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). +// Checks if the Application's select() loop has marked this fd as ready. +bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } #endif // Platform-specific inet_ntop wrappers @@ -81,26 +81,6 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s return 0; } -size_t Socket::getpeername_to(std::span buf) { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { - buf[0] = '\0'; - return 0; - } - return format_sockaddr_to(reinterpret_cast(&storage), len, buf); -} - -size_t Socket::getsockname_to(std::span buf) { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { - buf[0] = '\0'; - return 0; - } - return format_sockaddr_to(reinterpret_cast(&storage), len, buf); -} - std::unique_ptr socket_ip(int type, int protocol) { #if USE_NETWORK_IPV6 return socket(AF_INET6, type, protocol); @@ -109,11 +89,11 @@ std::unique_ptr socket_ip(int type, int protocol) { #endif /* USE_NETWORK_IPV6 */ } -std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { +std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { #if USE_NETWORK_IPV6 - return socket_loop_monitored(AF_INET6, type, protocol); + return socket_listen_loop_monitored(AF_INET6, type, protocol); #else - return socket_loop_monitored(AF_INET, type, protocol); + return socket_listen_loop_monitored(AF_INET, type, protocol); #endif /* USE_NETWORK_IPV6 */ } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a771e2fe1a9..86a4f0cba98 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -7,87 +7,41 @@ #include "headers.h" #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) + +// Include only the active implementation's header. +// SOCKADDR_STR_LEN is defined in headers.h. +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +#include "bsd_sockets_impl.h" +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +#include "lwip_sockets_impl.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include "lwip_raw_tcp_impl.h" +#endif + namespace esphome::socket { -// Maximum length for formatted socket address string (IP address without port) -// IPv4: "255.255.255.255" = 15 chars + null = 16 -// IPv6: full address = 45 chars + null = 46 -#if USE_NETWORK_IPV6 -static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN -#else -static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +// Type aliases — only one implementation is active per build. +// Socket is the concrete type for connected sockets. +// ListenSocket is the concrete type for listening/server sockets. +// On BSD and LWIP_SOCKETS, both aliases resolve to the same type. +// On LWIP_TCP, they are different types (no virtual dispatch between them). +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS +using Socket = BSDSocketImpl; +using ListenSocket = BSDSocketImpl; +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +using Socket = LwIPSocketImpl; +using ListenSocket = LwIPSocketImpl; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +using Socket = LWIPRawImpl; +using ListenSocket = LWIPRawListenImpl; #endif -class Socket { - public: - Socket() = default; - virtual ~Socket(); - Socket(const Socket &) = delete; - Socket &operator=(const Socket &) = delete; - - virtual std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) = 0; - /// Accept a connection and monitor it in the main loop - /// NOTE: This function is NOT thread-safe and must only be called from the main loop - virtual std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) { - return accept(addr, addrlen); // Default implementation for backward compatibility - } - virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0; - virtual int close() = 0; - // not supported yet: - // virtual int connect(const std::string &address) = 0; -#if defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) - virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0; -#endif - virtual int shutdown(int how) = 0; - - virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; - - /// Format peer address into a fixed-size buffer (no heap allocation) - /// Non-virtual wrapper around getpeername() - can be optimized away if unused - /// Returns number of characters written (excluding null terminator), or 0 on error - size_t getpeername_to(std::span buf); - /// Format local address into a fixed-size buffer (no heap allocation) - /// Non-virtual wrapper around getsockname() - can be optimized away if unused - size_t getsockname_to(std::span buf); - virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; - virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; - virtual int listen(int backlog) = 0; - virtual ssize_t read(void *buf, size_t len) = 0; - virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0; - virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0; - virtual ssize_t write(const void *buf, size_t len) = 0; - virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0; - virtual ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) = 0; - - virtual int setblocking(bool blocking) = 0; - virtual int loop() { return 0; }; - - /// Get the underlying file descriptor (returns -1 if not supported) - /// Non-virtual: only one socket implementation is active per build. #ifdef USE_SOCKET_SELECT_SUPPORT - int get_fd() const { return this->fd_; } -#else - int get_fd() const { return -1; } +/// Shared ready() helper for fd-based socket implementations. +/// Checks if the Application's select() loop has marked this fd as ready. +bool socket_ready_fd(int fd, bool loop_monitored); #endif - /// Check if socket has data ready to read. Must only be called from the main loop thread. - /// For select()-based sockets: non-virtual, checks Application's select() results - /// For LWIP raw TCP sockets: virtual, checks internal buffer state -#ifdef USE_SOCKET_SELECT_SUPPORT - bool ready() const; -#else - virtual bool ready() const { return true; } -#endif - - protected: -#ifdef USE_SOCKET_SELECT_SUPPORT - int fd_{-1}; - bool closed_{false}; - bool loop_monitored_{false}; -#endif -}; - /// Create a socket of the given domain, type and protocol. std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. @@ -100,7 +54,13 @@ std::unique_ptr socket_ip(int type, int protocol); /// NOTE: On ESP platforms, FD_SETSIZE is typically 10, limiting the number of monitored sockets. /// File descriptors >= FD_SETSIZE will not be monitored and will log an error. std::unique_ptr socket_loop_monitored(int domain, int type, int protocol); -std::unique_ptr socket_ip_loop_monitored(int type, int protocol); + +/// Create a listening socket of the given domain, type and protocol. +std::unique_ptr socket_listen(int domain, int type, int protocol); +/// Create a listening socket and monitor it for data in the main loop. +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); +/// Create a listening socket in the newest available IP domain and monitor it. +std::unique_ptr socket_ip_loop_monitored(int type, int protocol); /// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). /// @param addr Destination sockaddr structure diff --git a/esphome/core/application.h b/esphome/core/application.h index d2345e2b0b6..659b60222d9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -105,7 +105,10 @@ #endif namespace esphome::socket { -class Socket; +#ifdef USE_SOCKET_SELECT_SUPPORT +/// Shared ready() helper for fd-based socket implementations. +bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) +#endif } // namespace esphome::socket // Forward declarations for friend access from codegen-generated setup() @@ -520,7 +523,9 @@ class Application { protected: friend Component; - friend class socket::Socket; +#ifdef USE_SOCKET_SELECT_SUPPORT + friend bool socket::socket_ready_fd(int fd, bool loop_monitored); +#endif friend void ::setup(); friend void ::original_setup(); From a1d91ac779458545b0a6e5f014f57bc6d31d87ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 06:59:23 -1000 Subject: [PATCH 0323/2056] [core] Compile-time detection of loop() overrides (#14405) --- esphome/core/application.cpp | 19 +++++++-------- esphome/core/application.h | 10 +++++++- esphome/core/component.cpp | 12 ---------- esphome/core/component.h | 7 ++++-- esphome/core/config.py | 36 ++++++++++++++++++++++++++++ esphome/cpp_helpers.py | 5 ++++ tests/unit_tests/test_cpp_helpers.py | 12 ++++++++-- 7 files changed, 73 insertions(+), 28 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c977fd66b3a..26cd6706295 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -79,7 +79,12 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) { } } -void Application::register_component_(Component *comp) { this->components_.push_back(comp); } +void Application::register_component_impl_(Component *comp, bool has_loop) { + if (has_loop) { + comp->component_state_ |= COMPONENT_HAS_LOOP; + } + this->components_.push_back(comp); +} void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); @@ -382,16 +387,8 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { - // Count total components that need looping - size_t total_looping = 0; - for (auto *obj : this->components_) { - if (obj->has_overridden_loop()) { - total_looping++; - } - } - - // Initialize FixedVector with exact size - no reallocation possible - this->looping_components_.init(total_looping); + // FixedVector capacity was pre-initialized by codegen with the exact count + // of components that override loop(), computed at C++ compile time. // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization diff --git a/esphome/core/application.h b/esphome/core/application.h index 659b60222d9..44e8de7ee9f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -542,7 +543,14 @@ class Application { #endif #endif - void register_component_(Component *comp); + /// Register a component, detecting loop() override at compile time. + /// The template resolves &T::loop vs &Component::loop as a constexpr bool + /// and forwards it to register_component_impl_ which stores it in component_state_. + template void register_component_(T *comp) { + this->register_component_impl_(comp, !std::is_same_v); + } + + void register_component_impl_(Component *comp, bool has_loop); void calculate_looping_components_(); void add_looping_components_by_state_(bool match_loop_done); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5afd901da23..a71aa8b3a31 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -496,18 +496,6 @@ void Component::set_setup_priority(float priority) { } #endif -bool Component::has_overridden_loop() const { -#if defined(USE_HOST) || defined(CLANG_TIDY) - return true; -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpmf-conversions" - bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); -#pragma GCC diagnostic pop - return loop_overridden; -#endif -} - PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} void PollingComponent::call_setup() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 6b920da2901..d8102ea6708 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -76,6 +76,8 @@ inline constexpr uint8_t STATUS_LED_MASK = 0x18; inline constexpr uint8_t STATUS_LED_OK = 0x00; inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; +// Component loop override flag uses bit 5 (set at registration time) +inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; @@ -271,7 +273,7 @@ class Component { */ void status_momentary_error(const char *name, uint32_t length = 5000); - bool has_overridden_loop() const; + bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } /** Set where this component was loaded from for some debug messages. * @@ -510,7 +512,8 @@ class Component { /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR - /// Bits 5-7: Unused - reserved for future expansion + /// Bit 5: Has overridden loop() (set at registration time) + /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; diff --git a/esphome/core/config.py b/esphome/core/config.py index 593b4022303..3835fd3875f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import Counter import logging import os from pathlib import Path @@ -504,6 +505,40 @@ async def _add_controller_registry_define() -> None: cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_looping_components() -> None: + # Emit a constexpr that computes the looping component count at C++ compile time + # and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to + # detect loop() overrides. The constexpr goes in main.cpp's global section where + # all component types are in scope. calculate_looping_components_() then skips + # the counting pass and only does the two population passes. + entries = CORE.data.get("looping_component_entries", []) + if not entries: + return + + # Build constexpr sum for the exact count, deduplicating by type + type_counts = Counter(entries) + terms = [ + f"({count} * !std::is_same_v)" + for cpp_type, count in type_counts.items() + ] + constexpr_expr = " + \\\n ".join(terms) + cg.add_global( + cg.RawStatement( + f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n" + f" {constexpr_expr};" + ) + ) + + # Pre-init FixedVector with exact capacity so calculate_looping_components_() + # can skip the counting pass + cg.add( + cg.RawExpression( + "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" + ) + ) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) @@ -527,6 +562,7 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(_add_platform_defines) CORE.add_job(_add_controller_registry_define) + CORE.add_job(_add_looping_components) CORE.add_job(_add_automations, config) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b673eaa7e1f..8f8c6931403 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -80,6 +80,11 @@ async def register_component(var, config): add(var.set_component_source(LogStringLiteral(name))) add(App.register_component_(var)) + + # Collect C++ type for compile-time looping component count + comp_entries = CORE.data.setdefault("looping_component_entries", []) + comp_entries.append(str(var.base.type)) + return var diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 82ded409c7c..5b6eed156fa 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -14,7 +14,11 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): @pytest.mark.asyncio async def test_register_component(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) @@ -46,7 +50,11 @@ async def test_register_component__no_component_id(monkeypatch): @pytest.mark.asyncio async def test_register_component__with_setup_priority(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) From b9b1af1c3dd11e2d41c0b88542637a6ceaec5ecd Mon Sep 17 00:00:00 2001 From: netixx Date: Mon, 2 Mar 2026 18:38:28 +0100 Subject: [PATCH 0324/2056] [mcp23016] Fix register access to use 16-bit paired transactions (#13676) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/mcp23016/mcp23016.cpp | 71 +++++++++--------------- esphome/components/mcp23016/mcp23016.h | 15 +++-- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 56b2ecf9f4e..fbdb6903b84 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -8,90 +8,71 @@ namespace mcp23016 { static const char *const TAG = "mcp23016"; void MCP23016::setup() { - uint8_t iocon; - if (!this->read_reg_(MCP23016_IOCON0, &iocon)) { + uint16_t iocon; + // MCP23016 registers operate as paired 16-bit registers. Addressing the + // odd register (e.g. IOCON1) reads/writes that register first, then wraps + // to the even register (IOCON0) in the same pair. Starting from the odd + // address gives the correct byte order for 1 << pin mapping: + // high byte = port 1 (pins 8-15), low byte = port 0 (pins 0-7). + if (!this->read_reg_(MCP23016_IOCON1, &iocon)) { this->mark_failed(); return; } // Read current output register state - this->read_reg_(MCP23016_OLAT0, &this->olat_0_); - this->read_reg_(MCP23016_OLAT1, &this->olat_1_); + this->read_reg_(MCP23016_OLAT1, &this->olat_); // all pins input - this->write_reg_(MCP23016_IODIR0, 0xFF); - this->write_reg_(MCP23016_IODIR1, 0xFF); + this->write_reg_(MCP23016_IODIR1, 0xFFFF); } void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); } -bool MCP23016::digital_read_hw(uint8_t pin) { - uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; - uint8_t value = 0; - if (!this->read_reg_(reg_addr, &value)) { - return false; - } - - // Update the appropriate part of input_mask_ - if (pin < 8) { - this->input_mask_ = (this->input_mask_ & 0xFF00) | value; - } else { - this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); - } - return true; -} +bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); } bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } -void MCP23016::digital_write_hw(uint8_t pin, bool value) { - uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; - this->update_reg_(pin, value, reg_addr); -} +void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin, value, MCP23016_OLAT1); } void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { - uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1; if (flags == gpio::FLAG_INPUT) { - this->update_reg_(pin, true, iodir); + this->update_reg_(pin, true, MCP23016_IODIR1); } else if (flags == gpio::FLAG_OUTPUT) { - this->update_reg_(pin, false, iodir); + this->update_reg_(pin, false, MCP23016_IODIR1); } } -float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } -bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { +float MCP23016::get_setup_priority() const { return setup_priority::IO; } +bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) { if (this->is_failed()) return false; - return this->read_byte(reg, value); + return this->read_byte_16(reg, value); } -bool MCP23016::write_reg_(uint8_t reg, uint8_t value) { +bool MCP23016::write_reg_(uint8_t reg, uint16_t value) { if (this->is_failed()) return false; - return this->write_byte(reg, value); + return this->write_byte_16(reg, value); } void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { - uint8_t bit = pin % 8; - uint8_t reg_value = 0; - if (reg_addr == MCP23016_OLAT0) { - reg_value = this->olat_0_; - } else if (reg_addr == MCP23016_OLAT1) { - reg_value = this->olat_1_; + uint16_t reg_value = 0; + + if (reg_addr == MCP23016_OLAT1) { + reg_value = this->olat_; } else { this->read_reg_(reg_addr, ®_value); } if (pin_value) { - reg_value |= 1 << bit; + reg_value |= 1 << pin; } else { - reg_value &= ~(1 << bit); + reg_value &= ~(1 << pin); } this->write_reg_(reg_addr, reg_value); - if (reg_addr == MCP23016_OLAT0) { - this->olat_0_ = reg_value; - } else if (reg_addr == MCP23016_OLAT1) { - this->olat_1_ = reg_value; + if (reg_addr == MCP23016_OLAT1) { + this->olat_ = reg_value; } } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index c2bc885c958..494bc9c197a 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -19,13 +19,13 @@ enum MCP23016GPIORegisters { // 1 side MCP23016_GP1 = 0x01, MCP23016_OLAT1 = 0x03, - MCP23016_IPOL1 = 0x04, + MCP23016_IPOL1 = 0x05, MCP23016_IODIR1 = 0x07, - MCP23016_INTCAP1 = 0x08, + MCP23016_INTCAP1 = 0x09, MCP23016_IOCON1 = 0x0B, }; -class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { +class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { public: MCP23016() = default; @@ -42,16 +42,15 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: void digital_write_hw(uint8_t pin, bool value) override; // read a given register - bool read_reg_(uint8_t reg, uint8_t *value); + bool read_reg_(uint8_t reg, uint16_t *value); // write a value to a given register - bool write_reg_(uint8_t reg, uint8_t value); + bool write_reg_(uint8_t reg, uint16_t value); // update registers with given pin value. void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); - uint8_t olat_0_{0x00}; - uint8_t olat_1_{0x00}; + uint16_t olat_{0x0000}; // Cache for input values (16-bit combined for both banks) - uint16_t input_mask_{0x00}; + uint16_t input_mask_{0x0000}; }; class MCP23016GPIOPin : public GPIOPin { From 2fa244715d21e30338702c0ea017884f338efd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 08:54:54 -1000 Subject: [PATCH 0325/2056] [socket] Fix pre-existing bugs found during socket devirtualization review (#14404) --- esphome/components/socket/bsd_sockets_impl.h | 2 +- .../components/socket/lwip_raw_tcp_impl.cpp | 21 +++++++++++++++++-- esphome/components/socket/lwip_sockets_impl.h | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index edee39f3154..d9ed9dc567c 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -83,7 +83,7 @@ class BSDSocketImpl { return ::write(this->fd_, buf, len); #endif } - ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } + ssize_t send(const void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) { #if defined(USE_ESP32) return ::lwip_writev(this->fd_, iov, iovcnt); diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 6556979fe01..430356592fa 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -68,7 +68,7 @@ int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { } if (name == nullptr) { errno = EINVAL; - return 0; + return -1; } ip_addr_t ip; in_port_t port; @@ -319,6 +319,13 @@ int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *n // ---- LWIPRawImpl methods ---- LWIPRawImpl::~LWIPRawImpl() { + // Free any received pbufs that LWIP transferred ownership of via recv_fn. + // tcp_abort() in the base destructor won't free these since LWIP considers + // ownership transferred once the recv callback accepts them. + if (this->rx_buf_ != nullptr) { + pbuf_free(this->rx_buf_); + this->rx_buf_ = nullptr; + } // Base class destructor handles pcb_ cleanup via tcp_abort } @@ -545,7 +552,14 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { // ---- LWIPRawListenImpl methods ---- LWIPRawListenImpl::~LWIPRawListenImpl() { - // Base class destructor handles pcb_ cleanup via tcp_abort + // Listen PCBs must use tcp_close(), not tcp_abort(). + // tcp_abandon() asserts pcb->state != LISTEN and would access + // fields that don't exist in the smaller tcp_pcb_listen struct. + // Close here and null pcb_ so the base destructor skips tcp_abort. + if (this->pcb_ != nullptr) { + tcp_close(this->pcb_); + this->pcb_ = nullptr; + } } void LWIPRawListenImpl::init() { @@ -609,6 +623,9 @@ int LWIPRawListenImpl::listen(int backlog) { LWIP_LOG("tcp_arg(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); + // Note: tcp_err() is NOT re-registered here. tcp_listen_with_backlog() converts the + // full tcp_pcb to a smaller tcp_pcb_listen struct that lacks the errf field. + // Calling tcp_err() on a listen PCB writes past the struct boundary (undefined behavior). return 0; } diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index 2e319fcc4df..d6699aded26 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -57,7 +57,7 @@ class LwIPSocketImpl { } ssize_t readv(const struct iovec *iov, int iovcnt) { return lwip_readv(this->fd_, iov, iovcnt); } ssize_t write(const void *buf, size_t len) { return lwip_write(this->fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t send(const void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) { return lwip_writev(this->fd_, iov, iovcnt); } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) { return lwip_sendto(this->fd_, buf, len, flags, to, tolen); From cb232d828879b365bfa116ff3b4a7546c1e6ce62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 09:11:47 -1000 Subject: [PATCH 0326/2056] [core] Fix compile-time loop() detection for multiple inheritance (#14411) --- esphome/core/application.h | 13 ++++++++++--- esphome/core/config.py | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 44e8de7ee9f..13fd0180ab2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -118,6 +118,14 @@ void original_setup(); // NOLINT(readability-redundant-declaration) - used by c namespace esphome { +/// SFINAE helper: detects whether T overrides Component::loop(). +/// When &T::loop is ambiguous (multiple inheritance with separate loop() methods), +/// the ambiguity itself proves an override exists, so the true_type default is correct. +template struct HasLoopOverride : std::true_type {}; +template +struct HasLoopOverride> + : std::bool_constant> {}; + // Teardown timeout constant (in milliseconds) // For reboots, it's more important to shut down quickly than disconnect cleanly // since we're not entering deep sleep. The only consequence of not shutting down @@ -544,10 +552,9 @@ class Application { #endif /// Register a component, detecting loop() override at compile time. - /// The template resolves &T::loop vs &Component::loop as a constexpr bool - /// and forwards it to register_component_impl_ which stores it in component_state_. + /// Uses HasLoopOverride which handles ambiguous &T::loop from multiple inheritance. template void register_component_(T *comp) { - this->register_component_impl_(comp, !std::is_same_v); + this->register_component_impl_(comp, HasLoopOverride::value); } void register_component_impl_(Component *comp, bool has_loop); diff --git a/esphome/core/config.py b/esphome/core/config.py index 3835fd3875f..9411949bb92 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -517,9 +517,10 @@ async def _add_looping_components() -> None: return # Build constexpr sum for the exact count, deduplicating by type + # Uses HasLoopOverride which handles ambiguous &T::loop from multiple inheritance type_counts = Counter(entries) terms = [ - f"({count} * !std::is_same_v)" + f"({count} * HasLoopOverride<{cpp_type}>::value)" for cpp_type, count in type_counts.items() ] constexpr_expr = " + \\\n ".join(terms) From 38f671a9231348919a2ffd779ebf070906cbdef9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:52:06 -0500 Subject: [PATCH 0327/2056] [uart] Fix flow_control_pin inverted flag ignored on ESP-IDF (#14410) Co-authored-by: Claude Opus 4.6 --- esphome/components/uart/uart_component_esp_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 631db1e5c73..544404448b9 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -157,6 +157,9 @@ void IDFUARTComponent::load_settings(bool dump_config) { if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; } + if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) { + invert |= UART_SIGNAL_RTS_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { From 48a9c1cd67c56ff17ea095f98815e114e8473035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 11:12:35 -0600 Subject: [PATCH 0328/2056] [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) --- esphome/__main__.py | 14 --------- esphome/components/mqtt/__init__.py | 21 -------------- .../components/mqtt/mqtt_backend_esp8266.h | 5 ---- .../components/mqtt/mqtt_backend_libretiny.h | 5 ---- esphome/components/mqtt/mqtt_client.cpp | 7 ----- esphome/components/mqtt/mqtt_client.h | 15 ---------- esphome/const.py | 1 - esphome/mqtt.py | 29 ++----------------- 8 files changed, 2 insertions(+), 95 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c86b5604e14..488955f5030 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,12 +944,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1237,7 +1231,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1451,13 +1444,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index fe153fedfa2..44e88364873 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,5 +1,3 @@ -import re - from esphome import automation from esphome.automation import Condition import esphome.codegen as cg @@ -46,7 +44,6 @@ from esphome.const import ( CONF_RETAIN, CONF_SHUTDOWN_MESSAGE, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_STATE_TOPIC, CONF_SUBSCRIBE_QOS, CONF_TOPIC, @@ -221,13 +218,6 @@ def validate_config(value): return out -def validate_fingerprint(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{40}$", value) is None: - raise cv.Invalid("fingerprint must be valid SHA1 hash") - return value - - def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" # MQTT needs 1 socket for the broker connection @@ -291,9 +281,6 @@ CONFIG_SCHEMA = cv.All( ), validate_message_just_topic, ), - cv.Optional(CONF_SSL_FINGERPRINTS): cv.All( - cv.only_on_esp8266, cv.ensure_list(validate_fingerprint) - ), cv.Optional(CONF_KEEPALIVE, default="15s"): cv.positive_time_period_seconds, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -444,14 +431,6 @@ async def to_code(config): if CONF_LEVEL in log_topic: cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]])) - if CONF_SSL_FINGERPRINTS in config: - for fingerprint in config[CONF_SSL_FINGERPRINTS]: - arr = [ - cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) - ] - cg.add(var.add_ssl_fingerprint(arr)) - cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") - cg.add(var.set_keep_alive(config[CONF_KEEPALIVE])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 470d1e6a8be..0bf5b510a4c 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -21,11 +21,6 @@ class MQTTBackendESP8266 final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 24bf018a909..5fa3406193c 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -21,11 +21,6 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } -#if ASYNC_TCP_SSL_ENABLED - void set_secure(bool secure) { mqtt_client.setSecure(secure); } - void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } -#endif - void set_on_connect(std::function &&callback) final { this->mqtt_client_.onConnect(std::move(callback)); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 90b423c3867..2fb094f370c 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -746,13 +746,6 @@ void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&call this->on_disconnect_.add(std::move(callback_copy)); } -#if ASYNC_TCP_SSL_ENABLED -void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_backend_.setSecure(true); - this->mqtt_backend_.addServerFingerprint(fingerprint.data()); -} -#endif - MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // MQTTMessageTrigger diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 38bc0b4da37..7a51989a103 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -142,21 +142,6 @@ class MQTTClientComponent : public Component bool is_discovery_enabled() const; bool is_discovery_ip_enabled() const; -#if ASYNC_TCP_SSL_ENABLED - /** Add a SSL fingerprint to use for TCP SSL connections to the MQTT broker. - * - * To use this feature you first have to globally enable the `ASYNC_TCP_SSL_ENABLED` define flag. - * This function can be called multiple times and any certificate that matches any of the provided fingerprints - * will match. Calling this method will also automatically disable all non-ssl connections. - * - * @warning This is *not* secure and *not* how SSL is usually done. You'll have to add - * a separate fingerprint for every certificate you use. Additionally, the hashing - * algorithm used here due to the constraints of the MCU, SHA1, is known to be insecure. - * - * @param fingerprint The SSL fingerprint as a 20 value long std::array. - */ - void add_ssl_fingerprint(const std::array &fingerprint); -#endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } diff --git a/esphome/const.py b/esphome/const.py index 7d15964eab3..ea8d2b73bea 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -943,7 +943,6 @@ CONF_SPI = "spi" CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" -CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 042df12d67d..cbf78bd3f6e 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,6 +1,5 @@ import contextlib from datetime import datetime -import hashlib import json import logging import ssl @@ -22,14 +21,12 @@ from esphome.const import ( CONF_PASSWORD, CONF_PORT, CONF_SKIP_CERT_CN_CHECK, - CONF_SSL_FINGERPRINTS, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, ) -from esphome.core import CORE, EsphomeError +from esphome.core import EsphomeError from esphome.helpers import get_int_env, get_str_env -from esphome.log import AnsiFore, color from esphome.types import ConfigType from esphome.util import safe_print @@ -102,9 +99,7 @@ def prepare( elif username: client.username_pw_set(username, password) - if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( - CONF_CERTIFICATE_AUTHORITY - ): + if config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY): context = ssl.create_default_context( cadata=config[CONF_MQTT].get(CONF_CERTIFICATE_AUTHORITY) ) @@ -283,23 +278,3 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): client.publish(msg.topic, None, retain=True) return initialize(config, [topic], on_message, None, username, password, client_id) - - -# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py -def get_fingerprint(config): - addr = str(config[CONF_MQTT][CONF_BROKER]), int(config[CONF_MQTT][CONF_PORT]) - _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) - try: - cert_pem = ssl.get_server_certificate(addr) - except OSError as err: - _LOGGER.error("Unable to connect to server: %s", err) - return 1 - cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) - - sha1 = hashlib.sha1(cert_der).hexdigest() - - safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}") - safe_print( - f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}" - ) - return 0 From b5c36140faf58ba8966ad127f5b14670c6a3d2bb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:40:43 -0500 Subject: [PATCH 0329/2056] [sprinkler] Fix millis overflow and underflow bugs (#14299) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/sprinkler/sprinkler.cpp | 81 +++++++++++----------- esphome/components/sprinkler/sprinkler.h | 4 +- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 9e423c17609..d82d7baaf67 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler : controller_(controller), valve_(valve) {} void SprinklerValveOperator::loop() { + // Use wrapping subtraction so 32-bit millis() rollover is handled correctly: + // (now - start) yields the true elapsed time even across the 49.7-day boundary. uint32_t now = App.get_loop_component_start_time(); - if (now >= this->start_millis_) { // dummy check - switch (this->state_) { - case STARTING: - if (now > (this->start_millis_ + this->start_delay_)) { - this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state - } - break; + switch (this->state_) { + case STARTING: + if ((now - *this->start_millis_) > this->start_delay_) { + this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state + } + break; - case ACTIVE: - if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) { - this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down - } - break; + case ACTIVE: + if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) { + this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down + } + break; - case STOPPING: - if (now > (this->stop_millis_ + this->stop_delay_)) { - this->kill_(); // stop_delay_has been exceeded, ensure all valves are off - } - break; + case STOPPING: + if ((now - *this->stop_millis_) > this->stop_delay_) { + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + } + break; - default: - break; - } - } else { // perhaps millis() rolled over...or something else is horribly wrong! - this->stop(); // bail out (TODO: handle this highly unlikely situation better...) + default: + break; } } @@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (this->state_ != IDLE) { // Only kill if not already idle this->kill_(); // ensure everything is off before we let go! } - this->state_ = IDLE; // reset state - this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it - this->start_millis_ = 0; // reset because (new) valve has not been started yet - this->stop_millis_ = 0; // reset because (new) valve has not been started yet - this->valve_ = valve; // finally, set the pointer to the new valve + this->state_ = IDLE; // reset state + this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it + this->start_millis_.reset(); // reset because (new) valve has not been started yet + this->stop_millis_.reset(); // reset because (new) valve has not been started yet + this->valve_ = valve; // finally, set the pointer to the new valve } } @@ -162,7 +160,7 @@ void SprinklerValveOperator::start() { } else { this->run_(); // there is no start_delay_, so just start the pump and valve } - this->stop_millis_ = 0; + this->stop_millis_.reset(); this->start_millis_ = millis(); // save the time the start request was made } @@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() { uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; } uint32_t SprinklerValveOperator::time_remaining() { - if (this->start_millis_ == 0) { + if (!this->start_millis_.has_value()) { return this->run_duration(); // hasn't been started yet } - if (this->stop_millis_) { - if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) { + if (this->stop_millis_.has_value()) { + uint32_t elapsed = *this->stop_millis_ - *this->start_millis_; + if (elapsed >= this->start_delay_ + this->run_duration_) { return 0; // valve was active for more than its configured duration, so we are done - } else { - // we're stopped; return time remaining - return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000; } + if (elapsed <= this->start_delay_) { + return this->run_duration_ / 1000; // stopped during start delay, full run duration remains + } + return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000; } - auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_; - if (completed_millis > millis()) { - return (completed_millis - millis()) / 1000; // running now + uint32_t elapsed = millis() - *this->start_millis_; + uint32_t total_duration = this->start_delay_ + this->run_duration_; + if (elapsed < total_duration) { + return (total_duration - elapsed) / 1000; // running now } return 0; // run completed } @@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional repeat) { if (this->repeat_number_ == nullptr) { return; } - if (this->repeat_number_->state == repeat.value()) { + if (this->repeat_number_->state == repeat.value_or(0)) { return; } auto call = this->repeat_number_->make_call(); @@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional valve_number, optional void Sprinkler::queue_valve(optional valve_number, optional run_duration) { if (valve_number.has_value()) { if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) { - SprinklerQueueItem item{valve_number.value(), run_duration.value()}; + SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)}; this->queued_valves_.insert(this->queued_valves_.begin(), item); ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0), run_duration.value_or(0)); @@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { } } - if (incomplete_valve_count >= enabled_valve_count) { + if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) { incomplete_valve_count--; } if (incomplete_valve_count) { diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index a3cdef5b1a0..2598a5606a7 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -141,8 +141,8 @@ class SprinklerValveOperator { uint32_t start_delay_{0}; uint32_t stop_delay_{0}; uint32_t run_duration_{0}; - uint64_t start_millis_{0}; - uint64_t stop_millis_{0}; + optional start_millis_{}; + optional stop_millis_{}; Sprinkler *controller_{nullptr}; SprinklerValve *valve_{nullptr}; SprinklerState state_{IDLE}; From 97b712da9866f9a1d7ef46a1ea5bf1184fdb41cb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:48:05 -0500 Subject: [PATCH 0330/2056] [cc1101] Transition through IDLE in begin_tx/begin_rx for reliable state changes (#14321) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/cc1101/cc1101.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index b6973da78d1..51aa88b8f72 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -242,6 +242,9 @@ void CC1101Component::begin_tx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } + // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can + // block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration + this->enter_idle_(); if (!this->enter_tx_()) { ESP_LOGW(TAG, "Failed to enter TX state!"); } @@ -252,6 +255,8 @@ void CC1101Component::begin_rx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); } + // Transition through IDLE to ensure FS_AUTOCAL calibration occurs + this->enter_idle_(); if (!this->enter_rx_()) { ESP_LOGW(TAG, "Failed to enter RX state!"); } From 840859ab7cf323bbaa5d1d508c20dfced0d6f3e8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:17:04 -0500 Subject: [PATCH 0331/2056] [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 3 ++- esphome/components/zigbee/zigbee_zephyr.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 7e917a9d704..a327cc29886 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -8,7 +8,7 @@ from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType from .const_zephyr import ( @@ -96,6 +96,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_define("USE_ZIGBEE") if CORE.using_zephyr: diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 0b6daa9476a..a1e6ad3097e 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -179,6 +179,13 @@ async def zephyr_to_code(config: ConfigType) -> None: "USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF) ) cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT") + + # Generate attribute lists before any await that could yield (e.g., build_automation + # waiting for variables from other components). If the hub's priority decays while + # yielding, deferred entity jobs may add cluster list globals that reference these + # attribute lists before they're declared. + await _attr_to_code(config) + var = cg.new_Pvariable(config[CONF_ID]) if on_join_config := config.get(CONF_ON_JOIN): @@ -186,7 +193,6 @@ async def zephyr_to_code(config: ConfigType) -> None: await cg.register_component(var, config) - await _attr_to_code(config) CORE.add_job(_ctx_to_code, config) From 641914cdbe646747437f76b428991d028684222e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 17:27:51 -1000 Subject: [PATCH 0332/2056] [uart] Revert UART0 default pin workarounds (fixed in ESP-IDF 5.5.2) (#14363) --- .../uart/uart_component_esp_idf.cpp | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index ea7a09fee60..8699d37d7ad 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,7 +9,6 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" -#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -19,13 +18,6 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; -/// Check if a pin number matches one of the default UART0 GPIO pins. -/// These pins may have residual state from the boot console that requires -/// explicit reset before UART reconfiguration (ESP-IDF issue #17459). -static constexpr bool is_default_uart0_pin(int8_t pin_num) { - return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; -} - uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -149,34 +141,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - - // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 - // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks - // UART on default UART0 pins that may have residual state from boot console. - // Reset these pins before configuring UART to ensure they're in a clean state. - if (is_default_uart0_pin(tx)) { - gpio_reset_pin(static_cast(tx)); - } - if (is_default_uart0_pin(rx)) { - gpio_reset_pin(static_cast(rx)); - } - - // Setup pins after reset to configure GPIO direction and pull resistors. - // For UART0 default pins, setup() must always be called because gpio_reset_pin() - // above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(), - // uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for - // IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132). - // For other pins, only call setup() if pull or open-drain flags are set to avoid - // disturbing the default pin state which breaks some external components (#11823). auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; } const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; - if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { pin->setup(); } }; @@ -186,6 +156,10 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 91250fd46cc27ba1f36830b48ef3d6cb61c69970 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:37:04 +1100 Subject: [PATCH 0333/2056] [mipi_dsi] Fix Waveshare P4 7B board config (#14372) --- esphome/components/mipi_dsi/models/waveshare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index bf4f9063bb8..61829ca9c1f 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -90,8 +90,6 @@ DriverChip( (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00), (0xEF, 0xFF, 0xFF, 0x01), - (0x11, 0x00), - (0x29, 0x00), ], ) @@ -109,6 +107,7 @@ DriverChip( lane_bit_rate="900Mbps", no_transform=True, color_order="RGB", + reset_pin=33, initsequence=[ (0x80, 0x8B), (0x81, 0x78), From c9c99a22e0374d5d47a1fc131ce86384a5c038e2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:10:53 -0500 Subject: [PATCH 0334/2056] [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- esphome/components/binary_sensor/__init__.py | 36 +++++++++++-------- esphome/components/number/__init__.py | 31 +++++++++------- esphome/components/sensor/__init__.py | 37 +++++++++++--------- esphome/components/switch/__init__.py | 16 ++++++--- esphome/components/text_sensor/__init__.py | 19 ++++++---- 5 files changed, 83 insertions(+), 56 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d39..036d78da736 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -550,21 +550,8 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( - CONF_PUBLISH_INITIAL_STATE, False - ) - cg.add(var.set_trigger_on_initial_state(trigger)) - if inverted := config.get(CONF_INVERTED): - cg.add(var.set_inverted(inverted)) - if filters_config := config.get(CONF_FILTERS): - filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) - cg.add(var.add_filters(filters)) - +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_binary_sensor_automations(var, config): for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -616,6 +603,25 @@ async def setup_binary_sensor_core_(var, config): conf, ) + +async def setup_binary_sensor_core_(var, config): + await setup_entity(var, config, "binary_sensor") + + if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: + cg.add(var.set_device_class(device_class)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) + cg.add(var.add_filters(filters)) + + CORE.add_job(_build_binary_sensor_automations, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index b23da7799f1..d12ec7463b5 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -240,6 +240,23 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_number_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): @@ -254,19 +271,7 @@ async def setup_number_core_( if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: cg.add(var.traits.set_mode(config[CONF_MODE])) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_number_automations, var, config) if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 03784ba76b3..1e5f16a81dc 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -888,6 +888,26 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if (above := conf.get(CONF_ABOVE)) is not None: + template_ = await cg.templatable(above, [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if (below := conf.get(CONF_BELOW)) is not None: + template_ = await cg.templatable(below, [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + async def setup_sensor_core_(var, config): await setup_entity(var, config, "sensor") @@ -906,22 +926,7 @@ async def setup_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_VALUE_RANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await cg.register_component(trigger, conf) - if (above := conf.get(CONF_ABOVE)) is not None: - template_ = await cg.templatable(above, [(float, "x")], float) - cg.add(trigger.set_min(template_)) - if (below := conf.get(CONF_BELOW)) is not None: - template_ = await cg.templatable(below, [(float, "x")], float) - cg.add(trigger.set_max(template_)) - await automation.build_automation(trigger, [(float, "x")], conf) + CORE.add_job(_build_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 7424d7c92fe..cfc5e2b6e82 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -141,11 +141,8 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) -async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - - if (inverted := config.get(CONF_INVERTED)) is not None: - cg.add(var.set_inverted(inverted)) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_switch_automations(var, config): for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) @@ -156,6 +153,15 @@ async def setup_switch_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + +async def setup_switch_core_(var, config): + await setup_entity(var, config, "switch") + + if (inverted := config.get(CONF_INVERTED)) is not None: + cg.add(var.set_inverted(inverted)) + + CORE.add_job(_build_switch_automations, var, config) + if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 0d22400a8eb..58c293e67b3 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -197,6 +197,17 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_text_sensor_automations(var, config): + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_RAW_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + async def setup_text_sensor_core_(var, config): await setup_entity(var, config, "text_sensor") @@ -207,13 +218,7 @@ async def setup_text_sensor_core_(var, config): filters = await build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + CORE.add_job(_build_text_sensor_automations, var, config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) From 0ac61cbb9bed23de4b97611a1b78e3e303267201 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 10:23:10 -1000 Subject: [PATCH 0335/2056] [improv_serial] Add missing USE_IMPROV_SERIAL define to fix WiFi scan filtering (#14359) --- esphome/components/improv_serial/__init__.py | 1 + esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- esphome/components/wifi/wifi_component_libretiny.cpp | 2 +- esphome/components/wifi/wifi_component_pico_w.cpp | 9 +++++++-- esphome/core/defines.h | 1 + 8 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 9a2ac2f40f2..4266f5b78be 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -43,3 +43,4 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await improv_base.setup_improv_core(var, config, "improv_serial") + cg.add_define("USE_IMPROV_SERIAL") diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 61d05d76357..fbc1e946bb8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2048,7 +2048,7 @@ bool WiFiComponent::can_proceed() { #endif void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected() { +bool WiFiComponent::is_connected() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac28a1bc81d..2e285289e74 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -430,7 +430,7 @@ class WiFiComponent : public Component { void set_reboot_timeout(uint32_t reboot_timeout); - bool is_connected(); + bool is_connected() const; void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } @@ -665,7 +665,7 @@ class WiFiComponent : public Component { bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); - WiFiSTAConnectStatus wifi_sta_connect_status_(); + WiFiSTAConnectStatus wifi_sta_connect_status_() const; bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cbf7d7d80f8..7fe090c45cf 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -626,7 +626,7 @@ void WiFiComponent::wifi_pre_setup_() { this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { station_status_t status = wifi_station_get_connect_status(); if (status == STATION_GOT_IP) return WiFiSTAConnectStatus::CONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 52ee4821215..f594c13afeb 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -914,7 +914,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { if (s_sta_connected && this->got_ipv4_address_) { #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2cc05928afd..71cc4191078 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -621,7 +621,7 @@ void WiFiComponent::wifi_pre_setup_() { // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use state machine instead of querying WiFi.status() directly // State is updated in main loop from queued events, ensuring thread safety switch (s_sta_state) { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1baf21e2b2c..7a93de57281 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -115,8 +115,13 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "UNKNOWN"; } -WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); +WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { + // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino + // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif + // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's + // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. + // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. + int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: case CYW43_LINK_NOIP: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfa33e4e59f..e7d5caf7c2b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -52,6 +52,7 @@ #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_IMAGE +#define USE_IMPROV_SERIAL #define USE_IMPROV_SERIAL_NEXT_URL #define USE_INFRARED #define USE_IR_RF From d2a819eb77c966dc2d4675c49a7023ffa40d80a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:52:06 -0500 Subject: [PATCH 0336/2056] [uart] Fix flow_control_pin inverted flag ignored on ESP-IDF (#14410) Co-authored-by: Claude Opus 4.6 --- esphome/components/uart/uart_component_esp_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8699d37d7ad..7e34f835cd4 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -167,6 +167,9 @@ void IDFUARTComponent::load_settings(bool dump_config) { if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { invert |= UART_SIGNAL_RXD_INV; } + if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) { + invert |= UART_SIGNAL_RTS_INV; + } err = uart_set_line_inverse(this->uart_num_, invert); if (err != ESP_OK) { From dc56cd1d1fc39c2ca0beb5bfb8b0e19b8e4ff4a1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:57:52 +1300 Subject: [PATCH 0337/2056] Bump version to 2026.2.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 2de0460ef1e..5f351c1bbbb 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.2 +PROJECT_NUMBER = 2026.2.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index ea8d2b73bea..aaa34b2fd1c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.2" +__version__ = "2026.2.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d1de50c0e513431943607f678fc3f661aa605dd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:11:04 -1000 Subject: [PATCH 0338/2056] [core] Add ESP8266 support to wake_loop_any_context() (#14392) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- esphome/components/socket/socket.h | 5 +++-- esphome/core/application.h | 12 +++++++++++- esphome/core/component.cpp | 9 +++++---- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 430356592fa..d697bd47a50 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -36,7 +36,7 @@ void socket_delay(uint32_t ms) { esp_delay(ms, []() { return !s_socket_woke; }); } -void socket_wake() { +void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 86a4f0cba98..546d278260f 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -86,8 +86,9 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. void socket_delay(uint32_t ms); -/// Called by lwip callbacks to signal socket activity and wake delay. -void socket_wake(); +/// Signal socket/IO activity and wake the main loop from esp_delay() early. +/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). +void socket_wake(); // NOLINT(readability-redundant-declaration) #endif } // namespace esphome::socket diff --git a/esphome/core/application.h b/esphome/core/application.h index 13fd0180ab2..63d59c555e2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,11 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT - +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +namespace esphome::socket { +void socket_wake(); // NOLINT(readability-redundant-declaration) +} // namespace esphome::socket +#endif #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -530,6 +534,12 @@ class Application { #endif #endif +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context (ISR, thread, or main loop). + /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#endif + protected: friend Component; #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a71aa8b3a31..53cb50a44ce 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -323,10 +323,11 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) - // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, - // the main loop would not wake until the select timeout expires (~16ms). - // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) + // Wake the main loop from sleep. Without this, the main loop would not + // wake until the select/delay timeout expires (~16ms). + // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. + // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. Application::wake_loop_any_context(); #endif } From 3615a7b90c13f2bb100d5ea3241795aec5c7512b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:42:25 -1000 Subject: [PATCH 0339/2056] [core] Eliminate __udivdi3 in millis() on ESP32 and RP2040 (#14409) --- esphome/components/esp32/core.cpp | 4 +- esphome/components/rp2040/core.cpp | 4 +- esphome/core/helpers.h | 38 ++++++++++++ .../fixtures/micros_to_millis.yaml | 61 +++++++++++++++++++ tests/integration/test_micros_to_millis.py | 46 ++++++++++++++ 5 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/micros_to_millis.yaml create mode 100644 tests/integration/test_micros_to_millis.py diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 59b791da403..7ebbba609e8 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -22,8 +22,8 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { void HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } +uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); } +uint64_t HOT millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 6386d53292f..a15ee7e2635 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -11,8 +11,8 @@ namespace esphome { void HOT yield() { ::yield(); } -uint64_t millis_64() { return time_us_64() / 1000ULL; } -uint32_t HOT millis() { return static_cast(millis_64()); } +uint64_t millis_64() { return micros_to_millis(time_us_64()); } +uint32_t HOT millis() { return micros_to_millis(time_us_64()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t HOT micros() { return ::micros(); } void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c68cb549bb4..ae505a2d8a0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -599,6 +599,44 @@ template constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } +/// Convert a 64-bit microsecond count to milliseconds without calling +/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). +/// +/// Returns uint32_t by default (for millis()), or uint64_t when requested +/// (for millis_64()). The only difference is whether hi * Q is truncated +/// to 32 bits or widened to 64. +/// +/// On 32-bit targets, GCC does not optimize 64-bit constant division into a +/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 +/// (free divide-by-8), then use the Euclidean division identity to decompose +/// the remaining 64-bit divide-by-125 into a single 32-bit division: +/// +/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] +/// 2^32 = Q * 125 + R (34359738 * 125 + 46) +/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 +/// +/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal +/// (mulhu + shift), so no division instruction is emitted. +/// +/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). +/// +/// See: https://en.wikipedia.org/wiki/Euclidean_division +/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html +template inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) { + constexpr uint32_t d = 125U; + constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 + constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 + // 1000 = 8 * 125; divide-by-8 is a free shift + uint64_t x = us >> 3; + uint32_t lo = static_cast(x); + uint32_t hi = static_cast(x >> 32); + // Combine remainder term: hi * (2^32 % 125) + lo + uint32_t adj = hi * r + lo; + // If adj overflowed, the true value is 2^32 + adj; apply the identity again + // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q + return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); +} + /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); /// Return a random float between 0 and 1. diff --git a/tests/integration/fixtures/micros_to_millis.yaml b/tests/integration/fixtures/micros_to_millis.yaml new file mode 100644 index 00000000000..d11808c43a2 --- /dev/null +++ b/tests/integration/fixtures/micros_to_millis.yaml @@ -0,0 +1,61 @@ +esphome: + name: micros-to-millis-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::micros_to_millis; + const char *TAG = "MTM"; + int pass = 0, fail = 0; + + auto check = [&](const char *name, uint64_t us) { + uint32_t got = micros_to_millis(us); + uint32_t want = (uint32_t)(us / 1000ULL); + if (got == want) { pass++; } + else { ESP_LOGE(TAG, "%s FAILED: got=%u want=%u", name, got, want); fail++; } + }; + + // Basic values + check("zero", 0); + check("below_1ms", 999); + check("exactly_1ms", 1000); + check("above_1ms", 1001); + + // Shift boundary (1000 = 8 * 125, exercises the >>3 shift) + check("shift_7999", 7999); + check("shift_8000", 8000); + check("shift_8001", 8001); + + // 32-bit boundary + check("u32max_minus1", 0xFFFFFFFEULL); + check("u32max", 0xFFFFFFFFULL); + check("u32max_plus1", 0x100000000ULL); + + // Realistic uptimes + check("30_days", 2592000000000ULL); + check("1_year", 31536000000000ULL); + + // Carry path: construct x = us>>3 with specific hi/lo that trigger adj overflow + { uint64_t x = (603ULL << 32) | 0xFFFFFFFFU; check("carry_603", x << 3); } + { uint64_t x = (5000ULL << 32) | 0xFFFFFFFFU; check("carry_5000", x << 3); } + + // Carry boundary: exact transition where adj overflows (hi=1000, R=46) + { + uint32_t hi = 1000; + uint32_t thr = 0xFFFFFFFFU - hi * 46U; + uint64_t h = (uint64_t)hi << 32; + check("carry_before", (h | (thr - 1)) << 3); + check("carry_at", (h | thr) << 3); + check("carry_after", (h | (thr + 1)) << 3); + } + + // Mod-8 variations (exercises the >>3 truncation) + for (int i = 0; i < 8; i++) { check("mod8", 2592000000000ULL + i); } + + if (fail == 0) { ESP_LOGI(TAG, "ALL_PASSED %d tests", pass); } + else { ESP_LOGE(TAG, "%d FAILED out of %d", fail, pass + fail); } + +host: +api: +logger: diff --git a/tests/integration/test_micros_to_millis.py b/tests/integration/test_micros_to_millis.py new file mode 100644 index 00000000000..9960d6b017f --- /dev/null +++ b/tests/integration/test_micros_to_millis.py @@ -0,0 +1,46 @@ +"""Integration test for micros_to_millis Euclidean decomposition.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_micros_to_millis( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that micros_to_millis matches reference uint64 division.""" + + all_passed = asyncio.Event() + failures: list[str] = [] + + def on_log_line(line: str) -> None: + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + if "ALL_PASSED" in clean_line: + all_passed.set() + elif "FAILED" in clean_line and "[MTM" in clean_line: + failures.append(clean_line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "micros-to-millis-test" + + try: + await asyncio.wait_for(all_passed.wait(), timeout=2.0) + except TimeoutError: + if failures: + pytest.fail(f"micros_to_millis failures: {failures}") + pytest.fail("micros_to_millis test timed out") + + assert not failures, f"micros_to_millis failures: {failures}" From 5510b45f3bfa45e3204de607e0a3815b42623606 Mon Sep 17 00:00:00 2001 From: Lino Schmidt <72667500+LinoSchmidt@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:43:06 +0100 Subject: [PATCH 0340/2056] [const] Move CONF_WATCHDOG (#14415) --- esphome/components/as5600/__init__.py | 2 +- esphome/components/as5600/sensor/__init__.py | 1 - esphome/const.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index acb1c4d9dbb..b141329e945 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_ID, CONF_POWER_MODE, CONF_RANGE, + CONF_WATCHDOG, ) CODEOWNERS = ["@ammmze"] @@ -57,7 +58,6 @@ FAST_FILTER = { CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_START_POSITION = "start_position" diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index 1491852e074..e84733a4849 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_PWM_FREQUENCY = "pwm_frequency" diff --git a/esphome/const.py b/esphome/const.py index 7262a106d8e..bbd85ca66b4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1094,6 +1094,7 @@ CONF_WAND_ID = "wand_id" CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WARMUP_TIME = "warmup_time" +CONF_WATCHDOG = "watchdog" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" CONF_WATER_HEATER = "water_heater" From 727fa073777cb758dd580fd9ea31a664fc58133f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:44:53 -1000 Subject: [PATCH 0341/2056] Bump github/codeql-action from 4.32.4 to 4.32.5 (#14416) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5d7c32eaa97..4bd018b5c95 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 + uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 with: category: "/language:${{matrix.language}}" From 2e623fd6c3f83870ddeac768b855cbf3ba51e517 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 11:48:50 -1000 Subject: [PATCH 0342/2056] [tests] Fix flaky log assertion race in oversized payload tests (#14414) --- tests/integration/test_oversized_payloads.py | 69 ++++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/tests/integration/test_oversized_payloads.py b/tests/integration/test_oversized_payloads.py index 8bf890261a1..be488347aab 100644 --- a/tests/integration/test_oversized_payloads.py +++ b/tests/integration/test_oversized_payloads.py @@ -17,10 +17,10 @@ async def test_oversized_payload_plaintext( ) -> None: """Test that oversized payloads (>32768 bytes) from client cause disconnection without crashing.""" process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -30,7 +30,7 @@ async def test_oversized_payload_plaintext( and "Bad packet: message size" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect() as (client, disconnect_event): @@ -54,10 +54,13 @@ async def test_oversized_payload_plaintext( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message size exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect() as (client2, _): @@ -77,10 +80,10 @@ async def test_oversized_protobuf_message_id_plaintext( This tests the message type limit - message IDs must fit in a uint16_t (0-65535). """ process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -90,7 +93,7 @@ async def test_oversized_protobuf_message_id_plaintext( and "Bad packet: message type" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect() as (client, disconnect_event): @@ -114,10 +117,13 @@ async def test_oversized_protobuf_message_id_plaintext( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message type exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message type exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect() as (client2, _): @@ -135,10 +141,10 @@ async def test_oversized_payload_noise( """Test that oversized payloads from client cause disconnection without crashing with noise encryption.""" noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" process_exited = False - helper_log_found = False + helper_log_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, helper_log_found + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -149,7 +155,7 @@ async def test_oversized_payload_noise( and "Bad packet: message size" in line and "exceeds maximum" in line ): - helper_log_found = True + helper_log_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -177,10 +183,13 @@ async def test_oversized_payload_noise( # After disconnection, verify process didn't crash assert not process_exited, "ESPHome process should not crash" - # Verify we saw the expected HELPER_LOG message - assert helper_log_found, ( - "Expected to see HELPER_LOG about message size exceeding maximum" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(helper_log_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see HELPER_LOG about message size exceeding maximum" + ) # Try to reconnect to verify the process is still running async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -274,10 +283,10 @@ async def test_noise_corrupt_encrypted_frame( """ noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU=" process_exited = False - cipherstate_failed = False + cipherstate_event = asyncio.Event() def check_logs(line: str) -> None: - nonlocal process_exited, cipherstate_failed + nonlocal process_exited # Check for signs that the process exited/crashed if "Segmentation fault" in line or "core dumped" in line: process_exited = True @@ -290,7 +299,7 @@ async def test_noise_corrupt_encrypted_frame( "[W][api.connection" in line and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line ): - cipherstate_failed = True + cipherstate_event.set() async with run_compiled(yaml_config, line_callback=check_logs): async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( @@ -326,10 +335,14 @@ async def test_noise_corrupt_encrypted_frame( assert not process_exited, ( "ESPHome process should not crash on corrupt encrypted frames" ) - # Verify we saw the expected log message about decryption failure - assert cipherstate_failed, ( - "Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED" - ) + # Wait for the expected log message (may arrive after disconnect event) + try: + await asyncio.wait_for(cipherstate_event.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + "Expected to see log about noise_cipherstate_decrypt failure" + " or CIPHERSTATE_DECRYPT_FAILED" + ) # Verify we can still reconnect after handling the corrupt frame async with api_client_connected_with_disconnect(noise_psk=noise_key) as ( From 7a87348855aa7d49dc77926868717e0343bdb47a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:49:14 -0500 Subject: [PATCH 0343/2056] [ci] Skip PR title check for dependabot PRs (#14418) Co-authored-by: Claude Opus 4.6 --- .github/workflows/pr-title-check.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index f23c2c870ee..198b9a6b25c 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -26,14 +26,19 @@ jobs: } = require('./.github/scripts/detect-tags.js'); const title = context.payload.pull_request.title; + const author = context.payload.pull_request.user.login; + + // Skip bot PRs (e.g. dependabot) - they have their own title format + if (author === 'dependabot[bot]') { + return; + } // Block titles starting with "word:" or "word(scope):" patterns const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; if (commitStylePattern.test(title)) { core.setFailed( `PR title should not start with a "prefix:" style format.\n` + - `Please use the format: [component] Brief description\n` + - `Example: [pn532] Add health checking and auto-reset` + `Please use the format: [component] Brief description\n` ); return; } From 97d713ee6477c003a8241b0452c1bcf2d6557289 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 2 Mar 2026 19:16:38 -0600 Subject: [PATCH 0344/2056] [media_source] Add new Media Source platform component (#14417) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/media_source/__init__.py | 40 +++++ .../components/media_source/media_source.h | 159 ++++++++++++++++++ esphome/core/defines.h | 1 + 4 files changed, 201 insertions(+) create mode 100644 esphome/components/media_source/__init__.py create mode 100644 esphome/components/media_source/media_source.h diff --git a/CODEOWNERS b/CODEOWNERS index 4c97b7f99d6..21bee125c60 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -316,6 +316,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/media_source/* @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micronova/* @edenhaus @jorre05 esphome/components/microphone/* @jesserockz @kahrendt diff --git a/esphome/components/media_source/__init__.py b/esphome/components/media_source/__init__.py new file mode 100644 index 00000000000..43256db4afd --- /dev/null +++ b/esphome/components/media_source/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE +from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@kahrendt"] + +AUTO_LOAD = ["audio"] + +IS_PLATFORM_COMPONENT = True + +media_source_ns = cg.esphome_ns.namespace("media_source") + +MediaSource = media_source_ns.class_("MediaSource") + + +async def register_media_source(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + CORE.register_platform_component("media_source", var) + return var + + +_MEDIA_SOURCE_SCHEMA = cv.Schema({}) + + +def media_source_schema( + class_: MockObjClass, +) -> cv.Schema: + schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} + + return _MEDIA_SOURCE_SCHEMA.extend(schema) + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config): + cg.add_global(media_source_ns.using) + cg.add_define("USE_MEDIA_SOURCE") diff --git a/esphome/components/media_source/media_source.h b/esphome/components/media_source/media_source.h new file mode 100644 index 00000000000..688c27134f2 --- /dev/null +++ b/esphome/components/media_source/media_source.h @@ -0,0 +1,159 @@ +#pragma once + +#include "esphome/components/audio/audio.h" +#include "esphome/core/helpers.h" + +#include +#include + +namespace esphome::media_source { + +enum class MediaSourceState : uint8_t { + IDLE, // Not playing, ready to accept play_uri + PLAYING, // Currently playing media + PAUSED, // Playback paused, can be resumed + ERROR, // Error occurred during playback; sources are responsible for logging their own error details +}; + +/// @brief Commands that are sent from the orchestrator to a media source +enum class MediaSourceCommand : uint8_t { + // All sources should support these basic commands. + PLAY, + PAUSE, + STOP, + + // Only sources with internal playlists will handle these; simple sources should ignore them. + NEXT, + PREVIOUS, + CLEAR_PLAYLIST, + REPEAT_ALL, + REPEAT_ONE, + REPEAT_OFF, + SHUFFLE, + UNSHUFFLE, +}; + +/// @brief Callbacks from a MediaSource to its orchestrator +class MediaSourceListener { + public: + virtual ~MediaSourceListener() = default; + + // Callbacks that all sources use to send data and state changes to the orchestrator. + /// @brief Send audio data to the listener + virtual size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) = 0; + /// @brief Notify listener of state changes + virtual void report_state(MediaSourceState state) = 0; + + // Callbacks from smart sources requesting the orchestrator to change volume, mute, or start a new URI. + // Simple sources never invoke these. + /// @brief Request the orchestrator to change volume + virtual void request_volume(float volume) {} + /// @brief Request the orchestrator to change mute state + virtual void request_mute(bool is_muted) {} + /// @brief Request the orchestrator to play a new URI + virtual void request_play_uri(const std::string &uri) {} +}; + +/// @brief Abstract base class for media sources +/// MediaSource provides audio data to an orchestrator via the MediaSourceListener interface. It also receives commands +/// from the orchestrator to control playback. +class MediaSource { + public: + virtual ~MediaSource() = default; + + // === Playback Control === + + /// @brief Start playing the given URI + /// Sources should validate the URI and state, returning false if the source is busy. + /// The orchestrator is responsible for stopping active sources before starting a new one. + /// @param uri URI to play; e.g., "http://stream_url" + /// @return true if playback started successfully, false otherwise + virtual bool play_uri(const std::string &uri) = 0; + + /// @brief Handle playback commands (pause, stop, next, etc.) + /// @param command Command to execute + virtual void handle_command(MediaSourceCommand command) = 0; + + /// @brief Whether this source manages its own playlist internally + /// Smart sources that handle next/previous/repeat/shuffle should override this to return true. + virtual bool has_internal_playlist() const { return false; } + + // === State Access === + + /// @brief Get current playback state (must only be called from the main loop) + /// @return Current state of this source + MediaSourceState get_state() const { return this->state_; } + + // === URI Matching === + + /// @brief Check if this source can handle the given URI + /// Each source must override this to match its supported URI scheme(s). + /// @param uri URI to check + /// @return true if this source can handle the URI + virtual bool can_handle(const std::string &uri) const = 0; + + // === Listener: Source -> Orchestrator === + + /// @brief Set the listener that receives callbacks from this source + /// @param listener Pointer to the MediaSourceListener implementation + void set_listener(MediaSourceListener *listener) { this->listener_ = listener; } + + /// @brief Check if a listener has been registered + bool has_listener() const { return this->listener_ != nullptr; } + + /// @brief Write audio data to the listener + /// @param data Pointer to audio data buffer (not modified by this method) + /// @param length Number of bytes to write + /// @param timeout_ms Milliseconds to wait if the listener can't accept data immediately + /// @param stream_info Audio stream format information + /// @return Number of bytes written, or 0 if no listener is set + size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + if (this->listener_ != nullptr) { + return this->listener_->write_audio(data, length, timeout_ms, stream_info); + } + return 0; + } + + // === Callbacks: Orchestrator -> Source === + + /// @brief Notify the source that volume changed + /// Simple sources ignore this. Override for smart sources that track volume state. + /// @param volume New volume level (0.0 to 1.0) + virtual void notify_volume_changed(float volume) {} + + /// @brief Notify the source that mute state changed + /// Simple sources ignore this. Override for smart sources that track mute state. + /// @param is_muted New mute state + virtual void notify_mute_changed(bool is_muted) {} + + /// @brief Notify the source about audio that has been played + /// Called when the speaker reports that audio frames have been written to the DAC. + /// Sources can override this to track playback progress for synchronization. + /// @param frames Number of audio frames that were played + /// @param timestamp System time in microseconds when the frames finished writing to the DAC + virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {} + + protected: + /// @brief Update state and notify listener (must only be called from the main loop) + /// This is the only way to change state_, ensuring listener notifications always fire. + /// Sources running FreeRTOS tasks should signal via event groups and call this from loop(). + /// @param state New state to set + void set_state_(MediaSourceState state) { + if (this->state_ != state) { + this->state_ = state; + if (this->listener_ != nullptr) { + this->listener_->report_state(state); + } + } + } + + private: + // Private to enforce the invariant that listener notifications always fire on state changes. + // All state transitions must go through set_state_() which couples the update with notification. + MediaSourceState state_{MediaSourceState::IDLE}; + MediaSourceListener *listener_{nullptr}; +}; + +} // namespace esphome::media_source diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8c78afa7d49..7fbc5a0b535 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -106,6 +106,7 @@ #define MDNS_DYNAMIC_TXT_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER +#define USE_MEDIA_SOURCE #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_OUTPUT From c77241940b701e8a8c5709374340c80b75e7e7c6 Mon Sep 17 00:00:00 2001 From: melak Date: Tue, 3 Mar 2026 02:24:00 +0100 Subject: [PATCH 0345/2056] [lps22] Add support for the LPS22DF variant (#14397) --- esphome/components/lps22/lps22.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp index 7fc5774b08c..592b7faaf0f 100644 --- a/esphome/components/lps22/lps22.cpp +++ b/esphome/components/lps22/lps22.cpp @@ -8,6 +8,7 @@ static constexpr const char *const TAG = "lps22"; static constexpr uint8_t WHO_AM_I = 0x0F; static constexpr uint8_t LPS22HB_ID = 0xB1; static constexpr uint8_t LPS22HH_ID = 0xB3; +static constexpr uint8_t LPS22DF_ID = 0xB4; static constexpr uint8_t CTRL_REG2 = 0x11; static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1; static constexpr uint8_t STATUS = 0x27; @@ -24,8 +25,8 @@ static constexpr float TEMPERATURE_SCALE = 0.01f; void LPS22Component::setup() { uint8_t value = 0x00; this->read_register(WHO_AM_I, &value, 1); - if (value != LPS22HB_ID && value != LPS22HH_ID) { - ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value); + if (value != LPS22HB_ID && value != LPS22HH_ID && value != LPS22DF_ID) { + ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB/HH/DF ID", value); this->mark_failed(); } } From ae49b673218a9c5857c6cc8670a8fa4185d6c8ee Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 2 Mar 2026 18:47:40 -0700 Subject: [PATCH 0346/2056] [ld2450] Clear all related sensors when a target is not being tracked (#13602) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/ld2450/ld2450.cpp | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 583918e5f53..eb17cc7de70 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -474,15 +474,12 @@ void LD2450Component::handle_periodic_data_() { is_moving = false; // tx is used for further calculations, so always needs to be populated tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); // Y start = TARGET_Y + index * 8; ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); // RESOLUTION start = TARGET_RESOLUTION + index * 8; res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; - SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); #endif // SPEED start = TARGET_SPEED + index * 8; @@ -491,9 +488,6 @@ void LD2450Component::handle_periodic_data_() { is_moving = true; moving_target_count++; } -#ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); -#endif // DISTANCE // Optimized: use already decoded tx and ty values, replace pow() with multiplication int32_t x_squared = (int32_t) tx * tx; @@ -503,10 +497,23 @@ void LD2450Component::handle_periodic_data_() { target_count++; } #ifdef USE_SENSOR - SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); - // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed - angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); - SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); + if (td == 0) { + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_x_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_y_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_resolution_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_speed_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_distance_sensors_[index]); + SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_angle_sensors_[index]); + } else { + SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); + SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); + SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); + SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); + SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); + // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed + angle = atan2f(static_cast(-tx), static_cast(ty)) * (180.0f / std::numbers::pi_v); + SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); + } #endif #ifdef USE_TEXT_SENSOR // DIRECTION From db15b94cd7b0a2f323572ca66c172246c28752a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 17:17:20 -1000 Subject: [PATCH 0347/2056] [core] Inline HighFrequencyLoopRequester::is_high_frequency() (#14423) --- esphome/core/helpers.cpp | 1 - esphome/core/helpers.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6d801e7ebc4..c75799fe57f 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -794,7 +794,6 @@ void HighFrequencyLoopRequester::stop() { num_requests--; this->started_ = false; } -bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; } std::string get_mac_address() { uint8_t mac[6]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ae505a2d8a0..187b383f658 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1732,7 +1732,7 @@ class HighFrequencyLoopRequester { void stop(); /// Check whether the loop is running continuously. - static bool is_high_frequency(); + static bool is_high_frequency() { return num_requests > 0; } protected: bool started_{false}; From 60d66ca2dcdc59a351c51a070c5fd326d84d2fda Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:28:01 +0100 Subject: [PATCH 0348/2056] [openthread] Add tx power option (#14200) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 30 ++++++++++++++++++- esphome/components/openthread/openthread.cpp | 3 ++ esphome/components/openthread/openthread.h | 2 ++ .../components/openthread/openthread_esp.cpp | 6 ++++ .../openthread/test.esp32-c6-idf.yaml | 1 + 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5861c3db3f6..5c64cf31dce 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,13 +4,20 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + get_esp32_variant, include_builtin_idf_component, only_on_variant, require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS +from esphome.const import ( + CONF_CHANNEL, + CONF_ENABLE_IPV6, + CONF_ID, + CONF_OUTPUT_POWER, + CONF_USE_ADDRESS, +) from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -45,6 +52,20 @@ CONF_DEVICE_TYPES = [ ] +def _validate_txpower(value): + if CORE.is_esp32: + variant = get_esp32_variant() + + # HW limits: Datasheet section "802.15.4 RF Transmitter (TX) Characteristics" + # Further regulatory/soft limit may apply, e.g. by region + if variant in (VARIANT_ESP32C6, VARIANT_ESP32C5): + return cv.int_range(min=-15, max=20)(value) + if variant == VARIANT_ESP32H2: + return cv.int_range(min=-24, max=20)(value) + + return value # Unsupported, fail later with clear error + + def set_sdkconfig_options(config): # and expose options for using SPI/UART RCPs add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True) @@ -155,6 +176,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TLV): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OUTPUT_POWER): cv.All( + cv.decibel, + _validate_txpower, + ), } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -197,4 +222,7 @@ async def to_code(config): cg.add(srp.set_mdns(mdns_component)) await cg.register_component(srp, config) + if (output_power := config.get(CONF_OUTPUT_POWER)) is not None: + cg.add(ot.set_output_power(output_power)) + set_sdkconfig_options(config) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index d22a14aeae6..92897a7e96e 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -43,6 +43,9 @@ void OpenThreadComponent::dump_config() { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); } #endif + if (this->output_power_.has_value()) { + ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_); + } } bool OpenThreadComponent::is_connected() { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 9e429f289b8..728847afa54 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -38,6 +38,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif + void set_output_power(int8_t output_power) { this->output_power_ = output_power; } protected: std::optional get_omr_address_(InstanceLock &lock); @@ -45,6 +46,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; #endif + std::optional output_power_{}; bool teardown_started_{false}; bool teardown_complete_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9dd68a1ccce..2af78b729f3 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -135,6 +135,12 @@ void OpenThreadComponent::ot_main() { TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif + if (this->output_power_.has_value()) { + if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err)); + } + } + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 9df63b2f29b..77abc433c14 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -13,3 +13,4 @@ openthread: force_dataset: true use_address: open-thread-test.local poll_period: 20sec + output_power: 1dBm From c4fa476c3c3455a01712a71e2a2d7a8e18d1882d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:45:28 +1300 Subject: [PATCH 0349/2056] Bump version to 2026.2.4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 5f351c1bbbb..61dd690f975 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.3 +PROJECT_NUMBER = 2026.2.4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index aaa34b2fd1c..173e2b7be6a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.2.3" +__version__ = "2026.2.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 1b5bf2c84875977030d073228d31153fe7af64e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Mar 2026 16:39:01 -1000 Subject: [PATCH 0350/2056] [wifi] Revert cyw43_wifi_link_status change for RP2040 The switch from cyw43_tcpip_link_status to cyw43_wifi_link_status was intended for 2026.3.0 alongside the arduino-pico 5.5.0 framework update but was accidentally included in 2026.2.3. With the old framework (3.9.4), cyw43_wifi_link_status never returns CYW43_LINK_UP, so the CONNECTED state is unreachable. The device connects to WiFi but the status stays at CONNECTING until timeout, causing a connect/disconnect loop. Fixes https://github.com/esphome/esphome/issues/14422 --- esphome/components/wifi/wifi_component_pico_w.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 7a93de57281..b09cff76ece 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -116,12 +116,7 @@ const char *get_disconnect_reason_str(uint8_t reason) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { - // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino - // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif - // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's - // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. - // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. - int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); + int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: case CYW43_LINK_NOIP: From 903c67c99499ebdfb60c68816a3cd61114f44897 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:16:54 +1300 Subject: [PATCH 0351/2056] Revert "[wifi] Revert cyw43_wifi_link_status change for RP2040" This reverts commit 1b5bf2c84875977030d073228d31153fe7af64e5. --- esphome/components/wifi/wifi_component_pico_w.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index ad07e1ff255..270425d8c21 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -121,7 +121,12 @@ const char *get_disconnect_reason_str(uint8_t reason) { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { - int status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + // Use cyw43_wifi_link_status instead of cyw43_tcpip_link_status because the Arduino + // framework's __wrap_cyw43_cb_tcpip_init is a no-op — the SDK's internal netif + // (cyw43_state.netif[]) is never initialized. cyw43_tcpip_link_status checks that netif's + // flags and would only fall through to cyw43_wifi_link_status when the flags aren't set. + // Using cyw43_wifi_link_status directly gives us the actual WiFi radio join state. + int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class From cfde0613bbed38f9ce3d86affcde5fd0b300e972 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:53:18 +1100 Subject: [PATCH 0352/2056] [const][uart][usb_uart][weikai][core] Move constants to components/const (#14430) --- esphome/components/const/__init__.py | 3 +++ esphome/components/uart/__init__.py | 4 +--- esphome/components/usb_uart/__init__.py | 4 +--- esphome/components/weikai/__init__.py | 4 +--- esphome/const.py | 3 --- .../fixtures/external_components/uart_mock/__init__.py | 4 +--- 6 files changed, 7 insertions(+), 15 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 3201db5dfda..059bf3f26a9 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -8,14 +8,17 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" +CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" +CONF_PARITY = "parity" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" +CONF_STOP_BITS = "stop_bits" CONF_USE_PSRAM = "use_psram" ICON_CURRENT_DC = "mdi:current-dc" diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 69db4b44aae..3bc4263b31f 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -4,6 +4,7 @@ import re from esphome import automation, pins import esphome.codegen as cg +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -11,7 +12,6 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BYTES, CONF_DATA, - CONF_DATA_BITS, CONF_DEBUG, CONF_DELIMITER, CONF_DIRECTION, @@ -21,12 +21,10 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_NUMBER, - CONF_PARITY, CONF_PORT, CONF_RX_BUFFER_SIZE, CONF_RX_PIN, CONF_SEQUENCE, - CONF_STOP_BITS, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TX_PIN, diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index cc69c0cb5a7..f0ee53d0281 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import socket +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv @@ -7,12 +8,9 @@ from esphome.const import ( CONF_BAUD_RATE, CONF_BUFFER_SIZE, CONF_CHANNELS, - CONF_DATA_BITS, CONF_DEBUG, CONF_DUMMY_RECEIVER, CONF_ID, - CONF_PARITY, - CONF_STOP_BITS, ) from esphome.cpp_types import Component diff --git a/esphome/components/weikai/__init__.py b/esphome/components/weikai/__init__.py index 66cd4ce12aa..bc80f167efb 100644 --- a/esphome/components/weikai/__init__.py +++ b/esphome/components/weikai/__init__.py @@ -1,18 +1,16 @@ import esphome.codegen as cg from esphome.components import uart +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_CHANNEL, - CONF_DATA_BITS, CONF_ID, CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OUTPUT, - CONF_PARITY, - CONF_STOP_BITS, ) CODEOWNERS = ["@DrCoolZic"] diff --git a/esphome/const.py b/esphome/const.py index bbd85ca66b4..d5625f6a549 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -280,7 +280,6 @@ CONF_CUSTOM_PRESETS = "custom_presets" CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" -CONF_DATA_BITS = "data_bits" CONF_DATA_PIN = "data_pin" CONF_DATA_PINS = "data_pins" CONF_DATA_RATE = "data_rate" @@ -760,7 +759,6 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PARAMETERS = "parameters" -CONF_PARITY = "parity" CONF_PASSWORD = "password" CONF_PATH = "path" CONF_PATTERN = "pattern" @@ -963,7 +961,6 @@ CONF_STEP_PIN = "step_pin" CONF_STILL_THRESHOLD = "still_threshold" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" -CONF_STOP_BITS = "stop_bits" CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSCRIBE_QOS = "subscribe_qos" diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index dea8c385513..8deab4c21ec 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart +from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import ( CONF_RX_FULL_THRESHOLD, CONF_RX_TIMEOUT, @@ -12,14 +13,11 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, CONF_DATA, - CONF_DATA_BITS, CONF_DEBUG, CONF_DELAY, CONF_ID, CONF_INTERVAL, - CONF_PARITY, CONF_RX_BUFFER_SIZE, - CONF_STOP_BITS, CONF_TRIGGER_ID, ) from esphome.core import ID From b6f0bb9b6bcf12f7021a4904df24954712ab0f78 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Tue, 3 Mar 2026 07:59:01 -0800 Subject: [PATCH 0353/2056] [speaker] Add off on capability to media player (#9295) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Kevin Ahrendt --- .../speaker/media_player/__init__.py | 5 ++ .../media_player/speaker_media_player.cpp | 61 +++++++++++++++++++ .../media_player/speaker_media_player.h | 3 + esphome/core/defines.h | 1 + .../speaker/common-media_player_off_on.yaml | 18 ++++++ .../media_player_off_on.esp32-idf.yaml | 9 +++ 6 files changed, 97 insertions(+) create mode 100644 tests/components/speaker/common-media_player_off_on.yaml create mode 100644 tests/components/speaker/media_player_off_on.esp32-idf.yaml diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b302bd9b23c..42ca762858f 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, CONF_PATH, CONF_RAW_DATA_ID, CONF_SAMPLE_RATE, @@ -401,6 +403,9 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): + if CONF_ON_TURN_OFF in config or CONF_ON_TURN_ON in config: + cg.add_define("USE_SPEAKER_MEDIA_PLAYER_ON_OFF", True) + var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index fdf6bf66cd5..3f5cb2fda62 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -51,7 +51,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; static const char *const TAG = "speaker_media_player"; void SpeakerMediaPlayer::setup() { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + state = media_player::MEDIA_PLAYER_STATE_OFF; +#else state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); @@ -128,6 +132,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value(); if (media_command.url.has_value() || media_command.file.has_value()) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif PlaylistItem playlist_item; if (media_command.url.has_value()) { playlist_item.url = *media_command.url.value(); @@ -184,6 +194,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (media_command.command.has_value()) { switch (media_command.command.value()) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) { this->media_pipeline_->set_pause_state(false); } @@ -195,10 +211,26 @@ void SpeakerMediaPlayer::watch_media_commands_() { } this->is_paused_ = true; break; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + case media_player::MEDIA_PLAYER_COMMAND_TURN_ON: + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + this->publish_state(); + } + break; + case media_player::MEDIA_PLAYER_COMMAND_TURN_OFF: + this->is_turn_off_ = true; + // Intentional Fall-through +#endif case media_player::MEDIA_PLAYER_COMMAND_STOP: // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing. // This avoids an audible short segment playing after receiving the stop command in a paused state. +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) || + (this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) { +#else if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { +#endif if (this->announcement_pipeline_ != nullptr) { this->cancel_timeout("next_ann"); this->announcement_playlist_.clear(); @@ -366,7 +398,13 @@ void SpeakerMediaPlayer::loop() { } } else { if (this->is_paused_) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; +#endif } else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) { this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; } else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { @@ -399,7 +437,13 @@ void SpeakerMediaPlayer::loop() { } } } else { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif } } } @@ -409,6 +453,20 @@ void SpeakerMediaPlayer::loop() { this->publish_state(); ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED || + this->state == media_player::MEDIA_PLAYER_STATE_IDLE)) { + this->is_turn_off_ = false; + if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } + this->state = media_player::MEDIA_PLAYER_STATE_OFF; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +#endif } void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) { @@ -481,6 +539,9 @@ media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() { if (!this->single_pipeline_()) { traits.set_supports_pause(true); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + traits.set_supports_turn_off_on(true); +#endif if (this->announcement_format_.has_value()) { traits.get_supported_formats().push_back(this->announcement_format_.value()); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 6796fc9c003..3fa6f47b848 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -144,6 +144,9 @@ class SpeakerMediaPlayer : public Component, bool is_paused_{false}; bool is_muted_{false}; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + bool is_turn_off_{false}; +#endif uint8_t unpause_media_remaining_{0}; uint8_t unpause_announcement_remaining_{0}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fbc5a0b535..8d778edf2a0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -233,6 +233,7 @@ #define USE_LWIP_FAST_SELECT #define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER +#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER diff --git a/tests/components/speaker/common-media_player_off_on.yaml b/tests/components/speaker/common-media_player_off_on.yaml new file mode 100644 index 00000000000..a5bea62c84d --- /dev/null +++ b/tests/components/speaker/common-media_player_off_on.yaml @@ -0,0 +1,18 @@ +<<: !include common.yaml + +media_player: + - platform: speaker + id: speaker_media_player_id + announcement_pipeline: + speaker: speaker_id + buffer_size: 1000000 + volume_increment: 0.02 + volume_max: 0.95 + volume_min: 0.0 + task_stack_in_psram: true + on_turn_on: + then: + - logger.log: "Turn On Media Player" + on_turn_off: + then: + - logger.log: "Turn Off Media Player" diff --git a/tests/components/speaker/media_player_off_on.esp32-idf.yaml b/tests/components/speaker/media_player_off_on.esp32-idf.yaml new file mode 100644 index 00000000000..2d5eefff19b --- /dev/null +++ b/tests/components/speaker/media_player_off_on.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO23 + +<<: !include common-media_player_off_on.yaml From d53ff7892a85d623f7c116c253075d82b2fd0a95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:02 -1000 Subject: [PATCH 0354/2056] [socket] Cache lwip_sock pointers and inline ready() chain (#14408) --- .../components/socket/bsd_sockets_impl.cpp | 33 +++++++------- esphome/components/socket/bsd_sockets_impl.h | 7 +++ .../components/socket/lwip_sockets_impl.cpp | 33 +++++++------- esphome/components/socket/lwip_sockets_impl.h | 7 +++ esphome/components/socket/socket.cpp | 6 ++- esphome/components/socket/socket.h | 45 +++++++++++++++++-- esphome/core/application.cpp | 43 ++++++++++++------ esphome/core/application.h | 32 ++++++------- esphome/core/lwip_fast_select.c | 28 +++++------- esphome/core/lwip_fast_select.h | 45 ++++++++++++++++--- 10 files changed, 190 insertions(+), 89 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 92ecfc692b8..aea7c776c62 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -11,11 +11,15 @@ namespace esphome::socket { BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } + if (!monitor_loop || this->fd_ < 0) + return; +#ifdef USE_LWIP_FAST_SELECT + // Cache lwip_sock pointer and register for monitoring (hooks callback internally) + this->cached_sock_ = esphome_lwip_get_sock(this->fd_); + this->loop_monitored_ = App.register_socket(this->cached_sock_); +#else + this->loop_monitored_ = App.register_socket_fd(this->fd_); +#endif } BSDSocketImpl::~BSDSocketImpl() { @@ -26,10 +30,17 @@ BSDSocketImpl::~BSDSocketImpl() { int BSDSocketImpl::close() { if (!this->closed_) { - // Unregister from select() before closing if monitored + // Unregister before closing to avoid dangling pointer in monitored set +#ifdef USE_LWIP_FAST_SELECT + if (this->loop_monitored_) { + App.unregister_socket(this->cached_sock_); + this->cached_sock_ = nullptr; + } +#else if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } +#endif int ret = ::close(this->fd_); this->closed_ = true; return ret; @@ -48,8 +59,6 @@ int BSDSocketImpl::setblocking(bool blocking) { return 0; } -bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } - size_t BSDSocketImpl::getpeername_to(std::span buf) { struct sockaddr_storage storage; socklen_t len = sizeof(storage); @@ -86,14 +95,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -std::unique_ptr socket_listen(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, false); -} - -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, true); -} - } // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index d9ed9dc567c..9ebbe72002b 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -13,6 +13,10 @@ #include #endif +#ifdef USE_LWIP_FAST_SELECT +struct lwip_sock; +#endif + namespace esphome::socket { class BSDSocketImpl { @@ -105,6 +109,9 @@ class BSDSocketImpl { protected: int fd_{-1}; +#ifdef USE_LWIP_FAST_SELECT + struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() +#endif bool closed_{false}; bool loop_monitored_{false}; }; diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 0322820ef43..2fad429e0f7 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -11,11 +11,15 @@ namespace esphome::socket { LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { this->fd_ = fd; - // Register new socket with the application for select() if monitoring requested - if (monitor_loop && this->fd_ >= 0) { - // Only set loop_monitored_ to true if registration succeeds - this->loop_monitored_ = App.register_socket_fd(this->fd_); - } + if (!monitor_loop || this->fd_ < 0) + return; +#ifdef USE_LWIP_FAST_SELECT + // Cache lwip_sock pointer and register for monitoring (hooks callback internally) + this->cached_sock_ = esphome_lwip_get_sock(this->fd_); + this->loop_monitored_ = App.register_socket(this->cached_sock_); +#else + this->loop_monitored_ = App.register_socket_fd(this->fd_); +#endif } LwIPSocketImpl::~LwIPSocketImpl() { @@ -26,10 +30,17 @@ LwIPSocketImpl::~LwIPSocketImpl() { int LwIPSocketImpl::close() { if (!this->closed_) { - // Unregister from select() before closing if monitored + // Unregister before closing to avoid dangling pointer in monitored set +#ifdef USE_LWIP_FAST_SELECT + if (this->loop_monitored_) { + App.unregister_socket(this->cached_sock_); + this->cached_sock_ = nullptr; + } +#else if (this->loop_monitored_) { App.unregister_socket_fd(this->fd_); } +#endif int ret = lwip_close(this->fd_); this->closed_ = true; return ret; @@ -48,8 +59,6 @@ int LwIPSocketImpl::setblocking(bool blocking) { return 0; } -bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); } - size_t LwIPSocketImpl::getpeername_to(std::span buf) { struct sockaddr_storage storage; socklen_t len = sizeof(storage); @@ -86,14 +95,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -std::unique_ptr socket_listen(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, false); -} - -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { - return create_socket(domain, type, protocol, true); -} - } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index d6699aded26..c5792198635 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -9,6 +9,10 @@ #include "esphome/core/helpers.h" #include "headers.h" +#ifdef USE_LWIP_FAST_SELECT +struct lwip_sock; +#endif + namespace esphome::socket { class LwIPSocketImpl { @@ -71,6 +75,9 @@ class LwIPSocketImpl { protected: int fd_{-1}; +#ifdef USE_LWIP_FAST_SELECT + struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() +#endif bool closed_{false}; bool loop_monitored_{false}; }; diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index c04671c7ee8..bfb6ae8e130 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } @@ -89,6 +89,9 @@ std::unique_ptr socket_ip(int type, int protocol) { #endif /* USE_NETWORK_IPV6 */ } +#ifdef USE_SOCKET_IMPL_LWIP_TCP +// LWIP_TCP has separate Socket/ListenSocket types — needs out-of-line factory. +// BSD and LWIP_SOCKETS define this inline in socket.h. std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { #if USE_NETWORK_IPV6 return socket_listen_loop_monitored(AF_INET6, type, protocol); @@ -96,6 +99,7 @@ std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { return socket_listen_loop_monitored(AF_INET, type, protocol); #endif /* USE_NETWORK_IPV6 */ } +#endif socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) { #if USE_NETWORK_IPV6 diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 546d278260f..0884e4ba3e6 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -6,6 +6,10 @@ #include "esphome/core/optional.h" #include "headers.h" +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif + #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) // Include only the active implementation's header. @@ -36,12 +40,29 @@ using Socket = LWIPRawImpl; using ListenSocket = LWIPRawListenImpl; #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT +/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read. +inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { + return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); +} +#elif defined(USE_SOCKET_SELECT_SUPPORT) /// Shared ready() helper for fd-based socket implementations. /// Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored); #endif +// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd +// declared above, while the impl headers are included before those declarations. +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +inline bool Socket::ready() const { +#ifdef USE_LWIP_FAST_SELECT + return socket_ready(this->cached_sock_, this->loop_monitored_); +#else + return socket_ready_fd(this->fd_, this->loop_monitored_); +#endif +} +#endif + /// Create a socket of the given domain, type and protocol. std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. @@ -56,11 +77,29 @@ std::unique_ptr socket_ip(int type, int protocol); std::unique_ptr socket_loop_monitored(int domain, int type, int protocol); /// Create a listening socket of the given domain, type and protocol. -std::unique_ptr socket_listen(int domain, int type, int protocol); /// Create a listening socket and monitor it for data in the main loop. -std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); /// Create a listening socket in the newest available IP domain and monitor it. +#ifdef USE_SOCKET_IMPL_LWIP_TCP +// LWIP_TCP has separate Socket/ListenSocket types — needs distinct factory functions. +std::unique_ptr socket_listen(int domain, int type, int protocol); +std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol); std::unique_ptr socket_ip_loop_monitored(int type, int protocol); +#else +// BSD and LWIP_SOCKETS: Socket == ListenSocket, so listen variants just delegate. +inline std::unique_ptr socket_listen(int domain, int type, int protocol) { + return socket(domain, type, protocol); +} +inline std::unique_ptr socket_listen_loop_monitored(int domain, int type, int protocol) { + return socket_loop_monitored(domain, type, protocol); +} +inline std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { +#if USE_NETWORK_IPV6 + return socket_loop_monitored(AF_INET6, type, protocol); +#else + return socket_loop_monitored(AF_INET, type, protocol); +#endif +} +#endif /// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). /// @param addr Destination sockaddr structure diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 26cd6706295..8c2ba58c86e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -552,7 +552,32 @@ void Application::after_loop_tasks_() { this->in_loop_ = false; } -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT +bool Application::register_socket(struct lwip_sock *sock) { + // It modifies monitored_sockets_ without locking — must only be called from the main loop. + if (sock == nullptr) + return false; + esphome_lwip_hook_socket(sock); + this->monitored_sockets_.push_back(sock); + return true; +} + +void Application::unregister_socket(struct lwip_sock *sock) { + // It modifies monitored_sockets_ without locking — must only be called from the main loop. + for (size_t i = 0; i < this->monitored_sockets_.size(); i++) { + if (this->monitored_sockets_[i] != sock) + continue; + + // Swap with last element and pop - O(1) removal since order doesn't matter. + // No need to unhook the netconn callback — all LwIP sockets share the same + // static event_callback, and the socket will be closed by the caller. + if (i < this->monitored_sockets_.size() - 1) + this->monitored_sockets_[i] = this->monitored_sockets_.back(); + this->monitored_sockets_.pop_back(); + return; + } +} +#elif defined(USE_SOCKET_SELECT_SUPPORT) bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking @@ -571,15 +596,10 @@ bool Application::register_socket_fd(int fd) { #endif this->socket_fds_.push_back(fd); -#ifdef USE_LWIP_FAST_SELECT - // Hook the socket's netconn callback for instant wake on receive events - esphome_lwip_hook_socket(fd); -#else this->socket_fds_changed_ = true; if (fd > this->max_fd_) { this->max_fd_ = fd; } -#endif return true; } @@ -595,13 +615,9 @@ void Application::unregister_socket_fd(int fd) { continue; // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback on fast select platforms — all LwIP - // sockets share the same static event_callback, and the socket will be closed - // by the caller. if (i < this->socket_fds_.size() - 1) this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); -#ifndef USE_LWIP_FAST_SELECT this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { @@ -611,7 +627,6 @@ void Application::unregister_socket_fd(int fd) { this->max_fd_ = sock_fd; } } -#endif return; } } @@ -621,7 +636,7 @@ void Application::unregister_socket_fd(int fd) { void Application::yield_with_select_(uint32_t delay_ms) { // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket(). + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { yield(); @@ -632,8 +647,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { // If a socket still has unread data (rcvevent > 0) but the task notification was already // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. // This scan preserves select() semantics: return immediately when any fd is ready. - for (int fd : this->socket_fds_) { - if (esphome_lwip_socket_has_data(fd)) { + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { yield(); return; } diff --git a/esphome/core/application.h b/esphome/core/application.h index 63d59c555e2..40f8a00edd3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -500,14 +500,20 @@ class Application { Scheduler scheduler; - /// Register/unregister a socket file descriptor to be monitored for read events. -#ifdef USE_SOCKET_SELECT_SUPPORT - /// These functions update the fd_set used by select() in the main loop. + /// Register/unregister a socket to be monitored for read events. /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop. +#ifdef USE_LWIP_FAST_SELECT + /// Fast select path: hooks netconn callback and registers for monitoring. + /// @return true if registration was successful, false if sock is null + bool register_socket(struct lwip_sock *sock); + void unregister_socket(struct lwip_sock *sock); +#elif defined(USE_SOCKET_SELECT_SUPPORT) + /// Fallback select() path: monitors file descriptors. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. /// @return true if registration was successful, false if fd exceeds limits bool register_socket_fd(int fd); void unregister_socket_fd(int fd); +#endif #ifdef USE_WAKE_LOOP_THREADSAFE /// Wake the main event loop from another FreeRTOS task. @@ -532,7 +538,6 @@ class Application { static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } #endif #endif -#endif #if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Wake the main event loop from any context (ISR, thread, or main loop). @@ -542,23 +547,14 @@ class Application { protected: friend Component; -#ifdef USE_SOCKET_SELECT_SUPPORT +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif friend void ::setup(); friend void ::original_setup(); -#ifdef USE_SOCKET_SELECT_SUPPORT - /// Fast path for Socket::ready() via friendship - skips negative fd check. - /// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via - /// lwip_socket_dbg_get_socket(), which has no refcount; safe only because - /// the main loop owns socket lifetime (creates, reads, and closes sockets - /// on the same thread). -#ifdef USE_LWIP_FAST_SELECT - bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); } -#else +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } -#endif #endif /// Register a component, detecting loop() override at compile time. @@ -620,8 +616,12 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop FixedVector looping_components_{}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_LWIP_FAST_SELECT + std::vector monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read +#elif defined(USE_SOCKET_SELECT_SUPPORT) std::vector socket_fds_; // Vector of all monitored socket file descriptors +#endif +#ifdef USE_SOCKET_SELECT_SUPPORT #if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 989f66e9be9..c578a9aae91 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -140,8 +140,10 @@ _Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access"); _Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access"); -// rcvevent must fit in a single atomic read -_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access"); +// rcvevent must be exactly 2 bytes (s16_t) — the inline in lwip_fast_select.h reads it as int16_t. +// If lwIP changes this to int or similar, the offset assert would still pass but the load width would be wrong. +_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) == 2, + "rcvevent size changed — update int16_t cast in esphome_lwip_socket_has_data() in lwip_fast_select.h"); // Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM. // Misaligned access would not be atomic even if the size is <= 4 bytes. @@ -150,6 +152,10 @@ _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) == _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0, "lwip_sock.rcvevent must be naturally aligned for atomic access"); +// Verify the hardcoded offset used in the header's inline esphome_lwip_socket_has_data(). +_Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, + "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); + // Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. static TaskHandle_t s_main_loop_task = NULL; @@ -194,23 +200,11 @@ static inline struct lwip_sock *get_sock(int fd) { return sock; } -bool esphome_lwip_socket_has_data(int fd) { - struct lwip_sock *sock = get_sock(fd); - if (sock == NULL) - return false; - // volatile prevents the compiler from caching/reordering this cross-thread read. - // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value - // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on - // Xtensa/RISC-V/ARM and cannot produce torn values. - return *(volatile s16_t *) &sock->rcvevent > 0; +struct lwip_sock *esphome_lwip_get_sock(int fd) { + return get_sock(fd); } -void esphome_lwip_hook_socket(int fd) { - struct lwip_sock *sock = get_sock(fd); - if (sock == NULL) - return; - +void esphome_lwip_hook_socket(struct lwip_sock *sock) { // Save original callback once — all LwIP sockets share the same static event_callback // (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM). if (s_original_callback == NULL) { diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 6fce34fd76d..46c6b711cd2 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -4,6 +4,17 @@ // Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications. #include +#include + +// Forward declare lwip_sock for C++ callers that store cached pointers. +// The full definition is only available in the .c file (lwip/priv/sockets_priv.h +// conflicts with C++ compilation units). +struct lwip_sock; + +// Byte offset of rcvevent (s16_t) within struct lwip_sock. +// Verified at compile time in lwip_fast_select.c via _Static_assert. +// Anonymous enum for a compile-time constant that works in both C and C++. +enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 }; #ifdef __cplusplus extern "C" { @@ -13,16 +24,38 @@ extern "C" { /// Saves the current task handle for xTaskNotifyGive() wake notifications. void esphome_lwip_fast_select_init(void); -/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket). -/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that -/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime: -/// both has_data reads and socket close/unregister happen on the main loop thread. -bool esphome_lwip_socket_has_data(int fd); +/// Look up a LwIP socket struct from a file descriptor. +/// Returns NULL if fd is invalid or the socket/netconn is not initialized. +/// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data(). +struct lwip_sock *esphome_lwip_get_sock(int fd); + +/// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent. +/// This avoids lwIP core lock contention between the main loop (CPU0) and +/// streaming/networking work (CPU1). Correctness is preserved because callers +/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes +/// a harmless retry on the next loop iteration. In practice, stale reads have not +/// been observed across multi-day testing, but the design does not depend on that. +/// +/// The sock pointer must have been obtained from esphome_lwip_get_sock() and must +/// remain valid (caller owns socket lifetime — no concurrent close). +/// Hot path: inlined volatile 16-bit load — no function call overhead. +/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++. +/// The offset and size are verified at compile time in lwip_fast_select.c. +static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { + // Unlocked hint read — no lwIP core lock needed. + // volatile prevents the compiler from caching/reordering this cross-thread read. + // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a + // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value + // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on + // Xtensa/RISC-V/ARM and cannot produce torn values. + return *(volatile int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0; +} /// Hook a socket's netconn callback to notify the main loop task on receive events. /// Wraps the original event_callback with one that also calls xTaskNotifyGive(). /// Must be called from the main loop after socket creation. -void esphome_lwip_hook_socket(int fd); +/// The sock pointer must have been obtained from esphome_lwip_get_sock(). +void esphome_lwip_hook_socket(struct lwip_sock *sock); /// Wake the main loop task from another FreeRTOS task — costs <1 us. /// NOT ISR-safe — must only be called from task context. From 1f1b20f4feacd769279b673be5301c1d39f41ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:24 -1000 Subject: [PATCH 0355/2056] [core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171) --- .../alarm_control_panel/__init__.py | 2 +- esphome/components/api/api_connection.cpp | 4 +- esphome/components/binary_sensor/__init__.py | 12 +- .../components/binary_sensor/binary_sensor.h | 2 +- esphome/components/button/__init__.py | 12 +- esphome/components/button/button.h | 2 +- esphome/components/climate/__init__.py | 3 +- esphome/components/cover/__init__.py | 12 +- esphome/components/cover/cover.h | 2 +- esphome/components/datetime/__init__.py | 3 +- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 3 + esphome/components/event/__init__.py | 12 +- esphome/components/event/event.h | 2 +- esphome/components/fan/__init__.py | 3 +- esphome/components/host/core.cpp | 1 + esphome/components/infrared/__init__.py | 2 +- esphome/components/libretiny/core.cpp | 1 + esphome/components/light/__init__.py | 7 +- esphome/components/lock/__init__.py | 3 +- esphome/components/media_player/__init__.py | 2 +- esphome/components/mqtt/mqtt_number.cpp | 4 +- esphome/components/number/__init__.py | 16 +- esphome/components/number/number.cpp | 4 +- esphome/components/number/number_traits.h | 6 +- esphome/components/rp2040/core.cpp | 3 + esphome/components/select/__init__.py | 3 +- esphome/components/sensor/__init__.py | 16 +- esphome/components/sensor/sensor.h | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 +- esphome/components/switch/__init__.py | 12 +- esphome/components/switch/switch.h | 2 +- esphome/components/text/__init__.py | 3 +- esphome/components/text_sensor/__init__.py | 12 +- esphome/components/text_sensor/text_sensor.h | 2 +- esphome/components/update/__init__.py | 12 +- esphome/components/update/update_entity.h | 2 +- esphome/components/valve/__init__.py | 12 +- esphome/components/valve/valve.h | 2 +- esphome/components/water_heater/__init__.py | 3 +- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/zephyr/core.cpp | 1 + esphome/core/defines.h | 2 + esphome/core/entity_base.cpp | 64 ++--- esphome/core/entity_base.h | 100 ++++---- esphome/core/entity_helpers.py | 227 +++++++++++++++++- esphome/core/hal.h | 1 + script/clang-tidy | 1 + tests/component_tests/sensor/test_sensor.py | 2 +- .../text_sensor/test_text_sensor.py | 4 +- tests/unit_tests/core/test_entity_helpers.py | 110 +++++++-- 51 files changed, 519 insertions(+), 206 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index b1e2252ce74..b8555861527 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( ) +@setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 738dd1ef054..59476fac253 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -773,9 +773,9 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); + msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->traits.get_device_class_ref(); + msg.device_class = number->get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 036d78da736..1f641185602 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -60,7 +60,11 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -604,11 +608,9 @@ async def _build_binary_sensor_automations(var, config): ) +@setup_entity("binary_sensor") async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( CONF_PUBLISH_INITIAL_STATE, False ) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 4b655e1bd18..6ae5d04bcbf 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -30,7 +30,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceClass { +class BinarySensor : public StatefulEntityBase { public: explicit BinarySensor(){}; diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 94816a09748..12d9ebaba62 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -18,7 +18,11 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -84,15 +88,13 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) +@setup_entity("button") async def setup_button_core_(var, config): - await setup_entity(var, config, "button") - for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) - if device_class := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index be6e080917b..0f7576a419f 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -22,7 +22,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o * * A button is just a momentary switch that does not have a state, only a trigger. */ -class Button : public EntityBase, public EntityBase_DeviceClass { +class Button : public EntityBase { public: /** Press this button. This is called by the front-end. * diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 2150a30c3e4..1f449ad2a4b 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -268,9 +268,8 @@ def climate_schema( return _CLIMATE_SCHEMA.extend(schema) +@setup_entity("climate") async def setup_climate_core_(var, config): - await setup_entity(var, config, "climate") - visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 17095f41f65..c330241f4dc 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -37,7 +37,11 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObj, MockObjClass from esphome.types import ConfigType, TemplateArgsType @@ -190,11 +194,9 @@ def cover_schema( return _COVER_SCHEMA.extend(schema) +@setup_entity("cover") async def setup_cover_core_(var, config): - await setup_entity(var, config, "cover") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if CONF_ON_OPEN in config: _LOGGER.warning( diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 0af48f75de8..8cf9aa092aa 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -107,7 +107,7 @@ const LogString *cover_operation_to_str(CoverOperation op); * to control all values of the cover. Also implement get_traits() to return what operations * the cover supports. */ -class Cover : public EntityBase, public EntityBase_DeviceClass { +class Cover : public EntityBase { public: explicit Cover(); diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 602db3827ad..74c9d594f75 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: return _DATETIME_SCHEMA.extend(schema) +@setup_entity("datetime") async def setup_datetime_core_(var, config): - await setup_entity(var, config, "datetime") - if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 7ebbba609e8..46c000562e1 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -48,6 +48,7 @@ void arch_init() { void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index b665124d66f..159ec20e77b 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint16_t progmem_read_uint16(const uint16_t *addr) { return pgm_read_word(addr); // NOLINT } diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 8fac7a279c4..14cc1505ad8 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -18,7 +18,11 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@nohat"] @@ -85,17 +89,15 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) +@setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config, "event") - for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) cg.add(var.set_event_types(event_types)) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index a7451407bba..5b6a94b47c0 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -20,7 +20,7 @@ namespace event { LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } -class Event : public EntityBase, public EntityBase_DeviceClass { +class Event : public EntityBase { public: void trigger(const std::string &event_type); diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index e839df6aee6..da28c577c8e 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -222,9 +222,8 @@ def validate_preset_modes(value): return value +@setup_entity("fan") async def setup_fan_core_(var, config): - await setup_entity(var, config, "fan") - cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index cb2b2e19d7d..d5c61ec986c 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -59,6 +59,7 @@ void HOT arch_feed_wdt() { } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py index 5c759d6fd9c..6a2a72fa5d7 100644 --- a/esphome/components/infrared/__init__.py +++ b/esphome/components/infrared/__init__.py @@ -45,9 +45,9 @@ def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema: ) +@setup_entity("infrared") async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None: """Set up core infrared configuration.""" - await setup_entity(var, config, "infrared") async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None: diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 74b33a30a02..893a79440a7 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -36,6 +36,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 40382bbda73..4403281116a 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -243,9 +243,8 @@ def validate_color_temperature_channels(value): return value -async def setup_light_core_(light_var, output_var, config): - await setup_entity(light_var, config, "light") - +@setup_entity("light") +async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: @@ -312,7 +311,7 @@ async def register_light(output_var, config): cg.add(cg.App.register_light(light_var)) CORE.register_platform_component("light", light_var) await cg.register_component(light_var, config) - await setup_light_core_(light_var, output_var, config) + await setup_light_core_(light_var, config, output_var) async def new_light(config, *args): diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 9d893d3ad9d..e37092756fb 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -91,9 +91,8 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) +@setup_entity("lock") async def _setup_lock_core(var, config): - await setup_entity(var, config, "lock") - for conf in config.get(CONF_ON_LOCK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index b2afbe5e587..051e386eaf7 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -96,8 +96,8 @@ VolumeSetAction = media_player_ns.class_( ) +@setup_entity("media_player") async def setup_media_player_core_(var, config): - await setup_entity(var, config, "media_player") for conf_key, _ in _STATE_TRIGGERS: for conf in config.get(conf_key, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index fdc909fcc92..a2734f2beb0 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -48,7 +48,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + const auto unit_of_measurement = this->number_->get_unit_of_measurement_ref(); if (!unit_of_measurement.empty()) { root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; } @@ -57,7 +57,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = NumberMqttModeStrings::get_progmem_str(static_cast(mode), static_cast(NUMBER_MODE_BOX)); } - const auto device_class = this->number_->traits.get_device_class_ref(); + const auto device_class = this->number_->get_device_class_ref(); if (!device_class.empty()) { root[MQTT_DEVICE_CLASS] = device_class; } diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2238f2c0375..0570ac0b1ec 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -79,7 +79,12 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, + setup_unit_of_measurement, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -257,11 +262,10 @@ async def _build_number_automations(var, config): await automation.build_automation(trigger, [(float, "x")], conf) +@setup_entity("number") async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): - await setup_entity(var, config, "number") - cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) @@ -273,10 +277,8 @@ async def setup_number_core_( CORE.add_job(_build_number_automations, var, config) - if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: - cg.add(var.traits.set_unit_of_measurement(unit_of_measurement)) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.traits.set_device_class(device_class)) + setup_device_class(config) + setup_unit_of_measurement(config) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 1c4126496c5..c0653c3b304 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -15,8 +15,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); LOG_ENTITY_ICON(tag, prefix, *obj); - LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits); - LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits); + LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj); + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); } void Number::publish_state(float state) { diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index 5ccbb9ba489..f855813c9bf 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/entity_base.h" -#include "esphome/core/helpers.h" +#include +#include namespace esphome::number { @@ -11,7 +11,7 @@ enum NumberMode : uint8_t { NUMBER_MODE_SLIDER = 2, }; -class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { +class NumberTraits { public: // Set/get the number value boundaries. void set_min_value(float min_value) { min_value_ = min_value; } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index a15ee7e2635..63b154d80de 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c114b140a9a..b2c17f59ac1 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -92,9 +92,8 @@ def select_schema( return _SELECT_SCHEMA.extend(schema) +@setup_entity("select") async def setup_select_core_(var, config, *, options: list[str]): - await setup_entity(var, config, "select") - cg.add(var.traits.set_options(options)) for conf in config.get(CONF_ON_VALUE, []): diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 338aaae0b53..4be6ed1b841 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -106,7 +106,12 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, + setup_unit_of_measurement, +) from esphome.cpp_generator import MockObj, MockObjClass from esphome.util import Registry @@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config): await automation.build_automation(trigger, [(float, "x")], conf) +@setup_entity("sensor") async def setup_sensor_core_(var, config): - await setup_entity(var, config, "sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) + setup_unit_of_measurement(config) if (state_class := config.get(CONF_STATE_CLASS)) is not None: cg.add(var.set_state_class(state_class)) - if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None: - cg.add(var.set_unit_of_measurement(unit_of_measurement)) if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: cg.add(var.set_accuracy_decimals(accuracy_decimals)) # Only set force_update if True (default is False) diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 54e75ee2a13..197896f6f68 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -44,7 +44,7 @@ const LogString *state_class_to_string(StateClass state_class); * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. */ -class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { +class Sensor : public EntityBase { public: explicit Sensor(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index d82d7baaf67..44fb9092bc3 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 6f1be7d53d5..bbafc54bd1e 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -22,7 +22,11 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -154,9 +158,8 @@ async def _build_switch_automations(var, config): await automation.build_automation(trigger, [], conf) +@setup_entity("switch") async def setup_switch_core_(var, config): - await setup_entity(var, config, "switch") - if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) @@ -169,8 +172,7 @@ async def setup_switch_core_(var, config): if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) await zigbee.setup_switch(var, config) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 982c640cf94..c4f8525793a 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -35,7 +35,7 @@ enum SwitchRestoreMode : uint8_t { * A switch is basically just a combination of a binary sensor (for reporting switch values) * and a write_state method that writes a state to the hardware. */ -class Switch : public EntityBase, public EntityBase_DeviceClass { +class Switch : public EntityBase { public: explicit Switch(); diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 61f7119cadb..224f4580d4a 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -84,6 +84,7 @@ def text_schema( return _TEXT_SCHEMA.extend(schema) +@setup_entity("text") async def setup_text_core_( var, config, @@ -92,8 +93,6 @@ async def setup_text_core_( max_length: int | None, pattern: str | None, ): - await setup_entity(var, config, "text") - cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) if pattern is not None: diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 2edf202cd23..97f394ecf73 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -21,7 +21,11 @@ from esphome.const import ( DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -208,11 +212,9 @@ async def _build_text_sensor_automations(var, config): await automation.build_automation(trigger, [(cg.std_string, "x")], conf) +@setup_entity("text_sensor") async def setup_text_sensor_core_(var, config): - await setup_entity(var, config, "text_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if config.get(CONF_FILTERS): # must exist and not be empty cg.add_define("USE_TEXT_SENSOR_FILTER") diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 9916aa63b23..d26cfade966 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -25,7 +25,7 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text public: \ void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; } -class TextSensor : public EntityBase, public EntityBase_DeviceClass { +class TextSensor : public EntityBase { public: std::string state; diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index e146f7e6857..c36a4ab769d 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -15,7 +15,11 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@jesserockz"] @@ -87,11 +91,9 @@ def update_schema( return _UPDATE_SCHEMA.extend(schema) +@setup_entity("update") async def setup_update_core_(var, config): - await setup_entity(var, config, "update") - - if device_class_config := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class_config)) + setup_device_class(config) if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE): await automation.build_automation( diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 405346bee4f..82eaacaf76d 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -29,7 +29,7 @@ enum UpdateState : uint8_t { const LogString *update_state_to_string(UpdateState state); -class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { +class UpdateEntity : public EntityBase { public: void publish_state(); diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 73e907eb0f9..22cd01988d8 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -22,7 +22,11 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass IS_PLATFORM_COMPONENT = True @@ -129,11 +133,9 @@ def valve_schema( return _VALVE_SCHEMA.extend(schema) +@setup_entity("valve") async def _setup_valve_core(var, config): - await setup_entity(var, config, "valve") - - if device_class_config := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class_config)) + setup_device_class(config) for conf in config.get(CONF_ON_OPEN, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index cd461443727..aab819a7788 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -101,7 +101,7 @@ const LogString *valve_operation_to_str(ValveOperation op); * to control all values of the valve. Also implement get_traits() to return what operations * the valve supports. */ -class Valve : public EntityBase, public EntityBase_DeviceClass { +class Valve : public EntityBase { public: explicit Valve(); diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index db32c2d9193..58cf5a4054e 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -69,10 +69,9 @@ def water_heater_schema( return _WATER_HEATER_SCHEMA.extend(schema) +@setup_entity("water_heater") async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None: """Set up the core water heater properties in C++.""" - await setup_entity(var, config, "water_heater") - visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 47e427c0d12..6b94a103cc9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1139,7 +1139,7 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v json::JsonBuilder builder; JsonObject root = builder.root(); - const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const auto uom_ref = obj->get_unit_of_measurement_ref(); const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); // Need two buffers: one for value, one for state with UOM diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index cf3ea70245a..eee7fb3f4f8 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +const char *progmem_read_ptr(const char *const *addr) { return *addr; } uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } Mutex::Mutex() { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8d778edf2a0..07afefd91aa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -44,7 +44,9 @@ #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY +#define USE_ENTITY_DEVICE_CLASS #define USE_ENTITY_ICON +#define USE_ENTITY_UNIT_OF_MEASUREMENT #define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index f6a7ec1dfdd..eafc04f92a4 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,24 +45,42 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { } } -// Entity Icon -std::string EntityBase::get_icon() const { -#ifdef USE_ENTITY_ICON - if (this->icon_c_str_ == nullptr) { - return ""; - } - return this->icon_c_str_; +// Weak default lookup functions — overridden by generated code in main.cpp +__attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; } +__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; } +__attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; } + +// Entity device class (from index) +StringRef EntityBase::get_device_class_ref() const { +#ifdef USE_ENTITY_DEVICE_CLASS + return StringRef(entity_device_class_lookup(this->device_class_idx_)); #else - return ""; + return StringRef(entity_device_class_lookup(0)); #endif } -void EntityBase::set_icon(const char *icon) { -#ifdef USE_ENTITY_ICON - this->icon_c_str_ = icon; +std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); } + +// Entity unit of measurement (from index) +StringRef EntityBase::get_unit_of_measurement_ref() const { +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + return StringRef(entity_uom_lookup(this->uom_idx_)); #else - // No-op when USE_ENTITY_ICON is not defined + return StringRef(entity_uom_lookup(0)); #endif } +std::string EntityBase::get_unit_of_measurement() const { + return std::string(this->get_unit_of_measurement_ref().c_str()); +} + +// Entity icon (from index) +StringRef EntityBase::get_icon_ref() const { +#ifdef USE_ENTITY_ICON + return StringRef(entity_icon_lookup(this->icon_idx_)); +#else + return StringRef(entity_icon_lookup(0)); +#endif +} +std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); } // Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { @@ -134,24 +152,6 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve return global_preferences->make_preference(size, key); } -std::string EntityBase_DeviceClass::get_device_class() { - if (this->device_class_ == nullptr) { - return ""; - } - return this->device_class_; -} - -void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } - -std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { - if (this->unit_of_measurement_ == nullptr) - return ""; - return this->unit_of_measurement_; -} -void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - #ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_icon_ref().empty()) { @@ -160,13 +160,13 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) } #endif -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_device_class_ref().empty()) { ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); } } -void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) { +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj) { if (!obj.get_unit_of_measurement_ref().empty()) { ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str()); } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index cbc07cc44c0..042eebb40f3 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -14,6 +14,12 @@ namespace esphome { +// Extern lookup functions for entity string tables. +// Generated code provides strong definitions; weak defaults return "". +extern const char *entity_device_class_lookup(uint8_t index); +extern const char *entity_uom_lookup(uint8_t index); +extern const char *entity_icon_lookup(uint8_t index); + // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; @@ -89,20 +95,41 @@ class EntityBase { this->flags_.entity_category = static_cast(entity_category); } + // Set entity string table indices — one call per entity from codegen. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + void set_entity_strings([[maybe_unused]] uint32_t packed) { +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (packed >> 16) & 0xFF; +#endif + } + + // Get device class as StringRef (from packed index) + StringRef get_device_class_ref() const; + /// Get the device class as std::string (deprecated, prefer get_device_class_ref()) + ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " + "ESPHome 2026.9.0", + "2026.3.0") + std::string get_device_class() const; + // Get unit of measurement as StringRef (from packed index) + StringRef get_unit_of_measurement_ref() const; + /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) + ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " + "removed in ESPHome 2026.9.0", + "2026.3.0") + std::string get_unit_of_measurement() const; + // Get/set this entity's icon ESPDEPRECATED( "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", "2025.11.0") std::string get_icon() const; - void set_icon(const char *icon); - StringRef get_icon_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); -#ifdef USE_ENTITY_ICON - return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_); -#else - return EMPTY_STRING; -#endif - } + StringRef get_icon_ref() const; #ifdef USE_DEVICES // Get/set this entity's device id @@ -173,9 +200,6 @@ class EntityBase { void calc_object_id_(); StringRef name_; -#ifdef USE_ENTITY_ICON - const char *icon_c_str_{nullptr}; -#endif uint32_t object_id_hash_{}; #ifdef USE_DEVICES Device *device_{}; @@ -190,44 +214,16 @@ class EntityBase { uint8_t entity_category : 2; // Supports up to 4 categories uint8_t reserved : 2; // Reserved for future use } flags_{}; -}; - -class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) - public: - /// Get the device class, using the manual override if set. - ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " - "ESPHome 2026.5.0", - "2025.11.0") - std::string get_device_class(); - /// Manually set the device class. - void set_device_class(const char *device_class); - /// Get the device class as StringRef - StringRef get_device_class_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); - } - - protected: - const char *device_class_{nullptr}; ///< Device class override -}; - -class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) - public: - /// Get the unit of measurement, using the manual override if set. - ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " - "removed in ESPHome 2026.5.0", - "2025.11.0") - std::string get_unit_of_measurement(); - /// Manually set the unit of measurement. - void set_unit_of_measurement(const char *unit_of_measurement); - /// Get the unit of measurement as StringRef - StringRef get_unit_of_measurement_ref() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_); - } - - protected: - const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override + // String table indices — packed into the 3 padding bytes after flags_ +#ifdef USE_ENTITY_DEVICE_CLASS + uint8_t device_class_idx_{}; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + uint8_t uom_idx_{}; +#endif +#ifdef USE_ENTITY_ICON + uint8_t icon_idx_{}; +#endif }; /// Log entity icon if set (for use in dump_config) @@ -240,10 +236,10 @@ inline void log_entity_icon(const char *, const char *, const EntityBase &) {} #endif /// Log entity device class if set (for use in dump_config) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj); /// Log entity unit of measurement if set (for use in dump_config) #define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj) -void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj); +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj); /** * An entity that has a state. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index c1801c0bdaa..551e35df65c 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,9 +1,12 @@ from collections.abc import Callable +from dataclasses import dataclass, field +import functools import logging import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -11,15 +14,184 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) -from esphome.core import CORE, ID -from esphome.cpp_generator import MockObj, add, get_variable +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv -from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case +from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) +DOMAIN = "entity_string_pool" + +# Private config keys for storing registered string indices +_KEY_DC_IDX = "_entity_dc_idx" +_KEY_UOM_IDX = "_entity_uom_idx" +_KEY_ICON_IDX = "_entity_icon_idx" + +# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +_DC_SHIFT = 0 +_UOM_SHIFT = 8 +_ICON_SHIFT = 16 + +# Maximum unique strings per category (8-bit index, 0 = not set) +_MAX_DEVICE_CLASSES = 0xFF # 255 +_MAX_UNITS = 0xFF # 255 +_MAX_ICONS = 0xFF # 255 + + +@dataclass +class EntityStringPool: + """Pool of entity string properties for PROGMEM pointer tables. + + Strings are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (empty string). At render time, the pool + generates C++ PROGMEM pointer table + lookup function per category. + """ + + device_classes: dict[str, int] = field(default_factory=dict) + units: dict[str, int] = field(default_factory=dict) + icons: dict[str, int] = field(default_factory=dict) + tables_registered: bool = False + + +def _get_pool() -> EntityStringPool: + """Get or create the entity string pool from CORE.data.""" + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = EntityStringPool() + return CORE.data[DOMAIN] + + +def _ensure_tables_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_pool() + if pool.tables_registered: + return + pool.tables_registered = True + CORE.add_job(_generate_tables_job) + + +def _generate_category_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ code for one string category (PROGMEM pointer table + lookup). + + Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored + in flash (via PROGMEM) and read with progmem_read_ptr(). String literals + themselves remain in RAM but benefit from linker string deduplication. + Index 0 means "not set" and returns empty string. + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) + count = len(sorted_strings) + + return ( + f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" + f"const char *{lookup_fn}(uint8_t index) {{\n" + f' if (index == 0 || index > {count}) return "";\n' + f" return progmem_read_ptr(&{table_var}[index - 1]);\n" + f"}}\n" + ) + + +_CATEGORY_CONFIGS = ( + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_tables_job() -> None: + """Generate all entity string table C++ code as a FINAL-priority job. + + Runs after all component to_code() calls have registered their strings. + """ + pool = _get_pool() + parts = ["namespace esphome {"] + for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: + code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) + if code: + parts.append(code) + parts.append("} // namespace esphome") + cg.add_global(RawStatement("\n".join(parts))) + + +def _register_string( + value: str, category: dict[str, int], max_count: int, category_name: str +) -> int: + """Register a string in a category dict and return its 1-based index. + + Returns 0 if value is empty/None (meaning "not set"). + """ + if not value: + return 0 + if value in category: + return category[value] + idx = len(category) + 1 + if idx > max_count: + raise ValueError( + f"Too many unique {category_name} values (max {max_count}), got {idx}: '{value}'" + ) + category[value] = idx + _ensure_tables_registered() + return idx + + +def register_device_class(value: str) -> int: + """Register a device_class string and return its 1-based index.""" + return _register_string( + value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" + ) + + +def register_unit_of_measurement(value: str) -> int: + """Register a unit_of_measurement string and return its 1-based index.""" + return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") + + +def register_icon(value: str) -> int: + """Register an icon string and return its 1-based index.""" + return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") + + +def setup_device_class(config: ConfigType) -> None: + """Register config's device_class and store its index for finalize_entity_strings.""" + idx = register_device_class(config.get(CONF_DEVICE_CLASS, "")) + if idx: + cg.add_define("USE_ENTITY_DEVICE_CLASS") + config[_KEY_DC_IDX] = idx + + +def setup_unit_of_measurement(config: ConfigType) -> None: + """Register config's unit_of_measurement and store its index for finalize_entity_strings.""" + idx = register_unit_of_measurement(config.get(CONF_UNIT_OF_MEASUREMENT, "")) + if idx: + cg.add_define("USE_ENTITY_UNIT_OF_MEASUREMENT") + config[_KEY_UOM_IDX] = idx + + +def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: + """Emit a single set_entity_strings() call with all packed indices. + + Call this at the end of each component's setup function, after + setup_entity() and any register_device_class/register_unit_of_measurement calls. + """ + dc_idx = config.get(_KEY_DC_IDX, 0) + uom_idx = config.get(_KEY_UOM_IDX, 0) + icon_idx = config.get(_KEY_ICON_IDX, 0) + packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) + if packed != 0: + add(var.set_entity_strings(packed)) + def get_base_entity_object_id( name: str, friendly_name: str | None, device_name: str | None = None @@ -64,8 +236,48 @@ def get_base_entity_object_id( return sanitize(snake_case(base_str)) -async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: - """Set up generic properties of an Entity. +def setup_entity(var_or_platform, config=None, platform=None): + """Set up entity properties — works as both decorator and direct call. + + Decorator mode:: + + @setup_entity("sensor") + async def setup_sensor_core_(var, config): + setup_device_class(config) + setup_unit_of_measurement(config) + ... + + Direct call mode (for entities with no extra string properties):: + + await setup_entity(var, config, "camera") + """ + if isinstance(var_or_platform, str) and config is None: + # Decorator mode: @setup_entity("sensor") + platform = var_or_platform + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + async def wrapper( + var: MockObj, config: ConfigType, *args, **kwargs + ) -> None: + await _setup_entity_impl(var, config, platform) + await func(var, config, *args, **kwargs) + finalize_entity_strings(var, config) + + return wrapper + + return decorator + + # Direct call mode: await setup_entity(var, config, "camera") + async def _do() -> None: + await _setup_entity_impl(var_or_platform, config, platform) + finalize_entity_strings(var_or_platform, config) + + return _do() + + +async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> None: + """Set up generic properties of an Entity (internal implementation). This function sets up the common entity properties like name, icon, entity category, etc. @@ -92,12 +304,15 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: add(var.set_disabled_by_default(True)) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) + icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") - add(var.set_icon(config[CONF_ICON])) + icon_idx = register_icon(config[CONF_ICON]) if CONF_ENTITY_CATEGORY in config: add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + # Store icon index for finalize_entity_strings + config[_KEY_ICON_IDX] = icon_idx def inherit_property_from(property_to_inherit, parent_id_property, transform=None): diff --git a/esphome/core/hal.h b/esphome/core/hal.h index ef45be629d3..c2c9b1a325e 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -42,6 +42,7 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); +const char *progmem_read_ptr(const char *const *addr); uint16_t progmem_read_uint16(const uint16_t *addr); } // namespace esphome diff --git a/script/clang-tidy b/script/clang-tidy index 17bcafacc79..9c2899026d2 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -79,6 +79,7 @@ def clang_options(idedata): "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", + "-Dpgm_read_ptr(s)=(*(const void *const *)(s))", "-DPROGMEM=", "-DPGM_P=const char *", "-DPSTR(s)=(s)", diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 35ce1f4e11b..221e7edf2c3 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") # Then - assert 's_1->set_device_class("voltage");' in main_cpp + assert "s_1->set_entity_strings(" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1593d0b6d8e..4aaebe04d1c 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_2->set_device_class("timestamp");' in main_cpp - assert 'ts_3->set_device_class("date");' in main_cpp + assert "ts_2->set_entity_strings(" in main_cpp + assert "ts_3->set_entity_strings(" in main_cpp diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a58d4784cee..a5cfad5ab69 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -11,6 +11,7 @@ from esphome.config_validation import Invalid from esphome.const import ( CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, + CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_INTERNAL, @@ -18,6 +19,8 @@ from esphome.const import ( ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( + _register_string, + _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, setup_entity, @@ -305,7 +308,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> CONF_NAME: "Temperature", CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var1, config1, "sensor") + await _setup_entity_impl(var1, config1, "sensor") # Get object ID from first entity object_id1 = extract_object_id_from_expressions(added_expressions) @@ -319,7 +322,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> CONF_NAME: "Humidity", CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var2, config2, "sensor") + await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity object_id2 = extract_object_id_from_expressions(added_expressions) @@ -354,7 +357,7 @@ async def test_setup_entity_different_platforms( object_ids: list[str] = [] for var, platform in platforms: added_expressions.clear() - await setup_entity(var, config, platform) + await _setup_entity_impl(var, config, platform) object_id = extract_object_id_from_expressions(added_expressions) object_ids.append(object_id) @@ -416,7 +419,7 @@ async def test_setup_entity_with_devices( object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: added_expressions.clear() - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) object_ids.append(object_id) @@ -438,7 +441,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) # Should use friendly name @@ -460,7 +463,7 @@ async def test_setup_entity_special_characters( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") object_id = extract_object_id_from_expressions(added_expressions) # Special characters should be sanitized @@ -471,7 +474,7 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - added_expressions = setup_test_environment + setup_test_environment # noqa: F841 - fixture initializes CORE state var = MockObj("sensor1") @@ -481,12 +484,10 @@ async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None CONF_ICON: "mdi:thermometer", } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") - # Check icon was set - assert any( - 'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions - ) + # Check icon index was stored in config for finalize_entity_strings + assert config.get("_entity_icon_idx", 0) > 0 @pytest.mark.asyncio @@ -504,7 +505,7 @@ async def test_setup_entity_disabled_by_default( CONF_DISABLED_BY_DEFAULT: True, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # Check disabled_by_default was set assert any( @@ -790,7 +791,7 @@ async def test_setup_entity_empty_name_with_device( CONF_DEVICE_ID: device_id, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") entity_helpers.get_variable = original_get_variable @@ -826,7 +827,7 @@ async def test_setup_entity_empty_name_with_mac_suffix( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( @@ -858,7 +859,7 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( @@ -891,9 +892,84 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( CONF_DISABLED_BY_DEFAULT: False, } - await setup_entity(var, config, "sensor") + await _setup_entity_impl(var, config, "sensor") # For empty-name entities, Python passes 0 - C++ calculates hash at runtime assert any('set_name("", 0)' in expr for expr in added_expressions), ( f"Expected set_name with hash 0, got {added_expressions}" ) + + +def test_register_string_overflow() -> None: + """Test _register_string raises ValueError when max count is exceeded.""" + category: dict[str, int] = {} + for i in range(3): + _register_string(f"val_{i}", category, 3, "test") + with pytest.raises(ValueError, match="Too many unique test values"): + _register_string("overflow", category, 3, "test") + + +@pytest.mark.asyncio +async def test_setup_entity_with_entity_category( + setup_test_environment: list[str], +) -> None: + """Test setup_entity sets entity_category correctly.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "diagnostic", + } + await _setup_entity_impl(var, config, "sensor") + assert any( + 'set_entity_category("diagnostic")' in expr for expr in added_expressions + ) + + +@pytest.mark.asyncio +async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> None: + """Test setup_entity in direct call mode (legacy / backward compat).""" + added_expressions = setup_test_environment + + var = MockObj("camera1") + config = { + CONF_NAME: "My Camera", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:camera", + } + + # Direct call mode: await setup_entity(var, config, "camera") + await setup_entity(var, config, "camera") + + # Should have called set_name + object_id = extract_object_id_from_expressions(added_expressions) + assert object_id == "my_camera" + + # Icon index should have been stored and finalized + assert config.get("_entity_icon_idx", 0) > 0 + + +@pytest.mark.asyncio +async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> None: + """Test setup_entity in decorator mode.""" + added_expressions = setup_test_environment + + body_called = False + + @setup_entity("sensor") + async def my_setup(var, config): + nonlocal body_called + body_called = True + + var = MockObj("sensor1") + config = { + CONF_NAME: "Temperature", + CONF_DISABLED_BY_DEFAULT: False, + } + + await my_setup(var, config) + + assert body_called + object_id = extract_object_id_from_expressions(added_expressions) + assert object_id == "temperature" From 78602ccacb33dcf3acdad5a55be497af52cc4287 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:03:50 -1000 Subject: [PATCH 0356/2056] [ci] Add lint check to prevent powf in core and base entity platforms (#14126) --- esphome/core/helpers.cpp | 4 ++-- esphome/core/helpers.h | 4 ++-- script/ci-custom.py | 48 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c75799fe57f..00b447ebf29 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -706,7 +706,7 @@ float gamma_correct(float value, float gamma) { if (gamma <= 0.0f) return value; - return powf(value, gamma); + return powf(value, gamma); // NOLINT - deprecated, removal 2026.9.0 } float gamma_uncorrect(float value, float gamma) { if (value <= 0.0f) @@ -714,7 +714,7 @@ float gamma_uncorrect(float value, float gamma) { if (gamma <= 0.0f) return value; - return powf(value, 1 / gamma); + return powf(value, 1 / gamma); // NOLINT - deprecated, removal 2026.9.0 } void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 187b383f658..6ce5de4975c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -512,8 +512,8 @@ template class SmallBufferWithHeapFallb ///@{ /// Compute 10^exp using iterative multiplication/division. -/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. -/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT inline float pow10_int(int8_t exp) { float result = 1.0f; if (exp >= 0) { diff --git a/script/ci-custom.py b/script/ci-custom.py index f428eb0821b..b60d7d77401 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -841,6 +841,54 @@ def lint_no_scanf(fname, match): ) +# Base entity platforms - these are linked into most builds and should not +# pull in powf/__ieee754_powf (~2.3KB flash). +BASE_ENTITY_PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "button", + "climate", + "cover", + "datetime", + "event", + "fan", + "light", + "lock", + "media_player", + "number", + "select", + "sensor", + "switch", + "text", + "text_sensor", + "update", + "valve", + "water_heater", +] + +# Directories protected from powf: core + all base entity platforms +POWF_PROTECTED_DIRS = ["esphome/core"] + [ + f"esphome/components/{p}" for p in BASE_ENTITY_PLATFORMS +] + + +@lint_re_check( + r"[^\w]powf\s*\(" + CPP_RE_EOL, + include=[ + f"{d}/*.{ext}" for d in POWF_PROTECTED_DIRS for ext in ["h", "cpp", "tcc"] + ], +) +def lint_no_powf_in_core(fname, match): + return ( + f"{highlight('powf()')} pulls in __ieee754_powf (~2.3KB flash) and is not allowed in " + f"core or base entity platform code. These files are linked into every build.\n" + f"Please use alternatives:\n" + f" - {highlight('pow10_int(exp)')} for integer powers of 10 (from helpers.h)\n" + f" - Precomputed lookup tables for gamma/non-integer exponents\n" + f"(If powf is strictly necessary, add `// NOLINT` to the line)" + ) + + LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') From 4f69c487daa565dbe43a87f5b489f3dd40a3ff68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:04:12 -1000 Subject: [PATCH 0357/2056] [bk72xx] Fix ~100ms loop stalls by raising main task priority (#14420) --- esphome/components/libretiny/core.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 893a79440a7..6bb2d9dcc14 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -7,6 +7,9 @@ #include "esphome/core/helpers.h" #include "preferences.h" +#include +#include + void setup(); void loop(); @@ -22,6 +25,22 @@ void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { libretiny::setup_preferences(); lt_wdt_enable(10000L); +#ifdef USE_BK72XX + // BK72xx SDK creates the main Arduino task at priority 3, which is lower than + // all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop + // stalls whenever WiFi background processing runs, because the main task + // cannot resume until every higher-priority task finishes. + // + // By contrast, RTL87xx creates the main task at osPriorityRealtime (highest). + // + // Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the + // main loop, but below the TCP/IP thread (7) so packet processing keeps priority. + // This is safe because ESPHome yields voluntarily via yield_with_select_() and + // the Arduino mainTask yield() after each loop() iteration. + static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6; + static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES"); + vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY); +#endif #if LT_GPIO_RECOVER lt_gpio_recover(); #endif From b209c903bb6991c1242d40e347e3625f594bdaab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:05:15 -1000 Subject: [PATCH 0358/2056] [core] Inline trivial Component state accessors (#14425) --- esphome/core/component.cpp | 8 -------- esphome/core/component.h | 12 ++++++------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 53cb50a44ce..4ccc7478191 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -233,7 +233,6 @@ void Component::call_dump_config_() { } } -uint8_t Component::get_component_state() const { return this->component_state_; } void Component::call() { uint8_t state = this->component_state_ & COMPONENT_STATE_MASK; switch (state) { @@ -339,9 +338,6 @@ void Component::reset_to_construction_state() { this->status_clear_error(); } } -bool Component::is_in_loop_state() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; -} void Component::defer(std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } @@ -380,16 +376,12 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); #pragma GCC diagnostic pop } -bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_ready() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } -bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } bool Component::can_proceed() { return true; } -bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } -bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } bool Component::set_status_flag_(uint8_t flag) { if ((this->component_state_ & flag) != 0) return false; diff --git a/esphome/core/component.h b/esphome/core/component.h index d8102ea6708..e5127b0c9f2 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -142,7 +142,7 @@ class Component { */ virtual void on_powerdown() {} - uint8_t get_component_state() const; + uint8_t get_component_state() const { return this->component_state_; } /** Reset this component back to the construction state to allow setup to run again. * @@ -154,7 +154,7 @@ class Component { * * @return True if in loop state, false otherwise. */ - bool is_in_loop_state() const; + bool is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; } /** Check if this component is idle. * Being idle means being in LOOP_DONE state. @@ -162,7 +162,7 @@ class Component { * * @return True if the component is idle */ - bool is_idle() const; + bool is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; } /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. * @@ -230,15 +230,15 @@ class Component { */ void enable_loop_soon_any_context(); - bool is_failed() const; + bool is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool is_ready() const; virtual bool can_proceed(); - bool status_has_warning() const; + bool status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } - bool status_has_error() const; + bool status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); From 95544dddf8bd2171ff42da909fe9cd9786a10710 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 07:11:47 -1000 Subject: [PATCH 0359/2056] [ci] Add code-owner-approved label workflow (#14421) --- .github/scripts/auto-label-pr/detectors.js | 47 +----- .github/scripts/codeowners.js | 143 ++++++++++++++++ .../workflows/codeowner-approved-label.yml | 158 ++++++++++++++++++ .../workflows/codeowner-review-request.yml | 118 ++++--------- 4 files changed, 342 insertions(+), 124 deletions(-) create mode 100644 .github/scripts/codeowners.js create mode 100644 .github/workflows/codeowner-approved-label.yml diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 80d8847bc1f..832fcb41dba 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -7,6 +7,7 @@ const { hasDashboardChanges, hasGitHubActionsChanges, } = require('../detect-tags'); +const { loadCodeowners, getEffectiveOwners } = require('../codeowners'); // Strategy: Merge branch detection async function detectMergeBranch(context) { @@ -148,51 +149,15 @@ async function detectGitHubActionsChanges(changedFiles) { // Strategy: Code owner detection async function detectCodeOwner(github, context, changedFiles) { const labels = new Set(); - const { owner, repo } = context.repo; try { - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - }); - - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const codeownersPatterns = loadCodeowners(); const prAuthor = context.payload.pull_request.user.login; - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersRegexes = codeownersLines.map(line => { - const parts = line.split(/\s+/); - const pattern = parts[0]; - const owners = parts.slice(1); - - let regex; - if (pattern.endsWith('*')) { - const dir = pattern.slice(0, -1); - regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); - } else if (pattern.includes('*')) { - // First escape all regex special chars except *, then replace * with .* - const regexPattern = pattern - .replace(/[.+?^${}()|[\]\\]/g, '\\$&') - .replace(/\*/g, '.*'); - regex = new RegExp(`^${regexPattern}$`); - } else { - regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); - } - - return { regex, owners }; - }); - - for (const file of changedFiles) { - for (const { regex, owners } of codeownersRegexes) { - if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) { - labels.add('by-code-owner'); - return labels; - } - } + // Check if PR author is a codeowner of any changed file + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + if (effective.users.has(prAuthor)) { + labels.add('by-code-owner'); } } catch (error) { console.log('Failed to read or parse CODEOWNERS file:', error.message); diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js new file mode 100644 index 00000000000..9a10391699e --- /dev/null +++ b/.github/scripts/codeowners.js @@ -0,0 +1,143 @@ +// Shared CODEOWNERS parsing and matching utilities. +// +// Used by: +// - codeowner-review-request.yml +// - codeowner-approved-label.yml +// - auto-label-pr/detectors.js (detectCodeOwner) + +/** + * Convert a CODEOWNERS glob pattern to a RegExp. + * + * Handles **, *, and ? wildcards after escaping regex-special characters. + */ +function globToRegex(pattern) { + let regexStr = pattern + .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') + .replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace + .replace(/\*/g, '[^/]*') // single star + .replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar + .replace(/\?/g, '.'); + return new RegExp('^' + regexStr + '$'); +} + +/** + * Parse raw CODEOWNERS file content into an array of + * { pattern, regex, owners } objects. + * + * Each `owners` entry is the raw string from the file (e.g. "@user" or + * "@esphome/core"). + */ +function parseCodeowners(content) { + const lines = content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const patterns = []; + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + + const pattern = parts[0]; + const owners = parts.slice(1); + const regex = globToRegex(pattern); + patterns.push({ pattern, regex, owners }); + } + return patterns; +} + +/** + * Fetch and parse the CODEOWNERS file via the GitHub API. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {string} [ref] - git ref (SHA / branch) to read from + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +async function fetchCodeowners(github, owner, repo, ref) { + const params = { owner, repo, path: 'CODEOWNERS' }; + if (ref) params.ref = ref; + + const { data: file } = await github.rest.repos.getContent(params); + const content = Buffer.from(file.content, 'base64').toString('utf8'); + return parseCodeowners(content); +} + +/** + * Classify raw owner strings into individual users and teams. + * + * @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"] + * @returns {{ users: string[], teams: string[] }} + * users – login names without "@" + * teams – team slugs without the "org/" prefix + */ +function classifyOwners(rawOwners) { + const users = []; + const teams = []; + for (const o of rawOwners) { + const clean = o.startsWith('@') ? o.slice(1) : o; + if (clean.includes('/')) { + teams.push(clean.split('/')[1]); + } else { + users.push(clean); + } + } + return { users, teams }; +} + +/** + * For each file, find its effective codeowners using GitHub's + * "last match wins" semantics, then union across all files. + * + * @param {string[]} files - list of file paths + * @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners + * @returns {{ users: Set, teams: Set, matchedFileCount: number }} + */ +function getEffectiveOwners(files, codeownersPatterns) { + const users = new Set(); + const teams = new Set(); + let matchedFileCount = 0; + + for (const file of files) { + // Last matching pattern wins for each file + let effectiveOwners = null; + for (const { regex, owners } of codeownersPatterns) { + if (regex.test(file)) { + effectiveOwners = owners; + } + } + if (effectiveOwners) { + matchedFileCount++; + const classified = classifyOwners(effectiveOwners); + for (const u of classified.users) users.add(u); + for (const t of classified.teams) teams.add(t); + } + } + + return { users, teams, matchedFileCount }; +} + +/** + * Read and parse the CODEOWNERS file from disk. + * + * Use this when the repo is already checked out (avoids an API call). + * + * @param {string} [repoRoot='.'] - path to the repo root + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +function loadCodeowners(repoRoot = '.') { + const fs = require('fs'); + const path = require('path'); + const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8'); + return parseCodeowners(content); +} + +module.exports = { + globToRegex, + parseCodeowners, + fetchCodeowners, + loadCodeowners, + classifyOwners, + getEffectiveOwners +}; diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml new file mode 100644 index 00000000000..217ae06419b --- /dev/null +++ b/.github/workflows/codeowner-approved-label.yml @@ -0,0 +1,158 @@ +# This workflow adds/removes a 'code-owner-approved' label when a +# component-specific codeowner approves (or dismisses) a PR. +# This helps maintainers prioritize PRs that have codeowner sign-off. +# +# Only component-specific codeowners count — the catch-all @esphome/core +# team is excluded so the label reflects domain-expert approval. + +name: Codeowner Approved Label + +on: + pull_request_review: + types: [submitted, dismissed] + +permissions: + pull-requests: write + contents: read + +jobs: + codeowner-approved: + name: Run + if: ${{ github.repository == 'esphome/esphome' }} + runs-on: ubuntu-latest + steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + + - name: Check codeowner approval and update label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = context.payload.pull_request.number; + const LABEL_NAME = 'code-owner-approved'; + + console.log(`Processing PR #${pr_number} for codeowner approval label`); + + try { + // Get the list of changed files in this PR (with pagination) + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); + + const changedFiles = prFiles.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found, skipping'); + return; + } + + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); + + // Get effective owners using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + + // Only keep individual component-specific codeowners (exclude teams) + const componentCodeowners = effective.users; + + console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); + + if (componentCodeowners.size === 0) { + console.log('No component-specific codeowners found for changed files'); + // Remove label if present since there are no component codeowners + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`); + } catch (error) { + if (error.status !== 404) { + console.log(`Failed to remove label: ${error.message}`); + } + } + return; + } + + // Get all reviews on the PR + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: pr_number + } + ); + + // Get the latest review per user (reviews are returned chronologically) + const latestReviewByUser = new Map(); + for (const review of reviews) { + // Skip bot reviews and comment-only reviews + if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; + latestReviewByUser.set(review.user.login, review); + } + + // Check if any component-specific codeowner has an active approval + let hasCodeownerApproval = false; + for (const [login, review] of latestReviewByUser) { + if (review.state === 'APPROVED' && componentCodeowners.has(login)) { + console.log(`Codeowner '${login}' has approved`); + hasCodeownerApproval = true; + break; + } + } + + // Get current labels to check if label is already present + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr_number + }); + const hasLabel = currentLabels.some(label => label.name === LABEL_NAME); + + if (hasCodeownerApproval && !hasLabel) { + // Add the label + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (!hasCodeownerApproval && hasLabel) { + // Remove the label + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); + } catch (error) { + if (error.status !== 404) { + console.log(`Failed to remove label: ${error.message}`); + } + } + } else { + console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + } + + } catch (error) { + console.error(error); + core.setFailed(`Failed to process codeowner approval label: ${error.message}`); + } diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 6f4351b2984..02bf0e4a29e 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -24,10 +24,17 @@ jobs: if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: Request reviews from component codeowners uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const owner = context.repo.owner; const repo = context.repo.repo; const pr_number = context.payload.pull_request.number; @@ -38,12 +45,15 @@ jobs: const BOT_COMMENT_MARKER = ''; try { - // Get the list of changed files in this PR - const { data: files } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: pr_number - }); + // Get the list of changed files in this PR (with pagination) + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); const changedFiles = files.map(file => file.filename); console.log(`Found ${changedFiles.length} changed files`); @@ -53,32 +63,10 @@ jobs: return; } - // Fetch CODEOWNERS file from root - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - ref: context.payload.pull_request.base.sha - }); - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); - // Parse CODEOWNERS file to extract all patterns and their owners - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersPatterns = []; - - // Convert CODEOWNERS pattern to regex (robust glob handling) - function globToRegex(pattern) { - // Escape regex special characters except for glob wildcards - let regexStr = pattern - .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars - .replace(/\*\*/g, '.*') // globstar - .replace(/\*/g, '[^/]*') // single star - .replace(/\?/g, '.'); // question mark - return new RegExp('^' + regexStr + '$'); - } + console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); // Helper function to create comment body function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { @@ -93,50 +81,11 @@ jobs: } } - for (const line of codeownersLines) { - const parts = line.split(/\s+/); - if (parts.length < 2) continue; - - const pattern = parts[0]; - const owners = parts.slice(1); - - // Use robust glob-to-regex conversion - const regex = globToRegex(pattern); - codeownersPatterns.push({ pattern, regex, owners }); - } - - console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); - - // Match changed files against CODEOWNERS patterns - const matchedOwners = new Set(); - const matchedTeams = new Set(); - const fileMatches = new Map(); // Track which files matched which patterns - - for (const file of changedFiles) { - for (const { pattern, regex, owners } of codeownersPatterns) { - if (regex.test(file)) { - console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`); - - if (!fileMatches.has(file)) { - fileMatches.set(file, []); - } - fileMatches.get(file).push({ pattern, owners }); - - // Add owners to the appropriate set (remove @ prefix) - for (const owner of owners) { - const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; - if (cleanOwner.includes('/')) { - // Team mention (org/team-name) - const teamName = cleanOwner.split('/')[1]; - matchedTeams.add(teamName); - } else { - // Individual user - matchedOwners.add(cleanOwner); - } - } - } - } - } + // Match changed files against CODEOWNERS patterns using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const matchedOwners = effective.users; + const matchedTeams = effective.teams; + const matchedFileCount = effective.matchedFileCount; if (matchedOwners.size === 0 && matchedTeams.size === 0) { console.log('No codeowners found for any changed files'); @@ -170,11 +119,14 @@ jobs: } // Check for completed reviews to avoid re-requesting users who have already reviewed - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr_number - }); + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: pr_number + } + ); const reviewedUsers = new Set(); reviews.forEach(review => { @@ -247,7 +199,7 @@ jobs: } const totalReviewers = reviewersList.length + teamsList.length; - console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); + console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`); // Request reviews try { @@ -279,7 +231,7 @@ jobs: // Only add a comment if there are new codeowners to mention (not previously pinged) if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true); await github.rest.issues.createComment({ owner, @@ -297,7 +249,7 @@ jobs: // Only try to add a comment if there are new codeowners to mention if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false); try { await github.rest.issues.createComment({ From 380c0db0205db56960f3869b341fe20629c4a4f8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:49:38 +1100 Subject: [PATCH 0360/2056] [usb_uart] Don't claim interrupt interface for ch34x (#14431) --- esphome/components/usb_uart/ch34x.cpp | 9 +++++++++ esphome/components/usb_uart/usb_uart.cpp | 14 +++++++++----- esphome/components/usb_uart/usb_uart.h | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 7fa964c0cb6..e6e52a9e2ac 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -75,6 +75,15 @@ void USBUartTypeCH34X::enable_channels() { } this->start_channels(); } + +std::vector USBUartTypeCH34X::parse_descriptors(usb_device_handle_t dev_hdl) { + auto result = USBUartTypeCdcAcm::parse_descriptors(dev_hdl); + // ch34x doesn't use the interrupt endpoint, and we don't have endpoints to spare + for (auto &cdc_dev : result) { + cdc_dev.interrupt_interface_number = 0xFF; + } + return result; +} } // namespace esphome::usb_uart #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index de81bfc587b..5c0397b2cb4 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -20,6 +20,7 @@ static optional get_cdc(const usb_config_desc_t *config_desc, uint8_t in // look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out) CdcEps eps{}; eps.bulk_interface_number = 0xFF; + eps.interrupt_interface_number = 0xFF; for (;;) { const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset); if (!intf_desc) { @@ -130,7 +131,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) { } void USBUartChannel::write_array(const uint8_t *data, size_t len) { if (!this->initialised_.load()) { - ESP_LOGV(TAG, "Channel not initialised - write ignored"); + ESP_LOGD(TAG, "Channel not initialised - write ignored"); return; } #ifdef USE_UART_DEBUGGER @@ -415,14 +416,15 @@ void USBUartTypeCdcAcm::on_connected() { // Claim the communication (interrupt) interface so CDC class requests are accepted // by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before // they enable data flow on the bulk endpoints. - if (channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + if (channel->cdc_dev_.interrupt_interface_number != 0xFF && + channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number, 0); if (err_comm != ESP_OK) { ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, esp_err_to_name(err_comm)); + channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway } else { - channel->cdc_dev_.comm_interface_claimed = true; ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); } } @@ -436,6 +438,7 @@ void USBUartTypeCdcAcm::on_connected() { return; } } + this->status_clear_error(); this->enable_channels(); } @@ -453,9 +456,10 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } - if (channel->cdc_dev_.comm_interface_claimed) { + if (channel->cdc_dev_.interrupt_interface_number != 0xFF && + channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); - channel->cdc_dev_.comm_interface_claimed = false; + channel->cdc_dev_.interrupt_interface_number = 0xFF; } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 9a9fe1c2ca0..0d471e46f63 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -32,7 +32,6 @@ struct CdcEps { const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; uint8_t interrupt_interface_number; - bool comm_interface_claimed{false}; }; enum UARTParityOptions { @@ -192,6 +191,7 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { protected: void enable_channels() override; + std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; }; } // namespace esphome::usb_uart From 96793a99ce12fe46e349ca1a165869a9f29bb842 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 3 Mar 2026 21:55:56 +0100 Subject: [PATCH 0361/2056] [rtttl] add new codeowner (#14440) --- CODEOWNERS | 2 +- esphome/components/rtttl/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 21bee125c60..b22f85b71d7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -412,7 +412,7 @@ esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 -esphome/components/rtttl/* @glmnet +esphome/components/rtttl/* @glmnet @ximex esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index ebbe5366aaa..19412bb4547 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -17,7 +17,7 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) -CODEOWNERS = ["@glmnet"] +CODEOWNERS = ["@glmnet", "@ximex"] CONF_RTTTL = "rtttl" CONF_ON_FINISHED_PLAYBACK = "on_finished_playback" From ee78d7a0c05b8f3d6878751c95cd5c9dae232690 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:42:41 -0500 Subject: [PATCH 0362/2056] [tests] Fix integration test race condition in PlatformIO cache init (#14435) Co-authored-by: Claude Opus 4.6 --- tests/integration/conftest.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 36df1bc83ec..b7f7fc60b3b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -73,11 +73,6 @@ def shared_platformio_cache() -> Generator[Path]: test_cache_dir = Path.home() / ".esphome-integration-tests" cache_dir = test_cache_dir / "platformio" - # Create the temp directory that PlatformIO uses to avoid race conditions - # This ensures it exists and won't be deleted by parallel processes - platformio_tmp_dir = cache_dir / ".cache" / "tmp" - platformio_tmp_dir.mkdir(parents=True, exist_ok=True) - # Use a lock file in the home directory to ensure only one process initializes the cache # This is needed when running with pytest-xdist # The lock file must be in a directory that already exists to avoid race conditions @@ -87,8 +82,9 @@ def shared_platformio_cache() -> Generator[Path]: with open(lock_file, "w") as lock_fd: fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) - # Check if cache needs initialization while holding the lock - if not cache_dir.exists() or not any(cache_dir.iterdir()): + # Check if the native platform is installed (the actual indicator of a populated cache) + native_platform = cache_dir / "platforms" / "native" + if not native_platform.exists(): # Create the test cache directory if it doesn't exist test_cache_dir.mkdir(exist_ok=True) From 989330d6bc5caa2b94950b55d3811c5ee2c51be3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:54:40 +1300 Subject: [PATCH 0363/2056] [globals] Fix handling of string booleans in yaml (#14447) --- esphome/components/globals/__init__.py | 2 +- tests/components/globals/common.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fe11a93a4bb..fe83b1ea7c3 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -51,7 +51,7 @@ _RESTORING_SCHEMA = cv.Schema( def _globals_schema(config: ConfigType) -> ConfigType: """Select schema based on restore_value setting.""" - if config.get(CONF_RESTORE_VALUE, False): + if cv.boolean(config.get(CONF_RESTORE_VALUE, False)): return _RESTORING_SCHEMA(config) return _NON_RESTORING_SCHEMA(config) diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml index efa3cba0766..35dca0624f3 100644 --- a/tests/components/globals/common.yaml +++ b/tests/components/globals/common.yaml @@ -27,3 +27,14 @@ globals: type: bool restore_value: false initial_value: "false" + # Test restore_value with string "false" - should be converted to bool false + - id: glob_no_restore_string_false + type: int + restore_value: "false" + initial_value: "42" + # Test restore_value with string "true" - should be converted to bool true + - id: glob_restore_string_true + type: int + restore_value: "true" + initial_value: "99" + update_interval: 5s From 43a6fe9b6cb446974db2f3cce1f1f9b67cbe1719 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 3 Mar 2026 19:06:36 -0600 Subject: [PATCH 0364/2056] [core] add a StaticTask helper to manage task lifecycles (#14446) --- esphome/core/config.py | 4 +++ esphome/core/static_task.cpp | 64 ++++++++++++++++++++++++++++++++++++ esphome/core/static_task.h | 50 ++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 esphome/core/static_task.cpp create mode 100644 esphome/core/static_task.h diff --git a/esphome/core/config.py b/esphome/core/config.py index 9411949bb92..4f526404fe8 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -687,6 +687,10 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "static_task.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, "time_64.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/core/static_task.cpp b/esphome/core/static_task.cpp new file mode 100644 index 00000000000..4cfead44c29 --- /dev/null +++ b/esphome/core/static_task.cpp @@ -0,0 +1,64 @@ +#include "esphome/core/static_task.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" + +namespace esphome { + +bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, + bool use_psram) { + if (this->handle_ != nullptr) { + // Task is already created; must call destroy() first + return false; + } + + if (this->stack_buffer_ != nullptr && (stack_size > this->stack_size_ || use_psram != this->use_psram_)) { + // Existing buffer is too small or wrong memory type; deallocate to reallocate below + RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + allocator.deallocate(this->stack_buffer_, this->stack_size_); + this->stack_buffer_ = nullptr; + } + + if (this->stack_buffer_ == nullptr) { + this->stack_size_ = stack_size; + this->use_psram_ = use_psram; + RAMAllocator allocator(use_psram ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + this->stack_buffer_ = allocator.allocate(stack_size); + } + if (this->stack_buffer_ == nullptr) { + return false; + } + + this->handle_ = xTaskCreateStatic(fn, name, this->stack_size_, param, priority, this->stack_buffer_, &this->tcb_); + if (this->handle_ == nullptr) { + this->deallocate(); + return false; + } + return true; +} + +void StaticTask::destroy() { + if (this->handle_ != nullptr) { + TaskHandle_t handle = this->handle_; + this->handle_ = nullptr; + vTaskDelete(handle); + } +} + +void StaticTask::deallocate() { + this->destroy(); + if (this->stack_buffer_ != nullptr) { + RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL + : RAMAllocator::ALLOC_INTERNAL); + allocator.deallocate(this->stack_buffer_, this->stack_size_); + this->stack_buffer_ = nullptr; + this->stack_size_ = 0; + } +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/static_task.h b/esphome/core/static_task.h new file mode 100644 index 00000000000..5fd5b38f9ef --- /dev/null +++ b/esphome/core/static_task.h @@ -0,0 +1,50 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +#include + +namespace esphome { + +/** Helper for FreeRTOS static task management. + * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. + */ +class StaticTask { + public: + /// @brief Check if the task has been created and not yet destroyed. + bool is_created() const { return this->handle_ != nullptr; } + + /// @brief Get the FreeRTOS task handle. + TaskHandle_t get_handle() const { return this->handle_; } + + /// @brief Allocate stack and create task. + /// @param fn Task function + /// @param name Task name (for debug) + /// @param stack_size Stack size in StackType_t words + /// @param param Parameter passed to task function + /// @param priority FreeRTOS task priority + /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM + /// @return true on success + bool create(TaskFunction_t fn, const char *name, uint32_t stack_size, void *param, UBaseType_t priority, + bool use_psram); + + /// @brief Delete the task but keep the stack buffer allocated for reuse by a subsequent create() call. + void destroy(); + + /// @brief Delete the task (if running) and free the stack buffer. + void deallocate(); + + protected: + TaskHandle_t handle_{nullptr}; + StaticTask_t tcb_; + StackType_t *stack_buffer_{nullptr}; + uint32_t stack_size_{0}; + bool use_psram_{false}; +}; + +} // namespace esphome + +#endif // USE_ESP32 From 9371159a7e56e49f3b2ab71c6ad93034d1c29e01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2026 15:14:05 -1000 Subject: [PATCH 0365/2056] [core] Replace custom esphome::optional with std::optional (#14368) Co-authored-by: Claude Opus 4.6 --- esphome/components/am43/cover/am43_cover.cpp | 5 +- esphome/components/anova/anova.cpp | 10 +- esphome/components/ballu/ballu.cpp | 2 +- .../bang_bang/bang_bang_climate.cpp | 20 +- .../bedjet/climate/bedjet_climate.cpp | 20 +- esphome/components/bedjet/fan/bedjet_fan.cpp | 8 +- esphome/components/binary/fan/binary_fan.cpp | 15 +- .../ble_presence/ble_presence_device.h | 5 +- esphome/components/ble_rssi/ble_rssi_sensor.h | 5 +- esphome/components/climate_ir/climate_ir.cpp | 25 +- .../climate_ir_lg/climate_ir_lg.cpp | 2 +- .../components/climate_ir_lg/climate_ir_lg.h | 3 +- esphome/components/coolix/coolix.cpp | 2 +- esphome/components/coolix/coolix.h | 3 +- esphome/components/copy/cover/copy_cover.cpp | 15 +- esphome/components/copy/fan/copy_fan.cpp | 20 +- .../components/copy/select/copy_select.cpp | 5 +- .../current_based/current_based_cover.cpp | 5 +- esphome/components/daikin/daikin.cpp | 2 +- esphome/components/daikin_arc/daikin_arc.cpp | 7 +- esphome/components/daikin_brc/daikin_brc.cpp | 2 +- .../deep_sleep/deep_sleep_esp8266.cpp | 2 +- esphome/components/delonghi/delonghi.cpp | 2 +- .../demo/demo_alarm_control_panel.h | 9 +- esphome/components/demo/demo_climate.h | 48 ++-- esphome/components/demo/demo_cover.h | 10 +- esphome/components/demo/demo_fan.h | 20 +- esphome/components/demo/demo_lock.h | 5 +- esphome/components/demo/demo_valve.h | 11 +- esphome/components/emmeti/emmeti.cpp | 2 +- esphome/components/endstop/endstop_cover.cpp | 5 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 2 +- .../esp32_rmt_led_strip/led_strip.cpp | 5 +- .../components/fastled_base/fastled_light.cpp | 5 +- .../components/feedback/feedback_cover.cpp | 7 +- .../fujitsu_general/fujitsu_general.cpp | 2 +- esphome/components/gree/gree.cpp | 6 +- esphome/components/haier/hon_climate.cpp | 15 +- .../components/haier/smartair2_climate.cpp | 6 +- .../components/hbridge/fan/hbridge_fan.cpp | 20 +- esphome/components/he60r/he60r.cpp | 7 +- .../hitachi_ac344/hitachi_ac344.cpp | 2 +- .../hitachi_ac424/hitachi_ac424.cpp | 2 +- .../media_player/i2s_audio_media_player.cpp | 25 +- esphome/components/infrared/infrared.cpp | 5 +- esphome/components/ledc/ledc_output.cpp | 3 +- esphome/components/mcp4461/mcp4461.cpp | 5 +- esphome/components/midea/air_conditioner.cpp | 25 +- esphome/components/midea_ir/midea_ir.cpp | 23 +- esphome/components/mitsubishi/mitsubishi.cpp | 7 +- .../select/modbus_select.cpp | 2 +- esphome/components/noblex/noblex.cpp | 2 +- esphome/components/noblex/noblex.h | 3 +- .../components/output/lock/output_lock.cpp | 5 +- esphome/components/pid/pid_climate.cpp | 10 +- esphome/components/pzem004t/pzem004t.cpp | 5 +- esphome/components/select/select_call.cpp | 2 +- esphome/components/sgp4x/sgp4x.h | 18 +- .../media_player/speaker_media_player.cpp | 31 +-- esphome/components/speed/fan/speed_fan.cpp | 20 +- esphome/components/sprinkler/sprinkler.cpp | 21 +- esphome/components/tcl112/tcl112.cpp | 2 +- .../template_alarm_control_panel.cpp | 19 +- .../template/cover/template_cover.cpp | 10 +- .../template/datetime/template_date.cpp | 27 ++- .../template/datetime/template_datetime.cpp | 54 +++-- .../template/datetime/template_time.cpp | 27 ++- .../components/template/fan/template_fan.cpp | 20 +- .../template/lock/template_lock.cpp | 5 +- .../template/valve/template_valve.cpp | 5 +- .../water_heater/template_water_heater.cpp | 15 +- .../thermostat/thermostat_climate.cpp | 50 ++-- .../time_based/time_based_cover.cpp | 5 +- .../components/tormatic/tormatic_cover.cpp | 5 +- esphome/components/toshiba/toshiba.cpp | 8 +- .../components/tuya/climate/tuya_climate.cpp | 184 +++++++++------ esphome/components/tuya/cover/tuya_cover.cpp | 16 +- esphome/components/tuya/fan/tuya_fan.cpp | 88 ++++--- esphome/components/tuya/light/tuya_light.cpp | 5 +- .../climate/uponor_smatrix_climate.cpp | 5 +- esphome/components/whirlpool/whirlpool.cpp | 2 +- esphome/components/whynter/whynter.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 10 +- .../wifi/wifi_component_esp8266.cpp | 5 +- .../wifi/wifi_component_esp_idf.cpp | 5 +- esphome/components/yashima/yashima.cpp | 10 +- esphome/components/zhlt01/zhlt01.cpp | 6 +- esphome/core/entity_base.h | 2 +- esphome/core/optional.h | 218 +----------------- esphome/cpp_types.py | 4 +- tests/component_tests/text/test_text.py | 17 +- tests/components/template/common-base.yaml | 7 +- 92 files changed, 723 insertions(+), 701 deletions(-) diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 0d49439095e..2fa26d266a2 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) { ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (this->invert_position_) pos = 1 - pos; diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 2693224a97b..b625f92115d 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -24,8 +24,9 @@ void Anova::loop() { } void Anova::control(const ClimateCall &call) { - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_val = call.get_mode(); + if (mode_val.has_value()) { + ClimateMode mode = *mode_val; AnovaPacket *pkt; switch (mode) { case climate::CLIMATE_MODE_OFF: @@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } - if (call.get_target_temperature().has_value()) { - auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + auto *pkt = this->codec_->get_set_target_temp_request(*target_temp); auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp index b33ad11c1fb..deb742f8c67 100644 --- a/esphome/components/ballu/ballu.cpp +++ b/esphome/components/ballu/ballu.cpp @@ -47,7 +47,7 @@ void BalluClimate::transmit_state() { remote_state[11] = 0x1e; // Fan speed - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state[4] |= BALLU_FAN_HIGH; break; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 6871e9df5dc..1058bce6a45 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -45,17 +45,21 @@ void BangBangClimate::setup() { } void BangBangClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); + auto mode = call.get_mode(); + if (mode.has_value()) { + this->mode = *mode; } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) { + this->target_temperature_low = *target_temperature_low; } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) { + this->target_temperature_high = *target_temperature_high; } - if (call.get_preset().has_value()) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + auto preset = call.get_preset(); + if (preset.has_value()) { + this->change_away_(*preset == climate::CLIMATE_PRESET_AWAY); } this->compute_state_(); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 68a0342873b..a17407f08ff 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -96,8 +96,9 @@ void BedJetClimate::control(const ClimateCall &call) { return; } - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_opt = call.get_mode(); + if (mode_opt.has_value()) { + ClimateMode mode = *mode_opt; bool button_result; switch (mode) { case CLIMATE_MODE_OFF: @@ -125,8 +126,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_target_temperature().has_value()) { - auto target_temp = *call.get_target_temperature(); + auto target_temp_opt = call.get_target_temperature(); + if (target_temp_opt.has_value()) { + auto target_temp = *target_temp_opt; auto result = this->parent_->set_target_temp(target_temp); if (result) { @@ -134,8 +136,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_preset().has_value()) { - ClimatePreset preset = *call.get_preset(); + auto preset_opt = call.get_preset(); + if (preset_opt.has_value()) { + ClimatePreset preset = *preset_opt; bool result; if (preset == CLIMATE_PRESET_BOOST) { @@ -187,10 +190,11 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_fan_mode().has_value()) { + auto fan_mode_opt = call.get_fan_mode(); + if (fan_mode_opt.has_value()) { // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. - auto fan_mode = *call.get_fan_mode(); + auto fan_mode = *fan_mode_opt; bool result; if (fan_mode == CLIMATE_FAN_LOW) { result = this->parent_->set_fan_speed(20); diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index e2722410404..9539e169a45 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -19,7 +19,8 @@ void BedJetFan::control(const fan::FanCall &call) { } bool did_change = false; - if (call.get_state().has_value() && this->state != *call.get_state()) { + auto state_opt = call.get_state(); + if (state_opt.has_value() && this->state != *state_opt) { // Turning off is easy: if (this->state && this->parent_->button_off()) { this->state = false; @@ -36,8 +37,9 @@ void BedJetFan::control(const fan::FanCall &call) { } // ignore speed changes if not on or turning on - if (this->state && call.get_speed().has_value()) { - auto speed = *call.get_speed(); + auto speed_opt = call.get_speed(); + if (this->state && speed_opt.has_value()) { + auto speed = *speed_opt; if (speed >= 1) { this->speed = speed; // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index a2f75242de1..17d4df095a0 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -18,12 +18,15 @@ fan::FanTraits BinaryFan::get_traits() { return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); } void BinaryFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->write_state_(); this->publish_state(); diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index f2f0a3ed191..8ae5edab3ad 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -76,11 +76,12 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 80245a1fe10..81f21c94ddb 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -74,11 +74,12 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 50c8d459b01..cc291ff17cf 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -71,16 +71,21 @@ void ClimateIR::setup() { } void ClimateIR::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); - if (call.get_preset().has_value()) - this->preset = *call.get_preset(); + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->fan_mode = fan_mode; + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + auto preset = call.get_preset(); + if (preset.has_value()) + this->preset = preset; this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 7fe06462302..90e3d006a85 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -79,7 +79,7 @@ void LgIrClimate::transmit_state() { if (this->mode == climate::CLIMATE_MODE_OFF) { remote_state |= FAN_AUTO; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= FAN_MAX; break; diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h index 00fc99ae735..958245279f2 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.h +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -23,7 +23,8 @@ class LgIrClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { this->send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 5c6bfd7740a..d8ea6764781 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -83,7 +83,7 @@ void CoolixClimate::transmit_state() { this->fan_mode = climate::CLIMATE_FAN_AUTO; remote_state |= COOLIX_FAN_MODE_AUTO_DRY; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= COOLIX_FAN_MAX; break; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index f4b4ff8e0e8..51ddcdf8f2f 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -23,7 +23,8 @@ class CoolixClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp index 28f8c9877c9..c139869d8f5 100644 --- a/esphome/components/copy/cover/copy_cover.cpp +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -38,12 +38,15 @@ cover::CoverTraits CopyCover::get_traits() { void CopyCover::control(const cover::CoverCall &call) { auto call2 = source_->make_call(); call2.set_stop(call.get_stop()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); - if (call.get_position().has_value()) - call2.set_position(*call.get_position()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); + auto tilt = call.get_tilt(); + if (tilt.has_value()) + call2.set_tilt(*tilt); + auto position = call.get_position(); + if (position.has_value()) + call2.set_position(*position); + auto tilt2 = call.get_tilt(); + if (tilt2.has_value()) + call2.set_tilt(*tilt2); call2.perform(); } diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index b4a43cf2f18..14c600d71f4 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -45,14 +45,18 @@ fan::FanTraits CopyFan::get_traits() { void CopyFan::control(const fan::FanCall &call) { auto call2 = source_->make_call(); - if (call.get_state().has_value()) - call2.set_state(*call.get_state()); - if (call.get_oscillating().has_value()) - call2.set_oscillating(*call.get_oscillating()); - if (call.get_speed().has_value()) - call2.set_speed(*call.get_speed()); - if (call.get_direction().has_value()) - call2.set_direction(*call.get_direction()); + auto state = call.get_state(); + if (state.has_value()) + call2.set_state(*state); + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + call2.set_oscillating(*oscillating); + auto speed = call.get_speed(); + if (speed.has_value()) + call2.set_speed(*speed); + auto direction = call.get_direction(); + if (direction.has_value()) + call2.set_direction(*direction); if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index e85e08e3536..227fe33182b 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -11,8 +11,9 @@ void CopySelect::setup() { traits.set_options(source_->traits.get_options()); - if (source_->has_state()) - this->publish_state(source_->active_index().value()); + auto idx = this->source_->active_index(); + if (idx.has_value()) + this->publish_state(*idx); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 58ae7cbc34a..13bf11b9912 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -37,8 +37,9 @@ void CurrentBasedCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (fabsf(this->position - pos) < 0.01) { // already at target } else { diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index 359c63aecac..a285f3613db 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() const { uint16_t DaikinClimate::fan_speed_() const { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: fan_speed = DAIKIN_FAN_SILENT << 8; break; diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index 47263108065..c45fa307a7c 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -176,7 +176,7 @@ uint8_t DaikinArcClimate::operation_mode_() { uint16_t DaikinArcClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_FAN_1 << 8; break; @@ -485,8 +485,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } void DaikinArcClimate::control(const climate::ClimateCall &call) { - if (call.get_target_humidity().has_value()) { - this->target_humidity = *call.get_target_humidity(); + auto target_humidity = call.get_target_humidity(); + if (target_humidity.has_value()) { + this->target_humidity = *target_humidity; } climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp index 6683d70f807..1179cb07d70 100644 --- a/esphome/components/daikin_brc/daikin_brc.cpp +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -111,7 +111,7 @@ uint8_t DaikinBrcClimate::operation_mode_() { uint8_t DaikinBrcClimate::fan_speed_swing_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_BRC_FAN_1; break; diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index 54d2aa993de..efbd45c34e7 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -15,7 +15,7 @@ void DeepSleepComponent::dump_config_platform_() {} bool DeepSleepComponent::prepare_to_sleep_() { return true; } void DeepSleepComponent::deep_sleep_() { - ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) + ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance) } } // namespace deep_sleep diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp index 9bc0b5753d8..19af703ab26 100644 --- a/esphome/components/delonghi/delonghi.cpp +++ b/esphome/components/delonghi/delonghi.cpp @@ -64,7 +64,7 @@ uint8_t DelonghiClimate::operation_mode_() { uint16_t DelonghiClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DELONGHI_FAN_LOW; break; diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index f59434830b1..76cb24c2f4a 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -29,10 +29,11 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { protected: void control(const AlarmControlPanelCall &call) override { auto state = call.get_state().value_or(ACP_STATE_DISARMED); + auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code_to_arm() && code.has_value()) { + if (*code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -40,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code() && code.has_value()) { + if (*code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index e2dfb0142be..c5f07ac1145 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -45,33 +45,31 @@ class DemoClimate : public climate::Climate, public Component { protected: void control(const climate::ClimateCall &call) override { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); - } - if (call.get_target_temperature().has_value()) { - this->target_temperature = *call.get_target_temperature(); - } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); - } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); - } - if (call.get_fan_mode().has_value()) { - this->set_fan_mode_(*call.get_fan_mode()); - } - if (call.get_swing_mode().has_value()) { - this->swing_mode = *call.get_swing_mode(); - } - if (call.has_custom_fan_mode()) { + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) + this->target_temperature_low = *target_temperature_low; + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) + this->target_temperature_high = *target_temperature_high; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->set_fan_mode_(*fan_mode); + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + if (call.has_custom_fan_mode()) this->set_custom_fan_mode_(call.get_custom_fan_mode()); - } - if (call.get_preset().has_value()) { - this->set_preset_(*call.get_preset()); - } - if (call.has_custom_preset()) { + auto preset = call.get_preset(); + if (preset.has_value()) + this->set_preset_(*preset); + if (call.has_custom_preset()) this->set_custom_preset_(call.get_custom_preset()); - } this->publish_state(); } climate::ClimateTraits traits() override { diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index ec266d46ab0..69dd5a4d2d1 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -38,8 +38,9 @@ class DemoCover : public cover::Cover, public Component { protected: void control(const cover::CoverCall &call) override { - if (call.get_position().has_value()) { - float target = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + float target = *pos; this->current_operation = target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; @@ -49,8 +50,9 @@ class DemoCover : public cover::Cover, public Component { this->publish_state(); }); } - if (call.get_tilt().has_value()) { - this->tilt = *call.get_tilt(); + auto tilt = call.get_tilt(); + if (tilt.has_value()) { + this->tilt = *tilt; } if (call.get_stop()) { this->cancel_timeout("move"); diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index 09edc4e0b7f..a8b397f19ac 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -47,14 +47,18 @@ class DemoFan : public fan::Fan, public Component { protected: void control(const fan::FanCall &call) override { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto speed = call.get_speed(); + if (speed.has_value()) + this->speed = *speed; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->publish_state(); } diff --git a/esphome/components/demo/demo_lock.h b/esphome/components/demo/demo_lock.h index 94d0f70a143..1e3fd51db4c 100644 --- a/esphome/components/demo/demo_lock.h +++ b/esphome/components/demo/demo_lock.h @@ -8,8 +8,9 @@ namespace demo { class DemoLock : public lock::Lock { protected: void control(const lock::LockCall &call) override { - auto state = *call.get_state(); - this->publish_state(state); + auto state = call.get_state(); + if (state.has_value()) + this->publish_state(*state); } }; diff --git a/esphome/components/demo/demo_valve.h b/esphome/components/demo/demo_valve.h index 55d457f1768..9a3122aca5c 100644 --- a/esphome/components/demo/demo_valve.h +++ b/esphome/components/demo/demo_valve.h @@ -26,12 +26,15 @@ class DemoValve : public valve::Valve { protected: void control(const valve::ValveCall &call) override { - if (call.get_position().has_value()) { - this->position = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + this->position = *pos; this->publish_state(); return; - } else if (call.get_toggle().has_value()) { - if (call.get_toggle().value()) { + } + auto toggle = call.get_toggle(); + if (toggle.has_value()) { + if (*toggle) { if (this->position == valve::VALVE_OPEN) { this->position = valve::VALVE_CLOSED; this->publish_state(); diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index d3e923cbefc..04976d95d70 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -28,7 +28,7 @@ uint8_t EmmetiClimate::set_mode_() { } uint8_t EmmetiClimate::set_fan_speed_() { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return EMMETI_FAN_1; case climate::CLIMATE_FAN_MEDIUM: diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index ea8a5ec1869..5e0b9c72d3c 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -37,8 +37,9 @@ void EndstopCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (pos == this->position) { // already at target } else { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index fa0cdb6f452..7f1c2b0f7c8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -107,7 +107,7 @@ class ESPBTDevice { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); if (res.has_value()) - return *res; + return res; } return {}; } diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 8bb5cbb62ed..66b41931aac 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -162,7 +162,8 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + auto rate = this->max_refresh_rate_.value_or(0); + if (rate != 0 && (now - this->last_refresh_) < rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; @@ -301,7 +302,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { " RGB Order: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - rgb_order, *this->max_refresh_rate_, this->num_leds_); + rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_); } float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index b3946a34b5f..504b8d473e4 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -21,12 +21,13 @@ void FastLEDLightOutput::dump_config() { "FastLED light:\n" " Num LEDs: %u\n" " Max refresh rate: %u", - this->num_leds_, *this->max_refresh_rate_); + this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + uint32_t max_rate = this->max_refresh_rate_.value_or(0); + if (max_rate != 0 && (now - this->last_refresh_) < max_rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index ffb19fa091b..d247bada33f 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -269,9 +269,12 @@ void FeedbackCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_CLOSING); } } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; if (pos == this->position) { // already at target, diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 6c7adebfeaf..8aa0f517287 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -141,7 +141,7 @@ void FujitsuGeneralClimate::transmit_state() { } // Set fan - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH); break; diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index b8cf8a39a85..8a9f264932c 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -180,7 +180,7 @@ uint8_t GreeClimate::operation_mode_() { uint8_t GreeClimate::fan_speed_() { // YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high if (this->model_ == GREE_YX1FF) { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: return GREE_FAN_1; case climate::CLIMATE_FAN_LOW: @@ -195,7 +195,7 @@ uint8_t GreeClimate::fan_speed_() { } } - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return GREE_FAN_1; case climate::CLIMATE_FAN_MEDIUM: @@ -235,7 +235,7 @@ uint8_t GreeClimate::temperature_() { uint8_t GreeClimate::preset_() { // YX1FF has sleep preset if (this->model_ == GREE_YX1FF) { - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_NONE: return GREE_PRESET_NONE; case climate::CLIMATE_PRESET_SLEEP: diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index d98d273957a..be5035caa17 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -893,7 +893,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -936,7 +937,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode @@ -1301,7 +1303,8 @@ void HonClimate::clear_control_messages_queue_() { } bool HonClimate::prepare_pending_action() { - switch (this->action_request_.value().action) { + auto &action_request = this->action_request_.value(); // NOLINT(bugprone-unchecked-optional-access) + switch (action_request.action) { case ActionRequest::START_SELF_CLEAN: if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) { uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; @@ -1315,12 +1318,12 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; } else if (this->control_method_ == HonControlMethod::SET_SINGLE_PARAMETER) { - this->action_request_.value().message = + action_request.message = haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::SELF_CLEANING, @@ -1343,7 +1346,7 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 63c22821b3e..d24f8ad8498 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -402,7 +402,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -446,7 +447,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 38e4129e66f..89c162eebfc 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -49,14 +49,18 @@ void HBridgeFan::dump_config() { } void HBridgeFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value()) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value()) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value()) + this->direction = *call_direction; this->apply_preset_mode_(call); this->write_state_(); diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index ca179302726..fdcd1a29c05 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -171,9 +171,12 @@ void HE60rCover::control(const CoverCall &call) { } else { this->toggles_needed_++; } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; // are we at the target? if (pos == this->position) { this->start_direction_(COVER_OPERATION_IDLE); diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 2bcb205644c..69469cab2ea 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -175,7 +175,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC344_FAN_LOW); break; diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 64f23dfc174..0b3cc99a82b 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -176,7 +176,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC424_FAN_LOW); break; diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 39301220d5a..369c964a859 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -11,17 +11,18 @@ static const char *const TAG = "audio"; void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING; - if (call.get_announcement().has_value()) { - play_state = call.get_announcement().value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING - : media_player::MEDIA_PLAYER_STATE_PLAYING; + auto announcement = call.get_announcement(); + if (announcement.has_value()) { + play_state = *announcement ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING : media_player::MEDIA_PLAYER_STATE_PLAYING; } - if (call.get_media_url().has_value()) { - this->current_url_ = call.get_media_url(); + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + this->current_url_ = media_url; if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) { if (this->audio_->isRunning()) { this->audio_->stopSong(); } - this->audio_->connecttohost(this->current_url_.value().c_str()); + this->audio_->connecttohost(media_url->c_str()); this->state = play_state; } else { this->start(); @@ -32,13 +33,15 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { this->is_announcement_ = true; } - if (call.get_volume().has_value()) { - this->volume = call.get_volume().value(); + auto vol = call.get_volume(); + if (vol.has_value()) { + this->volume = *vol; this->set_volume_(volume); this->unmute_(); } - if (call.get_command().has_value()) { - switch (call.get_command().value()) { + auto cmd = call.get_command(); + if (cmd.has_value()) { + switch (*cmd) { case media_player::MEDIA_PLAYER_COMMAND_MUTE: this->mute_(); break; @@ -67,7 +70,7 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { if (this->i2s_state_ != I2S_STATE_RUNNING) { return; } - switch (call.get_command().value()) { + switch (*cmd) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: if (!this->audio_->isRunning()) this->audio_->pauseResume(); diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 44318699511..658c9fd0df5 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -90,8 +90,9 @@ void Infrared::control(const InfraredCall &call) { auto *transmit_data = transmit_call.get_data(); // Set carrier frequency - if (call.get_carrier_frequency().has_value()) { - transmit_data->set_carrier_frequency(call.get_carrier_frequency().value()); + auto freq = call.get_carrier_frequency(); + if (freq.has_value()) { + transmit_data->set_carrier_frequency(*freq); } // Set timings based on format diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a01d42ac8b2..21e06822575 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -56,7 +56,8 @@ optional ledc_bit_depth_for_frequency(float frequency) { esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, uint8_t channel, uint8_t &bit_depth, float frequency) { - bit_depth = *ledc_bit_depth_for_frequency(frequency); + auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); + bit_depth = bit_depth_opt.value_or(0); if (bit_depth < 1) { ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", frequency); } diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 2f2c75e05ad..dc7e7019aa0 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -19,8 +19,9 @@ void Mcp4461Component::setup() { // save WP/WL status this->update_write_protection_status_(); for (uint8_t i = 0; i < 8; i++) { - if (this->reg_[i].initial_value.has_value()) { - uint16_t initial_state = static_cast(*this->reg_[i].initial_value * 256.0f); + auto init_val = this->reg_[i].initial_value; + if (init_val.has_value()) { + uint16_t initial_state = static_cast(*init_val * 256.0f); this->write_wiper_level_(i, initial_state); } if (this->reg_[i].enabled) { diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index bc750e37135..4d59a4fbbca 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -56,20 +56,25 @@ void AirConditioner::on_status_change() { void AirConditioner::control(const ClimateCall &call) { dudanov::midea::ac::Control ctrl{}; - if (call.get_target_temperature().has_value()) - ctrl.targetTemp = call.get_target_temperature().value(); - if (call.get_swing_mode().has_value()) - ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); - if (call.get_mode().has_value()) - ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); - if (call.get_preset().has_value()) { - ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); + auto target_temp_val = call.get_target_temperature(); + if (target_temp_val.has_value()) + ctrl.targetTemp = *target_temp_val; + auto swing_mode_val = call.get_swing_mode(); + if (swing_mode_val.has_value()) + ctrl.swingMode = Converters::to_midea_swing_mode(*swing_mode_val); + auto mode_val = call.get_mode(); + if (mode_val.has_value()) + ctrl.mode = Converters::to_midea_mode(*mode_val); + auto preset_val = call.get_preset(); + if (preset_val.has_value()) { + ctrl.preset = Converters::to_midea_preset(*preset_val); } else if (call.has_custom_preset()) { // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str()); } - if (call.get_fan_mode().has_value()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); + auto fan_mode_val = call.get_fan_mode(); + if (fan_mode_val.has_value()) { + ctrl.fanMode = Converters::to_midea_fan_mode(*fan_mode_val); } else if (call.has_custom_fan_mode()) { // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str()); diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp index eaee1c731cb..220bb3f414d 100644 --- a/esphome/components/midea_ir/midea_ir.cpp +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -114,15 +114,20 @@ void MideaIR::control(const climate::ClimateCall &call) { if (call.get_mode() == climate::CLIMATE_MODE_OFF) { this->swing_mode = climate::CLIMATE_SWING_OFF; this->preset = climate::CLIMATE_PRESET_NONE; - } else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF && - this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || - (*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL && - this->swing_mode == climate::CLIMATE_SWING_OFF))) { - this->swing_ = true; - } else if (call.get_preset().has_value() && - ((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || - (*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { - this->boost_ = true; + } else { + auto swing = call.get_swing_mode(); + if (swing.has_value() && + ((*swing == climate::CLIMATE_SWING_OFF && this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || + (*swing == climate::CLIMATE_SWING_VERTICAL && this->swing_mode == climate::CLIMATE_SWING_OFF))) { + this->swing_ = true; + } else { + auto preset = call.get_preset(); + if (preset.has_value() && + ((*preset == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || + (*preset == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { + this->boost_ = true; + } + } } climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index d80b7aeff56..882163ff5db 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -180,7 +180,7 @@ void MitsubishiClimate::transmit_state() { // For 5Level: Low = 1, Middle = 2, Medium = 3, High = 4 // For 4Level + Quiet: Low = 1, Middle = 2, Medium = 3, High = 4, Quiet = 5 - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state[9] = 1; break; @@ -209,7 +209,8 @@ void MitsubishiClimate::transmit_state() { break; } - ESP_LOGD(TAG, "fan: %02x state: %02x", this->fan_mode.value(), remote_state[9]); + ESP_LOGD(TAG, "fan: %02x state: %02x", static_cast(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)), + remote_state[9]); // Vertical Vane switch (this->swing_mode) { @@ -227,7 +228,7 @@ void MitsubishiClimate::transmit_state() { ESP_LOGD(TAG, "default_vertical_direction_: %02X", this->default_vertical_direction_); // Special modes - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_ECO: remote_state[6] = MITSUBISHI_MODE_COOL | MITSUBISHI_OTHERWISE; remote_state[8] = (remote_state[8] & ~7) | MITSUBISHI_MODE_A_COOL; diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 853f4215c35..e2a54d3f602 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -52,7 +52,7 @@ void ModbusSelect::control(size_t index) { // Transform func requires string parameter for backward compatibility auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data); if (val.has_value()) { - mapval = *val; + mapval = val; ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); } else { ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control"); diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index 53f807809eb..f1e76eabf2b 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -71,7 +71,7 @@ void NoblexClimate::transmit_state() { break; } - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2); break; diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index a8e5f41547c..57990db0053 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -26,7 +26,8 @@ class NoblexClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/output/lock/output_lock.cpp b/esphome/components/output/lock/output_lock.cpp index 2545f624811..c373cd7b7c9 100644 --- a/esphome/components/output/lock/output_lock.cpp +++ b/esphome/components/output/lock/output_lock.cpp @@ -9,7 +9,10 @@ static const char *const TAG = "output.lock"; void OutputLock::dump_config() { LOG_LOCK("", "Output Lock", this); } void OutputLock::control(const lock::LockCall &call) { - auto state = *call.get_state(); + auto state_val = call.get_state(); + if (!state_val.has_value()) + return; + auto state = *state_val; if (state == lock::LOCK_STATE_LOCKED) { this->output_->turn_on(); } else if (state == lock::LOCK_STATE_UNLOCKED) { diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 2094c0e942f..54b7a688b41 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -41,10 +41,12 @@ void PIDClimate::setup() { } } void PIDClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); + auto call_mode = call.get_mode(); + if (call_mode.has_value()) + this->mode = *call_mode; + auto call_target = call.get_target_temperature(); + if (call_target.has_value()) + this->target_temperature = *call_target; // If switching to off mode, set output immediately if (this->mode == climate::CLIMATE_MODE_OFF) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index 356847825e6..d0f96d6d1e5 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -26,7 +26,10 @@ void PZEM004T::loop() { // PZEM004T packet size is 7 byte while (this->available() >= 7) { - auto resp = *this->read_array<7>(); + auto resp_opt = this->read_array<7>(); + if (!resp_opt.has_value()) + break; + auto resp = *resp_opt; // packet format: // 0: packet type // 1-5: data diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 2ff99c961d6..45fb42c1160 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -69,7 +69,7 @@ optional SelectCall::calculate_target_index_(const char *name) { ESP_LOGW(TAG, "'%s' - No option set", name); return {}; } - return this->index_.value(); + return this->index_; } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 8b31bca28cf..89fa627c61c 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -81,22 +81,16 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t std_initial, uint16_t gain_factor) { - voc_tuning_params_.value().index_offset = index_offset; - voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - voc_tuning_params_.value().std_initial = std_initial; - voc_tuning_params_.value().gain_factor = gain_factor; + this->voc_tuning_params_ = GasTuning{ + index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor}; } void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, uint16_t gain_factor) { - nox_tuning_params_.value().index_offset = index_offset; - nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; - nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; - nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; - nox_tuning_params_.value().std_initial = 50; - nox_tuning_params_.value().gain_factor = gain_factor; + this->nox_tuning_params_ = + GasTuning{index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, 50, + gain_factor}; } protected: diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 3f5cb2fda62..9f168f854d8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -144,7 +144,7 @@ void SpeakerMediaPlayer::watch_media_commands_() { delete media_command.url.value(); } if (media_command.file.has_value()) { - playlist_item.file = media_command.file.value(); + playlist_item.file = media_command.file; } if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { @@ -495,18 +495,21 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { MediaCallCommand media_command; - if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) { + auto ann = call.get_announcement(); + if (this->single_pipeline_() || (ann.has_value() && *ann)) { media_command.announce = true; } else { media_command.announce = false; } - if (call.get_media_url().has_value()) { - media_command.url = new std::string( - call.get_media_url().value()); // Must be manually deleted after receiving media_command from a queue + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + media_command.url = + new std::string(*media_url); // Must be manually deleted after receiving media_command from a queue - if (call.get_command().has_value()) { - if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) { + auto cmd = call.get_command(); + if (cmd.has_value()) { + if (*cmd == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) { media_command.enqueue = true; } } @@ -515,18 +518,20 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { return; } - if (call.get_volume().has_value()) { - media_command.volume = call.get_volume().value(); + auto vol = call.get_volume(); + if (vol.has_value()) { + media_command.volume = vol; // Wait 0 ticks for queue to be free, volume sets aren't that important! xQueueSend(this->media_control_command_queue_, &media_command, 0); return; } - if (call.get_command().has_value()) { - media_command.command = call.get_command().value(); + auto cmd = call.get_command(); + if (cmd.has_value()) { + media_command.command = cmd; TickType_t ticks_to_wait = portMAX_DELAY; - if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || - (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { + if ((*cmd == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) || + (*cmd == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) { ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important! } xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 55f7fd162c2..d45237c4677 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -21,14 +21,18 @@ void SpeedFan::setup() { void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } void SpeedFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value()) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value()) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value()) + this->direction = *call_direction; this->apply_preset_mode_(call); this->write_state_(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 44fb9092bc3..d1f74520540 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -44,7 +44,7 @@ SprinklerControllerSwitch::SprinklerControllerSwitch() = default; void SprinklerControllerSwitch::loop() { // Loop is only enabled when f_ has a value (see setup()) - auto s = (*this->f_)(); + auto s = (*this->f_)(); // NOLINT(bugprone-unchecked-optional-access) if (s.has_value()) { this->publish_state(*s); } @@ -89,20 +89,21 @@ void SprinklerValveOperator::loop() { uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case STARTING: - if ((now - *this->start_millis_) > this->start_delay_) { + if ((now - *this->start_millis_) > this->start_delay_) { // NOLINT(bugprone-unchecked-optional-access) this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state } break; case ACTIVE: - if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) { + if ((now - *this->start_millis_) > // NOLINT(bugprone-unchecked-optional-access) + (this->start_delay_ + this->run_duration_)) { this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down } break; case STOPPING: - if ((now - *this->stop_millis_) > this->stop_delay_) { - this->kill_(); // stop_delay_has been exceeded, ensure all valves are off + if ((now - *this->stop_millis_) > this->stop_delay_) { // NOLINT(bugprone-unchecked-optional-access) + this->kill_(); // stop_delay_has been exceeded, ensure all valves are off } break; @@ -1067,7 +1068,8 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { if (this->valve_is_enabled_(valve)) { enabled_valve_count++; if (!this->valve_cycle_complete_(valve)) { - if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { + auto active = this->active_valve(); + if (!active.has_value() || (valve != *active)) { total_time_remaining += this->valve_run_duration_adjusted(valve); incomplete_valve_count++; } else { @@ -1190,8 +1192,11 @@ switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) { } switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) { - if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { - return this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + if (this->is_a_valid_valve(valve_number)) { + auto idx = this->valve_[valve_number].pump_switch_index; + if (idx.has_value()) { + return this->pump_[*idx]; + } } return nullptr; } diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index a88e8e96a7e..afeee3d7396 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -89,7 +89,7 @@ void Tcl112Climate::transmit_state() { // Set fan uint8_t selected_fan; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: selected_fan = TCL112_FAN_HIGH; break; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 09efe678ce2..651aa3c489e 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -257,14 +257,16 @@ void TemplateAlarmControlPanel::bypass_before_arming() { } void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { - if (call.get_state()) { - if (call.get_state() == ACP_STATE_ARMED_AWAY) { + auto opt_state = call.get_state(); + if (opt_state) { + auto state = *opt_state; + if (state == ACP_STATE_ARMED_AWAY) { this->arm_(call.get_code(), ACP_STATE_ARMED_AWAY, this->arming_away_time_); - } else if (call.get_state() == ACP_STATE_ARMED_HOME) { + } else if (state == ACP_STATE_ARMED_HOME) { this->arm_(call.get_code(), ACP_STATE_ARMED_HOME, this->arming_home_time_); - } else if (call.get_state() == ACP_STATE_ARMED_NIGHT) { + } else if (state == ACP_STATE_ARMED_NIGHT) { this->arm_(call.get_code(), ACP_STATE_ARMED_NIGHT, this->arming_night_time_); - } else if (call.get_state() == ACP_STATE_DISARMED) { + } else if (state == ACP_STATE_DISARMED) { if (!this->is_code_valid_(call.get_code())) { ESP_LOGW(TAG, "Not disarming code doesn't match"); return; @@ -274,13 +276,12 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { #ifdef USE_BINARY_SENSOR this->bypassed_sensor_indicies_.clear(); #endif - } else if (call.get_state() == ACP_STATE_TRIGGERED) { + } else if (state == ACP_STATE_TRIGGERED) { this->publish_state(ACP_STATE_TRIGGERED); - } else if (call.get_state() == ACP_STATE_PENDING) { + } else if (state == ACP_STATE_PENDING) { this->publish_state(ACP_STATE_PENDING); } else { - ESP_LOGE(TAG, "State not yet implemented: %s", - LOG_STR_ARG(alarm_control_panel_state_to_string(*call.get_state()))); + ESP_LOGE(TAG, "State not yet implemented: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state))); } } } diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 7f5d68623fa..d5e0967e1e3 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -74,8 +74,9 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->stop_prev_trigger_(); if (pos == COVER_OPEN) { @@ -93,8 +94,9 @@ void TemplateCover::control(const CoverCall &call) { } } - if (call.get_tilt().has_value()) { - auto tilt = *call.get_tilt(); + auto tilt_val = call.get_tilt(); + if (tilt_val.has_value()) { + auto tilt = *tilt_val; this->tilt_trigger_.trigger(tilt); if (this->optimistic_) { diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 8a5f11b876d..c0f5d96c3da 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -48,46 +48,49 @@ void TemplateDate::update() { } void TemplateDate::control(const datetime::DateCall &call) { - bool has_year = call.get_year().has_value(); - bool has_month = call.get_month().has_value(); - bool has_day = call.get_day().has_value(); + auto opt_year = call.get_year(); + auto opt_month = call.get_month(); + auto opt_day = call.get_day(); + bool has_year = opt_year.has_value(); + bool has_month = opt_month.has_value(); + bool has_day = opt_day.has_value(); ESPTime value = {}; if (has_year) - value.year = *call.get_year(); + value.year = *opt_year; if (has_month) - value.month = *call.get_month(); + value.month = *opt_month; if (has_day) - value.day_of_month = *call.get_day(); + value.day_of_month = *opt_day; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) - this->year_ = *call.get_year(); + this->year_ = *opt_year; if (has_month) - this->month_ = *call.get_month(); + this->month_ = *opt_month; if (has_day) - this->day_ = *call.get_day(); + this->day_ = *opt_day; this->publish_state(); } if (this->restore_value_) { datetime::DateEntityRestoreState temp = {}; if (has_year) { - temp.year = *call.get_year(); + temp.year = *opt_year; } else { temp.year = this->year_; } if (has_month) { - temp.month = *call.get_month(); + temp.month = *opt_month; } else { temp.month = this->month_; } if (has_day) { - temp.day = *call.get_day(); + temp.day = *opt_day; } else { temp.day = this->day_; } diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 269a1d06ca8..5b8b308c008 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -54,79 +54,85 @@ void TemplateDateTime::update() { } void TemplateDateTime::control(const datetime::DateTimeCall &call) { - bool has_year = call.get_year().has_value(); - bool has_month = call.get_month().has_value(); - bool has_day = call.get_day().has_value(); - bool has_hour = call.get_hour().has_value(); - bool has_minute = call.get_minute().has_value(); - bool has_second = call.get_second().has_value(); + auto opt_year = call.get_year(); + auto opt_month = call.get_month(); + auto opt_day = call.get_day(); + auto opt_hour = call.get_hour(); + auto opt_minute = call.get_minute(); + auto opt_second = call.get_second(); + bool has_year = opt_year.has_value(); + bool has_month = opt_month.has_value(); + bool has_day = opt_day.has_value(); + bool has_hour = opt_hour.has_value(); + bool has_minute = opt_minute.has_value(); + bool has_second = opt_second.has_value(); ESPTime value = {}; if (has_year) - value.year = *call.get_year(); + value.year = *opt_year; if (has_month) - value.month = *call.get_month(); + value.month = *opt_month; if (has_day) - value.day_of_month = *call.get_day(); + value.day_of_month = *opt_day; if (has_hour) - value.hour = *call.get_hour(); + value.hour = *opt_hour; if (has_minute) - value.minute = *call.get_minute(); + value.minute = *opt_minute; if (has_second) - value.second = *call.get_second(); + value.second = *opt_second; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) - this->year_ = *call.get_year(); + this->year_ = *opt_year; if (has_month) - this->month_ = *call.get_month(); + this->month_ = *opt_month; if (has_day) - this->day_ = *call.get_day(); + this->day_ = *opt_day; if (has_hour) - this->hour_ = *call.get_hour(); + this->hour_ = *opt_hour; if (has_minute) - this->minute_ = *call.get_minute(); + this->minute_ = *opt_minute; if (has_second) - this->second_ = *call.get_second(); + this->second_ = *opt_second; this->publish_state(); } if (this->restore_value_) { datetime::DateTimeEntityRestoreState temp = {}; if (has_year) { - temp.year = *call.get_year(); + temp.year = *opt_year; } else { temp.year = this->year_; } if (has_month) { - temp.month = *call.get_month(); + temp.month = *opt_month; } else { temp.month = this->month_; } if (has_day) { - temp.day = *call.get_day(); + temp.day = *opt_day; } else { temp.day = this->day_; } if (has_hour) { - temp.hour = *call.get_hour(); + temp.hour = *opt_hour; } else { temp.hour = this->hour_; } if (has_minute) { - temp.minute = *call.get_minute(); + temp.minute = *opt_minute; } else { temp.minute = this->minute_; } if (has_second) { - temp.second = *call.get_second(); + temp.second = *opt_second; } else { temp.second = this->second_; } diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 9c816871168..b5efa62ae78 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -48,46 +48,49 @@ void TemplateTime::update() { } void TemplateTime::control(const datetime::TimeCall &call) { - bool has_hour = call.get_hour().has_value(); - bool has_minute = call.get_minute().has_value(); - bool has_second = call.get_second().has_value(); + auto opt_hour = call.get_hour(); + auto opt_minute = call.get_minute(); + auto opt_second = call.get_second(); + bool has_hour = opt_hour.has_value(); + bool has_minute = opt_minute.has_value(); + bool has_second = opt_second.has_value(); ESPTime value = {}; if (has_hour) - value.hour = *call.get_hour(); + value.hour = *opt_hour; if (has_minute) - value.minute = *call.get_minute(); + value.minute = *opt_minute; if (has_second) - value.second = *call.get_second(); + value.second = *opt_second; this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_hour) - this->hour_ = *call.get_hour(); + this->hour_ = *opt_hour; if (has_minute) - this->minute_ = *call.get_minute(); + this->minute_ = *opt_minute; if (has_second) - this->second_ = *call.get_second(); + this->second_ = *opt_second; this->publish_state(); } if (this->restore_value_) { datetime::TimeEntityRestoreState temp = {}; if (has_hour) { - temp.hour = *call.get_hour(); + temp.hour = *opt_hour; } else { temp.hour = this->hour_; } if (has_minute) { - temp.minute = *call.get_minute(); + temp.minute = *opt_minute; } else { temp.minute = this->minute_; } if (has_second) { - temp.second = *call.get_second(); + temp.second = *opt_second; } else { temp.second = this->second_; } diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index cd267bd552c..46a5cba9bb3 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -20,14 +20,18 @@ void TemplateFan::setup() { void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } void TemplateFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value() && (this->speed_count_ > 0)) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value() && this->has_oscillating_) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value() && this->has_direction_) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value() && (this->speed_count_ > 0)) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value() && this->has_oscillating_) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value() && this->has_direction_) + this->direction = *call_direction; this->apply_preset_mode_(call); this->publish_state(); diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index dbc4501ce71..6e73623ae9b 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -25,7 +25,10 @@ void TemplateLock::control(const lock::LockCall &call) { this->prev_trigger_->stop_action(); } - auto state = *call.get_state(); + auto opt_state = call.get_state(); + if (!opt_state.has_value()) + return; + auto state = *opt_state; if (state == LOCK_STATE_LOCKED) { this->prev_trigger_ = &this->lock_trigger_; this->lock_trigger_.trigger(); diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 2817e1a1327..3ebeec12856 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -77,8 +77,9 @@ void TemplateValve::control(const ValveCall &call) { this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->stop_prev_trigger_(); if (pos == VALVE_OPEN) { diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 57c76286a0d..73081d204b4 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -101,9 +101,10 @@ water_heater::WaterHeaterCallInternal TemplateWaterHeater::make_call() { } void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { - if (call.get_mode().has_value()) { + auto mode_val = call.get_mode(); + if (mode_val.has_value()) { if (this->optimistic_) { - this->mode_ = *call.get_mode(); + this->mode_ = *mode_val; } } if (!std::isnan(call.get_target_temperature())) { @@ -112,14 +113,16 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { } } - if (call.get_away().has_value()) { + auto away_val = call.get_away(); + if (away_val.has_value()) { if (this->optimistic_) { - this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away()); + this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away_val); } } - if (call.get_on().has_value()) { + auto on_val = call.get_on(); + if (on_val.has_value()) { if (this->optimistic_) { - this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on()); + this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *on_val); } } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index c6664197010..d52a22f880d 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -84,7 +84,7 @@ void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode, false); this->switch_to_action_(this->compute_action_(), false); this->switch_to_supplemental_action_(this->compute_supplemental_action_()); - this->switch_to_fan_mode_(this->fan_mode.value(), false); + this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON), false); this->switch_to_swing_mode_(this->swing_mode, false); this->switch_to_humidity_control_action_(this->compute_humidity_control_action_()); this->check_humidity_change_trigger_(); @@ -211,12 +211,13 @@ void ThermostatClimate::validate_target_humidity() { void ThermostatClimate::control(const climate::ClimateCall &call) { bool target_temperature_high_changed = false; - if (call.get_preset().has_value()) { + auto preset = call.get_preset(); + if (preset.has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_preset_(call.get_preset().value()); + this->change_preset_(*preset); } else { - this->preset = call.get_preset().value(); + this->preset = preset; } } if (call.has_custom_preset()) { @@ -229,34 +230,41 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { } } - if (call.get_mode().has_value()) { - this->mode = call.get_mode().value(); + auto mode = call.get_mode(); + if (mode.has_value()) { + this->mode = *mode; } - if (call.get_fan_mode().has_value()) { - this->fan_mode = call.get_fan_mode().value(); + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) { + this->fan_mode = fan_mode; } - if (call.get_swing_mode().has_value()) { - this->swing_mode = call.get_swing_mode().value(); + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) { + this->swing_mode = *swing_mode; } if (this->supports_two_points_) { - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = call.get_target_temperature_low().value(); + auto target_temp_low = call.get_target_temperature_low(); + if (target_temp_low.has_value()) { + this->target_temperature_low = *target_temp_low; } - if (call.get_target_temperature_high().has_value()) { - target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value(); - this->target_temperature_high = call.get_target_temperature_high().value(); + auto target_temp_high = call.get_target_temperature_high(); + if (target_temp_high.has_value()) { + target_temperature_high_changed = this->target_temperature_high != *target_temp_high; + this->target_temperature_high = *target_temp_high; } // ensure the two set points are valid and adjust one of them if necessary this->validate_target_temperatures(target_temperature_high_changed || (this->prev_mode_ == climate::CLIMATE_MODE_COOL)); } else { - if (call.get_target_temperature().has_value()) { - this->target_temperature = call.get_target_temperature().value(); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + this->target_temperature = *target_temp; this->validate_target_temperature(); } } - if (call.get_target_humidity().has_value()) { - this->target_humidity = call.get_target_humidity().value(); + auto target_humidity = call.get_target_humidity(); + if (target_humidity.has_value()) { + this->target_humidity = *target_humidity; this->validate_target_humidity(); } // make any changes happen @@ -1264,9 +1272,9 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem something_changed = true; } - if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) { + if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_)) { ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); - this->fan_mode = *config.fan_mode_; + this->fan_mode = config.fan_mode_; something_changed = true; } diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index f6a3048bd48..c83829ff592 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -79,8 +79,9 @@ void TimeBasedCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; if (pos == this->position) { // already at target if (this->manual_control_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index be412d62a84..f567be0674f 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -66,8 +66,9 @@ void Tormatic::control(const cover::CoverCall &call) { return; } - if (call.get_position().has_value()) { - auto pos = call.get_position().value(); + auto pos_val = call.get_position(); + if (pos_val.has_value()) { + auto pos = *pos_val; this->control_position_(pos); return; } diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 7b5e78af520..e0c150537a9 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -502,7 +502,7 @@ void ToshibaClimate::transmit_generic_() { } uint8_t fan; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: fan = TOSHIBA_FAN_SPEED_QUIET; break; @@ -567,7 +567,7 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() { message[2] = RAC_PT1411HWRU_NO_FAN.code1; message[7] = RAC_PT1411HWRU_NO_FAN.code2; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: message[2] = RAC_PT1411HWRU_FAN_LOW.code1; message[7] = RAC_PT1411HWRU_FAN_LOW.code2; @@ -811,12 +811,12 @@ void ToshibaClimate::transmit_ras_2819t_() { uint8_t temp_code = get_ras_2819t_temp_code(temperature); // Get fan speed encoding for rc_code_1 - climate::ClimateFanMode effective_fan_mode = this->fan_mode.value(); + climate::ClimateFanMode effective_fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_ON); // Dry mode only supports AUTO fan speed if (this->mode == climate::CLIMATE_MODE_DRY) { effective_fan_mode = climate::CLIMATE_FAN_AUTO; - if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) { + if (this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != climate::CLIMATE_FAN_AUTO) { ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO"); } } diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 4d8fd4b310a..6602ccd8c9a 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -7,8 +7,9 @@ namespace tuya { static const char *const TAG = "tuya.climate"; void TuyaClimate::setup() { - if (this->switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + this->parent_->register_listener(*switch_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); this->mode = climate::CLIMATE_MODE_OFF; if (datapoint.value_bool) { @@ -32,16 +33,18 @@ void TuyaClimate::setup() { this->cooling_state_pin_->setup(); this->cooling_state_ = this->cooling_state_pin_->digital_read(); } - if (this->active_state_id_.has_value()) { - this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) { + auto active_state_id = this->active_state_id_; + if (active_state_id.has_value()) { + this->parent_->register_listener(*active_state_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum); this->active_state_ = datapoint.value_enum; this->compute_state_(); this->publish_state(); }); } - if (this->target_temperature_id_.has_value()) { - this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { + auto target_temp_id = this->target_temperature_id_; + if (target_temp_id.has_value()) { + this->parent_->register_listener(*target_temp_id, [this](const TuyaDatapoint &datapoint) { this->manual_temperature_ = datapoint.value_int * this->target_temperature_multiplier_; if (this->reports_fahrenheit_) { this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9; @@ -53,8 +56,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->current_temperature_id_.has_value()) { - this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) { + auto current_temp_id = this->current_temperature_id_; + if (current_temp_id.has_value()) { + this->parent_->register_listener(*current_temp_id, [this](const TuyaDatapoint &datapoint) { this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_; if (this->reports_fahrenheit_) { this->current_temperature = (this->current_temperature - 32) * 5 / 9; @@ -65,8 +69,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->eco_id_.has_value()) { - this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { + auto eco_id = this->eco_id_; + if (eco_id.has_value()) { + this->parent_->register_listener(*eco_id, [this](const TuyaDatapoint &datapoint) { // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases this->eco_ = datapoint.value_bool; this->eco_type_ = datapoint.type; @@ -76,8 +81,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->sleep_id_.has_value()) { - this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) { + auto sleep_id = this->sleep_id_; + if (sleep_id.has_value()) { + this->parent_->register_listener(*sleep_id, [this](const TuyaDatapoint &datapoint) { this->sleep_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_)); this->compute_preset_(); @@ -85,8 +91,9 @@ void TuyaClimate::setup() { this->publish_state(); }); } - if (this->swing_vertical_id_.has_value()) { - this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) { + auto swing_vert_id = this->swing_vertical_id_; + if (swing_vert_id.has_value()) { + this->parent_->register_listener(*swing_vert_id, [this](const TuyaDatapoint &datapoint) { this->swing_vertical_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool)); this->compute_swingmode_(); @@ -94,8 +101,9 @@ void TuyaClimate::setup() { }); } - if (this->swing_horizontal_id_.has_value()) { - this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) { + auto swing_horiz_id = this->swing_horizontal_id_; + if (swing_horiz_id.has_value()) { + this->parent_->register_listener(*swing_horiz_id, [this](const TuyaDatapoint &datapoint) { this->swing_horizontal_ = datapoint.value_bool; ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool)); this->compute_swingmode_(); @@ -103,8 +111,9 @@ void TuyaClimate::setup() { }); } - if (this->fan_speed_id_.has_value()) { - this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) { + auto fan_speed_id = this->fan_speed_id_; + if (fan_speed_id.has_value()) { + this->parent_->register_listener(*fan_speed_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum); this->fan_state_ = datapoint.value_enum; this->compute_fanmode_(); @@ -139,21 +148,34 @@ void TuyaClimate::loop() { } void TuyaClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; + auto mode = call.get_mode(); + if (mode.has_value()) { + const bool switch_state = *mode != climate::CLIMATE_MODE_OFF; ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); - this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); - const climate::ClimateMode new_mode = *call.get_mode(); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + this->parent_->set_boolean_datapoint_value(*switch_dp_id, switch_state); + } + const climate::ClimateMode new_mode = *mode; - if (this->active_state_id_.has_value()) { + auto active_state_dp_id = this->active_state_id_; + if (active_state_dp_id.has_value()) { if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); + auto heating_val = this->active_state_heating_value_; + if (heating_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *heating_val); } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); - } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); - } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); + auto cooling_val = this->active_state_cooling_value_; + if (cooling_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *cooling_val); + } else if (new_mode == climate::CLIMATE_MODE_DRY) { + auto drying_val = this->active_state_drying_value_; + if (drying_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *drying_val); + } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY) { + auto fanonly_val = this->active_state_fanonly_value_; + if (fanonly_val.has_value()) + this->parent_->set_enum_datapoint_value(*active_state_dp_id, *fanonly_val); } } else { ESP_LOGW(TAG, "Active state (mode) datapoint not configured"); @@ -163,31 +185,38 @@ void TuyaClimate::control(const climate::ClimateCall &call) { control_swing_mode_(call); control_fan_mode_(call); - if (call.get_target_temperature().has_value()) { - float target_temperature = *call.get_target_temperature(); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + float target_temperature = *target_temp; if (this->reports_fahrenheit_) target_temperature = (target_temperature * 9 / 5) + 32; ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature); - this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, - (int) (target_temperature / this->target_temperature_multiplier_)); + auto target_temp_dp_id = this->target_temperature_id_; + if (target_temp_dp_id.has_value()) { + this->parent_->set_integer_datapoint_value(*target_temp_dp_id, + (int) (target_temperature / this->target_temperature_multiplier_)); + } } - if (call.get_preset().has_value()) { - const climate::ClimatePreset preset = *call.get_preset(); - if (this->eco_id_.has_value()) { + auto preset_val = call.get_preset(); + if (preset_val.has_value()) { + const climate::ClimatePreset preset = *preset_val; + auto eco_dp_id = this->eco_id_; + if (eco_dp_id.has_value()) { const bool eco = preset == climate::CLIMATE_PRESET_ECO; ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); if (this->eco_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->eco_id_, eco); + this->parent_->set_enum_datapoint_value(*eco_dp_id, eco); } else { - this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); + this->parent_->set_boolean_datapoint_value(*eco_dp_id, eco); } } - if (this->sleep_id_.has_value()) { + auto sleep_dp_id = this->sleep_id_; + if (sleep_dp_id.has_value()) { const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep)); - this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep); + this->parent_->set_boolean_datapoint_value(*sleep_dp_id, sleep); } } } @@ -196,8 +225,9 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { bool vertical_swing_changed = false; bool horizontal_swing_changed = false; - if (call.get_swing_mode().has_value()) { - const auto swing_mode = *call.get_swing_mode(); + auto swing_mode_val = call.get_swing_mode(); + if (swing_mode_val.has_value()) { + const auto swing_mode = *swing_mode_val; switch (swing_mode) { case climate::CLIMATE_SWING_OFF: @@ -241,14 +271,16 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { } } - if (vertical_swing_changed && this->swing_vertical_id_.has_value()) { + auto vert_dp_id = this->swing_vertical_id_; + if (vertical_swing_changed && vert_dp_id.has_value()) { ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_)); - this->parent_->set_boolean_datapoint_value(*this->swing_vertical_id_, swing_vertical_); + this->parent_->set_boolean_datapoint_value(*vert_dp_id, swing_vertical_); } - if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) { + auto horiz_dp_id = this->swing_horizontal_id_; + if (horizontal_swing_changed && horiz_dp_id.has_value()) { ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_)); - this->parent_->set_boolean_datapoint_value(*this->swing_horizontal_id_, swing_horizontal_); + this->parent_->set_boolean_datapoint_value(*horiz_dp_id, swing_horizontal_); } // Publish the state after updating the swing mode @@ -256,33 +288,35 @@ void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { } void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { - if (call.get_fan_mode().has_value()) { - climate::ClimateFanMode fan_mode = *call.get_fan_mode(); + auto fan_mode_val = call.get_fan_mode(); + if (fan_mode_val.has_value()) { + climate::ClimateFanMode fan_mode = *fan_mode_val; uint8_t tuya_fan_speed; switch (fan_mode) { case climate::CLIMATE_FAN_LOW: - tuya_fan_speed = *fan_speed_low_value_; + tuya_fan_speed = this->fan_speed_low_value_.value_or(0); break; case climate::CLIMATE_FAN_MEDIUM: - tuya_fan_speed = *fan_speed_medium_value_; + tuya_fan_speed = this->fan_speed_medium_value_.value_or(0); break; case climate::CLIMATE_FAN_MIDDLE: - tuya_fan_speed = *fan_speed_middle_value_; + tuya_fan_speed = this->fan_speed_middle_value_.value_or(0); break; case climate::CLIMATE_FAN_HIGH: - tuya_fan_speed = *fan_speed_high_value_; + tuya_fan_speed = this->fan_speed_high_value_.value_or(0); break; case climate::CLIMATE_FAN_AUTO: - tuya_fan_speed = *fan_speed_auto_value_; + tuya_fan_speed = this->fan_speed_auto_value_.value_or(0); break; default: tuya_fan_speed = 0; break; } - if (this->fan_speed_id_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed); + auto fan_speed_dp_id = this->fan_speed_id_; + if (fan_speed_dp_id.has_value()) { + this->parent_->set_enum_datapoint_value(*fan_speed_dp_id, tuya_fan_speed); } } } @@ -337,31 +371,39 @@ climate::ClimateTraits TuyaClimate::traits() { void TuyaClimate::dump_config() { LOG_CLIMATE("", "Tuya Climate", this); - if (this->switch_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id); } - if (this->active_state_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_); + auto active_state_dp_id = this->active_state_id_; + if (active_state_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *active_state_dp_id); } - if (this->target_temperature_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + auto target_temp_dp_id = this->target_temperature_id_; + if (target_temp_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *target_temp_dp_id); } - if (this->current_temperature_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + auto current_temp_dp_id = this->current_temperature_id_; + if (current_temp_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *current_temp_dp_id); } LOG_PIN(" Heating State Pin: ", this->heating_state_pin_); LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_); - if (this->eco_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); + auto eco_dp_id = this->eco_id_; + if (eco_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *eco_dp_id); } - if (this->sleep_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *this->sleep_id_); + auto sleep_dp_id = this->sleep_id_; + if (sleep_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *sleep_dp_id); } - if (this->swing_vertical_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *this->swing_vertical_id_); + auto swing_vert_dp_id = this->swing_vertical_id_; + if (swing_vert_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *swing_vert_dp_id); } - if (this->swing_horizontal_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_); + auto swing_horiz_dp_id = this->swing_horizontal_id_; + if (swing_horiz_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *swing_horiz_dp_id); } } diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index 14bf937cf72..125afec0483 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -39,6 +39,9 @@ void TuyaCover::setup() { } }); + if (!this->position_id_.has_value()) { + return; + } uint8_t report_id = *this->position_id_; if (this->position_report_id_.has_value()) { // A position report datapoint is configured; listen to that instead. @@ -60,29 +63,30 @@ void TuyaCover::control(const cover::CoverCall &call) { if (call.get_stop()) { if (this->control_id_.has_value()) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_STOP); - } else { + } else if (this->position_id_.has_value()) { auto pos = this->position; pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); + this->parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto pos_opt = call.get_position(); + if (pos_opt.has_value()) { + auto pos = *pos_opt; if (this->control_id_.has_value() && (pos == COVER_OPEN || pos == COVER_CLOSED)) { if (pos == COVER_OPEN) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_OPEN); } else { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_CLOSE); } - } else { + } else if (this->position_id_.has_value()) { pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); + this->parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 9b132e0de64..a387606b776 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -7,8 +7,9 @@ namespace tuya { static const char *const TAG = "tuya.fan"; void TuyaFan::setup() { - if (this->speed_id_.has_value()) { - this->parent_->register_listener(*this->speed_id_, [this](const TuyaDatapoint &datapoint) { + auto speed_id = this->speed_id_; + if (speed_id.has_value()) { + this->parent_->register_listener(*speed_id, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum); if (datapoint.value_enum >= this->speed_count_) { @@ -25,15 +26,17 @@ void TuyaFan::setup() { this->speed_type_ = datapoint.type; }); } - if (this->switch_id_.has_value()) { - this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + this->parent_->register_listener(*switch_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); this->state = datapoint.value_bool; this->publish_state(); }); } - if (this->oscillation_id_.has_value()) { - this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) { + auto oscillation_id = this->oscillation_id_; + if (oscillation_id.has_value()) { + this->parent_->register_listener(*oscillation_id, [this](const TuyaDatapoint &datapoint) { // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both // scenarios ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); @@ -43,8 +46,9 @@ void TuyaFan::setup() { this->oscillation_type_ = datapoint.type; }); } - if (this->direction_id_.has_value()) { - this->parent_->register_listener(*this->direction_id_, [this](const TuyaDatapoint &datapoint) { + auto direction_id = this->direction_id_; + if (direction_id.has_value()) { + this->parent_->register_listener(*direction_id, [this](const TuyaDatapoint &datapoint) { ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool)); this->direction = datapoint.value_bool ? fan::FanDirection::REVERSE : fan::FanDirection::FORWARD; this->publish_state(); @@ -60,17 +64,21 @@ void TuyaFan::setup() { void TuyaFan::dump_config() { LOG_FAN("", "Tuya Fan", this); - if (this->speed_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); + auto speed_dp_id = this->speed_id_; + if (speed_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *speed_dp_id); } - if (this->switch_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + auto switch_dp_id = this->switch_id_; + if (switch_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id); } - if (this->oscillation_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); + auto oscillation_dp_id = this->oscillation_id_; + if (oscillation_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *oscillation_dp_id); } - if (this->direction_id_.has_value()) { - ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); + auto direction_dp_id = this->direction_id_; + if (direction_dp_id.has_value()) { + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *direction_dp_id); } } @@ -80,25 +88,41 @@ fan::FanTraits TuyaFan::get_traits() { } void TuyaFan::control(const fan::FanCall &call) { - if (this->switch_id_.has_value() && call.get_state().has_value()) { - this->parent_->set_boolean_datapoint_value(*this->switch_id_, *call.get_state()); - } - if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) { - if (this->oscillation_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); - } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) { - this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating()); + auto switch_id = this->switch_id_; + if (switch_id.has_value()) { + auto state = call.get_state(); + if (state.has_value()) { + this->parent_->set_boolean_datapoint_value(*switch_id, *state); } } - if (this->direction_id_.has_value() && call.get_direction().has_value()) { - bool enable = *call.get_direction() == fan::FanDirection::REVERSE; - this->parent_->set_enum_datapoint_value(*this->direction_id_, enable); + auto osc_id = this->oscillation_id_; + if (osc_id.has_value()) { + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) { + if (this->oscillation_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*osc_id, *oscillating); + } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) { + this->parent_->set_boolean_datapoint_value(*osc_id, *oscillating); + } + } } - if (this->speed_id_.has_value() && call.get_speed().has_value()) { - if (this->speed_type_ == TuyaDatapointType::ENUM) { - this->parent_->set_enum_datapoint_value(*this->speed_id_, *call.get_speed() - 1); - } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { - this->parent_->set_integer_datapoint_value(*this->speed_id_, *call.get_speed()); + auto dir_id = this->direction_id_; + if (dir_id.has_value()) { + auto direction = call.get_direction(); + if (direction.has_value()) { + bool enable = *direction == fan::FanDirection::REVERSE; + this->parent_->set_enum_datapoint_value(*dir_id, enable); + } + } + auto spd_id = this->speed_id_; + if (spd_id.has_value()) { + auto speed = call.get_speed(); + if (speed.has_value()) { + if (this->speed_type_ == TuyaDatapointType::ENUM) { + this->parent_->set_enum_datapoint_value(*spd_id, *speed - 1); + } else if (this->speed_type_ == TuyaDatapointType::INTEGER) { + this->parent_->set_integer_datapoint_value(*spd_id, *speed); + } } } } diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 097b3c1af82..620bb88d0b7 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -57,6 +57,9 @@ void TuyaLight::setup() { return; } + if (!this->color_type_.has_value()) + return; + float red, green, blue; switch (*this->color_type_) { case TuyaColorType::RGBHSV: @@ -185,7 +188,7 @@ void TuyaLight::write_state(light::LightState *state) { } } - if (this->color_id_.has_value() && (brightness == 0.0f || !color_interlock_)) { + if (this->color_id_.has_value() && this->color_type_.has_value() && (brightness == 0.0f || !color_interlock_)) { std::string color_value; switch (*this->color_type_) { case TuyaColorType::RGB: { diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 4256b01c4e8..3eae4d2d966 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -42,8 +42,9 @@ climate::ClimateTraits UponorSmatrixClimate::traits() { } void UponorSmatrixClimate::control(const climate::ClimateCall &call) { - if (call.get_target_temperature().has_value()) { - uint16_t temp = celsius_to_raw(*call.get_target_temperature()); + auto val = call.get_target_temperature(); + if (val.has_value()) { + uint16_t temp = celsius_to_raw(*val); if (this->preset == climate::CLIMATE_PRESET_ECO) { // During ECO mode, the thermostat automatically substracts the setback value from the setpoint, // so we need to add it here first diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 6fe735362dc..e9f602e97f4 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -82,7 +82,7 @@ void WhirlpoolClimate::transmit_state() { remote_state[3] |= (uint8_t) (temp - this->temperature_min_()) << 4; // Fan speed - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state[2] |= WHIRLPOOL_FAN_HIGH; break; diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp index 9f57fdb8430..003d2e0ba65 100644 --- a/esphome/components/whynter/whynter.cpp +++ b/esphome/components/whynter/whynter.cpp @@ -69,7 +69,7 @@ void Whynter::transmit_state() { } mode_before_ = this->mode; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: remote_state |= FAN_LOW; break; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7d5d0133c1e..852ff922f1f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1094,8 +1094,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { - EAPAuth eap_config = ap.get_eap().value(); + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { + EAPAuth eap_config = *eap_opt; // clang-format off ESP_LOGV( TAG, @@ -1129,8 +1130,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGV(TAG, " Channel not set"); } #ifdef USE_WIFI_MANUAL_IP - if (ap.get_manual_ip().has_value()) { - ManualIP m = *ap.get_manual_ip(); + auto manual_ip = ap.get_manual_ip(); + if (manual_ip.has_value()) { + ManualIP m = *manual_ip; char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bd6a18a99b2..02ce59502b7 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -298,9 +298,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); + EAPAuth eap = *eap_opt; ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 734d1862052..bf432cea6e5 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -403,9 +403,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - if (ap.get_eap().has_value()) { + auto eap_opt = ap.get_eap(); + if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); + EAPAuth eap = *eap_opt; #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); #else diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index bf91420620e..4a64e6c41c6 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -120,10 +120,12 @@ void YashimaClimate::setup() { } void YashimaClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); + auto call_mode = call.get_mode(); + if (call_mode.has_value()) + this->mode = *call_mode; + auto call_target = call.get_target_temperature(); + if (call_target.has_value()) + this->target_temperature = *call_target; this->transmit_state_(); this->publish_state(); diff --git a/esphome/components/zhlt01/zhlt01.cpp b/esphome/components/zhlt01/zhlt01.cpp index 36d1737c14c..e5ab5915e4b 100644 --- a/esphome/components/zhlt01/zhlt01.cpp +++ b/esphome/components/zhlt01/zhlt01.cpp @@ -13,7 +13,7 @@ void ZHLT01Climate::transmit_state() { ir_message[1] = 0x00; // Timer off // Byte 3 : Turbo mode - if (this->preset.value() == climate::CLIMATE_PRESET_BOOST) { + if (this->preset.value_or(climate::CLIMATE_PRESET_NONE) == climate::CLIMATE_PRESET_BOOST) { ir_message[3] = AC1_FAN_TURBO; } @@ -47,7 +47,7 @@ void ZHLT01Climate::transmit_state() { } // -- Fan - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_BOOST: ir_message[7] |= AC1_FAN3; break; @@ -55,7 +55,7 @@ void ZHLT01Climate::transmit_state() { ir_message[7] |= AC1_FAN_SILENT; break; default: - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: ir_message[7] |= AC1_FAN1; break; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 042eebb40f3..54d4ae311f2 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -248,7 +248,7 @@ void log_entity_unit_of_measurement(const char *tag, const char *prefix, const E template class StatefulEntityBase : public EntityBase { public: virtual bool has_state() const { return this->state_.has_value(); } - virtual const T &get_state() const { return this->state_.value(); } + virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access) virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } void invalidate_state() { this->set_new_state({}); } diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 7f9db7817d6..88a02aa8b25 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -1,220 +1,12 @@ #pragma once -// -// Copyright (c) 2017 Martin Moene -// -// https://github.com/martinmoene/optional-bare -// -// This code is licensed under the MIT License (MIT). -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// -// Modified by Otto Winter on 18.05.18 -#include +#include namespace esphome { -// type for nullopt - -struct nullopt_t { // NOLINT - struct init {}; // NOLINT - nullopt_t(init /*unused*/) {} -}; - -// extra parenthesis to prevent the most vexing parse: - -const nullopt_t nullopt((nullopt_t::init())); // NOLINT - -// Simplistic optional: requires T to be default constructible, copyable. - -template class optional { // NOLINT - private: - using safe_bool = void (optional::*)() const; - - public: - using value_type = T; - - optional() {} - - optional(nullopt_t /*unused*/) {} - - optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT - - template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} - - optional &operator=(nullopt_t /*unused*/) { - reset(); - return *this; - } - bool operator==(optional const &rhs) const { - if (has_value() && rhs.has_value()) - return value() == rhs.value(); - return !has_value() && !rhs.has_value(); - } - - template optional &operator=(optional const &other) { - has_value_ = other.has_value(); - value_ = other.value(); - return *this; - } - - void swap(optional &rhs) noexcept { - using std::swap; - if (has_value() && rhs.has_value()) { - swap(**this, *rhs); - } else if (!has_value() && rhs.has_value()) { - initialize(*rhs); - rhs.reset(); - } else if (has_value() && !rhs.has_value()) { - rhs.initialize(**this); - reset(); - } - } - - // observers - - value_type const *operator->() const { return &value_; } - - value_type *operator->() { return &value_; } - - value_type const &operator*() const { return value_; } - - value_type &operator*() { return value_; } - - operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; } - - bool has_value() const { return has_value_; } - - value_type const &value() const { return value_; } - - value_type &value() { return value_; } - - template value_type value_or(U const &v) const { return has_value() ? value() : static_cast(v); } - - // modifiers - - void reset() { has_value_ = false; } - - private: - void this_type_does_not_support_comparisons() const {} // NOLINT - - template void initialize(V const &value) { // NOLINT - value_ = value; - has_value_ = true; - } - - bool has_value_{false}; // NOLINT - value_type value_; // NOLINT -}; - -// Relational operators - -template inline bool operator==(optional const &x, optional const &y) { - return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y; -} - -template inline bool operator!=(optional const &x, optional const &y) { - return !(x == y); -} - -template inline bool operator<(optional const &x, optional const &y) { - return (!y) ? false : (!x) ? true : *x < *y; -} - -template inline bool operator>(optional const &x, optional const &y) { return (y < x); } - -template inline bool operator<=(optional const &x, optional const &y) { return !(y < x); } - -template inline bool operator>=(optional const &x, optional const &y) { return !(x < y); } - -// Comparison with nullopt - -template inline bool operator==(optional const &x, nullopt_t /*unused*/) { return (!x); } - -template inline bool operator==(nullopt_t /*unused*/, optional const &x) { return (!x); } - -template inline bool operator!=(optional const &x, nullopt_t /*unused*/) { return bool(x); } - -template inline bool operator!=(nullopt_t /*unused*/, optional const &x) { return bool(x); } - -template inline bool operator<(optional const & /*unused*/, nullopt_t /*unused*/) { return false; } - -template inline bool operator<(nullopt_t /*unused*/, optional const &x) { return bool(x); } - -template inline bool operator<=(optional const &x, nullopt_t /*unused*/) { return (!x); } - -template inline bool operator<=(nullopt_t /*unused*/, optional const & /*unused*/) { return true; } - -template inline bool operator>(optional const &x, nullopt_t /*unused*/) { return bool(x); } - -template inline bool operator>(nullopt_t /*unused*/, optional const & /*unused*/) { return false; } - -template inline bool operator>=(optional const & /*unused*/, nullopt_t /*unused*/) { return true; } - -template inline bool operator>=(nullopt_t /*unused*/, optional const &x) { return (!x); } - -// Comparison with T - -template inline bool operator==(optional const &x, U const &v) { - return bool(x) ? *x == v : false; -} - -template inline bool operator==(U const &v, optional const &x) { - return bool(x) ? v == *x : false; -} - -template inline bool operator!=(optional const &x, U const &v) { - return bool(x) ? *x != v : true; -} - -template inline bool operator!=(U const &v, optional const &x) { - return bool(x) ? v != *x : true; -} - -template inline bool operator<(optional const &x, U const &v) { - return bool(x) ? *x < v : true; -} - -template inline bool operator<(U const &v, optional const &x) { - return bool(x) ? v < *x : false; -} - -template inline bool operator<=(optional const &x, U const &v) { - return bool(x) ? *x <= v : true; -} - -template inline bool operator<=(U const &v, optional const &x) { - return bool(x) ? v <= *x : false; -} - -template inline bool operator>(optional const &x, U const &v) { - return bool(x) ? *x > v : false; -} - -template inline bool operator>(U const &v, optional const &x) { - return bool(x) ? v > *x : true; -} - -template inline bool operator>=(optional const &x, U const &v) { - return bool(x) ? *x >= v : false; -} - -template inline bool operator>=(U const &v, optional const &x) { - return bool(x) ? v >= *x : true; -} - -// Specialized algorithms - -template void swap(optional &x, optional &y) noexcept { x.swap(y); } - -// Convenience function to create an optional. - -template inline optional make_optional(T const &v) { return optional(v); } +using std::make_optional; +using std::nullopt; +using std::nullopt_t; +using std::optional; } // namespace esphome diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 6d255bc0be4..8dd77de8434 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -31,9 +31,7 @@ Component = esphome_ns.class_("Component") ComponentPtr = Component.operator("ptr") PollingComponent = esphome_ns.class_("PollingComponent", Component) Application = esphome_ns.class_("Application") -# Create optional with explicit namespace to avoid ambiguity with std::optional -# The generated code will use esphome::optional instead of just optional -optional = global_ns.namespace("esphome").class_("optional") +optional = global_ns.namespace("std").class_("optional") arduino_json_ns = global_ns.namespace("ArduinoJson") JsonObject = arduino_json_ns.class_("JsonObject") JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 6b047bc62fb..16f5f980a5f 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -66,5 +66,20 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert "it_4->set_template([]() -> std::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp + + +def test_esphome_optional_alias_works(generate_main): + """ + Test that esphome::optional alias compiles (backward compatibility) + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + + # Then + # Codegen emits std::optional, but esphome::optional must also work + # via the using alias in esphome/core/optional.h + assert "std::optional" in main_cpp diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index e9ddfcf43e4..ed398b0abd9 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -28,9 +28,14 @@ esphome: # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- - id(template_sens).set_template([]() -> esphome::optional { + id(template_sens).set_template([]() -> std::optional { return 123.0f; }); + # Test that esphome::optional alias still works for backward compatibility + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 42.0f; + }); - datetime.date.set: id: test_date From 8911d9d28f1adb2414f67a00c0035295e554f388 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 3 Mar 2026 19:42:36 -0600 Subject: [PATCH 0366/2056] [media_source] Clarify threading contract (#14433) --- esphome/components/media_source/media_source.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/media_source/media_source.h b/esphome/components/media_source/media_source.h index 688c27134f2..f21ba486b87 100644 --- a/esphome/components/media_source/media_source.h +++ b/esphome/components/media_source/media_source.h @@ -67,11 +67,13 @@ class MediaSource { /// @brief Start playing the given URI /// Sources should validate the URI and state, returning false if the source is busy. /// The orchestrator is responsible for stopping active sources before starting a new one. + /// @note Must only be called from the main loop. /// @param uri URI to play; e.g., "http://stream_url" /// @return true if playback started successfully, false otherwise virtual bool play_uri(const std::string &uri) = 0; - /// @brief Handle playback commands (pause, stop, next, etc.) + /// @brief Handle playback commands; e.g., pause, stop, next, etc. + /// @note Must only be called from the main loop. /// @param command Command to execute virtual void handle_command(MediaSourceCommand command) = 0; @@ -81,7 +83,8 @@ class MediaSource { // === State Access === - /// @brief Get current playback state (must only be called from the main loop) + /// @brief Get current playback state + /// @note Must only be called from the main loop. /// @return Current state of this source MediaSourceState get_state() const { return this->state_; } @@ -136,9 +139,10 @@ class MediaSource { virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {} protected: - /// @brief Update state and notify listener (must only be called from the main loop) + /// @brief Update state and notify listener /// This is the only way to change state_, ensuring listener notifications always fire. /// Sources running FreeRTOS tasks should signal via event groups and call this from loop(). + /// @note Must only be called from the main loop. /// @param state New state to set void set_state_(MediaSourceState state) { if (this->state_ != state) { From cba34e770e8077f95305bdbbf5352def17d58dcf Mon Sep 17 00:00:00 2001 From: Tilman Vogel Date: Wed, 4 Mar 2026 03:18:36 +0100 Subject: [PATCH 0367/2056] [core] improve help text for --device option, mention `OTA` (#14445) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/__main__.py | 7 ++++--- esphome/const.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index ffedb90bde4..0164e2eeb33 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -23,6 +23,7 @@ import esphome.codegen as cg from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, + ARGUMENT_HELP_DEVICE, CONF_API, CONF_BAUD_RATE, CONF_BROKER, @@ -1367,7 +1368,7 @@ def parse_args(argv): parser_upload.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_upload.add_argument( "--upload_speed", @@ -1390,7 +1391,7 @@ def parse_args(argv): parser_logs.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_logs.add_argument( "--reset", @@ -1420,7 +1421,7 @@ def parse_args(argv): parser_run.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_run.add_argument( "--upload_speed", diff --git a/esphome/const.py b/esphome/const.py index d5625f6a549..060e9625739 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -11,6 +11,9 @@ VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) +# CLI Help Text Constants +ARGUMENT_HELP_DEVICE = "Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses. Use 'OTA' for resolving from MQTT, DNS or mDNS and avoiding the interactive prompt." + class Platform(StrEnum): """Platform identifiers for ESPHome.""" From 37146ff565aa239338948401b8493210af6ba5da Mon Sep 17 00:00:00 2001 From: JiriPrchal <163323169+JiriPrchal@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:00:09 +0100 Subject: [PATCH 0368/2056] [integration] Add set method to publish and save sensor value (#13316) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../integration/integration_sensor.h | 13 ++++++---- esphome/components/integration/sensor.py | 25 +++++++++++++++++-- .../components/integration/common-esp32.yaml | 10 ++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index f075d163fec..6c4ef7049bb 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -32,6 +32,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { void set_method(IntegrationMethod method) { method_ = method; } void set_restore(bool restore) { restore_ = restore; } void reset() { this->publish_and_save_(0.0f); } + void set_value(float value) { this->publish_and_save_(value); } protected: void process_sensor_value_(float value); @@ -71,14 +72,16 @@ class IntegrationSensor : public sensor::Sensor, public Component { float last_value_{0.0f}; }; -template class ResetAction : public Action { +template class ResetAction : public Action, public Parented { public: - explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(const Ts &...x) override { this->parent_->reset(); } +}; - protected: - IntegrationSensor *parent_; +template class SetValueAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, value) + + void play(const Ts &...x) override { this->parent_->set_value(this->value_.value(x...)); } }; } // namespace integration diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 3c04a338dde..26766385565 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_RESTORE, CONF_SENSOR, CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE, ) from esphome.core.entity_helpers import inherit_property_from @@ -17,6 +18,7 @@ IntegrationSensor = integration_ns.class_( "IntegrationSensor", sensor.Sensor, cg.Component ) ResetAction = integration_ns.class_("ResetAction", automation.Action) +SetValueAction = integration_ns.class_("SetValueAction", automation.Action) IntegrationSensorTime = integration_ns.enum("IntegrationSensorTime") INTEGRATION_TIMES = { @@ -111,5 +113,24 @@ async def to_code(config): ), ) async def sensor_integration_reset_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "sensor.integration.set_value", + SetValueAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(IntegrationSensor), + cv.Required(CONF_VALUE): cv.templatable(cv.float_), + } + ), +) +async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_value(template_)) + return var diff --git a/tests/components/integration/common-esp32.yaml b/tests/components/integration/common-esp32.yaml index 248106fd608..26550d3c5c9 100644 --- a/tests/components/integration/common-esp32.yaml +++ b/tests/components/integration/common-esp32.yaml @@ -1,9 +1,19 @@ +esphome: + on_boot: + then: + - sensor.integration.reset: + id: integration_sensor + - sensor.integration.set_value: + id: integration_sensor + value: 100.0 + sensor: - platform: adc id: my_sensor pin: ${pin} attenuation: 12db - platform: integration + id: integration_sensor sensor: my_sensor name: Integration Sensor time_unit: s From 065773ed4c3e84243e483409aefba0403c92f9a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:17:28 -1000 Subject: [PATCH 0369/2056] [runtime_stats] Use micros() for accurate per-component timing (#14452) --- .../runtime_stats/runtime_stats.cpp | 22 ++++---- .../components/runtime_stats/runtime_stats.h | 56 +++++++++---------- esphome/core/component.cpp | 7 ++- esphome/core/component.h | 16 +++++- 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 410695da040..d9fa22d9495 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -13,12 +13,12 @@ RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_ global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us, uint32_t current_time) { if (component == nullptr) return; // Record stats using component pointer as key - this->component_stats_[component].record_time(duration_ms); + this->component_stats_[component].record_time(duration_us); if (this->next_log_time_ == 0) { this->next_log_time_ = current_time + this->log_interval_; @@ -58,15 +58,16 @@ void RuntimeStatsCollector::log_stats_() { // Sort by period runtime (descending) std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms(); + return this->component_stats_[a].get_period_time_us() > this->component_stats_[b].get_period_time_us(); }); // Log top components by period runtime for (size_t i = 0; i < count; i++) { const auto &stats = this->component_stats_[sorted[i]]; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(), - stats.get_period_max_time_ms(), stats.get_period_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), + stats.get_period_avg_time_us() / 1000.0f, stats.get_period_max_time_us() / 1000.0f, + stats.get_period_time_us() / 1000.0f); } // Log total stats since boot (only for active components - idle ones haven't changed) @@ -74,14 +75,15 @@ void RuntimeStatsCollector::log_stats_() { // Re-sort by total runtime for all-time stats std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms(); + return this->component_stats_[a].get_total_time_us() > this->component_stats_[b].get_total_time_us(); }); for (size_t i = 0; i < count; i++) { const auto &stats = this->component_stats_[sorted[i]]; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(), - stats.get_total_max_time_ms(), stats.get_total_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), + stats.get_total_avg_time_us() / 1000.0f, stats.get_total_max_time_us() / 1000.0f, + stats.get_total_time_us() / 1000.0); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index c7fea7474b5..08475297208 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -22,58 +22,58 @@ class ComponentRuntimeStats { public: ComponentRuntimeStats() : period_count_(0), - period_time_ms_(0), - period_max_time_ms_(0), + period_time_us_(0), + period_max_time_us_(0), total_count_(0), - total_time_ms_(0), - total_max_time_ms_(0) {} + total_time_us_(0), + total_max_time_us_(0) {} - void record_time(uint32_t duration_ms) { + void record_time(uint32_t duration_us) { // 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; + this->period_time_us_ += duration_us; + if (duration_us > this->period_max_time_us_) + this->period_max_time_us_ = duration_us; - // Update total counters + // Update total counters (uint64_t to avoid overflow — uint32_t would overflow after ~10 hours) this->total_count_++; - this->total_time_ms_ += duration_ms; - if (duration_ms > this->total_max_time_ms_) - this->total_max_time_ms_ = duration_ms; + this->total_time_us_ += duration_us; + if (duration_us > this->total_max_time_us_) + this->total_max_time_us_ = duration_us; } void reset_period_stats() { this->period_count_ = 0; - this->period_time_ms_ = 0; - this->period_max_time_ms_ = 0; + this->period_time_us_ = 0; + this->period_max_time_us_ = 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; + uint32_t get_period_time_us() const { return this->period_time_us_; } + uint32_t get_period_max_time_us() const { return this->period_max_time_us_; } + float get_period_avg_time_us() const { + return this->period_count_ > 0 ? this->period_time_us_ / static_cast(this->period_count_) : 0.0f; } - // Total stats (persistent until reboot) + // Total stats (persistent until reboot, uint64_t to avoid overflow) 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; + uint64_t get_total_time_us() const { return this->total_time_us_; } + uint32_t get_total_max_time_us() const { return this->total_max_time_us_; } + float get_total_avg_time_us() const { + return this->total_count_ > 0 ? this->total_time_us_ / 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_; + uint32_t period_time_us_; + uint32_t period_max_time_us_; // Total stats (persistent until reboot) uint32_t total_count_; - uint32_t total_time_ms_; - uint32_t total_max_time_ms_; + uint64_t total_time_us_; + uint32_t total_max_time_us_; }; class RuntimeStatsCollector { @@ -83,7 +83,7 @@ class 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); + void record_component_time(Component *component, uint32_t duration_us, uint32_t current_time); // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 4ccc7478191..8c2c8d38e8a 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -529,9 +529,12 @@ 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 + // Use micros() for accurate sub-millisecond timing. millis() has insufficient + // resolution — most components complete in microseconds but millis() only has + // 1ms granularity, so results were essentially random noise. if (global_runtime_stats != nullptr) { - global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); + uint32_t duration_us = micros() - this->started_us_; + global_runtime_stats->record_component_time(this->component_, duration_us, curr_time); } #endif if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { diff --git a/esphome/core/component.h b/esphome/core/component.h index e5127b0c9f2..59222dc4f47 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -563,10 +563,21 @@ class PollingComponent : public Component { uint32_t update_interval_; }; +#ifdef USE_RUNTIME_STATS +uint32_t micros(); // Forward declare for inline constructor +#endif + class WarnIfComponentBlockingGuard { public: WarnIfComponentBlockingGuard(Component *component, uint32_t start_time) - : started_(start_time), component_(component) {} + : started_(start_time), + component_(component) +#ifdef USE_RUNTIME_STATS + , + started_us_(micros()) +#endif + { + } // Finish the timing operation and return the current time uint32_t finish(); @@ -576,6 +587,9 @@ class WarnIfComponentBlockingGuard { protected: uint32_t started_; Component *component_; +#ifdef USE_RUNTIME_STATS + uint32_t started_us_; +#endif }; // Function to clear setup priority overrides after all components are set up From ac19d05db26a891b6dd0d4301b8e8321f9c48fe0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:17:41 -1000 Subject: [PATCH 0370/2056] [core] Call loop() directly in main loop, bypass call() indirection (#14451) --- esphome/core/application.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8c2ba58c86e..db1c8a0c0a1 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -153,6 +153,14 @@ void Application::setup() { this->setup_wake_loop_threadsafe_(); #endif + // Ensure all active looping components are in LOOP state. + // Components after the last blocking component only got one call() during setup + // (CONSTRUCTION→SETUP) and never received the second call() (SETUP→LOOP). + // The main loop calls loop() directly, bypassing call()'s state machine. + for (uint16_t i = 0; i < this->looping_components_active_end_; i++) { + this->looping_components_[i]->set_component_state_(COMPONENT_STATE_LOOP); + } + this->schedule_dump_config(); } void Application::loop() { @@ -173,7 +181,7 @@ void Application::loop() { { this->set_current_component(component); WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->call(); + component->loop(); // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } From b2e8544c584c29a923a88622a7b7ba10373a761f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 07:18:31 -1000 Subject: [PATCH 0371/2056] [ld2412] Add integration tests with mock UART (#14448) --- .../fixtures/uart_mock_ld2412.yaml | 171 ++++++++ .../uart_mock_ld2412_engineering.yaml | 213 +++++++++ tests/integration/test_uart_mock_ld2412.py | 407 ++++++++++++++++++ 3 files changed, 791 insertions(+) create mode 100644 tests/integration/fixtures/uart_mock_ld2412.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2412_engineering.yaml create mode 100644 tests/integration/test_uart_mock_ld2412.py diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml new file mode 100644 index 00000000000..a502f36a253 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -0,0 +1,171 @@ +esphome: + name: uart-mock-ld2412-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2412 normal mode data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # Moving target: 100cm, energy 50 + # Still target: 120cm, energy 25 + # Target state: 0x03 (moving + still) + # detection_distance = 100 (LD2412 computes from moving target when MOVE_BITMASK set) + # + # Frame layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0D 00 = length 13 + # [6] 02 = data type (normal) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 64 00 = moving distance 100 (0x0064) + # [11] 32 = moving energy 50 + # [12-13] 78 00 = still distance 120 (0x0078) + # [14] 19 = still energy 25 + # [15-16] 64 00 = detect distance bytes (ignored by LD2412 code) + # [17] 00 = padding + # [18] 55 = data footer marker + # [19] 00 = CRC/check + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x64, 0x00, + 0x32, + 0x78, 0x00, + 0x19, + 0x64, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2412's parser rejects bytes that don't match the frame header at + # position 0 (must start with F4 or FD), so buffer stays empty. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated frame (header + partial data, no footer) + # Starts with valid data frame header so parser accepts it. + # After this, buffer_pos_ = 8. + - delay: 100ms + inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x0D, 0x00, 0x02, 0xAA] + + # Phase 4 (t=600ms): Overflow - inject 60 bytes of 0xFF (MAX_LINE_LENGTH=54) + # Buffer has 8 bytes from phase 3 (garbage in phase 2 was rejected). + # Overflow math: buffer_pos_ starts at 8, overflow triggers when + # buffer_pos_ reaches 53 (MAX_LINE_LENGTH - 1). Need 45 more bytes to + # fill positions 8-52, then byte 46 triggers overflow. After overflow, + # buffer_pos_ = 0 and remaining 0xFF bytes are rejected (don't match header). + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=700ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Moving target: 50cm, energy 100 + # Still target: 75cm, energy 80 + # detection_distance = 50 (moving target distance, since MOVE_BITMASK set) + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0D, 0x00, + 0x02, 0xAA, + 0x03, + 0x32, 0x00, + 0x64, + 0x4B, 0x00, + 0x50, + 0x32, 0x00, + 0x00, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_distance: + name: "Still Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + moving_energy: + name: "Moving Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + detection_distance: + name: "Detection Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: + - settle: 50ms + has_still_target: + name: "Has Still Target" + filters: + - settle: 50ms diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml new file mode 100644 index 00000000000..3c669fc9a9d --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -0,0 +1,213 @@ +esphome: + name: uart-mock-ld2412-eng-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + injections: + # Phase 1 (t=100ms): Valid LD2412 engineering mode data frame + # + # Engineering mode frame layout (52 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 2A 00 = length 42 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 (0x001E) + # [11] 64 = moving energy 100 + # [12-13] 1E 00 = still distance 30 (0x001E) + # [14] 64 = still energy 100 + # [15-16] 00 00 = detection distance bytes (ignored) + # [17-30] gate moving energies (14 gates) + # [31-44] gate still energies (14 gates) + # [45] 57 = light sensor value 87 + # [46] 55 = data footer marker + # [47] 00 = check + # [48-51] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Second engineering mode frame with different values + # Moving at 73cm, still at 30cm + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x49, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x21, 0x00, + 0x11, 0x64, 0x05, 0x29, 0x39, 0x10, 0x03, 0x11, 0x0E, 0x08, 0x06, 0x04, 0x03, 0x02, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Frame with still target at 291cm (multi-byte distance) + # This tests encode_uint16 with high byte > 0 + # Target state: 0x02 (still only) -> detection_distance = still distance = 291 + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x02, + 0x2F, 0x00, + 0x36, + 0x23, 0x01, + 0x64, + 0x21, 0x00, + 0x2F, 0x36, 0x09, 0x0D, 0x15, 0x0B, 0x06, 0x06, 0x08, 0x09, 0x08, 0x07, 0x06, 0x05, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x5A, 0x3D, 0x30, 0x20, 0x10, 0x08, 0x04, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_distance: + name: "Still Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + moving_energy: + name: "Moving Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + detection_distance: + name: "Detection Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + light: + name: "Light" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_0: + move_energy: + name: "Gate 0 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 0 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_1: + move_energy: + name: "Gate 1 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 1 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + gate_2: + move_energy: + name: "Gate 2 Move Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_energy: + name: "Gate 2 Still Energy" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: + - settle: 50ms + has_still_target: + name: "Has Still Target" + filters: + - settle: 50ms diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py new file mode 100644 index 00000000000..cf7324ceed3 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2412.py @@ -0,0 +1,407 @@ +"""Integration test for LD2412 component with mock UART. + +Tests: +test_uart_mock_ld2412 (normal mode): + 1. Happy path - valid data frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated frame handling - partial frame doesn't corrupt state + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2412 sends expected setup commands + +test_uart_mock_ld2412_engineering (engineering mode): + 1. Engineering mode frames with per-gate energy data and light sensor + 2. Multi-byte still distance (291cm) using high byte > 0 + 3. Gate energy sensor values + 4. Detection distance computed from target state +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ( + BinarySensorInfo, + BinarySensorState, + EntityState, + SensorInfo, + SensorState, +) +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2412 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see recovery frame values + recovery_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is the recovery frame (moving_distance = 50) + if ( + sensor_name == "moving_distance" + and state.state == pytest.approx(50.0) + and not recovery_received.done() + ): + recovery_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + # Sort by descending length to avoid substring collisions + # (e.g., "still_energy" matching "gate_0_still_energy") + all_names.sort(key=len, reverse=True) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 values are in the initial states (swallowed by InitialStateHelper). + # Verify them via initial_states dict. + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(100.0), ( + f"Initial moving distance should be 100, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(120.0), ( + f"Initial still distance should be 120, got {initial_still.state}" + ) + + moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) + assert moving_energy_entity is not None + initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) + assert initial_me is not None and isinstance(initial_me, SensorState) + assert initial_me.state == pytest.approx(50.0), ( + f"Initial moving energy should be 50, got {initial_me.state}" + ) + + still_energy_entity = find_entity(entities, "still_energy", SensorInfo) + assert still_energy_entity is not None + initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) + assert initial_se is not None and isinstance(initial_se, SensorState) + assert initial_se.state == pytest.approx(25.0), ( + f"Initial still energy should be 25, got {initial_se.state}" + ) + + # LD2412 detection_distance = moving_distance when MOVE_BITMASK is set + detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) + assert detect_dist_entity is not None + initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) + assert initial_dd is not None and isinstance(initial_dd, SensorState) + assert initial_dd.state == pytest.approx(100.0), ( + f"Initial detection distance should be 100, got {initial_dd.state}" + ) + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received sensor states:\n" + f" moving_distance: {sensor_states['moving_distance']}\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_energy: {sensor_states['moving_energy']}\n" + f" still_energy: {sensor_states['still_energy']}\n" + f" detection_distance: {sensor_states['detection_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2412 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2412 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2412 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + assert len(sensor_states["moving_distance"]) >= 1, ( + f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" + ) + # Find the recovery value (moving_distance = 50) + recovery_values = [ + v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" + ) + + # Recovery frame: moving=50, still=75, energy=100/80, detect=50 + recovery_idx = next( + i + for i, v in enumerate(sensor_states["moving_distance"]) + if v == pytest.approx(50.0) + ) + assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( + f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + ) + assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( + f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + ) + assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( + f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" + ) + # LD2412 detection_distance = moving_distance when MOVE_BITMASK set + assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( + 50.0 + ), ( + f"Recovery detection distance should be 50, got {sensor_states['detection_distance'][recovery_idx]}" + ) + + # Verify binary sensors detected targets + has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) + assert has_target_entity is not None + initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) + assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) + assert initial_ht.state is True, "Has target should be True" + + has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) + assert has_moving_entity is not None + initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) + assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) + assert initial_hm.state is True, "Has moving target should be True" + + has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) + assert has_still_entity is not None + initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) + assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) + assert initial_hs.state is True, "Has still target should be True" + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412_engineering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2412 engineering mode with per-gate energy, light, and multi-byte distance.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "moving_distance": [], + "still_distance": [], + "moving_energy": [], + "still_energy": [], + "detection_distance": [], + "light": [], + "gate_0_move_energy": [], + "gate_1_move_energy": [], + "gate_2_move_energy": [], + "gate_0_still_energy": [], + "gate_1_still_energy": [], + "gate_2_still_energy": [], + } + binary_states: dict[str, list[bool]] = { + "has_target": [], + "has_moving_target": [], + "has_still_target": [], + } + + # Signal when we see Phase 3 frame values + phase3_still_received = loop.create_future() + phase3_detect_received = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + if ( + sensor_name == "still_distance" + and state.state == pytest.approx(291.0) + and not phase3_still_received.done() + ): + phase3_still_received.set_result(True) + if ( + sensor_name == "detection_distance" + and state.state == pytest.approx(291.0) + and not phase3_detect_received.done() + ): + phase3_detect_received.set_result(True) + elif isinstance(state, BinarySensorState): + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in binary_states: + binary_states[sensor_name].append(state.state) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + all_names = list(sensor_states.keys()) + list(binary_states.keys()) + # Sort by descending length to avoid substring collisions + # (e.g., "still_energy" matching "gate_0_still_energy") + all_names.sort(key=len, reverse=True) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Phase 1 initial values (engineering mode frame): + # moving=30, energy=100, still=30, energy=100, detect=30 + moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) + assert moving_dist_entity is not None + initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) + assert initial_moving is not None and isinstance(initial_moving, SensorState) + assert initial_moving.state == pytest.approx(30.0), ( + f"Initial moving distance should be 30, got {initial_moving.state}" + ) + + still_dist_entity = find_entity(entities, "still_distance", SensorInfo) + assert still_dist_entity is not None + initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) + assert initial_still is not None and isinstance(initial_still, SensorState) + assert initial_still.state == pytest.approx(30.0), ( + f"Initial still distance should be 30, got {initial_still.state}" + ) + + # Verify engineering mode sensors from initial state + # Gate 0 moving energy = 0x64 = 100 + gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) + assert gate0_move_entity is not None + initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) + assert initial_g0m is not None and isinstance(initial_g0m, SensorState) + assert initial_g0m.state == pytest.approx(100.0), ( + f"Gate 0 move energy should be 100, got {initial_g0m.state}" + ) + + # Gate 1 moving energy = 0x41 = 65 + gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) + assert gate1_move_entity is not None + initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) + assert initial_g1m is not None and isinstance(initial_g1m, SensorState) + assert initial_g1m.state == pytest.approx(65.0), ( + f"Gate 1 move energy should be 65, got {initial_g1m.state}" + ) + + # Light sensor = 0x57 = 87 + light_entity = find_entity(entities, "light", SensorInfo) + assert light_entity is not None + initial_light = initial_state_helper.initial_states.get(light_entity.key) + assert initial_light is not None and isinstance(initial_light, SensorState) + assert initial_light.state == pytest.approx(87.0), ( + f"Light sensor should be 87, got {initial_light.state}" + ) + + # Wait for Phase 3 frame: still_distance = 291cm (multi-byte) + try: + await asyncio.wait_for(phase3_still_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 3 still_distance. Received:\n" + f" still_distance: {sensor_states['still_distance']}\n" + f" moving_distance: {sensor_states['moving_distance']}" + ) + + assert pytest.approx(291.0) in sensor_states["still_distance"], ( + f"Expected still_distance=291, got: {sensor_states['still_distance']}" + ) + + # Wait for Phase 3: detection_distance = 291 (still-only target) + # target_state=0x02 so LD2412 uses still_distance for detection_distance. + # The throttle_with_priority filter may delay this value. + try: + await asyncio.wait_for(phase3_detect_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for detection_distance=291 (still-only target). " + f"Received: {sensor_states['detection_distance']}" + ) + + assert pytest.approx(291.0) in sensor_states["detection_distance"], ( + f"Expected detection_distance=291, got: {sensor_states['detection_distance']}" + ) From 5ba880f19b967b153340befa1acf271066f8671c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:57:45 -0500 Subject: [PATCH 0372/2056] [sx127x] Fix preamble MSB register always written as zero (#14457) Co-authored-by: Claude Opus 4.6 --- esphome/components/sx127x/sx127x.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index caf68b6d513..f6aa11b6347 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -186,7 +186,7 @@ void SX127x::configure_fsk_ook_() { } else { this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF); } - this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 8); this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF); // config sync generation and setup ook threshold @@ -214,7 +214,7 @@ void SX127x::configure_lora_() { // config preamble if (this->preamble_size_ >= 6) { - this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 8); this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF); } From 9abba79c5429ee9c9c4e9a4f77ca4c67b4a756b2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:24 -0500 Subject: [PATCH 0373/2056] [remote_base][remote_receiver] Fix OOB access in pronto comparison and RMT buffer allocation (#14459) Co-authored-by: Claude Opus 4.6 --- esphome/components/remote_base/pronto_protocol.cpp | 6 +++++- esphome/components/remote_receiver/remote_receiver_rmt.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 43029cbc2fc..6903cd46058 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -44,9 +44,13 @@ bool ProntoData::operator==(const ProntoData &rhs) const { std::vector data1 = encode_pronto(data); std::vector data2 = encode_pronto(rhs.data); + if (data1.size() != data2.size() || data1.empty()) { + return false; + } + uint32_t total_diff = 0; // Don't need to check the last one, it's the large gap at the end. - for (std::vector::size_type i = 0; i < data1.size() - 1; ++i) { + for (size_t i = 0; i < data1.size() - 1; ++i) { int diff = data2[i] - data1[i]; diff *= diff; if (rhs.delta == -1 && diff > 9) diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index 357a36d052f..96b23bd0f52 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -106,7 +106,7 @@ void RemoteReceiverComponent::setup() { this->store_.filter_symbols = this->filter_symbols_; this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t); this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_); - this->store_.buffer = new uint8_t[this->buffer_size_]; + this->store_.buffer = new uint8_t[this->store_.buffer_size]; error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size, &this->store_.config); if (error != ESP_OK) { From 246a8bff0cb9c2de5a740fe8529f072c609dce51 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:42 -0500 Subject: [PATCH 0374/2056] [pn7160][pn7150][pn532] Fix tag purge skipping, NDEF bounds check, and NDEF length byte order (#14460) Co-authored-by: Claude Opus 4.6 --- esphome/components/pn532/pn532_mifare_ultralight.cpp | 4 ++-- esphome/components/pn7150/pn7150.cpp | 6 +++--- esphome/components/pn7150/pn7150_mifare_ultralight.cpp | 4 ++-- esphome/components/pn7160/pn7160.cpp | 6 +++--- esphome/components/pn7160/pn7160_mifare_ultralight.cpp | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index a8a8e2d5735..0e0dc1542f4 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -99,7 +99,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6 uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return false; } @@ -134,7 +134,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage * } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 8c76c8b88c9..d68bea41b3d 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -562,9 +562,9 @@ optional PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) { } void PN7150::purge_old_tags_() { - for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { - if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { - this->erase_tag_(i); + for (size_t i = this->discovered_endpoint_.size(); i > 0; i--) { + if (millis() - this->discovered_endpoint_[i - 1].last_seen > this->tag_ttl_) { + this->erase_tag_(i - 1); } } } diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 46f5dba2b76..854ddd1be18 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -100,7 +100,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_ uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return nfc::STATUS_FAILED; } @@ -135,7 +135,7 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 3fcd1221a72..5f0f8d0629c 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -589,9 +589,9 @@ optional PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) { } void PN7160::purge_old_tags_() { - for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { - if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { - this->erase_tag_(i); + for (size_t i = this->discovered_endpoint_.size(); i > 0; i--) { + if (millis() - this->discovered_endpoint_[i - 1].last_seen > this->tag_ttl_) { + this->erase_tag_(i - 1); } } } diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index 9dc8d3dd2d5..8ca0fa2c11b 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -100,7 +100,7 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_ uint8_t &message_start_index) { const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector - if (!(page_3_to_6.size() > p4_offset + 5)) { + if (!(page_3_to_6.size() > p4_offset + 6)) { return nfc::STATUS_FAILED; } @@ -135,7 +135,7 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha } else { encoded.insert(encoded.begin() + 1, 0xFF); encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); - encoded.insert(encoded.begin() + 2, message_length & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); } encoded.push_back(0xFE); From c37ab1de841631dd1802a2abbfde655f49513e12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:58:52 -0500 Subject: [PATCH 0375/2056] [fingerprint_grow] Fix OOB write and uint16 overflow (#14462) Co-authored-by: Claude Opus 4.6 --- .../fingerprint_grow/fingerprint_grow.cpp | 23 ++++++++++--------- .../fingerprint_grow/fingerprint_grow.h | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index da4535fc828..a633fbca282 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -361,7 +361,7 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } } -uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) { +uint8_t FingerprintGrowComponent::transfer_(std::vector &data_buffer) { while (this->available()) this->read(); this->write((uint8_t) (START_CODE >> 8)); @@ -372,12 +372,12 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write(this->address_[3]); this->write(COMMAND); - uint16_t wire_length = p_data_buffer->size() + 2; + uint16_t wire_length = data_buffer.size() + 2; this->write((uint8_t) (wire_length >> 8)); this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = (wire_length >> 8) + (wire_length & 0xFF) + COMMAND; - for (auto data : *p_data_buffer) { + for (auto data : data_buffer) { this->write(data); sum += data; } @@ -385,7 +385,7 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write((uint8_t) (sum >> 8)); this->write((uint8_t) (sum & 0xFF)); - p_data_buffer->clear(); + data_buffer.clear(); uint8_t byte; uint16_t idx = 0, length = 0; @@ -431,9 +431,9 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) length |= byte; break; default: - p_data_buffer->push_back(byte); + data_buffer.push_back(byte); if ((idx - 8) == length) { - switch ((*p_data_buffer)[0]) { + switch (data_buffer[0]) { case OK: case NO_FINGER: case IMAGE_FAIL: @@ -453,25 +453,26 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) ESP_LOGE(TAG, "Reader failed to process request"); break; default: - ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", (*p_data_buffer)[0]); + ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", data_buffer[0]); break; } this->last_transfer_ms_ = millis(); - return (*p_data_buffer)[0]; + return data_buffer[0]; } break; } idx++; } ESP_LOGE(TAG, "No response received from reader"); - (*p_data_buffer)[0] = TIMEOUT; + data_buffer.clear(); + data_buffer.push_back(TIMEOUT); this->last_transfer_ms_ = millis(); return TIMEOUT; } uint8_t FingerprintGrowComponent::send_command_() { this->sensor_wakeup_(); - return this->transfer_(&this->data_); + return this->transfer_(this->data_); } void FingerprintGrowComponent::sensor_wakeup_() { @@ -517,7 +518,7 @@ void FingerprintGrowComponent::sensor_wakeup_() { std::vector buffer = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; - if (this->transfer_(&buffer) != OK) { + if (this->transfer_(buffer) != OK) { ESP_LOGE(TAG, "Wrong password"); } } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 370b26f56a3..db9d5ce564f 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -169,7 +169,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool set_password_(); bool get_parameters_(); void get_fingerprint_count_(); - uint8_t transfer_(std::vector *p_data_buffer); + uint8_t transfer_(std::vector &data_buffer); uint8_t send_command_(); void sensor_wakeup_(); void sensor_sleep_(); @@ -190,7 +190,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool is_sensor_awake_ = false; uint32_t last_transfer_ms_ = 0; uint32_t last_aura_led_control_ = 0; - uint16_t last_aura_led_duration_ = 0; + uint32_t last_aura_led_duration_ = 0; uint16_t system_identifier_code_ = 0; uint32_t idle_period_to_sleep_ms_ = UINT32_MAX; sensor::Sensor *fingerprint_count_sensor_{nullptr}; From 22fc3aab392c8cad0c2d8a74e6074b83e7c17139 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:19:46 -0500 Subject: [PATCH 0376/2056] [ld2420] Fix buffer overflows in simple mode, energy mode, and calibration (#14458) Co-authored-by: Claude Opus 4.6 --- esphome/components/ld2420/ld2420.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index f14400d15a4..1e671363c9d 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -460,6 +460,10 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { uint8_t index = 6; // Start at presence byte position uint16_t range; const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]); + if (len < static_cast(index + 1 + sizeof(range) + elements * sizeof(this->gate_energy_[0]))) { + ESP_LOGW(TAG, "Energy frame too short: %d bytes", len); + return; + } this->set_presence_(buffer[index]); index++; memcpy(&range, &buffer[index], sizeof(range)); @@ -471,8 +475,11 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { } if (this->current_operating_mode == OP_CALIBRATE_MODE) { - this->update_radar_data(gate_energy_, sample_number_counter); - this->sample_number_counter > CALIBRATE_SAMPLES ? this->sample_number_counter = 0 : this->sample_number_counter++; + this->update_radar_data(gate_energy_, this->sample_number_counter); + this->sample_number_counter++; + if (this->sample_number_counter >= CALIBRATE_SAMPLES) { + this->sample_number_counter = 0; + } } // Resonable refresh rate for home assistant database size health @@ -503,22 +510,20 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { char *endptr{nullptr}; char outbuf[bufsize]{0}; while (true) { - if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { + if (pos >= 2 && inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { this->set_presence_(false); - } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { + } else if (pos >= 1 && inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { this->set_presence_(true); } if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { if (index < bufsize - 1) { outbuf[index++] = inbuf[pos]; - pos++; } + } + if (pos < len - 1) { + pos++; } else { - if (pos < len - 1) { - pos++; - } else { - break; - } + break; } } outbuf[index] = '\0'; From 4928e678d1f54ee261789ab283280f317b425357 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 4 Mar 2026 13:37:22 -0600 Subject: [PATCH 0377/2056] [mixer][resampler][speaker] Use core static task manager (#14454) --- .../mixer/speaker/mixer_speaker.cpp | 91 +++------------ .../components/mixer/speaker/mixer_speaker.h | 18 +-- .../resampler/speaker/resampler_speaker.cpp | 60 +--------- .../resampler/speaker/resampler_speaker.h | 19 +-- .../speaker/media_player/audio_pipeline.cpp | 110 ++++-------------- .../speaker/media_player/audio_pipeline.h | 13 +-- 6 files changed, 49 insertions(+), 262 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 100acbebc33..8e1278206f4 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -438,24 +438,14 @@ void MixerSpeaker::loop() { // Handle pending start request if (event_group_bits & MIXER_TASK_COMMAND_START) { // Only start the task if it's fully stopped and cleaned up - if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) { - esp_err_t err = this->start_task_(); - switch (err) { - case ESP_OK: - xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); - break; - case ESP_ERR_NO_MEM: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("memory-failure", 1000); - return; - case ESP_ERR_INVALID_STATE: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); - return; - default: - ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); - this->status_momentary_error("failure", 1000); - return; + if (!this->status_has_error() && !this->task_.is_created()) { + if (this->task_.create(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, MIXER_TASK_PRIORITY, + this->task_stack_in_psram_)) { + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START); + } else { + ESP_LOGE(TAG, "Failed to start; retrying in 1 second"); + this->status_momentary_error("failure", 1000); + return; } } } @@ -478,13 +468,12 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); } if (event_group_bits & MIXER_TASK_STATE_STOPPED) { - if (this->delete_task_() == ESP_OK) { - ESP_LOGD(TAG, "Stopped"); - xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); - } + this->task_.deallocate(); + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); } - if (this->task_handle_ != nullptr) { + if (this->task_.is_created()) { // If the mixer task is running, check if all source speakers are stopped bool all_stopped = true; @@ -497,7 +486,7 @@ void MixerSpeaker::loop() { // Send stop command signal to the mixer task since no source speakers are active xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); } - } else if (this->task_stack_buffer_ == nullptr) { + } else { // Task is fully stopped and cleaned up, check if we can disable loop event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits == 0) { @@ -538,60 +527,6 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { return ESP_OK; } -esp_err_t MixerSpeaker::start_task_() { - if (this->task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } - } - - if (this->task_stack_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, - MIXER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); - } - - if (this->task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } - - return ESP_OK; -} - -esp_err_t MixerSpeaker::delete_task_() { - if (this->task_handle_ != nullptr) { - // Delete the task - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - } - - if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) { - // Deallocate the task stack buffer - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } - - if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) { - return ESP_ERR_INVALID_STATE; - } - - return ESP_OK; -} - void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, int16_t *output_buffer, audio::AudioStreamInfo output_stream_info, uint32_t frames_to_transfer) { diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index e920f9895a0..0e0b33c39bc 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -8,8 +8,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/static_task.h" -#include #include #include @@ -143,8 +143,6 @@ class MixerSpeaker : public Component { /// @param stream_info The calling source speaker's audio stream information /// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample /// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream - /// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack - /// ESP_ERR_INVALID_STATE if the task fails to start /// ESP_OK if the incoming stream is compatible and the mixer task starts esp_err_t start(audio::AudioStreamInfo &stream_info); @@ -188,16 +186,6 @@ class MixerSpeaker : public Component { static void audio_mixer_task(void *params); - /// @brief Starts the mixer task after allocating memory for the task stack. - /// @return ESP_ERR_NO_MEM if there isn't enough memory for the task's stack - /// ESP_ERR_INVALID_STATE if the task didn't start - /// ESP_OK if successful - esp_err_t start_task_(); - - /// @brief If the task is stopped, it sets the task handle to the nullptr and deallocates its stack - /// @return ESP_OK if the task was stopped, ESP_ERR_INVALID_STATE otherwise. - esp_err_t delete_task_(); - EventGroupHandle_t event_group_{nullptr}; FixedVector source_speakers_; @@ -207,9 +195,7 @@ class MixerSpeaker : public Component { bool queue_mode_; bool task_stack_in_psram_{false}; - TaskHandle_t task_handle_{nullptr}; - StaticTask_t task_stack_; - StackType_t *task_stack_buffer_{nullptr}; + StaticTask task_; optional audio_stream_info_; diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 74420f906a1..1303bc459e5 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -147,7 +147,7 @@ void ResamplerSpeaker::loop() { xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - this->delete_task_(); + this->task_.deallocate(); ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } @@ -190,7 +190,7 @@ void ResamplerSpeaker::loop() { this->output_speaker_->stop(); } - if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) { + if (this->output_speaker_->is_stopped() && !this->task_.is_created()) { // Only transition to stopped state once the output speaker and resampler task are fully stopped this->waiting_for_output_ = false; this->state_ = speaker::STATE_STOPPED; @@ -209,9 +209,6 @@ void ResamplerSpeaker::loop() { void ResamplerSpeaker::set_start_error_(esp_err_t err) { switch (err) { - case ESP_ERR_INVALID_STATE: - this->status_set_error(LOG_STR("Task failed to start")); - break; case ESP_ERR_NO_MEM: this->status_set_error(LOG_STR("Not enough memory")); break; @@ -267,36 +264,12 @@ esp_err_t ResamplerSpeaker::start_() { if (this->requires_resampling_()) { // Start the resampler task to handle converting sample rates - return this->start_task_(); - } - - return ESP_OK; -} - -esp_err_t ResamplerSpeaker::start_task_() { - if (this->task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE); + if (!this->task_.create(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, RESAMPLER_TASK_PRIORITY, + this->task_stack_in_psram_)) { + return ESP_ERR_NO_MEM; } } - if (this->task_stack_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->task_handle_ == nullptr) { - this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this, - RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); - } - - if (this->task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } - return ESP_OK; } @@ -305,33 +278,12 @@ void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::CO void ResamplerSpeaker::enter_stopping_state_() { this->state_ = speaker::STATE_STOPPING; this->state_start_ms_ = App.get_loop_component_start_time(); - if (this->task_handle_ != nullptr) { + if (this->task_.is_created()) { xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP); } this->output_speaker_->stop(); } -void ResamplerSpeaker::delete_task_() { - if (this->task_handle_ != nullptr) { - // Delete the suspended task - vTaskDelete(this->task_handle_); - this->task_handle_ = nullptr; - } - - if (this->task_stack_buffer_ != nullptr) { - // Deallocate the task stack buffer - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } - - this->task_stack_buffer_ = nullptr; - } -} - void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); } bool ResamplerSpeaker::has_buffered_data() const { diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index c1ebd7e7b5b..cdbc1c22db4 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -7,8 +7,8 @@ #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" +#include "esphome/core/static_task.h" -#include #include namespace esphome { @@ -57,15 +57,9 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { protected: /// @brief Starts the output speaker after setting the resampled stream info. If resampling is required, it starts the /// task. - /// @return ESP_OK if resampling is required - /// return value of start_task_() if resampling is required - esp_err_t start_(); - - /// @brief Starts the resampler task after allocating the task stack /// @return ESP_OK if successful, - /// ESP_ERR_NO_MEM if the task stack couldn't be allocated - /// ESP_ERR_INVALID_STATE if the task wasn't created - esp_err_t start_task_(); + /// ESP_ERR_NO_MEM if the resampler task couldn't be created + esp_err_t start_(); /// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is /// running, and stops the output speaker. @@ -74,9 +68,6 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { /// @brief Sets the appropriate status error based on the start failure reason. void set_start_error_(esp_err_t err); - /// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers. - void delete_task_(); - /// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop. void send_command_(uint32_t command_bit, bool wake_loop = false); @@ -92,9 +83,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { bool task_stack_in_psram_{false}; bool waiting_for_output_{false}; - TaskHandle_t task_handle_{nullptr}; - StaticTask_t task_stack_; - StackType_t *task_stack_buffer_{nullptr}; + StaticTask task_; audio::AudioStreamInfo target_stream_info_; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 177743feb1d..8cea3abcfc9 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -87,20 +87,20 @@ void AudioPipeline::set_pause_state(bool pause_state) { } void AudioPipeline::suspend_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskSuspend(this->read_task_handle_); + if (this->read_task_.is_created()) { + vTaskSuspend(this->read_task_.get_handle()); } - if (this->decode_task_handle_ != nullptr) { - vTaskSuspend(this->decode_task_handle_); + if (this->decode_task_.is_created()) { + vTaskSuspend(this->decode_task_.get_handle()); } } void AudioPipeline::resume_tasks() { - if (this->read_task_handle_ != nullptr) { - vTaskResume(this->read_task_handle_); + if (this->read_task_.is_created()) { + vTaskResume(this->read_task_.get_handle()); } - if (this->decode_task_handle_ != nullptr) { - vTaskResume(this->decode_task_handle_); + if (this->decode_task_.is_created()) { + vTaskResume(this->decode_task_.get_handle()); } } @@ -159,7 +159,7 @@ AudioPipelineState AudioPipeline::process_state() { // Init command pending if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { // Only start if there is no pending stop command - if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr)) { + if (!this->read_task_.is_created() || !this->decode_task_.is_created()) { // At least one task isn't running this->start_tasks_(); } @@ -202,8 +202,9 @@ AudioPipelineState AudioPipeline::process_state() { if (!this->is_playing_) { // The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks - if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { - this->delete_tasks_(); + if (this->read_task_.is_created() || this->decode_task_.is_created()) { + this->read_task_.deallocate(); + this->decode_task_.deallocate(); if (this->hard_stop_) { // Stop command was sent, so immediately end the playback this->speaker_->stop(); @@ -234,7 +235,7 @@ AudioPipelineState AudioPipeline::process_state() { } } - if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) { + if (!this->read_task_.is_created() && !this->decode_task_.is_created()) { // No tasks are running, so the pipeline is stopped. xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); return AudioPipelineState::STOPPED; @@ -262,94 +263,25 @@ esp_err_t AudioPipeline::allocate_communications_() { } esp_err_t AudioPipeline::start_tasks_() { - if (this->read_task_handle_ == nullptr) { - if (this->read_task_stack_buffer_ == nullptr) { - // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is - // in PSRAM. As a workaround, always allocate the read task in internal memory. - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } - - if (this->read_task_stack_buffer_ == nullptr) { + if (!this->read_task_.is_created()) { + // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is + // in PSRAM. As a workaround, always allocate the read task in internal memory. + if (!this->read_task_.create(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this, + this->priority_, false)) { return ESP_ERR_NO_MEM; } - - if (this->read_task_handle_ == nullptr) { - this->read_task_handle_ = - xTaskCreateStatic(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this, - this->priority_, this->read_task_stack_buffer_, &this->read_task_stack_); - } - - if (this->read_task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } } - if (this->decode_task_handle_ == nullptr) { - if (this->decode_task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE); - } - } - - if (this->decode_task_stack_buffer_ == nullptr) { + if (!this->decode_task_.is_created()) { + if (!this->decode_task_.create(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, + (void *) this, this->priority_, this->task_stack_in_psram_)) { return ESP_ERR_NO_MEM; } - - if (this->decode_task_handle_ == nullptr) { - this->decode_task_handle_ = - xTaskCreateStatic(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, (void *) this, - this->priority_, this->decode_task_stack_buffer_, &this->decode_task_stack_); - } - - if (this->decode_task_handle_ == nullptr) { - return ESP_ERR_INVALID_STATE; - } } return ESP_OK; } -void AudioPipeline::delete_tasks_() { - if (this->read_task_handle_ != nullptr) { - vTaskDelete(this->read_task_handle_); - - if (this->read_task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE); - } - - this->read_task_stack_buffer_ = nullptr; - this->read_task_handle_ = nullptr; - } - } - - if (this->decode_task_handle_ != nullptr) { - vTaskDelete(this->decode_task_handle_); - - if (this->decode_task_stack_buffer_ != nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE); - } - - this->decode_task_stack_buffer_ = nullptr; - this->decode_task_handle_ = nullptr; - } - } -} - void AudioPipeline::read_task(void *params) { AudioPipeline *this_pipeline = (AudioPipeline *) params; diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 6fffde6c206..2c785728359 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -8,10 +8,10 @@ #include "esphome/components/speaker/speaker.h" #include "esphome/core/ring_buffer.h" +#include "esphome/core/static_task.h" #include "esp_err.h" -#include #include #include @@ -104,9 +104,6 @@ class AudioPipeline { /// @return ESP_OK if successful or an appropriate error if not esp_err_t start_tasks_(); - /// @brief Resets the task related pointers and deallocates their stacks. - void delete_tasks_(); - std::string base_name_; UBaseType_t priority_; @@ -143,15 +140,11 @@ class AudioPipeline { // Handles reading the media file from flash or a url static void read_task(void *params); - TaskHandle_t read_task_handle_{nullptr}; - StaticTask_t read_task_stack_; - StackType_t *read_task_stack_buffer_{nullptr}; + StaticTask read_task_; // Decodes the media file into PCM audio static void decode_task(void *params); - TaskHandle_t decode_task_handle_{nullptr}; - StaticTask_t decode_task_stack_; - StackType_t *decode_task_stack_buffer_{nullptr}; + StaticTask decode_task_; }; } // namespace speaker From 0c883b80c4376906eb5dc67f4b77d1249ee67b1d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:05:49 -0500 Subject: [PATCH 0378/2056] [inkplate][ezo_pmp][ezo][packet_transport] Fix use-after-free bugs (#14467) Co-authored-by: Claude Opus 4.6 --- esphome/components/ezo/ezo.cpp | 3 +- esphome/components/ezo_pmp/ezo_pmp.cpp | 36 ++++++++++--------- esphome/components/ezo_pmp/ezo_pmp.h | 2 +- esphome/components/inkplate/inkplate.cpp | 20 ++++++++--- .../packet_transport/packet_transport.cpp | 2 +- .../packet_transport/packet_transport.h | 2 +- 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index e4036021df7..2dc65b7d14c 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -66,8 +66,9 @@ void EZOSensor::loop() { if (to_run->command_type == EzoCommandType::EZO_SLEEP || to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data + bool update_address = to_run->command_type == EzoCommandType::EZO_I2C; this->commands_.pop_front(); - if (to_run->command_type == EzoCommandType::EZO_I2C) + if (update_address) this->address_ = this->new_address_; return; } diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index bf6e3926b87..4ce4da57ffc 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -165,22 +165,23 @@ void EzoPMP::read_command_result_() { continue; } - switch (current_parameter) { - case 1: - first_parameter_buffer[position_in_parameter_buffer] = current_char; - first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 2: - second_parameter_buffer[position_in_parameter_buffer] = current_char; - second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 3: - third_parameter_buffer[position_in_parameter_buffer] = current_char; - third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; + if (position_in_parameter_buffer < sizeof(first_parameter_buffer) - 1) { + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + position_in_parameter_buffer++; } - - position_in_parameter_buffer++; } auto parsed_first_parameter = parse_number(first_parameter_buffer); @@ -404,7 +405,8 @@ void EzoPMP::send_next_command_() { break; case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command - command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_.c_str()); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); break; @@ -541,7 +543,7 @@ void EzoPMP::change_i2c_address(int address) { } void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { - this->arbitrary_command_ = command.c_str(); + this->arbitrary_command_ = command; this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); } diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index d4917e7f4b4..bbfd8991707 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -85,7 +85,7 @@ class EzoPMP : public PollingComponent, public i2c::I2CDevice { bool is_paused_flag_ = false; bool is_dosing_flag_ = false; - const char *arbitrary_command_{nullptr}; + std::string arbitrary_command_{}; void send_next_command_(); void read_command_result_(); diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index c921c643fa3..df9c2b29c78 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -63,16 +63,26 @@ void Inkplate::initialize_() { if (buffer_size == 0) return; - if (this->partial_buffer_ != nullptr) + if (this->partial_buffer_ != nullptr) { allocator.deallocate(this->partial_buffer_, buffer_size); - if (this->partial_buffer_2_ != nullptr) + this->partial_buffer_ = nullptr; + } + if (this->partial_buffer_2_ != nullptr) { allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); - if (this->buffer_ != nullptr) + this->partial_buffer_2_ = nullptr; + } + if (this->buffer_ != nullptr) { allocator.deallocate(this->buffer_, buffer_size); - if (this->glut_ != nullptr) + this->buffer_ = nullptr; + } + if (this->glut_ != nullptr) { allocator32.deallocate(this->glut_, 256 * 9); - if (this->glut2_ != nullptr) + this->glut_ = nullptr; + } + if (this->glut2_ != nullptr) { allocator32.deallocate(this->glut2_, 256 * 9); + this->glut2_ = nullptr; + } this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 7b7a8523986..d2c59200017 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -249,7 +249,7 @@ void PacketTransport::init_data_() { } else { add(this->data_, DATA_KEY); } - for (auto pkey : this->ping_keys_) { + for (const auto &pkey : this->ping_keys_) { add(this->data_, PING_KEY); add(this->data_, pkey.second); } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 57f40874b53..a2367442317 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -150,7 +150,7 @@ class PacketTransport : public PollingComponent { std::vector ping_header_{}; std::vector header_{}; std::vector data_{}; - std::map ping_keys_{}; + std::map ping_keys_{}; const char *platform_name_{""}; void add_key_(const char *name, uint32_t key); void send_ping_pong_request_(); From e11a91411b800497b812a9baf374fca36af5e085 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:36:52 -0500 Subject: [PATCH 0379/2056] [esp32_improv][rf_bridge][esp32_ble_server][display][lvgl][pipsolar] Fix unsigned integer underflows (#14466) Co-authored-by: Claude Opus 4.6 Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/display/display.cpp | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 6 +++++- esphome/components/esp32_improv/esp32_improv_component.cpp | 2 ++ esphome/components/lvgl/lvgl_esphome.cpp | 2 +- esphome/components/pipsolar/pipsolar.cpp | 4 +++- esphome/components/rf_bridge/rf_bridge.cpp | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 2bd7d036006..f8569b6e7c9 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -661,6 +661,9 @@ void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } void Display::set_pages(std::vector pages) { + if (pages.empty()) + return; + for (auto *page : pages) page->set_parent(this); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index a1b1ff94bb3..d4ccefd9b29 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -209,7 +209,11 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt esp_gatt_rsp_t response; if (param->read.is_long) { - if (this->value_.size() - this->value_read_offset_ < max_offset) { + if (this->value_read_offset_ >= this->value_.size()) { + response.attr_value.len = 0; + response.attr_value.offset = this->value_read_offset_; + this->value_read_offset_ = 0; + } else if (this->value_.size() - this->value_read_offset_ < max_offset) { // Last message in the chain response.attr_value.len = this->value_.size() - this->value_read_offset_; response.attr_value.offset = this->value_read_offset_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 83bc842a3d3..e4ae49f2356 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -314,6 +314,8 @@ void ESP32ImprovComponent::dump_config() { } void ESP32ImprovComponent::process_incoming_data_() { + if (this->incoming_data_.size() < 3) + return; uint8_t length = this->incoming_data_[1]; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index bb373abb88b..3e447e9169d 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -421,7 +421,7 @@ void LvglComponent::write_random_() { col = col / this->draw_rounding * this->draw_rounding; auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; - auto size = (random_uint32() % 32) / this->draw_rounding * this->draw_rounding - 1; + auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index f95bf4aedb9..9c5caec7758 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -162,13 +162,15 @@ void Pipsolar::loop() { } uint8_t Pipsolar::check_incoming_length_(uint8_t length) { - if (this->read_pos_ - 3 == length) { + if (this->read_pos_ >= 3 && this->read_pos_ - 3 == length) { return 1; } return 0; } uint8_t Pipsolar::check_incoming_crc_() { + if (this->read_pos_ < 3) + return 0; uint16_t crc16; crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index d8c148145ce..700e2ba1623 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -74,7 +74,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { data.length = raw[2]; data.protocol = raw[3]; char next_byte[3]; // 2 hex chars + null - for (uint8_t i = 0; i < data.length - 1; i++) { + for (uint8_t i = 0; i + 1 < data.length; i++) { buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[4 + i]); data.code += next_byte; } From 61ea6c3b2f759c325c1428b371e4e9f8a00425fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 12:46:26 -1000 Subject: [PATCH 0380/2056] [ci] Add missing issues: write permission to codeowner approval workflow (#14477) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/codeowner-approved-label.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml index 217ae06419b..200f18f5448 100644 --- a/.github/workflows/codeowner-approved-label.yml +++ b/.github/workflows/codeowner-approved-label.yml @@ -12,7 +12,8 @@ on: types: [submitted, dismissed] permissions: - pull-requests: write + issues: write + pull-requests: read contents: read jobs: From 55103c0652bdece270ac09e9f3224c2693d7e2d2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:14 -0500 Subject: [PATCH 0381/2056] [ds2484] Fix read64() using uint8_t accumulator instead of uint64_t (#14479) Co-authored-by: Claude Opus 4.6 --- esphome/components/ds2484/ds2484.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp index 7c890ff4339..0b36f868749 100644 --- a/esphome/components/ds2484/ds2484.cpp +++ b/esphome/components/ds2484/ds2484.cpp @@ -110,9 +110,9 @@ uint8_t DS2484OneWireBus::read8() { } uint64_t DS2484OneWireBus::read64() { - uint8_t response = 0; + uint64_t response = 0; for (uint8_t i = 0; i < 8; i++) { - response |= (this->read8() << (i * 8)); + response |= (static_cast(this->read8()) << (i * 8)); } return response; } From b6d7e8e14de939b4d6714a42138edb914fd346c4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:28 -0500 Subject: [PATCH 0382/2056] [sgp30] Fix serial number truncation from 48-bit to 24-bit (#14478) Co-authored-by: Claude Opus 4.6 --- esphome/components/sgp30/sgp30.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 18814405d48..35e5b3dd42b 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -41,7 +41,9 @@ void SGP30Component::setup() { this->mark_failed(); return; } - this->serial_number_ = encode_uint24(raw_serial_number[0], raw_serial_number[1], raw_serial_number[2]); + this->serial_number_ = (static_cast(raw_serial_number[0]) << 32) | + (static_cast(raw_serial_number[1]) << 16) | + static_cast(raw_serial_number[2]); ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); // Featureset identification for future use From c8e7f78a2567e51d4fa178799ad3351ce046272d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:32:50 -0500 Subject: [PATCH 0383/2056] [zwave_proxy] Fix uint8_t overflow for buffer index and frame end (#14480) Co-authored-by: Claude Opus 4.6 --- esphome/components/zwave_proxy/zwave_proxy.cpp | 4 ++++ esphome/components/zwave_proxy/zwave_proxy.h | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 8506b19e7f4..b0836ac0728 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -281,6 +281,10 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { break; } case ZWAVE_PARSING_STATE_READ_BL_MENU: + if (this->buffer_index_ >= this->buffer_.size()) { + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + break; + } this->buffer_[this->buffer_index_++] = byte; if (!byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index eb26316f492..12cb9a90a1e 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -81,10 +81,10 @@ class ZWaveProxy : public uart::UARTDevice, public Component { api::APIConnection *api_connection_{nullptr}; // Current subscribed client uint32_t setup_time_{0}; // Time when setup() was called - // 8-bit values (grouped together to minimize padding) - uint8_t buffer_index_{0}; // Index for populating the data buffer - uint8_t end_frame_after_{0}; // Payload reception ends after this index - uint8_t last_response_{0}; // Last response type sent + // Small values (grouped by size to minimize padding) + uint16_t buffer_index_{0}; // Index for populating the data buffer + uint16_t end_frame_after_{0}; // Payload reception ends after this index + uint8_t last_response_{0}; // Last response type sent ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module From c0143ac6d662967a8a3fbb008242ee4bc5f92a1f Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:46:40 -0600 Subject: [PATCH 0384/2056] [ai] Add docs note about keeping component index pages in sync (#14465) Co-authored-by: Brandon Harvey Co-authored-by: J. Nick Koston --- .ai/instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 3c24177827f..240a47a52fd 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -286,6 +286,7 @@ This document provides essential context for AI models interacting with this pro * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome-docs` repository. * The contribution workflow is the same as for the codebase. + * When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync. * **Best Practices:** * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. From 5df4fd0a271251d0dc29e0b76148b50550cfb582 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 15:51:51 -1000 Subject: [PATCH 0385/2056] [tests] Fix flaky uart_mock integration tests (#14476) --- .../external_components/uart_mock/__init__.py | 5 + .../uart_mock/uart_mock.cpp | 37 +- .../external_components/uart_mock/uart_mock.h | 4 + .../fixtures/uart_mock_ld2410.yaml | 8 + .../uart_mock_ld2410_engineering.yaml | 8 + .../fixtures/uart_mock_ld2412.yaml | 8 + .../uart_mock_ld2412_engineering.yaml | 8 + .../fixtures/uart_mock_modbus.yaml | 8 + .../fixtures/uart_mock_modbus_timing.yaml | 8 + tests/integration/state_utils.py | 96 ++++- tests/integration/test_uart_mock_ld2410.py | 353 ++++++----------- tests/integration/test_uart_mock_ld2412.py | 362 ++++++------------ tests/integration/test_uart_mock_modbus.py | 14 +- 13 files changed, 420 insertions(+), 499 deletions(-) diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index 8deab4c21ec..abb3abcc419 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -43,6 +43,7 @@ CONF_INJECT_RX = "inject_rx" CONF_EXPECT_TX = "expect_tx" CONF_PERIODIC_RX = "periodic_rx" CONF_ON_TX = "on_tx" +CONF_AUTO_START = "auto_start" UART_PARITY_OPTIONS = { "NONE": uart.UARTParityOptions.UART_CONFIG_PARITY_NONE, @@ -95,6 +96,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_INJECTIONS, default=[]): cv.ensure_list(INJECTION_SCHEMA), cv.Optional(CONF_RESPONSES, default=[]): cv.ensure_list(RESPONSE_SCHEMA), cv.Optional(CONF_PERIODIC_RX, default=[]): cv.ensure_list(PERIODIC_RX_SCHEMA), + cv.Optional(CONF_AUTO_START, default=True): cv.boolean, cv.Optional(CONF_ON_TX): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MockUartTXTrigger), @@ -138,6 +140,9 @@ async def to_code(config): cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) + if not config[CONF_AUTO_START]: + cg.add(var.set_auto_start(False)) + for injection in config[CONF_INJECTIONS]: rx_data = injection[CONF_INJECT_RX] delay_ms = injection[CONF_DELAY] diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index a4a1c41234c..83a13793be7 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -16,17 +16,21 @@ void MockUartComponent::setup() { } void MockUartComponent::loop() { - uint32_t now = App.get_loop_component_start_time(); - - // Initialize scenario start time on first loop() call, after all components have - // finished setup(). This prevents injection delays from being consumed during setup. if (!this->loop_started_) { this->loop_started_ = true; - this->scenario_start_ms_ = now; - this->cumulative_delay_ms_ = 0; - ESP_LOGD(TAG, "Scenario started at %u ms", now); + if (this->auto_start_) { + this->start_scenario(); + } else { + ESP_LOGD(TAG, "Scenario waiting for manual start"); + } } + if (!this->scenario_active_) { + return; + } + + uint32_t now = App.get_loop_component_start_time(); + // Process at most ONE timed injection per loop iteration. // This ensures each injection is in a separate loop cycle, giving the consuming // component (e.g., LD2410) a chance to process each batch independently. @@ -50,6 +54,19 @@ void MockUartComponent::loop() { } } +void MockUartComponent::start_scenario() { + uint32_t now = App.get_loop_component_start_time(); + this->scenario_active_ = true; + this->scenario_start_ms_ = now; + this->cumulative_delay_ms_ = 0; + this->injection_index_ = 0; + this->tx_buffer_.clear(); + for (auto &periodic : this->periodic_rx_) { + periodic.last_inject_ms = now; + } + ESP_LOGD(TAG, "Scenario started at %u ms", now); +} + void MockUartComponent::dump_config() { ESP_LOGCONFIG(TAG, "Mock UART Component:\n" @@ -78,10 +95,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { } #endif - this->try_match_response_(); + if (this->scenario_active_) { + this->try_match_response_(); + } // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. - if (this->tx_hook_) { + if (this->tx_hook_ && this->scenario_active_) { std::vector buf(data, data + len); this->tx_hook_(buf); } diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 5bbc3c1bf60..b721512f96c 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -37,6 +37,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void add_response(const std::vector &expect_tx, const std::vector &inject_rx); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); + void start_scenario(); + void set_auto_start(bool auto_start) { this->auto_start_ = auto_start; } void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); @@ -55,6 +57,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { uint32_t scenario_start_ms_{0}; uint32_t cumulative_delay_ms_{0}; bool loop_started_{false}; + bool auto_start_{true}; + bool scenario_active_{false}; // TX-triggered responses struct Response { diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml index 9a814682630..59838b0599c 100644 --- a/tests/integration/fixtures/uart_mock_ld2410.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -20,6 +20,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2410 normal mode data frame - happy path # The buffer is clean at this point, so this frame should parse correctly. @@ -143,3 +144,10 @@ binary_sensor: name: "Has Moving Target" has_still_target: name: "Has Still Target" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml index 3b730fc1f8b..4625ae85115 100644 --- a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -19,6 +19,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2410 engineering mode data frame # Captured from a real Screek Human Presence Sensor 1U with LD2410 firmware 2.4.x @@ -154,3 +155,10 @@ binary_sensor: name: "Has Still Target" out_pin_presence_status: name: "Out Pin Presence" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml index a502f36a253..9cf9d6bb873 100644 --- a/tests/integration/fixtures/uart_mock_ld2412.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -20,6 +20,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2412 normal mode data frame - happy path # The buffer is clean at this point, so this frame should parse correctly. @@ -169,3 +170,10 @@ binary_sensor: name: "Has Still Target" filters: - settle: 50ms + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index 3c669fc9a9d..103dbed132f 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -19,6 +19,7 @@ uart: uart_mock: id: mock_uart baud_rate: 256000 + auto_start: false injections: # Phase 1 (t=100ms): Valid LD2412 engineering mode data frame # @@ -211,3 +212,10 @@ binary_sensor: name: "Has Still Target" filters: - settle: 50ms + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 89b9b91861f..0a3492a0d2f 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -22,6 +22,7 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + auto_start: false debug: responses: - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 @@ -38,3 +39,10 @@ sensor: name: "basic_register" address: 0x03 register_type: holding + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index cd485ca3944..c4e29e5fe8d 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -22,6 +22,7 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + auto_start: false debug: on_tx: - then: @@ -52,3 +53,10 @@ sensor: phase_a: voltage: name: sdm_voltage + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index b649056f2ba..e8c2cc5e663 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -3,10 +3,17 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging from typing import TypeVar -from aioesphomeapi import ButtonInfo, EntityInfo, EntityState +from aioesphomeapi import ( + BinarySensorState, + ButtonInfo, + EntityInfo, + EntityState, + SensorState, +) _LOGGER = logging.getLogger(__name__) @@ -234,3 +241,90 @@ class InitialStateHelper: asyncio.TimeoutError: If initial states aren't received within timeout """ await asyncio.wait_for(self._initial_states_received, timeout=timeout) + + +class SensorStateCollector: + """Collects sensor and binary sensor state updates and provides wait helpers. + + Usage: + collector = SensorStateCollector( + sensor_names=["moving_distance", "still_distance"], + binary_sensor_names=["has_target"], + ) + # Use collector.on_state as the callback (or wrap it) + client.subscribe_states(helper.on_state_wrapper(collector.on_state)) + + # Wait for all sensors to have at least one value + await collector.wait_for_all(timeout=3.0) + + # Access collected states + assert collector.sensor_states["moving_distance"][0] == approx(100.0) + """ + + def __init__( + self, + sensor_names: list[str], + binary_sensor_names: list[str] | None = None, + entities: list[EntityInfo] | None = None, + ) -> None: + self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} + self.binary_states: dict[str, list[bool]] = { + name: [] for name in (binary_sensor_names or []) + } + self._key_to_sensor: dict[int, str] = {} + self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = [] + + if entities is not None: + self.build_key_mapping(entities) + + def build_key_mapping(self, entities: list[EntityInfo]) -> None: + """Build key-to-name mapping from entities. Sorted by descending length.""" + all_names = list(self.sensor_states.keys()) + list(self.binary_states.keys()) + all_names.sort(key=len, reverse=True) + self._key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + def on_state(self, state: EntityState) -> None: + """Process a state update.""" + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.sensor_states: + self.sensor_states[sensor_name].append(state.state) + self._check_waiters() + elif isinstance(state, BinarySensorState): + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.binary_states: + self.binary_states[sensor_name].append(state.state) + self._check_waiters() + + def _check_waiters(self) -> None: + """Check all pending waiters and resolve any whose condition is met.""" + for condition, future in self._waiters: + if not future.done() and condition(): + future.set_result(True) + + def _all_have_values(self) -> bool: + """Check if all sensor and binary sensor lists have at least one value.""" + return all(len(v) >= 1 for v in self.sensor_states.values()) and all( + len(v) >= 1 for v in self.binary_states.values() + ) + + async def wait_for_all(self, timeout: float = 3.0) -> None: + """Wait until all sensors and binary sensors have at least one value.""" + if self._all_have_values(): + return + future: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + self._waiters.append((self._all_have_values, future)) + await asyncio.wait_for(future, timeout=timeout) + + def add_waiter(self, condition: Callable[[], bool]) -> asyncio.Future[bool]: + """Add a custom waiter that resolves when condition returns True. + + Returns: + A future that resolves when the condition is met. + """ + future: asyncio.Future[bool] = asyncio.get_running_loop().create_future() + if condition(): + future.set_result(True) + else: + self._waiters.append((condition, future)) + return future diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py index e01d6ff8e82..ce0e1bb7ec2 100644 --- a/tests/integration/test_uart_mock_ld2410.py +++ b/tests/integration/test_uart_mock_ld2410.py @@ -21,16 +21,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import ( - BinarySensorInfo, - BinarySensorState, - EntityState, - SensorInfo, - SensorState, -) +from aioesphomeapi import ButtonInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -64,100 +58,65 @@ async def test_uart_mock_ld2410( if "uart_mock" in line and "TX " in line: tx_log_lines.append(line) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see recovery frame values - recovery_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is the recovery frame (moving_distance = 50) - if ( - sensor_name == "moving_distance" - and state.state == pytest.approx(50.0) - and not recovery_received.done() - ): - recovery_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) async with ( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) # Set up initial state helper initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 values are in the initial states (swallowed by InitialStateHelper). - # Verify them via initial_states dict. - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(100.0), ( - f"Initial moving distance should be 100, got {initial_moving.state}" - ) + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(120.0), ( - f"Initial still distance should be 120, got {initial_still.state}" - ) + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) - moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) - assert moving_energy_entity is not None - initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) - assert initial_me is not None and isinstance(initial_me, SensorState) - assert initial_me.state == pytest.approx(50.0), ( - f"Initial moving energy should be 50, got {initial_me.state}" - ) - - still_energy_entity = find_entity(entities, "still_energy", SensorInfo) - assert still_energy_entity is not None - initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) - assert initial_se is not None and isinstance(initial_se, SensorState) - assert initial_se.state == pytest.approx(25.0), ( - f"Initial still energy should be 25, got {initial_se.state}" - ) - - detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) - assert detect_dist_entity is not None - initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) - assert initial_dd is not None and isinstance(initial_dd, SensorState) - assert initial_dd.state == pytest.approx(300.0), ( - f"Initial detection distance should be 300, got {initial_dd.state}" - ) + # Phase 1 values: moving=100, still=120, energy=50/25, detect=300 + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(120.0) + assert collector.sensor_states["moving_energy"][0] == pytest.approx(50.0) + assert collector.sensor_states["still_energy"][0] == pytest.approx(25.0) + assert collector.sensor_states["detection_distance"][0] == pytest.approx(300.0) # Wait for the recovery frame (Phase 5) to be parsed # This proves the component survived garbage + truncated + overflow @@ -165,12 +124,8 @@ async def test_uart_mock_ld2410( await asyncio.wait_for(recovery_received, timeout=15.0) except TimeoutError: pytest.fail( - f"Timeout waiting for recovery frame. Received sensor states:\n" - f" moving_distance: {sensor_states['moving_distance']}\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_energy: {sensor_states['moving_energy']}\n" - f" still_energy: {sensor_states['still_energy']}\n" - f" detection_distance: {sensor_states['detection_distance']}" + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" ) # Verify overflow warning was logged @@ -183,67 +138,36 @@ async def test_uart_mock_ld2410( # A5 (MAC), AB (distance res), AE (light), 61 (params), FE (config off) assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" tx_data = " ".join(tx_log_lines) - # Verify command frame header appears (FD:FC:FB:FA) assert "FD:FC:FB:FA" in tx_data, ( "Expected LD2410 command frame header FD:FC:FB:FA in TX log" ) - # Verify command frame footer appears (04:03:02:01) assert "04:03:02:01" in tx_data, ( "Expected LD2410 command frame footer 04:03:02:01 in TX log" ) - # Recovery frame values (Phase 5, after overflow) - assert len(sensor_states["moving_distance"]) >= 1, ( - f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" - ) - # Find the recovery value (moving_distance = 50) - recovery_values = [ - v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) - ] - assert len(recovery_values) >= 1, ( - f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" - ) - # Recovery frame: moving=50, still=75, energy=100/80, detect=127 recovery_idx = next( i - for i, v in enumerate(sensor_states["moving_distance"]) + for i, v in enumerate(collector.sensor_states["moving_distance"]) if v == pytest.approx(50.0) ) - assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( - f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( + 75.0 ) - assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( - f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( + 100.0 ) - assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( - f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" - ) - assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( - 127.0 - ), ( - f"Recovery detection distance should be 127, got {sensor_states['detection_distance'][recovery_idx]}" + assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( + 80.0 ) + assert collector.sensor_states["detection_distance"][ + recovery_idx + ] == pytest.approx(127.0) - # Verify binary sensors detected targets - # Binary sensors could be in initial states or forwarded states - has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) - assert has_target_entity is not None - initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) - assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) - assert initial_ht.state is True, "Has target should be True" - - has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) - assert has_moving_entity is not None - initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) - assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) - assert initial_hm.state is True, "Has moving target should be True" - - has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) - assert has_still_entity is not None - initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) - assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) - assert initial_hs.state is True, "Has still target should be True" + # Verify binary sensors detected targets (from Phase 1 frame) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True @pytest.mark.asyncio @@ -260,133 +184,82 @@ async def test_uart_mock_ld2410_engineering( "EXTERNAL_COMPONENT_PATH", external_components_path ) - loop = asyncio.get_running_loop() + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_1_move_energy", + "gate_2_move_energy", + "gate_0_still_energy", + "gate_1_still_energy", + "gate_2_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + "out_pin_presence", + ], + ) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - "light": [], - "gate_0_move_energy": [], - "gate_1_move_energy": [], - "gate_2_move_energy": [], - "gate_0_still_energy": [], - "gate_1_still_energy": [], - "gate_2_still_energy": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - "out_pin_presence": [], - } - - # Signal when we see Phase 3 frame (still_distance = 291) - phase3_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "still_distance" - and state.state == pytest.approx(291.0) - and not phase3_received.done() - ): - phase3_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + # Signal when we see Phase 3 frame values + phase3_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["still_distance"] + ) async with ( run_compiled(yaml_config), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 initial values (engineering mode frame): + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values (engineering mode frame): # moving=30, energy=100, still=30, energy=100, detect=0 - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(30.0), ( - f"Initial moving distance should be 30, got {initial_moving.state}" - ) - - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(30.0), ( - f"Initial still distance should be 30, got {initial_still.state}" - ) - - # Verify engineering mode sensors from initial state - # Gate 0 moving energy = 0x64 = 100 - gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) - assert gate0_move_entity is not None - initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) - assert initial_g0m is not None and isinstance(initial_g0m, SensorState) - assert initial_g0m.state == pytest.approx(100.0), ( - f"Gate 0 move energy should be 100, got {initial_g0m.state}" - ) - - # Gate 1 moving energy = 0x41 = 65 - gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) - assert gate1_move_entity is not None - initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) - assert initial_g1m is not None and isinstance(initial_g1m, SensorState) - assert initial_g1m.state == pytest.approx(65.0), ( - f"Gate 1 move energy should be 65, got {initial_g1m.state}" - ) - - # Light sensor = 0x57 = 87 - light_entity = find_entity(entities, "light", SensorInfo) - assert light_entity is not None - initial_light = initial_state_helper.initial_states.get(light_entity.key) - assert initial_light is not None and isinstance(initial_light, SensorState) - assert initial_light.state == pytest.approx(87.0), ( - f"Light sensor should be 87, got {initial_light.state}" - ) - - # Out pin presence = 0x01 = True - out_pin_entity = find_entity(entities, "out_pin_presence", BinarySensorInfo) - assert out_pin_entity is not None - initial_out = initial_state_helper.initial_states.get(out_pin_entity.key) - assert initial_out is not None and isinstance(initial_out, BinarySensorState) - assert initial_out.state is True, "Out pin presence should be True" + assert collector.sensor_states["moving_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["gate_1_move_energy"][0] == pytest.approx(65.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) + assert collector.binary_states["out_pin_presence"][0] is True # Wait for Phase 3 frame (still_distance = 291cm, multi-byte) try: await asyncio.wait_for(phase3_received, timeout=15.0) except TimeoutError: pytest.fail( - f"Timeout waiting for Phase 3 frame. Received sensor states:\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_distance: {sensor_states['moving_distance']}" + f"Timeout waiting for Phase 3 frame. Received:\n" + f" still_distance: {collector.sensor_states['still_distance']}" ) - # Phase 3: still distance = 0x0123 = 291cm (multi-byte distance test) - phase3_still = [ - v for v in sensor_states["still_distance"] if v == pytest.approx(291.0) - ] - assert len(phase3_still) >= 1, ( - f"Expected still_distance=291, got: {sensor_states['still_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["still_distance"] diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index cf7324ceed3..a964ba00738 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -21,16 +21,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import ( - BinarySensorInfo, - BinarySensorState, - EntityState, - SensorInfo, - SensorState, -) +from aioesphomeapi import ButtonInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -64,104 +58,65 @@ async def test_uart_mock_ld2412( if "uart_mock" in line and "TX " in line: tx_log_lines.append(line) - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see recovery frame values - recovery_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is the recovery frame (moving_distance = 50) - if ( - sensor_name == "moving_distance" - and state.state == pytest.approx(50.0) - and not recovery_received.done() - ): - recovery_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) async with ( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - # Sort by descending length to avoid substring collisions - # (e.g., "still_energy" matching "gate_0_still_energy") - all_names.sort(key=len, reverse=True) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) # Set up initial state helper initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 values are in the initial states (swallowed by InitialStateHelper). - # Verify them via initial_states dict. - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(100.0), ( - f"Initial moving distance should be 100, got {initial_moving.state}" - ) + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(120.0), ( - f"Initial still distance should be 120, got {initial_still.state}" - ) + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) - moving_energy_entity = find_entity(entities, "moving_energy", SensorInfo) - assert moving_energy_entity is not None - initial_me = initial_state_helper.initial_states.get(moving_energy_entity.key) - assert initial_me is not None and isinstance(initial_me, SensorState) - assert initial_me.state == pytest.approx(50.0), ( - f"Initial moving energy should be 50, got {initial_me.state}" - ) - - still_energy_entity = find_entity(entities, "still_energy", SensorInfo) - assert still_energy_entity is not None - initial_se = initial_state_helper.initial_states.get(still_energy_entity.key) - assert initial_se is not None and isinstance(initial_se, SensorState) - assert initial_se.state == pytest.approx(25.0), ( - f"Initial still energy should be 25, got {initial_se.state}" - ) - - # LD2412 detection_distance = moving_distance when MOVE_BITMASK is set - detect_dist_entity = find_entity(entities, "detection_distance", SensorInfo) - assert detect_dist_entity is not None - initial_dd = initial_state_helper.initial_states.get(detect_dist_entity.key) - assert initial_dd is not None and isinstance(initial_dd, SensorState) - assert initial_dd.state == pytest.approx(100.0), ( - f"Initial detection distance should be 100, got {initial_dd.state}" - ) + # Phase 1 values: moving=100, still=120, energy=50/25, detect=100 + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(120.0) + assert collector.sensor_states["moving_energy"][0] == pytest.approx(50.0) + assert collector.sensor_states["still_energy"][0] == pytest.approx(25.0) + assert collector.sensor_states["detection_distance"][0] == pytest.approx(100.0) # Wait for the recovery frame (Phase 5) to be parsed # This proves the component survived garbage + truncated + overflow @@ -169,12 +124,8 @@ async def test_uart_mock_ld2412( await asyncio.wait_for(recovery_received, timeout=3.0) except TimeoutError: pytest.fail( - f"Timeout waiting for recovery frame. Received sensor states:\n" - f" moving_distance: {sensor_states['moving_distance']}\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_energy: {sensor_states['moving_energy']}\n" - f" still_energy: {sensor_states['still_energy']}\n" - f" detection_distance: {sensor_states['detection_distance']}" + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" ) # Verify overflow warning was logged @@ -185,67 +136,36 @@ async def test_uart_mock_ld2412( # Verify LD2412 sent setup commands (TX logging) assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" tx_data = " ".join(tx_log_lines) - # Verify command frame header appears (FD:FC:FB:FA) assert "FD:FC:FB:FA" in tx_data, ( "Expected LD2412 command frame header FD:FC:FB:FA in TX log" ) - # Verify command frame footer appears (04:03:02:01) assert "04:03:02:01" in tx_data, ( "Expected LD2412 command frame footer 04:03:02:01 in TX log" ) - # Recovery frame values (Phase 5, after overflow) - assert len(sensor_states["moving_distance"]) >= 1, ( - f"Expected recovery moving_distance, got: {sensor_states['moving_distance']}" - ) - # Find the recovery value (moving_distance = 50) - recovery_values = [ - v for v in sensor_states["moving_distance"] if v == pytest.approx(50.0) - ] - assert len(recovery_values) >= 1, ( - f"Expected moving_distance=50 in recovery, got: {sensor_states['moving_distance']}" - ) - # Recovery frame: moving=50, still=75, energy=100/80, detect=50 recovery_idx = next( i - for i, v in enumerate(sensor_states["moving_distance"]) + for i, v in enumerate(collector.sensor_states["moving_distance"]) if v == pytest.approx(50.0) ) - assert sensor_states["still_distance"][recovery_idx] == pytest.approx(75.0), ( - f"Recovery still distance should be 75, got {sensor_states['still_distance'][recovery_idx]}" + assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( + 75.0 ) - assert sensor_states["moving_energy"][recovery_idx] == pytest.approx(100.0), ( - f"Recovery moving energy should be 100, got {sensor_states['moving_energy'][recovery_idx]}" + assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( + 100.0 ) - assert sensor_states["still_energy"][recovery_idx] == pytest.approx(80.0), ( - f"Recovery still energy should be 80, got {sensor_states['still_energy'][recovery_idx]}" - ) - # LD2412 detection_distance = moving_distance when MOVE_BITMASK set - assert sensor_states["detection_distance"][recovery_idx] == pytest.approx( - 50.0 - ), ( - f"Recovery detection distance should be 50, got {sensor_states['detection_distance'][recovery_idx]}" + assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( + 80.0 ) + assert collector.sensor_states["detection_distance"][ + recovery_idx + ] == pytest.approx(50.0) - # Verify binary sensors detected targets - has_target_entity = find_entity(entities, "has_target", BinarySensorInfo) - assert has_target_entity is not None - initial_ht = initial_state_helper.initial_states.get(has_target_entity.key) - assert initial_ht is not None and isinstance(initial_ht, BinarySensorState) - assert initial_ht.state is True, "Has target should be True" - - has_moving_entity = find_entity(entities, "has_moving_target", BinarySensorInfo) - assert has_moving_entity is not None - initial_hm = initial_state_helper.initial_states.get(has_moving_entity.key) - assert initial_hm is not None and isinstance(initial_hm, BinarySensorState) - assert initial_hm.state is True, "Has moving target should be True" - - has_still_entity = find_entity(entities, "has_still_target", BinarySensorInfo) - assert has_still_entity is not None - initial_hs = initial_state_helper.initial_states.get(has_still_entity.key) - assert initial_hs is not None and isinstance(initial_hs, BinarySensorState) - assert initial_hs.state is True, "Has still target should be True" + # Verify binary sensors detected targets (from Phase 1 frame) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True @pytest.mark.asyncio @@ -262,120 +182,75 @@ async def test_uart_mock_ld2412_engineering( "EXTERNAL_COMPONENT_PATH", external_components_path ) - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "moving_distance": [], - "still_distance": [], - "moving_energy": [], - "still_energy": [], - "detection_distance": [], - "light": [], - "gate_0_move_energy": [], - "gate_1_move_energy": [], - "gate_2_move_energy": [], - "gate_0_still_energy": [], - "gate_1_still_energy": [], - "gate_2_still_energy": [], - } - binary_states: dict[str, list[bool]] = { - "has_target": [], - "has_moving_target": [], - "has_still_target": [], - } + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_1_move_energy", + "gate_2_move_energy", + "gate_0_still_energy", + "gate_1_still_energy", + "gate_2_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) # Signal when we see Phase 3 frame values - phase3_still_received = loop.create_future() - phase3_detect_received = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "still_distance" - and state.state == pytest.approx(291.0) - and not phase3_still_received.done() - ): - phase3_still_received.set_result(True) - if ( - sensor_name == "detection_distance" - and state.state == pytest.approx(291.0) - and not phase3_detect_received.done() - ): - phase3_detect_received.set_result(True) - elif isinstance(state, BinarySensorState): - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in binary_states: - binary_states[sensor_name].append(state.state) + phase3_still_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["still_distance"] + ) + phase3_detect_received = collector.add_waiter( + lambda: pytest.approx(291.0) in collector.sensor_states["detection_distance"] + ) async with ( run_compiled(yaml_config), api_client_connected() as client, ): entities, _ = await client.list_entities_services() - - all_names = list(sensor_states.keys()) + list(binary_states.keys()) - # Sort by descending length to avoid substring collisions - # (e.g., "still_energy" matching "gate_0_still_energy") - all_names.sort(key=len, reverse=True) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) + collector.build_key_mapping(entities) initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) try: await initial_state_helper.wait_for_initial_states() except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Phase 1 initial values (engineering mode frame): + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values (engineering mode frame): # moving=30, energy=100, still=30, energy=100, detect=30 - moving_dist_entity = find_entity(entities, "moving_distance", SensorInfo) - assert moving_dist_entity is not None - initial_moving = initial_state_helper.initial_states.get(moving_dist_entity.key) - assert initial_moving is not None and isinstance(initial_moving, SensorState) - assert initial_moving.state == pytest.approx(30.0), ( - f"Initial moving distance should be 30, got {initial_moving.state}" - ) - - still_dist_entity = find_entity(entities, "still_distance", SensorInfo) - assert still_dist_entity is not None - initial_still = initial_state_helper.initial_states.get(still_dist_entity.key) - assert initial_still is not None and isinstance(initial_still, SensorState) - assert initial_still.state == pytest.approx(30.0), ( - f"Initial still distance should be 30, got {initial_still.state}" - ) - - # Verify engineering mode sensors from initial state - # Gate 0 moving energy = 0x64 = 100 - gate0_move_entity = find_entity(entities, "gate_0_move_energy", SensorInfo) - assert gate0_move_entity is not None - initial_g0m = initial_state_helper.initial_states.get(gate0_move_entity.key) - assert initial_g0m is not None and isinstance(initial_g0m, SensorState) - assert initial_g0m.state == pytest.approx(100.0), ( - f"Gate 0 move energy should be 100, got {initial_g0m.state}" - ) - - # Gate 1 moving energy = 0x41 = 65 - gate1_move_entity = find_entity(entities, "gate_1_move_energy", SensorInfo) - assert gate1_move_entity is not None - initial_g1m = initial_state_helper.initial_states.get(gate1_move_entity.key) - assert initial_g1m is not None and isinstance(initial_g1m, SensorState) - assert initial_g1m.state == pytest.approx(65.0), ( - f"Gate 1 move energy should be 65, got {initial_g1m.state}" - ) - - # Light sensor = 0x57 = 87 - light_entity = find_entity(entities, "light", SensorInfo) - assert light_entity is not None - initial_light = initial_state_helper.initial_states.get(light_entity.key) - assert initial_light is not None and isinstance(initial_light, SensorState) - assert initial_light.state == pytest.approx(87.0), ( - f"Light sensor should be 87, got {initial_light.state}" - ) + assert collector.sensor_states["moving_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["still_distance"][0] == pytest.approx(30.0) + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["gate_1_move_energy"][0] == pytest.approx(65.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) # Wait for Phase 3 frame: still_distance = 291cm (multi-byte) try: @@ -383,25 +258,18 @@ async def test_uart_mock_ld2412_engineering( except TimeoutError: pytest.fail( f"Timeout waiting for Phase 3 still_distance. Received:\n" - f" still_distance: {sensor_states['still_distance']}\n" - f" moving_distance: {sensor_states['moving_distance']}" + f" still_distance: {collector.sensor_states['still_distance']}" ) - assert pytest.approx(291.0) in sensor_states["still_distance"], ( - f"Expected still_distance=291, got: {sensor_states['still_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["still_distance"] # Wait for Phase 3: detection_distance = 291 (still-only target) - # target_state=0x02 so LD2412 uses still_distance for detection_distance. - # The throttle_with_priority filter may delay this value. try: await asyncio.wait_for(phase3_detect_received, timeout=3.0) except TimeoutError: pytest.fail( - f"Timeout waiting for detection_distance=291 (still-only target). " - f"Received: {sensor_states['detection_distance']}" + f"Timeout waiting for detection_distance=291. " + f"Received: {collector.sensor_states['detection_distance']}" ) - assert pytest.approx(291.0) in sensor_states["detection_distance"], ( - f"Expected detection_distance=291, got: {sensor_states['detection_distance']}" - ) + assert pytest.approx(291.0) in collector.sensor_states["detection_distance"] diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 309cb56dc99..bf3c0697502 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -12,10 +12,10 @@ from __future__ import annotations import asyncio from pathlib import Path -from aioesphomeapi import EntityState, SensorState +from aioesphomeapi import ButtonInfo, EntityState, SensorState import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction @@ -74,6 +74,11 @@ async def test_uart_mock_modbus( except TimeoutError: pytest.fail("Timeout waiting for initial states") + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + # Wait for basic register to be updated with successful parse try: await asyncio.wait_for(basic_register_changed, timeout=15.0) @@ -143,6 +148,11 @@ async def test_uart_mock_modbus_timing( except TimeoutError: pytest.fail("Timeout waiting for initial states") + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + # Wait for voltage to be updated with successful parse try: await asyncio.wait_for(voltage_changed, timeout=15.0) From 0ff5270632f59a8fd4d7ed25cd71c2ce92ae5b07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Mar 2026 16:57:19 -1000 Subject: [PATCH 0386/2056] [ci] Fix codeowner approval label workflow for fork PRs (#14490) Co-authored-by: Claude Opus 4.6 --- .github/scripts/codeowners.js | 88 +++++++++- .../codeowner-approved-label-update.yml | 95 +++++++++++ .../workflows/codeowner-approved-label.yml | 153 +++++------------- 3 files changed, 217 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/codeowner-approved-label-update.yml diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index 9a10391699e..5d69c11b1a2 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -2,7 +2,7 @@ // // Used by: // - codeowner-review-request.yml -// - codeowner-approved-label.yml +// - codeowner-approved-label.yml + codeowner-approved-label-update.yml // - auto-label-pr/detectors.js (detectCodeOwner) /** @@ -133,11 +133,95 @@ function loadCodeowners(repoRoot = '.') { return parseCodeowners(content); } +/** Possible label actions returned by determineLabelAction. */ +const LabelAction = Object.freeze({ + ADD: 'add', + REMOVE: 'remove', + NONE: 'none', +}); + +/** + * Determine what label action is needed for a PR based on codeowner approvals. + * + * Checks changed files against CODEOWNERS patterns, reviews, and current labels + * to decide if the label should be added, removed, or left unchanged. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {number} pr_number - pull request number + * @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners + * @param {string} labelName - label to manage + * @returns {Promise} + */ +async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) { + // Get the list of changed files in this PR + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { owner, repo, pull_number: pr_number } + ); + + const changedFiles = prFiles.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found'); + return LabelAction.NONE; + } + + // Get effective owners using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const componentCodeowners = effective.users; + + console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); + + // Get current labels + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number: pr_number + }); + const hasLabel = currentLabels.some(label => label.name === labelName); + + if (componentCodeowners.size === 0) { + console.log('No component-specific codeowners found'); + return hasLabel ? LabelAction.REMOVE : LabelAction.NONE; + } + + // Get all reviews and find latest per user + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { owner, repo, pull_number: pr_number } + ); + + const latestReviewByUser = new Map(); + for (const review of reviews) { + if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; + latestReviewByUser.set(review.user.login, review); + } + + // Check if any component-specific codeowner has an active approval + let hasCodeownerApproval = false; + for (const [login, review] of latestReviewByUser) { + if (review.state === 'APPROVED' && componentCodeowners.has(login)) { + console.log(`Codeowner '${login}' has approved`); + hasCodeownerApproval = true; + break; + } + } + + if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD; + if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE; + + console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + return LabelAction.NONE; +} + module.exports = { globToRegex, parseCodeowners, fetchCodeowners, loadCodeowners, classifyOwners, - getEffectiveOwners + getEffectiveOwners, + LabelAction, + determineLabelAction }; diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml new file mode 100644 index 00000000000..9168cce1d6b --- /dev/null +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -0,0 +1,95 @@ +# Fallback for fork PRs: phase 1 (codeowner-approved-label.yml) handles +# non-fork PRs directly but can't write labels on fork PRs (read-only token). +# This workflow re-determines the action and applies it if needed. + +name: Codeowner Approved Label Update + +on: + workflow_run: + workflows: ["Codeowner Approved Label"] + types: [completed] + +permissions: + issues: write + pull-requests: read + contents: read + +jobs: + update-label: + name: Run + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request_review' + runs-on: ubuntu-latest + steps: + - name: Get PR details + id: pr + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + REPO: ${{ github.repository }} + run: | + pr_data=$(gh pr list --repo "$REPO" --state open --search "$HEAD_SHA" \ + --json number,baseRefName --jq '.[0] // empty') + + if [ -z "$pr_data" ]; then + echo "No open PR found for SHA $HEAD_SHA, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_number=$(echo "$pr_data" | jq -r '.number') + base_ref=$(echo "$pr_data" | jq -r '.baseRefName') + + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" + echo "Found PR #$pr_number targeting $base_ref" + + - name: Checkout base repository + if: steps.pr.outputs.skip != 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ github.repository }} + ref: ${{ steps.pr.outputs.base_ref }} + sparse-checkout: | + .github/scripts/codeowners.js + CODEOWNERS + + - name: Update label + if: steps.pr.outputs.skip != 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + with: + script: | + const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = parseInt(process.env.PR_NUMBER, 10); + const LABEL_NAME = 'code-owner-approved'; + + console.log(`Processing PR #${pr_number} for codeowner approval label`); + + const codeownersPatterns = loadCodeowners(); + const action = await determineLabelAction( + github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME + ); + + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr_number, name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); + } catch (error) { + if (error.status !== 404) throw error; + } + } else { + console.log('No label change needed'); + } diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml index 200f18f5448..12199bd0b04 100644 --- a/.github/workflows/codeowner-approved-label.yml +++ b/.github/workflows/codeowner-approved-label.yml @@ -1,9 +1,9 @@ -# This workflow adds/removes a 'code-owner-approved' label when a -# component-specific codeowner approves (or dismisses) a PR. -# This helps maintainers prioritize PRs that have codeowner sign-off. +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. # -# Only component-specific codeowners count — the catch-all @esphome/core -# team is excluded so the label reflects domain-expert approval. +# Handles non-fork PRs directly. For fork PRs the GITHUB_TOKEN is read-only, +# so label writes are deferred to codeowner-approved-label-update.yml which +# triggers via workflow_run with write permissions. name: Codeowner Approved Label @@ -26,134 +26,53 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: | + .github/scripts/codeowners.js + CODEOWNERS - name: Check codeowner approval and update label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | - const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); const owner = context.repo.owner; const repo = context.repo.repo; - const pr_number = context.payload.pull_request.number; + const pr_number = parseInt(process.env.PR_NUMBER, 10); const LABEL_NAME = 'code-owner-approved'; console.log(`Processing PR #${pr_number} for codeowner approval label`); + const codeownersPatterns = loadCodeowners(); + const action = await determineLabelAction( + github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME + ); + + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + try { - // Get the list of changed files in this PR (with pagination) - const prFiles = await github.paginate( - github.rest.pulls.listFiles, - { - owner, - repo, - pull_number: pr_number - } - ); - - const changedFiles = prFiles.map(file => file.filename); - console.log(`Found ${changedFiles.length} changed files`); - - if (changedFiles.length === 0) { - console.log('No changed files found, skipping'); - return; - } - - // Parse CODEOWNERS from the checked-out base branch - const codeownersPatterns = loadCodeowners(); - - // Get effective owners using last-match-wins semantics - const effective = getEffectiveOwners(changedFiles, codeownersPatterns); - - // Only keep individual component-specific codeowners (exclude teams) - const componentCodeowners = effective.users; - - console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); - - if (componentCodeowners.size === 0) { - console.log('No component-specific codeowners found for changed files'); - // Remove label if present since there are no component codeowners - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pr_number, - name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`); - } catch (error) { - if (error.status !== 404) { - console.log(`Failed to remove label: ${error.message}`); - } - } - return; - } - - // Get all reviews on the PR - const reviews = await github.paginate( - github.rest.pulls.listReviews, - { - owner, - repo, - pull_number: pr_number - } - ); - - // Get the latest review per user (reviews are returned chronologically) - const latestReviewByUser = new Map(); - for (const review of reviews) { - // Skip bot reviews and comment-only reviews - if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; - latestReviewByUser.set(review.user.login, review); - } - - // Check if any component-specific codeowner has an active approval - let hasCodeownerApproval = false; - for (const [login, review] of latestReviewByUser) { - if (review.state === 'APPROVED' && componentCodeowners.has(login)) { - console.log(`Codeowner '${login}' has approved`); - hasCodeownerApproval = true; - break; - } - } - - // Get current labels to check if label is already present - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: pr_number - }); - const hasLabel = currentLabels.some(label => label.name === LABEL_NAME); - - if (hasCodeownerApproval && !hasLabel) { - // Add the label + if (action === LabelAction.ADD) { await github.rest.issues.addLabels({ - owner, - repo, - issue_number: pr_number, - labels: [LABEL_NAME] + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] }); console.log(`Added '${LABEL_NAME}' label`); - } else if (!hasCodeownerApproval && hasLabel) { - // Remove the label - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pr_number, - name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label`); - } catch (error) { - if (error.status !== 404) { - console.log(`Failed to remove label: ${error.message}`); - } - } - } else { - console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + } else if (action === LabelAction.REMOVE) { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr_number, name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); } - } catch (error) { - console.error(error); - core.setFailed(`Failed to process codeowner approval label: ${error.message}`); + if (error.status === 403) { + console.log('Fork PR: deferring label write to phase 2 workflow'); + } else if (error.status === 404) { + console.log('Label already removed'); + } else { + throw error; + } } From f5c37bf486d1a8905b7518478fec23a2e6e3b9ae Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:24:01 +1100 Subject: [PATCH 0387/2056] [packet_transport] Minimise heap allocations (#14482) --- .../packet_transport/packet_transport.cpp | 79 ++-- .../packet_transport/packet_transport.h | 19 +- script/cpp_unit_test.py | 44 +- tests/components/packet_transport/common.h | 98 ++++ .../components/packet_transport/cpp_test.yaml | 11 + .../packet_transport_test.cpp | 445 ++++++++++++++++++ 6 files changed, 659 insertions(+), 37 deletions(-) create mode 100644 tests/components/packet_transport/common.h create mode 100644 tests/components/packet_transport/cpp_test.yaml create mode 100644 tests/components/packet_transport/packet_transport_test.cpp diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index d2c59200017..f241cc61142 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "packet_transport.h" +#include + #include "esphome/components/xxtea/xxtea.h" namespace esphome { @@ -77,7 +79,7 @@ enum DecodeResult { DECODE_EMPTY, }; -static const size_t MAX_PING_KEYS = 4; +static constexpr size_t MAX_PING_KEYS = 4; static inline void add(std::vector &vec, uint32_t data) { vec.push_back(data & 0xFF); @@ -168,7 +170,7 @@ class PacketDecoder { return true; } - bool decrypt(const uint32_t *key) { + bool decrypt(const uint32_t *key) const { if (this->get_remaining_size() % 4 != 0) { return false; } @@ -249,9 +251,9 @@ void PacketTransport::init_data_() { } else { add(this->data_, DATA_KEY); } - for (const auto &pkey : this->ping_keys_) { + for (auto &value : this->ping_keys_ | std::views::values) { add(this->data_, PING_KEY); - add(this->data_, pkey.second); + add(this->data_, value); } } @@ -331,7 +333,7 @@ void PacketTransport::update() { auto now = millis() / 1000; if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { this->resend_ping_key_ = this->ping_pong_enable_; - ESP_LOGV(TAG, "Ping request, age %u", now - this->last_key_time_); + ESP_LOGV(TAG, "Ping request, age %" PRIu32, now - this->last_key_time_); this->last_key_time_ = now; } for (const auto &provider : this->providers_) { @@ -339,24 +341,32 @@ void PacketTransport::update() { if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) { - ESP_LOGI(TAG, "Ping status for %s timeout at %u with age %u", provider.first.c_str(), now, key_response_age); + ESP_LOGI(TAG, "Ping status for %s timeout at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, + key_response_age); provider.second.status_sensor->publish_state(false); } #endif #ifdef USE_SENSOR - for (auto &sensor : this->remote_sensors_[provider.first]) { - sensor.second->publish_state(NAN); + auto it = this->remote_sensors_.find(provider.first); + if (it != this->remote_sensors_.end()) { + for (auto &val : it->second | std::views::values) { + val->publish_state(NAN); + } } #endif #ifdef USE_BINARY_SENSOR - for (auto &sensor : this->remote_binary_sensors_[provider.first]) { - sensor.second->invalidate_state(); + auto bs_it = this->remote_binary_sensors_.find(provider.first); + if (bs_it != this->remote_binary_sensors_.end()) { + for (auto &val : bs_it->second | std::views::values) { + val->invalidate_state(); + } } #endif } else { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && !provider.second.status_sensor->state) { - ESP_LOGI(TAG, "Ping status for %s restored at %u with age %u", provider.first.c_str(), now, key_response_age); + ESP_LOGI(TAG, "Ping status for %s restored at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, + key_response_age); provider.second.status_sensor->publish_state(true); } #endif @@ -367,11 +377,16 @@ void PacketTransport::update() { void PacketTransport::add_key_(const char *name, uint32_t key) { if (!this->is_encrypted_()) return; - if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { - ESP_LOGW(TAG, "Ping key from %s discarded", name); - return; + auto it = this->ping_keys_.find(name); + if (it == this->ping_keys_.end()) { + if (this->ping_keys_.size() == MAX_PING_KEYS) { + ESP_LOGW(TAG, "Ping key from %s discarded", name); + return; + } + this->ping_keys_.emplace(name, key); // allocates string key once only + } else { + it->second = key; // key string already exists in map, no allocation } - this->ping_keys_[name] = key; this->updated_ = true; ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); } @@ -431,17 +446,19 @@ void PacketTransport::process_(std::span data) { return; } - if (this->providers_.count(namebuf) == 0) { + auto it = this->providers_.find(namebuf); + if (it == this->providers_.end()) { ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); return; } + auto &provider = it->second; ESP_LOGV(TAG, "Found hostname %s", namebuf); #ifdef USE_SENSOR - auto &sensors = this->remote_sensors_[namebuf]; + auto &sensors = this->remote_sensors_.try_emplace(namebuf).first->second; #endif #ifdef USE_BINARY_SENSOR - auto &binary_sensors = this->remote_binary_sensors_[namebuf]; + auto &binary_sensors = this->remote_binary_sensors_.try_emplace(namebuf).first->second; #endif if (!decoder.bump_to(4)) { @@ -453,7 +470,6 @@ void PacketTransport::process_(std::span data) { return; } - auto &provider = this->providers_[namebuf]; // if encryption not used with this host, ping check is pointless since it would be easily spoofed. if (provider.encryption_key.empty()) ping_key_seen = true; @@ -495,16 +511,19 @@ void PacketTransport::process_(std::span data) { if (decoder.decode(BINARY_SENSOR_KEY, namebuf, sizeof(namebuf), byte) == DECODE_OK) { ESP_LOGV(TAG, "Got binary sensor %s %d", namebuf, byte); #ifdef USE_BINARY_SENSOR - if (binary_sensors.count(namebuf) != 0) - binary_sensors[namebuf]->publish_state(byte != 0); + auto bs = binary_sensors.find(namebuf); + if (bs != binary_sensors.end()) { + bs->second->publish_state(byte != 0); + } #endif continue; } if (decoder.decode(SENSOR_KEY, namebuf, sizeof(namebuf), rdata.u32) == DECODE_OK) { ESP_LOGV(TAG, "Got sensor %s %f", namebuf, rdata.f32); #ifdef USE_SENSOR - if (sensors.count(namebuf) != 0) - sensors[namebuf]->publish_state(rdata.f32); + auto sensor_it = sensors.find(namebuf); + if (sensor_it != sensors.end()) + sensor_it->second->publish_state(rdata.f32); #endif continue; } @@ -537,12 +556,18 @@ void PacketTransport::dump_config() { ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); #ifdef USE_SENSOR - for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); + auto rs = this->remote_sensors_.find(host.first.c_str()); + if (rs != this->remote_sensors_.end()) { + for (const auto &key : rs->second | std::views::keys) + ESP_LOGCONFIG(TAG, " Sensor: %s", key.c_str()); + } #endif #ifdef USE_BINARY_SENSOR - for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); + auto rbs = this->remote_binary_sensors_.find(host.first.c_str()); + if (rbs != this->remote_binary_sensors_.end()) { + for (const auto &key : rbs->second | std::views::keys) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", key.c_str()); + } #endif } } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index a2367442317..b3798738e2c 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -24,6 +24,9 @@ namespace esphome { namespace packet_transport { +// std::less provides allocation-free comparison with const char * +template using string_map_t = std::map>; + struct Provider { std::vector encryption_key; const char *name; @@ -79,15 +82,15 @@ class PacketTransport : public PollingComponent { #endif void add_provider(const char *hostname) { - if (this->providers_.count(hostname) == 0) { + if (!this->providers_.contains(hostname)) { Provider provider{}; provider.name = hostname; this->providers_[hostname] = provider; #ifdef USE_SENSOR - this->remote_sensors_[hostname] = std::map(); + this->remote_sensors_[hostname] = string_map_t(); #endif #ifdef USE_BINARY_SENSOR - this->remote_binary_sensors_[hostname] = std::map(); + this->remote_binary_sensors_[hostname] = string_map_t(); #endif } } @@ -139,23 +142,23 @@ class PacketTransport : public PollingComponent { #ifdef USE_SENSOR std::vector sensors_{}; - std::map> remote_sensors_{}; + string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; - std::map> remote_binary_sensors_{}; + string_map_t> remote_binary_sensors_{}; #endif - std::map providers_{}; + string_map_t providers_{}; std::vector ping_header_{}; std::vector header_{}; std::vector data_{}; - std::map ping_keys_{}; + string_map_t ping_keys_{}; const char *platform_name_{""}; void add_key_(const char *name, uint32_t key); void send_ping_pong_request_(); - inline bool is_encrypted_() { return !this->encryption_key_.empty(); } + bool is_encrypted_() const { return !this->encryption_key_.empty(); } }; } // namespace packet_transport diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 02b133060aa..b87261ab332 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -12,6 +12,7 @@ from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config from esphome.core import CORE from esphome.platformio_api import get_idedata +from esphome.yaml_util import load_yaml # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -44,6 +45,38 @@ def filter_components_without_tests(components: list[str]) -> list[str]: return filtered_components +# Name of optional per-component YAML config merged into the test build +# before validation so that platform defines (USE_SENSOR, etc.) are generated. +CPP_TEST_CONFIG_FILE = "cpp_test.yaml" + + +def load_component_test_configs(components: list[str]) -> dict: + """Load cpp_test.yaml files from test component directories. + + These configs are merged into the base test config *before* validation + so that entity registration runs during code generation, which causes + the corresponding USE_* defines to be emitted. + """ + merged: dict = {} + for component in components: + config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE + if not config_file.exists(): + continue + component_config = load_yaml(config_file) + if not component_config: + continue + for key, value in component_config.items(): + if ( + key in merged + and isinstance(merged[key], list) + and isinstance(value, list) + ): + merged[key].extend(value) + else: + merged[key] = value + return merged + + def create_test_config(config_name: str, includes: list[str]) -> dict: """Create ESPHome test configuration for C++ unit tests. @@ -115,6 +148,11 @@ def run_tests(selected_components: list[str]) -> int: config = create_test_config(config_name, includes) + # Merge component-specific test configs (e.g. sensor instances) before + # validation so that entity registration and USE_* defines work. + extra_config = load_component_test_configs(components) + config.update(extra_config) + CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" CORE.dashboard = None @@ -122,8 +160,10 @@ def run_tests(selected_components: list[str]) -> int: config = validate_config(config, {}) # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. - config.update({key: {} for key in components_with_dependencies}) + # are added to the build. Use setdefault to avoid overwriting entries that were + # already validated (e.g. sensor instances from cpp_test.yaml). + for key in components_with_dependencies: + config.setdefault(key, {}) print(f"Testing components: {', '.join(components)}") CORE.config = config diff --git a/tests/components/packet_transport/common.h b/tests/components/packet_transport/common.h new file mode 100644 index 00000000000..f8caa7bb683 --- /dev/null +++ b/tests/components/packet_transport/common.h @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include +#include +#include +#include "esphome/components/packet_transport/packet_transport.h" + +namespace esphome::packet_transport::testing { + +// Protocol constants mirrored from packet_transport.cpp for test packet construction. +static constexpr uint16_t MAGIC_NUMBER = 0x4553; +static constexpr uint16_t MAGIC_PING = 0x5048; + +// Concrete testable implementation of PacketTransport. +// Captures sent packets and exposes protected members for verification. +// +// Sensor round-trip tests require USE_SENSOR / USE_BINARY_SENSOR to be defined, +// which happens when 'sensor' and 'binary_sensor' components are in the build. +// Run with --all or include those components to enable the full test suite. +class TestablePacketTransport : public PacketTransport { + public: + using PacketTransport::add_key_; + using PacketTransport::data_; + using PacketTransport::encryption_key_; + using PacketTransport::flush_; + using PacketTransport::header_; + using PacketTransport::increment_code_; + using PacketTransport::init_data_; + using PacketTransport::is_encrypted_; + using PacketTransport::is_provider_; + using PacketTransport::name_; + using PacketTransport::ping_key_; + using PacketTransport::ping_keys_; + using PacketTransport::ping_pong_enable_; + using PacketTransport::ping_pong_recyle_time_; + using PacketTransport::process_; + using PacketTransport::providers_; + using PacketTransport::rolling_code_; + using PacketTransport::rolling_code_enable_; + using PacketTransport::send_data_; + using PacketTransport::updated_; +#ifdef USE_SENSOR + using PacketTransport::add_data_; + using PacketTransport::remote_sensors_; + using PacketTransport::sensors_; +#endif +#ifdef USE_BINARY_SENSOR + using PacketTransport::add_binary_data_; + using PacketTransport::binary_sensors_; + using PacketTransport::remote_binary_sensors_; +#endif + + // NOTE: std::vector is used here for test convenience. For production code, + // consider using StaticVector or FixedVector from esphome/core/helpers.h instead. + mutable std::vector> sent_packets; + size_t max_packet_size{512}; + bool send_enabled{true}; + + void send_packet(const std::vector &buf) const override { this->sent_packets.push_back(buf); } + size_t get_max_packet_size() override { return this->max_packet_size; } + bool should_send() override { return this->send_enabled; } + + /// Build the packet header for testing without requiring App or global_preferences. + void init_for_test(const char *name) { + this->name_ = name; + this->header_.clear(); + // MAGIC_NUMBER as uint16_t little-endian + this->header_.push_back(MAGIC_NUMBER & 0xFF); + this->header_.push_back((MAGIC_NUMBER >> 8) & 0xFF); + // Length-prefixed hostname + auto len = strlen(name); + this->header_.push_back(static_cast(len)); + for (size_t i = 0; i < len; i++) + this->header_.push_back(name[i]); + // Pad to 4-byte boundary + while (this->header_.size() & 0x3) + this->header_.push_back(0); + } +}; + +/// Build a MAGIC_PING packet for testing add_key_ / ping-pong flows. +inline std::vector build_ping_packet(const char *hostname, uint32_t key) { + std::vector packet; + packet.push_back(MAGIC_PING & 0xFF); + packet.push_back((MAGIC_PING >> 8) & 0xFF); + auto len = strlen(hostname); + packet.push_back(static_cast(len)); + for (size_t i = 0; i < len; i++) + packet.push_back(hostname[i]); + packet.push_back(key & 0xFF); + packet.push_back((key >> 8) & 0xFF); + packet.push_back((key >> 16) & 0xFF); + packet.push_back((key >> 24) & 0xFF); + return packet; +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/components/packet_transport/cpp_test.yaml b/tests/components/packet_transport/cpp_test.yaml new file mode 100644 index 00000000000..fa39df3c0ae --- /dev/null +++ b/tests/components/packet_transport/cpp_test.yaml @@ -0,0 +1,11 @@ +# Extra component configuration required by C++ unit tests. +# Loaded by cpp_unit_test.py and merged into the test build config +# before validation, so that platform defines (USE_SENSOR, etc.) are generated. + +sensor: + - platform: template + id: test_cpp_sensor + +binary_sensor: + - platform: template + id: test_cpp_binary_sensor diff --git a/tests/components/packet_transport/packet_transport_test.cpp b/tests/components/packet_transport/packet_transport_test.cpp new file mode 100644 index 00000000000..d8f11ca6072 --- /dev/null +++ b/tests/components/packet_transport/packet_transport_test.cpp @@ -0,0 +1,445 @@ +#include "common.h" + +namespace esphome::packet_transport::testing { + +// --- Configuration setter tests --- + +TEST(PacketTransportTest, SetIsProvider) { + TestablePacketTransport transport; + transport.set_is_provider(true); + EXPECT_TRUE(transport.is_provider_); +} + +TEST(PacketTransportTest, SetEncryptionKey) { + TestablePacketTransport transport; + std::vector key(32, 0xAB); + transport.set_encryption_key(key); + EXPECT_EQ(transport.encryption_key_, key); + EXPECT_TRUE(transport.is_encrypted_()); +} + +TEST(PacketTransportTest, NoEncryptionByDefault) { + TestablePacketTransport transport; + EXPECT_FALSE(transport.is_encrypted_()); +} + +TEST(PacketTransportTest, SetRollingCodeEnable) { + TestablePacketTransport transport; + transport.set_rolling_code_enable(true); + EXPECT_TRUE(transport.rolling_code_enable_); +} + +TEST(PacketTransportTest, SetPingPongEnable) { + TestablePacketTransport transport; + transport.set_ping_pong_enable(true); + EXPECT_TRUE(transport.ping_pong_enable_); +} + +TEST(PacketTransportTest, SetPingPongRecycleTime) { + TestablePacketTransport transport; + transport.set_ping_pong_recycle_time(600); + EXPECT_EQ(transport.ping_pong_recyle_time_, 600u); +} + +// --- Provider management --- + +TEST(PacketTransportTest, AddProvider) { + TestablePacketTransport transport; + transport.add_provider("host1"); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.providers_.size(), 1u); +} + +TEST(PacketTransportTest, AddProviderDuplicate) { + TestablePacketTransport transport; + transport.add_provider("host1"); + transport.add_provider("host1"); + EXPECT_EQ(transport.providers_.size(), 1u); +} + +TEST(PacketTransportTest, SetProviderEncryption) { + TestablePacketTransport transport; + transport.add_provider("host1"); + std::vector key(32, 0xCD); + transport.set_provider_encryption("host1", key); + EXPECT_EQ(transport.providers_["host1"].encryption_key, key); +} + +// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, AddSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_sensor("temp", &s); + ASSERT_EQ(transport.sensors_.size(), 1u); + EXPECT_STREQ(transport.sensors_[0].id, "temp"); + EXPECT_EQ(transport.sensors_[0].sensor, &s); + EXPECT_TRUE(transport.sensors_[0].updated); +} + +TEST(PacketTransportTest, AddRemoteSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_remote_sensor("host1", "remote_temp", &s); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); +} +#endif + +#ifdef USE_BINARY_SENSOR +TEST(PacketTransportTest, AddBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_binary_sensor("motion", &bs); + ASSERT_EQ(transport.binary_sensors_.size(), 1u); + EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); + EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); +} + +TEST(PacketTransportTest, AddRemoteBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_remote_binary_sensor("host1", "remote_motion", &bs); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); +} +#endif + +// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, UnencryptedSensorRoundTrip) { + // Encoder + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + sensor::Sensor local_sensor; + local_sensor.state = 42.5f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + // Decoder + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; // sentinel + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); +} +#endif + +#ifdef USE_BINARY_SENSOR +TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + binary_sensor::BinarySensor local_bs; + local_bs.state = true; + encoder.add_binary_sensor("motion", &local_bs); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + binary_sensor::BinarySensor remote_bs; + decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_TRUE(remote_bs.state); +} +#endif + +#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR) +TEST(PacketTransportTest, MultipleSensorsRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 10.0f; + s2.state = 20.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + binary_sensor::BinarySensor bs1; + bs1.state = true; + encoder.add_binary_sensor("bs1", &bs1); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + binary_sensor::BinarySensor rbs1; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, 10.0f); + EXPECT_FLOAT_EQ(rs2.state, 20.0f); + EXPECT_TRUE(rbs1.state); +} +#endif + +// --- Encrypted round-trip --- + +#ifdef USE_SENSOR +TEST(PacketTransportTest, EncryptedSensorRoundTrip) { + std::vector key(32); + for (int i = 0; i < 32; i++) + key[i] = i; + + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + encoder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 99.9f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + decoder.set_provider_encryption("sender", key); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); +} + +// --- Selective send --- + +TEST(PacketTransportTest, SendDataOnlyUpdated) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 1.0f; + s2.state = 2.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + // Mark s1 as not updated, only s2 as updated + encoder.sensors_[0].updated = false; + encoder.sensors_[1].updated = true; + + encoder.send_data_(false); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent + EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent +} +#endif + +// --- Ping key tests --- + +TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + auto ping = build_ping_packet("requester", 0xDEADBEEF); + transport.process_({ping.data(), ping.size()}); + + ASSERT_EQ(transport.ping_keys_.size(), 1u); + EXPECT_EQ(transport.ping_keys_["requester"], 0xDEADBEEFu); +} + +TEST(PacketTransportTest, PingKeyIgnoredWhenNotEncrypted) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + // No encryption key — add_key_ should be a no-op + + auto ping = build_ping_packet("requester", 0xDEADBEEF); + transport.process_({ping.data(), ping.size()}); + + EXPECT_TRUE(transport.ping_keys_.empty()); +} + +TEST(PacketTransportTest, PingKeyUpdatedOnRepeat) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + auto ping1 = build_ping_packet("host1", 0x1111); + transport.process_({ping1.data(), ping1.size()}); + EXPECT_EQ(transport.ping_keys_["host1"], 0x1111u); + + // Same host, new key value — should update in place + auto ping2 = build_ping_packet("host1", 0x2222); + transport.process_({ping2.data(), ping2.size()}); + EXPECT_EQ(transport.ping_keys_.size(), 1u); + EXPECT_EQ(transport.ping_keys_["host1"], 0x2222u); +} + +TEST(PacketTransportTest, PingKeyMaxLimit) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + transport.set_encryption_key(std::vector(32, 0xAA)); + + // Fill to MAX_PING_KEYS (4) + for (int i = 0; i < 4; i++) { + char name[16]; + snprintf(name, sizeof(name), "host%d", i); + auto ping = build_ping_packet(name, 0x1000 + i); + transport.process_({ping.data(), ping.size()}); + } + EXPECT_EQ(transport.ping_keys_.size(), 4u); + + // 5th key should be discarded + auto ping = build_ping_packet("host4", 0x9999); + transport.process_({ping.data(), ping.size()}); + EXPECT_EQ(transport.ping_keys_.size(), 4u); + EXPECT_FALSE(transport.ping_keys_.contains("host4")); +} + +#ifdef USE_SENSOR +TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) { + std::vector key(32, 0xBB); + + // Responder: encrypted, owns a sensor + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + + // Requester sends a MAGIC_PING that the responder processes + auto ping = build_ping_packet("requester", 0xDEADBEEF); + responder.process_({ping.data(), ping.size()}); + ASSERT_EQ(responder.ping_keys_.size(), 1u); + + // Responder sends sensor data — ping key should be embedded + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + // The requester decrypts the packet and finds its ping key echoed back, + // which gates the sensor data — if the key is missing, data is blocked. + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); +} + +TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) { + std::vector key(32, 0xBB); + + // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester with ping-pong enabled expects a key that isn't in the packet + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found +} +#endif + +// --- Process error handling --- + +TEST(PacketTransportTest, ProcessShortBuffer) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + uint8_t buf[] = {0x53}; + // Too short for a magic number - should return safely + transport.process_({buf, 1}); +} + +TEST(PacketTransportTest, ProcessBadMagic) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + uint8_t buf[] = {0xFF, 0xFF, 0x00, 0x00}; + // Wrong magic - should return safely + transport.process_({buf, sizeof(buf)}); +} + +TEST(PacketTransportTest, ProcessOwnHostname) { + TestablePacketTransport transport; + transport.init_for_test("myself"); + // Build a packet from "myself" using a separate encoder + TestablePacketTransport fake_sender; + fake_sender.init_for_test("myself"); + fake_sender.send_data_(true); + ASSERT_EQ(fake_sender.sent_packets.size(), 1u); + + auto &packet = fake_sender.sent_packets[0]; + // Should be silently ignored because hostname matches our own + transport.process_({packet.data(), packet.size()}); +} + +TEST(PacketTransportTest, ProcessUnknownHostname) { + TestablePacketTransport transport; + transport.init_for_test("receiver"); + // No providers registered - "unknown" will not be found + TestablePacketTransport sender; + sender.init_for_test("unknown"); + sender.send_data_(true); + ASSERT_EQ(sender.sent_packets.size(), 1u); + + auto &packet = sender.sent_packets[0]; + // Should return safely without crash + transport.process_({packet.data(), packet.size()}); +} + +// --- Send disabled --- + +TEST(PacketTransportTest, NoSendWhenDisabled) { + TestablePacketTransport transport; + transport.init_for_test("sender"); + transport.send_enabled = false; + transport.send_data_(true); + EXPECT_TRUE(transport.sent_packets.empty()); +} + +} // namespace esphome::packet_transport::testing From 0e2a10c5f02cd4ba37ed53dde370bdd0a54c43ac Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Wed, 4 Mar 2026 19:34:13 -0800 Subject: [PATCH 0388/2056] [openthread] Cache is_connected() for cheap inline access (#14484) Co-authored-by: J. Nick Koston --- esphome/components/openthread/openthread.cpp | 25 ++++++------------- esphome/components/openthread/openthread.h | 5 +++- .../components/openthread/openthread_esp.cpp | 3 +++ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 92897a7e96e..9452f5a41eb 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,9 +1,7 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) #include "esp_openthread.h" -#endif #include @@ -48,22 +46,15 @@ void OpenThreadComponent::dump_config() { } } -bool OpenThreadComponent::is_connected() { - auto lock = InstanceLock::try_acquire(100); - if (!lock) { - ESP_LOGW(TAG, "Failed to acquire OpenThread lock in is_connected"); - return false; +void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) { + if (flags & OT_CHANGED_THREAD_ROLE) { + auto *self = static_cast(context); + // This runs on the OpenThread task thread with the OT lock held, + // so we can safely call otThreadGetDeviceRole directly. + otInstance *instance = esp_openthread_get_instance(); + otDeviceRole role = otThreadGetDeviceRole(instance); + self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } - - otInstance *instance = lock->get_instance(); - if (instance == nullptr) { - return false; - } - - otDeviceRole role = otThreadGetDeviceRole(instance); - - // TODO: If we're a leader, check that there is at least 1 known peer - return role >= OT_DEVICE_ROLE_CHILD; } // Gets the off-mesh routable address diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 728847afa54..d853c58f958 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -26,7 +27,7 @@ class OpenThreadComponent : public Component { bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } - bool is_connected(); + bool is_connected() const { return this->connected_; } network::IPAddresses get_ip_addresses(); std::optional get_omr_address(); void ot_main(); @@ -42,6 +43,7 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); + static void on_state_changed_(otChangedFlags flags, void *context); std::function factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; @@ -49,6 +51,7 @@ class OpenThreadComponent : public Component { std::optional output_power_{}; bool teardown_started_{false}; bool teardown_complete_{false}; + bool connected_{false}; private: // Stores a pointer to a string literal (static storage duration). diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 2af78b729f3..2296e32b7f7 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -175,6 +175,9 @@ void OpenThreadComponent::ot_main() { // Pass the existing dataset, or NULL which will use the preprocessor definitions ESP_ERROR_CHECK(esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr)); + // Register state change callback to update connected_ reactively instead of polling + otSetStateChangedCallback(instance, OpenThreadComponent::on_state_changed_, this); + esp_openthread_launch_mainloop(); // Clean up From c49c23d5d90ef0d4463479ffe8a2038252405d4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 07:21:04 -1000 Subject: [PATCH 0389/2056] [network] Inline network::is_connected() and ethernet is_connected() (#14464) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ethernet/ethernet_component.cpp | 2 - .../components/ethernet/ethernet_component.h | 2 +- esphome/components/network/util.cpp | 44 +----------------- esphome/components/network/util.h | 45 ++++++++++++++++++- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f855bc89cc7..098f7be972f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -687,8 +687,6 @@ void EthernetComponent::start_connect_() { this->status_set_warning(); } -bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } - void EthernetComponent::dump_connect_params_() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 1cd44d2b2cf..f5a31d78ebf 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -76,7 +76,7 @@ class EthernetComponent : public Component { void dump_config() override; float get_setup_priority() const override; void on_powerdown() override { powerdown(); } - bool is_connected(); + bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } #ifdef USE_ETHERNET_SPI void set_clk_pin(uint8_t clk_pin); diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index e397d770775..226b11b8cd3 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,53 +1,11 @@ #include "util.h" #include "esphome/core/defines.h" #ifdef USE_NETWORK -#ifdef USE_WIFI -#include "esphome/components/wifi/wifi_component.h" -#endif - -#ifdef USE_ETHERNET -#include "esphome/components/ethernet/ethernet_component.h" -#endif - -#ifdef USE_OPENTHREAD -#include "esphome/components/openthread/openthread.h" -#endif - -#ifdef USE_MODEM -#include "esphome/components/modem/modem_component.h" -#endif namespace esphome::network { // The order of the components is important: WiFi should come after any possible main interfaces (it may be used as -// an AP that use a previous interface for NAT). - -bool is_connected() { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) - return true; -#endif - -#ifdef USE_MODEM - if (modem::global_modem_component != nullptr) - return modem::global_modem_component->is_connected(); -#endif - -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->is_connected(); -#endif - -#ifdef USE_OPENTHREAD - if (openthread::global_openthread_component != nullptr) - return openthread::global_openthread_component->is_connected(); -#endif - -#ifdef USE_HOST - return true; // Assume its connected -#endif - return false; -} +// an AP that uses a previous interface for NAT). bool is_disabled() { #ifdef USE_MODEM diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index ae949ab0a85..4b700fe74c0 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -2,12 +2,55 @@ #include "esphome/core/defines.h" #ifdef USE_NETWORK #include +#include "esphome/core/helpers.h" #include "ip_address.h" +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif +#ifdef USE_MODEM +#include "esphome/components/modem/modem_component.h" +#endif +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif + namespace esphome::network { +// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as +// an AP that uses a previous interface for NAT). + /// Return whether the node is connected to the network (through wifi, eth, ...) -bool is_connected(); +ESPHOME_ALWAYS_INLINE inline bool is_connected() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) + return true; +#endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_connected(); +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_connected(); +#endif + +#ifdef USE_OPENTHREAD + if (openthread::global_openthread_component != nullptr) + return openthread::global_openthread_component->is_connected(); +#endif + +#ifdef USE_HOST + return true; // Assume it's connected +#endif + return false; +} + /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname From 2777d359904e117190e10f353c3601e67012478f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 07:21:44 -1000 Subject: [PATCH 0390/2056] [api] Devirtualize frame helper calls when protocol is fixed at compile time (#14468) --- esphome/components/api/api_connection.cpp | 5 +++-- esphome/components/api/api_connection.h | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 59476fac253..98ba1abe0b5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -114,9 +114,10 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = + std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 37855b2482a..aae8db3c688 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -3,6 +3,12 @@ #include "esphome/core/defines.h" #ifdef USE_API #include "api_frame_helper.h" +#ifdef USE_API_NOISE +#include "api_frame_helper_noise.h" +#endif +#ifdef USE_API_PLAINTEXT +#include "api_frame_helper_plaintext.h" +#endif #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" @@ -489,7 +495,13 @@ class APIConnection final : public APIServerConnectionBase { // === Optimal member ordering for 32-bit systems === // Group 1: Pointers (4 bytes each on 32-bit) +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) std::unique_ptr helper_; +#elif defined(USE_API_NOISE) + std::unique_ptr helper_; +#elif defined(USE_API_PLAINTEXT) + std::unique_ptr helper_; +#endif APIServer *parent_; // Group 2: Iterator union (saves ~16 bytes vs separate iterators) From a061397469da387ea8086fb494fcfe61861c222b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:16:06 -0500 Subject: [PATCH 0391/2056] [dfrobot_sen0395][sx1509] Fix structural bugs (#14494) Co-authored-by: Claude Opus 4.6 --- esphome/components/dfrobot_sen0395/commands.h | 14 ++++++-------- esphome/components/sx1509/sx1509.cpp | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index 3b0551b1843..95167efb4db 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -30,11 +30,9 @@ class Command { class ReadStateCommand : public Command { public: + ReadStateCommand() { timeout_ms_ = 500; } uint8_t execute(DfrobotSen0395Component *parent) override; uint8_t on_message(std::string &message) override; - - protected: - uint32_t timeout_ms_{500}; }; class PowerCommand : public Command { @@ -99,12 +97,12 @@ class ResetSystemCommand : public Command { class SaveCfgCommand : public Command { public: - SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; } + SaveCfgCommand() { + cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; + cmd_duration_ms_ = 3000; + timeout_ms_ = 3500; + } uint8_t on_message(std::string &message) override; - - protected: - uint32_t cmd_duration_ms_{3000}; - uint32_t timeout_ms_{3500}; }; class LedModeCommand : public Command { diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 746ec9cda35..dfe1277297a 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -56,11 +56,11 @@ void SX1509Component::loop() { return; } int row, col; - for (row = 0; row < 7; row++) { + for (row = 0; row < 8; row++) { if (key_data & (1 << row)) break; } - for (col = 8; col < 15; col++) { + for (col = 8; col < 16; col++) { if (key_data & (1 << col)) break; } @@ -229,7 +229,7 @@ void SX1509Component::setup_keypad_() { this->read_byte_16(REG_DIR_B, &this->ddr_mask_); for (int i = 0; i < this->rows_; i++) this->ddr_mask_ &= ~(1 << i); - for (int i = 8; i < (this->cols_ * 2); i++) + for (int i = 8; i < (8 + this->cols_); i++) this->ddr_mask_ |= (1 << i); this->write_byte_16(REG_DIR_B, this->ddr_mask_); From 01f4275202a8e9e82b2b7486ceefe18f5ffa1238 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:16:33 -0500 Subject: [PATCH 0392/2056] [veml7700] Fix initial settling timeout using raw enum instead of milliseconds (#14487) Co-authored-by: Claude Opus 4.6 --- esphome/components/veml7700/veml7700.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index eb286ba21b8..1ed484119bd 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -141,7 +141,7 @@ void VEML7700Component::loop() { // Datasheet: 2.5 ms before the first measurement is needed, allowing for the correct start of the signal processor // and oscillator. // Reality: wait for couple integration times to have first samples captured - this->set_timeout(2 * this->integration_time_, [this]() { this->state_ = State::IDLE; }); + this->set_timeout(2 * get_itime_ms(this->integration_time_), [this]() { this->state_ = State::IDLE; }); } if (this->is_ready()) { From 3df4ef9362cb7843f91c28cb2cfef1691dac8c52 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:31:26 -0500 Subject: [PATCH 0393/2056] [ssd1322][ssd1325][ssd1327] Fix nibble mask bug in grayscale draw_pixel (#14496) Co-authored-by: Claude Opus 4.6 --- esphome/components/ssd1322_base/ssd1322_base.cpp | 2 +- esphome/components/ssd1325_base/ssd1325_base.cpp | 2 +- esphome/components/ssd1327_base/ssd1327_base.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 23576e7b2c4..1fce826ad9d 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -169,7 +169,7 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1322_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1322_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index e7d2386ac71..fe7df9674b6 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -202,7 +202,7 @@ void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1325_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1325_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 2498bfcd67c..87e52206f2d 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -145,7 +145,7 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary color4 = (color4 & SSD1327_COLORMASK) << shift; // first mask off the nibble we must change... - this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift); + this->buffer_[pos] &= (static_cast(~SSD1327_COLORMASK) >> shift); // ...then lay the new nibble back on top. done! this->buffer_[pos] |= color4; } From 4a5d8449fd28aa80b801dc270a3f2e9e20344794 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:33:12 -0500 Subject: [PATCH 0394/2056] [sht4x][grove_tb6612fng] Fix logic bugs (#14497) Co-authored-by: Claude Opus 4.6 --- esphome/components/grove_tb6612fng/grove_tb6612fng.cpp | 2 +- esphome/components/sht4x/sht4x.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index a2499846473..428c8ec4a8c 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -131,7 +131,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, buffer_[4] = ms_per_step; buffer_[5] = (ms_per_step >> 8); - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 6) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Run stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 9d29746f0bf..42be3262029 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -10,7 +10,7 @@ static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; + uint8_t cmd[] = {this->heater_command_}; ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { From 9518d88a2aa7a5f7d5b263ffabeddeebd8f9d17d Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Thu, 5 Mar 2026 10:35:20 -0800 Subject: [PATCH 0395/2056] [openthread] static log level code quality improvement (#14456) Co-authored-by: J. Nick Koston --- esphome/components/openthread/__init__.py | 21 +++++++++++++++++++ .../openthread/test.esp32-c6-idf.yaml | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5c64cf31dce..21373b77dfb 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -14,9 +14,12 @@ import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, CONF_ENABLE_IPV6, + CONF_FRAMEWORK, CONF_ID, + CONF_LOG_LEVEL, CONF_OUTPUT_POWER, CONF_USE_ADDRESS, + PLATFORM_ESP32, ) from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv @@ -46,6 +49,15 @@ AUTO_LOAD = ["network"] CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] +IDF_TO_OT_LOG_LEVEL = { + "NONE": "NONE", + "ERROR": "CRIT", + "WARN": "WARN", + "INFO": "NOTE", + "DEBUG": "INFO", + "VERBOSE": "DEBG", +} + CONF_DEVICE_TYPES = [ "FTD", "MTD", @@ -198,6 +210,15 @@ def _final_validate(_): "Please set `enable_ipv6: true` in the `network` configuration." ) + if ( + (esp32_config := full_config.get(PLATFORM_ESP32)) is not None + and (fw_config := esp32_config.get(CONF_FRAMEWORK)) is not None + and (log_level := fw_config.get(CONF_LOG_LEVEL)) is not None + ): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC", False) + ot_log_level = IDF_TO_OT_LOG_LEVEL.get(log_level, log_level) + add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_LOG_LEVEL_{ot_log_level}", True) + FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 77abc433c14..008edd53972 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -1,3 +1,9 @@ +esp32: + board: esp32-c6-devkitc-1 + framework: + type: esp-idf + log_level: DEBUG + network: enable_ipv6: true From e1d0c6da09ecad433c77ff8efdad60c730f020d8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:09:23 -0500 Subject: [PATCH 0396/2056] [dfplayer][ufire_ise][ufire_ec][qmp6988][atm90e26] Fix wrong operators and masks (#14491) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/atm90e26/atm90e26.cpp | 2 +- esphome/components/dfplayer/dfplayer.cpp | 1 + esphome/components/qmp6988/qmp6988.cpp | 2 +- esphome/components/ufire_ec/ufire_ec.cpp | 2 +- esphome/components/ufire_ise/ufire_ise.cpp | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index 2203dd0d713..e6602411bb8 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() { float ATM90E26Component::get_power_factor_() { const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed if (val & 0x8000) { - return -(val & 0x7FF) / 1000.0f; + return -(val & 0x7FFF) / 1000.0f; } else { return val / 1000.0f; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 79f8fd03c3e..1e1c33adaf3 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -260,6 +260,7 @@ void DFPlayer::loop() { ESP_LOGV(TAG, "Playback finished (USB drive)"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); + break; case 0x3D: ESP_LOGV(TAG, "Playback finished (SD card)"); this->is_playing_ = false; diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 24fe34e7852..17d91c36330 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -251,7 +251,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) { void QMP6988Component::write_filter_(QMP6988IIRFilter filter) { uint8_t data; - data = (filter & 0x03); + data = (filter & QMP6988_CONFIG_REG_FILTER_MSK); this->write_byte(QMP6988_CONFIG_REG, data); delay(10); } diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 3868dc92b7f..a1c3568a1a3 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "ufire_ec"; void UFireECComponent::setup() { uint8_t version; - if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { this->mark_failed(); return; } diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 486a5063913..e967fc53c37 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "ufire_ise"; void UFireISEComponent::setup() { uint8_t version; - if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { + if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) { this->mark_failed(); return; } From cce7a09fa995692dae38a4fdecc34bd297c167a0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:09:34 -0500 Subject: [PATCH 0397/2056] [pn532_spi] Fix preamble check logic and OOB access when full_len is zero (#14486) Co-authored-by: Claude Opus 4.6 --- esphome/components/pn532_spi/pn532_spi.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 118421c47f3..553c6d26a6a 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -88,9 +88,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { #endif ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size())); - if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { + if (header[0] != 0x00 || header[1] != 0x00 || header[2] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); + this->disable(); return false; } @@ -100,15 +101,20 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { if (!valid_header) { ESP_LOGV(TAG, "read data invalid header!"); + this->disable(); return false; } - // full length of message, including command response + // full length of message, including command response (minimum 2: TFI + command response) uint8_t full_len = header[3]; + if (full_len < 2) { + ESP_LOGV(TAG, "read data has no payload"); + this->disable(); + return false; + } + // length of data, excluding command response uint8_t len = full_len - 1; - if (full_len == 0) - len = 0; ESP_LOGV(TAG, "Reading response of length %d", len); From e210e414bd0ed93c678ecdc3e8895918a517716d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 09:15:02 -1000 Subject: [PATCH 0398/2056] [ota] Devirtualize OTA backend calls (#14473) --- esphome/components/esphome/ota/ota_esphome.h | 4 +-- .../http_request/ota/ota_http_request.cpp | 7 +---- .../http_request/ota/ota_http_request.h | 4 +-- esphome/components/ota/ota_backend.h | 13 --------- .../ota/ota_backend_arduino_libretiny.cpp | 2 +- .../ota/ota_backend_arduino_libretiny.h | 16 ++++++----- .../ota/ota_backend_arduino_rp2040.cpp | 2 +- .../ota/ota_backend_arduino_rp2040.h | 16 ++++++----- .../components/ota/ota_backend_esp8266.cpp | 2 +- esphome/components/ota/ota_backend_esp8266.h | 16 ++++++----- .../components/ota/ota_backend_esp_idf.cpp | 2 +- esphome/components/ota/ota_backend_esp_idf.h | 16 ++++++----- esphome/components/ota/ota_backend_factory.h | 27 +++++++++++++++++++ esphome/components/ota/ota_backend_host.cpp | 2 +- esphome/components/ota/ota_backend_host.h | 16 ++++++----- .../web_server/ota/ota_web_server.cpp | 4 +-- 16 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 esphome/components/ota/ota_backend_factory.h diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 08edacad92f..f3a5952398f 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_OTA -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/components/socket/socket.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -86,7 +86,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; - std::unique_ptr backend_; + ota::OTABackendPtr backend_; uint32_t client_connect_time_{0}; uint16_t port_; diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 0db3a50b47d..5dd21c314c6 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -8,10 +8,6 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" -#include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_esp8266.h" -#include "esphome/components/ota/ota_backend_arduino_rp2040.h" -#include "esphome/components/ota/ota_backend_esp_idf.h" namespace esphome { namespace http_request { @@ -69,8 +65,7 @@ void OtaHttpRequestComponent::flash() { } } -void OtaHttpRequestComponent::cleanup_(std::unique_ptr backend, - const std::shared_ptr &container) { +void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container) { if (this->update_started_) { ESP_LOGV(TAG, "Aborting OTA backend"); backend->abort(); diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 6d39b0d466c..70e4559fa7c 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -39,7 +39,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< void flash(); protected: - void cleanup_(std::unique_ptr backend, const std::shared_ptr &container); + void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container); uint8_t do_ota_(); std::string get_url_with_auth_(const std::string &url); bool http_get_md5_(); diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index e03afd4fc6f..bc603a6e9e2 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -49,17 +49,6 @@ enum OTAState { OTA_ERROR, }; -class OTABackend { - public: - virtual ~OTABackend() = default; - virtual OTAResponseTypes begin(size_t image_size) = 0; - virtual void set_update_md5(const char *md5) = 0; - virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0; - virtual OTAResponseTypes end() = 0; - virtual void abort() = 0; - virtual bool supports_compression() = 0; -}; - /** Listener interface for OTA state changes. * * Components can implement this interface to receive OTA state updates @@ -130,7 +119,5 @@ OTAGlobalCallback *get_global_ota_callback(); // - notify_state_deferred_() when in separate task (e.g., web_server OTA) // This ensures proper listener execution in all contexts. #endif -std::unique_ptr make_ota_backend(); - } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index b4ecad1227e..d364f750074 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -12,7 +12,7 @@ namespace ota { static const char *const TAG = "ota.arduino_libretiny"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 8f9d268eec6..4514bf84bda 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -7,19 +7,21 @@ namespace esphome { namespace ota { -class ArduinoLibreTinyOTABackend final : public OTABackend { +class ArduinoLibreTinyOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index ee1ba48d504..e2a57ec665b 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -14,7 +14,7 @@ namespace ota { static const char *const TAG = "ota.arduino_rp2040"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { // OTA size of 0 is not currently handled, but diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 6a708f9c574..0956cb4b4b9 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -9,19 +9,21 @@ namespace esphome { namespace ota { -class ArduinoRP2040OTABackend final : public OTABackend { +class ArduinoRP2040OTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 4b84708cd91..1f9a77e4261 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -48,7 +48,7 @@ namespace esphome::ota { static const char *const TAG = "ota.esp8266"; -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index 52f657f0065..6213289accb 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -12,15 +12,15 @@ namespace esphome::ota { /// OTA backend for ESP8266 using native SDK functions. /// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM /// by not having a global Update object in .bss. -class ESP8266OTABackend final : public OTABackend { +class ESP8266OTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); // Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0) - bool supports_compression() override { return true; } + bool supports_compression() { return true; } protected: /// Erase flash sector if current address is at sector boundary @@ -54,5 +54,7 @@ class ESP8266OTABackend final : public OTABackend { bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace esphome::ota #endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 93c65a9624e..925bb396454 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -11,7 +11,7 @@ namespace esphome { namespace ota { -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes IDFOTABackend::begin(size_t image_size) { #ifdef USE_OTA_ROLLBACK diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 7f7f6115c50..a0f538afc02 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -10,14 +10,14 @@ namespace esphome { namespace ota { -class IDFOTABackend final : public OTABackend { +class IDFOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } private: esp_ota_handle_t update_handle_{0}; @@ -27,6 +27,8 @@ class IDFOTABackend final : public OTABackend { bool md5_set_{false}; }; +std::unique_ptr make_ota_backend(); + } // namespace ota } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_factory.h b/esphome/components/ota/ota_backend_factory.h new file mode 100644 index 00000000000..7c79f027027 --- /dev/null +++ b/esphome/components/ota/ota_backend_factory.h @@ -0,0 +1,27 @@ +#pragma once + +#include "ota_backend.h" + +#include + +#ifdef USE_ESP8266 +#include "ota_backend_esp8266.h" +#elif defined(USE_ESP32) +#include "ota_backend_esp_idf.h" +#elif defined(USE_RP2040) +#include "ota_backend_arduino_rp2040.h" +#elif defined(USE_LIBRETINY) +#include "ota_backend_arduino_libretiny.h" +#elif defined(USE_HOST) +#include "ota_backend_host.h" +#else +// Stub for static analysis when no platform is defined +namespace esphome::ota { +struct StubOTABackend {}; +std::unique_ptr make_ota_backend(); +} // namespace esphome::ota +#endif + +namespace esphome::ota { +using OTABackendPtr = decltype(make_ota_backend()); +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp index ddab174bed7..2e2132418d8 100644 --- a/esphome/components/ota/ota_backend_host.cpp +++ b/esphome/components/ota/ota_backend_host.cpp @@ -8,7 +8,7 @@ namespace esphome::ota { // Stub implementation - OTA is not supported on host platform. // All methods return error codes to allow compilation of configs with OTA triggers. -std::unique_ptr make_ota_backend() { return make_unique(); } +std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; } diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index 5a2dcfcf39b..300facf72f9 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -7,15 +7,17 @@ namespace esphome::ota { /// Stub OTA backend for host platform - allows compilation but does not implement OTA. /// All operations return error codes immediately. This enables configurations with /// OTA triggers to compile for host platform during development. -class HostOTABackend final : public OTABackend { +class HostOTABackend final { public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } + OTAResponseTypes begin(size_t image_size); + void set_update_md5(const char *md5); + OTAResponseTypes write(uint8_t *data, size_t len); + OTAResponseTypes end(); + void abort(); + bool supports_compression() { return false; } }; +std::unique_ptr make_ota_backend(); + } // namespace esphome::ota #endif diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 4be162ccd32..95b166901ad 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -1,7 +1,7 @@ #include "ota_web_server.h" #ifdef USE_WEBSERVER_OTA -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -71,7 +71,7 @@ class OTARequestHandler : public AsyncWebHandler { bool ota_success_{false}; private: - std::unique_ptr ota_backend_{nullptr}; + ota::OTABackendPtr ota_backend_{nullptr}; }; void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { From 44d314d069503831849c5d0de8e7839898f7eddd Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Thu, 5 Mar 2026 20:22:37 +0100 Subject: [PATCH 0399/2056] [GPS] fix component Python declaration to match C++ implementation (#14519) --- esphome/components/gps/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 045a5a6c847..ab48417a4e7 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -34,7 +34,7 @@ AUTO_LOAD = ["sensor"] CODEOWNERS = ["@coogle", "@ximex"] gps_ns = cg.esphome_ns.namespace("gps") -GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) +GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") MULTI_CONF = True From 6f0460b0ee3b34469888ac249b18c5a269648d51 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:46:47 -0500 Subject: [PATCH 0400/2056] [sim800l][tormatic][tx20] Fix OOB access, div-by-zero, and off-by-one (#14512) Co-authored-by: J. Nick Koston --- esphome/components/sim800l/sim800l.cpp | 5 +++-- esphome/components/tormatic/tormatic_cover.cpp | 3 +++ esphome/components/tx20/tx20.cpp | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 2115c72cefa..913d920c94e 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -196,7 +196,8 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_CREG_WAIT: { // Response: "+CREG: 0,1" -- the one there means registered ok // "+CREG: -,-" means not registered ok - bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); + bool registered = + message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); if (registered) { if (!this->registered_) { ESP_LOGD(TAG, "Registered OK"); @@ -205,7 +206,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->expect_ack_ = true; } else { ESP_LOGW(TAG, "Registration Fail"); - if (message[7] == '0') { // Network registration is disable, enable it + if (message.size() > 7 && message[7] == '0') { // Network registration is disabled, enable it send_cmd_("AT+CREG=1"); this->expect_ack_ = true; this->state_ = STATE_SETUP_CMGF; diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index f567be0674f..37a269088e6 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -183,6 +183,9 @@ void Tormatic::recompute_position_() { duration = this->close_duration_; } + if (duration == 0) + return; + auto delta = direction * diff / duration; this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN); diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 6bc5f0bb514..3e0234fac04 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -191,7 +191,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { arg->tx20_available = true; return; } - if (index <= MAX_BUFFER_SIZE) { + if (index < MAX_BUFFER_SIZE) { arg->buffer[index] = delay; } arg->spent_time += delay; From 05ddc85412c8baf985e76015ed750407faa3d4e0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:46:56 -0500 Subject: [PATCH 0401/2056] [rc522][sml][kamstrup_kmp] Fix buffer bounds checks (#14515) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 4 ++-- esphome/components/rc522/rc522.cpp | 1 + esphome/components/sml/sml_parser.cpp | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 00c65a19379..29de6512559 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -136,7 +136,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) { int timeout = 250; // ms // Read the data from the UART - while (timeout > 0) { + while (timeout > 0 && buffer_len < static_cast(sizeof(buffer))) { if (this->available()) { data = this->read(); if (data > -1) { @@ -246,7 +246,7 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ } void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) { - const char *unit = UNITS[unit_idx]; + const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : ""; // Standard sensors if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) { diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 91fae7fa345..c5f7ec2cd41 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -169,6 +169,7 @@ void RC522::loop() { default: ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_); state_ = STATE_DONE; + return; } buffer_[1] = 32; pcd_transceive_data_(2); diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index 16e37949dc7..ed086e385d2 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -35,6 +35,8 @@ bool SmlFile::setup_node(SmlNode *node) { // Check if we need additional length bytes if (overlength) { + if (this->pos_ + 1 >= this->buffer_.size()) + return false; // Shift the current length to the higher nibble // and add the lower nibble of the next byte to the length length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); From d6f3186b3d11d6d6aa330751ff634c0d1889e911 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:10 -0500 Subject: [PATCH 0402/2056] [haier][bedjet][vbus][lightwaverf] Fix buffer overflow bugs (#14493) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/bedjet/bedjet_codec.cpp | 28 +++++++++++++------ .../components/haier/smartair2_climate.cpp | 2 +- esphome/components/lightwaverf/LwTx.cpp | 4 +++ esphome/components/vbus/vbus.cpp | 4 ++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/bedjet/bedjet_codec.cpp b/esphome/components/bedjet/bedjet_codec.cpp index 9a312e226c1..7a959390f31 100644 --- a/esphome/components/bedjet/bedjet_codec.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -1,4 +1,5 @@ #include "bedjet_codec.h" +#include #include #include @@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, /** Decodes the extra bytes that were received after being notified with a partial packet. */ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length); + return; + } ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); uint8_t offset = this->last_buffer_size_; if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { @@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. */ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGW(TAG, "Received short packet: %d bytes", length); + return false; + } ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { // Clear old buffer memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); // Copy new data into buffer - memcpy(&this->buf_, data, length); - this->last_buffer_size_ = length; + size_t copy_len = std::min(static_cast(length), sizeof(BedjetStatusPacket)); + memcpy(&this->buf_, data, copy_len); + this->last_buffer_size_ = copy_len; // TODO: validate the packet checksum? if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && @@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { } } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. - ESP_LOGVV(TAG, - "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " - "[12]=%d, [-1]=%d", - bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], - data[9], data[10], data[11], data[12], data[length - 1]); + if (length >= 13) { + ESP_LOGVV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], + data[9], data[10], data[11], data[12], data[length - 1]); + } - if (this->has_status()) { + if (this->has_status() && length >= 7) { this->status_packet_->ambient_temp_step = data[6]; } } else { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index d24f8ad8498..e91224e2d8e 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -385,7 +385,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < sizeof(smartair2_protocol::HaierStatus)) + if (size != sizeof(smartair2_protocol::HaierStatus)) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; smartair2_protocol::HaierStatus packet; memcpy(&packet, packet_buffer, size); diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index f5ef6ddb2c0..b69b93b978d 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -108,6 +108,10 @@ bool LwTx::lwtx_free() { return !this->tx_msg_active; } Send a LightwaveRF message (10 nibbles in bytes) **/ void LwTx::lwtx_send(const std::vector &msg) { + if (msg.size() < TX_MSGLEN) { + ESP_LOGW("lightwaverf.sensor", "Message too short: %zu < %u", msg.size(), static_cast(TX_MSGLEN)); + return; + } if (this->tx_translate) { for (uint8_t i = 0; i < TX_MSGLEN; i++) { this->tx_buf[i] = TX_NIBBLE[msg[i] & 0xF]; diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index b9496a08dec..8616da010d1 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -1,6 +1,7 @@ #include "vbus.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -106,9 +107,10 @@ void VBus::loop() { continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)]; + size_t log_bytes = std::min(this->buffer_.size(), static_cast(VBUS_MAX_LOG_BYTES)); #endif ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, - format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size())); + format_hex_to(hex_buf, this->buffer_.data(), log_bytes)); for (auto &listener : this->listeners_) listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); this->state_ = 0; From 9961c8180aec582a856b455d35a2e0ae19e2d3e4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:32 -0500 Subject: [PATCH 0403/2056] [alpha3][mpu6886][emc2101] Fix copy-paste bugs (#14492) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/alpha3/alpha3.cpp | 2 +- esphome/components/emc2101/emc2101.cpp | 2 +- esphome/components/mpu6886/mpu6886.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index f22a8e24446..6e82ec047db 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc this->current_sensor_->publish_state(NAN); if (this->speed_sensor_ != nullptr) this->speed_sensor_->publish_state(NAN); - if (this->speed_sensor_ != nullptr) + if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(NAN); break; } diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 7d85cd31cfd..068e25568f1 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -72,7 +72,7 @@ void Emc2101Component::setup() { config |= EMC2101_DAC_BIT; } if (this->inverted_) { - config |= EMC2101_POLARITY_BIT; + reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_POLARITY_BIT; } if (this->dac_mode_) { // DAC mode configurations diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 68b77b59c99..02747da3064 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -80,7 +80,7 @@ void MPU6886Component::setup() { accel_config &= 0b11100111; accel_config |= (MPU6886_RANGE_2G << 3); ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); - if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) { + if (!this->write_byte(MPU6886_REGISTER_ACCEL_CONFIG, accel_config)) { this->mark_failed(); return; } From 22d90d702d36fb7f91a6fbfc3d69dd554aa2cbe9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:47:55 -0500 Subject: [PATCH 0404/2056] [usb_cdc_acm][scd4x][pulse_counter][mopeka_std_check][ruuvi_ble] Fix assorted one-liner bugs (#14495) Co-authored-by: Claude Opus 4.6 --- .../components/mopeka_std_check/mopeka_std_check.cpp | 6 +++--- .../components/mopeka_std_check/mopeka_std_check.h | 2 +- .../components/pulse_counter/pulse_counter_sensor.cpp | 3 ++- esphome/components/ruuvi_ble/ruuvi_ble.cpp | 11 +++++++---- esphome/components/scd4x/scd4x.cpp | 3 ++- esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6322b550c94..88bd7b02fdb 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -108,7 +108,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } // Get temperature of sensor - uint8_t temp_in_c = this->parse_temperature_(mopeka_data); + int8_t temp_in_c = this->parse_temperature_(mopeka_data); if (this->temperature_ != nullptr) { this->temperature_->publish_state(temp_in_c); } @@ -223,12 +223,12 @@ uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message) return (uint8_t) percent; } -uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { +int8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { uint8_t tmp = message->raw_temp; if (tmp == 0x0) { return -40; } else { - return (uint8_t) ((tmp - 25.0f) * 1.776964f); + return static_cast((tmp - 25.0f) * 1.776964f); } } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 897b5414ed0..45588988c53 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -71,7 +71,7 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi float get_lpg_speed_of_sound_(float temperature); uint8_t parse_battery_level_(const mopeka_std_package *message); - uint8_t parse_temperature_(const mopeka_std_package *message); + int8_t parse_temperature_(const mopeka_std_package *message); }; } // namespace mopeka_std_check diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 5e62c0a4107..ec00bd024e0 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -175,7 +175,8 @@ void PulseCounterSensor::setup() { void PulseCounterSensor::set_total_pulses(uint32_t pulses) { this->current_total_ = pulses; - this->total_sensor_->publish_state(pulses); + if (this->total_sensor_ != nullptr) + this->total_sensor_->publish_state(pulses); } void PulseCounterSensor::dump_config() { diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 1b126bdef0e..bf088873ce0 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -63,10 +63,13 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x; result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y; result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; - result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN - ? NAN - : sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + - acceleration_z * acceleration_z); + if ((data[6] != 0xFF || data[7] != 0xFF) && (data[8] != 0xFF || data[9] != 0xFF) && + (data[10] != 0xFF || data[11] != 0xFF)) { + result.acceleration = + sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + } else { + result.acceleration = NAN; + } result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; result.movement_counter = movement_counter; diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index a265386cc2f..0c108fba9d6 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -307,7 +307,7 @@ bool SCD4XComponent::start_measurement_() { break; } - static uint8_t remaining_retries = 3; + uint8_t remaining_retries = 3; while (remaining_retries) { if (!this->write_command(measurement_command)) { ESP_LOGE(TAG, "Error starting measurements"); @@ -316,6 +316,7 @@ bool SCD4XComponent::start_measurement_() { if (--remaining_retries == 0) return false; delay(50); // NOLINT wait 50 ms and try again + continue; } this->status_clear_warning(); return true; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index d33fb80f781..44de986f9a3 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -161,7 +161,7 @@ void USBCDCACMInstance::setup() { // Create a simple, unique task name per interface char task_name[] = "usb_tx_0"; - task_name[sizeof(task_name) - 1] = format_hex_char(static_cast(this->itf_)); + task_name[sizeof(task_name) - 2] = format_hex_char(static_cast(this->itf_)); xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_); if (this->usb_tx_task_handle_ == nullptr) { From 5c5ea8824edebb74e80a3ff9306add66b76b4b95 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 13:51:08 -0600 Subject: [PATCH 0405/2056] [audio_file] New component for embedding files into firmware (#14434) Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/audio_file/__init__.py | 255 ++++++++++++++++++ esphome/components/audio_file/audio_file.h | 28 ++ esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/components/audio_file/common.yaml | 5 + .../components/audio_file/test.esp32-idf.yaml | 1 + tests/components/audio_file/test.wav | Bin 0 -> 46 bytes 8 files changed, 292 insertions(+) create mode 100644 esphome/components/audio_file/__init__.py create mode 100644 esphome/components/audio_file/audio_file.h create mode 100644 tests/components/audio_file/common.yaml create mode 100644 tests/components/audio_file/test.esp32-idf.yaml create mode 100644 tests/components/audio_file/test.wav diff --git a/CODEOWNERS b/CODEOWNERS index b22f85b71d7..7c37b20e099 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,7 @@ esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 +esphome/components/audio_file/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py new file mode 100644 index 00000000000..3ed6c1cd928 --- /dev/null +++ b/esphome/components/audio_file/__init__.py @@ -0,0 +1,255 @@ +from dataclasses import dataclass, field +import hashlib +import logging +from pathlib import Path + +import puremagic + +from esphome import external_files +import esphome.codegen as cg +from esphome.components import audio +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, ID, HexInt +from esphome.cpp_generator import MockObj +from esphome.external_files import download_content +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@kahrendt"] + +AUTO_LOAD = ["audio"] + +DOMAIN = "audio_file" + +audio_file_ns = cg.esphome_ns.namespace("audio_file") + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + + +@dataclass +class AudioFileData: + file_ids: dict[str, ID] = field(default_factory=dict) + file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict) + + +def _get_data() -> AudioFileData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioFileData() + return CORE.data[DOMAIN] + + +def get_audio_file_ids() -> dict[str, ID]: + """Get all registered audio file IDs for cross-component access.""" + return _get_data().file_ids + + +def _compute_local_file_path(value: ConfigType) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value: ConfigType) -> ConfigType: + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +def _file_schema(value: ConfigType | str) -> ConfigType: + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +def _validate_file_shorthand(value: str) -> ConfigType: + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]: + """Read an audio file and determine its type. Used by this component and media_source platform.""" + conf_file = file_config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + else: + raise cv.Invalid("Unsupported file source") + + with open(path, "rb") as f: + data = f.read() + + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) + + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] + if file_type == "wav": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] + elif file_type in ("mp3", "mpeg", "mpga"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] + elif file_type == "flac": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type == "ogg" + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] + + return data, media_file_type + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +MEDIA_FILE_TYPE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(audio.AudioFile), + cv.Required(CONF_FILE): _file_schema, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + + +MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB + + +def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]: + for file_config in config: + data, media_file_type = read_audio_file_and_type(file_config) + + if len(data) > MAX_FILE_SIZE: + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)" + ) + + if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Unsupported media file from {source!r} (detected type: {media_file_type})" + ) + + # Cache the file data so to_code() doesn't need to re-read it + _get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type) + + media_file_type_str = str(media_file_type) + if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]): + audio.request_flac_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]): + audio.request_mp3_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]): + audio.request_opus_support() + + return config + + +CONFIG_SCHEMA = cv.All( + cv.only_on_esp32, + cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + _validate_supported_local_file, +) + + +async def to_code(config: list[ConfigType]) -> None: + cache = _get_data().file_cache + + for file_config in config: + file_id = str(file_config[CONF_ID]) + data, media_file_type = cache[file_id] + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + audio.AudioFile, + ( + "data", + prog_arr, + ), + ( + "length", + len(rhs), + ), + ( + "file_type", + media_file_type, + ), + ) + + cg.new_Pvariable( + file_config[CONF_ID], + media_files_struct, + ) + + # Store file ID for cross-component access + _get_data().file_ids[file_id] = file_config[CONF_ID] + + # Register all files in the shared C++ registry + cg.add_define("AUDIO_FILE_MAX_FILES", len(config)) + for file_config in config: + file_id = str(file_config[CONF_ID]) + file_var = await cg.get_variable(file_config[CONF_ID]) + cg.add(audio_file_ns.add_named_audio_file(file_var, file_id)) diff --git a/esphome/components/audio_file/audio_file.h b/esphome/components/audio_file/audio_file.h new file mode 100644 index 00000000000..537e19fb3ca --- /dev/null +++ b/esphome/components/audio_file/audio_file.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef AUDIO_FILE_MAX_FILES + +#include "esphome/components/audio/audio.h" +#include "esphome/core/helpers.h" + +namespace esphome::audio_file { + +struct NamedAudioFile { + audio::AudioFile *file; + const char *file_id; +}; + +inline StaticVector + named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) { + named_audio_files.push_back({file, file_id}); +} + +inline const StaticVector &get_named_audio_files() { return named_audio_files; } + +} // namespace esphome::audio_file + +#endif // AUDIO_FILE_MAX_FILES diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 07afefd91aa..1a6d9b3a803 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -137,6 +137,7 @@ // Feature flags which do not work for zephyr #ifndef USE_ZEPHYR +#define AUDIO_FILE_MAX_FILES 4 #define USE_AUDIO_DAC #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT diff --git a/script/ci-custom.py b/script/ci-custom.py index b60d7d77401..8e1652b505d 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -72,6 +72,7 @@ ignore_types = ( ".gif", ".webp", ".bin", + ".wav", ) LINT_FILE_CHECKS = [] diff --git a/tests/components/audio_file/common.yaml b/tests/components/audio_file/common.yaml new file mode 100644 index 00000000000..94042080946 --- /dev/null +++ b/tests/components/audio_file/common.yaml @@ -0,0 +1,5 @@ +audio_file: + - id: test_audio + file: + type: local + path: $component_dir/test.wav diff --git a/tests/components/audio_file/test.esp32-idf.yaml b/tests/components/audio_file/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/audio_file/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/audio_file/test.wav b/tests/components/audio_file/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..f9d07ef2238eb2fcb355055466d3789ee1a1fe0b GIT binary patch literal 46 ycmWIYbaPW Date: Thu, 5 Mar 2026 14:51:32 -0500 Subject: [PATCH 0406/2056] [wled][lcd_base][touchscreen][ee895] Fix off-by-one, buffer overrun, empty deref, and uninitialized pointers (#14513) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ee895/ee895.h | 6 +++--- esphome/components/lcd_base/lcd_display.cpp | 2 +- esphome/components/touchscreen/touchscreen.h | 7 ++++++- esphome/components/wled/wled_light_effect.cpp | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 259b7c524b5..ff1085e05d8 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -22,9 +22,9 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void write_command_(uint16_t addr, uint16_t reg_cnt); float read_float_(); uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); - sensor::Sensor *co2_sensor_; - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index cd08a739eb6..1f0ba482d74 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -99,7 +99,7 @@ void HOT LCDDisplay::display() { this->send(this->buffer_[this->columns_ * 2 + i], true); } - if (this->rows_ >= 1) { + if (this->rows_ >= 2) { this->command_(LCD_DISPLAY_COMMAND_SET_DDRAM_ADDR | 0x40); for (uint8_t i = 0; i < this->columns_; i++) diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 8016323d493..7451c207ec3 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -65,7 +65,12 @@ class Touchscreen : public PollingComponent { void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } - optional get_touch() { return this->touches_.begin()->second; } + optional get_touch() { + if (this->touches_.empty()) { + return {}; + } + return this->touches_.begin()->second; + } TouchPoints_t get_touches() { TouchPoints_t touches; diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 87bae5b1da4..db2708d6d02 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -161,7 +161,7 @@ bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const u // https://kno.wled.ge/interfaces/udp-notifier/ // https://github.com/Aircoookie/WLED/blob/main/wled00/udp.cpp - if (size < 34) { + if (size <= 34) { return false; } From 99a805cba675ab2399d432dd39d142a0852fafc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:52:10 -1000 Subject: [PATCH 0407/2056] Bump the docker-actions group across 1 directory with 2 updates (#14520) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index a83bcae0b06..4009ac1e174 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17a2616dffc..8f68e9c873f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,15 +99,15 @@ jobs: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -178,17 +178,17 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} From 291679126f532a6b036c10984c97c47db0be9cd3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:55:54 -0500 Subject: [PATCH 0408/2056] [nfc] Fix off-by-one in NDEF message parsing (#14485) Co-authored-by: Claude Opus 4.6 --- esphome/components/nfc/ndef_message.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index e7304445c55..35028555c55 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -8,8 +8,14 @@ static const char *const TAG = "nfc.ndef_message"; NdefMessage::NdefMessage(std::vector &data) { ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size()); - uint8_t index = 0; - while (index <= data.size()) { + size_t index = 0; + while (index < data.size()) { + // Minimum record: TNF byte + type length byte + payload length (1 or 4 bytes) + if (index + 2 >= data.size()) { + ESP_LOGE(TAG, "Truncated record header; aborting"); + break; + } + uint8_t tnf_byte = data[index++]; bool me = tnf_byte & 0x40; // Message End bit (is set if this is the last record of the message) bool sr = tnf_byte & 0x10; // Short record bit (is set if payload size is less or equal to 255 bytes) @@ -23,6 +29,10 @@ NdefMessage::NdefMessage(std::vector &data) { if (sr) { payload_length = data[index++]; } else { + if (index + 4 > data.size()) { + ESP_LOGE(TAG, "Truncated payload length; aborting"); + break; + } payload_length = (static_cast(data[index]) << 24) | (static_cast(data[index + 1]) << 16) | (static_cast(data[index + 2]) << 8) | static_cast(data[index + 3]); index += 4; @@ -30,6 +40,10 @@ NdefMessage::NdefMessage(std::vector &data) { uint8_t id_length = 0; if (il) { + if (index >= data.size()) { + ESP_LOGE(TAG, "Truncated ID length; aborting"); + break; + } id_length = data[index++]; } From e25d740968f656b8e50dedb793fbc5370573f3ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 09:58:58 -1000 Subject: [PATCH 0409/2056] [wifi] Cache is_connected() for cheap inline access (#14463) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 8 +++++--- esphome/components/wifi/wifi_component.h | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 852ff922f1f..8b60810d28a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -725,6 +725,7 @@ void WiFiComponent::restart_adapter() { void WiFiComponent::loop() { this->wifi_loop_(); const uint32_t now = App.get_loop_component_start_time(); + this->update_connected_state_(); if (this->has_sta()) { #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) @@ -776,7 +777,7 @@ void WiFiComponent::loop() { } case WIFI_COMPONENT_STATE_STA_CONNECTED: { - if (!this->is_connected()) { + if (!this->is_connected_()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); @@ -2118,15 +2119,16 @@ bool WiFiComponent::can_proceed() { if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { return true; } - return this->is_connected(); + return this->is_connected_(); } #endif void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected() const { +bool WiFiComponent::is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } +void WiFiComponent::update_connected_state_() { this->connected_ = this->is_connected_(); } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a6f03a08d9d..f340b708c90 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -443,7 +443,7 @@ class WiFiComponent : public Component { void set_reboot_timeout(uint32_t reboot_timeout); - bool is_connected() const; + bool is_connected() const { return this->connected_; } void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } @@ -678,6 +678,8 @@ class WiFiComponent : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_() const; + bool is_connected_() const; + void update_connected_state_(); bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP @@ -854,6 +856,7 @@ class WiFiComponent : public Component { bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started bool skip_cooldown_next_cycle_{false}; + bool connected_{false}; bool post_connect_roaming_{true}; // Enabled by default #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) bool is_high_performance_mode_{false}; From d11e7cab464a007bccf37cf0cf3da4d8b1f71861 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:18:54 -0500 Subject: [PATCH 0410/2056] [xiaomi_ble][pvvx_mithermometer][atc_mithermometer] Add BLE service data bounds checks (#14514) Co-authored-by: Claude Opus 4.6 --- .../components/atc_mithermometer/atc_mithermometer.cpp | 4 ++++ .../components/pvvx_mithermometer/pvvx_mithermometer.cpp | 4 ++++ esphome/components/xiaomi_ble/xiaomi_ble.cpp | 9 +++++++++ 3 files changed, 17 insertions(+) diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index b4d2929742a..9afd6334f5b 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -61,6 +61,10 @@ optional ATCMiThermometer::parse_header_(const esp32_ble_tracker::S } auto raw = service_data.data; + if (raw.size() < 13) { + ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size()); + return {}; + } static uint8_t last_frame_count = 0; if (last_frame_count == raw[12]) { diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 57124479090..239a1e74fe6 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -61,6 +61,10 @@ optional PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: } auto raw = service_data.data; + if (raw.size() < 14) { + ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size()); + return {}; + } static uint8_t last_frame_count = 0; if (last_frame_count == raw[13]) { diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 0018d35f1f5..97a660f0e3a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -121,6 +121,11 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult // Byte 2: length // Byte 3..3+len-1: data point value + if (result.raw_offset < 0 || static_cast(result.raw_offset) >= message.size()) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): raw_offset (%d) exceeds message size (%d)!", result.raw_offset, + message.size()); + return false; + } const uint8_t *payload = message.data() + result.raw_offset; uint8_t payload_length = message.size() - result.raw_offset; uint8_t payload_offset = 0; @@ -165,6 +170,10 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } auto raw = service_data.data; + if (raw.size() < 5) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): service data too short (%d).", raw.size()); + return {}; + } result.has_data = raw[0] & 0x40; result.has_capability = raw[0] & 0x20; result.has_encryption = raw[0] & 0x08; From b0be02e16d8b53efee692543c9be2c6da2118da7 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 5 Mar 2026 12:54:17 -0800 Subject: [PATCH 0411/2056] [modbus] Fix timing bugs and better adhere to spec (#8032) Co-authored-by: brambo123 <52667932+brambo123@users.noreply.github.com> Co-authored-by: Keith Burzinski Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../growatt_solar/growatt_solar.cpp | 2 +- esphome/components/modbus/__init__.py | 5 + esphome/components/modbus/modbus.cpp | 294 ++++++++++++------ esphome/components/modbus/modbus.h | 55 +++- .../components/modbus/modbus_definitions.h | 2 + .../components/modbus_controller/__init__.py | 3 +- .../modbus_controller/modbus_controller.cpp | 2 +- tests/components/modbus/common.yaml | 2 + .../external_components/uart_mock/__init__.py | 4 +- .../uart_mock/uart_mock.cpp | 26 +- .../external_components/uart_mock/uart_mock.h | 5 +- .../fixtures/uart_mock_modbus.yaml | 48 ++- .../fixtures/uart_mock_modbus_timing.yaml | 2 + tests/integration/test_uart_mock_modbus.py | 67 +++- 14 files changed, 396 insertions(+), 121 deletions(-) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index 686c1c232e7..29974258723 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -26,7 +26,7 @@ void GrowattSolar::update() { } // The bus might be slow, or there might be other devices, or other components might be talking to our device. - if (this->waiting_for_response()) { + if (!this->ready_for_immediate_send()) { this->waiting_to_update_ = true; return; } diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 2bd85c6121f..f6e0f98857d 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -20,6 +20,7 @@ MULTI_CONF = True CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" +CONF_TURNAROUND_TIME = "turnaround_time" ModbusRole = modbus_ns.enum("ModbusRole") MODBUS_ROLES = { @@ -36,6 +37,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_SEND_WAIT_TIME, default="250ms" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TURNAROUND_TIME, default="100ms" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean, } ) @@ -57,6 +61,7 @@ async def to_code(config): cg.add(var.set_flow_control_pin(pin)) cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + cg.add(var.set_turnaround_time(config[CONF_TURNAROUND_TIME])) cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC])) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index d40343db33d..28e26e307e3 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -15,10 +15,69 @@ void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); } -} -void Modbus::loop() { - const uint32_t now = App.get_loop_component_start_time(); + this->frame_delay_ms_ = + std::max(2, // 1750us minimum per spec - rounded up to 2ms. + // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay) + (uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1); + + this->long_rx_buffer_delay_ms_ = + (this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1; +} + +void Modbus::loop() { + // First process all available incoming data. + this->receive_and_parse_modbus_bytes_(); + + // If the response frame is finished (including interframe delay) - we timeout. + // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts + // when the buffer is filling the back half of the response + const uint16_t timeout = std::max( + (uint16_t) this->frame_delay_ms_, + (uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_ + : 0)); + // We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps + // It's critical in all timestamp comparisons that the left timestamp comes before the right one in time + // If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop + // then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout + // So in this component we don't use any cached timestamp values to avoid these annoying bugs + if (millis() - this->last_modbus_byte_ > timeout) { + this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true); + } + + // If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response + if (this->waiting_for_response_ != 0 && + millis() - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ && + (this->rx_buffer_.empty() || this->rx_buffer_[0] != this->waiting_for_response_)) { + ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", + this->waiting_for_response_, millis() - this->last_send_); + this->waiting_for_response_ = 0; + } + + // If there's no response pending and there's commands in the buffer + this->send_next_frame_(); +} + +bool Modbus::tx_blocked() { + const uint32_t now = millis(); + + // We block transmission in any of these case: + // 1. There are bytes in the UART Rx buffer + // 2. There are bytes in our Rx buffer + // 3. We're waiting for a response + // 4. The last sent byte isn't more than frame_delay ms ago (i.e. wait to tell receivers that our previous Tx is done) + // 5. The last received byte isn't more than frame_delay ms ago (i.e. wait to be sure there isn't more Rx coming) + // 6. If we're a client - also wait for the turnaround delay, to give the servers time to process the previous message + return this->available() || !this->rx_buffer_.empty() || (this->waiting_for_response_ != 0) || + (now - this->last_send_ < this->last_send_tx_offset_ + this->frame_delay_ms_ + + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)) || + (now - this->last_modbus_byte_ < + this->frame_delay_ms_ + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)); +} + +bool Modbus::tx_buffer_empty() { return this->tx_buffer_.empty(); } + +void Modbus::receive_and_parse_modbus_bytes_() { // Read all available bytes in batches to reduce UART call overhead. size_t avail = this->available(); uint8_t buf[64]; @@ -28,33 +87,20 @@ void Modbus::loop() { break; } avail -= to_read; - for (size_t i = 0; i < to_read; i++) { - if (this->parse_modbus_byte_(buf[i])) { - this->last_modbus_byte_ = now; + if (this->rx_buffer_.empty()) { + ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i], + millis() - this->last_send_); } else { - size_t at = this->rx_buffer_.size(); - if (at > 0) { - ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); - this->rx_buffer_.clear(); - } + ESP_LOGVV(TAG, "Received byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i], + millis() - this->last_send_); } - } - } - if (now - this->last_modbus_byte_ > 50) { - size_t at = this->rx_buffer_.size(); - if (at > 0) { - ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at); - this->rx_buffer_.clear(); - } - - // stop blocking new send commands after sent_wait_time_ ms after response received - if (now - this->last_send_ > send_wait_time_) { - if (waiting_for_response > 0) { - ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response); + // If the bytes in the rx buffer do not parse, clear out the buffer + if (!this->parse_modbus_byte_(buf[i])) { + this->clear_rx_buffer_(LOG_STR("parse failed"), true); } - waiting_for_response = 0; + this->last_modbus_byte_ = millis(); } } } @@ -63,7 +109,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; - ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); + // Byte 0: modbus address (match all) if (at == 0) return true; @@ -101,7 +147,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { if (computed_crc != remote_crc) return true; - ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); + ESP_LOGD(TAG, "User-defined function %02X found", function_code); } else { // data starts at 2 and length is 4 for read registers commands @@ -152,9 +198,19 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); if (computed_crc != remote_crc) { if (this->disable_crc_) { - ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc); + ESP_LOGD(TAG, "CRC check failed %" PRIu32 "ms after last send; ignoring", millis() - this->last_send_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc, + format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size())); } else { - ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + ESP_LOGW(TAG, "CRC check failed %" PRIu32 "ms after last send", millis() - this->last_send_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc, + format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size())); return false; } } @@ -164,52 +220,101 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { for (auto *device : this->devices_) { if (device->address_ == address) { found = true; - // Is it an error response? - if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { - ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); - if (waiting_for_response != 0) { - device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]); - } else { - // Ignore modbus exception not related to a pending command - ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); - } - continue; - } if (this->role == ModbusRole::SERVER) { if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS || function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) { device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), uint16_t(data[3]) | (uint16_t(data[2]) << 8)); - continue; - } - if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || - function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER || + function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { device->on_modbus_write_registers(function_code, data); - continue; + } + } else { // We're a client + // Is it an error response? + if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) { + uint8_t exception = raw[2]; + ESP_LOGW(TAG, + "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 + "ms after last send", + function_code, exception, address, millis() - this->last_send_); + if (this->waiting_for_response_ == address) { + device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception); + } else { + // Ignore modbus exception not related to a pending command + ESP_LOGD(TAG, "Ignoring error - not expecting a response from %" PRIu8 "", address); + } + } else { // Not an error response + if (this->waiting_for_response_ == address) { + device->on_modbus_data(data); + } else { + // Ignore modbus response not related to a pending command + ESP_LOGW(TAG, "Ignoring response - not expecting a response from %" PRIu8 ", %" PRIu32 "ms after last send", + address, millis() - this->last_send_); + } } } - // fallthrough for other function codes - device->on_modbus_data(data); } } - waiting_for_response = 0; - if (!found) { - ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); + if (!found && this->role == ModbusRole::CLIENT) { + ESP_LOGW(TAG, "Got frame from unknown address %" PRIu8 ", %" PRIu32 "ms after last send", address, + millis() - this->last_send_); } - // reset buffer - ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at); - this->rx_buffer_.clear(); + this->clear_rx_buffer_(LOG_STR("parse succeeded")); + + if (this->waiting_for_response_ == address) + this->waiting_for_response_ = 0; + return true; } +void Modbus::send_next_frame_() { + if (this->tx_buffer_.empty()) + return; + + if (this->tx_blocked()) + return; + + const ModbusDeviceCommand &frame = this->tx_buffer_.front(); + + if (this->role == ModbusRole::CLIENT) { + this->waiting_for_response_ = frame.data.get()[0]; + } + + if (this->flow_control_pin_ != nullptr) { + this->flow_control_pin_->digital_write(true); + this->write_array(frame.data.get(), frame.size); + this->flush(); + this->flow_control_pin_->digital_write(false); + this->last_send_tx_offset_ = 0; + } else { + this->write_array(frame.data.get(), frame.size); + this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1; + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send", format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), + millis() - this->last_send_); + this->last_send_ = millis(); + this->tx_buffer_.pop_front(); + if (!this->tx_buffer_.empty()) { + ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size()); + } +} + void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:\n" " Send Wait Time: %d ms\n" + " Turnaround Time: %d ms\n" + " Frame Delay: %d ms\n" + " Long Rx Buffer Delay: %d ms\n" " CRC Disabled: %s", - this->send_wait_time_, YESNO(this->disable_crc_)); + this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_, + this->long_rx_buffer_delay_ms_, YESNO(this->disable_crc_)); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); } float Modbus::get_setup_priority() const { @@ -228,15 +333,6 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address return; } - static constexpr size_t ADDR_SIZE = 1; - static constexpr size_t FC_SIZE = 1; - static constexpr size_t START_ADDR_SIZE = 2; - static constexpr size_t NUM_ENTITIES_SIZE = 2; - static constexpr size_t BYTE_COUNT_SIZE = 1; - static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits::max(); - static constexpr size_t CRC_SIZE = 2; - static constexpr size_t MAX_FRAME_SIZE = - ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE; uint8_t data[MAX_FRAME_SIZE]; size_t pos = 0; @@ -259,29 +355,16 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address } else { payload_len = 2; // Write single register or coil } + if (payload_len + pos + 2 > MAX_FRAME_SIZE) { // Check if payload fits (accounting for CRC) + ESP_LOGE(TAG, "Payload too large to send: %d bytes", payload_len); + return; + } for (int i = 0; i < payload_len; i++) { data[pos++] = payload[i]; } } - auto crc = crc16(data, pos); - data[pos++] = crc >> 0; - data[pos++] = crc >> 8; - - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(true); - - this->write_array(data, pos); - this->flush(); - - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(false); - waiting_for_response = address; - last_send_ = millis(); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; -#endif - ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos)); + this->queue_raw_(data, pos); } // Helper function for lambdas @@ -290,23 +373,44 @@ void Modbus::send_raw(const std::vector &payload) { if (payload.empty()) { return; } + // Frame size: payload + CRC(2) + if (payload.size() + 2 > MAX_FRAME_SIZE) { + ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %d bytes", MAX_FRAME_SIZE); + return; + } + // Use stack buffer - Modbus frames are small and bounded + uint8_t data[MAX_FRAME_SIZE]; - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(true); + std::memcpy(data, payload.data(), payload.size()); - auto crc = crc16(payload.data(), payload.size()); - this->write_array(payload); - this->write_byte(crc & 0xFF); - this->write_byte((crc >> 8) & 0xFF); - this->flush(); - if (this->flow_control_pin_ != nullptr) - this->flow_control_pin_->digital_write(false); - waiting_for_response = payload[0]; -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; + this->queue_raw_(data, payload.size()); +} + +// Assume data and length is valid and append CRC, then queue for sending. Used internally to avoid unnecessary copying +// of data into vectors +void Modbus::queue_raw_(const uint8_t *data, uint16_t len) { + if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) { + this->tx_buffer_.emplace_back(data, len); + } else { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; #endif - ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size())); - last_send_ = millis(); + ESP_LOGE(TAG, "Write buffer full, dropped: %s", format_hex_pretty_to(hex_buf, data, len)); + } +} + +void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + if (warn) { + ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + millis() - this->last_send_); + } else { + ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + millis() - this->last_send_); + } + this->rx_buffer_.clear(); + } } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index fac74aaadfd..c90d4c78ae2 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -5,11 +5,16 @@ #include "esphome/components/modbus/modbus_definitions.h" +#include +#include #include +#include namespace esphome { namespace modbus { +static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15; + enum ModbusRole { CLIENT, SERVER, @@ -17,6 +22,19 @@ enum ModbusRole { class ModbusDevice; +struct ModbusDeviceCommand { + // Frame with exact-size allocation to avoid std::vector overhead + std::unique_ptr data; + uint16_t size; // Modbus RTU max is 256 bytes + + ModbusDeviceCommand(const uint8_t *src, uint16_t len) : data(std::make_unique(len + 2)), size(len + 2) { + std::memcpy(this->data.get(), src, len); + auto crc = crc16(data.get(), len); + data[len + 0] = crc >> 0; + data[len + 1] = crc >> 8; + } +}; + class Modbus : public uart::UARTDevice, public Component { public: Modbus() = default; @@ -30,28 +48,45 @@ class Modbus : public uart::UARTDevice, public Component { void register_device(ModbusDevice *device) { this->devices_.push_back(device); } float get_setup_priority() const override; + bool tx_buffer_empty(); + bool tx_blocked(); void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr); void send_raw(const std::vector &payload); void set_role(ModbusRole role) { this->role = role; } void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } - uint8_t waiting_for_response{0}; - void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } - void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; } + void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; } + void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; } + void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; } ModbusRole role; protected: - GPIOPin *flow_control_pin_{nullptr}; - bool parse_modbus_byte_(uint8_t byte); - uint16_t send_wait_time_{250}; - bool disable_crc_; - std::vector rx_buffer_; + void receive_and_parse_modbus_bytes_(); + void clear_rx_buffer_(const LogString *reason, bool warn = false); + void send_next_frame_(); + void queue_raw_(const uint8_t *data, uint16_t len); + uint32_t last_modbus_byte_{0}; uint32_t last_send_{0}; + uint32_t last_send_tx_offset_{0}; + uint16_t frame_delay_ms_{5}; + uint16_t long_rx_buffer_delay_ms_{0}; + uint16_t send_wait_time_{250}; + uint16_t turnaround_delay_ms_{100}; + uint8_t waiting_for_response_{0}; + bool disable_crc_{false}; + + GPIOPin *flow_control_pin_{nullptr}; + + std::vector rx_buffer_; std::vector devices_; + // std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many + // requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling + // may change at run time. + std::deque tx_buffer_; }; class ModbusDevice { @@ -76,7 +111,9 @@ class ModbusDevice { this->send_raw(error_response); } // If more than one device is connected block sending a new command before a response is received - bool waiting_for_response() { return parent_->waiting_for_response != 0; } + ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0") + bool waiting_for_response() { return !ready_for_immediate_send(); } + bool ready_for_immediate_send() { return parent_->tx_buffer_empty() && !parent_->tx_blocked(); } protected: friend Modbus; diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 07f101ae4c5..c86d548578a 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -81,6 +81,8 @@ const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B // 6.3 03 (0x03) Read Holding Registers // 6.4 04 (0x04) Read Input Registers const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D + +static constexpr uint16_t MAX_FRAME_SIZE = 256; /// End of Modbus definitions } // namespace modbus } // namespace esphome diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index c45c338bb32..aea79b20536 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -48,6 +48,7 @@ CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") +modbus_ns = cg.esphome_ns.namespace("modbus") ModbusController = modbus_controller_ns.class_( "ModbusController", cg.PollingComponent, modbus.ModbusDevice ) @@ -56,7 +57,7 @@ SensorItem = modbus_controller_ns.struct("SensorItem") ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") -ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") MODBUS_FUNCTION_CODE = { "read_coils": ModbusFunctionCode.READ_COILS, diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 50bd9f45cbd..7f0eb230e0a 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -18,7 +18,7 @@ void ModbusController::setup() { this->create_register_ranges_(); } bool ModbusController::send_next_command_() { uint32_t last_send = millis() - this->last_command_timestamp_; - if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) { + if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) { auto &command = this->command_queue_.front(); // remove from queue if command was sent too often diff --git a/tests/components/modbus/common.yaml b/tests/components/modbus/common.yaml index d636143ec98..221aab4ed82 100644 --- a/tests/components/modbus/common.yaml +++ b/tests/components/modbus/common.yaml @@ -1,3 +1,5 @@ modbus: id: mod_bus1 flow_control_pin: ${flow_control_pin} + send_wait_time: 500ms + turnaround_time: 100ms diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index abb3abcc419..c10d73354e2 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -71,6 +71,7 @@ RESPONSE_SCHEMA = cv.Schema( { cv.Required(CONF_EXPECT_TX): [cv.hex_uint8_t], cv.Required(CONF_INJECT_RX): [cv.hex_uint8_t], + cv.Optional(CONF_DELAY, default="0ms"): cv.positive_time_period_milliseconds, } ) @@ -151,7 +152,8 @@ async def to_code(config): for response in config[CONF_RESPONSES]: tx_data = response[CONF_EXPECT_TX] rx_data = response[CONF_INJECT_RX] - cg.add(var.add_response(tx_data, rx_data)) + delay_ms = response[CONF_DELAY] + cg.add(var.add_response(tx_data, rx_data, delay_ms)) for periodic in config[CONF_PERIODIC_RX]: data = periodic[CONF_DATA] diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 83a13793be7..affcc8d908d 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -36,8 +36,8 @@ void MockUartComponent::loop() { // component (e.g., LD2410) a chance to process each batch independently. if (this->injection_index_ < this->injections_.size()) { auto &injection = this->injections_[this->injection_index_]; - uint32_t target_time = this->scenario_start_ms_ + this->cumulative_delay_ms_ + injection.delay_ms; - if (now >= target_time) { + uint32_t total_delay = this->cumulative_delay_ms_ + injection.delay_ms; + if (now - this->scenario_start_ms_ >= total_delay) { ESP_LOGD(TAG, "Injecting %zu RX bytes (injection %u)", injection.rx_data.size(), this->injection_index_); this->inject_to_rx_buffer(injection.rx_data); this->cumulative_delay_ms_ += injection.delay_ms; @@ -52,6 +52,15 @@ void MockUartComponent::loop() { periodic.last_inject_ms = now; } } + + // Process delayed responses + for (auto &response : this->responses_) { + if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { + ESP_LOGD(TAG, "Injecting %zu RX bytes for delayed response", response.inject_rx.size()); + this->inject_to_rx_buffer(response.inject_rx); + response.last_match_ms = 0; // Reset to prevent repeated injection + } + } } void MockUartComponent::start_scenario() { @@ -149,8 +158,9 @@ void MockUartComponent::add_injection(const std::vector &rx_data, uint3 this->injections_.push_back({rx_data, delay_ms}); } -void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx) { - this->responses_.push_back({expect_tx, inject_rx}); +void MockUartComponent::add_response(const std::vector &expect_tx, const std::vector &inject_rx, + uint32_t delay_ms) { + this->responses_.push_back({expect_tx, inject_rx, delay_ms, 0}); } void MockUartComponent::add_periodic_rx(const std::vector &data, uint32_t interval_ms) { @@ -166,7 +176,13 @@ void MockUartComponent::try_match_response_() { size_t offset = this->tx_buffer_.size() - response.expect_tx.size(); if (std::equal(response.expect_tx.begin(), response.expect_tx.end(), this->tx_buffer_.begin() + offset)) { ESP_LOGD(TAG, "TX match found, injecting %zu RX bytes", response.inject_rx.size()); - this->inject_to_rx_buffer(response.inject_rx); + if (response.delay_ms > 0) { + ESP_LOGD(TAG, "Delaying response by %u ms", response.delay_ms); + // Schedule the response injection as a future injection + response.last_match_ms = App.get_loop_component_start_time(); + } else { + this->inject_to_rx_buffer(response.inject_rx); + } this->tx_buffer_.clear(); return; } diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index b721512f96c..901e371dec4 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -34,7 +34,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { // Scenario configuration - called from generated code void add_injection(const std::vector &rx_data, uint32_t delay_ms); - void add_response(const std::vector &expect_tx, const std::vector &inject_rx); + void add_response(const std::vector &expect_tx, const std::vector &inject_rx, + uint32_t delay_ms = 0); void add_periodic_rx(const std::vector &data, uint32_t interval_ms); void start_scenario(); @@ -64,6 +65,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { struct Response { std::vector expect_tx; std::vector inject_rx; + uint32_t delay_ms; + uint32_t last_match_ms{0}; }; std::vector responses_; std::vector tx_buffer_; diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 0a3492a0d2f..3ff7ab01bdf 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -25,20 +25,64 @@ uart_mock: auto_start: false debug: responses: - - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 1 on device 1 + - expect_tx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_register) inject_rx: [0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5] # Return value 0x0103 (hex) = 259 (dec) + - expect_tx: [0x01, 0x03, 0x00, 0x05, 0x00, 0x01, 0x94, 0x0B] # Read holding register 5 on device 1 (delayed_response) + delay: 100ms # Shorter than modbus send_wait_time of 200ms, should succeed + inject_rx: [0x01, 0x03, 0x02, 0x00, 0xFF, 0xF8, 0x04] # Return value 0x00FF (hex) = 255 (dec) + - expect_tx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2 (late_response) + delay: 300ms # Longer than modbus send_wait_time of 200ms, should cause timeout + inject_rx: [0x02, 0x03, 0x02, 0x00, 0xF0, 0xFC, 0x00] # Return value 0x00F0 (hex) = 240 (dec) + - expect_tx: [0x03, 0x03, 0x00, 0x09, 0x00, 0x01, 0x55, 0xEA] # Read holding register 9 on device 3 (no_response) + inject_rx: [] # No response, should cause timeout + - expect_tx: [0x01, 0x03, 0x00, 0x0A, 0x00, 0x01, 0xA4, 0x08] # Read holding register A on device 1 (exception_response) + inject_rx: [0x01, 0x83, 0x02, 0xC0, 0xF1] # Exception response with code 2 (illegal data address) modbus: uart_id: virtual_uart_dev + send_wait_time: 200ms + turnaround_time: 10ms modbus_controller: - address: 1 + - address: 1 + id: modbus_controller_ok + max_cmd_retries: 0 + update_interval: 1s + - address: 2 + id: modbus_controller_slow + max_cmd_retries: 0 + update_interval: 1s + - address: 3 + id: modbus_controller_offline + max_cmd_retries: 0 + update_interval: 1s sensor: - platform: modbus_controller name: "basic_register" address: 0x03 register_type: holding + modbus_controller_id: modbus_controller_ok + - platform: modbus_controller + name: "delayed_response" + address: 0x05 + register_type: holding + modbus_controller_id: modbus_controller_ok + - platform: modbus_controller + name: "late_response" + address: 0x07 + register_type: holding + modbus_controller_id: modbus_controller_slow + - platform: modbus_controller + name: "no_response" + address: 0x09 + register_type: holding + modbus_controller_id: modbus_controller_offline + - platform: modbus_controller + name: "exception_response" + address: 0x0A + register_type: holding + modbus_controller_id: modbus_controller_ok button: - platform: template diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index c4e29e5fe8d..f4cf0bde37e 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -46,10 +46,12 @@ uart_mock: modbus: uart_id: virtual_uart_dev + turnaround_time: 10ms sensor: - platform: sdm_meter address: 2 + update_interval: 1s phase_a: voltage: name: sdm_voltage diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index bf3c0697502..6901dc27fe1 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -39,9 +39,17 @@ async def test_uart_mock_modbus( # Track sensor state updates (after initial state is swallowed) sensor_states: dict[str, list[float]] = { "basic_register": [], + "delayed_response": [], + "late_response": [], + "no_response": [], + "exception_response": [], } basic_register_changed = loop.create_future() + delayed_response_changed = loop.create_future() + late_response_changed = loop.create_future() + no_response_changed = loop.create_future() + exception_response_changed = loop.create_future() def on_state(state: EntityState) -> None: if isinstance(state, SensorState) and not state.missing_state: @@ -54,6 +62,23 @@ async def test_uart_mock_modbus( and not basic_register_changed.done() ): basic_register_changed.set_result(True) + elif ( + sensor_name == "delayed_response" + and state.state == 255.0 + and not delayed_response_changed.done() + ): + delayed_response_changed.set_result(True) + elif ( + sensor_name == "late_response" and not late_response_changed.done() + ): + late_response_changed.set_result(True) + elif sensor_name == "no_response" and not no_response_changed.done(): + no_response_changed.set_result(True) + elif ( + sensor_name == "exception_response" + and not exception_response_changed.done() + ): + exception_response_changed.set_result(True) async with ( run_compiled(yaml_config), @@ -79,20 +104,52 @@ async def test_uart_mock_modbus( assert start_btn is not None, "Start Scenario button not found" client.button_command(start_btn.key) + try: + await asyncio.wait_for(delayed_response_changed, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for delayed_response change. Received sensor states:\n" + f" delayed_response: {sensor_states['delayed_response']}\n" + ) + + try: + await asyncio.wait_for(late_response_changed, timeout=2.0) + pytest.fail( + f"late_response change should not have been triggered, but was. Received sensor states:\n" + f" late_response: {sensor_states['late_response']}\n" + ) + except TimeoutError: + pass # Expected timeout since we never inject a response for late_response + + try: + await asyncio.wait_for(no_response_changed, timeout=2.0) + pytest.fail( + f"no_response change should not have been triggered, but was. Received sensor states:\n" + f" no_response: {sensor_states['no_response']}\n" + ) + except TimeoutError: + pass # Expected timeout since we never inject a response for no_response + # Wait for basic register to be updated with successful parse try: - await asyncio.wait_for(basic_register_changed, timeout=15.0) + await asyncio.wait_for(basic_register_changed, timeout=2.0) except TimeoutError: pytest.fail( f"Timeout waiting for Basic Register change. Received sensor states:\n" f" basic_register: {sensor_states['basic_register']}\n" ) + try: + await asyncio.wait_for(exception_response_changed, timeout=2.0) + pytest.fail( + f"exception_response change should not have been triggered, but was. Received sensor states:\n" + f" exception_response: {sensor_states['exception_response']}\n" + ) + except TimeoutError: + pass + @pytest.mark.asyncio -@pytest.mark.xfail( - reason="There is a bug in UART which will timeout for long responses." -) async def test_uart_mock_modbus_timing( yaml_config: str, run_compiled: RunCompiledFunction, @@ -155,7 +212,7 @@ async def test_uart_mock_modbus_timing( # Wait for voltage to be updated with successful parse try: - await asyncio.wait_for(voltage_changed, timeout=15.0) + await asyncio.wait_for(voltage_changed, timeout=2.0) except TimeoutError: pytest.fail( f"Timeout waiting for SDM voltage change. Received sensor states:\n" From 3392e4d73b564877f1a7d9b4c80f534d24a48d56 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:08:58 -0500 Subject: [PATCH 0412/2056] [usb_uart][nextion][feedback][whirlpool][packet_transport][he60r][hc8][runtime_stats] Fix millis() wrapping bugs (#14474) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/feedback/feedback_cover.cpp | 13 +++++++++---- esphome/components/hc8/hc8.cpp | 16 ++++++++++------ esphome/components/hc8/hc8.h | 1 + esphome/components/he60r/he60r.cpp | 2 +- esphome/components/nextion/nextion.cpp | 6 +++--- .../packet_transport/packet_transport.cpp | 11 ++++++----- .../components/runtime_stats/runtime_stats.cpp | 14 +++----------- esphome/components/runtime_stats/runtime_stats.h | 10 +++++++--- esphome/components/usb_uart/usb_uart.cpp | 4 ++-- esphome/components/whirlpool/whirlpool.h | 2 +- esphome/core/component.cpp | 2 +- 11 files changed, 44 insertions(+), 37 deletions(-) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index d247bada33f..1dff210cd6b 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -437,10 +437,15 @@ void FeedbackCover::recompute_position_() { } // check if we have an acceleration_wait_time, and remove from position computation - if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { - this->position += - dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / - (action_dur - this->acceleration_wait_time_); + if (now - this->start_dir_time_ > this->acceleration_wait_time_) { + uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_; + uint32_t effective_start; + if (static_cast(accel_end_time - this->last_recompute_time_) >= 0) { + effective_start = accel_end_time; + } else { + effective_start = this->last_recompute_time_; + } + this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_); this->position = clamp(this->position, min_pos, max_pos); } this->last_recompute_time_ = now; diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4c2d367b244..900acca6918 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -24,12 +24,16 @@ void HC8Component::setup() { } void HC8Component::update() { - uint32_t now_ms = App.get_loop_component_start_time(); - uint32_t warmup_ms = this->warmup_seconds_ * 1000; - if (now_ms < warmup_ms) { - ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); - this->status_set_warning(); - return; + if (!this->warmup_complete_) { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + this->warmup_complete_ = true; + this->status_clear_warning(); } while (this->available()) diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 74257fab148..b060f38a806 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -23,6 +23,7 @@ class HC8Component : public PollingComponent, public uart::UARTDevice { protected: sensor::Sensor *co2_sensor_{nullptr}; uint32_t warmup_seconds_{0}; + bool warmup_complete_{false}; }; template class HC8CalibrateAction : public Action, public Parented { diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index fdcd1a29c05..47440cc1f73 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -239,7 +239,7 @@ void HE60rCover::recompute_position_() { return; const uint32_t now = millis(); - if (now > this->last_recompute_time_) { + if (now != this->last_recompute_time_) { auto diff = (unsigned) (now - last_recompute_time_); float delta; switch (this->current_operation) { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 9f1ce47837c..c8c1b6fa412 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -337,7 +337,7 @@ void Nextion::loop() { this->started_ms_ = App.get_loop_component_start_time(); if (this->startup_override_ms_ > 0 && - this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { + App.get_loop_component_start_time() - this->started_ms_ > this->startup_override_ms_) { ESP_LOGV(TAG, "Manual ready set"); this->connection_state_.nextion_reports_is_setup_ = true; } @@ -853,10 +853,10 @@ void Nextion::process_nextion_commands_() { const uint32_t ms = App.get_loop_component_start_time(); if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && - this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { NextionComponentBase *component = this->nextion_queue_[i]->component; - if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { + if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) { if (this->nextion_queue_[i]->queue_time == 0) { ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index f241cc61142..6f1286b4693 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -330,15 +330,16 @@ void PacketTransport::update() { if (!this->ping_pong_enable_) { return; } - auto now = millis() / 1000; - if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { + uint32_t now = millis(); + uint32_t ping_request_age = now - this->last_key_time_; + if (ping_request_age > this->ping_pong_recyle_time_ * 1000u) { this->resend_ping_key_ = this->ping_pong_enable_; - ESP_LOGV(TAG, "Ping request, age %" PRIu32, now - this->last_key_time_); + ESP_LOGV(TAG, "Ping request, age %" PRIu32, ping_request_age); this->last_key_time_ = now; } for (const auto &provider : this->providers_) { uint32_t key_response_age = now - provider.second.last_key_response_time; - if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) { + if (key_response_age > (this->ping_pong_recyle_time_ * 2000u)) { #ifdef USE_STATUS_SENSOR if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) { ESP_LOGI(TAG, "Ping status for %s timeout at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now, @@ -496,7 +497,7 @@ void PacketTransport::process_(std::span data) { if (decoder.decode(PING_KEY, key) == DECODE_OK) { if (key == this->ping_key_) { ping_key_seen = true; - provider.last_key_response_time = millis() / 1000; + provider.last_key_response_time = millis(); ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time); } else { ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index d9fa22d9495..cb28acc96c6 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -9,21 +9,16 @@ namespace esphome { namespace runtime_stats { -RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { +RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(60000) { global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us, uint32_t current_time) { +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) { if (component == nullptr) return; // Record stats using component pointer as key this->component_stats_[component].record_time(duration_us); - - if (this->next_log_time_ == 0) { - this->next_log_time_ = current_time + this->log_interval_; - return; - } } void RuntimeStatsCollector::log_stats_() { @@ -88,10 +83,7 @@ void RuntimeStatsCollector::log_stats_() { } void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { - if (this->next_log_time_ == 0) - return; - - if (current_time >= this->next_log_time_) { + if ((int32_t) (current_time - this->next_log_time_) >= 0) { this->log_stats_(); this->reset_stats_(); this->next_log_time_ = current_time + this->log_interval_; diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 08475297208..303d895985f 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -7,6 +7,7 @@ #include #include #include +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -80,10 +81,13 @@ class RuntimeStatsCollector { public: RuntimeStatsCollector(); - void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } + void set_log_interval(uint32_t log_interval) { + this->log_interval_ = log_interval; + this->next_log_time_ = millis() + log_interval; + } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_us, uint32_t current_time); + void record_component_time(Component *component, uint32_t duration_us); // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); @@ -101,7 +105,7 @@ class RuntimeStatsCollector { // We use Component* as the key since each component is unique std::map component_stats_; uint32_t log_interval_; - uint32_t next_log_time_; + uint32_t next_log_time_{0}; }; } // namespace runtime_stats diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 5c0397b2cb4..e20bbd02dbc 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -171,8 +171,8 @@ void USBUartChannel::flush() { // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; // in that case the main loop is blocked for the full duration. - uint32_t deadline = millis() + 100; // 100 ms safety timeout - while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() < deadline) { + uint32_t start = millis(); // 100 ms safety timeout + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < 100) { // Kick start_output() in case data arrived but no transfer is in flight yet. this->parent_->start_output(this); yield(); diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 907a21225ce..992b2a7adfd 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -48,7 +48,7 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; /// Set the time of the last transmission. - int32_t last_transmit_time_{}; + uint32_t last_transmit_time_{}; bool send_swing_cmd_{false}; Model model_; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8c2c8d38e8a..a9ff3ec1eb5 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -534,7 +534,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { // 1ms granularity, so results were essentially random noise. if (global_runtime_stats != nullptr) { uint32_t duration_us = micros() - this->started_us_; - global_runtime_stats->record_component_time(this->component_, duration_us, curr_time); + global_runtime_stats->record_component_time(this->component_, duration_us); } #endif if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { From de14e7055e969d1e94302fdec544a2aa39d20e89 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:10:26 -0500 Subject: [PATCH 0413/2056] [cse7761][ads1115][tmp1075][matrix_keypad][seeed_mr60bha2] Fix assorted bugs (#14518) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/ads1115/ads1115.cpp | 15 ++------------- esphome/components/cse7761/cse7761.cpp | 10 +++++++--- .../components/matrix_keypad/matrix_keypad.cpp | 4 ++-- .../components/seeed_mr60bha2/seeed_mr60bha2.cpp | 12 ++++++++---- esphome/components/tmp1075/tmp1075.cpp | 4 ++-- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index f4996cd3b10..d493a6a6d3c 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 } if (resolution == ADS1015_12_BITS) { - bool negative = (raw_conversion >> 15) == 1; - - // shift raw_conversion as it's only 12-bits, left justified - raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); - - // check if number was negative in order to keep the sign - if (negative) { - // the number was negative - // 1) set the negative bit back - raw_conversion |= 0x8000; - // 2) reset the former (shifted) negative bit - raw_conversion &= 0xF7FF; - } + // ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend + raw_conversion = static_cast(static_cast(raw_conversion) >> (16 - ADS1015_12_BITS)); } auto signed_conversion = static_cast(raw_conversion); diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index f4966357d4b..7525b901f8b 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -147,13 +147,17 @@ uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { } uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { + uint32_t coeff = 0; switch (unit) { case RMS_UC: - return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; + coeff = this->data_.coefficient[RMS_UC]; + return coeff ? 0x400000 * 100 / coeff : 0; case RMS_IAC: - return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits + coeff = this->data_.coefficient[RMS_IAC]; + return coeff ? (0x800000 * 100 / coeff) * 10 : 0; // Stay within 32 bits case POWER_PAC: - return 0x80000000 / this->data_.coefficient[POWER_PAC]; + coeff = this->data_.coefficient[POWER_PAC]; + return coeff ? 0x80000000 / coeff : 0; } return 0; } diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 43a20c49d15..febbe794e47 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -61,7 +61,7 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); for (auto &listener : this->listeners_) listener->button_released(row, col); - if (!this->keys_.empty()) { + if (this->pressed_key_ < (int) this->keys_.size()) { uint8_t keycode = this->keys_[this->pressed_key_]; ESP_LOGD(TAG, "key '%c' released", keycode); for (auto &listener : this->listeners_) @@ -84,7 +84,7 @@ void MatrixKeypad::loop() { ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col); for (auto &listener : this->listeners_) listener->button_pressed(row, col); - if (!this->keys_.empty()) { + if (key < (int) this->keys_.size()) { uint8_t keycode = this->keys_[key]; ESP_LOGD(TAG, "key '%c' pressed", keycode); for (auto &trigger : this->key_triggers_) diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 12f188fe03c..8628faac5ae 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -177,10 +177,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c uint16_t has_target_int = encode_uint16(data[1], data[0]); this->has_target_binary_sensor_->publish_state(has_target_int); if (has_target_int == 0) { - this->breath_rate_sensor_->publish_state(0.0); - this->heart_rate_sensor_->publish_state(0.0); - this->distance_sensor_->publish_state(0.0); - this->num_targets_sensor_->publish_state(0); + if (this->breath_rate_sensor_ != nullptr) + this->breath_rate_sensor_->publish_state(0.0); + if (this->heart_rate_sensor_ != nullptr) + this->heart_rate_sensor_->publish_state(0.0); + if (this->distance_sensor_ != nullptr) + this->distance_sensor_->publish_state(0.0); + if (this->num_targets_sensor_ != nullptr) + this->num_targets_sensor_->publish_state(0); } } break; diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 9eb1e86c751..3c7ed019706 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -118,8 +118,8 @@ void TMP1075Sensor::send_alert_limit_high_() { } static uint16_t temp2regvalue(const float temp) { - const uint16_t regvalue = temp / 0.0625f; - return regvalue << 4; + const int16_t regvalue = static_cast(temp / 0.0625f); + return static_cast(regvalue << 4); } static float regvalue2temp(const uint16_t regvalue) { From 06d6322fe3ce5d4c36a24a7fafb1711c647c4439 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 15:19:45 -0600 Subject: [PATCH 0414/2056] [audio] Extract detect_audio_file_type helper (#14507) Co-authored-by: J. Nick Koston --- esphome/components/audio/audio.cpp | 56 +++++++++++++++++++++++ esphome/components/audio/audio.h | 7 +++ esphome/components/audio/audio_reader.cpp | 50 ++------------------ esphome/components/audio/audio_reader.h | 5 -- 4 files changed, 66 insertions(+), 52 deletions(-) diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 40592f6107a..3d675109e49 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -1,5 +1,9 @@ #include "audio.h" +#include "esphome/core/helpers.h" + +#include + namespace esphome { namespace audio { @@ -58,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) { } } +AudioFileType detect_audio_file_type(const char *content_type, const char *url) { + // Try Content-Type header first + if (content_type != nullptr && content_type[0] != '\0') { +#ifdef USE_AUDIO_MP3_SUPPORT + if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || + strcasecmp(content_type, "audio/mpeg") == 0) { + return AudioFileType::MP3; + } +#endif + if (strcasecmp(content_type, "audio/wav") == 0) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_FLAC_SUPPORT + if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } +#endif + } + + // Fallback to URL extension + if (url != nullptr && url[0] != '\0') { + if (str_endswith_ignore_case(url, ".wav")) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_MP3_SUPPORT + if (str_endswith_ignore_case(url, ".mp3")) { + return AudioFileType::MP3; + } +#endif +#ifdef USE_AUDIO_FLAC_SUPPORT + if (str_endswith_ignore_case(url, ".flac")) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + if (str_endswith_ignore_case(url, ".opus")) { + return AudioFileType::OPUS; + } +#endif + } + + return AudioFileType::NONE; +} + void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 7d7db9e9444..d3b41a362f1 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -130,6 +130,13 @@ struct AudioFile { /// @return const char pointer to the readable file type const char *audio_file_type_to_string(AudioFileType file_type); +/// @brief Detect audio file type from a Content-Type header value and/or URL extension. +/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null. +/// @param content_type Content-Type header value (may be null or empty) +/// @param url URL to inspect for file extension (may be null or empty) +/// @return The detected AudioFileType, or NONE if unknown +AudioFileType detect_audio_file_type(const char *content_type, const char *url); + /// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer. /// @param audio_samples PCM int16 audio samples /// @param output_buffer Buffer to store the scaled samples diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 78d69d7a39d..79ebf58889f 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -185,26 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { return err; } - if (str_endswith_ignore_case(url, ".wav")) { - file_type = AudioFileType::WAV; - } -#ifdef USE_AUDIO_MP3_SUPPORT - else if (str_endswith_ignore_case(url, ".mp3")) { - file_type = AudioFileType::MP3; - } -#endif -#ifdef USE_AUDIO_FLAC_SUPPORT - else if (str_endswith_ignore_case(url, ".flac")) { - file_type = AudioFileType::FLAC; - } -#endif -#ifdef USE_AUDIO_OPUS_SUPPORT - else if (str_endswith_ignore_case(url, ".opus")) { - file_type = AudioFileType::OPUS; - } -#endif - else { - file_type = AudioFileType::NONE; + file_type = detect_audio_file_type(nullptr, url); + if (file_type == AudioFileType::NONE) { this->cleanup_connection_(); return ESP_ERR_NOT_SUPPORTED; } @@ -232,32 +214,6 @@ AudioReaderState AudioReader::read() { return AudioReaderState::FAILED; } -AudioFileType AudioReader::get_audio_type(const char *content_type) { -#ifdef USE_AUDIO_MP3_SUPPORT - if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || - strcasecmp(content_type, "audio/mpeg") == 0) { - return AudioFileType::MP3; - } -#endif - if (strcasecmp(content_type, "audio/wav") == 0) { - return AudioFileType::WAV; - } -#ifdef USE_AUDIO_FLAC_SUPPORT - if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { - return AudioFileType::FLAC; - } -#endif -#ifdef USE_AUDIO_OPUS_SUPPORT - // Match "audio/ogg" with a codecs parameter containing "opus" - // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. - // Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams - if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { - return AudioFileType::OPUS; - } -#endif - return AudioFileType::NONE; -} - esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224 AudioReader *this_reader = (AudioReader *) evt->user_data; @@ -265,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: if (strcasecmp(evt->header_key, "Content-Type") == 0) { - this_reader->audio_file_type_ = get_audio_type(evt->header_value); + this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr); } break; default: diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 0b73923e840..753b310213e 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -58,11 +58,6 @@ class AudioReader { /// @brief Monitors the http client events to attempt determining the file type from the Content-Type header static esp_err_t http_event_handler(esp_http_client_event_t *evt); - /// @brief Determines the audio file type from the http header's Content-Type key - /// @param content_type string with the Content-Type key - /// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE. - static AudioFileType get_audio_type(const char *content_type); - AudioReaderState file_read_(); AudioReaderState http_read_(); From fbf63d8e3bec76734ae5eb2e487aca43e0747913 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 11:23:00 -1000 Subject: [PATCH 0415/2056] [rp2040] Update arduino-pico to 5.5.1 and fix WiFi AP fallback (#14500) --- .clang-tidy.hash | 2 +- esphome/components/rp2040/__init__.py | 6 +- esphome/components/wifi/__init__.py | 8 +- .../components/wifi/wifi_component_pico_w.cpp | 81 ++++++++++++------- platformio.ini | 2 +- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 767da3f33ec..adcebadeb46 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052 +b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index ea269a47c58..1442a0a7f74 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -91,7 +91,7 @@ def _parse_platform_version(value): # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 1) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags @@ -101,8 +101,8 @@ RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 5, 0), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(5, 5, 0), None), + "dev": (cv.Version(5, 5, 1), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(5, 5, 1), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2aa63b87cc4..0f86ec059ee 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -210,7 +210,13 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( def wifi_network_ap(value): if value is None: value = {} - return WIFI_NETWORK_AP(value) + config = WIFI_NETWORK_AP(value) + if CONF_MANUAL_IP in config and CORE.is_rp2040: + raise cv.Invalid( + "Manual AP IP configuration is not supported on RP2040. " + "The AP uses the default IP 192.168.4.1" + ) + return config WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 270425d8c21..5140fb285e3 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -18,6 +18,25 @@ namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; +// Check if STA is fully connected (WiFi joined + has IP address). +// Do NOT use WiFi.status() or WiFi.connected() for this — in AP-only mode they +// unconditionally return true regardless of STA state, causing false positives +// when the fallback AP is active. +static bool wifi_sta_connected() { + int link = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); + IPAddress local = WiFi.localIP(); + if (link == CYW43_LINK_JOIN && local.isSet()) { + // Verify the IP is a real STA IP, not the AP's IP leaking through + IPAddress ap_ip = WiFi.softAPIP(); + if (local == ap_ip) { + ESP_LOGV(TAG, "wifi_sta_connected: localIP %s matches AP IP, ignoring", local.toString().c_str()); + return false; + } + return true; + } + return false; +} + // Track previous state for detecting changes static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -27,17 +46,21 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE); + } else { + // Leave the STA network so the radio is free for scanning. + // Use cyw43_wifi_leave directly to avoid corrupting Arduino framework state. + cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); } } - bool ap_state = false; if (ap.has_value()) { if (ap.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE); - ap_state = true; + } else { + cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, false, CYW43_COUNTRY_WORLDWIDE); } + this->ap_started_ = ap.value(); } - this->ap_started_ = ap_state; return true; } @@ -129,8 +152,8 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { int status = cyw43_wifi_link_status(&cyw43_state, CYW43_ITF_STA); switch (status) { case CYW43_LINK_JOIN: - // WiFi joined, check if we have an IP address via the Arduino framework's WiFi class - if (WiFi.status() == WL_CONNECTED) { + // WiFi joined, check if STA has an IP address via wifi_sta_connected() + if (wifi_sta_connected()) { return WiFiSTAConnectStatus::CONNECTED; } return WiFiSTAConnectStatus::CONNECTING; @@ -188,19 +211,9 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { #ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { - esphome::network::IPAddress ip_address, gateway, subnet, dns; - if (manual_ip.has_value()) { - ip_address = manual_ip->static_ip; - gateway = manual_ip->gateway; - subnet = manual_ip->subnet; - dns = manual_ip->static_ip; - } else { - ip_address = network::IPAddress(192, 168, 4, 1); - gateway = network::IPAddress(192, 168, 4, 1); - subnet = network::IPAddress(255, 255, 255, 0); - dns = network::IPAddress(192, 168, 4, 1); - } - WiFi.config(ip_address, dns, gateway, subnet); + // AP IP is configured by WiFi.beginAP() internally using defaults (192.168.4.1). + // Manual AP IP has never worked on RP2040 — WiFi.config() configures the STA + // interface, not the AP. This is now rejected at config validation time. return true; } @@ -219,18 +232,25 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); + // Pass nullptr for empty password — CYW43 uses the password pointer (not length) + // to choose between OPEN and WPA2 auth mode. + const char *ap_password = ap.password_.empty() ? nullptr : ap.password_.c_str(); + WiFi.beginAP(ap.ssid_.c_str(), ap_password, ap.has_channel() ? ap.get_channel() : 1); return true; } -network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.softAPIP()}; } #endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { - // Use Arduino WiFi.disconnect() instead of raw cyw43_wifi_leave() to properly - // clean up the lwIP netif, DHCP client, and internal Arduino state. - WiFi.disconnect(); + // Use cyw43_wifi_leave() directly instead of WiFi.disconnect(). + // WiFi.disconnect() sets _wifiHWInitted=false in the Arduino framework. beginAP() + // uses _wifiHWInitted to determine AP+STA vs AP-only mode — with it false, + // beginAP() enters AP-only mode (IP 192.168.42.1) instead of AP_STA mode + // (IP 192.168.4.1). In AP-only mode, _beginInternal() redirects all subsequent + // STA connect attempts to beginAP(), creating an infinite loop. + cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); return true; } @@ -251,14 +271,21 @@ const char *WiFiComponent::wifi_ssid_to(std::span buffer buffer[len] = '\0'; return buffer.data(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { return this->is_connected_() ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { network::IPAddresses addresses; uint8_t index = 0; + // Filter out AP interface addresses — addrList includes all lwIP netifs. + // The AP netif IP lingers even after the AP radio is disabled. + IPAddress ap_ip = WiFi.softAPIP(); for (auto addr : addrList) { - addresses[index++] = addr.ipFromNetifNum(); + IPAddress ip(addr.ipFromNetifNum()); + if (ip == ap_ip) { + continue; + } + addresses[index++] = ip; } return addresses; } @@ -288,9 +315,7 @@ void WiFiComponent::wifi_loop_() { // Poll for connection state changes // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, // so we need to poll the link status to detect state changes. - // Use WiFi.connected() which checks both the WiFi link and IP address via the - // Arduino framework's own netif (not the SDK's uninitialized one). - bool is_connected = WiFi.connected(); + bool is_connected = wifi_sta_connected(); // Detect connection state change if (is_connected && !s_sta_was_connected) { diff --git a/platformio.ini b/platformio.ini index 16a1b18211c..87f992759c5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -196,7 +196,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.0/rp2040-5.5.0.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.1/rp2040-5.5.1.zip framework = arduino lib_deps = From e8b1dce67b2d31c1d8f9b2e44dc0d292d6e9d381 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:28:41 -0500 Subject: [PATCH 0416/2056] [st7735][st7789v][st7920] Fix display buffer overflows and dead code (#14511) Co-authored-by: Claude Opus 4.6 --- esphome/components/st7735/st7735.cpp | 14 +------------ esphome/components/st7735/st7735.h | 1 - esphome/components/st7789v/st7789v.cpp | 29 +++++++++++++++++--------- esphome/components/st7920/st7920.cpp | 15 +++++++------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 58459b79bb0..0fcfdd6c717 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -466,7 +466,7 @@ void HOT ST7735::write_display_data_() { } void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t byte[4]; + uint8_t byte[4]; byte[0] = (addr1 >> 8) & 0xFF; byte[1] = addr1 & 0xFF; byte[2] = (addr2 >> 8) & 0xFF; @@ -476,17 +476,5 @@ void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { this->write_array(byte, 4); } -void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) { - static uint8_t byte[1024]; - int index = 0; - for (int i = 0; i < size; i++) { - byte[index++] = (color >> 8) & 0xFF; - byte[index++] = color & 0xFF; - } - - this->dc_pin_->digital_write(true); - write_array(byte, size * 2); -} - } // namespace st7735 } // namespace esphome diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 37fe673962b..e81be520ed9 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -68,7 +68,6 @@ class ST7735 : public display::DisplayBuffer, void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); void draw_absolute_pixel_internal(int x, int y, Color color) override; void spi_master_write_addr_(uint16_t addr1, uint16_t addr2); - void spi_master_write_color_(uint16_t color, uint16_t size); int get_width_internal() override; int get_height_internal() override; diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index cd0b6cabc35..6e4360ae742 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -1,11 +1,16 @@ #include "st7789v.h" #include "esphome/core/log.h" +#include namespace esphome { namespace st7789v { static const char *const TAG = "st7789v"; -static const size_t TEMP_BUFFER_SIZE = 128; +#ifdef USE_ESP32 +static constexpr size_t TEMP_BUFFER_SIZE = 1024; +#else +static constexpr size_t TEMP_BUFFER_SIZE = 512; +#endif void ST7789V::setup() { #ifdef USE_POWER_SUPPLY @@ -236,7 +241,7 @@ void ST7789V::write_data_(uint8_t value) { } void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { - static uint8_t byte[4]; + uint8_t byte[4]; byte[0] = (addr1 >> 8) & 0xFF; byte[1] = addr1 & 0xFF; byte[2] = (addr2 >> 8) & 0xFF; @@ -247,15 +252,19 @@ void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { } void ST7789V::write_color_(uint16_t color, uint16_t size) { - static uint8_t byte[1024]; - int index = 0; - for (int i = 0; i < size; i++) { - byte[index++] = (color >> 8) & 0xFF; - byte[index++] = color & 0xFF; - } - + uint8_t byte[TEMP_BUFFER_SIZE]; + uint16_t remaining = size; this->dc_pin_->digital_write(true); - write_array(byte, size * 2); + while (remaining > 0) { + uint16_t batch = std::min(remaining, static_cast(sizeof(byte) / 2)); + int index = 0; + for (int i = 0; i < batch; i++) { + byte[index++] = (color >> 8) & 0xFF; + byte[index++] = color & 0xFF; + } + this->write_array(byte, batch * 2); + remaining -= batch; + } } size_t ST7789V::get_buffer_length_() { diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index afd7cd61bd3..a840f981523 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -72,16 +72,19 @@ void ST7920::goto_xy_(uint16_t x, uint16_t y) { } void HOT ST7920::write_display_data() { - uint8_t i, j, b; - for (j = 0; j < (uint8_t) (this->get_height_internal() / 2); j++) { + int i, j; + uint8_t b; + int width_bytes = this->get_width_internal() / 8; + int half_height = this->get_height_internal() / 2; + for (j = 0; j < half_height; j++) { this->goto_xy_(0, j); this->enable(); - for (i = 0; i < 16; i++) { // 16 bytes from line #0+ - b = this->buffer_[i + j * 16]; + for (i = 0; i < width_bytes; i++) { + b = this->buffer_[i + j * width_bytes]; this->send_(LCD_DATA, b); } - for (i = 0; i < 16; i++) { // 16 bytes from line #32+ - b = this->buffer_[i + (j + 32) * 16]; + for (i = 0; i < width_bytes; i++) { + b = this->buffer_[i + (j + half_height) * width_bytes]; this->send_(LCD_DATA, b); } this->disable(); From b2c12d88fe15d31710365a0b8759317527abc2d7 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 5 Mar 2026 23:24:11 +0100 Subject: [PATCH 0417/2056] [uart] init tx_pin, rx_pin, flow control, rx_buffer_size (#14524) --- esphome/components/uart/uart_component.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index b6ffbbd51f5..078ce64b30f 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -183,10 +183,10 @@ class UARTComponent { virtual void check_logger_conflict() = 0; bool check_read_timeout_(size_t len = 1); - InternalGPIOPin *tx_pin_; - InternalGPIOPin *rx_pin_; - InternalGPIOPin *flow_control_pin_; - size_t rx_buffer_size_; + InternalGPIOPin *tx_pin_{}; + InternalGPIOPin *rx_pin_{}; + InternalGPIOPin *flow_control_pin_{}; + size_t rx_buffer_size_{}; size_t rx_full_threshold_{1}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; From 8a8f6824a200a8396a66f1873a7731504e75cc0b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:29:44 -0500 Subject: [PATCH 0418/2056] [openthread][ethernet][wifi] Add IPv6 address array bounds assert (#14488) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/ethernet_component.cpp | 1 + esphome/components/openthread/openthread.h | 2 +- esphome/components/openthread/openthread_esp.cpp | 1 + esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 1 + esphome/components/wifi/wifi_component_pico_w.cpp | 3 +++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 098f7be972f..0bc67c8b033 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -470,6 +470,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index d853c58f958..c87f4fa7c10 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -90,7 +90,7 @@ class InstanceLock { otInstance *get_instance(); private: - // Use a private constructor in order to force thehandling + // Use a private constructor in order to force the handling // of acquisition failure InstanceLock() {} }; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 2296e32b7f7..cdc7a404b2d 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -197,6 +197,7 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { esp_netif_t *netif = esp_netif_get_default_netif(); count = esp_netif_get_all_ip6(netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 02ce59502b7..355832b4340 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #ifdef USE_WIFI_WPA2_EAP @@ -205,6 +206,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { network::IPAddresses addresses; uint8_t index = 0; for (auto &addr : addrList) { + assert(index < addresses.size()); addresses[index++] = addr.ipFromNetifNum(); } return addresses; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index bf432cea6e5..eca3f192490 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -585,6 +585,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 5140fb285e3..1cfeee3c1bf 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -3,6 +3,8 @@ #ifdef USE_WIFI #ifdef USE_RP2040 +#include + #include "lwip/dns.h" #include "lwip/err.h" #include "lwip/netif.h" @@ -285,6 +287,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (ip == ap_ip) { continue; } + assert(index < addresses.size()); addresses[index++] = ip; } return addresses; From 64098122e77cc893d5e2cc288a5b30e6a1744724 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 16:30:13 -0600 Subject: [PATCH 0419/2056] [audio_file] Add media source platform (#14436) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + .../audio_file/media_source/__init__.py | 38 +++ .../media_source/audio_file_media_source.cpp | 283 ++++++++++++++++++ .../media_source/audio_file_media_source.h | 50 ++++ tests/components/audio_file/common.yaml | 4 + 5 files changed, 376 insertions(+) create mode 100644 esphome/components/audio_file/media_source/__init__.py create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.cpp create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.h diff --git a/CODEOWNERS b/CODEOWNERS index 7c37b20e099..8bf896d159e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 esphome/components/audio_file/* @kahrendt +esphome/components/audio_file/media_source/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_file/media_source/__init__.py b/esphome/components/audio_file/media_source/__init__.py new file mode 100644 index 00000000000..e9e292a2b2e --- /dev/null +++ b/esphome/components/audio_file/media_source/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import media_source, psram +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.types import ConfigType + +CODEOWNERS = ["@kahrendt"] +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["audio_file"] + +audio_file_ns = cg.esphome_ns.namespace("audio_file") +AudioFileMediaSource = audio_file_ns.class_( + "AudioFileMediaSource", cg.Component, media_source.MediaSource +) + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + AudioFileMediaSource, + ) + .extend( + { + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + if CONF_TASK_STACK_IN_PSRAM in config: + cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp new file mode 100644 index 00000000000..120f871d2ff --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -0,0 +1,283 @@ +#include "audio_file_media_source.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio_decoder.h" + +#include + +namespace esphome::audio_file { + +namespace { // anonymous namespace for internal linkage +struct AudioSinkAdapter : public audio::AudioSinkCallback { + media_source::MediaSource *source; + audio::AudioStreamInfo stream_info; + + size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override { + return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info); + } +}; +} // namespace + +#if defined(USE_AUDIO_OPUS_SUPPORT) +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif + +static const char *const TAG = "audio_file_media_source"; + +enum EventGroupBits : uint32_t { + // Requests to start playback (set by play_uri, handled by loop) + REQUEST_START = (1 << 0), + // Commands from main loop to decode task + COMMAND_STOP = (1 << 1), + COMMAND_PAUSE = (1 << 2), + // Decode task lifecycle signals (one-shot, cleared by loop) + TASK_STARTING = (1 << 7), + TASK_RUNNING = (1 << 8), + TASK_STOPPING = (1 << 9), + TASK_STOPPED = (1 << 10), + TASK_ERROR = (1 << 11), + // Decode task state (level-triggered, set/cleared by decode task) + TASK_PAUSED = (1 << 12), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +void AudioFileMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, "Audio File Media Source:"); + ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No"); +} + +void AudioFileMediaSource::setup() { + this->disable_loop(); + + this->event_group_ = xEventGroupCreate(); + if (this->event_group_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + this->mark_failed(); + return; + } +} + +void AudioFileMediaSource::loop() { + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + + if (event_bits & REQUEST_START) { + xEventGroupClearBits(this->event_group_, REQUEST_START); + this->decoding_state_ = AudioFileDecodingState::START_TASK; + } + + switch (this->decoding_state_) { + case AudioFileDecodingState::START_TASK: { + if (!this->decode_task_.is_created()) { + xEventGroupClearBits(this->event_group_, ALL_BITS); + if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1, + this->task_stack_in_psram_)) { + ESP_LOGE(TAG, "Failed to create task"); + this->status_momentary_error("task_create", 1000); + this->set_state_(media_source::MediaSourceState::ERROR); + this->decoding_state_ = AudioFileDecodingState::IDLE; + return; + } + } + this->decoding_state_ = AudioFileDecodingState::DECODING; + break; + } + case AudioFileDecodingState::DECODING: { + if (event_bits & TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, TASK_STARTING); + } + + if (event_bits & TASK_RUNNING) { + ESP_LOGV(TAG, "Started"); + xEventGroupClearBits(this->event_group_, TASK_RUNNING); + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PAUSED); + } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if (event_bits & TASK_STOPPING) { + ESP_LOGV(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, TASK_STOPPING); + } + + if (event_bits & TASK_ERROR) { + // Report error so the orchestrator knows playback failed; task will have already logged the specific error + this->set_state_(media_source::MediaSourceState::ERROR); + } + + if (event_bits & TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ALL_BITS); + + this->decode_task_.deallocate(); + this->set_state_(media_source::MediaSourceState::IDLE); + this->decoding_state_ = AudioFileDecodingState::IDLE; + } + break; + } + case AudioFileDecodingState::IDLE: { + if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) { + this->set_state_(media_source::MediaSourceState::IDLE); + } + break; + } + } + + if ((this->decoding_state_ == AudioFileDecodingState::IDLE) && + (this->get_state() == media_source::MediaSourceState::IDLE)) { + this->disable_loop(); + } +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +bool AudioFileMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() || + xEventGroupGetBits(this->event_group_) & REQUEST_START) { + return false; + } + + // Check if source is already playing + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + // Validate URI starts with "audio-file://" + if (!uri.starts_with("audio-file://")) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + // Strip "audio-file://" prefix and find the file + const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters + + for (const auto &named_file : get_named_audio_files()) { + if (strcmp(named_file.file_id, file_id) == 0) { + this->current_file_ = named_file.file; + xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START); + this->enable_loop(); + return true; + } + } + + ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + return false; +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) { + if (this->decoding_state_ != AudioFileDecodingState::DECODING) { + return; + } + + switch (command) { + case media_source::MediaSourceCommand::STOP: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP); + break; + case media_source::MediaSourceCommand::PAUSE: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + case media_source::MediaSourceCommand::PLAY: + xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + default: + break; + } +} + +void AudioFileMediaSource::decode_task(void *params) { + AudioFileMediaSource *this_source = static_cast(params); + + do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING); + + // 0 bytes for input transfer buffer makes it an inplace buffer + std::unique_ptr decoder = make_unique(0, 4096); + + esp_err_t err = decoder->start(this_source->current_file_->file_type); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING); + break; + } + + // Add the file as a const data source + decoder->add_source(this_source->current_file_->data, this_source->current_file_->length); + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING); + + AudioSinkAdapter audio_sink; + bool has_stream_info = false; + + while (true) { + EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_); + + if (event_bits & EventGroupBits::COMMAND_STOP) { + break; + } + + bool paused = event_bits & EventGroupBits::COMMAND_PAUSE; + decoder->set_pause_output_state(paused); + if (paused) { + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + vTaskDelay(pdMS_TO_TICKS(20)); + } else { + xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + } + + // Will stop gracefully once finished with the current file + audio::AudioDecoderState decoder_state = decoder->decode(true); + + if (decoder_state == audio::AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == audio::AudioDecoderState::FAILED) { + ESP_LOGE(TAG, "Decoder failed"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { + has_stream_info = true; + + audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); + + ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(), + stream_info.get_channels(), stream_info.get_sample_rate()); + + if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { + ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + audio_sink.source = this_source; + audio_sink.stream_info = stream_info; + esp_err_t err = decoder->add_sink(&audio_sink); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + } + } + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING); + } while (false); + + // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed. + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED); + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it +} + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h new file mode 100644 index 00000000000..75e18c13b88 --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio_file/audio_file.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/core/component.h" +#include "esphome/core/static_task.h" + +#include +#include + +namespace esphome::audio_file { + +enum class AudioFileDecodingState : uint8_t { + START_TASK, + DECODING, + IDLE, +}; + +class AudioFileMediaSource : public Component, public media_source::MediaSource { + public: + void setup() override; + void loop() override; + void dump_config() override; + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); } + + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + protected: + static void decode_task(void *params); + + audio::AudioFile *current_file_{nullptr}; + AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE}; + EventGroupHandle_t event_group_{nullptr}; + StaticTask decode_task_; + + bool task_stack_in_psram_{false}; +}; + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/tests/components/audio_file/common.yaml b/tests/components/audio_file/common.yaml index 94042080946..e7f55b4806c 100644 --- a/tests/components/audio_file/common.yaml +++ b/tests/components/audio_file/common.yaml @@ -3,3 +3,7 @@ audio_file: file: type: local path: $component_dir/test.wav + +media_source: + - platform: audio_file + id: audio_file_source From 58ab63096587dcfd75d7ad74a397d8b0687fb41f Mon Sep 17 00:00:00 2001 From: Gnuspice Date: Fri, 6 Mar 2026 12:37:07 +1300 Subject: [PATCH 0420/2056] [ethernet] add get_eth_handle() function (#14527) --- esphome/components/ethernet/ethernet_component.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f5a31d78ebf..e54e1543e3f 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -115,6 +115,7 @@ class EthernetComponent : public Component { const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); + esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } bool powerdown(); #ifdef USE_ETHERNET_IP_STATE_LISTENERS From 44870323dab606efe85a21d93ac0f32eaab22aee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:47:47 -0500 Subject: [PATCH 0421/2056] [host] Add null checks for getenv and fopen in preferences (#14531) Co-authored-by: Claude Opus 4.6 --- esphome/components/host/preferences.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 5ad87c1f2a0..275c202e3ed 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -4,6 +4,7 @@ #include #include "preferences.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace host { @@ -14,7 +15,12 @@ static const char *const TAG = "host.preferences"; void HostPreferences::setup_() { if (this->setup_complete_) return; - this->filename_.append(getenv("HOME")); + const char *home = getenv("HOME"); + if (home == nullptr) { + ESP_LOGE(TAG, "HOME environment variable is not set"); + abort(); + } + this->filename_.append(home); this->filename_.append("/.esphome"); this->filename_.append("/prefs"); fs::create_directories(this->filename_); @@ -44,9 +50,12 @@ void HostPreferences::setup_() { bool HostPreferences::sync() { this->setup_(); FILE *fp = fopen(this->filename_.c_str(), "wb"); - std::map>::iterator it; + if (fp == nullptr) { + ESP_LOGE(TAG, "Failed to open preferences file for writing: %s", this->filename_.c_str()); + return false; + } - for (it = this->data.begin(); it != this->data.end(); ++it) { + for (auto it = this->data.begin(); it != this->data.end(); ++it) { fwrite(&it->first, sizeof(uint32_t), 1, fp); uint8_t len = it->second.size(); fwrite(&len, sizeof(len), 1, fp); From 80fe54ed6948386a9b6218737b164c9e61c7d71e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:30:39 -0500 Subject: [PATCH 0422/2056] [bluetooth_proxy] Add null checks for api_connection (#14536) Co-authored-by: Claude Opus 4.6 --- .../bluetooth_proxy/bluetooth_connection.cpp | 25 +++++++++++++++---- .../bluetooth_proxy/bluetooth_proxy.cpp | 4 +++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 60f56fda547..b2000fbd943 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -415,11 +415,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -429,10 +432,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -442,10 +448,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -455,20 +464,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_NOTIFY_EVT: { ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index d45377b3f67..cab328e2f53 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -420,6 +420,8 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDevicePairingResponse call; call.address = address; call.paired = paired; @@ -429,6 +431,8 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDeviceUnpairingResponse call; call.address = address; call.success = success; From a2c0d70c2c1983cf996f045ba7801216ddcea9ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 08:00:17 +0100 Subject: [PATCH 0423/2056] [ble_nus] Add uart support (#14320) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/ble_nus/__init__.py | 53 +++++++- esphome/components/ble_nus/ble_nus.cpp | 125 +++++++++++++++--- esphome/components/ble_nus/ble_nus.h | 16 ++- esphome/core/defines.h | 2 + .../ble_nus/test-uart.nrf52-adafruit.yaml | 4 + 5 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 tests/components/ble_nus/test-uart.nrf52-adafruit.yaml diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py index 6581ce1cfab..c0837da4025 100644 --- a/esphome/components/ble_nus/__init__.py +++ b/esphome/components/ble_nus/__init__.py @@ -1,29 +1,64 @@ import esphome.codegen as cg from esphome.components.logger import request_log_listener +from esphome.components.uart import ( + UARTComponent, + debug_to_code, + maybe_empty_debug, + uart_ns, +) from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE +from esphome.const import ( + CONF_DEBUG, + CONF_ID, + CONF_LOGS, + CONF_RX_BUFFER_SIZE, + CONF_TX_BUFFER_SIZE, + CONF_TYPE, +) +from esphome.types import ConfigType -AUTO_LOAD = ["zephyr_ble_server"] +AUTO_LOAD = ["zephyr_ble_server", "uart"] CODEOWNERS = ["@tomaszduda23"] ble_nus_ns = cg.esphome_ns.namespace("ble_nus") -BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) +BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent) + +CONF_UART = "uart" + + +def validate_rx_buffer(config: ConfigType) -> ConfigType: + config = config.copy() + if config[CONF_TYPE] == CONF_LOGS: + if CONF_RX_BUFFER_SIZE in config: + raise cv.Invalid("logs does not support rx_buffer_size") + elif CONF_RX_BUFFER_SIZE not in config: + config[CONF_RX_BUFFER_SIZE] = 512 + return config + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLENUS), cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( - *[CONF_LOGS], lower=True + *[CONF_LOGS, CONF_UART], lower=True ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_RX_BUFFER_SIZE): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_framework("zephyr"), + validate_rx_buffer, ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) zephyr_add_prj_conf("BT_NUS", True) expose_log = config[CONF_TYPE] == CONF_LOGS @@ -31,3 +66,11 @@ async def to_code(config): if expose_log: request_log_listener() # Request a log listener slot for BLE NUS log streaming await cg.register_component(var, config) + cg.add_define("ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE", config[CONF_TX_BUFFER_SIZE]) + if CONF_RX_BUFFER_SIZE in config: + cg.add_define( + "ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE", config[CONF_RX_BUFFER_SIZE] + ) + if CONF_DEBUG in config: + cg.add_global(uart_ns.using) + await debug_to_code(config[CONF_DEBUG], var) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index a10132eb3ef..d1710100a09 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -11,25 +11,111 @@ namespace esphome::ble_nus { -constexpr size_t BLE_TX_BUF_SIZE = 2048; - // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) BLENUS *global_ble_nus; -RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); +RING_BUF_DECLARE(global_ble_tx_ring_buf, ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE +RING_BUF_DECLARE(global_ble_rx_ring_buf, ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE); +#endif // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) static const char *const TAG = "ble_nus"; -size_t BLENUS::write_array(const uint8_t *data, size_t len) { +void BLENUS::write_array(const uint8_t *data, size_t len) { if (atomic_get(&this->tx_status_) == TX_DISABLED) { - return 0; + return; + } + auto sent = ring_buf_put(&global_ble_tx_ring_buf, data, len); + if (sent < len) { + ESP_LOGE(TAG, "TX dropping %u bytes", len - sent); + return; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool BLENUS::peek_byte(uint8_t *data) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +#else + return false; +#endif +} + +bool BLENUS::read_array(uint8_t *data, size_t len) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (len == 0) { + return true; + } + if (this->available() < len) { + return false; + } + + // First, use the peek buffer if available + if (this->has_peek_) { + data[0] = this->peek_buffer_; + this->has_peek_ = false; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + if (ring_buf_get(&global_ble_rx_ring_buf, data, len) != len) { + ESP_LOGE(TAG, "UART BLE unexpected size"); + return false; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + return true; +#else + return false; +#endif +} + +size_t BLENUS::available() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + uint32_t size = ring_buf_size_get(&global_ble_rx_ring_buf); + ESP_LOGVV(TAG, "UART BLE available %u", size); + return size + (this->has_peek_ ? 1 : 0); +#else + return 0; +#endif +} + +void BLENUS::flush() { + constexpr uint32_t timeout_5sec = 5000; + uint32_t start = millis(); + while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { + if (millis() - start > timeout_5sec) { + ESP_LOGW(TAG, "Flush timeout"); + return; + } + delay(1); } - return ring_buf_put(&global_ble_tx_ring_buf, data, len); } void BLENUS::connected(bt_conn *conn, uint8_t err) { if (err == 0) { global_ble_nus->conn_.store(bt_conn_ref(conn)); + global_ble_nus->connected_ = true; } } @@ -38,6 +124,7 @@ void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { bt_conn_unref(global_ble_nus->conn_.load()); // Connection array is global static. // Reference can be kept even if disconnected. + global_ble_nus->connected_ = false; } } @@ -63,12 +150,19 @@ void BLENUS::send_enabled_callback(bt_nus_send_status status) { break; } } - void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { - ESP_LOGD(TAG, "Received %d bytes.", len); + ESP_LOGV(TAG, "Received %d bytes.", len); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + auto recv_len = ring_buf_put(&global_ble_rx_ring_buf, data, len); + if (recv_len < len) { + ESP_LOGE(TAG, "RX dropping %u bytes", len - recv_len); + } +#endif } - void BLENUS::setup() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + this->rx_buffer_size_ = ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE; +#endif bt_nus_cb callbacks = { .received = rx_callback, .sent = tx_callback, @@ -106,16 +200,17 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t #endif void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, - "ble nus:\n" - " log: %s", - YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); - if (conn) { + if (conn && this->connected_) { mtu = bt_nus_get_mtu(conn); } - ESP_LOGCONFIG(TAG, " MTU: %u", mtu); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s\n" + " connected: %s\n" + " MTU: %u", + YESNO(this->expose_log_), YESNO(this->connected_.load()), mtu); } void BLENUS::loop() { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b2b0ee7713a..67e9ae9f97a 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -10,7 +11,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public uart::UARTComponent, public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -21,7 +22,12 @@ class BLENUS : public Component { void setup() override; void dump_config() override; void loop() override; - size_t write_array(const uint8_t *data, size_t len); + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + void flush() override; + void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); @@ -37,6 +43,12 @@ class BLENUS : public Component { std::atomic conn_ = nullptr; bool expose_log_ = false; atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); + std::atomic connected_{}; +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + // RX buffer for peek functionality + uint8_t peek_buffer_{0}; + bool has_peek_{false}; +#endif }; } // namespace esphome::ble_nus diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1a6d9b3a803..be5fdc9006e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -356,6 +356,8 @@ #endif #ifdef USE_NRF52 +#define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 +#define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC diff --git a/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml new file mode 100644 index 00000000000..0d917ec1151 --- /dev/null +++ b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +ble_nus: + type: uart + tx_buffer_size: 160 + rx_buffer_size: 160 From 666fb7cf39aeaf2e2a7890695099535ed8b5a67c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:28 -0500 Subject: [PATCH 0424/2056] [sx127x][sx126x][max6956] Fix null deref, unterminated string, and pin bounds check (#14529) Co-authored-by: Claude Opus 4.6 --- esphome/components/max6956/max6956.cpp | 2 ++ esphome/components/sx126x/sx126x.cpp | 3 ++- esphome/components/sx127x/sx127x.cpp | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index a350e66ee0e..ce45541b635 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -111,6 +111,8 @@ void MAX6956::write_brightness_mode() { } void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + if (pin < MAX6956_MIN || pin > MAX6956_MAX) + return; uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; uint8_t config = 0; uint8_t shift = 4 * (pin % 2); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 64cd24b1713..ec62fad10af 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -155,7 +155,8 @@ void SX126x::configure() { } // check silicon version to make sure hw is ok - this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16); + this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_)); + this->version_[sizeof(this->version_) - 1] = '\0'; if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) { this->mark_failed(); return; diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index f6aa11b6347..66957a73424 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -260,6 +260,11 @@ SX127xError SX127x::transmit_packet(const std::vector &packet) { return SX127xError::INVALID_PARAMS; } + if (this->dio0_pin_ == nullptr) { + ESP_LOGE(TAG, "DIO0 pin not configured, cannot wait for transmit completion"); + return SX127xError::INVALID_PARAMS; + } + SX127xError ret = SX127xError::NONE; if (this->modulation_ == MOD_LORA) { this->set_mode_standby(); From 6c07c15c504c7f0a66ab1a9e2c2f91943526ffa2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:56 -0500 Subject: [PATCH 0425/2056] [mipi_dsi][e131] Fix semaphore cast, missing return, and light count overread (#14530) Co-authored-by: Claude Opus 4.6 --- esphome/components/e131/e131_addressable_light_effect.cpp | 4 +++- esphome/components/mipi_dsi/mipi_dsi.cpp | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7d62f739a24..f6010a7cc9f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -54,8 +54,10 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received + // packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights + int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0; int output_end = - std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); + std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet)); auto *input_data = packet.values + 1; auto effect_name = get_name(); diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 4d45cfb7990..815b9d75a1d 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -10,7 +10,7 @@ namespace mipi_dsi { static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { - auto *sem = static_cast(user_ctx); + auto sem = static_cast(user_ctx); BaseType_t need_yield = pdFALSE; xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); @@ -190,6 +190,7 @@ void MIPI_DSI::draw_pixels_at(int x_start, int y_start, int w, int h, const uint if (bitness != this->color_depth_) { display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; } this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); } From c0b7f41397e41ff53ed2d551fcd0e3cf03f89147 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:21:44 +0100 Subject: [PATCH 0426/2056] [esp32] Fix wrong variable usage in P4 pin validation error msg (#14539) --- esphome/components/esp32/gpio_esp32_p4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index b98b567da2c..2726c5932fa 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -33,7 +33,7 @@ def esp32_p4_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 54: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)") if is_input: # All ESP32 pins support input mode pass From 5084c32f3c3da1bb857b79f6200825ec456000c8 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:22:11 +0100 Subject: [PATCH 0427/2056] [esp32] Fix ESP32-S3 pin validation error message (#14540) --- esphome/components/esp32/gpio_esp32_s3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index 71205046933..aea378f499d 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) def esp32_s3_validate_gpio_pin(value): if value < 0 or value > 48: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") if value in _ESP_32S3_SPI_PSRAM_PINS: raise cv.Invalid( @@ -55,7 +55,7 @@ def esp32_s3_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 48: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)") if is_input: # All ESP32 pins support input mode pass From e59a2b3eded37c9de46d8148b2f8cb40b46d06ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 17:25:44 +0100 Subject: [PATCH 0428/2056] [nrf52] prepare for usb cdc (#14174) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 2 ++ esphome/core/event_pool.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a14d3af69ef..8ad84656ede 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -464,6 +464,8 @@ def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This featur unsupported = [unsupported] def validator_(obj): + if not CORE.is_esp32: + raise cv.Invalid(f"{msg_prefix} is only available on ESP32") variant = get_esp32_variant() if supported is not None and variant not in supported: raise cv.Invalid( diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 928a4e7dee2..99541d4a179 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_ZEPHYR) #include #include From 07e51886f3563061c09e6a8c9c36ab94300b0b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:57:52 -1000 Subject: [PATCH 0429/2056] [core] Move entity icon strings to PROGMEM on ESP8266 (#14437) --- esphome/components/api/api_connection.h | 3 +- esphome/components/mqtt/mqtt_component.cpp | 10 ++--- esphome/components/mqtt/mqtt_component.h | 4 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/config_validation.py | 17 ++++--- esphome/core/config.py | 4 ++ esphome/core/entity_base.cpp | 38 ++++++++++++++-- esphome/core/entity_base.h | 33 +++++++++++--- esphome/core/entity_helpers.py | 47 +++++++++++++++++--- tests/unit_tests/core/test_entity_helpers.py | 17 +++++++ tests/unit_tests/test_config_validation.py | 12 +++++ 11 files changed, 158 insertions(+), 30 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index aae8db3c688..88f0ef82d66 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -354,7 +354,8 @@ class APIConnection final : public APIServerConnectionBase { // Set common EntityBase properties #ifdef USE_ENTITY_ICON - msg.icon = entity->get_icon_ref(); + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); #endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f49069960b3..98fa10def95 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -209,12 +209,11 @@ bool MQTTComponent::send_discovery_() { if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto icon_ref = this->get_icon_ref_(); - if (!icon_ref.empty()) { - root[MQTT_ICON] = icon_ref; + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = this->get_icon_to_(icon_buf); + if (icon[0] != '\0') { + root[MQTT_ICON] = icon; } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { @@ -413,7 +412,6 @@ const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity StringRef MQTTComponent::get_default_object_id_to_(std::span buf) const { return this->get_entity()->get_object_id_to(buf); } -StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::compute_is_internal_() { if (this->custom_state_topic_.has_value()) { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 0ffe6341d37..2403ef64ea3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -298,8 +298,8 @@ class MQTTComponent : public Component { /// Get the friendly name of this MQTT component. const StringRef &friendly_name_() const; - /// Get the icon field of this component as StringRef - StringRef get_icon_ref_() const; + /// Get the icon field of this component into a stack buffer + const char *get_icon_to_(std::span buf) const { return this->get_entity()->get_icon_to(buf); } /// Get whether the underlying Entity is disabled by default bool is_disabled_by_default_() const; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6b94a103cc9..bc90c88e57f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -568,7 +568,8 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); + char icon_buf[MAX_ICON_LENGTH]; + root[ESPHOME_F("icon")] = obj->get_icon_to(icon_buf); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3b0e4da298a..1eac53e9b20 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -400,14 +400,21 @@ def string_strict(value): def icon(value): """Validate that a given config value is a valid icon.""" + from esphome.core.config import ICON_MAX_LENGTH + value = string_strict(value) if not value: return value - if re.match("^[\\w\\-]+:[\\w\\-]+$", value): - return value - raise Invalid( - 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' - ) + if not re.match("^[\\w\\-]+:[\\w\\-]+$", value): + raise Invalid( + 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' + ) + if len(value) > ICON_MAX_LENGTH: + raise Invalid( + f"Icon string is too long ({len(value)} chars, max {ICON_MAX_LENGTH}). " + "Icons are stored in PROGMEM with a 64-byte buffer limit." + ) + return value def sub_device_id(value: str | None) -> core.ID | None: diff --git a/esphome/core/config.py b/esphome/core/config.py index 4f526404fe8..9093ab3fe9f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -188,6 +188,10 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max icon string length (63 chars + null = 64-byte PROGMEM buffer) +# Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h +ICON_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index eafc04f92a4..12652775722 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,6 +1,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" namespace esphome { @@ -72,7 +73,27 @@ std::string EntityBase::get_unit_of_measurement() const { return std::string(this->get_unit_of_measurement_ref().c_str()); } -// Entity icon (from index) +// Entity icon — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_icon_to([[maybe_unused]] std::span buffer) const { +#ifdef USE_ENTITY_ICON + const uint8_t idx = this->icon_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *icon = entity_icon_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), icon, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_icon_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated icon accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_icon_ref() const { #ifdef USE_ENTITY_ICON return StringRef(entity_icon_lookup(this->icon_idx_)); @@ -80,7 +101,14 @@ StringRef EntityBase::get_icon_ref() const { return StringRef(entity_icon_lookup(0)); #endif } -std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); } +std::string EntityBase::get_icon() const { +#ifdef USE_ENTITY_ICON + return std::string(entity_icon_lookup(this->icon_idx_)); +#else + return std::string(entity_icon_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { @@ -154,8 +182,10 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve #ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = obj.get_icon_to(icon_buf); + if (icon[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, icon); } } #endif diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 54d4ae311f2..1ce1e658e02 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,10 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum icon string buffer size (63 chars + null terminator) +// Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_ICON_LENGTH = 64; + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -124,12 +128,31 @@ class EntityBase { "2026.3.0") std::string get_unit_of_measurement() const; - // Get/set this entity's icon - ESPDEPRECATED( - "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", - "2025.11.0") - std::string get_icon() const; + // Get this entity's icon into a stack buffer. + // On ESP32: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_icon_to(std::span buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed + // directly as const char*. Use get_icon_to() with a stack buffer instead. + template StringRef get_icon_ref() const { + static_assert(sizeof(T) == 0, + "get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return StringRef(""); + } + template std::string get_icon() const { + static_assert(sizeof(T) == 0, + "get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_icon_to() instead. Icons are in PROGMEM. + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_icon_ref() const; + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") + std::string get_icon() const; +#endif #ifdef USE_DEVICES // Get/set this entity's device id diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 551e35df65c..01fa27b833a 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core.config import ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -78,6 +79,8 @@ def _generate_category_code( table_var: str, lookup_fn: str, strings: dict[str, int], + *, + progmem_strings: bool = False, ) -> str: """Generate C++ code for one string category (PROGMEM pointer table + lookup). @@ -85,14 +88,40 @@ def _generate_category_code( in flash (via PROGMEM) and read with progmem_read_ptr(). String literals themselves remain in RAM but benefit from linker string deduplication. Index 0 means "not set" and returns empty string. + + When progmem_strings=True, each string is declared as a separate PROGMEM + char array. This ensures the string data itself is in flash on ESP8266 + (where .rodata is RAM). On other platforms PROGMEM is a no-op. """ if not strings: return "" sorted_strings = sorted(strings.items(), key=lambda x: x[1]) - entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) count = len(sorted_strings) + if progmem_strings: + # Emit individual PROGMEM char arrays so string data lives in flash + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + entries = ", ".join(var_names) + # Empty string must also be PROGMEM — on ESP8266, callers use strncpy_P + empty_var = f"{table_var}_EMPTY" + lines.append(f'static const char {empty_var}[] PROGMEM = "";') + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const char *{lookup_fn}(uint8_t index) {{") + lines.append(f" if (index == 0 || index > {count}) return {empty_var};") + lines.append(f" return progmem_read_ptr(&{table_var}[index - 1]);") + lines.append("}") + return "\n".join(lines) + "\n" + + entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) + return ( f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" f"const char *{lookup_fn}(uint8_t index) {{\n" @@ -103,9 +132,9 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), - ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), - ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -117,8 +146,10 @@ async def _generate_tables_job() -> None: """ pool = _get_pool() parts = ["namespace esphome {"] - for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: - code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) + for table_var, lookup_fn, attr, progmem_strs in _CATEGORY_CONFIGS: + code = _generate_category_code( + table_var, lookup_fn, getattr(pool, attr), progmem_strings=progmem_strs + ) if code: parts.append(code) parts.append("} // namespace esphome") @@ -160,6 +191,10 @@ def register_unit_of_measurement(value: str) -> int: def register_icon(value: str) -> int: """Register an icon string and return its 1-based index.""" + if value and len(value) > ICON_MAX_LENGTH: + raise ValueError( + f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a5cfad5ab69..79bc3095b92 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_icon, setup_entity, ) from esphome.cpp_generator import MockObj @@ -909,6 +910,22 @@ def test_register_string_overflow() -> None: _register_string("overflow", category, 3, "test") +def test_register_icon_max_length() -> None: + """Test register_icon rejects icons exceeding 63 characters.""" + # 63 chars should succeed + max_icon = "mdi:" + "a" * 59 # 63 total + idx = register_icon(max_icon) + assert idx > 0 + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 total + with pytest.raises(ValueError, match="Icon string too long"): + register_icon(too_long) + + # Empty string returns 0 + assert register_icon("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9602010ad30..c1849daf4ba 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -148,6 +148,18 @@ def test_icon__invalid(): config_validation.icon("foo") +def test_icon__max_length(): + """Test that icons exceeding 63 characters are rejected.""" + # Exactly 63 chars should pass + max_icon = "mdi:" + "a" * 59 # 63 chars total + assert config_validation.icon(max_icon) == max_icon + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 chars total + with pytest.raises(Invalid, match="Icon string is too long"): + config_validation.icon(too_long) + + @pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True)) def test_boolean__valid_true(value): assert config_validation.boolean(value) is True From 74e4b69654c2dd77f81cd712fe1c7ece3db78bfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:58:13 -1000 Subject: [PATCH 0430/2056] [core] Replace Application name/friendly_name std::string with StringRef (#14532) Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 6 +- esphome/components/openthread/openthread.cpp | 2 +- .../components/web_server/web_server_v1.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 2 +- esphome/core/application.h | 54 +++++++------ esphome/core/config.py | 60 +++++++++++++-- esphome/core/defines.h | 1 + esphome/core/entity_base.cpp | 6 +- tests/dummy_main.cpp | 4 +- tests/unit_tests/core/test_config.py | 77 +++++++++++++++++++ 14 files changed, 181 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3ae35e9be81..ba4f2f0642d 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -269,7 +269,7 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello - const std::string &name = App.get_name(); + const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 9d260188003..bbe972b9f33 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -273,7 +273,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5e5e1279d95..342a6e6c645 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -59,7 +59,7 @@ void MDNSComponent::compile_records_(StaticVectorget_port(); - const std::string &friendly_name = App.get_friendly_name(); + const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 98fa10def95..d31a78b0900 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -267,7 +267,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_UNIQUE_ID] = unique_id_buf; } - const std::string &node_name = App.get_name(); + const auto &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { // node_name (max 31) + "_" (1) + object_id (max 128) + null char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; @@ -275,8 +275,8 @@ bool MQTTComponent::send_discovery_() { root[MQTT_OBJECT_ID] = object_id_full; } - const std::string &friendly_name_ref = App.get_friendly_name(); - const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; + const auto &friendly_name_ref = App.get_friendly_name(); + const auto &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 9452f5a41eb..fb814812997 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() { // set the host name uint16_t size; char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size); - const std::string &host_name = App.get_name(); + const auto &host_name = App.get_name(); uint16_t host_name_len = host_name.size(); if (host_name_len > size) { ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name"); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index f7b90018dc6..85a4e80541b 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -75,7 +75,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); - const std::string &title = App.get_name(); + const auto &title = App.get_name(); stream->print(ESPHOME_F("")); stream->print(title.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b60810d28a..60764955cc9 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -913,7 +913,7 @@ void WiFiComponent::setup_ap_config_() { static constexpr size_t AP_SSID_PREFIX_LEN = 25; static constexpr size_t AP_SSID_SUFFIX_LEN = 7; - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); const char *name_ptr = app_name.c_str(); size_t name_len = app_name.length(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 355832b4340..a9b26c5935e 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -212,7 +212,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return addresses; } bool WiFiComponent::wifi_apply_hostname_() { - const std::string &hostname = App.get_name(); + const auto &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Set hostname failed"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 40f8a00edd3..87f9fdf59a1 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -138,26 +138,36 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { +#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. + void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); - this->name_add_mac_suffix_ = name_add_mac_suffix; - if (name_add_mac_suffix) { - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; - // MAC address suffix length (last 6 characters of 12-char MAC address string) - constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; - get_mac_address_into_buffer(mac_addr); - const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); - if (!friendly_name.empty()) { - this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); - } - } else { - this->name_ = name; - this->friendly_name_ = friendly_name; + this->name_add_mac_suffix_ = true; + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + // Overwrite the placeholder suffix in the mutable static buffers with actual MAC + // name is always non-empty (validated by validate_hostname in Python config) + memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); + if (friendly_name_len > 0) { + memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, + mac_address_suffix_len); } + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } +#else + /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. + void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { + arch_init(); + this->name_add_mac_suffix_ = false; + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); + } +#endif #ifdef USE_DEVICES void register_device(Device *device) { this->devices_.push_back(device); } @@ -274,10 +284,10 @@ class Application { void loop(); /// Get the name of this Application set by pre_setup(). - const std::string &get_name() const { return this->name_; } + const StringRef &get_name() const { return this->name_; } /// Get the friendly name of this Application set by pre_setup(). - const std::string &get_friendly_name() const { return this->friendly_name_; } + const StringRef &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). const char *get_area() const { @@ -627,9 +637,9 @@ class Application { #endif #endif - // std::string members (typically 24-32 bytes each) - std::string name_; - std::string friendly_name_; + // StringRef members (8 bytes each: pointer + size) + StringRef name_; + StringRef friendly_name_; // 4-byte members uint32_t last_loop_{0}; diff --git a/esphome/core/config.py b/esphome/core/config.py index 9093ab3fe9f..8631726a021 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -50,6 +50,7 @@ from esphome.core import ( ) from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, fnv1a_32bit_hash, get_str_env, walk_files, @@ -58,6 +59,38 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) +# C++ variable names and separators for app name buffers (used with MAC suffix) +_APP_NAME_BUF_VAR = "esphome_app_name_buf" +_APP_NAME_MAC_SEP = "-" +_APP_FRIENDLY_NAME_BUF_VAR = "esphome_app_friendly_name_buf" +_APP_FRIENDLY_NAME_MAC_SEP = " " +# Placeholder suffix for MAC address (last 6 hex chars) +_MAC_SUFFIX_PLACEHOLDER = "XXXXXX" + + +def make_app_name_cpp( + value: str, var_name: str, sep: str, *, add_mac_suffix: bool +) -> tuple[str, str | None, int]: + """Compute C++ expression and optional global declaration for an app name. + + Returns (cpp_expr, global_decl_or_none, byte_length). + - cpp_expr: The C++ expression to pass to pre_setup (var name or string literal). + - global_decl: A static char[] declaration string, or None if not needed. + - byte_length: The UTF-8 byte length of the string value. + """ + if add_mac_suffix: + buf_value = "" if not value else f"{value}{sep}{_MAC_SUFFIX_PLACEHOLDER}" + escaped = cpp_string_escape(buf_value) + return ( + var_name, + f"static char {var_name}[] = {escaped};", + len(buf_value.encode("utf-8")), + ) + if not value: + return '""', None, 0 + return cpp_string_escape(value), None, len(value.encode("utf-8")) + + StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() ) @@ -78,6 +111,8 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} def validate_hostname(config): # Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h + if not config[CONF_NAME]: + raise cv.Invalid("Hostname must not be empty", path=[CONF_NAME]) max_length = 31 if config[CONF_NAME_ADD_MAC_SUFFIX]: max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used @@ -555,13 +590,28 @@ async def to_code(config: ConfigType) -> None: # Construct App via placement new — see application.cpp for storage details cg.add_global(cg.RawStatement("#include <new>")) cg.add(cg.RawExpression("new (&App) Application()")) - cg.add( - cg.App.pre_setup( - config[CONF_NAME], - config[CONF_FRIENDLY_NAME], - config[CONF_NAME_ADD_MAC_SUFFIX], + name = config[CONF_NAME] + friendly_name = config[CONF_FRIENDLY_NAME] + name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] + + def _emit_app_name( + value: str, var_name: str, sep: str + ) -> tuple[cg.Expression, int]: + """Emit codegen for an app name and return (expression, byte_length).""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + value, var_name, sep, add_mac_suffix=name_add_mac_suffix ) + if global_decl is not None: + cg.add_global(cg.RawStatement(global_decl)) + return cg.RawExpression(cpp_expr), byte_len + + name_expr, name_len = _emit_app_name(name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP) + friendly_expr, friendly_len = _emit_app_name( + friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP ) + if name_add_mac_suffix: + cg.add_define("ESPHOME_NAME_ADD_MAC_SUFFIX") + cg.add(cg.App.pre_setup(name_expr, name_len, friendly_expr, friendly_len)) # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index be5fdc9006e..c5f38ab9aab 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -13,6 +13,7 @@ #define ESPHOME_PROJECT_VERSION "v2" #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" +#define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 12652775722..37e7fcc9987 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -23,13 +23,13 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { // Bug-for-bug compatibility with OLD behavior: // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name - const std::string &friendly = App.get_friendly_name(); + const auto &friendly = App.get_friendly_name(); if (App.is_name_add_mac_suffix_enabled()) { // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility - this->name_ = StringRef(friendly); + this->name_ = friendly; } else { // No MAC suffix - fallback to device name if friendly_name is empty - this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + this->name_ = !friendly.empty() ? friendly : App.get_name(); } } this->flags_.has_own_name = false; diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ccf35e04d2..6fa0c08aa3d 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,9 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", false); + static char name[] = "livingroom"; + static char friendly_name[] = "LivingRoom"; + App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 88801a9ca03..474d31a90af 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -23,6 +23,7 @@ from esphome.const import ( from esphome.core import CORE, config from esphome.core.config import ( Area, + make_app_name_cpp, preload_core_config, valid_include, valid_project_name, @@ -969,3 +970,79 @@ def test_config_hash_different_for_different_configs() -> None: hash2 = CORE.config_hash assert hash1 != hash2 + + +def test_make_app_name_cpp_no_mac_simple() -> None: + """Test simple name without MAC suffix returns string literal.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '"my-device"' + assert global_decl is None + assert byte_len == 9 + + +def test_make_app_name_cpp_no_mac_empty() -> None: + """Test empty name without MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '""' + assert global_decl is None + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix() -> None: + """Test name with MAC suffix emits static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert "my-device-XXXXXX" in global_decl + assert byte_len == len("my-device-XXXXXX") + + +def test_make_app_name_cpp_mac_suffix_empty() -> None: + """Test empty name with MAC suffix emits empty static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix_space_sep() -> None: + """Test friendly name uses space separator for MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "My Device", "esphome_app_friendly_name_buf", " ", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_friendly_name_buf" + assert global_decl is not None + assert "My Device XXXXXX" in global_decl + assert byte_len == len("My Device XXXXXX") + + +def test_make_app_name_cpp_non_ascii_utf8_length() -> None: + """Test non-ASCII characters use UTF-8 byte length.""" + _, global_decl, byte_len = make_app_name_cpp( + "café", "buf", "-", add_mac_suffix=False + ) + assert byte_len == len("café".encode()) # 5 bytes, not 4 chars + assert global_decl is None + + +def test_make_app_name_cpp_non_ascii_mac_suffix_utf8_length() -> None: + """Test non-ASCII with MAC suffix uses UTF-8 byte length.""" + _, _, byte_len = make_app_name_cpp("café", "buf", "-", add_mac_suffix=True) + assert byte_len == len("café-XXXXXX".encode()) + + +def test_make_app_name_cpp_special_chars_escaped() -> None: + """Test special characters are properly escaped in C++ string.""" + cpp_expr, _, _ = make_app_name_cpp('my "device"', "buf", "-", add_mac_suffix=False) + # cpp_string_escape uses octal escapes for quotes + assert '"' not in cpp_expr[1:-1] # no unescaped quotes inside the outer quotes From a16b8fc0ac30a015df61555d59fb98e19a9efe6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:00:31 -1000 Subject: [PATCH 0431/2056] [rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528) --- esphome/components/rp2040/boards.jinja2 | 25 + esphome/components/rp2040/boards.py | 2199 ++++++++++++++++- esphome/components/rp2040/generate_boards.py | 186 ++ esphome/components/rp2040/gpio.py | 22 +- .../components/test_rp2040_generate_boards.py | 273 ++ 5 files changed, 2688 insertions(+), 17 deletions(-) create mode 100644 esphome/components/rp2040/boards.jinja2 create mode 100644 esphome/components/rp2040/generate_boards.py create mode 100644 tests/unit_tests/components/test_rp2040_generate_boards.py diff --git a/esphome/components/rp2040/boards.jinja2 b/esphome/components/rp2040/boards.jinja2 new file mode 100644 index 00000000000..989fb83701a --- /dev/null +++ b/esphome/components/rp2040/boards.jinja2 @@ -0,0 +1,25 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= {{ cyw43_gpio_offset }} to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = {{ cyw43_gpio_offset }} +CYW43_MAX_GPIO = {{ cyw43_max_gpio }} +DEFAULT_MAX_PIN = {{ default_max_pin }} + +RP2040_BASE_PINS = {} + +RP2040_BOARD_PINS = { +{%- for name, pins in board_pins %} + {{ name | repr }}: {{ pins | format_pins }}, +{%- endfor %} +} + +BOARDS = { +{%- for name, info in boards %} + {{ name | repr }}: { + {%- for key, value in info.items() %} + {{ key | repr }}: {{ value | repr }}, + {%- endfor %} + }, +{%- endfor %} +} diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c761efba586..c99934567a1 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1,28 +1,2205 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = 64 +CYW43_MAX_GPIO = 66 +DEFAULT_MAX_PIN = 29 + RP2040_BASE_PINS = {} RP2040_BOARD_PINS = { - "pico": { - "SDA": 4, - "SCL": 5, - "LED": 25, - "SDA1": 26, - "SCL1": 27, + "0xcb_helios": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL1": 3, + "SDA1": 2, + "SS": 21, + "TX": 0, }, - "rpipico": "pico", - "rpipicow": { - "SDA": 4, + "DudesCab": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 19, + "SCL1": 11, + "SDA": 18, + "SDA1": 10, + "SS": 5, + "TX": 0, + }, + "MyRP_2350B": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, "SCL": 5, - "LED": 32, - "SDA1": 26, "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "MyRP_bot": { + "LED": 25, + "MISO": 12, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 4, + "SDA": 16, + "SDA1": 5, + "SS": 13, + }, + "adafruit_feather": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 25, + "SDA": 2, + "SDA1": 24, + "SS": 17, + "TX": 0, + }, + "adafruit_feather_adalogger": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_can": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_dvi": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_prop_maker": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rfm": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_adalogger": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_hstx": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "adafruit_feather_scorpio": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_thinkink": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_usb_host": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_floppsy": { + "LED": 28, + "MISO": 20, + "MOSI": 19, + "SCK": 18, + "SCL": 17, + "SDA": 16, + "SS": 24, + }, + "adafruit_fruitjam": { + "LED": 29, + "MISO": 36, + "MOSI": 35, + "RX": 9, + "SCK": 34, + "SCL": 21, + "SDA": 20, + "SS": 39, + "TX": 8, + }, + "adafruit_itsybitsy": { + "LED": 11, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 25, + "SCL1": 3, + "SDA": 24, + "SDA1": 2, + "TX": 0, + }, + "adafruit_kb2040": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "TX": 0, + }, + "adafruit_macropad2040": {"LED": 13, "SCL": 21, "SDA": 20}, + "adafruit_metro": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "SS": 23, + "TX": 0, + }, + "adafruit_metro_rp2350": { + "LED": 23, + "MISO": 28, + "MOSI": 31, + "RX": 1, + "SCK": 30, + "SCL": 21, + "SDA": 20, + "SS": 29, + "TX": 0, + }, + "adafruit_qtpy": { + "MISO": 4, + "MOSI": 3, + "RX": 29, + "SCK": 6, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "TX": 28, + }, + "adafruit_stemmafriend": { + "LED": 12, + "MISO": 4, + "MOSI": 7, + "RX": 27, + "SCK": 2, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 1, + "TX": 26, + }, + "adafruit_trinkeyrp2040qt": {"RX": 17, "SCL": 17, "SDA": 16, "TX": 16}, + "akana_r1": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "amken_bunny": {"LED": 24, "RX": 1, "TX": 0}, + "amken_revelop": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "amken_revelop_es": {"LED": 5, "MISO": 0, "MOSI": 3, "SCK": 2, "SS": 1, "TX": 20}, + "amken_revelop_plus": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "artronshop_rp2_nano": { + "LED": 13, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 19, + "SDA": 16, + "SDA1": 18, + "SS": 5, + "TX": 0, + }, + "bigtreetech_SKR_Pico": {"LED": 13, "RX": 1, "TX": 0}, + "breadstick_raspberry": { + "RX": 21, + "SCL": 13, + "SCL1": 23, + "SDA": 12, + "SDA1": 22, + "TX": 20, + }, + "bridgetek_idm2040_43a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "bridgetek_idm2040_7a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "challenger_2040_lora": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_lte": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_nfc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SCL1": 11, + "SDA": 0, + "SDA1": 10, + "SS": 21, + "TX": 16, + }, + "challenger_2040_sdrtc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_subghz": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_uwb": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi6_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2350_bconnect": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 11, + "SDA": 20, + "SDA1": 10, + "SS": 17, + "TX": 12, + }, + "challenger_2350_wifi6_ble5": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 17, + "TX": 12, + }, + "challenger_nb_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "connectivity_2040_lte_wifi_ble": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "cytron_iriv_io_controller": { + "LED": 29, + "MISO": 20, + "MOSI": 19, + "RX": 31, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 31, + }, + "cytron_maker_nano_rp2040": { + "LED": 2, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 1, + "SCL1": 27, + "SDA": 0, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "cytron_maker_pi_rp2040": { + "LED": 3, + "RX": 1, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "TX": 0, + }, + "cytron_maker_uno_rp2040": { + "LED": 3, + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 13, + "TX": 0, + }, + "cytron_motion_2350_pro": { + "LED": 2, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 17, + "SCL1": 27, + "SDA": 16, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "datanoisetv_picoadk": { + "LED": 15, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "datanoisetv_picoadk_v2": { + "LED": 2, + "MISO": 8, + "MOSI": 7, + "RX": 13, + "SCK": 6, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 5, + "TX": 12, + }, + "degz_suibo": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "dfrobot_beetle_rp2040": { + "LED": 13, + "MISO": 0, + "MOSI": 3, + "RX": 29, + "SCK": 2, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 1, + "TX": 28, + }, + "electroniccats_huntercat_nfc": {"LED": 8, "RX": 1, "SCL": 5, "SDA": 4, "TX": 0}, + "evn_alpha": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 1, + "TX": 0, + }, + "extelec_rc2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SDA": 4, + "SS": 5, + "TX": 0, + }, + "flyboard2040_core": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 15, + "SDA": 16, + "SDA1": 14, + "SS": 5, + "TX": 0, + }, + "geeekpi_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic_rp2350": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "groundstudio_marble_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "ilabs_rpico32": { + "MISO": 24, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "jumperless_v1": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 5, + "SCL1": 19, + "SDA": 4, + "SDA1": 18, + "SS": 1, + "TX": 16, + }, + "jumperless_v5": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 21, + "TX": 0, + }, + "melopero_cookie_rp2040": { + "LED": 21, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "melopero_shake_rp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 3, + "SDA": 8, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "mksthr36": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "mksthr42": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "nekosystems_bl2040_mini": { + "LED": 6, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "SS": 17, + "TX": 12, + }, + "newsan_archi": { + "MISO": 4, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 1, + "SCL1": 7, + "SDA": 0, + "SDA1": 6, + "SS": 5, + "TX": 16, + }, + "nullbits_bit_c_pro": { + "LED": 18, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 5, + "SDA": 2, + "SDA1": 4, + "SS": 21, + "TX": 0, + }, + "olimex_pico2bb48": { + "LED": 25, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 5, + "TX": 0, + }, + "olimex_pico2xl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "olimex_pico2xxl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "picolume": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "pimoroni_explorer": { + "MISO": 10, + "MOSI": 10, + "RX": 10, + "SCK": 10, + "SCL": 21, + "SCL1": 10, + "SDA": 20, + "SDA1": 10, + "SS": 10, + "TX": 10, + }, + "pimoroni_pico_plus_2": { + "LED": 25, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_pico_plus_2w": { + "LED": 64, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_plasma2040": {"LED": 16, "SCL": 21, "SDA": 20}, + "pimoroni_plasma2350": { + "LED": 16, + "MISO": 31, + "MOSI": 31, + "RX": 31, + "SCK": 31, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 31, + "TX": 31, + }, + "pimoroni_plasma2350w": { + "LED": 16, + "MISO": 24, + "MOSI": 24, + "RX": 31, + "SCK": 29, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 25, + "TX": 31, + }, + "pimoroni_servo2040": {"LED": 18, "SCL": 21, "SDA": 20}, + "pimoroni_tiny2040": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "pimoroni_tiny2350": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 7, + "SDA": 12, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "pintronix_pinmax": { + "LED": 27, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SDA": 4, + "SS": 17, + "TX": 0, + }, + "rakwireless_rak11300": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 21, + "SDA": 2, + "SDA1": 20, + "SS": 17, + "TX": 0, + }, + "rpipico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2w": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipicow": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "sea_picro": { + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "seeed_indicator_rp2040": { + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 21, + "SCL1": 15, + "SDA": 20, + "SDA1": 14, + "SS": 1, + "TX": 16, + }, + "seeed_xiao_rp2040": { + "LED": 17, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SDA": 6, + "TX": 0, + }, + "seeed_xiao_rp2350": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "silicognition_rp2040_shim": { + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "soldered_nula_rp2350": { + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 9, + "SCL1": 31, + "SDA": 8, + "SDA1": 30, + "SS": 5, + "TX": 0, + }, + "solderparty_rp2040_stamp": { + "LED": 20, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "solderparty_rp2350_stamp": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "solderparty_rp2350_stamp_xl": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "sparkfun_iotnode_lorawanrp2350": { + "LED": 25, + "MISO": 12, + "MOSI": 15, + "RX": 19, + "SCK": 14, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 13, + "TX": 18, + }, + "sparkfun_iotredboard_rp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 31, + "SDA": 4, + "SDA1": 30, + "SS": 21, + "TX": 0, + }, + "sparkfun_micromodrp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "sparkfun_thingplusrp2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "TX": 0, + }, + "sparkfun_thingplusrp2350": { + "LED": 64, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SCL1": 31, + "SDA": 6, + "SDA1": 31, + "SS": 9, + "TX": 0, + }, + "sparkfun_xrp_controller": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 5, + "SCL1": 39, + "SDA": 4, + "SDA1": 38, + "SS": 17, + "TX": 12, + }, + "sparkfun_xrp_controller_beta": {"LED": 64, "SCL": 19, "SDA": 18}, + "upesy_rp2040_devkit": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 17, + "TX": 0, + }, + "vccgnd_yd_rp2040": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "vicharak_shrike-lite": { + "LED": 4, + "MISO": 20, + "MOSI": 19, + "RX": 17, + "SCK": 18, + "SCL": 25, + "SCL1": 7, + "SDA": 24, + "SDA1": 6, + "SS": 21, + "TX": 16, + }, + "viyalab_mizu": { + "LED": 25, + "MISO": 16, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_1_28": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lora": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_matrix": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_one": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_pizero": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 21, + "TX": 0, + }, + "waveshare_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_pizero": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350b_plus_w": { + "LED": 23, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "wiznet_55rp20_evb_pico": { + "LED": 19, + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "wiznet_wizfi360_evb_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 27, + "SDA": 8, + "SDA1": 26, + "SS": 17, + "TX": 0, }, } BOARDS = { + "0xcb_helios": { + "name": "0xCB Helios", + "mcu": "rp2040", + "max_pin": 29, + }, + "DudesCab": { + "name": "L'atelier d'Arnoz DudesCab", + "mcu": "rp2040", + "max_pin": 29, + }, + "MyRP_2350B": { + "name": "MyMakers RP2350B", + "mcu": "rp2350", + "max_pin": 47, + }, + "MyRP_bot": { + "name": "MyMakers RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather": { + "name": "Adafruit Feather RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_adalogger": { + "name": "Adafruit Feather RP2040 Adalogger", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_can": { + "name": "Adafruit Feather RP2040 CAN", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_dvi": { + "name": "Adafruit Feather RP2040 DVI", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_prop_maker": { + "name": "Adafruit Feather RP2040 Prop-Maker", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rfm": { + "name": "Adafruit Feather RP2040 RFM", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rp2350_adalogger": { + "name": "Adafruit Feather RP2350 Adalogger", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_rp2350_hstx": { + "name": "Adafruit Feather RP2350 HSTX", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_scorpio": { + "name": "Adafruit Feather RP2040 SCORPIO", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_thinkink": { + "name": "Adafruit Feather RP2040 ThinkINK", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_usb_host": { + "name": "Adafruit Feather RP2040 USB Host", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_floppsy": { + "name": "Adafruit Floppsy", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_fruitjam": { + "name": "Adafruit Fruit Jam RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_itsybitsy": { + "name": "Adafruit ItsyBitsy RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_kb2040": { + "name": "Adafruit KB2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_macropad2040": { + "name": "Adafruit MacroPad RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro": { + "name": "Adafruit Metro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro_rp2350": { + "name": "Adafruit Metro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_qtpy": { + "name": "Adafruit QT Py RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_stemmafriend": { + "name": "Adafruit STEMMA Friend RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_trinkeyrp2040qt": { + "name": "Adafruit Trinkey RP2040 QT", + "mcu": "rp2040", + "max_pin": 29, + }, + "akana_r1": { + "name": "METE HOCA Akana R1", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_bunny": { + "name": "Amken BunnyBoard", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop": { + "name": "Amken Revelop", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_es": { + "name": "Amken Revelop eS", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_plus": { + "name": "Amken Revelop Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "arduino_nano_connect": { + "name": "Arduino Nano RP2040 Connect", + "mcu": "rp2040", + "max_pin": 29, + }, + "artronshop_rp2_nano": { + "name": "ArtronShop RP2 Nano", + "mcu": "rp2040", + "max_pin": 29, + }, + "bigtreetech_SKR_Pico": { + "name": "BIGTREETECH SKR-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "breadstick_raspberry": { + "name": "Breadstick Raspberry", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_43a": { + "name": "BridgeTek IDM2040-43A", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_7a": { + "name": "BridgeTek IDM2040-7A", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lora": { + "name": "iLabs Challenger 2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lte": { + "name": "iLabs Challenger 2040 LTE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_nfc": { + "name": "iLabs Challenger 2040 NFC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_sdrtc": { + "name": "iLabs Challenger 2040 SD/RTC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_subghz": { + "name": "iLabs Challenger 2040 SubGHz", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_uwb": { + "name": "iLabs Challenger 2040 UWB", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi": { + "name": "iLabs Challenger 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi6_ble": { + "name": "iLabs Challenger 2040 WiFi6/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi_ble": { + "name": "iLabs Challenger 2040 WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2350_bconnect": { + "name": "iLabs Challenger 2350 BConnect", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_2350_wifi6_ble5": { + "name": "iLabs Challenger 2350 WiFi/BLE", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_nb_2040_wifi": { + "name": "iLabs Challenger NB 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "connectivity_2040_lte_wifi_ble": { + "name": "iLabs Connectivity 2040 LTE/WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_iriv_io_controller": { + "name": "Cytron IRIV IO Controller", + "mcu": "rp2350", + "max_pin": 47, + }, + "cytron_maker_nano_rp2040": { + "name": "Cytron Maker Nano RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_pi_rp2040": { + "name": "Cytron Maker Pi RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_uno_rp2040": { + "name": "Cytron Maker Uno RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_motion_2350_pro": { + "name": "Cytron Motion 2350 Pro", + "mcu": "rp2350", + "max_pin": 47, + }, + "datanoisetv_picoadk": { + "name": "DatanoiseTV PicoADK", + "mcu": "rp2040", + "max_pin": 29, + }, + "datanoisetv_picoadk_v2": { + "name": "DatanoiseTV PicoADK v2", + "mcu": "rp2350", + "max_pin": 47, + }, + "degz_suibo": { + "name": "Degz Robotics Suibo RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "dfrobot_beetle_rp2040": { + "name": "DFRobot Beetle RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "electroniccats_huntercat_nfc": { + "name": "ElectronicCats HunterCat NFC RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "evn_alpha": { + "name": "EVN Alpha", + "mcu": "rp2040", + "max_pin": 29, + }, + "extelec_rc2040": { + "name": "ExtremeElectronics RC2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "flyboard2040_core": { + "name": "DeRuiLab FlyBoard2040Core", + "mcu": "rp2040", + "max_pin": 29, + }, + "geeekpi_rp2040_plus": { + "name": "GeeekPi RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic": { + "name": "Generic RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic_rp2350": { + "name": "Generic RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "groundstudio_marble_pico": { + "name": "GroundStudio Marble Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "ilabs_rpico32": { + "name": "iLabs RPICO32", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v1": { + "name": "Architeuthis Flux Jumperless", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v5": { + "name": "Architeuthis Flux Jumperless V5", + "mcu": "rp2350", + "max_pin": 47, + }, + "melopero_cookie_rp2040": { + "name": "Melopero Cookie RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "melopero_shake_rp2040": { + "name": "Melopero Shake RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr36": { + "name": "Makerbase MKS THR36", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr42": { + "name": "Makerbase MKS THR42", + "mcu": "rp2040", + "max_pin": 29, + }, + "nekosystems_bl2040_mini": { + "name": "Neko Systems BL2040 Mini", + "mcu": "rp2040", + "max_pin": 29, + }, + "newsan_archi": { + "name": "Newsan Archi", + "mcu": "rp2040", + "max_pin": 29, + }, + "nullbits_bit_c_pro": { + "name": "nullbits Bit-C PRO", + "mcu": "rp2040", + "max_pin": 29, + }, + "olimex_pico2bb48": { + "name": "Olimex Pico2BB48", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xl": { + "name": "Olimex Pico2XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xxl": { + "name": "Olimex Pico2XXL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_rp2040pico30": { + "name": "Olimex RP2040-Pico30", + "mcu": "rp2040", + "max_pin": 29, + }, + "picolume": { + "name": "PicoLume Transceiver", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_explorer": { + "name": "Pimoroni Explorer", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pga2040": { + "name": "Pimoroni PGA2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_pga2350": { + "name": "Pimoroni PGA2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2": { + "name": "Pimoroni PicoPlus2", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2w": { + "name": "Pimoroni PicoPlus2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "pimoroni_plasma2040": { + "name": "Pimoroni Plasma2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_plasma2350": { + "name": "Pimoroni Plasma2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_plasma2350w": { + "name": "Pimoroni Plasma2350W", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_servo2040": { + "name": "Pimoroni Servo2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2040": { + "name": "Pimoroni Tiny2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2350": { + "name": "Pimoroni Tiny2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pintronix_pinmax": { + "name": "Pintronix PinMax", + "mcu": "rp2040", + "max_pin": 29, + }, + "rakwireless_rak11300": { + "name": "RAKwireless RAK11300", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_eins": { + "name": "redscorp RP2040-Eins", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_promini": { + "name": "redscorp RP2040-ProMini", + "mcu": "rp2040", + "max_pin": 29, + }, "rpipico": { "name": "Raspberry Pi Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "rpipico2": { + "name": "Raspberry Pi Pico 2", + "mcu": "rp2350", + "max_pin": 47, + }, + "rpipico2w": { + "name": "Raspberry Pi Pico 2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "sea_picro": { + "name": "Generic Sea-Picro", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_indicator_rp2040": { + "name": "Seeed INDICATOR RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2040": { + "name": "Seeed XIAO RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2350": { + "name": "Seeed XIAO RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "silicognition_rp2040_shim": { + "name": "Silicognition RP2040-Shim", + "mcu": "rp2040", + "max_pin": 29, + }, + "soldered_nula_rp2350": { + "name": "Soldered Electronics NULA RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2040_stamp": { + "name": "Solder Party RP2040 Stamp", + "mcu": "rp2040", + "max_pin": 29, + }, + "solderparty_rp2350_stamp": { + "name": "Solder Party RP2350 Stamp", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2350_stamp_xl": { + "name": "Solder Party RP2350 Stamp XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotnode_lorawanrp2350": { + "name": "SparkFun IoT Node LoRaWAN", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotredboard_rp2350": { + "name": "SparkFun IoT RedBoard RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_micromodrp2040": { + "name": "SparkFun MicroMod RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2040": { + "name": "SparkFun ProMicro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2350": { + "name": "SparkFun ProMicro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_thingplusrp2040": { + "name": "SparkFun Thing Plus RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_thingplusrp2350": { + "name": "SparkFun Thing Plus RP2350", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller": { + "name": "SparkFun XRP Controller", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller_beta": { + "name": "SparkFun XRP Controller (Beta)", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "upesy_rp2040_devkit": { + "name": "uPesy RP2040 DevKit", + "mcu": "rp2040", + "max_pin": 29, + }, + "vccgnd_yd_rp2040": { + "name": "VCC-GND YD RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "vicharak_shrike-lite": { + "name": "Vicharak Shrike-Lite", + "mcu": "rp2040", + "max_pin": 29, + }, + "viyalab_mizu": { + "name": "Viyalab Mizu RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_0_96": { + "name": "Waveshare RP2040 LCD 0.96", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_1_28": { + "name": "Waveshare RP2040 LCD 1.28", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lora": { + "name": "Waveshare RP2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_matrix": { + "name": "Waveshare RP2040 Matrix", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_one": { + "name": "Waveshare RP2040 One", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_pizero": { + "name": "Waveshare RP2040 PiZero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_plus": { + "name": "Waveshare RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_zero": { + "name": "Waveshare RP2040 Zero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2350_lcd_0_96": { + "name": "Waveshare RP2350 LCD 0.96", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_pizero": { + "name": "Waveshare RP2350 PiZero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_plus": { + "name": "Waveshare RP2350 Plus", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_zero": { + "name": "Waveshare RP2350 Zero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350b_plus_w": { + "name": "Waveshare RP2350B Plus W", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5100s_evb_pico": { + "name": "WIZnet W5100S-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5100s_evb_pico2": { + "name": "WIZnet W5100S-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5500_evb_pico": { + "name": "WIZnet W5500-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5500_evb_pico2": { + "name": "WIZnet W5500-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_55rp20_evb_pico": { + "name": "WIZnet W55RP20-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico": { + "name": "WIZnet W6300-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico2": { + "name": "WIZnet W6300-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_wizfi360_evb_pico": { + "name": "WIZnet WizFi360-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, }, } diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py new file mode 100644 index 00000000000..a0e3699f37b --- /dev/null +++ b/esphome/components/rp2040/generate_boards.py @@ -0,0 +1,186 @@ +"""Generate boards.py from arduino-pico board definitions. + +Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> +""" + +import json +from pathlib import Path +import re +import sys + +from jinja2 import Environment, FileSystemLoader + +# Map arduino-pico pin defines to ESPHome-friendly names +PIN_NAME_MAP = { + "LED": "LED", + "WIRE0_SDA": "SDA", + "WIRE0_SCL": "SCL", + "WIRE1_SDA": "SDA1", + "WIRE1_SCL": "SCL1", + "SPI0_MISO": "MISO", + "SPI0_MOSI": "MOSI", + "SPI0_SCK": "SCK", + "SPI0_SS": "SS", + "SERIAL1_TX": "TX", + "SERIAL1_RX": "RX", +} + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs (pin - 64) +CYW43_GPIO_OFFSET = 64 +# CYW43 has 3 GPIOs: 0=LED, 1=VBUS_SENSE, 2=REG_ON +CYW43_GPIO_COUNT = 3 + +# Max GPIO pin per MCU (hardware specs from datasheets) +MCU_MAX_PIN = { + "rp2040": 29, # GPIO 0-29 + "rp2350": 47, # GPIO 0-47 (RP2350A) +} +DEFAULT_MAX_PIN = 29 + +PIN_DEFINE_RE = re.compile(r"#define\s+PIN_(\w+)\s+\((\d+)u\)") + + +def parse_variant_pins(variant_dir: Path) -> dict[str, int]: + """Parse pins_arduino.h and return mapped pin names.""" + header = variant_dir / "pins_arduino.h" + if not header.exists(): + return {} + + pins = {} + for match in PIN_DEFINE_RE.finditer(header.read_text(encoding="utf-8")): + raw_name = match.group(1) + value = int(match.group(2)) + if raw_name in PIN_NAME_MAP: + pins[PIN_NAME_MAP[raw_name]] = value + return pins + + +def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: + """Load all board definitions and return (board_pins, boards) dicts.""" + json_dir = arduino_pico_path / "tools" / "json" + variants_dir = arduino_pico_path / "variants" + + board_pins = {} + boards = {} + variant_pins_cache: dict[str, dict[str, int]] = {} + + for json_file in sorted(json_dir.glob("*.json")): + board_name = json_file.stem + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + + build = data.get("build", {}) + mcu = build.get("mcu", "rp2040") + variant = build.get("variant", board_name) + name = data.get("name", board_name) + vendor = data.get("vendor", "") + + display_name = f"{vendor} {name}".strip() if vendor else name + + boards[board_name] = { + "name": display_name, + "mcu": mcu, + "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), + } + + # Get pins for this variant + if variant not in variant_pins_cache: + variant_dir = variants_dir / variant + variant_pins_cache[variant] = parse_variant_pins(variant_dir) + + pins = variant_pins_cache[variant] + if pins: + max_pin = boards[board_name]["max_pin"] + cyw43_max = CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1 + # Filter out placeholder values (e.g. 99 = "not connected") + filtered = { + name: value + for name, value in pins.items() + if value <= max_pin or CYW43_GPIO_OFFSET <= value <= cyw43_max + } + if filtered: + board_pins[board_name] = filtered + + # Compute max_virtual_pin per board from pin maps + for board_name, pins in board_pins.items(): + if isinstance(pins, str): + continue + virtual_pins = [v for v in pins.values() if v >= CYW43_GPIO_OFFSET] + if virtual_pins and board_name in boards: + boards[board_name]["max_virtual_pin"] = max(virtual_pins) + + # Deduplicate: if board pins match its variant's pins, use string alias + for board_name in list(board_pins.keys()): + if board_name not in boards: + continue + build_variant = _get_variant(json_dir / f"{board_name}.json") + if ( + build_variant + and build_variant != board_name + and build_variant in board_pins + and board_pins[board_name] == board_pins[build_variant] + ): + board_pins[board_name] = build_variant + + return board_pins, boards + + +def _get_variant(json_file: Path) -> str | None: + """Get variant name from a board JSON file.""" + if not json_file.exists(): + return None + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + return data.get("build", {}).get("variant") + + +_TEMPLATE_DIR = Path(__file__).parent + + +def _format_pins(pins: dict[str, int] | str) -> str: + """Jinja2 filter to format a pin dict or alias as Python source.""" + if isinstance(pins, str): + return repr(pins) + items = ", ".join(f"{k!r}: {v}" for k, v in sorted(pins.items())) + return f"{{{items}}}" + + +_jinja_env = Environment( + loader=FileSystemLoader(_TEMPLATE_DIR), keep_trailing_newline=True +) +_jinja_env.filters["format_pins"] = _format_pins +_jinja_env.filters["repr"] = repr + + +def generate(arduino_pico_path: Path) -> str: + """Generate boards.py content.""" + board_pins, boards = load_boards(arduino_pico_path) + + template = _jinja_env.get_template("boards.jinja2") + return template.render( + cyw43_gpio_offset=CYW43_GPIO_OFFSET, + cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1, + default_max_pin=DEFAULT_MAX_PIN, + board_pins=sorted(board_pins.items()), + boards=sorted(boards.items()), + ) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} <arduino-pico-path>", file=sys.stderr) + sys.exit(1) + + arduino_pico_path = Path(sys.argv[1]) + if not (arduino_pico_path / "tools" / "json").exists(): + print(f"Error: {arduino_pico_path}/tools/json not found", file=sys.stderr) + sys.exit(1) + + output = generate(arduino_pico_path) + output_file = Path(__file__).parent / "boards.py" + output_file.write_text(output, encoding="utf-8") + print(f"Generated {output_file}") + + +if __name__ == "__main__": + main() diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 193e567d173..18fb09f76a4 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -54,19 +54,29 @@ def _translate_pin(value): return _lookup_pin(value) +def _board_max_virtual_pin(board): + """Get the max CYW43 virtual pin for this board, or None if no virtual pins.""" + return boards.BOARDS.get(board, {}).get("max_virtual_pin") + + def validate_gpio_pin(value): value = _translate_pin(value) board = CORE.data[KEY_RP2040][KEY_BOARD] - if board == "rpipicow" and value == 32: - return value # Special case for Pico-w LED pin - if value < 0 or value > 29: - raise cv.Invalid(f"RP2040: Invalid pin number: {value}") + max_virtual = _board_max_virtual_pin(board) + if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual: + return value + max_pin = boards.BOARDS.get(board, {}).get("max_pin", boards.DEFAULT_MAX_PIN) + if value < 0 or value > max_pin: + raise cv.Invalid(f"Invalid pin number: {value} (max {max_pin} for this board)") return value def validate_supports(value): board = CORE.data[KEY_RP2040][KEY_BOARD] - if board != "rpipicow" or value[CONF_NUMBER] != 32: + if ( + _board_max_virtual_pin(board) is None + or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET + ): return value mode = value[CONF_MODE] is_input = mode[CONF_INPUT] @@ -75,7 +85,7 @@ def validate_supports(value): is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] if not is_output or is_input or is_open_drain or is_pullup or is_pulldown: - raise cv.Invalid("Only output mode is supported for Pico-w LED pin") + raise cv.Invalid("Only output mode is supported for CYW43 virtual pins") return value diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py new file mode 100644 index 00000000000..2e40ed08ba1 --- /dev/null +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -0,0 +1,273 @@ +"""Tests for rp2040 generate_boards.py.""" + +from __future__ import annotations + +import json +from pathlib import Path +import textwrap + +import pytest + +from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins + +PICO_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_SERIAL1_TX (0u) + #define PIN_SERIAL1_RX (1u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (26u) + #define PIN_WIRE1_SCL (27u) + #define PIN_SPI0_MISO (16u) + #define PIN_SPI0_MOSI (19u) + #define PIN_SPI0_SCK (18u) + #define PIN_SPI0_SS (17u) + #include "../generic/common.h" +""") + +PICOW_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #include <cyw43_wrappers.h> + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #include "../generic/common.h" +""") + + +@pytest.fixture() +def arduino_pico(tmp_path: Path) -> Path: + """Create a minimal arduino-pico directory structure.""" + json_dir = tmp_path / "tools" / "json" + json_dir.mkdir(parents=True) + variants_dir = tmp_path / "variants" + variants_dir.mkdir() + + generic_dir = variants_dir / "generic" + generic_dir.mkdir() + (generic_dir / "common.h").write_text("#pragma once\n") + + return tmp_path + + +def _add_board( + arduino_pico: Path, + board_name: str, + mcu: str = "rp2040", + variant: str | None = None, + vendor: str = "", + name: str | None = None, + pins_header: str | None = None, +) -> None: + """Add a board JSON and variant to the fake arduino-pico tree.""" + if variant is None: + variant = board_name + if name is None: + name = board_name + + json_dir = arduino_pico / "tools" / "json" + variants_dir = arduino_pico / "variants" + + board_json = { + "build": { + "mcu": mcu, + "variant": variant, + }, + "name": name, + "vendor": vendor, + } + (json_dir / f"{board_name}.json").write_text(json.dumps(board_json)) + + variant_dir = variants_dir / variant + variant_dir.mkdir(exist_ok=True) + if pins_header is not None: + (variant_dir / "pins_arduino.h").write_text(pins_header) + + +def test_parse_basic_pins(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipico" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICO_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 25 + assert pins["SDA"] == 4 + assert pins["SCL"] == 5 + assert pins["SDA1"] == 26 + assert pins["SCL1"] == 27 + assert pins["MISO"] == 16 + assert pins["MOSI"] == 19 + assert pins["SCK"] == 18 + assert pins["SS"] == 17 + assert pins["TX"] == 0 + assert pins["RX"] == 1 + + +def test_parse_cyw43_led_pin(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipicow" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICOW_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 64 + + +def test_parse_missing_header(tmp_path: Path) -> None: + variant_dir = tmp_path / "noheader" + variant_dir.mkdir() + assert parse_variant_pins(variant_dir) == {} + + +def test_parse_unmapped_defines_ignored(tmp_path: Path) -> None: + variant_dir = tmp_path / "custom" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text( + "#define PIN_NEOPIXEL (16u)\n#define PIN_LED (25u)\n" + ) + + pins = parse_variant_pins(variant_dir) + assert "NEOPIXEL" not in pins + assert pins["LED"] == 25 + + +def test_load_basic_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + board_pins, boards = load_boards(arduino_pico) + + assert "rpipico" in boards + assert boards["rpipico"]["name"] == "Raspberry Pi Pico" + assert boards["rpipico"]["mcu"] == "rp2040" + assert boards["rpipico"]["max_pin"] == 29 + + assert "rpipico" in board_pins + assert board_pins["rpipico"]["LED"] == 25 + assert board_pins["rpipico"]["SDA"] == 4 + + +def test_load_rp2350_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico2", + mcu="rp2350", + vendor="Raspberry Pi", + name="Pico 2", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipico2"]["mcu"] == "rp2350" + assert boards["rpipico2"]["max_pin"] == 47 + + +def test_cyw43_board_has_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["max_virtual_pin"] == 64 + + +def test_non_cyw43_board_has_no_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert "max_virtual_pin" not in boards["rpipico"] + + +def test_board_without_variant_header(arduino_pico: Path) -> None: + _add_board(arduino_pico, "novariant", name="No Variant") + + board_pins, boards = load_boards(arduino_pico) + + assert "novariant" in boards + assert "novariant" not in board_pins + + +def test_shared_variant_deduplicates(arduino_pico: Path) -> None: + """Two boards sharing the same variant should alias.""" + _add_board(arduino_pico, "base_board", pins_header=PICO_PINS_HEADER) + _add_board(arduino_pico, "alias_board", variant="base_board") + + board_pins, _ = load_boards(arduino_pico) + + assert board_pins["base_board"] == parse_variant_pins( + arduino_pico / "variants" / "base_board" + ) + assert board_pins["alias_board"] == "base_board" + + +def test_display_name_with_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="Acme", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Acme Widget" + + +def test_display_name_without_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Widget" + + +def test_unknown_mcu_gets_default_max_pin(arduino_pico: Path) -> None: + _add_board(arduino_pico, "future", mcu="rp2450", pins_header=PICO_PINS_HEADER) + _, boards = load_boards(arduino_pico) + assert boards["future"]["max_pin"] == 29 + + +def test_placeholder_pins_filtered_out(arduino_pico: Path) -> None: + """Pins with placeholder values like 99 should be filtered out.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (99u) + #define PIN_WIRE1_SCL (99u) + """) + _add_board(arduino_pico, "placeholder", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "SDA1" not in board_pins["placeholder"] + assert "SCL1" not in board_pins["placeholder"] + assert board_pins["placeholder"]["LED"] == 25 + assert "max_virtual_pin" not in boards["placeholder"] + + +def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: + """Pin 99 should not cause max_virtual_pin to be set.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_SPI0_MISO (99u) + """) + _add_board(arduino_pico, "badpin", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "MISO" not in board_pins["badpin"] + assert boards["badpin"]["max_virtual_pin"] == 64 From 82629c397f699af8b263e66e7cc904bebcc297d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:01:50 -1000 Subject: [PATCH 0432/2056] [hlk_fm22x] Fix oversized response rejection breaking GET_ALL_FACE_IDS (#14506) --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 18d26f057a8..7c7c8782dee 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -133,24 +133,22 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { - ESP_LOGE(TAG, "Response too large: %u bytes", length); - // Discard exactly the remaining payload and checksum for this frame - for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i) - this->read(); - return; - } - + // Read up to buffer size; discard excess bytes while still computing checksum + // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) + // but handlers only need the first few bytes + size_t to_store = std::min(static_cast<size_t>(length), HLK_FM22X_MAX_RESPONSE_SIZE); for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - this->recv_buf_[idx] = byte; + if (idx < to_store) { + this->recv_buf_[idx] = byte; + } } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, - format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), to_store)); #endif byte = this->read(); @@ -160,10 +158,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(this->recv_buf_.data(), length); + this->handle_note_(this->recv_buf_.data(), to_store); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(this->recv_buf_.data(), length); + this->handle_reply_(this->recv_buf_.data(), to_store); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); From 6e3bc7b1ddb5b8ac91ecf0653087dc835264ca8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:33:05 -1000 Subject: [PATCH 0433/2056] [ci] Use pull_request_target for codeowner approved label workflow (#14561) --- .github/scripts/codeowners.js | 2 +- .../codeowner-approved-label-update.yml | 63 +++++---------- .../workflows/codeowner-approved-label.yml | 78 ------------------- 3 files changed, 21 insertions(+), 122 deletions(-) delete mode 100644 .github/workflows/codeowner-approved-label.yml diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js index 5d69c11b1a2..9b2f2922c01 100644 --- a/.github/scripts/codeowners.js +++ b/.github/scripts/codeowners.js @@ -2,7 +2,7 @@ // // Used by: // - codeowner-review-request.yml -// - codeowner-approved-label.yml + codeowner-approved-label-update.yml +// - codeowner-approved-label-update.yml // - auto-label-pr/detectors.js (detectCodeOwner) /** diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 9168cce1d6b..c2eb886913e 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -1,13 +1,15 @@ -# Fallback for fork PRs: phase 1 (codeowner-approved-label.yml) handles -# non-fork PRs directly but can't write labels on fork PRs (read-only token). -# This workflow re-determines the action and applies it if needed. +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. +# +# Uses pull_request_target so that fork PRs do not require workflow approval. +# The label is reconciled on every PR update; for review events specifically, +# this means the label is applied on the next push after a codeowner review. -name: Codeowner Approved Label Update +name: Codeowner Approved Label on: - workflow_run: - workflows: ["Codeowner Approved Label"] - types: [completed] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] permissions: issues: write @@ -15,51 +17,23 @@ permissions: contents: read jobs: - update-label: + codeowner-approved: name: Run - if: > - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'pull_request_review' + if: ${{ github.repository == 'esphome/esphome' }} runs-on: ubuntu-latest steps: - - name: Get PR details - id: pr - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - REPO: ${{ github.repository }} - run: | - pr_data=$(gh pr list --repo "$REPO" --state open --search "$HEAD_SHA" \ - --json number,baseRefName --jq '.[0] // empty') - - if [ -z "$pr_data" ]; then - echo "No open PR found for SHA $HEAD_SHA, skipping" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - pr_number=$(echo "$pr_data" | jq -r '.number') - base_ref=$(echo "$pr_data" | jq -r '.baseRefName') - - echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" - echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" - echo "Found PR #$pr_number targeting $base_ref" - - - name: Checkout base repository - if: steps.pr.outputs.skip != 'true' + - name: Checkout base branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - repository: ${{ github.repository }} - ref: ${{ steps.pr.outputs.base_ref }} + ref: ${{ github.event.pull_request.base.sha }} sparse-checkout: | .github/scripts/codeowners.js CODEOWNERS - - name: Update label - if: steps.pr.outputs.skip != 'true' + - name: Check codeowner approval and update label uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number }} with: script: | const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); @@ -76,6 +50,11 @@ jobs: github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME ); + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + if (action === LabelAction.ADD) { await github.rest.issues.addLabels({ owner, repo, issue_number: pr_number, labels: [LABEL_NAME] @@ -90,6 +69,4 @@ jobs: } catch (error) { if (error.status !== 404) throw error; } - } else { - console.log('No label change needed'); } diff --git a/.github/workflows/codeowner-approved-label.yml b/.github/workflows/codeowner-approved-label.yml deleted file mode 100644 index 12199bd0b04..00000000000 --- a/.github/workflows/codeowner-approved-label.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Adds/removes a 'code-owner-approved' label when a component-specific -# codeowner approves (or dismisses) a PR. -# -# Handles non-fork PRs directly. For fork PRs the GITHUB_TOKEN is read-only, -# so label writes are deferred to codeowner-approved-label-update.yml which -# triggers via workflow_run with write permissions. - -name: Codeowner Approved Label - -on: - pull_request_review: - types: [submitted, dismissed] - -permissions: - issues: write - pull-requests: read - contents: read - -jobs: - codeowner-approved: - name: Run - if: ${{ github.repository == 'esphome/esphome' }} - runs-on: ubuntu-latest - steps: - - name: Checkout base branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.base.sha }} - sparse-checkout: | - .github/scripts/codeowners.js - CODEOWNERS - - - name: Check codeowner approval and update label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - with: - script: | - const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const pr_number = parseInt(process.env.PR_NUMBER, 10); - const LABEL_NAME = 'code-owner-approved'; - - console.log(`Processing PR #${pr_number} for codeowner approval label`); - - const codeownersPatterns = loadCodeowners(); - const action = await determineLabelAction( - github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME - ); - - if (action === LabelAction.NONE) { - console.log('No label change needed'); - return; - } - - try { - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - await github.rest.issues.removeLabel({ - owner, repo, issue_number: pr_number, name: LABEL_NAME - }); - console.log(`Removed '${LABEL_NAME}' label`); - } - } catch (error) { - if (error.status === 403) { - console.log('Fork PR: deferring label write to phase 2 workflow'); - } else if (error.status === 404) { - console.log('Label already removed'); - } else { - throw error; - } - } From 65b7c73bf3fdbbe9260040b96e758561dc9be548 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 08:02:34 -1000 Subject: [PATCH 0434/2056] [sgp4x] Fix undefined behavior from mutating entity config at runtime (#14562) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/sgp4x/sgp4x.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 23589265ca0..44d0a54080b 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -35,13 +35,9 @@ void SGP4xComponent::setup() { this->self_test_time_ = SPG40_SELFTEST_TIME; this->measure_time_ = SGP40_MEASURE_TIME; if (this->nox_sensor_) { - ESP_LOGE(TAG, "SGP41 required for NOx"); - // disable the sensor - this->nox_sensor_->set_disabled_by_default(true); - // make sure it's not visible in HA - this->nox_sensor_->set_internal(true); - this->nox_sensor_->state = NAN; - // remove pointer to sensor + ESP_LOGE(TAG, "SGP41 required for NOx, disabling NOx sensor"); + // Drop the pointer so update() never publishes to it. + // The entity remains registered but will never receive state updates. this->nox_sensor_ = nullptr; } } else if (featureset == SGP41_FEATURESET) { From b2378e830e947ecc8d79f197abf838868927053e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Fri, 6 Mar 2026 19:11:52 +0100 Subject: [PATCH 0435/2056] [rtttl] Add AudioStreamInfo and set volume (#14439) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 40 ++++++++++-------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 4ccfc539eac..9bf0450993c 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,11 +29,6 @@ static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10; static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048; static constexpr uint16_t SAMPLE_RATE = 16000; -struct SpeakerSample { - int8_t left{0}; - int8_t right{0}; -}; - inline double deg2rad(double degrees) { static constexpr double PI_ON_180 = M_PI / 180.0; return degrees * PI_ON_180; @@ -108,6 +103,9 @@ void Rtttl::loop() { } } else if (this->state_ == State::INIT) { if (this->speaker_->is_stopped()) { + audio::AudioStreamInfo audio_stream_info = audio::AudioStreamInfo(16, 1, SAMPLE_RATE); + this->speaker_->set_audio_stream_info(audio_stream_info); + this->speaker_->set_volume(this->gain_); this->speaker_->start(); this->set_state_(State::STARTING); } @@ -120,35 +118,27 @@ void Rtttl::loop() { return; } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; + int16_t sample[SAMPLE_BUFFER_SIZE]; uint16_t sample_index = 0; double rem = 0.0; - while (true) { + while (sample_index < SAMPLE_BUFFER_SIZE && this->samples_sent_ < this->samples_count_) { // Try and send out the remainder of the existing note, one per `loop()` if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - - int8_t val = (127 * this->gain_) * sin(deg2rad(rem)); - - sample[sample_index].left = val; - sample[sample_index].right = val; + sample[sample_index] = INT16_MAX * sin(deg2rad(rem)); } else { - sample[sample_index].left = 0; - sample[sample_index].right = 0; - } - - if (sample_index >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) { - break; + sample[sample_index] = 0; } this->samples_sent_++; sample_index++; } if (sample_index > 0) { - size_t bytes_to_send = sample_index * sizeof(SpeakerSample); - size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send); - if (send != bytes_to_send) { - this->samples_sent_ -= (sample_index - (send / sizeof(SpeakerSample))); + size_t bytes = sample_index * sizeof(int16_t); + size_t sent_bytes = this->speaker_->play((uint8_t *) (&sample), bytes); + size_t samples_sent = sent_bytes / sizeof(int16_t); + if (samples_sent != sample_index) { + this->samples_sent_ -= (sample_index - samples_sent); } return; } @@ -408,11 +398,7 @@ void Rtttl::finish_() { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; + int16_t sample[2] = {0, 0}; this->speaker_->play((uint8_t *) (&sample), sizeof(sample)); this->speaker_->finish(); this->set_state_(State::STOPPING); From 8a915dcbbed3af2e285dce91f32c03743310ca21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 08:34:27 -1000 Subject: [PATCH 0436/2056] [core] Move device class strings to PROGMEM on ESP8266 (#14443) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 84 ++++++++++++++----- esphome/components/api/api_connection.h | 34 ++------ .../components/mqtt/mqtt_binary_sensor.cpp | 6 +- esphome/components/mqtt/mqtt_button.cpp | 6 -- esphome/components/mqtt/mqtt_component.cpp | 5 ++ esphome/components/mqtt/mqtt_cover.cpp | 7 +- esphome/components/mqtt/mqtt_event.cpp | 7 -- esphome/components/mqtt/mqtt_number.cpp | 4 - esphome/components/mqtt/mqtt_sensor.cpp | 5 -- esphome/components/mqtt/mqtt_text_sensor.cpp | 6 -- esphome/components/mqtt/mqtt_valve.cpp | 7 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/core/config.py | 6 ++ esphome/core/entity_base.cpp | 37 +++++++- esphome/core/entity_base.h | 33 ++++++-- esphome/core/entity_helpers.py | 8 +- tests/unit_tests/core/test_entity_helpers.py | 17 ++++ 17 files changed, 167 insertions(+), 108 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 98ba1abe0b5..77920432c0d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -396,6 +396,48 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess return static_cast<uint16_t>(header_padding + calculated_size + footer_size); } +uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + // Set common fields that are shared by all entity types + msg.key = entity->get_object_id_hash(); + + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_message_to_buffer is called + char object_id_buf[OBJECT_ID_MAX_LEN]; + if (!conn->client_supports_api_version(1, 14)) { + msg.object_id = entity->get_object_id_to(object_id_buf); + } + + if (entity->has_own_name()) { + msg.name = entity->get_name(); + } + + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); +#endif + msg.disabled_by_default = entity->is_disabled_by_default(); + msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size); +} + +uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, + uint8_t message_type, APIConnection *conn, + uint32_t remaining_size) { + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + device_class_field = StringRef(entity->get_device_class_to(dc_buf)); + return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); +} + #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE, @@ -414,10 +456,9 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -443,8 +484,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, + ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -609,9 +650,9 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, + ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -631,8 +672,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, + ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -661,9 +702,8 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; - msg.device_class = text_sensor->get_device_class_ref(); - return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info_with_device_class( + text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -776,11 +816,11 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode()); - msg.device_class = number->get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, + ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -925,8 +965,8 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; - msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, + ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -986,11 +1026,11 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c auto *valve = static_cast<valve::Valve *>(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class_ref(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, + ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1434,9 +1474,9 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, + ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1492,8 +1532,8 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; - msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, + ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 88f0ef82d66..2c66a194a6b 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -334,36 +334,12 @@ class APIConnection final : public APIServerConnectionBase { // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - // Set common fields that are shared by all entity types - msg.key = entity->get_object_id_hash(); + APIConnection *conn, uint32_t remaining_size); - // API 1.14+ clients compute object_id client-side from the entity name - // For older clients, we must send object_id for backward compatibility - // See: https://github.com/esphome/backlog/issues/76 - // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called - char object_id_buf[OBJECT_ID_MAX_LEN]; - if (!conn->client_supports_api_version(1, 14)) { - msg.object_id = entity->get_object_id_to(object_id_buf); - } - - if (entity->has_own_name()) { - msg.name = entity->get_name(); - } - - // Set common EntityBase properties -#ifdef USE_ENTITY_ICON - char icon_buf[MAX_ICON_LENGTH]; - msg.icon = StringRef(entity->get_icon_to(icon_buf)); -#endif - msg.disabled_by_default = entity->is_disabled_by_default(); - msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); - } + // Wrapper for entity types that have a device_class field + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size); #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 75995f61e06..ebb29db44f0 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,15 +30,11 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->binary_sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_OFF] = mqtt::global_mqtt_client->get_availability().payload_not_available; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } bool MQTTBinarySensorComponent::send_initial_state() { diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 718fe930165..7e0ae7d06e1 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -30,13 +30,7 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - const auto device_class = this->button_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button") diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index d31a78b0900..afc514609cc 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -214,6 +214,11 @@ bool MQTTComponent::send_discovery_() { if (icon[0] != '\0') { root[MQTT_ICON] = icon; } + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = this->get_entity()->get_device_class_to(dc_buf); + if (dc[0] != '\0') { + root[MQTT_DEVICE_CLASS] = dc; + } const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 97520040942..ddb4b2d69d2 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -91,12 +91,6 @@ void MQTTCoverComponent::dump_config() { } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->cover_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -129,6 +123,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf); } } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (traits.get_supports_tilt() && !traits.get_supports_position()) { config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 37d5c2551a9..93ff6971b36 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -20,13 +20,6 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->event_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a2734f2beb0..b0bac8b3d71 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -57,10 +57,6 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX)); } - const auto device_class = this->number_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a7d311d194a..c66465dd16f 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,11 +44,6 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - if (this->sensor_->has_accuracy_decimals()) { root[MQTT_SUGGESTED_DISPLAY_PRECISION] = this->sensor_->get_accuracy_decimals(); } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index a6b9f90b683..3acd71b50d9 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -14,12 +14,6 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->sensor_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2b9f02858b5..b155a4c8972 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -64,12 +64,6 @@ void MQTTValveComponent::dump_config() { } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto device_class = this->valve_->get_device_class_ref(); - if (!device_class.empty()) { - root[MQTT_DEVICE_CLASS] = device_class; - } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) - auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { root[MQTT_OPTIMISTIC] = true; @@ -78,6 +72,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve") diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index bc90c88e57f..5590e67b822 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2137,7 +2137,8 @@ json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } - root[ESPHOME_F("device_class")] = obj->get_device_class_ref(); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf); this->add_sorting_info_(root, obj); } diff --git a/esphome/core/config.py b/esphome/core/config.py index 8631726a021..d4a839cb795 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -223,6 +223,12 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max device class string length (47 chars + null = 48-byte PROGMEM buffer) +# Keep in sync with MAX_DEVICE_CLASS_LENGTH in esphome/core/entity_base.h: +# DEVICE_CLASS_MAX_LENGTH == MAX_DEVICE_CLASS_LENGTH - 1 (C++ includes the null) +DEVICE_CLASS_MAX_LENGTH = 47 + + # Max icon string length (63 chars + null = 64-byte PROGMEM buffer) # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 37e7fcc9987..5c4e1c44459 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -51,7 +51,27 @@ __attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return " __attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; } __attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; } -// Entity device class (from index) +// Entity device class — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_device_class_to([[maybe_unused]] std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const { +#ifdef USE_ENTITY_DEVICE_CLASS + const uint8_t idx = this->device_class_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *dc = entity_device_class_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), dc, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_device_class_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated device class accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_device_class_ref() const { #ifdef USE_ENTITY_DEVICE_CLASS return StringRef(entity_device_class_lookup(this->device_class_idx_)); @@ -59,7 +79,14 @@ StringRef EntityBase::get_device_class_ref() const { return StringRef(entity_device_class_lookup(0)); #endif } -std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); } +std::string EntityBase::get_device_class() const { +#ifdef USE_ENTITY_DEVICE_CLASS + return std::string(entity_device_class_lookup(this->device_class_idx_)); +#else + return std::string(entity_device_class_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity unit of measurement (from index) StringRef EntityBase::get_unit_of_measurement_ref() const { @@ -191,8 +218,10 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) #endif void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + const char *dc = obj.get_device_class_to(dc_buf); + if (dc[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, dc); } } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1ce1e658e02..20eb68b67a7 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,11 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum device class string buffer size (47 chars + null terminator) +// Longest standard device class: "volatile_organic_compounds_parts" (32 chars) +// Device classes are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_DEVICE_CLASS_LENGTH = 48; + // Maximum icon string buffer size (63 chars + null terminator) // Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. static constexpr size_t MAX_ICON_LENGTH = 64; @@ -113,13 +118,31 @@ class EntityBase { #endif } - // Get device class as StringRef (from packed index) + // Get this entity's device class into a stack buffer. + // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_device_class_to(std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed + // directly as const char*. Use get_device_class_to() with a stack buffer instead. + template<typename T = int> StringRef get_device_class_ref() const { + static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return StringRef(""); + } + template<typename T = int> std::string get_device_class() const { + static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). " + "Use get_device_class_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM. + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_device_class_ref() const; - /// Get the device class as std::string (deprecated, prefer get_device_class_ref()) - ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " - "ESPHome 2026.9.0", - "2026.3.0") + ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std::string get_device_class() const; +#endif // Get unit of measurement as StringRef (from packed index) StringRef get_unit_of_measurement_ref() const; /// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref()) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 01fa27b833a..a46d2466fdf 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import ICON_MAX_LENGTH +from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -132,7 +132,7 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", True), ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -179,6 +179,10 @@ def _register_string( def register_device_class(value: str) -> int: """Register a device_class string and return its 1-based index.""" + if value and len(value) > DEVICE_CLASS_MAX_LENGTH: + raise ValueError( + f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'" + ) return _register_string( value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" ) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 79bc3095b92..1392a1d0436 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_device_class, register_icon, setup_entity, ) @@ -926,6 +927,22 @@ def test_register_icon_max_length() -> None: assert register_icon("") == 0 +def test_register_device_class_max_length() -> None: + """Test register_device_class rejects device classes exceeding 47 characters.""" + # 47 chars should succeed + max_dc = "a" * 47 + idx = register_device_class(max_dc) + assert idx > 0 + + # 48 chars should fail + too_long = "a" * 48 + with pytest.raises(ValueError, match="Device class string too long"): + register_device_class(too_long) + + # Empty string returns 0 + assert register_device_class("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], From 9654140c00fecc7c86c554c7d733436c2505d2b5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:00:46 -0500 Subject: [PATCH 0437/2056] [tm1638][rp2040_pio_led_strip][atm90e32] Fix bounds checks and off-by-one (#14559) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/atm90e32/atm90e32.cpp | 4 +-- .../rp2040_pio_led_strip/led_strip.cpp | 2 +- esphome/components/tm1638/tm1638.cpp | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 412964d0f87..ee7fe5ce75e 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { - uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain); + uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); @@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { - uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain); + uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index dc0d3c315ac..fdb49fb3efe 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,7 +70,7 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] >= 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 8ef546ff323..c67ff1adbc2 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -147,35 +147,38 @@ void TM1638Component::set_intensity(uint8_t brightness_level) { uint8_t TM1638Component::print(uint8_t start_pos, const char *str) { uint8_t pos = start_pos; - bool last_was_dot = false; for (; *str != '\0'; str++) { uint8_t data = TM1638_UNKNOWN_CHAR; if (*str >= ' ' && *str <= '~') { - data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); // subract 32 to account for ASCII offset - } else if (data == TM1638_UNKNOWN_CHAR) { + // Subtract 32 to account for ASCII offset + data = progmem_read_byte(&TM1638Translation::SEVEN_SEG[*str - 32]); + } else { ESP_LOGW(TAG, "Encountered character '%c' with no TM1638 representation while translating string!", *str); } - if (*str == '.') // handle dots - { - if (pos != start_pos && - !last_was_dot) // if we are not at the first position, backup by one unless last char was a dot - { + if (*str == '.') { + // Merge dot onto previous character unless we're at the start or last was also a dot + if (pos != start_pos && !last_was_dot) { pos--; } - this->buffer_[pos] |= 0b10000000; // turn on the dot on the previous position - last_was_dot = true; // set a bit in case the next chracter is also a dot - } else // if not a dot, then just write the character to display - { + if (pos >= 8) { + ESP_LOGI(TAG, "TM1638 String is too long for the display!"); + break; + } + // Turn on the dot on the previous position + this->buffer_[pos] |= 0b10000000; + last_was_dot = true; + } else { + // Not a dot, write the character to display if (pos >= 8) { ESP_LOGI(TAG, "TM1638 String is too long for the display!"); break; } this->buffer_[pos] = data; - last_was_dot = false; // clear dot tracking bit + last_was_dot = false; } pos++; From 42dbb51022a61e07b26d82b6d348f470d2afa72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 09:03:54 -1000 Subject: [PATCH 0438/2056] [api] Devirtualize protobuf encode/calculate_size (#14449) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 274 ++-- esphome/components/api/api_connection.h | 94 +- esphome/components/api/api_pb2.cpp | 1436 ++++++++++------- esphome/components/api/api_pb2.h | 352 ++-- esphome/components/api/api_pb2_service.h | 8 - esphome/components/api/api_server.cpp | 8 +- esphome/components/api/api_server.h | 2 +- esphome/components/api/list_entities.cpp | 2 +- esphome/components/api/proto.h | 421 +---- .../bluetooth_proxy/bluetooth_connection.cpp | 21 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 18 +- .../voice_assistant/voice_assistant.cpp | 11 +- .../components/zwave_proxy/zwave_proxy.cpp | 6 +- script/api_protobuf/api_protobuf.py | 107 +- 14 files changed, 1373 insertions(+), 1387 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 77920432c0d..8721072e499 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -275,7 +275,7 @@ void APIConnection::check_keepalive_(uint32_t now) { // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + this->flags_.sent_ping = this->send_message(req); if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority @@ -336,7 +336,7 @@ bool APIConnection::send_disconnect_response_() { this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; - return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_disconnect_response() { // Don't close socket here, let APIServer::loop() do it @@ -344,61 +344,19 @@ void APIConnection::on_disconnect_response() { this->flags_.remove = true; } -// Encodes a message to the buffer and returns the total number of bytes used, -// including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - // If in log-only mode, just log and return - if (conn->flags_.log_only_mode) { - DumpBuffer dump_buf; - conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); - return 1; // Return non-zero to indicate "success" for logging - } +uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); #endif - - // Calculate size - uint32_t calculated_size = msg.calculated_size(); - - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); - const uint8_t footer_size = conn->helper_->frame_footer_size(); - - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) { - return 0; // Doesn't fit - } - - // Get buffer size after allocation (which includes header padding) - std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); - - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller, just clear flag - conn->flags_.batch_first_message = false; - } else { - // Batch message second or later - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve(current_size + total_calculated_size); - shared_buf.resize(current_size + footer_size + header_padding); - } - - // Pre-resize buffer to include payload, then encode through raw pointer - size_t write_start = shared_buf.size(); - shared_buf.resize(write_start + calculated_size); - ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); - - // Return total size (header + payload + footer) - return static_cast<uint16_t>(header_padding + calculated_size + footer_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, - uint8_t message_type, APIConnection *conn, - uint32_t remaining_size) { + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -406,7 +364,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp // For older clients, we must send object_id for backward compatibility // See: https://github.com/esphome/backlog/issues/76 // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called + // Buffer must remain in scope until encode_to_buffer is called char object_id_buf[OBJECT_ID_MAX_LEN]; if (!conn->client_supports_api_version(1, 14)) { msg.object_id = entity->get_object_id_to(object_id_buf); @@ -426,16 +384,17 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, StringRef &device_class_field, - uint8_t message_type, APIConnection *conn, + CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, uint32_t remaining_size) { char dc_buf[MAX_DEVICE_CLASS_LENGTH]; device_class_field = StringRef(entity->get_device_class_to(dc_buf)); - return fill_and_encode_entity_info(entity, msg, message_type, conn, remaining_size); + return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size); } #ifdef USE_BINARY_SENSOR @@ -449,16 +408,14 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); ListEntitiesBinarySensorResponse msg; msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info_with_device_class( - binary_sensor, msg, msg.device_class, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -474,7 +431,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(cover, msg, conn, remaining_size); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast<cover::Cover *>(entity); @@ -484,8 +441,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, - ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -517,7 +473,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast<enums::FanDirection>(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(fan, msg, conn, remaining_size); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast<fan::Fan *>(entity); @@ -528,7 +484,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(fan, msg, conn, remaining_size); } void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -571,7 +527,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(light, resp, conn, remaining_size); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast<light::LightState *>(entity); @@ -596,7 +552,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(light, msg, conn, remaining_size); } void APIConnection::on_light_command_request(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -641,7 +597,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -651,8 +607,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); - return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, - ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -665,15 +620,14 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast<switch_::Switch *>(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size); } uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast<switch_::Switch *>(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, - ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -697,13 +651,12 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); ListEntitiesTextSensorResponse msg; - return fill_and_encode_entity_info_with_device_class( - text_sensor, msg, msg.device_class, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -743,7 +696,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(climate, resp, conn, remaining_size); } uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast<climate::Climate *>(entity); @@ -770,7 +723,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(climate, msg, conn, remaining_size); } void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -808,7 +761,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(number, resp, conn, remaining_size); } uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -819,8 +772,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, - ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -840,12 +792,12 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(date, resp, conn, remaining_size); } uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast<datetime::DateEntity *>(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(date, msg, conn, remaining_size); } void APIConnection::on_date_command_request(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -865,12 +817,12 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(time, resp, conn, remaining_size); } uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast<datetime::TimeEntity *>(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(time, msg, conn, remaining_size); } void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -892,12 +844,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(datetime, resp, conn, remaining_size); } uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(datetime, msg, conn, remaining_size); } void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -916,7 +868,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(text, resp, conn, remaining_size); } uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -926,7 +878,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(text, msg, conn, remaining_size); } void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -945,14 +897,14 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(select, resp, conn, remaining_size); } uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast<select::Select *>(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(select, msg, conn, remaining_size); } void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -965,8 +917,7 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast<button::Button *>(entity); ListEntitiesButtonResponse msg; - return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, - ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size); } void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -983,7 +934,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast<lock::Lock *>(entity); LockStateResponse resp; resp.state = static_cast<enums::LockState>(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size); } uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -992,7 +943,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size); } void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -1020,7 +971,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(valve, resp, conn, remaining_size); } uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast<valve::Valve *>(entity); @@ -1029,8 +980,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, - ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1056,7 +1006,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(media_player, resp, conn, remaining_size); } uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast<media_player::MediaPlayer *>(entity); @@ -1073,8 +1023,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; } - return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_info(media_player, msg, conn, remaining_size); } void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1115,7 +1064,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message(msg)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1141,7 +1090,7 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast<camera::Camera *>(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(camera, msg, conn, remaining_size); } void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1296,7 +1245,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); @@ -1328,7 +1277,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { if (!this->send_voice_assistant_get_configuration_response_(msg)) { @@ -1363,8 +1312,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); - return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { @@ -1373,8 +1321,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size); + return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size); } void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1421,7 +1368,7 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(wh, resp, conn, remaining_size); } uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast<water_heater::WaterHeater *>(entity); @@ -1432,7 +1379,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(wh, msg, conn, remaining_size); } void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { @@ -1468,15 +1415,14 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(event, resp, conn, remaining_size); } uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast<event::Event *>(entity); ListEntitiesEventResponse msg; msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, - ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size); } #endif @@ -1493,9 +1439,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF #endif } -void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { - this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); -} +void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif #ifdef USE_INFRARED @@ -1503,7 +1447,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast<infrared::Infrared *>(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif @@ -1527,13 +1471,12 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_state(update, resp, conn, remaining_size); } uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast<update::UpdateEntity *>(entity); ListEntitiesUpdateResponse msg; - return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, - ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size); } void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1559,7 +1502,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast<enums::LogLevel>(level); msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); - return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message(msg); } void APIConnection::complete_authentication_() { @@ -1616,12 +1559,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); - return this->send_message(resp, HelloResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_ping_response_() { PingResponse resp; - return this->send_message(resp, PingResponse::MESSAGE_TYPE); + return this->send_message(resp); } bool APIConnection::send_device_info_response_() { @@ -1745,7 +1688,7 @@ bool APIConnection::send_device_info_response_() { } #endif - return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_hello_request(const HelloRequest &msg) { if (!this->send_hello_response_(msg)) { @@ -1845,7 +1788,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.call_id = call_id; resp.success = success; resp.error_message = error_message; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, @@ -1856,7 +1799,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.error_message = error_message; resp.response_data = response_data; resp.response_data_len = response_data_len; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1895,7 +1838,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio resp.success = true; } - return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); + return this->send_message(resp); } void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { if (!this->send_noise_encryption_set_key_response_(msg)) { @@ -1924,16 +1867,73 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } -bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { - uint32_t payload_size = msg.calculated_size(); - std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); +bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, + const void *msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise) + if (message_type != SubscribeLogsResponse::MESSAGE_TYPE +#ifdef USE_CAMERA + && message_type != CameraImageResponse::MESSAGE_TYPE +#endif + ) { + auto *proto_msg = static_cast<const ProtoMessage *>(msg); + DumpBuffer dump_buf; + this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + } +#endif + auto &shared_buf = this->parent_->get_shared_buffer_ref(); this->prepare_first_message_buffer(shared_buf, payload_size); size_t write_start = shared_buf.size(); shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; - msg.encode(buffer); + encode_fn(msg, buffer); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } +// Encodes a message to the buffer and returns the total number of bytes used, +// including header and footer overhead. Returns 0 if the message doesn't fit. +uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + auto *proto_msg = static_cast<const ProtoMessage *>(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + return 1; + } +#endif + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); + + // Calculate total size with padding for buffer allocation + size_t total_calculated_size = calculated_size + header_padding + footer_size; + + // Check if it fits + if (total_calculated_size > remaining_size) + return 0; // Doesn't fit + + std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); + + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; + } else { + // Batch message second or later + // Add padding for previous message footer + this message header + size_t current_size = shared_buf.size(); + shared_buf.reserve(current_size + total_calculated_size); + shared_buf.resize(current_size + footer_size + header_padding); + } + + // Pre-resize buffer to include payload, then encode through raw pointer + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + calculated_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + encode_fn(msg, buffer); + + // Return total size (header + payload + footer) + return static_cast<uint16_t>(header_padding + calculated_size + footer_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -2292,17 +2292,17 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(resp, conn, remaining_size); } uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); + return encode_message_to_buffer(req, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -2321,7 +2321,7 @@ void APIConnection::process_state_subscriptions_() { resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.once = it.once; - if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { + if (this->send_message(resp)) { this->state_subs_at_++; } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2c66a194a6b..54b6db68000 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,7 +129,7 @@ class APIConnection final : public APIServerConnectionBase { void send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); + this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; @@ -153,7 +153,7 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req, GetTimeRequest::MESSAGE_TYPE); + this->send_message(req); } #endif @@ -263,7 +263,19 @@ class APIConnection final : public APIServerConnectionBase { void on_fatal_error() override; void on_no_setup_connection() override; - bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; + + // Function pointer type for type-erased message encoding + using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + // Function pointer type for type-erased size calculation + using CalculateSizeFn = uint32_t (*)(const void *); + + template<typename T> bool send_message(const T &msg) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg); + } else { + return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg); + } + } void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -318,28 +330,68 @@ class APIConnection final : public APIServerConnectionBase { void process_state_subscriptions_(); #endif - // Non-template helper to encode any ProtoMessage - static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size); - - // Helper to fill entity state base and encode message - static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { - msg.key = entity->get_object_id_hash(); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size); + // Size thunk — converts void* back to concrete type for direct calculate_size() call + template<typename T> static uint32_t calc_size(const void *msg) { + return static_cast<const T *>(msg)->calculate_size(); } - // Helper to fill entity info base and encode message - static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) + static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} - // Wrapper for entity types that have a device_class field + // Non-template buffer management for send_message + bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); + + // Non-template buffer management for batch encoding + static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); + + // Thin template wrapper — computes size, delegates buffer work to non-template helper + template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size); + } else { + return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size); + } + } + + // Non-template core — fills state fields and encodes + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); + } + + // Non-template core — fills info fields, allocates buffers, and encodes + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size); + } + + // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, - StringRef &device_class_field, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size); + StringRef &device_class_field, CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template<typename T> + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, + StringRef &device_class_field, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>, + &proto_encode_msg<T>, conn, remaining_size); + } #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 9e74d5ddc7b..d8703aa416e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -37,20 +37,24 @@ void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, this->server_info); buffer.encode_string(4, this->name); } -void HelloResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->api_version_major); - size.add_uint32(1, this->api_version_minor); - size.add_length(1, this->server_info.size()); - size.add_length(1, this->name.size()); +uint32_t HelloResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->api_version_major); + size += ProtoSize::calc_uint32(1, this->api_version_minor); + size += ProtoSize::calc_length(1, this->server_info.size()); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } -void AreaInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->area_id); - size.add_length(1, this->name.size()); +uint32_t AreaInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->area_id); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #endif #ifdef USE_DEVICES @@ -59,10 +63,12 @@ void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } -void DeviceInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->device_id); - size.add_length(1, this->name.size()); - size.add_uint32(1, this->area_id); +uint32_t DeviceInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->device_id); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, this->area_id); + return size; } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { @@ -120,60 +126,62 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(24, this->zwave_home_id); #endif } -void DeviceInfoResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_length(1, this->mac_address.size()); - size.add_length(1, this->esphome_version.size()); - size.add_length(1, this->compilation_time.size()); - size.add_length(1, this->model.size()); +uint32_t DeviceInfoResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->mac_address.size()); + size += ProtoSize::calc_length(1, this->esphome_version.size()); + size += ProtoSize::calc_length(1, this->compilation_time.size()); + size += ProtoSize::calc_length(1, this->model.size()); #ifdef USE_DEEP_SLEEP - size.add_bool(1, this->has_deep_sleep); + size += ProtoSize::calc_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_name.size()); + size += ProtoSize::calc_length(1, this->project_name.size()); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_version.size()); + size += ProtoSize::calc_length(1, this->project_version.size()); #endif #ifdef USE_WEBSERVER - size.add_uint32(1, this->webserver_port); + size += ProtoSize::calc_uint32(1, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_uint32(1, this->bluetooth_proxy_feature_flags); + size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size.add_length(1, this->manufacturer.size()); - size.add_length(1, this->friendly_name.size()); + size += ProtoSize::calc_length(1, this->manufacturer.size()); + size += ProtoSize::calc_length(1, this->friendly_name.size()); #ifdef USE_VOICE_ASSISTANT - size.add_uint32(2, this->voice_assistant_feature_flags); + size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size.add_length(2, this->suggested_area.size()); + size += ProtoSize::calc_length(2, this->suggested_area.size()); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_length(2, this->bluetooth_mac_address.size()); + size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size()); #endif #ifdef USE_API_NOISE - size.add_bool(2, this->api_encryption_supported); + size += ProtoSize::calc_bool(2, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS - size.add_message_object(2, this->area); + size += ProtoSize::calc_message(2, this->area.calculate_size()); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_proxy_feature_flags); + size += ProtoSize::calc_uint32(2, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_home_id); + size += ProtoSize::calc_uint32(2, this->zwave_home_id); #endif + return size; } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { @@ -191,20 +199,22 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->is_status_binary_sensor); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -214,13 +224,15 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t BinarySensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_COVER @@ -242,23 +254,25 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_tilt); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCoverResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_tilt); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -269,14 +283,16 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void CoverStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_float(1, this->tilt); - size.add_uint32(1, static_cast<uint32_t>(this->current_operation)); +uint32_t CoverStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_float(1, this->tilt); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -337,27 +353,29 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_oscillation); - size.add_bool(1, this->supports_speed); - size.add_bool(1, this->supports_direction); - size.add_int32(1, this->supported_speed_count); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesFanResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_oscillation); + size += ProtoSize::calc_bool(1, this->supports_speed); + size += ProtoSize::calc_bool(1, this->supports_direction); + size += ProtoSize::calc_int32(1, this->supported_speed_count); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -370,16 +388,18 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void FanStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->oscillating); - size.add_uint32(1, static_cast<uint32_t>(this->direction)); - size.add_int32(1, this->speed_level); - size.add_length(1, this->preset_mode.size()); +uint32_t FanStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->oscillating); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->direction)); + size += ProtoSize::calc_int32(1, this->speed_level); + size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -464,30 +484,32 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLightResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_float(1, this->min_mireds); - size.add_float(1, this->max_mireds); + size += ProtoSize::calc_float(1, this->min_mireds); + size += ProtoSize::calc_float(1, this->max_mireds); if (!this->effects->empty()) { for (const char *it : *this->effects) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -507,23 +529,25 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void LightStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_float(1, this->brightness); - size.add_uint32(1, static_cast<uint32_t>(this->color_mode)); - size.add_float(1, this->color_brightness); - size.add_float(1, this->red); - size.add_float(1, this->green); - size.add_float(1, this->blue); - size.add_float(1, this->white); - size.add_float(1, this->color_temperature); - size.add_float(1, this->cold_white); - size.add_float(1, this->warm_white); - size.add_length(1, this->effect.size()); +uint32_t LightStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_float(1, this->brightness); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->color_mode)); + size += ProtoSize::calc_float(1, this->color_brightness); + size += ProtoSize::calc_float(1, this->red); + size += ProtoSize::calc_float(1, this->green); + size += ProtoSize::calc_float(1, this->blue); + size += ProtoSize::calc_float(1, this->white); + size += ProtoSize::calc_float(1, this->color_temperature); + size += ProtoSize::calc_float(1, this->cold_white); + size += ProtoSize::calc_float(1, this->warm_white); + size += ProtoSize::calc_length(1, this->effect.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -653,23 +677,25 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_length(1, this->unit_of_measurement.size()); - size.add_int32(1, this->accuracy_decimals); - size.add_bool(1, this->force_update); - size.add_length(1, this->device_class.size()); - size.add_uint32(1, static_cast<uint32_t>(this->state_class)); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_int32(1, this->accuracy_decimals); + size += ProtoSize::calc_bool(1, this->force_update); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state_class)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -679,13 +705,15 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t SensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_SWITCH @@ -704,20 +732,22 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSwitchResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -726,12 +756,14 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SwitchStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SwitchStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -774,19 +806,21 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -796,13 +830,15 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextSensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextSensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -822,9 +858,11 @@ void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } -void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->level)); - size.add_length(1, this->message_len_); +uint32_t SubscribeLogsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->level)); + size += ProtoSize::calc_length(1, this->message_len_); + return size; } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -840,16 +878,22 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } -void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { - size.add_length(1, this->key.size()); - size.add_length(1, this->value.size()); +uint32_t HomeassistantServiceMap::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->key.size()); + size += ProtoSize::calc_length(1, this->value.size()); + return size; } void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); @@ -873,21 +917,35 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(8, this->response_template); #endif } -void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { - size.add_length(1, this->service.size()); - size.add_repeated_message(1, this->data); - size.add_repeated_message(1, this->data_template); - size.add_repeated_message(1, this->variables); - size.add_bool(1, this->is_event); +uint32_t HomeassistantActionRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->service.size()); + if (!this->data.empty()) { + for (const auto &it : this->data) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->data_template.empty()) { + for (const auto &it : this->data_template) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->variables.empty()) { + for (const auto &it : this->variables) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_bool(1, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - size.add_uint32(1, this->call_id); + size += ProtoSize::calc_uint32(1, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_bool(1, this->wants_response); + size += ProtoSize::calc_bool(1, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_length(1, this->response_template.size()); + size += ProtoSize::calc_length(1, this->response_template.size()); #endif + return size; } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -929,10 +987,12 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } -void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->entity_id.size()); - size.add_length(1, this->attribute.size()); - size.add_bool(1, this->once); +uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->entity_id.size()); + size += ProtoSize::calc_length(1, this->attribute.size()); + size += ProtoSize::calc_bool(1, this->once); + return size; } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -1034,9 +1094,11 @@ void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); } -void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_uint32(1, static_cast<uint32_t>(this->type)); +uint32_t ListEntitiesServicesArgument::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + return size; } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); @@ -1046,11 +1108,17 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); } -void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_fixed32(1, this->key); - size.add_repeated_message(1, this->args); - size.add_uint32(1, static_cast<uint32_t>(this->supports_response)); +uint32_t ListEntitiesServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_fixed32(1, this->key); + if (!this->args.empty()) { + for (const auto &it : this->args) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->supports_response)); + return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1165,13 +1233,15 @@ void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(4, this->response_data, this->response_data_len); #endif } -void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->call_id); - size.add_bool(1, this->success); - size.add_length(1, this->error_message.size()); +uint32_t ExecuteServiceResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->call_id); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_length(1, this->error_message.size()); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - size.add_length(1, this->response_data_len); + size += ProtoSize::calc_length(1, this->response_data_len); #endif + return size; } #endif #ifdef USE_CAMERA @@ -1188,18 +1258,20 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCameraResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1209,13 +1281,15 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void CameraImageResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->data_len_); - size.add_bool(1, this->done); +uint32_t CameraImageResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->data_len_); + size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1275,60 +1349,62 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(27, this->feature_flags); } -void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_current_temperature); - size.add_bool(1, this->supports_two_point_target_temperature); +uint32_t ListEntitiesClimateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_current_temperature); + size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_float(1, this->visual_min_temperature); - size.add_float(1, this->visual_max_temperature); - size.add_float(1, this->visual_target_temperature_step); - size.add_bool(1, this->supports_action); + size += ProtoSize::calc_float(1, this->visual_min_temperature); + size += ProtoSize::calc_float(1, this->visual_max_temperature); + size += ProtoSize::calc_float(1, this->visual_target_temperature_step); + size += ProtoSize::calc_bool(1, this->supports_action); if (!this->supported_fan_modes->empty()) { for (const auto &it : *this->supported_fan_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } if (!this->supported_swing_modes->empty()) { for (const auto &it : *this->supported_swing_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { for (const auto &it : *this->supported_presets) { - size.add_uint32_force(2, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(2, static_cast<uint32_t>(it)); } } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { - size.add_length_force(2, strlen(it)); + size += ProtoSize::calc_length_force(2, strlen(it)); } } - size.add_bool(2, this->disabled_by_default); + size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(2, this->icon.size()); + size += ProtoSize::calc_length(2, this->icon.size()); #endif - size.add_uint32(2, static_cast<uint32_t>(this->entity_category)); - size.add_float(2, this->visual_current_temperature_step); - size.add_bool(2, this->supports_current_humidity); - size.add_bool(2, this->supports_target_humidity); - size.add_float(2, this->visual_min_humidity); - size.add_float(2, this->visual_max_humidity); + size += ProtoSize::calc_uint32(2, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_float(2, this->visual_current_temperature_step); + size += ProtoSize::calc_bool(2, this->supports_current_humidity); + size += ProtoSize::calc_bool(2, this->supports_target_humidity); + size += ProtoSize::calc_float(2, this->visual_min_humidity); + size += ProtoSize::calc_float(2, this->visual_max_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif - size.add_uint32(2, this->feature_flags); + size += ProtoSize::calc_uint32(2, this->feature_flags); + return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1349,24 +1425,26 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ClimateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); - size.add_uint32(1, static_cast<uint32_t>(this->action)); - size.add_uint32(1, static_cast<uint32_t>(this->fan_mode)); - size.add_uint32(1, static_cast<uint32_t>(this->swing_mode)); - size.add_length(1, this->custom_fan_mode.size()); - size.add_uint32(1, static_cast<uint32_t>(this->preset)); - size.add_length(1, this->custom_preset.size()); - size.add_float(1, this->current_humidity); - size.add_float(1, this->target_humidity); +uint32_t ClimateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->action)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->fan_mode)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->swing_mode)); + size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->preset)); + size += ProtoSize::calc_length(1, this->custom_preset.size()); + size += ProtoSize::calc_float(1, this->current_humidity); + size += ProtoSize::calc_float(1, this->target_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1481,27 +1559,29 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(12, this->supported_features); } -void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_float(1, this->min_temperature); - size.add_float(1, this->max_temperature); - size.add_float(1, this->target_temperature_step); + size += ProtoSize::calc_float(1, this->min_temperature); + size += ProtoSize::calc_float(1, this->max_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_step); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast<uint32_t>(it)); + size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); } } - size.add_uint32(1, this->supported_features); + size += ProtoSize::calc_uint32(1, this->supported_features); + return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1515,17 +1595,19 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_float(8, this->target_temperature_high); } -void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); +uint32_t WaterHeaterStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->state); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, this->state); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + return size; } bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1588,24 +1670,26 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesNumberResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_float(1, this->min_value); - size.add_float(1, this->max_value); - size.add_float(1, this->step); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->unit_of_measurement.size()); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_float(1, this->min_value); + size += ProtoSize::calc_float(1, this->max_value); + size += ProtoSize::calc_float(1, this->step); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1615,13 +1699,15 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void NumberStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t NumberStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1666,23 +1752,25 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSelectResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif if (!this->options->empty()) { for (const char *it : *this->options) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1692,13 +1780,15 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void SelectStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t SelectStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1753,25 +1843,27 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSirenResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { for (const char *it : *this->tones) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->supports_duration); - size.add_bool(1, this->supports_volume); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_duration); + size += ProtoSize::calc_bool(1, this->supports_volume); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1780,12 +1872,14 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void SirenStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SirenStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1860,22 +1954,24 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLockResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_open); - size.add_bool(1, this->requires_code); - size.add_length(1, this->code_format.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_open); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_length(1, this->code_format.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -1884,12 +1980,14 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void LockStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); +uint32_t LockStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1946,19 +2044,21 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesButtonResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1991,12 +2091,14 @@ void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, static_cast<uint32_t>(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } -void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { - size.add_length(1, this->format.size()); - size.add_uint32(1, this->sample_rate); - size.add_uint32(1, this->num_channels); - size.add_uint32(1, static_cast<uint32_t>(this->purpose)); - size.add_uint32(1, this->sample_bytes); +uint32_t MediaPlayerSupportedFormat::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->format.size()); + size += ProtoSize::calc_uint32(1, this->sample_rate); + size += ProtoSize::calc_uint32(1, this->num_channels); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->purpose)); + size += ProtoSize::calc_uint32(1, this->sample_bytes); + return size; } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); @@ -2016,21 +2118,27 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(11, this->feature_flags); } -void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_bool(1, this->supports_pause); - size.add_repeated_message(1, this->supported_formats); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_pause); + if (!this->supported_formats.empty()) { + for (const auto &it : this->supported_formats) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->feature_flags); + size += ProtoSize::calc_uint32(1, this->feature_flags); + return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2041,14 +2149,16 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(5, this->device_id); #endif } -void MediaPlayerStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); - size.add_float(1, this->volume); - size.add_bool(1, this->muted); +uint32_t MediaPlayerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += ProtoSize::calc_float(1, this->volume); + size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2122,21 +2232,25 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->address_type); buffer.encode_bytes(4, this->data, this->data_len); } -void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_sint32(1, this->rssi); - size.add_uint32(1, this->address_type); - size.add_length(1, this->data_len); +uint32_t BluetoothLERawAdvertisement::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint32(1, this->address_type); + size += ProtoSize::calc_length(1, this->data_len); + return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { buffer.encode_message(1, this->advertisements[i]); } } -void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { +uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { + uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size.add_message_object_force(1, this->advertisements[i]); + size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); } + return size; } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2163,11 +2277,13 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } -void BluetoothDeviceConnectionResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->connected); - size.add_uint32(1, this->mtu); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->connected); + size += ProtoSize::calc_uint32(1, this->mtu); + size += ProtoSize::calc_int32(1, this->error); + return size; } bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2187,13 +2303,15 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->short_uuid); } -void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTDescriptor::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2207,15 +2325,21 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(5, this->short_uuid); } -void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTCharacteristic::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->properties); - size.add_repeated_message(1, this->descriptors); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->properties); + if (!this->descriptors.empty()) { + for (const auto &it : this->descriptors) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { @@ -2228,14 +2352,20 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(4, this->short_uuid); } -void BluetoothGATTService::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTService::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_repeated_message(1, this->characteristics); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + if (!this->characteristics.empty()) { + for (const auto &it : this->characteristics) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); @@ -2243,14 +2373,24 @@ void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(2, it); } } -void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_repeated_message(1, this->services); +uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + if (!this->services.empty()) { + for (const auto &it : this->services) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + return size; } void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } -void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } +uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + return size; +} bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -2269,10 +2409,12 @@ void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTReadResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTReadResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2361,10 +2503,12 @@ void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); @@ -2375,80 +2519,96 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { } } } -void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->free); - size.add_uint32(1, this->limit); +uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->free); + size += ProtoSize::calc_uint32(1, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - size.add_uint64_force(1, it); + size += ProtoSize::calc_uint64_force(1, it); } } + return size; } void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } -void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_int32(1, this->error); +uint32_t BluetoothGATTErrorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTWriteResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTNotifyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } -void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->paired); - size.add_int32(1, this->error); +uint32_t BluetoothDevicePairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->paired); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); buffer.encode_uint32(3, static_cast<uint32_t>(this->configured_mode)); } -void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->state)); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); - size.add_uint32(1, static_cast<uint32_t>(this->configured_mode)); +uint32_t BluetoothScannerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->configured_mode)); + return size; } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2480,10 +2640,12 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); } -void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->noise_suppression_level); - size.add_uint32(1, this->auto_gain); - size.add_float(1, this->volume_multiplier); +uint32_t VoiceAssistantAudioSettings::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->noise_suppression_level); + size += ProtoSize::calc_uint32(1, this->auto_gain); + size += ProtoSize::calc_float(1, this->volume_multiplier); + return size; } void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); @@ -2492,12 +2654,14 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_message(4, this->audio_settings, false); buffer.encode_string(5, this->wake_word_phrase); } -void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { - size.add_bool(1, this->start); - size.add_length(1, this->conversation_id.size()); - size.add_uint32(1, this->flags); - size.add_message_object(1, this->audio_settings); - size.add_length(1, this->wake_word_phrase.size()); +uint32_t VoiceAssistantRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->start); + size += ProtoSize::calc_length(1, this->conversation_id.size()); + size += ProtoSize::calc_uint32(1, this->flags); + size += ProtoSize::calc_message(1, this->audio_settings.calculate_size()); + size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); + return size; } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2574,9 +2738,11 @@ void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } -void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { - size.add_length(1, this->data_len); - size.add_bool(1, this->end); +uint32_t VoiceAssistantAudio::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_bool(1, this->end); + return size; } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2642,7 +2808,11 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } -void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +uint32_t VoiceAssistantAnnounceFinished::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); @@ -2650,14 +2820,16 @@ void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(3, it, true); } } -void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { - size.add_length(1, this->id.size()); - size.add_length(1, this->wake_word.size()); +uint32_t VoiceAssistantWakeWord::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->id.size()); + size += ProtoSize::calc_length(1, this->wake_word.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - size.add_length_force(1, it.size()); + size += ProtoSize::calc_length_force(1, it.size()); } } + return size; } bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2719,14 +2891,20 @@ void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const } buffer.encode_uint32(3, this->max_active_wake_words); } -void VoiceAssistantConfigurationResponse::calculate_size(ProtoSize &size) const { - size.add_repeated_message(1, this->available_wake_words); - if (!this->active_wake_words->empty()) { - for (const auto &it : *this->active_wake_words) { - size.add_length_force(1, it.size()); +uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { + uint32_t size = 0; + if (!this->available_wake_words.empty()) { + for (const auto &it : this->available_wake_words) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size.add_uint32(1, this->max_active_wake_words); + if (!this->active_wake_words->empty()) { + for (const auto &it : *this->active_wake_words) { + size += ProtoSize::calc_length_force(1, it.size()); + } + } + size += ProtoSize::calc_uint32(1, this->max_active_wake_words); + return size; } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -2756,21 +2934,23 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_uint32(1, this->supported_features); - size.add_bool(1, this->requires_code); - size.add_bool(1, this->requires_code_to_arm); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->supported_features); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_bool(1, this->requires_code_to_arm); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2779,12 +2959,14 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void AlarmControlPanelStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast<uint32_t>(this->state)); +uint32_t AlarmControlPanelStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2841,22 +3023,24 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_uint32(1, this->min_length); - size.add_uint32(1, this->max_length); - size.add_length(1, this->pattern.size()); - size.add_uint32(1, static_cast<uint32_t>(this->mode)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->min_length); + size += ProtoSize::calc_uint32(1, this->max_length); + size += ProtoSize::calc_length(1, this->pattern.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2866,13 +3050,15 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void TextStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2922,18 +3108,20 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -2945,15 +3133,17 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void DateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->year); - size.add_uint32(1, this->month); - size.add_uint32(1, this->day); +uint32_t DateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->year); + size += ProtoSize::calc_uint32(1, this->month); + size += ProtoSize::calc_uint32(1, this->day); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3001,18 +3191,20 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3024,15 +3216,17 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void TimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->hour); - size.add_uint32(1, this->minute); - size.add_uint32(1, this->second); +uint32_t TimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->hour); + size += ProtoSize::calc_uint32(1, this->minute); + size += ProtoSize::calc_uint32(1, this->second); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3084,24 +3278,26 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesEventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3110,12 +3306,14 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(3, this->device_id); #endif } -void EventResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->event_type.size()); +uint32_t EventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_VALVE @@ -3136,22 +3334,24 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesValveResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3161,13 +3361,15 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void ValveStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_uint32(1, static_cast<uint32_t>(this->current_operation)); +uint32_t ValveStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3215,18 +3417,20 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3236,13 +3440,15 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(4, this->device_id); #endif } -void DateTimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_fixed32(1, this->epoch_seconds); +uint32_t DateTimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3285,19 +3491,21 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesUpdateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_fixed32(1, this->key); @@ -3314,20 +3522,22 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void UpdateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_bool(1, this->in_progress); - size.add_bool(1, this->has_progress); - size.add_float(1, this->progress); - size.add_length(1, this->current_version.size()); - size.add_length(1, this->latest_version.size()); - size.add_length(1, this->title.size()); - size.add_length(1, this->release_summary.size()); - size.add_length(1, this->release_url.size()); +uint32_t UpdateStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_bool(1, this->in_progress); + size += ProtoSize::calc_bool(1, this->has_progress); + size += ProtoSize::calc_float(1, this->progress); + size += ProtoSize::calc_length(1, this->current_version.size()); + size += ProtoSize::calc_length(1, this->latest_version.size()); + size += ProtoSize::calc_length(1, this->title.size()); + size += ProtoSize::calc_length(1, this->release_summary.size()); + size += ProtoSize::calc_length(1, this->release_url.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -3369,7 +3579,11 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu return true; } void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } -void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } +uint32_t ZWaveProxyFrame::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + return size; +} bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -3396,9 +3610,11 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast<uint32_t>(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } -void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast<uint32_t>(this->type)); - size.add_length(1, this->data_len); +uint32_t ZWaveProxyRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += ProtoSize::calc_length(1, this->data_len); + return size; } #endif #ifdef USE_INFRARED @@ -3416,19 +3632,21 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { #endif buffer.encode_uint32(8, this->capabilities); } -void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesInfraredResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += ProtoSize::calc_fixed32(1, this->key); + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->capabilities); + return size; } #endif #ifdef USE_IR_RF @@ -3482,16 +3700,18 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { buffer.encode_sint32(3, it, true); } } -void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const { +uint32_t InfraredRFReceiveEvent::calculate_size() const { + uint32_t size = 0; #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_fixed32(1, this->key); + size += ProtoSize::calc_fixed32(1, this->key); if (!this->timings->empty()) { for (const auto &it : *this->timings) { - size.add_sint32_force(1, it); + size += ProtoSize::calc_sint32_force(1, it); } } + return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a97f6c0a762..89cb1158f33 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,8 +388,8 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -453,8 +453,8 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -468,8 +468,8 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -533,8 +533,8 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -564,8 +564,8 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -581,8 +581,8 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -603,8 +603,8 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -621,8 +621,8 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -663,8 +663,8 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector<const char *> *supported_preset_modes{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -683,8 +683,8 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -730,8 +730,8 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector<const char *> *effects{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -757,8 +757,8 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -821,8 +821,8 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -838,8 +838,8 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -857,8 +857,8 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -873,8 +873,8 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -907,8 +907,8 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -924,8 +924,8 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -963,8 +963,8 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -996,8 +996,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1010,8 +1010,8 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1039,8 +1039,8 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1083,8 +1083,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1174,8 +1174,8 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1193,8 +1193,8 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector<ListEntitiesServicesArgument> args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1263,8 +1263,8 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1280,8 +1280,8 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1302,8 +1302,8 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1353,8 +1353,8 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1381,8 +1381,8 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1439,8 +1439,8 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1460,8 +1460,8 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1504,8 +1504,8 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1521,8 +1521,8 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1555,8 +1555,8 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_select_response"; } #endif const FixedVector<const char *> *options{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1572,8 +1572,8 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1609,8 +1609,8 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector<const char *> *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1625,8 +1625,8 @@ class SirenStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1670,8 +1670,8 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1686,8 +1686,8 @@ class LockStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1723,8 +1723,8 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_button_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1755,8 +1755,8 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1773,8 +1773,8 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector<MediaPlayerSupportedFormat> supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1791,8 +1791,8 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1847,8 +1847,8 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1864,8 +1864,8 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1901,8 +1901,8 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1929,8 +1929,8 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array<uint64_t, 2> uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1944,8 +1944,8 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector<BluetoothGATTDescriptor> descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1958,8 +1958,8 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector<BluetoothGATTCharacteristic> characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1975,8 +1975,8 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector<BluetoothGATTService> services{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1991,8 +1991,8 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2030,8 +2030,8 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2125,8 +2125,8 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2143,8 +2143,8 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2161,8 +2161,8 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2178,8 +2178,8 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2195,8 +2195,8 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2213,8 +2213,8 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2231,8 +2231,8 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2249,8 +2249,8 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2267,8 +2267,8 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2313,8 +2313,8 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2333,8 +2333,8 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2395,8 +2395,8 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2453,8 +2453,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2466,8 +2466,8 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector<std::string> trained_languages{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2516,8 +2516,8 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector<VoiceAssistantWakeWord> available_wake_words{}; const std::vector<std::string> *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2551,8 +2551,8 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2567,8 +2567,8 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2606,8 +2606,8 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2623,8 +2623,8 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2657,8 +2657,8 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2676,8 +2676,8 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2711,8 +2711,8 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2730,8 +2730,8 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2767,8 +2767,8 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector<const char *> *event_types{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2783,8 +2783,8 @@ class EventResponse final : public StateResponseProtoMessage { const char *message_name() const override { return "event_response"; } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2804,8 +2804,8 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2821,8 +2821,8 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2856,8 +2856,8 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2873,8 +2873,8 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2907,8 +2907,8 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_update_response"; } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2931,8 +2931,8 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2966,8 +2966,8 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2985,8 +2985,8 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3005,8 +3005,8 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_infrared_response"; } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3052,8 +3052,8 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector<int32_t> *timings{}; - void encode(ProtoWriteBuffer &buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1441507406d..e70b97196b4 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService { public: #endif - bool send_message(const ProtoMessage &msg, uint8_t message_type) { -#ifdef HAS_PROTO_MESSAGE_DUMP - DumpBuffer dump_buf; - this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); -#endif - return this->send_message_impl(msg, message_type); - } - virtual void on_hello_request(const HelloRequest &value){}; virtual void on_disconnect_request(){}; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 0352d7347bb..06816fe3e05 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) { #endif #ifdef USE_ZWAVE_PROXY -void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { +void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { // We could add code to manage a second subscription type, but, since this message type is // very infrequent and small, we simply send it to all clients for (auto &c : this->clients_) - c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + c->send_message(msg); } #endif @@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; - c->send_message(req, DisconnectRequest::MESSAGE_TYPE); + c->send_message(req); } }); } @@ -631,7 +631,7 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { DisconnectRequest req; - if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { + if (!c->send_message(req)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 6eff2005f8a..e6c10d15953 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -179,7 +179,7 @@ class APIServer : public Component, void on_update(update::UpdateEntity *obj) override; #endif #ifdef USE_ZWAVE_PROXY - void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); + void on_zwave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_IR_RF void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fe43a47c3b7..0a94c1699b1 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); + return this->client_->send_message(resp); } #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 750fff08102..702208d9de6 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -364,7 +364,11 @@ class ProtoWriteBuffer { /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); /// Encode a nested message field (force=true for repeated, false for singular) - void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); + /// Templated so concrete message type is preserved for direct encode/calculate_size calls. + template<typename T> void encode_message(uint32_t field_id, const T &value, bool force = true); + // Non-template core for encode_message — all buffer work happens here + void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: @@ -452,20 +456,20 @@ class DumpBuffer { class ProtoMessage { public: - // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer &buffer) const {} - // Default implementation for messages with no fields - virtual void calculate_size(ProtoSize &size) const {} - // Convenience: calculate and return size directly (defined after ProtoSize) - uint32_t calculated_size() const; + // Non-virtual defaults for messages with no fields. + // Concrete message classes hide these with their own implementations. + // All call sites use templates to preserve the concrete type, so virtual + // dispatch is not needed. This eliminates per-message vtable entries for + // encode/calculate_size, saving ~1.3 KB of flash across all message types. + void encode(ProtoWriteBuffer &buffer) const {} + uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif protected: - // Non-virtual: messages are never deleted polymorphically. - // Protected prevents accidental `delete base_ptr` (compile error). + // Non-virtual destructor is protected to prevent polymorphic deletion. ~ProtoMessage() = default; }; @@ -494,32 +498,7 @@ class ProtoDecodableMessage : public ProtoMessage { }; class ProtoSize { - private: - uint32_t total_size_ = 0; - public: - /** - * @brief ProtoSize class for Protocol Buffer serialization size calculation - * - * This class provides methods to calculate the exact byte counts needed - * for encoding various Protocol Buffer field types. The class now uses an - * object-based approach to reduce parameter passing overhead while keeping - * varint calculation methods static for external use. - * - * Implements Protocol Buffer encoding size calculation according to: - * https://protobuf.dev/programming-guides/encoding/ - * - * Key features: - * - Object-based approach reduces flash usage by eliminating parameter passing - * - Early-return optimization for zero/default values - * - Static varint methods for external callers - * - Specialized handling for different field types according to protobuf spec - */ - - ProtoSize() = default; - - uint32_t get_size() const { return total_size_; } - /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @@ -616,320 +595,77 @@ class ProtoSize { return varint(tag); } - /** - * @brief Common parameters for all add_*_field methods - * - * All add_*_field methods follow these common patterns: - * * @param field_id_size Pre-calculated size of the field ID in bytes - * @param value The value to calculate size for (type varies) - * @param force Whether to calculate size even if the value is default/zero/empty - * - * Each method follows this implementation pattern: - * 1. Skip calculation if value is default (0, false, empty) and not forced - * 2. Calculate the size based on the field's encoding rules - * 3. Add the field_id_size + calculated value size to total_size - */ - - /** - * @brief Calculates and adds the size of an int32 field to the total message size - */ - inline void add_int32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_int32_force(field_id_size, value); - } + // Static methods that RETURN size contribution (no ProtoSize object needed). + // Used by generated calculate_size() methods to accumulate into a plain uint32_t register. + static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0; } - - /** - * @brief Calculates and adds the size of an int32 field to the total message size (force version) - */ - inline void add_int32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when forced - // Negative values are encoded as 10-byte varints in protobuf - total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))); + static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))); } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size - */ - inline void add_uint32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - add_uint32_force(field_id_size, value); - } + static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size (force version) - */ - inline void add_uint32_force(uint32_t field_id_size, uint32_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size - */ - inline void add_bool(uint32_t field_id_size, bool value) { - if (value) { - // Boolean fields always use 1 byte when true - total_size_ += field_id_size + 1; - } + static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } + static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } + static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + return value != 0.0f ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size (force version) - */ - inline void add_bool_force(uint32_t field_id_size, bool value) { - // Always calculate size when force is true - // Boolean fields always use 1 byte - total_size_ += field_id_size + 1; + static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a float field to the total message size - */ - inline void add_float(uint32_t field_id_size, float value) { - if (value != 0.0f) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + 4 : 0; } - - // 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 - */ - inline void add_fixed32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + varint(encode_zigzag32(value)) : 0; } - - // 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 - */ - inline void add_sfixed32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + varint(encode_zigzag32(value)); } - - // 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 a sint32 field to the total message size - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_sint32_force(field_id_size, value); - } + static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size (force version) - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when force is true - // ZigZag encoding for sint32 - total_size_ += field_id_size + varint(encode_zigzag32(value)); + static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size - */ - inline void add_int64(uint32_t field_id_size, int64_t value) { - if (value != 0) { - add_int64_force(field_id_size, value); - } + static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size (force version) - */ - inline void add_int64_force(uint32_t field_id_size, int64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size - */ - inline void add_uint64(uint32_t field_id_size, uint64_t value) { - if (value != 0) { - add_uint64_force(field_id_size, value); - } + static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0; } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size (force version) - */ - inline void add_uint64_force(uint32_t field_id_size, uint64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) { + return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); } - - // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed - // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size - */ - inline void add_length(uint32_t field_id_size, size_t len) { - if (len != 0) { - add_length_force(field_id_size, len); - } + static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(encode_zigzag64(value)) : 0; } - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated - * field version) - */ - inline void add_length_force(uint32_t field_id_size, size_t len) { - // Always calculate size when force is true - // Field ID + length varint + data bytes - total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); + static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(encode_zigzag64(value)); } - - /** - * @brief Adds a pre-calculated size directly to the total - * - * This is used when we can calculate the total size by multiplying the number - * of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.) - * - * @param size The pre-calculated total size to add - */ - inline void add_precalculated_size(uint32_t size) { total_size_ += size; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This helper function directly updates the total_size reference if the nested size - * is greater than zero. - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) { - if (nested_size != 0) { - add_message_field_force(field_id_size, nested_size); - } + static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { - // Always calculate size when force is true - // Field ID + length varint + nested message content - total_size_ += field_id_size + varint(nested_size) + nested_size; + static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This version takes a ProtoMessage object, calculates its size internally, - * and updates the total_size reference. This eliminates the need for a temporary variable - * at the call site. - * - * @param message The nested message object - */ - inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field(field_id_size, nested_size); + static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) { + return nested_size ? field_id_size + varint(nested_size) + nested_size : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param message The nested message object - */ - inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field_force(field_id_size, nested_size); - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size - * - * This helper processes a vector of message objects, calculating the size for each message - * and adding it to the total size. - * - * @tparam MessageType The type of the nested messages in the vector - * @param messages Vector of message objects - */ - template<typename MessageType> - inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) { - // Skip if the vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector - * version) - * - * @tparam MessageType The type of the nested messages in the FixedVector - * @param messages FixedVector of message objects - */ - template<typename MessageType> - inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) { - // Skip if the fixed vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculate size of a packed repeated sint32 field - */ - inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) { - if (values.empty()) - return; - - size_t packed_size = 0; - for (int value : values) { - packed_size += varint(encode_zigzag32(value)); - } - - // field_id + length varint + packed data - total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size); + static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) { + return field_id_size + varint(nested_size) + nested_size; } }; // Implementation of methods that depend on ProtoSize being fully defined -inline uint32_t ProtoMessage::calculated_size() const { - ProtoSize size; - this->calculate_size(size); - return size.get_size(); -} - // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) { if (values.empty()) @@ -949,31 +685,30 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } } -// Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { - // Calculate the message size first - ProtoSize msg_size; - value.calculate_size(msg_size); - uint32_t msg_length_bytes = msg_size.get_size(); +// Encode thunk — converts void* back to concrete type for direct encode() call +template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) { + static_cast<const T *>(msg)->encode(buf); +} - // Skip empty singular messages (matches add_message_field which skips when nested_size == 0) - // Repeated messages (force=true) are always encoded since an empty item is meaningful +// Implementation of encode_message - must be after ProtoMessage is defined +template<typename T> inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { + this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>, force); +} + +// Non-template core for encode_message +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { if (msg_length_bytes == 0 && !force) return; - - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - - // Write the length varint directly through pos_ + this->encode_field_raw(field_id, 2); this->encode_varint_raw(msg_length_bytes); - - // Encode nested message - pos_ advances directly through the reference #ifdef ESPHOME_DEBUG_API uint8_t *start = this->pos_; - value.encode(*this); + encode_fn(value, *this); if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes) this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); #else - value.encode(*this); + encode_fn(value, *this); #endif } @@ -993,14 +728,6 @@ class ProtoService { virtual void on_no_setup_connection() = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - /** - * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. - * This is the implementation method - callers should use send_message() which adds logging. - * @param msg The protobuf message to send. - * @param message_type The message type identifier. - * @return True if the message was sent successfully, false otherwise. - */ - virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index b2000fbd943..21573f0184b 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -183,10 +183,7 @@ void BluetoothConnection::send_service_for_discovery_() { static constexpr size_t MAX_PACKET_SIZE = 1360; // Keep running total of actual message size - size_t current_size = 0; - api::ProtoSize size; - resp.calculate_size(size); - current_size = size.get_size(); + size_t current_size = resp.calculate_size(); while (this->send_service_ < this->service_count_) { esp_gattc_service_elem_t service_result; @@ -302,9 +299,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // end if (total_char_count > 0) // Calculate the actual size of just this service - api::ProtoSize service_sizer; - service_resp.calculate_size(service_sizer); - size_t service_size = service_sizer.get_size() + 1; // +1 for field tag + size_t service_size = service_resp.calculate_size() + 1; // +1 for field tag // Check if adding this service would exceed the limit if (current_size + service_size > MAX_PACKET_SIZE) { @@ -333,7 +328,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // Send the message with dynamically batched services - api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); + api_conn->send_message(resp); } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { @@ -422,7 +417,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -438,7 +433,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -454,7 +449,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -470,7 +465,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -483,7 +478,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index cab328e2f53..21da4ead144 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -44,7 +44,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.configured_mode = this->configured_scan_active_ ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; - this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); + this->api_connection_->send_message(resp); } void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { @@ -112,7 +112,7 @@ void BluetoothProxy::flush_pending_advertisements() { return; // Send the message - this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); + this->api_connection_->send_message(this->response_); ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); @@ -269,7 +269,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest call.success = ret == ESP_OK; call.error = ret; - this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); break; } @@ -389,7 +389,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_connections_free() { if (this->api_connection_ != nullptr) { @@ -398,7 +398,7 @@ void BluetoothProxy::send_connections_free() { } void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { - api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); + api_connection->send_message(this->connections_free_response_); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -406,7 +406,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { return; api::BluetoothGATTGetServicesDoneResponse call; call.address = address; - this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { @@ -416,7 +416,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ call.address = address; call.handle = handle; call.error = error; - this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { @@ -427,7 +427,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ call.paired = paired; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { @@ -438,7 +438,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e call.success = success; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d6cbfd4b215..51d52a8af8d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -251,8 +251,7 @@ void VoiceAssistant::loop() { } #endif - if (this->api_client_ == nullptr || - !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { + if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { ESP_LOGW(TAG, "Could not request start"); this->error_trigger_.trigger("not-connected", "Could not request start"); this->continuous_ = false; @@ -275,7 +274,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAudio msg; msg.data = this->send_buffer_; msg.data_len = read_bytes; - this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); + this->api_client_->send_message(msg); } else { if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { @@ -354,7 +353,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); break; } } @@ -612,7 +611,7 @@ void VoiceAssistant::signal_stop_() { ESP_LOGD(TAG, "Signaling stop"); api::VoiceAssistantRequest msg; msg.start = false; - this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE); + this->api_client_->send_message(msg); } void VoiceAssistant::start_playback_timeout_() { @@ -622,7 +621,7 @@ void VoiceAssistant::start_playback_timeout_() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); + this->api_client_->send_message(msg); }); } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index b0836ac0728..9e5c57814d2 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -119,7 +119,7 @@ void ZWaveProxy::process_uart_() { // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; } - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } } @@ -209,7 +209,7 @@ void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) { msg.data_len = this->home_id_.size(); if (conn != nullptr) { // Send to specific connection - conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + conn->send_message(msg); } else if (api::global_api_server != nullptr) { // We could add code to manage a second subscription type, but, since this message is // very infrequent and small, we simply send it to all clients @@ -346,7 +346,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->buffer_[0] = byte; this->outgoing_proto_msg_.data = this->buffer_.data(); this->outgoing_proto_msg_.data_len = 1; - this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); + this->api_connection_->send_message(this->outgoing_proto_msg_); } } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9c9cda4d36e..85352689e6b 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -270,18 +270,21 @@ class TypeInfo(ABC): def _get_simple_size_calculation( self, name: str, force: bool, base_method: str, value_expr: str = None ) -> str: - """Helper for simple size calculations. + """Helper for simple size calculations using static ProtoSize methods. Args: name: Field name force: Whether this is for a repeated field - base_method: Base method name (e.g., "add_int32") + base_method: Base method name (e.g., "int32") value_expr: Optional value expression (defaults to name) """ field_id_size = self.calculate_field_id_size() - method = f"{base_method}_force" if force else base_method + method = f"calc_{base_method}_force" if force else f"calc_{base_method}" + # calc_bool_force only takes field_id_size (no value needed - bool is always 1 byte) + if base_method == "bool" and force: + return f"size += ProtoSize::{method}({field_id_size});" value = value_expr or name - return f"size.{method}({field_id_size}, {value});" + return f"size += ProtoSize::{method}({field_id_size}, {value});" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -410,7 +413,7 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_double({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -434,7 +437,7 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_float({field_id_size}, {name});" + return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -457,7 +460,7 @@ class Int64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int64") + return self._get_simple_size_calculation(name, force, "int64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -477,7 +480,7 @@ class UInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint64") + return self._get_simple_size_calculation(name, force, "uint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -497,7 +500,7 @@ class Int32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int32") + return self._get_simple_size_calculation(name, force, "int32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -518,7 +521,7 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -542,7 +545,7 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_fixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -563,7 +566,7 @@ class BoolType(TypeInfo): return f"out.append(YESNO({name}));" def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_bool") + return self._get_simple_size_calculation(name, force, "bool") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte @@ -647,18 +650,18 @@ class StringType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # For SOURCE_CLIENT only messages, use the string field directly if not self._needs_encode: - return self._get_simple_size_calculation(name, force, "add_length") + return self._get_simple_size_calculation(name, force, "length") # Check if this is being called from a repeated field context # In that case, 'name' will be 'it' and we need to use the repeated version if name == "it": - # For repeated fields, we need to use add_length_force which includes field ID + # For repeated fields, we need to use length_force which includes field ID field_id_size = self.calculate_field_id_size() - return f"size.add_length_force({field_id_size}, it.size());" + return f"size += ProtoSize::calc_length_force({field_id_size}, it.size());" # For messages that need encoding, use the StringRef size field_id_size = self.calculate_field_id_size() - return f"size.add_length({field_id_size}, this->{self.field_name}_ref_.size());" + return f"size += ProtoSize::calc_length({field_id_size}, this->{self.field_name}_ref_.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -721,7 +724,9 @@ class MessageType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_message_object") + field_id_size = self.calculate_field_id_size() + method = "calc_message_force" if force else "calc_message" + return f"size += ProtoSize::{method}({field_id_size}, {name}.calculate_size());" def get_estimated_size(self) -> int: # For message types, we can't easily estimate the submessage size without @@ -822,7 +827,7 @@ class BytesType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -897,7 +902,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" class PointerToStringBufferType(PointerToBufferTypeBase): @@ -939,7 +944,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, "{self.name}", this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -1103,9 +1108,9 @@ class FixedArrayBytesType(TypeInfo): if force: # For repeated fields, always calculate size (no zero check) - return f"size.add_length_force({field_id_size}, {length_field});" - # For non-repeated fields, add_length already checks for zero - return f"size.add_length({field_id_size}, {length_field});" + return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" + # For non-repeated fields, length already checks for zero + return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -1132,7 +1137,7 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint32") + return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1168,7 +1173,7 @@ class EnumType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation( - name, force, "add_uint32", f"static_cast<uint32_t>({name})" + name, force, "uint32", f"static_cast<uint32_t>({name})" ) def get_estimated_size(self) -> int: @@ -1190,7 +1195,7 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed32({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -1214,7 +1219,7 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - return f"size.add_sfixed64({field_id_size}, {name});" + return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -1237,7 +1242,7 @@ class SInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint32") + return self._get_simple_size_calculation(name, force, "sint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1257,7 +1262,7 @@ class SInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint64") + return self._get_simple_size_calculation(name, force, "sint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -1694,11 +1699,17 @@ class RepeatedTypeInfo(TypeInfo): # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields - # Handle message types separately as they use a dedicated helper + # Handle message types separately - generate inline loop if isinstance(self._ti, MessageType): field_id_size = self._ti.calculate_field_id_size() - container = f"*{name}" if self._use_pointer else name - return f"size.add_repeated_message({field_id_size}, {container});" + container_ref = f"*{name}" if self._use_pointer else name + empty_check = f"{name}->empty()" if self._use_pointer else f"{name}.empty()" + o = f"if (!{empty_check}) {{\n" + o += f" for (const auto &it : {container_ref}) {{\n" + o += f" size += ProtoSize::calc_message_force({field_id_size}, it.calculate_size());\n" + o += " }\n" + o += "}" + return o # For non-message types, generate size calculation with iteration container_ref = f"*{name}" if self._use_pointer else name @@ -1713,14 +1724,14 @@ class RepeatedTypeInfo(TypeInfo): field_id_size = self._ti.calculate_field_id_size() bytes_per_element = field_id_size + num_bytes size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" - o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n" + o += f" size += {size_expr} * {bytes_per_element};\n" else: # Other types need the actual value # Special handling for const char* elements if self._use_pointer and "const char" in self._container_no_template: field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" - o += f" size.add_length_force({field_id_size}, strlen(it));\n" + o += f" size += ProtoSize::calc_length_force({field_id_size}, strlen(it));\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" @@ -2233,23 +2244,19 @@ def build_message_type( o += indent("\n".join(encode)) + "\n" o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer &buffer) const override;" + prot = "void encode(ProtoWriteBuffer &buffer) const;" public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # Add calculate_size method only if this message needs encoding and has fields if needs_encode and size_calc: - o = f"void {desc.name}::calculate_size(ProtoSize &size) const {{" - # For a single field, just inline it for simplicity - if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: - o += f" {size_calc[0]} }}\n" - else: - # For multiple fields - o += "\n" - o += indent("\n".join(size_calc)) + "\n" - o += "}\n" + o = f"uint32_t {desc.name}::calculate_size() const {{\n" + o += " uint32_t size = 0;\n" + o += indent("\n".join(size_calc)) + "\n" + o += " return size;\n" + o += "}\n" cpp += o - prot = "void calculate_size(ProtoSize &size) const override;" + prot = "uint32_t calculate_size() const;" public_content.append(prot) # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used @@ -2933,14 +2940,8 @@ static const char *const TAG = "api.service"; hpp += " public:\n" hpp += "#endif\n\n" - # Add non-template send_message method - hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" - hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - hpp += " DumpBuffer dump_buf;\n" - hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" - hpp += "#endif\n" - hpp += " return this->send_message_impl(msg, message_type);\n" - hpp += " }\n\n" + # send_message is now a template on APIConnection directly + # No non-template send_message method needed here # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" From 3c7956e72d34f3cc3fb9f5100afc09e974982f26 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:04:00 -0500 Subject: [PATCH 0439/2056] [multiple] Add default initializers to uninitialized member variables (#14556) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bedjet/bedjet_hub.h | 8 ++++---- .../components/current_based/current_based_cover.h | 12 ++++++------ esphome/components/deep_sleep/deep_sleep_component.h | 2 +- esphome/components/max6956/max6956.h | 4 ++-- esphome/components/ms8607/ms8607.h | 8 ++++---- .../remote_transmitter/remote_transmitter.h | 2 +- esphome/components/sen5x/sen5x.h | 8 ++++---- esphome/components/sim800l/sim800l.h | 10 +++++----- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 6258795b029..59b0af93ade 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo std::unique_ptr<BedjetCodec> codec_; bool discover_characteristics_(); - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; + uint16_t char_handle_cmd_{0}; + uint16_t char_handle_name_{0}; + uint16_t char_handle_status_{0}; + uint16_t config_descr_status_{0}; uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 76bd85cdf77..40b39517e4e 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -67,21 +67,21 @@ class CurrentBasedCover : public cover::Cover, public Component { sensor::Sensor *open_sensor_{nullptr}; Trigger<> open_trigger_; - float open_moving_current_threshold_; + float open_moving_current_threshold_{0.0f}; float open_obstacle_current_threshold_{FLT_MAX}; - uint32_t open_duration_; + uint32_t open_duration_{0}; sensor::Sensor *close_sensor_{nullptr}; Trigger<> close_trigger_; - float close_moving_current_threshold_; + float close_moving_current_threshold_{0.0f}; float close_obstacle_current_threshold_{FLT_MAX}; - uint32_t close_duration_; + uint32_t close_duration_{0}; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; Trigger<> malfunction_trigger_; - uint32_t start_sensing_delay_; - float obstacle_rollback_; + uint32_t start_sensing_delay_{0}; + float obstacle_rollback_{0.0f}; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1998b815f37..14713d51a1d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -145,7 +145,7 @@ class DeepSleepComponent : public Component { #endif // USE_BK72XX #ifdef USE_ESP32 - InternalGPIOPin *wakeup_pin_; + InternalGPIOPin *wakeup_pin_{nullptr}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0c609b0b436..31f97c11f8a 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -63,8 +63,8 @@ class MAX6956 : public Component, public i2c::I2CDevice { bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register bool write_reg_(uint8_t reg, uint8_t value); - max6956::MAX6956CURRENTMODE brightness_mode_; - uint8_t global_brightness_; + max6956::MAX6956CURRENTMODE brightness_mode_{}; + uint8_t global_brightness_{0}; private: int8_t prev_bright_[28] = {0}; diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index ceb3dd22c8b..2888b6cdd24 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -67,9 +67,9 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { /// use raw temperature & pressure to calculate & publish values void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure); - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; /** I2CDevice object to communicate with secondary I2C address for the humidity sensor * @@ -77,7 +77,7 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { * * Default address for humidity is 0x40 */ - MS8607HumidityDevice *humidity_device_; + MS8607HumidityDevice *humidity_device_{nullptr}; /// This device's pressure & temperature calibration values, read from PROM struct CalibrationValues { diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index aee52ea170b..6b4ebfe24bd 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -96,7 +96,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool inverted_{false}; bool non_blocking_{false}; #endif - uint8_t carrier_duty_percent_; + uint8_t carrier_duty_percent_{50}; Trigger<> transmit_trigger_; Trigger<> complete_trigger_; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index e3bf931b416..a9d4da86b89 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -104,12 +104,12 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri char serial_number_[17] = "UNKNOWN"; uint16_t voc_baseline_state_[4]{0}; - uint32_t voc_baseline_time_; - uint16_t firmware_version_; + uint32_t voc_baseline_time_{0}; + uint16_t firmware_version_{0}; Sen5xType type_{Sen5xType::UNKNOWN}; - ERRORCODE error_code_; + ERRORCODE error_code_{ERRORCODE::UNKNOWN}; bool initialized_{false}; - bool store_baseline_; + bool store_baseline_{false}; sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index a2da686ce1f..e9e2f66d789 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -107,11 +107,11 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { std::string recipient_; std::string outgoing_message_; std::string ussd_; - bool send_pending_; - bool dial_pending_; - bool connect_pending_; - bool disconnect_pending_; - bool send_ussd_pending_; + bool send_pending_{false}; + bool dial_pending_{false}; + bool connect_pending_{false}; + bool disconnect_pending_{false}; + bool send_ussd_pending_{false}; uint8_t call_state_{6}; CallbackManager<void(std::string, std::string)> sms_received_callback_; From 3db436e48e0cafd48711bd782600cbea7a5f9adc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:05:34 -0500 Subject: [PATCH 0440/2056] [esp32_ble_server][espnow][time] Fix logic bugs (#14553) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../components/esp32_ble_server/ble_server.cpp | 18 +++++++----------- esphome/components/espnow/automation.h | 4 ++-- esphome/components/time/real_time_clock.cpp | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index f292cf87220..ecc53e197f2 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,6 +7,7 @@ #ifdef USE_ESP32 +#include <algorithm> #include <nvs_flash.h> #include <freertos/FreeRTOSConfig.h> #include <esp_bt_main.h> @@ -38,21 +39,16 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - uint16_t index_to_remove = 0; - // Iterate over the services to start - for (unsigned i = 0; i < this->services_to_start_.size(); i++) { - BLEService *service = this->services_to_start_[i]; + for (auto &service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service - } else { - index_to_remove = i + 1; } } - // Remove the services that have been started - if (index_to_remove > 0) { - this->services_to_start_.erase(this->services_to_start_.begin(), - this->services_to_start_.begin() + index_to_remove - 1); - } + // Remove services that have been started + this->services_to_start_.erase( + std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), + [](BLEService *service) { return service->is_starting() || service->is_running(); }), + this->services_to_start_.end()); } break; } diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0b266814005..0fbb14e3888 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, protected: bool has_address_{false}; - const uint8_t *address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]; }; class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, public ESPNowUnknownPeerHandler { @@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_ protected: bool has_address_{false}; - const uint8_t *address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]; }; } // namespace esphome::espnow diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 2e758ad8e7a..566344fa880 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -90,7 +90,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { }; struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret == EINVAL) { + if (ret != 0 && errno == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL // while ESP32 expects it not to be NULL ret = settimeofday(&timev, nullptr); From 7b8ba9bf206f319f614ce52d4d80c5e649eb73ee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:14:12 -0500 Subject: [PATCH 0441/2056] [multiple] Fix cast/operator precedence bugs (#14560) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/datetime/date_entity.h | 2 +- esphome/components/es7210/es7210.cpp | 2 +- esphome/components/sgp4x/sgp4x.cpp | 2 +- esphome/components/tsl2591/tsl2591.cpp | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index cbf2b855060..8233e809a10 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -91,7 +91,7 @@ class DateCall { DateEntity *parent_; - optional<int16_t> year_; + optional<uint16_t> year_; optional<uint8_t> month_; optional<uint8_t> day_; }; diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 1358121c1b7..4371075fa90 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB mic_gain += 0.5; if (mic_gain <= 33.0) { - return (uint8_t) mic_gain / 3; + return (uint8_t) (mic_gain / 3); } if (mic_gain < 36.0) { return 12; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 44d0a54080b..cb41e374f82 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -199,7 +199,7 @@ void SGP4xComponent::measure_raw_() { response_words = 2; } } - uint16_t rhticks = llround((uint16_t) ((humidity * 65535) / 100)); + uint16_t rhticks = (uint16_t) llround((humidity * 65535) / 100); uint16_t tempticks = (uint16_t) (((temperature + 45) * 65535) / 175); // first parameter are the relative humidity ticks data[0] = rhticks; diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 42c524a0741..4ce673a91a1 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -327,7 +327,9 @@ uint16_t TSL2591Component::get_illuminance(TSL2591SensorChannel channel, uint32_ return (combined_illuminance >> 16); } else if (channel == TSL2591_SENSOR_CHANNEL_VISIBLE) { // Reads all and subtracts out the infrared - return ((combined_illuminance & 0xFFFF) - (combined_illuminance >> 16)); + uint16_t full = combined_illuminance & 0xFFFF; + uint16_t ir = combined_illuminance >> 16; + return (ir > full) ? 0 : (full - ir); } // unknown channel! ESP_LOGE(TAG, "get_illuminance() caller requested an unknown channel: %d", channel); From 219d5170e006e7297004b1cfeb7e5323307980b0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:15:54 -0500 Subject: [PATCH 0442/2056] [noblex] Fix IR receive losing decoded bytes between calls (#14533) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/noblex/noblex.cpp | 130 +++++++++++++-------------- esphome/components/noblex/noblex.h | 3 +- 2 files changed, 64 insertions(+), 69 deletions(-) diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index f1e76eabf2b..e7e421d1776 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -118,15 +118,15 @@ void NoblexClimate::transmit_state() { data->mark(NOBLEX_HEADER_MARK); data->space(NOBLEX_HEADER_SPACE); // Data (sent remote_state from the MSB to the LSB) - for (uint8_t i : remote_state) { - for (int8_t j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { + for (int byte_idx = 0; byte_idx < 8; byte_idx++) { + for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) { + if ((byte_idx == 4) && (bit_idx == 4)) { // Header intermediate data->mark(NOBLEX_BIT_MARK); data->space(NOBLEX_GAP); // gap en bit 36 } else { data->mark(NOBLEX_BIT_MARK); - bool bit = i & (1 << j); + bool bit = remote_state[byte_idx] & (1 << bit_idx); data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE); } } @@ -145,76 +145,71 @@ void NoblexClimate::transmit_state() { // Handle received IR Buffer bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { - uint8_t remote_state[8] = {0}; - uint8_t crc = 0, crc_calculated = 0; - - if (!receiving_) { - // Validate header - if (data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header"); - receiving_ = true; - // Read first 36 bits - for (int i = 0; i < 5; i++) { - // Read bit - for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j == 4)) { - remote_state[i] |= 1 << j; - // Header intermediate - ESP_LOGVV(TAG, "GAP"); - return false; - } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; - } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); - return false; - } - } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); - } - - } else { - ESP_LOGV(TAG, "Header fail"); - receiving_ = false; - return false; - } - - } else { - // Read the remaining 28 bits - for (int i = 4; i < 8; i++) { - // Read bit + if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) { + // First part: header + first 36 bits, followed by 20ms gap + data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE); + ESP_LOGV(TAG, "Header"); + this->receiving_ = false; + memset(this->remote_state_, 0, sizeof(this->remote_state_)); + for (int i = 0; i < 5; i++) { for (int j = 7; j >= 0; j--) { - if ((i == 4) & (j >= 4)) { - // nothing + if ((i == 4) && (j == 4)) { + this->remote_state_[i] |= 1 << j; + ESP_LOGVV(TAG, "GAP"); + this->receiving_ = true; + return false; } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - remote_state[i] |= 1 << j; + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "Byte %d %02X", i, remote_state[i]); + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } + return false; + } - // Read crc - for (int i = 3; i >= 0; i--) { - if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { - crc |= 1 << i; + // Second part: remaining 28 bits + 4-bit CRC + footer + if (!this->receiving_) { + return false; + } + this->receiving_ = false; + for (int i = 4; i < 8; i++) { + for (int j = 7; j >= 0; j--) { + if ((i == 4) && (j >= 4)) { + // already decoded in first part + } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + this->remote_state_[i] |= 1 << j; } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { - ESP_LOGVV(TAG, "Bit %d CRC fail", i); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } - ESP_LOGV(TAG, "CRC %02X", crc); - - // Validate footer - if (!data.expect_mark(NOBLEX_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); - return false; - } - receiving_ = false; + ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]); } - for (uint8_t i : remote_state) + // Read CRC + uint8_t crc = 0; + for (int i = 3; i >= 0; i--) { + if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) { + crc |= 1 << i; + } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Bit %d CRC fail", i); + return false; + } + } + ESP_LOGV(TAG, "CRC %02X", crc); + + // Validate footer + if (!data.expect_mark(NOBLEX_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + // Validate CRC + uint8_t crc_calculated = 0; + for (uint8_t i : this->remote_state_) crc_calculated += reverse_bits(i); crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4; ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated); @@ -224,11 +219,12 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { return false; } - ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1], - remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]); + ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0], + this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4], + this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]); auto powered_on = false; - if ((remote_state[0] & NOBLEX_POWER) == NOBLEX_POWER) { + if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) { powered_on = true; this->powered_on_assumed = powered_on; } else { @@ -241,7 +237,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { // Set received mode if (powered_on_assumed) { - auto mode = (remote_state[0] & 0xE0) >> 5; + auto mode = (this->remote_state_[0] & 0xE0) >> 5; ESP_LOGV(TAG, "Mode: %02X", mode); switch (mode) { case IRNoblexMode::IR_NOBLEX_MODE_AUTO: @@ -263,7 +259,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received temp - uint8_t temp = remote_state[1]; + uint8_t temp = this->remote_state_[1]; ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); temp = 0x0F & reverse_bits(temp); @@ -272,7 +268,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = temp; // Set received fan speed - auto fan = (remote_state[0] & 0x0C) >> 2; + auto fan = (this->remote_state_[0] & 0x0C) >> 2; ESP_LOGV(TAG, "Fan: %02X", fan); switch (fan) { case IRNoblexFan::IR_NOBLEX_FAN_HIGH: @@ -291,7 +287,7 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { } // Set received swing status - if (remote_state[0] & 0x02) { + if (this->remote_state_[0] & 0x02) { ESP_LOGV(TAG, "Swing vertical"); this->swing_mode = climate::CLIMATE_SWING_VERTICAL; } else { @@ -299,8 +295,6 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { this->swing_mode = climate::CLIMATE_SWING_OFF; } - for (uint8_t &i : remote_state) - i = 0; this->publish_state(); return true; } // end on_receive() diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index 57990db0053..3d52a1a5389 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -41,7 +41,8 @@ class NoblexClimate : public climate_ir::ClimateIR { /// Handle received IR Buffer. bool on_receive(remote_base::RemoteReceiveData data) override; bool send_swing_cmd_{false}; - bool receiving_ = false; + bool receiving_{false}; + uint8_t remote_state_[8]{}; }; } // namespace noblex From 9ab5f5d451a4d1aaf030fbff72bff0011478e5fb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:42:05 -0500 Subject: [PATCH 0443/2056] [light] Fix unsigned underflow in addressable scan effect (#14546) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../light/addressable_light_effect.h | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index a85ea4661d9..461ddbc085a 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -171,12 +171,27 @@ class AddressableScanEffect : public AddressableLightEffect { if (now - this->last_move_ < this->move_interval_) return; - if (direction_) { + const auto num_leds = static_cast<uint32_t>(it.size()); + if (this->scan_width_ >= num_leds) { + it.all() = current_color; + it.schedule_show(); + this->last_move_ = now; + return; + } + + const uint32_t max_pos = num_leds - this->scan_width_; + if (this->at_led_ >= max_pos) { + this->at_led_ = max_pos; + this->direction_ = false; + } + + if (this->direction_) { this->at_led_++; - if (this->at_led_ == it.size() - this->scan_width_) + if (this->at_led_ >= max_pos) this->direction_ = false; } else { - this->at_led_--; + if (this->at_led_ > 0) + this->at_led_--; if (this->at_led_ == 0) this->direction_ = true; } From a9cceebb33612054866465254456336fd4d76ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:48:50 -0500 Subject: [PATCH 0444/2056] [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/nextion/nextion_component.cpp | 2 +- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 1 + esphome/components/pn532_i2c/pn532_i2c.cpp | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 324ad873728..30c8b80524f 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -88,7 +88,7 @@ void NextionComponent::update_component_settings(bool force_update) { this->send_state_to_nextion(); } - if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { + if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco_is_set)) { this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); this->component_flags_.bco_needs_update = false; } diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index d1d9c200cf9..e1ddd1d7c67 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -97,7 +97,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); - bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->amplitude_detector_.is_amplitude_convergent(); if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway @@ -362,7 +362,7 @@ bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const for (auto v : this->phase_mins) global_min = std::min(global_min, v); for (auto v : this->phase_maxs) - global_max = std::min(global_max, v); + global_max = std::max(global_max, v); float global_amplitude = (global_max - global_min) / 2.0f; float mean_amplitude = this->get_mean_oscillation_amplitude(); return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 9c5caec7758..eb6d3931e05 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -647,6 +647,7 @@ void Pipsolar::handle_qpiws_(const char *message) { case 34: this->publish_binary_sensor_(enabled, this->warning_high_ac_input_during_bus_soft_start_); value_warnings_present |= enabled.value_or(false); + break; case 35: this->publish_binary_sensor_(enabled, this->warning_battery_equalization_); value_warnings_present |= enabled.value_or(false); diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index b306222a21c..41f0f079aae 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -49,7 +49,7 @@ bool PN532I2C::read_response(uint8_t command, std::vector<uint8_t> &data) { return false; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return false; @@ -95,7 +95,7 @@ uint8_t PN532I2C::read_response_length_() { return 0; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return 0; From 8f3db96291c06708a23f5171515cd26ec84e33a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:50:26 -0500 Subject: [PATCH 0445/2056] [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/ade7880/ade7880.cpp | 6 +++--- esphome/components/ade7880/ade7880_registers.h | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 4 ++-- esphome/components/weikai/weikai.cpp | 4 +++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index f6a15190cd4..8fb3e55b91a 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -121,7 +121,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -137,7 +137,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -153,7 +153,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 8b5b68abb0a..9fd8ca3bf5e 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402; constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t ARWATTHR = 0xE406; +constexpr uint16_t BRWATTHR = 0xE407; +constexpr uint16_t CRWATTHR = 0xE408; constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t CFVARHR = 0xE40B; diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d4ccefd9b29..1806354712d 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -310,8 +310,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); } } - esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id, + ESP_GATT_OK, nullptr); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 1d835daf1ef..3f5d6c787cb 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -445,6 +445,7 @@ void WeikaiChannel::flush() { } size_t WeikaiChannel::xfer_fifo_to_buffer_() { + size_t total = 0; size_t to_transfer; size_t free; while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) { @@ -458,9 +459,10 @@ size_t WeikaiChannel::xfer_fifo_to_buffer_() { this->reg(0).read_fifo(data, to_transfer); for (size_t i = 0; i < to_transfer; i++) this->receive_buffer_.push(data[i]); + total += to_transfer; } } // while work to do - return to_transfer; + return total; } /// From 0469612d0774ef26acef975446e4c187dde876f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:02:17 -0500 Subject: [PATCH 0446/2056] [multiple] Fix assorted medium-severity bugs (#14555) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bytebuffer/bytebuffer.h | 2 +- esphome/components/cap1188/cap1188.cpp | 2 +- esphome/components/hte501/hte501.cpp | 2 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +---- esphome/components/inkplate/inkplate.cpp | 10 ++++----- esphome/components/msa3xx/msa3xx.cpp | 6 +---- esphome/components/nfc/ndef_record_text.cpp | 5 +++++ esphome/components/nfc/nfc.cpp | 22 +++++++++++++------ esphome/components/nfc/nfc.h | 2 +- .../components/template/text/template_text.h | 6 ++++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 030484ce32a..3c68094dbcc 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -263,7 +263,7 @@ class ByteBuffer { void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } - void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } // Signed versions of the put functions diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 9e8c87d1472..64bdc620cd8 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -92,7 +92,7 @@ void CAP1188Component::loop() { this->read_register(CAP1188_MAIN, &data, 1); data = data & ~CAP1188_MAIN_INT; - this->write_register(CAP1188_MAIN, &data, 2); + this->write_register(CAP1188_MAIN, &data, 1); } for (auto *channel : this->channels_) { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 972e72c170a..ef9ef1fabf7 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -49,7 +49,7 @@ void HTE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) || i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 8a20192c1e4..9f510eef74f 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -599,11 +599,7 @@ bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { } int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } } // namespace ina2xx_base } // namespace esphome diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index df9c2b29c78..7551c6fc775 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -407,7 +407,7 @@ void Inkplate::display1b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); @@ -575,7 +575,7 @@ void Inkplate::display3b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); uint32_t pos; uint32_t data; @@ -646,7 +646,7 @@ bool Inkplate::partial_update_() { int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; eink_on_(); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); for (int k = 0; k < rep; k++) { vscan_start_(); @@ -704,7 +704,7 @@ void Inkplate::vscan_start_() { } void Inkplate::hscan_start_(uint32_t d) { - uint8_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; GPIO.out_w1tc = this->get_data_pin_mask_() | clock; @@ -751,7 +751,7 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { uint32_t send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | (((data & 0b11100000) >> 5) << 25); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); for (int k = 0; k < rep; k++) { vscan_start_(); diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index e46bfed193a..6d6b21e6af2 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -364,11 +364,7 @@ void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset } int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger, diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp index 80b0108b46c..8a9a2cb014f 100644 --- a/esphome/components/nfc/ndef_record_text.cpp +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -14,6 +14,11 @@ NdefRecordText::NdefRecordText(const std::vector<uint8_t> &payload) { uint8_t language_code_length = payload[0] & 0b00111111; // Todo, make use of encoding bit? + if (1 + language_code_length > payload.size()) { + ESP_LOGE(TAG, "Record payload too short for language code"); + return; + } + this->language_code_ = std::string(payload.begin() + 1, payload.begin() + 1 + language_code_length); this->text_ = std::string(payload.begin() + 1 + language_code_length, payload.end()); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 8567b0969ae..55543cd292a 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -35,7 +35,7 @@ uint8_t guess_tag_type(uint8_t uid_length) { } } -uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { +int8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { if (data[i] == 0x00) { // Do nothing, skip @@ -49,17 +49,25 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data) { } bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_length, uint8_t &message_start_index) { + if (data.size() < MIFARE_CLASSIC_BLOCK_SIZE) { + ESP_LOGE(TAG, "Error, data too short for NDEF detection."); + return false; + } auto i = get_mifare_classic_ndef_start_index(data); - if (data[i] != 0x03) { + if (i < 0 || data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; } - if (data[i + 1] == 0xFF) { - message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); - message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + uint8_t idx = static_cast<uint8_t>(i); + if (idx + 4 <= data.size() && data[idx + 1] == 0xFF) { + message_length = ((0xFF & data[idx + 2]) << 8) | (0xFF & data[idx + 3]); + message_start_index = idx + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else if (idx + 2 <= data.size()) { + message_length = data[idx + 1]; + message_start_index = idx + MIFARE_CLASSIC_SHORT_TLV_SIZE; } else { - message_length = data[i + 1]; - message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + ESP_LOGE(TAG, "Error, TLV data too short."); + return false; } return true; } diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index cdaea82af6d..8ca5cb7ea44 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -72,7 +72,7 @@ ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026. std::string format_bytes(std::span<const uint8_t> bytes); uint8_t guess_tag_type(uint8_t uid_length); -uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); +int8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); bool decode_mifare_classic_tlv(std::vector<uint8_t> &data, uint32_t &message_length, uint8_t &message_start_index); uint32_t get_mifare_classic_buffer_size(uint32_t message_length); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 88c6afdf2c6..7f176db09ef 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -52,7 +52,11 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { bool hasdata = this->pref_.load(&temp); if (hasdata) { - value.assign(temp + 1, (size_t) temp[0]); + size_t len = static_cast<uint8_t>(temp[0]); + if (len > SZ) { + len = SZ; + } + value.assign(temp + 1, len); } this->prev_.assign(value); From 4f4b2bfdecc1ccc06f344d4e0c6dbad88537a3a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:14:35 -0500 Subject: [PATCH 0447/2056] [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bl0906/bl0906.cpp | 4 +++- esphome/components/bmp581_base/bmp581_base.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index c1cd48a1ace..7b643bba98a 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -10,7 +10,9 @@ static const char *const TAG = "bl0906"; constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } -constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } +constexpr int32_t to_int32_t(sbe24_t input) { + return static_cast<int32_t>(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8; +} // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c4a96ebc39b..89a92de31d2 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -429,7 +429,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) return true; @@ -458,7 +458,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast<int32_t>(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] From 2c83c6a79f356d3f0f6910fc573e47c53e695c5e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:47:56 -0500 Subject: [PATCH 0448/2056] [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++ .../packet_transport/packet_transport.cpp | 2 + .../seeed_mr60fda2/seeed_mr60fda2.cpp | 44 ++++++++----------- .../shelly_dimmer/shelly_dimmer.cpp | 4 +- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 3e447e9169d..66cb25b864b 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -422,6 +422,9 @@ void LvglComponent::write_random_() { auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; + // clamp size so the square fits within the draw buffer + if ((size + 1) * (size + 1) > this->draw_buf_.size) + size = static_cast<decltype(size)>(sqrtf(this->draw_buf_.size)) - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6f1286b4693..964037a02c4 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -137,6 +137,8 @@ class PacketDecoder { return DECODE_EMPTY; if (this->buffer_[this->position_] != key) return DECODE_UNMATCHED; + if (this->position_ + 1 + sizeof(T) > this->len_) + return DECODE_ERROR; this->position_++; T value = 0; for (size_t i = 0; i != sizeof(T); ++i) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 5d571618d33..c6527a948ef 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -149,28 +149,25 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { switch (this->current_frame_locate_) { case LOCATE_FRAME_HEADER: // starting buffer if (buffer == FRAME_HEADER_BUFFER) { - this->current_frame_len_ = 1; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_len_ = 0; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_ID_FRAME1: this->current_frame_id_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_ID_FRAME2: this->current_frame_id_ += buffer; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_LENGTH_FRAME_H: this->current_data_frame_len_ = buffer << 8; - if (this->current_data_frame_len_ == 0x00) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + if (this->current_data_frame_len_ == 0) { + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -181,15 +178,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { this->current_frame_locate_ = LOCATE_FRAME_HEADER; } else { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_TYPE_FRAME1: this->current_frame_type_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_TYPE_FRAME2: @@ -198,8 +193,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) || (this->current_frame_type_ == RESULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RESULT_PARAMETERS) || (this->current_frame_type_ == RESULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RESULT_SENSITIVITY)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -207,8 +201,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { break; case LOCATE_HEAD_CKSUM_FRAME: if (validate_checksum(this->current_frame_buf_, this->current_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); @@ -223,21 +216,20 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { } break; case LOCATE_DATA_FRAME: - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; - this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; - if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { - this->current_frame_locate_++; - } - if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + if (this->current_frame_len_ >= FRAME_BUF_MAX_SIZE) { ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + } + this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME + 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; } break; case LOCATE_DATA_CKSUM_FRAME: if (validate_checksum(this->current_data_buf_, this->current_data_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; this->process_frame_(); } else { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index bdb33d31af5..88fcbcbfe15 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -188,8 +188,8 @@ bool ShellyDimmer::upgrade_firmware_() { break; } - std::memcpy(buffer, p, BUFFER_SIZE); - p += BUFFER_SIZE; + std::memcpy(buffer, p, len); + p += len; if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); From 587bf68091caffac14f4d7ed2714fac28848354c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:30 -0500 Subject: [PATCH 0449/2056] [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/ltr501/ltr501.cpp | 21 +++++++++---------- esphome/components/ltr501/ltr501.h | 3 +++ .../pvvx_mithermometer/pvvx_mithermometer.cpp | 7 +++---- .../pvvx_mithermometer/pvvx_mithermometer.h | 2 ++ esphome/components/smt100/smt100.cpp | 16 +++++++------- esphome/components/smt100/smt100.h | 3 +++ 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 04de91e3629..4c9006be1d9 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -146,7 +146,6 @@ void LTRAlsPs501Component::update() { void LTRAlsPs501Component::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -175,20 +174,20 @@ void LTRAlsPs501Component::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->tries_ = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->apply_lux_calculation_(this->als_readings_); this->state_ = State::DATA_COLLECTED; - } else if (tries >= MAX_TRIES) { + } else if (this->tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); - tries = 0; + this->tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->tries_++; } break; @@ -230,21 +229,21 @@ void LTRAlsPs501Component::loop() { } void LTRAlsPs501Component::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 02c025da304..d9a53c9bd46 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -74,6 +74,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { READY_TO_PUBLISH, KEEP_PUBLISHING } state_{State::NOT_INITIALIZED}; + uint8_t tries_{0}; LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; @@ -130,6 +131,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; // // Sensors for publishing data diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 239a1e74fe6..35badf48bb6 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -66,12 +66,11 @@ optional<ParseResult> PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: return {}; } - static uint8_t last_frame_count = 0; - if (last_frame_count == raw[13]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + if (this->last_frame_count_ == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_); return {}; } - last_frame_count = raw[13]; + this->last_frame_count_ = raw[13]; return result; } diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index c15e1e7e22e..09b5e91a16b 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -39,6 +39,8 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_voltage_{nullptr}; sensor::Sensor *signal_strength_{nullptr}; + uint8_t last_frame_count_{0}; + optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result); bool report_results_(const optional<ParseResult> &result, const char *address); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 1bcb9642649..105cc06edbf 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -12,10 +12,9 @@ void SMT100Component::update() { } void SMT100Component::loop() { - static char buffer[MAX_LINE_LENGTH]; while (this->available() != 0) { - if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); + if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { + int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -56,7 +55,6 @@ void SMT100Component::dump_config() { } int SMT100Component::readline_(int readch, char *buffer, int len) { - static int pos = 0; int rpos; if (readch > 0) { @@ -64,13 +62,13 @@ int SMT100Component::readline_(int readch, char *buffer, int len) { case '\n': // Ignore new-lines break; case '\r': // Return on CR - rpos = pos; - pos = 0; // Reset position index ready for next time + rpos = this->readline_pos_; + this->readline_pos_ = 0; // Reset position index ready for next time return rpos; default: - if (pos < len - 1) { - buffer[pos++] = readch; - buffer[pos] = 0; + if (this->readline_pos_ < len - 1) { + buffer[this->readline_pos_++] = readch; + buffer[this->readline_pos_] = 0; } } } diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index df8803e1c62..cb01b1ed554 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -28,6 +28,9 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { protected: int readline_(int readch, char *buffer, int len); + char readline_buffer_[MAX_LINE_LENGTH]{}; + int readline_pos_{0}; + sensor::Sensor *counts_sensor_{nullptr}; sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; From 5777908da712d64dde2f760d68aaa707b606dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:53 -0500 Subject: [PATCH 0450/2056] [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/beken_spi_led_strip/led_strip.cpp | 2 +- esphome/components/iaqcore/iaqcore.cpp | 10 ++++++---- esphome/components/scd30/scd30.cpp | 2 +- esphome/components/sen21231/sen21231.cpp | 7 ++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 67b84722573..f425f3ca5c4 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) { int source_clk = 0; int spi_clk = 0; int div = 0; - uint32_t param; + uint32_t param = PWD_SPI_CLK_BIT; if (max_hz > 4333000) { if (max_hz > 30000000) { spi_clk = 30000000; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 274f9086b69..c414eb8f608 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -10,11 +10,13 @@ static const char *const TAG = "iaqcore"; enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; +static constexpr size_t SENSOR_DATA_LENGTH = 9; + struct SensorData { - uint16_t co2; - IAQCoreErrorCode status; int32_t resistance; + uint16_t co2; uint16_t tvoc; + IAQCoreErrorCode status; SensorData(const uint8_t *buffer) { this->co2 = encode_uint16(buffer[0], buffer[1]); @@ -33,9 +35,9 @@ void IAQCore::setup() { } void IAQCore::update() { - uint8_t buffer[sizeof(SensorData)]; + uint8_t buffer[SENSOR_DATA_LENGTH]; - if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, SENSOR_DATA_LENGTH) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3c2c06fd685..e61d0142be6 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -222,7 +222,7 @@ bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) } uint16_t SCD30Component::get_forced_calibration_reference() { - uint16_t forced_calibration_reference; + uint16_t forced_calibration_reference = 0; // Get current CO2 calibration if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { ESP_LOGE(TAG, "Unable to read forced calibration reference."); diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index 67001c3f14b..8c9f3d71346 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -20,7 +20,12 @@ void Sen21231Sensor::dump_config() { void Sen21231Sensor::read_data_() { person_sensor_results_t results; - this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + if (!this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results))) { + ESP_LOGW(TAG, "Failed to read data from SEN21231"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); this->publish_state(results.num_faces); if (results.num_faces == 1) { From de7572bd3e7ebb9308744efb6f7faf88c480629d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:04:12 -0500 Subject: [PATCH 0451/2056] [lightwaverf] Fix ISR safety issues (#14563) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/lightwaverf/LwRx.cpp | 14 +++++++++++--- esphome/components/lightwaverf/LwRx.h | 4 ++-- esphome/components/lightwaverf/LwTx.cpp | 3 ++- esphome/components/lightwaverf/LwTx.h | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp index 2b1ad5e8700..97104578508 100644 --- a/esphome/components/lightwaverf/LwRx.cpp +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -8,6 +8,7 @@ #include "LwRx.h" #include <cstring> +#include "esphome/core/helpers.h" namespace esphome { namespace lightwaverf { @@ -185,13 +186,20 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { bool ret = true; int16_t j = 0; // int if (this->rx_msgcomplete && len <= RX_MSGLEN) { + // Copy message under interrupt lock to prevent ISR overwriting rx_msg mid-read + uint8_t msg_copy[RX_MSGLEN]; + { + InterruptLock lock; + memcpy(msg_copy, this->rx_msg, RX_MSGLEN); + this->rx_msgcomplete = false; + } for (uint8_t i = 0; ret && i < RX_MSGLEN; i++) { if (this->rx_translate || (len != RX_MSGLEN)) { - j = this->rx_find_nibble_(this->rx_msg[i]); + j = this->rx_find_nibble_(msg_copy[i]); if (j < 0) ret = false; } else { - j = this->rx_msg[i]; + j = msg_copy[i]; } switch (len) { case 4: @@ -199,6 +207,7 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { buf[2] = j; if (i == 2) buf[3] = j; + [[fallthrough]]; case 2: if (i == 3) buf[0] = j; @@ -212,7 +221,6 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { break; } } - this->rx_msgcomplete = false; } else { ret = false; } diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h index 7200f9a51c7..8b34de9fbbd 100644 --- a/esphome/components/lightwaverf/LwRx.h +++ b/esphome/components/lightwaverf/LwRx.h @@ -105,8 +105,8 @@ class LwRx { uint32_t rx_prev; // time of previous interrupt in microseconds - bool rx_msgcomplete = false; // set high when message available - bool rx_translate = true; // Set false to get raw data + volatile bool rx_msgcomplete = false; // set high when message available + bool rx_translate = true; // Set false to get raw data uint8_t rx_state = 0; diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index b69b93b978d..8852935bfd8 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -192,7 +192,8 @@ void LwTx::lwtx_set_gap_multiplier(uint8_t gap_multiplier) { this->tx_gap_multip void LwTx::lw_timer_start() { { InterruptLock lock; - static LwTx *arg = this; // NOLINT + static LwTx *arg; + arg = this; timer1_attachInterrupt([] { isr_t_xtimer(arg); }); timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); timer1_write(this->espPeriod); diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index fe7b942a3aa..9192426440f 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -62,8 +62,8 @@ class LwTx { uint8_t tx_repeats = 12; // Number of repeats of message sent uint8_t txon = 1; uint8_t txoff = 0; - bool tx_msg_active = false; // set true to activate message sending - bool tx_translate = true; // Set false to send raw data + volatile bool tx_msg_active = false; // set true to activate message sending + bool tx_translate = true; // Set false to send raw data uint8_t tx_buf[TX_MSGLEN]; // the message buffer during reception uint8_t tx_repeat = 0; // counter for repeats From c26c5935b6c9bb1b7a521efa7188a0c3dad74e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:19 -1000 Subject: [PATCH 0452/2056] Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4bd018b5c95..1a0c54da6d6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: category: "/language:${{matrix.language}}" From 086c1bb505dc718d35e9c91b6fbea5bb54705735 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:44 -1000 Subject: [PATCH 0453/2056] Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 38e93c4f175..a8952260303 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 035f98569326bafd7da350e4a15c2739c424e987 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:16:36 +0000 Subject: [PATCH 0454/2056] Bump ruff from 0.15.4 to 0.15.5 (#14565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70dd9d0e14..b036da6ef16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 6b2617b6563..93a20896aa8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.4 # also change in .pre-commit-config.yaml when updating +ruff==0.15.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d8deb2255d3a982bb3a755efa9291a2a59014c69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:18:09 -0500 Subject: [PATCH 0455/2056] [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/mipi_rgb/mipi_rgb.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 7ff6868c15f..ae7c7958464 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -288,9 +288,7 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { if (!this->check_buffer_()) return; size_t pos = (y * this->width_) + x; - uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); if (this->buffer_[pos] == new_color) return; this->buffer_[pos] = new_color; @@ -315,10 +313,12 @@ void MipiRgb::fill(Color color) { } auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_); - uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); std::fill_n(ptr_16, this->width_ * this->height_, new_color); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + this->y_high_ = this->height_ - 1; } int MipiRgb::get_width() { From f53ee70caadb75ef3ffe20f2a471d15ba06e34a7 Mon Sep 17 00:00:00 2001 From: AndreKR <haensel@creations.de> Date: Sat, 7 Mar 2026 01:29:20 +0100 Subject: [PATCH 0456/2056] [http_request] Make TLS buffer configurable on ESP8266 (#14009) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/http_request/__init__.py | 10 ++++++++++ .../components/http_request/http_request_arduino.cpp | 8 +++----- esphome/components/http_request/http_request_arduino.h | 10 ++++++++++ tests/components/http_request/test.esp8266-ard.yaml | 8 +++++--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 2d6ecae0bcd..81337ebdf6e 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -50,6 +50,8 @@ CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_TX = "buffer_size_tx" +CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx" +CONF_TLS_BUFFER_SIZE_TX = "tls_buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" @@ -124,6 +126,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( cv.uint16_t, cv.only_on_esp32 ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_RX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_TX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), @@ -150,6 +158,8 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + cg.add(var.set_tls_buffer_size_rx(config[CONF_TLS_BUFFER_SIZE_RX])) + cg.add(var.set_tls_buffer_size_tx(config[CONF_TLS_BUFFER_SIZE_TX])) if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): cg.add(var.set_watchdog_timeout(timeout_ms)) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 56b51e8b5a3..f0dd6492852 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -18,8 +18,6 @@ namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; #ifdef USE_ESP8266 -static constexpr int RX_BUFFER_SIZE = 512; -static constexpr int TX_BUFFER_SIZE = 512; // ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM static constexpr int ESP8266_SSL_ERR_OOM = -1000; #endif @@ -58,7 +56,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique<WiFiClientSecure>(); WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get()); - secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); + secure_client->setBufferSizes(this->tls_buffer_size_rx_, this->tls_buffer_size_tx_); secure_client->setInsecure(); } else { stream_ptr = std::make_unique<WiFiClient>(); @@ -138,8 +136,8 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur } ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); if (last_error == ESP8266_SSL_ERR_OOM) { - ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), - static_cast<unsigned int>(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + ESP_LOGW(TAG, "Configured TLS buffer sizes: %u/%u bytes, check max free heap block using the debug component", + (unsigned int) this->tls_buffer_size_rx_, (unsigned int) this->tls_buffer_size_tx_); } } else { ESP_LOGW(TAG, "Connection failure with no error code"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d5ce5c0ff3c..b009d45b1ca 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -47,10 +47,20 @@ class HttpContainerArduino : public HttpContainer { }; class HttpRequestArduino : public HttpRequestComponent { + public: +#ifdef USE_ESP8266 + void set_tls_buffer_size_rx(uint16_t size) { this->tls_buffer_size_rx_ = size; } + void set_tls_buffer_size_tx(uint16_t size) { this->tls_buffer_size_tx_ = size; } +#endif + protected: std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, const std::vector<Header> &request_headers, const std::vector<std::string> &lower_case_collect_headers) override; +#ifdef USE_ESP8266 + uint16_t tls_buffer_size_rx_{512}; + uint16_t tls_buffer_size_tx_{512}; +#endif }; } // namespace esphome::http_request diff --git a/tests/components/http_request/test.esp8266-ard.yaml b/tests/components/http_request/test.esp8266-ard.yaml index c1937b5a109..dd2d0df62ba 100644 --- a/tests/components/http_request/test.esp8266-ard.yaml +++ b/tests/components/http_request/test.esp8266-ard.yaml @@ -1,4 +1,6 @@ -substitutions: - verify_ssl: "false" - <<: !include common.yaml + +http_request: + verify_ssl: false + tls_buffer_size_rx: 16384 + tls_buffer_size_tx: 512 From df11e2765ed016dabcdc211b7321289fa7cbeccf Mon Sep 17 00:00:00 2001 From: Ricardo Sanz <me@ricardosa.nz> Date: Sat, 7 Mar 2026 02:00:52 +0100 Subject: [PATCH 0457/2056] [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) --- esphome/components/climate/__init__.py | 2 +- esphome/components/haier/climate.py | 8 ++------ esphome/components/template/water_heater/__init__.py | 2 +- esphome/const.py | 1 + 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 1f449ad2a4b..f5b91c502c7 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, CONF_CURRENT_HUMIDITY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -112,7 +113,6 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 8c3649058f8..6c208f6caa4 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -3,15 +3,11 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart -from esphome.components.climate import ( - CONF_CURRENT_TEMPERATURE, - ClimateMode, - ClimatePreset, - ClimateSwingMode, -) +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, + CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_ID, CONF_LEVEL, diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 71f98c826a7..cb5f2dbe56d 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -4,6 +4,7 @@ from esphome.components import water_heater import esphome.config_validation as cv from esphome.const import ( CONF_AWAY, + CONF_CURRENT_TEMPERATURE, CONF_ID, CONF_MODE, CONF_OPTIMISTIC, @@ -18,7 +19,6 @@ from esphome.types import ConfigType from .. import template_ns -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_IS_ON = "is_on" TemplateWaterHeater = template_ns.class_( diff --git a/esphome/const.py b/esphome/const.py index 060e9625739..88e3c33fbc6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -274,6 +274,7 @@ CONF_CURRENT = "current" CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" +CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" From 9b489c9eba639b8dea29dec7815c32c22cda28a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:52:51 +0000 Subject: [PATCH 0458/2056] Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580) 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 f111e05a9df..8e875eba622 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.2.0 +aioesphomeapi==44.3.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 05ae69b766e9745892e97c8ac40d7017d5fecb3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 19:00:37 -1000 Subject: [PATCH 0459/2056] [api] Sync api.proto from aioesphomeapi (#14579) --- esphome/components/api/api.proto | 132 ++++++++++++++ esphome/components/api/api_pb2.cpp | 161 +++++++++++++++++ esphome/components/api/api_pb2.h | 200 ++++++++++++++++++++- esphome/components/api/api_pb2_dump.cpp | 117 ++++++++++++ esphome/components/api/api_pb2_service.cpp | 66 +++++++ esphome/components/api/api_pb2_service.h | 21 +++ 6 files changed, 696 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 802e3e3ae21..618fd1b83c9 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -58,6 +58,7 @@ service APIConnection { rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} @@ -69,6 +70,12 @@ service APIConnection { rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} + + rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {} + rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} + rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} + rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} + rpc serial_proxy_request(SerialProxyRequest) returns (void) {} } @@ -198,6 +205,17 @@ message DeviceInfo { uint32 area_id = 3; } +enum SerialProxyPortType { + SERIAL_PROXY_PORT_TYPE_TTL = 0; + SERIAL_PROXY_PORT_TYPE_RS232 = 1; + SERIAL_PROXY_PORT_TYPE_RS485 = 2; +} + +message SerialProxyInfo { + string name = 1; // Human-readable port name + SerialProxyPortType port_type = 2; // Port type (RS232, RS485) +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -260,6 +278,9 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + + // Serial proxy instance metadata + repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; } message ListEntitiesRequest { @@ -2517,3 +2538,114 @@ message InfraredRFReceiveEvent { fixed32 key = 2; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } + +// ==================== SERIAL PROXY ==================== + +enum SerialProxyParity { + SERIAL_PROXY_PARITY_NONE = 0; + SERIAL_PROXY_PARITY_EVEN = 1; + SERIAL_PROXY_PARITY_ODD = 2; +} + +// Configure UART parameters for a serial proxy instance +message SerialProxyConfigureRequest { + option (id) = 138; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 baudrate = 2; // Baud rate in bits per second + bool flow_control = 3; // Enable hardware flow control + SerialProxyParity parity = 4; // Parity setting + uint32 stop_bits = 5; // Number of stop bits (1 or 2) + uint32 data_size = 6; // Number of data bits (5-8) +} + +// Data received from a serial device, forwarded to clients +message SerialProxyDataReceived { + option (id) = 139; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data received from the serial device +} + +// Write data to a serial device +message SerialProxyWriteRequest { + option (id) = 140; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data to write to the serial device +} + +// Set modem control pin states (RTS and DTR) +message SerialProxySetModemPinsRequest { + option (id) = 141; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +// Request current modem control pin states +message SerialProxyGetModemPinsRequest { + option (id) = 142; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) +} + +// Response with current modem control pin states +message SerialProxyGetModemPinsResponse { + option (id) = 143; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +enum SerialProxyRequestType { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) +} + +// Generic request message for simple serial proxy operations +message SerialProxyRequest { + option (id) = 144; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Request type +} + +// ==================== BLUETOOTH CONNECTION PARAMS ==================== +message BluetoothSetConnectionParamsRequest { + option (id) = 145; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 min_interval = 2; // units of 1.25ms + uint32 max_interval = 3; // units of 1.25ms + uint32 latency = 4; + uint32 timeout = 5; // units of 10ms +} + +message BluetoothSetConnectionParamsResponse { + option (id) = 146; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + int32 error = 2; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d8703aa416e..b1176de539a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -71,6 +71,18 @@ uint32_t DeviceInfo::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_uint32(2, static_cast<uint32_t>(this->port_type)); +} +uint32_t SerialProxyInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->port_type)); + return size; +} +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); @@ -125,6 +137,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(24, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + buffer.encode_message(25, it); + } +#endif } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -180,6 +197,11 @@ uint32_t DeviceInfoResponse::calculate_size() const { #endif #ifdef USE_ZWAVE_PROXY size += ProtoSize::calc_uint32(2, this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + size += ProtoSize::calc_message_force(2, it.calculate_size()); + } #endif return size; } @@ -3714,5 +3736,144 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->baudrate = value.as_uint32(); + break; + case 3: + this->flow_control = value.as_bool(); + break; + case 4: + this->parity = static_cast<enums::SerialProxyParity>(value.as_uint32()); + break; + case 5: + this->stop_bits = value.as_uint32(); + break; + case 6: + this->data_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +} +uint32_t SerialProxyDataReceived::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_length(1, this->data_len_); + return size; +} +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->line_states = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, this->line_states); +} +uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, this->line_states); + return size; +} +bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->type = static_cast<enums::SerialProxyRequestType>(value.as_uint32()); + break; + default: + return false; + } + return true; +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->address = value.as_uint64(); + break; + case 2: + this->min_interval = value.as_uint32(); + break; + case 3: + this->max_interval = value.as_uint32(); + break; + case 4: + this->latency = value.as_uint32(); + break; + case 5: + this->timeout = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_int32(2, this->error); +} +uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_int32(1, this->error); + return size; +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 89cb1158f33..a6167dc8101 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -11,6 +11,11 @@ namespace esphome::api { namespace enums { +enum SerialProxyPortType : uint32_t { + SERIAL_PROXY_PORT_TYPE_TTL = 0, + SERIAL_PROXY_PORT_TYPE_RS232 = 1, + SERIAL_PROXY_PORT_TYPE_RS485 = 2, +}; enum EntityCategory : uint32_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -317,6 +322,18 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif +#ifdef USE_SERIAL_PROXY +enum SerialProxyParity : uint32_t { + SERIAL_PROXY_PARITY_NONE = 0, + SERIAL_PROXY_PARITY_EVEN = 1, + SERIAL_PROXY_PARITY_ODD = 2, +}; +enum SerialProxyRequestType : uint32_t { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, +}; +#endif } // namespace enums @@ -477,10 +494,24 @@ class DeviceInfo final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyInfo final : public ProtoMessage { + public: + StringRef name{}; + enums::SerialProxyPortType port_type{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 255; + static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -532,6 +563,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; +#endif +#ifdef USE_SERIAL_PROXY + std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{}; #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -3061,5 +3095,169 @@ class InfraredRFReceiveEvent final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyConfigureRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 138; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_configure_request"; } +#endif + uint32_t instance{0}; + uint32_t baudrate{0}; + bool flow_control{false}; + enums::SerialProxyParity parity{}; + uint32_t stop_bits{0}; + uint32_t data_size{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyDataReceived final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 139; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_data_received"; } +#endif + uint32_t instance{0}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyWriteRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 140; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_write_request"; } +#endif + uint32_t instance{0}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 141; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 142; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } +#endif + uint32_t instance{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 143; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 144; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif +#ifdef USE_BLUETOOTH_PROXY +class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_request"; } +#endif + uint64_t address{0}; + uint32_t min_interval{0}; + uint32_t max_interval{0}; + uint32_t latency{0}; + uint32_t timeout{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothSetConnectionParamsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 146; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_response"; } +#endif + uint64_t address{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4eec42e936c..086b1bdc2f0 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint out.append(hex_buf).append("\n"); } +template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) { + switch (value) { + case enums::SERIAL_PROXY_PORT_TYPE_TTL: + return "SERIAL_PROXY_PORT_TYPE_TTL"; + case enums::SERIAL_PROXY_PORT_TYPE_RS232: + return "SERIAL_PROXY_PORT_TYPE_RS232"; + case enums::SERIAL_PROXY_PORT_TYPE_RS485: + return "SERIAL_PROXY_PORT_TYPE_RS485"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -752,6 +764,32 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums: } } #endif +#ifdef USE_SERIAL_PROXY +template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) { + switch (value) { + case enums::SERIAL_PROXY_PARITY_NONE: + return "SERIAL_PROXY_PARITY_NONE"; + case enums::SERIAL_PROXY_PARITY_EVEN: + return "SERIAL_PROXY_PARITY_EVEN"; + case enums::SERIAL_PROXY_PARITY_ODD: + return "SERIAL_PROXY_PARITY_ODD"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) { + switch (value) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + default: + return "UNKNOWN"; + } +} +#endif const char *HelloRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HelloRequest"); @@ -801,6 +839,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyInfo"); + dump_field(out, "name", this->name); + dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type)); + return out.c_str(); +} +#endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); dump_field(out, "name", this->name); @@ -861,6 +907,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif #ifdef USE_ZWAVE_PROXY dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + out.append(" serial_proxies: "); + it.dump_to(out); + out.append("\n"); + } #endif return out.c_str(); } @@ -2510,6 +2563,70 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "baudrate", this->baudrate); + dump_field(out, "flow_control", this->flow_control); + dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity)); + dump_field(out, "stop_bits", this->stop_bits); + dump_field(out, "data_size", this->data_size); + return out.c_str(); +} +const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyDataReceived"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + return out.c_str(); +} +const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyWriteRequest"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data, this->data_len); + return out.c_str(); +} +const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); + dump_field(out, "instance", this->instance); + return out.c_str(); +} +const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); + return out.c_str(); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); + dump_field(out, "address", this->address); + dump_field(out, "min_interval", this->min_interval); + dump_field(out, "max_interval", this->max_interval); + dump_field(out, "latency", this->latency); + dump_field(out, "timeout", this->timeout); + return out.c_str(); +} +const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); + dump_field(out, "address", this->address); + dump_field(out, "error", this->error); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f9151ae3b46..f2f7fa5238b 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_infrared_rf_transmit_raw_timings_request(msg); break; } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyConfigureRequest::MESSAGE_TYPE: { + SerialProxyConfigureRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg); +#endif + this->on_serial_proxy_configure_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyWriteRequest::MESSAGE_TYPE: { + SerialProxyWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg); +#endif + this->on_serial_proxy_write_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxySetModemPinsRequest::MESSAGE_TYPE: { + SerialProxySetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg); +#endif + this->on_serial_proxy_set_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: { + SerialProxyGetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg); +#endif + this->on_serial_proxy_get_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyRequest::MESSAGE_TYPE: { + SerialProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg); +#endif + this->on_serial_proxy_request(msg); + break; + } +#endif +#ifdef USE_BLUETOOTH_PROXY + case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: { + BluetoothSetConnectionParamsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg); +#endif + this->on_bluetooth_set_connection_params_request(msg); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e70b97196b4..a031d2d969e 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -216,6 +216,27 @@ class APIServerConnectionBase : public ProtoService { virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; +#endif + protected: void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; From cbebb811965d63f115ea4fba5c8fd7f6da393b29 Mon Sep 17 00:00:00 2001 From: rwrozelle <rwrozelle@gmail.com> Date: Sat, 7 Mar 2026 08:12:27 -0800 Subject: [PATCH 0460/2056] [openthread] move esp functions into correct file (#14588) --- esphome/components/openthread/openthread.cpp | 9 ++------- esphome/components/openthread/openthread.h | 2 ++ esphome/components/openthread/openthread_esp.cpp | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index fb814812997..1596b6e9908 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,7 +1,6 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#include "esp_openthread.h" #include <freertos/portmacro.h> @@ -51,7 +50,7 @@ void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) auto *self = static_cast<OpenThreadComponent *>(context); // This runs on the OpenThread task thread with the OT lock held, // so we can safely call otThreadGetDeviceRole directly. - otInstance *instance = esp_openthread_get_instance(); + otInstance *instance = self->get_openthread_instance_(); otDeviceRole role = otThreadGetDeviceRole(instance); self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } @@ -233,16 +232,12 @@ bool OpenThreadComponent::teardown() { otSrpClientClearHostAndServices(instance); otSrpClientBuffersFreeAllServices(instance); global_openthread_component = nullptr; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) ESP_LOGD(TAG, "Exit main loop "); - int error = esp_openthread_mainloop_exit(); + int error = this->openthread_stop_(); if (error != ESP_OK) { ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); this->teardown_complete_ = true; } -#else - this->teardown_complete_ = true; -#endif } return this->teardown_complete_; } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index c87f4fa7c10..75d8fe11fd7 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -44,6 +44,8 @@ class OpenThreadComponent : public Component { protected: std::optional<otIp6Address> get_omr_address_(InstanceLock &lock); static void on_state_changed_(otChangedFlags flags, void *context); + otInstance *get_openthread_instance_(); + int openthread_stop_(); std::function<void()> factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index cdc7a404b2d..9cc9223b523 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -190,6 +190,8 @@ void OpenThreadComponent::ot_main() { vTaskDelete(NULL); } +int OpenThreadComponent::openthread_stop_() { return esp_openthread_mainloop_exit(); } + network::IPAddresses OpenThreadComponent::get_ip_addresses() { network::IPAddresses addresses; struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -204,6 +206,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { return addresses; } +// not thread safe, only use in read-only use cases +otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } + std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); From 0e106d843c730eb15e083ebd0147124dadc99a5a Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sat, 7 Mar 2026 17:18:21 +0100 Subject: [PATCH 0461/2056] [nrf52][zephyr] support for multi on rate callbacks (#14557) --- esphome/components/nrf52/__init__.py | 3 +++ esphome/components/nrf52/dfu.cpp | 35 ++++++++------------------- esphome/components/nrf52/dfu.h | 1 - esphome/components/zephyr/__init__.py | 20 +++++++++++---- esphome/components/zephyr/cdc_acm.cpp | 30 +++++++++++++++++++++++ esphome/components/zephyr/cdc_acm.h | 27 +++++++++++++++++++++ esphome/components/zephyr/const.py | 2 ++ 7 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 esphome/components/zephyr/cdc_acm.cpp create mode 100644 esphome/components/zephyr/cdc_acm.h diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a12d1db1ab7..0a9fb5939a2 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -20,8 +20,10 @@ from esphome.components.zephyr import ( ) from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOOTLOADER, KEY_ZEPHYR, + CdcAcm, ) import esphome.config_validation as cv from esphome.const import ( @@ -159,6 +161,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), } ), set_framework, diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index 9e493734670..c2017248d20 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -2,42 +2,27 @@ #ifdef USE_NRF52_DFU -#include <zephyr/device.h> -#include <zephyr/drivers/uart.h> -#include <zephyr/drivers/uart/cdc_acm.h> #include "esphome/core/log.h" +#include "esphome/components/zephyr/cdc_acm.h" namespace esphome { namespace nrf52 { static const char *const TAG = "dfu"; -volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS -#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), - -static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { - if (rate == 1200) { - goto_dfu = true; - } -} void DeviceFirmwareUpdate::setup() { this->reset_pin_->setup(); - const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; - for (auto &idx : cdc_dev) { - cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); - } -} - -void DeviceFirmwareUpdate::loop() { - if (goto_dfu) { - goto_dfu = false; - volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; - (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; - this->reset_pin_->digital_write(true); - } +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) { + if (rate == 1200) { + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } + }); +#endif } void DeviceFirmwareUpdate::dump_config() { diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h index 979a4567cf5..71060e43c18 100644 --- a/esphome/components/nrf52/dfu.h +++ b/esphome/components/nrf52/dfu.h @@ -10,7 +10,6 @@ namespace nrf52 { class DeviceFirmwareUpdate : public Component { public: void setup() override; - void loop() override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void dump_config() override; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 4cc71bddca8..b8a091feb92 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -5,11 +5,13 @@ from typing import TypedDict import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.types import ConfigType from .const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, @@ -54,7 +56,7 @@ class ZephyrData(TypedDict): user: dict[str, list[str]] -def zephyr_set_core_data(config): +def zephyr_set_core_data(config: ConfigType) -> None: CORE.data[KEY_ZEPHYR] = ZephyrData( board=config[CONF_BOARD], bootloader=config[KEY_BOOTLOADER], @@ -64,7 +66,6 @@ def zephyr_set_core_data(config): pm_static=[], user={}, ) - return config def zephyr_data() -> ZephyrData: @@ -110,7 +111,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: cg.add_platformio_option("extra_scripts", [key]) -def zephyr_to_code(config): +def zephyr_to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ZEPHYR") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") @@ -132,6 +133,15 @@ def zephyr_to_code(config): Path(__file__).parent / "pre_build.py.script", ) + CORE.add_job(_cdc_acm_to_code, config) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _cdc_acm_to_code(config: ConfigType) -> None: + if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF]: + var = cg.new_Pvariable(config[CONF_CDC_ACM]) + await cg.register_component(var, {}) + def zephyr_setup_preferences(): cg.add(zephyr_ns.setup_preferences()) @@ -151,7 +161,7 @@ def _format_prj_conf_val(value: PrjConfValueType) -> str: raise ValueError -def zephyr_add_cdc_acm(config, id): +def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None: framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] if CORE.is_nrf52 and framework_ver >= cv.Version(3, 2, 0): zephyr_add_prj_conf("CONFIG_USB_DEVICE_STACK_NEXT", False) diff --git a/esphome/components/zephyr/cdc_acm.cpp b/esphome/components/zephyr/cdc_acm.cpp new file mode 100644 index 00000000000..04ee9a0bef6 --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.cpp @@ -0,0 +1,30 @@ +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) +#include "cdc_acm.h" +#include <zephyr/drivers/uart.h> +#include <zephyr/drivers/uart/cdc_acm.h> + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +namespace esphome::zephyr { + +CdcAcm::CdcAcm() { global_cdc_acm = this; } + +void CdcAcm::setup() { +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + // only one global callback can be registered + cdc_acm_dte_rate_callback_set(idx, CdcAcm::cdc_dte_rate_callback_); + } +#endif // DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) +} + +void CdcAcm::cdc_dte_rate_callback_(const struct device *device, uint32_t rate) { + global_cdc_acm->defer([device, rate]() { global_cdc_acm->rate_callbacks_.call(device, rate); }); +} + +CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h new file mode 100644 index 00000000000..2e9da85a111 --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.h @@ -0,0 +1,27 @@ +#pragma once +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include <zephyr/device.h> + +namespace esphome::zephyr { + +class CdcAcm : public Component { + public: + CdcAcm(); + void setup() override; + void add_on_rate_callback(std::function<void(const device *, uint32_t)> &&callback) { + this->rate_callbacks_.add(std::move(callback)); + } + + protected: + static void cdc_dte_rate_callback_(const device *device, uint32_t rate); + CallbackManager<void(const device *, uint32_t)> rate_callbacks_; +}; + +extern CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py index 06a4fc42bcf..f67b058ed78 100644 --- a/esphome/components/zephyr/const.py +++ b/esphome/components/zephyr/const.py @@ -14,3 +14,5 @@ KEY_BOARD: Final = "board" KEY_USER: Final = "user" zephyr_ns = cg.esphome_ns.namespace("zephyr") +CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component) +CONF_CDC_ACM = "cdc_acm" From 8b62c35ea7831d2c0e24f836e096b7b4c1cb1ca0 Mon Sep 17 00:00:00 2001 From: Simon Redman <simon@ergotech.com> Date: Sat, 7 Mar 2026 11:41:37 -0500 Subject: [PATCH 0462/2056] [uart] Add error message when initializing UART with unsupported configuration (#13229) --- esphome/components/uart/uart_component_libretiny.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index cb4465068d8..83d2acb332d 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -110,7 +110,7 @@ void LibreTinyUARTComponent::setup() { #if LT_HW_UART2 ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX); #endif - this->mark_failed(); + this->mark_failed(LOG_STR("SoftwareSerial is not implemented for this chip.")); return; #endif } From 15ffbb0b05da71f6ef008c0a38ffd185d0490edc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:51:02 -0500 Subject: [PATCH 0463/2056] [uart] Fully enable raw mode with host serial (#14573) --- esphome/components/uart/uart_component_host.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0e5ef3c6bd3..9dce25c500e 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -124,17 +124,10 @@ void HostUartComponent::setup() { fcntl(this->file_descriptor_, F_SETFL, 0); struct termios options; tcgetattr(this->file_descriptor_, &options); + cfmakeraw(&options); options.c_cflag &= ~CRTSCTS; options.c_cflag |= CREAD | CLOCAL; - options.c_lflag &= ~ICANON; - options.c_lflag &= ~ECHO; - options.c_lflag &= ~ECHOE; - options.c_lflag &= ~ECHONL; - options.c_lflag &= ~ISIG; - options.c_iflag &= ~(IXON | IXOFF | IXANY); - options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); - options.c_oflag &= ~OPOST; - options.c_oflag &= ~ONLCR; + options.c_iflag &= ~(IXOFF | IXANY); // Set data bits options.c_cflag &= ~CSIZE; // Mask the character size bits switch (this->data_bits_) { From abc870006cce42296a2f581478f79fbe69df2391 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:25:13 -1000 Subject: [PATCH 0464/2056] [captive_portal] Enable support for RP2040 (#14505) --- esphome/components/captive_portal/__init__.py | 9 ++++----- tests/components/captive_portal/test.rp2040-ard.yaml | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/components/captive_portal/test.rp2040-ard.yaml diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 6c190814c03..cd877fc8799 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, ) @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), @@ -103,11 +105,8 @@ async def to_code(config): if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino: - if CORE.is_esp8266: - cg.add_library("DNSServer", None) - if CORE.is_libretiny: - cg.add_library("DNSServer", None) + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + cg.add_library("DNSServer", None) # Only compile the ESP-IDF DNS server when using ESP-IDF framework diff --git a/tests/components/captive_portal/test.rp2040-ard.yaml b/tests/components/captive_portal/test.rp2040-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/captive_portal/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From f57fa4cc8d66a99f1e6bef885e2f0c8f96280c04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:25:33 -1000 Subject: [PATCH 0465/2056] [bluetooth_proxy] Add BLE connection parameters API (#14577) --- esphome/components/api/api_connection.cpp | 3 ++ esphome/components/api/api_connection.h | 1 + .../bluetooth_proxy/bluetooth_connection.h | 4 +++ .../bluetooth_proxy/bluetooth_proxy.cpp | 29 +++++++++++++++++++ .../bluetooth_proxy/bluetooth_proxy.h | 3 ++ .../esp32_ble_client/ble_client_base.cpp | 5 ++-- .../esp32_ble_client/ble_client_base.h | 4 +-- 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8721072e499..bd3de028951 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1188,6 +1188,9 @@ void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScanner bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } +void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg); +} #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 54b6db68000..b075bc83ab2 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -148,6 +148,7 @@ class APIConnection final : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; void on_subscribe_bluetooth_connections_free_request() override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 60bbc93e8b4..b50ea2d6a22 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); + } + void set_address(uint64_t address) override; protected: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 21da4ead144..87206996b27 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include <algorithm> #include <cstring> +#include <limits> #ifdef USE_ESP32 @@ -361,6 +363,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest } } +void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) { + if (this->api_connection_ == nullptr) + return; + + auto *connection = this->get_connection_(msg.address, false); + api::BluetoothSetConnectionParamsResponse resp; + resp.address = msg.address; + + if (connection == nullptr || !connection->connected()) { + ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", + connection ? static_cast<int>(connection->connection_index_) : -1, + connection ? connection->address_str() : "unknown"); + resp.error = ESP_GATT_NOT_CONNECTED; + this->api_connection_->send_message(resp); + return; + } + + // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values; + // clamp to uint16_t since the current BLE spec defines these as 16-bit. + constexpr uint32_t max_val = std::numeric_limits<uint16_t>::max(); + resp.error = connection->update_connection_params(static_cast<uint16_t>(std::min(msg.min_interval, max_val)), + static_cast<uint16_t>(std::min(msg.max_interval, max_val)), + static_cast<uint16_t>(std::min(msg.latency, max_val)), + static_cast<uint16_t>(std::min(msg.timeout, max_val))); + this->api_connection_->send_message(resp); +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 85461755aac..f1b723e7192 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -46,6 +46,7 @@ enum BluetoothProxyFeature : uint32_t { FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_STATE_AND_MODE = 1 << 6, + FEATURE_CONNECTION_PARAMS_SETTING = 1 << 7, }; enum BluetoothProxySubscriptionFlag : uint32_t { @@ -82,6 +83,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); @@ -130,6 +132,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_PAIRING; flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + flags |= BluetoothProxyFeature::FEATURE_CONNECTION_PARAMS_SETTING; } return flags; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6a85c784a9..2f17334c77c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,8 +236,8 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout, const char *param_type) { +esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = min_interval; @@ -249,6 +249,7 @@ void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_inte if (err != ESP_OK) { this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); } + return err; } void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b23498..af4f1b30290 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -129,8 +129,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); void log_gattc_data_event_(const char *name); - void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, - const char *param_type); + esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); From 45f20d9c06119e3c02aa8e8be131297c032da4e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:01 -1000 Subject: [PATCH 0466/2056] [core] Merge set_name + set_entity_strings into configure_entity_ (#14444) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/core/entity_base.cpp | 15 ++- esphome/core/entity_base.h | 30 ++--- esphome/core/entity_helpers.py | 17 ++- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/sensor/test_sensor.py | 15 ++- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 25 ++++- tests/unit_tests/core/test_entity_helpers.py | 103 +++++++----------- 9 files changed, 112 insertions(+), 99 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 5c4e1c44459..3274640eb34 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -10,8 +10,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { this->set_name(name, 0); } -void EntityBase::set_name(const char *name, uint32_t object_id_hash) { + +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,6 +44,17 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->calc_object_id_(); } } + // Unpack entity string table indices. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = entity_strings_packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; +#endif } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 20eb68b67a7..accd532b0d0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,10 @@ #include "device.h" #endif +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Extern lookup functions for entity string tables. @@ -54,12 +58,8 @@ enum EntityCategory : uint8_t { // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: - // Get/set the name of this Entity + // Get the name of this Entity const StringRef &get_name() const; - void set_name(const char *name); - /// Set name with pre-computed object_id hash (avoids runtime hash calculation) - /// Use hash=0 for dynamic names that need runtime calculation - void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -104,20 +104,6 @@ class EntityBase { this->flags_.entity_category = static_cast<uint8_t>(entity_category); } - // Set entity string table indices — one call per entity from codegen. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) - void set_entity_strings([[maybe_unused]] uint32_t packed) { -#ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = packed & 0xFF; -#endif -#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (packed >> 8) & 0xFF; -#endif -#ifdef USE_ENTITY_ICON - this->icon_idx_ = (packed >> 16) & 0xFF; -#endif - } - // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. @@ -239,6 +225,12 @@ class EntityBase { } protected: + friend void ::setup(); + friend void ::original_setup(); + + /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index a46d2466fdf..4fa109fb0e1 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -31,8 +31,10 @@ DOMAIN = "entity_string_pool" _KEY_DC_IDX = "_entity_dc_idx" _KEY_UOM_IDX = "_entity_uom_idx" _KEY_ICON_IDX = "_entity_icon_idx" +_KEY_ENTITY_NAME = "_entity_name" +_KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: # [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) _DC_SHIFT = 0 _UOM_SHIFT = 8 @@ -219,17 +221,18 @@ def setup_unit_of_measurement(config: ConfigType) -> None: def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single set_entity_strings() call with all packed indices. + """Emit a single configure_entity_() call with name, hash, and packed string indices. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. """ + entity_name = config[_KEY_ENTITY_NAME] + object_id_hash = config[_KEY_OBJECT_ID_HASH] dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - if packed != 0: - add(var.set_entity_strings(packed)) + add(var.configure_entity_(entity_name, object_id_hash, packed)) def get_base_entity_object_id( @@ -331,13 +334,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Set the entity name with pre-computed object_id hash + # Pre-compute entity name and object_id hash for configure_entity_() + # which is emitted later by finalize_entity_strings(). # For named entities: pre-compute hash from entity name # For empty-name entities: pass 0, C++ calculates hash at runtime from # device name, friendly_name, or app name (bug-for-bug compatibility) entity_name = config[CONF_NAME] object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 - add(var.set_name(entity_name, object_id_hash)) + config[_KEY_ENTITY_NAME] = entity_name + config[_KEY_OBJECT_ID_HASH] = object_id_hash # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index ce4e64681fe..fbc2f37d9a1 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1",' in main_cpp + assert 'bs_1->configure_entity_("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 797b6fb1a42..9f94d61c8c4 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1",' in main_cpp + assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 221e7edf2c3..d9ab3a022c8 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,5 +1,15 @@ """Tests for the sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_sensor_device_class_set(generate_main): """ @@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") - # Then - assert "s_1->set_entity_strings(" in main_cpp + # Then: device_class: voltage means packed value must be non-zero + packed = _extract_packed_value(main_cpp, "s_1") + assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 16f5f980a5f..3ceaa9b8f81 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text",' in main_cpp + assert 'it_1->configure_entity_("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 4aaebe04d1c..f30b820e94d 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,5 +1,15 @@ """Tests for the text sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_text_sensor_is_setup(generate_main): """ @@ -25,9 +35,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp + assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp + assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp + assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): @@ -53,6 +63,9 @@ def test_text_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_entity_strings(" in main_cpp - assert "ts_3->set_entity_strings(" in main_cpp + # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date + # so their packed values must be non-zero + packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + assert packed_ts_2 != 0 + packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 1392a1d0436..3f6faaee54c 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -32,9 +32,11 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting names from set_name calls -# Matches: .set_name("name", hash) or .set_name("name") -SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') +# Pre-compiled regex pattern for extracting names from configure_entity_/set_name calls +# Matches: .configure_entity_("name", ...) or .set_name("name", ...) +ENTITY_NAME_PATTERN = re.compile( + r'\.(?:configure_entity_|set_name)\(["\']([^"\']*)["\']' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -276,15 +278,23 @@ def setup_test_environment() -> Generator[list[str], None, None]: entity_helpers.add = original_add -def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that would be computed from set_name calls. +def extract_object_id_from_config(config: dict[str, Any]) -> str | None: + """Extract the object ID from config keys set by _setup_entity_impl.""" + name = config.get("_entity_name") + if name is None: + return None + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None - Since object_id is now computed from the name (via snake_case + sanitize), - we extract the name from set_name() calls and compute the expected object_id. - For empty names, we fall back to CORE.friendly_name or CORE.name. - """ + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID from configure_entity_() calls in generated expressions.""" for expr in expressions: - if match := SET_NAME_PATTERN.search(expr): + if match := ENTITY_NAME_PATTERN.search(expr): name = match.group(1) if name: return sanitize(snake_case(name)) @@ -299,8 +309,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None: async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: """Test setup_entity with unique names.""" - added_expressions = setup_test_environment - # Create mock entities var1 = MockObj("sensor1") var2 = MockObj("sensor2") @@ -312,13 +320,10 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> } await _setup_entity_impl(var1, config1, "sensor") - # Get object ID from first entity - object_id1 = extract_object_id_from_expressions(added_expressions) + # Get object ID from first entity (stored in config, emitted later by finalize) + object_id1 = extract_object_id_from_config(config1) assert object_id1 == "temperature" - # Clear for next entity - added_expressions.clear() - # Set up second entity with different name config2 = { CONF_NAME: "Humidity", @@ -327,7 +332,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity - object_id2 = extract_object_id_from_expressions(added_expressions) + object_id2 = extract_object_id_from_config(config2) assert object_id2 == "humidity" @@ -337,8 +342,6 @@ async def test_setup_entity_different_platforms( ) -> None: """Test that same name on different platforms doesn't conflict.""" - added_expressions = setup_test_environment - # Create mock entities sensor = MockObj("sensor1") binary_sensor = MockObj("binary_sensor1") @@ -356,15 +359,11 @@ async def test_setup_entity_different_platforms( (text_sensor, "text_sensor"), ] - object_ids: list[str] = [] for var, platform in platforms: - added_expressions.clear() await _setup_entity_impl(var, config, platform) - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) - # All should get base object ID without suffix - assert all(obj_id == "status" for obj_id in object_ids) + # All should get the same object ID (name stored in config, not platform-specific) + assert extract_object_id_from_config(config) == "status" @pytest.fixture @@ -389,7 +388,6 @@ async def test_setup_entity_with_devices( setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] ) -> None: """Test that same name on different devices doesn't conflict.""" - added_expressions = setup_test_environment # Create mock devices device1_id = ID("device1", type="Device") @@ -418,24 +416,18 @@ async def test_setup_entity_with_devices( } # Get object IDs - object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: - added_expressions.clear() await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) # Both should get base object ID without suffix (different devices) - assert object_ids[0] == "temperature" - assert object_ids[1] == "temperature" + assert extract_object_id_from_config(config1) == "temperature" + assert extract_object_id_from_config(config2) == "temperature" @pytest.mark.asyncio async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: """Test setup_entity with empty entity name.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -445,7 +437,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Should use friendly name assert object_id == "test_device" @@ -456,8 +448,6 @@ async def test_setup_entity_special_characters( ) -> None: """Test setup_entity with names containing special characters.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -466,7 +456,7 @@ async def test_setup_entity_special_characters( } await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Special characters should be sanitized assert object_id == "temperature_sensor_" @@ -476,8 +466,6 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -800,10 +788,9 @@ async def test_setup_entity_empty_name_with_device( # Check that set_device was called assert any("sensor1.set_device" in expr for expr in added_expressions) - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -815,7 +802,6 @@ async def test_setup_entity_empty_name_with_mac_suffix( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -831,10 +817,9 @@ async def test_setup_entity_empty_name_with_mac_suffix( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -847,7 +832,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( at runtime. In this case C++ will hash the empty friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -863,10 +847,9 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -878,7 +861,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from the device name. """ - added_expressions = setup_test_environment # No MAC suffix (either not set or False) CORE.config = {} @@ -896,10 +878,9 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 def test_register_string_overflow() -> None: @@ -976,7 +957,7 @@ async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> No # Direct call mode: await setup_entity(var, config, "camera") await setup_entity(var, config, "camera") - # Should have called set_name + # Should have emitted configure_entity_ object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "my_camera" From 77f2c371b2b20b00d6e064bd5cbc618b2f1e22c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:34 -1000 Subject: [PATCH 0467/2056] [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) --- esphome/components/api/api_pb2.cpp | 28 ++++++------ esphome/components/api/proto.cpp | 71 +++++++++++++++++++++++++++++ esphome/components/api/proto.h | 44 ++++++++---------- script/api_protobuf/api_protobuf.py | 17 +++---- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b1176de539a..1944aac6e87 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -120,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it); + buffer.encode_sub_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it); + buffer.encode_sub_message(21, it); } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area, false); + buffer.encode_optional_sub_message(22, this->area); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -920,13 +920,13 @@ uint32_t HomeassistantServiceMap::calculate_size() const { void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1126,7 +1126,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); } @@ -2133,7 +2133,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it); + buffer.encode_sub_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2264,7 +2264,7 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i]); + buffer.encode_sub_message(1, this->advertisements[i]); } } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { @@ -2343,7 +2343,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2370,7 +2370,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2392,7 +2392,7 @@ uint32_t BluetoothGATTService::calculate_size() const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { @@ -2673,7 +2673,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings, false); + buffer.encode_optional_sub_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } uint32_t VoiceAssistantRequest::calculate_size() const { @@ -2906,7 +2906,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it); + buffer.encode_sub_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index a252907fd7a..1ca6b702ada 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,6 @@ #include "proto.h" #include <cinttypes> +#include <cstring> #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -87,6 +88,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +// Single-pass encode for repeated submessage elements (non-template core). +// Writes field tag, reserves 1 byte for length varint, encodes the submessage body, +// then backpatches the actual length. For the common case (body < 128 bytes), this is +// just a single byte write with no memmove — all current repeated submessage types +// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take +// this fast path. +// +// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT +// characteristic with many descriptors). It is safe because calculate_size() already +// reserved space for the full multi-byte varint — the shift fills that reserved space: +// +// calculate_size() allocates per element: tag + varint_size(body) + body_size +// +// After encode, before memmove (1 byte reserved, body written): +// [tag][__][body ..... body][??] +// ^ ^-- unused byte (v2 space from calculate_size) +// len_pos +// +// After memmove(body_start+1, body_start, body_size): +// [tag][__][__][body ..... body] +// ^ ^-- body shifted forward, fills v2 space exactly +// len_pos +// +// After writing 2-byte varint at len_pos: +// [tag][v1][v2][body ..... body] +// ^-- pos_ = element end, within buffer +void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + this->encode_field_raw(field_id, 2); + // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) + uint8_t *len_pos = this->pos_; + this->debug_check_bounds_(1); + this->pos_++; + uint8_t *body_start = this->pos_; + encode_fn(value, *this); + uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start); + if (body_size < 128) [[likely]] { + // Common case: 1-byte varint, just backpatch + *len_pos = static_cast<uint8_t>(body_size); + return; + } + // Compute extra bytes needed for varint beyond the 1 already reserved + uint8_t extra = ProtoSize::varint(body_size) - 1; + // Shift body forward to make room for the extra varint bytes + this->debug_check_bounds_(extra); + std::memmove(body_start + extra, body_start, body_size); + uint8_t *end = this->pos_ + extra; + // Write the full varint at len_pos + this->pos_ = len_pos; + this->encode_varint_raw(body_size); + this->pos_ = end; +} + +// Non-template core for encode_optional_sub_message. +void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + if (nested_size == 0) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(nested_size); +#ifdef ESPHOME_DEBUG_API + uint8_t *start = this->pos_; + encode_fn(value, *this); + if (static_cast<uint32_t>(this->pos_ - start) != nested_size) + this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); +#else + encode_fn(value, *this); +#endif +} + #ifdef ESPHOME_DEBUG_API void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 702208d9de6..bbdd11b29d0 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -185,7 +185,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 +// Forward declarations for decode_to_message and related encoding helpers class ProtoDecodableMessage; class ProtoMessage; class ProtoSize; @@ -363,12 +363,18 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); - /// Encode a nested message field (force=true for repeated, false for singular) - /// Templated so concrete message type is preserved for direct encode/calculate_size calls. - template<typename T> void encode_message(uint32_t field_id, const T &value, bool force = true); - // Non-template core for encode_message — all buffer work happens here - void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); + /// Single-pass encode for repeated submessage elements. + /// Thin template wrapper; all buffer work is in the non-template core. + template<typename T> void encode_sub_message(uint32_t field_id, const T &value); + /// Encode an optional singular submessage field — skips if empty. + /// Thin template wrapper; all buffer work is in the non-template core. + template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value); + + // Non-template core for encode_sub_message — backpatch approach. + void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + // Non-template core for encode_optional_sub_message. + void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)); std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: @@ -690,26 +696,14 @@ template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &bu static_cast<const T *>(msg)->encode(buf); } -// Implementation of encode_message - must be after ProtoMessage is defined -template<typename T> inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { - this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>, force); +// Thin template wrapper; delegates to non-template core in proto.cpp. +template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { + this->encode_sub_message(field_id, &value, &proto_encode_msg<T>); } -// Non-template core for encode_message -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { - if (msg_length_bytes == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(msg_length_bytes); -#ifdef ESPHOME_DEBUG_API - uint8_t *start = this->pos_; - encode_fn(value, *this); - if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes) - this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); -#else - encode_fn(value, *this); -#endif +// Thin template wrapper; delegates to non-template core. +template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { + this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>); } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 85352689e6b..7ae7063a412 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -690,15 +690,12 @@ class MessageType(TypeInfo): @property def encode_func(self) -> str: - return "encode_message" + return "encode_optional_sub_message" @property def encode_content(self) -> str: - # Singular message fields pass force=false (skip empty messages) - # The default for encode_nested_message is force=true (for repeated fields) - return ( - f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" - ) + # Singular message fields skip encoding when empty + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -1322,9 +1319,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1650,9 +1647,9 @@ class RepeatedTypeInfo(TypeInfo): """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property From e7b8ec18f17b23709719c63e1d05a6b9ecfb0054 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:26:50 -1000 Subject: [PATCH 0468/2056] [api] Inline APIServer::is_connected() for common no-arg path (#14574) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 06816fe3e05..17d69405adf 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -582,11 +582,7 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected(bool state_subscription_only) const { - if (!state_subscription_only) { - return !this->clients_.empty(); - } - +bool APIServer::is_connected_with_state_subscription() const { for (const auto &client : this->clients_) { if (client->flags_.state_subscription) { return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e6c10d15953..e5f371d8a13 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,7 +185,8 @@ class APIServer : public Component, void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); #endif - bool is_connected(bool state_subscription_only = false) const; + bool is_connected() const { return !this->clients_.empty(); } + bool is_connected_with_state_subscription() const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -323,7 +324,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { TEMPLATABLE_VALUE(bool, state_subscription_only) public: bool check(const Ts &...x) override { - return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + if (this->state_subscription_only_.value(x...)) { + return global_api_server->is_connected_with_state_subscription(); + } + return global_api_server->is_connected(); } }; From a0cd35c5fc2b8b1e9ab0db69fd70b90c95e8dbad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 07:27:08 -1000 Subject: [PATCH 0469/2056] [core] Inline status_clear_warning/error fast path (#14571) --- esphome/core/component.cpp | 8 ++------ esphome/core/component.h | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9ff3ec1eb5..2879d4b5ab1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -422,15 +422,11 @@ void Component::status_set_error(const LogString *message) { store_component_error_message(this, LOG_STR_ARG(message), true); } } -void Component::status_clear_warning() { - if ((this->component_state_ & STATUS_LED_WARNING) == 0) - return; +void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_clear_error() { - if ((this->component_state_ & STATUS_LED_ERROR) == 0) - return; +void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 59222dc4f47..7266f57e151 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -251,9 +251,17 @@ class Component { void status_set_error(const char *message); void status_set_error(const LogString *message); - void status_clear_warning(); + void status_clear_warning() { + if ((this->component_state_ & STATUS_LED_WARNING) == 0) + return; + this->status_clear_warning_slow_path_(); + } - void status_clear_error(); + void status_clear_error() { + if ((this->component_state_ & STATUS_LED_ERROR) == 0) + return; + this->status_clear_error_slow_path_(); + } /** Set warning status flag and automatically clear it after a timeout. * @@ -505,6 +513,9 @@ class Component { bool cancel_defer(const char *name); // NOLINT bool cancel_defer(uint32_t id); // NOLINT + void status_clear_warning_slow_path_(); + void status_clear_error_slow_path_(); + // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) From 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:50:44 +0100 Subject: [PATCH 0470/2056] [component] Fix components for compatibility with stricter compilers (#14545) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 +- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- esphome/components/heatpumpir/climate.py | 1 + .../http_request/update/http_request_update.cpp | 3 ++- esphome/components/mlx90393/sensor_mlx90393.cpp | 2 +- esphome/components/whirlpool/whirlpool.cpp | 7 +++++++ esphome/components/whirlpool/whirlpool.h | 12 ++++++------ 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index bbe972b9f33..7fa5370072d 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -451,7 +451,7 @@ void ESP32BLE::loop() { ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT { - esp_ble_gap_cb_param_t *param; + esp_ble_gap_cb_param_t *param = NULL; // clang-format off switch (gap_event) { // All three scan complete events have the same structure with just status diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 5e73e991017..a19f7aa6b00 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -27,7 +27,7 @@ #include "esp_rom_sys.h" #include "esp_idf_version.h" -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0bc67c8b033..d6b0d40cd95 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -209,7 +209,7 @@ void EthernetComponent::setup() { break; } #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: { this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; @@ -374,7 +374,7 @@ void EthernetComponent::dump_config() { eth_type = "IP101"; break; #endif -#ifdef USE_ETHERNET_JL1101 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0d760938d08..7743da77abd 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,7 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.40") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 1900f69a691..c40590af955 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -128,7 +128,8 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>(); this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>(); - for (auto build : root[ESPHOME_F("builds")].as<JsonArray>()) { + auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); + for (auto build : builds_array) { if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index ee52f9b9ab0..01084e50df7 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -160,7 +160,7 @@ bool MLX90393Cls::verify_setting_(MLX90393Setting which) { uint8_t read_value = 0xFF; uint8_t expected_value = 0xFF; uint8_t read_status = -1; - char read_back_str[25] = {0}; + char read_back_str[33] = {0}; switch (which) { case MLX90393_GAIN_SEL: { diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index e9f602e97f4..86209cb7a6b 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -32,6 +32,13 @@ const uint8_t WHIRLPOOL_SWING_MASK = 128; const uint8_t WHIRLPOOL_POWER = 0x04; +WhirlpoolClimate::WhirlpoolClimate() + : climate_ir::ClimateIR( + WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}), + model_(MODEL_DG11J1_3A) {} + void WhirlpoolClimate::transmit_state() { this->last_transmit_time_ = millis(); // setting the time of the last transmission. uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 992b2a7adfd..ada5a36de92 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -19,11 +19,7 @@ const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; class WhirlpoolClimate : public climate_ir::ClimateIR { public: - WhirlpoolClimate() - : climate_ir::ClimateIR(temperature_min_(), temperature_max_(), 1.0f, true, true, - {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, - {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + WhirlpoolClimate(); void setup() override { climate_ir::ClimateIR::setup(); @@ -37,7 +33,11 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } - void set_model(Model model) { this->model_ = model; } + void set_model(Model model) { + this->model_ = model; + this->minimum_temperature_ = temperature_min_(); + this->maximum_temperature_ = temperature_max_(); + } // used to track when to send the power toggle command bool powered_on_assumed; From f2dfb5e1dcfd7bd47677f8d0c75b41bbd0e25bb7 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke <okleinecke@web.de> Date: Sun, 8 Mar 2026 00:16:12 +0100 Subject: [PATCH 0471/2056] [uart][usb_uart] Add debug_prefix option to distinguish multiple defined uarts in log (#14525) --- esphome/components/uart/__init__.py | 13 ++++- esphome/components/uart/uart_debugger.cpp | 18 +++--- esphome/components/uart/uart_debugger.h | 17 ++++-- esphome/components/usb_uart/__init__.py | 5 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- esphome/components/usb_uart/usb_uart.h | 3 + tests/components/uart/test.esp32-c3-idf.yaml | 16 +++++- tests/components/uart/test.esp32-idf.yaml | 60 +++++++++++++++----- tests/components/uart/test.esp8266-ard.yaml | 24 +++++--- tests/components/uart/test.host.yaml | 2 +- tests/components/uart/test.rp2040-ard.yaml | 16 +++++- tests/components/usb_uart/common.yaml | 8 +++ 12 files changed, 138 insertions(+), 48 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 3bc4263b31f..1b976b73a90 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -203,8 +203,10 @@ UART_DIRECTIONS = { # round numbers. AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"} +CONF_DEBUG_PREFIX = "debug_prefix" + # By default, log in hex format when no specific sequence is provided. -DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':');" +DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':', debug_prefix);" DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}] @@ -242,6 +244,7 @@ DEBUG_SCHEMA = cv.Schema( ): automation.validate_automation(), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) @@ -283,7 +286,11 @@ async def debug_to_code(config, parent): for action in config[CONF_SEQUENCE]: await automation.build_automation( trigger, - [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + [ + (UARTDirection, "direction"), + (cg.std_vector.template(cg.uint8), "bytes"), + (cg.StringRef, "debug_prefix"), + ], action, ) cg.add(trigger.set_direction(config[CONF_DIRECTION])) @@ -299,6 +306,8 @@ async def debug_to_code(config, parent): if config[CONF_DUMMY_RECEIVER]: dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) await cg.register_component(dummy, {}) + if debug_prefix := config[CONF_DEBUG_PREFIX]: + cg.add(trigger.set_debug_prefix(debug_prefix)) cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index 5490154d010..e0be599d8db 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -74,7 +74,7 @@ bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } void UARTDebugger::fire_trigger_() { this->is_triggering_ = true; - trigger(this->last_direction_, this->bytes_); + trigger(this->last_direction_, this->bytes_, this->debug_prefix_); this->bytes_.clear(); this->is_triggering_ = false; } @@ -94,7 +94,7 @@ void UARTDummyReceiver::loop() { // TCP connection(s). Without these delays, debug log lines could go // missing when UART devices block the main loop for too long. -void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< "; @@ -110,11 +110,11 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%02X", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes) { +void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix) { std::string res; if (direction == UART_DIRECTION_RX) { res += "<<< \""; @@ -154,11 +154,11 @@ void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes) } } res += '"'; - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -174,11 +174,11 @@ void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uin buf_append_printf(buf, sizeof(buf), 0, "%u", bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } -void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator) { +void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) { std::string res; size_t len = bytes.size(); if (direction == UART_DIRECTION_RX) { @@ -194,7 +194,7 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, buf_append_printf(buf, sizeof(buf), 0, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); res += buf; } - ESP_LOGD(TAG, "%s", res.c_str()); + ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str()); delay(10); } diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index df876559622..da33bea70c0 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -5,6 +5,7 @@ #include <vector> #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/string_ref.h" #include "uart.h" #include "uart_component.h" @@ -17,7 +18,7 @@ namespace esphome::uart { /// 'appropriate time' means exactly, is determined by a number of /// configurable constraints. E.g. when a given number of bytes is gathered /// and/or when no more data has been seen for a given time interval. -class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector<uint8_t>> { +class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector<uint8_t>, StringRef> { public: explicit UARTDebugger(UARTComponent *parent); void loop() override; @@ -41,6 +42,8 @@ class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector /// logging will be triggered. void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + protected: UARTDirection for_direction_; UARTDirection last_direction_{}; @@ -51,6 +54,7 @@ class UARTDebugger : public Component, public Trigger<UARTDirection, std::vector std::vector<uint8_t> after_delimiter_{}; size_t after_delimiter_pos_{}; bool is_triggering_{false}; + StringRef debug_prefix_{}; bool is_my_direction_(UARTDirection direction); bool is_recursive_(); @@ -81,18 +85,21 @@ class UARTDebug { public: /// Log the bytes as hex values, separated by the provided separator /// character. - static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as string values, escaping unprintable characters. - static void log_string(UARTDirection direction, std::vector<uint8_t> bytes); + static void log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix = StringRef()); /// Log the bytes as integer values, separated by the provided separator /// character. - static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); /// Log the bytes as '<binary> (<hex>)' values, separated by the provided /// separator. - static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator); + static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, + StringRef prefix = StringRef()); }; } // namespace esphome::uart diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index f0ee53d0281..eedf590ecae 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -90,6 +90,7 @@ def channel_schema(channels, baud_rate_required): ), cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, + cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, } ) ), @@ -129,6 +130,8 @@ async def to_code(config): cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) + if channel[CONF_DEBUG_PREFIX]: + cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) cg.add(var.add_channel(chvar)) if channel[CONF_DEBUG]: cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index e20bbd02dbc..a1f87384914 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -142,7 +142,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t n = std::min(len - off, BATCH); memcpy(buf, ">>> ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, data + off, n, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", this->debug_prefix_.c_str(), buf); } } #endif @@ -219,7 +219,7 @@ void USBUartComponent::loop() { char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex memcpy(buf, "<<< ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); - ESP_LOGD(TAG, "%s", buf); + ESP_LOGD(TAG, "%s%s", channel->debug_prefix_.c_str(), buf); } #endif diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 0d471e46f63..7290f8a958a 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -3,6 +3,7 @@ #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/uart/uart_component.h" #include "esphome/components/usb_host/usb_host.h" #include "esphome/core/lock_free_queue.h" @@ -114,6 +115,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -138,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; + StringRef debug_prefix_{}; }; class USBUartComponent : public usb_host::USBClient { diff --git a/tests/components/uart/test.esp32-c3-idf.yaml b/tests/components/uart/test.esp32-c3-idf.yaml index b053290a8bf..2eae37e8245 100644 --- a/tests/components/uart/test.esp32-c3-idf.yaml +++ b/tests/components/uart/test.esp32-c3-idf.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 flow_control_pin: 6 @@ -16,3 +20,9 @@ uart: rx_timeout: 1 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 18 + rx_pin: 19 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 2a97f9a5de0..fa76316b9c5 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -1,13 +1,19 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] - - uart.write: !lambda |- - return {0xAA, 0xBB, 0xCC}; + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - - id: uart_uart + - id: uart_id tx_pin: 17 rx_pin: 16 flow_control_pin: 4 @@ -18,32 +24,54 @@ uart: rx_timeout: 1 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 21 + rx_pin: 22 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " + - id: uart_debug_custom + tx_pin: 25 + rx_pin: 26 + baud_rate: 9600 + debug: + debug_prefix: "[UART2] " + after: + delimiter: "\n" + sequence: + - lambda: UARTDebug::log_string(direction, bytes, debug_prefix); + - id: uart_debug_no_prefix + tx_pin: 32 + rx_pin: 33 + baud_rate: 9600 + debug: packet_transport: - platform: uart + uart_id: uart_id switch: # Test uart switch with single state (array) - platform: uart name: "UART Switch Single Array" - uart_id: uart_uart + uart_id: uart_id data: [0x01, 0x02, 0x03] # Test uart switch with single state (string) - platform: uart name: "UART Switch Single String" - uart_id: uart_uart + uart_id: uart_id data: "ON" # Test uart switch with turn_on/turn_off (arrays) - platform: uart name: "UART Switch Dual Array" - uart_id: uart_uart + uart_id: uart_id data: turn_on: [0xA0, 0xA1, 0xA2] turn_off: [0xB0, 0xB1, 0xB2] # Test uart switch with turn_on/turn_off (strings) - platform: uart name: "UART Switch Dual String" - uart_id: uart_uart + uart_id: uart_id data: turn_on: "TURN_ON" turn_off: "TURN_OFF" @@ -61,24 +89,26 @@ button: # Test uart button with array data - platform: uart name: "UART Button Array" - uart_id: uart_uart + uart_id: uart_id data: [0xFF, 0xEE, 0xDD] # Test uart button with string data - platform: uart name: "UART Button String" - uart_id: uart_uart + uart_id: uart_id data: "BUTTON_PRESS" # Test uart button with lambda (function pointer) - platform: template name: "UART Lambda Test" on_press: - - uart.write: !lambda |- - std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; - return std::vector<uint8_t>(cmd.begin(), cmd.end()); + - uart.write: + id: uart_id + data: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector<uint8_t>(cmd.begin(), cmd.end()); event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index c2670b289a0..4c9d6fd7367 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,15 +17,21 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 14 + rx_pin: 12 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " switch: - platform: uart name: "UART Switch Array" - uart_id: uart_uart + uart_id: uart_id data: [0x01, 0x02, 0x03] - platform: uart name: "UART Switch Dual" - uart_id: uart_uart + uart_id: uart_id data: turn_on: [0xA0, 0xA1] turn_off: [0xB0, 0xB1] @@ -29,12 +39,12 @@ switch: button: - platform: uart name: "UART Button" - uart_id: uart_uart + uart_id: uart_id data: [0xFF, 0xEE] event: - platform: uart - uart_id: uart_uart + uart_id: uart_id name: "UART Event" event_types: - "string_event_A": "*A#" diff --git a/tests/components/uart/test.host.yaml b/tests/components/uart/test.host.yaml index 63f0ade0848..bc64245c7bc 100644 --- a/tests/components/uart/test.host.yaml +++ b/tests/components/uart/test.host.yaml @@ -5,7 +5,7 @@ esphome: - uart.write: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id port: "/dev/ttyS0" baud_rate: 9600 data_bits: 8 diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 09178f16639..5eb2b533ea0 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -1,11 +1,15 @@ esphome: on_boot: then: - - uart.write: 'Hello World' - - uart.write: [0x00, 0x20, 0x42] + - uart.write: + id: uart_id + data: 'Hello World' + - uart.write: + id: uart_id + data: [0x00, 0x20, 0x42] uart: - - id: uart_uart + - id: uart_id tx_pin: 4 rx_pin: 5 baud_rate: 9600 @@ -13,3 +17,9 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + - id: uart_debug + tx_pin: 8 + rx_pin: 9 + baud_rate: 115200 + debug: + debug_prefix: "[UART1] " diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 474c3f5c8de..5869b9468b6 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -34,3 +34,11 @@ usb_uart: - id: channel_4_1 debug: true dummy_receiver: true + debug_prefix: "[ESP_JTAG] " + - id: uart_5 + type: cp210x + channels: + - id: channel_5_1 + baud_rate: 9600 + debug: true + debug_prefix: "[CP210X] " From 545395a6f0d2180c648998f0ded5302f01df89cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 13:16:19 -1000 Subject: [PATCH 0472/2056] [ci] Add RP2350 to PR template test environment (#14599) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 965b186c31e..72013e411eb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 -- [ ] RP2040 +- [ ] RP2040/RP2350 - [ ] BK72xx - [ ] RTL87xx - [ ] LN882x From 888f3d804b9550036c20153218e4286d38c88775 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 13:22:50 -1000 Subject: [PATCH 0473/2056] [ld2420] Add integration tests with mock UART (#14471) --- .../uart_mock/uart_mock.cpp | 8 +- .../fixtures/uart_mock_ld2420.yaml | 187 ++++++++++++ .../fixtures/uart_mock_ld2420_simple.yaml | 141 +++++++++ tests/integration/test_uart_mock_ld2420.py | 273 ++++++++++++++++++ 4 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2420.yaml create mode 100644 tests/integration/fixtures/uart_mock_ld2420_simple.yaml create mode 100644 tests/integration/test_uart_mock_ld2420.py diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index affcc8d908d..e8c07d632de 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -104,12 +104,12 @@ void MockUartComponent::write_array(const uint8_t *data, size_t len) { } #endif - if (this->scenario_active_) { - this->try_match_response_(); - } + // Responses are always active - they are request-response pairs triggered by + // component TX, not timed injections. No race condition with test subscription. + this->try_match_response_(); // This directly calls a tx_hook (lambda) as an alternative to the simpler match_response mechanism. - if (this->tx_hook_ && this->scenario_active_) { + if (this->tx_hook_) { std::vector<uint8_t> buf(data, data + len); this->tx_hook_(buf); } diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml new file mode 100644 index 00000000000..5380b810719 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -0,0 +1,187 @@ +esphome: + name: uart-mock-ld2420-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Version-specific response: match the complete firmware version command TX. + # CMD_READ_VERSION (0x0000) TX = FD FC FB FA 02 00 00 00 04 03 02 01 + # Returns "v2.0.0" → get_firmware_int = 200 >= 154 → energy mode + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 0C 00 = length 12 + # [6] 00 = cmd (CMD_READ_VERSION) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10] 06 = ver_len = 6 + # [11] 00 = padding + # [12-17] "v2.0.0" = version string + # [18-21] 04 03 02 01 = footer + - expect_tx: + [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x0C, 0x00, + 0x00, 0x01, + 0x00, 0x00, + 0x06, 0x00, + 0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30, + 0x04, 0x03, 0x02, 0x01, + ] + + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch). + # All commands get unblocked via cmd_reply_.ack = true. + # Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0). + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path + # Buffer is clean (buffer_pos_=0). This frame should parse correctly. + # Presence: 1 (target detected), Distance: 100cm, Gate energies: all 0 + # + # Energy frame layout (45 bytes): + # [0-3] F4 F3 F2 F1 = energy frame header + # [4-5] 23 00 = length 35 (1+2+32) + # [6] 01 = presence (1 = target) + # [7-8] 64 00 = distance 100 (uint16_t LE) + # [9-40] 00 00 x16 = 16 gate energies (uint16_t LE each) + # [41-44] F8 F7 F6 F5 = energy frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes) + # This tests PR #14458 bug #3: missing length validation in handle_energy_mode_. + # The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7). + # These 13 bytes are appended at positions 7-19 (buffer_pos_=20). + # Energy footer at positions 16-19 triggers handle_energy_mode_(buffer, 20). + # + # Pre-fix: handle_energy_mode_ reads 32 bytes of gate energy from buffer[9:40], + # which is past the 20 actual bytes. Reads uninitialized data. + # No "Energy frame too short" warning exists. + # Post-fix: len=20 < 41 → logs "Energy frame too short: 20 bytes", returns early. + # + # Frame: header + length + presence + distance + footer (no gate data) + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x64, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # After Phase 3, buffer_pos_=0 (reset after energy footer detection). + # 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow. + # Logs "Max command length exceeded; ignoring", buffer_pos_=0. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=1500ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # Presence: 1 (target), Distance: 50cm + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x23, 0x00, + 0x01, + 0x32, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml new file mode 100644 index 00000000000..2ceca5d35d8 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -0,0 +1,141 @@ +esphome: + name: uart-mock-ld2420-simple-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + auto_start: false + responses: + # Catch-all response only (no version-specific response). + # Without a version response, firmware_ver_ stays at default "v0.0.0". + # get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE). + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid simple mode text frame - happy path + # "ON Range 0100\r\n" → presence=true, distance=100 + # Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_. + - delay: 100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x31, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50) + # buffer_pos_ starts at 7 (from Phase 2 garbage). + # Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49). + # After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6. + # Final buffer_pos_ = 7. + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 4 (t=1400ms): Recovery after overflow + # buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21. + # At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22). + # Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8, + # parses digits "0050" → distance=50. + # Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle. + - delay: 900ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x35, 0x30, + 0x0D, 0x0A, + ] + + # Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1 + # "ON Range 0000000000000000\r\n" has 16 digit characters. + # handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14). + # + # Pre-fix: At the 16th digit, index=15, (index < bufsize-1) is false. + # The digit branch doesn't increment pos. The else branch is skipped. + # pos stays at the 16th digit FOREVER → INFINITE LOOP. + # The binary hangs, no more state updates, test times out. + # Post-fix: pos always increments (moved outside digit branch). + # 16th digit skipped, loop continues to \r\n. distance=0. + - delay: 1100ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x0D, 0x0A, + ] + + # Phase 6 (t=3700ms): Post-bug-trigger recovery + # If Phase 5 didn't hang, this frame should parse correctly. + # "ON Range 0025\r\n" → distance=25 + # Delay=1200ms ensures >1000ms gap from Phase 5 for throttle. + - delay: 1200ms + inject_rx: + [ + 0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, + 0x30, 0x30, 0x32, 0x35, + 0x0D, 0x0A, + ] + +button: + - platform: template + name: "Start Scenario" + on_press: + - lambda: 'id(mock_uart).start_scenario();' + +ld2420: + id: ld2420_dev + uart_id: mock_uart + +sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + moving_distance: + name: "Moving Distance" + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +binary_sensor: + - platform: ld2420 + ld2420_id: ld2420_dev + has_target: + name: "Has Target" + filters: + - settle: 50ms diff --git a/tests/integration/test_uart_mock_ld2420.py b/tests/integration/test_uart_mock_ld2420.py new file mode 100644 index 00000000000..ae28da4d3e1 --- /dev/null +++ b/tests/integration/test_uart_mock_ld2420.py @@ -0,0 +1,273 @@ +"""Integration test for LD2420 component with mock UART. + +Tests: +test_uart_mock_ld2420 (energy mode): + 1. Happy path - valid energy frame publishes correct sensor values + 2. Garbage resilience - random bytes don't crash the component + 3. Truncated energy frame - triggers "Energy frame too short" warning (PR #14458 bug #3) + 4. Buffer overflow recovery - overflow resets the parser + 5. Post-overflow parsing - next valid frame after overflow is parsed correctly + 6. TX logging - verifies LD2420 sends expected setup commands + +test_uart_mock_ld2420_simple (simple mode): + 1. Happy path - valid simple mode text frame publishes correct values + 2. Garbage resilience + 3. Buffer overflow recovery + 4. 16-digit distance triggers infinite loop pre-fix (PR #14458 bug #1) + 5. Post-bug-trigger recovery proves the parser survived +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 energy mode: happy path, truncated frame, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track "Energy frame too short" warning (PR #14458 bug #3 fix) + # This message ONLY exists after the fix. Pre-fix, handle_energy_mode_ + # silently reads past the buffer without any warning. + truncated_frame_warning_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + if "Energy frame too short" in line and not truncated_frame_warning_seen.done(): + truncated_frame_warning_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal when we see recovery frame values + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 values: moving=100, has_target=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify truncated frame warning was logged (PR #14458 bug #3) + # This assertion FAILS before PR #14458 because the length check + # and warning message did not exist. + assert truncated_frame_warning_seen.done(), ( + "Expected 'Energy frame too short' warning in logs. " + "This indicates PR #14458 fix for handle_energy_mode_ length " + "validation is missing." + ) + + # Verify LD2420 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD.FC.FB.FA" in tx_data or "FD:FC:FB:FA" in tx_data, ( + "Expected LD2420 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04.03.02.01" in tx_data or "04:03:02:01" in tx_data, ( + "Expected LD2420 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow) + recovery_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(50.0) + ] + assert len(recovery_values) >= 1, ( + f"Expected moving_distance=50 in recovery, got: {collector.sensor_states['moving_distance']}" + ) + + +@pytest.mark.asyncio +async def test_uart_mock_ld2420_simple( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2420 simple mode: happy path, overflow, and 16-digit bug trigger.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=["moving_distance"], + binary_sensor_names=["has_target"], + ) + + # Signal for recovery frames + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + ) + post_bug_received = collector.add_waiter( + lambda: pytest.approx(25.0) in collector.sensor_states["moving_distance"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1: simple mode "ON Range 0100\r\n" → distance=100, presence=true + assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0) + assert collector.binary_states["has_target"][0] is True + + # Wait for Phase 4 recovery (distance=50) after overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" moving_distance: {collector.sensor_states['moving_distance']}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Wait for Phase 6: distance=25 (post-16-digit-bug recovery) + # This assertion FAILS before PR #14458 because the 16-digit frame + # in Phase 5 causes an infinite loop in handle_simple_mode_ pre-fix. + # The binary hangs, Phase 6 never fires, and this wait times out. + try: + await asyncio.wait_for(post_bug_received, timeout=8.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for post-bug recovery (distance=25). " + f"This likely means Phase 5 (16-digit frame) caused an infinite " + f"loop in handle_simple_mode_, indicating PR #14458 bug #1 fix " + f"is missing.\n" + f" moving_distance values: {collector.sensor_states['moving_distance']}" + ) + + # Verify post-bug value + post_bug_values = [ + v + for v in collector.sensor_states["moving_distance"] + if v == pytest.approx(25.0) + ] + assert len(post_bug_values) >= 1, ( + f"Expected moving_distance=25 after 16-digit test, " + f"got: {collector.sensor_states['moving_distance']}" + ) From ea7cfffddaf60c5da30bf9dfa976fcf044477c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:40:13 +0000 Subject: [PATCH 0474/2056] Bump aioesphomeapi from 44.3.1 to 44.4.0 (#14609) 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 8e875eba622..a1300a67f7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.3.1 +aioesphomeapi==44.4.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 66919ef969574f8e6553d2146089c41ee9cecd27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:33:54 -1000 Subject: [PATCH 0475/2056] [i2s_audio] Include legacy driver IDF component when use_legacy is set (#14613) --- esphome/components/i2s_audio/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 1cd2e97a5e8..65b09b93f6c 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -281,6 +281,8 @@ async def to_code(config): if use_legacy(): cg.add_define("USE_I2S_LEGACY") + # Legacy I2S API lives in the "driver" shim component (driver/i2s.h) + include_builtin_idf_component("driver") # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) From d55fe9a34b50400347a7d57f6679dbbb514ea8ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:34:35 -1000 Subject: [PATCH 0476/2056] [api] Fix value-initialization of DeviceInfoResponse (#14615) Co-authored-by: Keith Burzinski <kbx81x@gmail.com> --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bd3de028951..b9b33ddcc22 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1571,7 +1571,7 @@ bool APIConnection::send_ping_response_() { } bool APIConnection::send_device_info_response_() { - DeviceInfoResponse resp{}; + DeviceInfoResponse resp; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); #ifdef USE_AREAS From 9fea8fe01b4c976caced70881cb986729768c305 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:50:36 -0500 Subject: [PATCH 0477/2056] [vbus][rf_bridge][sensirion_common] Add buffer size guards (#14597) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/rf_bridge/rf_bridge.cpp | 3 +++ esphome/components/rf_bridge/rf_bridge.h | 1 + esphome/components/sensirion_common/i2c_sensirion.cpp | 2 +- esphome/components/vbus/vbus.cpp | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 700e2ba1623..5ca629c12b0 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -145,6 +145,9 @@ void RFBridgeComponent::loop() { } avail -= to_read; for (size_t i = 0; i < to_read; i++) { + if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { + this->rx_buffer_.clear(); + } if (this->parse_bridge_byte_(buf[i])) { ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); this->last_bridge_byte_ = now; diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index d2f75c819dc..c93b636c38c 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -30,6 +30,7 @@ static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1; static const uint8_t RF_CODE_BEEP = 0xC0; static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_DEBOUNCE = 200; +static const size_t MAX_RX_BUFFER_SIZE = 512; struct RFBridgeData { uint16_t sync; diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 26702c148c1..0279e08b9ff 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -12,7 +12,7 @@ static const char *const TAG = "sensirion_i2c"; static const size_t BUFFER_STACK_SIZE = 16; bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { - const uint8_t num_bytes = len * 3; + const size_t num_bytes = len * 3; uint8_t buf[num_bytes]; this->last_error_ = this->read(buf, num_bytes); diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index 8616da010d1..c6786ee31e8 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -87,6 +87,9 @@ void VBus::loop() { this->state_ = 0; ESP_LOGD(TAG, "P1 empty message"); } + } else if (this->buffer_.size() > 15) { + ESP_LOGW(TAG, "Unknown protocol 0x%02x, discarding", this->protocol_); + this->state_ = 0; } continue; } From be6c3c52ac89534f00e54dfa26974402d813a4bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 18:59:13 -1000 Subject: [PATCH 0478/2056] [api] Add force proto field option to skip zero checks on hot path (#14610) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api.proto | 6 ++-- esphome/components/api/api_options.proto | 6 ++++ esphome/components/api/api_pb2.cpp | 14 ++++----- script/api_protobuf/api_protobuf.py | 38 ++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 618fd1b83c9..257a7aaf827 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1604,11 +1604,11 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { - uint64 address = 1; - sint32 rssi = 2; + uint64 address = 1 [(force) = true]; + sint32 rssi = 2 [(force) = true]; uint32 address_type = 3; - bytes data = 4 [(fixed_array_size) = 62]; + bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } message BluetoothLERawAdvertisementsResponse { diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index a863f2c7a84..02600f0977f 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions { // - uint16_t <field>_length_{0}; // - uint16_t <field>_count_{0}; optional bool packed_buffer = 50015 [default=false]; + + // force: Always encode this field, even when its value equals the proto3 default. + // Skips the zero/empty check in calculate_size() and encode(), using the _force + // variant of the calc_ method. Use on fields that are almost always non-default + // to eliminate dead branches on hot paths. + optional bool force = 50016 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1944aac6e87..38ebfb94649 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -139,7 +139,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - buffer.encode_message(25, it); + buffer.encode_sub_message(25, it); } #endif } @@ -2249,17 +2249,17 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_sint32(2, this->rssi); + buffer.encode_uint64(1, this->address, true); + buffer.encode_sint32(2, this->rssi, true); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len); + buffer.encode_bytes(4, this->data, this->data_len, true); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint64(1, this->address); - size += ProtoSize::calc_sint32(1, this->rssi); + size += ProtoSize::calc_uint64_force(1, this->address); + size += ProtoSize::calc_sint32_force(1, this->rssi); size += ProtoSize::calc_uint32(1, this->address_type); - size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_length_force(1, this->data_len); return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7ae7063a412..206f8f558bd 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -151,6 +151,11 @@ class TypeInfo(ABC): """Check if the field is repeated.""" return self._field.label == FieldDescriptorProto.LABEL_REPEATED + @property + def force(self) -> bool: + """Check if this field should always be encoded (skip zero/empty check).""" + return get_field_opt(self._field, pb.force, False) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -218,6 +223,8 @@ class TypeInfo(ABC): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" encode_func = None @@ -413,6 +420,8 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -437,6 +446,8 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -521,6 +532,8 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -545,6 +558,8 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -607,6 +622,8 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if self.force: + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" def dump(self, name): @@ -694,7 +711,7 @@ class MessageType(TypeInfo): @property def encode_content(self) -> str: - # Singular message fields skip encoding when empty + # encode_sub_message always encodes (uses backpatch), no force needed return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property @@ -771,6 +788,8 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: @@ -876,6 +895,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @property @@ -923,6 +944,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if self.force: + return ( + f"buffer.encode_string({self.number}, this->{self.field_name}, true);" + ) return f"buffer.encode_string({self.number}, this->{self.field_name});" @property @@ -1086,6 +1111,8 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: @@ -1159,6 +1186,8 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if self.force: + return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));" def dump(self, name: str) -> str: @@ -1192,6 +1221,8 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -1216,6 +1247,8 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() + if force: + return f"size += {field_id_size + self.get_fixed_size_bytes()};" return f"size += ProtoSize::calc_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: @@ -2134,7 +2167,8 @@ def build_message_type( encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef)) size_calc.extend( wrap_with_ifdef( - ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef + ti.get_size_calculation(f"this->{ti.field_name}", ti.force), + field_ifdef, ) ) From 5e842a8b207230be2fd8db09baf5098f62767a21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sat, 7 Mar 2026 23:23:13 -0600 Subject: [PATCH 0479/2056] [uart] Return `flush` result, expose timeout via config (#14608) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/ble_nus/ble_nus.cpp | 5 +++-- esphome/components/ble_nus/ble_nus.h | 2 +- esphome/components/uart/__init__.py | 6 ++++++ esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 16 +++++++++++++++- .../components/uart/uart_component_esp8266.cpp | 3 ++- esphome/components/uart/uart_component_esp8266.h | 2 +- .../components/uart/uart_component_esp_idf.cpp | 13 +++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 5 ++++- esphome/components/uart/uart_component_host.cpp | 5 +++-- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 3 ++- .../components/uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 3 ++- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 11 ++++++++--- esphome/components/usb_uart/usb_uart.cpp | 5 ++++- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 5 +++-- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../external_components/uart_mock/uart_mock.cpp | 3 ++- .../external_components/uart_mock/uart_mock.h | 2 +- 25 files changed, 77 insertions(+), 30 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d1710100a09..e38dc998028 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -100,16 +100,17 @@ size_t BLENUS::available() { #endif } -void BLENUS::flush() { +uart::FlushResult BLENUS::flush() { constexpr uint32_t timeout_5sec = 5000; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_5sec) { ESP_LOGW(TAG, "Flush timeout"); - return; + return uart::FlushResult::TIMEOUT; } delay(1); } + return uart::FlushResult::SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index 67e9ae9f97a..b482c240e53 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 1b976b73a90..2cb6eac0509 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -183,6 +183,7 @@ UART_PARITY_OPTIONS = { "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, } +CONF_FLUSH_TIMEOUT = "flush_timeout" CONF_RX_FULL_THRESHOLD = "rx_full_threshold" CONF_RX_TIMEOUT = "rx_timeout" @@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All( cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92) ), + cv.Optional(CONF_FLUSH_TIMEOUT): cv.All( + cv.only_on_esp32, cv.positive_time_period_milliseconds + ), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum( @@ -345,6 +349,8 @@ async def to_code(config): ) cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) + if CONF_FLUSH_TIMEOUT in config: + cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index bb91e5cd7c0..2c4fb34c9a8 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - void flush() { this->parent_->flush(); } + FlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 078ce64b30f..3d7e71b89fd 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -29,6 +29,14 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); +/// Result of a flush() call. +enum class FlushResult { + SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + TIMEOUT, ///< Confirmed: timed out before TX completed. + FAILED, ///< Confirmed: driver or hardware error. + ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +}; + class UARTComponent { public: // Writes an array of bytes to the UART bus. @@ -72,7 +80,13 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - virtual void flush() = 0; + // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual FlushResult flush() = 0; + + // Sets the maximum time to wait for TX to drain during flush(). + // Only meaningful on ESP32 (IDF). Other platforms ignore this value. + // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. + virtual void set_flush_timeout(uint32_t flush_timeout_ms) {} // Sets the TX (transmit) pin for the UART bus. // @param tx_pin Pointer to the internal GPIO pin used for transmission. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 3ebf381c846..91218c43006 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,13 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -void ESP8266UartComponent::flush() { +FlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } + return FlushResult::ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e84cbe386d5..ca90dc59646 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 544404448b9..47ddf1a38d7 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -230,6 +230,9 @@ void IDFUARTComponent::dump_config() { " RX Timeout: %u", this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_); } + if (this->flush_timeout_ms_ > 0) { + ESP_LOGCONFIG(TAG, " Flush Timeout: %" PRIu32 " ms", this->flush_timeout_ms_); + } ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" @@ -332,9 +335,15 @@ size_t IDFUARTComponent::available() { return available; } -void IDFUARTComponent::flush() { +FlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - uart_wait_tx_done(this->uart_num_, portMAX_DELAY); + TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); + esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); + if (err == ESP_OK) + return FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return FlushResult::TIMEOUT; + return FlushResult::FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 631bf54cd56..9fa2013cfd1 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,9 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; + + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } uint8_t get_hw_serial_number() { return this->uart_num_; } @@ -57,6 +59,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). #ifdef USE_UART_WAKE_LOOP_ON_RX // ISR callback for UART RX data notification — wakes the main loop directly. diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 9dce25c500e..66026f3ccdd 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,12 +274,13 @@ size_t HostUartComponent::available() { return result; }; -void HostUartComponent::flush() { +FlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return; + return FlushResult::ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); + return FlushResult::ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index 89b951093b6..c22efdcb92d 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 83d2acb332d..6a550f296a1 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,9 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -void LibreTinyUARTComponent::flush() { +FlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 31f082d31e1..77df8080671 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index faf8f4d90f1..858f1a02ddf 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -187,9 +187,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -void RP2040UartComponent::flush() { +FlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); + return FlushResult::ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 4ca58e8dc60..891212ca74c 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + FlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index ddcc65232d4..624f41cf8c6 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; protected: void check_logger_conflict() override; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 44de986f9a3..1a36ef9f31f 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -326,10 +326,10 @@ size_t USBCDCACMInstance::available() { return waiting + (this->has_peek_ ? 1 : 0); } -void USBCDCACMInstance::flush() { +uart::FlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return; + return uart::FlushResult::ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -341,7 +341,12 @@ void USBCDCACMInstance::flush() { } // Also wait for USB to finish transmitting - tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100)); + esp_err_t err = tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100)); + if (err == ESP_OK) + return uart::FlushResult::SUCCESS; + if (err == ESP_ERR_TIMEOUT) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a1f87384914..a0101a55463 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -166,7 +166,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -void USBUartChannel::flush() { +uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The 100 ms timeout guards against a device that stops responding mid-flush; @@ -177,6 +177,9 @@ void USBUartChannel::flush() { this->parent_->start_output(this); yield(); } + if (!this->output_queue_.empty() || this->output_started_.load()) + return uart::FlushResult::TIMEOUT; + return uart::FlushResult::SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7290f8a958a..671f0cab8c0 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -110,7 +110,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override { return this->input_buffer_.get_available(); } - void flush() override; + uart::FlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 3f5d6c787cb..f01d164e9fd 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,15 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast<uint8_t *>(buffer), length); } -void WeikaiChannel::flush() { +uart::FlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return; + return uart::FlushResult::TIMEOUT; } yield(); // reschedule our thread to avoid blocking } + return uart::FlushResult::SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 43c3a1e4f4c..715b82bfc71 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - void flush() override; + uart::FlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index d5ffbe1295b..9f9e7b3e9f6 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(uart::FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 1f9bfa15e7f..f7e2d8a3f76 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(void, flush, (), (override)); + MOCK_METHOD(FlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index e8c07d632de..1edeb97cf16 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -144,8 +144,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -void MockUartComponent::flush() { +uart::FlushResult MockUartComponent::flush() { // Nothing to flush in mock + return uart::FlushResult::ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 901e371dec4..c9afb963579 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - void flush() override; + uart::FlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From 04cff1c916ece7a8abe16a17f0c8bc4e07628d56 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 00:04:14 -0600 Subject: [PATCH 0480/2056] [usb_uart] Return `flush` result, expose timeout via config (#14616) --- esphome/components/usb_uart/__init__.py | 6 +++++- esphome/components/usb_uart/usb_uart.cpp | 10 ++++++---- esphome/components/usb_uart/usb_uart.h | 11 ++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index eedf590ecae..2d85723d722 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS -from esphome.components.uart import CONF_DEBUG_PREFIX, UARTComponent +from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema import esphome.config_validation as cv from esphome.const import ( @@ -91,6 +91,9 @@ def channel_schema(channels, baud_rate_required): cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, cv.Optional(CONF_DEBUG, default=False): cv.boolean, cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string, + cv.Optional( + CONF_FLUSH_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, } ) ), @@ -129,6 +132,7 @@ async def to_code(config): cg.add(chvar.set_parity(channel[CONF_PARITY])) cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) + cg.add(chvar.set_flush_timeout(channel[CONF_FLUSH_TIMEOUT])) cg.add(chvar.set_debug(channel[CONF_DEBUG])) if channel[CONF_DEBUG_PREFIX]: cg.add(chvar.set_debug_prefix(channel[CONF_DEBUG_PREFIX])) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a0101a55463..83de0b39fcc 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,10 +169,10 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { uart::FlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. - // The 100 ms timeout guards against a device that stops responding mid-flush; + // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; // in that case the main loop is blocked for the full duration. - uint32_t start = millis(); // 100 ms safety timeout - while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < 100) { + uint32_t start = millis(); + while ((!this->output_queue_.empty() || this->output_started_.load()) && millis() - start < this->flush_timeout_ms_) { // Kick start_output() in case data arrived but no transfer is in flight yet. this->parent_->start_output(this); yield(); @@ -260,10 +260,12 @@ void USBUartComponent::dump_config() { " Data Bits: %u\n" " Parity: %s\n" " Stop bits: %s\n" + " Flush Timeout: %" PRIu32 " ms\n" " Debug: %s\n" " Dummy receiver: %s", channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_], - STOP_BITS_NAMES[channel->stop_bits_], YESNO(channel->debug_), YESNO(channel->dummy_receiver_)); + STOP_BITS_NAMES[channel->stop_bits_], channel->flush_timeout_ms_, YESNO(channel->debug_), + YESNO(channel->dummy_receiver_)); } } void USBUartComponent::start_input(USBUartChannel *channel) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 671f0cab8c0..b1748aebf28 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -116,6 +116,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_debug(bool debug) { this->debug_ = debug; } void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } + void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } /// Register a callback invoked immediately after data is pushed to the input ring buffer. /// Called from USBUartComponent::loop() in the main loop context. @@ -124,23 +125,23 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); } protected: - // Larger structures first for better alignment + // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_; EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_; std::function<void()> rx_callback_{}; CdcEps cdc_dev_{}; - // Enum (likely 4 bytes) + StringRef debug_prefix_{}; + // 4-byte fields UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; - // Group atomics together (each 1 byte) + uint32_t flush_timeout_ms_{100}; + // 1-byte fields (no padding between groups) std::atomic<bool> input_started_{true}; std::atomic<bool> output_started_{true}; std::atomic<bool> initialised_{false}; - // Group regular bytes together to minimize padding const uint8_t index_; bool debug_{}; bool dummy_receiver_{}; - StringRef debug_prefix_{}; }; class USBUartComponent : public usb_host::USBClient { From e4b89a69d4aba18e05b0a965e98e5f242637daf2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 07:32:20 +0100 Subject: [PATCH 0481/2056] [nrf52, ota] ble and serial OTA based on mcumgr (#11932) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/nrf52/__init__.py | 46 +++++- esphome/components/ota/__init__.py | 9 +- esphome/components/safe_mode/safe_mode.cpp | 15 +- esphome/components/zephyr_mcumgr/__init__.py | 0 .../components/zephyr_mcumgr/ota/__init__.py | 141 +++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp | 143 ++++++++++++++++++ .../zephyr_mcumgr/ota/ota_zephyr_mcumgr.h | 29 ++++ esphome/core/defines.h | 1 + script/helpers_zephyr.py | 16 ++ tests/components/ota/test.nrf52-mcumgr.yaml | 27 ++++ 11 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 esphome/components/zephyr_mcumgr/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/__init__.py create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp create mode 100644 esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h create mode 100644 tests/components/ota/test.nrf52-mcumgr.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8bf896d159e..d60dbc729d9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -587,6 +587,7 @@ esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 +esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zigbee/* @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 0a9fb5939a2..c3a10a99440 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -27,10 +27,15 @@ from esphome.components.zephyr.const import ( ) import esphome.config_validation as cv from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, + CONF_DISABLED, + CONF_ENABLE_OTA_ROLLBACK, CONF_FRAMEWORK, CONF_ID, + CONF_OTA, CONF_RESET_PIN, + CONF_SAFE_MODE, CONF_VERSION, CONF_VOLTAGE, KEY_CORE, @@ -41,6 +46,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +import esphome.final_validate as fv from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -133,6 +139,7 @@ CONF_UICR_ERASE = "uicr_erase" VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] + CONFIG_SCHEMA = cv.All( _detect_bootloader, set_core_data, @@ -156,9 +163,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } ), - cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-a"}): cv.Schema( + cv.Optional( + CONF_FRAMEWORK, + default={}, + ): cv.Schema( { - cv.Required(CONF_VERSION): cv.string_strict, + cv.Optional(CONF_VERSION, default="2.6.1-a"): cv.string_strict, + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional( + CONF_ENABLE_OTA_ROLLBACK, default=True + ): cv.boolean, + } + ), } ), cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), @@ -181,6 +198,24 @@ def _final_validate(config): _LOGGER.warning( "Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`" ) + full_config = fv.full_config.get() + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + # "disabled: false" means safe mode *is* enabled. + safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True}) + safe_mode_enabled = not safe_mode_config[CONF_DISABLED] + ota_enabled = CONF_OTA in full_config + # Both need to be enabled for rollback to work + if not (ota_enabled and safe_mode_enabled): + # But only warn if ota is even possible + if ota_enabled: + _LOGGER.warning( + "OTA rollback requires safe_mode, disabling rollback support" + ) + # disable the rollback feature anyway since it can't be used. + advanced[CONF_ENABLE_OTA_ROLLBACK] = False FINAL_VALIDATE_SCHEMA = _final_validate @@ -247,6 +282,11 @@ async def to_code(config: ConfigType) -> None: if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") + conf = config[CONF_FRAMEWORK] + advanced = conf[CONF_ADVANCED] + # Enable OTA rollback support + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + cg.add_define("USE_OTA_ROLLBACK") # c++ support if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("CPLUSPLUS", True) @@ -259,7 +299,7 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) # disable console zephyr_add_prj_conf("UART_CONSOLE", False) - zephyr_add_prj_conf("CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False, False) # use NFC pins as GPIO if framework_ver < cv.Version(2, 9, 2): zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index ee54d5f8d3f..8f31eb5cdd3 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -18,7 +18,14 @@ from esphome.coroutine import CoroPriority OTA_STATE_LISTENER_KEY = "ota_state_listener" CODEOWNERS = ["@esphome/core"] -AUTO_LOAD = ["md5", "safe_mode"] + + +def AUTO_LOAD() -> list[str]: + components = ["safe_mode"] + if not CORE.using_zephyr: + components.extend(["md5"]) + return components + IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index fe2acd96122..40fa03392b3 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,10 +9,14 @@ #include <cinttypes> #include <cstdio> -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#ifdef USE_OTA_ROLLBACK +#ifdef USE_ZEPHYR +#include <zephyr/dfu/mcuboot.h> +#elif defined(USE_ESP32) #include <esp_ota_ops.h> #include <esp_system.h> #endif +#endif namespace esphome::safe_mode { @@ -66,9 +70,16 @@ float SafeModeComponent::get_setup_priority() const { return setup_priority::AFT void SafeModeComponent::mark_successful() { this->clean_rtc(); this->boot_successful_ = true; -#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#if defined(USE_OTA_ROLLBACK) +// Mark OTA partition as valid to prevent rollback +#if defined(USE_ZEPHYR) + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#elif defined(USE_ESP32) // Mark OTA partition as valid to prevent rollback esp_ota_mark_app_valid_cancel_rollback(); +#endif #endif // Disable loop since we no longer need to check this->disable_loop(); diff --git a/esphome/components/zephyr_mcumgr/__init__.py b/esphome/components/zephyr_mcumgr/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/zephyr_mcumgr/ota/__init__.py b/esphome/components/zephyr_mcumgr/ota/__init__.py new file mode 100644 index 00000000000..b0d86190b8d --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/__init__.py @@ -0,0 +1,141 @@ +import esphome.codegen as cg +from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.components.zephyr import ( + zephyr_add_cdc_acm, + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import BOOTLOADER_MCUBOOT, KEY_BOOTLOADER +import esphome.config_validation as cv +from esphome.const import CONF_HARDWARE_UART, CONF_ID, Framework +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType + +CODEOWNERS = ["@tomaszduda23"] +DEPENDENCIES = ["zephyr"] + +ZephyrMcumgrOTAComponent = cg.esphome_ns.namespace("zephyr_mcumgr").class_( + "OTAComponent", OTAComponent +) + +CONF_BLE = "ble" +CONF_TRANSPORT = "transport" + + +def _validate_transport(conf: ConfigType) -> ConfigType: + transport = conf[CONF_TRANSPORT] + if transport[CONF_BLE] or CONF_HARDWARE_UART in transport: + return conf + raise cv.Invalid( + f"At least one transport protocol has to be enabled. Set '{CONF_BLE}: true' or '{CONF_HARDWARE_UART}'" + ) + + +UARTS = { + "CDC": ("cdc_acm_uart0", 0), + "CDC1": ("cdc_acm_uart1", 1), + "UART0": ("uart0", -1), + "UART1": ("uart1", -1), +} + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ZephyrMcumgrOTAComponent), + cv.Optional(CONF_TRANSPORT, default={CONF_BLE: True}): cv.Schema( + { + cv.Optional(CONF_BLE, default=False): cv.boolean, + cv.Optional( + CONF_HARDWARE_UART, + ): cv.one_of(*UARTS, upper=True), + } + ), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + _validate_transport, + cv.only_with_framework(Framework.ZEPHYR), +) + + +def _validate_mcumgr_bootloader(config: ConfigType) -> None: + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader != BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support OTA") + + +KEY_ZEPHYR_BLE_SERVER = "zephyr_ble_server" + + +def _validate_ble_server(config: ConfigType) -> None: + if ( + config[CONF_TRANSPORT][CONF_BLE] + and KEY_ZEPHYR_BLE_SERVER not in CORE.loaded_integrations + ): + raise cv.Invalid(f"'{KEY_ZEPHYR_BLE_SERVER}' component is required for BLE OTA") + + +def _final_validate(config: ConfigType) -> None: + _validate_mcumgr_bootloader(config) + _validate_ble_server(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +@coroutine_with_priority(CoroPriority.OTA_UPDATES) +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + + await cg.register_component(var, config) + + zephyr_add_prj_conf("NET_BUF", True) + zephyr_add_prj_conf("ZCBOR", True) + zephyr_add_prj_conf("MCUMGR", True) + + zephyr_add_prj_conf("MCUMGR_GRP_IMG", True) + + zephyr_add_prj_conf("IMG_MANAGER", True) + zephyr_add_prj_conf("STREAM_FLASH", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("FLASH", True) + + zephyr_add_prj_conf("IMG_ERASE_PROGRESSIVELY", True) + + zephyr_add_prj_conf("BOOTLOADER_MCUBOOT", True) + + zephyr_add_prj_conf("MCUMGR_MGMT_NOTIFICATION_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_STATUS_HOOKS", True) + zephyr_add_prj_conf("MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK", True) + transport = config[CONF_TRANSPORT] + if transport[CONF_BLE]: + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT", True) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT_REASSEMBLY", True) + + zephyr_add_prj_conf("MCUMGR_GRP_OS", True) + zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True) + + zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True) + if CONF_HARDWARE_UART in transport: + uart = UARTS[transport[CONF_HARDWARE_UART]] + uart_name = uart[0] + cdc_id = uart[1] + if cdc_id >= 0: + zephyr_add_cdc_acm(config, cdc_id) + zephyr_add_prj_conf("MCUMGR_TRANSPORT_UART", True) + zephyr_add_prj_conf("BASE64", True) + zephyr_add_prj_conf("CONSOLE", True) + zephyr_add_overlay( + f""" + / {{ + chosen {{ + zephyr,uart-mcumgr = &{uart_name}; + }}; + }}; + """ + ) diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp new file mode 100644 index 00000000000..f1eac462bc3 --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.cpp @@ -0,0 +1,143 @@ +#ifdef USE_ZEPHYR +#include "ota_zephyr_mcumgr.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include <zephyr/sys/math_extras.h> +#include <zephyr/usb/usb_device.h> +#include <zephyr/dfu/mcuboot.h> + +// It should be from below header but there is problem with internal includes. +// #include <zephyr/mgmt/mcumgr/grp/img_mgmt/img_mgmt.h> +// NOLINTBEGIN(readability-identifier-naming,google-runtime-int) +struct img_mgmt_upload_action { + /** The total size of the image. */ + unsigned long long size; +}; + +struct img_mgmt_upload_req { + uint32_t image; /* 0 by default */ + size_t off; /* SIZE_MAX if unspecified */ +}; +// NOLINTEND(readability-identifier-naming,google-runtime-int) + +namespace esphome::zephyr_mcumgr { + +static_assert(sizeof(struct img_mgmt_upload_action) == 8, "ABI mismatch"); +static_assert(sizeof(struct img_mgmt_upload_req) == 8, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, image) == 0, "ABI mismatch"); +static_assert(offsetof(struct img_mgmt_upload_req, off) == 4, "ABI mismatch"); + +static const char *const TAG = "zephyr_mcumgr"; +static OTAComponent *global_ota_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static enum mgmt_cb_return mcumgr_img_mgmt_cb(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, + uint16_t *group, bool *abort_more, void *data, size_t data_size) { + if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK == event) { + const img_mgmt_upload_check &upload = *static_cast<img_mgmt_upload_check *>(data); + global_ota_component->update_chunk(upload); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STARTED == event) { + global_ota_component->update_started(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE == event) { + global_ota_component->update_chunk_wrote(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_PENDING == event) { + global_ota_component->update_pending(); + } else if (MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED == event) { + global_ota_component->update_stopped(); + } else { + ESP_LOGD(TAG, "MCUmgr Image Management Event with the %d ID", u32_count_trailing_zeros(MGMT_EVT_GET_ID(event))); + } + return MGMT_CB_OK; +} + +OTAComponent::OTAComponent() { global_ota_component = this; } + +void OTAComponent::setup() { + this->img_mgmt_callback_.callback = mcumgr_img_mgmt_cb; + this->img_mgmt_callback_.event_id = MGMT_EVT_OP_IMG_MGMT_ALL; + mgmt_callback_register(&this->img_mgmt_callback_); +#ifdef CONFIG_USB_DEVICE_STACK + usb_enable(nullptr); +#endif +// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, +// in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + if (!boot_is_img_confirmed()) { + boot_write_img_confirmed(); + } +#endif +} + +#ifdef ESPHOME_LOG_HAS_CONFIG +static const char *swap_type_str(uint8_t type) { + switch (type) { + case BOOT_SWAP_TYPE_NONE: + return "none"; + case BOOT_SWAP_TYPE_TEST: + return "test"; + case BOOT_SWAP_TYPE_PERM: + return "perm"; + case BOOT_SWAP_TYPE_REVERT: + return "revert"; + case BOOT_SWAP_TYPE_FAIL: + return "fail"; + } + + return "unknown"; +} +#endif + +void OTAComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Over-The-Air Updates:\n" + " swap type after reboot: %s\n" + " image confirmed: %s", + swap_type_str(mcuboot_swap_type()), YESNO(boot_is_img_confirmed())); +} + +void OTAComponent::update_chunk(const img_mgmt_upload_check &upload) { + float percentage = (upload.req->off * 100.0f) / upload.action->size; + this->defer([this, percentage]() { this->percentage_ = percentage; }); +} + +void OTAComponent::update_started() { + this->defer([this]() { + ESP_LOGD(TAG, "Starting update"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); +#endif + }); +} + +void OTAComponent::update_chunk_wrote() { + uint32_t now = millis(); + if (now - this->last_progress_ > 1000) { + this->last_progress_ = now; + this->defer([this]() { + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", this->percentage_); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, this->percentage_, 0); +#endif + }); + } +} + +void OTAComponent::update_pending() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA pending"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); +#endif + }); +} + +void OTAComponent::update_stopped() { + this->defer([this]() { + ESP_LOGD(TAG, "OTA stopped"); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#endif + }); +} + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h new file mode 100644 index 00000000000..ab98f93598d --- /dev/null +++ b/esphome/components/zephyr_mcumgr/ota/ota_zephyr_mcumgr.h @@ -0,0 +1,29 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ZEPHYR +#include "esphome/components/ota/ota_backend.h" +#include <zephyr/mgmt/mcumgr/mgmt/callbacks.h> + +struct img_mgmt_upload_check; + +namespace esphome::zephyr_mcumgr { + +class OTAComponent : public ota::OTAComponent { + public: + OTAComponent(); + void setup() override; + void dump_config() override; + void update_chunk(const img_mgmt_upload_check &upload); + void update_started(); + void update_chunk_wrote(); + void update_pending(); + void update_stopped(); + + protected: + uint32_t last_progress_ = 0; + float percentage_ = 0; + mgmt_callback img_mgmt_callback_{}; +}; + +} // namespace esphome::zephyr_mcumgr +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c5f38ab9aab..48c467f69f3 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -367,6 +367,7 @@ #define USE_NRF52_DFU #define USE_NRF52_REG0_VOUT 5 #define USE_NRF52_UICR_ERASE +#define USE_OTA_ROLLBACK #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 #define USE_ZIGBEE diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index 1242a60cf4e..66ef6ffc98b 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -28,6 +28,22 @@ extern "C" void zboss_signal_handler() {}; CONFIG_NEWLIB_LIBC=y CONFIG_BT=y CONFIG_ADC=y +#mcumgr begin +CONFIG_NET_BUF=y +CONFIG_ZCBOR=y +CONFIG_MCUMGR=y +CONFIG_MCUMGR_GRP_IMG=y +CONFIG_IMG_MANAGER=y +CONFIG_STREAM_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH=y +CONFIG_IMG_ERASE_PROGRESSIVELY=y +CONFIG_BOOTLOADER_MCUBOOT=y +CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_STATUS_HOOKS=y +CONFIG_MCUMGR_GRP_IMG_UPLOAD_CHECK_HOOK=y +CONFIG_MCUMGR_TRANSPORT_UART=y +#mcumgr end #zigbee begin CONFIG_ZIGBEE=y CONFIG_CRYPTO=y diff --git a/tests/components/ota/test.nrf52-mcumgr.yaml b/tests/components/ota/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..1e7986f61a5 --- /dev/null +++ b/tests/components/ota/test.nrf52-mcumgr.yaml @@ -0,0 +1,27 @@ +zephyr_ble_server: + +ota: + - platform: zephyr_mcumgr + transport: + ble: true + hardware_uart: CDC + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); From d9e76da8064ba955c2406d13e07e1377ca77ea9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 07:59:25 +0000 Subject: [PATCH 0482/2056] Bump aioesphomeapi from 44.4.0 to 44.5.0 (#14617) 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 a1300a67f7c..4124bf87915 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.4.0 +aioesphomeapi==44.5.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From a530aeec22174300264d58f791518d993658e9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 7 Mar 2026 22:09:12 -1000 Subject: [PATCH 0483/2056] [api] Inline varint and encode_varint_raw fast paths for hot loop performance (#14607) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/proto.cpp | 12 ++++++++ esphome/components/api/proto.h | 51 ++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 1ca6b702ada..fb229928e5a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,6 +8,18 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); } + +void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { + do { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + } while (value > 0x7F); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast<uint8_t>(value); +} + #ifdef USE_API_VARINT64 optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index bbdd11b29d0..adde0a8a85b 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -240,14 +240,13 @@ class ProtoWriteBuffer { ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} - void encode_varint_raw(uint32_t value) { - while (value > 0x7F) { + inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { + if (value < 128) [[likely]] { this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value | 0x80); - value >>= 7; + *this->pos_++ = static_cast<uint8_t>(value); + return; } - this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value); + this->encode_varint_raw_slow_(value); } void encode_varint_raw_64(uint64_t value) { while (value > 0x7F) { @@ -378,6 +377,9 @@ class ProtoWriteBuffer { std::vector<uint8_t> *get_buffer() const { return buffer_; } protected: + // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small + void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline)); + #ifdef ESPHOME_DEBUG_API void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); @@ -511,24 +513,29 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static constexpr uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding - if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) - } + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { + if (value < 128) [[likely]] + return 1; // Fast path: 7 bits, most common case + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); } + private: + // Slow path for varint >= 128, outlined to keep fast path small + static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); + // Shared cascade for values >= 128 (used by both constexpr and noinline paths) + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { + if (value < 16384) + return 2; + if (value < 2097152) + return 3; + if (value < 268435456) + return 4; + return 5; + } + + public: /** * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint * From 2c705810cdaff126c8fa5270e012c8037c62311b Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 09:40:52 +0100 Subject: [PATCH 0484/2056] [nrf52] allow to update OTA via cmd (#12344) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/nrf52/__init__.py | 38 +++++-- esphome/components/nrf52/ota.py | 164 +++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 esphome/components/nrf52/ota.py diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index c3a10a99440..5054e5e0df4 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -392,22 +392,42 @@ def _upload_using_platformio( def upload_program(config: ConfigType, args, host: str) -> bool: from esphome.__main__ import check_permissions, get_port_type - result = 0 - handled = False + mcumgr_device: str | None = None if get_port_type(host) == "SERIAL": check_permissions(host) - result = _upload_using_platformio(config, host, ["-t", "upload"]) - handled = True + if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: + mcumgr_device = host + else: + result = _upload_using_platformio(config, host, ["-t", "upload"]) + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio serial upload if host == "PYOCD": result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) - handled = True + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + return True # Handled: platformio PYOCD upload - if result != 0: - raise EsphomeError(f"Upload failed with result: {result}") + # Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths + from .ble_logger import is_mac_address + from .ota import smpmgr_scan, smpmgr_upload - return handled + if host == "BLE": + mcumgr_device = asyncio.run(smpmgr_scan(CORE.name)) + + if is_mac_address(host): + mcumgr_device = host + + if mcumgr_device: + firmware = Path( + CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin") + ).resolve() + asyncio.run(smpmgr_upload(mcumgr_device, firmware)) + return True # Handled: mcumgr OTA upload + + return False # Not handled: let caller try default upload methods def show_logs(config: ConfigType, args, devices: list[str]) -> bool: @@ -415,7 +435,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool: from .ble_logger import is_mac_address, logger_connect, logger_scan if devices[0] == "BLE": - ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) + ble_device = asyncio.run(logger_scan(CORE.name)) if ble_device: address = ble_device.address else: diff --git a/esphome/components/nrf52/ota.py b/esphome/components/nrf52/ota.py new file mode 100644 index 00000000000..e4b26b45eb6 --- /dev/null +++ b/esphome/components/nrf52/ota.py @@ -0,0 +1,164 @@ +import asyncio +from dataclasses import asdict +import json +import logging +from pathlib import Path + +from bleak import BleakScanner +from bleak.exc import BleakDeviceNotFoundError +from smp.exceptions import SMPBadStartDelimiter +from smpclient import SMPClient +from smpclient.generics import error, success +from smpclient.mcuboot import IMAGE_TLV, ImageInfo, MCUBootImageError, TLVNotFound +from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite +from smpclient.requests.os_management import ResetWrite +from smpclient.transport import SMPTransportDisconnected +from smpclient.transport.ble import ( + SMPBLETransport, + SMPBLETransportDeviceNotFound, + SMPBLETransportException, +) +from smpclient.transport.serial import SMPSerialTransport + +from esphome.core import EsphomeError +from esphome.espota2 import ProgressBar + +from .ble_logger import is_mac_address + +SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84" +BLE_SCAN_TIMEOUT = 10.0 # seconds +RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute + +_LOGGER = logging.getLogger(__name__) + + +def _json_state(o: object) -> object: + """JSON serializer for SMP image state objects.""" + if isinstance(o, (bytes, bytearray)): + return o.hex() + if hasattr(o, "hex"): + return o.hex() + if hasattr(o, "__dict__"): + return vars(o) + return str(o) + + +async def smpmgr_scan(name: str) -> str: + _LOGGER.info("Scanning bluetooth for %s...", name) + for device in await BleakScanner.discover( + timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID] + ): + if device.name == name: + return device.address + raise EsphomeError(f"BLE device {name} with OTA service not found") + + +async def smpmgr_upload(device: str, firmware: Path) -> None: + try: + await _smpmgr_upload(device, firmware) + except SMPTransportDisconnected as exc: + raise EsphomeError(f"{device} was disconnected.") from exc + except SMPBLETransportDeviceNotFound as exc: + raise EsphomeError(f"{device} was not found.") from exc + + +def _get_image_tlv_sha256(file: Path) -> bytes: + _LOGGER.info("Checking image: %s", str(file)) + try: + image_info = ImageInfo.load_file(str(file)) + _LOGGER.info( + "Image header:\n%s", json.dumps(asdict(image_info.header), indent=2) + ) + _LOGGER.debug(str(image_info)) + except MCUBootImageError as exc: + raise EsphomeError("Inspection of FW image failed") from exc + except FileNotFoundError as exc: + raise EsphomeError( + f"Firmware image file not found: {file}. Build with zephyr_mcumgr enabled" + ) from exc + + try: + image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256) + _LOGGER.info("Image tlv sha256: %s", image_tlv_sha256) + except TLVNotFound as exc: + raise EsphomeError("Could not find IMAGE_TLV_SHA256 in image.") from exc + return image_tlv_sha256.value + + +async def _smpmgr_upload(device: str, firmware: Path) -> None: + image_tlv_sha256 = _get_image_tlv_sha256(firmware) + + if is_mac_address(device): + smp_client = SMPClient(SMPBLETransport(), device) + else: + smp_client = SMPClient(SMPSerialTransport(), device) + + _LOGGER.info("Connecting %s...", device) + try: + await smp_client.connect() + except BleakDeviceNotFoundError as exc: + raise EsphomeError(f"Device {device} not found") from exc + except SMPBLETransportException as exc: + raise EsphomeError(f"Connection error with {device}") from exc + + _LOGGER.info("Connected %s...", device) + try: + await _smpmgr_upload_connected(smp_client, device, firmware, image_tlv_sha256) + finally: + await smp_client.disconnect() + + +async def _smpmgr_upload_connected( + smp_client: SMPClient, device: str, firmware: Path, image_tlv_sha256: bytes +) -> None: + try: + image_state = await smp_client.request(ImageStatesRead(), 2.5) + except (SMPBadStartDelimiter, TimeoutError) as exc: + raise EsphomeError(f"mcumgr is not supported by device ({device})") from exc + + already_uploaded = False + + if error(image_state): + raise EsphomeError(f"Failed to read image state from {device}: {image_state}") + if success(image_state): + if len(image_state.images) == 0: + _LOGGER.warning("No images on device!") + for image in image_state.images: + _LOGGER.info( + "Image state:\n%s", + json.dumps(image, indent=2, default=_json_state), + ) + if image.active and not image.confirmed: + raise EsphomeError("No free slot. Testing mode but not confirmed yet.") + if image.hash == image_tlv_sha256: + if already_uploaded: + raise EsphomeError("Both slots have the same image already") + if image.confirmed: + raise EsphomeError("The same image already confirmed") + _LOGGER.warning("The same image already uploaded") + already_uploaded = True + + if not already_uploaded: + with open(firmware, "rb") as file: + image = file.read() + upload_size = len(image) + progress = ProgressBar() + progress.update(0) + try: + async for offset in smp_client.upload(image): + progress.update(offset / upload_size) + finally: + progress.done() + + _LOGGER.info("Mark image for testing") + r = await smp_client.request(ImageStatesWrite(hash=image_tlv_sha256), 1.0) + + if error(r): + raise EsphomeError(f"Failed to mark image for testing on {device}: {r}") + + await asyncio.sleep(RESET_DELAY) + _LOGGER.info("Reset") + r = await smp_client.request(ResetWrite(), 1.0) + + if error(r): + raise EsphomeError(f"Failed to reset {device}: {r}") diff --git a/requirements.txt b/requirements.txt index 4124bf87915..03a7cac5c72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 +smpclient==6.0.0 requests==2.32.5 # esp-idf >= 5.0 requires this From 0c4a44566f9ba56d5f01bb6af2b436a1c859128b Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 03:55:49 -0500 Subject: [PATCH 0485/2056] [serial_proxy] New component (#13944) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 1 + esphome/components/api/api.proto | 20 ++ esphome/components/api/api_connection.cpp | 93 +++++++++ esphome/components/api/api_connection.h | 10 + esphome/components/api/api_pb2.cpp | 14 ++ esphome/components/api/api_pb2.h | 26 +++ esphome/components/api/api_pb2_dump.cpp | 24 +++ esphome/components/api/api_pb2_service.h | 1 + esphome/components/serial_proxy/__init__.py | 104 ++++++++++ .../components/serial_proxy/serial_proxy.cpp | 188 ++++++++++++++++++ .../components/serial_proxy/serial_proxy.h | 129 ++++++++++++ esphome/core/application.h | 17 ++ esphome/core/defines.h | 2 + tests/components/serial_proxy/common.yaml | 10 + .../serial_proxy/test.esp32-idf.yaml | 8 + .../serial_proxy/test.esp8266-ard.yaml | 8 + .../serial_proxy/test.rp2040-ard.yaml | 8 + 17 files changed, 663 insertions(+) create mode 100644 esphome/components/serial_proxy/__init__.py create mode 100644 esphome/components/serial_proxy/serial_proxy.cpp create mode 100644 esphome/components/serial_proxy/serial_proxy.h create mode 100644 tests/components/serial_proxy/common.yaml create mode 100644 tests/components/serial_proxy/test.esp32-idf.yaml create mode 100644 tests/components/serial_proxy/test.esp8266-ard.yaml create mode 100644 tests/components/serial_proxy/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d60dbc729d9..cb415bb625a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -435,6 +435,7 @@ esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core +esphome/components/serial_proxy/* @kbx81 esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 257a7aaf827..28332d67a59 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2618,6 +2618,14 @@ enum SerialProxyRequestType { SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) } +enum SerialProxyStatus { + SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed + SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error + SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance +} + // Generic request message for simple serial proxy operations message SerialProxyRequest { option (id) = 144; @@ -2628,6 +2636,18 @@ message SerialProxyRequest { SerialProxyRequestType type = 2; // Request type } +// Response to a SerialProxyRequest (e.g. flush completion or failure) +message SerialProxyRequestResponse { + option (id) = 147; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Which request type this responds to + SerialProxyStatus status = 3; // Result status + string error_message = 4; // Additional detail on failure (optional) +} + // ==================== BLUETOOTH CONNECTION PARAMS ==================== message BluetoothSetConnectionParamsRequest { option (id) = 145; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b9b33ddcc22..43f5070a405 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1445,6 +1445,89 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif +#ifdef USE_SERIAL_PROXY +void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance, + static_cast<uint32_t>(proxies.size())); + return; + } + proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, + msg.data_size); +} + +void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->write_from_client(msg.data, msg.data_len); +} + +void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + proxies[msg.instance]->set_modem_pins(msg.line_states); +} + +void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + SerialProxyGetModemPinsResponse resp{}; + resp.instance = msg.instance; + resp.line_states = proxies[msg.instance]->get_modem_pins(); + this->send_message(resp); +} + +void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + return; + } + switch (msg.type) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + proxies[msg.instance]->serial_proxy_request(this, msg.type); + break; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { + SerialProxyRequestResponse resp{}; + resp.instance = msg.instance; + resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; + switch (proxies[msg.instance]->flush_port()) { + case uart::FlushResult::SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_OK; + break; + case uart::FlushResult::ASSUMED_SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + break; + case uart::FlushResult::TIMEOUT: + resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; + break; + case uart::FlushResult::FAILED: + resp.status = enums::SERIAL_PROXY_STATUS_ERROR; + break; + } + this->send_message(resp); + break; + } + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type)); + break; + } +} + +void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); } +#endif + #ifdef USE_INFRARED uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast<infrared::Infrared *>(entity); @@ -1666,6 +1749,16 @@ bool APIConnection::send_device_info_response_() { resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); #endif +#ifdef USE_SERIAL_PROXY + size_t serial_proxy_index = 0; + for (auto const &proxy : App.get_serial_proxies()) { + if (serial_proxy_index >= SERIAL_PROXY_COUNT) + break; + auto &info = resp.serial_proxies[serial_proxy_index++]; + info.name = StringRef(proxy->get_name()); + info.port_type = proxy->get_port_type(); + } +#endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b075bc83ab2..5d1469e419f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -189,6 +189,15 @@ class APIConnection final : public APIServerConnectionBase { void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; + void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void send_serial_proxy_data(const SerialProxyDataReceived &msg); +#endif + #ifdef USE_EVENT void send_event(event::Event *event); #endif @@ -254,6 +263,7 @@ class APIConnection final : public APIServerConnectionBase { return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + bool is_marked_for_removal() const { return this->flags_.remove; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } // Get client API version for feature detection diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 38ebfb94649..6fce10ca0fe 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3840,6 +3840,20 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } return true; } +void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); + buffer.encode_uint32(3, static_cast<uint32_t>(this->status)); + buffer.encode_string(4, this->error_message); +} +uint32_t SerialProxyRequestResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->status)); + size += ProtoSize::calc_length(1, this->error_message.size()); + return size; +} #endif #ifdef USE_BLUETOOTH_PROXY bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a6167dc8101..5c712508b9a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -333,6 +333,13 @@ enum SerialProxyRequestType : uint32_t { SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, }; +enum SerialProxyStatus : uint32_t { + SERIAL_PROXY_STATUS_OK = 0, + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1, + SERIAL_PROXY_STATUS_ERROR = 2, + SERIAL_PROXY_STATUS_TIMEOUT = 3, + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, +}; #endif } // namespace enums @@ -3220,6 +3227,25 @@ class SerialProxyRequest final : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class SerialProxyRequestResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 147; + static constexpr uint8_t ESTIMATED_SIZE = 17; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request_response"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; + enums::SerialProxyStatus status{}; + StringRef error_message{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; #endif #ifdef USE_BLUETOOTH_PROXY class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 086b1bdc2f0..740bf2e47fd 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -789,6 +789,22 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums return "UNKNOWN"; } } +template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) { + switch (value) { + case enums::SERIAL_PROXY_STATUS_OK: + return "SERIAL_PROXY_STATUS_OK"; + case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: + return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + case enums::SERIAL_PROXY_STATUS_ERROR: + return "SERIAL_PROXY_STATUS_ERROR"; + case enums::SERIAL_PROXY_STATUS_TIMEOUT: + return "SERIAL_PROXY_STATUS_TIMEOUT"; + case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: + return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + default: + return "UNKNOWN"; + } +} #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { @@ -2609,6 +2625,14 @@ const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); return out.c_str(); } +const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequestResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); + dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status)); + dump_field(out, "error_message", this->error_message); + return out.c_str(); +} #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index a031d2d969e..10fd88d8e13 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -233,6 +233,7 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_SERIAL_PROXY virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif + #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif diff --git a/esphome/components/serial_proxy/__init__.py b/esphome/components/serial_proxy/__init__.py new file mode 100644 index 00000000000..f9b8c375d21 --- /dev/null +++ b/esphome/components/serial_proxy/__init__.py @@ -0,0 +1,104 @@ +""" +Serial Proxy component for ESPHome. + +WARNING: This component is EXPERIMENTAL. The API (both Python configuration +and C++ interfaces) may change at any time without following the normal +breaking changes policy. Use at your own risk. + +Once the API is considered stable, this warning will be removed. + +Provides a proxy to/from a serial interface on the ESPHome device, allowing +Home Assistant to connect to the serial port and send/receive data to/from +an arbitrary serial device. +""" + +from dataclasses import dataclass + +from esphome import pins +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NAME +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["api", "uart"] + +MULTI_CONF = True + +serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy") +SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice) + +api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums") +SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType") +SERIAL_PROXY_PORT_TYPES = { + "TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL, + "RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232, + "RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485, +} + +CONF_DTR_PIN = "dtr_pin" +CONF_PORT_TYPE = "port_type" +CONF_RTS_PIN = "rts_pin" + +DOMAIN = "serial_proxy" + + +@dataclass +class SerialProxyData: + count: int = 0 + + +def _get_data() -> SerialProxyData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = SerialProxyData() + return CORE.data[DOMAIN] + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SerialProxy), + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True), + cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_serial_proxy_count_define(): + """Emit the SERIAL_PROXY_COUNT define once with the final instance count.""" + count = _get_data().count + if count > 0: + cg.add_define("SERIAL_PROXY_COUNT", count) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(cg.App.register_serial_proxy(var)) + cg.add(var.set_name(config[CONF_NAME])) + cg.add(var.set_port_type(config[CONF_PORT_TYPE])) + cg.add_define("USE_SERIAL_PROXY") + + # Track instance count for the FINAL priority define + data = _get_data() + if data.count == 0: + # Schedule the count define job only once (on the first instance) + CORE.add_job(_add_serial_proxy_count_define) + data.count += 1 + + if CONF_RTS_PIN in config: + rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN]) + cg.add(var.set_rts_pin(rts_pin)) + + if CONF_DTR_PIN in config: + dtr_pin = await cg.gpio_pin_expression(config[CONF_DTR_PIN]) + cg.add(var.set_dtr_pin(dtr_pin)) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp new file mode 100644 index 00000000000..340f9b0cb85 --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -0,0 +1,188 @@ +#include "serial_proxy.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#ifdef USE_API +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome::serial_proxy { + +static const char *const TAG = "serial_proxy"; + +void SerialProxy::setup() { + // Set up modem control pins if configured + if (this->rts_pin_ != nullptr) { + this->rts_pin_->setup(); + this->rts_pin_->digital_write(this->rts_state_); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_pin_->setup(); + this->dtr_pin_->digital_write(this->dtr_state_); + } +#ifdef USE_API + // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data + this->outgoing_msg_.instance = this->instance_index_; +#endif +} + +void SerialProxy::loop() { +#ifdef USE_API + // Detect subscriber disconnect + if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() || + !this->api_connection_->is_connection_setup() || !api_is_connected())) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; + } + + if (this->api_connection_ == nullptr) + return; + + // Read available data from UART and forward to subscribed client + size_t available = this->available(); + if (available == 0) + return; + + // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE + uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE]; + size_t to_read = std::min(available, sizeof(buffer)); + + if (!this->read_array(buffer, to_read)) + return; + + this->outgoing_msg_.set_data(buffer, to_read); + this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); +#endif +} + +void SerialProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "Serial Proxy [%u]:\n" + " Name: %s\n" + " Port Type: %s\n" + " RTS Pin: %s\n" + " DTR Pin: %s", + this->instance_index_, this->name_ != nullptr ? this->name_ : "", + this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? "RS485" + : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232" + : "TTL", + this->rts_pin_ != nullptr ? "configured" : "not configured", + this->dtr_pin_ != nullptr ? "configured" : "not configured"); +} + +void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, + uint8_t data_size) { + ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u", + this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size); + + auto *uart_comp = this->parent_; + if (uart_comp == nullptr) { + ESP_LOGE(TAG, "UART component not available"); + return; + } + + // Validate all parameters before applying any (values come from a remote client) + if (baudrate == 0) { + ESP_LOGW(TAG, "Invalid baud rate: 0"); + return; + } + if (stop_bits < 1 || stop_bits > 2) { + ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits); + return; + } + if (data_size < 5 || data_size > 8) { + ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size); + return; + } + if (parity > 2) { + ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity); + return; + } + + // Apply validated parameters + uart_comp->set_baud_rate(baudrate); + uart_comp->set_stop_bits(stop_bits); + uart_comp->set_data_bits(data_size); + + // Map parity value to UARTParityOptions + static const uart::UARTParityOptions PARITY_MAP[] = { + uart::UART_CONFIG_PARITY_NONE, + uart::UART_CONFIG_PARITY_EVEN, + uart::UART_CONFIG_PARITY_ODD, + }; + uart_comp->set_parity(PARITY_MAP[parity]); + + // load_settings() is available on ESP8266 and ESP32 platforms +#if defined(USE_ESP8266) || defined(USE_ESP32) + uart_comp->load_settings(true); +#endif + + if (flow_control) { + ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported"); + } +} + +void SerialProxy::write_from_client(const uint8_t *data, size_t len) { + if (data == nullptr || len == 0) + return; + this->write_array(data, len); +} + +void SerialProxy::set_modem_pins(uint32_t line_states) { + const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; + const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; + ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); + + if (this->rts_pin_ != nullptr) { + this->rts_state_ = rts; + this->rts_pin_->digital_write(rts); + } + if (this->dtr_pin_ != nullptr) { + this->dtr_state_ = dtr; + this->dtr_pin_->digital_write(dtr); + } +} + +uint32_t SerialProxy::get_modem_pins() const { + return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) | + (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); +} + +uart::FlushResult SerialProxy::flush_port() { + ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); + return this->flush(); +} + +#ifdef USE_API +void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { + switch (type) { + case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); + break; + case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_); + return; + } + this->api_connection_ = nullptr; + ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); + break; + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(type)); + break; + } +} +#endif + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h new file mode 100644 index 00000000000..52f0654ff0c --- /dev/null +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -0,0 +1,129 @@ +#pragma once + +// WARNING: This component is EXPERIMENTAL. The API may change at any time +// without following the normal breaking changes policy. Use at your own risk. +// Once the API is considered stable, this warning will be removed. + +#include "esphome/core/defines.h" + +#ifdef USE_SERIAL_PROXY + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/uart/uart.h" + +// Include api_pb2.h only when the API is enabled. The full include is needed +// to hold SerialProxyDataReceived by value as a pre-allocated member. +// Guarding prevents pulling conflicting Zephyr logging macro names into +// translation units that include this header without USE_API defined. +#ifdef USE_API +#include "esphome/components/api/api_pb2.h" +#endif + +// Forward-declare types needed outside the USE_API guard. +namespace esphome::api { +class APIConnection; +namespace enums { +enum SerialProxyPortType : uint32_t; +enum SerialProxyRequestType : uint32_t; +} // namespace enums +} // namespace esphome::api + +namespace esphome::serial_proxy { + +/// Bit flags for the line_states field exchanged with API clients. +/// Bit positions are stable API — new signals must use the next available bit. +enum SerialProxyLineStateFlag : uint32_t { + SERIAL_PROXY_LINE_STATE_FLAG_RTS = 1 << 0, ///< RTS (Request To Send) + SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready) +}; + +/// Maximum bytes to read from UART in a single loop iteration +inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; + +class SerialProxy : public uart::UARTDevice, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + /// Get the instance index (position in Application's serial_proxies_ vector) + uint32_t get_instance_index() const { return this->instance_index_; } + + /// Set the instance index (called by Application::register_serial_proxy) + void set_instance_index(uint32_t index) { this->instance_index_ = index; } + + /// Set the human-readable port name (from YAML configuration) + void set_name(const char *name) { this->name_ = name; } + + /// Get the human-readable port name + const char *get_name() const { return this->name_; } + + /// Set the port type (from YAML configuration) + void set_port_type(api::enums::SerialProxyPortType port_type) { this->port_type_ = port_type; } + + /// Get the port type + api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } + + /// Configure UART parameters and apply them + /// @param baudrate Baud rate in bits per second + /// @param flow_control True to enable hardware flow control + /// @param parity Parity setting (0=none, 1=even, 2=odd) + /// @param stop_bits Number of stop bits (1 or 2) + /// @param data_size Number of data bits (5-8) + void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + + /// Handle a subscribe/unsubscribe request from an API client + void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); + + /// Write data received from an API client to the serial device + /// @param data Pointer to data buffer + /// @param len Number of bytes to write + void write_from_client(const uint8_t *data, size_t len); + + /// Set modem pin states from a bitmask of SerialProxyLineStateFlag values + void set_modem_pins(uint32_t line_states); + + /// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values + uint32_t get_modem_pins() const; + + /// Flush the serial port (block until all TX data is sent) + uart::FlushResult flush_port(); + + /// Set the RTS GPIO pin (from YAML configuration) + void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } + + /// Set the DTR GPIO pin (from YAML configuration) + void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } + + protected: + /// Instance index for identifying this proxy in API messages + uint32_t instance_index_{0}; + + /// Subscribed API client (only one allowed at a time) + api::APIConnection *api_connection_{nullptr}; + +#ifdef USE_API + /// Pre-allocated outgoing message; instance field is set once in setup() + api::SerialProxyDataReceived outgoing_msg_; +#endif + + /// Human-readable port name (points to a string literal in flash) + const char *name_{nullptr}; + + /// Port type + api::enums::SerialProxyPortType port_type_{}; + + /// Optional GPIO pins for modem control + GPIOPin *rts_pin_{nullptr}; + GPIOPin *dtr_pin_{nullptr}; + + /// Current modem pin states + bool rts_state_{false}; + bool dtr_state_{false}; +}; + +} // namespace esphome::serial_proxy + +#endif // USE_SERIAL_PROXY diff --git a/esphome/core/application.h b/esphome/core/application.h index 87f9fdf59a1..49253b63244 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -102,6 +102,9 @@ void socket_wake(); // NOLINT(readability-redundant-declaration) #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_SERIAL_PROXY +#include "esphome/components/serial_proxy/serial_proxy.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -267,6 +270,13 @@ class Application { void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } #endif +#ifdef USE_SERIAL_PROXY + void register_serial_proxy(serial_proxy::SerialProxy *proxy) { + proxy->set_instance_index(this->serial_proxies_.size()); + this->serial_proxies_.push_back(proxy); + } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -498,6 +508,10 @@ class Application { GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) #endif +#ifdef USE_SERIAL_PROXY + auto &get_serial_proxies() const { return this->serial_proxies_; } +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -747,6 +761,9 @@ class Application { #ifdef USE_INFRARED StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{}; #endif +#ifdef USE_SERIAL_PROXY + StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{}; +#endif #ifdef USE_UPDATE StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 48c467f69f3..51f474d80ef 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,7 @@ #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 +#define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE @@ -119,6 +120,7 @@ #define USE_SELECT #define USE_SENSOR #define USE_SENSOR_FILTER +#define USE_SERIAL_PROXY #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR diff --git a/tests/components/serial_proxy/common.yaml b/tests/components/serial_proxy/common.yaml new file mode 100644 index 00000000000..6f03cf95dff --- /dev/null +++ b/tests/components/serial_proxy/common.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +serial_proxy: + - id: serial_proxy_1 + name: Test Serial Port + port_type: RS232 diff --git a/tests/components/serial_proxy/test.esp32-idf.yaml b/tests/components/serial_proxy/test.esp32-idf.yaml new file mode 100644 index 00000000000..b415125e84b --- /dev/null +++ b/tests/components/serial_proxy/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.esp8266-ard.yaml b/tests/components/serial_proxy/test.esp8266-ard.yaml new file mode 100644 index 00000000000..96ab4ef6aca --- /dev/null +++ b/tests/components/serial_proxy/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/serial_proxy/test.rp2040-ard.yaml b/tests/components/serial_proxy/test.rp2040-ard.yaml new file mode 100644 index 00000000000..b28f2b5e05e --- /dev/null +++ b/tests/components/serial_proxy/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml From a9b5f95c768c58aeb4e117a94effe0121fdb77c2 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke <okleinecke@web.de> Date: Sun, 8 Mar 2026 11:24:39 +0100 Subject: [PATCH 0486/2056] [usb_uart] ch34x chip-type & port-count enumeration (#14544) --- esphome/components/usb_uart/ch34x.cpp | 94 ++++++++++++++++++++++++-- esphome/components/usb_uart/usb_uart.h | 37 ++++++++++ 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index e6e52a9e2ac..d5428cc8d79 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -8,13 +8,97 @@ namespace esphome::usb_uart { using namespace bytebuffer; -/** - * CH34x - */ + +struct CH34xEntry { + uint16_t pid; + uint8_t byte_idx; // which status.data[] byte to inspect + uint8_t mask; // bitmask applied before comparison + uint8_t match; // 0xFF = wildcard (default/fallthrough for this PID) + CH34xChipType chiptype; + const char *name; + uint8_t num_ports; +}; + +static const CH34xEntry CH34X_TABLE[] = { + {0x55D2, 1, 0xFF, 0x41, CHIP_CH342K, "CH342K", 2}, + {0x55D2, 1, 0xFF, 0xFF, CHIP_CH342F, "CH342F", 2}, + {0x55D3, 1, 0xFF, 0x02, CHIP_CH343J, "CH343J", 1}, + {0x55D3, 1, 0xFF, 0x01, CHIP_CH343K, "CH343K", 1}, + {0x55D3, 1, 0xFF, 0x18, CHIP_CH343G_AUTOBAUD, "CH343G_AUTOBAUD", 1}, + {0x55D3, 1, 0xFF, 0xFF, CHIP_CH343GP, "CH343GP", 1}, + {0x55D4, 1, 0xFF, 0x09, CHIP_CH9102X, "CH9102X", 1}, + {0x55D4, 1, 0xFF, 0xFF, CHIP_CH9102F, "CH9102F", 1}, + {0x55D5, 1, 0xFF, 0xC0, CHIP_CH344L, "CH344L", 4}, // CH344L vs CH344L_V2 resolved below + {0x55D5, 1, 0xFF, 0xFF, CHIP_CH344Q, "CH344Q", 4}, + {0x55D7, 1, 0xFF, 0xFF, CHIP_CH9103M, "CH9103M", 2}, + {0x55D8, 1, 0xFF, 0x0A, CHIP_CH9101RY, "CH9101RY", 1}, + {0x55D8, 1, 0xFF, 0xFF, CHIP_CH9101UH, "CH9101UH", 1}, + {0x55DB, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DD, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 1}, + {0x55DA, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55DE, 1, 0xFF, 0xFF, CHIP_CH347TF, "CH347TF", 2}, + {0x55E7, 1, 0xFF, 0xFF, CHIP_CH339W, "CH339W", 1}, + {0x55DF, 1, 0xFF, 0xFF, CHIP_CH9104L, "CH9104L", 4}, + {0x55E9, 1, 0xFF, 0xFF, CHIP_CH9111L_M0, "CH9111L_M0", 1}, + {0x55EA, 1, 0xFF, 0xFF, CHIP_CH9111L_M1, "CH9111L_M1", 1}, + {0x55E8, 2, 0xFF, 0x48, CHIP_CH9114L, "CH9114L", 4}, + {0x55E8, 2, 0xFF, 0x49, CHIP_CH9114W, "CH9114W", 4}, + {0x55E8, 2, 0xFF, 0x4A, CHIP_CH9114F, "CH9114F", 4}, + {0x55EB, 4, 0x01, 0x01, CHIP_CH346C_M1, "CH346C_M1", 1}, + {0x55EB, 4, 0x01, 0xFF, CHIP_CH346C_M0, "CH346C_M0", 1}, + {0x55EC, 1, 0xFF, 0xFF, CHIP_CH346C_M2, "CH346C_M2", 2}, +}; void USBUartTypeCH34X::enable_channels() { - // enable the channels - for (auto channel : this->channels_) { + usb_host::transfer_cb_t cb = [this](const usb_host::TransferStatus &status) { + if (!status.success) { + this->defer([this, error_code = status.error_code]() { + ESP_LOGE(TAG, "CH34x chip detection failed: %s", esp_err_to_name(error_code)); + this->apply_line_settings_(); + }); + return; + } + CH34xChipType chiptype = CHIP_UNKNOWN; + uint8_t num_ports = 1; + for (const auto &e : CH34X_TABLE) { + if (e.pid != this->pid_) + continue; + if (e.match != 0xFF && (status.data[e.byte_idx] & e.mask) != e.match) + continue; + chiptype = e.chiptype; + num_ports = e.num_ports; + break; + } + // CH344L vs CH344L_V2 requires chipver (data[0]) in addition to chiptype (data[1]) + if (chiptype == CHIP_CH344L && (status.data[0] & 0xF0) != 0x40) + chiptype = CHIP_CH344L_V2; + const char *name = "unknown"; + for (const auto &e : CH34X_TABLE) { + if (e.chiptype == chiptype) { + name = e.name; + break; + } + } + this->defer([this, chiptype, num_ports, name]() { + this->chiptype_ = chiptype; + this->chip_name_ = name; + this->num_ports_ = num_ports; + ESP_LOGD(TAG, "CH34x chip: %s, ports: %u", name, this->num_ports_); + this->apply_line_settings_(); + }); + }; + // Vendor-specific GET_CHIP_VERSION request (bRequest=0x5F): returns chip ID bytes + // used to distinguish CH34x variants sharing the same PID. + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_IN, 0x5F, 0, 0, cb, {0, 0, 0, 0, 0, 0, 0, 0}); +} + +void USBUartTypeCH34X::dump_config() { + USBUartTypeCdcAcm::dump_config(); + ESP_LOGCONFIG(TAG, " CH34x chip: %s", this->chip_name_); +} + +void USBUartTypeCH34X::apply_line_settings_() { + for (auto *channel : this->channels_) { if (!channel->initialised_.load()) continue; usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index b1748aebf28..16469df7f60 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -35,6 +35,36 @@ struct CdcEps { uint8_t interrupt_interface_number; }; +enum CH34xChipType : uint8_t { + CHIP_CH342F = 0, + CHIP_CH342K, + CHIP_CH343GP, + CHIP_CH343G_AUTOBAUD, + CHIP_CH343K, + CHIP_CH343J, + CHIP_CH344L, + CHIP_CH344L_V2, + CHIP_CH344Q, + CHIP_CH347TF, + CHIP_CH9101UH, + CHIP_CH9101RY, + CHIP_CH9102F, + CHIP_CH9102X, + CHIP_CH9103M, + CHIP_CH9104L, + CHIP_CH340B, + CHIP_CH339W, + CHIP_CH9111L_M0, + CHIP_CH9111L_M1, + CHIP_CH9114L, + CHIP_CH9114W, + CHIP_CH9114F, + CHIP_CH346C_M0, + CHIP_CH346C_M1, + CHIP_CH346C_M2, + CHIP_UNKNOWN = 0xFF, +}; + enum UARTParityOptions { UART_CONFIG_PARITY_NONE = 0, UART_CONFIG_PARITY_ODD, @@ -192,10 +222,17 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { class USBUartTypeCH34X : public USBUartTypeCdcAcm { public: USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} + void dump_config() override; protected: void enable_channels() override; std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override; + + private: + void apply_line_settings_(); + CH34xChipType chiptype_{CHIP_UNKNOWN}; + const char *chip_name_{"unknown"}; + uint8_t num_ports_{1}; }; } // namespace esphome::usb_uart From 3f143d9f19ae29a408f1024e202aa869e86a7787 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:50:32 +0100 Subject: [PATCH 0487/2056] [ethernet] Fix commit 3f700bac1cebf7eb6ff3b20a87d8c8af5cb9fc41 (#14618) --- esphome/components/ethernet/esp_eth_phy_jl1101.c | 2 ++ esphome/components/ethernet/ethernet_component.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index a19f7aa6b00..b81d8227d45 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "esphome/core/defines.h" + #ifdef USE_ESP32 #include <string.h> diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index e54e1543e3f..d9f05be9de0 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -214,7 +214,7 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif From 1b3a7f0b6a8d927d50348b59979cae2aaa437f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:18:14 +0000 Subject: [PATCH 0488/2056] Bump aioesphomeapi from 44.5.0 to 44.5.1 (#14624) 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 03a7cac5c72..3da2d52b44b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.0 +aioesphomeapi==44.5.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 9be1876fae30e97e763480eb4e73ecaf3a4fa445 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Sun, 8 Mar 2026 21:52:16 +0100 Subject: [PATCH 0489/2056] [ble_nus] make ble_nus timeout shorter than watchdog (#14619) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/ble_nus/ble_nus.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index e38dc998028..d0d37dbf1cb 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -71,7 +71,10 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... - return true; // No more to read +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif + return true; // No more to read } } @@ -101,10 +104,10 @@ size_t BLENUS::available() { } uart::FlushResult BLENUS::flush() { - constexpr uint32_t timeout_5sec = 5000; + constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { - if (millis() - start > timeout_5sec) { + if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); return uart::FlushResult::TIMEOUT; } From ad5811280aaa9bce7ecfa49ea4c280522d5ac2c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 10:59:43 -1000 Subject: [PATCH 0490/2056] =?UTF-8?q?[ci]=20Add=20medium-pr=20label=20for?= =?UTF-8?q?=20PRs=20with=20=E2=89=A4100=20lines=20changed=20(#14628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 7 ++++++- .github/scripts/auto-label-pr/index.js | 3 ++- .github/workflows/auto-label-pr.yml | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 8c3a62cf194..1c33772c4c3 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -14,6 +14,7 @@ module.exports = { 'chained-pr', 'core', 'small-pr', + 'medium-pr', 'dashboard', 'github-actions', 'by-code-owner', diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 832fcb41dba..fc631980193 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -103,7 +103,7 @@ async function detectCoreChanges(changedFiles) { } // Strategy: PR size detection -async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) { +async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) { const labels = new Set(); if (totalChanges <= SMALL_PR_THRESHOLD) { @@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange return labels; } + if (totalChanges <= MEDIUM_PR_THRESHOLD) { + labels.add('medium-pr'); + return labels; + } + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) .reduce((sum, file) => sum + (file.additions || 0), 0); diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 483d2cb6267..42588c0bc87 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -35,6 +35,7 @@ async function fetchApiData() { module.exports = async ({ github, context }) => { // Environment variables const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD); + const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD); const MAX_LABELS = parseInt(process.env.MAX_LABELS); const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD); const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD); @@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => { detectNewComponents(prFiles), detectNewPlatforms(prFiles, apiData), detectCoreChanges(changedFiles), - detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD), + detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectDashboardChanges(changedFiles), detectGitHubActionsChanges(changedFiles), detectCodeOwner(github, context, changedFiles), diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 6fcb50b70a7..6376cf877e5 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -12,6 +12,7 @@ permissions: env: SMALL_PR_THRESHOLD: 30 + MEDIUM_PR_THRESHOLD: 100 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 COMPONENT_LABEL_THRESHOLD: 10 From 50b3f9d25cb91b31fb0adc6bad090f2f5bb3d778 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 8 Mar 2026 17:09:06 -0500 Subject: [PATCH 0491/2056] [mixer_speaker] Add task debounce (#14581) --- .../mixer/speaker/mixer_speaker.cpp | 25 ++++++++++++++----- .../components/mixer/speaker/mixer_speaker.h | 7 +++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 8e1278206f4..9d11abb3277 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -11,14 +11,14 @@ #include <array> #include <cstring> -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { static const UBaseType_t MIXER_TASK_PRIORITY = 10; static const uint32_t STOPPING_TIMEOUT_MS = 5000; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TASK_DELAY_MS = 25; +static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200; static const size_t TASK_STACK_SIZE = 4096; @@ -471,6 +471,7 @@ void MixerSpeaker::loop() { this->task_.deallocate(); ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); + this->all_stopped_since_ms_ = 0; } if (this->task_.is_created()) { @@ -483,8 +484,18 @@ void MixerSpeaker::loop() { } if (all_stopped) { - // Send stop command signal to the mixer task since no source speakers are active - xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + if (this->all_stopped_since_ms_ == 0) { + this->all_stopped_since_ms_ = millis(); + } else if ((millis() - this->all_stopped_since_ms_) >= MIXER_AUTO_STOP_DEBOUNCE_MS) { + // Send stop command only after a short debounce to avoid stop/start thrash during rapid seeks. + xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } + } else { + this->all_stopped_since_ms_ = 0; + // New activity detected; clear any stale auto-stop request before it can stop the running task. + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + } } } else { // Task is fully stopped and cleaned up, check if we can disable loop @@ -515,6 +526,9 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { this->enable_loop_soon_any_context(); // ensure loop processes command + // Starting a new stream supersedes any previously queued stop request. + xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP); + uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency @@ -755,7 +769,6 @@ void MixerSpeaker::audio_mixer_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 0e0b33c39bc..29876ea262f 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -14,8 +14,7 @@ #include <atomic> -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { /* Classes for mixing several source speaker audio streams and writing it to another speaker component. * - Volume controls are passed through to the output speaker @@ -200,9 +199,9 @@ class MixerSpeaker : public Component { optional<audio::AudioStreamInfo> audio_stream_info_; std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played + uint32_t all_stopped_since_ms_{0}; // Debounce transient all-stopped windows before stopping task }; -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif From d5dc4a39cb6f10fef73d4950fd21db3beca0f83a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:10:43 -0400 Subject: [PATCH 0492/2056] [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/i2s_audio/microphone/__init__.py | 6 ++++++ .../microphone/i2s_audio_microphone.cpp | 17 +++++++++-------- .../components/i2s_audio/speaker/__init__.py | 13 +++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 16 ++++++++-------- .../speaker/media_player/audio_pipeline.cpp | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index dd23673db55..bf583b9f818 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -46,6 +46,12 @@ def _validate_esp32_variant(config): if config[CONF_ADC_TYPE] == "external": if config[CONF_PDM] and variant not in PDM_VARIANTS: raise cv.Invalid(f"{variant} does not support PDM") + if ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config if config[CONF_ADC_TYPE] == "internal": if variant not in INTERNAL_ADC_VARIANTS: diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index cdebc214e2c..eb4506071e6 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -281,7 +281,7 @@ bool I2SAudioMicrophone::start_driver_() { } /* Before reading data, start the RX channel first */ - i2s_channel_enable(this->rx_handle_); + err = i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; @@ -454,13 +454,14 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w } this->status_clear_warning(); #if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY) - // For ESP32 8/16 bit standard mono mode samples need to be switched. - if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ <= 16 && !this->pdm_) { - size_t samples_read = bytes_read / sizeof(int16_t); - for (int i = 0; i < samples_read; i += 2) { - int16_t tmp = buf[i]; - buf[i] = buf[i + 1]; - buf[i + 1] = tmp; + // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. + if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { + int16_t *samples = reinterpret_cast<int16_t *>(buf); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 2e009a1de18..b84cf7de3b6 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -100,11 +100,16 @@ def _set_stream_limits(config): def _validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config variant = esp32.get_esp32_variant() - if variant not in INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") + if config[CONF_DAC_TYPE] == "internal": + if variant not in INTERNAL_DAC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal DAC") + elif ( + variant == esp32.VARIANT_ESP32 + and config.get(CONF_BITS_PER_SAMPLE) == 8 + and config.get(CONF_CHANNEL) in (CONF_MONO, CONF_LEFT, CONF_RIGHT) + ): + raise cv.Invalid("8-bit mono mode is not supported on ESP32") return config diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c934d12d652..a996702f8bb 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -372,15 +372,15 @@ void I2SAudioSpeaker::speaker_task(void *params) { } #ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 8/16 bit mono mode samples need to be switched. + // For ESP32 16-bit mono mode, adjacent samples need to be swapped. if (this_speaker->current_stream_info_.get_channels() == 1 && - this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { - size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) new_data; - for (size_t i = 0; i < len; i += 2) { - int16_t tmp = tmp_buf[i]; - tmp_buf[i] = tmp_buf[i + 1]; - tmp_buf[i + 1] = tmp; + this_speaker->current_stream_info_.get_bits_per_sample() == 16) { + int16_t *samples = reinterpret_cast<int16_t *>(new_data); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; } } #endif diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8cea3abcfc9..0822d80254e 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -504,7 +504,7 @@ void AudioPipeline::decode_task(void *params) { if (!started_playback && has_stream_info) { // Verify enough data is available before starting playback std::shared_ptr<RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (temp_ring_buffer->available() >= initial_bytes_to_buffer) { + if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) { started_playback = true; } } From e7730cff0024a9d48680ef6f2a2771f7461b6705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 13:59:40 -1000 Subject: [PATCH 0493/2056] [esp32_ble] Optimize BLE event hot path performance (#14627) --- esphome/components/esp32_ble/ble_event.h | 18 ++++++------------ esphome/core/lock_free_queue.h | 10 +++++++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 299fd7705fb..ba87fd88053 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -155,44 +155,38 @@ class BLEEvent { void release() { switch (this->type_) { case GAP: - // GAP events don't have heap allocations + // GAP events never have heap allocations break; case GATTC: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { delete[] this->event_.gattc.data.heap_data; + this->event_.gattc.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gattc.is_inline = false; - this->event_.gattc.data.heap_data = nullptr; break; case GATTS: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { delete[] this->event_.gatts.data.heap_data; + this->event_.gatts.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gatts.is_inline = false; - this->event_.gatts.data.heap_data = nullptr; break; } } // Load new event data for reuse (replaces previous event data) + // Note: release() is NOT called here because EventPool::release() already + // calls event->release() before returning to the free list. Every event + // from allocate() is already in a clean state. void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 522fbd36e1a..316186ea542 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -104,7 +104,15 @@ template<class T, uint8_t SIZE> class LockFreeQueue { } } - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + uint16_t get_and_reset_dropped_count() { + // Fast path: relaxed load is a single instruction on all platforms. + // The atomic exchange (especially for uint16_t on Xtensa) compiles to + // an expensive sub-word CAS retry loop (~25 instructions + memory barriers). + // Since drops are rare, avoid the exchange in the common case. + if (dropped_count_.load(std::memory_order_relaxed) == 0) + return 0; + return dropped_count_.exchange(0, std::memory_order_relaxed); + } void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } From 76c567a71cec76ca820ee0b23107b4258276b72e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:00:04 -1000 Subject: [PATCH 0494/2056] [scheduler] Use std::atomic<uint8_t> instead of std::atomic<bool> for remove flag (#14626) --- esphome/core/scheduler.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index cefbdd1b223..eb6cea4f37d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -178,9 +178,11 @@ class Scheduler { uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic for lock-free access - // Place atomic<bool> separately since it can't be packed with bit fields - std::atomic<bool> remove{false}; + // Multi-threaded with atomics: use atomic uint8_t for lock-free access. + // std::atomic<bool> is not used because GCC on Xtensa generates an indirect + // function call for std::atomic<bool>::load() instead of inlining it. + // std::atomic<uint8_t> inlines correctly on all platforms. + std::atomic<uint8_t> remove{0}; // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; @@ -204,7 +206,7 @@ class Scheduler { next_execution_low_(0), next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS - // remove is initialized in the member declaration as std::atomic<bool>{false} + // remove is initialized in the member declaration type(TIMEOUT), name_type_(NameType::STATIC_STRING), is_retry(false) { @@ -508,7 +510,7 @@ class Scheduler { // Multi-threaded with atomics: use atomic store with appropriate ordering // Release ordering when setting to true ensures cancellation is visible to other threads // Relaxed ordering when setting to false is sufficient for initialization - item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); + item->remove.store(removed ? 1 : 0, removed ? std::memory_order_release : std::memory_order_relaxed); #else // Single-threaded (ESPHOME_THREAD_SINGLE) or // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write From 771404668d04418b377c660398f6ae1812d8bdd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:01:01 -1000 Subject: [PATCH 0495/2056] [api] Inline fast path of try_to_clear_buffer (#14630) --- esphome/components/api/api_connection.cpp | 6 +----- esphome/components/api/api_connection.h | 10 +++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 43f5070a405..28a770a4fb2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1945,11 +1945,7 @@ void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSet #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } #endif -bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->flags_.remove) - return false; - if (this->helper_->can_write_without_blocking()) - return true; +bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5d1469e419f..ccb51186d62 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -305,7 +305,13 @@ class APIConnection final : public APIServerConnectionBase { this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); } - bool try_to_clear_buffer(bool log_out_of_space); + bool try_to_clear_buffer(bool log_out_of_space) { + if (this->flags_.remove) + return false; + if (this->helper_->can_write_without_blocking()) + return true; + return this->try_to_clear_buffer_slow_(log_out_of_space); + } bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; const char *get_name() const { return this->helper_->get_client_name(); } @@ -315,6 +321,8 @@ class APIConnection final : public APIServerConnectionBase { } protected: + bool try_to_clear_buffer_slow_(bool log_out_of_space); + // Helper function to handle authentication completion void complete_authentication_(); From 66a5ad0d75cf90ebc22ca85d139a698090be30ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:06:55 -1000 Subject: [PATCH 0496/2056] [core] Skip zero-initialization of StaticVector data array (#14592) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6ce5de4975c..11e0afe5260 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -215,7 +215,7 @@ template<typename T, size_t N> class StaticVector { using const_reverse_iterator = std::reverse_iterator<const_iterator>; private: - std::array<T, N> data_{}; + std::array<T, N> data_; // intentionally not value-initialized to avoid memset size_t count_{0}; public: From 93d7ec4d72dc77df92b01b3ca84cb29264e46ae5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:07:59 -1000 Subject: [PATCH 0497/2056] [esp32_ble] Inline ble_addr_to_uint64 to eliminate call overhead (#14591) --- esphome/components/esp32_ble/ble.cpp | 11 ----------- esphome/components/esp32_ble/ble.h | 11 ++++++++++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 7fa5370072d..ff9d9bb15aa 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -727,17 +727,6 @@ void ESP32BLE::dump_config() { } } -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2ce17e97be0..04bec3f7858 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -35,7 +35,16 @@ static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); +inline uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} // NOLINTNEXTLINE(modernize-use-using) typedef struct { From 88536ff72bce462b8290d48ca7bc2f3f40a5e9b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:31:42 -1000 Subject: [PATCH 0498/2056] [modbus] Fix timeout for non-hardware UARTs (e.g., USB UART) (#14614) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/modbus/modbus.cpp | 18 ++++- esphome/components/uart/uart_component.h | 6 +- .../external_components/uart_mock/__init__.py | 8 +- .../uart_mock/automation.h | 16 +++- .../uart_mock/uart_mock.cpp | 20 +++++ .../external_components/uart_mock/uart_mock.h | 10 +++ .../uart_mock_modbus_no_threshold.yaml | 64 +++++++++++++++ tests/integration/test_uart_mock_modbus.py | 79 +++++++++++++++++++ 8 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 28e26e307e3..82672217c56 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -11,6 +11,11 @@ static const char *const TAG = "modbus"; // Maximum bytes to log for Modbus frames (truncated if larger) static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; +// Approximate bits per character on the wire (depends on parity/stop bit config) +static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11; +// Milliseconds per second +static constexpr uint32_t MS_PER_SEC = 1000; + void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -19,10 +24,17 @@ void Modbus::setup() { this->frame_delay_ms_ = std::max(2, // 1750us minimum per spec - rounded up to 2ms. // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay) - (uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1); + (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1); + // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a + // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay. + // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks. + static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50; + size_t rx_threshold = this->parent_->get_rx_full_threshold(); this->long_rx_buffer_delay_ms_ = - (this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1; + rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET + ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1 + : DEFAULT_LONG_RX_BUFFER_DELAY_MS; } void Modbus::loop() { @@ -290,7 +302,7 @@ void Modbus::send_next_frame_() { this->last_send_tx_offset_ = 0; } else { this->write_array(frame.data.get(), frame.size); - this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1; + this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 3d7e71b89fd..853de719fef 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -39,6 +39,8 @@ enum class FlushResult { class UARTComponent { public: + static constexpr size_t RX_FULL_THRESHOLD_UNSET = 0; + // Writes an array of bytes to the UART bus. // @param data A vector of bytes to be written. void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); } @@ -201,7 +203,9 @@ class UARTComponent { InternalGPIOPin *rx_pin_{}; InternalGPIOPin *flow_control_pin_{}; size_t rx_buffer_size_{}; - size_t rx_full_threshold_{1}; + // ESP32 (both Arduino and ESP-IDF) always sets this at codegen time via set_rx_full_threshold(). + // Other platforms (USB UART, Arduino, etc.) leave it unset. + size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; uint8_t stop_bits_{0}; diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index c10d73354e2..fdd481b3977 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -62,6 +62,7 @@ CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MockUartComponent), cv.Required("data"): cv.templatable(validate_raw_data), + cv.Optional(CONF_DELAY): cv.positive_time_period_milliseconds, }, key=CONF_DATA, ) @@ -87,7 +88,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(MockUartComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, - cv.Optional(CONF_RX_FULL_THRESHOLD, default=10): cv.int_range(min=1, max=120), + cv.Optional(CONF_RX_FULL_THRESHOLD): cv.int_range(min=1, max=120), cv.Optional(CONF_RX_TIMEOUT, default=2): cv.int_range(min=0, max=92), cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), @@ -126,6 +127,8 @@ async def inject_rx_to_code(config, action_id, template_arg, args): arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) + if CONF_DELAY in config: + cg.add(var.set_delay(config[CONF_DELAY])) return var @@ -135,7 +138,8 @@ async def to_code(config): cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) - cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) + if CONF_RX_FULL_THRESHOLD in config: + cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD])) cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h index 83a057d3a05..b2336ad0656 100644 --- a/tests/integration/fixtures/external_components/uart_mock/automation.h +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -22,18 +22,30 @@ template<typename... Ts> class MockUartInjectRXAction : public Action<Ts...>, pu this->len_ = len; // Length >= 0 indicates static mode } + void set_delay(uint32_t delay_ms) { this->delay_ms_ = delay_ms; } + void play(const Ts &...x) override { if (this->len_ >= 0) { // Static mode: use pointer and length - this->parent_->inject_to_rx_buffer(this->code_.data, static_cast<size_t>(this->len_)); + if (this->delay_ms_ > 0) { + std::vector<uint8_t> data(this->code_.data, this->code_.data + this->len_); + this->parent_->inject_to_rx_buffer_delayed(data, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast<size_t>(this->len_)); + } } else { // Template mode: call function auto val = this->code_.func(x...); - this->parent_->inject_to_rx_buffer(val); + if (this->delay_ms_ > 0) { + this->parent_->inject_to_rx_buffer_delayed(val, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(val); + } } } protected: + uint32_t delay_ms_{0}; ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Code { std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1edeb97cf16..1a15da76d13 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -53,6 +53,15 @@ void MockUartComponent::loop() { } } + // Process staged RX - deliver bytes whose delay has elapsed + uint32_t now_ms = millis(); + while (!this->staged_rx_.empty() && (static_cast<int32_t>(now_ms - this->staged_rx_.front().available_at_ms) >= 0)) { + auto &staged = this->staged_rx_.front(); + ESP_LOGD(TAG, "Delivering %zu staged RX bytes", staged.data.size()); + this->inject_to_rx_buffer(staged.data); + this->staged_rx_.pop_front(); + } + // Process delayed responses for (auto &response : this->responses_) { if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { @@ -209,4 +218,15 @@ void MockUartComponent::inject_to_rx_buffer(const std::vector<uint8_t> &data) { } } +void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms) { + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay: %s", data.size(), delay_ms, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay (too large to log inline)", data.size(), delay_ms); + } + this->staged_rx_.push_back({data, millis() + delay_ms}); +} + } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index c9afb963579..82e3b3d5632 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -43,6 +43,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void set_tx_hook(std::function<void(const std::vector<uint8_t> &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector<uint8_t> &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); + // Stage bytes for delayed delivery - simulates transport-level latency (e.g., USB packets) + void inject_to_rx_buffer_delayed(const std::vector<uint8_t> &data, uint32_t delay_ms); protected: void check_logger_conflict() override {} @@ -82,6 +84,14 @@ class MockUartComponent : public uart::UARTComponent, public Component { }; std::vector<PeriodicRx> periodic_rx_; + // Staged RX - bytes that are pending delivery after a delay + // Simulates transport-level latency (e.g., USB packet delivery) + struct StagedRx { + std::vector<uint8_t> data; + uint32_t available_at_ms; // millis() time when bytes become available + }; + std::deque<StagedRx> staged_rx_; + // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml new file mode 100644 index 00000000000..e3e8c8c8da7 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -0,0 +1,64 @@ +esphome: + name: uart-mock-modbus-no-thresh + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +# Simulate a non-hardware UART (e.g., USB UART) by not setting rx_full_threshold. +# This leaves it at the default sentinel value (0), triggering the 50ms fallback timeout. +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + auto_start: false + debug: + on_tx: + - then: + - if: + condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) + lambda: "return data == std::vector<uint8_t>({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" + then: + - uart_mock.inject_rx: # First USB packet: SDM meter response part 1 + !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency) + delay: 40ms + data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + +modbus: + uart_id: virtual_uart_dev + turnaround_time: 10ms + +sensor: + - platform: sdm_meter + address: 2 + update_interval: 1s + phase_a: + voltage: + name: sdm_voltage + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(virtual_uart_dev).start_scenario();' diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 6901dc27fe1..e341d86f53f 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -5,6 +5,10 @@ test_uart_mock_modbus : 1. Read a single register and parse successfully (basic_register) 2. Read multiple registers from SDM meter and parse successfully (sdm_voltage), with some intermediate delay to simulate UART buffer time. +test_uart_mock_modbus_no_threshold : + Test modbus with no rx_full_threshold set (simulating USB UART / non-hardware UART). + Verifies the 50ms fallback timeout handles chunked data with USB packet gaps. + """ from __future__ import annotations @@ -218,3 +222,78 @@ async def test_uart_mock_modbus_timing( f"Timeout waiting for SDM voltage change. Received sensor states:\n" f" sdm_voltage: {sensor_states['sdm_voltage']}\n" ) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_no_threshold( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test modbus with no rx_full_threshold (simulating USB UART). + + Without the 50ms fallback timeout, the chunked response with a 40ms gap + between USB packets would cause a false timeout and CRC failure cascade. + """ + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track sensor state updates (after initial state is swallowed) + sensor_states: dict[str, list[float]] = { + "sdm_voltage": [], + } + + voltage_changed = loop.create_future() + + def on_state(state: EntityState) -> None: + if isinstance(state, SensorState) and not state.missing_state: + sensor_name = key_to_sensor.get(state.key) + if sensor_name and sensor_name in sensor_states: + sensor_states[sensor_name].append(state.state) + # Check if this is a good voltage reading (243V) + if ( + sensor_name == "sdm_voltage" + and state.state > 200.0 + and not voltage_changed.done() + ): + voltage_changed.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Build key mappings for all sensor types + all_names = list(sensor_states.keys()) + key_to_sensor = build_key_to_entity_mapping(entities, all_names) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for voltage to be updated with successful parse + try: + await asyncio.wait_for(voltage_changed, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for SDM voltage change. Received sensor states:\n" + f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + ) From c11ad7f0e6fdbce4c65eb714e794c0791a8c7352 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:35 -1000 Subject: [PATCH 0499/2056] [rp2040] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~9.2 KB flash) (#14622) --- esphome/components/rp2040/__init__.py | 17 ++++- esphome/components/rp2040/const.py | 1 + esphome/components/rp2040/printf_stubs.cpp | 74 ++++++++++++++++++++ tests/components/rp2040/test.rp2040-ard.yaml | 3 + 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 esphome/components/rp2040/printf_stubs.cpp diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 1442a0a7f74..54e1db27aa2 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -21,7 +21,13 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns +from .const import ( + CONF_ENABLE_FULL_PRINTF, + KEY_BOARD, + KEY_PIO_FILES, + KEY_RP2040, + rp2040_ns, +) # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa @@ -153,6 +159,7 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=8388)), ), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -190,6 +197,14 @@ async def to_code(config): ], ) + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~9.2 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.core", "earlephilhower") cg.add_platformio_option("board_build.filesystem_size", "1m") diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index ab5f42d7573..7eeddffc762 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,5 +1,6 @@ import esphome.codegen as cg +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/components/rp2040/printf_stubs.cpp b/esphome/components/rp2040/printf_stubs.cpp new file mode 100644 index 00000000000..c2174a1dece --- /dev/null +++ b/esphome/components/rp2040/printf_stubs.cpp @@ -0,0 +1,74 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The RP2040 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~8.9 KB). + * ESPHome never uses these — all logging goes through the logger component + * which uses snprintf/vsnprintf, so the libc FILE*-based printf path is + * dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary) + * and fwrite(), allowing the linker to dead-code eliminate _vfprintf_r. + * + * Saves ~8.9 KB of flash. + */ + +#if defined(USE_RP2040) && !defined(USE_FULL_PRINTF) +#include <cstdarg> +#include <cstdio> +#include <cstdlib> + +namespace esphome::rp2040 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome uses its own +// logging through snprintf/vsnprintf, not libc printf. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + // Use fwrite for the message to avoid recursive __wrap_printf call + static const char msg[] = "\nprintf buffer overflow\n"; + fwrite(msg, 1, sizeof(msg) - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_RP2040 && !USE_FULL_PRINTF diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml index 039a2610160..1eb315a3b47 100644 --- a/tests/components/rp2040/test.rp2040-ard.yaml +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +rp2040: + enable_full_printf: false + logger: level: VERBOSE From e1c849d5d22651a6e7d3457f2d5dbe206d21c386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:47 -1000 Subject: [PATCH 0500/2056] [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621) --- esphome/components/esp8266/__init__.py | 10 +++ esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/printf_stubs.cpp | 71 +++++++++++++++++++ .../components/esp8266/test.esp8266-ard.yaml | 3 + 4 files changed, 85 insertions(+) create mode 100644 esphome/components/esp8266/printf_stubs.cpp diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 927a59fd616..1ef4f5e037e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -23,6 +23,7 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, @@ -179,6 +180,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, } ), set_core_data, @@ -260,6 +262,14 @@ async def to_code(config): if CORE.testing_mode: cg.add_build_flag("-DESPHOME_TESTING_MODE") + # Wrap FILE*-based printf functions to eliminate newlib's _vfiprintf_r + # (~1.6 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 229ac61f245..57eb54f0f80 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,6 +6,7 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/esp8266/printf_stubs.cpp b/esphome/components/esp8266/printf_stubs.cpp new file mode 100644 index 00000000000..e6d4a748664 --- /dev/null +++ b/esphome/components/esp8266/printf_stubs.cpp @@ -0,0 +1,71 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The ESP8266 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~900 bytes). + * ESPHome never uses these — all logging writes directly to the UART via + * Arduino's Serial, so the libc FILE*-based printf path is dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary + * for ESPHome's logging) and fwrite(), allowing the linker to dead-code + * eliminate _vfprintf_r. + * + * Saves ~1.6 KB of flash. + */ + +#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF) +#include <cstdarg> +#include <cstdio> +#include <cstdlib> + +namespace esphome::esp8266 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome writes directly +// to the UART via Arduino's Serial, and Serial.printf() has its own implementation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 && !USE_FULL_PRINTF diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index 039a2610160..c77218f7a3c 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +esp8266: + enable_full_printf: false + logger: level: VERBOSE From aef2d74e41123f77900bc5cab37a75cfb9b6e2b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:32:59 -1000 Subject: [PATCH 0501/2056] [ld2450] Add integration tests with mock UART (#14611) --- .../fixtures/uart_mock_ld2450.yaml | 221 ++++++++++++++++++ tests/integration/state_utils.py | 28 ++- tests/integration/test_uart_mock_ld2450.py | 204 ++++++++++++++++ 3 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2450.yaml create mode 100644 tests/integration/test_uart_mock_ld2450.py diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml new file mode 100644 index 00000000000..269136da682 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -0,0 +1,221 @@ +esphome: + name: uart-mock-ld2450-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2450's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + responses: + # Catch-all response: match any command footer (04 03 02 01). + # Returns a generic ACK to unblock setup commands. + # + # Response layout: + # [0-3] FD FC FB FA = header + # [4-5] 04 00 = length 4 + # [6] FF = cmd (handled as CMD_ENABLE_CONF) + # [7] 01 = status (ACK) + # [8-9] 00 00 = error = 0 + # [10-13] 04 03 02 01 = footer + - expect_tx: [0x04, 0x03, 0x02, 0x01] + inject_rx: + [ + 0xFD, 0xFC, 0xFB, 0xFA, + 0x04, 0x00, + 0xFF, 0x01, + 0x00, 0x00, + 0x04, 0x03, 0x02, 0x01, + ] + + injections: + # Phase 1 (t=100ms): Valid LD2450 periodic data frame - happy path + # The buffer is clean at this point, so this frame should parse correctly. + # + # Target 1: X=-500mm, Y=1000mm, Speed=-50mm/s (approaching), Res=320mm + # X: magnitude=500 (0x01F4), negative → high=0x01, low=0xF4 + # Y: magnitude=1000 (0x03E8), positive → high=0x83, low=0xE8 + # Speed: raw=5, negative (approaching) → high=0x00, low=0x05, decoded=-50mm/s + # Resolution: 320 → low=0x40, high=0x01 + # Distance: sqrt(500²+1000²) = sqrt(1250000) ≈ 1118mm + # + # Target 2: X=200mm, Y=500mm, Speed=0 (stationary), Res=100mm + # X: magnitude=200 (0x00C8), positive → high=0x80, low=0xC8 + # Y: magnitude=500 (0x01F4), positive → high=0x81, low=0xF4 + # Speed: 0 → 0x00, 0x00 + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(200²+500²) = sqrt(290000) ≈ 538mm + # + # Target 3: No target (all zeros) + # Distance: 0 → sensors publish unknown/NaN + # + # Counts: target_count=2, moving_target_count=1, still_target_count=1 + # + # Frame layout (30 bytes): + # [0-3] AA FF 03 00 = periodic data header + # [4-11] Target 1 (8 bytes): X_L X_H Y_L Y_H SPD_L SPD_H RES_L RES_H + # [12-19] Target 2 (8 bytes) + # [20-27] Target 3 (8 bytes) + # [28-29] 55 CC = periodic data footer + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0xF4, 0x01, 0xE8, 0x83, 0x05, 0x00, 0x40, 0x01, + 0xC8, 0x80, 0xF4, 0x81, 0x00, 0x00, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + + # Phase 2 (t=300ms): Garbage bytes + # LD2450's readline_ does NOT reject bytes at position 0 (unlike LD2412), + # so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7. + - delay: 200ms + inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22] + + # Phase 3 (t=400ms): Truncated frame (header + partial data, no footer) + # More bytes accumulating in the buffer without a footer match. + # After this, buffer_pos_ = 7 + 8 = 15. + - delay: 100ms + inject_rx: [0xAA, 0xFF, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04] + + # Phase 4 (t=600ms): Overflow - inject 75 bytes of 0xFF (MAX_LINE_LENGTH=45) + # Buffer has 15 bytes from phases 2+3. + # readline_() stores bytes while buffer_pos_ < 44. When buffer_pos_ == 44, + # the next byte triggers overflow: logs warning, resets buffer_pos_ to 0, + # and discards that byte. + # + # First overflow: 29 bytes fill positions 15-43 (buffer_pos_=44), byte 30 + # triggers overflow (discarded). Total consumed: 30 bytes. + # Second overflow: 44 bytes fill positions 0-43 (buffer_pos_=44), byte 45 + # triggers overflow (discarded). Total consumed: 30+45 = 75 bytes. + # After both overflows, buffer_pos_ = 0 (clean state for recovery frame). + - delay: 200ms + inject_rx: + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ] + + # Phase 5 (t=700ms): Valid frame after overflow - recovery test + # Buffer was reset by overflow. This valid frame should parse correctly. + # + # Target 1: X=300mm, Y=400mm, Speed=30mm/s (moving away), Res=100mm + # X: magnitude=300 (0x012C), positive → high=0x81, low=0x2C + # Y: magnitude=400 (0x0190), positive → high=0x81, low=0x90 + # Speed: raw=3, positive (moving away) → high=0x80, low=0x03, decoded=30mm/s + # Resolution: 100 → low=0x64, high=0x00 + # Distance: sqrt(300²+400²) = 500mm + # + # Target 2 & 3: No target (all zeros) + # Counts: target_count=1, moving_target_count=1, still_target_count=0 + - delay: 100ms + inject_rx: + [ + 0xAA, 0xFF, 0x03, 0x00, + 0x2C, 0x81, 0x90, 0x81, 0x03, 0x80, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xCC, + ] + +ld2450: + id: ld2450_dev + uart_id: mock_uart + +sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_count: + name: "Target Count" + filters: &sensor_filters + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + still_target_count: + name: "Still Target Count" + filters: *sensor_filters + moving_target_count: + name: "Moving Target Count" + filters: *sensor_filters + target_1: + x: + name: "Target 1 X" + filters: *sensor_filters + y: + name: "Target 1 Y" + filters: *sensor_filters + speed: + name: "Target 1 Speed" + filters: *sensor_filters + distance: + name: "Target 1 Distance" + filters: *sensor_filters + resolution: + name: "Target 1 Resolution" + filters: *sensor_filters + angle: + name: "Target 1 Angle" + filters: *sensor_filters + target_2: + x: + name: "Target 2 X" + filters: *sensor_filters + y: + name: "Target 2 Y" + filters: *sensor_filters + speed: + name: "Target 2 Speed" + filters: *sensor_filters + distance: + name: "Target 2 Distance" + filters: *sensor_filters + +binary_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + has_target: + name: "Has Target" + filters: &binary_sensor_filters + - settle: 50ms + has_moving_target: + name: "Has Moving Target" + filters: *binary_sensor_filters + has_still_target: + name: "Has Still Target" + filters: *binary_sensor_filters + +text_sensor: + - platform: ld2450 + ld2450_id: ld2450_dev + target_1: + direction: + name: "Target 1 Direction" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index e8c2cc5e663..ab9fdb01bb9 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, SensorState, + TextSensorState, ) _LOGGER = logging.getLogger(__name__) @@ -244,12 +245,13 @@ class InitialStateHelper: class SensorStateCollector: - """Collects sensor and binary sensor state updates and provides wait helpers. + """Collects sensor, binary sensor, and text sensor state updates with wait helpers. Usage: collector = SensorStateCollector( sensor_names=["moving_distance", "still_distance"], binary_sensor_names=["has_target"], + text_sensor_names=["direction"], ) # Use collector.on_state as the callback (or wrap it) client.subscribe_states(helper.on_state_wrapper(collector.on_state)) @@ -259,18 +261,23 @@ class SensorStateCollector: # Access collected states assert collector.sensor_states["moving_distance"][0] == approx(100.0) + assert collector.text_sensor_states["direction"][0] == "Approaching" """ def __init__( self, sensor_names: list[str], binary_sensor_names: list[str] | None = None, + text_sensor_names: list[str] | None = None, entities: list[EntityInfo] | None = None, ) -> None: self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} self.binary_states: dict[str, list[bool]] = { name: [] for name in (binary_sensor_names or []) } + self.text_sensor_states: dict[str, list[str]] = { + name: [] for name in (text_sensor_names or []) + } self._key_to_sensor: dict[int, str] = {} self._waiters: list[tuple[Callable[[], bool], asyncio.Future[bool]]] = [] @@ -279,7 +286,11 @@ class SensorStateCollector: def build_key_mapping(self, entities: list[EntityInfo]) -> None: """Build key-to-name mapping from entities. Sorted by descending length.""" - all_names = list(self.sensor_states.keys()) + list(self.binary_states.keys()) + all_names = ( + list(self.sensor_states.keys()) + + list(self.binary_states.keys()) + + list(self.text_sensor_states.keys()) + ) all_names.sort(key=len, reverse=True) self._key_to_sensor = build_key_to_entity_mapping(entities, all_names) @@ -295,6 +306,11 @@ class SensorStateCollector: if sensor_name and sensor_name in self.binary_states: self.binary_states[sensor_name].append(state.state) self._check_waiters() + elif isinstance(state, TextSensorState) and not state.missing_state: + sensor_name = self._key_to_sensor.get(state.key) + if sensor_name and sensor_name in self.text_sensor_states: + self.text_sensor_states[sensor_name].append(state.state) + self._check_waiters() def _check_waiters(self) -> None: """Check all pending waiters and resolve any whose condition is met.""" @@ -303,9 +319,11 @@ class SensorStateCollector: future.set_result(True) def _all_have_values(self) -> bool: - """Check if all sensor and binary sensor lists have at least one value.""" - return all(len(v) >= 1 for v in self.sensor_states.values()) and all( - len(v) >= 1 for v in self.binary_states.values() + """Check if all sensor, binary sensor, and text sensor lists have at least one value.""" + return ( + all(len(v) >= 1 for v in self.sensor_states.values()) + and all(len(v) >= 1 for v in self.binary_states.values()) + and all(len(v) >= 1 for v in self.text_sensor_states.values()) ) async def wait_for_all(self, timeout: float = 3.0) -> None: diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py new file mode 100644 index 00000000000..b1aa2f6952b --- /dev/null +++ b/tests/integration/test_uart_mock_ld2450.py @@ -0,0 +1,204 @@ +"""Integration test for LD2450 component with mock UART. + +Tests: +test_uart_mock_ld2450: + 1. Happy path - valid periodic data frame publishes correct target sensor values + 2. Multi-target tracking - verifies target count, moving/still counts + 3. Target coordinate decoding - signed X/Y coordinates with sign-magnitude encoding + 4. Speed decoding - approaching (negative) and stationary (zero) targets + 5. Distance calculation - computed from X/Y via sqrt(x²+y²) + 6. Direction text sensor - "Approaching" for negative speed target + 7. Garbage resilience - random bytes don't crash the component + 8. Truncated frame handling - partial frame doesn't corrupt state + 9. Buffer overflow recovery - overflow resets the parser + 10. Post-overflow parsing - next valid frame after overflow is parsed correctly + 11. TX logging - verifies LD2450 sends expected setup commands +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import ButtonInfo +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, find_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_uart_mock_ld2450( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LD2450 data parsing with happy path, garbage, overflow, and recovery.""" + # Replace external component path placeholder + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track overflow warning in logs + overflow_seen = loop.create_future() + + # Track TX data logged by the mock for assertions + tx_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "Max command length exceeded" in line and not overflow_seen.done(): + overflow_seen.set_result(True) + # Capture all TX log lines from uart_mock + if "uart_mock" in line and "TX " in line: + tx_log_lines.append(line) + + collector = SensorStateCollector( + sensor_names=[ + "target_1_x", + "target_1_y", + "target_1_speed", + "target_1_distance", + "target_1_resolution", + "target_1_angle", + "target_2_x", + "target_2_y", + "target_2_speed", + "target_2_distance", + "target_count", + "still_target_count", + "moving_target_count", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + text_sensor_names=[ + "target_1_direction", + ], + ) + + # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + recovery_received = collector.add_waiter( + lambda: ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + # Set up initial state helper + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Start the UART mock scenario now that we're subscribed + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 - all sensors and binary sensors have at least one value + try: + await collector.wait_for_all(timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}\n" + f" text_states: {collector.text_sensor_states}" + ) + + # Phase 1 values: + # Target 1: X=-500, Y=1000, Speed=-50 (approaching), Res=320 + # Distance = sqrt(500²+1000²) ≈ 1118mm + assert collector.sensor_states["target_1_x"][0] == pytest.approx(-500.0) + assert collector.sensor_states["target_1_y"][0] == pytest.approx(1000.0) + assert collector.sensor_states["target_1_speed"][0] == pytest.approx(-50.0) + assert collector.sensor_states["target_1_resolution"][0] == pytest.approx(320.0) + # Distance computed from X/Y + assert collector.sensor_states["target_1_distance"][0] == pytest.approx( + 1118.0, abs=1.0 + ) + + # Target 2: X=200, Y=500, Speed=0 (stationary), Res=100 + # Distance = sqrt(200²+500²) ≈ 538mm + assert collector.sensor_states["target_2_x"][0] == pytest.approx(200.0) + assert collector.sensor_states["target_2_y"][0] == pytest.approx(500.0) + assert collector.sensor_states["target_2_speed"][0] == pytest.approx(0.0) + assert collector.sensor_states["target_2_distance"][0] == pytest.approx( + 538.0, abs=1.0 + ) + + # Target counts: 2 targets total, 1 moving, 1 still + assert collector.sensor_states["target_count"][0] == pytest.approx(2.0) + assert collector.sensor_states["moving_target_count"][0] == pytest.approx(1.0) + assert collector.sensor_states["still_target_count"][0] == pytest.approx(1.0) + + # Binary sensors: all true (targets detected) + assert collector.binary_states["has_target"][0] is True + assert collector.binary_states["has_moving_target"][0] is True + assert collector.binary_states["has_still_target"][0] is True + + # Direction text sensor: Target 1 is approaching (speed < 0) + assert collector.text_sensor_states["target_1_direction"][0] == "Approaching" + + # Wait for the recovery frame (Phase 5) to be parsed + # This proves the component survived garbage + truncated + overflow + try: + await asyncio.wait_for(recovery_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" sensor_states: {collector.sensor_states}" + ) + + # Verify overflow warning was logged + assert overflow_seen.done(), ( + "Expected 'Max command length exceeded' warning in logs" + ) + + # Verify LD2450 sent setup commands (TX logging) + assert len(tx_log_lines) > 0, "Expected TX log lines from uart_mock" + tx_data = " ".join(tx_log_lines) + # Verify command frame header appears (FD:FC:FB:FA) + assert "FD:FC:FB:FA" in tx_data, ( + "Expected LD2450 command frame header FD:FC:FB:FA in TX log" + ) + # Verify command frame footer appears (04:03:02:01) + assert "04:03:02:01" in tx_data, ( + "Expected LD2450 command frame footer 04:03:02:01 in TX log" + ) + + # Recovery frame values (Phase 5, after overflow): + # Target 1: X=300, Y=400, Distance=500, Speed=30 (moving away) + # target_count=1, moving=1, still=0 + # + # Note: throttle filters cause sensor lists to have different lengths, + # so we check each value appeared somewhere rather than using a shared index. + assert ( + pytest.approx(500.0, abs=1.0) + in collector.sensor_states["target_1_distance"] + ) + assert pytest.approx(300.0) in collector.sensor_states["target_1_x"] + assert pytest.approx(400.0) in collector.sensor_states["target_1_y"] + assert pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + assert pytest.approx(1.0) in collector.sensor_states["target_count"] + assert pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + assert pytest.approx(0.0) in collector.sensor_states["still_target_count"] From b05dbfccd31ec769bfa873783506b469ac6def27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:52:21 -1000 Subject: [PATCH 0502/2056] [api] Bump noise-c to 0.1.11 (#14632) --- .clang-tidy.hash | 2 +- esphome/components/api/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index adcebadeb46..ff25675918b 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf +e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index dd99862cc27..c7dec6e78be 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -453,7 +453,7 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.10") + cg.add_library("esphome/noise-c", "0.1.11") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/platformio.ini b/platformio.ini index 87f992759c5..deee23d049c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,7 +46,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.10 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -542,7 +542,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.10 ; used by api + esphome/noise-c@0.1.11 ; used by api build_flags = ${common.build_flags} -DUSE_HOST From 9547a54fac5de13f36a98d670e60ef18b1e88fe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 14:52:40 -1000 Subject: [PATCH 0503/2056] [const] Move CONF_ENABLE_FULL_PRINTF to const.py (#14633) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp8266/__init__.py | 2 +- esphome/components/esp8266/const.py | 1 - esphome/components/rp2040/__init__.py | 9 ++------- esphome/components/rp2040/const.py | 1 - esphome/const.py | 1 + 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8ad84656ede..52e70501dcf 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, @@ -954,7 +955,6 @@ CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 1ef4f5e037e..16043b6d69a 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -6,6 +6,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -23,7 +24,6 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, - CONF_ENABLE_FULL_PRINTF, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 57eb54f0f80..229ac61f245 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -6,7 +6,6 @@ KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_SERIAL = "enable_serial" CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 54e1db27aa2..359337adfb9 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -6,6 +6,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -21,13 +22,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed -from .const import ( - CONF_ENABLE_FULL_PRINTF, - KEY_BOARD, - KEY_PIO_FILES, - KEY_RP2040, - rp2040_ns, -) +from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index 7eeddffc762..ab5f42d7573 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,6 +1,5 @@ import esphome.codegen as cg -CONF_ENABLE_FULL_PRINTF = "enable_full_printf" KEY_BOARD = "board" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/const.py b/esphome/const.py index 88e3c33fbc6..d409514f3c7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -356,6 +356,7 @@ CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" CONF_ENABLE_BTM = "enable_btm" +CONF_ENABLE_FULL_PRINTF = "enable_full_printf" CONF_ENABLE_IPV6 = "enable_ipv6" CONF_ENABLE_ON_BOOT = "enable_on_boot" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" From d0285cdc41d479ed0aa7dc0984a71215ee4ff696 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:11:15 -1000 Subject: [PATCH 0504/2056] [core] Pack entity flags into configure_entity_() and protect setters (#14564) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/core/entity_base.cpp | 14 +- esphome/core/entity_base.h | 31 ++- esphome/core/entity_helpers.py | 84 ++++++- .../binary_sensor/test_binary_sensor.py | 8 +- tests/component_tests/button/test_button.py | 8 +- tests/component_tests/helpers.py | 19 ++ tests/component_tests/sensor/test_sensor.py | 12 +- tests/component_tests/text/test_text.py | 10 +- .../text_sensor/test_text_sensor.py | 20 +- tests/unit_tests/core/test_entity_helpers.py | 222 +++++++++++++++++- 10 files changed, 354 insertions(+), 74 deletions(-) create mode 100644 tests/component_tests/helpers.py diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 3274640eb34..818dae06de1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->calc_object_id_(); } } - // Unpack entity string table indices. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) + // Unpack entity string table indices and flags from entity_fields. #ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = entity_strings_packed & 0xFF; + this->device_class_idx_ = (entity_fields >> ENTITY_FIELD_DC_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; + this->uom_idx_ = (entity_fields >> ENTITY_FIELD_UOM_SHIFT) & 0xFF; #endif #ifdef USE_ENTITY_ICON - this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; + this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF; #endif + this->flags_.internal = (entity_fields >> ENTITY_FIELD_INTERNAL_SHIFT) & 1; + this->flags_.disabled_by_default = (entity_fields >> ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT) & 1; + this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3; } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index accd532b0d0..cccbafd2c36 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -55,6 +55,15 @@ enum EntityCategory : uint8_t { ENTITY_CATEGORY_DIAGNOSTIC = 2, }; +// Bit layout for entity_fields parameter in configure_entity_(). +// Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py +static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0; +static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8; +static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16; +static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24; +static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25; +static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26; + // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: @@ -88,21 +97,16 @@ class EntityBase { /// Useful for building compound strings without intermediate buffer size_t write_object_id_to(char *buf, size_t buf_size) const; - // Get/set whether this Entity should be hidden outside ESPHome + // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } - void set_internal(bool internal) { this->flags_.internal = internal; } // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. bool is_disabled_by_default() const { return this->flags_.disabled_by_default; } - void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; } - // Get/set the entity category. + // Get the entity category. EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); } - void set_entity_category(EntityCategory entity_category) { - this->flags_.entity_category = static_cast<uint8_t>(entity_category); - } // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). @@ -164,14 +168,13 @@ class EntityBase { #endif #ifdef USE_DEVICES - // Get/set this entity's device id + // Get this entity's device id uint32_t get_device_id() const { if (this->device_ == nullptr) { return 0; // No device set, return 0 } return this->device_->get_device_id(); } - void set_device(Device *device) { this->device_ = device; } // Get the device this entity belongs to (nullptr if main device) Device *get_device() const { return this->device_; } #endif @@ -228,8 +231,14 @@ class EntityBase { friend void ::setup(); friend void ::original_setup(); - /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. - void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. + /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields); + +#ifdef USE_DEVICES + // Codegen-only setter — only accessible from setup() via friend declaration. + void set_device_(Device *device) { this->device_ = device; } +#endif /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 4fa109fb0e1..0589b92364a 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx" _KEY_ENTITY_NAME = "_entity_name" _KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: -# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) +# Bit layout for entity_fields in configure_entity_(). +# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h _DC_SHIFT = 0 _UOM_SHIFT = 8 _ICON_SHIFT = 16 +_INTERNAL_SHIFT = 24 +_DISABLED_BY_DEFAULT_SHIFT = 25 +_ENTITY_CATEGORY_SHIFT = 26 + +# Private config keys for storing flags +_KEY_INTERNAL = "_entity_internal" +_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default" +_KEY_ENTITY_CATEGORY = "_entity_category" # Maximum unique strings per category (8-bit index, 0 = not set) _MAX_DEVICE_CLASSES = 0xFF # 255 @@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None: config[_KEY_UOM_IDX] = idx +def _sanitize_comment(text: str) -> str: + r"""Sanitize a string for safe inclusion in a C++ // line comment. + + Dangerous characters: + - \n, \r: break out of line comment, next line becomes code + - \: at end of line, splices next line into comment (eats real code) + """ + return text.replace("\\", "/").replace("\n", " ").replace("\r", "") + + +def _describe_packed_flags(config: ConfigType, entity_category: int) -> str: + """Build a human-readable description of packed entity flags for C++ comments.""" + parts: list[str] = [] + if config.get(_KEY_INTERNAL): + parts.append("internal") + if config.get(_KEY_DISABLED_BY_DEFAULT): + parts.append("disabled_by_default") + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + if entity_category < len(entity_cat_keys) and ( + cat_name := entity_cat_keys[entity_category] + ): + parts.append(f"category:{cat_name}") + if config.get(_KEY_DC_IDX) and (dc := config.get(CONF_DEVICE_CLASS)): + parts.append(f"dc:{_sanitize_comment(dc)}") + if config.get(_KEY_UOM_IDX) and (uom := config.get(CONF_UNIT_OF_MEASUREMENT)): + parts.append(f"uom:{_sanitize_comment(uom)}") + if config.get(_KEY_ICON_IDX) and (icon := config.get(CONF_ICON)): + parts.append(f"icon:{_sanitize_comment(icon)}") + return ", ".join(parts) + + def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single configure_entity_() call with name, hash, and packed string indices. + """Emit a single configure_entity_() call with name, hash, packed string indices, and flags. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. @@ -231,8 +270,24 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) - packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - add(var.configure_entity_(entity_name, object_id_hash, packed)) + internal = config.get(_KEY_INTERNAL, 0) + disabled_by_default = config.get(_KEY_DISABLED_BY_DEFAULT, 0) + entity_category = config.get(_KEY_ENTITY_CATEGORY, 0) + packed = ( + (dc_idx << _DC_SHIFT) + | (uom_idx << _UOM_SHIFT) + | (icon_idx << _ICON_SHIFT) + | (internal << _INTERNAL_SHIFT) + | (disabled_by_default << _DISABLED_BY_DEFAULT_SHIFT) + | (entity_category << _ENTITY_CATEGORY_SHIFT) + ) + # Build inline comment describing the packed flags for readability + comment = _describe_packed_flags(config, entity_category) + expr = var.configure_entity_(entity_name, object_id_hash, packed) + if comment: + add(RawStatement(f"{expr}; // {comment}")) + else: + add(expr) def get_base_entity_object_id( @@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> # Get device info if configured if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) - add(var.set_device(device)) + add(var.set_device_(device)) # Pre-compute entity name and object_id hash for configure_entity_() # which is emitted later by finalize_entity_strings(). @@ -343,18 +398,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 config[_KEY_ENTITY_NAME] = entity_name config[_KEY_OBJECT_ID_HASH] = object_id_hash - # Only set disabled_by_default if True (default is False) - if config[CONF_DISABLED_BY_DEFAULT]: - add(var.set_disabled_by_default(True)) + # Store flags for packing into configure_entity_() + config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT]) if CONF_INTERNAL in config: - add(var.set_internal(config[CONF_INTERNAL])) + config[_KEY_INTERNAL] = int(config[CONF_INTERNAL]) icon_idx = 0 if CONF_ICON in config: # Add USE_ENTITY_ICON define when icons are used cg.add_define("USE_ENTITY_ICON") icon_idx = register_icon(config[CONF_ICON]) if CONF_ENTITY_CATEGORY in config: - add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + # Derive integer value from key position in cv.ENTITY_CATEGORIES + # (must match C++ EntityCategory enum in entity_base.h) + entity_cat_str = str(config[CONF_ENTITY_CATEGORY]) + entity_cat_keys = list(cv.ENTITY_CATEGORIES) + config[_KEY_ENTITY_CATEGORY] = ( + entity_cat_keys.index(entity_cat_str) + if entity_cat_str in entity_cat_keys + else 0 + ) # Store icon index for finalize_entity_strings config[_KEY_ICON_IDX] = icon_idx diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index fbc2f37d9a1..10d7f808346 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -1,5 +1,7 @@ """Tests for the binary sensor component.""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_binary_sensor_is_setup(generate_main): """ @@ -44,9 +46,9 @@ def test_binary_sensor_config_value_internal_set(generate_main): "tests/component_tests/binary_sensor/test_binary_sensor.yaml" ) - # Then - assert "bs_1->set_internal(true);" in main_cpp - assert "bs_2->set_internal(false);" in main_cpp + # Then: bs_1 has internal: true, bs_2 has internal: false + assert extract_packed_value(main_cpp, "bs_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "bs_2") & INTERNAL_BIT == 0 def test_binary_sensor_config_value_use_raw_set(generate_main): diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 9f94d61c8c4..a35994a682c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -1,5 +1,7 @@ """Tests for the button component""" +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value + def test_button_is_setup(generate_main): """ @@ -39,6 +41,6 @@ def test_button_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/button/test_button.yaml") - # Then - assert "wol_1->set_internal(true);" in main_cpp - assert "wol_2->set_internal(false);" in main_cpp + # Then: wol_1 has internal: true, wol_2 has internal: false + assert extract_packed_value(main_cpp, "wol_1") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "wol_2") & INTERNAL_BIT == 0 diff --git a/tests/component_tests/helpers.py b/tests/component_tests/helpers.py new file mode 100644 index 00000000000..568d1639d0c --- /dev/null +++ b/tests/component_tests/helpers.py @@ -0,0 +1,19 @@ +"""Shared helpers for component tests.""" + +from __future__ import annotations + +import re + +INTERNAL_BIT = 1 << 24 + + +def extract_packed_value(main_cpp: str, var_name: str) -> int: + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = ( + rf"{re.escape(var_name)}->configure_entity_\(" + r'"(?:\\.|[^"\\])*"' + r",\s*\w+,\s*(\d+)\)" + ) + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index d9ab3a022c8..9d18fa36b8f 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,14 +1,6 @@ """Tests for the sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import extract_packed_value def test_sensor_device_class_set(generate_main): @@ -21,5 +13,5 @@ def test_sensor_device_class_set(generate_main): main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") # Then: device_class: voltage means packed value must be non-zero - packed = _extract_packed_value(main_cpp, "s_1") + packed = extract_packed_value(main_cpp, "s_1") assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 3ceaa9b8f81..c74dfb8a471 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,6 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" + +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_is_setup(generate_main): @@ -37,9 +39,9 @@ def test_text_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") - # Then - assert "it_2->set_internal(false);" in main_cpp - assert "it_3->set_internal(true);" in main_cpp + # Then: it_2 has internal: false, it_3 has internal: true + assert extract_packed_value(main_cpp, "it_2") & INTERNAL_BIT == 0 + assert extract_packed_value(main_cpp, "it_3") & INTERNAL_BIT != 0 def test_text_config_value_mode_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index f30b820e94d..1ff31ab96bd 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,14 +1,6 @@ """Tests for the text sensor component.""" -import re - - -def _extract_packed_value(main_cpp, var_name): - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" - return int(match.group(1)) +from tests.component_tests.helpers import INTERNAL_BIT, extract_packed_value def test_text_sensor_is_setup(generate_main): @@ -49,9 +41,9 @@ def test_text_sensor_config_value_internal_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_internal(true);" in main_cpp - assert "ts_3->set_internal(false);" in main_cpp + # Then: ts_2 has internal: true, ts_3 has internal: false + assert extract_packed_value(main_cpp, "ts_2") & INTERNAL_BIT != 0 + assert extract_packed_value(main_cpp, "ts_3") & INTERNAL_BIT == 0 def test_text_sensor_device_class_set(generate_main): @@ -65,7 +57,7 @@ def test_text_sensor_device_class_set(generate_main): # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date # so their packed values must be non-zero - packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + packed_ts_2 = extract_packed_value(main_cpp, "ts_2") assert packed_ts_2 != 0 - packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + packed_ts_3 = extract_packed_value(main_cpp, "ts_3") assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 3f6faaee54c..d6cbb8c6be0 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -9,6 +9,7 @@ import pytest from esphome.config_validation import Invalid from esphome.const import ( + CONF_DEVICE_CLASS, CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, @@ -16,16 +17,20 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, entity_helpers from esphome.core.entity_helpers import ( _register_string, _setup_entity_impl, entity_duplicate_validator, + finalize_entity_strings, get_base_entity_object_id, register_device_class, register_icon, + setup_device_class, setup_entity, + setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj from esphome.helpers import sanitize, snake_case @@ -486,8 +491,6 @@ async def test_setup_entity_disabled_by_default( ) -> None: """Test setup_entity sets disabled_by_default correctly.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -497,10 +500,8 @@ async def test_setup_entity_disabled_by_default( await _setup_entity_impl(var, config, "sensor") - # Check disabled_by_default was set - assert any( - "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions - ) + # disabled_by_default is now packed into config for configure_entity_() + assert config.get("_entity_disabled_by_default") == 1 def test_entity_duplicate_validator() -> None: @@ -785,8 +786,8 @@ async def test_setup_entity_empty_name_with_device( entity_helpers.get_variable = original_get_variable - # Check that set_device was called - assert any("sensor1.set_device" in expr for expr in added_expressions) + # Check that set_device_ was called (separate protected call, accessible via friend) + assert any("sensor1.set_device_" in expr for expr in added_expressions) # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime assert config.get("_entity_name") == "" @@ -928,7 +929,7 @@ def test_register_device_class_max_length() -> None: async def test_setup_entity_with_entity_category( setup_test_environment: list[str], ) -> None: - """Test setup_entity sets entity_category correctly.""" + """Test entity_category is packed correctly through the full setup flow.""" added_expressions = setup_test_environment var = MockObj("sensor1") config = { @@ -937,9 +938,10 @@ async def test_setup_entity_with_entity_category( CONF_ENTITY_CATEGORY: "diagnostic", } await _setup_entity_impl(var, config, "sensor") - assert any( - 'set_entity_category("diagnostic")' in expr for expr in added_expressions - ) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "category:diagnostic" in added_expressions[0] @pytest.mark.asyncio @@ -988,3 +990,199 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) -> assert body_called object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "temperature" + + +# Tests for finalize_entity_strings packing +# +# These tests verify that flags and string indices produce non-zero packed values +# and correct inline comments. The actual bit layout correctness (Python _*_SHIFT +# matching C++ ENTITY_FIELD_*_SHIFT) is verified end-to-end by the integration +# test test_host_mode_entity_fields, which compiles firmware and checks values +# via the native API. + + +def _extract_packed_value(expressions: list[str]) -> int: + """Extract the third argument (packed value) from a configure_entity_() call.""" + for expr in expressions: + if "configure_entity_" in expr: + # Match the last integer argument before the closing ");" + match = re.search(r",\s*(\d+)\s*\)", expr) + if match: + return int(match.group(1)) + raise AssertionError("No configure_entity_ call found") + + +@pytest.mark.asyncio +async def test_finalize_no_flags(setup_test_environment: list[str]) -> None: + """Test entity with no special flags — packed value is 0, no comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed == 0 + assert "//" not in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_internal(setup_test_environment: list[str]) -> None: + """Test entity with internal=True packs the internal flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_INTERNAL: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// internal" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_disabled_by_default( + setup_test_environment: list[str], +) -> None: + """Test entity with disabled_by_default=True packs the flag.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + assert "// disabled_by_default" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_entity_category( + setup_test_environment: list[str], +) -> None: + """Test entity_category values are packed and described in comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + + # Test diagnostic + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "diagnostic", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + packed_diag = _extract_packed_value(added_expressions) + assert packed_diag != 0 + assert "category:diagnostic" in added_expressions[0] + + # Test config — different packed value + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ENTITY_CATEGORY: "config", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + packed_cfg = _extract_packed_value(added_expressions) + assert packed_cfg != 0 + assert packed_cfg != packed_diag + assert "category:config" in added_expressions[0] + + +@pytest.mark.asyncio +async def test_finalize_string_indices( + setup_test_environment: list[str], +) -> None: + """Test device_class, unit_of_measurement, and icon produce non-zero packed value.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + comment = added_expressions[0] + assert "dc:temperature" in comment + assert "uom:°C" in comment + assert "icon:mdi:thermometer" in comment + + +@pytest.mark.asyncio +async def test_finalize_all_fields( + setup_test_environment: list[str], +) -> None: + """Test all fields set: flags, string indices, and comment.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: True, + CONF_INTERNAL: True, + CONF_ENTITY_CATEGORY: "diagnostic", + CONF_DEVICE_CLASS: "temperature", + CONF_UNIT_OF_MEASUREMENT: "°C", + CONF_ICON: "mdi:thermometer", + } + await _setup_entity_impl(var, config, "sensor") + setup_device_class(config) + setup_unit_of_measurement(config) + finalize_entity_strings(var, config) + packed = _extract_packed_value(added_expressions) + assert packed != 0 + # Verify comment contains all flags with actual string values + comment_line = added_expressions[0] + assert ( + "// internal, disabled_by_default, category:diagnostic," + " dc:temperature, uom:°C, icon:mdi:thermometer" in comment_line + ) + + +@pytest.mark.asyncio +async def test_finalize_comment_sanitization( + setup_test_environment: list[str], +) -> None: + """Test that user strings in comments are sanitized against injection.""" + added_expressions = setup_test_environment + var = MockObj("sensor1") + config = { + CONF_NAME: "Test", + CONF_DISABLED_BY_DEFAULT: False, + # Backslash at end would cause line splice eating next code line + CONF_ICON: "mdi:evil\\", + } + await _setup_entity_impl(var, config, "sensor") + finalize_entity_strings(var, config) + comment_line = added_expressions[0] + # Backslash must be replaced to prevent line splice + assert "\\" not in comment_line + assert "mdi:evil/" in comment_line + + added_expressions.clear() + config2 = { + CONF_NAME: "Test2", + CONF_DISABLED_BY_DEFAULT: False, + CONF_ICON: "mdi:evil\nINJECTED_CODE();", + } + await _setup_entity_impl(var, config2, "sensor") + finalize_entity_strings(var, config2) + comment_line = added_expressions[0] + # Newline must be replaced to prevent breaking out of comment + assert "\n" not in comment_line + assert "INJECTED_CODE" in comment_line # still visible but safe in comment From c681dc8872f88f894bb3b71026ffbec013c1bdc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:11:24 -1000 Subject: [PATCH 0505/2056] [socket] Add socket wake support for RP2040 (#14498) --- .../components/socket/lwip_raw_tcp_impl.cpp | 73 ++++++++++++++++++- esphome/components/socket/socket.h | 12 ++- esphome/core/application.cpp | 8 +- esphome/core/application.h | 8 +- esphome/core/component.cpp | 4 +- 5 files changed, 93 insertions(+), 12 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d697bd47a50..445a57809d2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,6 +11,9 @@ #ifdef USE_ESP8266 #include <coredecls.h> // For esp_schedule() +#elif defined(USE_RP2040) +#include <hardware/sync.h> // For __sev(), __wfe() +#include <pico/time.h> // For add_alarm_in_ms(), cancel_alarm() #endif namespace esphome::socket { @@ -40,6 +43,72 @@ void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } +#elif defined(USE_RP2040) +// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. +// +// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, +// then sleep with __wfe(). Wake on either: +// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout +// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake +// +// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, +// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept +// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + // Wake the main loop from __wfe() sleep — timeout expired. + __sev(); + // Return 0 = don't reschedule (one-shot) + return 0; +} + +void socket_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + // If a wake was already signalled, consume it and return immediately + // instead of going to sleep. This avoids losing a wake that arrived + // between loop iterations. + if (s_socket_woke) { + s_socket_woke = false; + return; + } + s_socket_woke = false; + s_delay_expired = false; + // Set a one-shot timer to wake us after the timeout. + // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + // Sleep until woken by either the timer alarm or socket_wake(). + // __wfe() may return spuriously (stale event register, other interrupts), + // so we loop checking both flags. + while (!s_socket_woke && !s_delay_expired) { + __wfe(); + } + // Cancel timer if we woke early (socket data arrived before timeout) + if (!s_delay_expired) + cancel_alarm(alarm); +} + +// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP +// callbacks via pendsv (not hard IRQ), so they execute from flash safely. +void socket_wake() { + s_socket_woke = true; + // Wake the main loop from __wfe() sleep. __sev() is a global event that + // wakes any core sleeping in __wfe(). This is ISR-safe. + __sev(); +} #endif static const char *const TAG = "socket.lwip"; @@ -371,7 +440,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. socket_wake(); #endif @@ -650,7 +719,7 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { sock->init(); this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. socket_wake(); #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 0884e4ba3e6..a21bd647305 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -120,13 +120,17 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf); -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Delay that can be woken early by socket activity. -/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +/// On ESP8266, uses esp_delay() with a callback that checks socket activity. +/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt +/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. void socket_delay(uint32_t ms); -/// Signal socket/IO activity and wake the main loop from esp_delay() early. -/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). +/// Signal socket/IO activity and wake the main loop early. +/// On ESP8266: sets flag + esp_schedule(). +/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). +/// ISR-safe on both platforms. void socket_wake(); // NOLINT(readability-redundant-declaration) #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index db1c8a0c0a1..8685bff360e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -32,7 +32,7 @@ #include "esphome/components/status_led/status_led.h" #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) #include "esphome/components/socket/socket.h" #endif @@ -713,8 +713,10 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity via esp_schedule() +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake socket::socket_delay(delay_ms); #else // No select support, use regular delay diff --git a/esphome/core/application.h b/esphome/core/application.h index 49253b63244..f357c6b1a3d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,7 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { void socket_wake(); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket @@ -565,8 +565,12 @@ class Application { #if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Wake the main event loop from any context (ISR, thread, or main loop). - /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context. + /// Sets the socket wake flag and calls __sev() to exit __wfe() early. + static void wake_loop_any_context() { socket::socket_wake(); } #endif protected: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2879d4b5ab1..cce0c7b3e04 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -322,11 +322,13 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ + ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. + // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. Application::wake_loop_any_context(); #endif } From 6ba5c9a7056a07fe431ebf3689dbda307892ea85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 15:22:39 -1000 Subject: [PATCH 0506/2056] [api] Skip state_action_() call in noise data path (#14629) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api_frame_helper.h | 11 +++++++++++ .../components/api/api_frame_helper_noise.cpp | 18 ++++-------------- .../api/api_frame_helper_plaintext.cpp | 14 +++++++------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 2b4e9ea3cdb..151314658ea 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -232,6 +232,17 @@ class APIFrameHelper { EXPLICIT_REJECT = 8, // Noise only }; + // Fast inline state check for read_packet/write_protobuf_messages hot path. + // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any + // other intermediate state to WOULD_BLOCK. + inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const { + if (this->state_ == State::DATA) + return APIError::OK; + if (this->state_ == State::CLOSED || this->state_ == State::FAILED) + return APIError::BAD_STATE; + return APIError::WOULD_BLOCK; + } + // Containers (size varies, but typically 12+ bytes on 32-bit) std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::vector<uint8_t> rx_buf_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index ba4f2f0642d..62523fb8358 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -397,14 +397,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr = this->state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } aerr = this->try_read_frame_(); if (aerr != APIError::OK) @@ -461,14 +456,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } if (messages.empty()) { return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index e2bb56e0acf..3c54ed7c70b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -195,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; - APIError aerr = this->try_read_frame_(); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -244,9 +244,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; if (messages.empty()) { return APIError::OK; From cac751e9e8d1a185ede1f001bb19cc397a3b6a7f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:29:41 +0100 Subject: [PATCH 0507/2056] [nextion] Add configurable HTTP parameters for TFT upload (#14234) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/nextion/base_component.py | 3 + esphome/components/nextion/display.py | 66 +++++++++++++++++-- esphome/components/nextion/nextion.cpp | 12 ++++ esphome/components/nextion/nextion.h | 31 +++++++++ .../nextion/nextion_upload_arduino.cpp | 10 +-- .../nextion/nextion_upload_esp32.cpp | 8 ++- .../components/nextion/common_tft_upload.yaml | 5 ++ .../nextion/common_tft_upload_watchdog.yaml | 3 + tests/components/nextion/test.esp32-ard.yaml | 6 +- tests/components/nextion/test.esp32-idf.yaml | 6 +- .../components/nextion/test.esp8266-ard.yaml | 5 +- 11 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 tests/components/nextion/common_tft_upload.yaml create mode 100644 tests/components/nextion/common_tft_upload_watchdog.yaml diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 86551cbe238..7705b21b0b4 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -27,6 +27,9 @@ CONF_PRECISION = "precision" CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" CONF_START_UP_PAGE = "start_up_page" CONF_STARTUP_OVERRIDE_MS = "startup_override_ms" +CONF_TFT_UPLOAD_HTTP_RETRIES = "tft_upload_http_retries" +CONF_TFT_UPLOAD_HTTP_TIMEOUT = "tft_upload_http_timeout" +CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT = "tft_upload_watchdog_timeout" CONF_TFT_URL = "tft_url" CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" CONF_VARIABLE_NAME = "variable_name" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 3bfcc959954..b8fcd5d8cfa 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -33,15 +33,24 @@ from .base_component import ( CONF_SKIP_CONNECTION_HANDSHAKE, CONF_START_UP_PAGE, CONF_STARTUP_OVERRIDE_MS, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] - DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] + + +def AUTO_LOAD() -> list[str]: + base = ["binary_sensor", "switch", "sensor", "text_sensor"] + if CORE.is_esp32: + base.append("watchdog") + return base + NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action @@ -55,7 +64,24 @@ BufferOverflowTrigger = nextion_ns.class_( "BufferOverflowTrigger", automation.Trigger.template() ) -CONFIG_SCHEMA = ( + +def _validate_tft_upload(config): + has_tft_url = CONF_TFT_URL in config + for conf_key in ( + CONF_TFT_UPLOAD_HTTP_TIMEOUT, + CONF_TFT_UPLOAD_HTTP_RETRIES, + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT, + ): + if conf_key in config and not has_tft_url: + raise cv.Invalid(f"{conf_key} requires {CONF_TFT_URL} to be set") + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config and not CORE.is_esp32: + raise cv.Invalid( + f"{CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT} is only available on ESP32" + ) + return config + + +CONFIG_SCHEMA = cv.All( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), @@ -115,6 +141,14 @@ CONFIG_SCHEMA = ( ), ), cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, + cv.Optional(CONF_TFT_UPLOAD_HTTP_RETRIES): cv.int_range(min=1, max=255), + cv.Optional(CONF_TFT_UPLOAD_HTTP_TIMEOUT): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + cv.Optional( + CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( 0, cv.int_range(min=3, max=65535) @@ -123,7 +157,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("5s")) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + _validate_tft_upload, ) @@ -176,6 +211,29 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + # TFT upload HTTP timeout (default: 4.5s) + if CONF_TFT_UPLOAD_HTTP_TIMEOUT in config: + cg.add( + var.set_tft_upload_http_timeout( + config[CONF_TFT_UPLOAD_HTTP_TIMEOUT].total_milliseconds + ) + ) + + # TFT upload HTTP retries (default: 5) + if CONF_TFT_UPLOAD_HTTP_RETRIES in config: + cg.add( + var.set_tft_upload_http_retries(config[CONF_TFT_UPLOAD_HTTP_RETRIES]) + ) + + # TFT upload watchdog timeout (default: 0 = no adjustment) + if CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT in config: + cg.add( + var.set_tft_upload_watchdog_timeout( + config[CONF_TFT_UPLOAD_WATCHDOG_TIMEOUT].total_milliseconds + ) + ) + if CORE.is_esp32: # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) esp32.include_builtin_idf_component("esp_http_client") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index c8c1b6fa412..cb20c34005c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -191,6 +191,18 @@ void Nextion::dump_config() { #ifdef USE_NEXTION_MAX_QUEUE_SIZE ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_); #endif +#ifdef USE_NEXTION_TFT_UPLOAD + ESP_LOGCONFIG(TAG, + " TFT URL: %s\n" + " TFT upload HTTP timeout: %" PRIu16 "ms\n" + " TFT upload HTTP retries: %u", + this->tft_url_.c_str(), this->tft_upload_http_timeout_, this->tft_upload_http_retries_); +#ifdef USE_ESP32 + if (this->tft_upload_watchdog_timeout_ > 0) { + ESP_LOGCONFIG(TAG, " TFT upload WDT timeout: %" PRIu32 "ms", this->tft_upload_watchdog_timeout_); + } +#endif // USE_ESP32 +#endif // USE_NEXTION_TFT_UPLOAD } void Nextion::update() { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c42ddba9b57..7999e3c4e3e 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1071,6 +1071,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); #ifdef USE_NEXTION_TFT_UPLOAD + /** + * @brief Set the HTTP timeout for TFT upload requests. + * @param timeout_ms Timeout in milliseconds. Defaults to 4500ms (4.5s). + */ + void set_tft_upload_http_timeout(uint16_t timeout_ms) { this->tft_upload_http_timeout_ = timeout_ms; } + +#ifdef USE_ESP32 + /** + * @brief Set the watchdog timeout during TFT upload. + * + * The system watchdog timeout is temporarily adjusted to this value + * during the entire TFT transfer process and restored to the original + * value after the transfer completes (whether successful or not). + * + * A value of 0 means no watchdog adjustment (default). + * + * @param timeout_ms Watchdog timeout in milliseconds. 0 = no adjustment. + */ + void set_tft_upload_watchdog_timeout(uint32_t timeout_ms) { this->tft_upload_watchdog_timeout_ = timeout_ms; } +#endif // USE_ESP32 + + /** + * @brief Set the number of HTTP retries for TFT upload requests. + * @param retries Number of retries. Defaults to 5. Range: 1-255. + */ + void set_tft_upload_http_retries(uint8_t retries) { this->tft_upload_http_retries_ = retries; } + /** * Set the tft file URL. */ @@ -1439,8 +1466,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int tft_size_ = 0; uint32_t original_baud_rate_ = 0; bool upload_first_chunk_sent_ = false; + uint16_t tft_upload_http_timeout_{4500}; ///< HTTP timeout in ms (default: 4.5s) + uint8_t tft_upload_http_retries_{5}; ///< HTTP retry count (default: 5) #ifdef USE_ESP32 + uint32_t tft_upload_watchdog_timeout_{0}; ///< WDT timeout in ms (0 = no adjustment) + /** * will request 4096 bytes chunks from the web server * and send each to Nextion diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 46a04c1b2e1..e03f1f470b2 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -166,7 +166,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Define the configuration for the HTTP client ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap()); HTTPClient http_client; - http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; #ifdef USE_ESP8266 @@ -192,15 +192,15 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.collectHeaders(header_names, 1); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); http_client.setReuse(true); - // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; int code = http_client.GET(); delay(100); // NOLINT App.feed_wdt(); - while (code != 200 && code != 206 && tries <= 5) { - ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/5", this->tft_url_.c_str(), - HTTPClient::errorToString(code).c_str(), tries); + while (code != 200 && code != 206 && tries <= this->tft_upload_http_retries_) { + ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/%u", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries, this->tft_upload_http_retries_); delay(250); // NOLINT App.feed_wdt(); diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 43f59a8d4b4..1014c728a81 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -7,6 +7,7 @@ #include <esp_http_client.h> #include <cinttypes> #include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -68,7 +69,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r int partial_read_len = 0; uint8_t retries = 0; // Attempt to read the chunk with retries. - while (retries < 5 && read_len < buffer_size) { + while (retries < this->tft_upload_http_retries_ && read_len < buffer_size) { partial_read_len = esp_http_client_read(http_client, reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len); if (partial_read_len > 0) { @@ -167,6 +168,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return false; } + // Temporarily adjust watchdog timeout for the duration of the TFT upload + watchdog::WatchdogManager wdm(this->tft_upload_watchdog_timeout_); + this->connection_state_.is_updating_ = true; if (exit_reparse) { @@ -190,7 +194,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, - .timeout_ms = 15000, + .timeout_ms = static_cast<int>(this->tft_upload_http_timeout_), .disable_auto_redirect = false, .max_redirection_count = 10, }; diff --git a/tests/components/nextion/common_tft_upload.yaml b/tests/components/nextion/common_tft_upload.yaml new file mode 100644 index 00000000000..190abbc7b19 --- /dev/null +++ b/tests/components/nextion/common_tft_upload.yaml @@ -0,0 +1,5 @@ +display: + - id: !extend main_lcd + tft_url: http://esphome.io/default35.tft + tft_upload_http_timeout: 20s + tft_upload_http_retries: 10 diff --git a/tests/components/nextion/common_tft_upload_watchdog.yaml b/tests/components/nextion/common_tft_upload_watchdog.yaml new file mode 100644 index 00000000000..385fee359e7 --- /dev/null +++ b/tests/components/nextion/common_tft_upload_watchdog.yaml @@ -0,0 +1,3 @@ +display: + - id: !extend main_lcd + tft_upload_watchdog_timeout: 30s diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml index 7e94a9b4a5b..a4f71628dac 100644 --- a/tests/components/nextion/test.esp32-ard.yaml +++ b/tests/components/nextion/test.esp32-ard.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml index 99820f0f8da..259b71c9d04 100644 --- a/tests/components/nextion/test.esp32-idf.yaml +++ b/tests/components/nextion/test.esp32-idf.yaml @@ -1,7 +1,5 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-idf.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml + tft_upload_watchdog: !include common_tft_upload_watchdog.yaml diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml index 49f79b2f4c5..a2b0e727cce 100644 --- a/tests/components/nextion/test.esp8266-ard.yaml +++ b/tests/components/nextion/test.esp8266-ard.yaml @@ -1,7 +1,4 @@ packages: uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft + tft_upload: !include common_tft_upload.yaml From 5b9cab02bee18da518c522fbdcf26b0b12fb17f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:37:54 -0400 Subject: [PATCH 0508/2056] [multiple] Add default initializers to uninitialized member variables (#14636) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/duty_time/duty_time_sensor.h | 6 +++--- esphome/components/growatt_solar/growatt_solar.h | 4 ++-- esphome/components/hte501/hte501.h | 4 ++-- esphome/components/select/select_call.h | 2 +- esphome/components/uponor_smatrix/uponor_smatrix.h | 4 ++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 +- esphome/components/x9c/x9c.h | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index d9fb2a6d604..d21802ebb6c 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -41,9 +41,9 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { sensor::Sensor *last_duty_time_sensor_{nullptr}; ESPPreferenceObject pref_; - uint32_t total_sec_; - uint32_t last_time_; - uint32_t edge_time_; + uint32_t total_sec_{0}; + uint32_t last_time_{0}; + uint32_t edge_time_{0}; bool last_state_{false}; bool restore_; }; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index b0ddd4b99d8..833d6a36ddb 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -56,8 +56,8 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: - bool waiting_to_update_; - uint32_t last_send_; + bool waiting_to_update_{false}; + uint32_t last_send_{0}; struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index b47daf9157c..7f29885f49d 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -18,8 +18,8 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index c9abbc69a0b..fbe7b82e929 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -43,7 +43,7 @@ class SelectCall { Select *const parent_; optional<size_t> index_; SelectOperation operation_{SELECT_OP_NONE}; - bool cycle_; + bool cycle_{false}; }; } // namespace esphome::select diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index bd760f0d77d..bd20e9b6a0a 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -89,8 +89,8 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { std::vector<uint8_t> rx_buffer_; std::queue<std::vector<uint8_t>> tx_queue_; - uint32_t last_rx_; - uint32_t last_tx_; + uint32_t last_rx_{0}; + uint32_t last_tx_{0}; #ifdef USE_TIME time::RealTimeClock *time_id_{nullptr}; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 8ef35a5f5d1..7ade170c022 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -23,7 +23,7 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor const network::IPAddress &dns2) override; protected: - std::array<text_sensor::TextSensor *, 5> ip_sensors_; + std::array<text_sensor::TextSensor *, 5> ip_sensors_{}; }; class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 66c3df14e19..7dcd79bb7ce 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -25,9 +25,9 @@ class X9cOutput : public output::FloatOutput, public Component { InternalGPIOPin *cs_pin_; InternalGPIOPin *inc_pin_; InternalGPIOPin *ud_pin_; - float initial_value_; - float pot_value_; - int step_delay_; + float initial_value_{0.0f}; + float pot_value_{0.0f}; + int step_delay_{0}; }; } // namespace x9c From 5d3893368d003a0904554e9170a1645f5c687efe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:16:32 -0400 Subject: [PATCH 0509/2056] [multiple] Add array bounds checks (#14635) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../addressable_light/addressable_light_display.cpp | 5 ++++- esphome/components/bme680_bsec/bme680_bsec.cpp | 2 +- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 1 + esphome/components/dac7678/dac7678_output.cpp | 2 ++ esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 5 +++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 6 ++++++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp index 16fab15b17b..329620bcf04 100644 --- a/esphome/components/addressable_light/addressable_light_display.cpp +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col if (this->pixel_mapper_f_.has_value()) { // Params are passed by reference, so they may be modified in call. - this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + int index = (*this->pixel_mapper_f_)(x, y); + if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size()) + return; + this->addressable_light_buffer_[index] = color; } else { this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 392d071b317..454be0c0fe4 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -383,7 +383,7 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: case BSEC_OUTPUT_STATIC_IAQ: { - uint8_t accuracy = outputs[i].accuracy; + uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1); this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 1a42c9d54b6..0210d1e67d7 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -438,6 +438,7 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou } } if (update_accuracy) { + max_accuracy = std::min<uint8_t>(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1); #ifdef USE_SENSOR this->queue_push_( [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 83f8722e7fc..27ab54f0be9 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -62,6 +62,8 @@ void DAC7678Output::register_channel(DAC7678Channel *channel) { } void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (channel >= std::size(this->dac_input_reg_)) + return; if (this->dac_input_reg_[channel] != value) { ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 99d519b434d..263603704ac 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -452,7 +452,8 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da } break; case 0x83: - if (this->custom_presence_of_detection_sensor_ != nullptr) { + if (this->custom_presence_of_detection_sensor_ != nullptr && + data[FRAME_DATA_INDEX] < std::size(S_PRESENCE_OF_DETECTION_RANGE_STR)) { this->custom_presence_of_detection_sensor_->publish_state( S_PRESENCE_OF_DETECTION_RANGE_STR[data[FRAME_DATA_INDEX]]); } @@ -646,7 +647,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { #ifdef USE_BINARY_SENSOR case 0x01: case 0x81: - if (this->has_target_binary_sensor_ != nullptr) { + if (this->has_target_binary_sensor_ != nullptr && data[FRAME_DATA_INDEX] < std::size(S_SOMEONE_EXISTS_STR)) { this->has_target_binary_sensor_->publish_state(S_SOMEONE_EXISTS_STR[data[FRAME_DATA_INDEX]]); } break; diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index c6527a948ef..e24e9b338e6 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -334,6 +334,8 @@ void MR60FDA2Component::process_frame_() { // Send Heartbeat Packet Command void MR60FDA2Component::set_install_height(uint8_t index) { + if (index >= std::size(INSTALL_HEIGHT)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x04, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -345,6 +347,8 @@ void MR60FDA2Component::set_install_height(uint8_t index) { } void MR60FDA2Component::set_height_threshold(uint8_t index) { + if (index >= std::size(HEIGHT_THRESHOLD)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x08, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00}; float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); @@ -356,6 +360,8 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) { } void MR60FDA2Component::set_sensitivity(uint8_t index) { + if (index >= std::size(SENSITIVITY)) + return; uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x0A, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00}; int_to_bytes(SENSITIVITY[index], &send_data[8]); From 088a8a4338940c061fa181b55845a5d25c6268ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 8 Mar 2026 17:23:58 -1000 Subject: [PATCH 0510/2056] [ci] Match symbols with changed signatures in memory impact analysis (#14600) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- script/ci_memory_impact_comment.py | 75 ++++++++++++++ tests/unit_tests/analyze_memory/__init__.py | 0 .../test_ci_memory_impact_comment.py | 99 +++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 tests/unit_tests/analyze_memory/__init__.py create mode 100644 tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index a2961306457..01316da27fe 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -160,6 +160,76 @@ def format_change(before: int, after: int, threshold: float | None = None) -> st return f"{emoji} {delta_str} ({pct_str})" +def _sig_base(sym: str) -> str: + """Strip argument types from a symbol name for fuzzy matching. + + Removes the entire outermost parenthesized argument list (including + the parentheses) from the symbol string. + + This makes, for example, "foo(int)::nested" and "foo(float)::nested" + share the same key "foo::nested", while "foo(int)" maps to "foo" and + therefore does NOT collide with "foo(int)::nested". + """ + start = sym.find("(") + if start == -1: + return sym + end = sym.rfind(")") + if end == -1: + return sym + return sym[:start] + sym[end + 1 :] + + +_AMBIGUOUS = object() + + +def _match_signature_changes( + changed_symbols: list[tuple[str, int, int, int]], + new_symbols: list[tuple[str, int]], + removed_symbols: list[tuple[str, int]], +) -> tuple[ + list[tuple[str, int, int, int]], + list[tuple[str, int]], + list[tuple[str, int]], +]: + """Match new/removed symbol pairs that only differ in argument types. + + When a function's argument types change (e.g. foo(vector<>&) -> foo(Buffer&)), + it appears as a new + removed symbol. This matches them by base name and moves + them to changed_symbols. Only matches unambiguous 1:1 pairs. + """ + if not new_symbols or not removed_symbols: + return changed_symbols, new_symbols, removed_symbols + + # Build base -> entry maps; mark ambiguous bases with sentinel + new_by_base: dict[str, tuple[str, int] | object] = {} + for entry in new_symbols: + base = _sig_base(entry[0]) + new_by_base[base] = _AMBIGUOUS if base in new_by_base else entry + removed_by_base: dict[str, tuple[str, int] | object] = {} + for entry in removed_symbols: + base = _sig_base(entry[0]) + removed_by_base[base] = _AMBIGUOUS if base in removed_by_base else entry + + matched: set[str] = set() # matched base keys + for base, new_entry in new_by_base.items(): + if new_entry is _AMBIGUOUS: + continue + rem_entry = removed_by_base.get(base) + if rem_entry is None or rem_entry is _AMBIGUOUS: + continue + pr_sym, pr_size = new_entry + _rm_sym, target_size = rem_entry + delta = pr_size - target_size + if delta != 0: + changed_symbols.append((pr_sym, target_size, pr_size, delta)) + matched.add(base) + + if matched: + new_symbols = [e for e in new_symbols if _sig_base(e[0]) not in matched] + removed_symbols = [e for e in removed_symbols if _sig_base(e[0]) not in matched] + return changed_symbols, new_symbols, removed_symbols + + def prepare_symbol_changes_data( target_symbols: dict | None, pr_symbols: dict | None ) -> dict | None: @@ -200,6 +270,11 @@ def prepare_symbol_changes_data( delta = pr_size - target_size changed_symbols.append((symbol, target_size, pr_size, delta)) + # Match new/removed symbols that only differ in argument types + changed_symbols, new_symbols, removed_symbols = _match_signature_changes( + changed_symbols, new_symbols, removed_symbols + ) + if not changed_symbols and not new_symbols and not removed_symbols: return None diff --git a/tests/unit_tests/analyze_memory/__init__.py b/tests/unit_tests/analyze_memory/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py new file mode 100644 index 00000000000..8399ac0303c --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_ci_memory_impact_comment.py @@ -0,0 +1,99 @@ +"""Tests for script/ci_memory_impact_comment.py symbol matching.""" + +from pathlib import Path +import sys + +# Add script directory to path so we can import the module +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "script")) + +from ci_memory_impact_comment import prepare_symbol_changes_data # noqa: E402 + + +def test_prepare_symbol_changes_signature_match() -> None: + """Symbols with same base name but different args are matched as changed.""" + target = { + "Foo::bar(std::vector<unsigned char>&, int)": 300, + "unchanged()": 50, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "unchanged()": 50, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert t_size == 300 + assert p_size == 320 + assert delta == 20 + + +def test_prepare_symbol_changes_ambiguous_overloads_not_matched() -> None: + """Multiple overloads with same base name stay as new/removed.""" + target = { + "Foo::bar(int)": 100, + "Foo::bar(float)": 200, + } + pr = { + "Foo::bar(double)": 150, + "Foo::bar(long)": 250, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 2 + assert len(result["removed_symbols"]) == 2 + + +def test_prepare_symbol_changes_no_parens_not_matched() -> None: + """Symbols without parens (variables) are not fuzzy-matched.""" + target = {"my_global_var": 100} + pr = {"my_global_var_v2": 120} + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 0 + assert len(result["new_symbols"]) == 1 + assert len(result["removed_symbols"]) == 1 + + +def test_prepare_symbol_changes_nested_symbols_matched_separately() -> None: + """Nested symbols like ::__pstr__ don't collide with parent function.""" + target = { + "Foo::bar(std::vector<unsigned char>&, int)": 300, + "Foo::bar(std::vector<unsigned char>&, int)::__pstr__": 19, + } + pr = { + "Foo::bar(ProtoByteBuffer&, int)": 320, + "Foo::bar(ProtoByteBuffer&, int)::__pstr__": 19, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + # Both the function and its nested __pstr__ should be matched (not new/removed) + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + # __pstr__ has delta=0 so it's silently dropped, only the function shows + assert len(result["changed_symbols"]) == 1 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(ProtoByteBuffer&, int)" + assert delta == 20 + + +def test_prepare_symbol_changes_exact_match_preferred() -> None: + """Exact name matches are found before fuzzy matching runs.""" + target = { + "Foo::bar(int)": 100, + } + pr = { + "Foo::bar(int)": 120, + } + result = prepare_symbol_changes_data(target, pr) + assert result is not None + assert len(result["changed_symbols"]) == 1 + assert len(result["new_symbols"]) == 0 + assert len(result["removed_symbols"]) == 0 + sym, t_size, p_size, delta = result["changed_symbols"][0] + assert sym == "Foo::bar(int)" + assert delta == 20 From f3ca86b67017991a13a1cb27b242b373e64ed29e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:48:03 +1100 Subject: [PATCH 0511/2056] [ci-custom] Directions on constant hoisting (#14637) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 8e1652b505d..06fcdadb8ce 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -519,7 +519,7 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " + f"constant to esphome/components/const/__init__.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. " "See https://developers.esphome.io/contributing/code/#python" ) return errs From 0db9137d9101a04c4b2a8836e13308de60280c35 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:10:48 -0400 Subject: [PATCH 0512/2056] [multiple] Add division by zero guards (#14634) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/bl0942/bl0942.cpp | 2 +- esphome/components/combination/combination.cpp | 11 ++++++++++- esphome/components/graph/graph.cpp | 2 +- esphome/components/mics_4514/mics_4514.cpp | 6 ++++++ esphome/components/tsl2561/tsl2561.cpp | 4 ++++ esphome/components/ufire_ec/ufire_ec.cpp | 14 +++++++++++--- esphome/components/ufire_ec/ufire_ec.h | 2 +- esphome/components/xxtea/xxtea.cpp | 4 ++++ 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 16ad33141d1..7d385974231 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -173,7 +173,7 @@ void BL0942::received_package_(DataPacket *data) { float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; float total_energy_consumption = cf_cnt / energy_reference_; - float frequency = 1000000.0f / data->frequency; + float frequency = data->frequency != 0 ? 1000000.0f / data->frequency : NAN; if (voltage_sensor_ != nullptr) { voltage_sensor_->publish_state(v_rms); diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index ece7cca4825..2f0bd26a02d 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -163,7 +163,7 @@ void MeanCombinationComponent::handle_new_value(float value) { return; float sum = 0.0; - size_t count = 0.0; + size_t count = 0; for (const auto &sensor : this->sensors_) { if (std::isfinite(sensor->state)) { @@ -172,6 +172,10 @@ void MeanCombinationComponent::handle_new_value(float value) { } } + if (count == 0) { + this->publish_state(NAN); + return; + } float mean = sum / count; this->publish_state(mean); @@ -238,6 +242,11 @@ void RangeCombinationComponent::handle_new_value(float value) { } } + if (sensor_states.empty()) { + this->publish_state(NAN); + return; + } + sort(sensor_states.begin(), sensor_states.end()); float range = sensor_states.back() - sensor_states.front(); diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c43cd07fe08..801c97e3f56 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -171,7 +171,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo bool prev_b = false; int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { - float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; + float v = yrange != 0 ? (trace->get_tracedata()->get_value(i) - ymin) / yrange : NAN; if (!std::isnan(v) && (thick > 0)) { int16_t x = this->width_ - 1 - i + x_offset; uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 60413b32d78..ce63a7d062b 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -59,6 +59,12 @@ void MICS4514Component::update() { return; } + if (this->red_calibration_ == 0 || this->ox_calibration_ == 0) { + ESP_LOGW(TAG, "Calibration values are zero, retrying"); + this->status_set_warning(); + this->initial_ = true; + return; + } float red_f = (float) (power - red) / this->red_calibration_; float ox_f = (float) (power - ox) / this->ox_calibration_; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index cb4c38a83ce..bccff1fb26a 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -70,6 +70,10 @@ float TSL2561Sensor::calculate_lx_(uint16_t ch0, uint16_t ch1) { return NAN; } + if (ch0 == 0) { + ESP_LOGVV(TAG, "No light detected"); + return 0.0f; + } float d0 = ch0, d1 = ch1; float ratio = d1 / d0; diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index a1c3568a1a3..40e3be2757e 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -1,5 +1,6 @@ #include "esphome/core/log.h" #include "ufire_ec.h" +#include <cmath> namespace esphome { namespace ufire_ec { @@ -60,9 +61,15 @@ float UFireECComponent::measure_temperature_() { return this->read_data_(REGISTE float UFireECComponent::measure_ms_() { return this->read_data_(REGISTER_MS); } -void UFireECComponent::set_solution_(float solution, float temperature) { - solution /= (1 - (this->temperature_coefficient_ * (temperature - 25))); +bool UFireECComponent::set_solution_(float solution, float temperature) { + float denom = 1 - (this->temperature_coefficient_ * (temperature - 25)); + if (std::abs(denom) < 1e-6f) { + ESP_LOGE(TAG, "Temperature compensation denominator is zero"); + return false; + } + solution /= denom; this->write_data_(REGISTER_SOLUTION, solution); + return true; } void UFireECComponent::set_compensation_(float temperature) { this->write_data_(REGISTER_COMPENSATION, temperature); } @@ -72,7 +79,8 @@ void UFireECComponent::set_coefficient_(float coefficient) { this->write_data_(R void UFireECComponent::set_temperature_(float temperature) { this->write_data_(REGISTER_TEMP, temperature); } void UFireECComponent::calibrate_probe(float solution, float temperature) { - this->set_solution_(solution, temperature); + if (!this->set_solution_(solution, temperature)) + return; this->write_byte(REGISTER_TASK, COMMAND_CALIBRATE_PROBE); } diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index bfbed1b43e0..8a648b5038b 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -44,7 +44,7 @@ class UFireECComponent : public PollingComponent, public i2c::I2CDevice { protected: float measure_temperature_(); float measure_ms_(); - void set_solution_(float solution, float temperature); + bool set_solution_(float solution, float temperature); void set_compensation_(float temperature); void set_coefficient_(float coefficient); void set_temperature_(float temperature); diff --git a/esphome/components/xxtea/xxtea.cpp b/esphome/components/xxtea/xxtea.cpp index aae663ee016..ba17530b243 100644 --- a/esphome/components/xxtea/xxtea.cpp +++ b/esphome/components/xxtea/xxtea.cpp @@ -7,6 +7,8 @@ static const uint32_t DELTA = 0x9e3779b9; #define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) void encrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; @@ -25,6 +27,8 @@ void encrypt(uint32_t *v, size_t n, const uint32_t *k) { } void decrypt(uint32_t *v, size_t n, const uint32_t *k) { + if (n == 0) + return; uint32_t z, y, sum, e; size_t p; size_t q = 6 + 52 / n; From 31f4b4d00d5242979ca83a98acc3b60b0bf0c84b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 07:33:08 -0400 Subject: [PATCH 0513/2056] [multiple] Fix undefined behavior across components (#14639) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/e131/e131_packet.cpp | 3 ++- .../components/globals/globals_component.h | 2 +- .../grove_tb6612fng/grove_tb6612fng.cpp | 3 ++- esphome/components/midea/appliance_base.h | 6 +++-- esphome/components/nextion/nextion.cpp | 12 +++------ esphome/components/ruuvi_ble/ruuvi_ble.cpp | 26 +++++++++---------- .../components/tormatic/tormatic_protocol.h | 2 +- 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index b90e6d5c914..600793f5d35 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,3 +1,4 @@ +#include <cstddef> #include <cstring> #include "e131.h" #ifdef USE_NETWORK @@ -57,7 +58,7 @@ union E131RawPacket { // We need to have at least one `1` value // Get the offset of `property_values[1]` -const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *) nullptr)->property_values[1]); +const size_t E131_MIN_PACKET_SIZE = offsetof(E131RawPacket, property_values) + sizeof(uint8_t); bool E131Component::join_igmp_groups_() { if (this->listen_method_ != E131_MULTICAST) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3db29bea356..520c068e6f4 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -84,7 +84,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P this->rtc_ = global_preferences->make_preference<uint8_t[SZ]>(1944399030U ^ this->name_hash_); bool hasdata = this->rtc_.load(&temp); if (hasdata) { - this->value_.assign(temp + 1, temp[0]); + this->value_.assign(temp + 1, static_cast<uint8_t>(temp[0])); } this->last_checked_value_.assign(this->value_); } diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index 428c8ec4a8c..c10fa4cf257 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -139,7 +139,8 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, } void GroveMotorDriveTB6612FNG::stepper_stop() { - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, &status, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Send stop stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 060cbd996bb..660d185b491 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -28,12 +28,14 @@ class UARTStream : public Stream { int available() override { return this->uart_->available(); } int read() override { uint8_t data; - this->uart_->read_byte(&data); + if (!this->uart_->read_byte(&data)) + return -1; return data; } int peek() override { uint8_t data; - this->uart_->peek_byte(&data); + if (!this->uart_->peek_byte(&data)) + return -1; return data; } size_t write(uint8_t data) override { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index cb20c34005c..7ae4d50fc80 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -651,11 +651,7 @@ void Nextion::process_nextion_commands_() { break; } - int value = 0; - - for (int i = 0; i < 4; ++i) { - value += to_process[i] << (8 * i); - } + int value = static_cast<int>(encode_uint32(to_process[3], to_process[2], to_process[1], to_process[0])); NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { @@ -751,10 +747,8 @@ void Nextion::process_nextion_commands_() { index = to_process.find('\0'); variable_name = to_process.substr(0, index); // // Get variable name - int value = 0; - for (int i = 0; i < 4; ++i) { - value += to_process[i + index + 1] << (8 * i); - } + int value = static_cast<int>( + encode_uint32(to_process[index + 4], to_process[index + 3], to_process[index + 2], to_process[index + 1])); ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value); diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index bf088873ce0..07f870b60cc 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -21,11 +21,11 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val; const float humidity = data[0] * 0.5f; - const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f; - const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f; - const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f; - const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f; + const float pressure = (encode_uint16(data[3], data[4]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast<int16_t>(encode_uint16(data[5], data[6])) / 1000.0f; + const float acceleration_y = static_cast<int16_t>(encode_uint16(data[7], data[8])) / 1000.0f; + const float acceleration_z = static_cast<int16_t>(encode_uint16(data[9], data[10])) / 1000.0f; + const float battery_voltage = encode_uint16(data[11], data[12]) / 1000.0f; result.humidity = humidity; result.temperature = temperature; @@ -43,19 +43,19 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP if (adv_data.size() != 24) return false; - const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; - const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f; - const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f; - const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f; - const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; - const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; + const float temperature = static_cast<int16_t>(encode_uint16(data[0], data[1])) * 0.005f; + const float humidity = encode_uint16(data[2], data[3]) / 400.0f; + const float pressure = (encode_uint16(data[4], data[5]) + 50000.0f) / 100.0f; + const float acceleration_x = static_cast<int16_t>(encode_uint16(data[6], data[7])) / 1000.0f; + const float acceleration_y = static_cast<int16_t>(encode_uint16(data[8], data[9])) / 1000.0f; + const float acceleration_z = static_cast<int16_t>(encode_uint16(data[10], data[11])) / 1000.0f; - const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]); + const uint16_t power_info = encode_uint16(data[12], data[13]); const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; const float movement_counter = float(data[14]); - const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16])); + const float measurement_sequence_number = float(encode_uint16(data[15], data[16])); result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature; result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity; diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 057713b8845..26a634b6306 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -99,7 +99,7 @@ struct MessageHeader { // payload_size returns the amount of payload bytes to be read from the uart // buffer after reading the header. - uint32_t payload_size() { return this->len - sizeof(this->type); } + uint32_t payload_size() { return this->len > sizeof(this->type) ? this->len - sizeof(this->type) : 0; } } __attribute__((packed)); // StatusType denotes which 'page' of information needs to be retrieved. From 019db745828289f5d105475a816f6dc1cabd814d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:44:27 +0000 Subject: [PATCH 0514/2056] Bump setuptools from 82.0.0 to 82.0.1 (#14665) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6a2c22a5d6..2e3a247768b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.1", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] From a379e5a6357340a696d9cbc67a8ad48a0bad0924 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:16:29 -0400 Subject: [PATCH 0515/2056] [runtime_image][st7701s] Fix BMP decoder and LCD init bugs (#14663) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../components/runtime_image/bmp_decoder.cpp | 18 +++++++++++++++--- esphome/components/st7701s/st7701s.cpp | 6 ++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 1a56484c606..7003f4da2ff 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -63,7 +63,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { switch (this->bits_per_pixel_) { case 1: - this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); + this->width_bytes_ = (this->width_ + 7) / 8; + this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; break; case 24: this->width_bytes_ = this->width_ * 3; @@ -92,15 +93,26 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { case 1: { while (index < size) { uint8_t current_byte = buffer[index]; + bool end_of_row = false; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i; + size_t x = this->paint_index_ % static_cast<size_t>(this->width_); size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); + this->paint_index_++; + // End of pixel row: skip remaining bits in this byte + if (x + 1 >= static_cast<size_t>(this->width_)) { + end_of_row = true; + break; + } } - this->paint_index_ += 8; this->current_index_++; index++; + // End of pixel row: skip row padding bytes (4-byte alignment) + if (end_of_row && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } } break; } diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 221fe39b9d8..ecce4eb4b27 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -36,11 +36,13 @@ void ST7701S::setup() { config.de_gpio_num = this->de_pin_->get_pin(); config.pclk_gpio_num = this->pclk_pin_->get_pin(); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); - ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); - ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; } + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); } void ST7701S::loop() { From 75f55adbfa0cd1a75c9e33a16cc6a95d7195c50c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:17:31 -0400 Subject: [PATCH 0516/2056] [api][at581x][vl53l0x] Fix bounds check issues in 3 components (#14660) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api_frame_helper_noise.cpp | 2 ++ esphome/components/at581x/at581x.cpp | 5 +++++ esphome/components/vl53l0x/vl53l0x_sensor.cpp | 4 ++-- esphome/components/vl53l0x/vl53l0x_sensor.h | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 62523fb8358..256357ce6a7 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -375,6 +375,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso #ifdef USE_STORE_LOG_STR_IN_FLASH // On ESP8266 with flash strings, we need to use PROGMEM-aware functions size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len); } @@ -382,6 +383,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso // Normal memory access const char *reason_str = LOG_STR_ARG(reason); size_t reason_len = strlen(reason_str); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string std::memcpy(data + 1, reason_str, reason_len); diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 728fbe20c69..6fc85b07902 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() { } // Set gain + if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) || + static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) { + ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_); + return false; + } if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { ESP_LOGE(TAG, "Failed to write AT581X gain registers"); diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index e833657fc4d..0b2b40d723a 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -87,9 +87,9 @@ void VL53L0XSensor::setup() { reg(0x94) = 0x6B; reg(0x83) = 0x00; - this->timeout_start_us_ = micros(); + uint32_t timeout_start_us = micros(); while (reg(0x83).get() == 0x00) { - if (this->timeout_us_ > 0 && ((uint16_t) (micros() - this->timeout_start_us_) > this->timeout_us_)) { + if (this->timeout_us_ > 0 && (micros() - timeout_start_us > this->timeout_us_)) { ESP_LOGE(TAG, "'%s' - setup timeout", this->name_.c_str()); this->mark_failed(); return; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 2bf90015fe4..f533005b5b3 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -64,8 +64,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c bool waiting_for_interrupt_{false}; uint8_t stop_variable_; - uint16_t timeout_start_us_; - uint16_t timeout_us_{}; + uint32_t timeout_us_{}; static std::list<VL53L0XSensor *> vl53_sensors; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From b721cd48e5d8d41ad12eedf9e4ec941483cd99c3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:07 -0400 Subject: [PATCH 0517/2056] [hmc5883l][mmc5603][honeywellabp2][xgzp68xx][max9611] Fix uninitialized members (#14659) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/hmc5883l/hmc5883l.h | 2 +- esphome/components/honeywellabp2_i2c/honeywellabp2.h | 4 ++-- esphome/components/max9611/sensor.py | 4 +++- esphome/components/mmc5603/mmc5603.h | 4 ++-- esphome/components/xgzp68xx/sensor.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 8eae0f7a50c..b5cf93e62b8 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -61,7 +61,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index 274de847ac7..d29ebb855dd 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -45,8 +45,8 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) - float max_count_; - float min_count_; + float max_count_{max_count_a_}; + float min_count_{min_count_a_}; bool measurement_running_ = false; uint8_t raw_data_[7]; // holds output data diff --git a/esphome/components/max9611/sensor.py b/esphome/components/max9611/sensor.py index 8405a3f75af..b3a73d8c10c 100644 --- a/esphome/components/max9611/sensor.py +++ b/esphome/components/max9611/sensor.py @@ -35,7 +35,9 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(MAX9611Component), - cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance, + cv.Required(CONF_SHUNT_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=1e-6) + ), cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index f827e27e042..9a77b78bc19 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -27,7 +27,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; } protected: - MMC5603Datarate datarate_; + MMC5603Datarate datarate_{MMC5603_DATARATE_75_0_HZ}; sensor::Sensor *x_sensor_{nullptr}; sensor::Sensor *y_sensor_{nullptr}; sensor::Sensor *z_sensor_{nullptr}; @@ -37,7 +37,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; }; } // namespace mmc5603 diff --git a/esphome/components/xgzp68xx/sensor.py b/esphome/components/xgzp68xx/sensor.py index 2b38392a027..6b83012eb42 100644 --- a/esphome/components/xgzp68xx/sensor.py +++ b/esphome/components/xgzp68xx/sensor.py @@ -56,7 +56,7 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_K_VALUE, default=4096): cv.uint16_t, + cv.Optional(CONF_K_VALUE, default=4096): cv.int_range(min=1, max=65535), } ) .extend(cv.polling_component_schema("60s")) From 08a0608a48965edfd7ef11301d2e38faefe2f5ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:21 -0400 Subject: [PATCH 0518/2056] [wifi][captive_portal][heatpumpir][es8388] Fix wrong behavior in 4 components (#14657) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../captive_portal/dns_server_esp32_idf.cpp | 5 +++-- esphome/components/es8388/es8388.cpp | 4 ++-- esphome/components/heatpumpir/heatpumpir.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 20 ++++--------------- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index bd9989a40cb..7b75f042419 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "captive_portal.dns"; // DNS constants static constexpr uint16_t DNS_PORT = 53; static constexpr uint16_t DNS_QR_FLAG = 1 << 15; +static constexpr uint16_t DNS_AA_FLAG = 1 << 10; static constexpr uint16_t DNS_OPCODE_MASK = 0x7800; static constexpr uint16_t DNS_QTYPE_A = 0x0001; static constexpr uint16_t DNS_QCLASS_IN = 0x0001; @@ -162,8 +163,8 @@ void DNSServer::process_next_request() { } // Build DNS response by modifying the request in-place - header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative - header->an_count = htons(1); // One answer + header->flags = htons(DNS_QR_FLAG | DNS_AA_FLAG); // Response + Authoritative + header->an_count = htons(1); // One answer // Add answer section after the question size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader); diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 72026a2a842..c252cdb707d 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -152,7 +152,7 @@ void ES8388::dump_config() { bool ES8388::set_volume(float volume) { volume = clamp(volume, 0.0f, 1.0f); - uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, -96, 0); + uint8_t value = remap<uint8_t, float>(volume, 0.0f, 1.0f, 192, 0); ESP_LOGD(TAG, "Setting ES8388_DACCONTROL4 / ES8388_DACCONTROL5 to 0x%02X (volume: %f)", value, volume); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL4, value)); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL5, value)); @@ -163,7 +163,7 @@ bool ES8388::set_volume(float volume) { float ES8388::volume() { uint8_t value; ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL4, &value)); - return remap<float, uint8_t>(value, -96, 0, 0.0f, 1.0f); + return remap<float, uint8_t>(value, 192, 0, 0.0f, 1.0f); } bool ES8388::set_mute_state_(bool mute_state) { diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 6b73a24dc4b..11e7672dc13 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -114,7 +114,7 @@ void HeatpumpIRClimate::setup() { this->current_temperature = state; IRSenderESPHome esp_sender(this->transmitter_); - this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5))); + this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature))); // current temperature changed, publish state this->publish_state(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a9b26c5935e..0bf7934878a 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -638,8 +638,6 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { - static bool first_scan = false; - // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -656,23 +654,13 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; - if (first_scan) { - if (passive) { - config.scan_time.passive = 200; - } else { - config.scan_time.active.min = 100; - config.scan_time.active.max = 200; - } + if (passive) { + config.scan_time.passive = 500; } else { - if (passive) { - config.scan_time.passive = 500; - } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; - } + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; } #endif - first_scan = false; bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed"); From 9418f35cc32e0a6216b09b813664aa40bcc3e216 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:18:44 -0400 Subject: [PATCH 0519/2056] [multiple] Remove unnecessary heap allocations in 4 components (#14656) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/daikin_arc/daikin_arc.cpp | 9 +++++---- esphome/components/pn7150_i2c/pn7150_i2c.cpp | 3 ++- esphome/components/pn7160_i2c/pn7160_i2c.cpp | 3 ++- esphome/components/toshiba/toshiba.cpp | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index c45fa307a7c..adb7b9fec76 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -350,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; size_t bytes_count = data.size() / 2 / 8; - size_t buf_size = bytes_count * 3 + 1; - std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill) + // Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf + char buf[40 * 3 + 1] = {}; + constexpr size_t buf_size = sizeof(buf); size_t buf_pos = 0; for (size_t i = 0; i < bytes_count; i++) { uint8_t byte = 0; @@ -363,9 +364,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { break; } } - buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); + buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte); } - ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); + ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf, data.size()); } if (!valid_daikin_frame) { char sbuf[16 * 10 + 1] = {0}; diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp index 38b3102b374..4ae884595bb 100644 --- a/esphome/components/pn7150_i2c/pn7150_i2c.cpp +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp index 7c6da9dd068..e33c6c793d0 100644 --- a/esphome/components/pn7160_i2c/pn7160_i2c.cpp +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -34,7 +34,8 @@ uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { } uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { - if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + auto encoded = tx.encode(); + if (this->write(encoded.data(), encoded.size()) == i2c::ERROR_OK) { return nfc::STATUS_OK; } return nfc::STATUS_FAILED; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index e0c150537a9..53114cc50fc 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -951,10 +951,10 @@ void ToshibaClimate::transmit_ras_2819t_() { } uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) { - const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, - RAC_PT1411HWRU_SWING_HEADER}; + static constexpr uint8_t HEADERS[] = {RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER, + RAC_PT1411HWRU_SWING_HEADER}; - for (auto i : header) { + for (auto i : HEADERS) { if ((message[0] == i) && (message[1] == static_cast<uint8_t>(~i))) return i; } From fecedeb01833d01cbc26ad331597a554e7904086 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:20:09 -0400 Subject: [PATCH 0520/2056] [multiple] Fix crashes from malformed external input (batch 2) (#14651) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 5 ++++ .../modbus_controller/modbus_controller.cpp | 23 +++++++++++++++++++ .../nextion/nextion_upload_arduino.cpp | 6 +++++ .../nextion/nextion_upload_esp32.cpp | 6 +++++ .../seeed_mr60bha2/seeed_mr60bha2.cpp | 2 +- .../components/usb_host/usb_host_client.cpp | 5 ++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 73a298d279a..0e2a515b40f 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -514,6 +514,11 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { continue; // Possible zero padded advertisement data } + // Validate field fits in remaining payload + if (offset + field_length > len) { + break; + } + // first byte of adv record is adv record type const uint8_t record_type = payload[offset++]; const uint8_t *record = &payload[offset]; diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 7f0eb230e0a..f77f51a20df 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -59,6 +59,10 @@ bool ModbusController::send_next_command_() { // Queue incoming response void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) { + if (this->command_queue_.empty()) { + ESP_LOGW(TAG, "Received modbus data but command queue is empty"); + return; + } auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { if (this->module_offline_) { @@ -92,6 +96,9 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); + if (this->command_queue_.empty()) { + return; + } // Remove pending command waiting for a response auto ¤t_command = this->command_queue_.front(); if (current_command != nullptr) { @@ -175,6 +182,11 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st uint16_t payload_offset; if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + if (data.size() < 5) { + ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); @@ -188,8 +200,19 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); return; } + if (data.size() < 5 + payload_size) { + ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(), + 5 + payload_size); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } payload_offset = 5; } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { + if (data.size() < 4) { + ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } number_of_registers = 1; payload_offset = 2; } else { diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index e03f1f470b2..6c454ab7459 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -86,6 +86,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 1014c728a81..166bbcc86a8 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -108,6 +108,12 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r static_cast<uint32_t>(esp_get_free_heap_size())); #endif upload_first_chunk_sent_ = true; + if (recv_string.empty()) { + ESP_LOGW(TAG, "No response from display during upload"); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD( diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 8628faac5ae..1b5eaf63679 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -199,7 +199,7 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } break; case DISTANCE_TYPE_BUFFER: - if (data[0] != 0) { + if (length >= 1 && data[0] != 0) { if (this->distance_sensor_ != nullptr && length >= 8) { uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]); float distance_float; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index c77d738ace7..2a460d1a077 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -492,6 +492,11 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u ESP_LOGE(TAG, "Too many requests queued"); return false; } + if (length > trq->transfer->data_buffer_size) { + ESP_LOGE(TAG, "transfer_in: data length %u exceeds buffer size %u", length, trq->transfer->data_buffer_size); + this->release_trq(trq); + return false; + } trq->callback = callback; trq->transfer->callback = transfer_callback; trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN; From 7c1b9f0cb4b7862764ace94d4ccb2119b1024419 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:22:06 -0400 Subject: [PATCH 0521/2056] [multiple] Fix wrong behavior in 5 components (#14647) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/anova/anova.cpp | 9 ++++++--- esphome/components/binary_sensor/filter.cpp | 1 - esphome/components/bl0906/bl0906.cpp | 8 +++----- .../components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- esphome/components/ledc/ledc_output.cpp | 3 +++ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index b625f92115d..f21230b075e 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -144,9 +144,12 @@ void Anova::update() { return; if (this->current_request_ < 2) { - auto *pkt = this->codec_->get_read_device_status_request(); - if (this->current_request_ == 0) - this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + AnovaPacket *pkt; + if (this->current_request_ == 0) { + pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + } else { + pkt = this->codec_->get_read_device_status_request(); + } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 25a69c413a6..5d525e967db 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -136,7 +136,6 @@ optional<bool> SettleFilter::new_value(bool value) { return {}; } else { this->steady_ = false; - this->output(value); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index 7b643bba98a..dcae4a25913 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -190,11 +190,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct float i_rms0 = measurements * ki; float i_rms = correction * ki; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; - data.l = value << 24 >> 24; - data.m = value << 16 >> 24; - if (value < 0) { - data.h = (value << 8 >> 24) | 0b10000000; - } + data.l = value & 0xFF; + data.m = (value >> 8) & 0xFF; + data.h = (value >> 16) & 0xFF; data.address = bl0906_checksum(address, &data); ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); this->write_byte(BL0906_WRITE_COMMAND); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0e2a515b40f..5a43cf7e49b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -549,7 +549,7 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { // CSS 1.5 TX POWER LEVEL // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." // CSS 1: Optional in this context (may appear more than once in a block). - this->tx_powers_.push_back(*payload); + this->tx_powers_.push_back(*record); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 21e06822575..763de851da3 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -76,6 +76,9 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n init_result = ledc_timer_config(&timer_conf); if (init_result != ESP_OK) { ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth); + if (bit_depth <= 1) { + break; + } // try again with a lower bit depth timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(--bit_depth); } From 9902447834e8b997bf8831b569484f0c164c8f33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:51:50 -0400 Subject: [PATCH 0522/2056] [multiple] Fix minor bugs in 8 components (#14650) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bl0906/bl0906.cpp | 35 ++++++++++--------- esphome/components/bmi160/bmi160.cpp | 3 +- .../components/esp32_camera/esp32_camera.cpp | 7 ++++ esphome/components/ld2450/ld2450.cpp | 2 +- esphome/components/ld2450/ld2450.h | 2 +- .../light/addressable_light_effect.h | 2 ++ .../mopeka_std_check/mopeka_std_check.cpp | 18 +++++----- .../mopeka_std_check/mopeka_std_check.h | 2 +- esphome/components/rtttl/rtttl.cpp | 13 ++++--- 9 files changed, 49 insertions(+), 35 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index dcae4a25913..70db235a375 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -138,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se this->write_byte(BL0906_READ_COMMAND); this->write_byte(address); - if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { - if (bl0906_checksum(address, &buffer) == buffer.checksum) { - if (signed_result) { - data_s24.l = buffer.l; - data_s24.m = buffer.m; - data_s24.h = buffer.h; - } else { - data_u24.l = buffer.l; - data_u24.m = buffer.m; - data_u24.h = buffer.h; - } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; - return; - } + if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + ESP_LOGW(TAG, "Read failed"); + return; + } + if (bl0906_checksum(address, &buffer) != buffer.checksum) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; } // Power if (reference == BL0906_PREF) { diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 1e8c91d7b79..ed92979d24e 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -6,6 +6,7 @@ namespace esphome { namespace bmi160 { static const char *const TAG = "bmi160"; +static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100; const uint8_t BMI160_REGISTER_CHIPID = 0x00; @@ -144,7 +145,7 @@ void BMI160Component::internal_setup_(int stage) { } ESP_LOGV(TAG, " Waiting for gyroscope to wake up"); // wait between 51 & 81ms, doing 100 to be safe - this->set_timeout(10, [this]() { this->internal_setup_(2); }); + this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); }); break; case 2: diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 655ae54f0a6..085feb8c8a7 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -146,6 +146,10 @@ void ESP32Camera::dump_config() { } sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + ESP_LOGE(TAG, " Camera sensor not available"); + return; + } auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u\n" @@ -483,6 +487,9 @@ void ESP32Camera::request_image(camera::CameraRequester requester) { this->singl camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + return; + } /* update image */ s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index eb17cc7de70..f9701cbdf66 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -133,7 +133,7 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC}; // MAC address the module uses when Bluetooth is disabled static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; -static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; +static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; }; static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { for (uint8_t i = 0; i < 4; i++) { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 39b0ebd9da0..9409dfc21df 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -168,7 +168,7 @@ class LD2450Component : public Component, public uart::UARTDevice { uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; - uint16_t timeout_ = 5; + uint32_t timeout_ = 5; uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 461ddbc085a..283b037acad 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -324,6 +324,8 @@ class AddressableFireworksEffect : public AddressableLightEffect { target *= 170; view = target; } + if (it.size() < 2) + return; int last = it.size() - 1; it[0].set(it[0].get() + (it[1].get() * 128)); for (int i = 1; i < last; i++) { diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 88bd7b02fdb..a4a31b82608 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -126,18 +126,18 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Copy measurements over into my array. { u_int8_t measurements_index = 0; - for (u_int8_t i = 0; i < 3; i++) { - measurements_time[measurements_index] = mopeka_data->val[i].time_0 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_0; + for (const auto &val : mopeka_data->val) { + measurements_time[measurements_index] = val.time_0 + 1; + measurements_value[measurements_index] = val.value_0; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_1 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_1; + measurements_time[measurements_index] = val.time_1 + 1; + measurements_value[measurements_index] = val.value_1; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_2 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_2; + measurements_time[measurements_index] = val.time_2 + 1; + measurements_value[measurements_index] = val.value_2; measurements_index++; - measurements_time[measurements_index] = mopeka_data->val[i].time_3 + 1; - measurements_value[measurements_index] = mopeka_data->val[i].value_3; + measurements_time[measurements_index] = val.time_3 + 1; + measurements_value[measurements_index] = val.value_3; measurements_index++; } } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 45588988c53..c0a02f27f20 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -40,7 +40,7 @@ struct mopeka_std_package { // NOLINT(readability-identifier-naming,altera-stru bool slow_update_rate : 1; bool sync_pressed : 1; - mopeka_std_values val[4]; + mopeka_std_values val[3]; } __attribute__((packed)); class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 9bf0450993c..01f5aad8109 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -146,16 +146,19 @@ void Rtttl::loop() { } #endif // USE_SPEAKER + // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case + while (this->position_ < this->rtttl_.length()) { + char c = this->rtttl_[this->position_]; + if (c != ',' && c != ' ') + break; + this->position_++; + } + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } - // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case - while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') { - this->position_++; - } - // First, get note duration, if available uint8_t note_denominator = this->get_integer_(); From 470d9160a512b41042710bf4eb48455dbbd17007 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:57:02 -0400 Subject: [PATCH 0523/2056] [demo] Fix alarm control panel auth bypass when code is omitted (#14645) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/demo/demo_alarm_control_panel.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 76cb24c2f4a..9976e5c7f06 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -32,8 +32,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code_to_arm()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -41,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && code.has_value()) { - if (*code != "1234") { + if (this->get_requires_code()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } From 308e8e78cd0fb549b64b1ed16a30dcd1be26c9e3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:59:36 -0400 Subject: [PATCH 0524/2056] [ble_scanner] Escape special characters in JSON output (#14664) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/ble_scanner/ble_scanner.h | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 7061b6d3365..c6d7f24ccec 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -16,12 +16,27 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - // Format JSON using stack buffer to avoid heap allocations from string concatenation - char buf[128]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Escape special characters in the device name for valid JSON + const char *name = device.get_name().c_str(); + char escaped_name[128]; + size_t pos = 0; + for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) { + uint8_t c = static_cast<uint8_t>(*name); + if (c == '"' || c == '\\') { + escaped_name[pos++] = '\\'; + escaped_name[pos++] = c; + } else if (c < 0x20) { + pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c); + } else { + escaped_name[pos++] = c; + } + } + escaped_name[pos] = '\0'; + + char buf[256]; snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", - static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), - device.get_name().c_str()); + static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name); this->publish_state(buf); return true; } From b3fc43c13c5bee4b60c0bae0dbc1b244e4f4c60c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:00:17 -0400 Subject: [PATCH 0525/2056] [multiple] Fix wrong behavior in sensor calculations and drivers (#14644) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/bme280_base/bme280_base.cpp | 7 +++++-- esphome/components/bme680/bme680.h | 4 ++-- esphome/components/cse7766/cse7766.cpp | 9 +++++---- esphome/components/hitachi_ac344/hitachi_ac344.h | 2 +- esphome/components/rx8130/rx8130.cpp | 4 ++-- esphome/components/usb_uart/cp210x.cpp | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index f396888fd19..addbfe618d8 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -147,8 +147,11 @@ void BME280Component::setup() { this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); - this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); - this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + // h4 and h5 are signed 12-bit values; shift left then arithmetic right shift to sign-extend + int16_t h4_raw = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); + this->calibration_.h4 = static_cast<int16_t>(h4_raw << 4) >> 4; + int16_t h5_raw = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + this->calibration_.h5 = static_cast<int16_t>(h5_raw << 4) >> 4; this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); uint8_t humid_control_val = 0; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index d48a42823b2..239823fa8c9 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -32,8 +32,8 @@ enum BME680Oversampling { /// Struct for storing calibration data for the BME680. struct BME680CalibrationData { uint16_t t1; - uint16_t t2; - uint8_t t3; + int16_t t2; + int8_t t3; uint16_t p1; int16_t p2; diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 806b79e19e0..ce77b62b7b4 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cmath> namespace esphome::cse7766 { @@ -192,12 +193,12 @@ void CSE7766Component::parse_data_() { this->apparent_power_sensor_->publish_state(apparent_power); } if (have_power && this->reactive_power_sensor_ != nullptr) { - const float reactive_power = apparent_power - power; - if (reactive_power < 0.0f) { - ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + const float q_squared = apparent_power * apparent_power - power * power; + if (q_squared < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared); this->reactive_power_sensor_->publish_state(0.0f); } else { - this->reactive_power_sensor_->publish_state(reactive_power); + this->reactive_power_sensor_->publish_state(std::sqrt(q_squared)); } } if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index c34f033d926..0877b832617 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -96,7 +96,7 @@ class HitachiClimate : public climate_ir::ClimateIR { void set_power_(bool on); uint8_t get_mode_(); void set_mode_(uint8_t mode); - void set_temp_(uint8_t celsius, bool set_previous = false); + void set_temp_(uint8_t celsius, bool set_previous = true); uint8_t get_fan_(); void set_fan_(uint8_t speed); void set_swing_v_toggle_(bool on); diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index ba092a48340..9e6f05ee15c 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -75,7 +75,7 @@ void RX8130Component::read_time() { .second = bcd2dec(date[0] & 0x7f), .minute = bcd2dec(date[1] & 0x7f), .hour = bcd2dec(date[2] & 0x3f), - .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_week = static_cast<uint8_t>((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), @@ -103,7 +103,7 @@ void RX8130Component::write_time() { buff[0] = dec2bcd(now.second); buff[1] = dec2bcd(now.minute); buff[2] = dec2bcd(now.hour); - buff[3] = dec2bcd(now.day_of_week); + buff[3] = 1 << (now.day_of_week - 1); buff[4] = dec2bcd(now.day_of_month); buff[5] = dec2bcd(now.month); buff[6] = dec2bcd(now.year % 100); diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index ae9170c5fbd..261f40c0dbb 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -65,7 +65,7 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev } for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) { - auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset); + auto data_desc = usb_parse_interface_descriptor(config_desc, i, 0, &conf_offset); if (!data_desc) { ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed"); break; From 468ce74c8e2822a87c5367f22ca2c8732732e940 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 9 Mar 2026 17:04:47 -0500 Subject: [PATCH 0526/2056] [api][serial_proxy] Fix dangling pointer (#14640) --- esphome/components/api/api_connection.cpp | 12 ++++++++++++ esphome/components/serial_proxy/serial_proxy.h | 3 +++ 2 files changed, 15 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 28a770a4fb2..7bd5d5120b8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -155,6 +155,18 @@ APIConnection::~APIConnection() { voice_assistant::global_voice_assistant->client_subscription(this, false); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) { + zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } +#endif +#ifdef USE_SERIAL_PROXY + for (auto *proxy : App.get_serial_proxies()) { + if (proxy->get_api_connection() == this) { + proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } + } +#endif } void APIConnection::destroy_active_iterator_() { diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 52f0654ff0c..62f942b19d8 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -74,6 +74,9 @@ class SerialProxy : public uart::UARTDevice, public Component { /// @param data_size Number of data bits (5-8) void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size); + /// Get the currently subscribed API connection (nullptr if none) + api::APIConnection *get_api_connection() { return this->api_connection_; } + /// Handle a subscribe/unsubscribe request from an API client void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); From d2686b49bef3fdf1d56f7f222e1532e8c25d4380 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:15:33 -0400 Subject: [PATCH 0527/2056] [canbus] Fix multiple MCP component bugs (#14461) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/canbus/canbus.cpp | 3 ++- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 4 ++-- esphome/components/mcp23x17_base/mcp23x17_base.cpp | 4 ++-- esphome/components/mcp2515/mcp2515.cpp | 1 + esphome/components/mcp4461/mcp4461.cpp | 12 ++++++------ esphome/components/mcp4728/mcp4728.h | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index e208b0fd66f..ce48bfbba51 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -1,4 +1,5 @@ #include "canbus.h" +#include <algorithm> #include "esphome/core/log.h" namespace esphome { @@ -82,7 +83,7 @@ void Canbus::loop() { std::vector<uint8_t> data; // show data received - for (int i = 0; i < can_message.can_data_length_code; i++) { + for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) { ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); data.push_back(can_message.data[i]); } diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 1593c376cda..92228be62c9 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -47,12 +47,12 @@ void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index b1f1f260b4e..6f95ee98fdb 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -59,12 +59,12 @@ void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterr case mcp23xxx_base::MCP23XXX_RISING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, true, defval); + this->update_reg(pin, false, defval); break; case mcp23xxx_base::MCP23XXX_FALLING: this->update_reg(pin, true, gpinten); this->update_reg(pin, true, intcon); - this->update_reg(pin, false, defval); + this->update_reg(pin, true, defval); break; case mcp23xxx_base::MCP23XXX_NO_INTERRUPT: this->update_reg(pin, false, gpinten); diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index 77bfaf92246..c2db9228c8b 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -506,6 +506,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo cfg3 = MCP_12MHZ_40KBPS_CFG3; break; case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_12MHZ_50KBPS_CFG1; cfg2 = MCP_12MHZ_50KBPS_CFG2; cfg3 = MCP_12MHZ_50KBPS_CFG3; break; diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index dc7e7019aa0..48d90377df8 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -79,8 +79,8 @@ void Mcp4461Component::dump_config() { // reworked to be a one-line intentionally, as output would not be in order if (i < 4) { ESP_LOGCONFIG(TAG, " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, A: %s, B: %s, W: %s", i, - this->reg_[i].state, ONOFF(this->reg_[i].terminal_hw), ONOFF(this->reg_[i].terminal_a), - ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w), ONOFF(this->reg_[i].enabled)); + this->reg_[i].state, ONOFF(this->reg_[i].enabled), ONOFF(this->reg_[i].terminal_hw), + ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w)); } else { ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state); } @@ -315,9 +315,9 @@ void Mcp4461Component::disable_wiper_(Mcp4461WiperIdx wiper) { return; } ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx); - this->reg_[wiper_idx].enabled = true; + this->reg_[wiper_idx].enabled = false; if (wiper_idx < 4) { - this->reg_[wiper_idx].terminal_hw = true; + this->reg_[wiper_idx].terminal_hw = false; this->reg_[wiper_idx].update_terminal = true; } } @@ -490,7 +490,7 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { @@ -517,7 +517,7 @@ void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); return; } - this->reg_[wiper_idx].update_terminal = false; + this->reg_[wiper_idx].update_terminal = true; } uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { diff --git a/esphome/components/mcp4728/mcp4728.h b/esphome/components/mcp4728/mcp4728.h index f2262f4a358..d6574080810 100644 --- a/esphome/components/mcp4728/mcp4728.h +++ b/esphome/components/mcp4728/mcp4728.h @@ -58,7 +58,7 @@ class MCP4728Component : public Component, public i2c::I2CDevice { void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); private: - DACInputData reg_[4]; + DACInputData reg_[4]{}; bool store_in_eeprom_ = false; bool update_ = false; }; From d96be88ff58d3dac9454364f6da24ff4ff0e3561 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:32:57 -0400 Subject: [PATCH 0528/2056] [multiple] Fix reliability issues in 5 components (#14655) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/bme680_bsec/bme680_bsec.cpp | 8 +++++++- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 16 ++++++++++++++++ esphome/components/lvgl/lvgl_esphome.cpp | 6 ++++++ esphome/components/mqtt/mqtt_client.cpp | 4 +++- esphome/components/usb_uart/usb_uart.cpp | 5 +++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 454be0c0fe4..75efb6835ab 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -271,10 +271,16 @@ void BME680BSECComponent::read_() { int64_t curr_time_ns = this->get_time_ns_(); if (this->bme680_settings_.trigger_measurement) { + uint32_t start = millis(); while (this->bme680_.power_mode != BME680_SLEEP_MODE) { + if (millis() - start > 50) { + ESP_LOGE(TAG, "Timeout waiting for BME680 to enter sleep mode"); + return; + } this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); if (this->bme680_status_ != BME680_OK) { - ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + ESP_LOGE(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; } } } diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 7c7c8782dee..7a0dc0690cc 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -6,6 +6,7 @@ namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; +static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20; void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); @@ -133,6 +134,21 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; + // Wait for remaining data (payload + checksum) to arrive. + // Header bytes are already consumed, so we must finish reading this message. + uint32_t start = millis(); + while (this->available() < length + 1) { + if (millis() - start > PAYLOAD_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length); + // Drain any partial payload bytes to resync the parser + while (this->available() > 0) { + this->read(); + } + return; + } + delay(1); + } + // Read up to buffer size; discard excess bytes while still computing checksum // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) // but handlers only need the first few bytes diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 66cb25b864b..5400054bb13 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -163,8 +163,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } @@ -172,8 +175,11 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; + size_t start = this->current_page_; do { this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + if (this->current_page_ == start) + return; // all pages have skip=true (guaranteed not to happen by YAML validation) } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1a03c5329ee..38daf8f8f6f 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -44,8 +44,10 @@ MQTTClientComponent::MQTTClientComponent() { void MQTTClientComponent::setup() { this->mqtt_backend_.set_on_message( [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { - if (index == 0) + if (index == 0) { + this->payload_buffer_.clear(); this->payload_buffer_.reserve(total); + } // append new payload, may contain incomplete MQTT message this->payload_buffer_.append(payload, len); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 83de0b39fcc..3d35f368fb8 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -106,10 +106,15 @@ std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev } void RingBuffer::push(uint8_t item) { + if (this->get_free_space() == 0) + return; this->buffer_[this->insert_pos_] = item; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; } void RingBuffer::push(const uint8_t *data, size_t len) { + size_t free = this->get_free_space(); + if (len > free) + len = free; for (size_t i = 0; i != len; i++) { this->buffer_[this->insert_pos_] = *data++; this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; From dadbdd0f7b2081031d395a2778f92706ab98647a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 12:34:31 -1000 Subject: [PATCH 0529/2056] [ci] Make codeowner label update non-fatal for fork PRs (#14668) --- .../codeowner-approved-label-update.yml | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index c2eb886913e..0bce33ebe29 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -55,18 +55,24 @@ jobs: return; } - if (action === LabelAction.ADD) { - await github.rest.issues.addLabels({ - owner, repo, issue_number: pr_number, labels: [LABEL_NAME] - }); - console.log(`Added '${LABEL_NAME}' label`); - } else if (action === LabelAction.REMOVE) { - try { + try { + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr_number, name: LABEL_NAME }); console.log(`Removed '${LABEL_NAME}' label`); - } catch (error) { - if (error.status !== 404) throw error; + } + } catch (error) { + if (error.status === 403) { + console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`); + } else if (error.status === 404) { + console.log(`Label '${LABEL_NAME}' not present, nothing to remove`); + } else { + throw error; } } From d6ce5dda81d6fdf966392e80b68a8c8cac0a3192 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 12:54:56 -1000 Subject: [PATCH 0530/2056] [ci] Skip YAML anchor keys in integration fixture component extraction (#14670) --- script/helpers.py | 6 ++++-- tests/script/test_helpers.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 202ac9b5fc7..d372d2a7ec4 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -705,8 +705,10 @@ def get_components_from_integration_fixtures() -> set[str]: if not config: continue - # Add all top-level component keys - components.update(config.keys()) + # Add all top-level component keys (skip YAML anchor keys starting with '.') + components.update( + k for k in config if isinstance(k, str) and not k.startswith(".") + ) # Add platform components (e.g., output.template) for value in config.values(): diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 7e60ba41fcd..2953a9fd428 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1057,6 +1057,30 @@ def test_get_components_from_integration_fixtures() -> None: assert components == expected_components +def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None: + """Test that YAML anchor keys (starting with '.') are excluded.""" + yaml_content = { + "sensor": [{"platform": "template", "name": "test"}], + "esphome": {"name": "test"}, + ".sensor_filters": {"filters": [{"timeout": "50ms"}]}, + ".binary_filters": {"filters": [{"settle": "50ms"}]}, + } + + mock_yaml_file = Mock() + + with ( + patch("pathlib.Path.glob") as mock_glob, + patch("esphome.yaml_util.load_yaml", return_value=yaml_content), + ): + mock_glob.return_value = [mock_yaml_file] + + components = helpers.get_components_from_integration_fixtures() + + assert ".sensor_filters" not in components + assert ".binary_filters" not in components + assert components == {"sensor", "esphome", "template"} + + @pytest.mark.parametrize( "output,expected", [ From c31ac662bd08cdb99859819dc92a8a9295fb828c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:39:58 -0400 Subject: [PATCH 0531/2056] [multiple] Fix crashes from malformed external input (#14643) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/b_parasite/b_parasite.cpp | 10 ++ .../components/kamstrup_kmp/kamstrup_kmp.cpp | 12 +- esphome/components/ld2412/ld2412.cpp | 39 ++-- esphome/components/nextion/nextion.cpp | 4 +- esphome/components/pipsolar/pipsolar.cpp | 9 +- esphome/components/smt100/smt100.cpp | 25 ++- .../uart_mock_ld2412_engineering.yaml | 93 +++------- ...art_mock_ld2412_engineering_truncated.yaml | 167 ++++++++++++++++++ tests/integration/test_uart_mock_ld2412.py | 125 +++++++++++++ 9 files changed, 390 insertions(+), 94 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 356f3964766..7be26efa7f2 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; + if (data.size() < 10) { + ESP_LOGW(TAG, "Service data too short: %zu", data.size()); + return false; + } + const uint8_t protocol_version = data[0] >> 4; if (protocol_version != 1 && protocol_version != 2) { ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); @@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Some b-parasite versions have an (optional) illuminance sensor. bool has_illuminance = data[0] & 0x1; + if (has_illuminance && data.size() < 18) { + ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size()); + return false; + } + // Counter for deduplicating messages. uint8_t counter = data[1] & 0x0f; if (last_processed_counter_ == counter) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 29de6512559..9f2557243c8 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -110,9 +110,17 @@ void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) { for (int i = 0; i < buffer_len; i++) { if (buffer[i] == 0x06 || buffer[i] == 0x0d || buffer[i] == 0x1b || buffer[i] == 0x40 || buffer[i] == 0x80) { + if (tx_msg_len + 2 >= static_cast<int>(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = 0x1b; tx_msg[tx_msg_len++] = buffer[i] ^ 0xff; } else { + if (tx_msg_len + 1 >= static_cast<int>(sizeof(tx_msg))) { + ESP_LOGE(TAG, "TX message overflow"); + return; + } tx_msg[tx_msg_len++] = buffer[i]; } } @@ -216,8 +224,8 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_ uint8_t unit_idx = msg[4]; uint8_t mantissa_range = msg[5]; - if (mantissa_range > 4) { - ESP_LOGE(TAG, "Received invalid message (mantissa size too large %d, expected 4)", mantissa_range); + if (mantissa_range > 4 || msg_len < 7 + mantissa_range) { + ESP_LOGE(TAG, "Received invalid message (mantissa size %d, msg_len %d)", mantissa_range, msg_len); return; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index ef0915d0bc0..37578dd8daf 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -413,24 +413,29 @@ void LD2412Component::handle_periodic_data_() { this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); } if (engineering_mode) { - /* - Moving distance range: 18th byte - Still distance range: 19th byte - Moving energy: 20~28th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + // Engineering mode needs at least LIGHT_SENSOR + 1 bytes + if (this->buffer_pos_ < LIGHT_SENSOR + 1) { + ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_); + } else { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor value + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } - /* - Still energy: 29~37th bytes - */ - for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) - } - /* - Light sensor: 38th bytes - */ - SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } else { for (auto &gate_move_sensor : this->gate_move_sensors_) { SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 7ae4d50fc80..01ceb3d765e 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -646,8 +646,8 @@ void Nextion::process_nextion_commands_() { break; } - if (to_process_length == 0) { - ESP_LOGE(TAG, "Numeric return but no data"); + if (to_process_length < 4) { + ESP_LOGE(TAG, "Numeric return but insufficient data (need 4, got %zu)", to_process_length); break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index eb6d3931e05..c304d206c00 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -192,8 +192,13 @@ bool Pipsolar::send_next_command_() { if (!this->command_queue_[this->command_queue_position_].empty()) { const char *command = this->command_queue_[this->command_queue_position_].c_str(); uint8_t byte_command[16]; - uint8_t length = this->command_queue_[this->command_queue_position_].length(); - for (uint8_t i = 0; i < length; i++) { + size_t length = this->command_queue_[this->command_queue_position_].length(); + if (length > sizeof(byte_command)) { + ESP_LOGE(TAG, "Command too long: %zu", length); + this->command_queue_[this->command_queue_position_].clear(); + return false; + } + for (size_t i = 0; i < length; i++) { byte_command[i] = (uint8_t) this->command_queue_[this->command_queue_position_].at(i); } this->state_ = STATE_COMMAND; diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 105cc06edbf..6eb6416447f 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -14,11 +14,26 @@ void SMT100Component::update() { void SMT100Component::loop() { while (this->available() != 0) { if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); - float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); - float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); - float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); - float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); + char *token = strtok(this->readline_buffer_, ","); + if (!token) + continue; + int counts = (int) strtol(token, nullptr, 10); + token = strtok(nullptr, ","); + if (!token) + continue; + float permittivity = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float moisture = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float temperature = (float) strtod(token, nullptr); + token = strtok(nullptr, ","); + if (!token) + continue; + float voltage = (float) strtod(token, nullptr); if (this->counts_sensor_ != nullptr) { counts_sensor_->publish_state(counts); diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index 103dbed132f..a69e18888e4 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -102,6 +102,18 @@ uart_mock: 0xF8, 0xF7, 0xF6, 0xF5, ] +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + ld2412: id: ld2412_dev uart_id: mock_uart @@ -111,107 +123,56 @@ sensor: ld2412_id: ld2412_dev moving_distance: name: "Moving Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_distance: name: "Still Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters moving_energy: name: "Moving Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters detection_distance: name: "Detection Distance" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters light: name: "Light" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_0: move_energy: name: "Gate 0 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 0 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_1: move_energy: name: "Gate 1 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 1 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters gate_2: move_energy: name: "Gate 2 Move Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters still_energy: name: "Gate 2 Still Energy" - filters: - - timeout: - timeout: 50ms - value: last - - throttle_with_priority: 50ms + <<: *sensor_filters binary_sensor: - platform: ld2412 ld2412_id: ld2412_dev has_target: name: "Has Target" - filters: - - settle: 50ms + <<: *binary_filters has_moving_target: name: "Has Moving Target" - filters: - - settle: 50ms + <<: *binary_filters has_still_target: name: "Has Still Target" - filters: - - settle: 50ms + <<: *binary_filters button: - platform: template diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml new file mode 100644 index 00000000000..c0bd5147629 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -0,0 +1,167 @@ +esphome: + name: uart-mock-ld2412-eng-trunc + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy ld2412's DEPENDENCIES = ["uart"] +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 256000 + auto_start: false + injections: + # Phase 1 (t=100ms): Valid engineering mode frame (52 bytes, buffer_pos_=52) + # Establishes baseline: gate_0_move=100, light=87 + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x64, 0x41, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x50, 0x40, 0x30, 0x20, 0x10, + 0x57, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 2 (t=200ms): Truncated engineering mode frame (24 bytes, buffer_pos_=24) + # This frame has data_type=0x01 (engineering) but only enough data for the + # basic target fields, not the gate energies or light sensor. + # buffer_pos_=24 passes the old check (>= 12) but fails the new check (< 46). + # Without the fix, indices 17-45 would read stale buffer data from Phase 1. + # + # Layout (24 bytes): + # [0-3] F4 F3 F2 F1 = data frame header + # [4-5] 0E 00 = length 14 + # [6] 01 = data type (engineering mode) + # [7] AA = data header marker + # [8] 03 = target states (moving+still) + # [9-10] 1E 00 = moving distance 30 + # [11] 50 = moving energy 80 + # [12-13] 1E 00 = still distance 30 + # [14] 50 = still energy 80 + # [15-16] FF FF = garbage detection distance bytes + # [17] FF = padding (would be gate data in full frame) + # [18] 55 = data footer marker (at buffer_pos_ - 6) + # [19] 00 = check byte + # [20-23] F8 F7 F6 F5 = data frame footer + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x0E, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x50, + 0x1E, 0x00, + 0x50, + 0xFF, 0xFF, + 0xFF, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + + # Phase 3 (t=300ms): Valid recovery frame with different values + # gate_0_move=50, light=42 — proves component recovered + - delay: 100ms + inject_rx: + [ + 0xF4, 0xF3, 0xF2, 0xF1, + 0x2A, 0x00, + 0x01, 0xAA, + 0x03, + 0x1E, 0x00, + 0x64, + 0x1E, 0x00, + 0x64, + 0x00, 0x00, + 0x32, 0x20, 0x06, 0x0E, 0x2B, 0x16, 0x03, 0x03, 0x07, 0x05, 0x09, 0x08, 0x07, 0x06, + 0x00, 0x00, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x28, 0x20, 0x18, 0x10, 0x08, + 0x2A, + 0x55, 0x00, + 0xF8, 0xF7, 0xF6, 0xF5, + ] + +# Common filter definitions +.sensor_filters: &sensor_filters + filters: + - timeout: + timeout: 50ms + value: last + - throttle_with_priority: 50ms + +.binary_filters: &binary_filters + filters: + - settle: 50ms + +ld2412: + id: ld2412_dev + uart_id: mock_uart + +sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + moving_distance: + name: "Moving Distance" + <<: *sensor_filters + still_distance: + name: "Still Distance" + <<: *sensor_filters + moving_energy: + name: "Moving Energy" + <<: *sensor_filters + still_energy: + name: "Still Energy" + <<: *sensor_filters + detection_distance: + name: "Detection Distance" + <<: *sensor_filters + light: + name: "Light" + <<: *sensor_filters + gate_0: + move_energy: + name: "Gate 0 Move Energy" + <<: *sensor_filters + still_energy: + name: "Gate 0 Still Energy" + <<: *sensor_filters + +binary_sensor: + - platform: ld2412 + ld2412_id: ld2412_dev + has_target: + name: "Has Target" + <<: *binary_filters + has_moving_target: + name: "Has Moving Target" + <<: *binary_filters + has_still_target: + name: "Has Still Target" + <<: *binary_filters + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: 'id(mock_uart).start_scenario();' diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index a964ba00738..9b928ef14f2 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -14,6 +14,12 @@ test_uart_mock_ld2412_engineering (engineering mode): 2. Multi-byte still distance (291cm) using high byte > 0 3. Gate energy sensor values 4. Detection distance computed from target state + +test_uart_mock_ld2412_engineering_truncated (truncated engineering mode): + 1. Valid engineering frame establishes baseline sensor values + 2. Truncated engineering frame (24 bytes) is rejected — gate/light sensors + must not receive garbage from stale buffer data or frame footer bytes + 3. Recovery frame with different values proves the component survived """ from __future__ import annotations @@ -273,3 +279,122 @@ async def test_uart_mock_ld2412_engineering( ) assert pytest.approx(291.0) in collector.sensor_states["detection_distance"] + + +@pytest.mark.asyncio +async def test_uart_mock_ld2412_engineering_truncated( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that truncated engineering mode frames don't corrupt sensor values. + + Without the fix, a 24-byte engineering mode frame passes the old buffer_pos_ >= 12 + check but reads indices 17-45 from stale buffer data, publishing garbage values + (e.g. frame footer bytes 0xF8=248 as gate energy). + """ + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track the truncated frame warning + truncated_warning_seen = loop.create_future() + + def line_callback(line: str) -> None: + if ( + "Engineering mode packet too short" in line + and not truncated_warning_seen.done() + ): + truncated_warning_seen.set_result(True) + + collector = SensorStateCollector( + sensor_names=[ + "moving_distance", + "still_distance", + "moving_energy", + "still_energy", + "detection_distance", + "light", + "gate_0_move_energy", + "gate_0_still_energy", + ], + binary_sensor_names=[ + "has_target", + "has_moving_target", + "has_still_target", + ], + ) + + # Signal when we see Phase 3 recovery values (gate_0_move=50) + recovery_received = collector.add_waiter( + lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + + # Wait for Phase 1 — valid engineering frame establishes baseline + try: + await collector.wait_for_all(timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for Phase 1 frame. Received:\n" + f" sensor_states: {collector.sensor_states}\n" + f" binary_states: {collector.binary_states}" + ) + + # Phase 1 baseline: gate_0_move=100, light=87 + assert collector.sensor_states["gate_0_move_energy"][0] == pytest.approx(100.0) + assert collector.sensor_states["light"][0] == pytest.approx(87.0) + + # Wait for Phase 3 recovery frame (gate_0_move=50) + try: + await asyncio.wait_for(recovery_received, timeout=3.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for recovery frame. Received:\n" + f" gate_0_move_energy: {collector.sensor_states['gate_0_move_energy']}\n" + f" light: {collector.sensor_states['light']}" + ) + + # Verify the truncated frame warning was logged + assert truncated_warning_seen.done(), ( + "Expected 'Engineering mode packet too short' warning in logs" + ) + + # Phase 3 recovery: gate_0_move=50, light=42 + assert pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + assert pytest.approx(42.0) in collector.sensor_states["light"] + + # The critical assertion: gate_0_move_energy must never have received + # garbage values from the truncated frame. Without the fix, + # buffer_data_[17] = 0xFF = 255 would be published as gate_0_move. + for value in collector.sensor_states["gate_0_move_energy"]: + assert value == pytest.approx(100.0) or value == pytest.approx(50.0), ( + f"gate_0_move_energy got unexpected value {value} — " + f"truncated frame likely leaked stale buffer data. " + f"All values: {collector.sensor_states['gate_0_move_energy']}" + ) From 00f809f5f001a1428f099f3e0846d2c5d848cdf3 Mon Sep 17 00:00:00 2001 From: Tobias Stanzel <tobi.stanzel@gmail.com> Date: Tue, 10 Mar 2026 02:45:20 +0100 Subject: [PATCH 0532/2056] [sen6x] fix memory leak issue (#14623) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sen6x/sen6x.cpp | 338 ++++++++++++++++------------- esphome/components/sen6x/sen6x.h | 6 + 2 files changed, 188 insertions(+), 156 deletions(-) diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp index baaadd64631..2a6ea64735d 100644 --- a/esphome/components/sen6x/sen6x.cpp +++ b/esphome/components/sen6x/sen6x.cpp @@ -2,13 +2,16 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include <cmath> -#include <functional> -#include <memory> namespace esphome::sen6x { static const char *const TAG = "sen6x"; +static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts +static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete +static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts +// Single numeric timeout ID — the chain is sequential so only one is active at a time. +static constexpr uint32_t TIMEOUT_POLL = 1; static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202; static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100; static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014; @@ -182,179 +185,202 @@ void SEN6XComponent::update() { return; } - uint16_t read_cmd; - uint8_t read_words; - set_read_command_and_words(this->sen6x_type_, read_cmd, read_words); + // Cancel any in-flight polling from a previous update() cycle. + this->cancel_timeout(TIMEOUT_POLL); - const uint8_t poll_retries = 24; - auto poll_ready = std::make_shared<std::function<void(uint8_t)>>(); - *poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) { - const uint8_t attempt = static_cast<uint8_t>(poll_retries - retries_left + 1); - ESP_LOGV(TAG, "Data ready polling attempt %u", attempt); + set_read_command_and_words(this->sen6x_type_, this->read_cmd_, this->read_words_); - if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + // Polling uses chained timeouts to guarantee each I2C operation completes + // before the next begins. The flow is: + // + // poll_data_ready_() + // -> write_command (data ready status) + // -> timeout I2C_READ_DELAY + // -> read_data (check ready flag) + // -> if not ready: timeout POLL_INTERVAL -> poll_data_ready_() (retry) + // -> if ready: read_measurements_() + // -> write_command (read measurement) + // -> timeout I2C_READ_DELAY + // -> parse_and_publish_measurements_() + // + // All timeouts share a single ID (TIMEOUT_POLL) since only one is active + // at a time. cancel_timeout in update() stops any in-flight chain. + this->poll_retries_remaining_ = POLL_RETRIES; + this->poll_data_ready_(); +} + +void SEN6XComponent::poll_data_ready_() { + if (this->poll_retries_remaining_ == 0) { + this->status_set_warning(); + ESP_LOGD(TAG, "Data not ready"); + return; + } + ESP_LOGV(TAG, "Data ready polling attempt %u", + static_cast<unsigned>(POLL_RETRIES - this->poll_retries_remaining_ + 1)); + this->poll_retries_remaining_--; + + if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + return; + } + + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { + uint16_t raw_read_status; + if (!this->read_data(&raw_read_status, 1)) { this->status_set_warning(); - ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_); + ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); return; } - this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() { - uint16_t raw_read_status; - if (!this->read_data(&raw_read_status, 1)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_); - return; - } + if ((raw_read_status & 0x0001) == 0) { + // Not ready yet; schedule next attempt after POLL_INTERVAL. + this->set_timeout(TIMEOUT_POLL, POLL_INTERVAL, [this]() { this->poll_data_ready_(); }); + return; + } - if ((raw_read_status & 0x0001) == 0) { - if (retries_left == 0) { - this->status_set_warning(); - ESP_LOGD(TAG, "Data not ready"); - return; - } - this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); }); - return; - } + this->read_measurements_(); + }); +} - if (!this->write_command(read_cmd)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); - return; - } +void SEN6XComponent::read_measurements_() { + if (!this->write_command(this->read_cmd_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_); + return; + } - this->set_timeout(20, [this, read_words]() { - uint16_t measurements[10]; + this->set_timeout(TIMEOUT_POLL, I2C_READ_DELAY, [this]() { this->parse_and_publish_measurements_(); }); +} - if (!this->read_data(measurements, read_words)) { - this->status_set_warning(); - ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); - return; - } - int8_t voc_index = -1; - int8_t nox_index = -1; - int8_t hcho_index = -1; - int8_t co2_index = -1; - bool co2_uint16 = false; - switch (this->sen6x_type_) { - case SEN62: - break; - case SEN63C: - co2_index = 6; - break; - case SEN65: - voc_index = 6; - nox_index = 7; - break; - case SEN66: - voc_index = 6; - nox_index = 7; - co2_index = 8; - co2_uint16 = true; - break; - case SEN68: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - break; - case SEN69C: - voc_index = 6; - nox_index = 7; - hcho_index = 8; - co2_index = 9; - break; - default: - break; - } +void SEN6XComponent::parse_and_publish_measurements_() { + uint16_t measurements[10]; - float pm_1_0 = measurements[0] / 10.0f; - if (measurements[0] == 0xFFFF) - pm_1_0 = NAN; - float pm_2_5 = measurements[1] / 10.0f; - if (measurements[1] == 0xFFFF) - pm_2_5 = NAN; - float pm_4_0 = measurements[2] / 10.0f; - if (measurements[2] == 0xFFFF) - pm_4_0 = NAN; - float pm_10_0 = measurements[3] / 10.0f; - if (measurements[3] == 0xFFFF) - pm_10_0 = NAN; - float humidity = static_cast<int16_t>(measurements[4]) / 100.0f; - if (measurements[4] == 0x7FFF) - humidity = NAN; - float temperature = static_cast<int16_t>(measurements[5]) / 200.0f; - if (measurements[5] == 0x7FFF) - temperature = NAN; + if (!this->read_data(measurements, this->read_words_)) { + this->status_set_warning(); + ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_); + return; + } + int8_t voc_index = -1; + int8_t nox_index = -1; + int8_t hcho_index = -1; + int8_t co2_index = -1; + bool co2_uint16 = false; + switch (this->sen6x_type_) { + case SEN62: + break; + case SEN63C: + co2_index = 6; + break; + case SEN65: + voc_index = 6; + nox_index = 7; + break; + case SEN66: + voc_index = 6; + nox_index = 7; + co2_index = 8; + co2_uint16 = true; + break; + case SEN68: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + break; + case SEN69C: + voc_index = 6; + nox_index = 7; + hcho_index = 8; + co2_index = 9; + break; + default: + break; + } - float voc = NAN; - float nox = NAN; - float hcho = NAN; - float co2 = NAN; + float pm_1_0 = measurements[0] / 10.0f; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0f; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0f; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0f; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = static_cast<int16_t>(measurements[4]) / 100.0f; + if (measurements[4] == 0x7FFF) + humidity = NAN; + float temperature = static_cast<int16_t>(measurements[5]) / 200.0f; + if (measurements[5] == 0x7FFF) + temperature = NAN; - if (voc_index >= 0) { - voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f; - if (measurements[voc_index] == 0x7FFF) - voc = NAN; - } - if (nox_index >= 0) { - nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f; - if (measurements[nox_index] == 0x7FFF) - nox = NAN; - } + float voc = NAN; + float nox = NAN; + float hcho = NAN; + float co2 = NAN; - if (hcho_index >= 0) { - const uint16_t hcho_raw = measurements[hcho_index]; - hcho = hcho_raw / 10.0f; - if (hcho_raw == 0xFFFF) - hcho = NAN; - } + if (voc_index >= 0) { + voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f; + if (measurements[voc_index] == 0x7FFF) + voc = NAN; + } + if (nox_index >= 0) { + nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f; + if (measurements[nox_index] == 0x7FFF) + nox = NAN; + } - if (co2_index >= 0) { - if (co2_uint16) { - const uint16_t co2_raw = measurements[co2_index]; - co2 = static_cast<float>(co2_raw); - if (co2_raw == 0xFFFF) - co2 = NAN; - } else { - const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]); - co2 = static_cast<float>(co2_raw); - if (co2_raw == 0x7FFF) - co2 = NAN; - } - } + if (hcho_index >= 0) { + const uint16_t hcho_raw = measurements[hcho_index]; + hcho = hcho_raw / 10.0f; + if (hcho_raw == 0xFFFF) + hcho = NAN; + } - if (!this->startup_complete_) { - ESP_LOGD(TAG, "Startup delay, ignoring values"); - this->status_clear_warning(); - return; - } + if (co2_index >= 0) { + if (co2_uint16) { + const uint16_t co2_raw = measurements[co2_index]; + co2 = static_cast<float>(co2_raw); + if (co2_raw == 0xFFFF) + co2 = NAN; + } else { + const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]); + co2 = static_cast<float>(co2_raw); + if (co2_raw == 0x7FFF) + co2 = NAN; + } + } - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5); - if (this->pm_4_0_sensor_ != nullptr) - this->pm_4_0_sensor_->publish_state(pm_4_0); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->voc_sensor_ != nullptr) - this->voc_sensor_->publish_state(voc); - if (this->nox_sensor_ != nullptr) - this->nox_sensor_->publish_state(nox); - if (this->hcho_sensor_ != nullptr) - this->hcho_sensor_->publish_state(hcho); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(co2); + if (!this->startup_complete_) { + ESP_LOGD(TAG, "Startup delay, ignoring values"); + this->status_clear_warning(); + return; + } - this->status_clear_warning(); - }); - }); - }; + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + if (this->hcho_sensor_ != nullptr) + this->hcho_sensor_->publish_state(hcho); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); - (*poll_ready)(poll_retries); + this->status_clear_warning(); } SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) { diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h index 01e89dce1bc..bc44611882f 100644 --- a/esphome/components/sen6x/sen6x.h +++ b/esphome/components/sen6x/sen6x.h @@ -30,13 +30,19 @@ class SEN6XComponent : public PollingComponent, public sensirion_common::Sensiri protected: Sen6xType infer_type_from_product_name_(const std::string &product_name); + void poll_data_ready_(); + void read_measurements_(); + void parse_and_publish_measurements_(); bool initialized_{false}; std::string product_name_; Sen6xType sen6x_type_{UNKNOWN}; std::string serial_number_; + uint16_t read_cmd_{0}; uint8_t firmware_version_major_{0}; uint8_t firmware_version_minor_{0}; + uint8_t poll_retries_remaining_{0}; + uint8_t read_words_{0}; bool startup_complete_{false}; }; From e82f0f443223a4985bf2014fbf9b5f4130b2f4b8 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Tue, 10 Mar 2026 03:41:02 +0100 Subject: [PATCH 0533/2056] [cpptests] support testing platform components (#13075) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/core/__init__.py | 15 + esphome/loader.py | 5 + script/cpp_unit_test.py | 153 +++++++---- script/helpers.py | 15 +- tests/components/README.md | 13 + .../binary_sensor/binary_sensor_test.cpp | 77 ++++++ .../components/packet_transport/cpp_test.yaml | 11 - .../packet_transport_test.cpp | 259 ------------------ .../packet_transport/sensor/sensor_test.cpp | 170 ++++++++++++ tests/script/test_helpers.py | 67 +++++ tests/unit_tests/test_core.py | 12 + 11 files changed, 467 insertions(+), 330 deletions(-) create mode 100644 tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp delete mode 100644 tests/components/packet_transport/cpp_test.yaml create mode 100644 tests/components/packet_transport/sensor/sensor_test.cpp diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 484f6793696..a86478aca19 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -615,6 +615,10 @@ class EsphomeCore: self.address_cache: AddressCache | None = None # Cached config hash (computed lazily) self._config_hash: int | None = None + # True if compiling for C++ unit tests + self.cpp_testing = False + # Allowlist of components whose to_code should run during C++ testing + self.cpp_testing_codegen: set[str] = set() def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -644,6 +648,8 @@ class EsphomeCore: self.current_component = None self.address_cache = None self._config_hash = None + self.cpp_testing = False + self.cpp_testing_codegen = set() PIN_SCHEMA_REGISTRY.reset() @contextmanager @@ -987,6 +993,15 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def testing_ensure_platform_registered(self, platform_name: str) -> None: + """Ensure a platform has at least one entity registered for testing. + + Used during C++ test builds to guarantee USE_* defines are emitted + without needing a real component variable. + """ + if not self.platform_counts[platform_name]: + self.platform_counts[platform_name] = 1 + def register_controller(self) -> None: """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) diff --git a/esphome/loader.py b/esphome/loader.py index 968c8cf3e05..5771e074738 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -71,6 +71,11 @@ class ComponentManifest: @property def to_code(self) -> Callable[[Any], None] | None: + if CORE.cpp_testing: + # During C++ testing, only run to_code for allowlisted components + name = self.module.__package__.rsplit(".", 1)[-1] + if name not in CORE.cpp_testing_codegen: + return None return getattr(self.module, "to_code", None) @property diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index b87261ab332..e11687dc16d 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -10,9 +10,10 @@ from helpers import get_all_components, get_all_dependencies, root_path from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config +from esphome.const import CONF_PLATFORM from esphome.core import CORE +from esphome.loader import get_component from esphome.platformio_api import get_idedata -from esphome.yaml_util import load_yaml # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -20,6 +21,13 @@ PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" # Path to /tests/components COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" +# Components whose to_code should run during C++ test builds. +# Most components don't need code generation for tests; only these +# essential ones (platform setup, logging, core config) are needed. +# Note: "core" is the esphome core config module (esphome/core/config.py), +# which registers under package name "core" not "esphome". +CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"} + def hash_components(components: list[str]) -> str: key = ",".join(components) @@ -30,12 +38,14 @@ def filter_components_without_tests(components: list[str]) -> list[str]: """Filter out components that do not have a corresponding test file. This is done by checking if the component's directory contains at - least a .cpp file. + least a .cpp or .h file. """ filtered_components: list[str] = [] for component in components: test_dir = COMPONENTS_TESTS_DIR / component - if test_dir.is_dir() and any(test_dir.glob("*.cpp")): + if test_dir.is_dir() and ( + any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h")) + ): filtered_components.append(component) else: print( @@ -45,38 +55,6 @@ def filter_components_without_tests(components: list[str]) -> list[str]: return filtered_components -# Name of optional per-component YAML config merged into the test build -# before validation so that platform defines (USE_SENSOR, etc.) are generated. -CPP_TEST_CONFIG_FILE = "cpp_test.yaml" - - -def load_component_test_configs(components: list[str]) -> dict: - """Load cpp_test.yaml files from test component directories. - - These configs are merged into the base test config *before* validation - so that entity registration runs during code generation, which causes - the corresponding USE_* defines to be emitted. - """ - merged: dict = {} - for component in components: - config_file = COMPONENTS_TESTS_DIR / component / CPP_TEST_CONFIG_FILE - if not config_file.exists(): - continue - component_config = load_yaml(config_file) - if not component_config: - continue - for key, value in component_config.items(): - if ( - key in merged - and isinstance(merged[key], list) - and isinstance(value, list) - ): - merged[key].extend(value) - else: - merged[key] = value - return merged - - def create_test_config(config_name: str, includes: list[str]) -> dict: """Create ESPHome test configuration for C++ unit tests. @@ -113,11 +91,52 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: } +def get_platform_components(components: list[str]) -> list[str]: + """Discover platform sub-components referenced by test directory structure. + + For each component being tested, any sub-directory named after a platform + domain (e.g. ``sensor``, ``binary_sensor``) is treated as a request to + include that ``<domain>.<component>`` platform in the build. The sub- + directory must name a valid platform domain; anything else raises an error + so that typos are caught early. + + Returns: + List of ``"domain.component"`` strings, one per discovered sub-directory. + """ + platform_components: list[str] = [] + for component in components: + test_dir = COMPONENTS_TESTS_DIR / component + if not test_dir.is_dir(): + continue + # Each sub-directory name is expected to be a platform domain + # (e.g. tests/components/bthome/sensor/ → sensor.bthome). + for domain_dir in test_dir.iterdir(): + if not domain_dir.is_dir(): + continue + domain = domain_dir.name + domain_module = get_component(domain) + if domain_module is None or not domain_module.is_platform_component: + raise ValueError( + f"Component tests for '{component}' reference non-existing or invalid domain '{domain}'" + f" in its directory structure. See ({COMPONENTS_TESTS_DIR / component / domain})." + ) + platform_components.append(f"{domain}.{component}") + return platform_components + + +# Exit codes for run_tests +EXIT_OK = 0 +EXIT_SKIPPED = 1 +EXIT_COMPILE_ERROR = 2 +EXIT_CONFIG_ERROR = 3 +EXIT_NO_EXECUTABLE = 4 + + def run_tests(selected_components: list[str]) -> int: # Skip tests on Windows if os.name == "nt": print("Skipping esphome tests on Windows", file=sys.stderr) - return 1 + return EXIT_SKIPPED # Remove components that do not have tests components = filter_components_without_tests(selected_components) @@ -127,45 +146,63 @@ def run_tests(selected_components: list[str]) -> int: "No components specified or no tests found for the specified components.", file=sys.stderr, ) - return 0 + return EXIT_OK components = sorted(components) - # Obtain possible dependencies for the requested components. - # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, - # which causes core/time.h to include components/time/posix_tz.h. - components_with_dependencies = sorted( - get_all_dependencies(set(components) | {"time"}) - ) - - # Build a list of include folders, one folder per component containing tests. - # A special replacement main.cpp is located in /tests/components/main.cpp + # Build a list of include folders relative to COMPONENTS_TESTS_DIR. These folders will + # be added along with their subfolders. + # "main.cpp" is a special entry that points to /tests/components/main.cpp, + # which provides a custom test runner entry-point replacing the default one. + # Each remaining entry is a component folder whose *.cpp files are compiled. includes: list[str] = ["main.cpp"] + components + # Obtain a list of platform components to be tested: + try: + platform_components = get_platform_components(components) + except ValueError as e: + print(f"Error obtaining platform components: {e}") + return EXIT_CONFIG_ERROR + + components = sorted(components + platform_components) + # Create a unique name for this config based on the actual components being tested # to maximize cache during testing config_name: str = "cpptests-" + hash_components(components) - config = create_test_config(config_name, includes) + # Obtain possible dependencies for the requested components. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies: list[str] = sorted( + get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + ) - # Merge component-specific test configs (e.g. sensor instances) before - # validation so that entity registration and USE_* defines work. - extra_config = load_component_test_configs(components) - config.update(extra_config) + config = create_test_config(config_name, includes) CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" CORE.dashboard = None + CORE.cpp_testing = True + CORE.cpp_testing_codegen = CPP_TESTING_CODEGEN_COMPONENTS # Validate config will expand the above with defaults: config = validate_config(config, {}) # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. Use setdefault to avoid overwriting entries that were - # already validated (e.g. sensor instances from cpp_test.yaml). - for key in components_with_dependencies: - config.setdefault(key, {}) + # are added to the build. + for component_name in components_with_dependencies: + if "." in component_name: + # Format is always "domain.component" (exactly one dot), + # as produced by get_platform_components(). + domain, component = component_name.split(".", maxsplit=1) + domain_list = config.setdefault(domain, []) + CORE.testing_ensure_platform_registered(domain) + domain_list.append({CONF_PLATFORM: component}) + else: + config.setdefault(component_name, []) - print(f"Testing components: {', '.join(components)}") + dependencies = set(components_with_dependencies) - set(components) + deps_str = ", ".join(dependencies) if dependencies else "None" + print(f"Testing components: {', '.join(components)}. Dependencies: {deps_str}") CORE.config = config args = parse_args(["program", "compile", str(CORE.config_path)]) try: @@ -178,13 +215,13 @@ def run_tests(selected_components: list[str]) -> int: print( f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" ) - return 2 + return EXIT_COMPILE_ERROR # After a successful compilation, locate the executable and run it: idedata = get_idedata(config) if idedata is None: print("Cannot find executable") - return 1 + return EXIT_NO_EXECUTABLE program_path: str = idedata.raw["prog_path"] run_cmd: list[str] = [program_path] diff --git a/script/helpers.py b/script/helpers.py index d372d2a7ec4..6ee286a657f 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -15,6 +15,8 @@ from typing import Any import colorama +from esphome.loader import get_platform + root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -624,11 +626,15 @@ def get_usable_cpu_count() -> int: ) -def get_all_dependencies(component_names: set[str]) -> set[str]: +def get_all_dependencies( + component_names: set[str], cpp_testing: bool = False +) -> set[str]: """Get all dependencies for a set of components. Args: component_names: Set of component names to get dependencies for + cpp_testing: If True, set CORE.cpp_testing so AUTO_LOAD callables that + conditionally include testing-only dependencies work correctly Returns: Set of all components including dependencies and auto-loaded components @@ -646,6 +652,7 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Reset CORE to ensure clean state CORE.reset() + CORE.cpp_testing = cpp_testing # Set up fake config path for component loading root = Path(__file__).parent.parent @@ -660,7 +667,11 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components: set[str] = set() for comp_name in all_components: - comp = get_component(comp_name) + if "." in comp_name: + domain, platform = comp_name.split(".", maxsplit=1) + comp = get_platform(domain, platform) + else: + comp = get_component(comp_name) if not comp: continue diff --git a/tests/components/README.md b/tests/components/README.md index 0901f2ef177..6da0dadd25e 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -7,10 +7,23 @@ testing binaries that combine many components. By convention, this unique namespace is `esphome::component::testing` (where "component" is the component under test), for example: `esphome::uart::testing`. +### Platform components + +For components that expose to a platform component, create a folder under your component test folder with the platform component name, e.g. `binary_sensor` and +include the relevant `.cpp` and `.h` test files there. + +### Override component code generation for testing + +When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not +need to generate configuration code for testing. + +If you do need to generate code to for example configure compilation flags or add libraries, +add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`. ## Running component unit tests (from the repository root) + ```bash ./script/cpp_unit_test.py component1 component2 ... ``` diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp new file mode 100644 index 00000000000..36af087d2c2 --- /dev/null +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -0,0 +1,77 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportBinarySensorTest, AddBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_binary_sensor("motion", &bs); + ASSERT_EQ(transport.binary_sensors_.size(), 1u); + EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); + EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); +} + +TEST(PacketTransportBinarySensorTest, AddRemoteBinarySensor) { + TestablePacketTransport transport; + binary_sensor::BinarySensor bs; + transport.add_remote_binary_sensor("host1", "remote_motion", &bs); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); +} + +TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + binary_sensor::BinarySensor local_bs; + local_bs.state = true; + encoder.add_binary_sensor("motion", &local_bs); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + binary_sensor::BinarySensor remote_bs; + decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_TRUE(remote_bs.state); +} + +TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 10.0f; + s2.state = 20.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + binary_sensor::BinarySensor bs1; + bs1.state = true; + encoder.add_binary_sensor("bs1", &bs1); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + binary_sensor::BinarySensor rbs1; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, 10.0f); + EXPECT_FLOAT_EQ(rs2.state, 20.0f); + EXPECT_TRUE(rbs1.state); +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/components/packet_transport/cpp_test.yaml b/tests/components/packet_transport/cpp_test.yaml deleted file mode 100644 index fa39df3c0ae..00000000000 --- a/tests/components/packet_transport/cpp_test.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Extra component configuration required by C++ unit tests. -# Loaded by cpp_unit_test.py and merged into the test build config -# before validation, so that platform defines (USE_SENSOR, etc.) are generated. - -sensor: - - platform: template - id: test_cpp_sensor - -binary_sensor: - - platform: template - id: test_cpp_binary_sensor diff --git a/tests/components/packet_transport/packet_transport_test.cpp b/tests/components/packet_transport/packet_transport_test.cpp index d8f11ca6072..59c0a88ed7c 100644 --- a/tests/components/packet_transport/packet_transport_test.cpp +++ b/tests/components/packet_transport/packet_transport_test.cpp @@ -65,198 +65,6 @@ TEST(PacketTransportTest, SetProviderEncryption) { EXPECT_EQ(transport.providers_["host1"].encryption_key, key); } -// --- Sensor management (requires USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, AddSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_sensor("temp", &s); - ASSERT_EQ(transport.sensors_.size(), 1u); - EXPECT_STREQ(transport.sensors_[0].id, "temp"); - EXPECT_EQ(transport.sensors_[0].sensor, &s); - EXPECT_TRUE(transport.sensors_[0].updated); -} - -TEST(PacketTransportTest, AddRemoteSensor) { - TestablePacketTransport transport; - sensor::Sensor s; - transport.add_remote_sensor("host1", "remote_temp", &s); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, AddBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_binary_sensor("motion", &bs); - ASSERT_EQ(transport.binary_sensors_.size(), 1u); - EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); - EXPECT_EQ(transport.binary_sensors_[0].sensor, &bs); -} - -TEST(PacketTransportTest, AddRemoteBinarySensor) { - TestablePacketTransport transport; - binary_sensor::BinarySensor bs; - transport.add_remote_binary_sensor("host1", "remote_motion", &bs); - EXPECT_TRUE(transport.providers_.contains("host1")); - EXPECT_EQ(transport.remote_binary_sensors_["host1"]["remote_motion"], &bs); -} -#endif - -// --- Unencrypted round-trip tests (require USE_SENSOR / USE_BINARY_SENSOR) --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, UnencryptedSensorRoundTrip) { - // Encoder - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - sensor::Sensor local_sensor; - local_sensor.state = 42.5f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - // Decoder - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; // sentinel - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); -} -#endif - -#ifdef USE_BINARY_SENSOR -TEST(PacketTransportTest, UnencryptedBinarySensorRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - binary_sensor::BinarySensor local_bs; - local_bs.state = true; - encoder.add_binary_sensor("motion", &local_bs); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - binary_sensor::BinarySensor remote_bs; - decoder.add_remote_binary_sensor("sender", "motion", &remote_bs); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_TRUE(remote_bs.state); -} -#endif - -#if defined(USE_SENSOR) && defined(USE_BINARY_SENSOR) -TEST(PacketTransportTest, MultipleSensorsRoundTrip) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 10.0f; - s2.state = 20.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - binary_sensor::BinarySensor bs1; - bs1.state = true; - encoder.add_binary_sensor("bs1", &bs1); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - binary_sensor::BinarySensor rbs1; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - decoder.add_remote_binary_sensor("sender", "bs1", &rbs1); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, 10.0f); - EXPECT_FLOAT_EQ(rs2.state, 20.0f); - EXPECT_TRUE(rbs1.state); -} -#endif - -// --- Encrypted round-trip --- - -#ifdef USE_SENSOR -TEST(PacketTransportTest, EncryptedSensorRoundTrip) { - std::vector<uint8_t> key(32); - for (int i = 0; i < 32; i++) - key[i] = i; - - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - encoder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 99.9f; - encoder.add_sensor("temp", &local_sensor); - - encoder.send_data_(true); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - decoder.add_remote_sensor("sender", "temp", &remote_sensor); - decoder.set_provider_encryption("sender", key); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); -} - -// --- Selective send --- - -TEST(PacketTransportTest, SendDataOnlyUpdated) { - TestablePacketTransport encoder; - encoder.init_for_test("sender"); - - sensor::Sensor s1, s2; - s1.state = 1.0f; - s2.state = 2.0f; - encoder.add_sensor("s1", &s1); - encoder.add_sensor("s2", &s2); - - // Mark s1 as not updated, only s2 as updated - encoder.sensors_[0].updated = false; - encoder.sensors_[1].updated = true; - - encoder.send_data_(false); - ASSERT_EQ(encoder.sent_packets.size(), 1u); - - TestablePacketTransport decoder; - decoder.init_for_test("receiver"); - sensor::Sensor rs1, rs2; - rs1.state = -999.0f; - rs2.state = -999.0f; - decoder.add_remote_sensor("sender", "s1", &rs1); - decoder.add_remote_sensor("sender", "s2", &rs2); - - auto &packet = encoder.sent_packets[0]; - decoder.process_({packet.data(), packet.size()}); - - EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent - EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent -} -#endif - // --- Ping key tests --- TEST(PacketTransportTest, PingKeyStoredWhenEncrypted) { @@ -319,73 +127,6 @@ TEST(PacketTransportTest, PingKeyMaxLimit) { EXPECT_FALSE(transport.ping_keys_.contains("host4")); } -#ifdef USE_SENSOR -TEST(PacketTransportTest, PingKeyIncludedInTransmittedPacket) { - std::vector<uint8_t> key(32, 0xBB); - - // Responder: encrypted, owns a sensor - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - - // Requester sends a MAGIC_PING that the responder processes - auto ping = build_ping_packet("requester", 0xDEADBEEF); - responder.process_({ping.data(), ping.size()}); - ASSERT_EQ(responder.ping_keys_.size(), 1u); - - // Responder sends sensor data — ping key should be embedded - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - // The requester decrypts the packet and finds its ping key echoed back, - // which gates the sensor data — if the key is missing, data is blocked. - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); -} - -TEST(PacketTransportTest, MissingPingKeyBlocksSensorData) { - std::vector<uint8_t> key(32, 0xBB); - - // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys - TestablePacketTransport responder; - responder.init_for_test("responder"); - responder.set_encryption_key(key); - sensor::Sensor local_sensor; - local_sensor.state = 77.7f; - responder.add_sensor("temp", &local_sensor); - responder.send_data_(true); - ASSERT_EQ(responder.sent_packets.size(), 1u); - - // Requester with ping-pong enabled expects a key that isn't in the packet - TestablePacketTransport requester; - requester.init_for_test("requester"); - requester.set_ping_pong_enable(true); - requester.ping_key_ = 0xDEADBEEF; - sensor::Sensor remote_sensor; - remote_sensor.state = -999.0f; - requester.add_remote_sensor("responder", "temp", &remote_sensor); - requester.set_provider_encryption("responder", key); - - auto &packet = responder.sent_packets[0]; - requester.process_({packet.data(), packet.size()}); - EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found -} -#endif - // --- Process error handling --- TEST(PacketTransportTest, ProcessShortBuffer) { diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp new file mode 100644 index 00000000000..2f681aee587 --- /dev/null +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -0,0 +1,170 @@ +#include "../common.h" + +namespace esphome::packet_transport::testing { + +TEST(PacketTransportSensorTest, AddSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_sensor("temp", &s); + ASSERT_EQ(transport.sensors_.size(), 1u); + EXPECT_STREQ(transport.sensors_[0].id, "temp"); + EXPECT_EQ(transport.sensors_[0].sensor, &s); + EXPECT_TRUE(transport.sensors_[0].updated); +} + +TEST(PacketTransportSensorTest, AddRemoteSensor) { + TestablePacketTransport transport; + sensor::Sensor s; + transport.add_remote_sensor("host1", "remote_temp", &s); + EXPECT_TRUE(transport.providers_.contains("host1")); + EXPECT_EQ(transport.remote_sensors_["host1"]["remote_temp"], &s); +} + +TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { + // Encoder + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + sensor::Sensor local_sensor; + local_sensor.state = 42.5f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + // Decoder + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; // sentinel + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 42.5f); +} + +TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { + std::vector<uint8_t> key(32); + for (int i = 0; i < 32; i++) + key[i] = i; + + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + encoder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 99.9f; + encoder.add_sensor("temp", &local_sensor); + + encoder.send_data_(true); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + decoder.add_remote_sensor("sender", "temp", &remote_sensor); + decoder.set_provider_encryption("sender", key); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 99.9f); +} + +TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { + TestablePacketTransport encoder; + encoder.init_for_test("sender"); + + sensor::Sensor s1, s2; + s1.state = 1.0f; + s2.state = 2.0f; + encoder.add_sensor("s1", &s1); + encoder.add_sensor("s2", &s2); + + // Mark s1 as not updated, only s2 as updated + encoder.sensors_[0].updated = false; + encoder.sensors_[1].updated = true; + + encoder.send_data_(false); + ASSERT_EQ(encoder.sent_packets.size(), 1u); + + TestablePacketTransport decoder; + decoder.init_for_test("receiver"); + sensor::Sensor rs1, rs2; + rs1.state = -999.0f; + rs2.state = -999.0f; + decoder.add_remote_sensor("sender", "s1", &rs1); + decoder.add_remote_sensor("sender", "s2", &rs2); + + auto &packet = encoder.sent_packets[0]; + decoder.process_({packet.data(), packet.size()}); + + EXPECT_FLOAT_EQ(rs1.state, -999.0f); // not updated, not sent + EXPECT_FLOAT_EQ(rs2.state, 2.0f); // updated, sent +} + +TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { + std::vector<uint8_t> key(32, 0xBB); + + // Responder: encrypted, owns a sensor + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + + // Requester sends a MAGIC_PING that the responder processes + auto ping = build_ping_packet("requester", 0xDEADBEEF); + responder.process_({ping.data(), ping.size()}); + ASSERT_EQ(responder.ping_keys_.size(), 1u); + + // Responder sends sensor data — ping key should be embedded + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester: encrypted provider, ping-pong enabled, expects key 0xDEADBEEF + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + // The requester decrypts the packet and finds its ping key echoed back, + // which gates the sensor data — if the key is missing, data is blocked. + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, 77.7f); +} + +TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { + std::vector<uint8_t> key(32, 0xBB); + + // Responder sends data WITHOUT receiving any MAGIC_PING first — no ping keys + TestablePacketTransport responder; + responder.init_for_test("responder"); + responder.set_encryption_key(key); + sensor::Sensor local_sensor; + local_sensor.state = 77.7f; + responder.add_sensor("temp", &local_sensor); + responder.send_data_(true); + ASSERT_EQ(responder.sent_packets.size(), 1u); + + // Requester with ping-pong enabled expects a key that isn't in the packet + TestablePacketTransport requester; + requester.init_for_test("requester"); + requester.set_ping_pong_enable(true); + requester.ping_key_ = 0xDEADBEEF; + sensor::Sensor remote_sensor; + remote_sensor.state = -999.0f; + requester.add_remote_sensor("responder", "temp", &remote_sensor); + requester.set_provider_encryption("responder", key); + + auto &packet = responder.sent_packets[0]; + requester.process_({packet.data(), packet.size()}); + EXPECT_FLOAT_EQ(remote_sensor.state, -999.0f); // blocked — ping key not found +} + +} // namespace esphome::packet_transport::testing diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 2953a9fd428..781054eb3b9 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1027,6 +1027,73 @@ def test_get_all_dependencies_empty_set() -> None: assert result == set() +def test_get_all_dependencies_platform_component() -> None: + """Platform components (domain.component) are looked up via get_platform, + not get_component.""" + platform_comp = Mock() + platform_comp.dependencies = [] + platform_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.return_value = None + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + mock_get_platform.assert_called_once_with("sensor", "bthome") + mock_get_component.assert_not_called() + assert result == {"sensor.bthome"} + + +def test_get_all_dependencies_platform_component_with_dependencies() -> None: + """Dependencies of a platform component are resolved transitively.""" + platform_comp = Mock() + platform_comp.dependencies = ["sensor"] + platform_comp.auto_load = [] + + sensor_comp = Mock() + sensor_comp.dependencies = [] + sensor_comp.auto_load = [] + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("helpers.get_platform") as mock_get_platform, + ): + mock_get_platform.return_value = platform_comp + mock_get_component.side_effect = lambda name: ( + sensor_comp if name == "sensor" else None + ) + + result = helpers.get_all_dependencies({"sensor.bthome"}) + + assert result == {"sensor.bthome", "sensor"} + + +def test_get_all_dependencies_cpp_testing_flag() -> None: + """cpp_testing=True propagates to CORE.cpp_testing during resolution.""" + from esphome.core import CORE + + with ( + patch("esphome.loader.get_component") as mock_get_component, + patch("esphome.loader.get_platform"), + ): + observed: list[bool] = [] + + def capturing_get_component(name: str): + observed.append(CORE.cpp_testing) + + mock_get_component.side_effect = capturing_get_component + + helpers.get_all_dependencies({"some_comp"}, cpp_testing=True) + + assert observed and all(observed), ( + "CORE.cpp_testing should be True during resolution" + ) + + def test_get_components_from_integration_fixtures() -> None: """Test extraction of components from fixture YAML files.""" yaml_content = { diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 174b3fec85d..22be59653aa 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -841,6 +841,18 @@ class TestEsphomeCore: assert "WiFi" in target.platformio_libraries + def test_testing_ensure_platform_registered__sets_count(self, target): + """Test testing_ensure_platform_registered sets count to 1 for new platform.""" + assert target.platform_counts["sensor"] == 0 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 1 + + def test_testing_ensure_platform_registered__does_not_overwrite(self, target): + """Test testing_ensure_platform_registered preserves existing count.""" + target.platform_counts["sensor"] = 3 + target.testing_ensure_platform_registered("sensor") + assert target.platform_counts["sensor"] == 3 + def test_add_library__extracts_short_name_from_path(self, target): """Test add_library extracts short name from library paths like owner/lib.""" target.data[const.KEY_CORE] = { From 4b50d14496896181d261a3c8a9dc5b4d4a062bda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 9 Mar 2026 21:10:03 -1000 Subject: [PATCH 0534/2056] [serial_proxy] Reduce loop() overhead by disabling when idle and splitting read path (#14673) --- .../components/serial_proxy/serial_proxy.cpp | 29 ++++++++++++++----- .../components/serial_proxy/serial_proxy.h | 5 ++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 340f9b0cb85..00d822b75cb 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -28,25 +28,38 @@ void SerialProxy::setup() { // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data this->outgoing_msg_.instance = this->instance_index_; #endif + // No subscriber at startup; disable loop until a client subscribes + this->disable_loop(); } void SerialProxy::loop() { #ifdef USE_API - // Detect subscriber disconnect - if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() || - !this->api_connection_->is_connection_setup() || !api_is_connected())) { - ESP_LOGW(TAG, "Subscriber disconnected"); - this->api_connection_ = nullptr; + // Safety check — loop should only run when subscribed, but guard against races + if (this->api_connection_ == nullptr) [[unlikely]] { + this->disable_loop(); + return; } - if (this->api_connection_ == nullptr) + // Detect subscriber disconnect + if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() || + !api_is_connected()) { + ESP_LOGW(TAG, "Subscriber disconnected"); + this->api_connection_ = nullptr; + this->disable_loop(); return; + } // Read available data from UART and forward to subscribed client size_t available = this->available(); if (available == 0) return; + this->read_and_send_(available); +#endif +} + +#ifdef USE_API +void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) { // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE]; size_t to_read = std::min(available, sizeof(buffer)); @@ -56,8 +69,8 @@ void SerialProxy::loop() { this->outgoing_msg_.set_data(buffer, to_read); this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); -#endif } +#endif void SerialProxy::dump_config() { ESP_LOGCONFIG(TAG, @@ -166,6 +179,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: return; } this->api_connection_ = api_connection; + this->enable_loop(); ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); break; case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: @@ -174,6 +188,7 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: return; } this->api_connection_ = nullptr; + this->disable_loop(); ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); break; default: diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 62f942b19d8..5adfa4fe53b 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -101,6 +101,11 @@ class SerialProxy : public uart::UARTDevice, public Component { void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } protected: +#ifdef USE_API + /// Read from UART and send to API client (slow path with 256-byte stack buffer) + void read_and_send_(size_t available); +#endif + /// Instance index for identifying this proxy in API messages uint32_t instance_index_{0}; From fba21e6dd4bd321d4b0ee3b31869e81fee75aab5 Mon Sep 17 00:00:00 2001 From: Anunay Kulshrestha <anunay@kul.sh> Date: Tue, 10 Mar 2026 20:14:19 +0530 Subject: [PATCH 0535/2056] [bl0940] Fix reset_calibration() declaration missing from header (#14676) Co-authored-by: Claude <noreply@anthropic.com> --- esphome/components/bl0940/bl0940.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 93d54003f5f..e0ca748a222 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -69,10 +69,8 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } #endif -#ifdef USE_BUTTON - // Resets all calibration values to defaults (can be triggered by a button) + // Resets all calibration values to defaults void reset_calibration(); -#endif // Core component methods void loop() override; From 06a127f64b49c7306815100cc667af8c9842c463 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:52:48 +0100 Subject: [PATCH 0536/2056] [core] ESP-IDF compilation fixes (#14541) --- esphome/build_gen/espidf.py | 6 ++-- esphome/espidf_api.py | 61 ++++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index f45efb82c1d..9df9b1069c5 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str: # Extract compile definitions from build flags (-DXXX -> XXX) compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(compile_defs) if compile_defs else "" + compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else "" # Extract compile options (-W flags, excluding linker flags) compile_opts = [ @@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str: for flag in CORE.build_flags if flag.startswith("-W") and not flag.startswith("-Wl,") ] - compile_opts_str = "\n ".join(compile_opts) if compile_opts else "" + compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else "" # Extract linker options (-Wl, flags) link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] - link_opts_str = "\n ".join(link_opts) if link_opts else "" + link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else "" return f"""\ # Auto-generated by ESPHome diff --git a/esphome/espidf_api.py b/esphome/espidf_api.py index 9e9c57bfbdb..9ebcc48513c 100644 --- a/esphome/espidf_api.py +++ b/esphome/espidf_api.py @@ -8,7 +8,6 @@ import shutil import subprocess from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE -from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME from esphome.core import CORE, EsphomeError _LOGGER = logging.getLogger(__name__) @@ -102,6 +101,55 @@ def run_reconfigure() -> int: return run_idf_py("reconfigure") +def has_outdated_files(): + """Check if the build configuration is stale. + + Returns True if required build files are missing or if configuration inputs + are newer than the generated CMake/Ninja build artifacts. + """ + cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") + + cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") + cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") + build_config_path = CORE.relative_build_path("build/config") + sdkconfig_internal_path = CORE.relative_build_path( + f"sdkconfig.{CORE.name}.esphomeinternal" + ) + dependency_lock_path = CORE.relative_build_path("dependencies.lock") + build_ninja_path = CORE.relative_build_path("build/build.ninja") + + if not os.path.isdir(build_config_path) or not os.listdir(build_config_path): + return True + if not os.path.isfile(cmakecache_txt_path): + return True + if not os.path.isfile(build_ninja_path): + return True + if os.path.isfile(dependency_lock_path) and os.path.getmtime( + dependency_lock_path + ) > os.path.getmtime(build_ninja_path): + return True + + cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) + return any( + os.path.getmtime(f) > cmakecache_txt_mtime + for f in [ + _get_idf_path(), + cmakelists_txt_build_path, + cmakelists_txt_src_path, + sdkconfig_internal_path, + build_config_path, + ] + if f and os.path.exists(f) + ) + + +def need_reconfigure() -> bool: + from esphome.build_gen.espidf import has_discovered_components + + # We need to reconfigure either if the files are outdated or if there is no component discovered + return has_outdated_files() or not has_discovered_components() + + def run_compile(config, verbose: bool) -> int: """Compile the ESP-IDF project. @@ -110,10 +158,10 @@ def run_compile(config, verbose: bool) -> int: 2. Regenerate CMakeLists.txt with discovered components 3. Run full build """ - from esphome.build_gen.espidf import has_discovered_components, write_project + from esphome.build_gen.espidf import write_project # Check if we need to do discovery phase - if not has_discovered_components(): + if need_reconfigure(): _LOGGER.info("Discovering available ESP-IDF components...") write_project(minimal=True) rc = run_reconfigure() @@ -124,15 +172,12 @@ def run_compile(config, verbose: bool) -> int: write_project(minimal=False) # Build - args = ["build"] + args = [] if verbose: args.append("-v") - # Add parallel job limit if configured - if CONF_COMPILE_PROCESS_LIMIT in config.get(CONF_ESPHOME, {}): - limit = config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT] - args.extend(["-j", str(limit)]) + args.append("build") # Set the sdkconfig file sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") From 2c7ef4f758522ac324a5983dfc89f963fa22af70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:10:33 -1000 Subject: [PATCH 0537/2056] [rp2040] Use picotool for BOOTSEL upload and improve upload UX (#14483) --- esphome/__main__.py | 211 ++++++++++++++++- esphome/espota2.py | 26 +-- esphome/helpers.py | 27 +++ esphome/util.py | 70 ++++++ tests/unit_tests/test_main.py | 420 ++++++++++++++++++++++++++++++++++ tests/unit_tests/test_util.py | 132 +++++++++++ 6 files changed, 859 insertions(+), 27 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 0164e2eeb33..f33e7f4b426 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -9,6 +9,8 @@ import logging import os from pathlib import Path import re +import shutil +import subprocess import sys import time from typing import Protocol @@ -44,7 +46,9 @@ from esphome.const import ( CONF_SUBSTITUTIONS, CONF_TOPIC, ENV_NOGITIGNORE, + KEY_CORE, KEY_NATIVE_IDF, + KEY_TARGET_PLATFORM, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -56,7 +60,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log from esphome.types import ConfigType from esphome.util import ( + PICOTOOL_PACKAGE, + detect_rp2040_bootsel, + get_picotool_path, get_serial_ports, + is_picotool_usb_permission_error, list_yaml_files, run_external_command, run_external_process, @@ -68,6 +76,21 @@ _LOGGER = logging.getLogger(__name__) # Maximum buffer size for serial log reading to prevent unbounded memory growth SERIAL_BUFFER_MAX_SIZE = 65536 +_RP2040_BOOTSEL_INSTRUCTIONS = ( + "To enter BOOTSEL mode:\n" + " 1. Unplug the device\n" + " 2. Hold the BOOT/BOOTSEL button\n" + " 3. Plug in the USB cable while holding the button\n" + " 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n" + "Then run the upload command again." +) + +_RP2040_UDEV_HINT = ( + "You may need to add a udev rule for RP2040 devices. " + "See: https://github.com/raspberrypi/picotool" + "/blob/master/udev/60-picotool.rules" +) + # Special non-component keys that appear in configs _NON_COMPONENT_KEYS = frozenset( { @@ -163,6 +186,7 @@ class PortType(StrEnum): NETWORK = "NETWORK" MQTT = "MQTT" MQTTIP = "MQTTIP" + BOOTSEL = "BOOTSEL" # Magic MQTT port types that require special handling @@ -241,6 +265,19 @@ def choose_upload_log_host( (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() ] + # Add RP2040 BOOTSEL device option when uploading + bootsel_permission_error = False + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and (picotool := _find_picotool()) is not None + ): + bootsel = detect_rp2040_bootsel(picotool) + if bootsel.device_count > 0: + options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL")) + elif bootsel.permission_error: + bootsel_permission_error = True + if purpose == Purpose.LOGGING: if has_mqtt_logging(): mqtt_config = CORE.config[CONF_MQTT] @@ -258,6 +295,25 @@ def choose_upload_log_host( if has_mqtt_ip_lookup(): options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + # Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options) + ): + if bootsel_permission_error: + _LOGGER.warning( + "An RP2040 device in BOOTSEL mode was detected but could " + "not be accessed due to USB permissions." + ) + if sys.platform.startswith("linux"): + _LOGGER.warning(_RP2040_UDEV_HINT) + if not options: + raise EsphomeError( + f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}" + ) + _LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS) + if check_default is not None and check_default in [opt[1] for opt in options]: return [check_default] return [choose_prompt(options, purpose=purpose)] @@ -404,10 +460,13 @@ def get_port_type(port: str) -> PortType: Returns: PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) + PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool PortType.MQTT for MQTT logging PortType.MQTTIP for MQTT IP lookup PortType.NETWORK for IP addresses, hostnames, or mDNS names """ + if port == "BOOTSEL": + return PortType.BOOTSEL if port.startswith("/") or port.startswith("COM"): return PortType.SERIAL if port == "MQTT": @@ -695,15 +754,138 @@ def upload_using_esptool( return run_esptool(115200) -def upload_using_platformio(config: ConfigType, port: str): +def upload_using_platformio(config: ConfigType, port: str) -> int: from esphome import platformio_api + # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for + # the upload target, but 'nobuild' skips the build phase that creates it. + # Create it here so the upload doesn't fail. + if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040: + idedata = platformio_api.get_idedata(config) + build_dir = Path(idedata.firmware_elf_path).parent + firmware_bin = build_dir / "firmware.bin" + signed_bin = build_dir / "firmware.bin.signed" + if firmware_bin.is_file() and not signed_bin.is_file(): + shutil.copy2(firmware_bin, signed_bin) + upload_args = ["-t", "upload", "-t", "nobuild"] if port is not None: upload_args += ["--upload-port", port] return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) +def _find_picotool() -> Path | None: + """Find the picotool binary from PlatformIO packages.""" + from esphome import platformio_api + + try: + idedata = platformio_api.get_idedata(CORE.config) + except Exception: # noqa: BLE001 # pylint: disable=broad-except + return None + return get_picotool_path(idedata.cc_path) + + +def upload_using_picotool(config: ConfigType) -> int: + """Upload firmware to RP2040 in BOOTSEL mode using picotool. + + Uses picotool to load the ELF firmware directly via USB, avoiding + the mass storage copy approach that causes "disk not ejected properly" + warnings on macOS. + """ + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + firmware_elf = Path(idedata.firmware_elf_path) + + if not firmware_elf.is_file(): + _LOGGER.error( + "Firmware ELF file not found at %s. " + "Make sure the project has been compiled first.", + firmware_elf, + ) + return 1 + + picotool = get_picotool_path(idedata.cc_path) + if picotool is None: + _LOGGER.error( + "picotool not found. Ensure the RP2040 PlatformIO platform " + "is installed (%s).", + PICOTOOL_PACKAGE, + ) + return 1 + + _LOGGER.info("Uploading firmware to RP2040 via picotool...") + try: + # Don't capture stdout — let picotool write directly to the terminal + # so progress bars display in real-time with \r updates. + # Capture stderr only so we can detect permission errors. + result = subprocess.run( + [str(picotool), "load", "-v", "-x", str(firmware_elf)], + stderr=subprocess.PIPE, + timeout=60, + check=False, + ) + except subprocess.TimeoutExpired: + _LOGGER.error("picotool upload timed out after 60 seconds.") + return 1 + except OSError as err: + _LOGGER.error("Failed to run picotool: %s", err) + return 1 + + if result.returncode != 0: + stderr = result.stderr.decode("utf-8", errors="replace").strip() + if stderr: + for line in stderr.splitlines(): + safe_print(line) + if is_picotool_usb_permission_error(stderr): + msg = "Permission denied accessing USB device." + if sys.platform.startswith("linux"): + msg += f" {_RP2040_UDEV_HINT}" + _LOGGER.error(msg) + else: + _LOGGER.error("picotool upload failed (exit code %d).", result.returncode) + return 1 + + return 0 + + +def _wait_for_serial_port( + port: str | None = None, + timeout: float = 30.0, + known_ports: set[str] | None = None, +) -> None: + """Wait for a serial port to appear, e.g. after a device reboot. + + USB-CDC devices disappear briefly after flashing while the device + reboots and re-enumerates on the USB bus. + + If port is given, wait for that specific path. If known_ports is + given, wait for a new port that wasn't in the set. Otherwise wait + for any serial port to appear. + """ + + def _port_found() -> bool: + ports = get_serial_ports() + if port is not None: + return any(p.path == port for p in ports) + if known_ports is not None: + return any(p.path not in known_ports for p in ports) + return bool(ports) + + if _port_found(): + return + if port is not None: + _LOGGER.info("Waiting for %s to come online...", port) + else: + _LOGGER.info("Waiting for device to reboot...") + start = time.monotonic() + while time.monotonic() - start < timeout: + time.sleep(0.05) + if _port_found(): + time.sleep(0.05) + return + + def check_permissions(port: str): if os.name == "posix" and get_port_type(port) == PortType.SERIAL: # Check if we can open selected serial port @@ -733,7 +915,15 @@ def upload_program( except AttributeError: pass - if get_port_type(host) == PortType.SERIAL: + port_type = get_port_type(host) + + if port_type == PortType.BOOTSEL: + exit_code = upload_using_picotool(config) + # Return None for device - BOOTSEL can't be used for logging, + # so command_run will show the interactive chooser for log source + return exit_code, None + + if port_type == PortType.SERIAL: check_permissions(host) exit_code = 1 @@ -787,6 +977,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int port_type = get_port_type(port) if port_type == PortType.SERIAL: + _wait_for_serial_port(port) check_permissions(port) return run_miniterm(config, port, args) @@ -925,6 +1116,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose=Purpose.UPLOADING, ) + # Snapshot current serial ports before upload so we can detect new ones + pre_upload_ports = {p.path for p in get_serial_ports()} + exit_code, successful_device = upload_program(config, args, devices) if exit_code == 0: _LOGGER.info("Successfully uploaded program.") @@ -935,6 +1129,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: if args.no_logs: return 0 + # After BOOTSEL upload, wait for a new serial port to appear + # so it shows up in the log chooser + if ( + successful_device is None + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + ): + _wait_for_serial_port(known_ports=pre_upload_ports) + # If exactly one new serial port appeared, use it directly + serial_ports = get_serial_ports() + new_ports = [p for p in serial_ports if p.path not in pre_upload_ports] + if len(new_ports) == 1: + successful_device = new_ports[0].path + # For logs, prefer the device we successfully uploaded to devices = choose_upload_log_host( default=successful_device, diff --git a/esphome/espota2.py b/esphome/espota2.py index c342eb4463c..c412bb51ffd 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -13,7 +13,7 @@ import time from typing import Any from esphome.core import EsphomeError -from esphome.helpers import resolve_ip_address +from esphome.helpers import ProgressBar, resolve_ip_address RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -63,30 +63,6 @@ _AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { } -class ProgressBar: - def __init__(self): - self.last_progress = None - - def update(self, progress): - bar_length = 60 - status = "" - if progress >= 1: - progress = 1 - status = "Done...\r\n" - new_progress = int(progress * 100) - if new_progress == self.last_progress: - return - self.last_progress = new_progress - block = int(round(bar_length * progress)) - text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" - sys.stderr.write(text) - sys.stderr.flush() - - def done(self): - sys.stderr.write("\n") - sys.stderr.flush() - - class OTAError(EsphomeError): pass diff --git a/esphome/helpers.py b/esphome/helpers.py index 145ebd40968..f41bec357d6 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -9,6 +9,7 @@ import platform import re import shutil import stat +import sys import tempfile from typing import TYPE_CHECKING from urllib.parse import urlparse @@ -585,6 +586,32 @@ def sanitize(value): return _DISALLOWED_CHARS.sub("_", value) +class ProgressBar: + """A simple terminal progress bar for upload operations.""" + + def __init__(self) -> None: + self.last_progress: int | None = None + + def update(self, progress: float) -> None: + bar_length = 60 + status = "" + if progress >= 1: + progress = 1 + status = "Done...\r\n" + new_progress = int(progress * 100) + if new_progress == self.last_progress: + return + self.last_progress = new_progress + block = int(round(bar_length * progress)) + text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" + sys.stderr.write(text) + sys.stderr.flush() + + def done(self) -> None: + sys.stderr.write("\n") + sys.stderr.flush() + + def docs_url(path: str) -> str: """Return the URL to the documentation for a given path.""" # Local import to avoid circular import diff --git a/esphome/util.py b/esphome/util.py index 686aa74306a..6a21b4f627f 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,6 @@ import collections from collections.abc import Callable +from dataclasses import dataclass import io import logging from pathlib import Path @@ -355,6 +356,75 @@ def get_serial_ports() -> list[SerialPort]: return result +PICOTOOL_PACKAGE = "tool-picotool-rp2040-earlephilhower" + + +def get_picotool_path(cc_path: str) -> Path | None: + """Derive the picotool binary path from the PlatformIO toolchain cc_path. + + The cc_path from IDEData points to the toolchain package, e.g.: + ~/.platformio/packages/toolchain-rp2040-earlephilhower/bin/arm-none-eabi-gcc + Picotool is in a sibling package: + ~/.platformio/packages/tool-picotool-rp2040-earlephilhower/picotool + """ + cc = Path(cc_path) + # Go from .../packages/toolchain-.../bin/gcc up to .../packages/ + packages_dir = cc.parent.parent.parent + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = packages_dir / PICOTOOL_PACKAGE / binary_name + if picotool.is_file(): + return picotool + return None + + +def is_picotool_usb_permission_error(output: str | bytes) -> bool: + """Check if picotool output indicates a USB permission error.""" + if isinstance(output, str): + return ( + "unable to connect" in output + or "LIBUSB_ERROR_ACCESS" in output + or "Permission denied" in output + ) + return ( + b"unable to connect" in output + or b"LIBUSB_ERROR_ACCESS" in output + or b"Permission denied" in output + ) + + +@dataclass +class BootselResult: + """Result of RP2040 BOOTSEL detection.""" + + device_count: int + permission_error: bool = False + + +def detect_rp2040_bootsel(picotool_path: str | Path) -> BootselResult: + """Detect RP2040/RP2350 devices in BOOTSEL mode using picotool. + + Returns a BootselResult with the number of devices found (by counting + 'type:' lines in output), and whether a permission error was detected. + """ + try: + result = subprocess.run( + [str(picotool_path), "info", "-d"], + capture_output=True, + timeout=10, + check=False, + ) + device_count = result.stdout.count(b"type:") + if device_count > 0: + return BootselResult(device_count) + # Check for permission issues — picotool can see the device + # on the USB bus but can't connect without proper permissions + if is_picotool_usb_permission_error(result.stderr + result.stdout): + return BootselResult(0, permission_error=True) + return BootselResult(0) + except (OSError, subprocess.TimeoutExpired): + return BootselResult(0) + + def get_esp32_arduino_flash_error_help() -> str | None: """Returns helpful message when ESP32 with Arduino runs out of flash space.""" from esphome.core import CORE diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index cef561c54b7..b6f1a28086a 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -8,6 +8,7 @@ import json import logging from pathlib import Path import re +import sys import time from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -40,6 +41,8 @@ from esphome.__main__ import ( show_logs, upload_program, upload_using_esptool, + upload_using_picotool, + upload_using_platformio, ) from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( @@ -70,6 +73,7 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError +from esphome.util import BootselResult def strip_ansi_codes(text: str) -> str: @@ -174,6 +178,13 @@ def mock_upload_using_platformio() -> Generator[Mock]: yield mock +@pytest.fixture +def mock_upload_using_picotool() -> Generator[Mock]: + """Mock upload_using_picotool for testing.""" + with patch("esphome.__main__.upload_using_picotool") as mock: + yield mock + + @pytest.fixture def mock_run_ota() -> Generator[Mock]: """Mock espota2.run_ota for testing.""" @@ -851,6 +862,221 @@ def test_choose_upload_log_host_no_address_with_ota_config() -> None: ) +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel( + mock_choose_prompt: Mock, +) -> None: + """Test interactive mode shows RP2040 BOOTSEL option via picotool.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)), + ): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert result == ["/dev/ttyUSB0"] # mock_choose_prompt default + mock_choose_prompt.assert_called_once_with( + [("RP2040 BOOTSEL (via picotool)", "BOOTSEL")], + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None: + """Test BOOTSEL instructions shown when no RP2040 device found.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + pytest.raises(EsphomeError, match="BOOTSEL"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test BOOTSEL tip shown when only OTA options exist for RP2040.""" + setup_core( + platform=PLATFORM_RP2040, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + caplog.at_level(logging.INFO, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert "BOOTSEL" in caplog.text + + +def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports( + caplog: pytest.LogCaptureFixture, + mock_choose_prompt: Mock, +) -> None: + """Test BOOTSEL tip shown when serial ports exist but no BOOTSEL device.""" + setup_core(platform=PLATFORM_RP2040) + + mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] + with ( + patch("esphome.__main__.get_serial_ports", return_value=mock_ports), + patch( + "esphome.__main__._find_picotool", + return_value=Path("/usr/bin/picotool"), + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)), + caplog.at_level(logging.INFO, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + assert "BOOTSEL" in caplog.text + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_permission_error_no_options( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test permission warning shown when BOOTSEL device found but not accessible.""" + setup_core(platform=PLATFORM_RP2040) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch( + "esphome.__main__.detect_rp2040_bootsel", + return_value=BootselResult(0, permission_error=True), + ), + patch("esphome.__main__.sys.platform", "linux"), + pytest.raises(EsphomeError, match="BOOTSEL"), + caplog.at_level(logging.WARNING, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert "USB permissions" in caplog.text + assert "udev" in caplog.text + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_rp2040_permission_error_with_ota( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test permission warning shown with OTA fallback available.""" + setup_core( + platform=PLATFORM_RP2040, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch( + "esphome.__main__.detect_rp2040_bootsel", + return_value=BootselResult(0, permission_error=True), + ), + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + caplog.at_level(logging.WARNING, logger="esphome.__main__"), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert "USB permissions" in caplog.text + + +def test_choose_upload_log_host_no_bootsel_for_non_rp2040( + mock_no_serial_ports: Mock, +) -> None: + """Test that BOOTSEL detection is not run for non-RP2040 platforms.""" + setup_core( + platform=PLATFORM_ESP32, + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + ) + + with ( + patch("esphome.__main__._find_picotool") as mock_find_picotool, + patch( + "esphome.__main__.choose_prompt", + return_value="192.168.1.100", + ), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + mock_find_picotool.assert_not_called() + + +def test_choose_upload_log_host_rp2040_serial_and_bootsel( + mock_choose_prompt: Mock, +) -> None: + """Test both serial ports and BOOTSEL option shown for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + + mock_ports = [MockSerialPort("/dev/ttyACM0", "RP2040 Serial")] + with ( + patch("esphome.__main__.get_serial_ports", return_value=mock_ports), + patch( + "esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool") + ), + patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + mock_choose_prompt.assert_called_once_with( + [ + ("/dev/ttyACM0 (RP2040 Serial)", "/dev/ttyACM0"), + ("RP2040 BOOTSEL (via picotool)", "BOOTSEL"), + ], + purpose=Purpose.UPLOADING, + ) + + @dataclass class MockArgs: """Mock args for testing.""" @@ -1060,6 +1286,46 @@ def test_upload_program_serial_platformio_platforms( mock_upload_using_platformio.assert_called_once_with(config, device) +def test_upload_using_platformio_creates_signed_bin_for_rp2040( + tmp_path: Path, +) -> None: + """Test that upload_using_platformio creates firmware.bin.signed for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_bin = build_dir / "firmware.bin" + firmware_bin.write_bytes(b"test firmware content") + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"elf") + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio_api.run_platformio_cli_run", return_value=0), + ): + result = upload_using_platformio({}, "/dev/ttyACM0") + + assert result == 0 + signed_bin = build_dir / "firmware.bin.signed" + assert signed_bin.is_file() + assert signed_bin.read_bytes() == b"test firmware content" + + +def test_upload_using_platformio_skips_signed_bin_for_non_rp2040( + tmp_path: Path, +) -> None: + """Test that upload_using_platformio doesn't create signed bin for non-RP2040.""" + setup_core(platform=PLATFORM_ESP32) + + with patch("esphome.platformio_api.run_platformio_cli_run", return_value=0): + result = upload_using_platformio({}, "/dev/ttyUSB0") + + assert result == 0 + + def test_upload_program_serial_upload_failed( mock_upload_using_esptool: Mock, mock_get_port_type: Mock, @@ -1082,6 +1348,158 @@ def test_upload_program_serial_upload_failed( mock_upload_using_esptool.assert_called_once() +def test_upload_program_bootsel( + mock_upload_using_picotool: Mock, + mock_get_port_type: Mock, +) -> None: + """Test upload_program with BOOTSEL for RP2040.""" + setup_core(platform=PLATFORM_RP2040) + mock_get_port_type.return_value = "BOOTSEL" + mock_upload_using_picotool.return_value = 0 + + config = {} + args = MockArgs() + devices = ["BOOTSEL"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + # BOOTSEL device can't be used for logging, so host should be None + assert host is None + mock_upload_using_picotool.assert_called_once_with(config) + + +def test_upload_program_bootsel_failed( + mock_upload_using_picotool: Mock, + mock_get_port_type: Mock, +) -> None: + """Test upload_program when BOOTSEL upload fails.""" + setup_core(platform=PLATFORM_RP2040) + mock_get_port_type.return_value = "BOOTSEL" + mock_upload_using_picotool.return_value = 1 + + config = {} + args = MockArgs() + devices = ["BOOTSEL"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 1 + assert host is None + mock_upload_using_picotool.assert_called_once_with(config) + + +def test_upload_using_picotool_success(tmp_path: Path) -> None: + """Test upload_using_picotool succeeds.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 1024) + + # Create picotool binary + packages_dir = tmp_path / "packages" + toolchain_bin = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_bin.mkdir(parents=True) + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = picotool_dir / binary_name + picotool.touch() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = str(toolchain_bin / "arm-none-eabi-gcc") + + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stderr = b"" + + config = {} + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("subprocess.run", return_value=mock_result), + ): + exit_code = upload_using_picotool(config) + + assert exit_code == 0 + + +def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: + """Test upload_using_picotool when ELF file is missing.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(build_dir / "firmware.elf") + mock_idedata.cc_path = "/fake/path/gcc" + + config = {} + with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + +def test_upload_using_picotool_not_found(tmp_path: Path) -> None: + """Test upload_using_picotool when picotool binary not found.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 512) + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = "/fake/path/gcc" + + config = {} + with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + +def test_upload_using_picotool_permission_error(tmp_path: Path) -> None: + """Test upload_using_picotool shows helpful message on permission error.""" + setup_core(platform=PLATFORM_RP2040, tmp_path=tmp_path) + + build_dir = tmp_path / "build" + build_dir.mkdir() + firmware_elf = build_dir / "firmware.elf" + firmware_elf.write_bytes(b"\x00" * 512) + + packages_dir = tmp_path / "packages" + toolchain_bin = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_bin.mkdir(parents=True) + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool = picotool_dir / binary_name + picotool.touch() + + mock_idedata = MagicMock() + mock_idedata.firmware_elf_path = str(firmware_elf) + mock_idedata.cc_path = str(toolchain_bin / "arm-none-eabi-gcc") + + mock_result = MagicMock() + mock_result.returncode = 1 + mock_result.stderr = b"LIBUSB_ERROR_ACCESS" + + config = {} + with ( + patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("subprocess.run", return_value=mock_result), + ): + exit_code = upload_using_picotool(config) + + assert exit_code == 1 + + def test_upload_program_ota_success( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -1606,6 +2024,8 @@ def test_get_port_type() -> None: assert get_port_type("esphome-device.local") == "NETWORK" assert get_port_type("10.0.0.1") == "NETWORK" + assert get_port_type("BOOTSEL") == "BOOTSEL" + def test_has_mqtt_ip_lookup() -> None: """Test has_mqtt_ip_lookup function.""" diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index 85873caea81..ca3fd9b78ab 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -3,6 +3,9 @@ from __future__ import annotations from pathlib import Path +import subprocess +import sys +from unittest.mock import MagicMock, patch import pytest @@ -402,3 +405,132 @@ def test_shlex_quote_edge_cases() -> None: assert util.shlex_quote("\t") == "'\t'" assert util.shlex_quote("\n") == "'\n'" assert util.shlex_quote(" ") == "' '" + + +def test_get_picotool_path_found(tmp_path: Path) -> None: + """Test picotool path derivation from cc_path.""" + # Create the expected directory structure + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc" + gcc.touch() + + binary_name = "picotool.exe" if sys.platform == "win32" else "picotool" + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + picotool = picotool_dir / binary_name + picotool.touch() + + result = util.get_picotool_path(str(gcc)) + assert result == picotool + + +def test_get_picotool_path_not_found(tmp_path: Path) -> None: + """Test picotool path returns None when not installed.""" + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc" + gcc.touch() + + result = util.get_picotool_path(str(gcc)) + assert result is None + + +def test_get_picotool_path_windows(tmp_path: Path) -> None: + """Test picotool path uses .exe on Windows.""" + packages_dir = tmp_path / "packages" + toolchain_dir = packages_dir / "toolchain-rp2040-earlephilhower" / "bin" + toolchain_dir.mkdir(parents=True) + gcc = toolchain_dir / "arm-none-eabi-gcc.exe" + gcc.touch() + + picotool_dir = packages_dir / "tool-picotool-rp2040-earlephilhower" + picotool_dir.mkdir(parents=True) + picotool = picotool_dir / "picotool.exe" + picotool.touch() + + with patch("esphome.util.sys.platform", "win32"): + result = util.get_picotool_path(str(gcc)) + assert result == picotool + + +def test_detect_rp2040_bootsel_found() -> None: + """Test BOOTSEL device detection when device is present.""" + mock_result = MagicMock() + mock_result.stdout = b"Device Information\n type: RP2040\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 1 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_multiple() -> None: + """Test BOOTSEL detection with multiple devices.""" + mock_result = MagicMock() + mock_result.stdout = b"type: RP2040\ntype: RP2350\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 2 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_none() -> None: + """Test BOOTSEL detection when no device found.""" + mock_result = MagicMock() + mock_result.stdout = ( + b"No accessible RP2040/RP2350 devices in BOOTSEL mode were found.\n" + ) + mock_result.stderr = b"" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_permission_error() -> None: + """Test BOOTSEL detection with device found but not accessible.""" + mock_result = MagicMock() + mock_result.stdout = ( + b"No accessible RP-series devices in BOOTSEL mode were found.\n" + ) + mock_result.stderr = ( + b"RP2040 device at bus 5, address 24 appears to be in BOOTSEL mode, " + b"but picotool was unable to connect. " + b"Maybe try 'sudo' or check your permissions.\n" + ) + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is True + + +def test_detect_rp2040_bootsel_libusb_access_error() -> None: + """Test BOOTSEL detection with LIBUSB_ERROR_ACCESS.""" + mock_result = MagicMock() + mock_result.stdout = b"" + mock_result.stderr = b"LIBUSB_ERROR_ACCESS\n" + with patch("esphome.util.subprocess.run", return_value=mock_result): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is True + + +def test_detect_rp2040_bootsel_oserror() -> None: + """Test BOOTSEL detection handles OSError.""" + with patch("esphome.util.subprocess.run", side_effect=OSError("not found")): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False + + +def test_detect_rp2040_bootsel_timeout() -> None: + """Test BOOTSEL detection handles timeout.""" + with patch( + "esphome.util.subprocess.run", + side_effect=subprocess.TimeoutExpired("picotool", 10), + ): + result = util.detect_rp2040_bootsel("/usr/bin/picotool") + assert result.device_count == 0 + assert result.permission_error is False From 6e468936ec670461e244aa93b7d3ea74f1fa2bef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:10:55 -1000 Subject: [PATCH 0538/2056] [api] Inline ProtoVarInt::parse fast path and return consumed in struct (#14638) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../api/api_frame_helper_plaintext.cpp | 24 +- esphome/components/api/api_pb2.cpp | 430 +++++++++--------- esphome/components/api/api_pb2.h | 102 ++--- esphome/components/api/proto.cpp | 75 +-- esphome/components/api/proto.h | 118 ++--- script/api_protobuf/api_protobuf.py | 20 +- 6 files changed, 376 insertions(+), 393 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 3c54ed7c70b..793cece3b82 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -128,37 +128,37 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Skip indicator byte at position 0 uint8_t varint_pos = 1; - uint32_t consumed = 0; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2 + auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_size_varint.has_value()) { // not enough data there yet continue; } - if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { + if (msg_size_varint.value > MAX_MESSAGE_SIZE) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - MAX_MESSAGE_SIZE); + HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", + static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_len_ = msg_size_varint->as_uint16(); + rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value); // Move to next varint position - varint_pos += consumed; + varint_pos += msg_size_varint.consumed; - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_type_varint.has_value()) { // not enough data there yet continue; } - if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { + if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), - std::numeric_limits<uint16_t>::max()); + HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", + static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max()); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_type_ = msg_type_varint->as_uint16(); + rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value); rx_header_parsed_ = true; } // header reading done diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6fce10ca0fe..01993cc5e5f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -7,13 +7,13 @@ namespace esphome::api { -bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HelloRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->api_version_major = value.as_uint32(); + this->api_version_major = value; break; case 3: - this->api_version_minor = value.as_uint32(); + this->api_version_minor = value; break; default: return false; @@ -316,20 +316,20 @@ uint32_t CoverStateResponse::calculate_size() const { #endif return size; } -bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CoverCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 6: - this->has_tilt = value.as_bool(); + this->has_tilt = value != 0; break; case 8: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 9: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -423,38 +423,38 @@ uint32_t FanStateResponse::calculate_size() const { #endif return size; } -bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool FanCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 6: - this->has_oscillating = value.as_bool(); + this->has_oscillating = value != 0; break; case 7: - this->oscillating = value.as_bool(); + this->oscillating = value != 0; break; case 8: - this->has_direction = value.as_bool(); + this->has_direction = value != 0; break; case 9: - this->direction = static_cast<enums::FanDirection>(value.as_uint32()); + this->direction = static_cast<enums::FanDirection>(value); break; case 10: - this->has_speed_level = value.as_bool(); + this->has_speed_level = value != 0; break; case 11: - this->speed_level = value.as_int32(); + this->speed_level = static_cast<int32_t>(value); break; case 12: - this->has_preset_mode = value.as_bool(); + this->has_preset_mode = value != 0; break; #ifdef USE_DEVICES case 14: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -571,59 +571,59 @@ uint32_t LightStateResponse::calculate_size() const { #endif return size; } -bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LightCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_brightness = value.as_bool(); + this->has_brightness = value != 0; break; case 22: - this->has_color_mode = value.as_bool(); + this->has_color_mode = value != 0; break; case 23: - this->color_mode = static_cast<enums::ColorMode>(value.as_uint32()); + this->color_mode = static_cast<enums::ColorMode>(value); break; case 20: - this->has_color_brightness = value.as_bool(); + this->has_color_brightness = value != 0; break; case 6: - this->has_rgb = value.as_bool(); + this->has_rgb = value != 0; break; case 10: - this->has_white = value.as_bool(); + this->has_white = value != 0; break; case 12: - this->has_color_temperature = value.as_bool(); + this->has_color_temperature = value != 0; break; case 24: - this->has_cold_white = value.as_bool(); + this->has_cold_white = value != 0; break; case 26: - this->has_warm_white = value.as_bool(); + this->has_warm_white = value != 0; break; case 14: - this->has_transition_length = value.as_bool(); + this->has_transition_length = value != 0; break; case 15: - this->transition_length = value.as_uint32(); + this->transition_length = value; break; case 16: - this->has_flash_length = value.as_bool(); + this->has_flash_length = value != 0; break; case 17: - this->flash_length = value.as_uint32(); + this->flash_length = value; break; case 18: - this->has_effect = value.as_bool(); + this->has_effect = value != 0; break; #ifdef USE_DEVICES case 28: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -787,14 +787,14 @@ uint32_t SwitchStateResponse::calculate_size() const { #endif return size; } -bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SwitchCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->state = value.as_bool(); + this->state = value != 0; break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -863,13 +863,13 @@ uint32_t TextSensorStateResponse::calculate_size() const { return size; } #endif -bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->level = static_cast<enums::LogLevel>(value.as_uint32()); + this->level = static_cast<enums::LogLevel>(value); break; case 2: - this->dump_config = value.as_bool(); + this->dump_config = value != 0; break; default: return false; @@ -971,13 +971,13 @@ uint32_t HomeassistantActionRequest::calculate_size() const { } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES -bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HomeassistantActionResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->call_id = value.as_uint32(); + this->call_id = value; break; case 2: - this->success = value.as_bool(); + this->success = value != 0; break; default: return false; @@ -1036,38 +1036,38 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif -bool DSTRule::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DSTRule::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->time_seconds = value.as_sint32(); + this->time_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; case 2: - this->day = value.as_uint32(); + this->day = value; break; case 3: - this->type = static_cast<enums::DSTRuleType>(value.as_uint32()); + this->type = static_cast<enums::DSTRuleType>(value); break; case 4: - this->month = value.as_uint32(); + this->month = value; break; case 5: - this->week = value.as_uint32(); + this->week = value; break; case 6: - this->day_of_week = value.as_uint32(); + this->day_of_week = value; break; default: return false; } return true; } -bool ParsedTimezone::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ParsedTimezone::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->std_offset_seconds = value.as_sint32(); + this->std_offset_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; case 2: - this->dst_offset_seconds = value.as_sint32(); + this->dst_offset_seconds = decode_zigzag32(static_cast<uint32_t>(value)); break; default: return false; @@ -1142,22 +1142,22 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->supports_response)); return size; } -bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->bool_ = value.as_bool(); + this->bool_ = value != 0; break; case 2: - this->legacy_int = value.as_int32(); + this->legacy_int = static_cast<int32_t>(value); break; case 5: - this->int_ = value.as_sint32(); + this->int_ = decode_zigzag32(static_cast<uint32_t>(value)); break; case 6: - this->bool_array.push_back(value.as_bool()); + this->bool_array.push_back(value != 0); break; case 7: - this->int_array.push_back(value.as_sint32()); + this->int_array.push_back(decode_zigzag32(static_cast<uint32_t>(value))); break; default: return false; @@ -1202,16 +1202,16 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { this->string_array.init(count_string_array); ProtoDecodableMessage::decode(buffer, length); } -bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 3: - this->call_id = value.as_uint32(); + this->call_id = value; break; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 4: - this->return_response = value.as_bool(); + this->return_response = value != 0; break; #endif default: @@ -1313,13 +1313,13 @@ uint32_t CameraImageResponse::calculate_size() const { #endif return size; } -bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->single = value.as_bool(); + this->single = value != 0; break; case 2: - this->stream = value.as_bool(); + this->stream = value != 0; break; default: return false; @@ -1468,53 +1468,53 @@ uint32_t ClimateStateResponse::calculate_size() const { #endif return size; } -bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ClimateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_mode = value.as_bool(); + this->has_mode = value != 0; break; case 3: - this->mode = static_cast<enums::ClimateMode>(value.as_uint32()); + this->mode = static_cast<enums::ClimateMode>(value); break; case 4: - this->has_target_temperature = value.as_bool(); + this->has_target_temperature = value != 0; break; case 6: - this->has_target_temperature_low = value.as_bool(); + this->has_target_temperature_low = value != 0; break; case 8: - this->has_target_temperature_high = value.as_bool(); + this->has_target_temperature_high = value != 0; break; case 12: - this->has_fan_mode = value.as_bool(); + this->has_fan_mode = value != 0; break; case 13: - this->fan_mode = static_cast<enums::ClimateFanMode>(value.as_uint32()); + this->fan_mode = static_cast<enums::ClimateFanMode>(value); break; case 14: - this->has_swing_mode = value.as_bool(); + this->has_swing_mode = value != 0; break; case 15: - this->swing_mode = static_cast<enums::ClimateSwingMode>(value.as_uint32()); + this->swing_mode = static_cast<enums::ClimateSwingMode>(value); break; case 16: - this->has_custom_fan_mode = value.as_bool(); + this->has_custom_fan_mode = value != 0; break; case 18: - this->has_preset = value.as_bool(); + this->has_preset = value != 0; break; case 19: - this->preset = static_cast<enums::ClimatePreset>(value.as_uint32()); + this->preset = static_cast<enums::ClimatePreset>(value); break; case 20: - this->has_custom_preset = value.as_bool(); + this->has_custom_preset = value != 0; break; case 22: - this->has_target_humidity = value.as_bool(); + this->has_target_humidity = value != 0; break; #ifdef USE_DEVICES case 24: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1631,21 +1631,21 @@ uint32_t WaterHeaterStateResponse::calculate_size() const { size += ProtoSize::calc_float(1, this->target_temperature_high); return size; } -bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_fields = value.as_uint32(); + this->has_fields = value; break; case 3: - this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32()); + this->mode = static_cast<enums::WaterHeaterMode>(value); break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 6: - this->state = value.as_uint32(); + this->state = value; break; default: return false; @@ -1731,11 +1731,11 @@ uint32_t NumberStateResponse::calculate_size() const { #endif return size; } -bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool NumberCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1812,11 +1812,11 @@ uint32_t SelectStateResponse::calculate_size() const { #endif return size; } -bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SelectCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1903,29 +1903,29 @@ uint32_t SirenStateResponse::calculate_size() const { #endif return size; } -bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SirenCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_tone = value.as_bool(); + this->has_tone = value != 0; break; case 6: - this->has_duration = value.as_bool(); + this->has_duration = value != 0; break; case 7: - this->duration = value.as_uint32(); + this->duration = value; break; case 8: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2011,17 +2011,17 @@ uint32_t LockStateResponse::calculate_size() const { #endif return size; } -bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LockCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::LockCommand>(value.as_uint32()); + this->command = static_cast<enums::LockCommand>(value); break; case 3: - this->has_code = value.as_bool(); + this->has_code = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2082,11 +2082,11 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { #endif return size; } -bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ButtonCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 2: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2182,29 +2182,29 @@ uint32_t MediaPlayerStateResponse::calculate_size() const { #endif return size; } -bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_command = value.as_bool(); + this->has_command = value != 0; break; case 3: - this->command = static_cast<enums::MediaPlayerCommand>(value.as_uint32()); + this->command = static_cast<enums::MediaPlayerCommand>(value); break; case 4: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; case 6: - this->has_media_url = value.as_bool(); + this->has_media_url = value != 0; break; case 8: - this->has_announcement = value.as_bool(); + this->has_announcement = value != 0; break; case 9: - this->announcement = value.as_bool(); + this->announcement = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2238,10 +2238,10 @@ bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_BLUETOOTH_PROXY -bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; @@ -2274,19 +2274,19 @@ uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { } return size; } -bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->request_type = static_cast<enums::BluetoothDeviceRequestType>(value.as_uint32()); + this->request_type = static_cast<enums::BluetoothDeviceRequestType>(value); break; case 3: - this->has_address_type = value.as_bool(); + this->has_address_type = value != 0; break; case 4: - this->address_type = value.as_uint32(); + this->address_type = value; break; default: return false; @@ -2307,10 +2307,10 @@ uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; default: return false; @@ -2413,13 +2413,13 @@ uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { size += ProtoSize::calc_uint64(1, this->address); return size; } -bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; @@ -2438,16 +2438,16 @@ uint32_t BluetoothGATTReadResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->response = value.as_bool(); + this->response = value != 0; break; default: return false; @@ -2466,26 +2466,26 @@ bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDeli } return true; } -bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; } return true; } -bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; @@ -2504,16 +2504,16 @@ bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, Proto } return true; } -bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->enable = value.as_bool(); + this->enable = value != 0; break; default: return false; @@ -2632,10 +2632,10 @@ uint32_t BluetoothScannerStateResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->configured_mode)); return size; } -bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->mode = static_cast<enums::BluetoothScannerMode>(value.as_uint32()); + this->mode = static_cast<enums::BluetoothScannerMode>(value); break; default: return false; @@ -2644,13 +2644,13 @@ bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarIn } #endif #ifdef USE_VOICE_ASSISTANT -bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->subscribe = value.as_bool(); + this->subscribe = value != 0; break; case 2: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; @@ -2685,13 +2685,13 @@ uint32_t VoiceAssistantRequest::calculate_size() const { size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); return size; } -bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->port = value.as_uint32(); + this->port = value; break; case 2: - this->error = value.as_bool(); + this->error = value != 0; break; default: return false; @@ -2713,10 +2713,10 @@ bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimi } return true; } -bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast<enums::VoiceAssistantEvent>(value.as_uint32()); + this->event_type = static_cast<enums::VoiceAssistantEvent>(value); break; default: return false; @@ -2734,10 +2734,10 @@ bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDe } return true; } -bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAudio::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->end = value.as_bool(); + this->end = value != 0; break; default: return false; @@ -2766,19 +2766,19 @@ uint32_t VoiceAssistantAudio::calculate_size() const { size += ProtoSize::calc_bool(1, this->end); return size; } -bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast<enums::VoiceAssistantTimerEvent>(value.as_uint32()); + this->event_type = static_cast<enums::VoiceAssistantTimerEvent>(value); break; case 4: - this->total_seconds = value.as_uint32(); + this->total_seconds = value; break; case 5: - this->seconds_left = value.as_uint32(); + this->seconds_left = value; break; case 6: - this->is_active = value.as_bool(); + this->is_active = value != 0; break; default: return false; @@ -2800,10 +2800,10 @@ bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLen } return true; } -bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->start_conversation = value.as_bool(); + this->start_conversation = value != 0; break; default: return false; @@ -2853,10 +2853,10 @@ uint32_t VoiceAssistantWakeWord::calculate_size() const { } return size; } -bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 5: - this->model_size = value.as_uint32(); + this->model_size = value; break; default: return false; @@ -2990,14 +2990,14 @@ uint32_t AlarmControlPanelStateResponse::calculate_size() const { #endif return size; } -bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::AlarmControlPanelStateCommand>(value.as_uint32()); + this->command = static_cast<enums::AlarmControlPanelStateCommand>(value); break; #ifdef USE_DEVICES case 4: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3082,11 +3082,11 @@ uint32_t TextStateResponse::calculate_size() const { #endif return size; } -bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TextCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3167,20 +3167,20 @@ uint32_t DateStateResponse::calculate_size() const { #endif return size; } -bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->year = value.as_uint32(); + this->year = value; break; case 3: - this->month = value.as_uint32(); + this->month = value; break; case 4: - this->day = value.as_uint32(); + this->day = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3250,20 +3250,20 @@ uint32_t TimeStateResponse::calculate_size() const { #endif return size; } -bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->hour = value.as_uint32(); + this->hour = value; break; case 3: - this->minute = value.as_uint32(); + this->minute = value; break; case 4: - this->second = value.as_uint32(); + this->second = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3393,17 +3393,17 @@ uint32_t ValveStateResponse::calculate_size() const { #endif return size; } -bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ValveCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 4: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3472,11 +3472,11 @@ uint32_t DateTimeStateResponse::calculate_size() const { #endif return size; } -bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateTimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3561,14 +3561,14 @@ uint32_t UpdateStateResponse::calculate_size() const { #endif return size; } -bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool UpdateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast<enums::UpdateCommand>(value.as_uint32()); + this->command = static_cast<enums::UpdateCommand>(value); break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3606,10 +3606,10 @@ uint32_t ZWaveProxyFrame::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len); return size; } -bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ZWaveProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->type = static_cast<enums::ZWaveProxyRequestType>(value.as_uint32()); + this->type = static_cast<enums::ZWaveProxyRequestType>(value); break; default: return false; @@ -3672,18 +3672,18 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { } #endif #ifdef USE_IR_RF -bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 1: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 3: - this->carrier_frequency = value.as_uint32(); + this->carrier_frequency = value; break; case 4: - this->repeat_count = value.as_uint32(); + this->repeat_count = value; break; default: return false; @@ -3737,25 +3737,25 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { } #endif #ifdef USE_SERIAL_PROXY -bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->baudrate = value.as_uint32(); + this->baudrate = value; break; case 3: - this->flow_control = value.as_bool(); + this->flow_control = value != 0; break; case 4: - this->parity = static_cast<enums::SerialProxyParity>(value.as_uint32()); + this->parity = static_cast<enums::SerialProxyParity>(value); break; case 5: - this->stop_bits = value.as_uint32(); + this->stop_bits = value; break; case 6: - this->data_size = value.as_uint32(); + this->data_size = value; break; default: return false; @@ -3772,10 +3772,10 @@ uint32_t SerialProxyDataReceived::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; default: return false; @@ -3794,23 +3794,23 @@ bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimi } return true; } -bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->line_states = value.as_uint32(); + this->line_states = value; break; default: return false; } return true; } -bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; default: return false; @@ -3827,13 +3827,13 @@ uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->line_states); return size; } -bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->instance = value.as_uint32(); + this->instance = value; break; case 2: - this->type = static_cast<enums::SerialProxyRequestType>(value.as_uint32()); + this->type = static_cast<enums::SerialProxyRequestType>(value); break; default: return false; @@ -3856,22 +3856,22 @@ uint32_t SerialProxyRequestResponse::calculate_size() const { } #endif #ifdef USE_BLUETOOTH_PROXY -bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->min_interval = value.as_uint32(); + this->min_interval = value; break; case 3: - this->max_interval = value.as_uint32(); + this->max_interval = value; break; case 4: - this->latency = value.as_uint32(); + this->latency = value; break; case 5: - this->timeout = value.as_uint32(); + this->timeout = value; break; default: return false; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5c712508b9a..a4ee0adb8b5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -399,7 +399,7 @@ class HelloRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class HelloResponse final : public ProtoMessage { public: @@ -688,7 +688,7 @@ class CoverCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_FAN @@ -756,7 +756,7 @@ class FanCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LIGHT @@ -846,7 +846,7 @@ class LightCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SENSOR @@ -936,7 +936,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT_SENSOR @@ -988,7 +988,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SubscribeLogsResponse final : public ProtoMessage { public: @@ -1110,7 +1110,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_HOMEASSISTANT_STATES @@ -1176,7 +1176,7 @@ class DSTRule final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class ParsedTimezone final : public ProtoDecodableMessage { public: @@ -1190,7 +1190,7 @@ class ParsedTimezone final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class GetTimeResponse final : public ProtoDecodableMessage { public: @@ -1261,7 +1261,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class ExecuteServiceRequest final : public ProtoDecodableMessage { public: @@ -1286,7 +1286,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1365,7 +1365,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_CLIMATE @@ -1464,7 +1464,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_WATER_HEATER @@ -1528,7 +1528,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_NUMBER @@ -1584,7 +1584,7 @@ class NumberCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SELECT @@ -1636,7 +1636,7 @@ class SelectCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SIREN @@ -1696,7 +1696,7 @@ class SirenCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LOCK @@ -1752,7 +1752,7 @@ class LockCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BUTTON @@ -1785,7 +1785,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_MEDIA_PLAYER @@ -1862,7 +1862,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BLUETOOTH_PROXY @@ -1879,7 +1879,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothLERawAdvertisement final : public ProtoMessage { public: @@ -1929,7 +1929,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothDeviceConnectionResponse final : public ProtoMessage { public: @@ -1963,7 +1963,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTDescriptor final : public ProtoMessage { public: @@ -2054,7 +2054,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadResponse final : public ProtoMessage { public: @@ -2097,7 +2097,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { public: @@ -2113,7 +2113,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { public: @@ -2132,7 +2132,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { public: @@ -2149,7 +2149,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: @@ -2329,7 +2329,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_VOICE_ASSISTANT @@ -2347,7 +2347,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudioSettings final : public ProtoMessage { public: @@ -2396,7 +2396,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantEventData final : public ProtoDecodableMessage { public: @@ -2424,7 +2424,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudio final : public ProtoDecodableMessage { public: @@ -2444,7 +2444,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { public: @@ -2465,7 +2465,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { public: @@ -2484,7 +2484,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceFinished final : public ProtoMessage { public: @@ -2530,7 +2530,7 @@ class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { public: @@ -2632,7 +2632,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT @@ -2687,7 +2687,7 @@ class TextCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATE @@ -2741,7 +2741,7 @@ class DateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_TIME @@ -2795,7 +2795,7 @@ class TimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_EVENT @@ -2886,7 +2886,7 @@ class ValveCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATETIME @@ -2936,7 +2936,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_UPDATE @@ -2994,7 +2994,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_ZWAVE_PROXY @@ -3034,7 +3034,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_INFRARED @@ -3079,7 +3079,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class InfraredRFReceiveEvent final : public ProtoMessage { public: @@ -3121,7 +3121,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyDataReceived final : public ProtoMessage { public: @@ -3161,7 +3161,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { public: @@ -3177,7 +3177,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { public: @@ -3192,7 +3192,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyGetModemPinsResponse final : public ProtoMessage { public: @@ -3225,7 +3225,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SerialProxyRequestResponse final : public ProtoMessage { public: @@ -3265,7 +3265,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothSetConnectionParamsResponse final : public ProtoMessage { public: diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index fb229928e5a..4f5b3f0918f 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -20,20 +20,40 @@ void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { *this->pos_++ = static_cast<uint8_t>(value); } +ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) { + // Multi-byte varint: first byte already checked to have high bit set + uint32_t result32 = buffer[0] & 0x7F; #ifdef USE_API_VARINT64 -optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, - uint32_t result32) { + uint32_t limit = std::min(len, uint32_t(4)); +#else + uint32_t limit = std::min(len, uint32_t(5)); +#endif + for (uint32_t i = 1; i < limit; i++) { + uint8_t val = buffer[i]; + result32 |= uint32_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + return {result32, i + 1}; + } + } +#ifdef USE_API_VARINT64 + return parse_wide(buffer, len, result32); +#else + return {0, PROTO_VARINT_PARSE_FAILED}; +#endif +} + +#ifdef USE_API_VARINT64 +ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) { uint64_t result64 = result32; uint32_t limit = std::min(len, uint32_t(10)); for (uint32_t i = 4; i < limit; i++) { uint8_t val = buffer[i]; result64 |= uint64_t(val & 0x7F) << (i * 7); if ((val & 0x80) == 0) { - *consumed = i + 1; - return ProtoVarInt(result64); + return {result64, i + 1}; } } - return {}; + return {0, PROTO_VARINT_PARSE_FAILED}; } #endif @@ -43,18 +63,16 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header (tag) - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header (tag) - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { break; // Invalid data, stop counting } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast<uint32_t>(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; // Count if this is the target field if (field_id == target_field_id) { @@ -64,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size // Skip field data based on wire type switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - parse and skip - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; // Invalid data, return what we have } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast<uint32_t>(res.value); + ptr += res.consumed; if (field_length > static_cast<size_t>(end - ptr)) { return count; // Out of bounds } @@ -190,41 +208,40 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); return; } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast<uint32_t>(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); return; } - if (!this->decode_varint(field_id, *res)) { - ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); + if (!this->decode_varint(field_id, res.value)) { + ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id, + static_cast<uint64_t>(res.value)); } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); return; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast<uint32_t>(res.value); + ptr += res.consumed; if (field_length > static_cast<size_t>(end - ptr)) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); return; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index adde0a8a85b..7050efb4460 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -98,90 +98,56 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { * within the same function scope where temporaries are created. */ -/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit +/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise +#ifdef USE_API_VARINT64 +using proto_varint_value_t = uint64_t; +#else +using proto_varint_value_t = uint32_t; +#endif + +/// Sentinel value for consumed field indicating parse failure +inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0; + +/// Result of parsing a varint: value + number of bytes consumed. +/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid). +struct ProtoVarIntResult { + proto_varint_value_t value; + uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed + + constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; } +}; + +/// Static varint parsing methods for the protobuf wire format. class ProtoVarInt { public: - ProtoVarInt() : value_(0) {} - explicit ProtoVarInt(uint64_t value) : value_(value) {} - - /// Parse a varint from buffer. consumed must be a valid pointer (not null). - static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { + /// Parse a varint from buffer. Caller must ensure len >= 1. + /// Returns result with consumed=0 on failure (truncated multi-byte varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) { #ifdef ESPHOME_DEBUG_API - assert(consumed != nullptr); + assert(len > 0); #endif - if (len == 0) - return {}; // Fast path: single-byte varints (0-127) are the most common case - // (booleans, small enums, field tags). Avoid loop overhead entirely. - if ((buffer[0] & 0x80) == 0) { - *consumed = 1; - return ProtoVarInt(buffer[0]); - } - // 32-bit phase: process remaining bytes with native 32-bit shifts. - // Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t - // shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. - // With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles - // byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). - uint32_t result32 = buffer[0] & 0x7F; -#ifdef USE_API_VARINT64 - uint32_t limit = std::min(len, uint32_t(4)); -#else - uint32_t limit = std::min(len, uint32_t(5)); -#endif - for (uint32_t i = 1; i < limit; i++) { - uint8_t val = buffer[i]; - result32 |= uint32_t(val & 0x7F) << (i * 7); - if ((val & 0x80) == 0) { - *consumed = i + 1; - return ProtoVarInt(result32); - } - } - // 64-bit phase for remaining bytes (BLE addresses etc.) -#ifdef USE_API_VARINT64 - return parse_wide(buffer, len, consumed, result32); -#else - return {}; -#endif + // (booleans, small enums, field tags, small message sizes/types). + if ((buffer[0] & 0x80) == 0) [[likely]] + return {buffer[0], 1}; + return parse_slow(buffer, len); + } + + /// Parse a varint from buffer (safe for empty buffers). + /// Returns result with consumed=0 on failure (empty buffer or truncated varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) { + if (len == 0) + return {0, PROTO_VARINT_PARSE_FAILED}; + return parse_non_empty(buffer, len); } -#ifdef USE_API_VARINT64 protected: + // Slow path for multi-byte varints (>= 128), outlined to keep fast path small + static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline)); + +#ifdef USE_API_VARINT64 /// Continue parsing varint bytes 4-9 with 64-bit arithmetic. - /// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path. - static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32) - __attribute__((noinline)); - - public: -#endif - - constexpr uint16_t as_uint16() const { return this->value_; } - constexpr uint32_t as_uint32() const { return this->value_; } - constexpr bool as_bool() const { return this->value_; } - constexpr int32_t as_int32() const { - // Not ZigZag encoded - return static_cast<int32_t>(this->value_); - } - constexpr int32_t as_sint32() const { - // with ZigZag encoding - return decode_zigzag32(static_cast<uint32_t>(this->value_)); - } -#ifdef USE_API_VARINT64 - constexpr uint64_t as_uint64() const { return this->value_; } - constexpr int64_t as_int64() const { - // Not ZigZag encoded - return static_cast<int64_t>(this->value_); - } - constexpr int64_t as_sint64() const { - // with ZigZag encoding - return decode_zigzag64(this->value_); - } -#endif - - protected: -#ifdef USE_API_VARINT64 - uint64_t value_; -#else - uint32_t value_; + static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline)); #endif }; @@ -499,7 +465,7 @@ class ProtoDecodableMessage : public ProtoMessage { protected: ~ProtoDecodableMessage() = default; - virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } + virtual bool decode_varint(uint32_t field_id, proto_varint_value_t 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; } // NOTE: decode_64bit removed - wire type 1 not supported diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 206f8f558bd..b4044c362c6 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -461,7 +461,7 @@ class FloatType(TypeInfo): class Int64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" - decode_varint = "value.as_int64()" + decode_varint = "static_cast<int64_t>(value)" encode_func = "encode_int64" wire_type = WireType.VARINT # Uses wire type 0 @@ -481,7 +481,7 @@ class Int64Type(TypeInfo): class UInt64Type(TypeInfo): cpp_type = "uint64_t" default_value = "0" - decode_varint = "value.as_uint64()" + decode_varint = "value" encode_func = "encode_uint64" wire_type = WireType.VARINT # Uses wire type 0 @@ -501,7 +501,7 @@ class UInt64Type(TypeInfo): class Int32Type(TypeInfo): cpp_type = "int32_t" default_value = "0" - decode_varint = "value.as_int32()" + decode_varint = "static_cast<int32_t>(value)" encode_func = "encode_int32" wire_type = WireType.VARINT # Uses wire type 0 @@ -573,7 +573,7 @@ class Fixed32Type(TypeInfo): class BoolType(TypeInfo): cpp_type = "bool" default_value = "false" - decode_varint = "value.as_bool()" + decode_varint = "value != 0" encode_func = "encode_bool" wire_type = WireType.VARINT # Uses wire type 0 @@ -1151,7 +1151,7 @@ class FixedArrayBytesType(TypeInfo): class UInt32Type(TypeInfo): cpp_type = "uint32_t" default_value = "0" - decode_varint = "value.as_uint32()" + decode_varint = "value" encode_func = "encode_uint32" wire_type = WireType.VARINT # Uses wire type 0 @@ -1175,7 +1175,7 @@ class EnumType(TypeInfo): @property def decode_varint(self) -> str: - return f"static_cast<{self.cpp_type}>(value.as_uint32())" + return f"static_cast<{self.cpp_type}>(value)" default_value = "" wire_type = WireType.VARINT # Uses wire type 0 @@ -1262,7 +1262,7 @@ class SFixed64Type(TypeInfo): class SInt32Type(TypeInfo): cpp_type = "int32_t" default_value = "0" - decode_varint = "value.as_sint32()" + decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" encode_func = "encode_sint32" wire_type = WireType.VARINT # Uses wire type 0 @@ -1282,7 +1282,7 @@ class SInt32Type(TypeInfo): class SInt64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" - decode_varint = "value.as_sint64()" + decode_varint = "decode_zigzag64(value)" encode_func = "encode_sint64" wire_type = WireType.VARINT # Uses wire type 0 @@ -2205,7 +2205,7 @@ def build_message_type( cpp = "" if decode_varint: - o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" + o = f"bool {desc.name}::decode_varint(uint32_t field_id, proto_varint_value_t value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_varint), " ") + "\n" o += " default: return false;\n" @@ -2213,7 +2213,7 @@ def build_message_type( o += " return true;\n" o += "}\n" cpp += o - prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" + prot = "bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;" protected_content.insert(0, prot) if decode_length: o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" From c709010c4cb04c7276f93af781dee82bf566ca85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:12 -1000 Subject: [PATCH 0539/2056] [api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill (#14593) --- esphome/components/api/api_buffer.cpp | 13 ++++ esphome/components/api/api_buffer.h | 67 +++++++++++++++++++ esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 8 +-- esphome/components/api/api_frame_helper.h | 10 +-- .../components/api/api_frame_helper_noise.cpp | 7 +- .../components/api/api_frame_helper_noise.h | 4 +- .../api/api_frame_helper_plaintext.cpp | 4 +- esphome/components/api/api_server.h | 5 +- esphome/components/api/proto.h | 10 +-- tests/components/api/test.ln882x-ard.yaml | 5 ++ 11 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 esphome/components/api/api_buffer.cpp create mode 100644 esphome/components/api/api_buffer.h create mode 100644 tests/components/api/test.ln882x-ard.yaml diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp new file mode 100644 index 00000000000..6db18b0365e --- /dev/null +++ b/esphome/components/api/api_buffer.cpp @@ -0,0 +1,13 @@ +#include "api_buffer.h" + +namespace esphome::api { + +void APIBuffer::grow_(size_t n) { + auto new_data = make_buffer(n); + if (this->size_) + std::memcpy(new_data.get(), this->data_.get(), this->size_); + this->data_ = std::move(new_data); + this->capacity_ = n; +} + +} // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h new file mode 100644 index 00000000000..00801e3ee58 --- /dev/null +++ b/esphome/components/api/api_buffer.h @@ -0,0 +1,67 @@ +#pragma once + +#include <cstdint> +#include <cstring> +#include <memory> + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Helper to use make_unique_for_overwrite where available (skips zero-fill), +/// falling back to make_unique on older GCC (ESP8266, LibreTiny). +inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) { +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + return std::make_unique<uint8_t[]>(n); +#else + return std::make_unique_for_overwrite<uint8_t[]>(n); +#endif +} + +/// Byte buffer that skips zero-initialization on resize(). +/// +/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the +/// shared protobuf write buffer, every byte is overwritten by the encoder, +/// making the zero-fill pure waste. For the receive buffer, bytes are +/// overwritten by socket reads. +/// +/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates +/// exactly the requested size (no growth factor) since callers resize to +/// known sizes rather than appending incrementally. +/// +/// Safe because: callers always write exactly the number of bytes they +/// resize for. In the protobuf write path, debug_check_bounds_ validates +/// writes in debug builds. +class APIBuffer { + public: + void clear() { this->size_ = 0; } + inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE { + if (n > this->capacity_) + this->grow_(n); + } + inline void resize(size_t n) ESPHOME_ALWAYS_INLINE { + this->reserve(n); + this->size_ = n; // no zero-fill + } + uint8_t *data() { return this->data_.get(); } + const uint8_t *data() const { return this->data_.get(); } + size_t size() const { return this->size_; } + bool empty() const { return this->size_ == 0; } + uint8_t &operator[](size_t i) { return this->data_[i]; } + const uint8_t &operator[](size_t i) const { return this->data_[i]; } + /// Release all memory (equivalent to std::vector swap trick). + void release() { + this->data_.reset(); + this->size_ = 0; + this->capacity_ = 0; + } + + protected: + void grow_(size_t n); + std::unique_ptr<uint8_t[]> data_; + size_t size_{0}; + size_t capacity_{0}; +}; + +} // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bd5d5120b8..dea3ba5460b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2016,7 +2016,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode if (total_calculated_size > remaining_size) return 0; // Doesn't fit - std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); if (conn->flags_.batch_first_message) { // First message - buffer already prepared by caller, just clear flag @@ -2184,7 +2184,7 @@ void APIConnection::process_batch_() { // Separated from process_batch_() so the single-message fast path gets a minimal // stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. -void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, +void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) { // Ensure MessageInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible<MessageInfo>::value, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index ccb51186d62..3356511684f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -288,7 +288,7 @@ class APIConnection final : public APIServerConnectionBase { } } - void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) @@ -299,7 +299,7 @@ class APIConnection final : public APIServerConnectionBase { } // Convenience overload - computes frame overhead internally - void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) { const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t footer_size = this->helper_->frame_footer_size(); this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); @@ -687,8 +687,8 @@ class APIConnection final : public APIServerConnectionBase { bool schedule_batch_(); void process_batch_(); - void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, - uint8_t footer_size) __attribute__((noinline)); + void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) + __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 151314658ea..98de24501ea 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,10 +5,10 @@ #include <memory> #include <span> #include <utility> -#include <vector> #include "esphome/core/defines.h" #ifdef USE_API +#include "esphome/components/api/api_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -178,8 +178,7 @@ class APIFrameHelper { // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { - // Use swap trick since shrink_to_fit() is non-binding and may be ignored - std::vector<uint8_t>().swap(this->rx_buf_); + this->rx_buf_.release(); } } @@ -206,9 +205,6 @@ class APIFrameHelper { // Common socket write error handling APIError handle_socket_write_error_(); - template<typename StateEnum> - APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, - const std::string &info, StateEnum &state, StateEnum failed_state); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr<socket::Socket> socket_; @@ -245,7 +241,7 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; - std::vector<uint8_t> rx_buf_; + APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 256357ce6a7..3e6ecf9dc30 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() { // During handshake, rx_buf_.size() is used in prologue construction, so // the buffer must be exactly msg_size to avoid prologue mismatch.) uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); - if (this->rx_buf_.size() != alloc_size) { - this->rx_buf_.resize(alloc_size); - } + this->rx_buf_.resize(alloc_size); if (rx_buf_len_ < msg_size) { // more data to read @@ -571,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - // Use swap idiom to actually release memory (= {} only clears size, not capacity) - std::vector<uint8_t>().swap(prologue_); + prologue_.release(); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 183b8c8a51d..83410febb26 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Reference to noise context (4 bytes on 32-bit) APINoiseContext &ctx_; - // Vector (12 bytes on 32-bit) - std::vector<uint8_t> prologue_; + // Buffer for noise handshake prologue (released after handshake) + APIBuffer prologue_; // NoiseProtocolId (size depends on implementation) NoiseProtocolId nid_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 793cece3b82..007da7ef2b1 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -165,9 +165,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Reserve space for body (+ null terminator so protobuf StringRef fields // can be safely null-terminated in-place after decode) - if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { - this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); - } + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); if (rx_buf_len_ < rx_header_parsed_len_) { // more data to read diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e5f371d8a13..69fc26cc00c 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_API +#include "api_buffer.h" #include "api_noise_context.h" #include "api_pb2.h" #include "api_pb2_service.h" @@ -65,7 +66,7 @@ class APIServer : public Component, void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections - std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; } + APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); @@ -276,7 +277,7 @@ class APIServer : public Component, // Not pre-allocated: all send paths call prepare_first_message_buffer() which // reserves the exact needed size. Pre-allocating here would cause heap fragmentation // since the buffer would almost always reallocate on first use. - std::vector<uint8_t> shared_write_buffer_; + APIBuffer shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector<HomeAssistantStateSubscription> state_subs_; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 7050efb4460..d1c955b1fb9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,6 +1,7 @@ #pragma once #include "api_pb2_defines.h" +#include "api_buffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -203,9 +204,8 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} - ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos) - : buffer_(buffer), pos_(buffer->data() + write_pos) {} + ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { if (value < 128) [[likely]] { this->debug_check_bounds_(1); @@ -340,7 +340,7 @@ class ProtoWriteBuffer { // Non-template core for encode_optional_sub_message. void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); - std::vector<uint8_t> *get_buffer() const { return buffer_; } + APIBuffer *get_buffer() const { return buffer_; } protected: // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small @@ -353,7 +353,7 @@ class ProtoWriteBuffer { void debug_check_bounds_([[maybe_unused]] size_t bytes) {} #endif - std::vector<uint8_t> *buffer_; + APIBuffer *buffer_; uint8_t *pos_; }; diff --git a/tests/components/api/test.ln882x-ard.yaml b/tests/components/api/test.ln882x-ard.yaml new file mode 100644 index 00000000000..46c01d926f2 --- /dev/null +++ b/tests/components/api/test.ln882x-ard.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 From 9dd3ec258c2f1ebf636cd7765981df2f1711c4a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:28 -1000 Subject: [PATCH 0540/2056] [scheduler] Replace unique_ptr with raw pointers, add leak detection (#14620) --- esphome/core/scheduler.cpp | 179 +++++++++++------- esphome/core/scheduler.h | 94 +++++---- .../fixtures/scheduler_bulk_cleanup.yaml | 1 + .../fixtures/scheduler_defer_cancel.yaml | 1 + .../scheduler_defer_cancels_regular.yaml | 1 + .../fixtures/scheduler_defer_fifo_simple.yaml | 1 + .../fixtures/scheduler_defer_stress.yaml | 1 + .../fixtures/scheduler_heap_stress.yaml | 1 + .../scheduler_internal_id_no_collision.yaml | 1 + .../fixtures/scheduler_null_name.yaml | 1 + .../fixtures/scheduler_numeric_id_test.yaml | 1 + .../scheduler_rapid_cancellation.yaml | 1 + .../fixtures/scheduler_recursive_timeout.yaml | 1 + .../fixtures/scheduler_removed_item_race.yaml | 1 + .../fixtures/scheduler_retry_test.yaml | 1 + .../scheduler_simultaneous_callbacks.yaml | 1 + .../fixtures/scheduler_string_lifetime.yaml | 1 + .../scheduler_string_name_stress.yaml | 1 + 18 files changed, 172 insertions(+), 117 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index ca560e8250a..63e1006b03c 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -30,11 +30,6 @@ static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; -// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines -// ~unique_ptr<SchedulerItem> (~30 bytes each) at every destruction site. Defining -// the deleter in the .cpp file ensures a single copy of the destructor + operator delete. -void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; } - #if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER) // Helper struct for formatting scheduler item names consistently in logs // Uses a stack buffer to avoid heap allocation @@ -122,8 +117,8 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id) { for (auto *container : {&this->items_, &this->to_add_}) { - for (auto &item : *container) { - if (item && this->is_item_removed_locked_(item.get()) && + for (auto *item : *container) { + if (item != nullptr && this->is_item_removed_locked_(item) && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, /* match_retry= */ true, /* skip_removed= */ false)) { return true; @@ -147,17 +142,31 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Take lock early to protect scheduler_item_pool_ access + // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check LockGuard guard{this->lock_}; + // For retries, check if there's a cancelled timeout first - before allocating an item. + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + // Skip check for defer (delay=0) - deferred retries bypass the cancellation check + if (is_retry && delay != 0 && (name_type != NameType::STATIC_STRING || static_name != nullptr) && + type == SchedulerItem::TIMEOUT && + this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { +#ifdef ESPHOME_DEBUG_SCHEDULER + SchedulerNameLog skip_name_log; + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", + skip_name_log.format(name_type, static_name, hash_or_id)); +#endif + return; + } + // Create and populate the scheduler item - auto item = this->get_item_from_pool_locked_(); + SchedulerItem *item = this->get_item_from_pool_locked_(); item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use - this->set_item_removed_(item.get(), false); + this->set_item_removed_(item, false); item->is_retry = is_retry; // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. @@ -193,29 +202,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); + this->debug_log_timer_(item, name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - - // For retries, check if there's a cancelled timeout first - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && - type == SchedulerItem::TIMEOUT && - this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { - // Skip scheduling - the retry was cancelled -#ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); -#endif - return; - } } // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } - target->push_back(std::move(item)); + target->push_back(item); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, @@ -395,7 +390,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { if (this->cleanup_() == 0) return {}; - auto &item = this->items_[0]; + SchedulerItem *item = this->items_[0]; const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) @@ -414,13 +409,13 @@ void Scheduler::full_cleanup_removed_items_() { // Compact in-place: move valid items forward, recycle removed ones size_t write = 0; for (size_t read = 0; read < this->items_.size(); ++read) { - if (!is_item_removed_locked_(this->items_[read].get())) { + if (!is_item_removed_locked_(this->items_[read])) { if (write != read) { - this->items_[write] = std::move(this->items_[read]); + this->items_[write] = this->items_[read]; } ++write; } else { - this->recycle_item_main_loop_(std::move(this->items_[read])); + this->recycle_item_main_loop_(this->items_[read]); } } this->items_.erase(this->items_.begin() + write, this->items_.end()); @@ -444,7 +439,7 @@ void Scheduler::compact_defer_queue_locked_() { // and recycled on the next loop iteration. size_t remaining = this->defer_queue_.size() - this->defer_queue_front_; for (size_t i = 0; i < remaining; i++) { - this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); + this->defer_queue_[i] = this->defer_queue_[this->defer_queue_front_ + i]; } // Use erase() instead of resize() to avoid instantiating _M_default_append // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. @@ -469,26 +464,26 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; - std::vector<SchedulerItemPtr> old_items; + std::vector<SchedulerItem *> old_items; ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), now_64); // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { - SchedulerItemPtr item; + SchedulerItem *item; { LockGuard guard{this->lock_}; item = this->pop_raw_locked_(); } SchedulerNameLog name_log; - bool is_cancelled = is_item_removed_(item.get()); + bool is_cancelled = is_item_removed_(item); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", item->get_type_str(), LOG_STR_ARG(item->get_source()), name_log.format(item->get_name_type(), item->get_name(), item->get_name_hash_or_id()), item->interval, item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); - old_items.push_back(std::move(item)); + old_items.push_back(item); } ESP_LOGD(TAG, "\n"); @@ -512,7 +507,7 @@ void HOT Scheduler::call(uint32_t now) { } while (!this->items_.empty()) { // Don't copy-by value yet - auto &item = this->items_[0]; + SchedulerItem *item = this->items_[0]; if (item->get_next_execution() > now_64) { // Not reached timeout yet, done for this call break; @@ -532,7 +527,7 @@ void HOT Scheduler::call(uint32_t now) { // Multi-threaded platforms without atomics: must take lock to safely read remove flag { LockGuard guard{this->lock_}; - if (is_item_removed_locked_(item.get())) { + if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; @@ -540,7 +535,7 @@ void HOT Scheduler::call(uint32_t now) { } #else // Single-threaded or multi-threaded with atomics: can check without lock - if (is_item_removed_(item.get())) { + if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; @@ -561,18 +556,18 @@ void HOT Scheduler::call(uint32_t now) { // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - now = this->execute_item_(item.get(), now); + now = this->execute_item_(item, now); LockGuard guard{this->lock_}; // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - auto executed_item = this->pop_raw_locked_(); + SchedulerItem *executed_item = this->pop_raw_locked_(); - if (this->is_item_removed_locked_(executed_item.get())) { + if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; - this->recycle_item_main_loop_(std::move(executed_item)); + this->recycle_item_main_loop_(executed_item); continue; } @@ -580,10 +575,10 @@ void HOT Scheduler::call(uint32_t now) { executed_item->set_next_execution(now_64 + executed_item->interval); // Add new item directly to to_add_ // since we have the lock held - this->to_add_.push_back(std::move(executed_item)); + this->to_add_.push_back(executed_item); } else { // Timeout completed - recycle it - this->recycle_item_main_loop_(std::move(executed_item)); + this->recycle_item_main_loop_(executed_item); } has_added_items |= !this->to_add_.empty(); @@ -592,17 +587,33 @@ void HOT Scheduler::call(uint32_t now) { if (has_added_items) { this->process_to_add(); } + +#ifdef ESPHOME_DEBUG_SCHEDULER + // Verify no items were leaked during this call() cycle. + // All items must be in items_, to_add_, defer_queue_, or the pool. + // Safe to check here because: + // - process_defer_queue_ has already run its cleanup_defer_queue_locked_(), + // so defer_queue_ contains no nullptr slots inflating the count. + // - The while loop above has finished, so no items are held in local variables; + // every item has been returned to a container (items_, to_add_, or pool). + // Lock needed to get a consistent snapshot of all containers. + { + LockGuard guard{this->lock_}; + this->debug_verify_no_leak_(); + } +#endif } void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; - for (auto &it : this->to_add_) { - if (is_item_removed_locked_(it.get())) { + for (auto *&it : this->to_add_) { + if (is_item_removed_locked_(it)) { // Recycle cancelled items - this->recycle_item_main_loop_(std::move(it)); + this->recycle_item_main_loop_(it); + it = nullptr; continue; } - this->items_.push_back(std::move(it)); + this->items_.push_back(it); std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); @@ -628,20 +639,18 @@ size_t HOT Scheduler::cleanup_() { // leading to race conditions LockGuard guard{this->lock_}; while (!this->items_.empty()) { - auto &item = this->items_[0]; - if (!this->is_item_removed_locked_(item.get())) + SchedulerItem *item = this->items_[0]; + if (!this->is_item_removed_locked_(item)) break; this->to_remove_--; this->recycle_item_main_loop_(this->pop_raw_locked_()); } return this->items_.size(); } -Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() { +Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Move the item out before popping - this is the item that was at the front of the heap - auto item = std::move(this->items_.back()); - + SchedulerItem *item = this->items_.back(); this->items_.pop_back(); return item; } @@ -699,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { +bool HOT Scheduler::SchedulerItem::cmp(SchedulerItem *a, SchedulerItem *b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) // Optimize for common case: check low bits first when high bits are equal return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) @@ -710,23 +719,26 @@ bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const Schedule // IMPORTANT: Caller must hold the scheduler lock before calling this function. // This protects scheduler_item_pool_ from concurrent access by other threads // that may be acquiring items from the pool in set_timer_common_(). -void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) { - if (!item) +void Scheduler::recycle_item_main_loop_(SchedulerItem *item) { + if (item == nullptr) return; if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { // Clear callback to release captured resources item->callback = nullptr; - this->scheduler_item_pool_.push_back(std::move(item)); + this->scheduler_item_pool_.push_back(item); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif } else { #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); +#endif + delete item; +#ifdef ESPHOME_DEBUG_SCHEDULER + this->debug_live_items_--; #endif } - // else: unique_ptr will delete the item when it goes out of scope } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -753,21 +765,54 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, // Helper to get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. -Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() { - SchedulerItemPtr item; +Scheduler::SchedulerItem *Scheduler::get_item_from_pool_locked_() { if (!this->scheduler_item_pool_.empty()) { - item = std::move(this->scheduler_item_pool_.back()); + SchedulerItem *item = this->scheduler_item_pool_.back(); this->scheduler_item_pool_.pop_back(); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); #endif - } else { - item = SchedulerItemPtr(new SchedulerItem()); -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Allocated new item (pool empty)"); -#endif + return item; } +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + auto *item = new SchedulerItem(); +#ifdef ESPHOME_DEBUG_SCHEDULER + this->debug_live_items_++; +#endif return item; } +#ifdef ESPHOME_DEBUG_SCHEDULER +bool Scheduler::debug_verify_no_leak_() const { + // Invariant: every live SchedulerItem must be in exactly one container. + // debug_live_items_ tracks allocations minus deletions. + size_t accounted = this->items_.size() + this->to_add_.size() + this->scheduler_item_pool_.size(); +#ifndef ESPHOME_THREAD_SINGLE + accounted += this->defer_queue_.size(); +#endif + if (accounted != this->debug_live_items_) { + ESP_LOGE(TAG, + "SCHEDULER LEAK DETECTED: live=%" PRIu32 " but accounted=%" PRIu32 " (items=%" PRIu32 " to_add=%" PRIu32 + " pool=%" PRIu32 +#ifndef ESPHOME_THREAD_SINGLE + " defer=%" PRIu32 +#endif + ")", + static_cast<uint32_t>(this->debug_live_items_), static_cast<uint32_t>(accounted), + static_cast<uint32_t>(this->items_.size()), static_cast<uint32_t>(this->to_add_.size()), + static_cast<uint32_t>(this->scheduler_item_pool_.size()) +#ifndef ESPHOME_THREAD_SINGLE + , + static_cast<uint32_t>(this->defer_queue_.size()) +#endif + ); + assert(false); + return false; + } + return true; +} +#endif + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index eb6cea4f37d..0476513bb99 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -2,7 +2,6 @@ #include "esphome/core/defines.h" #include <cstring> -#include <memory> #include <string> #include <vector> #ifdef ESPHOME_THREAD_MULTI_ATOMICS @@ -144,19 +143,6 @@ class Scheduler { }; protected: - struct SchedulerItem; - - // Custom deleter for SchedulerItem unique_ptr that prevents the compiler from - // inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC - // inlines ~unique_ptr<SchedulerItem> (~30 bytes: null check + ~std::function + - // operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline - // it into a single helper. This noinline deleter ensures only one copy exists. - // operator() is defined in scheduler.cpp to prevent inlining. - struct SchedulerItemDeleter { - void operator()(SchedulerItem *ptr) const noexcept; - }; - using SchedulerItemPtr = std::unique_ptr<SchedulerItem, SchedulerItemDeleter>; - struct SchedulerItem { // Ordered by size to minimize padding Component *component; @@ -219,14 +205,14 @@ class Scheduler { name_.static_name = nullptr; } - // Destructor - no dynamic memory to clean up + // Destructor - no dynamic memory to clean up (callback's std::function handles its own) ~SchedulerItem() = default; // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; SchedulerItem &operator=(const SchedulerItem &) = delete; - // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly + // Delete move operations: SchedulerItem objects are managed via raw pointers, never moved directly SchedulerItem(SchedulerItem &&) = delete; SchedulerItem &operator=(SchedulerItem &&) = delete; @@ -250,7 +236,7 @@ class Scheduler { name_type_ = type; } - static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b); + static bool cmp(SchedulerItem *a, SchedulerItem *b); // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. // The upper 16 bits of the 64-bit value are always zero, which is fine since @@ -301,12 +287,13 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - // Remove and return the front item from the heap + // Remove and return the front item from the heap as a raw pointer. + // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - SchedulerItemPtr pop_raw_locked_(); + SchedulerItem *pop_raw_locked_(); // Get or create a scheduler item from the pool // IMPORTANT: Caller must hold the scheduler lock before calling this function. - SchedulerItemPtr get_item_from_pool_locked_(); + SchedulerItem *get_item_from_pool_locked_(); private: // Helper to cancel items - must be called with lock held @@ -330,19 +317,16 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held - inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type, + inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded - // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. - // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check - // provides defense-in-depth: helper - // functions should be safe regardless of caller behavior. + // platforms, items can be nulled in defer_queue_ during processing. // Fixes: https://github.com/esphome/esphome/issues/11940 - if (!item) + if (item == nullptr) return false; - if (item->component != component || item->type != type || - (skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) { + if (item->component != component || item->type != type || (skip_removed && this->is_item_removed_locked_(item)) || + (match_retry && !item->is_retry)) { return false; } // Name type must match @@ -364,10 +348,12 @@ class Scheduler { } // Helper to recycle a SchedulerItem back to the pool. + // Takes a raw pointer — caller transfers ownership. The item is either added to the + // pool or deleted if the pool is full. // IMPORTANT: Only call from main loop context! Recycling clears the callback, // so calling from another thread while the callback is executing causes use-after-free. // IMPORTANT: Caller must hold the scheduler lock before calling this function. - void recycle_item_main_loop_(SchedulerItemPtr item); + void recycle_item_main_loop_(SchedulerItem *item); // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); @@ -423,27 +409,28 @@ class Scheduler { // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), // recycle each item after re-acquiring the lock for the next iteration (N+1 total). // The lock is held across: recycle → loop condition → move-out, then released for execution. - SchedulerItemPtr item; + SchedulerItem *item; this->lock_.lock(); while (this->defer_queue_front_ < defer_queue_end) { - // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. - // This is intentional and safe because: + // Take ownership of the item, leaving nullptr in the vector slot. + // This is safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = std::move(this->defer_queue_[this->defer_queue_front_]); + item = this->defer_queue_[this->defer_queue_front_]; + this->defer_queue_[this->defer_queue_front_] = nullptr; this->defer_queue_front_++; this->lock_.unlock(); // Execute callback without holding lock to prevent deadlocks // if the callback tries to call defer() again - if (!this->should_skip_item_(item.get())) { - now = this->execute_item_(item.get(), now); + if (!this->should_skip_item_(item)) { + now = this->execute_item_(item, now); } this->lock_.lock(); - this->recycle_item_main_loop_(std::move(item)); + this->recycle_item_main_loop_(item); } // Clean up the queue (lock already held from last recycle or initial acquisition) this->cleanup_defer_queue_locked_(); @@ -523,18 +510,14 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItemPtr> &container, + __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { size_t count = 0; - for (auto &item : container) { - // Skip nullptr items (can happen in defer_queue_ when items are being processed) - // The defer_queue_ uses index-based processing: items are std::moved out but left in the - // vector as nullptr until cleanup. Even though this function is called with lock held, - // the vector can still contain nullptr items from the processing loop. This check prevents crashes. - if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { - this->set_item_removed_(item.get(), true); + for (auto *item : container) { + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + this->set_item_removed_(item, true); count++; } } @@ -542,15 +525,15 @@ class Scheduler { } Mutex lock_; - std::vector<SchedulerItemPtr> items_; - std::vector<SchedulerItemPtr> to_add_; + std::vector<SchedulerItem *> items_; + std::vector<SchedulerItem *> to_add_; #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop - std::vector<SchedulerItemPtr> defer_queue_; // FIFO queue for defer() calls - size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls + size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; // Memory pool for recycling SchedulerItem objects to reduce heap churn. @@ -561,7 +544,18 @@ class Scheduler { // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation // can stall the entire system, causing timing issues and dropped events for any components that need // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) - std::vector<SchedulerItemPtr> scheduler_item_pool_; + std::vector<SchedulerItem *> scheduler_item_pool_; + +#ifdef ESPHOME_DEBUG_SCHEDULER + // Leak detection: tracks total live SchedulerItem allocations. + // Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_.size() + // Verified periodically in call() to catch leaks early. + size_t debug_live_items_{0}; + + // Verify the scheduler memory invariant: all allocated items are accounted for. + // Returns true if no leak detected. Logs an error and asserts on failure. + bool debug_verify_no_leak_() const; +#endif }; } // namespace esphome diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml index de876da8c47..3d2c47a0de5 100644 --- a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml +++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-bulk-cleanup external_components: diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml index 9e3f927c33c..92ae0062aca 100644 --- a/tests/integration/fixtures/scheduler_defer_cancel.yaml +++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-cancel host: diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml index fb6b1791dc4..cf7f6ec7338 100644 --- a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml +++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-cancel-regular host: diff --git a/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml index 7384082ac2d..f69e5c6c67b 100644 --- a/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml +++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-fifo-simple host: diff --git a/tests/integration/fixtures/scheduler_defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml index 0d9c1d14051..70eac01daf6 100644 --- a/tests/integration/fixtures/scheduler_defer_stress.yaml +++ b/tests/integration/fixtures/scheduler_defer_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-defer-stress-test external_components: diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml index d4d340b68ba..486a5d12764 100644 --- a/tests/integration/fixtures/scheduler_heap_stress.yaml +++ b/tests/integration/fixtures/scheduler_heap_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-heap-stress-test external_components: diff --git a/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml index 46dbb8e728d..e696e99efa6 100644 --- a/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml +++ b/tests/integration/fixtures/scheduler_internal_id_no_collision.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-internal-id-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_null_name.yaml b/tests/integration/fixtures/scheduler_null_name.yaml index 42eaacdd439..d5488761d68 100644 --- a/tests/integration/fixtures/scheduler_null_name.yaml +++ b/tests/integration/fixtures/scheduler_null_name.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-null-name host: diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index 1669f026f5c..25decf20f51 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-numeric-id-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml index 4824654c5c6..530b8241f58 100644 --- a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml +++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-rapid-cancel-test external_components: diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml index f1168802f6e..66b6f4b19bb 100644 --- a/tests/integration/fixtures/scheduler_recursive_timeout.yaml +++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-recursive-timeout external_components: diff --git a/tests/integration/fixtures/scheduler_removed_item_race.yaml b/tests/integration/fixtures/scheduler_removed_item_race.yaml index 2f8a7fb987b..55d2197d7ce 100644 --- a/tests/integration/fixtures/scheduler_removed_item_race.yaml +++ b/tests/integration/fixtures/scheduler_removed_item_race.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-removed-item-race host: diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml index ffe9082a69f..cdf71152bdc 100644 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-retry-test on_boot: priority: -100 diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml index 446ee7fdc0e..c15edc3ffd5 100644 --- a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml +++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-simul-callbacks-test external_components: diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml index ebd5052b8bf..5ae5a1914e7 100644 --- a/tests/integration/fixtures/scheduler_string_lifetime.yaml +++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: scheduler-string-lifetime-test external_components: diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml index d1ef55c8d5f..8f68d1d1023 100644 --- a/tests/integration/fixtures/scheduler_string_name_stress.yaml +++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml @@ -1,4 +1,5 @@ esphome: + debug_scheduler: true # Enable scheduler leak detection name: sched-string-name-stress external_components: From 89bb5d9e42fcc911a73af225af78390a96ad8e5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:11:45 -1000 Subject: [PATCH 0541/2056] [core] Require explicit synchronous= for register_action (#14606) --- esphome/automation.py | 38 ++++++++++--- esphome/components/ags10/sensor.py | 2 + esphome/components/aic3204/audio_dac.py | 5 +- .../alarm_control_panel/__init__.py | 40 +++++++++++--- esphome/components/animation/__init__.py | 12 +++-- esphome/components/api/__init__.py | 1 + esphome/components/at581x/__init__.py | 2 + esphome/components/audio_adc/__init__.py | 5 +- esphome/components/audio_dac/__init__.py | 13 +++-- esphome/components/binary_sensor/__init__.py | 1 + esphome/components/bl0906/sensor.py | 1 + esphome/components/ble_client/__init__.py | 22 ++++++-- esphome/components/bm8563/time.py | 3 ++ esphome/components/canbus/__init__.py | 1 + esphome/components/cc1101/__init__.py | 27 +++++++--- esphome/components/climate/__init__.py | 5 +- esphome/components/cm1106/sensor.py | 1 + esphome/components/cs5460a/sensor.py | 1 + esphome/components/datetime/__init__.py | 3 ++ esphome/components/deep_sleep/__init__.py | 7 ++- esphome/components/dfplayer/__init__.py | 16 ++++++ .../components/dfrobot_sen0395/__init__.py | 2 + esphome/components/display/__init__.py | 3 ++ .../components/display_menu_base/__init__.py | 33 +++++++++--- esphome/components/ds1307/time.py | 2 + esphome/components/duty_time/sensor.py | 12 +++-- esphome/components/esp32_ble/__init__.py | 8 ++- .../components/esp32_ble_server/__init__.py | 3 ++ .../components/esp32_ble_tracker/__init__.py | 2 + esphome/components/esp8266_pwm/output.py | 1 + esphome/components/esp_ldo/__init__.py | 1 + esphome/components/espnow/__init__.py | 5 ++ esphome/components/event/__init__.py | 4 +- esphome/components/ezo_pmp/__init__.py | 28 ++++++++-- esphome/components/fan/__init__.py | 1 + .../components/fingerprint_grow/__init__.py | 6 +++ .../components/grove_tb6612fng/__init__.py | 6 +++ esphome/components/haier/climate.py | 49 +++++++++++++---- esphome/components/hbridge/fan/__init__.py | 1 + esphome/components/hc8/sensor.py | 5 +- esphome/components/hdc302x/sensor.py | 10 +++- esphome/components/hlk_fm22x/__init__.py | 5 ++ esphome/components/http_request/__init__.py | 15 ++++-- .../components/http_request/ota/__init__.py | 1 + esphome/components/htu21d/sensor.py | 2 + esphome/components/hub75/display.py | 1 + esphome/components/integration/sensor.py | 2 + esphome/components/key_collector/__init__.py | 2 + esphome/components/ld2410/__init__.py | 5 +- esphome/components/ledc/output.py | 1 + esphome/components/libretiny_pwm/output.py | 1 + esphome/components/light/automation.py | 5 +- esphome/components/lightwaverf/__init__.py | 1 + esphome/components/lock/__init__.py | 12 +++-- esphome/components/logger/__init__.py | 1 + esphome/components/lvgl/automation.py | 27 ++++++++-- esphome/components/lvgl/styles.py | 1 + esphome/components/lvgl/types.py | 1 + esphome/components/lvgl/widgets/animimg.py | 2 + .../components/lvgl/widgets/buttonmatrix.py | 1 + esphome/components/lvgl/widgets/canvas.py | 8 +++ esphome/components/lvgl/widgets/meter.py | 1 + esphome/components/lvgl/widgets/page.py | 3 ++ esphome/components/lvgl/widgets/spinbox.py | 2 + esphome/components/lvgl/widgets/tabview.py | 1 + esphome/components/lvgl/widgets/tileview.py | 1 + esphome/components/max17043/sensor.py | 4 +- esphome/components/max6956/__init__.py | 2 + esphome/components/max7219digit/display.py | 35 +++++++++--- esphome/components/media_player/__init__.py | 7 ++- esphome/components/mhz19/sensor.py | 20 +++++-- .../components/micro_wake_word/__init__.py | 13 ++++- esphome/components/microphone/__init__.py | 22 +++++--- esphome/components/midea/climate.py | 4 +- esphome/components/mixer/speaker/__init__.py | 1 + esphome/components/mqtt/__init__.py | 2 + esphome/components/nau7802/sensor.py | 3 ++ .../nextion/binary_sensor/__init__.py | 1 + esphome/components/nextion/display.py | 1 + esphome/components/nextion/sensor/__init__.py | 1 + esphome/components/nextion/switch/__init__.py | 1 + .../nextion/text_sensor/__init__.py | 1 + esphome/components/online_image/__init__.py | 9 +++- esphome/components/output/__init__.py | 2 + esphome/components/pcf85063/time.py | 2 + esphome/components/pcf8563/time.py | 2 + esphome/components/pid/climate.py | 3 ++ .../components/pipsolar/output/__init__.py | 1 + esphome/components/pmwcs3/sensor.py | 3 ++ esphome/components/pn7150/__init__.py | 43 +++++++++++---- esphome/components/pn7160/__init__.py | 43 +++++++++++---- esphome/components/pulse_counter/sensor.py | 1 + esphome/components/pulse_meter/sensor.py | 1 + esphome/components/pzemac/sensor.py | 1 + esphome/components/pzemdc/sensor.py | 1 + esphome/components/remote_base/__init__.py | 5 +- .../components/remote_transmitter/__init__.py | 5 +- esphome/components/rf_bridge/__init__.py | 22 ++++++-- esphome/components/rotary_encoder/sensor.py | 1 + esphome/components/rp2040_pwm/output.py | 1 + esphome/components/rtttl/__init__.py | 2 + esphome/components/rx8130/time.py | 2 + esphome/components/safe_mode/__init__.py | 1 + esphome/components/scd30/sensor.py | 1 + esphome/components/scd4x/sensor.py | 6 ++- esphome/components/script/__init__.py | 1 + esphome/components/sen5x/sensor.py | 5 +- esphome/components/senseair/sensor.py | 17 ++++-- esphome/components/servo/__init__.py | 2 + esphome/components/sim800l/__init__.py | 16 ++++-- esphome/components/sound_level/sensor.py | 8 ++- esphome/components/speaker/__init__.py | 23 +++++--- .../speaker/media_player/__init__.py | 1 + esphome/components/sprinkler/__init__.py | 53 +++++++++++++++---- esphome/components/sps30/sensor.py | 15 ++++-- esphome/components/stepper/__init__.py | 5 ++ esphome/components/sx126x/__init__.py | 30 ++++++++--- esphome/components/sx127x/__init__.py | 30 ++++++++--- .../template/binary_sensor/__init__.py | 1 + esphome/components/template/cover/__init__.py | 1 + esphome/components/template/lock/__init__.py | 1 + .../components/template/sensor/__init__.py | 1 + .../components/template/switch/__init__.py | 1 + .../template/text_sensor/__init__.py | 1 + esphome/components/template/valve/__init__.py | 1 + .../template/water_heater/__init__.py | 1 + esphome/components/tm1651/__init__.py | 12 ++++- esphome/components/uart/__init__.py | 1 + esphome/components/udp/__init__.py | 1 + esphome/components/ufire_ec/sensor.py | 2 + esphome/components/ufire_ise/sensor.py | 3 ++ esphome/components/update/__init__.py | 2 + esphome/components/valve/__init__.py | 20 +++++-- .../components/voice_assistant/__init__.py | 6 ++- esphome/components/wifi/__init__.py | 9 +++- esphome/components/wireguard/__init__.py | 2 + esphome/components/zigbee/__init__.py | 1 + 137 files changed, 852 insertions(+), 187 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index d9b8b2ec574..36ab30b654a 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -57,22 +59,41 @@ def maybe_conf(conf, *validators): return validate +_LOGGER = logging.getLogger(__name__) + + def register_action( name: str, action_type: MockObjClass, schema: cv.Schema, *, - synchronous: bool = False, + synchronous: bool | None = None, ): """Register an action type. - Actions default to ``synchronous=False`` (safe default), meaning string - arguments use owning std::string to prevent dangling references. + All callers must pass ``synchronous`` explicitly. - Set ``synchronous=True`` only for actions that complete synchronously - and never store trigger arguments for later execution. This allows - the code generator to use non-owning StringRef for zero-copy access. + ``synchronous=True`` — the action never defers ``play_next_()`` to a + later point (callback, timer, or ``loop()``). Trigger arguments are + only used during the initial call, so string args can use non-owning + StringRef for zero-copy access. + + ``synchronous=False`` — the action defers ``play_next_()`` via a + callback, timer, or ``Component::loop()``. Trigger arguments must + outlive the initial call, so string args use owning std::string to + prevent dangling references. """ + if synchronous is None: + _LOGGER.warning( + "register_action('%s', ...) is missing the synchronous= parameter. " + "Defaulting to synchronous=False (safe but prevents StringRef " + "optimization). Check the C++ class: use synchronous=False if " + "play_next_() is deferred to a callback, timer, or loop(); " + "use synchronous=True if play_next_() always runs before the " + "initial play/play_complex call returns", + name, + ) + synchronous = False return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) @@ -353,6 +374,7 @@ async def component_is_idle_condition_to_code( "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds), + synchronous=False, ) async def delay_action_to_code( config: ConfigType, @@ -465,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value( ) -@register_action("wait_until", WaitUntilAction, _validate_wait_until) +@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False) async def wait_until_action_to_code( config: ConfigType, action_id: ID, @@ -611,7 +633,7 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool: Non-synchronous actions (delay, wait_until, script.wait, etc.) store trigger args for later execution, making non-owning types like StringRef - unsafe. Actions that haven't been audited default to non-synchronous. + unsafe. """ if isinstance(actions, list): return any(has_non_synchronous_actions(item) for item in actions) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 8f0f3729516..4cfa9e67ec1 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( "ags10.new_i2c_address", AGS10NewI2cAddressAction, AGS10_NEW_I2C_ADDRESS_SCHEMA, + synchronous=True, ) async def ags10newi2caddress_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( "ags10.set_zero_point", AGS10SetZeroPointAction, AGS10_SET_ZERO_POINT_SCHEMA, + synchronous=True, ) async def ags10setzeropoint_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index da7a54df54d..a644638f696 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA + "aic3204.set_auto_mute_mode", + SetAutoMuteAction, + SET_AUTO_MUTE_ACTION_SCHEMA, + synchronous=True, ) async def aic3204_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index b8555861527..aefb18d25cc 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args): @automation.register_action( - "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_away", + ArmAwayAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_home", + ArmHomeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_night", + ArmNightAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.disarm", + DisarmAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_disarm_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.pending", + PendingAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_pending_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.triggered", + TriggeredAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_trigger_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.chime", + ChimeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_chime_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.ready", + ReadyAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) @automation.register_condition( "alarm_control_panel.ready", diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index c4ac7adb236..e9630f5266b 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema( ) -@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) -@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) -@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) +@automation.register_action( + "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True +) async def animation_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index c7dec6e78be..9772e6afca7 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -712,6 +712,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All( "api.respond", APIRespondAction, API_RESPOND_ACTION_SCHEMA, + synchronous=True, ) async def api_respond_to_code( config: ConfigType, diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 117ada123db..0780814ea6e 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio cv.Required(CONF_ID): cv.use_id(AT581XComponent), } ), + synchronous=True, ) async def at581x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema( "at581x.settings", AT581XSettingsAction, RADAR_SETTINGS_SCHEMA, + synchronous=True, ) async def at581x_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 2f95a039f5c..3c9b32e6107 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA + "audio_adc.set_mic_gain", + SetMicGainAction, + SET_MIC_GAIN_ACTION_SCHEMA, + synchronous=True, ) async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 92e6cb18fa0..a950c1967bf 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( ) -@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) -@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) +@automation.register_action( + "audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True +) async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA + "audio_dac.set_volume", + SetVolumeAction, + SET_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1f641185602..37cccc01be6 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -685,6 +685,7 @@ async def to_code(config): }, key=CONF_ID, ), + synchronous=True, ) async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 42c6f06092b..059e10e962d 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -143,6 +143,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( cv.Required(CONF_ID): cv.use_id(BL0906), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 37db181584b..56ac2ea1472 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -172,7 +172,10 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.disconnect", + BLEDisconnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_disconnect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -180,7 +183,10 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.connect", + BLEConnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_connect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -188,7 +194,10 @@ async def ble_connect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA + "ble_client.ble_write", + BLEWriteAction, + BLE_WRITE_ACTION_SCHEMA, + synchronous=False, ) async def ble_write_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -247,6 +256,7 @@ async def ble_write_to_code(config, action_id, template_arg, args): "ble_client.numeric_comparison_reply", BLENumericComparisonReplyAction, BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -263,7 +273,10 @@ async def numeric_comparison_reply_to_code(config, action_id, template_arg, args @automation.register_action( - "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA + "ble_client.passkey_reply", + BLEPasskeyReplyAction, + BLE_PASSKEY_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def passkey_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -283,6 +296,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): "ble_client.remove_bond", BLERemoveBondAction, BLE_REMOVE_BOND_ACTION_SCHEMA, + synchronous=True, ) async def remove_bond_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index 2785315af27..ba264f00bfe 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -33,6 +33,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -49,6 +50,7 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args): cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), } ), + synchronous=True, ) async def bm8563_start_timer_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -66,6 +68,7 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 7b51c2c45c1..c94c8647a95 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -155,6 +155,7 @@ async def register_canbus(var, config): validate_id, key=CONF_DATA, ), + synchronous=True, ) async def canbus_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index e2e5986daff..27092908621 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -287,10 +287,18 @@ CC1101_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA) +@automation.register_action( + "cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True +) async def cc1101_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -317,7 +325,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "cc1101.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -419,9 +430,9 @@ def _register_setter_actions(): cg.add(getattr(var, _setter)(_map[data] if _map else data)) return var - automation.register_action(f"cc1101.{setter_name}", action_cls, schema)( - _setter_action_to_code - ) + automation.register_action( + f"cc1101.{setter_name}", action_cls, schema, synchronous=True + )(_setter_action_to_code) _register_setter_actions() diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index f5b91c502c7..8cf5fa9b0c2 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -476,7 +476,10 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA + "climate.control", + ControlAction, + CLIMATE_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 1d95bcc6665..3c82fac977f 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -65,6 +65,7 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( "cm1106.calibrate_zero", CM1106CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: """Service code generation entry point.""" diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index 07b5ea1c635..d2383bd01b8 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -132,6 +132,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(CS5460AComponent), } ), + synchronous=True, ) async def restart_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 74c9d594f75..90835624bf1 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -187,6 +187,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def datetime_date_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -218,6 +219,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def datetime_time_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -249,6 +251,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ), }, ), + synchronous=True, ) async def datetime_datetime_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 3cfe7aa6417..4098fd3fb8b 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -405,7 +405,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( @automation.register_action( - "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA + "deep_sleep.enter", + EnterDeepSleepAction, + DEEP_SLEEP_ENTER_SCHEMA, + synchronous=True, ) async def deep_sleep_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -428,11 +431,13 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): "deep_sleep.prevent", PreventDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) @automation.register_action( "deep_sleep.allow", AllowDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) async def deep_sleep_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 53ebda6bcc8..9df108c9c0a 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -91,6 +91,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_next_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -106,6 +107,7 @@ async def dfplayer_next_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_previous_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -123,6 +125,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -143,6 +146,7 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +170,7 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args): cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -191,6 +196,7 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): }, key=CONF_DEVICE, ), + synchronous=True, ) async def dfplayer_set_device_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +216,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args): }, key=CONF_VOLUME, ), + synchronous=True, ) async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -227,6 +234,7 @@ async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -242,6 +250,7 @@ async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -259,6 +268,7 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): }, key=CONF_EQ_PRESET, ), + synchronous=True, ) async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -276,6 +286,7 @@ async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_sleep_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -291,6 +302,7 @@ async def dfplayer_sleep_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -306,6 +318,7 @@ async def dfplayer_reset_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -321,6 +334,7 @@ async def dfplayer_start_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_pause_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -336,6 +350,7 @@ async def dfplayer_pause_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -351,6 +366,7 @@ async def dfplayer_stop_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_random_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index d54b147036e..ba77e56abb0 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -52,6 +52,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DfrobotSen0395Component), } ), + synchronous=True, ) async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -151,6 +152,7 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( "dfrobot_sen0395.settings", DfrobotSen0395SettingsAction, MMWAVE_SETTINGS_SCHEMA, + synchronous=True, ) async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 695e7cde476..6367f88acc4 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -159,6 +159,7 @@ async def register_display(var, config): cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)), } ), + synchronous=True, ) async def display_page_show_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -179,6 +180,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_next_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -193,6 +195,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_previous_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 658292ec7a3..c9a0c7ee93e 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -294,50 +294,67 @@ MENU_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_up_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_down_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_left_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_right_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_show_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_hide_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA + "display_menu.show_main", + ShowMainAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) async def menu_show_main_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py index 42b7184db9b..0e7bb976a2d 100644 --- a/esphome/components/ds1307/time.py +++ b/esphome/components/ds1307/time.py @@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -42,6 +43,7 @@ async def ds1307_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 1907b3fcfec..456859f8e49 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -90,21 +90,27 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( ) -@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 8b368afc2e2..43208eb87eb 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -604,11 +604,15 @@ async def ble_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) -@automation.register_action("ble.enable", BLEEnableAction, cv.Schema({})) +@automation.register_action( + "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True +) async def ble_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) -@automation.register_action("ble.disable", BLEDisableAction, cv.Schema({})) +@automation.register_action( + "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True +) async def ble_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index b08e791e7e3..827ddba9554 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -622,6 +622,7 @@ async def to_code(config): ), validate_set_value_action, ), + synchronous=True, ) async def ble_server_characteristic_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -641,6 +642,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a cv.Required(CONF_VALUE): value_schema(), } ), + synchronous=True, ) async def ble_server_descriptor_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -662,6 +664,7 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ), validate_notify_action, ), + synchronous=True, ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 37e74672ed8..c5e8f3178d0 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -373,6 +373,7 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( "esp32_ble_tracker.start_scan", ESP32BLEStartScanAction, ESP32_BLE_START_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_start_scan_action_to_code( config, action_id, template_arg, args @@ -396,6 +397,7 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( "esp32_ble_tracker.stop_scan", ESP32BLEStopScanAction, ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_stop_scan_action_to_code( config, action_id, template_arg, args diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index a78831c516c..b9b6dcc95a3 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -57,6 +57,7 @@ async def to_code(config) -> None: cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index 5235a9411ee..a489651b598 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -129,6 +129,7 @@ def adjusted_ldo_id(value): ), } ), + synchronous=True, ) async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index faeccd910e0..d1a85ae8fd8 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -220,6 +220,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) "espnow.send", SendAction, SEND_SCHEMA, + synchronous=False, ) @automation.register_action( "espnow.broadcast", @@ -232,6 +233,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) ), key=CONF_DATA, ), + synchronous=False, ) async def send_action( config: ConfigType, @@ -271,6 +273,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) @automation.register_action( "espnow.peer.delete", @@ -279,6 +282,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) async def peer_action( config: ConfigType, @@ -303,6 +307,7 @@ async def peer_action( }, key=CONF_CHANNEL, ), + synchronous=True, ) async def channel_action( config: ConfigType, diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 14cc1505ad8..300902b8cad 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -129,7 +129,9 @@ TRIGGER_EVENT_SCHEMA = cv.Schema( ) -@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA) +@automation.register_action( + "event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True +) async def event_fire_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index c1f72bb05d5..1538e303f12 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -81,7 +81,10 @@ EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( @automation.register_action( - "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.find", + EzoPMPFindAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_find_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -92,6 +95,7 @@ async def ezo_pmp_find_to_code(config, action_id, template_arg, args): "ezo_pmp.dose_continuously", EzoPMPDoseContinuouslyAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -102,6 +106,7 @@ async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, arg "ezo_pmp.clear_total_volume_dosed", EzoPMPClearTotalVolumeDispensedAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_total_volume_dosed_to_code( config, action_id, template_arg, args @@ -114,6 +119,7 @@ async def ezo_pmp_clear_total_volume_dosed_to_code( "ezo_pmp.clear_calibration", EzoPMPClearCalibrationAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -121,7 +127,10 @@ async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, arg @automation.register_action( - "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.pause_dosing", + EzoPMPPauseDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -129,7 +138,10 @@ async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): @automation.register_action( - "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.stop_dosing", + EzoPMPStopDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -149,7 +161,10 @@ EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( @automation.register_action( - "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA + "ezo_pmp.dose_volume", + EzoPMPDoseVolumeAction, + EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -178,6 +193,7 @@ EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_volume_over_time", EzoPMPDoseVolumeOverTimeAction, EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -209,6 +225,7 @@ EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_with_constant_flow_rate", EzoPMPDoseWithConstantFlowRateAction, EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_with_constant_flow_rate_to_code( config, action_id, template_arg, args @@ -239,6 +256,7 @@ EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( "ezo_pmp.set_calibration_volume", EzoPMPSetCalibrationVolumeAction, EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -262,6 +280,7 @@ EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( "ezo_pmp.change_i2c_address", EzoPMPChangeI2CAddressAction, EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -285,6 +304,7 @@ EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( "ezo_pmp.arbitrary_command", EzoPMPArbitraryCommandAction, EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index da28c577c8e..df71c6ab3fd 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -365,6 +365,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean, } ), + synchronous=True, ) async def fan_cycle_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 115b89433bc..2637097be81 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -261,6 +261,7 @@ async def to_code(config): }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -282,6 +283,7 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -299,6 +301,7 @@ async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -317,6 +320,7 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -337,6 +341,7 @@ FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( "fingerprint_grow.led_control", LEDControlAction, FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -359,6 +364,7 @@ async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), } ), + synchronous=True, ) async def fingerprint_grow_aura_led_control_to_code( config, action_id, template_arg, args diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 869c05387ff..210e2f7babb 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -72,6 +72,7 @@ async def to_code(config): cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), } ), + synchronous=True, ) async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -96,6 +97,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -115,6 +117,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -133,6 +136,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -149,6 +153,7 @@ async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args) cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +171,7 @@ async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, ar cv.Required(CONF_ADDRESS): cv.i2c_address, } ), + synchronous=True, ) async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 6c208f6caa4..caaaa18dd68 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -319,10 +319,16 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_on", + DisplayOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_off", + DisplayOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def display_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -330,10 +336,16 @@ async def display_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_on", + BeeperOnAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_off", + BeeperOffAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def beeper_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -345,11 +357,13 @@ async def beeper_action_to_code(config, action_id, template_arg, args): "climate.haier.start_self_cleaning", StartSelfCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "climate.haier.start_steri_cleaning", StartSteriCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def start_cleaning_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -368,6 +382,7 @@ async def start_cleaning_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -391,6 +406,7 @@ async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, ar ), } ), + synchronous=True, ) async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -403,10 +419,16 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, @automation.register_action( - "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_on", + HealthOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_off", + HealthOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def health_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -414,13 +436,22 @@ async def health_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_on", + PowerOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_off", + PowerOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_toggle", + PowerToggleAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def power_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 31a20a8981f..8ea8677ba2c 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -52,6 +52,7 @@ CONFIG_SCHEMA = ( "fan.hbridge.brake", BrakeAction, maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), + synchronous=True, ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py index 2f39b47f3cd..29b428e3109 100644 --- a/esphome/components/hc8/sensor.py +++ b/esphome/components/hc8/sensor.py @@ -68,7 +68,10 @@ CALIBRATION_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA + "hc8.calibrate", + HC8CalibrateAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def hc8_calibration_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py index 7215a4cfb7a..a6265b9b980 100644 --- a/esphome/components/hdc302x/sensor.py +++ b/esphome/components/hdc302x/sensor.py @@ -114,7 +114,10 @@ HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA + "hdc302x.heater_on", + HeaterOnAction, + HDC302X_HEATER_ON_ACTION_SCHEMA, + synchronous=True, ) async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -127,7 +130,10 @@ async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): @automation.register_action( - "hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA + "hdc302x.heater_off", + HeaterOffAction, + HDC302X_ACTION_SCHEMA, + synchronous=True, ) async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index efd64b65139..cb6d5cdfd6d 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -170,6 +170,7 @@ async def to_code(config): }, key=CONF_NAME, ), + synchronous=True, ) async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -192,6 +193,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): }, key=CONF_FACE_ID, ), + synchronous=True, ) async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +212,7 @@ async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -225,6 +228,7 @@ async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -240,6 +244,7 @@ async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 81337ebdf6e..416432cfc44 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -279,13 +279,22 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( @automation.register_action( - "http_request.get", HttpRequestSendAction, HTTP_REQUEST_GET_ACTION_SCHEMA + "http_request.get", + HttpRequestSendAction, + HTTP_REQUEST_GET_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.post", HttpRequestSendAction, HTTP_REQUEST_POST_ACTION_SCHEMA + "http_request.post", + HttpRequestSendAction, + HTTP_REQUEST_POST_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA + "http_request.send", + HttpRequestSendAction, + HTTP_REQUEST_SEND_ACTION_SCHEMA, + synchronous=True, ) async def http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index d2c574d8c61..fb59e51943f 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -70,6 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( "ota.http_request.flash", OtaHttpRequestComponentFlashAction, OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, + synchronous=True, ) async def ota_http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index a578670e375..92c088a22fa 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -93,6 +93,7 @@ async def to_code(config): }, key=CONF_LEVEL, ), + synchronous=True, ) async def set_heater_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -112,6 +113,7 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): }, key=CONF_STATUS, ), + synchronous=True, ) async def set_heater_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index f1e6ef42438..4f62ce7a94a 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -652,6 +652,7 @@ async def to_code(config: ConfigType) -> None: }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def hub75_set_brightness_to_code( config: ConfigType, diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 26766385565..d0aae4201e4 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -111,6 +111,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(IntegrationSensor), } ), + synchronous=True, ) async def sensor_integration_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -127,6 +128,7 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index badb28c32ca..1f4519df2d0 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -142,6 +142,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(KeyCollector), } ), + synchronous=True, ) async def enable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -157,6 +158,7 @@ async def enable_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(KeyCollector), } ), + synchronous=True, ) async def disable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index b492bbcd149..360e56330af 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -97,7 +97,10 @@ BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( @automation.register_action( - "bluetooth_password.set", BluetoothPasswordSetAction, BLUETOOTH_PASSWORD_SET_SCHEMA + "bluetooth_password.set", + BluetoothPasswordSetAction, + BLUETOOTH_PASSWORD_SET_SCHEMA, + synchronous=True, ) async def bluetooth_password_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 7a45b9dc3f6..62ff5ad30ac 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -77,6 +77,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def ledc_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 28556514d89..e812b6a8f25 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -38,6 +38,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(cv.int_), } ), + synchronous=True, ) async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 08fd26a9379..55273003b95 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -278,7 +278,10 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "light.addressable_set", AddressableSet, LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA + "light.addressable_set", + AddressableSet, + LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA, + synchronous=True, ) async def light_addressable_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 802b341601b..acbbbb4de97 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -55,6 +55,7 @@ LIGHTWAVE_SEND_SCHEMA = cv.Any( "lightwaverf.send_raw", LightwaveRawAction, LIGHTWAVE_SEND_SCHEMA, + synchronous=True, ) async def send_raw_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index e37092756fb..fe4db23ae3a 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -129,9 +129,15 @@ LOCK_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA) -@automation.register_action("lock.lock", LockAction, LOCK_ACTION_SCHEMA) -@automation.register_action("lock.open", OpenAction, LOCK_ACTION_SCHEMA) +@automation.register_action( + "lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "lock.lock", LockAction, LOCK_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True +) async def lock_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 026b8aaf246..83a78541652 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -545,6 +545,7 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL, ), + synchronous=True, ) async def logger_set_level_to_code(config, action_id, template_arg, args): level = LOG_LEVELS[config[CONF_LEVEL]] diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index b589e42f3b7..f9adca9c337 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -182,6 +182,7 @@ async def disp_update(disp, config: dict): ), LVGL_SCHEMA, ), + synchronous=True, ) async def obj_invalidate_to_code(config, action_id, template_arg, args): if CONF_LVGL_ID in config: @@ -202,6 +203,7 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) ), + synchronous=True, ) async def lvgl_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config, CONF_LVGL_ID) @@ -222,6 +224,7 @@ async def lvgl_update_to_code(config, action_id, template_arg, args): cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, } ), + synchronous=True, ) async def pause_action_to_code(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) @@ -237,6 +240,7 @@ async def pause_action_to_code(config, action_id, template_arg, args): "lvgl.resume", LvglAction, LVGL_SCHEMA, + synchronous=True, ) async def resume_action_to_code(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) @@ -247,7 +251,9 @@ async def resume_action_to_code(config, action_id, template_arg, args): return var -@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_disable_to_code(config, action_id, template_arg, args): async def do_disable(widget: Widget): widget.add_state(LV_STATE.DISABLED) @@ -257,7 +263,9 @@ async def obj_disable_to_code(config, action_id, template_arg, args): ) -@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_enable_to_code(config, action_id, template_arg, args): async def do_enable(widget: Widget): widget.clear_state(LV_STATE.DISABLED) @@ -267,7 +275,9 @@ async def obj_enable_to_code(config, action_id, template_arg, args): ) -@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): widget.add_flag("LV_OBJ_FLAG_HIDDEN") @@ -276,7 +286,9 @@ async def obj_hide_to_code(config, action_id, template_arg, args): return await action_to_code(widgets, do_hide, action_id, template_arg, args) -@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA) +@automation.register_action( + "lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA, synchronous=True +) async def obj_show_to_code(config, action_id, template_arg, args): async def do_show(widget: Widget): widget.clear_flag("LV_OBJ_FLAG_HIDDEN") @@ -318,6 +330,7 @@ def focused_id(value): key=CONF_ID, ), ), + synchronous=True, ) async def widget_focus(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -357,7 +370,10 @@ async def widget_focus(config, action_id, template_arg, args): @automation.register_action( - "lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS) + "lvgl.widget.update", + ObjUpdateAction, + base_update_schema(lv_obj_base_t, PARTS), + synchronous=True, ) async def obj_update_to_code(config, action_id, template_arg, args): async def do_update(widget: Widget): @@ -389,6 +405,7 @@ def validate_refresh_config(config): ), validate_refresh_config, ), + synchronous=True, ) async def obj_refresh_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 3969c9f3887..b9801b41332 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -59,6 +59,7 @@ async def styles_to_code(config): cv.Required(CONF_ID): cv.use_id(lv_style_t), } ), + synchronous=True, ) async def style_update_to_code(config, action_id, template_arg, args): await wait_for_widgets() diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 9c92ca7e986..09d40bb7efe 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -164,6 +164,7 @@ class WidgetType: f"lvgl.{self.name}.update", ObjUpdateAction, base_update_schema(self, self.parts).extend(self.modify_schema), + synchronous=True, )(update_to_code) @property diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index b824d28fb84..8e2db5ff350 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -83,6 +83,7 @@ animimg_spec = AnimimgType() }, key=CONF_ID, ), + synchronous=True, ) async def animimg_start(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -102,6 +103,7 @@ async def animimg_start(config, action_id, template_arg, args): }, key=CONF_ID, ), + synchronous=True, ) async def animimg_stop(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index fe421aa4770..f94f12b69b2 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -245,6 +245,7 @@ buttonmatrix_spec = ButtonMatrixType() cv.Optional(CONF_SELECTED): lv_bool, } ), + synchronous=True, ) async def button_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config[CONF_ID]) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index ead352aa77d..50cc8b0af62 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -97,6 +97,7 @@ canvas_spec = CanvasType() cv.Optional(CONF_OPA, default="COVER"): opacity, }, ), + synchronous=True, ) async def canvas_fill(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -120,6 +121,7 @@ async def canvas_fill(config, action_id, template_arg, args): cv.Required(CONF_POINTS): cv.ensure_list(point_schema), }, ), + synchronous=True, ) async def canvas_set_pixel(config, action_id, template_arg, args): widget = await get_widgets(config) @@ -229,6 +231,7 @@ RECT_PROPS = { **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, } ), + synchronous=True, ) async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) @@ -268,6 +271,7 @@ TEXT_PROPS = { **{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}, }, ), + synchronous=True, ) async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) @@ -302,6 +306,7 @@ IMG_PROPS = { **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_image(config, action_id, template_arg, args): src = await lv_image.process(config[CONF_SRC]) @@ -341,6 +346,7 @@ LINE_PROPS = { **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ @@ -369,6 +375,7 @@ async def canvas_draw_line(config, action_id, template_arg, args): **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, ), + synchronous=True, ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ @@ -408,6 +415,7 @@ ARC_PROPS = { **{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}, } ), + synchronous=True, ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index aefda0e71a3..b7e3af9a788 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -297,6 +297,7 @@ meter_spec = MeterType() cv.Optional(CONF_OPA): opacity, } ), + synchronous=True, ) async def indicator_update_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py index 23c162e010e..7e75ab6a2da 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -85,6 +85,7 @@ page_spec = PageType() "lvgl.page.next", LvglAction, SHOW_SCHEMA, + synchronous=True, ) async def page_next_to_code(config, action_id, template_arg, args): animation = await LV_ANIM.process(config[CONF_ANIMATION]) @@ -125,6 +126,7 @@ async def page_is_showing_to_code(config, condition_id, template_arg, args): "lvgl.page.previous", LvglAction, SHOW_SCHEMA, + synchronous=True, ) async def page_previous_to_code(config, action_id, template_arg, args): animation = await LV_ANIM.process(config[CONF_ANIMATION]) @@ -148,6 +150,7 @@ async def page_previous_to_code(config, action_id, template_arg, args): ), key=CONF_ID, ), + synchronous=True, ) async def page_show_to_code(config, action_id, template_arg, args): widget = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index c6f25e9587c..58e3435c5c7 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -147,6 +147,7 @@ spinbox_spec = SpinboxType() }, key=CONF_ID, ), + synchronous=True, ) async def spinbox_increment(config, action_id, template_arg, args): widgets = await get_widgets(config) @@ -166,6 +167,7 @@ async def spinbox_increment(config, action_id, template_arg, args): }, key=CONF_ID, ), + synchronous=True, ) async def spinbox_decrement(config, action_id, template_arg, args): widgets = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index e8931bab7ca..cd7cf7b4716 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -109,6 +109,7 @@ tabview_spec = TabviewType() cv.Required(CONF_INDEX): lv_int, }, ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), + synchronous=True, ) async def tabview_select(config, action_id, template_arg, args): widget = await get_widgets(config) diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 5e3a95f017b..430a386d2e2 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -112,6 +112,7 @@ def tile_select_validate(config): cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), }, ).add_extra(tile_select_validate), + synchronous=True, ) async def tileview_select(config, action_id, template_arg, args): widgets = await get_widgets(config) diff --git a/esphome/components/max17043/sensor.py b/esphome/components/max17043/sensor.py index 3da0f953b09..ebb045dfcea 100644 --- a/esphome/components/max17043/sensor.py +++ b/esphome/components/max17043/sensor.py @@ -71,7 +71,9 @@ MAX17043_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA) +@automation.register_action( + "max17043.sleep_mode", SleepAction, MAX17043_ACTION_SCHEMA, synchronous=True +) async def max17043_sleep_mode_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 0d2ff527c7d..be6390fc17f 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -112,6 +112,7 @@ async def max6956_pin_to_code(config): }, key=CONF_BRIGHTNESS_GLOBAL, ), + synchronous=True, ) async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,6 +134,7 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, }, key=CONF_BRIGHTNESS_MODE, ), + synchronous=True, ) async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index a251eaccea5..eb751b995d1 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -133,10 +133,16 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "max7219digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.invert_off", + DisplayInvertAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.invert_on", + DisplayInvertAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -146,10 +152,16 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7219digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.turn_off", + DisplayVisibilityAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.turn_on", + DisplayVisibilityAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -159,10 +171,16 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): @automation.register_action( - "max7219digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA + "max7219digit.reverse_off", + DisplayReverseAction, + MAX7219_OFF_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "max7219digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA + "max7219digit.reverse_on", + DisplayReverseAction, + MAX7219_ON_ACTION_SCHEMA, + synchronous=True, ) async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -183,7 +201,10 @@ MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "max7219digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA + "max7219digit.intensity", + DisplayIntensityAction, + MAX7219_INTENSITY_SCHEMA, + synchronous=True, ) async def max7219digit_intensity_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 051e386eaf7..a5baca29940 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -177,6 +177,7 @@ MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( }, key=CONF_MEDIA_URL, ), + synchronous=True, ) async def media_player_play_media_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -206,7 +207,10 @@ def _register_command_actions(): class_name, automation.Action, cg.Parented.template(MediaPlayer) ) automation.register_action( - f"media_player.{action_name}", action_class, MEDIA_PLAYER_ACTION_SCHEMA + f"media_player.{action_name}", + action_class, + MEDIA_PLAYER_ACTION_SCHEMA, + synchronous=True, )(handler) @@ -242,6 +246,7 @@ _register_state_conditions() }, key=CONF_VOLUME, ), + synchronous=True, ) async def media_player_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 2841afde7ac..b7d0ad1998e 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -112,13 +112,22 @@ NO_ARGS_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, NO_ARGS_ACTION_SCHEMA + "mhz19.calibrate_zero", + MHZ19CalibrateZeroAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "mhz19.abc_enable", MHZ19ABCEnableAction, NO_ARGS_ACTION_SCHEMA + "mhz19.abc_enable", + MHZ19ABCEnableAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "mhz19.abc_disable", MHZ19ABCDisableAction, NO_ARGS_ACTION_SCHEMA + "mhz19.abc_disable", + MHZ19ABCDisableAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def mhz19_no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -137,7 +146,10 @@ RANGE_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.detection_range_set", MHZ19DetectionRangeSetAction, RANGE_ACTION_SCHEMA + "mhz19.detection_range_set", + MHZ19DetectionRangeSetAction, + RANGE_ACTION_SCHEMA, + synchronous=True, ) async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 74696584da9..372eb4c3b00 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -529,8 +529,15 @@ async def to_code(config): MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) -@register_action("micro_wake_word.start", StartAction, MICRO_WAKE_WORD_ACTION_SCHEMA) -@register_action("micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA) +@register_action( + "micro_wake_word.start", + StartAction, + MICRO_WAKE_WORD_ACTION_SCHEMA, + synchronous=True, +) +@register_action( + "micro_wake_word.stop", StopAction, MICRO_WAKE_WORD_ACTION_SCHEMA, synchronous=True +) @register_condition( "micro_wake_word.is_running", IsRunningCondition, MICRO_WAKE_WORD_ACTION_SCHEMA ) @@ -551,11 +558,13 @@ MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA = automation.maybe_simple_id( "micro_wake_word.enable_model", EnableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, + synchronous=True, ) @register_action( "micro_wake_word.disable_model", DisableModelAction, MICRO_WAKE_WORLD_MODEL_ACTION_SCHEMA, + synchronous=True, ) @register_condition( "micro_wake_word.model_is_enabled", diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index ce314844134..6b5ee8c3e19 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -190,19 +190,25 @@ async def microphone_action(config, action_id, template_arg, args): automation.register_action( - "microphone.capture", CaptureAction, MICROPHONE_ACTION_SCHEMA + "microphone.capture", + CaptureAction, + MICROPHONE_ACTION_SCHEMA, + synchronous=True, )(microphone_action) automation.register_action( - "microphone.stop_capture", StopCaptureAction, MICROPHONE_ACTION_SCHEMA + "microphone.stop_capture", + StopCaptureAction, + MICROPHONE_ACTION_SCHEMA, + synchronous=True, )(microphone_action) -automation.register_action("microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA)( - microphone_action -) -automation.register_action("microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA)( - microphone_action -) +automation.register_action( + "microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True +)(microphone_action) +automation.register_action( + "microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True +)(microphone_action) automation.register_condition( "microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 8a3d4f22ba5..c954b450330 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -53,7 +53,9 @@ def templatize(value): def register_action(name, type_, schema): validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) - registerer = automation.register_action(f"midea_ac.{name}", type_, validator) + registerer = automation.register_action( + f"midea_ac.{name}", type_, validator, synchronous=True + ) def decorator(func): async def new_func(config, action_id, template_arg, args): diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index a3025d71210..63b419cc98e 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -162,6 +162,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def ducking_set_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index c25c4720387..d110d7c1602 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -607,6 +607,7 @@ async def mqtt_connected_to_code(config, condition_id, template_arg, args): cv.GenerateID(): cv.use_id(MQTTClientComponent), } ), + synchronous=True, ) async def mqtt_enable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -621,6 +622,7 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(MQTTClientComponent), } ), + synchronous=True, ) async def mqtt_disable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nau7802/sensor.py b/esphome/components/nau7802/sensor.py index 9192f48f53c..9798c1c2970 100644 --- a/esphome/components/nau7802/sensor.py +++ b/esphome/components/nau7802/sensor.py @@ -117,16 +117,19 @@ NAU7802_CALIBRATE_SCHEMA = maybe_simple_id( "nau7802.calibrate_internal_offset", NAU7802CalbrateInternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) @automation.register_action( "nau7802.calibrate_external_offset", NAU7802CalbrateExternalOffsetAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) @automation.register_action( "nau7802.calibrate_gain", NAU7802CalbrateGainAction, NAU7802_CALIBRATE_SCHEMA, + synchronous=True, ) async def nau7802_calibrate_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py index 7ef72c6491b..5b5922887ce 100644 --- a/esphome/components/nextion/binary_sensor/__init__.py +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -70,6 +70,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index b8fcd5d8cfa..5b2dfc488d2 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -172,6 +172,7 @@ CONFIG_SCHEMA = cv.All( }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def nextion_set_brightness_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index 9802762ff35..cab531f1db6 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -110,6 +110,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py index 1974ff3b9ea..81e6721d0f9 100644 --- a/esphome/components/nextion/switch/__init__.py +++ b/esphome/components/nextion/switch/__init__.py @@ -52,6 +52,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py index 8fc0a8ceaf0..168a6724979 100644 --- a/esphome/components/nextion/text_sensor/__init__.py +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -48,6 +48,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 057244e03d4..35a9de3537e 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -106,9 +106,14 @@ RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) @automation.register_action( - "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA + "online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True +) +@automation.register_action( + "online_image.release", + ReleaseImageAction, + RELEASE_IMAGE_SCHEMA, + synchronous=True, ) async def online_image_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index a4c960927ba..a4ce2b2d1a2 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -118,6 +118,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args): cv.Required(CONF_MIN_POWER): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_min_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -136,6 +137,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args): cv.Required(CONF_MAX_POWER): cv.templatable(cv.percentage), } ), + synchronous=True, ) async def output_set_max_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py index f3c0c3230f6..8e19178cc99 100644 --- a/esphome/components/pcf85063/time.py +++ b/esphome/components/pcf85063/time.py @@ -29,6 +29,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(PCF85063Component), } ), + synchronous=True, ) async def pcf85063_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -44,6 +45,7 @@ async def pcf85063_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(PCF85063Component), } ), + synchronous=True, ) async def pcf85063_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py index e3b3b572aa3..0d4de3cb73d 100644 --- a/esphome/components/pcf8563/time.py +++ b/esphome/components/pcf8563/time.py @@ -32,6 +32,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(pcf8563Component), } ), + synchronous=True, ) async def pcf8563_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -47,6 +48,7 @@ async def pcf8563_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(pcf8563Component), } ), + synchronous=True, ) async def pcf8563_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 5fa3166f9df..0e66b676377 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -133,6 +133,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(PIDClimate), } ), + synchronous=True, ) async def pid_reset_integral_term(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -154,6 +155,7 @@ async def pid_reset_integral_term(config, action_id, template_arg, args): ): cv.possibly_negative_percentage, } ), + synchronous=True, ) async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -175,6 +177,7 @@ async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), } ), + synchronous=True, ) async def set_control_parameters(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 829f8f70370..81e99e15a29 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -98,6 +98,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), } ), + synchronous=True, ) async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index 075b9b00b52..bb40f3e4995 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -106,11 +106,13 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( "pmwcs3.air_calibration", PMWCS3AirCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, + synchronous=True, ) @automation.register_action( "pmwcs3.water_calibration", PMWCS3WaterCalibrationAction, PMWCS3_CALIBRATION_SCHEMA, + synchronous=True, ) async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -130,6 +132,7 @@ PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( "pmwcs3.new_i2c_address", PMWCS3NewI2cAddressAction, PMWCS3_NEW_I2C_ADDRESS_SCHEMA, + synchronous=True, ) async def pmwcs3newi2caddress_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 131b56e30ea..6af14128818 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -119,11 +119,13 @@ PN7150_SCHEMA = cv.Schema( "tag.set_emulation_message", SetEmulationMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "tag.set_write_message", SetWriteMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) async def pn7150_set_message_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -138,22 +140,43 @@ async def pn7150_set_message_to_code(config, action_id, template_arg, args): @automation.register_action( - "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA -) -@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action( - "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_off", + EmulationOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "tag.set_clean_mode", + SetCleanModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_format_mode", + SetFormatModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_read_mode", + SetReadModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_write_mode", + SetWriteModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) async def pn7150_simple_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 899ecd595ea..54e4b74796b 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -123,11 +123,13 @@ PN7160_SCHEMA = cv.Schema( "tag.set_emulation_message", SetEmulationMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "tag.set_write_message", SetWriteMessageAction, SET_MESSAGE_ACTION_SCHEMA, + synchronous=True, ) async def pn7160_set_message_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -142,22 +144,43 @@ async def pn7160_set_message_to_code(config, action_id, template_arg, args): @automation.register_action( - "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA -) -@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) -@automation.register_action( - "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_off", + EmulationOffAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA + "tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA, synchronous=True ) @automation.register_action( - "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA + "tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "tag.set_clean_mode", + SetCleanModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_format_mode", + SetFormatModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_read_mode", + SetReadModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "tag.set_write_mode", + SetWriteModeAction, + SIMPLE_ACTION_SCHEMA, + synchronous=True, ) async def pn7160_simple_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 01244635679..c09d778eda2 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -155,6 +155,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), } ), + synchronous=True, ) async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index ca026eefa42..499b7309c88 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -105,6 +105,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t), } ), + synchronous=True, ) async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 3af73b86951..fa1c3961d0f 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -88,6 +88,7 @@ CONFIG_SCHEMA = ( cv.Required(CONF_ID): cv.use_id(PZEMAC), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 383a9dbb2c5..3291be4c341 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -72,6 +72,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_ID): cv.use_id(PZEMDC), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9d3e655c571..bf17ac27b8f 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -163,7 +163,10 @@ BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( def register_action(name, type_, schema): validator = templatize(schema).extend(BASE_REMOTE_TRANSMITTER_SCHEMA) registerer = automation.register_action( - f"remote_transmitter.transmit_{name}", type_, validator + f"remote_transmitter.transmit_{name}", + type_, + validator, + synchronous=True, ) def decorator(func): diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 371dbb685f7..89019e296e5 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -120,7 +120,10 @@ DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA + "remote_transmitter.digital_write", + DigitalWriteAction, + DIGITAL_WRITE_ACTION_SCHEMA, + synchronous=True, ) async def digital_write_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index b4770726b4f..934f24b7890 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -114,7 +114,10 @@ RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema( @automation.register_action( - "rf_bridge.send_code", RFBridgeSendCodeAction, RFBRIDGE_SEND_CODE_SCHEMA + "rf_bridge.send_code", + RFBridgeSendCodeAction, + RFBRIDGE_SEND_CODE_SCHEMA, + synchronous=True, ) async def rf_bridge_send_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,7 +136,9 @@ async def rf_bridge_send_code_to_code(config, action_id, template_args, args): RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)}) -@automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) +@automation.register_action( + "rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA, synchronous=True +) async def rf_bridge_learnx_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_args, paren) @@ -143,6 +148,7 @@ async def rf_bridge_learnx_to_code(config, action_id, template_args, args): "rf_bridge.start_advanced_sniffing", RFBridgeStartAdvancedSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_start_advanced_sniffing_to_code( config, action_id, template_args, args @@ -155,6 +161,7 @@ async def rf_bridge_start_advanced_sniffing_to_code( "rf_bridge.stop_advanced_sniffing", RFBridgeStopAdvancedSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_stop_advanced_sniffing_to_code( config, action_id, template_args, args @@ -167,6 +174,7 @@ async def rf_bridge_stop_advanced_sniffing_to_code( "rf_bridge.start_bucket_sniffing", RFBridgeStartBucketSniffingAction, RFBRIDGE_ID_SCHEMA, + synchronous=True, ) async def rf_bridge_start_bucket_sniffing_to_code( config, action_id, template_args, args @@ -189,6 +197,7 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( "rf_bridge.send_advanced_code", RFBridgeSendAdvancedCodeAction, RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA, + synchronous=True, ) async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -211,7 +220,10 @@ RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema( @automation.register_action( - "rf_bridge.send_raw", RFBridgeSendRawAction, RFBRIDGE_SEND_RAW_SCHEMA + "rf_bridge.send_raw", + RFBridgeSendRawAction, + RFBRIDGE_SEND_RAW_SCHEMA, + synchronous=True, ) async def rf_bridge_send_raw_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) @@ -229,7 +241,9 @@ RFBRIDGE_BEEP_SCHEMA = cv.Schema( ) -@automation.register_action("rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA) +@automation.register_action( + "rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA, synchronous=True +) async def rf_bridge_beep_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 645b4a81c5c..be315db55d2 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -139,6 +139,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.int_), } ), + synchronous=True, ) async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index 441a52de7f9..4ea488a6cd1 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -42,6 +42,7 @@ async def to_code(config): cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 19412bb4547..35667342006 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -117,6 +117,7 @@ async def to_code(config): }, key=CONF_RTTTL, ), + synchronous=True, ) async def rtttl_play_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -134,6 +135,7 @@ async def rtttl_play_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(Rtttl), } ), + synchronous=True, ) async def rtttl_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py index cb0402bd323..4f6310358c5 100644 --- a/esphome/components/rx8130/time.py +++ b/esphome/components/rx8130/time.py @@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(RX8130Component), } ), + synchronous=True, ) async def rx8130_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -42,6 +43,7 @@ async def rx8130_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(RX8130Component), } ), + synchronous=True, ) async def rx8130_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index f54151b7460..e868985054e 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -62,6 +62,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.use_id(SafeModeComponent), } ), + synchronous=True, ) async def safe_mode_mark_successful_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 194df8ec4f5..f60e913a0cb 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -128,6 +128,7 @@ async def to_code(config): }, key=CONF_VALUE, ), + synchronous=True, ) async def scd30_force_recalibration_with_reference_to_code( config, action_id, template_arg, args diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index ec90234ac39..6f14118660a 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -141,6 +141,7 @@ SCD4X_ACTION_SCHEMA = maybe_simple_id( "scd4x.perform_forced_calibration", PerformForcedCalibrationAction, SCD4X_ACTION_SCHEMA, + synchronous=True, ) async def scd4x_frc_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -158,7 +159,10 @@ SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA + "scd4x.factory_reset", + FactoryResetAction, + SCD4X_RESET_ACTION_SCHEMA, + synchronous=True, ) async def scd4x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 369cefad91a..51cae695b76 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -221,6 +221,7 @@ async def script_stop_action_to_code(config, action_id, template_arg, args): "script.wait", ScriptWaitAction, maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), + synchronous=False, ) async def script_wait_action_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 538a2f52395..9fe51121f16 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -267,7 +267,10 @@ SEN5X_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA + "sen5x.start_fan_autoclean", + StartFanAction, + SEN5X_ACTION_SCHEMA, + synchronous=True, ) async def sen54_fan_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 2eb2617e307..c5bef76741a 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -73,20 +73,31 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( "senseair.background_calibration", SenseAirBackgroundCalibrationAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "senseair.background_calibration_result", SenseAirBackgroundCalibrationResultAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_enable", SenseAirABCEnableAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_enable", + SenseAirABCEnableAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_disable", SenseAirABCDisableAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_disable", + SenseAirABCDisableAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "senseair.abc_get_period", SenseAirABCGetPeriodAction, CALIBRATION_ACTION_SCHEMA + "senseair.abc_get_period", + SenseAirABCGetPeriodAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def senseair_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index 2fee2840a5c..a23bb53536a 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -62,6 +62,7 @@ async def to_code(config): cv.Required(CONF_LEVEL): cv.templatable(cv.possibly_negative_percentage), } ), + synchronous=True, ) async def servo_write_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -79,6 +80,7 @@ async def servo_write_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(Servo), } ), + synchronous=True, ) async def servo_detach_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c48a3c63c41..ebb74302a9c 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -135,7 +135,10 @@ SIM800L_SEND_SMS_SCHEMA = cv.Schema( @automation.register_action( - "sim800l.send_sms", Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA + "sim800l.send_sms", + Sim800LSendSmsAction, + SIM800L_SEND_SMS_SCHEMA, + synchronous=True, ) async def sim800l_send_sms_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -155,7 +158,9 @@ SIM800L_DIAL_SCHEMA = cv.Schema( ) -@automation.register_action("sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA) +@automation.register_action( + "sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA, synchronous=True +) async def sim800l_dial_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -168,6 +173,7 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): "sim800l.connect", Sim800LConnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), + synchronous=True, ) async def sim800l_connect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -183,7 +189,10 @@ SIM800L_SEND_USSD_SCHEMA = cv.Schema( @automation.register_action( - "sim800l.send_ussd", Sim800LSendUssdAction, SIM800L_SEND_USSD_SCHEMA + "sim800l.send_ussd", + Sim800LSendUssdAction, + SIM800L_SEND_USSD_SCHEMA, + synchronous=True, ) async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -197,6 +206,7 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): "sim800l.disconnect", Sim800LDisconnectAction, cv.Schema({cv.GenerateID(): cv.use_id(Sim800LComponent)}), + synchronous=True, ) async def sim800l_disconnect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index 8ca0feccc0f..44f31979b46 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -89,8 +89,12 @@ SOUND_LEVEL_ACTION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action("sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA) -@automation.register_action("sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA) +@automation.register_action( + "sound_level.start", StartAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sound_level.stop", StopAction, SOUND_LEVEL_ACTION_SCHEMA, synchronous=True +) async def sound_level_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 10ee6d52125..8480eebcdb2 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -78,6 +78,7 @@ async def speaker_action(config, action_id, template_arg, args): }, key=CONF_DATA, ), + synchronous=True, ) async def speaker_play_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -95,12 +96,12 @@ async def speaker_play_action(config, action_id, template_arg, args): return var -automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( - speaker_action -) -automation.register_action("speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA)( - speaker_action -) +automation.register_action( + "speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True +)(speaker_action) +automation.register_action( + "speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True +)(speaker_action) automation.register_condition( "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA @@ -121,6 +122,7 @@ automation.register_condition( }, key=CONF_VOLUME, ), + synchronous=True, ) async def speaker_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -131,9 +133,14 @@ async def speaker_volume_set_action(config, action_id, template_arg, args): @automation.register_action( - "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA + "speaker.mute_off", + MuteOffAction, + SPEAKER_AUTOMATION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA, synchronous=True ) -@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA) async def speaker_mute_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 42ca762858f..92a178fe95e 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -505,6 +505,7 @@ async def to_code(config): }, key=CONF_MEDIA_FILE, ), + synchronous=True, ) async def play_on_device_media_media_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 50c69f94967..6e2ff4ee2e1 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -422,6 +422,7 @@ CONFIG_SCHEMA = cv.All( "sprinkler.set_divider", SetDividerAction, SPRINKLER_ACTION_SET_DIVIDER_SCHEMA, + synchronous=True, ) async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -435,6 +436,7 @@ async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): "sprinkler.set_multiplier", SetMultiplierAction, SPRINKLER_ACTION_SET_MULTIPLIER_SCHEMA, + synchronous=True, ) async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -448,6 +450,7 @@ async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args "sprinkler.queue_valve", QueueValveAction, SPRINKLER_ACTION_QUEUE_VALVE_SCHEMA, + synchronous=True, ) async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -463,6 +466,7 @@ async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, ar "sprinkler.set_repeat", SetRepeatAction, SPRINKLER_ACTION_REPEAT_SCHEMA, + synchronous=True, ) async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -476,6 +480,7 @@ async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): "sprinkler.set_valve_run_duration", SetRunDurationAction, SPRINKLER_ACTION_SET_RUN_DURATION_SCHEMA, + synchronous=True, ) async def sprinkler_set_valve_run_duration_to_code( config, action_id, template_arg, args @@ -490,7 +495,10 @@ async def sprinkler_set_valve_run_duration_to_code( @automation.register_action( - "sprinkler.start_from_queue", StartFromQueueAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.start_from_queue", + StartFromQueueAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -498,7 +506,10 @@ async def sprinkler_start_from_queue_to_code(config, action_id, template_arg, ar @automation.register_action( - "sprinkler.start_full_cycle", StartFullCycleAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.start_full_cycle", + StartFullCycleAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -509,6 +520,7 @@ async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, ar "sprinkler.start_single_valve", StartSingleValveAction, SPRINKLER_ACTION_SINGLE_VALVE_SCHEMA, + synchronous=True, ) async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -522,21 +534,40 @@ async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, @automation.register_action( - "sprinkler.clear_queued_valves", ClearQueuedValvesAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.clear_queued_valves", + ClearQueuedValvesAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.next_valve", NextValveAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.next_valve", + NextValveAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.previous_valve", PreviousValveAction, SPRINKLER_ACTION_SCHEMA -) -@automation.register_action("sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA) -@automation.register_action("sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA) -@automation.register_action( - "sprinkler.resume_or_start_full_cycle", ResumeOrStartAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.previous_valve", + PreviousValveAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sprinkler.shutdown", ShutdownAction, SPRINKLER_ACTION_SCHEMA + "sprinkler.pause", PauseAction, SPRINKLER_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sprinkler.resume", ResumeAction, SPRINKLER_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "sprinkler.resume_or_start_full_cycle", + ResumeOrStartAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "sprinkler.shutdown", + ShutdownAction, + SPRINKLER_ACTION_SCHEMA, + synchronous=True, ) async def sprinkler_simple_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 3c967fc01be..40557f2cbde 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -180,13 +180,22 @@ SPS30_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA + "sps30.start_fan_autoclean", + StartFanAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA + "sps30.start_measurement", + StartMeasurementAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA + "sps30.stop_measurement", + StopMeasurementAction, + SPS30_ACTION_SCHEMA, + synchronous=True, ) async def sps30_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 62bc71f2d17..27d4fc276d9 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -97,6 +97,7 @@ async def register_stepper(var, config): cv.Required(CONF_TARGET): cv.templatable(cv.int_), } ), + synchronous=True, ) async def stepper_set_target_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -115,6 +116,7 @@ async def stepper_set_target_to_code(config, action_id, template_arg, args): cv.Required(CONF_POSITION): cv.templatable(cv.int_), } ), + synchronous=True, ) async def stepper_report_position_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -133,6 +135,7 @@ async def stepper_report_position_to_code(config, action_id, template_arg, args) cv.Required(CONF_SPEED): cv.templatable(validate_speed), } ), + synchronous=True, ) async def stepper_set_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -151,6 +154,7 @@ async def stepper_set_speed_to_code(config, action_id, template_arg, args): cv.Required(CONF_ACCELERATION): cv.templatable(validate_acceleration), } ), + synchronous=True, ) async def stepper_set_acceleration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -169,6 +173,7 @@ async def stepper_set_acceleration_to_code(config, action_id, template_arg, args cv.Required(CONF_DECELERATION): cv.templatable(validate_acceleration), } ), + synchronous=True, ) async def stepper_set_deceleration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 413eb139d65..08f4c0fb882 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -290,19 +290,34 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA + "sx126x.run_image_cal", + RunImageCalAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_tx", + SetModeTxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_rx", + SetModeRxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_sleep", + SetModeSleepAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA + "sx126x.set_mode_standby", + SetModeStandbyAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -320,7 +335,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "sx126x.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index f3a9cca93f4..7f554fbf841 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -283,19 +283,34 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA + "sx127x.run_image_cal", + RunImageCalAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_tx", + SetModeTxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_rx", + SetModeRxAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_sleep", + SetModeSleepAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA + "sx127x.set_mode_standby", + SetModeStandbyAction, + NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def no_args_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -313,7 +328,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "sx127x.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 9d4208dcca5..e537e1f97cf 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -59,6 +59,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index a4fb0b70210..cfc19c00cdf 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -125,6 +125,7 @@ async def to_code(config): cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), } ), + synchronous=True, ) async def cover_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py index 4c74a521fa2..d8bd9d16c66 100644 --- a/esphome/components/template/lock/__init__.py +++ b/esphome/components/template/lock/__init__.py @@ -90,6 +90,7 @@ async def to_code(config): }, key=CONF_STATE, ), + synchronous=True, ) async def lock_template_publish_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 2c325427e99..b0f48ade465 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -44,6 +44,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.float_), } ), + synchronous=True, ) async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index 8ae5a07dc3b..eb6f0f46de7 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -80,6 +80,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def switch_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py index 550b27356d1..ddbdd6dadb7 100644 --- a/esphome/components/template/text_sensor/__init__.py +++ b/esphome/components/template/text_sensor/__init__.py @@ -43,6 +43,7 @@ async def to_code(config): cv.Required(CONF_STATE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def text_sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index 526751564de..3e8fd816030 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -112,6 +112,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def valve_template_publish_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index cb5f2dbe56d..814aa401935 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -134,6 +134,7 @@ async def to_code(config: ConfigType) -> None: cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def water_heater_template_publish_to_code( config: ConfigType, diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 49796d9b426..fb35eb21b51 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -73,6 +73,7 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -92,6 +93,7 @@ async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL, ), + synchronous=True, ) async def tm1651_set_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -111,6 +113,7 @@ async def tm1651_set_level_to_code(config, action_id, template_arg, args): }, key=CONF_LEVEL_PERCENT, ), + synchronous=True, ) async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -121,7 +124,10 @@ async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args @automation.register_action( - "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA + "tm1651.turn_off", + TurnOffAction, + BINARY_OUTPUT_ACTION_SCHEMA, + synchronous=True, ) async def output_turn_off_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -129,7 +135,9 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): return var -@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +@automation.register_action( + "tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA, synchronous=True +) async def output_turn_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 2cb6eac0509..83649cc2092 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -500,6 +500,7 @@ async def register_uart_device(var, config): }, key=CONF_DATA, ), + synchronous=True, ) async def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 37dd871a6c1..17bbf19c9ea 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -171,6 +171,7 @@ def validate_raw_data(value): }, key=CONF_DATA, ), + synchronous=True, ) async def udp_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 9edf0f89ffa..10b4ece6141 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -97,6 +97,7 @@ UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( "ufire_ec.calibrate_probe", UFireECCalibrateProbeAction, UFIRE_EC_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -119,6 +120,7 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( "ufire_ec.reset", UFireECResetAction, UFIRE_EC_RESET_SCHEMA, + synchronous=True, ) async def ufire_ec_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index 8009cdaa6a8..a116012d055 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -91,6 +91,7 @@ UFIRE_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( "ufire_ise.calibrate_probe_low", UFireISECalibrateProbeLowAction, UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -104,6 +105,7 @@ async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, "ufire_ise.calibrate_probe_high", UFireISECalibrateProbeHighAction, UFIRE_ISE_CALIBRATE_PROBE_SCHEMA, + synchronous=True, ) async def ufire_ise_calibrate_probe_high_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -120,6 +122,7 @@ UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent "ufire_ise.reset", UFireISEResetAction, UFIRE_ISE_RESET_SCHEMA, + synchronous=True, ) async def ufire_ise_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index c36a4ab769d..db6c1445e34 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -138,6 +138,7 @@ async def to_code(config): cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def update_perform_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -156,6 +157,7 @@ async def update_perform_action_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(UpdateEntity), } ), + synchronous=True, ) async def update_check_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 22cd01988d8..0319ff50e7f 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -180,25 +180,33 @@ VALVE_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("valve.open", OpenAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.open", OpenAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_open_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.close", CloseAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.close", CloseAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_close_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.stop", StopAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.stop", StopAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_stop_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA) +@automation.register_action( + "valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA, synchronous=True +) async def valve_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -214,7 +222,9 @@ VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA) +@automation.register_action( + "valve.control", ControlAction, VALVE_CONTROL_ACTION_SCHEMA, synchronous=True +) async def valve_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 8b7dcb4f212..d970df2a44b 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -393,6 +393,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis "voice_assistant.start_continuous", StartContinuousAction, VOICE_ASSISTANT_ACTION_SCHEMA, + synchronous=True, ) @register_action( "voice_assistant.start", @@ -403,6 +404,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis cv.Optional(CONF_WAKE_WORD): cv.templatable(cv.string), } ), + synchronous=True, ) async def voice_assistant_listen_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -415,7 +417,9 @@ async def voice_assistant_listen_to_code(config, action_id, template_arg, args): return var -@register_action("voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA) +@register_action( + "voice_assistant.stop", StopAction, VOICE_ASSISTANT_ACTION_SCHEMA, synchronous=True +) async def voice_assistant_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 0f86ec059ee..2808d313111 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -670,12 +670,16 @@ async def wifi_ap_active_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) -@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) +@automation.register_action( + "wifi.enable", WiFiEnableAction, cv.Schema({}), synchronous=True +) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) -@automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({})) +@automation.register_action( + "wifi.disable", WiFiDisableAction, cv.Schema({}), synchronous=True +) async def wifi_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) @@ -781,6 +785,7 @@ async def final_step(): cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), } ), + synchronous=False, ) async def wifi_set_sta_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 124d9a8c328..e2ea61a5428 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -168,6 +168,7 @@ async def wireguard_enabled_to_code(config, condition_id, template_arg, args): "wireguard.enable", WireguardEnableAction, cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), + synchronous=True, ) async def wireguard_enable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -179,6 +180,7 @@ async def wireguard_enable_to_code(config, action_id, template_arg, args): "wireguard.disable", WireguardDisableAction, cv.Schema({cv.GenerateID(): cv.use_id(Wireguard)}), + synchronous=True, ) async def wireguard_disable_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index a327cc29886..280ff6b50cc 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -195,6 +195,7 @@ FactoryResetAction = zigbee_ns.class_( "zigbee.factory_reset", FactoryResetAction, ZIGBEE_ACTION_SCHEMA, + synchronous=True, ) async def reset_zigbee_to_code( config: ConfigType, From 4d2ef09a296c4fd3e1b5e723024770ab8b8c3339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:12:10 -1000 Subject: [PATCH 0542/2056] [log] Detect early log calls before logger init and optimize hot path (#14538) --- esphome/components/logger/__init__.py | 5 +- esphome/core/application.h | 2 + esphome/core/log.cpp | 60 +++++++++++++------ esphome/core/log.h | 12 +++- script/cpp_unit_test.py | 1 + tests/component_tests/logger/__init__.py | 0 tests/component_tests/logger/test_logger.py | 50 ++++++++++++++++ tests/component_tests/logger/test_logger.yaml | 14 +++++ tests/components/main.cpp | 8 +++ tests/integration/conftest.py | 1 + 10 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 tests/component_tests/logger/__init__.py create mode 100644 tests/component_tests/logger/test_logger.py create mode 100644 tests/component_tests/logger/test_logger.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 83a78541652..e370f4215d4 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -337,6 +337,10 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) + # pre_setup() must be called before init_log_buffer() because + # init_log_buffer() calls disable_loop() which may log at VV level, + # and global_logger must be set before any logging occurs. + cg.add(log.pre_setup()) if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: @@ -356,7 +360,6 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - cg.add(log.pre_setup()) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/core/application.h b/esphome/core/application.h index f357c6b1a3d..23bb209eaf4 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -142,6 +142,7 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: #ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + // Called before Logger::pre_setup() — must not log (global_logger is not yet set). /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); @@ -163,6 +164,7 @@ class Application { this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } #else + // Called before Logger::pre_setup() — must not log (global_logger is not yet set). /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { arch_init(); diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 8338efbb33c..0da457adec2 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -1,6 +1,7 @@ #include "log.h" #include "defines.h" #include "helpers.h" +#include <cstdio> #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -8,40 +9,63 @@ namespace esphome { +#ifdef ESPHOME_DEBUG +static void early_log_printf_(const char *tag, int line, const char *format, va_list args) { + fprintf(stderr, "LOG BEFORE LOGGER INIT [%s:%d]: ", tag, line); + vfprintf(stderr, format, args); + fputc('\n', stderr); + assert(false && "log called before Logger::pre_setup()"); // NOLINT +} +#endif + void HOT esp_log_printf_(int level, const char *tag, int line, const char *format, ...) { // NOLINT +#ifdef USE_LOGGER +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + va_list arg; + va_start(arg, format); + early_log_printf_(tag, line, format, arg); + va_end(arg); + return; + } +#endif va_list arg; va_start(arg, format); - esp_log_vprintf_(level, tag, line, format, arg); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, arg); va_end(arg); +#endif } + #ifdef USE_STORE_LOG_STR_IN_FLASH void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) { +#ifdef USE_LOGGER + ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr); va_list arg; va_start(arg, format); - esp_log_vprintf_(level, tag, line, format, arg); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, arg); va_end(arg); +#endif } #endif void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + early_log_printf_(tag, line, format, args); return; - - log->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); + } +#endif + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); #endif } #ifdef USE_STORE_LOG_STR_IN_FLASH -void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, - va_list args) { // NOLINT +// Remove before 2026.9.0 +void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) - return; - - log->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); + ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr); + logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args); #endif } #endif @@ -49,11 +73,13 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr #ifdef USE_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER - auto *log = logger::global_logger; - if (log == nullptr) +#ifdef ESPHOME_DEBUG + if (logger::global_logger == nullptr) { + early_log_printf_("esp-idf", 0, format, args); return 0; - - log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); + } +#endif + logger::global_logger->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif return 0; } diff --git a/esphome/core/log.h b/esphome/core/log.h index a2c4b35c6e2..ff39633142e 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -4,6 +4,14 @@ #include <cassert> #include <cstdarg> + +// Debug assert that only fires when ESPHOME_DEBUG is defined (e.g. in CI/test builds). +// Zero cost in production firmware. +#ifdef ESPHOME_DEBUG +#define ESPHOME_DEBUG_ASSERT(expr) assert(expr) // NOLINT +#else +#define ESPHOME_DEBUG_ASSERT(expr) ((void) 0) +#endif // for PRIu32 and friends #include <cinttypes> #include <string> @@ -61,7 +69,9 @@ void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHe #endif void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH -void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); +// Remove before 2026.9.0 +__attribute__((deprecated("Use esp_log_printf_() instead. Removed in 2026.9.0."))) void esp_log_vprintf_( + int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif #if defined(USE_ESP32) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index e11687dc16d..c9174584722 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -78,6 +78,7 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: "build_flags": [ "-Og", # optimize for debug "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing + "-DESPHOME_DEBUG", # enable debug assertions ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info diff --git a/tests/component_tests/logger/__init__.py b/tests/component_tests/logger/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py new file mode 100644 index 00000000000..98aa7419642 --- /dev/null +++ b/tests/component_tests/logger/test_logger.py @@ -0,0 +1,50 @@ +"""Tests for the logger component.""" + +import re + + +def test_logger_pre_setup_before_other_components(generate_main): + """Logger::pre_setup() must be called before any other component is created. + + Log functions call global_logger->log_vprintf_() without a null check, + so global_logger must be set before anything can log. + """ + main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml") + + # Find the logger's pre_setup() call specifically + logger_pre_setup = re.search(r"logger_logger->pre_setup\(\)", main_cpp) + if logger_pre_setup is None: + # Fall back to finding any logger-related pre_setup + logger_pre_setup = re.search(r"logger\w*->pre_setup\(\)", main_cpp) + assert logger_pre_setup is not None, ( + "Logger pre_setup() not found in generated code" + ) + + # Find all "new " allocations (component creation) + new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + assert len(new_allocations) > 0, "No component allocations found" + + # Separate logger and non-logger allocations + logger_allocs = [a for a in new_allocations if "logger" in a.group().lower()] + non_logger_allocs = [ + a + for a in new_allocations + if "logger" not in a.group().lower() + # Skip placement new for App + and "(&App)" not in main_cpp[max(0, a.start() - 5) : a.start()] + ] + + assert len(logger_allocs) > 0, ( + f"Logger allocation not found in: {[a.group() for a in new_allocations]}" + ) + assert len(non_logger_allocs) > 0, ( + "No non-logger component allocations found — " + "add a component to test_logger.yaml so the ordering check is meaningful" + ) + + # All non-logger allocations must appear after logger pre_setup() + for alloc in non_logger_allocs: + assert alloc.start() > logger_pre_setup.start(), ( + f"Component allocation '{alloc.group()}' at position {alloc.start()} " + f"appears before logger pre_setup() at position {logger_pre_setup.start()}" + ) diff --git a/tests/component_tests/logger/test_logger.yaml b/tests/component_tests/logger/test_logger.yaml new file mode 100644 index 00000000000..f43e99d94fd --- /dev/null +++ b/tests/component_tests/logger/test_logger.yaml @@ -0,0 +1,14 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini_lite + +logger: + level: DEBUG + +# Need at least one non-logger component so the ordering test +# can verify that logger pre_setup() comes before other allocations. +preferences: + flash_write_interval: 1min diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 928f0e60593..373fde71516 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -1,5 +1,7 @@ #include <gtest/gtest.h> +#include "esphome/components/logger/logger.h" + /* This special main.cpp replaces the default one. It will run all the Google Tests found in all compiled cpp files and then exit with the result @@ -18,6 +20,12 @@ void original_setup() { } void setup() { + // Log functions call global_logger->log_vprintf_() without a null check, + // so we must set up a Logger before any test that triggers logging. + static esphome::logger::Logger test_logger(0); + test_logger.set_log_level(ESPHOME_LOG_LEVEL); + test_logger.pre_setup(); + ::testing::InitGoogleTest(); int exit_code = RUN_ALL_TESTS(); exit(exit_code); diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b7f7fc60b3b..b652b4174cc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -193,6 +193,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s " platformio_options:\n" " build_flags:\n" ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-DESPHOME_DEBUG" # Enable ESPHOME_DEBUG_ASSERT checks\n' ' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n' ' - "-g" # Add debug symbols', ) From 9404eadaf8fe50fab155bae12252adad31530336 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 09:12:28 -1000 Subject: [PATCH 0543/2056] [rp2040_ble] Add BLE component for RP2040/RP2350 (#14603) --- CODEOWNERS | 1 + esphome/components/rp2040_ble/__init__.py | 31 +++++ esphome/components/rp2040_ble/rp2040_ble.cpp | 124 ++++++++++++++++++ esphome/components/rp2040_ble/rp2040_ble.h | 51 +++++++ esphome/core/defines.h | 1 + tests/components/rp2040_ble/common.yaml | 1 + .../test-disable-on-boot.rp2040-ard.yaml | 2 + .../rp2040_ble/test.rp2040-ard.yaml | 1 + 8 files changed, 212 insertions(+) create mode 100644 esphome/components/rp2040_ble/__init__.py create mode 100644 esphome/components/rp2040_ble/rp2040_ble.cpp create mode 100644 esphome/components/rp2040_ble/rp2040_ble.h create mode 100644 tests/components/rp2040_ble/common.yaml create mode 100644 tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml create mode 100644 tests/components/rp2040_ble/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index cb415bb625a..a95e100cbff 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -410,6 +410,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rp2040/* @jesserockz +esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py new file mode 100644 index 00000000000..648f22691c4 --- /dev/null +++ b/esphome/components/rp2040_ble/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID +from esphome.types import ConfigType + +DEPENDENCIES = ["rp2040"] +CODEOWNERS = ["@bdraco"] + +rp2040_ble_ns = cg.esphome_ns.namespace("rp2040_ble") +RP2040BLE = rp2040_ble_ns.class_("RP2040BLE", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RP2040BLE), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + + # Enable Bluetooth in the arduino-pico build + # This switches linking from liblwip.a to liblwip-bt.a and defines + # ENABLE_CLASSIC, ENABLE_BLE, CYW43_ENABLE_BLUETOOTH + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH") + + cg.add_define("USE_RP2040_BLE") diff --git a/esphome/components/rp2040_ble/rp2040_ble.cpp b/esphome/components/rp2040_ble/rp2040_ble.cpp new file mode 100644 index 00000000000..4125da7ec01 --- /dev/null +++ b/esphome/components/rp2040_ble/rp2040_ble.cpp @@ -0,0 +1,124 @@ +#include "rp2040_ble.h" + +#ifdef USE_RP2040_BLE + +#include "esphome/core/log.h" + +namespace esphome::rp2040_ble { + +static const char *const TAG = "rp2040_ble"; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +RP2040BLE *global_ble = nullptr; + +void RP2040BLE::setup() { + global_ble = this; + + if (this->enable_on_boot_) { + this->enable(); + } else { + this->state_ = BLEComponentState::DISABLED; + } +} + +void RP2040BLE::enable() { + if (this->state_ == BLEComponentState::ACTIVE || this->state_ == BLEComponentState::ENABLING) { + return; + } + + ESP_LOGD(TAG, "Enabling BLE..."); + this->state_ = BLEComponentState::ENABLING; + this->active_logged_ = false; + + if (!this->btstack_initialized_) { + // BTstack init functions are not idempotent — only call once + l2cap_init(); + sm_init(); + + this->hci_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + hci_add_event_handler(&this->hci_event_callback_registration_); + + this->sm_event_callback_registration_.callback = &RP2040BLE::packet_handler_; + sm_add_event_handler(&this->sm_event_callback_registration_); + + this->btstack_initialized_ = true; + } + + hci_power_control(HCI_POWER_ON); +} + +void RP2040BLE::disable() { + if (this->state_ == BLEComponentState::DISABLED || this->state_ == BLEComponentState::OFF) { + return; + } + + ESP_LOGD(TAG, "Disabling BLE..."); + this->state_ = BLEComponentState::DISABLING; + + hci_power_control(HCI_POWER_OFF); + + this->state_ = BLEComponentState::DISABLED; + ESP_LOGD(TAG, "BLE disabled"); +} + +void RP2040BLE::loop() { + if (this->state_ == BLEComponentState::ACTIVE && !this->active_logged_) { + this->active_logged_ = true; + ESP_LOGI(TAG, "BLE active"); + } +} + +static const char *state_to_str(BLEComponentState state) { + switch (state) { + case BLEComponentState::OFF: + return "OFF"; + case BLEComponentState::ENABLING: + return "ENABLING"; + case BLEComponentState::ACTIVE: + return "ACTIVE"; + case BLEComponentState::DISABLING: + return "DISABLING"; + case BLEComponentState::DISABLED: + return "DISABLED"; + default: + return "UNKNOWN"; + } +} + +void RP2040BLE::dump_config() { + ESP_LOGCONFIG(TAG, + "RP2040 BLE:\n" + " Enable on boot: %s\n" + " State: %s", + YESNO(this->enable_on_boot_), state_to_str(this->state_)); +} + +float RP2040BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } + +void RP2040BLE::packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) { + if (global_ble == nullptr) { + return; + } + + if (type != HCI_EVENT_PACKET) { + return; + } + + uint8_t event_type = hci_event_packet_get_type(packet); + + switch (event_type) { + case BTSTACK_EVENT_STATE: { + uint8_t state = btstack_event_state_get_state(packet); + if (state == HCI_STATE_WORKING && global_ble->state_ == BLEComponentState::ENABLING) { + global_ble->state_ = BLEComponentState::ACTIVE; + } + break; + } + default: + break; + } +} + +} // namespace esphome::rp2040_ble + +#endif // USE_RP2040_BLE diff --git a/esphome/components/rp2040_ble/rp2040_ble.h b/esphome/components/rp2040_ble/rp2040_ble.h new file mode 100644 index 00000000000..24b3860cc1e --- /dev/null +++ b/esphome/components/rp2040_ble/rp2040_ble.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/defines.h" // Must be included before conditional includes + +#ifdef USE_RP2040_BLE + +#include "esphome/core/component.h" + +#include <btstack.h> + +namespace esphome::rp2040_ble { + +enum class BLEComponentState : uint8_t { + OFF = 0, + ENABLING, + ACTIVE, + DISABLING, + DISABLED, +}; + +class RP2040BLE : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + void enable(); + void disable(); + bool is_active() const { return this->state_ == BLEComponentState::ACTIVE; } + + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + + protected: + static void packet_handler_(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size); + + btstack_packet_callback_registration_t hci_event_callback_registration_{}; + btstack_packet_callback_registration_t sm_event_callback_registration_{}; + + BLEComponentState state_{BLEComponentState::OFF}; + bool enable_on_boot_{true}; + bool btstack_initialized_{false}; + bool active_logged_{false}; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern RP2040BLE *global_ble; + +} // namespace esphome::rp2040_ble + +#endif // USE_RP2040_BLE diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 51f474d80ef..44918fe00c2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -339,6 +339,7 @@ #define USE_I2C #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP +#define USE_RP2040_BLE #define USE_SPI #endif diff --git a/tests/components/rp2040_ble/common.yaml b/tests/components/rp2040_ble/common.yaml new file mode 100644 index 00000000000..f6205a4724f --- /dev/null +++ b/tests/components/rp2040_ble/common.yaml @@ -0,0 +1 @@ +rp2040_ble: diff --git a/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml b/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml new file mode 100644 index 00000000000..2154536111f --- /dev/null +++ b/tests/components/rp2040_ble/test-disable-on-boot.rp2040-ard.yaml @@ -0,0 +1,2 @@ +rp2040_ble: + enable_on_boot: false diff --git a/tests/components/rp2040_ble/test.rp2040-ard.yaml b/tests/components/rp2040_ble/test.rp2040-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/rp2040_ble/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 04d80cfb75ffc19eaa14fcadce805c4c57369919 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:17:30 -0400 Subject: [PATCH 0544/2056] [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#14680) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 4 ++-- esphome/idf_component.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 720d81acc4a..6d49053d6d5 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -103,9 +103,9 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 550e7b9af79..acd7f7a4798 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -12,7 +12,7 @@ dependencies: espressif/mdns: version: 1.10.0 espressif/esp_wifi_remote: - version: 1.3.2 + version: 1.4.0 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: @@ -20,7 +20,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.11.5 + version: 2.12.0 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 780e009bf46854cabb42fb937659569eadc23d83 Mon Sep 17 00:00:00 2001 From: mahumpula <90641469+mahumpula@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:23:49 +0100 Subject: [PATCH 0545/2056] [runtime_image] Add support for 8bit BMPs and fix existing issues (#10733) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/runtime_image/bmp_decoder.cpp | 109 ++++++++++++++---- .../components/runtime_image/bmp_decoder.h | 4 + tests/components/online_image/common.yaml | 4 + 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 7003f4da2ff..174f924b285 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -12,7 +12,10 @@ static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; - if (this->current_index_ == 0 && index == 0 && size > 14) { + if (this->current_index_ == 0) { + if (size <= 14) { + return 0; // Need more data for file header + } /** * BMP file format: * 0-1: Signature (BM) @@ -39,7 +42,10 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->current_index_ = 14; index = 14; } - if (this->current_index_ == 14 && index == 14 && size > this->data_offset_) { + if (this->current_index_ == 14) { + if (size <= this->data_offset_) { + return 0; // Need more data for DIB header and color table + } /** * BMP DIB header: * 14-17: DIB header size @@ -66,6 +72,28 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->width_bytes_ = (this->width_ + 7) / 8; this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; break; + case 8: { + this->width_bytes_ = this->width_; + if (this->color_table_entries_ == 0) { + this->color_table_entries_ = 256; + } else if (this->color_table_entries_ > 256) { + ESP_LOGE(TAG, "Too many color table entries: %" PRIu32, this->color_table_entries_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + size_t header_size = encode_uint32(buffer[17], buffer[16], buffer[15], buffer[14]); + size_t offset = 14 + header_size; + + this->color_table_ = std::make_unique<uint32_t[]>(this->color_table_entries_); + + for (size_t i = 0; i < this->color_table_entries_; i++) { + this->color_table_[i] = encode_uint32(buffer[offset + i * 4 + 3], buffer[offset + i * 4 + 2], + buffer[offset + i * 4 + 1], buffer[offset + i * 4]); + } + + this->padding_bytes_ = (4 - (this->width_bytes_ % 4)) % 4; + + break; + } case 24: this->width_bytes_ = this->width_ * 3; if (this->width_bytes_ % 4 != 0) { @@ -91,21 +119,24 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } switch (this->bits_per_pixel_) { case 1: { + size_t width = static_cast<size_t>(this->width_); while (index < size) { - uint8_t current_byte = buffer[index]; - bool end_of_row = false; - for (uint8_t i = 0; i < 8; i++) { - size_t x = this->paint_index_ % static_cast<size_t>(this->width_); - size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_)); - Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; - this->draw(x, y, 1, 1, c); - this->paint_index_++; - // End of pixel row: skip remaining bits in this byte - if (x + 1 >= static_cast<size_t>(this->width_)) { - end_of_row = true; - break; - } + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t remaining_in_row = width - x; + uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8); + bool end_of_row = remaining_in_row <= 8; + size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0); + if (index + needed > size) { + this->decoded_bytes_ += index; + return index; } + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < pixels_in_byte; i++) { + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x + i, y, 1, 1, c); + } + this->paint_index_ += pixels_in_byte; this->current_index_++; index++; // End of pixel row: skip row padding bytes (4-byte alignment) @@ -116,23 +147,57 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } break; } - case 24: { + case 8: { + size_t width = static_cast<size_t>(this->width_); + size_t last_col = width - 1; while (index < size) { - if (index + 2 >= size) { + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0); + if (index + needed > size) { + this->decoded_bytes_ += index; + return index; + } + + uint8_t color_index = buffer[index]; + if (color_index >= this->color_table_entries_) { + ESP_LOGE(TAG, "Invalid color index: %u", color_index); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + uint32_t rgb = this->color_table_[color_index]; + uint8_t b = rgb & 0xff; + uint8_t g = (rgb >> 8) & 0xff; + uint8_t r = (rgb >> 16) & 0xff; + this->draw(x, y, 1, 1, Color(r, g, b)); + this->paint_index_++; + this->current_index_++; + index++; + if (x == last_col && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } + } + break; + } + case 24: { + size_t width = static_cast<size_t>(this->width_); + size_t last_col = width - 1; + while (index < size) { + size_t x = this->paint_index_ % width; + size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width); + size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0); + if (index + needed > size) { this->decoded_bytes_ += index; return index; } uint8_t b = buffer[index]; uint8_t g = buffer[index + 1]; uint8_t r = buffer[index + 2]; - size_t x = this->paint_index_ % static_cast<size_t>(this->width_); - size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_)); - Color c = Color(r, g, b); - this->draw(x, y, 1, 1, c); + this->draw(x, y, 1, 1, Color(r, g, b)); this->paint_index_++; this->current_index_ += 3; index += 3; - size_t last_col = static_cast<size_t>(this->width_) - 1; if (x == last_col && this->padding_bytes_ > 0) { index += this->padding_bytes_; this->current_index_ += this->padding_bytes_; diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index 37db6b49405..73e54f54302 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -3,6 +3,9 @@ #include "esphome/core/defines.h" #ifdef USE_RUNTIME_IMAGE_BMP +#include <algorithm> +#include <memory> + #include "image_decoder.h" #include "runtime_image.h" @@ -36,6 +39,7 @@ class BmpDecoder : public ImageDecoder { uint32_t compression_method_{0}; uint32_t image_data_size_{0}; uint32_t color_table_entries_{0}; + std::unique_ptr<uint32_t[]> color_table_; size_t width_bytes_{0}; size_t data_offset_{0}; uint8_t padding_bytes_{0}; diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 422a24b5406..fc3cc942172 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -40,6 +40,10 @@ online_image: url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp format: BMP type: BINARY + - id: online_rgb_bmp_8bit + url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp + format: BMP + type: RGB - id: online_jpeg_image url: http://www.faqs.org/images/library.jpg format: JPEG From 8ca6ee4349acb28878d4c15ef32d67999b96f139 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 10 Mar 2026 15:25:26 -0500 Subject: [PATCH 0546/2056] [speaker_source] Add new media player (#14649) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 1 + esphome/components/speaker_source/__init__.py | 0 .../components/speaker_source/media_player.py | 212 +++++++ .../speaker_source_media_player.cpp | 546 ++++++++++++++++++ .../speaker_source_media_player.h | 217 +++++++ tests/components/speaker_source/common.yaml | 43 ++ .../speaker_source/test.esp32-idf.yaml | 9 + tests/components/speaker_source/test.wav | Bin 0 -> 46 bytes 8 files changed, 1028 insertions(+) create mode 100644 esphome/components/speaker_source/__init__.py create mode 100644 esphome/components/speaker_source/media_player.py create mode 100644 esphome/components/speaker_source/speaker_source_media_player.cpp create mode 100644 esphome/components/speaker_source/speaker_source_media_player.h create mode 100644 tests/components/speaker_source/common.yaml create mode 100644 tests/components/speaker_source/test.esp32-idf.yaml create mode 100644 tests/components/speaker_source/test.wav diff --git a/CODEOWNERS b/CODEOWNERS index a95e100cbff..12aff01e730 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -459,6 +459,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam +esphome/components/speaker_source/* @kahrendt esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow diff --git a/esphome/components/speaker_source/__init__.py b/esphome/components/speaker_source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py new file mode 100644 index 00000000000..a44cdcbf01e --- /dev/null +++ b/esphome/components/speaker_source/media_player.py @@ -0,0 +1,212 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import audio, media_player, media_source, speaker +import esphome.config_validation as cv +from esphome.const import ( + CONF_FORMAT, + CONF_ID, + CONF_NUM_CHANNELS, + CONF_SAMPLE_RATE, + CONF_SPEAKER, +) +from esphome.core.entity_helpers import inherit_property_from +from esphome.types import ConfigType + +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["media_source", "speaker"] + +CODEOWNERS = ["@kahrendt"] + +CONF_MEDIA_PIPELINE = "media_pipeline" +CONF_ON_MUTE = "on_mute" +CONF_ON_UNMUTE = "on_unmute" +CONF_ON_VOLUME = "on_volume" +CONF_SOURCES = "sources" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_INITIAL = "volume_initial" +CONF_VOLUME_MAX = "volume_max" +CONF_VOLUME_MIN = "volume_min" + +speaker_source_ns = cg.esphome_ns.namespace("speaker_source") + +SpeakerSourceMediaPlayer = speaker_source_ns.class_( + "SpeakerSourceMediaPlayer", cg.Component, media_player.MediaPlayer +) + +PipelineContext = speaker_source_ns.struct("PipelineContext") + +Pipeline = speaker_source_ns.enum("Pipeline") + + +FORMAT_MAPPING = { + "FLAC": "flac", + "MP3": "mp3", + "OPUS": "opus", + "WAV": "wav", +} + + +# Returns a media_player.MediaPlayerSupportedFormat struct with the configured +# format, sample rate, number of channels, purpose, and bytes per sample +def _get_supported_format_struct(pipeline: ConfigType): + args = [ + media_player.MediaPlayerSupportedFormat, + ] + + args.append(("format", FORMAT_MAPPING[pipeline[CONF_FORMAT]])) + + args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) + args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) + args.append(("purpose", media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"])) + + # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails + # if the number of bytes per sample is specified for MP3. + if pipeline[CONF_FORMAT] != "MP3": + args.append(("sample_bytes", 2)) + + return cg.StructInitializer(*args) + + +def _validate_pipeline(config: ConfigType) -> ConfigType: + # Inherit settings from speaker if not manually set + inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) + inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) + + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + + audio.final_validate_audio_schema( + "speaker_source media_player", + audio_device=CONF_SPEAKER, + bits_per_sample=16, + channels=config.get(CONF_NUM_CHANNELS), + sample_rate=config.get(CONF_SAMPLE_RATE), + )(config) + + return config + + +PIPELINE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id( + PipelineContext + ), # Needed to inherit audio settings from the speaker + cv.Required(CONF_SPEAKER): cv.use_id(speaker.Speaker), + cv.Required(CONF_SOURCES): cv.All( + cv.ensure_list(cv.use_id(media_source.MediaSource)), + cv.Length(min=1), + ), + cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM), + cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1), + cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2), + } +) + + +def _validate_volume_settings(config: ConfigType) -> ConfigType: + # CONF_VOLUME_INITIAL is in the scaled volume domain (0.0-1.0) and doesn't need to be validated + if config[CONF_VOLUME_MIN] > config[CONF_VOLUME_MAX]: + raise cv.Invalid( + f"{CONF_VOLUME_MIN} ({config[CONF_VOLUME_MIN]}) must be less than or equal to {CONF_VOLUME_MAX} ({config[CONF_VOLUME_MAX]})" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, + cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, + cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, + cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, + cv.Required(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(media_player.media_player_schema(SpeakerSourceMediaPlayer)), + cv.only_on_esp32, + _validate_volume_settings, +) + + +def _final_validate_codecs(config: ConfigType) -> ConfigType: + pipeline = config[CONF_MEDIA_PIPELINE] + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + elif fmt == "FLAC": + audio.request_flac_support() + elif fmt == "MP3": + audio.request_mp3_support() + elif fmt == "OPUS": + audio.request_opus_support() + + return config + + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_MEDIA_PIPELINE): _validate_pipeline, + }, + extra=cv.ALLOW_EXTRA, + ), + _final_validate_codecs, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_player.register_media_player(var, config) + + cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) + cg.add(var.set_volume_initial(config[CONF_VOLUME_INITIAL])) + cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) + cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) + + pipeline_config = config[CONF_MEDIA_PIPELINE] + pipeline_enum = Pipeline.MEDIA_PIPELINE + + for source in pipeline_config[CONF_SOURCES]: + src = await cg.get_variable(source) + cg.add(var.add_media_source(pipeline_enum, src)) + + cg.add( + var.set_speaker( + pipeline_enum, + await cg.get_variable(pipeline_config[CONF_SPEAKER]), + ) + ) + if pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_format( + pipeline_enum, + _get_supported_format_struct(pipeline_config), + ) + ) + + if on_mute := config.get(CONF_ON_MUTE): + await automation.build_automation( + var.get_mute_trigger(), + [], + on_mute, + ) + if on_unmute := config.get(CONF_ON_UNMUTE): + await automation.build_automation( + var.get_unmute_trigger(), + [], + on_unmute, + ) + if on_volume := config.get(CONF_ON_VOLUME): + await automation.build_automation( + var.get_volume_trigger(), + [(cg.float_, "x")], + on_volume, + ) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp new file mode 100644 index 00000000000..a3679891d2d --- /dev/null +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -0,0 +1,546 @@ +#include "speaker_source_media_player.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::speaker_source { + +static constexpr uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; + +static const char *const TAG = "speaker_source_media_player"; + +// SourceBinding method implementations (defined here because SpeakerSourceMediaPlayer is forward-declared in the +// header) + +// THREAD CONTEXT: Called from media source decode task thread +size_t SourceBinding::write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + return this->player->handle_media_output_(this->pipeline, this->source, data, length, timeout_ms, stream_info); +} + +// THREAD CONTEXT: Called from main loop (media source's loop() calls set_state_ which calls report_state) +void SourceBinding::report_state(media_source::MediaSourceState state) { + this->player->handle_media_state_changed_(this->pipeline, this->source, state); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_volume(float volume) { + this->player->defer([this, volume]() { this->player->handle_volume_request_(volume); }); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_mute(bool is_muted) { + this->player->defer([this, is_muted]() { this->player->handle_mute_request_(is_muted); }); +} + +// THREAD CONTEXT: Called from media source task thread; uses defer() to marshal to main loop +void SourceBinding::request_play_uri(const std::string &uri) { + this->player->defer([this, uri]() { this->player->handle_play_uri_request_(this->pipeline, uri); }); +} + +// THREAD CONTEXT: Called during code generation setup (main loop) +void SpeakerSourceMediaPlayer::add_media_source(uint8_t pipeline, media_source::MediaSource *media_source) { + auto &binding = + this->pipelines_[pipeline].sources.emplace_back(std::make_unique<SourceBinding>(this, media_source, pipeline)); + media_source->set_listener(binding.get()); +} + +void SpeakerSourceMediaPlayer::dump_config() { + ESP_LOGCONFIG(TAG, + "Speaker Source Media Player:\n" + " Volume Increment: %.2f\n" + " Volume Min: %.2f\n" + " Volume Max: %.2f", + this->volume_increment_, this->volume_min_, this->volume_max_); +} + +void SpeakerSourceMediaPlayer::setup() { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + + this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaPlayerControlCommand)); + + this->pref_ = this->make_entity_preference<VolumeRestoreState>(); + + VolumeRestoreState volume_restore_state; + if (this->pref_.load(&volume_restore_state)) { + this->set_volume_(volume_restore_state.volume); + this->set_mute_state_(volume_restore_state.is_muted); + } else { + this->set_volume_(this->volume_initial_); + this->set_mute_state_(false); + } + + // Register callbacks to receive playback notifications from speakers + for (size_t i = 0; i < this->pipelines_.size(); i++) { + if (this->pipelines_[i].is_configured()) { + this->pipelines_[i].speaker->add_audio_output_callback([this, i](uint32_t frames, int64_t timestamp) { + this->handle_speaker_playback_callback_(frames, timestamp, i); + }); + } + } +} + +// THREAD CONTEXT: Called from the speaker's playback callback task (not main loop) +void SpeakerSourceMediaPlayer::handle_speaker_playback_callback_(uint32_t frames, int64_t timestamp, uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Load once so the null check and use below are consistent + media_source::MediaSource *active_source = ps.active_source.load(std::memory_order_relaxed); + if (active_source == nullptr) { + return; + } + + // CAS loop to safely subtract frames without underflow. If pending_frames is reset to 0 (new source + // starting) between the load and the subtract, compare_exchange_weak will fail and reload the current value. + uint32_t current = ps.pending_frames.load(std::memory_order_relaxed); + uint32_t source_frames; + do { + source_frames = std::min(frames, current); + } while (source_frames > 0 && + !ps.pending_frames.compare_exchange_weak(current, current - source_frames, std::memory_order_relaxed)); + + if (source_frames > 0) { + // Notify the source about the played audio + active_source->notify_audio_played(source_frames, timestamp); + } +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_volume_request_(float volume) { + // Update the media player's volume + this->set_volume_(volume); + this->publish_state(); +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_mute_request_(bool is_muted) { + // Update the media player's mute state + this->set_mute_state_(is_muted); + this->publish_state(); +} + +// THREAD CONTEXT: Called from main loop via defer() +void SpeakerSourceMediaPlayer::handle_play_uri_request_(uint8_t pipeline, const std::string &uri) { + // Smart source is requesting the player to play a different URI + auto call = this->make_call(); + call.set_media_url(uri); + call.perform(); +} + +// THREAD CONTEXT: Called from main loop (media source's loop() calls set_state_ which calls report_state) +void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, media_source::MediaSource *source, + media_source::MediaSourceState state) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (state == media_source::MediaSourceState::IDLE) { + // Source went idle - clear stopping flag if this was the source we asked to stop + if (ps.stopping_source == source) { + ps.stopping_source = nullptr; + } + + // Clear pending flag if this was the source we asked to play + if (ps.pending_source == source) { + ps.pending_source = nullptr; + } + + // Source went idle - clear it if it's the active source + if (ps.active_source == source) { + ps.last_source = ps.active_source; + ps.active_source = nullptr; + + // Finish the speaker to ensure it's ready for the next playback + ps.speaker->finish(); + } + } else if (state == media_source::MediaSourceState::PLAYING) { + // Source started playing - make it the active source if no one else is active + if (ps.active_source == nullptr) { + ps.active_source = source; + ps.last_source = nullptr; + + // Clear pending flag now that the source is active + if (ps.pending_source == source) { + ps.pending_source = nullptr; + } + } + } +} + +// THREAD CONTEXT: Called from media source decode task thread (not main loop). +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// ps.speaker methods (speaker pointer is immutable after setup). +size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, + const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Single read; the if-body only uses ps.speaker (immutable after setup) and the source parameter. + if (ps.active_source == source) { + // This source is active - play the audio + if (ps.speaker->get_audio_stream_info() != stream_info) { + // Setup the speaker to play this stream + ps.speaker->set_audio_stream_info(stream_info); + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; + } + size_t bytes_written = ps.speaker->play(data, length, pdMS_TO_TICKS(timeout_ms)); + if (bytes_written > 0) { + // Track frames sent to speaker for this source + ps.pending_frames.fetch_add(stream_info.bytes_to_frames(bytes_written), std::memory_order_relaxed); + } + return bytes_written; + } + + // Not the active source - wait for state callback to set us as active when we transition to PLAYING + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; +} + +media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( + media_source::MediaSource *source) const { + if (source != nullptr) { + switch (source->get_state()) { + case media_source::MediaSourceState::PLAYING: + return media_player::MEDIA_PLAYER_STATE_PLAYING; + case media_source::MediaSourceState::PAUSED: + return media_player::MEDIA_PLAYER_STATE_PAUSED; + case media_source::MediaSourceState::ERROR: + ESP_LOGE(TAG, "Source error"); + return media_player::MEDIA_PLAYER_STATE_IDLE; + case media_source::MediaSourceState::IDLE: + default: + return media_player::MEDIA_PLAYER_STATE_IDLE; + } + } + + return media_player::MEDIA_PLAYER_STATE_IDLE; +} + +void SpeakerSourceMediaPlayer::loop() { + // Process queued control commands + MediaPlayerControlCommand control_command; + + // Use peek to check command without removing it + if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) == pdTRUE) { + bool command_executed = false; + uint8_t pipeline = control_command.pipeline; + + switch (control_command.type) { + case MediaPlayerControlCommand::PLAY_URI: { + command_executed = this->try_execute_play_uri_(*control_command.data.uri, pipeline); + break; + } + + case MediaPlayerControlCommand::SEND_COMMAND: { + PipelineContext &ps = this->pipelines_[pipeline]; + + // Determine target source: prefer active, fall back to last + media_source::MediaSource *target_source = nullptr; + if (ps.active_source != nullptr) { + target_source = ps.active_source; + } else if (ps.last_source != nullptr) { + target_source = ps.last_source; + } + + media_player::MediaPlayerCommand player_command = control_command.data.command; + switch (player_command) { + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { + media_source::MediaSource *active_source = ps.active_source; + if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + } else { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PLAY: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_STOP: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::STOP); + } + break; + } + + default: + break; + } + + command_executed = true; + break; + } + } + + // Only remove from queue if successfully executed + if (command_executed) { + xQueueReceive(this->media_control_command_queue_, &control_command, 0); + + // Delete the allocated string for PLAY_URI commands + if (control_command.type == MediaPlayerControlCommand::PLAY_URI) { + delete control_command.data.uri; + } + } + } + + // Update state based on active sources + media_player::MediaPlayerState old_state = this->state; + + PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; + this->state = this->get_media_pipeline_state_(media_ps.active_source); + + if (this->state != old_state) { + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +} + +media_source::MediaSource *SpeakerSourceMediaPlayer::find_source_for_uri_(const std::string &uri, uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *first_match = nullptr; + for (auto &binding : ps.sources) { + if (binding->source->can_handle(uri)) { + // Prefer an idle source; otherwise remember the first match (will be stopped by try_execute_play_uri_) + if (binding->source->get_state() == media_source::MediaSourceState::IDLE) { + return binding->source; + } + if (first_match == nullptr) { + first_match = binding->source; + } + } + } + return first_match; +} + +bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uint8_t pipeline) { + // Find target source + media_source::MediaSource *target_source = this->find_source_for_uri_(uri, pipeline); + if (target_source == nullptr) { + ESP_LOGW(TAG, "No source for URI"); + ESP_LOGV(TAG, "URI: %s", uri.c_str()); + return true; // Remove from queue (unrecoverable) + } + + PipelineContext &ps = this->pipelines_[pipeline]; + + media_source::MediaSource *active_source = ps.active_source; + + // If active source exists and is not IDLE, stop it and wait + if (active_source != nullptr) { + media_source::MediaSourceState active_state = active_source->get_state(); + if (active_state != media_source::MediaSourceState::IDLE) { + // Only send END command once per source - check if we've already asked this source to stop + if (ps.stopping_source != active_source) { + ESP_LOGV(TAG, "Pipeline %u: stopping active source", pipeline); + active_source->handle_command(media_source::MediaSourceCommand::STOP); + ps.speaker->stop(); + ps.stopping_source = active_source; + } + return false; // Leave in queue, retry next loop + } + } + + // Also check target source directly - handles case where source errored before PLAYING state + media_source::MediaSourceState target_state = target_source->get_state(); + if (target_state != media_source::MediaSourceState::IDLE) { + // Only send STOP command once per source + if (ps.stopping_source != target_source) { + ESP_LOGV(TAG, "Pipeline %u: target source busy, stopping", pipeline); + target_source->handle_command(media_source::MediaSourceCommand::STOP); + ps.speaker->stop(); + ps.stopping_source = target_source; + } + return false; // Leave in queue, retry next loop + } + + // Clear stopping flag since we're past the stopping phase + ps.stopping_source = nullptr; + + // Check if speaker is ready + if (!ps.speaker->is_stopped()) { + return false; // Speaker not ready yet, retry later + } + + // Set pending source so handle_media_state_changed_ can recognize it when the source transitions to PLAYING + ps.pending_source = target_source; + + // Speaker is ready, try to play + if (!target_source->play_uri(uri)) { + ESP_LOGE(TAG, "Pipeline %u: Failed to play URI: %s", pipeline, uri.c_str()); + ps.pending_source = nullptr; + } + + // Reset pending frame counter for this pipeline since we're starting a new source + ps.pending_frames.store(0, std::memory_order_relaxed); + + return true; // Remove from queue +} + +// THREAD CONTEXT: Called from main loop only. Entry points: +// - HA/automation commands (direct) +// - handle_play_uri_request_() via make_call().perform() (deferred from source tasks) +void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (!this->is_ready()) { + return; + } + + MediaPlayerControlCommand control_command; + control_command.pipeline = MEDIA_PIPELINE; + + auto media_url = call.get_media_url(); + if (media_url.has_value()) { + control_command.type = MediaPlayerControlCommand::PLAY_URI; + // Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens + // can easily exceed 500 bytes). Deleted after the command is consumed. FreeRTOS queues require items to be + // copyable, so we store a pointer to the string in the queue rather than the string itself. + control_command.data.uri = new std::string(media_url.value()); + if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + delete control_command.data.uri; + ESP_LOGE(TAG, "Queue full, URI dropped"); + } + return; + } + + auto volume = call.get_volume(); + if (volume.has_value()) { + this->set_volume_(volume.value()); + this->publish_state(); + return; + } + + auto cmd = call.get_command(); + if (cmd.has_value()) { + switch (cmd.value()) { + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->set_mute_state_(true); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->set_mute_state_(false); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: + this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_)); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_)); + break; + default: + // Queue command for processing in loop() + control_command.type = MediaPlayerControlCommand::SEND_COMMAND; + control_command.data.command = cmd.value(); + if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + ESP_LOGE(TAG, "Queue full, command dropped"); + } + return; + } + this->publish_state(); + } +} + +media_player::MediaPlayerTraits SpeakerSourceMediaPlayer::get_traits() { + auto traits = media_player::MediaPlayerTraits(); + traits.set_supports_pause(true); + + for (const auto &ps : this->pipelines_) { + if (ps.format.has_value()) { + traits.get_supported_formats().push_back(ps.format.value()); + } + } + + return traits; +} + +void SpeakerSourceMediaPlayer::save_volume_restore_state_() { + VolumeRestoreState volume_restore_state; + volume_restore_state.volume = this->volume; + volume_restore_state.is_muted = this->is_muted_; + this->pref_.save(&volume_restore_state); +} + +void SpeakerSourceMediaPlayer::set_mute_state_(bool mute_state, bool publish) { + if (this->is_muted_ == mute_state) { + return; + } + + for (auto &ps : this->pipelines_) { + if (ps.is_configured()) { + ps.speaker->set_mute_state(mute_state); + } + } + + this->is_muted_ = mute_state; + + if (publish) { + this->save_volume_restore_state_(); + } + + // Notify all media sources about the mute state change + for (auto &ps : this->pipelines_) { + for (auto &binding : ps.sources) { + binding->source->notify_mute_changed(mute_state); + } + } + + if (mute_state) { + this->defer([this]() { this->mute_trigger_.trigger(); }); + } else { + this->defer([this]() { this->unmute_trigger_.trigger(); }); + } +} + +void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { + // Remap the volume to fit within the configured limits + float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + + for (auto &ps : this->pipelines_) { + if (ps.is_configured()) { + ps.speaker->set_volume(bounded_volume); + } + } + + if (publish) { + this->volume = volume; + } + + // Notify all media sources about the volume change + for (auto &ps : this->pipelines_) { + for (auto &binding : ps.sources) { + binding->source->notify_volume_changed(volume); + } + } + + // Turn on the mute state if the volume is effectively zero, off otherwise. + // Pass publish=false to avoid saving twice. + if (volume < 0.001) { + this->set_mute_state_(true, false); + } else { + this->set_mute_state_(false, false); + } + + // Save after mute mutation so the restored state has the correct is_muted_ value + if (publish) { + this->save_volume_restore_state_(); + } + + this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); +} + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h new file mode 100644 index 00000000000..7896fef295a --- /dev/null +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -0,0 +1,217 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/components/media_player/media_player.h" +#include "esphome/components/speaker/speaker.h" + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +#include <array> +#include <atomic> +#include <memory> +#include <vector> +#include <freertos/FreeRTOS.h> +#include <freertos/queue.h> + +namespace esphome::speaker_source { + +// THREADING MODEL: +// This component coordinates media sources that run their own decode tasks with speakers +// that have their own playback callback tasks. Three thread contexts exist: +// +// - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), +// handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), +// set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), +// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_() +// +// - Media source task(s): handle_media_output_() via SourceBinding::write_audio(). +// Called from each source's decode task thread when streaming audio data. +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// ps.speaker methods (speaker pointer is immutable after setup). +// +// - Speaker callback task: handle_speaker_playback_callback_() via speaker's +// add_audio_output_callback(). Called when the speaker finishes writing frames to the DAC. +// Reads ps.active_source (atomic), writes ps.pending_frames (atomic), and calls +// active_source->notify_audio_played(). +// +// control() is only called from the main loop (HA/automation commands). +// Source tasks use defer() for all requests (volume, mute, play_uri). +// +// Thread-safe communication: +// - FreeRTOS queue (media_control_command_queue_): control() -> loop() for play/command dispatch +// - defer(): SourceBinding::request_volume/request_mute/request_play_uri -> main loop +// - Atomic fields (active_source, pending_frames): shared between all three thread contexts +// +// Non-atomic pipeline fields (last_source, stopping_source, pending_source) are only accessed +// from the main loop thread. + +enum Pipeline : uint8_t { + MEDIA_PIPELINE = 0, +}; + +// Forward declaration +class SpeakerSourceMediaPlayer; + +/// @brief Per-source listener binding that captures the source pointer at registration time. +/// Each binding implements MediaSourceListener and forwards callbacks to the player with the source identified. +/// Defined before PipelineContext so pipelines can own their bindings directly. +struct SourceBinding : public media_source::MediaSourceListener { + SourceBinding(SpeakerSourceMediaPlayer *player, media_source::MediaSource *source, uint8_t pipeline) + : player(player), source(source), pipeline(pipeline) {} + SpeakerSourceMediaPlayer *player; + media_source::MediaSource *source; + uint8_t pipeline; + + // Implementations are in the .cpp file because SpeakerSourceMediaPlayer is only forward-declared here + size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms, + const audio::AudioStreamInfo &stream_info) override; + void report_state(media_source::MediaSourceState state) override; + void request_volume(float volume) override; + void request_mute(bool is_muted) override; + void request_play_uri(const std::string &uri) override; +}; + +struct PipelineContext { + speaker::Speaker *speaker{nullptr}; + optional<media_player::MediaPlayerSupportedFormat> format; + + std::atomic<media_source::MediaSource *> active_source{nullptr}; + media_source::MediaSource *last_source{nullptr}; + media_source::MediaSource *stopping_source{nullptr}; // Source we've asked to stop, awaiting IDLE + media_source::MediaSource *pending_source{nullptr}; // Source we've asked to play, awaiting PLAYING + + // Each SourceBinding pairs a MediaSource* with its listener implementation. + // Uses unique_ptr so binding addresses are stable and set_listener() can be called in add_media_source(). + // Uses std::vector because the count varies across instances (multiple speaker_source media players may exist). + std::vector<std::unique_ptr<SourceBinding>> sources; + + // Track frames sent to speaker to correlate with playback callbacks. + // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback + // callback. + std::atomic<uint32_t> pending_frames{0}; + + /// @brief Check if this pipeline is configured (has a speaker assigned) + bool is_configured() const { return this->speaker != nullptr; } +}; + +struct MediaPlayerControlCommand { + enum Type : uint8_t { + PLAY_URI, // Find a source that can handle this URI and play it + SEND_COMMAND, // Send command to active source + }; + Type type; + uint8_t pipeline; + + union { + std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI) + media_player::MediaPlayerCommand command; + } data; +}; + +struct VolumeRestoreState { + float volume; + bool is_muted; +}; + +class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPlayer { + friend struct SourceBinding; + + public: + float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + void dump_config() override; + + // MediaPlayer implementations + media_player::MediaPlayerTraits get_traits() override; + bool is_muted() const override { return this->is_muted_; } + + // Percentage to increase or decrease the volume for volume up or volume down commands + void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } + + // Volume used initially on first boot when no volume had been previously saved + void set_volume_initial(float volume_initial) { this->volume_initial_ = volume_initial; } + + void set_volume_max(float volume_max) { this->volume_max_ = volume_max; } + void set_volume_min(float volume_min) { this->volume_min_ = volume_min; } + + /// @brief Adds a media source to a pipeline and registers this player as its listener + void add_media_source(uint8_t pipeline, media_source::MediaSource *media_source); + + void set_speaker(uint8_t pipeline, speaker::Speaker *speaker) { this->pipelines_[pipeline].speaker = speaker; } + void set_format(uint8_t pipeline, const media_player::MediaPlayerSupportedFormat &format) { + this->pipelines_[pipeline].format = format; + } + + Trigger<> *get_mute_trigger() { return &this->mute_trigger_; } + Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } + Trigger<float> *get_volume_trigger() { return &this->volume_trigger_; } + + protected: + // Callbacks from source bindings (pipeline index is captured at binding creation time) + size_t handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, const uint8_t *data, size_t length, + uint32_t timeout_ms, const audio::AudioStreamInfo &stream_info); + void handle_media_state_changed_(uint8_t pipeline, media_source::MediaSource *source, + media_source::MediaSourceState state); + void handle_volume_request_(float volume); + void handle_mute_request_(bool is_muted); + void handle_play_uri_request_(uint8_t pipeline, const std::string &uri); + + void handle_speaker_playback_callback_(uint32_t frames, int64_t timestamp, uint8_t pipeline); + + // Receives commands from HA or from the voice assistant component + // Sends commands to the media_control_command_queue_ + void control(const media_player::MediaPlayerCall &call) override; + + /// @brief Updates this->volume and saves volume/mute state to flash for restoration if publish is true. + void set_volume_(float volume, bool publish = true); + + /// @brief Sets the mute state. + /// @param mute_state If true, audio will be muted. If false, audio will be unmuted + /// @param publish If true, saves volume/mute state to flash for restoration + void set_mute_state_(bool mute_state, bool publish = true); + + /// @brief Saves the current volume and mute state to the flash for restoration. + void save_volume_restore_state_(); + + /// @brief Determine media player state from the media pipeline's active source + /// @param media_source Active source for the media pipeline (may be nullptr) + /// @return The appropriate MediaPlayerState + media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source) const; + + bool try_execute_play_uri_(const std::string &uri, uint8_t pipeline); + media_source::MediaSource *find_source_for_uri_(const std::string &uri, uint8_t pipeline); + QueueHandle_t media_control_command_queue_; + + // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. + std::array<PipelineContext, 1> pipelines_; + + // Used to save volume/mute state for restoration on reboot + ESPPreferenceObject pref_; + + Trigger<> mute_trigger_; + Trigger<> unmute_trigger_; + Trigger<float> volume_trigger_; + + // The amount to change the volume on volume up/down commands + float volume_increment_; + + // The initial volume used by Setup when no previous volume was saved + float volume_initial_; + + float volume_max_; + float volume_min_; + + bool is_muted_{false}; +}; + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml new file mode 100644 index 00000000000..cfcb065f57c --- /dev/null +++ b/tests/components/speaker_source/common.yaml @@ -0,0 +1,43 @@ +i2s_audio: + i2s_lrclk_pin: ${i2s_bclk_pin} + i2s_bclk_pin: ${i2s_lrclk_pin} + i2s_mclk_pin: ${i2s_mclk_pin} + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: ${i2s_dout_pin} + sample_rate: 48000 + num_channels: 2 + +audio_file: + - id: test_audio + file: + type: local + path: $component_dir/test.wav + +media_source: + - platform: audio_file + id: audio_file_source + +media_player: + - platform: speaker_source + id: media_player_id + name: Media Player + volume_increment: 0.02 + volume_initial: 0.75 + volume_max: 0.95 + volume_min: 0.0 + media_pipeline: + speaker: speaker_id + format: FLAC + num_channels: 1 + sources: + - audio_file_source + on_mute: + - media_player.pause: + id: media_player_id + on_unmute: + - media_player.play: + id: media_player_id diff --git a/tests/components/speaker_source/test.esp32-idf.yaml b/tests/components/speaker_source/test.esp32-idf.yaml new file mode 100644 index 00000000000..e2439ebdf21 --- /dev/null +++ b/tests/components/speaker_source/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO23 + +<<: !include common.yaml diff --git a/tests/components/speaker_source/test.wav b/tests/components/speaker_source/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..f9d07ef2238eb2fcb355055466d3789ee1a1fe0b GIT binary patch literal 46 ycmWIYbaPW<U|<M$40BD(Em06)U|?WmU}R{pV_;yYWnf@p5MW42EJ<Wy0098Ji3Z~U literal 0 HcmV?d00001 From 8d988723cdcd18dbe5a464f3065017a3cf64cbcf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:38:50 -0400 Subject: [PATCH 0547/2056] [config] Allow !extend/!remove on components without id in schema (#14682) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/voluptuous_schema.py | 6 ++ .../external_components/common.yaml | 6 +- .../external_components/test.esp32-idf.yaml | 4 + tests/unit_tests/test_voluptuous_schema.py | 86 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/test_voluptuous_schema.py diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 7220fb307ff..0703c54a7a8 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -175,6 +175,12 @@ class _Schema(vol.Schema): else: if self.extra == vol.ALLOW_EXTRA: out[key] = value + elif key == "id": + # Silently drop 'id' on any dict so that + # !extend / !remove work on every list-based + # config without requiring each component to + # declare an id in its schema. + pass elif self.extra != vol.REMOVE_EXTRA: if isinstance(key, str) and key_names: matches = difflib.get_close_matches(key, key_names) diff --git a/tests/components/external_components/common.yaml b/tests/components/external_components/common.yaml index 2b51267ec68..c55129094c4 100644 --- a/tests/components/external_components/common.yaml +++ b/tests/components/external_components/common.yaml @@ -1,6 +1,8 @@ external_components: - - source: github://esphome/esphome@dev + - id: my_ext + source: github://esphome/esphome@dev refresh: 1d components: [bh1750] - - source: ../../../esphome/components + - id: my_local + source: ../../../esphome/components components: [sntp] diff --git a/tests/components/external_components/test.esp32-idf.yaml b/tests/components/external_components/test.esp32-idf.yaml index dade44d145b..afe2bd5d6ae 100644 --- a/tests/components/external_components/test.esp32-idf.yaml +++ b/tests/components/external_components/test.esp32-idf.yaml @@ -1 +1,5 @@ +# WARNING: Using !extend or !remove prevents automatic component grouping in CI, making builds slower. <<: !include common.yaml + +external_components: + - id: !remove my_local diff --git a/tests/unit_tests/test_voluptuous_schema.py b/tests/unit_tests/test_voluptuous_schema.py new file mode 100644 index 00000000000..21c7decede1 --- /dev/null +++ b/tests/unit_tests/test_voluptuous_schema.py @@ -0,0 +1,86 @@ +"""Tests for voluptuous_schema.py.""" + +import pytest +import voluptuous as vol + +from esphome.voluptuous_schema import _Schema + + +class TestIdKeyDropping: + """Test that 'id' keys are silently dropped in PREVENT_EXTRA schemas.""" + + def test_id_key_silently_dropped(self): + """Schema without 'id' should accept and drop 'id' key from input.""" + schema = _Schema( + { + vol.Required("name"): str, + vol.Optional("value", default=0): int, + } + ) + result = schema({"name": "test", "value": 42, "id": "my_id"}) + assert result == {"name": "test", "value": 42} + assert "id" not in result + + def test_id_key_dropped_with_only_required(self): + """Schema with only required keys should still drop 'id'.""" + schema = _Schema( + { + vol.Required("source"): str, + } + ) + result = schema({"source": "github://test", "id": "my_component"}) + assert result == {"source": "github://test"} + + def test_other_extra_keys_still_rejected(self): + """Non-'id' extra keys should still raise errors.""" + schema = _Schema( + { + vol.Required("name"): str, + } + ) + with pytest.raises(vol.MultipleInvalid, match="extra keys not allowed"): + schema({"name": "test", "unknown_key": "value"}) + + def test_id_key_not_dropped_when_in_schema(self): + """When 'id' is declared in the schema, it should be validated normally.""" + schema = _Schema( + { + vol.Required("id"): str, + vol.Required("name"): str, + } + ) + result = schema({"id": "my_id", "name": "test"}) + assert result == {"id": "my_id", "name": "test"} + + def test_id_key_not_dropped_with_allow_extra(self): + """With ALLOW_EXTRA, 'id' should be kept (not dropped).""" + schema = _Schema( + { + vol.Required("name"): str, + }, + extra=vol.ALLOW_EXTRA, + ) + result = schema({"name": "test", "id": "my_id"}) + assert result == {"name": "test", "id": "my_id"} + + def test_id_key_dropped_with_remove_extra(self): + """With REMOVE_EXTRA, 'id' should be removed along with other extras.""" + schema = _Schema( + { + vol.Required("name"): str, + }, + extra=vol.REMOVE_EXTRA, + ) + result = schema({"name": "test", "id": "my_id", "other": "value"}) + assert result == {"name": "test"} + + def test_without_id_no_extra_keys(self): + """Normal validation without 'id' key should work as before.""" + schema = _Schema( + { + vol.Required("name"): str, + vol.Optional("value", default=0): int, + } + ) + result = schema({"name": "test"}) + assert result == {"name": "test", "value": 0} From 6356e3def9dce68f3e815d5bd58d2c42ef4ccb7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 10:42:38 -1000 Subject: [PATCH 0548/2056] [core] Warn on crystal frequency mismatch during serial upload (#14582) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/__main__.py | 58 ++++++++++- esphome/util.py | 45 +++++++-- tests/unit_tests/test_main.py | 124 ++++++++++++++++++++++++ tests/unit_tests/test_util.py | 175 ++++++++++++++++++++++++++++++++++ 4 files changed, 391 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index f33e7f4b426..3f0da85a694 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,6 +1,7 @@ # PYTHON_ARGCOMPLETE_OK import argparse from collections.abc import Callable +from contextlib import suppress from datetime import datetime import functools import getpass @@ -687,6 +688,47 @@ def _check_and_emit_build_info() -> None: ) +def _get_configured_xtal_freq() -> int | None: + """Read the configured crystal frequency from the sdkconfig file.""" + sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") + if not sdkconfig_path.is_file(): + return None + with suppress(OSError, ValueError): + content = sdkconfig_path.read_text() + for line in content.splitlines(): + if line.startswith("CONFIG_XTAL_FREQ="): + return int(line.split("=", 1)[1]) + return None + + +def _make_crystal_freq_callback( + configured_freq: int, +) -> Callable[[str], str | None]: + """Create a callback that checks esptool crystal frequency output.""" + crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz") + + def check_crystal_line(line: str) -> str | None: + if not (match := crystal_re.search(line)): + return None + detected = int(match.group(1)) + if detected == configured_freq: + return None + return ( + f"\n\033[33mWARNING: Crystal frequency mismatch! " + f"Device reports {detected}MHz but firmware is configured " + f"for {configured_freq}MHz.\n" + f"UART logging and other clock-dependent features will not " + f"work correctly.\n" + f"Set the correct crystal frequency with sdkconfig_options:\n" + f" esp32:\n" + f" framework:\n" + f" sdkconfig_options:\n" + f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n" + ) + + return check_crystal_line + + def upload_using_esptool( config: ConfigType, port: str, file: str, speed: int ) -> str | int: @@ -715,6 +757,14 @@ def upload_using_esptool( mcu = get_esp32_variant().lower() + line_callbacks: list[Callable[[str], str | None]] = [] + if ( + CORE.is_esp32 + and file is None + and (configured_freq := _get_configured_xtal_freq()) is not None + ): + line_callbacks.append(_make_crystal_freq_callback(configured_freq)) + def run_esptool(baud_rate): cmd = [ "esptool", @@ -739,9 +789,13 @@ def upload_using_esptool( if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool - return run_external_command(esptool.main, *cmd) # pylint: disable=no-member + return run_external_command( + esptool.main, # pylint: disable=no-member + *cmd, + line_callbacks=line_callbacks, + ) - return run_external_process(*cmd) + return run_external_process(*cmd, line_callbacks=line_callbacks) rc = run_esptool(first_baudrate) if rc == 0 or first_baudrate == 115200: diff --git a/esphome/util.py b/esphome/util.py index 6a21b4f627f..73cc3aa5ab6 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -125,7 +125,12 @@ ANSI_ESCAPE = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]") class RedirectText: - def __init__(self, out, filter_lines=None): + def __init__( + self, + out, + filter_lines: list[str] | None = None, + line_callbacks: list[Callable[[str], str | None]] | None = None, + ) -> None: self._out = out if filter_lines is None: self._filter_pattern = None @@ -133,6 +138,7 @@ class RedirectText: pattern = r"|".join(r"(?:" + pattern + r")" for pattern in filter_lines) self._filter_pattern = re.compile(pattern) self._line_buffer = "" + self._line_callbacks = line_callbacks or [] def __getattr__(self, item): return getattr(self._out, item) @@ -157,7 +163,7 @@ class RedirectText: if not isinstance(s, str): s = s.decode() - if self._filter_pattern is not None: + if self._filter_pattern is not None or self._line_callbacks: self._line_buffer += s lines = self._line_buffer.splitlines(True) for line in lines: @@ -169,7 +175,10 @@ class RedirectText: line_without_ansi = ANSI_ESCAPE.sub("", line) line_without_end = line_without_ansi.rstrip() - if self._filter_pattern.match(line_without_end) is not None: + if ( + self._filter_pattern is not None + and self._filter_pattern.match(line_without_end) is not None + ): # Filter pattern matched, ignore the line continue @@ -181,6 +190,9 @@ class RedirectText: and (help_msg := get_esp32_arduino_flash_error_help()) ): self._write_color_replace(help_msg) + for callback in self._line_callbacks: + if msg := callback(line_without_end): + self._write_color_replace(msg) else: self._write_color_replace(s) @@ -194,7 +206,11 @@ class RedirectText: def run_external_command( - func, *cmd, capture_stdout: bool = False, filter_lines: str = None + func, + *cmd, + capture_stdout: bool = False, + filter_lines: list[str] | None = None, + line_callbacks: list[Callable[[str], str | None]] | None = None, ) -> int | str: """ Run a function from an external package that acts like a main method. @@ -204,7 +220,9 @@ def run_external_command( :param func: Function to execute :param cmd: Command to run as (eg first element of sys.argv) :param capture_stdout: Capture text from stdout and return that. - :param filter_lines: Regular expression used to filter captured output. + Note: line_callbacks are not invoked when capture_stdout is True. + :param filter_lines: Regular expressions used to filter captured output. + :param line_callbacks: Callbacks invoked per line; non-None returns are written to output. :return: str if `capture_stdout` is set else int exit code. """ @@ -218,9 +236,13 @@ def run_external_command( _LOGGER.debug("Running: %s", full_cmd) orig_stdout = sys.stdout - sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sys.stdout = RedirectText( + sys.stdout, filter_lines=filter_lines, line_callbacks=line_callbacks + ) orig_stderr = sys.stderr - sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + sys.stderr = RedirectText( + sys.stderr, filter_lines=filter_lines, line_callbacks=line_callbacks + ) if capture_stdout: cap_stdout = sys.stdout = io.StringIO() @@ -254,14 +276,19 @@ def run_external_process(*cmd: str, **kwargs: Any) -> int | str: full_cmd = " ".join(shlex_quote(x) for x in cmd) _LOGGER.debug("Running: %s", full_cmd) filter_lines = kwargs.get("filter_lines") + line_callbacks = kwargs.get("line_callbacks") capture_stdout = kwargs.get("capture_stdout", False) if capture_stdout: sub_stdout = subprocess.PIPE else: - sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sub_stdout = RedirectText( + sys.stdout, filter_lines=filter_lines, line_callbacks=line_callbacks + ) - sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + sub_stderr = RedirectText( + sys.stderr, filter_lines=filter_lines, line_callbacks=line_callbacks + ) try: proc = subprocess.run( diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index b6f1a28086a..b8534611517 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -6,6 +6,7 @@ from collections.abc import Generator from dataclasses import dataclass import json import logging +import os from pathlib import Path import re import sys @@ -19,6 +20,8 @@ from pytest import CaptureFixture from esphome import platformio_api from esphome.__main__ import ( Purpose, + _get_configured_xtal_freq, + _make_crystal_freq_callback, choose_upload_log_host, command_analyze_memory, command_clean_all, @@ -3717,3 +3720,124 @@ esp32: clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else "" ) assert "secrets.yaml" not in summary_section + + +def test_get_configured_xtal_freq_reads_sdkconfig(tmp_path: Path) -> None: + """Test reading XTAL_FREQ from sdkconfig.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text( + "CONFIG_SOC_XTAL_SUPPORT_26M=y\nCONFIG_XTAL_FREQ=26\nCONFIG_XTAL_FREQ_26=y\n" + ) + assert _get_configured_xtal_freq() == 26 + + +def test_get_configured_xtal_freq_default_40(tmp_path: Path) -> None: + """Test reading default 40MHz XTAL_FREQ from sdkconfig.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\nCONFIG_XTAL_FREQ_40=y\n") + assert _get_configured_xtal_freq() == 40 + + +def test_get_configured_xtal_freq_missing_file(tmp_path: Path) -> None: + """Test that missing sdkconfig returns None.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + assert _get_configured_xtal_freq() is None + + +def test_get_configured_xtal_freq_no_xtal_line(tmp_path: Path) -> None: + """Test that sdkconfig without XTAL_FREQ returns None.""" + CORE.name = "test-device" + CORE.build_path = tmp_path + sdkconfig = tmp_path / "sdkconfig.test-device" + sdkconfig.write_text("CONFIG_OTHER=123\n") + assert _get_configured_xtal_freq() is None + + +def test_crystal_freq_callback_mismatch() -> None: + """Test callback returns warning on crystal frequency mismatch.""" + callback = _make_crystal_freq_callback(40) + result = callback("Crystal frequency: 26MHz") + assert result is not None + assert "26MHz" in result + assert "40MHz" in result + assert "CONFIG_XTAL_FREQ_26" in result + + +def test_crystal_freq_callback_match() -> None: + """Test callback returns None when frequencies match.""" + callback = _make_crystal_freq_callback(40) + result = callback("Crystal frequency: 40MHz") + assert result is None + + +def test_crystal_freq_callback_no_crystal_line() -> None: + """Test callback returns None for unrelated lines.""" + callback = _make_crystal_freq_callback(40) + assert callback("Chip type: ESP8684H") is None + assert callback("MAC: a0:b7:65:8b:16:d4") is None + assert callback("") is None + + +def test_upload_using_esptool_passes_crystal_callback( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, +) -> None: + """Test that upload_using_esptool passes crystal freq callback for ESP32.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + # Create sdkconfig with XTAL_FREQ + build_dir = Path(CORE.build_path) + build_dir.mkdir(parents=True, exist_ok=True) + sdkconfig = build_dir / "sdkconfig.test" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [] + mock_get_idedata.return_value = mock_idedata + (tmp_path / "firmware.bin").touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + # Verify line_callbacks was passed with the crystal callback + call_kwargs = mock_run_external_command_main.call_args[1] + assert "line_callbacks" in call_kwargs + assert len(call_kwargs["line_callbacks"]) == 1 + + +def test_upload_using_esptool_subprocess_passes_crystal_callback( + mock_run_external_process: Mock, + mock_get_idedata: Mock, + tmp_path: Path, +) -> None: + """Test that crystal freq callback is passed via run_external_process.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + # Create sdkconfig with XTAL_FREQ + build_dir = Path(CORE.build_path) + build_dir.mkdir(parents=True, exist_ok=True) + sdkconfig = build_dir / "sdkconfig.test" + sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [] + mock_get_idedata.return_value = mock_idedata + (tmp_path / "firmware.bin").touch() + + config = {CONF_ESPHOME: {"platformio_options": {}}} + with patch.dict(os.environ, {"ESPHOME_USE_SUBPROCESS": "1"}): + upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + call_kwargs = mock_run_external_process.call_args[1] + assert "line_callbacks" in call_kwargs + assert len(call_kwargs["line_callbacks"]) == 1 diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index ca3fd9b78ab..ff58fb13944 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -2,9 +2,12 @@ from __future__ import annotations +from collections.abc import Callable +import io from pathlib import Path import subprocess import sys +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -407,6 +410,178 @@ def test_shlex_quote_edge_cases() -> None: assert util.shlex_quote(" ") == "' '" +def _make_redirect( + line_callbacks: list[Callable[[str], str | None]] | None = None, + filter_lines: list[str] | None = None, +) -> tuple[util.RedirectText, io.StringIO]: + """Create a RedirectText that writes to a StringIO buffer.""" + buf = io.StringIO() + redirect = util.RedirectText( + buf, filter_lines=filter_lines, line_callbacks=line_callbacks + ) + return redirect, buf + + +def test_redirect_text_callback_called_on_matching_line() -> None: + """Test that a line callback is called and its output is written.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "target" in line: + return "CALLBACK OUTPUT\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("some target line\n") + + assert "some target line" in buf.getvalue() + assert "CALLBACK OUTPUT" in buf.getvalue() + assert len(results) == 1 + + +def test_redirect_text_callback_not_triggered_on_non_matching_line() -> None: + """Test that callback returns None for non-matching lines.""" + + def callback(line: str) -> str | None: + if "target" in line: + return "FOUND\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("no match here\n") + + assert "no match here" in buf.getvalue() + assert "FOUND" not in buf.getvalue() + + +def test_redirect_text_callback_works_without_filter_pattern() -> None: + """Test that callbacks fire even when no filter_lines is set.""" + + def callback(line: str) -> str | None: + if "Crystal" in line: + return "WARNING: mismatch\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("Crystal frequency: 26MHz\n") + + assert "Crystal frequency: 26MHz" in buf.getvalue() + assert "WARNING: mismatch" in buf.getvalue() + + +def test_redirect_text_callback_works_with_filter_pattern() -> None: + """Test that callbacks fire alongside filter patterns.""" + + def callback(line: str) -> str | None: + if "important" in line: + return "NOTED\n" + return None + + redirect, buf = _make_redirect( + line_callbacks=[callback], + filter_lines=[r"^skip this.*"], + ) + redirect.write("skip this line\n") + redirect.write("important line\n") + + assert "skip this" not in buf.getvalue() + assert "important line" in buf.getvalue() + assert "NOTED" in buf.getvalue() + + +def test_redirect_text_multiple_callbacks() -> None: + """Test that multiple callbacks are all invoked.""" + + def callback_a(line: str) -> str | None: + if "test" in line: + return "FROM A\n" + return None + + def callback_b(line: str) -> str | None: + if "test" in line: + return "FROM B\n" + return None + + redirect, buf = _make_redirect(line_callbacks=[callback_a, callback_b]) + redirect.write("test line\n") + + output = buf.getvalue() + assert "FROM A" in output + assert "FROM B" in output + + +def test_redirect_text_incomplete_line_buffered() -> None: + """Test that incomplete lines are buffered until newline.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + return None + + redirect, buf = _make_redirect(line_callbacks=[callback]) + redirect.write("partial") + assert len(results) == 0 + + redirect.write(" line\n") + assert len(results) == 1 + assert results[0] == "partial line" + + +def test_run_external_command_line_callbacks(capsys: pytest.CaptureFixture) -> None: + """Test that run_external_command passes line_callbacks to RedirectText.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "hello" in line: + return "CALLBACK FIRED\n" + return None + + def fake_main() -> int: + print("hello world") + return 0 + + rc = util.run_external_command(fake_main, "fake", line_callbacks=[callback]) + + assert rc == 0 + assert len(results) == 1 + assert "hello world" in results[0] + captured = capsys.readouterr() + assert "CALLBACK FIRED" in captured.out + + +def test_run_external_process_line_callbacks() -> None: + """Test that run_external_process passes line_callbacks to RedirectText.""" + results: list[str] = [] + + def callback(line: str) -> str | None: + results.append(line) + if "from subprocess" in line: + return "PROCESS CALLBACK\n" + return None + + with patch("esphome.util.subprocess.run") as mock_run: + + def run_side_effect(*args: Any, **kwargs: Any) -> MagicMock: + # Simulate subprocess writing to the stdout RedirectText + stdout = kwargs.get("stdout") + if stdout is not None and isinstance(stdout, util.RedirectText): + stdout.write("from subprocess\n") + return MagicMock(returncode=0) + + mock_run.side_effect = run_side_effect + + rc = util.run_external_process( + "echo", + "test", + line_callbacks=[callback], + ) + + assert rc == 0 + assert any("from subprocess" in r for r in results) + + def test_get_picotool_path_found(tmp_path: Path) -> None: """Test picotool path derivation from cc_path.""" # Create the expected directory structure From 9513edc46875904c9638133c9feb612de9301c62 Mon Sep 17 00:00:00 2001 From: CFlix <38142312+CFlix@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:17:13 +0100 Subject: [PATCH 0549/2056] [dew_point] Add dew_point sensor component (#14441) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/dew_point/__init__.py | 1 + esphome/components/dew_point/dew_point.cpp | 82 +++++++++++++++++++ esphome/components/dew_point/dew_point.h | 26 ++++++ esphome/components/dew_point/sensor.py | 46 +++++++++++ tests/components/dew_point/common.yaml | 19 +++++ .../components/dew_point/test.esp32-idf.yaml | 1 + .../dew_point/test.esp8266-ard.yaml | 1 + .../components/dew_point/test.rp2040-ard.yaml | 1 + 9 files changed, 178 insertions(+) create mode 100644 esphome/components/dew_point/__init__.py create mode 100644 esphome/components/dew_point/dew_point.cpp create mode 100644 esphome/components/dew_point/dew_point.h create mode 100644 esphome/components/dew_point/sensor.py create mode 100644 tests/components/dew_point/common.yaml create mode 100644 tests/components/dew_point/test.esp32-idf.yaml create mode 100644 tests/components/dew_point/test.esp8266-ard.yaml create mode 100644 tests/components/dew_point/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 12aff01e730..e72b1647614 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -132,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core esphome/components/datetime/* @jesserockz @rfdarter esphome/components/debug/* @esphome/core esphome/components/delonghi/* @grob6000 +esphome/components/dew_point/* @CFlix esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter diff --git a/esphome/components/dew_point/__init__.py b/esphome/components/dew_point/__init__.py new file mode 100644 index 00000000000..3b852436c3c --- /dev/null +++ b/esphome/components/dew_point/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@CFlix"] diff --git a/esphome/components/dew_point/dew_point.cpp b/esphome/components/dew_point/dew_point.cpp new file mode 100644 index 00000000000..04ac305e3d2 --- /dev/null +++ b/esphome/components/dew_point/dew_point.cpp @@ -0,0 +1,82 @@ + +#include "dew_point.h" + +namespace esphome::dew_point { + +static const char *const TAG = "dew_point.sensor"; + +void DewPointComponent::setup() { + // Register callbacks for sensor updates + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->temperature_sensor_->has_state()) { + this->temperature_value_ = this->temperature_sensor_->get_state(); + } + } + + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->humidity_sensor_->has_state()) { + this->humidity_value_ = this->humidity_sensor_->get_state(); + } + } +} + +void DewPointComponent::dump_config() { + LOG_SENSOR("", "Dew Point", this); + ESP_LOGCONFIG(TAG, + "Sources\n" + " Temperature: '%s'\n" + " Humidity: '%s'", + this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); +} + +float DewPointComponent::get_setup_priority() const { return setup_priority::DATA; } + +void DewPointComponent::loop() { + // Only run once + this->disable_loop(); + + // Check if we have valid values for both sensors + if (std::isnan(this->temperature_value_) || std::isnan(this->humidity_value_)) { + ESP_LOGW(TAG, "Temperature or humidity value is NaN, skipping calculation"); + this->publish_state(NAN); + return; + } + + // Check for valid humidity range + if (this->humidity_value_ <= 0.0f || this->humidity_value_ > 100.0f) { + ESP_LOGW(TAG, "Humidity value out of range (0-100): %.2f", this->humidity_value_); + this->publish_state(NAN); + return; + } + + // Magnus formula constants + const float a{17.625f}; + const float b{243.04f}; + + // Calculate dew point using Magnus formula + // Td = (b * alpha) / (a - alpha) + // where alpha = ln(RH/100) + (a * T) / (b + T) + + const float alpha{std::log(this->humidity_value_ / 100.0f) + + (a * this->temperature_value_) / (b + this->temperature_value_)}; + + const float dew_point{(b * alpha) / (a - alpha)}; + + // Publish the calculated dew point + this->publish_state(dew_point); + + ESP_LOGD(TAG, "'%s' >> %.1f°C (T: %.1f°C, RH: %.1f%%)", this->get_name().c_str(), dew_point, this->temperature_value_, + this->humidity_value_); +} + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/dew_point.h b/esphome/components/dew_point/dew_point.h new file mode 100644 index 00000000000..833c50fba25 --- /dev/null +++ b/esphome/components/dew_point/dew_point.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::dew_point { + +class DewPointComponent : public Component, public sensor::Sensor { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void setup() override; + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + float temperature_value_{NAN}; + float humidity_value_{NAN}; +}; + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/sensor.py b/esphome/components/dew_point/sensor.py new file mode 100644 index 00000000000..4fee0956021 --- /dev/null +++ b/esphome/components/dew_point/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["sensor"] + +dew_point_ns = cg.esphome_ns.namespace("dew_point") +DewPointComponent = dew_point_ns.class_( + "DewPointComponent", cg.Component, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + DewPointComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:weather-rainy", + ) + .extend( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(temperature_sensor)) + + humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(humidity_sensor)) diff --git a/tests/components/dew_point/common.yaml b/tests/components/dew_point/common.yaml new file mode 100644 index 00000000000..527eeb2f84b --- /dev/null +++ b/tests/components/dew_point/common.yaml @@ -0,0 +1,19 @@ +sensor: + - platform: dew_point + name: Dew Point + temperature: template_temperature + humidity: template_humidity + - platform: template + id: template_humidity + lambda: |- + if (millis() > 10000) { + return 0.6; + } + return 0.0; + - platform: template + id: template_temperature + lambda: |- + if (millis() > 10000) { + return 42.0; + } + return 0.0; diff --git a/tests/components/dew_point/test.esp32-idf.yaml b/tests/components/dew_point/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/dew_point/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dew_point/test.esp8266-ard.yaml b/tests/components/dew_point/test.esp8266-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/dew_point/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/dew_point/test.rp2040-ard.yaml b/tests/components/dew_point/test.rp2040-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/dew_point/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 30c8c6870383f5c5e6d3f47328b3b685fc8a1518 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 11:22:23 -1000 Subject: [PATCH 0550/2056] [socket] Fix RP2040 TCP race condition between lwip callbacks and main loop (#14679) --- esphome/components/rp2040/helpers.cpp | 16 +++++- .../components/socket/lwip_raw_tcp_impl.cpp | 52 +++++++++++++++++++ esphome/components/socket/lwip_raw_tcp_impl.h | 6 +++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index 30b40a723a3..4191c2164ad 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -7,6 +7,7 @@ #if defined(USE_WIFI) #include <WiFi.h> +#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) #endif #include <hardware/structs/rosc.h> #include <hardware/sync.h> @@ -44,9 +45,22 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } -// RP2040 doesn't support lwIP core locking, so this is a no-op +// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. +// This means lwip callbacks run from a low-priority user IRQ context, not the +// main loop (see low_priority_irq_handler() in pico-sdk +// async_context_threadsafe_background.c). cyw43_arch_lwip_begin/end acquires the +// async_context recursive mutex to prevent IRQ callbacks from firing during +// critical sections. See esphome#10681. +// +// When CYW43 is not available (non-WiFi RP2040 boards), this is a no-op since +// there's no network stack and no lwip callbacks to race with. +#if defined(USE_WIFI) +LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); } +LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); } +#else LwIPLock::LwIPLock() {} LwIPLock::~LwIPLock() {} +#endif void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 445a57809d2..d7fa6a26945 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -111,6 +111,24 @@ void socket_wake() { } #endif +// ---- LWIP thread safety ---- +// +// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. +// This means lwip callbacks (recv_fn, accept_fn, err_fn) run from a low-priority +// user IRQ context, not the main loop (see low_priority_irq_handler() in pico-sdk +// async_context_threadsafe_background.c). They can preempt main-loop code at any point. +// +// Without locking, this causes race conditions between recv_fn and read() on the +// shared rx_buf_ pbuf chain — recv_fn calls pbuf_cat() while read() is freeing +// nodes, leading to use-after-free and infinite-loop crashes. See esphome#10681. +// +// On ESP8266, lwip callbacks run from the SYS context which cooperates with user +// code (CONT context) — they never preempt each other, so no locking is needed. +// +// esphome::LwIPLock is the platform-provided RAII guard (see helpers.h/helpers.cpp). +// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op. +#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT + static const char *const TAG = "socket.lwip"; // set to 1 to enable verbose lwip logging @@ -123,6 +141,7 @@ static const char *const TAG = "socket.lwip"; // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { + LWIP_LOCK(); if (this->pcb_ != nullptr) { LWIP_LOG("tcp_abort(%p)", this->pcb_); tcp_abort(this->pcb_); @@ -131,6 +150,7 @@ LWIPRawCommon::~LWIPRawCommon() { } int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return -1; @@ -196,6 +216,7 @@ int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { } int LWIPRawCommon::close() { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -214,6 +235,7 @@ int LWIPRawCommon::close() { } int LWIPRawCommon::shutdown(int how) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -240,6 +262,7 @@ int LWIPRawCommon::shutdown(int how) { } int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -252,6 +275,7 @@ int LWIPRawCommon::getpeername(struct sockaddr *name, socklen_t *addrlen) { } int LWIPRawCommon::getsockname(struct sockaddr *name, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -284,6 +308,7 @@ size_t LWIPRawCommon::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) { } int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -318,6 +343,7 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o } int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -388,6 +414,7 @@ int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *n // ---- LWIPRawImpl methods ---- LWIPRawImpl::~LWIPRawImpl() { + LWIP_LOCK(); // Free any received pbufs that LWIP transferred ownership of via recv_fn. // tcp_abort() in the base destructor won't free these since LWIP considers // ownership transferred once the recv callback accepts them. @@ -399,6 +426,7 @@ LWIPRawImpl::~LWIPRawImpl() { } void LWIPRawImpl::init() { + LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); @@ -406,6 +434,9 @@ void LWIPRawImpl::init() { } void LWIPRawImpl::s_err_fn(void *arg, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. + // No LWIP_LOCK() needed — acquiring it would be redundant (recursive mutex). + // // "If a connection is aborted because of an error, the application is alerted of this event by // the err callback." // pcb is already freed when this callback is called @@ -422,6 +453,7 @@ err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, er } err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. LWIP_LOG("recv(pb=%p err=%d)", pb, err); if (err != 0) { // "An error code if there has been an error receiving Only return ERR_ABRT if you have @@ -448,6 +480,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } ssize_t LWIPRawImpl::read(void *buf, size_t len) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -507,6 +540,7 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { } ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); @@ -525,6 +559,7 @@ ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { } ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -557,6 +592,11 @@ ssize_t LWIPRawImpl::internal_write_(const void *buf, size_t len) { } int LWIPRawImpl::internal_output_() { + LWIP_LOCK(); + if (this->pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } LWIP_LOG("tcp_output(%p)", this->pcb_); err_t err = tcp_output(this->pcb_); if (err == ERR_ABRT) { @@ -576,6 +616,7 @@ int LWIPRawImpl::internal_output_() { } ssize_t LWIPRawImpl::write(const void *buf, size_t len) { + LWIP_LOCK(); // Hold for write + optional output ssize_t written = this->internal_write_(buf, len); if (written == -1) return -1; @@ -592,6 +633,7 @@ ssize_t LWIPRawImpl::write(const void *buf, size_t len) { } ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t written = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = this->internal_write_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); @@ -621,6 +663,7 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { // ---- LWIPRawListenImpl methods ---- LWIPRawListenImpl::~LWIPRawListenImpl() { + LWIP_LOCK(); // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. @@ -632,6 +675,7 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { } void LWIPRawListenImpl::init() { + LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_accept(this->pcb_, LWIPRawListenImpl::s_accept_fn); @@ -639,6 +683,7 @@ void LWIPRawListenImpl::init() { } void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); arg_this->pcb_ = nullptr; @@ -650,6 +695,7 @@ err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t er } std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, socklen_t *addrlen) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return nullptr; @@ -674,6 +720,7 @@ std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, so } int LWIPRawListenImpl::listen(int backlog) { + LWIP_LOCK(); if (this->pcb_ == nullptr) { errno = EBADF; return -1; @@ -699,6 +746,7 @@ int LWIPRawListenImpl::listen(int backlog) { } err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { + // Called by lwip core which already holds the async_context lock on RP2040. LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -734,6 +782,7 @@ std::unique_ptr<Socket> socket(int domain, int type, int protocol) { errno = EPROTOTYPE; return nullptr; } + LWIP_LOCK(); auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; @@ -753,6 +802,7 @@ std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) errno = EPROTOTYPE; return nullptr; } + LWIP_LOCK(); auto *pcb = tcp_new(); if (pcb == nullptr) return nullptr; @@ -766,6 +816,8 @@ std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, return socket_listen(domain, type, protocol); } +#undef LWIP_LOCK + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index c171e0537f3..5b2c11cfe2c 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -95,8 +95,13 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ENOSYS; return -1; } + // Intentionally unlocked — this is a polling check called every loop iteration. + // A stale read at worst delays processing by one loop tick; the actual I/O in + // read() holds the lwip lock and re-checks properly. See esphome#10681. bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; } + // No lock needed — only called during setup before callbacks are registered. + // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles). int setblocking(bool blocking) { if (this->pcb_ == nullptr) { errno = ECONNRESET; @@ -134,6 +139,7 @@ class LWIPRawListenImpl : public LWIPRawCommon { void init(); + // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment. bool ready() const { return this->accepted_socket_count_ > 0; } std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen); From dcbf3c8728a1a0040e0f2899a6bfc899888f0e9e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Tue, 10 Mar 2026 23:18:35 +0100 Subject: [PATCH 0551/2056] [esp32] gpio type improvements (#14517) --- esphome/components/esp32/gpio.py | 4 ++-- esphome/components/esp32/gpio_esp32.py | 5 +++-- esphome/components/esp32/gpio_esp32_c2.py | 5 +++-- esphome/components/esp32/gpio_esp32_c3.py | 5 +++-- esphome/components/esp32/gpio_esp32_c5.py | 5 +++-- esphome/components/esp32/gpio_esp32_c6.py | 5 +++-- esphome/components/esp32/gpio_esp32_c61.py | 5 +++-- esphome/components/esp32/gpio_esp32_h2.py | 5 +++-- esphome/components/esp32/gpio_esp32_p4.py | 5 +++-- esphome/components/esp32/gpio_esp32_s2.py | 5 +++-- esphome/components/esp32/gpio_esp32_s3.py | 5 +++-- 11 files changed, 32 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index c0803f40a83..a7180cbcd7e 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -88,8 +88,8 @@ def _translate_pin(value): @dataclass class ESP32ValidationFunctions: - pin_validation: Callable[[Any], Any] - usage_validation: Callable[[Any], Any] + pin_validation: Callable[[int], int] + usage_validation: Callable[[dict[str, Any]], dict[str, Any]] _esp32_validations = { diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 973d2dc0ef3..b3166cf822e 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -22,7 +23,7 @@ _ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15} _LOGGER = logging.getLogger(__name__) -def esp32_validate_gpio_pin(value): +def esp32_validate_gpio_pin(value: int) -> int: if value < 0 or value > 39: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: @@ -41,7 +42,7 @@ def esp32_validate_gpio_pin(value): return value -def esp32_validate_supports(value): +def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index 32a24050ca7..2d6e3a4a4ed 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -9,14 +10,14 @@ _ESP32C2_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c2_validate_gpio_pin(value): +def esp32_c2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 20: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") return value -def esp32_c2_validate_supports(value): +def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index c1427cc02aa..93e0b970934 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -18,7 +19,7 @@ _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c3_validate_gpio_pin(value): +def esp32_c3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 21: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") if value in _ESP32C3_SPI_PSRAM_PINS: @@ -29,7 +30,7 @@ def esp32_c3_validate_gpio_pin(value): return value -def esp32_c3_validate_supports(value): +def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index fa2ce1a689c..639ed64c9ea 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -22,7 +23,7 @@ _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28} _LOGGER = logging.getLogger(__name__) -def esp32_c5_validate_gpio_pin(value): +def esp32_c5_validate_gpio_pin(value: int) -> int: if value < 0 or value > 28: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)") if value in _ESP32C5_SPI_PSRAM_PINS: @@ -33,7 +34,7 @@ def esp32_c5_validate_gpio_pin(value): return value -def esp32_c5_validate_supports(value): +def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index 5d679dede25..cfd3bca8334 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -22,7 +23,7 @@ _ESP32C6_STRAPPING_PINS = {8, 9, 15} _LOGGER = logging.getLogger(__name__) -def esp32_c6_validate_gpio_pin(value): +def esp32_c6_validate_gpio_pin(value: int) -> int: if value < 0 or value > 23: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") if value in _ESP32C6_SPI_PSRAM_PINS: @@ -33,7 +34,7 @@ def esp32_c6_validate_gpio_pin(value): return value -def esp32_c6_validate_supports(value): +def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py index 77be42db3e7..2f3abe6a0f9 100644 --- a/esphome/components/esp32/gpio_esp32_c61.py +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -20,7 +21,7 @@ _ESP32C61_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c61_validate_gpio_pin(value): +def esp32_c61_validate_gpio_pin(value: int) -> int: if value < 0 or value > 29: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)") if value in _ESP32C61_SPI_PSRAM_PINS: @@ -31,7 +32,7 @@ def esp32_c61_validate_gpio_pin(value): return value -def esp32_c61_validate_supports(value): +def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index f37297764b8..5e7a6158f9b 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -13,7 +14,7 @@ _ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} _LOGGER = logging.getLogger(__name__) -def esp32_h2_validate_gpio_pin(value): +def esp32_h2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 27: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") if value in _ESP32H2_SPI_FLASH_PINS: @@ -33,7 +34,7 @@ def esp32_h2_validate_gpio_pin(value): return value -def esp32_h2_validate_supports(value): +def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 2726c5932fa..865db926524 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -14,7 +15,7 @@ _ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38} _LOGGER = logging.getLogger(__name__) -def esp32_p4_validate_gpio_pin(value): +def esp32_p4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: @@ -27,7 +28,7 @@ def esp32_p4_validate_gpio_pin(value): return value -def esp32_p4_validate_supports(value): +def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 331aeb9d94b..4978f48a1c2 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -26,7 +27,7 @@ _ESP32S2_STRAPPING_PINS = {0, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s2_validate_gpio_pin(value): +def esp32_s2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 46: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") @@ -43,7 +44,7 @@ def esp32_s2_validate_gpio_pin(value): return value -def esp32_s2_validate_supports(value): +def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index aea378f499d..cb0eb8178c3 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -27,7 +28,7 @@ _ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s3_validate_gpio_pin(value): +def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") @@ -49,7 +50,7 @@ def esp32_s3_validate_gpio_pin(value): return value -def esp32_s3_validate_supports(value): +def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] From b84d773becad306de294beadb0a3efa73c9a854d Mon Sep 17 00:00:00 2001 From: CFlix <38142312+CFlix@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:24:46 +0100 Subject: [PATCH 0552/2056] [bme280] Change communication error message to include "no response" hint. (#14686) --- esphome/components/bme280_base/bme280_base.cpp | 2 +- esphome/components/bmp280_base/bmp280_base.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index addbfe618d8..f31940df106 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -7,7 +7,7 @@ #include <esphome/components/sensor/sensor.h> #include <esphome/core/component.h> -#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bme280_base { diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index de685e7c278..603966a2b52 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -2,7 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bmp280_base { From 794098de99f083105440f8a36f2f070d1e6f9a04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 16:40:45 -1000 Subject: [PATCH 0553/2056] [rp2040] Add HardFault crash handler with backtrace (#14685) --- esphome/components/logger/logger_rp2040.cpp | 2 + esphome/components/rp2040/__init__.py | 47 ++++ esphome/components/rp2040/core.cpp | 2 + esphome/components/rp2040/crash_handler.cpp | 227 ++++++++++++++++++++ esphome/components/rp2040/crash_handler.h | 17 ++ 5 files changed, 295 insertions(+) create mode 100644 esphome/components/rp2040/crash_handler.cpp create mode 100644 esphome/components/rp2040/crash_handler.h diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 1f435031f61..f76b823a8f7 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -1,5 +1,6 @@ #ifdef USE_RP2040 #include "logger.h" +#include "esphome/components/rp2040/crash_handler.h" #include "esphome/core/log.h" namespace esphome::logger { @@ -25,6 +26,7 @@ void Logger::pre_setup() { } global_logger = this; ESP_LOGI(TAG, "Log initialized"); + rp2040::crash_handler_log(); } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 359337adfb9..b15811241ca 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,6 +1,8 @@ import logging from pathlib import Path +import re from string import ascii_letters, digits +import subprocess import esphome.codegen as cg import esphome.config_validation as cv @@ -264,3 +266,48 @@ def copy_files(): path = CORE.relative_src_path("esphome.h") content = read_file(path).rstrip("\n") write_file_if_changed(path, content + '\n#include "pio_includes.h"\n') + + +# RP2040 crash handler stacktrace decoding +# Matches output from esphome/components/rp2040/crash_handler.cpp +_CRASH_RE = re.compile(r"CRASH DETECTED ON PREVIOUS BOOT") +_CRASH_ADDR_RE = re.compile( + r"(?:PC|LR|BT\d):\s+(0x[0-9a-fA-F]{8})\s+\((?:fault location|return address|stack backtrace)\)" +) + + +def _addr2line(tool: str, elf: Path, addr: str) -> str: + try: + result = subprocess.run( + [tool, "-pfiaC", "-e", str(elf), addr], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except (OSError, subprocess.CalledProcessError): + return f"{addr} (decode failed)" + + +def process_stacktrace(config, line: str, backtrace_state: bool) -> bool: + """Decode RP2040 crash handler output using addr2line.""" + if _CRASH_RE.search(line): + _LOGGER.error("RP2040 crash detected - decoding addresses") + return True + + if backtrace_state: + if match := _CRASH_ADDR_RE.search(line): + from esphome.platformio_api import get_idedata + + idedata = get_idedata(config) + if idedata.addr2line_path: + elf = idedata.firmware_elf_path + if elf.exists(): + decoded = _addr2line(idedata.addr2line_path, elf, match.group(1)) + _LOGGER.error(" %s => %s", match.group(1), decoded) + + # Stop backtrace state after addr2line hint (last line of crash dump) + if "addr2line" in line: + return False + + return backtrace_state diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 63b154d80de..5e5a96c78b1 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_RP2040 #include "core.h" +#include "crash_handler.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -24,6 +25,7 @@ void arch_restart() { } void arch_init() { + rp2040::crash_handler_read_and_clear(); #if USE_RP2040_WATCHDOG_TIMEOUT > 0 watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); #endif diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp new file mode 100644 index 00000000000..6ab46da4449 --- /dev/null +++ b/esphome/components/rp2040/crash_handler.cpp @@ -0,0 +1,227 @@ +#ifdef USE_RP2040 + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> +#include <hardware/regs/addressmap.h> +#include <hardware/structs/watchdog.h> +#include <hardware/watchdog.h> + +// Cortex-M0+ exception frame offsets (words) +// When a fault occurs, the CPU pushes: R0, R1, R2, R3, R12, LR, PC, xPSR +static constexpr uint32_t EF_LR = 5; +static constexpr uint32_t EF_PC = 6; + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; + +// We only have 8 scratch registers (32 bytes) that survive watchdog reboot. +// Use them for the most important data, then scan the stack for code addresses. +// +// Scratch register layout: +// [0] = magic (CRASH_MAGIC) +// [1] = PC (program counter at fault) +// [2] = LR (link register from exception frame) +// [3] = SP (stack pointer at fault) +// [4..7] = up to 4 additional code addresses found by scanning the stack +// (return addresses from callers, giving a deeper backtrace) + +// Flash is mapped at XIP_BASE (0x10000000). We use a conservative upper bound +// to keep false positives low during stack scanning. Wider ranges would match +// more stale data on the stack that happens to look like code addresses. +#if defined(PICO_RP2350) +static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x400000; // 4MB — RP2350 typical max +#else +static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x200000; // 2MB — RP2040 typical max +#endif + +static inline bool is_code_addr(uint32_t val) { + uint32_t cleared = val & ~1u; // Clear Thumb bit + return cleared >= XIP_BASE && cleared < FLASH_SCAN_END; +} + +static constexpr size_t MAX_BACKTRACE = 4; + +namespace esphome::rp2040 { + +static const char *const TAG = "rp2040.crash"; + +// Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). +// The valid field is explicitly cleared in crash_handler_read_and_clear() instead. +static struct { + bool valid; + uint32_t pc; + uint32_t lr; + uint32_t sp; + uint32_t backtrace[MAX_BACKTRACE]; + uint8_t backtrace_count; +} __attribute__((section(".noinit"))) s_crash_data; + +void crash_handler_read_and_clear() { + s_crash_data.valid = false; + if (watchdog_hw->scratch[0] == CRASH_MAGIC) { + s_crash_data.valid = true; + s_crash_data.pc = watchdog_hw->scratch[1]; + s_crash_data.lr = watchdog_hw->scratch[2]; + s_crash_data.sp = watchdog_hw->scratch[3]; + s_crash_data.backtrace_count = 0; + for (size_t i = 0; i < MAX_BACKTRACE; i++) { + uint32_t addr = watchdog_hw->scratch[4 + i]; + if (addr == 0) + break; + s_crash_data.backtrace[i] = addr; + s_crash_data.backtrace_count++; + } + } + // Clear scratch registers regardless + for (int i = 0; i < 8; i++) { + watchdog_hw->scratch[i] = 0; + } +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console (miniterm), making it possible to see partial output if +// the device crashes again during boot, and allowing the CLI's process_stacktrace +// to match and decode each address individually. +void crash_handler_log() { + if (!s_crash_data.valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_crash_data.pc); + ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_crash_data.lr); + ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_crash_data.sp); + for (uint8_t i = 0; i < s_crash_data.backtrace_count; i++) { + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", i, s_crash_data.backtrace[i]); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[160]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32, + s_crash_data.pc, s_crash_data.lr); + for (uint8_t i = 0; i < s_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_crash_data.backtrace[i]); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::rp2040 + +// --- HardFault handler --- +// Overrides the weak isr_hardfault from arduino-pico's crt0.S. +// On Cortex-M0+, the CPU pushes {R0,R1,R2,R3,R12,LR,PC,xPSR} onto the +// active stack (MSP or PSP). We determine which stack was active, +// extract key registers, store them in watchdog scratch registers +// (which survive watchdog reboot), then trigger a reboot. + +// Check if a pointer falls within SRAM (valid for stack access). +// SRAM_BASE and SRAM_END are chip-specific SDK defines: +// RP2040: 0x20000000 - 0x20042000 (264KB) +// RP2350: 0x20000000 - 0x20082000 (520KB) +static inline bool is_valid_sram_ptr(const uint32_t *ptr) { + auto addr = reinterpret_cast<uintptr_t>(ptr); + // Exception frame is 8 words (32 bytes), so frame+7 must also be in SRAM. + // Check alignment (must be word-aligned) and that the full frame fits. + return (addr % 4 == 0) && addr >= SRAM_BASE && (addr + 32) <= SRAM_END; +} + +// C handler called from the asm wrapper with the exception frame pointer. +static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame, uint32_t /*exc_return*/) { + // watchdog_reboot() overwrites scratch[4]-[7], so we must call it first + // then write ALL our data after. The 10ms timeout gives us plenty of time. + watchdog_reboot(0, 0, 10); + + // Validate frame pointer before dereferencing. If the HardFault was caused + // by a stacking error or corrupted SP, frame may be invalid. Write a minimal + // crash marker so we at least know a crash occurred. + if (!is_valid_sram_ptr(frame)) { + watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[1] = 0; // PC unknown + watchdog_hw->scratch[2] = 0; // LR unknown + watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis + for (uint32_t i = 0; i < MAX_BACKTRACE; i++) { + watchdog_hw->scratch[4 + i] = 0; + } + while (true) { + __asm volatile("nop"); + } + } + + // Pre-fault SP: the exception frame is 8 words pushed onto the stack, + // so the SP before the fault was frame + 8 words. If xPSR bit 9 is set, + // the hardware pushed an extra alignment word to maintain 8-byte stack + // alignment (ARMv6-M/ARMv7-M spec), so add 1 more word. + static constexpr uint32_t EF_XPSR = 7; + uint32_t extra_align = (frame[EF_XPSR] & (1u << 9)) ? 1 : 0; + uint32_t *post_frame = frame + 8 + extra_align; + uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame); + + // Write key registers + watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[1] = frame[EF_PC]; + watchdog_hw->scratch[2] = frame[EF_LR]; + watchdog_hw->scratch[3] = pre_fault_sp; + + // Scan stack for code addresses to build a deeper backtrace. + // The exception frame is 8 words (32 bytes) at 'frame', plus an optional + // alignment word. Walk up to 64 words looking for return addresses. + uint32_t *scan_start = post_frame; + // SRAM_END is chip-specific: 0x20042000 (RP2040) or 0x20082000 (RP2350) + uint32_t *stack_top = reinterpret_cast<uint32_t *>(SRAM_END); + // Scan up to 64 words (256 bytes) — covers typical nested call frames + // without scanning too much stale stack data that could produce false positives. + uint32_t bt_count = 0; + + for (uint32_t *p = scan_start; p < stack_top && p < scan_start + 64 && bt_count < MAX_BACKTRACE; p++) { + uint32_t val = *p; + // Check if this looks like a code address in flash + // Skip if it's the same as PC or LR we already saved + if (is_code_addr(val) && val != frame[EF_PC] && val != frame[EF_LR]) { + watchdog_hw->scratch[4 + bt_count] = val; + bt_count++; + } + } + // Zero remaining slots + for (uint32_t i = bt_count; i < MAX_BACKTRACE; i++) { + watchdog_hw->scratch[4 + i] = 0; + } + + while (true) { + __asm volatile("nop"); + } +} + +// Naked asm wrapper - Cortex-M0+ compatible (no ITE/conditional execution). +// Determines active stack pointer and branches to C handler. +// Uses literal pool (.word) for addresses since M0+ has limited immediate encoding. +// +// Based on the standard Cortex-M0+ HardFault handler pattern described in: +// - ARM Application Note AN209: "Using Cortex-M3/M4/M7 Fault Exceptions" +// (adapted for M0+ which lacks conditional execution instructions) +// - Memfault: "How to debug a HardFault on an ARM Cortex-M MCU" +// https://interrupt.memfault.com/blog/cortex-m-hardfault-debug +// - Raspberry Pi Forums: "Cortex-M0+ Hard Fault handler porting" +// https://www.eevblog.com/forum/microcontrollers/cortex-m0-hard-fault-handler-porting/ +// +// The key M0+ adaptation: replaces ITE/MRSEQ/MRSNE (Cortex-M3+) with +// MOVS+TST+BEQ branch sequence, and uses a literal pool for the C handler address. +extern "C" void __attribute__((naked, used)) isr_hardfault() { + __asm volatile("movs r0, #4 \n" // Prepare bit 2 mask + "mov r1, lr \n" // r1 = EXC_RETURN + "tst r1, r0 \n" // Test bit 2 + "beq 1f \n" // If 0, was using MSP + "mrs r0, psp \n" // Bit 2 set = PSP was active + "b 2f \n" + "1: \n" + "mrs r0, msp \n" // Bit 2 clear = MSP was active + "2: \n" + // r0 = exception frame pointer, r1 = EXC_RETURN (still in r1) + "ldr r2, 3f \n" // Load C handler address from literal pool + "bx r2 \n" // Branch to handler (r0=frame, r1=exc_return) + ".align 2 \n" + "3: .word %c0 \n" // Literal pool: address of C handler + : + : "i"(hard_fault_handler_c)); +} + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2040/crash_handler.h new file mode 100644 index 00000000000..f10db47c234 --- /dev/null +++ b/esphome/components/rp2040/crash_handler.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef USE_RP2040 + +#include <cstdint> + +namespace esphome::rp2040 { + +/// Read crash data from watchdog scratch registers and clear them. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +} // namespace esphome::rp2040 + +#endif // USE_RP2040 From 6561c9bc95500915584afbe13a8cf3da6f3da051 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 10 Mar 2026 22:32:29 -0500 Subject: [PATCH 0554/2056] [core] Fix waiting for port indefinitely (#14688) --- esphome/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 3f0da85a694..58b995e8df4 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -919,9 +919,11 @@ def _wait_for_serial_port( """ def _port_found() -> bool: - ports = get_serial_ports() if port is not None: - return any(p.path == port for p in ports) + if os.name == "posix": + return os.path.exists(port) + return any(p.path == port for p in get_serial_ports()) + ports = get_serial_ports() if known_ports is not None: return any(p.path not in known_ports for p in ports) return bool(ports) From d0f37ae69460ac90e2f95a39216ec09090373df6 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 10 Mar 2026 23:31:27 -0500 Subject: [PATCH 0555/2056] [logger] Fix UART selection not applied before `pre_setup()` (#14690) --- esphome/components/logger/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e370f4215d4..675f9a2ca4b 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -337,6 +337,16 @@ async def to_code(config): ) if CORE.is_esp32: cg.add(log.create_pthread_key()) + # set_uart_selection() must be called before pre_setup() because + # pre_setup() switches on uart_ to decide which hardware to initialize + # (e.g. UART0 vs USB_SERIAL_JTAG). Without this, uart_ is still the + # default UART_SELECTION_UART0 and the wrong hardware gets initialized. + if CONF_HARDWARE_UART in config: + cg.add( + log.set_uart_selection( + HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] + ) + ) # pre_setup() must be called before init_log_buffer() because # init_log_buffer() calls disable_loop() which may log at VV level, # and global_logger must be set before any logging occurs. @@ -354,12 +364,6 @@ async def to_code(config): cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host cg.add(log.set_log_level(initial_level)) - if CONF_HARDWARE_UART in config: - cg.add( - log.set_uart_selection( - HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] - ) - ) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] From 4df3d3554e5767edf6469977972409d3b882cd2e Mon Sep 17 00:00:00 2001 From: Adam DeMuri <adam.demuri@gmail.com> Date: Tue, 10 Mar 2026 23:44:05 -0600 Subject: [PATCH 0556/2056] Enable the address and behavior sanitizers for C++ component unit tests (#13490) --- script/cpp_unit_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index c9174584722..c6cfd8270fa 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -79,6 +79,10 @@ def create_test_config(config_name: str, includes: list[str]) -> dict: "-Og", # optimize for debug "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing "-DESPHOME_DEBUG", # enable debug assertions + # Enable the address and undefined behavior sanitizers + "-fsanitize=address", + "-fsanitize=undefined", + "-fno-omit-frame-pointer", ], "debug_build_flags": [ # only for debug builds "-g3", # max debug info From e8e700a683f2ad210892393e6f67260855e493d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 10 Mar 2026 20:51:54 -1000 Subject: [PATCH 0557/2056] [socket] Fix RP2040 heap corruption from malloc in lwip accept callback (#14687) --- .../components/socket/lwip_raw_tcp_impl.cpp | 142 ++++++++++++++---- esphome/components/socket/lwip_raw_tcp_impl.h | 41 ++--- 2 files changed, 138 insertions(+), 45 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d7fa6a26945..fd1b8a95542 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -425,20 +425,24 @@ LWIPRawImpl::~LWIPRawImpl() { // Base class destructor handles pcb_ cleanup via tcp_abort } -void LWIPRawImpl::init() { +void LWIPRawImpl::init(struct pbuf *initial_rx, bool initial_rx_closed) { LWIP_LOCK(); LWIP_LOG("init(%p)", this->pcb_); tcp_arg(this->pcb_, this); tcp_recv(this->pcb_, LWIPRawImpl::s_recv_fn); tcp_err(this->pcb_, LWIPRawImpl::s_err_fn); + if (initial_rx != nullptr) { + this->rx_buf_ = initial_rx; + this->rx_buf_offset_ = 0; + } + this->rx_closed_ = initial_rx_closed; } void LWIPRawImpl::s_err_fn(void *arg, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. - // No LWIP_LOCK() needed — acquiring it would be redundant (recursive mutex). + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // No LWIP_LOCK() needed — lwip core already holds the async_context lock. // - // "If a connection is aborted because of an error, the application is alerted of this event by - // the err callback." // pcb is already freed when this callback is called // ERR_RST: connection was reset by remote host // ERR_ABRT: aborted through tcp_abort or TCP timer @@ -453,11 +457,15 @@ err_t LWIPRawImpl::s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, er } err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). LWIP_LOG("recv(pb=%p err=%d)", pb, err); if (err != 0) { // "An error code if there has been an error receiving Only return ERR_ABRT if you have // called tcp_abort from within the callback function!" + if (pb != nullptr) { + pbuf_free(pb); + } this->rx_closed_ = true; return ERR_OK; } @@ -664,6 +672,22 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { LWIPRawListenImpl::~LWIPRawListenImpl() { LWIP_LOCK(); + // Abort any queued PCBs that were never accepted by the main loop. + // Clear the error callback first — tcp_abort triggers it, and we don't + // want s_queued_err_fn writing to slots during destruction. + for (uint8_t i = 0; i < this->accepted_socket_count_; i++) { + auto &entry = this->accepted_pcbs_[i]; + if (entry.pcb != nullptr) { + tcp_err(entry.pcb, nullptr); + tcp_abort(entry.pcb); + entry.pcb = nullptr; + } + if (entry.rx_buf != nullptr) { + pbuf_free(entry.rx_buf); + entry.rx_buf = nullptr; + } + } + this->accepted_socket_count_ = 0; // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. @@ -683,12 +707,49 @@ void LWIPRawListenImpl::init() { } void LWIPRawListenImpl::s_err_fn(void *arg, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); ESP_LOGVV(TAG, "socket %p: err(err=%d)", arg_this, err); arg_this->pcb_ = nullptr; } +void LWIPRawListenImpl::s_queued_err_fn(void *arg, err_t err) { + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // Called when a queued (not yet accepted) PCB errors — e.g., remote sent RST. + // The PCB is already freed by lwip. Null our pointer so accept() skips it. + (void) err; + auto *entry = reinterpret_cast<QueuedPcb *>(arg); + entry->pcb = nullptr; + // Don't free rx_buf here — accept() will clean it up when it sees pcb==nullptr +} + +err_t LWIPRawListenImpl::s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) { + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). + // Temporary recv callback for PCBs queued between accept_fn_ and accept(). + // Without this, lwip's default tcp_recv_null handler would ACK and drop the data, + // causing the API handshake to silently fail (client sends Hello, server never sees it). + (void) pcb; + auto *entry = reinterpret_cast<QueuedPcb *>(arg); + if (pb == nullptr || err != ERR_OK) { + // Remote closed or error + if (pb != nullptr) { + pbuf_free(pb); + } + entry->rx_closed = true; + return ERR_OK; + } + // Buffer the data — tcp_recved() is deferred to read() after accept() creates the socket. + if (entry->rx_buf == nullptr) { + entry->rx_buf = pb; + } else { + pbuf_cat(entry->rx_buf, pb); + } + return ERR_OK; +} + err_t LWIPRawListenImpl::s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { auto *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg); return arg_this->accept_fn_(newpcb, err); @@ -700,23 +761,40 @@ std::unique_ptr<LWIPRawImpl> LWIPRawListenImpl::accept(struct sockaddr *addr, so errno = EBADF; return nullptr; } - if (this->accepted_socket_count_ == 0) { - errno = EWOULDBLOCK; - return nullptr; + // Dequeue front entry, skipping any null entries (PCBs freed by lwip while queued). + // The error callback nulled their pcb pointers; clean up buffered data and discard. + while (this->accepted_socket_count_ > 0) { + QueuedPcb entry = this->accepted_pcbs_[0]; + // Shift remaining entries forward, updating tcp_arg pointers as we go. + // Safe because we hold LWIP_LOCK, so err/recv callbacks can't fire during the update. + for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { + this->accepted_pcbs_[i - 1] = this->accepted_pcbs_[i]; + if (this->accepted_pcbs_[i - 1].pcb != nullptr) { + tcp_arg(this->accepted_pcbs_[i - 1].pcb, &this->accepted_pcbs_[i - 1]); + } + } + this->accepted_pcbs_[this->accepted_socket_count_ - 1] = {}; + this->accepted_socket_count_--; + if (entry.pcb == nullptr) { + // PCB was freed by lwip (RST/timeout) while queued — discard and try next + if (entry.rx_buf != nullptr) { + pbuf_free(entry.rx_buf); + } + continue; + } + LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); + // Create socket wrapper on the main loop (not in accept callback) to avoid + // heap allocation in IRQ context on RP2040. Transfer any data received while queued. + auto sock = make_unique<LWIPRawImpl>(this->family_, entry.pcb); + sock->init(entry.rx_buf, entry.rx_closed); + if (addr != nullptr) { + sock->getpeername(addr, addrlen); + } + LWIP_LOG("accept(%p)", sock.get()); + return sock; } - // Take from front for FIFO ordering - std::unique_ptr<LWIPRawImpl> sock = std::move(this->accepted_sockets_[0]); - // Shift remaining sockets forward - for (uint8_t i = 1; i < this->accepted_socket_count_; i++) { - this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]); - } - this->accepted_socket_count_--; - LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_); - if (addr != nullptr) { - sock->getpeername(addr, addrlen); - } - LWIP_LOG("accept(%p)", sock.get()); - return sock; + errno = EWOULDBLOCK; + return nullptr; } int LWIPRawListenImpl::listen(int backlog) { @@ -746,7 +824,8 @@ int LWIPRawListenImpl::listen(int backlog) { } err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { - // Called by lwip core which already holds the async_context lock on RP2040. + // LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). + // No heap allocation allowed — malloc is not IRQ-safe (see #14687). LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -763,9 +842,18 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { // Must return ERR_ABRT since we called tcp_abort() return ERR_ABRT; } - auto sock = make_unique<LWIPRawImpl>(this->family_, newpcb); - sock->init(); - this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); + // Store the raw PCB — LWIPRawImpl creation is deferred to the main-loop accept(). + // This avoids heap allocation in this callback, which is unsafe from IRQ context on RP2040. + uint8_t idx = this->accepted_socket_count_++; + this->accepted_pcbs_[idx] = {newpcb, nullptr, false}; + // Register temporary callbacks so that while the PCB is queued: + // - err: nulls our pointer if the connection errors (RST, timeout) + // - recv: buffers any data that arrives before accept() creates the LWIPRawImpl + // (without this, lwip's default tcp_recv_null would ACK and drop the data) + // tcp_arg points to our queue entry; accept() updates these pointers after shifting. + tcp_arg(newpcb, &this->accepted_pcbs_[idx]); + tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn); + tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); #if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 5b2c11cfe2c..95931afcf3f 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -66,7 +66,7 @@ class LWIPRawImpl : public LWIPRawCommon { using LWIPRawCommon::LWIPRawCommon; ~LWIPRawImpl(); - void init(); + void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false); // Non-listening sockets return error std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) { @@ -182,23 +182,28 @@ class LWIPRawListenImpl : public LWIPRawCommon { err_t accept_fn_(struct tcp_pcb *newpcb, err_t err); static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err); - // Accept queue - holds incoming connections briefly until the event loop calls accept() - // This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop - // 3 slots is plenty since connections are pulled out quickly by the event loop - // - // Memory analysis: std::array<3> vs original std::queue implementation: - // - std::queue uses std::deque internally which on 32-bit systems needs: - // 24 bytes (deque object) + 32+ bytes (map array) + heap allocations - // Total: ~56+ bytes minimum, plus heap fragmentation - // - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes) - // Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations - // Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation) - // - // By using a separate listening socket class, regular connected sockets save - // 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems - static constexpr size_t MAX_ACCEPTED_SOCKETS = 3; - std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_; - uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue + // Temporary callbacks for queued PCBs (between accept_fn_ and accept()) + static void s_queued_err_fn(void *arg, err_t err); + static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); + + // Accept queue entry — stores a raw tcp_pcb and any data received while queued. + // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register + // a temporary recv callback to buffer any data that arrives between accept_fn_ + // (which stores the PCB) and accept() (which creates the LWIPRawImpl). + struct QueuedPcb { + struct tcp_pcb *pcb{nullptr}; + struct pbuf *rx_buf{nullptr}; // Data received while queued (before accept() picks it up) + bool rx_closed{false}; // Remote sent FIN while queued + }; + + // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects. + // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids: + // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040) + // - Dangling LWIPRawImpl if the connection errors before accept() picks it up + // 2 slots is plenty since the main loop drains the queue every iteration. + static constexpr size_t MAX_ACCEPTED_SOCKETS = 2; + std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{}; + uint8_t accepted_socket_count_ = 0; // Number of entries currently in queue }; } // namespace esphome::socket From 236f6b1935aec81d6350213e3049c6b0f4fb0794 Mon Sep 17 00:00:00 2001 From: Robert Resch <robert@resch.dev> Date: Wed, 11 Mar 2026 07:52:43 +0100 Subject: [PATCH 0558/2056] [micronova] Add command queue (#12268) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: edenhaus <edenhaus@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/micronova/__init__.py | 54 +++- .../components/micronova/button/__init__.py | 2 + .../micronova/button/micronova_button.cpp | 10 +- .../micronova/button/micronova_button.h | 11 +- esphome/components/micronova/micronova.cpp | 256 +++++++++++------- esphome/components/micronova/micronova.h | 93 ++++--- .../components/micronova/number/__init__.py | 7 +- .../micronova/number/micronova_number.cpp | 10 +- .../micronova/number/micronova_number.h | 9 +- .../micronova/sensor/micronova_sensor.cpp | 7 + .../micronova/sensor/micronova_sensor.h | 8 +- .../components/micronova/switch/__init__.py | 2 + .../micronova/switch/micronova_switch.cpp | 21 +- .../micronova/switch/micronova_switch.h | 8 +- .../text_sensor/micronova_text_sensor.cpp | 7 + .../text_sensor/micronova_text_sensor.h | 8 +- esphome/core/defines.h | 2 + esphome/core/helpers.h | 75 +++++ 18 files changed, 399 insertions(+), 191 deletions(-) diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py index d6ef93cf301..b4623522292 100644 --- a/esphome/components/micronova/__init__.py +++ b/esphome/components/micronova/__init__.py @@ -1,14 +1,34 @@ +from dataclasses import dataclass + from esphome import pins import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority +from esphome.coroutine import CoroPriority CODEOWNERS = ["@jorre05", "@edenhaus"] DEPENDENCIES = ["uart"] DOMAIN = "micronova" + + +@dataclass +class MicronovaData: + """Track micronova component state during code generation.""" + + listener_count: int = 0 + has_writer: bool = False + + +def _get_data() -> MicronovaData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = MicronovaData() + return CORE.data[DOMAIN] + + CONF_MICRONOVA_ID = f"{DOMAIN}_id" CONF_ENABLE_RX_PIN = "enable_rx_pin" CONF_MEMORY_LOCATION = "memory_location" @@ -66,16 +86,42 @@ def MICRONOVA_ADDRESS_SCHEMA( return schema +def register_micronova_writer() -> None: + """Register a component that can write to the stove (button, switch, number).""" + _get_data().has_writer = True + + async def to_code_micronova_listener(mv, var, config): + _get_data().listener_count += 1 await cg.register_component(var, config) - cg.add(mv.register_micronova_listener(var)) cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION])) cg.add(var.set_memory_address(config[CONF_MEMORY_ADDRESS])) + # Register listener as last step as we need all properties set before registering + cg.add(mv.register_micronova_listener(var)) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) + var = cg.new_Pvariable(config[CONF_ID], enable_rx_pin) await cg.register_component(var, config) await uart.register_uart_device(var, config) - enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN]) - cg.add(var.set_enable_rx_pin(enable_rx_pin)) + CORE.add_job(_final_step) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _final_step() -> None: + """Add defines for listener and writer counts after all are registered.""" + data = _get_data() + if data.listener_count == 0 and not data.has_writer: + raise cv.Invalid( + "No micronova entities configured. Add at least one micronova entity." + ) + if data.listener_count > 255: + raise cv.Invalid( + f"Too many micronova reading entities ({data.listener_count}). Maximum is 255." + ) + if data.listener_count > 0: + cg.add_define("MICRONOVA_LISTENER_COUNT", data.listener_count) + + if data.has_writer: + cg.add_define("USE_MICRONOVA_WRITER") diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 6adf8d96fec..1ef359ea6cf 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -9,6 +9,7 @@ from .. import ( MICRONOVA_ADDRESS_SCHEMA, MicroNova, micronova_ns, + register_micronova_writer, ) MicroNovaButton = micronova_ns.class_("MicroNovaButton", button.Button, cg.Component) @@ -36,6 +37,7 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if custom_button_config := config.get(CONF_CUSTOM_BUTTON): + register_micronova_writer() bt = await button.new_button(custom_button_config, mv) cg.add(bt.set_memory_location(custom_button_config[CONF_MEMORY_LOCATION])) cg.add(bt.set_memory_address(custom_button_config[CONF_MEMORY_ADDRESS])) diff --git a/esphome/components/micronova/button/micronova_button.cpp b/esphome/components/micronova/button/micronova_button.cpp index 3f49d4b5b3b..13c0e1f1176 100644 --- a/esphome/components/micronova/button/micronova_button.cpp +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -2,9 +2,15 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.button"; + +void MicroNovaButton::dump_config() { + LOG_BUTTON("", "Micronova button", this); + this->dump_base_config(); +} + void MicroNovaButton::press_action() { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_); - this->micronova_->request_update_listeners(); + this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_); } } // namespace esphome::micronova diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h index 951ae8bba38..0258dbb53c2 100644 --- a/esphome/components/micronova/button/micronova_button.h +++ b/esphome/components/micronova/button/micronova_button.h @@ -6,19 +6,18 @@ namespace esphome::micronova { -class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener { +class MicroNovaButton : public Component, public button::Button, public MicroNovaBaseListener { public: - MicroNovaButton(MicroNova *m) : MicroNovaButtonListener(m) {} - void dump_config() override { - LOG_BUTTON("", "Micronova button", this); - this->dump_base_config(); - } + MicroNovaButton(MicroNova *m) : MicroNovaBaseListener(m) {} + void dump_config() override; void set_memory_data(uint8_t f) { this->memory_data_ = f; } uint8_t get_memory_data() { return this->memory_data_; } protected: void press_action() override; + + uint8_t memory_data_ = 0; }; } // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp index 22daef4fe6c..c2e3ef86405 100644 --- a/esphome/components/micronova/micronova.cpp +++ b/esphome/components/micronova/micronova.cpp @@ -3,8 +3,12 @@ namespace esphome::micronova { -static const int STOVE_REPLY_DELAY = 60; -static const uint8_t WRITE_BIT = 1 << 7; // 0x80 +static const char *const TAG = "micronova"; +static constexpr uint8_t STOVE_REPLY_SIZE = 2; +static constexpr uint32_t STOVE_REPLY_TIMEOUT = 200; // ms +static constexpr uint8_t WRITE_BIT = 1 << 7; // 0x80 + +bool MicroNovaCommand::is_write() const { return this->memory_location & WRITE_BIT; } void MicroNovaBaseListener::dump_base_config() { ESP_LOGCONFIG(TAG, @@ -18,139 +22,193 @@ void MicroNovaListener::dump_base_config() { LOG_UPDATE_INTERVAL(this); } +void MicroNovaListener::request_value_from_stove_() { + this->micronova_->queue_read_request(this->memory_location_, this->memory_address_); +} + void MicroNova::setup() { - if (this->enable_rx_pin_ != nullptr) { - this->enable_rx_pin_->setup(); - this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT); - this->enable_rx_pin_->digital_write(false); - } - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = 0; - this->current_transmission_.memory_address = 0; - this->current_transmission_.reply_pending = false; - this->current_transmission_.initiating_listener = nullptr; + this->enable_rx_pin_->setup(); + this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->enable_rx_pin_->digital_write(false); } void MicroNova::dump_config() { ESP_LOGCONFIG(TAG, "MicroNova:"); - if (this->enable_rx_pin_ != nullptr) { - LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_); - } + LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_); } -void MicroNova::request_update_listeners() { - ESP_LOGD(TAG, "Schedule listener update"); - for (auto &mv_listener : this->micronova_listeners_) { - mv_listener->set_needs_update(true); +#ifdef MICRONOVA_LISTENER_COUNT +void MicroNova::register_micronova_listener(MicroNovaListener *listener) { + this->listeners_.push_back(listener); + // Request initial value + this->queue_read_request(listener->get_memory_location(), listener->get_memory_address()); +} + +void MicroNova::request_update_listeners_() { + ESP_LOGD(TAG, "Requesting update from all listeners"); + for (auto *listener : this->listeners_) { + this->queue_read_request(listener->get_memory_location(), listener->get_memory_address()); } } +#endif void MicroNova::loop() { - // Only read one sensor that needs update per loop - // If STOVE_REPLY_DELAY time has passed since last loop() - // check for a reply from the stove - if ((this->current_transmission_.reply_pending) && - (millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) { - int stove_reply_value = this->read_stove_reply(); - if (this->current_transmission_.initiating_listener != nullptr) { - this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value); - this->current_transmission_.initiating_listener = nullptr; - } - this->current_transmission_.reply_pending = false; - return; - } else if (!this->current_transmission_.reply_pending) { - for (auto &mv_listener : this->micronova_listeners_) { - if (mv_listener->get_needs_update()) { - mv_listener->set_needs_update(false); - this->current_transmission_.initiating_listener = mv_listener; - mv_listener->request_value_from_stove(); - return; + // Check if we're processing a command and waiting for reply + if (this->reply_pending_) { + // Check if all reply bytes have arrived + if (this->available() >= STOVE_REPLY_SIZE) { +#ifdef MICRONOVA_LISTENER_COUNT + int stove_reply_value = this->read_stove_reply_(); + if (this->current_command_.is_write()) { + if (stove_reply_value == -1) { + ESP_LOGW(TAG, "Write to [0x%02X:0x%02X] may have failed (checksum mismatch in reply)", + this->current_command_.memory_location & ~WRITE_BIT, this->current_command_.memory_address); + } + } else { + // For READ commands, notify all listeners registered for this address + uint8_t loc = this->current_command_.memory_location; + uint8_t addr = this->current_command_.memory_address; + for (auto *listener : this->listeners_) { + if (listener->get_memory_location() == loc && listener->get_memory_address() == addr) { + listener->process_value_from_stove(stove_reply_value); + } + } } +#else + this->read_stove_reply_(); +#endif + this->reply_pending_ = false; + } else if (millis() - this->transmission_time_ > STOVE_REPLY_TIMEOUT) { + // Timeout - no reply received (buffer cleared before next command) + ESP_LOGW(TAG, "Timeout waiting for reply from [0x%02X:0x%02X], available: %d", + this->current_command_.memory_location, this->current_command_.memory_address, this->available()); + this->reply_pending_ = false; } + return; } + + // No reply pending - process next command (writes have priority over reads) +#ifdef USE_MICRONOVA_WRITER + if (!this->write_queue_.empty()) { + this->current_command_ = this->write_queue_.front(); + this->write_queue_.pop(); + this->send_current_command_(); + return; + } +#endif +#ifdef MICRONOVA_LISTENER_COUNT + if (!this->read_queue_.empty()) { + this->current_command_ = this->read_queue_.front(); + this->read_queue_.pop(); + this->send_current_command_(); + } +#endif } -void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaListener *listener) { - uint8_t write_data[2] = {0, 0}; +#ifdef MICRONOVA_LISTENER_COUNT +void MicroNova::queue_read_request(uint8_t location, uint8_t address) { + // Check if this read is already queued + for (const auto &queued : this->read_queue_) { + if (queued.memory_location == location && queued.memory_address == address) { + ESP_LOGV(TAG, "Read [%02X,%02X] already queued, skipping", location, address); + return; + } + } + + MicroNovaCommand cmd; + cmd.memory_location = location; + cmd.memory_address = address; + cmd.data = 0; + + if (!this->read_queue_.push(cmd)) { + ESP_LOGW(TAG, "Read queue full, dropping read [%02X,%02X]", location, address); + return; + } + ESP_LOGV(TAG, "Queued read [%02X,%02X] (queue size: %u)", location, address, this->read_queue_.size()); +} +#endif + +void MicroNova::send_current_command_() { uint8_t trash_rx; - if (this->reply_pending_mutex_.try_lock()) { - // clear rx buffer. - // Stove hickups may cause late replies in the rx - while (this->available()) { - this->read_byte(&trash_rx); - ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx); - } - - write_data[0] = location; - write_data[1] = address; - ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]); - - this->enable_rx_pin_->digital_write(true); - this->write_array(write_data, 2); - this->flush(); - this->enable_rx_pin_->digital_write(false); - - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = location; - this->current_transmission_.memory_address = address; - this->current_transmission_.reply_pending = true; - this->current_transmission_.initiating_listener = listener; - } else { - ESP_LOGE(TAG, "Reply is pending, skipping read request"); + // Clear rx buffer - stove hiccups may cause late replies in the rx + while (this->available()) { + this->read_byte(&trash_rx); + ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx); } + + uint8_t write_data[4] = {this->current_command_.memory_location, this->current_command_.memory_address, 0, 0}; + size_t write_len; + + if (this->current_command_.is_write()) { + write_len = 4; + write_data[2] = this->current_command_.data; + // calculate checksum + write_data[3] = write_data[0] + write_data[1] + write_data[2]; + ESP_LOGV(TAG, "Sending write request [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], + write_data[3]); + } else { + write_len = 2; + ESP_LOGV(TAG, "Sending read request [%02X,%02X]", write_data[0], write_data[1]); + } + + this->enable_rx_pin_->digital_write(true); + this->write_array(write_data, write_len); + this->flush(); + this->enable_rx_pin_->digital_write(false); + + this->transmission_time_ = millis(); + this->reply_pending_ = true; } -int MicroNova::read_stove_reply() { +int MicroNova::read_stove_reply_() { uint8_t reply_data[2] = {0, 0}; - uint8_t checksum = 0; - // assert enable_rx_pin is false this->read_array(reply_data, 2); - this->reply_pending_mutex_.unlock(); ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]); - checksum = ((uint16_t) this->current_transmission_.memory_location + - (uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) & - 0xFF; + uint8_t checksum = this->current_command_.memory_location + this->current_command_.memory_address + reply_data[1]; if (reply_data[0] != checksum) { - ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X", - this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0], + ESP_LOGE(TAG, "Checksum mismatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X", + this->current_command_.memory_location, this->current_command_.memory_address, reply_data[0], reply_data[1], checksum, reply_data[0]); return -1; } return ((int) reply_data[1]); } -void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { - uint8_t write_data[4] = {0, 0, 0, 0}; - uint16_t checksum = 0; +#ifdef USE_MICRONOVA_WRITER +bool MicroNova::queue_write_command(uint8_t location, uint8_t address, uint8_t data) { + MicroNovaCommand cmd; + cmd.memory_location = location | WRITE_BIT; + cmd.memory_address = address; + cmd.data = data; - if (this->reply_pending_mutex_.try_lock()) { - uint8_t write_location = location | WRITE_BIT; - write_data[0] = write_location; - write_data[1] = address; - write_data[2] = data; - - checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF; - write_data[3] = checksum; - - ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]); - - this->enable_rx_pin_->digital_write(true); - this->write_array(write_data, 4); - this->flush(); - this->enable_rx_pin_->digital_write(false); - - this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = write_location; - this->current_transmission_.memory_address = address; - this->current_transmission_.reply_pending = true; - this->current_transmission_.initiating_listener = nullptr; - } else { - ESP_LOGE(TAG, "Reply is pending, skipping write"); + // Check if a write to the same address is already queued - update data in-place + for (auto &queued : this->write_queue_) { + if (queued.memory_location == cmd.memory_location && queued.memory_address == cmd.memory_address) { + if (queued.data != cmd.data) { + ESP_LOGD(TAG, "Updating queued write [%02X,%02X] data 0x%02X -> 0x%02X", location, address, queued.data, data); + queued.data = cmd.data; + } else { + ESP_LOGV(TAG, "Write [%02X,%02X] with data 0x%02X already queued, skipping", location, address, data); + } + return true; + } } + + if (!this->write_queue_.push(cmd)) { + ESP_LOGW(TAG, "Write queue full, dropping command"); + return false; + } + ESP_LOGD(TAG, "Queued write [%02X,%02X] (queue size: %u)", location, address, this->write_queue_.size()); +#ifdef MICRONOVA_LISTENER_COUNT + // Automatically queue sensor updates after write commands + this->request_update_listeners_(); +#endif + return true; } +#endif } // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h index a70f355ead4..58cca30b836 100644 --- a/esphome/components/micronova/micronova.h +++ b/esphome/components/micronova/micronova.h @@ -6,11 +6,19 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include <vector> - namespace esphome::micronova { -static const char *const TAG = "micronova"; +static constexpr uint8_t WRITE_QUEUE_SIZE = 10; + +/// Represents a command to be sent to the stove +/// Write commands have the high bit (0x80) set in memory_location +struct MicroNovaCommand { + uint8_t memory_location; + uint8_t memory_address; + uint8_t data; ///< Only used for write commands + + bool is_write() const; +}; class MicroNova; @@ -18,11 +26,8 @@ class MicroNova; // Interface classes. class MicroNovaBaseListener { public: - MicroNovaBaseListener() {} MicroNovaBaseListener(MicroNova *m) { this->micronova_ = m; } - void set_micronova_object(MicroNova *m) { this->micronova_ = m; } - void set_memory_location(uint8_t l) { this->memory_location_ = l; } uint8_t get_memory_location() { return this->memory_location_; } @@ -32,70 +37,76 @@ class MicroNovaBaseListener { void dump_base_config(); protected: - MicroNova *micronova_{nullptr}; + MicroNova *micronova_; uint8_t memory_location_ = 0; uint8_t memory_address_ = 0; }; class MicroNovaListener : public MicroNovaBaseListener, public PollingComponent { public: - MicroNovaListener() {} MicroNovaListener(MicroNova *m) : MicroNovaBaseListener(m) {} - virtual void request_value_from_stove() = 0; + + void update() override { this->request_value_from_stove_(); } + virtual void process_value_from_stove(int value_from_stove) = 0; - void set_needs_update(bool u) { this->needs_update_ = u; } - bool get_needs_update() { return this->needs_update_; } - - void update() override { this->set_needs_update(true); } - void dump_base_config(); protected: - bool needs_update_ = false; -}; - -class MicroNovaButtonListener : public MicroNovaBaseListener { - public: - MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {} - - protected: - uint8_t memory_data_ = 0; + void request_value_from_stove_(); }; ///////////////////////////////////////////////////////////////////// // Main component class class MicroNova : public Component, public uart::UARTDevice { public: - MicroNova() {} + MicroNova(GPIOPin *enable_rx_pin) : enable_rx_pin_(enable_rx_pin) {} void setup() override; void loop() override; void dump_config() override; - void register_micronova_listener(MicroNovaListener *l) { this->micronova_listeners_.push_back(l); } - void request_update_listeners(); - void request_address(uint8_t location, uint8_t address, MicroNovaListener *listener); - void write_address(uint8_t location, uint8_t address, uint8_t data); - int read_stove_reply(); +#ifdef MICRONOVA_LISTENER_COUNT + void register_micronova_listener(MicroNovaListener *listener); - void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; } + /// Queue a read request to the stove (low priority - added at back) + /// All listeners registered for this address will be notified with the result + /// @param location Memory location on the stove + /// @param address Memory address on the stove + void queue_read_request(uint8_t location, uint8_t address); +#endif + +#ifdef USE_MICRONOVA_WRITER + /// Queue a write command to the stove (processed before reads) + /// @param location Memory location on the stove + /// @param address Memory address on the stove + /// @param data Data to write + /// @return true if command was queued, false if queue was full + bool queue_write_command(uint8_t location, uint8_t address, uint8_t data); +#endif protected: - GPIOPin *enable_rx_pin_{nullptr}; + void send_current_command_(); + int read_stove_reply_(); +#ifdef MICRONOVA_LISTENER_COUNT + void request_update_listeners_(); +#endif - struct MicroNovaSerialTransmission { - uint32_t request_transmission_time; - uint8_t memory_location; - uint8_t memory_address; - bool reply_pending; - MicroNovaListener *initiating_listener; - }; + GPIOPin *enable_rx_pin_; - Mutex reply_pending_mutex_; - MicroNovaSerialTransmission current_transmission_; +#ifdef USE_MICRONOVA_WRITER + StaticRingBuffer<MicroNovaCommand, WRITE_QUEUE_SIZE> write_queue_; +#endif +#ifdef MICRONOVA_LISTENER_COUNT + StaticRingBuffer<MicroNovaCommand, MICRONOVA_LISTENER_COUNT> read_queue_; +#endif + MicroNovaCommand current_command_{}; + uint32_t transmission_time_{0}; ///< Time when current command was sent + bool reply_pending_{false}; ///< True if we are waiting for a reply from the stove - std::vector<MicroNovaListener *> micronova_listeners_{}; +#ifdef MICRONOVA_LISTENER_COUNT + StaticVector<MicroNovaListener *, MICRONOVA_LISTENER_COUNT> listeners_; +#endif }; } // namespace esphome::micronova diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py index ef6cc0f7d7c..bcc972c5a9d 100644 --- a/esphome/components/micronova/number/__init__.py +++ b/esphome/components/micronova/number/__init__.py @@ -9,6 +9,7 @@ from .. import ( MicroNova, MicroNovaListener, micronova_ns, + register_micronova_writer, to_code_micronova_listener, ) @@ -59,22 +60,24 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE): + register_micronova_writer() numb = await number.new_number( thermostat_temperature_config, + mv, min_value=0, max_value=40, step=thermostat_temperature_config.get(CONF_STEP), ) await to_code_micronova_listener(mv, numb, thermostat_temperature_config) - cg.add(numb.set_micronova_object(mv)) cg.add(numb.set_use_step_scaling(True)) if power_level_config := config.get(CONF_POWER_LEVEL): + register_micronova_writer() numb = await number.new_number( power_level_config, + mv, min_value=1, max_value=5, step=1, ) await to_code_micronova_listener(mv, numb, power_level_config) - cg.add(numb.set_micronova_object(mv)) diff --git a/esphome/components/micronova/number/micronova_number.cpp b/esphome/components/micronova/number/micronova_number.cpp index 80279474689..02a6de1c844 100644 --- a/esphome/components/micronova/number/micronova_number.cpp +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.number"; + +void MicroNovaNumber::dump_config() { + LOG_NUMBER("", "Micronova number", this); + this->dump_base_config(); +} + void MicroNovaNumber::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state(NAN); @@ -22,8 +29,7 @@ void MicroNovaNumber::control(float value) { } else { new_number = static_cast<uint8_t>(value); } - this->micronova_->write_address(this->memory_location_, this->memory_address_, new_number); - this->micronova_->request_update_listeners(); + this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, new_number); } } // namespace esphome::micronova diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h index 3fc5838a4fc..73666b632bf 100644 --- a/esphome/components/micronova/number/micronova_number.h +++ b/esphome/components/micronova/number/micronova_number.h @@ -7,16 +7,9 @@ namespace esphome::micronova { class MicroNovaNumber : public number::Number, public MicroNovaListener { public: - MicroNovaNumber() {} MicroNovaNumber(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_NUMBER("", "Micronova number", this); - this->dump_base_config(); - } + void dump_config() override; void control(float value) override; - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } void process_value_from_stove(int value_from_stove) override; void set_use_step_scaling(bool v) { this->use_step_scaling_ = v; } diff --git a/esphome/components/micronova/sensor/micronova_sensor.cpp b/esphome/components/micronova/sensor/micronova_sensor.cpp index d845e0ab3cb..8d528145cd2 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.cpp +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.sensor"; + +void MicroNovaSensor::dump_config() { + LOG_SENSOR("", "Micronova sensor", this); + this->dump_base_config(); +} + void MicroNovaSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state(NAN); diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h index a2f232c7dc5..f3b06d140e4 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.h +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -8,14 +8,8 @@ namespace esphome::micronova { class MicroNovaSensor : public sensor::Sensor, public MicroNovaListener { public: MicroNovaSensor(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_SENSOR("", "Micronova sensor", this); - this->dump_base_config(); - } + void dump_config() override; - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } void process_value_from_stove(int value_from_stove) override; void set_divisor(uint8_t d) { this->divisor_ = d; } diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index c937a4cac9e..d9722b5d488 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -9,6 +9,7 @@ from .. import ( MicroNova, MicroNovaListener, micronova_ns, + register_micronova_writer, to_code_micronova_listener, ) @@ -48,6 +49,7 @@ async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) if stove_config := config.get(CONF_STOVE): + register_micronova_writer() sw = await switch.new_switch(stove_config, mv) await to_code_micronova_listener(mv, sw, stove_config) cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON])) diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index 9b9ad610183..d01cc572543 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -2,33 +2,42 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.switch"; + +void MicroNovaSwitch::dump_config() { + LOG_SWITCH("", "Micronova switch", this); + this->dump_base_config(); +} + void MicroNovaSwitch::write_state(bool state) { if (state) { // Only send power-on when current state is Off if (this->raw_state_ == 0) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); - this->publish_state(true); + if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_on_)) { + this->publish_state(true); + } } else { ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", this->raw_state_); } } else { // don't send power-off when status is Off or Final cleaning if (this->raw_state_ != 0 && this->raw_state_ != 6) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); - this->publish_state(false); + if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, + this->memory_data_off_)) { + this->publish_state(false); + } } else { ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", this->raw_state_); } } - this->set_needs_update(true); } void MicroNovaSwitch::process_value_from_stove(int value_from_stove) { - this->raw_state_ = value_from_stove; if (value_from_stove == -1) { ESP_LOGE(TAG, "Error reading stove state"); return; } + this->raw_state_ = value_from_stove; // set the stove switch to on for any value but 0 bool state = value_from_stove != 0; diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h index 96c2c14e9ef..fee3c739769 100644 --- a/esphome/components/micronova/switch/micronova_switch.h +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -9,13 +9,7 @@ namespace esphome::micronova { class MicroNovaSwitch : public switch_::Switch, public MicroNovaListener { public: MicroNovaSwitch(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_SWITCH("", "Micronova switch", this); - this->dump_base_config(); - } - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } + void dump_config() override; void process_value_from_stove(int value_from_stove) override; void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; } diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp index 2217ed6d6f1..50a0d34b3a7 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -2,6 +2,13 @@ namespace esphome::micronova { +static const char *const TAG = "micronova.text_sensor"; + +void MicroNovaTextSensor::dump_config() { + LOG_TEXT_SENSOR("", "Micronova text sensor", this); + this->dump_base_config(); +} + void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { this->publish_state("unknown"); diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h index 290f0ca45a7..6918a372e86 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.h +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -20,13 +20,7 @@ static const char *const STOVE_STATES[11] = {"Off", class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaListener { public: MicroNovaTextSensor(MicroNova *m) : MicroNovaListener(m) {} - void dump_config() override { - LOG_TEXT_SENSOR("", "Micronova text sensor", this); - this->dump_base_config(); - } - void request_value_from_stove() override { - this->micronova_->request_address(this->memory_location_, this->memory_address_, this); - } + void dump_config() override; void process_value_from_stove(int value_from_stove) override; }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 44918fe00c2..cec77fe2e27 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -107,6 +107,8 @@ #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 +#define MICRONOVA_LISTENER_COUNT 1 +#define USE_MICRONOVA_WRITER #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 11e0afe5260..70ac1574f0c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -293,6 +293,81 @@ template<typename T, size_t N> class StaticVector { operator std::span<const T>() const { return std::span<const T>(data_.data(), count_); } }; +/// Fixed-size circular buffer with FIFO semantics and iteration support. +/// +/// A tiny ring buffer that avoids dynamic allocations from std::deque/std::queue +/// (which can be wasteful on MCUs), while supporting iteration over queued elements. +/// +/// Not thread-safe. All access (push/pop/iteration) must occur from a single +/// context, or the caller must provide external synchronization. +template<typename T, size_t N> class StaticRingBuffer { + using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>; + + public: + class Iterator { + public: + Iterator(StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + T &operator*() { return buf_->data_[(buf_->head_ + pos_) % N]; } + Iterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const Iterator &other) const { return pos_ != other.pos_; } + + private: + StaticRingBuffer *buf_; + index_type pos_; + }; + + class ConstIterator { + public: + ConstIterator(const StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % N]; } + ConstIterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; } + + private: + const StaticRingBuffer *buf_; + index_type pos_; + }; + + bool push(const T &value) { + if (this->count_ >= N) { + return false; + } + this->data_[this->tail_] = value; + this->tail_ = (this->tail_ + 1) % N; + ++this->count_; + return true; + } + + void pop() { + if (this->count_ > 0) { + this->head_ = (this->head_ + 1) % N; + --this->count_; + } + } + + T &front() { return this->data_[this->head_]; } + const T &front() const { return this->data_[this->head_]; } + index_type size() const { return this->count_; } + bool empty() const { return this->count_ == 0; } + + Iterator begin() { return Iterator(this, 0); } + Iterator end() { return Iterator(this, this->count_); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator end() const { return ConstIterator(this, this->count_); } + + protected: + T data_[N]; + index_type head_{0}; + index_type tail_{0}; + index_type count_{0}; +}; + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time From c52a48ed38923eccf0d5c38a70650d34e17a4f22 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:11:46 -0400 Subject: [PATCH 0559/2056] [multiple] Convert static function locals to member variables (#14689) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/daikin_arc/daikin_arc.cpp | 5 ++--- esphome/components/daikin_arc/daikin_arc.h | 1 + esphome/components/haier/hon_climate.cpp | 5 ++--- esphome/components/haier/hon_climate.h | 1 + .../components/ina2xx_base/ina2xx_base.cpp | 5 ++--- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 21 +++++++++---------- esphome/components/ltr_als_ps/ltr_als_ps.h | 5 ++++- .../matrix_keypad/matrix_keypad.cpp | 12 +++++------ .../components/matrix_keypad/matrix_keypad.h | 2 ++ .../components/mqtt/mqtt_backend_esp32.cpp | 9 ++++---- esphome/components/mqtt/mqtt_backend_esp32.h | 1 + esphome/components/sgp4x/sgp4x.cpp | 5 +++-- esphome/components/sgp4x/sgp4x.h | 3 ++- 13 files changed, 39 insertions(+), 36 deletions(-) diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index adb7b9fec76..18f12dbfc6c 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -91,11 +91,10 @@ void DaikinArcClimate::transmit_state() { remote_state[5] = this->operation_mode_() | 0x08; remote_state[6] = this->temperature_(); remote_state[7] = this->humidity_(); - static uint8_t last_humidity = 0x66; - if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) { + if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) { ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]); remote_header[9] |= 0x10; - last_humidity = remote_state[7]; + this->last_humidity_ = remote_state[7]; } uint16_t fan_speed = this->fan_speed_(); remote_state[8] = fan_speed >> 8; diff --git a/esphome/components/daikin_arc/daikin_arc.h b/esphome/components/daikin_arc/daikin_arc.h index 6cfffd47255..2b4d4375aae 100644 --- a/esphome/components/daikin_arc/daikin_arc.h +++ b/esphome/components/daikin_arc/daikin_arc.h @@ -70,6 +70,7 @@ class DaikinArcClimate : public climate_ir::ClimateIR { // Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; bool parse_state_frame_(const uint8_t frame[]); + uint8_t last_humidity_{0x66}; }; } // namespace daikin_arc diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index be5035caa17..b8889ef2bdc 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -1375,9 +1375,8 @@ void HonClimate::process_protocol_reset() { bool HonClimate::should_get_big_data_() { if (this->big_data_sensors_ > 0) { - static uint8_t counter = 0; - counter = (counter + 1) % 3; - return counter == 1; + this->big_data_counter_ = (this->big_data_counter_ + 1) % 3; + return this->big_data_counter_ == 1; } return false; } diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 4565ed2981d..9bddac3f923 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -188,6 +188,7 @@ class HonClimate : public HaierClimateBase { float active_alarm_count_{NAN}; std::chrono::steady_clock::time_point last_alarm_request_; int big_data_sensors_{0}; + uint8_t big_data_counter_{0}; esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{}; esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{}; HonSettings settings_{}; diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 9f510eef74f..2d08562e542 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -572,9 +572,8 @@ bool INA2XX::write_unsigned_16_(uint8_t reg, uint16_t val) { } bool INA2XX::read_unsigned_(uint8_t reg, uint8_t reg_size, uint64_t &data_out) { - static uint8_t rx_buf[5] = {0}; // max buffer size - - if (reg_size > 5) { + uint8_t rx_buf[5]{}; + if (reg_size > sizeof(rx_buf)) { return false; } diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index f9c1474c85a..ff335fe34c6 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -137,7 +137,6 @@ void LTRAlsPsComponent::update() { void LTRAlsPsComponent::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -166,20 +165,20 @@ void LTRAlsPsComponent::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->read_data_tries_ = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->state_ = State::DATA_COLLECTED; this->apply_lux_calculation_(this->als_readings_); - } else if (tries >= MAX_TRIES) { + } else if (this->read_data_tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries."); - tries = 0; + this->read_data_tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->read_data_tries_++; } break; @@ -221,21 +220,21 @@ void LTRAlsPsComponent::loop() { } void LTRAlsPsComponent::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index c6052300de8..3ab2cea074f 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -126,10 +126,13 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; float glass_attenuation_factor_{1.0}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; uint16_t ps_cooldown_time_s_{5}; - PsGain ps_gain_{PsGain::PS_GAIN_16}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint8_t read_data_tries_{0}; + PsGain ps_gain_{PsGain::PS_GAIN_16}; // // Sensors for publishing data diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index febbe794e47..cc46ba98d63 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -27,8 +27,6 @@ void MatrixKeypad::setup() { } void MatrixKeypad::loop() { - static uint32_t active_start = 0; - static int active_key = -1; uint32_t now = App.get_loop_component_start_time(); int key = -1; bool error = false; @@ -54,8 +52,8 @@ void MatrixKeypad::loop() { if (error) return; - if (key != active_key) { - if ((active_key != -1) && (this->pressed_key_ == active_key)) { + if (key != this->active_key_) { + if ((this->active_key_ != -1) && (this->pressed_key_ == this->active_key_)) { row = this->pressed_key_ / this->columns_.size(); col = this->pressed_key_ % this->columns_.size(); ESP_LOGD(TAG, "key @ row %d, col %d released", row, col); @@ -70,13 +68,13 @@ void MatrixKeypad::loop() { this->pressed_key_ = -1; } - active_key = key; + this->active_key_ = key; if (key == -1) return; - active_start = now; + this->active_start_ = now; } - if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_)) + if ((this->pressed_key_ == key) || (now - this->active_start_ < this->debounce_time_)) return; row = key / this->columns_.size(); diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 258ab4fadc8..8963612d0c7 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -44,6 +44,8 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { bool has_diodes_{false}; bool has_pulldowns_{false}; int pressed_key_ = -1; + uint32_t active_start_{0}; + int active_key_{-1}; std::vector<MatrixKeypadListener *> listeners_{}; std::vector<MatrixKeyTrigger *> key_triggers_; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 8a7fb965e96..5642fd5f7b6 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -150,17 +150,16 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { this->on_publish_.call((int) event.msg_id); break; case MQTT_EVENT_DATA: { - static std::string topic; if (!event.topic.empty()) { // When a single message arrives as multiple chunks, the topic will be empty // on any but the first message, leading to event.topic being an empty string. // To ensure handlers get the correct topic, cache the last seen topic to // simulate always receiving the topic from underlying library - topic = event.topic; + this->cached_topic_ = event.topic; } - ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); - this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset, - event.total_data_len); + ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", this->cached_topic_.c_str()); + this->on_message_.call(this->cached_topic_.c_str(), event.data.data(), event.data.size(), + event.current_data_offset, event.total_data_len); } break; case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index ccc4c4026c7..5c4dc413bdc 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -265,6 +265,7 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_; CallbackManager<on_message_callback_t> on_message_; CallbackManager<on_publish_user_callback_t> on_publish_; + std::string cached_topic_; std::queue<Event> mqtt_events_; #if defined(USE_MQTT_IDF_ENQUEUE) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index cb41e374f82..bbf1ffd4c09 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -124,6 +124,7 @@ void SGP4xComponent::self_test_() { } this->self_test_complete_ = true; + this->nox_conditioning_start_ = millis(); ESP_LOGD(TAG, "Self-test complete"); }); } @@ -161,7 +162,6 @@ void SGP4xComponent::update_gas_indices_() { void SGP4xComponent::measure_raw_() { float humidity = NAN; - static uint32_t nox_conditioning_start = millis(); if (!this->self_test_complete_) { ESP_LOGW(TAG, "Self-test incomplete"); @@ -191,10 +191,11 @@ void SGP4xComponent::measure_raw_() { response_words = 1; } else { // SGP41 sensor must use NOx conditioning command for the first 10 seconds - if (millis() - nox_conditioning_start < 10000) { + if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) { command = SGP41_CMD_NOX_CONDITIONING; response_words = 1; } else { + this->nox_conditioning_start_.reset(); command = SGP41_CMD_MEASURE_RAW; response_words = 2; } diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 89fa627c61c..6b8b598aff3 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -127,8 +127,9 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se uint16_t measure_time_; uint8_t samples_read_ = 0; uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2; - bool store_baseline_; + + optional<uint32_t> nox_conditioning_start_{}; ESPPreferenceObject pref_; uint32_t seconds_since_last_store_; SGP4xBaselines voc_baselines_storage_; From 4e16f270a3928738460139426748c3d96dafdddd Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 12:47:58 -0500 Subject: [PATCH 0560/2056] [speaker_source] Add playlist management (#14652) --- .../components/speaker_source/automation.h | 29 ++ .../components/speaker_source/media_player.py | 43 ++ .../speaker_source_media_player.cpp | 401 ++++++++++++++---- .../speaker_source_media_player.h | 46 +- tests/components/speaker_source/common.yaml | 5 + 5 files changed, 429 insertions(+), 95 deletions(-) create mode 100644 esphome/components/speaker_source/automation.h diff --git a/esphome/components/speaker_source/automation.h b/esphome/components/speaker_source/automation.h new file mode 100644 index 00000000000..b436149a03f --- /dev/null +++ b/esphome/components/speaker_source/automation.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "speaker_source_media_player.h" + +namespace esphome::speaker_source { + +template<typename... Ts> class SetPlaylistDelayAction : public Action<Ts...> { + public: + explicit SetPlaylistDelayAction(SpeakerSourceMediaPlayer *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(uint8_t, pipeline) + TEMPLATABLE_VALUE(uint32_t, delay) + + void play(const Ts &...x) override { + this->parent_->set_playlist_delay_ms(this->pipeline_.value(x...), this->delay_.value(x...)); + } + + protected: + SpeakerSourceMediaPlayer *parent_; +}; + +} // namespace esphome::speaker_source + +#endif // USE_ESP32 diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index a44cdcbf01e..9080bebcae0 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -3,13 +3,16 @@ import esphome.codegen as cg from esphome.components import audio, media_player, media_source, speaker import esphome.config_validation as cv from esphome.const import ( + CONF_DELAY, CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE, CONF_SPEAKER, ) +from esphome.core import ID from esphome.core.entity_helpers import inherit_property_from +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType AUTO_LOAD = ["audio"] @@ -19,6 +22,7 @@ CODEOWNERS = ["@kahrendt"] CONF_MEDIA_PIPELINE = "media_pipeline" CONF_ON_MUTE = "on_mute" +CONF_PIPELINE = "pipeline" CONF_ON_UNMUTE = "on_unmute" CONF_ON_VOLUME = "on_volume" CONF_SOURCES = "sources" @@ -36,6 +40,13 @@ SpeakerSourceMediaPlayer = speaker_source_ns.class_( PipelineContext = speaker_source_ns.struct("PipelineContext") Pipeline = speaker_source_ns.enum("Pipeline") +PIPELINE_ENUM = { + "media": Pipeline.MEDIA_PIPELINE, +} + +SetPlaylistDelayAction = speaker_source_ns.class_( + "SetPlaylistDelayAction", automation.Action +) FORMAT_MAPPING = { @@ -210,3 +221,35 @@ async def to_code(config: ConfigType) -> None: [(cg.float_, "x")], on_volume, ) + + +SET_PLAYLIST_DELAY_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(SpeakerSourceMediaPlayer), + cv.Required(CONF_PIPELINE): cv.enum(PIPELINE_ENUM, lower=True), + cv.Required(CONF_DELAY): cv.templatable(cv.positive_time_period_milliseconds), + } +) + + +@automation.register_action( + "speaker_source.set_playlist_delay", + SetPlaylistDelayAction, + SET_PLAYLIST_DELAY_ACTION_SCHEMA, + synchronous=True, +) +async def set_playlist_delay_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + cg.add(var.set_pipeline(config[CONF_PIPELINE])) + + template_ = await cg.templatable(config[CONF_DELAY], args, cg.uint32) + cg.add(var.set_delay(template_)) + + return var diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index a3679891d2d..3724e206673 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -135,8 +135,12 @@ void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, med PipelineContext &ps = this->pipelines_[pipeline]; if (state == media_source::MediaSourceState::IDLE) { + // Track whether this IDLE was from an orchestrator-initiated stop (e.g., NEXT/PREV/PLAY_URI) + // so we can suppress spurious PLAYLIST_ADVANCE below + bool was_stopping = (ps.stopping_source == source); + // Source went idle - clear stopping flag if this was the source we asked to stop - if (ps.stopping_source == source) { + if (was_stopping) { ps.stopping_source = nullptr; } @@ -152,6 +156,11 @@ void SpeakerSourceMediaPlayer::handle_media_state_changed_(uint8_t pipeline, med // Finish the speaker to ensure it's ready for the next playback ps.speaker->finish(); + + // Only advance the playlist if the track finished naturally (not stopped by the orchestrator) + if (!was_stopping) { + this->queue_command_(MediaPlayerControlCommand::PLAYLIST_ADVANCE, pipeline); + } } } else if (state == media_source::MediaSourceState::PLAYING) { // Source started playing - make it the active source if no one else is active @@ -197,8 +206,9 @@ size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_so return 0; } +// THREAD CONTEXT: Called from main loop (loop) media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( - media_source::MediaSource *source) const { + media_source::MediaSource *source, bool playlist_active, media_player::MediaPlayerState old_state) const { if (source != nullptr) { switch (source->get_state()) { case media_source::MediaSourceState::PLAYING: @@ -214,97 +224,27 @@ media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_stat } } + // No active source. Stay PLAYING during playlist transitions + if (playlist_active && old_state == media_player::MEDIA_PLAYER_STATE_PLAYING) { + return media_player::MEDIA_PLAYER_STATE_PLAYING; + } return media_player::MEDIA_PLAYER_STATE_IDLE; } void SpeakerSourceMediaPlayer::loop() { // Process queued control commands - MediaPlayerControlCommand control_command; - - // Use peek to check command without removing it - if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) == pdTRUE) { - bool command_executed = false; - uint8_t pipeline = control_command.pipeline; - - switch (control_command.type) { - case MediaPlayerControlCommand::PLAY_URI: { - command_executed = this->try_execute_play_uri_(*control_command.data.uri, pipeline); - break; - } - - case MediaPlayerControlCommand::SEND_COMMAND: { - PipelineContext &ps = this->pipelines_[pipeline]; - - // Determine target source: prefer active, fall back to last - media_source::MediaSource *target_source = nullptr; - if (ps.active_source != nullptr) { - target_source = ps.active_source; - } else if (ps.last_source != nullptr) { - target_source = ps.last_source; - } - - media_player::MediaPlayerCommand player_command = control_command.data.command; - switch (player_command) { - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { - media_source::MediaSource *active_source = ps.active_source; - if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PAUSE); - } - } else { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PLAY); - } - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_PLAY: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PLAY); - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::PAUSE); - } - break; - } - - case media_player::MEDIA_PLAYER_COMMAND_STOP: { - if (target_source != nullptr) { - target_source->handle_command(media_source::MediaSourceCommand::STOP); - } - break; - } - - default: - break; - } - - command_executed = true; - break; - } - } - - // Only remove from queue if successfully executed - if (command_executed) { - xQueueReceive(this->media_control_command_queue_, &control_command, 0); - - // Delete the allocated string for PLAY_URI commands - if (control_command.type == MediaPlayerControlCommand::PLAY_URI) { - delete control_command.data.uri; - } - } - } + this->process_control_queue_(); // Update state based on active sources media_player::MediaPlayerState old_state = this->state; PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; - this->state = this->get_media_pipeline_state_(media_ps.active_source); + + // Check playlist state to detect transitions between items + bool media_playlist_active = (media_ps.playlist_index < media_ps.playlist.size()) || + (media_ps.repeat_mode != REPEAT_OFF && !media_ps.playlist.empty()); + + this->state = this->get_media_pipeline_state_(media_ps.active_source, media_playlist_active, old_state); if (this->state != old_state) { this->publish_state(); @@ -349,9 +289,9 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin // Only send END command once per source - check if we've already asked this source to stop if (ps.stopping_source != active_source) { ESP_LOGV(TAG, "Pipeline %u: stopping active source", pipeline); + ps.stopping_source = active_source; active_source->handle_command(media_source::MediaSourceCommand::STOP); ps.speaker->stop(); - ps.stopping_source = active_source; } return false; // Leave in queue, retry next loop } @@ -363,9 +303,9 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin // Only send STOP command once per source if (ps.stopping_source != target_source) { ESP_LOGV(TAG, "Pipeline %u: target source busy, stopping", pipeline); + ps.stopping_source = target_source; target_source->handle_command(media_source::MediaSourceCommand::STOP); ps.speaker->stop(); - ps.stopping_source = target_source; } return false; // Leave in queue, retry next loop } @@ -385,6 +325,7 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin if (!target_source->play_uri(uri)) { ESP_LOGE(TAG, "Pipeline %u: Failed to play URI: %s", pipeline, uri.c_str()); ps.pending_source = nullptr; + this->queue_command_(MediaPlayerControlCommand::PLAYLIST_ADVANCE, pipeline); } // Reset pending frame counter for this pipeline since we're starting a new source @@ -393,6 +334,280 @@ bool SpeakerSourceMediaPlayer::try_execute_play_uri_(const std::string &uri, uin return true; // Remove from queue } +// THREAD CONTEXT: Called from main loop (process_control_queue_, queue_play_current_, handle_media_state_changed_) +void SpeakerSourceMediaPlayer::queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline) { + MediaPlayerControlCommand cmd{}; + cmd.type = type; + cmd.pipeline = pipeline; + if (xQueueSend(this->media_control_command_queue_, &cmd, 0) != pdTRUE) { + ESP_LOGE(TAG, "Queue full, command dropped"); + } +} + +// THREAD CONTEXT: Called from main loop via automation commands (direct) +void SpeakerSourceMediaPlayer::set_playlist_delay_ms(uint8_t pipeline, uint32_t delay_ms) { + if (pipeline < this->pipelines_.size()) { + this->pipelines_[pipeline].playlist_delay_ms = delay_ms; + } +} + +// THREAD CONTEXT: Called from main loop (process_control_queue_). +// The timeout callback also runs on the main loop. +void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t delay_ms) { + if (delay_ms > 0) { + this->set_timeout(PipelineContext::TIMEOUT_IDS[pipeline], delay_ms, + [this, pipeline]() { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); }); + } else { + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } +} + +// THREAD CONTEXT: Called from main loop (loop) +void SpeakerSourceMediaPlayer::process_control_queue_() { + MediaPlayerControlCommand control_command; + + // Use peek to check command without removing it + if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { + return; + } + + bool command_executed = false; + uint8_t pipeline = control_command.pipeline; + + // Get pipeline state + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *active_source = ps.active_source; + + switch (control_command.type) { + case MediaPlayerControlCommand::PLAY_URI: { + // Always use our local playlist to start playback + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + ps.playlist.clear(); + ps.playlist_index = 0; // Reset index + ps.playlist.push_back(*control_command.data.uri); + + // Queue PLAY_CURRENT to initiate playback + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + command_executed = true; + break; + } + + case MediaPlayerControlCommand::ENQUEUE_URI: { + // Always add to our local playlist + ps.playlist.push_back(*control_command.data.uri); + + // If nothing is playing and no upcoming items are queued, start the new item. + bool nothing_playing = + (active_source == nullptr) || (active_source->get_state() == media_source::MediaSourceState::IDLE); + if (nothing_playing && ps.playlist_index >= ps.playlist.size() - 1) { + ps.playlist_index = ps.playlist.size() - 1; // Point to newly added item + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + command_executed = true; + break; + } + + case MediaPlayerControlCommand::PLAYLIST_ADVANCE: { + // Internal message: a track finished, advance to next + if (ps.repeat_mode != REPEAT_ONE) { + ps.playlist_index++; + } + + // Check if we should continue playback + if (ps.playlist_index < ps.playlist.size()) { + this->queue_play_current_(pipeline, ps.playlist_delay_ms); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = 0; + this->queue_play_current_(pipeline, ps.playlist_delay_ms); + } + command_executed = true; + break; + } + + case MediaPlayerControlCommand::PLAY_CURRENT: { + // Play the item at current playlist index + if (ps.playlist_index < ps.playlist.size()) { + command_executed = this->try_execute_play_uri_(ps.playlist[ps.playlist_index], pipeline); + } else { + command_executed = true; // Index out of bounds or empty playlist + } + break; + } + + case MediaPlayerControlCommand::SEND_COMMAND: { + this->handle_player_command_(control_command.data.command, pipeline); + command_executed = true; + break; + } + } + + // Only remove from queue if successfully executed + if (command_executed) { + xQueueReceive(this->media_control_command_queue_, &control_command, 0); + + // Delete the allocated string for PLAY_URI and ENQUEUE_URI commands + if (control_command.type == MediaPlayerControlCommand::PLAY_URI || + control_command.type == MediaPlayerControlCommand::ENQUEUE_URI) { + delete control_command.data.uri; + } + } +} + +// THREAD CONTEXT: Called from main loop only (via process_control_queue_) +void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerCommand player_command, + uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + media_source::MediaSource *active_source = ps.active_source; + bool has_internal_playlist = (active_source != nullptr) && active_source->has_internal_playlist(); + + // Determine target source: prefer active, fall back to last + media_source::MediaSource *target_source = nullptr; + if (active_source != nullptr) { + target_source = active_source; + } else if (ps.last_source != nullptr) { + target_source = ps.last_source; + } + + switch (player_command) { + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: { + // Convert TOGGLE to PLAY or PAUSE based on current state + if ((active_source != nullptr) && (active_source->get_state() == media_source::MediaSourceState::PLAYING)) { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + } else if (!has_internal_playlist && active_source == nullptr && !ps.playlist.empty()) { + bool last_has_internal_playlist = (ps.last_source != nullptr) && ps.last_source->has_internal_playlist(); + if (last_has_internal_playlist) { + ps.last_source->handle_command(media_source::MediaSourceCommand::PLAY); + } else { + if (ps.playlist_index >= ps.playlist.size()) { + ps.playlist_index = 0; + } + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PLAY: { + if (!has_internal_playlist && active_source == nullptr && !ps.playlist.empty()) { + bool last_has_internal_playlist = (ps.last_source != nullptr) && ps.last_source->has_internal_playlist(); + if (last_has_internal_playlist) { + ps.last_source->handle_command(media_source::MediaSourceCommand::PLAY); + } else { + if (ps.playlist_index >= ps.playlist.size()) { + ps.playlist_index = 0; + } + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PLAY); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: { + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PAUSE); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_STOP: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + ps.playlist.clear(); + ps.playlist_index = 0; + } + if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::STOP); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_NEXT: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index + 1 < ps.playlist.size()) { + ps.playlist_index++; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = 0; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::NEXT); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index > 0) { + ps.playlist_index--; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + ps.playlist_index = ps.playlist.size() - 1; + this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::PREVIOUS); + } + break; + } + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_ONE; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_ONE); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_OFF; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_OFF); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ALL: + if (!has_internal_playlist) { + ps.repeat_mode = REPEAT_ALL; + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::REPEAT_ALL); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: { + if (!has_internal_playlist) { + this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + if (ps.playlist_index < ps.playlist.size()) { + ps.playlist[0] = std::move(ps.playlist[ps.playlist_index]); + ps.playlist.resize(1); + ps.playlist_index = 0; + } else { + ps.playlist.clear(); + ps.playlist_index = 0; + } + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::CLEAR_PLAYLIST); + } + break; + } + + default: + // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL), SHUFFLE/UNSHUFFLE (PR3) are no-ops + break; + } +} + // THREAD CONTEXT: Called from main loop only. Entry points: // - HA/automation commands (direct) // - handle_play_uri_request_() via make_call().perform() (deferred from source tasks) @@ -406,10 +621,17 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call auto media_url = call.get_media_url(); if (media_url.has_value()) { - control_command.type = MediaPlayerControlCommand::PLAY_URI; + auto command = call.get_command(); + bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE; + + if (enqueue) { + control_command.type = MediaPlayerControlCommand::ENQUEUE_URI; + } else { + control_command.type = MediaPlayerControlCommand::PLAY_URI; + } // Heap allocation is unavoidable: URIs from Home Assistant are arbitrary-length (media URLs with tokens - // can easily exceed 500 bytes). Deleted after the command is consumed. FreeRTOS queues require items to be - // copyable, so we store a pointer to the string in the queue rather than the string itself. + // can easily exceed 500 bytes). Deleted in process_control_queue_() after the command is consumed. FreeRTOS queues + // require items to be copyable, so we store a pointer to the string in the queue rather than the string itself. control_command.data.uri = new std::string(media_url.value()); if (xQueueSend(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { delete control_command.data.uri; @@ -454,6 +676,9 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call } media_player::MediaPlayerTraits SpeakerSourceMediaPlayer::get_traits() { + // This media player supports more traits like playlists, repeat, and shuffle, but the ESPHome API currently (March + // 2026) doesn't support those commands, so we only report pause support for now since that's used by the frontend and + // supported by our player. auto traits = media_player::MediaPlayerTraits(); traits.set_supports_pause(true); diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 7896fef295a..09670845047 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -29,7 +29,8 @@ namespace esphome::speaker_source { // - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), // handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), // set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), -// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_() +// find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_(), +// process_control_queue_(), handle_player_command_(), queue_command_(), queue_play_current_() // // - Media source task(s): handle_media_output_() via SourceBinding::write_audio(). // Called from each source's decode task thread when streaming audio data. @@ -49,13 +50,19 @@ namespace esphome::speaker_source { // - defer(): SourceBinding::request_volume/request_mute/request_play_uri -> main loop // - Atomic fields (active_source, pending_frames): shared between all three thread contexts // -// Non-atomic pipeline fields (last_source, stopping_source, pending_source) are only accessed -// from the main loop thread. +// Non-atomic pipeline fields (last_source, stopping_source, pending_source, playlist, +// playlist_index, repeat_mode) are only accessed from the main loop thread. enum Pipeline : uint8_t { MEDIA_PIPELINE = 0, }; +enum RepeatMode : uint8_t { + REPEAT_OFF = 0, + REPEAT_ONE = 1, + REPEAT_ALL = 2, +}; + // Forward declaration class SpeakerSourceMediaPlayer; @@ -79,6 +86,9 @@ struct SourceBinding : public media_source::MediaSourceListener { }; struct PipelineContext { + /// @brief Timeout IDs for playlist delay, indexed by Pipeline enum + static constexpr const char *const TIMEOUT_IDS[] = {"next_media"}; + speaker::Speaker *speaker{nullptr}; optional<media_player::MediaPlayerSupportedFormat> format; @@ -92,6 +102,14 @@ struct PipelineContext { // Uses std::vector because the count varies across instances (multiple speaker_source media players may exist). std::vector<std::unique_ptr<SourceBinding>> sources; + // Dynamic allocation is unavoidable here: URIs from Home Assistant are arbitrary-length strings + // (media URLs with tokens can easily exceed 500 bytes), and playlist size is unbounded. + // Pre-allocating fixed buffers would waste significant RAM when idle without covering worst cases. + std::vector<std::string> playlist; + size_t playlist_index{0}; + RepeatMode repeat_mode{REPEAT_OFF}; + uint32_t playlist_delay_ms{0}; + // Track frames sent to speaker to correlate with playback callbacks. // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback // callback. @@ -103,14 +121,17 @@ struct PipelineContext { struct MediaPlayerControlCommand { enum Type : uint8_t { - PLAY_URI, // Find a source that can handle this URI and play it - SEND_COMMAND, // Send command to active source + PLAY_URI, // Clear playlist, reset index, add URI, queue PLAY_CURRENT + ENQUEUE_URI, // Add URI to playlist, queue PLAY_CURRENT if idle + PLAYLIST_ADVANCE, // Advance index (or wrap for repeat_all), queue PLAY_CURRENT if more items + PLAY_CURRENT, // Play item at current playlist index (can retry if speaker not ready) + SEND_COMMAND, // Send command to active source }; Type type; uint8_t pipeline; union { - std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI) + std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI and ENQUEUE_URI) media_player::MediaPlayerCommand command; } data; }; @@ -154,6 +175,8 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } Trigger<float> *get_volume_trigger() { return &this->volume_trigger_; } + void set_playlist_delay_ms(uint8_t pipeline, uint32_t delay_ms); + protected: // Callbacks from source bindings (pipeline index is captured at binding creation time) size_t handle_media_output_(uint8_t pipeline, media_source::MediaSource *source, const uint8_t *data, size_t length, @@ -183,11 +206,20 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla /// @brief Determine media player state from the media pipeline's active source /// @param media_source Active source for the media pipeline (may be nullptr) + /// @param playlist_active Whether the media pipeline's playlist is in progress + /// @param old_state Previous media player state (used for transition smoothing) /// @return The appropriate MediaPlayerState - media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source) const; + media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source, + bool playlist_active, + media_player::MediaPlayerState old_state) const; + void process_control_queue_(); + void handle_player_command_(media_player::MediaPlayerCommand player_command, uint8_t pipeline); bool try_execute_play_uri_(const std::string &uri, uint8_t pipeline); media_source::MediaSource *find_source_for_uri_(const std::string &uri, uint8_t pipeline); + void queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline); + void queue_play_current_(uint8_t pipeline, uint32_t delay_ms = 0); + QueueHandle_t media_control_command_queue_; // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index cfcb065f57c..9e4c309c063 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -41,3 +41,8 @@ media_player: on_unmute: - media_player.play: id: media_player_id + on_volume: + - speaker_source.set_playlist_delay: + id: media_player_id + pipeline: media + delay: 500ms From 3d4ebe74ce2dc7948d2163cdf11f7f89be3c6dec Mon Sep 17 00:00:00 2001 From: Big Mike <mikelawrence@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:00:42 -0500 Subject: [PATCH 0561/2056] [sensirion_common] Use SmallBufferWithHeapFallback helper (#14270) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../sensirion_common/i2c_sensirion.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 0279e08b9ff..6e244faf596 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -12,24 +12,25 @@ static const char *const TAG = "sensirion_i2c"; static const size_t BUFFER_STACK_SIZE = 16; bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { - const size_t num_bytes = len * 3; - uint8_t buf[num_bytes]; + const size_t required_buffer_len = len * 3; + SmallBufferWithHeapFallback<BUFFER_STACK_SIZE> buffer(required_buffer_len); + uint8_t *temp = buffer.get(); - this->last_error_ = this->read(buf, num_bytes); + this->last_error_ = this->read(temp, required_buffer_len); if (this->last_error_ != i2c::ERROR_OK) { return false; } - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; + for (size_t i = 0; i < len; i++) { + const size_t j = i * 3; // Use MSB first since Sensirion devices use CRC-8 with MSB first - uint8_t crc = crc8(&buf[j], 2, 0xFF, CRC_POLYNOMIAL, true); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC invalid @ %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + uint8_t crc = crc8(&temp[j], 2, 0xFF, CRC_POLYNOMIAL, true); + if (crc != temp[j + 2]) { + ESP_LOGE(TAG, "CRC invalid @ %zu! 0x%02X != 0x%02X", i, temp[j + 2], crc); this->last_error_ = i2c::ERROR_CRC; return false; } - data[i] = encode_uint16(buf[j], buf[j + 1]); + data[i] = encode_uint16(temp[j], temp[j + 1]); } return true; } From b27165a8427cb45044f7795cb46a3d6d2835cb52 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 13:11:00 -0500 Subject: [PATCH 0562/2056] [speaker_source] Add shuffle support (#14653) --- .../speaker_source_media_player.cpp | 90 +++++++++++++++++-- .../speaker_source_media_player.h | 13 +++ tests/components/speaker_source/common.yaml | 4 +- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 3724e206673..cf7169df6a9 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -5,6 +5,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <algorithm> + namespace esphome::speaker_source { static constexpr uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20; @@ -383,7 +385,8 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { // Always use our local playlist to start playback this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); ps.playlist.clear(); - ps.playlist_index = 0; // Reset index + ps.shuffle_indices.clear(); // Clear shuffle when starting fresh playlist + ps.playlist_index = 0; // Reset index ps.playlist.push_back(*control_command.data.uri); // Queue PLAY_CURRENT to initiate playback @@ -396,6 +399,11 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { // Always add to our local playlist ps.playlist.push_back(*control_command.data.uri); + // If shuffle is active, add the new item to the end of the shuffle order + if (!ps.shuffle_indices.empty()) { + ps.shuffle_indices.push_back(ps.playlist.size() - 1); + } + // If nothing is playing and no upcoming items are queued, start the new item. bool nothing_playing = (active_source == nullptr) || (active_source->get_state() == media_source::MediaSourceState::IDLE); @@ -425,9 +433,10 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { } case MediaPlayerControlCommand::PLAY_CURRENT: { - // Play the item at current playlist index + // Play the item at current playlist index (mapped through shuffle if active) if (ps.playlist_index < ps.playlist.size()) { - command_executed = this->try_execute_play_uri_(ps.playlist[ps.playlist_index], pipeline); + size_t actual_position = this->get_playlist_position_(pipeline); + command_executed = this->try_execute_play_uri_(ps.playlist[actual_position], pipeline); } else { command_executed = true; // Index out of bounds or empty playlist } @@ -521,6 +530,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC if (!has_internal_playlist) { this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); ps.playlist.clear(); + ps.shuffle_indices.clear(); ps.playlist_index = 0; } if (target_source != nullptr) { @@ -589,21 +599,39 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC if (!has_internal_playlist) { this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); if (ps.playlist_index < ps.playlist.size()) { - ps.playlist[0] = std::move(ps.playlist[ps.playlist_index]); + size_t actual_position = this->get_playlist_position_(pipeline); + ps.playlist[0] = std::move(ps.playlist[actual_position]); ps.playlist.resize(1); ps.playlist_index = 0; } else { ps.playlist.clear(); ps.playlist_index = 0; } + ps.shuffle_indices.clear(); } else if (target_source != nullptr) { target_source->handle_command(media_source::MediaSourceCommand::CLEAR_PLAYLIST); } break; } + case media_player::MEDIA_PLAYER_COMMAND_SHUFFLE: + if (!has_internal_playlist) { + this->shuffle_playlist_(pipeline); + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::SHUFFLE); + } + break; + + case media_player::MEDIA_PLAYER_COMMAND_UNSHUFFLE: + if (!has_internal_playlist) { + this->unshuffle_playlist_(pipeline); + } else if (target_source != nullptr) { + target_source->handle_command(media_source::MediaSourceCommand::UNSHUFFLE); + } + break; + default: - // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL), SHUFFLE/UNSHUFFLE (PR3) are no-ops + // TURN_ON, TURN_OFF, ENQUEUE (handled separately with URL) are no-ops break; } } @@ -766,6 +794,58 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } +size_t SpeakerSourceMediaPlayer::get_playlist_position_(uint8_t pipeline) const { + const PipelineContext &ps = this->pipelines_[pipeline]; + + if (ps.shuffle_indices.empty() || ps.playlist_index >= ps.shuffle_indices.size()) { + return ps.playlist_index; + } + return ps.shuffle_indices[ps.playlist_index]; +} + +void SpeakerSourceMediaPlayer::shuffle_playlist_(uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (ps.playlist.size() <= 1) { + ps.shuffle_indices.clear(); + return; + } + + // Capture current actual position BEFORE modifying shuffle_indices + size_t current_actual = this->get_playlist_position_(pipeline); + + // Build indices vector + ps.shuffle_indices.resize(ps.playlist.size()); + for (size_t i = 0; i < ps.playlist.size(); i++) { + ps.shuffle_indices[i] = i; + } + + // Fisher-Yates shuffle using ESPHome's random helper + for (size_t i = ps.shuffle_indices.size() - 1; i > 0; i--) { + size_t j = random_uint32() % (i + 1); + std::swap(ps.shuffle_indices[i], ps.shuffle_indices[j]); + } + + // Move current track to current position (so playback continues seamlessly) + if (ps.playlist_index < ps.shuffle_indices.size()) { + for (size_t i = 0; i < ps.shuffle_indices.size(); i++) { + if (ps.shuffle_indices[i] == current_actual) { + std::swap(ps.shuffle_indices[i], ps.shuffle_indices[ps.playlist_index]); + break; + } + } + } +} + +void SpeakerSourceMediaPlayer::unshuffle_playlist_(uint8_t pipeline) { + PipelineContext &ps = this->pipelines_[pipeline]; + + if (!ps.shuffle_indices.empty() && ps.playlist_index < ps.shuffle_indices.size()) { + ps.playlist_index = ps.shuffle_indices[ps.playlist_index]; + } + ps.shuffle_indices.clear(); +} + } // namespace esphome::speaker_source #endif // USE_ESP32 diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 09670845047..4fbb534110f 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -110,6 +110,10 @@ struct PipelineContext { RepeatMode repeat_mode{REPEAT_OFF}; uint32_t playlist_delay_ms{0}; + // When non-empty, playlist_index indexes into these vectors + // which contain the actual playlist indices in shuffled order + std::vector<size_t> shuffle_indices; + // Track frames sent to speaker to correlate with playback callbacks. // Atomic because it is written from the main loop/source tasks and read/decremented from the speaker playback // callback. @@ -220,6 +224,15 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla void queue_command_(MediaPlayerControlCommand::Type type, uint8_t pipeline); void queue_play_current_(uint8_t pipeline, uint32_t delay_ms = 0); + /// @brief Maps playlist_index through shuffle indices if shuffle is active + size_t get_playlist_position_(uint8_t pipeline) const; + + /// @brief Generates shuffled indices for the playlist, keeping current track at current position + void shuffle_playlist_(uint8_t pipeline); + + /// @brief Clears shuffle indices and adjusts playlist_index to maintain current track + void unshuffle_playlist_(uint8_t pipeline); + QueueHandle_t media_control_command_queue_; // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index 9e4c309c063..05b5181dfd4 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -36,10 +36,10 @@ media_player: sources: - audio_file_source on_mute: - - media_player.pause: + - media_player.shuffle: id: media_player_id on_unmute: - - media_player.play: + - media_player.unshuffle: id: media_player_id on_volume: - speaker_source.set_playlist_delay: From 03c0ce704b0c89a87f1fe2b67dbbcb6a74eb9688 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:22:48 -0400 Subject: [PATCH 0563/2056] Bump pyupgrade to v3.21.2 for Python 3.14 compatibility (#14699) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b036da6ef16..2d8f6983959 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py311-plus] From 04bcd9f56ba24d40199f8f4cadf7761429bc942c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:25:36 -0400 Subject: [PATCH 0564/2056] [dashboard] Use sys.executable for dashboard subprocess commands (#14698) Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/__main__.py | 13 +++++++------ esphome/dashboard/const.py | 5 ++++- esphome/dashboard/web_server.py | 6 +++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 58b995e8df4..4b0fc2cec7f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -74,6 +74,8 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +ESPHOME_COMMAND = [sys.executable, "-m", "esphome"] + # Maximum buffer size for serial log reading to prevent unbounded memory growth SERIAL_BUFFER_MAX_SIZE = 65536 @@ -1307,9 +1309,8 @@ def command_update_all(args: ArgsProtocol) -> int | None: files = list_yaml_files(args.configuration) def build_command(f): - if CORE.dashboard: - return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"] - return ["esphome", "run", f, "--no-logs", "--device", "OTA"] + dashboard = ["--dashboard"] if CORE.dashboard else [] + return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"] return run_multiple_configs(files, build_command) @@ -1458,7 +1459,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: new_path.write_text(new_raw, encoding="utf-8") - rc = run_external_process("esphome", "config", str(new_path)) + rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path)) if rc != 0: print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) new_path.unlink() @@ -1476,7 +1477,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: cli_args.insert(0, "--dashboard") try: - rc = run_external_process("esphome", *cli_args) + rc = run_external_process(*ESPHOME_COMMAND, *cli_args) except KeyboardInterrupt: rc = 1 if rc != 0: @@ -1873,7 +1874,7 @@ def run_esphome(argv): # argv[0] is the program path, skip it since we prefix with "esphome" def build_command(f): return ( - ["esphome"] + [*ESPHOME_COMMAND] + [arg for arg in argv[1:] if arg not in args.configuration] + [str(f)] ) diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index ada5575d0e1..9cadc442ef2 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + from esphome.enum import StrEnum @@ -26,4 +28,5 @@ MAX_EXECUTOR_WORKERS = 48 SENTINEL = object() -DASHBOARD_COMMAND = ["esphome", "--dashboard"] +ESPHOME_COMMAND = [sys.executable, "-m", "esphome"] +DASHBOARD_COMMAND = [*ESPHOME_COMMAND, "--dashboard"] diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 92cab929ef4..b8e17244e53 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -52,7 +52,7 @@ from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader from ..helpers import write_file -from .const import DASHBOARD_COMMAND, DashboardEvent +from .const import DASHBOARD_COMMAND, ESPHOME_COMMAND, DashboardEvent from .core import DASHBOARD, ESPHomeDashboard, Event from .entries import UNKNOWN_STATE, DashboardEntry, entry_state_to_bool from .models import build_device_list_response @@ -1079,7 +1079,7 @@ class DownloadBinaryRequestHandler(BaseHandler): return if not path.is_file(): - args = ["esphome", "idedata", settings.rel_path(configuration)] + args = [*ESPHOME_COMMAND, "idedata", settings.rel_path(configuration)] rc, stdout, _ = await async_run_system_command(args) if rc != 0: @@ -1462,7 +1462,7 @@ class JsonConfigRequestHandler(BaseHandler): self.send_error(404) return - args = ["esphome", "config", str(filename), "--show-secrets"] + args = [*ESPHOME_COMMAND, "config", str(filename), "--show-secrets"] rc, stdout, stderr = await async_run_system_command(args) From bef5e4de9c0a89f319c9f7124814cf7390fb922e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 11 Mar 2026 13:29:17 -0500 Subject: [PATCH 0565/2056] [speaker_source] Add announcement pipeline (#14654) --- .../components/speaker_source/media_player.py | 125 +++++++++++++----- .../speaker_source_media_player.cpp | 74 +++++++++-- .../speaker_source_media_player.h | 27 ++-- tests/components/speaker_source/common.yaml | 21 ++- 4 files changed, 186 insertions(+), 61 deletions(-) diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index 9080bebcae0..fe50536e8fd 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -20,6 +20,7 @@ DEPENDENCIES = ["media_source", "speaker"] CODEOWNERS = ["@kahrendt"] +CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" CONF_MEDIA_PIPELINE = "media_pipeline" CONF_ON_MUTE = "on_mute" CONF_PIPELINE = "pipeline" @@ -42,6 +43,19 @@ PipelineContext = speaker_source_ns.struct("PipelineContext") Pipeline = speaker_source_ns.enum("Pipeline") PIPELINE_ENUM = { "media": Pipeline.MEDIA_PIPELINE, + "announcement": Pipeline.ANNOUNCEMENT_PIPELINE, +} + +# Maps config key -> (C++ Pipeline enum value, format purpose) +_PIPELINE_INFO = { + CONF_MEDIA_PIPELINE: ( + Pipeline.MEDIA_PIPELINE, + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], + ), + CONF_ANNOUNCEMENT_PIPELINE: ( + Pipeline.ANNOUNCEMENT_PIPELINE, + media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], + ), } SetPlaylistDelayAction = speaker_source_ns.class_( @@ -59,7 +73,7 @@ FORMAT_MAPPING = { # Returns a media_player.MediaPlayerSupportedFormat struct with the configured # format, sample rate, number of channels, purpose, and bytes per sample -def _get_supported_format_struct(pipeline: ConfigType): +def _get_supported_format_struct(pipeline: ConfigType, purpose: MockObj): args = [ media_player.MediaPlayerSupportedFormat, ] @@ -68,7 +82,7 @@ def _get_supported_format_struct(pipeline: ConfigType): args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) - args.append(("purpose", media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"])) + args.append(("purpose", purpose)) # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails # if the number of bytes per sample is specified for MP3. @@ -115,6 +129,40 @@ PIPELINE_SCHEMA = cv.Schema( ) +def _validate_no_shared_resources(config: ConfigType) -> ConfigType: + announcement_config = config.get(CONF_ANNOUNCEMENT_PIPELINE) + media_config = config.get(CONF_MEDIA_PIPELINE) + + # Check for duplicates within each pipeline + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline_config := config.get(pipeline_key): + source_ids = [s.id for s in pipeline_config[CONF_SOURCES]] + if len(source_ids) != len(set(source_ids)): + raise cv.Invalid( + f"Duplicate media sources in {pipeline_key}. " + "Each media source can only appear once per pipeline." + ) + + # Check for sources shared between pipelines + if announcement_config and media_config: + if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]: + raise cv.Invalid( + "The announcement and media pipelines cannot use the same speaker. " + "Use the `mixer` speaker component to create two source speakers." + ) + + announcement_source_ids = {s.id for s in announcement_config[CONF_SOURCES]} + media_source_ids = {s.id for s in media_config[CONF_SOURCES]} + shared = announcement_source_ids & media_source_ids + if shared: + raise cv.Invalid( + f"Media sources cannot be shared between pipelines: {', '.join(shared)}. " + "Create separate media source instances for each pipeline." + ) + + return config + + def _validate_volume_settings(config: ConfigType) -> ConfigType: # CONF_VOLUME_INITIAL is in the scaled volume domain (0.0-1.0) and doesn't need to be validated if config[CONF_VOLUME_MIN] > config[CONF_VOLUME_MAX]: @@ -131,7 +179,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage, - cv.Required(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, + cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True), cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True), cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), @@ -140,23 +189,37 @@ CONFIG_SCHEMA = cv.All( .extend(cv.COMPONENT_SCHEMA) .extend(media_player.media_player_schema(SpeakerSourceMediaPlayer)), cv.only_on_esp32, + cv.has_at_least_one_key(CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE), + _validate_no_shared_resources, _validate_volume_settings, ) def _final_validate_codecs(config: ConfigType) -> ConfigType: - pipeline = config[CONF_MEDIA_PIPELINE] - fmt = pipeline[CONF_FORMAT] - if fmt == "NONE": + # "NONE" means the pipeline accepts any format at runtime, so all optional codecs must be available. + # When a specific format is set, only that codec is requested. + needed_formats: set[str] = set() + need_all = False + + for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): + if pipeline := config.get(pipeline_key): + fmt = pipeline[CONF_FORMAT] + if fmt == "NONE": + need_all = True + else: + needed_formats.add(fmt) + + if need_all: audio.request_flac_support() audio.request_mp3_support() audio.request_opus_support() - elif fmt == "FLAC": - audio.request_flac_support() - elif fmt == "MP3": - audio.request_mp3_support() - elif fmt == "OPUS": - audio.request_opus_support() + else: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() return config @@ -164,7 +227,8 @@ def _final_validate_codecs(config: ConfigType) -> ConfigType: FINAL_VALIDATE_SCHEMA = cv.All( cv.Schema( { - cv.Required(CONF_MEDIA_PIPELINE): _validate_pipeline, + cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): _validate_pipeline, + cv.Optional(CONF_MEDIA_PIPELINE): _validate_pipeline, }, extra=cv.ALLOW_EXTRA, ), @@ -182,26 +246,25 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_volume_max(config[CONF_VOLUME_MAX])) cg.add(var.set_volume_min(config[CONF_VOLUME_MIN])) - pipeline_config = config[CONF_MEDIA_PIPELINE] - pipeline_enum = Pipeline.MEDIA_PIPELINE + for pipeline_key, (pipeline_enum, purpose) in _PIPELINE_INFO.items(): + if pipeline_config := config.get(pipeline_key): + for source in pipeline_config[CONF_SOURCES]: + src = await cg.get_variable(source) + cg.add(var.add_media_source(pipeline_enum, src)) - for source in pipeline_config[CONF_SOURCES]: - src = await cg.get_variable(source) - cg.add(var.add_media_source(pipeline_enum, src)) - - cg.add( - var.set_speaker( - pipeline_enum, - await cg.get_variable(pipeline_config[CONF_SPEAKER]), - ) - ) - if pipeline_config[CONF_FORMAT] != "NONE": - cg.add( - var.set_format( - pipeline_enum, - _get_supported_format_struct(pipeline_config), + cg.add( + var.set_speaker( + pipeline_enum, + await cg.get_variable(pipeline_config[CONF_SPEAKER]), + ) ) - ) + if pipeline_config[CONF_FORMAT] != "NONE": + cg.add( + var.set_format( + pipeline_enum, + _get_supported_format_struct(pipeline_config, purpose), + ) + ) if on_mute := config.get(CONF_ON_MUTE): await automation.build_automation( diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index cf7169df6a9..9d02ac9c744 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -128,6 +128,7 @@ void SpeakerSourceMediaPlayer::handle_play_uri_request_(uint8_t pipeline, const // Smart source is requesting the player to play a different URI auto call = this->make_call(); call.set_media_url(uri); + call.set_announcement(pipeline == ANNOUNCEMENT_PIPELINE); call.perform(); } @@ -209,7 +210,7 @@ size_t SpeakerSourceMediaPlayer::handle_media_output_(uint8_t pipeline, media_so } // THREAD CONTEXT: Called from main loop (loop) -media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_state_( +media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_source_state_( media_source::MediaSource *source, bool playlist_active, media_player::MediaPlayerState old_state) const { if (source != nullptr) { switch (source->get_state()) { @@ -218,7 +219,7 @@ media_player::MediaPlayerState SpeakerSourceMediaPlayer::get_media_pipeline_stat case media_source::MediaSourceState::PAUSED: return media_player::MEDIA_PLAYER_STATE_PAUSED; case media_source::MediaSourceState::ERROR: - ESP_LOGE(TAG, "Source error"); + ESP_LOGE(TAG, "Media source error"); return media_player::MEDIA_PLAYER_STATE_IDLE; case media_source::MediaSourceState::IDLE: default: @@ -237,16 +238,47 @@ void SpeakerSourceMediaPlayer::loop() { // Process queued control commands this->process_control_queue_(); - // Update state based on active sources + // Update state based on active sources - announcement pipeline takes priority media_player::MediaPlayerState old_state = this->state; + PipelineContext &ann_ps = this->pipelines_[ANNOUNCEMENT_PIPELINE]; PipelineContext &media_ps = this->pipelines_[MEDIA_PIPELINE]; // Check playlist state to detect transitions between items + bool announcement_playlist_active = (ann_ps.playlist_index < ann_ps.playlist.size()) || + (ann_ps.repeat_mode != REPEAT_OFF && !ann_ps.playlist.empty()); bool media_playlist_active = (media_ps.playlist_index < media_ps.playlist.size()) || (media_ps.repeat_mode != REPEAT_OFF && !media_ps.playlist.empty()); - this->state = this->get_media_pipeline_state_(media_ps.active_source, media_playlist_active, old_state); + // Check announcement pipeline first + media_source::MediaSource *announcement_source = ann_ps.active_source; + if (announcement_source != nullptr) { + media_source::MediaSourceState announcement_state = announcement_source->get_state(); + if (announcement_state != media_source::MediaSourceState::IDLE) { + // Announcement is active - announcements take priority and never report PAUSED + switch (announcement_state) { + case media_source::MediaSourceState::PLAYING: + case media_source::MediaSourceState::PAUSED: // Treat paused announcements as announcing + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + break; + case media_source::MediaSourceState::ERROR: + ESP_LOGE(TAG, "Announcement source error"); + // Fall through to media pipeline state + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + break; + default: + break; + } + } else { + // Announcement source is idle, fall through to media pipeline + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + } + } else if (announcement_playlist_active && old_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) { + this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; + } else { + // No active announcement, check media pipeline + this->state = this->get_source_state_(media_ps.active_source, media_playlist_active, old_state); + } if (this->state != old_state) { this->publish_state(); @@ -357,7 +389,7 @@ void SpeakerSourceMediaPlayer::set_playlist_delay_ms(uint8_t pipeline, uint32_t // The timeout callback also runs on the main loop. void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t delay_ms) { if (delay_ms > 0) { - this->set_timeout(PipelineContext::TIMEOUT_IDS[pipeline], delay_ms, + this->set_timeout(PIPELINE_TIMEOUT_IDS[pipeline], delay_ms, [this, pipeline]() { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); }); } else { this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -366,7 +398,7 @@ void SpeakerSourceMediaPlayer::queue_play_current_(uint8_t pipeline, uint32_t de // THREAD CONTEXT: Called from main loop (loop) void SpeakerSourceMediaPlayer::process_control_queue_() { - MediaPlayerControlCommand control_command; + MediaPlayerControlCommand control_command{}; // Use peek to check command without removing it if (xQueuePeek(this->media_control_command_queue_, &control_command, 0) != pdTRUE) { @@ -383,7 +415,7 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { switch (control_command.type) { case MediaPlayerControlCommand::PLAY_URI: { // Always use our local playlist to start playback - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); ps.playlist.clear(); ps.shuffle_indices.clear(); // Clear shuffle when starting fresh playlist ps.playlist_index = 0; // Reset index @@ -528,7 +560,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_STOP: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); ps.playlist.clear(); ps.shuffle_indices.clear(); ps.playlist_index = 0; @@ -541,7 +573,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_NEXT: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index + 1 < ps.playlist.size()) { ps.playlist_index++; this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -557,7 +589,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index > 0) { ps.playlist_index--; this->queue_command_(MediaPlayerControlCommand::PLAY_CURRENT, pipeline); @@ -597,7 +629,7 @@ void SpeakerSourceMediaPlayer::handle_player_command_(media_player::MediaPlayerC case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: { if (!has_internal_playlist) { - this->cancel_timeout(PipelineContext::TIMEOUT_IDS[pipeline]); + this->cancel_timeout(PIPELINE_TIMEOUT_IDS[pipeline]); if (ps.playlist_index < ps.playlist.size()) { size_t actual_position = this->get_playlist_position_(pipeline); ps.playlist[0] = std::move(ps.playlist[actual_position]); @@ -644,8 +676,24 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call return; } - MediaPlayerControlCommand control_command; - control_command.pipeline = MEDIA_PIPELINE; + MediaPlayerControlCommand control_command{}; + + // Determine which pipeline to use based on announcement flag, falling back if the preferred pipeline + // is not configured + auto announcement = call.get_announcement(); + if (announcement.has_value() && announcement.value()) { + if (this->pipelines_[ANNOUNCEMENT_PIPELINE].is_configured()) { + control_command.pipeline = ANNOUNCEMENT_PIPELINE; + } else { + control_command.pipeline = MEDIA_PIPELINE; + } + } else { + if (this->pipelines_[MEDIA_PIPELINE].is_configured()) { + control_command.pipeline = MEDIA_PIPELINE; + } else { + control_command.pipeline = ANNOUNCEMENT_PIPELINE; + } + } auto media_url = call.get_media_url(); if (media_url.has_value()) { diff --git a/esphome/components/speaker_source/speaker_source_media_player.h b/esphome/components/speaker_source/speaker_source_media_player.h index 4fbb534110f..652390edd22 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.h +++ b/esphome/components/speaker_source/speaker_source_media_player.h @@ -28,7 +28,7 @@ namespace esphome::speaker_source { // // - Main loop task: setup(), loop(), dump_config(), handle_media_state_changed_(), // handle_volume_request_(), handle_mute_request_(), handle_play_uri_request_(), -// set_volume_(), set_mute_state_(), control(), get_media_pipeline_state_(), +// set_volume_(), set_mute_state_(), control(), get_source_state_(), // find_source_for_uri_(), try_execute_play_uri_(), save_volume_restore_state_(), // process_control_queue_(), handle_player_command_(), queue_command_(), queue_play_current_() // @@ -55,6 +55,7 @@ namespace esphome::speaker_source { enum Pipeline : uint8_t { MEDIA_PIPELINE = 0, + ANNOUNCEMENT_PIPELINE = 1, }; enum RepeatMode : uint8_t { @@ -85,10 +86,10 @@ struct SourceBinding : public media_source::MediaSourceListener { void request_play_uri(const std::string &uri) override; }; -struct PipelineContext { - /// @brief Timeout IDs for playlist delay, indexed by Pipeline enum - static constexpr const char *const TIMEOUT_IDS[] = {"next_media"}; +/// @brief Timeout IDs for playlist delay, indexed by Pipeline enum +static constexpr uint32_t PIPELINE_TIMEOUT_IDS[] = {1, 2}; +struct PipelineContext { speaker::Speaker *speaker{nullptr}; optional<media_player::MediaPlayerSupportedFormat> format; @@ -132,7 +133,7 @@ struct MediaPlayerControlCommand { SEND_COMMAND, // Send command to active source }; Type type; - uint8_t pipeline; + uint8_t pipeline; // MEDIA_PIPELINE or ANNOUNCEMENT_PIPELINE union { std::string *uri; // Owned pointer, must delete after xQueueReceive (for PLAY_URI and ENQUEUE_URI) @@ -208,14 +209,13 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla /// @brief Saves the current volume and mute state to the flash for restoration. void save_volume_restore_state_(); - /// @brief Determine media player state from the media pipeline's active source - /// @param media_source Active source for the media pipeline (may be nullptr) - /// @param playlist_active Whether the media pipeline's playlist is in progress + /// @brief Determine media player state from a pipeline's active source + /// @param media_source Active source (may be nullptr) + /// @param playlist_active Whether the pipeline's playlist is in progress /// @param old_state Previous media player state (used for transition smoothing) /// @return The appropriate MediaPlayerState - media_player::MediaPlayerState get_media_pipeline_state_(media_source::MediaSource *media_source, - bool playlist_active, - media_player::MediaPlayerState old_state) const; + media_player::MediaPlayerState get_source_state_(media_source::MediaSource *media_source, bool playlist_active, + media_player::MediaPlayerState old_state) const; void process_control_queue_(); void handle_player_command_(media_player::MediaPlayerCommand player_command, uint8_t pipeline); @@ -235,8 +235,9 @@ class SpeakerSourceMediaPlayer : public Component, public media_player::MediaPla QueueHandle_t media_control_command_queue_; - // Pipeline context for media pipeline. See THREADING MODEL at top of namespace for access rules. - std::array<PipelineContext, 1> pipelines_; + // Pipeline context for media (index 0) and announcement (index 1) pipelines. + // See THREADING MODEL at top of namespace for access rules. + std::array<PipelineContext, 2> pipelines_; // Used to save volume/mute state for restoration on reboot ESPPreferenceObject pref_; diff --git a/tests/components/speaker_source/common.yaml b/tests/components/speaker_source/common.yaml index 05b5181dfd4..7d663b802c6 100644 --- a/tests/components/speaker_source/common.yaml +++ b/tests/components/speaker_source/common.yaml @@ -10,6 +10,11 @@ speaker: i2s_dout_pin: ${i2s_dout_pin} sample_rate: 48000 num_channels: 2 + - platform: mixer + output_speaker: speaker_id + source_speakers: + - id: announcement_mixer_speaker_id + - id: media_mixer_speaker_id audio_file: - id: test_audio @@ -19,7 +24,9 @@ audio_file: media_source: - platform: audio_file - id: audio_file_source + id: announcement_audio_file_source + - platform: audio_file + id: media_audio_file_source media_player: - platform: speaker_source @@ -29,12 +36,18 @@ media_player: volume_initial: 0.75 volume_max: 0.95 volume_min: 0.0 - media_pipeline: - speaker: speaker_id + announcement_pipeline: + speaker: announcement_mixer_speaker_id format: FLAC num_channels: 1 sources: - - audio_file_source + - announcement_audio_file_source + media_pipeline: + speaker: media_mixer_speaker_id + format: FLAC + num_channels: 1 + sources: + - media_audio_file_source on_mute: - media_player.shuffle: id: media_player_id From e7c3277eeb095a5b3e2c88ca4c7631d43528099c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:34:53 +1300 Subject: [PATCH 0566/2056] Bump version to 2026.4.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1a9e0b4e10f..cfdb74bd196 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0-dev +PROJECT_NUMBER = 2026.4.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index d409514f3c7..33a2526d38b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0-dev" +__version__ = "2026.4.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 02f7aee680bb0a7b7a7a54e1b2cd759f8876d77c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:34:53 +1300 Subject: [PATCH 0567/2056] Bump version to 2026.3.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1a9e0b4e10f..9f5cd0a2ce3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0-dev +PROJECT_NUMBER = 2026.3.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index d409514f3c7..eb49c9a1d77 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0-dev" +__version__ = "2026.3.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 928f6f18660af1ffde5a1fc91f02c8a31fd43021 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 08:57:43 -1000 Subject: [PATCH 0568/2056] [ci] Add PR title check for unescaped angle brackets (#14701) --- .github/workflows/pr-title-check.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 198b9a6b25c..2ad023ed1b0 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -65,6 +65,18 @@ jobs: return; } + // Check for angle brackets not wrapped in backticks. + // Astro docs MDX treats bare < as JSX component opening tags. + const stripped = title.replace(/`[^`]*`/g, ''); + if (/[<>]/.test(stripped)) { + core.setFailed( + 'PR title contains `<` or `>` not wrapped in backticks.\n' + + 'Astro docs MDX interprets bare `<` as JSX components.\n' + + 'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support' + ); + return; + } + // Check title starts with [tag] prefix const bracketPattern = /^\[\w+\]/; if (!bracketPattern.test(title)) { From b6ff7185e74da275db2bef375732b26d2e125f20 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:04:07 +1300 Subject: [PATCH 0569/2056] [ci] Dont run codeowners workflows on release or beta PRs (#14703) --- .github/workflows/codeowner-approved-label-update.yml | 3 +++ .github/workflows/codeowner-review-request.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 0bce33ebe29..34ff934b773 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -10,6 +10,9 @@ name: Codeowner Approved Label on: pull_request_target: types: [opened, synchronize, reopened, ready_for_review] + branches-ignore: + - release + - beta permissions: issues: write diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 02bf0e4a29e..a89c03ba042 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -13,6 +13,9 @@ on: # Needs to be pull_request_target to get write permissions pull_request_target: types: [opened, reopened, synchronize, ready_for_review] + branches-ignore: + - release + - beta permissions: pull-requests: write From 73f305ff9c9c94c3ca7e7e6a3f2b8e10749a0147 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:28:19 -1000 Subject: [PATCH 0570/2056] Bump tornado from 6.5.4 to 6.5.5 (#14704) 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 3da2d52b44b..e634bcb1046 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.4 +tornado==6.5.5 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 From a060f175ad04bf0f497a178501ca4df414b002a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:28:46 -1000 Subject: [PATCH 0571/2056] Bump actions/download-artifact from 8.0.0 to 8.0.1 (#14705) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c9e8c58bcc..461e676c4e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -945,13 +945,13 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: memory-analysis-pr path: ./memory-analysis diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f68e9c873f..0ed41d99c7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download digests - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: digests-* path: /tmp/digests From 409640c0ee441d683b2939c2235f2bee184aecaf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:30:44 -0400 Subject: [PATCH 0572/2056] [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 6d49053d6d5..a51ae2cd666 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -105,7 +105,7 @@ async def to_code(config): if framework_ver >= cv.Version(5, 5, 0): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index acd7f7a4798..f7fd3e67bc3 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -20,7 +20,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.0 + version: 2.12.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From ddc40f44fa81f41ad8970e1e607d50d646e8f2cc Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 19:56:25 -0500 Subject: [PATCH 0573/2056] [ethernet] ESP32-P4 Ethernet compilation fix (#14714) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/ethernet/ethernet_component.cpp | 18 +----------------- .../components/ethernet/ethernet_component.h | 2 ++ esphome/components/ethernet/ethernet_helpers.c | 8 ++++++++ .../components/ethernet/test.esp32-p4-idf.yaml | 1 + 4 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 esphome/components/ethernet/ethernet_helpers.c create mode 100644 tests/components/ethernet/test.esp32-p4-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d6b0d40cd95..e0788e11498 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -21,22 +21,6 @@ namespace esphome::ethernet { -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) -// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 -#ifdef USE_ESP32_VARIANT_ESP32P4 -#undef ETH_ESP32_EMAC_DEFAULT_CONFIG -#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ - { \ - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ - .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ - .emac_dataif_gpio = \ - {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ - } -#endif -#endif - static const char *const TAG = "ethernet"; // PHY register size for hex logging @@ -162,7 +146,7 @@ void EthernetComponent::setup() { phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; - eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d9f05be9de0..c464e20b843 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,6 +15,8 @@ #include "esp_mac.h" #include "esp_idf_version.h" +extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); + namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c new file mode 100644 index 00000000000..96faccad24c --- /dev/null +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -0,0 +1,8 @@ +#include "esp_eth_mac_esp.h" + +// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers +// which are valid in C but not in C++. This wrapper allows C++ code to get +// the default config without replicating the macro's contents. +eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { + return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); +} diff --git a/tests/components/ethernet/test.esp32-p4-idf.yaml b/tests/components/ethernet/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..e52329d7ea2 --- /dev/null +++ b/tests/components/ethernet/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ip101.yaml From 8daa946afa37e92500809cd0ad60e1fd429cb3ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 15:00:20 -1000 Subject: [PATCH 0574/2056] [esp32] Add crash handler to capture and report backtrace across reboots (#14709) --- esphome/components/api/api_connection.h | 6 + esphome/components/api/client.py | 21 ++ esphome/components/esp32/__init__.py | 5 + esphome/components/esp32/core.cpp | 6 + esphome/components/esp32/crash_handler.cpp | 355 +++++++++++++++++++++ esphome/components/esp32/crash_handler.h | 18 ++ esphome/components/logger/logger_esp32.cpp | 4 + esphome/core/defines.h | 1 + esphome/platformio_api.py | 7 + tests/unit_tests/test_platformio_api.py | 28 ++ 10 files changed, 451 insertions(+) create mode 100644 esphome/components/esp32/crash_handler.cpp create mode 100644 esphome/components/esp32/crash_handler.h diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3356511684f..60cc3e91b11 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -14,6 +14,9 @@ #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_CRASH_HANDLER +#include "esphome/components/esp32/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -235,6 +238,9 @@ class APIConnection final : public APIServerConnectionBase { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } #ifdef USE_API_HOMEASSISTANT_SERVICES void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 200d0938bd5..0e71ad8fcbf 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime +import importlib import logging from typing import TYPE_CHECKING, Any import warnings @@ -18,6 +19,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE +from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard + backtrace_state = False + + # Try platform-specific stacktrace handler first, fall back to generic + platform_process_stacktrace = None + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + platform_process_stacktrace = getattr(module, "process_stacktrace") + except (AttributeError, ImportError): + pass def on_log(msg: SubscribeLogsResponse) -> None: """Handle a new log message.""" + nonlocal backtrace_state time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") @@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: ) for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + for raw_line in text.splitlines(): + if platform_process_stacktrace: + backtrace_state = platform_process_stacktrace( + config, raw_line, backtrace_state + ) + else: + backtrace_state = process_stacktrace( + config, raw_line, backtrace_state=backtrace_state + ) stop = await async_run(cli, on_log, name=name) try: diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 52e70501dcf..475de6aa3e4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1442,6 +1442,11 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32") cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") + # Arduino already wraps esp_panic_handler for its own backtrace handler, + # so only add our wrap when using ESP-IDF framework to avoid linker conflicts. + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + cg.add_build_flag("-Wl,--wrap=esp_panic_handler") + cg.add_define("USE_ESP32_CRASH_HANDLER") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 46c000562e1..cba25bca2b2 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esphome/core/defines.h" +#include "crash_handler.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -36,6 +37,11 @@ void arch_restart() { } void arch_init() { +#ifdef USE_ESP32_CRASH_HANDLER + // Read crash data from previous boot before anything else + esp32::crash_handler_read_and_clear(); +#endif + // Enable the task watchdog only on the loop task (from which we're currently running) esp_task_wdt_add(nullptr); diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp new file mode 100644 index 00000000000..ecf30d78781 --- /dev/null +++ b/esphome/components/esp32/crash_handler.cpp @@ -0,0 +1,355 @@ +#ifdef USE_ESP32 + +#include "esphome/core/defines.h" +#ifdef USE_ESP32_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> +#include <cstring> +#include <esp_attr.h> +#include <esp_private/panic_internal.h> +#include <soc/soc.h> + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +#include <esp_cpu_utils.h> +#include <esp_debug_helpers.h> +#include <xtensa_context.h> +#elif CONFIG_IDF_TARGET_ARCH_RISCV +#include <riscv/rvruntime-frames.h> +#endif + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +static constexpr size_t MAX_BACKTRACE = 16; + +// Check if an address looks like code (flash-mapped or IRAM). +// Must be safe to call from panic context (no flash access needed). +static inline bool IRAM_ATTR is_code_addr(uint32_t addr) { + return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH); +} + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Check if a code address is a real return address by verifying the preceding +// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during +// panic) so flash cache is available and both IRAM and IROM are safely readable. +static inline bool is_return_addr(uint32_t addr) { + if (!is_code_addr(addr) || addr < 4) + return false; + // A return address on the stack points to the instruction after a call. + // Check for 4-byte JAL/JALR call instruction before this address. + // Use memcpy for alignment safety — RISC-V C extension means code addresses + // are only 2-byte aligned, so addr-4 may not be 4-byte aligned. + uint32_t inst; + memcpy(&inst, (const void *) (addr - 4), sizeof(inst)); + // RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd + uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode + uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7) + // Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7) + if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80) + return true; + // Check for 2-byte compressed c.jalr before this address (C extension). + // c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10 + if (addr >= 2) { + uint16_t c_inst = *(uint16_t *) (addr - 2); + if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0) + return true; + } + return false; +} +#endif + +// Raw crash data written by the panic handler wrapper. +// Lives in .noinit so it survives software reset but contains garbage after power cycle. +// Validated by magic marker. Static linkage since it's only used within this file. +// Version field is first so future firmware can always identify the struct layout. +// Magic is second to validate the data. Remaining fields can change between versions. +// Version is uint32_t because it would be padded to 4 bytes anyway before the next +// uint32_t field, so we use the full width rather than wasting 3 bytes of padding. +static constexpr uint32_t CRASH_DATA_VERSION = 1; +struct RawCrashData { + uint32_t version; + uint32_t magic; + uint32_t pc; + uint8_t backtrace_count; + uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned) + uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG) + uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) +}; +static RawCrashData __attribute__((section(".noinit"))) +s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Whether crash data was found and validated this boot. +static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +namespace esphome::esp32 { + +static const char *const TAG = "esp32.crash"; + +void crash_handler_read_and_clear() { + if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) { + s_crash_data_valid = true; + // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data + if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count) + s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count; + if (s_raw_crash_data.exception > 4) // panic_exception_t max value + s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT + if (s_raw_crash_data.pseudo_excause > 1) + s_raw_crash_data.pseudo_excause = 0; + } + // Clear magic regardless so we don't re-report on next normal reboot + s_raw_crash_data.magic = 0; +} + +bool crash_handler_has_data() { return s_crash_data_valid; } + +// Look up the exception cause as a human-readable string. +// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays +// not exposed via any public API. +static const char *get_exception_reason() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + if (s_raw_crash_data.pseudo_excause) { + // SoC-level panic: watchdog, cache error, etc. + // Keep in sync with ESP-IDF's PANIC_RSN_* defines + static const char *const PSEUDO_REASON[] = { + "Unknown reason", // 0 + "Unhandled debug exception", // 1 + "Double exception", // 2 + "Unhandled kernel exception", // 3 + "Coprocessor exception", // 4 + "Interrupt wdt timeout on CPU0", // 5 + "Interrupt wdt timeout on CPU1", // 6 + "Cache error", // 7 + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0])) + return PSEUDO_REASON[cause]; + return PSEUDO_REASON[0]; + } + // Real Xtensa exception + static const char *const REASON[] = { + "IllegalInstruction", + "Syscall", + "InstructionFetchError", + "LoadStoreError", + "Level1Interrupt", + "Alloca", + "IntegerDivideByZero", + "PCValue", + "Privileged", + "LoadStoreAlignment", + nullptr, + nullptr, + "InstrPDAddrError", + "LoadStorePIFDataError", + "InstrPIFAddrError", + "LoadStorePIFAddrError", + "InstTLBMiss", + "InstTLBMultiHit", + "InstFetchPrivilege", + nullptr, + "InstrFetchProhibited", + nullptr, + nullptr, + nullptr, + "LoadStoreTLBMiss", + "LoadStoreTLBMultihit", + "LoadStorePrivilege", + nullptr, + "LoadProhibited", + "StoreProhibited", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal + // interrupt numbers, not standard RISC-V cause codes. The exception type + // field already identifies these, so just return null to use the type name. + if (s_raw_crash_data.pseudo_excause) + return nullptr; + static const char *const REASON[] = { + "Instruction address misaligned", + "Instruction access fault", + "Illegal instruction", + "Breakpoint", + "Load address misaligned", + "Load access fault", + "Store address misaligned", + "Store access fault", + "Environment call from U-mode", + "Environment call from S-mode", + nullptr, + "Environment call from M-mode", + "Instruction page fault", + "Load page fault", + nullptr, + "Store page fault", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#endif + return "Unknown"; +} + +// Exception type names matching panic_exception_t enum +static const char *get_exception_type() { + static const char *const TYPES[] = { + "Debug exception", // PANIC_EXCEPTION_DEBUG + "Interrupt wdt", // PANIC_EXCEPTION_IWDT + "Task wdt", // PANIC_EXCEPTION_TWDT + "Abort", // PANIC_EXCEPTION_ABORT + "Fault", // PANIC_EXCEPTION_FAULT + }; + uint8_t exc = s_raw_crash_data.exception; + if (exc < sizeof(TYPES) / sizeof(TYPES[0])) + return TYPES[exc]; + return "Unknown"; +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console, making it possible to see partial output if the device +// crashes again during boot, and allowing the CLI's process_stacktrace to match +// and decode each address individually. +void crash_handler_log() { + if (!s_crash_data_valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + const char *reason = get_exception_reason(); + if (reason != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason); + } else { + ESP_LOGE(TAG, " Reason: %s", get_exception_type()); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); + uint8_t bt_num = 0; + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif +#if CONFIG_IDF_TARGET_ARCH_RISCV + const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[256]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::esp32 + +// --- Panic handler wrapper --- +// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data +// into NOINIT memory before the normal panic handler runs. +// +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +// Names are mandated by the --wrap linker mechanism +extern void __real_esp_panic_handler(panic_info_t *info); + +void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { + // Save the faulting PC and exception info + s_raw_crash_data.pc = (uint32_t) info->addr; + s_raw_crash_data.backtrace_count = 0; + s_raw_crash_data.reg_frame_count = 0; + s_raw_crash_data.exception = (uint8_t) info->exception; + s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + +#if CONFIG_IDF_TARGET_ARCH_XTENSA + // Xtensa: walk the backtrace using the public API + if (info->frame != nullptr) { + auto *xt_frame = (XtExcFrame *) info->frame; + s_raw_crash_data.cause = xt_frame->exccause; + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) xt_frame->pc, + .sp = (uint32_t) xt_frame->a1, + .next_pc = (uint32_t) xt_frame->a0, + .exc_frame = xt_frame, + }; + + uint8_t count = 0; + // First frame PC + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + s_raw_crash_data.backtrace[count++] = first_pc; + } + // Walk remaining frames + while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) { + break; + } + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + s_raw_crash_data.backtrace[count++] = pc; + } + } + s_raw_crash_data.backtrace_count = count; + } + +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // RISC-V: capture MEPC + RA, then scan stack for code addresses + if (info->frame != nullptr) { + auto *rv_frame = (RvExcFrame *) info->frame; + s_raw_crash_data.cause = rv_frame->mcause; + uint8_t count = 0; + + // Save MEPC (fault PC) and RA (return address) + if (is_code_addr(rv_frame->mepc)) { + s_raw_crash_data.backtrace[count++] = rv_frame->mepc; + } + if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { + s_raw_crash_data.backtrace[count++] = rv_frame->ra; + } + + // Track how many entries came from registers (MEPC/RA) so we can + // skip return-address validation for them at log time. + s_raw_crash_data.reg_frame_count = count; + + // Scan stack for code addresses — captures broadly during panic, + // filtered by is_return_addr() at log time when flash is accessible. + auto *scan_start = (uint32_t *) rv_frame->sp; + for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { + s_raw_crash_data.backtrace[count++] = val; + } + } + s_raw_crash_data.backtrace_count = count; + } +#endif + + // Write version and magic last — ensures all data is written before we mark it valid + s_raw_crash_data.version = CRASH_DATA_VERSION; + s_raw_crash_data.magic = CRASH_MAGIC; + + // Call the real panic handler (prints to UART, does core dump, reboots, etc.) + __real_esp_panic_handler(info); +} + +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +} // extern "C" + +#endif // USE_ESP32_CRASH_HANDLER +#endif // USE_ESP32 diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h new file mode 100644 index 00000000000..97a4d4e1162 --- /dev/null +++ b/esphome/components/esp32/crash_handler.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef USE_ESP32_CRASH_HANDLER + +namespace esphome::esp32 { + +/// Read crash data from NOINIT memory and clear the magic marker. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + +} // namespace esphome::esp32 + +#endif // USE_ESP32_CRASH_HANDLER diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index d6ad77ff4fa..f5bf7822899 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "logger.h" +#include "esphome/components/esp32/crash_handler.h" #include <esp_log.h> #include <driver/uart.h> @@ -117,6 +118,9 @@ void Logger::pre_setup() { esp_log_set_vprintf(esp_idf_log_vprintf_); ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cec77fe2e27..a33f10cb9c0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -195,6 +195,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 5d4065207f0..cb080b2a953 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -340,6 +340,8 @@ STACKTRACE_ESP32_BACKTRACE_RE = re.compile( r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" ) STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") +# ESP32 crash handler (stored backtrace from previous boot) +STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})") STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") @@ -371,6 +373,11 @@ def process_stacktrace(config, line, backtrace_state): ) _decode_pc(config, match.group(1)) + # ESP32 crash handler backtrace (from previous boot) + match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line) + if match is not None: + _decode_pc(config, match.group(1)) + # ESP32 single-line backtrace match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line) if match is not None: diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 16861442777..e1b3908c249 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -673,6 +673,34 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_process_stacktrace_esp32_crash_handler( + setup_core: Path, mock_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP32 crash handler backtrace lines.""" + config = {"name": "test"} + + # Simulate crash handler log lines as they appear from the API/serial + line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)" + state = platformio_api.process_stacktrace(config, line_pc, False) + # PC line is matched by existing STACKTRACE_ESP32_PC_RE + mock_decode_pc.assert_called_with(config, "400D1234") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt0, False) + mock_decode_pc.assert_called_once_with(config, "400D5678") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt1, False) + mock_decode_pc.assert_called_once_with(config, "42005ABC") + assert state is False + + def test_patch_file_downloader_succeeds_first_try() -> None: """Test patch_file_downloader succeeds on first attempt.""" mock_exception_cls = type("PackageException", (Exception,), {}) From bb7d96b954d12e99c75a02c4ebc6f12c62382942 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:31:17 +0100 Subject: [PATCH 0575/2056] [const] Add UNIT_METER_PER_SECOND, UNIT_MILLILITRE, UNIT_POUND to const.py (#14713) --- esphome/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index 33a2526d38b..29ce0303297 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1235,6 +1235,7 @@ UNIT_LITRE = "L" UNIT_LUX = "lx" UNIT_MEGAJOULE = "MJ" UNIT_METER = "m" +UNIT_METER_PER_SECOND = "m/s" UNIT_METER_PER_SECOND_SQUARED = "m/s²" UNIT_MICROAMP = "µA" UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³" @@ -1244,6 +1245,7 @@ UNIT_MICROSILVERTS_PER_HOUR = "µSv/h" UNIT_MICROTESLA = "µT" UNIT_MILLIAMP = "mA" UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" +UNIT_MILLILITRE = "mL" UNIT_MILLIMETER = "mm" UNIT_MILLISECOND = "ms" UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm" @@ -1255,6 +1257,7 @@ UNIT_PARTS_PER_MILLION = "ppm" UNIT_PASCAL = "Pa" UNIT_PERCENT = "%" UNIT_PH = "pH" +UNIT_POUND = "lb" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" UNIT_REVOLUTIONS_PER_MINUTE = "RPM" From 7f38d95424d0d5a29acfaba0b891d5f847320ad7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 23:48:27 -0500 Subject: [PATCH 0576/2056] [ethernet] ESP32-S3 Ethernet compilation fix (#14717) --- esphome/components/ethernet/ethernet_component.h | 3 +++ esphome/components/ethernet/ethernet_helpers.c | 2 ++ tests/components/ethernet/common-w5500.yaml | 4 ++-- tests/components/ethernet/test.esp32-s3-idf.yaml | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/components/ethernet/test.esp32-s3-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c464e20b843..f7a0996fb74 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,11 +11,14 @@ #include "esp_eth.h" #include "esp_eth_mac.h" +#include "esp_eth_mac_esp.h" #include "esp_netif.h" #include "esp_mac.h" #include "esp_idf_version.h" +#if CONFIG_ETH_USE_ESP32_EMAC extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); +#endif namespace esphome::ethernet { diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c index 96faccad24c..963db3ff1c4 100644 --- a/esphome/components/ethernet/ethernet_helpers.c +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -3,6 +3,8 @@ // ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers // which are valid in C but not in C++. This wrapper allows C++ code to get // the default config without replicating the macro's contents. +#if CONFIG_ETH_USE_ESP32_EMAC eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); } +#endif diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index 1f8b8650dd0..bf3f6f3f0c4 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -2,10 +2,10 @@ ethernet: type: W5500 clk_pin: 19 mosi_pin: 21 - miso_pin: 23 + miso_pin: 17 cs_pin: 18 interrupt_pin: 36 - reset_pin: 22 + reset_pin: 12 clock_speed: 10Mhz manual_ip: static_ip: 192.168.178.56 diff --git a/tests/components/ethernet/test.esp32-s3-idf.yaml b/tests/components/ethernet/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..36f1b5365f1 --- /dev/null +++ b/tests/components/ethernet/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-w5500.yaml From f8a22b87b8908c33903355a74e3d593e29584f54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 19:23:01 -1000 Subject: [PATCH 0577/2056] [rp2040] Fix crash handler design flaws (#14716) --- esphome/components/api/api_connection.h | 6 ++++++ esphome/components/logger/logger_rp2040.cpp | 5 +++++ esphome/components/rp2040/__init__.py | 1 + esphome/components/rp2040/core.cpp | 6 +++++- esphome/components/rp2040/crash_handler.cpp | 23 ++++++++++++++++----- esphome/components/rp2040/crash_handler.h | 8 ++++++- esphome/core/defines.h | 1 + 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 60cc3e91b11..68f698d1902 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -17,6 +17,9 @@ #ifdef USE_ESP32_CRASH_HANDLER #include "esphome/components/esp32/crash_handler.h" #endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -240,6 +243,9 @@ class APIConnection final : public APIServerConnectionBase { App.schedule_dump_config(); #ifdef USE_ESP32_CRASH_HANDLER esp32::crash_handler_log(); +#endif +#ifdef USE_RP2040_CRASH_HANDLER + rp2040::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f76b823a8f7..b7225c2a258 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -1,6 +1,9 @@ #ifdef USE_RP2040 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,7 +29,9 @@ void Logger::pre_setup() { } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b15811241ca..276187b273c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -212,6 +212,7 @@ async def to_code(config): ) cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) + cg.add_define("USE_RP2040_CRASH_HANDLER") def add_pio_file(component: str, key: str, data: str): diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 5e5a96c78b1..7079cbca155 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,8 +1,10 @@ #ifdef USE_RP2040 #include "core.h" -#include "crash_handler.h" #include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER +#include "crash_handler.h" +#endif #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -25,7 +27,9 @@ void arch_restart() { } void arch_init() { +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_read_and_clear(); +#endif #if USE_RP2040_WATCHDOG_TIMEOUT > 0 watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); #endif diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 6ab46da4449..1f579c2d18e 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -1,5 +1,8 @@ #ifdef USE_RP2040 +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER + #include "crash_handler.h" #include "esphome/core/log.h" @@ -13,13 +16,19 @@ static constexpr uint32_t EF_LR = 5; static constexpr uint32_t EF_PC = 6; -static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +// Version encoded in the magic value: upper 16 bits are sentinel (0xDEAD), +// lower 16 bits are the version number. This avoids using a separate scratch +// register for versioning (we only have 8 total). Future firmware reads the +// sentinel to confirm it's crash data, then the version to know the layout. +static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000; +static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION; // We only have 8 scratch registers (32 bytes) that survive watchdog reboot. // Use them for the most important data, then scan the stack for code addresses. // // Scratch register layout: -// [0] = magic (CRASH_MAGIC) +// [0] = versioned magic (upper 16 bits = 0xDEAD sentinel, lower 16 bits = version) // [1] = PC (program counter at fault) // [2] = LR (link register from exception frame) // [3] = SP (stack pointer at fault) @@ -57,9 +66,12 @@ static struct { uint8_t backtrace_count; } __attribute__((section(".noinit"))) s_crash_data; +bool crash_handler_has_data() { return s_crash_data.valid; } + void crash_handler_read_and_clear() { s_crash_data.valid = false; - if (watchdog_hw->scratch[0] == CRASH_MAGIC) { + uint32_t magic = watchdog_hw->scratch[0]; + if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) { s_crash_data.valid = true; s_crash_data.pc = watchdog_hw->scratch[1]; s_crash_data.lr = watchdog_hw->scratch[2]; @@ -135,7 +147,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame // by a stacking error or corrupted SP, frame may be invalid. Write a minimal // crash marker so we at least know a crash occurred. if (!is_valid_sram_ptr(frame)) { - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = 0; // PC unknown watchdog_hw->scratch[2] = 0; // LR unknown watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis @@ -157,7 +169,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame); // Write key registers - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = frame[EF_PC]; watchdog_hw->scratch[2] = frame[EF_LR]; watchdog_hw->scratch[3] = pre_fault_sp; @@ -224,4 +236,5 @@ extern "C" void __attribute__((naked, used)) isr_hardfault() { : "i"(hard_fault_handler_c)); } +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2040/crash_handler.h index f10db47c234..78e8ede08c8 100644 --- a/esphome/components/rp2040/crash_handler.h +++ b/esphome/components/rp2040/crash_handler.h @@ -2,7 +2,9 @@ #ifdef USE_RP2040 -#include <cstdint> +#include "esphome/core/defines.h" + +#ifdef USE_RP2040_CRASH_HANDLER namespace esphome::rp2040 { @@ -12,6 +14,10 @@ void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + } // namespace esphome::rp2040 +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a33f10cb9c0..073170aafbf 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -338,6 +338,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) #define USE_LOOP_PRIORITY +#define USE_RP2040_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From 8a5f008aee25d70a9350959cfcbd5de23d2f83e8 Mon Sep 17 00:00:00 2001 From: Adam DeMuri <adam.demuri@gmail.com> Date: Thu, 12 Mar 2026 02:00:26 -0600 Subject: [PATCH 0578/2056] [modbus] Fix buffer overflow in modbus (#14719) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/modbus/modbus.cpp | 12 ++--- tests/components/modbus/modbus_test.cpp | 59 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/components/modbus/modbus_test.cpp diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 82672217c56..7a61868e6e9 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -125,13 +125,17 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Byte 0: modbus address (match all) if (at == 0) return true; - uint8_t address = raw[0]; - uint8_t function_code = raw[1]; + // Byte 1: function code + if (at == 1) + return true; // Byte 2: Size (with modbus rtu function code 4/3) // See also https://en.wikipedia.org/wiki/Modbus if (at == 2) return true; + uint8_t address = raw[0]; + uint8_t function_code = raw[1]; + uint8_t data_len = raw[2]; uint8_t data_offset = 3; @@ -146,10 +150,6 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // chance that this is a complete message ... admittedly there is a small chance is // isn't but that is quite small given the purpose of the CRC in the first place - // Fewer than 2 bytes can't calc CRC - if (at < 2) - return true; - data_len = at - 2; data_offset = 1; diff --git a/tests/components/modbus/modbus_test.cpp b/tests/components/modbus/modbus_test.cpp new file mode 100644 index 00000000000..afe5ced082b --- /dev/null +++ b/tests/components/modbus/modbus_test.cpp @@ -0,0 +1,59 @@ +#include <gtest/gtest.h> +#include "esphome/components/modbus/modbus.h" +#include "esphome/core/helpers.h" + +namespace esphome::modbus { + +// Exposes protected methods for testing. +class TestModbus : public Modbus { + public: + bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); } + void test_clear_rx_buffer() { this->rx_buffer_.clear(); } + void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; } +}; + +class MockDevice : public ModbusDevice { + public: + void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; } + bool data_received{false}; +}; + +TEST(ModbusTest, TwoByteRegressionTest) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + // First byte (at=0) + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01)); + // Second byte (at=1) + // This used to reach raw[2] because it skipped the if(at==2) check, causing a + // buffer overflow. + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03)); +} + +TEST(ModbusTest, TestValidFrame) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + + MockDevice device; + device.set_parent(&modbus); + device.set_address(0x01); + modbus.register_device(&device); + modbus.set_waiting(0x01); + + // Address 1, Function 3, Length 2, Data 0x1234 + uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34}; + uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data)); + + std::vector<uint8_t> frame; + for (uint8_t b : frame_data) + frame.push_back(b); + frame.push_back(crc & 0xFF); + frame.push_back((crc >> 8) & 0xFF); + + for (size_t i = 0; i < frame.size(); i++) { + bool result = modbus.test_parse_modbus_byte(frame[i]); + EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")"; + } + EXPECT_TRUE(device.data_received); +} + +} // namespace esphome::modbus From 657890695f8215aeaa4166d6c41b950273538b64 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 12 Mar 2026 03:16:02 -0500 Subject: [PATCH 0579/2056] [ledc] Fix high-pressure crash & recovery (#14720) --- esphome/components/ledc/ledc_output.cpp | 53 +++++++++++++++++++++++-- esphome/components/ledc/ledc_output.h | 8 ++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 763de851da3..592fc7bd0c1 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -5,6 +5,10 @@ #include <driver/ledc.h> #include <cinttypes> +#include <esp_private/periph_ctrl.h> +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +#include <hal/ledc_ll.h> +#endif #define CLOCK_FREQUENCY 80e6f @@ -16,10 +20,10 @@ static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; -namespace esphome { -namespace ledc { +namespace esphome::ledc { static const char *const TAG = "ledc.output"; +static bool ledc_peripheral_reset_done = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; #if SOC_LEDC_SUPPORT_HS_MODE @@ -32,6 +36,28 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +// Classic ESP32 (currently the only target without SOC_LEDC_SUPPORT_FADE_STOP) can block in +// ledc_ll_set_duty_start() while duty_start is set. We check the same conf1.duty_start bit here +// to defer updates and avoid entering IDF's unbounded wait loop. +// +// This intentionally depends on the classic ESP32 LEDC register layout used by IDF's own LL HAL. +// If another target without SOC_LEDC_SUPPORT_FADE_STOP is introduced, revisit this helper. +static_assert( +#if defined(CONFIG_IDF_TARGET_ESP32) + true, +#else + false, +#endif + "LEDC duty_start pending check assumes classic ESP32 register layout; " + "re-evaluate for this target"); + +static bool ledc_duty_update_pending(ledc_mode_t speed_mode, ledc_channel_t chan_num) { + auto *hw = LEDC_LL_GET_HW(); + return hw->channel_group[speed_mode].channel[chan_num].conf1.duty_start != 0; +} +#endif + float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth); } @@ -105,21 +131,40 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast<uint32_t>(duty_rounded); + if (duty == this->last_duty_) { + return; + } + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); auto speed_mode = get_speed_mode(this->channel_); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); if (duty == max_duty) { ledc_stop(speed_mode, chan_num, 1); + this->last_duty_ = duty; } else if (duty == 0) { ledc_stop(speed_mode, chan_num, 0); + this->last_duty_ = duty; } else { +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) + if (ledc_duty_update_pending(speed_mode, chan_num)) { + ESP_LOGV(TAG, "Skipping LEDC duty update on channel %u while previous duty_start is still set", this->channel_); + return; + } +#endif ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); + this->last_duty_ = duty; } } void LEDCOutput::setup() { + if (!ledc_peripheral_reset_done) { + ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot"); + periph_module_reset(PERIPH_LEDC_MODULE); + ledc_peripheral_reset_done = true; + } + auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); @@ -207,12 +252,12 @@ void LEDCOutput::update_frequency(float frequency) { this->status_clear_error(); // re-apply duty + this->last_duty_ = UINT32_MAX; this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index b24e3cfdb23..bf5cdb93055 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -4,11 +4,11 @@ #include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" +#include <cstdint> #ifdef USE_ESP32 -namespace esphome { -namespace ledc { +namespace esphome::ledc { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern uint8_t next_ledc_channel; @@ -39,6 +39,7 @@ class LEDCOutput : public output::FloatOutput, public Component { float phase_angle_{0.0f}; float frequency_{}; float duty_{0.0f}; + uint32_t last_duty_{UINT32_MAX}; bool initialized_ = false; }; @@ -56,7 +57,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { LEDCOutput *parent_; }; -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif From fe2d60ccecf6ceb18def3895a174277037942791 Mon Sep 17 00:00:00 2001 From: Massimo Antonello <31179882+MaxPlap@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:52:58 +0100 Subject: [PATCH 0580/2056] [one_wire] allow changing address at runtime (#12150) --- esphome/components/one_wire/one_wire.cpp | 5 +++++ esphome/components/one_wire/one_wire.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 187f559ca6a..d14c1c92bdd 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -13,6 +13,11 @@ const std::string &OneWireDevice::get_address_name() { return this->address_name_; } +void OneWireDevice::set_address(uint64_t address) { + this->address_ = address; + this->address_name_.clear(); +} + bool OneWireDevice::send_command_(uint8_t cmd) { if (!this->bus_->select(this->address_)) return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index f6a956a92c7..324e46cd55d 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -15,7 +15,7 @@ class OneWireDevice { public: /// @brief store the address of the device /// @param address of the device - void set_address(uint64_t address) { this->address_ = address; } + void set_address(uint64_t address); void set_index(uint8_t index) { this->index_ = index; } From c4c19c8a6ca7feaf08f4c30e9153c3d4c87fb0b9 Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 02:07:26 -0700 Subject: [PATCH 0581/2056] [web_server] use DETAIL_ALL in update_all_json_generator (#14711) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5590e67b822..40830196433 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2181,7 +2181,7 @@ json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *we } json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL); } json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson From 511d18577276dcd4b434e82f4abe469c77a0bf47 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 12 Mar 2026 07:56:01 -0500 Subject: [PATCH 0582/2056] [audio] Bump microOpus to v0.3.5 (#14727) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index d95fcf66d7b..b28c2ed3d8c 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.4") + add_idf_component(name="esphome/micro-opus", ref="0.3.5") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f7fd3e67bc3..df651ae15dd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.4 + version: 0.3.5 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From a76767a0abdc371ad7f3169427cea5b0b83a7594 Mon Sep 17 00:00:00 2001 From: guillempages <guillempages@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:15:20 +0100 Subject: [PATCH 0583/2056] [runtime_image] Update jpegdec lib version (#14726) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/runtime_image/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ff25675918b..87b4ebb2c69 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e +8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 0773a53d911..7c22bfc9d19 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -74,7 +74,7 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") class PNGFormat(Format): diff --git a/platformio.ini b/platformio.ini index deee23d049c..3c3d62ef76a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,11 +46,11 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.11 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library - https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image + https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image ; This dependency is used only in unit tests. ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py ; See scripts/cpp_unit_test.py and tests/components/README.md From 25c30ac5bb5f32c59663dbeb4946b4f8c5e9d533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6nig?= <Matthias.Konitzny@kabelmail.de> Date: Thu, 12 Mar 2026 17:00:08 +0100 Subject: [PATCH 0584/2056] [mqtt] Fixed permission denied error for client certificates on Windows (#13525) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/mqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index cbf78bd3f6e..ccacbaea54f 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -2,6 +2,7 @@ import contextlib from datetime import datetime import json import logging +import os import ssl import tempfile import time @@ -109,14 +110,18 @@ def prepare( CONF_CLIENT_CERTIFICATE_KEY ): with ( - tempfile.NamedTemporaryFile(mode="w+") as cert_file, - tempfile.NamedTemporaryFile(mode="w+") as key_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file, ): - cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) - cert_file.flush() - key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) - key_file.flush() - context.load_cert_chain(cert_file.name, key_file.name) + try: + cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) + key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) + cert_file.close() + key_file.close() + context.load_cert_chain(cert_file.name, key_file.name) + finally: + os.unlink(cert_file.name) + os.unlink(key_file.name) client.tls_set_context(context) try: From 07f8ae6c8266ae2f6207463bb3c0bebe4eb5c062 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:14:49 -1000 Subject: [PATCH 0585/2056] [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) --- .../components/socket/lwip_raw_tcp_impl.cpp | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index fd1b8a95542..1e03a4935c2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -138,13 +138,46 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif +// Clear arg, recv, and err callbacks, then abort a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// Must be called before destroying the object that tcp_arg points to — +// tcp_abort() triggers the err callback synchronously, which would +// otherwise call back into a partially-destroyed object. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +static void pcb_detach_abort(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + tcp_abort(pcb); +} + +// Clear arg, recv, and err callbacks, then gracefully close a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// After tcp_close(), the PCB remains alive during the TCP close handshake +// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP +// would call recv/err on a destroyed socket object, corrupting the heap. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +// Returns ERR_OK on success; on failure the PCB is aborted instead. +static err_t pcb_detach_close(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + err_t err = tcp_close(pcb); + if (err != ERR_OK) { + tcp_abort(pcb); + } + return err; +} + // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { LWIP_LOCK(); if (this->pcb_ != nullptr) { LWIP_LOG("tcp_abort(%p)", this->pcb_); - tcp_abort(this->pcb_); + pcb_detach_abort(this->pcb_); this->pcb_ = nullptr; } } @@ -222,15 +255,13 @@ int LWIPRawCommon::close() { return -1; } LWIP_LOG("tcp_close(%p)", this->pcb_); - err_t err = tcp_close(this->pcb_); + err_t err = pcb_detach_close(this->pcb_); + this->pcb_ = nullptr; if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - tcp_abort(this->pcb_); - this->pcb_ = nullptr; errno = err == ERR_MEM ? ENOMEM : EIO; return -1; } - this->pcb_ = nullptr; return 0; } @@ -673,13 +704,10 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { LWIPRawListenImpl::~LWIPRawListenImpl() { LWIP_LOCK(); // Abort any queued PCBs that were never accepted by the main loop. - // Clear the error callback first — tcp_abort triggers it, and we don't - // want s_queued_err_fn writing to slots during destruction. for (uint8_t i = 0; i < this->accepted_socket_count_; i++) { auto &entry = this->accepted_pcbs_[i]; if (entry.pcb != nullptr) { - tcp_err(entry.pcb, nullptr); - tcp_abort(entry.pcb); + pcb_detach_abort(entry.pcb); entry.pcb = nullptr; } if (entry.rx_buf != nullptr) { @@ -691,6 +719,10 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. + // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access + // fields that only exist in the full tcp_pcb, not tcp_pcb_listen. + // tcp_close() on a listen PCB is synchronous (frees immediately), + // so there are no async callbacks to worry about. // Close here and null pcb_ so the base destructor skips tcp_abort. if (this->pcb_ != nullptr) { tcp_close(this->pcb_); From a3a88acfcf799a6e0a56051dcdface547642b7aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:04 -1000 Subject: [PATCH 0586/2056] [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) --- esphome/components/socket/bsd_sockets_impl.h | 11 ++++++++++- esphome/components/socket/lwip_sockets_impl.h | 11 ++++++++++- esphome/core/lwip_fast_select.c | 16 ++++++++++++++++ esphome/core/lwip_fast_select.h | 7 +++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index 9ebbe72002b..339a699bc97 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -14,7 +14,7 @@ #endif #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -56,6 +56,15 @@ class BSDSocketImpl { return ::getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return ::setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return ::listen(this->fd_, backlog); } diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index c5792198635..bfc4da9926a 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -10,7 +10,7 @@ #include "headers.h" #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -52,6 +52,15 @@ class LwIPSocketImpl { return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return lwip_setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return lwip_listen(this->fd_, backlog); } diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index c578a9aae91..a695fa396bc 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -112,6 +112,7 @@ // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include <lwip/api.h> #include <lwip/priv/sockets_priv.h> +#include <lwip/tcp.h> // FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not #ifdef USE_ESP32 #include <freertos/FreeRTOS.h> @@ -216,6 +217,21 @@ void esphome_lwip_hook_socket(struct lwip_sock *sock) { sock->conn->callback = esphome_socket_event_callback; } +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { + if (sock == NULL || sock->conn == NULL) + return false; + if (NETCONNTYPE_GROUP(sock->conn->type) != NETCONN_TCP) + return false; + if (sock->conn->pcb.tcp == NULL) + return false; + if (enable) { + tcp_nagle_disable(sock->conn->pcb.tcp); + } else { + tcp_nagle_enable(sock->conn->pcb.tcp); + } + return true; +} + // Wake the main loop from another FreeRTOS task. NOT ISR-safe. void esphome_lwip_wake_main_loop(void) { TaskHandle_t task = s_main_loop_task; diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 46c6b711cd2..50706ba9f69 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -66,6 +66,13 @@ void esphome_lwip_wake_main_loop(void); /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); +/// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. +/// Must be called with the TCPIP core lock held (LwIPLock in C++). +/// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, +/// hooks, refcounting) — just a direct pcb->flags bit set/clear. +/// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); + /// Wake the main loop task from any context (ISR, thread, or main loop). /// ESP32-only: uses xPortInIsrContext() to detect ISR context. /// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. From 03c091adfcf1981c2259715e563d84b8f5210935 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:21 -1000 Subject: [PATCH 0587/2056] [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_ble_client/ble_client_base.cpp | 43 ++++++++++++++++--- .../esp32_ble_client/ble_client_base.h | 17 +++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2f17334c77c..9d6e079d926 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -27,6 +27,7 @@ static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -62,6 +63,15 @@ void BLEClientBase::loop() { // will enable it again when a connection is needed. else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); + } else if (this->state() == espbt::ClientState::DISCONNECTING && + (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) { + ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_, + this->address_str_); + // release_services() must be called before set_idle_() — if we entered DISCONNECTING + // via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF + // never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call. + this->release_services(); + this->set_idle_(); } } @@ -101,12 +111,16 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - // Prevent duplicate connection attempts + // Prevent duplicate connection attempts or connecting while still disconnecting if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || this->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state())); return; + } else if (this->state() == espbt::ClientState::DISCONNECTING) { + ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect", + this->connection_index_, this->address_str_); + return; } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; @@ -174,7 +188,7 @@ void BLEClientBase::unconditional_disconnect() { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { - this->set_state(espbt::ClientState::DISCONNECTING); + this->set_disconnecting_(); } } @@ -220,6 +234,7 @@ void BLEClientBase::log_connection_params_(const char *param_type) { void BLEClientBase::handle_connection_result_(esp_err_t ret) { if (ret) { this->log_gattc_warning_("esp_ble_gattc_open", ret); + // Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID. this->set_state(espbt::ClientState::IDLE); } } @@ -311,15 +326,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); - this->set_state(espbt::ClientState::IDLE); + // Connection was never established so CLOSE_EVT may not follow + this->set_idle_(); break; } if (this->want_disconnect_) { // Disconnect was requested after connecting started, // but before the connection was established. Now that we have // this->conn_id_ set, we can disconnect it. + // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_(). this->unconditional_disconnect(); - this->conn_id_ = UNSET_CONN_ID; break; } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT @@ -363,8 +379,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); } + // For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before + // DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go + // backwards to DISCONNECTING — the connection is already fully cleaned up. + if (this->state() == espbt::ClientState::IDLE) { + this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE"); + break; + } + // For passive disconnects (remote device disconnected or link lost), + // DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for + // CLOSE_EVT to ensure the controller has fully freed resources (L2CAP + // channels, ATT resources, HCI connection handle). Transitioning to IDLE + // here would allow reconnection before cleanup is complete, causing the + // controller to reject the new connection (status=133) or crash with + // ASSERT_PARAM in lld_evt.c. this->release_services(); - this->set_state(espbt::ClientState::IDLE); + this->set_disconnecting_(); break; } @@ -387,8 +417,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; + this->set_idle_(); break; } case ESP_GATTC_SEARCH_RES_EVT: { diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index af4f1b30290..4e0b22cc299 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -113,11 +113,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; esp_bd_addr_t remote_bda_; // 6 bytes - // Group 5: 2-byte types + // Group 5: 4-byte types + uint32_t disconnecting_started_{0}; + + // Group 6: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; uint16_t mtu_{23}; - // Group 6: 1-byte types and small enums + // Group 7: 1-byte types and small enums esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; uint8_t connection_index_; @@ -137,6 +140,16 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); + /// Transition to IDLE and reset conn_id — call when the connection is fully dead. + void set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; + } + /// Transition to DISCONNECTING and start the safety timeout. + void set_disconnecting_() { + this->disconnecting_started_ = millis(); + this->set_state(espbt::ClientState::DISCONNECTING); + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From fd1d0167951b7b228f6c13c2ce463059a2636417 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:34 -1000 Subject: [PATCH 0588/2056] [time] Fix settimeofday() failure on ESP8266 (#14707) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/time/real_time_clock.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 566344fa880..4e623942ac5 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -88,16 +88,16 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, }; +#ifdef USE_ESP8266 + // ESP8266 settimeofday() requires tz to be nullptr + int ret = settimeofday(&timev, nullptr); +#else struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret != 0 && errno == EINVAL) { - // Some ESP8266 frameworks abort when timezone parameter is not NULL - // while ESP32 expects it not to be NULL - ret = settimeofday(&timev, nullptr); - } +#endif if (ret != 0) { - ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + ESP_LOGW(TAG, "settimeofday() failed with code %d", ret); } #endif auto time = this->now(); From 4a21afe7ce056115b7af94ccb02c7bc5bd3d8f36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:48 -1000 Subject: [PATCH 0589/2056] [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) --- .../components/esphome/ota/ota_esphome.cpp | 46 +++++++++++- esphome/components/socket/headers.h | 2 + .../components/socket/lwip_raw_tcp_impl.cpp | 71 +++++++++++++++++-- esphome/components/socket/lwip_raw_tcp_impl.h | 16 +++-- 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index a1cdf59d2b7..d8dbe2dee2d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -18,6 +18,7 @@ #include <cerrno> #include <cstdio> +#include <sys/time.h> namespace esphome { @@ -238,6 +239,31 @@ void ESPHomeOTAComponent::handle_data_() { /// and reboots on success. /// /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. + /// + /// Socket I/O strategy: + /// + /// Before this function, the handshake states use non-blocking I/O: + /// read()/write() return immediately with EWOULDBLOCK if no data + /// loop() retries on next iteration (~16ms), no delay needed + /// + /// This function switches to blocking mode with SO_RCVTIMEO/SO_SNDTIMEO: + /// + /// Path | Wait mechanism | WDT strategy + /// --------------|------------------------|--------------------------- + /// Main read | SO_RCVTIMEO (2s block) | feed_wdt() only, no delay + /// readall_() | SO_RCVTIMEO (2s block) | feed_wdt() + delay(0) + /// writeall_() | SO_SNDTIMEO (2s block) | feed_wdt() + delay(1) + /// + /// readall_() uses delay(0) because SO_RCVTIMEO already waited — just yield. + /// writeall_() uses delay(1) because on raw TCP (ESP8266, RP2040) writes + /// never block (tcp_write returns immediately), so delay(1) prevents spinning. + /// + /// Platform details: + /// BSD sockets (ESP32): setblocking(true) makes read/write block + /// lwip sockets (LT): setblocking(true) makes read/write block + /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses + /// socket_delay()/socket_wake() in read(); + /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -249,6 +275,14 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif + // Set socket timeouts and blocking mode (see strategy table above) + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + this->client_->setblocking(true); + // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); @@ -299,7 +333,8 @@ void ESPHomeOTAComponent::handle_data_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { - this->yield_and_feed_watchdog_(); + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); continue; } ESP_LOGW(TAG, "Read err %d", errno); @@ -401,7 +436,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { } else { at += read; } - this->yield_and_feed_watchdog_(); + // read() already waited via SO_RCVTIMEO, just yield without 1ms stall + App.feed_wdt(); + delay(0); } return true; @@ -422,10 +459,13 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); return false; } + // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning + this->yield_and_feed_watchdog_(); } else { at += written; + // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT + App.feed_wdt(); } - this->yield_and_feed_watchdog_(); } return true; } diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 16e4d23d3ba..0eece6480f6 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -51,6 +51,8 @@ #define SO_REUSEADDR 0x0004 /* Allow local address reuse */ #define SO_KEEPALIVE 0x0008 /* keep connections alive */ #define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ +#define SO_RCVTIMEO 0x1006 /* receive timeout */ +#define SO_SNDTIMEO 0x1005 /* send timeout */ #define SOL_SOCKET 0xfff /* options for socket level */ diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 1e03a4935c2..96328e68c73 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -5,6 +5,7 @@ #include <cerrno> #include <cstring> +#include <sys/time.h> #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,7 +82,9 @@ void socket_delay(uint32_t ms) { s_socket_woke = false; return; } - s_socket_woke = false; + // Don't clear s_socket_woke here — if an IRQ fires between the check above + // and the while loop below, the while condition sees it immediately. Clearing + // here would lose that wake and sleep until the timer fires. s_delay_expired = false; // Set a one-shot timer to wake us after the timeout. // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. @@ -99,6 +102,7 @@ void socket_delay(uint32_t ms) { // Cancel timer if we woke early (socket data arrived before timeout) if (!s_delay_expired) cancel_alarm(alarm); + s_socket_woke = false; // consume the wake for next call } // No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP @@ -359,6 +363,18 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o *optlen = 4; return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (*optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + uint32_t ms = this->recv_timeout_cs_ * 10; + auto *tv = reinterpret_cast<struct timeval *>(optval); + tv->tv_sec = ms / 1000; + tv->tv_usec = (ms % 1000) * 1000; + *optlen = sizeof(struct timeval); + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (*optlen < 4) { errno = EINVAL; @@ -388,6 +404,21 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle // to prevent warnings return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + const auto *tv = reinterpret_cast<const struct timeval *>(optval); + uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000; + uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond + this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs); + return 0; + } + if (level == SOL_SOCKET && optname == SO_SNDTIMEO) { + // Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op. + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (optlen != 4) { errno = EINVAL; @@ -518,8 +549,25 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { return ERR_OK; } -ssize_t LWIPRawImpl::read(void *buf, size_t len) { - LWIP_LOCK(); +void LWIPRawImpl::wait_for_data_() { + // Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040 + // (needs async_context lock). + // + // Loop until data arrives, connection closes, or the full timeout elapses. + // socket_delay() may return early due to other sockets waking the global + // socket_wake() flag, so we re-enter for the remaining time. + uint32_t timeout_ms = this->recv_timeout_cs_ * 10; + uint32_t start = millis(); + while (this->waiting_for_data_()) { + uint32_t elapsed = millis() - start; + if (elapsed >= timeout_ms) + break; + socket_delay(timeout_ms - elapsed); + } +} + +ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) { + // Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf. if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -578,11 +626,26 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { return read; } +ssize_t LWIPRawImpl::read(void *buf, size_t len) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + + LWIP_LOCK(); + return this->read_locked_(buf, len); +} + ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { - ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); + ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); if (err == -1) { if (ret != 0) { // if we already read some don't return an error diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 95931afcf3f..3c27d71062f 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -57,6 +57,7 @@ class LWIPRawCommon { // instead use it for determining whether to call lwip_output bool nodelay_ = false; sa_family_t family_ = 0; + uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s) }; /// Connected socket implementation for LWIP raw TCP. @@ -107,11 +108,8 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ECONNRESET; return -1; } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } + // Raw TCP doesn't use a blocking flag directly. Blocking behavior + // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). return 0; } int loop() { return 0; } @@ -122,6 +120,14 @@ class LWIPRawImpl : public LWIPRawCommon { static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); protected: + // True when the socket could receive data but none has arrived yet. + // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool, + // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does + // an unnecessary wait (stale true) or skips it (stale false), and the + // authoritative recheck happens under LWIP_LOCK afterward. + bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; } + void wait_for_data_(); + ssize_t read_locked_(void *buf, size_t len); ssize_t internal_write_(const void *buf, size_t len); int internal_output_(); From 70d188202a1ddca4a3a342ca332f2d0c13a47588 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:08 -1000 Subject: [PATCH 0590/2056] [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) --- esphome/components/adc/adc_sensor_rp2040.cpp | 7 +++++++ tests/components/adc/test.rp2040-pico2-ard.yaml | 11 +++++++++++ tests/components/spi/test.rp2040-pico2-ard.yaml | 6 ++++++ .../build_components_base.rp2040-pico2-ard.yaml | 15 +++++++++++++++ .../common/spi/rp2040-pico2-ard.yaml | 12 ++++++++++++ 5 files changed, 51 insertions(+) create mode 100644 tests/components/adc/test.rp2040-pico2-ard.yaml create mode 100644 tests/components/spi/test.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/common/spi/rp2040-pico2-ard.yaml diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 8496e0f41e4..a79707e2347 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -8,6 +8,13 @@ #endif // CYW43_USES_VSYS_PIN #include <hardware/adc.h> +// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h), +// but the Arduino framework's config_autogen.h includes a generic board header +// that doesn't define it. Provide the standard value (pin 29) as a fallback. +#ifndef PICO_VSYS_PIN +#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage) +#endif + namespace esphome { namespace adc { diff --git a/tests/components/adc/test.rp2040-pico2-ard.yaml b/tests/components/adc/test.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..4cc865bb5d3 --- /dev/null +++ b/tests/components/adc/test.rp2040-pico2-ard.yaml @@ -0,0 +1,11 @@ +sensor: + - id: my_sensor + platform: adc + pin: VCC + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/spi/test.rp2040-pico2-ard.yaml b/tests/components/spi/test.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..81a8acafd88 --- /dev/null +++ b/tests/components/spi/test.rp2040-pico2-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..0922a5238e8 --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestrp2040pico2ard + friendly_name: $component_name + +rp2040: + board: rpipico2 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..205beb6e1bb --- /dev/null +++ b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for RP2040 Pico 2 (RP2350) Arduino tests + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO19 + miso_pin: GPIO16 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} From 618312f0ee0944a9575b58bc9eb62fb929755fa9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:23 -1000 Subject: [PATCH 0591/2056] [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) --- esphome/components/api/api_frame_helper_noise.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3e6ecf9dc30..f945253c89d 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -258,10 +258,13 @@ APIError APINoiseFrameHelper::state_action_() { // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data size_t old_size = this->prologue_.size(); - this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); - this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); - this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); + size_t rx_size = this->rx_buf_.size(); + this->prologue_.resize(old_size + 2 + rx_size); + this->prologue_[old_size] = (uint8_t) (rx_size >> 8); + this->prologue_[old_size + 1] = (uint8_t) rx_size; + if (rx_size > 0) { + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + } state_ = State::SERVER_HELLO; } From 186ca4e458cdc16cf1ee9fa692b9dbe066d57b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:38 -1000 Subject: [PATCH 0592/2056] [uart] Allow hardware UART with single pin on RP2040 (#14725) --- .../components/uart/uart_component_rp2040.cpp | 37 +++++++++++++++---- tests/components/uart/test.rp2040-ard.yaml | 3 ++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 858f1a02ddf..6f6f1fb96b7 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -105,15 +105,34 @@ void RP2040UartComponent::setup() { } } + // Determine which hardware UART to use. A pin that is not specified + // should not prevent hardware UART selection — one-way UART is valid. + // When both pins are configured, both must be HW-capable and agree on UART number. + // When only one pin is configured (nullptr other), use that pin's HW UART. + // If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO. + int8_t hw_uart = -1; + const bool tx_configured = (this->tx_pin_ != nullptr); + const bool rx_configured = (this->rx_pin_ != nullptr); + + if (tx_configured && rx_configured) { + // Both pins configured — both must map to the same hardware UART + if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) { + hw_uart = tx_hw; + } + } else if (tx_configured) { + hw_uart = tx_hw; + } else if (rx_configured) { + hw_uart = rx_hw; + } + #ifdef USE_LOGGER - if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) { - ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw); - tx_hw = -1; - rx_hw = -1; + if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) { + ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart); + hw_uart = -1; } #endif - if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { + if (hw_uart == -1) { ESP_LOGV(TAG, "Using SerialPIO"); pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin(); pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin(); @@ -127,13 +146,15 @@ void RP2040UartComponent::setup() { } else { ESP_LOGV(TAG, "Using Hardware Serial"); SerialUART *serial; - if (tx_hw == 0) { + if (hw_uart == 0) { serial = &Serial1; } else { serial = &Serial2; } - serial->setTX(this->tx_pin_->get_pin()); - serial->setRX(this->rx_pin_->get_pin()); + if (this->tx_pin_ != nullptr) + serial->setTX(this->tx_pin_->get_pin()); + if (this->rx_pin_ != nullptr) + serial->setRX(this->rx_pin_->get_pin()); serial->setFIFOSize(this->rx_buffer_size_); serial->begin(this->baud_rate_, config); this->serial_ = serial; diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 5eb2b533ea0..1d5f91c6a7d 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -23,3 +23,6 @@ uart: baud_rate: 115200 debug: debug_prefix: "[UART1] " + - id: uart_rx_only + rx_pin: 17 + baud_rate: 1200 From 05d285ba861572e5af149676dfe763edc39129c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:53 -1000 Subject: [PATCH 0593/2056] [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 740bf2e47fd..5a53f0281fa 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -13,7 +13,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b4044c362c6..dff6c7690a0 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -642,7 +642,7 @@ class StringType(TypeInfo): # For SOURCE_BOTH, check if StringRef is set (sending) or use string (received) return ( f"if (!this->{self.field_name}_ref_.empty()) {{" - f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");' + f' out.append("\'").append(this->{self.field_name}_ref_.c_str(), this->{self.field_name}_ref_.size()).append("\'");' f"}} else {{" f' out.append("\'").append(this->{self.field_name}).append("\'");' f"}}" @@ -2705,7 +2705,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } From 25c74c8f99bb1948d9ceece698f4d8d5a80a8a53 Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 16:23:29 -0700 Subject: [PATCH 0594/2056] [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) --- esphome/components/ota/ota_backend_esp8266.cpp | 7 +++++-- esphome/components/ota/ota_backend_esp8266.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 1f9a77e4261..93e6249fb3d 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -105,6 +105,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { this->current_address_ = this->start_address_; this->image_size_ = image_size; + this->bytes_received_ = 0; this->buffer_len_ = 0; this->md5_set_ = false; @@ -140,6 +141,7 @@ OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); this->buffer_len_ += to_buffer; + this->bytes_received_ += to_buffer; written += to_buffer; // If buffer is full, write to flash @@ -252,8 +254,8 @@ OTAResponseTypes ESP8266OTABackend::end() { } } - // Calculate actual bytes written - size_t actual_size = this->current_address_ - this->start_address_; + // Calculate actual bytes written (exact uploaded size, excluding flash write padding) + size_t actual_size = this->bytes_received_; // Check if any data was written if (actual_size == 0) { @@ -304,6 +306,7 @@ void ESP8266OTABackend::abort() { this->buffer_.reset(); this->buffer_len_ = 0; this->image_size_ = 0; + this->bytes_received_ = 0; esp8266::preferences_prevent_write(false); } diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index 6213289accb..b364e216a36 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -48,6 +48,7 @@ class ESP8266OTABackend final { uint32_t start_address_{0}; uint32_t current_address_{0}; size_t image_size_{0}; + size_t bytes_received_{0}; md5::MD5Digest md5_{}; uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest From fd8e510745542d097e2ab0dcc36352b428cd5f4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 13:28:25 -1000 Subject: [PATCH 0595/2056] [light] Fix ambiguous set_effect overload for const char* (#14732) --- .../addressable_light/addressable_light_display.h | 2 +- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_call.h | 2 ++ tests/components/light/common.yaml | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 53f8604b7de..d9b8680547f 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { // - Save the current effect index. this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. - light_state_->make_call().set_effect(0).perform(); + light_state_->make_call().set_effect(uint32_t{0}).perform(); } } enabled_ = enabled; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 14cd0e92f69..cd45994f625 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -506,7 +506,7 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { LightCall &LightCall::set_effect(const char *effect, size_t len) { if (len == 4 && strncasecmp(effect, "none", 4) == 0) { - this->set_effect(0); + this->set_effect(uint32_t{0}); return *this; } diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 0926ab6108e..0eb1785239c 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -130,6 +130,8 @@ class LightCall { LightCall &set_effect(optional<std::string> effect); /// Set the effect of the light by its name. LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); } + /// Set the effect of the light by its name (const char * overload to resolve ambiguity). + LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); } /// Set the effect of the light by its name and length (zero-copy from API). LightCall &set_effect(const char *effect, size_t len); /// Set the effect of the light by its internal index number (only for internal use). diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index e5fab62a793..e1216e7b60b 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -60,6 +60,12 @@ esphome: } } + # Test set_effect with const char* doesn't cause ambiguous overload (issue #14728) + - lambda: |- + auto call = id(test_monochromatic_light).turn_on(); + call.set_effect("None"); + call.perform(); + - light.toggle: test_binary_light - light.turn_off: test_rgb_light - light.turn_on: From 7bb4e754591a4b3e56a6398dfeda1a0113e2ee81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:47:16 -1000 Subject: [PATCH 0596/2056] [rp2040] Use full flash for sketch in testing mode (#14747) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/rp2040/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 276187b273c..71e5f1488cb 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -203,7 +203,12 @@ async def to_code(config): cg.add_build_flag(f"-Wl,--wrap={symbol}") cg.add_platformio_option("board_build.core", "earlephilhower") - cg.add_platformio_option("board_build.filesystem_size", "1m") + # In testing mode, use all flash for sketch to allow linking grouped component tests. + # Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine + # many components that exceed the 1MB sketch partition. + cg.add_platformio_option( + "board_build.filesystem_size", "0m" if CORE.testing_mode else "1m" + ) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] cg.add_define( From 2ca13972b939a551278d6064c98d6660be69440a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:06 -1000 Subject: [PATCH 0597/2056] [debug] Fix missing reset reason for RP2040/RP2350 (#14740) --- esphome/components/debug/debug_rp2040.cpp | 64 +++++++++++++++++++++-- esphome/core/helpers.h | 22 ++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index c9d41942dbc..8dc84a26732 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -1,23 +1,81 @@ #include "debug_component.h" #ifdef USE_RP2040 +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include <Arduino.h> +#include <hardware/watchdog.h> +#if defined(PICO_RP2350) +#include <hardware/structs/powman.h> +#else +#include <hardware/structs/vreg_and_chip_reset.h> +#endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif namespace esphome { namespace debug { static const char *const TAG = "debug"; -const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + size_t pos = 0; + +#if defined(PICO_RP2350) + uint32_t chip_reset = powman_hw->chip_reset; + if (chip_reset & 0x04000000) // HAD_GLITCH_DETECT + pos = buf_append_str(buf, size, pos, "Power supply glitch|"); + if (chip_reset & 0x00040000) // HAD_RUN_LOW + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00020000) // HAD_BOR + pos = buf_append_str(buf, size, pos, "Brown-out|"); + if (chip_reset & 0x00010000) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#else + uint32_t chip_reset = vreg_and_chip_reset_hw->chip_reset; + if (chip_reset & 0x00010000) // HAD_RUN + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00000100) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#endif + + if (watchdog_caused_reboot()) { + bool handled = false; +#ifdef USE_RP2040_CRASH_HANDLER + if (rp2040::crash_handler_has_data()) { + pos = buf_append_str(buf, size, pos, "Crash (HardFault)|"); + handled = true; + } +#endif + if (!handled) { + if (watchdog_enable_caused_reboot()) { + pos = buf_append_str(buf, size, pos, "Watchdog timeout|"); + } else { + pos = buf_append_str(buf, size, pos, "Software reset|"); + } + } + } + + // Remove trailing '|' + if (pos > 0 && buf[pos - 1] == '|') { + buf[pos - 1] = '\0'; + } else if (pos == 0) { + return "Unknown"; + } + + return buf; +} const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); } +uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) { constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - uint32_t cpu_freq = rp2040.f_cpu(); + uint32_t cpu_freq = ::rp2040.f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 70ac1574f0c..b2517e2d7ac 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -942,6 +942,28 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, } #endif +/// Safely append a string to buffer without format parsing, returning new position (capped at size). +/// More efficient than buf_append_printf for plain string literals. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param str String to append (must not be null) +/// @return New position after appending (capped at size on overflow) +inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str) { + if (pos >= size) { + return size; + } + size_t remaining = size - pos - 1; // reserve space for null terminator + size_t len = strlen(str); + if (len > remaining) { + len = remaining; + } + memcpy(buf + pos, str, len); + pos += len; + buf[pos] = '\0'; + return pos; +} + /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. /// Maximum name length supported is 120 characters for friendly names. From e15b19b2237739c945010d8addb3108b06733219 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:29 -1000 Subject: [PATCH 0598/2056] [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/captive_portal/captive_portal.cpp | 4 ++-- esphome/components/web_server_base/web_server_base.cpp | 4 ++++ esphome/components/web_server_base/web_server_base.h | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 5af6ab29a2e..183f16c5f84 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -61,7 +61,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { // Defer save to main loop thread to avoid NVS operations from HTTP thread this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif - request->redirect(ESPHOME_F("/?save")); + request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting...")); } void CaptivePortal::setup() { @@ -71,7 +71,7 @@ void CaptivePortal::setup() { void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { - this->base_->add_handler(this); + this->base_->add_handler_without_auth(this); } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index dbbcd10d8df..3e1baf34bad 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -11,6 +11,10 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } #endif + this->add_handler_without_auth(handler); +} + +void WebServerBase::add_handler_without_auth(AsyncWebHandler *handler) { this->handlers_.push_back(handler); if (this->server_ != nullptr) { this->server_->addHandler(handler); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 54421c851e5..48e13ad71e2 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -122,6 +122,14 @@ class WebServerBase { #endif void add_handler(AsyncWebHandler *handler); + /** + * WARNING: Registers a handler that bypasses the USE_WEBSERVER_AUTH middleware. + * + * This should only be used for endpoints that are intentionally unauthenticated + * (for example, captive portal or very limited-status endpoints). For normal + * endpoints that should respect web server authentication, use add_handler(). + */ + void add_handler_without_auth(AsyncWebHandler *handler); void set_port(uint16_t port) { port_ = port; } uint16_t get_port() const { return port_; } From 89719cf4b2490e068c057db61f1dce782839f7d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:41 -1000 Subject: [PATCH 0599/2056] [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) --- .../template/water_heater/template_water_heater.cpp | 1 + tests/integration/test_water_heater_template.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 73081d204b4..092df6fdca3 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -26,6 +26,7 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { if (!this->supported_modes_.empty()) { traits.set_supported_modes(this->supported_modes_); + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_OPERATION_MODE); } traits.set_supports_current_temperature(true); diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index 096d4c84615..d63d1d69845 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -102,7 +102,11 @@ async def test_water_heater_template( f"Expected target temp 60.0, got {initial_state.target_temperature}" ) - # Verify supported features: away mode and on/off (fixture has away + is_on lambdas) + # Verify supported features: operation mode, away mode, and on/off + assert ( + test_water_heater.supported_features + & WaterHeaterFeature.SUPPORTS_OPERATION_MODE + ) != 0, "Expected SUPPORTS_OPERATION_MODE in supported_features" assert ( test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE ) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features" From 22b25724ae23dc19982baef702fad6061eb544c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:55 -1000 Subject: [PATCH 0600/2056] [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) --- esphome/components/wifi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2808d313111..480ccd65c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -166,6 +166,7 @@ TTLS_PHASE_2 = { } EAP_AUTH_SCHEMA = cv.All( + cv.only_on([Platform.ESP32, Platform.ESP8266]), cv.Schema( { cv.Optional(CONF_IDENTITY): cv.string_strict, From 7e8e085a040a946906085661253c8a8ba693f19f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:49:07 -1000 Subject: [PATCH 0601/2056] [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) --- esphome/components/light/light_call.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index cd45994f625..0b2d391fd6e 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -214,7 +214,14 @@ LightColorValues LightCall::validate_() { if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; this->set_flag_(FLAG_HAS_STATE); - this->brightness_ = 1.0f; + if (color_mode & ColorCapability::BRIGHTNESS) { + // Reset brightness so the light has nonzero brightness when turned back on. + this->brightness_ = 1.0f; + } else { + // Light doesn't support brightness; clear the flag to avoid a spurious + // "brightness not supported" warning during capability validation. + this->clear_flag_(FLAG_HAS_BRIGHTNESS); + } } // Set color brightness to 100% if currently zero and a color is set. From 59c1368440f13bdc6baa0f40a21edfd7f99d71db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:53:46 -1000 Subject: [PATCH 0602/2056] [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) --- esphome/components/i2c/__init__.py | 37 ++++++++++++++++++++++ esphome/components/i2c/i2c_bus_arduino.cpp | 10 +++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index de3f2be6740..1684f479ba3 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -93,11 +93,31 @@ def _bus_declare_type(value): raise NotImplementedError +def _rp2040_i2c_controller(pin): + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. + + See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): + https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + See RP2350 datasheet Table 7 (section 9.4, "Function Select"): + https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + """ + return (pin // 2) % 2 + + def validate_config(config): if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) + if CORE.is_rp2040: + sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) + scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) + if sda_controller != scl_controller: + raise cv.Invalid( + f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but " + f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. " + f"Both pins must be on the same I2C controller." + ) return config @@ -146,6 +166,23 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_rp2040: + if len(full_config) > 2: + raise cv.Invalid( + "The maximum number of I2C interfaces for RP2040/RP2350 is 2" + ) + if len(full_config) > 1: + controllers = [ + _rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config + ] + if len(set(controllers)) != len(controllers): + raise cv.Invalid( + "Multiple I2C buses are configured to use the same I2C controller. " + "Each bus must use pins on a different controller. " + "The I2C controller is determined by (gpio / 2) % 2: " + "even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, " + "odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1." + ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 5120eb4c007..47a06abe9ec 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -20,12 +20,14 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) - static bool first = true; - if (first) { + // Select Wire instance based on pin assignment, not definition order. + // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 + // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; - first = false; } else { - wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + wire_ = &Wire1; } #endif From a744261934717a3ab52c1e2ec252c0fd46c4ea8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:12:22 -1000 Subject: [PATCH 0603/2056] [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mdns/mdns_component.h | 4 +++ esphome/components/mdns/mdns_rp2040.cpp | 32 ++++++++++++++++++---- esphome/components/wifi/__init__.py | 7 ----- esphome/components/wifi/wifi_component.cpp | 14 ---------- esphome/components/wifi/wifi_component.h | 4 --- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 13c8ccf2884..47cad4bf71b 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -129,6 +129,10 @@ class MDNSComponent final : public Component { #endif #ifdef USE_MDNS_STORE_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; +#endif +#ifdef USE_RP2040 + bool was_connected_{false}; + bool initialized_{false}; #endif void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf); }; diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 05d991c1fad..c0b22aa84fb 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -36,12 +36,32 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV } void MDNSComponent::setup() { - this->setup_buffers_and_register_(register_rp2040); - // Schedule MDNS.update() via set_interval() instead of overriding loop(). - // This removes the component from the per-iteration loop list entirely, - // eliminating virtual dispatch overhead on every main loop cycle. - // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. - this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + // RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart + // mDNS when the network interface reconnects. However, stateUpCB() is stubbed out + // in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used + // schedule_function() which doesn't exist in arduino-pico, and the callback can't + // safely run directly since netif status callbacks fire from IRQ context + // (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets. + // + // Workaround: defer MDNS.begin() and service registration until the network is + // connected (has an IP), then call notifyAPChange() on subsequent reconnects to + // restart mDNS probing and announcing — all from main loop context so it's + // thread-safe. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (connected && !this->was_connected_) { + if (!this->initialized_) { + this->setup_buffers_and_register_(register_rp2040); + this->initialized_ = true; + } else { + MDNS.notifyAPChange(); + } + } + this->was_connected_ = connected; + if (this->initialized_) { + MDNS.update(); + } + }); } void MDNSComponent::on_shutdown() { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 480ccd65c54..9f73b1cc6f5 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -563,13 +563,6 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) - # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart - # mDNS when the network interface reconnects. However, this callback is disabled - # in the arduino-pico framework. As a workaround, we block component setup until - # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an - # active connection. This define enables the loop priority sorting infrastructure - # used during the setup blocking phase. - cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 60764955cc9..09f883ed617 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2109,20 +2109,6 @@ void WiFiComponent::retry_connect() { } } -#ifdef USE_RP2040 -// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart -// mDNS when the network interface reconnects. However, this callback is disabled -// in the arduino-pico framework. As a workaround, we block component setup until -// WiFi is connected, ensuring mDNS.begin() is called with an active connection. - -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; - } - return this->is_connected_(); -} -#endif - void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f340b708c90..883cc1344b4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -437,10 +437,6 @@ class WiFiComponent : public Component { void retry_connect(); -#ifdef USE_RP2040 - bool can_proceed() override; -#endif - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected() const { return this->connected_; } From 920af91db693e36ad54033db100ecbb3b2915c19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:37:46 -1000 Subject: [PATCH 0604/2056] [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) --- esphome/components/mdns/mdns_rp2040.cpp | 5 +++++ esphome/components/rp2040/crash_handler.cpp | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index c0b22aa84fb..88f707afd37 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -7,7 +7,12 @@ #include "esphome/core/log.h" #include "mdns_component.h" +// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty. +// Save and restore our definition around the include to avoid a redefinition warning. +#pragma push_macro("IRAM_ATTR") +#undef IRAM_ATTR #include <ESP8266mDNS.h> +#pragma pop_macro("IRAM_ATTR") namespace esphome::mdns { diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 1f579c2d18e..f9eb42a0f8a 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -57,14 +57,14 @@ static const char *const TAG = "rp2040.crash"; // Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). // The valid field is explicitly cleared in crash_handler_read_and_clear() instead. -static struct { +static struct CrashData { bool valid; uint32_t pc; uint32_t lr; uint32_t sp; uint32_t backtrace[MAX_BACKTRACE]; uint8_t backtrace_count; -} __attribute__((section(".noinit"))) s_crash_data; +} s_crash_data __attribute__((section(".noinit"))); bool crash_handler_has_data() { return s_crash_data.valid; } From 15ec46abfe2f5f93a77cfdac7b8891a726a329f0 Mon Sep 17 00:00:00 2001 From: Michael Kerscher <github@kerscher-michael.de> Date: Fri, 13 Mar 2026 06:31:16 +0100 Subject: [PATCH 0605/2056] [vbus] add DeltaSol CS4 (Citrin Solar 1.3) (#12477) --- esphome/components/vbus/__init__.py | 1 + .../components/vbus/binary_sensor/__init__.py | 41 +++++ .../vbus/binary_sensor/vbus_binary_sensor.cpp | 19 +++ .../vbus/binary_sensor/vbus_binary_sensor.h | 17 +++ esphome/components/vbus/sensor/__init__.py | 141 +++++++++++++++++- .../components/vbus/sensor/vbus_sensor.cpp | 46 ++++++ esphome/components/vbus/sensor/vbus_sensor.h | 35 +++++ tests/components/vbus/common.yaml | 8 + 8 files changed, 307 insertions(+), 1 deletion(-) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index 5790a9cce05..26634964569 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -19,6 +19,7 @@ CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" CONF_DELTASOL_BS2 = "deltasol_bs2" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" +CONF_DELTASOL_CS4 = "deltasol_cs4" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index 70dda943007..85f1172166f 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -20,6 +20,7 @@ from .. import ( CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, CONF_DELTASOL_CS2, + CONF_DELTASOL_CS4, CONF_DELTASOL_CS_PLUS, CONF_VBUS_ID, VBus, @@ -31,6 +32,7 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) +DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component) VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component) @@ -186,6 +188,28 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS4), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), @@ -350,6 +374,23 @@ async def to_code(config): sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_CS4: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1122)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x2211)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index c1d7bc1b18d..e598b1de6b7 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -110,6 +110,25 @@ void DeltaSolCS2BSensor::handle_message(std::vector<uint8_t> &message) { this->s4_error_bsensor_->publish_state(message[18] & 8); } +void DeltaSolCS4BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS4:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolCS4BSensor::handle_message(std::vector<uint8_t> &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[20] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[20] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[20] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[20] & 8); +} + void DeltaSolCSPlusBSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 2decdde602a..04c9a7b8264 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -94,6 +94,23 @@ class DeltaSolCS2BSensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolCS4BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class DeltaSolCSPlusBSensor : public VBusListener, public Component { public: void dump_config() override; diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index ff8ef98a1aa..9c3665eb1c7 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -36,6 +36,7 @@ from .. import ( CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, CONF_DELTASOL_CS2, + CONF_DELTASOL_CS4, CONF_DELTASOL_CS_PLUS, CONF_VBUS_ID, VBus, @@ -47,6 +48,7 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) +DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component) VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component) @@ -438,6 +440,99 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_CS4), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema( + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus), @@ -734,7 +829,51 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) - if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: + elif config[CONF_MODEL] == CONF_DELTASOL_CS4: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x1122)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_TEMPERATURE_5 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5]) + cg.add(var.set_temperature5_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + if CONF_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_FLOW_RATE]) + cg.add(var.set_flow_rate_sensor(sens)) + + elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x2211)) cg.add(var.set_dest(0x0010)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index 75c9ea1aeed..1cabb49703b 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -168,6 +168,52 @@ void DeltaSolCS2Sensor::handle_message(std::vector<uint8_t> &message) { this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f); } +void DeltaSolCS4Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol CS4:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Temperature 5", this->temperature5_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); + LOG_SENSOR(" ", "Flow Rate", this->flow_rate_sensor_); +} + +void DeltaSolCS4Sensor::handle_message(std::vector<uint8_t> &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->temperature5_sensor_ != nullptr) + this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[12]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) + this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 22)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f); + if (this->flow_rate_sensor_ != nullptr) + this->flow_rate_sensor_->publish_state(get_u16(message, 38)); +} + void DeltaSolCSPlusSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol CS Plus:"); LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index cea2ee1c862..ea248b1db24 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -122,6 +122,41 @@ class DeltaSolCS2Sensor : public VBusListener, public Component { void handle_message(std::vector<uint8_t> &message) override; }; +class DeltaSolCS4Sensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *temperature5_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + sensor::Sensor *flow_rate_sensor_{nullptr}; + + void handle_message(std::vector<uint8_t> &message) override; +}; + class DeltaSolCSPlusSensor : public VBusListener, public Component { public: void dump_config() override; diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index 5c771be922b..bdd75a2b975 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -19,6 +19,10 @@ binary_sensor: name: BS2 Sensor 3 Error sensor4_error: name: BS2 Sensor 4 Error + - platform: vbus + model: deltasol_cs4 + sensor1_error: + name: "DeltaSol CS4 Sensor 1 Error" - platform: vbus model: custom command: 0x100 @@ -44,6 +48,10 @@ sensor: name: DeltaSol C Heat Quantity time: name: DeltaSol C System Time + - platform: vbus + model: deltasol_cs4 + temperature_1: + name: "DeltaSol CS4 Temperature 1" - platform: vbus model: deltasol_bs2 temperature_1: From 7524590bcfe3fb3e28cdaf4d33b9f9f1a41201f2 Mon Sep 17 00:00:00 2001 From: Thomas SAMTER <7680607+P4uLT@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:17:11 +0100 Subject: [PATCH 0606/2056] [const] Add CONF_CLIMATE_ID for climate component sub-entities (#14764) --- esphome/components/const/__init__.py | 1 + esphome/components/pid/sensor/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 059bf3f26a9..f6da32569fe 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,6 +3,7 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +CONF_CLIMATE_ID = "climate_id" BYTE_ORDER_LITTLE = "little_endian" BYTE_ORDER_BIG = "big_endian" diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index 4547f4d7082..d26e88e38ae 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import CONF_CLIMATE_ID import esphome.config_validation as cv from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT @@ -21,7 +22,6 @@ PID_CLIMATE_SENSOR_TYPES = { "KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD, } -CONF_CLIMATE_ID = "climate_id" CONFIG_SCHEMA = ( sensor.sensor_schema( PIDClimateSensor, From 326769e43c8c85f6bcb8001301028f7be805acf8 Mon Sep 17 00:00:00 2001 From: Kjell Braden <afflux@pentabarf.de> Date: Fri, 13 Mar 2026 14:18:42 +0100 Subject: [PATCH 0607/2056] [runtime_image] fix BMP parsing (#14762) --- .../components/runtime_image/bmp_decoder.h | 4 + .../fixtures/online_image_bmp.yaml | 27 ++++ tests/integration/test_online_image_bmp.py | 119 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 tests/integration/fixtures/online_image_bmp.yaml create mode 100644 tests/integration/test_online_image_bmp.py diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index 73e54f54302..a52a5615849 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -26,6 +26,10 @@ class BmpDecoder : public ImageDecoder { int HOT decode(uint8_t *buffer, size_t size) override; bool is_finished() const override { + if (this->bits_per_pixel_ == 0) { + // header not yet received, so dimensions not yet determined + return false; + } // BMP is finished when we've decoded all pixel data return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_); } diff --git a/tests/integration/fixtures/online_image_bmp.yaml b/tests/integration/fixtures/online_image_bmp.yaml new file mode 100644 index 00000000000..e36514e9ae2 --- /dev/null +++ b/tests/integration/fixtures/online_image_bmp.yaml @@ -0,0 +1,27 @@ +esphome: + name: online-image-bmp + +host: + +http_request: + +display: + +online_image: + - url: http://127.0.0.1:HTTP_PORT/foo.bmp + id: myimg + format: BMP + type: RGB + on_download_finished: + logger.log: + format: "download finished. cache hit: %u" + args: [cached] + +api: + actions: + - action: fetch_image + then: + - component.update: myimg + +logger: + level: DEBUG diff --git a/tests/integration/test_online_image_bmp.py b/tests/integration/test_online_image_bmp.py new file mode 100644 index 00000000000..7c32154fdd6 --- /dev/null +++ b/tests/integration/test_online_image_bmp.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# black 8x8 RGB BMP, generated with +# from PIL import Image +# from io import BytesIO +# b = BytesIO() +# img = Image.new("RGB", (8, 8)) +# img.save(b, format="BMP") +# b.getvalue() +BMP_IMAGE = b"BM\xf6\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xc0\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +LEN_BMP_IMAGE = len(BMP_IMAGE) + + +def handle_http(http_request_future): + async def handler(reader, writer): + try: + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n") + + # ensure our request matches the expectation + expected_request = b"GET /foo.bmp HTTP/1.1\r\n" + assert data[: len(expected_request)] == expected_request + + # consume rest of request + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n\r\n") + + http_request_future.set_result(True) + + http_response = [ + b"HTTP/1.1 200 OK", + b"Content-Length: %d" % LEN_BMP_IMAGE, + b"Content-Type: text/plain", + b"Connection: close", + b"", + b"", + ] + writer.write(b"\r\n".join(http_response)) + await writer.drain() + + writer.write(BMP_IMAGE) + + await writer.drain() + except Exception as exc: + if not http_request_future.done(): + http_request_future.set_exception(exc) + raise + finally: + writer.close() + + return handler + + +@pytest.mark.asyncio +async def test_online_image_bmp( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Esphome shouldn't block the main loop when a http response is slow""" + loop = asyncio.get_running_loop() + + # Track http request + http_request_future = loop.create_future() + download_finished_future = loop.create_future() + downloaded_bytes_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + + if match := re.search(r"Image fully downloaded, (\d+) bytes", line): + downloaded_bytes_future.set_result(int(match.group(1))) + + if "download finished" in line: + download_finished_future.set_result(True) + + server = await asyncio.start_server( + handle_http(http_request_future), "127.0.0.1", 0 + ) + http_server_port = server.sockets[0].getsockname()[1] + + config = yaml_config.replace("HTTP_PORT", str(http_server_port)) + + # Run with log monitoring + async with ( + server, + run_compiled(config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "online-image-bmp" + + # List services to find our test service + _, services = await client.list_entities_services() + + # Find test service + request_service = next((s for s in services if s.name == "fetch_image"), None) + + assert request_service is not None, "fetch_image service not found" + + await client.execute_service(request_service, {}) + + async with asyncio.timeout(0.1): + await http_request_future + + async with asyncio.timeout(0.5): + numbytes = await downloaded_bytes_future + assert numbytes == LEN_BMP_IMAGE + await download_finished_future From 5920fa97e4b671e424f7d53cbbcb1bd1ba8d250c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 03:20:50 -1000 Subject: [PATCH 0608/2056] [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) --- esphome/components/select/select_call.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 45fb42c1160..83f5052fc8a 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -41,7 +41,7 @@ SelectCall &SelectCall::with_index(size_t index) { this->operation_ = SELECT_OP_SET; if (index >= this->parent_->size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); - this->index_ = {}; // Store nullopt for invalid index + this->index_ = nullopt; // Store nullopt for invalid index } else { this->index_ = index; } @@ -52,7 +52,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { ESP_LOGW(TAG, "'%s' - Select has no options", name); - return {}; + return nullopt; } if (this->operation_ == SELECT_OP_FIRST) { @@ -67,7 +67,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { ESP_LOGD(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); - return {}; + return nullopt; } return this->index_; } @@ -96,7 +96,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { return active_index + 1; } - return {}; // Can't navigate further without cycling + return nullopt; // Can't navigate further without cycling } void SelectCall::perform() { From 8936be628f4cd4d223aeefa6bed333682073db84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:30 -1000 Subject: [PATCH 0609/2056] [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) --- esphome/components/api/api_frame_helper.h | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 98de24501ea..5e07ad43a93 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -134,12 +134,16 @@ class APIFrameHelper { // // For log messages: Use Nagle to coalesce multiple small log packets into // fewer larger packets, reducing WiFi overhead. However, we limit batching - // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained - // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into - // shared pbufs, but holding data too long waiting for Nagle's timer causes - // buffer exhaustion and dropped messages. + // to avoid excessive LWIP buffer pressure on memory-constrained devices. + // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but + // holding data too long waiting for Nagle's timer causes buffer exhaustion + // and dropped messages. // - // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle + // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers) + // + // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { @@ -150,7 +154,7 @@ class APIFrameHelper { return; } - // Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd) + // Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush) if (this->nodelay_state_ == NODELAY_ON) { this->set_nodelay_raw_(false); this->nodelay_state_ = 1; @@ -255,10 +259,16 @@ class APIFrameHelper { uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1-2 count log messages in the current Nagle batch. + // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. + // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. static constexpr int8_t NODELAY_ON = -1; +#ifdef USE_ESP8266 static constexpr int8_t LOG_NAGLE_COUNT = 2; +#else + static constexpr int8_t LOG_NAGLE_COUNT = 3; +#endif int8_t nodelay_state_{NODELAY_ON}; // Internal helper to set TCP_NODELAY socket option From bd844fcd0aa0cf9857f4a548d58543b7fe020331 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:44 -1000 Subject: [PATCH 0610/2056] [template] Fix misleading 'Text value too long to save' warning (#14753) --- .../components/template/text/template_text.h | 32 ++--- .../fixtures/template_text_save.yaml | 23 +++ tests/integration/test_template_text_save.py | 131 ++++++++++++++++++ 3 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tests/integration/fixtures/template_text_save.yaml create mode 100644 tests/integration/test_template_text_save.py diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 7f176db09ef..229a61d9b8e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -24,23 +24,23 @@ class TemplateTextSaverBase { template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { public: bool save(const std::string &value) override { - int diff = value.compare(this->prev_); - if (diff != 0) { - // If string is bigger than the allocation, do not save it. - // We don't need to waste ram setting prev_value either. - int size = value.size(); - if (size <= SZ) { - // Make it into a length prefixed thing - unsigned char temp[SZ + 1]; - memcpy(temp + 1, value.c_str(), size); - // SZ should be pre checked at the schema level, it can't go past the char range. - temp[0] = ((unsigned char) size); - this->pref_.save(&temp); - this->prev_.assign(value); - return true; - } + if (value == this->prev_) { + return true; // No change, nothing to save } - return false; + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = value.size(); + if (size > SZ) { + return false; + } + // Make it into a length prefixed thing + unsigned char temp[SZ + 1]; + memcpy(temp + 1, value.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->pref_.save(&temp); + this->prev_.assign(value); + return true; } // Make the preference object. Fill the provided location with the saved data diff --git a/tests/integration/fixtures/template_text_save.yaml b/tests/integration/fixtures/template_text_save.yaml new file mode 100644 index 00000000000..526561732de --- /dev/null +++ b/tests/integration/fixtures/template_text_save.yaml @@ -0,0 +1,23 @@ +esphome: + name: host-template-text-save-test + +host: + +api: + batch_delay: 0ms + +logger: + +preferences: + flash_write_interval: 0s + +text: + - platform: template + name: "Test Text Restore" + id: test_text_restore + optimistic: true + min_length: 0 + max_length: 10 + mode: text + initial_value: "hello" + restore_value: true diff --git a/tests/integration/test_template_text_save.py b/tests/integration/test_template_text_save.py new file mode 100644 index 00000000000..47c8e3188ab --- /dev/null +++ b/tests/integration/test_template_text_save.py @@ -0,0 +1,131 @@ +"""Integration test for template text restore_value persistence. + +Tests that: +1. A template text with restore_value saves its value to preferences +2. The saved value persists across restarts (binary re-run) +3. Setting the same value again does not produce a spurious "too long" warning +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import socket +from typing import Any + +from aioesphomeapi import TextInfo, TextState +import pytest + +from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client +from .state_utils import InitialStateHelper, require_entity +from .types import CompileFunction, ConfigWriter + + +@pytest.mark.asyncio +async def test_template_text_save( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> None: + """Test template text save/restore persistence and duplicate-save behavior.""" + port, port_socket = reserved_tcp_port + + # Clean up any stale preference file from previous runs + prefs_file = ( + Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs" + ) + if prefs_file.exists(): + prefs_file.unlink() + + # Write and compile once + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # Release the reserved port so the binary can bind to it + port_socket.close() + + # --- First run: set a value and verify no spurious warnings --- + warning_lines: list[str] = [] + + def capture_warnings(line: str) -> None: + if "too long to save" in line.lower(): + warning_lines.append(line) + + async with ( + run_binary_and_wait_for_port( + binary_path, "127.0.0.1", port, line_callback=capture_warnings + ), + wait_and_connect_api_client(port=port) as client, + ): + device_info = await client.device_info() + assert device_info.name == "host-template-text-save-test" + + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + # Set up state tracking + loop = asyncio.get_running_loop() + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + # Verify initial value from config + initial = initial_state_helper.initial_states[text_entity.key] + assert isinstance(initial, TextState) + assert initial.state == "hello" + + async def wait_for_state(key: int, timeout: float = 2.0) -> Any: + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Set a new value that fits within max_length + client.text_command(key=text_entity.key, state="world") + state = await wait_for_state(text_entity.key) + assert state.state == "world" + + # Set the same value again - should NOT produce "too long" warning + client.text_command(key=text_entity.key, state="world") + # Give time for the warning to appear (if any) + await asyncio.sleep(0.5) + + # No warnings should have appeared + assert warning_lines == [], ( + f"Unexpected 'too long to save' warning(s): {warning_lines}" + ) + + # --- Second run: verify the value was restored from preferences --- + async with ( + run_binary_and_wait_for_port(binary_path, "127.0.0.1", port), + wait_and_connect_api_client(port=port) as client, + ): + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None)) + await initial_state_helper.wait_for_initial_states() + + # The value should be "world" - restored from preferences + restored = initial_state_helper.initial_states[text_entity.key] + assert isinstance(restored, TextState) + assert restored.state == "world", ( + f"Expected restored value 'world', got '{restored.state}'" + ) + + # Clean up preference file + if prefs_file.exists(): + prefs_file.unlink() From b147830ef954ed5d6c012a041c5b20ea7d88f93b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:24:39 -0400 Subject: [PATCH 0611/2056] [core] Fix std::isnan conflict with picolibc on ESP-IDF 6.0 (#14768) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/core/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index d4a839cb795..e112720f2b4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -589,7 +589,10 @@ async def _add_looping_components() -> None: async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) # These can be used by user lambdas, put them to default scope + # picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan + cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__")) cg.add_global(cg.RawExpression("using std::isnan")) + cg.add_global(cg.RawStatement("#endif")) cg.add_global(cg.RawExpression("using std::min")) cg.add_global(cg.RawExpression("using std::max")) From 6700347a4894ca991a7176bac7a6227afd3289e3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:47:12 -0400 Subject: [PATCH 0612/2056] [wifi] Fix ESP-IDF 6.0 compatibility (#14766) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 2 +- .../wifi/wifi_component_esp_idf.cpp | 44 +++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 09f883ed617..346276692a1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -6,7 +6,7 @@ #include <type_traits> #ifdef USE_ESP32 -#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include <esp_eap_client.h> #else #include <esp_wpa2.h> diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 883cc1344b4..aeb32352a97 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -18,7 +18,7 @@ #endif #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include <esp_eap_client.h> #else #include <esp_wpa2.h> diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index eca3f192490..2866ec15137 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -17,7 +17,7 @@ #include <memory> #include <utility> #ifdef USE_WIFI_WPA2_EAP -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include <esp_eap_client.h> #else #include <esp_wpa2.h> @@ -75,7 +75,11 @@ struct IDFWiFiEvent { #if USE_NETWORK_IPV6 ip_event_got_ip6_t ip_got_ip6; #endif /* USE_NETWORK_IPV6 */ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + ip_event_assigned_ip_to_client_t ip_assigned_ip_to_client; +#else ip_event_ap_staipassigned_t ip_ap_staipassigned; +#endif } data; }; @@ -116,8 +120,13 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t)); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) { memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t)); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + } else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) { + memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t)); +#else } else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) { memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t)); +#endif } else { // did not match any event, don't send anything return; @@ -407,7 +416,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. EAPAuth eap = *eap_opt; -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); #else err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); @@ -419,7 +428,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { int client_cert_len = strlen(eap.client_cert); int client_key_len = strlen(eap.client_key); if (ca_cert_len) { -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); #else err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); @@ -432,7 +441,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // validation is not required as the config tool has already validated it if (client_cert_len && client_key_len) { // if we have certs, this must be EAP-TLS -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1, (uint8_t *) eap.client_key, client_key_len + 1, (uint8_t *) eap.password.c_str(), eap.password.length()); @@ -446,7 +455,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } } else { // in the absence of certs, assume this is username/password based -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); #else err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); @@ -454,7 +463,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (err != ESP_OK) { ESP_LOGV(TAG, "set_username failed %d", err); } -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); #else err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); @@ -463,7 +472,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "set_password failed %d", err); } // set TTLS Phase 2, defaults to MSCHAPV2 -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2); #else err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2); @@ -472,7 +481,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err); } } -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_wifi_sta_enterprise_enable(); #else err = esp_wifi_sta_wpa2_ent_enable(); @@ -628,14 +637,26 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Auth Expired"; case WIFI_REASON_AUTH_LEAVE: return "Auth Leave"; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY: + return "Disassociated Due to Inactivity"; +#else case WIFI_REASON_ASSOC_EXPIRE: return "Association Expired"; +#endif case WIFI_REASON_ASSOC_TOOMANY: return "Too Many Associations"; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + case WIFI_REASON_CLASS2_FRAME_FROM_NONAUTH_STA: + return "Class 2 Frame from Non-Authenticated STA"; + case WIFI_REASON_CLASS3_FRAME_FROM_NONASSOC_STA: + return "Class 3 Frame from Non-Associated STA"; +#else case WIFI_REASON_NOT_AUTHED: return "Not Authenticated"; case WIFI_REASON_NOT_ASSOCED: return "Not Associated"; +#endif case WIFI_REASON_ASSOC_LEAVE: return "Association Leave"; case WIFI_REASON_ASSOC_NOT_AUTHED: @@ -688,7 +709,7 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association comeback time too long"; case WIFI_REASON_SA_QUERY_TIMEOUT: return "SA query timeout"; -#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: return "No AP found with compatible security"; case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: @@ -917,8 +938,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf); #endif +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) { + const auto &it = data->data.ip_assigned_ip_to_client; +#else } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; +#endif ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } From f41aa8b18c739f98a50f38f6e8cfbca945e04c1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:35:10 +0000 Subject: [PATCH 0613/2056] Bump ruff from 0.15.5 to 0.15.6 (#14774) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d8f6983959..5e2bfe09cee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.5 + rev: v0.15.6 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 93a20896aa8..acd8383a2fc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.5 # also change in .pre-commit-config.yaml when updating +ruff==0.15.6 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From a6c08576be6e87df3007d0c7cc11e780377604a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 10:17:40 -1000 Subject: [PATCH 0614/2056] [sensor] Use FixedRingBuffer in SlidingWindowFilter, add window_size limit (#14736) --- esphome/components/sensor/__init__.py | 34 +++---- esphome/components/sensor/filter.cpp | 34 ++----- esphome/components/sensor/filter.h | 33 +++---- esphome/core/helpers.h | 131 +++++++++++++++++++++++++- 4 files changed, 171 insertions(+), 61 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 4be6ed1b841..64d4dc41778 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -403,9 +403,9 @@ async def filter_out_filter_to_code(config, filter_id): QUANTILE_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, } ), @@ -427,9 +427,9 @@ async def quantile_filter_to_code(config, filter_id): MEDIAN_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), } ), validate_send_first_at, @@ -449,9 +449,9 @@ async def median_filter_to_code(config, filter_id): MIN_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), } ), validate_send_first_at, @@ -483,9 +483,9 @@ async def min_filter_to_code(config, filter_id): MAX_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), } ), validate_send_first_at, @@ -509,9 +509,9 @@ async def max_filter_to_code(config, filter_id): SLIDING_AVERAGE_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_WINDOW_SIZE, default=15): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), } ), validate_send_first_at, @@ -540,8 +540,8 @@ EXPONENTIAL_AVERAGE_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float, - cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), } ), validate_send_first_at, diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 0fe1effe179..d995ee4111c 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -41,26 +41,14 @@ void Filter::initialize(Sensor *parent, Filter *next) { } // SlidingWindowFilter -SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at) - : window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) { - // Allocate ring buffer once at initialization +SlidingWindowFilter::SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at) { this->window_.init(window_size); } optional<float> SlidingWindowFilter::new_value(float value) { - // Add value to ring buffer - if (this->window_count_ < this->window_size_) { - // Buffer not yet full - just append - this->window_.push_back(value); - this->window_count_++; - } else { - // Buffer full - overwrite oldest value (ring buffer) - this->window_[this->window_head_] = value; - this->window_head_++; - if (this->window_head_ >= this->window_size_) { - this->window_head_ = 0; - } - } + // Add value to ring buffer (overwrites oldest when full) + this->window_.push_overwrite(value); // Check if we should send a result if (++this->send_at_ >= this->send_every_) { @@ -77,9 +65,8 @@ FixedVector<float> SortedWindowFilter::get_window_values_() { // Copy window without NaN values using FixedVector (no heap allocation) // Returns unsorted values - caller will use std::nth_element for partial sorting as needed FixedVector<float> values; - values.init(this->window_count_); - for (size_t i = 0; i < this->window_count_; i++) { - float v = this->window_[i]; + values.init(this->window_.size()); + for (float v : this->window_) { if (!std::isnan(v)) { values.push_back(v); } @@ -150,8 +137,7 @@ float MaxFilter::compute_result() { return this->find_extremum_<std::greater<flo float SlidingWindowMovingAverageFilter::compute_result() { float sum = 0; size_t valid_count = 0; - for (size_t i = 0; i < this->window_count_; i++) { - float v = this->window_[i]; + for (float v : this->window_) { if (!std::isnan(v)) { sum += v; valid_count++; @@ -161,7 +147,7 @@ float SlidingWindowMovingAverageFilter::compute_result() { } // ExponentialMovingAverageFilter -ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at) +ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at) : alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {} optional<float> ExponentialMovingAverageFilter::new_value(float value) { if (!std::isnan(value)) { @@ -183,7 +169,7 @@ optional<float> ExponentialMovingAverageFilter::new_value(float value) { } return {}; } -void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; } void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; } // ThrottleAverageFilter @@ -511,7 +497,7 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) { } // StreamingFilter (base class) -StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at) +StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at) : window_size_(window_size), send_first_at_(send_first_at) {} optional<float> StreamingFilter::new_value(float value) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 8bfcdb37cfb..6a76bd373e3 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -52,7 +52,7 @@ class Filter { */ class SlidingWindowFilter : public Filter { public: - SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at); + SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at); optional<float> new_value(float value) final; @@ -60,14 +60,10 @@ class SlidingWindowFilter : public Filter { /// Called by new_value() to compute the filtered result from the current window virtual float compute_result() = 0; - /// Access the sliding window values (ring buffer implementation) - /// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; } - FixedVector<float> window_; - size_t window_head_{0}; ///< Index where next value will be written - size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_) - size_t window_size_; ///< Maximum window size - size_t send_every_; ///< Send result every N values - size_t send_at_; ///< Counter for send_every + /// Sliding window ring buffer - automatically overwrites oldest values when full + FixedRingBuffer<float> window_; + uint16_t send_every_; ///< Send result every N values + uint16_t send_at_; ///< Counter for send_every }; /** Base class for Min/Max filters. @@ -84,8 +80,7 @@ class MinMaxFilter : public SlidingWindowFilter { template<typename Compare> float find_extremum_() { float result = NAN; Compare comp; - for (size_t i = 0; i < this->window_count_; i++) { - float v = this->window_[i]; + for (float v : this->window_) { if (!std::isnan(v)) { result = std::isnan(result) ? v : (comp(v, result) ? v : result); } @@ -239,18 +234,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter { */ class ExponentialMovingAverageFilter : public Filter { public: - ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at); + ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at); optional<float> new_value(float value) override; - void set_send_every(size_t send_every); + void set_send_every(uint16_t send_every); void set_alpha(float alpha); protected: float accumulator_{NAN}; float alpha_; - size_t send_every_; - size_t send_at_; + uint16_t send_every_; + uint16_t send_at_; bool first_value_{true}; }; @@ -570,7 +565,7 @@ class ToNTCTemperatureFilter : public Filter { */ class StreamingFilter : public Filter { public: - StreamingFilter(size_t window_size, size_t send_first_at); + StreamingFilter(uint16_t window_size, uint16_t send_first_at); optional<float> new_value(float value) final; @@ -584,9 +579,9 @@ class StreamingFilter : public Filter { /// Called by new_value() to reset internal state after sending a result virtual void reset_batch() = 0; - size_t window_size_; - size_t count_{0}; - size_t send_first_at_; + uint16_t window_size_; + uint16_t count_{0}; + uint16_t send_first_at_; bool first_send_{true}; }; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b2517e2d7ac..9828df29cb4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -301,7 +301,7 @@ template<typename T, size_t N> class StaticVector { /// Not thread-safe. All access (push/pop/iteration) must occur from a single /// context, or the caller must provide external synchronization. template<typename T, size_t N> class StaticRingBuffer { - using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>; + using index_type = std::conditional_t<(N <= std::numeric_limits<uint8_t>::max()), uint8_t, uint16_t>; public: class Iterator { @@ -356,6 +356,13 @@ template<typename T, size_t N> class StaticRingBuffer { index_type size() const { return this->count_; } bool empty() const { return this->count_ == 0; } + /// Clear all elements (reset to empty) + void clear() { + this->head_ = 0; + this->tail_ = 0; + this->count_ = 0; + } + Iterator begin() { return Iterator(this, 0); } Iterator end() { return Iterator(this, this->count_); } ConstIterator begin() const { return ConstIterator(this, 0); } @@ -368,6 +375,128 @@ template<typename T, size_t N> class StaticRingBuffer { index_type count_{0}; }; +/// Fixed-capacity circular buffer - allocates once at runtime, never reallocates. +/// Runtime-sized equivalent of StaticRingBuffer - use when capacity is only known at initialization. +/// Supports FIFO push/pop and iteration over queued elements. +/// Not thread-safe. +template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> class FixedRingBuffer { + using index_type = std::conditional_t< + (MAX_CAPACITY <= std::numeric_limits<uint8_t>::max()), uint8_t, + std::conditional_t<(MAX_CAPACITY <= std::numeric_limits<uint16_t>::max()), uint16_t, uint32_t>>; + + public: + class Iterator { + public: + Iterator(FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + T &operator*() { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; } + Iterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const Iterator &other) const { return pos_ != other.pos_; } + + private: + FixedRingBuffer *buf_; + index_type pos_; + }; + + class ConstIterator { + public: + ConstIterator(const FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {} + const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; } + ConstIterator &operator++() { + ++pos_; + return *this; + } + bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; } + + private: + const FixedRingBuffer *buf_; + index_type pos_; + }; + + FixedRingBuffer() = default; + ~FixedRingBuffer() { + if constexpr (std::is_trivial<T>::value) { + ::operator delete(this->data_); + } else { + delete[] this->data_; + } + } + + // Disable copy + FixedRingBuffer(const FixedRingBuffer &) = delete; + FixedRingBuffer &operator=(const FixedRingBuffer &) = delete; + + /// Allocate capacity - can only be called once + void init(index_type capacity) { + if constexpr (std::is_trivial<T>::value) { + // Raw allocation without initialization (elements are written before read) + // NOLINTNEXTLINE(bugprone-sizeof-expression) + this->data_ = static_cast<T *>(::operator new(capacity * sizeof(T))); + } else { + this->data_ = new T[capacity]; + } + this->capacity_ = capacity; + } + + /// Push a value. Returns false if full. + bool push(const T &value) { + if (this->count_ >= this->capacity_) + return false; + this->data_[this->tail_] = value; + this->tail_ = (this->tail_ + 1) % this->capacity_; + ++this->count_; + return true; + } + + /// Push a value, overwriting the oldest if full. + void push_overwrite(const T &value) { + this->data_[this->tail_] = value; + this->tail_ = (this->tail_ + 1) % this->capacity_; + if (this->count_ >= this->capacity_) { + // Buffer full - advance head to drop oldest, count stays at capacity + this->head_ = this->tail_; + } else { + ++this->count_; + } + } + + /// Remove the oldest element. + void pop() { + if (this->count_ > 0) { + this->head_ = (this->head_ + 1) % this->capacity_; + --this->count_; + } + } + + T &front() { return this->data_[this->head_]; } + const T &front() const { return this->data_[this->head_]; } + index_type size() const { return this->count_; } + bool empty() const { return this->count_ == 0; } + index_type capacity() const { return this->capacity_; } + bool full() const { return this->count_ == this->capacity_; } + + /// Clear all elements (reset to empty, keep capacity) + void clear() { + this->head_ = 0; + this->tail_ = 0; + this->count_ = 0; + } + + Iterator begin() { return Iterator(this, 0); } + Iterator end() { return Iterator(this, this->count_); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator end() const { return ConstIterator(this, this->count_); } + + protected: + T *data_{nullptr}; + index_type head_{0}; + index_type tail_{0}; + index_type count_{0}; + index_type capacity_{0}; +}; + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time From 1eed1adfa0314bb53b3716ea59187b49aa40a036 Mon Sep 17 00:00:00 2001 From: Thomas SAMTER <7680607+P4uLT@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:38:45 +0100 Subject: [PATCH 0615/2056] [pid] Replace std::deque with FixedRingBuffer (#14733) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/pid/climate.py | 24 ++++++++++------- esphome/components/pid/pid_climate.h | 10 ++++++- esphome/components/pid/pid_controller.cpp | 32 +++++++++++------------ esphome/components/pid/pid_controller.h | 24 ++++++++++------- 4 files changed, 53 insertions(+), 37 deletions(-) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 0e66b676377..18e33b80391 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_, cv.Optional( CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1 - ): cv.int_, + ): cv.positive_not_null_int, } ), cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema( @@ -68,8 +68,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STARTING_INTEGRAL_TERM, default=0.0): cv.float_, cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, - cv.Optional(CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1): cv.int_, - cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_, + cv.Optional( + CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1 + ): cv.positive_not_null_int, + cv.Optional( + CONF_OUTPUT_AVERAGING_SAMPLES, default=1 + ): cv.positive_not_null_int, } ), } @@ -102,13 +106,15 @@ async def to_code(config): cg.add(var.set_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM])) cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES])) - cg.add(var.set_output_samples(params[CONF_OUTPUT_AVERAGING_SAMPLES])) + output_samples = params[CONF_OUTPUT_AVERAGING_SAMPLES] + cg.add(var.set_output_samples(output_samples)) if CONF_MIN_INTEGRAL in params: cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL])) if CONF_MAX_INTEGRAL in params: cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL])) + deadband_output_samples = 1 if CONF_DEADBAND_PARAMETERS in config: params = config[CONF_DEADBAND_PARAMETERS] cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW])) @@ -116,11 +122,11 @@ async def to_code(config): cg.add(var.set_kp_multiplier(params[CONF_KP_MULTIPLIER])) cg.add(var.set_ki_multiplier(params[CONF_KI_MULTIPLIER])) cg.add(var.set_kd_multiplier(params[CONF_KD_MULTIPLIER])) - cg.add( - var.set_deadband_output_samples( - params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES] - ) - ) + deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES] + cg.add(var.set_deadband_output_samples(deadband_output_samples)) + + # Single shared output buffer sized to max of both modes + cg.add(var.init_output_buffer(max(output_samples, deadband_output_samples))) cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index dc0a92efed5..3708c29ff1b 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -28,7 +28,11 @@ class PIDClimate : public climate::Climate, public Component { void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; } void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; } void set_output_samples(int in) { controller_.output_samples_ = in; } - void set_derivative_samples(int in) { controller_.derivative_samples_ = in; } + void set_derivative_samples(int in) { + controller_.derivative_samples_ = in; + if (in > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits) + controller_.derivative_window_.init(in); + } void set_threshold_low(float in) { controller_.threshold_low_ = in; } void set_threshold_high(float in) { controller_.threshold_high_ = in; } @@ -38,6 +42,10 @@ class PIDClimate : public climate::Climate, public Component { void set_starting_integral_term(float in) { controller_.set_starting_integral_term(in); } void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = in; } + void init_output_buffer(int size) { + if (size > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits) + controller_.output_window_.init(size); + } float get_output_value() const { return output_value_; } float get_error_value() const { return controller_.error_; } diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 5d7aecdb052..cab15331cde 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) { // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; - // smooth/sample the output + // smooth/sample the output using shared buffer with mode-appropriate sample count int samples = in_deadband() ? deadband_output_samples_ : output_samples_; - return weighted_average_(output_list_, output, samples); + return ring_buffer_average_(output_window_, output, samples); } bool PIDController::in_deadband() { @@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) { previous_setpoint_ = setpoint; // smooth the derivative samples - derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); + derivative = ring_buffer_average_(derivative_window_, derivative, derivative_samples_); derivative_term_ = kd_ * derivative; @@ -93,25 +93,23 @@ void PIDController::calculate_derivative_term_(float setpoint) { } } -float PIDController::weighted_average_(std::deque<float> &list, float new_value, int samples) { - // if only 1 sample needed, clear the list and return - if (samples == 1) { - list.clear(); +float PIDController::ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples) { + // if only 1 sample needed (or invalid), clear the buffer and return + if (max_samples <= 1) { + buf.clear(); return new_value; } - // add the new item to the list - list.push_front(new_value); + // Trim oldest entries to make room (handles mode-switching where buffer + // may have more entries than the current mode needs) + while (buf.size() >= static_cast<size_t>(max_samples)) + buf.pop(); + buf.push(new_value); - // keep only 'samples' readings, by popping off the back of the list - while (samples > 0 && list.size() > static_cast<size_t>(samples)) - list.pop_back(); - - // calculate and return the average of all values in the list float sum = 0; - for (auto &elem : list) - sum += elem; - return sum / list.size(); + for (auto val : buf) + sum += val; + return sum / buf.size(); } float PIDController::calculate_relative_time_() { diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index e2a7030b571..6848a23965a 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -1,6 +1,7 @@ #pragma once + #include "esphome/core/hal.h" -#include <deque> +#include "esphome/core/helpers.h" #include <cmath> namespace esphome { @@ -24,10 +25,10 @@ struct PIDController { /// Differential gain K_d. float kd_ = 0; - // smooth the derivative value using a weighted average over X samples - int derivative_samples_ = 8; + // smooth the derivative value using an average over X samples + int derivative_samples_ = 1; - /// smooth the output value using a weighted average over X values + /// smooth the output value using an average over X values int output_samples_ = 1; float threshold_low_ = 0.0f; @@ -50,7 +51,10 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); void calculate_derivative_term_(float setpoint); - float weighted_average_(std::deque<float> &list, float new_value, int samples); + + /// Ring buffer smoothing using FixedRingBuffer (single allocation at setup) + float ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples); + float calculate_relative_time_(); /// Error from previous update used for derivative term @@ -60,12 +64,12 @@ struct PIDController { float accumulated_integral_ = 0; uint32_t last_time_ = 0; - // this is a list of derivative values for smoothing. - std::deque<float> derivative_list_; + // Ring buffer for derivative smoothing + FixedRingBuffer<float> derivative_window_; - // this is a list of output values for smoothing. - std::deque<float> output_list_; + // Ring buffer for output smoothing (shared between normal and deadband modes) + FixedRingBuffer<float> output_window_; -}; // Struct PID Controller +}; // Struct PIDController } // namespace pid } // namespace esphome From cdb445f69da73e51d72a45bb4488aa760be1427b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:00:28 -0400 Subject: [PATCH 0616/2056] [mipi_dsi] Fix ESP-IDF 6.0 compatibility for LCD color format (#14785) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/mipi_dsi/mipi_dsi.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 815b9d75a1d..7103e0868dc 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -54,6 +54,17 @@ void MIPI_DSI::setup() { this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } + // clang-format off +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + auto color_format = LCD_COLOR_FMT_RGB565; + if (this->color_depth_ == display::COLOR_BITNESS_888) { + color_format = LCD_COLOR_FMT_RGB888; + } + esp_lcd_dpi_panel_config_t dpi_config = {.virtual_channel = 0, + .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, + .dpi_clock_freq_mhz = this->pclk_frequency_, + .in_color_format = color_format, +#else auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; if (this->color_depth_ == display::COLOR_BITNESS_888) { pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888; @@ -62,6 +73,7 @@ void MIPI_DSI::setup() { .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, .dpi_clock_freq_mhz = this->pclk_frequency_, .pixel_format = pixel_format, +#endif .num_fbs = 1, // number of frame buffers to allocate .video_timing = { @@ -77,6 +89,7 @@ void MIPI_DSI::setup() { .flags = { .use_dma2d = true, }}; + // clang-format on err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); From ab3b677113ff9bf6a00a2159b6f3d6817a17b916 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:11:18 -0400 Subject: [PATCH 0617/2056] [adc] Fix ESP-IDF 6.0 compatibility for ADC_ATTEN_DB_12 (#14784) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/adc/adc_sensor.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 91cf4eaafcc..cf48ccd9c3d 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -22,7 +22,8 @@ namespace adc { #ifdef USE_ESP32 // clang-format off -#if (ESP_IDF_VERSION_MAJOR == 5 && \ +#if ESP_IDF_VERSION_MAJOR >= 6 || \ + (ESP_IDF_VERSION_MAJOR == 5 && \ ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ (ESP_IDF_VERSION_MINOR >= 2)) \ From 22062d79a2b7d82bf9f62f9dc92f37d8f8e91686 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 13:20:17 -1000 Subject: [PATCH 0618/2056] [analyze-memory] Add function call frequency analysis (#14779) --- esphome/analyze_memory/__init__.py | 56 ++++++++++++++- esphome/analyze_memory/cli.py | 109 +++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 3 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index bf1bcbfa050..7954c228224 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -1,6 +1,6 @@ """Memory usage analyzer for ESPHome compiled binaries.""" -from collections import defaultdict +from collections import Counter, defaultdict from dataclasses import dataclass, field import logging from pathlib import Path @@ -40,6 +40,15 @@ _READELF_SECTION_PATTERN = re.compile( r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" ) +# Regex for extracting call targets from objdump disassembly +# Matches direct call instructions across architectures: +# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol> +# ARM: bl/blx <addr> <symbol> +# Captures the mangled symbol name inside angle brackets. +_CALL_TARGET_PATTERN = re.compile( + r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>" +) + # Component category prefixes _COMPONENT_PREFIX_ESPHOME = "[esphome]" _COMPONENT_PREFIX_EXTERNAL = "[external]" @@ -197,6 +206,8 @@ class MemoryAnalyzer: self._lib_hash_to_name: dict[str, str] = {} # Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns" self._heuristic_to_lib: dict[str, str] = {} + # Function call counts: mangled_name -> call_count + self._function_call_counts: Counter[str] = Counter() def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" @@ -206,6 +217,7 @@ class MemoryAnalyzer: self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() + self._analyze_function_calls() return dict(self.components) def _parse_sections(self) -> None: @@ -384,8 +396,9 @@ class MemoryAnalyzer: return _LOGGER.info("Demangling %d symbols", len(symbols)) - self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) - _LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache)) + demangled = batch_demangle(symbols, objdump_path=self.objdump_path) + self._demangle_cache.update(demangled) + _LOGGER.info("Successfully demangled %d symbols", len(demangled)) def _demangle_symbol(self, symbol: str) -> str: """Get demangled C++ symbol name from cache.""" @@ -1011,6 +1024,43 @@ class MemoryAnalyzer: total_size, ) + def _analyze_function_calls(self) -> None: + """Count function call sites by parsing disassembly output. + + Parses direct call instructions (call0/call8/bl/blx) from objdump -d + to count how many times each function is called. This helps identify + inlining candidates — frequently called small functions benefit most + from inlining. + """ + result = run_tool( + [self.objdump_path, "-d", str(self.elf_path)], + timeout=60, + ) + if result is None or result.returncode != 0: + _LOGGER.debug("Failed to disassemble ELF for function call analysis") + return + + self._function_call_counts = Counter( + match.group(1) + for line in result.stdout.splitlines() + if (match := _CALL_TARGET_PATTERN.search(line)) + ) + + # Demangle any call targets not already in the cache + missing = [ + name + for name in self._function_call_counts + if name not in self._demangle_cache + ] + if missing: + self._batch_demangle_symbols(missing) + + _LOGGER.debug( + "Function call analysis: %d unique targets, %d total calls", + len(self._function_call_counts), + sum(self._function_call_counts.values()), + ) + def get_unattributed_ram(self) -> tuple[int, int, int]: """Get unattributed RAM sizes (SDK/framework overhead). diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index dbc19c6b89d..acaf5f45621 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -231,6 +231,110 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f" {size:>6,} B {sym_name}") lines.append("") + # Number of top called functions to show + TOP_CALLS_LIMIT: int = 50 + # Number of inlining candidates to show + INLINE_CANDIDATES_LIMIT: int = 25 + # Maximum function size in bytes to consider for inlining + INLINE_SIZE_THRESHOLD: int = 16 + + def _build_symbol_sizes(self) -> dict[str, int]: + """Build a size lookup from all component symbols: mangled_name -> size.""" + return { + symbol: size + for symbols in self._component_symbols.values() + for symbol, _, size, _ in symbols + } + + def _format_call_row( + self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int] + ) -> str: + """Format a single row for call frequency tables.""" + demangled = self._demangle_cache.get(mangled, mangled) + if len(demangled) > 80: + demangled = f"{demangled[:77]}..." + size = symbol_sizes.get(mangled) + size_str = f"{size:>5,} B" if size is not None else " ?" + return f"{index:>3} {count:>5} {size_str} {demangled}" + + def _add_call_table_header(self, lines: list[str]) -> None: + """Add the header row for call frequency tables.""" + lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function") + lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}") + + def _add_function_call_analysis(self, lines: list[str]) -> None: + """Add function call frequency analysis section. + + Shows the most frequently called functions by call site count. + """ + self._add_section_header(lines, "Top Called Functions") + + symbol_sizes = self._build_symbol_sizes() + + # Sort by call count descending + sorted_calls = sorted( + self._function_call_counts.items(), key=lambda x: x[1], reverse=True + ) + + self._add_call_table_header(lines) + + for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]): + lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes)) + + total_calls = sum(self._function_call_counts.values()) + lines.append("") + lines.append( + f"Total: {len(self._function_call_counts)} unique targets, " + f"{total_calls:,} call sites" + ) + lines.append("") + + def _add_inline_candidates(self, lines: list[str]) -> None: + """Add inlining candidates section. + + Shows frequently called functions that are small enough to benefit + from inlining (< 16 bytes). These are the best candidates for + reducing call overhead. + """ + self._add_section_header( + lines, + f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)", + ) + + symbol_sizes = self._build_symbol_sizes() + + # Filter to small functions with known size, sort by call count + candidates = sorted( + ( + (mangled, count) + for mangled, count in self._function_call_counts.items() + if mangled in symbol_sizes + and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD + ), + key=lambda x: x[1], + reverse=True, + ) + + if not candidates: + lines.append("No candidates found.") + lines.append("") + return + + self._add_call_table_header(lines) + + for i, (mangled, count) in enumerate( + candidates[: self.INLINE_CANDIDATES_LIMIT] + ): + lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes)) + + lines.append("") + lines.append( + f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} " + f"of {len(candidates)} functions under " + f"{self.INLINE_SIZE_THRESHOLD} B" + ) + lines.append("") + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -533,6 +637,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): if self._cswtch_symbols: self._add_cswtch_analysis(lines) + # Function call frequency analysis + if self._function_call_counts: + self._add_function_call_analysis(lines) + self._add_inline_candidates(lines) + lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) From 56f7b3e61b0bb6defd652092d9f4c13e4dabd593 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 13:20:35 -1000 Subject: [PATCH 0619/2056] [ci] Only run integration tests for changed components (#14776) --- .github/workflows/ci.yml | 17 ++- script/determine-jobs.py | 104 ++++++++----- script/helpers.py | 132 +++++++++++++++-- tests/script/test_determine_jobs.py | 217 ++++++++++++++++++++++------ tests/script/test_helpers.py | 30 +++- 5 files changed, 400 insertions(+), 100 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 461e676c4e6..fedfebf393f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,6 +170,8 @@ jobs: - common outputs: integration-tests: ${{ steps.determine.outputs.integration-tests }} + integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }} + integration-test-files: ${{ steps.determine.outputs.integration-test-files }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} @@ -210,6 +212,8 @@ jobs: # Extract individual fields echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT + echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT + echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT @@ -261,9 +265,20 @@ jobs: - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run integration tests + env: + INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }} + INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }} run: | . venv/bin/activate - pytest -vv --no-cov --tb=native -n auto tests/integration/ + if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then + echo "Running all integration tests" + pytest -vv --no-cov --tb=native -n auto tests/integration/ + else + # Parse JSON array into bash array to avoid shell expansion issues + mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]') + echo "Running ${#test_files[@]} specific integration tests" + pytest -vv --no-cov --tb=native -n auto "${test_files[@]}" + fi cpp-unit-tests: name: Run C++ unit tests diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 318ac04a7d0..6808a3cf6c9 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -6,6 +6,8 @@ what files have changed. It outputs JSON with the following structure: { "integration_tests": true/false, + "integration_tests_run_all": true/false, + "integration_test_files": ["tests/integration/test_foo.py", ...], "clang_tidy": true/false, "clang_format": true/false, "python_linters": true/false, @@ -56,13 +58,13 @@ from helpers import ( core_changed, filter_component_and_test_cpp_files, filter_component_and_test_files, - get_all_dependencies, get_changed_components, get_component_from_path, get_component_test_files, - get_components_from_integration_fixtures, get_components_with_dependencies, get_cpp_changed_components, + get_fixture_to_test_files, + get_integration_test_files_for_components, get_target_branch, git_ls_files, parse_test_filename, @@ -143,65 +145,88 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [ ] -def should_run_integration_tests(branch: str | None = None) -> bool: - """Determine if integration tests should run based on changed files. +def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[str]]: + """Determine which integration tests should run based on changed files. - This function is used by the CI workflow to intelligently skip integration tests when they're - not needed, saving significant CI time and resources. + This function is used by the CI workflow to intelligently skip or filter + integration tests, saving significant CI time and resources. - Integration tests will run when ANY of the following conditions are met: + Returns (run_all=True, []) when ANY of the following conditions are met: 1. Core C++ files changed (esphome/core/*) - Any .cpp, .h, .tcc files in the core directory - These files contain fundamental functionality used throughout ESPHome - - Examples: esphome/core/component.cpp, esphome/core/application.h 2. Core Python files changed (esphome/core/*.py) - Only .py files in the esphome/core/ directory - These are core Python files that affect the entire system - - Examples: esphome/core/config.py, esphome/core/__init__.py - - NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py - 3. Integration test files changed - - Any file in tests/integration/ directory - - This includes test files themselves and fixture YAML files - - Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml + 3. Integration test infrastructure files changed + - conftest.py, types.py, const.py, entity_utils.py, state_utils.py, etc. - 4. Components used by integration tests (or their dependencies) changed - - The function parses all YAML files in tests/integration/fixtures/ - - Extracts which components are used in integration tests - - Recursively finds all dependencies of those components - - If any of these components have changes, tests must run - - Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket', - then changes to sensor/, api/, or socket/ components trigger tests + Returns (run_all=False, [test_files...]) when: + + 4. Specific integration test files changed + - Only those specific test files are returned + + 5. Components used by integration tests (or their dependencies) changed + - Only test files whose fixtures use the changed components are returned Args: branch: Branch to compare against. If None, uses default. Returns: - True if integration tests should run, False otherwise. + Tuple of (run_all, test_files) where: + - run_all: True if all integration tests should run + - test_files: List of specific test file paths to run (empty if run_all + is True, or if no tests need to run) """ files = changed_files(branch) if core_changed(files): - # If any core files changed, run integration tests - return True + # If any core files changed, run all integration tests + return (True, []) - # Check if any integration test files changed - if any("tests/integration" in file for file in files): - return True + # If infrastructure Python files changed (conftest, utils, etc.), run all tests + # Excludes test files (test_*.py), fixtures, and non-Python files (README.md) + if any( + f.startswith("tests/integration/") + and f.endswith(".py") + and not f.startswith("tests/integration/test_") + and "/fixtures/" not in f + for f in files + ): + return (True, []) - # Get all components used in integration tests and their dependencies - fixture_components = get_components_from_integration_fixtures() - all_required_components = get_all_dependencies(fixture_components) + # Collect specific test files that need to run + test_files: set[str] = set() + fixture_to_test_files = get_fixture_to_test_files() - # Check if any required components changed - for file in files: - component = get_component_from_path(file) - if component and component in all_required_components: - return True + for f in files: + if f.startswith("tests/integration/test_") and f.endswith(".py"): + test_files.add(f) + elif f.startswith("tests/integration/fixtures/"): + if f.endswith(".yaml"): + # Fixture YAML changed - add corresponding test file(s) + test_files.update(fixture_to_test_files.get(Path(f).stem, ())) + else: + # Non-YAML fixture file changed (e.g., external_components/) + # Run all tests since we can't determine which tests are affected + return (True, []) - return False + # Find test files whose fixtures use any of the changed components + changed_component_set = { + component for file in files if (component := get_component_from_path(file)) + } + if changed_component_set: + test_files.update( + get_integration_test_files_for_components(changed_component_set) + ) + + if test_files: + return (False, sorted(test_files)) + + return (False, []) @cache @@ -682,7 +707,10 @@ def main() -> None: args = parser.parse_args() # Determine what should run - run_integration = should_run_integration_tests(args.branch) + integration_run_all, integration_test_files = determine_integration_tests( + args.branch + ) + run_integration = integration_run_all or bool(integration_test_files) run_clang_tidy = should_run_clang_tidy(args.branch) run_clang_format = should_run_clang_format(args.branch) run_python_linters = should_run_python_linters(args.branch) @@ -810,6 +838,8 @@ def main() -> None: output: dict[str, Any] = { "integration_tests": run_integration, + "integration_tests_run_all": integration_run_all, + "integration_test_files": integration_test_files, "clang_tidy": run_clang_tidy, "clang_tidy_mode": clang_tidy_mode, "clang_format": run_clang_format, diff --git a/script/helpers.py b/script/helpers.py index 6ee286a657f..9665af70ec7 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -700,37 +700,141 @@ def get_all_dependencies( return all_components +def _extract_components_from_yaml(config: dict) -> set[str]: + """Extract component names from a parsed YAML config. + + Args: + config: Parsed YAML configuration dictionary + + Returns: + Set of component names found in the config + """ + components: set[str] = set() + + # Add all top-level component keys (skip YAML anchor keys starting with '.') + components.update(k for k in config if isinstance(k, str) and not k.startswith(".")) + + # Add platform values from list entries (e.g., sensor -> platform: template adds "template") + for value in config.values(): + if isinstance(value, list): + components.update( + item["platform"] + for item in value + if isinstance(item, dict) and "platform" in item + ) + + return components + + def get_components_from_integration_fixtures() -> set[str]: """Extract all components used in integration test fixtures. Returns: Set of component names used in integration test fixtures """ + return { + comp + for components in get_components_per_integration_fixture().values() + for comp in components + } + + +@cache +def get_components_per_integration_fixture() -> dict[str, set[str]]: + """Extract components used in each integration test fixture. + + Returns: + Dictionary mapping fixture name (stem) to set of component names + """ from esphome import yaml_util - components: set[str] = set() + result: dict[str, set[str]] = {} fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures" for yaml_file in fixtures_dir.glob("*.yaml"): - config: dict[str, any] | None = yaml_util.load_yaml(yaml_file) + config: dict[str, Any] | None = yaml_util.load_yaml(yaml_file) if not config: continue - # Add all top-level component keys (skip YAML anchor keys starting with '.') - components.update( - k for k in config if isinstance(k, str) and not k.startswith(".") - ) + result[yaml_file.stem] = _extract_components_from_yaml(config) - # Add platform components (e.g., output.template) - for value in config.values(): - if not isinstance(value, list): - continue + return result - for item in value: - if isinstance(item, dict) and "platform" in item: - components.add(item["platform"]) - return components +_TEST_FUNC_RE = re.compile(r"async def (test_\w+)") + + +@cache +def get_fixture_to_test_files() -> dict[str, frozenset[str]]: + """Map integration test fixture names to the test files that use them. + + Returns: + Dictionary mapping fixture name to frozenset of test file paths + (relative to repo root) + """ + integration_dir = Path(__file__).parent.parent / "tests" / "integration" + result: dict[str, set[str]] = {} + + for test_file in integration_dir.glob("test_*.py"): + content = test_file.read_text(encoding="utf-8") + rel_path = test_file.relative_to(Path(__file__).parent.parent).as_posix() + for func in _TEST_FUNC_RE.findall(content): + base_name = func.replace("test_", "").partition("[")[0] + result.setdefault(base_name, set()).add(rel_path) + + return {k: frozenset(v) for k, v in result.items()} + + +@cache +def _get_component_to_integration_test_files() -> dict[str, frozenset[str]]: + """Build index mapping each component to the test files that depend on it. + + Resolves full dependency trees once per fixture, then inverts the mapping + so lookups are O(1) per component. + + Returns: + Dictionary mapping component name to frozenset of test file paths + """ + fixture_components = get_components_per_integration_fixture() + fixture_to_test_files = get_fixture_to_test_files() + + result: dict[str, set[str]] = {} + for fixture_name, components in fixture_components.items(): + test_files = fixture_to_test_files.get(fixture_name) + if not test_files: + continue + # Get full dependency tree for this fixture's components + all_deps = get_all_dependencies(components) + for dep in all_deps: + result.setdefault(dep, set()).update(test_files) + + return {k: frozenset(v) for k, v in result.items()} + + +def get_integration_test_files_for_components( + changed_components: set[str], +) -> list[str]: + """Get integration test file paths that use any of the given components. + + Uses a precomputed component → test files index for O(C) lookup + where C is the number of changed components. + + Args: + changed_components: Set of component names that have changed + + Returns: + Sorted list of test file paths relative to repo root + (e.g., ["tests/integration/test_api.py", ...]) + """ + component_to_tests = _get_component_to_integration_test_files() + + return sorted( + { + test_file + for component in changed_components + for test_file in component_to_tests.get(component, ()) + } + ) def filter_component_and_test_files(file_path: str) -> bool: diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 61ef8985df9..5c81ad374b1 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -29,9 +29,9 @@ spec.loader.exec_module(determine_jobs) @pytest.fixture -def mock_should_run_integration_tests() -> Generator[Mock, None, None]: - """Mock should_run_integration_tests from helpers.""" - with patch.object(determine_jobs, "should_run_integration_tests") as mock: +def mock_determine_integration_tests() -> Generator[Mock, None, None]: + """Mock determine_integration_tests.""" + with patch.object(determine_jobs, "determine_integration_tests") as mock: yield mock @@ -87,7 +87,7 @@ def clear_determine_jobs_caches() -> None: def test_main_all_tests_should_run( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -100,7 +100,7 @@ def test_main_all_tests_should_run( # Ensure we're not in GITHUB_ACTIONS mode for this test monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = True + mock_determine_integration_tests.return_value = (True, []) mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = True mock_should_run_python_linters.return_value = True @@ -152,6 +152,8 @@ def test_main_all_tests_should_run( output = json.loads(captured.out) assert output["integration_tests"] is True + assert output["integration_tests_run_all"] is True + assert output["integration_test_files"] == [] assert output["clang_tidy"] is True assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is True @@ -183,7 +185,7 @@ def test_main_all_tests_should_run( def test_main_no_tests_should_run( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -196,7 +198,7 @@ def test_main_no_tests_should_run( # Ensure we're not in GITHUB_ACTIONS mode for this test monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False @@ -233,6 +235,8 @@ def test_main_no_tests_should_run( output = json.loads(captured.out) assert output["integration_tests"] is False + assert output["integration_tests_run_all"] is False + assert output["integration_test_files"] == [] assert output["clang_tidy"] is False assert output["clang_tidy_mode"] == "disabled" assert output["clang_format"] is False @@ -253,7 +257,7 @@ def test_main_no_tests_should_run( def test_main_with_branch_argument( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -266,7 +270,7 @@ def test_main_with_branch_argument( # Ensure we're not in GITHUB_ACTIONS mode for this test monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = True @@ -302,7 +306,7 @@ def test_main_with_branch_argument( determine_jobs.main() # Check that functions were called with branch - mock_should_run_integration_tests.assert_called_once_with("main") + mock_determine_integration_tests.assert_called_once_with("main") mock_should_run_clang_tidy.assert_called_once_with("main") mock_should_run_clang_format.assert_called_once_with("main") mock_should_run_python_linters.assert_called_once_with("main") @@ -312,6 +316,8 @@ def test_main_with_branch_argument( output = json.loads(captured.out) assert output["integration_tests"] is False + assert output["integration_tests_run_all"] is False + assert output["integration_test_files"] == [] assert output["clang_tidy"] is True assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is False @@ -334,30 +340,33 @@ def test_main_with_branch_argument( assert output["cpp_unit_tests_components"] == ["mqtt"] -def test_should_run_integration_tests( +def test_determine_integration_tests( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test should_run_integration_tests function.""" - # Core C++ files trigger tests + """Test determine_integration_tests function.""" + # Core C++ files trigger run_all with patch.object( determine_jobs, "changed_files", return_value=["esphome/core/component.cpp"] ): - result = determine_jobs.should_run_integration_tests() - assert result is True + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] - # Core Python files trigger tests + # Core Python files trigger run_all with patch.object( determine_jobs, "changed_files", return_value=["esphome/core/config.py"] ): - result = determine_jobs.should_run_integration_tests() - assert result is True + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] # Python files directly in esphome/ do NOT trigger tests with patch.object( determine_jobs, "changed_files", return_value=["esphome/config.py"] ): - result = determine_jobs.should_run_integration_tests() - assert result is False + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == [] # Python files in subdirectories (not core) do NOT trigger tests with patch.object( @@ -365,35 +374,151 @@ def test_should_run_integration_tests( "changed_files", return_value=["esphome/dashboard/web_server.py"], ): - result = determine_jobs.should_run_integration_tests() - assert result is False + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == [] -def test_should_run_integration_tests_with_branch() -> None: - """Test should_run_integration_tests with branch argument.""" +def test_determine_integration_tests_with_branch() -> None: + """Test determine_integration_tests with branch argument.""" with patch.object(determine_jobs, "changed_files") as mock_changed: mock_changed.return_value = [] - determine_jobs.should_run_integration_tests("release") + run_all, test_files = determine_jobs.determine_integration_tests("release") mock_changed.assert_called_once_with("release") + assert run_all is False + assert test_files == [] -def test_should_run_integration_tests_component_dependency() -> None: - """Test that integration tests run when components used in fixtures change.""" +def test_determine_integration_tests_component_dependency() -> None: + """Test that integration tests return specific test files when components used in fixtures change.""" with ( patch.object( determine_jobs, "changed_files", return_value=["esphome/components/api/api.cpp"], ), + patch.object(determine_jobs, "get_fixture_to_test_files") as mock_fixture_map, patch.object( - determine_jobs, "get_components_from_integration_fixtures" - ) as mock_fixtures, + determine_jobs, "get_integration_test_files_for_components" + ) as mock_test_files, ): - mock_fixtures.return_value = {"api", "sensor"} - with patch.object(determine_jobs, "get_all_dependencies") as mock_deps: - mock_deps.return_value = {"api", "sensor", "network"} - result = determine_jobs.should_run_integration_tests() - assert result is True + mock_fixture_map.return_value = {} + mock_test_files.return_value = [ + "tests/integration/test_api.py", + "tests/integration/test_sensor.py", + ] + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == [ + "tests/integration/test_api.py", + "tests/integration/test_sensor.py", + ] + + +def test_determine_integration_tests_component_only_affected_tests() -> None: + """Test that only tests using the changed component are returned.""" + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["esphome/components/modbus/modbus.cpp"], + ), + patch.object(determine_jobs, "get_fixture_to_test_files", return_value={}), + patch.object( + determine_jobs, "get_integration_test_files_for_components" + ) as mock_test_files, + ): + mock_test_files.return_value = [ + "tests/integration/test_uart_mock_modbus.py", + ] + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == ["tests/integration/test_uart_mock_modbus.py"] + # Verify it was called with the right component + mock_test_files.assert_called_once_with({"modbus"}) + + +def test_determine_integration_tests_infra_file_runs_all() -> None: + """Test that changing infrastructure files (conftest.py, etc.) runs all tests.""" + with patch.object( + determine_jobs, + "changed_files", + return_value=["tests/integration/conftest.py"], + ): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] + + +def test_determine_integration_tests_readme_does_not_run_all() -> None: + """Test that changing README.md does not trigger integration tests.""" + with patch.object( + determine_jobs, + "changed_files", + return_value=["tests/integration/README.md"], + ): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == [] + + +def test_determine_integration_tests_changed_test_file() -> None: + """Test that changing a specific test file only runs that test.""" + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["tests/integration/test_syslog.py"], + ), + patch.object(determine_jobs, "get_fixture_to_test_files", return_value={}), + patch.object( + determine_jobs, + "get_integration_test_files_for_components", + return_value=[], + ), + ): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == ["tests/integration/test_syslog.py"] + + +def test_determine_integration_tests_changed_fixture_yaml() -> None: + """Test that changing a fixture YAML runs the corresponding test file.""" + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["tests/integration/fixtures/uart_mock_modbus.yaml"], + ), + patch.object(determine_jobs, "get_fixture_to_test_files") as mock_fixture_map, + patch.object( + determine_jobs, + "get_integration_test_files_for_components", + return_value=[], + ), + ): + mock_fixture_map.return_value = { + "uart_mock_modbus": frozenset( + {"tests/integration/test_uart_mock_modbus.py"} + ), + } + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is False + assert test_files == ["tests/integration/test_uart_mock_modbus.py"] + + +def test_determine_integration_tests_non_yaml_fixture_runs_all() -> None: + """Test that non-YAML changes under fixtures/ (e.g., external_components) run all tests.""" + with patch.object( + determine_jobs, + "changed_files", + return_value=[ + "tests/integration/fixtures/external_components/test_component/__init__.py" + ], + ): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] @pytest.mark.parametrize( @@ -538,7 +663,7 @@ def test_count_changed_cpp_files_with_branch() -> None: def test_main_filters_components_without_tests( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -551,7 +676,7 @@ def test_main_filters_components_without_tests( # Ensure we're not in GITHUB_ACTIONS mode for this test monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False @@ -631,7 +756,7 @@ def test_main_filters_components_without_tests( def test_main_detects_components_with_variant_tests( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -649,7 +774,7 @@ def test_main_detects_components_with_variant_tests( # Ensure we're not in GITHUB_ACTIONS mode for this test monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False @@ -999,7 +1124,7 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: def test_clang_tidy_mode_full_scan( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -1010,7 +1135,7 @@ def test_clang_tidy_mode_full_scan( """Test that full scan (hash changed) always uses split mode.""" monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False @@ -1065,7 +1190,7 @@ def test_clang_tidy_mode_targeted_scan( component_count: int, files_per_component: int, expected_mode: str, - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -1076,7 +1201,7 @@ def test_clang_tidy_mode_targeted_scan( """Test clang-tidy mode selection based on files_to_check count.""" monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False @@ -1123,7 +1248,7 @@ def test_clang_tidy_mode_targeted_scan( def test_main_core_files_changed_still_detects_components( - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -1135,7 +1260,7 @@ def test_main_core_files_changed_still_detects_components( """Test that component changes are detected even when core files change.""" monkeypatch.delenv("GITHUB_ACTIONS", raising=False) - mock_should_run_integration_tests.return_value = True + mock_determine_integration_tests.return_value = (True, []) mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = True mock_should_run_python_linters.return_value = True @@ -1604,7 +1729,7 @@ def test_detect_platform_hint_from_filename_case_insensitive( def test_component_batching_beta_branch_40_per_batch( tmp_path: Path, - mock_should_run_integration_tests: Mock, + mock_determine_integration_tests: Mock, mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, @@ -1628,7 +1753,7 @@ def test_component_batching_beta_branch_40_per_batch( (comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}") # Setup mocks - mock_should_run_integration_tests.return_value = False + mock_determine_integration_tests.return_value = (False, []) mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 781054eb3b9..e3802d2d51f 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -36,6 +36,7 @@ def clear_helpers_cache() -> None: """Clear cached functions before each test.""" helpers._get_github_event_data.cache_clear() helpers._get_changed_files_github_actions.cache_clear() + helpers.get_components_per_integration_fixture.cache_clear() @pytest.mark.parametrize( @@ -1111,7 +1112,7 @@ def test_get_components_from_integration_fixtures() -> None: "gpio", } - mock_yaml_file = Mock() + mock_yaml_file = Mock(stem="test_fixture") with ( patch("pathlib.Path.glob") as mock_glob, @@ -1133,7 +1134,7 @@ def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None: ".binary_filters": {"filters": [{"settle": "50ms"}]}, } - mock_yaml_file = Mock() + mock_yaml_file = Mock(stem="test_fixture") with ( patch("pathlib.Path.glob") as mock_glob, @@ -1148,6 +1149,31 @@ def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None: assert components == {"sensor", "esphome", "template"} +def test_get_integration_test_files_for_components_real_fixtures() -> None: + """Test that component changes map to the correct real integration test files. + + This test uses real fixtures to verify the mapping stays correct + as new tests are added. + """ + # modbus should include at least the modbus test + modbus_tests = helpers.get_integration_test_files_for_components({"modbus"}) + assert "tests/integration/test_uart_mock_modbus.py" in modbus_tests + + # ld2410 should include at least the ld2410 test + ld2410_tests = helpers.get_integration_test_files_for_components({"ld2410"}) + assert "tests/integration/test_uart_mock_ld2410.py" in ld2410_tests + + # syslog should include at least the syslog test + syslog_tests = helpers.get_integration_test_files_for_components({"syslog"}) + assert "tests/integration/test_syslog.py" in syslog_tests + + # A component not used by any fixture should return nothing + fake_tests = helpers.get_integration_test_files_for_components( + {"nonexistent_component_xyz"} + ) + assert fake_tests == [] + + @pytest.mark.parametrize( "output,expected", [ From 7cceb72cc310e784acd249ff1108793abe36293f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 13:23:41 -1000 Subject: [PATCH 0620/2056] [api] Inline force-variant ProtoSize calc methods (#14781) --- esphome/components/api/proto.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d1c955b1fb9..814a3f44560 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -602,7 +602,7 @@ class ProtoSize { static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { return value ? field_id_size + varint(encode_zigzag32(value)) : 0; } - static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) { return field_id_size + varint(encode_zigzag32(value)); } static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { @@ -614,13 +614,13 @@ class ProtoSize { static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) { return value ? field_id_size + varint(value) : 0; } - static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) { return field_id_size + varint(value); } static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0; } - static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) { return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); } static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) { @@ -638,7 +638,8 @@ class ProtoSize { static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) { return nested_size ? field_id_size + varint(nested_size) + nested_size : 0; } - static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) { + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size, + uint32_t nested_size) { return field_id_size + varint(nested_size) + nested_size; } }; From 86b79330815c75cb3cd980afecfe80eb87b1026a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:24:41 -0400 Subject: [PATCH 0621/2056] [esp32_rmt_led_strip][remote_transmitter][remote_receiver] Fix ESP-IDF 6.0 RMT compatibility (#14783) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../components/esp32_rmt_led_strip/led_strip.cpp | 2 -- .../remote_receiver/remote_receiver_rmt.cpp | 1 - .../remote_transmitter/remote_transmitter_rmt.cpp | 13 +++++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 66b41931aac..ca97a181fd4 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -99,8 +99,6 @@ void ESP32RMTLEDStripLightOutput::setup() { channel.gpio_num = gpio_num_t(this->pin_); channel.mem_block_symbols = this->rmt_symbols_; channel.trans_queue_depth = 1; - channel.flags.io_loop_back = 0; - channel.flags.io_od_mode = 0; channel.flags.invert_out = this->invert_out_; channel.flags.with_dma = this->use_dma_; channel.intr_priority = 0; diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index 96b23bd0f52..596608a4d07 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -44,7 +44,6 @@ void RemoteReceiverComponent::setup() { channel.intr_priority = 0; channel.flags.invert_in = 0; channel.flags.with_dma = this->with_dma_; - channel.flags.io_loop_back = 0; esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_); if (error != ESP_OK) { this->error_code_ = error; diff --git a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp index 71773e3ddf8..3c9a12d472f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp @@ -120,11 +120,13 @@ void RemoteTransmitterComponent::configure_rmt_() { channel.gpio_num = gpio_num_t(this->pin_->get_pin()); channel.mem_block_symbols = this->rmt_symbols_; channel.trans_queue_depth = 1; - channel.flags.io_loop_back = open_drain; - channel.flags.io_od_mode = open_drain; channel.flags.invert_out = 0; channel.flags.with_dma = this->with_dma_; channel.intr_priority = 0; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + channel.flags.io_loop_back = open_drain; + channel.flags.io_od_mode = open_drain; +#endif error = rmt_new_tx_channel(&channel, &this->channel_); if (error != ESP_OK) { this->error_code_ = error; @@ -136,6 +138,13 @@ void RemoteTransmitterComponent::configure_rmt_() { this->mark_failed(); return; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (open_drain) { + gpio_num_t gpio = gpio_num_t(this->pin_->get_pin()); + gpio_od_enable(gpio); + gpio_input_enable(gpio); + } +#endif if (this->pin_->get_flags() & gpio::FLAG_PULLUP) { gpio_pullup_en(gpio_num_t(this->pin_->get_pin())); } else { From d6d3bbbad8f6e14a78c2f6c7505bdbfcbd3dad2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 13:28:34 -1000 Subject: [PATCH 0622/2056] [scheduler] Use integer math for interval offset calculation (#14755) --- esphome/core/scheduler.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 63e1006b03c..72b183384e3 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -105,10 +105,11 @@ static void validate_static_string(const char *name) { // avoid the main thread modifying the list while it is being accessed. // Calculate random offset for interval timers -// Extracted from set_timer_common_ to reduce code size - float math + random_float() -// only needed for intervals, not timeouts +// Extracted from set_timer_common_ to reduce code size - only needed for intervals, not timeouts uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { - return static_cast<uint32_t>(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); + uint32_t max_offset = std::min(delay / 2, MAX_INTERVAL_DELAY); + // Multiply-and-shift: uniform random in [0, max_offset) without floating point + return static_cast<uint32_t>((static_cast<uint64_t>(random_uint32()) * max_offset) >> 32); } // Check if a retry was already cancelled in items_ or to_add_ From 5e3c44d48fc5e7870725e5572accf0d0b0cd9de0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 13:28:55 -1000 Subject: [PATCH 0623/2056] [rp2040] Add CI check for boards.py freshness (#14754) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + esphome/components/rp2040/generate_boards.py | 12 +++- script/generate-rp2040-boards.py | 61 ++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100755 script/generate-rp2040-boards.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fedfebf393f..f7710589c50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,7 @@ jobs: script/build_codeowners.py --check script/build_language_schema.py --check script/generate-esp32-boards.py --check + script/generate-rp2040-boards.py --check pytest: name: Run pytest diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py index a0e3699f37b..7ea02d185e9 100644 --- a/esphome/components/rp2040/generate_boards.py +++ b/esphome/components/rp2040/generate_boards.py @@ -6,6 +6,7 @@ Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> import json from pathlib import Path import re +import subprocess import sys from jinja2 import Environment, FileSystemLoader @@ -157,7 +158,7 @@ def generate(arduino_pico_path: Path) -> str: board_pins, boards = load_boards(arduino_pico_path) template = _jinja_env.get_template("boards.jinja2") - return template.render( + content = template.render( cyw43_gpio_offset=CYW43_GPIO_OFFSET, cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1, default_max_pin=DEFAULT_MAX_PIN, @@ -165,6 +166,15 @@ def generate(arduino_pico_path: Path) -> str: boards=sorted(boards.items()), ) + # Format output to match pre-commit ruff formatting + result = subprocess.run( + [sys.executable, "-m", "ruff", "format", "--stdin-filename", "boards.py"], + input=content.encode(), + capture_output=True, + check=True, + ) + return result.stdout.decode() + def main(): if len(sys.argv) < 2: diff --git a/script/generate-rp2040-boards.py b/script/generate-rp2040-boards.py new file mode 100755 index 00000000000..1b4846fd2b8 --- /dev/null +++ b/script/generate-rp2040-boards.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +from pathlib import Path +import subprocess +import sys +import tempfile + +from esphome.components.rp2040 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION +from esphome.components.rp2040.generate_boards import generate +from esphome.helpers import write_file_if_changed + +ver = RECOMMENDED_ARDUINO_FRAMEWORK_VERSION +version_tag: str = f"{ver.major}.{ver.minor}.{ver.patch}" +root: Path = Path(__file__).parent.parent +boards_file_path: Path = root / "esphome" / "components" / "rp2040" / "boards.py" + + +def main(check: bool) -> None: + with tempfile.TemporaryDirectory() as tempdir: + subprocess.run( + [ + "git", + "clone", + "-q", + "-c", + "advice.detachedHead=false", + "--depth", + "1", + "--branch", + version_tag, + "https://github.com/earlephilhower/arduino-pico", + tempdir, + ], + check=True, + ) + + content: str = generate(Path(tempdir)) + + if check: + existing_content: str = boards_file_path.read_text(encoding="utf-8") + if existing_content != content: + print("esphome/components/rp2040/boards.py is not up to date.") + print("Please run `script/generate-rp2040-boards.py`") + sys.exit(1) + print("esphome/components/rp2040/boards.py is up to date") + elif write_file_if_changed(boards_file_path, content): + print("RP2040 boards updated successfully.") + + +if __name__ == "__main__": + parser: argparse.ArgumentParser = argparse.ArgumentParser() + parser.add_argument( + "--check", + help="Check if the boards.py file is up to date.", + action="store_true", + ) + args: argparse.Namespace = parser.parse_args() + main(args.check) From fcf5637aa5f42823d14015ec10c8aced123c1842 Mon Sep 17 00:00:00 2001 From: leccelecce <24962424+leccelecce@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:15:54 +0000 Subject: [PATCH 0624/2056] [online_image] Log download duration in milliseconds instead of seconds (#14803) --- esphome/components/online_image/online_image.cpp | 6 +++--- esphome/components/online_image/online_image.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index da866599c9d..22bf6a3056f 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -129,7 +129,7 @@ void OnlineImage::update() { } ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); - this->start_time_ = ::time(nullptr); + this->start_time_ = millis(); this->enable_loop(); } @@ -155,8 +155,8 @@ void OnlineImage::loop() { // Finalize decoding this->end_decode(); - ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), - (uint32_t) (::time(nullptr) - this->start_time_)); + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(), + millis() - this->start_time_); // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index c7c80c7c667..12c25645260 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent, */ std::string last_modified_ = ""; - time_t start_time_; + uint32_t start_time_{0}; }; template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { From 0716c9f7227873bc236009ce5438cf0974bffe53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:12:04 -1000 Subject: [PATCH 0625/2056] [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) --- esphome/components/esp8266/helpers.cpp | 4 +--- esphome/components/libretiny/helpers.cpp | 4 +--- esphome/components/zephyr/core.cpp | 4 +--- esphome/core/helpers.h | 22 +++++++++++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 036594fa178..4a64ae181e3 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -22,9 +22,7 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -// ESP8266 doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// ESP8266 LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index 37ae0fb455a..21913e4a16d 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } -// LibreTiny doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// LibreTiny LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) WiFi.macAddress(mac); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index eee7fb3f4f8..1d105a10572 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -76,9 +76,7 @@ void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } -// Zephyr doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// Zephyr LwIPLock is defined inline as a no-op in helpers.h uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9828df29cb4..22672087524 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1930,19 +1930,27 @@ class InterruptLock { /** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads. * - * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled. - * It ensures thread-safe access to lwIP APIs. + * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, + * and on RP2040 when CYW43 WiFi is active (cyw43_arch_lwip_begin/end). * - * @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp + * On platforms without lwIP core locking (ESP8266, LibreTiny, Zephyr), + * this is a no-op defined inline so the compiler can eliminate all call overhead. */ class LwIPLock { public: - LwIPLock(); - ~LwIPLock(); - - // Delete copy constructor and copy assignment operator to prevent accidental copying LwIPLock(const LwIPLock &) = delete; LwIPLock &operator=(const LwIPLock &) = delete; + +#if defined(USE_ESP32) || defined(USE_RP2040) + // Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp + LwIPLock(); + ~LwIPLock(); +#else + // No lwIP core locking — inline no-ops (empty bodies instead of = default + // to prevent clang-tidy unused-variable warnings at call sites) + LwIPLock() {} + ~LwIPLock() {} +#endif }; /** Helper class to request `loop()` to be called as fast as possible. From 0043be616529e78cf7b9717f9fadf50749b904d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:13:01 -1000 Subject: [PATCH 0626/2056] [core] Inline trivial EntityBase accessors (#14782) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/core/entity_base.cpp | 5 ----- esphome/core/entity_base.h | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 818dae06de1..a47af1dd93c 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -8,9 +8,6 @@ namespace esphome { static const char *const TAG = "entity_base"; -// Entity Name -const StringRef &EntityBase::get_name() const { return this->name_; } - void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) { this->name_ = StringRef(name); if (this->name_.empty()) { @@ -176,8 +173,6 @@ StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) c return StringRef(buf.data(), len); } -uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } - // Migrate preference data from old_key to new_key if they differ. // This helper is exposed so callers with custom key computation (like TextPrefs) // can use it for manual migration. See: https://github.com/esphome/backlog/issues/85 diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index cccbafd2c36..012a62f1c08 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -68,7 +68,7 @@ static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26; class EntityBase { public: // Get the name of this Entity - const StringRef &get_name() const; + const StringRef &get_name() const { return this->name_; } // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -86,7 +86,7 @@ class EntityBase { std::string get_object_id() const; // Get the unique Object ID of this Entity - uint32_t get_object_id_hash(); + uint32_t get_object_id_hash() const { return this->object_id_hash_; } /// Get object_id with zero heap allocation /// For static case: returns StringRef to internal storage (buffer unused) From f2968e044903ca35db4da1e1773323c74b19fee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:13:50 -1000 Subject: [PATCH 0627/2056] [api] Reduce API code size with buffer and nodelay optimizations (#14797) --- esphome/components/api/api_buffer.h | 6 +++++ esphome/components/api/api_connection.cpp | 3 +-- esphome/components/api/api_connection.h | 6 ++--- esphome/components/api/api_frame_helper.h | 29 ++++++++++------------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h index 00801e3ee58..1d0cccf61ca 100644 --- a/esphome/components/api/api_buffer.h +++ b/esphome/components/api/api_buffer.h @@ -44,6 +44,12 @@ class APIBuffer { this->reserve(n); this->size_ = n; // no zero-fill } + /// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size. + /// Single grow_ check regardless of argument order. + inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { + this->reserve(std::max(reserve_size, new_size)); + this->size_ = new_size; + } uint8_t *data() { return this->data_.get(); } const uint8_t *data() const { return this->data_.get(); } size_t size() const { return this->size_; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index dea3ba5460b..d55b5dffb6a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2025,8 +2025,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode // Batch message second or later // Add padding for previous message footer + this message header size_t current_size = shared_buf.size(); - shared_buf.reserve(current_size + total_calculated_size); - shared_buf.resize(current_size + footer_size + header_padding); + shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding); } // Pre-resize buffer to include payload, then encode through raw pointer diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 68f698d1902..85c8e777a94 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -305,9 +305,9 @@ class APIConnection final : public APIServerConnectionBase { // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) - shared_buf.reserve(total_size); - // Resize to add header padding so message encoding starts at the correct position - shared_buf.resize(header_padding); + // Reserve full size but only set initial size to header padding + // so message encoding starts at the correct position + shared_buf.reserve_and_resize(total_size, header_padding); } // Convenience overload - computes frame overhead internally diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 5e07ad43a93..b2561f2b328 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -147,22 +147,18 @@ class APIFrameHelper { // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { - if (this->nodelay_state_ != NODELAY_ON) { + if (this->nodelay_counter_) { this->set_nodelay_raw_(true); - this->nodelay_state_ = NODELAY_ON; + this->nodelay_counter_ = 0; } return; } - - // Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush) - if (this->nodelay_state_ == NODELAY_ON) { + // Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT + if (!this->nodelay_counter_) this->set_nodelay_raw_(false); - this->nodelay_state_ = 1; - } else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) { + if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) { this->set_nodelay_raw_(true); - this->nodelay_state_ = NODELAY_ON; - } else { - this->nodelay_state_++; + this->nodelay_counter_ = 0; } } virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; @@ -258,18 +254,17 @@ class APIFrameHelper { uint8_t tx_buf_head_{0}; uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; - // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. - // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send). + // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. + // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0. // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. - static constexpr int8_t NODELAY_ON = -1; #ifdef USE_ESP8266 - static constexpr int8_t LOG_NAGLE_COUNT = 2; + static constexpr uint8_t LOG_NAGLE_COUNT = 2; #else - static constexpr int8_t LOG_NAGLE_COUNT = 3; + static constexpr uint8_t LOG_NAGLE_COUNT = 3; #endif - int8_t nodelay_state_{NODELAY_ON}; + uint8_t nodelay_counter_{0}; // Internal helper to set TCP_NODELAY socket option void set_nodelay_raw_(bool enable) { From ca279110c9157da5026451839e4a9b8b9724a69b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:31:50 -1000 Subject: [PATCH 0628/2056] [output] Inline trivial FloatOutput accessors (#14786) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/output/float_output.cpp | 6 ------ esphome/components/output/float_output.h | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 3b83c857165..46014e0903a 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -11,16 +11,10 @@ void FloatOutput::set_max_power(float max_power) { this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0 } -float FloatOutput::get_max_power() const { return this->max_power_; } - void FloatOutput::set_min_power(float min_power) { this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX } -void FloatOutput::set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } - -float FloatOutput::get_min_power() const { return this->min_power_; } - void FloatOutput::set_level(float state) { state = clamp(state, 0.0f, 1.0f); diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 3e2b3ada8d7..5225f88c669 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -48,9 +48,9 @@ class FloatOutput : public BinaryOutput { /** Sets this output to ignore min_power for a 0 state * - * @param zero True if a 0 state should mean 0 and not min_power. + * @param zero_means_zero True if a 0 state should mean 0 and not min_power. */ - void set_zero_means_zero(bool zero_means_zero); + void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } /** Set the level of this float output, this is called from the front-end. * @@ -70,10 +70,10 @@ class FloatOutput : public BinaryOutput { // (In most use cases you won't need these) /// Get the maximum power output. - float get_max_power() const; + float get_max_power() const { return this->max_power_; } /// Get the minimum power output. - float get_min_power() const; + float get_min_power() const { return this->min_power_; } protected: /// Implement BinarySensor's write_enabled; this should never be called. From f12531e7e0b2a2702d3a68cbf95515839c885377 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:32:17 -0400 Subject: [PATCH 0629/2056] [esp32_camera] Bump esp32-camera to 2.1.5 (#14806) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/camera_encoder/__init__.py | 2 +- esphome/components/esp32_camera/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index 89181d27b4a..a0c59a517a7 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None: buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: - add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_component(name="espressif/esp32-camera", ref="2.1.5") cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER") var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 3a5d87792b6..afab849a7c6 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -400,7 +400,7 @@ async def to_code(config): if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") - add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_component(name="espressif/esp32-camera", ref="2.1.5") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index df651ae15dd..d83a71624c9 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -8,7 +8,7 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.1.1 + version: 2.1.5 espressif/mdns: version: 1.10.0 espressif/esp_wifi_remote: From c52042e023c6178801a1c74e9480cfe4b9747689 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:01:29 -0400 Subject: [PATCH 0630/2056] [tinyusb][usb_cdc_acm] Bump esp_tinyusb to 2.1.1 (#14796) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/tinyusb/__init__.py | 2 +- esphome/components/tinyusb/tinyusb_component.cpp | 12 ++++++++---- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 7 +++---- esphome/idf_component.yml | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 90043e969c1..df94ad75346 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -54,7 +54,7 @@ async def to_code(config): if config[CONF_USB_SERIAL_STR]: cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR])) - add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1") + add_idf_component(name="espressif/esp_tinyusb", ref="2.1.1") add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False) add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index 19bb545c4b5..7f8fea52647 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -16,10 +16,14 @@ void TinyUSB::setup() { } this->tusb_cfg_ = { - .descriptor = &this->usb_descriptor_, - .string_descriptor = this->string_descriptor_, - .string_descriptor_count = SIZE, - .external_phy = false, + .port = TINYUSB_PORT_FULL_SPEED_0, + .phy = {.skip_setup = false}, + .descriptor = + { + .device = &this->usb_descriptor_, + .string = this->string_descriptor_, + .string_count = SIZE, + }, }; esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 624f41cf8c6..020542e7490 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -8,7 +8,7 @@ #include <functional> #include "freertos/ringbuf.h" -#include "tusb_cdc_acm.h" +#include "tinyusb_cdc_acm.h" namespace esphome::usb_cdc_acm { diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 1a36ef9f31f..583aa77d063 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -11,7 +11,7 @@ #include "esp_log.h" #include "tusb.h" -#include "tusb_cdc_acm.h" +#include "tinyusb_cdc_acm.h" namespace esphome::usb_cdc_acm { @@ -140,7 +140,6 @@ void USBCDCACMInstance::setup() { // Configure this CDC interface const tinyusb_config_cdcacm_t acm_cfg = { - .usb_dev = TINYUSB_USBDEV_0, .cdc_port = static_cast<tinyusb_cdcacm_itf_t>(this->itf_), .callback_rx = &tinyusb_cdc_rx_callback, .callback_rx_wanted_char = NULL, @@ -148,9 +147,9 @@ void USBCDCACMInstance::setup() { .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback, }; - esp_err_t result = tusb_cdc_acm_init(&acm_cfg); + esp_err_t result = tinyusb_cdcacm_init(&acm_cfg); if (result != ESP_OK) { - ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result); + ESP_LOGE(TAG, "tinyusb_cdcacm_init failed: %d", result); this->parent_->mark_failed(); return; } diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index d83a71624c9..bb94de7e052 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -30,7 +30,7 @@ dependencies: rules: - if: "target in [esp32, esp32p4]" espressif/esp_tinyusb: - version: "1.7.6~1" + version: "2.1.1" rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: From 417858f09816f6d7ec726b11155eaeca63b8d9dc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:01:49 -0400 Subject: [PATCH 0631/2056] [psram] Add ESP32-C61 PSRAM support (#14795) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/psram/__init__.py | 3 +++ tests/components/psram/test.esp32-c61-idf.yaml | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 tests/components/psram/test.esp32-c61-idf.yaml diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 39afb407f10..ccf35b851cc 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -8,6 +8,7 @@ from esphome.components.esp32 import ( CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, VARIANT_ESP32, VARIANT_ESP32C5, + VARIANT_ESP32C61, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -53,6 +54,7 @@ CONF_ENABLE_ECC = "enable_ecc" SPIRAM_MODES = { VARIANT_ESP32: (TYPE_QUAD,), VARIANT_ESP32C5: (TYPE_QUAD,), + VARIANT_ESP32C61: (TYPE_QUAD,), VARIANT_ESP32S2: (TYPE_QUAD,), VARIANT_ESP32S3: (TYPE_QUAD, TYPE_OCTAL), VARIANT_ESP32P4: (TYPE_HEX,), @@ -62,6 +64,7 @@ SPIRAM_MODES = { SPIRAM_SPEEDS = { VARIANT_ESP32: (40, 80, 120), VARIANT_ESP32C5: (40, 80, 120), + VARIANT_ESP32C61: (40, 80), VARIANT_ESP32S2: (40, 80, 120), VARIANT_ESP32S3: (40, 80, 120), VARIANT_ESP32P4: (20, 100, 200), diff --git a/tests/components/psram/test.esp32-c61-idf.yaml b/tests/components/psram/test.esp32-c61-idf.yaml new file mode 100644 index 00000000000..d443aab9518 --- /dev/null +++ b/tests/components/psram/test.esp32-c61-idf.yaml @@ -0,0 +1,7 @@ +esp32: + framework: + type: esp-idf + +psram: + speed: 80MHz + ignore_not_found: false From 271b423b227e8d92938f58ed126246c61afa3fae Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:01:58 -0400 Subject: [PATCH 0632/2056] [psram] Fix ESP-IDF 6.0 compatibility for PSRAM sdkconfig options (#14794) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/psram/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index ccf35b851cc..9b364584ff3 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -181,9 +181,6 @@ async def to_code(config): if config[CONF_MODE] == TYPE_OCTAL: cg.add_platformio_option("board_build.arduino.memory_type", "qio_opi") - add_idf_sdkconfig_option( - f"CONFIG_{get_esp32_variant().upper()}_SPIRAM_SUPPORT", True - ) add_idf_sdkconfig_option("CONFIG_SOC_SPIRAM_SUPPORTED", True) add_idf_sdkconfig_option("CONFIG_SPIRAM", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) @@ -198,11 +195,19 @@ async def to_code(config): speed = int(config[CONF_SPEED][:-3]) add_idf_sdkconfig_option(f"CONFIG_SPIRAM_SPEED_{speed}M", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed) - if config[CONF_MODE] == TYPE_OCTAL and speed == 120: - add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0): + if speed == 120: + variant = get_esp32_variant() + # On chips with MSPI timing tuning, FLASH and PSRAM share the core + # clock so flash frequency must match PSRAM frequency. + # ESP32 and ESP32-S2 don't have this constraint. + if variant not in (VARIANT_ESP32, VARIANT_ESP32S2): + add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) + if config[CONF_MODE] == TYPE_OCTAL and CORE.data[KEY_CORE][ + KEY_FRAMEWORK_VERSION + ] >= cv.Version(5, 4, 0): add_idf_sdkconfig_option( - "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True + "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", + True, ) if config[CONF_ENABLE_ECC]: add_idf_sdkconfig_option("CONFIG_SPIRAM_ECC_ENABLE", True) From d4e1e32a300733b205c5da85d115e3c7c4df4336 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:02:06 -0400 Subject: [PATCH 0633/2056] [mipi_dsi] Fix ESP-IDF 6.0 compatibility for use_dma2d flag (#14792) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/mipi_dsi/mipi_dsi.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 7103e0868dc..e8e9ca2bfbf 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -87,7 +87,9 @@ void MIPI_DSI::setup() { .vsync_front_porch = this->vsync_front_porch_, }, .flags = { +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) .use_dma2d = true, +#endif }}; // clang-format on err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); @@ -95,6 +97,13 @@ void MIPI_DSI::setup() { this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + err = esp_lcd_dpi_panel_enable_dma2d(this->handle_); + if (err != ESP_OK) { + this->smark_failed(LOG_STR("esp_lcd_dpi_panel_enable_dma2d failed"), err); + return; + } +#endif if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(true); From b126f3af3b3949d7e9d9b2cddad211e0fc7bdd29 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:02:13 -0400 Subject: [PATCH 0634/2056] [ledc] Fix ESP-IDF 6.0 compatibility for peripheral reset (#14790) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/ledc/ledc_output.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 592fc7bd0c1..a3d1e4d3922 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -5,10 +5,9 @@ #include <driver/ledc.h> #include <cinttypes> +#include <esp_idf_version.h> #include <esp_private/periph_ctrl.h> -#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) #include <hal/ledc_ll.h> -#endif #define CLOCK_FREQUENCY 80e6f @@ -161,7 +160,14 @@ void LEDCOutput::write_state(float state) { void LEDCOutput::setup() { if (!ledc_peripheral_reset_done) { ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot"); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + PERIPH_RCC_ATOMIC() { + ledc_ll_enable_reset_reg(true); + ledc_ll_enable_reset_reg(false); + } +#else periph_module_reset(PERIPH_LEDC_MODULE); +#endif ledc_peripheral_reset_done = true; } From 158a119a5a6a4591e531739aae6e5f9fb1cb3880 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 10:43:04 -1000 Subject: [PATCH 0635/2056] [sha256] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14809) --- esphome/components/sha256/sha256.cpp | 23 ++++++++++++++++++++++- esphome/components/sha256/sha256.h | 19 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 23995e6534b..079665c9596 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -8,7 +8,28 @@ namespace esphome::sha256 { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_SHA256_PSA) + +// ESP-IDF 6.0 ships mbedtls 4.0 which removed the legacy mbedtls_sha256_* API. +// Use the PSA Crypto API instead. PSA crypto is auto-initialized by ESP-IDF +// at startup, so no psa_crypto_init() call is needed. + +SHA256::~SHA256() { psa_hash_abort(&this->op_); } + +void SHA256::init() { + psa_hash_abort(&this->op_); + this->op_ = PSA_HASH_OPERATION_INIT; + psa_hash_setup(&this->op_, PSA_ALG_SHA_256); +} + +void SHA256::add(const uint8_t *data, size_t len) { psa_hash_update(&this->op_, data, len); } + +void SHA256::calculate() { + size_t hash_length; + psa_hash_finish(&this->op_, this->digest_, sizeof(this->digest_), &hash_length); +} + +#elif defined(USE_SHA256_MBEDTLS) // CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): // diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index bafb359485c..0f995fcd916 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -10,7 +10,20 @@ #include <memory> #include "esphome/core/hash_base.h" -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls_sha256_* API. +// Use the PSA Crypto API instead. PSA crypto is auto-initialized by +// ESP-IDF at startup (esp_psa_crypto_init.c, priority 104). +#define USE_SHA256_PSA +#include <psa/crypto.h> +#else +#define USE_SHA256_MBEDTLS +#include "mbedtls/sha256.h" +#endif +#elif defined(USE_LIBRETINY) +#define USE_SHA256_MBEDTLS #include "mbedtls/sha256.h" #elif defined(USE_ESP8266) || defined(USE_RP2040) #include <bearssl/bearssl_hash.h> @@ -51,7 +64,9 @@ class SHA256 : public esphome::HashBase { size_t get_size() const override { return 32; } protected: -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_SHA256_PSA) + psa_hash_operation_t op_ = PSA_HASH_OPERATION_INIT; +#elif defined(USE_SHA256_MBEDTLS) // The mbedtls context for ESP32-S3 hardware SHA requires proper alignment and stack frame constraints. // See class documentation above for critical requirements. mbedtls_sha256_context ctx_{}; From 27942f19733d442998bd457531ffbc58ac3e8f63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 11:05:39 -1000 Subject: [PATCH 0636/2056] [helpers] Replace deprecated std::is_trivial in FixedRingBuffer (#14808) --- esphome/core/helpers.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 22672087524..c2f4cace9a9 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -417,7 +417,7 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> FixedRingBuffer() = default; ~FixedRingBuffer() { - if constexpr (std::is_trivial<T>::value) { + if constexpr (std::is_trivially_copyable<T>::value && std::is_trivially_default_constructible<T>::value) { ::operator delete(this->data_); } else { delete[] this->data_; @@ -430,7 +430,7 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> /// Allocate capacity - can only be called once void init(index_type capacity) { - if constexpr (std::is_trivial<T>::value) { + if constexpr (std::is_trivially_copyable<T>::value && std::is_trivially_default_constructible<T>::value) { // Raw allocation without initialization (elements are written before read) // NOLINTNEXTLINE(bugprone-sizeof-expression) this->data_ = static_cast<T *>(::operator new(capacity * sizeof(T))); From 447c4669b1d7c3e66becaeeaaaf83071f07928ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 11:26:20 -1000 Subject: [PATCH 0637/2056] [esp32] Disable SHA-512 in mbedTLS on IDF 6.0+ and add idf_version() helper (#14810) --- esphome/components/esp32/__init__.py | 53 +++++++++++++++++++-- esphome/components/esp32/const.py | 1 + esphome/components/esp32_hosted/__init__.py | 15 ++---- esphome/components/ethernet/__init__.py | 10 ++-- esphome/components/psram/__init__.py | 7 +-- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 475de6aa3e4..eaa9aa163d9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -59,6 +59,7 @@ from .const import ( # noqa KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, + KEY_IDF_VERSION, KEY_PATH, KEY_REF, KEY_REPO, @@ -420,9 +421,20 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded # Initialize Arduino library tracking - cg.add_library() auto-enables libraries CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() - CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION] - ) + framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION]) + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver + + # Store the underlying IDF version for framework-agnostic checks + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver + elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: + CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver + else: + raise cv.Invalid( + f"Arduino version {framework_ver} has no known ESP-IDF version mapping. " + "Please update ARDUINO_IDF_VERSION_LOOKUP.", + path=[CONF_FRAMEWORK, CONF_VERSION], + ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE] @@ -974,6 +986,7 @@ KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" KEY_FATFS_REQUIRED = "fatfs_required" +KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" def require_vfs_select() -> None: @@ -1043,6 +1056,25 @@ def require_mbedtls_pkcs7() -> None: CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True +def require_mbedtls_sha512() -> None: + """Mark that mbedTLS SHA-384/SHA-512 support is required by a component. + + Call this from components that need to verify TLS certificates or signatures + using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C + and CONFIG_MBEDTLS_SHA512_C from being disabled. + """ + CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True + + +def idf_version() -> cv.Version: + """Return the underlying ESP-IDF version regardless of framework choice. + + For ESP-IDF builds this is the framework version directly. + For Arduino builds this is the mapped IDF version from ARDUINO_IDF_VERSION_LOOKUP. + """ + return CORE.data[KEY_ESP32][KEY_IDF_VERSION] + + def require_fatfs() -> None: """Mark that FATFS support is required by a component. @@ -1802,6 +1834,21 @@ async def to_code(config): elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]: add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False) + # Disable SHA-384 and SHA-512 in mbedTLS + # ESPHome doesn't use either algorithm. SHA-384 shares the same + # compression function as SHA-512 (mbedtls_internal_sha512_process), + # so both must be disabled to eliminate the ~3KB software fallback + # that IDF 6.0's PSA parallel engine always links in. + # On IDF < 6.0 these are a single config and hardware-only (no + # software fallback), so there was no code size cost to leaving + # them enabled. + # Components that need SHA-384/SHA-512 can call require_mbedtls_sha512() + if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get( + KEY_MBEDTLS_SHA512_REQUIRED, False + ): + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 7874c1c759c..d0d00723fcb 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -15,6 +15,7 @@ KEY_PATH = "path" KEY_SUBMODULES = "submodules" KEY_EXTRA_BUILD_FILES = "extra_build_files" KEY_FULL_CERT_BUNDLE = "full_cert_bundle" +KEY_IDF_VERSION = "idf_version" VARIANT_ESP32 = "ESP32" VARIANT_ESP32C2 = "ESP32C2" diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index a51ae2cd666..3f9185745dd 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -4,14 +4,7 @@ from pathlib import Path from esphome import pins from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import ( - CONF_CLK_PIN, - CONF_RESET_PIN, - CONF_VARIANT, - KEY_CORE, - KEY_FRAMEWORK_VERSION, -) -from esphome.core import CORE +from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] @@ -100,9 +93,9 @@ async def to_code(config): int(config[CONF_SDIO_FREQUENCY] // 1000), ) - framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" - if framework_ver >= cv.Version(5, 5, 0): + idf_ver = esp32.idf_version() + os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}" + if idf_ver >= cv.Version(5, 5, 0): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 935d2004d49..e520c0e914d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, get_esp32_variant, + idf_version, include_builtin_idf_component, ) from esphome.components.network import ip_address_literal @@ -176,13 +177,12 @@ ManualIP = ethernet_ns.struct("ManualIP") def _is_framework_spi_polling_mode_supported(): # SPI Ethernet without IRQ feature is added in # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) - # Note: Arduino now uses ESP-IDF as a component, so we only check IDF version - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if framework_version >= cv.Version(5, 3, 0): + ver = idf_version() + if ver >= cv.Version(5, 3, 0): return True - if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): + if cv.Version(5, 3, 0) > ver >= cv.Version(5, 2, 1): return True - if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 + if cv.Version(5, 2, 0) > ver >= cv.Version(5, 1, 4): # noqa: SIM103 return True return False diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 9b364584ff3..86c17ce9ca0 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, add_idf_sdkconfig_option, get_esp32_variant, + idf_version, ) import esphome.config_validation as cv from esphome.const import ( @@ -23,8 +24,6 @@ from esphome.const import ( CONF_ID, CONF_MODE, CONF_SPEED, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, ) from esphome.core import CORE @@ -202,9 +201,7 @@ async def to_code(config): # ESP32 and ESP32-S2 don't have this constraint. if variant not in (VARIANT_ESP32, VARIANT_ESP32S2): add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) - if config[CONF_MODE] == TYPE_OCTAL and CORE.data[KEY_CORE][ - KEY_FRAMEWORK_VERSION - ] >= cv.Version(5, 4, 0): + if config[CONF_MODE] == TYPE_OCTAL and idf_version() >= cv.Version(5, 4, 0): add_idf_sdkconfig_option( "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True, From 234ca7c9514351e059bb3cea5a1c0d15fea88f5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:17:32 -1000 Subject: [PATCH 0638/2056] [debug] Fix shared buffer between reset reason and wakeup cause (#14813) --- esphome/components/debug/debug_component.h | 3 ++- esphome/components/debug/debug_esp32.cpp | 9 +++++---- esphome/components/debug/debug_esp8266.cpp | 2 +- esphome/components/debug/debug_host.cpp | 2 +- esphome/components/debug/debug_libretiny.cpp | 2 +- esphome/components/debug/debug_rp2040.cpp | 2 +- esphome/components/debug/debug_zephyr.cpp | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index e4f4bb36eba..3da6b800c6f 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -18,6 +18,7 @@ namespace debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; +static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128; // buf_append_printf is now provided by esphome/core/helpers.h @@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent { #endif // USE_TEXT_SENSOR const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); - const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); + const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer); uint32_t get_free_heap_(); size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos); void update_platform_(); diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 6898621dd05..c9df4fdf210 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = { "BT", }; -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { const char *wake_reason; unsigned reason = esp_sleep_get_wakeup_cause(); if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) { @@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; - const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); - const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); + char reset_buffer[RESET_REASON_BUFFER_SIZE]; + char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE]; + const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer)); + const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer)); uint8_t mac[6]; get_mac_address_raw(mac); diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 4df4aaa8513..0519ab72fe9 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buffer.data(); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // ESP8266 doesn't have detailed wakeup cause like ESP32 return ""; } diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp index 2fa88f0909c..0dfab86e4c4 100644 --- a/esphome/components/debug/debug_host.cpp +++ b/esphome/components/debug/debug_host.cpp @@ -7,7 +7,7 @@ namespace debug { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 39269d6f2f2..1d458c602a6 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return lt_get_reboot_reason_name(lt_get_reboot_reason()); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index 8dc84a26732..73f08492c86 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bd6432e9499..bf87b7ae3dc 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // Zephyr doesn't have detailed wakeup cause like ESP32 return ""; } From cc4c13930f7ad862c17dbbd630169f09a5e9af1e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:17:43 -1000 Subject: [PATCH 0639/2056] [hmac_sha256] Migrate to PSA Crypto MAC API for ESP-IDF 6.0 (#14814) --- .../components/hmac_sha256/hmac_sha256.cpp | 52 ++++++++++++++++++- esphome/components/hmac_sha256/hmac_sha256.h | 21 +++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index 2146e961bc9..c113cb48a6f 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -7,7 +7,55 @@ namespace esphome::hmac_sha256 { constexpr size_t SHA256_DIGEST_SIZE = 32; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_HMAC_SHA256_PSA) + +// ESP-IDF 6.0 ships mbedtls 4.0 which removed the legacy mbedtls_md HMAC API. +// Use the PSA Crypto MAC API instead. + +HmacSHA256::~HmacSHA256() { + psa_mac_abort(&this->op_); + psa_destroy_key(this->key_id_); +} + +void HmacSHA256::init(const uint8_t *key, size_t len) { + psa_mac_abort(&this->op_); + psa_destroy_key(this->key_id_); + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_HMAC); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attributes, PSA_ALG_HMAC(PSA_ALG_SHA_256)); + psa_import_key(&attributes, key, len, &this->key_id_); + + this->op_ = PSA_MAC_OPERATION_INIT; + psa_mac_sign_setup(&this->op_, this->key_id_, PSA_ALG_HMAC(PSA_ALG_SHA_256)); +} + +void HmacSHA256::add(const uint8_t *data, size_t len) { psa_mac_update(&this->op_, data, len); } + +void HmacSHA256::calculate() { + size_t mac_length; + psa_mac_sign_finish(&this->op_, this->digest_, sizeof(this->digest_), &mac_length); +} + +void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } + +void HmacSHA256::get_hex(char *output) { + format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); +} + +bool HmacSHA256::equals_bytes(const uint8_t *expected) { + return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0; +} + +bool HmacSHA256::equals_hex(const char *expected) { + char hex_output[SHA256_DIGEST_SIZE * 2 + 1]; + this->get_hex(hex_output); + hex_output[SHA256_DIGEST_SIZE * 2] = '\0'; + return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0; +} + +#elif defined(USE_HMAC_SHA256_MBEDTLS) HmacSHA256::~HmacSHA256() { mbedtls_md_free(&this->ctx_); } @@ -93,7 +141,7 @@ bool HmacSHA256::equals_bytes(const uint8_t *expected) { return this->ohash_.equ bool HmacSHA256::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } -#endif // USE_ESP32 || USE_LIBRETINY +#endif // USE_HMAC_SHA256_PSA / USE_HMAC_SHA256_MBEDTLS } // namespace esphome::hmac_sha256 #endif diff --git a/esphome/components/hmac_sha256/hmac_sha256.h b/esphome/components/hmac_sha256/hmac_sha256.h index 85622cac46b..22129b1182f 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.h +++ b/esphome/components/hmac_sha256/hmac_sha256.h @@ -5,7 +5,19 @@ #include <string> -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls_md HMAC API. +// Use the PSA Crypto MAC API instead. +#define USE_HMAC_SHA256_PSA +#include <psa/crypto.h> +#else +#define USE_HMAC_SHA256_MBEDTLS +#include "mbedtls/md.h" +#endif +#elif defined(USE_LIBRETINY) +#define USE_HMAC_SHA256_MBEDTLS #include "mbedtls/md.h" #else #include "esphome/components/sha256/sha256.h" @@ -45,7 +57,12 @@ class HmacSHA256 { bool equals_hex(const char *expected); protected: -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_HMAC_SHA256_PSA) + static constexpr size_t SHA256_DIGEST_SIZE = 32; + psa_mac_operation_t op_ = PSA_MAC_OPERATION_INIT; + mbedtls_svc_key_id_t key_id_ = MBEDTLS_SVC_KEY_ID_INIT; + uint8_t digest_[SHA256_DIGEST_SIZE]{}; +#elif defined(USE_HMAC_SHA256_MBEDTLS) static constexpr size_t SHA256_DIGEST_SIZE = 32; mbedtls_md_context_t ctx_{}; uint8_t digest_[SHA256_DIGEST_SIZE]{}; From 0edc0fd9c885da0ecc5184a80bae878c02777041 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:17:56 -1000 Subject: [PATCH 0640/2056] [esp32_ble_tracker] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14811) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 44 ++++++++++++++++--- .../esp32_ble_tracker/esp32_ble_tracker.h | 7 +++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5a43cf7e49b..6dce70f8396 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -27,8 +27,14 @@ #include <esp_coexist.h> #endif +#ifdef USE_ESP32_BLE_DEVICE +#ifdef USE_BLE_TRACKER_PSA_AES +#include <psa/crypto.h> +#else #define MBEDTLS_AES_ALT #include <aes_alt.h> +#endif +#endif // USE_ESP32_BLE_DEVICE // bt_trace.h #undef TAG @@ -738,23 +744,48 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { } bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { - uint8_t ecb_key[16]; - uint8_t ecb_plaintext[16]; - uint8_t ecb_ciphertext[16]; + static constexpr size_t AES_BLOCK_SIZE = 16; + static constexpr size_t AES_KEY_BITS = 128; + + uint8_t ecb_key[AES_BLOCK_SIZE]; + uint8_t ecb_plaintext[AES_BLOCK_SIZE]; + uint8_t ecb_ciphertext[AES_BLOCK_SIZE]; uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_); - memcpy(&ecb_key, irk, 16); - memset(&ecb_plaintext, 0, 16); + memcpy(&ecb_key, irk, AES_BLOCK_SIZE); + memset(&ecb_plaintext, 0, AES_BLOCK_SIZE); ecb_plaintext[13] = (addr64 >> 40) & 0xff; ecb_plaintext[14] = (addr64 >> 32) & 0xff; ecb_plaintext[15] = (addr64 >> 24) & 0xff; +#ifdef USE_BLE_TRACKER_PSA_AES + // Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+) + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, AES_KEY_BITS); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING); + + mbedtls_svc_key_id_t key_id; + if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) { + return false; + } + + size_t output_length; + psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE, + ecb_ciphertext, AES_BLOCK_SIZE, &output_length); + psa_destroy_key(key_id); + if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) { + return false; + } +#else + // Use legacy mbedtls AES API (IDF < 6.0) mbedtls_aes_context ctx = {0, 0, {0}}; mbedtls_aes_init(&ctx); - if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) { + if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) { mbedtls_aes_free(&ctx); return false; } @@ -765,6 +796,7 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { } mbedtls_aes_free(&ctx); +#endif return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 7f1c2b0f7c8..f50ed107b6d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -12,6 +12,13 @@ #ifdef USE_ESP32 +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API. +// Use the PSA Crypto API instead. +#define USE_BLE_TRACKER_PSA_AES +#endif + #include <esp_bt_defs.h> #include <esp_gap_ble_api.h> #include <esp_gattc_api.h> From efc508a82bf4edb19196761ee57cc388ec78d3b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:18:40 -1000 Subject: [PATCH 0641/2056] [dlms_meter] Migrate GCM to PSA AEAD API for ESP-IDF 6.0 (#14817) --- esphome/components/dlms_meter/dlms_meter.cpp | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index bd2150e8ddf..052a0f4d01f 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -3,9 +3,14 @@ #if defined(USE_ESP8266_FRAMEWORK_ARDUINO) #include <bearssl/bearssl.h> #elif defined(USE_ESP32) +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#include <psa/crypto.h> +#else #include "mbedtls/esp_config.h" #include "mbedtls/gcm.h" #endif +#endif namespace esphome::dlms_meter { @@ -240,6 +245,35 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m br_gcm_flip(&gcm_ctx); br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length); #elif defined(USE_ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // PSA Crypto multipart AEAD (no tag verification, matching legacy behavior) + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, this->decryption_key_.size() * 8); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_GCM); + + mbedtls_svc_key_id_t key_id; + bool decrypt_failed = true; + if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) { + psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT; + if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS && + psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) { + size_t outlen = 0; + if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS && + outlen == message_length) { + decrypt_failed = false; + } + } + psa_aead_abort(&op); + psa_destroy_key(key_id); + } + if (decrypt_failed) { + ESP_LOGE(TAG, "Decryption failed"); + this->receive_buffer_.clear(); + return false; + } +#else size_t outlen = 0; mbedtls_gcm_context gcm_ctx; mbedtls_gcm_init(&gcm_ctx); @@ -252,6 +286,7 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m this->receive_buffer_.clear(); return false; } +#endif #else #error "Invalid Platform" #endif From d7c42bc9ec72f02ca802f75152bb869734e30d05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:38:51 -1000 Subject: [PATCH 0642/2056] [debug] Fix ESP-IDF 6.0 compatibility for wakeup cause API (#14812) --- esphome/components/debug/debug_esp32.cpp | 85 ++++++++++++++++++------ 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index c9df4fdf210..aa379599c62 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -5,6 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include <esp_sleep.h> +#include <esp_idf_version.h> #include <esp_heap_caps.h> #include <esp_system.h> @@ -82,32 +83,74 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) static const char *const WAKEUP_CAUSES[] = { - "undefined", - "undefined", - "external signal using RTC_IO", - "external signal using RTC_CNTL", - "timer", - "touchpad", - "ULP program", - "GPIO", - "UART", - "WIFI", - "COCPU int", - "COCPU crash", - "BT", + "undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0) + "undefined", // ESP_SLEEP_WAKEUP_ALL (1) + "external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2) + "external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3) + "timer", // ESP_SLEEP_WAKEUP_TIMER (4) + "touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5) + "ULP program", // ESP_SLEEP_WAKEUP_ULP (6) + "GPIO", // ESP_SLEEP_WAKEUP_GPIO (7) + "UART", // ESP_SLEEP_WAKEUP_UART (8) + "UART1", // ESP_SLEEP_WAKEUP_UART1 (9) + "UART2", // ESP_SLEEP_WAKEUP_UART2 (10) + "WIFI", // ESP_SLEEP_WAKEUP_WIFI (11) + "COCPU int", // ESP_SLEEP_WAKEUP_COCPU (12) + "COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (13) + "BT", // ESP_SLEEP_WAKEUP_BT (14) + "VAD", // ESP_SLEEP_WAKEUP_VAD (15) + "VBAT under voltage", // ESP_SLEEP_WAKEUP_VBAT_UNDER_VOLT (16) }; +#else +static const char *const WAKEUP_CAUSES[] = { + "undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0) + "undefined", // ESP_SLEEP_WAKEUP_ALL (1) + "external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2) + "external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3) + "timer", // ESP_SLEEP_WAKEUP_TIMER (4) + "touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5) + "ULP program", // ESP_SLEEP_WAKEUP_ULP (6) + "GPIO", // ESP_SLEEP_WAKEUP_GPIO (7) + "UART", // ESP_SLEEP_WAKEUP_UART (8) + "WIFI", // ESP_SLEEP_WAKEUP_WIFI (9) + "COCPU int", // ESP_SLEEP_WAKEUP_COCPU (10) + "COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (11) + "BT", // ESP_SLEEP_WAKEUP_BT (12) +}; +#endif const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { - const char *wake_reason; - unsigned reason = esp_sleep_get_wakeup_cause(); - if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) { - wake_reason = WAKEUP_CAUSES[reason]; - } else { - wake_reason = "unknown source"; + static constexpr auto NUM_CAUSES = sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0]); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // IDF 6.0+ returns a bitmap of all wakeup sources + uint32_t causes = esp_sleep_get_wakeup_causes(); + if (causes == 0) { + return WAKEUP_CAUSES[0]; // "undefined" } - // Return the static string directly - no need to copy to buffer - return wake_reason; + char *p = buffer.data(); + char *end = p + buffer.size(); + *p = '\0'; + const char *sep = ""; + for (unsigned i = 0; i < NUM_CAUSES && p < end; i++) { + if (causes & (1U << i)) { + size_t needed = strlen(sep) + strlen(WAKEUP_CAUSES[i]); + if (p + needed >= end) { + break; + } + p += snprintf(p, end - p, "%s%s", sep, WAKEUP_CAUSES[i]); + sep = ", "; + } + } + return buffer.data(); +#else + unsigned reason = esp_sleep_get_wakeup_cause(); + if (reason < NUM_CAUSES) { + return WAKEUP_CAUSES[reason]; + } + return "unknown source"; +#endif } void DebugComponent::log_partition_info_() { From d37f8876d73a637045b3e65f4aca525572a5aab1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:39:07 -1000 Subject: [PATCH 0643/2056] [bthome_mithermometer][xiaomi_ble] Migrate CCM to PSA AEAD API for ESP-IDF 6.0 (#14816) --- .../bthome_mithermometer/bthome_ble.cpp | 37 ++++++++++++++++++ esphome/components/xiaomi_ble/xiaomi_ble.cpp | 39 +++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index 2b73d8735c7..32278dbfbd4 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -10,7 +10,12 @@ #ifdef USE_ESP32 +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#include <psa/crypto.h> +#else #include "mbedtls/ccm.h" +#endif namespace esphome { namespace bthome_mithermometer { @@ -196,6 +201,37 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da const uint8_t *ciphertext = data.data() + 1; const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // PSA AEAD expects ciphertext + tag concatenated + // BLE advertisement max payload is 31 bytes, so this is always sufficient + static constexpr size_t MAX_CT_WITH_TAG = 32; + uint8_t ct_with_tag[MAX_CT_WITH_TAG]; + size_t ct_with_tag_size = ciphertext_size + BTHOME_MIC_SIZE; + memcpy(ct_with_tag, ciphertext, ciphertext_size); + memcpy(ct_with_tag + ciphertext_size, mic, BTHOME_MIC_SIZE); + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BTHOME_BINDKEY_SIZE * 8); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE)); + + mbedtls_svc_key_id_t key_id; + if (psa_import_key(&attributes, this->bindkey_, BTHOME_BINDKEY_SIZE, &key_id) != PSA_SUCCESS) { + ESP_LOGVV(TAG, "psa_import_key() failed."); + return false; + } + + size_t plaintext_length; + psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE), + nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size, + payload.data(), ciphertext_size, &plaintext_length); + psa_destroy_key(key_id); + if (status != PSA_SUCCESS || plaintext_length != ciphertext_size) { + ESP_LOGVV(TAG, "BTHome decryption failed."); + return false; + } +#else mbedtls_ccm_context ctx; mbedtls_ccm_init(&ctx); @@ -213,6 +249,7 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret); return false; } +#endif return true; } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 97a660f0e3a..2c1611d0c7d 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -5,7 +5,12 @@ #ifdef USE_ESP32 #include <vector> +#include <esp_idf_version.h> +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#include <psa/crypto.h> +#else #include "mbedtls/ccm.h" +#endif namespace esphome { namespace xiaomi_ble { @@ -314,6 +319,32 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // PSA AEAD expects ciphertext + tag concatenated + uint8_t ct_with_tag[sizeof(vector.ciphertext) + sizeof(vector.tag)]; + memcpy(ct_with_tag, vector.ciphertext, vector.datasize); + memcpy(ct_with_tag + vector.datasize, vector.tag, vector.tagsize); + size_t ct_with_tag_size = vector.datasize + vector.tagsize; + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, vector.keysize * 8); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, vector.tagsize)); + + mbedtls_svc_key_id_t key_id; + if (psa_import_key(&attributes, vector.key, vector.keysize, &key_id) != PSA_SUCCESS) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): psa_import_key() failed."); + return false; + } + + size_t plaintext_length; + psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, vector.tagsize), + vector.iv, vector.ivsize, vector.authdata, vector.authsize, ct_with_tag, + ct_with_tag_size, vector.plaintext, vector.datasize, &plaintext_length); + psa_destroy_key(key_id); + bool decrypt_ok = (status == PSA_SUCCESS && plaintext_length == vector.datasize); +#else mbedtls_ccm_context ctx; mbedtls_ccm_init(&ctx); @@ -326,7 +357,11 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); - if (ret) { + mbedtls_ccm_free(&ctx); + bool decrypt_ok = (ret == 0); +#endif + + if (!decrypt_ok) { uint8_t mac_address[6] = {0}; memcpy(mac_address, mac_reverse + 5, 1); memcpy(mac_address + 1, mac_reverse + 4, 1); @@ -346,7 +381,6 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty_to(hex_buf, vector.iv, vector.ivsize)); ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty_to(hex_buf, vector.ciphertext, vector.datasize)); ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty_to(hex_buf, vector.tag, vector.tagsize)); - mbedtls_ccm_free(&ctx); return false; } @@ -367,7 +401,6 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty_to(hex_buf, raw.data() + cipher_pos, vector.datasize), static_cast<int>(raw[4])); - mbedtls_ccm_free(&ctx); return true; } From fe9f19d9ed078db9eb3d82e2fb698c90dc9e6043 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:30:12 -0400 Subject: [PATCH 0644/2056] [mqtt] Fix ESP-IDF 6.0 compatibility for external MQTT component (#14822) --- esphome/components/mqtt/__init__.py | 8 +++++++- esphome/idf_component.yml | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index d110d7c1602..817f99375ea 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -3,7 +3,9 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger, socket from esphome.components.esp32 import ( + add_idf_component, add_idf_sdkconfig_option, + idf_version, include_builtin_idf_component, ) from esphome.config_helpers import filter_source_files_from_platform @@ -351,7 +353,11 @@ async def to_code(config): if CORE.is_esp32: socket.require_wake_loop_threadsafe() # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) - include_builtin_idf_component("mqtt") + # IDF 6.0 moved esp-mqtt to an external component + if idf_version() >= cv.Version(6, 0, 0): + add_idf_component(name="espressif/mqtt", ref="1.0.0") + else: + include_builtin_idf_component("mqtt") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index bb94de7e052..1e2d452919c 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -37,5 +37,9 @@ dependencies: version: 0.3.2 rules: - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" + espressif/mqtt: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0" esp32async/asynctcp: version: 3.4.91 From 7f418d969e9bd3de284123fcbae113e5c36a09d6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:57:52 -0400 Subject: [PATCH 0645/2056] [multiple] Fix implicit int-to-gpio_num_t conversions for GCC 15 (#14830) --- esphome/components/ledc/ledc_output.cpp | 3 ++- esphome/components/mipi_rgb/mipi_rgb.cpp | 17 +++++++++-------- .../pulse_counter/pulse_counter_sensor.cpp | 5 +++-- esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 13 +++++++------ esphome/components/st7701s/st7701s.cpp | 13 +++++++------ 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index a3d1e4d3922..d2f2d72acb9 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -3,6 +3,7 @@ #ifdef USE_ESP32 +#include <driver/gpio.h> #include <driver/ledc.h> #include <cinttypes> #include <esp_idf_version.h> @@ -189,7 +190,7 @@ void LEDCOutput::setup() { this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; - chan_conf.gpio_num = this->pin_->get_pin(); + chan_conf.gpio_num = static_cast<gpio_num_t>(this->pin_->get_pin()); chan_conf.speed_mode = speed_mode; chan_conf.channel = chan_num; chan_conf.intr_type = LEDC_INTR_DISABLE; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index ae7c7958464..824ff6afe79 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -4,7 +4,8 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esp_lcd_panel_rgb.h" +#include <driver/gpio.h> +#include <esp_lcd_panel_rgb.h> #include <span> namespace esphome { @@ -153,18 +154,18 @@ void MipiRgb::common_setup_() { config.clk_src = LCD_CLK_SRC_PLL160M; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { - config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin()); } config.data_width = data_pin_count; - config.disp_gpio_num = -1; - config.hsync_gpio_num = this->hsync_pin_->get_pin(); - config.vsync_gpio_num = this->vsync_pin_->get_pin(); + config.disp_gpio_num = GPIO_NUM_NC; + config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin()); + config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin()); if (this->de_pin_) { - config.de_gpio_num = this->de_pin_->get_pin(); + config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin()); } else { - config.de_gpio_num = -1; + config.de_gpio_num = GPIO_NUM_NC; } - config.pclk_gpio_num = this->pclk_pin_->get_pin(); + config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin()); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); if (err == ESP_OK) err = esp_lcd_panel_reset(this->handle_); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index ec00bd024e0..5d73bef7da0 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #ifdef HAS_PCNT +#include <driver/gpio.h> #include <esp_private/esp_clk.h> #include <hal/pcnt_ll.h> #endif @@ -76,8 +77,8 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } pcnt_chan_config_t chan_config = { - .edge_gpio_num = this->pin->get_pin(), - .level_gpio_num = -1, + .edge_gpio_num = static_cast<gpio_num_t>(this->pin->get_pin()), + .level_gpio_num = GPIO_NUM_NC, }; error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel); if (error != ESP_OK) { diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 363f4b63b8e..d29f6a0bcb9 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -2,6 +2,7 @@ #include "rpi_dpi_rgb.h" #include "esphome/core/gpio.h" #include "esphome/core/log.h" +#include <driver/gpio.h> namespace esphome { namespace rpi_dpi_rgb { @@ -25,14 +26,14 @@ void RpiDpiRgb::setup() { config.clk_src = LCD_CLK_SRC_PLL160M; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { - config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin()); } config.data_width = data_pin_count; - config.disp_gpio_num = -1; - config.hsync_gpio_num = this->hsync_pin_->get_pin(); - config.vsync_gpio_num = this->vsync_pin_->get_pin(); - config.de_gpio_num = this->de_pin_->get_pin(); - config.pclk_gpio_num = this->pclk_pin_->get_pin(); + config.disp_gpio_num = GPIO_NUM_NC; + config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin()); + config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin()); + config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin()); + config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin()); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index ecce4eb4b27..701b6dd79ea 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -2,6 +2,7 @@ #include "st7701s.h" #include "esphome/core/gpio.h" #include "esphome/core/log.h" +#include <driver/gpio.h> namespace esphome { namespace st7701s { @@ -27,14 +28,14 @@ void ST7701S::setup() { config.clk_src = LCD_CLK_SRC_PLL160M; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { - config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin()); } config.data_width = data_pin_count; - config.disp_gpio_num = -1; - config.hsync_gpio_num = this->hsync_pin_->get_pin(); - config.vsync_gpio_num = this->vsync_pin_->get_pin(); - config.de_gpio_num = this->de_pin_->get_pin(); - config.pclk_gpio_num = this->pclk_pin_->get_pin(); + config.disp_gpio_num = GPIO_NUM_NC; + config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin()); + config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin()); + config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin()); + config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin()); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); From 18a082de30abe7e3e0a525fac10dcf67fb3ef375 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:58:01 -0400 Subject: [PATCH 0646/2056] [ci] Support URL and version extras in generate-esp32-boards.py (#14828) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- script/generate-esp32-boards.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/script/generate-esp32-boards.py b/script/generate-esp32-boards.py index ab4a38ced5b..9fa0f652efb 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -7,17 +7,26 @@ import subprocess import sys import tempfile +from esphome import config_validation as cv from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP from esphome.helpers import write_file_if_changed ver = PLATFORM_VERSION_LOOKUP["recommended"] -version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" root = Path(__file__).parent.parent boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py" def get_boards(): with tempfile.TemporaryDirectory() as tempdir: + if isinstance(ver, cv.Version): + branch = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + branch += f"-{ver.extra}" + repo = "https://github.com/pioarduino/platform-espressif32" + else: + # URL format: "https://github.com/user/repo.git#branch" + url = str(ver) + repo, branch = url.rsplit("#", 1) if "#" in url else (url, "main") subprocess.run( [ "git", @@ -28,8 +37,8 @@ def get_boards(): "--depth", "1", "--branch", - f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}", - "https://github.com/pioarduino/platform-espressif32", + branch, + repo, tempdir, ], check=True, From 33f9ad9cee4e712d14721966318f99d8d0f63238 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:58:12 -0400 Subject: [PATCH 0647/2056] [esp32] Support non-numeric version extras in IDF version string (#14826) --- esphome/components/esp32/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index eaa9aa163d9..18178c83ffc 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -612,10 +612,12 @@ def _format_framework_espidf_version( ext = "tar.xz" else: ext = "zip" - # Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1") + # Build version string with extra separator based on type: + # numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1") ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" if ver.extra: - ver_str += f".{ver.extra}" + sep = "." if str(ver.extra).isdigit() else "-" + ver_str += f"{sep}{ver.extra}" if release: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" From 4219d6d3673d8a1e4d8f36ac6ce45b162c898292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:28:19 -1000 Subject: [PATCH 0648/2056] Bump tornado from 6.5.4 to 6.5.5 (#14704) 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 3da2d52b44b..e634bcb1046 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.4 +tornado==6.5.5 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 From 2627490a11952d55ef282c5b6b57522e6467123e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:30:44 -0400 Subject: [PATCH 0649/2056] [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 6d49053d6d5..a51ae2cd666 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -105,7 +105,7 @@ async def to_code(config): if framework_ver >= cv.Version(5, 5, 0): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index acd7f7a4798..f7fd3e67bc3 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -20,7 +20,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.0 + version: 2.12.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 064bd13ebb6ba91576c7e6c78c23a2986ad5e5d5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 19:56:25 -0500 Subject: [PATCH 0650/2056] [ethernet] ESP32-P4 Ethernet compilation fix (#14714) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/ethernet/ethernet_component.cpp | 18 +----------------- .../components/ethernet/ethernet_component.h | 2 ++ esphome/components/ethernet/ethernet_helpers.c | 8 ++++++++ .../components/ethernet/test.esp32-p4-idf.yaml | 1 + 4 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 esphome/components/ethernet/ethernet_helpers.c create mode 100644 tests/components/ethernet/test.esp32-p4-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d6b0d40cd95..e0788e11498 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -21,22 +21,6 @@ namespace esphome::ethernet { -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) -// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 -#ifdef USE_ESP32_VARIANT_ESP32P4 -#undef ETH_ESP32_EMAC_DEFAULT_CONFIG -#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ - { \ - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ - .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ - .emac_dataif_gpio = \ - {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ - } -#endif -#endif - static const char *const TAG = "ethernet"; // PHY register size for hex logging @@ -162,7 +146,7 @@ void EthernetComponent::setup() { phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; - eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d9f05be9de0..c464e20b843 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,6 +15,8 @@ #include "esp_mac.h" #include "esp_idf_version.h" +extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); + namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c new file mode 100644 index 00000000000..96faccad24c --- /dev/null +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -0,0 +1,8 @@ +#include "esp_eth_mac_esp.h" + +// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers +// which are valid in C but not in C++. This wrapper allows C++ code to get +// the default config without replicating the macro's contents. +eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { + return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); +} diff --git a/tests/components/ethernet/test.esp32-p4-idf.yaml b/tests/components/ethernet/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..e52329d7ea2 --- /dev/null +++ b/tests/components/ethernet/test.esp32-p4-idf.yaml @@ -0,0 +1 @@ +<<: !include common-ip101.yaml From 2b0c471ed7b08e422fe48fd7153c22cfd8f1a23a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 15:00:20 -1000 Subject: [PATCH 0651/2056] [esp32] Add crash handler to capture and report backtrace across reboots (#14709) --- esphome/components/api/api_connection.h | 6 + esphome/components/api/client.py | 21 ++ esphome/components/esp32/__init__.py | 5 + esphome/components/esp32/core.cpp | 6 + esphome/components/esp32/crash_handler.cpp | 355 +++++++++++++++++++++ esphome/components/esp32/crash_handler.h | 18 ++ esphome/components/logger/logger_esp32.cpp | 4 + esphome/core/defines.h | 1 + esphome/platformio_api.py | 7 + tests/unit_tests/test_platformio_api.py | 28 ++ 10 files changed, 451 insertions(+) create mode 100644 esphome/components/esp32/crash_handler.cpp create mode 100644 esphome/components/esp32/crash_handler.h diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3356511684f..60cc3e91b11 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -14,6 +14,9 @@ #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_CRASH_HANDLER +#include "esphome/components/esp32/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -235,6 +238,9 @@ class APIConnection final : public APIServerConnectionBase { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } #ifdef USE_API_HOMEASSISTANT_SERVICES void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 200d0938bd5..0e71ad8fcbf 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime +import importlib import logging from typing import TYPE_CHECKING, Any import warnings @@ -18,6 +19,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE +from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard + backtrace_state = False + + # Try platform-specific stacktrace handler first, fall back to generic + platform_process_stacktrace = None + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + platform_process_stacktrace = getattr(module, "process_stacktrace") + except (AttributeError, ImportError): + pass def on_log(msg: SubscribeLogsResponse) -> None: """Handle a new log message.""" + nonlocal backtrace_state time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") @@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: ) for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + for raw_line in text.splitlines(): + if platform_process_stacktrace: + backtrace_state = platform_process_stacktrace( + config, raw_line, backtrace_state + ) + else: + backtrace_state = process_stacktrace( + config, raw_line, backtrace_state=backtrace_state + ) stop = await async_run(cli, on_log, name=name) try: diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 52e70501dcf..475de6aa3e4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1442,6 +1442,11 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32") cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") + # Arduino already wraps esp_panic_handler for its own backtrace handler, + # so only add our wrap when using ESP-IDF framework to avoid linker conflicts. + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + cg.add_build_flag("-Wl,--wrap=esp_panic_handler") + cg.add_define("USE_ESP32_CRASH_HANDLER") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 46c000562e1..cba25bca2b2 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esphome/core/defines.h" +#include "crash_handler.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -36,6 +37,11 @@ void arch_restart() { } void arch_init() { +#ifdef USE_ESP32_CRASH_HANDLER + // Read crash data from previous boot before anything else + esp32::crash_handler_read_and_clear(); +#endif + // Enable the task watchdog only on the loop task (from which we're currently running) esp_task_wdt_add(nullptr); diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp new file mode 100644 index 00000000000..ecf30d78781 --- /dev/null +++ b/esphome/components/esp32/crash_handler.cpp @@ -0,0 +1,355 @@ +#ifdef USE_ESP32 + +#include "esphome/core/defines.h" +#ifdef USE_ESP32_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> +#include <cstring> +#include <esp_attr.h> +#include <esp_private/panic_internal.h> +#include <soc/soc.h> + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +#include <esp_cpu_utils.h> +#include <esp_debug_helpers.h> +#include <xtensa_context.h> +#elif CONFIG_IDF_TARGET_ARCH_RISCV +#include <riscv/rvruntime-frames.h> +#endif + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +static constexpr size_t MAX_BACKTRACE = 16; + +// Check if an address looks like code (flash-mapped or IRAM). +// Must be safe to call from panic context (no flash access needed). +static inline bool IRAM_ATTR is_code_addr(uint32_t addr) { + return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH); +} + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Check if a code address is a real return address by verifying the preceding +// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during +// panic) so flash cache is available and both IRAM and IROM are safely readable. +static inline bool is_return_addr(uint32_t addr) { + if (!is_code_addr(addr) || addr < 4) + return false; + // A return address on the stack points to the instruction after a call. + // Check for 4-byte JAL/JALR call instruction before this address. + // Use memcpy for alignment safety — RISC-V C extension means code addresses + // are only 2-byte aligned, so addr-4 may not be 4-byte aligned. + uint32_t inst; + memcpy(&inst, (const void *) (addr - 4), sizeof(inst)); + // RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd + uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode + uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7) + // Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7) + if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80) + return true; + // Check for 2-byte compressed c.jalr before this address (C extension). + // c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10 + if (addr >= 2) { + uint16_t c_inst = *(uint16_t *) (addr - 2); + if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0) + return true; + } + return false; +} +#endif + +// Raw crash data written by the panic handler wrapper. +// Lives in .noinit so it survives software reset but contains garbage after power cycle. +// Validated by magic marker. Static linkage since it's only used within this file. +// Version field is first so future firmware can always identify the struct layout. +// Magic is second to validate the data. Remaining fields can change between versions. +// Version is uint32_t because it would be padded to 4 bytes anyway before the next +// uint32_t field, so we use the full width rather than wasting 3 bytes of padding. +static constexpr uint32_t CRASH_DATA_VERSION = 1; +struct RawCrashData { + uint32_t version; + uint32_t magic; + uint32_t pc; + uint8_t backtrace_count; + uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned) + uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG) + uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) +}; +static RawCrashData __attribute__((section(".noinit"))) +s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Whether crash data was found and validated this boot. +static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +namespace esphome::esp32 { + +static const char *const TAG = "esp32.crash"; + +void crash_handler_read_and_clear() { + if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) { + s_crash_data_valid = true; + // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data + if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count) + s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count; + if (s_raw_crash_data.exception > 4) // panic_exception_t max value + s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT + if (s_raw_crash_data.pseudo_excause > 1) + s_raw_crash_data.pseudo_excause = 0; + } + // Clear magic regardless so we don't re-report on next normal reboot + s_raw_crash_data.magic = 0; +} + +bool crash_handler_has_data() { return s_crash_data_valid; } + +// Look up the exception cause as a human-readable string. +// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays +// not exposed via any public API. +static const char *get_exception_reason() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + if (s_raw_crash_data.pseudo_excause) { + // SoC-level panic: watchdog, cache error, etc. + // Keep in sync with ESP-IDF's PANIC_RSN_* defines + static const char *const PSEUDO_REASON[] = { + "Unknown reason", // 0 + "Unhandled debug exception", // 1 + "Double exception", // 2 + "Unhandled kernel exception", // 3 + "Coprocessor exception", // 4 + "Interrupt wdt timeout on CPU0", // 5 + "Interrupt wdt timeout on CPU1", // 6 + "Cache error", // 7 + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0])) + return PSEUDO_REASON[cause]; + return PSEUDO_REASON[0]; + } + // Real Xtensa exception + static const char *const REASON[] = { + "IllegalInstruction", + "Syscall", + "InstructionFetchError", + "LoadStoreError", + "Level1Interrupt", + "Alloca", + "IntegerDivideByZero", + "PCValue", + "Privileged", + "LoadStoreAlignment", + nullptr, + nullptr, + "InstrPDAddrError", + "LoadStorePIFDataError", + "InstrPIFAddrError", + "LoadStorePIFAddrError", + "InstTLBMiss", + "InstTLBMultiHit", + "InstFetchPrivilege", + nullptr, + "InstrFetchProhibited", + nullptr, + nullptr, + nullptr, + "LoadStoreTLBMiss", + "LoadStoreTLBMultihit", + "LoadStorePrivilege", + nullptr, + "LoadProhibited", + "StoreProhibited", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal + // interrupt numbers, not standard RISC-V cause codes. The exception type + // field already identifies these, so just return null to use the type name. + if (s_raw_crash_data.pseudo_excause) + return nullptr; + static const char *const REASON[] = { + "Instruction address misaligned", + "Instruction access fault", + "Illegal instruction", + "Breakpoint", + "Load address misaligned", + "Load access fault", + "Store address misaligned", + "Store access fault", + "Environment call from U-mode", + "Environment call from S-mode", + nullptr, + "Environment call from M-mode", + "Instruction page fault", + "Load page fault", + nullptr, + "Store page fault", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#endif + return "Unknown"; +} + +// Exception type names matching panic_exception_t enum +static const char *get_exception_type() { + static const char *const TYPES[] = { + "Debug exception", // PANIC_EXCEPTION_DEBUG + "Interrupt wdt", // PANIC_EXCEPTION_IWDT + "Task wdt", // PANIC_EXCEPTION_TWDT + "Abort", // PANIC_EXCEPTION_ABORT + "Fault", // PANIC_EXCEPTION_FAULT + }; + uint8_t exc = s_raw_crash_data.exception; + if (exc < sizeof(TYPES) / sizeof(TYPES[0])) + return TYPES[exc]; + return "Unknown"; +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console, making it possible to see partial output if the device +// crashes again during boot, and allowing the CLI's process_stacktrace to match +// and decode each address individually. +void crash_handler_log() { + if (!s_crash_data_valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + const char *reason = get_exception_reason(); + if (reason != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason); + } else { + ESP_LOGE(TAG, " Reason: %s", get_exception_type()); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); + uint8_t bt_num = 0; + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif +#if CONFIG_IDF_TARGET_ARCH_RISCV + const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[256]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::esp32 + +// --- Panic handler wrapper --- +// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data +// into NOINIT memory before the normal panic handler runs. +// +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +// Names are mandated by the --wrap linker mechanism +extern void __real_esp_panic_handler(panic_info_t *info); + +void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { + // Save the faulting PC and exception info + s_raw_crash_data.pc = (uint32_t) info->addr; + s_raw_crash_data.backtrace_count = 0; + s_raw_crash_data.reg_frame_count = 0; + s_raw_crash_data.exception = (uint8_t) info->exception; + s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + +#if CONFIG_IDF_TARGET_ARCH_XTENSA + // Xtensa: walk the backtrace using the public API + if (info->frame != nullptr) { + auto *xt_frame = (XtExcFrame *) info->frame; + s_raw_crash_data.cause = xt_frame->exccause; + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) xt_frame->pc, + .sp = (uint32_t) xt_frame->a1, + .next_pc = (uint32_t) xt_frame->a0, + .exc_frame = xt_frame, + }; + + uint8_t count = 0; + // First frame PC + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + s_raw_crash_data.backtrace[count++] = first_pc; + } + // Walk remaining frames + while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) { + break; + } + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + s_raw_crash_data.backtrace[count++] = pc; + } + } + s_raw_crash_data.backtrace_count = count; + } + +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // RISC-V: capture MEPC + RA, then scan stack for code addresses + if (info->frame != nullptr) { + auto *rv_frame = (RvExcFrame *) info->frame; + s_raw_crash_data.cause = rv_frame->mcause; + uint8_t count = 0; + + // Save MEPC (fault PC) and RA (return address) + if (is_code_addr(rv_frame->mepc)) { + s_raw_crash_data.backtrace[count++] = rv_frame->mepc; + } + if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { + s_raw_crash_data.backtrace[count++] = rv_frame->ra; + } + + // Track how many entries came from registers (MEPC/RA) so we can + // skip return-address validation for them at log time. + s_raw_crash_data.reg_frame_count = count; + + // Scan stack for code addresses — captures broadly during panic, + // filtered by is_return_addr() at log time when flash is accessible. + auto *scan_start = (uint32_t *) rv_frame->sp; + for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { + s_raw_crash_data.backtrace[count++] = val; + } + } + s_raw_crash_data.backtrace_count = count; + } +#endif + + // Write version and magic last — ensures all data is written before we mark it valid + s_raw_crash_data.version = CRASH_DATA_VERSION; + s_raw_crash_data.magic = CRASH_MAGIC; + + // Call the real panic handler (prints to UART, does core dump, reboots, etc.) + __real_esp_panic_handler(info); +} + +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +} // extern "C" + +#endif // USE_ESP32_CRASH_HANDLER +#endif // USE_ESP32 diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h new file mode 100644 index 00000000000..97a4d4e1162 --- /dev/null +++ b/esphome/components/esp32/crash_handler.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef USE_ESP32_CRASH_HANDLER + +namespace esphome::esp32 { + +/// Read crash data from NOINIT memory and clear the magic marker. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + +} // namespace esphome::esp32 + +#endif // USE_ESP32_CRASH_HANDLER diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index d6ad77ff4fa..f5bf7822899 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "logger.h" +#include "esphome/components/esp32/crash_handler.h" #include <esp_log.h> #include <driver/uart.h> @@ -117,6 +118,9 @@ void Logger::pre_setup() { esp_log_set_vprintf(esp_idf_log_vprintf_); ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cec77fe2e27..a33f10cb9c0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -195,6 +195,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 5d4065207f0..cb080b2a953 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -340,6 +340,8 @@ STACKTRACE_ESP32_BACKTRACE_RE = re.compile( r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" ) STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") +# ESP32 crash handler (stored backtrace from previous boot) +STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})") STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") @@ -371,6 +373,11 @@ def process_stacktrace(config, line, backtrace_state): ) _decode_pc(config, match.group(1)) + # ESP32 crash handler backtrace (from previous boot) + match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line) + if match is not None: + _decode_pc(config, match.group(1)) + # ESP32 single-line backtrace match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line) if match is not None: diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 16861442777..e1b3908c249 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -673,6 +673,34 @@ def test_process_stacktrace_bad_alloc( assert state is False +def test_process_stacktrace_esp32_crash_handler( + setup_core: Path, mock_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP32 crash handler backtrace lines.""" + config = {"name": "test"} + + # Simulate crash handler log lines as they appear from the API/serial + line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)" + state = platformio_api.process_stacktrace(config, line_pc, False) + # PC line is matched by existing STACKTRACE_ESP32_PC_RE + mock_decode_pc.assert_called_with(config, "400D1234") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt0, False) + mock_decode_pc.assert_called_once_with(config, "400D5678") + assert state is False + + mock_decode_pc.reset_mock() + + line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)" + state = platformio_api.process_stacktrace(config, line_bt1, False) + mock_decode_pc.assert_called_once_with(config, "42005ABC") + assert state is False + + def test_patch_file_downloader_succeeds_first_try() -> None: """Test patch_file_downloader succeeds on first attempt.""" mock_exception_cls = type("PackageException", (Exception,), {}) From 7cec2d3029ae4d2f873633c060b49ade4f77eadd Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 11 Mar 2026 23:48:27 -0500 Subject: [PATCH 0652/2056] [ethernet] ESP32-S3 Ethernet compilation fix (#14717) --- esphome/components/ethernet/ethernet_component.h | 3 +++ esphome/components/ethernet/ethernet_helpers.c | 2 ++ tests/components/ethernet/common-w5500.yaml | 4 ++-- tests/components/ethernet/test.esp32-s3-idf.yaml | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/components/ethernet/test.esp32-s3-idf.yaml diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c464e20b843..f7a0996fb74 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,11 +11,14 @@ #include "esp_eth.h" #include "esp_eth_mac.h" +#include "esp_eth_mac_esp.h" #include "esp_netif.h" #include "esp_mac.h" #include "esp_idf_version.h" +#if CONFIG_ETH_USE_ESP32_EMAC extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); +#endif namespace esphome::ethernet { diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c index 96faccad24c..963db3ff1c4 100644 --- a/esphome/components/ethernet/ethernet_helpers.c +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -3,6 +3,8 @@ // ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers // which are valid in C but not in C++. This wrapper allows C++ code to get // the default config without replicating the macro's contents. +#if CONFIG_ETH_USE_ESP32_EMAC eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); } +#endif diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index 1f8b8650dd0..bf3f6f3f0c4 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -2,10 +2,10 @@ ethernet: type: W5500 clk_pin: 19 mosi_pin: 21 - miso_pin: 23 + miso_pin: 17 cs_pin: 18 interrupt_pin: 36 - reset_pin: 22 + reset_pin: 12 clock_speed: 10Mhz manual_ip: static_ip: 192.168.178.56 diff --git a/tests/components/ethernet/test.esp32-s3-idf.yaml b/tests/components/ethernet/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..36f1b5365f1 --- /dev/null +++ b/tests/components/ethernet/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common-w5500.yaml From e8f51fec889ada351d35311c2b3ce1ed7239d35e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 11 Mar 2026 19:23:01 -1000 Subject: [PATCH 0653/2056] [rp2040] Fix crash handler design flaws (#14716) --- esphome/components/api/api_connection.h | 6 ++++++ esphome/components/logger/logger_rp2040.cpp | 5 +++++ esphome/components/rp2040/__init__.py | 1 + esphome/components/rp2040/core.cpp | 6 +++++- esphome/components/rp2040/crash_handler.cpp | 23 ++++++++++++++++----- esphome/components/rp2040/crash_handler.h | 8 ++++++- esphome/core/defines.h | 1 + 7 files changed, 43 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 60cc3e91b11..68f698d1902 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -17,6 +17,9 @@ #ifdef USE_ESP32_CRASH_HANDLER #include "esphome/components/esp32/crash_handler.h" #endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -240,6 +243,9 @@ class APIConnection final : public APIServerConnectionBase { App.schedule_dump_config(); #ifdef USE_ESP32_CRASH_HANDLER esp32::crash_handler_log(); +#endif +#ifdef USE_RP2040_CRASH_HANDLER + rp2040::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f76b823a8f7..b7225c2a258 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -1,6 +1,9 @@ #ifdef USE_RP2040 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,7 +29,9 @@ void Logger::pre_setup() { } global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif } void HOT Logger::write_msg_(const char *msg, uint16_t len) { diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b15811241ca..276187b273c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -212,6 +212,7 @@ async def to_code(config): ) cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) + cg.add_define("USE_RP2040_CRASH_HANDLER") def add_pio_file(component: str, key: str, data: str): diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 5e5a96c78b1..7079cbca155 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,8 +1,10 @@ #ifdef USE_RP2040 #include "core.h" -#include "crash_handler.h" #include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER +#include "crash_handler.h" +#endif #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -25,7 +27,9 @@ void arch_restart() { } void arch_init() { +#ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_read_and_clear(); +#endif #if USE_RP2040_WATCHDOG_TIMEOUT > 0 watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); #endif diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 6ab46da4449..1f579c2d18e 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -1,5 +1,8 @@ #ifdef USE_RP2040 +#include "esphome/core/defines.h" +#ifdef USE_RP2040_CRASH_HANDLER + #include "crash_handler.h" #include "esphome/core/log.h" @@ -13,13 +16,19 @@ static constexpr uint32_t EF_LR = 5; static constexpr uint32_t EF_PC = 6; -static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +// Version encoded in the magic value: upper 16 bits are sentinel (0xDEAD), +// lower 16 bits are the version number. This avoids using a separate scratch +// register for versioning (we only have 8 total). Future firmware reads the +// sentinel to confirm it's crash data, then the version to know the layout. +static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000; +static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION; // We only have 8 scratch registers (32 bytes) that survive watchdog reboot. // Use them for the most important data, then scan the stack for code addresses. // // Scratch register layout: -// [0] = magic (CRASH_MAGIC) +// [0] = versioned magic (upper 16 bits = 0xDEAD sentinel, lower 16 bits = version) // [1] = PC (program counter at fault) // [2] = LR (link register from exception frame) // [3] = SP (stack pointer at fault) @@ -57,9 +66,12 @@ static struct { uint8_t backtrace_count; } __attribute__((section(".noinit"))) s_crash_data; +bool crash_handler_has_data() { return s_crash_data.valid; } + void crash_handler_read_and_clear() { s_crash_data.valid = false; - if (watchdog_hw->scratch[0] == CRASH_MAGIC) { + uint32_t magic = watchdog_hw->scratch[0]; + if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) { s_crash_data.valid = true; s_crash_data.pc = watchdog_hw->scratch[1]; s_crash_data.lr = watchdog_hw->scratch[2]; @@ -135,7 +147,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame // by a stacking error or corrupted SP, frame may be invalid. Write a minimal // crash marker so we at least know a crash occurred. if (!is_valid_sram_ptr(frame)) { - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = 0; // PC unknown watchdog_hw->scratch[2] = 0; // LR unknown watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis @@ -157,7 +169,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame); // Write key registers - watchdog_hw->scratch[0] = CRASH_MAGIC; + watchdog_hw->scratch[0] = CRASH_MAGIC_V1; watchdog_hw->scratch[1] = frame[EF_PC]; watchdog_hw->scratch[2] = frame[EF_LR]; watchdog_hw->scratch[3] = pre_fault_sp; @@ -224,4 +236,5 @@ extern "C" void __attribute__((naked, used)) isr_hardfault() { : "i"(hard_fault_handler_c)); } +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/components/rp2040/crash_handler.h b/esphome/components/rp2040/crash_handler.h index f10db47c234..78e8ede08c8 100644 --- a/esphome/components/rp2040/crash_handler.h +++ b/esphome/components/rp2040/crash_handler.h @@ -2,7 +2,9 @@ #ifdef USE_RP2040 -#include <cstdint> +#include "esphome/core/defines.h" + +#ifdef USE_RP2040_CRASH_HANDLER namespace esphome::rp2040 { @@ -12,6 +14,10 @@ void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + } // namespace esphome::rp2040 +#endif // USE_RP2040_CRASH_HANDLER #endif // USE_RP2040 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a33f10cb9c0..073170aafbf 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -338,6 +338,7 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) #define USE_LOOP_PRIORITY +#define USE_RP2040_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C #define USE_LOGGER_USB_CDC From 6002badb3c35b20475a76cef3781f13967c1260e Mon Sep 17 00:00:00 2001 From: Adam DeMuri <adam.demuri@gmail.com> Date: Thu, 12 Mar 2026 02:00:26 -0600 Subject: [PATCH 0654/2056] [modbus] Fix buffer overflow in modbus (#14719) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/modbus/modbus.cpp | 12 ++--- tests/components/modbus/modbus_test.cpp | 59 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/components/modbus/modbus_test.cpp diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 82672217c56..7a61868e6e9 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -125,13 +125,17 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Byte 0: modbus address (match all) if (at == 0) return true; - uint8_t address = raw[0]; - uint8_t function_code = raw[1]; + // Byte 1: function code + if (at == 1) + return true; // Byte 2: Size (with modbus rtu function code 4/3) // See also https://en.wikipedia.org/wiki/Modbus if (at == 2) return true; + uint8_t address = raw[0]; + uint8_t function_code = raw[1]; + uint8_t data_len = raw[2]; uint8_t data_offset = 3; @@ -146,10 +150,6 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // chance that this is a complete message ... admittedly there is a small chance is // isn't but that is quite small given the purpose of the CRC in the first place - // Fewer than 2 bytes can't calc CRC - if (at < 2) - return true; - data_len = at - 2; data_offset = 1; diff --git a/tests/components/modbus/modbus_test.cpp b/tests/components/modbus/modbus_test.cpp new file mode 100644 index 00000000000..afe5ced082b --- /dev/null +++ b/tests/components/modbus/modbus_test.cpp @@ -0,0 +1,59 @@ +#include <gtest/gtest.h> +#include "esphome/components/modbus/modbus.h" +#include "esphome/core/helpers.h" + +namespace esphome::modbus { + +// Exposes protected methods for testing. +class TestModbus : public Modbus { + public: + bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); } + void test_clear_rx_buffer() { this->rx_buffer_.clear(); } + void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; } +}; + +class MockDevice : public ModbusDevice { + public: + void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; } + bool data_received{false}; +}; + +TEST(ModbusTest, TwoByteRegressionTest) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + // First byte (at=0) + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01)); + // Second byte (at=1) + // This used to reach raw[2] because it skipped the if(at==2) check, causing a + // buffer overflow. + EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03)); +} + +TEST(ModbusTest, TestValidFrame) { + TestModbus modbus; + modbus.set_role(ModbusRole::CLIENT); + + MockDevice device; + device.set_parent(&modbus); + device.set_address(0x01); + modbus.register_device(&device); + modbus.set_waiting(0x01); + + // Address 1, Function 3, Length 2, Data 0x1234 + uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34}; + uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data)); + + std::vector<uint8_t> frame; + for (uint8_t b : frame_data) + frame.push_back(b); + frame.push_back(crc & 0xFF); + frame.push_back((crc >> 8) & 0xFF); + + for (size_t i = 0; i < frame.size(); i++) { + bool result = modbus.test_parse_modbus_byte(frame[i]); + EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")"; + } + EXPECT_TRUE(device.data_received); +} + +} // namespace esphome::modbus From 4b1c4ba5c0561cba93d979aa8fb76da0bafff301 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 12 Mar 2026 03:16:02 -0500 Subject: [PATCH 0655/2056] [ledc] Fix high-pressure crash & recovery (#14720) --- esphome/components/ledc/ledc_output.cpp | 53 +++++++++++++++++++++++-- esphome/components/ledc/ledc_output.h | 8 ++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 763de851da3..592fc7bd0c1 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -5,6 +5,10 @@ #include <driver/ledc.h> #include <cinttypes> +#include <esp_private/periph_ctrl.h> +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +#include <hal/ledc_ll.h> +#endif #define CLOCK_FREQUENCY 80e6f @@ -16,10 +20,10 @@ static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; -namespace esphome { -namespace ledc { +namespace esphome::ledc { static const char *const TAG = "ledc.output"; +static bool ledc_peripheral_reset_done = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; #if SOC_LEDC_SUPPORT_HS_MODE @@ -32,6 +36,28 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) +// Classic ESP32 (currently the only target without SOC_LEDC_SUPPORT_FADE_STOP) can block in +// ledc_ll_set_duty_start() while duty_start is set. We check the same conf1.duty_start bit here +// to defer updates and avoid entering IDF's unbounded wait loop. +// +// This intentionally depends on the classic ESP32 LEDC register layout used by IDF's own LL HAL. +// If another target without SOC_LEDC_SUPPORT_FADE_STOP is introduced, revisit this helper. +static_assert( +#if defined(CONFIG_IDF_TARGET_ESP32) + true, +#else + false, +#endif + "LEDC duty_start pending check assumes classic ESP32 register layout; " + "re-evaluate for this target"); + +static bool ledc_duty_update_pending(ledc_mode_t speed_mode, ledc_channel_t chan_num) { + auto *hw = LEDC_LL_GET_HW(); + return hw->channel_group[speed_mode].channel[chan_num].conf1.duty_start != 0; +} +#endif + float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth); } @@ -105,21 +131,40 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast<uint32_t>(duty_rounded); + if (duty == this->last_duty_) { + return; + } + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); auto speed_mode = get_speed_mode(this->channel_); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); if (duty == max_duty) { ledc_stop(speed_mode, chan_num, 1); + this->last_duty_ = duty; } else if (duty == 0) { ledc_stop(speed_mode, chan_num, 0); + this->last_duty_ = duty; } else { +#if !defined(SOC_LEDC_SUPPORT_FADE_STOP) + if (ledc_duty_update_pending(speed_mode, chan_num)) { + ESP_LOGV(TAG, "Skipping LEDC duty update on channel %u while previous duty_start is still set", this->channel_); + return; + } +#endif ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); + this->last_duty_ = duty; } } void LEDCOutput::setup() { + if (!ledc_peripheral_reset_done) { + ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot"); + periph_module_reset(PERIPH_LEDC_MODULE); + ledc_peripheral_reset_done = true; + } + auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2); auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); @@ -207,12 +252,12 @@ void LEDCOutput::update_frequency(float frequency) { this->status_clear_error(); // re-apply duty + this->last_duty_ = UINT32_MAX; this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index b24e3cfdb23..bf5cdb93055 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -4,11 +4,11 @@ #include "esphome/core/hal.h" #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" +#include <cstdint> #ifdef USE_ESP32 -namespace esphome { -namespace ledc { +namespace esphome::ledc { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern uint8_t next_ledc_channel; @@ -39,6 +39,7 @@ class LEDCOutput : public output::FloatOutput, public Component { float phase_angle_{0.0f}; float frequency_{}; float duty_{0.0f}; + uint32_t last_duty_{UINT32_MAX}; bool initialized_ = false; }; @@ -56,7 +57,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { LEDCOutput *parent_; }; -} // namespace ledc -} // namespace esphome +} // namespace esphome::ledc #endif From df2ddc47ec2baae7da48a6f14831c429c8f292af Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 02:07:26 -0700 Subject: [PATCH 0656/2056] [web_server] use DETAIL_ALL in update_all_json_generator (#14711) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5590e67b822..40830196433 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2181,7 +2181,7 @@ json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *we } json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL); } json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson From 440734dadf0e4c60accb4a5d6b79eaf6a9c9c60d Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 12 Mar 2026 07:56:01 -0500 Subject: [PATCH 0657/2056] [audio] Bump microOpus to v0.3.5 (#14727) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index d95fcf66d7b..b28c2ed3d8c 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.4") + add_idf_component(name="esphome/micro-opus", ref="0.3.5") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f7fd3e67bc3..df651ae15dd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.4 + version: 0.3.5 espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: From da130c900f82326080c1b790be68c9625912b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6nig?= <Matthias.Konitzny@kabelmail.de> Date: Thu, 12 Mar 2026 17:00:08 +0100 Subject: [PATCH 0658/2056] [mqtt] Fixed permission denied error for client certificates on Windows (#13525) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/mqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index cbf78bd3f6e..ccacbaea54f 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -2,6 +2,7 @@ import contextlib from datetime import datetime import json import logging +import os import ssl import tempfile import time @@ -109,14 +110,18 @@ def prepare( CONF_CLIENT_CERTIFICATE_KEY ): with ( - tempfile.NamedTemporaryFile(mode="w+") as cert_file, - tempfile.NamedTemporaryFile(mode="w+") as key_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file, + tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file, ): - cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) - cert_file.flush() - key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) - key_file.flush() - context.load_cert_chain(cert_file.name, key_file.name) + try: + cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE)) + key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) + cert_file.close() + key_file.close() + context.load_cert_chain(cert_file.name, key_file.name) + finally: + os.unlink(cert_file.name) + os.unlink(key_file.name) client.tls_set_context(context) try: From 3a838d897fac21b120b8d913141dee194cf87c6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:14:49 -1000 Subject: [PATCH 0659/2056] [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) --- .../components/socket/lwip_raw_tcp_impl.cpp | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index fd1b8a95542..1e03a4935c2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -138,13 +138,46 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif +// Clear arg, recv, and err callbacks, then abort a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// Must be called before destroying the object that tcp_arg points to — +// tcp_abort() triggers the err callback synchronously, which would +// otherwise call back into a partially-destroyed object. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +static void pcb_detach_abort(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + tcp_abort(pcb); +} + +// Clear arg, recv, and err callbacks, then gracefully close a connected PCB. +// Only valid for full tcp_pcb (not tcp_pcb_listen). +// After tcp_close(), the PCB remains alive during the TCP close handshake +// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP +// would call recv/err on a destroyed socket object, corrupting the heap. +// tcp_sent/tcp_poll are not cleared because this implementation +// never registers them. +// Returns ERR_OK on success; on failure the PCB is aborted instead. +static err_t pcb_detach_close(struct tcp_pcb *pcb) { + tcp_arg(pcb, nullptr); + tcp_recv(pcb, nullptr); + tcp_err(pcb, nullptr); + err_t err = tcp_close(pcb); + if (err != ERR_OK) { + tcp_abort(pcb); + } + return err; +} + // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { LWIP_LOCK(); if (this->pcb_ != nullptr) { LWIP_LOG("tcp_abort(%p)", this->pcb_); - tcp_abort(this->pcb_); + pcb_detach_abort(this->pcb_); this->pcb_ = nullptr; } } @@ -222,15 +255,13 @@ int LWIPRawCommon::close() { return -1; } LWIP_LOG("tcp_close(%p)", this->pcb_); - err_t err = tcp_close(this->pcb_); + err_t err = pcb_detach_close(this->pcb_); + this->pcb_ = nullptr; if (err != ERR_OK) { LWIP_LOG(" -> err %d", err); - tcp_abort(this->pcb_); - this->pcb_ = nullptr; errno = err == ERR_MEM ? ENOMEM : EIO; return -1; } - this->pcb_ = nullptr; return 0; } @@ -673,13 +704,10 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) { LWIPRawListenImpl::~LWIPRawListenImpl() { LWIP_LOCK(); // Abort any queued PCBs that were never accepted by the main loop. - // Clear the error callback first — tcp_abort triggers it, and we don't - // want s_queued_err_fn writing to slots during destruction. for (uint8_t i = 0; i < this->accepted_socket_count_; i++) { auto &entry = this->accepted_pcbs_[i]; if (entry.pcb != nullptr) { - tcp_err(entry.pcb, nullptr); - tcp_abort(entry.pcb); + pcb_detach_abort(entry.pcb); entry.pcb = nullptr; } if (entry.rx_buf != nullptr) { @@ -691,6 +719,10 @@ LWIPRawListenImpl::~LWIPRawListenImpl() { // Listen PCBs must use tcp_close(), not tcp_abort(). // tcp_abandon() asserts pcb->state != LISTEN and would access // fields that don't exist in the smaller tcp_pcb_listen struct. + // Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access + // fields that only exist in the full tcp_pcb, not tcp_pcb_listen. + // tcp_close() on a listen PCB is synchronous (frees immediately), + // so there are no async callbacks to worry about. // Close here and null pcb_ so the base destructor skips tcp_abort. if (this->pcb_ != nullptr) { tcp_close(this->pcb_); From 1d881ef6f4d4a4ba8575fe5ac1c4bb50c121f767 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:04 -1000 Subject: [PATCH 0660/2056] [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) --- esphome/components/socket/bsd_sockets_impl.h | 11 ++++++++++- esphome/components/socket/lwip_sockets_impl.h | 11 ++++++++++- esphome/core/lwip_fast_select.c | 16 ++++++++++++++++ esphome/core/lwip_fast_select.h | 7 +++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index 9ebbe72002b..339a699bc97 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -14,7 +14,7 @@ #endif #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -56,6 +56,15 @@ class BSDSocketImpl { return ::getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return ::setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return ::listen(this->fd_, backlog); } diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index c5792198635..bfc4da9926a 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -10,7 +10,7 @@ #include "headers.h" #ifdef USE_LWIP_FAST_SELECT -struct lwip_sock; +#include "esphome/core/lwip_fast_select.h" #endif namespace esphome::socket { @@ -52,6 +52,15 @@ class LwIPSocketImpl { return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) { +#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING) + // Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock, + // bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting). + if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) { + LwIPLock lock; + if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0)) + return 0; + } +#endif return lwip_setsockopt(this->fd_, level, optname, optval, optlen); } int listen(int backlog) { return lwip_listen(this->fd_, backlog); } diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index c578a9aae91..a695fa396bc 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -112,6 +112,7 @@ // LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc. #include <lwip/api.h> #include <lwip/priv/sockets_priv.h> +#include <lwip/tcp.h> // FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not #ifdef USE_ESP32 #include <freertos/FreeRTOS.h> @@ -216,6 +217,21 @@ void esphome_lwip_hook_socket(struct lwip_sock *sock) { sock->conn->callback = esphome_socket_event_callback; } +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { + if (sock == NULL || sock->conn == NULL) + return false; + if (NETCONNTYPE_GROUP(sock->conn->type) != NETCONN_TCP) + return false; + if (sock->conn->pcb.tcp == NULL) + return false; + if (enable) { + tcp_nagle_disable(sock->conn->pcb.tcp); + } else { + tcp_nagle_enable(sock->conn->pcb.tcp); + } + return true; +} + // Wake the main loop from another FreeRTOS task. NOT ISR-safe. void esphome_lwip_wake_main_loop(void) { TaskHandle_t task = s_main_loop_task; diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 46c6b711cd2..50706ba9f69 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -66,6 +66,13 @@ void esphome_lwip_wake_main_loop(void); /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); +/// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. +/// Must be called with the TCPIP core lock held (LwIPLock in C++). +/// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, +/// hooks, refcounting) — just a direct pcb->flags bit set/clear. +/// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. +bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); + /// Wake the main loop task from any context (ISR, thread, or main loop). /// ESP32-only: uses xPortInIsrContext() to detect ISR context. /// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. From 1b7d0f9c0b6745f3610dc84b38dd7236fc5dced7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:21 -1000 Subject: [PATCH 0661/2056] [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../esp32_ble_client/ble_client_base.cpp | 43 ++++++++++++++++--- .../esp32_ble_client/ble_client_base.h | 17 +++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2f17334c77c..9d6e079d926 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -27,6 +27,7 @@ static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -62,6 +63,15 @@ void BLEClientBase::loop() { // will enable it again when a connection is needed. else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); + } else if (this->state() == espbt::ClientState::DISCONNECTING && + (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) { + ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_, + this->address_str_); + // release_services() must be called before set_idle_() — if we entered DISCONNECTING + // via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF + // never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call. + this->release_services(); + this->set_idle_(); } } @@ -101,12 +111,16 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - // Prevent duplicate connection attempts + // Prevent duplicate connection attempts or connecting while still disconnecting if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || this->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state())); return; + } else if (this->state() == espbt::ClientState::DISCONNECTING) { + ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect", + this->connection_index_, this->address_str_); + return; } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; @@ -174,7 +188,7 @@ void BLEClientBase::unconditional_disconnect() { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { - this->set_state(espbt::ClientState::DISCONNECTING); + this->set_disconnecting_(); } } @@ -220,6 +234,7 @@ void BLEClientBase::log_connection_params_(const char *param_type) { void BLEClientBase::handle_connection_result_(esp_err_t ret) { if (ret) { this->log_gattc_warning_("esp_ble_gattc_open", ret); + // Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID. this->set_state(espbt::ClientState::IDLE); } } @@ -311,15 +326,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); - this->set_state(espbt::ClientState::IDLE); + // Connection was never established so CLOSE_EVT may not follow + this->set_idle_(); break; } if (this->want_disconnect_) { // Disconnect was requested after connecting started, // but before the connection was established. Now that we have // this->conn_id_ set, we can disconnect it. + // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_(). this->unconditional_disconnect(); - this->conn_id_ = UNSET_CONN_ID; break; } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT @@ -363,8 +379,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); } + // For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before + // DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go + // backwards to DISCONNECTING — the connection is already fully cleaned up. + if (this->state() == espbt::ClientState::IDLE) { + this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE"); + break; + } + // For passive disconnects (remote device disconnected or link lost), + // DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for + // CLOSE_EVT to ensure the controller has fully freed resources (L2CAP + // channels, ATT resources, HCI connection handle). Transitioning to IDLE + // here would allow reconnection before cleanup is complete, causing the + // controller to reject the new connection (status=133) or crash with + // ASSERT_PARAM in lld_evt.c. this->release_services(); - this->set_state(espbt::ClientState::IDLE); + this->set_disconnecting_(); break; } @@ -387,8 +417,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; + this->set_idle_(); break; } case ESP_GATTC_SEARCH_RES_EVT: { diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index af4f1b30290..4e0b22cc299 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -113,11 +113,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; esp_bd_addr_t remote_bda_; // 6 bytes - // Group 5: 2-byte types + // Group 5: 4-byte types + uint32_t disconnecting_started_{0}; + + // Group 6: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; uint16_t mtu_{23}; - // Group 6: 1-byte types and small enums + // Group 7: 1-byte types and small enums esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; uint8_t connection_index_; @@ -137,6 +140,16 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); + /// Transition to IDLE and reset conn_id — call when the connection is fully dead. + void set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; + } + /// Transition to DISCONNECTING and start the safety timeout. + void set_disconnecting_() { + this->disconnecting_started_ = millis(); + this->set_state(espbt::ClientState::DISCONNECTING); + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From 33475703da772d24e7a6df3e8a15160c5db53f8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:34 -1000 Subject: [PATCH 0662/2056] [time] Fix settimeofday() failure on ESP8266 (#14707) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/time/real_time_clock.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 566344fa880..4e623942ac5 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -88,16 +88,16 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0, }; +#ifdef USE_ESP8266 + // ESP8266 settimeofday() requires tz to be nullptr + int ret = settimeofday(&timev, nullptr); +#else struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); - if (ret != 0 && errno == EINVAL) { - // Some ESP8266 frameworks abort when timezone parameter is not NULL - // while ESP32 expects it not to be NULL - ret = settimeofday(&timev, nullptr); - } +#endif if (ret != 0) { - ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + ESP_LOGW(TAG, "settimeofday() failed with code %d", ret); } #endif auto time = this->now(); From c8cf9b74b104a1965897c16706ee30999d6fe25b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:15:48 -1000 Subject: [PATCH 0663/2056] [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) --- .../components/esphome/ota/ota_esphome.cpp | 46 +++++++++++- esphome/components/socket/headers.h | 2 + .../components/socket/lwip_raw_tcp_impl.cpp | 71 +++++++++++++++++-- esphome/components/socket/lwip_raw_tcp_impl.h | 16 +++-- 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index a1cdf59d2b7..d8dbe2dee2d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -18,6 +18,7 @@ #include <cerrno> #include <cstdio> +#include <sys/time.h> namespace esphome { @@ -238,6 +239,31 @@ void ESPHomeOTAComponent::handle_data_() { /// and reboots on success. /// /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. + /// + /// Socket I/O strategy: + /// + /// Before this function, the handshake states use non-blocking I/O: + /// read()/write() return immediately with EWOULDBLOCK if no data + /// loop() retries on next iteration (~16ms), no delay needed + /// + /// This function switches to blocking mode with SO_RCVTIMEO/SO_SNDTIMEO: + /// + /// Path | Wait mechanism | WDT strategy + /// --------------|------------------------|--------------------------- + /// Main read | SO_RCVTIMEO (2s block) | feed_wdt() only, no delay + /// readall_() | SO_RCVTIMEO (2s block) | feed_wdt() + delay(0) + /// writeall_() | SO_SNDTIMEO (2s block) | feed_wdt() + delay(1) + /// + /// readall_() uses delay(0) because SO_RCVTIMEO already waited — just yield. + /// writeall_() uses delay(1) because on raw TCP (ESP8266, RP2040) writes + /// never block (tcp_write returns immediately), so delay(1) prevents spinning. + /// + /// Platform details: + /// BSD sockets (ESP32): setblocking(true) makes read/write block + /// lwip sockets (LT): setblocking(true) makes read/write block + /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses + /// socket_delay()/socket_wake() in read(); + /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -249,6 +275,14 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif + // Set socket timeouts and blocking mode (see strategy table above) + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + this->client_->setblocking(true); + // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); @@ -299,7 +333,8 @@ void ESPHomeOTAComponent::handle_data_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (this->would_block_(errno)) { - this->yield_and_feed_watchdog_(); + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); continue; } ESP_LOGW(TAG, "Read err %d", errno); @@ -401,7 +436,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { } else { at += read; } - this->yield_and_feed_watchdog_(); + // read() already waited via SO_RCVTIMEO, just yield without 1ms stall + App.feed_wdt(); + delay(0); } return true; @@ -422,10 +459,13 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); return false; } + // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning + this->yield_and_feed_watchdog_(); } else { at += written; + // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT + App.feed_wdt(); } - this->yield_and_feed_watchdog_(); } return true; } diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 16e4d23d3ba..0eece6480f6 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -51,6 +51,8 @@ #define SO_REUSEADDR 0x0004 /* Allow local address reuse */ #define SO_KEEPALIVE 0x0008 /* keep connections alive */ #define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */ +#define SO_RCVTIMEO 0x1006 /* receive timeout */ +#define SO_SNDTIMEO 0x1005 /* send timeout */ #define SOL_SOCKET 0xfff /* options for socket level */ diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 1e03a4935c2..96328e68c73 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -5,6 +5,7 @@ #include <cerrno> #include <cstring> +#include <sys/time.h> #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,7 +82,9 @@ void socket_delay(uint32_t ms) { s_socket_woke = false; return; } - s_socket_woke = false; + // Don't clear s_socket_woke here — if an IRQ fires between the check above + // and the while loop below, the while condition sees it immediately. Clearing + // here would lose that wake and sleep until the timer fires. s_delay_expired = false; // Set a one-shot timer to wake us after the timeout. // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. @@ -99,6 +102,7 @@ void socket_delay(uint32_t ms) { // Cancel timer if we woke early (socket data arrived before timeout) if (!s_delay_expired) cancel_alarm(alarm); + s_socket_woke = false; // consume the wake for next call } // No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP @@ -359,6 +363,18 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o *optlen = 4; return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (*optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + uint32_t ms = this->recv_timeout_cs_ * 10; + auto *tv = reinterpret_cast<struct timeval *>(optval); + tv->tv_sec = ms / 1000; + tv->tv_usec = (ms % 1000) * 1000; + *optlen = sizeof(struct timeval); + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (*optlen < 4) { errno = EINVAL; @@ -388,6 +404,21 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle // to prevent warnings return 0; } + if (level == SOL_SOCKET && optname == SO_RCVTIMEO) { + if (optlen < sizeof(struct timeval)) { + errno = EINVAL; + return -1; + } + const auto *tv = reinterpret_cast<const struct timeval *>(optval); + uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000; + uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond + this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs); + return 0; + } + if (level == SOL_SOCKET && optname == SO_SNDTIMEO) { + // Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op. + return 0; + } if (level == IPPROTO_TCP && optname == TCP_NODELAY) { if (optlen != 4) { errno = EINVAL; @@ -518,8 +549,25 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { return ERR_OK; } -ssize_t LWIPRawImpl::read(void *buf, size_t len) { - LWIP_LOCK(); +void LWIPRawImpl::wait_for_data_() { + // Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040 + // (needs async_context lock). + // + // Loop until data arrives, connection closes, or the full timeout elapses. + // socket_delay() may return early due to other sockets waking the global + // socket_wake() flag, so we re-enter for the remaining time. + uint32_t timeout_ms = this->recv_timeout_cs_ * 10; + uint32_t start = millis(); + while (this->waiting_for_data_()) { + uint32_t elapsed = millis() - start; + if (elapsed >= timeout_ms) + break; + socket_delay(timeout_ms - elapsed); + } +} + +ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) { + // Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf. if (this->pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -578,11 +626,26 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) { return read; } +ssize_t LWIPRawImpl::read(void *buf, size_t len) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + + LWIP_LOCK(); + return this->read_locked_(buf, len); +} + ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) { + // See waiting_for_data_() for safety of unlocked reads. + if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) { + this->wait_for_data_(); + } + LWIP_LOCK(); // Hold for entire scatter-gather operation ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { - ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); + ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); if (err == -1) { if (ret != 0) { // if we already read some don't return an error diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 95931afcf3f..3c27d71062f 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -57,6 +57,7 @@ class LWIPRawCommon { // instead use it for determining whether to call lwip_output bool nodelay_ = false; sa_family_t family_ = 0; + uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s) }; /// Connected socket implementation for LWIP raw TCP. @@ -107,11 +108,8 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ECONNRESET; return -1; } - if (blocking) { - // blocking operation not supported - errno = EINVAL; - return -1; - } + // Raw TCP doesn't use a blocking flag directly. Blocking behavior + // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). return 0; } int loop() { return 0; } @@ -122,6 +120,14 @@ class LWIPRawImpl : public LWIPRawCommon { static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err); protected: + // True when the socket could receive data but none has arrived yet. + // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool, + // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does + // an unnecessary wait (stale true) or skips it (stale false), and the + // authoritative recheck happens under LWIP_LOCK afterward. + bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; } + void wait_for_data_(); + ssize_t read_locked_(void *buf, size_t len); ssize_t internal_write_(const void *buf, size_t len); int internal_output_(); From 2ba807efe85c5e1216f793b2b1354576d38a49bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:08 -1000 Subject: [PATCH 0664/2056] [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) --- esphome/components/adc/adc_sensor_rp2040.cpp | 7 +++++++ tests/components/adc/test.rp2040-pico2-ard.yaml | 11 +++++++++++ tests/components/spi/test.rp2040-pico2-ard.yaml | 6 ++++++ .../build_components_base.rp2040-pico2-ard.yaml | 15 +++++++++++++++ .../common/spi/rp2040-pico2-ard.yaml | 12 ++++++++++++ 5 files changed, 51 insertions(+) create mode 100644 tests/components/adc/test.rp2040-pico2-ard.yaml create mode 100644 tests/components/spi/test.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml create mode 100644 tests/test_build_components/common/spi/rp2040-pico2-ard.yaml diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 8496e0f41e4..a79707e2347 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -8,6 +8,13 @@ #endif // CYW43_USES_VSYS_PIN #include <hardware/adc.h> +// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h), +// but the Arduino framework's config_autogen.h includes a generic board header +// that doesn't define it. Provide the standard value (pin 29) as a fallback. +#ifndef PICO_VSYS_PIN +#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage) +#endif + namespace esphome { namespace adc { diff --git a/tests/components/adc/test.rp2040-pico2-ard.yaml b/tests/components/adc/test.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..4cc865bb5d3 --- /dev/null +++ b/tests/components/adc/test.rp2040-pico2-ard.yaml @@ -0,0 +1,11 @@ +sensor: + - id: my_sensor + platform: adc + pin: VCC + name: ADC Test sensor + update_interval: "1:01" + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/spi/test.rp2040-pico2-ard.yaml b/tests/components/spi/test.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..81a8acafd88 --- /dev/null +++ b/tests/components/spi/test.rp2040-pico2-ard.yaml @@ -0,0 +1,6 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..0922a5238e8 --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040-pico2-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestrp2040pico2ard + friendly_name: $component_name + +rp2040: + board: rpipico2 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml new file mode 100644 index 00000000000..205beb6e1bb --- /dev/null +++ b/tests/test_build_components/common/spi/rp2040-pico2-ard.yaml @@ -0,0 +1,12 @@ +# Common SPI configuration for RP2040 Pico 2 (RP2350) Arduino tests + +substitutions: + clk_pin: GPIO18 + mosi_pin: GPIO19 + miso_pin: GPIO16 + +spi: + - id: spi_bus + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} From cb4d1d1b5e2f6a354b4b873e444386a4fd4b2924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:23 -1000 Subject: [PATCH 0665/2056] [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) --- esphome/components/api/api_frame_helper_noise.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3e6ecf9dc30..f945253c89d 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -258,10 +258,13 @@ APIError APINoiseFrameHelper::state_action_() { // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data size_t old_size = this->prologue_.size(); - this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); - this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); - this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); + size_t rx_size = this->rx_buf_.size(); + this->prologue_.resize(old_size + 2 + rx_size); + this->prologue_[old_size] = (uint8_t) (rx_size >> 8); + this->prologue_[old_size + 1] = (uint8_t) rx_size; + if (rx_size > 0) { + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + } state_ = State::SERVER_HELLO; } From 23c7e0f8030d5caa5b07a9447293114639e89472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:38 -1000 Subject: [PATCH 0666/2056] [uart] Allow hardware UART with single pin on RP2040 (#14725) --- .../components/uart/uart_component_rp2040.cpp | 37 +++++++++++++++---- tests/components/uart/test.rp2040-ard.yaml | 3 ++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 858f1a02ddf..6f6f1fb96b7 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -105,15 +105,34 @@ void RP2040UartComponent::setup() { } } + // Determine which hardware UART to use. A pin that is not specified + // should not prevent hardware UART selection — one-way UART is valid. + // When both pins are configured, both must be HW-capable and agree on UART number. + // When only one pin is configured (nullptr other), use that pin's HW UART. + // If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO. + int8_t hw_uart = -1; + const bool tx_configured = (this->tx_pin_ != nullptr); + const bool rx_configured = (this->rx_pin_ != nullptr); + + if (tx_configured && rx_configured) { + // Both pins configured — both must map to the same hardware UART + if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) { + hw_uart = tx_hw; + } + } else if (tx_configured) { + hw_uart = tx_hw; + } else if (rx_configured) { + hw_uart = rx_hw; + } + #ifdef USE_LOGGER - if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) { - ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw); - tx_hw = -1; - rx_hw = -1; + if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) { + ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart); + hw_uart = -1; } #endif - if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) { + if (hw_uart == -1) { ESP_LOGV(TAG, "Using SerialPIO"); pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin(); pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin(); @@ -127,13 +146,15 @@ void RP2040UartComponent::setup() { } else { ESP_LOGV(TAG, "Using Hardware Serial"); SerialUART *serial; - if (tx_hw == 0) { + if (hw_uart == 0) { serial = &Serial1; } else { serial = &Serial2; } - serial->setTX(this->tx_pin_->get_pin()); - serial->setRX(this->rx_pin_->get_pin()); + if (this->tx_pin_ != nullptr) + serial->setTX(this->tx_pin_->get_pin()); + if (this->rx_pin_ != nullptr) + serial->setRX(this->rx_pin_->get_pin()); serial->setFIFOSize(this->rx_buffer_size_); serial->begin(this->baud_rate_, config); this->serial_ = serial; diff --git a/tests/components/uart/test.rp2040-ard.yaml b/tests/components/uart/test.rp2040-ard.yaml index 5eb2b533ea0..1d5f91c6a7d 100644 --- a/tests/components/uart/test.rp2040-ard.yaml +++ b/tests/components/uart/test.rp2040-ard.yaml @@ -23,3 +23,6 @@ uart: baud_rate: 115200 debug: debug_prefix: "[UART1] " + - id: uart_rx_only + rx_pin: 17 + baud_rate: 1200 From 14c3e2d9d948837a35aff984646bf55a9a225b33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 07:16:53 -1000 Subject: [PATCH 0667/2056] [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 740bf2e47fd..5a53f0281fa 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -13,7 +13,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b4044c362c6..dff6c7690a0 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -642,7 +642,7 @@ class StringType(TypeInfo): # For SOURCE_BOTH, check if StringRef is set (sending) or use string (received) return ( f"if (!this->{self.field_name}_ref_.empty()) {{" - f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");' + f' out.append("\'").append(this->{self.field_name}_ref_.c_str(), this->{self.field_name}_ref_.size()).append("\'");' f"}} else {{" f' out.append("\'").append(this->{self.field_name}).append("\'");' f"}}" @@ -2705,7 +2705,7 @@ namespace esphome::api { static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } From 390bb0451ffd06cb4e2d88674c9af76bb88c25f0 Mon Sep 17 00:00:00 2001 From: Brian Kaufman <bkaufx@gmail.com> Date: Thu, 12 Mar 2026 16:23:29 -0700 Subject: [PATCH 0668/2056] [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) --- esphome/components/ota/ota_backend_esp8266.cpp | 7 +++++-- esphome/components/ota/ota_backend_esp8266.h | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 1f9a77e4261..93e6249fb3d 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -105,6 +105,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { this->current_address_ = this->start_address_; this->image_size_ = image_size; + this->bytes_received_ = 0; this->buffer_len_ = 0; this->md5_set_ = false; @@ -140,6 +141,7 @@ OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); this->buffer_len_ += to_buffer; + this->bytes_received_ += to_buffer; written += to_buffer; // If buffer is full, write to flash @@ -252,8 +254,8 @@ OTAResponseTypes ESP8266OTABackend::end() { } } - // Calculate actual bytes written - size_t actual_size = this->current_address_ - this->start_address_; + // Calculate actual bytes written (exact uploaded size, excluding flash write padding) + size_t actual_size = this->bytes_received_; // Check if any data was written if (actual_size == 0) { @@ -304,6 +306,7 @@ void ESP8266OTABackend::abort() { this->buffer_.reset(); this->buffer_len_ = 0; this->image_size_ = 0; + this->bytes_received_ = 0; esp8266::preferences_prevent_write(false); } diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index 6213289accb..b364e216a36 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -48,6 +48,7 @@ class ESP8266OTABackend final { uint32_t start_address_{0}; uint32_t current_address_{0}; size_t image_size_{0}; + size_t bytes_received_{0}; md5::MD5Digest md5_{}; uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest From 93be53978925e7eb76035a7ca0356282bea7ae21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 13:28:25 -1000 Subject: [PATCH 0669/2056] [light] Fix ambiguous set_effect overload for const char* (#14732) --- .../addressable_light/addressable_light_display.h | 2 +- esphome/components/light/light_call.cpp | 2 +- esphome/components/light/light_call.h | 2 ++ tests/components/light/common.yaml | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 53f8604b7de..d9b8680547f 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { // - Save the current effect index. this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. - light_state_->make_call().set_effect(0).perform(); + light_state_->make_call().set_effect(uint32_t{0}).perform(); } } enabled_ = enabled; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 14cd0e92f69..cd45994f625 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -506,7 +506,7 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { LightCall &LightCall::set_effect(const char *effect, size_t len) { if (len == 4 && strncasecmp(effect, "none", 4) == 0) { - this->set_effect(0); + this->set_effect(uint32_t{0}); return *this; } diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 0926ab6108e..0eb1785239c 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -130,6 +130,8 @@ class LightCall { LightCall &set_effect(optional<std::string> effect); /// Set the effect of the light by its name. LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); } + /// Set the effect of the light by its name (const char * overload to resolve ambiguity). + LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); } /// Set the effect of the light by its name and length (zero-copy from API). LightCall &set_effect(const char *effect, size_t len); /// Set the effect of the light by its internal index number (only for internal use). diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index e5fab62a793..e1216e7b60b 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -60,6 +60,12 @@ esphome: } } + # Test set_effect with const char* doesn't cause ambiguous overload (issue #14728) + - lambda: |- + auto call = id(test_monochromatic_light).turn_on(); + call.set_effect("None"); + call.perform(); + - light.toggle: test_binary_light - light.turn_off: test_rgb_light - light.turn_on: From 0b99e8f08d0cca47ccc49e93f0e063b7315cd80b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:47:16 -1000 Subject: [PATCH 0670/2056] [rp2040] Use full flash for sketch in testing mode (#14747) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/rp2040/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 276187b273c..71e5f1488cb 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -203,7 +203,12 @@ async def to_code(config): cg.add_build_flag(f"-Wl,--wrap={symbol}") cg.add_platformio_option("board_build.core", "earlephilhower") - cg.add_platformio_option("board_build.filesystem_size", "1m") + # In testing mode, use all flash for sketch to allow linking grouped component tests. + # Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine + # many components that exceed the 1MB sketch partition. + cg.add_platformio_option( + "board_build.filesystem_size", "0m" if CORE.testing_mode else "1m" + ) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] cg.add_define( From 910784ca841d1a117285df0eb8ba408fe6798b44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:06 -1000 Subject: [PATCH 0671/2056] [debug] Fix missing reset reason for RP2040/RP2350 (#14740) --- esphome/components/debug/debug_rp2040.cpp | 64 +++++++++++++++++++++-- esphome/core/helpers.h | 22 ++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index c9d41942dbc..8dc84a26732 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -1,23 +1,81 @@ #include "debug_component.h" #ifdef USE_RP2040 +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include <Arduino.h> +#include <hardware/watchdog.h> +#if defined(PICO_RP2350) +#include <hardware/structs/powman.h> +#else +#include <hardware/structs/vreg_and_chip_reset.h> +#endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif namespace esphome { namespace debug { static const char *const TAG = "debug"; -const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + size_t pos = 0; + +#if defined(PICO_RP2350) + uint32_t chip_reset = powman_hw->chip_reset; + if (chip_reset & 0x04000000) // HAD_GLITCH_DETECT + pos = buf_append_str(buf, size, pos, "Power supply glitch|"); + if (chip_reset & 0x00040000) // HAD_RUN_LOW + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00020000) // HAD_BOR + pos = buf_append_str(buf, size, pos, "Brown-out|"); + if (chip_reset & 0x00010000) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#else + uint32_t chip_reset = vreg_and_chip_reset_hw->chip_reset; + if (chip_reset & 0x00010000) // HAD_RUN + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00000100) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#endif + + if (watchdog_caused_reboot()) { + bool handled = false; +#ifdef USE_RP2040_CRASH_HANDLER + if (rp2040::crash_handler_has_data()) { + pos = buf_append_str(buf, size, pos, "Crash (HardFault)|"); + handled = true; + } +#endif + if (!handled) { + if (watchdog_enable_caused_reboot()) { + pos = buf_append_str(buf, size, pos, "Watchdog timeout|"); + } else { + pos = buf_append_str(buf, size, pos, "Software reset|"); + } + } + } + + // Remove trailing '|' + if (pos > 0 && buf[pos - 1] == '|') { + buf[pos - 1] = '\0'; + } else if (pos == 0) { + return "Unknown"; + } + + return buf; +} const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); } +uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) { constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - uint32_t cpu_freq = rp2040.f_cpu(); + uint32_t cpu_freq = ::rp2040.f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 70ac1574f0c..b2517e2d7ac 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -942,6 +942,28 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, } #endif +/// Safely append a string to buffer without format parsing, returning new position (capped at size). +/// More efficient than buf_append_printf for plain string literals. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param str String to append (must not be null) +/// @return New position after appending (capped at size on overflow) +inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str) { + if (pos >= size) { + return size; + } + size_t remaining = size - pos - 1; // reserve space for null terminator + size_t len = strlen(str); + if (len > remaining) { + len = remaining; + } + memcpy(buf + pos, str, len); + pos += len; + buf[pos] = '\0'; + return pos; +} + /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. /// Maximum name length supported is 120 characters for friendly names. From c263c2c382292eb33bec5ed560620964b6a25c34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:29 -1000 Subject: [PATCH 0672/2056] [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/captive_portal/captive_portal.cpp | 4 ++-- esphome/components/web_server_base/web_server_base.cpp | 4 ++++ esphome/components/web_server_base/web_server_base.h | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 5af6ab29a2e..183f16c5f84 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -61,7 +61,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { // Defer save to main loop thread to avoid NVS operations from HTTP thread this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif - request->redirect(ESPHOME_F("/?save")); + request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting...")); } void CaptivePortal::setup() { @@ -71,7 +71,7 @@ void CaptivePortal::setup() { void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { - this->base_->add_handler(this); + this->base_->add_handler_without_auth(this); } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index dbbcd10d8df..3e1baf34bad 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -11,6 +11,10 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } #endif + this->add_handler_without_auth(handler); +} + +void WebServerBase::add_handler_without_auth(AsyncWebHandler *handler) { this->handlers_.push_back(handler); if (this->server_ != nullptr) { this->server_->addHandler(handler); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 54421c851e5..48e13ad71e2 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -122,6 +122,14 @@ class WebServerBase { #endif void add_handler(AsyncWebHandler *handler); + /** + * WARNING: Registers a handler that bypasses the USE_WEBSERVER_AUTH middleware. + * + * This should only be used for endpoints that are intentionally unauthenticated + * (for example, captive portal or very limited-status endpoints). For normal + * endpoints that should respect web server authentication, use add_handler(). + */ + void add_handler_without_auth(AsyncWebHandler *handler); void set_port(uint16_t port) { port_ = port; } uint16_t get_port() const { return port_; } From dc5032f72fa5571eebb2381c208f9b4deb33f93d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:41 -1000 Subject: [PATCH 0673/2056] [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) --- .../template/water_heater/template_water_heater.cpp | 1 + tests/integration/test_water_heater_template.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 73081d204b4..092df6fdca3 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -26,6 +26,7 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { if (!this->supported_modes_.empty()) { traits.set_supported_modes(this->supported_modes_); + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_OPERATION_MODE); } traits.set_supports_current_temperature(true); diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index 096d4c84615..d63d1d69845 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -102,7 +102,11 @@ async def test_water_heater_template( f"Expected target temp 60.0, got {initial_state.target_temperature}" ) - # Verify supported features: away mode and on/off (fixture has away + is_on lambdas) + # Verify supported features: operation mode, away mode, and on/off + assert ( + test_water_heater.supported_features + & WaterHeaterFeature.SUPPORTS_OPERATION_MODE + ) != 0, "Expected SUPPORTS_OPERATION_MODE in supported_features" assert ( test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE ) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features" From aacbaab5f800e26c7f614ec641f061dfcda4ce77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:48:55 -1000 Subject: [PATCH 0674/2056] [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) --- esphome/components/wifi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2808d313111..480ccd65c54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -166,6 +166,7 @@ TTLS_PHASE_2 = { } EAP_AUTH_SCHEMA = cv.All( + cv.only_on([Platform.ESP32, Platform.ESP8266]), cv.Schema( { cv.Optional(CONF_IDENTITY): cv.string_strict, From b0447dc52165abf25b18281bb0e5277e50ba896a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:49:07 -1000 Subject: [PATCH 0675/2056] [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) --- esphome/components/light/light_call.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index cd45994f625..0b2d391fd6e 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -214,7 +214,14 @@ LightColorValues LightCall::validate_() { if (this->has_brightness() && this->brightness_ == 0.0f) { this->state_ = false; this->set_flag_(FLAG_HAS_STATE); - this->brightness_ = 1.0f; + if (color_mode & ColorCapability::BRIGHTNESS) { + // Reset brightness so the light has nonzero brightness when turned back on. + this->brightness_ = 1.0f; + } else { + // Light doesn't support brightness; clear the flag to avoid a spurious + // "brightness not supported" warning during capability validation. + this->clear_flag_(FLAG_HAS_BRIGHTNESS); + } } // Set color brightness to 100% if currently zero and a color is set. From 039efdb02a765f554e33f0d2c897cd69fa9fd581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 14:53:46 -1000 Subject: [PATCH 0676/2056] [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) --- esphome/components/i2c/__init__.py | 37 ++++++++++++++++++++++ esphome/components/i2c/i2c_bus_arduino.cpp | 10 +++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index de3f2be6740..1684f479ba3 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -93,11 +93,31 @@ def _bus_declare_type(value): raise NotImplementedError +def _rp2040_i2c_controller(pin): + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. + + See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): + https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + See RP2350 datasheet Table 7 (section 9.4, "Function Select"): + https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + """ + return (pin // 2) % 2 + + def validate_config(config): if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) + if CORE.is_rp2040: + sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) + scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) + if sda_controller != scl_controller: + raise cv.Invalid( + f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but " + f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. " + f"Both pins must be on the same I2C controller." + ) return config @@ -146,6 +166,23 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_rp2040: + if len(full_config) > 2: + raise cv.Invalid( + "The maximum number of I2C interfaces for RP2040/RP2350 is 2" + ) + if len(full_config) > 1: + controllers = [ + _rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config + ] + if len(set(controllers)) != len(controllers): + raise cv.Invalid( + "Multiple I2C buses are configured to use the same I2C controller. " + "Each bus must use pins on a different controller. " + "The I2C controller is determined by (gpio / 2) % 2: " + "even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, " + "odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1." + ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 5120eb4c007..47a06abe9ec 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -20,12 +20,14 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) - static bool first = true; - if (first) { + // Select Wire instance based on pin assignment, not definition order. + // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 + // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; - first = false; } else { - wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + wire_ = &Wire1; } #endif From 1ab1534028ef2052825e09301f26bb3e02f45d82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:12:22 -1000 Subject: [PATCH 0677/2056] [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mdns/mdns_component.h | 4 +++ esphome/components/mdns/mdns_rp2040.cpp | 32 ++++++++++++++++++---- esphome/components/wifi/__init__.py | 7 ----- esphome/components/wifi/wifi_component.cpp | 14 ---------- esphome/components/wifi/wifi_component.h | 4 --- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 13c8ccf2884..47cad4bf71b 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -129,6 +129,10 @@ class MDNSComponent final : public Component { #endif #ifdef USE_MDNS_STORE_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; +#endif +#ifdef USE_RP2040 + bool was_connected_{false}; + bool initialized_{false}; #endif void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf); }; diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 05d991c1fad..c0b22aa84fb 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -36,12 +36,32 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV } void MDNSComponent::setup() { - this->setup_buffers_and_register_(register_rp2040); - // Schedule MDNS.update() via set_interval() instead of overriding loop(). - // This removes the component from the per-iteration loop list entirely, - // eliminating virtual dispatch overhead on every main loop cycle. - // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. - this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + // RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart + // mDNS when the network interface reconnects. However, stateUpCB() is stubbed out + // in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used + // schedule_function() which doesn't exist in arduino-pico, and the callback can't + // safely run directly since netif status callbacks fire from IRQ context + // (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets. + // + // Workaround: defer MDNS.begin() and service registration until the network is + // connected (has an IP), then call notifyAPChange() on subsequent reconnects to + // restart mDNS probing and announcing — all from main loop context so it's + // thread-safe. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (connected && !this->was_connected_) { + if (!this->initialized_) { + this->setup_buffers_and_register_(register_rp2040); + this->initialized_ = true; + } else { + MDNS.notifyAPChange(); + } + } + this->was_connected_ = connected; + if (this->initialized_) { + MDNS.update(); + } + }); } void MDNSComponent::on_shutdown() { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 480ccd65c54..9f73b1cc6f5 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -563,13 +563,6 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) elif CORE.is_rp2040: cg.add_library("WiFi", None) - # RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart - # mDNS when the network interface reconnects. However, this callback is disabled - # in the arduino-pico framework. As a workaround, we block component setup until - # WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an - # active connection. This define enables the loop priority sorting infrastructure - # used during the setup blocking phase. - cg.add_define("USE_LOOP_PRIORITY") if CORE.is_esp32: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 60764955cc9..09f883ed617 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2109,20 +2109,6 @@ void WiFiComponent::retry_connect() { } } -#ifdef USE_RP2040 -// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart -// mDNS when the network interface reconnects. However, this callback is disabled -// in the arduino-pico framework. As a workaround, we block component setup until -// WiFi is connected, ensuring mDNS.begin() is called with an active connection. - -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; - } - return this->is_connected_(); -} -#endif - void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f340b708c90..883cc1344b4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -437,10 +437,6 @@ class WiFiComponent : public Component { void retry_connect(); -#ifdef USE_RP2040 - bool can_proceed() override; -#endif - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected() const { return this->connected_; } From 45e40223ac3a32fdca7a535f35da73b6d866f29b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 12 Mar 2026 15:37:46 -1000 Subject: [PATCH 0678/2056] [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) --- esphome/components/mdns/mdns_rp2040.cpp | 5 +++++ esphome/components/rp2040/crash_handler.cpp | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index c0b22aa84fb..88f707afd37 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -7,7 +7,12 @@ #include "esphome/core/log.h" #include "mdns_component.h" +// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty. +// Save and restore our definition around the include to avoid a redefinition warning. +#pragma push_macro("IRAM_ATTR") +#undef IRAM_ATTR #include <ESP8266mDNS.h> +#pragma pop_macro("IRAM_ATTR") namespace esphome::mdns { diff --git a/esphome/components/rp2040/crash_handler.cpp b/esphome/components/rp2040/crash_handler.cpp index 1f579c2d18e..f9eb42a0f8a 100644 --- a/esphome/components/rp2040/crash_handler.cpp +++ b/esphome/components/rp2040/crash_handler.cpp @@ -57,14 +57,14 @@ static const char *const TAG = "rp2040.crash"; // Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear(). // The valid field is explicitly cleared in crash_handler_read_and_clear() instead. -static struct { +static struct CrashData { bool valid; uint32_t pc; uint32_t lr; uint32_t sp; uint32_t backtrace[MAX_BACKTRACE]; uint8_t backtrace_count; -} __attribute__((section(".noinit"))) s_crash_data; +} s_crash_data __attribute__((section(".noinit"))); bool crash_handler_has_data() { return s_crash_data.valid; } From 18b54f075ee02f1f36da37681968b147554f89ba Mon Sep 17 00:00:00 2001 From: Kjell Braden <afflux@pentabarf.de> Date: Fri, 13 Mar 2026 14:18:42 +0100 Subject: [PATCH 0679/2056] [runtime_image] fix BMP parsing (#14762) --- .../components/runtime_image/bmp_decoder.h | 4 + .../fixtures/online_image_bmp.yaml | 27 ++++ tests/integration/test_online_image_bmp.py | 119 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 tests/integration/fixtures/online_image_bmp.yaml create mode 100644 tests/integration/test_online_image_bmp.py diff --git a/esphome/components/runtime_image/bmp_decoder.h b/esphome/components/runtime_image/bmp_decoder.h index 73e54f54302..a52a5615849 100644 --- a/esphome/components/runtime_image/bmp_decoder.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -26,6 +26,10 @@ class BmpDecoder : public ImageDecoder { int HOT decode(uint8_t *buffer, size_t size) override; bool is_finished() const override { + if (this->bits_per_pixel_ == 0) { + // header not yet received, so dimensions not yet determined + return false; + } // BMP is finished when we've decoded all pixel data return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_); } diff --git a/tests/integration/fixtures/online_image_bmp.yaml b/tests/integration/fixtures/online_image_bmp.yaml new file mode 100644 index 00000000000..e36514e9ae2 --- /dev/null +++ b/tests/integration/fixtures/online_image_bmp.yaml @@ -0,0 +1,27 @@ +esphome: + name: online-image-bmp + +host: + +http_request: + +display: + +online_image: + - url: http://127.0.0.1:HTTP_PORT/foo.bmp + id: myimg + format: BMP + type: RGB + on_download_finished: + logger.log: + format: "download finished. cache hit: %u" + args: [cached] + +api: + actions: + - action: fetch_image + then: + - component.update: myimg + +logger: + level: DEBUG diff --git a/tests/integration/test_online_image_bmp.py b/tests/integration/test_online_image_bmp.py new file mode 100644 index 00000000000..7c32154fdd6 --- /dev/null +++ b/tests/integration/test_online_image_bmp.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# black 8x8 RGB BMP, generated with +# from PIL import Image +# from io import BytesIO +# b = BytesIO() +# img = Image.new("RGB", (8, 8)) +# img.save(b, format="BMP") +# b.getvalue() +BMP_IMAGE = b"BM\xf6\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xc0\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +LEN_BMP_IMAGE = len(BMP_IMAGE) + + +def handle_http(http_request_future): + async def handler(reader, writer): + try: + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n") + + # ensure our request matches the expectation + expected_request = b"GET /foo.bmp HTTP/1.1\r\n" + assert data[: len(expected_request)] == expected_request + + # consume rest of request + async with asyncio.timeout(1.0): + data = await reader.readuntil(b"\r\n\r\n") + + http_request_future.set_result(True) + + http_response = [ + b"HTTP/1.1 200 OK", + b"Content-Length: %d" % LEN_BMP_IMAGE, + b"Content-Type: text/plain", + b"Connection: close", + b"", + b"", + ] + writer.write(b"\r\n".join(http_response)) + await writer.drain() + + writer.write(BMP_IMAGE) + + await writer.drain() + except Exception as exc: + if not http_request_future.done(): + http_request_future.set_exception(exc) + raise + finally: + writer.close() + + return handler + + +@pytest.mark.asyncio +async def test_online_image_bmp( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Esphome shouldn't block the main loop when a http response is slow""" + loop = asyncio.get_running_loop() + + # Track http request + http_request_future = loop.create_future() + download_finished_future = loop.create_future() + downloaded_bytes_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + + if match := re.search(r"Image fully downloaded, (\d+) bytes", line): + downloaded_bytes_future.set_result(int(match.group(1))) + + if "download finished" in line: + download_finished_future.set_result(True) + + server = await asyncio.start_server( + handle_http(http_request_future), "127.0.0.1", 0 + ) + http_server_port = server.sockets[0].getsockname()[1] + + config = yaml_config.replace("HTTP_PORT", str(http_server_port)) + + # Run with log monitoring + async with ( + server, + run_compiled(config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "online-image-bmp" + + # List services to find our test service + _, services = await client.list_entities_services() + + # Find test service + request_service = next((s for s in services if s.name == "fetch_image"), None) + + assert request_service is not None, "fetch_image service not found" + + await client.execute_service(request_service, {}) + + async with asyncio.timeout(0.1): + await http_request_future + + async with asyncio.timeout(0.5): + numbytes = await downloaded_bytes_future + assert numbytes == LEN_BMP_IMAGE + await download_finished_future From e9c265914768a3c5fea24b6c2fb28eeb211cfb90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 03:20:50 -1000 Subject: [PATCH 0680/2056] [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) --- esphome/components/select/select_call.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 45fb42c1160..83f5052fc8a 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -41,7 +41,7 @@ SelectCall &SelectCall::with_index(size_t index) { this->operation_ = SELECT_OP_SET; if (index >= this->parent_->size()) { ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); - this->index_ = {}; // Store nullopt for invalid index + this->index_ = nullopt; // Store nullopt for invalid index } else { this->index_ = index; } @@ -52,7 +52,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { const auto &options = this->parent_->traits.get_options(); if (options.empty()) { ESP_LOGW(TAG, "'%s' - Select has no options", name); - return {}; + return nullopt; } if (this->operation_ == SELECT_OP_FIRST) { @@ -67,7 +67,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { ESP_LOGD(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); - return {}; + return nullopt; } return this->index_; } @@ -96,7 +96,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { return active_index + 1; } - return {}; // Can't navigate further without cycling + return nullopt; // Can't navigate further without cycling } void SelectCall::perform() { From 49107f217463b8ceb9200fd3bb51aa38d1bb9527 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:30 -1000 Subject: [PATCH 0681/2056] [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) --- esphome/components/api/api_frame_helper.h | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 98de24501ea..5e07ad43a93 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -134,12 +134,16 @@ class APIFrameHelper { // // For log messages: Use Nagle to coalesce multiple small log packets into // fewer larger packets, reducing WiFi overhead. However, we limit batching - // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained - // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into - // shared pbufs, but holding data too long waiting for Nagle's timer causes - // buffer exhaustion and dropped messages. + // to avoid excessive LWIP buffer pressure on memory-constrained devices. + // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but + // holding data too long waiting for Nagle's timer causes buffer exhaustion + // and dropped messages. // - // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle + // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers) + // + // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { @@ -150,7 +154,7 @@ class APIFrameHelper { return; } - // Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd) + // Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush) if (this->nodelay_state_ == NODELAY_ON) { this->set_nodelay_raw_(false); this->nodelay_state_ = 1; @@ -255,10 +259,16 @@ class APIFrameHelper { uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1-2 count log messages in the current Nagle batch. + // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. + // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. static constexpr int8_t NODELAY_ON = -1; +#ifdef USE_ESP8266 static constexpr int8_t LOG_NAGLE_COUNT = 2; +#else + static constexpr int8_t LOG_NAGLE_COUNT = 3; +#endif int8_t nodelay_state_{NODELAY_ON}; // Internal helper to set TCP_NODELAY socket option From a064eceb9bf750979c74009b508c8148eca70d81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 13 Mar 2026 07:37:44 -1000 Subject: [PATCH 0682/2056] [template] Fix misleading 'Text value too long to save' warning (#14753) --- .../components/template/text/template_text.h | 32 ++--- .../fixtures/template_text_save.yaml | 23 +++ tests/integration/test_template_text_save.py | 131 ++++++++++++++++++ 3 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tests/integration/fixtures/template_text_save.yaml create mode 100644 tests/integration/test_template_text_save.py diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 7f176db09ef..229a61d9b8e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -24,23 +24,23 @@ class TemplateTextSaverBase { template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { public: bool save(const std::string &value) override { - int diff = value.compare(this->prev_); - if (diff != 0) { - // If string is bigger than the allocation, do not save it. - // We don't need to waste ram setting prev_value either. - int size = value.size(); - if (size <= SZ) { - // Make it into a length prefixed thing - unsigned char temp[SZ + 1]; - memcpy(temp + 1, value.c_str(), size); - // SZ should be pre checked at the schema level, it can't go past the char range. - temp[0] = ((unsigned char) size); - this->pref_.save(&temp); - this->prev_.assign(value); - return true; - } + if (value == this->prev_) { + return true; // No change, nothing to save } - return false; + // If string is bigger than the allocation, do not save it. + // We don't need to waste ram setting prev_value either. + int size = value.size(); + if (size > SZ) { + return false; + } + // Make it into a length prefixed thing + unsigned char temp[SZ + 1]; + memcpy(temp + 1, value.c_str(), size); + // SZ should be pre checked at the schema level, it can't go past the char range. + temp[0] = ((unsigned char) size); + this->pref_.save(&temp); + this->prev_.assign(value); + return true; } // Make the preference object. Fill the provided location with the saved data diff --git a/tests/integration/fixtures/template_text_save.yaml b/tests/integration/fixtures/template_text_save.yaml new file mode 100644 index 00000000000..526561732de --- /dev/null +++ b/tests/integration/fixtures/template_text_save.yaml @@ -0,0 +1,23 @@ +esphome: + name: host-template-text-save-test + +host: + +api: + batch_delay: 0ms + +logger: + +preferences: + flash_write_interval: 0s + +text: + - platform: template + name: "Test Text Restore" + id: test_text_restore + optimistic: true + min_length: 0 + max_length: 10 + mode: text + initial_value: "hello" + restore_value: true diff --git a/tests/integration/test_template_text_save.py b/tests/integration/test_template_text_save.py new file mode 100644 index 00000000000..47c8e3188ab --- /dev/null +++ b/tests/integration/test_template_text_save.py @@ -0,0 +1,131 @@ +"""Integration test for template text restore_value persistence. + +Tests that: +1. A template text with restore_value saves its value to preferences +2. The saved value persists across restarts (binary re-run) +3. Setting the same value again does not produce a spurious "too long" warning +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import socket +from typing import Any + +from aioesphomeapi import TextInfo, TextState +import pytest + +from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client +from .state_utils import InitialStateHelper, require_entity +from .types import CompileFunction, ConfigWriter + + +@pytest.mark.asyncio +async def test_template_text_save( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> None: + """Test template text save/restore persistence and duplicate-save behavior.""" + port, port_socket = reserved_tcp_port + + # Clean up any stale preference file from previous runs + prefs_file = ( + Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs" + ) + if prefs_file.exists(): + prefs_file.unlink() + + # Write and compile once + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # Release the reserved port so the binary can bind to it + port_socket.close() + + # --- First run: set a value and verify no spurious warnings --- + warning_lines: list[str] = [] + + def capture_warnings(line: str) -> None: + if "too long to save" in line.lower(): + warning_lines.append(line) + + async with ( + run_binary_and_wait_for_port( + binary_path, "127.0.0.1", port, line_callback=capture_warnings + ), + wait_and_connect_api_client(port=port) as client, + ): + device_info = await client.device_info() + assert device_info.name == "host-template-text-save-test" + + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + # Set up state tracking + loop = asyncio.get_running_loop() + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + # Verify initial value from config + initial = initial_state_helper.initial_states[text_entity.key] + assert isinstance(initial, TextState) + assert initial.state == "hello" + + async def wait_for_state(key: int, timeout: float = 2.0) -> Any: + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Set a new value that fits within max_length + client.text_command(key=text_entity.key, state="world") + state = await wait_for_state(text_entity.key) + assert state.state == "world" + + # Set the same value again - should NOT produce "too long" warning + client.text_command(key=text_entity.key, state="world") + # Give time for the warning to appear (if any) + await asyncio.sleep(0.5) + + # No warnings should have appeared + assert warning_lines == [], ( + f"Unexpected 'too long to save' warning(s): {warning_lines}" + ) + + # --- Second run: verify the value was restored from preferences --- + async with ( + run_binary_and_wait_for_port(binary_path, "127.0.0.1", port), + wait_and_connect_api_client(port=port) as client, + ): + entities, _ = await client.list_entities_services() + text_entity = require_entity( + entities, "test_text_restore", TextInfo, "Test Text Restore" + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None)) + await initial_state_helper.wait_for_initial_states() + + # The value should be "world" - restored from preferences + restored = initial_state_helper.initial_states[text_entity.key] + assert isinstance(restored, TextState) + assert restored.state == "world", ( + f"Expected restored value 'world', got '{restored.state}'" + ) + + # Clean up preference file + if prefs_file.exists(): + prefs_file.unlink() From 98d98716203da8d8d7a42f3312bb547ce2c425cb Mon Sep 17 00:00:00 2001 From: leccelecce <24962424+leccelecce@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:15:54 +0000 Subject: [PATCH 0683/2056] [online_image] Log download duration in milliseconds instead of seconds (#14803) --- esphome/components/online_image/online_image.cpp | 6 +++--- esphome/components/online_image/online_image.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index da866599c9d..22bf6a3056f 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -129,7 +129,7 @@ void OnlineImage::update() { } ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); - this->start_time_ = ::time(nullptr); + this->start_time_ = millis(); this->enable_loop(); } @@ -155,8 +155,8 @@ void OnlineImage::loop() { // Finalize decoding this->end_decode(); - ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), - (uint32_t) (::time(nullptr) - this->start_time_)); + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(), + millis() - this->start_time_); // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index c7c80c7c667..12c25645260 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent, */ std::string last_modified_ = ""; - time_t start_time_; + uint32_t start_time_{0}; }; template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { From 632dbc8fe83a7de72841ad72965a4e484898cc81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 08:12:04 -1000 Subject: [PATCH 0684/2056] [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) --- esphome/components/esp8266/helpers.cpp | 4 +--- esphome/components/libretiny/helpers.cpp | 4 +--- esphome/components/zephyr/core.cpp | 4 +--- esphome/core/helpers.h | 22 +++++++++++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 036594fa178..4a64ae181e3 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -22,9 +22,7 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -// ESP8266 doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// ESP8266 LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index 37ae0fb455a..21913e4a16d 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } -// LibreTiny doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// LibreTiny LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) WiFi.macAddress(mac); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index eee7fb3f4f8..1d105a10572 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -76,9 +76,7 @@ void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } -// Zephyr doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// Zephyr LwIPLock is defined inline as a no-op in helpers.h uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b2517e2d7ac..dafd899ae43 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1801,19 +1801,27 @@ class InterruptLock { /** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads. * - * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled. - * It ensures thread-safe access to lwIP APIs. + * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, + * and on RP2040 when CYW43 WiFi is active (cyw43_arch_lwip_begin/end). * - * @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp + * On platforms without lwIP core locking (ESP8266, LibreTiny, Zephyr), + * this is a no-op defined inline so the compiler can eliminate all call overhead. */ class LwIPLock { public: - LwIPLock(); - ~LwIPLock(); - - // Delete copy constructor and copy assignment operator to prevent accidental copying LwIPLock(const LwIPLock &) = delete; LwIPLock &operator=(const LwIPLock &) = delete; + +#if defined(USE_ESP32) || defined(USE_RP2040) + // Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp + LwIPLock(); + ~LwIPLock(); +#else + // No lwIP core locking — inline no-ops (empty bodies instead of = default + // to prevent clang-tidy unused-variable warnings at call sites) + LwIPLock() {} + ~LwIPLock() {} +#endif }; /** Helper class to request `loop()` to be called as fast as possible. From 22ea2764d4c48edfb365150a11be9c557806f9bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 14 Mar 2026 13:17:32 -1000 Subject: [PATCH 0685/2056] [debug] Fix shared buffer between reset reason and wakeup cause (#14813) --- esphome/components/debug/debug_component.h | 3 ++- esphome/components/debug/debug_esp32.cpp | 9 +++++---- esphome/components/debug/debug_esp8266.cpp | 2 +- esphome/components/debug/debug_host.cpp | 2 +- esphome/components/debug/debug_libretiny.cpp | 2 +- esphome/components/debug/debug_rp2040.cpp | 2 +- esphome/components/debug/debug_zephyr.cpp | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index e4f4bb36eba..3da6b800c6f 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -18,6 +18,7 @@ namespace debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; +static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128; // buf_append_printf is now provided by esphome/core/helpers.h @@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent { #endif // USE_TEXT_SENSOR const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); - const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer); + const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer); uint32_t get_free_heap_(); size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos); void update_platform_(); diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 6898621dd05..c9df4fdf210 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = { "BT", }; -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { const char *wake_reason; unsigned reason = esp_sleep_get_wakeup_cause(); if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) { @@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - char reason_buffer[RESET_REASON_BUFFER_SIZE]; - const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); - const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); + char reset_buffer[RESET_REASON_BUFFER_SIZE]; + char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE]; + const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer)); + const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer)); uint8_t mac[6]; get_mac_address_raw(mac); diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 4df4aaa8513..0519ab72fe9 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buffer.data(); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // ESP8266 doesn't have detailed wakeup cause like ESP32 return ""; } diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp index 2fa88f0909c..0dfab86e4c4 100644 --- a/esphome/components/debug/debug_host.cpp +++ b/esphome/components/debug/debug_host.cpp @@ -7,7 +7,7 @@ namespace debug { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 39269d6f2f2..1d458c602a6 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return lt_get_reboot_reason_name(lt_get_reboot_reason()); } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index 8dc84a26732..73f08492c86 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bd6432e9499..bf87b7ae3dc 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { // Zephyr doesn't have detailed wakeup cause like ESP32 return ""; } From deb6b97eea98feea201c73402df493c30a502e7d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:25:21 +1300 Subject: [PATCH 0686/2056] Bump version to 2026.3.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 9f5cd0a2ce3..4ec3a24c9fe 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b1 +PROJECT_NUMBER = 2026.3.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index eb49c9a1d77..2466f2c49c0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b1" +__version__ = "2026.3.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 92d5e7b18c233403ea58835e12fffd4a52431ede Mon Sep 17 00:00:00 2001 From: Bonne Eggleston <bonne@exciton.com.au> Date: Sun, 15 Mar 2026 16:02:23 -0700 Subject: [PATCH 0687/2056] [tests] Fix integration helper to match entities exactly (#14837) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../fixtures/sensor_filters_nan_handling.yaml | 18 +++++++++--------- .../fixtures/sensor_filters_ring_buffer.yaml | 18 +++++++++--------- .../sensor_filters_ring_buffer_wraparound.yaml | 8 ++++---- tests/integration/state_utils.py | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/integration/fixtures/sensor_filters_nan_handling.yaml b/tests/integration/fixtures/sensor_filters_nan_handling.yaml index fcb12cfde58..beaf55eacf8 100644 --- a/tests/integration/fixtures/sensor_filters_nan_handling.yaml +++ b/tests/integration/fixtures/sensor_filters_nan_handling.yaml @@ -3,7 +3,7 @@ esphome: host: api: - batch_delay: 0ms # Disable batching to receive all state updates + batch_delay: 0ms # Disable batching to receive all state updates logger: level: DEBUG @@ -15,8 +15,8 @@ sensor: - platform: copy source_id: source_nan_sensor - name: "Min NaN Sensor" - id: min_nan_sensor + name: "Min NaN" + id: min_nan filters: - min: window_size: 5 @@ -25,8 +25,8 @@ sensor: - platform: copy source_id: source_nan_sensor - name: "Max NaN Sensor" - id: max_nan_sensor + name: "Max NaN" + id: max_nan filters: - max: window_size: 5 @@ -42,7 +42,7 @@ script: - delay: 20ms - sensor.template.publish: id: source_nan_sensor - state: !lambda 'return NAN;' + state: !lambda "return NAN;" - delay: 20ms - sensor.template.publish: id: source_nan_sensor @@ -50,7 +50,7 @@ script: - delay: 20ms - sensor.template.publish: id: source_nan_sensor - state: !lambda 'return NAN;' + state: !lambda "return NAN;" - delay: 20ms - sensor.template.publish: id: source_nan_sensor @@ -62,7 +62,7 @@ script: - delay: 20ms - sensor.template.publish: id: source_nan_sensor - state: !lambda 'return NAN;' + state: !lambda "return NAN;" - delay: 20ms - sensor.template.publish: id: source_nan_sensor @@ -74,7 +74,7 @@ script: - delay: 20ms - sensor.template.publish: id: source_nan_sensor - state: !lambda 'return NAN;' + state: !lambda "return NAN;" button: - platform: template diff --git a/tests/integration/fixtures/sensor_filters_ring_buffer.yaml b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml index ea7a326b8dd..b9b8ed8f74d 100644 --- a/tests/integration/fixtures/sensor_filters_ring_buffer.yaml +++ b/tests/integration/fixtures/sensor_filters_ring_buffer.yaml @@ -3,7 +3,7 @@ esphome: host: api: - batch_delay: 0ms # Disable batching to receive all state updates + batch_delay: 0ms # Disable batching to receive all state updates logger: level: DEBUG @@ -18,8 +18,8 @@ sensor: # Window of 5, send every 2 values - platform: copy source_id: source_sensor - name: "Sliding Min Sensor" - id: sliding_min_sensor + name: "Sliding Min" + id: sliding_min filters: - min: window_size: 5 @@ -28,8 +28,8 @@ sensor: - platform: copy source_id: source_sensor - name: "Sliding Max Sensor" - id: sliding_max_sensor + name: "Sliding Max" + id: sliding_max filters: - max: window_size: 5 @@ -38,8 +38,8 @@ sensor: - platform: copy source_id: source_sensor - name: "Sliding Median Sensor" - id: sliding_median_sensor + name: "Sliding Median" + id: sliding_median filters: - median: window_size: 5 @@ -48,8 +48,8 @@ sensor: - platform: copy source_id: source_sensor - name: "Sliding Moving Avg Sensor" - id: sliding_moving_avg_sensor + name: "Sliding Moving Avg" + id: sliding_moving_avg filters: - sliding_window_moving_average: window_size: 5 diff --git a/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml b/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml index bd5980160bf..d1528e4438a 100644 --- a/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml +++ b/tests/integration/fixtures/sensor_filters_ring_buffer_wraparound.yaml @@ -3,20 +3,20 @@ esphome: host: api: - batch_delay: 0ms # Disable batching to receive all state updates + batch_delay: 0ms # Disable batching to receive all state updates logger: level: DEBUG sensor: - platform: template - name: "Source Wraparound Sensor" + name: "Source Wraparound" id: source_wraparound accuracy_decimals: 2 - platform: copy source_id: source_wraparound - name: "Wraparound Min Sensor" - id: wraparound_min_sensor + name: "Wraparound Min" + id: wraparound_min filters: - min: window_size: 3 diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index ab9fdb01bb9..5792a8e8046 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -88,7 +88,7 @@ def build_key_to_entity_mapping( Args: entities: List of entity info objects from the API - entity_names: List of entity names to search for in object_ids + entity_names: List of entity names to match exactly against object_ids Returns: Dictionary mapping entity keys to entity names @@ -97,7 +97,7 @@ def build_key_to_entity_mapping( for entity in entities: obj_id = entity.object_id.lower() for entity_name in entity_names: - if entity_name in obj_id: + if entity_name == obj_id: key_to_entity[entity.key] = entity_name break return key_to_entity From d97c23b8e3c8cb7e7feec411c58fe0c3a4a7b006 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 15:13:10 -1000 Subject: [PATCH 0688/2056] [core] Add no-arg status_set_warning() to allow linker GC of const char* overload (#14821) --- esphome/components/rx8130/rx8130.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.cpp | 2 +- esphome/components/zwave_proxy/zwave_proxy.cpp | 2 +- esphome/core/component.cpp | 1 + esphome/core/component.h | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 9e6f05ee15c..07ed7acc565 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -68,7 +68,7 @@ void RX8130Component::dump_config() { void RX8130Component::read_time() { uint8_t date[7]; if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } ESPTime rtc_time{ @@ -109,7 +109,7 @@ void RX8130Component::write_time() { buff[6] = dec2bcd(now.year % 100); this->stop_(true); if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); } else { ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, now.minute, now.second); @@ -120,7 +120,7 @@ void RX8130Component::write_time() { void RX8130Component::stop_(bool stop) { const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS; if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); } } diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 3d35f368fb8..997f8361464 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -416,7 +416,7 @@ void USBUartTypeCdcAcm::on_connected() { for (auto *channel : this->channels_) { if (i == cdc_devs.size()) { ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); - this->status_set_warning("No configuration found for channel"); + this->status_set_warning(LOG_STR("No configuration found for channel")); break; } channel->cdc_dev_ = cdc_devs[i++]; diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 9e5c57814d2..ad4357663ff 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -90,7 +90,7 @@ void ZWaveProxy::process_uart_() { while (this->available()) { uint8_t byte; if (!this->read_byte(&byte)) { - this->status_set_warning("UART read failed"); + this->status_set_warning(LOG_STR("UART read failed")); return; } if (this->parse_byte_(byte)) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index cce0c7b3e04..761f1bd4858 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -392,6 +392,7 @@ bool Component::set_status_flag_(uint8_t flag) { return true; } +void Component::status_set_warning() { this->status_set_warning((const LogString *) nullptr); } void Component::status_set_warning(const char *message) { if (!this->set_status_flag_(STATUS_LED_WARNING)) return; diff --git a/esphome/core/component.h b/esphome/core/component.h index 7266f57e151..1aac1c72191 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -240,7 +240,8 @@ class Component { bool status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } - void status_set_warning(const char *message = nullptr); + void status_set_warning(); // Set warning flag without message + void status_set_warning(const char *message); void status_set_warning(const LogString *message); void status_set_error(); // Set error flag without message From 29501ef4f87870db3e9bee59c13681a99534ca00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 15:13:34 -1000 Subject: [PATCH 0689/2056] [core] Mark leaf Component subclasses as final (#14833) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/api/api_server.h | 6 +++--- esphome/components/binary_sensor/filter.h | 2 +- esphome/components/gpio/binary_sensor/gpio_binary_sensor.h | 2 +- esphome/components/gpio/switch/gpio_switch.h | 2 +- esphome/components/homeassistant/time/homeassistant_time.h | 2 +- esphome/components/md5/md5.h | 2 +- esphome/components/preferences/syncer.h | 2 +- esphome/components/restart/button/restart_button.h | 2 +- esphome/components/safe_mode/button/safe_mode_button.h | 2 +- esphome/components/sha256/sha256.h | 2 +- esphome/components/version/version_text_sensor.h | 2 +- esphome/components/wifi/wifi_component.h | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 69fc26cc00c..ccba6deb00a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -36,11 +36,11 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -class APIServer : public Component, - public Controller +class APIServer final : public Component, + public Controller #ifdef USE_CAMERA , - public camera::CameraListener + public camera::CameraListener #endif { public: diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2735a32ab07..0813847ca21 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -37,7 +37,7 @@ class TimeoutFilter : public Filter, public Component { TemplatableValue<uint32_t> timeout_delay_{}; }; -class DelayedOnOffFilter : public Filter, public Component { +class DelayedOnOffFilter final : public Filter, public Component { public: optional<bool> new_value(bool value) override; diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 8cf52f540b3..8b1cc296134 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -39,7 +39,7 @@ class GPIOBinarySensorStore { Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() }; -class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { +class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { public: // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index 080decac082..a73fb9e18cc 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace gpio { -class GPIOSwitch : public switch_::Switch, public Component { +class GPIOSwitch final : public switch_::Switch, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; } diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 7b5842fefdb..455ded20226 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -7,7 +7,7 @@ namespace esphome { namespace homeassistant { -class HomeassistantTime : public time::RealTimeClock { +class HomeassistantTime final : public time::RealTimeClock { public: void setup() override; void update() override; diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index 6ff651b02ed..80e74d188ef 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -32,7 +32,7 @@ namespace esphome { namespace md5 { -class MD5Digest : public HashBase { +class MD5Digest final : public HashBase { public: MD5Digest() = default; ~MD5Digest() override; diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index b6b422d4bae..96716d3f304 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -6,7 +6,7 @@ namespace esphome { namespace preferences { -class IntervalSyncer : public Component { +class IntervalSyncer final : public Component { public: void set_write_interval(uint32_t write_interval) { this->write_interval_ = write_interval; } void setup() override { diff --git a/esphome/components/restart/button/restart_button.h b/esphome/components/restart/button/restart_button.h index db18f1dadcd..fd51282d36d 100644 --- a/esphome/components/restart/button/restart_button.h +++ b/esphome/components/restart/button/restart_button.h @@ -6,7 +6,7 @@ namespace esphome { namespace restart { -class RestartButton : public button::Button, public Component { +class RestartButton final : public button::Button, public Component { public: void dump_config() override; diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h index fea0955abb6..0307a81febd 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.h +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -7,7 +7,7 @@ namespace esphome { namespace safe_mode { -class SafeModeButton : public button::Button, public Component { +class SafeModeButton final : public button::Button, public Component { public: void dump_config() override; void set_safe_mode(SafeModeComponent *safe_mode_component); diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index 0f995fcd916..d10d418c7a8 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -48,7 +48,7 @@ namespace esphome::sha256 { /// hasher.init(); /// hasher.add(data, len); /// hasher.calculate(); -class SHA256 : public esphome::HashBase { +class SHA256 final : public esphome::HashBase { public: SHA256() = default; ~SHA256() override; diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index fec898ae038..d2ca0ba6f65 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -5,7 +5,7 @@ namespace esphome::version { -class VersionTextSensor : public text_sensor::TextSensor, public Component { +class VersionTextSensor final : public text_sensor::TextSensor, public Component { public: void set_hide_hash(bool hide_hash); void set_hide_timestamp(bool hide_timestamp); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index aeb32352a97..057f2c06613 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -399,7 +399,7 @@ class WiFiPowerSaveListener { }; /// This component is responsible for managing the ESP WiFi interface. -class WiFiComponent : public Component { +class WiFiComponent final : public Component { public: /// Construct a WiFiComponent. WiFiComponent(); From 1377776d218bc8061d7bb9da96517e1e34105867 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 15:17:21 -1000 Subject: [PATCH 0690/2056] [ethernet] Restructure for multi-platform support (#14819) --- esphome/components/ethernet/__init__.py | 306 ++++--- .../ethernet/ethernet_component.cpp | 850 +----------------- .../components/ethernet/ethernet_component.h | 89 +- .../ethernet/ethernet_component_esp32.cpp | 841 +++++++++++++++++ .../components/ethernet/ethernet_helpers.c | 3 + .../ethernet_info_text_sensor.cpp | 4 +- .../ethernet_info/ethernet_info_text_sensor.h | 4 +- esphome/core/defines.h | 6 + 8 files changed, 1099 insertions(+), 1004 deletions(-) create mode 100644 esphome/components/ethernet/ethernet_component_esp32.cpp diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e520c0e914d..83bef4d91cb 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -2,23 +2,8 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components.esp32 import ( - VARIANT_ESP32, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32P4, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - add_idf_component, - add_idf_sdkconfig_option, - get_esp32_variant, - idf_version, - include_builtin_idf_component, -) from esphome.components.network import ip_address_literal -from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -50,6 +35,8 @@ from esphome.const import ( CONF_VALUE, KEY_CORE, KEY_FRAMEWORK_VERSION, + Platform, + PlatformFramework, ) from esphome.core import ( CORE, @@ -61,7 +48,6 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] -DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) @@ -174,9 +160,16 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) ManualIP = ethernet_ns.struct("ManualIP") -def _is_framework_spi_polling_mode_supported(): - # SPI Ethernet without IRQ feature is added in - # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) +def _is_framework_spi_polling_mode_supported() -> bool: + """Check if ESP-IDF framework supports SPI polling mode (ESP32 only). + + SPI Ethernet without IRQ feature is added in + esp-idf >= (5.3+, 5.2.1+, 5.1.4) + """ + if not CORE.is_esp32: + return False + from esphome.components.esp32 import idf_version + ver = idf_version() if ver >= cv.Version(5, 3, 0): return True @@ -195,52 +188,63 @@ def _validate(config): use_address = CORE.name + config[CONF_DOMAIN] config[CONF_USE_ADDRESS] = use_address - if config[CONF_TYPE] in SPI_ETHERNET_TYPES: - if _is_framework_spi_polling_mode_supported(): - if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: - raise cv.Invalid( - f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" - ) - if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config: - config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL - else: - if CONF_POLLING_INTERVAL in config: - raise cv.Invalid( - "In this version of the framework " - f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " - f"'{CONF_POLLING_INTERVAL}' is not supported." - ) - if CONF_INTERRUPT_PIN not in config: - raise cv.Invalid( - "In this version of the framework " - f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " - f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." - ) - elif config[CONF_TYPE] != "OPENETH": - if CONF_CLK_MODE in config: - mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated. " - "Please replace 'clk_mode: %s' with:\n" - " clk:\n" - " mode: %s\n" - " pin: %s\n" - "Removal scheduled for 2026.7.0.", - config[CONF_CLK_MODE], - mode, - pin, - ) - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) - del config[CONF_CLK_MODE] - elif CONF_CLK not in config: - raise cv.Invalid("'clk' is a required option for [ethernet].") - variant = get_esp32_variant() - if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): - raise cv.Invalid( - f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " - f"on ESP32 classic and ESP32-P4, not {variant}" + if CORE.is_esp32: + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: + if _is_framework_spi_polling_mode_supported(): + if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: + raise cv.Invalid( + f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" + ) + if ( + CONF_POLLING_INTERVAL not in config + and CONF_INTERRUPT_PIN not in config + ): + config[CONF_POLLING_INTERVAL] = ( + SPI_ETHERNET_DEFAULT_POLLING_INTERVAL + ) + else: + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_POLLING_INTERVAL}' is not supported." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." + ) + elif config[CONF_TYPE] != "OPENETH": + from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32P4, + get_esp32_variant, ) + if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] + LOGGER.warning( + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, + ) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) + del config[CONF_CLK_MODE] + elif CONF_CLK not in config: + raise cv.Invalid("'clk' is a required option for [ethernet].") + variant = get_esp32_variant() + if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): + raise cv.Invalid( + f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " + f"on ESP32 classic and ESP32-P4, not {variant}" + ) return config @@ -269,41 +273,47 @@ CLK_SCHEMA = cv.Schema( cv.Required(CONF_PIN): pins.internal_gpio_pin_number, } ) -RMII_SCHEMA = BASE_SCHEMA.extend( - cv.Schema( - { - cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLK_MODE): cv.enum( - CLK_MODES_DEPRECATED, upper=True, space="_" - ), - cv.Optional(CONF_CLK): CLK_SCHEMA, - cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), - cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), - } - ) +RMII_SCHEMA = cv.All( + BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLK_MODE): cv.enum( + CLK_MODES_DEPRECATED, upper=True, space="_" + ), + cv.Optional(CONF_CLK): CLK_SCHEMA, + cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), + cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), + } + ) + ), + cv.only_on([Platform.ESP32]), ) -SPI_SCHEMA = BASE_SCHEMA.extend( - cv.Schema( - { - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( - cv.frequency, cv.int_range(int(8e6), int(80e6)) - ), - # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() - cv.Optional(CONF_POLLING_INTERVAL): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), - ), - } +SPI_SCHEMA = cv.All( + BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( + cv.frequency, cv.int_range(int(8e6), int(80e6)) + ), + # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() + cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), + ), + } + ), ), + cv.only_on([Platform.ESP32]), ) CONFIG_SCHEMA = cv.All( @@ -317,7 +327,7 @@ CONFIG_SCHEMA = cv.All( "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, "W5500": SPI_SCHEMA, - "OPENETH": BASE_SCHEMA, + "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, "LAN8670": RMII_SCHEMA, }, @@ -328,8 +338,21 @@ CONFIG_SCHEMA = cv.All( def _final_validate_spi(config): + if not CORE.is_esp32: + return # SPI interface validation is ESP32-only if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return + from esphome.components.esp32 import ( + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + get_esp32_variant, + ) + from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface + if spi_configs := fv.full_config.get().get(CONF_SPI): variant = get_esp32_variant() if variant in ( @@ -378,6 +401,47 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if CORE.is_esp32: + await _to_code_esp32(var, config) + + cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) + cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + + if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") + cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) + + # Add compile-time define for PHY types with specific code + if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): + cg.add_define(phy_define) + + if mac_address := config.get(CONF_MAC_ADDRESS): + cg.add(var.set_fixed_mac(mac_address.parts)) + + cg.add_define("USE_ETHERNET") + + if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") + await automation.build_automation( + var.get_connect_trigger(), [], on_connect_config + ) + + if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER") + await automation.build_automation( + var.get_disconnect_trigger(), [], on_disconnect_config + ) + + CORE.add_job(final_step) + + +async def _to_code_esp32(var, config): + from esphome.components.esp32 import ( + add_idf_component, + add_idf_sdkconfig_option, + include_builtin_idf_component, + ) + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) @@ -415,22 +479,6 @@ async def to_code(config): ) cg.add(var.add_phy_register(reg)) - cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) - - if CONF_MANUAL_IP in config: - cg.add_define("USE_ETHERNET_MANUAL_IP") - cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) - - # Add compile-time define for PHY types with specific code - if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): - cg.add_define(phy_define) - - if mac_address := config.get(CONF_MAC_ADDRESS): - cg.add(var.set_fixed_mac(mac_address.parts)) - - cg.add_define("USE_ETHERNET") - # Disable WiFi when using Ethernet to save memory add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) # Also disable WiFi/BT coexistence since WiFi is disabled @@ -443,27 +491,21 @@ async def to_code(config): # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") - if on_connect_config := config.get(CONF_ON_CONNECT): - cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") - await automation.build_automation( - var.get_connect_trigger(), [], on_connect_config - ) - - if on_disconnect_config := config.get(CONF_ON_DISCONNECT): - cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER") - await automation.build_automation( - var.get_disconnect_trigger(), [], on_disconnect_config - ) - - CORE.add_job(final_step) - def _final_validate_rmii_pins(config: ConfigType) -> None: """Validate that RMII pins are not used by other components.""" + if not CORE.is_esp32: + return # RMII validation is ESP32-only # Only validate for RMII-based PHYs on ESP32/ESP32P4 if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": return # SPI and OPENETH don't use RMII + from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32P4, + get_esp32_variant, + ) + variant = get_esp32_variant() if variant == VARIANT_ESP32: rmii_pins = ESP32_RMII_FIXED_PINS @@ -521,3 +563,13 @@ async def final_step(): if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0): cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS") cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "ethernet_component_esp32.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, + } +) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index e0788e11498..4421a1c7aaf 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -1,547 +1,28 @@ #include "ethernet_component.h" -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" + +#ifdef USE_ETHERNET + #include "esphome/core/log.h" -#include "esphome/core/util.h" - -#ifdef USE_ESP32 - -#include <lwip/dns.h> -#include <cinttypes> -#include "esp_event.h" - -#ifdef USE_ETHERNET_LAN8670 -#include "esp_eth_phy_lan867x.h" -#endif - -#ifdef USE_ETHERNET_SPI -#include <driver/gpio.h> -#include <driver/spi_master.h> -#endif namespace esphome::ethernet { -static const char *const TAG = "ethernet"; - -// PHY register size for hex logging -static constexpr size_t PHY_REG_SIZE = 2; - EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { - ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); - this->mark_failed(); -} - -#define ESPHL_ERROR_CHECK(err, message) \ - if ((err) != ESP_OK) { \ - this->log_error_and_mark_failed_(err, message); \ - return; \ - } - -#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ - if ((err) != ESP_OK) { \ - this->log_error_and_mark_failed_(err, message); \ - return ret; \ - } - EthernetComponent::EthernetComponent() { global_eth_component = this; } -void EthernetComponent::setup() { - if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { - // Delay here to allow power to stabilise before Ethernet is initialized. - delay(300); // NOLINT - } - - esp_err_t err; - -#ifdef USE_ETHERNET_SPI - // Install GPIO ISR handler to be able to service SPI Eth modules interrupts - gpio_install_isr_service(0); - - spi_bus_config_t buscfg = { - .mosi_io_num = this->mosi_pin_, - .miso_io_num = this->miso_pin_, - .sclk_io_num = this->clk_pin_, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .data4_io_num = -1, - .data5_io_num = -1, - .data6_io_num = -1, - .data7_io_num = -1, - .max_transfer_sz = 0, - .flags = 0, - .intr_flags = 0, - }; - -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - auto host = SPI2_HOST; -#else - auto host = SPI3_HOST; -#endif - - err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); - ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); -#endif - - err = esp_netif_init(); - ESPHL_ERROR_CHECK(err, "ETH netif init error"); - err = esp_event_loop_create_default(); - ESPHL_ERROR_CHECK(err, "ETH event loop error"); - - esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); - this->eth_netif_ = esp_netif_new(&cfg); - - // Init MAC and PHY configs to default - eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); - -#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module - spi_device_interface_config_t devcfg = { - .command_bits = 0, - .address_bits = 0, - .dummy_bits = 0, - .mode = 0, - .duty_cycle_pos = 0, - .cs_ena_pretrans = 0, - .cs_ena_posttrans = 0, - .clock_speed_hz = this->clock_speed_, - .input_delay_ns = 0, - .spics_io_num = this->cs_pin_, - .flags = 0, - .queue_size = 20, - .pre_cb = nullptr, - .post_cb = nullptr, - }; - -#if CONFIG_ETH_SPI_ETHERNET_W5500 - eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); -#endif - -#if CONFIG_ETH_SPI_ETHERNET_W5500 - w5500_config.int_gpio_num = this->interrupt_pin_; -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - w5500_config.poll_period_ms = this->polling_interval_; -#endif -#endif - -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - dm9051_config.int_gpio_num = this->interrupt_pin_; -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - dm9051_config.poll_period_ms = this->polling_interval_; -#endif -#endif - - phy_config.phy_addr = this->phy_addr_spi_; - phy_config.reset_gpio_num = this->reset_pin_; - - esp_eth_mac_t *mac = nullptr; -#elif defined(USE_ETHERNET_OPENETH) - esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); -#else - phy_config.phy_addr = this->phy_addr_; - phy_config.reset_gpio_num = this->power_pin_; - - eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) - esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; - esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; -#else - esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; - esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; -#endif - esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; - esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; - - esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); -#endif - - switch (this->type_) { -#ifdef USE_ETHERNET_OPENETH - case ETHERNET_TYPE_OPENETH: { - phy_config.autonego_timeout_ms = 1000; - this->phy_ = esp_eth_phy_new_dp83848(&phy_config); - break; - } -#endif -#if CONFIG_ETH_USE_ESP32_EMAC -#ifdef USE_ETHERNET_LAN8720 - case ETHERNET_TYPE_LAN8720: { - this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); - break; - } -#endif -#ifdef USE_ETHERNET_RTL8201 - case ETHERNET_TYPE_RTL8201: { - this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); - break; - } -#endif -#ifdef USE_ETHERNET_DP83848 - case ETHERNET_TYPE_DP83848: { - this->phy_ = esp_eth_phy_new_dp83848(&phy_config); - break; - } -#endif -#ifdef USE_ETHERNET_IP101 - case ETHERNET_TYPE_IP101: { - this->phy_ = esp_eth_phy_new_ip101(&phy_config); - break; - } -#endif -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) - case ETHERNET_TYPE_JL1101: { - this->phy_ = esp_eth_phy_new_jl1101(&phy_config); - break; - } -#endif -#ifdef USE_ETHERNET_KSZ8081 - case ETHERNET_TYPE_KSZ8081: - case ETHERNET_TYPE_KSZ8081RNA: { - this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); - break; - } -#endif -#ifdef USE_ETHERNET_LAN8670 - case ETHERNET_TYPE_LAN8670: { - this->phy_ = esp_eth_phy_new_lan867x(&phy_config); - break; - } -#endif -#endif -#ifdef USE_ETHERNET_SPI -#if CONFIG_ETH_SPI_ETHERNET_W5500 - case ETHERNET_TYPE_W5500: { - mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); - this->phy_ = esp_eth_phy_new_w5500(&phy_config); - break; - } -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - case ETHERNET_TYPE_DM9051: { - mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); - this->phy_ = esp_eth_phy_new_dm9051(&phy_config); - break; - } -#endif -#endif - default: { - this->mark_failed(); - return; - } - } - - esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); - this->eth_handle_ = nullptr; - err = esp_eth_driver_install(ð_config, &this->eth_handle_); - ESPHL_ERROR_CHECK(err, "ETH driver install error"); - -#ifndef USE_ETHERNET_SPI -#ifdef USE_ETHERNET_KSZ8081 - if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { - // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. - this->ksz8081_set_clock_reference_(mac); - } -#endif // USE_ETHERNET_KSZ8081 - - for (const auto &phy_register : this->phy_registers_) { - this->write_phy_register_(mac, phy_register); - } -#endif - - // use ESP internal eth mac - uint8_t mac_addr[6]; - if (this->fixed_mac_.has_value()) { - memcpy(mac_addr, this->fixed_mac_->data(), 6); - } else { - esp_read_mac(mac_addr, ESP_MAC_ETH); - } - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); - ESPHL_ERROR_CHECK(err, "set mac address error"); - - /* attach Ethernet driver to TCP/IP stack */ - err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); - ESPHL_ERROR_CHECK(err, "ETH netif attach error"); - - // Register user defined event handers - err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "ETH event handler register error"); - err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); -#if USE_NETWORK_IPV6 - err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); -#endif /* USE_NETWORK_IPV6 */ - - /* start Ethernet driver state machine */ - err = esp_eth_start(this->eth_handle_); - ESPHL_ERROR_CHECK(err, "ETH start error"); -} - -void EthernetComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - switch (this->state_) { - case EthernetComponentState::STOPPED: - if (this->started_) { - ESP_LOGI(TAG, "Starting connection"); - this->state_ = EthernetComponentState::CONNECTING; - this->start_connect_(); - } - break; - case EthernetComponentState::CONNECTING: - if (!this->started_) { - ESP_LOGI(TAG, "Stopped connection"); - this->state_ = EthernetComponentState::STOPPED; - } else if (this->connected_) { - // connection established - ESP_LOGI(TAG, "Connected"); - this->state_ = EthernetComponentState::CONNECTED; - - this->dump_connect_params_(); - this->status_clear_warning(); -#ifdef USE_ETHERNET_CONNECT_TRIGGER - this->connect_trigger_.trigger(); -#endif - } else if (now - this->connect_begin_ > 15000) { - ESP_LOGW(TAG, "Connecting failed; reconnecting"); - this->start_connect_(); - } - break; - case EthernetComponentState::CONNECTED: - if (!this->started_) { - ESP_LOGI(TAG, "Stopped connection"); - this->state_ = EthernetComponentState::STOPPED; -#ifdef USE_ETHERNET_DISCONNECT_TRIGGER - this->disconnect_trigger_.trigger(); -#endif - } else if (!this->connected_) { - ESP_LOGW(TAG, "Connection lost; reconnecting"); - this->state_ = EthernetComponentState::CONNECTING; - this->start_connect_(); -#ifdef USE_ETHERNET_DISCONNECT_TRIGGER - this->disconnect_trigger_.trigger(); -#endif - } else { - this->finish_connect_(); - // When connected and stable, disable the loop to save CPU cycles - this->disable_loop(); - } - break; - } -} - -void EthernetComponent::dump_config() { - const char *eth_type; - switch (this->type_) { -#ifdef USE_ETHERNET_LAN8720 - case ETHERNET_TYPE_LAN8720: - eth_type = "LAN8720"; - break; -#endif -#ifdef USE_ETHERNET_RTL8201 - case ETHERNET_TYPE_RTL8201: - eth_type = "RTL8201"; - break; -#endif -#ifdef USE_ETHERNET_DP83848 - case ETHERNET_TYPE_DP83848: - eth_type = "DP83848"; - break; -#endif -#ifdef USE_ETHERNET_IP101 - case ETHERNET_TYPE_IP101: - eth_type = "IP101"; - break; -#endif -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) - case ETHERNET_TYPE_JL1101: - eth_type = "JL1101"; - break; -#endif -#ifdef USE_ETHERNET_KSZ8081 - case ETHERNET_TYPE_KSZ8081: - eth_type = "KSZ8081"; - break; - - case ETHERNET_TYPE_KSZ8081RNA: - eth_type = "KSZ8081RNA"; - break; -#endif -#if CONFIG_ETH_SPI_ETHERNET_W5500 - case ETHERNET_TYPE_W5500: - eth_type = "W5500"; - break; -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - case ETHERNET_TYPE_DM9051: - eth_type = "DM9051"; - break; -#endif -#ifdef USE_ETHERNET_OPENETH - case ETHERNET_TYPE_OPENETH: - eth_type = "OPENETH"; - break; -#endif -#ifdef USE_ETHERNET_LAN8670 - case ETHERNET_TYPE_LAN8670: - eth_type = "LAN8670"; - break; -#endif - - default: - eth_type = "Unknown"; - break; - } - - ESP_LOGCONFIG(TAG, - "Ethernet:\n" - " Connected: %s", - YESNO(this->is_connected())); - this->dump_connect_params_(); -#ifdef USE_ETHERNET_SPI - ESP_LOGCONFIG(TAG, - " CLK Pin: %u\n" - " MISO Pin: %u\n" - " MOSI Pin: %u\n" - " CS Pin: %u", - this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - if (this->polling_interval_ != 0) { - ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_); - } else -#endif - { - ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_); - } - ESP_LOGCONFIG(TAG, - " Reset Pin: %d\n" - " Clock Speed: %d MHz", - this->reset_pin_, this->clock_speed_ / 1000000); -#else - if (this->power_pin_ != -1) { - ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); - } - ESP_LOGCONFIG(TAG, - " CLK Pin: %u\n" - " MDC Pin: %u\n" - " MDIO Pin: %u\n" - " PHY addr: %u", - this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); -#endif - ESP_LOGCONFIG(TAG, " Type: %s", eth_type); -} - float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -network::IPAddresses EthernetComponent::get_ip_addresses() { - network::IPAddresses addresses; - esp_netif_ip_info_t ip; - esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); - // TODO: do something smarter - // return false; - } else { - addresses[0] = network::IPAddress(&ip.ip); - } -#if USE_NETWORK_IPV6 - struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - uint8_t count = 0; - count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); - assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); - assert(count < addresses.size()); - for (int i = 0; i < count; i++) { - addresses[i + 1] = network::IPAddress(&if_ip6s[i]); - } -#endif /* USE_NETWORK_IPV6 */ +void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } - return addresses; -} - -network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { - LwIPLock lock; - const ip_addr_t *dns_ip = dns_getserver(num); - return dns_ip; -} - -void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { - const char *event_name; - - switch (event) { - case ETHERNET_EVENT_START: - event_name = "ETH started"; - global_eth_component->started_ = true; - global_eth_component->enable_loop_soon_any_context(); - break; - case ETHERNET_EVENT_STOP: - event_name = "ETH stopped"; - global_eth_component->started_ = false; - global_eth_component->connected_ = false; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes - break; - case ETHERNET_EVENT_CONNECTED: - event_name = "ETH connected"; - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP) - if (global_eth_component->manual_ip_.has_value()) { - global_eth_component->notify_ip_state_listeners_(); - } +#ifdef USE_ETHERNET_MANUAL_IP +void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif - break; - case ETHERNET_EVENT_DISCONNECTED: - event_name = "ETH disconnected"; - global_eth_component->connected_ = false; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes - break; - default: - return; - } - ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); -} +// set_use_address() is guaranteed to be called during component setup by Python code generation, +// so use_address_ will always be valid when get_use_address() is called - no fallback needed. +const char *EthernetComponent::get_use_address() const { return this->use_address_; } -void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, - void *event_data) { - ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; - const esp_netif_ip_info_t *ip_info = &event->ip_info; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); - global_eth_component->got_ipv4_address_ = true; -#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) - global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#else - global_eth_component->connected_ = true; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#endif /* USE_NETWORK_IPV6 */ -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - global_eth_component->notify_ip_state_listeners_(); -#endif -} - -#if USE_NETWORK_IPV6 -void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, - void *event_data) { - ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); - global_eth_component->ipv6_count_ += 1; -#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) - global_eth_component->connected_ = - global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#else - global_eth_component->connected_ = global_eth_component->got_ipv4_address_; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#endif -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - global_eth_component->notify_ip_state_listeners_(); -#endif -} -#endif /* USE_NETWORK_IPV6 */ +void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { @@ -554,315 +35,6 @@ void EthernetComponent::notify_ip_state_listeners_() { } #endif // USE_ETHERNET_IP_STATE_LISTENERS -void EthernetComponent::finish_connect_() { -#if USE_NETWORK_IPV6 - // Retry IPv6 link-local setup if it failed during initial connect - // This handles the case where min_ipv6_addr_count is NOT set (or is 0), - // allowing us to reach CONNECTED state with just IPv4. - // If IPv6 setup failed in start_connect_() because the interface wasn't ready: - // - Bootup timing issues (#10281) - // - Cable unplugged/network interruption (#10705) - // We can now retry since we're in CONNECTED state and the interface is definitely up. - if (!this->ipv6_setup_done_) { - esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_); - if (err == ESP_OK) { - ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)"); - } - // Always set the flag to prevent continuous retries - // If IPv6 setup fails here with the interface up and stable, it's - // likely a persistent issue (IPv6 disabled at router, hardware - // limitation, etc.) that won't be resolved by further retries. - // The device continues to work with IPv4. - this->ipv6_setup_done_ = true; - } -#endif /* USE_NETWORK_IPV6 */ -} - -void EthernetComponent::start_connect_() { - global_eth_component->got_ipv4_address_ = false; -#if USE_NETWORK_IPV6 - global_eth_component->ipv6_count_ = 0; - this->ipv6_setup_done_ = false; -#endif /* USE_NETWORK_IPV6 */ - this->connect_begin_ = millis(); - this->status_set_warning(LOG_STR("waiting for IP configuration")); - - esp_err_t err; - err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); - if (err != ERR_OK) { - ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); - } - - esp_netif_ip_info_t info; -#ifdef USE_ETHERNET_MANUAL_IP - if (this->manual_ip_.has_value()) { - info.ip = this->manual_ip_->static_ip; - info.gw = this->manual_ip_->gateway; - info.netmask = this->manual_ip_->subnet; - } else -#endif - { - info.ip.addr = 0; - info.gw.addr = 0; - info.netmask.addr = 0; - } - - esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT; - - err = esp_netif_dhcpc_get_status(this->eth_netif_, &status); - ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!"); - - ESP_LOGV(TAG, "DHCP Client Status: %d", status); - - err = esp_netif_dhcpc_stop(this->eth_netif_); - if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESPHL_ERROR_CHECK(err, "DHCPC stop error"); - } - - err = esp_netif_set_ip_info(this->eth_netif_, &info); - ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); - -#ifdef USE_ETHERNET_MANUAL_IP - if (this->manual_ip_.has_value()) { - LwIPLock lock; - if (this->manual_ip_->dns1.is_set()) { - ip_addr_t d; - d = this->manual_ip_->dns1; - dns_setserver(0, &d); - } - if (this->manual_ip_->dns2.is_set()) { - ip_addr_t d; - d = this->manual_ip_->dns2; - dns_setserver(1, &d); - } - } else -#endif - { - err = esp_netif_dhcpc_start(this->eth_netif_); - if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { - ESPHL_ERROR_CHECK(err, "DHCPC start error"); - } - } -#if USE_NETWORK_IPV6 - // Attempt to create IPv6 link-local address - // We MUST attempt this here, not just in finish_connect_(), because with - // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6. - // However, this may fail with ESP_FAIL if the interface is not up yet: - // - At bootup when link isn't ready (#10281) - // - After disconnection/cable unplugged (#10705) - // We'll retry in finish_connect_() if it fails here. - err = esp_netif_create_ip6_linklocal(this->eth_netif_); - if (err != ESP_OK) { - if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) { - // This is a programming error, not a transient failure - ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters"); - } else { - // ESP_FAIL means the interface isn't up yet - // This is expected and non-fatal, happens in multiple scenarios: - // - During reconnection after network interruptions (#10705) - // - At bootup when the link isn't ready yet (#10281) - // We'll retry once we reach CONNECTED state and the interface is up - ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err)); - // Don't mark component as failed - this is a transient error - } - } -#endif /* USE_NETWORK_IPV6 */ - - this->connect_begin_ = millis(); - this->status_set_warning(); -} - -void EthernetComponent::dump_connect_params_() { - esp_netif_ip_info_t ip; - esp_netif_get_ip_info(this->eth_netif_, &ip); - const ip_addr_t *dns_ip1; - const ip_addr_t *dns_ip2; - { - LwIPLock lock; - dns_ip1 = dns_getserver(0); - dns_ip2 = dns_getserver(1); - } - - // Use stack buffers for IP address formatting to avoid heap allocations - char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - ESP_LOGCONFIG(TAG, - " IP Address: %s\n" - " Hostname: '%s'\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s\n" - " MAC Address: %s\n" - " Is Full Duplex: %s\n" - " Link Speed: %u", - network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), - network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), - network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf), - this->get_eth_mac_address_pretty_into_buffer(mac_buf), - YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); - -#if USE_NETWORK_IPV6 - struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - uint8_t count = 0; - count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); - assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); - for (int i = 0; i < count; i++) { - ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); - } -#endif /* USE_NETWORK_IPV6 */ -} - -#ifdef USE_ETHERNET_SPI -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } -void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT -void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } -#endif -#else -void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } -void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } -void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } -void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } -void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } -#endif -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } -#ifdef USE_ETHERNET_MANUAL_IP -void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } -#endif - -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *EthernetComponent::get_use_address() const { return this->use_address_; } - -void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - -void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { - esp_err_t err; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); -} - -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - -const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( - std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) { - uint8_t mac[6]; - get_eth_mac_address_raw(mac); - format_mac_addr_upper(mac, buf.data()); - return buf.data(); -} - -eth_duplex_t EthernetComponent::get_duplex_mode() { - esp_err_t err; - eth_duplex_t duplex_mode; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); - ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); - return duplex_mode; -} - -eth_speed_t EthernetComponent::get_link_speed() { - esp_err_t err; - eth_speed_t speed; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); - ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); - return speed; -} - -bool EthernetComponent::powerdown() { - ESP_LOGI(TAG, "Powering down ethernet PHY"); - if (this->phy_ == nullptr) { - ESP_LOGE(TAG, "Ethernet PHY not assigned"); - return false; - } - this->connected_ = false; - this->started_ = false; - // No need to enable_loop() here as this is only called during shutdown/reboot - if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { - ESP_LOGE(TAG, "Error powering down ethernet PHY"); - return false; - } - return true; -} - -#ifndef USE_ETHERNET_SPI - -#ifdef USE_ETHERNET_KSZ8081 -constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; - -void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { - esp_err_t err; - - uint32_t phy_control_2; - err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); - ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; -#endif - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); - - /* - * Bit 7 is `RMII Reference Clock Select`. Default is `0`. - * KSZ8081RNA: - * 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode. - * 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. - * KSZ8081RND: - * 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. - * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode. - */ - if ((phy_control_2 & (1 << 7)) != (1 << 7)) { - phy_control_2 |= 1 << 7; - err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); - ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); - err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); - ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", - format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); - } -} -#endif // USE_ETHERNET_KSZ8081 - -void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { - esp_err_t err; - -#ifdef USE_ETHERNET_RTL8201 - constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; - if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { - ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); - err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); - ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); - } -#endif - - ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); - err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); - ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); - -#ifdef USE_ETHERNET_RTL8201 - if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { - ESP_LOGD(TAG, "Select PHY Register Page 0x00"); - err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); - ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); - } -#endif -} - -#endif - } // namespace esphome::ethernet -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f7a0996fb74..80038d50ecb 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -7,8 +7,9 @@ #include "esphome/core/automation.h" #include "esphome/components/network/ip_address.h" -#ifdef USE_ESP32 +#ifdef USE_ETHERNET +#ifdef USE_ESP32 #include "esp_eth.h" #include "esp_eth_mac.h" #include "esp_eth_mac_esp.h" @@ -19,6 +20,7 @@ #if CONFIG_ETH_USE_ESP32_EMAC extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif +#endif // USE_ESP32 namespace esphome::ethernet { @@ -73,6 +75,12 @@ enum class EthernetComponentState : uint8_t { CONNECTED, }; +// Platform-neutral duplex/speed types +#ifndef USE_ESP32 +enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL }; +enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M }; +#endif + class EthernetComponent : public Component { public: EthernetComponent(); @@ -83,6 +91,28 @@ class EthernetComponent : public Component { void on_powerdown() override { powerdown(); } bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } + void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP + void set_manual_ip(const ManualIP &manual_ip); +#endif + void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; } + + network::IPAddresses get_ip_addresses(); + network::IPAddress get_dns_address(uint8_t num); + const char *get_use_address() const; + void set_use_address(const char *use_address); + void get_eth_mac_address_raw(uint8_t *mac); + // Remove before 2026.9.0 + ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") + std::string get_eth_mac_address_pretty(); + const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf); + eth_duplex_t get_duplex_mode(); + eth_speed_t get_link_speed(); + bool powerdown(); + +#ifdef USE_ESP32 + esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } + #ifdef USE_ETHERNET_SPI void set_clk_pin(uint8_t clk_pin); void set_miso_pin(uint8_t miso_pin); @@ -102,26 +132,8 @@ class EthernetComponent : public Component { void set_clk_pin(uint8_t clk_pin); void set_clk_mode(emac_rmii_clock_mode_t clk_mode); void add_phy_register(PHYRegister register_value); -#endif - void set_type(EthernetType type); -#ifdef USE_ETHERNET_MANUAL_IP - void set_manual_ip(const ManualIP &manual_ip); -#endif - void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; } - - network::IPAddresses get_ip_addresses(); - network::IPAddress get_dns_address(uint8_t num); - const char *get_use_address() const; - void set_use_address(const char *use_address); - void get_eth_mac_address_raw(uint8_t *mac); - // Remove before 2026.9.0 - ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") - std::string get_eth_mac_address_pretty(); - const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf); - eth_duplex_t get_duplex_mode(); - eth_speed_t get_link_speed(); - esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } - bool powerdown(); +#endif // USE_ETHERNET_SPI +#endif // USE_ESP32 #ifdef USE_ETHERNET_IP_STATE_LISTENERS void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } @@ -133,19 +145,22 @@ class EthernetComponent : public Component { #ifdef USE_ETHERNET_DISCONNECT_TRIGGER Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } #endif + protected: + void start_connect_(); + void finish_connect_(); + void dump_connect_params_(); + +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + void notify_ip_state_listeners_(); +#endif + +#ifdef USE_ESP32 static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #if LWIP_IPV6 static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif /* LWIP_IPV6 */ -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - void notify_ip_state_listeners_(); -#endif - - void start_connect_(); - void finish_connect_(); - void dump_connect_params_(); void log_error_and_mark_failed_(esp_err_t err, const char *message); #ifdef USE_ETHERNET_KSZ8081 /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. @@ -177,7 +192,15 @@ class EthernetComponent : public Component { uint8_t phy_addr_{0}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; -#endif +#endif // USE_ETHERNET_SPI + + // ESP32 pointers + esp_netif_t *eth_netif_{nullptr}; + esp_eth_handle_t eth_handle_; + esp_eth_phy_t *phy_{nullptr}; +#endif // USE_ESP32 + + // Common members #ifdef USE_ETHERNET_MANUAL_IP optional<ManualIP> manual_ip_{}; #endif @@ -194,10 +217,6 @@ class EthernetComponent : public Component { bool ipv6_setup_done_{false}; #endif /* LWIP_IPV6 */ - // Pointers at the end (naturally aligned) - esp_netif_t *eth_netif_{nullptr}; - esp_eth_handle_t eth_handle_; - esp_eth_phy_t *phy_{nullptr}; optional<std::array<uint8_t, 6>> fixed_mac_; #ifdef USE_ETHERNET_IP_STATE_LISTENERS @@ -219,10 +238,12 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; +#ifdef USE_ESP32 #if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif +#endif // USE_ESP32 } // namespace esphome::ethernet -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp new file mode 100644 index 00000000000..ac8680f3e13 --- /dev/null +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -0,0 +1,841 @@ +#include "ethernet_component.h" + +#if defined(USE_ETHERNET) && defined(USE_ESP32) + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include <lwip/dns.h> +#include <cinttypes> +#include "esp_event.h" + +#ifdef USE_ETHERNET_LAN8670 +#include "esp_eth_phy_lan867x.h" +#endif + +#ifdef USE_ETHERNET_SPI +#include <driver/gpio.h> +#include <driver/spi_master.h> +#endif + +namespace esphome::ethernet { + +static const char *const TAG = "ethernet"; + +// PHY register size for hex logging +static constexpr size_t PHY_REG_SIZE = 2; + +void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { + ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); + this->mark_failed(); +} + +#define ESPHL_ERROR_CHECK(err, message) \ + if ((err) != ESP_OK) { \ + this->log_error_and_mark_failed_(err, message); \ + return; \ + } + +#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ + if ((err) != ESP_OK) { \ + this->log_error_and_mark_failed_(err, message); \ + return ret; \ + } + +void EthernetComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting failed; reconnecting"); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection lost; reconnecting"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else { + this->finish_connect_(); + // When connected and stable, disable the loop to save CPU cycles + this->disable_loop(); + } + break; + } +} + +void EthernetComponent::setup() { + if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { + // Delay here to allow power to stabilise before Ethernet is initialized. + delay(300); // NOLINT + } + + esp_err_t err; + +#ifdef USE_ETHERNET_SPI + // Install GPIO ISR handler to be able to service SPI Eth modules interrupts + gpio_install_isr_service(0); + + spi_bus_config_t buscfg = { + .mosi_io_num = this->mosi_pin_, + .miso_io_num = this->miso_pin_, + .sclk_io_num = this->clk_pin_, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = 0, + .flags = 0, + .intr_flags = 0, + }; + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + auto host = SPI2_HOST; +#else + auto host = SPI3_HOST; +#endif + + err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); + ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); +#endif + + err = esp_netif_init(); + ESPHL_ERROR_CHECK(err, "ETH netif init error"); + err = esp_event_loop_create_default(); + ESPHL_ERROR_CHECK(err, "ETH event loop error"); + + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + this->eth_netif_ = esp_netif_new(&cfg); + + // Init MAC and PHY configs to default + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + +#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module + spi_device_interface_config_t devcfg = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = this->clock_speed_, + .input_delay_ns = 0, + .spics_io_num = this->cs_pin_, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + +#if CONFIG_ETH_SPI_ETHERNET_W5500 + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#endif + +#if CONFIG_ETH_SPI_ETHERNET_W5500 + w5500_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + w5500_config.poll_period_ms = this->polling_interval_; +#endif +#endif + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + dm9051_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + dm9051_config.poll_period_ms = this->polling_interval_; +#endif +#endif + + phy_config.phy_addr = this->phy_addr_spi_; + phy_config.reset_gpio_num = this->reset_pin_; + + esp_eth_mac_t *mac = nullptr; +#elif defined(USE_ETHERNET_OPENETH) + esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); +#else + phy_config.phy_addr = this->phy_addr_; + phy_config.reset_gpio_num = this->power_pin_; + + eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; + esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; +#else + esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; + esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; +#endif + esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; + esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#endif + + switch (this->type_) { +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: { + phy_config.autonego_timeout_ms = 1000; + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } +#endif +#if CONFIG_ETH_USE_ESP32_EMAC +#ifdef USE_ETHERNET_LAN8720 + case ETHERNET_TYPE_LAN8720: { + this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_RTL8201 + case ETHERNET_TYPE_RTL8201: { + this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_DP83848 + case ETHERNET_TYPE_DP83848: { + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_IP101 + case ETHERNET_TYPE_IP101: { + this->phy_ = esp_eth_phy_new_ip101(&phy_config); + break; + } +#endif +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) + case ETHERNET_TYPE_JL1101: { + this->phy_ = esp_eth_phy_new_jl1101(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_KSZ8081 + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { + this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: { + this->phy_ = esp_eth_phy_new_lan867x(&phy_config); + break; + } +#endif +#endif +#ifdef USE_ETHERNET_SPI +#if CONFIG_ETH_SPI_ETHERNET_W5500 + case ETHERNET_TYPE_W5500: { + mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + this->phy_ = esp_eth_phy_new_w5500(&phy_config); + break; + } +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + case ETHERNET_TYPE_DM9051: { + mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + this->phy_ = esp_eth_phy_new_dm9051(&phy_config); + break; + } +#endif +#endif + default: { + this->mark_failed(); + return; + } + } + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); + this->eth_handle_ = nullptr; + err = esp_eth_driver_install(ð_config, &this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH driver install error"); + +#ifndef USE_ETHERNET_SPI +#ifdef USE_ETHERNET_KSZ8081 + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } +#endif // USE_ETHERNET_KSZ8081 + + for (const auto &phy_register : this->phy_registers_) { + this->write_phy_register_(mac, phy_register); + } +#endif + + // use ESP internal eth mac + uint8_t mac_addr[6]; + if (this->fixed_mac_.has_value()) { + memcpy(mac_addr, this->fixed_mac_->data(), 6); + } else { + esp_read_mac(mac_addr, ESP_MAC_ETH); + } + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); + ESPHL_ERROR_CHECK(err, "set mac address error"); + + /* attach Ethernet driver to TCP/IP stack */ + err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); + ESPHL_ERROR_CHECK(err, "ETH netif attach error"); + + // Register user defined event handers + err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "ETH event handler register error"); + err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); +#if USE_NETWORK_IPV6 + err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); +#endif /* USE_NETWORK_IPV6 */ + + /* start Ethernet driver state machine */ + err = esp_eth_start(this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH start error"); +} + +void EthernetComponent::dump_config() { + const char *eth_type; + switch (this->type_) { +#ifdef USE_ETHERNET_LAN8720 + case ETHERNET_TYPE_LAN8720: + eth_type = "LAN8720"; + break; +#endif +#ifdef USE_ETHERNET_RTL8201 + case ETHERNET_TYPE_RTL8201: + eth_type = "RTL8201"; + break; +#endif +#ifdef USE_ETHERNET_DP83848 + case ETHERNET_TYPE_DP83848: + eth_type = "DP83848"; + break; +#endif +#ifdef USE_ETHERNET_IP101 + case ETHERNET_TYPE_IP101: + eth_type = "IP101"; + break; +#endif +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) + case ETHERNET_TYPE_JL1101: + eth_type = "JL1101"; + break; +#endif +#ifdef USE_ETHERNET_KSZ8081 + case ETHERNET_TYPE_KSZ8081: + eth_type = "KSZ8081"; + break; + + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 + case ETHERNET_TYPE_W5500: + eth_type = "W5500"; + break; +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + case ETHERNET_TYPE_DM9051: + eth_type = "DM9051"; + break; +#endif +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; +#endif +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: + eth_type = "LAN8670"; + break; +#endif + + default: + eth_type = "Unknown"; + break; + } + + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); + this->dump_connect_params_(); +#ifdef USE_ETHERNET_SPI + ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" + " MISO Pin: %u\n" + " MOSI Pin: %u\n" + " CS Pin: %u", + this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + if (this->polling_interval_ != 0) { + ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_); + } else +#endif + { + ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_); + } + ESP_LOGCONFIG(TAG, + " Reset Pin: %d\n" + " Clock Speed: %d MHz", + this->reset_pin_, this->clock_speed_ / 1000000); +#else + if (this->power_pin_ != -1) { + ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); + } + ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" + " MDC Pin: %u\n" + " MDIO Pin: %u\n" + " PHY addr: %u", + this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); +#endif + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); +} + +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + + return addresses; +} + +network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + LwIPLock lock; + const ip_addr_t *dns_ip = dns_getserver(num); + return dns_ip; +} + +void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { + const char *event_name; + + switch (event) { + case ETHERNET_EVENT_START: + event_name = "ETH started"; + global_eth_component->started_ = true; + global_eth_component->enable_loop_soon_any_context(); + break; + case ETHERNET_EVENT_STOP: + event_name = "ETH stopped"; + global_eth_component->started_ = false; + global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes + break; + case ETHERNET_EVENT_CONNECTED: + event_name = "ETH connected"; + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP) + if (global_eth_component->manual_ip_.has_value()) { + global_eth_component->notify_ip_state_listeners_(); + } +#endif + break; + case ETHERNET_EVENT_DISCONNECTED: + event_name = "ETH disconnected"; + global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes + break; + default: + return; + } + + ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); +} + +void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + const esp_netif_ip_info_t *ip_info = &event->ip_info; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); + global_eth_component->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#else + global_eth_component->connected_ = true; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#endif /* USE_NETWORK_IPV6 */ +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + global_eth_component->notify_ip_state_listeners_(); +#endif +} + +#if USE_NETWORK_IPV6 +void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); + global_eth_component->ipv6_count_ += 1; +#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = + global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#else + global_eth_component->connected_ = global_eth_component->got_ipv4_address_; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#endif +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + global_eth_component->notify_ip_state_listeners_(); +#endif +} +#endif /* USE_NETWORK_IPV6 */ + +void EthernetComponent::finish_connect_() { +#if USE_NETWORK_IPV6 + // Retry IPv6 link-local setup if it failed during initial connect + // This handles the case where min_ipv6_addr_count is NOT set (or is 0), + // allowing us to reach CONNECTED state with just IPv4. + // If IPv6 setup failed in start_connect_() because the interface wasn't ready: + // - Bootup timing issues (#10281) + // - Cable unplugged/network interruption (#10705) + // We can now retry since we're in CONNECTED state and the interface is definitely up. + if (!this->ipv6_setup_done_) { + esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err == ESP_OK) { + ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)"); + } + // Always set the flag to prevent continuous retries + // If IPv6 setup fails here with the interface up and stable, it's + // likely a persistent issue (IPv6 disabled at router, hardware + // limitation, etc.) that won't be resolved by further retries. + // The device continues to work with IPv4. + this->ipv6_setup_done_ = true; + } +#endif /* USE_NETWORK_IPV6 */ +} + +void EthernetComponent::start_connect_() { + global_eth_component->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + global_eth_component->ipv6_count_ = 0; + this->ipv6_setup_done_ = false; +#endif /* USE_NETWORK_IPV6 */ + this->connect_begin_ = millis(); + this->status_set_warning(LOG_STR("waiting for IP configuration")); + + esp_err_t err; + err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); + } + + esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + info.ip = this->manual_ip_->static_ip; + info.gw = this->manual_ip_->gateway; + info.netmask = this->manual_ip_->subnet; + } else +#endif + { + info.ip.addr = 0; + info.gw.addr = 0; + info.netmask.addr = 0; + } + + esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT; + + err = esp_netif_dhcpc_get_status(this->eth_netif_, &status); + ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!"); + + ESP_LOGV(TAG, "DHCP Client Status: %d", status); + + err = esp_netif_dhcpc_stop(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + } + + err = esp_netif_set_ip_info(this->eth_netif_, &info); + ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); + +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + LwIPLock lock; + if (this->manual_ip_->dns1.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns1; + dns_setserver(0, &d); + } + if (this->manual_ip_->dns2.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns2; + dns_setserver(1, &d); + } + } else +#endif + { + err = esp_netif_dhcpc_start(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { + ESPHL_ERROR_CHECK(err, "DHCPC start error"); + } + } +#if USE_NETWORK_IPV6 + // Attempt to create IPv6 link-local address + // We MUST attempt this here, not just in finish_connect_(), because with + // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6. + // However, this may fail with ESP_FAIL if the interface is not up yet: + // - At bootup when link isn't ready (#10281) + // - After disconnection/cable unplugged (#10705) + // We'll retry in finish_connect_() if it fails here. + err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err != ESP_OK) { + if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) { + // This is a programming error, not a transient failure + ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters"); + } else { + // ESP_FAIL means the interface isn't up yet + // This is expected and non-fatal, happens in multiple scenarios: + // - During reconnection after network interruptions (#10705) + // - At bootup when the link isn't ready yet (#10281) + // We'll retry once we reach CONNECTED state and the interface is up + ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err)); + // Don't mark component as failed - this is a transient error + } + } +#endif /* USE_NETWORK_IPV6 */ + + this->connect_begin_ = millis(); + this->status_set_warning(); +} + +void EthernetComponent::dump_connect_params_() { + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(this->eth_netif_, &ip); + const ip_addr_t *dns_ip1; + const ip_addr_t *dns_ip2; + { + LwIPLock lock; + dns_ip1 = dns_getserver(0); + dns_ip2 = dns_getserver(1); + } + + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, + " IP Address: %s\n" + " Hostname: '%s'\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s\n" + " MAC Address: %s\n" + " Is Full Duplex: %s\n" + " Link Speed: %u", + network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); + +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); + } +#endif /* USE_NETWORK_IPV6 */ +} + +#ifdef USE_ETHERNET_SPI +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } +void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT +void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } +#endif +#else +void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } +void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } +void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } +void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } +void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } +#endif + +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + esp_err_t err; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) { + uint8_t mac[6]; + get_eth_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + esp_err_t err; + eth_duplex_t duplex_mode; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); + return duplex_mode; +} + +eth_speed_t EthernetComponent::get_link_speed() { + esp_err_t err; + eth_speed_t speed; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); + return speed; +} + +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet PHY"); + if (this->phy_ == nullptr) { + ESP_LOGE(TAG, "Ethernet PHY not assigned"); + return false; + } + this->connected_ = false; + this->started_ = false; + // No need to enable_loop() here as this is only called during shutdown/reboot + if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { + ESP_LOGE(TAG, "Error powering down ethernet PHY"); + return false; + } + return true; +} + +#ifndef USE_ETHERNET_SPI + +#ifdef USE_ETHERNET_KSZ8081 +constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; + +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { + esp_err_t err; + + uint32_t phy_control_2; + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; +#endif + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", + format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); + } +} +#endif // USE_ETHERNET_KSZ8081 + +void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { + esp_err_t err; + +#ifdef USE_ETHERNET_RTL8201 + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); + ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); + } +#endif + + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); + err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); + ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); + +#ifdef USE_ETHERNET_RTL8201 + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page 0x00"); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); + ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); + } +#endif +} + +#endif + +} // namespace esphome::ethernet + +#endif // USE_ETHERNET && USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c index 963db3ff1c4..49fbe825c84 100644 --- a/esphome/components/ethernet/ethernet_helpers.c +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -1,3 +1,5 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP32 #include "esp_eth_mac_esp.h" // ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers @@ -8,3 +10,4 @@ eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); } #endif +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index 72ce9c86e22..15ef6a1f205 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -1,7 +1,7 @@ #include "ethernet_info_text_sensor.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 +#ifdef USE_ETHERNET namespace esphome::ethernet_info { @@ -49,4 +49,4 @@ void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo M } // namespace esphome::ethernet_info -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 912a39a83f3..11002d51bad 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/ethernet/ethernet_component.h" -#ifdef USE_ESP32 +#ifdef USE_ETHERNET namespace esphome::ethernet_info { @@ -50,4 +50,4 @@ class MACAddressEthernetInfo final : public Component, public text_sensor::TextS } // namespace esphome::ethernet_info -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 073170aafbf..75e63b1462b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -278,6 +278,12 @@ #define USE_ETHERNET_JL1101 #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_LAN8670 +#define USE_ETHERNET_SPI +#define USE_ETHERNET_SPI_POLLING_SUPPORT +#define USE_ETHERNET_OPENETH +#define CONFIG_ETH_SPI_ETHERNET_W5500 1 +#define CONFIG_ETH_SPI_ETHERNET_DM9051 1 +#define CONFIG_ETH_USE_ESP32_EMAC 1 #define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_IP_STATE_LISTENERS #define USE_ETHERNET_CONNECT_TRIGGER From ccb467b219409fc8d5632f81af531c423437fba7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:14:41 -1000 Subject: [PATCH 0691/2056] [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) --- esphome/components/fastled_base/__init__.py | 5 +++++ tests/components/fastled_clockless/test.esp32-s3-ard.yaml | 1 + 2 files changed, 6 insertions(+) create mode 100644 tests/components/fastled_clockless/test.esp32-s3-ard.yaml diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 11e8423258c..c944e8a930c 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_RGB_ORDER, ) +from esphome.core import CORE CODEOWNERS = ["@OttoWinter"] fastled_base_ns = cg.esphome_ns.namespace("fastled_base") @@ -41,5 +42,9 @@ async def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) cg.add_library("fastled/FastLED", "3.9.16") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") await light.register_light(var, config) return var diff --git a/tests/components/fastled_clockless/test.esp32-s3-ard.yaml b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 9948adc6a004c2961f7c578f5fd329a030784bb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:15:01 -1000 Subject: [PATCH 0692/2056] [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/runtime_image/__init__.py | 8 ++++++++ esphome/idf_component.yml | 2 ++ .../online_image/test.esp32-s3-ard.yaml | 19 +++++++++++++++++++ .../online_image/test.esp32-s3-idf.yaml | 19 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 tests/components/online_image/test.esp32-s3-ard.yaml create mode 100644 tests/components/online_image/test.esp32-s3-idf.yaml diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 7c22bfc9d19..3ae35cc5f17 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -11,6 +11,7 @@ from esphome.components.image import ( ) import esphome.config_validation as cv from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE +from esphome.core import CORE AUTO_LOAD = ["image"] CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] @@ -75,6 +76,13 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") + if CORE.is_esp32: + from esphome.components.esp32 import add_idf_component + + # JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level + # ARDUINO_ESP32S3_DEV define) that require esp-dsp headers. + # On Arduino this overwrites the stub; on IDF it adds the component. + add_idf_component(name="espressif/esp-dsp", ref="1.7.1") class PNGFormat(Format): diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1e2d452919c..59876e8b3da 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -5,6 +5,8 @@ dependencies: version: 2.0.3 esphome/micro-opus: version: 0.3.5 + espressif/esp-dsp: + version: "1.7.1" espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/online_image/test.esp32-s3-ard.yaml b/tests/components/online_image/test.esp32-s3-ard.yaml new file mode 100644 index 00000000000..9116fd86e09 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-ard.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-ard.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/test.esp32-s3-idf.yaml b/tests/components/online_image/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..f219f71ee25 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-idf.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); From c09edb94c1984add3e28fcbe56003800d588ae15 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 16 Mar 2026 00:04:07 -0500 Subject: [PATCH 0693/2056] [tinyusb] Fix regression from bump to 2.x in #14796 (#14848) --- .../components/tinyusb/tinyusb_component.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index 7f8fea52647..2ec696c3e43 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -2,6 +2,7 @@ #include "tinyusb_component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "tinyusb_default_config.h" namespace esphome::tinyusb { @@ -15,19 +16,19 @@ void TinyUSB::setup() { this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf; } - this->tusb_cfg_ = { - .port = TINYUSB_PORT_FULL_SPEED_0, - .phy = {.skip_setup = false}, - .descriptor = - { - .device = &this->usb_descriptor_, - .string = this->string_descriptor_, - .string_count = SIZE, - }, + // Start from esp_tinyusb defaults to keep required task settings valid across esp_tinyusb updates. + this->tusb_cfg_ = TINYUSB_DEFAULT_CONFIG(); + this->tusb_cfg_.port = TINYUSB_PORT_FULL_SPEED_0; + this->tusb_cfg_.phy.skip_setup = false; + this->tusb_cfg_.descriptor = { + .device = &this->usb_descriptor_, + .string = this->string_descriptor_, + .string_count = SIZE, }; esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); if (result != ESP_OK) { + ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result)); this->mark_failed(); } } From 1183ef825b6d1fbb316b1df150d846bac0c09aeb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:09:55 -0400 Subject: [PATCH 0694/2056] [usb_host] Fix ESP-IDF 6.0 compatibility for external USB host component (#14844) --- esphome/components/usb_host/__init__.py | 5 +++++ esphome/idf_component.yml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index e4c11be4894..5eb0371e5c5 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -4,7 +4,9 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_component, add_idf_sdkconfig_option, + idf_version, only_on_variant, ) import esphome.config_validation as cv @@ -64,6 +66,9 @@ async def register_usb_client(config): async def to_code(config: ConfigType) -> None: + # IDF 6.0 moved USB host to an external component + if idf_version() >= cv.Version(6, 0, 0): + add_idf_component(name="espressif/usb", ref="1.3.0") add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) if config.get(CONF_ENABLE_HUBS): add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 59876e8b3da..c949c3b0266 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -43,5 +43,9 @@ dependencies: version: "1.0.0" rules: - if: "idf_version >=6.0.0" + espressif/usb: + version: "1.3.0" + rules: + - if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]" esp32async/asynctcp: version: 3.4.91 From e1252e32d146d547c54edfd54d69e4b91c65f5ff Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:10:30 -0400 Subject: [PATCH 0695/2056] [deep_sleep] Fix ESP-IDF 6.0 GPIO wakeup API rename (#14846) --- esphome/components/deep_sleep/deep_sleep_esp32.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 79c34f627a9..4f4d262d30d 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -3,6 +3,7 @@ #include "driver/gpio.h" #include "deep_sleep_component.h" #include "esphome/core/log.h" +#include <esp_idf_version.h> namespace esphome { namespace deep_sleep { @@ -26,7 +27,7 @@ namespace deep_sleep { // - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup) // - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup) // - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup) -// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup) +// - GPIO wakeup: GPIO wakeup for RTC pins static const char *const TAG = "deep_sleep"; @@ -135,8 +136,13 @@ void DeepSleepComponent::deep_sleep_() { } // Internal pullup/pulldown resistors are enabled automatically, when // ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is) - esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(), + static_cast<esp_sleep_gpio_wake_up_mode_t>(level)); +#else + esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(), static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level)); +#endif } #endif From 2ee0df1da3d5dc2e025cbe1ae5b352c523bc61d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:43:38 +0000 Subject: [PATCH 0696/2056] Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849) 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 e634bcb1046..da95dd5a13d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.1 +aioesphomeapi==44.5.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 414182fe6d9ca2173631843338c690c9e304ea5d Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 16 Mar 2026 08:08:05 +0100 Subject: [PATCH 0697/2056] [ble_nus] fix uart debug (#14850) --- esphome/components/ble_nus/ble_nus.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d0d37dbf1cb..2f60f814718 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { // First, use the peek buffer if available if (this->has_peek_) { +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif data[0] = this->peek_buffer_; this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... -#ifdef USE_UART_DEBUGGER - this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); -#endif - return true; // No more to read + return true; // No more to read } } From f86bb2bdb045e97392caa4ef6ce35048bd3d4d93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 02:02:59 -1000 Subject: [PATCH 0698/2056] [ethernet] Add IDF 6.0 registry component dependencies (#14847) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 69 ++++++++++++++++++- .../components/ethernet/esp_eth_phy_jl1101.c | 3 +- .../components/ethernet/ethernet_component.h | 3 +- .../ethernet/ethernet_component_esp32.cpp | 59 +++++++++++++--- esphome/core/defines.h | 2 + esphome/idf_component.yml | 28 ++++++++ 6 files changed, 149 insertions(+), 15 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 83bef4d91cb..e1ceefeacd6 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import logging from esphome import automation, pins @@ -35,6 +36,7 @@ from esphome.const import ( CONF_VALUE, KEY_CORE, KEY_FRAMEWORK_VERSION, + KEY_NATIVE_IDF, Platform, PlatformFramework, ) @@ -53,6 +55,9 @@ LOGGER = logging.getLogger(__name__) # Key for tracking IP state listener count in CORE.data ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners" +# Key for tracking configured ethernet type +ETHERNET_TYPE_KEY = "ethernet_type" +KEY_ETHERNET = "ethernet" def request_ethernet_ip_state_listener() -> None: @@ -126,9 +131,32 @@ _PHY_TYPE_TO_DEFINE = { "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "W5500": "USE_ETHERNET_W5500", + "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", } + +@dataclass(frozen=True) +class IDFRegistryComponent: + """An ESP-IDF component from the Espressif Component Registry.""" + + name: str + version: str + + +# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry. +_IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { + "LAN8720": IDFRegistryComponent("espressif/lan87xx", "1.0.0"), + "RTL8201": IDFRegistryComponent("espressif/rtl8201", "1.0.1"), + "DP83848": IDFRegistryComponent("espressif/dp83848", "1.0.0"), + "IP101": IDFRegistryComponent("espressif/ip101", "1.0.0"), + "KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), + "KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), + "W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"), + "DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"), +} + SPI_ETHERNET_TYPES = ["W5500", "DM9051"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) @@ -406,6 +434,7 @@ async def to_code(config): cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE] if CONF_MANUAL_IP in config: cg.add_define("USE_ETHERNET_MANUAL_IP") @@ -439,6 +468,7 @@ async def _to_code_esp32(var, config): from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, + idf_version, include_builtin_idf_component, ) @@ -459,7 +489,11 @@ async def _to_code_esp32(var, config): cg.add_define("USE_ETHERNET_SPI") add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True) + # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 + if idf_version() < cv.Version(6, 0, 0): + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) @@ -491,6 +525,12 @@ async def _to_code_esp32(var, config): # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") + # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry + if idf_version() >= cv.Version(6, 0, 0) and ( + component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE]) + ): + add_idf_component(name=component.name, ref=component.version) + def _final_validate_rmii_pins(config: ConfigType) -> None: """Validate that RMII pins are not used by other components.""" @@ -565,11 +605,36 @@ async def final_step(): cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count) -FILTER_SOURCE_FILES = filter_source_files_from_platform( +_platform_filter = filter_source_files_from_platform( { "ethernet_component_esp32.cpp": { PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, }, + "esp_eth_phy_jl1101.c": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, } ) + + +def _filter_source_files() -> list[str]: + excluded = _platform_filter() + eth_data = CORE.data.get(KEY_ETHERNET, {}) + eth_type = eth_data.get(ETHERNET_TYPE_KEY) + # Only compile the custom JL1101 driver when JL1101 is configured + # and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x) + if eth_type != "JL1101": + excluded.append("esp_eth_phy_jl1101.c") + elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False): + from esphome.components.esp32 import idf_version + + # pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver + # to avoid shadowing. Native IDF builds always need the custom driver. + if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0): + excluded.append("esp_eth_phy_jl1101.c") + return excluded + + +FILTER_SOURCE_FILES = _filter_source_files diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index b81d8227d45..46671a2dd06 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -29,7 +29,8 @@ #include "esp_rom_sys.h" #include "esp_idf_version.h" -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \ + ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 80038d50ecb..cc73c01df4b 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -239,7 +239,8 @@ class EthernetComponent : public Component { extern EthernetComponent *global_eth_component; #ifdef USE_ESP32 -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \ + ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif #endif // USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index ac8680f3e13..fb69a901aac 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -10,6 +10,36 @@ #include <cinttypes> #include "esp_event.h" +// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry; +// they are no longer included via esp_eth.h and need explicit includes. +// On IDF 5.x these headers don't exist as standalone files. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#ifdef USE_ETHERNET_LAN8720 +#include "esp_eth_phy_lan87xx.h" +#endif +#ifdef USE_ETHERNET_RTL8201 +#include "esp_eth_phy_rtl8201.h" +#endif +#ifdef USE_ETHERNET_DP83848 +#include "esp_eth_phy_dp83848.h" +#endif +#ifdef USE_ETHERNET_IP101 +#include "esp_eth_phy_ip101.h" +#endif +#ifdef USE_ETHERNET_KSZ8081 +#include "esp_eth_phy_ksz80xx.h" +#endif +#ifdef USE_ETHERNET_W5500 +#include "esp_eth_mac_w5500.h" +#include "esp_eth_phy_w5500.h" +#endif +#ifdef USE_ETHERNET_DM9051 +#include "esp_eth_mac_dm9051.h" +#include "esp_eth_phy_dm9051.h" +#endif +#endif // ESP_IDF_VERSION >= 6.0.0 + +// LAN867x header exists on all IDF versions (external component since IDF 5.3) #ifdef USE_ETHERNET_LAN8670 #include "esp_eth_phy_lan867x.h" #endif @@ -164,21 +194,21 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#if CONFIG_ETH_SPI_ETHERNET_W5500 +#ifdef USE_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); #endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 +#ifdef USE_ETHERNET_DM9051 eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); #endif -#if CONFIG_ETH_SPI_ETHERNET_W5500 +#ifdef USE_ETHERNET_W5500 w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif #endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 +#ifdef USE_ETHERNET_DM9051 dm9051_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT dm9051_config.poll_period_ms = this->polling_interval_; @@ -204,7 +234,8 @@ void EthernetComponent::setup() { esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; #endif esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; - esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; + esp32_emac_config.clock_config.rmii.clock_gpio = + static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_); esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); #endif @@ -213,7 +244,11 @@ void EthernetComponent::setup() { #ifdef USE_ETHERNET_OPENETH case ETHERNET_TYPE_OPENETH: { phy_config.autonego_timeout_ms = 1000; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + this->phy_ = esp_eth_phy_new_generic(&phy_config); +#else this->phy_ = esp_eth_phy_new_dp83848(&phy_config); +#endif break; } #endif @@ -242,8 +277,10 @@ void EthernetComponent::setup() { break; } #endif -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: { + // PlatformIO (pioarduino): builtin esp_eth_phy_new_jl1101() on all IDF versions + // Non-PlatformIO: custom ESPHome driver (esp_eth_phy_jl1101.c) this->phy_ = esp_eth_phy_new_jl1101(&phy_config); break; } @@ -263,14 +300,14 @@ void EthernetComponent::setup() { #endif #endif #ifdef USE_ETHERNET_SPI -#if CONFIG_ETH_SPI_ETHERNET_W5500 +#ifdef USE_ETHERNET_W5500 case ETHERNET_TYPE_W5500: { mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } #endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 +#ifdef USE_ETHERNET_DM9051 case ETHERNET_TYPE_DM9051: { mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); this->phy_ = esp_eth_phy_new_dm9051(&phy_config); @@ -354,7 +391,7 @@ void EthernetComponent::dump_config() { eth_type = "IP101"; break; #endif -#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) +#ifdef USE_ETHERNET_JL1101 case ETHERNET_TYPE_JL1101: eth_type = "JL1101"; break; @@ -368,12 +405,12 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081RNA"; break; #endif -#if CONFIG_ETH_SPI_ETHERNET_W5500 +#ifdef USE_ETHERNET_W5500 case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; #endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 +#ifdef USE_ETHERNET_DM9051 case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 75e63b1462b..513c70e17ec 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -281,6 +281,8 @@ #define USE_ETHERNET_SPI #define USE_ETHERNET_SPI_POLLING_SUPPORT #define USE_ETHERNET_OPENETH +#define USE_ETHERNET_W5500 +#define USE_ETHERNET_DM9051 #define CONFIG_ETH_SPI_ETHERNET_W5500 1 #define CONFIG_ETH_SPI_ETHERNET_DM9051 1 #define CONFIG_ETH_USE_ESP32_EMAC 1 diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c949c3b0266..1478d9544e6 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -31,6 +31,34 @@ dependencies: version: "2.0.0" rules: - if: "target in [esp32, esp32p4]" + espressif/lan87xx: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0 && target in [esp32, esp32p4]" + espressif/rtl8201: + version: "1.0.1" + rules: + - if: "idf_version >=6.0.0 && target in [esp32, esp32p4]" + espressif/dp83848: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0 && target in [esp32, esp32p4]" + espressif/ip101: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0 && target in [esp32, esp32p4]" + espressif/ksz80xx: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0 && target in [esp32, esp32p4]" + espressif/w5500: + version: "1.0.1" + rules: + - if: "idf_version >=6.0.0" + espressif/dm9051: + version: "1.0.0" + rules: + - if: "idf_version >=6.0.0" espressif/esp_tinyusb: version: "2.1.1" rules: From 2cd93daa5e57eb22c64ba98d67f38818cf4b7957 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 07:32:58 -1000 Subject: [PATCH 0699/2056] [api] Optimize plaintext varint encoding and devirtualize write_protobuf_packet (#14758) --- esphome/components/api/api_frame_helper.h | 12 +++++++++--- .../components/api/api_frame_helper_noise.cpp | 8 -------- esphome/components/api/api_frame_helper_noise.h | 1 - .../api/api_frame_helper_plaintext.cpp | 17 +++++++---------- .../components/api/api_frame_helper_plaintext.h | 1 - esphome/components/api/proto.h | 14 ++++++++++---- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index b2561f2b328..e78c71507c8 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -12,6 +12,7 @@ #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "proto.h" namespace esphome::api { @@ -37,8 +38,6 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; -class ProtoWriteBuffer; - // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; @@ -161,7 +160,14 @@ class APIFrameHelper { this->nodelay_counter_ = 0; } } - virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + // Resize buffer to include footer space if needed (e.g. Noise MAC) + if (frame_footer_size_) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + MessageInfo msg{type, 0, + static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); + } // Write multiple protobuf messages in a single operation // messages contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index f945253c89d..b635d84f168 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -450,14 +450,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - // Resize to include MAC space (required for Noise encryption) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - MessageInfo msg{type, 0, - static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); -} - APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { APIError aerr = this->check_data_state_(); if (aerr != APIError::OK) diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 83410febb26..a6b17ff3b92 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; protected: diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 007da7ef2b1..e97b558fa39 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -235,11 +235,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } -APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); -} - APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { APIError aerr = this->check_data_state_(); @@ -257,9 +252,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe uint16_t total_write_len = 0; for (const auto &msg : messages) { - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type)); + // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead + uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE + ? 1 + : (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); + uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -281,8 +278,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) - // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [1-3] - Payload size varint (3 bytes, for sizes 16384-65535) + // [4-5] - Message type varint (2 bytes, for types 128-16383) // [6...] - Actual payload data // // The message starts at offset + frame_header_padding_ diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 96d47e9c7bf..f8161c039d3 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; protected: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 814a3f44560..6752dfb9cd0 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -473,6 +473,12 @@ class ProtoDecodableMessage : public ProtoMessage { class ProtoSize { public: + // Varint encoding thresholds: values below each threshold fit in N bytes + static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128 + static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384 + static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 + static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 + /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @@ -480,7 +486,7 @@ class ProtoSize { * @return The number of bytes needed to encode the value */ static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { - if (value < 128) [[likely]] + if (value < VARINT_THRESHOLD_1_BYTE) [[likely]] return 1; // Fast path: 7 bits, most common case if (__builtin_is_constant_evaluated()) return varint_wide(value); @@ -492,11 +498,11 @@ class ProtoSize { static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); // Shared cascade for values >= 128 (used by both constexpr and noinline paths) static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { - if (value < 16384) + if (value < VARINT_THRESHOLD_2_BYTE) return 2; - if (value < 2097152) + if (value < VARINT_THRESHOLD_3_BYTE) return 3; - if (value < 268435456) + if (value < VARINT_THRESHOLD_4_BYTE) return 4; return 5; } From 7b4af76a61a37aeeeac00f747adcab4d4b742c28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 07:33:10 -1000 Subject: [PATCH 0700/2056] [core] Inline Mutex on all embedded platforms (#14756) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/helpers.cpp | 6 ------ esphome/components/esp8266/helpers.cpp | 8 ++----- esphome/components/libretiny/helpers.cpp | 6 ------ esphome/components/rp2040/helpers.cpp | 7 +----- esphome/core/helpers.h | 27 ++++++++++++++++++------ 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 051b7ce1624..76f1c59c739 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -20,12 +20,6 @@ bool random_bytes(uint8_t *data, size_t len) { return true; } -Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } -Mutex::~Mutex() {} -void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } -bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } -void Mutex::unlock() { xSemaphoreGive(this->handle_); } - // only affects the executing core // so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 4a64ae181e3..aadfc311970 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -12,12 +12,8 @@ namespace esphome { uint32_t random_uint32() { return os_random(); } bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } -// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. -Mutex::Mutex() {} -Mutex::~Mutex() {} -void Mutex::lock() {} -bool Mutex::try_lock() { return true; } -void Mutex::unlock() {} +// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set, +// independent of the ESPHOME_THREAD_SINGLE thread model define. IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index 21913e4a16d..ffbd181c54c 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -15,12 +15,6 @@ bool random_bytes(uint8_t *data, size_t len) { return true; } -Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } -Mutex::~Mutex() {} -void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } -bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } -void Mutex::unlock() { xSemaphoreGive(this->handle_); } - // only affects the executing core // so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index 4191c2164ad..a69b8da4806 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -35,12 +35,7 @@ bool random_bytes(uint8_t *data, size_t len) { return true; } -// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. -Mutex::Mutex() {} -Mutex::~Mutex() {} -void Mutex::lock() {} -bool Mutex::try_lock() { return true; } -void Mutex::unlock() {} +// RP2040 Mutex is defined inline in helpers.h for RP2040/ESP8266 builds. IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c2f4cace9a9..d220626bcf3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1866,19 +1866,34 @@ template<typename T> class Parented { */ class Mutex { public: - Mutex(); Mutex(const Mutex &) = delete; + Mutex &operator=(const Mutex &) = delete; + +#if defined(USE_ESP8266) || defined(USE_RP2040) + // Single-threaded platforms: inline no-ops so the compiler eliminates all call overhead. + Mutex() = default; + ~Mutex() = default; + void lock() {} + bool try_lock() { return true; } + void unlock() {} +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) + // FreeRTOS platforms: inline to avoid out-of-line call overhead. + Mutex() { handle_ = xSemaphoreCreateMutex(); } + ~Mutex() = default; + void lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } + bool try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } + void unlock() { xSemaphoreGive(this->handle_); } + + private: + SemaphoreHandle_t handle_; +#else + Mutex(); ~Mutex(); void lock(); bool try_lock(); void unlock(); - Mutex &operator=(const Mutex &) = delete; - private: -#if defined(USE_ESP32) || defined(USE_LIBRETINY) - SemaphoreHandle_t handle_; -#else // d-pointer to store private data on new platforms void *handle_; // NOLINT(clang-diagnostic-unused-private-field) #endif From 7131eafc09b69eb8da0e1587435a90c4bda84b98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 07:35:04 -1000 Subject: [PATCH 0701/2056] [logger] Reduce per-message overhead by inlining hot path helpers (#14851) --- esphome/components/logger/log_buffer.h | 31 ++++++++++++------- esphome/components/logger/logger.h | 18 ++++++++++- esphome/components/logger/logger_esp32.cpp | 17 ---------- esphome/components/logger/logger_esp32.h | 28 +++++++++++++++++ esphome/components/logger/logger_esp8266.cpp | 5 --- esphome/components/logger/logger_esp8266.h | 13 ++++++++ .../components/logger/logger_libretiny.cpp | 2 -- esphome/components/logger/logger_libretiny.h | 13 ++++++++ esphome/components/logger/logger_rp2040.cpp | 5 --- esphome/components/logger/logger_rp2040.h | 13 ++++++++ 10 files changed, 103 insertions(+), 42 deletions(-) create mode 100644 esphome/components/logger/logger_esp32.h create mode 100644 esphome/components/logger/logger_esp8266.h create mode 100644 esphome/components/logger/logger_libretiny.h create mode 100644 esphome/components/logger/logger_rp2040.h diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index a56276f732f..734cb14dc58 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -111,7 +111,12 @@ struct LogBuffer { } #endif void write_body(const char *text, uint16_t text_length) { - this->write_(text, text_length); + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (text_length < available) ? text_length : available; + if (copy_len > 0) { + memcpy(this->current_(), text, copy_len); + this->pos += copy_len; + } this->finalize_(); } @@ -119,21 +124,23 @@ struct LogBuffer { bool full_() const { return this->pos >= this->size; } uint16_t remaining_() const { return this->size - this->pos; } char *current_() { return this->data + this->pos; } - void write_(const char *value, uint16_t length) { - const uint16_t available = this->remaining_(); - const uint16_t copy_len = (length < available) ? length : available; - if (copy_len > 0) { - memcpy(this->current_(), value, copy_len); - this->pos += copy_len; - } - } void finalize_() { - // Write color reset sequence - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + this->write_ansi_reset_(); // Null terminate this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; } + // Write ANSI reset sequence inline ("\033[0m") - avoids write_() call overhead + static constexpr uint16_t ANSI_RESET_LEN = 4; // "\033[0m" + void write_ansi_reset_() { + if (this->remaining_() >= ANSI_RESET_LEN) { + char *p = this->current_(); + *p++ = '\033'; + *p++ = '['; + *p++ = '0'; + *p++ = 'm'; + this->pos += ANSI_RESET_LEN; + } + } void strip_trailing_newlines_() { while (this->pos > 0 && this->data[this->pos - 1] == '\n') this->pos--; diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 263d12b4441..8c38cadcbc5 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -233,7 +233,11 @@ class Logger final : public Component { void cdc_loop_(); #endif void process_messages_(); +#if defined(USE_HOST) || defined(USE_ZEPHYR) void write_msg_(const char *msg, uint16_t len); +#else + inline void write_msg_(const char *msg, uint16_t len); // Defined in platform-specific logger_*.h +#endif // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on) @@ -366,7 +370,7 @@ class Logger final : public Component { bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny #endif #else - bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms + bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif // Large buffer placed last to keep frequently-accessed member offsets small @@ -498,3 +502,15 @@ class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const c }; } // namespace esphome::logger + +// Platform-specific inline implementations of write_msg_() +// Must be included after the Logger class definition is complete +#if defined(USE_ESP32) +#include "logger_esp32.h" +#elif defined(USE_ESP8266) +#include "logger_esp8266.h" +#elif defined(USE_RP2040) +#include "logger_rp2040.h" +#elif defined(USE_LIBRETINY) +#include "logger_libretiny.h" +#endif diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index f5bf7822899..8e0c00267a7 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -123,23 +123,6 @@ void Logger::pre_setup() { #endif } -void HOT Logger::write_msg_(const char *msg, uint16_t len) { -#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) - // USB CDC/JTAG - single write including newline (already in buffer) - // Use fwrite to stdout which goes through VFS to USB console - // - // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). - // They are ONLY defined when the user explicitly selects USB as the logger output in their config. - // This is compile-time selection, not runtime detection - if USB is configured, it's always used. - // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility - // to configure correctly for their hardware. This approach eliminates runtime overhead. - fwrite(msg, 1, len, stdout); -#else - // Regular UART - single write including newline (already in buffer) - uart_write_bytes(this->uart_num_, msg, len); -#endif -} - const LogString *Logger::get_uart_selection_() { switch (this->uart_) { case UART_SELECTION_UART0: diff --git a/esphome/components/logger/logger_esp32.h b/esphome/components/logger/logger_esp32.h new file mode 100644 index 00000000000..905111c7184 --- /dev/null +++ b/esphome/components/logger/logger_esp32.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef USE_ESP32 +#include "esphome/core/helpers.h" +#include <driver/uart.h> + +namespace esphome::logger { + +inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); +#else + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); +#endif +} + +} // namespace esphome::logger + +#endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 0a3433d1321..b9507e707a7 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -28,11 +28,6 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, uint16_t len) { - // Single write with newline already in buffer (added by caller) - this->hw_serial_->write(msg, len); -} - const LogString *Logger::get_uart_selection_() { #if defined(USE_ESP8266_LOGGER_SERIAL) if (this->uart_ == UART_SELECTION_UART0_SWAP) { diff --git a/esphome/components/logger/logger_esp8266.h b/esphome/components/logger/logger_esp8266.h new file mode 100644 index 00000000000..719c10f7bae --- /dev/null +++ b/esphome/components/logger/logger_esp8266.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_ESP8266 +#include "esphome/core/helpers.h" + +namespace esphome::logger { + +// Single write with newline already in buffer (added by caller) +inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } + +} // namespace esphome::logger + +#endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index aab8a97abff..bc3922c436a 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,8 +49,6 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } - const LogString *Logger::get_uart_selection_() { switch (this->uart_) { case UART_SELECTION_DEFAULT: diff --git a/esphome/components/logger/logger_libretiny.h b/esphome/components/logger/logger_libretiny.h new file mode 100644 index 00000000000..7b1f174ff96 --- /dev/null +++ b/esphome/components/logger/logger_libretiny.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_LIBRETINY +#include "esphome/core/helpers.h" + +namespace esphome::logger { + +// Single write with newline already in buffer (added by caller) +inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } + +} // namespace esphome::logger + +#endif diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index b7225c2a258..a0215ec9ec7 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -34,11 +34,6 @@ void Logger::pre_setup() { #endif } -void HOT Logger::write_msg_(const char *msg, uint16_t len) { - // Single write with newline already in buffer (added by caller) - this->hw_serial_->write(msg, len); -} - const LogString *Logger::get_uart_selection_() { switch (this->uart_) { case UART_SELECTION_UART0: diff --git a/esphome/components/logger/logger_rp2040.h b/esphome/components/logger/logger_rp2040.h new file mode 100644 index 00000000000..604d8b8ca6a --- /dev/null +++ b/esphome/components/logger/logger_rp2040.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_RP2040 +#include "esphome/core/helpers.h" + +namespace esphome::logger { + +// Single write with newline already in buffer (added by caller) +inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } + +} // namespace esphome::logger + +#endif From 808c7b67b3ad3c85b7241bd96134b4844fb2b502 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 07:35:19 -1000 Subject: [PATCH 0702/2056] [core] Inline WarnIfComponentBlockingGuard::finish() into header (#14798) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/core/component.cpp | 13 ++++--------- esphome/core/component.h | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 761f1bd4858..bfe9beb2723 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -510,7 +510,8 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } -static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time) { +void __attribute__((noinline, cold)) +WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { bool should_warn; if (component != nullptr) { should_warn = component->should_warn_of_blocking(blocking_time); @@ -524,10 +525,8 @@ static void __attribute__((noinline, cold)) warn_blocking(Component *component, } } -uint32_t WarnIfComponentBlockingGuard::finish() { - uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS +void WarnIfComponentBlockingGuard::record_runtime_stats_() { // Use micros() for accurate sub-millisecond timing. millis() has insufficient // resolution — most components complete in microseconds but millis() only has // 1ms granularity, so results were essentially random noise. @@ -535,12 +534,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t duration_us = micros() - this->started_us_; global_runtime_stats->record_component_time(this->component_, duration_us); } -#endif - if (blocking_time > WARN_IF_BLOCKING_OVER_MS) { - warn_blocking(this->component_, blocking_time); - } - return curr_time; } +#endif #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 1aac1c72191..5fdf23e128a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -6,6 +6,7 @@ #include <string> #include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -575,9 +576,7 @@ class PollingComponent : public Component { uint32_t update_interval_; }; -#ifdef USE_RUNTIME_STATS -uint32_t micros(); // Forward declare for inline constructor -#endif +// millis() and micros() are available via hal.h class WarnIfComponentBlockingGuard { public: @@ -592,7 +591,18 @@ class WarnIfComponentBlockingGuard { } // Finish the timing operation and return the current time - uint32_t finish(); + // Inlined: the fast path is just millis() + subtract + compare + inline uint32_t HOT finish() { + uint32_t curr_time = millis(); + uint32_t blocking_time = curr_time - this->started_; +#ifdef USE_RUNTIME_STATS + this->record_runtime_stats_(); +#endif + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { + warn_blocking(this->component_, blocking_time); + } + return curr_time; + } ~WarnIfComponentBlockingGuard() = default; @@ -601,7 +611,12 @@ class WarnIfComponentBlockingGuard { Component *component_; #ifdef USE_RUNTIME_STATS uint32_t started_us_; + void record_runtime_stats_(); #endif + + private: + // Cold path for blocking warning - defined in component.cpp + static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time); }; // Function to clear setup priority overrides after all components are set up From db405c483e10daa21adf57c2444ba4ab48775416 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 07:35:34 -1000 Subject: [PATCH 0703/2056] [core] Cache errno to avoid duplicate __errno() calls (#14751) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/api/api_frame_helper.cpp | 10 +++--- .../components/async_tcp/async_tcp_socket.cpp | 32 +++++++++++-------- .../captive_portal/dns_server_esp32_idf.cpp | 5 +-- .../components/esphome/ota/ota_esphome.cpp | 15 +++++---- .../web_server_idf/web_server_idf.cpp | 5 +-- esphome/core/application.cpp | 10 ++++-- 6 files changed, 47 insertions(+), 30 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e432a976b0d..fbee2940226 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -113,10 +113,11 @@ APIError APIFrameHelper::loop() { // Common socket write error handling APIError APIFrameHelper::handle_socket_write_error_() { - if (errno == EWOULDBLOCK || errno == EAGAIN) { + const int err = errno; + if (err == EWOULDBLOCK || err == EAGAIN) { return APIError::WOULD_BLOCK; } - HELPER_LOG("Socket write failed with errno %d", errno); + HELPER_LOG("Socket write failed with errno %d", err); this->state_ = State::FAILED; return APIError::SOCKET_WRITE_FAILED; } @@ -278,11 +279,12 @@ APIError APIFrameHelper::init_common_() { APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { + const int err = errno; + if (err == EWOULDBLOCK || err == EAGAIN) { return APIError::WOULD_BLOCK; } state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); + HELPER_LOG("Socket read failed with errno %d", err); return APIError::SOCKET_READ_FAILED; } else if (received == 0) { state_ = State::FAILED; diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index f64e494f5f6..e8c0f163b39 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) { connect_cb_(connect_arg_, this); return true; } - if (errno != EINPROGRESS) { - ESP_LOGE(TAG, "Connect failed: %d", errno); + const int saved_errno = errno; + if (saved_errno != EINPROGRESS) { + ESP_LOGE(TAG, "Connect failed: %d", saved_errno); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, saved_errno); return false; } @@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) { ssize_t sent = socket_->write(data, len); if (sent < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { - ESP_LOGE(TAG, "Write error: %d", errno); + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK) { + ESP_LOGE(TAG, "Write error: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, err); } return 0; } @@ -129,10 +131,11 @@ void AsyncClient::loop() { error_cb_(error_arg_, this, error); } } else if (ret < 0) { - ESP_LOGE(TAG, "Select error: %d", errno); + const int err = errno; + ESP_LOGE(TAG, "Select error: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, err); } } else if (connected_) { // For connected sockets, use the Application's select() results @@ -148,11 +151,14 @@ void AsyncClient::loop() { } else if (len > 0) { if (data_cb_) data_cb_(data_arg_, this, buf, len); - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - ESP_LOGW(TAG, "Read error: %d", errno); - close(); - if (error_cb_) - error_cb_(error_arg_, this, errno); + } else { + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK) { + ESP_LOGW(TAG, "Read error: %d", err); + close(); + if (error_cb_) + error_cb_(error_arg_, this, err); + } } } } diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 7b75f042419..56ad9f7176b 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -100,8 +100,9 @@ void DNSServer::process_next_request() { &client_addr_len); if (len < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { - ESP_LOGE(TAG, "recvfrom failed: %d", errno); + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) { + ESP_LOGE(TAG, "recvfrom failed: %d", err); } return; } diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index d8dbe2dee2d..972d2b2b8d5 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -332,12 +332,13 @@ void ESPHomeOTAComponent::handle_data_() { size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; ssize_t read = this->client_->read(buf, requested); if (read == -1) { - if (this->would_block_(errno)) { + const int err = errno; + if (this->would_block_(err)) { // read() already waited up to SO_RCVTIMEO for data, just feed WDT App.feed_wdt(); continue; } - ESP_LOGW(TAG, "Read err %d", errno); + ESP_LOGW(TAG, "Read err %d", err); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } else if (read == 0) { ESP_LOGW(TAG, "Remote closed"); @@ -426,8 +427,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { - if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno); + const int err = errno; + if (!this->would_block_(err)) { + ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err); return false; } } else if (read == 0) { @@ -455,8 +457,9 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { - if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); + const int err = errno; + if (!this->would_block_(err)) { + ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err); return false; } // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f18570965b7..60816fc6dd2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -74,12 +74,13 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT); if (ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + const int err = errno; + if (err == EAGAIN || err == EWOULDBLOCK) { // Buffer full - retry later return HTTPD_SOCK_ERR_TIMEOUT; } // Real error - ESP_LOGD(TAG, "send error: errno %d", errno); + ESP_LOGD(TAG, "send error: errno %d", err); return HTTPD_SOCK_ERR_FAIL; } return ret; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8685bff360e..3a9e825e041 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -698,18 +698,22 @@ void Application::yield_with_select_(uint32_t delay_ms) { #endif // Process select() result: - // ret < 0: error (except EINTR which is normal) // ret > 0: socket(s) have data ready - normal and expected // ret == 0: timeout occurred - normal and expected - if (ret >= 0 || errno == EINTR) [[likely]] { + if (ret >= 0) [[likely]] { // Yield if zero timeout since select(0) only polls without yielding if (delay_ms == 0) [[unlikely]] { yield(); } return; } + // ret < 0: error (EINTR is normal, anything else is unexpected) + const int err = errno; + if (err == EINTR) { + return; + } // select() error - log and fall through to delay() - ESP_LOGW(TAG, "select() failed with errno %d", errno); + ESP_LOGW(TAG, "select() failed with errno %d", err); } // No sockets registered or select() failed - use regular delay delay(delay_ms); From b14255797941f285fcb6df301b96275a4c601257 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 16 Mar 2026 08:26:06 -1000 Subject: [PATCH 0704/2056] [ethernet] Add RP2040 W5500 Ethernet support (#14820) --- esphome/components/ethernet/__init__.py | 35 +- .../components/ethernet/ethernet_component.h | 25 ++ .../ethernet/ethernet_component_rp2040.cpp | 315 ++++++++++++++++++ esphome/components/rp2040/helpers.cpp | 23 +- .../components/socket/lwip_raw_tcp_impl.cpp | 3 +- esphome/core/defines.h | 6 + .../ethernet/common-w5500-rp2040.yaml | 18 + .../ethernet/test-w5500.rp2040-ard.yaml | 1 + 8 files changed, 415 insertions(+), 11 deletions(-) create mode 100644 esphome/components/ethernet/ethernet_component_rp2040.cpp create mode 100644 tests/components/ethernet/common-w5500-rp2040.yaml create mode 100644 tests/components/ethernet/test-w5500.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e1ceefeacd6..f519d79aa19 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -158,6 +158,8 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { } SPI_ETHERNET_TYPES = ["W5500", "DM9051"] +# RP2040-supported SPI ethernet types +RP2040_SPI_ETHERNET_TYPES = ["W5500"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -273,6 +275,11 @@ def _validate(config): f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " f"on ESP32 classic and ESP32-P4, not {variant}" ) + elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: + raise cv.Invalid( + f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, " + f"not {config[CONF_TYPE]}" + ) return config @@ -330,18 +337,21 @@ SPI_SCHEMA = cv.All( cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( - cv.frequency, cv.int_range(int(8e6), int(80e6)) + cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All( + cv.only_on_esp32, + cv.frequency, + cv.int_range(int(8e6), int(80e6)), ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.only_on_esp32, cv.positive_time_period_milliseconds, cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), ), } ), ), - cv.only_on([Platform.ESP32]), + cv.only_on([Platform.ESP32, Platform.RP2040]), ) CONFIG_SCHEMA = cv.All( @@ -431,6 +441,8 @@ async def to_code(config): if CORE.is_esp32: await _to_code_esp32(var, config) + elif CORE.is_rp2040: + await _to_code_rp2040(var, config) cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) @@ -464,7 +476,7 @@ async def to_code(config): CORE.add_job(final_step) -async def _to_code_esp32(var, config): +async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, @@ -532,6 +544,20 @@ async def _to_code_esp32(var, config): add_idf_component(name=component.name, ref=component.version) +async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: + cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) + cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) + cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) + cg.add(var.set_cs_pin(config[CONF_CS_PIN])) + if CONF_INTERRUPT_PIN in config: + cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN])) + if CONF_RESET_PIN in config: + cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) + + cg.add_define("USE_ETHERNET_SPI") + cg.add_library("lwIP_w5500", None) + + def _final_validate_rmii_pins(config: ConfigType) -> None: """Validate that RMII pins are not used by other components.""" if not CORE.is_esp32: @@ -611,6 +637,7 @@ _platform_filter = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, }, + "ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, "esp_eth_phy_jl1101.c": { PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO, diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index cc73c01df4b..901d9bc0bb4 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -22,6 +22,10 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif #endif // USE_ESP32 +#ifdef USE_RP2040 +#include <W5500lwIP.h> +#endif + namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS @@ -135,6 +139,15 @@ class EthernetComponent : public Component { #endif // USE_ETHERNET_SPI #endif // USE_ESP32 +#ifdef USE_RP2040 + void set_clk_pin(uint8_t clk_pin); + void set_miso_pin(uint8_t miso_pin); + void set_mosi_pin(uint8_t mosi_pin); + void set_cs_pin(uint8_t cs_pin); + void set_interrupt_pin(int8_t interrupt_pin); + void set_reset_pin(int8_t reset_pin); +#endif // USE_RP2040 + #ifdef USE_ETHERNET_IP_STATE_LISTENERS void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } #endif @@ -200,6 +213,18 @@ class EthernetComponent : public Component { esp_eth_phy_t *phy_{nullptr}; #endif // USE_ESP32 +#ifdef USE_RP2040 + static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls + Wiznet5500lwIP *eth_{nullptr}; + uint32_t last_link_check_{0}; + uint8_t clk_pin_; + uint8_t miso_pin_; + uint8_t mosi_pin_; + uint8_t cs_pin_; + int8_t interrupt_pin_{-1}; + int8_t reset_pin_{-1}; +#endif // USE_RP2040 + // Common members #ifdef USE_ETHERNET_MANUAL_IP optional<ManualIP> manual_ip_{}; diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp new file mode 100644 index 00000000000..77b1a22d66c --- /dev/null +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -0,0 +1,315 @@ +#include "ethernet_component.h" + +#if defined(USE_ETHERNET) && defined(USE_RP2040) + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "esphome/components/rp2040/gpio.h" + +#include <SPI.h> +#include <lwip/dns.h> +#include <lwip/netif.h> + +namespace esphome::ethernet { + +static const char *const TAG = "ethernet"; + +void EthernetComponent::setup() { + // Configure SPI pins + SPI.setRX(this->miso_pin_); + SPI.setTX(this->mosi_pin_); + SPI.setSCK(this->clk_pin_); + + // Toggle reset pin if configured + if (this->reset_pin_ >= 0) { + rp2040::RP2040GPIOPin reset_pin; + reset_pin.set_pin(this->reset_pin_); + reset_pin.set_flags(gpio::FLAG_OUTPUT); + reset_pin.setup(); + reset_pin.digital_write(false); + delay(1); // NOLINT + reset_pin.digital_write(true); + delay(10); // NOLINT - wait for W5500 to initialize after reset + } + + // Create the W5500 device instance + this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT + + // Set hostname before begin() so the LWIP netif gets it + this->eth_->hostname(App.get_name().c_str()); + + // Configure static IP if set (must be done before begin()) +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + IPAddress ip(this->manual_ip_->static_ip); + IPAddress gateway(this->manual_ip_->gateway); + IPAddress subnet(this->manual_ip_->subnet); + IPAddress dns1(this->manual_ip_->dns1); + IPAddress dns2(this->manual_ip_->dns2); + this->eth_->config(ip, gateway, subnet, dns1, dns2); + } +#endif + + // Begin with fixed MAC or auto-generated + bool success; + if (this->fixed_mac_.has_value()) { + success = this->eth_->begin(this->fixed_mac_->data()); + } else { + success = this->eth_->begin(); + } + + if (!success) { + ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet"); + delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory) + this->eth_ = nullptr; + this->mark_failed(); + return; + } + + // Make this the default interface for routing + this->eth_->setDefault(true); + + // The arduino-pico LwipIntfDev automatically handles packet processing + // via __addEthernetPacketHandler when no interrupt pin is used, + // or via GPIO interrupt when one is provided. + + // Don't set started_ here — let the link polling in loop() set it + // when the W5500 link is actually up. Setting it prematurely causes + // a "Starting → Stopped → Starting" log sequence because the W5500 + // needs time after begin() before the PHY link is ready. +} + +void EthernetComponent::loop() { + // On RP2040, we need to poll connection state since there are no events. + const uint32_t now = App.get_loop_component_start_time(); + + // Throttle link/IP polling to avoid excessive SPI transactions from linkStatus() + // which reads the W5500 PHY register via SPI on every call. + // connected() reads netif->ip_addr without LwIPLock, but this is a single + // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale + // value, which is benign for polling. + if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) { + this->last_link_check_ = now; + bool link_up = this->eth_->linkStatus() == LinkON; + bool has_ip = this->eth_->connected(); + + if (!link_up) { + if (this->started_) { + this->started_ = false; + this->connected_ = false; + } + } else { + if (!this->started_) { + this->started_ = true; + } + bool was_connected = this->connected_; + this->connected_ = has_ip; + if (this->connected_ && !was_connected) { +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + this->notify_ip_state_listeners_(); +#endif + } + } + } + + // State machine + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting failed; reconnecting"); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection lost; reconnecting"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else { + this->finish_connect_(); + } + break; + } +} + +void EthernetComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Type: W5500\n" + " Connected: %s\n" + " CLK Pin: %u\n" + " MISO Pin: %u\n" + " MOSI Pin: %u\n" + " CS Pin: %u\n" + " IRQ Pin: %d\n" + " Reset Pin: %d", + YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, + this->interrupt_pin_, this->reset_pin_); + this->dump_connect_params_(); +} + +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; + if (this->eth_ != nullptr) { + LwIPLock lock; + addresses[0] = network::IPAddress(this->eth_->localIP()); + } + return addresses; +} + +network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + LwIPLock lock; + const ip_addr_t *dns_ip = dns_getserver(num); + return dns_ip; +} + +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + if (this->eth_ != nullptr) { + this->eth_->macAddress(mac); + } else { + memset(mac, 0, 6); + } +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) { + uint8_t mac[6]; + get_eth_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + // W5500 is always full duplex + return ETH_DUPLEX_FULL; +} + +eth_speed_t EthernetComponent::get_link_speed() { + // W5500 is always 100Mbps + return ETH_SPEED_100M; +} + +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet"); + if (this->eth_ != nullptr) { + this->eth_->end(); + } + this->connected_ = false; + this->started_ = false; + return true; +} + +void EthernetComponent::start_connect_() { + this->got_ipv4_address_ = false; + this->connect_begin_ = millis(); + this->status_set_warning(LOG_STR("waiting for IP configuration")); + + // Hostname is already set in setup() via LwipIntf::setHostname() + +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + // Static IP was already configured before begin() in setup() + // Set DNS servers + LwIPLock lock; + if (this->manual_ip_->dns1.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns1; + dns_setserver(0, &d); + } + if (this->manual_ip_->dns2.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns2; + dns_setserver(1, &d); + } + } +#endif +} + +void EthernetComponent::finish_connect_() { + // No additional work needed on RP2040 for now + // IPv6 link-local could be added here in the future +} + +void EthernetComponent::dump_connect_params_() { + if (this->eth_ == nullptr) { + return; + } + + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + + // Copy all lwIP state under the lock to avoid races with IRQ callbacks + ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr; + { + LwIPLock lock; + auto *netif = this->eth_->getNetIf(); + ip_addr = netif->ip_addr; + netmask = netif->netmask; + gw = netif->gw; + dns1_addr = *dns_getserver(0); + dns2_addr = *dns_getserver(1); + } + ESP_LOGCONFIG(TAG, + " IP Address: %s\n" + " Hostname: '%s'\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s\n" + " MAC Address: %s", + network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf), + network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf)); +} + +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } + +} // namespace esphome::ethernet + +#endif // USE_ETHERNET && USE_RP2040 diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index a69b8da4806..ad69192af97 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -8,6 +8,8 @@ #if defined(USE_WIFI) #include <WiFi.h> #include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) +#elif defined(USE_ETHERNET) +#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock) #endif #include <hardware/structs/rosc.h> #include <hardware/sync.h> @@ -40,18 +42,27 @@ bool random_bytes(uint8_t *data, size_t len) { IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } -// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. -// This means lwip callbacks run from a low-priority user IRQ context, not the +// On RP2040, lwip callbacks run from a low-priority user IRQ context, not the // main loop (see low_priority_irq_handler() in pico-sdk -// async_context_threadsafe_background.c). cyw43_arch_lwip_begin/end acquires the -// async_context recursive mutex to prevent IRQ callbacks from firing during -// critical sections. See esphome#10681. +// async_context_threadsafe_background.c). This applies to both WiFi (CYW43) and +// Ethernet (W5500) — both use async_context_threadsafe_background. // -// When CYW43 is not available (non-WiFi RP2040 boards), this is a no-op since +// Without locking, recv_fn() from IRQ context races with read_locked_() on the +// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat +// assertion failures). See esphome#10681. +// +// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end. +// Both acquire the async_context recursive mutex to prevent IRQ callbacks from +// firing during critical sections. +// +// When neither WiFi nor Ethernet is configured, this is a no-op since // there's no network stack and no lwip callbacks to race with. #if defined(USE_WIFI) LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); } LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); } +#elif defined(USE_ETHERNET) +LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); } +LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); } #else LwIPLock::LwIPLock() {} LwIPLock::~LwIPLock() {} diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 96328e68c73..3bcbd880850 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -130,7 +130,8 @@ void socket_wake() { // code (CONT context) — they never preempt each other, so no locking is needed. // // esphome::LwIPLock is the platform-provided RAII guard (see helpers.h/helpers.cpp). -// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op. +// On RP2040, it acquires cyw43_arch_lwip_begin/end (WiFi) or ethernet_arch_lwip_begin/end +// (Ethernet). On ESP8266, it's a no-op. #define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT static const char *const TAG = "socket.lwip"; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 513c70e17ec..390ac8ddd7f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -353,6 +353,12 @@ #define USE_SOCKET_IMPL_LWIP_TCP #define USE_RP2040_BLE #define USE_SPI +#ifndef USE_ETHERNET +#define USE_ETHERNET +#endif +#ifndef USE_ETHERNET_SPI +#define USE_ETHERNET_SPI +#endif #endif #ifdef USE_LIBRETINY diff --git a/tests/components/ethernet/common-w5500-rp2040.yaml b/tests/components/ethernet/common-w5500-rp2040.yaml new file mode 100644 index 00000000000..78b2b952fc0 --- /dev/null +++ b/tests/components/ethernet/common-w5500-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W5500 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-w5500.rp2040-ard.yaml b/tests/components/ethernet/test-w5500.rp2040-ard.yaml new file mode 100644 index 00000000000..7953198b7e3 --- /dev/null +++ b/tests/components/ethernet/test-w5500.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w5500-rp2040.yaml From cdf2867bafd8464d1147c66c5b5ef109b519aafb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:05:56 -0400 Subject: [PATCH 0705/2056] [hub75] Bump esp-hub75 to 0.3.4 (#14862) --- esphome/components/hub75/display.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 4f62ce7a94a..4cca0cea5da 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -587,7 +587,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.3.2", + ref="0.3.4", ) # Set compile-time configuration via build flags (so external library sees them) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1478d9544e6..a847e34b022 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -64,7 +64,7 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.3.2 + version: 0.3.4 rules: - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" espressif/mqtt: From 05590a3a216dcfa56669926ed6e0d04c134bb10d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:39:26 -0400 Subject: [PATCH 0706/2056] [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) --- esphome/components/dallas_temp/dallas_temp.cpp | 3 +++ esphome/components/gpio/one_wire/gpio_one_wire.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 13f2fa59bd7..f119e28e78e 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() { float DallasTemperatureSensor::get_temp_c_() { int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0]; if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + if (this->scratch_pad_[7] == 0) { + return NAN; + } return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; } switch (this->resolution_) { diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 4191c45de15..4e2a306fc94 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() { uint64_t IRAM_ATTR GPIOOneWireBus::read64() { InterruptLock lock; uint64_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i < 64; i++) { ret |= (uint64_t(this->read_bit_()) << i); } return ret; From c8f708c13ce39f012a049872fd32aadaff6215df Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:40:24 -0400 Subject: [PATCH 0707/2056] [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) --- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b29e4c21540..ee6c2ee4718 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() { this->x_raw_max_ = this->display_->get_native_width(); } if (this->y_raw_max_ == this->y_raw_min_) { - this->x_raw_max_ = this->display_->get_native_height(); + this->y_raw_max_ = this->display_->get_native_height(); } } } @@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() { } point = buffer[5] & 0xF; + if (point > 2) { + ESP_LOGW(TAG, "Invalid touch point count: %d", point); + point = 2; + } if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); From 9362d9745ef22b96b8da73fb246ee05665c81739 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:41:21 -0400 Subject: [PATCH 0708/2056] [ci] Fix clang-tidy hash check 403 error on fork PRs (#14860) --- .github/workflows/ci-clang-tidy-hash.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 5054a622077..7905739b15e 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -40,7 +40,7 @@ jobs: echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY - - if: failure() + - if: failure() && github.event.pull_request.head.repo.full_name == github.repository name: Request changes uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: @@ -53,7 +53,7 @@ jobs: body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.' }) - - if: success() + - if: success() && github.event.pull_request.head.repo.full_name == github.repository name: Dismiss review uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: From 0bbba7575714e2b2b2509e821d555fcd7b3fb768 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:42:13 -0400 Subject: [PATCH 0709/2056] [am43] Fix battery update throttle using wrong type (#14864) --- esphome/components/am43/sensor/am43_sensor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 91973d8e33f..195b96a19eb 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent uint8_t current_sensor_; // The AM43 often gets into a state where it spams loads of battery update // notifications. Here we will limit to no more than every 10s. - uint8_t last_battery_update_; + uint32_t last_battery_update_; }; } // namespace am43 From 2f86e48a836f02eec8858d139cb2c1dee7fb4b4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:44:55 -0400 Subject: [PATCH 0710/2056] [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) --- esphome/components/as3935/as3935.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index 5dff1cb0aef..5f46dadfa8c 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -41,7 +41,7 @@ enum AS3935RegisterMasks { INT_MASK = 0xF0, THRESH_MASK = 0x0F, R_SPIKE_MASK = 0xF0, - ENERGY_MASK = 0xF0, + ENERGY_MASK = 0xE0, CAP_MASK = 0xF0, LIGHT_MASK = 0xCF, DISTURB_MASK = 0xDF, From c47f4fbc1c41948fc08c87c0f0412a83e0027f3c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:45:16 -0400 Subject: [PATCH 0711/2056] [core] Support both dot and dash separators in Version.parse (#14858) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1eac53e9b20..32689dab27e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -314,7 +314,7 @@ class Version: @classmethod def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) From 037f75e0ff453f66758df15ce7b1679ed3490679 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:57:17 -1000 Subject: [PATCH 0712/2056] Bump github/codeql-action from 4.32.6 to 4.33.0 (#14869) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1a0c54da6d6..2ef1a5af314 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: category: "/language:${{matrix.language}}" From 5ee3e94ca1dfcb2cca8b75c565b585a3d5a27da8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:57:33 -1000 Subject: [PATCH 0713/2056] Bump actions/create-github-app-token from 2.2.1 to 3.0.0 (#14868) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 6376cf877e5..3b5e9f0d158 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -27,7 +27,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ed41d99c7b..4aa63f6a168 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,7 +221,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -256,7 +256,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -287,7 +287,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From 80730fd012ee039acd821551c748ada8b9c01f37 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:57:53 -0400 Subject: [PATCH 0714/2056] [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) --- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 263603704ac..c9fe3a2e6ef 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { this->sg_recv_data_state_ = FRAME_DATA_LEN_H; break; case FRAME_DATA_LEN_H: - if (value <= 4) { - this->sg_data_len_ = value * 256; + if (value == 0) { this->sg_frame_buf_[4] = value; this->sg_recv_data_state_ = FRAME_DATA_LEN_L; } else { - this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value); } break; case FRAME_DATA_LEN_L: - this->sg_data_len_ += value; - if (this->sg_data_len_ > 32) { + this->sg_data_len_ = value; + if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) { ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value); this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; @@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { } break; case FRAME_DATA_BYTES: - this->sg_data_len_ -= 1; this->sg_frame_buf_[this->sg_frame_len_++] = value; - if (this->sg_data_len_ <= 0) { + if (--this->sg_data_len_ == 0) { this->sg_recv_data_state_ = FRAME_DATA_CRC; } break; From 8577c263583cacbb77c531608c8b045a7c269f69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:03:09 -0400 Subject: [PATCH 0715/2056] [i2c] Handle ESP_ERR_INVALID_RESPONSE as NACK for IDF 6.0 (#14867) --- esphome/components/i2c/i2c_bus_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index eaefabf75be..4aca4f0fae1 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100); - if (err == ESP_ERR_INVALID_STATE) { + if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) { ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; } else if (err == ESP_ERR_TIMEOUT) { From c3327d0b435f5e2a736a23ca03ce5149e157e319 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:04:20 -0400 Subject: [PATCH 0716/2056] [i2s_audio] Fix ESP-IDF 6.0 compatibility for I2S port types (#14818) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/i2s_audio/__init__.py | 70 +++++++++++++++++++ esphome/components/i2s_audio/i2s_audio.cpp | 27 ------- esphome/components/i2s_audio/i2s_audio.h | 13 ++-- .../i2s_audio/microphone/__init__.py | 4 +- .../microphone/i2s_audio_microphone.cpp | 21 ------ 5 files changed, 80 insertions(+), 55 deletions(-) delete mode 100644 esphome/components/i2s_audio/i2s_audio.cpp diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 65b09b93f6c..977a239497a 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass, field + from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( @@ -26,6 +28,9 @@ CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] MULTI_CONF = True +CONF_PDM = "pdm" +CONF_ADC_TYPE = "adc_type" + CONF_I2S_DOUT_PIN = "i2s_dout_pin" CONF_I2S_DIN_PIN = "i2s_din_pin" CONF_I2S_MCLK_PIN = "i2s_mclk_pin" @@ -254,7 +259,65 @@ CONFIG_SCHEMA = cv.All( ) +@dataclass +class I2SAudioData: + """I2S audio component state stored in CORE.data.""" + + port_map: dict[str, int] = field(default_factory=dict) + + +def _get_data() -> I2SAudioData: + if CONF_I2S_AUDIO not in CORE.data: + CORE.data[CONF_I2S_AUDIO] = I2SAudioData() + return CORE.data[CONF_I2S_AUDIO] + + +def _assign_ports() -> None: + """Assign I2S port numbers, prioritizing instances with microphone children. + + Microphones (especially PDM) require port 0 on most ESP32 variants. + This runs once and stores the mapping in CORE.data. + """ + data = _get_data() + if data.port_map: + return + + full_config = fv.full_config.get() + i2s_configs = full_config[CONF_I2S_AUDIO] + + # Find i2s_audio instances with microphones that require port 0 + # (PDM and internal ADC only work on I2S port 0) + port0_parent_id = None + for mic_config in full_config.get("microphone", []): + if CONF_I2S_AUDIO_ID not in mic_config: + continue + if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal": + if port0_parent_id is not None: + raise cv.Invalid( + "Only one PDM/ADC microphone is supported (requires I2S port 0)" + ) + port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID]) + + # Assign ports: port 0 parent first (if any), rest get sequential + next_port = 0 + if port0_parent_id is not None: + data.port_map[port0_parent_id] = next_port + next_port += 1 + for config in i2s_configs: + config_id = str(config[CONF_ID]) + if config_id != port0_parent_id: + data.port_map[config_id] = next_port + next_port += 1 + + def _final_validate(_): + from esphome.components.esp32 import idf_version + + if use_legacy() and idf_version() >= cv.Version(6, 0, 0): + raise cv.Invalid( + "The legacy I2S driver is not available in ESP-IDF 6.0+. " + "Set 'use_legacy: false' in i2s_audio configuration." + ) i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] variant = get_esp32_variant() if variant not in I2S_PORTS: @@ -263,6 +326,7 @@ def _final_validate(_): raise cv.Invalid( f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" ) + _assign_ports() def use_legacy(): @@ -276,6 +340,12 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Assign I2S port from _final_validate computed mapping + data = _get_data() + if (port := data.port_map.get(str(config[CONF_ID]))) is None: + raise ValueError(f"No I2S port assigned for {config[CONF_ID]}") + cg.add(var.set_port(port)) + # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) include_builtin_idf_component("esp_driver_i2s") diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp deleted file mode 100644 index 43064498cc5..00000000000 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "i2s_audio.h" - -#ifdef USE_ESP32 - -#include "esphome/core/log.h" - -namespace esphome { -namespace i2s_audio { - -static const char *const TAG = "i2s_audio"; - -void I2SAudioComponent::setup() { - static i2s_port_t next_port_num = I2S_NUM_0; - if (next_port_num >= SOC_I2S_NUM) { - ESP_LOGE(TAG, "Too many components"); - this->mark_failed(); - return; - } - - this->port_ = next_port_num; - next_port_num = (i2s_port_t) (next_port_num + 1); -} - -} // namespace i2s_audio -} // namespace esphome - -#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index cfccf7e01f6..f26ffddd467 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -5,6 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include <esp_idf_version.h> #ifdef USE_I2S_LEGACY #include <driver/i2s.h> #else @@ -56,8 +57,6 @@ class I2SAudioOut : public I2SAudioBase {}; class I2SAudioComponent : public Component { public: - void setup() override; - #ifdef USE_I2S_LEGACY i2s_pin_config_t get_pin_config() const { return { @@ -86,13 +85,17 @@ class I2SAudioComponent : public Component { void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } + void set_port(int port) { this->port_ = port; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + int get_port() const { return this->port_; } +#else + i2s_port_t get_port() const { return static_cast<i2s_port_t>(this->port_); } +#endif void lock() { this->lock_.lock(); } bool try_lock() { return this->lock_.try_lock(); } void unlock() { this->lock_.unlock(); } - i2s_port_t get_port() const { return this->port_; } - protected: Mutex lock_; @@ -106,7 +109,7 @@ class I2SAudioComponent : public Component { int bclk_pin_{I2S_GPIO_UNUSED}; #endif int lrclk_pin_; - i2s_port_t port_{}; + int port_{}; }; } // namespace i2s_audio diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index bf583b9f818..aa15625cd78 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -13,9 +13,11 @@ from esphome.const import ( ) from .. import ( + CONF_ADC_TYPE, CONF_I2S_DIN_PIN, CONF_LEFT, CONF_MONO, + CONF_PDM, CONF_RIGHT, I2SAudioIn, i2s_audio_component_schema, @@ -29,9 +31,7 @@ CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" -CONF_ADC_TYPE = "adc_type" CONF_CORRECT_DC_OFFSET = "correct_dc_offset" -CONF_PDM = "pdm" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index eb4506071e6..a17cb0d84a0 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -37,27 +37,6 @@ enum MicrophoneEventGroupBits : uint32_t { }; void I2SAudioMicrophone::setup() { -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_ADC - if (this->adc_) { - if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "Internal ADC only works on I2S0"); - this->mark_failed(); - return; - } - } else -#endif -#endif - { - if (this->pdm_) { - if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "PDM only works on I2S0"); - this->mark_failed(); - return; - } - } - } - this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); if (this->active_listeners_semaphore_ == nullptr) { ESP_LOGE(TAG, "Creating semaphore failed"); From f81e04b0366732793e718d3745d700969b02dd50 Mon Sep 17 00:00:00 2001 From: KamilCuk <kamilcukrowski@gmail.com> Date: Mon, 16 Mar 2026 22:30:31 +0100 Subject: [PATCH 0717/2056] [web_server] Fix wrong printf format specifier (#14836) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 40830196433..1dda6204fee 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -415,7 +415,7 @@ void WebServer::setup() { this->set_interval(10000, [this]() { char buf[32]; auto uptime = static_cast<uint32_t>(millis_64() / 1000); - buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); + buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); } From 2142bc1b767c0f0e178d8b34b0bf7d186e0529ca Mon Sep 17 00:00:00 2001 From: Fabrice <fabrice@weinberg.me> Date: Mon, 16 Mar 2026 23:25:11 +0100 Subject: [PATCH 0718/2056] [mipi_rgb] Make h- and v-sync pins optional (#14870) --- esphome/components/mipi_rgb/display.py | 18 ++++++++++++------ esphome/components/mipi_rgb/mipi_rgb.cpp | 12 ++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 24988cfcf86..0aa8c56719b 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -194,8 +194,12 @@ def model_schema(config): CONF_DE_PIN, cv.UNDEFINED ): pins.internal_gpio_output_pin_schema, model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, - model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, - model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option( + CONF_HSYNC_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, + model.option( + CONF_VSYNC_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, } ) @@ -307,10 +311,12 @@ async def to_code(config): cg.add(var.set_de_pin(pin)) pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) cg.add(var.set_pclk_pin(pin)) - pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) - cg.add(var.set_hsync_pin(pin)) - pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) - cg.add(var.set_vsync_pin(pin)) + if hsync_pin := config.get(CONF_HSYNC_PIN): + pin = await cg.gpio_pin_expression(hsync_pin) + cg.add(var.set_hsync_pin(pin)) + if vsync_pin := config.get(CONF_VSYNC_PIN): + pin = await cg.gpio_pin_expression(vsync_pin) + cg.add(var.set_vsync_pin(pin)) await display.register_display(var, config) if lamb := config.get(CONF_LAMBDA): diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 824ff6afe79..0b0a5344e47 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -158,8 +158,16 @@ void MipiRgb::common_setup_() { } config.data_width = data_pin_count; config.disp_gpio_num = GPIO_NUM_NC; - config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin()); - config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin()); + if (this->hsync_pin_) { + config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin()); + } else { + config.hsync_gpio_num = GPIO_NUM_NC; + } + if (this->vsync_pin_) { + config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin()); + } else { + config.vsync_gpio_num = GPIO_NUM_NC; + } if (this->de_pin_) { config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin()); } else { From 06d1498c47e78a9635333a109a089c58309f50c2 Mon Sep 17 00:00:00 2001 From: guillempages <guillempages@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:15:20 +0100 Subject: [PATCH 0719/2056] [runtime_image] Update jpegdec lib version (#14726) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/runtime_image/__init__.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ff25675918b..87b4ebb2c69 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e +8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 0773a53d911..7c22bfc9d19 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -74,7 +74,7 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") - cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") class PNGFormat(Format): diff --git a/platformio.ini b/platformio.ini index deee23d049c..3c3d62ef76a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,11 +46,11 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} - esphome/noise-c@0.1.11 ; api + esphome/noise-c@0.1.11 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library - https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image + https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image ; This dependency is used only in unit tests. ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py ; See scripts/cpp_unit_test.py and tests/components/README.md From 5d5c2723b2570d06dddab60a1152903df592d5a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:14:41 -1000 Subject: [PATCH 0720/2056] [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) --- esphome/components/fastled_base/__init__.py | 5 +++++ tests/components/fastled_clockless/test.esp32-s3-ard.yaml | 1 + 2 files changed, 6 insertions(+) create mode 100644 tests/components/fastled_clockless/test.esp32-s3-ard.yaml diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 11e8423258c..c944e8a930c 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_RGB_ORDER, ) +from esphome.core import CORE CODEOWNERS = ["@OttoWinter"] fastled_base_ns = cg.esphome_ns.namespace("fastled_base") @@ -41,5 +42,9 @@ async def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) cg.add_library("fastled/FastLED", "3.9.16") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") await light.register_light(var, config) return var diff --git a/tests/components/fastled_clockless/test.esp32-s3-ard.yaml b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/fastled_clockless/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From d6fba390378b6e8469254c518b3e0e5d1312026b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 15 Mar 2026 18:15:01 -1000 Subject: [PATCH 0721/2056] [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/runtime_image/__init__.py | 8 ++++++++ esphome/idf_component.yml | 2 ++ .../online_image/test.esp32-s3-ard.yaml | 19 +++++++++++++++++++ .../online_image/test.esp32-s3-idf.yaml | 19 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 tests/components/online_image/test.esp32-s3-ard.yaml create mode 100644 tests/components/online_image/test.esp32-s3-idf.yaml diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 7c22bfc9d19..3ae35cc5f17 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -11,6 +11,7 @@ from esphome.components.image import ( ) import esphome.config_validation as cv from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE +from esphome.core import CORE AUTO_LOAD = ["image"] CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] @@ -75,6 +76,13 @@ class JPEGFormat(Format): def actions(self) -> None: cg.add_define("USE_RUNTIME_IMAGE_JPEG") cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4") + if CORE.is_esp32: + from esphome.components.esp32 import add_idf_component + + # JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level + # ARDUINO_ESP32S3_DEV define) that require esp-dsp headers. + # On Arduino this overwrites the stub; on IDF it adds the component. + add_idf_component(name="espressif/esp-dsp", ref="1.7.1") class PNGFormat(Format): diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index df651ae15dd..381ecdcbf6e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -5,6 +5,8 @@ dependencies: version: 2.0.3 esphome/micro-opus: version: 0.3.5 + espressif/esp-dsp: + version: "1.7.1" espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: diff --git a/tests/components/online_image/test.esp32-s3-ard.yaml b/tests/components/online_image/test.esp32-s3-ard.yaml new file mode 100644 index 00000000000..9116fd86e09 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-ard.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-ard.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/test.esp32-s3-idf.yaml b/tests/components/online_image/test.esp32-s3-idf.yaml new file mode 100644 index 00000000000..f219f71ee25 --- /dev/null +++ b/tests/components/online_image/test.esp32-s3-idf.yaml @@ -0,0 +1,19 @@ +packages: + spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml + +<<: !include common.yaml + +http_request: + +display: + - platform: ili9xxx + spi_id: spi_bus + id: main_lcd + model: ili9342 + cs_pin: 20 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); From ffce637ea547698c034ab6fde3d04c3ee8fc6e41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:43:38 +0000 Subject: [PATCH 0722/2056] Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849) 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 e634bcb1046..da95dd5a13d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.1 +aioesphomeapi==44.5.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From b8ce907976689a5f78ca4f80d483087d001fc5d8 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 16 Mar 2026 08:08:05 +0100 Subject: [PATCH 0723/2056] [ble_nus] fix uart debug (#14850) --- esphome/components/ble_nus/ble_nus.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index d0d37dbf1cb..2f60f814718 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) { // First, use the peek buffer if available if (this->has_peek_) { +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif data[0] = this->peek_buffer_; this->has_peek_ = false; data++; if (--len == 0) { // Decrement len first, then check it... -#ifdef USE_UART_DEBUGGER - this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); -#endif - return true; // No more to read + return true; // No more to read } } From 0c260e483e2700401724706ffd02bc038c65fc93 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:39:26 -0400 Subject: [PATCH 0724/2056] [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) --- esphome/components/dallas_temp/dallas_temp.cpp | 3 +++ esphome/components/gpio/one_wire/gpio_one_wire.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 13f2fa59bd7..f119e28e78e 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() { float DallasTemperatureSensor::get_temp_c_() { int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0]; if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + if (this->scratch_pad_[7] == 0) { + return NAN; + } return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; } switch (this->resolution_) { diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 4191c45de15..4e2a306fc94 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() { uint64_t IRAM_ATTR GPIOOneWireBus::read64() { InterruptLock lock; uint64_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i < 64; i++) { ret |= (uint64_t(this->read_bit_()) << i); } return ret; From bb0a5dc8a8c2df1217dd9cab0402b2e9c29e2b7d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:40:24 -0400 Subject: [PATCH 0725/2056] [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) --- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b29e4c21540..ee6c2ee4718 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() { this->x_raw_max_ = this->display_->get_native_width(); } if (this->y_raw_max_ == this->y_raw_min_) { - this->x_raw_max_ = this->display_->get_native_height(); + this->y_raw_max_ = this->display_->get_native_height(); } } } @@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() { } point = buffer[5] & 0xF; + if (point > 2) { + ESP_LOGW(TAG, "Invalid touch point count: %d", point); + point = 2; + } if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); From f36b0fcb61f78ab2a01cfb03c57f5b1ad75b8fdc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:42:13 -0400 Subject: [PATCH 0726/2056] [am43] Fix battery update throttle using wrong type (#14864) --- esphome/components/am43/sensor/am43_sensor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 91973d8e33f..195b96a19eb 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent uint8_t current_sensor_; // The AM43 often gets into a state where it spams loads of battery update // notifications. Here we will limit to no more than every 10s. - uint8_t last_battery_update_; + uint32_t last_battery_update_; }; } // namespace am43 From 9133582aa0c636bd3e81bc424b46c5248427ccb3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:44:55 -0400 Subject: [PATCH 0727/2056] [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) --- esphome/components/as3935/as3935.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index 5dff1cb0aef..5f46dadfa8c 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -41,7 +41,7 @@ enum AS3935RegisterMasks { INT_MASK = 0xF0, THRESH_MASK = 0x0F, R_SPIKE_MASK = 0xF0, - ENERGY_MASK = 0xF0, + ENERGY_MASK = 0xE0, CAP_MASK = 0xF0, LIGHT_MASK = 0xCF, DISTURB_MASK = 0xDF, From 0816b27398b0abb63c11d6f7a10e65ef95f6dcec Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:45:16 -0400 Subject: [PATCH 0728/2056] [core] Support both dot and dash separators in Version.parse (#14858) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1eac53e9b20..32689dab27e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -314,7 +314,7 @@ class Version: @classmethod def parse(cls, value: str) -> Version: - match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value) + match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) From d6c67d5c357669e547f94a461614976e0d3d54cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:45:03 +1300 Subject: [PATCH 0729/2056] Bump version to 2026.3.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 4ec3a24c9fe..96295b3fc8e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b2 +PROJECT_NUMBER = 2026.3.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 2466f2c49c0..561a27d2286 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b2" +__version__ = "2026.3.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 73ca0ff1069ad2582830316890faaa9e43fad3b2 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:22:31 +0100 Subject: [PATCH 0730/2056] [core] Small improvements (#14884) --- esphome/components/bme68x_bsec2/__init__.py | 4 ++-- script/merge_component_configs.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index 4200b2f0b85..5f0afa9c9f2 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -186,8 +186,8 @@ async def to_code_base(config): cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", - "1.3.40408", - "https://github.com/boschsensortec/Bosch-BME68x-Library", + None, + "https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408", ) cg.add_library( "BSEC2 Software Library", diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 5e98f1fef5a..41bbafcd021 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -384,7 +384,7 @@ def merge_component_configs( # Write merged config output_file.parent.mkdir(parents=True, exist_ok=True) yaml_content = yaml_util.dump(merged_config_data) - output_file.write_text(yaml_content) + output_file.write_text(yaml_content, encoding="utf-8") print(f"Successfully merged {len(component_names)} components into {output_file}") From b083491e7493f4f2f1c6acadee673c0c05e29bd0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:46:32 -0400 Subject: [PATCH 0731/2056] [microphone] Switch IDF test to new I2S driver (#14886) --- tests/components/microphone/common.yaml | 10 +++++++++- .../components/microphone/test.esp32-idf.yaml | 20 +++---------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/components/microphone/common.yaml b/tests/components/microphone/common.yaml index 00d33bcc3db..39ab06da616 100644 --- a/tests/components/microphone/common.yaml +++ b/tests/components/microphone/common.yaml @@ -6,7 +6,7 @@ i2s_audio: microphone: - platform: i2s_audio id: mic_id_external - i2s_din_pin: ${i2s_din_pin} + i2s_din_pin: ${i2s_din_pin1} adc_type: external pdm: false mclk_multiple: 384 @@ -15,7 +15,15 @@ microphone: - if: condition: - microphone.is_muted: + id: mic_id_external then: - microphone.unmute: + id: mic_id_external else: - microphone.mute: + id: mic_id_external + - platform: i2s_audio + id: mic_id_pdm + i2s_din_pin: ${i2s_din_pin2} + adc_type: external + pdm: true diff --git a/tests/components/microphone/test.esp32-idf.yaml b/tests/components/microphone/test.esp32-idf.yaml index 830f0156d77..2f39263a435 100644 --- a/tests/components/microphone/test.esp32-idf.yaml +++ b/tests/components/microphone/test.esp32-idf.yaml @@ -2,21 +2,7 @@ substitutions: i2s_bclk_pin: GPIO15 i2s_lrclk_pin: GPIO4 i2s_mclk_pin: GPIO5 - i2s_din_pin: GPIO33 + i2s_din_pin1: GPIO33 + i2s_din_pin2: GPIO34 -i2s_audio: - i2s_bclk_pin: ${i2s_bclk_pin} - i2s_lrclk_pin: ${i2s_lrclk_pin} - i2s_mclk_pin: ${i2s_mclk_pin} - use_legacy: true - -microphone: - - platform: i2s_audio - id: mic_id_external - i2s_din_pin: ${i2s_din_pin} - adc_type: external - pdm: false - - platform: i2s_audio - id: mic_id_adc - adc_pin: 32 - adc_type: internal +<<: !include common.yaml From 3826e9550616bf154fcdcb2541289b9bf2f3a1db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 08:14:36 -1000 Subject: [PATCH 0732/2056] [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) --- esphome/components/api/proto.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 6752dfb9cd0..d6e993d3a5a 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -442,8 +442,12 @@ class ProtoMessage { virtual const char *message_name() const { return "unknown"; } #endif +#ifndef USE_HOST protected: +#endif // Non-virtual destructor is protected to prevent polymorphic deletion. + // On host platform, made public to allow value-initialization of std::array + // members (e.g. DeviceInfoResponse::devices) without clang errors. ~ProtoMessage() = default; }; From 82ccc37ba11a81894a38c729ab6f176db86b87ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 08:14:52 -1000 Subject: [PATCH 0733/2056] [ethernet] Mark EthernetComponent as final (#14842) --- esphome/components/ethernet/ethernet_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 901d9bc0bb4..88a86bc0432 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -85,7 +85,7 @@ enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL }; enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M }; #endif -class EthernetComponent : public Component { +class EthernetComponent final : public Component { public: EthernetComponent(); void setup() override; From b3210de374aec9f63126af7617cf31d235875abf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 08:53:36 -1000 Subject: [PATCH 0734/2056] [core] Extract shared C++ build helpers from cpp_unit_test.py (#14883) --- script/cpp_unit_test.py | 257 ++++---------------------- script/test_helpers.py | 400 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 436 insertions(+), 221 deletions(-) create mode 100644 script/test_helpers.py diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index c6cfd8270fa..81c56b82da9 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -1,238 +1,53 @@ #!/usr/bin/env python3 import argparse -import hashlib -import os from pathlib import Path -import subprocess import sys -from helpers import get_all_components, get_all_dependencies, root_path - -from esphome.__main__ import command_compile, parse_args -from esphome.config import validate_config -from esphome.const import CONF_PLATFORM -from esphome.core import CORE -from esphome.loader import get_component -from esphome.platformio_api import get_idedata - -# This must coincide with the version in /platformio.ini -PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" +from helpers import get_all_components, root_path +from test_helpers import ( + BASE_CODEGEN_COMPONENTS, + PLATFORMIO_GOOGLE_TEST_LIB, + USE_TIME_TIMEZONE_FLAG, + build_and_run, +) # Path to /tests/components COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" -# Components whose to_code should run during C++ test builds. -# Most components don't need code generation for tests; only these -# essential ones (platform setup, logging, core config) are needed. -# Note: "core" is the esphome core config module (esphome/core/config.py), -# which registers under package name "core" not "esphome". -CPP_TESTING_CODEGEN_COMPONENTS = {"core", "host", "logger"} - - -def hash_components(components: list[str]) -> str: - key = ",".join(components) - return hashlib.sha256(key.encode()).hexdigest()[:16] - - -def filter_components_without_tests(components: list[str]) -> list[str]: - """Filter out components that do not have a corresponding test file. - - This is done by checking if the component's directory contains at - least a .cpp or .h file. - """ - filtered_components: list[str] = [] - for component in components: - test_dir = COMPONENTS_TESTS_DIR / component - if test_dir.is_dir() and ( - any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h")) - ): - filtered_components.append(component) - else: - print( - f"WARNING: No tests found for component '{component}', skipping.", - file=sys.stderr, - ) - return filtered_components - - -def create_test_config(config_name: str, includes: list[str]) -> dict: - """Create ESPHome test configuration for C++ unit tests. - - Args: - config_name: Unique name for this test configuration - includes: List of include folders for the test build - - Returns: - Configuration dict for ESPHome - """ - return { - "esphome": { - "name": config_name, - "friendly_name": "CPP Unit Tests", - "libraries": PLATFORMIO_GOOGLE_TEST_LIB, - "platformio_options": { - "build_type": "debug", - "build_unflags": [ - "-Os", # remove size-opt flag - ], - "build_flags": [ - "-Og", # optimize for debug - "-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing - "-DESPHOME_DEBUG", # enable debug assertions - # Enable the address and undefined behavior sanitizers - "-fsanitize=address", - "-fsanitize=undefined", - "-fno-omit-frame-pointer", - ], - "debug_build_flags": [ # only for debug builds - "-g3", # max debug info - "-ggdb3", - ], - }, - "includes": includes, - }, - "host": {}, - "logger": {"level": "DEBUG"}, - } - - -def get_platform_components(components: list[str]) -> list[str]: - """Discover platform sub-components referenced by test directory structure. - - For each component being tested, any sub-directory named after a platform - domain (e.g. ``sensor``, ``binary_sensor``) is treated as a request to - include that ``<domain>.<component>`` platform in the build. The sub- - directory must name a valid platform domain; anything else raises an error - so that typos are caught early. - - Returns: - List of ``"domain.component"`` strings, one per discovered sub-directory. - """ - platform_components: list[str] = [] - for component in components: - test_dir = COMPONENTS_TESTS_DIR / component - if not test_dir.is_dir(): - continue - # Each sub-directory name is expected to be a platform domain - # (e.g. tests/components/bthome/sensor/ → sensor.bthome). - for domain_dir in test_dir.iterdir(): - if not domain_dir.is_dir(): - continue - domain = domain_dir.name - domain_module = get_component(domain) - if domain_module is None or not domain_module.is_platform_component: - raise ValueError( - f"Component tests for '{component}' reference non-existing or invalid domain '{domain}'" - f" in its directory structure. See ({COMPONENTS_TESTS_DIR / component / domain})." - ) - platform_components.append(f"{domain}.{component}") - return platform_components - - -# Exit codes for run_tests -EXIT_OK = 0 -EXIT_SKIPPED = 1 -EXIT_COMPILE_ERROR = 2 -EXIT_CONFIG_ERROR = 3 -EXIT_NO_EXECUTABLE = 4 +PLATFORMIO_OPTIONS = { + "build_type": "debug", + "build_unflags": [ + "-Os", # remove size-opt flag + ], + "build_flags": [ + "-Og", # optimize for debug + USE_TIME_TIMEZONE_FLAG, + "-DESPHOME_DEBUG", # enable debug assertions + # Enable the address and undefined behavior sanitizers + "-fsanitize=address", + "-fsanitize=undefined", + "-fno-omit-frame-pointer", + ], + "debug_build_flags": [ # only for debug builds + "-g3", # max debug info + "-ggdb3", + ], +} def run_tests(selected_components: list[str]) -> int: - # Skip tests on Windows - if os.name == "nt": - print("Skipping esphome tests on Windows", file=sys.stderr) - return EXIT_SKIPPED - - # Remove components that do not have tests - components = filter_components_without_tests(selected_components) - - if len(components) == 0: - print( - "No components specified or no tests found for the specified components.", - file=sys.stderr, - ) - return EXIT_OK - - components = sorted(components) - - # Build a list of include folders relative to COMPONENTS_TESTS_DIR. These folders will - # be added along with their subfolders. - # "main.cpp" is a special entry that points to /tests/components/main.cpp, - # which provides a custom test runner entry-point replacing the default one. - # Each remaining entry is a component folder whose *.cpp files are compiled. - includes: list[str] = ["main.cpp"] + components - - # Obtain a list of platform components to be tested: - try: - platform_components = get_platform_components(components) - except ValueError as e: - print(f"Error obtaining platform components: {e}") - return EXIT_CONFIG_ERROR - - components = sorted(components + platform_components) - - # Create a unique name for this config based on the actual components being tested - # to maximize cache during testing - config_name: str = "cpptests-" + hash_components(components) - - # Obtain possible dependencies for the requested components. - # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, - # which causes core/time.h to include components/time/posix_tz.h. - components_with_dependencies: list[str] = sorted( - get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + return build_and_run( + selected_components=selected_components, + tests_dir=COMPONENTS_TESTS_DIR, + codegen_components=BASE_CODEGEN_COMPONENTS, + config_prefix="cpptests", + friendly_name="CPP Unit Tests", + libraries=PLATFORMIO_GOOGLE_TEST_LIB, + platformio_options=PLATFORMIO_OPTIONS, + main_entry="main.cpp", + label="unit tests", ) - config = create_test_config(config_name, includes) - - CORE.config_path = COMPONENTS_TESTS_DIR / "dummy.yaml" - CORE.dashboard = None - CORE.cpp_testing = True - CORE.cpp_testing_codegen = CPP_TESTING_CODEGEN_COMPONENTS - - # Validate config will expand the above with defaults: - config = validate_config(config, {}) - - # Add all components and dependencies to the base configuration after validation, so their files - # are added to the build. - for component_name in components_with_dependencies: - if "." in component_name: - # Format is always "domain.component" (exactly one dot), - # as produced by get_platform_components(). - domain, component = component_name.split(".", maxsplit=1) - domain_list = config.setdefault(domain, []) - CORE.testing_ensure_platform_registered(domain) - domain_list.append({CONF_PLATFORM: component}) - else: - config.setdefault(component_name, []) - - dependencies = set(components_with_dependencies) - set(components) - deps_str = ", ".join(dependencies) if dependencies else "None" - print(f"Testing components: {', '.join(components)}. Dependencies: {deps_str}") - CORE.config = config - args = parse_args(["program", "compile", str(CORE.config_path)]) - try: - exit_code: int = command_compile(args, config) - - if exit_code != 0: - print(f"Error compiling unit tests for {', '.join(components)}") - return exit_code - except Exception as e: - print( - f"Error compiling unit tests for {', '.join(components)}. Check path. : {e}" - ) - return EXIT_COMPILE_ERROR - - # After a successful compilation, locate the executable and run it: - idedata = get_idedata(config) - if idedata is None: - print("Cannot find executable") - return EXIT_NO_EXECUTABLE - - program_path: str = idedata.raw["prog_path"] - run_cmd: list[str] = [program_path] - run_proc = subprocess.run(run_cmd, check=False) - return run_proc.returncode - def main() -> None: parser = argparse.ArgumentParser( diff --git a/script/test_helpers.py b/script/test_helpers.py new file mode 100644 index 00000000000..e872bbc5160 --- /dev/null +++ b/script/test_helpers.py @@ -0,0 +1,400 @@ +"""Shared helpers for C++ unit test and benchmark build scripts.""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import subprocess +import sys + +from helpers import get_all_dependencies +import yaml + +from esphome.__main__ import command_compile, parse_args +from esphome.config import validate_config +from esphome.const import CONF_PLATFORM +from esphome.core import CORE +from esphome.loader import get_component +from esphome.platformio_api import get_idedata + +# This must coincide with the version in /platformio.ini +PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" + +# Google Benchmark library for PlatformIO +# Format: name=repository_url (see esphome/core/config.py library parsing) +PLATFORMIO_GOOGLE_BENCHMARK_LIB = ( + "benchmark=https://github.com/google/benchmark.git#v1.9.1" +) + +# Key names for the base config sections +ESPHOME_KEY = "esphome" +HOST_KEY = "host" +LOGGER_KEY = "logger" + +# Base config keys that are always present and must not be fully overridden +# by component benchmark.yaml files. esphome: allows sub-key merging. +BASE_CONFIG_KEYS = frozenset({ESPHOME_KEY, HOST_KEY, LOGGER_KEY}) + +# Shared build flag — enables timezone code paths for testing/benchmarking. +USE_TIME_TIMEZONE_FLAG = "-DUSE_TIME_TIMEZONE" + +# Components whose to_code should always run during C++ test/benchmark builds. +# These are the minimal infrastructure components needed for host compilation. +# Note: "core" is the esphome core config module (esphome/core/config.py), +# which registers under package name "core" not "esphome". +BASE_CODEGEN_COMPONENTS = {"core", "host", "logger"} + +# Exit codes +EXIT_OK = 0 +EXIT_SKIPPED = 1 +EXIT_COMPILE_ERROR = 2 +EXIT_CONFIG_ERROR = 3 +EXIT_NO_EXECUTABLE = 4 + +# Name of the per-component YAML config file in benchmark directories +BENCHMARK_YAML_FILENAME = "benchmark.yaml" + + +def hash_components(components: list[str]) -> str: + """Create a short hash of component names for unique config naming.""" + key = ",".join(components) + return hashlib.sha256(key.encode()).hexdigest()[:16] + + +def filter_components_with_files(components: list[str], tests_dir: Path) -> list[str]: + """Filter out components that do not have .cpp or .h files in the tests dir. + + Args: + components: List of component names to check + tests_dir: Base directory containing component test/benchmark folders + + Returns: + Filtered list of components that have test files + """ + filtered_components: list[str] = [] + for component in components: + test_dir = tests_dir / component + if test_dir.is_dir() and ( + any(test_dir.glob("*.cpp")) or any(test_dir.glob("*.h")) + ): + filtered_components.append(component) + else: + print( + f"WARNING: No files found for component '{component}' in {test_dir}, skipping.", + file=sys.stderr, + ) + return filtered_components + + +def get_platform_components(components: list[str], tests_dir: Path) -> list[str]: + """Discover platform sub-components referenced by test directory structure. + + For each component, any sub-directory named after a platform domain + (e.g. ``sensor``, ``binary_sensor``) is treated as a request to include + that ``<domain>.<component>`` platform in the build. + + Args: + components: List of component names to scan + tests_dir: Base directory containing component test/benchmark folders + + Returns: + List of ``"domain.component"`` strings + """ + platform_components: list[str] = [] + for component in components: + test_dir = tests_dir / component + if not test_dir.is_dir(): + continue + for domain_dir in test_dir.iterdir(): + if not domain_dir.is_dir(): + continue + domain = domain_dir.name + domain_module = get_component(domain) + if domain_module is None or not domain_module.is_platform_component: + raise ValueError( + f"Component '{component}' references non-existing or invalid domain '{domain}'" + f" in its directory structure. See ({tests_dir / component / domain})." + ) + platform_components.append(f"{domain}.{component}") + return platform_components + + +def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict: + """Load and merge benchmark.yaml files from component directories. + + Each component directory may contain a ``benchmark.yaml`` file that + declares additional ESPHome components needed for the build (e.g. + ``api:``, ``sensor:``). These get merged into the base config before + validation so that dependencies are properly resolved with defaults. + + The ``esphome:`` key is special: its sub-keys are merged into the + existing esphome config (e.g. to add ``areas:`` or ``devices:``). + Other base config keys (``host:``, ``logger:``) are not overridable. + + Args: + components: List of component directory names + tests_dir: Base directory containing component folders + + Returns: + Merged dict of component configs to add to the base config + """ + # Note: components are processed in sorted order. For conflicting keys + # (e.g. two benchmark.yaml files both declaring sensor:), the first + # component alphabetically wins via setdefault(). This is fine for now + # with a single benchmark component (api) but would need a real merge + # strategy if multiple components declare overlapping configs. + merged: dict = {} + for component in components: + yaml_path = tests_dir / component / BENCHMARK_YAML_FILENAME + if not yaml_path.is_file(): + continue + with open(yaml_path) as f: + component_config = yaml.safe_load(f) + if component_config and isinstance(component_config, dict): + for key, value in component_config.items(): + if key in BASE_CONFIG_KEYS - {ESPHOME_KEY}: + # host: and logger: are not overridable + continue + if key == ESPHOME_KEY and isinstance(value, dict): + # Merge esphome sub-keys rather than replacing + esphome_extra = merged.setdefault(ESPHOME_KEY, {}) + for sub_key, sub_value in value.items(): + esphome_extra.setdefault(sub_key, sub_value) + continue + merged.setdefault(key, value) + return merged + + +def create_host_config( + config_name: str, + friendly_name: str, + libraries: str | list[str], + includes: list[str], + platformio_options: dict, +) -> dict: + """Create an ESPHome host configuration for C++ builds. + + Args: + config_name: Unique name for this configuration + friendly_name: Human-readable name + libraries: PlatformIO library specification(s) + includes: List of include folders for the build + platformio_options: Dict of platformio_options to set + + Returns: + Configuration dict for ESPHome + """ + return { + ESPHOME_KEY: { + "name": config_name, + "friendly_name": friendly_name, + "libraries": libraries, + "platformio_options": platformio_options, + "includes": includes, + }, + HOST_KEY: {}, + LOGGER_KEY: {"level": "DEBUG"}, + } + + +def compile_and_get_binary( + config: dict, + components: list[str], + codegen_components: set[str], + tests_dir: Path, + label: str = "build", +) -> tuple[int, str | None]: + """Compile an ESPHome configuration and return the binary path. + + Args: + config: ESPHome configuration dict (already created via create_host_config) + components: List of components to include in the build + codegen_components: Set of component names whose to_code should run + tests_dir: Base directory for test files (used as config_path base) + label: Label for log messages (e.g. "unit tests", "benchmarks") + + Returns: + Tuple of (exit_code, program_path_or_none) + """ + # Load any benchmark.yaml files from component directories and merge + # them into the config BEFORE dependency resolution and validation. + # This allows each benchmark/test dir to declare which ESPHome components + # it needs (e.g. api:) so they get proper config defaults. + extra_config = load_component_yaml_configs(components, tests_dir) + for key, value in extra_config.items(): + if key == ESPHOME_KEY and isinstance(value, dict): + # Merge esphome sub-keys into existing esphome config. + # For list values (e.g. libraries), extend rather than replace. + for sub_key, sub_value in value.items(): + existing = config[ESPHOME_KEY].get(sub_key) + if existing is not None and isinstance(sub_value, list): + # Ensure existing is a list, then extend + if not isinstance(existing, list): + config[ESPHOME_KEY][sub_key] = [existing] + config[ESPHOME_KEY][sub_key].extend(sub_value) + else: + config[ESPHOME_KEY].setdefault(sub_key, sub_value) + else: + config.setdefault(key, value) + + # Obtain possible dependencies BEFORE validate_config, because + # get_all_dependencies calls CORE.reset() which clears build_path. + # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, + # which causes core/time.h to include components/time/posix_tz.h. + components_with_dependencies: list[str] = sorted( + get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + ) + + CORE.config_path = tests_dir / "dummy.yaml" + CORE.dashboard = None + CORE.cpp_testing = True + CORE.cpp_testing_codegen = codegen_components + + # Validate config will expand the above with defaults: + config = validate_config(config, {}) + + # Add remaining components and dependencies to the configuration after + # validation, so their source files are included in the build. + for component_name in components_with_dependencies: + if "." in component_name: + domain, component = component_name.split(".", maxsplit=1) + domain_list = config.setdefault(domain, []) + CORE.testing_ensure_platform_registered(domain) + domain_list.append({CONF_PLATFORM: component}) + # Skip "core" — it's a pseudo-component handled by the build + # system, not a real loadable component (get_component returns None) + elif get_component(component_name) is not None: + config.setdefault(component_name, []) + + # Register platforms from the extra config (benchmark.yaml) so + # USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing + # real entity instances. + for key in extra_config: + if key == ESPHOME_KEY: + continue + comp = get_component(key) + if comp is not None and comp.is_platform_component: + CORE.testing_ensure_platform_registered(key) + + dependencies = set(components_with_dependencies) - set(components) + deps_str = ", ".join(dependencies) if dependencies else "None" + print(f"Building {label}: {', '.join(components)}. Dependencies: {deps_str}") + CORE.config = config + args = parse_args(["program", "compile", str(CORE.config_path)]) + try: + exit_code: int = command_compile(args, config) + + if exit_code != 0: + print(f"Error compiling {label} for {', '.join(components)}") + return exit_code, None + except Exception as e: + print(f"Error compiling {label} for {', '.join(components)}: {e}") + return EXIT_COMPILE_ERROR, None + + # After a successful compilation, locate the executable: + idedata = get_idedata(config) + if idedata is None: + print("Cannot find executable") + return EXIT_NO_EXECUTABLE, None + + program_path: str = idedata.raw["prog_path"] + return EXIT_OK, program_path + + +def build_and_run( + selected_components: list[str], + tests_dir: Path, + codegen_components: set[str], + config_prefix: str, + friendly_name: str, + libraries: str | list[str], + platformio_options: dict, + main_entry: str, + label: str = "build", + build_only: bool = False, + extra_run_args: list[str] | None = None, + extra_include_dirs: list[Path] | None = None, +) -> int: + """Build and optionally run a C++ test/benchmark binary. + + This is the main orchestration function shared between unit tests + and benchmarks. + + Args: + selected_components: Components to include (directory names in tests_dir) + tests_dir: Directory containing test/benchmark files + codegen_components: Components whose to_code should run + config_prefix: Prefix for the config name (e.g. "cpptests", "cppbench") + friendly_name: Human-readable name for the config + libraries: PlatformIO library specification(s) + platformio_options: PlatformIO options dict + main_entry: Name of the main entry file (e.g. "main.cpp") + label: Label for log messages + build_only: If True, print binary path and return without running + extra_run_args: Extra arguments to pass to the binary + extra_include_dirs: Additional directories whose .cpp files + should be compiled (resolved relative to tests_dir if possible) + + Returns: + Exit code + """ + # Skip on Windows + if os.name == "nt": + print(f"Skipping {label} on Windows", file=sys.stderr) + return EXIT_SKIPPED + + # Remove components that do not have files + components = filter_components_with_files(selected_components, tests_dir) + + if len(components) == 0: + print( + f"No components specified or no files found for {label}.", + file=sys.stderr, + ) + return EXIT_OK + + components = sorted(components) + + # Build include list: main entry point + component folders + extra dirs + includes: list[str] = [main_entry] + components + if extra_include_dirs: + for d in extra_include_dirs: + if d.is_dir() and (any(d.glob("*.cpp")) or any(d.glob("*.h"))): + # ESPHome includes are relative to the config directory (tests_dir) + rel = os.path.relpath(d, tests_dir) + includes.append(rel) + + # Discover platform sub-components + try: + platform_components = get_platform_components(components, tests_dir) + except ValueError as e: + print(f"Error obtaining platform components: {e}") + return EXIT_CONFIG_ERROR + + components = sorted(components + platform_components) + + # Create unique config name + config_name: str = f"{config_prefix}-" + hash_components(components) + + config = create_host_config( + config_name, friendly_name, libraries, includes, platformio_options + ) + + exit_code, program_path = compile_and_get_binary( + config, components, codegen_components, tests_dir, label + ) + + if exit_code != EXIT_OK or program_path is None: + return exit_code + + if build_only: + print(f"BUILD_BINARY={program_path}") + return EXIT_OK + + # Run the binary + run_cmd: list[str] = [program_path] + if extra_run_args: + run_cmd.extend(extra_run_args) + run_proc = subprocess.run(run_cmd, check=False) + return run_proc.returncode From 53fa346ddc6ce6e99ff712ddc8964841368c6851 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:18:49 -0400 Subject: [PATCH 0735/2056] [speaker] Fix media playlist using announcement delay (#14889) --- .../components/speaker/media_player/speaker_media_player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9f168f854d8..930373c6fcd 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() { this->media_playlist_.pop_front(); } // Only delay starting playback if moving on the next playlist item or repeating the current item - timeout_ms = this->announcement_playlist_delay_ms_; + timeout_ms = this->media_playlist_delay_ms_; } if (!this->media_playlist_.empty()) { PlaylistItem playlist_item = this->media_playlist_.front(); From 9a729608d57f993deac55144e39414185421aa3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 09:58:05 -1000 Subject: [PATCH 0736/2056] [core] Add back deprecated set_internal() for external projects (#14887) --- esphome/core/entity_base.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 012a62f1c08..4c6e5f6596b 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -100,6 +100,14 @@ class EntityBase { // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } + // Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients + // are NOT notified of the change, the flag may have already been read during setup, and there + // is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead. + ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT " + "notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.", + "2026.3.0") + void set_internal(bool internal) { this->flags_.internal = internal; } + // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. From 851e8b6c0def9938b9603887166a3d1f2693c03b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:28:13 -0400 Subject: [PATCH 0737/2056] [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) --- esphome/components/gree/gree.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 8a9f264932c..732ebd9632e 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -87,19 +87,12 @@ void GreeClimate::transmit_state() { // Calculate the checksum if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); - } else if (this->model_ == GREE_YAG) { + } else { remote_state[7] = ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + ((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) & 0x0F) << 4); - } else { - remote_state[7] = - ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + - ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & - 0x0F) - << 4) | - (remote_state[7] & 0x0F); } auto transmit = this->transmitter_->transmit(); From 5f06679d78316582e09b8d0f6f56ef0dd8816b1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:16:44 -1000 Subject: [PATCH 0738/2056] [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) --- esphome/components/espnow/espnow_component.cpp | 6 ++++-- esphome/components/espnow/espnow_component.h | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 991803d8703..78916891f4d 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 9941e972270..ee4adc1b4d4 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -163,10 +163,14 @@ class ESPNowComponent : public Component { uint8_t own_address_[ESP_NOW_ETH_ALEN]{0}; LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{}; - EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{}; LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{}; - EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment. + EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{}; ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none uint8_t wifi_channel_{0}; From 97382ed8148fbe42c18bfd1ed7c10cd76b3b922e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:43 -1000 Subject: [PATCH 0739/2056] [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) --- .../components/usb_cdc_acm/usb_cdc_acm.cpp | 26 +++++++------------ esphome/components/usb_cdc_acm/usb_cdc_acm.h | 6 ++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index a4c2e6c4a48..253626f0a3e 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { event->data.line_state.dtr = dtr; event->data.line_state.rts = rts; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ event->data.line_coding.parity = parity; event->data.line_coding.data_bits = data_bits; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 020542e7490..a56abc9ee8e 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC LineStateCallback line_state_callback_{nullptr}; // Lock-free queue and event pool for cross-task event passing - EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing both a pool slot leak and an SPSC + // violation on the pool's internal free list. + EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_; LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_; }; From c19c75220bbf2f20e37dec81ac18dc0735e5e058 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:59 -1000 Subject: [PATCH 0740/2056] [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) --- esphome/components/usb_host/usb_host.h | 5 ++++- esphome/components/usb_host/usb_host_client.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 2eec0c9699b..dcb76a3a3ba 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -144,7 +144,10 @@ class USBClient : public Component { // Lock-free event queue and pool for USB task to main loop communication // Must be public for access from static callbacks LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue; - EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool; protected: // Process USB events from the queue. Returns true if any work was done. diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2a460d1a077..18d938344c7 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * return; } - // Push to lock-free queue (always succeeds since pool size == queue size) + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. client->event_queue.push(event); // Re-enable component loop to process the queued event From a94bb74d043da0b1128c7a899a1765a3e13f6af0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:18:31 -1000 Subject: [PATCH 0741/2056] [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) --- esphome/components/usb_uart/usb_uart.cpp | 11 +++++------ esphome/components/usb_uart/usb_uart.h | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 997f8361464..a5d312f1912 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); memcpy(chunk->data, data, chunk_len); chunk->length = static_cast<uint8_t>(chunk_len); - if (!this->output_queue_.push(chunk)) { - this->output_pool_.release(chunk); - ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len); - break; - } + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->output_queue_.push(chunk); data += chunk_len; len -= chunk_len; } @@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { chunk->channel = channel; // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. this->usb_data_queue_.push(chunk); // Re-enable component loop to process the queued data diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 16469df7f60..7a06b04f114 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_; - EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_; std::function<void()> rx_callback_{}; CdcEps cdc_dev_{}; StringRef debug_prefix_{}; @@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient { // Lock-free data transfer from USB task to main loop static constexpr int USB_DATA_QUEUE_SIZE = 32; LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_; - EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_; + // Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment. + EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_; protected: std::vector<USBUartChannel *> channels_{}; From 1adf05e2d59b60e7b22d0a0efedd5f9181f1fa12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:24:02 -1000 Subject: [PATCH 0742/2056] [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) --- esphome/components/esp32_ble/ble.cpp | 3 ++- esphome/components/esp32_ble/ble.h | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ff9d9bb15aa..fee1c546be4 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) { load_ble_event(event, args...); // Push the event to the queue + // Push always succeeds: pool is sized to queue capacity (N-1), so if + // allocate() returned non-null, the queue is guaranteed to have room. global_ble->ble_events_.push(event); - // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size } // Explicit template instantiations for the friend function diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 04bec3f7858..752ddc9d1ff 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -221,7 +221,13 @@ class ESP32BLE : public Component { // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; - esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 1670f04a8715380be9452e37d0b0c133c092c5f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:29:38 -1000 Subject: [PATCH 0743/2056] [core] Add CodSpeed C++ benchmarks for protobuf, main loop, and helpers (#14878) --- .github/workflows/ci.yml | 34 ++ esphome/core/component.h | 2 + script/clang-tidy | 3 + script/cpp_benchmark.py | 130 ++++++++ script/determine-jobs.py | 61 ++++ script/setup_codspeed_lib.py | 231 ++++++++++++++ tests/benchmarks/components/.gitignore | 2 + .../components/api/bench_proto_decode.cpp | 93 ++++++ .../components/api/bench_proto_encode.cpp | 298 ++++++++++++++++++ .../components/api/bench_proto_varint.cpp | 133 ++++++++ .../benchmarks/components/api/benchmark.yaml | 114 +++++++ tests/benchmarks/components/main.cpp | 42 +++ .../core/bench_application_loop.cpp | 22 ++ tests/benchmarks/core/bench_helpers.cpp | 41 +++ tests/benchmarks/core/bench_logger.cpp | 54 ++++ tests/benchmarks/core/bench_scheduler.cpp | 133 ++++++++ tests/script/test_determine_jobs.py | 148 +++++++++ 17 files changed, 1541 insertions(+) create mode 100755 script/cpp_benchmark.py create mode 100755 script/setup_codspeed_lib.py create mode 100644 tests/benchmarks/components/.gitignore create mode 100644 tests/benchmarks/components/api/bench_proto_decode.cpp create mode 100644 tests/benchmarks/components/api/bench_proto_encode.cpp create mode 100644 tests/benchmarks/components/api/bench_proto_varint.cpp create mode 100644 tests/benchmarks/components/api/benchmark.yaml create mode 100644 tests/benchmarks/components/main.cpp create mode 100644 tests/benchmarks/core/bench_application_loop.cpp create mode 100644 tests/benchmarks/core/bench_helpers.cpp create mode 100644 tests/benchmarks/core/bench_logger.cpp create mode 100644 tests/benchmarks/core/bench_scheduler.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7710589c50..1926ad5bf4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,6 +185,7 @@ jobs: cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }} + benchmarks: ${{ steps.determine.outputs.benchmarks }} steps: - name: Check out code from GitHub uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -227,6 +228,7 @@ jobs: echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT + echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 @@ -308,6 +310,38 @@ jobs: script/cpp_unit_test.py $ARGS fi + benchmarks: + name: Run CodSpeed benchmarks + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Build benchmarks + id: build + run: | + . venv/bin/activate + export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py) + # --build-only prints BUILD_BINARY=<path> to stdout + BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-) + echo "binary=$BINARY" >> $GITHUB_OUTPUT + + - name: Run CodSpeed benchmarks + uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4 + with: + run: ${{ steps.build.outputs.binary }} + mode: simulation + clang-tidy-single: name: ${{ matrix.name }} runs-on: ubuntu-24.04 diff --git a/esphome/core/component.h b/esphome/core/component.h index 5fdf23e128a..557ba09bbc9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -598,9 +598,11 @@ class WarnIfComponentBlockingGuard { #ifdef USE_RUNTIME_STATS this->record_runtime_stats_(); #endif +#ifndef USE_BENCHMARK if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } +#endif return curr_time; } diff --git a/script/clang-tidy b/script/clang-tidy index 9c2899026d2..f2834b44ac8 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -231,6 +231,9 @@ def main(): cwd = os.getcwd() files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])] + # Exclude benchmark files — they require google benchmark headers not + # available in the ESP32 toolchain and use different naming conventions. + files = [f for f in files if not f.startswith("tests/benchmarks/")] # Print initial file count if it's large if len(files) > 50: diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py new file mode 100755 index 00000000000..bd92266ea62 --- /dev/null +++ b/script/cpp_benchmark.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Build and run C++ benchmarks for ESPHome components using Google Benchmark.""" + +import argparse +import json +import os +from pathlib import Path +import sys + +from helpers import root_path +from test_helpers import ( + BASE_CODEGEN_COMPONENTS, + PLATFORMIO_GOOGLE_BENCHMARK_LIB, + USE_TIME_TIMEZONE_FLAG, + build_and_run, +) + +# Path to /tests/benchmarks/components +BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components" + +# Path to /tests/benchmarks/core (always included, not a component) +CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core" + +# Additional codegen components beyond the base set. +# json is needed because its to_code adds the ArduinoJson library +# (auto-loaded by api, but cpp_testing suppresses to_code unless listed). +BENCHMARK_CODEGEN_COMPONENTS = BASE_CODEGEN_COMPONENTS | {"json"} + +PLATFORMIO_OPTIONS = { + "build_unflags": [ + "-Os", # remove default size-opt + ], + "build_flags": [ + "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) + "-g", # debug symbols for profiling + USE_TIME_TIMEZONE_FLAG, + "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() + ], + # Use deep+ LDF mode to ensure PlatformIO detects the benchmark + # library dependency from nested includes. + "lib_ldf_mode": "deep+", +} + + +def run_benchmarks(selected_components: list[str], build_only: bool = False) -> int: + # Allow CI to override the benchmark library (e.g. with CodSpeed's fork). + # BENCHMARK_LIB_CONFIG is a JSON string from setup_codspeed_lib.py + # containing {"lib_path": "/path/to/google_benchmark"}. + lib_config_json = os.environ.get("BENCHMARK_LIB_CONFIG") + + pio_options = PLATFORMIO_OPTIONS + if lib_config_json: + lib_config = json.loads(lib_config_json) + benchmark_lib = f"benchmark=symlink://{lib_config['lib_path']}" + # These defines must be global (not just in library.json) because + # benchmark.h uses #ifdef CODSPEED_ENABLED to switch benchmark + # registration to CodSpeed-instrumented variants, and + # CODSPEED_ROOT_DIR is used to display relative file paths in reports. + project_root = Path(__file__).resolve().parent.parent + codspeed_flags = [ + "-DNDEBUG", + "-DCODSPEED_ENABLED", + "-DCODSPEED_ANALYSIS", + f'-DCODSPEED_ROOT_DIR=\\"{project_root}\\"', + ] + pio_options = { + **PLATFORMIO_OPTIONS, + "build_flags": PLATFORMIO_OPTIONS["build_flags"] + codspeed_flags, + } + else: + benchmark_lib = PLATFORMIO_GOOGLE_BENCHMARK_LIB + + return build_and_run( + selected_components=selected_components, + tests_dir=BENCHMARKS_DIR, + codegen_components=BENCHMARK_CODEGEN_COMPONENTS, + config_prefix="cppbench", + friendly_name="CPP Benchmarks", + libraries=benchmark_lib, + platformio_options=pio_options, + main_entry="main.cpp", + label="benchmarks", + build_only=build_only, + extra_include_dirs=[CORE_BENCHMARKS_DIR], + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Build and run C++ benchmarks for ESPHome components." + ) + parser.add_argument( + "components", + nargs="*", + help="List of components to benchmark (must have files in tests/benchmarks/components/).", + ) + parser.add_argument( + "--all", + action="store_true", + help="Benchmark all components with benchmark files.", + ) + parser.add_argument( + "--build-only", + action="store_true", + help="Only build, print binary path without running.", + ) + + args = parser.parse_args() + + if args.all: + # Find all component directories that have .cpp files + components: list[str] = ( + sorted( + d.name + for d in BENCHMARKS_DIR.iterdir() + if d.is_dir() + and d.name != "__pycache__" + and (any(d.glob("*.cpp")) or any(d.glob("*.h"))) + ) + if BENCHMARKS_DIR.is_dir() + else [] + ) + else: + components: list[str] = args.components + + sys.exit(run_benchmarks(components, build_only=args.build_only)) + + +if __name__ == "__main__": + main() diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 6808a3cf6c9..ad08f8dce56 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -381,6 +381,63 @@ def determine_cpp_unit_tests( return (False, get_cpp_changed_components(cpp_files)) +# Paths within tests/benchmarks/ that contain component benchmark files +BENCHMARKS_COMPONENTS_PATH = "tests/benchmarks/components" + +# Files that, when changed, should trigger benchmark runs +BENCHMARK_INFRASTRUCTURE_FILES = frozenset( + { + "script/cpp_benchmark.py", + "script/test_helpers.py", + "script/setup_codspeed_lib.py", + } +) + + +def should_run_benchmarks(branch: str | None = None) -> bool: + """Determine if C++ benchmarks should run based on changed files. + + Benchmarks run when any of the following conditions are met: + + 1. Core C++ files changed (esphome/core/*) + 2. A directly changed component has benchmark files (no dependency expansion) + 3. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py, + script/test_helpers.py, script/setup_codspeed_lib.py) + + Unlike unit tests, benchmarks do NOT expand to dependent components. + Changing ``sensor`` does not trigger ``api`` benchmarks just because + api depends on sensor. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if benchmarks should run, False otherwise. + """ + files = changed_files(branch) + if core_changed(files): + return True + + # Check if benchmark infrastructure changed + if any( + f.startswith("tests/benchmarks/") or f in BENCHMARK_INFRASTRUCTURE_FILES + for f in files + ): + return True + + # Check if any directly changed component has benchmarks + benchmarks_dir = Path(root_path) / BENCHMARKS_COMPONENTS_PATH + if not benchmarks_dir.is_dir(): + return False + benchmarked_components = { + d.name + for d in benchmarks_dir.iterdir() + if d.is_dir() and (any(d.glob("*.cpp")) or any(d.glob("*.h"))) + } + # Only direct changes — no dependency expansion + return any(get_component_from_path(f) in benchmarked_components for f in files) + + def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool: """Check if a changed file ends with any of the specified extensions.""" return any(file.endswith(extensions) for file in changed_files(branch)) @@ -804,6 +861,9 @@ def main() -> None: # Determine which C++ unit tests to run cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) + # Determine if benchmarks should run + run_benchmarks = should_run_benchmarks(args.branch) + # Split components into batches for CI testing # This intelligently groups components with similar bus configurations component_test_batches: list[str] @@ -856,6 +916,7 @@ def main() -> None: "cpp_unit_tests_run_all": cpp_run_all, "cpp_unit_tests_components": cpp_components, "component_test_batches": component_test_batches, + "benchmarks": run_benchmarks, } # Output as JSON diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py new file mode 100755 index 00000000000..959c89d05b6 --- /dev/null +++ b/script/setup_codspeed_lib.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Set up CodSpeed's google_benchmark fork as a PlatformIO library. + +CodSpeed requires their codspeed-cpp fork for CPU simulation instrumentation. +This script clones the repo and assembles a flat PlatformIO-compatible library +by combining google_benchmark sources, codspeed core, and instrument-hooks. + +PlatformIO quirks addressed: + - .cc files renamed to .cpp (PlatformIO ignores .cc) + - All sources merged into one src/ dir (PlatformIO can't compile from + multiple source directories in a single library) + - library.json created with required CodSpeed preprocessor defines + +Usage: + python script/setup_codspeed_lib.py [--output-dir DIR] + +Prints JSON to stdout with lib_path for cpp_benchmark.py. +Git output goes to stderr. + +See https://codspeed.io/docs/benchmarks/cpp#custom-build-systems +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import shutil +import subprocess +import sys + +# Pin to a specific release for reproducibility +CODSPEED_CPP_REPO = "https://github.com/CodSpeedHQ/codspeed-cpp.git" +CODSPEED_CPP_SHA = "e633aca00da3d0ad14e7bf424d9cb47165a29028" # v2.1.0 + +DEFAULT_OUTPUT_DIR = "/tmp/codspeed-cpp" + +# Well-known paths within the codspeed-cpp repository +GOOGLE_BENCHMARK_SUBDIR = "google_benchmark" +CORE_SUBDIR = "core" +INSTRUMENT_HOOKS_SUBDIR = Path(CORE_SUBDIR) / "instrument-hooks" +INSTRUMENT_HOOKS_INCLUDES = INSTRUMENT_HOOKS_SUBDIR / "includes" +INSTRUMENT_HOOKS_DIST = INSTRUMENT_HOOKS_SUBDIR / "dist" / "core.c" +CORE_CMAKE = Path(CORE_SUBDIR) / "CMakeLists.txt" + + +def _git(args: list[str], **kwargs: object) -> None: + """Run a git command, sending output to stderr.""" + subprocess.run( + ["git", *args], + check=True, + stdout=kwargs.pop("stdout", sys.stderr), + stderr=kwargs.pop("stderr", sys.stderr), + **kwargs, + ) + + +def _clone_repo(output_dir: Path) -> None: + """Shallow-clone codspeed-cpp at the pinned SHA with submodules.""" + output_dir.mkdir(parents=True, exist_ok=True) + _git(["init", str(output_dir)]) + _git(["-C", str(output_dir), "remote", "add", "origin", CODSPEED_CPP_REPO]) + _git(["-C", str(output_dir), "fetch", "--depth", "1", "origin", CODSPEED_CPP_SHA]) + _git( + ["-C", str(output_dir), "checkout", "FETCH_HEAD"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + _git( + [ + "-C", + str(output_dir), + "submodule", + "update", + "--init", + "--recursive", + "--depth", + "1", + ] + ) + + +def _read_codspeed_version(cmake_path: Path) -> str: + """Extract CODSPEED_VERSION from core/CMakeLists.txt.""" + if not cmake_path.exists(): + return "0.0.0" + for line in cmake_path.read_text().splitlines(): + if line.startswith("set(CODSPEED_VERSION"): + return line.split()[1].rstrip(")") + return "0.0.0" + + +def _rename_cc_to_cpp(src_dir: Path) -> None: + """Rename .cc files to .cpp so PlatformIO compiles them.""" + for cc_file in src_dir.glob("*.cc"): + cpp_file = cc_file.with_suffix(".cpp") + if not cpp_file.exists(): + cc_file.rename(cpp_file) + + +def _copy_if_missing(src: Path, dest: Path) -> None: + """Copy a file only if the destination doesn't already exist.""" + if not dest.exists(): + shutil.copy2(src, dest) + + +def _merge_codspeed_core_into_lib(core_src: Path, lib_src: Path) -> None: + """Copy codspeed core sources into the benchmark library src/. + + .cpp files get a ``codspeed_`` prefix to avoid name collisions with + google_benchmark's own sources. .h files keep their original names + since they're referenced by ``#include "walltime.h"`` etc. + """ + for src_file in core_src.iterdir(): + if src_file.suffix == ".cpp": + _copy_if_missing(src_file, lib_src / f"codspeed_{src_file.name}") + elif src_file.suffix == ".h": + _copy_if_missing(src_file, lib_src / src_file.name) + + +def _write_library_json( + benchmark_dir: Path, + core_include: Path, + hooks_include: Path, + version: str, + project_root: Path, +) -> None: + """Write a PlatformIO library.json with CodSpeed build flags.""" + library_json = { + "name": "benchmark", + "version": "0.0.0", + "build": { + "flags": [ + f"-I{core_include}", + f"-I{hooks_include}", + # google benchmark build flags + # -O2 is critical: without it, instrument_hooks_start_benchmark_inline + # doesn't get inlined and shows up as overhead in profiles + "-O2", + "-DNDEBUG", + "-DHAVE_STD_REGEX", + "-DHAVE_STEADY_CLOCK", + "-DBENCHMARK_STATIC_DEFINE", + # CodSpeed instrumentation flags + # https://codspeed.io/docs/benchmarks/cpp#custom-build-systems + "-DCODSPEED_ENABLED", + "-DCODSPEED_ANALYSIS", + f'-DCODSPEED_VERSION=\\"{version}\\"', + f'-DCODSPEED_ROOT_DIR=\\"{project_root}\\"', + '-DCODSPEED_MODE_DISPLAY=\\"simulation\\"', + ], + "includeDir": "include", + }, + } + (benchmark_dir / "library.json").write_text( + json.dumps(library_json, indent=2) + "\n" + ) + + +def setup_codspeed_lib(output_dir: Path) -> None: + """Clone codspeed-cpp and assemble a flat PlatformIO library. + + The resulting library at ``output_dir/google_benchmark/`` contains: + - google_benchmark sources (.cc renamed to .cpp) + - codspeed core sources (prefixed ``codspeed_``) + - instrument-hooks C source (as ``instrument_hooks.c``) + - library.json with all required CodSpeed defines + + Args: + output_dir: Directory to clone the repository into + """ + if not (output_dir / ".git").exists(): + _clone_repo(output_dir) + else: + # Verify the existing checkout matches the pinned SHA + result = subprocess.run( + ["git", "-C", str(output_dir), "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0 or result.stdout.strip() != CODSPEED_CPP_SHA: + print( + f"Stale codspeed-cpp checkout, re-cloning at {CODSPEED_CPP_SHA}", + file=sys.stderr, + ) + shutil.rmtree(output_dir) + _clone_repo(output_dir) + + benchmark_dir = output_dir / GOOGLE_BENCHMARK_SUBDIR + lib_src = benchmark_dir / "src" + core_dir = output_dir / CORE_SUBDIR + core_include = core_dir / "include" + hooks_include = output_dir / INSTRUMENT_HOOKS_INCLUDES + hooks_dist_c = output_dir / INSTRUMENT_HOOKS_DIST + project_root = Path(__file__).resolve().parent.parent + + # 1. Rename .cc → .cpp (PlatformIO doesn't compile .cc) + _rename_cc_to_cpp(lib_src) + + # 2. Merge codspeed core sources into the library + _merge_codspeed_core_into_lib(core_dir / "src", lib_src) + + # 3. Copy instrument-hooks C source (provides instrument_hooks_* symbols) + if hooks_dist_c.exists(): + _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") + + # 4. Write library.json + version = _read_codspeed_version(output_dir / CORE_CMAKE) + _write_library_json( + benchmark_dir, core_include, hooks_include, version, project_root + ) + + # Output JSON config for cpp_benchmark.py + print(json.dumps({"lib_path": str(benchmark_dir)})) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--output-dir", + type=Path, + default=Path(DEFAULT_OUTPUT_DIR), + help=f"Directory to clone codspeed-cpp into (default: {DEFAULT_OUTPUT_DIR})", + ) + args = parser.parse_args() + setup_codspeed_lib(args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/tests/benchmarks/components/.gitignore b/tests/benchmarks/components/.gitignore new file mode 100644 index 00000000000..163bec7b80a --- /dev/null +++ b/tests/benchmarks/components/.gitignore @@ -0,0 +1,2 @@ +/.esphome/ +/secrets.yaml diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp new file mode 100644 index 00000000000..113201dd8a1 --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -0,0 +1,93 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// Helper: encode a message into a buffer and return it. +// Benchmarks encode once in setup, then decode the resulting bytes in a loop. +// This keeps decode benchmarks in sync with the actual protobuf schema — +// hand-encoded byte arrays would silently break when fields change. +template<typename T> static APIBuffer encode_message(const T &msg) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + return buffer; +} + +// --- HelloRequest decode (string + varint fields) --- + +static void Decode_HelloRequest(benchmark::State &state) { + HelloRequest source; + source.client_info = StringRef::from_lit("aioesphomeapi"); + source.api_version_major = 1; + source.api_version_minor = 10; + auto encoded = encode_message(source); + + for (auto _ : state) { + HelloRequest msg; + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } + benchmark::DoNotOptimize(msg.api_version_major); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_HelloRequest); + +// --- SwitchCommandRequest decode (simple command) --- + +static void Decode_SwitchCommandRequest(benchmark::State &state) { + SwitchCommandRequest source; + source.key = 0x12345678; + source.state = true; + auto encoded = encode_message(source); + + for (auto _ : state) { + SwitchCommandRequest msg; + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } + benchmark::DoNotOptimize(msg.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_SwitchCommandRequest); + +// --- LightCommandRequest decode (complex command with many fields) --- + +static void Decode_LightCommandRequest(benchmark::State &state) { + LightCommandRequest source; + source.key = 0x11223344; + source.has_state = true; + source.state = true; + source.has_brightness = true; + source.brightness = 0.8f; + source.has_rgb = true; + source.red = 1.0f; + source.green = 0.5f; + source.blue = 0.2f; + source.has_effect = true; + source.effect = StringRef::from_lit("rainbow"); + auto encoded = encode_message(source); + + for (auto _ : state) { + LightCommandRequest msg; + for (int i = 0; i < kInnerIterations; i++) { + msg.decode(encoded.data(), encoded.size()); + } + benchmark::DoNotOptimize(msg.brightness); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_LightCommandRequest); + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp new file mode 100644 index 00000000000..656c1e17dba --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -0,0 +1,298 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// --- SensorStateResponse (highest frequency message) --- + +static void Encode_SensorStateResponse(benchmark::State &state) { + APIBuffer buffer; + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_SensorStateResponse); + +static void CalculateSize_SensorStateResponse(benchmark::State &state) { + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_SensorStateResponse); + +// Steady state: buffer already allocated from previous iteration +static void CalcAndEncode_SensorStateResponse(benchmark::State &state) { + APIBuffer buffer; + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_SensorStateResponse); + +// Cold path: fresh buffer each iteration (measures heap allocation cost). +// Inner loop still needed to amortize CodSpeed instrumentation overhead. +// Each inner iteration creates a fresh buffer, so this measures +// alloc+calc+encode per item. +static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) { + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_SensorStateResponse_Fresh); + +// --- BinarySensorStateResponse --- + +static void Encode_BinarySensorStateResponse(benchmark::State &state) { + APIBuffer buffer; + BinarySensorStateResponse msg; + msg.key = 0xAABBCCDD; + msg.state = true; + msg.missing_state = false; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_BinarySensorStateResponse); + +// --- HelloResponse (string fields) --- + +static void Encode_HelloResponse(benchmark::State &state) { + APIBuffer buffer; + HelloResponse msg; + msg.api_version_major = 1; + msg.api_version_minor = 10; + msg.server_info = StringRef::from_lit("esphome v2026.3.0"); + msg.name = StringRef::from_lit("living-room-sensor"); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_HelloResponse); + +// --- LightStateResponse (complex multi-field message) --- + +static void Encode_LightStateResponse(benchmark::State &state) { + APIBuffer buffer; + LightStateResponse msg; + msg.key = 0x11223344; + msg.state = true; + msg.brightness = 0.8f; + msg.color_mode = enums::COLOR_MODE_RGB_WHITE; + msg.color_brightness = 1.0f; + msg.red = 1.0f; + msg.green = 0.5f; + msg.blue = 0.2f; + msg.white = 0.0f; + msg.color_temperature = 4000.0f; + msg.cold_white = 0.0f; + msg.warm_white = 0.0f; + msg.effect = StringRef::from_lit("rainbow"); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_LightStateResponse); + +static void CalculateSize_LightStateResponse(benchmark::State &state) { + LightStateResponse msg; + msg.key = 0x11223344; + msg.state = true; + msg.brightness = 0.8f; + msg.color_mode = enums::COLOR_MODE_RGB_WHITE; + msg.color_brightness = 1.0f; + msg.red = 1.0f; + msg.green = 0.5f; + msg.blue = 0.2f; + msg.white = 0.0f; + msg.color_temperature = 4000.0f; + msg.cold_white = 0.0f; + msg.warm_white = 0.0f; + msg.effect = StringRef::from_lit("rainbow"); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_LightStateResponse); + +// --- DeviceInfoResponse (nested submessages: 20 devices + 20 areas) --- + +static DeviceInfoResponse make_device_info_response() { + DeviceInfoResponse msg; + msg.name = StringRef::from_lit("living-room-sensor"); + msg.mac_address = StringRef::from_lit("AA:BB:CC:DD:EE:FF"); + msg.esphome_version = StringRef::from_lit("2026.3.0"); + msg.compilation_time = StringRef::from_lit("Mar 16 2026, 12:00:00"); + msg.model = StringRef::from_lit("esp32-poe-iso"); + msg.manufacturer = StringRef::from_lit("Olimex"); + msg.friendly_name = StringRef::from_lit("Living Room Sensor"); +#ifdef USE_DEVICES + for (uint32_t i = 0; i < ESPHOME_DEVICE_COUNT && i < 20; i++) { + msg.devices[i].device_id = i + 1; + msg.devices[i].name = StringRef::from_lit("device"); + msg.devices[i].area_id = (i % 20) + 1; + } +#endif +#ifdef USE_AREAS + for (uint32_t i = 0; i < ESPHOME_AREA_COUNT && i < 20; i++) { + msg.areas[i].area_id = i + 1; + msg.areas[i].name = StringRef::from_lit("area"); + } +#endif + return msg; +} + +static void CalculateSize_DeviceInfoResponse(benchmark::State &state) { + auto msg = make_device_info_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_DeviceInfoResponse); + +static void Encode_DeviceInfoResponse(benchmark::State &state) { + auto msg = make_device_info_response(); + APIBuffer buffer; + uint32_t total_size = msg.calculate_size(); + buffer.resize(total_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_DeviceInfoResponse); + +// Steady state: buffer already allocated from previous iteration +static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) { + auto msg = make_device_info_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_DeviceInfoResponse); + +// Cold path: fresh buffer each iteration (measures heap allocation cost). +// Inner loop still needed to amortize CodSpeed instrumentation overhead. +// Each inner iteration creates a fresh buffer, so this measures +// alloc+calc+encode per item. +static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { + auto msg = make_device_info_response(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh); + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_proto_varint.cpp b/tests/benchmarks/components/api/bench_proto_varint.cpp new file mode 100644 index 00000000000..0b5ccc2b7de --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_varint.cpp @@ -0,0 +1,133 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/api/proto.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// --- ProtoVarInt::parse() benchmarks --- + +static void ProtoVarInt_Parse_SingleByte(benchmark::State &state) { + uint8_t buf[] = {0x42}; // value = 66 + + for (auto _ : state) { + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ProtoVarInt_Parse_SingleByte); + +static void ProtoVarInt_Parse_TwoByte(benchmark::State &state) { + uint8_t buf[] = {0x80, 0x01}; // value = 128 + + for (auto _ : state) { + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ProtoVarInt_Parse_TwoByte); + +static void ProtoVarInt_Parse_FiveByte(benchmark::State &state) { + uint8_t buf[] = {0xFF, 0xFF, 0xFF, 0xFF, 0x0F}; + + for (auto _ : state) { + ProtoVarIntResult result{}; + for (int i = 0; i < kInnerIterations; i++) { + result = ProtoVarInt::parse(buf, sizeof(buf)); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ProtoVarInt_Parse_FiveByte); + +// --- Varint encoding benchmarks --- + +static void Encode_Varint_Small(benchmark::State &state) { + APIBuffer buffer; + buffer.resize(16); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(42); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_Varint_Small); + +static void Encode_Varint_Large(benchmark::State &state) { + APIBuffer buffer; + buffer.resize(16); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(300); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_Varint_Large); + +static void Encode_Varint_MaxUint32(benchmark::State &state) { + APIBuffer buffer; + buffer.resize(16); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + writer.encode_varint_raw(0xFFFFFFFF); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_Varint_MaxUint32); + +// --- ProtoSize::varint() benchmarks --- + +static void ProtoSize_Varint_Small(benchmark::State &state) { + // Use varying input to prevent constant folding. + // Values 0-127 all take 1 byte but the compiler can't prove that. + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += ProtoSize::varint(static_cast<uint32_t>(i) & 0x7F); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ProtoSize_Varint_Small); + +static void ProtoSize_Varint_Large(benchmark::State &state) { + // Use varying input to prevent constant folding. + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += ProtoSize::varint(0xFFFF0000 | static_cast<uint32_t>(i)); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ProtoSize_Varint_Large); + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/benchmark.yaml b/tests/benchmarks/components/api/benchmark.yaml new file mode 100644 index 00000000000..bfc24d7440d --- /dev/null +++ b/tests/benchmarks/components/api/benchmark.yaml @@ -0,0 +1,114 @@ +# Components needed for API protobuf benchmarks. +# Merged into the base config before validation so all +# dependencies get proper defaults. +# +# esphome: sub-keys are merged into the base config. +esphome: + areas: + - id: area_1 + name: "Area 1" + - id: area_2 + name: "Area 2" + - id: area_3 + name: "Area 3" + - id: area_4 + name: "Area 4" + - id: area_5 + name: "Area 5" + - id: area_6 + name: "Area 6" + - id: area_7 + name: "Area 7" + - id: area_8 + name: "Area 8" + - id: area_9 + name: "Area 9" + - id: area_10 + name: "Area 10" + - id: area_11 + name: "Area 11" + - id: area_12 + name: "Area 12" + - id: area_13 + name: "Area 13" + - id: area_14 + name: "Area 14" + - id: area_15 + name: "Area 15" + - id: area_16 + name: "Area 16" + - id: area_17 + name: "Area 17" + - id: area_18 + name: "Area 18" + - id: area_19 + name: "Area 19" + - id: area_20 + name: "Area 20" + devices: + - id: device_1 + name: "Device 1" + area_id: area_1 + - id: device_2 + name: "Device 2" + area_id: area_2 + - id: device_3 + name: "Device 3" + area_id: area_3 + - id: device_4 + name: "Device 4" + area_id: area_4 + - id: device_5 + name: "Device 5" + area_id: area_5 + - id: device_6 + name: "Device 6" + area_id: area_6 + - id: device_7 + name: "Device 7" + area_id: area_7 + - id: device_8 + name: "Device 8" + area_id: area_8 + - id: device_9 + name: "Device 9" + area_id: area_9 + - id: device_10 + name: "Device 10" + area_id: area_10 + - id: device_11 + name: "Device 11" + area_id: area_11 + - id: device_12 + name: "Device 12" + area_id: area_12 + - id: device_13 + name: "Device 13" + area_id: area_13 + - id: device_14 + name: "Device 14" + area_id: area_14 + - id: device_15 + name: "Device 15" + area_id: area_15 + - id: device_16 + name: "Device 16" + area_id: area_16 + - id: device_17 + name: "Device 17" + area_id: area_17 + - id: device_18 + name: "Device 18" + area_id: area_18 + - id: device_19 + name: "Device 19" + area_id: area_19 + - id: device_20 + name: "Device 20" + area_id: area_20 + +api: +sensor: +binary_sensor: +light: +switch: diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp new file mode 100644 index 00000000000..9bc0c31a159 --- /dev/null +++ b/tests/benchmarks/components/main.cpp @@ -0,0 +1,42 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/logger/logger.h" + +/* +This special main.cpp provides the entry point for Google Benchmark. +It replaces the default ESPHome main with a benchmark runner. + +*/ + +// Auto generated code by esphome +// ========== AUTO GENERATED INCLUDE BLOCK BEGIN =========== +// ========== AUTO GENERATED INCLUDE BLOCK END =========== + +void original_setup() { + // Code-generated App initialization (pre_setup, area/device registration, etc.) + + // ========== AUTO GENERATED CODE BEGIN =========== + // =========== AUTO GENERATED CODE END ============ +} + +void setup() { + // Run auto-generated initialization (App.pre_setup, area/device registration, + // looping_components_.init, etc.) so benchmarks that use App work correctly. + original_setup(); + + // Log functions call global_logger->log_vprintf_() without a null check, + // so we must set up a Logger before any test that triggers logging. + static esphome::logger::Logger test_logger(0); + test_logger.set_log_level(ESPHOME_LOG_LEVEL); + test_logger.pre_setup(); + + int argc = 1; + char arg0[] = "benchmark"; + char *argv[] = {arg0, nullptr}; + ::benchmark::Initialize(&argc, argv); + ::benchmark::RunSpecifiedBenchmarks(); + ::benchmark::Shutdown(); + exit(0); +} + +void loop() {} diff --git a/tests/benchmarks/core/bench_application_loop.cpp b/tests/benchmarks/core/bench_application_loop.cpp new file mode 100644 index 00000000000..dde78ae7390 --- /dev/null +++ b/tests/benchmarks/core/bench_application_loop.cpp @@ -0,0 +1,22 @@ +#include <benchmark/benchmark.h> + +#include "esphome/core/application.h" + +namespace esphome::benchmarks { + +// Benchmark Application::loop() with no registered components. +// App is initialized by original_setup() in main.cpp (code-generated +// pre_setup, area/device registration, looping_components_.init). +// This measures the baseline overhead of the main loop: scheduler, +// timing, before/after loop tasks, and yield_with_select_. +static void ApplicationLoop_Empty(benchmark::State &state) { + // Set loop interval to 0 so yield_with_select_ returns immediately + // instead of sleeping. This benchmarks the loop overhead, not the sleep. + App.set_loop_interval(0); + for (auto _ : state) { + App.loop(); + } +} +BENCHMARK(ApplicationLoop_Empty); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp new file mode 100644 index 00000000000..c6e1e6930eb --- /dev/null +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -0,0 +1,41 @@ +#include <benchmark/benchmark.h> + +#include "esphome/core/helpers.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// --- random_float() --- +// Ported from ol.yaml:148 "Random Float Benchmark" + +static void RandomFloat(benchmark::State &state) { + for (auto _ : state) { + float result = 0.0f; + for (int i = 0; i < kInnerIterations; i++) { + result += random_float(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(RandomFloat); + +// --- random_uint32() --- + +static void RandomUint32(benchmark::State &state) { + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += random_uint32(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(RandomUint32); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/core/bench_logger.cpp b/tests/benchmarks/core/bench_logger.cpp new file mode 100644 index 00000000000..b7e9a1c4eac --- /dev/null +++ b/tests/benchmarks/core/bench_logger.cpp @@ -0,0 +1,54 @@ +#include <benchmark/benchmark.h> + +#include "esphome/core/log.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +static const char *const TAG = "bench"; + +// --- Log a message with no format specifiers (fastest path) --- + +static void Logger_NoFormat(benchmark::State &state) { + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Something happened"); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_NoFormat); + +// --- Log a message with 3 uint32_t format specifiers --- + +static void Logger_3Uint32(benchmark::State &state) { + uint32_t a = 12345, b = 67890, c = 99999; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Values: %" PRIu32 " %" PRIu32 " %" PRIu32, a, b, c); + } + benchmark::DoNotOptimize(a); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_3Uint32); + +// --- Log a message with 3 floats (common for sensor values) --- + +static void Logger_3Float(benchmark::State &state) { + float temp = 23.456f, humidity = 67.89f, pressure = 1013.25f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ESP_LOGW(TAG, "Sensor: %.2f %.1f %.2f", temp, humidity, pressure); + } + benchmark::DoNotOptimize(temp); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Logger_3Float); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp new file mode 100644 index 00000000000..764f17ed735 --- /dev/null +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -0,0 +1,133 @@ +#include <benchmark/benchmark.h> + +#include "esphome/core/scheduler.h" +#include "esphome/core/hal.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// --- Scheduler fast path: no work to do --- + +static void Scheduler_Call_NoWork(benchmark::State &state) { + Scheduler scheduler; + uint32_t now = millis(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.call(now); + } + benchmark::DoNotOptimize(now); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Call_NoWork); + +// --- Scheduler with timers: call() when timers exist but aren't due --- + +static void Scheduler_Call_TimersNotDue(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // Add some timeouts far in the future + for (int i = 0; i < 10; i++) { + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i), 1000000, []() {}); + } + scheduler.process_to_add(); + + uint32_t now = millis(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.call(now); + } + benchmark::DoNotOptimize(now); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Call_TimersNotDue); + +// --- Scheduler with 5 intervals firing every call --- + +static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + int fire_count = 0; + + // Benchmarks the heap-based scheduler dispatch with 5 callbacks firing. + // Uses monotonically increasing fake time so intervals reliably fire every call. + // USE_BENCHMARK ifdef in component.h disables WarnIfComponentBlockingGuard + // (fake now > real millis() would cause underflow in finish()). + // interval=0 would cause an infinite loop (reschedules at same now). + for (int i = 0; i < 5; i++) { + scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i), 1, [&fire_count]() { fire_count++; }); + } + scheduler.process_to_add(); + + uint32_t now = millis() + 100; + + for (auto _ : state) { + scheduler.call(now); + now++; + benchmark::DoNotOptimize(fire_count); + } +} +BENCHMARK(Scheduler_Call_5IntervalsFiring); + +// --- Scheduler: set_timeout registration --- + +static void Scheduler_SetTimeout(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {}); + } + scheduler.process_to_add(); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_SetTimeout); + +// --- Scheduler: set_interval registration --- + +static void Scheduler_SetInterval(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {}); + } + scheduler.process_to_add(); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_SetInterval); + +// --- Scheduler: defer registration (set_timeout with delay=0) --- + +static void Scheduler_Defer(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // defer() is Component::defer which calls set_timeout(delay=0). + // Call set_timeout directly since defer() is protected. + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 0, []() {}); + } + scheduler.process_to_add(); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Defer); + +} // namespace esphome::benchmarks diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 5c81ad374b1..29535d1fd38 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1821,3 +1821,151 @@ def test_component_batching_beta_branch_40_per_batch( all_components.extend(batch_str.split()) assert len(all_components) == 120 assert set(all_components) == set(component_names) + + +# --- should_run_benchmarks tests --- + + +def test_should_run_benchmarks_core_change() -> None: + """Test benchmarks trigger on core C++ file changes.""" + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/core/scheduler.cpp"] + ): + assert determine_jobs.should_run_benchmarks() is True + + +def test_should_run_benchmarks_core_header_change() -> None: + """Test benchmarks trigger on core header changes.""" + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/core/helpers.h"] + ): + assert determine_jobs.should_run_benchmarks() is True + + +def test_should_run_benchmarks_benchmark_infra_change() -> None: + """Test benchmarks trigger on benchmark infrastructure changes.""" + for infra_file in [ + "script/cpp_benchmark.py", + "script/test_helpers.py", + "script/setup_codspeed_lib.py", + ]: + with patch.object(determine_jobs, "changed_files", return_value=[infra_file]): + assert determine_jobs.should_run_benchmarks() is True, ( + f"Expected benchmarks to run for {infra_file}" + ) + + +def test_should_run_benchmarks_benchmark_file_change() -> None: + """Test benchmarks trigger on benchmark file changes.""" + with patch.object( + determine_jobs, + "changed_files", + return_value=["tests/benchmarks/components/api/bench_proto_encode.cpp"], + ): + assert determine_jobs.should_run_benchmarks() is True + + +def test_should_run_benchmarks_core_benchmark_file_change() -> None: + """Test benchmarks trigger on core benchmark file changes.""" + with patch.object( + determine_jobs, + "changed_files", + return_value=["tests/benchmarks/core/bench_scheduler.cpp"], + ): + assert determine_jobs.should_run_benchmarks() is True + + +def test_should_run_benchmarks_benchmarked_component_change(tmp_path: Path) -> None: + """Test benchmarks trigger when a benchmarked component changes.""" + # Create a fake benchmarks directory with an 'api' component + benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api" + benchmarks_dir.mkdir(parents=True) + (benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark") + + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["esphome/components/api/proto.h"], + ), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object( + determine_jobs, + "BENCHMARKS_COMPONENTS_PATH", + "tests/benchmarks/components", + ), + ): + assert determine_jobs.should_run_benchmarks() is True + + +def test_should_run_benchmarks_non_benchmarked_component_change( + tmp_path: Path, +) -> None: + """Test benchmarks do NOT trigger for non-benchmarked component changes.""" + # Create a fake benchmarks directory with only 'api' + benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api" + benchmarks_dir.mkdir(parents=True) + (benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark") + + with ( + patch.object( + determine_jobs, + "changed_files", + return_value=["esphome/components/sensor/__init__.py"], + ), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object( + determine_jobs, + "BENCHMARKS_COMPONENTS_PATH", + "tests/benchmarks/components", + ), + ): + assert determine_jobs.should_run_benchmarks() is False + + +def test_should_run_benchmarks_no_dependency_expansion(tmp_path: Path) -> None: + """Test benchmarks do NOT expand to dependent components. + + Changing 'sensor' should not trigger 'api' benchmarks even if api + depends on sensor. This is intentional — benchmark runs should be + targeted to directly changed components only. + """ + benchmarks_dir = tmp_path / "tests" / "benchmarks" / "components" / "api" + benchmarks_dir.mkdir(parents=True) + (benchmarks_dir / "bench_proto_encode.cpp").write_text("// benchmark") + + with ( + patch.object( + determine_jobs, + "changed_files", + # sensor is a dependency of api, but benchmarks don't expand + return_value=["esphome/components/sensor/sensor.cpp"], + ), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object( + determine_jobs, + "BENCHMARKS_COMPONENTS_PATH", + "tests/benchmarks/components", + ), + ): + assert determine_jobs.should_run_benchmarks() is False + + +def test_should_run_benchmarks_unrelated_change() -> None: + """Test benchmarks do NOT trigger for unrelated changes.""" + with patch.object(determine_jobs, "changed_files", return_value=["README.md"]): + assert determine_jobs.should_run_benchmarks() is False + + +def test_should_run_benchmarks_no_changes() -> None: + """Test benchmarks do NOT trigger with no changes.""" + with patch.object(determine_jobs, "changed_files", return_value=[]): + assert determine_jobs.should_run_benchmarks() is False + + +def test_should_run_benchmarks_with_branch() -> None: + """Test should_run_benchmarks passes branch to changed_files.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.should_run_benchmarks("release") + mock_changed.assert_called_with("release") From 6b91df8d756686cbcd9e575733402be78f02d929 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:05:16 -1000 Subject: [PATCH 0744/2056] [esp32_ble][esp32_ble_server] Inline is_active/is_running and remove STL bloat (#14875) --- esphome/components/esp32_ble/ble.cpp | 2 -- esphome/components/esp32_ble/ble.h | 2 +- .../components/esp32_ble_server/ble_server.cpp | 16 +++++++--------- esphome/components/esp32_ble_server/ble_server.h | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index fee1c546be4..317f8fd11b0 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -81,8 +81,6 @@ void ESP32BLE::disable() { this->state_ = BLE_COMPONENT_STATE_DISABLE; } -bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } - #ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 752ddc9d1ff..82b27894611 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -135,7 +135,7 @@ class ESP32BLE : public Component { void enable(); void disable(); - bool is_active(); + ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } void setup() override; void loop() override; void dump_config() override; diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index ecc53e197f2..be0691dc065 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,7 +7,6 @@ #ifdef USE_ESP32 -#include <algorithm> #include <nvs_flash.h> #include <freertos/FreeRTOSConfig.h> #include <esp_bt_main.h> @@ -39,16 +38,17 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - for (auto &service : this->services_to_start_) { + size_t write_idx = 0; + for (auto *service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service } + // Remove services that have started or are starting + if (!service->is_starting() && !service->is_running()) { + this->services_to_start_[write_idx++] = service; + } } - // Remove services that have been started - this->services_to_start_.erase( - std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), - [](BLEService *service) { return service->is_starting() || service->is_running(); }), - this->services_to_start_.end()); + this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end()); } break; } @@ -91,8 +91,6 @@ void BLEServer::loop() { } } -bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } - bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); } void BLEServer::restart_advertising_() { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index ff7e0044e4c..1b419d2ee45 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv float get_setup_priority() const override; bool can_proceed() override; - bool is_running(); + ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } void set_manufacturer_data(const std::vector<uint8_t> &data) { this->manufacturer_data_ = data; From 77b7201eb88434531cba130c2772c9d40623b833 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:08:45 -1000 Subject: [PATCH 0745/2056] [ci] Run CodSpeed benchmarks on push to dev for baseline (#14899) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1926ad5bf4d..0a03579abc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -316,7 +316,9 @@ jobs: needs: - common - determine-jobs - if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true' + if: >- + (github.event_name == 'push' && github.ref_name == 'dev') || + (github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true') steps: - name: Check out code from GitHub uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From f3409acfa85a8bd7f7cda4d1a76a6deb07f4a0c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:08:58 -1000 Subject: [PATCH 0746/2056] [core] Document EventPool sizing requirement with LockFreeQueue (#14897) --- esphome/core/event_pool.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 99541d4a179..ee8e81225a9 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -13,6 +13,14 @@ namespace esphome { // Events are allocated on first use and reused thereafter, growing to peak usage // @tparam T The type of objects managed by the pool (must have a release() method) // @tparam SIZE The maximum number of objects in the pool (1-255, limited by uint8_t) +// +// SIZING: When paired with a LockFreeQueue<T, Q_SIZE>, the pool SIZE should be +// Q_SIZE - 1 (the queue's actual capacity, since the ring buffer reserves one slot). +// This ensures allocate() returns nullptr before push() can fail, which: +// - Prevents the allocate-succeeds-but-push-fails mismatch that permanently +// leaks a pool slot (the element is never returned to the pool) +// - Avoids needing release() on the producer path after a failed push(), +// preserving the SPSC contract on the internal free list template<class T, uint8_t SIZE> class EventPool { public: EventPool() : total_created_(0) {} From ece235218fe77108170d640b7320337d13c2260a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:27:46 -1000 Subject: [PATCH 0747/2056] [debug][bme680_bsec] Use fnv1_hash_extend to avoid temporary string allocations (#14876) --- esphome/components/bme680_bsec/bme680_bsec.cpp | 2 +- esphome/components/debug/debug_esp32.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 75efb6835ab..bb0417b823d 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -521,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() { } void BME680BSECComponent::load_state_() { - uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_); + uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_); this->bsec_state_ = global_preferences->make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true); if (!this->bsec_state_.load(&this->bsec_state_data_)) { diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aa379599c62..2e04090749e 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -49,7 +49,8 @@ static const size_t REBOOT_MAX_LEN = 24; void DebugComponent::on_shutdown() { auto *component = App.get_current_component(); char buffer[REBOOT_MAX_LEN]{}; - auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); + auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, + fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); if (component != nullptr) { strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1); buffer[REBOOT_MAX_LEN - 1] = '\0'; @@ -66,7 +67,8 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE unsigned reason = esp_reset_reason(); if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) { if (reason == ESP_RST_SW) { - auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); + auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, + fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); char reboot_source[REBOOT_MAX_LEN]{}; if (pref.load(&reboot_source)) { reboot_source[REBOOT_MAX_LEN - 1] = '\0'; From 83484a8828f2bb4936e41d069700c3b12cf2fa49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:38:41 -1000 Subject: [PATCH 0748/2056] [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) --- .../components/esp32_ble_server/ble_characteristic.cpp | 10 +--------- .../components/esp32_ble_server/ble_characteristic.h | 4 ---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 1806354712d..aa82b773ba6 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() { for (auto *descriptor : this->descriptors_) { delete descriptor; // NOLINT(cppcoreguidelines-owning-memory) } - vSemaphoreDelete(this->set_value_lock_); } BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { - this->set_value_lock_ = xSemaphoreCreateBinary(); - xSemaphoreGive(this->set_value_lock_); - this->properties_ = (esp_gatt_char_prop_t) 0; this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); @@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { - xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(buffer); - xSemaphoreGive(this->set_value_lock_); -} +void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); } void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) { this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 72897d1dfbe..062052cdf8c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -16,8 +16,6 @@ #include <esp_gattc_api.h> #include <esp_gatts_api.h> #include <esp_bt_defs.h> -#include <freertos/FreeRTOS.h> -#include <freertos/semphr.h> namespace esphome { namespace esp32_ble_server { @@ -84,8 +82,6 @@ class BLECharacteristic { uint16_t value_read_offset_{0}; std::vector<uint8_t> value_; - SemaphoreHandle_t set_value_lock_; - std::vector<BLEDescriptor *> descriptors_; struct ClientNotificationEntry { From 53bfb02a21487d70b4070a75a2709fcad2e3ff58 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:46:26 -0400 Subject: [PATCH 0749/2056] [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) --- esphome/components/ee895/ee895.cpp | 15 ++--- esphome/components/hdc2010/hdc2010.cpp | 76 +++++++++++--------------- esphome/components/sensor/__init__.py | 4 +- 3 files changed, 40 insertions(+), 55 deletions(-) diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 22f28be9bc3..93e5d4203b6 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -24,7 +24,7 @@ void EE895Component::setup() { this->read(serial_number, 20); crc16_check = (serial_number[19] << 8) + serial_number[18]; - if (crc16_check != calc_crc16_(serial_number, 19)) { + if (crc16_check != calc_crc16_(serial_number, 18)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { address[2] = addr & 0xFF; address[3] = (reg_cnt >> 8) & 0xFF; address[4] = reg_cnt & 0xFF; - crc16 = calc_crc16_(address, 6); + crc16 = calc_crc16_(address, 5); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; this->write(address, 7); @@ -95,7 +95,7 @@ float EE895Component::read_float_() { uint8_t i2c_response[8]; this->read(i2c_response, 8); crc16_check = (i2c_response[7] << 8) + i2c_response[6]; - if (crc16_check != calc_crc16_(i2c_response, 7)) { + if (crc16_check != calc_crc16_(i2c_response, 6)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return 0; @@ -107,12 +107,9 @@ float EE895Component::read_float_() { } uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { - uint8_t crc_check_buf[22]; - for (int i = 0; i < len; i++) { - crc_check_buf[i + 1] = buf[i]; - } - crc_check_buf[0] = this->address_; - return crc16(crc_check_buf, len); + uint8_t addr = this->address_; + uint16_t crc = crc16(&addr, 1); + return crc16(buf, len, crc); } } // namespace ee895 } // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp index c53fdb3f5be..0334b30eeca 100644 --- a/esphome/components/hdc2010/hdc2010.cpp +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -7,50 +7,36 @@ namespace hdc2010 { static const char *const TAG = "hdc2010"; -static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet -static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; -static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; -static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; -static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; -static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; -static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; -static const uint8_t CONFIG = 0x0E; -static const uint8_t MEASUREMENT_CONFIG = 0x0F; +// Register addresses +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; +static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F; + +// REG_MEASUREMENT_CONF (0x0F) bit masks +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger +static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode +static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution +static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution + +// REG_RESET_DRDY_INT_CONF (0x0E) bit masks +static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode void HDC2010Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; - - if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { - ESP_LOGW(TAG, "Initial config instruction error"); - this->status_set_warning(); - return; - } - - // Set measurement mode to temperature and humidity + // Set 14-bit resolution for both sensors and measurement mode to temp + humidity uint8_t config_contents; - this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); - config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); - // Set rate to manual - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x8F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set temperature resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x3F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set humidity resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0xCF; - this->write_bytes(CONFIG, &config_contents, 1); + // Set auto measurement rate to manual (on-demand via MEAS_TRIG) + this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1); + config_contents &= ~AMM_MASK; + this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1); } void HDC2010Component::dump_config() { @@ -67,9 +53,9 @@ void HDC2010Component::dump_config() { void HDC2010Component::update() { // Trigger measurement uint8_t config_contents; - this->read_register(CONFIG, &config_contents, 1); - config_contents |= 0x01; - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents |= MEAS_TRIG; + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); // 1ms delay after triggering the sample set_timeout(1, [this]() { @@ -90,8 +76,8 @@ void HDC2010Component::update() { float HDC2010Component::read_temp() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1); uint16_t temp = encode_uint16(byte[1], byte[0]); return (float) temp * 0.0025177f - 40.0f; @@ -100,8 +86,8 @@ float HDC2010Component::read_temp() { float HDC2010Component::read_humidity() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + this->read_register(REG_HUMIDITY_LOW, &byte[0], 1); + this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1); uint16_t humidity = encode_uint16(byte[1], byte[0]); return (float) humidity * 0.001525879f; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 64d4dc41778..9f3c1484b0c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -406,7 +406,9 @@ QUANTILE_SCHEMA = cv.All( cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), - cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, + cv.Optional(CONF_QUANTILE, default=0.9): cv.float_range( + min=0, min_included=False, max=1 + ), } ), validate_send_first_at, From 62f9bc79c4e4ee929e158cab64087c7bcd289072 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:48:21 -1000 Subject: [PATCH 0750/2056] [ci] Add CodSpeed badge to README (#14901) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8ce8d091dc..16497ee0be2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) +# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/esphome/esphome) <a href="https://esphome.io/"> <picture> From 342020e1d3c53378febedc38819674b10049ea82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:49:24 -1000 Subject: [PATCH 0751/2056] [mqtt] Fix data race on inbound event queue (#14891) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/mqtt/mqtt_backend_esp32.cpp | 34 ++++++--- esphome/components/mqtt/mqtt_backend_esp32.h | 69 +++++++++++-------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 5642fd5f7b6..ab067c44182 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() { void MQTTBackendESP32::loop() { // process new events // handle only 1 message per loop iteration - if (!mqtt_events_.empty()) { - auto &event = mqtt_events_.front(); - mqtt_event_handler_(event); - mqtt_events_.pop(); + Event *event = this->mqtt_event_queue_.pop(); + if (event != nullptr) { + this->mqtt_event_handler_(*event); + this->mqtt_event_pool_.release(event); + } + + // Log dropped inbound events (check is cheap - single atomic load in common case) + uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count(); + if (inbound_dropped > 0) { + ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped); } #if defined(USE_MQTT_IDF_ENQUEUE) @@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args); - // queue event to decouple processing + // queue event to decouple processing from ESP-IDF MQTT task to main loop if (instance) { - auto event = *static_cast<esp_mqtt_event_t *>(event_data); - instance->mqtt_events_.emplace(event); + auto *event = instance->mqtt_event_pool_.allocate(); + if (event == nullptr) { + // Pool exhausted, drop event (counted via queue's dropped counter) + instance->mqtt_event_queue_.increment_dropped_count(); + return; + } + event->populate(*static_cast<esp_mqtt_event_t *>(event_data)); + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + instance->mqtt_event_queue_.push(event); // Wake main loop immediately to process MQTT event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) { break; } } - this_mqtt->mqtt_event_pool_.release(elem); + this_mqtt->mqtt_outbound_pool_.release(elem); } } } bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, size_t len) { - auto *elem = this->mqtt_event_pool_.allocate(); + auto *elem = this->mqtt_outbound_pool_.allocate(); if (!elem) { // Queue is full - increment counter but don't log immediately. @@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, // Use the helper to allocate and copy data if (!elem->set_data(topic, payload, len)) { // Allocation failed, return elem to pool - this->mqtt_event_pool_.release(elem); + this->mqtt_outbound_pool_.release(elem); // Increment counter without logging to avoid cascade effect during memory pressure this->mqtt_queue_.increment_dropped_count(); return false; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 5c4dc413bdc..58d1b29b325 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -5,7 +5,6 @@ #ifdef USE_ESP32 #include <string> -#include <queue> #include <cstring> #include <mqtt_client.h> #include <freertos/FreeRTOS.h> @@ -18,32 +17,39 @@ namespace esphome::mqtt { struct Event { - esp_mqtt_event_id_t event_id; + esp_mqtt_event_id_t event_id{}; std::vector<char> data; - int total_data_len; - int current_data_offset; + int total_data_len{0}; + int current_data_offset{0}; std::string topic; - int msg_id; - bool retain; - int qos; - bool dup; - bool session_present; - esp_mqtt_error_codes_t error_handle; + int msg_id{0}; + bool retain{false}; + int qos{0}; + bool dup{false}; + bool session_present{false}; + esp_mqtt_error_codes_t error_handle{}; - // Construct from esp_mqtt_event_t - // Any pointer values that are unsafe to keep are converted to safe copies - Event(const esp_mqtt_event_t &event) - : event_id(event.event_id), - data(event.data, event.data + event.data_len), - total_data_len(event.total_data_len), - current_data_offset(event.current_data_offset), - topic(event.topic, event.topic_len), - msg_id(event.msg_id), - retain(event.retain), - qos(event.qos), - dup(event.dup), - session_present(event.session_present), - error_handle(*event.error_handle) {} + // Populate from esp_mqtt_event_t + // Copies pointer-based data to owned storage for safe cross-thread transfer + void populate(const esp_mqtt_event_t &event) { + this->event_id = event.event_id; + this->data.assign(event.data, event.data + event.data_len); + this->total_data_len = event.total_data_len; + this->current_data_offset = event.current_data_offset; + this->topic.assign(event.topic, event.topic_len); + this->msg_id = event.msg_id; + this->retain = event.retain; + this->qos = event.qos; + this->dup = event.dup; + this->session_present = event.session_present; + this->error_handle = *event.error_handle; + } + + // Release owned resources for pool reuse (keeps allocated capacity for efficiency) + void release() { + this->data.clear(); + this->topic.clear(); + } }; enum MqttQueueTypeT : uint8_t { @@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend { static constexpr size_t TASK_STACK_SIZE = 3072; static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations static constexpr ssize_t TASK_PRIORITY = 5; - static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend { bool skip_cert_cn_check_{false}; #if defined(USE_MQTT_IDF_ENQUEUE) static void esphome_mqtt_task(void *params); - EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_; + // Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment. + EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_; NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_; TaskHandle_t task_handle_{nullptr}; bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, @@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager<on_message_callback_t> on_message_; CallbackManager<on_publish_user_callback_t> on_publish_; std::string cached_topic_; - std::queue<Event> mqtt_events_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_; + LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_; #if defined(USE_MQTT_IDF_ENQUEUE) uint32_t last_dropped_log_time_{0}; From 0c5f055d45a333733460d5fe10877cefeb1a71ed Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:16:01 +0100 Subject: [PATCH 0752/2056] [core] cpp tests: Allow customizing code generation during tests (#14681) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/core/__init__.py | 6 - esphome/loader.py | 15 +- script/{test_helpers.py => build_helpers.py} | 128 ++++++--- script/cpp_benchmark.py | 18 +- script/cpp_unit_test.py | 13 +- script/determine-jobs.py | 4 +- script/helpers.py | 5 +- tests/benchmarks/components/core/__init__.py | 7 + tests/benchmarks/components/host/__init__.py | 7 + tests/benchmarks/components/json/__init__.py | 7 + .../benchmarks/components/logger/__init__.py | 7 + tests/benchmarks/components/time/__init__.py | 9 + tests/components/README.md | 60 +++- tests/components/core/__init__.py | 8 + tests/components/host/__init__.py | 7 + tests/components/logger/__init__.py | 7 + tests/components/time/__init__.py | 9 + tests/script/test_determine_jobs.py | 2 +- tests/script/test_helpers.py | 22 -- tests/script/test_test_helpers.py | 260 ++++++++++++++++++ tests/testing_helpers.py | 63 +++++ tests/unit_tests/test_loader.py | 99 ++++++- 22 files changed, 667 insertions(+), 96 deletions(-) rename script/{test_helpers.py => build_helpers.py} (78%) create mode 100644 tests/benchmarks/components/core/__init__.py create mode 100644 tests/benchmarks/components/host/__init__.py create mode 100644 tests/benchmarks/components/json/__init__.py create mode 100644 tests/benchmarks/components/logger/__init__.py create mode 100644 tests/benchmarks/components/time/__init__.py create mode 100644 tests/components/core/__init__.py create mode 100644 tests/components/host/__init__.py create mode 100644 tests/components/logger/__init__.py create mode 100644 tests/components/time/__init__.py create mode 100644 tests/script/test_test_helpers.py create mode 100644 tests/testing_helpers.py diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a86478aca19..009fef2f863 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -615,10 +615,6 @@ class EsphomeCore: self.address_cache: AddressCache | None = None # Cached config hash (computed lazily) self._config_hash: int | None = None - # True if compiling for C++ unit tests - self.cpp_testing = False - # Allowlist of components whose to_code should run during C++ testing - self.cpp_testing_codegen: set[str] = set() def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -648,8 +644,6 @@ class EsphomeCore: self.current_component = None self.address_cache = None self._config_hash = None - self.cpp_testing = False - self.cpp_testing_codegen = set() PIN_SCHEMA_REGISTRY.reset() @contextmanager diff --git a/esphome/loader.py b/esphome/loader.py index 5771e074738..68664aaa265 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -71,11 +71,6 @@ class ComponentManifest: @property def to_code(self) -> Callable[[Any], None] | None: - if CORE.cpp_testing: - # During C++ testing, only run to_code for allowlisted components - name = self.module.__package__.rsplit(".", 1)[-1] - if name not in CORE.cpp_testing_codegen: - return None return getattr(self.module, "to_code", None) @property @@ -243,3 +238,13 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None: _COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() _COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) + + +def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None: + """Replace the cached manifest for a component. + + This is an intentionally-supported hook for the C++ test infrastructure + to install ``ComponentManifestOverride`` wrappers. Normal application + code should never call this. + """ + _COMPONENT_CACHE[domain] = manifest diff --git a/script/test_helpers.py b/script/build_helpers.py similarity index 78% rename from script/test_helpers.py rename to script/build_helpers.py index e872bbc5160..1cfae51fca9 100644 --- a/script/test_helpers.py +++ b/script/build_helpers.py @@ -2,21 +2,29 @@ from __future__ import annotations +from collections.abc import Callable import hashlib +import importlib.util import os from pathlib import Path import subprocess import sys -from helpers import get_all_dependencies +from helpers import get_all_dependencies, root_path as _root_path import yaml +# Ensure the repo root is on sys.path so that ``tests.testing_helpers`` and +# override ``__init__.py`` modules can ``from tests.testing_helpers import ...``. +if _root_path not in sys.path: + sys.path.insert(0, _root_path) + from esphome.__main__ import command_compile, parse_args from esphome.config import validate_config from esphome.const import CONF_PLATFORM from esphome.core import CORE -from esphome.loader import get_component +from esphome.loader import get_component, get_platform from esphome.platformio_api import get_idedata +from tests.testing_helpers import ComponentManifestOverride, set_testing_manifest # This must coincide with the version in /platformio.ini PLATFORMIO_GOOGLE_TEST_LIB = "google/googletest@^1.15.2" @@ -32,19 +40,6 @@ ESPHOME_KEY = "esphome" HOST_KEY = "host" LOGGER_KEY = "logger" -# Base config keys that are always present and must not be fully overridden -# by component benchmark.yaml files. esphome: allows sub-key merging. -BASE_CONFIG_KEYS = frozenset({ESPHOME_KEY, HOST_KEY, LOGGER_KEY}) - -# Shared build flag — enables timezone code paths for testing/benchmarking. -USE_TIME_TIMEZONE_FLAG = "-DUSE_TIME_TIMEZONE" - -# Components whose to_code should always run during C++ test/benchmark builds. -# These are the minimal infrastructure components needed for host compilation. -# Note: "core" is the esphome core config module (esphome/core/config.py), -# which registers under package name "core" not "esphome". -BASE_CODEGEN_COMPONENTS = {"core", "host", "logger"} - # Exit codes EXIT_OK = 0 EXIT_SKIPPED = 1 @@ -110,6 +105,8 @@ def get_platform_components(components: list[str], tests_dir: Path) -> list[str] if not domain_dir.is_dir(): continue domain = domain_dir.name + if domain.startswith("__"): + continue domain_module = get_component(domain) if domain_module is None or not domain_module.is_platform_component: raise ValueError( @@ -130,7 +127,8 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict: The ``esphome:`` key is special: its sub-keys are merged into the existing esphome config (e.g. to add ``areas:`` or ``devices:``). - Other base config keys (``host:``, ``logger:``) are not overridable. + Keys already present in the base config (e.g. ``host:``, ``logger:``) + are protected by ``setdefault`` in the caller. Args: components: List of component directory names @@ -139,11 +137,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict: Returns: Merged dict of component configs to add to the base config """ - # Note: components are processed in sorted order. For conflicting keys - # (e.g. two benchmark.yaml files both declaring sensor:), the first - # component alphabetically wins via setdefault(). This is fine for now - # with a single benchmark component (api) but would need a real merge - # strategy if multiple components declare overlapping configs. merged: dict = {} for component in components: yaml_path = tests_dir / component / BENCHMARK_YAML_FILENAME @@ -153,9 +146,6 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict: component_config = yaml.safe_load(f) if component_config and isinstance(component_config, dict): for key, value in component_config.items(): - if key in BASE_CONFIG_KEYS - {ESPHOME_KEY}: - # host: and logger: are not overridable - continue if key == ESPHOME_KEY and isinstance(value, dict): # Merge esphome sub-keys rather than replacing esphome_extra = merged.setdefault(ESPHOME_KEY, {}) @@ -198,11 +188,78 @@ def create_host_config( } +def _wrap_manifest( + comp_name: str, +) -> ComponentManifestOverride | None: + """Wrap a component manifest in a ComponentManifestOverride with to_code suppressed. + + If the manifest is already wrapped or not found, returns None. + Otherwise returns the newly created override after installing it. + """ + if "." in comp_name: + domain, component = comp_name.split(".", maxsplit=1) + manifest = get_platform(domain, component) + cache_key = f"{component}.{domain}" + else: + manifest = get_component(comp_name) + cache_key = comp_name + + if manifest is None or isinstance(manifest, ComponentManifestOverride): + return None + + override = ComponentManifestOverride(manifest) + override.to_code = None # suppress by default + set_testing_manifest(cache_key, override) + return override + + +def load_test_manifest_overrides( + components: list[str], + tests_dir: Path, +) -> None: + """Apply per-component manifest overrides from test ``__init__.py`` files. + + For every component, wraps its manifest and suppresses ``to_code``. + If the component's test directory contains an ``__init__.py`` that + defines ``override_manifest(manifest)``, it is called to customise + the override (e.g. ``manifest.enable_codegen()``). + """ + for comp_name in components: + override = _wrap_manifest(comp_name) + if override is None: + continue + + if "." in comp_name: + domain, component = comp_name.split(".", maxsplit=1) + cache_key = f"{component}.{domain}" + test_init = tests_dir / component / domain / "__init__.py" + else: + cache_key = comp_name + test_init = tests_dir / comp_name / "__init__.py" + + if not test_init.is_file(): + continue + spec = importlib.util.spec_from_file_location( + f"_test_manifest_override.{cache_key}", test_init + ) + if spec is None or spec.loader is None: + continue + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + override_fn = getattr(mod, "override_manifest", None) + if override_fn is not None: + override_fn(override) + + +# Type alias for manifest override loaders +ManifestOverrideLoader = Callable[[list[str]], None] + + def compile_and_get_binary( config: dict, components: list[str], - codegen_components: set[str], tests_dir: Path, + manifest_override_loader: ManifestOverrideLoader, label: str = "build", ) -> tuple[int, str | None]: """Compile an ESPHome configuration and return the binary path. @@ -210,8 +267,8 @@ def compile_and_get_binary( Args: config: ESPHome configuration dict (already created via create_host_config) components: List of components to include in the build - codegen_components: Set of component names whose to_code should run tests_dir: Base directory for test files (used as config_path base) + manifest_override_loader: Callback to apply manifest overrides for components label: Label for log messages (e.g. "unit tests", "benchmarks") Returns: @@ -238,18 +295,21 @@ def compile_and_get_binary( else: config.setdefault(key, value) + # Apply manifest overrides before dependency resolution so that any + # dependency additions made by override_manifest() are picked up. + manifest_override_loader(components) + # Obtain possible dependencies BEFORE validate_config, because # get_all_dependencies calls CORE.reset() which clears build_path. - # Always include 'time' because USE_TIME_TIMEZONE is defined as a build flag, - # which causes core/time.h to include components/time/posix_tz.h. components_with_dependencies: list[str] = sorted( - get_all_dependencies(set(components) | {"time"}, cpp_testing=True) + get_all_dependencies(set(components)) ) + # Apply overrides for any transitively discovered dependencies. + manifest_override_loader(components_with_dependencies) + CORE.config_path = tests_dir / "dummy.yaml" CORE.dashboard = None - CORE.cpp_testing = True - CORE.cpp_testing_codegen = codegen_components # Validate config will expand the above with defaults: config = validate_config(config, {}) @@ -305,7 +365,7 @@ def compile_and_get_binary( def build_and_run( selected_components: list[str], tests_dir: Path, - codegen_components: set[str], + manifest_override_loader: ManifestOverrideLoader, config_prefix: str, friendly_name: str, libraries: str | list[str], @@ -324,7 +384,7 @@ def build_and_run( Args: selected_components: Components to include (directory names in tests_dir) tests_dir: Directory containing test/benchmark files - codegen_components: Components whose to_code should run + manifest_override_loader: Callback to apply manifest overrides for components config_prefix: Prefix for the config name (e.g. "cpptests", "cppbench") friendly_name: Human-readable name for the config libraries: PlatformIO library specification(s) @@ -382,7 +442,7 @@ def build_and_run( ) exit_code, program_path = compile_and_get_binary( - config, components, codegen_components, tests_dir, label + config, components, tests_dir, manifest_override_loader, label ) if exit_code != EXIT_OK or program_path is None: diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index bd92266ea62..a54d3752df6 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -2,18 +2,18 @@ """Build and run C++ benchmarks for ESPHome components using Google Benchmark.""" import argparse +from functools import partial import json import os from pathlib import Path import sys -from helpers import root_path -from test_helpers import ( - BASE_CODEGEN_COMPONENTS, +from build_helpers import ( PLATFORMIO_GOOGLE_BENCHMARK_LIB, - USE_TIME_TIMEZONE_FLAG, build_and_run, + load_test_manifest_overrides, ) +from helpers import root_path # Path to /tests/benchmarks/components BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components" @@ -21,11 +21,6 @@ BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components" # Path to /tests/benchmarks/core (always included, not a component) CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core" -# Additional codegen components beyond the base set. -# json is needed because its to_code adds the ArduinoJson library -# (auto-loaded by api, but cpp_testing suppresses to_code unless listed). -BENCHMARK_CODEGEN_COMPONENTS = BASE_CODEGEN_COMPONENTS | {"json"} - PLATFORMIO_OPTIONS = { "build_unflags": [ "-Os", # remove default size-opt @@ -33,7 +28,6 @@ PLATFORMIO_OPTIONS = { "build_flags": [ "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling - USE_TIME_TIMEZONE_FLAG, "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark @@ -73,7 +67,9 @@ def run_benchmarks(selected_components: list[str], build_only: bool = False) -> return build_and_run( selected_components=selected_components, tests_dir=BENCHMARKS_DIR, - codegen_components=BENCHMARK_CODEGEN_COMPONENTS, + manifest_override_loader=partial( + load_test_manifest_overrides, tests_dir=BENCHMARKS_DIR + ), config_prefix="cppbench", friendly_name="CPP Benchmarks", libraries=benchmark_lib, diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 81c56b82da9..5594d642400 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 import argparse +from functools import partial from pathlib import Path import sys -from helpers import get_all_components, root_path -from test_helpers import ( - BASE_CODEGEN_COMPONENTS, +from build_helpers import ( PLATFORMIO_GOOGLE_TEST_LIB, - USE_TIME_TIMEZONE_FLAG, build_and_run, + load_test_manifest_overrides, ) +from helpers import get_all_components, root_path # Path to /tests/components COMPONENTS_TESTS_DIR: Path = Path(root_path) / "tests" / "components" @@ -21,7 +21,6 @@ PLATFORMIO_OPTIONS = { ], "build_flags": [ "-Og", # optimize for debug - USE_TIME_TIMEZONE_FLAG, "-DESPHOME_DEBUG", # enable debug assertions # Enable the address and undefined behavior sanitizers "-fsanitize=address", @@ -39,7 +38,9 @@ def run_tests(selected_components: list[str]) -> int: return build_and_run( selected_components=selected_components, tests_dir=COMPONENTS_TESTS_DIR, - codegen_components=BASE_CODEGEN_COMPONENTS, + manifest_override_loader=partial( + load_test_manifest_overrides, tests_dir=COMPONENTS_TESTS_DIR + ), config_prefix="cpptests", friendly_name="CPP Unit Tests", libraries=PLATFORMIO_GOOGLE_TEST_LIB, diff --git a/script/determine-jobs.py b/script/determine-jobs.py index ad08f8dce56..9f322387801 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -388,7 +388,7 @@ BENCHMARKS_COMPONENTS_PATH = "tests/benchmarks/components" BENCHMARK_INFRASTRUCTURE_FILES = frozenset( { "script/cpp_benchmark.py", - "script/test_helpers.py", + "script/build_helpers.py", "script/setup_codspeed_lib.py", } ) @@ -402,7 +402,7 @@ def should_run_benchmarks(branch: str | None = None) -> bool: 1. Core C++ files changed (esphome/core/*) 2. A directly changed component has benchmark files (no dependency expansion) 3. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py, - script/test_helpers.py, script/setup_codspeed_lib.py) + script/build_helpers.py, script/setup_codspeed_lib.py) Unlike unit tests, benchmarks do NOT expand to dependent components. Changing ``sensor`` does not trigger ``api`` benchmarks just because diff --git a/script/helpers.py b/script/helpers.py index 9665af70ec7..290dcadf0b9 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -627,14 +627,12 @@ def get_usable_cpu_count() -> int: def get_all_dependencies( - component_names: set[str], cpp_testing: bool = False + component_names: set[str], ) -> set[str]: """Get all dependencies for a set of components. Args: component_names: Set of component names to get dependencies for - cpp_testing: If True, set CORE.cpp_testing so AUTO_LOAD callables that - conditionally include testing-only dependencies work correctly Returns: Set of all components including dependencies and auto-loaded components @@ -652,7 +650,6 @@ def get_all_dependencies( # Reset CORE to ensure clean state CORE.reset() - CORE.cpp_testing = cpp_testing # Set up fake config path for component loading root = Path(__file__).parent.parent diff --git a/tests/benchmarks/components/core/__init__.py b/tests/benchmarks/components/core/__init__.py new file mode 100644 index 00000000000..d676ab669b6 --- /dev/null +++ b/tests/benchmarks/components/core/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # core (esphome/core/config.py) must run its to_code during builds + # because it bootstraps the fundamental application infrastructure. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/host/__init__.py b/tests/benchmarks/components/host/__init__.py new file mode 100644 index 00000000000..f418c25f887 --- /dev/null +++ b/tests/benchmarks/components/host/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # host must run its to_code during builds because it sets up + # the host platform target execution environment. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/json/__init__.py b/tests/benchmarks/components/json/__init__.py new file mode 100644 index 00000000000..56826b64bdf --- /dev/null +++ b/tests/benchmarks/components/json/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # json must run its to_code during benchmark builds because it + # adds the ArduinoJson library dependency needed by the API component. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/logger/__init__.py b/tests/benchmarks/components/logger/__init__.py new file mode 100644 index 00000000000..cfab73e1e51 --- /dev/null +++ b/tests/benchmarks/components/logger/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # logger must run its to_code during builds because it configures + # the logging subsystem used by ESP_LOG* macros. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/time/__init__.py b/tests/benchmarks/components/time/__init__.py new file mode 100644 index 00000000000..7f68003e29d --- /dev/null +++ b/tests/benchmarks/components/time/__init__.py @@ -0,0 +1,9 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + async def to_code(config): + cg.add_build_flag("-DUSE_TIME_TIMEZONE") + + manifest.to_code = to_code diff --git a/tests/components/README.md b/tests/components/README.md index 6da0dadd25e..145a3440d29 100644 --- a/tests/components/README.md +++ b/tests/components/README.md @@ -14,11 +14,63 @@ include the relevant `.cpp` and `.h` test files there. ### Override component code generation for testing -When generating code for testing, ESPHome won't invoke the component's `to_code` function, since most components do not -need to generate configuration code for testing. +During C++ test builds, `to_code` is suppressed for every component by default — most components do not +need to generate configuration code for a unit test binary. -If you do need to generate code to for example configure compilation flags or add libraries, -add the component name to the `CPP_TESTING_CODEGEN_COMPONENTS` allowlist in `script/cpp_unit_test.py`. +#### Manifest overrides + +If your component needs to customise code generation behavior for testing — for example to re-enable +`to_code`, supply a lightweight stub, add a test-only dependency, or change any other manifest attribute — +create an `__init__.py` in your component's test directory and define `override_manifest`: + +**Top-level component** (`tests/components/<component>/__init__.py`): + +```python +from tests.testing_helpers import ComponentManifestOverride + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Re-enable the component's own to_code (needed when the component must + # emit C++ setup code that the test binary depends on at link time). + manifest.enable_codegen() +``` + +Or supply a lightweight stub instead of the real `to_code`: + +```python +from tests.testing_helpers import ComponentManifestOverride + +def override_manifest(manifest: ComponentManifestOverride) -> None: + async def to_code_testing(config): + # Only emit what the C++ tests actually need + pass + + manifest.to_code = to_code_testing + manifest.dependencies = manifest.dependencies + ["some_test_only_dep"] +``` + +**Platform component** (`tests/components/<component>/<domain>/__init__.py`, +e.g. `tests/components/my_sensor/sensor/__init__.py`): + +```python +from tests.testing_helpers import ComponentManifestOverride + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() +``` + +`override_manifest` receives a `ComponentManifestOverride` that wraps the real manifest. +Attribute assignments store an override; reads fall back to the real manifest when no +override is present. + +Key methods: + +| Method | Effect | +|---|---| +| `manifest.enable_codegen()` | Remove the `to_code` suppression, re-enabling code generation | +| `manifest.restore()` | Clear **all** overrides, reverting every attribute to the original | + +The function is called after `to_code` has already been set to `None`, so calling +`enable_codegen()` is a deliberate opt-in. ## Running component unit tests diff --git a/tests/components/core/__init__.py b/tests/components/core/__init__.py new file mode 100644 index 00000000000..34ca4fbe4f0 --- /dev/null +++ b/tests/components/core/__init__.py @@ -0,0 +1,8 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # core (esphome/core/config.py) must run its to_code during C++ test builds + # because it bootstraps the fundamental application infrastructure that all + # components depend on (component registration, event loop, etc.). + manifest.enable_codegen() diff --git a/tests/components/host/__init__.py b/tests/components/host/__init__.py new file mode 100644 index 00000000000..bcb363cdc39 --- /dev/null +++ b/tests/components/host/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # host must run its to_code during C++ test builds because it sets up the + # host platform target, which is the execution environment for all unit tests. + manifest.enable_codegen() diff --git a/tests/components/logger/__init__.py b/tests/components/logger/__init__.py new file mode 100644 index 00000000000..3acfe02748c --- /dev/null +++ b/tests/components/logger/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # logger must run its to_code during C++ test builds because it configures + # the logging subsystem used by ESP_LOG* macros throughout component code. + manifest.enable_codegen() diff --git a/tests/components/time/__init__.py b/tests/components/time/__init__.py new file mode 100644 index 00000000000..7f68003e29d --- /dev/null +++ b/tests/components/time/__init__.py @@ -0,0 +1,9 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + async def to_code(config): + cg.add_build_flag("-DUSE_TIME_TIMEZONE") + + manifest.to_code = to_code diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 29535d1fd38..de239ee0b55 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1846,7 +1846,7 @@ def test_should_run_benchmarks_benchmark_infra_change() -> None: """Test benchmarks trigger on benchmark infrastructure changes.""" for infra_file in [ "script/cpp_benchmark.py", - "script/test_helpers.py", + "script/build_helpers.py", "script/setup_codspeed_lib.py", ]: with patch.object(determine_jobs, "changed_files", return_value=[infra_file]): diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index e3802d2d51f..28f111d7587 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1073,28 +1073,6 @@ def test_get_all_dependencies_platform_component_with_dependencies() -> None: assert result == {"sensor.bthome", "sensor"} -def test_get_all_dependencies_cpp_testing_flag() -> None: - """cpp_testing=True propagates to CORE.cpp_testing during resolution.""" - from esphome.core import CORE - - with ( - patch("esphome.loader.get_component") as mock_get_component, - patch("esphome.loader.get_platform"), - ): - observed: list[bool] = [] - - def capturing_get_component(name: str): - observed.append(CORE.cpp_testing) - - mock_get_component.side_effect = capturing_get_component - - helpers.get_all_dependencies({"some_comp"}, cpp_testing=True) - - assert observed and all(observed), ( - "CORE.cpp_testing should be True during resolution" - ) - - def test_get_components_from_integration_fixtures() -> None: """Test extraction of components from fixture YAML files.""" yaml_content = { diff --git a/tests/script/test_test_helpers.py b/tests/script/test_test_helpers.py new file mode 100644 index 00000000000..467940fc33d --- /dev/null +++ b/tests/script/test_test_helpers.py @@ -0,0 +1,260 @@ +"""Unit tests for script/build_helpers.py manifest override and build helpers.""" + +import os +from pathlib import Path +import sys +import textwrap +from unittest.mock import MagicMock, patch + +import pytest + +# Add the script directory to Python path so we can import build_helpers +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script")) +) + +import build_helpers # noqa: E402 + +from esphome.loader import ComponentManifest # noqa: E402 +from tests.testing_helpers import ComponentManifestOverride # noqa: E402 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_component_manifest(*, to_code=None, dependencies=None) -> ComponentManifest: + mod = MagicMock() + mod.to_code = to_code + mod.DEPENDENCIES = dependencies or [] + return ComponentManifest(mod) + + +# --------------------------------------------------------------------------- +# filter_components_with_files +# --------------------------------------------------------------------------- + + +def test_filter_keeps_components_with_cpp_files(tmp_path: Path) -> None: + comp_dir = tmp_path / "mycomp" + comp_dir.mkdir() + (comp_dir / "mycomp_test.cpp").write_text("") + + result = build_helpers.filter_components_with_files(["mycomp"], tmp_path) + + assert result == ["mycomp"] + + +def test_filter_keeps_components_with_h_files(tmp_path: Path) -> None: + comp_dir = tmp_path / "mycomp" + comp_dir.mkdir() + (comp_dir / "helpers.h").write_text("") + + result = build_helpers.filter_components_with_files(["mycomp"], tmp_path) + + assert result == ["mycomp"] + + +def test_filter_drops_components_without_test_dir(tmp_path: Path) -> None: + result = build_helpers.filter_components_with_files(["nodir"], tmp_path) + + assert result == [] + + +def test_filter_drops_components_with_no_cpp_or_h(tmp_path: Path) -> None: + comp_dir = tmp_path / "mycomp" + comp_dir.mkdir() + (comp_dir / "README.md").write_text("") + + result = build_helpers.filter_components_with_files(["mycomp"], tmp_path) + + assert result == [] + + +# --------------------------------------------------------------------------- +# get_platform_components +# --------------------------------------------------------------------------- + + +def test_get_platform_components_discovers_subdirectory(tmp_path: Path) -> None: + (tmp_path / "bthome" / "sensor").mkdir(parents=True) + + sensor_mod = MagicMock() + sensor_mod.IS_PLATFORM_COMPONENT = True + + with patch( + "build_helpers.get_component", return_value=ComponentManifest(sensor_mod) + ): + result = build_helpers.get_platform_components(["bthome"], tmp_path) + + assert result == ["sensor.bthome"] + + +def test_get_platform_components_skips_pycache(tmp_path: Path) -> None: + (tmp_path / "bthome" / "__pycache__").mkdir(parents=True) + + result = build_helpers.get_platform_components(["bthome"], tmp_path) + + assert result == [] + + +def test_get_platform_components_raises_for_invalid_domain(tmp_path: Path) -> None: + (tmp_path / "bthome" / "notadomain").mkdir(parents=True) + + with ( + patch("build_helpers.get_component", return_value=None), + pytest.raises(ValueError, match="notadomain"), + ): + build_helpers.get_platform_components(["bthome"], tmp_path) + + +# --------------------------------------------------------------------------- +# load_test_manifest_overrides +# --------------------------------------------------------------------------- + + +def test_load_suppresses_to_code(tmp_path: Path) -> None: + """to_code is always set to None before the override is called.""" + + async def real_to_code(config): + pass + + inner = _make_component_manifest(to_code=real_to_code) + + with ( + patch("build_helpers.get_component", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + installed: ComponentManifestOverride = mock_set.call_args[0][1] + + assert installed.to_code is None + + +def test_load_calls_override_fn(tmp_path: Path) -> None: + """override_manifest() in test_init is called with the ComponentManifestOverride.""" + comp_dir = tmp_path / "mycomp" + comp_dir.mkdir() + init_py = comp_dir / "__init__.py" + init_py.write_text( + textwrap.dedent("""\ + def override_manifest(manifest): + manifest.dependencies = ["injected"] + """) + ) + + inner = _make_component_manifest() + override = ComponentManifestOverride(inner) + override.to_code = None + + with ( + patch("build_helpers.get_component", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + installed: ComponentManifestOverride = mock_set.call_args[0][1] + + assert installed.dependencies == ["injected"] + + +def test_load_enable_codegen_in_override(tmp_path: Path) -> None: + """An override_manifest that calls enable_codegen() restores to_code.""" + + async def real_to_code(config): + pass + + comp_dir = tmp_path / "mycomp" + comp_dir.mkdir() + init_py = comp_dir / "__init__.py" + init_py.write_text( + textwrap.dedent("""\ + def override_manifest(manifest): + manifest.enable_codegen() + """) + ) + + inner = _make_component_manifest(to_code=real_to_code) + + with ( + patch("build_helpers.get_component", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + installed: ComponentManifestOverride = mock_set.call_args[0][1] + + assert installed.to_code is real_to_code + + +def test_load_no_override_file(tmp_path: Path) -> None: + """No override file: manifest is wrapped and to_code suppressed, nothing else.""" + inner = _make_component_manifest() + + with ( + patch("build_helpers.get_component", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + + mock_set.assert_called_once() + key, installed = mock_set.call_args[0] + assert key == "mycomp" + assert isinstance(installed, ComponentManifestOverride) + + +def test_load_skips_already_wrapped(tmp_path: Path) -> None: + """Components already wrapped as ComponentManifestOverride are not double-wrapped.""" + inner = _make_component_manifest() + already_wrapped = ComponentManifestOverride(inner) + + with ( + patch("build_helpers.get_component", return_value=already_wrapped), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + + mock_set.assert_not_called() + + +def test_load_skips_platform_component_already_wrapped(tmp_path: Path) -> None: + inner = _make_component_manifest() + already_wrapped = ComponentManifestOverride(inner) + + with ( + patch("build_helpers.get_platform", return_value=already_wrapped), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["sensor.bthome"], tmp_path) + + mock_set.assert_not_called() + + +def test_load_wraps_top_level_component(tmp_path: Path) -> None: + inner = _make_component_manifest() + + with ( + patch("build_helpers.get_component", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["mycomp"], tmp_path) + + mock_set.assert_called_once() + key, installed = mock_set.call_args[0] + assert key == "mycomp" + assert isinstance(installed, ComponentManifestOverride) + assert installed.to_code is None + + +def test_load_wraps_platform_component(tmp_path: Path) -> None: + inner = _make_component_manifest() + + with ( + patch("build_helpers.get_platform", return_value=inner), + patch("build_helpers.set_testing_manifest") as mock_set, + ): + build_helpers.load_test_manifest_overrides(["sensor.bthome"], tmp_path) + + mock_set.assert_called_once() + key, installed = mock_set.call_args[0] + assert key == "bthome.sensor" + assert isinstance(installed, ComponentManifestOverride) + assert installed.to_code is None diff --git a/tests/testing_helpers.py b/tests/testing_helpers.py new file mode 100644 index 00000000000..20b76697a19 --- /dev/null +++ b/tests/testing_helpers.py @@ -0,0 +1,63 @@ +from typing import Any + +from esphome.loader import ComponentManifest, _replace_component_manifest + + +class ComponentManifestOverride: + """Mutable wrapper around ComponentManifest for test-specific attribute overrides. + + When ``tests/components/<name>/__init__.py`` defines:: + + def override_manifest(manifest: ComponentManifestOverride) -> None: + ... + + the function receives an instance of this class wrapping the real component + manifest. Any attribute assignment stores an override; reads fall back to + the underlying ``ComponentManifest`` when no override has been set. + + Example:: + + def override_manifest(manifest: ComponentManifestOverride) -> None: + async def to_code_testing(config): + pass # lightweight no-op stub for C++ unit tests + + manifest.to_code = to_code_testing + manifest.dependencies = manifest.dependencies + ["extra_dep_for_tests"] + """ + + def __init__(self, wrapped: "ComponentManifest") -> None: + object.__setattr__(self, "_wrapped", wrapped) + object.__setattr__(self, "_overrides", {}) + + def __getattr__(self, name: str) -> Any: + overrides: dict[str, Any] = object.__getattribute__(self, "_overrides") + if name in overrides: + return overrides[name] + wrapped: ComponentManifest = object.__getattribute__(self, "_wrapped") + return getattr(wrapped, name) + + def __setattr__(self, name: str, value: Any) -> None: + overrides: dict[str, Any] = object.__getattribute__(self, "_overrides") + overrides[name] = value + + def enable_codegen(self) -> None: + """Remove the to_code suppression, re-enabling code generation for this component. + + Call this from ``override_manifest`` when the component needs its real (or a + custom stub) ``to_code`` to run during C++ unit test builds. + """ + overrides: dict[str, Any] = object.__getattribute__(self, "_overrides") + overrides.pop("to_code", None) + + def restore(self) -> None: + """Clear all overrides, reverting to the wrapped manifest's values.""" + object.__getattribute__(self, "_overrides").clear() + + +def set_testing_manifest(domain: str, manifest: ComponentManifestOverride) -> None: + """Install a testing manifest override into the component cache. + + Called from the C++ unit test infrastructure when a component's test + directory provides an ``override_manifest`` function. + """ + _replace_component_manifest(domain, manifest) diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py index c6d4c4aef04..a42cc5cca73 100644 --- a/tests/unit_tests/test_loader.py +++ b/tests/unit_tests/test_loader.py @@ -2,7 +2,104 @@ from unittest.mock import MagicMock, patch -from esphome.loader import ComponentManifest +from esphome.loader import ComponentManifest, _replace_component_manifest, get_component +from tests.testing_helpers import ComponentManifestOverride + +# --------------------------------------------------------------------------- +# ComponentManifestOverride +# --------------------------------------------------------------------------- + + +def _make_manifest(*, to_code=None, dependencies=None) -> ComponentManifest: + """Return a ComponentManifest backed by a minimal mock module.""" + mod = MagicMock() + mod.to_code = to_code + mod.DEPENDENCIES = dependencies or [] + return ComponentManifest(mod) + + +def test_testing_manifest_delegates_to_wrapped() -> None: + """Unoverridden attributes fall through to the wrapped manifest.""" + inner = _make_manifest(dependencies=["wifi"]) + tm = ComponentManifestOverride(inner) + assert tm.dependencies == ["wifi"] + + +def test_testing_manifest_override_shadows_wrapped() -> None: + """An assigned attribute shadows the wrapped value.""" + inner = _make_manifest(dependencies=["wifi"]) + tm = ComponentManifestOverride(inner) + tm.dependencies = ["ble"] + assert tm.dependencies == ["ble"] + # Wrapped value unchanged + assert inner.dependencies == ["wifi"] + + +def test_testing_manifest_to_code_suppression() -> None: + """Setting to_code=None suppresses code generation.""" + + async def real_to_code(config): + pass + + inner = _make_manifest(to_code=real_to_code) + tm = ComponentManifestOverride(inner) + tm.to_code = None + assert tm.to_code is None + + +def test_testing_manifest_enable_codegen_removes_suppression() -> None: + """enable_codegen() removes the to_code override, restoring the original.""" + + async def real_to_code(config): + pass + + inner = _make_manifest(to_code=real_to_code) + tm = ComponentManifestOverride(inner) + tm.to_code = None + assert tm.to_code is None + + tm.enable_codegen() + assert tm.to_code is real_to_code + + +def test_testing_manifest_enable_codegen_preserves_other_overrides() -> None: + """enable_codegen() only removes to_code; other overrides survive.""" + inner = _make_manifest(dependencies=["wifi"]) + tm = ComponentManifestOverride(inner) + tm.to_code = None + tm.dependencies = ["ble"] + + tm.enable_codegen() + + assert tm.to_code is inner.to_code + assert tm.dependencies == ["ble"] + + +def test_testing_manifest_restore_clears_all_overrides() -> None: + """restore() removes every override, reverting all attributes to wrapped values.""" + + async def real_to_code(config): + pass + + inner = _make_manifest(to_code=real_to_code, dependencies=["wifi"]) + tm = ComponentManifestOverride(inner) + tm.to_code = None + tm.dependencies = ["ble"] + + tm.restore() + + assert tm.to_code is real_to_code + assert tm.dependencies == ["wifi"] + + +def test_replace_component_manifest_installs_override() -> None: + """_replace_component_manifest replaces the cached manifest for a domain.""" + inner = _make_manifest() + override = ComponentManifestOverride(inner) + + _replace_component_manifest("_test_dummy_domain", override) + + assert get_component("_test_dummy_domain") is override def test_component_manifest_resources_with_filter_source_files() -> None: From b9e8da92c734a4b761b9835b6e584d87c1d47eaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 14:19:31 -1000 Subject: [PATCH 0753/2056] [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/core/scheduler.cpp | 48 ++++++----- esphome/core/scheduler.h | 159 ++++++++++++++++++++++++++++++++++--- 2 files changed, 177 insertions(+), 30 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 72b183384e3..db40ede78c6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -212,6 +212,14 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } target->push_back(item); + if (target == &this->to_add_) { + this->to_add_count_increment_(); + } +#ifndef ESPHOME_THREAD_SINGLE + else { + this->defer_count_increment_(); + } +#endif } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, @@ -388,7 +396,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { // safe when called from the main thread. Other threads must not call this method. // If no items, return empty optional - if (this->cleanup_() == 0) + if (!this->cleanup_()) return {}; SchedulerItem *item = this->items_[0]; @@ -422,7 +430,7 @@ void Scheduler::full_cleanup_removed_items_() { this->items_.erase(this->items_.begin() + write, this->items_.end()); // Rebuild the heap structure since items are no longer in heap order std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; + this->to_remove_clear_(); } #ifndef ESPHOME_THREAD_SINGLE @@ -503,7 +511,7 @@ void HOT Scheduler::call(uint32_t now) { // If we still have too many cancelled items, do a full cleanup // This only happens if cancelled items are stuck in the middle/bottom of the heap - if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { + if (this->to_remove_count_() >= MAX_LOGICALLY_DELETED_ITEMS) { this->full_cleanup_removed_items_(); } while (!this->items_.empty()) { @@ -530,7 +538,7 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } } @@ -539,7 +547,7 @@ void HOT Scheduler::call(uint32_t now) { if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } #endif @@ -567,7 +575,7 @@ void HOT Scheduler::call(uint32_t now) { if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(executed_item); continue; } @@ -577,6 +585,7 @@ void HOT Scheduler::call(uint32_t now) { // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(executed_item); + this->to_add_count_increment_(); } else { // Timeout completed - recycle it this->recycle_item_main_loop_(executed_item); @@ -605,6 +614,10 @@ void HOT Scheduler::call(uint32_t now) { #endif } void HOT Scheduler::process_to_add() { + // Fast path: skip lock acquisition when nothing to add. + // Worst case is a one-loop-iteration delay before newly added items are processed. + if (this->to_add_empty_()) + return; LockGuard guard{this->lock_}; for (auto *&it : this->to_add_) { if (is_item_removed_locked_(it)) { @@ -618,17 +631,14 @@ void HOT Scheduler::process_to_add() { std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); + this->to_add_count_clear_(); } -size_t HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just return the current size - // Reading to_remove_ without lock is safe because: - // 1. We only call this from the main thread during call() - // 2. If it's 0, there's definitely nothing to cleanup - // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration - // 4. Not all platforms support atomics, so we accept this race in favor of performance - // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless - if (this->to_remove_ == 0) - return this->items_.size(); +bool HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just check if items exist. + // Uses atomic load on platforms with atomics, falls back to always taking the lock otherwise. + // Worst case is a one-loop-iteration delay in cleanup. + if (this->to_remove_empty_()) + return !this->items_.empty(); // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access @@ -643,10 +653,10 @@ size_t HOT Scheduler::cleanup_() { SchedulerItem *item = this->items_[0]; if (!this->is_item_removed_locked_(item)) break; - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(this->pop_raw_locked_()); } - return this->items_.size(); + return !this->items_.empty(); } Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); @@ -699,7 +709,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry); total_cancelled += heap_cancelled; - this->to_remove_ += heap_cancelled; + this->to_remove_add_(heap_cancelled); } // Cancel items in to_add_ diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 0476513bb99..e545055fcae 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,9 +284,9 @@ class Scheduler { #endif } // Cleanup logically deleted items from the scheduler - // Returns the number of items remaining after cleanup + // Returns true if items remain after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). - size_t cleanup_(); + bool cleanup_(); // Remove and return the front item from the heap as a raw pointer. // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -395,15 +395,9 @@ class Scheduler { // erase() on every pop, which would be O(n). The queue is processed once per loop - // any items added during processing are left for the next loop iteration. - // Snapshot the queue end point - only process items that existed at loop start - // Items added during processing (by callbacks or other threads) run next loop - // No lock needed: single consumer (main loop), stale read just means we process less this iteration - size_t defer_queue_end = this->defer_queue_.size(); - // Fast path: nothing to process, avoid lock entirely. - // Safe without lock: single consumer (main loop) reads front_, and a stale size() read - // from a concurrent push can only make us see fewer items — they'll be processed next loop. - if (this->defer_queue_front_ >= defer_queue_end) + // Worst case is a one-loop-iteration delay before newly deferred items are processed. + if (this->defer_empty_()) return; // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), @@ -412,6 +406,13 @@ class Scheduler { SchedulerItem *item; this->lock_.lock(); + // Reset counter and snapshot queue end under lock + this->defer_count_clear_(); + size_t defer_queue_end = this->defer_queue_.size(); + if (this->defer_queue_front_ >= defer_queue_end) { + this->lock_.unlock(); + return; + } while (this->defer_queue_front_ < defer_queue_end) { // Take ownership of the item, leaving nullptr in the vector slot. // This is safe because: @@ -527,14 +528,150 @@ class Scheduler { Mutex lock_; std::vector<SchedulerItem *> items_; std::vector<SchedulerItem *> to_add_; + +#ifndef ESPHOME_THREAD_SINGLE + // Fast-path counter for process_to_add() to skip taking the lock when there is + // nothing to add. Uses std::atomic on platforms that support it, plain uint32_t + // otherwise. On non-atomic platforms, callers must hold the scheduler lock when + // mutating this counter. Not needed on single-threaded platforms where we can + // check to_add_.empty() directly. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_add_count_{0}; +#else + uint32_t to_add_count_{0}; +#endif +#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path helper for process_to_add() to decide if it can try the lock-free path. + // - On ESPHOME_THREAD_SINGLE: direct container check is safe (no concurrent writers). + // - On ESPHOME_THREAD_MULTI_ATOMICS: performs a lock-free check via to_add_count_. + // - On ESPHOME_THREAD_MULTI_NO_ATOMICS: always returns false to force the caller + // down the locked path; this is NOT a lock-free emptiness check on that platform. + bool to_add_empty_() const { +#ifdef ESPHOME_THREAD_SINGLE + return this->to_add_.empty(); +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + return this->to_add_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + // Increment to_add_count_ (no-op on single-threaded platforms) + void to_add_count_increment_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->to_add_count_++; +#endif + } + + // Reset to_add_count_ (no-op on single-threaded platforms) + void to_add_count_clear_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.store(0, std::memory_order_relaxed); +#else + this->to_add_count_ = 0; +#endif + } + #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path counter for process_defer_queue_() to skip lock when nothing to process. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> defer_count_{0}; +#else + uint32_t defer_count_{0}; +#endif + + bool defer_empty_() const { + // defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path + // ESPHOME_THREAD_MULTI_NO_ATOMICS: always take the lock +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->defer_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + void defer_count_increment_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->defer_count_++; +#endif + } + + void defer_count_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.store(0, std::memory_order_relaxed); +#else + this->defer_count_ = 0; +#endif + } + +#endif /* ESPHOME_THREAD_SINGLE */ + + // Counter for items marked for removal. Incremented cross-thread in cancel_item_locked_(). + // On ESPHOME_THREAD_MULTI_ATOMICS this is read without a lock in the cleanup_() fast path; + // on ESPHOME_THREAD_MULTI_NO_ATOMICS the fast path is disabled so cleanup_() always takes the lock. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_remove_{0}; +#else uint32_t to_remove_{0}; +#endif + + // Lock-free check if there are items to remove (for fast-path in cleanup_) + bool to_remove_empty_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed) == 0; +#elif defined(ESPHOME_THREAD_SINGLE) + return this->to_remove_ == 0; +#else + return false; // Always take the lock path +#endif + } + + void to_remove_add_(uint32_t count) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#else + this->to_remove_ += count; +#endif + } + + void to_remove_decrement_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#else + this->to_remove_--; +#endif + } + + void to_remove_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.store(0, std::memory_order_relaxed); +#else + this->to_remove_ = 0; +#endif + } + + uint32_t to_remove_count_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed); +#else + return this->to_remove_; +#endif + } // Memory pool for recycling SchedulerItem objects to reduce heap churn. // Design decisions: From 8bbfadb59aa39b02156653dee670f6c2990cd506 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:22:31 +0100 Subject: [PATCH 0754/2056] [core] Small improvements (#14884) --- esphome/components/bme68x_bsec2/__init__.py | 4 ++-- script/merge_component_configs.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index 4200b2f0b85..5f0afa9c9f2 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -186,8 +186,8 @@ async def to_code_base(config): cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", - "1.3.40408", - "https://github.com/boschsensortec/Bosch-BME68x-Library", + None, + "https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408", ) cg.add_library( "BSEC2 Software Library", diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 5e98f1fef5a..41bbafcd021 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -384,7 +384,7 @@ def merge_component_configs( # Write merged config output_file.parent.mkdir(parents=True, exist_ok=True) yaml_content = yaml_util.dump(merged_config_data) - output_file.write_text(yaml_content) + output_file.write_text(yaml_content, encoding="utf-8") print(f"Successfully merged {len(component_names)} components into {output_file}") From 37f9541f322d3ccee1c232df8b2a9a503165d41a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 08:14:36 -1000 Subject: [PATCH 0755/2056] [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) --- esphome/components/api/proto.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d1c955b1fb9..44d8f04585d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -442,8 +442,12 @@ class ProtoMessage { virtual const char *message_name() const { return "unknown"; } #endif +#ifndef USE_HOST protected: +#endif // Non-virtual destructor is protected to prevent polymorphic deletion. + // On host platform, made public to allow value-initialization of std::array + // members (e.g. DeviceInfoResponse::devices) without clang errors. ~ProtoMessage() = default; }; From c5d42b05696f0238929447fcbb3db6ba1018ae82 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:18:49 -0400 Subject: [PATCH 0756/2056] [speaker] Fix media playlist using announcement delay (#14889) --- .../components/speaker/media_player/speaker_media_player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9f168f854d8..930373c6fcd 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() { this->media_playlist_.pop_front(); } // Only delay starting playback if moving on the next playlist item or repeating the current item - timeout_ms = this->announcement_playlist_delay_ms_; + timeout_ms = this->media_playlist_delay_ms_; } if (!this->media_playlist_.empty()) { PlaylistItem playlist_item = this->media_playlist_.front(); From 4122fa5dddaea6b354592dd41e20ad6179223b76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 09:58:05 -1000 Subject: [PATCH 0757/2056] [core] Add back deprecated set_internal() for external projects (#14887) --- esphome/core/entity_base.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index cccbafd2c36..723bf54584d 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -100,6 +100,14 @@ class EntityBase { // Get whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } + // Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients + // are NOT notified of the change, the flag may have already been read during setup, and there + // is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead. + ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT " + "notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.", + "2026.3.0") + void set_internal(bool internal) { this->flags_.internal = internal; } + // Check if this object is declared to be disabled by default. // That means that when the device gets added to Home Assistant (or other clients) it should // not be added to the default view by default, and a user action is necessary to manually add it. From 1b70df2c1f8a9a7edbc3b389fe6095ddd7e9f2b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:16:44 -1000 Subject: [PATCH 0758/2056] [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) --- esphome/components/espnow/espnow_component.cpp | 6 ++++-- esphome/components/espnow/espnow_component.h | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 991803d8703..78916891f4d 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 9941e972270..ee4adc1b4d4 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -163,10 +163,14 @@ class ESPNowComponent : public Component { uint8_t own_address_[ESP_NOW_ETH_ALEN]{0}; LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{}; - EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{}; LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{}; - EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment. + EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{}; ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none uint8_t wifi_channel_{0}; From 8caa11dcf45aed498be5ec8ef83611be86b9a547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:43 -1000 Subject: [PATCH 0759/2056] [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) --- .../components/usb_cdc_acm/usb_cdc_acm.cpp | 26 +++++++------------ esphome/components/usb_cdc_acm/usb_cdc_acm.h | 6 ++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index a4c2e6c4a48..253626f0a3e 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { event->data.line_state.dtr = dtr; event->data.line_state.rts = rts; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ event->data.line_coding.parity = parity; event->data.line_coding.data_bits = data_bits; - if (!this->event_queue_.push(event)) { - ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_); - // Return event to pool since we couldn't queue it - this->event_pool_.release(event); - } else { - // Wake main loop immediately to process event + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->event_queue_.push(event); + #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - App.wake_loop_threadsafe(); + App.wake_loop_threadsafe(); #endif - } } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 624f41cf8c6..90c673a89e2 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC LineStateCallback line_state_callback_{nullptr}; // Lock-free queue and event pool for cross-task event passing - EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing both a pool slot leak and an SPSC + // violation on the pool's internal free list. + EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_; LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_; }; From 3bde7ec978f2d691586700ab107cff59a0f014c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:17:59 -1000 Subject: [PATCH 0760/2056] [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) --- esphome/components/usb_host/usb_host.h | 5 ++++- esphome/components/usb_host/usb_host_client.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 2eec0c9699b..dcb76a3a3ba 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -144,7 +144,10 @@ class USBClient : public Component { // Lock-free event queue and pool for USB task to main loop communication // Must be public for access from static callbacks LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue; - EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool; protected: // Process USB events from the queue. Returns true if any work was done. diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2a460d1a077..18d938344c7 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * return; } - // Push to lock-free queue (always succeeds since pool size == queue size) + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. client->event_queue.push(event); // Re-enable component loop to process the queued event From 6154b673c2b6d25d05ec899731d3f4abd171f033 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:18:31 -1000 Subject: [PATCH 0761/2056] [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) --- esphome/components/usb_uart/usb_uart.cpp | 11 +++++------ esphome/components/usb_uart/usb_uart.h | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 3d35f368fb8..7c4358fdbd0 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); memcpy(chunk->data, data, chunk_len); chunk->length = static_cast<uint8_t>(chunk_len); - if (!this->output_queue_.push(chunk)) { - this->output_pool_.release(chunk); - ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len); - break; - } + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + this->output_queue_.push(chunk); data += chunk_len; len -= chunk_len; } @@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { chunk->channel = channel; // Push to lock-free queue for main loop processing - // Push always succeeds because pool size == queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. this->usb_data_queue_.push(chunk); // Re-enable component loop to process the queued data diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 16469df7f60..7a06b04f114 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon // Larger structures first (8+ bytes) RingBuffer input_buffer_; LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_; - EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_; std::function<void()> rx_callback_{}; CdcEps cdc_dev_{}; StringRef debug_prefix_{}; @@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient { // Lock-free data transfer from USB task to main loop static constexpr int USB_DATA_QUEUE_SIZE = 32; LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_; - EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_; + // Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment. + EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_; protected: std::vector<USBUartChannel *> channels_{}; From ccf672d7ee37389dcd85f46f49ca0a5b5471f3d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 12:24:02 -1000 Subject: [PATCH 0762/2056] [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) --- esphome/components/esp32_ble/ble.cpp | 3 ++- esphome/components/esp32_ble/ble.h | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ff9d9bb15aa..fee1c546be4 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) { load_ble_event(event, args...); // Push the event to the queue + // Push always succeeds: pool is sized to queue capacity (N-1), so if + // allocate() returned non-null, the queue is guaranteed to have room. global_ble->ble_events_.push(event); - // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size } // Explicit template instantiations for the friend function diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 04bec3f7858..752ddc9d1ff 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -221,7 +221,13 @@ class ESP32BLE : public Component { // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; - esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 80bd6489cf12c6eadafe02604facb7f841cc5600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:38:41 -1000 Subject: [PATCH 0763/2056] [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) --- .../components/esp32_ble_server/ble_characteristic.cpp | 10 +--------- .../components/esp32_ble_server/ble_characteristic.h | 4 ---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 1806354712d..aa82b773ba6 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() { for (auto *descriptor : this->descriptors_) { delete descriptor; // NOLINT(cppcoreguidelines-owning-memory) } - vSemaphoreDelete(this->set_value_lock_); } BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { - this->set_value_lock_ = xSemaphoreCreateBinary(); - xSemaphoreGive(this->set_value_lock_); - this->properties_ = (esp_gatt_char_prop_t) 0; this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); @@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { - xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(buffer); - xSemaphoreGive(this->set_value_lock_); -} +void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); } void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) { this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 72897d1dfbe..062052cdf8c 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -16,8 +16,6 @@ #include <esp_gattc_api.h> #include <esp_gatts_api.h> #include <esp_bt_defs.h> -#include <freertos/FreeRTOS.h> -#include <freertos/semphr.h> namespace esphome { namespace esp32_ble_server { @@ -84,8 +82,6 @@ class BLECharacteristic { uint16_t value_read_offset_{0}; std::vector<uint8_t> value_; - SemaphoreHandle_t set_value_lock_; - std::vector<BLEDescriptor *> descriptors_; struct ClientNotificationEntry { From be2e4a5278a83155fa3128e68c4b9824c311f1cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 13:49:24 -1000 Subject: [PATCH 0764/2056] [mqtt] Fix data race on inbound event queue (#14891) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/mqtt/mqtt_backend_esp32.cpp | 34 ++++++--- esphome/components/mqtt/mqtt_backend_esp32.h | 69 +++++++++++-------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 5642fd5f7b6..ab067c44182 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() { void MQTTBackendESP32::loop() { // process new events // handle only 1 message per loop iteration - if (!mqtt_events_.empty()) { - auto &event = mqtt_events_.front(); - mqtt_event_handler_(event); - mqtt_events_.pop(); + Event *event = this->mqtt_event_queue_.pop(); + if (event != nullptr) { + this->mqtt_event_handler_(*event); + this->mqtt_event_pool_.release(event); + } + + // Log dropped inbound events (check is cheap - single atomic load in common case) + uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count(); + if (inbound_dropped > 0) { + ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped); } #if defined(USE_MQTT_IDF_ENQUEUE) @@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args); - // queue event to decouple processing + // queue event to decouple processing from ESP-IDF MQTT task to main loop if (instance) { - auto event = *static_cast<esp_mqtt_event_t *>(event_data); - instance->mqtt_events_.emplace(event); + auto *event = instance->mqtt_event_pool_.allocate(); + if (event == nullptr) { + // Pool exhausted, drop event (counted via queue's dropped counter) + instance->mqtt_event_queue_.increment_dropped_count(); + return; + } + event->populate(*static_cast<esp_mqtt_event_t *>(event_data)); + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. + instance->mqtt_event_queue_.push(event); // Wake main loop immediately to process MQTT event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) { break; } } - this_mqtt->mqtt_event_pool_.release(elem); + this_mqtt->mqtt_outbound_pool_.release(elem); } } } bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, size_t len) { - auto *elem = this->mqtt_event_pool_.allocate(); + auto *elem = this->mqtt_outbound_pool_.allocate(); if (!elem) { // Queue is full - increment counter but don't log immediately. @@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, // Use the helper to allocate and copy data if (!elem->set_data(topic, payload, len)) { // Allocation failed, return elem to pool - this->mqtt_event_pool_.release(elem); + this->mqtt_outbound_pool_.release(elem); // Increment counter without logging to avoid cascade effect during memory pressure this->mqtt_queue_.increment_dropped_count(); return false; diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 5c4dc413bdc..58d1b29b325 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -5,7 +5,6 @@ #ifdef USE_ESP32 #include <string> -#include <queue> #include <cstring> #include <mqtt_client.h> #include <freertos/FreeRTOS.h> @@ -18,32 +17,39 @@ namespace esphome::mqtt { struct Event { - esp_mqtt_event_id_t event_id; + esp_mqtt_event_id_t event_id{}; std::vector<char> data; - int total_data_len; - int current_data_offset; + int total_data_len{0}; + int current_data_offset{0}; std::string topic; - int msg_id; - bool retain; - int qos; - bool dup; - bool session_present; - esp_mqtt_error_codes_t error_handle; + int msg_id{0}; + bool retain{false}; + int qos{0}; + bool dup{false}; + bool session_present{false}; + esp_mqtt_error_codes_t error_handle{}; - // Construct from esp_mqtt_event_t - // Any pointer values that are unsafe to keep are converted to safe copies - Event(const esp_mqtt_event_t &event) - : event_id(event.event_id), - data(event.data, event.data + event.data_len), - total_data_len(event.total_data_len), - current_data_offset(event.current_data_offset), - topic(event.topic, event.topic_len), - msg_id(event.msg_id), - retain(event.retain), - qos(event.qos), - dup(event.dup), - session_present(event.session_present), - error_handle(*event.error_handle) {} + // Populate from esp_mqtt_event_t + // Copies pointer-based data to owned storage for safe cross-thread transfer + void populate(const esp_mqtt_event_t &event) { + this->event_id = event.event_id; + this->data.assign(event.data, event.data + event.data_len); + this->total_data_len = event.total_data_len; + this->current_data_offset = event.current_data_offset; + this->topic.assign(event.topic, event.topic_len); + this->msg_id = event.msg_id; + this->retain = event.retain; + this->qos = event.qos; + this->dup = event.dup; + this->session_present = event.session_present; + this->error_handle = *event.error_handle; + } + + // Release owned resources for pool reuse (keeps allocated capacity for efficiency) + void release() { + this->data.clear(); + this->topic.clear(); + } }; enum MqttQueueTypeT : uint8_t { @@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend { static constexpr size_t TASK_STACK_SIZE = 3072; static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations static constexpr ssize_t TASK_PRIORITY = 5; - static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 + static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend { bool skip_cert_cn_check_{false}; #if defined(USE_MQTT_IDF_ENQUEUE) static void esphome_mqtt_task(void *params); - EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_; + // Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment. + EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_; NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_; TaskHandle_t task_handle_{nullptr}; bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, @@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager<on_message_callback_t> on_message_; CallbackManager<on_publish_user_callback_t> on_publish_; std::string cached_topic_; - std::queue<Event> mqtt_events_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_; + LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_; #if defined(USE_MQTT_IDF_ENQUEUE) uint32_t last_dropped_log_time_{0}; From 0fa96b6e1ea76b46a19d8cc4f847dcea3606452a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 14:19:31 -1000 Subject: [PATCH 0765/2056] [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/core/scheduler.cpp | 48 ++++++----- esphome/core/scheduler.h | 159 ++++++++++++++++++++++++++++++++++--- 2 files changed, 177 insertions(+), 30 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 63e1006b03c..8c4ff0ddb50 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -211,6 +211,14 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } target->push_back(item); + if (target == &this->to_add_) { + this->to_add_count_increment_(); + } +#ifndef ESPHOME_THREAD_SINGLE + else { + this->defer_count_increment_(); + } +#endif } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, @@ -387,7 +395,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { // safe when called from the main thread. Other threads must not call this method. // If no items, return empty optional - if (this->cleanup_() == 0) + if (!this->cleanup_()) return {}; SchedulerItem *item = this->items_[0]; @@ -421,7 +429,7 @@ void Scheduler::full_cleanup_removed_items_() { this->items_.erase(this->items_.begin() + write, this->items_.end()); // Rebuild the heap structure since items are no longer in heap order std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_ = 0; + this->to_remove_clear_(); } #ifndef ESPHOME_THREAD_SINGLE @@ -502,7 +510,7 @@ void HOT Scheduler::call(uint32_t now) { // If we still have too many cancelled items, do a full cleanup // This only happens if cancelled items are stuck in the middle/bottom of the heap - if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { + if (this->to_remove_count_() >= MAX_LOGICALLY_DELETED_ITEMS) { this->full_cleanup_removed_items_(); } while (!this->items_.empty()) { @@ -529,7 +537,7 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } } @@ -538,7 +546,7 @@ void HOT Scheduler::call(uint32_t now) { if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_--; + this->to_remove_decrement_(); continue; } #endif @@ -566,7 +574,7 @@ void HOT Scheduler::call(uint32_t now) { if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(executed_item); continue; } @@ -576,6 +584,7 @@ void HOT Scheduler::call(uint32_t now) { // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(executed_item); + this->to_add_count_increment_(); } else { // Timeout completed - recycle it this->recycle_item_main_loop_(executed_item); @@ -604,6 +613,10 @@ void HOT Scheduler::call(uint32_t now) { #endif } void HOT Scheduler::process_to_add() { + // Fast path: skip lock acquisition when nothing to add. + // Worst case is a one-loop-iteration delay before newly added items are processed. + if (this->to_add_empty_()) + return; LockGuard guard{this->lock_}; for (auto *&it : this->to_add_) { if (is_item_removed_locked_(it)) { @@ -617,17 +630,14 @@ void HOT Scheduler::process_to_add() { std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); + this->to_add_count_clear_(); } -size_t HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just return the current size - // Reading to_remove_ without lock is safe because: - // 1. We only call this from the main thread during call() - // 2. If it's 0, there's definitely nothing to cleanup - // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration - // 4. Not all platforms support atomics, so we accept this race in favor of performance - // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless - if (this->to_remove_ == 0) - return this->items_.size(); +bool HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just check if items exist. + // Uses atomic load on platforms with atomics, falls back to always taking the lock otherwise. + // Worst case is a one-loop-iteration delay in cleanup. + if (this->to_remove_empty_()) + return !this->items_.empty(); // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access @@ -642,10 +652,10 @@ size_t HOT Scheduler::cleanup_() { SchedulerItem *item = this->items_[0]; if (!this->is_item_removed_locked_(item)) break; - this->to_remove_--; + this->to_remove_decrement_(); this->recycle_item_main_loop_(this->pop_raw_locked_()); } - return this->items_.size(); + return !this->items_.empty(); } Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); @@ -698,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry); total_cancelled += heap_cancelled; - this->to_remove_ += heap_cancelled; + this->to_remove_add_(heap_cancelled); } // Cancel items in to_add_ diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 0476513bb99..e545055fcae 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,9 +284,9 @@ class Scheduler { #endif } // Cleanup logically deleted items from the scheduler - // Returns the number of items remaining after cleanup + // Returns true if items remain after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). - size_t cleanup_(); + bool cleanup_(); // Remove and return the front item from the heap as a raw pointer. // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -395,15 +395,9 @@ class Scheduler { // erase() on every pop, which would be O(n). The queue is processed once per loop - // any items added during processing are left for the next loop iteration. - // Snapshot the queue end point - only process items that existed at loop start - // Items added during processing (by callbacks or other threads) run next loop - // No lock needed: single consumer (main loop), stale read just means we process less this iteration - size_t defer_queue_end = this->defer_queue_.size(); - // Fast path: nothing to process, avoid lock entirely. - // Safe without lock: single consumer (main loop) reads front_, and a stale size() read - // from a concurrent push can only make us see fewer items — they'll be processed next loop. - if (this->defer_queue_front_ >= defer_queue_end) + // Worst case is a one-loop-iteration delay before newly deferred items are processed. + if (this->defer_empty_()) return; // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), @@ -412,6 +406,13 @@ class Scheduler { SchedulerItem *item; this->lock_.lock(); + // Reset counter and snapshot queue end under lock + this->defer_count_clear_(); + size_t defer_queue_end = this->defer_queue_.size(); + if (this->defer_queue_front_ >= defer_queue_end) { + this->lock_.unlock(); + return; + } while (this->defer_queue_front_ < defer_queue_end) { // Take ownership of the item, leaving nullptr in the vector slot. // This is safe because: @@ -527,14 +528,150 @@ class Scheduler { Mutex lock_; std::vector<SchedulerItem *> items_; std::vector<SchedulerItem *> to_add_; + +#ifndef ESPHOME_THREAD_SINGLE + // Fast-path counter for process_to_add() to skip taking the lock when there is + // nothing to add. Uses std::atomic on platforms that support it, plain uint32_t + // otherwise. On non-atomic platforms, callers must hold the scheduler lock when + // mutating this counter. Not needed on single-threaded platforms where we can + // check to_add_.empty() directly. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_add_count_{0}; +#else + uint32_t to_add_count_{0}; +#endif +#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path helper for process_to_add() to decide if it can try the lock-free path. + // - On ESPHOME_THREAD_SINGLE: direct container check is safe (no concurrent writers). + // - On ESPHOME_THREAD_MULTI_ATOMICS: performs a lock-free check via to_add_count_. + // - On ESPHOME_THREAD_MULTI_NO_ATOMICS: always returns false to force the caller + // down the locked path; this is NOT a lock-free emptiness check on that platform. + bool to_add_empty_() const { +#ifdef ESPHOME_THREAD_SINGLE + return this->to_add_.empty(); +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + return this->to_add_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + // Increment to_add_count_ (no-op on single-threaded platforms) + void to_add_count_increment_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->to_add_count_++; +#endif + } + + // Reset to_add_count_ (no-op on single-threaded platforms) + void to_add_count_clear_() { +#ifdef ESPHOME_THREAD_SINGLE + // No counter needed — to_add_empty_() checks the vector directly +#elif defined(ESPHOME_THREAD_MULTI_ATOMICS) + this->to_add_count_.store(0, std::memory_order_relaxed); +#else + this->to_add_count_ = 0; +#endif + } + #ifndef ESPHOME_THREAD_SINGLE // Single-core platforms don't need the defer queue and save ~32 bytes of RAM // Using std::vector instead of std::deque avoids 512-byte chunked allocations // Index tracking avoids O(n) erase() calls when draining the queue each loop std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) -#endif /* ESPHOME_THREAD_SINGLE */ + + // Fast-path counter for process_defer_queue_() to skip lock when nothing to process. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> defer_count_{0}; +#else + uint32_t defer_count_{0}; +#endif + + bool defer_empty_() const { + // defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path + // ESPHOME_THREAD_MULTI_NO_ATOMICS: always take the lock +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->defer_count_.load(std::memory_order_relaxed) == 0; +#else + return false; +#endif + } + + void defer_count_increment_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.fetch_add(1, std::memory_order_relaxed); +#else + this->defer_count_++; +#endif + } + + void defer_count_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->defer_count_.store(0, std::memory_order_relaxed); +#else + this->defer_count_ = 0; +#endif + } + +#endif /* ESPHOME_THREAD_SINGLE */ + + // Counter for items marked for removal. Incremented cross-thread in cancel_item_locked_(). + // On ESPHOME_THREAD_MULTI_ATOMICS this is read without a lock in the cleanup_() fast path; + // on ESPHOME_THREAD_MULTI_NO_ATOMICS the fast path is disabled so cleanup_() always takes the lock. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + std::atomic<uint32_t> to_remove_{0}; +#else uint32_t to_remove_{0}; +#endif + + // Lock-free check if there are items to remove (for fast-path in cleanup_) + bool to_remove_empty_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed) == 0; +#elif defined(ESPHOME_THREAD_SINGLE) + return this->to_remove_ == 0; +#else + return false; // Always take the lock path +#endif + } + + void to_remove_add_(uint32_t count) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#else + this->to_remove_ += count; +#endif + } + + void to_remove_decrement_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#else + this->to_remove_--; +#endif + } + + void to_remove_clear_() { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + this->to_remove_.store(0, std::memory_order_relaxed); +#else + this->to_remove_ = 0; +#endif + } + + uint32_t to_remove_count_() const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return this->to_remove_.load(std::memory_order_relaxed); +#else + return this->to_remove_; +#endif + } // Memory pool for recycling SchedulerItem objects to reduce heap churn. // Design decisions: From 5cc03d9befb7326fe708e7368778416eddd47253 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:35:21 +1300 Subject: [PATCH 0766/2056] Bump version to 2026.3.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 96295b3fc8e..ea34106f36a 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b3 +PROJECT_NUMBER = 2026.3.0b4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 561a27d2286..4ba8b1a23fc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b3" +__version__ = "2026.3.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 3e845d387a482db37a7c4dfea4692b974ffa9566 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 17 Mar 2026 14:44:17 -1000 Subject: [PATCH 0767/2056] [tests] Fix test_show_logs_serial taking 30s due to unmocked serial port wait (#14903) --- tests/unit_tests/test_main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index b8534611517..5e36c06bb31 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -167,6 +167,13 @@ def mock_run_miniterm() -> Generator[Mock]: yield mock +@pytest.fixture +def mock_wait_for_serial_port() -> Generator[Mock]: + """Mock _wait_for_serial_port for testing.""" + with patch("esphome.__main__._wait_for_serial_port") as mock: + yield mock + + @pytest.fixture def mock_upload_using_esptool() -> Generator[Mock]: """Mock upload_using_esptool for testing.""" @@ -1706,6 +1713,7 @@ def test_show_logs_serial( mock_get_port_type: Mock, mock_check_permissions: Mock, mock_run_miniterm: Mock, + mock_wait_for_serial_port: Mock, ) -> None: """Test show_logs with serial port.""" setup_core(config={"logger": {}}, platform=PLATFORM_ESP32) From 2531fb1a021cf30e238754de5194e77a05be1800 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:12:13 -0400 Subject: [PATCH 0768/2056] [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) --- esphome/components/micro_wake_word/streaming_model.cpp | 1 + esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 47d2c70e138..0ab6cd3772d 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -80,6 +80,7 @@ bool StreamingModel::load_model_() { TfLiteTensor *output = this->interpreter_->output(0); if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) { ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1."); + return false; } if (output->type != kTfLiteUInt8) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 51d52a8af8d..15124e422ff 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -619,6 +619,8 @@ void VoiceAssistant::start_playback_timeout_() { this->cancel_timeout("speaker-timeout"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + if (this->api_client_ == nullptr) + return; api::VoiceAssistantAnnounceFinished msg; msg.success = true; this->api_client_->send_message(msg); From 16c52243416332d18f2ff066c378b2968dbd5083 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:48:43 -0400 Subject: [PATCH 0769/2056] [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) --- esphome/components/apds9960/apds9960.cpp | 8 ++++---- esphome/components/tc74/tc74.cpp | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 260de82d143..a07175f2c9c 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() { uint8_t buf[128]; for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) { - // The ESP's i2c driver has a limited buffer size. - // This way of retrieving the data should be wrong according to the datasheet - // but it seems to work. + // Read in 32-byte chunks due to ESP8266 I2C buffer limit. + // Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF + // and advances its internal pointer after every 4th byte. uint8_t read = std::min(32, fifo_level * 4 - pos); - APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed."); + APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed."); } if (millis() - this->gesture_start_ > 500) { diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index 969ef3671e2..cb58e583dca 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -50,8 +50,9 @@ void TC74Component::read_temperature_() { } } - uint8_t temperature_reg; - if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) { + int8_t temperature_reg; + if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) != + i2c::ERROR_OK) { this->status_set_warning(); return; } From 1d07f37d6215f35980946d5f543d97347d15e797 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:22:28 -0400 Subject: [PATCH 0770/2056] [opentherm] Migrate from legacy timer API to GPTimer API (#14859) --- esphome/components/opentherm/__init__.py | 5 +- esphome/components/opentherm/opentherm.cpp | 89 +++++++--------------- esphome/components/opentherm/opentherm.h | 18 +++-- 3 files changed, 40 insertions(+), 72 deletions(-) diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 36f85a97661..85632d0bf87 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -81,10 +81,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: dict[str, Any]) -> None: if CORE.is_esp32: - # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) - # Provides driver/timer.h header for hardware timer API - # TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h) - include_builtin_idf_component("driver") + include_builtin_idf_component("esp_driver_gptimer") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index cdf89207bcf..97cf83a5aa2 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -8,10 +8,7 @@ #include "opentherm.h" #include "esphome/core/helpers.h" #include <cinttypes> -// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) -// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details. #ifdef USE_ESP32 -#include "driver/timer.h" #include "esp_err.h" #endif #ifdef ESP8266 @@ -33,10 +30,6 @@ OpenTherm *OpenTherm::instance = nullptr; OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout) : in_pin_(in_pin), out_pin_(out_pin), -#ifdef USE_ESP32 - timer_group_(TIMER_GROUP_0), - timer_idx_(TIMER_0), -#endif mode_(OperationMode::IDLE), error_type_(ProtocolErrorType::NO_ERROR), capture_(0), @@ -134,7 +127,12 @@ void IRAM_ATTR OpenTherm::read_() { // period in OpenTherm. } +#ifdef USE_ESP32 +bool IRAM_ATTR OpenTherm::timer_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { + auto *arg = static_cast<OpenTherm *>(user_ctx); +#else bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { +#endif if (arg->mode_ == OperationMode::LISTEN) { if (arg->timeout_counter_ == 0) { arg->mode_ = OperationMode::ERROR_TIMEOUT; @@ -243,67 +241,35 @@ void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) { #ifdef USE_ESP32 bool OpenTherm::init_esp32_timer_() { - // Search for a free timer. Maybe unstable, we'll see. - int cur_timer = 0; - timer_group_t timer_group = TIMER_GROUP_0; - timer_idx_t timer_idx = TIMER_0; - bool timer_found = false; - - for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) { - timer_config_t temp_config; - timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1; - timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2); - - auto err = timer_get_config(timer_group, timer_idx, &temp_config); - if (err == ESP_ERR_INVALID_ARG) { - // Error means timer was not initialized (or other things, but we are careful with our args) - timer_found = true; - break; - } - - ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx); - } - - if (!timer_found) { - ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer."); - return false; - } - - ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx); - this->timer_group_ = timer_group; - this->timer_idx_ = timer_idx; - - timer_config_t const config = { - .alarm_en = TIMER_ALARM_EN, - .counter_en = TIMER_PAUSE, - .intr_type = TIMER_INTR_LEVEL, - .counter_dir = TIMER_COUNT_UP, - .auto_reload = TIMER_AUTORELOAD_EN, - .clk_src = TIMER_SRC_CLK_DEFAULT, - .divider = 80, + // 80MHz / 80 = 1MHz resolution (1µs per tick) + gptimer_config_t config = { + .clk_src = GPTIMER_CLK_SRC_DEFAULT, + .direction = GPTIMER_COUNT_UP, + .resolution_hz = 1000000, }; - esp_err_t result; - - result = timer_init(this->timer_group_, this->timer_idx_, &config); + esp_err_t result = gptimer_new_timer(&config, &this->timer_handle_); if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to init timer. Error: %s", error); + ESP_LOGE(TAG, "Failed to create timer: %s", esp_err_to_name(result)); return false; } - result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + gptimer_event_callbacks_t cbs = { + .on_alarm = OpenTherm::timer_isr, + }; + result = gptimer_register_event_callbacks(this->timer_handle_, &cbs, this); if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error); + ESP_LOGE(TAG, "Failed to register timer callback: %s", esp_err_to_name(result)); + gptimer_del_timer(this->timer_handle_); + this->timer_handle_ = nullptr; return false; } - result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast<bool (*)(void *)>(timer_isr), - this, 0); + result = gptimer_enable(this->timer_handle_); if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error); + ESP_LOGE(TAG, "Failed to enable timer: %s", esp_err_to_name(result)); + gptimer_del_timer(this->timer_handle_); + this->timer_handle_ = nullptr; return false; } @@ -315,12 +281,13 @@ void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { this->timer_error_ = ESP_OK; this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); + this->alarm_config_.alarm_count = alarm_value; + this->timer_error_ = gptimer_set_alarm_action(this->timer_handle_, &this->alarm_config_); if (this->timer_error_ != ESP_OK) { this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR; return; } - this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_); + this->timer_error_ = gptimer_start(this->timer_handle_); if (this->timer_error_ != ESP_OK) { this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR; } @@ -356,12 +323,12 @@ void IRAM_ATTR OpenTherm::stop_timer_() { this->timer_error_ = ESP_OK; this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_); + this->timer_error_ = gptimer_stop(this->timer_handle_); if (this->timer_error_ != ESP_OK) { this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR; return; } - this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + this->timer_error_ = gptimer_set_raw_count(this->timer_handle_, 0); if (this->timer_error_ != ESP_OK) { this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR; } diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index a2c347d0d8e..eb8c5b3ad6c 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -12,12 +12,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) -// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the -// "driver" IDF component dependency. See: -// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4 #ifdef USE_ESP32 -#include "driver/timer.h" +#include "driver/gptimer.h" #endif namespace esphome { @@ -348,7 +344,11 @@ class OpenTherm { const char *operation_mode_to_str(OperationMode mode); const char *message_id_to_str(MessageId id); +#ifdef USE_ESP32 + static bool timer_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx); +#else static bool timer_isr(OpenTherm *arg); +#endif #ifdef ESP8266 static void esp8266_timer_isr(); @@ -361,8 +361,12 @@ class OpenTherm { ISRInternalGPIOPin isr_out_pin_; #ifdef USE_ESP32 - timer_group_t timer_group_; - timer_idx_t timer_idx_; + gptimer_handle_t timer_handle_{nullptr}; + gptimer_alarm_config_t alarm_config_{ + .alarm_count = 0, + .reload_count = 0, + .flags = {.auto_reload_on_alarm = true}, + }; #endif OperationMode mode_; From 3f28ab88cafb51f04cbef929a2b525be03f64c83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:46:18 -1000 Subject: [PATCH 0771/2056] [http_request] Fix data race on update_info_ strings in update task (#14909) --- .../update/http_request_update.cpp | 222 ++++++++++-------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c40590af955..a15dc616753 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -23,6 +23,12 @@ namespace http_request { static const char *const TAG = "http_request.update"; +// Wraps UpdateInfo + error for the task→main-loop handoff. +struct TaskResult { + update::UpdateInfo info; + const LogString *error_str{nullptr}; +}; + static const size_t MAX_READ_SIZE = 256; static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; @@ -77,134 +83,148 @@ void HttpRequestUpdate::update() { void HttpRequestUpdate::update_task(void *params) { HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + // Allocate once — every path below returns via the single defer at the end. + // On failure, error_str is set; on success it is nullptr. + auto *result = new TaskResult(); + auto *info = &result->info; + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); - UPDATE_RETURN; + if (container != nullptr) + container->end(); + result->error_str = LOG_STR("Failed to fetch manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - RAMAllocator<uint8_t> allocator; - uint8_t *data = allocator.allocate(container->content_length); - if (data == nullptr) { - ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer( - [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); - container->end(); - UPDATE_RETURN; - } - - auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, - this_update->request_parent_->get_timeout()); - if (read_result.status != HttpReadStatus::OK) { - if (read_result.status == HttpReadStatus::TIMEOUT) { - ESP_LOGE(TAG, "Timeout reading manifest"); - } else { - ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); + { + RAMAllocator<uint8_t> allocator; + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to allocate memory for manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); }); - allocator.deallocate(data, container->content_length); - container->end(); - UPDATE_RETURN; - } - size_t read_index = container->get_bytes_read(); - size_t content_length = container->content_length; - container->end(); - container.reset(); // Release ownership of the container's shared_ptr - - bool valid = false; - { // Scope to ensure JsonDocument is destroyed before deallocating buffer - valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { - if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || - !root[ESPHOME_F("builds")].is<JsonArray>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, + this_update->request_parent_->get_timeout()); + if (read_result.status != HttpReadStatus::OK) { + if (read_result.status == HttpReadStatus::TIMEOUT) { + ESP_LOGE(TAG, "Timeout reading manifest"); + } else { + ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); } - this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>(); - this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>(); + allocator.deallocate(data, container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to read manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } + size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; - auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); - for (auto build : builds_array) { - if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { + container->end(); + container.reset(); // Release ownership of the container's shared_ptr + + bool valid = false; + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool { + if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || + !root[ESPHOME_F("builds")].is<JsonArray>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { - if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + info->title = root[ESPHOME_F("name")].as<std::string>(); + info->latest_version = root[ESPHOME_F("version")].as<std::string>(); + + auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); + for (auto build : builds_array) { + if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); - if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { + if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); + if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + info->firmware_url = ota[ESPHOME_F("path")].as<std::string>(); + info->md5 = ota[ESPHOME_F("md5")].as<std::string>(); + + if (ota[ESPHOME_F("summary")].is<const char *>()) + info->summary = ota[ESPHOME_F("summary")].as<std::string>(); + if (ota[ESPHOME_F("release_url")].is<const char *>()) + info->release_url = ota[ESPHOME_F("release_url")].as<std::string>(); + + return true; } - this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>(); - this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>(); - - if (ota[ESPHOME_F("summary")].is<const char *>()) - this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>(); - if (ota[ESPHOME_F("release_url")].is<const char *>()) - this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>(); - - return true; } - } - return false; - }); - } - allocator.deallocate(data, content_length); + return false; + }); + } + allocator.deallocate(data, content_length); - if (!valid) { - ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); - UPDATE_RETURN; - } + if (!valid) { + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); + result->error_str = LOG_STR("Failed to parse manifest JSON"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } - // Merge source_url_ and this_update->update_info_.firmware_url - if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this_update->update_info_.firmware_url; - if (path[0] == '/') { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); - this_update->update_info_.firmware_url = domain + path; - } else { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); - this_update->update_info_.firmware_url = domain + path; + // Merge source_url_ and firmware_url + if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) { + std::string path = info->firmware_url; + if (path[0] == '/') { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + info->firmware_url = domain + path; + } else { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + info->firmware_url = domain + path; + } } - } #ifdef ESPHOME_PROJECT_VERSION - this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; + info->current_version = ESPHOME_PROJECT_VERSION; #else - this_update->update_info_.current_version = ESPHOME_VERSION; + info->current_version = ESPHOME_VERSION; #endif - - bool trigger_update_available = false; - - if (this_update->update_info_.latest_version.empty() || - this_update->update_info_.latest_version == this_update->update_info_.current_version) { - this_update->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { - trigger_update_available = true; - } - this_update->state_ = update::UPDATE_STATE_AVAILABLE; } - // Defer to main loop to ensure thread-safe execution of: - // - status_clear_error() performs non-atomic read-modify-write on component_state_ - // - publish_state() triggers API callbacks that write to the shared protobuf buffer - // which can be corrupted if accessed concurrently from task and main loop threads - // - update_available trigger to ensure consistent state when the trigger fires - this_update->defer([this_update, trigger_update_available]() { - this_update->update_info_.has_progress = false; - this_update->update_info_.progress = 0.0f; +defer: + // Release container before vTaskDelete (which doesn't call destructors) + container.reset(); + + // Defer to the main loop so all update_info_ and state_ writes happen on the + // same thread as readers (API, MQTT, web server). This is a single defer for + // both success and error paths to avoid multiple std::function instantiations. + // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. + this_update->defer([this_update, result]() { + if (result->error_str != nullptr) { + this_update->status_set_error(result->error_str); + delete result; + return; + } + + // Determine new state on main loop (avoids extra lambda captures from task) + bool trigger_update_available = false; + update::UpdateState new_state; + if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) { + new_state = update::UPDATE_STATE_NO_UPDATE; + } else { + new_state = update::UPDATE_STATE_AVAILABLE; + if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { + trigger_update_available = true; + } + } + + this_update->update_info_ = std::move(result->info); + this_update->state_ = new_state; + delete result; // Safe: moved-from state is valid for destruction this_update->status_clear_error(); this_update->publish_state(); From 45be290392f902905c4b17b7bf3c998c0ce400fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:47:17 -1000 Subject: [PATCH 0772/2056] [ci] Bump Python to 3.14 in sync-device-classes workflow (#14912) --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index b0d966555be..a71e5ef4cad 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: 3.13 + python-version: "3.14" - name: Install Home Assistant run: | From e88c9ba0661131f39d5ee3c9de77a6e48204b4c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:47:42 -1000 Subject: [PATCH 0773/2056] [core] Inline progmem_read functions on non-ESP8266 platforms (#14913) --- esphome/components/esp32/core.cpp | 3 --- esphome/components/host/core.cpp | 3 --- esphome/components/libretiny/core.cpp | 3 --- esphome/components/rp2040/core.cpp | 7 ------- esphome/components/zephyr/core.cpp | 3 --- esphome/core/hal.h | 9 +++++++++ 6 files changed, 9 insertions(+), 19 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index cba25bca2b2..83bd09b643d 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -53,9 +53,6 @@ void arch_init() { } void HOT arch_feed_wdt() { esp_task_wdt_reset(); } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -const char *progmem_read_ptr(const char *const *addr) { return *addr; } -uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index d5c61ec986c..a662e842ee7 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -58,9 +58,6 @@ void HOT arch_feed_wdt() { // pass } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -const char *progmem_read_ptr(const char *const *addr) { return *addr; } -uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 6bb2d9dcc14..1cfe68e9241 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -54,9 +54,6 @@ void arch_restart() { void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -const char *progmem_read_ptr(const char *const *addr) { return *addr; } -uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 7079cbca155..b7a90006123 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -37,13 +37,6 @@ void arch_init() { void HOT arch_feed_wdt() { watchdog_update(); } -uint8_t progmem_read_byte(const uint8_t *addr) { - return pgm_read_byte(addr); // NOLINT -} -const char *progmem_read_ptr(const char *const *addr) { - return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT -} -uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 1d105a10572..d7c77fdd2ca 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -59,9 +59,6 @@ void arch_feed_wdt() { void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -const char *progmem_read_ptr(const char *const *addr) { return *addr; } -uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } Mutex::Mutex() { auto *mutex = new k_mutex(); diff --git a/esphome/core/hal.h b/esphome/core/hal.h index c2c9b1a325e..03a30b7459f 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -41,8 +41,17 @@ void arch_init(); void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); + +#ifdef USE_ESP8266 +// ESP8266: pgm_read_* does real flash reads on Harvard architecture uint8_t progmem_read_byte(const uint8_t *addr); const char *progmem_read_ptr(const char *const *addr); uint16_t progmem_read_uint16(const uint16_t *addr); +#else +// All other platforms: PROGMEM is a no-op, so these are direct dereferences +inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +inline const char *progmem_read_ptr(const char *const *addr) { return *addr; } +inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } +#endif } // namespace esphome From c9e6c85e6a66cee3dcc867e68b2ecd2589dbdc1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:48:11 -1000 Subject: [PATCH 0774/2056] [scheduler] Inline fast-path checks into header (#14905) --- esphome/core/scheduler.cpp | 69 +++++++++++++++++++++++----- esphome/core/scheduler.h | 92 ++++++++++++++------------------------ 2 files changed, 91 insertions(+), 70 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index db40ede78c6..44fc277ec82 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -454,6 +454,61 @@ void Scheduler::compact_defer_queue_locked_() { // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); } +void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) { + // Process defer queue to guarantee FIFO execution order for deferred items. + // Previously, defer() used the heap which gave undefined order for equal timestamps, + // causing race conditions on multi-core systems (ESP32, BK7200). + // With the defer queue: + // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ + // - Items execute in exact order they were deferred (FIFO guarantee) + // - No deferred items exist in to_add_, so processing order doesn't affect correctness + // Single-core platforms don't use this queue and fall back to the heap-based approach. + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are skipped during execution by should_skip_item_(). + // This is intentional - no memory leak occurs. + // + // We use an index (defer_queue_front_) to track the read position instead of calling + // erase() on every pop, which would be O(n). The queue is processed once per loop - + // any items added during processing are left for the next loop iteration. + + // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), + // recycle each item after re-acquiring the lock for the next iteration (N+1 total). + // The lock is held across: recycle → loop condition → move-out, then released for execution. + SchedulerItem *item; + + this->lock_.lock(); + // Reset counter and snapshot queue end under lock + this->defer_count_clear_(); + size_t defer_queue_end = this->defer_queue_.size(); + if (this->defer_queue_front_ >= defer_queue_end) { + this->lock_.unlock(); + return; + } + while (this->defer_queue_front_ < defer_queue_end) { + // Take ownership of the item, leaving nullptr in the vector slot. + // This is safe because: + // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) + // 3. The lock protects concurrent access, but the nullptr remains until cleanup + item = this->defer_queue_[this->defer_queue_front_]; + this->defer_queue_[this->defer_queue_front_] = nullptr; + this->defer_queue_front_++; + this->lock_.unlock(); + + // Execute callback without holding lock to prevent deadlocks + // if the callback tries to call defer() again + if (!this->should_skip_item_(item)) { + now = this->execute_item_(item, now); + } + + this->lock_.lock(); + this->recycle_item_main_loop_(item); + } + // Clean up the queue (lock already held from last recycle or initial acquisition) + this->cleanup_defer_queue_locked_(); + this->lock_.unlock(); +} #endif /* not ESPHOME_THREAD_SINGLE */ void HOT Scheduler::call(uint32_t now) { @@ -613,11 +668,7 @@ void HOT Scheduler::call(uint32_t now) { } #endif } -void HOT Scheduler::process_to_add() { - // Fast path: skip lock acquisition when nothing to add. - // Worst case is a one-loop-iteration delay before newly added items are processed. - if (this->to_add_empty_()) - return; +void HOT Scheduler::process_to_add_slow_path_() { LockGuard guard{this->lock_}; for (auto *&it : this->to_add_) { if (is_item_removed_locked_(it)) { @@ -633,13 +684,7 @@ void HOT Scheduler::process_to_add() { this->to_add_.clear(); this->to_add_count_clear_(); } -bool HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just check if items exist. - // Uses atomic load on platforms with atomics, falls back to always taking the lock otherwise. - // Worst case is a one-loop-iteration delay in cleanup. - if (this->to_remove_empty_()) - return !this->items_.empty(); - +bool HOT Scheduler::cleanup_slow_path_() { // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index e545055fcae..36c853ad176 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -131,7 +131,18 @@ class Scheduler { // @param now Fresh timestamp from millis() - must not be stale/cached void call(uint32_t now); - void process_to_add(); + // Move items from to_add_ into the main heap. + // IMPORTANT: This method should only be called from the main thread (loop task). + // Inlined: the fast path (nothing to add) is just an atomic load / empty check. + // The lock-free fast path uses to_add_count_ (atomic) or to_add_.empty() + // (single-threaded). This is safe because the main loop is the only thread + // that reads to_add_ without holding lock_; other threads may read it only + // while holding the mutex (e.g. cancel_item_locked_). + inline void HOT process_to_add() { + if (this->to_add_empty_()) + return; + this->process_to_add_slow_path_(); + } // Name storage type discriminator for SchedulerItem // Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs @@ -286,7 +297,20 @@ class Scheduler { // Cleanup logically deleted items from the scheduler // Returns true if items remain after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). - bool cleanup_(); + // Inlined: the fast path (nothing to remove) is just an atomic load + empty check. + // Reading items_.empty() without the lock is safe here because only the main + // loop thread structurally modifies items_ (push/pop/erase). Other threads may + // iterate items_ and mark items removed under lock_, but never change the + // vector's size or data pointer. + inline bool HOT cleanup_() { + if (this->to_remove_empty_()) + return !this->items_.empty(); + return this->cleanup_slow_path_(); + } + // Slow path for cleanup_() when there are items to remove - defined in scheduler.cpp + bool cleanup_slow_path_(); + // Slow path for process_to_add() when there are items to merge - defined in scheduler.cpp + void process_to_add_slow_path_(); // Remove and return the front item from the heap as a raw pointer. // Caller takes ownership and must either recycle or delete the item. // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -376,68 +400,20 @@ class Scheduler { #endif /* ESPHOME_DEBUG_SCHEDULER */ #ifndef ESPHOME_THREAD_SINGLE - // Helper to process defer queue - inline for performance in hot path - inline void process_defer_queue_(uint32_t &now) { - // Process defer queue first to guarantee FIFO execution order for deferred items. - // Previously, defer() used the heap which gave undefined order for equal timestamps, - // causing race conditions on multi-core systems (ESP32, BK7200). - // With the defer queue: - // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ - // - Items execute in exact order they were deferred (FIFO guarantee) - // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // Single-core platforms don't use this queue and fall back to the heap-based approach. - // - // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still - // processed here. They are skipped during execution by should_skip_item_(). - // This is intentional - no memory leak occurs. - // - // We use an index (defer_queue_front_) to track the read position instead of calling - // erase() on every pop, which would be O(n). The queue is processed once per loop - - // any items added during processing are left for the next loop iteration. - + // Process defer queue for FIFO execution of deferred items. + // IMPORTANT: This method should only be called from the main thread (loop task). + // Inlined: the fast path (nothing deferred) is just an atomic load check. + inline void HOT process_defer_queue_(uint32_t &now) { // Fast path: nothing to process, avoid lock entirely. // Worst case is a one-loop-iteration delay before newly deferred items are processed. if (this->defer_empty_()) return; - - // Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total), - // recycle each item after re-acquiring the lock for the next iteration (N+1 total). - // The lock is held across: recycle → loop condition → move-out, then released for execution. - SchedulerItem *item; - - this->lock_.lock(); - // Reset counter and snapshot queue end under lock - this->defer_count_clear_(); - size_t defer_queue_end = this->defer_queue_.size(); - if (this->defer_queue_front_ >= defer_queue_end) { - this->lock_.unlock(); - return; - } - while (this->defer_queue_front_ < defer_queue_end) { - // Take ownership of the item, leaving nullptr in the vector slot. - // This is safe because: - // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_) - // 3. The lock protects concurrent access, but the nullptr remains until cleanup - item = this->defer_queue_[this->defer_queue_front_]; - this->defer_queue_[this->defer_queue_front_] = nullptr; - this->defer_queue_front_++; - this->lock_.unlock(); - - // Execute callback without holding lock to prevent deadlocks - // if the callback tries to call defer() again - if (!this->should_skip_item_(item)) { - now = this->execute_item_(item, now); - } - - this->lock_.lock(); - this->recycle_item_main_loop_(item); - } - // Clean up the queue (lock already held from last recycle or initial acquisition) - this->cleanup_defer_queue_locked_(); - this->lock_.unlock(); + this->process_defer_queue_slow_path_(now); } + // Slow path for process_defer_queue_() - defined in scheduler.cpp + void process_defer_queue_slow_path_(uint32_t &now); + // Helper to cleanup defer_queue_ after processing. // Keeps the common clear() path inline, outlines the rare compaction to keep // cold code out of the hot instruction cache lines. From 9a80c980cb91b783387cfd1552284e5f36d82ba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:48:26 -1000 Subject: [PATCH 0775/2056] [scheduler] Early exit cancel path after first match (#14902) --- esphome/core/scheduler.cpp | 27 +++++++++++++++++------ esphome/core/scheduler.h | 21 +++++++++++++----- tests/benchmarks/core/bench_scheduler.cpp | 12 +++++++++- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 44fc277ec82..51cbfb208ea 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -138,7 +138,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Still need to cancel existing timer if we have a name/id if (!skip_cancel) { LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, + /* find_first= */ true); } return; } @@ -209,7 +210,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, + /* find_first= */ true); } target->push_back(item); if (target == &this->to_add_) { @@ -723,13 +725,20 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { LockGuard guard{this->lock_}; + // Public cancel path uses default find_first=false to cancel ALL matches because + // DelayAction parallel mode (skip_cancel=true) can create multiple items with the same key. return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, match_retry); } -// Helper to cancel items - must be called with lock held +// Helper to cancel matching items - must be called with lock held. +// When find_first=true, stops after the first match and exits across containers +// (used by set_timer_common_ where cancel-before-add guarantees at most one match). +// When find_first=false, cancels ALL matches across all containers (needed for +// public cancel path where DelayAction parallel mode can create duplicates). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry) { + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, + bool find_first) { // Early return if static string name is invalid if (name_type == NameType::STATIC_STRING && static_name == nullptr) { return false; @@ -741,7 +750,9 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { total_cancelled += this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_type, static_name, - hash_or_id, type, match_retry); + hash_or_id, type, match_retry, find_first); + if (find_first && total_cancelled > 0) + return true; } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -752,14 +763,16 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // Only the main loop in call() should recycle items after execution completes. if (!this->items_.empty()) { size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, - hash_or_id, type, match_retry); + hash_or_id, type, match_retry, find_first); total_cancelled += heap_cancelled; this->to_remove_add_(heap_cancelled); + if (find_first && total_cancelled > 0) + return true; } // Cancel items in to_add_ total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_type, static_name, - hash_or_id, type, match_retry); + hash_or_id, type, match_retry, find_first); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 36c853ad176..1e44f41da84 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -320,10 +320,14 @@ class Scheduler { SchedulerItem *get_item_from_pool_locked_(); private: - // Helper to cancel items - must be called with lock held + // Helper to cancel matching items - must be called with lock held. + // When find_first=true, stops after the first match (used by set_timer_common_ where + // the cancel-before-add invariant guarantees at most one match). + // When find_first=false (default), cancels ALL matches (needed for DelayAction parallel + // mode where skip_cancel=true allows multiple items with the same key). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id bool cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry = false); + SchedulerItem::Type type, bool match_retry = false, bool find_first = false); // Common implementation for cancel operations - handles locking bool cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, @@ -483,18 +487,25 @@ class Scheduler { #endif } - // Helper to mark matching items in a container as removed + // Helper to mark matching items in a container as removed. + // When find_first=true, stops after the first match (used by set_timer_common_ where + // the cancel-before-add invariant guarantees at most one match). + // When find_first=false, marks ALL matches (needed for public cancel path where + // DelayAction parallel mode with skip_cancel=true can create multiple items with the same key). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id - // Returns the number of items marked for removal + // Returns the number of items marked for removal. // IMPORTANT: Must be called with scheduler lock held __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry) { + SchedulerItem::Type type, bool match_retry, + bool find_first = false) { size_t count = 0; for (auto *item : container) { if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item, true); + if (find_first) + return 1; count++; } } diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 764f17ed735..9357734cc8e 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -99,11 +99,21 @@ BENCHMARK(Scheduler_SetTimeout); static void Scheduler_SetInterval(benchmark::State &state) { Scheduler scheduler; Component dummy_component; + // Number of distinct interval keys; controls how many unique timers exist + // simultaneously and the drain cadence for process_to_add(). + static constexpr int kKeyCount = 5; for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {}); + scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kKeyCount), 1000, []() {}); + // Drain to_add_ periodically to reflect production behavior where + // process_to_add() runs each main loop iteration. Without this, + // cancelled items accumulate in to_add_ causing O(n²) scan cost. + if ((i + 1) % kKeyCount == 0) { + scheduler.process_to_add(); + } } + // Final drain in case kInnerIterations is not a multiple of 5 scheduler.process_to_add(); benchmark::DoNotOptimize(scheduler); } From 6cf32af33f4ec92d0b06939129be8c7cb254cb59 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:57:53 -0400 Subject: [PATCH 0776/2056] [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) --- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 263603704ac..c9fe3a2e6ef 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { this->sg_recv_data_state_ = FRAME_DATA_LEN_H; break; case FRAME_DATA_LEN_H: - if (value <= 4) { - this->sg_data_len_ = value * 256; + if (value == 0) { this->sg_frame_buf_[4] = value; this->sg_recv_data_state_ = FRAME_DATA_LEN_L; } else { - this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value); } break; case FRAME_DATA_LEN_L: - this->sg_data_len_ += value; - if (this->sg_data_len_ > 32) { + this->sg_data_len_ = value; + if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) { ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value); this->sg_data_len_ = 0; this->sg_recv_data_state_ = FRAME_IDLE; @@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) { } break; case FRAME_DATA_BYTES: - this->sg_data_len_ -= 1; this->sg_frame_buf_[this->sg_frame_len_++] = value; - if (this->sg_data_len_ <= 0) { + if (--this->sg_data_len_ == 0) { this->sg_recv_data_state_ = FRAME_DATA_CRC; } break; From 91e66cfd9d7e3a31ca4a0923463dcc107de321d1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:28:13 -0400 Subject: [PATCH 0777/2056] [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) --- esphome/components/gree/gree.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 8a9f264932c..732ebd9632e 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -87,19 +87,12 @@ void GreeClimate::transmit_state() { // Calculate the checksum if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); - } else if (this->model_ == GREE_YAG) { + } else { remote_state[7] = ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + ((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) & 0x0F) << 4); - } else { - remote_state[7] = - ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + - ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & - 0x0F) - << 4) | - (remote_state[7] & 0x0F); } auto transmit = this->transmitter_->transmit(); From 4cb93d4df8c3661158ffb7e96966eccdb08bedad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:46:26 -0400 Subject: [PATCH 0778/2056] [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) --- esphome/components/ee895/ee895.cpp | 15 ++--- esphome/components/hdc2010/hdc2010.cpp | 76 +++++++++++--------------- esphome/components/sensor/__init__.py | 10 ++-- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 22f28be9bc3..93e5d4203b6 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -24,7 +24,7 @@ void EE895Component::setup() { this->read(serial_number, 20); crc16_check = (serial_number[19] << 8) + serial_number[18]; - if (crc16_check != calc_crc16_(serial_number, 19)) { + if (crc16_check != calc_crc16_(serial_number, 18)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { address[2] = addr & 0xFF; address[3] = (reg_cnt >> 8) & 0xFF; address[4] = reg_cnt & 0xFF; - crc16 = calc_crc16_(address, 6); + crc16 = calc_crc16_(address, 5); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; this->write(address, 7); @@ -95,7 +95,7 @@ float EE895Component::read_float_() { uint8_t i2c_response[8]; this->read(i2c_response, 8); crc16_check = (i2c_response[7] << 8) + i2c_response[6]; - if (crc16_check != calc_crc16_(i2c_response, 7)) { + if (crc16_check != calc_crc16_(i2c_response, 6)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return 0; @@ -107,12 +107,9 @@ float EE895Component::read_float_() { } uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { - uint8_t crc_check_buf[22]; - for (int i = 0; i < len; i++) { - crc_check_buf[i + 1] = buf[i]; - } - crc_check_buf[0] = this->address_; - return crc16(crc_check_buf, len); + uint8_t addr = this->address_; + uint16_t crc = crc16(&addr, 1); + return crc16(buf, len, crc); } } // namespace ee895 } // namespace esphome diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp index c53fdb3f5be..0334b30eeca 100644 --- a/esphome/components/hdc2010/hdc2010.cpp +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -7,50 +7,36 @@ namespace hdc2010 { static const char *const TAG = "hdc2010"; -static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet -static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; -static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; -static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; -static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; -static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; -static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; -static const uint8_t CONFIG = 0x0E; -static const uint8_t MEASUREMENT_CONFIG = 0x0F; +// Register addresses +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; +static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F; + +// REG_MEASUREMENT_CONF (0x0F) bit masks +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger +static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode +static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution +static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution + +// REG_RESET_DRDY_INT_CONF (0x0E) bit masks +static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode void HDC2010Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; - - if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { - ESP_LOGW(TAG, "Initial config instruction error"); - this->status_set_warning(); - return; - } - - // Set measurement mode to temperature and humidity + // Set 14-bit resolution for both sensors and measurement mode to temp + humidity uint8_t config_contents; - this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); - config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); - // Set rate to manual - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x8F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set temperature resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x3F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set humidity resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0xCF; - this->write_bytes(CONFIG, &config_contents, 1); + // Set auto measurement rate to manual (on-demand via MEAS_TRIG) + this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1); + config_contents &= ~AMM_MASK; + this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1); } void HDC2010Component::dump_config() { @@ -67,9 +53,9 @@ void HDC2010Component::dump_config() { void HDC2010Component::update() { // Trigger measurement uint8_t config_contents; - this->read_register(CONFIG, &config_contents, 1); - config_contents |= 0x01; - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents |= MEAS_TRIG; + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); // 1ms delay after triggering the sample set_timeout(1, [this]() { @@ -90,8 +76,8 @@ void HDC2010Component::update() { float HDC2010Component::read_temp() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1); uint16_t temp = encode_uint16(byte[1], byte[0]); return (float) temp * 0.0025177f - 40.0f; @@ -100,8 +86,8 @@ float HDC2010Component::read_temp() { float HDC2010Component::read_humidity() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + this->read_register(REG_HUMIDITY_LOW, &byte[0], 1); + this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1); uint16_t humidity = encode_uint16(byte[1], byte[0]); return (float) humidity * 0.001525879f; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 4be6ed1b841..02db381ff4c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -403,10 +403,12 @@ async def filter_out_filter_to_code(config, filter_id): QUANTILE_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, - cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, - cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float, + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535), + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535), + cv.Optional(CONF_QUANTILE, default=0.9): cv.float_range( + min=0, min_included=False, max=1 + ), } ), validate_send_first_at, From 98d3dce6727f39dd0a0efd4fdabf0daa9b369140 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:12:13 -0400 Subject: [PATCH 0779/2056] [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) --- esphome/components/micro_wake_word/streaming_model.cpp | 1 + esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 47d2c70e138..0ab6cd3772d 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -80,6 +80,7 @@ bool StreamingModel::load_model_() { TfLiteTensor *output = this->interpreter_->output(0); if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) { ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1."); + return false; } if (output->type != kTfLiteUInt8) { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 51d52a8af8d..15124e422ff 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -619,6 +619,8 @@ void VoiceAssistant::start_playback_timeout_() { this->cancel_timeout("speaker-timeout"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + if (this->api_client_ == nullptr) + return; api::VoiceAssistantAnnounceFinished msg; msg.success = true; this->api_client_->send_message(msg); From fc67551edc0bfc4e5966e11b325c2d5277114df5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:48:43 -0400 Subject: [PATCH 0780/2056] [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) --- esphome/components/apds9960/apds9960.cpp | 8 ++++---- esphome/components/tc74/tc74.cpp | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 260de82d143..a07175f2c9c 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() { uint8_t buf[128]; for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) { - // The ESP's i2c driver has a limited buffer size. - // This way of retrieving the data should be wrong according to the datasheet - // but it seems to work. + // Read in 32-byte chunks due to ESP8266 I2C buffer limit. + // Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF + // and advances its internal pointer after every 4th byte. uint8_t read = std::min(32, fifo_level * 4 - pos); - APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed."); + APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed."); } if (millis() - this->gesture_start_ > 500) { diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index 969ef3671e2..cb58e583dca 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -50,8 +50,9 @@ void TC74Component::read_temperature_() { } } - uint8_t temperature_reg; - if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) { + int8_t temperature_reg; + if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) != + i2c::ERROR_OK) { this->status_set_warning(); return; } From 448402ca2cb49160c66472e3464dafe2209ab47b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 07:46:18 -1000 Subject: [PATCH 0781/2056] [http_request] Fix data race on update_info_ strings in update task (#14909) --- .../update/http_request_update.cpp | 222 ++++++++++-------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c40590af955..a15dc616753 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -23,6 +23,12 @@ namespace http_request { static const char *const TAG = "http_request.update"; +// Wraps UpdateInfo + error for the task→main-loop handoff. +struct TaskResult { + update::UpdateInfo info; + const LogString *error_str{nullptr}; +}; + static const size_t MAX_READ_SIZE = 256; static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; @@ -77,134 +83,148 @@ void HttpRequestUpdate::update() { void HttpRequestUpdate::update_task(void *params) { HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + // Allocate once — every path below returns via the single defer at the end. + // On failure, error_str is set; on success it is nullptr. + auto *result = new TaskResult(); + auto *info = &result->info; + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); - UPDATE_RETURN; + if (container != nullptr) + container->end(); + result->error_str = LOG_STR("Failed to fetch manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - RAMAllocator<uint8_t> allocator; - uint8_t *data = allocator.allocate(container->content_length); - if (data == nullptr) { - ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer( - [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); - container->end(); - UPDATE_RETURN; - } - - auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, - this_update->request_parent_->get_timeout()); - if (read_result.status != HttpReadStatus::OK) { - if (read_result.status == HttpReadStatus::TIMEOUT) { - ESP_LOGE(TAG, "Timeout reading manifest"); - } else { - ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); + { + RAMAllocator<uint8_t> allocator; + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to allocate memory for manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); }); - allocator.deallocate(data, container->content_length); - container->end(); - UPDATE_RETURN; - } - size_t read_index = container->get_bytes_read(); - size_t content_length = container->content_length; - container->end(); - container.reset(); // Release ownership of the container's shared_ptr - - bool valid = false; - { // Scope to ensure JsonDocument is destroyed before deallocating buffer - valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { - if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || - !root[ESPHOME_F("builds")].is<JsonArray>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, + this_update->request_parent_->get_timeout()); + if (read_result.status != HttpReadStatus::OK) { + if (read_result.status == HttpReadStatus::TIMEOUT) { + ESP_LOGE(TAG, "Timeout reading manifest"); + } else { + ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); } - this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>(); - this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>(); + allocator.deallocate(data, container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to read manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } + size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; - auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); - for (auto build : builds_array) { - if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { + container->end(); + container.reset(); // Release ownership of the container's shared_ptr + + bool valid = false; + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool { + if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || + !root[ESPHOME_F("builds")].is<JsonArray>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { - if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + info->title = root[ESPHOME_F("name")].as<std::string>(); + info->latest_version = root[ESPHOME_F("version")].as<std::string>(); + + auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>(); + for (auto build : builds_array) { + if (!build[ESPHOME_F("chipFamily")].is<const char *>()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); - if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { + if (!build[ESPHOME_F("ota")].is<JsonObject>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>(); + if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + info->firmware_url = ota[ESPHOME_F("path")].as<std::string>(); + info->md5 = ota[ESPHOME_F("md5")].as<std::string>(); + + if (ota[ESPHOME_F("summary")].is<const char *>()) + info->summary = ota[ESPHOME_F("summary")].as<std::string>(); + if (ota[ESPHOME_F("release_url")].is<const char *>()) + info->release_url = ota[ESPHOME_F("release_url")].as<std::string>(); + + return true; } - this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>(); - this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>(); - - if (ota[ESPHOME_F("summary")].is<const char *>()) - this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>(); - if (ota[ESPHOME_F("release_url")].is<const char *>()) - this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>(); - - return true; } - } - return false; - }); - } - allocator.deallocate(data, content_length); + return false; + }); + } + allocator.deallocate(data, content_length); - if (!valid) { - ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); - UPDATE_RETURN; - } + if (!valid) { + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); + result->error_str = LOG_STR("Failed to parse manifest JSON"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } - // Merge source_url_ and this_update->update_info_.firmware_url - if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this_update->update_info_.firmware_url; - if (path[0] == '/') { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); - this_update->update_info_.firmware_url = domain + path; - } else { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); - this_update->update_info_.firmware_url = domain + path; + // Merge source_url_ and firmware_url + if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) { + std::string path = info->firmware_url; + if (path[0] == '/') { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + info->firmware_url = domain + path; + } else { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + info->firmware_url = domain + path; + } } - } #ifdef ESPHOME_PROJECT_VERSION - this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; + info->current_version = ESPHOME_PROJECT_VERSION; #else - this_update->update_info_.current_version = ESPHOME_VERSION; + info->current_version = ESPHOME_VERSION; #endif - - bool trigger_update_available = false; - - if (this_update->update_info_.latest_version.empty() || - this_update->update_info_.latest_version == this_update->update_info_.current_version) { - this_update->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { - trigger_update_available = true; - } - this_update->state_ = update::UPDATE_STATE_AVAILABLE; } - // Defer to main loop to ensure thread-safe execution of: - // - status_clear_error() performs non-atomic read-modify-write on component_state_ - // - publish_state() triggers API callbacks that write to the shared protobuf buffer - // which can be corrupted if accessed concurrently from task and main loop threads - // - update_available trigger to ensure consistent state when the trigger fires - this_update->defer([this_update, trigger_update_available]() { - this_update->update_info_.has_progress = false; - this_update->update_info_.progress = 0.0f; +defer: + // Release container before vTaskDelete (which doesn't call destructors) + container.reset(); + + // Defer to the main loop so all update_info_ and state_ writes happen on the + // same thread as readers (API, MQTT, web server). This is a single defer for + // both success and error paths to avoid multiple std::function instantiations. + // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. + this_update->defer([this_update, result]() { + if (result->error_str != nullptr) { + this_update->status_set_error(result->error_str); + delete result; + return; + } + + // Determine new state on main loop (avoids extra lambda captures from task) + bool trigger_update_available = false; + update::UpdateState new_state; + if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) { + new_state = update::UPDATE_STATE_NO_UPDATE; + } else { + new_state = update::UPDATE_STATE_AVAILABLE; + if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { + trigger_update_available = true; + } + } + + this_update->update_info_ = std::move(result->info); + this_update->state_ = new_state; + delete result; // Safe: moved-from state is valid for destruction this_update->status_clear_error(); this_update->publish_state(); From af9366fdd4822cb5e8a1a7c571c08d6901993cc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:19:26 +1300 Subject: [PATCH 0782/2056] Bump version to 2026.3.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index ea34106f36a..53eae489669 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b4 +PROJECT_NUMBER = 2026.3.0b5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 4ba8b1a23fc..579235ff69a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b4" +__version__ = "2026.3.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 89066e3e20c84c1d2eac1a7ebd54059381c9a253 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:33:00 -1000 Subject: [PATCH 0783/2056] Bump actions/cache from 5.0.3 to 5.0.4 (#14929) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a03579abc5..cf5c7029c5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv # yamllint disable-line rule:line-length @@ -159,7 +159,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -198,7 +198,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -231,7 +231,7 @@ jobs: echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -253,7 +253,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -387,14 +387,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -466,14 +466,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -555,14 +555,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -817,7 +817,7 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -841,7 +841,7 @@ jobs: - name: Cache platformio if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -882,7 +882,7 @@ jobs: - name: Save memory analysis to cache if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -929,7 +929,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} From 3a47317fc890e04d2148edcb567beac8de6d8268 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:33:15 -1000 Subject: [PATCH 0784/2056] Bump actions/cache from 5.0.3 to 5.0.4 in /.github/actions/restore-python (#14930) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 6d7d4f8c129..af54175c01e 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv # yamllint disable-line rule:line-length From ef3afe3e2183d01d34d1910ef8aae22a7f36fd8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:33:29 -1000 Subject: [PATCH 0785/2056] Bump codecov/codecov-action from 5.5.2 to 5.5.3 (#14928) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf5c7029c5b..ead87ad0874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache From 16667bf5be6c17df7125d01bff3acac87a3fd8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:39:26 -1000 Subject: [PATCH 0786/2056] Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927) 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 da95dd5a13d..f8f60f19329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.2 +aioesphomeapi==44.6.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 47909d529982a7e8ac400750b3237ac689822c47 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:47:14 -0400 Subject: [PATCH 0787/2056] [hub75] Bump esp-hub75 to 0.3.5 (#14915) --- esphome/components/hub75/display.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 4cca0cea5da..ede5078c33b 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -587,7 +587,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.3.4", + ref="0.3.5", ) # Set compile-time configuration via build flags (so external library sees them) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index a847e34b022..620dc6131dd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -64,7 +64,7 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.3.4 + version: 0.3.5 rules: - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" espressif/mqtt: From cc0655a9048202ccd62fcca881b2fb6fc3a1b12d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:42:13 -0400 Subject: [PATCH 0788/2056] [bedjet][light][i2s_audio][ld2412] Fix uninitialized pointers, div-by-zero, and buffer validation (#14925) --- esphome/components/bedjet/bedjet_codec.h | 2 +- .../i2s_audio/speaker/i2s_audio_speaker.h | 2 +- esphome/components/ld2412/ld2412.cpp | 41 +++++++------------ esphome/components/light/effects.py | 2 +- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 07aee32d540..3936ba23151 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -183,7 +183,7 @@ class BedjetCodec { BedjetPacket packet_; - BedjetStatusPacket *status_packet_; + BedjetStatusPacket *status_packet_{nullptr}; BedjetStatusPacket buf_; }; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 1d03a4c4951..93ec754178e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -110,7 +110,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; - QueueHandle_t i2s_event_queue_; + QueueHandle_t i2s_event_queue_{nullptr}; std::weak_ptr<RingBuffer> audio_ring_buffer_; diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 37578dd8daf..6ff6963e9f7 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -455,12 +455,10 @@ void LD2412Component::handle_periodic_data_() { } #ifdef USE_NUMBER -std::function<void(void)> set_number_value(number::Number *n, float value) { +void set_number_value(number::Number *n, float value) { if (n != nullptr && (!n->has_state() || n->state != value)) { - n->state = value; - return [n, value]() { n->publish_state(value); }; + n->publish_state(value); } - return []() {}; } #endif @@ -504,6 +502,9 @@ bool LD2412Component::handle_ack_data_() { break; case CMD_QUERY_VERSION: { + if (this->buffer_pos_ < 12 + sizeof(this->version_)) { + return false; + } std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); char version_s[20]; ld24xx::format_version_str(this->version_, version_s); @@ -596,13 +597,8 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_MOTION_GATE_SENS: { #ifdef USE_NUMBER - std::vector<std::function<void(void)>> updates; - updates.reserve(this->gate_still_threshold_numbers_.size()); - for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { - updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i])); - } - for (auto &update : updates) { - update(); + for (size_t i = 0; i < this->gate_move_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) { + set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]); } #endif break; @@ -610,13 +606,8 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_STATIC_GATE_SENS: { #ifdef USE_NUMBER - std::vector<std::function<void(void)>> updates; - updates.reserve(this->gate_still_threshold_numbers_.size()); - for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { - updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i])); - } - for (auto &update : updates) { - update(); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) { + set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]); } #endif break; @@ -625,20 +616,21 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_BASIC_CONF: // Query parameters response { #ifdef USE_NUMBER + if (this->buffer_pos_ < 15) { + return false; + } /* Moving distance range: 9th byte Still distance range: 10th byte */ - std::vector<std::function<void(void)>> updates; - updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10])); - updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1)); + set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]); + set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1); ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10], this->buffer_data_[11]); /* None Duration: 11~12th bytes */ - updates.push_back( - set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]))); + set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12])); ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12])); /* Output pin configuration: 13th bytes @@ -650,9 +642,6 @@ bool LD2412Component::handle_ack_data_() { this->out_pin_level_select_->publish_state(out_pin_level_str); } #endif - for (auto &update : updates) { - update(); - } #endif } break; default: diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 15d9272d1a1..4088a78e0d2 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -392,7 +392,7 @@ async def addressable_lambda_effect_to_code(config, effect_id): "Rainbow", { cv.Optional(CONF_SPEED, default=10): cv.uint32_t, - cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, + cv.Optional(CONF_WIDTH, default=50): cv.int_range(min=1, max=65535), }, ) async def addressable_rainbow_effect_to_code(config, effect_id): From 4a93d5b54465647a8abfa226e2091650d9057b4c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:42:53 -0400 Subject: [PATCH 0789/2056] [vl53l0x][ld2420][ble_client][inkplate] Fix state corruption, crash, OOB read, and shift UB (#14919) --- .../ble_client/sensor/ble_sensor.cpp | 8 +++++++- .../text_sensor/ble_text_sensor.cpp | 4 ++++ esphome/components/inkplate/inkplate.cpp | 2 +- esphome/components/inkplate/inkplate.h | 20 +++++++++---------- esphome/components/ld2420/ld2420.cpp | 20 +++++++++++-------- esphome/components/vl53l0x/vl53l0x_sensor.cpp | 1 + 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index fe5f11bbc2b..4bd871dc817 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -102,6 +102,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.value_len == 0) { + ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str()); + break; + } ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); if (param->notify.handle != this->handle) @@ -131,8 +135,10 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { if (this->has_data_to_value_) { std::vector<uint8_t> data(value, value + value_len); return this->data_to_value_func_(data); - } else { + } else if (value_len > 0) { return value[0]; + } else { + return NAN; } } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index cacf1b48357..7eaa6af0761 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -104,6 +104,10 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_NOTIFY_EVT: { if (param->notify.handle != this->handle) break; + if (param->notify.value_len == 0) { + ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str()); + break; + } ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len); diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 7551c6fc775..326bdff774e 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -229,7 +229,7 @@ void Inkplate::eink_off_() { this->oe_pin_->digital_write(false); this->gmod_pin_->digital_write(false); - GPIO.out &= ~(this->get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin())); + GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin())); this->ckv_pin_->digital_write(false); this->sph_pin_->digital_write(false); this->spv_pin_->digital_write(false); diff --git a/esphome/components/inkplate/inkplate.h b/esphome/components/inkplate/inkplate.h index fb4674b522d..bcd56b829a0 100644 --- a/esphome/components/inkplate/inkplate.h +++ b/esphome/components/inkplate/inkplate.h @@ -152,16 +152,16 @@ class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice { size_t get_buffer_length_(); - int get_data_pin_mask_() { - int data = 0; - data |= (1 << this->display_data_0_pin_->get_pin()); - data |= (1 << this->display_data_1_pin_->get_pin()); - data |= (1 << this->display_data_2_pin_->get_pin()); - data |= (1 << this->display_data_3_pin_->get_pin()); - data |= (1 << this->display_data_4_pin_->get_pin()); - data |= (1 << this->display_data_5_pin_->get_pin()); - data |= (1 << this->display_data_6_pin_->get_pin()); - data |= (1 << this->display_data_7_pin_->get_pin()); + uint32_t get_data_pin_mask_() { + uint32_t data = 0; + data |= (1UL << this->display_data_0_pin_->get_pin()); + data |= (1UL << this->display_data_1_pin_->get_pin()); + data |= (1UL << this->display_data_2_pin_->get_pin()); + data |= (1UL << this->display_data_3_pin_->get_pin()); + data |= (1UL << this->display_data_4_pin_->get_pin()); + data |= (1UL << this->display_data_5_pin_->get_pin()); + data |= (1UL << this->display_data_6_pin_->get_pin()); + data |= (1UL << this->display_data_7_pin_->get_pin()); return data; } diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 1e671363c9d..ae622cda283 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -170,14 +170,18 @@ static uint8_t calc_checksum(void *data, size_t size) { return checksum; } -static int get_firmware_int(const char *version_string) { - std::string version_str = version_string; - if (version_str[0] == 'v') { - version_str.erase(0, 1); +static int32_t get_firmware_int(const char *version_string) { + // Convert "v1.5.4" -> 154 by skipping 'v' and '.', accumulating digits + const char *p = (*version_string == 'v') ? version_string + 1 : version_string; + int32_t result = 0; + for (; *p != '\0'; p++) { + if (*p == '.') + continue; + if (*p < '0' || *p > '9') + return 0; + result = result * 10 + (*p - '0'); } - version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); - int version_integer = stoi(version_str); - return version_integer; + return result; } float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } @@ -683,7 +687,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { retry = 0; } if (this->cmd_reply_.error > 0) { - this->handle_cmd_error(error); + this->handle_cmd_error(this->cmd_reply_.error); } } return error; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 0b2b40d723a..8a76ed77605 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -266,6 +266,7 @@ void VL53L0XSensor::update() { this->status_momentary_warning("update", 5000); ESP_LOGW(TAG, "%s - update called before prior reading complete - initiated:%d waiting_for_interrupt:%d", this->name_.c_str(), this->initiated_read_, this->waiting_for_interrupt_); + return; } // initiate single shot measurement From 73a49493a261a2a7fb748875e66d85bf0ff2b050 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:43:42 -0400 Subject: [PATCH 0790/2056] [vbus][shelly_dimmer][st7789v][modbus_controller] Fix integer overflows, off-by-one, and coordinate swap (#14916) --- .../modbus_controller/modbus_controller.h | 2 +- .../shelly_dimmer/shelly_dimmer.cpp | 2 +- esphome/components/st7789v/st7789v.cpp | 4 +-- .../components/vbus/sensor/vbus_sensor.cpp | 26 ++++++++++++------- esphome/components/vbus/vbus.cpp | 3 +-- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index fca29265682..bd3d4d705ec 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -178,7 +178,7 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) { return result; } for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { - if ((mask & (1 << pos)) != 0) + if ((mask & (1UL << pos)) != 0) return result >> pos; } return 0; diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 88fcbcbfe15..230fb963b13 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -402,7 +402,7 @@ bool ShellyDimmer::handle_frame_() { // Handle response. switch (cmd) { case SHELLY_DIMMER_PROTO_CMD_POLL: { - if (payload_len < 16) { + if (payload_len < 17) { return false; } diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 6e4360ae742..dc03fb04cae 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -156,9 +156,9 @@ void ST7789V::update() { void ST7789V::set_model_str(const char *model_str) { this->model_str_ = model_str; } void ST7789V::write_display_data() { - uint16_t x1 = this->offset_height_; + uint16_t x1 = this->offset_width_; uint16_t x2 = x1 + get_width_internal() - 1; - uint16_t y1 = this->offset_width_; + uint16_t y1 = this->offset_height_; uint16_t y2 = y1 + get_height_internal() - 1; this->enable(); diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index 1cabb49703b..407a81c83b1 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -48,8 +48,8 @@ void DeltaSolBSPlusSensor::handle_message(std::vector<uint8_t> &message) { if (this->operating_hours2_sensor_ != nullptr) this->operating_hours2_sensor_->publish_state(get_u16(message, 18)); if (this->heat_quantity_sensor_ != nullptr) { - this->heat_quantity_sensor_->publish_state(get_u16(message, 20) + get_u16(message, 22) * 1000 + - get_u16(message, 24) * 1000000); + this->heat_quantity_sensor_->publish_state(get_u16(message, 20) + get_u16(message, 22) * 1000.0f + + get_u16(message, 24) * 1000000.0f); } if (this->time_sensor_ != nullptr) this->time_sensor_->publish_state(get_u16(message, 12)); @@ -130,8 +130,8 @@ void DeltaSolCSensor::handle_message(std::vector<uint8_t> &message) { if (this->operating_hours2_sensor_ != nullptr) this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); if (this->heat_quantity_sensor_ != nullptr) { - this->heat_quantity_sensor_->publish_state(get_u16(message, 16) + get_u16(message, 18) * 1000 + - get_u16(message, 20) * 1000000); + this->heat_quantity_sensor_->publish_state(get_u16(message, 16) + get_u16(message, 18) * 1000.0f + + get_u16(message, 20) * 1000000.0f); } if (this->time_sensor_ != nullptr) this->time_sensor_->publish_state(get_u16(message, 22)); @@ -162,8 +162,10 @@ void DeltaSolCS2Sensor::handle_message(std::vector<uint8_t> &message) { this->pump_speed_sensor_->publish_state(message[12]); if (this->operating_hours_sensor_ != nullptr) this->operating_hours_sensor_->publish_state(get_u16(message, 14)); - if (this->heat_quantity_sensor_ != nullptr) - this->heat_quantity_sensor_->publish_state((get_u16(message, 26) << 16) + get_u16(message, 24)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state((static_cast<uint32_t>(get_u16(message, 26)) << 16) | + get_u16(message, 24)); + } if (this->version_sensor_ != nullptr) this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f); } @@ -204,8 +206,10 @@ void DeltaSolCS4Sensor::handle_message(std::vector<uint8_t> &message) { this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); if (this->operating_hours2_sensor_ != nullptr) this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); - if (this->heat_quantity_sensor_ != nullptr) - this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state((static_cast<uint32_t>(get_u16(message, 30)) << 16) | + get_u16(message, 28)); + } if (this->time_sensor_ != nullptr) this->time_sensor_->publish_state(get_u16(message, 22)); if (this->version_sensor_ != nullptr) @@ -250,8 +254,10 @@ void DeltaSolCSPlusSensor::handle_message(std::vector<uint8_t> &message) { this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); if (this->operating_hours2_sensor_ != nullptr) this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); - if (this->heat_quantity_sensor_ != nullptr) - this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state((static_cast<uint32_t>(get_u16(message, 30)) << 16) | + get_u16(message, 28)); + } if (this->time_sensor_ != nullptr) this->time_sensor_->publish_state(get_u16(message, 22)); if (this->version_sensor_ != nullptr) diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index c6786ee31e8..195d6ed5680 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -67,8 +67,7 @@ void VBus::loop() { } septet_spread(this->buffer_.data(), 7, 6, this->buffer_[13]); uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7]; - uint32_t value = - (this->buffer_[12] << 24) + (this->buffer_[11] << 16) + (this->buffer_[10] << 8) + this->buffer_[9]; + uint32_t value = encode_uint32(this->buffer_[12], this->buffer_[11], this->buffer_[10], this->buffer_[9]); ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04" PRIx32 " (%" PRIu32 ")", this->command_, this->source_, this->dest_, id, value, value); } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) { From 097e6eb41fba8fede3495e63ae0357880b26bd9d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:42:56 -0400 Subject: [PATCH 0791/2056] [i2s_audio] Remove legacy I2S driver support (#14932) --- CODEOWNERS | 1 - esphome/components/i2s_audio/__init__.py | 101 ++----- esphome/components/i2s_audio/i2s_audio.h | 35 --- .../i2s_audio/media_player/__init__.py | 122 +------- .../media_player/i2s_audio_media_player.cpp | 260 ------------------ .../media_player/i2s_audio_media_player.h | 87 ------ .../i2s_audio/microphone/__init__.py | 20 +- .../microphone/i2s_audio_microphone.cpp | 97 +------ .../microphone/i2s_audio_microphone.h | 21 -- .../components/i2s_audio/speaker/__init__.py | 36 +-- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 126 --------- .../i2s_audio/speaker/i2s_audio_speaker.h | 18 -- esphome/core/defines.h | 1 - .../components/i2s_audio/test.esp32-ard.yaml | 16 -- tests/components/media_player/common.yaml | 16 +- 15 files changed, 50 insertions(+), 907 deletions(-) delete mode 100644 esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp delete mode 100644 esphome/components/i2s_audio/media_player/i2s_audio_media_player.h delete mode 100644 tests/components/i2s_audio/test.esp32-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e72b1647614..88f62c3194e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -244,7 +244,6 @@ esphome/components/hyt271/* @Philippe12 esphome/components/i2c/* @esphome/core esphome/components/i2c_device/* @gabest11 esphome/components/i2s_audio/* @jesserockz -esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 977a239497a..ffa63f5ee8e 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -53,8 +53,6 @@ CONF_RIGHT = "right" CONF_STEREO = "stereo" CONF_BOTH = "both" -CONF_USE_LEGACY = "use_legacy" - i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) I2SAudioBase = i2s_audio_ns.class_( @@ -154,20 +152,6 @@ def validate_mclk_divisible_by_3(config): return config -# Key for storing legacy driver setting in CORE.data -I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver" - - -def _get_use_legacy_driver(): - """Get the legacy driver setting from CORE.data.""" - return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY) - - -def _set_use_legacy_driver(value: bool) -> None: - """Set the legacy driver setting in CORE.data.""" - CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value - - def i2s_audio_component_schema( class_: MockObjClass, *, @@ -192,10 +176,6 @@ def i2s_audio_component_schema( *I2S_MODE_OPTIONS, lower=True ), cv.Optional(CONF_USE_APLL, default=False): cv.boolean, - cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( - cv.Any(cv.float_with_unit("bits", "bit"), "default"), - cv.one_of(*I2S_BITS_PER_CHANNEL), - ), cv.Optional(CONF_MCLK_MULTIPLE, default=256): cv.one_of(*I2S_MCLK_MULTIPLE), } ) @@ -203,59 +183,28 @@ def i2s_audio_component_schema( async def register_i2s_audio_component(var, config): await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - if use_legacy(): - cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]])) - cg.add(var.set_channel(I2S_CHANNELS[config[CONF_CHANNEL]])) - cg.add( - var.set_bits_per_sample(I2S_BITS_PER_SAMPLE[config[CONF_BITS_PER_SAMPLE]]) - ) - cg.add( - var.set_bits_per_channel( - I2S_BITS_PER_CHANNEL[config[CONF_BITS_PER_CHANNEL]] - ) - ) - else: - cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) - slot_mode = config[CONF_CHANNEL] - if slot_mode != CONF_STEREO: - slot_mode = CONF_MONO - slot_mask = config[CONF_CHANNEL] - if slot_mask not in [CONF_LEFT, CONF_RIGHT]: - slot_mask = CONF_BOTH - cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode])) - cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask])) - cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]])) + cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) + slot_mode = config[CONF_CHANNEL] + if slot_mode != CONF_STEREO: + slot_mode = CONF_MONO + slot_mask = config[CONF_CHANNEL] + if slot_mask not in [CONF_LEFT, CONF_RIGHT]: + slot_mask = CONF_BOTH + cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode])) + cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask])) + cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_use_apll(config[CONF_USE_APLL])) cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]])) -def validate_use_legacy(value): - if CONF_USE_LEGACY in value: - existing_value = _get_use_legacy_driver() - if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]): - raise cv.Invalid( - f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value." - ) - if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): - raise cv.Invalid("Arduino supports only the legacy i2s driver") - _set_use_legacy_driver(value[CONF_USE_LEGACY]) - elif CORE.using_arduino: - _set_use_legacy_driver(True) - return value - - -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(I2SAudioComponent), - cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_USE_LEGACY): cv.boolean, - }, - ), - validate_use_legacy, +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + }, ) @@ -311,13 +260,6 @@ def _assign_ports() -> None: def _final_validate(_): - from esphome.components.esp32 import idf_version - - if use_legacy() and idf_version() >= cv.Version(6, 0, 0): - raise cv.Invalid( - "The legacy I2S driver is not available in ESP-IDF 6.0+. " - "Set 'use_legacy: false' in i2s_audio configuration." - ) i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] variant = get_esp32_variant() if variant not in I2S_PORTS: @@ -329,10 +271,6 @@ def _final_validate(_): _assign_ports() -def use_legacy(): - return _get_use_legacy_driver() - - FINAL_VALIDATE_SCHEMA = _final_validate @@ -349,11 +287,6 @@ async def to_code(config): # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) include_builtin_idf_component("esp_driver_i2s") - if use_legacy(): - cg.add_define("USE_I2S_LEGACY") - # Legacy I2S API lives in the "driver" shim component (driver/i2s.h) - include_builtin_idf_component("driver") - # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index f26ffddd467..5b260fa7ede 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -6,11 +6,7 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include <esp_idf_version.h> -#ifdef USE_I2S_LEGACY -#include <driver/i2s.h> -#else #include <driver/i2s_std.h> -#endif namespace esphome { namespace i2s_audio { @@ -19,33 +15,19 @@ class I2SAudioComponent; class I2SAudioBase : public Parented<I2SAudioComponent> { public: -#ifdef USE_I2S_LEGACY - void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } - void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } - void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } - void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } -#else void set_i2s_role(i2s_role_t role) { this->i2s_role_ = role; } void set_slot_mode(i2s_slot_mode_t slot_mode) { this->slot_mode_ = slot_mode; } void set_std_slot_mask(i2s_std_slot_mask_t std_slot_mask) { this->std_slot_mask_ = std_slot_mask; } void set_slot_bit_width(i2s_slot_bit_width_t slot_bit_width) { this->slot_bit_width_ = slot_bit_width; } -#endif void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; } protected: -#ifdef USE_I2S_LEGACY - i2s_mode_t i2s_mode_{}; - i2s_channel_fmt_t channel_; - i2s_bits_per_sample_t bits_per_sample_; - i2s_bits_per_chan_t bits_per_channel_; -#else i2s_role_t i2s_role_{}; i2s_slot_mode_t slot_mode_; i2s_std_slot_mask_t std_slot_mask_; i2s_slot_bit_width_t slot_bit_width_; -#endif uint32_t sample_rate_; bool use_apll_; i2s_mclk_multiple_t mclk_multiple_; @@ -57,17 +39,6 @@ class I2SAudioOut : public I2SAudioBase {}; class I2SAudioComponent : public Component { public: -#ifdef USE_I2S_LEGACY - i2s_pin_config_t get_pin_config() const { - return { - .mck_io_num = this->mclk_pin_, - .bck_io_num = this->bclk_pin_, - .ws_io_num = this->lrclk_pin_, - .data_out_num = I2S_PIN_NO_CHANGE, - .data_in_num = I2S_PIN_NO_CHANGE, - }; - } -#else i2s_std_gpio_config_t get_pin_config() const { return {.mclk = (gpio_num_t) this->mclk_pin_, .bclk = (gpio_num_t) this->bclk_pin_, @@ -80,7 +51,6 @@ class I2SAudioComponent : public Component { .ws_inv = false, }}; } -#endif void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } @@ -101,13 +71,8 @@ class I2SAudioComponent : public Component { I2SAudioIn *audio_in_{nullptr}; I2SAudioOut *audio_out_{nullptr}; -#ifdef USE_I2S_LEGACY - int mclk_pin_{I2S_PIN_NO_CHANGE}; - int bclk_pin_{I2S_PIN_NO_CHANGE}; -#else int mclk_pin_{I2S_GPIO_UNUSED}; int bclk_pin_{I2S_GPIO_UNUSED}; -#endif int lrclk_pin_; int port_{}; }; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 426b211f475..b366d4fb05c 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -1,121 +1,7 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import esp32, media_player import esphome.config_validation as cv -from esphome.const import CONF_MODE -from .. import ( - CONF_I2S_AUDIO_ID, - CONF_I2S_DOUT_PIN, - CONF_LEFT, - CONF_MONO, - CONF_RIGHT, - CONF_STEREO, - I2SAudioComponent, - I2SAudioOut, - i2s_audio_ns, - use_legacy, +CONFIG_SCHEMA = cv.invalid( + "The I2S audio media player has been removed. " + "Use the speaker media player component instead. " + "See https://esphome.io/components/media_player/speaker.html for details." ) - -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["i2s_audio"] - -I2SAudioMediaPlayer = i2s_audio_ns.class_( - "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer, I2SAudioOut -) - -i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") - - -CONF_MUTE_PIN = "mute_pin" -CONF_AUDIO_ID = "audio_id" -CONF_DAC_TYPE = "dac_type" -CONF_I2S_COMM_FMT = "i2s_comm_fmt" - -INTERNAL_DAC_OPTIONS = { - CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, -} - -EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO] - -NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2] - -I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] - - -def validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config - variant = esp32.get_esp32_variant() - if variant in NO_INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") - return config - - -CONFIG_SCHEMA = cv.All( - cv.typed_schema( - { - "internal": media_player.media_player_schema(I2SAudioMediaPlayer) - .extend( - { - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), - } - ) - .extend(cv.COMPONENT_SCHEMA), - "external": media_player.media_player_schema(I2SAudioMediaPlayer) - .extend( - { - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Required( - CONF_I2S_DOUT_PIN - ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_MODE, default="mono"): cv.one_of( - *EXTERNAL_DAC_OPTIONS, lower=True - ), - cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( - *I2C_COMM_FMT_OPTIONS, lower=True - ), - } - ) - .extend(cv.COMPONENT_SCHEMA), - }, - key=CONF_DAC_TYPE, - ), - cv.only_with_arduino, - validate_esp32_variant, -) - - -def _final_validate(_): - if not use_legacy(): - raise cv.Invalid("I2S media player is only compatible with legacy i2s driver") - - -FINAL_VALIDATE_SCHEMA = _final_validate - - -async def to_code(config): - var = await media_player.new_media_player(config) - await cg.register_component(var, config) - - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - - if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_MODE])) - else: - cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - if CONF_MUTE_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) - cg.add(var.set_mute_pin(pin)) - cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) - cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) - - cg.add_library("WiFi", None) - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.3.0") - cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp deleted file mode 100644 index 369c964a859..00000000000 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "i2s_audio_media_player.h" - -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include "esphome/core/log.h" - -namespace esphome { -namespace i2s_audio { - -static const char *const TAG = "audio"; - -void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { - media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING; - auto announcement = call.get_announcement(); - if (announcement.has_value()) { - play_state = *announcement ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING : media_player::MEDIA_PLAYER_STATE_PLAYING; - } - auto media_url = call.get_media_url(); - if (media_url.has_value()) { - this->current_url_ = media_url; - if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) { - if (this->audio_->isRunning()) { - this->audio_->stopSong(); - } - this->audio_->connecttohost(media_url->c_str()); - this->state = play_state; - } else { - this->start(); - } - } - - if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) { - this->is_announcement_ = true; - } - - auto vol = call.get_volume(); - if (vol.has_value()) { - this->volume = *vol; - this->set_volume_(volume); - this->unmute_(); - } - auto cmd = call.get_command(); - if (cmd.has_value()) { - switch (*cmd) { - case media_player::MEDIA_PLAYER_COMMAND_MUTE: - this->mute_(); - break; - case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: - this->unmute_(); - break; - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: { - float new_volume = this->volume + 0.1f; - if (new_volume > 1.0f) - new_volume = 1.0f; - this->set_volume_(new_volume); - this->unmute_(); - break; - } - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: { - float new_volume = this->volume - 0.1f; - if (new_volume < 0.0f) - new_volume = 0.0f; - this->set_volume_(new_volume); - this->unmute_(); - break; - } - default: - break; - } - if (this->i2s_state_ != I2S_STATE_RUNNING) { - return; - } - switch (*cmd) { - case media_player::MEDIA_PLAYER_COMMAND_PLAY: - if (!this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = play_state; - break; - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: - if (this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - break; - case media_player::MEDIA_PLAYER_COMMAND_STOP: - this->stop(); - break; - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: - this->audio_->pauseResume(); - if (this->audio_->isRunning()) { - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - } else { - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - } - break; - default: - break; - } - } - this->publish_state(); -} - -void I2SAudioMediaPlayer::mute_() { - if (this->mute_pin_ != nullptr) { - this->mute_pin_->digital_write(true); - } else { - this->set_volume_(0.0f, false); - } - this->muted_ = true; -} -void I2SAudioMediaPlayer::unmute_() { - if (this->mute_pin_ != nullptr) { - this->mute_pin_->digital_write(false); - } else { - this->set_volume_(this->volume, false); - } - this->muted_ = false; -} -void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { - if (this->audio_ != nullptr) - this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21)); - if (publish) - this->volume = volume; -} - -void I2SAudioMediaPlayer::setup() { this->state = media_player::MEDIA_PLAYER_STATE_IDLE; } - -void I2SAudioMediaPlayer::loop() { - switch (this->i2s_state_) { - case I2S_STATE_STARTING: - this->start_(); - break; - case I2S_STATE_RUNNING: - this->play_(); - break; - case I2S_STATE_STOPPING: - this->stop_(); - break; - case I2S_STATE_STOPPED: - break; - } -} - -void I2SAudioMediaPlayer::play_() { - this->audio_->loop(); - if ((this->state == media_player::MEDIA_PLAYER_STATE_PLAYING || - this->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) && - !this->audio_->isRunning()) { - this->stop(); - } -} - -void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; } -void I2SAudioMediaPlayer::start_() { - if (!this->parent_->try_lock()) { - return; // Waiting for another i2s to return lock - } - -#if SOC_I2S_SUPPORTS_DAC - if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_, this->parent_->get_port()); - } else { -#endif - this->audio_ = make_unique<Audio>(false, 3, this->parent_->get_port()); - - i2s_pin_config_t pin_config = this->parent_->get_pin_config(); - pin_config.data_out_num = this->dout_pin_; - i2s_set_pin(this->parent_->get_port(), &pin_config); - - this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_); - this->audio_->forceMono(this->external_dac_channels_ == 1); - if (this->mute_pin_ != nullptr) { - this->mute_pin_->setup(); - this->mute_pin_->digital_write(false); - } -#if SOC_I2S_SUPPORTS_DAC - } -#endif - - this->i2s_state_ = I2S_STATE_RUNNING; - this->high_freq_.start(); - this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21)); - if (this->current_url_.has_value()) { - this->audio_->connecttohost(this->current_url_.value().c_str()); - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - if (this->is_announcement_) { - this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING; - } - this->publish_state(); - } -} -void I2SAudioMediaPlayer::stop() { - if (this->i2s_state_ == I2S_STATE_STOPPED) { - return; - } - if (this->i2s_state_ == I2S_STATE_STARTING) { - this->i2s_state_ = I2S_STATE_STOPPED; - return; - } - this->i2s_state_ = I2S_STATE_STOPPING; -} -void I2SAudioMediaPlayer::stop_() { - if (this->audio_->isRunning()) { - this->audio_->stopSong(); - return; - } - - this->audio_ = nullptr; - this->current_url_ = {}; - this->parent_->unlock(); - this->i2s_state_ = I2S_STATE_STOPPED; - - this->high_freq_.stop(); - this->state = media_player::MEDIA_PLAYER_STATE_IDLE; - this->publish_state(); - this->is_announcement_ = false; -} - -media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() { - auto traits = media_player::MediaPlayerTraits(); - traits.set_supports_pause(true); - return traits; -}; - -void I2SAudioMediaPlayer::dump_config() { - ESP_LOGCONFIG(TAG, "Audio:"); - if (this->is_failed()) { - ESP_LOGCONFIG(TAG, "Audio failed to initialize!"); - return; - } -#if SOC_I2S_SUPPORTS_DAC - if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - switch (this->internal_dac_mode_) { - case I2S_DAC_CHANNEL_LEFT_EN: - ESP_LOGCONFIG(TAG, " Internal DAC mode: Left"); - break; - case I2S_DAC_CHANNEL_RIGHT_EN: - ESP_LOGCONFIG(TAG, " Internal DAC mode: Right"); - break; - case I2S_DAC_CHANNEL_BOTH_EN: - ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right"); - break; - default: - break; - } - } else { -#endif - ESP_LOGCONFIG(TAG, - " External DAC channels: %d\n" - " I2S DOUT Pin: %d", - this->external_dac_channels_, this->dout_pin_); - LOG_PIN(" Mute Pin: ", this->mute_pin_); -#if SOC_I2S_SUPPORTS_DAC - } -#endif -} - -} // namespace i2s_audio -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h deleted file mode 100644 index 4672f94d7eb..00000000000 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once - -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include "../i2s_audio.h" - -#include <driver/i2s.h> - -#include "esphome/components/media_player/media_player.h" -#include "esphome/core/component.h" -#include "esphome/core/gpio.h" -#include "esphome/core/helpers.h" - -#include <Audio.h> - -namespace esphome { -namespace i2s_audio { - -enum I2SState : uint8_t { - I2S_STATE_STOPPED = 0, - I2S_STATE_STARTING, - I2S_STATE_RUNNING, - I2S_STATE_STOPPING, -}; - -class I2SAudioMediaPlayer : public Component, public Parented<I2SAudioComponent>, public media_player::MediaPlayer { - public: - void setup() override; - float get_setup_priority() const override { return esphome::setup_priority::LATE; } - - void loop() override; - - void dump_config() override; - - void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } - void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; } -#if SOC_I2S_SUPPORTS_DAC - void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } -#endif - void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } - - void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; } - - media_player::MediaPlayerTraits get_traits() override; - - bool is_muted() const override { return this->muted_; } - - void start(); - void stop(); - - protected: - void control(const media_player::MediaPlayerCall &call) override; - - void mute_(); - void unmute_(); - void set_volume_(float volume, bool publish = true); - - void start_(); - void stop_(); - void play_(); - - I2SState i2s_state_{I2S_STATE_STOPPED}; - std::unique_ptr<Audio> audio_; - - uint8_t dout_pin_{0}; - - GPIOPin *mute_pin_{nullptr}; - bool muted_{false}; - float unmuted_volume_{0}; - -#if SOC_I2S_SUPPORTS_DAC - i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; -#endif - uint8_t external_dac_channels_; - - bool i2s_comm_fmt_lsb_; - - HighFrequencyLoopRequester high_freq_; - - optional<std::string> current_url_{}; - bool is_announcement_{false}; -}; - -} // namespace i2s_audio -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index aa15625cd78..761cbb7f48b 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -1,14 +1,13 @@ from esphome import pins import esphome.codegen as cg from esphome.components import audio, esp32, microphone -from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin +from esphome.components.adc import validate_adc_pin import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_NUM_CHANNELS, - CONF_NUMBER, CONF_SAMPLE_RATE, ) @@ -23,7 +22,6 @@ from .. import ( i2s_audio_component_schema, i2s_audio_ns, register_i2s_audio_component, - use_legacy, validate_mclk_divisible_by_3, ) @@ -127,8 +125,10 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): - if not use_legacy() and config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver") + if config[CONF_ADC_TYPE] == "internal": + raise cv.Invalid( + "Internal ADC is no longer supported. Use an external I2S microphone instead." + ) FINAL_VALIDATE_SCHEMA = _final_validate @@ -140,13 +140,7 @@ async def to_code(config): await register_i2s_audio_component(var, config) await microphone.register_microphone(var, config) - if config[CONF_ADC_TYPE] == "internal": - variant = esp32.get_esp32_variant() - pin_num = config[CONF_ADC_PIN][CONF_NUMBER] - channel = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] - cg.add(var.set_adc_channel(channel)) - else: - cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) - cg.add(var.set_pdm(config[CONF_PDM])) + cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) + cg.add(var.set_pdm(config[CONF_PDM])) cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET])) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index a17cb0d84a0..d697808c998 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -2,12 +2,8 @@ #ifdef USE_ESP32 -#ifdef USE_I2S_LEGACY -#include <driver/i2s.h> -#else #include <driver/i2s_std.h> #include <driver/i2s_pdm.h> -#endif #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -65,13 +61,6 @@ void I2SAudioMicrophone::dump_config() { void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; -#ifdef USE_I2S_LEGACY - uint8_t bits_per_sample = this->bits_per_sample_; - - if (this->channel_ == I2S_CHANNEL_FMT_RIGHT_LEFT) { - channel_count = 2; - } -#else uint8_t bits_per_sample = 16; if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { bits_per_sample = this->slot_bit_width_; @@ -80,7 +69,6 @@ void I2SAudioMicrophone::configure_stream_settings_() { if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) { channel_count = 2; } -#endif #ifdef USE_ESP32_VARIANT_ESP32 // ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will @@ -114,65 +102,6 @@ bool I2SAudioMicrophone::start_driver_() { this->locked_driver_ = true; esp_err_t err; -#ifdef USE_I2S_LEGACY - i2s_driver_config_t config = { - .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX), - .sample_rate = this->sample_rate_, - .bits_per_sample = this->bits_per_sample_, - .channel_format = this->channel_, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 4, - .dma_buf_len = 240, // Must be divisible by 3 to support 24 bits per sample on old driver and newer variants - .use_apll = this->use_apll_, - .tx_desc_auto_clear = false, - .fixed_mclk = 0, - .mclk_multiple = this->mclk_multiple_, - .bits_per_chan = this->bits_per_channel_, - }; - -#if SOC_I2S_SUPPORTS_ADC - if (this->adc_) { - config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); - err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); - return false; - } - - err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error setting ADC mode: %s", esp_err_to_name(err)); - return false; - } - - err = i2s_adc_enable(this->parent_->get_port()); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error enabling ADC: %s", esp_err_to_name(err)); - return false; - } - } else -#endif - { - if (this->pdm_) - config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); - - err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); - return false; - } - - i2s_pin_config_t pin_config = this->parent_->get_pin_config(); - pin_config.data_in_num = this->din_pin_; - - err = i2s_set_pin(this->parent_->get_port(), &pin_config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err)); - return false; - } - } -#else i2s_chan_config_t chan_cfg = { .id = this->parent_->get_port(), .role = this->i2s_role_, @@ -265,7 +194,6 @@ bool I2SAudioMicrophone::start_driver_() { ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; } -#endif this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation @@ -284,24 +212,6 @@ void I2SAudioMicrophone::stop_driver_() { // ensures that we stop/unload the driver when it only partially starts. esp_err_t err; -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_ADC - if (this->adc_) { - err = i2s_adc_disable(this->parent_->get_port()); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); - } - } -#endif - err = i2s_stop(this->parent_->get_port()); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); - } - err = i2s_driver_uninstall(this->parent_->get_port()); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err)); - } -#else if (this->rx_handle_ != nullptr) { /* Have to stop the channel before deleting it */ err = i2s_channel_disable(this->rx_handle_); @@ -315,7 +225,6 @@ void I2SAudioMicrophone::stop_driver_() { } this->rx_handle_ = nullptr; } -#endif if (this->locked_driver_) { this->parent_->unlock(); this->locked_driver_ = false; @@ -412,12 +321,8 @@ void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) { size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; -#ifdef USE_I2S_LEGACY - esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, ticks_to_wait); -#else // i2s_channel_read expects the timeout value in ms, not ticks esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, pdTICKS_TO_MS(ticks_to_wait)); -#endif if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) { // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call if (!this->status_has_warning()) { @@ -432,7 +337,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w return 0; } this->status_clear_warning(); -#if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY) +#ifdef USE_ESP32_VARIANT_ESP32 // For ESP32 16-bit standard mono mode, adjacent samples need to be swapped. if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) { int16_t *samples = reinterpret_cast<int16_t *>(buf); diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index de272ba23dd..e277409262a 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -26,23 +26,10 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; } -#ifdef USE_I2S_LEGACY - void set_din_pin(int8_t pin) { this->din_pin_ = pin; } -#else void set_din_pin(int8_t pin) { this->din_pin_ = (gpio_num_t) pin; } -#endif void set_pdm(bool pdm) { this->pdm_ = pdm; } -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_ADC - void set_adc_channel(adc_channel_t channel) { - this->adc_channel_ = (adc1_channel_t) channel; - this->adc_ = true; - } -#endif -#endif - protected: /// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings. /// @return True if succesful, false otherwise @@ -68,16 +55,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub TaskHandle_t task_handle_{nullptr}; -#ifdef USE_I2S_LEGACY - int8_t din_pin_{I2S_PIN_NO_CHANGE}; -#if SOC_I2S_SUPPORTS_ADC - adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; - bool adc_{false}; -#endif -#else gpio_num_t din_pin_{I2S_GPIO_UNUSED}; i2s_chan_handle_t rx_handle_; -#endif bool pdm_{false}; bool correct_dc_offset_; diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index b84cf7de3b6..d1d1bc3ee3a 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -26,7 +26,6 @@ from .. import ( i2s_audio_component_schema, i2s_audio_ns, register_i2s_audio_component, - use_legacy, validate_mclk_divisible_by_3, ) @@ -166,13 +165,12 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): - if not use_legacy(): - if config[CONF_DAC_TYPE] == "internal": - raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver") - if config[CONF_I2S_COMM_FMT] == "stand_max": - raise cv.Invalid( - "I2S standard max format only implemented with legacy i2s driver." - ) + if config[CONF_DAC_TYPE] == "internal": + raise cv.Invalid( + "Internal DAC is no longer supported. Use an external I2S DAC instead." + ) + if config[CONF_I2S_COMM_FMT] == "stand_max": + raise cv.Invalid("I2S standard max format is no longer supported.") FINAL_VALIDATE_SCHEMA = _final_validate @@ -184,21 +182,13 @@ async def to_code(config): await register_i2s_audio_component(var, config) await speaker.register_speaker(var, config) - if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_MODE])) - else: - cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - if use_legacy(): - cg.add( - var.set_i2s_comm_fmt(I2C_COMM_FMT_OPTIONS[config[CONF_I2S_COMM_FMT]]) - ) - else: - fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long - if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: - fmt = "msb" - elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: - fmt = "pcm" - cg.add(var.set_i2s_comm_fmt(fmt)) + cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) + fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long + if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: + fmt = "msb" + elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: + fmt = "pcm" + cg.add(var.set_i2s_comm_fmt(fmt)) if config[CONF_TIMEOUT] != CONF_NEVER: cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index a996702f8bb..dde1f70bc58 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -2,11 +2,7 @@ #ifdef USE_ESP32 -#ifdef USE_I2S_LEGACY -#include <driver/i2s.h> -#else #include <driver/i2s_std.h> -#endif #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_transfer_buffer.h" @@ -79,14 +75,7 @@ void I2SAudioSpeaker::dump_config() { if (this->timeout_.has_value()) { ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value()); } -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_DAC - ESP_LOGCONFIG(TAG, " Internal DAC mode: %d", static_cast<int8_t>(this->internal_dac_mode_)); -#endif - ESP_LOGCONFIG(TAG, " Communication format: %d", static_cast<int8_t>(this->i2s_comm_fmt_)); -#else ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str()); -#endif } void I2SAudioSpeaker::loop() { @@ -300,14 +289,6 @@ void I2SAudioSpeaker::speaker_task(void *params) { // Audio stream info changed, stop the speaker task so it will restart with the proper settings. break; } -#ifdef USE_I2S_LEGACY - i2s_event_t i2s_event; - while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) { - if (i2s_event.type == I2S_EVENT_TX_Q_OVF) { - tx_dma_underflow = true; - } - } -#else int64_t write_timestamp; while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) { // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes @@ -326,7 +307,6 @@ void I2SAudioSpeaker::speaker_task(void *params) { this_speaker->audio_output_callback_(frames_sent, write_timestamp); } } -#endif if (this_speaker->pause_state_) { // Pause state is accessed atomically, so thread safe @@ -393,17 +373,6 @@ void I2SAudioSpeaker::speaker_task(void *params) { vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); } else { size_t bytes_written = 0; -#ifdef USE_I2S_LEGACY - if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { - i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), - transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); - } else if (this_speaker->current_stream_info_.get_bits_per_sample() < - (uint8_t) this_speaker->bits_per_sample_) { - i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), - transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(), - this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); - } -#else if (tx_dma_underflow) { // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing // callbacks are accurate. Preload the data. @@ -420,14 +389,12 @@ void I2SAudioSpeaker::speaker_task(void *params) { i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), &bytes_written, DMA_BUFFER_DURATION_MS); } -#endif if (bytes_written > 0) { last_data_received_time = millis(); frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written); transfer_buffer->decrease_buffer_length(bytes_written); if (tx_dma_underflow) { tx_dma_underflow = false; -#ifndef USE_I2S_LEGACY // Reset the event queue timestamps // Enable the on_sent callback to accurately track the timestamps of played audio // Enable the I2S channel to start sending the preloaded audio @@ -440,14 +407,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); i2s_channel_enable(this_speaker->tx_handle_); -#endif } -#ifdef USE_I2S_LEGACY - // The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but - // less accurate approach. - this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written), - esp_timer_get_time() + dma_buffers_duration_ms * 1000); -#endif } } } @@ -496,22 +456,14 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) { esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use -#ifdef USE_I2S_LEGACY - if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT -#else if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT -#endif // Can't reconfigure I2S bus, so the sample rate must match the configured value ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration"); return ESP_ERR_NOT_SUPPORTED; } -#ifdef USE_I2S_LEGACY - if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) { -#else if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO && (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { -#endif // Currently can't handle the case when the incoming audio has more bits per sample than the configured value ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported"); return ESP_ERR_NOT_SUPPORTED; @@ -524,77 +476,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); -#ifdef USE_I2S_LEGACY - i2s_channel_fmt_t channel = this->channel_; - - if (audio_stream_info.get_channels() == 1) { - if (this->channel_ == I2S_CHANNEL_FMT_ONLY_LEFT) { - channel = I2S_CHANNEL_FMT_ONLY_LEFT; - } else { - channel = I2S_CHANNEL_FMT_ONLY_RIGHT; - } - } else if (audio_stream_info.get_channels() == 2) { - channel = I2S_CHANNEL_FMT_RIGHT_LEFT; - } - - i2s_driver_config_t config = { - .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), - .sample_rate = audio_stream_info.get_sample_rate(), - .bits_per_sample = this->bits_per_sample_, - .channel_format = channel, - .communication_format = this->i2s_comm_fmt_, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = DMA_BUFFERS_COUNT, - .dma_buf_len = (int) dma_buffer_length, - .use_apll = this->use_apll_, - .tx_desc_auto_clear = true, - .fixed_mclk = I2S_PIN_NO_CHANGE, - .mclk_multiple = this->mclk_multiple_, - .bits_per_chan = this->bits_per_channel_, -#if SOC_I2S_SUPPORTS_TDM - .chan_mask = (i2s_channel_t) (I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1), - .total_chan = 2, - .left_align = false, - .big_edin = false, - .bit_order_msb = false, - .skip_msk = false, -#endif - }; -#if SOC_I2S_SUPPORTS_DAC - if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN); - } -#endif - - esp_err_t err = - i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to install I2S legacy driver"); - // Failed to install the driver, so unlock the I2S port - this->parent_->unlock(); - return err; - } - -#if SOC_I2S_SUPPORTS_DAC - if (this->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) { -#endif - i2s_pin_config_t pin_config = this->parent_->get_pin_config(); - pin_config.data_out_num = this->dout_pin_; - - err = i2s_set_pin(this->parent_->get_port(), &pin_config); -#if SOC_I2S_SUPPORTS_DAC - } else { - i2s_set_dac_mode(this->internal_dac_mode_); - } -#endif - - if (err != ESP_OK) { - // Failed to set the data out pin, so uninstall the driver and unlock the I2S port - ESP_LOGE(TAG, "Failed to set the data out pin"); - i2s_driver_uninstall(this->parent_->get_port()); - this->parent_->unlock(); - } -#else i2s_chan_config_t chan_cfg = { .id = this->parent_->get_port(), .role = this->i2s_role_, @@ -683,12 +564,10 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea } i2s_channel_enable(this->tx_handle_); -#endif return err; } -#ifndef USE_I2S_LEGACY bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { int64_t now = esp_timer_get_time(); @@ -709,16 +588,11 @@ bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_eve return need_yield1 | need_yield2 | need_yield3; } -#endif void I2SAudioSpeaker::stop_i2s_driver_() { -#ifdef USE_I2S_LEGACY - i2s_driver_uninstall(this->parent_->get_port()); -#else i2s_channel_disable(this->tx_handle_); i2s_del_channel(this->tx_handle_); this->tx_handle_ = nullptr; -#endif this->parent_->unlock(); } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 93ec754178e..76b66922094 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -29,16 +29,8 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } void set_timeout(uint32_t ms) { this->timeout_ = ms; } -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_DAC - void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } -#endif - void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } - void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; } -#else void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; } void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); } -#endif void start() override; void stop() override; @@ -83,14 +75,12 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. void stop_(bool wait_on_empty); -#ifndef USE_I2S_LEGACY /// @brief Callback function used to send playback timestamps the to the speaker task. /// @param handle (i2s_chan_handle_t) /// @param event (i2s_event_data_t) /// @param user_ctx (void*) User context pointer that the callback accesses /// @return True if a higher priority task was interrupted static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); -#endif /// @brief Starts the ESP32 I2S driver. /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out @@ -124,17 +114,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info -#ifdef USE_I2S_LEGACY -#if SOC_I2S_SUPPORTS_DAC - i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; -#endif - uint8_t dout_pin_; - i2s_comm_format_t i2s_comm_fmt_; -#else gpio_num_t dout_pin_; std::string i2s_comm_fmt_; i2s_chan_handle_t tx_handle_; -#endif }; } // namespace i2s_audio diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 390ac8ddd7f..09269ea1b4d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -185,7 +185,6 @@ #ifdef USE_ARDUINO #define USE_PROMETHEUS #define USE_WIFI_WPA2_EAP -#define USE_I2S_LEGACY #endif // Platforms with native 64-bit time sources (no rollover tracking needed) diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml deleted file mode 100644 index 4276b4f9225..00000000000 --- a/tests/components/i2s_audio/test.esp32-ard.yaml +++ /dev/null @@ -1,16 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO15 - i2s_lrclk_pin: GPIO4 - i2s_mclk_pin: GPIO5 - -<<: !include common.yaml - -wifi: - ssid: test - password: test1234 - -media_player: - - platform: i2s_audio - name: Test Media Player - dac_type: internal - mode: stereo diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index c83ee89ad43..89600f70f60 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -1,18 +1,18 @@ -wifi: - ssid: MySSID - password: password1 - i2s_audio: i2s_lrclk_pin: 13 i2s_bclk_pin: 14 i2s_mclk_pin: 15 -media_player: +speaker: - platform: i2s_audio - name: None - dac_type: external + id: test_speaker i2s_dout_pin: 18 - mute_pin: 19 + dac_type: external + +media_player: + - platform: speaker + name: None + speaker: test_speaker on_state: - media_player.play: - media_player.play_media: http://localhost/media.mp3 From ef0eef8117af41b994b0f86c265ff1c5dc077dbf Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 18 Mar 2026 16:57:33 -0500 Subject: [PATCH 0792/2056] [const] Move shared volume constants to components/const (#14935) --- esphome/components/const/__init__.py | 4 ++++ esphome/components/speaker/media_player/__init__.py | 10 ++++++---- esphome/components/speaker_source/media_player.py | 10 ++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index f6da32569fe..2a972a2939a 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -21,6 +21,10 @@ CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_STOP_BITS = "stop_bits" CONF_USE_PSRAM = "use_psram" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_INITIAL = "volume_initial" +CONF_VOLUME_MAX = "volume_max" +CONF_VOLUME_MIN = "volume_min" ICON_CURRENT_DC = "mdi:current-dc" ICON_SOLAR_PANEL = "mdi:solar-panel" diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 92a178fe95e..b16f882cbad 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -7,6 +7,12 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg from esphome.components import audio, esp32, media_player, network, ota, psram, speaker +from esphome.components.const import ( + CONF_VOLUME_INCREMENT, + CONF_VOLUME_INITIAL, + CONF_VOLUME_MAX, + CONF_VOLUME_MIN, +) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -55,10 +61,6 @@ CONF_ON_MUTE = "on_mute" CONF_ON_UNMUTE = "on_unmute" CONF_ON_VOLUME = "on_volume" CONF_STREAM = "stream" -CONF_VOLUME_INCREMENT = "volume_increment" -CONF_VOLUME_INITIAL = "volume_initial" -CONF_VOLUME_MIN = "volume_min" -CONF_VOLUME_MAX = "volume_max" speaker_ns = cg.esphome_ns.namespace("speaker") diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index fe50536e8fd..7f0f776ee5f 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -1,6 +1,12 @@ from esphome import automation import esphome.codegen as cg from esphome.components import audio, media_player, media_source, speaker +from esphome.components.const import ( + CONF_VOLUME_INCREMENT, + CONF_VOLUME_INITIAL, + CONF_VOLUME_MAX, + CONF_VOLUME_MIN, +) import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, @@ -27,10 +33,6 @@ CONF_PIPELINE = "pipeline" CONF_ON_UNMUTE = "on_unmute" CONF_ON_VOLUME = "on_volume" CONF_SOURCES = "sources" -CONF_VOLUME_INCREMENT = "volume_increment" -CONF_VOLUME_INITIAL = "volume_initial" -CONF_VOLUME_MAX = "volume_max" -CONF_VOLUME_MIN = "volume_min" speaker_source_ns = cg.esphome_ns.namespace("speaker_source") From 9f4c7739636bbb4d9d66c92d1b5b5c6511ea1d72 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 18 Mar 2026 17:34:16 -0500 Subject: [PATCH 0793/2056] [media_source] Add request helpers for smart sources (#14936) --- .../components/media_source/media_source.h | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/esphome/components/media_source/media_source.h b/esphome/components/media_source/media_source.h index f21ba486b87..ec2fcbc44f8 100644 --- a/esphome/components/media_source/media_source.h +++ b/esphome/components/media_source/media_source.h @@ -153,6 +153,31 @@ class MediaSource { } } + // === Listener request helpers for smart sources === + // Smart sources (those with internal playlists or external control) use these to request + // the orchestrator to take actions. Simple sources never call these. + + /// @brief Request the orchestrator to play a new URI + void request_play_uri_(const std::string &uri) { + if (this->listener_ != nullptr) { + this->listener_->request_play_uri(uri); + } + } + + /// @brief Request the orchestrator to change volume + void request_volume_(float volume) { + if (this->listener_ != nullptr) { + this->listener_->request_volume(volume); + } + } + + /// @brief Request the orchestrator to change mute state + void request_mute_(bool is_muted) { + if (this->listener_ != nullptr) { + this->listener_->request_mute(is_muted); + } + } + private: // Private to enforce the invariant that listener notifications always fire on state changes. // All state transitions must go through set_state_() which couples the update with notification. From a2a048e3bf28947b65bf62e7633f3eb8dbdb3340 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 13:02:07 -1000 Subject: [PATCH 0794/2056] [ld2412] Inline trivial gate threshold number setters (#14937) --- esphome/components/ld2412/ld2412.cpp | 7 ------- esphome/components/ld2412/ld2412.h | 6 ++++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 6ff6963e9f7..38e1a59abab 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -825,13 +825,6 @@ void LD2412Component::get_gate_threshold() { this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0); } -void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { - this->gate_still_threshold_numbers_[gate] = n; -} - -void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { - this->gate_move_threshold_numbers_[gate] = n; -} #endif void LD2412Component::set_light_out_control() { diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 5dd5e7bcdea..7fd22459787 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -85,8 +85,10 @@ class LD2412Component : public Component, public uart::UARTDevice { void set_light_out_control(); void set_basic_config(); #ifdef USE_NUMBER - void set_gate_move_threshold_number(uint8_t gate, number::Number *n); - void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_move_threshold_number(uint8_t gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; } + void set_gate_still_threshold_number(uint8_t gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; + } void set_gate_threshold(); void get_gate_threshold(); #endif From 5856d05701bd10b052a8bdade43e5e106694c788 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 14:05:57 -1000 Subject: [PATCH 0795/2056] [core] Devirtualize PollingComponent::set_update_interval (#14938) --- esphome/components/sds011/sds011.h | 2 -- esphome/components/sds011/sensor.py | 7 +++++-- esphome/core/component.cpp | 1 - esphome/core/component.h | 4 +--- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 3be74e66d10..d65299c6350 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -21,8 +21,6 @@ class SDS011Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; - void set_update_interval(uint32_t val) { /* ignore */ - } void set_update_interval_min(uint8_t update_interval_min); void set_working_state(bool working_state); diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index ae1cc58a950..76abc70bb70 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -64,12 +64,15 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Pop update_interval before register_component so it doesn't generate + # a set_update_interval call — sds011 handles this via set_update_interval_min + update_interval = config.pop(CONF_UPDATE_INTERVAL, None) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - if CONF_UPDATE_INTERVAL in config: - cg.add(var.set_update_interval_min(config[CONF_UPDATE_INTERVAL])) + if update_interval is not None: + cg.add(var.set_update_interval_min(update_interval)) cg.add(var.set_rx_mode_only(config[CONF_RX_ONLY])) if CONF_PM_2_5 in config: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index bfe9beb2723..a9e93f9131e 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -508,7 +508,6 @@ void PollingComponent::stop_poller() { } uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } -void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void __attribute__((noinline, cold)) WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 557ba09bbc9..02598b53ff5 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -548,12 +548,10 @@ class PollingComponent : public Component { explicit PollingComponent(uint32_t update_interval); /** Manually set the update interval in ms for this polling object. - * - * Override this if you want to do some validation for the update interval. * * @param update_interval The update interval in ms. */ - virtual void set_update_interval(uint32_t update_interval); + void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } // ========== OVERRIDE METHODS ========== // (You'll only need this when creating your own custom sensor) From 44037c4f9ba5d81b68fc0f5e367bc08821f52337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 14:06:41 -1000 Subject: [PATCH 0796/2056] [http_request] Prevent double update task launch (#14910) --- .../components/http_request/update/http_request_update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index a15dc616753..1c52a281059 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -74,6 +74,10 @@ void HttpRequestUpdate::update() { } this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 + if (this->update_task_handle_ != nullptr) { + ESP_LOGW(TAG, "Update check already in progress"); + return; + } xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else this->update_task(this); @@ -204,6 +208,9 @@ defer: // both success and error paths to avoid multiple std::function instantiations. // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. this_update->defer([this_update, result]() { +#ifdef USE_ESP32 + this_update->update_task_handle_ = nullptr; +#endif if (result->error_str != nullptr) { this_update->status_set_error(result->error_str); delete result; From 4d86049c217b5e170ffba1cfe75847526c25df70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 14:06:55 -1000 Subject: [PATCH 0797/2056] [ota] Pack deferred state args into uint32 to avoid heap allocation (#14922) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/ota/ota_backend.cpp | 13 +++++++++++++ esphome/components/ota/ota_backend.h | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 8fb9f672145..01a18a58efe 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -13,6 +13,19 @@ OTAGlobalCallback *get_global_ota_callback() { return global_ota_callback; } +void OTAComponent::notify_state_deferred_(OTAState state, float progress, uint8_t error) { + // Pack state, error, and progress into a single uint32_t so the lambda + // captures only [this, packed] (8 bytes) — fits in std::function SBO. + // Layout: [state:8][error:8][progress_fixed:16] where progress is 0–10000 (0.01% resolution) + static_assert(OTA_ERROR <= 0xFF, "OTAState must fit in 8 bits for packing"); + uint32_t packed = (static_cast<uint32_t>(state) << 24) | (static_cast<uint32_t>(error) << 16) | + static_cast<uint16_t>(progress * 100.0f); + this->defer([this, packed]() { + this->notify_state_(static_cast<OTAState>(packed >> 24), static_cast<float>(packed & 0xFFFF) / 100.0f, + static_cast<uint8_t>(packed >> 16)); + }); +} + void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) { for (auto *listener : this->state_listeners_) { listener->on_ota_state(state, progress, error); diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index bc603a6e9e2..ab0ec58e8a8 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -73,9 +73,7 @@ class OTAComponent : public Component { * This should be used by OTA implementations that run in separate tasks * (like web_server OTA) to ensure listeners execute in the main loop. */ - void notify_state_deferred_(OTAState state, float progress, uint8_t error) { - this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); }); - } + void notify_state_deferred_(OTAState state, float progress, uint8_t error); std::vector<OTAStateListener *> state_listeners_; #endif From a50d70c8d3ea39b27681bfe6e8f5a62519dfe760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 14:08:03 -1000 Subject: [PATCH 0798/2056] [core] Remove call_loop_ wrapper and call loop() directly (#14931) --- esphome/core/component.cpp | 5 ++--- esphome/core/component.h | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9e93f9131e..00dda0cc268 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -209,7 +209,6 @@ bool Component::cancel_retry(uint32_t id) { #pragma GCC diagnostic pop } -void Component::call_loop_() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config_() { this->dump_config(); @@ -259,11 +258,11 @@ void Component::call() { case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop this->set_component_state_(COMPONENT_STATE_LOOP); - this->call_loop_(); + this->loop(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop - this->call_loop_(); + this->loop(); break; case COMPONENT_STATE_FAILED: // State failed: Do nothing diff --git a/esphome/core/component.h b/esphome/core/component.h index 02598b53ff5..119681f64c4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -301,7 +301,6 @@ class Component { protected: friend class Application; - void call_loop_(); virtual void call_setup(); void call_dump_config_(); From 2c10adba858b409fc5a7f445863fbda6589afc04 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:09:03 +1300 Subject: [PATCH 0799/2056] Bump version to 2026.3.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 53eae489669..d8a030536e9 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0b5 +PROJECT_NUMBER = 2026.3.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 579235ff69a..756ad694647 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0b5" +__version__ = "2026.3.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From f8be27ce6d07d42e13dbbce08d5f6252ea4a2acf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:42 -0400 Subject: [PATCH 0800/2056] [ble_client] Fix RSSI sensor reporting same value for all clients (#14939) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index dc032a7a98b..715298e5928 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl switch (event) { // server response on RSSI request: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr)) + return; if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); From 403ba262c683dab7266d54ed56807ad6074d8718 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:23:47 -0400 Subject: [PATCH 0801/2056] [openthread] Guard InstanceLock against uninitialized semaphore (#14940) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/openthread/openthread.h | 4 ++++ .../components/openthread/openthread_esp.cpp | 22 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 75d8fe11fd7..bd10774fcf5 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -11,6 +11,7 @@ #include <openthread/instance.h> #include <openthread/thread.h> +#include <atomic> #include <optional> #include <vector> @@ -28,6 +29,8 @@ class OpenThreadComponent : public Component { float get_setup_priority() const override { return setup_priority::WIFI; } bool is_connected() const { return this->connected_; } + /// Returns true once esp_openthread_init() has completed and the OT lock is usable. + bool is_lock_initialized() const { return this->lock_initialized_; } network::IPAddresses get_ip_addresses(); std::optional<otIp6Address> get_omr_address(); void ot_main(); @@ -51,6 +54,7 @@ class OpenThreadComponent : public Component { uint32_t poll_period_{0}; #endif std::optional<int8_t> output_power_{}; + std::atomic<bool> lock_initialized_{false}; bool teardown_started_{false}; bool teardown_complete_{false}; bool connected_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9cc9223b523..27712bd86ad 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -8,6 +8,7 @@ #include "esp_openthread_lock.h" #include "esp_task_wdt.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Mark lock as initialized so InstanceLock callers know it's safe to acquire. + // Must be set after esp_openthread_init() which creates the internal semaphore. + this->lock_initialized_ = true; // Fetch OT instance once to avoid repeated call into OT stack otInstance *instance = esp_openthread_get_instance(); @@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() { esp_openthread_launch_mainloop(); - // Clean up + // Clean up - reset lock flag before deinit destroys the semaphore + this->lock_initialized_ = false; esp_openthread_deinit(); esp_openthread_netif_glue_deinit(); esp_netif_destroy(openthread_netif); @@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { + if (!global_openthread_component->is_lock_initialized()) { + return {}; + } if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); } @@ -217,6 +225,18 @@ std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { } InstanceLock InstanceLock::acquire() { + // Wait for the lock to be created by ot_main() before attempting to acquire it. + // esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init(). + constexpr uint32_t lock_init_timeout_ms = 10000; + uint32_t start = millis(); + while (!global_openthread_component->is_lock_initialized()) { + if (millis() - start > lock_init_timeout_ms) { + ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms); + abort(); + } + delay(10); + esp_task_wdt_reset(); + } while (!esp_openthread_lock_acquire(100)) { esp_task_wdt_reset(); } From 8fe36cde23f7974683eab4090a1639890ef1b41d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 18:41:05 -1000 Subject: [PATCH 0802/2056] [core] Replace std::function with lightweight Callback in CallbackManager (#14853) --- .../alarm_control_panel.cpp | 16 ---- .../alarm_control_panel/alarm_control_panel.h | 31 ++++---- esphome/components/button/button.cpp | 1 - esphome/components/button/button.h | 4 +- esphome/components/canbus/canbus.h | 5 +- esphome/components/climate/climate.cpp | 8 -- esphome/components/climate/climate.h | 8 +- esphome/components/cover/cover.cpp | 1 - esphome/components/cover/cover.h | 2 +- esphome/components/datetime/datetime_base.h | 4 +- esphome/components/dfplayer/dfplayer.h | 4 +- .../components/display_menu_base/menu_item.h | 10 +-- .../esp32_improv/esp32_improv_component.h | 4 +- esphome/components/event/event.cpp | 4 - esphome/components/event/event.h | 4 +- esphome/components/ezo/ezo.h | 20 ++--- .../components/factory_reset/factory_reset.h | 4 +- esphome/components/fan/fan.cpp | 1 - esphome/components/fan/fan.h | 4 +- .../fingerprint_grow/fingerprint_grow.h | 32 ++++---- .../graphical_display_menu.h | 2 +- esphome/components/haier/haier_base.cpp | 4 - esphome/components/haier/haier_base.h | 4 +- esphome/components/haier/hon_climate.cpp | 8 -- esphome/components/haier/hon_climate.h | 8 +- esphome/components/hlk_fm22x/hlk_fm22x.h | 25 +++---- .../components/key_collector/key_collector.h | 12 +-- .../components/key_provider/key_provider.cpp | 4 - .../components/key_provider/key_provider.h | 2 +- esphome/components/ld2450/ld2450.cpp | 4 - esphome/components/ld2450/ld2450.h | 2 +- esphome/components/lock/lock.cpp | 2 - esphome/components/lock/lock.h | 4 +- esphome/components/ltr501/ltr501.h | 8 +- esphome/components/ltr_als_ps/ltr_als_ps.h | 8 +- esphome/components/lvgl/lvgl_esphome.h | 4 +- .../components/media_player/media_player.cpp | 4 - .../components/media_player/media_player.h | 4 +- esphome/components/microphone/microphone.cpp | 19 ----- esphome/components/microphone/microphone.h | 11 ++- .../microphone/microphone_source.cpp | 18 ----- .../components/microphone/microphone_source.h | 17 ++++- .../modbus_controller/modbus_controller.cpp | 12 --- .../modbus_controller/modbus_controller.h | 12 ++- esphome/components/nextion/nextion.cpp | 24 ------ esphome/components/nextion/nextion.h | 22 ++++-- esphome/components/number/number.cpp | 4 - esphome/components/number/number.h | 4 +- .../components/online_image/online_image.cpp | 8 -- .../components/online_image/online_image.h | 8 +- esphome/components/opentherm/hub.h | 8 +- esphome/components/pid/pid_climate.h | 4 +- esphome/components/pn532/pn532.h | 4 +- esphome/components/pn7150/pn7150.h | 8 +- esphome/components/pn7160/pn7160.h | 8 +- esphome/components/rf_bridge/rf_bridge.h | 8 +- .../rotary_encoder/rotary_encoder.h | 10 +-- esphome/components/rtttl/rtttl.h | 4 +- esphome/components/safe_mode/safe_mode.h | 4 +- esphome/components/sdl/sdl_esphome.h | 4 +- esphome/components/select/select.cpp | 4 - esphome/components/select/select.h | 4 +- esphome/components/sensor/sensor.cpp | 5 -- esphome/components/sensor/sensor.h | 6 +- esphome/components/sim800l/sim800l.h | 20 ++--- esphome/components/sml/sml.cpp | 4 - esphome/components/sml/sml.h | 2 +- esphome/components/speaker/speaker.h | 4 +- esphome/components/switch/switch.cpp | 3 - esphome/components/switch/switch.h | 4 +- esphome/components/text/text.cpp | 4 - esphome/components/text/text.h | 4 +- .../components/text_sensor/text_sensor.cpp | 7 -- esphome/components/text_sensor/text_sensor.h | 6 +- esphome/components/time/real_time_clock.h | 6 +- esphome/components/tuya/tuya.h | 4 +- esphome/components/uart/uart_component.h | 4 +- esphome/components/udp/udp_component.h | 4 +- esphome/components/update/update_entity.h | 4 +- esphome/components/valve/valve.cpp | 1 - esphome/components/valve/valve.h | 2 +- esphome/components/zephyr/cdc_acm.h | 4 +- esphome/components/zigbee/zigbee_zephyr.h | 2 +- esphome/core/entity_base.h | 24 +++--- esphome/core/helpers.h | 75 ++++++++++++++++--- 85 files changed, 319 insertions(+), 385 deletions(-) delete mode 100644 esphome/components/microphone/microphone.cpp diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index ab0a780cefb..fb61776532f 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } } -void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) { - this->cleared_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) { - this->chime_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) { - this->ready_callback_.add(std::move(callback)); -} - void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code) { auto call = this->make_call(); diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index e8dc197e26f..cf99d359e75 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase { * * @param callback The callback function */ - void add_on_state_callback(std::function<void()> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } - /** Add a callback for when the state of the alarm_control_panel clears from triggered - * - * @param callback The callback function - */ - void add_on_cleared_callback(std::function<void()> &&callback); + /** Add a callback for when the state of the alarm_control_panel clears from triggered. */ + template<typename F> void add_on_cleared_callback(F &&callback) { + this->cleared_callback_.add(std::forward<F>(callback)); + } - /** Add a callback for when a chime zone goes from closed to open - * - * @param callback The callback function - */ - void add_on_chime_callback(std::function<void()> &&callback); + /** Add a callback for when a chime zone goes from closed to open. */ + template<typename F> void add_on_chime_callback(F &&callback) { + this->chime_callback_.add(std::forward<F>(callback)); + } - /** Add a callback for when a ready state changes - * - * @param callback The callback function - */ - void add_on_ready_callback(std::function<void()> &&callback); + /** Add a callback for when a ready state changes. */ + template<typename F> void add_on_ready_callback(F &&callback) { + this->ready_callback_.add(std::forward<F>(callback)); + } /** A numeric representation of the supported features as per HomeAssistant * diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 8c06cfe59b8..b1c491805e0 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -20,6 +20,5 @@ void Button::press() { this->press_action(); this->press_callback_.call(); } -void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); } } // namespace esphome::button diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 0f7576a419f..96e9107532b 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -34,7 +34,9 @@ class Button : public EntityBase { * * @param callback The void() callback. */ - void add_on_press_callback(std::function<void()> &&callback); + template<typename F> void add_on_press_callback(F &&callback) { + this->press_callback_.add(std::forward<F>(callback)); + } protected: /** You should implement this virtual method if you want to create your own button. diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index f7b84111bd5..420125e1d34 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -91,10 +91,7 @@ class Canbus : public Component { * - rtr If this is a remote transmission request * - data The message data */ - void add_callback( - std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) { - this->callback_manager_.add(std::move(callback)); - } + template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); } protected: template<typename... Ts> friend class CanbusSendAction; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 43d25effa33..3f44b986dc0 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -356,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) return *this; } -void Climate::add_on_state_callback(std::function<void(Climate &)> &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callback) { - this->control_callback_.add(std::move(callback)); -} - // Random 32bit value; If this changes existing restore preferences are invalidated static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index aa9ca91bc2c..e2cb743c0a6 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -192,7 +192,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_state_callback(std::function<void(Climate &)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } /** * Add a callback for the climate device configuration; each time the configuration parameters of a climate device @@ -200,7 +202,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_control_callback(std::function<void(ClimateCall &)> &&callback); + template<typename F> void add_on_control_callback(F &&callback) { + this->control_callback_.add(std::forward<F>(callback)); + } /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0589aa23796..bb5965d861b 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -139,7 +139,6 @@ bool CoverCall::get_stop() const { return this->stop_; } CoverCall Cover::make_call() { return {this}; } -void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); } void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8cf9aa092aa..9a75e684871 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -125,7 +125,7 @@ class Cover : public EntityBase { /// Construct a new cover call used to control the cover. CoverCall make_call(); - void add_on_state_callback(std::function<void()> &&f); + template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); } /** Publish the current state of the cover. * diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 1b0b3d54639..98f23aa7137 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase { public: virtual ESPTime state_as_esptime() const = 0; - void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } #ifdef USE_TIME void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; } diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 03d2230ca68..2c4ee03470d 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component { bool is_playing() { return is_playing_; } void dump_config() override; - void add_on_finished_playback_callback(std::function<void()> callback) { - this->on_finished_playback_callback_.add(std::move(callback)); + template<typename F> void add_on_finished_playback_callback(F &&callback) { + this->on_finished_playback_callback_.add(std::forward<F>(callback)); } protected: diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index 36de1460310..57d7350b9e7 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -44,9 +44,9 @@ class MenuItem { MenuItemMenu *get_parent() { return this->parent_; } MenuItemType get_type() const { return this->item_type_; } template<typename V> void set_text(V val) { this->text_ = val; } - void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); } - void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); } - void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); } + template<typename F> void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward<F>(cb)); } + template<typename F> void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward<F>(cb)); } + template<typename F> void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward<F>(cb)); } std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); } virtual bool get_immediate_edit() const { return false; } @@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem { class MenuItemCustom : public MenuItemEditable { public: explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {} - void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); } - void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); } + template<typename F> void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward<F>(cb)); } + template<typename F> void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward<F>(cb)); } bool has_value() const override { return this->value_getter_.has_value(); } std::string get_value_text() const override; diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 8f4cfd79581..41799f23251 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK - void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) { - this->state_callback_.add(std::move(callback)); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); } #endif #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 667d4218f3c..ec63fd9c3eb 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -45,9 +45,5 @@ void Event::set_event_types(const std::vector<const char *> &event_types) { this->last_event_type_ = nullptr; // Reset when types change } -void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) { - this->event_callback_.add(std::move(callback)); -} - } // namespace event } // namespace esphome diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 5b6a94b47c0..ebbee0bfe23 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -66,7 +66,9 @@ class Event : public EntityBase { /// Check if an event has been triggered. bool has_event() const { return this->last_event_type_ != nullptr; } - void add_on_event_callback(std::function<void(StringRef event_type)> &&callback); + template<typename F> void add_on_event_callback(F &&callback) { + this->event_callback_.add(std::forward<F>(callback)); + } protected: LazyCallbackManager<void(StringRef event_type)> event_callback_; diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index f1a2802cbd7..d80869fbd9a 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -44,8 +44,8 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 // Device Information void get_device_information(); - void add_device_infomation_callback(std::function<void(std::string)> &&callback) { - this->device_infomation_callback_.add(std::move(callback)); + template<typename F> void add_device_infomation_callback(F &&callback) { + this->device_infomation_callback_.add(std::forward<F>(callback)); } // Sleep @@ -56,15 +56,13 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 // Slope void get_slope(); - void add_slope_callback(std::function<void(std::string)> &&callback) { - this->slope_callback_.add(std::move(callback)); - } + template<typename F> void add_slope_callback(F &&callback) { this->slope_callback_.add(std::forward<F>(callback)); } // T void get_t(); void set_t(float value); void set_tempcomp_value(float temp); // For backwards compatibility - void add_t_callback(std::function<void(std::string)> &&callback) { this->t_callback_.add(std::move(callback)); } + template<typename F> void add_t_callback(F &&callback) { this->t_callback_.add(std::forward<F>(callback)); } // Calibration void get_calibration(); @@ -73,20 +71,18 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void set_calibration_point_high(float value); void set_calibration_generic(float value); void clear_calibration(); - void add_calibration_callback(std::function<void(std::string)> &&callback) { - this->calibration_callback_.add(std::move(callback)); + template<typename F> void add_calibration_callback(F &&callback) { + this->calibration_callback_.add(std::forward<F>(callback)); } // LED void get_led_state(); void set_led_state(bool on); - void add_led_state_callback(std::function<void(bool)> &&callback) { this->led_callback_.add(std::move(callback)); } + template<typename F> void add_led_state_callback(F &&callback) { this->led_callback_.add(std::forward<F>(callback)); } // Custom void send_custom(const std::string &to_send); - void add_custom_callback(std::function<void(std::string)> &&callback) { - this->custom_callback_.add(std::move(callback)); - } + template<typename F> void add_custom_callback(F &&callback) { this->custom_callback_.add(std::forward<F>(callback)); } protected: std::deque<std::unique_ptr<EzoCommand>> commands_; diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 990bb2edb66..34f89d73b60 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -17,8 +17,8 @@ class FactoryResetComponent : public Component { void dump_config() override; void setup() override; - void add_increment_callback(std::function<void(uint8_t, uint8_t)> &&callback) { - this->increment_callback_.add(std::move(callback)); + template<typename F> void add_increment_callback(F &&callback) { + this->increment_callback_.add(std::forward<F>(callback)); } protected: diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index c1e0a3dc2e1..97336e17b58 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -193,7 +193,6 @@ void Fan::apply_preset_mode_(const FanCall &call) { } } -void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 2caf3a712a2..e7b3681e32e 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -122,7 +122,9 @@ class Fan : public EntityBase { FanCall make_call(); /// Register a callback that will be called each time the state changes. - void add_on_state_callback(std::function<void()> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } void publish_state(); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index db9d5ce564f..63839534f64 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -127,30 +127,30 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { this->enrolling_binary_sensor_ = enrolling_binary_sensor; } - void add_on_finger_scan_start_callback(std::function<void()> callback) { - this->finger_scan_start_callback_.add(std::move(callback)); + template<typename F> void add_on_finger_scan_start_callback(F &&callback) { + this->finger_scan_start_callback_.add(std::forward<F>(callback)); } - void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) { - this->finger_scan_matched_callback_.add(std::move(callback)); + template<typename F> void add_on_finger_scan_matched_callback(F &&callback) { + this->finger_scan_matched_callback_.add(std::forward<F>(callback)); } - void add_on_finger_scan_unmatched_callback(std::function<void()> callback) { - this->finger_scan_unmatched_callback_.add(std::move(callback)); + template<typename F> void add_on_finger_scan_unmatched_callback(F &&callback) { + this->finger_scan_unmatched_callback_.add(std::forward<F>(callback)); } - void add_on_finger_scan_misplaced_callback(std::function<void()> callback) { - this->finger_scan_misplaced_callback_.add(std::move(callback)); + template<typename F> void add_on_finger_scan_misplaced_callback(F &&callback) { + this->finger_scan_misplaced_callback_.add(std::forward<F>(callback)); } - void add_on_finger_scan_invalid_callback(std::function<void()> callback) { - this->finger_scan_invalid_callback_.add(std::move(callback)); + template<typename F> void add_on_finger_scan_invalid_callback(F &&callback) { + this->finger_scan_invalid_callback_.add(std::forward<F>(callback)); } - void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) { - this->enrollment_scan_callback_.add(std::move(callback)); + template<typename F> void add_on_enrollment_scan_callback(F &&callback) { + this->enrollment_scan_callback_.add(std::forward<F>(callback)); } - void add_on_enrollment_done_callback(std::function<void(uint16_t)> callback) { - this->enrollment_done_callback_.add(std::move(callback)); + template<typename F> void add_on_enrollment_done_callback(F &&callback) { + this->enrollment_done_callback_.add(std::forward<F>(callback)); } - void add_on_enrollment_failed_callback(std::function<void(uint16_t)> callback) { - this->enrollment_failed_callback_.add(std::move(callback)); + template<typename F> void add_on_enrollment_failed_callback(F &&callback) { + this->enrollment_failed_callback_.add(std::forward<F>(callback)); } void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers); diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 96f2bd79fd5..007889557dd 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -44,7 +44,7 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { void set_foreground_color(Color foreground_color); void set_background_color(Color background_color); - void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + template<typename F> void add_on_redraw_callback(F &&cb) { this->on_redraw_callbacks_.add(std::forward<F>(cb)); } void draw(display::Display *display, const display::Rect *bounds); diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 1882aa439ef..35eaf36d329 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -197,10 +197,6 @@ void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &m this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); } -void HaierClimateBase::add_status_message_callback(std::function<void(const char *, size_t)> &&callback) { - this->status_message_callback_.add(std::move(callback)); -} - haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index e24217bfd9d..87aa1d65eff 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -74,7 +74,9 @@ class HaierClimateBase : public esphome::Component, void set_answer_timeout(uint32_t timeout); void set_send_wifi(bool send_wifi); void send_custom_command(const haier_protocol::HaierMessage &message); - void add_status_message_callback(std::function<void(const char *, size_t)> &&callback); + template<typename F> void add_status_message_callback(F &&callback) { + this->status_message_callback_.add(std::forward<F>(callback)); + } protected: enum class ProtocolPhases { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b8889ef2bdc..b7888f7976e 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -114,14 +114,6 @@ void HonClimate::start_steri_cleaning() { } } -void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) { - this->alarm_start_callback_.add(std::move(callback)); -} - -void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) { - this->alarm_end_callback_.add(std::move(callback)); -} - haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 9bddac3f923..7c48a3748b8 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -124,8 +124,12 @@ class HonClimate : public HaierClimateBase { void set_extra_sensors_packet_bytes_size(size_t size) { this->extra_sensors_packet_bytes_ = size; }; void set_status_message_header_size(size_t size) { this->status_message_header_size_ = size; }; void set_control_method(HonControlMethod method) { this->control_method_ = method; }; - void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback); - void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback); + template<typename F> void add_alarm_start_callback(F &&callback) { + this->alarm_start_callback_.add(std::forward<F>(callback)); + } + template<typename F> void add_alarm_end_callback(F &&callback) { + this->alarm_end_callback_.add(std::forward<F>(callback)); + } float get_active_alarm_count() const { return this->active_alarm_count_; } protected: diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 0ea4636281c..d897d51881b 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -91,24 +91,23 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) { this->version_text_sensor_ = version_text_sensor; } - void add_on_face_scan_matched_callback(std::function<void(int16_t, std::string)> callback) { - this->face_scan_matched_callback_.add(std::move(callback)); + template<typename F> void add_on_face_scan_matched_callback(F &&callback) { + this->face_scan_matched_callback_.add(std::forward<F>(callback)); } - void add_on_face_scan_unmatched_callback(std::function<void()> callback) { - this->face_scan_unmatched_callback_.add(std::move(callback)); + template<typename F> void add_on_face_scan_unmatched_callback(F &&callback) { + this->face_scan_unmatched_callback_.add(std::forward<F>(callback)); } - void add_on_face_scan_invalid_callback(std::function<void(uint8_t)> callback) { - this->face_scan_invalid_callback_.add(std::move(callback)); + template<typename F> void add_on_face_scan_invalid_callback(F &&callback) { + this->face_scan_invalid_callback_.add(std::forward<F>(callback)); } - void add_on_face_info_callback( - std::function<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> callback) { - this->face_info_callback_.add(std::move(callback)); + template<typename F> void add_on_face_info_callback(F &&callback) { + this->face_info_callback_.add(std::forward<F>(callback)); } - void add_on_enrollment_done_callback(std::function<void(int16_t, uint8_t)> callback) { - this->enrollment_done_callback_.add(std::move(callback)); + template<typename F> void add_on_enrollment_done_callback(F &&callback) { + this->enrollment_done_callback_.add(std::forward<F>(callback)); } - void add_on_enrollment_failed_callback(std::function<void(uint8_t)> callback) { - this->enrollment_failed_callback_.add(std::move(callback)); + template<typename F> void add_on_enrollment_failed_callback(F &&callback) { + this->enrollment_failed_callback_.add(std::forward<F>(callback)); } void enroll_face(const std::string &name, HlkFm22xFaceDirection direction); diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 8e30c333df7..014e2034bd3 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -21,14 +21,14 @@ class KeyCollector : public Component { void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; - void add_on_progress_callback(std::function<void(const std::string &, uint8_t)> &&callback) { - this->progress_callbacks_.add(std::move(callback)); + template<typename F> void add_on_progress_callback(F &&callback) { + this->progress_callbacks_.add(std::forward<F>(callback)); } - void add_on_result_callback(std::function<void(const std::string &, uint8_t, uint8_t)> &&callback) { - this->result_callbacks_.add(std::move(callback)); + template<typename F> void add_on_result_callback(F &&callback) { + this->result_callbacks_.add(std::forward<F>(callback)); } - void add_on_timeout_callback(std::function<void(const std::string &, uint8_t)> &&callback) { - this->timeout_callbacks_.add(std::move(callback)); + template<typename F> void add_on_timeout_callback(F &&callback) { + this->timeout_callbacks_.add(std::forward<F>(callback)); } void set_timeout(int timeout) { this->timeout_ = timeout; }; void set_enabled(bool enabled); diff --git a/esphome/components/key_provider/key_provider.cpp b/esphome/components/key_provider/key_provider.cpp index 5a0e24b13fa..64b0729d4d1 100644 --- a/esphome/components/key_provider/key_provider.cpp +++ b/esphome/components/key_provider/key_provider.cpp @@ -3,10 +3,6 @@ namespace esphome { namespace key_provider { -void KeyProvider::add_on_key_callback(std::function<void(uint8_t)> &&callback) { - this->key_callback_.add(std::move(callback)); -} - void KeyProvider::send_key_(uint8_t key) { this->key_callback_.call(key); } } // namespace key_provider diff --git a/esphome/components/key_provider/key_provider.h b/esphome/components/key_provider/key_provider.h index 272d3eecadc..97403427512 100644 --- a/esphome/components/key_provider/key_provider.h +++ b/esphome/components/key_provider/key_provider.h @@ -9,7 +9,7 @@ namespace key_provider { /// interface for components that provide keypresses class KeyProvider { public: - void add_on_key_callback(std::function<void(uint8_t)> &&callback); + template<typename F> void add_on_key_callback(F &&callback) { this->key_callback_.add(std::forward<F>(callback)); } protected: void send_key_(uint8_t key); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index f9701cbdf66..0a1147c9240 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -407,10 +407,6 @@ void LD2450Component::restart_and_read_all_info() { this->set_timeout(1500, [this]() { this->read_all_info(); }); } -void LD2450Component::add_on_data_callback(std::function<void()> &&callback) { - this->data_callback_.add(std::move(callback)); -} - // Send command with values to LD2450 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { ESP_LOGV(TAG, "Sending COMMAND %02X", command); diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 9409dfc21df..e774dd9c756 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -145,7 +145,7 @@ class LD2450Component : public Component, public uart::UARTDevice { int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); /// Add a callback that will be called after each successfully processed periodic data frame. - void add_on_data_callback(std::function<void()> &&callback); + template<typename F> void add_on_data_callback(F &&callback) { this->data_callback_.add(std::forward<F>(callback)); } protected: void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 939c84720bf..4aa636e998c 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -48,8 +48,6 @@ void Lock::publish_state(LockState state) { #endif } -void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } - void LockCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index bebd296eacb..707431d5433 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -150,7 +150,9 @@ class Lock : public EntityBase { * * @param callback The void(bool) callback. */ - void add_on_state_callback(std::function<void()> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } protected: friend LockCall; diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index d9a53c9bd46..2bd838a0fe2 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -160,12 +160,12 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; - void add_on_ps_high_trigger_callback_(std::function<void()> callback) { - this->on_ps_high_trigger_callback_.add(std::move(callback)); + template<typename F> void add_on_ps_high_trigger_callback_(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); } - void add_on_ps_low_trigger_callback_(std::function<void()> callback) { - this->on_ps_low_trigger_callback_.add(std::move(callback)); + template<typename F> void add_on_ps_low_trigger_callback_(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); } }; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 3ab2cea074f..2e24a14283d 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -160,12 +160,12 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; - void add_on_ps_high_trigger_callback_(std::function<void()> callback) { - this->on_ps_high_trigger_callback_.add(std::move(callback)); + template<typename F> void add_on_ps_high_trigger_callback_(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); } - void add_on_ps_low_trigger_callback_(std::function<void()> callback) { - this->on_ps_low_trigger_callback_.add(std::move(callback)); + template<typename F> void add_on_ps_low_trigger_callback_(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); } }; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 9c82f3646bc..aa8dd2fba52 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -158,9 +158,7 @@ class LvglComponent : public PollingComponent { void setup() override; void update() override; void loop() override; - void add_on_idle_callback(std::function<void(uint32_t)> &&callback) { - this->idle_callbacks_.add(std::move(callback)); - } + template<typename F> void add_on_idle_callback(F &&callback) { this->idle_callbacks_.add(std::forward<F>(callback)); } static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); static void render_start_cb(lv_disp_drv_t *disp_drv); diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a53d598b0fe..70086089ff2 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -198,10 +198,6 @@ MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { return *this; } -void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) { - this->state_callback_.add(std::move(callback)); -} - void MediaPlayer::publish_state() { this->state_callback_.call(); #if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 35097477181..26eca469e7f 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -155,7 +155,9 @@ class MediaPlayer : public EntityBase { void publish_state(); - void add_on_state_callback(std::function<void()> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } virtual bool is_muted() const { return false; } diff --git a/esphome/components/microphone/microphone.cpp b/esphome/components/microphone/microphone.cpp deleted file mode 100644 index 0fbb393fd20..00000000000 --- a/esphome/components/microphone/microphone.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "microphone.h" - -namespace esphome { -namespace microphone { - -void Microphone::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) { - std::function<void(const std::vector<uint8_t> &)> mute_handled_callback = - [this, data_callback](const std::vector<uint8_t> &data) { - if (this->mute_state_) { - data_callback(std::vector<uint8_t>(data.size(), 0)); - } else { - data_callback(data); - }; - }; - this->data_callbacks_.add(std::move(mute_handled_callback)); -} - -} // namespace microphone -} // namespace esphome diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index fcf98224581..50ce1a72818 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -4,7 +4,6 @@ #include <cstddef> #include <cstdint> -#include <functional> #include <vector> #include "esphome/core/helpers.h" @@ -22,7 +21,15 @@ class Microphone { public: virtual void start() = 0; virtual void stop() = 0; - void add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback); + template<typename F> void add_data_callback(F &&data_callback) { + this->data_callbacks_.add([this, data_callback](const std::vector<uint8_t> &data) { + if (this->mute_state_) { + data_callback(std::vector<uint8_t>(data.size(), 0)); + } else { + data_callback(data); + } + }); + } bool is_running() const { return this->state_ == STATE_RUNNING; } bool is_stopped() const { return this->state_ == STATE_STOPPED; } diff --git a/esphome/components/microphone/microphone_source.cpp b/esphome/components/microphone/microphone_source.cpp index 00efcf22a14..fb4ebc4a042 100644 --- a/esphome/components/microphone/microphone_source.cpp +++ b/esphome/components/microphone/microphone_source.cpp @@ -6,24 +6,6 @@ namespace microphone { static const int32_t Q25_MAX_VALUE = (1 << 25) - 1; static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE; -void MicrophoneSource::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) { - std::function<void(const std::vector<uint8_t> &)> filtered_callback = - [this, data_callback](const std::vector<uint8_t> &data) { - if (this->enabled_ || this->passive_) { - if (this->processed_samples_.use_count() == 0) { - // Create vector if its unused - this->processed_samples_ = std::make_shared<std::vector<uint8_t>>(); - } - - // Take temporary ownership of samples vector to avoid deallaction before the callback finishes - std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_; - this->process_audio_(data, *output_samples); - data_callback(*output_samples); - } - }; - this->mic_->add_data_callback(std::move(filtered_callback)); -} - audio::AudioStreamInfo MicrophoneSource::get_audio_stream_info() { return audio::AudioStreamInfo(this->bits_per_sample_, this->channels_.count(), this->mic_->get_audio_stream_info().get_sample_rate()); diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 1e81a284b67..5c8053e502e 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -7,7 +7,6 @@ #include <bitset> #include <cstddef> #include <cstdint> -#include <functional> #include <vector> namespace esphome { @@ -47,7 +46,21 @@ class MicrophoneSource { /// @param channel 0-indexed channel number to enable void add_channel(uint8_t channel) { this->channels_.set(channel); } - void add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback); + template<typename F> void add_data_callback(F &&data_callback) { + this->mic_->add_data_callback([this, data_callback](const std::vector<uint8_t> &data) { + if (this->enabled_ || this->passive_) { + if (this->processed_samples_.use_count() == 0) { + // Create vector if its unused + this->processed_samples_ = std::make_shared<std::vector<uint8_t>>(); + } + + // Take temporary ownership of samples vector to avoid deallocation before the callback finishes + std::shared_ptr<std::vector<uint8_t>> output_samples = this->processed_samples_; + this->process_audio_(data, *output_samples); + data_callback(*output_samples); + } + }); + } void set_gain_factor(int32_t gain_factor) { this->gain_factor_ = clamp<int32_t>(gain_factor, 1, MAX_GAIN_FACTOR); } int32_t get_gain_factor() { return this->gain_factor_; } diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index f77f51a20df..ea6ba9d0859 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -837,17 +837,5 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens return value; } -void ModbusController::add_on_command_sent_callback(std::function<void(int, int)> &&callback) { - this->command_sent_callback_.add(std::move(callback)); -} - -void ModbusController::add_on_online_callback(std::function<void(int, int)> &&callback) { - this->online_callback_.add(std::move(callback)); -} - -void ModbusController::add_on_offline_callback(std::function<void(int, int)> &&callback) { - this->offline_callback_.add(std::move(callback)); -} - } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index bd3d4d705ec..78c3b959654 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -508,11 +508,17 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// get if the module is offline, didn't respond the last command bool get_module_offline() { return module_offline_; } /// Set callback for commands - void add_on_command_sent_callback(std::function<void(int, int)> &&callback); + template<typename F> void add_on_command_sent_callback(F &&callback) { + this->command_sent_callback_.add(std::forward<F>(callback)); + } /// Set callback for online changes - void add_on_online_callback(std::function<void(int, int)> &&callback); + template<typename F> void add_on_online_callback(F &&callback) { + this->online_callback_.add(std::forward<F>(callback)); + } /// Set callback for offline changes - void add_on_offline_callback(std::function<void(int, int)> &&callback); + template<typename F> void add_on_offline_callback(F &&callback) { + this->offline_callback_.add(std::forward<F>(callback)); + } /// called by esphome generated code to set the max_cmd_retries. void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } /// get how many times a command will be (re)sent if no response is received diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 01ceb3d765e..85da6af48aa 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -214,30 +214,6 @@ void Nextion::update() { } } -void Nextion::add_sleep_state_callback(std::function<void()> &&callback) { - this->sleep_callback_.add(std::move(callback)); -} - -void Nextion::add_wake_state_callback(std::function<void()> &&callback) { - this->wake_callback_.add(std::move(callback)); -} - -void Nextion::add_setup_state_callback(std::function<void()> &&callback) { - this->setup_callback_.add(std::move(callback)); -} - -void Nextion::add_new_page_callback(std::function<void(uint8_t)> &&callback) { - this->page_callback_.add(std::move(callback)); -} - -void Nextion::add_touch_event_callback(std::function<void(uint8_t, uint8_t, bool)> &&callback) { - this->touch_callback_.add(std::move(callback)); -} - -void Nextion::add_buffer_overflow_event_callback(std::function<void()> &&callback) { - this->buffer_overflow_callback_.add(std::move(callback)); -} - void Nextion::update_all_components() { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 7999e3c4e3e..2842e57ce83 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1138,37 +1138,47 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * @param callback The void() callback. */ - void add_sleep_state_callback(std::function<void()> &&callback); + template<typename F> void add_sleep_state_callback(F &&callback) { + this->sleep_callback_.add(std::forward<F>(callback)); + } /** Add a callback to be notified of wake state changes. * * @param callback The void() callback. */ - void add_wake_state_callback(std::function<void()> &&callback); + template<typename F> void add_wake_state_callback(F &&callback) { + this->wake_callback_.add(std::forward<F>(callback)); + } /** Add a callback to be notified when the nextion completes its initialize setup. * * @param callback The void() callback. */ - void add_setup_state_callback(std::function<void()> &&callback); + template<typename F> void add_setup_state_callback(F &&callback) { + this->setup_callback_.add(std::forward<F>(callback)); + } /** Add a callback to be notified when the nextion changes pages. * * @param callback The void(std::string) callback. */ - void add_new_page_callback(std::function<void(uint8_t)> &&callback); + template<typename F> void add_new_page_callback(F &&callback) { this->page_callback_.add(std::forward<F>(callback)); } /** Add a callback to be notified when Nextion has a touch event. * * @param callback The void() callback. */ - void add_touch_event_callback(std::function<void(uint8_t, uint8_t, bool)> &&callback); + template<typename F> void add_touch_event_callback(F &&callback) { + this->touch_callback_.add(std::forward<F>(callback)); + } /** Add a callback to be notified when the nextion reports a buffer overflow. * * @param callback The void() callback. */ - void add_buffer_overflow_event_callback(std::function<void()> &&callback); + template<typename F> void add_buffer_overflow_event_callback(F &&callback) { + this->buffer_overflow_callback_.add(std::forward<F>(callback)); + } void update_all_components(); diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index c0653c3b304..fb5d6e9f28c 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -29,8 +29,4 @@ void Number::publish_state(float state) { #endif } -void Number::add_on_state_callback(std::function<void(float)> &&callback) { - this->state_callback_.add(std::move(callback)); -} - } // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 0425714702f..579d488cf06 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -34,7 +34,9 @@ class Number : public EntityBase { NumberCall make_call() { return NumberCall(this); } - void add_on_state_callback(std::function<void(float)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } NumberTraits traits; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 22bf6a3056f..24926aa4dcd 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -228,14 +228,6 @@ void OnlineImage::end_connection_() { this->disable_loop(); } -void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) { - this->download_finished_callback_.add(std::move(callback)); -} - -void OnlineImage::add_on_error_callback(std::function<void()> &&callback) { - this->download_error_callback_.add(std::move(callback)); -} - void OnlineImage::release() { // Clear cache headers this->etag_ = ""; diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 12c25645260..3a348cbb07a 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -65,8 +65,12 @@ class OnlineImage : public PollingComponent, */ void release(); - void add_on_finished_callback(std::function<void(bool)> &&callback); - void add_on_error_callback(std::function<void()> &&callback); + template<typename F> void add_on_finished_callback(F &&callback) { + this->download_finished_callback_.add(std::forward<F>(callback)); + } + template<typename F> void add_on_error_callback(F &&callback) { + this->download_error_callback_.add(std::forward<F>(callback)); + } protected: bool validate_url_(const std::string &url); diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index ee0cfd104d1..960e23d6dd3 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -160,11 +160,11 @@ class OpenthermHub : public Component { void set_dhw_block(bool value) { this->dhw_block = value; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } - void add_on_before_send_callback(std::function<void(OpenthermData &)> &&callback) { - this->before_send_callback_.add(std::move(callback)); + template<typename F> void add_on_before_send_callback(F &&callback) { + this->before_send_callback_.add(std::forward<F>(callback)); } - void add_on_before_process_response_callback(std::function<void(OpenthermData &)> &&callback) { - this->before_process_response_callback_.add(std::move(callback)); + template<typename F> void add_on_before_process_response_callback(F &&callback) { + this->before_process_response_callback_.add(std::forward<F>(callback)); } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 3708c29ff1b..479a0e48ee4 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -72,8 +72,8 @@ class PIDClimate : public climate::Climate, public Component { // float get_deadband() const { return controller_.deadband; } // float get_proportional_deadband_multiplier() const { return controller_.proportional_deadband_multiplier; } - void add_on_pid_computed_callback(std::function<void()> &&callback) { - pid_computed_callback_.add(std::move(callback)); + template<typename F> void add_on_pid_computed_callback(F &&callback) { + this->pid_computed_callback_.add(std::forward<F>(callback)); } void set_default_target_temperature(float default_target_temperature) { default_target_temperature_ = default_target_temperature; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index e57ecd81046..1f6a6b3bc3f 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -43,8 +43,8 @@ class PN532 : public PollingComponent { void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } - void add_on_finished_write_callback(std::function<void()> callback) { - this->on_finished_write_callback_.add(std::move(callback)); + template<typename F> void add_on_finished_write_callback(F &&callback) { + this->on_finished_write_callback_.add(std::forward<F>(callback)); } bool is_writing() { return this->next_task_ != READ; }; diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index c5dd2838325..a468d80943e 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -167,12 +167,12 @@ class PN7150 : public nfc::Nfcc, public Component { void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } - void add_on_emulated_tag_scan_callback(std::function<void()> callback) { - this->on_emulated_tag_scan_callback_.add(std::move(callback)); + template<typename F> void add_on_emulated_tag_scan_callback(F &&callback) { + this->on_emulated_tag_scan_callback_.add(std::forward<F>(callback)); } - void add_on_finished_write_callback(std::function<void()> callback) { - this->on_finished_write_callback_.add(std::move(callback)); + template<typename F> void add_on_finished_write_callback(F &&callback) { + this->on_finished_write_callback_.add(std::forward<F>(callback)); } bool is_writing() { return this->next_task_ != EP_READ; }; diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 77ab49399c5..44f7eb07965 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -184,12 +184,12 @@ class PN7160 : public nfc::Nfcc, public Component { void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } - void add_on_emulated_tag_scan_callback(std::function<void()> callback) { - this->on_emulated_tag_scan_callback_.add(std::move(callback)); + template<typename F> void add_on_emulated_tag_scan_callback(F &&callback) { + this->on_emulated_tag_scan_callback_.add(std::forward<F>(callback)); } - void add_on_finished_write_callback(std::function<void()> callback) { - this->on_finished_write_callback_.add(std::move(callback)); + template<typename F> void add_on_finished_write_callback(F &&callback) { + this->on_finished_write_callback_.add(std::forward<F>(callback)); } bool is_writing() { return this->next_task_ != EP_READ; }; diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index c93b636c38c..e5780c9ebe5 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -49,11 +49,11 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { public: void loop() override; void dump_config() override; - void add_on_code_received_callback(std::function<void(RFBridgeData)> callback) { - this->data_callback_.add(std::move(callback)); + template<typename F> void add_on_code_received_callback(F &&callback) { + this->data_callback_.add(std::forward<F>(callback)); } - void add_on_advanced_code_received_callback(std::function<void(RFBridgeAdvancedData)> callback) { - this->advanced_data_callback_.add(std::move(callback)); + template<typename F> void add_on_advanced_code_received_callback(F &&callback) { + this->advanced_data_callback_.add(std::forward<F>(callback)); } void send_code(RFBridgeData data); void send_advanced_code(const RFBridgeAdvancedData &data); diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 865554cd4d5..4b776fe55e9 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -82,15 +82,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void dump_config() override; void loop() override; - void add_on_clockwise_callback(std::function<void()> callback) { - this->on_clockwise_callback_.add(std::move(callback)); + template<typename F> void add_on_clockwise_callback(F &&callback) { + this->on_clockwise_callback_.add(std::forward<F>(callback)); } - void add_on_anticlockwise_callback(std::function<void()> callback) { - this->on_anticlockwise_callback_.add(std::move(callback)); + template<typename F> void add_on_anticlockwise_callback(F &&callback) { + this->on_anticlockwise_callback_.add(std::forward<F>(callback)); } - void register_listener(std::function<void(uint32_t)> listener) { this->listeners_.add(std::move(listener)); } + template<typename F> void register_listener(F &&listener) { this->listeners_.add(std::forward<F>(listener)); } protected: InternalGPIOPin *pin_a_; diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index e37cccae9ef..bff43d2edd8 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -45,8 +45,8 @@ class Rtttl : public Component { bool is_playing() { return this->state_ != State::STOPPED; } - void add_on_finished_playback_callback(std::function<void()> callback) { - this->on_finished_playback_callback_.add(std::move(callback)); + template<typename F> void add_on_finished_playback_callback(F &&callback) { + this->on_finished_playback_callback_.add(std::forward<F>(callback)); } protected: diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 1b28ea28f20..2733054962e 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -34,8 +34,8 @@ class SafeModeComponent final : public Component { void mark_successful(); #ifdef USE_SAFE_MODE_CALLBACK - void add_on_safe_mode_callback(std::function<void()> &&callback) { - this->safe_mode_callback_.add(std::move(callback)); + template<typename F> void add_on_safe_mode_callback(F &&callback) { + this->safe_mode_callback_.add(std::forward<F>(callback)); } #endif diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index bf5fde14282..c025e8ff6e9 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -37,11 +37,11 @@ class Sdl : public display::Display { int get_height() override { return this->height_; } float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } - void add_key_listener(int32_t keycode, std::function<void(bool)> &&callback) { + template<typename F> void add_key_listener(int32_t keycode, F &&callback) { if (!this->key_callbacks_.count(keycode)) { this->key_callbacks_[keycode] = CallbackManager<void(bool)>(); } - this->key_callbacks_[keycode].add(std::move(callback)); + this->key_callbacks_[keycode].add(std::forward<F>(callback)); } int mouse_x{}; diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 91e27b30dee..df90c657e2b 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -42,10 +42,6 @@ StringRef Select::current_option() const { return this->has_state() ? StringRef(this->option_at(this->active_index_)) : StringRef(); } -void Select::add_on_state_callback(std::function<void(size_t)> &&callback) { - this->state_callback_.add(std::move(callback)); -} - bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index c91acd1e19e..465283d92a2 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -76,7 +76,9 @@ class Select : public EntityBase { /// Return the option value at the provided index offset (as const char* from flash). const char *option_at(size_t index) const; - void add_on_state_callback(std::function<void(size_t)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } protected: friend class SelectCall; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index a7af6403efc..b4e59dfeb57 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -79,11 +79,6 @@ void Sensor::publish_state(float state) { #endif } -void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); } -void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) { - this->raw_callback_.add(std::move(callback)); -} - #ifdef USE_SENSOR_FILTER void Sensor::add_filter(Filter *filter) { // inefficient, but only happens once on every sensor setup and nobody's going to have massive amounts of diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 197896f6f68..b3bd9620364 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -111,9 +111,11 @@ class Sensor : public EntityBase { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Add a callback that will be called every time a filtered value arrives. - void add_on_state_callback(std::function<void(float)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { this->callback_.add(std::forward<F>(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function<void(float)> &&callback); + template<typename F> void add_on_raw_state_callback(F &&callback) { + this->raw_callback_.add(std::forward<F>(callback)); + } /** This member variable stores the last state that has passed through all filters. * diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index e9e2f66d789..d79279ea720 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -61,20 +61,20 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { #ifdef USE_SENSOR void set_rssi_sensor(sensor::Sensor *rssi_sensor) { rssi_sensor_ = rssi_sensor; } #endif - void add_on_sms_received_callback(std::function<void(std::string, std::string)> callback) { - this->sms_received_callback_.add(std::move(callback)); + template<typename F> void add_on_sms_received_callback(F &&callback) { + this->sms_received_callback_.add(std::forward<F>(callback)); } - void add_on_incoming_call_callback(std::function<void(std::string)> callback) { - this->incoming_call_callback_.add(std::move(callback)); + template<typename F> void add_on_incoming_call_callback(F &&callback) { + this->incoming_call_callback_.add(std::forward<F>(callback)); } - void add_on_call_connected_callback(std::function<void()> callback) { - this->call_connected_callback_.add(std::move(callback)); + template<typename F> void add_on_call_connected_callback(F &&callback) { + this->call_connected_callback_.add(std::forward<F>(callback)); } - void add_on_call_disconnected_callback(std::function<void()> callback) { - this->call_disconnected_callback_.add(std::move(callback)); + template<typename F> void add_on_call_disconnected_callback(F &&callback) { + this->call_disconnected_callback_.add(std::forward<F>(callback)); } - void add_on_ussd_received_callback(std::function<void(std::string)> callback) { - this->ussd_received_callback_.add(std::move(callback)); + template<typename F> void add_on_ussd_received_callback(F &&callback) { + this->ussd_received_callback_.add(std::forward<F>(callback)); } void send_sms(const std::string &recipient, const std::string &message); void send_ussd(const std::string &ussd_code); diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp index c1ffdc0e689..c8d5fcc2694 100644 --- a/esphome/components/sml/sml.cpp +++ b/esphome/components/sml/sml.cpp @@ -61,10 +61,6 @@ void Sml::loop() { } } -void Sml::add_on_data_callback(std::function<void(std::vector<uint8_t>, bool)> &&callback) { - this->data_callbacks_.add(std::move(callback)); -} - void Sml::process_sml_file_(const BytesView &sml_data) { SmlFile sml_file(sml_data); std::vector<ObisInfo> obis_info = sml_file.get_obis_info(); diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h index 15ca43944cb..29a2f48bbec 100644 --- a/esphome/components/sml/sml.h +++ b/esphome/components/sml/sml.h @@ -24,7 +24,7 @@ class Sml : public Component, public uart::UARTDevice { void loop() override; void dump_config() override; std::vector<SmlListener *> sml_listeners_{}; - void add_on_data_callback(std::function<void(std::vector<uint8_t>, bool)> &&callback); + template<typename F> void add_on_data_callback(F &&callback) { this->data_callbacks_.add(std::forward<F>(callback)); } protected: void process_sml_file_(const BytesView &sml_data); diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 373d2e3a746..5b89d00c69f 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -106,8 +106,8 @@ class Speaker { /// Parameters: /// - Frames played /// - System time in microseconds when the frames were written to the DAC - void add_audio_output_callback(std::function<void(uint32_t, int64_t)> &&callback) { - this->audio_output_callback_.add(std::move(callback)); + template<typename F> void add_audio_output_callback(F &&callback) { + this->audio_output_callback_.add(std::forward<F>(callback)); } protected: diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 9e9af213680..df762addbb1 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -69,9 +69,6 @@ void Switch::publish_state(bool state) { } bool Switch::assumed_state() { return false; } -void Switch::add_on_state_callback(std::function<void(bool)> &&callback) { - this->state_callback_.add(std::move(callback)); -} void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } bool Switch::is_inverted() const { return this->inverted_; } diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index c4f8525793a..b7761cba0a1 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -93,7 +93,9 @@ class Switch : public EntityBase { * * @param callback The void(bool) callback. */ - void add_on_state_callback(std::function<void(bool)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } /** Returns the initial state of the switch, as persisted previously, or empty if never persisted. diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index d8ab6b1b92c..12abc5d9390 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -29,8 +29,4 @@ void Text::publish_state(const char *state, size_t len) { #endif } -void Text::add_on_state_callback(std::function<void(const std::string &)> &&callback) { - this->state_callback_.add(std::move(callback)); -} - } // namespace esphome::text diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 7d255e56880..eb6a68f9980 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -30,7 +30,9 @@ class Text : public EntityBase { /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } - void add_on_state_callback(std::function<void(const std::string &)> &&callback); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } protected: friend class TextCall; diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 91561c5f420..aa49a85d265 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -83,13 +83,6 @@ void TextSensor::clear_filters() { } #endif // USE_TEXT_SENSOR_FILTER -void TextSensor::add_on_state_callback(std::function<void(const std::string &)> callback) { - this->callback_.add(std::move(callback)); -} -void TextSensor::add_on_raw_state_callback(std::function<void(const std::string &)> callback) { - this->raw_callback_.add(std::move(callback)); -} - const std::string &TextSensor::get_state() const { return this->state; } const std::string &TextSensor::get_raw_state() const { #ifdef USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index d26cfade966..8941790e7cc 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -62,9 +62,11 @@ class TextSensor : public EntityBase { void clear_filters(); #endif - void add_on_state_callback(std::function<void(const std::string &)> callback); + template<typename F> void add_on_state_callback(F &&callback) { this->callback_.add(std::forward<F>(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function<void(const std::string &)> callback); + template<typename F> void add_on_raw_state_callback(F &&callback) { + this->raw_callback_.add(std::forward<F>(callback)); + } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index f9de5f5614c..06ee2ea5af4 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -55,9 +55,9 @@ class RealTimeClock : public PollingComponent { /// Get the current time as the UTC epoch since January 1st 1970. time_t timestamp_now() { return ::time(nullptr); } - void add_on_time_sync_callback(std::function<void()> &&callback) { - this->time_sync_callback_.add(std::move(callback)); - }; + template<typename F> void add_on_time_sync_callback(F &&callback) { + this->time_sync_callback_.add(std::forward<F>(callback)); + } void dump_config() override; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 76431ddfe45..7e6b50f0847 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -112,8 +112,8 @@ class Tuya : public Component, public uart::UARTDevice { void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) { this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints); } - void add_on_initialized_callback(std::function<void()> callback) { - this->initialized_callback_.add(std::move(callback)); + template<typename F> void add_on_initialized_callback(F &&callback) { + this->initialized_callback_.add(std::forward<F>(callback)); } protected: diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 853de719fef..ee2b006039c 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -190,9 +190,7 @@ class UARTComponent { #endif // USE_ESP8266 || USE_ESP32 #ifdef USE_UART_DEBUGGER - void add_debug_callback(std::function<void(UARTDirection, uint8_t)> &&callback) { - this->debug_callback_.add(std::move(callback)); - } + template<typename F> void add_debug_callback(F &&callback) { this->debug_callback_.add(std::forward<F>(callback)); } #endif protected: diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 7fd63080655..fb0edf2ebd7 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -28,9 +28,7 @@ class UDPComponent : public Component { void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; } void set_should_broadcast() { this->should_broadcast_ = true; } void set_should_listen() { this->should_listen_ = true; } - void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) { - this->packet_listeners_.add(std::move(listener)); - } + template<typename F> void add_listener(F &&listener) { this->packet_listeners_.add(std::forward<F>(listener)); } void setup() override; void loop() override; void dump_config() override; diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 82eaacaf76d..f7d0032f217 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -40,7 +40,9 @@ class UpdateEntity : public EntityBase { const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; - void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } Trigger<const UpdateInfo &> *get_update_available_trigger() { if (!update_available_trigger_) { update_available_trigger_ = std::make_unique<Trigger<const UpdateInfo &>>(); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 493ffd8da26..636da1f3c34 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -125,7 +125,6 @@ bool ValveCall::get_stop() const { return this->stop_; } ValveCall Valve::make_call() { return {this}; } -void Valve::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); } void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index aab819a7788..b4141f5ff59 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -117,7 +117,7 @@ class Valve : public EntityBase { /// Construct a new valve call used to control the valve. ValveCall make_call(); - void add_on_state_callback(std::function<void()> &&f); + template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); } /** Publish the current state of the valve. * diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h index 2e9da85a111..4dc14397d83 100644 --- a/esphome/components/zephyr/cdc_acm.h +++ b/esphome/components/zephyr/cdc_acm.h @@ -11,9 +11,7 @@ class CdcAcm : public Component { public: CdcAcm(); void setup() override; - void add_on_rate_callback(std::function<void(const device *, uint32_t)> &&callback) { - this->rate_callbacks_.add(std::move(callback)); - } + template<typename F> void add_on_rate_callback(F &&callback) { this->rate_callbacks_.add(std::forward<F>(callback)); } protected: static void cdc_dte_rate_callback_(const device *device, uint32_t rate); diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index dcc2b40a166..3fa5818ec57 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -74,7 +74,7 @@ class ZigbeeComponent : public Component { // endpoints are enumerated from 1 this->callbacks_[endpoint - 1] = std::move(cb); } - void add_join_callback(std::function<void()> &&cb) { this->join_cb_.add(std::move(cb)); } + template<typename F> void add_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } void zboss_signal_handler_esphome(zb_bufid_t bufid); void factory_reset(); Trigger<> *get_join_trigger() { return &this->join_trigger_; }; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4c6e5f6596b..8c1f1a213e5 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -307,15 +307,11 @@ template<typename T> class StatefulEntityBase : public EntityBase { virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } void invalidate_state() { this->set_new_state({}); } - void add_full_state_callback(std::function<void(optional<T> previous, optional<T> current)> &&callback) { - if (this->full_state_callbacks_ == nullptr) - this->full_state_callbacks_ = new CallbackManager<void(optional<T> previous, optional<T> current)>(); // NOLINT - this->full_state_callbacks_->add(std::move(callback)); + template<typename F> void add_full_state_callback(F &&callback) { + this->full_state_callbacks_.add(std::forward<F>(callback)); } - void add_on_state_callback(std::function<void(T)> &&callback) { - if (this->state_callbacks_ == nullptr) - this->state_callbacks_ = new CallbackManager<void(T)>(); // NOLINT - this->state_callbacks_->add(std::move(callback)); + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callbacks_.add(std::forward<F>(callback)); } void set_trigger_on_initial_state(bool trigger_on_initial_state) { @@ -333,21 +329,19 @@ template<typename T> class StatefulEntityBase : public EntityBase { virtual bool set_new_state(const optional<T> &new_state) { if (this->state_ != new_state) { // call the full state callbacks with the previous and new state - if (this->full_state_callbacks_ != nullptr) - this->full_state_callbacks_->call(this->state_, new_state); + this->full_state_callbacks_.call(this->state_, new_state); // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or // the previous state was valid auto had_state = this->has_state(); this->state_ = new_state; - if (this->state_callbacks_ != nullptr && new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) - this->state_callbacks_->call(new_state.value()); + if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) + this->state_callbacks_.call(new_state.value()); return true; } return false; } bool trigger_on_initial_state_{true}; - // callbacks with full state and previous state - CallbackManager<void(optional<T> previous, optional<T> current)> *full_state_callbacks_{}; - CallbackManager<void(T)> *state_callbacks_{}; + LazyCallbackManager<void(optional<T> previous, optional<T> current)> full_state_callbacks_; + LazyCallbackManager<void(T)> state_callbacks_; }; } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d220626bcf3..a703b5a5f37 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1729,6 +1729,51 @@ constexpr float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1. /// @name Utilities /// @{ +/// Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead. +/// No null check, no exceptions, no heap allocation for small trivially-copyable callables. +/// +/// With C++20 if constexpr, automatically detects [this] lambdas (sizeof <= sizeof(void*), +/// trivially copyable) and stores them inline. Larger callables are heap-allocated. +template<typename... X> struct Callback; + +template<typename... Ts> struct Callback<void(Ts...)> { + // The inline storage path stores callable bytes in ctx_ via memcpy. + // sizeof equality with uintptr_t ensures void* can round-trip arbitrary bit patterns, + // which combined with flat address spaces on all ESPHome targets means no trap representations. + static_assert(sizeof(void *) == sizeof(std::uintptr_t), "void* must be the same size as uintptr_t"); + + void (*fn_)(void *, Ts...){nullptr}; + void *ctx_{nullptr}; + + /// Invoke the callback. Only valid on Callbacks created via create(), never on default-constructed instances. + void call(Ts... args) const { this->fn_(this->ctx_, args...); } + + /// Create from any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline in the ctx pointer without heap allocation. + template<typename F> static Callback create(F &&callable) { + using DecayF = std::decay_t<F>; + if constexpr (sizeof(DecayF) <= sizeof(void *) && std::is_trivially_copyable_v<DecayF>) { + // Small trivial callable (e.g. [this]() { this->method(); }) - store inline in ctx. + // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly + // creates objects of implicit-lifetime types (trivially copyable qualifies). + Callback cb; // fn and ctx are zero-initialized by default + __builtin_memcpy(&cb.ctx_, &callable, sizeof(DecayF)); + cb.fn_ = [](void *c, Ts... args) { + alignas(DecayF) char buf[sizeof(DecayF)]; + __builtin_memcpy(buf, &c, sizeof(DecayF)); + (*std::launder(reinterpret_cast<DecayF *>(buf)))(args...); + }; + return cb; + } else { + // Large or non-trivial callable - heap allocate. + // Intentionally never freed: callbacks in ESPHome are registered during setup() + // and live for device lifetime. Same lifetime as the previous std::function approach. + auto *stored = new DecayF(std::forward<F>(callable)); + return {[](void *c, Ts... args) { (*static_cast<DecayF *>(c))(args...); }, static_cast<void *>(stored)}; + } + } +}; + template<typename... X> class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. @@ -1737,13 +1782,14 @@ template<typename... X> class CallbackManager; */ template<typename... Ts> class CallbackManager<void(Ts...)> { public: - /// Add a callback to the list. - void add(std::function<void(Ts...)> &&callback) { this->callbacks_.push_back(std::move(callback)); } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline without heap allocation or std::function. + template<typename F> void add(F &&callback) { this->add_(Callback<void(Ts...)>::create(std::forward<F>(callback))); } - /// Call all callbacks in this manager. + /// Call all callbacks in this manager. No null check on invoke. void call(Ts... args) { for (auto &cb : this->callbacks_) - cb(args...); + cb.call(args...); } size_t size() const { return this->callbacks_.size(); } @@ -1751,7 +1797,10 @@ template<typename... Ts> class CallbackManager<void(Ts...)> { void operator()(Ts... args) { call(args...); } protected: - std::vector<std::function<void(Ts...)>> callbacks_; + template<typename...> friend class LazyCallbackManager; + /// Non-template core to avoid code duplication per lambda type. + void add_(Callback<void(Ts...)> cb) { this->callbacks_.push_back(cb); } + std::vector<Callback<void(Ts...)>> callbacks_; }; template<typename... X> class LazyCallbackManager; @@ -1784,13 +1833,8 @@ template<typename... Ts> class LazyCallbackManager<void(Ts...)> { LazyCallbackManager(LazyCallbackManager &&) = delete; LazyCallbackManager &operator=(LazyCallbackManager &&) = delete; - /// Add a callback to the list. Allocates the underlying CallbackManager on first use. - void add(std::function<void(Ts...)> &&callback) { - if (!this->callbacks_) { - this->callbacks_ = new CallbackManager<void(Ts...)>(); - } - this->callbacks_->add(std::move(callback)); - } + /// Add any callable. Allocates the underlying CallbackManager on first use. + template<typename F> void add(F &&callback) { this->add_(Callback<void(Ts...)>::create(std::forward<F>(callback))); } /// Call all callbacks in this manager. No-op if no callbacks registered. void call(Ts... args) { @@ -1809,6 +1853,13 @@ template<typename... Ts> class LazyCallbackManager<void(Ts...)> { void operator()(Ts... args) { this->call(args...); } protected: + /// Non-template core to avoid code duplication per lambda type. + void add_(Callback<void(Ts...)> cb) { + if (!this->callbacks_) { + this->callbacks_ = new CallbackManager<void(Ts...)>(); + } + this->callbacks_->add_(cb); + } CallbackManager<void(Ts...)> *callbacks_{nullptr}; }; From 2271ac64701b5524997d2c1c7335b585a97cc939 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 18:41:45 -1000 Subject: [PATCH 0803/2056] [api] Extract overflow buffer from frame helper into APIOverflowBuffer (#14871) --- esphome/components/api/__init__.py | 5 +- esphome/components/api/api_frame_helper.cpp | 161 ++++-------------- esphome/components/api/api_frame_helper.h | 43 ++--- .../components/api/api_frame_helper_noise.cpp | 6 +- .../api/api_frame_helper_plaintext.cpp | 6 +- .../components/api/api_overflow_buffer.cpp | 73 ++++++++ esphome/components/api/api_overflow_buffer.h | 76 +++++++++ 7 files changed, 214 insertions(+), 156 deletions(-) create mode 100644 esphome/components/api/api_overflow_buffer.cpp create mode 100644 esphome/components/api/api_overflow_buffer.h diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9772e6afca7..4c3cf81927d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All( # Maximum queued send buffers per connection before dropping connection # Each buffer uses ~8-12 bytes overhead plus actual message size # Platform defaults based on available RAM and typical message rates: + # CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo cv.SplitDefault( CONF_MAX_SEND_QUEUE, - esp8266=5, # Limited RAM, need to fail fast + esp8266=4, # Limited RAM, need to fail fast esp32=8, # More RAM, can buffer more - rp2040=5, # Limited RAM + rp2040=8, # Moderate RAM bk72xx=8, # Moderate RAM nrf52=8, # Moderate RAM rtl87xx=8, # Moderate RAM diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index fbee2940226..6d3bd51b586 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -100,150 +100,61 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("UNKNOWN"); } -// Default implementation for loop - handles sending buffered data -APIError APIFrameHelper::loop() { - if (this->tx_buf_count_ > 0) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; +APIError APIFrameHelper::drain_overflow_and_handle_errors_() { + if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) { + int err = errno; + if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + HELPER_LOG("Socket write failed with errno %d", err); + return APIError::SOCKET_WRITE_FAILED; } } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + return APIError::OK; } -// Common socket write error handling -APIError APIFrameHelper::handle_socket_write_error_() { - const int err = errno; - if (err == EWOULDBLOCK || err == EAGAIN) { - return APIError::WOULD_BLOCK; - } - HELPER_LOG("Socket write failed with errno %d", err); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; -} - -// Helper method to buffer data from IOVs -void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, - uint16_t offset) { - // Check if queue is full - if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { - HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); - this->state_ = State::FAILED; - return; - } - - uint16_t buffer_size = total_write_len - offset; - auto &buffer = this->tx_buf_[this->tx_buf_tail_]; - buffer = std::make_unique<SendBuffer>(SendBuffer{ - .data = std::make_unique<uint8_t[]>(buffer_size), - .size = buffer_size, - .offset = 0, - }); - - uint16_t to_skip = offset; - uint16_t write_pos = 0; - - for (int i = 0; i < iovcnt; i++) { - if (to_skip >= iov[i].iov_len) { - // Skip this entire segment - to_skip -= static_cast<uint16_t>(iov[i].iov_len); - } else { - // Include this segment (partially or fully) - const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip; - uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip; - std::memcpy(buffer->data.get() + write_pos, src, len); - write_pos += len; - to_skip = 0; - } - } - - // Update circular buffer tracking - this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE; - this->tx_buf_count_++; -} - -// This method writes data to socket or buffers it +// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full. +// Returns OK if all data was sent or successfully queued. +// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED). APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { - // Returns APIError::OK if successful (or would block, but data has been buffered) - // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED - - if (iovcnt == 0) - return APIError::OK; // Nothing to do, success - #ifdef HELPER_LOG_PACKETS for (int i = 0; i < iovcnt; i++) { LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); } #endif - // Try to send any existing buffered data first if there is any - if (this->tx_buf_count_ > 0) { - APIError send_result = try_send_tx_buf_(); - // If real error occurred (not just WOULD_BLOCK), return it - if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { - return send_result; - } + uint16_t skip = 0; - // If there is still data in the buffer, we can't send, buffer - // the new data and return - if (this->tx_buf_count_ > 0) { - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); - return APIError::OK; // Success, data buffered - } + // Drain any existing backlog first + if (!this->overflow_buf_.empty()) [[unlikely]] { + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; } - // Try to send directly if no buffered data - // Optimize for single iovec case (common for plaintext API) - ssize_t sent = - (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); + // If backlog is clear, try direct send + if (this->overflow_buf_.empty()) [[likely]] { + ssize_t sent = + (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); - if (sent == -1) { - APIError err = this->handle_socket_write_error_(); - if (err == APIError::WOULD_BLOCK) { - // Socket would block, buffer the data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); - return APIError::OK; // Success, data buffered - } - return err; // Socket write failed - } else if (static_cast<uint16_t>(sent) < total_write_len) { - // Partially sent, buffer the remaining data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent)); - } - - return APIError::OK; // Success, all data sent or buffered -} - -// Common implementation for trying to send buffered data -// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method -APIError APIFrameHelper::try_send_tx_buf_() { - // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check - while (this->tx_buf_count_ > 0) { - // Get the first buffer in the queue - SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); - - // Try to send the remaining data in this buffer - ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); - - if (sent == -1) { - return this->handle_socket_write_error_(); - } else if (sent == 0) { - // Nothing sent but not an error - return APIError::WOULD_BLOCK; - } else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) { - // Partially sent, update offset - // Cast to ensure no overflow issues with uint16_t - front_buffer->offset += static_cast<uint16_t>(sent); - return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer + if (sent == -1) [[unlikely]] { + int err = errno; + if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + HELPER_LOG("Socket write failed with errno %d", err); + return APIError::SOCKET_WRITE_FAILED; + } + } else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] { + return APIError::OK; } else { - // Buffer completely sent, remove it from the queue - this->tx_buf_[this->tx_buf_head_].reset(); - this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; - this->tx_buf_count_--; - // Continue loop to try sending the next buffer + skip = static_cast<uint16_t>(sent); } } - return APIError::OK; // All buffers sent successfully + // Queue unsent data into overflow buffer + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) { + HELPER_LOG("Overflow buffer full, dropping connection"); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } + return APIError::OK; } const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index e78c71507c8..72ccf8aa562 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -9,6 +9,7 @@ #include "esphome/core/defines.h" #ifdef USE_API #include "esphome/components/api/api_buffer.h" +#include "esphome/components/api/api_overflow_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -104,9 +105,9 @@ class APIFrameHelper { } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; - virtual APIError loop(); + virtual APIError loop() = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; - bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } + bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { if (state_ == State::CLOSED) @@ -189,28 +190,23 @@ class APIFrameHelper { } protected: - // Buffer containing data to be sent - struct SendBuffer { - std::unique_ptr<uint8_t[]> data; - uint16_t size{0}; // Total size of the buffer - uint16_t offset{0}; // Current offset within the buffer - - // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes - uint16_t remaining() const { return size - offset; } - const uint8_t *current_data() const { return data.get() + offset; } - }; + // Drain backlogged overflow data to the socket and handle errors. + // Called when overflow_buf_.empty() is false. Out-of-line to keep the + // fast path (empty check) inline at call sites. + // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. + APIError drain_overflow_and_handle_errors_(); // Common implementation for writing raw data to socket APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); - // Try to send data from the tx buffer - APIError try_send_tx_buf_(); - - // Helper method to buffer data from IOVs - void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); - - // Common socket write error handling - APIError handle_socket_write_error_(); + // Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN). + // Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors. + APIError check_socket_write_err_(int err) { + if (err == EWOULDBLOCK || err == EAGAIN) + return APIError::WOULD_BLOCK; + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr<socket::Socket> socket_; @@ -245,8 +241,8 @@ class APIFrameHelper { return APIError::WOULD_BLOCK; } - // Containers (size varies, but typically 12+ bytes on 32-bit) - std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; + // Backlog for unsent data when TCP send buffer is full (rarely used in production) + APIOverflowBuffer overflow_buf_; APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername @@ -257,9 +253,6 @@ class APIFrameHelper { State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - uint8_t tx_buf_head_{0}; - uint8_t tx_buf_tail_{0}; - uint8_t tx_buf_count_{0}; // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send). // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0. diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index b635d84f168..78e87793fc8 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() { } } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); + if (!this->overflow_buf_.empty()) [[unlikely]] { + return this->drain_overflow_and_handle_errors_(); + } + return APIError::OK; } /** Read a packet into the rx_buf_. diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index e97b558fa39..9e669b31ee2 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); + if (!this->overflow_buf_.empty()) [[unlikely]] { + return this->drain_overflow_and_handle_errors_(); + } + return APIError::OK; } /** Read a packet into the rx_buf_. diff --git a/esphome/components/api/api_overflow_buffer.cpp b/esphome/components/api/api_overflow_buffer.cpp new file mode 100644 index 00000000000..e242d4553e4 --- /dev/null +++ b/esphome/components/api/api_overflow_buffer.cpp @@ -0,0 +1,73 @@ +#include "api_overflow_buffer.h" +#ifdef USE_API +#include <cstring> + +namespace esphome::api { + +APIOverflowBuffer::~APIOverflowBuffer() { + for (auto *entry : this->queue_) { + if (entry != nullptr) + Entry::destroy(entry); + } +} + +ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { + while (this->count_ > 0) { + Entry *front = this->queue_[this->head_]; + + ssize_t sent = socket->write(front->current_data(), front->remaining()); + + if (sent <= 0) { + // -1 = error (caller checks errno for EWOULDBLOCK vs hard error) + // 0 = nothing sent (treat as no progress) + return sent; + } + + if (static_cast<uint16_t>(sent) < front->remaining()) { + // Partially sent, update offset and stop + front->offset += static_cast<uint16_t>(sent); + return sent; + } + + // Entry fully sent — free it and advance + Entry::destroy(front); + this->queue_[this->head_] = nullptr; + this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE; + this->count_--; + } + + return 0; // All drained +} + +bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) { + if (this->count_ >= API_MAX_SEND_QUEUE) + return false; + + uint16_t buffer_size = total_len - skip; + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0}; + this->queue_[this->tail_] = entry; + + uint16_t to_skip = skip; + uint16_t write_pos = 0; + + for (int i = 0; i < iovcnt; i++) { + if (to_skip >= iov[i].iov_len) { + to_skip -= static_cast<uint16_t>(iov[i].iov_len); + } else { + const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip; + uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip; + std::memcpy(entry->data + write_pos, src, len); + write_pos += len; + to_skip = 0; + } + } + + this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE; + this->count_++; + return true; +} + +} // namespace esphome::api + +#endif // USE_API diff --git a/esphome/components/api/api_overflow_buffer.h b/esphome/components/api/api_overflow_buffer.h new file mode 100644 index 00000000000..19aae680f01 --- /dev/null +++ b/esphome/components/api/api_overflow_buffer.h @@ -0,0 +1,76 @@ +#pragma once +#include <array> +#include <cstdint> +#include <sys/types.h> + +#include "esphome/core/defines.h" +#ifdef USE_API + +#include "esphome/components/socket/headers.h" +#include "esphome/components/socket/socket.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Circular queue of heap-allocated byte buffers used as a TCP send backlog. +/// +/// Under normal operation this buffer is **never used** — data goes straight +/// from the frame helper to the socket. It only fills when the LWIP TCP +/// send buffer is full (slow client, congested network, heavy logging). +/// The queue drains automatically on subsequent write/loop calls once the +/// socket becomes writable again. +/// +/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python +/// config). If the queue fills completely the connection is marked failed. +class APIOverflowBuffer { + public: + /// A single heap-allocated send-backlog entry. + /// Lifetime is manually managed — see destroy(). + struct Entry { + uint8_t *data; + uint16_t size; // Total size of the buffer + uint16_t offset; // Current send offset within the buffer + + uint16_t remaining() const { return this->size - this->offset; } + const uint8_t *current_data() const { return this->data + this->offset; } + + /// Free this entry and its data buffer. + static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) { + delete[] entry->data; + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } + }; + + ~APIOverflowBuffer(); + + /// True when no backlogged data is waiting. + bool empty() const { return this->count_ == 0; } + + /// True when the queue has no room for another entry. + bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; } + + /// Number of entries currently queued. + uint8_t count() const { return this->count_; } + + /// Try to drain queued data to the socket. + /// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress, + /// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors). + /// Callers only need to act on -1; 0 and positive values both mean "no error". + /// Frees entries as they are fully sent. + ssize_t try_drain(socket::Socket *socket); + + /// Enqueue unsent IOV data into the backlog. + /// Copies iov data starting at byte offset `skip` into a new entry. + /// Returns false if the queue is full (caller should fail the connection). + bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip); + + protected: + std::array<Entry *, API_MAX_SEND_QUEUE> queue_{}; + uint8_t head_{0}; + uint8_t tail_{0}; + uint8_t count_{0}; +}; + +} // namespace esphome::api + +#endif // USE_API From a1aff7cadf88b14e39fcda559d3f2895b0a43e8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 18:42:05 -1000 Subject: [PATCH 0804/2056] [preferences] Devirtualize preference backend and manager classes (#14825) --- esphome/components/esp32/preference_backend.h | 27 ++ esphome/components/esp32/preferences.cpp | 306 +++++++++--------- esphome/components/esp32/preferences.h | 29 +- .../components/esp8266/preference_backend.h | 29 ++ esphome/components/esp8266/preferences.cpp | 248 +++++++------- esphome/components/esp8266/preferences.h | 24 +- esphome/components/host/preference_backend.h | 29 ++ esphome/components/host/preferences.cpp | 11 +- esphome/components/host/preferences.h | 39 +-- .../components/libretiny/preference_backend.h | 32 ++ esphome/components/libretiny/preferences.cpp | 269 +++++++-------- esphome/components/libretiny/preferences.h | 26 +- .../components/rp2040/preference_backend.h | 27 ++ esphome/components/rp2040/preferences.cpp | 189 +++++------ esphome/components/rp2040/preferences.h | 29 +- .../components/zephyr/preference_backend.h | 48 +++ esphome/components/zephyr/preferences.cpp | 233 ++++++------- esphome/components/zephyr/preferences.h | 31 +- esphome/core/preference_backend.h | 83 +++++ esphome/core/preferences.h | 75 ++--- 20 files changed, 1021 insertions(+), 763 deletions(-) create mode 100644 esphome/components/esp32/preference_backend.h create mode 100644 esphome/components/esp8266/preference_backend.h create mode 100644 esphome/components/host/preference_backend.h create mode 100644 esphome/components/libretiny/preference_backend.h create mode 100644 esphome/components/rp2040/preference_backend.h create mode 100644 esphome/components/zephyr/preference_backend.h create mode 100644 esphome/core/preference_backend.h diff --git a/esphome/components/esp32/preference_backend.h b/esphome/components/esp32/preference_backend.h new file mode 100644 index 00000000000..893bc35f0c0 --- /dev/null +++ b/esphome/components/esp32/preference_backend.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef USE_ESP32 + +#include <cstddef> +#include <cstdint> + +namespace esphome::esp32 { + +class ESP32PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t key; + uint32_t nvs_handle; +}; + +class ESP32Preferences; +ESP32Preferences *get_preferences(); + +} // namespace esphome::esp32 + +namespace esphome { +using PreferenceBackend = esp32::ESP32PreferenceBackend; +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index a3ef10b21f9..7260bf54e01 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -1,16 +1,14 @@ #ifdef USE_ESP32 +#include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" #include <nvs_flash.h> #include <cinttypes> #include <cstring> -#include <memory> #include <vector> -namespace esphome { -namespace esp32 { +namespace esphome::esp32 { static const char *const TAG = "esp32.preferences"; @@ -24,185 +22,175 @@ struct NVSData { static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class ESP32PreferenceBackend : public ESPPreferenceBackend { - public: - uint32_t key; - uint32_t nvs_handle; - bool save(const uint8_t *data, size_t len) override { - // try find in pending saves and update that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - obj.data.set(data, len); - return true; - } +bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { + // try find in pending saves and update that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + obj.data.set(data, len); + return true; } - NVSData save{}; - save.key = this->key; - save.data.set(data, len); - s_pending_save.push_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); - return true; } - bool load(uint8_t *data, size_t len) override { - // try find in pending saves and load from that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - if (obj.data.size() != len) { - // size mismatch - return false; - } - memcpy(data, obj.data.data(), len); - return true; - } - } + NVSData save{}; + save.key = this->key; + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); + return true; +} +bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { + // try find in pending saves and load from that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + if (obj.data.size() != len) { + // size mismatch + return false; + } + memcpy(data, obj.data.data(), len); + return true; + } + } + + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + size_t actual_len; + esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); + return false; + } + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); + return false; + } + err = nvs_get_blob(this->nvs_handle, key_str, data, &len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); + return false; + } else { + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); + } + return true; +} + +void ESP32Preferences::open() { + nvs_flash_init(); + esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); + if (err == 0) + return; + + ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); + nvs_flash_deinit(); + nvs_flash_erase(); + nvs_flash_init(); + + err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); + if (err != 0) { + this->nvs_handle = 0; + } +} + +ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { + auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->nvs_handle = this->nvs_handle; + pref->key = type; + + return ESPPreferenceObject(pref); +} + +bool ESP32Preferences::sync() { + if (s_pending_save.empty()) + return true; + + ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); + int cached = 0, written = 0, failed = 0; + esp_err_t last_err = ESP_OK; + uint32_t last_key = 0; + + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); - size_t actual_len; - esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); - return false; - } - if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); - return false; - } - err = nvs_get_blob(this->nvs_handle, key_str, data, &len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); - return false; - } else { - ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); - } - return true; - } -}; - -class ESP32Preferences : public ESPPreferences { - public: - uint32_t nvs_handle; - - void open() { - nvs_flash_init(); - esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); - if (err == 0) - return; - - ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); - nvs_flash_deinit(); - nvs_flash_erase(); - nvs_flash_init(); - - err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); - if (err != 0) { - nvs_handle = 0; - } - } - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return this->make_preference(length, type); - } - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->nvs_handle = this->nvs_handle; - pref->key = type; - - return ESPPreferenceObject(pref); - } - - bool sync() override { - if (s_pending_save.empty()) - return true; - - ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - int cached = 0, written = 0, failed = 0; - esp_err_t last_err = ESP_OK; - uint32_t last_key = 0; - - for (const auto &save : s_pending_save) { - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); - ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); - if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); - failed++; - last_err = err; - last_key = save.key; - continue; - } - written++; - } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); - cached++; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); + if (this->is_changed_(this->nvs_handle, save, key_str)) { + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); + failed++; + last_err = err; + last_key = save.key; + continue; } + written++; + } else { + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); + cached++; } - s_pending_save.clear(); + } + s_pending_save.clear(); - ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, - failed); - if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), - last_key); - } - - // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes - esp_err_t err = nvs_commit(this->nvs_handle); - if (err != 0) { - ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); - return false; - } - - return failed == 0; + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + if (failed > 0) { + ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), + last_key); } - protected: - bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { - size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); - return true; - } - // Check size first before allocating memory - if (actual_len != to_save.data.size()) { - return true; - } - // Most preferences are small, use stack buffer with heap fallback for large ones - SmallBufferWithHeapFallback<256> stored_data(actual_len); - err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); - return true; - } - return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; + // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes + esp_err_t err = nvs_commit(this->nvs_handle); + if (err != 0) { + ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); + return false; } - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - s_pending_save.clear(); + return failed == 0; +} - nvs_flash_deinit(); - nvs_flash_erase(); - // Make the handle invalid to prevent any saves until restart - nvs_handle = 0; +bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return true; } -}; + // Check size first before allocating memory + if (actual_len != to_save.data.size()) { + return true; + } + // Most preferences are small, use stack buffer with heap fallback for large ones + SmallBufferWithHeapFallback<256> stored_data(actual_len); + err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); + return true; + } + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; +} + +bool ESP32Preferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + s_pending_save.clear(); + + nvs_flash_deinit(); + nvs_flash_erase(); + // Make the handle invalid to prevent any saves until restart + this->nvs_handle = 0; + return true; +} static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +ESP32Preferences *get_preferences() { return &s_preferences; } + void setup_preferences() { s_preferences.open(); global_preferences = &s_preferences; } -} // namespace esp32 +} // namespace esphome::esp32 +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/esp32/preferences.h b/esphome/components/esp32/preferences.h index e44213e4cf9..0e187d87a99 100644 --- a/esphome/components/esp32/preferences.h +++ b/esphome/components/esp32/preferences.h @@ -1,12 +1,33 @@ #pragma once #ifdef USE_ESP32 -namespace esphome { -namespace esp32 { +#include "esphome/core/preference_backend.h" + +namespace esphome::esp32 { + +struct NVSData; + +class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> { + public: + using PreferencesMixin<ESP32Preferences>::make_preference; + void open(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { + return this->make_preference(length, type); + } + ESPPreferenceObject make_preference(size_t length, uint32_t type); + bool sync(); + bool reset(); + + uint32_t nvs_handle; + + protected: + bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str); +}; void setup_preferences(); -} // namespace esp32 -} // namespace esphome +} // namespace esphome::esp32 + +DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences) #endif // USE_ESP32 diff --git a/esphome/components/esp8266/preference_backend.h b/esphome/components/esp8266/preference_backend.h new file mode 100644 index 00000000000..f9da8ff1656 --- /dev/null +++ b/esphome/components/esp8266/preference_backend.h @@ -0,0 +1,29 @@ +#pragma once +#ifdef USE_ESP8266 + +#include <cstddef> +#include <cstdint> + +namespace esphome::esp8266 { + +class ESP8266PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t type = 0; + uint16_t offset = 0; + uint8_t length_words = 0; // Max 255 words (1020 bytes of data) + bool in_flash = false; +}; + +class ESP8266Preferences; +ESP8266Preferences *get_preferences(); + +} // namespace esphome::esp8266 + +namespace esphome { +using PreferenceBackend = esp8266::ESP8266PreferenceBackend; +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index e749b1f6332..0b31c53ff87 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -5,11 +5,9 @@ extern "C" { #include "spi_flash.h" } -#include "esphome/core/defines.h" +#include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" -#include "preferences.h" #include <cstring> @@ -137,155 +135,135 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { static constexpr size_t PREF_MAX_BUFFER_WORDS = ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS; -class ESP8266PreferenceBackend : public ESPPreferenceBackend { - public: - uint32_t type = 0; - uint16_t offset = 0; - uint8_t length_words = 0; // Max 255 words (1020 bytes of data) - bool in_flash = false; +bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) { + if (bytes_to_words(len) != this->length_words) + return false; + const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; + memset(buffer, 0, buffer_size * sizeof(uint32_t)); + memcpy(buffer, data, len); + buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); + return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) + : save_to_rtc(this->offset, buffer, buffer_size); +} - bool save(const uint8_t *data, size_t len) override { - if (bytes_to_words(len) != this->length_words) - return false; - const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; - if (buffer_size > PREF_MAX_BUFFER_WORDS) - return false; - uint32_t buffer[PREF_MAX_BUFFER_WORDS]; - memset(buffer, 0, buffer_size * sizeof(uint32_t)); - memcpy(buffer, data, len); - buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); - return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) - : save_to_rtc(this->offset, buffer, buffer_size); +bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) { + if (bytes_to_words(len) != this->length_words) + return false; + const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; + bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) + : load_from_rtc(this->offset, buffer, buffer_size); + if (!ret) + return false; + if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) + return false; + memcpy(data, buffer, len); + return true; +} + +void ESP8266Preferences::setup() { + ESP_LOGVV(TAG, "Loading preferences from flash"); + + { + InterruptLock lock; + spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } +} + +ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) { + const uint32_t length_words = bytes_to_words(length); + if (length_words > MAX_PREFERENCE_WORDS) { + ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words)); + return {}; } - bool load(uint8_t *data, size_t len) override { - if (bytes_to_words(len) != this->length_words) - return false; - const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; - if (buffer_size > PREF_MAX_BUFFER_WORDS) - return false; - uint32_t buffer[PREF_MAX_BUFFER_WORDS]; - bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) - : load_from_rtc(this->offset, buffer, buffer_size); - if (!ret) - return false; - if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) - return false; - memcpy(data, buffer, len); - return true; - } -}; + const uint32_t total_words = length_words + 1; // +1 for CRC + uint16_t offset; -class ESP8266Preferences : public ESPPreferences { - public: - uint32_t current_offset = 0; - uint32_t current_flash_offset = 0; // in words - - void setup() { - ESP_LOGVV(TAG, "Loading preferences from flash"); - - { - InterruptLock lock; - spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); - } - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - const uint32_t length_words = bytes_to_words(length); - if (length_words > MAX_PREFERENCE_WORDS) { - ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words)); + if (in_flash) { + if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE) return {}; + offset = static_cast<uint16_t>(this->current_flash_offset); + this->current_flash_offset += total_words; + } else { + uint32_t start = this->current_offset; + bool in_normal = start < RTC_NORMAL_REGION_WORDS; + // Normal: offset 0-95 maps to RTC offset 32-127 + // Eboot: offset 96-127 maps to RTC offset 0-31 + if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) { + // start is in normal but end is not -> switch to Eboot + this->current_offset = start = RTC_NORMAL_REGION_WORDS; + in_normal = false; } - - const uint32_t total_words = length_words + 1; // +1 for CRC - uint16_t offset; - - if (in_flash) { - if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE) - return {}; - offset = static_cast<uint16_t>(this->current_flash_offset); - this->current_flash_offset += total_words; - } else { - uint32_t start = this->current_offset; - bool in_normal = start < RTC_NORMAL_REGION_WORDS; - // Normal: offset 0-95 maps to RTC offset 32-127 - // Eboot: offset 96-127 maps to RTC offset 0-31 - if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) { - // start is in normal but end is not -> switch to Eboot - this->current_offset = start = RTC_NORMAL_REGION_WORDS; - in_normal = false; - } - if (start + total_words > PREF_TOTAL_WORDS) - return {}; // Doesn't fit in RTC memory - // Convert preference offset to RTC memory offset - offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS); - this->current_offset = start + total_words; - } - - auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = offset; - pref->type = type; - pref->length_words = static_cast<uint8_t>(length_words); - pref->in_flash = in_flash; - return pref; + if (start + total_words > PREF_TOTAL_WORDS) + return {}; // Doesn't fit in RTC memory + // Convert preference offset to RTC memory offset + offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS); + this->current_offset = start + total_words; } - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { -#ifdef USE_ESP8266_PREFERENCES_FLASH - return make_preference(length, type, true); -#else - return make_preference(length, type, false); -#endif - } + auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = offset; + pref->type = type; + pref->length_words = static_cast<uint8_t>(length_words); + pref->in_flash = in_flash; + return ESPPreferenceObject(pref); +} - bool sync() override { - if (!s_flash_dirty) - return true; - if (s_prevent_write) - return false; - - ESP_LOGD(TAG, "Saving"); - SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; - { - InterruptLock lock; - erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); - if (erase_res == SPI_FLASH_RESULT_OK) { - write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); - } - } - if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Erasing failed"); - return false; - } - if (write_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Writing failed"); - return false; - } - - s_flash_dirty = false; +bool ESP8266Preferences::sync() { + if (!s_flash_dirty) return true; + if (s_prevent_write) + return false; + + ESP_LOGD(TAG, "Saving"); + SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res == SPI_FLASH_RESULT_OK) { + write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } + } + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erasing failed"); + return false; + } + if (write_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Writing failed"); + return false; } - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - SpiFlashOpResult erase_res; - { - InterruptLock lock; - erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); - } - if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Erasing failed"); - return false; - } + s_flash_dirty = false; + return true; +} - // Protect flash from writing till restart - s_prevent_write = true; - return true; +bool ESP8266Preferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + SpiFlashOpResult erase_res; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); } -}; + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erasing failed"); + return false; + } + + // Protect flash from writing till restart + s_prevent_write = true; + return true; +} static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +ESP8266Preferences *get_preferences() { return &s_preferences; } + void setup_preferences() { s_preferences.setup(); global_preferences = &s_preferences; diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h index 16cf80a1295..43557d5ec5b 100644 --- a/esphome/components/esp8266/preferences.h +++ b/esphome/components/esp8266/preferences.h @@ -1,12 +1,34 @@ #pragma once - #ifdef USE_ESP8266 +#include "esphome/core/preference_backend.h" + namespace esphome::esp8266 { +class ESP8266Preferences final : public PreferencesMixin<ESP8266Preferences> { + public: + using PreferencesMixin<ESP8266Preferences>::make_preference; + void setup(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); + ESPPreferenceObject make_preference(size_t length, uint32_t type) { +#ifdef USE_ESP8266_PREFERENCES_FLASH + return this->make_preference(length, type, true); +#else + return this->make_preference(length, type, false); +#endif + } + bool sync(); + bool reset(); + + uint32_t current_offset = 0; + uint32_t current_flash_offset = 0; // in words +}; + void setup_preferences(); void preferences_prevent_write(bool prevent); } // namespace esphome::esp8266 +DECLARE_PREFERENCE_ALIASES(esphome::esp8266::ESP8266Preferences) + #endif // USE_ESP8266 diff --git a/esphome/components/host/preference_backend.h b/esphome/components/host/preference_backend.h new file mode 100644 index 00000000000..68537cad28f --- /dev/null +++ b/esphome/components/host/preference_backend.h @@ -0,0 +1,29 @@ +#pragma once +#ifdef USE_HOST + +#include <cstddef> +#include <cstdint> + +namespace esphome::host { + +class HostPreferenceBackend final { + public: + explicit HostPreferenceBackend(uint32_t key) : key_(key) {} + + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + protected: + uint32_t key_{}; +}; + +class HostPreferences; +HostPreferences *get_preferences(); + +} // namespace esphome::host + +namespace esphome { +using PreferenceBackend = host::HostPreferenceBackend; +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 275c202e3ed..fce3d62dda0 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -6,8 +6,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace host { +namespace esphome::host { namespace fs = std::filesystem; static const char *const TAG = "host.preferences"; @@ -77,6 +76,8 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +HostPreferences *get_preferences() { return &s_preferences; } + void setup_preferences() { host_preferences = &s_preferences; global_preferences = &s_preferences; @@ -88,9 +89,11 @@ bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); } -HostPreferences *host_preferences; -} // namespace host +HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +} // namespace esphome::host + +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h index 6b2e7eb8f94..25858799ff1 100644 --- a/esphome/components/host/preferences.h +++ b/esphome/components/host/preferences.h @@ -1,33 +1,22 @@ #pragma once - #ifdef USE_HOST -#include "esphome/core/preferences.h" +#include "esphome/core/preference_backend.h" +#include <cstring> #include <map> +#include <string> +#include <vector> -namespace esphome { -namespace host { +namespace esphome::host { -class HostPreferenceBackend : public ESPPreferenceBackend { +class HostPreferences final : public PreferencesMixin<HostPreferences> { public: - explicit HostPreferenceBackend(uint32_t key) { this->key_ = key; } + using PreferencesMixin<HostPreferences>::make_preference; + bool sync(); + bool reset(); - bool save(const uint8_t *data, size_t len) override; - bool load(uint8_t *data, size_t len) override; - - protected: - uint32_t key_{}; -}; - -class HostPreferences : public ESPPreferences { - public: - bool sync() override; - bool reset() override; - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override; - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - return make_preference(length, type, false); - } + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); + ESPPreferenceObject make_preference(size_t length, uint32_t type) { return make_preference(length, type, false); } bool save(uint32_t key, const uint8_t *data, size_t len) { if (len > 255) @@ -58,10 +47,12 @@ class HostPreferences : public ESPPreferences { std::string filename_{}; std::map<uint32_t, std::vector<uint8_t>> data{}; }; + void setup_preferences(); extern HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace host -} // namespace esphome +} // namespace esphome::host + +DECLARE_PREFERENCE_ALIASES(esphome::host::HostPreferences) #endif // USE_HOST diff --git a/esphome/components/libretiny/preference_backend.h b/esphome/components/libretiny/preference_backend.h new file mode 100644 index 00000000000..66b6847bee6 --- /dev/null +++ b/esphome/components/libretiny/preference_backend.h @@ -0,0 +1,32 @@ +#pragma once +#ifdef USE_LIBRETINY + +#include <cstddef> +#include <cstdint> + +// Forward declare FlashDB types to avoid pulling in flashdb.h +struct fdb_kvdb; +struct fdb_blob; + +namespace esphome::libretiny { + +class LibreTinyPreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t key; + struct fdb_kvdb *db; + struct fdb_blob *blob; +}; + +class LibreTinyPreferences; +LibreTinyPreferences *get_preferences(); + +} // namespace esphome::libretiny + +namespace esphome { +using PreferenceBackend = libretiny::LibreTinyPreferenceBackend; +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 1c101136e16..f22c12f1fbe 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -1,12 +1,11 @@ #ifdef USE_LIBRETINY +#include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" -#include <flashdb.h> #include <cinttypes> #include <cstring> -#include <memory> +#include <vector> namespace esphome::libretiny { @@ -22,163 +21,147 @@ struct NVSData { static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LibreTinyPreferenceBackend : public ESPPreferenceBackend { - public: - uint32_t key; - fdb_kvdb_t db; - fdb_blob_t blob; +bool LibreTinyPreferenceBackend::save(const uint8_t *data, size_t len) { + // try find in pending saves and update that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + obj.data.set(data, len); + return true; + } + } + NVSData save{}; + save.key = this->key; + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); + return true; +} - bool save(const uint8_t *data, size_t len) override { - // try find in pending saves and update that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - obj.data.set(data, len); - return true; +bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) { + // try find in pending saves and load from that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + if (obj.data.size() != len) { + // size mismatch + return false; } + memcpy(data, obj.data.data(), len); + return true; } - NVSData save{}; - save.key = this->key; - save.data.set(data, len); - s_pending_save.push_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); - return true; } - bool load(uint8_t *data, size_t len) override { - // try find in pending saves and load from that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - if (obj.data.size() != len) { - // size mismatch - return false; - } - memcpy(data, obj.data.data(), len); - return true; - } - } + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + fdb_blob_make(this->blob, data, len); + size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); + return false; + } else { + ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len); + } + return true; +} +void LibreTinyPreferences::open() { + // + fdb_err_t err = fdb_kvdb_init(&this->db, "esphome", "kvs", NULL, NULL); + if (err != FDB_NO_ERR) { + LT_E("fdb_kvdb_init(...) failed: %d", err); + } else { + LT_I("Preferences initialized"); + } +} + +ESPPreferenceObject LibreTinyPreferences::make_preference(size_t length, uint32_t type) { + auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->db = &this->db; + pref->blob = &this->blob; + pref->key = type; + + return ESPPreferenceObject(pref); +} + +bool LibreTinyPreferences::sync() { + if (s_pending_save.empty()) + return true; + + ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); + int cached = 0, written = 0, failed = 0; + fdb_err_t last_err = FDB_NO_ERR; + uint32_t last_key = 0; + + for (const auto &save : s_pending_save) { char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); - fdb_blob_make(this->blob, data, len); - size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); - if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); - return false; - } else { - ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len); - } - return true; - } -}; - -class LibreTinyPreferences : public ESPPreferences { - public: - struct fdb_kvdb db; - struct fdb_blob blob; - - void open() { - // - fdb_err_t err = fdb_kvdb_init(&db, "esphome", "kvs", NULL, NULL); - if (err != FDB_NO_ERR) { - LT_E("fdb_kvdb_init(...) failed: %d", err); - } else { - LT_I("Preferences initialized"); - } - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return this->make_preference(length, type); - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->db = &this->db; - pref->blob = &this->blob; - pref->key = type; - - return ESPPreferenceObject(pref); - } - - bool sync() override { - if (s_pending_save.empty()) - return true; - - ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - int cached = 0, written = 0, failed = 0; - fdb_err_t last_err = FDB_NO_ERR; - uint32_t last_key = 0; - - for (const auto &save : s_pending_save) { - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); - ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); - if (this->is_changed_(&this->db, save, key_str)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); - fdb_blob_make(&this->blob, save.data.data(), save.data.size()); - fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); - if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); - failed++; - last_err = err; - last_key = save.key; - continue; - } - written++; - } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); - cached++; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); + if (this->is_changed_(&this->db, save, key_str)) { + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + fdb_blob_make(&this->blob, save.data.data(), save.data.size()); + fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); + if (err != FDB_NO_ERR) { + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); + failed++; + last_err = err; + last_key = save.key; + continue; } + written++; + } else { + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); + cached++; } - s_pending_save.clear(); + } + s_pending_save.clear(); - ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, - failed); - if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%" PRIu32, failed, last_err, last_key); - } - - return failed == 0; + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + if (failed > 0) { + ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%" PRIu32, failed, last_err, last_key); } - protected: - bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) { - struct fdb_kv kv; - fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv); - if (kvp == nullptr) { - ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str); - return true; - } + return failed == 0; +} - // Check size first - if different, data has changed - if (kv.value_len != to_save.data.size()) { - return true; - } - - // Most preferences are small, use stack buffer with heap fallback for large ones - SmallBufferWithHeapFallback<256> stored_data(kv.value_len); - fdb_blob_make(&this->blob, stored_data.get(), kv.value_len); - size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob); - if (actual_len != kv.value_len) { - ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", key_str, actual_len, kv.value_len); - return true; - } - - // Compare the actual data - return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; - } - - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - s_pending_save.clear(); - - fdb_kv_set_default(&db); - fdb_kvdb_deinit(&db); +bool LibreTinyPreferences::is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) { + struct fdb_kv kv; + fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv); + if (kvp == nullptr) { + ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str); return true; } -}; + + // Check size first - if different, data has changed + if (kv.value_len != to_save.data.size()) { + return true; + } + + // Most preferences are small, use stack buffer with heap fallback for large ones + SmallBufferWithHeapFallback<256> stored_data(kv.value_len); + fdb_blob_make(&this->blob, stored_data.get(), kv.value_len); + size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob); + if (actual_len != kv.value_len) { + ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %zu != %zu", key_str, actual_len, (size_t) kv.value_len); + return true; + } + + // Compare the actual data + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; +} + +bool LibreTinyPreferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + s_pending_save.clear(); + + fdb_kv_set_default(&this->db); + fdb_kvdb_deinit(&this->db); + return true; +} static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +LibreTinyPreferences *get_preferences() { return &s_preferences; } + void setup_preferences() { s_preferences.open(); global_preferences = &s_preferences; @@ -187,9 +170,7 @@ void setup_preferences() { } // namespace esphome::libretiny namespace esphome { - ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/preferences.h b/esphome/components/libretiny/preferences.h index 68f377bd3ee..8365d590c22 100644 --- a/esphome/components/libretiny/preferences.h +++ b/esphome/components/libretiny/preferences.h @@ -1,11 +1,35 @@ #pragma once - #ifdef USE_LIBRETINY +#include "esphome/core/preference_backend.h" +#include <flashdb.h> + namespace esphome::libretiny { +struct NVSData; + +class LibreTinyPreferences final : public PreferencesMixin<LibreTinyPreferences> { + public: + using PreferencesMixin<LibreTinyPreferences>::make_preference; + void open(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { + return this->make_preference(length, type); + } + ESPPreferenceObject make_preference(size_t length, uint32_t type); + bool sync(); + bool reset(); + + struct fdb_kvdb db; + struct fdb_blob blob; + + protected: + bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str); +}; + void setup_preferences(); } // namespace esphome::libretiny +DECLARE_PREFERENCE_ALIASES(esphome::libretiny::LibreTinyPreferences) + #endif // USE_LIBRETINY diff --git a/esphome/components/rp2040/preference_backend.h b/esphome/components/rp2040/preference_backend.h new file mode 100644 index 00000000000..790ee8831de --- /dev/null +++ b/esphome/components/rp2040/preference_backend.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef USE_RP2040 + +#include <cstddef> +#include <cstdint> + +namespace esphome::rp2040 { + +class RP2040PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + size_t offset = 0; + uint32_t type = 0; +}; + +class RP2040Preferences; +RP2040Preferences *get_preferences(); + +} // namespace esphome::rp2040 + +namespace esphome { +using PreferenceBackend = rp2040::RP2040PreferenceBackend; +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index fa72fd9a246..0a91136a9fd 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -11,10 +11,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" -namespace esphome { -namespace rp2040 { +namespace esphome::rp2040 { static const char *const TAG = "rp2040.preferences"; @@ -39,129 +37,116 @@ template<class It> uint8_t calculate_crc(It first, It last, uint32_t type) { return crc; } -class RP2040PreferenceBackend : public ESPPreferenceBackend { - public: - size_t offset = 0; - uint32_t type = 0; +bool RP2040PreferenceBackend::save(const uint8_t *data, size_t len) { + const size_t buffer_size = len + 1; + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; + memcpy(buffer, data, len); + buffer[len] = calculate_crc(buffer, buffer + len, this->type); - bool save(const uint8_t *data, size_t len) override { - const size_t buffer_size = len + 1; - if (buffer_size > PREF_MAX_BUFFER_SIZE) + for (size_t i = 0; i < buffer_size; i++) { + uint32_t j = this->offset + i; + if (j >= RP2040_FLASH_STORAGE_SIZE) return false; - uint8_t buffer[PREF_MAX_BUFFER_SIZE]; - memcpy(buffer, data, len); - buffer[len] = calculate_crc(buffer, buffer + len, this->type); - - for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = this->offset + i; - if (j >= RP2040_FLASH_STORAGE_SIZE) - return false; - uint8_t v = buffer[i]; - uint8_t *ptr = &s_flash_storage[j]; - if (*ptr != v) - s_flash_dirty = true; - *ptr = v; - } - return true; + uint8_t v = buffer[i]; + uint8_t *ptr = &s_flash_storage[j]; + if (*ptr != v) + s_flash_dirty = true; + *ptr = v; } - bool load(uint8_t *data, size_t len) override { - const size_t buffer_size = len + 1; - if (buffer_size > PREF_MAX_BUFFER_SIZE) + return true; +} + +bool RP2040PreferenceBackend::load(uint8_t *data, size_t len) { + const size_t buffer_size = len + 1; + if (buffer_size > PREF_MAX_BUFFER_SIZE) + return false; + uint8_t buffer[PREF_MAX_BUFFER_SIZE]; + + for (size_t i = 0; i < buffer_size; i++) { + uint32_t j = this->offset + i; + if (j >= RP2040_FLASH_STORAGE_SIZE) return false; - uint8_t buffer[PREF_MAX_BUFFER_SIZE]; + buffer[i] = s_flash_storage[j]; + } - for (size_t i = 0; i < buffer_size; i++) { - uint32_t j = this->offset + i; - if (j >= RP2040_FLASH_STORAGE_SIZE) - return false; - buffer[i] = s_flash_storage[j]; - } + uint8_t crc = calculate_crc(buffer, buffer + len, this->type); + if (buffer[len] != crc) { + return false; + } - uint8_t crc = calculate_crc(buffer, buffer + len, this->type); - if (buffer[len] != crc) { - return false; - } + memcpy(data, buffer, len); + return true; +} - memcpy(data, buffer, len); +RP2040Preferences::RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} + +void RP2040Preferences::setup() { + ESP_LOGVV(TAG, "Loading preferences from flash"); + memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); +} + +ESPPreferenceObject RP2040Preferences::make_preference(size_t length, uint32_t type) { + uint32_t start = this->current_flash_offset; + uint32_t end = start + length + 1; + if (end > RP2040_FLASH_STORAGE_SIZE) { + return {}; + } + auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = start; + pref->type = type; + this->current_flash_offset = end; + return ESPPreferenceObject(pref); +} + +bool RP2040Preferences::sync() { + if (!s_flash_dirty) return true; - } -}; + if (s_prevent_write) + return false; -class RP2040Preferences : public ESPPreferences { - public: - uint32_t current_flash_offset = 0; + ESP_LOGD(TAG, "Saving"); - RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} - void setup() { - ESP_LOGVV(TAG, "Loading preferences from flash"); - memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); + { + InterruptLock lock; + ::rp2040.idleOtherCore(); + flash_range_erase((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, 4096); + flash_range_program((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage, + RP2040_FLASH_STORAGE_SIZE); + ::rp2040.resumeOtherCore(); } - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); + s_flash_dirty = false; + return true; +} + +bool RP2040Preferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + { + InterruptLock lock; + ::rp2040.idleOtherCore(); + flash_range_erase((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, 4096); + ::rp2040.resumeOtherCore(); } - - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - uint32_t start = this->current_flash_offset; - uint32_t end = start + length + 1; - if (end > RP2040_FLASH_STORAGE_SIZE) { - return {}; - } - auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = start; - pref->type = type; - current_flash_offset = end; - return {pref}; - } - - bool sync() override { - if (!s_flash_dirty) - return true; - if (s_prevent_write) - return false; - - ESP_LOGD(TAG, "Saving"); - - { - InterruptLock lock; - ::rp2040.idleOtherCore(); - flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096); - flash_range_program((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage, RP2040_FLASH_STORAGE_SIZE); - ::rp2040.resumeOtherCore(); - } - - s_flash_dirty = false; - return true; - } - - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - { - InterruptLock lock; - ::rp2040.idleOtherCore(); - flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096); - ::rp2040.resumeOtherCore(); - } - s_prevent_write = true; - return true; - } - - protected: - uint8_t *eeprom_sector_; -}; + s_prevent_write = true; + return true; +} static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +RP2040Preferences *get_preferences() { return &s_preferences; } + void setup_preferences() { s_preferences.setup(); global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } -} // namespace rp2040 +} // namespace esphome::rp2040 +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_RP2040 diff --git a/esphome/components/rp2040/preferences.h b/esphome/components/rp2040/preferences.h index b815c6d58a6..eb8c3e5f64f 100644 --- a/esphome/components/rp2040/preferences.h +++ b/esphome/components/rp2040/preferences.h @@ -1,14 +1,33 @@ #pragma once - #ifdef USE_RP2040 -namespace esphome { -namespace rp2040 { +#include "esphome/core/preference_backend.h" + +namespace esphome::rp2040 { + +class RP2040Preferences final : public PreferencesMixin<RP2040Preferences> { + public: + using PreferencesMixin<RP2040Preferences>::make_preference; + RP2040Preferences(); + void setup(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { + return this->make_preference(length, type); + } + ESPPreferenceObject make_preference(size_t length, uint32_t type); + bool sync(); + bool reset(); + + uint32_t current_flash_offset = 0; + + protected: + uint8_t *eeprom_sector_; +}; void setup_preferences(); void preferences_prevent_write(bool prevent); -} // namespace rp2040 -} // namespace esphome +} // namespace esphome::rp2040 + +DECLARE_PREFERENCE_ALIASES(esphome::rp2040::RP2040Preferences) #endif // USE_RP2040 diff --git a/esphome/components/zephyr/preference_backend.h b/esphome/components/zephyr/preference_backend.h new file mode 100644 index 00000000000..07e5d8053c7 --- /dev/null +++ b/esphome/components/zephyr/preference_backend.h @@ -0,0 +1,48 @@ +#pragma once +#ifdef USE_ZEPHYR +#ifdef CONFIG_SETTINGS + +#include <cstddef> +#include <cstdint> +#include <cinttypes> +#include <cstdio> +#include <cstring> +#include <vector> + +namespace esphome::zephyr { + +static constexpr const char *ESPHOME_SETTINGS_KEY = "esphome"; + +// Buffer size for key: "esphome/" (8) + max hex uint32 (8) + null terminator (1) = 17; use 20 for safety margin +static constexpr size_t KEY_BUFFER_SIZE = 20; + +class ZephyrPreferenceBackend final { + public: + explicit ZephyrPreferenceBackend(uint32_t type) : type_(type) {} + ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)), type_(type) {} + + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t get_type() const { return this->type_; } + void format_key(char *buf, size_t size) const { + snprintf(buf, size, "%s/%" PRIx32, ESPHOME_SETTINGS_KEY, this->type_); + } + + std::vector<uint8_t> data; + + protected: + uint32_t type_ = 0; +}; + +class ZephyrPreferences; +ZephyrPreferences *get_preferences(); + +} // namespace esphome::zephyr + +namespace esphome { +using PreferenceBackend = zephyr::ZephyrPreferenceBackend; +} // namespace esphome + +#endif // CONFIG_SETTINGS +#endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index f02fa16326c..df69c0e6522 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -2,167 +2,138 @@ #ifdef CONFIG_SETTINGS #include <zephyr/kernel.h> -#include "esphome/core/preferences.h" +#include "preferences.h" #include "esphome/core/log.h" #include <zephyr/settings/settings.h> #include <cinttypes> #include <cstring> -namespace esphome { -namespace zephyr { +namespace esphome::zephyr { static const char *const TAG = "zephyr.preferences"; -#define ESPHOME_SETTINGS_KEY "esphome" +bool ZephyrPreferenceBackend::save(const uint8_t *data, size_t len) { + this->data.resize(len); + std::memcpy(this->data.data(), data, len); + ESP_LOGVV(TAG, "save key: %" PRIu32 ", len: %zu", this->type_, len); + return true; +} -// Buffer size for key: "esphome/" (8) + max hex uint32 (8) + null terminator (1) = 17; use 20 for safety margin -static constexpr size_t KEY_BUFFER_SIZE = 20; - -class ZephyrPreferenceBackend : public ESPPreferenceBackend { - public: - ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } - ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; } - - bool save(const uint8_t *data, size_t len) override { - this->data.resize(len); - std::memcpy(this->data.data(), data, len); - ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len); - return true; - } - - bool load(uint8_t *data, size_t len) override { - if (len != this->data.size()) { - char key_buf[KEY_BUFFER_SIZE]; - this->format_key(key_buf, sizeof(key_buf)); - ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", key_buf, this->data.size(), len); - return false; - } - std::memcpy(data, this->data.data(), len); - ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len); - return true; - } - - uint32_t get_type() const { return this->type_; } - void format_key(char *buf, size_t size) const { snprintf(buf, size, ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } - - std::vector<uint8_t> data; - - protected: - uint32_t type_ = 0; -}; - -class ZephyrPreferences : public ESPPreferences { - public: - void open() { - int err = settings_subsys_init(); - if (err) { - ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); - return; - } - - static struct settings_handler settings_cb = { - .name = ESPHOME_SETTINGS_KEY, - .h_set = load_setting, - .h_export = export_settings, - }; - - err = settings_register(&settings_cb); - if (err) { - ESP_LOGE(TAG, "setting_register failed, err, %d", err); - return; - } - - err = settings_load_subtree(ESPHOME_SETTINGS_KEY); - if (err) { - ESP_LOGE(TAG, "Cannot load settings, err: %d", err); - return; - } - ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size()); - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - for (auto *backend : this->backends_) { - if (backend->get_type() == type) { - return ESPPreferenceObject(backend); - } - } - printf("type %u size %u\n", type, this->backends_.size()); - auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory) +bool ZephyrPreferenceBackend::load(uint8_t *data, size_t len) { + if (len != this->data.size()) { char key_buf[KEY_BUFFER_SIZE]; - pref->format_key(key_buf, sizeof(key_buf)); - ESP_LOGD(TAG, "Add new setting %s.", key_buf); - this->backends_.push_back(pref); - return ESPPreferenceObject(pref); + this->format_key(key_buf, sizeof(key_buf)); + ESP_LOGE(TAG, "size of setting key %s changed, from: %zu, to: %zu", key_buf, this->data.size(), len); + return false; + } + std::memcpy(data, this->data.data(), len); + ESP_LOGVV(TAG, "load key: %" PRIu32 ", len: %zu", this->type_, len); + return true; +} + +void ZephyrPreferences::open() { + int err = settings_subsys_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); + return; } - bool sync() override { - ESP_LOGD(TAG, "Save settings"); - int err = settings_save(); - if (err) { - ESP_LOGE(TAG, "Cannot save settings, err: %d", err); - return false; + static struct settings_handler settings_cb = { + .name = ESPHOME_SETTINGS_KEY, + .h_set = load_setting, + .h_export = export_settings, + }; + + err = settings_register(&settings_cb); + if (err) { + ESP_LOGE(TAG, "setting_register failed, err, %d", err); + return; + } + + err = settings_load_subtree(ESPHOME_SETTINGS_KEY); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + return; + } + ESP_LOGD(TAG, "Loaded %zu settings.", this->backends_.size()); +} + +ESPPreferenceObject ZephyrPreferences::make_preference(size_t length, uint32_t type) { + for (auto *backend : this->backends_) { + if (backend->get_type() == type) { + return ESPPreferenceObject(backend); } - return true; } + auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory) + char key_buf[KEY_BUFFER_SIZE]; + pref->format_key(key_buf, sizeof(key_buf)); + ESP_LOGD(TAG, "Add new setting %s.", key_buf); + this->backends_.push_back(pref); + return ESPPreferenceObject(pref); +} - bool reset() override { - ESP_LOGD(TAG, "Reset settings"); - for (auto *backend : this->backends_) { - // save empty delete data - backend->data.clear(); - } - sync(); - return true; +bool ZephyrPreferences::sync() { + ESP_LOGD(TAG, "Save settings"); + int err = settings_save(); + if (err) { + ESP_LOGE(TAG, "Cannot save settings, err: %d", err); + return false; } + return true; +} - protected: - std::vector<ZephyrPreferenceBackend *> backends_; - - static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { - auto type = parse_hex<uint32_t>(name); - if (!type.has_value()) { - std::string full_name(ESPHOME_SETTINGS_KEY); - full_name += "/"; - full_name += name; - // Delete unusable keys. Otherwise it will stay in flash forever. - settings_delete(full_name.c_str()); - return 1; - } - std::vector<uint8_t> data(len); - int err = read_cb(cb_arg, data.data(), len); - - ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err); - auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory) - static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref); - return 0; +bool ZephyrPreferences::reset() { + ESP_LOGD(TAG, "Reset settings"); + for (auto *backend : this->backends_) { + // save empty delete data + backend->data.clear(); } + this->sync(); + return true; +} - static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { - for (auto *backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) { - char name[KEY_BUFFER_SIZE]; - backend->format_key(name, sizeof(name)); - int err = cb(name, backend->data.data(), backend->data.size()); - ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name, backend->data.size(), err); - } - return 0; +int ZephyrPreferences::load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { + auto type = parse_hex<uint32_t>(name); + if (!type.has_value()) { + std::string full_name(ESPHOME_SETTINGS_KEY); + full_name += "/"; + full_name += name; + // Delete unusable keys. Otherwise it will stay in flash forever. + settings_delete(full_name.c_str()); + return 1; } -}; + std::vector<uint8_t> data(len); + int err = read_cb(cb_arg, data.data(), len); + + ESP_LOGD(TAG, "load setting, name: %s(%" PRIu32 "), len %zu, err %d", name, *type, len, err); + auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory) + get_preferences()->backends_.push_back(pref); + return 0; +} + +int ZephyrPreferences::export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { + for (auto *backend : get_preferences()->backends_) { + char name[KEY_BUFFER_SIZE]; + backend->format_key(name, sizeof(name)); + int err = cb(name, backend->data.data(), backend->data.size()); + ESP_LOGD(TAG, "save in flash, name %s, len %zu, err %d", name, backend->data.size(), err); + } + return 0; +} static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +ZephyrPreferences *get_preferences() { return &s_preferences; } + void setup_preferences() { global_preferences = &s_preferences; s_preferences.open(); } -} // namespace zephyr +} // namespace esphome::zephyr +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h index 4bee96d79e8..9e2555f9105 100644 --- a/esphome/components/zephyr/preferences.h +++ b/esphome/components/zephyr/preferences.h @@ -1,11 +1,36 @@ #pragma once - #ifdef USE_ZEPHYR +#ifdef CONFIG_SETTINGS + +#include "esphome/core/preference_backend.h" +#include <zephyr/settings/settings.h> +#include <vector> namespace esphome::zephyr { +class ZephyrPreferences final : public PreferencesMixin<ZephyrPreferences> { + public: + using PreferencesMixin<ZephyrPreferences>::make_preference; + void open(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { + return this->make_preference(length, type); + } + ESPPreferenceObject make_preference(size_t length, uint32_t type); + bool sync(); + bool reset(); + + protected: + std::vector<ZephyrPreferenceBackend *> backends_; + + static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg); + static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)); +}; + void setup_preferences(); -} +} // namespace esphome::zephyr -#endif +DECLARE_PREFERENCE_ALIASES(esphome::zephyr::ZephyrPreferences) + +#endif // CONFIG_SETTINGS +#endif // USE_ZEPHYR diff --git a/esphome/core/preference_backend.h b/esphome/core/preference_backend.h new file mode 100644 index 00000000000..3766934da46 --- /dev/null +++ b/esphome/core/preference_backend.h @@ -0,0 +1,83 @@ +#pragma once + +#include <cstdint> + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +// Include the concrete preference backend for the active platform. +// Each header defines its backend class, forward-declares its manager class, +// declares get_preferences(), and provides the PreferenceBackend alias. +#ifdef USE_ESP32 +#include "esphome/components/esp32/preference_backend.h" +#elif defined(USE_ESP8266) +#include "esphome/components/esp8266/preference_backend.h" +#elif defined(USE_RP2040) +#include "esphome/components/rp2040/preference_backend.h" +#elif defined(USE_LIBRETINY) +#include "esphome/components/libretiny/preference_backend.h" +#elif defined(USE_HOST) +#include "esphome/components/host/preference_backend.h" +#elif defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS) +#include "esphome/components/zephyr/preference_backend.h" +#endif + +namespace esphome { + +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ + !defined(USE_HOST) && !(defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS)) +// Stub for static analysis when no platform is defined. +struct PreferenceBackend { + bool save(const uint8_t *, size_t) { return false; } + bool load(uint8_t *, size_t) { return false; } +}; +#endif + +using ESPPreferenceBackend = PreferenceBackend; + +class ESPPreferenceObject { + public: + ESPPreferenceObject() = default; + explicit ESPPreferenceObject(PreferenceBackend *backend) : backend_(backend) {} + + template<typename T> bool save(const T *src) { + if (this->backend_ == nullptr) + return false; + return this->backend_->save(reinterpret_cast<const uint8_t *>(src), sizeof(T)); + } + + template<typename T> bool load(T *dest) { + if (this->backend_ == nullptr) + return false; + return this->backend_->load(reinterpret_cast<uint8_t *>(dest), sizeof(T)); + } + + protected: + PreferenceBackend *backend_{nullptr}; +}; + +/// CRTP mixin providing type-safe template make_preference<T>() helpers. +/// Platform preferences classes inherit this to avoid duplicating these templates. +template<typename Derived> class PreferencesMixin { + public: + template<typename T, enable_if_t<is_trivially_copyable<T>::value, bool> = true> + ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { + return static_cast<Derived *>(this)->make_preference(sizeof(T), type, in_flash); + } + + template<typename T, enable_if_t<is_trivially_copyable<T>::value, bool> = true> + ESPPreferenceObject make_preference(uint32_t type) { + return static_cast<Derived *>(this)->make_preference(sizeof(T), type); + } +}; + +// Macro for platform preferences.h headers to declare the standard aliases. +// Must be used at file scope (outside any namespace). +#define DECLARE_PREFERENCE_ALIASES(platform_class) \ + namespace esphome { \ + using Preferences = platform_class; \ + using ESPPreferences = Preferences; \ + extern ESPPreferences *global_preferences; /* NOLINT(cppcoreguidelines-avoid-non-const-global-variables) */ \ + } + +} // namespace esphome diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 6d2dd967e9a..64a0a927e67 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -1,50 +1,35 @@ #pragma once -#include <cstring> -#include <cstdint> - -#include "esphome/core/helpers.h" +#include "esphome/core/preference_backend.h" +// Include the concrete preferences manager for the active platform. +// Each header defines its manager class and provides the Preferences, +// ESPPreferences, and global_preferences declarations. +#ifdef USE_ESP32 +#include "esphome/components/esp32/preferences.h" +#elif defined(USE_ESP8266) +#include "esphome/components/esp8266/preferences.h" +#elif defined(USE_RP2040) +#include "esphome/components/rp2040/preferences.h" +#elif defined(USE_LIBRETINY) +#include "esphome/components/libretiny/preferences.h" +#elif defined(USE_HOST) +#include "esphome/components/host/preferences.h" +#elif defined(USE_ZEPHYR) && defined(CONFIG_SETTINGS) +#include "esphome/components/zephyr/preferences.h" +#else namespace esphome { - -class ESPPreferenceBackend { - public: - virtual bool save(const uint8_t *data, size_t len) = 0; - virtual bool load(uint8_t *data, size_t len) = 0; -}; - -class ESPPreferenceObject { - public: - ESPPreferenceObject() = default; - ESPPreferenceObject(ESPPreferenceBackend *backend) : backend_(backend) {} - - template<typename T> bool save(const T *src) { - if (backend_ == nullptr) - return false; - return backend_->save(reinterpret_cast<const uint8_t *>(src), sizeof(T)); - } - - template<typename T> bool load(T *dest) { - if (backend_ == nullptr) - return false; - return backend_->load(reinterpret_cast<uint8_t *>(dest), sizeof(T)); - } - - protected: - ESPPreferenceBackend *backend_{nullptr}; -}; - -class ESPPreferences { - public: - virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) = 0; - virtual ESPPreferenceObject make_preference(size_t length, uint32_t type) = 0; +struct Preferences : public PreferencesMixin<Preferences> { + using PreferencesMixin<Preferences>::make_preference; + ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; } + ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; } /** * Commit pending writes to flash. * * @return true if write is successful. */ - virtual bool sync() = 0; + bool sync() { return false; } /** * Forget all unsaved changes and re-initialize the permanent preferences storage. @@ -52,19 +37,9 @@ class ESPPreferences { * * @return true if operation is successful. */ - virtual bool reset() = 0; - - template<typename T, enable_if_t<is_trivially_copyable<T>::value, bool> = true> - ESPPreferenceObject make_preference(uint32_t type, bool in_flash) { - return this->make_preference(sizeof(T), type, in_flash); - } - - template<typename T, enable_if_t<is_trivially_copyable<T>::value, bool> = true> - ESPPreferenceObject make_preference(uint32_t type) { - return this->make_preference(sizeof(T), type); - } + bool reset() { return false; } }; - +using ESPPreferences = Preferences; extern ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome +#endif From e1334cf57f8b41bff21cab25cafb65bb5c0f3171 Mon Sep 17 00:00:00 2001 From: Nate Clark <nate@nateclark.com> Date: Thu, 19 Mar 2026 01:40:48 -0400 Subject: [PATCH 0805/2056] [mqtt] Support JSON payload with code for alarm control panel commands (#14731) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- .../mqtt/mqtt_alarm_control_panel.cpp | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index a461a140ae1..74a60b3624b 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -23,27 +23,52 @@ static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + +static bool apply_command(AlarmControlPanelCall &call, const char *state) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_AWAY")) == 0) { + call.arm_away(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_HOME")) == 0) { + call.arm_home(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_NIGHT")) == 0) { + call.arm_night(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_VACATION")) == 0) { + call.arm_vacation(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) { + call.arm_custom_bypass(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("DISARM")) == 0) { + call.disarm(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("PENDING")) == 0) { + call.pending(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("TRIGGERED")) == 0) { + call.triggered(); + } else { + return false; + } + return true; +} + void MQTTAlarmControlPanelComponent::setup() { this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); - if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_AWAY")) == 0) { - call.arm_away(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_HOME")) == 0) { - call.arm_home(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_NIGHT")) == 0) { - call.arm_night(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_VACATION")) == 0) { - call.arm_vacation(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) { - call.arm_custom_bypass(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("DISARM")) == 0) { - call.disarm(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("PENDING")) == 0) { - call.pending(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("TRIGGERED")) == 0) { - call.triggered(); - } else { + if (!payload.empty() && payload[0] == '{') { + // JSON payload: {"state": "DISARM", "code": "1234"} + JsonDocument doc = json::parse_json(payload); + JsonObject root = doc.as<JsonObject>(); + if (!root.isNull()) { + const char *state = root[ESPHOME_F("state")]; + if (state == nullptr) { + ESP_LOGW(TAG, "'%s': JSON payload missing 'state' key", this->friendly_name_().c_str()); + } else if (!apply_command(call, state)) { + ESP_LOGW(TAG, "'%s': Received unknown state in JSON payload: %s", this->friendly_name_().c_str(), state); + } else { + const char *code = root[ESPHOME_F("code")]; + if (code != nullptr) { + call.set_code(code); + } + } + } + } else if (!apply_command(call, payload.c_str())) { ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); From 9d6f2f71e89673d33ce5360d2ed7aab4da331bd0 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 19 Mar 2026 00:51:17 -0500 Subject: [PATCH 0806/2056] [speaker_source] Reshuffle playlist on repeat all restart (#14773) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/speaker_source/speaker_source_media_player.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 9d02ac9c744..2caab828fb4 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -457,6 +457,9 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { if (ps.playlist_index < ps.playlist.size()) { this->queue_play_current_(pipeline, ps.playlist_delay_ms); } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + if (!ps.shuffle_indices.empty()) { + this->shuffle_playlist_(pipeline); + } ps.playlist_index = 0; this->queue_play_current_(pipeline, ps.playlist_delay_ms); } From 2341d510d39cc2bd9bfb8a93b64dc421d1d3e545 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:31:33 +1000 Subject: [PATCH 0807/2056] [lvgl] Migrate to library v9.5.0 (#12312) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/font/__init__.py | 1 + esphome/components/font/font.cpp | 85 ++- esphome/components/font/font.h | 2 +- esphome/components/image/__init__.py | 94 ++- esphome/components/image/image.cpp | 28 +- esphome/components/image/image.h | 2 +- esphome/components/lvgl/__init__.py | 168 +++-- esphome/components/lvgl/automation.py | 97 ++- esphome/components/lvgl/defines.py | 188 +++++- esphome/components/lvgl/encoders.py | 2 +- esphome/components/lvgl/gradient.py | 50 +- esphome/components/lvgl/keypads.py | 2 +- esphome/components/lvgl/layout.py | 24 +- esphome/components/lvgl/light/lvgl_light.h | 2 +- esphome/components/lvgl/lv_validation.py | 117 ++-- esphome/components/lvgl/lvcode.py | 8 + esphome/components/lvgl/lvgl_esphome.cpp | 393 ++++++++---- esphome/components/lvgl/lvgl_esphome.h | 133 ++-- esphome/components/lvgl/lvgl_hal.h | 21 - esphome/components/lvgl/number/__init__.py | 6 +- esphome/components/lvgl/schemas.py | 62 +- esphome/components/lvgl/select/lvgl_select.h | 6 +- esphome/components/lvgl/styles.py | 43 +- esphome/components/lvgl/text/lvgl_text.h | 9 +- esphome/components/lvgl/touchscreens.py | 2 - esphome/components/lvgl/trigger.py | 4 +- esphome/components/lvgl/types.py | 154 +---- esphome/components/lvgl/widgets/__init__.py | 348 ++++++++--- esphome/components/lvgl/widgets/arc.py | 47 +- esphome/components/lvgl/widgets/button.py | 8 +- .../components/lvgl/widgets/buttonmatrix.py | 36 +- esphome/components/lvgl/widgets/canvas.py | 278 ++++++--- esphome/components/lvgl/widgets/container.py | 12 +- esphome/components/lvgl/widgets/img.py | 49 +- esphome/components/lvgl/widgets/label.py | 4 +- esphome/components/lvgl/widgets/led.py | 6 +- esphome/components/lvgl/widgets/line.py | 6 +- esphome/components/lvgl/widgets/lv_bar.py | 4 +- esphome/components/lvgl/widgets/meter.py | 585 +++++++++++++----- esphome/components/lvgl/widgets/msgbox.py | 173 +++--- esphome/components/lvgl/widgets/obj.py | 3 +- esphome/components/lvgl/widgets/qrcode.py | 47 +- esphome/components/lvgl/widgets/slider.py | 14 +- esphome/components/lvgl/widgets/spinner.py | 21 +- esphome/components/lvgl/widgets/tabview.py | 45 +- esphome/components/lvgl/widgets/tileview.py | 14 +- esphome/components/online_image/__init__.py | 9 + esphome/components/runtime_image/__init__.py | 3 +- .../runtime_image/runtime_image.cpp | 9 +- esphome/core/defines.h | 1 + esphome/idf_component.yml | 2 + platformio.ini | 5 +- tests/components/lvgl/lvgl-package.yaml | 75 ++- 54 files changed, 2312 insertions(+), 1197 deletions(-) delete mode 100644 esphome/components/lvgl/lvgl_hal.h diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 87b4ebb2c69..72023e511db 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a +44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 2667dbdbdfb..c8813bf1bc3 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -552,6 +552,7 @@ async def to_code(config): """ # get the codepoints from glyphsets and flatten to a set of chrs. + cg.add_define("USE_FONT") point_set: set[str] = { chr(x) for x in flatten( diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 5e3bf1dd204..ecf0ca6bdd2 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -9,13 +9,87 @@ namespace font { static const char *const TAG = "font"; #ifdef USE_LVGL_FONT -const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (Font *) font->dsc; - const auto *gd = fe->get_glyph_data_(unicode_letter); +static const uint8_t OPA4_TABLE[16] = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255}; + +static const uint8_t OPA2_TABLE[4] = {0, 85, 170, 255}; + +const void *Font::get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf) { + const auto *font = dsc->resolved_font; + auto *const fe = (Font *) font->dsc; + + const auto *gd = fe->get_glyph_data_(dsc->gid.index); if (gd == nullptr) { return nullptr; } - return gd->data; + + const uint8_t *bitmap_in = gd->data; + uint8_t *bitmap_out_tmp = draw_buf->data; + int32_t i = 0; + int32_t x, y; + uint32_t stride = lv_draw_buf_width_to_stride(gd->width, LV_COLOR_FORMAT_A8); + + switch (fe->get_bpp()) { + case 1: { + uint8_t mask = 0; + uint8_t byte = 0; + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++) { + if (mask == 0) { + mask = 0x80; + byte = *bitmap_in++; + } + bitmap_out_tmp[x] = byte & mask ? 255 : 0; + mask >>= 1; + } + bitmap_out_tmp += stride; + } + } break; + + case 2: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + switch (i & 0x3) { + default: + bitmap_out_tmp[x] = OPA2_TABLE[(*bitmap_in) >> 6]; + break; + case 1: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 4) & 0x3]; + break; + case 2: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 2) & 0x3]; + break; + case 3: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 0) & 0x3]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 4: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + i = i & 0x1; + if (i == 0) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) >> 4]; + } else if (i == 1) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) & 0xF]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 8: + memcpy(bitmap_out_tmp, bitmap_in, gd->width * gd->height); + break; + default: + ESP_LOGD(TAG, "Unknown bpp: %d", fe->get_bpp()); + break; + } + return draw_buf; } bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { @@ -30,7 +104,8 @@ bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uin dsc->box_w = gd->width; dsc->box_h = gd->height; dsc->is_placeholder = 0; - dsc->bpp = fe->get_bpp(); + dsc->format = (lv_font_glyph_format_t) fe->get_bpp(); + dsc->gid.index = unicode_letter; return true; } diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 262ded3be4c..4a09d7314d2 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -90,7 +90,7 @@ class Font uint8_t bpp_; // bits per pixel #ifdef USE_LVGL_FONT lv_font_t lv_font_{}; - static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static const void *get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf); static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); const Glyph *get_glyph_data_(uint32_t unicode_letter); uint32_t last_letter_{}; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 6ff75d77092..6fb0e46d937 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt +from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,7 @@ class ImageEncoder: def __init__(self, width, height, transparency, dither, invert_alpha): """ - :param width: The image width in pixels + :param width: The image width in pixels (or bytes) :param height: The image height in pixels :param transparency: Transparency type :param dither: Dither method @@ -93,11 +94,12 @@ class ImageEncoder: self.transparency = transparency self.width = width self.height = height - self.data = [0 for _ in range(width * height)] + self.data = [0] * width * height self.dither = dither self.index = 0 self.invert_alpha = invert_alpha self.path = "" + self.big_endian = False def convert(self, image, path): """ @@ -119,12 +121,21 @@ class ImageEncoder: :return: """ + def end_image(self): + """ + Called at the end of the image. + :return: + """ + + def set_big_endian(self, big_endian: bool) -> None: + self.big_endian = big_endian + @classmethod def is_endian(cls) -> bool: """ Check if the image encoder supports endianness configuration """ - return getattr(cls, "set_big_endian", None) is not None + return False @classmethod def get_options(cls) -> list[str]: @@ -212,18 +223,21 @@ class ImageGrayscale(ImageEncoder): class ImageRGB565(ImageEncoder): def __init__(self, width, height, transparency, dither, invert_alpha): - stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2 super().__init__( - width * stride, + width * 2, height, transparency, dither, invert_alpha, ) - self.big_endian = True + self.alpha = [0] * width * height - def set_big_endian(self, big_endian: bool) -> None: - self.big_endian = big_endian + @classmethod + def is_endian(cls) -> bool: + """ + Check if the image encoder supports endianness configuration + """ + return True def convert(self, image, path): return image.convert("RGBA") @@ -233,6 +247,9 @@ class ImageRGB565(ImageEncoder): r = r >> 3 g = g >> 2 b = b >> 3 + if self.invert_alpha: + a ^= 0xFF + self.alpha[self.index // 2] = a if self.transparency == CONF_CHROMA_KEY: if r == 0 and g == 1 and b == 0: g = 0 @@ -251,11 +268,10 @@ class ImageRGB565(ImageEncoder): self.index += 1 self.data[self.index] = rgb >> 8 self.index += 1 + + def end_image(self): if self.transparency == CONF_ALPHA_CHANNEL: - if self.invert_alpha: - a ^= 0xFF - self.data[self.index] = a - self.index += 1 + self.data.extend(self.alpha) class ImageRGB(ImageEncoder): @@ -281,11 +297,11 @@ class ImageRGB(ImageEncoder): r = 0 g = 1 b = 0 - self.data[self.index] = r + self.data[self.index] = b self.index += 1 self.data[self.index] = g self.index += 1 - self.data[self.index] = b + self.data[self.index] = r self.index += 1 if self.transparency == CONF_ALPHA_CHANNEL: if self.invert_alpha: @@ -655,6 +671,24 @@ def _config_schema(value): CONFIG_SCHEMA = _config_schema +def _final_validate(config): + """ + For LVGL 9 the default byte order for RGB565 images is little-endian + :param config: + :return: + """ + fv = full_config.get() + if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config): + config = config.copy() + for c in config: + if not c.get(CONF_BYTE_ORDER): + c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def write_image(config, all_frames=False): path = Path(config[CONF_FILE]) if not path.is_file(): @@ -720,6 +754,7 @@ async def write_image(config, all_frames=False): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() + encoder.end_image() rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) @@ -729,31 +764,24 @@ async def write_image(config, all_frames=False): return prog_arr, width, height, image_type, trans_value, frame_count -async def _image_to_code(entry): - """ - Convert a single image entry to code and return its metadata. - :param entry: The config entry for the image. - :return: An ImageMetaData object - """ - prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) - cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value) - return ImageMetaData( - width, - height, - entry[CONF_TYPE], - entry[CONF_TRANSPARENCY], +def add_metadata(id: str, width: int, height: int, image_type: str, transparency): + all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + all_metadata[str(id)] = ImageMetaData( + width=width, height=height, image_type=image_type, transparency=transparency ) async def to_code(config): cg.add_define("USE_IMAGE") # By now the config will be a simple list. - # Use a subkey to allow for other data in the future - CORE.data[DOMAIN] = { - KEY_METADATA: { - entry[CONF_ID].id: await _image_to_code(entry) for entry in config - } - } + for entry in config: + prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) + cg.new_Pvariable( + entry[CONF_ID], prog_arr, width, height, image_type, trans_value + ) + add_metadata( + entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY] + ) def get_all_image_metadata() -> dict[str, ImageMetaData]: diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 90e021467f0..a6f9e35e2e5 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -105,22 +105,22 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off } } #ifdef USE_LVGL -lv_img_dsc_t *Image::get_lv_img_dsc() { +lv_image_dsc_t *Image::get_lv_image_dsc() { // lazily construct lvgl image_dsc. if (this->dsc_.data != this->data_start_) { this->dsc_.data = this->data_start_; - this->dsc_.header.always_zero = 0; - this->dsc_.header.reserved = 0; + this->dsc_.header.reserved_2 = 0; + this->dsc_.header.stride = this->get_width_stride(); this->dsc_.header.w = this->width_; this->dsc_.header.h = this->height_; this->dsc_.data_size = this->get_width_stride() * this->get_height(); switch (this->get_type()) { case IMAGE_TYPE_BINARY: - this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT; + this->dsc_.header.cf = LV_COLOR_FORMAT_A1; break; case IMAGE_TYPE_GRAYSCALE: - this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; + this->dsc_.header.cf = LV_COLOR_FORMAT_A8; break; case IMAGE_TYPE_RGB: @@ -138,7 +138,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { } #else this->dsc_.header.cf = - this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888; + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_COLOR_FORMAT_ARGB8888 : LV_COLOR_FORMAT_RGB888; #endif break; @@ -146,14 +146,10 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { #if LV_COLOR_DEPTH == 16 switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; - break; - case TRANSPARENCY_CHROMA_KEY: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565A8; break; default: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; - break; + this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565; } #else this->dsc_.header.cf = @@ -173,8 +169,8 @@ bool Image::get_binary_pixel_(int x, int y) const { } Color Image::get_rgb_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8; - Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), 0xFF); + Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 0), 0xFF); switch (this->transparency_) { case TRANSPARENCY_CHROMA_KEY: @@ -200,7 +196,7 @@ Color Image::get_rgb565_pixel_(int x, int y) const { auto a = 0xFF; switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: - a = progmem_read_byte(pos + 2); + a = progmem_read_byte(this->data_start_ + this->width_ * this->height_ * 2 + (x + y * this->width_)); break; case TRANSPARENCY_CHROMA_KEY: if (rgb565 == 0x0020) @@ -239,7 +235,7 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T this->bpp_ = 8; break; case IMAGE_TYPE_RGB565: - this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16; + this->bpp_ = 16; break; case IMAGE_TYPE_RGB: this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24; diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 4024ab13572..d4865570e40 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -41,7 +41,7 @@ class Image : public display::BaseImage { bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; } #ifdef USE_LVGL - lv_img_dsc_t *get_lv_img_dsc(); + lv_image_dsc_t *get_lv_image_dsc(); #endif protected: bool get_binary_pixel_(int x, int y) const; diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c9cad1ac905..c37a32eccab 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,12 +1,30 @@ import importlib -import logging from pathlib import Path import pkgutil from esphome.automation import build_automation, validate_automation import esphome.codegen as cg -from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING +from esphome.components.const import ( + CONF_BYTE_ORDER, + CONF_COLOR_DEPTH, + CONF_DRAW_ROUNDING, +) from esphome.components.display import Display +from esphome.components.esp32 import ( + VARIANT_ESP32P4, + add_idf_component, + add_idf_sdkconfig_option, + get_esp32_variant, +) +from esphome.components.image import ( + CONF_OPAQUE, + IMAGE_TYPE, + ImageBinary, + ImageGrayscale, + ImageRGB, + ImageRGB565, + get_image_metadata, +) from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( @@ -21,7 +39,6 @@ from esphome.const import ( CONF_PAGES, CONF_TIMEOUT, CONF_TRIGGER_ID, - CONF_TYPE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj @@ -30,8 +47,7 @@ from esphome.helpers import write_file_if_changed from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets -from .automation import disp_update, focused_widgets, refreshed_widgets -from .defines import add_define +from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -45,12 +61,13 @@ from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, + STYLE_REMAP, WIDGET_TYPES, any_widget_schema, container_schema, obj_schema, ) -from .styles import add_top_layer, styles_to_code, theme_to_code +from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import add_on_boot_triggers, generate_triggers from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns @@ -58,7 +75,7 @@ from .widgets import ( LvScrActType, Widget, add_widgets, - get_scr_act, + get_screen_active, set_obj_properties, styles_used, ) @@ -84,7 +101,6 @@ DOMAIN = "lvgl" DEPENDENCIES = ["display"] AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] -LOGGER = logging.getLogger(__name__) HELLO_WORLD_FILE = "hello_world.yaml" @@ -102,6 +118,7 @@ def as_macro(macro, value): return f"#define {macro} {value}" +LVGL_VERSION = "9.5.0" LV_CONF_FILENAME = "lv_conf.h" LV_CONF_H_FORMAT = """\ #pragma once @@ -110,7 +127,17 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()] + # Get all possible LV_ config defines based on the widgets used in the config, and the standard LVGL options + all_defines = set( + df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES) + ) + # Get the defines that are actually used based on the config + lv_defines = df.get_data(df.KEY_LV_DEFINES) + unused_defines = all_defines - set(lv_defines) + # Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled + definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [ + as_macro(m, "0") for m in unused_defines + ] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) @@ -133,7 +160,7 @@ def multi_conf_validate(configs: list[dict]): for item in ( CONF_LOG_LEVEL, CONF_COLOR_DEPTH, - df.CONF_BYTE_ORDER, + CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, ): if base_config[item] != config[item]: @@ -166,14 +193,7 @@ def final_validation(config_list): ) buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config: - LOGGER.warning("buffer_size: may need to be reduced without PSRAM") - for image_id in lv_images_used: - path = global_config.get_path_for_id(image_id)[:-1] - image_conf = global_config.get_config_for_path(path) - if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): - raise cv.Invalid( - "Using RGBA or RGB24 in image config not compatible with LVGL", path - ) + df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM") for w in focused_widgets: path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) @@ -205,39 +225,48 @@ def final_validation(config_list): async def to_code(configs): config_0 = configs[0] # Global configuration - cg.add_library("lvgl/lvgl", "8.4.0") + if CORE.is_esp32: + if get_esp32_variant() == VARIANT_ESP32P4: + add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64) + # disable use of PPA for fills until upstream bugs fixed + df.add_define("LV_USE_PPA", "0") + df.add_define("LV_DRAW_BUF_ALIGN", "64") + else: + df.add_define("LV_DRAW_BUF_ALIGN", "32") + add_idf_component(name="lvgl/lvgl", ref=LVGL_VERSION) + else: + df.add_define("LV_DRAW_BUF_ALIGN", "1") + cg.add_library("lvgl/lvgl", LVGL_VERSION) + df.add_define("LV_DRAW_BUF_STRIDE_ALIGN", "1") + df.add_define("LV_USE_DRAW_SW", "1") + df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB") + df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB") + df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets - add_define("_LV_KCONFIG_PRESENT") + # cg.add_define("LV_KCONFIG_PRESENT") # Always enable - lots of things use it. - add_define("LV_DRAW_COMPLEX", "1") - add_define("LV_TICK_CUSTOM", "1") - add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') - add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())") - add_define("LV_MEM_CUSTOM", "1") - add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc") - add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free") - add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc") - add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') + df.add_define("LV_DRAW_SW_COMPLEX", "1") - add_define( + df.add_define( "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}", ) + df.add_define("LV_USE_LOG", "1") cg.add_define( "LVGL_LOG_LEVEL", cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) + df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: - add_define(f"LV_FONT_{font.upper()}") + df.add_define(f"LV_FONT_{font.upper()}") if config_0[CONF_COLOR_DEPTH] == 16: - add_define( + df.add_define( "LV_COLOR_16_SWAP", - "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", + "1" if config_0[CONF_BYTE_ORDER] == "big_endian" else "0", ) - add_define( + df.add_define( "LV_COLOR_CHROMA_KEY", await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]), ) @@ -248,7 +277,7 @@ async def to_code(configs): await cg.get_variable(font) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): - add_define( + df.add_define( "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" ) globfont_id = ID( @@ -262,9 +291,9 @@ async def to_code(configs): MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), static=False, ) - add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) + df.add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: - add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) + df.add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) cg.add(lvgl_static.esphome_lvgl_init()) default_group = get_default_group(config_0) @@ -293,8 +322,9 @@ async def to_code(configs): await cg.register_component(lv_component, config) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) - lv_scr_act = get_scr_act(lv_component) + lv_scr_act = get_screen_active(lv_component) async with LvContext(): + cg.add(lv_component.set_big_endian(config[CONF_BYTE_ORDER] == "big_endian")) await touchscreens_to_code(lv_component, config) await encoders_to_code(lv_component, config, default_group) await keypads_to_code(lv_component, config, default_group) @@ -304,9 +334,10 @@ async def to_code(configs): await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) await add_pages(lv_component, config) - await add_top_layer(lv_component, config) + await layers_to_code(lv_component, config) + await lvgl_update(lv_component, config) await msgboxes_to_code(lv_component, config) - await disp_update(lv_component.get_disp(), config) + # await disp_update(lv_component.get_disp(), config) # Set this directly since we are limited in how many methods can be added to the Widget class. Widget.widgets_completed = True async with LvContext(): @@ -336,15 +367,53 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - if {"transform_angle", "transform_zoom"} & styles_used: - add_define("LV_COLOR_SCREEN_TRANSP", "1") + lv_image_formats = df.get_color_formats().copy() + if { + "transform_rotation", + "transform_scale", + "transform_scale_x", + "transform_scale_y", + } & styles_used: + df.add_define("LV_COLOR_SCREEN_TRANSP", "1") + lv_image_formats.add("ARGB8888") + lv_image_formats.add( + "RGB565" + ) # Currently always need RGB565 for the display buffer for use in helpers.lv_uses: - add_define(f"LV_USE_{use.upper()}") + df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") + + for image_id in lv_images_used: + await cg.get_variable(image_id) + metadata = get_image_metadata(image_id.id) + image_type = IMAGE_TYPE[metadata.image_type] + transparent = metadata.transparency != CONF_OPAQUE + if transparent: + # Internal draw layer will use ARGB8888 + lv_image_formats.add("ARGB8888") + if image_type == ImageBinary: + lv_image_formats.add("I1") + if image_type == ImageGrayscale: + lv_image_formats.add("A8") + if image_type == ImageRGB565: + lv_image_formats.add("RGB565A8" if transparent else "RGB565") + if image_type == ImageRGB: + lv_image_formats.add("ARGB8888" if transparent else "RGB8888") + if df.is_defined("LV_GRADIENT_MAX_STOPS"): + lv_image_formats.add("RGB888") + for fmt in lv_image_formats: + df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") - cg.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"') + cg.add_build_flag(f'-DLV_CONF_PATH=\\"{LV_CONF_FILENAME}\\"') + + for prop in df.get_remapped_uses(): + df.LOGGER.warning( + "Property '%s' is deprecated, use '%s' instead", prop, STYLE_REMAP[prop] + ) + for warning in df.get_warnings(): + df.LOGGER.warning(warning) def display_schema(config): @@ -357,7 +426,9 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: - LOGGER.info("No pages or widgets configured, creating default hello_world page") + df.LOGGER.info( + "No pages or widgets configured, creating default hello_world page" + ) hello_world_path = Path(__file__).parent / HELLO_WORLD_FILE config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path)) return config @@ -395,8 +466,8 @@ LVGL_SCHEMA = cv.All( cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), - cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( - "big_endian", "little_endian" + cv.Optional(CONF_BYTE_ORDER, default="big_endian"): cv.one_of( + "big_endian", "little_endian", lower=True ), cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend( @@ -424,6 +495,7 @@ LVGL_SCHEMA = cv.All( cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), + cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec), cv.Optional( df.CONF_TRANSPARENCY_KEY, default=0x000400 ): lvalid.lv_color, diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index f9adca9c337..24579e5be80 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -10,18 +10,21 @@ from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr from .defines import ( - CONF_DISP_BG_COLOR, - CONF_DISP_BG_IMAGE, - CONF_DISP_BG_OPA, + CONF_BG_OPA, + CONF_BOTTOM_LAYER, CONF_EDITING, CONF_FREEZE, CONF_LVGL_ID, + CONF_MAIN, + CONF_OBJ, + CONF_SCROLLBAR, CONF_SHOW_SNOW, + CONF_TOP_LAYER, PARTS, - literal, - static_cast, + StaticCastExpression, + add_warning, ) -from .lv_validation import lv_bool, lv_color, lv_image, lv_milliseconds, opacity +from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( LVGL_COMP_ARG, UPDATE_EVENT, @@ -42,13 +45,13 @@ from .schemas import ( LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema, + part_schema, ) from .types import ( LV_STATE, LvglAction, LvglCondition, ObjUpdateAction, - lv_disp_t, lv_group_t, lv_obj_base_t, lv_obj_t, @@ -56,7 +59,9 @@ from .types import ( ) from .widgets import ( Widget, - get_scr_act, + WidgetType, + add_widgets, + get_screen_active, get_widgets, set_obj_properties, wait_for_widgets, @@ -67,6 +72,41 @@ focused_widgets = set() refreshed_widgets = set() +async def layers_to_code(lv_component, config): + if top_conf := config.get(CONF_TOP_LAYER): + top_layer = lv_expr.display_get_layer_top(lv_component.get_disp()) + with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: + top_w = Widget(top_layer_obj, layer_spec, top_conf) + await set_obj_properties(top_w, top_conf) + await add_widgets(top_w, top_conf) + if bottom_conf := config.get(CONF_BOTTOM_LAYER): + bottom_layer = lv_expr.display_get_layer_bottom(lv_component.get_disp()) + with LocalVariable("bottom_layer", lv_obj_t, bottom_layer) as bottom_layer_obj: + bottom_w = Widget(bottom_layer_obj, layer_spec, bottom_conf) + await set_obj_properties(bottom_w, bottom_conf) + await add_widgets(bottom_w, bottom_conf) + + +async def lvgl_update(lv_component, config): + bottom = {k.removeprefix("disp_"): v for k, v in config.items() if k in DISP_PROPS} + if not bottom: + return + plural = len(bottom) != 1 + add_warning( + "The propert" + + ("ies " if plural else "y ") + + "'" + + "','".join(k for k in config if k in DISP_PROPS) + + "'" + + (" are " if plural else " is ") + + "deprecated, use 'bottom_layer' instead." + ) + # Preserve default opacity from 8.x + if CONF_BG_OPA not in bottom: + bottom[CONF_BG_OPA] = 1.0 + await layers_to_code(lv_component, {CONF_BOTTOM_LAYER: bottom}) + + async def action_to_code( widgets: list[Widget], action: Callable[[Widget], Any], @@ -151,25 +191,6 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): return var -async def disp_update(disp, config: dict): - if ( - CONF_DISP_BG_COLOR not in config - and CONF_DISP_BG_IMAGE not in config - and CONF_DISP_BG_OPA not in config - ): - return - with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: - if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: - lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) - if bg_image := config.get(CONF_DISP_BG_IMAGE): - if bg_image == "none": - lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr")) - else: - lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) - if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None: - lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa)) - - @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, @@ -187,7 +208,7 @@ async def disp_update(disp, config: dict): async def obj_invalidate_to_code(config, action_id, template_arg, args): if CONF_LVGL_ID in config: lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - widgets = [get_scr_act(lv_comp)] + widgets = [get_screen_active(lv_comp)] else: widgets = await get_widgets(config) @@ -197,20 +218,30 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) +layer_spec = WidgetType(CONF_OBJ, lv_obj_t, (CONF_MAIN, CONF_SCROLLBAR), is_mock=True) + +DISP_PROPS = {str(x) for x in DISP_BG_SCHEMA.schema} + + @automation.register_action( "lvgl.update", LvglAction, - DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( - cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) + part_schema(layer_spec.parts) + .extend(LVGL_SCHEMA) + .extend(DISP_BG_SCHEMA) + .extend( + { + cv.Optional(CONF_TOP_LAYER): part_schema(layer_spec.parts), + cv.Optional(CONF_BOTTOM_LAYER): part_schema(layer_spec.parts), + } ), synchronous=True, ) async def lvgl_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config, CONF_LVGL_ID) w = widgets[0] - disp = literal(f"{w.obj}->get_disp()") async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: - await disp_update(disp, config) + await lvgl_update(w.var, config) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) await cg.register_parented(var, w.var) return var @@ -336,7 +367,7 @@ async def widget_focus(config, action_id, template_arg, args): widget = await get_widgets(config) if widget: widget = widget[0] - group = static_cast( + group = StaticCastExpression( lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj) ) elif group := config.get(CONF_GROUP): diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 91077a1ff49..d6d7a5e161d 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -10,8 +10,12 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS from esphome.core import CORE, ID, Lambda -from esphome.cpp_generator import LambdaExpression, MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_generator import ( + CallExpression, + LambdaExpression, + MockObj, + MockObjClass, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType @@ -21,8 +25,11 @@ LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") DOMAIN = "lvgl" +KEY_COLOR_FORMATS = "color_formats" KEY_LV_DEFINES = "lv_defines" +KEY_REMAPPED_USES = "remapped_uses" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_WARNINGS = "warnings" def get_data(key, default=None): @@ -33,10 +40,37 @@ def get_data(key, default=None): :return: """ return CORE.data.setdefault(DOMAIN, {}).setdefault( - key, default if default is not None else {} + key, {} if default is None else default ) +def get_warnings(): + return get_data(KEY_WARNINGS, set()) + + +def get_remapped_uses(): + return get_data(KEY_REMAPPED_USES, set()) + + +def get_color_formats(): + return get_data(KEY_COLOR_FORMATS, set()) + + +def add_warning(msg: str): + get_warnings().add(msg) + + +class StaticCastExpression(Expression): + __slots__ = ("type", "exp") + + def __init__(self, type: Any, exp: SafeExpType): + self.type = str(type) + self.exp = cg.safe_exp(exp) + + def __str__(self): + return f"static_cast<{self.type}>({self.exp})" + + def add_define(macro, value="1"): lv_defines = get_data(KEY_LV_DEFINES) value = str(value) @@ -47,27 +81,43 @@ def add_define(macro, value="1"): lv_defines[macro] = value +def is_defined(macro): + return macro in get_data(KEY_LV_DEFINES) + + def literal(arg) -> MockObj: if isinstance(arg, str): return MockObj(arg) return arg -def static_cast(type, value): - return literal(f"static_cast<{type}>({value})") +def addr(arg) -> MockObj: + return MockObj(f"&{arg}") def call_lambda(lamb: LambdaExpression): + """ + Given a lambda, either reduce to a simple expression or call it, possibly with parameters + from the surrounding context + :param lamb: + :return: + """ expr = lamb.content.strip() if expr.startswith("return") and expr.endswith(";"): - return expr[6:-1].strip() - # If lambda has parameters, call it with those parameter names + # Convert a lambda returning a simple expression to just that expression + expr = cg.RawExpression(expr[6:-1].strip()) + # Don't cast if the return type is a class + if isinstance(lamb.return_type, MockObjClass): + return expr + return StaticCastExpression(lamb.return_type, expr) + # If lambda has parameters, call it with their names # Parameter names come from hardcoded component code (like "x", "it", "event") # not from user input, so they're safe to use directly if lamb.parameters and lamb.parameters.parameters: - param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) - return f"{lamb}({param_names})" - return f"{lamb}()" + return CallExpression( + lamb, *[MockObj(x.id) for x in lamb.parameters.parameters] + ) + return CallExpression(lamb) class LValidator: @@ -76,7 +126,7 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, retmapper=None, requires=None): + def __init__(self, validator, rtype: MockObj, retmapper=None, requires=None): self.validator = validator self.rtype = rtype self.retmapper = retmapper @@ -99,10 +149,9 @@ class LValidator: from .lvcode import get_lambda_context_args args = args or get_lambda_context_args() - return cg.RawExpression( - call_lambda( - await cg.process_lambda(value, args, return_type=self.rtype) - ) + + return call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) ) if self.retmapper is not None: return self.retmapper(value) @@ -112,6 +161,8 @@ class LValidator: value = [ await cg.get_variable(x) if isinstance(x, ID) else x for x in value ] + if self.rtype is cg.int_: + value = int(value) return cg.safe_exp(value) @@ -122,10 +173,11 @@ class LvConstant(LValidator): The property `one_of` has the single case validator, and `several_of` allows a list of constants. """ - def __init__(self, prefix: str, *choices): + def __init__(self, prefix: str, *choices, typename=None): self.prefix = prefix - self.choices = choices - prefixed_choices = [prefix + v for v in choices] + self.choices = tuple(x.upper() for x in choices) + self.typename = typename or prefix.lower() + "t" + prefixed_choices = [prefix + v.upper() for v in choices] prefixed_validator = cv.one_of(*prefixed_choices, upper=True) @schema_extractor("one_of") @@ -136,24 +188,30 @@ class LvConstant(LValidator): return prefixed_validator(value) return self.prefix + cv.one_of(*choices, upper=True)(value) - super().__init__(validator, rtype=uint32) + super().__init__(validator, rtype=cg.uint32) self.retmapper = self.mapper - self.one_of = LValidator(validator, uint32, retmapper=self.mapper) + self.one_of = LValidator(validator, cg.uint32, retmapper=self.mapper) self.several_of = LValidator( - cv.ensure_list(self.one_of), uint32, retmapper=self.mapper + cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper ) def mapper(self, value): if not isinstance(value, list): value = [value] - return literal( - "|".join( - [ - str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) - for v in value - ] - ).upper() - ) + value = [ + ( + str(v).upper() + if str(v).startswith(self.prefix) + else self.prefix + str(v).upper() + ) + for v in value + ] + if len(value) == 1: + return literal(value[0]) + value = literal("|".join(value)) + if self.typename is None: + return value + return StaticCastExpression(self.typename, value) def extend(self, *choices): """ @@ -161,7 +219,14 @@ class LvConstant(LValidator): :param choices: The extra choices :return: A new LVConstant instance """ - return LvConstant(self.prefix, *(self.choices + choices)) + return LvConstant( + self.prefix, *(self.choices + choices), typename=self.typename + ) + + def __getattr__(self, item): + if item.upper() not in self.choices: + raise AttributeError(f"{item} not one of {self.choices}") + return self.mapper(item) # Parts @@ -277,11 +342,13 @@ PARTS = ( CONF_KNOB, CONF_SELECTED, CONF_ITEMS, - CONF_TICKS, + # CONF_TICKS, CONF_CURSOR, CONF_TEXTAREA_PLACEHOLDER, ) +LV_PART = LvConstant("LV_PART_", *(p.upper() for p in PARTS)) + KEYBOARD_MODES = LvConstant( "LV_KEYBOARD_MODE_", "TEXT_LOWER", @@ -359,6 +426,7 @@ OBJ_FLAGS = ( "overflow_visible", "layout_1", "layout_2", + "send_draw_task_events", "widget_1", "widget_2", "user_1", @@ -366,12 +434,14 @@ OBJ_FLAGS = ( "user_3", "user_4", ) +LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS) ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") +SLIDER_MODES = LvConstant("LV_SLIDER_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") BUTTONMATRIX_CTRLS = LvConstant( - "LV_BTNMATRIX_CTRL_", + "LV_BUTTONMATRIX_CTRL_", "HIDDEN", "NO_REPEAT", "DISABLED", @@ -434,12 +504,16 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" +CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" +CONF_ANIMATIONS = "animations" CONF_ANTIALIAS = "antialias" CONF_ARC_LENGTH = "arc_length" CONF_AUTO_START = "auto_start" CONF_BACKGROUND_STYLE = "background_style" +CONF_BG_OPA = "bg_opa" +CONF_BOTTOM_LAYER = "bottom_layer" CONF_BUTTON_STYLE = "button_style" CONF_DECIMAL_PLACES = "decimal_places" CONF_COLUMN = "column" @@ -449,9 +523,11 @@ CONF_DISP_BG_IMAGE = "disp_bg_image" CONF_DISP_BG_OPA = "disp_bg_opa" CONF_BODY = "body" CONF_BUTTONS = "buttons" -CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" +CONF_COLOR_DEPTH = "color_depth" +CONF_COLOR_END = "color_end" +CONF_COLOR_START = "color_start" CONF_CONTAINER = "container" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" @@ -483,8 +559,10 @@ CONF_GRID_COLUMN_ALIGN = "grid_column_align" CONF_GRID_COLUMNS = "grid_columns" CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" +CONF_HEADER_BUTTONS = "header_buttons" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_INDICATORS = "indicators" CONF_INITIAL_FOCUS = "initial_focus" CONF_SELECTED_DIGIT = "selected_digit" CONF_KEY_CODE = "key_code" @@ -496,6 +574,7 @@ CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" +CONF_MAJOR_TICKS_STYLE = "major_ticks_style" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" @@ -517,12 +596,15 @@ CONF_PIVOT_Y = "pivot_y" CONF_PLACEHOLDER_TEXT = "placeholder_text" CONF_POINTS = "points" CONF_PREVIOUS = "previous" +CONF_RADIUS = "radius" CONF_REPEAT_COUNT = "repeat_count" CONF_RECOLOR = "recolor" CONF_RESUME_ON_INPUT = "resume_on_input" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROWS = "rows" +CONF_SCALE = "scale" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLL_DIR = "scroll_dir" @@ -536,6 +618,7 @@ CONF_SRC = "src" CONF_START_ANGLE = "start_angle" CONF_START_VALUE = "start_value" CONF_STATES = "states" +CONF_STRIDE = "stride" CONF_STYLE = "style" CONF_STYLES = "styles" CONF_STYLE_DEFINITIONS = "style_definitions" @@ -544,6 +627,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_TICK_STYLE = "tick_style" CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" @@ -551,6 +635,8 @@ CONF_TILES = "tiles" CONF_TITLE = "title" CONF_TOP_LAYER = "top_layer" CONF_TOUCHSCREENS = "touchscreens" +CONF_TRANSFORM_ROTATION = "transform_rotation" +CONF_TRANSFORM_SCALE = "transform_scale" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_UPDATE_ON_RELEASE = "update_on_release" @@ -578,6 +664,16 @@ LV_KEYS = LvConstant( "END", ) +LV_SCALE_MODE = LvConstant( + "LV_SCALE_MODE_", + "HORIZONTAL_TOP", + "HORIZONTAL_BOTTOM", + "VERTICAL_LEFT", + "VERTICAL_RIGHT", + "ROUND_INNER", + "ROUND_OUTER", +) + DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" @@ -590,3 +686,29 @@ def join_enums(enums, prefix=""): if prefix: return literal("|".join(f"{prefix}{e.upper()}" for e in enums)) return literal("|".join(f"(int){e.upper()}" for e in enums)) + + +# fmt: off +LV_COLOR_FORMATS = ( + "RGB565", "SWAPPED", "RGB565A8", "RGB888", "XRGB8888", "ARGB8888", "PREMULTIPLIED", "L8", "AL88", "A8", "I1", +) + +LV_DEFINES = ( + "LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT", + "LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D", + "LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP", + "LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE", + "LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL", + "LV_USE_CALENDAR", "LV_USE_CALENDAR_HEADER_ARROW", "LV_USE_CALENDAR_HEADER_DROPDOWN", "LV_USE_CHART", + "LV_USE_LIST", "LV_USE_MENU", "LV_USE_MSGBOX", "LV_USE_SCALE", + "LV_USE_TABLE", "LV_USE_SPAN", "LV_USE_WIN", "LV_USE_THEME_DEFAULT", + "LV_THEME_DEFAULT_GROW", "LV_USE_THEME_SIMPLE", "LV_USE_THEME_MONO", "LV_USE_FLEX", + "LV_USE_GRID", "LV_USE_PROFILER_BUILTIN", "LV_PROFILER_BUILTIN_DEFAULT_ENABLE", "LV_PROFILER_LAYOUT", + "LV_PROFILER_REFR", "LV_PROFILER_DRAW", "LV_PROFILER_INDEV", "LV_PROFILER_DECODER", + "LV_PROFILER_FONT", "LV_PROFILER_FS", "LV_PROFILER_TIMER", "LV_PROFILER_CACHE", + "LV_PROFILER_EVENT", "LV_USE_OBSERVER", "LV_IME_PINYIN_USE_DEFAULT_DICT", "LV_IME_PINYIN_USE_K9_MODE", + "LV_FILE_EXPLORER_QUICK_ACCESS", "LV_TEST_SCREENSHOT_CREATE_REFERENCE_IMAGE", "LV_LINUX_FBDEV_MMAP", + "LV_USE_NUTTX_MOUSE_MOVE_STEP", "LV_USE_GENERIC_MIPI", "LV_BUILD_EXAMPLES", "LV_BUILD_DEMOS", + "LV_WAYLAND_USE_EGL", "LV_WAYLAND_USE_G2D", "LV_WAYLAND_USE_SHM", "LV_LINUX_DRM_USE_EGL", + "LV_USE_LZ4", "LV_USE_THORVG", "LV_SDL_USE_EGL", "LV_USE_EGL", "LV_LABEL_LONG_TXT_HINT", "LV_LABEL_TEXT_SELECTION", +) + tuple(f"LV_DRAW_SW_SUPPORT_{f}" for f in LV_COLOR_FORMATS) diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index 259c3440306..bafda8382ee 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -71,7 +71,7 @@ async def encoders_to_code(var, config, default_group): lv_assign(group, lv_expr.group_create()) else: group = default_group - lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + lv.indev_set_group(listener.get_drv(), group) async def initial_focus_to_code(config): diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index bc89470d47f..f3ded6a518a 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -7,12 +7,13 @@ from esphome.const import ( CONF_ID, CONF_POSITION, ) +from esphome.core import ID from esphome.cpp_generator import MockObj -from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define -from .lv_validation import lv_color, lv_fraction -from .lvcode import lv_assign -from .types import lv_gradient_t +from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning +from .lv_validation import lv_color, lv_percentage, opacity +from .lvcode import lv +from .types import lv_color_t, lv_gradient_t, lv_opa_t CONF_STOPS = "stops" @@ -27,14 +28,17 @@ GRADIENT_SCHEMA = cv.ensure_list( cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), - cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of, + cv.Required(CONF_DIRECTION): cv.one_of( + "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True + ), cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, cv.Required(CONF_STOPS): cv.All( [ cv.Schema( { cv.Required(CONF_COLOR): lv_color, - cv.Required(CONF_POSITION): lv_fraction, + cv.Optional(CONF_OPA, default=1.0): opacity, + cv.Required(CONF_POSITION): lv_percentage, } ) ], @@ -47,15 +51,31 @@ GRADIENT_SCHEMA = cv.ensure_list( async def gradients_to_code(config): max_stops = 2 + if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())): + add_warning( + "The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored" + ) for gradient in config.get(CONF_GRADIENTS, ()): var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") - max_stops = max(max_stops, len(gradient[CONF_STOPS])) - lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION])) - lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER])) - lv_assign(var.stops_count, len(gradient[CONF_STOPS])) - for index, stop in enumerate(gradient[CONF_STOPS]): - lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR])) - lv_assign( - var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION]) - ) + idbase = gradient[CONF_ID].id + stops = gradient[CONF_STOPS] + max_stops = max(max_stops, len(stops)) + if gradient[CONF_DIRECTION].startswith("VER"): + lv.grad_vertical_init(var) + else: + lv.grad_horizontal_init(var) + stop_colors = cg.static_const_array( + ID(idbase + "_colors_", type=lv_color_t), + [await lv_color.process(x[CONF_COLOR]) for x in stops], + ) + stop_opacities = cg.static_const_array( + ID(idbase + "_opacities_", type=lv_opa_t), + [await opacity.process(x[CONF_OPA]) for x in stops], + ) + stop_positions = cg.static_const_array( + ID(idbase + "_positions_", type=cg.uint8), + [await lv_percentage.process(x[CONF_POSITION]) for x in stops], + ) + lv.grad_init_stops(var, stop_colors, stop_opacities, stop_positions, len(stops)) + add_define("LV_GRADIENT_MAX_STOPS", max_stops) diff --git a/esphome/components/lvgl/keypads.py b/esphome/components/lvgl/keypads.py index 5e2953d57f7..7d8b3dd1282 100644 --- a/esphome/components/lvgl/keypads.py +++ b/esphome/components/lvgl/keypads.py @@ -67,7 +67,7 @@ async def keypads_to_code(var, config, default_group): lv_assign(group, lv_expr.group_create()) else: group = default_group - lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + lv.indev_set_group(listener.get_drv(), group) async def initial_focus_to_code(config): diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index b27a0b54a2c..46026852af0 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -88,8 +88,8 @@ grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_spa GRID_CELL_SCHEMA = { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, } @@ -198,12 +198,8 @@ class GridLayout(Layout): { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional( - CONF_GRID_CELL_ROW_SPAN, default=1 - ): cv.positive_int, - cv.Optional( - CONF_GRID_CELL_COLUMN_SPAN, default=1 - ): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional( CONF_GRID_CELL_X_ALIGN, default="center" ): grid_alignments, @@ -231,8 +227,8 @@ class GridLayout(Layout): { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, }, @@ -299,11 +295,13 @@ class GridLayout(Layout): w[CONF_GRID_CELL_ROW_POS] = row w[CONF_GRID_CELL_COLUMN_POS] = column - for i in range(w[CONF_GRID_CELL_ROW_SPAN]): - for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]): + row_span = w.get(CONF_GRID_CELL_ROW_SPAN, 1) + column_span = w.get(CONF_GRID_CELL_COLUMN_SPAN, 1) + for i in range(row_span): + for j in range(column_span): if row + i >= rows or column + j >= columns: raise cv.Invalid( - f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} " + f"Cell at {row}/{column} span {row_span}x{column_span} " f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 569f9a03c0b..7309df97638 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -38,7 +38,7 @@ class LVLight : public light::LightOutput { void set_value_(lv_color_t value) { lv_led_set_color(this->obj_, value); lv_led_on(this->obj_); - lv_event_send(this->obj_, lv_api_event, nullptr); + lv_obj_send_event(this->obj_, lv_api_event, nullptr); } lv_obj_t *obj_{}; optional<lv_color_t> initial_value_{}; diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 3c1838219c5..503730098e1 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -30,6 +30,7 @@ from .defines import ( LV_FONTS, LValidator, LvConstant, + StaticCastExpression, call_lambda, literal, ) @@ -40,22 +41,28 @@ from .helpers import ( lv_fonts_used, requires_component, ) -from .types import lv_gradient_t +from .types import lv_gradient_t, lv_opa_t -opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") +LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER") @schema_extractor("one_of") def opacity_validator(value): if value == SCHEMA_EXTRACT: - return opacity_consts.choices - value = cv.Any(cv.percentage, opacity_consts.one_of)(value) - if isinstance(value, float): - return int(value * 255) - return value + return LV_OPA.choices + value = cv.Any(cv.percentage, LV_OPA.one_of)(value) + if value == str(LV_OPA.COVER): + value = 1.0 + if value == str(LV_OPA.TRANSP): + value = 0.0 + return cv.float_range(0.0, 1.0)(value) -opacity = LValidator(opacity_validator, uint32, retmapper=literal) +opacity = LValidator( + opacity_validator, + lv_opa_t, + retmapper=lambda opa: StaticCastExpression(cg.uint8, opa * 255.0), +) COLOR_NAMES = { "aliceblue": 0xF0F8FF, @@ -244,7 +251,17 @@ def option_string(value): return value -lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) +class LvColor(LValidator): + def __init__(self): + super().__init__(color, ty.lv_color_t, retmapper=color_retmapper) + + def __getattr__(self, item): + if item in COLOR_NAMES: + return color_retmapper(COLOR_NAMES[item]) + raise AttributeError(item) + + +lv_color = LvColor() def pixels_or_percent_validator(value): @@ -252,16 +269,17 @@ def pixels_or_percent_validator(value): if value == SCHEMA_EXTRACT: return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): - value = cv.int_(value[:-2]) + return cv.int_(value[:-2]) if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): - return value - value = cv.Any(cv.int_, cv.percentage)(value) - if isinstance(value, int): - return value - return f"lv_pct({int(value * 100)})" + return int(value[6:-1]) / 100.0 + return cv.Any(cv.int_, cv.possibly_negative_percentage)(value) -pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) +pixels_or_percent = LValidator( + pixels_or_percent_validator, + uint32, + retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"), +) def pixels_validator(value): @@ -282,15 +300,11 @@ def padding_validator(value): padding = LValidator(padding_validator, int32, retmapper=literal) -def zoom_validator(value): +def scale_validator(value): return cv.float_range(0.1, 10.0)(value) -def zoom_retmapper(value): - return int(value * 256) - - -zoom = LValidator(zoom_validator, uint32, retmapper=zoom_retmapper) +scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 256)) def angle(value): @@ -321,17 +335,23 @@ def size_validator(value): return pixels_or_percent_validator(value) -size = LValidator(size_validator, uint32, retmapper=literal) +size = LValidator( + size_validator, + uint32, + retmapper=lambda x: ( + literal(x) if isinstance(x, str) else pixels_or_percent.retmapper(x) + ), +) -radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") +LV_RADIUS = LvConstant("LV_RADIUS_", "CIRCLE") @schema_extractor("one_of") def fraction_validator(value): if value == SCHEMA_EXTRACT: - return radius_consts.choices - value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) + return LV_RADIUS.choices + value = cv.Any(size, cv.percentage, LV_RADIUS.one_of)(value) if isinstance(value, float): return int(value * 255) return value @@ -374,12 +394,6 @@ lv_image_list = LValidator( lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) -def lv_pct(value: int | float): - if isinstance(value, float): - value = int(value * 100) - return literal(f"lv_pct({value})") - - def lvms_validator_(value): if value == "never": value = "2147483647ms" @@ -424,30 +438,28 @@ class TextValidator(LValidator): if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): - time_format = cpp_string_escape(time_format) - return cg.RawExpression( + source = MockObj( call_lambda( await cg.process_lambda(source, args, return_type=ESPTime) ) - + f".strftime({time_format}).c_str()" ) # must be an ID - source = await cg.get_variable(source) - return source.now().strftime(time_format).c_str() + else: + source = (await cg.get_variable(source)).now() + return source.strftime(time_format).c_str() if isinstance(value, Lambda): value = call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) ) + textvalue = str(value) # Was the lambda call reduced to a string? - if value.endswith("c_str()") or ( - value.endswith('"') and value.startswith('"') + if textvalue.endswith("c_str()") or ( + textvalue.endswith('"') and textvalue.startswith('"') ): - pass - else: - # Either a std::string or a lambda call returning that. We need const char* - value = f"({value}).c_str()" - return cg.RawExpression(value) + return value + # Either a std::string or a lambda call returning that. We need const char* + return MockObj(f"({value}).c_str()") return await super().process(value, args) @@ -455,21 +467,24 @@ lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_) lv_int = LValidator(cv.int_, cg.int_) lv_positive_int = LValidator(cv.positive_int, cg.int_) -lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) -def gradient_mapper(value): - return MockObj(value) +def _percentage_validator(value): + value = cv.Any(cv.percentage, cv.float_range(0.0, 1.0), cv.int_range(0, 255))(value) + if isinstance(value, int): + return value / 255.0 + return value -def gradient_validator(value): - return cv.use_id(lv_gradient_t)(value) +lv_percentage = LValidator( + _percentage_validator, cg.float_, retmapper=lambda x: int(x * 255) +) lv_gradient = LValidator( - validator=gradient_validator, + validator=cv.use_id(lv_gradient_t), rtype=lv_gradient_t, - retmapper=gradient_mapper, + retmapper=MockObj, ) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index b79d1e88dd7..146b261f260 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -168,6 +168,9 @@ class LambdaContext(CodeContext): def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: return self.parameters + def get_parameter(self, index: int): + return literal(self.parameters[index][1]) + async def __aenter__(self): await super().__aenter__() add_line_marks(self.where) @@ -250,10 +253,14 @@ class MockLv: A mock object that can be used to generate LVGL calls. """ + # Mapping for LVGL 9 + ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"} + def __init__(self, base): self.base = base def __getattr__(self, attr: str) -> "MockLv": + attr = MockLv.ATTR_MAP.get(attr, attr) return MockLv(f"{self.base}{attr}") def append(self, expression): @@ -307,6 +314,7 @@ class ReturnStatement(ExpressionStatement): class LvExpr(MockLv): def __getattr__(self, attr: str) -> "MockLv": + attr = MockLv.ATTR_MAP.get(attr, attr) return LvExpr(f"{self.base}{attr}") def append(self, expression): diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 5400054bb13..b1618f77c47 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -2,16 +2,21 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "lvgl_hal.h" #include "lvgl_esphome.h" +#include "core/lv_global.h" +#include "core/lv_obj_class_private.h" + #include <numeric> -namespace esphome { -namespace lvgl { +static void *lv_alloc_draw_buf(size_t size, bool internal); +static void *draw_buf_alloc_cb(size_t size, lv_color_format_t color_format) { return lv_alloc_draw_buf(size, false); }; + +namespace esphome::lvgl { static const char *const TAG = "lvgl"; -static const size_t MIN_BUFFER_FRAC = 8; +static const size_t MIN_BUFFER_FRAC = 8; // buffer must be at least 1/8 of the display size +static const size_t MIN_BUFFER_SIZE = 2048; // Sensible minimum buffer size static const char *const EVENT_NAMES[] = { "NONE", @@ -61,7 +66,14 @@ static const char *const EVENT_NAMES[] = { "GET_SELF_SIZE", }; -std::string lv_event_code_name_for(uint8_t event_code) { +static const unsigned LOG_LEVEL_MAP[] = { + ESPHOME_LOG_LEVEL_DEBUG, ESPHOME_LOG_LEVEL_INFO, ESPHOME_LOG_LEVEL_WARN, + ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_NONE, + +}; + +std::string lv_event_code_name_for(lv_event_t *event) { + auto event_code = lv_event_get_code(event); if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) { return EVENT_NAMES[event_code]; } @@ -71,11 +83,12 @@ std::string lv_event_code_name_for(uint8_t event_code) { return buf; } -static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { +static void rounder_cb(lv_event_t *event) { + auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event)); + auto *area = static_cast<lv_area_t *>(lv_event_get_param(event)); // cater for display driver chips with special requirements for bounds of partial // draw areas. Extend the draw area to satisfy: // * Coordinates must be a multiple of draw_rounding - auto *comp = static_cast<LvglComponent *>(disp_drv->user_data); auto draw_rounding = comp->draw_rounding; // round down the start coordinates area->x1 = area->x1 / draw_rounding * draw_rounding; @@ -85,15 +98,14 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; } -void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) { - ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time); - auto *comp = static_cast<LvglComponent *>(disp_drv->user_data); +void LvglComponent::render_end_cb(lv_event_t *event) { + auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event)); comp->draw_end_(); } -void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) { +void LvglComponent::render_start_cb(lv_event_t *event) { ESP_LOGVV(TAG, "Draw start"); - auto *comp = static_cast<LvglComponent *>(disp_drv->user_data); + auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event)); comp->draw_start_(); } @@ -106,16 +118,15 @@ void LvglComponent::dump_config() { " Buffer size: %zu%%\n" " Rotation: %d\n" " Draw rounding: %d", - this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation, - (int) this->draw_rounding); + this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); } void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; this->show_snow_ = show_snow; - if (!paused && lv_scr_act() != nullptr) { - lv_disp_trig_activity(this->disp_); // resets the inactivity time - lv_obj_invalidate(lv_scr_act()); + if (!paused && lv_screen_active() != nullptr) { + lv_display_trigger_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_screen_active()); } if (paused && this->pause_callback_ != nullptr) this->pause_callback_->trigger(); @@ -125,6 +136,14 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { void LvglComponent::esphome_lvgl_init() { lv_init(); + // override draw buf alloc to ensure proper alignment for PPA + LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_free_cb = lv_free_core; + LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_free_cb = lv_free_core; + LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core; + lv_tick_set_cb([] { return millis(); }); lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id()); lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id()); } @@ -149,7 +168,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); - lv_disp_set_default(this->disp_); + lv_display_set_default(this->disp_); page->setup(this->pages_.size() - 1); } @@ -187,13 +206,13 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } -void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { +void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - lv_color_t *dst = this->rotate_buf_; + lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_); switch (this->rotation) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { @@ -202,7 +221,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } } y1 = x1; - x1 = this->disp_drv_.ver_res - area->y1 - height; + x1 = this->height_ - area->y1 - height; height = width; width = height_rounded; break; @@ -213,8 +232,8 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { dst[y * width + x] = *ptr++; } } - x1 = this->disp_drv_.hor_res - x1 - width; - y1 = this->disp_drv_.ver_res - y1 - height; + x1 = this->width_ - x1 - width; + y1 = this->height_ - y1 - height; break; case display::DISPLAY_ROTATION_270_DEGREES: @@ -224,7 +243,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } } x1 = y1; - y1 = this->disp_drv_.hor_res - area->x1 - width; + y1 = this->width_ - area->x1 - width; height = width; width = height_rounded; break; @@ -234,20 +253,19 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { break; } for (auto *display : this->displays_) { - ESP_LOGV(TAG, "draw buffer x1=%d, y1=%d, width=%d, height=%d", x1, y1, width, height); display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, - LV_COLOR_16_SWAP); + this->big_endian_); } } -void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { +void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) { if (!this->is_paused()) { auto now = millis(); - this->draw_buffer_(area, color_p); - ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p)); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), + lv_area_get_height(area), (int) (millis() - now)); } - lv_disp_flush_ready(disp_drv); + lv_display_flush_ready(disp_drv); } IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) { @@ -264,14 +282,14 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeo #ifdef USE_LVGL_TOUCHSCREEN LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { this->set_parent(parent); - lv_indev_drv_init(&this->drv_); - this->drv_.disp = parent->get_disp(); - this->drv_.long_press_repeat_time = long_press_repeat_time; - this->drv_.long_press_time = long_press_time; - this->drv_.type = LV_INDEV_TYPE_POINTER; - this->drv_.user_data = this; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast<LVTouchListener *>(d->user_data); + this->drv_ = lv_indev_create(); + lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER); + lv_indev_set_disp(this->drv_, parent->get_disp()); + lv_indev_set_long_press_time(this->drv_, long_press_time); + // long press repeat time TBD + lv_indev_set_user_data(this->drv_, this); + lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) { + auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d)); if (l->touch_pressed_) { data->point.x = l->touch_point_.x; data->point.y = l->touch_point_.y; @@ -279,7 +297,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r } else { data->state = LV_INDEV_STATE_RELEASED; } - }; + }); } void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { @@ -289,21 +307,78 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { } #endif // USE_LVGL_TOUCHSCREEN +#ifdef USE_LVGL_METER + +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) { + auto *scale = lv_obj_get_parent(obj); + auto min_value = lv_scale_get_range_min_value(scale); + return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) + + lv_scale_get_rotation((scale))) % + 360; +} + +void IndicatorLine::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_line_set_points(lv_obj, this->points_, 2); + lv_obj_add_event_cb( + lv_obj_get_parent(obj), + [](lv_event_t *e) { + auto *indicator = static_cast<IndicatorLine *>(lv_event_get_user_data(e)); + indicator->update_length_(); + ESP_LOGV(TAG, "Updated length, value = %d", indicator->angle_); + }, + LV_EVENT_SIZE_CHANGED, this); +} + +void IndicatorLine::set_value(int value) { + auto angle = lv_get_needle_angle_for_value(this->obj, value); + if (angle != this->angle_) { + this->angle_ = angle; + this->update_length_(); + } +} + +void IndicatorLine::update_length_() { + uint32_t actual_needle_length; + auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN); + auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN); + if (LV_COORD_IS_PCT(radial_offset)) { + radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100; + } + if (LV_COORD_IS_PCT(length)) { + actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100; + } else if (length < 0) { + actual_needle_length = radius + length; + } else { + actual_needle_length = length; + } + auto x = lv_trigo_cos(this->angle_) / 32768.0f; + auto y = lv_trigo_sin(this->angle_) / 32768.0f; + this->points_[0].x = radius + radial_offset * x; + this->points_[0].y = radius + radial_offset * y; + this->points_[1].x = x * actual_needle_length + radius; + this->points_[1].y = y * actual_needle_length + radius; + lv_obj_refresh_self_size(this->obj); + lv_obj_invalidate(this->obj); +} +#endif + #ifdef USE_LVGL_KEY_LISTENER -LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { - lv_indev_drv_init(&this->drv_); - this->drv_.type = type; - this->drv_.user_data = this; - this->drv_.long_press_time = lpt; - this->drv_.long_press_repeat_time = lprt; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast<LVEncoderListener *>(d->user_data); +LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) { + this->drv_ = lv_indev_create(); + lv_indev_set_type(this->drv_, type); + lv_indev_set_long_press_time(this->drv_, long_press_time); + lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time); + lv_indev_set_user_data(this->drv_, this); + lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) { + auto *l = static_cast<LVEncoderListener *>(lv_indev_get_user_data(d)); data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; data->key = l->key_; data->enc_diff = (int16_t) (l->count_ - l->last_count_); l->last_count_ = l->count_; data->continue_reading = false; - }; + }); } #endif // USE_LVGL_KEY_LISTENER @@ -325,7 +400,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a auto index = std::find(this->options_.begin(), this->options_.end(), text); if (index != this->options_.end()) { this->set_selected_index(index - this->options_.begin(), anim); - lv_event_send(this->obj, lv_api_event, nullptr); + lv_obj_send_event(this->obj, lv_api_event, nullptr); } } @@ -335,7 +410,7 @@ void LvSelectable::set_options(std::vector<std::string> options) { index = options.size() - 1; this->options_ = std::move(options); this->set_option_string(join_string(this->options_).c_str()); - lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr); + lv_obj_send_event(this->obj, LV_EVENT_REFRESH, nullptr); this->set_selected_index(index, LV_ANIM_OFF); } #endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER @@ -346,17 +421,17 @@ void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { lv_obj_add_event_cb( lv_obj, [](lv_event_t *event) { - auto *self = static_cast<LvButtonMatrixType *>(event->user_data); + auto *self = static_cast<LvButtonMatrixType *>(lv_event_get_user_data(event)); if (self->key_callback_.size() == 0) return; - auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); - if (key_idx == LV_BTNMATRIX_BTN_NONE) + auto key_idx = lv_buttonmatrix_get_selected_button(self->obj); + if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE) return; if (self->key_map_.count(key_idx) != 0) { self->send_key_(self->key_map_[key_idx]); return; } - const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); + const auto *str = lv_buttonmatrix_get_button_text(self->obj, key_idx); auto len = strlen(str); while (len--) self->send_key_(*str++); @@ -376,14 +451,14 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { lv_obj_add_event_cb( lv_obj, [](lv_event_t *event) { - auto *self = static_cast<LvKeyboardType *>(event->user_data); + auto *self = static_cast<LvKeyboardType *>(lv_event_get_user_data(event)); if (self->key_callback_.size() == 0) return; - auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); - if (key_idx == LV_BTNMATRIX_BTN_NONE) + auto key_idx = lv_buttonmatrix_get_selected_button(self->obj); + if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE) return; - const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); + const char *txt = lv_buttonmatrix_get_button_text(self->obj, key_idx); if (txt == nullptr) return; for (const auto *kb_special_key : KB_SPECIAL_KEYS) { @@ -419,33 +494,29 @@ bool LvglComponent::is_paused() const { } void LvglComponent::write_random_() { - int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; + int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) iterations = 1; while (iterations-- != 0) { - auto col = random_uint32() % this->disp_drv_.hor_res; + int32_t col = random_uint32() % this->width_; col = col / this->draw_rounding * this->draw_rounding; - auto row = random_uint32() % this->disp_drv_.ver_res; + int32_t row = random_uint32() % this->height_; row = row / this->draw_rounding * this->draw_rounding; - auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; - // clamp size so the square fits within the draw buffer - if ((size + 1) * (size + 1) > this->draw_buf_.size) - size = static_cast<decltype(size)>(sqrtf(this->draw_buf_.size)) - 1; - lv_area_t area; - area.x1 = col; - area.y1 = row; - area.x2 = col + size; - area.y2 = row + size; - if (area.x2 >= this->disp_drv_.hor_res) - area.x2 = this->disp_drv_.hor_res - 1; - if (area.y2 >= this->disp_drv_.ver_res) - area.y2 = this->disp_drv_.ver_res - 1; + // size will be between 8 and 32, and a multiple of draw_rounding + int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding; + lv_area_t area{col, row, col + size - 1, row + size - 1}; + // clip to display bounds just in case + if (area.x2 >= this->width_) + area.x2 = this->width_ - 1; + if (area.y2 >= this->height_) + area.y2 = this->height_ - 1; + // line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2; for (size_t i = 0; i != line_len; i++) { - ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + ((uint32_t *) (this->draw_buf_))[i] = random_uint32(); } - this->draw_buffer_(&area, (lv_color_t *) this->draw_buf_.buf1); + this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_); } } @@ -477,38 +548,32 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf full_refresh_(full_refresh), resume_on_input_(resume_on_input), update_when_display_idle_(update_when_display_idle) { - lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); - lv_disp_drv_init(&this->disp_drv_); - this->disp_drv_.draw_buf = &this->draw_buf_; - this->disp_drv_.user_data = this; - this->disp_drv_.full_refresh = this->full_refresh_; - this->disp_drv_.flush_cb = static_flush_cb; - this->disp_drv_.rounder_cb = rounder_cb; - this->disp_ = lv_disp_drv_register(&this->disp_drv_); + this->disp_ = lv_display_create(240, 240); } void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; // cater for displays with dimensions that don't divide by the required rounding + this->width_ = display->get_width(); + this->height_ = display->get_height(); auto width = (display->get_width() + rounding - 1) / rounding * rounding; auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; - size_t buffer_pixels = width * height / frac; - auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; + auto buf_bytes = clamp_at_least(width * height / frac * LV_COLOR_DEPTH / 8, MIN_BUFFER_SIZE); void *buffer = nullptr; + // for small buffers, try to allocate in internal memory first to improve performance if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2) - buffer = malloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, true); // NOLINT if (buffer == nullptr) - buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT // if specific buffer size not set and can't get 100%, try for a smaller one if (buffer == nullptr && this->buffer_frac_ == 0) { frac = MIN_BUFFER_FRAC; - buffer_pixels /= MIN_BUFFER_FRAC; buf_bytes /= MIN_BUFFER_FRAC; - buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT } this->buffer_frac_ = frac; if (buffer == nullptr) { @@ -516,13 +581,17 @@ void LvglComponent::setup() { this->mark_failed(); return; } - lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = display->get_width(); - this->disp_drv_.ver_res = display->get_height(); - lv_disp_drv_update(this->disp_, &this->disp_drv_); + this->draw_buf_ = static_cast<uint8_t *>(buffer); + lv_display_set_resolution(this->disp_, this->width_, this->height_); + lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); + lv_display_set_flush_cb(this->disp_, static_flush_cb); + lv_display_set_user_data(this->disp_, this); + lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); + lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, + this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { - this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT + this->rotate_buf_ = static_cast<lv_color_t *>(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); @@ -530,26 +599,28 @@ void LvglComponent::setup() { } } if (this->draw_start_callback_ != nullptr) { - this->disp_drv_.render_start_cb = render_start_cb; + lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this); } if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { - this->disp_drv_.monitor_cb = monitor_cb; + lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this); } #if LV_USE_LOG - lv_log_register_print_cb([](const char *buf) { + lv_log_register_print_cb([](lv_log_level_t level, const char *buf) { auto next = strchr(buf, ')'); if (next != nullptr) buf = next + 1; while (isspace(*buf)) buf++; - esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); + if (level >= sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0])) + level = sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]) - 1; + esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); }); #endif // Rotation will be handled by our drawing function, so reset the display rotation. for (auto *disp : this->displays_) disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); - lv_disp_trig_activity(this->disp_); + lv_display_trigger_activity(this->disp_); } void LvglComponent::update() { @@ -557,7 +628,7 @@ void LvglComponent::update() { if (this->is_paused()) { return; } - this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); + this->idle_callbacks_.call(lv_display_get_inactive_time(this->disp_)); } void LvglComponent::loop() { @@ -565,41 +636,129 @@ void LvglComponent::loop() { if (this->paused_ && this->show_snow_) this->write_random_(); } else { - lv_timer_handler_run_in_period(5); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + auto now = millis(); + lv_timer_handler(); + auto elapsed = millis() - now; + if (elapsed > 15) { + ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now)); + } +#else + lv_timer_handler(); +#endif } } #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj) { - auto *animg = (lv_animimg_t *) obj; - int32_t duration = animg->anim.time; + int32_t duration = lv_animimg_get_duration(obj); lv_animimg_set_duration(obj, 0); lv_animimg_start(obj); lv_animimg_set_duration(obj, duration); } #endif -void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); +void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) { + reinterpret_cast<LvglComponent *>(lv_display_get_user_data(disp_drv))->flush_cb_(disp_drv, area, color_p); } -} // namespace lvgl -} // namespace esphome -size_t lv_millis(void) { return esphome::millis(); } +#ifdef USE_LVGL_SCALE +/** + * Function to apply colors to ticks based on position + * @param e The event data + * @param color_start The color to apply to the first tick + * @param color_end The color to apply to the last tick + */ +void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, + lv_color_t color_end, bool local) { + auto *scale = static_cast<lv_obj_t *>(lv_event_get_target(e)); + lv_draw_task_t *task = lv_event_get_draw_task(e); + + if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) { + auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task)); + auto tick = line_dsc->base.id1; + if (tick >= range_start && tick <= range_end) { + unsigned range = range_end - range_start; + if (local) { + tick -= range_start; + } else { + range = lv_scale_get_total_tick_count(scale) - 1; + } + if (range == 0) + range = 1; + auto ratio = (tick * 255) / range; + line_dsc->color = lv_color_mix(color_end, color_start, ratio); + } + } +} +#endif // USE_LVGL_SCALE + +static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) { + LV_TRACE_OBJ_CREATE("begin"); + LV_UNUSED(class_p); +} + +// Container class. Name is based on LVGL naming convention but upper case to keep ESPHome clang-tidy happy +const lv_obj_class_t LV_CONTAINER_CLASS = { + .base_class = &lv_obj_class, + .constructor_cb = lv_container_constructor, + .name = "lv_container", +}; + +lv_obj_t *lv_container_create(lv_obj_t *parent) { + lv_obj_t *obj = lv_obj_class_create_obj(&LV_CONTAINER_CLASS, parent); + lv_obj_class_init_obj(obj); + return obj; +} +} // namespace esphome::lvgl + +lv_result_t lv_mem_test_core() { return LV_RESULT_OK; } + +void lv_mem_init() {} + +void lv_mem_deinit() {} #if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) -void *lv_custom_mem_alloc(size_t size) { +void *lv_malloc_core(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); } return ptr; } -void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT -void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT -#else +void lv_free_core(void *ptr) { return free(ptr); } // NOLINT +void *lv_realloc_core(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT + +void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { memset(mon_p, 0, sizeof(lv_mem_monitor_t)); } +static void *lv_alloc_draw_buf(size_t size, bool internal) { + return malloc(size); // NOLINT +} + +#elif defined(USE_ESP32) static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT -void *lv_custom_mem_alloc(size_t size) { +static void *lv_alloc_draw_buf(size_t size, bool internal) { + void *buffer; + size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN; + buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT + if (buffer == nullptr) + ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); + return buffer; +} + +void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { + multi_heap_info_t heap_info; + heap_caps_get_info(&heap_info, cap_bits); + mon_p->total_size = heap_info.total_allocated_bytes + heap_info.total_free_bytes; + mon_p->free_size = heap_info.total_free_bytes; + mon_p->max_used = heap_info.total_allocated_bytes; + mon_p->free_biggest_size = heap_info.largest_free_block; + mon_p->used_cnt = heap_info.allocated_blocks; + mon_p->free_cnt = heap_info.free_blocks; + mon_p->used_pct = heap_info.allocated_blocks * 100 / (heap_info.allocated_blocks + heap_info.free_blocks); + mon_p->frag_pct = 0; +} + +void *lv_malloc_core(size_t size) { void *ptr; ptr = heap_caps_malloc(size, cap_bits); if (ptr == nullptr) { @@ -614,14 +773,14 @@ void *lv_custom_mem_alloc(size_t size) { return ptr; } -void lv_custom_mem_free(void *ptr) { +void lv_free_core(void *ptr) { ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); if (ptr == nullptr) return; heap_caps_free(ptr); } -void *lv_custom_mem_realloc(void *ptr, size_t size) { +void *lv_realloc_core(void *ptr, size_t size) { ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); return heap_caps_realloc(ptr, size, cap_bits); } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index aa8dd2fba52..4ce7296159e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -4,7 +4,7 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif // USE_BINARY_SENSOR -#ifdef USE_LVGL_IMAGE +#ifdef USE_IMAGE #include "esphome/components/image/image.h" #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ROTARY_ENCODER @@ -19,16 +19,17 @@ #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/log.h" + +#include <list> #include <lvgl.h> #include <map> #include <utility> #include <vector> -#ifdef USE_LVGL_FONT +#ifdef USE_FONT #include "esphome/components/font/font.h" #endif // USE_LVGL_FONT -#ifdef USE_LVGL_TOUCHSCREEN +#ifdef USE_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN @@ -36,12 +37,24 @@ #include "esphome/components/key_provider/key_provider.h" #endif // USE_LVGL_BUTTONMATRIX -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { + +#if LV_COLOR_DEPTH == 16 +using lv_color_data = uint16_t; +#endif +#if LV_COLOR_DEPTH == 32 +using lv_color_data = uint32_t; +#endif extern lv_event_code_t lv_api_event; // NOLINT extern lv_event_code_t lv_update_event; // NOLINT -extern std::string lv_event_code_name_for(uint8_t event_code); +extern std::string lv_event_code_name_for(lv_event_t *event); + +lv_obj_t *lv_container_create(lv_obj_t *parent); +#ifdef USE_LVGL_SCALE +void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, + lv_color_t color_end, bool local); +#endif #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; #elif LV_COLOR_DEPTH == 32 @@ -50,7 +63,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; #endif // LV_COLOR_DEPTH -#ifdef USE_LVGL_FONT +#if defined(USE_FONT) && defined(USE_LVGL_FONT) inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) { lv_obj_set_style_text_font(obj, font->get_lv_font(), part); } @@ -58,50 +71,37 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#ifdef USE_LVGL_IMAGE +#ifdef USE_IMAGE // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. -inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) { - lv_img_set_src(obj, image->get_lv_img_dsc()); -} -inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) { - lv_disp_set_bg_image(disp, image->get_lv_img_dsc()); +inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { + lv_image_set_src(obj, image->get_lv_image_dsc()); } -inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector); +inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } -#ifdef USE_LVGL_CANVAS -inline void lv_canvas_draw_img(lv_obj_t *canvas, lv_coord_t x, lv_coord_t y, image::Image *image, - lv_draw_img_dsc_t *dsc) { - lv_canvas_draw_img(canvas, x, y, image->get_lv_img_dsc(), dsc); -} -#endif - -#ifdef USE_LVGL_METER -inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src, - lv_coord_t pivot_x, lv_coord_t pivot_y) { - return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y); -} -#endif // USE_LVGL_METER #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { - auto *dsc = static_cast<std::vector<lv_img_dsc_t *> *>(lv_obj_get_user_data(img)); + auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img)); if (dsc == nullptr) { // object will be lazily allocated but never freed. - dsc = new std::vector<lv_img_dsc_t *>(images.size()); // NOLINT + dsc = new std::vector<lv_image_dsc_t *>(images.size()); // NOLINT lv_obj_set_user_data(img, dsc); } dsc->clear(); for (auto &image : images) { - dsc->push_back(image->get_lv_img_dsc()); + dsc->push_back(image->get_lv_image_dsc()); } lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size()); } - #endif // USE_LVGL_ANIMIMG +#ifdef USE_LVGL_METER +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value); +#endif + // Parent class for things that wrap an LVGL object class LvCompound { public: @@ -130,7 +130,7 @@ class LvPageType : public Parented<LvglComponent> { using LvLambdaType = std::function<void(lv_obj_t *)>; using set_value_lambda_t = std::function<void(float)>; -using event_callback_t = void(_lv_event_t *); +using event_callback_t = void(lv_event_t *); using text_lambda_t = std::function<const char *()>; template<typename... Ts> class ObjUpdateAction : public Action<Ts...> { @@ -152,7 +152,7 @@ class LvglComponent : public PollingComponent { public: LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding, bool resume_on_input, bool update_when_display_idle); - static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); + static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } void setup() override; @@ -160,11 +160,11 @@ class LvglComponent : public PollingComponent { void loop() override; template<typename F> void add_on_idle_callback(F &&callback) { this->idle_callbacks_.add(std::forward<F>(callback)); } - static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); - static void render_start_cb(lv_disp_drv_t *disp_drv); + static void render_end_cb(lv_event_t *event); + static void render_start_cb(lv_event_t *event); void dump_config() override; lv_disp_t *get_disp() { return this->disp_; } - lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); } + lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. @@ -187,11 +187,13 @@ class LvglComponent : public PollingComponent { static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3); + void add_page(LvPageType *page); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); void show_next_page(lv_scr_load_anim_t anim, uint32_t time); void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; } size_t get_current_page() const; void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } void restore_focus_mark(lv_group_t *group) { @@ -216,22 +218,25 @@ class LvglComponent : public PollingComponent { void draw_start_() const { this->draw_start_callback_->trigger(); } void write_random_(); - void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); - void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); + void draw_buffer_(const lv_area_t *area, lv_color_data *ptr); + void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); + std::vector<display::Display *> displays_{}; size_t buffer_frac_{1}; bool full_refresh_{}; bool resume_on_input_{}; bool update_when_display_idle_{}; - lv_disp_draw_buf_t draw_buf_{}; - lv_disp_drv_t disp_drv_{}; - lv_disp_t *disp_{}; + uint8_t *draw_buf_{}; + lv_display_t *disp_{}; + uint16_t width_{}; + uint16_t height_{}; bool paused_{}; std::vector<LvPageType *> pages_{}; size_t current_page_{0}; bool show_snow_{}; bool page_wrap_{true}; + bool big_endian_{}; std::map<lv_group_t *, lv_obj_t *> focus_marks_{}; CallbackManager<void(uint32_t)> idle_callbacks_{}; @@ -239,7 +244,7 @@ class LvglComponent : public PollingComponent { Trigger<> *resume_callback_{}; Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; - lv_color_t *rotate_buf_{}; + void *rotate_buf_{}; }; class IdleTrigger : public Trigger<> { @@ -278,19 +283,37 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC touch_pressed_ = false; this->parent_->maybe_wakeup(); } - lv_indev_drv_t *get_drv() { return &this->drv_; } + lv_indev_t *get_drv() { return this->drv_; } protected: - lv_indev_drv_t drv_{}; + lv_indev_t *drv_{}; touchscreen::TouchPoint touch_point_{}; bool touch_pressed_{}; }; #endif // USE_LVGL_TOUCHSCREEN +#ifdef USE_LVGL_METER + +class IndicatorLine : public LvCompound { + public: + IndicatorLine() = default; + + void set_obj(lv_obj_t *lv_obj) override; + + void set_value(int value); + + private: + void update_length_(); + + int16_t angle_{}; + lv_point_precise_t points_[2]{}; +}; +#endif + #ifdef USE_LVGL_KEY_LISTENER class LVEncoderListener : public Parented<LvglComponent> { public: - LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); + LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time); #ifdef USE_BINARY_SENSOR void add_button(binary_sensor::BinarySensor *button, lv_key_t key) { @@ -322,10 +345,10 @@ class LVEncoderListener : public Parented<LvglComponent> { } } - lv_indev_drv_t *get_drv() { return &this->drv_; } + lv_indev_t *get_drv() { return this->drv_; } protected: - lv_indev_drv_t drv_{}; + lv_indev_t *drv_{}; bool pressed_{}; int32_t count_{}; int32_t last_count_{}; @@ -336,14 +359,13 @@ class LVEncoderListener : public Parented<LvglComponent> { #ifdef USE_LVGL_LINE class LvLineType : public LvCompound { public: - std::vector<lv_point_t> get_points() { return this->points_; } - void set_points(std::vector<lv_point_t> points) { + void set_points(FixedVector<lv_point_precise_t> points) { this->points_ = std::move(points); - lv_line_set_points(this->obj, this->points_.data(), this->points_.size()); + lv_line_set_points(this->obj, this->points_.begin(), this->points_.size()); } protected: - std::vector<lv_point_t> points_{}; + FixedVector<lv_point_precise_t> points_{}; }; #endif #if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER) @@ -392,7 +414,7 @@ class LvRollerType : public LvSelectable { class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { public: void set_obj(lv_obj_t *lv_obj) override; - uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } + uint16_t get_selected() { return lv_buttonmatrix_get_selected_button(this->obj); } void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } protected: @@ -406,5 +428,4 @@ class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { void set_obj(lv_obj_t *lv_obj) override; }; #endif // USE_LVGL_KEYBOARD -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/lvgl_hal.h b/esphome/components/lvgl/lvgl_hal.h deleted file mode 100644 index 754cc703915..00000000000 --- a/esphome/components/lvgl/lvgl_hal.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Created by Clyde Stubbs on 20/9/2023. -// - -#pragma once - -#ifdef __cplusplus -#define EXTERNC extern "C" -#include <cstddef> -namespace esphome { -namespace lvgl {} -} // namespace esphome -#else -#define EXTERNC extern -#include <stddef.h> -#endif - -EXTERNC size_t lv_millis(void); -EXTERNC void *lv_custom_mem_alloc(size_t size); -EXTERNC void lv_custom_mem_free(void *ptr); -EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size); diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 98f8423b7c0..c48e051eac3 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -52,9 +52,9 @@ async def to_code(config): await value.get_lambda(), event_code, config[CONF_RESTORE_VALUE], - max_value=widget.get_max(), - min_value=widget.get_min(), - step=widget.get_step(), + max_value=widget.type.get_max(widget.config), + min_value=widget.type.get_min(widget.config), + step=widget.type.get_step(widget.config), ) async with LambdaContext(EVENT_ARG) as event: event.add(var.on_value()) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 2aeeedbd100..4e2bfeae85c 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -29,6 +29,7 @@ from .defines import ( CONF_SCROLLBAR_MODE, CONF_TIME_FORMAT, LV_GRAD_DIR, + get_remapped_uses, ) from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( @@ -42,16 +43,17 @@ from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( LVEncoderListener, LvType, - WidgetType, lv_group_t, lv_obj_t, lv_pseudo_button_t, lv_style_t, ) +from .widgets import WidgetType # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} + TIME_TEXT_SCHEMA = cv.Schema( { cv.Required(CONF_TIME_FORMAT): cv.string, @@ -176,23 +178,28 @@ STYLE_PROPS = { "height": lvalid.size, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, - "line_width": lvalid.lv_positive_int, - "line_dash_width": lvalid.lv_positive_int, - "line_dash_gap": lvalid.lv_positive_int, - "line_rounded": lvalid.lv_bool, "line_color": lvalid.lv_color, + "line_dash_gap": lvalid.lv_positive_int, + "line_dash_width": lvalid.lv_positive_int, + "line_opa": lvalid.opacity, + "line_rounded": lvalid.lv_bool, + "line_width": lvalid.lv_positive_int, "opa": lvalid.opacity, "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, "outline_pad": lvalid.padding, "outline_width": lvalid.pixels, + "length": lvalid.pixels_or_percent, "pad_all": lvalid.padding, "pad_bottom": lvalid.padding, "pad_left": lvalid.padding, "pad_right": lvalid.padding, "pad_top": lvalid.padding, + "radial_offset": lvalid.size, "shadow_color": lvalid.lv_color, + "shadow_offset_x": lvalid.lv_int, + "shadow_offset_y": lvalid.lv_int, "shadow_ofs_x": lvalid.lv_int, "shadow_ofs_y": lvalid.lv_int, "shadow_opa": lvalid.opacity, @@ -213,7 +220,13 @@ STYLE_PROPS = { "transform_height": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent, - "transform_zoom": lvalid.zoom, + "transform_rotation": lvalid.lv_angle, + "transform_scale": lvalid.scale, + "transform_scale_x": lvalid.scale, + "transform_scale_y": lvalid.scale, + "transform_skew_x": lvalid.lv_angle, + "transform_skew_y": lvalid.lv_angle, + "transform_zoom": lvalid.scale, "translate_x": lvalid.pixels_or_percent, "translate_y": lvalid.pixels_or_percent, "max_height": lvalid.pixels_or_percent, @@ -227,15 +240,23 @@ STYLE_PROPS = { } STYLE_REMAP = { - "bg_image_opa": "bg_img_opa", - "bg_image_recolor": "bg_img_recolor", - "bg_image_recolor_opa": "bg_img_recolor_opa", - "bg_image_src": "bg_img_src", - "bg_image_tiled": "bg_img_tiled", - "image_recolor": "img_recolor", - "image_recolor_opa": "img_recolor_opa", + "transform_angle": "transform_rotation", + "transform_zoom": "transform_scale", + "zoom": "scale", + "angle": "rotation", + "shadow_ofs_x": "shadow_offset_x", + "shadow_ofs_y": "shadow_offset_y", + "r_mod": "length", } + +def remap_property(prop): + if prop in STYLE_REMAP: + get_remapped_uses().add(prop) + return STYLE_REMAP[prop] + return prop + + # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -276,7 +297,7 @@ SET_STATE_SCHEMA = cv.Schema( ) # Setting object flags FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) -FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) +FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of) def part_schema(parts): @@ -418,6 +439,17 @@ ALL_STYLES = { } +def strip_defaults(schema: cv.Schema): + """ + Take a schema and remove any default values, also convert Required to Optional. + Useful for converting an object schema to a modify schema + :param schema: The original Schema + :return: A new schema with no defaults and all items optional + """ + + return cv.Schema({cv.Optional(k): v for k, v in schema.schema.items()}) + + def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus @@ -481,9 +513,9 @@ def any_widget_schema(extras=None): container_validator, requires_component(required) ) # Apply custom validation - value = widget_type.validate(value or {}) path = [key] if is_dict else [index, key] with prepend_path(path): + value = widget_type.validate(value or {}) result.append({key: container_validator(value)}) return result diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index ba03920a88f..3b00310b67b 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -6,7 +6,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../lvgl.h" +#include "esphome/components/lvgl/lvgl_esphome.h" namespace esphome { namespace lvgl { @@ -28,12 +28,12 @@ class LVGLSelect : public select::Select, public Component { lv_obj_add_event_cb( this->widget_->obj, [](lv_event_t *e) { - auto *it = static_cast<LVGLSelect *>(e->user_data); + auto *it = static_cast<LVGLSelect *>(lv_event_get_user_data(e)); it->set_options_(); }, LV_EVENT_REFRESH, this); auto lamb = [](lv_event_t *e) { - auto *self = static_cast<LVGLSelect *>(e->user_data); + auto *self = static_cast<LVGLSelect *>(lv_event_get_user_data(e)); self->publish(); }; lv_obj_add_event_cb(this->widget_->obj, lamb, LV_EVENT_VALUE_CHANGED, this); diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index b9801b41332..6f43e78f90e 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -13,7 +13,7 @@ from .defines import ( ) from .helpers import add_lv_use from .lvcode import LambdaContext, LocalVariable, lv -from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP +from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property from .types import ObjUpdateAction, lv_obj_t, lv_style_t from .widgets import ( Widget, @@ -26,6 +26,10 @@ from .widgets import ( from .widgets.obj import obj_spec +def has_style_props(config) -> bool: + return any(prop in config for prop in ALL_STYLES) + + async def style_set(svar, style): for prop, validator in ALL_STYLES.items(): if (value := style.get(prop)) is not None: @@ -33,22 +37,44 @@ async def style_set(svar, style): value = await validator.process(value) if isinstance(value, list): value = "|".join(value) - remapped_prop = STYLE_REMAP.get(prop, prop) - lv.call(f"style_set_{remapped_prop}", svar, literal(value)) + lv.call(f"style_set_{remap_property(prop)}", svar, literal(value)) -async def create_style(style, id_name): +async def create_style(id_name, style=None): style_id = ID(id_name, True, lv_style_t) svar = cg.new_Pvariable(style_id) lv.style_init(svar) - await style_set(svar, style) + if style: + await style_set(svar, style) return svar +class LVStyle: + """ + A class to lazily create a named style + """ + + named_styles = {} + + def __init__(self, id_name, style=None): + self.id_name = id_name + self.style = style + self._style_var = None + + async def get_var(self): + if self._style_var is None: + self._style_var = await create_style(self.id_name + "_style", self.style) + return self._style_var + + @classmethod + def get_style(cls, id_name): + return cls.named_styles.setdefault(id_name, LVStyle(id_name)) + + async def styles_to_code(config): """Convert styles to C__ code.""" for style in config.get(CONF_STYLE_DEFINITIONS, ()): - await create_style(style, style[CONF_ID].id) + await create_style(style[CONF_ID].id, style) @automation.register_action( @@ -81,8 +107,7 @@ async def theme_to_code(config): for part, states in collect_parts(style).items(): styles[part] = { state: await create_style( - props, - "_lv_theme_style_" + w_name + "_" + part + "_" + state, + "_lv_theme_style_" + w_name + "_" + part + "_" + state, props ) for state, props in states.items() } @@ -90,7 +115,7 @@ async def theme_to_code(config): async def add_top_layer(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + top_layer = lv.disp_get_layer_top(lv_component.var.get_disp()) if top_conf := config.get(CONF_TOP_LAYER): with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: top_w = Widget(top_layer_obj, obj_spec, top_conf) diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index 4c380d69a23..eacf69b6ec8 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -1,19 +1,16 @@ #pragma once -#include <utility> - #include "esphome/components/text/text.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/preferences.h" namespace esphome { namespace lvgl { class LVGLText : public text::Text { public: - void set_control_lambda(std::function<void(const std::string)> control_lambda) { - this->control_lambda_ = std::move(control_lambda); + void set_control_lambda(const std::function<void(std::string)> &control_lambda) { + this->control_lambda_ = control_lambda; if (this->initial_state_.has_value()) { this->control_lambda_(this->initial_state_.value()); this->initial_state_.reset(); @@ -28,7 +25,7 @@ class LVGLText : public text::Text { this->initial_state_ = value; } } - std::function<void(const std::string)> control_lambda_{}; + std::function<void(std::string)> control_lambda_{}; optional<std::string> initial_state_{}; }; diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index f2dd013f6db..0eb9f22f12e 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -10,7 +10,6 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lvcode import lv from .schemas import PRESS_TIME from .types import LVTouchListener @@ -40,5 +39,4 @@ async def touchscreens_to_code(lv_component, config): lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component) - lv.indev_drv_register(listener.get_drv()) cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 2f8b454ec41..c5ad4d402ef 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -30,7 +30,7 @@ from .lvcode import ( lvgl_static, ) from .types import LV_EVENT -from .widgets import LvScrActType, get_scr_act, widget_map +from .widgets import LvScrActType, get_screen_active, widget_map async def add_on_boot_triggers(triggers): @@ -48,7 +48,7 @@ async def generate_triggers(): for w in widget_map.values(): if isinstance(w.type, LvScrActType): - w = get_scr_act(w.var) + w = get_screen_active(w.var) if w.config: for event, conf in { diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 09d40bb7efe..03739f3ff16 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,15 +1,9 @@ -import sys - from esphome import automation, codegen as cg -from esphome.automation import register_action -from esphome.config_validation import Schema -from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE -from esphome.core import EsphomeError -from esphome.cpp_generator import MockObj, MockObjClass +from esphome.const import CONF_TEXT, CONF_VALUE +from esphome.cpp_generator import MockObj from esphome.cpp_types import esphome_ns from .defines import lvgl_ns -from .lvcode import lv_expr class LvType(cg.MockObjClass): @@ -26,6 +20,10 @@ class LvType(cg.MockObjClass): return None return [arg[0] for arg in self.args] + @property + def name(self): + return self.base.removeprefix("lv_").removesuffix("_t") + class LvNumber(LvType): def __init__(self, *args): @@ -63,6 +61,7 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_opa_t = cg.global_ns.struct("lv_opa_t") lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") @@ -70,10 +69,11 @@ lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_img_t = LvType("lv_img_t") lv_gradient_t = LvType("lv_grad_dsc_t") +lv_event_t = LvType("lv_event_t") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") -LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") +LV_BTNMATRIX_CTRL = MockObj(base="LV_BUTTONMATRIX_CTRL_", op="") class LvText(LvType): @@ -93,7 +93,7 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.is_checked(), + lvalue=lambda w: w.has_state(LV_STATE.CHECKED), has_on_value=True, **kwargs, ) @@ -110,137 +110,3 @@ class LvSelect(LvType): parents=parens, **kwargs, ) - - -class WidgetType: - """ - Describes a type of Widget, e.g. "bar" or "line" - """ - - def __init__( - self, - name: str, - w_type: LvType, - parts: tuple, - schema=None, - modify_schema=None, - lv_name=None, - is_mock: bool = False, - ): - """ - :param name: The widget name, e.g. "bar" - :param w_type: The C type of the widget - :param parts: What parts this widget supports - :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget, defaults to the same as the schema - :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name - :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget - """ - self.name = name - self.lv_name = lv_name or name - self.w_type = w_type - self.parts = parts - if not isinstance(schema, Schema): - schema = Schema(schema or {}) - self.schema = schema - if modify_schema is None: - modify_schema = schema - if not isinstance(modify_schema, Schema): - modify_schema = Schema(modify_schema) - self.modify_schema = modify_schema - self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") - - # Local import to avoid circular import - from .automation import update_to_code - from .schemas import WIDGET_TYPES, base_update_schema - - if not is_mock: - if self.name in WIDGET_TYPES: - raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") - WIDGET_TYPES[self.name] = self - - # Register the update action automatically, adding widget-specific properties - register_action( - f"lvgl.{self.name}.update", - ObjUpdateAction, - base_update_schema(self, self.parts).extend(self.modify_schema), - synchronous=True, - )(update_to_code) - - @property - def animated(self): - return False - - @property - def required_component(self): - return None - - def is_compound(self): - return self.w_type.inherits_from(LvCompound) - - async def to_code(self, w, config: dict): - """ - Generate code for a given widget - :param w: The widget - :param config: Its configuration - """ - - async def obj_creator(self, parent: MockObjClass, config: dict): - """ - Create an instance of the widget type - :param parent: The parent to which it should be attached - :param config: Its configuration - :return: Generated code as a single text line - """ - return lv_expr.call(f"{self.lv_name}_create", parent) - - def on_create(self, var: MockObj, config: dict): - """ - Called from to_code when the widget is created, to set up any initial properties - :param var: The variable representing the widget - :param config: Its configuration - """ - - def get_uses(self): - """ - Get a list of other widgets used by this one - :return: - """ - return () - - def get_max(self, config: dict): - return sys.maxsize - - def get_min(self, config: dict): - return -sys.maxsize - - def get_step(self, config: dict): - return 1 - - def get_scale(self, config: dict): - return 1.0 - - def validate(self, value): - """ - Provides an opportunity for custom validation for a given widget type - :param value: - :return: - """ - return value - - def final_validate(self, widget, update_config, widget_config, path): - """ - Allow final validation for a given widget type update action - :param widget: A widget - :param update_config: The configuration for the update action - :param widget_config: The configuration for the widget itself - :param path: The path to the widget, for error reporting - """ - - -class NumberType(WidgetType): - def get_max(self, config: dict): - return int(config.get(CONF_MAX_VALUE, 100)) - - def get_min(self, config: dict): - return int(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index b1d157325be..a2a8cf2129c 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -2,9 +2,18 @@ import sys from typing import Any from esphome import codegen as cg, config_validation as cv -from esphome.config_validation import Invalid -from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE -from esphome.core import ID, TimePeriod +from esphome.automation import register_action +from esphome.config_validation import Invalid, Schema +from esphome.const import ( + CONF_DEFAULT, + CONF_GROUP, + CONF_ID, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_STATE, + CONF_TYPE, +) +from esphome.core import ID, EsphomeError, TimePeriod from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import MockObj @@ -21,6 +30,7 @@ from ..defines import ( CONF_MAIN, CONF_PAD_COLUMN, CONF_PAD_ROW, + CONF_SCALE, CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, @@ -44,8 +54,15 @@ from ..lvcode import ( lv_obj, lv_Pvariable, ) -from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES -from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr +from ..types import ( + LV_STATE, + LvCompound, + LvType, + ObjUpdateAction, + lv_coord_t, + lv_obj_t, + lv_obj_t_ptr, +) EVENT_LAMB = "event_lamb__" @@ -53,6 +70,171 @@ theme_widget_map = {} styles_used = set() +class WidgetType: + """ + Describes a type of Widget, e.g. "bar" or "line" + """ + + def __init__( + self, + name: str, + w_type: LvType, + parts: tuple, + schema=None, + modify_schema=None, + lv_name=None, + is_mock: bool = False, + ): + """ + :param name: The widget name, e.g. "bar" + :param w_type: The C type of the widget + :param parts: What parts this widget supports + :param schema: The config schema for defining a widget + :param modify_schema: A schema to update the widget, defaults to the same as the schema + :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name + :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget + """ + self.name = name + self.lv_name = lv_name or name + self.w_type = w_type + self.parts = parts + if not isinstance(schema, Schema): + schema = Schema(schema or {}) + self.schema = schema + if modify_schema is None: + modify_schema = schema + if not isinstance(modify_schema, Schema): + modify_schema = Schema(modify_schema) + self.modify_schema = modify_schema + self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + + # Local import to avoid circular import + from ..automation import update_to_code + from ..schemas import WIDGET_TYPES, base_update_schema + + if not is_mock: + if self.name in WIDGET_TYPES: + raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") + WIDGET_TYPES[self.name] = self + + # Register the update action automatically, adding widget-specific properties + register_action( + f"lvgl.{self.name}.update", + ObjUpdateAction, + base_update_schema(self, self.parts).extend(self.modify_schema), + synchronous=True, + )(update_to_code) + + @property + def animated(self): + return False + + @property + def required_component(self): + return None + + def is_compound(self): + return self.w_type.inherits_from(LvCompound) + + async def create_to_code(self, config: dict, parent: MockObj) -> "Widget": + """ + Generate code for a widget creation. + :param config: The configuration for the widget + :param parent: The parent to which it should be attached + """ + + creator = await self.obj_creator(parent, config) + add_lv_use(self.name) + add_lv_use(*self.get_uses()) + wid = config[CONF_ID] + add_line_marks(wid) + if self.is_compound(): + var = cg.new_Pvariable(wid) + lv_add(var.set_obj(creator)) + await self.on_create(var.obj, config) + else: + var = lv_Pvariable(lv_obj_t, wid) + lv_assign(var, creator) + await self.on_create(var, config) + + w = Widget.create(wid, var, self, config) + if theme := theme_widget_map.get(self.w_type.name): + for part, states in theme.items(): + part = "LV_PART_" + part.upper() + for state, style in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + w.add_style(style, lv_state) + await set_obj_properties(w, config) + await add_widgets(w, config) + await self.to_code(w, config) + return w + + async def to_code(self, w: "Widget", config: dict): + """ + Update a widget, also called when creating + :param config: + :return: + """ + + async def obj_creator(self, parent: MockObj, config: dict): + """ + Create an instance of the widget type + :param parent: The parent to which it should be attached + :param config: Its configuration + :return: Generated code as a single text line + """ + return lv_expr.call(f"{self.lv_name}_create", parent) + + async def on_create(self, var: MockObj, config: dict): + """ + Called from to_code when the widget is created, to set up any initial properties + :param var: The variable representing the widget + :param config: Its configuration + """ + + def get_uses(self): + """ + Get a list of other widgets used by this one + :return: + """ + return () + + def get_max(self, config: dict): + return sys.maxsize + + def get_min(self, config: dict): + return -sys.maxsize + + def get_step(self, config: dict): + return 1 + + def get_scale(self, config: dict): + return 1.0 + + def validate(self, value): + """ + Provides an opportunity for custom validation for a given widget type + :param value: + :return: + """ + return value + + def final_validate(self, widget, update_config, widget_config, path): + """ + Allow final validation for a given widget type update action + :param widget: A widget + :param update_config: The configuration for the update action + :param widget_config: The configuration for the widget itself + :param path: The path to the widget, for error reporting + """ + + class Widget: """ Represents a Widget. @@ -74,19 +256,25 @@ class Widget: self.obj = var self.outer = None self.move_to_foreground = False + # Properties for linear equations + self.slope = None + self.y_int = None @staticmethod def create(name, var, wtype: WidgetType, config: dict = None): w = Widget(var, wtype, config) - if name is not None: - widget_map[name] = w + widget_map[name] = w return w def add_state(self, state): + if "|" in state: + state = f"(lv_state_t)({state})" return lv_obj.add_state(self.obj, literal(state)) def clear_state(self, state): - return lv_obj.clear_state(self.obj, literal(state)) + if "|" in state: + state = f"(lv_state_t)({state})" + return lv_obj.remove_state(self.obj, literal(state)) def has_state(self, state): return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 @@ -98,12 +286,23 @@ class Widget: return self.has_state(LV_STATE.CHECKED) def add_flag(self, flag): + if "|" in flag: + flag = f"(lv_obj_flag_t)({flag})" return lv_obj.add_flag(self.obj, literal(flag)) def clear_flag(self, flag): - return lv_obj.clear_flag(self.obj, literal(flag)) + if "|" in flag: + flag = f"(lv_obj_flag_t)({flag})" + return lv_obj.remove_flag(self.obj, literal(flag)) - async def set_property(self, prop, value, animated: bool = None, lv_name=None): + def add_style(self, style_id, state=LV_STATE.DEFAULT): + if "|" in state: + state = f"(lv_state_t)({state})" + lv_obj.add_style(self.obj, MockObj(style_id), literal(state)) + + async def set_property( + self, prop, value, animated: bool = None, lv_name=None, processor=None + ): """ Set a property of the widget. :param prop: The property name @@ -111,18 +310,28 @@ class Widget: :param animated: If the change should be animated :param lv_name: The base type of the widget e.g. "obj" """ + + from ..schemas import ALL_STYLES, remap_property + if isinstance(value, dict): value = value.get(prop) - if isinstance(ALL_STYLES.get(prop), LValidator): - value = await ALL_STYLES[prop].process(value) - else: - value = literal(value) - if value is None: + if value is None: + return + if not processor and isinstance(ALL_STYLES.get(prop), LValidator): + processor = ALL_STYLES[prop] + if isinstance(processor, LValidator): + processor = processor.process + if processor: + value = await processor(value) + elif value is None: return + prop = remap_property(prop) if isinstance(value, TimePeriod): value = value.total_milliseconds - if isinstance(value, str): + elif isinstance(value, str): value = literal(value) + elif isinstance(value, ID): + value = MockObj(value) lv_name = lv_name or self.type.lv_name if animated is None or self.type.animated is not True: lv.call(f"{lv_name}_set_{prop}", self.obj, value) @@ -138,10 +347,12 @@ class Widget: ltype = ltype or self.__type_base() return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") - def set_style(self, prop, value, state): + def set_style(self, prop, value, state=LV_STATE.DEFAULT): if value is None: return styles_used.add(prop) + if isinstance(value, str): + value = literal(value) lv.call(f"obj_set_style_{prop}", self.obj, value, state) def __type_base(self): @@ -189,15 +400,6 @@ class Widget: """ return - def get_max(self): - return self.type.get_max(self.config) - - def get_min(self): - return self.type.get_min(self.config) - - def get_step(self): - return self.type.get_step(self.config) - def get_scale(self): return self.type.get_scale(self.config) @@ -212,14 +414,14 @@ class LvScrActType(WidgetType): """ def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True) + super().__init__("lv_screen_active()", lv_obj_t, (), is_mock=True) async def to_code(self, w, config: dict): - return [] + pass -def get_scr_act(lv_comp: MockObj) -> Widget: - return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {}) +def get_screen_active(lv_comp: MockObj) -> Widget: + return Widget(lv_comp.get_screen_active(), LvScrActType(), {}) def get_widget_generator(wid): @@ -271,10 +473,17 @@ def collect_props(config): :param config: :return: """ + + from ..schemas import ALL_STYLES + props = {} for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: if prop in config: - props[prop] = config[prop] + if prop == CONF_SCALE: + props[CONF_SCALE + "_x"] = config[prop] + props[CONF_SCALE + "_y"] = config[prop] + else: + props[prop] = config[prop] return props @@ -304,34 +513,41 @@ def collect_parts(config): return parts +def _size_to_str(value): + if isinstance(value, float): + return f"lv_pct({int(value * 100)})" + return str(value) + + async def set_obj_properties(w: Widget, config): """Generate a list of C++ statements to apply properties to an lv_obj_t""" + + from ..schemas import ALL_STYLES, OBJ_PROPERTIES, remap_property + if layout := config.get(CONF_LAYOUT): layout_type: str = layout[CONF_TYPE] add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) if (pad_row := layout.get(CONF_PAD_ROW)) is not None: - w.set_style(CONF_PAD_ROW, pad_row, 0) + w.set_style(CONF_PAD_ROW, pad_row) if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: - w.set_style(CONF_PAD_COLUMN, pad_column, 0) + w.set_style(CONF_PAD_COLUMN, pad_column) if layout_type == TYPE_GRID: wid = config[CONF_ID] - rows = [str(x) for x in layout[CONF_GRID_ROWS]] + rows = [_size_to_str(x) for x in layout[CONF_GRID_ROWS]] rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) - w.set_style("grid_row_dsc_array", row_array, 0) - columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] + w.set_style("grid_row_dsc_array", row_array) + columns = [_size_to_str(x) for x in layout[CONF_GRID_COLUMNS]] columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) - w.set_style("grid_column_dsc_array", column_array, 0) + w.set_style("grid_column_dsc_array", column_array) w.set_style( - CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 - ) - w.set_style( - CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 + CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)) ) + w.set_style(CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN))) if layout_type == TYPE_FLEX: lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) main = literal(layout[CONF_FLEX_ALIGN_MAIN]) @@ -353,13 +569,13 @@ async def set_obj_properties(w: Widget, config): else: lv_state = join_enums((state, part)) for style_id in props.get(CONF_STYLES, ()): - lv_obj.add_style(w.obj, MockObj(style_id), lv_state) + w.add_style(style_id, lv_state) for prop, value in { k: v for k, v in props.items() if k in ALL_STYLES }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - prop_r = STYLE_REMAP.get(prop, prop) + prop_r = remap_property(prop) w.set_style(prop_r, value, lv_state) if group := config.get(CONF_GROUP): group = await cg.get_variable(group) @@ -429,7 +645,7 @@ async def add_widgets(parent: Widget, config: dict): await widget_to_code(w_cnfig, w_type, parent.obj) -async def widget_to_code(w_cnfig, w_type: WidgetType, parent): +async def widget_to_code(w_cnfig, w_type: WidgetType | str, parent) -> Widget: """ Converts a Widget definition to C code. :param w_cnfig: The widget configuration @@ -437,34 +653,18 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): :param parent: The parent to which the widget should be added :return: """ - spec: WidgetType = WIDGET_TYPES[w_type] - creator = await spec.obj_creator(parent, w_cnfig) - add_lv_use(spec.name) - add_lv_use(*spec.get_uses()) - wid = w_cnfig[CONF_ID] - add_line_marks(wid) - if spec.is_compound(): - var = cg.new_Pvariable(wid) - lv_add(var.set_obj(creator)) - spec.on_create(var.obj, w_cnfig) - else: - var = lv_Pvariable(lv_obj_t, wid) - lv_assign(var, creator) - spec.on_create(var, w_cnfig) - w = Widget.create(wid, var, spec, w_cnfig) - if theme := theme_widget_map.get(w_type): - for part, states in theme.items(): - part = "LV_PART_" + part.upper() - for state, style in states.items(): - state = "LV_STATE_" + state.upper() - if state == "LV_STATE_DEFAULT": - lv_state = literal(part) - elif part == "LV_PART_MAIN": - lv_state = literal(state) - else: - lv_state = join_enums((state, part)) - lv.obj_add_style(w.obj, style, lv_state) - await set_obj_properties(w, w_cnfig) - await add_widgets(w, w_cnfig) - await spec.to_code(w, w_cnfig) + from ..schemas import WIDGET_TYPES + + spec: WidgetType = ( + w_type if isinstance(w_type, WidgetType) else WIDGET_TYPES[w_type] + ) + return await spec.create_to_code(w_cnfig, parent) + + +class NumberType(WidgetType): + def get_max(self, config: dict): + return int(config.get(CONF_MAX_VALUE, 100)) + + def get_min(self, config: dict): + return int(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 34ac9c51f75..9eaf3dadce4 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -18,7 +18,8 @@ from ..defines import ( CONF_KNOB, CONF_MAIN, CONF_START_ANGLE, - literal, + LV_OBJ_FLAG, + LV_PART, ) from ..lv_validation import ( get_start_value, @@ -28,8 +29,8 @@ from ..lv_validation import ( lv_positive_int, ) from ..lvcode import lv, lv_expr, lv_obj -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( @@ -71,39 +72,17 @@ class ArcType(NumberType): ) async def to_code(self, w: Widget, config): - if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: - max_value = await lv_int.process(config[CONF_MAX_VALUE]) - min_value = await lv_int.process(config[CONF_MIN_VALUE]) - lv.arc_set_range(w.obj, min_value, max_value) - elif CONF_MIN_VALUE in config: - max_value = w.get_property(CONF_MAX_VALUE) - min_value = await lv_int.process(config[CONF_MIN_VALUE]) - lv.arc_set_range(w.obj, min_value, max_value) - elif CONF_MAX_VALUE in config: - max_value = await lv_int.process(config[CONF_MAX_VALUE]) - min_value = w.get_property(CONF_MIN_VALUE) - lv.arc_set_range(w.obj, min_value, max_value) - - await w.set_property( - "bg_start_angle", - await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), - ) - await w.set_property( - "bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) - ) - await w.set_property( - CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) - ) - await w.set_property(CONF_MODE, config) - await w.set_property( - CONF_CHANGE_RATE, - await lv_positive_int.process(config.get(CONF_CHANGE_RATE)), - ) - + for prop, validator in ARC_MODIFY_SCHEMA.schema.items(): + if prop != CONF_VALUE: + # start_angle and end_angle are mapped to bg_start_angle and bg_end_angle + prop = str(prop) + if prop.endswith("_angle"): + prop = "bg_" + prop + await w.set_property(prop, config, processor=validator) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: - lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) - w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB) + w.clear_flag(LV_OBJ_FLAG.CLICKABLE) elif CONF_GROUP not in config: # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index 5f2910174f1..b943a4d9aa6 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -7,11 +7,11 @@ from ..helpers import add_lv_use from ..lv_validation import lv_text from ..lvcode import lv, lv_expr from ..schemas import TEXT_SCHEMA -from ..types import LvBoolean, WidgetType -from . import Widget +from ..types import LvBoolean +from . import Widget, WidgetType from .label import label_spec -lv_button_t = LvBoolean("lv_btn_t") +lv_button_t = LvBoolean("lv_button_t") class ButtonType(WidgetType): @@ -30,7 +30,7 @@ class ButtonType(WidgetType): def get_uses(self): return ("btn",) - def on_create(self, var: MockObj, config: dict): + async def on_create(self, var: MockObj, config: dict): if CONF_TEXT in config: lv.label_create(var) return var diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index f94f12b69b2..f5ae0deba93 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -118,15 +118,15 @@ class MatrixButton(Widget): def has_state(self, state): state = self.map_ctrls(state) - return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) + return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state) def add_state(self, state): state = self.map_ctrls(state) - return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) + return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state) def clear_state(self, state): state = self.map_ctrls(state) - return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) + return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state) def is_pressed(self): return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) @@ -161,7 +161,7 @@ async def get_button_data(config, buttonmatrix: Widget): text_list.append(button_conf.get(CONF_TEXT) or "") key_list.append(button_conf.get(CONF_KEY_CODE) or 0) width_list.append(button_conf[CONF_WIDTH]) - ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] + ctrl = ["CLICK_TRIG"] for item in button_conf.get(CONF_CONTROL, ()): ctrl.extend([k for k, v in item.items() if v]) ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) @@ -187,7 +187,7 @@ class ButtonMatrixType(WidgetType): (CONF_MAIN, CONF_ITEMS), BUTTONMATRIX_SCHEMA, {}, - lv_name="btnmatrix", + lv_name="buttonmatrix", ) async def to_code(self, w: Widget, config): @@ -199,22 +199,22 @@ class ButtonMatrixType(WidgetType): ) text_id = config[CONF_BUTTON_TEXT_LIST_ID] text_id = cg.static_const_array(text_id, text_list) - lv.btnmatrix_set_map(w.obj, text_id) + lv.buttonmatrix_set_map(w.obj, text_id) set_btn_data(w.obj, ctrl_list, width_list) - lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) + lv.buttonmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) for index, key in enumerate(key_list): if key != 0: lv_add(w.var.set_key(index, key)) def get_uses(self): - return ("btnmatrix",) + return ("buttonmatrix",) def set_btn_data(obj, ctrl_list, width_list): for index, ctrl in enumerate(ctrl_list): - lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) + lv.buttonmatrix_set_button_ctrl(obj, index, ctrl) for index, width in enumerate(width_list): - lv.btnmatrix_set_btn_width(obj, index, width) + lv.buttonmatrix_set_button_width(obj, index, width) buttonmatrix_spec = ButtonMatrixType() @@ -253,25 +253,21 @@ async def button_update_to_code(config, action_id, template_arg, args): async def do_button_update(w): if (width := config.get(CONF_WIDTH)) is not None: - lv.btnmatrix_set_btn_width(w.obj, w.index, width) + lv.buttonmatrix_set_button_width(w.obj, w.index, width) if config.get(CONF_SELECTED): - lv.btnmatrix_set_selected_btn(w.obj, w.index) + lv.buttonmatrix_set_selected_button(w.obj, w.index) if controls := config.get(CONF_CONTROL): adds = [] clrs = [] for item in controls: - adds.extend( - [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] - ) - clrs.extend( - [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] - ) + adds.extend([f"{k.upper()}" for k, v in item.items() if v]) + clrs.extend([f"{k.upper()}" for k, v in item.items() if not v]) if adds: - lv.btnmatrix_set_btn_ctrl( + lv.buttonmatrix_set_button_ctrl( w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) ) if clrs: - lv.btnmatrix_clear_btn_ctrl( + lv.buttonmatrix_clear_button_ctrl( w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) ) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 50cc8b0af62..c670e3732cd 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -1,3 +1,22 @@ +""" +LVGL 9.4 Canvas Widget Implementation + +This module implements the canvas widget for LVGL 9.4. Key changes from LVGL 8.4: + +1. Buffer allocation: + - LV_IMG_CF_TRUE_COLOR → LV_COLOR_FORMAT_RGB565 + - LV_IMG_CF_TRUE_COLOR_ALPHA → LV_COLOR_FORMAT_ARGB8888 + - LV_CANVAS_BUF_SIZE_TRUE_COLOR → LV_CANVAS_BUF_SIZE(w, h, bpp, stride) + +2. Drawing API: + - All lv_canvas_draw_* functions removed + - Use layer-based drawing: lv_canvas_init_layer() / lv_canvas_finish_layer() + - Draw using low-level lv_draw_* functions (rect, line, arc, image, label) + +3. Pixel operations: + - lv_canvas_set_px_color + lv_canvas_set_px_opa → lv_canvas_set_px(color, opa) +""" + from esphome import automation, codegen as cg, config_validation as cv from esphome.components.display_menu_base import CONF_LABEL from esphome.const import ( @@ -9,7 +28,7 @@ from esphome.const import ( CONF_X, CONF_Y, ) -from esphome.cpp_generator import Literal, MockObj +from esphome.cpp_types import FixedVector from ..automation import action_to_code from ..defines import ( @@ -19,8 +38,11 @@ from ..defines import ( CONF_PIVOT_X, CONF_PIVOT_Y, CONF_POINTS, + CONF_RADIUS, CONF_SRC, CONF_START_ANGLE, + addr, + get_color_formats, literal, ) from ..lv_validation import ( @@ -34,18 +56,20 @@ from ..lv_validation import ( size, ) from ..lvcode import LocalVariable, lv, lv_assign, lv_expr -from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema -from ..types import LvType, ObjUpdateAction, WidgetType -from . import Widget, get_widgets -from .line import lv_point_t, process_coord +from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property +from ..types import LvType, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .img import CONF_IMAGE +from .line import lv_point_precise_t, process_coord CONF_CANVAS = "canvas" CONF_BUFFER_ID = "buffer_id" CONF_MAX_WIDTH = "max_width" CONF_TRANSPARENT = "transparent" -CONF_RADIUS = "radius" +CONF_DRAW_BUF_ID = "draw_buf_id" lv_canvas_t = LvType("lv_canvas_t") +lv_draw_buf_t = LvType("lv_draw_buf_t") class CanvasType(WidgetType): @@ -59,32 +83,44 @@ class CanvasType(WidgetType): cv.Required(CONF_WIDTH): size, cv.Required(CONF_HEIGHT): size, cv.Optional(CONF_TRANSPARENT, default=False): cv.boolean, + cv.GenerateID(CONF_DRAW_BUF_ID): cv.declare_id(lv_draw_buf_t), } ), + modify_schema={}, ) def get_uses(self): - return "img", CONF_LABEL + return CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): width = config[CONF_WIDTH] height = config[CONF_HEIGHT] - use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" - buf_size = literal( - f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" + # LVGL 9.4: Use LV_COLOR_FORMAT instead of LV_IMG_CF + # RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel) + if config[CONF_TRANSPARENT]: + color_format = "LV_COLOR_FORMAT_ARGB8888" + get_color_formats().add("ARGB8888") + else: + color_format = "LV_COLOR_FORMAT_NATIVE" + + # LVGL 9.4: LV_CANVAS_BUF_SIZE(width, height, bits_per_pixel, stride) + # stride is 0 for default (width * bytes_per_pixel) + draw_buf = cg.new_Pvariable(config[CONF_DRAW_BUF_ID]) + buf_size = literal(f"LV_DRAW_BUF_SIZE({width}, {height}, {color_format})") + lv.draw_buf_init( + draw_buf, + width, + height, + literal(color_format), + 0, + lv_expr.malloc_core(buf_size), + literal(buf_size), ) - with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: - cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) - lv.canvas_set_buffer( - w.obj, - buf, - width, - height, - literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), - ) + lv.draw_buf_set_flag(draw_buf, literal("LV_IMAGE_FLAGS_MODIFIABLE")) + lv.canvas_set_draw_buf(w.obj, draw_buf) -canvas_spec = CanvasType() +CanvasType() @automation.register_action( @@ -117,7 +153,7 @@ async def canvas_fill(config, action_id, template_arg, args): { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Required(CONF_COLOR): lv_color, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default="COVER"): opacity, cv.Required(CONF_POINTS): cv.ensure_list(point_schema), }, ), @@ -126,7 +162,7 @@ async def canvas_fill(config, action_id, template_arg, args): async def canvas_set_pixel(config, action_id, template_arg, args): widget = await get_widgets(config) color = await lv_color.process(config[CONF_COLOR]) - opa = await opacity.process(config.get(CONF_OPA)) + opa = await opacity.process(config.get(CONF_OPA), "COVER") points = [ ( await pixels.process(p[CONF_X]), @@ -136,25 +172,11 @@ async def canvas_set_pixel(config, action_id, template_arg, args): ] async def do_set_pixels(w: Widget): - if isinstance(color, MockObj): - for point in points: - x, y = point - lv.canvas_set_px_color(w.obj, x, y, color) - else: - with LocalVariable("color", "lv_color_t", color, modifier="") as color_var: - for point in points: - x, y = point - lv.canvas_set_px_color(w.obj, x, y, color_var) - if opa: - if isinstance(opa, Literal): - for point in points: - x, y = point - lv.canvas_set_px_opa(w.obj, x, y, opa) - else: - with LocalVariable("opa", "lv_opa_t", opa, modifier="") as opa_var: - for point in points: - x, y = point - lv.canvas_set_px_opa(w.obj, x, y, opa_var) + # LVGL 9.4: lv_canvas_set_px combines color and opacity + # Could optimize this for lambda values + for point in points: + x, y = point + lv.canvas_set_px(w.obj, x, y, color, opa) return await action_to_code( widget, do_set_pixels, action_id, template_arg, args, config @@ -178,18 +200,21 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg y = await pixels.process(config.get(CONF_Y)) async def action_func(w: Widget): - with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc: - dsc_addr = literal(f"&{dsc}") - lv.call(f"draw_{dsc_type}_dsc_init", dsc_addr) - if CONF_OPA in config: - opa = await opacity.process(config[CONF_OPA]) - lv_assign(dsc.opa, opa) - for prop, validator in props.items(): - if prop in config: - value = await validator.process(config[prop]) - mapped_prop = STYLE_REMAP.get(prop, prop) - lv_assign(getattr(dsc, mapped_prop), value) - await do_draw(w, x, y, dsc_addr) + # LVGL 9.4: Create a layer for drawing on canvas + with LocalVariable("layer", "lv_layer_t", modifier="") as layer: + lv.canvas_init_layer(w.obj, addr(layer)) + with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc: + lv.call(f"draw_{dsc_type}_dsc_init", addr(dsc)) + if CONF_OPA in config: + opa = await opacity.process(config[CONF_OPA]) + lv_assign(dsc.opa, opa) + for prop, validator in props.items(): + if prop in config: + value = await validator.process(config[prop]) + mapped_prop = remap_property(prop) + lv_assign(getattr(dsc, mapped_prop), value) + await do_draw(addr(layer), x, y, dsc) + lv.canvas_finish_layer(w.obj, addr(layer)) return await action_to_code( widget, action_func, action_id, template_arg, args, config @@ -212,6 +237,8 @@ RECT_PROPS = { "outline_opa", "shadow_color", "shadow_width", + "shadow_offset_x", + "shadow_offset_y", "shadow_ofs_x", "shadow_ofs_y", "shadow_spread", @@ -220,6 +247,24 @@ RECT_PROPS = { } +def _draw_line(layer, dsc, points): + # LVGL 9.4: Use lv_draw_line for each line segment + with ( + LocalVariable( + "points", FixedVector.template(lv_point_precise_t), points, modifier="" + ) as points_var, + LocalVariable("i", "uint32_t", literal("0"), modifier="") as i, + ): + # Draw lines between consecutive points + lv.append( + cg.RawStatement(f"for ({i} = 0; {i} != {points_var}.size() - 1; {i}++) {{") + ) + lv_assign(dsc.p1, points_var[i]) + lv_assign(dsc.p2, points_var[i + 1]) + lv.draw_line(layer, addr(dsc)) + lv.append(cg.RawStatement("}")) + + @automation.register_action( "lvgl.canvas.draw_rectangle", ObjUpdateAction, @@ -237,8 +282,14 @@ async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) height = await pixels.process(config[CONF_HEIGHT]) - async def do_draw_rect(w: Widget, x, y, dsc_addr): - lv.canvas_draw_rect(w.obj, x, y, width, height, dsc_addr) + async def do_draw_rect(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_rect with area + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + lv_assign(area.x2, literal(f"{x} + {width} - 1")) + lv_assign(area.y2, literal(f"{y} + {height} - 1")) + lv.draw_rect(layer, addr(dsc), addr(area)) return await draw_to_code( config, "rect", RECT_PROPS, do_draw_rect, action_id, template_arg, args @@ -277,21 +328,55 @@ async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) max_width = await pixels.process(config[CONF_MAX_WIDTH]) - async def do_draw_text(w: Widget, x, y, dsc_addr): - lv.canvas_draw_text(w.obj, x, y, max_width, dsc_addr, text) + async def do_draw_text(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_label with area and hint + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + lv_assign(area.x2, literal(f"{x} + {max_width} - 1")) + lv_assign(area.y2, literal(f"{y} + LV_COORD_MAX")) + lv_assign(dsc.text, text) + lv.draw_label(layer, addr(dsc), addr(area)) return await draw_to_code( config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args ) -IMG_PROPS = { - "angle": STYLE_PROPS["transform_angle"], - "zoom": STYLE_PROPS["transform_zoom"], - "recolor": STYLE_PROPS["image_recolor"], - "recolor_opa": STYLE_PROPS["image_recolor_opa"], - "opa": STYLE_PROPS["opa"], -} +IMG_PROPS = ( + "angle", + "rotation", + "scale_x", + "scale_y", + "skew_x", + "skew_y", + "scale", + "zoom", + "recolor", + "recolor_opa", + "opa", +) + + +def _scale_map(config): + config = {remap_property(p): v for p, v in config.items()} + if "scale" in config and {"scale_x", "scale_y"} & config.keys(): + raise cv.Invalid("Cannot specify both scale and scale_x/scale_y") + if "scale" in config: + config.update({"scale_x": config["scale"], "scale_y": config["scale"]}) + del config["scale"] + return config + + +def _get_prop_validator(prop): + return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop) + + +def _prop_validator(prop): + def validator(value): + return _get_prop_validator(prop)(value) + + return validator @automation.register_action( @@ -303,9 +388,9 @@ IMG_PROPS = { cv.Required(CONF_SRC): lv_image, cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels, - **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, + **{cv.Optional(prop): _prop_validator(prop) for prop in IMG_PROPS}, } - ), + ).add_extra(_scale_map), synchronous=True, ) async def canvas_draw_image(config, action_id, template_arg, args): @@ -313,15 +398,29 @@ async def canvas_draw_image(config, action_id, template_arg, args): pivot_x = await pixels.process(config[CONF_PIVOT_X]) pivot_y = await pixels.process(config[CONF_PIVOT_Y]) - async def do_draw_image(w: Widget, x, y, dsc_addr): - dsc = MockObj(f"(*{dsc_addr})") + async def do_draw_image(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_image with area + lv_assign(dsc.src, src.get_lv_image_dsc()) if pivot_x or pivot_y: # pylint :disable=no-member - lv_assign(dsc.pivot, literal(f"{{{pivot_x}, {pivot_y}}}")) - lv.canvas_draw_img(w.obj, x, y, src, dsc_addr) + lv_assign(dsc.pivot.x, pivot_x) + lv_assign(dsc.pivot.y, pivot_y) + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + # Image size will be determined from the image descriptor + lv_assign(area.x2, x) + lv_assign(area.y2, y) + lv.draw_image(layer, addr(dsc), addr(area)) return await draw_to_code( - config, "img", IMG_PROPS, do_draw_image, action_id, template_arg, args + config, + "image", + {prop: _get_prop_validator(prop) for prop in IMG_PROPS}, + do_draw_image, + action_id, + template_arg, + args, ) @@ -354,11 +453,8 @@ async def canvas_draw_line(config, action_id, template_arg, args): for p in config[CONF_POINTS] ] - async def do_draw_line(w: Widget, x, y, dsc_addr): - with LocalVariable( - "points", cg.std_vector.template(lv_point_t), points, modifier="" - ) as points_var: - lv.canvas_draw_line(w.obj, points_var.data(), points_var.size(), dsc_addr) + async def do_draw_line(layer, _x, _y, dsc): + _draw_line(layer, dsc, points) return await draw_to_code( config, "line", LINE_PROPS, do_draw_line, action_id, template_arg, args @@ -382,14 +478,20 @@ async def canvas_draw_polygon(config, action_id, template_arg, args): [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] for p in config[CONF_POINTS] ] + # Close the polygon + points.append(points[0]) - async def do_draw_polygon(w: Widget, x, y, dsc_addr): - with LocalVariable( - "points", cg.std_vector.template(lv_point_t), points, modifier="" - ) as points_var: - lv.canvas_draw_polygon( - w.obj, points_var.data(), points_var.size(), dsc_addr - ) + async def do_draw_polygon(layer, x, y, dsc): + # LVGL 9.4: Draw polygon using line drawing in a closed path + # Note: This draws outline only. For filled polygons, would need different approach + # Convert rect descriptor to line descriptor for polygon outline + with LocalVariable("line_dsc", "lv_draw_line_dsc_t", modifier="") as line_dsc: + lv.draw_line_dsc_init(addr(line_dsc)) + # Copy border properties from rect descriptor to line descriptor + lv_assign(line_dsc.color, dsc.border_color) + lv_assign(line_dsc.width, dsc.border_width) + lv_assign(line_dsc.opa, dsc.border_opa) + _draw_line(layer, line_dsc, points) return await draw_to_code( config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args @@ -422,8 +524,14 @@ async def canvas_draw_arc(config, action_id, template_arg, args): start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE]) end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE]) - async def do_draw_arc(w: Widget, x, y, dsc_addr): - lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr) + async def do_draw_arc(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_arc with center point + lv_assign(dsc.center.x, x) + lv_assign(dsc.center.y, y) + lv_assign(dsc.start_angle, start_angle) + lv_assign(dsc.end_angle, end_angle) + lv_assign(dsc.radius, radius) + lv.draw_arc(layer, addr(dsc)) return await draw_to_code( config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args diff --git a/esphome/components/lvgl/widgets/container.py b/esphome/components/lvgl/widgets/container.py index 2ac1a3b244b..427b46affa1 100644 --- a/esphome/components/lvgl/widgets/container.py +++ b/esphome/components/lvgl/widgets/container.py @@ -1,11 +1,10 @@ import esphome.config_validation as cv from esphome.const import CONF_HEIGHT, CONF_WIDTH -from esphome.cpp_generator import MockObj -from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_SCROLLBAR from ..lv_validation import size -from ..lvcode import lv -from ..types import WidgetType, lv_obj_t +from ..types import lv_obj_t +from . import WidgetType CONTAINER_SCHEMA = cv.Schema( { @@ -28,12 +27,9 @@ class ContainerType(WidgetType): (CONF_MAIN, CONF_SCROLLBAR), schema=CONTAINER_SCHEMA, modify_schema={}, - lv_name=CONF_OBJ, + lv_name=CONF_CONTAINER, ) self.styles = {} - def on_create(self, var: MockObj, config: dict): - lv.obj_remove_style_all(var) - container_spec = ContainerType() diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 8ec18e30335..ed6fd30c097 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,17 +1,22 @@ import esphome.config_validation as cv -from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y +from esphome.const import ( + CONF_ANGLE, + CONF_MODE, + CONF_OFFSET_X, + CONF_OFFSET_Y, + CONF_ROTATION, +) from ..defines import ( CONF_ANTIALIAS, CONF_MAIN, CONF_PIVOT_X, CONF_PIVOT_Y, + CONF_SCALE, CONF_SRC, CONF_ZOOM, - LvConstant, ) -from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom -from ..lvcode import lv +from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size from ..types import lv_img_t from . import Widget, WidgetType from .label import CONF_LABEL @@ -22,14 +27,14 @@ BASE_IMG_SCHEMA = cv.Schema( { cv.Optional(CONF_PIVOT_X): size, cv.Optional(CONF_PIVOT_Y): size, - cv.Optional(CONF_ANGLE): lv_angle, - cv.Optional(CONF_ZOOM): zoom, + cv.Exclusive(CONF_ANGLE, CONF_ROTATION): lv_angle, + cv.Exclusive(CONF_ROTATION, CONF_ROTATION): lv_angle, + cv.Exclusive(CONF_ZOOM, CONF_SCALE): scale, + cv.Exclusive(CONF_SCALE, CONF_SCALE): scale, cv.Optional(CONF_OFFSET_X): size, cv.Optional(CONF_OFFSET_Y): size, cv.Optional(CONF_ANTIALIAS): lv_bool, - cv.Optional(CONF_MODE): LvConstant( - "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" - ).one_of, + cv.Optional(CONF_MODE): cv.invalid(f"{CONF_MODE} is not supported in LVGL 9.x"), } ) @@ -54,33 +59,15 @@ class ImgType(WidgetType): (CONF_MAIN,), IMG_SCHEMA, IMG_MODIFY_SCHEMA, - lv_name="img", ) def get_uses(self): - return "img", CONF_LABEL + return CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): - if src := config.get(CONF_SRC): - lv.img_set_src(w.obj, await lv_image.process(src)) - if (pivot_x := config.get(CONF_PIVOT_X)) and ( - pivot_y := config.get(CONF_PIVOT_Y) - ): - lv.img_set_pivot( - w.obj, await size.process(pivot_x), await size.process(pivot_y) - ) - if (cf_angle := config.get(CONF_ANGLE)) is not None: - lv.img_set_angle(w.obj, await lv_angle.process(cf_angle)) - if (img_zoom := config.get(CONF_ZOOM)) is not None: - lv.img_set_zoom(w.obj, await zoom.process(img_zoom)) - if (offset := config.get(CONF_OFFSET_X)) is not None: - lv.img_set_offset_x(w.obj, await size.process(offset)) - if (offset := config.get(CONF_OFFSET_Y)) is not None: - lv.img_set_offset_y(w.obj, await size.process(offset)) - if CONF_ANTIALIAS in config: - lv.img_set_antialias(w.obj, await lv_bool.process(config[CONF_ANTIALIAS])) - if mode := config.get(CONF_MODE): - await w.set_property("size_mode", mode) + await w.set_property(CONF_SRC, await lv_image.process(config.get(CONF_SRC))) + for prop, validator in BASE_IMG_SCHEMA.schema.items(): + await w.set_property(prop, config, processor=validator) img_spec = ImgType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 8afd8d610f0..bb5900b8c92 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -11,8 +11,8 @@ from ..defines import ( ) from ..lv_validation import lv_bool, lv_text from ..schemas import TEXT_SCHEMA -from ..types import LvText, WidgetType -from . import Widget +from ..types import LvText +from . import Widget, WidgetType CONF_LABEL = "label" diff --git a/esphome/components/lvgl/widgets/led.py b/esphome/components/lvgl/widgets/led.py index 647973c9b79..f0092debaa3 100644 --- a/esphome/components/lvgl/widgets/led.py +++ b/esphome/components/lvgl/widgets/led.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED from ..defines import CONF_MAIN -from ..lv_validation import lv_brightness, lv_color +from ..lv_validation import lv_color, lv_percentage from ..lvcode import lv from ..types import LvType from . import Widget, WidgetType @@ -10,7 +10,7 @@ from . import Widget, WidgetType LED_SCHEMA = cv.Schema( { cv.Optional(CONF_COLOR): lv_color, - cv.Optional(CONF_BRIGHTNESS): lv_brightness, + cv.Optional(CONF_BRIGHTNESS): lv_percentage, } ) @@ -23,7 +23,7 @@ class LedType(WidgetType): if (color := config.get(CONF_COLOR)) is not None: lv.led_set_color(w.obj, await lv_color.process(color)) if (brightness := config.get(CONF_BRIGHTNESS)) is not None: - lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) + lv.led_set_brightness(w.obj, await lv_percentage.process(brightness)) led_spec = LedType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index 57cb9657374..a9b202163f9 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -14,6 +14,7 @@ CONF_POINTS = "points" CONF_POINT_LIST_ID = "point_list_id" lv_point_t = cg.global_ns.struct("lv_point_t") +lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") LINE_SCHEMA = { @@ -23,10 +24,7 @@ LINE_SCHEMA = { async def process_coord(coord): if isinstance(coord, Lambda): - coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) - if not coord.endswith("()"): - coord = f"static_cast<lv_coord_t>({coord})" - return cg.RawExpression(coord) + return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) return cg.safe_exp(coord) diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py index f0fdd6d278a..d3398077467 100644 --- a/esphome/components/lvgl/widgets/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -11,8 +11,8 @@ from ..defines import ( ) from ..lv_validation import animated, lv_int from ..lvcode import lv -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget # Note this file cannot be called "bar.py" because that name is disallowed. diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index b7e3af9a788..d45371b3a76 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -1,9 +1,11 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.image import get_image_metadata import esphome.config_validation as cv from esphome.const import ( CONF_COLOR, CONF_COUNT, + CONF_HEIGHT, CONF_ID, CONF_ITEMS, CONF_LENGTH, @@ -13,22 +15,39 @@ from esphome.const import ( CONF_ROTATION, CONF_VALUE, CONF_WIDTH, + CONF_X, ) +from esphome.cpp_generator import MockObj +from esphome.cpp_types import nullptr +from .. import obj_spec, set_obj_properties from ..automation import action_to_code from ..defines import ( + CHILD_ALIGNMENTS, + CONF_ALIGN, + CONF_CONTAINER, CONF_END_VALUE, CONF_INDICATOR, + CONF_LINE_WIDTH, CONF_MAIN, CONF_OPA, CONF_PIVOT_X, CONF_PIVOT_Y, + CONF_RADIUS, + CONF_SCALE, CONF_SRC, CONF_START_VALUE, CONF_TICKS, + LV_OBJ_FLAG, + LV_PART, + LV_SCALE_MODE, + get_remapped_uses, + get_warnings, ) -from ..helpers import add_lv_use, lvgl_components_required +from ..helpers import add_lv_use from ..lv_validation import ( + LV_OPA, + LV_RADIUS, get_end_value, get_start_value, lv_angle_degrees, @@ -36,105 +55,168 @@ from ..lv_validation import ( lv_color, lv_float, lv_image, + lv_int, opacity, + padding, + pixels, + pixels_or_percent, + pixels_or_percent_validator, requires_component, size, ) -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj -from ..types import LvType, ObjUpdateAction -from . import Widget, WidgetType, get_widgets +from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj +from ..schemas import STATE_SCHEMA +from ..styles import LVStyle +from ..types import ( + LV_EVENT, + LvCompound, + LvType, + ObjUpdateAction, + lv_event_t, + lv_img_t, + lv_obj_t, +) +from . import Widget, WidgetType, get_widgets, widget_to_code from .arc import CONF_ARC from .img import CONF_IMAGE from .line import CONF_LINE -from .obj import obj_spec CONF_ANGLE_RANGE = "angle_range" CONF_COLOR_END = "color_end" CONF_COLOR_START = "color_start" +CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top" +CONF_IMAGE_ID = "image_id" CONF_INDICATORS = "indicators" +CONF_LINE_ID = "line_id" CONF_LABEL_GAP = "label_gap" CONF_MAJOR = "major" CONF_METER = "meter" +CONF_PIVOT = "pivot" CONF_R_MOD = "r_mod" +CONF_RADIAL_OFFSET = "radial_offset" CONF_SCALES = "scales" CONF_STRIDE = "stride" CONF_TICK_STYLE = "tick_style" +# LVGL 9.4 Migration: Use scale widget instead of removed meter widget +# +# The lv_meter widget was removed in LVGL 9.4 and replaced with the more +# flexible lv_scale widget. This implementation emulates meter functionality +# using the scale widget with the following mappings: +# +# - lv_meter -> lv_scale (set to LV_SCALE_MODE_ROUND_OUTER for circular meters) +# - lv_meter_scale -> scale configuration (range, ticks, etc.) +# - lv_meter_indicator -> lv_scale_section (colored ranges on the scale) +# + + +# For compatibility, keep meter types but map to scale +lv_scale_t = LvType("lv_obj_t") lv_meter_t = LvType("lv_meter_t") -lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") -lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") - - -def pixels(value): - """A size in one axis in pixels""" - if isinstance(value, str) and value.lower().endswith("px"): - return cv.int_(value[:-2]) - return cv.int_(value) +lv_scale_section_t = LvType("lv_scale_section_t") +lv_meter_indicator_t = LvType("lv_meter_indicator_t") +lv_meter_indicator_ticks_t = LvType( + "lv_scale_section_t", parents=(lv_meter_indicator_t,) +) +lv_meter_indicator_arc_t = LvType("lv_scale_section_t", parents=(lv_meter_indicator_t,)) +lv_meter_indicator_line_t = LvType( + "IndicatorLine", + parents=( + LvCompound, + lv_meter_indicator_t, + ), +) +lv_meter_indicator_image_t = LvType("lv_image_t", parents=(lv_meter_indicator_t,)) +DEFAULT_LABEL_GAP = 10 # Default label gap for major ticks added by LVGL INDICATOR_LINE_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_R_MOD, default=0): size, - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_R_MOD): padding, + cv.Optional(CONF_LENGTH): pixels_or_percent_validator, + cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator, + cv.Optional(CONF_VALUE, default=0.0): lv_float, + cv.Optional(CONF_OPA, default=1.0): opacity, } -) +).add_extra(cv.has_at_most_one_key(CONF_R_MOD, CONF_LENGTH)) + + +class ScaleType(WidgetType): + """ + Will migrate to scale.py in due course + """ + + def __init__(self): + super().__init__( + CONF_SCALE, + lv_scale_t, + (CONF_MAIN, CONF_ITEMS, CONF_INDICATOR), + {}, + is_mock=True, + ) + + +scale_spec = ScaleType() + INDICATOR_IMG_SCHEMA = cv.Schema( { cv.Required(CONF_SRC): lv_image, - cv.Required(CONF_PIVOT_X): pixels, - cv.Required(CONF_PIVOT_Y): pixels, + cv.Optional(CONF_PIVOT_X, default=0): pixels, + cv.Optional(CONF_PIVOT_Y): pixels, cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default=1.0): opacity, } ) INDICATOR_ARC_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_R_MOD, default=0): size, - cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, - cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_R_MOD): padding, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, cv.Optional(CONF_OPA): opacity, } -) +).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) + INDICATOR_TICKS_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR_START, default=0): lv_color, cv.Optional(CONF_COLOR_END): lv_color, - cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, - cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, cv.Optional(CONF_LOCAL, default=False): lv_bool, } -) +).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) + INDICATOR_SCHEMA = cv.Schema( { cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_line_t), } ), cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( INDICATOR_IMG_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t), + cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t), } ), requires_component("image"), ), cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_arc_t), } ), cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_ticks_t), } ), } @@ -142,32 +224,96 @@ INDICATOR_SCHEMA = cv.Schema( SCALE_SCHEMA = cv.Schema( { + cv.GenerateID(): cv.declare_id(lv_scale_t), cv.Optional(CONF_TICKS): cv.Schema( { cv.Optional(CONF_COUNT, default=12): cv.positive_int, - cv.Optional(CONF_WIDTH, default=2): size, + cv.Optional(CONF_WIDTH, default=2): cv.positive_int, cv.Optional(CONF_LENGTH, default=10): size, + cv.Optional(CONF_RADIAL_OFFSET, default=0): size, cv.Optional(CONF_COLOR, default=0x808080): lv_color, cv.Optional(CONF_MAJOR): cv.Schema( { cv.Optional(CONF_STRIDE, default=3): cv.positive_int, cv.Optional(CONF_WIDTH, default=5): size, cv.Optional(CONF_LENGTH, default="15%"): size, + cv.Optional(CONF_RADIAL_OFFSET, default=0): size, cv.Optional(CONF_COLOR, default=0): lv_color, cv.Optional(CONF_LABEL_GAP, default=4): size, } ), } ), - cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, - cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, - cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), - cv.Optional(CONF_ROTATION): lv_angle_degrees, + cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int, + cv.Optional(CONF_RANGE_TO, default=100.0): lv_int, + cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees, + cv.Optional(CONF_ROTATION, default=0): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), + cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } ) -METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} +METER_SCHEMA = { + cv.Optional(CONF_PIVOT): STATE_SCHEMA, + cv.Optional(CONF_INDICATOR): STATE_SCHEMA, + cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA), +} + +LIGHT_STYLE = LVStyle( + "lv_meter_light", + { + "bg_opa": 1.0, + "bg_color": 0xEEEEEE, + "line_width": 1, + "line_color": 0xEEEEEE, + "arc_width": 2, + "arc_color": 0xEEEEEE, + "pad_all": 10, + "border_width": 2, + "border_color": 0xEEEEEE, + "radius": "LV_RADIUS_CIRCLE", + }, +) + +PIVOT_STYLE = { + CONF_RADIUS: LV_RADIUS.CIRCLE, + CONF_ALIGN: CHILD_ALIGNMENTS.CENTER, + "bg_color": 0x000000, + "bg_opa": 1.0, + CONF_WIDTH: 15, + CONF_HEIGHT: 15, +} + + +line_indicator_type = WidgetType( + CONF_INDICATOR, + lv_meter_indicator_line_t, + (CONF_MAIN,), + lv_name=CONF_LINE, + is_mock=True, +) + + +class SectionType(WidgetType): + def __init__(self): + super().__init__( + "scale_section", + lv_meter_indicator_arc_t, + (CONF_MAIN,), + is_mock=True, + lv_name="scale_section", + ) + + +arc_indicator_type = SectionType() + +image_indicator_type = WidgetType( + CONF_INDICATOR, + lv_meter_indicator_image_t, + (CONF_MAIN,), + lv_name=CONF_IMAGE, + is_mock=True, +) class MeterType(WidgetType): @@ -175,111 +321,240 @@ class MeterType(WidgetType): super().__init__( CONF_METER, lv_meter_t, + # Note that mapping from 8.x to 9.x, indicator styling is applied to needles, and tick styling + # is migrated to indicator (CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS), METER_SCHEMA, + lv_name=CONF_CONTAINER, ) - async def to_code(self, w: Widget, config): - """For a meter object, create and set parameters""" + def get_uses(self): + return CONF_SCALE, CONF_LINE - lvgl_components_required.add(CONF_METER) + def validate(self, value): + return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) + + async def on_create(self, var: MockObj, config: dict): + # Remove theme styling from outer container + lv.obj_add_style(var, await LIGHT_STYLE.get_var(), LV_PART.MAIN) + + async def create_to_code(self, config: dict, parent: MockObj): + """For a meter object using scale widget, create and set parameters""" + + add_lv_use(*self.get_uses()) + outer_config = config.copy() + indicator_config = {CONF_INDICATOR: outer_config.pop(CONF_TICKS, {})} + w = await super().create_to_code(outer_config, parent) var = w.obj + + # LVGL 9.4 scale widget setup + # Background style will be applied. for scale_conf in config.get(CONF_SCALES, ()): - rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 - if CONF_ROTATION in scale_conf: - rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) - with LocalVariable( - "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) - ) as meter_var: - lv.meter_set_scale_range( - var, - meter_var, - scale_conf[CONF_RANGE_FROM], - scale_conf[CONF_RANGE_TO], - scale_conf[CONF_ANGLE_RANGE], - rotation, - ) - if ticks := scale_conf.get(CONF_TICKS): - color = await lv_color.process(ticks[CONF_COLOR]) - lv.meter_set_scale_ticks( - var, - meter_var, - ticks[CONF_COUNT], - await size.process(ticks[CONF_WIDTH]), - await size.process(ticks[CONF_LENGTH]), - color, + scale_var = cg.Pvariable(scale_conf[CONF_ID], lv_expr.scale_create(var)) + percent100 = await pixels_or_percent.process(1.0) + lv_obj.set_style_height(scale_var, percent100, LV_PART.MAIN) + lv_obj.set_style_width(scale_var, percent100, LV_PART.MAIN) + lv_obj.set_style_align(scale_var, CHILD_ALIGNMENTS.CENTER, LV_PART.MAIN) + lv_obj.set_style_bg_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) + lv_obj.set_style_radius(scale_var, LV_RADIUS.CIRCLE, 0) + await set_obj_properties(Widget(scale_var, scale_spec), indicator_config) + + lv.scale_set_mode(scale_var, LV_SCALE_MODE.ROUND_INNER) + # Set the scale range + range_from = await lv_int.process(scale_conf[CONF_RANGE_FROM]) + range_to = await lv_int.process(scale_conf[CONF_RANGE_TO]) + lv.scale_set_range(scale_var, range_from, range_to) + + angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE]) + rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) + # Set angle range + lv.scale_set_angle_range( + scale_var, + angle_range, + ) + + # Set rotation if specified + if rotation: + lv.scale_set_rotation(scale_var, rotation) + + # Handle indicators as sections + for indicator in scale_conf.get(CONF_INDICATORS, ()): + (t, v) = next(iter(indicator.items())) + iid = v[CONF_ID] + + # Enable getting the meter to which this belongs. + + # Set section range based on indicator values + start_value = await get_start_value(v) or scale_conf[CONF_RANGE_FROM] + end_value = await get_end_value(v) or scale_conf[CONF_RANGE_TO] + + # Create and apply styles based on indicator type + if t == CONF_ARC: + props = { + "arc_width": v[CONF_WIDTH], + "arc_color": v[CONF_COLOR], + "arc_rounded": v.get("arc_rounded", False), + } + if (opa := v.get(CONF_OPA)) is not None: + props["arc_opa"] = opa + if CONF_R_MOD in v: + get_warnings().add( + "The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored." + ) + arc_style = LVStyle(f"meter_arc_{iid.id}", props) + tvar = cg.Pvariable(iid, lv_expr.scale_add_section(scale_var)) + lv.scale_section_set_style( + tvar, LV_PART.MAIN, await arc_style.get_var() ) - if CONF_MAJOR in ticks: - major = ticks[CONF_MAJOR] - lv.meter_set_scale_major_ticks( - var, - meter_var, - major[CONF_STRIDE], - await size.process(major[CONF_WIDTH]), - await size.process(major[CONF_LENGTH]), - await lv_color.process(major[CONF_COLOR]), - await size.process(major[CONF_LABEL_GAP]), - ) - for indicator in scale_conf.get(CONF_INDICATORS, ()): - (t, v) = next(iter(indicator.items())) - iid = v[CONF_ID] - ivar = cg.Pvariable(iid, cg.nullptr, type_=lv_meter_indicator_t) - # Enable getting the meter to which this belongs. - wid = Widget.create(iid, var, obj_spec, v) - wid.obj = ivar - if t == CONF_LINE: - color = await lv_color.process(v[CONF_COLOR]) - lv_assign( - ivar, - lv_expr.meter_add_needle_line( - var, - meter_var, - await size.process(v[CONF_WIDTH]), - color, - await size.process(v[CONF_R_MOD]), - ), - ) - if t == CONF_ARC: - color = await lv_color.process(v[CONF_COLOR]) - lv_assign( - ivar, - lv_expr.meter_add_arc( - var, - meter_var, - await size.process(v[CONF_WIDTH]), - color, - await size.process(v[CONF_R_MOD]), - ), - ) - if t == CONF_TICK_STYLE: - color_start = await lv_color.process(v[CONF_COLOR_START]) - color_end = await lv_color.process( - v.get(CONF_COLOR_END) or color_start - ) - lv_assign( - ivar, - lv_expr.meter_add_scale_lines( - var, - meter_var, + lw = Widget(tvar, arc_indicator_type) + await set_indicator_values(lw, v) + + if t == CONF_TICK_STYLE: + # No object created for this + color_start = await lv_color.process(v[CONF_COLOR_START]) + color_end = await lv_color.process(v[CONF_COLOR_END]) + local = v[CONF_LOCAL] + if color_start and color_end: + async with LambdaContext( + [(lv_event_t.operator("ptr"), "e")] + ) as lambda_: + lv.scale_draw_event_cb( + lambda_.get_parameter(0), + start_value, + end_value, color_start, color_end, - v[CONF_LOCAL], - await size.process(v[CONF_WIDTH]), - ), + local, + ) + lv_obj.add_event_cb( + scale_var, + await lambda_.get_lambda(), + LV_EVENT.DRAW_TASK_ADDED, + nullptr, ) - if t == CONF_IMAGE: - add_lv_use("img") - lv_assign( - ivar, - lv_expr.meter_add_needle_img( - var, - meter_var, - await lv_image.process(v[CONF_SRC]), - v[CONF_PIVOT_X], - v[CONF_PIVOT_Y], - ), - ) - await set_indicator_values(var, ivar, v) + lv.obj_add_flag(scale_var, LV_OBJ_FLAG.SEND_DRAW_TASK_EVENTS) + + if t == CONF_LINE: + # Needle represented by a line + if CONF_LENGTH in v: + length = v[CONF_LENGTH] + elif r_mod := v.get(CONF_R_MOD): + get_remapped_uses().add(CONF_R_MOD) + length = -abs(r_mod) + else: + length = 1.0 + props = { + CONF_ID: v[CONF_ID], + CONF_OPA: v[CONF_OPA], + CONF_LINE_WIDTH: v[CONF_WIDTH], + "line_color": v[CONF_COLOR], + "line_rounded": True, + CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT, + CONF_LENGTH: length, + CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET], + } + lw = await widget_to_code(props, line_indicator_type, scale_var) + await set_indicator_values(lw, v) + + if t == CONF_IMAGE: + add_lv_use(CONF_IMAGE) + src = v[CONF_SRC] + src_data = get_image_metadata(src.id) + pivot_x = await pixels.process(v[CONF_PIVOT_X]) + pivot_y = await pixels.process( + v.get(CONF_PIVOT_Y, src_data.height // 2) + ) + props = { + CONF_X: src_data.width // 2 - pivot_x, + "transform_pivot_x": pivot_x, + "transform_pivot_y": pivot_y, + CONF_SRC: src, + CONF_OPA: v[CONF_OPA], + CONF_ID: v[CONF_ID], + CONF_ALIGN: CHILD_ALIGNMENTS.CENTER, + } + iw = await widget_to_code(props, image_indicator_type, scale_var) + await iw.set_property(CONF_SRC, await lv_image.process(src)) + await set_indicator_values(iw, v) + + if ticks := scale_conf.get(CONF_TICKS): + # Set total tick count + lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT]) + lv.scale_set_draw_ticks_on_top( + scale_var, scale_conf[CONF_DRAW_TICKS_ON_TOP] + ) + + # Set tick styling + lv_obj.set_style_length( + scale_var, await size.process(ticks[CONF_LENGTH]), LV_PART.ITEMS + ) + lv_obj.set_style_line_width( + scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS + ) + lv_obj.set_style_radial_offset( + scale_var, + await size.process(ticks[CONF_RADIAL_OFFSET]), + LV_PART.ITEMS, + ) + lv_obj.set_style_line_color( + scale_var, + await lv_color.process(ticks[CONF_COLOR]), + LV_PART.ITEMS, + ) + + # Hide the scale line + lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) + if CONF_MAJOR in ticks: + major = ticks[CONF_MAJOR] + # Set major tick frequency + lv.scale_set_major_tick_every(scale_var, major[CONF_STRIDE]) + + # Enable labels for major ticks + lv.scale_set_label_show(scale_var, True) + + # Set major tick styling + lv_obj.set_style_length( + scale_var, + await size.process(major[CONF_LENGTH]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_radial_offset( + scale_var, + await size.process(ticks[CONF_RADIAL_OFFSET]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_line_width( + scale_var, + await size.process(major[CONF_WIDTH]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_line_color( + scale_var, + await lv_color.process(major[CONF_COLOR]), + LV_PART.INDICATOR, + ) + + # Set label gap (padding) + label_gap = await size.process(major[CONF_LABEL_GAP]) + if isinstance(label_gap, int): + label_gap -= DEFAULT_LABEL_GAP + lv_obj.set_style_pad_radial( + scale_var, + label_gap, + LV_PART.INDICATOR, + ) + else: + lv.scale_set_major_tick_every(scale_var, 0) + else: + lv.scale_set_total_tick_count(scale_var, 0) + + # Add a pivot + # Get the default style + pivot_style = PIVOT_STYLE.copy() + pivot_style.update(config.get(CONF_INDICATOR, config.get(CONF_PIVOT, {}))) + with LocalVariable("pivot", lv_obj_t, lv_expr.container_create(var)) as pivot: + pw = Widget(pivot, obj_spec, pivot_style) + await set_obj_properties(pw, pivot_style) meter_spec = MeterType() @@ -303,23 +578,39 @@ async def indicator_update_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) async def set_value(w: Widget): - await set_indicator_values(w.var, w.obj, config) + await set_indicator_values(w, config) return await action_to_code( widget, set_value, action_id, template_arg, args, config ) -async def set_indicator_values(meter, indicator, config): +async def set_indicator_values(indicator: Widget, config): + """Update scale section values (replaces meter indicator values)""" start_value = await get_start_value(config) end_value = await get_end_value(config) - if start_value is not None: - if end_value is None: - lv.meter_set_indicator_value(meter, indicator, start_value) - else: - lv.meter_set_indicator_start_value(meter, indicator, start_value) - if end_value is not None: - lv.meter_set_indicator_end_value(meter, indicator, end_value) - if (opa := config.get(CONF_OPA)) is not None: - lv_assign(indicator.opa, await opacity.process(opa)) - lv_obj.invalidate(meter) + if indicator.type is arc_indicator_type: + # For scale sections, we update the range + if start_value is not None and end_value is not None: + lv.scale_section_set_range(indicator.obj, start_value, end_value) + elif start_value is not None: + # If only start value, use it as both start and end (single point) + lv.scale_section_set_range(indicator.obj, start_value, start_value) + elif end_value is not None: + # If only end value, assume range from 0 to end_value + lv.scale_section_set_range(indicator.obj, 0, end_value) + return + + if start_value is None: + return + if indicator.type is line_indicator_type: + # Line needle + lv_add(indicator.var.set_value(start_value)) + return + if indicator.type is image_indicator_type: + # Needle represented by an image + lv_obj.set_style_transform_rotation( + indicator.obj, + lv.get_needle_angle_for_value(indicator.obj, start_value) * 10, + LV_PART.MAIN, + ) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index 82b24423785..af27ee75531 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -1,7 +1,7 @@ -from esphome import config_validation as cv -from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT +from esphome import codegen as cg, config_validation as cv +from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT from esphome.core import ID -from esphome.cpp_generator import new_Pvariable, static_const_array +from esphome.cpp_generator import MockObjClass from esphome.cpp_types import nullptr from ..defines import ( @@ -9,40 +9,82 @@ from ..defines import ( CONF_BUTTON_STYLE, CONF_BUTTONS, CONF_CLOSE_BUTTON, + CONF_HEADER_BUTTONS, + CONF_MAIN, CONF_MSGBOXES, + CONF_SRC, CONF_TITLE, + LV_OBJ_FLAG, TYPE_FLEX, + add_warning, literal, ) -from ..helpers import add_lv_use, lvgl_components_required -from ..lv_validation import lv_bool, lv_pct, lv_text -from ..lvcode import ( - EVENT_ARG, - LambdaContext, - LocalVariable, - lv, - lv_add, - lv_assign, - lv_expr, - lv_obj, - lv_Pvariable, -) -from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema -from ..types import LV_EVENT, char_ptr, lv_obj_t -from . import Widget, add_widgets, set_obj_properties -from .button import button_spec -from .buttonmatrix import ( - BUTTONMATRIX_BUTTON_SCHEMA, - CONF_BUTTON_TEXT_LIST_ID, - buttonmatrix_spec, - get_button_data, - lv_buttonmatrix_t, - set_btn_data, +from ..helpers import add_lv_use +from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent +from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj +from ..schemas import ( + STYLE_SCHEMA, + STYLED_TEXT_SCHEMA, + TEXT_SCHEMA, + container_schema, + part_schema, ) +from ..styles import LVStyle +from ..types import LV_EVENT, lv_obj_t +from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code +from .button import button_spec, lv_button_t from .label import CONF_LABEL from .obj import obj_spec CONF_MSGBOX = "msgbox" + +OUTER_STYLE = LVStyle( + "msgbox_outer", + { + "bg_opa": 128, + "bg_color": "black", + "border_width": 0, + "pad_all": 0, + "radius": 0, + }, +) + + +class FooterButtonType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTON, lv_button_t, (CONF_MAIN,), TEXT_SCHEMA, is_mock=True + ) + + async def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.msgbox_add_footer_button(parent, config[CONF_TEXT]) + + +footer_button_spec = FooterButtonType() + + +class HeaderButtonType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTON, + lv_button_t, + (CONF_MAIN,), + cv.Schema( + { + cv.Required(CONF_SRC): lv_image, + } + ), + is_mock=True, + ) + + async def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.msgbox_add_header_button( + parent, await lv_image.process(config[CONF_SRC]) + ) + + +header_button_spec = HeaderButtonType() + MSGBOX_SCHEMA = container_schema( obj_spec, STYLE_SCHEMA.extend( @@ -50,10 +92,14 @@ MSGBOX_SCHEMA = container_schema( cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, - cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), - cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts), + cv.Optional(CONF_BUTTONS): cv.ensure_list( + container_schema(footer_button_spec) + ), + cv.Optional(CONF_HEADER_BUTTONS): cv.ensure_list( + container_schema(header_button_spec) + ), cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, - cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + cv.Optional(CONF_BUTTON_STYLE): part_schema(button_spec.parts), } ), ) @@ -62,7 +108,9 @@ MSGBOX_SCHEMA = container_schema( async def msgbox_to_code(top_layer, conf): """ Construct a message box. This consists of a full-screen translucent background enclosing a centered container - with an optional title, body, close button and a button matrix. And any other widgets the user cares to add + with an optional title, body, close button and a set of footer buttons. + Header buttons can be added - they can be image buttons only. + The body of the message box may have any widgets the user wants to add. :param conf: The config data :return: code to add to the init lambda """ @@ -71,60 +119,42 @@ async def msgbox_to_code(top_layer, conf): CONF_BUTTON, CONF_LABEL, CONF_MSGBOX, - *buttonmatrix_spec.get_uses(), *button_spec.get_uses(), ) - lvgl_components_required.add("BUTTONMATRIX") - messagebox_id = conf[CONF_ID] - outer_id = f"{messagebox_id.id}_outer" - outer = lv_Pvariable(lv_obj_t, messagebox_id.id + "_outer") - buttonmatrix = new_Pvariable( - ID( - f"{messagebox_id.id}_buttonmatrix_", - is_declaration=True, - type=lv_buttonmatrix_t, + if CONF_BUTTON_STYLE in conf: + add_warning( + "'button_style' for msgbox is deprecated - style the buttons directly." ) - ) - msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id) - outer_widget = Widget.create(outer_id, outer, obj_spec, conf) + messagebox_id = conf[CONF_ID] + outer_id = ID(f"{messagebox_id.id}_outer", type=lv_obj_t) + outer = cg.Pvariable(outer_id, lv_expr.obj_create(top_layer)) + outer_widget = Widget.create(outer_id.id, outer, obj_spec, conf) + msgbox = cg.Pvariable(messagebox_id, lv_expr.msgbox_create(outer)) outer_widget.move_to_foreground = True msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf) msgbox_widget.outer = outer_widget - buttonmatrix_widget = Widget.create( - str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf - ) - text_list, ctrl_list, width_list, _ = await get_button_data( - (conf,), buttonmatrix_widget - ) - text_id = conf[CONF_BUTTON_TEXT_LIST_ID] - text_list = static_const_array(text_id, text_list) text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, "")) title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, "")) close_button = conf[CONF_CLOSE_BUTTON] - lv_assign(outer, lv_expr.obj_create(top_layer)) - lv_obj.set_width(outer, lv_pct(100)) - lv_obj.set_height(outer, lv_pct(100)) - lv_obj.set_style_bg_opa(outer, 128, 0) - lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) - lv_obj.set_style_border_width(outer, 0, 0) - lv_obj.set_style_pad_all(outer, 0, 0) - lv_obj.set_style_radius(outer, 0, 0) - outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") - lv_assign( - msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) - ) + percent100 = await pixels_or_percent.process(1.0) + lv_obj.set_size(outer, percent100, percent100) + outer_widget.add_style(await OUTER_STYLE.get_var()) + outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) + lv.msgbox_add_title(msgbox, title) + lv.msgbox_add_text(msgbox, text) lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) - lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) - if button_style := conf.get(CONF_BUTTON_STYLE): - button_style = {CONF_ITEMS: button_style} - await set_obj_properties(buttonmatrix_widget, button_style) await set_obj_properties(msgbox_widget, conf) await add_widgets(msgbox_widget, conf) + for button in conf.get(CONF_BUTTONS, ()): + await widget_to_code(button, footer_button_spec, msgbox) + for button in conf.get(CONF_HEADER_BUTTONS, ()): + await widget_to_code(button, header_button_spec, msgbox) + async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: - outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) if close_button: with LocalVariable( - "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) + "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) ) as close_btn: lv_obj.remove_event_cb(close_btn, nullptr) lv_obj.add_event_cb( @@ -138,9 +168,6 @@ async def msgbox_to_code(top_layer, conf): outer, await close_action.get_lambda(), LV_EVENT.CLICKED, nullptr ) - if len(ctrl_list) != 0 or len(width_list) != 0: - set_btn_data(buttonmatrix.obj, ctrl_list, width_list) - async def msgboxes_to_code(lv_component, config): top_layer = lv.disp_get_layer_top(lv_component.get_disp()) diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py index ab22a5ce866..079a0734ef5 100644 --- a/esphome/components/lvgl/widgets/obj.py +++ b/esphome/components/lvgl/widgets/obj.py @@ -1,5 +1,6 @@ from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR -from ..types import WidgetType, lv_obj_t +from ..types import lv_obj_t +from . import WidgetType class ObjType(WidgetType): diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index ad46f67c6b6..82c43705433 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -1,14 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT -from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN -from ..lv_validation import lv_color, lv_text -from ..lvcode import LocalVariable, lv, lv_expr +from ..defines import CONF_MAIN, get_color_formats +from ..lv_validation import color, lv_color, lv_int, lv_text +from ..lvcode import LocalVariable, lv from ..schemas import TEXT_SCHEMA -from ..types import WidgetType, lv_obj_t -from . import Widget +from ..types import lv_obj_t +from . import Widget, WidgetType +from .canvas import CONF_CANVAS +from .img import CONF_IMAGE CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" @@ -16,9 +17,16 @@ CONF_LIGHT_COLOR = "light_color" QRCODE_SCHEMA = { **TEXT_SCHEMA, - cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, - cv.Required(CONF_SIZE): cv.int_, + cv.Optional(CONF_DARK_COLOR, default="black"): color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): color, + cv.Required(CONF_SIZE): lv_int, +} + +QRCODE_MODIFY_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_DARK_COLOR): lv_color, + cv.Optional(CONF_LIGHT_COLOR): lv_color, + cv.Optional(CONF_SIZE): lv_int, } @@ -29,20 +37,25 @@ class QrCodeType(WidgetType): lv_obj_t, (CONF_MAIN,), QRCODE_SCHEMA, - modify_schema=TEXT_SCHEMA, + modify_schema=QRCODE_MODIFY_SCHEMA, ) def get_uses(self): - return "canvas", "img", "label" - - async def obj_creator(self, parent: MockObjClass, config: dict): - dark_color = await lv_color.process(config[CONF_DARK_COLOR]) - light_color = await lv_color.process(config[CONF_LIGHT_COLOR]) - size = config[CONF_SIZE] - return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) + return CONF_CANVAS, CONF_IMAGE async def to_code(self, w: Widget, config): + get_color_formats().add("ARGB8888") + await w.set_property( + CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) + ) + await w.set_property( + CONF_DARK_COLOR, await lv_color.process(config.get(CONF_DARK_COLOR)) + ) + await w.set_property(CONF_SIZE, await lv_int.process(config.get(CONF_SIZE))) if (value := config.get(CONF_TEXT)) is not None: + if isinstance(value, str): + lv.qrcode_update(w.obj, value, len(value)) + return value = await lv_text.process(value) with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) diff --git a/esphome/components/lvgl/widgets/slider.py b/esphome/components/lvgl/widgets/slider.py index d5017668e44..85096c302a3 100644 --- a/esphome/components/lvgl/widgets/slider.py +++ b/esphome/components/lvgl/widgets/slider.py @@ -2,18 +2,18 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE from ..defines import ( - BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_KNOB, CONF_MAIN, + SLIDER_MODES, literal, ) -from ..helpers import add_lv_use from ..lv_validation import animated, get_start_value, lv_float from ..lvcode import lv -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget +from .label import CONF_LABEL from .lv_bar import CONF_BAR CONF_SLIDER = "slider" @@ -29,7 +29,7 @@ SLIDER_SCHEMA = cv.Schema( cv.Optional(CONF_VALUE): lv_float, cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_MODE, default="NORMAL"): SLIDER_MODES.one_of, cv.Optional(CONF_ANIMATED, default=True): animated, } ) @@ -49,8 +49,10 @@ class SliderType(NumberType): def animated(self): return True + def get_uses(self): + return (CONF_BAR, CONF_LABEL) + async def to_code(self, w: Widget, config): - add_lv_use(CONF_BAR) if CONF_MIN_VALUE in config: # not modify case lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py index 83aac25a599..0e409ba3b79 100644 --- a/esphome/components/lvgl/widgets/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -1,9 +1,8 @@ import esphome.config_validation as cv -from esphome.cpp_generator import MockObjClass from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME from ..lv_validation import lv_angle_degrees, lv_milliseconds -from ..lvcode import lv_expr +from ..lvcode import lv from ..types import LvType from . import Widget, WidgetType from .arc import CONF_ARC @@ -12,8 +11,10 @@ CONF_SPINNER = "spinner" SPINNER_SCHEMA = cv.Schema( { - cv.Required(CONF_ARC_LENGTH): lv_angle_degrees, - cv.Required(CONF_SPIN_TIME): lv_milliseconds, + cv.Optional(CONF_ARC_LENGTH, default=200): cv.All( + lv_angle_degrees, cv.int_range(min=0, max=360) + ), + cv.Optional(CONF_SPIN_TIME, default="2s"): lv_milliseconds, } ) @@ -25,19 +26,17 @@ class SpinnerType(WidgetType): LvType("lv_spinner_t"), (CONF_MAIN, CONF_INDICATOR), SPINNER_SCHEMA, - {}, ) async def to_code(self, w: Widget, config): - return [] + spin_time = await lv_milliseconds.process(config.get(CONF_SPIN_TIME)) + arc_length = int(config[CONF_ARC_LENGTH]) + if arc_length < 180: + arc_length += 180 + lv.spinner_set_anim_params(w.obj, spin_time, arc_length) def get_uses(self): return (CONF_ARC,) - async def obj_creator(self, parent: MockObjClass, config: dict): - spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME]) - arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH]) - return lv_expr.call("spinner_create", parent, spin_time, arc_length) - spinner_spec = SpinnerType() diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index cd7cf7b4716..60ba664f047 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -1,8 +1,14 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE -from esphome.cpp_generator import MockObjClass +from esphome.const import ( + CONF_ID, + CONF_INDEX, + CONF_ITEMS, + CONF_NAME, + CONF_POSITION, + CONF_SIZE, +) from ..automation import action_to_code from ..defines import ( @@ -15,10 +21,11 @@ from ..defines import ( literal, ) from ..lv_validation import animated, lv_int, size -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .button import button_spec from .buttonmatrix import buttonmatrix_spec from .obj import obj_spec @@ -69,6 +76,10 @@ class TabviewType(WidgetType): return "btnmatrix", TYPE_FLEX async def to_code(self, w: Widget, config: dict): + await w.set_property( + "tab_bar_position", await DIRECTIONS.process(config[CONF_POSITION]) + ) + await w.set_property("tab_bar_size", await size.process(config[CONF_SIZE])) for tab_conf in config[CONF_TABS]: w_id = tab_conf[CONF_ID] tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) @@ -76,25 +87,27 @@ class TabviewType(WidgetType): lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) await set_obj_properties(tab_widget, tab_conf) await add_widgets(tab_widget, tab_conf) - if button_style := config.get(CONF_TAB_STYLE): + tab_style = config.get(CONF_TAB_STYLE, {}) + tab_items_style = tab_style.get(CONF_ITEMS, {}) + if tab_style: with LocalVariable( - "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) - ) as btnmatrix_obj: - await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) + "tabview_bar", lv_obj_t, rhs=lv_expr.tabview_get_tab_bar(w.obj) + ) as bar_obj: + tab_bar = Widget(bar_obj, obj_spec) + await set_obj_properties(tab_bar, tab_style) + if tab_items_style: + for index, tab_conf in enumerate(config[CONF_TABS]): + await set_obj_properties( + Widget(lv_obj.get_child(bar_obj, index), button_spec), + tab_items_style, + ) + if content_style := config.get(CONF_CONTENT_STYLE): with LocalVariable( "tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj) ) as content_obj: await set_obj_properties(Widget(content_obj, obj_spec), content_style) - async def obj_creator(self, parent: MockObjClass, config: dict): - return lv_expr.call( - "tabview_create", - parent, - await DIRECTIONS.process(config[CONF_POSITION]), - await size.process(config[CONF_SIZE]), - ) - tabview_spec = TabviewType() @@ -117,6 +130,6 @@ async def tabview_select(config, action_id, template_arg, args): async def do_select(w: Widget): lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) - lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + lv.obj_send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) return await action_to_code(widget, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 430a386d2e2..dadaef7d07e 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -15,7 +15,7 @@ from ..defines import ( TILE_DIRECTIONS, literal, ) -from ..lv_validation import animated, lv_int, lv_pct +from ..lv_validation import animated, lv_int, pixels_or_percent from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr @@ -68,17 +68,19 @@ class TileviewType(WidgetType): w_id = tile_conf[CONF_ID] tile_obj = lv_Pvariable(lv_obj_t, w_id) tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) - dirs = tile_conf[CONF_DIR] - if isinstance(dirs, list): - dirs = "|".join(dirs) + dirs = await TILE_DIRECTIONS.process(tile_conf[CONF_DIR]) row_pos = tile_conf[CONF_ROW] col_pos = tile_conf[CONF_COLUMN] lv_assign( tile_obj, - lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)), + lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, dirs), ) # Bugfix for LVGL 8.x - lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100)) + lv_obj.set_pos( + tile_obj, + await pixels_or_percent.process(float(col_pos)), + await pixels_or_percent.process(float(row_pos)), + ) await set_obj_properties(tile, tile_conf) await add_widgets(tile, tile_conf) if tiles: diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 35a9de3537e..292e2bb3bb2 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -5,12 +5,14 @@ import esphome.codegen as cg from esphome.components import runtime_image from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TRIGGER_ID, + CONF_TYPE, CONF_URL, ) from esphome.core import Lambda @@ -131,6 +133,13 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): # Use the enhanced helper function to get all runtime image parameters settings = await runtime_image.process_runtime_image_config(config) + add_metadata( + config[CONF_ID], + settings.width, + settings.height, + config[CONF_TYPE], + config[CONF_TRANSPARENCY], + ) url = config[CONF_URL] var = cg.new_Pvariable( diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 3ae35cc5f17..8db69aa53e3 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -181,7 +181,8 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) # Get byte order (True for big endian, False for little endian) - byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + # If unspecified, use little endian + byte_order_big_endian = config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN" # Get placeholder if specified placeholder = None diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 5a4a2ea7d69..2ebe67c3a55 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -106,7 +106,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { break; } case image::IMAGE_TYPE_RGB565: { - uint32_t pos = this->get_position_(x, y); + const size_t pos = (x + y * this->buffer_width_) * 2; Color mapped_color = color; this->map_chroma_key(mapped_color); uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); @@ -118,7 +118,8 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF); } if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; + const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2; + this->buffer_[alpha_pos] = color.w; } break; } @@ -283,6 +284,10 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { } size_t RuntimeImage::get_buffer_size_(int width, int height) const { + if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + // Add extra alpha channel for RGB565 with alpha + return width * height * 3; + } return (this->get_bpp() * width + 7u) / 8u * height; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 09269ea1b4d..c817f8ef27a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -92,6 +92,7 @@ #define USE_LVGL_MSGBOX #define USE_LVGL_ROLLER #define USE_LVGL_ROTARY_ENCODER +#define USE_LVGL_SCALE #define USE_LVGL_SLIDER #define USE_LVGL_SPAN #define USE_LVGL_SPINBOX diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 620dc6131dd..5cfa8532ff5 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -77,3 +77,5 @@ dependencies: - if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]" esp32async/asynctcp: version: 3.4.91 + lvgl/lvgl: + version: 9.5.0 diff --git a/platformio.ini b/platformio.ini index 3c3d62ef76a..c5a4c630dff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -42,7 +42,6 @@ lib_deps_base = https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library - lvgl/lvgl@8.4.0 ; lvgl lib_deps = ${common.lib_deps_base} @@ -120,6 +119,7 @@ lib_deps = ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) droscy/esp_wireguard@0.4.2 ; wireguard + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} @@ -204,6 +204,7 @@ lib_deps = ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -221,6 +222,7 @@ lib_deps = bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.2 ; wireguard + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY @@ -242,6 +244,7 @@ build_flags = lib_deps = ${common.lib_deps_base} bblanchon/ArduinoJson@7.4.2 ; json + lvgl/lvgl@9.5.0 ; lvgl ; All the actual environments are defined below. diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 3635fc710f5..7d96b12a019 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -42,6 +42,11 @@ lvgl: disp_bg_color: color_id disp_bg_image: cat_image disp_bg_opa: cover + bottom_layer: + widgets: + - obj: + bg_color: 0x000000 + bg_opa: cover theme: obj: border_width: 1 @@ -49,12 +54,14 @@ lvgl: gradients: - id: color_bar direction: hor - dither: err_diff + # dither: err_diff stops: - color: 0xFF0000 position: 0 + opa: 100% - color: 0xFFFF00 position: 42 + opa: 80% - color: 0x00FF00 position: 84 - color: 0x00FFFF @@ -156,6 +163,16 @@ lvgl: offset_x: !lambda return 20; offset_y: !lambda return 20; antialias: !lambda return true; + - id: msgbox_with_header_buttons + title: Header Buttons Test + body: + text: Testing header buttons + header_buttons: + - src: cat_image + on_click: + logger.log: Header button clicked + buttons: + - text: OK - id: simple_msgbox title: Simple @@ -199,11 +216,11 @@ lvgl: text: Unloaded - lvgl.label.update: id: msgbox_label - text: "" # Empty text + text: "" # Empty text on_all_events: logger.log: format: "Event %s" - args: ['lv_event_code_name_for(event->code).c_str()'] + args: ['lv_event_code_name_for(event).c_str()'] skip: true layout: type: Flex @@ -241,7 +258,7 @@ lvgl: on_all_events: - logger.log: format: "Event %s" - args: ['lv_event_code_name_for(event->code).c_str()'] + args: ['lv_event_code_name_for(event).c_str()'] - lvgl.animimg.update: id: anim_img src: !lambda "return {dog_image, cat_image};" @@ -306,7 +323,6 @@ lvgl: anim_time: 1s bg_color: light_blue bg_grad_color: light_blue - bg_dither_mode: ordered bg_grad_dir: hor bg_grad_stop: 128 bg_image_opa: transp @@ -354,10 +370,18 @@ lvgl: text_line_space: 4 text_opa: cover transform_angle: 180 + transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% transform_pivot_y: 50% transform_zoom: 0.5 + transform_scale: 2.0 + transform_scale_x: 1.5 + transform_scale_y: 0.8 + transform_skew_x: 10 + transform_skew_y: 20 + shadow_offset_x: 3 + shadow_offset_y: 3 translate_x: 10 translate_y: 10 max_height: 100 @@ -549,14 +573,16 @@ lvgl: arc_length: 120 spin_time: 2s align: left_mid + - spinner: + align: right_mid + send_draw_task_events: true - image: id: lv_image src: cat_image align: top_left y: "50" - mode: real - zoom: 2.0 - angle: 45 + scale: 2.0 + rotation: 45 - tileview: id: tileview_id scrollbar_mode: active @@ -661,8 +687,11 @@ lvgl: src: cat_image x: 100 y: 100 - angle: 90 - zoom: 2.0 + rotation: 90 + scale_x: 2.0 + scale_y: 1.5 + skew_x: 10 + skew_y: 5 pivot_x: 25 pivot_y: 25 - lvgl.canvas.draw_line: @@ -710,6 +739,9 @@ lvgl: text: format: "A string with a number %d" args: ['(int)(random_uint32() % 1000)'] + size: 120 + dark_color: navy + light_color: white - slider: min_value: 0 @@ -928,7 +960,6 @@ lvgl: grid_cell_row_pos: 0 grid_cell_column_pos: 0 src: !lambda return dog_image; - mode: virtual on_click: then: - lvgl.tabview.select: @@ -1023,10 +1054,18 @@ lvgl: text_color: 0xFFFFFF scales: - ticks: - width: !lambda return 1; + width: 1 count: 61 length: 20% + radial_offset: 5 color: 0xFFFFFF + major: + stride: 5 + width: 2 + length: 8 + color: 0xC0C0C0 + radial_offset: 3 + label_gap: 6 range_from: 0 range_to: 60 angle_range: 360 @@ -1037,15 +1076,15 @@ lvgl: end_value: 60 color_start: 0x0000bd color_end: 0xbd0000 - width: !lambda return 1; + width: 1 - line: opa: 50% id: minute_hand color: 0xFF0000 - r_mod: !lambda return -1; - width: !lambda return 3; - - - angle_range: 330 + length: 99% + radial_offset: 2 + width: 1 + - angle_range: 330 rotation: 300 range_from: 1 range_to: 12 @@ -1069,7 +1108,7 @@ lvgl: value: 180 width: 4 color: 0xA0A0A0 - r_mod: -20 + length: 80% opa: 0% - id: page3 layout: Horizontal From c2c50ceea7124d189785493f27ae3b99cb185243 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:10:01 +0100 Subject: [PATCH 0808/2056] [substitutions] substitutions pass and !include redesign (package refactor part 2a) (#14917) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/yaml_util.py | 52 +++++++++++- tests/unit_tests/test_yaml_util.py | 126 +++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index bba4bbf4872..d0eab4e44ee 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -24,6 +24,7 @@ except ImportError: from esphome import core from esphome.config_helpers import Extend, Remove +from esphome.const import CONF_DEFAULTS from esphome.core import ( CORE, DocumentRange, @@ -88,6 +89,47 @@ def make_data_base( return value +class ConfigContext: + """This is a mixin class that holds substitution vars that should be applied + to the tagged node and its children. During configuration loading, context vars can + be added to nodes using `add_context` function, which applies the mixin storing + the captured values and unevaluated expressions. + The substitution pass then recreates the effective context by merging the context vars + from this node and parent nodes. + """ + + @property + def vars(self) -> dict[str, Any]: + return self._context_vars + + def set_context(self, vars: dict[str, Any]) -> None: + # pylint: disable=attribute-defined-outside-init + self._context_vars = vars + + +def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: + """Tags a list/string/dict value with context vars that must be applied to it and its children + during the substitution pass. If no vars are given, no tagging is done. + If the value is already tagged, the new context vars are merged with existing ones, + with new vars taking precedence. Returns the value tagged with ConfigContext. Returns + the original value if value is not a list/string/dict. + """ + if isinstance(value, dict) and CONF_DEFAULTS in value: + context_vars = { + **value.pop(CONF_DEFAULTS), + **(context_vars or {}), + } + + if isinstance(value, ConfigContext): + value.set_context({**value.vars, **(context_vars or {})}) + return value + + if context_vars and isinstance(value, (dict, list, str)): + value = add_class_to_obj(value, ConfigContext) + value.set_context(context_vars) + return value + + def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): @@ -455,7 +497,7 @@ def parse_yaml( def substitute_vars(config, vars): from esphome.components import substitutions - from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS + from esphome.const import CONF_SUBSTITUTIONS org_subs = None result = config @@ -612,6 +654,12 @@ class ESPHomeDumper(yaml.SafeDumper): return self.represent_secret(value.value) return self.represent_scalar(tag="!lambda", value=value.value, style="|") + def represent_extend(self, value): + return self.represent_scalar(tag="!extend", value=value.value) + + def represent_remove(self, value): + return self.represent_scalar(tag="!remove", value=value.value) + def represent_id(self, value): if is_secret(value.id): return self.represent_secret(value.id) @@ -638,6 +686,8 @@ ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_string ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) +ESPHomeDumper.add_multi_representer(Extend, ESPHomeDumper.represent_extend) +ESPHomeDumper.add_multi_representer(Remove, ESPHomeDumper.represent_remove) ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id) ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index c8cb3e144f3..adb7658bfd6 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -6,6 +6,7 @@ import pytest from esphome import core, yaml_util from esphome.components import substitutions +from esphome.config_helpers import Extend, Remove from esphome.core import EsphomeError from esphome.util import OrderedDict @@ -306,3 +307,128 @@ def test_dump_sort_keys() -> None: # nested keys should also be sorted assert "a_key:" in sorted_dump assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:") + + +@pytest.mark.parametrize( + "data", + [ + { + "key1": "value1", + "key2": 42, + }, + [1, 2, 3], + "simple string", + ], +) +def test_config_context_mixin(data) -> None: + """Test that ConfigContext mixin correctly stores and retrieves context vars in a dict.""" + + context_vars = { + "var1": "context_value1", + "var2": 100, + } + + # Add context to the data + tagged_data = yaml_util.add_context(data, context_vars) + + # Check that tagged_data has ConfigContext and correct vars + assert isinstance(tagged_data, type(data)) + assert isinstance(tagged_data, yaml_util.ConfigContext) + assert tagged_data.vars == context_vars + + # Check that original data is preserved + assert tagged_data == data + + +def test_config_context_mixin_no_context() -> None: + """Test that add_context does not tag data when no context vars are provided.""" + data = {"key": "value"} + + # Add context with None + tagged_data = yaml_util.add_context(data, None) + + # Should return original data without tagging + assert tagged_data is data + assert not isinstance(tagged_data, yaml_util.ConfigContext) + + +def test_config_context_mixin_merge_contexts() -> None: + """Test that add_context merges new context vars with existing ones.""" + data = {"key": "value"} + + initial_context = { + "var1": "initial_value", + } + + # First, add initial context + tagged_data = yaml_util.add_context(data, initial_context) + + assert isinstance(tagged_data, yaml_util.ConfigContext) + assert tagged_data.vars == initial_context + + # Now, add more context vars + new_context = { + "var2": "new_value", + "var1": "overridden_value", # This should override the initial var1 + } + + merged_tagged_data = yaml_util.add_context(tagged_data, new_context) + + # Check that merged_tagged_data has merged context vars + expected_context = { + "var1": "overridden_value", + "var2": "new_value", + } + assert isinstance(merged_tagged_data, yaml_util.ConfigContext) + assert merged_tagged_data.vars == expected_context + + # Check that original data is preserved + assert merged_tagged_data == data + + +@pytest.mark.parametrize("data", [42, 3.14, True, None]) +def test_config_context_non_taggable(data) -> None: + """Test that add_context ignores non-string scalar values.""" + + context_vars = { + "var1": "context_value", + } + + # Add context to the scalar data + tagged_data = yaml_util.add_context(data, context_vars) + + # Check that tagged_data has ConfigContext and correct vars + assert not isinstance(tagged_data, yaml_util.ConfigContext) + + # Check that original data is preserved + assert tagged_data == data + + +def test_config_context_defaults_only() -> None: + """Test that defaults: key is popped and used as context vars when no explicit vars given.""" + data = {"defaults": {"x": "1", "y": "2"}, "key": "value"} + tagged = yaml_util.add_context(data, None) + + assert isinstance(tagged, yaml_util.ConfigContext) + assert tagged.vars == {"x": "1", "y": "2"} + assert "defaults" not in tagged + + +def test_config_context_defaults_explicit_vars_override() -> None: + """Test that explicit vars take precedence over defaults: values.""" + data = {"defaults": {"x": "default_x", "z": "default_z"}, "key": "value"} + tagged = yaml_util.add_context(data, {"x": "explicit_x", "w": "explicit_w"}) + + assert isinstance(tagged, yaml_util.ConfigContext) + assert tagged.vars == {"x": "explicit_x", "z": "default_z", "w": "explicit_w"} + assert "defaults" not in tagged + + +def test_represent_extend() -> None: + """Test that Extend objects are dumped as plain !extend scalars.""" + assert yaml_util.dump({"key": Extend("my_id")}) == "key: !extend 'my_id'\n" + + +def test_represent_remove() -> None: + """Test that Remove objects are dumped as plain !remove scalars.""" + assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n" From 0a3393bed3fbc8623e9d955461783a65ae6c273c Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:13:36 +0100 Subject: [PATCH 0809/2056] [core] Disable LeakSanitizer in C++ unit tests (#14712) --- script/cpp_unit_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 5594d642400..f8bab394149 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse from functools import partial +import os from pathlib import Path import sys @@ -35,6 +36,7 @@ PLATFORMIO_OPTIONS = { def run_tests(selected_components: list[str]) -> int: + os.environ["ASAN_OPTIONS"] = "detect_leaks=0" return build_and_run( selected_components=selected_components, tests_dir=COMPONENTS_TESTS_DIR, From 2c31bdc6a251eeaa2ba56e9ae59ab27163950a07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:43:50 -1000 Subject: [PATCH 0810/2056] Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954) 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 f8f60f19329..9e2f4efe3e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.0 +aioesphomeapi==44.6.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 96da6dd075435d18dd50dc2b700f5fada19620f0 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:41:11 +0100 Subject: [PATCH 0811/2056] [esp32] Add custom partitions and refactor partition table generation (#7682) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 287 +++++++++++++++--- esphome/config_validation.py | 7 + tests/components/esp32/test.esp32-s3-idf.yaml | 5 + 3 files changed, 252 insertions(+), 47 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 18178c83ffc..64e5f44081b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_SAFE_MODE, + CONF_SIZE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -96,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" @@ -1258,6 +1260,43 @@ def _set_default_framework(config): return config +RESERVED_PARTITION_NAMES = { + "nvs", + "app0", + "app1", + "otadata", + "eeprom", + "spiffs", + "phy_init", +} + +VALID_APP_SUBTYPES = {"factory", "test"} +VALID_DATA_SUBTYPES = { + "nvs", + "nvs_keys", + "spiffs", + "coredump", + "efuse", + "fat", + "undefined", + "littlefs", +} + + +def _validate_custom_partition(config: ConfigType) -> ConfigType: + """Voluptuous validator for custom partition schema.""" + try: + _validate_partition( + config[CONF_NAME], + config[CONF_TYPE], + config[CONF_SUBTYPE], + config[CONF_SIZE], + ) + except ValueError as e: + raise cv.Invalid(str(e)) from e + return config + + FLASH_SIZES = [ "2MB", "4MB", @@ -1280,7 +1319,28 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.file_, + cv.Optional(CONF_PARTITIONS): cv.Any( + cv.file_, + cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_TYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SUBTYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SIZE): cv.int_range(min=0x1000), + } + ), + _validate_custom_partition, + ), + ), + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } @@ -1749,9 +1809,18 @@ async def to_code(config): if use_platformio: cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - add_extra_build_file( - "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) - ) + if isinstance(config[CONF_PARTITIONS], list): + for partition in config[CONF_PARTITIONS]: + add_partition( + partition[CONF_NAME], + partition[CONF_TYPE], + partition[CONF_SUBTYPE], + partition[CONF_SIZE], + ) + else: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): for key, flag in ASSERTION_LEVELS.items(): @@ -1885,45 +1954,175 @@ async def to_code(config): CORE.add_job(_write_arduino_libraries_sdkconfig) -APP_PARTITION_SIZES = { - "2MB": 0x0C0000, # 768 KB - "4MB": 0x1C0000, # 1792 KB - "8MB": 0x3C0000, # 3840 KB - "16MB": 0x7C0000, # 7936 KB - "32MB": 0xFC0000, # 16128 KB +KEY_CUSTOM_PARTITIONS = "custom_partitions" + + +@dataclass +class PartitionEntry: + name: str + type: str + subtype: str + size: int + + +# Partition sizes (offsets auto-placed by gen_esp32part.py). +# These constants are the single source of truth — used in both +# the CSV generation and the overhead calculation. +BOOTLOADER_SIZE = 0x8000 +PARTITION_TABLE_SIZE = 0x1000 +FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE +OTADATA_SIZE = 0x2000 +PHY_INIT_SIZE = 0x1000 +EEPROM_SIZE = 0x1000 # Arduino only +SPIFFS_SIZE = 0xF000 # Arduino only +ARDUINO_NVS_SIZE = 0x60000 +IDF_NVS_SIZE = 0x70000 + + +def _get_partition_overhead() -> int: + """Total non-app partition budget (system partitions + nvs + padding). + + Custom partitions are appended at the end and steal from app. + """ + # otadata + phy_init are followed by app0 which requires 64KB alignment, + # so pad up to the next 64KB boundary. + overhead = ( + FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF + ) & ~0xFFFF + if CORE.using_arduino: + overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE + else: + overhead += IDF_NVS_SIZE + return overhead + + +VALID_SUBTYPES: dict[str, set[str]] = { + "app": VALID_APP_SUBTYPES, + "data": VALID_DATA_SUBTYPES, } -def get_arduino_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] - eeprom_partition_size = 0x1000 # 4 KB - spiffs_partition_size = 0xF000 # 60 KB - - app0_partition_start = 0x010000 # 64 KB - app1_partition_start = app0_partition_start + app_partition_size - eeprom_partition_start = app1_partition_start + app_partition_size - spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - - return f"""\ -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xE000, 0x2000, -app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, -app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, -eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, -spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} -""" +def _validate_partition( + name: str, p_type: str | int, subtype: str | int, size: int +) -> None: + """Validate partition parameters. Raises ValueError on invalid input.""" + if name in RESERVED_PARTITION_NAMES: + raise ValueError(f"Partition name '{name}' is reserved.") + if size % 0x1000 != 0: + raise ValueError("Partition size must be 4KB (0x1000) aligned.") + # Numeric or already-normalized hex types/subtypes skip string validation + if not isinstance(p_type, str) or p_type.startswith("0x"): + return + if p_type not in VALID_SUBTYPES: + raise ValueError( + f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed." + " Use numbers for custom types." + ) + if not isinstance(subtype, str) or subtype.startswith("0x"): + return + valid = VALID_SUBTYPES[p_type] + if subtype not in valid: + raise ValueError( + f"Subtype '{subtype}' is invalid for {p_type} type." + f" Only {', '.join(sorted(valid))} are allowed." + " Use numbers for custom subtypes." + ) -def get_idf_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] +def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None: + """Register a custom partition to be appended to the partition table. - return f"""\ -otadata, data, ota, , 0x2000, -phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x{app_partition_size:X}, -app1, app, ota_1, , 0x{app_partition_size:X}, -nvs, data, nvs, , 0x6D000, -""" + Called from component to_code() to request additional flash partitions. + Size must be 4KB aligned. Integer types/subtypes are converted to hex strings. + """ + if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}): + raise ValueError(f"Partition name '{name}' is already defined.") + _validate_partition(name, p_type, subtype, size) + p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type + subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype + custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {}) + custom_partitions[name] = PartitionEntry( + name=name, type=p_type_str, subtype=subtype_str, size=size + ) + + +def _flash_size_to_bytes(flash_size_mb: str) -> int: + """Convert flash size string (e.g. '4MB') to bytes.""" + return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024 + + +def _get_custom_partitions_total_size() -> int: + """Total size of custom partitions including alignment padding.""" + size = 0 + for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values(): + if partition.type == "app": + size = (size + 0xFFFF) & ~0xFFFF # align to 64KB + size += partition.size + return size + + +def _get_app_partition_size(flash_size_mb: str) -> int: + flash_bytes = _flash_size_to_bytes(flash_size_mb) + custom_total = _get_custom_partitions_total_size() + # Align down to 64KB — app partitions require 64KB-aligned offsets, + # so the size must also be aligned to avoid unbudgeted padding. + raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2 + app_size = raw_size & ~0xFFFF + wasted = (raw_size - app_size) * 2 + if wasted: + _LOGGER.info( + "Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.", + wasted // 1024, + ) + if app_size <= 0x10000: # 64 KB + raise ValueError( + "Custom partitions are too large to fit in the available flash size. " + "Reduce custom partition sizes." + ) + if app_size <= 0x80000: # 512 KB + _LOGGER.warning( + "App partition size is only %dKB. This may be too small for firmware with " + "many components. Consider reducing custom partition sizes or using a " + "larger flash chip.", + app_size // 1024, + ) + return app_size + + +def get_partition_csv(flash_size_mb: str) -> str: + app_size = _get_app_partition_size(flash_size_mb) + + partitions: list[PartitionEntry] = [ + PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE), + PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE), + PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size), + PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size), + ] + if CORE.using_arduino: + partitions.append( + PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE) + ) + partitions.append( + PartitionEntry( + name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE + ) + ) + partitions.append( + PartitionEntry( + name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE + ) + ) + else: + partitions.append( + PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE) + ) + partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values()) + + csv = "".join( + f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions + ) + _LOGGER.debug("Partition table:\n%s", csv) + return csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -2030,16 +2229,10 @@ def copy_files(): if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] - if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv(flash_size), - ) - else: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv(flash_size), - ) + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_partition_csv(flash_size), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 32689dab27e..56f255a076f 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -494,6 +494,13 @@ def hex_int(value): return HexInt(int_(value)) +def int_to_hex_string(value: int | str) -> str: + """Convert an integer to a hex string (e.g. 64 -> '0x40'). Pass-through strings.""" + if isinstance(value, int): + return f"0x{value:X}" + return value + + def int_(value): """Validate that the config option is an integer. diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index 7a3bbe55b32..b9a3b804a8d 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -1,5 +1,10 @@ esp32: variant: esp32s3 + partitions: + - name: my_data + type: data + subtype: spiffs + size: 0x1000 framework: type: esp-idf advanced: From 0858ecbb8e425c54638fe2151cce3a2bad5048e8 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:05:12 -0400 Subject: [PATCH 0812/2056] [spa06_base] Add SPA06-003 Temperature and Pressure Sensor (Part 1 of 3) (#14521) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_base/__init__.py | 201 ++++++++++++ esphome/components/spa06_base/spa06_base.cpp | 320 +++++++++++++++++++ esphome/components/spa06_base/spa06_base.h | 257 +++++++++++++++ 4 files changed, 779 insertions(+) create mode 100644 esphome/components/spa06_base/__init__.py create mode 100644 esphome/components/spa06_base/spa06_base.cpp create mode 100644 esphome/components/spa06_base/spa06_base.h diff --git a/CODEOWNERS b/CODEOWNERS index 88f62c3194e..5869925d7c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -457,6 +457,7 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt +esphome/components/spa06_base/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py new file mode 100644 index 00000000000..97d09aad816 --- /dev/null +++ b/esphome/components/spa06_base/__init__.py @@ -0,0 +1,201 @@ +import math + +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@danielkent-net"] + +spa06_ns = cg.esphome_ns.namespace("spa06_base") + +SampleRate = spa06_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "1": SampleRate.SAMPLE_RATE_1, + "2": SampleRate.SAMPLE_RATE_2, + "4": SampleRate.SAMPLE_RATE_4, + "8": SampleRate.SAMPLE_RATE_8, + "16": SampleRate.SAMPLE_RATE_16, + "32": SampleRate.SAMPLE_RATE_32, + "64": SampleRate.SAMPLE_RATE_64, + "128": SampleRate.SAMPLE_RATE_128, + "25p16": SampleRate.SAMPLE_RATE_25P16, + "25p8": SampleRate.SAMPLE_RATE_25P8, + "25p4": SampleRate.SAMPLE_RATE_25P4, + "25p2": SampleRate.SAMPLE_RATE_25P2, + "25": SampleRate.SAMPLE_RATE_25, + "50": SampleRate.SAMPLE_RATE_50, + "100": SampleRate.SAMPLE_RATE_100, + "200": SampleRate.SAMPLE_RATE_200, +} + +Oversampling = spa06_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent) + + +def spa_oversample_time(oversample): + # Pressure oversampling conversion times are listed on datasheet Pg. 26 + # Datasheet does not have a table for temperature oversampling; + # assumption is that it is the same as pressure + OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 3.6, + "2X": 5.2, + "4X": 8.4, + "8X": 14.8, + "16X": 27.6, + "32X": 53.2, + "64X": 104.4, + "128X": 206.8, + } + return OVERSAMPLING_CONVERSION_TIMES[oversample] + + +def spa_sample_rate(rate): + SAMPLE_RATE_OPTIONS_HZ = { + "1": 1.0, + "2": 2.0, + "4": 4.0, + "8": 8.0, + "16": 16.0, + "32": 32.0, + "64": 64.0, + "128": 128.0, + "25p16": 25.0 / 16.0, + "25p8": 25.0 / 8.0, + "25p4": 25.0 / 4.0, + "25p2": 25.0 / 2.0, + "25": 25.0, + "50": 50.0, + "100": 100.0, + "200": 200.0, + } + return SAMPLE_RATE_OPTIONS_HZ[rate] + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # No conversion time necessary without a pressure sensor + pressure_conversion_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = spa_oversample_time( + pressure_config.get(CONF_OVERSAMPLING) + ) + # Temperature required in all cases, default to minimum sample time + temperature_conversion_time = 3.6 + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = spa_oversample_time( + temperature_config.get(CONF_OVERSAMPLING) + ) + + # TODO: Read datasheet to find conversion time error + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +def measurement_timing_check(config): + + temp_time = 0.0 + if temperature_config := config.get(CONF_TEMPERATURE): + temp_oss = ( + spa_oversample_time(temperature_config.get(CONF_OVERSAMPLING)) / 1000.0 + ) + temp_hz = spa_sample_rate(temperature_config.get(CONF_SAMPLE_RATE)) + temp_time = temp_oss * temp_hz + + pres_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pres_oss = spa_oversample_time(pressure_config.get(CONF_OVERSAMPLING)) / 1000.0 + pres_hz = spa_sample_rate(pressure_config.get(CONF_SAMPLE_RATE)) + pres_time = pres_oss * pres_hz + + if temp_time + pres_time >= 1: + raise cv.Invalid( + "Combined sample_rate and oversampling for temperature and pressure is too high" + ) + return config + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPA06Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + }, +).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_sample_rate_config(temperature_config[CONF_SAMPLE_RATE]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_sample_rate_config(pressure_config[CONF_SAMPLE_RATE])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) + return var diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp new file mode 100644 index 00000000000..36268aa9a26 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -0,0 +1,320 @@ +#include "spa06_base.h" + +#include "esphome/core/helpers.h" + +namespace esphome::spa06_base { + +static const char *const TAG = "spa06"; + +// Sign extension function for <=16 bit types +inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) { + return static_cast<int16_t>(encode_uint16(msb, lsb) << head) >> (16 - bits); +} + +// Sign extension function for <=32 bit types +inline int32_t decode32(uint8_t xmsb, uint8_t msb, uint8_t lsb, uint8_t xlsb, size_t bits, size_t head = 0) { + return static_cast<int32_t>(encode_uint32(xmsb, msb, lsb, xlsb) << head) >> (32 - bits); +} + +void SPA06Component::dump_config() { + ESP_LOGCONFIG(TAG, "SPA06:"); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->temperature_rate_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->pressure_rate_))); + } +} + +void SPA06Component::setup() { + // Startup sequence for SPA06 (Pg. 16, Figure 4.6.4): + // 1. Perform a soft reset + // 2. Verify sensor chip ID matches + // 3. Verify coefficients are ready + // 4. Read coefficients + // 5. Configure temperature and pressure sensors + // 6. Write communication settings + // 7. Write measurement settings (background measurement mode) + + // 1. Soft reset + if (!this->soft_reset_()) { + this->mark_failed(LOG_STR("Reset failed")); + return; + } + + // soft_reset_() internally delays by 3ms to make sure that + // the sensor is in a ready state and coefficients are ready. + + // 2. Read chip ID + // TODO: check ID for consistency? + if (!spa_read_byte(SPA06_ID, &this->prod_id_.reg)) { + this->mark_failed(LOG_STR("Chip ID read failure")); + return; + } + ESP_LOGV(TAG, + "Product Info:\n" + " Prod ID: %u\n" + " Rev ID: %u", + this->prod_id_.bit.prod_id, this->prod_id_.bit.rev_id); + + // 3. Read chip readiness from MEAS_CFG + // First check if the sensor reports ready + if (!spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + this->mark_failed(LOG_STR("Sensor status read failure")); + return; + } + // Check if the sensor reports coefficients are ready + if (!meas_.bit.coef_ready) { + this->mark_failed(LOG_STR("Coefficients not ready")); + return; + } + + // 4. Read coefficients + if (!this->read_coefficients_()) { + this->mark_failed(LOG_STR("Coefficients read error")); + return; + } + + // 5. Configure temperature and pressure sensors + // Default to measuring both temperature and pressure + + // Temperature must be read regardless of configuration to compute pressure + // If temperature is not configured in config: + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->temperature_sensor_) { + this->temperature_rate_ = SAMPLE_RATE_1; + this->temperature_oversampling_ = OVERSAMPLING_NONE; + this->kt_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // If pressure is not configured in config + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->pressure_sensor_) { + this->pressure_rate_ = SAMPLE_RATE_1; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + this->kp_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // Write temperature settings + if (!write_temperature_settings_(this->temperature_oversampling_, this->temperature_rate_)) { + this->mark_failed(LOG_STR("Temperature settings write fail")); + return; + } + + // Write pressure settings + if (!write_pressure_settings_(this->pressure_oversampling_, this->pressure_rate_)) { + this->mark_failed(LOG_STR("Pressure settings write fail")); + return; + } + // 6. Write communication settings + // This call sets the bit shifts for pressure and temperature if + // their respective oversampling config is > X8 + // This call also disables interrupts, FIFO, and specifies SPI 4-wire + if (!write_communication_settings_(this->pressure_oversampling_ > OVERSAMPLING_X8, + this->temperature_oversampling_ > OVERSAMPLING_X8)) { + this->mark_failed(LOG_STR("Comm settings write fail")); + return; + } + + // 7. Write measurement settings + // This function sets background measurement mode without FIFO + if (!write_measurement_settings_(this->pressure_sensor_ ? MeasCrtl::MEASCRTL_BG_BOTH : MeasCrtl::MEASCRTL_BG_TEMP)) { + this->mark_failed(LOG_STR("Measurement settings write fail")); + return; + } +} + +bool SPA06Component::write_temperature_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_TMP_CFG); +} + +bool SPA06Component::write_pressure_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_PSR_CFG); +} + +bool SPA06Component::write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg) { + if (reg != SPA06_PSR_CFG && reg != SPA06_TMP_CFG) { + return false; + } + this->pt_meas_cfg_.bit.rate = rate; + this->pt_meas_cfg_.bit.prc = oversampling; + ESP_LOGD(TAG, "Config write: %02x", this->pt_meas_cfg_.reg); + return spa_write_byte(reg, this->pt_meas_cfg_.reg); +} + +bool SPA06Component::write_measurement_settings_(MeasCrtl crtl) { + this->meas_.bit.meas_crtl = crtl; + return spa_write_byte(SPA06_MEAS_CFG, this->meas_.reg); +} + +bool SPA06Component::write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl, + bool interrupt_fifo, bool interrupt_tmp, bool interrupt_prs, + bool enable_fifo, bool spi_3wire) { + this->cfg_.bit.p_shift = pressure_shift; + this->cfg_.bit.t_shift = temperature_shift; + this->cfg_.bit.int_hl = interrupt_hl; + this->cfg_.bit.int_fifo = interrupt_fifo; + this->cfg_.bit.int_tmp = interrupt_tmp; + this->cfg_.bit.int_prs = interrupt_prs; + this->cfg_.bit.fifo_en = enable_fifo; + this->cfg_.bit.spi_3wire = spi_3wire; + return spa_write_byte(SPA06_CFG_REG, this->cfg_.reg); +} + +bool SPA06Component::read_coefficients_() { + uint8_t coef[SPA06_COEF_LEN]; + if (!spa_read_bytes(SPA06_COEF, coef, SPA06_COEF_LEN)) { + return false; + } + this->c0_ = decode16(coef[0], coef[1], 12); + this->c1_ = decode16(coef[1], coef[2], 12, 4); + this->c00_ = decode32(coef[3], coef[4], coef[5], 0, 20); + this->c10_ = decode32(coef[5], coef[6], coef[7], 0, 20, 4); + this->c01_ = decode16(coef[8], coef[9], 16); + this->c11_ = decode16(coef[10], coef[11], 16); + this->c20_ = decode16(coef[12], coef[13], 16); + this->c21_ = decode16(coef[14], coef[15], 16); + this->c30_ = decode16(coef[16], coef[17], 16); + this->c31_ = decode16(coef[18], coef[19], 12); + this->c40_ = decode16(coef[19], coef[20], 12, 4); + + ESP_LOGV(TAG, + "Coefficients:\n" + " c0: %i, c1: %i,\n" + " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c01: %i, c11: %i, c21: %i, c31: %i", + this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, + this->c21_, this->c31_); + return true; +} + +bool SPA06Component::soft_reset_() { + // Setup steps for SPA06: + // 1. Perform a protocol reset (required to write command for SPI code, noop for I2C) + this->protocol_reset(); + + // 2. Perform the actual reset + this->reset_.bit.fifo_flush = true; + this->reset_.bit.soft_rst = SPA06_SOFT_RESET; + if (!this->spa_write_byte(SPA06_RESET, this->reset_.reg)) { + return false; + } + + // 3. Wait for chip to become ready. Datasheet specifies 2 ms; wait 3 + delay(3); + // 4. Perform another protocol reset (required for SPI code, noop for I2C) + this->protocol_reset(); + return true; +} + +// Temperature conversion formula. See datasheet pg. 14 +float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; } +// Pressure conversion formula. See datasheet pg. 14 +float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) { + float p2_raw_sc = p_raw_sc * p_raw_sc; + float p3_raw_sc = p2_raw_sc * p_raw_sc; + float p4_raw_sc = p3_raw_sc * p_raw_sc; + return this->c00_ + (float) this->c10_ * p_raw_sc + (float) this->c20_ * p2_raw_sc + (float) this->c30_ * p3_raw_sc + + (float) this->c40_ * p4_raw_sc + + t_raw_sc * ((float) this->c01_ + (float) this->c11_ * p_raw_sc + (float) this->c21_ * p2_raw_sc + + (float) this->c31_ * p3_raw_sc); +} + +void SPA06Component::update() { + // Verify either a temperature or pressure sensor is defined before proceeding + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + // Queue a background task for retrieving the measurement + this->set_timeout("measurement", this->conversion_time_, [this]() { + float raw_temperature; + float temperature = 0.0; + float pressure = 0.0; + + // Check measurement register for readiness + if (!this->spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + ESP_LOGW(TAG, "Cannot read meas config"); + this->status_set_warning(); + return; + } + if (this->pressure_sensor_) { + if (!this->meas_.bit.prs_ready || !this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature and pressure not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_and_pressure_(temperature, pressure, raw_temperature)) { + ESP_LOGW(TAG, "Temperature and pressure read failure"); + this->status_set_warning(); + return; + } + } else { + if (!this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_(temperature, raw_temperature)) { + ESP_LOGW(TAG, "Temperature read fail"); + this->status_set_warning(); + return; + } + } + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } else { + ESP_LOGV(TAG, "No temperature sensor configured"); + } + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } else { + ESP_LOGV(TAG, "No pressure sensor configured"); + } + this->status_clear_warning(); + }); +} + +bool SPA06Component::read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc) { + // Temperature read and decode + if (!this->read_temperature_(temperature, t_raw_sc)) { + return false; + } + // Read raw pressure from device + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_PSR, buf, 3)) { + return false; + } + // Calculate raw scaled pressure value + float p_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kp_; + + // Calculate full pressure values + pressure = this->convert_pressure_(p_raw_sc, t_raw_sc); + return true; +} + +bool SPA06Component::read_temperature_(float &temperature, float &t_raw_sc) { + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_TMP, buf, 3)) { + return false; + } + + t_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kt_; + temperature = this->convert_temperature_(t_raw_sc); + return true; +} +} // namespace esphome::spa06_base diff --git a/esphome/components/spa06_base/spa06_base.h b/esphome/components/spa06_base/spa06_base.h new file mode 100644 index 00000000000..5239e80ec5c --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.h @@ -0,0 +1,257 @@ +// SPA06 interface code for ESPHome +// All datasheet page references refer to Goermicro SPA06-003 datasheet version 2.0 + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/progmem.h" + +namespace esphome::spa06_base { + +// Read sizes. All other registers are size 1 +constexpr size_t SPA06_MEAS_LEN = 3; +constexpr size_t SPA06_COEF_LEN = 21; + +// Soft reset command (0b1001, 0x9) +constexpr uint8_t SPA06_SOFT_RESET = 0x9; + +// SPA06 Register Addresses +enum Register : uint8_t { + SPA06_PSR = 0x00, // Pressure Reading MSB (or all 3) + SPA06_PSR_B1 = 0x01, // Pressure Reading LSB + SPA06_PSR_B0 = 0x02, // Pressure Reading XLSB (LSB: Pressure flag in FIFO) + SPA06_TMP = 0x03, // Temperature Reading MSB (or all 3) + SPA06_TMP_B1 = 0x04, // Temperature Reading LSB + SPA06_TMP_B0 = 0x05, // Temperature Reading XLSB + SPA06_PSR_CFG = 0x06, // Pressure Configuration + SPA06_TMP_CFG = 0x07, // Temperature Configuration + SPA06_MEAS_CFG = 0x08, // Measurement Configuration (includes readiness) + SPA06_CFG_REG = 0x09, // Configuration Register + SPA06_INT_STS = 0x0A, // Interrupt Status + SPA06_FIFO_STS = 0x0B, // FIFO Status + SPA06_RESET = 0x0C, // Reset + FIFO Flush + SPA06_ID = 0x0D, // Product ID and revision + SPA06_COEF = 0x10, // Coefficients (0x10-0x24) + SPA06_INVALID_CMD = 0x25, // End of enum command +}; + +// Oversampling config. +enum Oversampling : uint8_t { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7, + OVERSAMPLING_COUNT = 0x8, +}; + +// Measuring rate config +enum SampleRate : uint8_t { + SAMPLE_RATE_1 = 0x0, + SAMPLE_RATE_2 = 0x1, + SAMPLE_RATE_4 = 0x2, + SAMPLE_RATE_8 = 0x3, + SAMPLE_RATE_16 = 0x4, + SAMPLE_RATE_32 = 0x5, + SAMPLE_RATE_64 = 0x6, + SAMPLE_RATE_128 = 0x7, + SAMPLE_RATE_25P16 = 0x8, + SAMPLE_RATE_25P8 = 0x9, + SAMPLE_RATE_25P4 = 0xA, + SAMPLE_RATE_25P2 = 0xB, + SAMPLE_RATE_25 = 0xC, + SAMPLE_RATE_50 = 0xD, + SAMPLE_RATE_100 = 0xE, + SAMPLE_RATE_200 = 0xF, +}; + +// Measuring control config, set in MEAS_CFG register. +// See datasheet pages 28-29 +enum MeasCrtl : uint8_t { + MEASCRTL_IDLE = 0x0, + MEASCRTL_PRES = 0x1, + MEASCRTL_TEMP = 0x2, + MEASCRTL_BG_PRES = 0x5, + MEASCRTL_BG_TEMP = 0x6, + MEASCRTL_BG_BOTH = 0x7, +}; + +// Oversampling scale factors. See datasheet page 15. +constexpr uint32_t OVERSAMPLING_K_LUT[8] = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960}; +PROGMEM_STRING_TABLE(MeasRateStrings, "1Hz", "2Hz", "4Hz", "8Hz", "16Hz", "32Hz", "64Hz", "128Hz", "1.5625Hz", + "3.125Hz", "6.25Hz", "12.5Hz", "25Hz", "50Hz", "100Hz", "200Hz"); +PROGMEM_STRING_TABLE(OversamplingStrings, "X1", "X2", "X4", "X8", "X16", "X32", "X64", "X128"); + +inline static const LogString *oversampling_to_str(const Oversampling oversampling) { + return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX); +} +inline static const LogString *meas_rate_to_str(SampleRate rate) { + return MeasRateStrings::get_log_str(static_cast<uint8_t>(rate), MeasRateStrings::LAST_INDEX); +} +inline uint32_t oversampling_to_scale_factor(const Oversampling oversampling) { + return OVERSAMPLING_K_LUT[static_cast<uint8_t>(oversampling)]; +}; + +class SPA06Component : public PollingComponent { + public: + //// Standard ESPHome component class functions + void setup() override; + void update() override; + void dump_config() override; + + //// ESPHome-side settings + void set_conversion_time(uint16_t conversion_time) { this->conversion_time_ = conversion_time; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + this->kt_ = oversampling_to_scale_factor(temperature_oversampling); + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + this->kp_ = oversampling_to_scale_factor(pressure_oversampling); + } + void set_pressure_sample_rate_config(SampleRate rate) { this->pressure_rate_ = rate; } + void set_temperature_sample_rate_config(SampleRate rate) { this->temperature_rate_ = rate; } + + protected: + // Virtual read functions. Implemented in SPI/I2C components + virtual bool spa_read_byte(uint8_t reg, uint8_t *data) = 0; + virtual bool spa_write_byte(uint8_t reg, uint8_t data) = 0; + virtual bool spa_read_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + virtual bool spa_write_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + + //// Protocol-specific read functions + // Soft reset + bool soft_reset_(); + // Protocol-specific reset (used for SPI only, implemented as noop for I2C) + virtual void protocol_reset() {} + // Read temperature and calculate Celsius and scaled raw temperatures + bool read_temperature_(float &temperature, float &t_raw_sc); + // No pressure only read! Pressure calculation depends on scaled temperature value + // Read temperature and calculate Celsius temperature, Pascal pressure, and scaled raw temperature + bool read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc); + // Read coefficients. Stores in class variables. + bool read_coefficients_(); + + //// Protocol-specific write functions + // Write temperature settings to TMP_CFG register + bool write_temperature_settings_(Oversampling oversampling, SampleRate rate); + // Write pressure settings to PRS_CFG register + bool write_pressure_settings_(Oversampling oversampling, SampleRate rate); + // Write measurement settings to MEAS_CRTL register + bool write_measurement_settings_(MeasCrtl crtl); + + // Write communication settings to CFG_REG register + // Set pressure_shift to true if pressure oversampling >X8 + // Set temperature_shift to true if temperature oversampling >X8 + bool write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl = false, + bool interrupt_fifo = false, bool interrupt_tmp = false, + bool interrupt_prs = false, bool enable_fifo = false, bool spi_3wire = false); + + //// Protocol helper functions + // Write function for both temperature and pressure (deduplicates code) + bool write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg); + // Convert raw temperature reading into Celsius + float convert_temperature_(const float &t_raw_sc); + // Convert raw pressure and scaled raw temperature into Pascals + float convert_pressure_(const float &p_raw_sc, const float &t_raw_sc); + + //// Protocol-related variables + // Oversampling scale factors. Defaults are for X16 (pressure) and X1 (temp) + uint32_t kp_{253952}, kt_{524288}; + // Coefficients for calculating pressure and temperature from raw values + // Obtained from IC during setup + int32_t c00_{0}, c10_{0}; + int16_t c0_{0}, c1_{0}, c01_{0}, c11_{0}, c20_{0}, c21_{0}, c30_{0}, c31_{0}, c40_{0}; + + //// ESPHome class objects and configuration + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + Oversampling temperature_oversampling_{Oversampling::OVERSAMPLING_NONE}; + Oversampling pressure_oversampling_{Oversampling::OVERSAMPLING_X16}; + SampleRate temperature_rate_{SampleRate::SAMPLE_RATE_1}; + SampleRate pressure_rate_{SampleRate::SAMPLE_RATE_1}; + // Default conversion time: 27.6ms (16x pres) + 3.6ms (1x temp) ~ 32ms + uint16_t conversion_time_{32}; + + union { + struct { + Oversampling prc : 4; + SampleRate rate : 4; + } bit; + uint8_t reg; + } pt_meas_cfg_ = {.reg = 0}; // PRS_CFG and TMP_CFG + + union { + struct { + uint8_t meas_crtl : 3; + bool tmp_ext : 1; + bool prs_ready : 1; + bool tmp_ready : 1; + bool sensor_ready : 1; + bool coef_ready : 1; + } bit; + uint8_t reg; + } meas_ = {.reg = 0}; // MEAS_REG + + union { + struct { + uint8_t _reserved : 5; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo_full : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; // INT_STS + + union { + struct { + bool spi_3wire : 1; + bool fifo_en : 1; + bool p_shift : 1; + bool t_shift : 1; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo : 1; + bool int_hl : 1; + } bit; + uint8_t reg; + } cfg_ = {.reg = 0}; // CFG_REG + + union { + struct { + bool fifo_empty : 1; + bool fifo_full : 1; + uint8_t _reserved : 6; + } bit; + uint8_t reg; + } fifo_sts_ = {.reg = 0}; // FIFO_STS + + union { + struct { + // Set to true to flush FIFO + bool fifo_flush : 1; + // Reserved bits + uint8_t _reserved : 3; + // Soft reset. Set to 1001 (0x9) to perform reset. + uint8_t soft_rst : 4; + } bit; + uint8_t reg = 0; + } reset_ = {.reg = 0}; // RESET + + union { + struct { + uint8_t prod_id : 4; + uint8_t rev_id : 4; + } bit; + uint8_t reg = 0; + } prod_id_ = {.reg = 0}; // ID + +}; // class SPA06Component +} // namespace esphome::spa06_base From cb23f9453f29c8f83ac6bf9e4b0ddd3f2b33d91c Mon Sep 17 00:00:00 2001 From: CFlix <38142312+CFlix@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:09:00 +0100 Subject: [PATCH 0813/2056] [absolute_humidity] loop() improvement (#14684) Co-authored-by: DAVe3283 <DAVe3283+GitHub@gmail.com> --- .../absolute_humidity/absolute_humidity.cpp | 102 +++++++++--------- .../absolute_humidity/absolute_humidity.h | 20 +--- 2 files changed, 55 insertions(+), 67 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 40676f8655c..3137a59fad6 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -1,22 +1,29 @@ #include "esphome/core/log.h" #include "absolute_humidity.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { -static const char *const TAG = "absolute_humidity.sensor"; +static const char *const TAG{"absolute_humidity.sensor"}; void AbsoluteHumidityComponent::setup() { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); - this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); + // Get initial value if (this->temperature_sensor_->has_state()) { - this->temperature_callback_(this->temperature_sensor_->get_state()); + this->temperature_ = this->temperature_sensor_->get_state(); } + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); - this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); + // Get initial value if (this->humidity_sensor_->has_state()) { - this->humidity_callback_(this->humidity_sensor_->get_state()); + this->humidity_ = this->humidity_sensor_->get_state(); } } @@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() { } void AbsoluteHumidityComponent::loop() { - if (!this->next_update_) { - return; - } - this->next_update_ = false; + // Only run once + this->disable_loop(); // Ensure we have source data - const bool no_temperature = std::isnan(this->temperature_); - const bool no_humidity = std::isnan(this->humidity_); + const bool no_temperature{std::isnan(this->temperature_)}; + const bool no_humidity{std::isnan(this->humidity_)}; if (no_temperature || no_humidity) { if (no_temperature) { ESP_LOGW(TAG, "No valid state from temperature sensor!"); @@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() { } // Convert to desired units - const float temperature_c = this->temperature_; - const float temperature_k = temperature_c + 273.15; - const float hr = this->humidity_ / 100; + const float temperature_c{this->temperature_}; + const float temperature_k{temperature_c + 273.15f}; + const float hr{this->humidity_ / 100.0f}; // Calculate saturation vapor pressure float es; @@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() { } // Calculate absolute humidity - const float absolute_humidity = vapor_density(es, hr, temperature_k); + const float absolute_humidity{vapor_density(es, hr, temperature_k)}; ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); @@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() { // More accurate than Tetens in normal meteorologic conditions float AbsoluteHumidityComponent::es_buck(float temperature_c) { float a, b, c, d; - if (temperature_c >= 0) { - a = 0.61121; - b = 18.678; - c = 234.5; - d = 257.14; + if (temperature_c >= 0.0f) { + a = 0.61121f; + b = 18.678f; + c = 234.5f; + d = 257.14f; } else { - a = 0.61115; - b = 18.678; - c = 233.7; - d = 279.82; + a = 0.61115f; + b = 18.678f; + c = 233.7f; + d = 279.82f; } return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); } @@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) { // Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) float AbsoluteHumidityComponent::es_tetens(float temperature_c) { float a, b; - if (temperature_c >= 0) { - a = 17.27; - b = 237.3; + if (temperature_c >= 0.0f) { + a = 17.27f; + b = 237.3f; } else { - a = 21.875; - b = 265.5; + a = 21.875f; + b = 265.5f; } - return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); + return 0.61078f * expf((a * temperature_c) / (temperature_c + b)); } // Wobus equation @@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) { // // Baker, Schlatter 17-MAY-1982 Original version. - const float c0 = +0.99999683e00; - const float c1 = -0.90826951e-02; - const float c2 = +0.78736169e-04; - const float c3 = -0.61117958e-06; - const float c4 = +0.43884187e-08; - const float c5 = -0.29883885e-10; - const float c6 = +0.21874425e-12; - const float c7 = -0.17892321e-14; - const float c8 = +0.11112018e-16; - const float c9 = -0.30994571e-19; - const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); - return 0.61078 / pow(p, 8); + constexpr float c0{+0.99999683e+00f}; + constexpr float c1{-0.90826951e-02f}; + constexpr float c2{+0.78736169e-04f}; + constexpr float c3{-0.61117958e-06f}; + constexpr float c4{+0.43884187e-08f}; + constexpr float c5{-0.29883885e-10f}; + constexpr float c6{+0.21874425e-12f}; + constexpr float c7{-0.17892321e-14f}; + constexpr float c8{+0.11112018e-16f}; + constexpr float c9{-0.30994571e-19f}; + const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))}; + return 0.61078f / powf(p, 8.0f); } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ @@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { // hr = relative humidity [0-1] // ta = absolute temperature (K) - const float ea = hr * es * 1000; // vapor pressure of the air (Pa) - const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹) - const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹) + const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa) + const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹) + const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹) return (ea * mw) / (r * ta); } -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 71feee2c429..be28d3dc509 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { /// Enum listing all implemented saturation vapor pressure equations. enum SaturationVaporPressureEquation { @@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation { /// This class implements calculation of absolute humidity from temperature and relative humidity. class AbsoluteHumidityComponent : public sensor::Sensor, public Component { public: - AbsoluteHumidityComponent() = default; - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } @@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void loop() override; protected: - void temperature_callback_(float state) { - this->next_update_ = true; - this->temperature_ = state; - } - void humidity_callback_(float state) { - this->next_update_ = true; - this->humidity_ = state; - } - /** Buck equation for saturation vapor pressure in kPa. * * @param temperature_c Air temperature in °C. @@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { * @param es Saturation vapor pressure in kPa. * @param hr Relative humidity 0 to 1. * @param ta Absolute temperature in K. - * @param heater_duration The duration in ms that the heater should turn on for when measuring. */ static float vapor_density(float es, float hr, float ta); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - bool next_update_{false}; - float temperature_{NAN}; float humidity_{NAN}; SaturationVaporPressureEquation equation_; }; -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity From b9439036d4d2605a40378ae6f624e1b4eecb14b8 Mon Sep 17 00:00:00 2001 From: aanban <stiller-gast@gmx.de> Date: Thu, 19 Mar 2026 14:19:24 +0100 Subject: [PATCH 0814/2056] [remote_base] add support for brennenstuhl comfort-line switches (#9407) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/remote_base/__init__.py | 44 ++++++ .../remote_base/brennenstuhl_protocol.cpp | 144 ++++++++++++++++++ .../remote_base/brennenstuhl_protocol.h | 34 +++++ .../remote_receiver/common-actions.yaml | 5 + .../remote_transmitter/common-buttons.yaml | 6 + 5 files changed, 233 insertions(+) create mode 100644 esphome/components/remote_base/brennenstuhl_protocol.cpp create mode 100644 esphome/components/remote_base/brennenstuhl_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index bf17ac27b8f..a0594d7f679 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -310,6 +310,50 @@ async def beo4_action(var, config, args): cg.add(var.set_repeats(template_)) +# Brennenstuhl +( + BrennenstuhlData, + BrennenstuhlBinarySensor, + BrennenstuhlTrigger, + BrennenstuhlAction, + BrennenstuhlDumper, +) = declare_protocol("Brennenstuhl") + +BRENNENSTUHL_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.hex_uint32_t, + } +) + + +@register_binary_sensor("brennenstuhl", BrennenstuhlBinarySensor, BRENNENSTUHL_SCHEMA) +def brennenstuhl_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + BrennenstuhlData, + ("code", config[CONF_CODE]), + ) + ) + ) + + +@register_trigger("brennenstuhl", BrennenstuhlTrigger, BrennenstuhlData) +def brennenstuhl_trigger(var, config): + pass + + +@register_dumper("brennenstuhl", BrennenstuhlDumper) +def brennenstuhl_dumper(var, config): + pass + + +@register_action("brennenstuhl", BrennenstuhlAction, BRENNENSTUHL_SCHEMA) +async def brennenstuhl_action(var, config, args): + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_code(template_)) + + # ByronSX ( ByronSXData, diff --git a/esphome/components/remote_base/brennenstuhl_protocol.cpp b/esphome/components/remote_base/brennenstuhl_protocol.cpp new file mode 100644 index 00000000000..35261dc00a0 --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.cpp @@ -0,0 +1,144 @@ +#include "brennenstuhl_protocol.h" +#include "esphome/core/log.h" + +#include <cinttypes> + +namespace esphome::remote_base { + +static const char *const TAG = "remote.brennenstuhl"; + +// receiver timing ranges [µs] +constexpr uint32_t START_PULSE_MIN = 200; +constexpr uint32_t START_PULSE_MAX = 500; +constexpr uint32_t START_SYMBOL_MIN = 2600; +constexpr uint32_t START_SYMBOL_MAX = 2700; +constexpr uint32_t DATA_SYMBOL_MIN = 1500; +constexpr uint32_t DATA_SYMBOL_MAX = 1600; + +// transmitter timings [µs] +constexpr uint32_t PW_SHORT_US = 390; +constexpr uint32_t PW_LONG_US = 1160; +constexpr uint32_t PW_START_US = 2300; + +// number of data bits +constexpr uint32_t N_BITS = 24; + +// number of required symbols = 2 x (start + N_BITS) = 50 +constexpr uint32_t N_SYMBOLS_REQ = 2u * (N_BITS + 1); + +// number of bs codes within received frame +constexpr int32_t N_FRAME_CODES = 4; + +// decoder finite-state-machine +enum class RxSt { START_PULSE, START_SYMBOL, PULSE, DATA_SYMBOL }; + +// The encode() member function reserves and fills a complete frame, to be send. The Brennenstuhl +// RC receivers demand a frame with a start-symbol followed by 4 repeated codes. +void BrennenstuhlProtocol::encode(RemoteTransmitData *dst, const BrennenstuhlData &data) { + uint32_t code = data.code; + dst->reserve((N_SYMBOLS_REQ * N_FRAME_CODES) + 1); + for (int32_t kc = 0; kc != N_FRAME_CODES; kc++) { + dst->item(PW_SHORT_US, PW_START_US); + for (int32_t ic = (N_BITS - 1); ic != -1; ic--) { + if ((code >> ic) & 1) { + dst->item(PW_LONG_US, PW_SHORT_US); + } else { + dst->item(PW_SHORT_US, PW_LONG_US); + } + } + } +} + +// The decode() member function extracts Brennenstuhl codes from the received frame. Instead +// of validating the pulse width of the carriers and pauses individually, it is more accurate +// to validate the symbols (symbol=carrier+pause) The symbol pulsewidth is around 1550µs, but +// the pulse with of the carrier and the pauses vary greatly. Once the symbol pulsewidth is +// valid, a code bit becomes "1" if the carrier is longer then the pause and "0" else. A total +// frame consists of a start symbol and up to four codes. The decoder decodes all codes and +// returns the best code (the one with the most identical codes) +optional<BrennenstuhlData> BrennenstuhlProtocol::decode(RemoteReceiveData src) { + uint32_t n_received = static_cast<uint32_t>(src.size()); + BrennenstuhlData data{ + .code = 0, + }; + // suppress noisy frames, at least a complete bs_code should be available + if (n_received > N_SYMBOLS_REQ) { + uint32_t bs_codes[4] = {0, 0, 0, 0}; // internal codes + int32_t bs_cnt = 0; // number of bs codes found within frame + int32_t bs_idx = -1; // index to best bs code + uint32_t bit_cnt = 0; // bit counter [0..23] + uint32_t pw_pre = 0; // pulsewidth of previous carrier (abs value) + RxSt fsm = RxSt::START_PULSE; + for (uint32_t ic = 0; (ic != n_received) && (bs_cnt != N_FRAME_CODES); ic++) { + uint32_t pw_cur = (uint32_t) (src[ic] < 0 ? -src[ic] : src[ic]); // current pulsewidth + uint32_t pw_sym = pw_cur + pw_pre; // symbol=pulse+pause + switch (fsm) { + case RxSt::START_PULSE: { // check if start pulse is valid + if ((src[ic] > 0) && (pw_cur >= START_PULSE_MIN) && (pw_cur <= START_PULSE_MAX)) { + bs_codes[bs_cnt] = 0; + bit_cnt = 0; + pw_pre = pw_cur; + fsm = RxSt::START_SYMBOL; + } + break; + } + case RxSt::START_SYMBOL: { // check if start symbol is valid + if ((src[ic] < 0) && (pw_sym >= START_SYMBOL_MIN) && (pw_sym <= START_SYMBOL_MAX)) { + fsm = RxSt::PULSE; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::PULSE: { // just grab pulse, validation is done in DATA_SYMBOL state + if (src[ic] > 0) { + pw_pre = pw_cur; + fsm = RxSt::DATA_SYMBOL; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::DATA_SYMBOL: { // check if data symbol is valid and append bit to data + if ((src[ic] < 0) && (pw_sym >= DATA_SYMBOL_MIN) && (pw_sym <= DATA_SYMBOL_MAX)) { + bs_codes[bs_cnt] <<= 1; + bs_codes[bs_cnt] += (pw_cur < pw_pre) ? 1 : 0; + if (++bit_cnt < N_BITS) { + fsm = RxSt::PULSE; + } else { + bs_cnt++; // complete code found + fsm = RxSt::START_PULSE; // start over for further codes in frame + } + } else { + fsm = RxSt::START_PULSE; // decoding failed, start over for further codes + } + break; + } + } + } + if (bs_cnt > 0) { // complete codes found, find best code in list now + int32_t identical_max = 0; + for (int32_t ic = 0; ic != bs_cnt; ic++) { + int32_t identical_cnt = 0; + for (int32_t jc = 0; jc != bs_cnt; jc++) { + identical_cnt += (bs_codes[ic] == bs_codes[jc]) ? 1 : 0; + } + if (identical_cnt > identical_max) { + identical_max = identical_cnt; + bs_idx = ic; // save index to best code + } + } + if (bs_idx > -1) { + data.code = bs_codes[bs_idx]; + return data; // return best bs code of list + } + } + } + return {}; +} + +void BrennenstuhlProtocol::dump(const BrennenstuhlData &data) { + ESP_LOGI(TAG, "Brennenstuhl: code=0x%06" PRIx32, data.code); +} + +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/brennenstuhl_protocol.h b/esphome/components/remote_base/brennenstuhl_protocol.h new file mode 100644 index 00000000000..1d5b6217147 --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.h @@ -0,0 +1,34 @@ +#pragma once + +#include "remote_base.h" + +#include <cinttypes> + +namespace esphome::remote_base { + +struct BrennenstuhlData { + uint32_t code; + bool operator==(const BrennenstuhlData &rhs) const { return code == rhs.code; } +}; + +class BrennenstuhlProtocol : public RemoteProtocol<BrennenstuhlData> { + public: + void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override; + optional<BrennenstuhlData> decode(RemoteReceiveData src) override; + void dump(const BrennenstuhlData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Brennenstuhl) + +template<typename... Ts> class BrennenstuhlAction : public RemoteTransmitterActionBase<Ts...> { + public: + TEMPLATABLE_VALUE(uint32_t, code) + + void encode(RemoteTransmitData *dst, Ts... x) override { + BrennenstuhlData data{}; + data.code = this->code_.value(x...); + BrennenstuhlProtocol().encode(dst, data); + } +}; + +} // namespace esphome::remote_base diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index de01fa36027..30b99eeb700 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -8,6 +8,11 @@ on_beo4: - logger.log: format: "on_beo4: %u %u" args: ["x.source", "x.command"] +on_brennenstuhl: + then: + - logger.log: + format: "on_brennenstuhl: %u" + args: ["x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index d48d36bd548..c6c70496059 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -14,6 +14,12 @@ button: remote_transmitter.transmit_beo4: source: 0x01 command: 0x0C + - platform: template + name: brennenstuhl + id: button_a_on + on_press: + remote_transmitter.transmit_brennenstuhl: + code: 0xBD2E2C - platform: template name: Dyson fan up id: dyson_fan_up From 0afcdbfe731702fdd9cac7ab7a6f971c1267d1b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:40:18 -1000 Subject: [PATCH 0815/2056] [binary_sensor] Replace std::bind with inline lambda in MultiClickTrigger (#14956) --- esphome/components/binary_sensor/automation.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f8b130e08a0..f30f9d32792 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component { void setup() override { this->last_state_ = this->parent_->get_state_default(false); - auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); - this->parent_->add_on_state_callback(f); + this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); }); } float get_setup_priority() const override { return setup_priority::HARDWARE; } From 16ec237ac6a0a214bd99cc11d8b56496638065c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:40:32 -1000 Subject: [PATCH 0816/2056] [wireguard] Replace std::bind with inline lambdas (#14957) --- esphome/components/wireguard/wireguard.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 2022e25b6cf..b4641894db1 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -2,7 +2,6 @@ #ifdef USE_WIREGUARD #include <cinttypes> #include <ctime> -#include <functional> #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -48,8 +47,8 @@ void Wireguard::setup() { if (this->wg_initialized_ == ESP_OK) { ESP_LOGI(TAG, "Initialized"); this->wg_peer_offline_time_ = millis(); - this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this)); - this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup + this->srctime_->add_on_time_sync_callback([this]() { this->start_connection_(); }); + this->defer([this]() { this->start_connection_(); }); // defer to avoid blocking setup #ifdef USE_TEXT_SENSOR if (this->address_sensor_ != nullptr) { @@ -206,7 +205,7 @@ void Wireguard::enable() { void Wireguard::disable() { this->enabled_ = false; - this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop + this->defer([this]() { this->stop_connection_(); }); // defer to avoid blocking running loop ESP_LOGI(TAG, "Disabled"); this->publish_enabled_state(); } From 40a65d36b468ceccfe97d9c2e181199d2404bcff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:41:01 -1000 Subject: [PATCH 0817/2056] [nau7802] Replace std::bind with lambda to fit std::function SBO (#14958) --- esphome/components/nau7802/nau7802.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 937239b98d2..66d36dd7416 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -83,8 +83,7 @@ void NAU7802Sensor::setup() { // turn on AFE pu_ctrl |= PU_CTRL_POWERUP_ANALOG; - auto f = std::bind(&NAU7802Sensor::complete_setup_, this); - this->set_timeout(600, f); + this->set_timeout(600, [this]() { this->complete_setup_(); }); } void NAU7802Sensor::complete_setup_() { From 2ca6681896ad876d79f9b39b9e6a4b52b12de993 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:41:15 -1000 Subject: [PATCH 0818/2056] [xiaomi_rtcgq02lm] Drop unused capture from timeout lambdas (#14959) --- esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index ee3ad316e1b..0f27f09c871 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -58,15 +58,13 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) #ifdef USE_BINARY_SENSOR if (res->has_motion.has_value() && this->motion_ != nullptr) { this->motion_->publish_state(*res->has_motion); - this->set_timeout("motion_timeout", this->motion_timeout_, - [this, res]() { this->motion_->publish_state(false); }); + this->set_timeout("motion_timeout", this->motion_timeout_, [this]() { this->motion_->publish_state(false); }); } if (res->is_light.has_value() && this->light_ != nullptr) this->light_->publish_state(*res->is_light); if (res->button_press.has_value() && this->button_ != nullptr) { this->button_->publish_state(*res->button_press); - this->set_timeout("button_timeout", this->button_timeout_, - [this, res]() { this->button_->publish_state(false); }); + this->set_timeout("button_timeout", this->button_timeout_, [this]() { this->button_->publish_state(false); }); } #endif #ifdef USE_SENSOR From 14107ec452576327780f65f4ec07375c460b4ced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:42:08 -1000 Subject: [PATCH 0819/2056] [bme68x_bsec2] Store trigger time as member to avoid std::function SBO overflow (#14960) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 3 ++- esphome/components/bme68x_bsec2/bme68x_bsec2.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 0210d1e67d7..ed2ec808962 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() { uint32_t meas_dur = 0; meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); - this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { ESP_LOGV(TAG, "Measurement not required"); this->read_(curr_time_ns); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 8f4d8f61c25..1ed72eee037 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component { int8_t bme68x_status_{BME68X_OK}; int64_t last_time_ms_{0}; + int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit + // toolchains with small std::function SBO uint32_t millis_overflow_counter_{0}; std::queue<std::function<void()>> queue_; From d1aa1881bba7744ababf24cc7b773f1e08209ef7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:42:26 -1000 Subject: [PATCH 0820/2056] [core] Replace std::bind with lambdas across 13 components (#14961) --- esphome/components/dps310/dps310.cpp | 3 +-- .../components/esp32_improv/esp32_improv_component.cpp | 3 +-- .../components/improv_serial/improv_serial_component.cpp | 3 +-- esphome/components/max31855/max31855.cpp | 3 +-- esphome/components/max31856/max31856.cpp | 4 ++-- esphome/components/max31865/max31865.cpp | 3 +-- esphome/components/max6675/max6675.cpp | 3 +-- esphome/components/midea/appliance_base.h | 2 +- esphome/components/ms5611/ms5611.cpp | 6 ++---- esphome/components/ms8607/ms8607.cpp | 9 +++------ esphome/components/sht4x/sht4x.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 ++-- 12 files changed, 17 insertions(+), 28 deletions(-) diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index aa0a77cdd86..b1366cd0697 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -127,8 +127,7 @@ void DPS310Component::read_() { this->update_in_progress_ = false; this->status_clear_warning(); } else { - auto f = std::bind(&DPS310Component::read_, this); - this->set_timeout("dps310", 10, f); + this->set_timeout("dps310", 10, [this]() { this->read_(); }); } } diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index e4ae49f2356..c24b08b06fa 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() { ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); this->incoming_data_.clear(); break; } diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index edceb9a3b13..003328d535e 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -245,8 +245,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); return true; } case improv::GET_CURRENT_STATE: diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 99129880f41..8370977ce2e 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -15,8 +15,7 @@ void MAX31855Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX31855Sensor::read_data_, this); - this->set_timeout("value", 220, f); + this->set_timeout("value", 220, [this]() { this->read_data_(); }); } void MAX31855Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index ff65c8c5c94..35e12309ba3 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -41,8 +41,8 @@ void MAX31856Sensor::update() { this->one_shot_temperature_(); // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz - auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); - this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, + [this]() { this->read_thermocouple_temperature_(); }); } void MAX31856Sensor::read_thermocouple_temperature_() { diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 09b8368d071..8b06a01166e 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -60,8 +60,7 @@ void MAX31865Sensor::update() { this->write_config_(0b11100000, 0b10100000); // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz - auto f = std::bind(&MAX31865Sensor::read_data_, this); - this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); }); } void MAX31865Sensor::setup() { diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index c329cdfd426..b8527c6b1dd 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -13,8 +13,7 @@ void MAX6675Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX6675Sensor::read_data_, this); - this->set_timeout("value", 250, f); + this->set_timeout("value", 250, [this]() { this->read_data_(); }); } void MAX6675Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 660d185b491..c7737ba7d69 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -59,7 +59,7 @@ template<typename T> class ApplianceBase : public Component { public: ApplianceBase() { this->base_.setStream(&this->stream_); - this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + this->base_.addOnStateCallback([this]() { this->on_status_change(); }); dudanov::midea::ApplianceBase::setLogger( [](int level, const char *tag, int line, const String &format, va_list args) { esp_log_vprintf_(level, tag, line, format.c_str(), args); diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 7ed73400c80..d47ca245b84 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -45,8 +45,7 @@ void MS5611Component::update() { return; } - auto f = std::bind(&MS5611Component::read_temperature_, this); - this->set_timeout("temperature", 10, f); + this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); }); } void MS5611Component::read_temperature_() { uint8_t bytes[3]; @@ -62,8 +61,7 @@ void MS5611Component::read_temperature_() { return; } - auto f = std::bind(&MS5611Component::read_pressure_, this, raw_temperature); - this->set_timeout("pressure", 10, f); + this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); }); } void MS5611Component::read_pressure_(uint32_t raw_temperature) { uint8_t bytes[3]; diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index 88a3e6d7dc9..d141dcb1916 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -281,9 +281,8 @@ void MS8607Component::request_read_temperature_() { return; } - auto f = std::bind(&MS8607Component::read_temperature_, this); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("temperature", 20, f); + this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); }); } void MS8607Component::read_temperature_() { @@ -303,9 +302,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { return; } - auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("pressure", 20, f); + this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); }); } void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { @@ -325,9 +323,8 @@ void MS8607Component::request_read_humidity_(float temperature_float) { return; } - auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float); // datasheet says 15.89ms max conversion time at OSR 8192 - this->set_timeout("humidity", 20, f); + this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); }); } void MS8607Component::read_humidity_(float temperature_float) { diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 42be3262029..bf23e42e661 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -63,7 +63,7 @@ void SHT4XComponent::setup() { } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); + this->set_interval(heater_interval, [this]() { this->start_heater_(); }); } } diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index d1f74520540..0802cdec8e4 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -335,8 +335,8 @@ Sprinkler::Sprinkler(const char *name) : name_(name) { // The `name` is stored for dump_config logging this->timer_.init(2); // Timer names only need to be unique within this component instance - this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); - this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); + this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }}); + this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }}); } void Sprinkler::setup() { From cdc4ba6295f8da19a369691e072f35943760083d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:43:06 -1000 Subject: [PATCH 0821/2056] [api] Replace std::bind with lambdas in CustomAPIDevice (#14963) --- esphome/components/api/custom_api_device.h | 27 ++++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 2fd9cb0dd24..c2b0f14bcc5 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -136,8 +136,9 @@ class CustomAPIDevice { template<typename T> void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f)); + auto *obj = static_cast<T *>(this); + global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), + [obj, callback](StringRef state) { (obj->*callback)(state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -148,10 +149,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); + auto *obj = static_cast<T *>(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), - std::function<void(const std::string &)>(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional<std::string>(attribute), + std::function<void(const std::string &)>( + [obj, callback](const std::string &state) { (obj->*callback)(state); })); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant. @@ -176,8 +179,10 @@ class CustomAPIDevice { template<typename T> void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f)); + auto *obj = static_cast<T *>(this); + global_api_server->subscribe_home_assistant_state( + entity_id, optional<std::string>(attribute), + [obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -188,10 +193,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + auto *obj = static_cast<T *>(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), - std::function<void(const std::string &)>(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional<std::string>(attribute), + std::function<void(const std::string &)>( + [obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); })); } #else template<typename T> From 5637116378d5a01c7783f0f90bf1beade4341547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:43:22 -1000 Subject: [PATCH 0822/2056] [mqtt] Replace std::bind with lambdas in CustomMQTTDevice (#14964) --- esphome/components/mqtt/custom_mqtt_device.h | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 09ed7bd6d17..a003379fe03 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -189,28 +189,33 @@ class CustomMQTTDevice { template<typename T> void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &, const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast<T *>(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &t, const std::string &payload) { (obj->*callback)(t, payload); }, qos); } template<typename T> void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast<T *>(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &payload) { (obj->*callback)(payload); }, qos); } template<typename T> void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos) { - auto f = std::bind(callback, (T *) this); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast<T *>(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &) { (obj->*callback)(); }, qos); } template<typename T> void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast<T *>(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &t, JsonObject root) { (obj->*callback)(t, root); }, qos); } template<typename T> void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast<T *>(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &, JsonObject root) { (obj->*callback)(root); }, qos); } } // namespace esphome::mqtt From 1ba55049445d1e32567d0c5c9eb03d73c73c17f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:43:47 -1000 Subject: [PATCH 0823/2056] [mqtt] Replace std::bind with lambda in MQTTPublishJsonAction (#14965) --- esphome/components/mqtt/mqtt_client.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 127e4073b01..0d52d98d2fa 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -405,15 +405,14 @@ template<typename... Ts> class MQTTPublishJsonAction : public Action<Ts...> { void set_payload(std::function<void(Ts..., JsonObject)> payload) { this->payload_ = payload; } void play(const Ts &...x) override { - auto f = std::bind(&MQTTPublishJsonAction<Ts...>::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); auto retain = this->retain_.value(x...); - this->parent_->publish_json(topic, f, qos, retain); + this->parent_->publish_json( + topic, [this, x...](JsonObject root) { this->payload_(x..., root); }, qos, retain); } protected: - void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); } std::function<void(Ts..., JsonObject)> payload_; MQTTClientComponent *parent_; }; From e7dcf54a770125800372efd5195be7480ddad93e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:44:02 -1000 Subject: [PATCH 0824/2056] [http_request] Replace std::bind with lambdas in HttpRequestSendAction (#14966) --- esphome/components/http_request/http_request.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8bdea470b5f..73dbda86943 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -487,12 +487,10 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { body = this->body_.value(x...); } if (!this->json_.empty()) { - auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); }); } if (this->json_func_ != nullptr) { - auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); }); } std::vector<Header> request_headers; request_headers.reserve(this->request_headers_.size()); @@ -561,7 +559,6 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{}; std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"}; From 63f0d054b7b3182f03a94c520c6f239980dce4dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:44:16 -1000 Subject: [PATCH 0825/2056] [core] Replace std::bind with lambda in DelayAction (#14968) --- esphome/core/base_automation.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 67e1755cc93..985f26e7116 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -188,7 +188,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon // Issue #10264: This is a workaround for parallel script delays interfering with each other. // Optimization: For no-argument delays (most common case), use direct lambda - // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + // to avoid overhead from capturing arguments by value if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, @@ -196,9 +196,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { - // For delays with arguments, use std::bind to preserve argument values + // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay - auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...); + auto f = [this, x...]() { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), From a8ed781f3ece3326765e99a4f85876a9be754648 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:44:35 -1000 Subject: [PATCH 0826/2056] [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) --- esphome/components/time/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ffa408db97..9821046a737 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = { def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples + if not iana_key: + return None try: package_loc, resource = iana_key.rsplit("/", 1) except ValueError: - return None - package = "tzdata.zoneinfo." + package_loc.replace("/", ".") + # Handle top-level timezone entries like "UTC", "GMT" + package = "tzdata.zoneinfo" + resource = iana_key + else: + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: return (resources.files(package) / resource).read_bytes() - except (FileNotFoundError, ModuleNotFoundError): + except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None From 37a3c3ab3abef07f016efa7e6723ee24457baed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:50:38 -1000 Subject: [PATCH 0827/2056] [core] Replace std::bind with placeholders to lambdas (#14962) --- esphome/components/haier/haier_base.cpp | 2 +- esphome/components/haier/hon_climate.cpp | 38 +++++++++++-------- .../components/haier/smartair2_climate.cpp | 17 +++++---- .../number/homeassistant_number.cpp | 16 ++++---- .../wifi/wifi_component_libretiny.cpp | 4 +- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 35eaf36d329..4a06066d3c9 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -242,7 +242,7 @@ void HaierClimateBase::setup() { this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); this->haier_protocol_.set_default_timeout_handler( - std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); }); this->set_handlers(); this->initialization(); } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b7888f7976e..92defe560e0 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -301,32 +301,38 @@ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_ID, - std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_id_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_management_information_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_ALARM_STATUS, - std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_alarm_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_message_handler( - haier_protocol::FrameType::ALARM_STATUS, - std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); + this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS, + [this](haier_protocol::FrameType type, const uint8_t *data, size_t size) { + return this->alarm_status_message_handler_(type, data, size); + }); } void HonClimate::dump_config() { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index e91224e2d8e..2be5d130504 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_default_timeout_handler( - std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); }); } void Smartair2Climate::dump_config() { diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 00ea88ff16b..da802b7fe9f 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) { } void HomeassistantNumber::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, + [this](StringRef state) { this->state_changed_(state); }); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state(this->entity_id_, "min", + [this](StringRef min) { this->min_retrieved_(min); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "max", + [this](StringRef max) { this->max_retrieved_(max); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "step", + [this](StringRef step) { this->step_retrieved_(step); }); } void HomeassistantNumber::dump_config() { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 4c4150e44d4..b049a0413c1 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -629,8 +629,8 @@ void WiFiComponent::wifi_pre_setup_() { return; } - auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); + WiFi.onEvent( + [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); }); // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } From c2a96ea293694126c9e43d8c56cf58130289221b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:54:53 -1000 Subject: [PATCH 0828/2056] Bump ruff from 0.15.6 to 0.15.7 (#14977) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index acd8383a2fc..0d3f0671f54 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.6 # also change in .pre-commit-config.yaml when updating +ruff==0.15.7 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 902258b56e3401b1e2c3f439c22e6e6bf9ee84e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:11:06 -1000 Subject: [PATCH 0829/2056] [preferences] Compile out loop() when flash_write_interval is non-zero (#14943) --- esphome/components/preferences/__init__.py | 6 +++++- esphome/components/preferences/syncer.h | 17 +++++++---------- esphome/core/defines.h | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py index c6bede891ab..c4268727282 100644 --- a/esphome/components/preferences/__init__.py +++ b/esphome/components/preferences/__init__.py @@ -21,5 +21,9 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.PREFERENCES) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_write_interval(config[CONF_FLASH_WRITE_INTERVAL])) + write_interval = config[CONF_FLASH_WRITE_INTERVAL] + if write_interval.total_milliseconds == 0: + cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP") + else: + cg.add(var.set_write_interval(write_interval)) await cg.register_component(var, config) diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index 96716d3f304..e28cc8c8d54 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -8,24 +8,21 @@ namespace preferences { class IntervalSyncer final : public Component { public: +#ifdef USE_PREFERENCES_SYNC_EVERY_LOOP + void loop() override { global_preferences->sync(); } +#else void set_write_interval(uint32_t write_interval) { this->write_interval_ = write_interval; } void setup() override { - if (this->write_interval_ != 0) { - set_interval(this->write_interval_, []() { global_preferences->sync(); }); - // When using interval-based syncing, we don't need the loop - this->disable_loop(); - } - } - void loop() override { - if (this->write_interval_ == 0) { - global_preferences->sync(); - } + this->set_interval(this->write_interval_, []() { global_preferences->sync(); }); } +#endif void on_shutdown() override { global_preferences->sync(); } float get_setup_priority() const override { return setup_priority::BUS; } +#ifndef USE_PREFERENCES_SYNC_EVERY_LOOP protected: uint32_t write_interval_{60000}; +#endif }; } // namespace preferences diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c817f8ef27a..d94b7e9f5d2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -118,6 +118,7 @@ #define USE_NUMBER #define USE_OUTPUT #define USE_POWER_SUPPLY +#define USE_PREFERENCES_SYNC_EVERY_LOOP #define USE_QR_CODE #define USE_SAFE_MODE_CALLBACK #define USE_SELECT From a9cb7143dc262f09e4c73c04a0bac4c846ed109f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:11:17 -1000 Subject: [PATCH 0830/2056] [core] Inline calculate_looping_components_ into header (#14944) --- esphome/core/application.cpp | 16 ---------------- esphome/core/application.h | 14 +++++++++++++- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3a9e825e041..08df3854755 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -394,22 +394,6 @@ void Application::teardown_components(uint32_t timeout_ms) { } } -void Application::calculate_looping_components_() { - // FixedVector capacity was pre-initialized by codegen with the exact count - // of components that override loop(), computed at C++ compile time. - - // Add all components with loop override that aren't already LOOP_DONE - // Some components (like logger) may call disable_loop() during initialization - // before setup runs, so we need to respect their LOOP_DONE state - this->add_looping_components_by_state_(false); - - this->looping_components_active_end_ = this->looping_components_.size(); - - // Then add any components that are already LOOP_DONE to the inactive section - // This handles components that called disable_loop() during initialization - this->add_looping_components_by_state_(true); -} - void Application::add_looping_components_by_state_(bool match_loop_done) { for (auto *obj : this->components_) { if (obj->has_overridden_loop() && diff --git a/esphome/core/application.h b/esphome/core/application.h index 23bb209eaf4..26abc154332 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -595,7 +595,19 @@ class Application { void register_component_impl_(Component *comp, bool has_loop); - void calculate_looping_components_(); + void calculate_looping_components_() { + // FixedVector capacity was pre-initialized by codegen with the exact count + // of components that override loop(), computed at C++ compile time. + + // Add all components with loop override that aren't already LOOP_DONE + // Some components (like logger) may call disable_loop() during initialization + // before setup runs, so we need to respect their LOOP_DONE state + this->add_looping_components_by_state_(false); + this->looping_components_active_end_ = this->looping_components_.size(); + // Then add any components that are already LOOP_DONE to the inactive section + // This handles components that called disable_loop() during initialization + this->add_looping_components_by_state_(true); + } void add_looping_components_by_state_(bool match_loop_done); // These methods are called by Component::disable_loop() and Component::enable_loop() From de177d24451f92b2233836e964d2b442981c50cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:11:49 -1000 Subject: [PATCH 0831/2056] [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) --- esphome/components/logger/__init__.py | 29 ++++++++++++++++++--------- esphome/core/config.py | 4 +++- esphome/coroutine.py | 10 +++++++-- esphome/writer.py | 3 +++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 675f9a2ca4b..a5601e6a8f4 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -56,6 +56,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -323,12 +324,11 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(CoroPriority.DIAGNOSTICS) -async def to_code(config): - baud_rate = config[CONF_BAUD_RATE] +@coroutine_with_priority(CoroPriority.EARLY_INIT) +async def to_code(config: ConfigType) -> None: + baud_rate: int = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level - initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( @@ -347,10 +347,23 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - # pre_setup() must be called before init_log_buffer() because - # init_log_buffer() calls disable_loop() which may log at VV level, - # and global_logger must be set before any logging occurs. + # pre_setup() sets global_logger and must run before any other code + # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) + initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + cg.add(log.set_log_level(initial_level)) + + # Schedule the rest of logger setup at DIAGNOSTICS priority, after + # Application is constructed (CORE priority) but before most components. + CORE.add_job(_late_logger_init, config) + + +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def _late_logger_init(config: ConfigType) -> None: + """Finish logger setup after Application is constructed.""" + log = await cg.get_variable(config[CONF_ID]) + level = config[CONF_LEVEL] + baud_rate: int = config[CONF_BAUD_RATE] if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: @@ -363,8 +376,6 @@ async def to_code(config): cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host - cg.add(log.set_log_level(initial_level)) - # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: diff --git a/esphome/core/config.py b/esphome/core/config.py index e112720f2b4..e02c6ec75f2 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -587,7 +587,9 @@ async def _add_looping_components() -> None: @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: - cg.add_global(cg.global_ns.namespace("esphome").using) + # using namespace esphome is hardcoded in writer.py to guarantee it + # precedes all variable declarations regardless of coroutine priority. + # These can be used by user lambdas, put them to default scope # picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__")) diff --git a/esphome/coroutine.py b/esphome/coroutine.py index f5d512e510e..3ce94cc9791 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -63,7 +63,13 @@ class CoroPriority(enum.IntEnum): resolution during code generation. """ - # Platform initialization - must run first + # Early init - runs before platform init and before Application exists. + # Currently used only to connect logging so ESP_LOG* calls work + # immediately in all subsequent phases. + # Examples: logger (1100) + EARLY_INIT = 1100 + + # Platform initialization # Examples: esp32, esp8266, rp2040 PLATFORM = 1000 @@ -83,7 +89,7 @@ class CoroPriority(enum.IntEnum): CORE = 100 # Diagnostic and debugging systems - # Examples: logger (90) + # Examples: debug component (90) DIAGNOSTICS = 90 # Status and monitoring systems diff --git a/esphome/writer.py b/esphome/writer.py index fd4c811fb35..69a35d00e34 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -381,7 +381,10 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() + # using namespace esphome must precede all variable declarations since + # codegen types assume this namespace is in scope (esphome_ns = global_ns). global_s = '#include "esphome.h"\n' + global_s += "using namespace esphome;\n" global_s += CORE.cpp_global_section full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" From 7ac001e994428e28f6c838c7b216527af6e2affd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:12:03 -1000 Subject: [PATCH 0832/2056] [mhz19] Fix unused function warning for detection_range_to_log_string (#14981) --- esphome/components/mhz19/mhz19.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index bccea7d423a..ff518808d9e 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -16,6 +16,7 @@ static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88}; static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10}; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) { switch (range) { case MHZ19_DETECTION_RANGE_0_2000PPM: @@ -28,6 +29,7 @@ static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) return LOG_STR("default"); } } +#endif uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; From 151f71e033988cef02905d6600f89f98fb0aff77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:12:15 -1000 Subject: [PATCH 0833/2056] [ci] Add libretiny and zephyr to memory impact platform filter (#14985) --- script/determine-jobs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 9f322387801..d94d472c9ed 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -111,11 +111,13 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( "esp32", # ESP32 platform implementation "esp8266", # ESP8266 platform implementation "rp2040", # Raspberry Pi Pico / RP2040 platform implementation + "libretiny", # LibreTiny base platform implementation "bk72xx", # Beken BK72xx platform implementation (uses LibreTiny) "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) "host", # Host platform (for testing on development machine) "nrf52", # Nordic nRF52 platform implementation (uses Zephyr) + "zephyr", # Zephyr RTOS platform implementation } ) From b02f0e3c5feb2112e82302ac543a77caee451b80 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:39:10 +1000 Subject: [PATCH 0834/2056] [sdl] Fix get_width()/height() when rotation used (#14950) --- esphome/components/sdl/sdl_esphome.cpp | 37 ++++++++++++++++++++++++++ esphome/components/sdl/sdl_esphome.h | 4 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index f235e4e68cf..74ca2ce39a5 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -5,6 +5,30 @@ namespace esphome { namespace sdl { +int Sdl::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int Sdl::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + void Sdl::setup() { SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, @@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = this->width_ - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = this->height_ - x - 1; + x = tmp; + } + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index c025e8ff6e9..ce34cb817e0 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -33,8 +33,8 @@ class Sdl : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } template<typename F> void add_key_listener(int32_t keycode, F &&callback) { From 7df550f2a9a9049e742496298f94c9a6bb46cf9e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:52:52 +1000 Subject: [PATCH 0835/2056] Ensure lvgl libs available when editing for host (#14987) --- .clang-tidy.hash | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 72023e511db..c32978d4111 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb +9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6 diff --git a/platformio.ini b/platformio.ini index c5a4c630dff..d3a482b652b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -546,6 +546,7 @@ extends = common platform = platformio/native lib_deps = esphome/noise-c@0.1.11 ; used by api + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} -DUSE_HOST From 6e87f8eb4e350f4a7bbcb0d6dd6fc3c8d9d2b11e Mon Sep 17 00:00:00 2001 From: Kent Gibson <warthog618@gmail.com> Date: Fri, 20 Mar 2026 10:06:58 +0800 Subject: [PATCH 0836/2056] [template] alarm_control_panel collapse SensorDataStore and bypassed_sensor_indicies into SensorInfo (#14852) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- .../template_alarm_control_panel.cpp | 51 ++++++++++--------- .../template_alarm_control_panel.h | 24 ++++----- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 651aa3c489e..a224ab84599 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -16,17 +16,15 @@ static const char *const TAG = "template.alarm_control_panel"; TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; #ifdef USE_BINARY_SENSOR -void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) { - // Save the flags and type. Assign a store index for the per sensor data type. - SensorDataStore sd; - sd.last_chime_state = false; +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags, AlarmSensorType type) { + // Save the sensor pointer, flags, and type in the per-sensor info structure. AlarmSensor alarm_sensor; alarm_sensor.sensor = sensor; alarm_sensor.info.flags = flags; alarm_sensor.info.type = type; - alarm_sensor.info.store_index = this->next_store_index_++; + alarm_sensor.info.chime_active = false; + alarm_sensor.info.auto_bypassed = false; this->sensors_.push_back(alarm_sensor); - this->sensor_data_.push_back(sd); }; // Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS @@ -55,7 +53,7 @@ void TemplateAlarmControlPanel::dump_config() { (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR for (const auto &alarm_sensor : this->sensors_) { - const uint16_t flags = alarm_sensor.info.flags; + const uint8_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -95,7 +93,7 @@ void TemplateAlarmControlPanel::loop() { delay = this->arming_night_time_; } if ((millis() - this->last_update_) > delay) { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(this->desired_state_); } return; @@ -117,26 +115,25 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (const auto &alarm_sensor : this->sensors_) { - const auto &info = alarm_sensor.info; + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open - if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { + if ((!info.chime_active) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { this->chime_callback_.call(); } } // Record the sensor state change - this->sensor_data_[info.store_index].last_chime_state = sensor->state; + info.chime_active = sensor->state; } // Check for faulted sensors if (sensor->state) { // Skip if auto bypassed - if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), - info.store_index) == 1) { + if (info.auto_bypassed) { continue; } // Skip if bypass armed home @@ -239,23 +236,33 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p if (delay > 0) { this->publish_state(ACP_STATE_ARMING); } else { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(state); } } -void TemplateAlarmControlPanel::bypass_before_arming() { +void TemplateAlarmControlPanel::auto_bypass_sensors_() { #ifdef USE_BINARY_SENSOR - for (const auto &alarm_sensor : this->sensors_) { + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for faulted bypass_auto sensors and remove them from monitoring - if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); + if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); + info.auto_bypassed = true; } } #endif } +void TemplateAlarmControlPanel::clear_auto_bypassed_sensors_() { +#ifdef USE_BINARY_SENSOR + for (auto &alarm_sensor : this->sensors_) { + alarm_sensor.info.auto_bypassed = false; + } +#endif +} + void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { auto opt_state = call.get_state(); if (opt_state) { @@ -273,9 +280,7 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { } this->desired_state_ = ACP_STATE_DISARMED; this->publish_state(ACP_STATE_DISARMED); -#ifdef USE_BINARY_SENSOR - this->bypassed_sensor_indicies_.clear(); -#endif + this->clear_auto_bypassed_sensors_(); } else if (state == ACP_STATE_TRIGGERED) { this->publish_state(ACP_STATE_TRIGGERED); } else if (state == ACP_STATE_PENDING) { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 4f32e99fd7b..57a99f2830e 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -18,7 +18,7 @@ namespace esphome::template_ { #ifdef USE_BINARY_SENSOR -enum BinarySensorFlags : uint16_t { +enum BinarySensorFlags : uint8_t { BINARY_SENSOR_MODE_NORMAL = 1 << 0, BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, @@ -41,14 +41,11 @@ enum TemplateAlarmControlPanelRestoreMode { }; #ifdef USE_BINARY_SENSOR -struct SensorDataStore { - bool last_chime_state; -}; - struct SensorInfo { - uint16_t flags; + uint8_t flags; AlarmSensorType type; - uint8_t store_index; + bool chime_active; + bool auto_bypassed; }; struct AlarmSensor { @@ -68,7 +65,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } - void bypass_before_arming(); + // Remove before 2026.10.0 + ESPDEPRECATED("bypass_before_arming() is deprecated and will be removed in 2026.10.0", "2026.4.0") + void bypass_before_arming() { this->auto_bypass_sensors_(); } #ifdef USE_BINARY_SENSOR /** Initialize the sensors vector with the specified capacity. @@ -83,7 +82,7 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl * @param flags The OR of BinarySensorFlags for the sensor. * @param type The sensor type which determines its triggering behaviour. */ - void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, + void add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags = 0, AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif @@ -141,11 +140,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl #ifdef USE_BINARY_SENSOR // List of binary sensors with their alarm-specific info FixedVector<AlarmSensor> sensors_; - // a list of automatically bypassed sensors - std::vector<uint8_t> bypassed_sensor_indicies_; - // Per sensor data store - std::vector<SensorDataStore> sensor_data_; - uint8_t next_store_index_ = 0; #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -170,6 +164,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool is_code_valid_(optional<std::string> code); void arm_(optional<std::string> code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); + void auto_bypass_sensors_(); + void clear_auto_bypassed_sensors_(); }; } // namespace esphome::template_ From 02ada93ea5aeb5cee2eb79dff8853269331981ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 18:40:33 -1000 Subject: [PATCH 0837/2056] [wifi] Reject WiFi config on RP2040/RP2350 boards without CYW43 chip (#14990) --- esphome/components/rp2040/__init__.py | 18 ++++++++ esphome/components/rp2040/boards.py | 10 +++++ esphome/components/rp2040/generate_boards.py | 8 +++- esphome/components/wifi/__init__.py | 8 ++++ .../components/test_rp2040_generate_boards.py | 45 +++++++++++++++++-- 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 71e5f1488cb..0bb18110693 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed +from . import boards from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema @@ -35,6 +36,23 @@ AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def get_board() -> str: + """Return the configured board name.""" + return CORE.data[KEY_RP2040][KEY_BOARD] + + +def board_has_wifi() -> bool: + """Return True if the configured board has WiFi (CYW43 wireless chip). + + Returns True for unknown/custom boards to avoid rejecting valid + configurations for boards not in the generated list. + """ + board_info = boards.BOARDS.get(get_board()) + if board_info is None: + return True + return board_info.get("wifi", False) + + def set_core_data(config): CORE.data[KEY_RP2040] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040 diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c99934567a1..aac12eae5ae 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1910,6 +1910,7 @@ BOARDS = { "name": "Pimoroni PicoPlus2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "pimoroni_plasma2040": { @@ -1926,6 +1927,7 @@ BOARDS = { "name": "Pimoroni Plasma2350W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "pimoroni_servo2040": { "name": "Pimoroni Servo2040", @@ -1976,12 +1978,14 @@ BOARDS = { "name": "Raspberry Pi Pico 2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "sea_picro": { @@ -2013,6 +2017,7 @@ BOARDS = { "name": "Soldered Electronics NULA RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "solderparty_rp2040_stamp": { "name": "Solder Party RP2040 Stamp", @@ -2038,6 +2043,7 @@ BOARDS = { "name": "SparkFun IoT RedBoard RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "sparkfun_micromodrp2040": { "name": "SparkFun MicroMod RP2040", @@ -2063,18 +2069,21 @@ BOARDS = { "name": "SparkFun Thing Plus RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller": { "name": "SparkFun XRP Controller", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller_beta": { "name": "SparkFun XRP Controller (Beta)", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "upesy_rp2040_devkit": { @@ -2161,6 +2170,7 @@ BOARDS = { "name": "Waveshare RP2350B Plus W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "wiznet_5100s_evb_pico": { "name": "WIZnet W5100S-EVB-Pico", diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py index 7ea02d185e9..8af261396cc 100644 --- a/esphome/components/rp2040/generate_boards.py +++ b/esphome/components/rp2040/generate_boards.py @@ -78,11 +78,17 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: display_name = f"{vendor} {name}".strip() if vendor else name - boards[board_name] = { + extra_flags = build.get("extra_flags", "") + has_wifi = "PICO_CYW43_SUPPORTED=1" in extra_flags + + board_entry: dict = { "name": display_name, "mcu": mcu, "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), } + if has_wifi: + board_entry["wifi"] = True + boards[board_name] = board_entry # Get pins for this variant if variant not in variant_pins_cache: diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 9f73b1cc6f5..33557f03c75 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -235,6 +235,14 @@ def validate_variant(_): variant = get_esp32_variant() if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") + if CORE.is_rp2040: + from esphome.components.rp2040 import board_has_wifi, get_board + + if not board_has_wifi(): + raise cv.Invalid( + f"Board '{get_board()}' does not have WiFi support (no CYW43 wireless chip). " + f"Use a WiFi-capable board like 'rpipicow' or 'rpipico2w'." + ) def _apply_min_auth_mode_default(config): diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py index 2e40ed08ba1..551e88f6f6f 100644 --- a/tests/unit_tests/components/test_rp2040_generate_boards.py +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -59,6 +59,7 @@ def _add_board( vendor: str = "", name: str | None = None, pins_header: str | None = None, + extra_flags: str = "", ) -> None: """Add a board JSON and variant to the fake arduino-pico tree.""" if variant is None: @@ -69,11 +70,15 @@ def _add_board( json_dir = arduino_pico / "tools" / "json" variants_dir = arduino_pico / "variants" + build: dict = { + "mcu": mcu, + "variant": variant, + } + if extra_flags: + build["extra_flags"] = extra_flags + board_json = { - "build": { - "mcu": mcu, - "variant": variant, - }, + "build": build, "name": name, "vendor": vendor, } @@ -271,3 +276,35 @@ def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: assert "MISO" not in board_pins["badpin"] assert boards["badpin"]["max_virtual_pin"] == 64 + + +def test_cyw43_supported_flag_sets_wifi(arduino_pico: Path) -> None: + """Boards with PICO_CYW43_SUPPORTED=1 in extra_flags should have wifi=True.""" + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO_W -DPICO_CYW43_SUPPORTED=1 -DCYW43_PIN_WL_DYNAMIC=1", + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["wifi"] is True + + +def test_board_without_cyw43_has_no_wifi(arduino_pico: Path) -> None: + """Boards without PICO_CYW43_SUPPORTED should not have wifi field.""" + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO", + ) + + _, boards = load_boards(arduino_pico) + + assert "wifi" not in boards["rpipico"] From d59c006ff9067c8f7b6a439548f5ee0ef0c85e43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 20:56:51 -1000 Subject: [PATCH 0838/2056] [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) --- .../uart/uart_component_esp_idf.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d7..8168e49805a 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast<gpio_num_t>(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast<gpio_num_t>(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 12b3aec5672312332c77eaf184f64b2ec32e74a5 Mon Sep 17 00:00:00 2001 From: Keith Roehrenbeck <kroehre@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:11:57 -0500 Subject: [PATCH 0839/2056] [ld2450] Fix zone target counts including untracked ghost targets (#15026) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 0a1147c9240..6230a8c30bb 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -530,10 +530,11 @@ void LD2450Component::handle_periodic_data_() { } #endif - // Store target info for zone target count - this->target_info_[index].x = tx; - this->target_info_[index].y = ty; - this->target_info_[index].is_moving = is_moving; + // Store target info for zone target count. Zero out untracked targets (td==0) + // so stale coordinates don't produce ghost counts in count_targets_in_zone_(). + this->target_info_[index].x = (td > 0) ? tx : 0; + this->target_info_[index].y = (td > 0) ? ty : 0; + this->target_info_[index].is_moving = (td > 0) && is_moving; } // End loop thru targets From 5a9977cf5c1ff918f6c20e2281301f82ed253ecd Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:35:41 +0100 Subject: [PATCH 0840/2056] [lvgl] Fix arc indicator widget not registered in widget_map (#14986) --- esphome/components/lvgl/widgets/meter.py | 2 +- tests/components/lvgl/lvgl-package.yaml | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index d45371b3a76..63cc645f221 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -406,7 +406,7 @@ class MeterType(WidgetType): lv.scale_section_set_style( tvar, LV_PART.MAIN, await arc_style.get_var() ) - lw = Widget(tvar, arc_indicator_type) + lw = Widget.create(iid, tvar, arc_indicator_type) await set_indicator_values(lw, v) if t == CONF_TICK_STYLE: diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7d96b12a019..606f57d6a11 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -37,7 +37,11 @@ lvgl: on_resume: logger.log: LVGL has resumed on_boot: - logger.log: LVGL has started + - logger.log: LVGL has started + - lvgl.indicator.update: + id: meter_arc_indicator + start_value: 0 + end_value: 180 bg_color: light_blue disp_bg_color: color_id disp_bg_image: cat_image @@ -1110,6 +1114,12 @@ lvgl: color: 0xA0A0A0 length: 80% opa: 0% + - arc: + id: meter_arc_indicator + color: 0xFF0000 + width: 6 + start_value: 0 + end_value: 360 - id: page3 layout: Horizontal pad_all: 6px From 7257bed1e95ddc4bde5a9c61d83a93b57c88566a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:41:20 -1000 Subject: [PATCH 0841/2056] Bump CodSpeedHQ/action from 4.11.1 to 4.12.1 (#15024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ead87ad0874..965e23870d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4 + uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation From a3fd1d5d00714968e630ec692893752b3d16e2e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:41:45 -1000 Subject: [PATCH 0842/2056] Bump github/codeql-action from 4.33.0 to 4.34.1 (#15023) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2ef1a5af314..6baab70b427 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: category: "/language:${{matrix.language}}" From 9e7cdaf4758ff997cf5f385946aab97c992213f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:10:40 -1000 Subject: [PATCH 0843/2056] Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027) 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 9e2f4efe3e9..10e56c3b49d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.1 +aioesphomeapi==44.6.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 896b6ec8c9acd581a2d95d66ea00b9d617ad36ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 12:06:23 -1000 Subject: [PATCH 0844/2056] [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) --- esphome/components/api/api_connection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d55b5dffb6a..40c27b224b2 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * // A stalled handshake from a buggy client or network glitch holds a connection // slot, which can prevent legitimate clients from reconnecting. Also hardens // against the less likely case of intentional connection slot exhaustion. -static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); From 5e516e78e43e7b9b61fb334f5832ac671ec20b74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 12:13:49 -1000 Subject: [PATCH 0845/2056] [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) --- esphome/components/wifi/wifi_component_esp8266.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0bf7934878a..5514f1c6be5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -92,13 +92,23 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { return ret; } bool WiFiComponent::wifi_apply_power_save_() { + // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode. + // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2 + // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451 + // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP + // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55 sleep_type_t power_save; switch (this->power_save_) { case WIFI_POWER_SAVE_LIGHT: - power_save = LIGHT_SLEEP_T; + // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active. + // Matches ESP32's WIFI_PS_MIN_MODEM. + power_save = MODEM_SLEEP_T; break; case WIFI_POWER_SAVE_HIGH: - power_save = MODEM_SLEEP_T; + // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons. + // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM. + // See https://github.com/esphome/esphome/issues/14999 + power_save = LIGHT_SLEEP_T; break; case WIFI_POWER_SAVE_NONE: default: From ed8c062d9fc688e97758e702fb743d966334db5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:02 -0400 Subject: [PATCH 0846/2056] [esp32_touch] Fix initial state never published when sensor untouched (#15032) --- esphome/components/esp32_touch/esp32_touch.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f9..0d331b29d60 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } From 0b01f9fc42aa7e848dfbab78500d8ba97778dc29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:57:51 -1000 Subject: [PATCH 0847/2056] [web_server] Increase httpd task stack size to prevent stack overflow (#14997) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 60816fc6dd2..fb0c17c854d 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -121,7 +121,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. From 8fa2e75afaacb55d6b8aaa3ca49f746789ab8b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:58:02 -1000 Subject: [PATCH 0848/2056] [core] Add copy() method to StringRef for std::string compatibility (#15028) --- esphome/core/string_ref.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 60472027530..34ba2474b2a 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast<const uint8_t *>(base_); } From a9a8f4cb3bf9f304abcd872995dc91aa988859ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:58:14 -1000 Subject: [PATCH 0849/2056] [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) --- esphome/core/time.cpp | 16 ++++++---------- esphome/core/time.h | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7b..6add82e7d14 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4a..1716c51ffd5 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include <cstdint> #include <cstdlib> #include <cstring> From 2d39cc2540e877f7bec755b74c2cdf60e625c6b0 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:38:04 -0400 Subject: [PATCH 0850/2056] [spa06_i2c] Add SPA06-003 Temperature and Pressure Sensor - I2C support (Part 2 of 3) (#14522) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_i2c/__init__.py | 0 esphome/components/spa06_i2c/sensor.py | 23 +++++++++++++++++++ esphome/components/spa06_i2c/spa06_i2c.cpp | 14 +++++++++++ esphome/components/spa06_i2c/spa06_i2c.h | 20 ++++++++++++++++ tests/components/spa06_i2c/common.yaml | 15 ++++++++++++ .../components/spa06_i2c/test.esp32-idf.yaml | 4 ++++ .../spa06_i2c/test.esp8266-ard.yaml | 4 ++++ .../components/spa06_i2c/test.rp2040-ard.yaml | 4 ++++ 9 files changed, 85 insertions(+) create mode 100644 esphome/components/spa06_i2c/__init__.py create mode 100644 esphome/components/spa06_i2c/sensor.py create mode 100644 esphome/components/spa06_i2c/spa06_i2c.cpp create mode 100644 esphome/components/spa06_i2c/spa06_i2c.h create mode 100644 tests/components/spa06_i2c/common.yaml create mode 100644 tests/components/spa06_i2c/test.esp32-idf.yaml create mode 100644 tests/components/spa06_i2c/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_i2c/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 5869925d7c7..e3e09cbc113 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -458,6 +458,7 @@ esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net +esphome/components/spa06_i2c/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_i2c/__init__.py b/esphome/components/spa06_i2c/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/spa06_i2c/sensor.py b/esphome/components/spa06_i2c/sensor.py new file mode 100644 index 00000000000..b48a5bca505 --- /dev/null +++ b/esphome/components/spa06_i2c/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["i2c"] + +spa06_ns = cg.esphome_ns.namespace("spa06_i2c") +SPA06I2CComponent = spa06_ns.class_( + "SPA06I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(SPA06I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/spa06_i2c/spa06_i2c.cpp b/esphome/components/spa06_i2c/spa06_i2c.cpp new file mode 100644 index 00000000000..4970b0822d7 --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.cpp @@ -0,0 +1,14 @@ +#include "spa06_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::spa06_i2c { + +static const char *const TAG = "spa06_i2c"; + +void SPA06I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + SPA06Component::dump_config(); +} + +} // namespace esphome::spa06_i2c diff --git a/esphome/components/spa06_i2c/spa06_i2c.h b/esphome/components/spa06_i2c/spa06_i2c.h new file mode 100644 index 00000000000..6b4bce3a4ec --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.h @@ -0,0 +1,20 @@ +#pragma once +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::spa06_i2c { + +class SPA06I2CComponent : public spa06_base::SPA06Component, public i2c::I2CDevice { + public: + bool spa_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool spa_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return write_bytes(a_register, data, len); + } + void dump_config() override; +}; + +} // namespace esphome::spa06_i2c diff --git a/tests/components/spa06_i2c/common.yaml b/tests/components/spa06_i2c/common.yaml new file mode 100644 index 00000000000..d2be0e3ac97 --- /dev/null +++ b/tests/components/spa06_i2c/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_i2c + i2c_id: i2c_bus + address: 0x77 + temperature: + id: spa06_i2c_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_i2c_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_i2c/test.esp32-idf.yaml b/tests/components/spa06_i2c/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/spa06_i2c/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.esp8266-ard.yaml b/tests/components/spa06_i2c/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/spa06_i2c/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.rp2040-ard.yaml b/tests/components/spa06_i2c/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/spa06_i2c/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 12ead0408ab97d21f2d01fc691db2baed7a99d9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:00:56 -1000 Subject: [PATCH 0851/2056] [gpio] Use constexpr uint32_t timer ID for interlock timeout (#15010) --- esphome/components/gpio/switch/gpio_switch.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9043a6a493e..d461fab051d 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,6 +5,7 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -51,7 +52,7 @@ void GPIOSwitch::write_state(bool state) { } } if (found && this->interlock_wait_time_ != 0) { - this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + this->set_timeout(INTERLOCK_TIMEOUT_ID, this->interlock_wait_time_, [this, state] { // Don't write directly, call the function again // (some other switch may have changed state while we were waiting) this->write_state(state); @@ -61,7 +62,7 @@ void GPIOSwitch::write_state(bool state) { } else if (this->interlock_wait_time_ != 0) { // If we are switched off during the interlock wait time, cancel any pending // re-activations - this->cancel_timeout("interlock"); + this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } this->pin_->digital_write(state); From 391ffe34f87158a0b308634018be3a03ac33814a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:01:11 -1000 Subject: [PATCH 0852/2056] [rp2040] Fix get_mac_address_raw to use ethernet MAC when WiFi unavailable (#15033) --- esphome/components/rp2040/helpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index ad69192af97..e360b45ebbb 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -10,6 +10,7 @@ #include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) #include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock) +#include "esphome/components/ethernet/ethernet_component.h" #endif #include <hardware/structs/rosc.h> #include <hardware/sync.h> @@ -71,6 +72,8 @@ LwIPLock::~LwIPLock() {} void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI WiFi.macAddress(mac); +#elif defined(USE_ETHERNET) + ethernet::global_eth_component->get_eth_mac_address_raw(mac); #endif } From 51335e88301d8ae4f6872cb8546c799ef300f964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:01:30 -1000 Subject: [PATCH 0853/2056] [ledc] Fix deprecated intr_type warning on ESP-IDF 6.0+ (#15009) --- esphome/components/ledc/ledc_output.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index d2f2d72acb9..5b7b6c7ee63 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -193,7 +193,9 @@ void LEDCOutput::setup() { chan_conf.gpio_num = static_cast<gpio_num_t>(this->pin_->get_pin()); chan_conf.speed_mode = speed_mode; chan_conf.channel = chan_num; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) chan_conf.intr_type = LEDC_INTR_DISABLE; +#endif chan_conf.timer_sel = timer_num; chan_conf.duty = this->inverted_ == this->pin_->is_inverted() ? 0 : (1U << this->bit_depth_); chan_conf.hpoint = hpoint; From edf5542559a4868c57df319ad89e4a7ebe829658 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:05:17 -1000 Subject: [PATCH 0854/2056] [analyze-memory] Attribute extern C symbols to components via source file mapping (#15006) --- esphome/analyze_memory/__init__.py | 125 +++++++++++++++++++++++++++++ esphome/analyze_memory/const.py | 1 - 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 7954c228224..48ecf2c1dc3 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -201,6 +201,9 @@ class MemoryAnalyzer: self._cswtch_symbols: list[tuple[str, int, str, str]] = [] # Library symbol mapping: symbol_name -> library_name self._lib_symbol_map: dict[str, str] = {} + # Source file symbol mapping: symbol_name -> component_name + # Used for extern "C" and other symbols without C++ namespace + self._source_symbol_map: dict[str, str] = {} # Library dir to name mapping: "lib641" -> "espsoftwareserial", # "espressif__mdns" -> "mdns" self._lib_hash_to_name: dict[str, str] = {} @@ -214,6 +217,7 @@ class MemoryAnalyzer: self._parse_sections() self._parse_symbols() self._scan_libraries() + self._scan_source_symbols() self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() @@ -363,6 +367,11 @@ class MemoryAnalyzer: if lib_name := self._lib_symbol_map.get(symbol_name): return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + # Check source file mapping (catches extern "C" functions in ESPHome sources) + # Must be before heuristic patterns since source attribution is authoritative + if component := self._source_symbol_map.get(symbol_name): + return component + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): @@ -653,6 +662,7 @@ class MemoryAnalyzer: return None symbol_map: dict[str, str] = {} + source_symbol_map: dict[str, str] = {} current_symbol: str | None = None section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") @@ -688,9 +698,18 @@ class MemoryAnalyzer: if dir_key in source_path: symbol_map[current_symbol] = lib_name break + else: + # Map ESPHome source files to components for extern "C" + # and other symbols without C++ namespace + component = self._source_file_to_component(source_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_symbol_map[current_symbol] = component current_symbol = None + self._source_symbol_map = source_symbol_map return symbol_map or None def _scan_libraries(self) -> None: @@ -741,6 +760,112 @@ class MemoryAnalyzer: len(libraries), ) + def _scan_source_symbols(self) -> None: + """Scan ESPHome source object files to map extern "C" symbols to components. + + When no linker map file is available, this uses ``nm`` to scan ``.o`` files + under ``src/esphome/`` and build a symbol-to-component mapping. This catches + ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + + Skips scanning if ``_source_symbol_map`` was already populated by + ``_parse_map_file()``. + """ + if self._source_symbol_map or not self.nm_path: + return + + obj_dir = self._find_object_files_dir() + if obj_dir is None: + return + + # Find ESPHome source object files + esphome_src_dir = obj_dir / "src" / "esphome" + if not esphome_src_dir.is_dir(): + return + + obj_files = sorted(esphome_src_dir.rglob("*.o")) + if not obj_files: + return + + # Run nm with --print-file-name to get file:symbol mapping + result = run_tool( + [self.nm_path, "--print-file-name", "-g", "--defined-only"] + + [str(f) for f in obj_files], + ) + if result is None or result.returncode != 0: + _LOGGER.debug("nm scan of source objects failed") + return + + self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir) + if self._source_symbol_map: + _LOGGER.info( + "Built source symbol map from nm: %d symbols", + len(self._source_symbol_map), + ) + + def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]: + """Parse nm output to map non-namespaced symbols to ESPHome components. + + Extracts global defined symbols from ESPHome source object files that + don't use C++ namespacing (e.g. ``extern "C"`` functions). + + Args: + output: Raw stdout from ``nm --print-file-name -g --defined-only`` + or ``nm --print-file-name -S``. + base_dir: Build directory for computing relative paths. + + Returns: + Dict mapping symbol names to component names. + """ + source_map: dict[str, str] = {} + for line in output.splitlines(): + # Format: /path/to/file.o: addr type name + # or: /path/to/file.o: addr size type name (with -S) + colon_idx = line.rfind(".o:") + if colon_idx == -1: + continue + + file_path = line[: colon_idx + 2] + fields = line[colon_idx + 3 :].split() + if len(fields) < 3: + continue + + # With -S flag, format is: addr size type name + # Without -S flag: addr type name + # type is a single char; size is hex digits + # Detect by checking if fields[1] is a single uppercase letter (type) + if len(fields[1]) == 1 and fields[1].isalpha(): + # addr type name + sym_type = fields[1] + symbol_name = fields[2] + elif len(fields) >= 4: + # addr size type name + sym_type = fields[2] + symbol_name = fields[3] + else: + continue + + # Only global defined symbols (uppercase type) + if not sym_type.isupper() or sym_type == "U": + continue + + # Skip symbols already in esphome:: namespace + if symbol_name.startswith("_ZN7esphome"): + continue + + # Make path relative to base_dir for _source_file_to_component + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + continue + + component = self._source_file_to_component(rel_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_map[symbol_name] = component + + return source_map + def _find_object_files_dir(self) -> Path | None: """Find the directory containing object files for this build. diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 3bdf555ae39..0c871d07270 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -408,7 +408,6 @@ SYMBOL_PATTERNS = { ], "arduino_core": [ "pinMode", - "resetPins", "millis", "micros", "delay(", # More specific - Arduino delay function with parenthesis From 564d155cb6980e1b24cbd640cb014128dcb1cc92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:08:33 -1000 Subject: [PATCH 0855/2056] [wifi] Use LOG_STR_LITERAL for scan complete log on ESP8266 (#15001) --- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 5514f1c6be5..f2fabb9080f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -735,7 +735,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + needs_full ? LOG_STR_LITERAL("") : LOG_STR_LITERAL(" (filtered)")); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->pending_.scan_complete = true; // Defer listener callbacks to main loop From 7f500c4b6ef14967771cec2cb2f8785cc552b891 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:08:46 -1000 Subject: [PATCH 0856/2056] [modbus] Fix size_t format warning in clear_rx_buffer_ (#15002) --- esphome/components/modbus/modbus.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 7a61868e6e9..4146a54c877 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -415,10 +415,10 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { size_t at = this->rx_buffer_.size(); if (at > 0) { if (warn) { - ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } else { - ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } this->rx_buffer_.clear(); From 51ccad8461069ac04cd568971b9bf4b1b148e26e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:09:01 -1000 Subject: [PATCH 0857/2056] [preferences] Shorten TAG strings across all platforms (#15004) --- esphome/components/esp32/preferences.cpp | 2 +- esphome/components/esp8266/preferences.cpp | 2 +- esphome/components/host/preferences.cpp | 2 +- esphome/components/libretiny/preferences.cpp | 2 +- esphome/components/rp2040/preferences.cpp | 2 +- esphome/components/zephyr/preferences.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7260bf54e01..e88ace3e6be 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::esp32 { -static const char *const TAG = "esp32.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0b31c53ff87..906fed2b29a 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -13,7 +13,7 @@ extern "C" { namespace esphome::esp8266 { -static const char *const TAG = "esp8266.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index fce3d62dda0..c0be2700622 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::host { namespace fs = std::filesystem; -static const char *const TAG = "host.preferences"; +static const char *const TAG = "preferences"; void HostPreferences::setup_() { if (this->setup_complete_) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index f22c12f1fbe..344ca4a8b3e 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 0a91136a9fd..cfc802b28f5 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -14,7 +14,7 @@ namespace esphome::rp2040 { -static const char *const TAG = "rp2040.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index df69c0e6522..c26a1d6d536 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::zephyr { -static const char *const TAG = "zephyr.preferences"; +static const char *const TAG = "preferences"; bool ZephyrPreferenceBackend::save(const uint8_t *data, size_t len) { this->data.resize(len); From 2c872600464c2b0b168e09277bb5bbe16da4fac0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:09:13 -1000 Subject: [PATCH 0858/2056] [core] Optimize Component::is_ready() with bitmask check (#15005) --- esphome/core/component.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00dda0cc268..89ac0c7a2a2 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -378,9 +378,10 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: #pragma GCC diagnostic pop } bool Component::is_ready() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; + // Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4) + // (1 << state) & 0b10110 checks membership in one instruction + return ((1u << (this->component_state_ & COMPONENT_STATE_MASK)) & + ((1u << COMPONENT_STATE_SETUP) | (1u << COMPONENT_STATE_LOOP) | (1u << COMPONENT_STATE_LOOP_DONE))) != 0; } bool Component::can_proceed() { return true; } bool Component::set_status_flag_(uint8_t flag) { From 32db055b98cbb964663396b6aa63239cc4f97591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:09:28 -1000 Subject: [PATCH 0859/2056] [number] Clean up NumberCall::perform() increment/decrement logic (#15000) --- esphome/components/number/number_call.cpp | 22 ++++++---------------- esphome/components/number/number_call.h | 3 +++ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 27a857c112e..aac9b2a23d8 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -80,35 +80,25 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); target_value = parent->state + (std::isnan(step) ? 1 : step); - if (target_value > max_value) { - if (this->cycle_ && !std::isnan(min_value)) { - target_value = min_value; - } else { - target_value = max_value; - } - } + if (target_value > max_value) + target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); target_value = parent->state - (std::isnan(step) ? 1 : step); - if (target_value < min_value) { - if (this->cycle_ && !std::isnan(max_value)) { - target_value = max_value; - } else { - target_value = min_value; - } - } + if (target_value < min_value) + target_value = this->cycle_or_clamp_(min_value, max_value); } if (target_value < min_value) { diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 584c13f4131..29eaeb72d95 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -33,6 +33,9 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + float cycle_or_clamp_(float clamp, float opposite) const { + return (this->cycle_ && !std::isnan(opposite)) ? opposite : clamp; + } void log_perform_warning_(const LogString *message); void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, float limit); From 21e384cafd89f1a441a7de2c67816338ca90c569 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:10:18 -1000 Subject: [PATCH 0860/2056] [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ (#15008) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 64e5f44081b..f85f13fe737 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1920,6 +1920,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via: + # esp32: + # framework: + # sdkconfig_options: + # CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: From f3cddcee214fc8c510ecfc88dd3ef277c0d882df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:25:40 -1000 Subject: [PATCH 0861/2056] [core] Store parent pointers as members to enable inline Callback storage (#14923) --- esphome/components/cover/automation.h | 18 +++--- esphome/components/datetime/datetime_base.h | 7 ++- .../components/display_menu_base/automation.h | 35 ++++++++---- esphome/components/esp32_improv/automation.h | 55 ++++++++++++------- esphome/components/fan/automation.h | 49 ++++++++++------- .../graphical_display_menu.h | 7 ++- esphome/components/lock/automation.h | 9 ++- esphome/components/media_player/automation.h | 9 ++- esphome/components/mqtt/mqtt_fan.cpp | 3 +- esphome/components/valve/automation.h | 18 ++++-- 10 files changed, 134 insertions(+), 76 deletions(-) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 12ec46725d6..f121e5c2d67 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -105,17 +105,18 @@ template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<f template<bool OPEN> class CoverPositionTrigger : public Trigger<> { public: - CoverPositionTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->position != this->last_position_) { - this->last_position_ = a_cover->position; - if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + if (this->cover_->position != this->last_position_) { + this->last_position_ = this->cover_->position; + if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) this->trigger(); } }); } protected: + Cover *cover_; float last_position_{NAN}; }; @@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger<false>; template<CoverOperation OP> class CoverTrigger : public Trigger<> { public: - CoverTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - auto current_op = a_cover->current_operation; + CoverTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + auto current_op = this->cover_->current_operation; if (current_op == OP) { if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { this->trigger(); @@ -137,6 +138,7 @@ template<CoverOperation OP> class CoverTrigger : public Trigger<> { } protected: + Cover *cover_; optional<CoverOperation> last_operation_{}; }; } // namespace esphome::cover diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 98f23aa7137..6c0a33c8420 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -33,9 +33,12 @@ class DateTimeBase : public EntityBase { class DateTimeStateTrigger : public Trigger<ESPTime> { public: - explicit DateTimeStateTrigger(DateTimeBase *parent) { - parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { + parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); } + + protected: + DateTimeBase *parent_; }; } // namespace esphome::datetime diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 9c64794cefe..50c26c344cd 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -96,37 +96,52 @@ template<typename... Ts> class IsActiveCondition : public Condition<Ts...> { class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> { public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { - parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> { public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { - parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> { public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) { - parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> { public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { - parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> { public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { - parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; } // namespace display_menu_base diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index 52c5da125b7..cd2bd84c30c 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -12,58 +12,73 @@ namespace esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovProvisioningTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStartTrigger : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !parent->is_failed()) { - trigger(); + !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStateTrigger : public Trigger<improv::State, improv::Error> { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (!parent->is_failed()) { - trigger(state, error); + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (!this->parent_->is_failed()) { + this->trigger(state, error); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStoppedTrigger : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; } // namespace esp32_improv diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3c3b0ce519e..3ee6f89e557 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -113,16 +113,19 @@ template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> { class FanStateTrigger : public Trigger<Fan *> { public: - FanStateTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { this->trigger(state); }); + FanStateTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { this->trigger(this->fan_); }); } + + protected: + Fan *fan_; }; class FanTurnOnTrigger : public Trigger<> { public: - FanTurnOnTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOnTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = is_on && !this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -133,14 +136,15 @@ class FanTurnOnTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanTurnOffTrigger : public Trigger<> { public: - FanTurnOffTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOffTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = !is_on && this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -151,14 +155,15 @@ class FanTurnOffTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanDirectionSetTrigger : public Trigger<FanDirection> { public: - FanDirectionSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto direction = state->direction; + FanDirectionSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto direction = this->fan_->direction; auto should_trigger = direction != this->last_direction_; this->last_direction_ = direction; if (should_trigger) { @@ -169,14 +174,15 @@ class FanDirectionSetTrigger : public Trigger<FanDirection> { } protected: + Fan *fan_; FanDirection last_direction_; }; class FanOscillatingSetTrigger : public Trigger<bool> { public: - FanOscillatingSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto oscillating = state->oscillating; + FanOscillatingSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto oscillating = this->fan_->oscillating; auto should_trigger = oscillating != this->last_oscillating_; this->last_oscillating_ = oscillating; if (should_trigger) { @@ -187,14 +193,15 @@ class FanOscillatingSetTrigger : public Trigger<bool> { } protected: + Fan *fan_; bool last_oscillating_; }; class FanSpeedSetTrigger : public Trigger<int> { public: - FanSpeedSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto speed = state->speed; + FanSpeedSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto speed = this->fan_->speed; auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { @@ -205,14 +212,15 @@ class FanSpeedSetTrigger : public Trigger<int> { } protected: + Fan *fan_; int last_speed_; }; class FanPresetSetTrigger : public Trigger<StringRef> { public: - FanPresetSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + FanPresetSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto preset_mode = this->fan_->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { @@ -223,6 +231,7 @@ class FanPresetSetTrigger : public Trigger<StringRef> { } protected: + Fan *fan_; StringRef last_preset_mode_{}; }; diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 007889557dd..ce1db185251 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -75,9 +75,12 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { class GraphicalDisplayMenuOnRedrawTrigger : public Trigger<const GraphicalDisplayMenu *> { public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { - parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { + parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); } + + protected: + GraphicalDisplayMenu *parent_; }; } // namespace graphical_display_menu diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 011c6cc6afc..6f3c4226933 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -51,13 +51,16 @@ template<typename... Ts> class LockCondition : public Condition<Ts...> { template<LockState State> class LockStateTrigger : public Trigger<> { public: - explicit LockStateTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == State) { + explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { + a_lock->add_on_state_callback([this]() { + if (this->lock_->state == State) { this->trigger(); } }); } + + protected: + Lock *lock_; }; using LockLockTrigger = LockStateTrigger<LockState::LOCK_STATE_LOCKED>; diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 90e7bf75b5c..031f6657f49 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -80,12 +80,15 @@ class StateTrigger : public Trigger<> { template<MediaPlayerState State> class MediaPlayerStateTrigger : public Trigger<> { public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this, player]() { - if (player->state == State) + explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { + player->add_on_state_callback([this]() { + if (this->player_->state == State) this->trigger(); }); } + + protected: + MediaPlayer *player_; }; using IdleTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ae2b8c46002..3a8658a55fe 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -121,8 +121,7 @@ void MQTTFanComponent::setup() { }); } - auto f = std::bind(&MQTTFanComponent::publish_state, this); - this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); + this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTFanComponent::dump_config() { diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 87e9cde0885..a064f375f72 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -87,24 +87,30 @@ template<typename... Ts> class ValveIsClosedCondition : public Condition<Ts...> class ValveOpenTrigger : public Trigger<> { public: - ValveOpenTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_open()) { + ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_open()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; class ValveClosedTrigger : public Trigger<> { public: - ValveClosedTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_closed()) { + ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_closed()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; } // namespace valve From 95dea59382d422f8d1a44020a64d79935bd6ac19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 15:25:54 -1000 Subject: [PATCH 0862/2056] [core] Use SplitMix32 PRNG for random_uint32() (#14984) --- esphome/components/esp32/helpers.cpp | 1 - esphome/components/host/helpers.cpp | 9 -------- esphome/components/libretiny/helpers.cpp | 2 -- esphome/components/rp2040/helpers.cpp | 9 -------- esphome/components/zephyr/__init__.py | 2 ++ esphome/components/zephyr/core.cpp | 1 - esphome/core/helpers.cpp | 26 ++++++++++++++++++++++++ esphome/core/helpers.h | 7 ++++++- 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 76f1c59c739..654a35b4735 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -14,7 +14,6 @@ namespace esphome { -uint32_t random_uint32() { return esp_random(); } bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb69..7e8849b3e19 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include <sys/ioctl.h> #endif #include <unistd.h> -#include <limits> -#include <random> #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index ffbd181c54c..52332ef53d8 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -8,8 +8,6 @@ namespace esphome { -uint32_t random_uint32() { return rand(); } - bool random_bytes(uint8_t *data, size_t len) { lt_rand_bytes(data, len); return true; diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index e360b45ebbb..8cb5f7c18df 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -17,15 +17,6 @@ namespace esphome { -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} - bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index b8a091feb92..348e7a3cf2d 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -122,6 +122,8 @@ def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) zephyr_add_prj_conf("STD_CPP20", True) + # random_bytes() uses sys_rand_get() which requires the entropy subsystem + zephyr_add_prj_conf("ENTROPY_GENERATOR", True) # <err> os: ***** USAGE FAULT ***** # <err> os: Illegal load of EXC_RETURN into PC diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7c77fdd2ca..a3b0471ebcc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -75,7 +75,6 @@ IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } // Zephyr LwIPLock is defined inline as a no-op in helpers.h -uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { sys_rand_get(data, len); return true; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 00b447ebf29..ee99c541963 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,6 +156,32 @@ uint32_t fnv1_hash(const char *str) { return hash; } +// SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family +// (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment +// and the MurmurHash3 32-bit finalizer as output mixing function. +// Reference: https://doi.org/10.1145/2714064.2660195 +// Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ +// Seeded lazily from the platform's secure RNG via random_bytes(). +// ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). +#ifndef USE_ESP8266 +static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t random_uint32() { + // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, + // triggering one extra random_bytes() call — an acceptable trade-off vs. adding + // a separate bool flag (4 bytes BSS + branch on every call). + if (splitmix32_state == 0) { + random_bytes(reinterpret_cast<uint8_t *>(&splitmix32_state), sizeof(splitmix32_state)); + splitmix32_state |= 1; // ensure non-zero seed + } + splitmix32_state += 0x9e3779b9u; + uint32_t z = splitmix32_state; + z = (z ^ (z >> 16)) * 0x85ebca6bu; + z = (z ^ (z >> 13)) * 0xc2b2ae35u; + return z ^ (z >> 16); +} +#endif + float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); } // Strings diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a703b5a5f37..43431299dea 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -842,10 +842,15 @@ template<typename ReturnT = uint32_t> inline constexpr ESPHOME_ALWAYS_INLINE Ret } /// Return a random 32-bit unsigned integer. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. uint32_t random_uint32(); /// Return a random float between 0 and 1. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes. +/// Generate \p len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG). +/// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); ///@} From 1920d8a887407a9d9f2f23f8c1d9f83480a497a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 17:35:17 -1000 Subject: [PATCH 0863/2056] [benchmark] Add noise encryption benchmarks (#15037) --- script/setup_codspeed_lib.py | 8 +- tests/benchmarks/components/api/__init__.py | 7 + .../components/api/bench_noise_encrypt.cpp | 177 ++++++++++++++++++ .../benchmarks/components/api/benchmark.yaml | 1 + tests/benchmarks/components/mdns/__init__.py | 5 + .../benchmarks/components/network/__init__.py | 5 + .../benchmarks/components/socket/__init__.py | 7 + 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/benchmarks/components/api/__init__.py create mode 100644 tests/benchmarks/components/api/bench_noise_encrypt.cpp create mode 100644 tests/benchmarks/components/mdns/__init__.py create mode 100644 tests/benchmarks/components/network/__init__.py create mode 100644 tests/benchmarks/components/socket/__init__.py diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 959c89d05b6..4f5d1bff24b 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -205,7 +205,13 @@ def setup_codspeed_lib(output_dir: Path) -> None: if hooks_dist_c.exists(): _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") - # 4. Write library.json + # 4. Copy instrument-hooks headers (core.h, callgrind.h, valgrind.h) next to + # measurement.hpp so they are found before any same-named headers from + # other libraries (e.g. libsodium's core.h). + for header in hooks_include.glob("*.h"): + _copy_if_missing(header, core_include / header.name) + + # 5. Write library.json version = _read_codspeed_version(output_dir / CORE_CMAKE) _write_library_json( benchmark_dir, core_include, hooks_include, version, project_root diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py new file mode 100644 index 00000000000..0687c3f87fb --- /dev/null +++ b/tests/benchmarks/components/api/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # api must run its to_code to define USE_API, USE_API_PLAINTEXT, + # and add the noise-c library dependency. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp new file mode 100644 index 00000000000..223e6ada0d0 --- /dev/null +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -0,0 +1,177 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_NOISE + +#include <benchmark/benchmark.h> +#include <cstring> +#include <memory> + +#include "noise/protocol.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create and initialize a NoiseCipherState with ChaChaPoly. +// Returns nullptr on failure. +static NoiseCipherState *create_cipher() { + NoiseCipherState *cipher = nullptr; + int err = noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY); + if (err != NOISE_ERROR_NONE || cipher == nullptr) + return nullptr; + + // Initialize with a dummy 32-byte key (same pattern as handshake split produces) + uint8_t key[32]; + memset(key, 0xAB, sizeof(key)); + err = noise_cipherstate_init_key(cipher, key, sizeof(key)); + if (err != NOISE_ERROR_NONE) { + noise_cipherstate_free(cipher); + return nullptr; + } + return cipher; +} + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::write_protobuf_messages: +// - noise_buffer_init + noise_buffer_set_inout (same as production) +// - No explicit set_nonce (production relies on internal nonce increment) +// - Error checking on encrypt return +static void noise_encrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *cipher = create_cipher(); + if (cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(cipher); + size_t buf_capacity = plaintext_size + mac_len; + auto buffer = std::make_unique<uint8_t[]>(buf_capacity); + memset(buffer.get(), 0x42, plaintext_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Match production: init buffer, set inout, encrypt + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), plaintext_size, buf_capacity); + + int err = noise_cipherstate_encrypt(cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_encrypt failed"); + noise_cipherstate_free(cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(cipher); +} + +// --- Encrypt a typical sensor state message (small payload ~14 bytes) --- +// This is the most common message encrypted on every sensor update. +// 4 bytes type+len header + ~10 bytes payload. + +static void NoiseEncrypt_SmallMessage(benchmark::State &state) { noise_encrypt_bench(state, 14); } +BENCHMARK(NoiseEncrypt_SmallMessage); + +// --- Encrypt a medium message (~128 bytes, typical for LightStateResponse) --- + +static void NoiseEncrypt_MediumMessage(benchmark::State &state) { noise_encrypt_bench(state, 128); } +BENCHMARK(NoiseEncrypt_MediumMessage); + +// --- Encrypt a large message (~1024 bytes, typical for DeviceInfoResponse) --- + +static void NoiseEncrypt_LargeMessage(benchmark::State &state) { noise_encrypt_bench(state, 1024); } +BENCHMARK(NoiseEncrypt_LargeMessage); + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::read_packet: +// - noise_buffer_init + noise_buffer_set_inout with capacity == size (decrypt shrinks) +// - Error checking on decrypt return +// +// Pre-encrypts kInnerIterations messages with sequential nonces before the +// timed loop. Each outer iteration re-inits the decrypt key to reset the +// nonce back to 0, then decrypts all pre-encrypted messages in sequence. +// The init_key cost is amortized over kInnerIterations decrypts. +static void noise_decrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *encrypt_cipher = create_cipher(); + NoiseCipherState *decrypt_cipher = create_cipher(); + if (encrypt_cipher == nullptr || decrypt_cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + if (encrypt_cipher) + noise_cipherstate_free(encrypt_cipher); + if (decrypt_cipher) + noise_cipherstate_free(decrypt_cipher); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(encrypt_cipher); + size_t encrypted_size = plaintext_size + mac_len; + + // Pre-encrypt kInnerIterations messages with sequential nonces (0..N-1). + auto ciphertexts = std::make_unique<uint8_t[]>(encrypted_size * kInnerIterations); + for (int i = 0; i < kInnerIterations; i++) { + uint8_t *ct = ciphertexts.get() + i * encrypted_size; + memset(ct, 0x42, plaintext_size); + NoiseBuffer enc_buf; + noise_buffer_init(enc_buf); + noise_buffer_set_inout(enc_buf, ct, plaintext_size, encrypted_size); + int err = noise_cipherstate_encrypt(encrypt_cipher, &enc_buf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Pre-encrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + + // Working buffer — decrypt modifies in place + auto buffer = std::make_unique<uint8_t[]>(encrypted_size); + static constexpr uint8_t KEY[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + + for (auto _ : state) { + // Reset nonce to 0 by re-initing the key (amortized over kInnerIterations) + noise_cipherstate_init_key(decrypt_cipher, KEY, sizeof(KEY)); + + for (int i = 0; i < kInnerIterations; i++) { + // Copy ciphertext into working buffer (decrypt modifies in place) + memcpy(buffer.get(), ciphertexts.get() + i * encrypted_size, encrypted_size); + + // Decrypt matching production pattern + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), encrypted_size, encrypted_size); + + int err = noise_cipherstate_decrypt(decrypt_cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_decrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); +} + +// --- Decrypt benchmarks (matching read_packet path) --- + +static void NoiseDecrypt_SmallMessage(benchmark::State &state) { noise_decrypt_bench(state, 14); } +BENCHMARK(NoiseDecrypt_SmallMessage); + +static void NoiseDecrypt_MediumMessage(benchmark::State &state) { noise_decrypt_bench(state, 128); } +BENCHMARK(NoiseDecrypt_MediumMessage); + +static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } +BENCHMARK(NoiseDecrypt_LargeMessage); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_NOISE diff --git a/tests/benchmarks/components/api/benchmark.yaml b/tests/benchmarks/components/api/benchmark.yaml index bfc24d7440d..e57276ea660 100644 --- a/tests/benchmarks/components/api/benchmark.yaml +++ b/tests/benchmarks/components/api/benchmark.yaml @@ -108,6 +108,7 @@ esphome: area_id: area_20 api: + encryption: sensor: binary_sensor: light: diff --git a/tests/benchmarks/components/mdns/__init__.py b/tests/benchmarks/components/mdns/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/mdns/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/network/__init__.py b/tests/benchmarks/components/network/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/network/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/socket/__init__.py b/tests/benchmarks/components/socket/__init__.py new file mode 100644 index 00000000000..7a20f9f2300 --- /dev/null +++ b/tests/benchmarks/components/socket/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # socket must run its to_code to define USE_SOCKET_IMPL_BSD_SOCKETS + # which is needed by the api frame helper benchmarks. + manifest.enable_codegen() From d203a46ef808f76c22018eec570bab9f29b999a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 18:17:37 -1000 Subject: [PATCH 0864/2056] [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium (#15038) --- esphome/components/api/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 4c3cf81927d..84589d540d3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -455,6 +455,9 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") From 8dd69207ea09f8ca8c2d90d9437c7396ae0dc2aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:24:56 +0000 Subject: [PATCH 0865/2056] Bump aioesphomeapi from 44.6.2 to 44.7.0 (#15052) 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 10e56c3b49d..2e09e2ed994 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.2 +aioesphomeapi==44.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 2a6ec597b4ca81bd63f062307799c97c024f2726 Mon Sep 17 00:00:00 2001 From: Samuel Sieb <samuel-github@sieb.net> Date: Sat, 21 Mar 2026 11:13:08 -0700 Subject: [PATCH 0866/2056] [analog_threshhold] add missing header (#15058) --- .../components/analog_threshold/analog_threshold_binary_sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d85705..dd707681054 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" From 86ec218f75685454a3cc012cd632c350ba60ad5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 21 Mar 2026 13:15:35 -1000 Subject: [PATCH 0867/2056] [benchmark] Add plaintext API frame write benchmarks (#15036) --- .../components/api/bench_plaintext_frame.cpp | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/benchmarks/components/api/bench_plaintext_frame.cpp diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp new file mode 100644 index 00000000000..79bffaf9534 --- /dev/null +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -0,0 +1,162 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_PLAINTEXT + +#include <benchmark/benchmark.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "esphome/components/api/api_frame_helper_plaintext.h" +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +static void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper +// on the write end. Returns the helper and the read-side fd. +// Uses real TCP sockets so TCP_NODELAY succeeds during init(). +static std::pair<std::unique_ptr<APIPlaintextFrameHelper>, int> create_plaintext_helper() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Increase socket buffer sizes to reduce drain frequency + int bufsize = 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + auto sock = std::make_unique<socket::Socket>(write_fd); + auto helper = std::make_unique<APIPlaintextFrameHelper>(std::move(sock)); + helper->init(); + + return {std::move(helper), read_fd}; +} + +// --- Write a single SensorStateResponse through plaintext framing --- +// Measures the full write path: header construction, varint encoding, +// iovec assembly, and socket write. + +static void PlaintextFrame_WriteSensorState(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(padding + size); + ProtoWriteBuffer writer(&buffer, padding); + msg.encode(writer); + + helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteSensorState); + +// --- Write a batch of 5 SensorStateResponses in one call --- +// Measures batched write: multiple messages assembled into one writev. + +static void PlaintextFrame_WriteBatch5(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + uint8_t footer = helper->frame_footer_size(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + + for (int j = 0; j < 5; j++) { + uint16_t offset = buffer.size(); + SensorStateResponse msg; + msg.key = static_cast<uint32_t>(j); + msg.state = 23.5f + static_cast<float>(j); + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(offset + padding + size + footer); + ProtoWriteBuffer writer(&buffer, offset + padding); + msg.encode(writer); + + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + } + + helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5)); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteBatch5); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT From dd82a91d8fa8b3922e7e4b8177df5875c1d56d04 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:13:17 +1000 Subject: [PATCH 0868/2056] [lvgl] Don't animate page change when not requested (#15069) --- esphome/components/lvgl/defines.py | 2 +- esphome/components/lvgl/lvgl_esphome.cpp | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d6d7a5e161d..0a53d886693 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -280,7 +280,7 @@ SWIPE_TRIGGERS = tuple( LV_ANIM = LvConstant( - "LV_SCR_LOAD_ANIM_", + "LV_SCREEN_LOAD_ANIM_", "NONE", "OVER_LEFT", "OVER_RIGHT", diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b1618f77c47..fb5e5957134 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -176,7 +176,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti if (index >= this->pages_.size()) return; this->current_page_ = index; - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + if (anim == LV_SCREEN_LOAD_ANIM_NONE) { + lv_scr_load(this->pages_[this->current_page_]->obj); + } else { + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + } } void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { @@ -262,8 +266,8 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p)); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1, + (int) lv_area_get_width(area), (int) lv_area_get_height(area), (int) (millis() - now)); } lv_display_flush_ready(disp_drv); } @@ -619,7 +623,7 @@ void LvglComponent::setup() { // Rotation will be handled by our drawing function, so reset the display rotation. for (auto *disp : this->displays_) disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); - this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); + this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } From 8224da3460819a7a41fb302018f9ee3265fd46a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 21 Mar 2026 15:32:24 -1000 Subject: [PATCH 0869/2056] [core] Inline Component::get_component_log_str() (#15068) --- esphome/core/component.cpp | 3 --- esphome/core/component.h | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 89ac0c7a2a2..caaea891432 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -272,9 +272,6 @@ void Component::call() { break; } } -const LogString *Component::get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_; -} bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { // Prevent overflow when adding increment - if we're about to overflow, just max out diff --git a/esphome/core/component.h b/esphome/core/component.h index 119681f64c4..46cd77b0343 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,9 @@ class Component { * * Returns LOG_STR("<unknown>") if source not set */ - const LogString *get_component_log_str() const; + const LogString *get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_; + } bool should_warn_of_blocking(uint32_t blocking_time); From c48fd0738ba6ec1e88860758687c2fae06fae430 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 21 Mar 2026 15:33:42 -1000 Subject: [PATCH 0870/2056] [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) --- esphome/components/mqtt/mqtt_client.cpp | 17 ++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6f..ab665e2579a 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea3..7983e04870e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); From a0d552531238ce2f4d9ae95b6d717f4ab838ed11 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:01:49 +1000 Subject: [PATCH 0871/2056] [lvgl] Meter fixes (#15073) --- esphome/components/lvgl/lvgl_esphome.cpp | 4 +++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/meter.py | 21 +++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index fb5e5957134..b3cb4d56ad0 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -671,9 +671,10 @@ void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *are * @param e The event data * @param color_start The color to apply to the first tick * @param color_end The color to apply to the last tick + * @param width */ void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, - lv_color_t color_end, bool local) { + lv_color_t color_end, int width, bool local) { auto *scale = static_cast<lv_obj_t *>(lv_event_get_target(e)); lv_draw_task_t *task = lv_event_get_draw_task(e); @@ -691,6 +692,7 @@ void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_ range = 1; auto ratio = (tick * 255) / range; line_dsc->color = lv_color_mix(color_end, color_start, ratio); + line_dsc->width += width; } } } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 4ce7296159e..8e34f16c98f 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -53,7 +53,7 @@ extern std::string lv_event_code_name_for(lv_event_t *event); lv_obj_t *lv_container_create(lv_obj_t *parent); #ifdef USE_LVGL_SCALE void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, - lv_color_t color_end, bool local); + lv_color_t color_end, int width, bool local); #endif #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 63cc645f221..6a7559c42c6 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -177,7 +177,7 @@ INDICATOR_ARC_SCHEMA = cv.Schema( cv.Optional(CONF_VALUE): lv_float, cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default=1.0): opacity, } ).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) @@ -247,7 +247,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int, cv.Optional(CONF_RANGE_TO, default=100.0): lv_int, cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees, - cv.Optional(CONF_ROTATION, default=0): lv_angle_degrees, + cv.Optional(CONF_ROTATION): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } @@ -329,7 +329,7 @@ class MeterType(WidgetType): ) def get_uses(self): - return CONF_SCALE, CONF_LINE + return CONF_SCALE, CONF_LINE, CONF_IMAGE def validate(self, value): return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) @@ -366,16 +366,17 @@ class MeterType(WidgetType): lv.scale_set_range(scale_var, range_from, range_to) angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE]) - rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) + if (rotation := scale_conf.get(CONF_ROTATION)) is not None: + rotation = await lv_angle_degrees.process(rotation) + else: + rotation = 90 + (360 - angle_range) // 2 + # Set angle range lv.scale_set_angle_range( scale_var, angle_range, ) - - # Set rotation if specified - if rotation: - lv.scale_set_rotation(scale_var, rotation) + lv.scale_set_rotation(scale_var, rotation) # Handle indicators as sections for indicator in scale_conf.get(CONF_INDICATORS, ()): @@ -393,10 +394,9 @@ class MeterType(WidgetType): props = { "arc_width": v[CONF_WIDTH], "arc_color": v[CONF_COLOR], + "arc_opa": v[CONF_OPA], "arc_rounded": v.get("arc_rounded", False), } - if (opa := v.get(CONF_OPA)) is not None: - props["arc_opa"] = opa if CONF_R_MOD in v: get_warnings().add( "The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored." @@ -424,6 +424,7 @@ class MeterType(WidgetType): end_value, color_start, color_end, + v[CONF_WIDTH], local, ) lv_obj.add_event_cb( From 5e68282519caf8601bf2192923b1975d85580044 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:14:52 -1000 Subject: [PATCH 0872/2056] [light] Fix constant_brightness broken by gamma LUT refactor (#15048) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/light/light_color_values.h | 10 + esphome/components/light/light_state.cpp | 47 ++++- .../fixtures/light_constant_brightness.yaml | 57 ++++++ .../test_light_constant_brightness.py | 188 ++++++++++++++++++ 4 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/light_constant_brightness.yaml create mode 100644 tests/integration/test_light_constant_brightness.py diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 3a9ca8c8c23..a2c2dbca465 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -154,6 +154,16 @@ class LightColorValues { } /// Convert these light color values to an CWWW representation with the given parameters. + /// + /// Note on gamma and constant_brightness: This method operates on the raw/internal channel + /// values stored in this object. For cold_white_ and warm_white_ specifically, these + /// may already be gamma-uncorrected when derived from a color_temperature value. + /// For constant_brightness=false, additional gamma for the output can be applied after + /// this method since gamma commutes with simple multiplication. For constant_brightness=true, + /// the caller (LightState::current_values_as_cwww) must apply gamma to the individual + /// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does + /// not commute with gamma. See LightState::current_values_as_cwww() for the correct + /// implementation. void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { const float cw_level = this->cold_white_; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 161092532ac..1b736d84f68 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + this->current_values.as_rgb(red, green, blue); *red = this->gamma_correct_lut(*red); *green = this->gamma_correct_lut(*green); *blue = this->gamma_correct_lut(*blue); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + this->current_values_as_cwww(cold_white, warm_white, constant_brightness); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { @@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, constant_brightness); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + if (!constant_brightness) { + // Without constant_brightness, gamma commutes with simple multiplication: + // gamma(white_level * cw) = gamma(white_level) * gamma(cw) + // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b)) + // so applying gamma after is mathematically equivalent and simpler. + this->current_values.as_cwww(cold_white, warm_white, false); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); + return; + } + + // For constant_brightness mode, gamma MUST be applied to the individual + // channel values BEFORE the balancing formula (max/sum ratio), not after. + // + // Why: The cold_white_ and warm_white_ values stored in LightColorValues + // are gamma-uncorrected (see transform_parameters_() which applies + // gamma_uncorrect to the linear CW/WW fractions derived from color + // temperature). Applying gamma_correct here recovers the original linear + // fractions, which the constant_brightness formula then uses to distribute + // power evenly. The max/sum formula ensures cold+warm PWM output sums to + // a constant, keeping total power (and perceived brightness) the same + // across all color temperatures. + // + // Applying gamma AFTER the formula would be incorrect because gamma is + // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced + // ratio would be distorted, causing a severe brightness dip at mid-range + // color temperatures. + const auto &v = this->current_values; + if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) { + *cold_white = *warm_white = 0; + return; + } + + const float cw_level = this->gamma_correct_lut(v.get_cold_white()); + const float ww_level = this->gamma_correct_lut(v.get_warm_white()); + const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness()); + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); diff --git a/tests/integration/fixtures/light_constant_brightness.yaml b/tests/integration/fixtures/light_constant_brightness.yaml new file mode 100644 index 00000000000..4357a16d58b --- /dev/null +++ b/tests/integration/fixtures/light_constant_brightness.yaml @@ -0,0 +1,57 @@ +esphome: + name: light-cb-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: cb_cold_white_output + type: float + write_action: + - logger.log: + format: "CB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: cb_warm_white_output + type: float + write_action: + - logger.log: + format: "CB_WW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_cold_white_output + type: float + write_action: + - logger.log: + format: "NCB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_warm_white_output + type: float + write_action: + - logger.log: + format: "NCB_WW_OUTPUT:%.6f" + args: [state] + +light: + - platform: cwww + name: "Test CB Light" + id: test_cb_light + cold_white: cb_cold_white_output + warm_white: cb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + gamma_correct: 2.8 + + - platform: cwww + name: "Test NCB Light" + id: test_ncb_light + cold_white: ncb_cold_white_output + warm_white: ncb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: false + gamma_correct: 2.8 diff --git a/tests/integration/test_light_constant_brightness.py b/tests/integration/test_light_constant_brightness.py new file mode 100644 index 00000000000..622dc0e0650 --- /dev/null +++ b/tests/integration/test_light_constant_brightness.py @@ -0,0 +1,188 @@ +"""Integration test for constant_brightness with gamma correction. + +Tests both constant_brightness: true and false cwww lights with gamma +correction in a single compilation to verify: +- constant_brightness: true maintains constant total CW+WW power output +- constant_brightness: false correctly varies total power across color temps + +This is a regression test for https://github.com/esphome/esphome/issues/15040 +where the gamma LUT refactor (#14123) broke constant_brightness by applying +gamma after the balancing formula instead of before it. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_constant_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test constant_brightness true and false behavior with gamma correction.""" + # Track output values for both lights from log lines + cb_cw_pattern = re.compile(r"(?<!N)CB_CW_OUTPUT:([\d.]+)") + cb_ww_pattern = re.compile(r"(?<!N)CB_WW_OUTPUT:([\d.]+)") + ncb_cw_pattern = re.compile(r"NCB_CW_OUTPUT:([\d.]+)") + ncb_ww_pattern = re.compile(r"NCB_WW_OUTPUT:([\d.]+)") + + latest: dict[str, float] = { + "cb_cw": 0.0, + "cb_ww": 0.0, + "ncb_cw": 0.0, + "ncb_ww": 0.0, + } + + def on_log_line(line: str) -> None: + for pattern, key in [ + (cb_cw_pattern, "cb_cw"), + (cb_ww_pattern, "cb_ww"), + (ncb_cw_pattern, "ncb_cw"), + (ncb_ww_pattern, "ncb_ww"), + ]: + match = pattern.search(line) + if match: + latest[key] = float(match.group(1)) + + loop = asyncio.get_running_loop() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + lights = [e for e in entities if isinstance(e, LightInfo)] + cb_light = next(e for e in lights if e.object_id.endswith("cb_light")) + ncb_light = next(e for e in lights if e.object_id.endswith("ncb_light")) + + # Use InitialStateHelper to wait for initial state broadcast + initial_state_helper = InitialStateHelper(entities) + + # Track state changes per light key + state_futures: dict[int, asyncio.Future[EntityState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def send_and_wait( + light_key: int, timeout: float = 5.0, **kwargs: Any + ) -> LightState: + """Send a light command and wait for the state response.""" + state_futures[light_key] = loop.create_future() + client.light_command(key=light_key, **kwargs) + try: + return await asyncio.wait_for(state_futures[light_key], timeout=timeout) + except TimeoutError: + pytest.fail(f"Timeout waiting for light state after command: {kwargs}") + + # --- Test constant_brightness: true --- + + # Turn on CB light at full brightness + await send_and_wait( + cb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + test_mireds = [ + 153.0, # Pure cold white + 200.0, # Mostly cold + 280.0, # Mixed + 326.5, # Midpoint + 400.0, # Mostly warm + 500.0, # Pure warm white + ] + + cb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + cb_light.key, color_temperature=mireds, transition_length=0 + ) + cb_totals.append((mireds, latest["cb_cw"], latest["cb_ww"])) + + # All totals should be approximately equal (constant brightness) + reference_total = next((cw + ww for _, cw, ww in cb_totals if cw + ww > 0), 0) + assert reference_total > 0, ( + f"Reference total power is zero, CB light outputs not working. " + f"Values: {cb_totals}" + ) + + for mireds, cw, ww in cb_totals: + total = cw + ww + assert total == pytest.approx(reference_total, rel=0.05), ( + f"constant_brightness: Total power at {mireds} mireds " + f"({total:.4f}) differs from reference ({reference_total:.4f}) " + f"by more than 5%. CW={cw:.4f}, WW={ww:.4f}. " + f"All values: {cb_totals}" + ) + + # --- Test constant_brightness: false --- + + # Turn on NCB light at full brightness + await send_and_wait( + ncb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + ncb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + ncb_light.key, color_temperature=mireds, transition_length=0 + ) + ncb_totals.append((mireds, latest["ncb_cw"], latest["ncb_ww"])) + + extreme_cw = ncb_totals[0] # 153 mireds - pure cold + extreme_ww = ncb_totals[-1] # 500 mireds - pure warm + midpoint = ncb_totals[3] # 326.5 mireds - midpoint + + # At pure cold white, WW should be ~0 + assert extreme_cw[2] == pytest.approx(0.0, abs=0.01), ( + f"Pure cold white should have WW~0, got WW={extreme_cw[2]:.4f}" + ) + # At pure warm white, CW should be ~0 + assert extreme_ww[1] == pytest.approx(0.0, abs=0.01), ( + f"Pure warm white should have CW~0, got CW={extreme_ww[1]:.4f}" + ) + + # At midpoint, both channels should be non-zero + assert midpoint[1] > 0.05, f"Midpoint CW should be >0.05, got {midpoint[1]:.4f}" + assert midpoint[2] > 0.05, f"Midpoint WW should be >0.05, got {midpoint[2]:.4f}" + + # Total power at midpoint should be higher than at the extremes + midpoint_total = midpoint[1] + midpoint[2] + extreme_cw_total = extreme_cw[1] + extreme_cw[2] + extreme_ww_total = extreme_ww[1] + extreme_ww[2] + + assert midpoint_total > extreme_cw_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure CW total " + f"({extreme_cw_total:.4f}). All values: {ncb_totals}" + ) + assert midpoint_total > extreme_ww_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure WW total " + f"({extreme_ww_total:.4f}). All values: {ncb_totals}" + ) From ca0523b86ca11cf7eac5bb86c1ff762279371aff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:16:46 -1000 Subject: [PATCH 0873/2056] [sht4x] Fix heater causing measurement jitter (#15030) --- esphome/components/sht4x/sht4x.cpp | 40 ++++++++++++++++-------------- esphome/components/sht4x/sht4x.h | 3 ++- tests/components/sht4x/common.yaml | 4 +++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index bf23e42e661..43c2436a563 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -9,14 +9,12 @@ static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; -void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {this->heater_command_}; - - ESP_LOGD(TAG, "Heater turning on"); - if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error(LOG_STR("Failed to turn on heater")); - } -} +// Conversion constants from SHT4x datasheet +static constexpr float TEMPERATURE_OFFSET = -45.0f; +static constexpr float TEMPERATURE_SPAN = 175.0f; +static constexpr float HUMIDITY_OFFSET = -6.0f; +static constexpr float HUMIDITY_SPAN = 125.0f; +static constexpr float RAW_MAX = 65535.0f; void SHT4XComponent::read_serial_number_() { uint16_t buffer[2]; @@ -39,8 +37,8 @@ void SHT4XComponent::setup() { this->read_serial_number_(); if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { - uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); + this->heater_interval_ = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { @@ -62,8 +60,6 @@ void SHT4XComponent::setup() { } } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - - this->set_interval(heater_interval, [this]() { this->start_heater_(); }); } } @@ -106,19 +102,27 @@ void SHT4XComponent::update() { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { // Temp is contained in the first result word - float sensor_value_temp = buffer[0]; - float temp = -45 + 175 * sensor_value_temp / 65535; - + float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast<float>(buffer[0]) / RAW_MAX; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { // Relative humidity is in the second result word - float sensor_value_rh = buffer[1]; - float rh = -6 + 125 * sensor_value_rh / 65535; - + float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast<float>(buffer[1]) / RAW_MAX; this->humidity_sensor_->publish_state(rh); } + + // Fire heater after measurement to maximize cooldown time before the next reading. + // The heater command produces a measurement that we don't need (datasheet 4.9). + if (this->heater_interval_ > 0) { + uint32_t now = millis(); + if (now - this->last_heater_millis_ >= this->heater_interval_) { + ESP_LOGD(TAG, "Heater turning on"); + if (this->write_command(this->heater_command_)) { + this->last_heater_millis_ = now; + } + } + } }); } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index aec0f3d7f8e..51f473fe3f2 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri SHT4XHEATERTIME heater_time_; float duty_cycle_; - void start_heater_(); void read_serial_number_(); uint8_t heater_command_; + uint32_t heater_interval_{0}; + uint32_t last_heater_millis_{0}; uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 50d5ad8ca46..bec192d6db5 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -6,4 +6,8 @@ sensor: humidity: name: SHT4X Humidity address: 0x44 + precision: High + heater_max_duty: 0.02 + heater_power: High + heater_time: Long update_interval: 15s From ba4be2a904b18d0509b9768aa5076bb9e218b8fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:17:59 -1000 Subject: [PATCH 0874/2056] [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) --- esphome/components/uart/uart_component.h | 5 +++++ tests/components/uart/test.rtl87xx-ard.yaml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/components/uart/test.rtl87xx-ard.yaml diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index ee2b006039c..00a4c788784 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,12 +30,17 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. +// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. +// Save and restore around the enum to avoid collisions with our scoped enum value. +#pragma push_macro("SUCCESS") +#undef SUCCESS enum class FlushResult { SUCCESS, ///< Confirmed: all bytes left the TX FIFO. TIMEOUT, ///< Confirmed: timed out before TX completed. FAILED, ///< Confirmed: driver or hardware error. ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; +#pragma pop_macro("SUCCESS") class UARTComponent { public: diff --git a/tests/components/uart/test.rtl87xx-ard.yaml b/tests/components/uart/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..414bf1f14d8 --- /dev/null +++ b/tests/components/uart/test.rtl87xx-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_id + tx_pin: PA23 + rx_pin: PA18 + baud_rate: 9600 + data_bits: 8 + parity: NONE + stop_bits: 1 + +switch: + - platform: uart + name: "UART Switch" + uart_id: uart_id + data: [0x01, 0x02, 0x03] From 6a77b8b1f457fb5d2be9e8863b82e0328c8b4b29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:19:28 -1000 Subject: [PATCH 0875/2056] [light] Fix gamma LUT quantizing small brightness to zero (#15060) --- esphome/components/light/__init__.py | 28 +++-- tests/unit_tests/components/light/__init__.py | 0 .../components/light/test_gamma_table.py | 117 ++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/components/light/__init__.py create mode 100644 tests/unit_tests/components/light/test_gamma_table.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4403281116a..64452e42820 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -81,18 +81,32 @@ def _get_data() -> LightData: return CORE.data[DOMAIN] +def generate_gamma_table(gamma_correct: float) -> list[HexInt]: + """Generate a 256-entry uint16 gamma lookup table. + + For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve + the invariant that non-zero input always produces non-zero output. Without + this, small brightness values (e.g. 1%) get quantized to exactly 0.0, + which breaks zero_means_zero logic in FloatOutput. + """ + if gamma_correct > 0: + return [ + HexInt( + max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + if i > 0 + else HexInt(0) + ) + for i in range(256) + ] + return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + def _get_or_create_gamma_table(gamma_correct): data = _get_data() if gamma_correct in data.gamma_tables: return data.gamma_tables[gamma_correct] - if gamma_correct > 0: - forward = [ - HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) - for i in range(256) - ] - else: - forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + forward = generate_gamma_table(gamma_correct) gamma_str = f"{gamma_correct}".replace(".", "_") fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) diff --git a/tests/unit_tests/components/light/__init__.py b/tests/unit_tests/components/light/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py new file mode 100644 index 00000000000..a302a355dc7 --- /dev/null +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -0,0 +1,117 @@ +"""Tests for the gamma LUT table generation.""" + +import pytest + +from esphome.components.light import generate_gamma_table + + +def _simulate_gamma_correct_lut(table: list[int], value: float) -> float: + """Simulate the C++ gamma_correct_lut interpolation from light_state.cpp.""" + if value <= 0.0: + return 0.0 + if value >= 1.0: + return 1.0 + scaled = value * 255.0 + idx = int(scaled) + if idx >= 255: + return table[255] / 65535.0 + frac = scaled - idx + a = float(table[idx]) + b = float(table[idx + 1]) + return (a + frac * (b - a)) / 65535.0 + + +def test_table_length() -> None: + """Table must always have exactly 256 entries.""" + table = generate_gamma_table(2.8) + assert len(table) == 256 + + +def test_index_zero_is_zero() -> None: + """Index 0 must be 0 so true off remains off.""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[0] == 0, f"gamma={gamma}" + + +def test_index_255_is_max() -> None: + """Index 255 must be 65535 (full on).""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[255] == 65535, f"gamma={gamma}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_nonzero_indices_are_nonzero(gamma: float) -> None: + """All indices > 0 must produce non-zero values. + + This prevents zero_means_zero breakage: non-zero input must always + produce non-zero output so FloatOutput applies min_power scaling. + """ + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_table_monotonically_nondecreasing(gamma: float) -> None: + """The gamma table must be monotonically non-decreasing.""" + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= table[i - 1], ( + f"gamma={gamma}: table[{i}]={table[i]} < table[{i - 1}]={table[i - 1]}" + ) + + +def test_linear_gamma() -> None: + """With gamma=0 (linear), table should be evenly spaced.""" + table = generate_gamma_table(0) + assert table[0] == 0 + assert table[128] == round(128 / 255.0 * 65535) + assert table[255] == 65535 + + +@pytest.mark.parametrize("brightness", [0.01, 0.005, 0.001, 1 / 255]) +def test_small_brightness_nonzero_after_lut(brightness: float) -> None: + """Small but non-zero brightness must produce non-zero output through the LUT. + + Regression test for #15055: with zero_means_zero=true, a gamma-corrected + value of exactly 0.0 causes FloatOutput to skip min_power scaling, turning + the LED off instead of to minimum brightness. + """ + table = generate_gamma_table(2.8) + result = _simulate_gamma_correct_lut(table, brightness) + assert result > 0.0, ( + f"brightness={brightness}: gamma LUT returned 0.0, would break zero_means_zero" + ) + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_small_brightness_nonzero_all_gammas(gamma: float) -> None: + """1% brightness must be non-zero for all common gamma values.""" + table = generate_gamma_table(gamma) + result = _simulate_gamma_correct_lut(table, 0.01) + assert result > 0.0, f"gamma={gamma}: 1% brightness returned 0.0" + + +def test_lut_zero_returns_zero() -> None: + """LUT with input 0.0 must return 0.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 0.0) == 0.0 + + +def test_lut_one_returns_one() -> None: + """LUT with input 1.0 must return 1.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 1.0) == 1.0 + + +def test_lut_output_monotonically_nondecreasing() -> None: + """LUT output must be monotonically non-decreasing across the full range.""" + table = generate_gamma_table(2.8) + prev = 0.0 + for i in range(1001): + value = i / 1000.0 + result = _simulate_gamma_correct_lut(table, value) + assert result >= prev, f"value={value}: result {result} < previous {prev}" + prev = result From 12b10d8b89721d4e58ccef224eec4c7c9f37ea6c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:19:46 -0400 Subject: [PATCH 0876/2056] [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) --- .../ultrasonic/ultrasonic_sensor.cpp | 29 +++++++++---------- .../components/ultrasonic/ultrasonic_sensor.h | 3 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index d3f7e694440..a354b198b4b 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering) +static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through) static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (arg->echo_pin_isr.digital_read()) { + // Ignore edges after measurement complete or too soon after trigger pulse + if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) { + return; + } + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { InterruptLock lock; - this->store_.echo_start_us = 0; - this->store_.echo_end_us = 0; this->store_.echo_start = false; this->store_.echo_end = false; + this->store_.measurement_start_us = micros(); this->trigger_pin_isr_.digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_isr_.digital_write(false); this->measurement_pending_ = true; - this->measurement_start_us_ = micros(); + this->measurement_start_us_ = this->store_.measurement_start_us; } void UltrasonicSensorComponent::setup() { @@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() { } if (this->store_.echo_end) { - float result; - if (this->store_.echo_start) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", - this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); - result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); - } else { - ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); - result = NAN; - } + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); this->measurement_pending_ = false; return; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 541f7d2b700..7d333a1b243 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,8 +11,7 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); - ISRInternalGPIOPin echo_pin_isr; - volatile uint32_t wait_start_us{0}; + volatile uint32_t measurement_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; From c917b8ce06b80f1db580d84c2512cc981dc7e687 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:20:28 -1000 Subject: [PATCH 0877/2056] [logger] Fix race condition in task log buffer initialization (#15071) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/logger/__init__.py | 39 ++++++++++++++++----------- esphome/components/logger/logger.cpp | 20 ++++++-------- esphome/components/logger/logger.h | 5 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a5601e6a8f4..3da81e12a01 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,11 +331,27 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -364,17 +380,10 @@ async def _late_logger_init(config: ConfigType) -> None: log = await cg.get_variable(config[CONF_ID]) level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host + zephyr_add_prj_conf("MPSC_PBUF", True) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2ee..ceacded7756 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif -} #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif } -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8c38cadcbc5..c81b8e4e94f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; From 2b6d63fd09a7c4d40385114bd9222037397804da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20K=C3=B6lker?= <jason@koelker.net> Date: Sun, 22 Mar 2026 20:21:08 +0000 Subject: [PATCH 0878/2056] [pmsx003] Keep active-mode reads aligned (#14832) --- esphome/components/pmsx003/pmsx003.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 114ecf435e0..6275ff60c2d 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -95,10 +95,6 @@ void PMSX003Component::loop() { // Just go ahead and read stuff break; } - } else if (now - this->last_update_ < this->update_interval_) { - // Otherwise just leave the sensor powered up and come back when we hit the update - // time - return; } if (now - this->last_transmission_ >= 500) { @@ -114,10 +110,11 @@ void PMSX003Component::loop() { this->read_byte(&this->data_[this->data_index_]); auto check = this->check_byte_(); if (!check.has_value()) { - // finished - this->parse_data_(); + if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) { + this->parse_data_(); + this->last_update_ = now; + } this->data_index_ = 0; - this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -138,7 +135,7 @@ optional<bool> PMSX003Component::check_byte_() { return true; } - ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1); + ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char); return false; } From 705d548435d648eda29e044c43d538034df7f98d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:39:26 -1000 Subject: [PATCH 0879/2056] Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927) 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 da95dd5a13d..f8f60f19329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.5.2 +aioesphomeapi==44.6.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 99d968f80a5057ec653e9fba8a55f2aa9ec5ee14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 18 Mar 2026 14:06:41 -1000 Subject: [PATCH 0880/2056] [http_request] Prevent double update task launch (#14910) --- .../components/http_request/update/http_request_update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index a15dc616753..1c52a281059 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -74,6 +74,10 @@ void HttpRequestUpdate::update() { } this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 + if (this->update_task_handle_ != nullptr) { + ESP_LOGW(TAG, "Update check already in progress"); + return; + } xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else this->update_task(this); @@ -204,6 +208,9 @@ defer: // both success and error paths to avoid multiple std::function instantiations. // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. this_update->defer([this_update, result]() { +#ifdef USE_ESP32 + this_update->update_task_handle_ = nullptr; +#endif if (result->error_str != nullptr) { this_update->status_set_error(result->error_str); delete result; From 8af0991590f27f870d3544e0b2bd6798380d9564 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:42 -0400 Subject: [PATCH 0881/2056] [ble_client] Fix RSSI sensor reporting same value for all clients (#14939) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index dc032a7a98b..715298e5928 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl switch (event) { // server response on RSSI request: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr)) + return; if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); From 729d3d4bc22718d0116f3dd1d47eea7d12362ef9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:23:47 -0400 Subject: [PATCH 0882/2056] [openthread] Guard InstanceLock against uninitialized semaphore (#14940) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/openthread/openthread.h | 4 ++++ .../components/openthread/openthread_esp.cpp | 22 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 75d8fe11fd7..bd10774fcf5 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -11,6 +11,7 @@ #include <openthread/instance.h> #include <openthread/thread.h> +#include <atomic> #include <optional> #include <vector> @@ -28,6 +29,8 @@ class OpenThreadComponent : public Component { float get_setup_priority() const override { return setup_priority::WIFI; } bool is_connected() const { return this->connected_; } + /// Returns true once esp_openthread_init() has completed and the OT lock is usable. + bool is_lock_initialized() const { return this->lock_initialized_; } network::IPAddresses get_ip_addresses(); std::optional<otIp6Address> get_omr_address(); void ot_main(); @@ -51,6 +54,7 @@ class OpenThreadComponent : public Component { uint32_t poll_period_{0}; #endif std::optional<int8_t> output_power_{}; + std::atomic<bool> lock_initialized_{false}; bool teardown_started_{false}; bool teardown_complete_{false}; bool connected_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9cc9223b523..27712bd86ad 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -8,6 +8,7 @@ #include "esp_openthread_lock.h" #include "esp_task_wdt.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() { // Initialize the OpenThread stack // otLoggingSetLevel(OT_LOG_LEVEL_DEBG); ESP_ERROR_CHECK(esp_openthread_init(&config)); + // Mark lock as initialized so InstanceLock callers know it's safe to acquire. + // Must be set after esp_openthread_init() which creates the internal semaphore. + this->lock_initialized_ = true; // Fetch OT instance once to avoid repeated call into OT stack otInstance *instance = esp_openthread_get_instance(); @@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() { esp_openthread_launch_mainloop(); - // Clean up + // Clean up - reset lock flag before deinit destroys the semaphore + this->lock_initialized_ = false; esp_openthread_deinit(); esp_openthread_netif_glue_deinit(); esp_netif_destroy(openthread_netif); @@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { + if (!global_openthread_component->is_lock_initialized()) { + return {}; + } if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); } @@ -217,6 +225,18 @@ std::optional<InstanceLock> InstanceLock::try_acquire(int delay) { } InstanceLock InstanceLock::acquire() { + // Wait for the lock to be created by ot_main() before attempting to acquire it. + // esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init(). + constexpr uint32_t lock_init_timeout_ms = 10000; + uint32_t start = millis(); + while (!global_openthread_component->is_lock_initialized()) { + if (millis() - start > lock_init_timeout_ms) { + ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms); + abort(); + } + delay(10); + esp_task_wdt_reset(); + } while (!esp_openthread_lock_acquire(100)) { esp_task_wdt_reset(); } From 5cbe9362569e17447c661e2cf5ad9a528b2a7ef7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:43:50 -1000 Subject: [PATCH 0883/2056] Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954) 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 f8f60f19329..9e2f4efe3e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.0 +aioesphomeapi==44.6.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 08cab43548fb7546690cdabd07cda2f7a0c0e74a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 08:44:35 -1000 Subject: [PATCH 0884/2056] [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) --- esphome/components/time/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ffa408db97..9821046a737 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = { def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples + if not iana_key: + return None try: package_loc, resource = iana_key.rsplit("/", 1) except ValueError: - return None - package = "tzdata.zoneinfo." + package_loc.replace("/", ".") + # Handle top-level timezone entries like "UTC", "GMT" + package = "tzdata.zoneinfo" + resource = iana_key + else: + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: return (resources.files(package) / resource).read_bytes() - except (FileNotFoundError, ModuleNotFoundError): + except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None From daf3502e1527dde0a919f39898ca9cc61849dc8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 14:11:49 -1000 Subject: [PATCH 0885/2056] [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) --- esphome/components/logger/__init__.py | 29 ++++++++++++++++++--------- esphome/core/config.py | 4 +++- esphome/coroutine.py | 10 +++++++-- esphome/writer.py | 3 +++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 675f9a2ca4b..a5601e6a8f4 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -56,6 +56,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -323,12 +324,11 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(CoroPriority.DIAGNOSTICS) -async def to_code(config): - baud_rate = config[CONF_BAUD_RATE] +@coroutine_with_priority(CoroPriority.EARLY_INIT) +async def to_code(config: ConfigType) -> None: + baud_rate: int = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level - initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) log = cg.new_Pvariable( @@ -347,10 +347,23 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - # pre_setup() must be called before init_log_buffer() because - # init_log_buffer() calls disable_loop() which may log at VV level, - # and global_logger must be set before any logging occurs. + # pre_setup() sets global_logger and must run before any other code + # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) + initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] + cg.add(log.set_log_level(initial_level)) + + # Schedule the rest of logger setup at DIAGNOSTICS priority, after + # Application is constructed (CORE priority) but before most components. + CORE.add_job(_late_logger_init, config) + + +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def _late_logger_init(config: ConfigType) -> None: + """Finish logger setup after Application is constructed.""" + log = await cg.get_variable(config[CONF_ID]) + level = config[CONF_LEVEL] + baud_rate: int = config[CONF_BAUD_RATE] if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] if task_log_buffer_size > 0: @@ -363,8 +376,6 @@ async def to_code(config): cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host - cg.add(log.set_log_level(initial_level)) - # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: diff --git a/esphome/core/config.py b/esphome/core/config.py index d4a839cb795..bdfb1cc4171 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -587,7 +587,9 @@ async def _add_looping_components() -> None: @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: - cg.add_global(cg.global_ns.namespace("esphome").using) + # using namespace esphome is hardcoded in writer.py to guarantee it + # precedes all variable declarations regardless of coroutine priority. + # These can be used by user lambdas, put them to default scope cg.add_global(cg.RawExpression("using std::isnan")) cg.add_global(cg.RawExpression("using std::min")) diff --git a/esphome/coroutine.py b/esphome/coroutine.py index f5d512e510e..3ce94cc9791 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -63,7 +63,13 @@ class CoroPriority(enum.IntEnum): resolution during code generation. """ - # Platform initialization - must run first + # Early init - runs before platform init and before Application exists. + # Currently used only to connect logging so ESP_LOG* calls work + # immediately in all subsequent phases. + # Examples: logger (1100) + EARLY_INIT = 1100 + + # Platform initialization # Examples: esp32, esp8266, rp2040 PLATFORM = 1000 @@ -83,7 +89,7 @@ class CoroPriority(enum.IntEnum): CORE = 100 # Diagnostic and debugging systems - # Examples: logger (90) + # Examples: debug component (90) DIAGNOSTICS = 90 # Status and monitoring systems diff --git a/esphome/writer.py b/esphome/writer.py index fd4c811fb35..69a35d00e34 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -381,7 +381,10 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() + # using namespace esphome must precede all variable declarations since + # codegen types assume this namespace is in scope (esphome_ns = global_ns). global_s = '#include "esphome.h"\n' + global_s += "using namespace esphome;\n" global_s += CORE.cpp_global_section full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" From 08187a01b1de0897a9e42f7bf9b4eb5e577c5275 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:39:10 +1000 Subject: [PATCH 0886/2056] [sdl] Fix get_width()/height() when rotation used (#14950) --- esphome/components/sdl/sdl_esphome.cpp | 37 ++++++++++++++++++++++++++ esphome/components/sdl/sdl_esphome.h | 4 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index f235e4e68cf..74ca2ce39a5 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -5,6 +5,30 @@ namespace esphome { namespace sdl { +int Sdl::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int Sdl::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + void Sdl::setup() { SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, @@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = this->width_ - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = this->height_ - x - 1; + x = tmp; + } + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index bf5fde14282..968434a04fb 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -33,8 +33,8 @@ class Sdl : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } void add_key_listener(int32_t keycode, std::function<void(bool)> &&callback) { From d4f7cb984cdc2510a676f65bc5ca7f70f5eba903 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 19 Mar 2026 20:56:51 -1000 Subject: [PATCH 0887/2056] [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) --- .../uart/uart_component_esp_idf.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d7..8168e49805a 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast<gpio_num_t>(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast<gpio_num_t>(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 0297260a5773d38a2a65ade6cf35333f9aff09f4 Mon Sep 17 00:00:00 2001 From: Keith Roehrenbeck <kroehre@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:11:57 -0500 Subject: [PATCH 0888/2056] [ld2450] Fix zone target counts including untracked ghost targets (#15026) --- esphome/components/ld2450/ld2450.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index f9701cbdf66..1946831ca88 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -534,10 +534,11 @@ void LD2450Component::handle_periodic_data_() { } #endif - // Store target info for zone target count - this->target_info_[index].x = tx; - this->target_info_[index].y = ty; - this->target_info_[index].is_moving = is_moving; + // Store target info for zone target count. Zero out untracked targets (td==0) + // so stale coordinates don't produce ghost counts in count_targets_in_zone_(). + this->target_info_[index].x = (td > 0) ? tx : 0; + this->target_info_[index].y = (td > 0) ? ty : 0; + this->target_info_[index].is_moving = (td > 0) && is_moving; } // End loop thru targets From 28e8250b69e7ae9662e166a6bcf69085f5e3d48f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:10:40 -1000 Subject: [PATCH 0889/2056] Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027) 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 9e2f4efe3e9..10e56c3b49d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.1 +aioesphomeapi==44.6.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 12eed0d38407c77c70179b2dcbb9c969c06a79c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 12:06:23 -1000 Subject: [PATCH 0890/2056] [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) --- esphome/components/api/api_connection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index dea3ba5460b..d97ef97762a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * // A stalled handshake from a buggy client or network glitch holds a connection // slot, which can prevent legitimate clients from reconnecting. Also hardens // against the less likely case of intentional connection slot exhaustion. -static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); From 3fe84eadef1e5448f6bae7c5f88ac74154e9f6d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 12:13:49 -1000 Subject: [PATCH 0891/2056] [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) --- esphome/components/wifi/wifi_component_esp8266.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0bf7934878a..5514f1c6be5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -92,13 +92,23 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { return ret; } bool WiFiComponent::wifi_apply_power_save_() { + // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode. + // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2 + // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451 + // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP + // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55 sleep_type_t power_save; switch (this->power_save_) { case WIFI_POWER_SAVE_LIGHT: - power_save = LIGHT_SLEEP_T; + // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active. + // Matches ESP32's WIFI_PS_MIN_MODEM. + power_save = MODEM_SLEEP_T; break; case WIFI_POWER_SAVE_HIGH: - power_save = MODEM_SLEEP_T; + // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons. + // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM. + // See https://github.com/esphome/esphome/issues/14999 + power_save = LIGHT_SLEEP_T; break; case WIFI_POWER_SAVE_NONE: default: From 0bf6e1e8398a3793d5b28b50de20459ec183c132 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:53:02 -0400 Subject: [PATCH 0892/2056] [esp32_touch] Fix initial state never published when sensor untouched (#15032) --- esphome/components/esp32_touch/esp32_touch.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f9..0d331b29d60 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } From 98b4e1ea15c8badd9e701e225dc59a5de12a3824 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:57:51 -1000 Subject: [PATCH 0893/2056] [web_server] Increase httpd task stack size to prevent stack overflow (#14997) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f18570965b7..45ee711b1eb 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -120,7 +120,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. From 06cc5a29a733c1078ead4bdf7f6dfc46c5bbe57d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:58:02 -1000 Subject: [PATCH 0894/2056] [core] Add copy() method to StringRef for std::string compatibility (#15028) --- esphome/core/string_ref.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 60472027530..34ba2474b2a 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast<const uint8_t *>(base_); } From 42da28185456200efbf886da2e4cdad2e670bc97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 20 Mar 2026 13:58:14 -1000 Subject: [PATCH 0895/2056] [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) --- esphome/core/time.cpp | 16 ++++++---------- esphome/core/time.h | 2 ++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7b..6add82e7d14 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4a..1716c51ffd5 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include <cstdint> #include <cstdlib> #include <cstring> From 77264de3f6a17c2f5f0f1bb62bee0f75febab28f Mon Sep 17 00:00:00 2001 From: Samuel Sieb <samuel-github@sieb.net> Date: Sat, 21 Mar 2026 11:13:08 -0700 Subject: [PATCH 0896/2056] [analog_threshhold] add missing header (#15058) --- .../components/analog_threshold/analog_threshold_binary_sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d85705..dd707681054 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" From 2352c732de79b76bb277dc6e1927f73105de12ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 21 Mar 2026 15:33:42 -1000 Subject: [PATCH 0897/2056] [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) --- esphome/components/mqtt/mqtt_client.cpp | 17 ++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6f..ab665e2579a 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8) PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version", "Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea3..7983e04870e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); From b5880df93c6830003b46d106181a26516a4fac07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:14:52 -1000 Subject: [PATCH 0898/2056] [light] Fix constant_brightness broken by gamma LUT refactor (#15048) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/light/light_color_values.h | 10 + esphome/components/light/light_state.cpp | 47 ++++- .../fixtures/light_constant_brightness.yaml | 57 ++++++ .../test_light_constant_brightness.py | 188 ++++++++++++++++++ 4 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/light_constant_brightness.yaml create mode 100644 tests/integration/test_light_constant_brightness.py diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 3a9ca8c8c23..a2c2dbca465 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -154,6 +154,16 @@ class LightColorValues { } /// Convert these light color values to an CWWW representation with the given parameters. + /// + /// Note on gamma and constant_brightness: This method operates on the raw/internal channel + /// values stored in this object. For cold_white_ and warm_white_ specifically, these + /// may already be gamma-uncorrected when derived from a color_temperature value. + /// For constant_brightness=false, additional gamma for the output can be applied after + /// this method since gamma commutes with simple multiplication. For constant_brightness=true, + /// the caller (LightState::current_values_as_cwww) must apply gamma to the individual + /// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does + /// not commute with gamma. See LightState::current_values_as_cwww() for the correct + /// implementation. void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { const float cw_level = this->cold_white_; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 161092532ac..1b736d84f68 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + this->current_values.as_rgb(red, green, blue); *red = this->gamma_correct_lut(*red); *green = this->gamma_correct_lut(*green); *blue = this->gamma_correct_lut(*blue); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + this->current_values_as_cwww(cold_white, warm_white, constant_brightness); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { @@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, constant_brightness); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + if (!constant_brightness) { + // Without constant_brightness, gamma commutes with simple multiplication: + // gamma(white_level * cw) = gamma(white_level) * gamma(cw) + // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b)) + // so applying gamma after is mathematically equivalent and simpler. + this->current_values.as_cwww(cold_white, warm_white, false); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); + return; + } + + // For constant_brightness mode, gamma MUST be applied to the individual + // channel values BEFORE the balancing formula (max/sum ratio), not after. + // + // Why: The cold_white_ and warm_white_ values stored in LightColorValues + // are gamma-uncorrected (see transform_parameters_() which applies + // gamma_uncorrect to the linear CW/WW fractions derived from color + // temperature). Applying gamma_correct here recovers the original linear + // fractions, which the constant_brightness formula then uses to distribute + // power evenly. The max/sum formula ensures cold+warm PWM output sums to + // a constant, keeping total power (and perceived brightness) the same + // across all color temperatures. + // + // Applying gamma AFTER the formula would be incorrect because gamma is + // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced + // ratio would be distorted, causing a severe brightness dip at mid-range + // color temperatures. + const auto &v = this->current_values; + if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) { + *cold_white = *warm_white = 0; + return; + } + + const float cw_level = this->gamma_correct_lut(v.get_cold_white()); + const float ww_level = this->gamma_correct_lut(v.get_warm_white()); + const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness()); + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); diff --git a/tests/integration/fixtures/light_constant_brightness.yaml b/tests/integration/fixtures/light_constant_brightness.yaml new file mode 100644 index 00000000000..4357a16d58b --- /dev/null +++ b/tests/integration/fixtures/light_constant_brightness.yaml @@ -0,0 +1,57 @@ +esphome: + name: light-cb-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: cb_cold_white_output + type: float + write_action: + - logger.log: + format: "CB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: cb_warm_white_output + type: float + write_action: + - logger.log: + format: "CB_WW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_cold_white_output + type: float + write_action: + - logger.log: + format: "NCB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_warm_white_output + type: float + write_action: + - logger.log: + format: "NCB_WW_OUTPUT:%.6f" + args: [state] + +light: + - platform: cwww + name: "Test CB Light" + id: test_cb_light + cold_white: cb_cold_white_output + warm_white: cb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + gamma_correct: 2.8 + + - platform: cwww + name: "Test NCB Light" + id: test_ncb_light + cold_white: ncb_cold_white_output + warm_white: ncb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: false + gamma_correct: 2.8 diff --git a/tests/integration/test_light_constant_brightness.py b/tests/integration/test_light_constant_brightness.py new file mode 100644 index 00000000000..622dc0e0650 --- /dev/null +++ b/tests/integration/test_light_constant_brightness.py @@ -0,0 +1,188 @@ +"""Integration test for constant_brightness with gamma correction. + +Tests both constant_brightness: true and false cwww lights with gamma +correction in a single compilation to verify: +- constant_brightness: true maintains constant total CW+WW power output +- constant_brightness: false correctly varies total power across color temps + +This is a regression test for https://github.com/esphome/esphome/issues/15040 +where the gamma LUT refactor (#14123) broke constant_brightness by applying +gamma after the balancing formula instead of before it. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_constant_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test constant_brightness true and false behavior with gamma correction.""" + # Track output values for both lights from log lines + cb_cw_pattern = re.compile(r"(?<!N)CB_CW_OUTPUT:([\d.]+)") + cb_ww_pattern = re.compile(r"(?<!N)CB_WW_OUTPUT:([\d.]+)") + ncb_cw_pattern = re.compile(r"NCB_CW_OUTPUT:([\d.]+)") + ncb_ww_pattern = re.compile(r"NCB_WW_OUTPUT:([\d.]+)") + + latest: dict[str, float] = { + "cb_cw": 0.0, + "cb_ww": 0.0, + "ncb_cw": 0.0, + "ncb_ww": 0.0, + } + + def on_log_line(line: str) -> None: + for pattern, key in [ + (cb_cw_pattern, "cb_cw"), + (cb_ww_pattern, "cb_ww"), + (ncb_cw_pattern, "ncb_cw"), + (ncb_ww_pattern, "ncb_ww"), + ]: + match = pattern.search(line) + if match: + latest[key] = float(match.group(1)) + + loop = asyncio.get_running_loop() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + lights = [e for e in entities if isinstance(e, LightInfo)] + cb_light = next(e for e in lights if e.object_id.endswith("cb_light")) + ncb_light = next(e for e in lights if e.object_id.endswith("ncb_light")) + + # Use InitialStateHelper to wait for initial state broadcast + initial_state_helper = InitialStateHelper(entities) + + # Track state changes per light key + state_futures: dict[int, asyncio.Future[EntityState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def send_and_wait( + light_key: int, timeout: float = 5.0, **kwargs: Any + ) -> LightState: + """Send a light command and wait for the state response.""" + state_futures[light_key] = loop.create_future() + client.light_command(key=light_key, **kwargs) + try: + return await asyncio.wait_for(state_futures[light_key], timeout=timeout) + except TimeoutError: + pytest.fail(f"Timeout waiting for light state after command: {kwargs}") + + # --- Test constant_brightness: true --- + + # Turn on CB light at full brightness + await send_and_wait( + cb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + test_mireds = [ + 153.0, # Pure cold white + 200.0, # Mostly cold + 280.0, # Mixed + 326.5, # Midpoint + 400.0, # Mostly warm + 500.0, # Pure warm white + ] + + cb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + cb_light.key, color_temperature=mireds, transition_length=0 + ) + cb_totals.append((mireds, latest["cb_cw"], latest["cb_ww"])) + + # All totals should be approximately equal (constant brightness) + reference_total = next((cw + ww for _, cw, ww in cb_totals if cw + ww > 0), 0) + assert reference_total > 0, ( + f"Reference total power is zero, CB light outputs not working. " + f"Values: {cb_totals}" + ) + + for mireds, cw, ww in cb_totals: + total = cw + ww + assert total == pytest.approx(reference_total, rel=0.05), ( + f"constant_brightness: Total power at {mireds} mireds " + f"({total:.4f}) differs from reference ({reference_total:.4f}) " + f"by more than 5%. CW={cw:.4f}, WW={ww:.4f}. " + f"All values: {cb_totals}" + ) + + # --- Test constant_brightness: false --- + + # Turn on NCB light at full brightness + await send_and_wait( + ncb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + ncb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + ncb_light.key, color_temperature=mireds, transition_length=0 + ) + ncb_totals.append((mireds, latest["ncb_cw"], latest["ncb_ww"])) + + extreme_cw = ncb_totals[0] # 153 mireds - pure cold + extreme_ww = ncb_totals[-1] # 500 mireds - pure warm + midpoint = ncb_totals[3] # 326.5 mireds - midpoint + + # At pure cold white, WW should be ~0 + assert extreme_cw[2] == pytest.approx(0.0, abs=0.01), ( + f"Pure cold white should have WW~0, got WW={extreme_cw[2]:.4f}" + ) + # At pure warm white, CW should be ~0 + assert extreme_ww[1] == pytest.approx(0.0, abs=0.01), ( + f"Pure warm white should have CW~0, got CW={extreme_ww[1]:.4f}" + ) + + # At midpoint, both channels should be non-zero + assert midpoint[1] > 0.05, f"Midpoint CW should be >0.05, got {midpoint[1]:.4f}" + assert midpoint[2] > 0.05, f"Midpoint WW should be >0.05, got {midpoint[2]:.4f}" + + # Total power at midpoint should be higher than at the extremes + midpoint_total = midpoint[1] + midpoint[2] + extreme_cw_total = extreme_cw[1] + extreme_cw[2] + extreme_ww_total = extreme_ww[1] + extreme_ww[2] + + assert midpoint_total > extreme_cw_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure CW total " + f"({extreme_cw_total:.4f}). All values: {ncb_totals}" + ) + assert midpoint_total > extreme_ww_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure WW total " + f"({extreme_ww_total:.4f}). All values: {ncb_totals}" + ) From 9abc112f7606870ae868954f07547769e0040b83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:16:46 -1000 Subject: [PATCH 0899/2056] [sht4x] Fix heater causing measurement jitter (#15030) --- esphome/components/sht4x/sht4x.cpp | 40 ++++++++++++++++-------------- esphome/components/sht4x/sht4x.h | 3 ++- tests/components/sht4x/common.yaml | 4 +++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 42be3262029..43c2436a563 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -9,14 +9,12 @@ static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; -void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {this->heater_command_}; - - ESP_LOGD(TAG, "Heater turning on"); - if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error(LOG_STR("Failed to turn on heater")); - } -} +// Conversion constants from SHT4x datasheet +static constexpr float TEMPERATURE_OFFSET = -45.0f; +static constexpr float TEMPERATURE_SPAN = 175.0f; +static constexpr float HUMIDITY_OFFSET = -6.0f; +static constexpr float HUMIDITY_SPAN = 125.0f; +static constexpr float RAW_MAX = 65535.0f; void SHT4XComponent::read_serial_number_() { uint16_t buffer[2]; @@ -39,8 +37,8 @@ void SHT4XComponent::setup() { this->read_serial_number_(); if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { - uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); + this->heater_interval_ = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { @@ -62,8 +60,6 @@ void SHT4XComponent::setup() { } } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - - this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); } } @@ -106,19 +102,27 @@ void SHT4XComponent::update() { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { // Temp is contained in the first result word - float sensor_value_temp = buffer[0]; - float temp = -45 + 175 * sensor_value_temp / 65535; - + float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast<float>(buffer[0]) / RAW_MAX; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { // Relative humidity is in the second result word - float sensor_value_rh = buffer[1]; - float rh = -6 + 125 * sensor_value_rh / 65535; - + float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast<float>(buffer[1]) / RAW_MAX; this->humidity_sensor_->publish_state(rh); } + + // Fire heater after measurement to maximize cooldown time before the next reading. + // The heater command produces a measurement that we don't need (datasheet 4.9). + if (this->heater_interval_ > 0) { + uint32_t now = millis(); + if (now - this->last_heater_millis_ >= this->heater_interval_) { + ESP_LOGD(TAG, "Heater turning on"); + if (this->write_command(this->heater_command_)) { + this->last_heater_millis_ = now; + } + } + } }); } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index aec0f3d7f8e..51f473fe3f2 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri SHT4XHEATERTIME heater_time_; float duty_cycle_; - void start_heater_(); void read_serial_number_(); uint8_t heater_command_; + uint32_t heater_interval_{0}; + uint32_t last_heater_millis_{0}; uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 50d5ad8ca46..bec192d6db5 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -6,4 +6,8 @@ sensor: humidity: name: SHT4X Humidity address: 0x44 + precision: High + heater_max_duty: 0.02 + heater_power: High + heater_time: Long update_interval: 15s From 67ab2e143c8c549bcdbc1fe0f05c2e488075b661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:17:59 -1000 Subject: [PATCH 0900/2056] [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) --- esphome/components/uart/uart_component.h | 5 +++++ tests/components/uart/test.rtl87xx-ard.yaml | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/components/uart/test.rtl87xx-ard.yaml diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 853de719fef..7b61501d443 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,12 +30,17 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. +// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. +// Save and restore around the enum to avoid collisions with our scoped enum value. +#pragma push_macro("SUCCESS") +#undef SUCCESS enum class FlushResult { SUCCESS, ///< Confirmed: all bytes left the TX FIFO. TIMEOUT, ///< Confirmed: timed out before TX completed. FAILED, ///< Confirmed: driver or hardware error. ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; +#pragma pop_macro("SUCCESS") class UARTComponent { public: diff --git a/tests/components/uart/test.rtl87xx-ard.yaml b/tests/components/uart/test.rtl87xx-ard.yaml new file mode 100644 index 00000000000..414bf1f14d8 --- /dev/null +++ b/tests/components/uart/test.rtl87xx-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_id + tx_pin: PA23 + rx_pin: PA18 + baud_rate: 9600 + data_bits: 8 + parity: NONE + stop_bits: 1 + +switch: + - platform: uart + name: "UART Switch" + uart_id: uart_id + data: [0x01, 0x02, 0x03] From de3292c828c2116713dafddf86e725e6f2b69fc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:19:28 -1000 Subject: [PATCH 0901/2056] [light] Fix gamma LUT quantizing small brightness to zero (#15060) --- esphome/components/light/__init__.py | 28 +++-- tests/unit_tests/components/light/__init__.py | 0 .../components/light/test_gamma_table.py | 117 ++++++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/components/light/__init__.py create mode 100644 tests/unit_tests/components/light/test_gamma_table.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4403281116a..64452e42820 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -81,18 +81,32 @@ def _get_data() -> LightData: return CORE.data[DOMAIN] +def generate_gamma_table(gamma_correct: float) -> list[HexInt]: + """Generate a 256-entry uint16 gamma lookup table. + + For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve + the invariant that non-zero input always produces non-zero output. Without + this, small brightness values (e.g. 1%) get quantized to exactly 0.0, + which breaks zero_means_zero logic in FloatOutput. + """ + if gamma_correct > 0: + return [ + HexInt( + max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + if i > 0 + else HexInt(0) + ) + for i in range(256) + ] + return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + def _get_or_create_gamma_table(gamma_correct): data = _get_data() if gamma_correct in data.gamma_tables: return data.gamma_tables[gamma_correct] - if gamma_correct > 0: - forward = [ - HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) - for i in range(256) - ] - else: - forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + forward = generate_gamma_table(gamma_correct) gamma_str = f"{gamma_correct}".replace(".", "_") fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) diff --git a/tests/unit_tests/components/light/__init__.py b/tests/unit_tests/components/light/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py new file mode 100644 index 00000000000..a302a355dc7 --- /dev/null +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -0,0 +1,117 @@ +"""Tests for the gamma LUT table generation.""" + +import pytest + +from esphome.components.light import generate_gamma_table + + +def _simulate_gamma_correct_lut(table: list[int], value: float) -> float: + """Simulate the C++ gamma_correct_lut interpolation from light_state.cpp.""" + if value <= 0.0: + return 0.0 + if value >= 1.0: + return 1.0 + scaled = value * 255.0 + idx = int(scaled) + if idx >= 255: + return table[255] / 65535.0 + frac = scaled - idx + a = float(table[idx]) + b = float(table[idx + 1]) + return (a + frac * (b - a)) / 65535.0 + + +def test_table_length() -> None: + """Table must always have exactly 256 entries.""" + table = generate_gamma_table(2.8) + assert len(table) == 256 + + +def test_index_zero_is_zero() -> None: + """Index 0 must be 0 so true off remains off.""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[0] == 0, f"gamma={gamma}" + + +def test_index_255_is_max() -> None: + """Index 255 must be 65535 (full on).""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[255] == 65535, f"gamma={gamma}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_nonzero_indices_are_nonzero(gamma: float) -> None: + """All indices > 0 must produce non-zero values. + + This prevents zero_means_zero breakage: non-zero input must always + produce non-zero output so FloatOutput applies min_power scaling. + """ + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_table_monotonically_nondecreasing(gamma: float) -> None: + """The gamma table must be monotonically non-decreasing.""" + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= table[i - 1], ( + f"gamma={gamma}: table[{i}]={table[i]} < table[{i - 1}]={table[i - 1]}" + ) + + +def test_linear_gamma() -> None: + """With gamma=0 (linear), table should be evenly spaced.""" + table = generate_gamma_table(0) + assert table[0] == 0 + assert table[128] == round(128 / 255.0 * 65535) + assert table[255] == 65535 + + +@pytest.mark.parametrize("brightness", [0.01, 0.005, 0.001, 1 / 255]) +def test_small_brightness_nonzero_after_lut(brightness: float) -> None: + """Small but non-zero brightness must produce non-zero output through the LUT. + + Regression test for #15055: with zero_means_zero=true, a gamma-corrected + value of exactly 0.0 causes FloatOutput to skip min_power scaling, turning + the LED off instead of to minimum brightness. + """ + table = generate_gamma_table(2.8) + result = _simulate_gamma_correct_lut(table, brightness) + assert result > 0.0, ( + f"brightness={brightness}: gamma LUT returned 0.0, would break zero_means_zero" + ) + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_small_brightness_nonzero_all_gammas(gamma: float) -> None: + """1% brightness must be non-zero for all common gamma values.""" + table = generate_gamma_table(gamma) + result = _simulate_gamma_correct_lut(table, 0.01) + assert result > 0.0, f"gamma={gamma}: 1% brightness returned 0.0" + + +def test_lut_zero_returns_zero() -> None: + """LUT with input 0.0 must return 0.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 0.0) == 0.0 + + +def test_lut_one_returns_one() -> None: + """LUT with input 1.0 must return 1.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 1.0) == 1.0 + + +def test_lut_output_monotonically_nondecreasing() -> None: + """LUT output must be monotonically non-decreasing across the full range.""" + table = generate_gamma_table(2.8) + prev = 0.0 + for i in range(1001): + value = i / 1000.0 + result = _simulate_gamma_correct_lut(table, value) + assert result >= prev, f"value={value}: result {result} < previous {prev}" + prev = result From bbfe324dd6da2ffddcc98fe67d9655bccb854f6d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:19:46 -0400 Subject: [PATCH 0902/2056] [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) --- .../ultrasonic/ultrasonic_sensor.cpp | 29 +++++++++---------- .../components/ultrasonic/ultrasonic_sensor.h | 3 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index d3f7e694440..a354b198b4b 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering) +static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through) static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (arg->echo_pin_isr.digital_read()) { + // Ignore edges after measurement complete or too soon after trigger pulse + if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) { + return; + } + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { InterruptLock lock; - this->store_.echo_start_us = 0; - this->store_.echo_end_us = 0; this->store_.echo_start = false; this->store_.echo_end = false; + this->store_.measurement_start_us = micros(); this->trigger_pin_isr_.digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_isr_.digital_write(false); this->measurement_pending_ = true; - this->measurement_start_us_ = micros(); + this->measurement_start_us_ = this->store_.measurement_start_us; } void UltrasonicSensorComponent::setup() { @@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() { } if (this->store_.echo_end) { - float result; - if (this->store_.echo_start) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", - this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); - result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); - } else { - ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); - result = NAN; - } + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); this->measurement_pending_ = false; return; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 541f7d2b700..7d333a1b243 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,8 +11,7 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); - ISRInternalGPIOPin echo_pin_isr; - volatile uint32_t wait_start_us{0}; + volatile uint32_t measurement_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; From 036be63f7b022023a7ab36f138dbd6e7df1dde84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:20:28 -1000 Subject: [PATCH 0903/2056] [logger] Fix race condition in task log buffer initialization (#15071) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/logger/__init__.py | 39 ++++++++++++++++----------- esphome/components/logger/logger.cpp | 20 ++++++-------- esphome/components/logger/logger.h | 5 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index a5601e6a8f4..3da81e12a01 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,11 +331,27 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -364,17 +380,10 @@ async def _late_logger_init(config: ConfigType) -> None: log = await cg.get_variable(config[CONF_ID]) level = config[CONF_LEVEL] baud_rate: int = config[CONF_BAUD_RATE] - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host + zephyr_add_prj_conf("MPSC_PBUF", True) # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2ee..ceacded7756 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif -} #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif } -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 263d12b4441..fdb330c1336 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; From a3c483edf3838ba12fb83a927e9d4b1a744d9364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20K=C3=B6lker?= <jason@koelker.net> Date: Sun, 22 Mar 2026 20:21:08 +0000 Subject: [PATCH 0904/2056] [pmsx003] Keep active-mode reads aligned (#14832) --- esphome/components/pmsx003/pmsx003.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 114ecf435e0..6275ff60c2d 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -95,10 +95,6 @@ void PMSX003Component::loop() { // Just go ahead and read stuff break; } - } else if (now - this->last_update_ < this->update_interval_) { - // Otherwise just leave the sensor powered up and come back when we hit the update - // time - return; } if (now - this->last_transmission_ >= 500) { @@ -114,10 +110,11 @@ void PMSX003Component::loop() { this->read_byte(&this->data_[this->data_index_]); auto check = this->check_byte_(); if (!check.has_value()) { - // finished - this->parse_data_(); + if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) { + this->parse_data_(); + this->last_update_ = now; + } this->data_index_ = 0; - this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -138,7 +135,7 @@ optional<bool> PMSX003Component::check_byte_() { return true; } - ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1); + ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char); return false; } From 320474b62d26fd4d8e6d55eba84b6c76e5bad48c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:28:58 +1300 Subject: [PATCH 0905/2056] Bump version to 2026.3.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index d8a030536e9..d86894435f8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.0 +PROJECT_NUMBER = 2026.3.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 756ad694647..52ac7acd228 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.0" +__version__ = "2026.3.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From daafa8faa38fd8ba9cb450da18407961dbfde19a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:36:18 -1000 Subject: [PATCH 0906/2056] [wifi] Inline trivial WiFiAP and WiFiComponent accessors (#15075) --- esphome/components/wifi/wifi_component.cpp | 5 ----- esphome/components/wifi/wifi_component.h | 10 +++++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 346276692a1..08065a7544e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -871,9 +871,6 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } -bool WiFiComponent::has_ap() const { return this->has_ap_; } -bool WiFiComponent::is_ap_active() const { return this->ap_started_; } -bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } @@ -2250,8 +2247,6 @@ bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } #ifdef USE_WIFI_WPA2_EAP const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; } #endif -uint8_t WiFiAP::get_channel() const { return this->channel_; } -bool WiFiAP::has_channel() const { return this->channel_ != 0; } #ifdef USE_WIFI_MANUAL_IP const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 057f2c06613..bd202604d6f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -263,8 +263,8 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP const optional<EAPAuth> &get_eap() const; #endif // USE_WIFI_WPA2_EAP - uint8_t get_channel() const; - bool has_channel() const; + uint8_t get_channel() const { return this->channel_; } + bool has_channel() const { return this->channel_ != 0; } int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP const optional<ManualIP> &get_manual_ip() const; @@ -470,9 +470,9 @@ class WiFiComponent final : public Component { /// Reconnect WiFi if required. void loop() override; - bool has_sta() const; - bool has_ap() const; - bool is_ap_active() const; + bool has_sta() const { return !this->sta_.empty(); } + bool has_ap() const { return this->has_ap_; } + bool is_ap_active() const { return this->ap_started_; } #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); From 593dbc9e67f4347dcb5642ffe60cec63edf97408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:50:58 -1000 Subject: [PATCH 0907/2056] [logger] Fix unit test and benchmark Logger constructor calls (#15085) --- tests/benchmarks/components/main.cpp | 2 +- tests/components/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 9bc0c31a159..901dc44c077 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 373fde71516..622b1f107b0 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); From 45c0e6ef7f3cf29ff1934cc8d94f8dcb9217df09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 10:52:46 -1000 Subject: [PATCH 0908/2056] [logger] Fix unit test Logger constructor call (#15086) --- tests/components/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 373fde71516..622b1f107b0 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); From 27f3a5f5f472ee3c80b5b8699e25a2f4d588b73c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 11:54:54 -1000 Subject: [PATCH 0909/2056] [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) --- esphome/components/sht4x/sht4x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 43c2436a563..b1dbde22a4d 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -1,4 +1,5 @@ #include "sht4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { From 6d16c57747773431da0484b0e10e10e5129fed39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 11:54:54 -1000 Subject: [PATCH 0910/2056] [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) --- esphome/components/sht4x/sht4x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 43c2436a563..b1dbde22a4d 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -1,4 +1,5 @@ #include "sht4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { From 5cc4f6e85ab77fe914000fb41013f6fc5aadded2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:29:12 -1000 Subject: [PATCH 0911/2056] [logger] Add task_log_buffer_zephyr.cpp to platform source filter (#15081) --- esphome/components/logger/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3da81e12a01..4345e291a30 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -614,6 +614,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "task_log_buffer_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) From 4d09eb2cec628cd5ad082678f3fa5266ef46fe1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:29:28 -1000 Subject: [PATCH 0912/2056] [tests] Fix flaky ld24xx integration tests by disabling API batching (#15050) --- tests/integration/fixtures/uart_mock_ld2410.yaml | 1 + tests/integration/fixtures/uart_mock_ld2410_engineering.yaml | 1 + tests/integration/fixtures/uart_mock_ld2412.yaml | 1 + tests/integration/fixtures/uart_mock_ld2412_engineering.yaml | 1 + .../fixtures/uart_mock_ld2412_engineering_truncated.yaml | 1 + tests/integration/fixtures/uart_mock_ld2420.yaml | 1 + tests/integration/fixtures/uart_mock_ld2420_simple.yaml | 1 + tests/integration/fixtures/uart_mock_ld2450.yaml | 1 + 8 files changed, 8 insertions(+) diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml index 59838b0599c..e7568c0e3f5 100644 --- a/tests/integration/fixtures/uart_mock_ld2410.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml index 4625ae85115..5e62a4b8a8a 100644 --- a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml index 9cf9d6bb873..525ab0d5c47 100644 --- a/tests/integration/fixtures/uart_mock_ld2412.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index a69e18888e4..9f83e3226f1 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml index c0bd5147629..fd1cd9fd334 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml index 5380b810719..ee22f807d4e 100644 --- a/tests/integration/fixtures/uart_mock_ld2420.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml index 2ceca5d35d8..d3b6ad5d927 100644 --- a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml index 269136da682..4140ff659cd 100644 --- a/tests/integration/fixtures/uart_mock_ld2450.yaml +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE From 9152f77cdd3833d51026ee95c917ed868c234ba8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:31:48 -1000 Subject: [PATCH 0913/2056] [core] Reduce automation call chain stack depth (#15042) --- esphome/core/automation.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7934fdbec97..ca4a2c8b6b0 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,9 @@ template<typename... Ts> class Automation; template<typename... Ts> class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(const Ts &...x) { + // Force-inline: collapses the Trigger→Automation→ActionList forwarding + // chain into a single frame, reducing automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +431,9 @@ template<typename... Ts> class ActionList { this->add_action(action); } } - void play(const Ts &...x) { + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void play(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +477,9 @@ template<typename... Ts> class Automation { void stop() { this->actions_.stop(); } - void trigger(const Ts &...x) { this->actions_.play(x...); } + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } From 6caa9ee22770acee9ccbe2369001900dc772a7d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:32:08 -1000 Subject: [PATCH 0914/2056] [logger] Move log level lookup tables to PROGMEM (#15003) --- esphome/components/logger/log_buffer.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 734cb14dc58..067ce04114f 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -8,8 +9,8 @@ namespace esphome::logger { // Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) static constexpr uint16_t MAX_HEADER_SIZE = 128; -// ANSI color code last digit (30-38 range, store only last digit to save RAM) -static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { +// ANSI color code last digit (30-38 range, store only last digit to save RAM on ESP8266) +static const char LOG_LEVEL_COLOR_DIGIT[] PROGMEM = { '\0', // NONE '1', // ERROR (31 = red) '3', // WARNING (33 = yellow) @@ -20,7 +21,7 @@ static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '8', // VERY_VERBOSE (38 = white) }; -static constexpr char LOG_LEVEL_LETTER_CHARS[] = { +static const char LOG_LEVEL_LETTER_CHARS[] PROGMEM = { '\0', // NONE 'E', // ERROR 'W', // WARNING @@ -64,7 +65,7 @@ struct LogBuffer { *p++ = 'V'; // VERY_VERBOSE = "VV" *p++ = 'V'; } else { - *p++ = LOG_LEVEL_LETTER_CHARS[level]; + *p++ = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(&LOG_LEVEL_LETTER_CHARS[level]))); } } *p++ = ']'; @@ -184,7 +185,7 @@ struct LogBuffer { *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold *p++ = ';'; *p++ = '3'; - *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(&LOG_LEVEL_COLOR_DIGIT[level]))); *p++ = 'm'; } // Copy string without null terminator, updates pointer in place From 30f66be1dab8dc645fdd77293c0730521d03f4dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:32:42 -1000 Subject: [PATCH 0915/2056] [esp32] Mention ignore_pin_validation_error in flash pin error message (#14998) --- esphome/components/esp32/gpio_esp32.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index b3166cf822e..fec257f90b8 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -28,7 +28,11 @@ def esp32_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + f"This pin cannot be used on ESP32s and is already used by the flash interface" + f" (function: {_ESP_SDIO_PINS[value]})." + f" If you are using an ESP32 module that uses a different flash pin" + f" configuration (e.g. ESP32-PICO-V3-02), you can set" + f" 'ignore_pin_validation_error: true' to bypass this check." ) if 9 <= value <= 10: _LOGGER.warning( From b2b61bea6a12993bf6bcb0c2160a4fd735fcfe0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:33:06 -1000 Subject: [PATCH 0916/2056] [web_server_idf] Inline send() to reduce httpd task stack depth (#15045) --- .../components/web_server_idf/web_server_idf.cpp | 13 ------------- esphome/components/web_server_idf/web_server_idf.h | 13 +++++++++++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index fb0c17c854d..38ccfccb76d 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -262,19 +262,6 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co return StringRef(buffer.data(), decoded_len); } -void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { - httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); -} - -void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { - this->init_response_(nullptr, code, content_type); - if (content) { - httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); - } else { - httpd_resp_send(*this, nullptr, 0); - } -} - void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 9d81d89ec76..81683e8d852 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -134,8 +134,17 @@ class AsyncWebServerRequest { void redirect(const std::string &url); - void send(AsyncWebServerResponse *response); - void send(int code, const char *content_type = nullptr, const char *content = nullptr); + inline void ESPHOME_ALWAYS_INLINE send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); + } + inline void ESPHOME_ALWAYS_INLINE send(int code, const char *content_type = nullptr, const char *content = nullptr) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } + } // NOLINTNEXTLINE(readability-identifier-naming) AsyncWebServerResponse *beginResponse(int code, const char *content_type) { auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) From aef987dccf26fbe06a9a54f870e59eb1daa7a176 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:37:46 -1000 Subject: [PATCH 0917/2056] [core] Fix Callback::create memcpy from function reference (#14995) --- esphome/core/helpers.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 43431299dea..82c6b3833ce 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1762,7 +1762,10 @@ template<typename... Ts> struct Callback<void(Ts...)> { // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly // creates objects of implicit-lifetime types (trivially copyable qualifies). Callback cb; // fn and ctx are zero-initialized by default - __builtin_memcpy(&cb.ctx_, &callable, sizeof(DecayF)); + // Decay callable to a local variable first. When F is a function reference + // (e.g. void(&)(int)), &callable would point at machine code, not a pointer variable. + DecayF decayed = std::forward<F>(callable); + __builtin_memcpy(&cb.ctx_, &decayed, sizeof(DecayF)); cb.fn_ = [](void *c, Ts... args) { alignas(DecayF) char buf[sizeof(DecayF)]; __builtin_memcpy(buf, &c, sizeof(DecayF)); From 84727b1f71e2b3a08f297941592ef6df64fd2ebd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:41:01 -1000 Subject: [PATCH 0918/2056] [esp32] Validate eFuse MAC reads and reject garbage MACs (#15049) --- esphome/components/esp32/helpers.cpp | 46 ++++++++++++++++++++++------ esphome/core/helpers.cpp | 11 ++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 654a35b4735..afcec8bfc70 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -9,11 +9,14 @@ #include <freertos/FreeRTOS.h> #include <freertos/portmacro.h> +#include "esphome/core/log.h" #include "esp_random.h" #include "esp_system.h" namespace esphome { +static const char *const TAG = "esp32"; + bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; @@ -63,22 +66,43 @@ LwIPLock::~LwIPLock() { #endif } +/// Read MAC and validate both the return code and content. +static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK && mac_address_is_valid(mac); } + +static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. - if (has_custom_mac_address()) { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); - } else { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + // Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback + // (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks). + if (has_custom_mac_address() && + read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) { + return; + } + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + return; } #else - if (has_custom_mac_address()) { - esp_efuse_mac_get_custom(mac); - } else { - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) { + return; + } + if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) { + return; + } + // Default MAC read failed (e.g., eFuse CRC error) - try reading raw eFuse bytes + // directly, bypassing CRC validation. A MAC that passes mac_address_is_valid() + // (non-zero, non-broadcast, unicast) is almost certainly the real factory MAC + // with a corrupted CRC byte, which is far better than returning garbage or zeros. + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + ESP_LOGW(TAG, "eFuse MAC CRC failed but raw bytes appear valid - using raw eFuse MAC"); + return; } #endif + // All methods failed - zero the MAC rather than returning garbage + ESP_LOGE(TAG, "Failed to read a valid MAC address from eFuse"); + memset(mac, 0, MAC_ADDRESS_SIZE); } void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } @@ -88,9 +112,11 @@ bool has_custom_mac_address() { uint8_t mac[6]; // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails #ifndef USE_ESP32_VARIANT_ESP32 - return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #else - return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #endif #else return false; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ee99c541963..1732fc72e8b 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -863,7 +863,16 @@ bool mac_address_is_valid(const uint8_t *mac) { is_all_ones = false; } } - return !(is_all_zeros || is_all_ones); + if (is_all_zeros || is_all_ones) { + return false; + } + // Reject multicast MACs (bit 0 of first byte set) - device MACs must be unicast. + // This catches garbage data from corrupted eFuse custom MAC areas, which often + // has random values that would otherwise pass the all-zeros/all-ones check. + if (mac[0] & 0x01) { + return false; + } + return true; } void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { From 2c06464f7baa843bf8a45d9bbe93c8314af708a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:41:54 -1000 Subject: [PATCH 0919/2056] [packet_transport] Use FixedVector and parent pointer to enable inline Callback storage (#14946) --- esphome/components/packet_transport/__init__.py | 10 ++++++++-- .../packet_transport/packet_transport.cpp | 16 ++++++++++------ .../packet_transport/packet_transport.h | 15 +++++++++++---- .../binary_sensor/binary_sensor_test.cpp | 4 ++++ .../packet_transport/sensor/sensor_test.cpp | 6 ++++++ 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 1930e45e854..0b166bb65c2 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -177,13 +177,19 @@ async def register_packet_transport(var, config): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) is_provider = False - for sens_conf in config.get(CONF_SENSORS, ()): + sensors = config.get(CONF_SENSORS, ()) + binary_sensors = config.get(CONF_BINARY_SENSORS, ()) + if sensors: + cg.add(var.set_sensor_count(len(sensors))) + if binary_sensors: + cg.add(var.set_binary_sensor_count(len(binary_sensors))) + for sens_conf in sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + for sens_conf in binary_sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 964037a02c4..a2199977aa0 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,16 +221,20 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](float x) { + sensor.parent->updated_ = true; sensor.updated = true; }); } #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](bool value) { + sensor.parent->updated_ = true; sensor.updated = true; }); } @@ -548,11 +552,11 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (auto sensor : this->sensors_) + for (const auto &sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); #endif #ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); #endif for (const auto &host : this->providers_) { diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index b3798738e2c..836775bc855 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -37,11 +38,14 @@ struct Provider { #endif }; +class PacketTransport; + #ifdef USE_SENSOR struct Sensor { sensor::Sensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif #ifdef USE_BINARY_SENSOR @@ -49,6 +53,7 @@ struct BinarySensor { binary_sensor::BinarySensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif @@ -60,8 +65,9 @@ class PacketTransport : public PollingComponent { void dump_config() override; #ifdef USE_SENSOR + void set_sensor_count(size_t count) { this->sensors_.init(count); } void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; + Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); } void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { @@ -70,8 +76,9 @@ class PacketTransport : public PollingComponent { } #endif #ifdef USE_BINARY_SENSOR + void set_binary_sensor_count(size_t count) { this->binary_sensors_.init(count); } void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; + BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); } @@ -141,11 +148,11 @@ class PacketTransport : public PollingComponent { std::vector<uint8_t> encryption_key_{}; #ifdef USE_SENSOR - std::vector<Sensor> sensors_{}; + FixedVector<Sensor> sensors_{}; string_map_t<string_map_t<sensor::Sensor *>> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector<BinarySensor> binary_sensors_{}; + FixedVector<BinarySensor> binary_sensors_{}; string_map_t<string_map_t<binary_sensor::BinarySensor *>> remote_binary_sensors_{}; #endif diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp index 36af087d2c2..5ad25c2d7d1 100644 --- a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportBinarySensorTest, AddBinarySensor) { TestablePacketTransport transport; binary_sensor::BinarySensor bs; + transport.set_binary_sensor_count(1); transport.add_binary_sensor("motion", &bs); ASSERT_EQ(transport.binary_sensors_.size(), 1u); EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); @@ -24,6 +25,7 @@ TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { encoder.init_for_test("sender"); binary_sensor::BinarySensor local_bs; local_bs.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("motion", &local_bs); encoder.send_data_(true); @@ -46,11 +48,13 @@ TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { sensor::Sensor s1, s2; s1.state = 10.0f; s2.state = 20.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); binary_sensor::BinarySensor bs1; bs1.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("bs1", &bs1); encoder.send_data_(true); diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp index 2f681aee587..5d1cfb4bc2b 100644 --- a/tests/components/packet_transport/sensor/sensor_test.cpp +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportSensorTest, AddSensor) { TestablePacketTransport transport; sensor::Sensor s; + transport.set_sensor_count(1); transport.add_sensor("temp", &s); ASSERT_EQ(transport.sensors_.size(), 1u); EXPECT_STREQ(transport.sensors_[0].id, "temp"); @@ -26,6 +27,7 @@ TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { encoder.init_for_test("sender"); sensor::Sensor local_sensor; local_sensor.state = 42.5f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -53,6 +55,7 @@ TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { encoder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 99.9f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -77,6 +80,7 @@ TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { sensor::Sensor s1, s2; s1.state = 1.0f; s2.state = 2.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); @@ -111,6 +115,7 @@ TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); // Requester sends a MAGIC_PING that the responder processes @@ -148,6 +153,7 @@ TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); responder.send_data_(true); ASSERT_EQ(responder.sent_packets.size(), 1u); From d0e705d948c20aa68eab915ffbd5c3699916bd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 12:46:28 -1000 Subject: [PATCH 0920/2056] [core] Inline Application::loop() to eliminate stack frame (#15041) --- esphome/components/esp32/core.cpp | 4 +- esphome/components/socket/socket.h | 2 +- esphome/core/application.cpp | 145 +-------------------------- esphome/core/application.h | 154 ++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 149 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 83bd09b643d..313818e6019 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "crash_handler.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,7 +16,6 @@ #include <freertos/task.h> void setup(); // NOLINT(readability-redundant-declaration) -void loop(); // NOLINT(readability-redundant-declaration) // Weak stub for initArduino - overridden when the Arduino component is present extern "C" __attribute__((weak)) void initArduino() {} @@ -65,7 +65,7 @@ TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non- void loop_task(void *pv_params) { setup(); while (true) { - loop(); + App.loop(); } } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a21bd647305..226a669e31a 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -125,7 +125,7 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, uses esp_delay() with a callback that checks socket activity. /// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt /// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) /// Signal socket/IO activity and wake the main loop early. /// On ESP8266: sets flag + esp_schedule(). diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 08df3854755..c020a8ed585 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,21 +12,11 @@ #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include <freertos/FreeRTOS.h> -#include <freertos/task.h> -#else -#include <FreeRTOS.h> -#include <task.h> -#endif #endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include <algorithm> #include <ranges> -#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" @@ -163,66 +153,6 @@ void Application::setup() { this->schedule_dump_config(); } -void Application::loop() { - uint8_t new_app_state = 0; - - // Get the initial loop time at the start - uint32_t last_op_end_time = millis(); - - this->before_loop_tasks_(last_op_end_time); - - for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; - this->current_loop_index_++) { - Component *component = this->looping_components_[this->current_loop_index_]; - - // Update the cached time before each component runs - this->loop_component_start_time_ = last_op_end_time; - - { - this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->loop(); - // Use the finish method to get the current time as the end time - last_op_end_time = guard.finish(); - } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; - this->feed_wdt(last_op_end_time); - } - - this->after_loop_tasks_(); - 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()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; - 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); - delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); - } - this->last_loop_ = last_op_end_time; - - if (this->dump_config_at_ < this->components_.size()) { - this->process_dump_config_(); - } -} void Application::process_dump_config_() { if (this->dump_config_at_ == 0) { @@ -509,41 +439,6 @@ void Application::enable_pending_loops_() { } } -void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) - // Drain wake notifications first to clear socket for next wake - this->drain_wake_notifications_(); -#endif - - // Process scheduled tasks - this->scheduler.call(loop_start_time); - - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; -} - -void Application::after_loop_tasks_() { - // Clear the in_loop_ flag to indicate we're done processing components - this->in_loop_ = false; -} - #ifdef USE_LWIP_FAST_SELECT bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. @@ -625,36 +520,10 @@ void Application::unregister_socket_fd(int fd) { #endif +// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); - -#elif defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). - // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -701,16 +570,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); -#endif } +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 26abc154332..06ff30e81f6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,6 +27,13 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#else +#include <FreeRTOS.h> +#include <task.h> +#endif #else #include <sys/select.h> #ifdef USE_WAKE_LOOP_THREADSAFE @@ -34,9 +41,13 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket #endif #ifdef USE_BINARY_SENSOR @@ -293,7 +304,7 @@ class Application { void setup(); /// Make a loop iteration. Call this in your loop() function. - void loop(); + inline void ESPHOME_ALWAYS_INLINE loop(); /// Get the name of this Application set by pre_setup(). const StringRef &get_name() const { return this->name_; } @@ -617,8 +628,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - void before_loop_tasks_(uint32_t loop_start_time); - void after_loop_tasks_(); + inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -628,7 +639,12 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); +#else + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket @@ -814,4 +830,134 @@ inline void Application::drain_wake_notifications_() { } #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + + // Process scheduled tasks + this->scheduler.call(loop_start_time); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +inline void ESPHOME_ALWAYS_INLINE Application::loop() { + uint8_t new_app_state = 0; + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + this->before_loop_tasks_(last_op_end_time); + + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + + { + this->set_current_component(component); + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; + component->loop(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(last_op_end_time); + } + + this->after_loop_tasks_(); + 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()) { + // Even if we overran the loop interval, we still need to select() + // to know if any sockets have data ready + this->yield_with_select_(0); + } else { + uint32_t delay_time = this->loop_interval_ - elapsed; + 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); + delay_time = std::min(next_schedule, delay_time); + + this->yield_with_select_(delay_time); + } + this->last_loop_ = last_op_end_time; + + if (this->dump_config_at_ < this->components_.size()) { + this->process_dump_config_(); + } +} + +// Inline yield_with_select_ for all paths except the select() fallback +#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake + socket::socket_delay(delay_ms); +#else + // No select support, use regular delay + delay(delay_ms); +#endif +} +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) + } // namespace esphome From e85065b1c4211280a49b3b473905c33c83eae337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 14:06:00 -1000 Subject: [PATCH 0921/2056] [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- tests/dummy_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3d..329286e2fab 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From 83d02c602a911181ad0e0ef89ef50fde17ef0bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 14:06:00 -1000 Subject: [PATCH 0922/2056] [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- tests/dummy_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3d..329286e2fab 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From cd05462e9fc4a295b523412c04fa17acb6c86171 Mon Sep 17 00:00:00 2001 From: Kamil Cukrowski <kamilcukrowski@gmail.com> Date: Mon, 23 Mar 2026 01:42:04 +0100 Subject: [PATCH 0923/2056] [core] Use placement new allocation for Pvariables (#15079) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/cpp_generator.py | 48 +++++++++++++++---- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 3 +- tests/component_tests/conftest.py | 2 +- .../deep_sleep/test_deep_sleep.py | 6 ++- tests/component_tests/globals/__init__.py | 0 .../globals/config/globals_test.yaml | 16 +++++++ tests/component_tests/globals/test_globals.py | 27 +++++++++++ .../gpio/test_gpio_binary_sensor.py | 3 +- tests/component_tests/image/test_init.py | 10 +++- tests/component_tests/logger/test_logger.py | 4 ++ .../mipi_dsi/test_mipi_dsi_config.py | 10 +++- tests/component_tests/status_led/__init__.py | 0 .../status_led/config/status_led_test.yaml | 8 ++++ .../status_led/test_status_led.py | 23 +++++++++ tests/component_tests/text/test_text.py | 3 +- .../text_sensor/test_text_sensor.py | 3 +- 17 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 tests/component_tests/globals/__init__.py create mode 100644 tests/component_tests/globals/config/globals_test.yaml create mode 100644 tests/component_tests/globals/test_globals.py create mode 100644 tests/component_tests/status_led/__init__.py create mode 100644 tests/component_tests/status_led/config/status_led_test.yaml create mode 100644 tests/component_tests/status_led/test_status_led.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5457485d254..3ed5d0ba37d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -579,10 +579,41 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) - CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs) - CORE.add(assignment) + + if isinstance(rhs, MockObj) and rhs.is_new_expr: + # For 'new' allocations, use placement new into static storage + # to avoid heap fragmentation on embedded devices. + the_type = id_.type + storage_name = f"{id_.id}__pstorage" + + # Declare aligned byte array for the object storage + CORE.add_global( + RawStatement( + f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];" + ) + ) + CORE.add_global( + AssignmentExpression( + f"static {the_type}", + "*const ", + id_, + MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"), + ) + ) + # Extract args from the CallExpression and rebuild as placement new. + # Template args are already encoded in the_type (e.g. GlobalsComponent<int>), + # so we only pass the constructor args, not template_args. + call_expr = rhs.base + assert isinstance(call_expr, CallExpression), ( + f"Expected CallExpression for placement new, got {type(call_expr)}" + ) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + CORE.add(ExpressionStatement(placement_new)) + else: + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) + CORE.add_global(decl) + CORE.add(AssignmentExpression(None, None, id_, rhs)) + CORE.register_variable(id_, obj) return obj @@ -799,11 +830,12 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op") + __slots__ = ("base", "op", "is_new_expr") - def __init__(self, base, op="."): + def __init__(self, base, op=".", is_new_expr=False) -> None: self.base = base self.op = op + self.is_new_expr = is_new_expr def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -818,7 +850,7 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op) + return MockObj(call, self.op, is_new_expr=self.is_new_expr) def __str__(self): return str(self.base) @@ -832,7 +864,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->") + return MockObj(f"new {self.base}", "->", is_new_expr=True) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 10d7f808346..4f41f2cc704 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -15,7 +15,7 @@ def test_binary_sensor_is_setup(generate_main): ) # Then - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp assert "App.register_binary_sensor" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index a35994a682c..544e748f913 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -13,7 +13,8 @@ def test_button_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert "new wake_on_lan::WakeOnLanButton();" in main_cpp + assert "static wake_on_lan::WakeOnLanButton *const" in main_cpp + assert ") wake_on_lan::WakeOnLanButton();" in main_cpp assert "App.register_button" in main_cpp assert "App.register_component" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 0641e698e97..763628f57c9 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -134,7 +134,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: CORE.config_path = Path(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) - return CORE.cpp_main_section + return CORE.cpp_global_section + CORE.cpp_main_section yield generator diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 41ddd72febd..212f61e44b9 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -7,7 +7,11 @@ def test_deep_sleep_setup(generate_main): """ main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") - assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert ( + "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deepsleep__pstorage);" + in main_cpp + ) + assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp assert "App.register_component_(deepsleep);" in main_cpp diff --git a/tests/component_tests/globals/__init__.py b/tests/component_tests/globals/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/globals/config/globals_test.yaml b/tests/component_tests/globals/config/globals_test.yaml new file mode 100644 index 00000000000..1d1a9edaa62 --- /dev/null +++ b/tests/component_tests/globals/config/globals_test.yaml @@ -0,0 +1,16 @@ +esphome: + name: test + +esp32: + board: esp32dev + +globals: + - id: my_global_int + type: int + initial_value: "42" + - id: my_global_float + type: float + initial_value: "1.5" + - id: my_global_bool + type: bool + initial_value: "true" diff --git a/tests/component_tests/globals/test_globals.py b/tests/component_tests/globals/test_globals.py new file mode 100644 index 00000000000..04fd6d5f7de --- /dev/null +++ b/tests/component_tests/globals/test_globals.py @@ -0,0 +1,27 @@ +"""Tests for the globals component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_globals_placement_new_with_template_args( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that globals uses placement new with template arguments preserved.""" + main_cpp = generate_main(component_config_path("globals_test.yaml")) + + # Globals uses Pvariable with Type.new(template_args, initial_value) + # which exercises the template_args preservation in placement new. + assert "static globals::GlobalsComponent<int> *const my_global_int" in main_cpp + assert "sizeof(globals::GlobalsComponent<int>)" in main_cpp + assert "new(my_global_int) globals::GlobalsComponent<int>" in main_cpp + + # Verify initial value is passed as constructor arg + assert "42" in main_cpp + + # Check other globals are also generated + assert "sizeof(globals::GlobalsComponent<float>)" in main_cpp + assert "sizeof(globals::GlobalsComponent<bool>)" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 73665dc45d2..f336a9105ec 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -16,7 +16,8 @@ def test_gpio_binary_sensor_basic_setup( """ main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp + assert ") gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp # set_use_interrupt(true) should NOT be generated (uses C++ default) assert "bs_gpio->set_use_interrupt(true);" not in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index c9481a0e1d7..9003a4ee5d9 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,7 +242,15 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" + "alignas(image::Image) static unsigned char cat_img__pstorage[sizeof(image::Image)];" + in main_cpp + ) + assert ( + "static image::Image *const cat_img = reinterpret_cast<image::Image *>(cat_img__pstorage);" + in main_cpp + ) + assert ( + "new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" in main_cpp ) diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 98aa7419642..94a6f7ac7bc 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -22,6 +22,10 @@ def test_logger_pre_setup_before_other_components(generate_main): # Find all "new " allocations (component creation) new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + # Find all "new(" allocations (component creation) and combine them + new_allocations.extend(re.finditer(r"\bnew\([^)]+\) [\w:]+", main_cpp)) + # Sort allocations by position in the file + new_allocations.sort(key=lambda m: m.start()) assert len(new_allocations) > 0, "No component allocations found" # Separate logger and non-logger allocations diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 92f56b5451a..e6f344b086c 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,7 +119,15 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "alignas(mipi_dsi::MIPI_DSI) static unsigned char p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" + in main_cpp + ) + assert ( + "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(p4_nano__pstorage);" + in main_cpp + ) + assert ( + "new(p4_nano) mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp diff --git a/tests/component_tests/status_led/__init__.py b/tests/component_tests/status_led/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/status_led/config/status_led_test.yaml b/tests/component_tests/status_led/config/status_led_test.yaml new file mode 100644 index 00000000000..c86197d2256 --- /dev/null +++ b/tests/component_tests/status_led/config/status_led_test.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + +status_led: + pin: GPIO2 diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py new file mode 100644 index 00000000000..0e96e631f5b --- /dev/null +++ b/tests/component_tests/status_led/test_status_led.py @@ -0,0 +1,23 @@ +"""Tests for status_led.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_status_led_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test status_led generation.""" + main_cpp = generate_main(component_config_path("status_led_test.yaml")) + assert ( + "alignas(status_led::StatusLED) static unsigned char status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" + in main_cpp + ) + assert ( + "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast<status_led::StatusLED *>(status_led_statusled_id__pstorage);" + in main_cpp + ) + assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index c74dfb8a471..63eb4f19515 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -13,7 +13,8 @@ def test_text_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "new template_::TemplateText();" in main_cpp + assert "static template_::TemplateText *const" in main_cpp + assert ") template_::TemplateText();" in main_cpp assert "App.register_text" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1ff31ab96bd..ae094fadf87 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -13,7 +13,8 @@ def test_text_sensor_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert "new template_::TemplateTextSensor();" in main_cpp + assert "static template_::TemplateTextSensor *const" in main_cpp + assert ") template_::TemplateTextSensor();" in main_cpp assert "App.register_text_sensor" in main_cpp From 9cdc17566a9d9b96ff37943cb815e4d66703e47e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 15:06:45 -1000 Subject: [PATCH 0924/2056] [combination] Use FixedVector and parent pointer to enable inline Callback storage (#14947) --- .../components/combination/combination.cpp | 34 +++++++++---------- esphome/components/combination/combination.h | 18 +++++++--- esphome/components/combination/sensor.py | 3 ++ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 2f0bd26a02d..b858eee4ee4 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -4,8 +4,6 @@ #include "esphome/core/hal.h" #include <cmath> -#include <functional> -#include <vector> namespace esphome { namespace combination { @@ -20,12 +18,12 @@ void CombinationComponent::log_config_(const LogString *combo_type) { void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &stddev) { - this->sensor_pairs_.emplace_back(sensor, stddev); +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &compute) { + this->sensor_sources_.push_back({sensor, compute, this}); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function<float(float)>{[stddev](float x) -> float { return stddev; }}); +void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) { + this->add_source(sensor, std::function<float(float)>{[value](float x) -> float { return value; }}); } void CombinationNoParameterComponent::log_source_sensors() { @@ -37,9 +35,8 @@ void CombinationNoParameterComponent::log_source_sensors() { void CombinationOneParameterComponent::log_source_sensors() { ESP_LOGCONFIG(TAG, " Source Sensors:"); - for (const auto &sensor : this->sensor_pairs_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + for (const auto &source : this->sensor_sources_) { + ESP_LOGCONFIG(TAG, " - %s", source.sensor->get_name().c_str()); } } @@ -62,9 +59,12 @@ void KalmanCombinationComponent::dump_config() { } void KalmanCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + for (auto &source : this->sensor_sources_) { + // [&source] is safe: source refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + source.sensor->add_on_state_callback([&source](float x) -> void { + static_cast<KalmanCombinationComponent *>(source.parent)->correct_(x, source.compute(x)); + }); } } @@ -117,10 +117,10 @@ void KalmanCombinationComponent::correct_(float value, float stddev) { } void LinearCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { + for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor.first->add_on_state_callback( + source.sensor->add_on_state_callback( [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); } } @@ -133,10 +133,10 @@ void LinearCombinationComponent::handle_new_value(float value) { float sum = 0.0; - for (const auto &sensor : this->sensor_pairs_) { - const float sensor_state = sensor.first->state; + for (const auto &source : this->sensor_sources_) { + const float sensor_state = source.sensor->state; if (std::isfinite(sensor_state)) { - sum += sensor_state * sensor.second(sensor_state); + sum += sensor_state * source.compute(sensor_state); } } diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index fb5e156da9a..463eedc5644 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -1,9 +1,10 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" -#include <vector> +#include <functional> namespace esphome { namespace combination { @@ -41,14 +42,21 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: - void add_source(Sensor *sensor, std::function<float(float)> const &stddev); - void add_source(Sensor *sensor, float stddev); + void set_source_count(size_t count) { this->sensor_sources_.init(count); } + void add_source(Sensor *sensor, std::function<float(float)> const &compute); + void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_pairs_ + /// @brief Logs all source sensors' names in sensor_sources_ void log_source_sensors() override; protected: - std::vector<std::pair<Sensor *, std::function<float(float)>>> sensor_pairs_; + struct SensorSource { + sensor::Sensor *sensor; + std::function<float(float)> compute; + CombinationOneParameterComponent *parent; + }; + + FixedVector<SensorSource> sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index 0204162e8d9..327cedee1ef 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -180,6 +180,9 @@ async def to_code(config): if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): cg.add(var.set_process_std_dev(proces_std_dev)) + if config[CONF_TYPE] in (CONF_KALMAN, CONF_LINEAR): + cg.add(var.set_source_count(len(config[CONF_SOURCES]))) + for source_conf in config[CONF_SOURCES]: source = await cg.get_variable(source_conf[CONF_SOURCE]) if config[CONF_TYPE] == CONF_KALMAN: From fbe3e7d99c59da1bdbef279cd99501707b2945eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 15:28:46 -1000 Subject: [PATCH 0925/2056] [api] Emit raw tag+value writes for forced fixed32 key fields (#15051) --- esphome/components/api/api.proto | 142 ++++++++++---------- esphome/components/api/api_pb2.cpp | 200 ++++++++++++++-------------- esphome/components/api/proto.h | 18 ++- script/api_protobuf/api_protobuf.py | 17 ++- 4 files changed, 202 insertions(+), 175 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 28332d67a59..86daa9a2bf6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -316,7 +316,7 @@ message ListEntitiesBinarySensorResponse { option (ifdef) = "USE_BINARY_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -334,7 +334,7 @@ message BinarySensorStateResponse { option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -350,7 +350,7 @@ message ListEntitiesCoverResponse { option (ifdef) = "USE_COVER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -383,7 +383,7 @@ message CoverStateResponse { option (ifdef) = "USE_COVER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change // Deprecated in API version 1.1 @@ -409,7 +409,7 @@ message CoverCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change @@ -434,7 +434,7 @@ message ListEntitiesFanResponse { option (ifdef) = "USE_FAN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -466,7 +466,7 @@ message FanStateResponse { option (ifdef) = "USE_FAN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; bool oscillating = 3; // Deprecated in API version 1.6 @@ -483,7 +483,7 @@ message FanCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; // Deprecated in API version 1.6 @@ -522,7 +522,7 @@ message ListEntitiesLightResponse { option (ifdef) = "USE_LIGHT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -551,7 +551,7 @@ message LightStateResponse { option (ifdef) = "USE_LIGHT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; float brightness = 3; ColorMode color_mode = 11; @@ -573,7 +573,7 @@ message LightCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_brightness = 4; @@ -627,7 +627,7 @@ message ListEntitiesSensorResponse { option (ifdef) = "USE_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -651,7 +651,7 @@ message SensorStateResponse { option (ifdef) = "USE_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -667,7 +667,7 @@ message ListEntitiesSwitchResponse { option (ifdef) = "USE_SWITCH"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -685,7 +685,7 @@ message SwitchStateResponse { option (ifdef) = "USE_SWITCH"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -696,7 +696,7 @@ message SwitchCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -709,7 +709,7 @@ message ListEntitiesTextSensorResponse { option (ifdef) = "USE_TEXT_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -726,7 +726,7 @@ message TextSensorStateResponse { option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -922,7 +922,7 @@ message ListEntitiesServicesResponse { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; SupportsResponseType supports_response = 4; } @@ -945,7 +945,7 @@ message ExecuteServiceRequest { option (no_delay) = true; option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; @@ -972,7 +972,7 @@ message ListEntitiesCameraResponse { option (ifdef) = "USE_CAMERA"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; @@ -987,7 +987,7 @@ message CameraImageResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bytes data = 2; bool done = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -1057,7 +1057,7 @@ message ListEntitiesClimateResponse { option (ifdef) = "USE_CLIMATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1095,7 +1095,7 @@ message ClimateStateResponse { option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; ClimateMode mode = 2; float current_temperature = 3; float target_temperature = 4; @@ -1121,7 +1121,7 @@ message ClimateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_mode = 2; ClimateMode mode = 3; bool has_target_temperature = 4; @@ -1168,7 +1168,7 @@ message ListEntitiesWaterHeaterResponse { option (ifdef) = "USE_WATER_HEATER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -1189,7 +1189,7 @@ message WaterHeaterStateResponse { option (ifdef) = "USE_WATER_HEATER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float current_temperature = 2; float target_temperature = 3; WaterHeaterMode mode = 4; @@ -1219,7 +1219,7 @@ message WaterHeaterCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // Bitmask of which fields are set (see WaterHeaterCommandHasField) uint32 has_fields = 2; WaterHeaterMode mode = 3; @@ -1244,7 +1244,7 @@ message ListEntitiesNumberResponse { option (ifdef) = "USE_NUMBER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1266,7 +1266,7 @@ message NumberStateResponse { option (ifdef) = "USE_NUMBER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1280,7 +1280,7 @@ message NumberCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1293,7 +1293,7 @@ message ListEntitiesSelectResponse { option (ifdef) = "USE_SELECT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1310,7 +1310,7 @@ message SelectStateResponse { option (ifdef) = "USE_SELECT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1324,7 +1324,7 @@ message SelectCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1337,7 +1337,7 @@ message ListEntitiesSirenResponse { option (ifdef) = "USE_SIREN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1356,7 +1356,7 @@ message SirenStateResponse { option (ifdef) = "USE_SIREN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1367,7 +1367,7 @@ message SirenCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_tone = 4; @@ -1400,7 +1400,7 @@ message ListEntitiesLockResponse { option (ifdef) = "USE_LOCK"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1422,7 +1422,7 @@ message LockStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1432,7 +1432,7 @@ message LockCommandRequest { option (ifdef) = "USE_LOCK"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockCommand command = 2; // Not yet implemented: @@ -1449,7 +1449,7 @@ message ListEntitiesButtonResponse { option (ifdef) = "USE_BUTTON"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1466,7 +1466,7 @@ message ButtonCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } @@ -1516,7 +1516,7 @@ message ListEntitiesMediaPlayerResponse { option (ifdef) = "USE_MEDIA_PLAYER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1538,7 +1538,7 @@ message MediaPlayerStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; MediaPlayerState state = 2; float volume = 3; bool muted = 4; @@ -1551,7 +1551,7 @@ message MediaPlayerCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_command = 2; MediaPlayerCommand command = 3; @@ -2104,7 +2104,7 @@ message ListEntitiesAlarmControlPanelResponse { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2122,7 +2122,7 @@ message AlarmControlPanelStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2133,7 +2133,7 @@ message AlarmControlPanelCommandRequest { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelStateCommand command = 2; string code = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2151,7 +2151,7 @@ message ListEntitiesTextResponse { option (ifdef) = "USE_TEXT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2171,7 +2171,7 @@ message TextStateResponse { option (ifdef) = "USE_TEXT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -2185,7 +2185,7 @@ message TextCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2199,7 +2199,7 @@ message ListEntitiesDateResponse { option (ifdef) = "USE_DATETIME_DATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2215,7 +2215,7 @@ message DateStateResponse { option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the date does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2231,7 +2231,7 @@ message DateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 year = 2; uint32 month = 3; uint32 day = 4; @@ -2246,7 +2246,7 @@ message ListEntitiesTimeResponse { option (ifdef) = "USE_DATETIME_TIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2262,7 +2262,7 @@ message TimeStateResponse { option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the time does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2278,7 +2278,7 @@ message TimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 hour = 2; uint32 minute = 3; uint32 second = 4; @@ -2293,7 +2293,7 @@ message ListEntitiesEventResponse { option (ifdef) = "USE_EVENT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2311,7 +2311,7 @@ message EventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string event_type = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2324,7 +2324,7 @@ message ListEntitiesValveResponse { option (ifdef) = "USE_VALVE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2351,7 +2351,7 @@ message ValveStateResponse { option (ifdef) = "USE_VALVE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float position = 2; ValveOperation current_operation = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2364,7 +2364,7 @@ message ValveCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_position = 2; float position = 3; bool stop = 4; @@ -2379,7 +2379,7 @@ message ListEntitiesDateTimeResponse { option (ifdef) = "USE_DATETIME_DATETIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2395,7 +2395,7 @@ message DateTimeStateResponse { option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the datetime does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2409,7 +2409,7 @@ message DateTimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; fixed32 epoch_seconds = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2422,7 +2422,7 @@ message ListEntitiesUpdateResponse { option (ifdef) = "USE_UPDATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2439,7 +2439,7 @@ message UpdateStateResponse { option (ifdef) = "USE_UPDATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool missing_state = 2; bool in_progress = 3; bool has_progress = 4; @@ -2463,7 +2463,7 @@ message UpdateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; UpdateCommand command = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2505,7 +2505,7 @@ message ListEntitiesInfraredResponse { option (ifdef) = "USE_INFRARED"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -2521,7 +2521,7 @@ message InfraredRFTransmitRawTimingsRequest { option (ifdef) = "USE_IR_RF"; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the transmitter instance + fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance uint32 carrier_frequency = 3; // Carrier frequency in Hz uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) @@ -2535,7 +2535,7 @@ message InfraredRFReceiveEvent { option (no_delay) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the receiver instance + fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01993cc5e5f..61b034c7eae 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -208,7 +208,7 @@ uint32_t DeviceInfoResponse::calculate_size() const { #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); @@ -224,7 +224,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); @@ -239,7 +239,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -248,7 +248,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t BinarySensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -260,7 +260,7 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); @@ -279,7 +279,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); @@ -297,7 +297,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation)); @@ -307,7 +307,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CoverStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_float(1, this->tilt); size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); @@ -357,7 +357,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); @@ -378,7 +378,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_oscillation); size += ProtoSize::calc_bool(1, this->supports_speed); @@ -400,7 +400,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast<uint32_t>(this->direction)); @@ -412,7 +412,7 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t FanStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->oscillating); size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->direction)); @@ -487,7 +487,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast<uint32_t>(it), true); @@ -509,7 +509,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { @@ -534,7 +534,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); buffer.encode_uint32(11, static_cast<uint32_t>(this->color_mode)); @@ -553,7 +553,7 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LightStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_float(1, this->brightness); size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->color_mode)); @@ -683,7 +683,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -702,7 +702,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -720,7 +720,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -729,7 +729,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -741,7 +741,7 @@ uint32_t SensorStateResponse::calculate_size() const { #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -757,7 +757,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -772,7 +772,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -780,7 +780,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SwitchStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -816,7 +816,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -831,7 +831,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -845,7 +845,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -854,7 +854,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextSensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1124,7 +1124,7 @@ uint32_t ListEntitiesServicesArgument::calculate_size() const { } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (auto &it : this->args) { buffer.encode_sub_message(3, it); } @@ -1133,7 +1133,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesServicesResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->args.empty()) { for (const auto &it : this->args) { size += ProtoSize::calc_message_force(1, it.calculate_size()); @@ -1269,7 +1269,7 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1283,7 +1283,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1296,7 +1296,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); #ifdef USE_DEVICES @@ -1305,7 +1305,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CameraImageResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->data_len_); size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES @@ -1330,7 +1330,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); @@ -1374,7 +1374,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); @@ -1429,7 +1429,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); @@ -1449,7 +1449,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); @@ -1563,7 +1563,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -1584,7 +1584,7 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1606,7 +1606,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); buffer.encode_uint32(4, static_cast<uint32_t>(this->mode)); @@ -1619,7 +1619,7 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t WaterHeaterStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); @@ -1675,7 +1675,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1695,7 +1695,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1714,7 +1714,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1723,7 +1723,7 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t NumberStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1760,7 +1760,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1777,7 +1777,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1795,7 +1795,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1804,7 +1804,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SelectStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1849,7 +1849,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1868,7 +1868,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1888,7 +1888,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -1896,7 +1896,7 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SirenStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -1961,7 +1961,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1979,7 +1979,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1996,7 +1996,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2004,7 +2004,7 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -2054,7 +2054,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2069,7 +2069,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2124,7 +2124,7 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2143,7 +2143,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2163,7 +2163,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); @@ -2173,7 +2173,7 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); size += ProtoSize::calc_float(1, this->volume); size += ProtoSize::calc_bool(1, this->muted); @@ -2942,7 +2942,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2959,7 +2959,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2975,7 +2975,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2983,7 +2983,7 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3030,7 +3030,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3048,7 +3048,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3065,7 +3065,7 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -3074,7 +3074,7 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -3119,7 +3119,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3133,7 +3133,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3146,7 +3146,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); @@ -3157,7 +3157,7 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->year); size += ProtoSize::calc_uint32(1, this->month); @@ -3202,7 +3202,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3216,7 +3216,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3229,7 +3229,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); @@ -3240,7 +3240,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->hour); size += ProtoSize::calc_uint32(1, this->minute); @@ -3285,7 +3285,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3303,7 +3303,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3322,7 +3322,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -3330,7 +3330,7 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t EventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3341,7 +3341,7 @@ uint32_t EventResponse::calculate_size() const { #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3359,7 +3359,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3376,7 +3376,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES @@ -3385,7 +3385,7 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ValveStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES @@ -3428,7 +3428,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3442,7 +3442,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3455,7 +3455,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); #ifdef USE_DEVICES @@ -3464,7 +3464,7 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateTimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES @@ -3501,7 +3501,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3516,7 +3516,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3530,7 +3530,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); @@ -3546,7 +3546,7 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t UpdateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_bool(1, this->in_progress); size += ProtoSize::calc_bool(1, this->has_progress); @@ -3642,7 +3642,7 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -3657,7 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3717,7 +3717,7 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (const auto &it : *this->timings) { buffer.encode_sint32(3, it, true); } @@ -3727,7 +3727,7 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->timings->empty()) { for (const auto &it : *this->timings) { size += ProtoSize::calc_sint32_force(1, it); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6e993d3a5a..cd229157030 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -236,6 +236,21 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a precomputed tag byte + 32-bit value in one operation. + /// Tag must be a single-byte varint (< 128). No zero check. + inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(5); + this->pos_[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(this->pos_ + 1, &value, 4); +#else + this->pos_[1] = static_cast<uint8_t>(value & 0xFF); + this->pos_[2] = static_cast<uint8_t>((value >> 8) & 0xFF); + this->pos_[3] = static_cast<uint8_t>((value >> 16) & 0xFF); + this->pos_[4] = static_cast<uint8_t>((value >> 24) & 0xFF); +#endif + this->pos_ += 5; + } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; @@ -276,8 +291,7 @@ class ProtoWriteBuffer { this->debug_check_bounds_(1); *this->pos_++ = value ? 0x01 : 0x00; } - // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy - __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dff6c7690a0..aca31e49c8e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -254,14 +254,17 @@ class TypeInfo(ABC): def dump(self, name: str) -> str: """Dump the value to the output.""" + def calculate_tag(self) -> int: + """Calculate the protobuf tag (field_id << 3 | wire_type).""" + return (self.number << 3) | (self.wire_type & 0b111) + def calculate_field_id_size(self) -> int: """Calculates the size of a field ID in bytes. Returns: The number of bytes needed to encode the field ID """ - # Calculate the tag by combining field_id and wire_type - tag = (self.number << 3) | (self.wire_type & 0b111) + tag = self.calculate_tag() # Calculate the varint size if tag < 128: @@ -556,6 +559,16 @@ class Fixed32Type(TypeInfo): o += "out.append(buffer);" return o + @property + def encode_content(self) -> str: + tag = self.calculate_tag() + if self.force and tag < 128: + # Emit combined tag+value write: precomputed tag + direct memcpy + return f"buffer.write_tag_and_fixed32({tag}, this->{self.field_name});" + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() if force: From 6992219e341b4eca9f2bfdf28474cbf45883bd08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 16:27:07 -1000 Subject: [PATCH 0926/2056] [core] Attribute placement new storage symbols to components (#15092) --- esphome/analyze_memory/__init__.py | 29 ++++++ esphome/analyze_memory/cli.py | 48 +++++++--- esphome/cpp_generator.py | 11 ++- .../deep_sleep/test_deep_sleep.py | 2 +- tests/component_tests/image/test_init.py | 4 +- .../mipi_dsi/test_mipi_dsi_config.py | 4 +- .../status_led/test_status_led.py | 4 +- .../test_pstorage_attribution.py | 90 +++++++++++++++++++ 8 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 tests/unit_tests/analyze_memory/test_pstorage_attribution.py diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 48ecf2c1dc3..f56d720ec2a 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" +# Placement new storage suffix (generated by codegen Pvariable) +_PSTORAGE_SUFFIX = "__pstorage" + + # C++ namespace prefixes _NAMESPACE_ESPHOME = "esphome::" _NAMESPACE_STD = "std::" @@ -332,6 +336,13 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) + # Check for placement new storage symbols (generated by codegen) + # Format: {component}__{id}__pstorage + if demangled.endswith(_PSTORAGE_SUFFIX) and ( + component := self._match_pstorage_component(demangled) + ): + return component + # Check for special component classes first (before namespace pattern) # This handles cases like esphome::ESPHomeOTAComponent which should map to ota if _NAMESPACE_ESPHOME in demangled: @@ -399,6 +410,24 @@ class MemoryAnalyzer: # Track uncategorized symbols for analysis return "other" + def _match_pstorage_component(self, symbol_name: str) -> str | None: + """Match a __pstorage symbol to its ESPHome component. + + Symbol format: {component}__{id}__pstorage + The component namespace is embedded by codegen before the double underscore. + """ + prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)] + # Extract component namespace before the first double underscore + dunder_pos = prefix.find("__") + if dunder_pos == -1: + return None + component_name = prefix[:dunder_pos] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + return None + def _batch_demangle_symbols(self, symbols: list[str]) -> None: """Batch demangle C++ symbol names for efficiency.""" if not symbols: diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index acaf5f45621..b7561e8ffc0 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,7 @@ from . import ( _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_LIB, + _PSTORAGE_SUFFIX, RAM_SECTIONS, MemoryAnalyzer, ) @@ -23,6 +24,17 @@ if TYPE_CHECKING: from . import ComponentMemory +def _format_pstorage_name(name: str) -> str: + """Format a __pstorage symbol as 'storage for {id}'.""" + if not name.endswith(_PSTORAGE_SUFFIX): + return name + prefix = name[: -len(_PSTORAGE_SUFFIX)] + # Strip component namespace prefix: {component}__{id} -> {id} + dunder_pos = prefix.find("__") + var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix + return f"storage for {var_id}" + + class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" @@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): If section is one of the RAM sections (.data or .bss), a label like " [data]" or " [bss]" is appended. For non-RAM sections or when section is None, no section label is added. + + Placement new storage symbols are formatted as "storage for {id}". """ + display_name = _format_pstorage_name(demangled) section_label = "" if section in RAM_SECTIONS: section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] - return f"{demangled} ({size:,} B){section_label}" + return f"{display_name} ({size:,} B){section_label}" def _add_top_symbols(self, lines: list[str]) -> None: """Add a section showing the top largest symbols in the binary.""" @@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for i, (_, demangled, size, section, component) in enumerate(top_symbols): # Format section label section_label = f"[{section[1:]}]" if section else "" - # Truncate demangled name if too long + # Format storage symbols readably + display_name = _format_pstorage_name(demangled) + # Truncate if too long demangled_display = ( - f"{demangled[:truncate_limit]}..." - if len(demangled) > self.COL_TOP_SYMBOL_NAME - else demangled + f"{display_name[:truncate_limit]}..." + if len(display_name) > self.COL_TOP_SYMBOL_NAME + else display_name ) lines.append( f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" @@ -573,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols above threshold for better visibility + # Show symbols above threshold, always include storage symbols large_symbols = [ (sym, dem, size, sec) for sym, dem, size, sec in sorted_symbols if size > self.SYMBOL_SIZE_THRESHOLD + or dem.endswith(_PSTORAGE_SUFFIX) ] lines.append( - f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size, section) in enumerate(large_symbols): lines.append( @@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Sort by size descending sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) large_ram_syms = [ - s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + s + for s in sorted_ram_syms + if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + or s[1].endswith(_PSTORAGE_SUFFIX) ] lines.append(f"{name} ({mem.ram_total:,} B total RAM):") @@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for symbol, demangled, size, section in large_ram_syms[:10]: # Format section label consistently by stripping leading dot section_label = section.lstrip(".") if section else "" + display_name = _format_pstorage_name(demangled) # Add ellipsis if name is truncated - demangled_display = ( - f"{demangled[:70]}..." if len(demangled) > 70 else demangled - ) - lines.append( - f" {size:>6,} B [{section_label}] {demangled_display}" + display_name = ( + f"{display_name[:70]}..." + if len(display_name) > 70 + else display_name ) + lines.append(f" {size:>6,} B [{section_label}] {display_name}") if len(large_ram_syms) > 10: lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 3ed5d0ba37d..e97bd71a48d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -584,7 +584,16 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. the_type = id_.type - storage_name = f"{id_.id}__pstorage" + # Extract component namespace from type for memory analysis attribution + type_str = str(the_type) + # Strip leading esphome:: to get the component namespace + # e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger + bare = type_str.removeprefix("esphome::") + if "::" in bare: + component_ns = bare.split("::", maxsplit=1)[0].rstrip("_") + else: + component_ns = "esphome" + storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage CORE.add_global( diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 212f61e44b9..8c1278a3323 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main): main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert ( - "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deepsleep__pstorage);" + "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast<deep_sleep::DeepSleepComponent *>(deep_sleep__deepsleep__pstorage);" in main_cpp ) assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 9003a4ee5d9..6f73888c7d1 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,11 +242,11 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "alignas(image::Image) static unsigned char cat_img__pstorage[sizeof(image::Image)];" + "alignas(image::Image) static unsigned char image__cat_img__pstorage[sizeof(image::Image)];" in main_cpp ) assert ( - "static image::Image *const cat_img = reinterpret_cast<image::Image *>(cat_img__pstorage);" + "static image::Image *const cat_img = reinterpret_cast<image::Image *>(image__cat_img__pstorage);" in main_cpp ) assert ( diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index e6f344b086c..1ae8cc644e0 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,11 +119,11 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "alignas(mipi_dsi::MIPI_DSI) static unsigned char p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" + "alignas(mipi_dsi::MIPI_DSI) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" in main_cpp ) assert ( - "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(p4_nano__pstorage);" + "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(mipi_dsi__p4_nano__pstorage);" in main_cpp ) assert ( diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py index 0e96e631f5b..f7e0a9de86d 100644 --- a/tests/component_tests/status_led/test_status_led.py +++ b/tests/component_tests/status_led/test_status_led.py @@ -13,11 +13,11 @@ def test_status_led_generation( """Test status_led generation.""" main_cpp = generate_main(component_config_path("status_led_test.yaml")) assert ( - "alignas(status_led::StatusLED) static unsigned char status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" + "alignas(status_led::StatusLED) static unsigned char status_led__status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" in main_cpp ) assert ( - "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast<status_led::StatusLED *>(status_led_statusled_id__pstorage);" + "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast<status_led::StatusLED *>(status_led__status_led_statusled_id__pstorage);" in main_cpp ) assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/unit_tests/analyze_memory/test_pstorage_attribution.py b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py new file mode 100644 index 00000000000..a57b283f448 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py @@ -0,0 +1,90 @@ +"""Tests for __pstorage symbol attribution in memory analyzer.""" + +from unittest.mock import patch + +from esphome.analyze_memory import _PSTORAGE_SUFFIX, MemoryAnalyzer + + +def _make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer: + """Create a MemoryAnalyzer with mocked dependencies.""" + with patch.object(MemoryAnalyzer, "__init__", lambda self, *a, **kw: None): + analyzer = MemoryAnalyzer.__new__(MemoryAnalyzer) + analyzer.external_components = external_components or set() + return analyzer + + +def test_pstorage_suffix_constant() -> None: + """Verify the suffix constant matches what codegen produces.""" + assert _PSTORAGE_SUFFIX == "__pstorage" + + +def test_match_pstorage_simple_component() -> None: + """Simple component name like 'logger'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__logger_id__pstorage") + assert result == "[esphome]logger" + + +def test_match_pstorage_underscore_component() -> None: + """Component with underscore like 'web_server'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("web_server__webserver_id__pstorage") + assert result == "[esphome]web_server" + + +def test_match_pstorage_api() -> None: + """API component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("api__apiserver_id__pstorage") + assert result == "[esphome]api" + + +def test_match_pstorage_deep_sleep() -> None: + """Component with underscore: deep_sleep.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("deep_sleep__deepsleep__pstorage") + assert result == "[esphome]deep_sleep" + + +def test_match_pstorage_status_led() -> None: + """Component with underscore: status_led.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("status_led__statusled_id__pstorage") + assert result == "[esphome]status_led" + + +def test_match_pstorage_external_component() -> None: + """External component should be attributed correctly.""" + analyzer = _make_analyzer(external_components={"my_custom"}) + result = analyzer._match_pstorage_component("my_custom__thing_id__pstorage") + assert result == "[external]my_custom" + + +def test_match_pstorage_no_dunder_returns_none() -> None: + """Symbol without double underscore separator returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("something__pstorage") + assert result is None + + +def test_match_pstorage_unknown_component_returns_none() -> None: + """Unknown component namespace returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("nonexistent__thing_id__pstorage") + assert result is None + + +def test_match_pstorage_esphome_component() -> None: + """esphome:: namespace types map to the esphome component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component( + "esphome__esphomeotacomponent_id__pstorage" + ) + assert result == "[esphome]esphome" + + +def test_match_pstorage_user_id_with_component_prefix() -> None: + """User-chosen ID that happens to contain a component name.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__relay1__pstorage") + assert result == "[esphome]logger" From 98d9fd76b35f995e14fca86e07515317f2220d22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 16:27:20 -1000 Subject: [PATCH 0927/2056] [mqtt] Fix const-correctness for trigger constructors (#15093) --- esphome/components/mqtt/mqtt_client.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 0d52d98d2fa..14473f737a0 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -366,14 +366,14 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> { class MQTTConnectTrigger : public Trigger<bool> { public: - explicit MQTTConnectTrigger(MQTTClientComponent *&client) { + explicit MQTTConnectTrigger(MQTTClientComponent *client) { client->set_on_connect([this](bool session_present) { this->trigger(session_present); }); } }; class MQTTDisconnectTrigger : public Trigger<MQTTClientDisconnectReason> { public: - explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) { + explicit MQTTDisconnectTrigger(MQTTClientComponent *client) { client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); }); } }; From 8a3b5a8defe9d2f58ce9b1ca4447e2d0bfe8f77e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 17:09:23 -1000 Subject: [PATCH 0928/2056] [core] Fix placement new storage name for templated types (#15096) --- esphome/cpp_generator.py | 32 ++++++++++++++++++++++++-------- tests/unit_tests/test_codegen.py | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index e97bd71a48d..a8efe96cce3 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -565,6 +565,29 @@ def new_variable( return obj +def _extract_component_ns(type_str: str) -> str: + """Extract the component namespace from a fully-qualified C++ type string. + + Strips leading ``esphome::`` and template arguments, then returns + the first namespace segment. Falls back to ``"esphome"`` when the + type has no namespace qualifier (after stripping templates). + + Examples:: + + esphome::dsmr::Dsmr -> dsmr + esphome::logger::Logger -> logger + esphome::Automation<std::optional<bool>, std::optional<bool>> -> esphome + Logger -> esphome + """ + bare = type_str.removeprefix("esphome::") + # Strip template arguments before namespace extraction to avoid + # matching :: inside template params (e.g. Automation<std::optional<bool>>) + bare_no_template = bare.split("<", maxsplit=1)[0] + if "::" in bare_no_template: + return bare_no_template.split("::", maxsplit=1)[0].rstrip("_") + return "esphome" + + def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new pointer variable in the code generation. @@ -585,14 +608,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": # to avoid heap fragmentation on embedded devices. the_type = id_.type # Extract component namespace from type for memory analysis attribution - type_str = str(the_type) - # Strip leading esphome:: to get the component namespace - # e.g. esphome::dsmr::Dsmr -> dsmr, logger::Logger -> logger - bare = type_str.removeprefix("esphome::") - if "::" in bare: - component_ns = bare.split("::", maxsplit=1)[0].rstrip("_") - else: - component_ns = "esphome" + component_ns = _extract_component_ns(str(the_type)) storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 3f32a117ff9..8d01fef7c20 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -1,6 +1,7 @@ import pytest from esphome import codegen as cg +from esphome.cpp_generator import _extract_component_ns # Test interface remains the same. @@ -75,3 +76,29 @@ from esphome import codegen as cg ) def test_exists(attr): assert hasattr(cg, attr) + + +@pytest.mark.parametrize( + ("type_str", "expected"), + ( + ("esphome::dsmr::Dsmr", "dsmr"), + ("esphome::logger::Logger", "logger"), + ("esphome::web_server::WebServer", "web_server"), + ("esphome::deep_sleep::DeepSleep", "deep_sleep"), + ("esphome::Component", "esphome"), + ("Logger", "esphome"), + # Template types with :: in template args must not confuse extraction + ( + "esphome::Automation<std::optional<bool>, std::optional<bool>>", + "esphome", + ), + ( + "esphome::StatelessLambdaAction<std::optional<bool>, std::optional<bool>>", + "esphome", + ), + # Namespaced template type + ("esphome::sensor::Sensor<std::string>", "sensor"), + ), +) +def test_extract_component_ns(type_str, expected): + assert _extract_component_ns(type_str) == expected From 597bb185432ac4b5c069bf7936ff9a425af94641 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 17:30:57 -1000 Subject: [PATCH 0929/2056] [benchmark] Add binary sensor publish and sensor filter benchmarks (#15035) --- .../components/binary_sensor/__init__.py | 5 ++ .../bench_binary_sensor_publish.cpp | 61 +++++++++++++++ .../components/binary_sensor/benchmark.yaml | 1 + .../benchmarks/components/sensor/__init__.py | 12 +++ .../components/sensor/bench_sensor_filter.cpp | 78 +++++++++++++++++++ .../components/sensor/benchmark.yaml | 1 + 6 files changed, 158 insertions(+) create mode 100644 tests/benchmarks/components/binary_sensor/__init__.py create mode 100644 tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp create mode 100644 tests/benchmarks/components/binary_sensor/benchmark.yaml create mode 100644 tests/benchmarks/components/sensor/__init__.py create mode 100644 tests/benchmarks/components/sensor/bench_sensor_filter.cpp create mode 100644 tests/benchmarks/components/sensor/benchmark.yaml diff --git a/tests/benchmarks/components/binary_sensor/__init__.py b/tests/benchmarks/components/binary_sensor/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp new file mode 100644 index 00000000000..8bae943e2ea --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp @@ -0,0 +1,61 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome::binary_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: publish_state with alternating values (forces state change every time) +static void BinarySensorPublish_Alternating(benchmark::State &state) { + BinarySensor sensor; + + // First publish to establish initial state + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_Alternating); + +// Benchmark: publish_state with same value (tests dedup fast path) +static void BinarySensorPublish_NoChange(benchmark::State &state) { + BinarySensor sensor; + + sensor.publish_initial_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(true); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_NoChange); + +// Benchmark: publish_state with a callback registered +static void BinarySensorPublish_WithCallback(benchmark::State &state) { + BinarySensor sensor; + + int callback_count = 0; + sensor.add_on_state_callback([&callback_count](bool) { callback_count++; }); + + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_WithCallback); + +} // namespace esphome::binary_sensor::benchmarks diff --git a/tests/benchmarks/components/binary_sensor/benchmark.yaml b/tests/benchmarks/components/binary_sensor/benchmark.yaml new file mode 100644 index 00000000000..fc0db6c52c9 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/benchmark.yaml @@ -0,0 +1 @@ +binary_sensor: diff --git a/tests/benchmarks/components/sensor/__init__.py b/tests/benchmarks/components/sensor/__init__.py new file mode 100644 index 00000000000..5a593aa8c27 --- /dev/null +++ b/tests/benchmarks/components/sensor/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Sensor filter benchmarks need USE_SENSOR_FILTER defined. + # We use a custom to_code instead of enable_codegen() to avoid + # pulling in the full sensor component setup. + async def to_code(config): + cg.add_define("USE_SENSOR_FILTER") + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp new file mode 100644 index 00000000000..e4aa3976901 --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -0,0 +1,78 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensor/filter.h" + +namespace esphome::sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1) +static void SensorFilter_SlidingWindowAvg(benchmark::State &state) { + Sensor sensor; + + // Create filter: window_size=5, send_every=1, send_first_at=1 + auto *filter = new SlidingWindowMovingAverageFilter(5, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_SlidingWindowAvg); + +// Benchmark: sensor publish through ExponentialMovingAverageFilter +static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) { + Sensor sensor; + + // alpha=0.1, send_every=1, send_first_at=1 + auto *filter = new ExponentialMovingAverageFilter(0.1f, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_ExponentialMovingAvg); + +// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window) +static void SensorFilter_Chain3(benchmark::State &state) { + Sensor sensor; + + sensor.add_filters({ + new OffsetFilter(1.0f), + new MultiplyFilter(2.0f), + new SlidingWindowMovingAverageFilter(5, 1, 1), + }); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_Chain3); + +} // namespace esphome::sensor::benchmarks diff --git a/tests/benchmarks/components/sensor/benchmark.yaml b/tests/benchmarks/components/sensor/benchmark.yaml new file mode 100644 index 00000000000..e1fb52cdd6a --- /dev/null +++ b/tests/benchmarks/components/sensor/benchmark.yaml @@ -0,0 +1 @@ +sensor: From 0de2c758aa1c42d6b9734fb410244c18bf7156f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 17:31:27 -1000 Subject: [PATCH 0930/2056] [scheduler] Use placement-new for std::function move in set_timer_common_ (#14757) --- esphome/core/scheduler.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 51cbfb208ea..9ee3b2fdd2a 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -166,7 +166,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; - item->callback = std::move(func); + // Use destroy + placement-new instead of move-assignment. + // GCC's std::function::operator=(function&&) does a full swap dance even when the + // target is empty. Since recycled/new items always have an empty callback, we can + // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of + // swap/destructor code on Xtensa. + item->callback.~function(); + new (&item->callback) std::function<void()>(std::move(func)); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item, false); item->is_retry = is_retry; From baf365404cb2b2d9a359718661a31f1c8d2604f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 18:18:43 -1000 Subject: [PATCH 0931/2056] [network] Inline get_use_address() to eliminate function call overhead (#14942) --- .../ethernet/ethernet_component.cpp | 6 ----- .../components/ethernet/ethernet_component.h | 4 ++-- esphome/components/network/util.cpp | 24 ------------------- esphome/components/network/util.h | 24 ++++++++++++++++++- esphome/components/openthread/openthread.cpp | 6 ----- esphome/components/openthread/openthread.h | 4 ++-- esphome/components/wifi/wifi_component.cpp | 4 ---- esphome/components/wifi/wifi_component.h | 4 ++-- 8 files changed, 29 insertions(+), 47 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 4421a1c7aaf..42cb0b3cfc0 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -18,12 +18,6 @@ void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *EthernetComponent::get_use_address() const { return this->use_address_; } - -void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { auto ips = this->get_ip_addresses(); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 88a86bc0432..b6699e80204 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -103,8 +103,8 @@ class EthernetComponent final : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); // Remove before 2026.9.0 ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 226b11b8cd3..79ddd3844c5 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -42,29 +42,5 @@ network::IPAddresses get_ip_addresses() { return {}; } -const char *get_use_address() { - // Global component pointers are guaranteed to be set by component constructors when USE_* is defined -#ifdef USE_ETHERNET - return ethernet::global_eth_component->get_use_address(); -#endif - -#ifdef USE_MODEM - return modem::global_modem_component->get_use_address(); -#endif - -#ifdef USE_WIFI - return wifi::global_wifi_component->get_use_address(); -#endif - -#ifdef USE_OPENTHREAD - return openthread::global_openthread_component->get_use_address(); -#endif - -#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) - // Fallback when no network component is defined (e.g., host platform) - return ""; -#endif -} - } // namespace esphome::network #endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 4b700fe74c0..e4e8a01f8cb 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -54,7 +54,29 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() { /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const char *get_use_address(); +ESPHOME_ALWAYS_INLINE inline const char *get_use_address() { + // Global component pointers are guaranteed to be set by component constructors when USE_* is defined +#ifdef USE_ETHERNET + return ethernet::global_eth_component->get_use_address(); +#endif + +#ifdef USE_MODEM + return modem::global_modem_component->get_use_address(); +#endif + +#ifdef USE_WIFI + return wifi::global_wifi_component->get_use_address(); +#endif + +#ifdef USE_OPENTHREAD + return openthread::global_openthread_component->get_use_address(); +#endif + +#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) + // Fallback when no network component is defined (e.g., host platform) + return ""; +#endif +} IPAddresses get_ip_addresses(); } // namespace esphome::network diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 1596b6e9908..7c9a3083034 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -257,11 +257,5 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) { ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } - -void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - } // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index bd10774fcf5..b42fdd2d307 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -37,8 +37,8 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function<void()> callback); void defer_factory_reset_external_callback(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 08065a7544e..0fd13852581 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -891,10 +891,6 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { return this->wifi_dns_ip_(num); return {}; } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index bd202604d6f..718f4a6e12c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -481,8 +481,8 @@ class WiFiComponent final : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; } From e67b5a78d0b4fd4d06ddc347141eb4e19c7d4f10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 20:51:40 -1000 Subject: [PATCH 0932/2056] [esp32] Patch DRAM segment for testing mode to fix grouped component test overflow (#15102) --- esphome/components/esp32/iram_fix.py.script | 70 ++++++++++++--------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32/iram_fix.py.script b/esphome/components/esp32/iram_fix.py.script index 0d23f9a81ba..b656d7d1a6f 100644 --- a/esphome/components/esp32/iram_fix.py.script +++ b/esphome/components/esp32/iram_fix.py.script @@ -4,12 +4,40 @@ import re # pylint: disable=E0602 Import("env") # noqa -# IRAM size for testing mode (2MB - large enough to accommodate grouped tests) -TESTING_IRAM_SIZE = 0x200000 +# Memory sizes for testing mode (large enough to accommodate grouped tests) +TESTING_IRAM_SIZE = 0x200000 # 2MB +TESTING_DRAM_SIZE = 0x200000 # 2MB + + +def patch_segment(content, segment_name, new_size): + """Patch a memory segment's length in linker script content. + + Handles both single-line and multi-line segment definitions, e.g.: + iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 + or split across lines: + dram0_0_seg (RW) : org = 0x3FFB0000 + 0xdb5c, + len = 0x2c200 - 0xdb5c + + Args: + content: Full linker script content as string + segment_name: Name of the segment (e.g., 'iram0_0_seg') + new_size: New size as integer + + Returns: + Tuple of (new_content, was_patched) + """ + # Match segment name through to "len = <value>" allowing newlines between org and len + pattern = rf'({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)' + if match := re.search(pattern, content, re.DOTALL): + replacement = f"{match.group(1)}{new_size:#x}" + new_content = content[:match.start()] + replacement + content[match.end():] + if new_content != content: + return new_content, True + return content, False def patch_idf_linker_script(source, target, env): - """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" + """Patch ESP-IDF linker script to increase IRAM and DRAM size for testing mode.""" # Check if we're in testing mode by looking for the define build_flags = env.get("BUILD_FLAGS", []) testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) @@ -34,36 +62,22 @@ def patch_idf_linker_script(source, target, env): print(f"ESPHome: Error reading linker script: {e}") return - # Check if this file contains iram0_0_seg - if 'iram0_0_seg' not in content: - print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") - return + patches = [] - # Look for iram0_0_seg definition and increase its length - # ESP-IDF format can be: - # iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 - # or more complex with nested parentheses: - # iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) - # We want to change len to TESTING_IRAM_SIZE for testing + content, patched = patch_segment(content, 'iram0_0_seg', TESTING_IRAM_SIZE) + if patched: + patches.append(f"IRAM={TESTING_IRAM_SIZE:#x}") - # Use a more robust approach: find the line and manually parse it - lines = content.split('\n') - for i, line in enumerate(lines): - if 'iram0_0_seg' in line and 'len' in line: - # Find the position of "len = " and replace everything after it until the end of the statement - match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) - if match: - lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" - break + content, patched = patch_segment(content, 'dram0_0_seg', TESTING_DRAM_SIZE) + if patched: + patches.append(f"DRAM={TESTING_DRAM_SIZE:#x}") - updated = '\n'.join(lines) - - if updated != content: + if patches: with open(memory_ld, "w") as f: - f.write(updated) - print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") + f.write(content) + print(f"ESPHome: Patched {', '.join(patches)} in {memory_ld} for testing mode") else: - print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") + print(f"ESPHome: Warning - could not patch memory segments in {memory_ld}") # Hook into the build process before linking From 225330413a137acbff12e81127b983ad05d92e00 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 23 Mar 2026 01:55:14 -0500 Subject: [PATCH 0933/2056] [uart] Rename `FlushResult` to `UARTFlushResult` with `UART_FLUSH_RESULT_` prefix (#15101) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --- esphome/components/api/api_connection.cpp | 8 ++++---- esphome/components/ble_nus/ble_nus.cpp | 6 +++--- esphome/components/ble_nus/ble_nus.h | 2 +- .../components/serial_proxy/serial_proxy.cpp | 2 +- .../components/serial_proxy/serial_proxy.h | 2 +- esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.h | 19 +++++++------------ .../uart/uart_component_esp8266.cpp | 4 ++-- .../components/uart/uart_component_esp8266.h | 2 +- .../uart/uart_component_esp_idf.cpp | 8 ++++---- .../components/uart/uart_component_esp_idf.h | 2 +- .../components/uart/uart_component_host.cpp | 6 +++--- esphome/components/uart/uart_component_host.h | 2 +- .../uart/uart_component_libretiny.cpp | 4 ++-- .../uart/uart_component_libretiny.h | 2 +- .../components/uart/uart_component_rp2040.cpp | 4 ++-- .../components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../usb_cdc_acm/usb_cdc_acm_esp32.cpp | 10 +++++----- esphome/components/usb_uart/usb_uart.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 6 +++--- esphome/components/weikai/weikai.h | 2 +- tests/components/ld2450/common.h | 2 +- tests/components/uart/common.h | 2 +- .../uart_mock/uart_mock.cpp | 4 ++-- .../external_components/uart_mock/uart_mock.h | 2 +- 27 files changed, 55 insertions(+), 60 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 40c27b224b2..82d7e3f674d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1519,16 +1519,16 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { resp.instance = msg.instance; resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; switch (proxies[msg.instance]->flush_port()) { - case uart::FlushResult::SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_OK; break; - case uart::FlushResult::ASSUMED_SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; break; - case uart::FlushResult::TIMEOUT: + case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; break; - case uart::FlushResult::FAILED: + case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: resp.status = enums::SERIAL_PROXY_STATUS_ERROR; break; } diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 2f60f814718..71d98332e09 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -103,17 +103,17 @@ size_t BLENUS::available() { #endif } -uart::FlushResult BLENUS::flush() { +uart::UARTFlushResult BLENUS::flush() { constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } delay(1); } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b482c240e53..f1afd54af9e 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 00d822b75cb..f3c256c62a7 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -165,7 +165,7 @@ uint32_t SerialProxy::get_modem_pins() const { (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); } -uart::FlushResult SerialProxy::flush_port() { +uart::UARTFlushResult SerialProxy::flush_port() { ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); return this->flush(); } diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 5adfa4fe53b..c435787a61e 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -92,7 +92,7 @@ class SerialProxy : public uart::UARTDevice, public Component { uint32_t get_modem_pins() const; /// Flush the serial port (block until all TX data is sent) - uart::FlushResult flush_port(); + uart::UARTFlushResult flush_port(); /// Set the RTS GPIO pin (from YAML configuration) void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 2c4fb34c9a8..899d349e212 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - FlushResult flush() { return this->parent_->flush(); } + UARTFlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 00a4c788784..abc77fbae82 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,17 +30,12 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. -// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro. -// Save and restore around the enum to avoid collisions with our scoped enum value. -#pragma push_macro("SUCCESS") -#undef SUCCESS -enum class FlushResult { - SUCCESS, ///< Confirmed: all bytes left the TX FIFO. - TIMEOUT, ///< Confirmed: timed out before TX completed. - FAILED, ///< Confirmed: driver or hardware error. - ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +enum class UARTFlushResult { + UART_FLUSH_RESULT_SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + UART_FLUSH_RESULT_TIMEOUT, ///< Confirmed: timed out before TX completed. + UART_FLUSH_RESULT_FAILED, ///< Confirmed: driver or hardware error. + UART_FLUSH_RESULT_ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; -#pragma pop_macro("SUCCESS") class UARTComponent { public: @@ -87,8 +82,8 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. - virtual FlushResult flush() = 0; + // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual UARTFlushResult flush() = 0; // Sets the maximum time to wait for TX to drain during flush(). // Only meaningful on ESP32 (IDF). Other platforms ignore this value. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 91218c43006..0ea79307605 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,14 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -FlushResult ESP8266UartComponent::flush() { +UARTFlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index ca90dc59646..7f844d9b651 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8168e49805a..bd2f915d3a8 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -360,15 +360,15 @@ size_t IDFUARTComponent::available() { return available; } -FlushResult IDFUARTComponent::flush() { +UARTFlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); if (err == ESP_OK) - return FlushResult::SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return FlushResult::TIMEOUT; - return FlushResult::FAILED; + return UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 9fa2013cfd1..ec4f2884b29 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 66026f3ccdd..0042ffae23f 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,13 +274,13 @@ size_t HostUartComponent::available() { return result; }; -FlushResult HostUartComponent::flush() { +UARTFlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index c22efdcb92d..56ff525bc3d 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 6a550f296a1..4172e7c1649 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,10 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -FlushResult LibreTinyUARTComponent::flush() { +UARTFlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 77df8080671..872ea866014 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 6f6f1fb96b7..1aaf98dc84b 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -208,10 +208,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -FlushResult RP2040UartComponent::flush() { +UARTFlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 891212ca74c..198c698af9c 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index a56abc9ee8e..89405ab8939 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; protected: void check_logger_conflict() override; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 583aa77d063..5498c385159 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -325,10 +325,10 @@ size_t USBCDCACMInstance::available() { return waiting + (this->has_peek_ ? 1 : 0); } -uart::FlushResult USBCDCACMInstance::flush() { +uart::UARTFlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -342,10 +342,10 @@ uart::FlushResult USBCDCACMInstance::flush() { // Also wait for USB to finish transmitting esp_err_t err = tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100)); if (err == ESP_OK) - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::FAILED; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a5d312f1912..0b8589f6713 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,7 +169,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -uart::FlushResult USBUartChannel::flush() { +uart::UARTFlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; @@ -181,8 +181,8 @@ uart::FlushResult USBUartChannel::flush() { yield(); } if (!this->output_queue_.empty() || this->output_started_.load()) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7a06b04f114..8a47f0cf4be 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -140,7 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override { return this->input_buffer_.get_available(); } - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index f01d164e9fd..2ec56326912 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,16 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast<uint8_t *>(buffer), length); } -uart::FlushResult WeikaiChannel::flush() { +uart::UARTFlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } yield(); // reschedule our thread to avoid blocking } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 715b82bfc71..36d8f66265e 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index 9f9e7b3e9f6..304634edcaa 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(uart::FlushResult, flush, (), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index f7e2d8a3f76..de3ea3029ef 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(FlushResult, flush, (), (override)); + MOCK_METHOD(UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1a15da76d13..d0690e75154 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -153,9 +153,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -uart::FlushResult MockUartComponent::flush() { +uart::UARTFlushResult MockUartComponent::flush() { // Nothing to flush in mock - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 82e3b3d5632..0b3b49893df 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; From f4097d5a95a37faa963ec2d8ad1588b70b259e3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 20:57:40 -1000 Subject: [PATCH 0934/2056] [api] Devirtualize API command dispatch (#15044) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 156 ++++++++++++--------- esphome/components/api/api_pb2_service.cpp | 3 +- esphome/components/api/api_pb2_service.h | 134 +++++++++--------- esphome/components/api/proto.h | 23 +-- script/api_protobuf/api_protobuf.py | 13 +- 6 files changed, 164 insertions(+), 167 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 82d7e3f674d..d023cd21a8d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -234,7 +234,7 @@ void APIConnection::loop() { this->last_traffic_ = now; } // read a packet - this->read_message(buffer.data_len, buffer.type, buffer.data); + this->read_message_(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 85c8e777a94..3d8563b1ae5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -49,11 +49,29 @@ class APIConnection final : public APIServerConnectionBase { friend class APIServer; friend class ListEntitiesIterator; APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); - virtual ~APIConnection(); + ~APIConnection(); void start(); void loop(); + protected: + // read_message_ is defined here (instead of in APIServerConnectionBase) so the + // compiler can devirtualize and inline on_* handler calls within this final class. + void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data); + + // Auth helpers defined here (not in ProtoService) so the compiler can + // devirtualize is_connection_setup()/on_no_setup_connection() calls + // within this final class. + inline bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + inline bool check_authenticated_() { return this->check_connection_setup_(); } + + public: bool send_list_info_done() { return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); @@ -63,72 +81,72 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void on_cover_command_request(const CoverCommandRequest &msg) override; + void on_cover_command_request(const CoverCommandRequest &msg); #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void on_fan_command_request(const FanCommandRequest &msg) override; + void on_fan_command_request(const FanCommandRequest &msg); #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void on_light_command_request(const LightCommandRequest &msg) override; + void on_light_command_request(const LightCommandRequest &msg); #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void on_switch_command_request(const SwitchCommandRequest &msg) override; + void on_switch_command_request(const SwitchCommandRequest &msg); #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_CAMERA void set_camera_state(std::shared_ptr<camera::CameraImage> image); - void on_camera_image_request(const CameraImageRequest &msg) override; + void on_camera_image_request(const CameraImageRequest &msg); #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void on_climate_command_request(const ClimateCommandRequest &msg) override; + void on_climate_command_request(const ClimateCommandRequest &msg); #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void on_number_command_request(const NumberCommandRequest &msg) override; + void on_number_command_request(const NumberCommandRequest &msg); #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void on_date_command_request(const DateCommandRequest &msg) override; + void on_date_command_request(const DateCommandRequest &msg); #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void on_time_command_request(const TimeCommandRequest &msg) override; + void on_time_command_request(const TimeCommandRequest &msg); #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void on_date_time_command_request(const DateTimeCommandRequest &msg) override; + void on_date_time_command_request(const DateTimeCommandRequest &msg); #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void on_text_command_request(const TextCommandRequest &msg) override; + void on_text_command_request(const TextCommandRequest &msg); #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void on_select_command_request(const SelectCommandRequest &msg) override; + void on_select_command_request(const SelectCommandRequest &msg); #endif #ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; + void on_button_command_request(const ButtonCommandRequest &msg); #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void on_lock_command_request(const LockCommandRequest &msg) override; + void on_lock_command_request(const LockCommandRequest &msg); #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void on_valve_command_request(const ValveCommandRequest &msg) override; + void on_valve_command_request(const ValveCommandRequest &msg); #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; + void on_media_player_command_request(const MediaPlayerCommandRequest &msg); #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -138,23 +156,23 @@ class APIConnection final : public APIServerConnectionBase { this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; + void on_homeassistant_action_response(const HomeassistantActionResponse &msg); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void on_unsubscribe_bluetooth_le_advertisements_request() override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg); + void on_unsubscribe_bluetooth_le_advertisements_request(); - void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; - void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; - void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; - void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; - void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; - void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; - void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; - void on_subscribe_bluetooth_connections_free_request() override; - void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; - void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg); + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg); + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg); + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg); + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg); + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg); + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg); + void on_subscribe_bluetooth_connections_free_request(); + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg); + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg); #endif #ifdef USE_HOMEASSISTANT_TIME @@ -165,42 +183,42 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT - void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; - void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; - void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; - void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; - void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; - void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; - void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg); + void on_voice_assistant_response(const VoiceAssistantResponse &msg); + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg); + void on_voice_assistant_audio(const VoiceAssistantAudio &msg); + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg); + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg); + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg); + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg); #endif #ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; - void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg); + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg); #endif #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg); #endif #ifdef USE_IR_RF - void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif #ifdef USE_SERIAL_PROXY - void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; - void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; - void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; - void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; - void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg); + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg); + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); + void on_serial_proxy_request(const SerialProxyRequest &msg); void send_serial_proxy_data(const SerialProxyDataReceived &msg); #endif @@ -210,26 +228,26 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void on_update_command_request(const UpdateCommandRequest &msg) override; + void on_update_command_request(const UpdateCommandRequest &msg); #endif - void on_disconnect_response() override; - void on_ping_response() override { + void on_disconnect_response(); + void on_ping_response() { // we initiated ping this->flags_.sent_ping = false; } #ifdef USE_API_HOMEASSISTANT_STATES - void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; + void on_home_assistant_state_response(const HomeAssistantStateResponse &msg); #endif #ifdef USE_HOMEASSISTANT_TIME - void on_get_time_response(const GetTimeResponse &value) override; + void on_get_time_response(const GetTimeResponse &value); #endif - void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request() override; - void on_ping_request() override; - void on_device_info_request() override; - void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void on_subscribe_states_request() override { + void on_hello_request(const HelloRequest &msg); + void on_disconnect_request(); + void on_ping_request(); + void on_device_info_request(); + void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void on_subscribe_states_request() { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -237,7 +255,7 @@ class APIConnection final : public APIServerConnectionBase { this->begin_iterator_(ActiveIterator::INITIAL_STATE); } } - void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override { + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); @@ -249,13 +267,13 @@ class APIConnection final : public APIServerConnectionBase { #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } + void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request() override; + void on_subscribe_home_assistant_states_request(); #endif #ifdef USE_API_USER_DEFINED_ACTIONS - void on_execute_service_request(const ExecuteServiceRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON @@ -265,13 +283,13 @@ class APIConnection final : public APIServerConnectionBase { #endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE - void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg); #endif - bool is_authenticated() override { + bool is_authenticated() { return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; } - bool is_connection_setup() override { + bool is_connection_setup() { return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } @@ -284,8 +302,8 @@ class APIConnection final : public APIServerConnectionBase { (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); } - void on_fatal_error() override; - void on_no_setup_connection() override; + void on_fatal_error(); + void on_no_setup_connection(); // Function pointer type for type-erased message encoding using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); @@ -324,7 +342,7 @@ class APIConnection final : public APIServerConnectionBase { return true; return this->try_to_clear_buffer_slow_(log_out_of_space); } - bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; + bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type); const char *get_name() const { return this->helper_->get_client_name(); } /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f2f7fa5238b..d86cf912db9 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,6 +1,7 @@ // This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -20,7 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { +void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements switch (msg_type) { case HelloRequest::MESSAGE_TYPE: // No setup required diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 10fd88d8e13..4925a6497a3 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -8,7 +8,7 @@ namespace esphome::api { -class APIServerConnectionBase : public ProtoService { +class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: @@ -19,227 +19,223 @@ class APIServerConnectionBase : public ProtoService { public: #endif - virtual void on_hello_request(const HelloRequest &value){}; + void on_hello_request(const HelloRequest &value){}; - virtual void on_disconnect_request(){}; - virtual void on_disconnect_response(){}; - virtual void on_ping_request(){}; - virtual void on_ping_response(){}; - virtual void on_device_info_request(){}; + void on_disconnect_request(){}; + void on_disconnect_response(){}; + void on_ping_request(){}; + void on_ping_response(){}; + void on_device_info_request(){}; - virtual void on_list_entities_request(){}; + void on_list_entities_request(){}; - virtual void on_subscribe_states_request(){}; + void on_subscribe_states_request(){}; #ifdef USE_COVER - virtual void on_cover_command_request(const CoverCommandRequest &value){}; + void on_cover_command_request(const CoverCommandRequest &value){}; #endif #ifdef USE_FAN - virtual void on_fan_command_request(const FanCommandRequest &value){}; + void on_fan_command_request(const FanCommandRequest &value){}; #endif #ifdef USE_LIGHT - virtual void on_light_command_request(const LightCommandRequest &value){}; + void on_light_command_request(const LightCommandRequest &value){}; #endif #ifdef USE_SWITCH - virtual void on_switch_command_request(const SwitchCommandRequest &value){}; + void on_switch_command_request(const SwitchCommandRequest &value){}; #endif - virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; + void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; #ifdef USE_API_NOISE - virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(){}; + void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; + void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(){}; + void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; + void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; #endif - virtual void on_get_time_response(const GetTimeResponse &value){}; + void on_get_time_response(const GetTimeResponse &value){}; #ifdef USE_API_USER_DEFINED_ACTIONS - virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; + void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif #ifdef USE_CAMERA - virtual void on_camera_image_request(const CameraImageRequest &value){}; + void on_camera_image_request(const CameraImageRequest &value){}; #endif #ifdef USE_CLIMATE - virtual void on_climate_command_request(const ClimateCommandRequest &value){}; + void on_climate_command_request(const ClimateCommandRequest &value){}; #endif #ifdef USE_WATER_HEATER - virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; + void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; #endif #ifdef USE_NUMBER - virtual void on_number_command_request(const NumberCommandRequest &value){}; + void on_number_command_request(const NumberCommandRequest &value){}; #endif #ifdef USE_SELECT - virtual void on_select_command_request(const SelectCommandRequest &value){}; + void on_select_command_request(const SelectCommandRequest &value){}; #endif #ifdef USE_SIREN - virtual void on_siren_command_request(const SirenCommandRequest &value){}; + void on_siren_command_request(const SirenCommandRequest &value){}; #endif #ifdef USE_LOCK - virtual void on_lock_command_request(const LockCommandRequest &value){}; + void on_lock_command_request(const LockCommandRequest &value){}; #endif #ifdef USE_BUTTON - virtual void on_button_command_request(const ButtonCommandRequest &value){}; + void on_button_command_request(const ButtonCommandRequest &value){}; #endif #ifdef USE_MEDIA_PLAYER - virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; + void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &value){}; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; + void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(){}; + void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request(){}; + void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){}; + void on_voice_assistant_response(const VoiceAssistantResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; + void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; #endif #ifdef USE_ALARM_CONTROL_PANEL - virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif #ifdef USE_TEXT - virtual void on_text_command_request(const TextCommandRequest &value){}; + void on_text_command_request(const TextCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATE - virtual void on_date_command_request(const DateCommandRequest &value){}; + void on_date_command_request(const DateCommandRequest &value){}; #endif #ifdef USE_DATETIME_TIME - virtual void on_time_command_request(const TimeCommandRequest &value){}; + void on_time_command_request(const TimeCommandRequest &value){}; #endif #ifdef USE_VALVE - virtual void on_valve_command_request(const ValveCommandRequest &value){}; + void on_valve_command_request(const ValveCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATETIME - virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; + void on_date_time_command_request(const DateTimeCommandRequest &value){}; #endif #ifdef USE_UPDATE - virtual void on_update_command_request(const UpdateCommandRequest &value){}; + void on_update_command_request(const UpdateCommandRequest &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; + void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif #ifdef USE_IR_RF - virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; + void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif - - protected: - void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index cd229157030..a1d825ead95 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -711,26 +711,7 @@ inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) template<typename T> const char *proto_enum_to_string(T value); -class ProtoService { - public: - protected: - virtual bool is_authenticated() = 0; - virtual bool is_connection_setup() = 0; - virtual void on_fatal_error() = 0; - virtual void on_no_setup_connection() = 0; - virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Authentication helper methods - inline bool check_connection_setup_() { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return false; - } - return true; - } - - inline bool check_authenticated_() { return this->check_connection_setup_(); } -}; +// ProtoService removed — its methods were inlined into APIConnection. +// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary. } // namespace esphome::api diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index aca31e49c8e..221ccc8d69a 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2608,7 +2608,7 @@ def build_service_message_type( is_empty = not has_fields if is_empty: EMPTY_MESSAGES.add(mt.name) - hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" + hout += f"void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" case = "" if not is_empty: case += f"{mt.name} msg;\n" @@ -2960,6 +2960,7 @@ namespace esphome::api { cpp = FILE_HEADER cpp += """\ #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -2970,7 +2971,7 @@ static const char *const TAG = "api.service"; class_name = "APIServerConnectionBase" - hpp += f"class {class_name} : public ProtoService {{\n" + hpp += f"class {class_name} {{\n" hpp += " public:\n" # Add logging helper method declarations @@ -3063,11 +3064,11 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - # Generate read_message with auth check before dispatch - hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + # Generate read_message_ as APIConnection method (not base class) so the compiler + # can devirtualize and inline the on_* handler calls within the same class. + # APIConnection declares this method in api_connection.h. - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" + out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" # Auth check block before dispatch switch out += " // Check authentication/connection requirements\n" From 5560c9eef78e2a7be9fc5d728e767c34a23630c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 22 Mar 2026 21:10:51 -1000 Subject: [PATCH 0935/2056] [test] Fix flakey ld2412 integration test race condition (#15100) --- tests/integration/test_uart_mock_ld2412.py | 33 +++++++++------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index 9b928ef14f2..12aa3f8397c 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -79,9 +79,15 @@ async def test_uart_mock_ld2412( ], ) - # Signal when we see recovery frame values + # Signal when we see all recovery frame values recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["moving_distance"] + and pytest.approx(75.0) in collector.sensor_states["still_distance"] + and pytest.approx(100.0) in collector.sensor_states["moving_energy"] + and pytest.approx(80.0) in collector.sensor_states["still_energy"] + and pytest.approx(50.0) in collector.sensor_states["detection_distance"] + ) ) async with ( @@ -150,23 +156,12 @@ async def test_uart_mock_ld2412( ) # Recovery frame: moving=50, still=75, energy=100/80, detect=50 - recovery_idx = next( - i - for i, v in enumerate(collector.sensor_states["moving_distance"]) - if v == pytest.approx(50.0) - ) - assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( - 75.0 - ) - assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( - 100.0 - ) - assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( - 80.0 - ) - assert collector.sensor_states["detection_distance"][ - recovery_idx - ] == pytest.approx(50.0) + # Check values exist (waiter already ensured all are present) + assert pytest.approx(50.0) in collector.sensor_states["moving_distance"] + assert pytest.approx(75.0) in collector.sensor_states["still_distance"] + assert pytest.approx(100.0) in collector.sensor_states["moving_energy"] + assert pytest.approx(80.0) in collector.sensor_states["still_energy"] + assert pytest.approx(50.0) in collector.sensor_states["detection_distance"] # Verify binary sensors detected targets (from Phase 1 frame) assert collector.binary_states["has_target"][0] is True From 43879964bd3ceff183ff4e113b6b5f4d05b34d77 Mon Sep 17 00:00:00 2001 From: Simone Rossetto <simros85@gmail.com> Date: Mon, 23 Mar 2026 10:03:19 +0100 Subject: [PATCH 0936/2056] [wireguard] bump esp_wireguard to 0.4.4 for mbedtls 4.0+ compatibility (#15104) Co-authored-by: J. Nick Koston <nick@koston.org> --- .clang-tidy.hash | 2 +- esphome/components/wireguard/__init__.py | 2 +- platformio.ini | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index c32978d4111..5c7eab517bd 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6 +f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6 diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index e2ea61a5428..1b543913766 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -137,7 +137,7 @@ async def to_code(config): # the '+1' modifier is relative to the device's own address that will # be automatically added to the provided list. cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}") - cg.add_library("droscy/esp_wireguard", "0.4.2") + cg.add_library("droscy/esp_wireguard", "0.4.4") await cg.register_component(var, config) diff --git a/platformio.ini b/platformio.ini index d3a482b652b..e0f7c7d443a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = @@ -154,7 +154,7 @@ lib_deps = DNSServer ; captive_portal (Arduino built-in) makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = @@ -176,7 +176,7 @@ platform_packages = framework = espidf lib_deps = ${common:idf.lib_deps} - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word tonia/HeatpumpIR@1.0.40 ; heatpumpir build_flags = @@ -221,7 +221,7 @@ lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base - droscy/esp_wireguard@0.4.2 ; wireguard + droscy/esp_wireguard@0.4.4 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} From 5a984b54cfce79fbd6ad93365cf8ccc148942d6f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 23 Mar 2026 07:31:05 -0500 Subject: [PATCH 0937/2056] [audio] Bump microOpus to avoid creating an extra opus-staged directory (#14974) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- script/analyze_component_buses.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index b28c2ed3d8c..9cc80b9b33a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -214,4 +214,4 @@ async def to_code(config): cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.5") + add_idf_component(name="esphome/micro-opus", ref="0.3.6") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 5cfa8532ff5..4148147a3b7 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/esp-audio-libs: version: 2.0.3 esphome/micro-opus: - version: 0.3.5 + version: 0.3.6 espressif/esp-dsp: version: "1.7.1" espressif/esp-tflite-micro: diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 427602dff24..17af7af5771 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -83,6 +83,7 @@ ISOLATED_COMPONENTS = { "openthread": "Conflicts with wifi: used by most components", "openthread_info": "Conflicts with wifi: used by most components", "matrix_keypad": "Needs isolation due to keypad", + "microphone": "Defines PDM microphone requiring I2S port 0 - conflicts with micro_wake_word PDM mic when merged", "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", From e8c5dfca3ecc959f17722bdafb13471ad4e3f1a0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:09:30 +1000 Subject: [PATCH 0938/2056] [lvgl] Various fixes (#15098) --- esphome/components/lvgl/__init__.py | 8 +++++++- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/textarea.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c37a32eccab..a6afa12afa4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -226,6 +226,9 @@ async def to_code(configs): config_0 = configs[0] # Global configuration if CORE.is_esp32: + # Skip compiling lvgl examples + add_idf_sdkconfig_option("CONFIG_LV_BUILD_EXAMPLES", False) + add_idf_sdkconfig_option("CONFIG_LV_BUILD_DEMOS", False) if get_esp32_variant() == VARIANT_ESP32P4: add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64) # disable use of PPA for fills until upstream bugs fixed @@ -406,7 +409,10 @@ async def to_code(configs): lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") - cg.add_build_flag(f'-DLV_CONF_PATH=\\"{LV_CONF_FILENAME}\\"') + # handle windows paths in a way that doesn't break the generated C++ + lv_conf_h_path = Path(lv_conf_h_file).as_posix() + cg.add_build_flag(f'-DLV_CONF_PATH=\\"{lv_conf_h_path}\\"') + cg.add_build_flag("-DLV_KCONFIG_IGNORE") for prop in df.get_remapped_uses(): df.LOGGER.warning( diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8e34f16c98f..66f823d549e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -71,7 +71,7 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#ifdef USE_IMAGE +#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index e5ab8846859..baa40ee2c6e 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -16,6 +16,7 @@ from ..lv_validation import lv_bool, lv_int, lv_text from ..schemas import TEXT_SCHEMA from ..types import LvText from . import Widget, WidgetType +from .label import CONF_LABEL CONF_TEXTAREA = "textarea" @@ -46,6 +47,9 @@ class TextareaType(WidgetType): TEXTAREA_SCHEMA, ) + def get_uses(self): + return (CONF_LABEL,) + async def to_code(self, w: Widget, config: dict): for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): if (value := config.get(prop)) is not None: From 3b5b51b4f0f918fef64e4996dcf0a551389afc4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:22:25 -1000 Subject: [PATCH 0939/2056] [time] Point to valid IANA timezone list on validation failure (#15110) --- esphome/components/time/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 9821046a737..c31ccbc7eac 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -284,13 +284,23 @@ def validate_tz(value: str) -> str: tzfile = _load_tzdata(value) if tzfile is not None: value = _extract_tz_string(tzfile) + is_iana = True + else: + is_iana = False # Validate that the POSIX TZ string is parseable (skip empty strings) if value: try: parse_posix_tz_python(value) except ValueError as e: - raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + if is_iana: + raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + raise cv.Invalid( + f"Invalid POSIX timezone string '{value}': {e}. " + f"If you meant to use an IANA timezone, check the list of valid " + f"timezones at " + f"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + ) from e return value From 03d6b36fe03176ea2bca019f701f8a706dc3f3c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:22:38 -1000 Subject: [PATCH 0940/2056] [gpio] Compile out interlock fields when unused (#15111) --- esphome/components/gpio/switch/__init__.py | 1 + esphome/components/gpio/switch/gpio_switch.cpp | 9 +++++++++ esphome/components/gpio/switch/gpio_switch.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 15 insertions(+) diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 604de6d8092..9462cd01615 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -32,6 +32,7 @@ async def to_code(config): cg.add(var.set_pin(pin)) if CONF_INTERLOCK in config: + cg.add_define("USE_GPIO_SWITCH_INTERLOCK") interlock = [] for it in config[CONF_INTERLOCK]: lock = await cg.get_variable(it) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d461fab051d..9c6464815a0 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,7 +5,9 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +#ifdef USE_GPIO_SWITCH_INTERLOCK static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; +#endif float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -28,6 +30,7 @@ void GPIOSwitch::setup() { void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); LOG_PIN(" Pin: ", this->pin_); +#ifdef USE_GPIO_SWITCH_INTERLOCK if (!this->interlock_.empty()) { ESP_LOGCONFIG(TAG, " Interlocks:"); for (auto *lock : this->interlock_) { @@ -36,8 +39,10 @@ void GPIOSwitch::dump_config() { ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str()); } } +#endif } void GPIOSwitch::write_state(bool state) { +#ifdef USE_GPIO_SWITCH_INTERLOCK if (state != this->inverted_) { // Turning ON, check interlocking @@ -64,11 +69,15 @@ void GPIOSwitch::write_state(bool state) { // re-activations this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } +#endif this->pin_->digital_write(state); this->publish_state(state); } + +#ifdef USE_GPIO_SWITCH_INTERLOCK void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } +#endif } // namespace gpio } // namespace esphome diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index a73fb9e18cc..f7415d1dba1 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -18,15 +18,19 @@ class GPIOSwitch final : public switch_::Switch, public Component { void setup() override; void dump_config() override; +#ifdef USE_GPIO_SWITCH_INTERLOCK void set_interlock(const std::initializer_list<Switch *> &interlock); void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } +#endif protected: void write_state(bool state) override; GPIOPin *pin_; +#ifdef USE_GPIO_SWITCH_INTERLOCK FixedVector<Switch *> interlock_; uint32_t interlock_wait_time_{0}; +#endif }; } // namespace gpio diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d94b7e9f5d2..f437e30a955 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -53,6 +53,7 @@ #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN +#define USE_GPIO_SWITCH_INTERLOCK #define USE_GRAPH #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME From 36d2e58b11836420fff50447c9c2a9ee70cdf432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:23:08 -1000 Subject: [PATCH 0941/2056] [api] Make ProtoDecodableMessage::decode() non-virtual (#15076) --- esphome/components/api/api_pb2.h | 4 ++-- esphome/components/api/proto.h | 22 +++++++--------------- script/api_protobuf/api_protobuf.py | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a4ee0adb8b5..86289a28d6e 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1253,7 +1253,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { FixedVector<int32_t> int_array{}; FixedVector<float> float_array{}; FixedVector<std::string> string_array{}; - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1278,7 +1278,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES bool return_response{false}; #endif - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a1d825ead95..d6f9c947d71 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -152,8 +152,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message and related encoding helpers -class ProtoDecodableMessage; +// Forward declarations for encoding helpers class ProtoMessage; class ProtoSize; @@ -166,16 +165,9 @@ class ProtoLengthDelimited { const uint8_t *data() const { return this->value_; } size_t size() const { return this->length_; } - /** - * Decode the length-delimited data into an existing ProtoDecodableMessage instance. - * - * This method allows decoding without templates, enabling use in contexts - * where the message type is not known at compile time. The ProtoDecodableMessage's - * decode() method will be called with the raw data and length. - * - * @param msg The ProtoDecodableMessage instance to decode into - */ - void decode_to_message(ProtoDecodableMessage &msg) const; + /// Decode the length-delimited data into a message instance. + /// Template preserves concrete type so decode() resolves statically. + template<typename T> void decode_to_message(T &msg) const; protected: const uint8_t *const value_; @@ -468,7 +460,7 @@ class ProtoMessage { // Base class for messages that support decoding class ProtoDecodableMessage : public ProtoMessage { public: - virtual void decode(const uint8_t *buffer, size_t length); + void decode(const uint8_t *buffer, size_t length); /** * Count occurrences of a repeated field in a protobuf buffer. @@ -704,8 +696,8 @@ template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(u this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>); } -// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined -inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { +// Template decode_to_message - preserves concrete type so decode() resolves statically +template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const { msg.decode(this->value_, this->length_); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 221ccc8d69a..0bb569fdb54 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2275,7 +2275,7 @@ def build_message_type( o += "}\n" cpp += o # Generate the decode() declaration in header (public method) - prot = "void decode(const uint8_t *buffer, size_t length) override;" + prot = "void decode(const uint8_t *buffer, size_t length);" public_content.append(prot) # Only generate encode method if this message needs encoding and has fields From 9385f1612820541019f3685810d07f352b9b5ee1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:23:22 -1000 Subject: [PATCH 0942/2056] [text_sensor] Guard raw_callback_ behind USE_TEXT_SENSOR_FILTER, save 4 bytes per instance (#15097) --- esphome/components/text_sensor/text_sensor.cpp | 2 ++ esphome/components/text_sensor/text_sensor.h | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index aa49a85d265..0dc29f9a942 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -31,7 +31,9 @@ void TextSensor::publish_state(const char *state, size_t len) { if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { this->state.assign(state, len); } +#ifdef USE_TEXT_SENSOR_FILTER this->raw_callback_.call(this->state); +#endif ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); this->notify_frontend_(); #ifdef USE_TEXT_SENSOR_FILTER diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 8941790e7cc..3f69e91c8d4 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -64,8 +64,14 @@ class TextSensor : public EntityBase { template<typename F> void add_on_state_callback(F &&callback) { this->callback_.add(std::forward<F>(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. + /// When USE_TEXT_SENSOR_FILTER is not enabled, delegates to the regular callback + /// since raw state equals filtered state without filter support compiled in. template<typename F> void add_on_raw_state_callback(F &&callback) { +#ifdef USE_TEXT_SENSOR_FILTER this->raw_callback_.add(std::forward<F>(callback)); +#else + this->callback_.add(std::forward<F>(callback)); +#endif } // ========== INTERNAL METHODS ========== @@ -77,8 +83,10 @@ class TextSensor : public EntityBase { protected: /// Notify frontend that state has changed (assumes this->state is already set) void notify_frontend_(); +#ifdef USE_TEXT_SENSOR_FILTER LazyCallbackManager<void(const std::string &)> raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager<void(const std::string &)> callback_; ///< Storage for filtered state callbacks. +#endif + LazyCallbackManager<void(const std::string &)> callback_; ///< Storage for filtered state callbacks. #ifdef USE_TEXT_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. From 4b0c711f7743495db05f97168d09bf991f60e204 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:23:35 -1000 Subject: [PATCH 0943/2056] [ci] Ban std::bind in new C++ code (#14969) --- script/ci-custom.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 06fcdadb8ce..1da923b095a 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -890,6 +890,22 @@ def lint_no_powf_in_core(fname, match): ) +@lint_re_check( + r"[^\w]std\s*::\s*bind\s*\(" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_std_bind(fname, match): + return ( + f"{highlight('std::bind()')} is not allowed in new ESPHome code. " + f"Lambdas are clearer, produce smaller binaries, and are more likely to fit within " + f"the {highlight('std::function')} small-buffer optimization (avoiding heap allocation).\n" + f"Please use a lambda instead.\n" + f" Before: {highlight('std::bind(&Class::method, this, std::placeholders::_1)')}\n" + f" After: {highlight('[this](auto arg) { this->method(arg); }')}\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL) LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])') LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)') From 9da0c5bc857b13a1a6387670a19629bcd5ab20fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:47:15 -1000 Subject: [PATCH 0944/2056] [wifi] Fix roaming attempt counter reset on disconnect during scan (#15099) --- esphome/components/wifi/wifi_component.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0fd13852581..e1d4b07471b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -269,11 +269,11 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ │ /// │ ┌──────────────┼──────────────┐ │ /// │ ↓ ↓ ↓ │ -/// │ scan error no better AP +10 dB better AP │ +/// │ disconnect no better AP +10 dB better AP │ /// │ │ │ │ │ /// │ ↓ ↓ ↓ │ /// │ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ -/// │ │ → IDLE │ │ CONNECTING │ │ +/// │ │ → RECONNECTING │ │ CONNECTING │ │ /// │ │ (counter preserved) │ │ (process_roaming_scan_) │ │ /// │ └──────────────────────────────┘ └────────────┬─────────────┘ │ /// │ │ │ @@ -296,7 +296,7 @@ bool CompactString::operator==(const StringRef &other) const { /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Scan error (SCANNING→IDLE): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ @@ -2068,9 +2068,10 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::SCANNING) { - // Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter - ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); - this->roaming_state_ = RoamingState::IDLE; + // Disconnected during roam scan - transition to RECONNECTING so the attempts + // counter is preserved when reconnection succeeds (IDLE would reset it) + ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { // Not a roaming-triggered reconnect, reset state this->clear_roaming_state_(); From 4c1363b104f198aa837a11bd1ba1ecad28371ec4 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:07:40 -0400 Subject: [PATCH 0945/2056] [spi] Add LOG_SPI_DEVICE macro (#15118) --- esphome/components/spi/spi.h | 2 ++ script/ci-custom.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index e237cf44f45..84c8bca2671 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -34,6 +34,8 @@ using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) */ namespace esphome::spi { +#define LOG_SPI_DEVICE(this) ESP_LOGCONFIG(TAG, " CS Pin: %d", esphome::spi::Utility::get_pin_no(this->cs_)); + /// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. enum SPIBitOrder { /// The least significant bit is transmitted/received first. diff --git a/script/ci-custom.py b/script/ci-custom.py index 1da923b095a..25a0cf21277 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -963,6 +963,7 @@ def lint_log_multiline_continuation(fname, content): "esphome/components/nextion/nextion_base.h", "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", + "esphome/components/spi/spi.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", "esphome/components/text/text.h", From 1e16b30380a4703142318a5a1a93f6ad3626f6f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 09:18:58 -1000 Subject: [PATCH 0946/2056] [ethernet] Add ENC28J60 SPI Ethernet support (#14945) --- esphome/components/ethernet/__init__.py | 44 ++++++++++++++----- .../components/ethernet/ethernet_component.h | 13 ++++++ .../ethernet/ethernet_component_esp32.cpp | 41 +++++++++++------ .../ethernet/ethernet_component_rp2040.cpp | 27 +++++++++--- .../ethernet/common-enc28j60-rp2040.yaml | 18 ++++++++ .../components/ethernet/common-enc28j60.yaml | 19 ++++++++ .../ethernet/test-enc28j60.esp32-idf.yaml | 1 + .../ethernet/test-enc28j60.rp2040-ard.yaml | 1 + 8 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 tests/components/ethernet/common-enc28j60-rp2040.yaml create mode 100644 tests/components/ethernet/common-enc28j60.yaml create mode 100644 tests/components/ethernet/test-enc28j60.esp32-idf.yaml create mode 100644 tests/components/ethernet/test-enc28j60.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index f519d79aa19..e17abfcc931 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -119,6 +119,7 @@ ETHERNET_TYPES = { "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, + "ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60, } # PHY types that need compile-time defines for conditional compilation @@ -134,6 +135,7 @@ _PHY_TYPE_TO_DEFINE = { "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", + "ENC28J60": "USE_ETHERNET_ENC28J60", } @@ -155,11 +157,16 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { "KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), "W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"), "DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"), + "ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"), + "LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"), } -SPI_ETHERNET_TYPES = ["W5500", "DM9051"] +# These types are always external IDF components (never built-in to ESP-IDF) +_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} + +SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500"] +RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -220,7 +227,18 @@ def _validate(config): if CORE.is_esp32: if config[CONF_TYPE] in SPI_ETHERNET_TYPES: - if _is_framework_spi_polling_mode_supported(): + # ENC28J60 driver does not support polling mode - interrupt is required + if config[CONF_TYPE] == "ENC28J60": + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. " + f"'{CONF_INTERRUPT_PIN}' is required." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60." + ) + elif _is_framework_spi_polling_mode_supported(): if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: raise cv.Invalid( f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" @@ -367,6 +385,7 @@ CONFIG_SCHEMA = cv.All( "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, + "ENC28J60": SPI_SCHEMA, "LAN8670": RMII_SCHEMA, }, upper=True, @@ -502,7 +521,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 - if idf_version() < cv.Version(6, 0, 0): + # ENC28J60 was never built-in to IDF, so it has no Kconfig option + if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60": add_idf_sdkconfig_option( f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True ) @@ -533,12 +553,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) include_builtin_idf_component("esp_eth") - if config[CONF_TYPE] == "LAN8670": - # Add LAN867x 10BASE-T1S PHY support component - add_idf_component(name="espressif/lan867x", ref="2.0.0") - - # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry - if idf_version() >= cv.Version(6, 0, 0) and ( + if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS: + component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]] + add_idf_component(name=component.name, ref=component.version) + elif idf_version() >= cv.Version(6, 0, 0) and ( + # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE]) ): add_idf_component(name=component.name, ref=component.version) @@ -555,7 +574,10 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - cg.add_library("lwIP_w5500", None) + if config[CONF_TYPE] == "ENC28J60": + cg.add_library("lwIP_enc28j60", None) + else: + cg.add_library("lwIP_w5500", None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b6699e80204..4c85c39eb86 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -23,7 +23,13 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif // USE_ESP32 #ifdef USE_RP2040 +#if defined(USE_ETHERNET_W5500) #include <W5500lwIP.h> +#elif defined(USE_ETHERNET_ENC28J60) +#include <ENC28J60lwIP.h> +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif #endif namespace esphome::ethernet { @@ -57,6 +63,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, ETHERNET_TYPE_LAN8670, + ETHERNET_TYPE_ENC28J60, }; struct ManualIP { @@ -215,7 +222,13 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_ENC28J60) + ENC28J60lwIP *eth_{nullptr}; +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif uint32_t last_link_check_{0}; uint8_t clk_pin_; uint8_t miso_pin_; diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index fb69a901aac..a170239e032 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -44,6 +44,11 @@ #include "esp_eth_phy_lan867x.h" #endif +// ENC28J60 header exists on all IDF versions (always an external component) +#ifdef USE_ETHERNET_ENC28J60 +#include "esp_eth_enc28j60.h" +#endif + #ifdef USE_ETHERNET_SPI #include <driver/gpio.h> #include <driver/spi_master.h> @@ -194,25 +199,27 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#elif defined(USE_ETHERNET_ENC28J60) + eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg); #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif -#endif - -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) dm9051_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT dm9051_config.poll_period_ms = this->polling_interval_; #endif +#elif defined(USE_ETHERNET_ENC28J60) + enc28j60_config.int_gpio_num = this->interrupt_pin_; + // ENC28J60 does not support poll_period_ms #endif phy_config.phy_addr = this->phy_addr_spi_; @@ -300,19 +307,24 @@ void EthernetComponent::setup() { #endif #endif #ifdef USE_ETHERNET_SPI -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: { mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: { mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); this->phy_ = esp_eth_phy_new_dm9051(&phy_config); break; } +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: { + mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config); + this->phy_ = esp_eth_phy_new_enc28j60(&phy_config); + break; + } #endif #endif default: { @@ -405,15 +417,18 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081RNA"; break; #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: + eth_type = "ENC28J60"; + break; #endif #ifdef USE_ETHERNET_OPENETH case ETHERNET_TYPE_OPENETH: diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index 77b1a22d66c..bd8c4589851 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,11 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for W5500 to initialize after reset + delay(10); // NOLINT - wait for chip to initialize after reset } - // Create the W5500 device instance + // Create the SPI Ethernet device instance +#if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_ENC28J60) + this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#endif // Set hostname before begin() so the LWIP netif gets it this->eth_->hostname(App.get_name().c_str()); @@ -61,7 +65,7 @@ void EthernetComponent::setup() { } if (!success) { - ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet"); + ESP_LOGE(TAG, "Failed to initialize Ethernet"); delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory) this->eth_ = nullptr; this->mark_failed(); @@ -164,9 +168,15 @@ void EthernetComponent::loop() { } void EthernetComponent::dump_config() { + const char *type_str = "Unknown"; +#if defined(USE_ETHERNET_W5500) + type_str = "W5500"; +#elif defined(USE_ETHERNET_ENC28J60) + type_str = "ENC28J60"; +#endif ESP_LOGCONFIG(TAG, "Ethernet:\n" - " Type: W5500\n" + " Type: %s\n" " Connected: %s\n" " CLK Pin: %u\n" " MISO Pin: %u\n" @@ -174,7 +184,7 @@ void EthernetComponent::dump_config() { " CS Pin: %u\n" " IRQ Pin: %d\n" " Reset Pin: %d", - YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, + type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, this->interrupt_pin_, this->reset_pin_); this->dump_connect_params_(); } @@ -216,13 +226,18 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // W5500 is always full duplex + // Both W5500 and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } eth_speed_t EthernetComponent::get_link_speed() { +#ifdef USE_ETHERNET_ENC28J60 + // ENC28J60 is 10Mbps only + return ETH_SPEED_10M; +#else // W5500 is always 100Mbps return ETH_SPEED_100M; +#endif } bool EthernetComponent::powerdown() { diff --git a/tests/components/ethernet/common-enc28j60-rp2040.yaml b/tests/components/ethernet/common-enc28j60-rp2040.yaml new file mode 100644 index 00000000000..0718723d8af --- /dev/null +++ b/tests/components/ethernet/common-enc28j60-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: ENC28J60 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-enc28j60.yaml b/tests/components/ethernet/common-enc28j60.yaml new file mode 100644 index 00000000000..6b288bed19c --- /dev/null +++ b/tests/components/ethernet/common-enc28j60.yaml @@ -0,0 +1,19 @@ +ethernet: + type: ENC28J60 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-enc28j60.esp32-idf.yaml b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml new file mode 100644 index 00000000000..b7c1b7f5aa4 --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60.yaml diff --git a/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml new file mode 100644 index 00000000000..61d1cd86f5f --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60-rp2040.yaml From 11b829dda17b8cca2174272069caf36d0dca2628 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:59:17 -0400 Subject: [PATCH 0947/2056] [spa06_spi] Add SPA06-003 Temperature and Pressure Sensor - SPI support (Part 3 of 3) (#14523) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_spi/__init__.py | 0 esphome/components/spa06_spi/sensor.py | 41 +++++++++++ esphome/components/spa06_spi/spa06_spi.cpp | 72 +++++++++++++++++++ esphome/components/spa06_spi/spa06_spi.h | 22 ++++++ tests/components/spa06_spi/common.yaml | 15 ++++ .../components/spa06_spi/test.esp32-idf.yaml | 7 ++ .../spa06_spi/test.esp8266-ard.yaml | 7 ++ .../components/spa06_spi/test.rp2040-ard.yaml | 7 ++ 9 files changed, 172 insertions(+) create mode 100644 esphome/components/spa06_spi/__init__.py create mode 100644 esphome/components/spa06_spi/sensor.py create mode 100644 esphome/components/spa06_spi/spa06_spi.cpp create mode 100644 esphome/components/spa06_spi/spa06_spi.h create mode 100644 tests/components/spa06_spi/common.yaml create mode 100644 tests/components/spa06_spi/test.esp32-idf.yaml create mode 100644 tests/components/spa06_spi/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e3e09cbc113..afe4cdb8712 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -459,6 +459,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net +esphome/components/spa06_spi/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_spi/__init__.py b/esphome/components/spa06_spi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/spa06_spi/sensor.py b/esphome/components/spa06_spi/sensor.py new file mode 100644 index 00000000000..b82186ec21e --- /dev/null +++ b/esphome/components/spa06_spi/sensor.py @@ -0,0 +1,41 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["spi"] + +spa06_ns = cg.esphome_ns.namespace("spa06_spi") +SPA06SPIComponent = spa06_ns.class_( + "SPA06SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = {3: "MODE3", "3": "MODE3", "MODE3": "MODE3"} + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("SPA06 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(SPA06SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/spa06_spi/spa06_spi.cpp b/esphome/components/spa06_spi/spa06_spi.cpp new file mode 100644 index 00000000000..9f3683d2197 --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.cpp @@ -0,0 +1,72 @@ +#include <cstdint> +#include <cstddef> + +#include "spa06_spi.h" +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +// OR (|) register with SPA06_SPI_READ for read. +inline constexpr uint8_t SPA06_SPI_READ = 0x80; + +// AND (&) register with SPA06_SPI_WRITE for write. +inline constexpr uint8_t SPA06_SPI_WRITE = 0x7F; + +namespace esphome::spa06_spi { + +static const char *const TAG = "spa06_spi"; + +void SPA06SPIComponent::dump_config() { + SPA06Component::dump_config(); + LOG_SPI_DEVICE(this) +} + +void SPA06SPIComponent::setup() { + this->spi_setup(); + SPA06Component::setup(); +} + +void SPA06SPIComponent::protocol_reset() { + // Forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->spa_read_byte(spa06_base::SPA06_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address +// is not used and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, +// the byte 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions SPA06_SPI_READ (| with register) and SPA06_SPI_WRITE (& with register) +// are defined for readability. + +bool SPA06SPIComponent::spa_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::spa06_spi diff --git a/esphome/components/spa06_spi/spa06_spi.h b/esphome/components/spa06_spi/spa06_spi.h new file mode 100644 index 00000000000..ffbc162d6fe --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::spa06_spi { + +class SPA06SPIComponent : public spa06_base::SPA06Component, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, + spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> { + void setup() override; + bool spa_read_byte(uint8_t a_register, uint8_t *data) override; + bool spa_write_byte(uint8_t a_register, uint8_t data) override; + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void protocol_reset() override; +}; + +} // namespace esphome::spa06_spi diff --git a/tests/components/spa06_spi/common.yaml b/tests/components/spa06_spi/common.yaml new file mode 100644 index 00000000000..9263202ab40 --- /dev/null +++ b/tests/components/spa06_spi/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_spi + spi_id: spi_bus + cs_pin: ${cs_pin} + temperature: + id: spa06_spi_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_spi_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_spi/test.esp32-idf.yaml b/tests/components/spa06_spi/test.esp32-idf.yaml new file mode 100644 index 00000000000..a3352cf880d --- /dev/null +++ b/tests/components/spa06_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.esp8266-ard.yaml b/tests/components/spa06_spi/test.esp8266-ard.yaml new file mode 100644 index 00000000000..595f31046ab --- /dev/null +++ b/tests/components/spa06_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.rp2040-ard.yaml b/tests/components/spa06_spi/test.rp2040-ard.yaml new file mode 100644 index 00000000000..93e19cfea4a --- /dev/null +++ b/tests/components/spa06_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO17 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + +<<: !include common.yaml From 6956bf7e539589ee06282d8560b97f91989397c3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:24:25 +1000 Subject: [PATCH 0948/2056] [text] Add text_sensor for read-only view of text component (#15090) --- .../components/text/text_sensor/__init__.py | 25 +++++++++++++++++++ .../text/text_sensor/text_text_sensor.cpp | 16 ++++++++++++ .../text/text_sensor/text_text_sensor.h | 19 ++++++++++++++ tests/components/text/common.yaml | 5 ++++ 4 files changed, 65 insertions(+) create mode 100644 esphome/components/text/text_sensor/__init__.py create mode 100644 esphome/components/text/text_sensor/text_text_sensor.cpp create mode 100644 esphome/components/text/text_sensor/text_text_sensor.h diff --git a/esphome/components/text/text_sensor/__init__.py b/esphome/components/text/text_sensor/__init__.py new file mode 100644 index 00000000000..5e45f101939 --- /dev/null +++ b/esphome/components/text/text_sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Text, text_ns + +TextTextSensor = text_ns.class_("TextTextSensor", text_sensor.TextSensor, cg.Component) + + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(TextTextSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Text), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await text_sensor.new_text_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/text/text_sensor/text_text_sensor.cpp b/esphome/components/text/text_sensor/text_text_sensor.cpp new file mode 100644 index 00000000000..50504c605ea --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.cpp @@ -0,0 +1,16 @@ +#include "text_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::text { + +static const char *const TAG = "text.text_sensor"; + +void TextTextSensor::setup() { + this->source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void TextTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Text Text Sensor", this); } + +} // namespace esphome::text diff --git a/esphome/components/text/text_sensor/text_text_sensor.h b/esphome/components/text/text_sensor/text_text_sensor.h new file mode 100644 index 00000000000..fd70ea3451a --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../text.h" +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text { + +class TextTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit TextTextSensor(Text *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Text *source_; +}; + +} // namespace esphome::text diff --git a/tests/components/text/common.yaml b/tests/components/text/common.yaml index 26618be03a7..561d17143f4 100644 --- a/tests/components/text/common.yaml +++ b/tests/components/text/common.yaml @@ -23,3 +23,8 @@ text: min_length: 8 max_length: 32 mode: password + +text_sensor: + - platform: text + name: "Test Text State" + source_id: test_text From 332118db5644b161c09c90f64e07f2289d6f86a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:44:26 -1000 Subject: [PATCH 0949/2056] Bump pytest-cov from 7.0.0 to 7.1.0 (#15123) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0d3f0671f54..1440b20333b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==9.0.2 -pytest-cov==7.0.0 +pytest-cov==7.1.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 pytest-xdist==3.8.0 From bf6000ef3d38bf663d4fc2d69a220d5a64889e78 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:50:28 +0100 Subject: [PATCH 0950/2056] [substitutions] substitutions pass and !include redesign (package refactor part 2b) (#14918) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 444 +++++++++++++----- esphome/components/substitutions/jinja.py | 94 +--- esphome/config.py | 30 +- esphome/yaml_util.py | 41 +- .../component_tests/packages/test_packages.py | 2 + .../substitutions/00-simple_var.approved.yaml | 17 + .../substitutions/00-simple_var.input.yaml | 10 + .../02-expressions.approved.yaml | 6 + .../substitutions/02-expressions.input.yaml | 6 + .../07-package_merging.approved.yaml | 46 ++ .../07-package_merging.input.yaml | 63 +++ ...-include_vars_without_substs.approved.yaml | 5 + .../09-include_vars_without_substs.input.yaml | 7 + tests/unit_tests/test_substitutions.py | 244 +++++++++- tests/unit_tests/test_yaml_util.py | 2 +- 16 files changed, 753 insertions(+), 273 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 6d353ccf119..793cb946dd4 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -226,7 +226,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - new_yaml = yaml_util.substitute_vars(new_yaml, vars) + new_yaml = yaml_util.add_context(new_yaml, vars or None) packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: raise cv.Invalid( @@ -296,6 +296,13 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def process_package_callback(package_config: dict) -> dict: """This will be called for each package found in the config.""" + if isinstance(package_config, yaml_util.ConfigContext): + context_vars = package_config.vars + if CONF_PACKAGES in package_config or CONF_URL in package_config: + # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. + from esphome.components.substitutions import substitute_context_vars + + substitute_context_vars(package_config, context_vars) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 7e15f714f73..ecee816ce91 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,31 +1,50 @@ +from collections import ChainMap import logging -from re import Match from typing import Any from esphome import core from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS -from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base +from esphome.types import ConfigType +from esphome.util import OrderedDict +from esphome.yaml_util import ( + ConfigContext, + ESPHomeDataBase, + ESPLiteralValue, + make_data_base, +) -from .jinja import Jinja, JinjaError, JinjaStr, has_jinja +from .jinja import Jinja, JinjaError, Missing, Resolver, UndefinedError, has_jinja CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) +ContextVars = ChainMap[str, Any] +SubstitutionPath = list[int | str] +ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] +# Module-level instance is safe: context_vars is passed per-call, and context_trace +# is stack-saved/restored within expand(). Not thread-safe — only use from one thread. +jinja = Jinja() -def validate_substitution_key(value): + +def validate_substitution_key(value: Any) -> str: + """Validate and normalize a substitution key, stripping a leading ``$`` if present.""" value = cv.string(value) if not value: raise cv.Invalid("Substitution key must not be empty") if value[0] == "$": value = value[1:] + if not value: + raise cv.Invalid("Substitution key must not be empty") if value[0].isdigit(): raise cv.Invalid("First character in substitutions cannot be a digit.") for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - f"Substitution must only consist of upper/lowercase characters, the underscore and numbers. The character '{char}' cannot be used" + f"Substitution must only consist of upper/lowercase characters," + f" the underscore and numbers." + f" The character '{char}' cannot be used" ) return value @@ -37,8 +56,8 @@ CONFIG_SCHEMA = cv.Schema( ) -async def to_code(config): - pass +async def to_code(config: ConfigType) -> None: + """No runtime code generation needed — substitutions are resolved at config time.""" def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: @@ -62,91 +81,122 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _expand_jinja( - value: str | JinjaStr, - orig_value: str | JinjaStr, - path, - jinja: Jinja, - ignore_missing: bool, -) -> Any: - if has_jinja(value): - # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved - # Jinja expression from a previous pass. - if isinstance(orig_value, JinjaStr): - # Rebuild the JinjaStr in case it was lost while replacing substitutions. - value = JinjaStr(value, orig_value.upvalues) - try: - # Invoke the jinja engine to evaluate the expression. - value, err = jinja.expand(value) - if err is not None and not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like an expression," - " but could not resolve all the variables: %s", - value, - "->".join(str(x) for x in path), - err.message, - ) - except JinjaError as err: - raise cv.Invalid( - f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}." - f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}" - f"\nRelevant context:\n{err.context_trace_str()}" - f"\nSee {'->'.join(str(x) for x in path)}", - path, - ) - # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), - # assign this same document metadata to the resulting value. - if isinstance(orig_value, ESPHomeDataBase): - value = _restore_data_base(value, orig_value) +def _try_substitute(value: Any, context: ContextVars) -> Any: + """Substitute variables in value, returning the result or the original if unchanged.""" + result = _substitute_item(value, [], context, strict_undefined=True) + return result if result is not None else value - return value + +def _resolve_var(name: str, context_vars: ContextVars) -> Any: + """Look up a substitution variable, falling back to the resolver callback.""" + sub = context_vars.get(name, Missing) + if sub is Missing: + resolver = context_vars.get(Resolver) + if resolver: + sub = resolver(name) + return sub + + +def _handle_undefined( + err: UndefinedError, + path: SubstitutionPath, + value: Any, + strict_undefined: bool, + errors: ErrList | None, +) -> None: + """Handle an undefined variable. + + In strict mode, raises immediately. Otherwise, appends to the errors + list for deferred warning at the end of the substitution pass. + """ + if strict_undefined: + raise err + if errors is not None: + errors.append((err, path, value)) def _expand_substitutions( - substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool + value: str, + path: SubstitutionPath, + context_vars: ContextVars, + strict_undefined: bool, + errors: ErrList | None, ) -> Any: + """Expand ``$var``, ``${var}``, and Jinja expressions in a string. + + Works in two phases: + + 1. **Simple substitution** — scan for ``$name`` / ``${name}`` tokens + and replace them with the value from *context_vars*. If the token + spans the entire string, return the raw value (preserving type). + 2. **Jinja evaluation** — if the result still contains Jinja syntax + (e.g. ``${a * b}``), render it through the Jinja engine with the + full *context_vars* as template variables. + + Returns the expanded value (may be a non-string type) or the + original *value* unchanged if there is nothing to substitute. + """ if "$" not in value: return value orig_value = value - i = 0 - while True: - m: Match[str] = cv.VARIABLE_PROG.search(value, i) - if not m: - # No more variable substitutions found. See if the remainder looks like a jinja template - value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) - break - - i, j = m.span(0) + # Phase 1: Replace $var and ${var} references + search_pos = 0 + while (m := cv.VARIABLE_PROG.search(value, search_pos)) is not None: + match_start, match_end = m.span(0) name: str = m.group(1) if name.startswith("{") and name.endswith("}"): name = name[1:-1] - if name not in substitutions: - if not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", - orig_value, - "->".join(str(x) for x in path), - name, - ) - i = j + sub = _resolve_var(name, context_vars) + if sub is Missing: + _handle_undefined( + err=UndefinedError(f"'{name}' is undefined"), + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + search_pos = match_end continue - sub: Any = substitutions[name] - - if i == 0 and j == len(value): - # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly - # to conserve its type. + if match_start == 0 and match_end == len(value): + # The variable spans the whole expression, e.g., "${varName}". + # Return its resolved value directly to conserve its type. value = sub break - tail = value[j:] - value = value[:i] + str(sub) - i = len(value) + tail = value[match_end:] + value = value[:match_start] + str(sub) + search_pos = len(value) value += tail + # Phase 2: Evaluate any remaining jinja expressions (e.g., "${a * b}") + if isinstance(value, str) and has_jinja(value): + try: + value = jinja.expand(value, context_vars) + except UndefinedError as err: + _handle_undefined( + err=err, + path=path, + value=value, + strict_undefined=strict_undefined, + errors=errors, + ) + except JinjaError as err: + raise cv.Invalid( + f"{err.error_name()} Error evaluating jinja expression" + f" '{value}': {str(err.parent())}." + f"\nEvaluation stack: (most recent evaluation last)" + f"\n{err.stack_trace_str()}" + f"\nRelevant context:\n{err.context_trace_str()}" + f"\nSee {'->'.join(str(x) for x in path)}", + path, + ) + else: + if isinstance(orig_value, ESPHomeDataBase): + value = _restore_data_base(value, orig_value) + # orig_value can also already be a lambda with esp_range info, and only # a plain string is sent in orig_value if isinstance(orig_value, ESPHomeDataBase): @@ -157,83 +207,221 @@ def _expand_substitutions( return value +def _push_context( + local_vars: dict[str, Any], + parent_context: ContextVars, + errors: ErrList | None = None, +) -> tuple[ContextVars, dict[str, Any]]: + """Resolve local_vars and layer them on top of parent_context. + + Returns ``(child_context, resolved_vars)`` where *child_context* is a + new :class:`ChainMap` whose front map is *resolved_vars* (an + :class:`OrderedDict` of successfully-resolved variables). + + Variables may reference each other (e.g. ``b: ${a + 1}``). + Dependencies are resolved recursively via a *resolver* callback + that Jinja invokes on cache-miss. If vars are already in + dependency order, the loop iterates exactly once per variable. + + The ChainMap stack used during resolution is:: + + resolver_context → resolved_vars → parent maps … + ↑ ↑ + holds Resolver filled as vars + callback are resolved + """ + # Vars still waiting to be resolved — popped one-by-one by resolve(). + unresolved_vars = local_vars.copy() + # Accumulates resolved values in dependency order; becomes the front + # map of the returned child context so later lookups find them first. + resolved_vars = OrderedDict() + # The context callees will search: resolved_vars (initially empty) + # shadowing whatever the parent already provides. + context_vars = parent_context.new_child(resolved_vars) + + # Vars that failed resolution (missing or circular references). + # Maps name → (original_value, cause_error) for deferred warnings. + unresolvables: dict[str, tuple[Any, UndefinedError]] = {} + + # One extra child layer so the Resolver callback lives in its own + # map and doesn't pollute resolved_vars. + resolver_context = context_vars.new_child() + + def resolve(key: str) -> Any: + """Resolve a variable, recursively resolving any dependencies it references.""" + value = unresolved_vars.pop(key, Missing) + if value is Missing: + return Missing + try: + value = _try_substitute(value, resolver_context) + except UndefinedError as err: + unresolvables[key] = (value, err) + return Missing + resolved_vars[key] = value + return value + + # Set up the resolver for use during substitution + resolver_context[Resolver] = resolve + + # Resolve all variables, recursively resolving dependencies as needed. + # Each call to resolve() resolves that variable and any variables it depends on. + while unresolved_vars: + resolve(next(iter(unresolved_vars))) + + for name, (value, cause) in unresolvables.items(): + resolved_vars[name] = value + if errors is not None: + _handle_undefined( + err=UndefinedError( + f"Could not resolve substitution variable '{name}': {cause}" + ), + path=["substitutions", name], + value=value, + strict_undefined=False, + errors=errors, + ) + + return context_vars, resolved_vars + + +def push_context( + config_node: Any, + parent_context: ContextVars, + errors: ErrList | None = None, +) -> ContextVars: + """Returns the context vars this config node must be evaluated with.""" + if isinstance(config_node, ConfigContext): + return _push_context(config_node.vars, parent_context, errors)[0] + + # This node does not define any vars itself, so just return parent context + return parent_context + + def _substitute_item( - substitutions: dict, item: Any, - path: list[int | str], - jinja: Jinja, - ignore_missing: bool, + path: SubstitutionPath, + parent_context: ContextVars, + strict_undefined: bool, + errors: ErrList | None = None, ) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks - if isinstance(item, list): - for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) - if sub is not None: - item[i] = sub - elif isinstance(item, dict): - replace_keys = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item( - substitutions, k, path + [k], jinja, ignore_missing - ) + """Recursively substitute variables in a config item. + + Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, + replacing variable references with values from context_vars. + Mutates containers in-place; returns a replacement value for + strings/scalars, or None if the item was unchanged. + """ + + def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: + if isinstance(item, ESPLiteralValue): + return None # do not substitute inside literal blocks + + ctx = push_context(item, parent_ctx, errors) + + if isinstance(item, list): + for idx, it in enumerate(item): + sub = _walk(it, path + [idx], ctx) if sub is not None: - replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k], jinja, ignore_missing) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(old), item.get(new)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) - if isinstance(sub, JinjaStr) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)): - sub = _expand_substitutions( - substitutions, item.value, path, jinja, ignore_missing + item[idx] = sub + elif isinstance(item, dict): + replace_keys: list[tuple[str, Any]] = [] + for k, v in item.items(): + if path or k != CONF_SUBSTITUTIONS: + sub = _walk(k, path + [k], ctx) + if sub is not None: + replace_keys.append((k, sub)) + sub = _walk(v, path + [k], ctx) + if sub is not None: + item[k] = sub + for old, new in replace_keys: + if str(new) == str(old): + item[new] = item[old] + else: + item[new] = merge_config(item.get(new), item.get(old)) + del item[old] + elif isinstance(item, str): + sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) + if not isinstance(sub, str) or sub != item: + return sub + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) + if sub != item.value: + item.value = sub + return None + + return _walk(item, path, parent_context) + + +def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: + """Eagerly substitute context vars into a config node in-place. + + Undefined variables are silently ignored — this is used before + the main substitution pass when not all variables are visible yet. + """ + _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + + +def _warn_unresolved_variables(errors: ErrList) -> None: + """Log warnings for unresolved substitution variables, skipping password fields.""" + for err, path, expression in errors: + if "password" in path: + continue + location: str = "->".join(str(x) for x in path) + if isinstance(expression, ESPHomeDataBase) and expression.esp_range is not None: + location += f" in {str(expression.esp_range.start_mark)}" + + _LOGGER.warning( + "The string '%s' looks like an expression," + " but could not resolve all the variables: %s (see %s)", + expression, + err.message, + location, ) - if sub != item: - item.value = sub - return None def do_substitution_pass( - config: dict, command_line_substitutions: dict, ignore_missing: bool = False -) -> None: - if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: - return + config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None +) -> OrderedDict: + """Run the substitution pass over the entire config. - # Merge substitutions in config, overriding with substitutions coming from command line: + Extracts the ``substitutions:`` block, merges in any command-line + overrides, resolves inter-variable dependencies, then walks the + config tree replacing all ``$var`` / ``${expr}`` references. + Returns the (mutated) config dict with resolved substitutions + restored at the front. + """ + # Extract substitutions from config, overriding with substitutions coming from command line: # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() - substitutions = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {} - ) - with cv.prepend_path("substitutions"): + substitutions = config.pop(CONF_SUBSTITUTIONS, {}) + with cv.prepend_path(CONF_SUBSTITUTIONS): if not isinstance(substitutions, dict): raise cv.Invalid( f"Substitutions must be a key to value mapping, got {type(substitutions)}" ) + substitutions = merge_dicts_ordered( + substitutions, command_line_substitutions or {} + ) - replace_keys = [] - for key, value in substitutions.items(): + replace_keys: list[tuple[str, str]] = [] + for key in substitutions: with cv.prepend_path(key): sub = validate_substitution_key(key) if sub != key: replace_keys.append((key, sub)) - substitutions[key] = value for old, new in replace_keys: substitutions[new] = substitutions[old] del substitutions[old] - config[CONF_SUBSTITUTIONS] = substitutions - # Move substitutions to the first place to replace substitutions in them correctly - config.move_to_end(CONF_SUBSTITUTIONS, False) + errors: ErrList = [] # Collect undefined errors during substitution + parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - # Create a Jinja environment that will consider substitutions in scope: - jinja = Jinja(substitutions) - _substitute_item(substitutions, config, [], jinja, ignore_missing) + _substitute_item(config, [], parent_context, False, errors) + + if errors: + _warn_unresolved_variables(errors) + + # Restore substitutions to front of dict for readability + if substitutions: + config[CONF_SUBSTITUTIONS] = substitutions + config.move_to_end(CONF_SUBSTITUTIONS, last=False) + return config diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index fb9f843da25..37e9fa4d2db 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,7 +1,6 @@ from ast import literal_eval -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from itertools import chain, islice -import logging import math import re from types import GeneratorType @@ -9,16 +8,17 @@ from typing import Any import jinja2 as jinja from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate - -from esphome.yaml_util import ESPLiteralValue +from jinja2.runtime import missing as Missing TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError TemplateRuntimeError = jinja.TemplateRuntimeError UndefinedError = jinja.UndefinedError Undefined = jinja.Undefined +# Sentinel key for resolver callback in ContextVars. +# Dots are invalid in substitution names so this can never collide with user keys. +Resolver = ".resolver" -_LOGGER = logging.getLogger(__name__) DETECT_JINJA = r"(\$\{)" detect_jinja_re = re.compile( @@ -52,33 +52,6 @@ SAFE_GLOBALS = { } -class JinjaStr(str): - """ - Wraps a string containing an unresolved Jinja expression, - storing the variables visible to it when it failed to resolve. - For example, an expression inside a package, `${ A * B }` may fail - to resolve at package parsing time if `A` is a local package var - but `B` is a substitution defined in the root yaml. - Therefore, we store the value of `A` as an upvalue bound - to the original string so we may be able to resolve `${ A * B }` - later in the main substitutions pass. - """ - - Undefined = object() - - def __new__(cls, value: str, upvalues=None): - if isinstance(value, JinjaStr): - base = str(value) - merged = {**value.upvalues, **(upvalues or {})} - else: - base = value - merged = dict(upvalues or {}) - obj = super().__new__(cls, base) - obj.upvalues = merged - obj.result = JinjaStr.Undefined - return obj - - class JinjaError(Exception): def __init__(self, context_trace: dict, expr: str): self.context_trace = context_trace @@ -106,9 +79,13 @@ class JinjaError(Exception): class TrackerContext(jinja.runtime.Context): def resolve_or_missing(self, key): val = super().resolve_or_missing(key) - if isinstance(val, JinjaStr): - self.environment.context_trace[key] = val - val, _ = self.environment.expand(val) + if val is Missing: + # Variable not in the template context — check if a resolver callback + # was registered (by _push_context) to lazily resolve dependencies + # between substitution variables in the same block. + resolver = super().resolve_or_missing(Resolver) + if resolver is not Missing: + val = resolver(key) self.environment.context_trace[key] = val return val @@ -160,15 +137,13 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any: class Jinja(jinja.Environment): - """ - Wraps a Jinja environment - """ + """Jinja environment configured for ESPHome substitution expressions.""" # jinja environment customization overrides code_generator_class = NativeCodeGenerator concat = staticmethod(_concat_nodes_override) - def __init__(self, context_vars: dict): + def __init__(self) -> None: super().__init__( trim_blocks=True, lstrip_blocks=True, @@ -183,49 +158,25 @@ class Jinja(jinja.Environment): self.context_class = TrackerContext self.add_extension("jinja2.ext.do") self.context_trace = {} - self.context_vars = {**context_vars} - for k, v in self.context_vars.items(): - if isinstance(v, ESPLiteralValue): - continue - if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v): - self.context_vars[k] = JinjaStr(v, self.context_vars) - self.globals = { - **self.globals, - **self.context_vars, - **SAFE_GLOBALS, - } + self.globals = {**self.globals, **SAFE_GLOBALS} - def expand(self, content_str: str | JinjaStr) -> Any: + def expand(self, content_str: str, context_vars: Mapping[str, Any]) -> Any: """ Renders a string that may contain Jinja expressions or statements Returns the resulting value if all variables and expressions could be resolved. - Otherwise, it returns a tagged (JinjaStr) string that captures variables - in scope (upvalues), like a closure for later evaluation. """ result = None - override_vars = {} - if isinstance(content_str, JinjaStr): - if content_str.result is not JinjaStr.Undefined: - return content_str.result, None - # If `value` is already a JinjaStr, it means we are trying to evaluate it again - # in a parent pass. - # Hopefully, all required variables are visible now. - override_vars = content_str.upvalues old_trace = self.context_trace self.context_trace = {} try: template = self.from_string(content_str) - result = template.render(override_vars) + result = template.render(context_vars) if isinstance(result, Undefined): - print("" + result) # force a UndefinedError exception - except (TemplateSyntaxError, UndefinedError) as err: - # `content_str` contains a Jinja expression that refers to a variable that is undefined - # in this scope. Perhaps it refers to a root substitution that is not visible yet. - # Therefore, return `content_str` as a JinjaStr, which contains the variables - # that are actually visible to it at this point to postpone evaluation. - return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + str(result) # force a UndefinedError exception + except UndefinedError as err: + raise err except JinjaError as err: err.context_trace = {**self.context_trace, **err.context_trace} err.eval_stack.append(content_str) @@ -242,10 +193,7 @@ class Jinja(jinja.Environment): finally: self.context_trace = old_trace - if isinstance(content_str, JinjaStr): - content_str.result = result - - return result, None + return result class JinjaTemplate(NativeTemplate): diff --git a/esphome/config.py b/esphome/config.py index 6f6ad4886bb..b80aaf37007 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,7 +12,8 @@ from typing import Any import voluptuous as vol from esphome import core, loader, pins, yaml_util -from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered +from esphome.components.substitutions import do_substitution_pass +from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -974,7 +975,7 @@ class PinUseValidationCheck(ConfigValidationStep): def validate_config( config: dict[str, Any], - command_line_substitutions: dict[str, Any], + command_line_substitutions: dict[str, Any] | None, skip_external_update: bool = False, ) -> Config: result = Config() @@ -994,21 +995,15 @@ def validate_config( result.add_error(err) return result - CORE.raw_config = config - # 1. Load substitutions if CONF_SUBSTITUTIONS in config or command_line_substitutions: - from esphome.components import substitutions - - result[CONF_SUBSTITUTIONS] = merge_dicts_ordered( - config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions - ) result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) - try: - substitutions.do_substitution_pass(config, command_line_substitutions) - except vol.Invalid as err: - result.add_error(err) - return result + try: + config = do_substitution_pass(config, command_line_substitutions) + except vol.Invalid as err: + CORE.raw_config = config + result.add_error(err) + return result # 1.1. Merge packages if CONF_PACKAGES in config: @@ -1016,6 +1011,9 @@ def validate_config( config = merge_packages(config) + # Remove substitutions from config during validation to prevent + # re-substitution. Re-added to result at the end of this function. + substitutions = config.pop(CONF_SUBSTITUTIONS, None) CORE.raw_config = config # 1.2. Resolve !extend and !remove and check for REPLACEME @@ -1089,6 +1087,10 @@ def validate_config( result.run_validation_steps() + if substitutions is not None: + result[CONF_SUBSTITUTIONS] = substitutions + result.move_to_end(CONF_SUBSTITUTIONS, last=False) + return result diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d0eab4e44ee..e001316a222 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -325,9 +325,7 @@ class ESPHomeLoaderMixin: return val @_add_data_ref - def construct_include( - self, node: yaml.Node - ) -> dict[str, Any] | OrderedDict[str, Any]: + def construct_include(self, node: yaml.Node) -> Any: from esphome.const import CONF_VARS def extract_file_vars(node): @@ -344,9 +342,7 @@ class ESPHomeLoaderMixin: file, vars = node.value, None result = self.yaml_loader(self._rel_path(file)) - if not vars: - vars = {} - return substitute_vars(result, vars) + return add_context(result, vars) @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: @@ -495,39 +491,6 @@ def parse_yaml( ) -def substitute_vars(config, vars): - from esphome.components import substitutions - from esphome.const import CONF_SUBSTITUTIONS - - org_subs = None - result = config - if not isinstance(config, dict): - # when the included yaml contains a list or a scalar - # wrap it into an OrderedDict because do_substitution_pass expects it - result = OrderedDict([("yaml", config)]) - elif CONF_SUBSTITUTIONS in result: - org_subs = result.pop(CONF_SUBSTITUTIONS) - - defaults = {} - if CONF_DEFAULTS in result: - defaults = result.pop(CONF_DEFAULTS) - - result[CONF_SUBSTITUTIONS] = vars - for k, v in defaults.items(): - if k not in result[CONF_SUBSTITUTIONS]: - result[CONF_SUBSTITUTIONS][k] = v - - # Ignore missing vars that refer to the top level substitutions - substitutions.do_substitution_pass(result, None, ignore_missing=True) - result.pop(CONF_SUBSTITUTIONS) - - if not isinstance(config, dict): - result = result["yaml"] # unwrap the result - elif org_subs: - result[CONF_SUBSTITUTIONS] = org_subs - return result - - def _load_yaml_internal_with_type( loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader], fname: Path, diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 22fb2c4e325..60dc0dccda3 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +from esphome.components.substitutions import do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove @@ -71,6 +72,7 @@ def fixture_basic_esphome(): def packages_pass(config): """Wrapper around packages_pass that also resolves Extend and Remove.""" config = do_packages_pass(config) + config = do_substitution_pass(config) config = merge_packages(config) resolve_extend_remove(config) return config diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index 9ed9b99c49a..87f0e3fa219 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -38,3 +38,20 @@ test_list: - '{ 79, 82 }' - a: 15 should be 15, overridden from command line b: 20 should stay as 20, not overridden + - aa: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + bb: + - 7 + - 8 + - 9 + - aa: + x: 1 + y: 3 + z: 4 + bb: + w: 5 diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 64701c03ddc..d70372f280d 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -44,3 +44,13 @@ test_list: - '{ ${position.x}, ${position.y} }' - a: ${a} should be 15, overridden from command line b: ${b} should stay as 20, not overridden + + # Test merging lists when substituted keys resolve to an existing key + - ${ "aa" }: [1, 2, 3] + ${ "a" + "a" }: [4, 5, 6] + ${ "bb" }: [7, 8, 9] + + # Test merging dicts when substituted keys resolve to an existing key + - ${ "aa" }: {"x": 1, "y": 2} + ${ "a" + "a" }: {"y": 3, "z": 4} + ${ "bb" }: {"w": 5} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml index 1a51fc44cfc..b8c76fbf524 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml @@ -9,6 +9,11 @@ substitutions: numberOne: 1 var1: 79 double_width: 14 + double_height: 16 + y: ${x} + x: ${y} + b: 79 + c: 80 test_list: - The area is 56 - 56 @@ -27,3 +32,4 @@ test_list: - chr(97) = a - len([1,2,3]) = 3 - width = 7, double_width = 14 + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml index 4612f581b5b..9593867f490 100644 --- a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml @@ -1,4 +1,7 @@ substitutions: + y: ${x} # Circular reference, expect to pass unresolved. + x: ${y} # Circular reference, expect to pass unresolved. + double_height: ${height * 2} width: 7 height: 8 enabled: true @@ -9,6 +12,8 @@ substitutions: numberOne: 1 var1: 79 double_width: ${width * 2} + c: ${b+1} + b: ${undefined_variable | default(79) } test_list: - "The area is ${width * height}" @@ -25,3 +30,4 @@ test_list: - chr(97) = ${ chr(97) } - len([1,2,3]) = ${ len([1,2,3]) } - width = ${width}, double_width = ${double_width} + - a = ${a} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml new file mode 100644 index 00000000000..867889b7bc1 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -0,0 +1,46 @@ +fancy_component: &id001 + - id: component9 + value: 9 +some_component: + - id: component1 + value: 1 + - id: component2 + value: 2 + - id: component3 + value: 3 + - id: component4 + value: 4 + - id: component5 + value: 79 + power: 200 + - id: component6 + value: 6 + - id: component7 + value: 7 +switch: &id002 + - platform: gpio + id: switch1 + pin: 12 + - platform: gpio + id: switch2 + pin: 13 +display: + - platform: ili9xxx + dimensions: + width: 100 + height: 480 +substitutions: + extended_component: component5 + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: *id001 + pin: 12 + some_switches: *id002 + package_selection: fancy_package + fancy_subst: 42 diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml new file mode 100644 index 00000000000..cc7b841aba8 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.input.yaml @@ -0,0 +1,63 @@ +substitutions: + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + substitutions: + fancy_subst: 42 + fancy_component: + - id: component9 + value: 9 + + pin: 12 + some_switches: + - platform: gpio + id: switch1 + pin: ${pin} + - platform: gpio + id: switch2 + pin: ${pin+1} + + package_selection: fancy_package + +packages: + - ${ package_options[package_selection] } + - some_component: + - id: component1 + value: 1 + - some_component: + - id: component2 + value: 2 + - switch: ${ some_switches } + - packages: + package_with_defaults: !include + file: display.yaml + vars: + native_width: 100 + high_dpi: false + my_package: + packages: + - packages: + special_package: + substitutions: + extended_component: component5 + some_component: + - id: component3 + value: 3 + some_component: + - id: component4 + value: 4 + - id: !extend ${ extended_component } + power: 200 + value: 79 + some_component: + - id: component5 + value: 5 + +some_component: + - id: component6 + value: 6 + - id: component7 + value: 7 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml new file mode 100644 index 00000000000..4abaf4471d3 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.approved.yaml @@ -0,0 +1,5 @@ +values: + - var1: $var1 + - a: 10 + - b: B-default + - c: The value of C is 79 diff --git a/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml new file mode 100644 index 00000000000..91eb0e9a3f2 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/09-include_vars_without_substs.input.yaml @@ -0,0 +1,7 @@ +# Test that include_vars with vars works even when there are no substitutions key defined. +packages: + - !include + file: inc1.yaml + vars: + a: 10 + c: 79 diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 1d8cb7631d4..db46a27dfb3 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -10,9 +10,10 @@ from esphome import config as config_module, yaml_util from esphome.components import substitutions from esphome.components.packages import do_packages_pass, merge_packages from esphome.config import resolve_extend_remove -from esphome.config_helpers import merge_config +from esphome.config_helpers import Extend, merge_config +import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS -from esphome.core import CORE +from esphome.core import CORE, Lambda from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -144,7 +145,7 @@ def test_substitutions_fixtures( config = do_packages_pass(config) - substitutions.do_substitution_pass(config, command_line_substitutions) + config = substitutions.do_substitution_pass(config, command_line_substitutions) config = merge_packages(config) @@ -206,7 +207,7 @@ def test_substitutions_with_command_line_maintains_ordered_dict() -> None: command_line_subs = {"var2": "override", "var3": "new_value"} # Call do_substitution_pass with command line substitutions - substitutions.do_substitution_pass(config, command_line_subs) + config = substitutions.do_substitution_pass(config, command_line_subs) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -234,7 +235,7 @@ def test_substitutions_without_command_line_maintains_ordered_dict() -> None: config["other_key"] = "other_value" # Call without command line substitutions - substitutions.do_substitution_pass(config, None) + config = substitutions.do_substitution_pass(config, None) # Verify that config is still an OrderedDict assert isinstance(config, OrderedDict), "Config should remain an OrderedDict" @@ -268,7 +269,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: ) # Now try to run substitution pass on the merged config - substitutions.do_substitution_pass(merged_config, None) + merged_config = substitutions.do_substitution_pass(merged_config, None) # Should not raise AttributeError assert isinstance(merged_config, OrderedDict), ( @@ -279,7 +280,7 @@ def test_substitutions_after_merge_config_maintains_ordered_dict() -> None: def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( - tmp_path, + tmp_path: Path, ) -> None: """Test that validate_config preserves OrderedDict when merging command-line substitutions. @@ -288,7 +289,7 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( """ # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -314,17 +315,11 @@ def test_validate_config_with_command_line_substitutions_maintains_ordered_dict( assert result[CONF_SUBSTITUTIONS]["var3"] == "new_value" -def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( - tmp_path, -) -> None: - """Test that validate_config preserves OrderedDict without command-line substitutions. - - This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set - using merge_dicts_ordered() when command_line_substitutions is None. - """ +def _get_test_minimal_valid_config(tmp_path: Path) -> OrderedDict: + """Helper to create a minimal valid config for testing.""" # Create a minimal valid config test_config = OrderedDict() - test_config["esphome"] = {"name": "test_device", "platform": "ESP32"} + test_config["esphome"] = {"name": "test_device"} test_config[CONF_SUBSTITUTIONS] = OrderedDict({"var1": "value1", "var2": "value2"}) test_config["esp32"] = {"board": "esp32dev"} @@ -332,6 +327,19 @@ def test_validate_config_without_command_line_substitutions_maintains_ordered_di test_yaml = tmp_path / "test.yaml" test_yaml.write_text("# test config") CORE.config_path = test_yaml + return test_config + + +def test_validate_config_without_command_line_substitutions_maintains_ordered_dict( + tmp_path: Path, +) -> None: + """Test that validate_config preserves OrderedDict without command-line substitutions. + + This tests the code path in config.py where result[CONF_SUBSTITUTIONS] is set + using merge_dicts_ordered() when command_line_substitutions is None. + """ + + test_config = _get_test_minimal_valid_config(tmp_path) # Call validate_config without command line substitutions result = config_module.validate_config(test_config, None) @@ -384,3 +392,205 @@ def test_merge_config_preserves_ordered_dict() -> None: assert not isinstance(result, OrderedDict), ( "dict + dict should not return OrderedDict" ) + + +def test_substitution_pass_error_gets_captured( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """vol.Invalid from do_substitution_pass is captured by validate_config.""" + + # Patch the target: in config_module.do_substitution_pass (NOT where it's defined) + def fake_do_substitution_pass(*args, **kwargs): + raise cv.Invalid("Error in do_substitutions_pass!!") + + monkeypatch.setattr( + config_module, "do_substitution_pass", fake_do_substitution_pass + ) + + # Prepare minimal config + no CLI substitutions + config = _get_test_minimal_valid_config(tmp_path) + + # Call the function under test + result = config_module.validate_config(config, None) + + # Now assert that add_error was called with the vol.Invalid + + assert "Error in do_substitutions_pass!!" in str(result.get_error_for_path([])) + + +@pytest.mark.parametrize( + "value", ["", " ", "1foo", "9VAR", "0abc", "$1foo", "$9VAR", "$0abc"] +) +def test_validate_substitution_key_empty_raises(value: str) -> None: + """Empty (or all-whitespace) substitution keys are rejected.""" + with pytest.raises(cv.Invalid): + substitutions.validate_substitution_key(value) + + +@pytest.mark.parametrize( + "input_value, expected_output", + [ + ("$FOO_bar9", "FOO_bar9"), # Valid key with leading '$' + ("Foo_bar9", "Foo_bar9"), # Normal valid key + ], +) +def test_validate_substitution_key_valid( + input_value: str, expected_output: str +) -> None: + """Valid substitution keys are accepted with optional leading '$'.""" + result = substitutions.validate_substitution_key(input_value) + assert result == expected_output + + +def test_circular_dependency_warnings( + caplog: pytest.LogCaptureFixture, +) -> None: + """Circular substitution references produce warnings naming the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"x": "${y}", "y": "${x}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'x'" in caplog.text + assert "'y' is undefined" in caplog.text + assert "Could not resolve substitution variable 'y'" in caplog.text + assert "'x' is undefined" in caplog.text + # Verify path includes location + assert "substitutions->x" in caplog.text + assert "substitutions->y" in caplog.text + + +def test_missing_dependency_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A substitution referencing an undefined variable warns with the cause.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"a": "${missing}"}), + "key": "value", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'missing' is undefined" in caplog.text + assert "substitutions->a" in caplog.text + + +def test_undefined_variable_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """A reference to an undefined variable in config values produces a warning.""" + config = OrderedDict( + { + "key": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_var' is undefined" in caplog.text + + +def test_password_field_warnings_suppressed( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined variables in password fields should not produce warnings.""" + config = OrderedDict( + { + "password": "${undefined_var}", + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert caplog.text == "" + + +def test_config_context_unresolvable_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """Unresolvable vars in a ConfigContext produce warnings via push_context.""" + inner = OrderedDict({"key": "${a}"}) + yaml_util.add_context(inner, {"a": "${undefined}"}) + config = OrderedDict({"items": [inner]}) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "Could not resolve substitution variable 'a'" in caplog.text + assert "'undefined' is undefined" in caplog.text + + +def test_non_string_substitution_value_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Undefined vars in non-string contexts (e.g. dict keys) produce warnings.""" + config = OrderedDict( + { + "items": {"${undefined_key}": "value"}, + } + ) + with caplog.at_level(logging.WARNING): + substitutions.do_substitution_pass(config) + + assert "'undefined_key' is undefined" in caplog.text + + +def test_lambda_substitution() -> None: + """Substitution inside a Lambda value should be expanded.""" + lam = Lambda("return ${var};") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value == "return 42;" + + +def test_lambda_no_substitution_unchanged() -> None: + """A Lambda with no variable references should not be mutated.""" + lam = Lambda("return 1;") + original_value = lam.value + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"var": "42"}), + "lambda": lam, + } + ) + substitutions.do_substitution_pass(config) + assert lam.value is original_value + + +def test_extend_substitution() -> None: + """Substitution inside an Extend value should be expanded.""" + ext = Extend("${component_id}") + config = OrderedDict( + { + CONF_SUBSTITUTIONS: OrderedDict({"component_id": "my_sensor"}), + "sensor": ext, + } + ) + substitutions.do_substitution_pass(config) + assert ext.value == "my_sensor" + + +def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: + """Non-mapping substitutions raises cv.Invalid.""" + config = OrderedDict( + { + CONF_SUBSTITUTIONS: ["not", "a", "mapping"], + "other": "value", + } + ) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + substitutions.do_substitution_pass(config) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index adb7658bfd6..35a4bc37073 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -25,7 +25,7 @@ def test_include_with_vars(fixture_path: Path) -> None: yaml_file = fixture_path / "yaml_util" / "includetest.yaml" actual = yaml_util.load_yaml(yaml_file) - substitutions.do_substitution_pass(actual, None) + actual = substitutions.do_substitution_pass(actual, None) assert actual["esphome"]["name"] == "original" assert actual["esphome"]["libraries"][0] == "Wire" assert actual["esp8266"]["board"] == "nodemcu" From e6a73cab8f1e090244d63d4d63766a570a45ec62 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:04:53 +1000 Subject: [PATCH 0951/2056] [number] Add sensor platform (#15125) --- esphome/components/number/sensor/__init__.py | 25 +++++++++++++++++++ .../number/sensor/number_sensor.cpp | 16 ++++++++++++ .../components/number/sensor/number_sensor.h | 19 ++++++++++++++ tests/components/number/common.yaml | 13 ++++++++++ tests/components/number/test.esp32-idf.yaml | 2 ++ tests/components/number/test.esp8266-ard.yaml | 2 ++ 6 files changed, 77 insertions(+) create mode 100644 esphome/components/number/sensor/__init__.py create mode 100644 esphome/components/number/sensor/number_sensor.cpp create mode 100644 esphome/components/number/sensor/number_sensor.h create mode 100644 tests/components/number/common.yaml create mode 100644 tests/components/number/test.esp32-idf.yaml create mode 100644 tests/components/number/test.esp8266-ard.yaml diff --git a/esphome/components/number/sensor/__init__.py b/esphome/components/number/sensor/__init__.py new file mode 100644 index 00000000000..0d4b580d7e4 --- /dev/null +++ b/esphome/components/number/sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Number, number_ns + +NumberSensor = number_ns.class_("NumberSensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema(NumberSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Number), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await sensor.new_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/number/sensor/number_sensor.cpp b/esphome/components/number/sensor/number_sensor.cpp new file mode 100644 index 00000000000..227202622ae --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.cpp @@ -0,0 +1,16 @@ +#include "number_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::number { + +static const char *const TAG = "number.sensor"; + +void NumberSensor::setup() { + this->source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void NumberSensor::dump_config() { LOG_SENSOR("", "Number Sensor", this); } + +} // namespace esphome::number diff --git a/esphome/components/number/sensor/number_sensor.h b/esphome/components/number/sensor/number_sensor.h new file mode 100644 index 00000000000..2d6825a2989 --- /dev/null +++ b/esphome/components/number/sensor/number_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../number.h" +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::number { + +class NumberSensor : public sensor::Sensor, public Component { + public: + explicit NumberSensor(Number *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Number *source_; +}; + +} // namespace esphome::number diff --git a/tests/components/number/common.yaml b/tests/components/number/common.yaml new file mode 100644 index 00000000000..c17c2dd5f83 --- /dev/null +++ b/tests/components/number/common.yaml @@ -0,0 +1,13 @@ +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +sensor: + - platform: number + name: "Test Number Value" + source_id: test_number diff --git a/tests/components/number/test.esp32-idf.yaml b/tests/components/number/test.esp32-idf.yaml new file mode 100644 index 00000000000..25cb37a0b42 --- /dev/null +++ b/tests/components/number/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/number/test.esp8266-ard.yaml b/tests/components/number/test.esp8266-ard.yaml new file mode 100644 index 00000000000..25cb37a0b42 --- /dev/null +++ b/tests/components/number/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml From 0fb31726f69b304581caec41614a080774feda8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:39:29 -1000 Subject: [PATCH 0952/2056] [esp32] Add sram1_as_iram option and bootloader version detection (#14874) --- esphome/components/esp32/__init__.py | 19 ++++++++ esphome/core/application.cpp | 50 ++++++++++++++++++---- esphome/core/defines.h | 1 + tests/components/esp32/test.esp32-idf.yaml | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f85f13fe737..1ecc270fd1c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -97,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SRAM1_AS_IRAM = "sram1_as_iram" CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" @@ -884,6 +885,13 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]: + errs.append( + cv.Invalid( + f"'{CONF_SRAM1_AS_IRAM}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM], + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE) is not None @@ -1131,6 +1139,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( *ESP32_CHIP_REVISIONS ), + cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -1655,6 +1664,16 @@ async def to_code(config): for rev, flag in ESP32_CHIP_REVISIONS.items(): add_idf_sdkconfig_option(flag, rev == min_rev) cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") + + # Use SRAM1 region as IRAM on ESP32 (original) variant + # This provides an additional 40KB of IRAM by using SRAM1 memory that was previously + # reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later. + # WARNING: If the device has an old bootloader (pre-v5.1), the app will fail to boot. + # A USB flash will update the bootloader automatically. OTA updates do not. + # See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/performance/ram-usage.html + if variant == VARIANT_ESP32 and conf[CONF_ADVANCED][CONF_SRAM1_AS_IRAM]: + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM", True) + cg.add_define("USE_ESP32_SRAM1_AS_IRAM") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c020a8ed585..ce15aed1e28 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -9,6 +9,8 @@ #endif #ifdef USE_ESP32 #include <esp_chip_info.h> +#include <esp_ota_ops.h> +#include <esp_bootloader_desc.h> #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" @@ -167,19 +169,49 @@ void Application::process_dump_config_() { esp_chip_info(&chip_info); ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); -#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) - // Suggest optimization for chips that don't need the PSRAM cache workaround - if (chip_info.revision >= 300) { -#ifdef USE_PSRAM - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, - chip_info.revision % 100); -#else - ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, - chip_info.revision % 100); +#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM)) + static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced"; #endif +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) + { + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { +#ifdef USE_PSRAM + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to save ~10KB IRAM", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#else + ESP_LOGW(TAG, "Chip rev >= 3.0 detected. Set minimum_chip_revision: \"%d.%d\" %s to reduce binary size", + chip_info.revision / 100, chip_info.revision % 100, ESP32_ADVANCED_PATH); +#endif + } } #endif + { + // esp_bootloader_desc_t is available in ESP-IDF >= 5.2; if readable the bootloader is modern. + // + // Design decision: We intentionally do NOT mention sram1_as_iram when the bootloader is too old. + // Enabling sram1_as_iram with an old bootloader causes a hard brick (device fails to boot, + // requires USB reflash to recover). Users don't always read warnings carefully, so we only + // suggest the option once we've confirmed the bootloader can handle it. In practice this + // means a user with an old bootloader may need to flash twice: once via USB to update the + // bootloader (they'll see the suggestion on next boot), then OTA with sram1_as_iram: true. + // Two flashes is a better outcome than a bricked device. + esp_bootloader_desc_t boot_desc; + if (esp_ota_get_bootloader_description(nullptr, &boot_desc) != ESP_OK) { +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGW(TAG, "Bootloader too old for OTA rollback and SRAM1 as IRAM (+40KB). " + "Flash via USB once to update the bootloader"); +#else + ESP_LOGW(TAG, "Bootloader too old for OTA rollback. Flash via USB once to update the bootloader"); #endif + } +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_SRAM1_AS_IRAM) + else { + ESP_LOGW(TAG, "Bootloader supports SRAM1 as IRAM (+40KB). Set sram1_as_iram: true %s", ESP32_ADVANCED_PATH); + } +#endif + } +#endif // USE_ESP32 } this->components_[this->dump_config_at_]->call_dump_config_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f437e30a955..996818c2e63 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -202,6 +202,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET +#define USE_ESP32_SRAM1_AS_IRAM #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index da85aa3b0f2..b999f23e1c2 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -19,6 +19,7 @@ esp32: disable_mbedtls_pkcs7: true disable_regi2c_in_iram: true disable_fatfs: true + sram1_as_iram: true wifi: ssid: MySSID From a0d0516b22e7ea8cd29c718ef510e9d33d3de5a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:40:41 -1000 Subject: [PATCH 0953/2056] [benchmark] Add noise handshake benchmark (#15039) --- .../components/api/bench_noise_encrypt.cpp | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp index 223e6ada0d0..9ef928192cb 100644 --- a/tests/benchmarks/components/api/bench_noise_encrypt.cpp +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -172,6 +172,135 @@ BENCHMARK(NoiseDecrypt_MediumMessage); static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } BENCHMARK(NoiseDecrypt_LargeMessage); +// --- Full Noise_NNpsk0 handshake benchmark --- +// Measures the complete handshake between initiator and responder: +// - Create handshake states for both sides +// - Set PSK and prologue +// - Exchange messages (initiator write -> responder read -> responder write -> initiator read) +// - Split to get cipher states +// This is dominated by Curve25519 DH operations (expensive on ESP8266). +// No inner iterations — each handshake is already expensive enough. + +static void NoiseHandshake_Full(benchmark::State &state) { + // Matching ESPHome's protocol: Noise_NNpsk0_25519_ChaChaPoly_SHA256 + NoiseProtocolId nid; + memset(&nid, 0, sizeof(nid)); + nid.pattern_id = NOISE_PATTERN_NN; + nid.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid.dh_id = NOISE_DH_CURVE25519; + nid.prefix_id = NOISE_PREFIX_STANDARD; + nid.hybrid_id = NOISE_DH_NONE; + nid.hash_id = NOISE_HASH_SHA256; + nid.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + // Dummy PSK (32 bytes) and prologue matching production setup + static constexpr uint8_t PSK[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + static constexpr uint8_t PROLOGUE[] = "NoESPHome"; + + // Message buffer for handshake exchange (max handshake message ~96 bytes) + uint8_t msg_buf[128]; + + for (auto _ : state) { + NoiseHandshakeState *initiator = nullptr; + NoiseHandshakeState *responder = nullptr; + NoiseCipherState *init_send = nullptr, *init_recv = nullptr; + NoiseCipherState *resp_send = nullptr, *resp_recv = nullptr; + int err; + + // Create both handshake states + err = noise_handshakestate_new_by_id(&initiator, &nid, NOISE_ROLE_INITIATOR); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create initiator"); + return; + } + err = noise_handshakestate_new_by_id(&responder, &nid, NOISE_ROLE_RESPONDER); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Failed to create responder"); + noise_handshakestate_free(initiator); + return; + } + + // Set PSK and prologue on both sides + noise_handshakestate_set_pre_shared_key(initiator, PSK, sizeof(PSK)); + noise_handshakestate_set_pre_shared_key(responder, PSK, sizeof(PSK)); + noise_handshakestate_set_prologue(initiator, PROLOGUE, sizeof(PROLOGUE) - 1); + noise_handshakestate_set_prologue(responder, PROLOGUE, sizeof(PROLOGUE) - 1); + + noise_handshakestate_start(initiator); + noise_handshakestate_start(responder); + + // Message 1: Initiator -> Responder + NoiseBuffer write_buf, read_buf; + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(initiator, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(responder, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Message 2: Responder -> Initiator + noise_buffer_set_output(write_buf, msg_buf, sizeof(msg_buf)); + err = noise_handshakestate_write_message(responder, &write_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder write_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + noise_buffer_set_input(read_buf, msg_buf, write_buf.size); + err = noise_handshakestate_read_message(initiator, &read_buf, nullptr); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator read_message failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + + // Split to get cipher states + err = noise_handshakestate_split(initiator, &init_send, &init_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Initiator split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + return; + } + err = noise_handshakestate_split(responder, &resp_send, &resp_recv); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Responder split failed"); + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + return; + } + + benchmark::DoNotOptimize(init_send); + + // Cleanup + noise_handshakestate_free(initiator); + noise_handshakestate_free(responder); + noise_cipherstate_free(init_send); + noise_cipherstate_free(init_recv); + noise_cipherstate_free(resp_send); + noise_cipherstate_free(resp_recv); + } +} +BENCHMARK(NoiseHandshake_Full); + } // namespace esphome::api::benchmarks #endif // USE_API_NOISE From 382de7ca906b2b584b1c6188105a50523182f2f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:40:53 -1000 Subject: [PATCH 0954/2056] [api] Store dump strings in PROGMEM to save RAM on ESP8266 (#14982) --- esphome/components/api/api_pb2.h | 274 +-- esphome/components/api/api_pb2_dump.cpp | 2425 ++++++++++---------- esphome/components/api/api_pb2_service.cpp | 4 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/proto.h | 20 +- script/api_protobuf/api_protobuf.py | 96 +- 6 files changed, 1443 insertions(+), 1378 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 86289a28d6e..16586e6e9a4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -388,7 +388,7 @@ class HelloRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_request"; } + const LogString *message_name() const override { return LOG_STR("hello_request"); } #endif StringRef client_info{}; uint32_t api_version_major{0}; @@ -406,7 +406,7 @@ class HelloResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_response"; } + const LogString *message_name() const override { return LOG_STR("hello_response"); } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -425,7 +425,7 @@ class DisconnectRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_request"; } + const LogString *message_name() const override { return LOG_STR("disconnect_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -438,7 +438,7 @@ class DisconnectResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_response"; } + const LogString *message_name() const override { return LOG_STR("disconnect_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -451,7 +451,7 @@ class PingRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_request"; } + const LogString *message_name() const override { return LOG_STR("ping_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -464,7 +464,7 @@ class PingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_response"; } + const LogString *message_name() const override { return LOG_STR("ping_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -520,7 +520,7 @@ class DeviceInfoResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_response"; } + const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif StringRef name{}; StringRef mac_address{}; @@ -587,7 +587,7 @@ class ListEntitiesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_done_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_done_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -601,7 +601,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_binary_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); } #endif StringRef device_class{}; bool is_status_binary_sensor{false}; @@ -618,7 +618,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "binary_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("binary_sensor_state_response"); } #endif bool state{false}; bool missing_state{false}; @@ -637,7 +637,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_cover_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); } #endif bool assumed_state{false}; bool supports_position{false}; @@ -657,7 +657,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_state_response"; } + const LogString *message_name() const override { return LOG_STR("cover_state_response"); } #endif float position{0.0f}; float tilt{0.0f}; @@ -675,7 +675,7 @@ class CoverCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_command_request"; } + const LogString *message_name() const override { return LOG_STR("cover_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -697,7 +697,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_fan_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_fan_response"); } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -717,7 +717,7 @@ class FanStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_state_response"; } + const LogString *message_name() const override { return LOG_STR("fan_state_response"); } #endif bool state{false}; bool oscillating{false}; @@ -737,7 +737,7 @@ class FanCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_command_request"; } + const LogString *message_name() const override { return LOG_STR("fan_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -765,7 +765,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_light_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_light_response"); } #endif const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; @@ -784,7 +784,7 @@ class LightStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_state_response"; } + const LogString *message_name() const override { return LOG_STR("light_state_response"); } #endif bool state{false}; float brightness{0.0f}; @@ -811,7 +811,7 @@ class LightCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_command_request"; } + const LogString *message_name() const override { return LOG_STR("light_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -855,7 +855,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); } #endif StringRef unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -875,7 +875,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("sensor_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -894,7 +894,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_switch_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); } #endif bool assumed_state{false}; StringRef device_class{}; @@ -911,7 +911,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_state_response"; } + const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -927,7 +927,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_command_request"; } + const LogString *message_name() const override { return LOG_STR("switch_command_request"); } #endif bool state{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -945,7 +945,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -961,7 +961,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -979,7 +979,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_request"); } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -995,7 +995,7 @@ class SubscribeLogsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_response"); } #endif enums::LogLevel level{}; const uint8_t *message_ptr_{nullptr}; @@ -1018,7 +1018,7 @@ class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_request"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_request"); } #endif const uint8_t *key{nullptr}; uint16_t key_len{0}; @@ -1034,7 +1034,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_response"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1064,7 +1064,7 @@ class HomeassistantActionRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_request"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); } #endif StringRef service{}; FixedVector<HomeassistantServiceMap> data{}; @@ -1095,7 +1095,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 130; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_response"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1119,7 +1119,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef attribute{}; @@ -1137,7 +1137,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef state{}; @@ -1155,7 +1155,7 @@ class GetTimeRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_request"; } + const LogString *message_name() const override { return LOG_STR("get_time_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1197,7 +1197,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_response"; } + const LogString *message_name() const override { return LOG_STR("get_time_response"); } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; @@ -1228,7 +1228,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 50; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_services_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); } #endif StringRef name{}; uint32_t key{0}; @@ -1268,7 +1268,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_request"; } + const LogString *message_name() const override { return LOG_STR("execute_service_request"); } #endif uint32_t key{0}; FixedVector<ExecuteServiceArgument> args{}; @@ -1295,7 +1295,7 @@ class ExecuteServiceResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 131; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_response"; } + const LogString *message_name() const override { return LOG_STR("execute_service_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1319,7 +1319,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_camera_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -1334,7 +1334,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_response"; } + const LogString *message_name() const override { return LOG_STR("camera_image_response"); } #endif const uint8_t *data_ptr_{nullptr}; size_t data_len_{0}; @@ -1356,7 +1356,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_request"; } + const LogString *message_name() const override { return LOG_STR("camera_image_request"); } #endif bool single{false}; bool stream{false}; @@ -1374,7 +1374,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t ESTIMATED_SIZE = 150; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_climate_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1407,7 +1407,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_state_response"; } + const LogString *message_name() const override { return LOG_STR("climate_state_response"); } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1435,7 +1435,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_command_request"; } + const LogString *message_name() const override { return LOG_STR("climate_command_request"); } #endif bool has_mode{false}; enums::ClimateMode mode{}; @@ -1473,7 +1473,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 132; static constexpr uint8_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_water_heater_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); } #endif float min_temperature{0.0f}; float max_temperature{0.0f}; @@ -1493,7 +1493,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 133; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_state_response"; } + const LogString *message_name() const override { return LOG_STR("water_heater_state_response"); } #endif float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1514,7 +1514,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 134; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_command_request"; } + const LogString *message_name() const override { return LOG_STR("water_heater_command_request"); } #endif uint32_t has_fields{0}; enums::WaterHeaterMode mode{}; @@ -1537,7 +1537,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_number_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_number_response"); } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1558,7 +1558,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_state_response"; } + const LogString *message_name() const override { return LOG_STR("number_state_response"); } #endif float state{0.0f}; bool missing_state{false}; @@ -1575,7 +1575,7 @@ class NumberCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_command_request"; } + const LogString *message_name() const override { return LOG_STR("number_command_request"); } #endif float state{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1593,7 +1593,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_select_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector<const char *> *options{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1609,7 +1609,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_state_response"; } + const LogString *message_name() const override { return LOG_STR("select_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -1626,7 +1626,7 @@ class SelectCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_command_request"; } + const LogString *message_name() const override { return LOG_STR("select_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1645,7 +1645,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_siren_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_siren_response"); } #endif const FixedVector<const char *> *tones{}; bool supports_duration{false}; @@ -1663,7 +1663,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_state_response"; } + const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -1679,7 +1679,7 @@ class SirenCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_command_request"; } + const LogString *message_name() const override { return LOG_STR("siren_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -1705,7 +1705,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_lock_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_lock_response"); } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1724,7 +1724,7 @@ class LockStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_state_response"; } + const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1740,7 +1740,7 @@ class LockCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_command_request"; } + const LogString *message_name() const override { return LOG_STR("lock_command_request"); } #endif enums::LockCommand command{}; bool has_code{false}; @@ -1761,7 +1761,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_button_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -1777,7 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "button_command_request"; } + const LogString *message_name() const override { return LOG_STR("button_command_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1809,7 +1809,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_media_player_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); } #endif bool supports_pause{false}; std::vector<MediaPlayerSupportedFormat> supported_formats{}; @@ -1827,7 +1827,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_state_response"; } + const LogString *message_name() const override { return LOG_STR("media_player_state_response"); } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1845,7 +1845,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_command_request"; } + const LogString *message_name() const override { return LOG_STR("media_player_command_request"); } #endif bool has_command{false}; enums::MediaPlayerCommand command{}; @@ -1871,7 +1871,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_bluetooth_le_advertisements_request"); } #endif uint32_t flags{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1901,7 +1901,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_le_raw_advertisements_response"); } #endif std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{}; uint16_t advertisements_len{0}; @@ -1918,7 +1918,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_request"); } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1936,7 +1936,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_connection_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_connection_response"); } #endif uint64_t address{0}; bool connected{false}; @@ -1955,7 +1955,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_request"); } #endif uint64_t address{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2012,7 +2012,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_response"); } #endif uint64_t address{0}; std::vector<BluetoothGATTService> services{}; @@ -2029,7 +2029,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); } #endif uint64_t address{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -2045,7 +2045,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2061,7 +2061,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2084,7 +2084,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2104,7 +2104,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2120,7 +2120,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2139,7 +2139,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2156,7 +2156,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_data_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2179,7 +2179,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_connections_free_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_connections_free_response"); } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2197,7 +2197,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_error_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_error_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2215,7 +2215,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2232,7 +2232,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2249,7 +2249,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_pairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_pairing_response"); } #endif uint64_t address{0}; bool paired{false}; @@ -2267,7 +2267,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_unpairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_unpairing_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2285,7 +2285,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_clear_cache_response"); } #endif uint64_t address{0}; bool success{false}; @@ -2303,7 +2303,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 126; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_state_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_state_response"); } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2321,7 +2321,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_set_mode_request"); } #endif enums::BluetoothScannerMode mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2338,7 +2338,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_voice_assistant_request"); } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2367,7 +2367,7 @@ class VoiceAssistantRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); } #endif bool start{false}; StringRef conversation_id{}; @@ -2387,7 +2387,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_response"); } #endif uint32_t port{0}; bool error{false}; @@ -2414,7 +2414,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_event_response"); } #endif enums::VoiceAssistantEvent event_type{}; std::vector<VoiceAssistantEventData> data{}; @@ -2431,7 +2431,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_audio"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_audio"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -2451,7 +2451,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_timer_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_timer_event_response"); } #endif enums::VoiceAssistantTimerEvent event_type{}; StringRef timer_id{}; @@ -2472,7 +2472,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_request"); } #endif StringRef media_id{}; StringRef text{}; @@ -2491,7 +2491,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_finished"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; void encode(ProtoWriteBuffer &buffer) const; @@ -2537,7 +2537,7 @@ class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_request"); } #endif std::vector<VoiceAssistantExternalWakeWord> external_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2552,7 +2552,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_response"); } #endif std::vector<VoiceAssistantWakeWord> available_wake_words{}; const std::vector<std::string> *active_wake_words{}; @@ -2570,7 +2570,7 @@ class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_set_configuration"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_set_configuration"); } #endif std::vector<std::string> active_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2587,7 +2587,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_alarm_control_panel_response"); } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2605,7 +2605,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_state_response"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2621,7 +2621,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_command_request"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_command_request"); } #endif enums::AlarmControlPanelStateCommand command{}; StringRef code{}; @@ -2641,7 +2641,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_response"); } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2660,7 +2660,7 @@ class TextStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_state_response"); } #endif StringRef state{}; bool missing_state{false}; @@ -2677,7 +2677,7 @@ class TextCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_command_request"; } + const LogString *message_name() const override { return LOG_STR("text_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2696,7 +2696,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2711,7 +2711,7 @@ class DateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_state_response"); } #endif bool missing_state{false}; uint32_t year{0}; @@ -2730,7 +2730,7 @@ class DateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_command_request"); } #endif uint32_t year{0}; uint32_t month{0}; @@ -2750,7 +2750,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2765,7 +2765,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_state_response"; } + const LogString *message_name() const override { return LOG_STR("time_state_response"); } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2784,7 +2784,7 @@ class TimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_command_request"; } + const LogString *message_name() const override { return LOG_STR("time_command_request"); } #endif uint32_t hour{0}; uint32_t minute{0}; @@ -2804,7 +2804,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_event_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); } #endif StringRef device_class{}; const FixedVector<const char *> *event_types{}; @@ -2821,7 +2821,7 @@ class EventResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "event_response"; } + const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2839,7 +2839,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_valve_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); } #endif StringRef device_class{}; bool assumed_state{false}; @@ -2858,7 +2858,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_state_response"; } + const LogString *message_name() const override { return LOG_STR("valve_state_response"); } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2875,7 +2875,7 @@ class ValveCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_command_request"; } + const LogString *message_name() const override { return LOG_STR("valve_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -2895,7 +2895,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -2910,7 +2910,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_time_state_response"); } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2927,7 +2927,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_time_command_request"); } #endif uint32_t epoch_seconds{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2945,7 +2945,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_update_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; void encode(ProtoWriteBuffer &buffer) const; @@ -2961,7 +2961,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_state_response"; } + const LogString *message_name() const override { return LOG_STR("update_state_response"); } #endif bool missing_state{false}; bool in_progress{false}; @@ -2985,7 +2985,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_command_request"; } + const LogString *message_name() const override { return LOG_STR("update_command_request"); } #endif enums::UpdateCommand command{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3003,7 +3003,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 128; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_frame"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_frame"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; @@ -3021,7 +3021,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 129; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request"); } #endif enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; @@ -3043,7 +3043,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 135; static constexpr uint8_t ESTIMATED_SIZE = 44; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_infrared_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; void encode(ProtoWriteBuffer &buffer) const; @@ -3061,7 +3061,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 136; static constexpr uint8_t ESTIMATED_SIZE = 220; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3086,7 +3086,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 137; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_receive_event"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_receive_event"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3108,7 +3108,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 138; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_configure_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_configure_request"); } #endif uint32_t instance{0}; uint32_t baudrate{0}; @@ -3128,7 +3128,7 @@ class SerialProxyDataReceived final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 139; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_data_received"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_data_received"); } #endif uint32_t instance{0}; const uint8_t *data_ptr_{nullptr}; @@ -3150,7 +3150,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 140; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_write_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_write_request"); } #endif uint32_t instance{0}; const uint8_t *data{nullptr}; @@ -3168,7 +3168,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 141; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_set_modem_pins_request"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3184,7 +3184,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 142; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_request"); } #endif uint32_t instance{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3199,7 +3199,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 143; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } #endif uint32_t instance{0}; uint32_t line_states{0}; @@ -3216,7 +3216,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 144; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3232,7 +3232,7 @@ class SerialProxyRequestResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 147; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "serial_proxy_request_response"; } + const LogString *message_name() const override { return LOG_STR("serial_proxy_request_response"); } #endif uint32_t instance{0}; enums::SerialProxyRequestType type{}; @@ -3253,7 +3253,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 145; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_request"); } #endif uint64_t address{0}; uint32_t min_interval{0}; @@ -3272,7 +3272,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 146; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_set_connection_params_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_response"); } #endif uint64_t address{0}; int32_t error{0}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 5a53f0281fa..a11f3b231e2 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2,6 +2,7 @@ // See script/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include <cinttypes> @@ -9,6 +10,21 @@ namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -19,8 +35,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -28,10 +45,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -41,6 +59,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); @@ -85,56 +107,59 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\n"); } +// proto_enum_to_string returns PROGMEM pointers, so use append_p template<typename T> static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string<T>(value)); + out.append_p(proto_enum_to_string<T>(value)); out.append("\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\n"); } +#pragma GCC diagnostic pop template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) { switch (value) { case enums::SERIAL_PROXY_PORT_TYPE_TTL: - return "SERIAL_PROXY_PORT_TYPE_TTL"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_TTL"); case enums::SERIAL_PROXY_PORT_TYPE_RS232: - return "SERIAL_PROXY_PORT_TYPE_RS232"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232"); case enums::SERIAL_PROXY_PORT_TYPE_RS485: - return "SERIAL_PROXY_PORT_TYPE_RS485"; + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: - return "ENTITY_CATEGORY_NONE"; + return ESPHOME_PSTR("ENTITY_CATEGORY_NONE"); case enums::ENTITY_CATEGORY_CONFIG: - return "ENTITY_CATEGORY_CONFIG"; + return ESPHOME_PSTR("ENTITY_CATEGORY_CONFIG"); case enums::ENTITY_CATEGORY_DIAGNOSTIC: - return "ENTITY_CATEGORY_DIAGNOSTIC"; + return ESPHOME_PSTR("ENTITY_CATEGORY_DIAGNOSTIC"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_COVER template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: - return "COVER_OPERATION_IDLE"; + return ESPHOME_PSTR("COVER_OPERATION_IDLE"); case enums::COVER_OPERATION_IS_OPENING: - return "COVER_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_OPENING"); case enums::COVER_OPERATION_IS_CLOSING: - return "COVER_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -142,11 +167,11 @@ template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverO template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: - return "FAN_DIRECTION_FORWARD"; + return ESPHOME_PSTR("FAN_DIRECTION_FORWARD"); case enums::FAN_DIRECTION_REVERSE: - return "FAN_DIRECTION_REVERSE"; + return ESPHOME_PSTR("FAN_DIRECTION_REVERSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -154,29 +179,29 @@ template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirec template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode value) { switch (value) { case enums::COLOR_MODE_UNKNOWN: - return "COLOR_MODE_UNKNOWN"; + return ESPHOME_PSTR("COLOR_MODE_UNKNOWN"); case enums::COLOR_MODE_ON_OFF: - return "COLOR_MODE_ON_OFF"; + return ESPHOME_PSTR("COLOR_MODE_ON_OFF"); case enums::COLOR_MODE_LEGACY_BRIGHTNESS: - return "COLOR_MODE_LEGACY_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_LEGACY_BRIGHTNESS"); case enums::COLOR_MODE_BRIGHTNESS: - return "COLOR_MODE_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_BRIGHTNESS"); case enums::COLOR_MODE_WHITE: - return "COLOR_MODE_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_WHITE"); case enums::COLOR_MODE_COLOR_TEMPERATURE: - return "COLOR_MODE_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_COLOR_TEMPERATURE"); case enums::COLOR_MODE_COLD_WARM_WHITE: - return "COLOR_MODE_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_COLD_WARM_WHITE"); case enums::COLOR_MODE_RGB: - return "COLOR_MODE_RGB"; + return ESPHOME_PSTR("COLOR_MODE_RGB"); case enums::COLOR_MODE_RGB_WHITE: - return "COLOR_MODE_RGB_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_WHITE"); case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: - return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLOR_TEMPERATURE"); case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: - return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLD_WARM_WHITE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -184,91 +209,91 @@ template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode v template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: - return "STATE_CLASS_NONE"; + return ESPHOME_PSTR("STATE_CLASS_NONE"); case enums::STATE_CLASS_MEASUREMENT: - return "STATE_CLASS_MEASUREMENT"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT"); case enums::STATE_CLASS_TOTAL_INCREASING: - return "STATE_CLASS_TOTAL_INCREASING"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL_INCREASING"); case enums::STATE_CLASS_TOTAL: - return "STATE_CLASS_TOTAL"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL"); case enums::STATE_CLASS_MEASUREMENT_ANGLE: - return "STATE_CLASS_MEASUREMENT_ANGLE"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT_ANGLE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: - return "LOG_LEVEL_NONE"; + return ESPHOME_PSTR("LOG_LEVEL_NONE"); case enums::LOG_LEVEL_ERROR: - return "LOG_LEVEL_ERROR"; + return ESPHOME_PSTR("LOG_LEVEL_ERROR"); case enums::LOG_LEVEL_WARN: - return "LOG_LEVEL_WARN"; + return ESPHOME_PSTR("LOG_LEVEL_WARN"); case enums::LOG_LEVEL_INFO: - return "LOG_LEVEL_INFO"; + return ESPHOME_PSTR("LOG_LEVEL_INFO"); case enums::LOG_LEVEL_CONFIG: - return "LOG_LEVEL_CONFIG"; + return ESPHOME_PSTR("LOG_LEVEL_CONFIG"); case enums::LOG_LEVEL_DEBUG: - return "LOG_LEVEL_DEBUG"; + return ESPHOME_PSTR("LOG_LEVEL_DEBUG"); case enums::LOG_LEVEL_VERBOSE: - return "LOG_LEVEL_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERBOSE"); case enums::LOG_LEVEL_VERY_VERBOSE: - return "LOG_LEVEL_VERY_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERY_VERBOSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::DSTRuleType>(enums::DSTRuleType value) { switch (value) { case enums::DST_RULE_TYPE_NONE: - return "DST_RULE_TYPE_NONE"; + return ESPHOME_PSTR("DST_RULE_TYPE_NONE"); case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: - return "DST_RULE_TYPE_MONTH_WEEK_DAY"; + return ESPHOME_PSTR("DST_RULE_TYPE_MONTH_WEEK_DAY"); case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: - return "DST_RULE_TYPE_JULIAN_NO_LEAP"; + return ESPHOME_PSTR("DST_RULE_TYPE_JULIAN_NO_LEAP"); case enums::DST_RULE_TYPE_DAY_OF_YEAR: - return "DST_RULE_TYPE_DAY_OF_YEAR"; + return ESPHOME_PSTR("DST_RULE_TYPE_DAY_OF_YEAR"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: - return "SERVICE_ARG_TYPE_BOOL"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL"); case enums::SERVICE_ARG_TYPE_INT: - return "SERVICE_ARG_TYPE_INT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT"); case enums::SERVICE_ARG_TYPE_FLOAT: - return "SERVICE_ARG_TYPE_FLOAT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT"); case enums::SERVICE_ARG_TYPE_STRING: - return "SERVICE_ARG_TYPE_STRING"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING"); case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: - return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL_ARRAY"); case enums::SERVICE_ARG_TYPE_INT_ARRAY: - return "SERVICE_ARG_TYPE_INT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT_ARRAY"); case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: - return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT_ARRAY"); case enums::SERVICE_ARG_TYPE_STRING_ARRAY: - return "SERVICE_ARG_TYPE_STRING_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING_ARRAY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) { switch (value) { case enums::SUPPORTS_RESPONSE_NONE: - return "SUPPORTS_RESPONSE_NONE"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_NONE"); case enums::SUPPORTS_RESPONSE_OPTIONAL: - return "SUPPORTS_RESPONSE_OPTIONAL"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_OPTIONAL"); case enums::SUPPORTS_RESPONSE_ONLY: - return "SUPPORTS_RESPONSE_ONLY"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_ONLY"); case enums::SUPPORTS_RESPONSE_STATUS: - return "SUPPORTS_RESPONSE_STATUS"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_STATUS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -276,103 +301,103 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums:: template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { switch (value) { case enums::CLIMATE_MODE_OFF: - return "CLIMATE_MODE_OFF"; + return ESPHOME_PSTR("CLIMATE_MODE_OFF"); case enums::CLIMATE_MODE_HEAT_COOL: - return "CLIMATE_MODE_HEAT_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT_COOL"); case enums::CLIMATE_MODE_COOL: - return "CLIMATE_MODE_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_COOL"); case enums::CLIMATE_MODE_HEAT: - return "CLIMATE_MODE_HEAT"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT"); case enums::CLIMATE_MODE_FAN_ONLY: - return "CLIMATE_MODE_FAN_ONLY"; + return ESPHOME_PSTR("CLIMATE_MODE_FAN_ONLY"); case enums::CLIMATE_MODE_DRY: - return "CLIMATE_MODE_DRY"; + return ESPHOME_PSTR("CLIMATE_MODE_DRY"); case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; + return ESPHOME_PSTR("CLIMATE_MODE_AUTO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::ClimateFanMode>(enums::ClimateFanMode value) { switch (value) { case enums::CLIMATE_FAN_ON: - return "CLIMATE_FAN_ON"; + return ESPHOME_PSTR("CLIMATE_FAN_ON"); case enums::CLIMATE_FAN_OFF: - return "CLIMATE_FAN_OFF"; + return ESPHOME_PSTR("CLIMATE_FAN_OFF"); case enums::CLIMATE_FAN_AUTO: - return "CLIMATE_FAN_AUTO"; + return ESPHOME_PSTR("CLIMATE_FAN_AUTO"); case enums::CLIMATE_FAN_LOW: - return "CLIMATE_FAN_LOW"; + return ESPHOME_PSTR("CLIMATE_FAN_LOW"); case enums::CLIMATE_FAN_MEDIUM: - return "CLIMATE_FAN_MEDIUM"; + return ESPHOME_PSTR("CLIMATE_FAN_MEDIUM"); case enums::CLIMATE_FAN_HIGH: - return "CLIMATE_FAN_HIGH"; + return ESPHOME_PSTR("CLIMATE_FAN_HIGH"); case enums::CLIMATE_FAN_MIDDLE: - return "CLIMATE_FAN_MIDDLE"; + return ESPHOME_PSTR("CLIMATE_FAN_MIDDLE"); case enums::CLIMATE_FAN_FOCUS: - return "CLIMATE_FAN_FOCUS"; + return ESPHOME_PSTR("CLIMATE_FAN_FOCUS"); case enums::CLIMATE_FAN_DIFFUSE: - return "CLIMATE_FAN_DIFFUSE"; + return ESPHOME_PSTR("CLIMATE_FAN_DIFFUSE"); case enums::CLIMATE_FAN_QUIET: - return "CLIMATE_FAN_QUIET"; + return ESPHOME_PSTR("CLIMATE_FAN_QUIET"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::ClimateSwingMode>(enums::ClimateSwingMode value) { switch (value) { case enums::CLIMATE_SWING_OFF: - return "CLIMATE_SWING_OFF"; + return ESPHOME_PSTR("CLIMATE_SWING_OFF"); case enums::CLIMATE_SWING_BOTH: - return "CLIMATE_SWING_BOTH"; + return ESPHOME_PSTR("CLIMATE_SWING_BOTH"); case enums::CLIMATE_SWING_VERTICAL: - return "CLIMATE_SWING_VERTICAL"; + return ESPHOME_PSTR("CLIMATE_SWING_VERTICAL"); case enums::CLIMATE_SWING_HORIZONTAL: - return "CLIMATE_SWING_HORIZONTAL"; + return ESPHOME_PSTR("CLIMATE_SWING_HORIZONTAL"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::ClimateAction value) { switch (value) { case enums::CLIMATE_ACTION_OFF: - return "CLIMATE_ACTION_OFF"; + return ESPHOME_PSTR("CLIMATE_ACTION_OFF"); case enums::CLIMATE_ACTION_COOLING: - return "CLIMATE_ACTION_COOLING"; + return ESPHOME_PSTR("CLIMATE_ACTION_COOLING"); case enums::CLIMATE_ACTION_HEATING: - return "CLIMATE_ACTION_HEATING"; + return ESPHOME_PSTR("CLIMATE_ACTION_HEATING"); case enums::CLIMATE_ACTION_IDLE: - return "CLIMATE_ACTION_IDLE"; + return ESPHOME_PSTR("CLIMATE_ACTION_IDLE"); case enums::CLIMATE_ACTION_DRYING: - return "CLIMATE_ACTION_DRYING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DRYING"); case enums::CLIMATE_ACTION_FAN: - return "CLIMATE_ACTION_FAN"; + return ESPHOME_PSTR("CLIMATE_ACTION_FAN"); case enums::CLIMATE_ACTION_DEFROSTING: - return "CLIMATE_ACTION_DEFROSTING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DEFROSTING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::ClimatePreset value) { switch (value) { case enums::CLIMATE_PRESET_NONE: - return "CLIMATE_PRESET_NONE"; + return ESPHOME_PSTR("CLIMATE_PRESET_NONE"); case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + return ESPHOME_PSTR("CLIMATE_PRESET_HOME"); case enums::CLIMATE_PRESET_AWAY: - return "CLIMATE_PRESET_AWAY"; + return ESPHOME_PSTR("CLIMATE_PRESET_AWAY"); case enums::CLIMATE_PRESET_BOOST: - return "CLIMATE_PRESET_BOOST"; + return ESPHOME_PSTR("CLIMATE_PRESET_BOOST"); case enums::CLIMATE_PRESET_COMFORT: - return "CLIMATE_PRESET_COMFORT"; + return ESPHOME_PSTR("CLIMATE_PRESET_COMFORT"); case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + return ESPHOME_PSTR("CLIMATE_PRESET_ECO"); case enums::CLIMATE_PRESET_SLEEP: - return "CLIMATE_PRESET_SLEEP"; + return ESPHOME_PSTR("CLIMATE_PRESET_SLEEP"); case enums::CLIMATE_PRESET_ACTIVITY: - return "CLIMATE_PRESET_ACTIVITY"; + return ESPHOME_PSTR("CLIMATE_PRESET_ACTIVITY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -380,21 +405,21 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) { switch (value) { case enums::WATER_HEATER_MODE_OFF: - return "WATER_HEATER_MODE_OFF"; + return ESPHOME_PSTR("WATER_HEATER_MODE_OFF"); case enums::WATER_HEATER_MODE_ECO: - return "WATER_HEATER_MODE_ECO"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ECO"); case enums::WATER_HEATER_MODE_ELECTRIC: - return "WATER_HEATER_MODE_ELECTRIC"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ELECTRIC"); case enums::WATER_HEATER_MODE_PERFORMANCE: - return "WATER_HEATER_MODE_PERFORMANCE"; + return ESPHOME_PSTR("WATER_HEATER_MODE_PERFORMANCE"); case enums::WATER_HEATER_MODE_HIGH_DEMAND: - return "WATER_HEATER_MODE_HIGH_DEMAND"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HIGH_DEMAND"); case enums::WATER_HEATER_MODE_HEAT_PUMP: - return "WATER_HEATER_MODE_HEAT_PUMP"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HEAT_PUMP"); case enums::WATER_HEATER_MODE_GAS: - return "WATER_HEATER_MODE_GAS"; + return ESPHOME_PSTR("WATER_HEATER_MODE_GAS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -402,36 +427,36 @@ template<> const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) { switch (value) { case enums::WATER_HEATER_COMMAND_HAS_NONE: - return "WATER_HEATER_COMMAND_HAS_NONE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_NONE"); case enums::WATER_HEATER_COMMAND_HAS_MODE: - return "WATER_HEATER_COMMAND_HAS_MODE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_MODE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"); case enums::WATER_HEATER_COMMAND_HAS_STATE: - return "WATER_HEATER_COMMAND_HAS_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_STATE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"); case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: - return "WATER_HEATER_COMMAND_HAS_ON_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_ON_STATE"); case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: - return "WATER_HEATER_COMMAND_HAS_AWAY_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_AWAY_STATE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_NUMBER template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) { switch (value) { case enums::NUMBER_MODE_AUTO: - return "NUMBER_MODE_AUTO"; + return ESPHOME_PSTR("NUMBER_MODE_AUTO"); case enums::NUMBER_MODE_BOX: - return "NUMBER_MODE_BOX"; + return ESPHOME_PSTR("NUMBER_MODE_BOX"); case enums::NUMBER_MODE_SLIDER: - return "NUMBER_MODE_SLIDER"; + return ESPHOME_PSTR("NUMBER_MODE_SLIDER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -439,31 +464,31 @@ template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState value) { switch (value) { case enums::LOCK_STATE_NONE: - return "LOCK_STATE_NONE"; + return ESPHOME_PSTR("LOCK_STATE_NONE"); case enums::LOCK_STATE_LOCKED: - return "LOCK_STATE_LOCKED"; + return ESPHOME_PSTR("LOCK_STATE_LOCKED"); case enums::LOCK_STATE_UNLOCKED: - return "LOCK_STATE_UNLOCKED"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKED"); case enums::LOCK_STATE_JAMMED: - return "LOCK_STATE_JAMMED"; + return ESPHOME_PSTR("LOCK_STATE_JAMMED"); case enums::LOCK_STATE_LOCKING: - return "LOCK_STATE_LOCKING"; + return ESPHOME_PSTR("LOCK_STATE_LOCKING"); case enums::LOCK_STATE_UNLOCKING: - return "LOCK_STATE_UNLOCKING"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockCommand value) { switch (value) { case enums::LOCK_UNLOCK: - return "LOCK_UNLOCK"; + return ESPHOME_PSTR("LOCK_UNLOCK"); case enums::LOCK_LOCK: - return "LOCK_LOCK"; + return ESPHOME_PSTR("LOCK_LOCK"); case enums::LOCK_OPEN: - return "LOCK_OPEN"; + return ESPHOME_PSTR("LOCK_OPEN"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -471,65 +496,65 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) { switch (value) { case enums::MEDIA_PLAYER_STATE_NONE: - return "MEDIA_PLAYER_STATE_NONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_NONE"); case enums::MEDIA_PLAYER_STATE_IDLE: - return "MEDIA_PLAYER_STATE_IDLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_IDLE"); case enums::MEDIA_PLAYER_STATE_PLAYING: - return "MEDIA_PLAYER_STATE_PLAYING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PLAYING"); case enums::MEDIA_PLAYER_STATE_PAUSED: - return "MEDIA_PLAYER_STATE_PAUSED"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PAUSED"); case enums::MEDIA_PLAYER_STATE_ANNOUNCING: - return "MEDIA_PLAYER_STATE_ANNOUNCING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ANNOUNCING"); case enums::MEDIA_PLAYER_STATE_OFF: - return "MEDIA_PLAYER_STATE_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_OFF"); case enums::MEDIA_PLAYER_STATE_ON: - return "MEDIA_PLAYER_STATE_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ON"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) { switch (value) { case enums::MEDIA_PLAYER_COMMAND_PLAY: - return "MEDIA_PLAYER_COMMAND_PLAY"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PLAY"); case enums::MEDIA_PLAYER_COMMAND_PAUSE: - return "MEDIA_PLAYER_COMMAND_PAUSE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PAUSE"); case enums::MEDIA_PLAYER_COMMAND_STOP: - return "MEDIA_PLAYER_COMMAND_STOP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_STOP"); case enums::MEDIA_PLAYER_COMMAND_MUTE: - return "MEDIA_PLAYER_COMMAND_MUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_MUTE"); case enums::MEDIA_PLAYER_COMMAND_UNMUTE: - return "MEDIA_PLAYER_COMMAND_UNMUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_UNMUTE"); case enums::MEDIA_PLAYER_COMMAND_TOGGLE: - return "MEDIA_PLAYER_COMMAND_TOGGLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TOGGLE"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP: - return "MEDIA_PLAYER_COMMAND_VOLUME_UP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_UP"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_DOWN"); case enums::MEDIA_PLAYER_COMMAND_ENQUEUE: - return "MEDIA_PLAYER_COMMAND_ENQUEUE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_ENQUEUE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE: - return "MEDIA_PLAYER_COMMAND_REPEAT_ONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_ONE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF: - return "MEDIA_PLAYER_COMMAND_REPEAT_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_OFF"); case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: - return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"); case enums::MEDIA_PLAYER_COMMAND_TURN_ON: - return "MEDIA_PLAYER_COMMAND_TURN_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_ON"); case enums::MEDIA_PLAYER_COMMAND_TURN_OFF: - return "MEDIA_PLAYER_COMMAND_TURN_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_OFF"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::MediaPlayerFormatPurpose>(enums::MediaPlayerFormatPurpose value) { switch (value) { case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"); case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -538,49 +563,49 @@ template<> const char *proto_enum_to_string<enums::BluetoothDeviceRequestType>(enums::BluetoothDeviceRequestType value) { switch (value) { case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::BluetoothScannerState>(enums::BluetoothScannerState value) { switch (value) { case enums::BLUETOOTH_SCANNER_STATE_IDLE: - return "BLUETOOTH_SCANNER_STATE_IDLE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_IDLE"); case enums::BLUETOOTH_SCANNER_STATE_STARTING: - return "BLUETOOTH_SCANNER_STATE_STARTING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STARTING"); case enums::BLUETOOTH_SCANNER_STATE_RUNNING: - return "BLUETOOTH_SCANNER_STATE_RUNNING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_RUNNING"); case enums::BLUETOOTH_SCANNER_STATE_FAILED: - return "BLUETOOTH_SCANNER_STATE_FAILED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_FAILED"); case enums::BLUETOOTH_SCANNER_STATE_STOPPING: - return "BLUETOOTH_SCANNER_STATE_STOPPING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPING"); case enums::BLUETOOTH_SCANNER_STATE_STOPPED: - return "BLUETOOTH_SCANNER_STATE_STOPPED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::BluetoothScannerMode>(enums::BluetoothScannerMode value) { switch (value) { case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: - return "BLUETOOTH_SCANNER_MODE_PASSIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_PASSIVE"); case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: - return "BLUETOOTH_SCANNER_MODE_ACTIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_ACTIVE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -588,76 +613,76 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::VoiceAssistantSubscribeFlag value) { switch (value) { case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: - return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE"); case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: - return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::VoiceAssistantRequestFlag>(enums::VoiceAssistantRequestFlag value) { switch (value) { case enums::VOICE_ASSISTANT_REQUEST_NONE: - return "VOICE_ASSISTANT_REQUEST_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_NONE"); case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: - return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_VAD"); case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: - return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_VOICE_ASSISTANT template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::VoiceAssistantEvent value) { switch (value) { case enums::VOICE_ASSISTANT_ERROR: - return "VOICE_ASSISTANT_ERROR"; + return ESPHOME_PSTR("VOICE_ASSISTANT_ERROR"); case enums::VOICE_ASSISTANT_RUN_START: - return "VOICE_ASSISTANT_RUN_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_START"); case enums::VOICE_ASSISTANT_RUN_END: - return "VOICE_ASSISTANT_RUN_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_END"); case enums::VOICE_ASSISTANT_STT_START: - return "VOICE_ASSISTANT_STT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_START"); case enums::VOICE_ASSISTANT_STT_END: - return "VOICE_ASSISTANT_STT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_END"); case enums::VOICE_ASSISTANT_INTENT_START: - return "VOICE_ASSISTANT_INTENT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_START"); case enums::VOICE_ASSISTANT_INTENT_END: - return "VOICE_ASSISTANT_INTENT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_END"); case enums::VOICE_ASSISTANT_TTS_START: - return "VOICE_ASSISTANT_TTS_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_START"); case enums::VOICE_ASSISTANT_TTS_END: - return "VOICE_ASSISTANT_TTS_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_END"); case enums::VOICE_ASSISTANT_WAKE_WORD_START: - return "VOICE_ASSISTANT_WAKE_WORD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_START"); case enums::VOICE_ASSISTANT_WAKE_WORD_END: - return "VOICE_ASSISTANT_WAKE_WORD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_END"); case enums::VOICE_ASSISTANT_STT_VAD_START: - return "VOICE_ASSISTANT_STT_VAD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START"); case enums::VOICE_ASSISTANT_STT_VAD_END: - return "VOICE_ASSISTANT_STT_VAD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END"); case enums::VOICE_ASSISTANT_TTS_STREAM_START: - return "VOICE_ASSISTANT_TTS_STREAM_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START"); case enums::VOICE_ASSISTANT_TTS_STREAM_END: - return "VOICE_ASSISTANT_TTS_STREAM_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_END"); case enums::VOICE_ASSISTANT_INTENT_PROGRESS: - return "VOICE_ASSISTANT_INTENT_PROGRESS"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_PROGRESS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::VoiceAssistantTimerEvent>(enums::VoiceAssistantTimerEvent value) { switch (value) { case enums::VOICE_ASSISTANT_TIMER_STARTED: - return "VOICE_ASSISTANT_TIMER_STARTED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_STARTED"); case enums::VOICE_ASSISTANT_TIMER_UPDATED: - return "VOICE_ASSISTANT_TIMER_UPDATED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_UPDATED"); case enums::VOICE_ASSISTANT_TIMER_CANCELLED: - return "VOICE_ASSISTANT_TIMER_CANCELLED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_CANCELLED"); case enums::VOICE_ASSISTANT_TIMER_FINISHED: - return "VOICE_ASSISTANT_TIMER_FINISHED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_FINISHED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -665,48 +690,48 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantTimerEvent>(enu template<> const char *proto_enum_to_string<enums::AlarmControlPanelState>(enums::AlarmControlPanelState value) { switch (value) { case enums::ALARM_STATE_DISARMED: - return "ALARM_STATE_DISARMED"; + return ESPHOME_PSTR("ALARM_STATE_DISARMED"); case enums::ALARM_STATE_ARMED_HOME: - return "ALARM_STATE_ARMED_HOME"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_HOME"); case enums::ALARM_STATE_ARMED_AWAY: - return "ALARM_STATE_ARMED_AWAY"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_AWAY"); case enums::ALARM_STATE_ARMED_NIGHT: - return "ALARM_STATE_ARMED_NIGHT"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_NIGHT"); case enums::ALARM_STATE_ARMED_VACATION: - return "ALARM_STATE_ARMED_VACATION"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_VACATION"); case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: - return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_CUSTOM_BYPASS"); case enums::ALARM_STATE_PENDING: - return "ALARM_STATE_PENDING"; + return ESPHOME_PSTR("ALARM_STATE_PENDING"); case enums::ALARM_STATE_ARMING: - return "ALARM_STATE_ARMING"; + return ESPHOME_PSTR("ALARM_STATE_ARMING"); case enums::ALARM_STATE_DISARMING: - return "ALARM_STATE_DISARMING"; + return ESPHOME_PSTR("ALARM_STATE_DISARMING"); case enums::ALARM_STATE_TRIGGERED: - return "ALARM_STATE_TRIGGERED"; + return ESPHOME_PSTR("ALARM_STATE_TRIGGERED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::AlarmControlPanelStateCommand>(enums::AlarmControlPanelStateCommand value) { switch (value) { case enums::ALARM_CONTROL_PANEL_DISARM: - return "ALARM_CONTROL_PANEL_DISARM"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_DISARM"); case enums::ALARM_CONTROL_PANEL_ARM_AWAY: - return "ALARM_CONTROL_PANEL_ARM_AWAY"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_AWAY"); case enums::ALARM_CONTROL_PANEL_ARM_HOME: - return "ALARM_CONTROL_PANEL_ARM_HOME"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_HOME"); case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: - return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_NIGHT"); case enums::ALARM_CONTROL_PANEL_ARM_VACATION: - return "ALARM_CONTROL_PANEL_ARM_VACATION"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_VACATION"); case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: - return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"); case enums::ALARM_CONTROL_PANEL_TRIGGER: - return "ALARM_CONTROL_PANEL_TRIGGER"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_TRIGGER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -714,11 +739,11 @@ const char *proto_enum_to_string<enums::AlarmControlPanelStateCommand>(enums::Al template<> const char *proto_enum_to_string<enums::TextMode>(enums::TextMode value) { switch (value) { case enums::TEXT_MODE_TEXT: - return "TEXT_MODE_TEXT"; + return ESPHOME_PSTR("TEXT_MODE_TEXT"); case enums::TEXT_MODE_PASSWORD: - return "TEXT_MODE_PASSWORD"; + return ESPHOME_PSTR("TEXT_MODE_PASSWORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -726,13 +751,13 @@ template<> const char *proto_enum_to_string<enums::TextMode>(enums::TextMode val template<> const char *proto_enum_to_string<enums::ValveOperation>(enums::ValveOperation value) { switch (value) { case enums::VALVE_OPERATION_IDLE: - return "VALVE_OPERATION_IDLE"; + return ESPHOME_PSTR("VALVE_OPERATION_IDLE"); case enums::VALVE_OPERATION_IS_OPENING: - return "VALVE_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_OPENING"); case enums::VALVE_OPERATION_IS_CLOSING: - return "VALVE_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -740,13 +765,13 @@ template<> const char *proto_enum_to_string<enums::ValveOperation>(enums::ValveO template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateCommand value) { switch (value) { case enums::UPDATE_COMMAND_NONE: - return "UPDATE_COMMAND_NONE"; + return ESPHOME_PSTR("UPDATE_COMMAND_NONE"); case enums::UPDATE_COMMAND_UPDATE: - return "UPDATE_COMMAND_UPDATE"; + return ESPHOME_PSTR("UPDATE_COMMAND_UPDATE"); case enums::UPDATE_COMMAND_CHECK: - return "UPDATE_COMMAND_CHECK"; + return ESPHOME_PSTR("UPDATE_COMMAND_CHECK"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -754,13 +779,13 @@ template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateC template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums::ZWaveProxyRequestType value) { switch (value) { case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: - return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -768,165 +793,165 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums: template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) { switch (value) { case enums::SERIAL_PROXY_PARITY_NONE: - return "SERIAL_PROXY_PARITY_NONE"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_NONE"); case enums::SERIAL_PROXY_PARITY_EVEN: - return "SERIAL_PROXY_PARITY_EVEN"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_EVEN"); case enums::SERIAL_PROXY_PARITY_ODD: - return "SERIAL_PROXY_PARITY_ODD"; + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_ODD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) { switch (value) { case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: - return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) { switch (value) { case enums::SERIAL_PROXY_STATUS_OK: - return "SERIAL_PROXY_STATUS_OK"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_OK"); case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: - return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"); case enums::SERIAL_PROXY_STATUS_ERROR: - return "SERIAL_PROXY_STATUS_ERROR"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ERROR"); case enums::SERIAL_PROXY_STATUS_TIMEOUT: - return "SERIAL_PROXY_STATUS_TIMEOUT"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: - return "SERIAL_PROXY_STATUS_NOT_SUPPORTED"; + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest")); + dump_field(out, ESPHOME_PSTR("client_info"), this->client_info); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); return out.c_str(); } const char *HelloResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloResponse"); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); - dump_field(out, "server_info", this->server_info); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloResponse")); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); + dump_field(out, ESPHOME_PSTR("server_info"), this->server_info); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } const char *DisconnectRequest::dump_to(DumpBuffer &out) const { - out.append("DisconnectRequest {}"); + out.append_p(ESPHOME_PSTR("DisconnectRequest {}")); return out.c_str(); } const char *DisconnectResponse::dump_to(DumpBuffer &out) const { - out.append("DisconnectResponse {}"); + out.append_p(ESPHOME_PSTR("DisconnectResponse {}")); return out.c_str(); } const char *PingRequest::dump_to(DumpBuffer &out) const { - out.append("PingRequest {}"); + out.append_p(ESPHOME_PSTR("PingRequest {}")); return out.c_str(); } const char *PingResponse::dump_to(DumpBuffer &out) const { - out.append("PingResponse {}"); + out.append_p(ESPHOME_PSTR("PingResponse {}")); return out.c_str(); } #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AreaInfo"); - dump_field(out, "area_id", this->area_id); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("AreaInfo")); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } #endif #ifdef USE_DEVICES const char *DeviceInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfo"); - dump_field(out, "device_id", this->device_id); - dump_field(out, "name", this->name); - dump_field(out, "area_id", this->area_id); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfo")); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyInfo"); - dump_field(out, "name", this->name); - dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("port_type"), static_cast<enums::SerialProxyPortType>(this->port_type)); return out.c_str(); } #endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfoResponse"); - dump_field(out, "name", this->name); - dump_field(out, "mac_address", this->mac_address); - dump_field(out, "esphome_version", this->esphome_version); - dump_field(out, "compilation_time", this->compilation_time); - dump_field(out, "model", this->model); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfoResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("mac_address"), this->mac_address); + dump_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version); + dump_field(out, ESPHOME_PSTR("compilation_time"), this->compilation_time); + dump_field(out, ESPHOME_PSTR("model"), this->model); #ifdef USE_DEEP_SLEEP - dump_field(out, "has_deep_sleep", this->has_deep_sleep); + dump_field(out, ESPHOME_PSTR("has_deep_sleep"), this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_name", this->project_name); + dump_field(out, ESPHOME_PSTR("project_name"), this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_version", this->project_version); + dump_field(out, ESPHOME_PSTR("project_version"), this->project_version); #endif #ifdef USE_WEBSERVER - dump_field(out, "webserver_port", this->webserver_port); + dump_field(out, ESPHOME_PSTR("webserver_port"), this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("bluetooth_proxy_feature_flags"), this->bluetooth_proxy_feature_flags); #endif - dump_field(out, "manufacturer", this->manufacturer); - dump_field(out, "friendly_name", this->friendly_name); + dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer); + dump_field(out, ESPHOME_PSTR("friendly_name"), this->friendly_name); #ifdef USE_VOICE_ASSISTANT - dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); + dump_field(out, ESPHOME_PSTR("voice_assistant_feature_flags"), this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - dump_field(out, "suggested_area", this->suggested_area); + dump_field(out, ESPHOME_PSTR("suggested_area"), this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address); + dump_field(out, ESPHOME_PSTR("bluetooth_mac_address"), this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - dump_field(out, "api_encryption_supported", this->api_encryption_supported); + dump_field(out, ESPHOME_PSTR("api_encryption_supported"), this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - out.append(" devices: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("devices")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - out.append(" areas: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("areas")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS - out.append(" area: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("area")).append(": "); this->area.dump_to(out); out.append("\n"); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("zwave_proxy_feature_flags"), this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_home_id", this->zwave_home_id); + dump_field(out, ESPHOME_PSTR("zwave_home_id"), this->zwave_home_id); #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - out.append(" serial_proxies: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("serial_proxies")).append(": "); it.dump_to(out); out.append("\n"); } @@ -934,1720 +959,1720 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesDoneResponse {}"); + out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}")); return out.c_str(); } #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "device_class", this->device_class); - dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("is_status_binary_sensor"), this->is_status_binary_sensor); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *BinarySensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BinarySensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("BinarySensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_COVER const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_tilt", this->supports_tilt); - dump_field(out, "device_class", this->device_class); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCoverResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_tilt"), this->supports_tilt); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "tilt", this->tilt); - dump_field(out, "current_operation", static_cast<enums::CoverOperation>(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast<enums::CoverOperation>(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "has_tilt", this->has_tilt); - dump_field(out, "tilt", this->tilt); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("has_tilt"), this->has_tilt); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_FAN const char *ListEntitiesFanResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesFanResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_oscillation", this->supports_oscillation); - dump_field(out, "supports_speed", this->supports_speed); - dump_field(out, "supports_direction", this->supports_direction); - dump_field(out, "supported_speed_count", this->supported_speed_count); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesFanResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_oscillation"), this->supports_oscillation); + dump_field(out, ESPHOME_PSTR("supports_speed"), this->supports_speed); + dump_field(out, ESPHOME_PSTR("supports_direction"), this->supports_direction); + dump_field(out, ESPHOME_PSTR("supported_speed_count"), this->supported_speed_count); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); for (const auto &it : *this->supported_preset_modes) { - dump_field(out, "supported_preset_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_preset_modes"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "direction", static_cast<enums::FanDirection>(this->direction)); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("direction"), static_cast<enums::FanDirection>(this->direction)); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_oscillating", this->has_oscillating); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "has_direction", this->has_direction); - dump_field(out, "direction", static_cast<enums::FanDirection>(this->direction)); - dump_field(out, "has_speed_level", this->has_speed_level); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "has_preset_mode", this->has_preset_mode); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_oscillating"), this->has_oscillating); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("has_direction"), this->has_direction); + dump_field(out, ESPHOME_PSTR("direction"), static_cast<enums::FanDirection>(this->direction)); + dump_field(out, ESPHOME_PSTR("has_speed_level"), this->has_speed_level); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("has_preset_mode"), this->has_preset_mode); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LIGHT const char *ListEntitiesLightResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLightResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLightResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); for (const auto &it : *this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_color_modes"), static_cast<enums::ColorMode>(it), 4); } - dump_field(out, "min_mireds", this->min_mireds); - dump_field(out, "max_mireds", this->max_mireds); + dump_field(out, ESPHOME_PSTR("min_mireds"), this->min_mireds); + dump_field(out, ESPHOME_PSTR("max_mireds"), this->max_mireds); for (const auto &it : *this->effects) { - dump_field(out, "effects", it, 4); + dump_field(out, ESPHOME_PSTR("effects"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "brightness", this->brightness); - dump_field(out, "color_mode", static_cast<enums::ColorMode>(this->color_mode)); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "white", this->white); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast<enums::ColorMode>(this->color_mode)); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_brightness", this->has_brightness); - dump_field(out, "brightness", this->brightness); - dump_field(out, "has_color_mode", this->has_color_mode); - dump_field(out, "color_mode", static_cast<enums::ColorMode>(this->color_mode)); - dump_field(out, "has_color_brightness", this->has_color_brightness); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "has_rgb", this->has_rgb); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "has_white", this->has_white); - dump_field(out, "white", this->white); - dump_field(out, "has_color_temperature", this->has_color_temperature); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "has_cold_white", this->has_cold_white); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "has_warm_white", this->has_warm_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "has_transition_length", this->has_transition_length); - dump_field(out, "transition_length", this->transition_length); - dump_field(out, "has_flash_length", this->has_flash_length); - dump_field(out, "flash_length", this->flash_length); - dump_field(out, "has_effect", this->has_effect); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_brightness"), this->has_brightness); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("has_color_mode"), this->has_color_mode); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast<enums::ColorMode>(this->color_mode)); + dump_field(out, ESPHOME_PSTR("has_color_brightness"), this->has_color_brightness); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("has_rgb"), this->has_rgb); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("has_white"), this->has_white); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("has_color_temperature"), this->has_color_temperature); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("has_cold_white"), this->has_cold_white); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("has_warm_white"), this->has_warm_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("has_transition_length"), this->has_transition_length); + dump_field(out, ESPHOME_PSTR("transition_length"), this->transition_length); + dump_field(out, ESPHOME_PSTR("has_flash_length"), this->has_flash_length); + dump_field(out, ESPHOME_PSTR("flash_length"), this->flash_length); + dump_field(out, ESPHOME_PSTR("has_effect"), this->has_effect); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SENSOR const char *ListEntitiesSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "accuracy_decimals", this->accuracy_decimals); - dump_field(out, "force_update", this->force_update); - dump_field(out, "device_class", this->device_class); - dump_field(out, "state_class", static_cast<enums::SensorStateClass>(this->state_class)); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("accuracy_decimals"), this->accuracy_decimals); + dump_field(out, ESPHOME_PSTR("force_update"), this->force_update); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("state_class"), static_cast<enums::SensorStateClass>(this->state_class)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SWITCH const char *ListEntitiesSwitchResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSwitchResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT_SENSOR const char *ListEntitiesTextSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextSensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextSensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextSensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif const char *SubscribeLogsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsRequest"); - dump_field(out, "level", static_cast<enums::LogLevel>(this->level)); - dump_field(out, "dump_config", this->dump_config); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsRequest")); + dump_field(out, ESPHOME_PSTR("level"), static_cast<enums::LogLevel>(this->level)); + dump_field(out, ESPHOME_PSTR("dump_config"), this->dump_config); return out.c_str(); } const char *SubscribeLogsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsResponse"); - dump_field(out, "level", static_cast<enums::LogLevel>(this->level)); - dump_bytes_field(out, "message", this->message_ptr_, this->message_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsResponse")); + dump_field(out, ESPHOME_PSTR("level"), static_cast<enums::LogLevel>(this->level)); + dump_bytes_field(out, ESPHOME_PSTR("message"), this->message_ptr_, this->message_len_); return out.c_str(); } #ifdef USE_API_NOISE const char *NoiseEncryptionSetKeyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); - dump_bytes_field(out, "key", this->key, this->key_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyRequest")); + dump_bytes_field(out, ESPHOME_PSTR("key"), this->key, this->key_len); return out.c_str(); } const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyResponse")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantServiceMap"); - dump_field(out, "key", this->key); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionRequest"); - dump_field(out, "service", this->service); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest")); + dump_field(out, ESPHOME_PSTR("service"), this->service); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->data_template) { - out.append(" data_template: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data_template")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->variables) { - out.append(" variables: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("variables")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "is_event", this->is_event); + dump_field(out, ESPHOME_PSTR("is_event"), this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "wants_response", this->wants_response); + dump_field(out, ESPHOME_PSTR("wants_response"), this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "response_template", this->response_template); + dump_field(out, ESPHOME_PSTR("response_template"), this->response_template); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "attribute", this->attribute); - dump_field(out, "once", this->once); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeHomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); + dump_field(out, ESPHOME_PSTR("once"), this->once); return out.c_str(); } const char *HomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "state", this->state); - dump_field(out, "attribute", this->attribute); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); return out.c_str(); } #endif const char *GetTimeRequest::dump_to(DumpBuffer &out) const { - out.append("GetTimeRequest {}"); + out.append_p(ESPHOME_PSTR("GetTimeRequest {}")); return out.c_str(); } const char *DSTRule::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DSTRule"); - dump_field(out, "time_seconds", this->time_seconds); - dump_field(out, "day", this->day); - dump_field(out, "type", static_cast<enums::DSTRuleType>(this->type)); - dump_field(out, "month", this->month); - dump_field(out, "week", this->week); - dump_field(out, "day_of_week", this->day_of_week); + MessageDumpHelper helper(out, ESPHOME_PSTR("DSTRule")); + dump_field(out, ESPHOME_PSTR("time_seconds"), this->time_seconds); + dump_field(out, ESPHOME_PSTR("day"), this->day); + dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::DSTRuleType>(this->type)); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("week"), this->week); + dump_field(out, ESPHOME_PSTR("day_of_week"), this->day_of_week); return out.c_str(); } const char *ParsedTimezone::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ParsedTimezone"); - dump_field(out, "std_offset_seconds", this->std_offset_seconds); - dump_field(out, "dst_offset_seconds", this->dst_offset_seconds); - out.append(" dst_start: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("ParsedTimezone")); + dump_field(out, ESPHOME_PSTR("std_offset_seconds"), this->std_offset_seconds); + dump_field(out, ESPHOME_PSTR("dst_offset_seconds"), this->dst_offset_seconds); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_start")).append(": "); this->dst_start.dump_to(out); out.append("\n"); - out.append(" dst_end: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_end")).append(": "); this->dst_end.dump_to(out); out.append("\n"); return out.c_str(); } const char *GetTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "GetTimeResponse"); - dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); - out.append(" parsed_timezone: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse")); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); + dump_field(out, ESPHOME_PSTR("timezone"), this->timezone); + out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": "); this->parsed_timezone.dump_to(out); out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); - dump_field(out, "name", this->name); - dump_field(out, "type", static_cast<enums::ServiceArgType>(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type)); return out.c_str(); } const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); - dump_field(out, "name", this->name); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response)); + dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response)); return out.c_str(); } const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceArgument"); - dump_field(out, "bool_", this->bool_); - dump_field(out, "legacy_int", this->legacy_int); - dump_field(out, "float_", this->float_); - dump_field(out, "string_", this->string_); - dump_field(out, "int_", this->int_); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceArgument")); + dump_field(out, ESPHOME_PSTR("bool_"), this->bool_); + dump_field(out, ESPHOME_PSTR("legacy_int"), this->legacy_int); + dump_field(out, ESPHOME_PSTR("float_"), this->float_); + dump_field(out, ESPHOME_PSTR("string_"), this->string_); + dump_field(out, ESPHOME_PSTR("int_"), this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", static_cast<bool>(it), 4); + dump_field(out, ESPHOME_PSTR("bool_array"), static_cast<bool>(it), 4); } for (const auto &it : this->int_array) { - dump_field(out, "int_array", it, 4); + dump_field(out, ESPHOME_PSTR("int_array"), it, 4); } for (const auto &it : this->float_array) { - dump_field(out, "float_array", it, 4); + dump_field(out, ESPHOME_PSTR("float_array"), it, 4); } for (const auto &it : this->string_array) { - dump_field(out, "string_array", it, 4); + dump_field(out, ESPHOME_PSTR("string_array"), it, 4); } return out.c_str(); } const char *ExecuteServiceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "return_response", this->return_response); + dump_field(out, ESPHOME_PSTR("return_response"), this->return_response); #endif return out.c_str(); } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES const char *ExecuteServiceResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_CAMERA const char *ListEntitiesCameraResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCameraResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageResponse"); - dump_field(out, "key", this->key); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); - dump_field(out, "done", this->done); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); + dump_field(out, ESPHOME_PSTR("done"), this->done); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageRequest"); - dump_field(out, "single", this->single); - dump_field(out, "stream", this->stream); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageRequest")); + dump_field(out, ESPHOME_PSTR("single"), this->single); + dump_field(out, ESPHOME_PSTR("stream"), this->stream); return out.c_str(); } #endif #ifdef USE_CLIMATE const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_current_temperature", this->supports_current_temperature); - dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesClimateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_current_temperature"), this->supports_current_temperature); + dump_field(out, ESPHOME_PSTR("supports_two_point_target_temperature"), this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast<enums::ClimateMode>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::ClimateMode>(it), 4); } - dump_field(out, "visual_min_temperature", this->visual_min_temperature); - dump_field(out, "visual_max_temperature", this->visual_max_temperature); - dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); - dump_field(out, "supports_action", this->supports_action); + dump_field(out, ESPHOME_PSTR("visual_min_temperature"), this->visual_min_temperature); + dump_field(out, ESPHOME_PSTR("visual_max_temperature"), this->visual_max_temperature); + dump_field(out, ESPHOME_PSTR("visual_target_temperature_step"), this->visual_target_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_action"), this->supports_action); for (const auto &it : *this->supported_fan_modes) { - dump_field(out, "supported_fan_modes", static_cast<enums::ClimateFanMode>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_fan_modes"), static_cast<enums::ClimateFanMode>(it), 4); } for (const auto &it : *this->supported_swing_modes) { - dump_field(out, "supported_swing_modes", static_cast<enums::ClimateSwingMode>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_swing_modes"), static_cast<enums::ClimateSwingMode>(it), 4); } for (const auto &it : *this->supported_custom_fan_modes) { - dump_field(out, "supported_custom_fan_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_fan_modes"), it, 4); } for (const auto &it : *this->supported_presets) { - dump_field(out, "supported_presets", static_cast<enums::ClimatePreset>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_presets"), static_cast<enums::ClimatePreset>(it), 4); } for (const auto &it : *this->supported_custom_presets) { - dump_field(out, "supported_custom_presets", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_presets"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); - dump_field(out, "supports_current_humidity", this->supports_current_humidity); - dump_field(out, "supports_target_humidity", this->supports_target_humidity); - dump_field(out, "visual_min_humidity", this->visual_min_humidity); - dump_field(out, "visual_max_humidity", this->visual_max_humidity); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("visual_current_temperature_step"), this->visual_current_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_current_humidity"), this->supports_current_humidity); + dump_field(out, ESPHOME_PSTR("supports_target_humidity"), this->supports_target_humidity); + dump_field(out, ESPHOME_PSTR("visual_min_humidity"), this->visual_min_humidity); + dump_field(out, ESPHOME_PSTR("visual_max_humidity"), this->visual_max_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *ClimateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "mode", static_cast<enums::ClimateMode>(this->mode)); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "action", static_cast<enums::ClimateAction>(this->action)); - dump_field(out, "fan_mode", static_cast<enums::ClimateFanMode>(this->fan_mode)); - dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode)); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset)); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "current_humidity", this->current_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::ClimateMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("action"), static_cast<enums::ClimateAction>(this->action)); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast<enums::ClimateFanMode>(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast<enums::ClimateSwingMode>(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("preset"), static_cast<enums::ClimatePreset>(this->preset)); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("current_humidity"), this->current_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_mode", this->has_mode); - dump_field(out, "mode", static_cast<enums::ClimateMode>(this->mode)); - dump_field(out, "has_target_temperature", this->has_target_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "has_fan_mode", this->has_fan_mode); - dump_field(out, "fan_mode", static_cast<enums::ClimateFanMode>(this->fan_mode)); - dump_field(out, "has_swing_mode", this->has_swing_mode); - dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode)); - dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "has_preset", this->has_preset); - dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset)); - dump_field(out, "has_custom_preset", this->has_custom_preset); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "has_target_humidity", this->has_target_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_mode"), this->has_mode); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::ClimateMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("has_target_temperature"), this->has_target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("has_target_temperature_low"), this->has_target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("has_target_temperature_high"), this->has_target_temperature_high); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("has_fan_mode"), this->has_fan_mode); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast<enums::ClimateFanMode>(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("has_swing_mode"), this->has_swing_mode); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast<enums::ClimateSwingMode>(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("has_custom_fan_mode"), this->has_custom_fan_mode); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("has_preset"), this->has_preset); + dump_field(out, ESPHOME_PSTR("preset"), static_cast<enums::ClimatePreset>(this->preset)); + dump_field(out, ESPHOME_PSTR("has_custom_preset"), this->has_custom_preset); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("has_target_humidity"), this->has_target_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_WATER_HEATER const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesWaterHeaterResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "min_temperature", this->min_temperature); - dump_field(out, "max_temperature", this->max_temperature); - dump_field(out, "target_temperature_step", this->target_temperature_step); + dump_field(out, ESPHOME_PSTR("min_temperature"), this->min_temperature); + dump_field(out, ESPHOME_PSTR("max_temperature"), this->max_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_step"), this->target_temperature_step); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4); } - dump_field(out, "supported_features", this->supported_features); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); return out.c_str(); } const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::WaterHeaterMode>(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_fields", this->has_fields); - dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode)); - dump_field(out, "target_temperature", this->target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_fields"), this->has_fields); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::WaterHeaterMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } #endif #ifdef USE_NUMBER const char *ListEntitiesNumberResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesNumberResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "min_value", this->min_value); - dump_field(out, "max_value", this->max_value); - dump_field(out, "step", this->step); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "mode", static_cast<enums::NumberMode>(this->mode)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("min_value"), this->min_value); + dump_field(out, ESPHOME_PSTR("max_value"), this->max_value); + dump_field(out, ESPHOME_PSTR("step"), this->step); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::NumberMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SELECT const char *ListEntitiesSelectResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSelectResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif for (const auto &it : *this->options) { - dump_field(out, "options", it, 4); + dump_field(out, ESPHOME_PSTR("options"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SIREN const char *ListEntitiesSirenResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSirenResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); for (const auto &it : *this->tones) { - dump_field(out, "tones", it, 4); + dump_field(out, ESPHOME_PSTR("tones"), it, 4); } - dump_field(out, "supports_duration", this->supports_duration); - dump_field(out, "supports_volume", this->supports_volume); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_duration"), this->supports_duration); + dump_field(out, ESPHOME_PSTR("supports_volume"), this->supports_volume); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_tone", this->has_tone); - dump_field(out, "tone", this->tone); - dump_field(out, "has_duration", this->has_duration); - dump_field(out, "duration", this->duration); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_tone"), this->has_tone); + dump_field(out, ESPHOME_PSTR("tone"), this->tone); + dump_field(out, ESPHOME_PSTR("has_duration"), this->has_duration); + dump_field(out, ESPHOME_PSTR("duration"), this->duration); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LOCK const char *ListEntitiesLockResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLockResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLockResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_open", this->supports_open); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "code_format", this->code_format); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_open"), this->supports_open); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("code_format"), this->code_format); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast<enums::LockState>(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast<enums::LockState>(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast<enums::LockCommand>(this->command)); - dump_field(out, "has_code", this->has_code); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast<enums::LockCommand>(this->command)); + dump_field(out, ESPHOME_PSTR("has_code"), this->has_code); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BUTTON const char *ListEntitiesButtonResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesButtonResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ButtonCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ButtonCommandRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ButtonCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_MEDIA_PLAYER const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); - dump_field(out, "format", this->format); - dump_field(out, "sample_rate", this->sample_rate); - dump_field(out, "num_channels", this->num_channels); - dump_field(out, "purpose", static_cast<enums::MediaPlayerFormatPurpose>(this->purpose)); - dump_field(out, "sample_bytes", this->sample_bytes); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerSupportedFormat")); + dump_field(out, ESPHOME_PSTR("format"), this->format); + dump_field(out, ESPHOME_PSTR("sample_rate"), this->sample_rate); + dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels); + dump_field(out, ESPHOME_PSTR("purpose"), static_cast<enums::MediaPlayerFormatPurpose>(this->purpose)); + dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes); return out.c_str(); } const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesMediaPlayerResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "supports_pause", this->supports_pause); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause); for (const auto &it : this->supported_formats) { - out.append(" supported_formats: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *MediaPlayerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast<enums::MediaPlayerState>(this->state)); - dump_field(out, "volume", this->volume); - dump_field(out, "muted", this->muted); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast<enums::MediaPlayerState>(this->state)); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("muted"), this->muted); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *MediaPlayerCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_command", this->has_command); - dump_field(out, "command", static_cast<enums::MediaPlayerCommand>(this->command)); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); - dump_field(out, "has_media_url", this->has_media_url); - dump_field(out, "media_url", this->media_url); - dump_field(out, "has_announcement", this->has_announcement); - dump_field(out, "announcement", this->announcement); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_command"), this->has_command); + dump_field(out, ESPHOME_PSTR("command"), static_cast<enums::MediaPlayerCommand>(this->command)); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("has_media_url"), this->has_media_url); + dump_field(out, ESPHOME_PSTR("media_url"), this->media_url); + dump_field(out, ESPHOME_PSTR("has_announcement"), this->has_announcement); + dump_field(out, ESPHOME_PSTR("announcement"), this->announcement); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *SubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeBluetoothLEAdvertisementsRequest")); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *BluetoothLERawAdvertisement::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); - dump_field(out, "address", this->address); - dump_field(out, "rssi", this->rssi); - dump_field(out, "address_type", this->address_type); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisement")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("rssi"), this->rssi); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothLERawAdvertisementsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisementsResponse")); for (uint16_t i = 0; i < this->advertisements_len; i++) { - out.append(" advertisements: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("advertisements")).append(": "); this->advertisements[i].dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothDeviceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceRequest"); - dump_field(out, "address", this->address); - dump_field(out, "request_type", static_cast<enums::BluetoothDeviceRequestType>(this->request_type)); - dump_field(out, "has_address_type", this->has_address_type); - dump_field(out, "address_type", this->address_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("request_type"), static_cast<enums::BluetoothDeviceRequestType>(this->request_type)); + dump_field(out, ESPHOME_PSTR("has_address_type"), this->has_address_type); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); return out.c_str(); } const char *BluetoothDeviceConnectionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); - dump_field(out, "address", this->address); - dump_field(out, "connected", this->connected); - dump_field(out, "mtu", this->mtu); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceConnectionResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("connected"), this->connected); + dump_field(out, ESPHOME_PSTR("mtu"), this->mtu); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTGetServicesRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTDescriptor::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTDescriptor")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTCharacteristic::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTCharacteristic")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "properties", this->properties); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("properties"), this->properties); for (const auto &it : this->descriptors) { - out.append(" descriptors: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("descriptors")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTService::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTService"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTService")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); for (const auto &it : this->characteristics) { - out.append(" characteristics: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("characteristics")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTGetServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); for (const auto &it : this->services) { - out.append(" services: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("services")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothGATTGetServicesDoneResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesDoneResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTReadRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTReadResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothGATTWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "response", this->response); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("response"), this->response); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTReadDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTWriteDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTNotifyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "enable", this->enable); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("enable"), this->enable); return out.c_str(); } const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyDataResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); - dump_field(out, "free", this->free); - dump_field(out, "limit", this->limit); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothConnectionsFreeResponse")); + dump_field(out, ESPHOME_PSTR("free"), this->free); + dump_field(out, ESPHOME_PSTR("limit"), this->limit); for (const auto &it : this->allocated) { - dump_field(out, "allocated", it, 4); + dump_field(out, ESPHOME_PSTR("allocated"), it, 4); } return out.c_str(); } const char *BluetoothGATTErrorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTErrorResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTWriteResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTNotifyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothDevicePairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "paired", this->paired); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDevicePairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("paired"), this->paired); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceUnpairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceClearCacheResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothScannerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); - dump_field(out, "state", static_cast<enums::BluetoothScannerState>(this->state)); - dump_field(out, "mode", static_cast<enums::BluetoothScannerMode>(this->mode)); - dump_field(out, "configured_mode", static_cast<enums::BluetoothScannerMode>(this->configured_mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerStateResponse")); + dump_field(out, ESPHOME_PSTR("state"), static_cast<enums::BluetoothScannerState>(this->state)); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::BluetoothScannerMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("configured_mode"), static_cast<enums::BluetoothScannerMode>(this->configured_mode)); return out.c_str(); } const char *BluetoothScannerSetModeRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); - dump_field(out, "mode", static_cast<enums::BluetoothScannerMode>(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerSetModeRequest")); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::BluetoothScannerMode>(this->mode)); return out.c_str(); } #endif #ifdef USE_VOICE_ASSISTANT const char *SubscribeVoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); - dump_field(out, "subscribe", this->subscribe); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeVoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("subscribe"), this->subscribe); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *VoiceAssistantAudioSettings::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); - dump_field(out, "noise_suppression_level", this->noise_suppression_level); - dump_field(out, "auto_gain", this->auto_gain); - dump_field(out, "volume_multiplier", this->volume_multiplier); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudioSettings")); + dump_field(out, ESPHOME_PSTR("noise_suppression_level"), this->noise_suppression_level); + dump_field(out, ESPHOME_PSTR("auto_gain"), this->auto_gain); + dump_field(out, ESPHOME_PSTR("volume_multiplier"), this->volume_multiplier); return out.c_str(); } const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantRequest"); - dump_field(out, "start", this->start); - dump_field(out, "conversation_id", this->conversation_id); - dump_field(out, "flags", this->flags); - out.append(" audio_settings: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("start"), this->start); + dump_field(out, ESPHOME_PSTR("conversation_id"), this->conversation_id); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); + out.append(2, ' ').append_p(ESPHOME_PSTR("audio_settings")).append(": "); this->audio_settings.dump_to(out); out.append("\n"); - dump_field(out, "wake_word_phrase", this->wake_word_phrase); + dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase); return out.c_str(); } const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantResponse"); - dump_field(out, "port", this->port); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantResponse")); + dump_field(out, ESPHOME_PSTR("port"), this->port); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *VoiceAssistantEventData::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventData"); - dump_field(out, "name", this->name); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventData")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *VoiceAssistantEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); - dump_field(out, "event_type", static_cast<enums::VoiceAssistantEvent>(this->event_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast<enums::VoiceAssistantEvent>(this->event_type)); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantAudio::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudio"); - dump_bytes_field(out, "data", this->data, this->data_len); - dump_field(out, "end", this->end); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudio")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); + dump_field(out, ESPHOME_PSTR("end"), this->end); return out.c_str(); } const char *VoiceAssistantTimerEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); - dump_field(out, "event_type", static_cast<enums::VoiceAssistantTimerEvent>(this->event_type)); - dump_field(out, "timer_id", this->timer_id); - dump_field(out, "name", this->name); - dump_field(out, "total_seconds", this->total_seconds); - dump_field(out, "seconds_left", this->seconds_left); - dump_field(out, "is_active", this->is_active); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantTimerEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast<enums::VoiceAssistantTimerEvent>(this->event_type)); + dump_field(out, ESPHOME_PSTR("timer_id"), this->timer_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("total_seconds"), this->total_seconds); + dump_field(out, ESPHOME_PSTR("seconds_left"), this->seconds_left); + dump_field(out, ESPHOME_PSTR("is_active"), this->is_active); return out.c_str(); } const char *VoiceAssistantAnnounceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); - dump_field(out, "media_id", this->media_id); - dump_field(out, "text", this->text); - dump_field(out, "preannounce_media_id", this->preannounce_media_id); - dump_field(out, "start_conversation", this->start_conversation); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceRequest")); + dump_field(out, ESPHOME_PSTR("media_id"), this->media_id); + dump_field(out, ESPHOME_PSTR("text"), this->text); + dump_field(out, ESPHOME_PSTR("preannounce_media_id"), this->preannounce_media_id); + dump_field(out, ESPHOME_PSTR("start_conversation"), this->start_conversation); return out.c_str(); } const char *VoiceAssistantAnnounceFinished::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceFinished")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } const char *VoiceAssistantWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } return out.c_str(); } const char *VoiceAssistantExternalWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantExternalWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } - dump_field(out, "model_type", this->model_type); - dump_field(out, "model_size", this->model_size); - dump_field(out, "model_hash", this->model_hash); - dump_field(out, "url", this->url); + dump_field(out, ESPHOME_PSTR("model_type"), this->model_type); + dump_field(out, ESPHOME_PSTR("model_size"), this->model_size); + dump_field(out, ESPHOME_PSTR("model_hash"), this->model_hash); + dump_field(out, ESPHOME_PSTR("url"), this->url); return out.c_str(); } const char *VoiceAssistantConfigurationRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationRequest")); for (const auto &it : this->external_wake_words) { - out.append(" external_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("external_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantConfigurationResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationResponse")); for (const auto &it : this->available_wake_words) { - out.append(" available_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("available_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : *this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } - dump_field(out, "max_active_wake_words", this->max_active_wake_words); + dump_field(out, ESPHOME_PSTR("max_active_wake_words"), this->max_active_wake_words); return out.c_str(); } const char *VoiceAssistantSetConfiguration::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantSetConfiguration")); for (const auto &it : this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } return out.c_str(); } #endif #ifdef USE_ALARM_CONTROL_PANEL const char *ListEntitiesAlarmControlPanelResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesAlarmControlPanelResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "supported_features", this->supported_features); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("requires_code_to_arm"), this->requires_code_to_arm); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast<enums::AlarmControlPanelState>(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast<enums::AlarmControlPanelState>(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast<enums::AlarmControlPanelStateCommand>(this->command)); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast<enums::AlarmControlPanelStateCommand>(this->command)); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT const char *ListEntitiesTextResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "min_length", this->min_length); - dump_field(out, "max_length", this->max_length); - dump_field(out, "pattern", this->pattern); - dump_field(out, "mode", static_cast<enums::TextMode>(this->mode)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("min_length"), this->min_length); + dump_field(out, ESPHOME_PSTR("max_length"), this->max_length); + dump_field(out, ESPHOME_PSTR("pattern"), this->pattern); + dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::TextMode>(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATE const char *ListEntitiesDateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_TIME const char *ListEntitiesTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_EVENT const char *ListEntitiesEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesEventResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesEventResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); for (const auto &it : *this->event_types) { - dump_field(out, "event_types", it, 4); + dump_field(out, ESPHOME_PSTR("event_types"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *EventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "EventResponse"); - dump_field(out, "key", this->key); - dump_field(out, "event_type", this->event_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("EventResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("event_type"), this->event_type); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_VALVE const char *ListEntitiesValveResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesValveResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesValveResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "current_operation", static_cast<enums::ValveOperation>(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast<enums::ValveOperation>(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATETIME const char *ListEntitiesDateTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_UPDATE const char *ListEntitiesUpdateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesUpdateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "in_progress", this->in_progress); - dump_field(out, "has_progress", this->has_progress); - dump_field(out, "progress", this->progress); - dump_field(out, "current_version", this->current_version); - dump_field(out, "latest_version", this->latest_version); - dump_field(out, "title", this->title); - dump_field(out, "release_summary", this->release_summary); - dump_field(out, "release_url", this->release_url); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("in_progress"), this->in_progress); + dump_field(out, ESPHOME_PSTR("has_progress"), this->has_progress); + dump_field(out, ESPHOME_PSTR("progress"), this->progress); + dump_field(out, ESPHOME_PSTR("current_version"), this->current_version); + dump_field(out, ESPHOME_PSTR("latest_version"), this->latest_version); + dump_field(out, ESPHOME_PSTR("title"), this->title); + dump_field(out, ESPHOME_PSTR("release_summary"), this->release_summary); + dump_field(out, ESPHOME_PSTR("release_url"), this->release_url); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast<enums::UpdateCommand>(this->command)); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast<enums::UpdateCommand>(this->command)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_ZWAVE_PROXY const char *ZWaveProxyFrame::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyFrame"); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyFrame")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyRequest"); - dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type)); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequest")); + dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZWaveProxyRequestType>(this->type)); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } #endif #ifdef USE_INFRARED const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesInfraredResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesInfraredResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "capabilities", this->capabilities); + dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); return out.c_str(); } #endif #ifdef USE_IR_RF const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); - dump_field(out, "carrier_frequency", this->carrier_frequency); - dump_field(out, "repeat_count", this->repeat_count); - out.append(" timings: "); - out.append("packed buffer ["); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("carrier_frequency"), this->carrier_frequency); + dump_field(out, ESPHOME_PSTR("repeat_count"), this->repeat_count); + out.append(2, ' ').append_p(ESPHOME_PSTR("timings")).append(": "); + out.append_p(ESPHOME_PSTR("packed buffer [")); append_uint(out, this->timings_count_); - out.append(" values, "); + out.append_p(ESPHOME_PSTR(" values, ")); append_uint(out, this->timings_length_); - out.append(" bytes]\n"); + out.append_p(ESPHOME_PSTR(" bytes]\n")); return out.c_str(); } const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFReceiveEvent"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFReceiveEvent")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : *this->timings) { - dump_field(out, "timings", it, 4); + dump_field(out, ESPHOME_PSTR("timings"), it, 4); } return out.c_str(); } #endif #ifdef USE_SERIAL_PROXY const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "baudrate", this->baudrate); - dump_field(out, "flow_control", this->flow_control); - dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity)); - dump_field(out, "stop_bits", this->stop_bits); - dump_field(out, "data_size", this->data_size); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("baudrate"), this->baudrate); + dump_field(out, ESPHOME_PSTR("flow_control"), this->flow_control); + dump_field(out, ESPHOME_PSTR("parity"), static_cast<enums::SerialProxyParity>(this->parity)); + dump_field(out, ESPHOME_PSTR("stop_bits"), this->stop_bits); + dump_field(out, ESPHOME_PSTR("data_size"), this->data_size); return out.c_str(); } const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyDataReceived"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyDataReceived")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyWriteRequest"); - dump_field(out, "instance", this->instance); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyWriteRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); - dump_field(out, "instance", this->instance); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); return out.c_str(); } const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "line_states", this->line_states); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); return out.c_str(); } const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequest"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::SerialProxyRequestType>(this->type)); return out.c_str(); } const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SerialProxyRequestResponse"); - dump_field(out, "instance", this->instance); - dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type)); - dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status)); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequestResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::SerialProxyRequestType>(this->type)); + dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status)); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); - dump_field(out, "address", this->address); - dump_field(out, "min_interval", this->min_interval); - dump_field(out, "max_interval", this->max_interval); - dump_field(out, "latency", this->latency); - dump_field(out, "timeout", this->timeout); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("min_interval"), this->min_interval); + dump_field(out, ESPHOME_PSTR("max_interval"), this->max_interval); + dump_field(out, ESPHOME_PSTR("latency"), this->latency); + dump_field(out, ESPHOME_PSTR("timeout"), this->timeout); return out.c_str(); } const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); - dump_field(out, "address", this->address); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d86cf912db9..b41233eddd1 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -9,8 +9,8 @@ namespace esphome::api { static const char *const TAG = "api.service"; #ifdef HAS_PROTO_MESSAGE_DUMP -void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) { - ESP_LOGVV(TAG, "send_message %s: %s", name, dump); +void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { + ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 4925a6497a3..6ff988902f3 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -12,7 +12,7 @@ class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: - void log_send_message_(const char *name, const char *dump); + void log_send_message_(const LogString *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); void log_receive_message_(const LogString *name); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6f9c947d71..95a79105a32 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -5,6 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include <cassert> @@ -400,6 +401,23 @@ class DumpBuffer { return *this; } + /// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms) + DumpBuffer &append_p(const char *str) { + if (str) { +#ifdef USE_ESP8266 + append_p_esp8266(str); +#else + append_impl_(str, strlen(str)); +#endif + } + return *this; + } + +#ifdef USE_ESP8266 + /// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site + void append_p_esp8266(const char *str); +#endif + const char *c_str() const { return buf_; } size_t size() const { return pos_; } @@ -445,7 +463,7 @@ class ProtoMessage { uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; - virtual const char *message_name() const { return "unknown"; } + virtual const LogString *message_name() const { return LOG_STR("unknown"); } #endif #ifndef USE_HOST diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 0bb569fdb54..81ea93caf43 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -248,7 +248,7 @@ class TypeInfo(ABC): @property def dump_content(self) -> str: # Default implementation - subclasses can override if they need special handling - return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), {self.dump_field_value(f"this->{self.field_name}")});' @abstractmethod def dump(self, name: str) -> str: @@ -665,14 +665,14 @@ class StringType(TypeInfo): def dump_content(self) -> str: # For SOURCE_CLIENT only, use std::string if not self._needs_encode: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' # For SOURCE_SERVER, use StringRef with _ref_ suffix if not self._needs_decode: - return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name}_ref_);' # For SOURCE_BOTH, we need custom logic - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += self.dump(f"this->{self.field_name}") + "\n" o += 'out.append("\\n");' return o @@ -745,7 +745,7 @@ class MessageType(TypeInfo): @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' + o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += f"this->{self.field_name}.dump_to(out);\n" o += 'out.append("\\n");' return o @@ -831,7 +831,7 @@ class BytesType(TypeInfo): # For SOURCE_CLIENT only, always use std::string if not self._needs_encode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), " f"this->{self.field_name}.size());" ) @@ -839,17 +839,17 @@ class BytesType(TypeInfo): # For SOURCE_SERVER, always use pointer/length if not self._needs_decode: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);" ) # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) return ( f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);\n" f"}} else {{\n" - f' dump_bytes_field(out, "{self.name}", ' + f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), " f"this->{self.field_name}.size());\n" f"}}" @@ -928,7 +928,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -976,7 +976,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: - return f'dump_field(out, "{self.name}", this->{self.field_name});' + return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" @@ -1036,12 +1036,12 @@ class PackedBufferTypeInfo(TypeInfo): 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(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' + + 'out.append_p(ESPHOME_PSTR("packed buffer ["));\n' + f"append_uint(out, this->{self.field_name}_count_);\n" - + 'out.append(" values, ");\n' + + 'out.append_p(ESPHOME_PSTR(" values, "));\n' + f"append_uint(out, this->{self.field_name}_length_);\n" - + 'out.append(" bytes]\\n");' + + 'out.append_p(ESPHOME_PSTR(" bytes]\\n"));' ) def dump(self, name: str) -> str: @@ -1134,7 +1134,7 @@ class FixedArrayBytesType(TypeInfo): @property def dump_content(self) -> str: return ( - f'dump_bytes_field(out, "{self.name}", ' + f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), ' f"this->{self.field_name}, this->{self.field_name}_len);" ) @@ -1204,7 +1204,7 @@ class EnumType(TypeInfo): return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));" def dump(self, name: str) -> str: - return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" + return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" def dump_field_value(self, value: str) -> str: # Enums need explicit cast for the template @@ -1326,15 +1326,15 @@ def _generate_array_dump_content( # Check if underlying type can use dump_field if is_const_char_ptr: # Special case for const char* - use it directly - o += f' dump_field(out, "{name}", it, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), it, 4);\n' elif ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent # std::vector<bool> iterators return proxy objects, need explicit cast value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it") - o += f' dump_field(out, "{name}", {value_expr}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{name}"), {value_expr}, 4);\n' else: # For complex types (messages, bytes), use the old pattern - o += f' out.append(" {name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{name}")).append(": ");\n' o += indent(ti.dump("it")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -1543,9 +1543,9 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" # Check if underlying type can use dump_field if self._ti.can_use_dump_field(): - o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' + o += f' dump_field(out, ESPHOME_PSTR("{self.name}"), {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' else: - o += f' out.append(" {self.name}: ");\n' + o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n' o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" o += ' out.append("\\n");\n' o += "}" @@ -2023,9 +2023,9 @@ def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: dump_cpp += " switch (value) {\n" for v in desc.value: dump_cpp += f" case enums::{v.name}:\n" - dump_cpp += f' return "{v.name}";\n' + dump_cpp += f' return ESPHOME_PSTR("{v.name}");\n' dump_cpp += " default:\n" - dump_cpp += ' return "UNKNOWN";\n' + dump_cpp += ' return ESPHOME_PSTR("UNKNOWN");\n' dump_cpp += " }\n" dump_cpp += "}\n" @@ -2107,7 +2107,7 @@ def build_message_type( public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") snake_name = camel_to_snake(desc.name) public_content.append( - f'const char *message_name() const override {{ return "{snake_name}"; }}' + f'const LogString *message_name() const override {{ return LOG_STR("{snake_name}"); }}' ) public_content.append("#endif") @@ -2315,12 +2315,12 @@ def build_message_type( if dump: # Always use MessageDumpHelper for consistent output formatting dump_impl += "\n" - dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' + dump_impl += f' MessageDumpHelper helper(out, ESPHOME_PSTR("{desc.name}"));\n' dump_impl += indent("\n".join(dump)) + "\n" dump_impl += " return out.c_str();\n" else: dump_impl += "\n" - dump_impl += f' out.append("{desc.name} {{}}");\n' + dump_impl += f' out.append_p(ESPHOME_PSTR("{desc.name} {{}}"));\n' dump_impl += " return out.c_str();\n" dump_impl += "}\n" @@ -2707,6 +2707,7 @@ namespace esphome::api { dump_cpp += """\ #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include <cinttypes> @@ -2714,6 +2715,21 @@ namespace esphome::api { namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); @@ -2724,8 +2740,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { @@ -2733,10 +2750,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) { } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -2746,6 +2764,10 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { append_field_prefix(out, field_name, indent); out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); @@ -2790,21 +2812,23 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\\n"); } -template<typename T> -static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { +// proto_enum_to_string returns PROGMEM pointers, so use append_p +template<typename T> static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string<T>(value)); + out.append_p(proto_enum_to_string<T>(value)); out.append("\\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); out.append(hex_buf).append("\\n"); } +#pragma GCC diagnostic pop """ @@ -2977,7 +3001,7 @@ static const char *const TAG = "api.service"; # Add logging helper method declarations hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += " protected:\n" - hpp += " void log_send_message_(const char *name, const char *dump);\n" + hpp += " void log_send_message_(const LogString *name, const char *dump);\n" hpp += ( " void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n" ) @@ -2990,10 +3014,8 @@ static const char *const TAG = "api.service"; # Add logging helper method implementations to cpp cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" - cpp += ( - f"void {class_name}::log_send_message_(const char *name, const char *dump) {{\n" - ) - cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", name, dump);\n' + cpp += f"void {class_name}::log_send_message_(const LogString *name, const char *dump) {{\n" + cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);\n' cpp += "}\n" cpp += f"void {class_name}::log_receive_message_(const LogString *name, const ProtoMessage &msg) {{\n" cpp += " DumpBuffer dump_buf;\n" From 13d3968d9b9220fd777b953aac8efbbba1819013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:41:09 -1000 Subject: [PATCH 0955/2056] [api] Avoid heap allocation in PSK update timeout lambda (#14921) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/api_server.cpp | 28 ++++++++++++++++++--------- esphome/components/api/api_server.h | 4 +++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17d69405adf..1151bc5983d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -46,10 +46,8 @@ void APIServer::setup() { #ifndef USE_API_NOISE_PSK_FROM_YAML // Only load saved PSK if not set from YAML - SavedNoisePsk noise_pref_saved{}; - if (this->noise_pref_.load(&noise_pref_saved)) { + if (this->load_and_apply_noise_psk_()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } #endif #endif @@ -514,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, - const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { + const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); return false; @@ -526,9 +524,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); if (make_active) { - this->set_timeout(100, [this, active_psk]() { + this->set_timeout(100, [this]() { + // Re-read the PSK from preferences rather than capturing the 32-byte array + // in the lambda (which would exceed std::function SBO and heap-allocate). + if (!this->load_and_apply_noise_psk_()) { + ESP_LOGW(TAG, "Failed to load saved PSK for activation"); + return; + } ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; c->send_message(req); @@ -538,6 +541,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString return true; } +bool APIServer::load_and_apply_noise_psk_() { + SavedNoisePsk saved{}; + if (!this->noise_pref_.load(&saved)) + return false; + this->set_noise_psk(saved.psk); + return true; +} + bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_API_NOISE_PSK_FROM_YAML // When PSK is set from YAML, this function should never be called @@ -552,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } SavedNoisePsk new_saved_psk{psk}; - return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, + return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), make_active); #endif } @@ -564,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) { return false; #else SavedNoisePsk empty_psk{}; - psk_t empty{}; - return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, + return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); #endif } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index ccba6deb00a..65076879a24 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -239,7 +239,9 @@ class APIServer final : public Component, #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, - const psk_t &active_psk, bool make_active); + bool make_active); + // Load saved PSK from preferences and apply it. Returns true on success. + bool load_and_apply_noise_psk_(); #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication From a3d9854704a7c448908847f95ea5179e54094547 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:56:36 -1000 Subject: [PATCH 0956/2056] [gpio] Remove redundant last_state_ and pack GPIOBinarySensor fields (#15113) --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 26 +++++++++---------- .../gpio/binary_sensor/gpio_binary_sensor.h | 20 +++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 38ebbc90e4b..39b1a2f7132 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -23,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) { void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); - if (new_state != arg->last_state_) { + if (new_state != arg->state_) { arg->state_ = new_state; - arg->last_state_ = new_state; arg->changed_ = true; // Wake up the component from its disabled loop state if (arg->component_ != nullptr) { @@ -34,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { } } -void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { pin->setup(); this->isr_pin_ = pin->to_isr(); this->component_ = component; // Read initial state - this->last_state_ = pin->digital_read(); - this->state_ = this->last_state_; + this->state_ = pin->digital_read(); // Attach interrupt - from this point on, any changes will be caught by the interrupt - pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_); } void GPIOBinarySensor::setup() { - if (this->use_interrupt_ && !this->pin_->is_internal()) { + if (this->store_.use_interrupt_ && !this->pin_->is_internal()) { ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); - this->use_interrupt_ = false; + this->store_.use_interrupt_ = false; } - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_); - this->store_.setup(internal_pin, this->interrupt_type_, this); + this->store_.setup(internal_pin, this); this->publish_initial_state(this->store_.get_state()); } else { this->pin_->setup(); @@ -66,14 +64,14 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); - if (this->use_interrupt_) { - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); + if (this->store_.use_interrupt_) { + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } } void GPIOBinarySensor::loop() { - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { if (this->store_.is_changed()) { // Clear the flag immediately to minimize the window where we might miss changes this->store_.clear_changed(); diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 8b1cc296134..24efc2a0e60 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -8,10 +8,10 @@ namespace esphome { namespace gpio { -// Store class for ISR data (no vtables, ISR-safe) +// Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { public: - void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + void setup(InternalGPIOPin *pin, Component *component); static void gpio_intr(GPIOBinarySensorStore *arg); @@ -32,11 +32,13 @@ class GPIOBinarySensorStore { } protected: + friend class GPIOBinarySensor; ISRInternalGPIOPin isr_pin_; - volatile bool state_{false}; - volatile bool last_state_{false}; - volatile bool changed_{false}; Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() + volatile bool state_{false}; + volatile bool changed_{false}; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; }; class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { @@ -44,9 +46,9 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. - void set_pin(GPIOPin *pin) { pin_ = pin; } - void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } - void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } + void set_pin(GPIOPin *pin) { this->pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -59,8 +61,6 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon protected: GPIOPin *pin_; - bool use_interrupt_{true}; - gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; GPIOBinarySensorStore store_; }; From 8ad8f89e504bb93273ff6e703c6113458ae43536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:56:53 -1000 Subject: [PATCH 0957/2056] [light] Reorder LightState fields to eliminate padding (#15112) --- esphome/components/light/light_state.h | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b8d72cc8325..ab7f2e4df85 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -322,22 +322,6 @@ class LightState : public EntityBase, public Component { FixedVector<LightEffect *> effects_; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; - /// Value for storing the index of the currently active effect. 0 if no effect is active - uint32_t active_effect_index_{}; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; - /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; - /// Gamma correction factor for the light. - float gamma_correct_{}; -#ifdef USE_LIGHT_GAMMA_LUT - const uint16_t *gamma_table_{nullptr}; -#endif // USE_LIGHT_GAMMA_LUT - - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; /** Listeners for remote values changes. * @@ -361,6 +345,22 @@ class LightState : public EntityBase, public Component { /// Initial state of the light. optional<LightStateRTCState> initial_state_{}; + /// Value for storing the index of the currently active effect. 0 if no effect is active + uint32_t active_effect_index_{}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; + /// Gamma correction factor for the light. + float gamma_correct_{}; +#ifdef USE_LIGHT_GAMMA_LUT + const uint16_t *gamma_table_{nullptr}; +#endif // USE_LIGHT_GAMMA_LUT + + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_{false}; /// Restore mode of the light. LightRestoreMode restore_mode_; }; From 69911c3db1828e3d7e9447cf92f10d728eaaa543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:58:36 -1000 Subject: [PATCH 0958/2056] [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) --- .../components/wifi/wifi_component_esp8266.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index f2fabb9080f..03800cc3a92 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -664,11 +664,22 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + // Use shorter dwell times for roaming scans - we only need to detect strong + // nearby APs, not do a thorough survey. This also reduces off-channel time + // which can cause Beacon Timeout disconnects on some APs. + // Roaming times match the ESP32 IDF scan defaults. + static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300; + static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400; + static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100; + static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300; + bool roaming = this->roaming_state_ == RoamingState::SCANNING; if (passive) { - config.scan_time.passive = 500; + config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS; } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; + config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } #endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); From df4318505f79ce12c15bd848c3a89ae59aa1c9e9 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:28:04 +0100 Subject: [PATCH 0959/2056] [substitutions] refactor substitute() as a pure function (package refactor part 3) (#15031) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/packages/__init__.py | 9 +- esphome/components/substitutions/__init__.py | 93 ++++++++------------ tests/unit_tests/test_substitutions.py | 46 ++++++++-- 3 files changed, 82 insertions(+), 66 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 793cb946dd4..f9bdb677a7a 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -300,9 +300,14 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: context_vars = package_config.vars if CONF_PACKAGES in package_config or CONF_URL in package_config: # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import substitute_context_vars + from esphome.components.substitutions import ContextVars, substitute - substitute_context_vars(package_config, context_vars) + package_config = substitute( + package_config, + [], + ContextVars(context_vars), + strict_undefined=False, + ) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, str): return package_config # Jinja string, skip processing diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ecee816ce91..aab1712b652 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -81,12 +81,6 @@ def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBa return value -def _try_substitute(value: Any, context: ContextVars) -> Any: - """Substitute variables in value, returning the result or the original if unchanged.""" - result = _substitute_item(value, [], context, strict_undefined=True) - return result if result is not None else value - - def _resolve_var(name: str, context_vars: ContextVars) -> Any: """Look up a substitution variable, falling back to the resolver callback.""" sub = context_vars.get(name, Missing) @@ -253,7 +247,7 @@ def _push_context( if value is Missing: return Missing try: - value = _try_substitute(value, resolver_context) + value = substitute(value, [], resolver_context, True) except UndefinedError as err: unresolvables[key] = (value, err) return Missing @@ -297,68 +291,51 @@ def push_context( return parent_context -def _substitute_item( +def substitute( item: Any, path: SubstitutionPath, parent_context: ContextVars, strict_undefined: bool, errors: ErrList | None = None, -) -> Any | None: - """Recursively substitute variables in a config item. +) -> Any: + """Returns a recursively substituted version of `item`.""" - Walks dicts, lists, strings, Lambdas, Extend, and Remove nodes, - replacing variable references with values from context_vars. - Mutates containers in-place; returns a replacement value for - strings/scalars, or None if the item was unchanged. - """ + if isinstance(item, ESPLiteralValue): + return item # do not substitute inside literal blocks - def _walk(item: Any, path: SubstitutionPath, parent_ctx: ContextVars) -> Any | None: - if isinstance(item, ESPLiteralValue): - return None # do not substitute inside literal blocks + # Push the current item's context onto the context stack + context_vars = push_context(item, parent_context, errors) - ctx = push_context(item, parent_ctx, errors) + result = item - if isinstance(item, list): - for idx, it in enumerate(item): - sub = _walk(it, path + [idx], ctx) - if sub is not None: - item[idx] = sub - elif isinstance(item, dict): - replace_keys: list[tuple[str, Any]] = [] - for k, v in item.items(): - if path or k != CONF_SUBSTITUTIONS: - sub = _walk(k, path + [k], ctx) - if sub is not None: - replace_keys.append((k, sub)) - sub = _walk(v, path + [k], ctx) - if sub is not None: - item[k] = sub - for old, new in replace_keys: - if str(new) == str(old): - item[new] = item[old] - else: - item[new] = merge_config(item.get(new), item.get(old)) - del item[old] - elif isinstance(item, str): - sub = _expand_substitutions(item, path, ctx, strict_undefined, errors) - if not isinstance(sub, str) or sub != item: - return sub - elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: - sub = _expand_substitutions(item.value, path, ctx, strict_undefined, errors) - if sub != item.value: - item.value = sub - return None + if isinstance(item, list): + result = [ + substitute(it, path + [i], context_vars, strict_undefined, errors) + for i, it in enumerate(item) + ] - return _walk(item, path, parent_context) + elif isinstance(item, dict): + result = OrderedDict() + for k, v in item.items(): + v = substitute(v, path + [k], context_vars, strict_undefined, errors) + k = substitute(k, path + [k], context_vars, strict_undefined, errors) + result[k] = merge_config(result.get(k), v) + elif isinstance(item, str): + result = _expand_substitutions( + item, path, context_vars, strict_undefined, errors + ) -def substitute_context_vars(node: Any, context_vars: dict[str, Any]) -> None: - """Eagerly substitute context vars into a config node in-place. + elif isinstance(item, (core.Lambda, Extend, Remove)) and item.value: + value = _expand_substitutions( + item.value, path, context_vars, strict_undefined, errors + ) + if item.value != value: + result = type(item)(value) - Undefined variables are silently ignored — this is used before - the main substitution pass when not all variables are visible yet. - """ - _substitute_item(node, [], ContextVars(context_vars), strict_undefined=False) + if isinstance(item, ESPHomeDataBase): + result = make_data_base(result, item) + return result def _warn_unresolved_variables(errors: ErrList) -> None: @@ -387,7 +364,7 @@ def do_substitution_pass( Extracts the ``substitutions:`` block, merges in any command-line overrides, resolves inter-variable dependencies, then walks the config tree replacing all ``$var`` / ``${expr}`` references. - Returns the (mutated) config dict with resolved substitutions + Returns a new config dict with resolved substitutions restored at the front. """ # Extract substitutions from config, overriding with substitutions coming from command line: @@ -415,7 +392,7 @@ def do_substitution_pass( errors: ErrList = [] # Collect undefined errors during substitution parent_context, substitutions = _push_context(substitutions, ContextVars(), errors) - _substitute_item(config, [], parent_context, False, errors) + config = substitute(config, [], parent_context, False, errors) if errors: _warn_unresolved_variables(errors) diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index db46a27dfb3..30478f9521d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -550,8 +550,8 @@ def test_lambda_substitution() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value == "return 42;" + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value == "return 42;" def test_lambda_no_substitution_unchanged() -> None: @@ -564,8 +564,8 @@ def test_lambda_no_substitution_unchanged() -> None: "lambda": lam, } ) - substitutions.do_substitution_pass(config) - assert lam.value is original_value + config = substitutions.do_substitution_pass(config) + assert config["lambda"].value is original_value def test_extend_substitution() -> None: @@ -577,8 +577,42 @@ def test_extend_substitution() -> None: "sensor": ext, } ) - substitutions.do_substitution_pass(config) - assert ext.value == "my_sensor" + config = substitutions.do_substitution_pass(config) + assert config["sensor"].value == "my_sensor" + + +def test_substitute_does_not_mutate_input() -> None: + """substitute() must return a new tree without modifying the original.""" + inner_list = ["${var}", "static"] + inner_dict = OrderedDict({"key": "${var}"}) + lam = Lambda("return ${var};") + config = OrderedDict( + { + "a_list": inner_list, + "a_dict": inner_dict, + "a_lambda": lam, + "plain": "${var}", + } + ) + context = substitutions.ContextVars({"var": "replaced"}) + result = substitutions.substitute(config, [], context, strict_undefined=True) + + # Result has substitutions applied + assert result["plain"] == "replaced" + assert result["a_list"] == ["replaced", "static"] + assert result["a_dict"]["key"] == "replaced" + assert result["a_lambda"].value == "return replaced;" + + # Original input is untouched + assert config["plain"] == "${var}" + assert inner_list == ["${var}", "static"] + assert inner_dict["key"] == "${var}" + assert lam.value == "return ${var};" + + # Containers are new objects, not the originals + assert result["a_list"] is not inner_list + assert result["a_dict"] is not inner_dict + assert result["a_lambda"] is not lam def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> None: From fe2c4e47bfc3c622179b6d006703151a3ee7f39f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 14:40:02 -1000 Subject: [PATCH 0960/2056] [sensor] Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER (#15094) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/haier/hon_climate.cpp | 2 +- .../nextion/sensor/nextion_sensor.cpp | 4 --- esphome/components/sensor/sensor.cpp | 10 +++++-- esphome/components/sensor/sensor.h | 28 ++++++++++++++----- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 92defe560e0..1cee95bf16b 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -748,7 +748,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) { if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { size_t index = (size_t) type; if ((this->sub_sensors_[index] != nullptr) && - ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value))) this->sub_sensors_[index]->publish_state(value); } } diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 03b7261239e..9ea12cf808a 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -85,10 +85,6 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } this->publish_state(published_state); - } else { - this->raw_state = state; - this->state = state; - this->set_has_state(true); } } this->update_component_settings(); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index b4e59dfeb57..aad7f86dcf2 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -40,7 +40,10 @@ const LogString *state_class_to_string(StateClass state_class) { return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0); } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" Sensor::Sensor() : state(NAN), raw_state(NAN) {} +#pragma GCC diagnostic pop int8_t Sensor::get_accuracy_decimals() { if (this->sensor_flags_.has_accuracy_override) @@ -63,8 +66,13 @@ StateClass Sensor::get_state_class() { } void Sensor::publish_state(float state) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->raw_state = state; +#pragma GCC diagnostic pop +#ifdef USE_SENSOR_FILTER this->raw_callback_.call(state); +#endif ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -110,8 +118,6 @@ void Sensor::clear_filters() { this->filter_list_ = nullptr; } #endif // USE_SENSOR_FILTER -float Sensor::get_state() const { return this->state; } -float Sensor::get_raw_state() const { return this->raw_state; } void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index b3bd9620364..f4ea4af9851 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -95,9 +95,14 @@ class Sensor : public EntityBase { #endif /// Getter-syntax for .state. - float get_state() const; + float get_state() const { return this->state; } /// Getter-syntax for .raw_state - float get_raw_state() const; + float get_raw_state() const { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return this->raw_state; +#pragma GCC diagnostic pop + } /** Publish a new state to the front-end. * @@ -113,8 +118,14 @@ class Sensor : public EntityBase { /// Add a callback that will be called every time a filtered value arrives. template<typename F> void add_on_state_callback(F &&callback) { this->callback_.add(std::forward<F>(callback)); } /// Add a callback that will be called every time the sensor sends a raw value. + /// When USE_SENSOR_FILTER is not enabled, delegates to the regular callback + /// since raw state equals filtered state without filter support compiled in. template<typename F> void add_on_raw_state_callback(F &&callback) { +#ifdef USE_SENSOR_FILTER this->raw_callback_.add(std::forward<F>(callback)); +#else + this->callback_.add(std::forward<F>(callback)); +#endif } /** This member variable stores the last state that has passed through all filters. @@ -126,17 +137,20 @@ class Sensor : public EntityBase { */ float state; - /** This member variable stores the current raw state of the sensor, without any filters applied. - * - * Unlike .state,this will be updated immediately when publish_state is called. - */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0. + ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0") float raw_state; +#pragma GCC diagnostic pop void internal_send_state_to_frontend(float state); protected: +#ifdef USE_SENSOR_FILTER LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks. +#endif + LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks. #ifdef USE_SENSOR_FILTER Filter *filter_list_{nullptr}; ///< Store all active filters. From 793813790a36d782a726b9b9258b3403ce08c34a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 15:52:39 -1000 Subject: [PATCH 0961/2056] [api] Precompute tag bytes for forced varint and length-delimited fields (#15067) --- esphome/components/api/api_pb2.cpp | 10 ++-- esphome/components/api/proto.h | 11 +++++ script/api_protobuf/api_protobuf.py | 75 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 61b034c7eae..f77f4df5451 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2249,10 +2249,14 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, return true; } void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address, true); - buffer.encode_sint32(2, this->rssi, true); + buffer.write_raw_byte(8); + buffer.encode_varint_raw_64(this->address); + buffer.write_raw_byte(16); + buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len, true); + buffer.write_raw_byte(34); + buffer.encode_varint_raw(this->data_len); + buffer.encode_raw(this->data, this->data_len); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 95a79105a32..b629018a919 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -229,6 +229,17 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a single precomputed tag byte. Tag must be < 128. + inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(1); + *this->pos_++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(len); + std::memcpy(this->pos_, data, len); + this->pos_ += len; + } /// Write a precomputed tag byte + 32-bit value in one operation. /// Tag must be a single-byte varint (< 128). No zero check. inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 81ea93caf43..f2a11141af0 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -221,8 +221,58 @@ class TypeInfo(ABC): decode_64bit = None + # Mapping from encode_func to raw encode expression template. + # When a forced field has a single-byte tag, the code generator emits + # write_raw_byte(tag) + raw encode instead of the full encode_* method, + # eliminating the zero-check branch and encode_field_raw indirection. + # {value} is replaced with the actual field expression. + RAW_ENCODE_MAP: dict[str, str] = { + "encode_uint32": "buffer.encode_varint_raw({value});", + "encode_uint64": "buffer.encode_varint_raw_64({value});", + "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", + "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", + "encode_int64": "buffer.encode_varint_raw_64(static_cast<uint64_t>({value}));", + "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", + } + + def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: + """Try to emit a precomputed-tag encode for a forced field. + + Returns the raw encode string if the tag is a single byte and the + encode_func has a known raw equivalent, or None otherwise. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + if raw_expr is None: + return None + return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" + + def _encode_bytes_with_precomputed_tag( + self, data_expr: str, len_expr: str + ) -> str | None: + """Try to emit a precomputed-tag encode for a forced bytes/string field. + + Returns the raw encode string if the tag is a single byte, or None. + """ + if not self.force: + return None + tag = self.calculate_tag() + if tag >= 128: + return None + return ( + f"buffer.write_raw_byte({tag});\n" + f"buffer.encode_varint_raw({len_expr});\n" + f"buffer.encode_raw({data_expr}, {len_expr});" + ) + @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @@ -635,6 +685,11 @@ class StringType(TypeInfo): @property def encode_content(self) -> str: # Use the StringRef + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ref_.c_str()", + f"this->{self.field_name}_ref_.size()", + ): + return result if self.force: return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" @@ -801,6 +856,10 @@ class BytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}_ptr_", f"this->{self.field_name}_len_" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" @@ -908,6 +967,10 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -957,6 +1020,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + ): + return result if self.force: return ( f"buffer.encode_string({self.number}, this->{self.field_name}, true);" @@ -1124,6 +1191,10 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_bytes_with_precomputed_tag( + f"this->{self.field_name}", f"this->{self.field_name}_len" + ): + return result if self.force: return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" @@ -1199,6 +1270,10 @@ class EnumType(TypeInfo): @property def encode_content(self) -> str: + if result := self._encode_with_precomputed_tag( + f"static_cast<uint32_t>(this->{self.field_name})" + ): + return result if self.force: return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));" From 7eddf429ea3810f4e1b019b13c20f768de936411 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:57:22 +0100 Subject: [PATCH 0962/2056] [substitutions] speed up config loading: substitutions pass and `!include` redesign (package refactor part 4) (#12126) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/packages/__init__.py | 406 ++++++++++++------ esphome/config.py | 6 +- tests/component_tests/packages/test_init.py | 4 +- .../component_tests/packages/test_packages.py | 178 +++++++- .../06-remote_packages.approved.yaml | 21 +- .../06-remote_packages.input.yaml | 22 +- .../07-package_merging.approved.yaml | 2 - .../08-include_hierarchy.approved.yaml | 49 +++ .../08-include_hierarchy.input.yaml | 16 + .../10-dynamic_packages.approved.yaml | 69 +++ .../10-dynamic_packages.input.yaml | 62 +++ .../substitutions/level1_package.yaml | 21 + .../substitutions/level2_package.yaml | 21 + .../substitutions/level3_package.yaml | 16 + .../fixtures/substitutions/package2.yaml | 10 + tests/unit_tests/test_substitutions.py | 4 +- tests/unit_tests/test_yaml_util.py | 42 +- 17 files changed, 781 insertions(+), 168 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level1_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level2_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/level3_package.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/package2.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index f9bdb677a7a..1a6df84fe07 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any from esphome import git, yaml_util +from esphome.components.substitutions import ContextVars, push_context, substitute from esphome.components.substitutions.jinja import has_jinja from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv @@ -32,43 +33,44 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_PACKAGES -def validate_has_jinja(value: Any): - if not isinstance(value, str) or not has_jinja(value): - raise cv.Invalid("string does not contain Jinja syntax") - return value +def is_remote_package(package_config: dict) -> bool: + """Returns True if the package_config is a remote package definition.""" + return CONF_URL in package_config -def valid_package_contents(allow_jinja: bool = True) -> Callable[[Any], dict]: - """Returns a validator that checks if a package_config that will be merged looks as - much as possible to a valid config to fail early on obvious mistakes.""" +def valid_package_contents(package_config: dict) -> dict: + """Validate that a package looks like a plausible ESPHome config fragment. - def validator(package_config: dict) -> dict: - if isinstance(package_config, dict): - if CONF_URL in package_config: - # If a URL key is found, then make sure the config conforms to a remote package schema: - return REMOTE_PACKAGE_SCHEMA(package_config) - - # Validate manually since Voluptuous would regenerate dicts and lose metadata - # such as ESPHomeDataBase - for k, v in package_config.items(): - if not isinstance(k, str): - raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list, Remove)): - continue # e.g. script: [], psram: !remove, logger: {level: debug} - if v is None: - continue # e.g. web_server: - if allow_jinja and isinstance(v, str) and has_jinja(v): - # e.g: remote package shorthand: - # package_name: github://esphome/repo/file.yaml@${ branch }, or: - # switch: ${ expression that evals to a switch } - continue - - raise cv.Invalid("Invalid component content in package definition") - return package_config + Rejects non-dict values, remote package schemas (which should have been + handled earlier), non-string keys, and scalar values that aren't Jinja + expressions. This is a lightweight check to catch obvious mistakes before + full component validation runs later. + """ + if not isinstance(package_config, dict): raise cv.Invalid("Package contents must be a dict") - return validator + if is_remote_package(package_config): + # Package contents must not contain a root `url:` key + raise cv.Invalid("Remote package schema not expected here") + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + for k, v in package_config.items(): + if not isinstance(k, str): + raise cv.Invalid("Package content keys must be strings") + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} + if v is None: + continue # e.g. web_server: + if isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch }, or: + # switch: ${ expression that evals to a switch } + continue + + raise cv.Invalid("Invalid component content in package definition") + return package_config def expand_file_to_files(config: dict): @@ -105,7 +107,7 @@ def validate_source_shorthand(value): return REMOTE_PACKAGE_SCHEMA(conf) -def deprecate_single_package(config): +def deprecate_single_package(config: dict) -> dict: _LOGGER.warning( """ Including a single package under `packages:`, i.e., `packages: !include mypackage.yaml` is deprecated. @@ -158,10 +160,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All( PACKAGE_SCHEMA = cv.Any( # A package definition is either: validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or - validate_has_jinja, # a Jinja string that may resolve to a package, or - valid_package_contents( - allow_jinja=True - ), # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} # which will have to be fully validated later as per each component's schema. ) @@ -179,7 +178,15 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: def _process_remote_package(config: dict, skip_update: bool = False) -> dict: - # When skip_update is True, use NEVER_REFRESH to prevent updates + """Clone/update a git repo and load the YAML files listed in the package definition. + + Returns ``{"packages": {<filename>: <loaded_yaml>, ...}}`` so the caller + can recurse into the loaded packages. Each loaded YAML node is tagged + with any ``vars:`` from the file entry via :func:`yaml_util.add_context`. + + If loading fails after cloning, attempts a revert and retry in case + a prior cached checkout is stale. + """ actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( url=config[CONF_URL], @@ -189,7 +196,7 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), ) - files = [] + files: list[dict[str, Any]] = [] if base_path := config.get(CONF_PATH): repo_dir = repo_dir / base_path @@ -200,126 +207,255 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: else: files.append(file) - def get_packages(files) -> dict: - packages = {} + def _load_package_yaml(yaml_file: Path, filename: str) -> dict: + """Load a YAML file from a remote package, validating min_version.""" + try: + new_yaml = yaml_util.load_yaml(yaml_file) + except EsphomeError as e: + raise cv.Invalid( + f"{filename} is not a valid YAML file." + f" Please check the file contents.\n{e}" + ) from e + esphome_config = new_yaml.get(CONF_ESPHOME) or {} + min_version = esphome_config.get(CONF_MIN_VERSION) + if min_version is not None and cv.Version.parse(min_version) > cv.Version.parse( + ESPHOME_VERSION + ): + raise cv.Invalid( + f"Current ESPHome Version is too old to use" + f" this package: {ESPHOME_VERSION} < {min_version}" + ) + return new_yaml + + def get_packages(files: list[dict[str, Any]]) -> dict: + packages: dict[str, Any] = {} for idx, file in enumerate(files): filename = file[CONF_PATH] yaml_file: Path = repo_dir / filename - vars = file.get(CONF_VARS, {}) - if not yaml_file.is_file(): raise cv.Invalid( f"{filename} does not exist in repository", path=[CONF_FILES, idx, CONF_PATH], ) - - try: - new_yaml = yaml_util.load_yaml(yaml_file) - if ( - CONF_ESPHOME in new_yaml - and CONF_MIN_VERSION in new_yaml[CONF_ESPHOME] - ): - min_version = new_yaml[CONF_ESPHOME][CONF_MIN_VERSION] - if cv.Version.parse(min_version) > cv.Version.parse( - ESPHOME_VERSION - ): - raise cv.Invalid( - f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" - ) - new_yaml = yaml_util.add_context(new_yaml, vars or None) - packages[f"{filename}{idx}"] = new_yaml - except EsphomeError as e: - raise cv.Invalid( - f"{filename} is not a valid YAML file. Please check the file contents.\n{e}" - ) from e + new_yaml = _load_package_yaml(yaml_file, filename) + new_yaml = yaml_util.add_context(new_yaml, file.get(CONF_VARS)) + packages[f"{filename}{idx}"] = new_yaml return packages - packages = None - error = "" - - try: - packages = get_packages(files) - except cv.Invalid as e: - error = e + if revert is not None: + # If loading fails, the cached checkout may be stale — revert and retry once. try: - if revert is not None: - revert() - packages = get_packages(files) - except cv.Invalid as er: - error = er + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid: + revert() + try: + return {CONF_PACKAGES: get_packages(files)} + except cv.Invalid as err: + raise cv.Invalid(f"Failed to load packages. {err}", path=err.path) from err - if packages is None: - raise cv.Invalid(f"Failed to load packages. {error}", path=error.path) + return {CONF_PACKAGES: get_packages(files)} - return {"packages": packages} + +def _walk_package_dict( + packages: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> cv.Invalid | None: + """Iterate a packages dict in reverse priority order, invoking callback on each entry. + + Returns ``None`` on success, or the first :class:`cv.Invalid` error if a callback fails. + """ + for package_name, package_config in reversed(packages.items()): + with cv.prepend_path(package_name): + try: + packages[package_name] = callback(package_config, context) + except cv.Invalid as err: + return err + return None + + +def _walk_package_list( + packages: list, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None, +) -> None: + """Iterate a packages list in reverse priority order, invoking callback on each entry.""" + for idx in reversed(range(len(packages))): + with cv.prepend_path(idx): + packages[idx] = callback(packages[idx], context) def _walk_packages( - config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True + config: dict, + callback: Callable[[dict, ContextVars | None], dict], + context: ContextVars | None = None, + validate_deprecated: bool = True, ) -> dict: + """Walks the packages structure in priority order, invoking ``callback`` on each package definition found. + + This function only iterates over the immediate ``packages:`` entries in *config*. + If packages may contain nested ``packages:`` keys, the *callback* is responsible + for recursing by calling ``_walk_packages`` on the returned package config. + """ if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] - # The following block and `validate_deprecated` parameter can be safely removed - # once single-package deprecation is effective - if validate_deprecated: - packages = CONFIG_SCHEMA(packages) + if not isinstance(packages, (dict, list)): + raise cv.Invalid( + f"Packages must be a key to value mapping or list, got {type(packages)} instead" + ) with cv.prepend_path(CONF_PACKAGES): - if isinstance(packages, dict): - for package_name, package_config in reversed(packages.items()): - with cv.prepend_path(package_name): - package_config = callback(package_config) - packages[package_name] = _walk_packages(package_config, callback) - elif isinstance(packages, list): - for idx in reversed(range(len(packages))): - with cv.prepend_path(idx): - package_config = callback(packages[idx]) - packages[idx] = _walk_packages(package_config, callback) - else: - raise cv.Invalid( - f"Packages must be a key to value mapping or list, got {type(packages)} instead" - ) + if not isinstance(packages, dict): + _walk_package_list(packages, callback, context) + elif (result := _walk_package_dict(packages, callback, context)) is not None: + if not validate_deprecated: + raise result + # Fallback: treat the dict as a single deprecated package. + # Note: this catches *any* cv.Invalid from the callback, which may + # mask real validation errors in named package dicts. + # This block can be removed once the single-package + # deprecation period (2026.7.0) is over. + config[CONF_PACKAGES] = [packages] + return _walk_packages(deprecate_single_package(config), callback, context) + config[CONF_PACKAGES] = packages return config -def do_packages_pass(config: dict, skip_update: bool = False) -> dict: - """Processes, downloads and validates all packages in the config. - Also extracts and merges all substitutions found in packages into the main config substitutions. +def _substitute_package_definition( + package_config: dict | str, context_vars: ContextVars | None +) -> dict | str: + """Substitute variables in a package definition string or remote package dict. + + Only substitutes strings and remote package dicts (URLs, refs, paths). + Local package contents are left untouched — they will be substituted + later during the main substitution pass. + """ + if isinstance(package_config, str) or ( + isinstance(package_config, dict) and is_remote_package(package_config) + ): + package_config = substitute( + item=package_config, + path=[], + parent_context=context_vars or ContextVars(), + strict_undefined=False, + ) + return package_config + + +def _update_substitutions_context( + parent_context: UserDict, + package_substitutions: dict[str, Any], +) -> None: + """Resolve and add new substitutions to the parent context. + + Skips keys already present (higher-priority sources win). + String values are substituted against the current context so that + cross-references between substitutions are expanded when possible. + """ + for key, value in package_substitutions.items(): + if key in parent_context: + continue + if not isinstance(value, str): + parent_context[key] = value + continue + parent_context[key] = substitute( + item=value, + path=[CONF_SUBSTITUTIONS, key], + parent_context=ContextVars(parent_context), + strict_undefined=False, + ) + + +class _PackageProcessor: + """Stateful processor that resolves packages and collects substitutions. + + Packages are processed highest-priority first (later-declared before + earlier-declared) so that their substitutions are available when + resolving lower-priority package definitions. For each entry: + + 1. Substitute variables in remote package definitions (URLs, refs, paths). + 2. Validate against ``PACKAGE_SCHEMA`` and download remote packages. + 3. Extract ``substitutions:`` and merge into the shared context + (higher-priority packages win on conflicts). + 4. Recurse into any nested ``packages:`` keys. + + Command-line substitutions take the highest priority and are never overridden. + """ + + def __init__( + self, + substitutions: UserDict, + command_line_substitutions: dict[str, Any] | None, + skip_update: bool, + ) -> None: + self.substitutions = substitutions + self.parent_context = UserDict(command_line_substitutions or {}) + self.skip_update = skip_update + + def resolve_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Substitute variables in the definition and fetch remote packages. + + The input may be a ``str`` (git shorthand or Jinja expression) or a + ``dict`` (remote or local package). After ``PACKAGE_SCHEMA`` validation + the result is always a ``dict``. + """ + package_config = _substitute_package_definition(package_config, context_vars) + package_config = PACKAGE_SCHEMA(package_config) + if is_remote_package(package_config): + package_config = _process_remote_package(package_config, self.skip_update) + return package_config + + def collect_substitutions(self, package_config: dict) -> None: + """Extract substitutions from a package and merge into the shared context.""" + if subs := package_config.pop(CONF_SUBSTITUTIONS, {}): + self.substitutions.data = merge_config(subs, self.substitutions.data) + _update_substitutions_context(self.parent_context, subs) + + def process_package( + self, package_config: dict | str, context_vars: ContextVars | None + ) -> dict: + """Resolve a single package and recurse into any nested packages.""" + package_config = self.resolve_package(package_config, context_vars) + self.collect_substitutions(package_config) + + if CONF_PACKAGES not in package_config: + return package_config + + # Push context from !include vars on the package root and on the packages key + context_vars = push_context(package_config, context_vars) + context_vars = push_context(package_config[CONF_PACKAGES], context_vars) + return _walk_packages(package_config, self.process_package, context_vars) + + +def do_packages_pass( + config: dict, + *, + command_line_substitutions: dict[str, Any] | None = None, + skip_update: bool = False, +) -> dict: + """Load, validate, and flatten all packages in the config. + + Returns the config with all packages loaded in-place (but not yet merged) + and a consolidated ``substitutions:`` block restored at the front. """ if CONF_PACKAGES not in config: return config substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + processor = _PackageProcessor( + substitutions, command_line_substitutions, skip_update + ) + _update_substitutions_context(processor.parent_context, substitutions) - def process_package_callback(package_config: dict) -> dict: - """This will be called for each package found in the config.""" - if isinstance(package_config, yaml_util.ConfigContext): - context_vars = package_config.vars - if CONF_PACKAGES in package_config or CONF_URL in package_config: - # Remote package definition: eagerly resolve before PACKAGE_SCHEMA validation. - from esphome.components.substitutions import ContextVars, substitute - - package_config = substitute( - package_config, - [], - ContextVars(context_vars), - strict_undefined=False, - ) - package_config = PACKAGE_SCHEMA(package_config) - if isinstance(package_config, str): - return package_config # Jinja string, skip processing - if CONF_URL in package_config: - package_config = _process_remote_package(package_config, skip_update) - # Extract substitutions from the package and merge them into the main substitutions: - substitutions.data = merge_config( - package_config.pop(CONF_SUBSTITUTIONS, {}), substitutions.data - ) - return package_config - - _walk_packages(config, process_package_callback) + context_vars = push_context( + config[CONF_PACKAGES], ContextVars(processor.parent_context) + ) + _walk_packages(config, processor.process_package, context_vars) if substitutions: config[CONF_SUBSTITUTIONS] = substitutions.data @@ -328,19 +464,27 @@ def do_packages_pass(config: dict, skip_update: bool = False) -> dict: def merge_packages(config: dict) -> dict: - """Merges all packages into the main config and removes the `packages:` key.""" + """Flatten the ``packages:`` tree into the main config. + + Collects every package (including nested ones) into a flat list in + priority order, then merges them into *config* using :func:`merge_config`. + Higher-priority packages (declared later) override lower-priority ones. + + The ``packages:`` key is removed from the returned config. + Must be called after :func:`do_packages_pass` has resolved all packages. + """ if CONF_PACKAGES not in config: return config # Build flat list of all package configs to merge in priority order: merge_list: list[dict] = [] - validate_package = valid_package_contents(allow_jinja=False) - - def process_package_callback(package_config: dict) -> dict: + def process_package_callback( + package_config: dict, context: ContextVars | None + ) -> dict: """This will be called for each package found in the config.""" - merge_list.append(validate_package(package_config)) - return package_config + merge_list.append(package_config) + return _walk_packages(package_config, process_package_callback) _walk_packages(config, process_package_callback, validate_deprecated=False) # Merge all packages into the main config: diff --git a/esphome/config.py b/esphome/config.py index b80aaf37007..7a6feea3d31 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -989,7 +989,11 @@ def validate_config( result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) try: - config = do_packages_pass(config, skip_update=skip_external_update) + config = do_packages_pass( + config, + command_line_substitutions=command_line_substitutions, + skip_update=skip_external_update, + ) except vol.Invalid as err: result.update(config) result.add_error(err) diff --git a/tests/component_tests/packages/test_init.py b/tests/component_tests/packages/test_init.py index 779244e2ed4..fd30c2433f2 100644 --- a/tests/component_tests/packages/test_init.py +++ b/tests/component_tests/packages/test_init.py @@ -69,7 +69,7 @@ def test_packages_skip_update_false( } # Call with skip_update=False (default) - do_packages_pass(config, skip_update=False) + do_packages_pass(config, command_line_substitutions={}, skip_update=False) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() @@ -104,7 +104,7 @@ def test_packages_default_no_skip( } # Call without skip_update parameter - do_packages_pass(config) + do_packages_pass(config, command_line_substitutions={}) # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 60dc0dccda3..0893c7dcbbc 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -37,6 +37,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict +from esphome.yaml_util import add_context # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -70,7 +71,7 @@ def fixture_basic_esphome(): def packages_pass(config): - """Wrapper around packages_pass that also resolves Extend and Remove.""" + """Passes the config through the packages processing steps.""" config = do_packages_pass(config) config = do_substitution_pass(config) config = merge_packages(config) @@ -705,6 +706,85 @@ def test_remote_packages_with_files_list( assert actual == expected +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_with_files_list_and_substitutions( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """ + Ensures that packages are loaded as mixed list of dictionary and strings + """ + # Mock the response from git.clone_or_update + mock_revert = MagicMock() + mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert) + + # Mock the response from pathlib.Path.is_file + mock_is_file.return_value = True + + # Mock the response from esphome.yaml_util.load_yaml + mock_load_yaml.side_effect = [ + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + } + ] + } + ), + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + } + ] + } + ), + ] + + # Define the input config + config = { + CONF_PACKAGES: { + "package1": add_context( + { + CONF_URL: r"${url}", + CONF_REF: r"${branch}", + CONF_FILES: [ + {CONF_PATH: r"$file"}, + "sensor2.yaml", + ], + CONF_REFRESH: "1d", + }, + { + "branch": "main", + "file": TEST_YAML_FILENAME, + "url": "https://github.com/esphome/non-existant-repo", + }, + ) + } + } + + expected = { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = packages_pass(config) + assert actual == expected + + @patch("esphome.yaml_util.load_yaml") @patch("pathlib.Path.is_file") @patch("esphome.git.clone_or_update") @@ -906,7 +986,7 @@ def test_packages_merge_substitutions() -> None: }, } - actual = do_packages_pass(config) + actual = do_packages_pass(config, command_line_substitutions={}) assert actual == expected @@ -970,33 +1050,107 @@ def test_package_merge() -> None: assert actual == expected +def test_packages_invalid_type_raises() -> None: + """Packages that are not a dict or list raise cv.Invalid.""" + config = { + CONF_PACKAGES: "not_a_dict_or_list", + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): + do_packages_pass(config) + + @pytest.mark.parametrize( "invalid_package", [ 6, "some string", - ["some string"], - None, True, - {"some_component": 8}, - {3: 2}, - {"some_component": r"${unevaluated expression}"}, ], ) -def test_package_merge_invalid(invalid_package) -> None: - """ - Tests that trying to merge an invalid package raises an error. - """ +def test_invalid_package_contents_rejected(invalid_package: object) -> None: + """Invalid package contents are rejected by PACKAGE_SCHEMA during do_packages_pass.""" config = { CONF_PACKAGES: { "some_package": invalid_package, }, } - with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +@pytest.mark.xfail( + reason="Deprecated single-package fallback swallows these errors. " + "Remove xfail when single-package deprecation is removed (2026.7.0).", + strict=True, +) +@pytest.mark.parametrize( + "invalid_package", + [ + None, + ["some string"], + {"some_component": 8}, + {3: 2}, + ], +) +def test_invalid_package_contents_masked_by_deprecation( + invalid_package: object, +) -> None: + """These invalid packages are swallowed by the deprecated single-package fallback.""" + config = { + CONF_PACKAGES: { + "some_package": invalid_package, + }, + } + with pytest.raises(cv.Invalid): + do_packages_pass(config) + + +def test_merge_packages_invalid_nested_type_raises() -> None: + """Invalid nested packages type during merge raises cv.Invalid.""" + config = { + CONF_PACKAGES: { + "pkg": { + CONF_PACKAGES: "invalid", + }, + }, + } + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ): merge_packages(config) +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_packages_no_revert( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """Remote packages with revert=None load without retry logic.""" + mock_clone_or_update.return_value = (Path("/tmp/noexists"), None) + mock_is_file.return_value = True + mock_load_yaml.return_value = OrderedDict( + {CONF_SENSOR: [{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}]} + ) + + config = { + CONF_PACKAGES: { + "pkg": { + CONF_URL: "https://github.com/esphome/repo", + CONF_REF: "main", + CONF_FILES: [{CONF_PATH: "file.yaml"}], + CONF_REFRESH: "1d", + } + } + } + actual = packages_pass(config) + assert actual[CONF_SENSOR] == [ + {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"} + ] + + def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: """Test that CORE.raw_config contains esphome section from merged package. diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml index 0fffbfb7cba..300cd859508 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -1,7 +1,3 @@ -substitutions: - x: 10 - y: 20 - z: 30 values_from_repo1_main: - package_name: package1 x: 3 @@ -28,3 +24,20 @@ values_from_repo1_main: y: 20 z: 5 volume: 1000 + - package_name: package6 + x: 12 + y: 13 + z: 5 + volume: 780 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 +substitutions: + x: 10 + y: 20 + z: 30 + my_repo: repo1 + my_file: file1 + my_ref: main diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml index 772860bf19f..c61eeab28d1 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -2,16 +2,26 @@ substitutions: x: 10 y: 20 z: 30 + my_repo: default_repo + my_file: default_file + my_ref: main + +# The following key is only used by the test framework +# to simulate command line substitutions +command_line_substitutions: + my_repo: repo1 + my_file: file1 + packages: package1: url: https://github.com/esphome/repo1 + ref: main files: - path: file1.yaml vars: package_name: package1 x: 3 y: 4 - ref: main package2: !include # a package that just includes the given remote package file: remote_package_proxy.yaml vars: @@ -41,3 +51,13 @@ packages: repo: repo1 file: file1.yaml ref: main + package6: + url: https://github.com/esphome/${my_repo} + ref: ${my_ref} + files: + - path: ${my_file + ".yaml"} + vars: + package_name: package6 + x: 12 + y: 13 + package7: github://esphome/${my_repo}/${my_file + ".yaml"}@${my_ref} diff --git a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml index 867889b7bc1..9e62fcae865 100644 --- a/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/07-package_merging.approved.yaml @@ -37,8 +37,6 @@ substitutions: - id: component8 value: 8 fancy_package: - substitutions: - fancy_subst: 42 fancy_component: *id001 pin: 12 some_switches: *id002 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml new file mode 100644 index 00000000000..fce47b01bfc --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.approved.yaml @@ -0,0 +1,49 @@ +substitutions: + a: 10 + b: 20 + x: 79 +test_list: + - level1: + a: 10 + b: 20 + c: 10 + d: 20 + e: ${e} + f: ${f} + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 80 + y: 40 + level2: + - level2: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: ${g} + h: ${h} + i: ${i} + j: ${j} + x: 81 + y: 40 + level3: + - level3: + a: 10 + b: 20 + c: 10 + d: 20 + e: 20 + f: 40 + g: 100 + h: 200 + i: 30 + j: ${undefined_variable} + x: 82 + y: 40 + - a: 10 + b: 20 + x: 79 diff --git a/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml new file mode 100644 index 00000000000..6997ef56c16 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/08-include_hierarchy.input.yaml @@ -0,0 +1,16 @@ +substitutions: + a: 10 + b: 20 + x: 79 + +test_list: + - !include + file: level1_package.yaml + vars: + x: ${x+1} + y: ${d*2} + c: ${a} + d: ${b} + - a: ${a} + b: ${b} + x: ${x} diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml new file mode 100644 index 00000000000..ec2ae711bb1 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.approved.yaml @@ -0,0 +1,69 @@ +substitutions: + a: from base config + b: from package3 + c: from nested package4 + nested_package: + nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package4: + packages: + - nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package_map: + package1: + package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + package2: + package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 + package3: &id001 + package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 + selected_package_number: 3 + selected_package_name: package3 + selected_package: *id001 +base_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package1_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +package2_test_list: + - a: from package2 vars + - b: from package3 + - c: from nested package4 +package3_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 +nested_package_test_list: + - a: from base config + - b: from package3 + - c: from nested package4 diff --git a/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml new file mode 100644 index 00000000000..800e3cc7a86 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/10-dynamic_packages.input.yaml @@ -0,0 +1,62 @@ +command_line_substitutions: + selected_package_number: 3 + +substitutions: + a: from base config + + package1: &p1 + substitutions: + a: from package1 + b: from package1 + c: from package1 + package1_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package2: &p2 !include + file: package2.yaml + vars: + a: from package2 vars + + package3: &p3 + substitutions: + a: from package3 + b: from package3 + c: from package3 + package3_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + + package4: + substitutions: + nested_package: + substitutions: + c: from nested package4 + nested_package_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } + packages: + - ${ nested_package } + + package_map: + package1: *p1 + package2: *p2 + package3: *p3 + + selected_package_number: 2 # will be overridden by command line substitutions + selected_package_name: package${ selected_package_number } + selected_package: ${ package_map[selected_package_name] } + +packages: + - ${ package1 } + - ${ package2 } + - ${ selected_package } + - ${ package4 } + +base_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/fixtures/substitutions/level1_package.yaml b/tests/unit_tests/fixtures/substitutions/level1_package.yaml new file mode 100644 index 00000000000..d8a994b7b48 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level1_package.yaml @@ -0,0 +1,21 @@ +# this file is included by 07-include_hierarchy.input.yaml +level1: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # from vars when including + d: ${d} # from vars when including + e: ${e} # undefined at this level + f: ${f} # undefined at this level + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level2: + - !include + file: level2_package.yaml + vars: + e: ${c*2} + f: ${d*2} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level2_package.yaml b/tests/unit_tests/fixtures/substitutions/level2_package.yaml new file mode 100644 index 00000000000..460135553a6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level2_package.yaml @@ -0,0 +1,21 @@ +# this file is included by level1_package.yaml +level2: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # from vars when including + f: ${f} # from vars when including + g: ${g} # undefined at this level + h: ${h} # undefined at this level + i: ${i} # undefined at this level + j: ${j} # undefined at this level + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated + level3: + - !include + file: level3_package.yaml + vars: + g: ${e*5} + h: ${f*5} + x: ${x+1} diff --git a/tests/unit_tests/fixtures/substitutions/level3_package.yaml b/tests/unit_tests/fixtures/substitutions/level3_package.yaml new file mode 100644 index 00000000000..b16ed5fcf6f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/level3_package.yaml @@ -0,0 +1,16 @@ +# this file is included by level2_package.yaml +defaults: + i: 30 +level3: + a: ${a} # top-level substitution + b: ${b} # top-level substitution + c: ${c} # visible from level1 vars + d: ${d} # visible from level1 vars + e: ${e} # visible from level2 vars + f: ${f} # visible from level2 vars + g: ${g} # from vars when including + h: ${h} # from vars when including + i: ${i} # Should take the default value of 30 + j: ${undefined_variable} # Does not exist, should be output as-is + x: ${x} # from vars when including, calculated + y: ${y} # from vars when including, calculated diff --git a/tests/unit_tests/fixtures/substitutions/package2.yaml b/tests/unit_tests/fixtures/substitutions/package2.yaml new file mode 100644 index 00000000000..998cd74c52c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/package2.yaml @@ -0,0 +1,10 @@ +# included from 10-dynamic_packages.input.yaml +substitutions: + a: from package2 # must not override base config's a + # b not defined here, won't override package1's b + c: from package2 # will override package1's c + +package2_test_list: + - a: ${ a } + - b: ${ b } + - c: ${ c } diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 30478f9521d..c7b0bbcf7ca 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -143,7 +143,9 @@ def test_substitutions_fixtures( command_line_substitutions = config.pop("command_line_substitutions", None) - config = do_packages_pass(config) + config = do_packages_pass( + config, command_line_substitutions=command_line_substitutions + ) config = substitutions.do_substitution_pass(config, command_line_substitutions) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 35a4bc37073..667b5938190 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -98,13 +98,15 @@ def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None: """Test that missing secrets raise proper errors.""" # Create a YAML file with a secret that doesn't exist test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ esphome: name: test wifi: password: !secret nonexistent_secret -""") +""" + ) # Create an empty secrets file secrets_yaml = tmp_path / "secrets.yaml" @@ -118,10 +120,12 @@ def test_construct_secret_no_secrets_file(tmp_path: Path) -> None: """Test that missing secrets.yaml file raises proper error.""" # Create a YAML file with a secret but no secrets.yaml test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret some_secret -""") +""" + ) # Mock CORE.config_path to avoid NoneType error with ( @@ -140,10 +144,12 @@ def test_construct_secret_fallback_to_main_config_dir( subdir.mkdir() test_yaml = subdir / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ wifi: password: !secret test_secret -""") +""" + ) # Create secrets.yaml in the main directory main_secrets = tmp_path / "secrets.yaml" @@ -164,9 +170,11 @@ def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None # Create test YAML that uses include_dir_named test_yaml = dst_dir / "test_include_named.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) actual_sensor = actual["sensor"] @@ -199,9 +207,11 @@ def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None: empty_dir.mkdir() test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ sensor: !include_dir_named empty_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -231,9 +241,11 @@ def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None: hidden_subfile.write_text("key: hidden_subfile_value") test_yaml = tmp_path / "test.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ test: !include_dir_named test_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) @@ -255,9 +267,11 @@ def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None: # This indirectly tests _find_files by using include_dir_named test_yaml = dst_dir / "test_include_recursive.yaml" - test_yaml.write_text(""" + test_yaml.write_text( + """ all_sensors: !include_dir_named named_dir -""") +""" + ) actual = yaml_util.load_yaml(test_yaml) From b3390d40fb959808fcc520bff17c4a1350f98420 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:31:42 +0100 Subject: [PATCH 0963/2056] [core] Fix cg.add_define propagation to dependencies in native ESP-IDF builds (#15137) --- esphome/build_gen/espidf.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 9df9b1069c5..01923baaac2 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -53,6 +53,13 @@ def get_project_cmakelists() -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # Extract compile definitions from build flags (-DXXX -> XXX) + compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] + extra_compile_options = "\n".join( + f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' + for compile_def in compile_defs + ) + return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) @@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) + +{extra_compile_options} + project({CORE.name}) """ @@ -70,10 +80,6 @@ def get_component_cmakelists(minimal: bool = False) -> str: idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) - # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else "" - # Extract compile options (-W flags, excluding linker flags) compile_opts = [ flag @@ -104,11 +110,6 @@ idf_component_register( # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) -# ESPHome compile definitions -target_compile_definitions(${{COMPONENT_LIB}} PUBLIC - {compile_defs_str} -) - # ESPHome compile options target_compile_options(${{COMPONENT_LIB}} PUBLIC {compile_opts_str} From 3cd50f0495564c14f35b55448332079874682f12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:31:08 -0400 Subject: [PATCH 0964/2056] [ci] Block new CONF_ constants from being added to esphome/const.py (#15145) --- script/ci-custom.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 25a0cf21277..7d0680a4919 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -525,6 +525,29 @@ def lint_constants_usage(): return errs +# Maximum allowed CONF_ constants in esphome/const.py. +# This file is frozen — new constants go in esphome/components/const/__init__.py. +# Decrease this number when constants are moved out of const.py. +CONST_PY_MAX_CONF = 1011 + + +@lint_content_check(include=["esphome/const.py"]) +def lint_const_py_frozen(fname, content): + """Block new CONF_ constants from being added to esphome/const.py. + + New constants should go in esphome/components/const/__init__.py instead. + """ + count = sum(1 for line in content.splitlines() if line.startswith("CONF_")) + if count > CONST_PY_MAX_CONF: + return ( + "esphome/const.py is frozen. " + "Add new constants to esphome/components/const/__init__.py instead." + ) + if count < CONST_PY_MAX_CONF: + return f"CONST_PY_MAX_CONF in ci-custom.py should be updated to {count}." + return None + + def relative_cpp_search_text(fname: Path, content) -> str: parts = fname.parts integration = parts[2] From 55df21db516263a1f7b381035477a66cd84c5e8b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:44:28 -0400 Subject: [PATCH 0965/2056] [esp32] Default CPU frequency to maximum supported (#15143) --- esphome/components/esp32/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1ecc270fd1c..0e216485ac2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -379,12 +379,11 @@ FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values( def set_core_data(config): cpu_frequency = config.get(CONF_CPU_FREQUENCY, None) variant = config[CONF_VARIANT] - # if not specified in config, set to 160MHz if supported, the fastest otherwise + # if not specified in config, default to the maximum supported frequency + # (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz) if cpu_frequency is None: choices = CPU_FREQUENCIES[variant] - if "160MHZ" in choices: - cpu_frequency = "160MHZ" - elif "360MHZ" in choices: + if variant == VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE): cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] From 22bc47da23a97187c74477fb282748d0935251aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Bl=C3=A4se?= <fabian@blaese.de> Date: Tue, 24 Mar 2026 20:57:58 +0100 Subject: [PATCH 0966/2056] [light] Fix incorrect mode change handling on transition to off (#15147) --- esphome/components/light/transformers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index b6e5e08f2bb..61fe098ad74 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -27,7 +27,7 @@ class LightTransitionTransformer : public LightTransformer { } // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. - if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + if (this->start_values_.get_color_mode() != this->end_values_.get_color_mode()) { this->changing_color_mode_ = true; this->intermediate_values_ = this->start_values_; this->intermediate_values_.set_state(false); @@ -39,8 +39,8 @@ class LightTransitionTransformer : public LightTransformer { // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. if (this->changing_color_mode_ && p > 0.5f && - this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { - this->intermediate_values_ = this->target_values_; + this->intermediate_values_.get_color_mode() != this->end_values_.get_color_mode()) { + this->intermediate_values_ = this->end_values_; this->intermediate_values_.set_state(false); } From 8751f348c8ab50db26b4cfbc5abcc1c4703ba739 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:04:27 -0400 Subject: [PATCH 0967/2056] [sx127x] Fix FIFO read corruption (#15114) --- esphome/components/sx127x/sx127x.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 66957a73424..0fddfdccdb7 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -38,14 +38,18 @@ void SX127x::write_register_(uint8_t reg, uint8_t value) { void SX127x::read_fifo_(std::vector<uint8_t> &packet) { this->enable(); this->write_byte(REG_FIFO & 0x7F); - this->read_array(packet.data(), packet.size()); + for (auto &byte : packet) { + byte = this->transfer_byte(0x00); + } this->disable(); } void SX127x::write_fifo_(const std::vector<uint8_t> &packet) { this->enable(); this->write_byte(REG_FIFO | 0x80); - this->write_array(packet.data(), packet.size()); + for (const auto &byte : packet) { + this->transfer_byte(byte); + } this->disable(); } From 13baf260505116b2f6e3ae8e1ed8fbcd18820b58 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:26:21 +0100 Subject: [PATCH 0968/2056] [core] get_log_str: fix false-positive error on null-terminated strings with stricter compilers (#15136) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/core/progmem.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 6c6a5252cf6..031860e3a64 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -3,6 +3,7 @@ #include <array> #include <cstddef> #include <cstdint> +#include <new> #include "esphome/core/hal.h" // For PROGMEM definition @@ -104,7 +105,9 @@ struct LogString; static const char *get_(uint8_t idx, uint8_t fallback) { \ if (idx >= COUNT) \ idx = fallback; \ - return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + /* std::launder is used here to prevent the inter-procedural analysis that */ \ + /* causes the false positive that the string is not null terminated */ \ + return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \ } \ static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ From 4ff85e2a1e0122f28a3b9d366e79b9fd112215f2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:48:17 -0400 Subject: [PATCH 0969/2056] [core] Fix clean-all to handle custom build paths (#15146) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/writer.py | 31 ++++++-- tests/unit_tests/test_writer.py | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 69a35d00e34..4aac16ffd4d 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -18,7 +18,6 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, cpp_string_escape, - get_str_env, is_ha_addon, read_file, rmtree, @@ -441,18 +440,42 @@ def clean_build(clear_pio_cache: bool = True): rmtree(cache_dir) +def _get_custom_build_dir(item: Path, data_dir: Path) -> Path | None: + """Parse a YAML config to find a custom build directory.""" + from esphome import yaml_util + + try: + raw = yaml_util.load_yaml(item) + except (EsphomeError, OSError) as e: + _LOGGER.debug("Could not parse %s to find build_path: %s", item, e) + return None + if not isinstance(raw, dict): + return None + esphome_conf = raw.get("esphome", {}) + if not isinstance(esphome_conf, dict): + return None + if build_path := esphome_conf.get("build_path"): + return data_dir / build_path + return None + + def clean_all(configuration: list[str]): data_dirs = [] for config in configuration: item = Path(config) if item.is_file() and item.suffix in (".yaml", ".yml"): - data_dirs.append(item.parent / ".esphome") + data_dir = item.parent / ".esphome" + data_dirs.append(data_dir) + if custom := _get_custom_build_dir(item, data_dir): + data_dirs.append(custom) else: data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) - if "ESPHOME_DATA_DIR" in os.environ: - data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None))) + if env_data_dir := os.environ.get("ESPHOME_DATA_DIR"): + data_dirs.append(Path(env_data_dir)) + if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"): + data_dirs.append(Path(env_build_path)) # Clean build dir for dir in data_dirs: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 134b63df4a8..6ace38a7d7d 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -866,6 +866,130 @@ def test_clean_all_with_yaml_file( assert str(build_dir) in caplog.text +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans absolute build_path specified in YAML config.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create an absolute custom build path directory with contents + custom_build = tmp_path / "custom_build" + custom_build.mkdir() + (custom_build / "firmware.bin").write_text("x") + sub = custom_build / "subdir" + sub.mkdir() + (sub / "file.txt").write_text("x") + + yaml_file = config_dir / "test.yaml" + # Absolute build_path: data_dir / absolute = absolute (Python Path behavior) + yaml_file.write_text(f"esphome:\n name: test\n build_path: {custom_build}\n") + + # Also create the normal .esphome dir + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Both .esphome and custom build_path should be cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + assert custom_build.exists() + assert not (custom_build / "firmware.bin").exists() + assert not sub.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_parse_error( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all still cleans .esphome when YAML parse fails.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("invalid: yaml: content: [") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # .esphome should still be cleaned despite YAML parse failure + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_with_env_build_path( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all cleans ESPHOME_BUILD_PATH directory.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + # Create env build path directory + env_build = tmp_path / "env_build" + env_build.mkdir() + (env_build / "firmware.bin").write_text("x") + + from esphome.writer import clean_all + + with ( + caplog.at_level("INFO"), + patch.dict(os.environ, {"ESPHOME_BUILD_PATH": str(env_build)}), + ): + clean_all([str(config_dir)]) + + # Both should be cleaned + assert not (build_dir / "dummy.txt").exists() + assert env_build.exists() + assert not (env_build / "firmware.bin").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_ignores_empty_env_vars( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all ignores empty ESPHOME_BUILD_PATH/ESPHOME_DATA_DIR.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + # Create a file in cwd that must NOT be cleaned + marker = tmp_path / "important.txt" + marker.write_text("do not delete") + + from esphome.writer import clean_all + + with patch.dict( + os.environ, + {"ESPHOME_BUILD_PATH": "", "ESPHOME_DATA_DIR": ""}, + ): + clean_all([str(config_dir)]) + + # Empty env vars must not cause cwd to be cleaned + assert marker.exists() + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 752fe30332749f1608823753dcd65c20f91448e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:01:59 -1000 Subject: [PATCH 0970/2056] [api] Add descriptive message to status warning when waiting for client (#15148) --- esphome/components/api/api_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1151bc5983d..d9c3cc68467 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -108,7 +108,7 @@ void APIServer::setup() { this->last_connected_ = App.get_loop_component_start_time(); // Set warning status if reboot timeout is enabled if (this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); } } @@ -187,7 +187,7 @@ void APIServer::remove_client_(size_t client_index) { // Last client disconnected - set warning and start tracking for reboot timeout if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); this->last_connected_ = App.get_loop_component_start_time(); } From 9fb5b6aa158582ea35e968f9b8209f4f1849fe06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:03:18 -1000 Subject: [PATCH 0971/2056] [light] Replace initial_state storage with flash-resident callback (#15133) --- esphome/components/light/__init__.py | 12 +++++- esphome/components/light/light_state.cpp | 7 ++-- esphome/components/light/light_state.h | 10 +++-- .../fixtures/light_initial_state.yaml | 39 +++++++++++++++++++ tests/integration/test_light_initial_state.py | 38 ++++++++++++++++++ 5 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 tests/integration/fixtures/light_initial_state.yaml create mode 100644 tests/integration/test_light_initial_state.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 64452e42820..4090ca57c25 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -262,6 +262,8 @@ async def setup_light_core_(light_var, config, output_var): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None: + # Emit a stateless lambda that constructs the initial state — values live + # in flash as code, not stored in the LightState object (~40 bytes saved). initial_state = LightStateRTCState( initial_state_config.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), initial_state_config.get(CONF_STATE, False), @@ -275,7 +277,13 @@ async def setup_light_core_(light_var, config, output_var): initial_state_config.get(CONF_COLD_WHITE, 1.0), initial_state_config.get(CONF_WARM_WHITE, 1.0), ) - cg.add(light_var.set_initial_state(initial_state)) + args = [(LightStateRTCState.operator("ref"), "s")] + lamb = await cg.process_lambda( + Lambda(f"s = {initial_state};"), + args, + return_type=cg.void, + ) + cg.add(light_var.set_initial_state(lamb)) if ( default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 1b736d84f68..bd778926d55 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -37,8 +37,9 @@ void LightState::setup() { auto call = this->make_call(); LightStateRTCState recovered{}; - if (this->initial_state_.has_value()) { - recovered = *this->initial_state_; + if (this->initial_state_callback_) { + this->initial_state_callback_(recovered); + this->initial_state_callback_ = nullptr; // One-shot — no longer needed } switch (this->restore_mode_) { case LIGHT_RESTORE_DEFAULT_OFF: @@ -195,7 +196,7 @@ void LightState::set_flash_transition_length(uint32_t flash_transition_length) { uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } -void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } +void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ab7f2e4df85..5efc05358b7 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -188,8 +188,9 @@ class LightState : public EntityBase, public Component { /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); - /// Set the initial state of this light - void set_initial_state(const LightStateRTCState &initial_state); + /// Set a callback to populate the initial state defaults during setup. + /// The callback is called once, then cleared. Values live in flash as code. + void set_initial_state(void (*callback)(LightStateRTCState &)); /// Return whether the light has any effects that meet the trait requirements. bool supports_effects(); @@ -342,8 +343,9 @@ class LightState : public EntityBase, public Component { */ std::unique_ptr<std::vector<LightTargetStateReachedListener *>> target_state_reached_listeners_; - /// Initial state of the light. - optional<LightStateRTCState> initial_state_{}; + /// Callback to populate initial state defaults — called once during setup, then cleared. + /// Values live in flash as function body; no per-instance data storage beyond this pointer. + void (*initial_state_callback_)(LightStateRTCState &){nullptr}; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; diff --git a/tests/integration/fixtures/light_initial_state.yaml b/tests/integration/fixtures/light_initial_state.yaml new file mode 100644 index 00000000000..2654c76aa05 --- /dev/null +++ b/tests/integration/fixtures/light_initial_state.yaml @@ -0,0 +1,39 @@ +esphome: + name: light-initial-state-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: test_red + type: float + write_action: + - lambda: "" + - platform: template + id: test_green + type: float + write_action: + - lambda: "" + - platform: template + id: test_blue + type: float + write_action: + - lambda: "" + +light: + - platform: rgb + name: "Test Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + restore_mode: ALWAYS_OFF + initial_state: + color_mode: RGB + state: true + brightness: 0.75 + red: 1.0 + green: 0.5 + blue: 0.0 diff --git a/tests/integration/test_light_initial_state.py b/tests/integration/test_light_initial_state.py new file mode 100644 index 00000000000..f1cd96dbf03 --- /dev/null +++ b/tests/integration/test_light_initial_state.py @@ -0,0 +1,38 @@ +"""Integration test for light initial_state configuration. + +Tests that the initial_state values are correctly applied at boot when +no saved preferences exist. The initial_state callback populates defaults +that the restore logic uses as a fallback. +""" + +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_initial_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that initial_state values are applied at boot.""" + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + light = require_entity(entities, "test_light") + + helper = InitialStateHelper(entities) + client.subscribe_states(helper.on_state_wrapper(lambda s: None)) + await helper.wait_for_initial_states() + + state = helper.initial_states[light.key] + + # restore_mode: ALWAYS_OFF overrides state to false + assert state.state is False + + # But the color values from initial_state should be applied + assert state.brightness == pytest.approx(0.75, abs=0.05) + assert state.red == pytest.approx(1.0, abs=0.01) + assert state.green == pytest.approx(0.5, abs=0.01) + assert state.blue == pytest.approx(0.0, abs=0.01) From b6aec4fa25bb9f7fdb09e9738385f5b8fed45267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:03:30 -1000 Subject: [PATCH 0972/2056] [ethernet] Add W5100 support for RP2040 (#15131) --- esphome/components/ethernet/__init__.py | 20 +++++++++----- .../components/ethernet/ethernet_component.h | 10 +++++++ .../ethernet/ethernet_component_rp2040.cpp | 26 ++++++++++++++----- esphome/core/defines.h | 1 + .../ethernet/common-w5100-rp2040.yaml | 18 +++++++++++++ .../ethernet/test-w5100.rp2040-ard.yaml | 1 + 6 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 tests/components/ethernet/common-w5100-rp2040.yaml create mode 100644 tests/components/ethernet/test-w5100.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e17abfcc931..17459cabb65 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -115,6 +115,7 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5100": EthernetType.ETHERNET_TYPE_W5100, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, @@ -132,6 +133,7 @@ _PHY_TYPE_TO_DEFINE = { "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "W5100": "USE_ETHERNET_W5100", "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", @@ -164,9 +166,15 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { # These types are always external IDF components (never built-in to ESP-IDF) _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} -SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] +# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) +SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] +RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"} +_RP2040_SPI_LIBRARIES = { + "W5100": "lwIP_w5100", + "W5500": "lwIP_w5500", + "ENC28J60": "lwIP_enc28j60", +} SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -295,7 +303,7 @@ def _validate(config): ) elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: raise cv.Invalid( - f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, " + f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, " f"not {config[CONF_TYPE]}" ) return config @@ -382,6 +390,7 @@ CONFIG_SCHEMA = cv.All( "JL1101": RMII_SCHEMA, "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, + "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, @@ -574,10 +583,7 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - if config[CONF_TYPE] == "ENC28J60": - cg.add_library("lwIP_enc28j60", None) - else: - cg.add_library("lwIP_w5500", None) + cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 4c85c39eb86..c6e37d01ea2 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -25,6 +25,8 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #ifdef USE_RP2040 #if defined(USE_ETHERNET_W5500) #include <W5500lwIP.h> +#elif defined(USE_ETHERNET_W5100) +#include <W5100lwIP.h> #elif defined(USE_ETHERNET_ENC28J60) #include <ENC28J60lwIP.h> #else @@ -59,6 +61,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5100, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, @@ -222,8 +225,15 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5100) + static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time +#else + static constexpr uint32_t RESET_DELAY_MS = 10; +#endif #if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W5100) + Wiznet5100lwIP *eth_{nullptr}; #elif defined(USE_ETHERNET_ENC28J60) ENC28J60lwIP *eth_{nullptr}; #else diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index bd8c4589851..9771bc59d5a 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,12 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for chip to initialize after reset + // W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms + delay(RESET_DELAY_MS); // NOLINT } // Create the SPI Ethernet device instance #if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W5100) + this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #elif defined(USE_ETHERNET_ENC28J60) this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #endif @@ -80,8 +83,8 @@ void EthernetComponent::setup() { // or via GPIO interrupt when one is provided. // Don't set started_ here — let the link polling in loop() set it - // when the W5500 link is actually up. Setting it prematurely causes - // a "Starting → Stopped → Starting" log sequence because the W5500 + // when the link is actually up. Setting it prematurely causes + // a "Starting → Stopped → Starting" log sequence because the chip // needs time after begin() before the PHY link is ready. } @@ -89,14 +92,21 @@ void EthernetComponent::loop() { // On RP2040, we need to poll connection state since there are no events. const uint32_t now = App.get_loop_component_start_time(); - // Throttle link/IP polling to avoid excessive SPI transactions from linkStatus() - // which reads the W5500 PHY register via SPI on every call. + // Throttle link/IP polling to avoid excessive SPI transactions. + // W5500/ENC28J60 read PHY register via SPI on every linkStatus() call. + // W5100 can't detect link state, so we skip the SPI read and assume link-up. // connected() reads netif->ip_addr without LwIPLock, but this is a single // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale // value, which is benign for polling. if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) { this->last_link_check_ = now; +#if defined(USE_ETHERNET_W5100) + // W5100 can't detect link (isLinkDetectable() returns false), so linkStatus() + // returns Unknown — assume link is up after successful begin() + bool link_up = true; +#else bool link_up = this->eth_->linkStatus() == LinkON; +#endif bool has_ip = this->eth_->connected(); if (!link_up) { @@ -171,6 +181,8 @@ void EthernetComponent::dump_config() { const char *type_str = "Unknown"; #if defined(USE_ETHERNET_W5500) type_str = "W5500"; +#elif defined(USE_ETHERNET_W5100) + type_str = "W5100"; #elif defined(USE_ETHERNET_ENC28J60) type_str = "ENC28J60"; #endif @@ -226,7 +238,7 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // Both W5500 and ENC28J60 are full-duplex on RP2040 + // W5100, W5500, and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } @@ -235,7 +247,7 @@ eth_speed_t EthernetComponent::get_link_speed() { // ENC28J60 is 10Mbps only return ETH_SPEED_10M; #else - // W5500 is always 100Mbps + // W5100 and W5500 are 100Mbps return ETH_SPEED_100M; #endif } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 996818c2e63..676ad3024f7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -284,6 +284,7 @@ #define USE_ETHERNET_SPI #define USE_ETHERNET_SPI_POLLING_SUPPORT #define USE_ETHERNET_OPENETH +#define USE_ETHERNET_W5100 #define USE_ETHERNET_W5500 #define USE_ETHERNET_DM9051 #define CONFIG_ETH_SPI_ETHERNET_W5500 1 diff --git a/tests/components/ethernet/common-w5100-rp2040.yaml b/tests/components/ethernet/common-w5100-rp2040.yaml new file mode 100644 index 00000000000..4c6d0313df9 --- /dev/null +++ b/tests/components/ethernet/common-w5100-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W5100 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-w5100.rp2040-ard.yaml b/tests/components/ethernet/test-w5100.rp2040-ard.yaml new file mode 100644 index 00000000000..e101f3112aa --- /dev/null +++ b/tests/components/ethernet/test-w5100.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w5100-rp2040.yaml From f457b995f726d54581421f0a885fdab4b922cdb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:03:56 -1000 Subject: [PATCH 0973/2056] [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) --- .../components/datetime/datetime_entity.cpp | 3 + esphome/core/time.cpp | 2 +- esphome/core/time.h | 18 ++++-- tests/components/time/posix_tz_parser.cpp | 56 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 730abb3ca8b..fa50271f044 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const { obj.year = this->year_; obj.month = this->month_; obj.day_of_month = this->day_; + obj.day_of_week = 0; + obj.day_of_year = 0; + obj.is_dst = false; obj.hour = this->hour_; obj.minute = this->minute_; obj.second = this->second_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 6add82e7d14..650c61d37bb 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -231,7 +231,7 @@ void ESPTime::increment_day() { void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { time_t res = 0; - if (!this->fields_in_range()) { + if (!this->fields_in_range(false, use_day_of_year)) { this->timestamp = -1; return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 1716c51ffd5..ed474320381 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -79,11 +79,19 @@ struct ESPTime { /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && - this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + /// Check if time fields are in range. + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool fields_in_range(bool check_day_of_week = true, bool check_day_of_year = true) const { + bool valid = this->second < 61 && this->minute < 60 && this->hour < 24 && this->month > 0 && this->month < 13 && + this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + if (check_day_of_week) { + valid = valid && this->day_of_week > 0 && this->day_of_week < 8; + } + if (check_day_of_year) { + valid = valid && this->day_of_year > 0 && this->day_of_year < 367; + } + return valid; } /** Convert a string to ESPTime struct as specified by the format argument. diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index d1747ef5b19..b7cf2a4afad 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -1036,8 +1036,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m t.hour = hour; t.minute = min; t.second = sec; - t.day_of_week = 1; // Placeholder for fields_in_range() - t.day_of_year = 1; t.recalc_timestamp_local(); return t.timestamp; } @@ -1187,6 +1185,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { EXPECT_EQ(esp_result, libc_result); } +TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) { + // Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs + // an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or + // day_of_year). recalc_timestamp_local() must work without those fields. + const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Construct ESPTime with only date/time fields (like state_as_esptime does) + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 20; + t.hour = 23; + t.minute = 14; + t.second = 55; + // day_of_week and day_of_year are deliberately left as 0 + t.recalc_timestamp_local(); + + // Must NOT return -1 (the bug: fields_in_range() rejected valid times) + EXPECT_NE(t.timestamp, -1); + + // Verify against libc + time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55); + EXPECT_EQ(t.timestamp, libc_result); +} + +TEST(RecalcTimestampLocal, MinimalFieldsNoDST) { + // Same test but with a timezone that has no DST + const char *tz_str = "IST-5:30"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 23; + t.hour = 10; + t.minute = 0; + t.second = 0; + t.recalc_timestamp_local(); + + EXPECT_NE(t.timestamp, -1); + + time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0); + EXPECT_EQ(t.timestamp, libc_result); +} + TEST(RecalcTimestampLocal, YearBoundaryDST) { // Test southern hemisphere DST across year boundary // Australia/Sydney: DST active from October to April (spans Jan 1) From 238adbe008b5adbe60fca970cf96343e96a3dba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:04:17 -1000 Subject: [PATCH 0974/2056] [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) --- esphome/components/wifi/wifi_component.cpp | 66 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 6 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e1d4b07471b..2ed4b32a7a5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -287,18 +287,25 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ (counter reset to 0) │ │ (retry_connect called) │ /// │ └──────────────────────────────────┘ └───────────┬─────────────┘ /// │ │ │ -/// │ ↓ │ -/// │ ┌───────────────────────┐ │ -/// │ │ → IDLE │ │ -/// │ │ (counter preserved!) │ │ -/// │ └───────────────────────┘ │ +/// │ ┌─────────┴─────────┐ │ +/// │ ↓ ↓ │ +/// │ on target BSSID on other AP │ +/// │ │ │ │ +/// │ ↓ ↓ │ +/// │ ┌──────────────────┐ ┌────────────┐│ +/// │ │ → IDLE │ │ → IDLE ││ +/// │ │ (counter reset) │ │ (counter ││ +/// │ │ (roam worked!) │ │ preserved)││ +/// │ └──────────────────┘ └────────────┘│ /// │ │ /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect after scan (within grace period): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ -/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ +/// │ - Roaming success via retry (on target BSSID): counter reset │ +/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │ /// └──────────────────────────────────────────────────────────────────────┘ // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) @@ -1576,17 +1583,33 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Only preserve attempts if reconnecting after a failed roam attempt // This prevents ping-pong between APs when a roam target is unreachable if (this->roaming_state_ == RoamingState::CONNECTING) { - // Successful roam to better AP - reset attempts so we can roam again later + // Successful roam to better AP on first try - reset attempts so we can roam again later ESP_LOGD(TAG, "Roam successful"); this->roaming_attempts_ = 0; } else if (this->roaming_state_ == RoamingState::RECONNECTING) { - // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong - ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + // Check if we ended up on the roam target despite needing a retry + // (e.g., first connect failed but scan-based retry found and connected to the same better AP) + bssid_t current_bssid = this->wifi_bssid(); + if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) { + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(current_bssid.data(), bssid_buf); + ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS, + bssid_buf); + this->roaming_attempts_ = 0; + } else if (this->roaming_target_bssid_ != bssid_t{}) { + // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong + ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } else { + // Reconnected after scan-induced disconnect (no roam target) - keep attempts + ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } } else { // Normal connection (boot, credentials changed, etc.) this->roaming_attempts_ = 0; } this->roaming_state_ = RoamingState::IDLE; + this->roaming_target_bssid_ = {}; + this->roaming_scan_end_ = 0; // Clear all priority penalties - the next reconnect will happen when an AP disconnects, // which means the landscape has likely changed and previous tracked failures are stale @@ -2073,8 +2096,16 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { - // Not a roaming-triggered reconnect, reset state - this->clear_roaming_state_(); + // Check if a roaming scan recently completed - on ESP8266, going off-channel + // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes. + // Transition to RECONNECTING so the attempts counter is preserved on reconnect. + if (this->roaming_scan_end_ != 0 && millis() - this->roaming_scan_end_ < ROAMING_SCAN_GRACE_PERIOD) { + ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; + } else { + // Not a roaming-triggered reconnect, reset state + this->clear_roaming_state_(); + } } // RECONNECTING: keep state and counter, still trying to reconnect @@ -2307,6 +2338,8 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> void WiFiComponent::clear_roaming_state_() { this->roaming_attempts_ = 0; this->roaming_last_check_ = 0; + this->roaming_scan_end_ = 0; + this->roaming_target_bssid_ = {}; this->roaming_state_ = RoamingState::IDLE; } @@ -2374,7 +2407,7 @@ void WiFiComponent::check_roaming_(uint32_t now) { // Guard: skip scan if signal is already good (no meaningful improvement possible) int8_t rssi = this->wifi_rssi(); if (rssi > ROAMING_GOOD_RSSI) { - ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, + ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); return; } @@ -2388,6 +2421,9 @@ void WiFiComponent::process_roaming_scan_() { this->scan_done_ = false; // Default to IDLE - will be set to CONNECTING if we find a better AP this->roaming_state_ = RoamingState::IDLE; + // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout) + // can be attributed to the scan and avoid resetting the attempts counter + this->roaming_scan_end_ = millis(); // Get current connection info int8_t current_rssi = this->wifi_rssi(); @@ -2436,10 +2472,12 @@ void WiFiComponent::process_roaming_scan_() { WiFiAP roam_params = *selected; apply_scan_result_to_params(roam_params, *best); - this->release_scan_results_(); // Mark as roaming attempt - affects retry behavior if connection fails this->roaming_state_ = RoamingState::CONNECTING; + this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results + + this->release_scan_results_(); // Connect directly - wifi_sta_connect_ handles disconnect internally this->start_connecting(roam_params); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 718f4a6e12c..99b23436f7d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -779,6 +779,10 @@ class WiFiComponent final : public Component { static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; + // Grace period after roaming scan completes. If WiFi disconnects within this + // window (e.g., ESP8266 Beacon Timeout caused by going off-channel during scan), + // the disconnect is treated as roaming-related and the attempts counter is preserved. + static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD = 30 * 1000; // 30 seconds // 4-byte members float output_power_{NAN}; @@ -786,6 +790,7 @@ class WiFiComponent final : public Component { uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t roaming_last_check_{0}; + uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif @@ -810,6 +815,7 @@ class WiFiComponent final : public Component { bool error_from_callback_{false}; RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; + bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; #endif From 9c9ae190ee040b7eb97f7e82d74e73b7ea6cd7d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:13:59 -1000 Subject: [PATCH 0975/2056] [core] Use compile-time HasElse parameter in IfAction (#15134) --- esphome/automation.py | 7 +++++-- esphome/core/base_automation.h | 27 ++++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 36ab30b654a..17966dc782c 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -413,13 +413,16 @@ async def if_action_to_code( template_arg: cg.TemplateArguments, args: TemplateArgsType, ) -> MockObj: + has_else = CONF_ELSE in config + # Prepend HasElse bool to template arguments: IfAction<HasElse, Ts...> + if_template_arg = cg.TemplateArguments(has_else, *template_arg) cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) condition = await build_condition(config[cond_conf], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, condition) + var = cg.new_Pvariable(action_id, if_template_arg, condition) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) - if CONF_ELSE in config: + if has_else: actions = await build_action_list(config[CONF_ELSE], template_arg, args) cg.add(var.add_else(actions)) return var diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 985f26e7116..efcffa8824d 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -264,7 +264,7 @@ template<typename... Ts> class WhileLoopContinuation : public Action<Ts...> { WhileAction<Ts...> *parent_; }; -template<typename... Ts> class IfAction : public Action<Ts...> { +template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> { public: explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {} @@ -273,27 +273,25 @@ template<typename... Ts> class IfAction : public Action<Ts...> { this->then_.add_action(new ContinuationAction<Ts...>(this)); } - void add_else(const std::initializer_list<Action<Ts...> *> &actions) { + void add_else(const std::initializer_list<Action<Ts...> *> &actions) requires(HasElse) { this->else_.add_actions(actions); this->else_.add_action(new ContinuationAction<Ts...>(this)); } void play_complex(const Ts &...x) override { this->num_running_++; - bool res = this->condition_->check(x...); - if (res) { - if (this->then_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + if (this->condition_->check(x...)) { + if (!this->then_.empty() && this->num_running_ > 0) { this->then_.play(x...); + return; } - } else { - if (this->else_.empty()) { - this->play_next_(x...); - } else if (this->num_running_ > 0) { + } else if constexpr (HasElse) { + if (!this->else_.empty() && this->num_running_ > 0) { this->else_.play(x...); + return; } } + this->play_next_(x...); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -301,13 +299,16 @@ template<typename... Ts> class IfAction : public Action<Ts...> { void stop() override { this->then_.stop(); - this->else_.stop(); + if constexpr (HasElse) { + this->else_.stop(); + } } protected: Condition<Ts...> *condition_; ActionList<Ts...> then_; - ActionList<Ts...> else_; + struct NoElse {}; + [[no_unique_address]] std::conditional_t<HasElse, ActionList<Ts...>, NoElse> else_; }; template<typename... Ts> class WhileAction : public Action<Ts...> { From 26e78c840ce4e35589245279e7d39f27039af851 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:21:04 -0400 Subject: [PATCH 0976/2056] [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) --- esphome/components/wifi/wifi_component.cpp | 8 ++++++++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2ed4b32a7a5..620d1a083dc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2222,6 +2222,14 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) || + (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) { + ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel); + this->selected_sta_index_ = -1; + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 99b23436f7d..55e532c37d2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -774,6 +774,8 @@ class WiFiComponent final : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif + static constexpr uint8_t FIRST_5GHZ_CHANNEL = 36; + // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 2866ec15137..1b80adc82eb 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -987,6 +987,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = 100; config.scan_time.active.max = 300; } + // When scanning while connected (roaming), return to home channel between + // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) + if (this->roaming_state_ == RoamingState::SCANNING) { + config.coex_background_scan = true; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From 690dc324c97d753788491b1fa8c423776af2a1a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:52:37 -1000 Subject: [PATCH 0977/2056] [logger] Move task log buffer storage to BSS (#15153) --- esphome/components/logger/__init__.py | 20 +++++-------- esphome/components/logger/logger.cpp | 26 +++++------------ esphome/components/logger/logger.h | 13 +++------ .../logger/task_log_buffer_esp32.cpp | 18 +++--------- .../components/logger/task_log_buffer_esp32.h | 13 ++++----- .../logger/task_log_buffer_host.cpp | 17 +++-------- .../components/logger/task_log_buffer_host.h | 13 ++------- .../logger/task_log_buffer_libretiny.cpp | 29 +++++++------------ .../logger/task_log_buffer_libretiny.h | 12 ++++---- .../logger/task_log_buffer_zephyr.cpp | 13 ++++----- .../logger/task_log_buffer_zephyr.h | 10 ++++--- esphome/core/defines.h | 6 ++++ tests/benchmarks/components/main.cpp | 2 +- tests/components/main.cpp | 2 +- tests/dummy_main.cpp | 2 +- 15 files changed, 69 insertions(+), 127 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 4345e291a30..4144543b89a 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -331,9 +331,8 @@ async def to_code(config: ConfigType) -> None: CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early - # so the constructor can allocate the buffer immediately, preventing a race - # where another task logs before the buffer is initialized. + # Determine task log buffer size. The buffer is a direct member of Logger + # (no separate heap allocation). task_log_buffer_size = 0 if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] @@ -341,16 +340,11 @@ async def to_code(config: ConfigType) -> None: task_log_buffer_size = 64 # Fixed 64 slots for host if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - task_log_buffer_size, - ) - else: - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) + cg.add_define("ESPHOME_TASK_LOG_BUFFER_SIZE", task_log_buffer_size) + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index ceacded7756..cd6543bfb84 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -83,7 +83,7 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks message_sent = - this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args); + this->log_buffer_.send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args); if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -152,23 +152,13 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } -#ifdef USE_ESPHOME_TASK_LOG_BUFFER -Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { -#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) -this->main_thread_ = pthread_self(); -#endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); - // Note: we don't disable loop here because the component isn't registered with App yet. - // The loop self-disables on its first iteration when it finds no messages to process. + this->main_thread_ = pthread_self(); #endif } @@ -184,16 +174,16 @@ void Logger::loop() { void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available - if (this->log_buffer_->has_messages()) { + if (this->log_buffer_.has_messages()) { logger::TaskLogBuffer::LogMessage *message; uint16_t text_length; - while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { + while (this->log_buffer_.borrow_message_main_loop(message, text_length)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; LogBuffer buf{this->tx_buffer_, ESPHOME_LOGGER_TX_BUFFER_SIZE}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text_data(), text_length, buf); // Release the message to allow other tasks to use it as soon as possible - this->log_buffer_->release_message_main_loop(); + this->log_buffer_.release_message_main_loop(); this->write_log_buffer_to_console_(buf); } } @@ -239,13 +229,11 @@ void Logger::dump_config() { this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER - if (this->log_buffer_) { #ifdef USE_HOST - ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast<unsigned int>(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast<unsigned int>(this->log_buffer_.size())); #else - ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast<unsigned int>(this->log_buffer_->size())); + ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast<unsigned int>(this->log_buffer_.size())); #endif - } #endif #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index c81b8e4e94f..784cbea67ee 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,11 +143,7 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); -#else explicit Logger(uint32_t baud_rate); -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; #endif @@ -353,10 +349,6 @@ class Logger final : public Component { #ifdef USE_LOGGER_LEVEL_LISTENERS std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners #endif -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed -#endif - // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -374,8 +366,11 @@ class Logger final : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - // Large buffer placed last to keep frequently-accessed member offsets small + // Large buffers placed last to keep frequently-accessed member offsets small char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + logger::TaskLogBuffer log_buffer_; // Embedded in Logger (no separate heap allocation) +#endif // --- get_thread_name_ overloads (per-platform) --- diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index e747ddc4d8b..cb97f5504fa 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -1,33 +1,23 @@ #ifdef USE_ESP32 #include "task_log_buffer_esp32.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // Store the buffer size - this->size_ = total_buffer_size; - // Allocate memory for the ring buffer using ESPHome's RAM allocator - RAMAllocator<uint8_t> allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create a static ring buffer with RINGBUF_TYPE_NOSPLIT for message integrity - this->ring_buffer_ = xRingbufferCreateStatic(this->size_, RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); + // Storage is a member array (embedded in Logger), no heap allocation needed + this->ring_buffer_ = + xRingbufferCreateStatic(sizeof(this->storage_), RINGBUF_TYPE_NOSPLIT, this->storage_, &this->structure_); } TaskLogBuffer::~TaskLogBuffer() { if (this->ring_buffer_ != nullptr) { - // Delete the ring buffer vRingbufferDelete(this->ring_buffer_); this->ring_buffer_ = nullptr; - - // Free the allocated memory - RAMAllocator<uint8_t> allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; } } diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index 88d72eacfc4..e8197667954 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -8,7 +8,6 @@ #ifdef USE_ESPHOME_TASK_LOG_BUFFER #include <cstddef> #include <cstring> -#include <memory> #include <atomic> #include <freertos/FreeRTOS.h> #include <freertos/ringbuf.h> @@ -47,8 +46,7 @@ class TaskLogBuffer { inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the ring buffer, only call from main loop @@ -67,13 +65,12 @@ class TaskLogBuffer { } // Get the total buffer size in bytes - inline size_t size() const { return size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: - RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle - StaticRingbuffer_t structure_; // Static structure for the ring buffer - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory + RingbufHandle_t ring_buffer_{nullptr}; // FreeRTOS ring buffer handle + StaticRingbuffer_t structure_; // Static structure for the ring buffer + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Atomic counter for message tracking (only differences matter) std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index c2ab009db47..8ebc9463837 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -10,22 +10,13 @@ namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { - // Allocate message slots - this->slots_ = std::make_unique<LogMessage[]>(slot_count); -} - -TaskLogBuffer::~TaskLogBuffer() { - // unique_ptr handles cleanup automatically -} - int TaskLogBuffer::acquire_write_slot_() { // Try to reserve a slot using compare-and-swap size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); while (true) { // Calculate next index (with wrap-around) - size_t next_reserve = (current_reserve + 1) % this->slot_count_; + size_t next_reserve = (current_reserve + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // Check if buffer would be full // Buffer is full when next write position equals read position @@ -50,7 +41,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Try to advance the write_index if we're the next expected commit // This ensures messages are read in order size_t expected = slot_index; - size_t next = (slot_index + 1) % this->slot_count_; + size_t next = (slot_index + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; // We only advance write_index if this slot is the next one expected // This handles out-of-order commits correctly @@ -63,7 +54,7 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) { // Successfully advanced, check if next slot is also ready expected = next; - next = (next + 1) % this->slot_count_; + next = (next + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; if (!this->slots_[expected].ready.load(std::memory_order_acquire)) { break; } @@ -142,7 +133,7 @@ void TaskLogBuffer::release_message_main_loop() { this->slots_[current_read].ready.store(false, std::memory_order_release); // Advance read index - size_t next_read = (current_read + 1) % this->slot_count_; + size_t next_read = (current_read + 1) % ESPHOME_TASK_LOG_BUFFER_SIZE; this->read_index_.store(next_read, std::memory_order_release); } diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index 1d4d2b0ec1c..25e9c4da58c 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -11,7 +11,6 @@ #include <cstdarg> #include <cstddef> #include <cstring> -#include <memory> #include <pthread.h> namespace esphome::logger { @@ -50,9 +49,6 @@ namespace esphome::logger { */ class TaskLogBuffer { public: - // Default number of message slots - host has plenty of memory - static constexpr size_t DEFAULT_SLOT_COUNT = 64; - // Structure for a log message (fixed size for lock-free operation) struct LogMessage { // Size constants @@ -74,9 +70,7 @@ class TaskLogBuffer { inline char *text_data() { return this->text; } }; - /// Constructor that takes the number of message slots - explicit TaskLogBuffer(size_t slot_count); - ~TaskLogBuffer(); + TaskLogBuffer() = default; // NOT thread-safe - get next message from buffer, only call from main loop // Returns true if a message was retrieved, false if buffer is empty @@ -96,7 +90,7 @@ class TaskLogBuffer { } // Get the buffer size (number of slots) - inline size_t size() const { return slot_count_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Acquire a slot for writing (thread-safe) @@ -106,8 +100,7 @@ class TaskLogBuffer { // Commit a slot after writing (thread-safe) void commit_write_slot_(int slot_index); - std::unique_ptr<LogMessage[]> slots_; // Pre-allocated message slots - size_t slot_count_; // Number of slots + LogMessage slots_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) // Lock-free indices using atomics // - reserve_index_: Next slot to reserve (producers CAS this to claim slots) diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 5969f6fb401..b6d6b22ab5b 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -1,19 +1,15 @@ #ifdef USE_LIBRETINY #include "task_log_buffer_libretiny.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER namespace esphome::logger { -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - this->size_ = total_buffer_size; - // Allocate memory for the circular buffer using ESPHome's RAM allocator - RAMAllocator<uint8_t> allocator; - this->storage_ = allocator.allocate(this->size_); +TaskLogBuffer::TaskLogBuffer() { // Create mutex for thread-safe access + // Storage is a member array (embedded in Logger), no heap allocation needed this->mutex_ = xSemaphoreCreateMutex(); } @@ -22,11 +18,6 @@ TaskLogBuffer::~TaskLogBuffer() { vSemaphoreDelete(this->mutex_); this->mutex_ = nullptr; } - if (this->storage_ != nullptr) { - RAMAllocator<uint8_t> allocator; - allocator.deallocate(this->storage_, this->size_); - this->storage_ = nullptr; - } } size_t TaskLogBuffer::available_contiguous_space() const { @@ -34,7 +25,7 @@ size_t TaskLogBuffer::available_contiguous_space() const { // head is ahead of or equal to tail // Available space is from head to end, plus from start to tail // But for contiguous, just from head to end (minus 1 to avoid head==tail ambiguity) - size_t space_to_end = this->size_ - this->head_; + size_t space_to_end = ESPHOME_TASK_LOG_BUFFER_SIZE - this->head_; if (this->tail_ == 0) { // Can't use the last byte or head would equal tail return space_to_end > 0 ? space_to_end - 1 : 0; @@ -48,8 +39,8 @@ size_t TaskLogBuffer::available_contiguous_space() const { } bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { return false; } @@ -86,7 +77,7 @@ void TaskLogBuffer::release_message_main_loop() { this->tail_ += this->current_message_size_; // Handle wrap-around if we've reached the end - if (this->tail_ >= this->size_) { + if (this->tail_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->tail_ = 0; } @@ -115,9 +106,9 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin // Calculate total size needed (header + text length + null terminator) size_t total_size = message_total_size(text_length); - // Check if buffer was initialized successfully - if (this->mutex_ == nullptr || this->storage_ == nullptr) { - return false; // Buffer not initialized, fall back to direct output + // Check if mutex was initialized successfully + if (this->mutex_ == nullptr) { + return false; // Mutex not initialized, fall back to direct output } // Try to acquire mutex without blocking - don't block logging tasks @@ -185,7 +176,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin this->head_ += total_size; // Handle wrap-around (shouldn't happen due to contiguous space check, but be safe) - if (this->head_ >= this->size_) { + if (this->head_ >= ESPHOME_TASK_LOG_BUFFER_SIZE) { this->head_ = 0; } diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index c065065fe78..b42894502a1 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -59,8 +59,7 @@ class TaskLogBuffer { // Valid log levels are 0-7, so 0xFF cannot be a real message static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); + TaskLogBuffer(); ~TaskLogBuffer(); // NOT thread-safe - borrow a message from the buffer, only call from main loop @@ -78,7 +77,7 @@ class TaskLogBuffer { inline bool HOT has_messages() const { return this->message_count_ != 0; } // Get the total buffer size in bytes - inline size_t size() const { return this->size_; } + static constexpr size_t size() { return ESPHOME_TASK_LOG_BUFFER_SIZE; } private: // Calculate total size needed for a message (header + text + null terminator) @@ -87,10 +86,9 @@ class TaskLogBuffer { // Calculate available contiguous space at write position size_t available_contiguous_space() const; - uint8_t *storage_{nullptr}; // Pointer to allocated memory - size_t size_{0}; // Size of allocated memory - size_t head_{0}; // Write position - size_t tail_{0}; // Read position + uint8_t storage_[ESPHOME_TASK_LOG_BUFFER_SIZE]; // Embedded in Logger (no separate heap allocation) + size_t head_{0}; // Write position + size_t tail_{0}; // Read position SemaphoreHandle_t mutex_{nullptr}; // FreeRTOS mutex for thread safety volatile uint16_t message_count_{0}; // Fast check counter (dirty read OK) diff --git a/esphome/components/logger/task_log_buffer_zephyr.cpp b/esphome/components/logger/task_log_buffer_zephyr.cpp index 44d12d08a3b..a994925a54f 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.cpp +++ b/esphome/components/logger/task_log_buffer_zephyr.cpp @@ -17,19 +17,16 @@ static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) { return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length); } -TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { - // alignment to 4 bytes - total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t); - this->mpsc_config_.buf = new uint32_t[total_buffer_size]; - this->mpsc_config_.size = total_buffer_size; +TaskLogBuffer::TaskLogBuffer() { + // Storage is a member array (embedded in Logger), no heap allocation needed + this->mpsc_config_.buf = this->buf_storage_; + this->mpsc_config_.size = BUF_WORD_COUNT; this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE; - this->mpsc_config_.get_wlen = get_wlen, + this->mpsc_config_.get_wlen = get_wlen; mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_); } -TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; } - bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) diff --git a/esphome/components/logger/task_log_buffer_zephyr.h b/esphome/components/logger/task_log_buffer_zephyr.h index cc2ed1f6872..4f192366ad8 100644 --- a/esphome/components/logger/task_log_buffer_zephyr.h +++ b/esphome/components/logger/task_log_buffer_zephyr.h @@ -33,15 +33,14 @@ class TaskLogBuffer { // Methods for accessing message contents inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); } }; - // Constructor that takes a total buffer size - explicit TaskLogBuffer(size_t total_buffer_size); - ~TaskLogBuffer(); + TaskLogBuffer(); + ~TaskLogBuffer() = default; // Check if there are messages ready to be processed using an atomic counter for performance inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); } // Get the total buffer size in bytes - inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); } + static constexpr size_t size() { return BUF_WORD_COUNT * sizeof(uint32_t); } // NOT thread-safe - borrow a message from the ring buffer, only call from main loop bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); @@ -54,6 +53,9 @@ class TaskLogBuffer { const char *format, va_list args); protected: + // Round up byte size to 32-bit word count for mpsc_pbuf alignment requirement + static constexpr size_t BUF_WORD_COUNT = (ESPHOME_TASK_LOG_BUFFER_SIZE + 3) / sizeof(uint32_t); + uint32_t buf_storage_[BUF_WORD_COUNT]; // Embedded in Logger (no separate heap allocation) mpsc_pbuf_buffer_config mpsc_config_{}; mpsc_pbuf_buffer log_buffer_{}; const mpsc_pbuf_generic *current_token_{}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 676ad3024f7..b5612a1d3f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -200,6 +200,7 @@ #define USE_ESP32_CRASH_HANDLER #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_SRAM1_AS_IRAM @@ -373,18 +374,23 @@ #define USE_WEBSERVER #define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #endif #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif #ifdef USE_NRF52 #define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 #define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER +#define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC #define USE_LOGGER_USB_CDC diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 901dc44c077..9bc0c31a159 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 622b1f107b0..373fde71516 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0, 64); + static esphome::logger::Logger test_logger(0); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 329286e2fab..6fa0c08aa3d 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200, 512); // NOLINT + auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); From af5b98c635f3209cfcc940a8341545eaaf74749a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 15:07:28 -1000 Subject: [PATCH 0978/2056] [time] Remove dummy placeholder values for recalc_timestamp_utc() (#15129) --- esphome/components/bm8563/bm8563.cpp | 1 - esphome/components/ds1307/ds1307.cpp | 3 --- esphome/components/gps/time/gps_time.cpp | 4 ---- esphome/components/pcf85063/pcf85063.cpp | 3 --- esphome/components/pcf8563/pcf8563.cpp | 3 --- esphome/components/rx8130/rx8130.cpp | 3 --- 6 files changed, 17 deletions(-) diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 07831485c1a..269acfea447 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -56,7 +56,6 @@ void BM8563::read_time() { ESPTime rtc_time; this->get_time_(rtc_time); this->get_date_(rtc_time); - rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 5c0e98290b7..8fff4213b4c 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -40,11 +40,8 @@ void DS1307Component::read_time() { .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), .day_of_week = uint8_t(ds1307_.reg.weekday), .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index cff8c1fb073..fb662a3d60e 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,10 +16,6 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); - // Set these to valid value for recalc_timestamp_utc - it's not used for calculation - val.day_of_week = 1; - val.day_of_year = 1; - val.hour = tiny_gps.time.hour(); val.minute = tiny_gps.time.minute(); val.second = tiny_gps.time.second(); diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 03ed78654fa..1cf28a49557 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -40,11 +40,8 @@ void PCF85063Component::read_time() { .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), .day_of_week = uint8_t(pcf85063_.reg.weekday), .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10), .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index dc68807aefa..b748f0156aa 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -40,11 +40,8 @@ void PCF8563Component::read_time() { .hour = uint8_t(pcf8563_.reg.hour + 10u * pcf8563_.reg.hour_10), .day_of_week = uint8_t(pcf8563_.reg.weekday), .day_of_month = uint8_t(pcf8563_.reg.day + 10u * pcf8563_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(pcf8563_.reg.month + 10u * pcf8563_.reg.month_10), .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0, // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 07ed7acc565..3b704d2551f 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -77,11 +77,8 @@ void RX8130Component::read_time() { .hour = bcd2dec(date[2] & 0x3f), .day_of_week = static_cast<uint8_t>((date[3] & 0x7f) ? __builtin_ctz(date[3] & 0x7f) + 1 : 1), .day_of_month = bcd2dec(date[4] & 0x3f), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = bcd2dec(date[5] & 0x1f), .year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { From 7a407595678d7b855fb79f8b2912fc13d1f1aaad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:55:12 +0000 Subject: [PATCH 0979/2056] Bump aioesphomeapi from 44.7.0 to 44.8.0 (#15159) 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 2e09e2ed994..9e75e6d0390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.7.0 +aioesphomeapi==44.8.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From c45c9da771c47dfbdfa966902a60540e778792c8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:51:23 +1000 Subject: [PATCH 0980/2056] [lvgl] Various 9.5 fixes (#15157) --- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/widgets/__init__.py | 2 +- esphome/components/lvgl/widgets/meter.py | 45 +++++++++++++++------ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b3cb4d56ad0..d26bcdc714b 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -673,14 +673,14 @@ void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *are * @param color_end The color to apply to the last tick * @param width */ -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local) { auto *scale = static_cast<lv_obj_t *>(lv_event_get_target(e)); lv_draw_task_t *task = lv_event_get_draw_task(e); if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) { auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task)); - auto tick = line_dsc->base.id1; + int tick = line_dsc->base.id2; if (tick >= range_start && tick <= range_end) { unsigned range = range_end - range_start; if (local) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 66f823d549e..7baeeb233b5 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -52,7 +52,7 @@ extern std::string lv_event_code_name_for(lv_event_t *event); lv_obj_t *lv_container_create(lv_obj_t *parent); #ifdef USE_LVGL_SCALE -void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, +void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local); #endif #if LV_COLOR_DEPTH == 16 diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index a2a8cf2129c..b383196963c 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -158,7 +158,7 @@ class WidgetType: await self.on_create(var, config) w = Widget.create(wid, var, self, config) - if theme := theme_widget_map.get(self.w_type.name): + if theme := theme_widget_map.get(self.name): for part, states in theme.items(): part = "LV_PART_" + part.upper() for state, style in states.items(): diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 6a7559c42c6..d32efd145bf 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -79,6 +79,7 @@ from ..types import ( from . import Widget, WidgetType, get_widgets, widget_to_code from .arc import CONF_ARC from .img import CONF_IMAGE +from .label import CONF_LABEL from .line import CONF_LINE CONF_ANGLE_RANGE = "angle_range" @@ -222,12 +223,31 @@ INDICATOR_SCHEMA = cv.Schema( } ) + +def _scale_validate(config): + if indicators := config.get(CONF_INDICATORS): + style_index = next( + ( + i + for i, indicator in enumerate(indicators) + if CONF_TICK_STYLE in indicator + ), + -1, + ) + if style_index >= 0 and CONF_TICKS not in config: + raise cv.Invalid( + "'tick_style' can't be applied if the enclosing scale has no 'ticks' configured", + path=[CONF_INDICATORS, style_index], + ) + return config + + SCALE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(lv_scale_t), cv.Optional(CONF_TICKS): cv.Schema( { - cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2), cv.Optional(CONF_WIDTH, default=2): cv.positive_int, cv.Optional(CONF_LENGTH, default=10): size, cv.Optional(CONF_RADIAL_OFFSET, default=0): size, @@ -251,7 +271,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } -) +).add_extra(_scale_validate) METER_SCHEMA = { cv.Optional(CONF_PIVOT): STATE_SCHEMA, @@ -259,17 +279,14 @@ METER_SCHEMA = { cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA), } +# Only handling light style at the moment LIGHT_STYLE = LVStyle( "lv_meter_light", { "bg_opa": 1.0, - "bg_color": 0xEEEEEE, - "line_width": 1, - "line_color": 0xEEEEEE, - "arc_width": 2, - "arc_color": 0xEEEEEE, + "bg_color": 0xFFFFFF, "pad_all": 10, - "border_width": 2, + "border_width": 3, "border_color": 0xEEEEEE, "radius": "LV_RADIUS_CIRCLE", }, @@ -329,7 +346,7 @@ class MeterType(WidgetType): ) def get_uses(self): - return CONF_SCALE, CONF_LINE, CONF_IMAGE + return CONF_SCALE, CONF_LINE, CONF_IMAGE, CONF_LABEL def validate(self, value): return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) @@ -478,6 +495,8 @@ class MeterType(WidgetType): await iw.set_property(CONF_SRC, await lv_image.process(src)) await set_indicator_values(iw, v) + # Hide the scale line + lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if ticks := scale_conf.get(CONF_TICKS): # Set total tick count lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT]) @@ -503,8 +522,6 @@ class MeterType(WidgetType): LV_PART.ITEMS, ) - # Hide the scale line - lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) if CONF_MAJOR in ticks: major = ticks[CONF_MAJOR] # Set major tick frequency @@ -547,7 +564,11 @@ class MeterType(WidgetType): else: lv.scale_set_major_tick_every(scale_var, 0) else: - lv.scale_set_total_tick_count(scale_var, 0) + # Must have at least 2 ticks otherwise the scale isn't even drawn + lv.scale_set_total_tick_count(scale_var, 2) + # Hide the ticks by making them 0 width + lv_obj.set_style_line_width(scale_var, 0, LV_PART.ITEMS) + lv.scale_set_major_tick_every(scale_var, 0) # Add a pivot # Get the default style From f5bbff0b05a677e7aa0d28218291a8b868c0fb39 Mon Sep 17 00:00:00 2001 From: Piotr Szulc <piotr.szulc@gmail.com> Date: Wed, 25 Mar 2026 12:40:39 +0100 Subject: [PATCH 0981/2056] [core] Add CONF_LIBRETINY constant to const.py (#15141) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/const/__init__.py | 1 + esphome/components/libretiny/const.py | 1 - esphome/components/libretiny/text_sensor.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 2a972a2939a..1fbf88c2764 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -13,6 +13,7 @@ CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_LIBRETINY = "libretiny" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index bc4ca99ab41..332be0de1dd 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -14,7 +14,6 @@ class LibreTinyComponent: supports_atomics: bool = False # True for Cortex-M4(F) with LDREX/STREX -CONF_LIBRETINY = "libretiny" CONF_LOGLEVEL = "loglevel" CONF_SDK_SILENT = "sdk_silent" CONF_GPIO_RECOVER = "gpio_recover" diff --git a/esphome/components/libretiny/text_sensor.py b/esphome/components/libretiny/text_sensor.py index fa33fb6c029..c1012774c88 100644 --- a/esphome/components/libretiny/text_sensor.py +++ b/esphome/components/libretiny/text_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.components.const import CONF_LIBRETINY import esphome.config_validation as cv from esphome.const import ( CONF_VERSION, @@ -7,7 +8,7 @@ from esphome.const import ( ICON_CELLPHONE_ARROW_DOWN, ) -from .const import CONF_LIBRETINY, LTComponent +from .const import LTComponent DEPENDENCIES = ["libretiny"] From 2355fcb44e0804b68d2892fbe49f574baf9a7a6a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:51:51 +1000 Subject: [PATCH 0982/2056] [lvgl] Update function and type names (#15109) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/lvgl/gradient.py | 2 +- esphome/components/lvgl/hello_world.yaml | 6 ++-- esphome/components/lvgl/lvgl_esphome.cpp | 10 +++---- esphome/components/lvgl/lvgl_esphome.h | 8 +++--- esphome/components/lvgl/schemas.py | 12 ++++++-- esphome/components/lvgl/trigger.py | 2 +- esphome/components/lvgl/types.py | 3 +- esphome/components/lvgl/widgets/canvas.py | 4 ++- esphome/components/lvgl/widgets/img.py | 4 +-- esphome/components/lvgl/widgets/meter.py | 4 +-- esphome/components/lvgl/widgets/tileview.py | 8 +++--- tests/components/lvgl/lvgl-package.yaml | 31 ++++++++------------- 12 files changed, 47 insertions(+), 47 deletions(-) diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index f3ded6a518a..c4a3c8f2cb6 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -31,7 +31,7 @@ GRADIENT_SCHEMA = cv.ensure_list( cv.Required(CONF_DIRECTION): cv.one_of( "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True ), - cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, + cv.Optional(CONF_DITHER): LV_DITHER.one_of, cv.Required(CONF_STOPS): cv.All( [ cv.Schema( diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index 359e73cd525..4af179a5890 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -43,14 +43,14 @@ on_boot: lvgl.widget.refresh: hello_world_title_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 400; - checkbox: text: Checkbox id: hello_world_checkbox_ on_boot: lvgl.widget.refresh: hello_world_checkbox_ hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 240; + return lv_obj_get_width(lv_screen_active()) < 240; on_click: lvgl.label.update: id: hello_world_label_ @@ -94,7 +94,7 @@ outline_width: 0 border_width: 0 hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400; widgets: - label: text_font: montserrat_14 diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index d26bcdc714b..bf86a4e9ee0 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -172,18 +172,18 @@ void LvglComponent::add_page(LvPageType *page) { page->setup(this->pages_.size() - 1); } -void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; if (anim == LV_SCREEN_LOAD_ANIM_NONE) { - lv_scr_load(this->pages_[this->current_page_]->obj); + lv_screen_load(this->pages_[this->current_page_]->obj); } else { - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + lv_screen_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } } -void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_next_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; size_t start = this->current_page_; @@ -195,7 +195,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { this->show_page(this->current_page_, anim, time); } -void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { +void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; size_t start = this->current_page_; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 7baeeb233b5..8de82d50c01 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -163,7 +163,7 @@ class LvglComponent : public PollingComponent { static void render_end_cb(lv_event_t *event); static void render_start_cb(lv_event_t *event); void dump_config() override; - lv_disp_t *get_disp() { return this->disp_; } + lv_display_t *get_disp() { return this->disp_; } lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. @@ -189,9 +189,9 @@ class LvglComponent : public PollingComponent { lv_event_code_t event3); void add_page(LvPageType *page); - void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); - void show_next_page(lv_scr_load_anim_t anim, uint32_t time); - void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); + void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time); + void show_next_page(lv_screen_load_anim_t anim, uint32_t time); + void show_prev_page(lv_screen_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; } size_t get_current_page() const; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 4e2bfeae85c..bcbb193ce34 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -250,9 +250,17 @@ STYLE_REMAP = { } -def remap_property(prop): +def remap_property(prop, record=True): + """ + Remap an old style property to new style property. + Optionally record the use of the deprecated property. + :param prop: Name of the style property to remap. + :param record: Whether to record the use of the deprecated property. + :return: The remapped property name, or ``prop`` if no remapping exists. + """ if prop in STYLE_REMAP: - get_remapped_uses().add(prop) + if record: + get_remapped_uses().add(prop) return STYLE_REMAP[prop] return prop diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index c5ad4d402ef..077ff06bb75 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -72,7 +72,7 @@ async def generate_triggers(): dir = DIRECTIONS.mapper(dir) w.clear_flag("LV_OBJ_FLAG_SCROLLABLE") selected = literal( - f"lv_indev_get_gesture_dir(lv_indev_get_act()) == {dir}" + f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) await add_trigger( conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 03739f3ff16..8343a542a9f 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -59,7 +59,6 @@ lv_style_t = cg.global_ns.struct("lv_style_t") lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") -lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") lv_opa_t = cg.global_ns.struct("lv_opa_t") lv_group_t = cg.global_ns.struct("lv_group_t") @@ -67,7 +66,7 @@ LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) -lv_img_t = LvType("lv_img_t") +lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index c670e3732cd..0e40d0dfbef 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -369,7 +369,9 @@ def _scale_map(config): def _get_prop_validator(prop): - return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop) + return STYLE_PROPS.get( + f"transform_{remap_property(prop, False)}" + ) or STYLE_PROPS.get(prop) def _prop_validator(prop): diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index ed6fd30c097..8a046fea334 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -17,7 +17,7 @@ from ..defines import ( CONF_ZOOM, ) from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size -from ..types import lv_img_t +from ..types import lv_image_t from . import Widget, WidgetType from .label import CONF_LABEL @@ -55,7 +55,7 @@ class ImgType(WidgetType): def __init__(self): super().__init__( CONF_IMAGE, - lv_img_t, + lv_image_t, (CONF_MAIN,), IMG_SCHEMA, IMG_MODIFY_SCHEMA, diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index d32efd145bf..494f811a8eb 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -73,7 +73,7 @@ from ..types import ( LvType, ObjUpdateAction, lv_event_t, - lv_img_t, + lv_image_t, lv_obj_t, ) from . import Widget, WidgetType, get_widgets, widget_to_code @@ -205,7 +205,7 @@ INDICATOR_SCHEMA = cv.Schema( INDICATOR_IMG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t), - cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t), + cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t), } ), requires_component("image"), diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index dadaef7d07e..8e9d95f3495 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -29,7 +29,7 @@ lv_tile_t = LvType("lv_tileview_tile_t") lv_tileview_t = LvType( "lv_tileview_t", largs=[(lv_obj_t_ptr, "tile")], - lvalue=lambda w: w.get_property("tile_act"), + lvalue=lambda w: w.get_property("tile_active"), has_on_value=True, ) @@ -85,7 +85,7 @@ class TileviewType(WidgetType): await add_widgets(tile, tile_conf) if tiles: # Set the first tile as active - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF") ) @@ -122,11 +122,11 @@ async def tileview_select(config, action_id, template_arg, args): async def do_select(w: Widget): if tile := config.get(CONF_TILE_ID): tile = await cg.get_variable(tile) - lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + lv.tileview_set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) else: row = await lv_int.process(config[CONF_ROW]) column = await lv_int.process(config[CONF_COLUMN]) - lv_obj.set_tile_id( + lv.tileview_set_tile_by_index( widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) ) lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 606f57d6a11..b168578a98c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -43,9 +43,6 @@ lvgl: start_value: 0 end_value: 180 bg_color: light_blue - disp_bg_color: color_id - disp_bg_image: cat_image - disp_bg_opa: cover bottom_layer: widgets: - obj: @@ -58,7 +55,6 @@ lvgl: gradients: - id: color_bar direction: hor - # dither: err_diff stops: - color: 0xFF0000 position: 0 @@ -143,12 +139,11 @@ lvgl: body: text: This is a sample messagebox bg_color: 0x808080 - button_style: - bg_color: 0xff00 - border_width: 4 buttons: - id: msgbox_button text: Button + bg_color: 0x00ff00 + border_width: 4 - id: msgbox_apply text: "Close" on_click: @@ -160,8 +155,8 @@ lvgl: bg_opa: !lambda return 0.5; - lvgl.image.update: id: lv_image - zoom: !lambda return 512; - angle: !lambda return 100; + scale: !lambda return 512; + rotation: !lambda return 100; pivot_x: !lambda return 20; pivot_y: !lambda return 20; offset_x: !lambda return 20; @@ -287,8 +282,8 @@ lvgl: then: - lvgl.animimg.stop: anim_img - lvgl.update: - disp_bg_color: 0xffff00 - disp_bg_image: none + bottom_layer: + bg_color: 0xffff00 - lvgl.widget.show: message_box - label: text: "Hello shiny day" @@ -361,8 +356,6 @@ lvgl: pad_right: 10px pad_top: 10px shadow_color: light_blue - shadow_ofs_x: 5 - shadow_ofs_y: 5 shadow_opa: cover shadow_spread: 5 shadow_width: 10 @@ -373,12 +366,10 @@ lvgl: text_letter_space: 4 text_line_space: 4 text_opa: cover - transform_angle: 180 transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% transform_pivot_y: 50% - transform_zoom: 0.5 transform_scale: 2.0 transform_scale_x: 1.5 transform_scale_y: 0.8 @@ -470,11 +461,11 @@ lvgl: id: button_button width: 20% height: 10% - transform_angle: !lambda return(180*100); + transform_rotation: !lambda return(180*100); arc_width: !lambda return 4; border_width: !lambda return 6; - shadow_ofs_x: !lambda return 6; - shadow_ofs_y: !lambda return 6; + shadow_offset_x: !lambda return 6; + shadow_offset_y: !lambda return 6; shadow_spread: !lambda return 6; shadow_width: !lambda return 6; pressed: @@ -646,8 +637,8 @@ lvgl: border_opa: 80% shadow_color: black shadow_width: 10 - shadow_ofs_x: 5 - shadow_ofs_y: 5 + shadow_offset_x: 5 + shadow_offset_y: 5 shadow_spread: 4 shadow_opa: cover outline_color: red From 6c981e83db9186381742618e5c5bf17f9da689e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brandon=20der=20Bl=C3=A4tter?= <intcreator@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:52:50 -0700 Subject: [PATCH 0983/2056] [hub75] Add SCAN_1_8_32PX_FULL wiring option (#15130) --- esphome/components/hub75/display.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index ede5078c33b..0d1b87941de 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -128,6 +128,7 @@ SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_32PX_FULL": Hub75ScanWiring.SCAN_1_8_32PX_FULL, "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } From b66ff374a2be7a6ebf4761f8124546c9b14684f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:26:33 +0100 Subject: [PATCH 0984/2056] [esp32] Fix GPIO strapping pins and add USB-JTAG warnings (#15105) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32_c3.py | 8 ++++++++ esphome/components/esp32/gpio_esp32_c6.py | 10 +++++++++- esphome/components/esp32/gpio_esp32_h2.py | 6 +++--- esphome/components/esp32/gpio_esp32_p4.py | 4 ++-- esphome/components/esp32/gpio_esp32_s3.py | 22 +++++++++++++++------- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 93e0b970934..6eb002f3f0a 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -14,6 +14,8 @@ _ESP32C3_SPI_PSRAM_PINS = { 17: "SPIQ", } +_ESP32C3_USB_JTAG_PINS = {18, 19} + _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,12 @@ def esp32_c3_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index cfd3bca8334..993606d9dee 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -18,7 +18,9 @@ _ESP32C6_SPI_PSRAM_PINS = { 30: "SPID", } -_ESP32C6_STRAPPING_PINS = {8, 9, 15} +_ESP32C6_USB_JTAG_PINS = {12, 13} + +_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15} _LOGGER = logging.getLogger(__name__) @@ -30,6 +32,12 @@ def esp32_c6_validate_gpio_pin(value: int) -> int: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C6_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 5e7a6158f9b..9dd65376944 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -9,7 +9,7 @@ _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} -_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} +_ESP32H2_STRAPPING_PINS = {8, 9, 25} _LOGGER = logging.getLogger(__name__) @@ -26,8 +26,8 @@ def esp32_h2_validate_gpio_pin(value: int) -> int: ) if value in _ESP32H2_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 865db926524..6e9227c5015 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -20,8 +20,8 @@ def esp32_p4_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index cb0eb8178c3..f528de4ccde 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -5,7 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin -_ESP_32S3_SPI_PSRAM_PINS = { +_ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", 28: "SPIWP", @@ -15,7 +15,7 @@ _ESP_32S3_SPI_PSRAM_PINS = { 32: "SPID", } -_ESP_32_ESP32_S3R8_PSRAM_PINS = { +_ESP32S3R8_PSRAM_PINS = { 33: "SPIIO4", 34: "SPIIO5", 35: "SPIIO6", @@ -23,7 +23,9 @@ _ESP_32_ESP32_S3R8_PSRAM_PINS = { 37: "SPIDQS", } -_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} +_ESP32S3_USB_JTAG_PINS = {19, 20} + +_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) @@ -32,11 +34,11 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") - if value in _ESP_32S3_SPI_PSRAM_PINS: + if value in _ESP32S3_SPI_PSRAM_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + if value in _ESP32S3R8_PSRAM_PINS: _LOGGER.warning( "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", value, @@ -46,6 +48,12 @@ def esp32_s3_validate_gpio_pin(value: int) -> int: # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + if value in _ESP32S3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value @@ -61,5 +69,5 @@ def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: # All ESP32 pins support input mode pass - check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value From e0d8000007beb22f66fb093399379de8f592075c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:34:34 +1000 Subject: [PATCH 0985/2056] [ai] Add instructions regarding constructor parameters (#15091) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .ai/instructions.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.ai/instructions.md b/.ai/instructions.md index 240a47a52fd..a7e08f9c4d5 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro * **Indentation:** Use spaces (two per indentation level), not tabs * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` * **Line length:** Wrap lines at no more than 120 characters + * **Constructor parameters vs setters:** Component properties that are both **required** and **invariant** + (never change after construction) should be constructor parameters rather than set via setter methods. + This makes the dependency explicit and prevents use of the object in an incompletely-initialized state. + In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments. + ```cpp + // Good - required invariant dependency as constructor parameter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit SourceTextSensor(text::Text *source) : source_(source) {} + protected: + text::Text *source_; + }; + ``` + ```cpp + // Bad - required invariant dependency as setter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text::Text *source) { this->source_ = source; } + protected: + text::Text *source_{nullptr}; + }; + ``` * **Component Structure:** * **Standard Files:** From 5d67868ac6d72159b40f080f556bc8534a1831e9 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:39:46 +0100 Subject: [PATCH 0986/2056] [nextion] Fix inline doc parameter types for page and touch callbacks (#14972) --- esphome/components/nextion/nextion.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2842e57ce83..bb5998cf5dd 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1160,13 +1160,13 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** Add a callback to be notified when the nextion changes pages. * - * @param callback The void(std::string) callback. + * @param callback The void(uint8_t) callback. */ template<typename F> void add_new_page_callback(F &&callback) { this->page_callback_.add(std::forward<F>(callback)); } /** Add a callback to be notified when Nextion has a touch event. * - * @param callback The void() callback. + * @param callback The void(uint8_t, uint8_t, bool) callback. */ template<typename F> void add_touch_event_callback(F &&callback) { this->touch_callback_.add(std::forward<F>(callback)); From a15389318f41373a4c4466c69056cff9f6466e4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:57:33 -0400 Subject: [PATCH 0987/2056] [audio] Bump esp-audio-libs to 2.0.4 (#15164) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 9cc80b9b33a..acc3b5d351e 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -204,7 +204,7 @@ async def to_code(config): add_idf_component( name="esphome/esp-audio-libs", - ref="2.0.3", + ref="2.0.4", ) data = _get_data() diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 4148147a3b7..c44853969ed 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: bblanchon/arduinojson: version: "7.4.2" esphome/esp-audio-libs: - version: 2.0.3 + version: 2.0.4 esphome/micro-opus: version: 0.3.6 espressif/esp-dsp: From 010516aef2f1a155f569fe51a8437e6a48a2f876 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 25 Mar 2026 07:33:17 -1000 Subject: [PATCH 0988/2056] [benchmark] Add sensor publish_state benchmarks (#15034) --- .../sensor/bench_sensor_publish.cpp | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/benchmarks/components/sensor/bench_sensor_publish.cpp diff --git a/tests/benchmarks/components/sensor/bench_sensor_publish.cpp b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp new file mode 100644 index 00000000000..9639191a4de --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_publish.cpp @@ -0,0 +1,79 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/sensor/sensor.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +// Without this, the ~60ns per-iteration valgrind start/stop cost dominates +// sub-microsecond benchmarks. +static constexpr int kInnerIterations = 2000; + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestSensor : public sensor::Sensor { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// --- Sensor::publish_state() with no callbacks registered --- +// Measures baseline publish overhead: state assignment, logging, +// internal_send_state_to_frontend, ControllerRegistry notification. + +static void SensorPublish_NoCallbacks(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast<float>(i)); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_NoCallbacks); + +// --- Sensor::publish_state() with one state callback --- +// Measures callback dispatch overhead through LazyCallbackManager. + +static void SensorPublish_WithCallback(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + float callback_value = 0.0f; + sensor.add_on_state_callback([&callback_value](float value) { callback_value = value; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(static_cast<float>(i)); + } + benchmark::DoNotOptimize(callback_value); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_WithCallback); + +// --- Sensor::publish_state() with the same value every time --- +// Steady-state pattern: sensor reports an unchanged reading. +// Sensor doesn't dedup today, so this exercises the same code path +// as changing values, but tracks the common real-world pattern +// separately for regression detection. + +static void SensorPublish_SameValue(benchmark::State &state) { + TestSensor sensor; + sensor.configure("test_sensor"); + + // Warm up so has_state is already set + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(23.5f); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorPublish_SameValue); + +} // namespace esphome::benchmarks From a22d47c71924c2e6355d12cc014a134619e0e536 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 25 Mar 2026 07:36:53 -1000 Subject: [PATCH 0989/2056] [api] Add --no-states flag to esphome logs command (#15160) --- esphome/__main__.py | 11 +++++++- esphome/components/api/client.py | 18 +++++++++--- tests/unit_tests/test_main.py | 47 ++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 4b0fc2cec7f..87abd7f7965 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1046,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int ): from esphome.components.api.client import run_logs - return run_logs(config, network_devices) + return run_logs( + config, + network_devices, + subscribe_states=not getattr(args, "no_states", False), + ) if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging(): from esphome import mqtt @@ -1664,6 +1668,11 @@ def parse_args(argv): help="Reset the device before starting serial logs.", default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), ) + parser_logs.add_argument( + "--no-states", + action="store_true", + help="Do not show entity state changes in log output.", + ) parser_discover = subparsers.add_parser( "discover", diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0e71ad8fcbf..0c6c569c7d6 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -32,7 +32,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: +async def async_run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -89,14 +93,20 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: config, raw_line, backtrace_state=backtrace_state ) - stop = await async_run(cli, on_log, name=name) + stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states) try: await asyncio.Event().wait() finally: await stop() -def run_logs(config: dict[str, Any], addresses: list[str]) -> None: +def run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command.""" with contextlib.suppress(KeyboardInterrupt): - asyncio.run(async_run_logs(config, addresses)) + asyncio.run( + async_run_logs(config, addresses, subscribe_states=subscribe_states) + ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 5e36c06bb31..115ce38c938 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1762,7 +1762,34 @@ def test_show_logs_api( assert result == 0 mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.1.101"] + CORE.config, ["192.168.1.100", "192.168.1.101"], subscribe_states=True + ) + + +@patch("esphome.components.api.client.run_logs") +def test_show_logs_api_no_states( + mock_run_logs: Mock, +) -> None: + """Test show_logs with --no-states flag.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + args.no_states = True + devices = ["192.168.1.100"] + + result = show_logs(CORE.config, args, devices) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=False ) @@ -1788,7 +1815,9 @@ def test_show_logs_api_with_fqdn_mdns_disabled( assert result == 0 # Should use the FQDN directly, not try MQTT lookup - mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["device.example.com"], subscribe_states=True + ) @patch("esphome.components.api.client.run_logs") @@ -1816,7 +1845,9 @@ def test_show_logs_api_with_mqtt_fallback( assert result == 0 mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.200"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.200"], subscribe_states=True + ) @patch("esphome.mqtt.show_logs") @@ -2746,7 +2777,7 @@ def test_show_logs_api_static_ip_with_mqttip( # Verify run_logs was called with both IPs mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.1.100", "192.168.2.50"] + CORE.config, ["192.168.1.100", "192.168.2.50"], subscribe_states=True ) @@ -2782,7 +2813,9 @@ def test_show_logs_api_multiple_mqttip_resolves_once( # Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution # The _resolve_network_devices helper filters out both after first resolution mock_run_logs.assert_called_once_with( - CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"] + CORE.config, + ["192.168.2.50", "192.168.2.51", "192.168.1.100"], + subscribe_states=True, ) @@ -2862,7 +2895,9 @@ def test_show_logs_api_mqtt_timeout_fallback( mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client") # Verify run_logs was called with only the static IP (MQTT failed) - mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"]) + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=True + ) def test_detect_external_components_no_external( From 65d0a91fcc0ad85de3d7a1792ad95a0e8c8977ec Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:01:52 +0100 Subject: [PATCH 0990/2056] [nextion] Add defined keys to `defines.h` (#14971) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 8 ++--- esphome/core/defines.h | 7 ++++ tests/components/nextion/common.yaml | 47 ++++++++++++++++---------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 85da6af48aa..ac17e14312f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -50,10 +50,10 @@ bool Nextion::check_connect_() { return true; #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE - ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake - this->is_connected_ = true; // Set the connection status to true - return true; // Return true indicating the connection is set -#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake + this->connection_state_.is_connected_ = true; // Set the connection status to true + return true; // Return true indicating the connection is set +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE if (this->comok_sent_ == 0) { this->reset_(false); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b5612a1d3f4..8cf331c4d60 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -115,6 +115,13 @@ #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_MEDIA_SOURCE +#define USE_NEXTION_COMMAND_SPACING +#define USE_NEXTION_CONF_START_UP_PAGE +#define USE_NEXTION_CONFIG_DUMP_DEVICE_INFO +#define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START +#define USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE +#define USE_NEXTION_MAX_COMMANDS_PER_LOOP +#define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_OUTPUT diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 4373fe5462e..d9493db50c9 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -273,26 +273,39 @@ text_sensor: display: - platform: nextion id: main_lcd + auto_wake_on_touch: true + brightness: 80% + command_spacing: 5ms + dump_device_info: true + exit_reparse_on_start: true + lambda: |- + ESP_LOGD("display","Display is being tested!"); max_commands_per_loop: 20 + max_queue_age: 5000ms # Remove queue items after 5s max_queue_size: 50 - update_interval: 5s - on_sleep: - then: - lambda: 'ESP_LOGD("display","Display went to sleep");' - on_wake: - then: - lambda: 'ESP_LOGD("display","Display woke up");' - on_setup: - then: - lambda: 'ESP_LOGD("display","Display setup completed");' - on_page: - then: - lambda: 'ESP_LOGD("display","Display shows new page %u", x);' on_buffer_overflow: then: logger.log: "Nextion reported a buffer overflow!" - - command_spacing: 5ms - dump_device_info: true - max_queue_age: 5000ms # Remove queue items after 5s + on_page: + then: + lambda: 'ESP_LOGD("display","Display shows new page %u", x);' + on_setup: + then: + lambda: 'ESP_LOGD("display","Display setup completed");' + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_touch: + then: + lambda: |- + ESP_LOGD("display", + "Display was touched at page %u, component %u, touch event: %s", + page_id, component_id, touch_event ? "press" : "release"); + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' + update_interval: 5s + start_up_page: 1 startup_override_ms: 10000ms # Wait 10s for display ready + touch_sleep_timeout: 3 + wake_up_page: 2 From c42c6745b960b989e3373a7bedc663b6360d57f5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:06:48 -0400 Subject: [PATCH 0991/2056] [mcp9600] Fix setup success check using OR instead of AND (#15165) --- esphome/components/mcp9600/mcp9600.cpp | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index ff411bef7ac..0c5362b4bad 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -40,20 +40,20 @@ void MCP9600Component::setup() { } bool success = this->write_byte(MCP9600_REGISTER_STATUS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); - success |= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); - success |= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); - success |= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); + success &= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); + success &= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); + success &= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); + success &= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); if (!success) { this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; From 19615f2eaeeb37c5245a7de66abe0965b0e740f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:04 -0400 Subject: [PATCH 0992/2056] [bme68x_bsec2] Fix uninitialized bme68x_conf in measurement duration calculation (#15168) --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index ed2ec808962..cf516f6ca68 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -276,8 +276,8 @@ void BME68xBSEC2Component::run_() { } if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { - uint32_t meas_dur = 0; - meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); this->trigger_time_ns_ = curr_time_ns; this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); From f6c5767a8347ecccb446e4ff60627f18bd354d18 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:10:28 -0400 Subject: [PATCH 0993/2056] [inkplate] Use atomic GPIO write to prevent ISR race (#15166) --- esphome/components/inkplate/inkplate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 326bdff774e..3b4b1a63d57 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -229,7 +229,7 @@ void Inkplate::eink_off_() { this->oe_pin_->digital_write(false); this->gmod_pin_->digital_write(false); - GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin())); + GPIO.out_w1tc = this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin()); this->ckv_pin_->digital_write(false); this->sph_pin_->digital_write(false); this->spv_pin_->digital_write(false); From d8fbce365aff406360a8ea64e980f25ff510f503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:38:20 -1000 Subject: [PATCH 0994/2056] Bump requests from 2.32.5 to 2.33.0 (#15170) 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 9e75e6d0390..ce735f398ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.32.5 +requests==2.33.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From ec60da893f6613edceccf6003bc23a3001ed50fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 25 Mar 2026 09:45:06 -1000 Subject: [PATCH 0995/2056] [core] Move state logging to client-side formatting, console to VERBOSE (#15155) --- .../alarm_control_panel.cpp | 2 +- .../binary_sensor/binary_sensor.cpp | 2 +- esphome/components/climate/climate.cpp | 48 +++++++++---------- esphome/components/cover/cover.cpp | 26 +++++----- esphome/components/datetime/date_entity.cpp | 10 ++-- .../components/datetime/datetime_entity.cpp | 16 +++---- esphome/components/datetime/time_entity.cpp | 10 ++-- esphome/components/event/event.cpp | 2 +- esphome/components/fan/fan.cpp | 22 ++++----- esphome/components/light/light_call.cpp | 24 +++++----- esphome/components/lock/lock.cpp | 6 +-- .../components/media_player/media_player.cpp | 10 ++-- esphome/components/number/number.cpp | 2 +- esphome/components/number/number_call.cpp | 8 ++-- esphome/components/select/select.cpp | 2 +- esphome/components/select/select_call.cpp | 4 +- esphome/components/sensor/sensor.cpp | 2 +- esphome/components/switch/switch.cpp | 2 +- esphome/components/text/text.cpp | 4 +- esphome/components/text/text_call.cpp | 4 +- .../components/text_sensor/text_sensor.cpp | 2 +- esphome/components/update/update_entity.cpp | 14 +++--- esphome/components/valve/valve.cpp | 22 ++++----- .../components/water_heater/water_heater.cpp | 26 +++++----- 24 files changed, 135 insertions(+), 135 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index fb61776532f..623241851ae 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,7 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index c4d3a29a1e5..8ace7eafd14 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -45,7 +45,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) { #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); return true; } return false; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 3f44b986dc0..5cbe9a5dafb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -46,45 +46,45 @@ constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { void ClimateCall::perform() { this->parent_->control_callback_.call(*this); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { const LogString *mode_s = climate_mode_to_string(*this->mode_); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); + ESP_LOGV(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } if (this->swing_mode_.has_value()) { const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); - ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); + ESP_LOGV(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); } if (this->target_temperature_.has_value()) { - ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", *this->target_temperature_); } if (this->target_temperature_low_.has_value()) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); } if (this->target_temperature_high_.has_value()) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } if (this->target_humidity_.has_value()) { - ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + ESP_LOGV(TAG, " Target Humidity: %.0f", *this->target_humidity_); } this->parent_->control(*this); } @@ -435,43 +435,43 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); + ESP_LOGV(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { - ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); + ESP_LOGV(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { - ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); + ESP_LOGV(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature); } if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, this->target_temperature_high); } else { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { - ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + ESP_LOGV(TAG, " Current Humidity: %.0f%%", this->current_humidity); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { - ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + ESP_LOGV(TAG, " Target Humidity: %.0f%%", this->target_humidity); } // Send state to frontend diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index bb5965d861b..e98a555fe56 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -68,24 +68,24 @@ CoverCall &CoverCall::set_tilt(float tilt) { return *this; } void CoverCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -143,23 +143,23 @@ void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == COVER_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == COVER_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } if (traits.get_supports_tilt()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 3ba488c0aa9..997aec3f696 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGV(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); @@ -83,16 +83,16 @@ void DateCall::validate_() { void DateCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index fa50271f044..a8e00d6eb3e 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,7 +45,7 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, + ESP_LOGV(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) @@ -127,25 +127,25 @@ void DateTimeCall::validate_() { void DateTimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 74e43fbbe75..1cc9eaf2fb6 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,7 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); + ESP_LOGV(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); @@ -52,15 +52,15 @@ void TimeCall::validate_() { void TimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index ec63fd9c3eb..a5d64a2748d 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(StringRef(found)); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 97336e17b58..dc7a75018cb 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -44,22 +44,22 @@ FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) { } void FanCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); if (this->binary_state_.has_value()) { - ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_)); + ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_)); } if (this->oscillating_.has_value()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); } if (this->speed_.has_value()) { - ESP_LOGD(TAG, " Speed: %d", *this->speed_); + ESP_LOGV(TAG, " Speed: %d", *this->speed_); } if (this->direction_.has_value()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -196,21 +196,21 @@ void Fan::apply_preset_mode_(const FanCall &call) { void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { - ESP_LOGD(TAG, " Speed: %d", this->speed); + ESP_LOGV(TAG, " Speed: %d", this->speed); } if (traits.supports_oscillation()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating)); } if (traits.supports_direction()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->state_callback_.call(); #if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0b2d391fd6e..41bd98de7b6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -62,9 +62,9 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { } // Helper to log percentage values -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static void log_percent(const LogString *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); + ESP_LOGV(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else #define log_percent(param, value) @@ -76,20 +76,20 @@ void LightCall::perform() { const bool publish = this->get_publish_(); if (publish) { - ESP_LOGD(TAG, "'%s' Setting:", name); + ESP_LOGV(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode; if (target_color_mode != current_color_mode) { - ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); + ESP_LOGV(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); } // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); bool target_state = this->has_state() ? this->state_ : current_state; if (target_state != current_state) { - ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); + ESP_LOGV(TAG, " State: %s", ONOFF(v.is_on())); } if (this->has_brightness()) { @@ -100,7 +100,7 @@ void LightCall::perform() { log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { - ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + ESP_LOGV(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } @@ -108,11 +108,11 @@ void LightCall::perform() { log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { - ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + ESP_LOGV(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); } if (this->has_cold_white() || this->has_warm_white()) { - ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + ESP_LOGV(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, v.get_warm_white() * 100.0f); } } @@ -120,20 +120,20 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH if (publish) { - ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); + ESP_LOGV(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, this->flash_length_, publish); } else if (this->has_transition_()) { // TRANSITION if (publish) { - ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); + ESP_LOGV(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { if (publish) { - ESP_LOGD(TAG, " Effect: 'None'"); + ESP_LOGV(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } @@ -150,7 +150,7 @@ void LightCall::perform() { } if (publish) { - ESP_LOGD(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); + ESP_LOGV(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str()); } this->parent_->start_effect_(this->effect_); diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 4aa636e998c..90937485b9e 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -41,7 +41,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); @@ -49,10 +49,10 @@ void Lock::publish_state(LockState state) { } void LockCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->state_.has_value()) { - ESP_LOGD(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); + ESP_LOGV(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); } this->parent_->control(*this); } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 70086089ff2..a0eb7b55007 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -110,20 +110,20 @@ void MediaPlayerCall::validate_() { } void MediaPlayerCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->command_.has_value()) { const char *command_s = media_player_command_to_string(this->command_.value()); - ESP_LOGD(TAG, " Command: %s", command_s); + ESP_LOGV(TAG, " Command: %s", command_s); } if (this->media_url_.has_value()) { - ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str()); + ESP_LOGV(TAG, " Media URL: %s", this->media_url_.value().c_str()); } if (this->volume_.has_value()) { - ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value()); + ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value()); } if (this->announcement_.has_value()) { - ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); + ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); } this->parent_->control(*this); } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index fb5d6e9f28c..ca5aab64693 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -22,7 +22,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o void Number::publish_state(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); + ESP_LOGV(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); this->state_callback_.call(state); #if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index aac9b2a23d8..e300ca72de8 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -61,7 +61,7 @@ void NumberCall::perform() { float max_value = traits.get_max_value(); if (this->operation_ == NUMBER_OP_SET) { - ESP_LOGD(TAG, "'%s': Setting value", name); + ESP_LOGV(TAG, "'%s': Setting value", name); if (!this->value_.has_value() || std::isnan(*this->value_)) { this->log_perform_warning_(LOG_STR("No value")); return; @@ -80,7 +80,7 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; @@ -90,7 +90,7 @@ void NumberCall::perform() { if (target_value > max_value) target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); + ESP_LOGV(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; @@ -110,7 +110,7 @@ void NumberCall::perform() { return; } - ESP_LOGD(TAG, " New value: %f", target_value); + ESP_LOGV(TAG, " New value: %f", target_value); this->parent_->control(target_value); } diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index df90c657e2b..7c3dab15ad5 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -31,7 +31,7 @@ void Select::publish_state(size_t index) { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop - ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); + ESP_LOGV(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 83f5052fc8a..0e14371d004 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -64,7 +64,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { } if (this->operation_ == SELECT_OP_SET) { - ESP_LOGD(TAG, "'%s' - Setting", name); + ESP_LOGV(TAG, "'%s' - Setting", name); if (!this->index_.has_value()) { ESP_LOGW(TAG, "'%s' - No option set", name); return nullopt; @@ -73,7 +73,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) { } // SELECT_OP_NEXT or SELECT_OP_PREVIOUS - ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, + ESP_LOGV(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index aad7f86dcf2..59e011932b1 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -122,7 +122,7 @@ void Sensor::clear_filters() { void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, + ESP_LOGV(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, this->get_unit_of_measurement_ref().c_str()); this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index df762addbb1..11840db3a31 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -61,7 +61,7 @@ void Switch::publish_state(bool state) { if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); + ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); #if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_switch_update(this); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 12abc5d9390..032ea468e6f 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -19,9 +19,9 @@ void Text::publish_state(const char *state, size_t len) { this->state.assign(state, len); } if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); } else { - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); } this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/text/text_call.cpp b/esphome/components/text/text_call.cpp index b7aed098c77..b7692658af1 100644 --- a/esphome/components/text/text_call.cpp +++ b/esphome/components/text/text_call.cpp @@ -48,10 +48,10 @@ void TextCall::perform() { std::string target_value = this->value_.value(); if (this->parent_->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s' - Setting password value: " LOG_SECRET("'%s'"), this->parent_->get_name().c_str(), target_value.c_str()); } else { - ESP_LOGD(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); + ESP_LOGV(TAG, "'%s' - Setting text value: %s", this->parent_->get_name().c_str(), target_value.c_str()); } this->parent_->control(target_value); } diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0dc29f9a942..31543117b8f 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -112,7 +112,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) void TextSensor::notify_frontend_() { this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); + ESP_LOGV(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); this->callback_.call(this->state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 7edea2fe226..1a5a55577f9 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -18,28 +18,28 @@ const LogString *update_state_to_string(UpdateState state) { } void UpdateEntity::publish_state() { - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Current Version: %s", this->name_.c_str(), this->update_info_.current_version.c_str()); if (!this->update_info_.md5.empty()) { - ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); + ESP_LOGV(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); } if (!this->update_info_.firmware_url.empty()) { - ESP_LOGD(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); + ESP_LOGV(TAG, " Firmware URL: %s", this->update_info_.firmware_url.c_str()); } - ESP_LOGD(TAG, " Title: %s", this->update_info_.title.c_str()); + ESP_LOGV(TAG, " Title: %s", this->update_info_.title.c_str()); if (!this->update_info_.summary.empty()) { - ESP_LOGD(TAG, " Summary: %s", this->update_info_.summary.c_str()); + ESP_LOGV(TAG, " Summary: %s", this->update_info_.summary.c_str()); } if (!this->update_info_.release_url.empty()) { - ESP_LOGD(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); + ESP_LOGV(TAG, " Release URL: %s", this->update_info_.release_url.c_str()); } if (this->update_info_.has_progress) { - ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); + ESP_LOGV(TAG, " Progress: %.0f%%", this->update_info_.progress); } this->set_has_state(true); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 636da1f3c34..9e1ef9da508 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -68,21 +68,21 @@ ValveCall &ValveCall::set_position(float position) { return *this; } void ValveCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); } } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -128,20 +128,20 @@ ValveCall Valve::make_call() { return {this}; } void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == VALVE_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == VALVE_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 3989230d2d9..9a74877f0a6 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -83,25 +83,25 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) { } void WaterHeaterCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); } if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", this->target_temperature_); } if (!std::isnan(this->target_temperature_low_)) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); } if (!std::isnan(this->target_temperature_high_)) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); + ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } if (this->state_mask_ & WATER_HEATER_STATE_ON) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } @@ -158,24 +158,24 @@ void WaterHeaterCall::validate_() { void WaterHeater::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature_); } if (traits.get_supports_two_point_target_temperature()) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, this->target_temperature_high_); } else if (!std::isnan(this->target_temperature_)) { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature_); } if (this->state_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGD(TAG, " Away: YES"); + ESP_LOGV(TAG, " Away: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { - ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } #if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) From a075f63b59c68dd6e627c505397c1cdf5a052b45 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:50:37 -0400 Subject: [PATCH 0996/2056] [uart] Fix debug callback missing peeked byte and reading past end (#15169) --- esphome/components/uart/uart_component_esp_idf.cpp | 6 ++++-- esphome/components/uart/uart_component_host.cpp | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index bd2f915d3a8..cd77cd11897 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -324,6 +324,9 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { } bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return false; + } size_t length_to_read = len; int32_t read_len = 0; if (!this->check_read_timeout_(len)) @@ -331,11 +334,10 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; - data++; this->has_peek_ = false; } if (length_to_read > 0) - read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0042ffae23f..085610a9835 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -235,16 +235,14 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { } if (!this->check_read_timeout_(len)) return false; - uint8_t *data_ptr = data; size_t length_to_read = len; if (this->has_peek_) { length_to_read--; - *data_ptr = this->peek_byte_; - data_ptr++; + *data = this->peek_byte_; this->has_peek_ = false; } if (length_to_read > 0) { - int sz = ::read(this->file_descriptor_, data_ptr, length_to_read); + int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read); if (sz == -1) { this->update_error_(strerror(errno)); return false; From 29e263ad7d42fc90d54e809e41709f03d2e7f006 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 25 Mar 2026 13:43:01 -1000 Subject: [PATCH 0997/2056] [esp32] Wrap vfprintf to fix printf stub on picolibc (IDF 6) (#15172) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/esp32/printf_stubs.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0e216485ac2..91eb913e3df 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1587,7 +1587,7 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: cg.add_define("USE_FULL_PRINTF") else: - for symbol in ("vprintf", "printf", "fprintf"): + for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index c6f03bc363d..386fbbd79d1 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -2,10 +2,11 @@ * Linker wrap stubs for FILE*-based printf functions. * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), and vprintf() which pull in newlib's _vfprintf_r - * (~11 KB). This is a separate implementation from _svfprintf_r (used by - * snprintf/vsnprintf) that handles FILE* stream I/O with buffering and - * locking. + * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full + * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on + * picolibc's vfprintf). This is a separate implementation from the one + * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering + * and locking. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() @@ -70,11 +71,15 @@ int __wrap_printf(const char *fmt, ...) { return len; } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); - char buf[PRINTF_BUFFER_SIZE]; - int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + int len = __wrap_vfprintf(stream, fmt, ap); va_end(ap); return len; } From 676ac9d8b876044b0278f48fbd70100cf8594141 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 25 Mar 2026 21:30:46 -0500 Subject: [PATCH 0998/2056] [infrared][ir_rf_proxy] Add `receiver_frequency` config for IR receiver demodulation frequency (#15156) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- esphome/components/api/api_pb2_dump.cpp | 1 + esphome/components/const/__init__.py | 1 + esphome/components/infrared/infrared.h | 4 ++++ esphome/components/ir_rf_proxy/infrared.py | 15 ++++++++++++++- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 3 +++ tests/components/ir_rf_proxy/common-rx.yaml | 1 + 10 files changed, 30 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 86daa9a2bf6..96ee2fb9209 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2512,6 +2512,7 @@ message ListEntitiesInfraredResponse { EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags + uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) } // Command to transmit infrared/RF data using raw timings diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d023cd21a8d..0a99adcacfb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1549,6 +1549,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection auto *infrared = static_cast<infrared::Infrared *>(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); + msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz(); return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f77f4df5451..ae2cd2bae8d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3657,6 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, this->device_id); #endif buffer.encode_uint32(8, this->capabilities); + buffer.encode_uint32(9, this->receiver_frequency); } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; @@ -3672,6 +3673,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->device_id); #endif size += ProtoSize::calc_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->receiver_frequency); return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 16586e6e9a4..14f6c704ae4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3041,11 +3041,12 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 135; - static constexpr uint8_t ESTIMATED_SIZE = 44; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; + uint32_t receiver_frequency{0}; void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a11f3b231e2..640c3473719 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2572,6 +2572,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); + dump_field(out, ESPHOME_PSTR("receiver_frequency"), this->receiver_frequency); return out.c_str(); } #endif diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 1fbf88c2764..0eb37e3029e 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -18,6 +18,7 @@ CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_PARITY = "parity" +CONF_RECEIVER_FREQUENCY = "receiver_frequency" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_STOP_BITS = "stop_bits" diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 59535f499a0..6d91c97cce3 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -101,9 +101,13 @@ class InfraredTraits { bool get_supports_receiver() const { return this->supports_receiver_; } void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; } + uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; } + void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; } + protected: bool supports_transmitter_{false}; bool supports_receiver_{false}; + uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) }; /// Infrared - Base class for infrared remote control implementations diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 4a4d9fa8604..3218889721c 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -4,6 +4,7 @@ from typing import Any import esphome.codegen as cg from esphome.components import infrared, remote_receiver, remote_transmitter +from esphome.components.const import CONF_RECEIVER_FREQUENCY import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv @@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All( infrared.infrared_schema(IrRfProxy).extend( { cv.Optional(CONF_FREQUENCY, default=0): cv.frequency, + cv.Optional(CONF_RECEIVER_FREQUENCY): cv.frequency, cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id( remote_receiver.RemoteReceiverComponent ), @@ -33,7 +35,14 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config: dict[str, Any]) -> None: """Validate that transmitters have a proper carrier duty cycle.""" - # Only validate if this is an infrared (not RF) configuration with a transmitter + # receiver_frequency is only meaningful for receiver configurations + if CONF_RECEIVER_FREQUENCY in config and CONF_REMOTE_RECEIVER_ID not in config: + raise cv.Invalid( + f"'{CONF_RECEIVER_FREQUENCY}' can only be used with '{CONF_REMOTE_RECEIVER_ID}', " + "not with a transmitter" + ) + + # Only validate duty cycle if this is an infrared (not RF) configuration with a transmitter if config.get(CONF_FREQUENCY, 0) != 0 or CONF_REMOTE_TRANSMITTER_ID not in config: return @@ -75,3 +84,7 @@ async def to_code(config: dict[str, Any]) -> None: if CONF_REMOTE_RECEIVER_ID in config: receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) cg.add(var.set_receiver(receiver)) + + # Set receiver demodulation frequency if specified (metadata only, no hardware effect) + if CONF_RECEIVER_FREQUENCY in config: + cg.add(var.set_receiver_frequency(config[CONF_RECEIVER_FREQUENCY])) diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index f067a6e17a3..05b988f2877 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -22,6 +22,9 @@ class IrRfProxy : public infrared::Infrared { /// Check if this is RF mode (non-zero frequency) bool is_rf() const { return this->frequency_khz_ > 0; } + /// Set the receiver's hardware demodulation frequency in Hz (metadata only, does not affect hardware) + void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); } + protected: // RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF uint32_t frequency_khz_{0}; diff --git a/tests/components/ir_rf_proxy/common-rx.yaml b/tests/components/ir_rf_proxy/common-rx.yaml index 0f758f832d3..37033a128e1 100644 --- a/tests/components/ir_rf_proxy/common-rx.yaml +++ b/tests/components/ir_rf_proxy/common-rx.yaml @@ -8,6 +8,7 @@ infrared: - platform: ir_rf_proxy id: ir_rx name: "IR Receiver" + receiver_frequency: 38kHz remote_receiver_id: ir_receiver # RF 900MHz receiver From 8a6b009173a8373df76b3a4d07040c8852162b8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 25 Mar 2026 16:53:33 -1000 Subject: [PATCH 0999/2056] [light] Move normal state logging to VERBOSE (#15177) --- esphome/components/light/light_call.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 41bd98de7b6..7c936b51b76 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -385,7 +385,7 @@ void LightCall::transform_parameters_() { !(this->color_mode_ & ColorCapability::WHITE) && // !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // min_mireds > 0.0f && max_mireds > 0.0f) { - ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", + ESP_LOGV(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); // Only compute cold_white/warm_white from color_temperature if they're not already explicitly set. // This is important for state restoration, where both color_temperature and cold_white/warm_white @@ -432,7 +432,7 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is in the intersection (suitable AND supported) if (ColorModeMask::mask_contains(intersection, current_mode)) { - ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -440,7 +440,7 @@ ColorMode LightCall::compute_color_mode_() { // Use the preferred suitable mode. if (intersection != 0) { ColorMode mode = ColorModeMask::first_value_from_mask(intersection); - ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), + ESP_LOGV(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } From 92604017471dd0dbcfbb90f6991f62160545b2e8 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:11:46 -0400 Subject: [PATCH 1000/2056] [bmp581] Add SPI support for BMP581 (#13124) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/bmp581_base/bmp581_base.cpp | 11 ++- esphome/components/bmp581_base/bmp581_base.h | 3 + esphome/components/bmp581_spi/__init__.py | 0 esphome/components/bmp581_spi/bmp581_spi.cpp | 73 +++++++++++++++++++ esphome/components/bmp581_spi/bmp581_spi.h | 24 ++++++ esphome/components/bmp581_spi/sensor.py | 48 ++++++++++++ tests/components/bmp581_spi/common.yaml | 9 +++ .../components/bmp581_spi/test.esp32-idf.yaml | 7 ++ .../bmp581_spi/test.esp8266-ard.yaml | 7 ++ .../bmp581_spi/test.rp2040-ard.yaml | 7 ++ 11 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 esphome/components/bmp581_spi/__init__.py create mode 100644 esphome/components/bmp581_spi/bmp581_spi.cpp create mode 100644 esphome/components/bmp581_spi/bmp581_spi.h create mode 100644 esphome/components/bmp581_spi/sensor.py create mode 100644 tests/components/bmp581_spi/common.yaml create mode 100644 tests/components/bmp581_spi/test.esp32-idf.yaml create mode 100644 tests/components/bmp581_spi/test.esp8266-ard.yaml create mode 100644 tests/components/bmp581_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index afe4cdb8712..8d297d7b072 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp581_base/* @danielkent-net @kahrendt esphome/components/bmp581_i2c/* @danielkent-net @kahrendt +esphome/components/bmp581_spi/* @danielkent-net @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/bthome_mithermometer/* @nagyrobi diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 89a92de31d2..c9d250545bc 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -469,14 +469,18 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } bool BMP581Component::reset_() { + // - activates interface (only relevant for SPI mode) // - writes reset command to the command register // - waits for sensor to complete reset + // - activates interface (only relevant for SPI mode) // - returns the Power-On-Reboot interrupt status, which is asserted if successful + // activates communication interface (SPI only) + this->activate_interface(); + // writes reset command to BMP's command register if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) { ESP_LOGE(TAG, "Failed to write reset command"); - return false; } @@ -484,6 +488,9 @@ bool BMP581Component::reset_() { // - round up to 3 ms delay(3); + // reactivates communication interface after reset (SPI only) + this->activate_interface(); + // read interrupt status register if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { ESP_LOGE(TAG, "Failed to read interrupt status register"); @@ -491,7 +498,7 @@ bool BMP581Component::reset_() { return false; } - // Power-On-Reboot bit is asserted if sensor successfully reset + // power-On-Reboot bit is asserted if sensor successfully reset return this->int_status_.bit.por; } diff --git a/esphome/components/bmp581_base/bmp581_base.h b/esphome/components/bmp581_base/bmp581_base.h index d99c420272a..c3920512e0d 100644 --- a/esphome/components/bmp581_base/bmp581_base.h +++ b/esphome/components/bmp581_base/bmp581_base.h @@ -87,6 +87,9 @@ class BMP581Component : public PollingComponent { virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + // Interface activation function. Only used for SPI interface; no-op for I2C. + virtual void activate_interface() {} + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; diff --git a/esphome/components/bmp581_spi/__init__.py b/esphome/components/bmp581_spi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/bmp581_spi/bmp581_spi.cpp b/esphome/components/bmp581_spi/bmp581_spi.cpp new file mode 100644 index 00000000000..01435880f08 --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.cpp @@ -0,0 +1,73 @@ +#include <cstdint> +#include <cstddef> + +#include "bmp581_spi.h" +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +static const char *const TAG = "bmp581_spi"; + +// OR (|) register with BMP_SPI_READ for read +inline constexpr uint8_t BMP_SPI_READ = 0x80; + +// AND (&) register with BMP_SPI_WRITE for write +inline constexpr uint8_t BMP_SPI_WRITE = 0x7F; + +void BMP581SPIComponent::dump_config() { + BMP581Component::dump_config(); + LOG_SPI_DEVICE(this); +} + +void BMP581SPIComponent::setup() { + this->spi_setup(); + BMP581Component::setup(); +} + +void BMP581SPIComponent::activate_interface() { + // - forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->bmp_read_byte(bmp581_base::BMP581_CHIP_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions BMP_SPI_READ (| with register) and BMP_SPI_WRITE (& with register) +// are defined for readability. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp581-ds004.pdf + +bool BMP581SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/bmp581_spi.h b/esphome/components/bmp581_spi/bmp581_spi.h new file mode 100644 index 00000000000..57f75588d5e --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +// BMP581 is technically compatible with SPI Mode0 and Mode3. Default to Mode3. +class BMP581SPIComponent : public esphome::bmp581_base::BMP581Component, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, + spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> { + public: + void setup() override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void activate_interface() override; +}; + +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/sensor.py b/esphome/components/bmp581_spi/sensor.py new file mode 100644 index 00000000000..75f60b24604 --- /dev/null +++ b/esphome/components/bmp581_spi/sensor.py @@ -0,0 +1,48 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["bmp581_base"] +CODEOWNERS = ["@kahrendt", "@danielkent-net"] +DEPENDENCIES = ["spi"] + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = { + 0: "MODE0", + "0": "MODE0", + "MODE0": "MODE0", + 3: "MODE3", + "3": "MODE3", + "MODE3": "MODE3", +} + +bmp581_ns = cg.esphome_ns.namespace("bmp581_spi") +BMP581SPIComponent = bmp581_ns.class_( + "BMP581SPIComponent", cg.PollingComponent, spi.SPIDevice +) + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("BMP581 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(BMP581SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/tests/components/bmp581_spi/common.yaml b/tests/components/bmp581_spi/common.yaml new file mode 100644 index 00000000000..f22074f867d --- /dev/null +++ b/tests/components/bmp581_spi/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: bmp581_spi + cs_pin: ${cs_pin} + temperature: + name: BMP581 Temperature + iir_filter: 2x + pressure: + name: BMP581 Pressure + oversampling: 128x diff --git a/tests/components/bmp581_spi/test.esp32-idf.yaml b/tests/components/bmp581_spi/test.esp32-idf.yaml new file mode 100644 index 00000000000..a3352cf880d --- /dev/null +++ b/tests/components/bmp581_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bmp581_spi/test.esp8266-ard.yaml b/tests/components/bmp581_spi/test.esp8266-ard.yaml new file mode 100644 index 00000000000..595f31046ab --- /dev/null +++ b/tests/components/bmp581_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bmp581_spi/test.rp2040-ard.yaml b/tests/components/bmp581_spi/test.rp2040-ard.yaml new file mode 100644 index 00000000000..79ea6ce90b0 --- /dev/null +++ b/tests/components/bmp581_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + +<<: !include common.yaml From f3a31be6d0f4d896db0356b1cabea72741a73921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 07:32:39 -1000 Subject: [PATCH 1001/2056] [benchmark] Add climate publish_state and call benchmarks (#15180) --- .../benchmarks/components/climate/__init__.py | 5 + .../components/climate/bench_climate.cpp | 142 ++++++++++++++++++ .../components/climate/benchmark.yaml | 1 + 3 files changed, 148 insertions(+) create mode 100644 tests/benchmarks/components/climate/__init__.py create mode 100644 tests/benchmarks/components/climate/bench_climate.cpp create mode 100644 tests/benchmarks/components/climate/benchmark.yaml diff --git a/tests/benchmarks/components/climate/__init__.py b/tests/benchmarks/components/climate/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/climate/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/climate/bench_climate.cpp b/tests/benchmarks/components/climate/bench_climate.cpp new file mode 100644 index 00000000000..316a72b2b64 --- /dev/null +++ b/tests/benchmarks/components/climate/bench_climate.cpp @@ -0,0 +1,142 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/climate/climate.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Climate for benchmarking — control() is a no-op. +class BenchClimate : public climate::Climate { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + climate::ClimateTraits traits() override { return this->traits_; } + + climate::ClimateTraits traits_; + + protected: + void control(const climate::ClimateCall & /*call*/) override {} +}; + +// Helper to create a typical HVAC climate device for benchmarks. +// Note: setup() is not called (no preferences backend), so save_state_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_hvac_climate(BenchClimate &climate) { + climate.configure("test_climate"); + climate.traits_.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT_COOL, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, + }); + climate.traits_.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + }); + climate.traits_.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, + }); + climate.traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + }); + climate.traits_.set_visual_min_temperature(16.0f); + climate.traits_.set_visual_max_temperature(30.0f); + climate.traits_.set_visual_target_temperature_step(0.5f); + climate.traits_.set_visual_current_temperature_step(0.1f); + climate.traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); +} + +// --- Climate::publish_state() with temperature update --- +// Measures the publish path for a thermostat reporting state — +// the hot path during HVAC operation. + +static void ClimatePublish_State(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + climate.action = climate::CLIMATE_ACTION_HEATING; + climate.target_temperature = 22.0f; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + climate.current_temperature = 20.0f + static_cast<float>(i % 100) / 10.0f; + climate.publish_state(); + } + benchmark::DoNotOptimize(climate.current_temperature); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimatePublish_State); + +// --- Climate::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void ClimatePublish_WithCallback(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + climate.target_temperature = 22.0f; + + uint64_t callback_count = 0; + climate.add_on_state_callback([&callback_count](climate::Climate & /*c*/) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + climate.current_temperature = 20.0f + static_cast<float>(i % 100) / 10.0f; + climate.publish_state(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimatePublish_WithCallback); + +// --- ClimateCall::perform() set target temperature --- +// The most common climate call — adjusting the thermostat setpoint. + +static void ClimateCall_SetTemperature(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + climate.mode = climate::CLIMATE_MODE_HEAT; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float temp = 18.0f + static_cast<float>(i % 25) * 0.5f; + climate.make_call().set_target_temperature(temp).perform(); + } + benchmark::DoNotOptimize(climate.target_temperature); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimateCall_SetTemperature); + +// --- ClimateCall::perform() mode change with fan --- +// Exercises the validation path with multiple fields set. + +static void ClimateCall_ModeChange(benchmark::State &state) { + BenchClimate climate; + setup_hvac_climate(climate); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + auto mode = (i % 2 == 0) ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_COOL; + auto fan = (i % 2 == 0) ? climate::CLIMATE_FAN_HIGH : climate::CLIMATE_FAN_LOW; + climate.make_call().set_mode(mode).set_fan_mode(fan).set_target_temperature(22.0f).perform(); + } + benchmark::DoNotOptimize(climate.mode); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ClimateCall_ModeChange); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/climate/benchmark.yaml b/tests/benchmarks/components/climate/benchmark.yaml new file mode 100644 index 00000000000..8e79ed0ae7b --- /dev/null +++ b/tests/benchmarks/components/climate/benchmark.yaml @@ -0,0 +1 @@ +climate: From 689828436107c797a0525dbf5f3b86f96189ec47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 07:32:54 -1000 Subject: [PATCH 1002/2056] [benchmark] Add cover publish_state and call benchmarks (#15179) --- tests/benchmarks/components/cover/__init__.py | 5 + .../components/cover/bench_cover_publish.cpp | 107 ++++++++++++++++++ .../components/cover/benchmark.yaml | 1 + 3 files changed, 113 insertions(+) create mode 100644 tests/benchmarks/components/cover/__init__.py create mode 100644 tests/benchmarks/components/cover/bench_cover_publish.cpp create mode 100644 tests/benchmarks/components/cover/benchmark.yaml diff --git a/tests/benchmarks/components/cover/__init__.py b/tests/benchmarks/components/cover/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/cover/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/cover/bench_cover_publish.cpp b/tests/benchmarks/components/cover/bench_cover_publish.cpp new file mode 100644 index 00000000000..794d967edb8 --- /dev/null +++ b/tests/benchmarks/components/cover/bench_cover_publish.cpp @@ -0,0 +1,107 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/cover/cover.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Cover for benchmarking — control() is a no-op. +class BenchCover : public cover::Cover { + public: + cover::CoverTraits get_traits() override { return this->traits_; } + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + cover::CoverTraits traits_; + + protected: + void control(const cover::CoverCall & /*call*/) override {} +}; + +// --- Cover::publish_state() with position updates --- +// Measures the publish path for a garage door reporting position +// during open/close — the hot path during movement. + +static void CoverPublish_Position(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + cover.traits_.set_supports_tilt(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + cover.position = static_cast<float>(i % 101) / 100.0f; + cover.current_operation = (i % 2 == 0) ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; + cover.publish_state(false); + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverPublish_Position); + +// --- Cover::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void CoverPublish_WithCallback(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + uint64_t callback_count = 0; + cover.add_on_state_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + cover.position = static_cast<float>(i % 101) / 100.0f; + cover.publish_state(false); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverPublish_WithCallback); + +// --- CoverCall::perform() open/close cycle --- +// Measures the full call path: validation + control delegation. + +static void CoverCall_OpenClose(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + if (i % 2 == 0) { + cover.make_call().set_command_open().perform(); + } else { + cover.make_call().set_command_close().perform(); + } + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverCall_OpenClose); + +// --- CoverCall::perform() set position --- +// Measures the position-setting call path. + +static void CoverCall_SetPosition(benchmark::State &state) { + BenchCover cover; + cover.configure("test_cover"); + cover.traits_.set_supports_position(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float pos = static_cast<float>(i % 101) / 100.0f; + cover.make_call().set_position(pos).perform(); + } + benchmark::DoNotOptimize(cover.position); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CoverCall_SetPosition); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/cover/benchmark.yaml b/tests/benchmarks/components/cover/benchmark.yaml new file mode 100644 index 00000000000..477724be5ae --- /dev/null +++ b/tests/benchmarks/components/cover/benchmark.yaml @@ -0,0 +1 @@ +cover: From 02e23eb386bd3fde73c34a41cebdf2b2a08b41af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 07:33:10 -1000 Subject: [PATCH 1003/2056] [benchmark] Add light call and publish benchmarks (#15176) --- tests/benchmarks/components/light/__init__.py | 28 ++ .../components/light/bench_light_call.cpp | 253 ++++++++++++++++++ .../components/light/benchmark.yaml | 1 + 3 files changed, 282 insertions(+) create mode 100644 tests/benchmarks/components/light/__init__.py create mode 100644 tests/benchmarks/components/light/bench_light_call.cpp create mode 100644 tests/benchmarks/components/light/benchmark.yaml diff --git a/tests/benchmarks/components/light/__init__.py b/tests/benchmarks/components/light/__init__.py new file mode 100644 index 00000000000..233a3c246e5 --- /dev/null +++ b/tests/benchmarks/components/light/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components.light import generate_gamma_table +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Light benchmarks need USE_LIGHT_GAMMA_LUT defined and a gamma table + # with external linkage that the benchmark .cpp can reference. + manifest.enable_codegen() + original_to_code = manifest.to_code + + async def to_code(config): + await original_to_code(config) + cg.add_define("USE_LIGHT_GAMMA_LUT") + # Use the light component's own generate_gamma_table() so the + # benchmark stays in sync with any formula changes. + forward = generate_gamma_table(2.8) + values = ", ".join(f"0x{int(v):04X}" for v in forward) + # Use extern-visible (non-static) array so the benchmark .cpp + # can reference it via extern declaration. + cg.add_global( + cg.RawStatement( + f"extern const uint16_t bench_gamma_2_8_fwd[256] PROGMEM = {{{values}}};" + ) + ) + + to_code.priority = original_to_code.priority + manifest.to_code = to_code diff --git a/tests/benchmarks/components/light/bench_light_call.cpp b/tests/benchmarks/components/light/bench_light_call.cpp new file mode 100644 index 00000000000..c1ef0c425e0 --- /dev/null +++ b/tests/benchmarks/components/light/bench_light_call.cpp @@ -0,0 +1,253 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/light/light_output.h" +#include "esphome/components/light/light_state.h" + +// Gamma 2.8 forward LUT generated by the light component's Python codegen +// (see tests/benchmarks/components/light/__init__.py which calls generate_gamma_table()) +extern const uint16_t bench_gamma_2_8_fwd[256]; + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal LightOutput for benchmarking — no real hardware interaction. +class BenchLightOutput : public light::LightOutput { + public: + light::LightTraits get_traits() override { return this->traits_; } + void write_state(light::LightState * /*state*/) override {} + + light::LightTraits traits_; +}; + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestLightState : public light::LightState { + public: + using LightState::LightState; + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// Helper to create a configured RGBWW light state for benchmarks. +// Note: setup() is not called (no preferences backend), so save_remote_values_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_rgbww_light(BenchLightOutput &output, TestLightState &light) { + output.traits_.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); + output.traits_.set_min_mireds(153.0f); + output.traits_.set_max_mireds(500.0f); + light.configure("test_light"); + light.set_default_transition_length(0); + light.set_gamma_correct(2.8f); + light.set_gamma_table(bench_gamma_2_8_fwd); + light.set_restore_mode(light::LIGHT_ALWAYS_OFF); +} + +// --- LightCall::perform() with instant RGB color change (Home Assistant API path) --- +// Measures the full call path: validation, set_immediately_, publish, and save. +// HA sends color_mode explicitly since API 1.6. + +static void LightCall_RGBInstant(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + // Turn on first so subsequent calls are color changes + light.make_call().set_state(true).set_brightness(1.0f).set_color_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float v = static_cast<float>(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_red(v) + .set_green(1.0f - v) + .set_blue(v * 0.5f) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_RGBInstant); + +// --- LightCall::perform() turn on/off cycle (Home Assistant API path) --- +// HA sends color_mode explicitly since API 1.6, skipping compute_color_mode_(). + +static void LightCall_ToggleOnOff(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.make_call() + .set_state(i % 2 == 0) + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ToggleOnOff); + +// --- LightCall::perform() turn on/off via MQTT --- +// MQTT never sends color_mode, so compute_color_mode_() runs every call. + +static void LightCall_ToggleOnOff_MQTT(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.make_call().set_state(i % 2 == 0).set_transition_length(0).perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ToggleOnOff_MQTT); + +// --- LightCall::perform() with color temperature via MQTT --- +// Exercises the transform_parameters_() path that converts color_temperature +// to cold/warm white fractions. MQTT never sends color_mode, so this also +// hits compute_color_mode_() every call. Modern HA avoids this path entirely +// by converting color temp to CW/WW client-side. + +static void LightCall_ColorTemperature_MQTT(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Sweep through color temperature range + float ct = 153.0f + static_cast<float>(i % 348); + light.make_call().set_color_temperature(ct).set_transition_length(0).perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ColorTemperature_MQTT); + +// --- LightCall::perform() with 1s transition (Home Assistant API path) --- +// Exercises start_transition_() which allocates a LightTransformer. +// This is the default HA path when transition_length > 0. + +static void LightCall_Transition(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float v = static_cast<float>(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_red(v) + .set_green(1.0f - v) + .set_blue(v * 0.5f) + .set_transition_length(1000) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_Transition); + +// --- LightCall::perform() with cold/warm white (Home Assistant API path) --- +// Mirrors what modern HA sends: explicit color_mode with direct cold_white +// and warm_white values. HA converts color temp to CW/WW client-side for +// CWWW lights (API >= 1.6), so this is the primary HA path. + +static void LightCall_ColdWarmWhite(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call().set_state(true).set_brightness(1.0f).set_transition_length(0).perform(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float frac = static_cast<float>(i % 256) / 255.0f; + light.make_call() + .set_color_mode(light::ColorMode::RGB_COLD_WARM_WHITE) + .set_cold_white(1.0f - frac) + .set_warm_white(frac) + .set_transition_length(0) + .perform(); + } + benchmark::DoNotOptimize(light.remote_values); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightCall_ColdWarmWhite); + +// --- LightState::publish_state() with a remote values listener --- +// Measures listener notification overhead. + +static void LightPublish_WithListener(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + struct TestListener : public light::LightRemoteValuesListener { + void on_light_remote_values_update() override { count_++; } + uint64_t count_{0}; + } listener; + light.add_remote_values_listener(&listener); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.publish_state(); + } + benchmark::DoNotOptimize(listener.count_); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightPublish_WithListener); + +// --- current_values_as_rgbww output conversion with gamma LUT --- +// Measures the output conversion path that real light drivers call +// from write_state() to get hardware PWM values, including gamma +// table lookups via the LUT generated by Python codegen. + +static void LightOutput_RGBWW(benchmark::State &state) { + BenchLightOutput output; + TestLightState light(&output); + setup_rgbww_light(output, light); + + light.make_call() + .set_state(true) + .set_brightness(0.8f) + .set_color_brightness(0.6f) + .set_red(1.0f) + .set_green(0.5f) + .set_blue(0.2f) + .set_cold_white(0.7f) + .set_warm_white(0.3f) + .set_transition_length(0) + .perform(); + + float r, g, b, cw, ww; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + light.current_values_as_rgbww(&r, &g, &b, &cw, &ww); + } + benchmark::DoNotOptimize(r); + benchmark::DoNotOptimize(cw); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(LightOutput_RGBWW); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/light/benchmark.yaml b/tests/benchmarks/components/light/benchmark.yaml new file mode 100644 index 00000000000..2b7c9385819 --- /dev/null +++ b/tests/benchmarks/components/light/benchmark.yaml @@ -0,0 +1 @@ +light: From c2456409bd4bde9b793a5c6c98bebbc18f62511d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:39:19 -0400 Subject: [PATCH 1004/2056] [core] Improve clean-all with no arguments (#15184) --- esphome/writer.py | 10 ++++++++ tests/unit_tests/test_writer.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/esphome/writer.py b/esphome/writer.py index 4aac16ffd4d..06a22301184 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -476,6 +476,16 @@ def clean_all(configuration: list[str]): data_dirs.append(Path(env_data_dir)) if env_build_path := os.environ.get("ESPHOME_BUILD_PATH"): data_dirs.append(Path(env_build_path)) + if not data_dirs: + # No config files or known data dirs, check current directory + cwd_esphome = Path.cwd() / ".esphome" + if cwd_esphome.is_dir(): + data_dirs.append(cwd_esphome) + else: + _LOGGER.warning( + "No configuration files specified and no .esphome directory found in current directory. " + "Pass YAML files or a configuration directory to clean build artifacts." + ) # Clean build dir for dir in data_dirs: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 6ace38a7d7d..940a394c080 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -990,6 +990,47 @@ def test_clean_all_ignores_empty_env_vars( assert marker.exists() +@patch("esphome.writer.CORE") +def test_clean_all_no_args_with_esphome_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with no args cleans .esphome in cwd.""" + esphome_dir = tmp_path / ".esphome" + esphome_dir.mkdir() + (esphome_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with ( + caplog.at_level("INFO"), + patch("esphome.writer.Path.cwd", return_value=tmp_path), + ): + clean_all([]) + + assert esphome_dir.exists() + assert not (esphome_dir / "dummy.txt").exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_no_args_no_esphome_dir( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with no args and no .esphome dir warns.""" + from esphome.writer import clean_all + + with ( + caplog.at_level("WARNING"), + patch("esphome.writer.Path.cwd", return_value=tmp_path), + ): + clean_all([]) + + assert "No configuration files specified" in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From bf89a191f06a4fea3c3b9014f1d46200c89fa2df Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:39:35 -0400 Subject: [PATCH 1005/2056] [wifi] Guard coex_background_scan with CONFIG_SOC_WIFI_SUPPORTED (#15187) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1b80adc82eb..d8b3db96679 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -989,9 +989,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } // When scanning while connected (roaming), return to home channel between // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) +#ifdef CONFIG_SOC_WIFI_SUPPORTED if (this->roaming_state_ == RoamingState::SCANNING) { config.coex_background_scan = true; } +#endif esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From d9ada4536cbcaacdba36ea43806753841e06d515 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:58:12 +0100 Subject: [PATCH 1006/2056] [nextion] Fix leading space in pressed color string commands (#15190) --- esphome/components/nextion/nextion_commands.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 2adf314a2e3..4ddbfbee6a9 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -106,7 +106,7 @@ void Nextion::set_component_pressed_foreground_color(const char *component, uint } void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", " %s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { @@ -134,7 +134,7 @@ void Nextion::set_component_pressed_font_color(const char *component, uint16_t c } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", " %s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_font_color(const char *component, Color color) { From 1edf952ddacb7a0ad4d7ea1d106bd340642e6c99 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 27 Mar 2026 04:59:06 +1000 Subject: [PATCH 1007/2056] [font] Add unit tests verifying correct processing of glyphs (#15178) --- tests/component_tests/font/.gitattributes | 2 + .../component_tests/font/NotoSans-Regular.ttf | Bin 0 -> 455188 bytes tests/component_tests/font/__init__.py | 0 tests/component_tests/font/test_font.py | 337 ++++++++++++++++++ tests/components/font/.gitattributes | 3 +- 5 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/font/.gitattributes create mode 100644 tests/component_tests/font/NotoSans-Regular.ttf create mode 100644 tests/component_tests/font/__init__.py create mode 100644 tests/component_tests/font/test_font.py diff --git a/tests/component_tests/font/.gitattributes b/tests/component_tests/font/.gitattributes new file mode 100644 index 00000000000..4df6726184f --- /dev/null +++ b/tests/component_tests/font/.gitattributes @@ -0,0 +1,2 @@ +*.pcf -text +*.ttf -text diff --git a/tests/component_tests/font/NotoSans-Regular.ttf b/tests/component_tests/font/NotoSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a1b8994edeacd70067de843a4691b15a0ce5921b GIT binary patch literal 455188 zcmZQzWME(rVq{=oVNh^)adrD}{qA!H21XqQ2G#@a0sg`BX718pU|@U0!0<}KJvh|K z#r#zk1H(ra1_rSP|6qNi9D%k_21fQ41_p+NkPtV=9f1;u8Q4CEFfcIwPtHv&I5hv? z69z{1CkzZ+ddX!a3U#vtO&Qo87%(s}mZTM==UQp>)iAI<T*AP>o130kT)@D<Ai%)( zK!bsSfg?SqGOg(74+929ArS_an?4z-i75h88ILirzD{6ZU@*(bNKNF~ETX}{`g#Tf z1A|INZb=1$u5lOx>+3xX3`TQu@{<$gjK5D|V3UhrU@+Q~n^;l6KS6+*fh|ykfq_9G zFEKZD+Vg!Y8Q20%7#P@M3i69fHhyJa#lRLmfq|i8YC%zIf!VCi1O~SEAoYJ47#Wxt z_!t<NqZk;N*Dx?K^f0I}*fKCOxG=;rFfueT%wS+-n8mPzfstVsqbCC+V<=-N10!QN z69WSylQ@$&10$0HlL7-HlLnIp10$0ylPv=ylLM0j10$0YlPd!wlRJ|a10$0UQzio= zQyx<x10z!@b0`BNa{@~%10%~KmSqf#EC*Q*GBC3oWjW5k$a0e93<D#}IaWaiMpjW) zQ3gg<3Dz(MM%EbC7zRexWY!c0M%FaeOa?~QY}Nt>M%EJ65(Y-rcGjs3jI7hyUNbPV zy<>aFz{vKIU7UfDU5Z_efsx&T-GzaX-GkkOfss9iy^w*Cy_LO#ftkINeKi9k`w{lb z42<m8*l#m1vfpRF&%nt3m>m?qFWKKPFtWd6|Hi<`{*(PD10x432P*?32Nwr710x47 z2Ok3?hX98V10#nhhZqARhct%`10#nUhdTo!hYyE810zQuM-T%eM+iqK10zQSM<fFy zM+`>{10zQqM?3=~M+!$e10zQ!M<xR!M+rwY10zQ*M=Jv(M;k{E0~1FtCm#bNrvRrg z10$yxrz8U-rwpeI10$yvry&C)r!l8710$y`r#Ay5ryr+310!b`XBYz`XAEaN10!cL zXEFmLXDVkZ10!cXXFdZXXAx%+10!b%X9WWzXBB5110!b-XD<UI=XA~)42+y}IOi}h za<1WA%fQIFo^w3|Bj-lWjSP&On>jZ#Fmi6^+|Izrxr=ib10&}i&OHo_oQFA&GB9#p z<GjYe$a#<R83QBd3(l7ejGV7I-!L$8o!~mbz{qut>ly<i*A1@w42)clxE?bwa=qeu z&A`a@p6fjWBX<LL0|O&>BX>6gGj|Vn4+A52FZTilX6}XDix?QW7jtiBVCLSzeV2im z`yTgw21f1&+z%O;xgT*qVqoNc#{G(ck^43G8wO_Xx7=?T7`fkZzhhwLe$T_gz{tbK z!_UCTBgCW0z{sP+qszd^qt9c=z{q37W5mGBW5r{`z{q3AW6!|I<G|y{z|7;s<Ho?q z<IdyGz|7;p<H5kl<INMyz|0fM6UxBI6UGz9z|0fQ6T`sBlfaX}z{r!#lgz-#lg3lb zz|2#^Q^LT=Q_9oMz|7OZ)4{;V)5X)pz{oR!XCeb5gAfBdg9?KVg9(EPgA;=jgBJrM z13SZKhA$lL9332;99<my85kK@7#JCYp)?Z%69XFq7Xu@sH3K74IaE{^N-KkD1_n^R zy}`i1@QZ<gaR&nvg9143F>p`gp2@(-J%@V}11t9q?%fPB+=sZ2Feq@J;6BNq%zcjg zB7-XT74EAHn%vj9uQO<K-{QW-pu>Ha`!0hn_e1W740_zJxnDErbARCez+k}ro%;`i zA@_eC76x-3HXdFETOI)(H3nB64IWK~7;wnOfkQTd$C$^GA(_X9$A%$`$CJmCA)CjK z$B!Y0CzL0KAr~BKr95doxeR4I`8>r8bv&g!<qXX{l{^g$Z9Gjp?F@Ykj12tTo4L1e zZ{^;`y`6`RhlfXo$Cf9Tr;w+Nr;4YJr<s9?L7p*~F@%8uTvjl#y<vO9z|8iJ?E?b~ z+i$kN4D9S2>|6|7>=Nu!47}{B>}m`G>|yL-3_{?XF3fe0>mGwRcO&-{1_|yx+y@zS zxQ}ulXE5SE&3%@^l=~9*B?b%bYuwiuEV*xR-(;}jzRi7y!G`-D_X7qy?nm6O7#z9Z zaKC5p;{M3}jlqxmFZVx&5FQ2|eugj}As$19G#)D+Ylb!+J01^)4jykFUxvv%{ya$x z(|FQ&+8I_cFf!<HPvxG)J)8S74<io~4+{?)4?7Ph4;K$N4-XG7j|`6-k35eek2)xH zc_MhSd2)I3c?x(6d5U;SdCGXoc`A9TcxrfRdFpuTc^Y_{c$#@ycv^Ydc-p~r9T$TM zLmmT*i<?gfgH&>5Q4WJtdQoaNgH%ppNgjg~0}BHK1H=FS49pA+;PRe1JGCf}K_oY^ zD4RhlH#;|*K@ThgQpN#M!N9`6%D~3J&cML{sz*5)xEQz@co=vYWb)rlG@5p1U(AI= zmvSzfTzPZ7?XJ_^M=xX;m>4)1m>9UEmoYFhhk@*5T*m*4Z5jVBjy3@v{x1SNV$(Ql z#M8t!i7jFK!oE&CjWa`R6Q_yTCiXiVZem~fU-7>ZPZPVtS;W!CQ6lyZ1UcF`CJ1N= z7;roin}!6Jh^L7yfx=B<$nXxQ2}mDEEvJ)s8U%`M;*0~?3e~ZMGXi8cX9i~mXB>!x zny^Id3kdSR5_>0h2ZTYTCTEe@G_fUOo5V%L-Nco|6U0S0Yq&DRMK~vkyK&tR7vX#& z?j~-+rNosXuEf>Eb%X1kc!Ic+IG;El?*r}#UOrwG-Xh)yyw5~ViA>|$BFZ7MM`Rk` zK9N&mJ)#_<90Hd_riq*q*~8Bw$|2Sx)*~`UWRAcY2oyO5atREBT*Utc41e+e65!zn zlc2B>PY`euR}z~hwnyxkz!{NgBGUw7Kv*D2WDm$Tu^xdl0+&Rd2;2}X68R=FN8}sG zW{`a%PXv2}%!KBE+#qyK=$gnkkQ;<P2+tDb5Pl(62I7hJfJ_wS5Sb&g2yEUSky9d1 zz-9=E3W@cI3W*AdN{Cj8o&c*yjwNI^EVf`Ya-3}v+XIKk#9$B{1E5$CdnfiqTnQ4U zpm2p_5ph)LCXRq0^&p%ejtcqUx^b~!W`cBsunE{ykSWmA1W8kHvOv5I0VOs-pm>k? zH4udG&`I$J5_%FFB(_P$N%Ba>fnkp1J~%ufr3Z&LQf^WyQmdr4NJlX+GA?6aWq{;F zeg**sK?W5DRR%Q%bp{OvO$IFnJqCRSBL-sz69!WTGX`@8O9m?jYX%zzTLwD@H-<=t zD25n@IEG|~T!vbPMusMaW`-7qR)#i)c7_gyPKGXqZiXI)UWQ2wQy8W)Ok<eNFoR(h z!+eGX4Eq@lG8|$!!f=e?1j8wYGYsb#E-+kVxWsUU;TppYhFc7G7(Ot3XZXeNhv6T? ze?|sICPrRHenvq?VMb*}RYrA2Q${mJb4CkBOGYb38%8@uCq@@WH^xxLD8^{U7{)lp zc*X?AM8;&sRK|41490B69L7Axe8vLCLdGJ-V#X52QpR$|3dUN-dd4Qk7RENlcE(P| z$&7Ou=P}M_T*kPZaV6s_#x;y<8P_vzX57iRi*YyO9>%?l`xy5#9%4Mrc!BXT<5R|u zjGq|4GyY`!#rT`?5943Pe~kZ`7?>EDn3$NESee+E*qJz)xR|(^gqcK`6qxLp9GF~~ z+?hO?f|){?!kEIDBA6nX5|~n$(wGXEN|`E{Dw(R8YMJVo>Y19DnweUe+L=0;x|w>J z`k5v$O=OzPG?i&O(@dt>Omms$Gc9IX!nBHM4bxhtbxfO>wlVEw+Rb#7=>*eBrt3^M znQk-PXL`)^l<7IsYo@nM@0mU_eP;T~^o{8|(@&<~On;gFGcz(XGqW<YGjlR?GxIX@ zGYc{cGmA2dGfOf{Gs`l|Gb=JHGix$yGwU+zGaE7+Gn+D-Gg~rSGutxTGrKaoGkY?7 zGy5|8GY2pSG6yjSGlwuIu$*E!&1T1D&*s49$mYc6%;v)8%I3!A&gQ}9$>zo8&E~`A z%htfw$kxQx%+|uz%GSr$&o+T=BHJvs*=%#z=CaLWo6oj@Z6Vttw#954*fz3lV%yBN zg>5U_Hn#0-57-{EJz{&z_Jr*z+cUQ3Y%kbevb|y#W*1=>Wfx-?XIEfXWLIKW=IG_< z<LKv@z%h|y631kYDI8Ndrg2Q?n8h)hV-Ck$j(HsOITmm%<XFV9m}3dYdXD29Cpb=V zT;aINagF0T#|@5~9Je@bbKK#$%W;q60mnm*M;vcC-f_I=_`vaz;}gd}j{lqtoQ#}I zoXngooUEK|oa~$&oSa-MxK?wm<yz0Rk-MHjn8E!25eCcuZy2opzhSWX|A=A!|3{3< z|Bo=L{y)N~{{IN0>Hjy3=KmitTK<2;`1Ai8#^3+nF#i4jhVlRZN6h~JA2A31f5aU8 z{}FS-|04`~3_J`X4E+C(FbMuX!l1+;!l3g14TA%N2txvc2t(8VHw?}H-!QcNf5XuF z{|!Ui|2GWn|KBhi`u~RE$p1GC$Ns-zbo&2>iT(c(Chq@7nE3x6VRHHZhUxGBH%$Nk zzhP$l|Av|Q{~Ko3|8JPt|G#18{QriT`~Mqe-v4iy`TxIR7X1H)S@{1Oj^6)|IQsrS z;^_bXh-1S4M;sIXKjN75{}IRJ|BpDP{C~tT_5UM|Y5yN_O#lCgW5)k;95er)W8mg^ z_x}yY`~TlKKKy^f@$vr~j!*yJaQyrKjg$5NH%_+y-#FR-f8*r%|BaLL|2GCHhTQ+( zn1ug-<Cy*b8^@gg-#F&}|Hd)z|2K~L|G#l8`2US#!~bs_8~=af*!2G!$L9avIJW%% z#&PBUH;$|Szj0jq|Bd7N|8E>O{(s}R`TrZot^eORZvX$rap(Uxj=TTAaoqd=jpP3R zZyb;Qf5YppH=Jz$-*B@3f5XY~{|zVS|2N?FiU<Q2g9sBFg9sBBg9rl?gDyi9gD-;y zg9w8zgAs%I|2GVl|GzO<{r`p(+iw_E|G!~W|Nn;3^#3=c*#5@&`~Nq_zyH5siS0LF z6H^$>|KDJ+{Qro->i;7KoBuZ$=KsIJsQmv1qw4<~jOzbyFq-~<#AyEi2BYQw8;n2y zKVtm-{}JQg|Bo2||G&ZP|NjPa;Qt%U!T)bCC;Y#`z{~&&UF-jEINtt$!@$a*%aF%$ zm?41U2txqJ`~QzP{{4T$zzFsS6N3^%0K-cLHwGq-_y3P@{QG|d%=!k3GX_=${{P<? z1pj|yV*me*iTnRICJ<kQA@~0q##jas#?=4c7<2xAV=Vgrh;bT&2$S&tH*7f!B5b(~ zB5ZjKB5e5#B5VZ=B5Z{WB5W%dMA-f@h_L-<5MgIv5MgIz5MgIx5MgHqrF6zv1}?^F z3|wqE3|wrv3|wq^3|wsa3|wpl3|wr53|wq07`WK}F>tZ{XW(LIVBlhBWZ+_FV&GzD zX5eBF1f?RzSO$K^X$<_J)Wnv<z|WS;z|WS)z|WS?z|U5|z|U65z|XdVfuHRk13%k; z27Y!127Y!%27Y!X27Y#C27U%M#-jh{7)$>@Vr=;Th;j1&H%touA2Hegf5RZk!2kao zgW&&jjB)?pFed(g!<hX45o5;xH;noJzcCj4zrk4e{}E&H|8I=t{~s~7gG1{a6Zijf zO#J`PF**Ey#B%!oBev)N->|*-|At-o|08yh|Bo0LL7~PV2g~K)@QnL^gE8^{4RE+- z{J#MX+ur}*z+sD&oAHM6^Zz&4Ui`m-E3Dc1|9@i_`2UR|mqFnFImTE9ImU|rZy0O- ze`Bou|Aw*Q{~N}Z|KAw9|G#1E`TvHo@BbUd{{L?nC;WfIIPw2C#!3I*Fi!dZjd3d2 zRdS5e|9@kg@&66u%>UmQXZ`=i#Q6Ul6Vv~5Ow9k!vDy89!)E{g4V%ONH*Ajo->^CT zf5Ybd{|%eV|2J%|;FKcAmdhZ=md7B+md_x^R=^;~R>&a7R`vf4TlN1pY&HMiu+{#5 z!&dkI4O{*HH*9VHzp=Id|Hjtw{~KH9|8Hzv|G%+y|Nq9;^Zy%L@BeRXlm35WoBaP9 z+m!#`*rxvf#y0K$H@4~jzp>5u|BY?t|8H!I{=Z>c{QnKx3I;j0RsY|xt^WUpZO#90 zY-|62V_WzC8{7K--`F<%f5W!%{~NYV|KG4}{{Mz;%l|iQTmQdd+xGtr+xGu&*e?A4 z#&+@lH?~Xvzp-8Z|Bda+|8Hzp|9@k<_Wv8(_5a`49{hj9_VE83wnzWpus#0&hV9A! zH*8P;zhQg!{|(ze206C>407xY407y@407yD407zu407z;|G%;G{Qt(z`~Mp|-~Vsy zg8#p<3;qAbF8%)-yUhP@?6UvAvCIAc#xDQ=8#qV4VGsnD?r)g5|G#14|Nn+T7#xS7 z_yffoEY3i2HRJyqcs#(u6&4P#Fo5|P>RtwE2G;*a7`Xl)VXOeBif@ed|KBin{(r;R z{r?*@ReWRY|Njk~Dkgzbz&9qg|3{d({vTnp`~Qv2{{J_&s{h~Es==ZDjcxJ&Z)~gn ze`8w>4&iTX8~=Y}d;I?!+mrv_7#Kl0hQXMD|NkQff&U=CJ%alNlp4`}G?_sJl;W5e z|G!~k`u~QB85|amn7IExV&eb*2<}^4sSXwj=&1`=sNhNocw(A?iRs<{H%uQuzF}bf z|Av9<{~N}Y|GzOF`2U89?f)AluK#bC!v23_O8EbVY5xB=Obh?NVOskC4b$@fZ<x0H zf5Wu%|2L-H|GzQq{r`>WE~utq;QxPvLGb?##yS7rFfRW8hH>ftH;l{wKVn?@{|)2X z|8E%A|9``{;r|=PE&tyz?*9LXanJupjQjq-VLbf*4I`*MaQ6Qj#w-8dFn$Dw)(s}^ z|2LTU|KDH=`~QY1{Qn!Kxc_gMQvQEqO8ftfss8^PrpEtom^%NzVVe5?5!3YlkC<lu zf5bHV{~M;c|KBjJ`u~P$-TyaC>%lI4!?gSV8>YSg-!L8h|Ay)K|2Irm|G#0n{{Iov z&Hs;>ZvTJ8^z{E5rsx0PFunc%hUxwPH%uTmeER=}>HGgTOuzrXVfyp`4XgP7Hw>V< zNQC1pxNI!=e}jPqToO0^f5Z6p|0AZv|KAwc84LdJ0+-Vz|Bo=X{C~r^_x}x!_uv-B z4F*QWg8z>gSQ)J0CG!!+;{R_LPeAj{BL>0$kC@p1!_x`_BiK|a2LAuM7zF?CVq6Rk z(Ho3w|36~f^8XPdYN+jE;{LyjiU0pDrm+7vz@fJ8|08H9++f=M{|3|E|2LRk{=dQW z`u`24kN+PrfqeP<|0B2$8JSLjYF?(O|KAwY7+C*5V&MA!h;h#UM~sUYj2M^xe*})Z z1OIO@9{&G`@%H~Wj1T_5VSEfuHIJCM{y$<$_<w^b<^Ln5wEvHoI{!an>Vn46wEu6I zrvHD#G!q<0kC^6xQ`QZp1>n?mgJ}^sW!+%f`u`2n-Tyb3?t$au5!1W>kC@*7f5i0R z|0AaF{~s~^`2U814dyZ?w*L@YVX0Y*f%X3e2Cn}b80Y*y2X*r~#@+vyF&_ATgz@nI zbBwqD-+;RL1{2%=4NP4BH!vmqKf;vqe-~5Q|6NR-|IaaX{lCF9_5U)aY5#99P5*z6 zY3BcPOmqLAW19E>2GjiiN1$##0(SG#|BslKfpf+qrWIg69D(}b2-FYDnBM)r!Sw$B zGNupzZ!mrTe~#(L{~HXF46OfmF>w9g#d!PwIjD=yF|qyM#l-c07gON>H%zJj-!P^B zf5Vjj{|!^u|8q>!{-0x-@&66etp9JA=KVhhb_FQZK(>Qy28A3b)ZYEy#q{C-E~X#< z&oS_U^CKuPg7Vy9q&$SkJKvaQ{{IHfH?Z{o9Tdild;dRTd;w0|H<+UTzhO%J|Ay)3 z|8ESSFovZINIu{F{|F-_O}qi8i97$lF+TYJ4U{ezzy1f6ypUKzO&cIzf%5MWCP><V zg)_)^pi&2vHoh?}{0~YS-<Y=i2c?Z~OgH|2W4iVK8`B+d$pcC!-<aP1zX2}K-v7S= zD$~IE7L-ne8Cd_HW8ebE`WwbOkd(^!6`WenF>!%o`VH82M7X|Ty7B)F)2;t+nC|?4 z1CBXRtbt6^hNU`iy4nrS+i$@BzWx6ZGzK5RQ)D2x^aSMvP)vf#El_NJW6J;kjj8MZ zBc`eUK{kWRt~X3G{)6HG6tmx$=KX)fH2*)y@1Rl_<abcXdk&h$&Oy@{C@hfk3CREF zm>~WKr7uwHZ7(P%GrnMu1Lv1-3|!!t_{IdHVflp_9Lp#Ee`9?6{~H4vsI+2y@&6Iy z+y6(HqW_;`O8kG0>D2#63@l)Epi&N!5<nu081g`(Oz{kI;8Kc-5md&4QV1hc)c-dO z67V_!RI`A}e^8k1!zkasp_J>OaG4Gck8j}e98{Jg*EXOs99mv8fZ`WYcb)(LjS-aU z?*0D;O>vMqtl|GRraAw=F|Gdpjp^8b)YS9r|2L)=|G$A#4yYys#qSLUE^r<82wc{I z>Mn@8A$8ROaD4?S_aJ574JIyV9R;c>Q~tkUO8fr?TtDF|`yh1`to#GTF{HkN)T*d; z)!qMZFzO~)8ORDsaquz#n)ex)7#A_*F`Z%%VPFRJp5T2c5YGl$4}D``0@pzE|ASaM z3?dBH3`PvB3{Vy`3j-Ik90M1#0RtBU3j^o>M+`~~pjz)7sMpNE2;woYF@SoZ5OG-j z_J)%MTr;+VY=_o^Zx~C!^(3hORr>!MsJ>)U`2UT8g)#a6H*nnt>dSz}2H3zNkp2p& zuL9Ef4czhq)tYa>eLhe>{S5;v)}A^e*P8#|7=*yBG_+cjqxb(ej=uljIQsv8<CyUO z8^^@|-#8}y|Hd)-|2K{)|G#le{r`<)+W&7H)Bk_tnDPG&$4pRN4-LB;48jcj|2HrQ z{@=jF{(l1#_x}w{{QozQIQDRvW9I+M-1YyzF(@#A+QVq=V-j8XjbrBjZyfKyV;J8! zKK}p4@#+6Jj{pC^aWee>#>x2q8z<BMZ=B3H#xxi?S^j@x5QVk-VC_BVxX4HJaS<Hm zVvI>}ZTJrwDZuW#HynNc-*EK*f5S21{~L~p|KD&-`u~Pw^8Ys+Q~tl<nEL+>$F%=% zIHv!90}FppY=Xm!nd9yMZ(M8sKjN<c{}I#^hqN=9*#E=Zh#bBD$ua*8*nB3gHUB}Z zHw==r9^(P|i2>O^pj3UB!G_}qgAD^a2b9Ih`X3`~83Z}r{eJ|?e;gnFKjQfK{}IP0 z$S4WN|Nn0|8UDZFWc>ezlj;8(PUiohd<YuNdc?{0{}Ctq|3{o0{~vLJ@-Y*~VFo#l zBMfp3nk4!4Fhdl_5r!yo#<e)!{lCHS;r|VekN<CQeENTb<KO>B9RL46;$-;$h?DXE zBTlCOk2snCKjLKle}j|l{|!#||2H@}{@>u_{C|T%faBf&%iw(Y;s0fhkN+=oeENSG zY67OOS^s14v84!TZqDX|`vZnZh9J<CA3ps54L(^?<o<tysYbA1B7=|q51L`e=`5HJ zU^GsJG?gIVc4A}o{}FT#fWqZJXml1`0?Plt3(5e^INxAkK;|Q4kXWEqQAk`&CN8(a zd=H}$Ix$5U7(niZsrdg0#zLmC=|YwVv2nQtBu6^_|ArVl85pQ*E2cY<%_r=l|KD(_ zK^8}5W7<dq5zwj_h_MJ3gij|DzmL&n(fKe(;u43+qtc|QM%9bX!_4dGDv<dE^EHxU z<n)CkjLein<_tGBa@m97H-hCKh7NoJG)enJ#2=`%hu81;9Ed6R{~NOTpfVAJG1Vc; z@c(a+bzoJA)M`O3uaL`rWH!W<|K}JOK$yXZf#LrnOnHbZbP^K3Al3hGpynb74{Rcm zCWt7K0G#>%(f<t)4t5{n%I~<uA!ebI*v&>)MTk!wQy{jG=6;Bt!A%Y|A5k+lNsXDI z7FOu~8&LNnN_+$h-GNl#lk3O-H;7AF|DQukUMjeiG$Sy51}V>BrV!_TnCihmWBLf2 ztBCU#F{*H@!7YRAB9JN&Mo$3{F=UmfY(ny=s&Vm1GX=kDNKFrFpZtIH|J?s0*lK!k zJK-G6H6R|0jg1DGh;9nVe307z-%wL3NDLFh+7zU;*kPvN_bp5nh$hA*Aa#Qk6PIW3 zyB@b0gkl+CE-1bcJaiWE@qw-r#)p}J=~mQKhR+@UK`R!)2I5FlMqo)W0aEe*5zJPQ z(Em61%m%4I!7#H>Yd4rEic&BKw+vXCJOU9m{~!Io@&DWZUH^~3Q@jnxOo$IbJ_Grf zfeR!C8oBv@gaP7m2ni7e^_Eb>WEX=7SRYg#kzQadWLc;pC<W><fH0Jci-Ls<sylG^ zK5@(7vVmlA%rr={5d*3L66X*nfMOoP!bSeS0gFq73Wy1a)P<-4K{A-?5jqhnA!4xf z0n!H%fsp^-AWZ*%<NpSH?)-n`|2ar|4Q3k1448@V^bYPJgH-(A1yT=E1H%6wfo(!f zUmyvRFvK>HOOUleL?I%O+7DR-B90gz0P#WfFNg+VP>F+ZJ#xswOaYA%fJPmVRe^NE zFhZPAngz+@4r7ok%uHPL|8J0Z2Ztu8q=1fPfb@c7Kp2-^usFCp0<&O*8;k*=VZMg& z7#JAP<l*9EGcasGH;JGd(KRq&>cCJ!oB*oZK%t6VA7(g%bpAhrDGt*Ql83X8Ffb5N zD#Mhc(GZg$B$^ZvTu2TftdLmq@u|ZTet2il&|QbfVdyfX@JTfVpU*JuB~3M^euxO} z^aGK{B$4eUtQ&XiBbQ6aY)l(*i{OrL+<Gu&h%*J*-<W0$pa_L^9J0$P^CNM(L8?F) z)E=dt-N+{4W)q(yAufcN1geiAENYMtlmDMXP4f^@i22BDhzN`XwR7O?M<5<3?Sf1~ z>H&aiY52T1NDU~Y!L5!B|BpcUa5KQQIVcw+#Ic3Z|2H5t5M3aZu+{)rL<DR;xO|1Q z@35HwRtY7bZ8L`d%b)@%6v#&?0z-@gial6pL1=7kZJ4J(s<F@RfMOM)3Stt3k7^z$ zJ|N<_Nr)}j<?+cubRfF~B7%*CnFC5`*yKqUgPBNJC!rKa*aX}vaHmJmC^2q*=rY8G zBDyYgJ}ey|hYd0tT|M!9kUvrKHM&+h^ua+^LVQD9j1s2`WGX7g)CY+jP#Azzg4_TR z0rQaNk|7}kk^8?3)TRcFt3vvzAhj?IQ3v87=TC?}kSGNIKL?WqsRx+^5eJbV8B}+| zTnCaR9TQuN5o-%5j8I(xOPdf;nA_3Sp_dQ;Z$QVSAlgt!NG%C60TezcQlMH6w4N0# z2vz}_Ek_y00!u+osDUz(DPsMJtcTuga^3j<94JHxyB3rlLFtUJ%0aGz>SG3^`<L9f z0=1q%wvnqJG*Sf8PY4s2B4Dn8nMSNGnEH`OBgY(SDNA1HA)ALkJYi)lepRH%qLty$ znL$i{fJzRSx&LqAa|fblf~^c^fS1o8JHWFU;GP7`Y{E21A9hu!e!;E=B1*hUi2A`z z5}$sM{ee3i5vq`59U+R$!nRHgVlr|)14{`Y84yM`fl@Zay#H_TrAb_Npz1;PEvmmz z)geqHFXR#W2LtQ>8&Inj<`PKD7B*`S6Gf&$;SGyVWN~8H<mv<Um=WefLLD|!fe=S! zk(<ZK)rV?3ZXT|*gj)ywWk7L^(^kY>2z7gi)Ley)aM>VeDPj*vNGgWN;SFzy5)w(& z9s$05_rDHiG;;X|6GNr3rCk!-4qcakY9=u}gd0JsvAYwWJMhUN&&YyIM__!RhEN4# zp}G+!icBNNII=i|4RIr=ET(k{1hJ1a65@7<8xeg5%$NYx`XJ082MSkAdF-}A)PYhL zXx$pJ8c=wEOoEAl)Zp9K1sZFHxRREk2nh|Cn<%827?v=wGO#i*FmQoR-(}!u5M*Ft zP+?GEU}aEa&|qK#pRLHrV8md;z|COJV9UVA;KJa_Aj;su;K?A);LYI8AjuHG5X>OO z5XunBAjc5S5Y8aa5Xlh3pumvJkjtRLP|Hxupvut1(9594Fo|IjgAv0Nh8YaT46_&( zFjz1wVpzdo&#;PNErT1wCWcK6UJP3pb})D|>|)r(5WujX;RHh<!zqSS4ABf{7|t-n zFq~sJ#}LbKf#DKE9K#icy9|j8&l#RGWHWqc_|A~S@RQ*eLoUM~hCdAX4F4Dz7z!Af z7?~J~8Ce-w8A=%08QB?18Mzp_7|IxV7)2P$8O0dI7#bKQ7$q1Q8KoGd7@8QB88sQ2 z8MPU;8M+vC8Fd-D8TA?U8G0BE84VeF8I2i@8TuGa8Lb%l8EqJC7^X7XG1@UqV{~G4 zW0(#;A$$&FBx4-IT+qpm42v0488aA`GUhPmFsx+EW6Wb%1v=f4VKrkhV==>8#!|)# zhINcpjMWUA7;7188MZLiGuAU~Wo%|_X4uBq%Gk=Vow1X#lVJyAH)A)$PR3rwUWQ$a z{fzw#yBQ}jPGZ=@IE`@{!(PT2j58SaG0tI}$FQGqG2?QELyRjKS27%DT+O(a;RNG) z#tjUo88<U-W;n~ZopC$EImVrgdl=4x&WU8W%y@|LFvC^Gqm0KHt}~uwJjrm2@eJd6 zhTDvn880*3XS~jMo#6rFO~#uH4;gPW-e-8k_=xc_!*j-`jL#WfGQMPd&G3ftBjZ<w z_l)0}m>Iq>u`;nUGBR;8aWk@jPLyP12c415$O$?Zoso;lnaP!r8+0x@BOj9=lOLlH zQvg#4qcBq#Qxu~FQw&oKqbyS#QyillQzBCVqdZe7Qw^g&Q$157qd8MEQy-%h(<G+J zjBZR*nHDg5FfC<T!5G4{ifJ`t1k*aE^^8$Wo0v8;#xQMT+Rhlqw3BHMV*=AYrhSYl zOb3{bGo~_~XS&0f%XFXV0b@1OW2P63wM?&>zA!d3{b2gR*vs^rS%tBWS(Dk3aR;+A zvoqsGW>@AA#!Jiz%!!PTK&M7BK4G(C^JRR-7R46L_>C=#t(x%%TLW7w6Bk<_+e9V- zwpnb8nMBx@v8`s3W81)XkV%E@1lxHgGqxLSubJ%FKC*pg3S#@k_KPW$?H@ZkQy4ox zy8u%HyD+;rQxdxZy9!e}M;k{QQw~Qj$8@G#j@cYbm?}7qa-3mm;<&<bkEx5}5yvB@ zsT}V(-Z4$%_`va(X*$P$PG+X1oUEL@Oslw-axG=r%(a|rInx%dbzJM1wlXj=UIOQB zMR49$WZ-AuXRu)qU=U!iW)Nf$WUv9}b7lrL1~mq426YB?1{MYl1`P&Q20aEn24@C+ z27LxC1|tR|1{MZm24jX$@Oj=W4CV~x3?>W~3>FOh;FDI>8LSzs8Jrnx7;G4%8EnDO zg~5e^iNTY>lOX_HMldl1Fa$C%fy)Rha2a6$K9`l7A(|nY!G$4)A%=k)TweHr%L`wI zCWdwfW`+)i4hB|+PKHhfZH6v}UItch3Bm#{L0A}OG0b9MVVKP@kAa_IKEr$lX@&(1 z3m8Hf7BMVmU}D(Mu%CgO;UL3725yE!42Kw)84fcXW?*7C!f=Fvnc*nIQ3fW4V+_X_ zm>G^U9A{u+IKgm&K^t7wXoJfdZE#tm&2WL?0s{-fMTUzEfee=zE-|n$Tw%Dvz{+rq z;Ti)g!wrTT46F>d7;Z7JGTdRf!@vqIkC?#aku$?jhMx>B48ItDF=&BHB`t=34F4Fk z82&T-X9#2j9bv5nE}^u*B@`35gkl1hP)v+Gj64jW6J>cBSQz;k`59Ok1sMeySQv#F zg&A1EC6^Vr<gx;nTvm+AjLHlqj4F&O3<iv<jH(PKjB1Q(3<iwqjOq*~j2es@3<ivv zjG7Gm;L?pBT)OdtOE-RS>BbK(-S`<z8BH0~8O<2Y7`PeD8O<4_87&ws7`PcN87 z8Lb$t7;M31pDnoTvt@K*bYfs;bY^sBU}AJ(bYWm+bYpa5U;&qgoQ#o-kqlgnQH)Ux z;*8OZ(F}ZyF^n+`0*tYYu?#$nag1>coQw&K2@K(kiHwO1oQ$cAsSM(bX^d$MT#V_A z=?r0v8H^bWoZvE499)KmFcvTtFoZJ}G8QuMflJab#!|*o1_8!0#xe#j#&X7T20q3L z#tH^e#wx}t1`)<;#%cyea0x34E@Az_C9DXzgk=Pmu>Rl@RsvkYN`On)2yh820WM*A zz$L5%;~d603~G#X8Rs&nGR|Y1$6&~~m~knC8sjp?Wek#x%Ndt5$b(B>W5(5ts~L<K z*D$VOP-R@pxRybPaXsUD21&*Zj2jq?z-6%{xGd&n+{w6;A((L&<1Pk6#@&p&8B`hf zFz#WHXWYlQk3o`gKjVG|A;v?DhZuwy4>KNSP-8sGc$7hn@fhQA23~M^?ZkM7@eG3; z<5|YD41$d37|$`tGM;BV&maXZ!KJ_@I2*VGXJfq0c$-0v@ebo11{ub?jCUE>81FIO zV~}FJ&v>6fkns`YBL+dn$Bd5|R2ZK!K4nm0e8%{kfsOGc<4Xov##fB57=jpIGrne! z1Dz?%pu+f>@iRjZ;}^y+3{s3=8NV{fFn(wJ&Y;TpgYgH09OF;MpA71ZzZicpa5Mg9 z{LP@w_=oWi12^Me#=i{ejQ<$_F>o{fXZ+6~4LbUtft!huiJ8F~bRsc>A`?3kJA)1A zTw(@ACT=Ef1{)?}CSe9YCJ`nP247H(!XN;uQ5g7`oSB>%n3!CcTo{;{T$x-Mn3&v| z+!-{PJeWKfl$bo3JQ*~YyqLTg6hQS2gDg`3Qvd@mQy^0yg9TF%QxJnUQ!rC7g9=j! zQwW10Qy5bigE3P$Q#gYcQv_24gE3PiQzU~IQxsDagD0qdV(<jjPYgy(iA;$MY)nZ^ zNesM9$xO)%Y)mOkDGc&VsZ6O17EEbOX$<mA=}hSi7EBpTnG7;aSxi|ByiD0l*$iw< zIZQbWyiB=FxeRPfc}#f>yiEB_`3x3J1xy7DhD@bQr3^w$WlUuZf=uO1<qY0T6-*Tj zDomA3l?=g5RZLY3Y)sWm)eLq_HB2=O9!&L2^$g}r4NMIT`b>>XjSNOi%}mV<=1eV2 zEe!fhtxT;9l1yz(Z46RO?M&?ql1v>;9Sl-TolKn!l1yDpT?|r8-AvsKLQFkOJq&_O zy-d9fc1(RteGGC;lb9wkurW<$n#^FxG?i&8gDTTBrfCdvOw*aBGq^L&V4A_;#x#>@ zCWAZEET&luZcMY8W;2*G&0(6upwBdyX)c2h(>$hm41!GandUPHF)d(Pz#z!9lxZo0 zE7LNjWel=R%bAulxH7F^TEQU8w2EmJgCx^xrqv8mOzW7|F$giOXIjr7$h3)R6N3=b zW~R*yf=t_(wlN4XZD-ofAjq_nX(xj*(=Mi63{FhDnRYW6Gwos8!{7v}?-^v74lo^H zkYzf^bdZ6U=@8Q)1~#U{OothGnT{|WVPIoA%5;=LlIa-JF$O87<4ngHjF`?dooC=> zy1;aSfsN@R(?teerb|qh7}%IDGhJrjWxB$2g@KLfD$`X4UZ!hI*BIECt}|U{kY~EV zbb~>L=_b=n26?7iOt%<hm~J!OW{_vP!*qv1hUq@jeFjyg2TTtb<d_~aJ!Y_DdcyRC z!Gq~3(^Cd>re{pg81$K*Gd*XJWO~8$f<d3@HPdSbGp098Zy5BL-ZH&qFk^bh^o~J~ z={?hX1|g;oOdl8onLaXoWH4j;#Po?lkLfehX9hE-FHBz;^gt~b20f<VOurd~nEo*R zVGv~c%k-Clndu+XKL#eI|4jcGn3+LWATTjAGBYwTGcz$WF)%SRg9Mpbm{}N@m|2-w z8JL;bnAsSZnAw@x8JL+lm^m1jm^qm_8JL;5n7J63n7Ns`8JL-Qn0XkOn0c9b8JL;* znE4o(nE9Fc8JL*`m<1S^m<5>y8JL-cn1vXan1z{z8JL+xm_-<vm_?aI8CaRcnZ+4c znI)Me8CaR6nWY(6nPr(}8CaR+ndKQ+nH8B88CaQ>nUxt>nKhX;8CaOLnY9^Mn01+T z8CaP0ne`c1m<^c?8CaN&nT;7(m`#~Y8CaOjnavqAnJt(t7?hYTnJpPKnXQ<u7?hZ; znXMT#nQfSD7?hZ8nQa*~neCYE7?hapne7=gnH`uN7?hYDnH?GUL9IOoer8u@R|XAc zH)b~m1!i|<cLoh+4`vSr1!hlXPX-NUFJ><W1!iw%Zw3u!A7&p01!iAnUj_|kKW0A$ z1!jL{e+Fsh0OkM&ZstJdKn7{%Am$(jZsuU-U<PUC5atjDZsr8$1O^l4MCL>W15j&{ zfe&;FIfE#h1Dh)YJDVGuF9Rp2Wyv7Imc^FMz{r-vR>;7^R>W4xz{OU@R?Q#*YHKox zgW8%5d~CDW7BUEc&LC$HWn0F!oI!+b1=~snMz&RKs~IFf=aMssvTb79&cM#LgKa+p zBijMCgA5{|_9z1*+YPq+42+<&${E<%9<e=P5M_JH_Kbm@?K#^k22Qru;G2X#vVCR{ z0ku{c7}@@@Gc)k8v#_%>aDm#a3=-_Z>|zXj?BeX=3?ZO1%^Ac&r<yYeu&c1EfN$|> zV+a7XVj09ZW^*iHQ07?3v4nwx<0!{51|QIA<_r!TCpk_s*mIoXIK|+=ahl@{gFUE? z%fJC@<1%o7+PDms93MD7F<5eZ=J?AX3~K2z=yI}hax&;}a&hu9n1b574341oE`uYe zz02Upz{JD@Ztq%w+q+f_JPbSx_TW~p4pOUE2i)rA1h;xw8MHtr7=qiltl-uyE4X!Q z1a95(fLpgp;C8JfxK(S*;K1O(V9x+*-Lf;dGPp8`Ft{;zFff8!y3!0@3|<Uc;P$Qv zgCB!Gg9y0Q%f=AI5X8X95X=zFz{U{55W>I+ZU?h5gfWCMSb|%_8VnH(5e#hLezO2W zEJG}VDMJE70)sucP3!}16MKW(#GK$ZaUi%&%nEJ?voe6%!K~nRFb}vLEXlBlVG#o( z!(xUN4D1Xm8P+l|f_v4B;9fN&!)}Is3~b=`u>ivXh64;D;8wB#xRuNaZY8sWTgjZ@ zRx&%dmCOllC9{KD$*c?~8BQ`Vg4@ch45t}RGcbZ%%&ZJ&8O|~=g4@lk4CfinGcbbN z&5R6}87?z0GF)Z2%D~8Qo#8qIBg0LGn+%K$w;66TFf!a_xXZxEaF5|011rOQhWiX! z3=bF{Ft9Q_WO&G+#qfv$bWZAHhQ|zA3{M!IFt9Q_Wq8V<1#V}HF#KTn!5{)|XR|T< zX86q@!tj^jFM|lUg)PF!$jHbb0&ZKgf!o$>;8ryoBQGN_11lpRBOe1JxNXhKD8MMd zzzA+(voZ=X3NbK3+u0(FA`Fb+);1fswavyT!zjaG$*91nz+ee(a~px%+#29Ew-LC_ ztpRRx8-d&08sIiJ52F^N76Ti&<;}yW!>Gf+25x`zFzPYtF|dJK;XI56j0OyB;I=pq zqY<MK0~@$S&ckTJXu`k-ZkH>8+vNh_cDW?DT`mA_mrFw1<<^YW45rZ5xh<nDgDJF) zZqI1XU<z)jbAnsy?BJGqAfqdzD+4>Yz0S($&gjm-$mq%F$zTa?o%4fR=f>c6xiPq1 z9sq8O8-v^80pRwxGPt#^3~p_6GgdNIGH8R_+(L|?Hn%2Y4Py-h6S(Eg4Q_e+GBz?c zGH8R_-$LN_w<cp7V;h4IV+UgggAlka?h9^<b2IiZ_Aq#YTjX|(eT@AK+>8?#Cop(2 zPGp?Ozyxlcb2Cn1oXWriZlm)vPG_9Xzyxlob2H9loW;NdZm-LOTk3p_iy0R)$T2Qu zT*@F1ZmAo9Tk3}3*0~(GbuIyJohveKVcfzX2X3A7f!pPL;C8tmxLs}lZkJ0i9$-Ac zpa5=>%QGHhJjS31Zi~w@o@PA5zzlASi-X(ZT;R627~=)T3k>><7a1=zFf(3Zyuu*P zc$M)Q12edF&c%3x@g@T^<1NNp3|!zgx(v9Dt_N<TbAj9FV&FD9Gvfos2MoH54;ddZ zFoRp_GK^0cpE58rK4W~wpwIZ6@i_xCxV_E=Zm-*b+w07XZy4V&a4~*l{Kz2A_=)io z12eeoZUb(+GlSdidW_!~zcDa_+wRQZw!0Fz?JfXryDKq*+U^42w!0Fz?JfXryGue_ z?QBeJ3|8P)yABf<6BmOOxYe%1#LvXfpaX8Z`+(c--r!cdGPu>w25z-;f?MtE;8r^; zxYf=GZng7(TkUM%Ry!AyKa)QLGq~-}2X4EYgWK-vOrcDn49wtGyCJyME)H(5Gc(09 z#WFC1Tk48T2}}tL3gDJHAGoE?1#YQ(fLrS3OrVy!2e_qf&XmEF!NA9q$&|^!1#YkN zf!pg`;PyHnxV_E=Zm;u!+w11w_BtO^AyXj(7r4F73~sNhgWKyi;FdZwxTUVnRL4}u zzzlAwOMqMIV&Im#B2yDn6N5gurOpg)qcels=<47$x(&FEE)Q;_%Ya+w%;0u8Gt&g7 z2@K3k6PYG5@PXUtT;Mi3AJY`3DGXfTmbxFfr7nZFr7i((sf&SI>hj>0x(v9bt^jVS z>oP55TF9Wlw1{aDgD%q&rX>u_NNsl)q_(>Yxa}?hZo7*ytz}xvzzlA|%Yj?);!GQv zHZm}STky<GTbQ;mC@^hh+RC5{ZpSl&+wq3rcDyXO9d8J3$ICM9W!lSNh}4?r0=MR! z!L4~&aBJQf+?tmKx8^0lt$8tUYhID*1k(uyeWo)^XBe27&M}>1U<SAE`M~XaE^zyv z58S@z0=Mt^!0mf3aQj{n+`iWbx9<(W?R!0N``!TDzSje{?+w81dp)MROm`U+nC>y% zW6%Y+@|l?)F+E~n2DkOq!L57=a4TO7+{%{#xAMi9UNXI8aAJDJ^ooHC+~RivxA?ih zEq+IEi{Am<;&%kM_#MD4emQW9UmV=lXJ-1!^p$~+=^N8G1}>)WOy3#g!L5H8re93I z7`VWF06B0UKpfl$-~{&p*ui}OPH-Q99oz@t1or{h!F>Qua36pj+y~$U_W{_!eE?2y zAAlX)2jB$v0ocKP08VfpfSnoC2jB$v0ocKP08VfpfF0Zi-~{&p*ui}OPH-Q9omq@o zjDe9^f?0xrky(maih+??hFOMzky(yej)9R`fmwlpky(jZiGh(>g;|Ax5!?r0W!7TW zVqjzj^#oX%b(nP+7{UDkR%ShBJqAW_uYi@=fZ2e75!^RmWj10qVqgUK5LlT_m`xZM z!TkhQW;13p21amiffd|aU<CITSi!voMsRO|72I231oswL!Mz1WaBqPX+*@D-_ZE1V zotT{%*uZ@T9%dJ27X~(P&w&Trb6^Aa9C*Mz2R3lefd|}kU<3CYc)&deHgL~@2i$XD z1NR&x!9527aL+*!+;b2B_Z%d_JqH1B&%p@XbI@Qp&2pMSnN^%soIxDif8Yl9A9&gP z*!&r^*#g*t8PveN2qCr@wnPR&a9=_b+?U`7_aeC2YS`)-%-Gu4dKsA5Cb7+AU;_6d zxY?GlEn!e&TgtYSK^xqo5Mo;azJX*V+e!vaaKD0^Z4KLc1}1Rtf*ahs;05<9gxC(T z9cK_^yTEpxfr;$_+am^E&>ct&+~9r%FX$d524gl*k3xv;3)@!)L2!?PpPieXkAVr? zqp)KaWEWy!0^O9vpv*4KF3-Tk(Z<omAPw$Ch;q!}n91PJF^gjs0}HrE!NIYRV?6^4 z#|Dlq3>+L=Ikq#fa2)11!r;Yml;bi33&$0X`wSc$4>%q&ut0khk2#((uy8!*c*7tC z?p;`bdlwc+y$e-v??MIKyD$Nr6UxBIz`?+nzKnr~fidG5Qyv2oLn;I4hFAs$&<(Lt z;2UC1z&FHtgKvn<0*`q90^bn(n@OBWoN+0W0+RycGSJ<yjLSi{!ZNM^&wi`{-3rUN z0dy-Y<2lf+u%LMcmR6?Cpj%;?Z-VZFWxmI9lI0}xLzXivXP6&>ZiQuj47wGTMICf2 zEQ<!{R#=vF(5<j68La87nJihLTVYvpLAS!P6oYPsWhnvO3d_>W_L1!u>kZJH1M4Gp zZgz3jH|)~vDy%<2cf+zVgYJf9<6w_rFJ$8c-3rSl54shW%?WfXEL#x!6ZR);5$rG7 z->^lpzhnQv7R&yL{To{X=vG*^RM4%kY-yldVcF6_x5Bbzf^LOn%jOW}5M#>&-3rTA z0J;^HZ4!qMhd<jC(5<j+GdMyyLfK}4ZiQu=4Z0PUZ7%3mShjf_DIDo+3qZHRvMu2# z;izU?3c3}RZ7oMPCm-83(0#D%R-pS}+3i60!LmDXYH{kbJ98Rx8nSzG+Hrcbdx7qQ zWsd;e2g@D>x(}8;8gw5ldpzj&SM~(Z?XT>qpxa;Bvp~1Mvgd$qe`U`D-Tum+&$*s+ zJ$oVO_E+{I(Cx46#h}|?*-JpTzp|HtZhvJj<2=lHl)aqu8s|0kD$wn(?A4&#U)ftg zx4*Ktfo^|g?*QHY%HGL!gX=bX7uS8R`|N#OkGLMQ_k-?$WuM6Pp6fmPWY8V3>{CGZ zrm|1v>Eh{PpT;wRX9D|l_8D=9#5PI261yYEByJ$(C(|LhkKar7l^m0-ht!97FX;fO zCvr=qzQt{j=9AkXaZbX*dx?aX>^IpPvfrdv#plIckv<{&M{b^UlJq(04bt0W^y2fR zCrQtcz9P#OH!t2odY){ERF_16q+Z-11Qhxtc0rB_0%14t30Z*bi}!*+*$`<y2=rbe z9U!wn_8S6<>=L^cp9g``7El-fvKb;4pC=s<ZvlZ40n$q(0^$zIegk2CBk4C%uVlX= zW3fBZtAs3M^yHZ0^I(|YLi!VkE#)VD0*3kRq)*6n$o`RBBcms|Puc>8r8#6+B=_;V z2;JiMl1}0e;182NC&whcfj@>n2}HxN)CU<o{tW&+FkK>*CZiGW6`u#g{8jOMa!aJI z@Hg<cfoQ33vTPvC-y^&OOi$vU5w{_39tcbG#ap0b{&_N766a(?yq8D>fMQ5ePr?g= zrM>u<NS%@WCiF>Umu!f10RJlf4RMF~w|QwvAL8G`zen<#?6kN;B8#MrWK86kB)nug z;=LsI$-WYf1Eqkt4e?$g2GRjS8{#&2FOl+-u#obT4UydEy+n>fQcp%hcA8X_9EbER z>1QB5{}mYxnHRFtq|XT@@gL$p!GA73Pxh7M1^z2Cda`U%Q6gL9bOa(~LqNLX^Q59= zL*$qw*GL=5-jMnRN(ZvXgs1S|0)?>5EdB>^hvbaJHc1=F`pGc~Cd6BK&-0!qqbKVm z>j82j$W>CW;x@?40=bI+nP3k88v#B3Pl9!V2jo`q{{Z<}j!8y9phI?De4b!he2nZ3 z*&9+_vMb!@2)~HWlbIzwN%kB6KZyX5EmB<qEK*UjU9w#QT#}C@yhPT?&XL{|pC_Xs z&>^xymQ8M+w1sq%h?Ycyj2<X83kb+$34Z~FHw=TqU37zNh;)*yhx9f9F##FbkoY_y z7YPeFChvLid9pVo-w9+1C^<Y5&=AlQyCbwjz(l}G$U^FjfP;XWfR8|slpiQ$qyrot zNxOisK%77dSWkgKnLv#|lT?(1msFJW5~(hM4uL*_DFU+u7RYc3EE8BGunFWU89nK9 z0y|`8Nofh}6LJxLA#g<Cln|fb0f7tBZv?Ih-0@x_@CbwjUI@Gs_#*I2kU@}5Fh@E- zFh}-}AdmMOK_Nj2$$f$ef@y+sf+~Vqf(C+Sf;LiGf=+@Sf_{P_f>DzDKzRs+L1_t; zKZIPQbwC)TE8ZeLMtYU-9q)OvH$WJavO(z$gymL=tdp4~5dg!2IZ{!AMS>NAb&`(+ zTLilVCkRfHu#oDKGm>5<I7i};mzGqH;3B~lg6kyy32qVGCG|n@fZ#E?CDMl^^#sod zUJ|?^cn{<bbc{_LBqpIC_yOczY#1a5%FBXJ1YZe$6Z`;jH#&xh%NfOcA+X>#!9PMw zLL8u+D8wfuf`X-l6ok~kIT4f_17!3>rU~iD83|d)8Oa$rJQ6aJSpzDAge;_TKq5kR zGD<=&@fJc}5-~ynLSaHNlKW);fJ#)MB%utUJQ)qy5TO#GDxn7HB|>dNJ+f><8^pMT zkH`oJO%j<V8zMBrdx_9Ip(R4Aq*sCBK=_W(2I*DtG13Y`+l2PWbO;?1Iw8y=%q1)! zbWZ4s&@G_{LeGTWfI<)CbC7RAzCgzyc@PH0w$KmhC7_Z}_LcNGp?}h!Kp1Qj*i?{= zu$Zt6sB8o20AW~IgKPj{kS!n#D&0UBW}C2*um-4H71k3r5w?<fA?zUR2GTFCBCZ8e zkBmXNOxVYLj&P80g!GKKLsGA#<m8y*HVE$!z982i{6%DuG@o#s9FuU0aF+BW;R5L; zAe%tCWWRyJS<XmGNG3~qiJXzNj<k-Xo*a|(B<Us6CnQ!0m&v>nu94A`(U6-bJ1tH_ zxJkG}dXjLT@D$-$!n0&FWHe+JNC$vQP?0Ud3xpR4F9X9h!keV`NbeEeAw3BKh4%>` ziMNmrkcyIdCwxj~hb)8e1>tMLcZBZ<{{q8D@m?|la!eAdgkMNc5`HKAMS2NHT!cY{ zO@v28NJK(JPDDjSOT<8Chx8$75or+-GtmvwB_cK=P9h#6ej*_vQ6dQ<X(BlyMIsfl zY_i{E*+l9@wurPyUXY3s>5@JtGC^d8$TSHr&>g~(7i7OlC&`A0tPoiz9U#3#>XlTT z$QH>9G7F?$$t;k1B0U3Ce#u^vIwrD9jzi><%nO+pBKKq&L=K1?6FKAcN19JYPi~dS zCFuYVjOP=%VZTIfl_-<!CD}_N_heqk=!rZLc_lqh<b$MMe2mC9kw2nLq8y@pVku%N zq9USpqEey?qH3Z#qDG<?qIRM#qF$n2q5+~|VjZF}qDi6|qIqIUVoG9tq9vkLq79<w z#FWHliMEMZiS~$25}hFyCwfkFo<pDL60tJTRbow|8$`E>?h)N1dPwwy=sB@0(JP|2 zL?4Jg6MZB4N%V*4KQR_DE*U*B0Wq;S4cTeZlcWP=Z%D5KwNS)lq$i2>i7AO`h;@kR ziJ6F%iCKv`$nuD}iTQ~6h?R+zi3N#8h((CSiN%Sfh;@i%i4};IiPeZTiFL^B5$hA1 zA~s8Gf!H#!HFA5zHp$))+ab13?1<PYu?u3?K<x*yM`ABPHz$kz0(Fwad0=T@e1f=; zxP-VIC@qT{#9aa96Hr<gHxsu3(@x?Z;(p>G;!)xWpgaP~UE*otIpRg)6%q>Kbs#L> zBHkrFL3|phUJ;)oz6g}Z#8-%~6W=1fOZ<TNG35LKVoL{rd@el+l=8*Th+hKZ8{+ps ztqAca;;%q72!q(5)`a*6@o(aPB$y;PB=}%?QbI&R3dWXWlAb4_AfX0IDH1voMxb&U zgh9DV0@TK^lW+mivVY>{$%aS-$bOUdlH)*ypq8IRn8XQ*7^xhIB#9(RJp`1<kjRk8 zlPHm>lBfdbGf-ZGVu=QcHi;gINfI+8=1DA(6p)>U3ME#_egoa@EU`yok8GF3Az1ql zL`$5II45yM;+Dh%iDyz#s8HgK#3zX#(rvOKs8HgcB#R`Mq=2NDq>QAJqz1?rAeVyN z1+p1rF32R1F3AOG&_vQi(nr!t(m~Qq(nrz<qz4&G21!Op#!03~&XSxZnI%~u`2r1= zN!Ca<Nj6D#NcKrik(>p#8*GOhhvWiK2!gQWGK86uYh*ekH%Zk=?f|(BhC%Hp$s>}d zK<)%#$qS&8K`P37iT4u8Ym#>)A3^Mu4Uv2yy-o6q<U7ePlE0*Qq!^?aq}Zf*q_;^4 zNgGLTkT#MIkdly+lTwk=k}{Aold_R=lJfALC+!mVN6Js;o%A_r1*s5_E0HnG6)+m) z0%VLKo*<Pbbu6AwDo3hFszRzxss+@Rk(wYiO=^zRBB>Qp>!h|w?ed-{bwKKv)ETKu zavV}OWY);O0zo+@P+vhhz<Zw5J*g*BPvkb(-jVts^-A`Y)CZ|=Qh(wtq?r=E;tt6% zNpnc^NsCBJNh^TbadI5ehvH+T)ueT#jifE4?WB{WU8KFFy`%%A!=z)RlcbZROQe&e zGo<rmd8A9EtE3yG+oXGB*`&{b;Rcxn;2zEl>3Pyiq?bsqg7kZ&_sHl;ACf*HeNILX z)ccXX<#j;%f%G%!H`1S^e@Oq6VUgjI5s+mAm7NGIBL-p1fPj*W9vEtXNfQ|eTqA=H ztz;Zze8A96#s|`il8HlsK{9bN5i)TyDKaTCSuzDOHDFjKQzO$P(;+hp4EtoJ$jp*i zfE6wS+qp?*2N>>?IR%DCWKPL|`fUwz4RQ@K*JSR1>I)S72+VsY^92rn$uhtpn=AzK z$O_3y$Z=R2$;!#9$ZE+N$ePL8$ohd=_MjM*^^*;ejgn1}O_R-Wdm&pSTOnH~+alW~ zx60v>9Fyz>*=e$KWEaV<1GVI3*U4^?-6ea#;gR<|*<-S2WG~6yki94SB<@hWh3qTY z53=86|HNI9W0K>L<C7DSlaf=AQ<GDZ(~&chvyiisbCL6s3y=$wi;+u`%aF^HE0L@6 zo+Q^G*JgXi_KRGP+$6afa`WVt$gPswAh%6!kK7@-6AX-uF8@^+7#VfI<fQ+~42+CM z|D6~Z|Nr?P#=ywv1s353lR6-hp%Fwf)__S<Fj>vOz`)3O0Ze*;L>THoBqQU0(2SrM zh|O>fL^9@pZIJ_uGlE1IK7dHZMzG#(VA29CD+FRQy#H^@z{v0kL^9q7i7@8<cVS>; zEC!KG>|l{75Xr;`A{pL+NJfkQZVZf!F<`dkf6$zq%fI&wjEs_C5_E43BhyrnEE6Y~ z^aPQNpwm?tnTkMcCQ%T{sP<orfsx_se>>3HH;}8ClE9=2h-B;rt8oIc8NPu?#zv5< z7`;F?GTa2qy8UNkU}WU}&&|Ndcmyol2bTQ`4hMg*h&b4_(O~uVAXz3!u&zyDT_s?Z zVjz_a??AE)|G;+HfJ7KYz<Sq%*;yc2#so0i94!7GZ038gNIh6K25hnf*yIebxEfeI z4=ipD7WW2=#DZmw!RDL>v#Y@3tHEZLgUMJh$qABWxCxeB3l*^f%}@WgVqj#n0h7XD z@-SG|7Nnk$7p(p*SmY0wJPjrt!DJekOabds1(PoS13)ndA{kbJ$z~AAs0$`Lz@!P7 z%mI^XV6qKtP9T_U1B(QLNof$ta19)ipwlfG8FRsE^!_V=ZfXXzS-`TgVDd6Zgy9T` zWaI~tOw1sXu?8f|Bn4(K2eBE$!EE#Yb3pUoAU4BR5XqPUCi%f?<UnkO*C3Kn7EG#v z#2HqA*-ZccGcYo=gV;<Qpq$901d?Tx0Fg{zKqAb1V0HtDWZVf-$v6*8c7Rd~^G^_) z8SGl-ND!OxDwy;JtC#xU&cMj<;eRkVAAxe{hX2zU7#Rb=YBWKi&F~sb?gxo590HMy z&%mSsnAH7mz`)4R333S|*bNN-{tGcMGMa)yk6|xJ4a0g6$><5T@d}uI1;l1J4`MT% z2eZMcg<&n2{Q*pF{ol&K$gmZ}X6yv3d<|yv|6j?#$nX=yW_bNykb#lW9wf`i4Yu(X zh|K`@Bhwxbn`tvhCDR^|E+){4Ta1jZV38Kk4a7`lAU5MtkV?ip5Xo?gQJ#U3AqY&i zLr8{x21bT$VD>*SX$uxv$}oX}ks$;uG6zhq0h3)Il2ILGJ|k#P7O2!RW?*D^2DVEK zOtOMWSFnvgz~pxj$(Rim2i@Ml$k6rQ9JG?-e<=eaBWP<1BO_>U2_qxe7Yr}K>bt<C z>Hl5^M#dBnn*kg<41dAw-(d1G*gf(flHnehTn!=_SwSSjdoY;?BLDwo*vY`ia1F#} zI1C~gl|dxKcQE-MOx^;MSHa{~F!>flGHQWINf60!5lrp_le55N6vHwGMuv4DHlq=U zWT*j?=fGq)m^=w084W-r!xAw01x&60hxQRLdo!3F`hNi^j=<!8Fu4RwegTs!Kw-#m z6imJWli)PKXv@sOz{qeOWDX-YI8|>4u^HBa$@yS18B87m+j5d&CIcfw2UsKnO!k3D zMgyoUC{>&Vi7>nYi_8I&Az-o$OzsEE9tD#pz~oXeITuXM1gqJ=@S1^<VG)SUXbp-l zMr%-s#OMSjt^emTFfuy*&tYI>ECKn2Q4&msfmMcq)u@2UFtAD$Fc}6`1F4(B{#!6G zGOB<{T`;Nh-<g4tQ5Rh5>VQ@1f=L#znFV0e%)n#;SR9f^3c%{kKqSMe|DbhR%^;G| z3#^hGOnQNB=l-9{z{t=D7SZ`%3}zRA+Z7gIwhaR#;{mYjbud{2CQZR)4M;De>HkCq zM#dVjYfZtV2S}Eo4n#6~{7+|KWT^X}&%nsY2ohoV03sR1KqSL85XmU^zYvs?!R`?Q znZR)EzcB+NV-ASTsP^9!?4AGyMutCN^{2pOJD5}jhl3`F&F~3CGHQZE7~;U8u@b~) zoB}3ig2`GC$s_<KxxpkTyD~EF2C*4Icb_pbd4bqW&R{YaO!|OHNXtYX#Abw)lMBFX zaCyrJDQ`D|MNWZ8rUWnvZXqy%_Qo<Yf!)Tq7%XlACOtqT<2o?84@5FS+C|_r!vrq3 znGC_=x?s{DL^6B=k<6f7wv5c+dWv~ISY#oX1lRQ}ydX9UxUIzku4P!}f<-{*!7;MT z0kb8*q#Bsq1SY}lE0!5xb_bZ83?dozz%>-4t|<nKfJ<y<aNCbz6<CB1OhVdV`rz<i z0~Xl=CR@Q|JIEYHa2>}8t~Z&%?L&qm;L>0XnA{E~!J)xu4mL*;TytFmk&IQ~c8D3c zh2sTgbA#D(AU4BmFnbc%u52)A3??(dwO$RF1pAQ@;;)%t^N)em>;RMCwk6|FaLZ~Y zsAa{Z1ZHmou^CH2wlG?Nau#DQnC%E=ANcRUz{r>kHo*Z*=7UIPa6V-MhcIJ1xMf}f z5@)OdlaRFc0L)foI>o@qunHVLIbabhu-n=}B*P1k2*X1V$p~q;uz}bN;80~~1?yr0 zi-1!Xqdu4oZf`JvTlfs%x{M(V9JY<%5|s~Bjxn@?N(P2jP~KrG1nFX`2ayboAbT0X z^&kVJrDp;bQGv*UNXF}65}b+{)xm5nuo`gAVgT2tj1^#UaQbIt2Fpr<U8@Ts89Kls z_8^j>86?7}1|}82x;Vh>I4~QWk{L}wDj7||BFZ3=;TuQ|!zB=#(H-0?aR;;6|0gjp zGE4%?g3|*-8AyZ?v>%$0kqe}f;WLB;sb{zYA{h(7qy<Qv(E_aA0xSaBf6vHR24XXS zPAUVnkSiD%8UBMs{)5yrJO;Uq;W1d`F<9gQSmXhS&8P)(38NNR_8*93YzC1G;8q_4 zxJAwI0W1;<u5pYQ%NQ6LA??+?|0WEKOgq5oEf$>8GQgw~NIhc_SUd^LJ^&`8z#`zb zD5E9F4NQ*Ue4zy<b-<)DNSxsbSbY_UWPr5u4}#g?J_y4_5Swukm~;T^QUtS=L2QOc zV0Rt_llMU4|NnsU7X!EyW#j;fFgk%r8*uxI8I-~q!KEu>6IfglOqzj79Wbc}BAFm* z8`63|3KC(w$)v=<$oPUuih+?)1mto?aQVq_3Y?zTfyodM$!G*38BT)95RgiSIbe1- znEVKGEu%4rWatEw)nF2m2OXJCGcYo*V9a1(WCFL%n9@PEGk{}?5mHv$f<zd#K_o*L zlQ07#!y%9fjP)SjF@f?OIM0LDQ25A!)=>DkFo}U<Qk_AYL7%~h!JNUJ!JEOKA&4QI zVG6@khG`7b8D=nSVc5#BjbS^(4u%U17a5*0JZJdD@SEWe!(WDf4F4HH)h90_A0t1b z0HYwI5Ti1q3Zp8c8lwhd1Y;Cq3S%l`4r2jhH{%S(ZH(I)cQamR%4Ax?be`z~(<^38 zW-VrIW*uf-W<6$oW&>tJW+P@}W)o&pW;4((Pv(Wpi<lQPFJWHFyo`Ar^LplE%*UBe zFrQ>T!+e(c9P>lwN6e3zpD@2?{=od3`4977=6}rpSr}LtS(sROS@>9tSVCA*S!!7p zv;1Y1VO3yNVbx&OVKrg3V|8ZDW$kC(!n&1p8|!w~9jrT9cd_nf-NU+<^$hD-)^n`q zSue0&WWB_Cne__mRn`Zr4_P0vK4yKw`jqt<>vPsGtY2Bbv3_U$!TOW+7wc~}AvR$) z5jIgaF*b2F2{uVK3pPu(B(`L>6t+~hG`4iM47N<rN*}fwwpzA2wtBV(wnnxlwq~{# zwpP$89<~!~C)rN1on|}3c9!iN+j+JhY(Lq4fmZReJFzFTr?TH?f5QHb{XP2!4pt69 z4lxdS4tLPH4UQC!RE{Q&4vsF4ZjK&~g&d1P>ux}+`8iH<oZ>jmafRb5$2E@Y95*;_ za@^v$&2f+89mfZbj~t&k^*GmZ?&93Td7twk=QGaNT-Ug6a-U*kW{P3D!dzhA!}Noh zi`mUS&wieLjC~Ir&a;O?rWj@}rYp>D%mwy&$k@KeejW@n{jiS#Vfz>+4kkXPD<D&t zet=8>u^}vGH;`&(E)a<egUkT!^J3;=E&$nutPUa$69Ksh#)i?%1x!Df-Izq|dziwQ zq?i<#)R=UbjF>E#?3i4byqE%*xtPMhz5)3N8DC*`1DS`6LAE1fm>n=0>{6(&L2PC> z`xvGqrVOS$rV^$qrUs@Srb8hA!!T1D(>bOdrVUJ!m}Y>)nC3ApVOqts0ko@%X%EvO zrV~u(n65D0VtT;z%sz(Mjp+x|8&K@n_b?YQePa5-T)_0tK98A&nae)UK95;|S&UhR zS&3PLSp&2Z546$_imgB^>QJx)X#E_sgMAOP8?z5{5OV}`9CHeD7AS<7L1p_1##9DI z#;qWdVGAP<xPN+pfss)I%zn%Gm4T4~w8xy0VHKGD4@_<alP4GtFfcM41(Ri9@+634 z%mtaoxDQMo2a!y^VA2gthJi_c5Xra!L^8#LNp&!(0U{YsfypBvk|`QYMuEuyFqsS{ zBS9qNelU3wOcsO5YhZFYh-5g*;K;zp%nV{PJ_eIwAd;yO<U2+?FlhoJnGHcC<4rL6 z5kxXg0g;T#jB=osKZs<w10or1KqSKh5Xoo?Qp0c=OdbW3CqXtcgn-F1Fxd$X6E={& z3=E8^h}CWGDxi|t;|LX2w>dC5Fs=Zvaa##q<F*#O#%&{bjoW$f8n?^L2`sHlTfl4F zZh_ah-Df!oU)}Z$yt+*byt+-BHHI~YB@4W|Et?gzx-A#Hx~%}bx~&Ynx~-gbI@@cO zHt_1U+u+r0Pr<9(-h)@S{Q<9TV*{^l;|8y8;{mU3Qv$DUa|N$%3k9!kiw3W5ivh20 zOJE1BZc7HQZp#3#Zp#F(Zp-4}<=|t>;Sk^uV#@=sZYu<@ZYu__Zkq~T-8LOmO0vxc zuWp+QUfnhiyt-`xcy-%C@ane3;MHx*!K>R=fLFI|0IzP_30~c13trvk2wvUh3SQmj z243Cf176+c3trt84PM<A3trt82VUKl1YX^i3|`%q0bbpf3trup4_@6?2wvS*1YX@% z0$$x#3SQk-243A(4qn|>0bbo!30~b+1zz1&%XyFU8G9Y)3(lA9?VPVU->`Rbo!~mb z-VI*e*28s!>n?jQcy-%kF3{?>Dd5#@)3`vZ+h%}Qx6K5vZkxpeTHQ9A2ei6v4*OiI zI;mv}O7eLM0{k{2*Q6yB#8enm3>0LPYZR2M-pIePdZY3|{-0Hja*)a&86!~}aWR<; zg;P>%RBRL$c+Rn!p|DBCN#&KwD}@UR7ZjE$T(cHX=u;_Dc%<;iDoWv+!W7dlf-K@P z@*-kVAjt0|l%t{|?4_V2rUQZktHdloP+)`nHhvE=FA(JS6FMW3B31%|(oY0aRQ{+) zh$(;|e~3^Kf0XGj6&o=l5EK*>vjagvCH@5IZ(;!;$e$*3!&*SB3IwHJ2{S3o68a{` z#-Ag{Ag0D&q;N`Vlhh6nmiFVX0MYz)3Jc`7iR|NV;qQ{VBlU=X0{=7x0na&NI$}m* zOky1TbNCnWui#%Na!rgwY?9avX$ci4{w@5w<P!J~$VG{{D2S=N;y=cJhW`@(4KX_v z1BDCXYxwU;8z{&qTtLQBo5ZRVloYOkX#OW4mgo)9dtzZ?F=Ad~0sOBdc8MiP?^5}o z@CZapZ;{>w!t(zV9)W199IGh)5AsYPTKa?ZH~w$@f5aNZ+60&cIK)cCs+5Bit_k`G z1_`DJGYN#r#K<HG@Ck^3Xz_glQUVGBY63a}Mj)F&CR){r%o4B=|0Q52;3Bt3z{{#m zoJB!EAV5JtCPP}n>WzY!ih(#6I0fWby-^O5e*;PZ;$njPL~R83S-lb1AP^=HBakGJ zA&@6fqT;8pL*a;mp16iUm7I`3gYpIOHKJ_t8^o0a+N=cxepm}g_$gddc%)J!Z{fK@ z@QB<BfgXWL0y6~W2`mv<B`qPQBehKapMsdc1{DT@Z326wB?Jx$oKOz3suQ^*wM;=k zxk*7lZjse3fpY>^#Fi+RDc6A92y&Hzl2wl27m%x@B?Oyn1q5y>Hwiotc&74A;Ent_ zg;N5bJlBc$DZlWXqx{I~pTG}+e?mN-i<BRUFS8bq-=NT^Vk4v?E+b(h$RfYN^O*7` z<xPTIf&vPgBr24zDXb9`6WJ#yBT=OANadBFlFBPV4M9DTeS#)}R>~cM4uWpr@CJo> zgkT&f+@-Rt-iX;rm<eVH7Fg9OoU(djEdYuaF&)7&F(bhmxnqKN1e*jqM2iIb1g8kj zGHnuEAiqudj^HxEHG-Q2cL?qiJOa`qcuL`jn3UiJlPiMPKzgK>2|f~hA^1+h!%soN zL#0SqL1Kd77x6WMzl0c6v=pp_*o1iGd*mm{_b6Nt5|ZB_&mq1{xj=lGkc5z&$SmO) z1tk?5s~jN}aWQ!v5SFO(Tp{yDNK42-#ZNiTT0l8Y#YxCa$OeQJ#DtuLJcPOw)+ncl zdMF2}_{nn!`6<VVmkEUkMJb30B?zSn<p>oCRY-IR)d{r-bt$+BO%R$UG)HKW&<de- zqBi1vLR*A(2^|nRCUi#V2FO1`mxOKz-4l8u^h)f3&<CM!LVtvrggJ!yK>iRG5tahc z5_Q6A!aDMkMD8do5H=FF5Vn)Al7A!YBJ5=%B^)3erl2GoBb+3hA^uBTLpV>k1Qfo) zRl*IzZNfcZJV|(l@I2uqVp3uX!mETgfbllrJ;H~CPk?ehC`SvQ6TTvROZb8CGvPO2 z{7LwS@IMh2P)--&5)lC9WRQDB#6)C7lteVZSWm=6#7e{glygMfM0~(FNF+iePOeKN zMWqN7zam*81tMi4HDKH%(jn3(GDT#T$O3UOaT$?iB5Oo8iR=*B2g<n$v*fpl+)<ci zRVQ*p{+Y-rkqh$MM6Q8EJhzBEQZ7)@lF1NxA^%V0oyZpjF_B-Q3<?)S*+h91l;ni0 z-iQi`N{Gsd?@;+5s-o~n{-3Cpc!9E;sDTwIkEV!=iFe5J$%}~EfZ{;BPt;7*M!`zd zNz_BsPc%d!NHj_`K{QP?N3=+^LbOh_MYKzF0w}b^EX3?Uz5|saqSHj@h%ORcA^k^& z2}Fyo6Wt=ZOZ0%~G0`)kmsE;C<)O5ev;ioTK&6-H6VX?qA4I>2{sGB|@rj9mN;i<H zpfCr8E2u;T*#Igj!M1?PyCmsdVi{t2AR9qtm{^a*B}mCCHcxDc*eaO}c@eP<V%tD* zCbmcH5E!2jJ12HU?3Uadu?J$$lpl$`5&I<e!>Ud}$?Thgj8%@pBY6?Af8v|OS(Jms zxfBG%1ymSRUa4$RDFT(KpqNlNrBb9)B)&uLo97vYS>ihs+{Bf{HB>wl%EWglZ1UVC zuOqLca7|oKTu<ed@&zdYaT9SXg$v>i;%*?Qa82QwxR14fM3;DwgpEXnc!Wd+2#Uvv z$0@8)SflVrJViW3p-enQJWIR)R11oi$?KSY5w8(%k{1#05bqQ36W=GkPkf5Ji1;ke zbt)d>3&fYHyb@m{zDaxsSmu=Y5%E*v7sRiL-w}T#{zCkn_!seC5)2XyQdSad$|h0~ z5<C(-5<*6O5)x8PQdUwC5^@qM3XfE56dp-vNf=0&N!Um@Sqn&bNchPcNxYH>k%*E= zkVuoru@+FVQF&$cMxscfLgkObECnUy8hHzA0f{;VC5bw#H!4LkMy6jR&PcRKba}3m z*rGf|d5Xj(i3t+ZB<4sgl2{?JPT`uw0f{Zv0w5@{%X5Xq0f}Q0-z3gRoRPSsJVoJ} z#0`mi3R5JW$crdgNxYKyAo0!Ui^LyECP^kq4oN;q5lJbPIg$#JYLYsVMv@kic9M3I zE|Ol70g_>oF_KA=8IpODc~Ui!C6ZN=4N_u~ZIV5bla#+m&XBT_oF|nfRUo-Ua+Tx; z$!(H*q_U*4q}(JANgk3sA$d;nisUWH2MTd26I5CxpGm%v{3Q28@`vO<g=<nQQe093 z$`=&+tOcajC@cWAQxq;J^hwo7iAl*wDM@Ka=_wQ_f04>kHc=>$GLbS-Hj%QDa*%S9 zas%7pBUK<3Bo!eQCzS$nfmE4PjZ~9Vhg64DpVSnoSyBt6)<`XbVo-}>pTYw9JX9=o zMCz2(1*vPGbPU3vydd>L>YdaVsbA6z(rloXgfx$|5SW%waRQY;(sI%&pd1EjM}RP> z1tD!FZ3Ch~SlUV2L%Bv_nY5pD2&fGK!_raG3DRlOInqVa71DLmEz(`m6Qrj}&yijv zeF>BUq*q9<1Jh@uFM(PBpf-i{0qJAXXQVGl-;llsDxF|h`ib-_P#XhOwt-3l84ejf z84(#N83lzYGHNn9pwuE`A!8@wBI6|!AOmh~fbuK|t2oKz$&|=c$u!8c$wnZ-IN3O> zI;%QUJ((W)PcoBaX2{HtnTG_I$SjdrC9^?ho6I)ZCM4J)+aaqYvq$ET%n6eonR7B% zWNyhkKtQ<=xe%*cGS6h*$b6F1lGBp;A@fhJ1p#GQ<P1y&%rj)UOhjY_5KwM{+yqlS z`5&@kvNE!pkl+s49VS(>O6D0RJ+d0Ida@?6R<d7YzsNesI><@LNyxg%y2<*;`p5>! z2Fb0HTZiPs2-!H<IN2206xkZtEZG9tGT9p08YG`I$%6dSCp$%Umh1xAN3xG(m&vX{ zvU!v24%r>D`(%&Eo{~K!dqMV^>>b%hvM*%c$$nAsll>z5OZJx>gB%+u6y<p2B;<tT zB;-IqMShi>mYji{nVgNBlbnZ~hn$~Wh+LFhf?S$hj$Dykg<PFn9XLd~<R-{Xlba*A z$fQKRL2jMg3b}Q1TjaLL?UFkncTDb#+$Fgia`)t($i0&Lps-Btn{t-I1?4Q2KPn&O z{>U@QbEr6hQY5I3QQ0CdC9fc_Ca)uJq@W>hA#VqTF7goQB@clC@(>s%4}mfA5SSzn zff@1;n5Up5e?q=QzDmA9ex7oTd>gnuGedr!sg?W^`Bm~8<hRN1QQ0DYNdAQUIr%H{ zx8xtlKT|OPK?NDBH=urs{3rPzAePD>l>-V&3Q8&l3QF?-6j-coDR7xzvZ_-MP!LlN zQjk$lQqWK+Qz%o=Q!r7mQgBdkQ}6-v%M^kXA`~L51r*{GQotew3S|me3S|lf3LsXQ zLXAR`!Xt1$2h`W;0QGZJiWH_O%u<-8umIBgQCOpJO?isKCWQ;27O%o1g&hj}6z(V- zQ8?u}2PCHQO5vKq9fd~<FBIM>d{OwN$e_rk$fGEvD4{5)sG_K)XrOpZiBE}1i9?A` zi9^v$(Mi!s(MHipu}INF(N8f%F+?#+F+nj+F-NgTu|ly<u|=^<af0GBr8cED#W{+X z6c;J3P+X_DMRAwn0mWmAXB00f-cbCYcu(<(;uFOWimwzufZ;dAKX6<4lth$bl%$jt zl#G<rlysDglq{6&lw6d&lme8(l){u^l=764lrog^lp2&ul&X{(z_?ARM`?!AB&8Wj zGnD4}@hL4)+NQKhX@k->r9DcAlujs}Q@WybOX-2qGo?35pOk(m{ZnR9=28|=7E_i{ zR#Mhb)>Af7wo-Obc2o9I4pNR#j#Exi&QdN=ZUVLMLGh^EWc5b5L%C0Rii(HwEaex< z3zU~BuTkElyhC}P@)6}z$`_QcDc@0k0cy`HzfgXs{6+bfRh`Nf6$TYH6&@8K6$uqN z6%`dN6$2GB6&n>Ns~IXDDt;;<Dp4v4DrqV?Dmk7jREkt8z_3oGMWst+g32_NIVy`( zR;a8~IiRvdWtYkUm18PrR4%F9P`Rh_MCFyr2bFIse^i-NIaK*nK_^gXfyUJt?U*Nm zW|_fcJD8MYx(`}e#B9jG$f(cs7fgahn3ggyGTJenWng5KVS32G$e78zfq{|HkU5-z zkx>>bYsfqq%m&F$WuC^s$f(Kmgn^N9Dp*_-q>Et{$QFi1kSz?2AYBZrz_LbQ_cVjm zbb?G~=mC+8)l3qg)pTH4F|bG;*hW*3UdBA;$DoxqVAm>v%wc#9GJ!FV=?ViQqXY9k zuwOvrPO#n$O#c`d8Bc&s+X^PPfk=iXX3!crWe}TDnz@I8k<px4m4T6w2TZzwNQNd5 z$!G#n$?%Ek2LmIc7ASle-h<c-O-#2K7#U5#q&`$7$b?of+Yn?X!zvKTcm-_BeK2VX zc9jM53I;~TZm>uPh|R<aW^V+Ojv$hW4@5HRfNW&+0*8q^(>Df2Ms_ee223`BNm-^_ zp!H@TpE5Lp*o;fT;=92lCs_PHST7fIECVCMEv79DjEon-F6jixGF}9`vlFaW8Z3Sc zOx_2}Hh|eqV0IALRd!4*44_jwK`|8u7Lfzn^#(*T6@f&UL_usuHRe#z8bYQS42+DE zK;n$GOy3z887;tUCXks-Dqxk}Ad=x5h-7SJo()=i33js^SmhBg*$0;O2eZY&>}U|1 zNfJadZUTugyaSPpSzs~&EdCx$)`LY>gF>6J9Bi^TSmZQVwhGL)1ItQ+W$nOWA_)$Y zonSSFU|CtPYz$bW2&_g5tY$TsT@Gfef!GXR!R!Wb>U9FA-Ug63qZ3%X9n5ZLt_SBh zP+GDDu^GODWi7zEEI?`){zFAJfaXa-A;uUB5@$39i?0Q<TfuBdDiQ|!MHTGA!(j3d zD8(`!1+#g<vTwocKVb4SSi})brh!E~!0Z$-TNTW<V+OfUk{Psy#E$tQIG5>w$_Ex6 zFgqR02Bil{=3)?=`6F1|2gGI(2D9_PY|tE@By%CSBmtQz3pUdbR7x<)f=UTSL$LX> zpuPHxhG6q$nL*|ovVhE&1)0yV3gjM!MzG#?5SvjS$_BYjAFQ_>tXChbw;ilkAEcMD z1Du{sz#-WI)@uUR+W~e%2iVtnU~$k1VT_DzVD*7u^=)ABK(P8Yu=+r-o7=#;0>NP= z4N}AKAFM_iq=w-?SdA$-7L34T4KpZYWWc$5Dp;=xnB)PI8<^P`7#S~vOk?N*i<p5) zZm@cOkV}}rv%pMJVDaT3m5kwFwlIif*bGw1uoXly%7RH1u=oomP>HPuQqQmg?2-Zy z$<PjxW&Q;6Df3Z~`OHBek_qfKre1Iv+YD04+z3`#2@+v~mc7iNa><k#v@$u5MG{;- zgW}5$EG`Kaw_|<@P6MtCjEp<MX?p`WM{EL_#@G%P(FcpDfZV|F6J$H{Cy*@jQIKny zgFv=3P6f#_YJy}LuYzP5y}_g%SX>e;E(xxK48bC@AQ6UDAT~oInB54LZ3l@k>VrfW z+rc%LK3LowEN%`GVfX+R$ped+f>q{$#2HP&;(}m%i$SssP0aTg7#TN!T^Ils*8;Qc zKs6MjBq*#H+rgos4-SoXu(&={927G8%&y?vy_A8G0UXW@hrluQ3~bIbPzW$Q0GsRz zCJn%(ELc_=#Af&pQo|?&CV9Z5DJb3<_JY(ftOt>do?!9{Sk?wiwu4B9^B{4CwP1E5 zSl3oCdn?!t#$fh;@JhnBAT}dEh-CN)Ca;3@GPW~aWng5K2kB+B2Z=LsgGh#3Ad=Ap zB+hUZL^53msb|^)CPl#JZv%_BfY?m-AU5MvkSt>!h-CN=@*Sf(n6v_!#&DA96=;Pp zQzB@EDwtdbCRc;WE|4svI@p}~U|o<rk_|GAu?{R^3?dmmfld1XVl#??)oX&;tYEe) zR8|7CeiFoHv;oT|gV`HEB*RTG*##!=fk;LM5Xo>CL^9@pNmeikR>^1!X4^9IGB7f7 zfY=N#K_nyC1V&S)KG2F*kO%`L#{YuZzd<CU187YJ!$mOp8cZGolj}evBRhy>$ODs+ zVDc`Qj0Ta6>L8NgJDB_jCU1esb71lmnA{2`-+@R*JrK#T9Tc97k|2`d4%2f6MuyE` zauZl&9hmF|lTjd&(HulF)PTuz5E3+R$Z!M9PGMTbz{s!!%)ShYBZd`V_7Sj|Z@}!$ zU~&n=BL+r>%Z%9!j0`Kl>^ER?Gl*og1Fsl44_3JV6fz7u!R)nQ@+jEk`C#@EFgpXx z4guTJ2VyfCfJlb3VDb%Ed=8i$0#;uJcIQd3$WbtP2uz*;lS{$mOfb0tOfCYGhd_E6 zoxu6p37k$K<y8%+d|;FTmmZm5SwrRt;5IX;#4u!@1!lhh*M^|>vn;r5(E_Pt_yd-m z3N9Np!DVAHSiFh(AZYD6sNQF2V%`N}gKT721uA<P8o~8l2*_TBMo_uL&;v4op^>?j zfsyeT$OOjw;F6~X%r*tH+gU(6U-ZHCayttPs7=Bg#K6ed4tA|R6UepgVE5=Vfm~|~ zGM~{0>@QoeIX+->%)xAPu#F$UZk7e9WcUD9DGM^0;RD!YS&&MG4`5wlVD>ELNCrm6 zJdnQ_O~LJ%JW%b)XbP@9^S~}N1-md0WDcV#*c=^@8iqe$H9DYhV7v%2jj<DK@<p(S z3Rr|4ETRH7pB-$zG)SD`KUiD|?9SI<cPfE&G0Xy+-wEn#F#HFRjFQYkpi>CJ;jkK{ zhOr+^P6nMZ#K;4#wVOa9Orl^?08FxhNXFwJlF1iLg4?uA;C3uiJV-B7B#2~a0-FG7 z&8`QjWLy9y4}xSF4}r-IAQ2`=d1wMs&*T9XDFn;z1GB-k1Ct?`tqT%o@&}QOJWPKW z7#W&CB%?l<Gy#+5AafYanPV6jnZASa6muGw3;>fsU=q^fa09D!1F2-*2NvG}B3b;w zq#u|J2a~=alCc%6z7?$gBuIq$I*4Qem!d3gV74civ;>nbAd=A>oD0ms>Bk)6LU8=r zf$Lv8Q0ii41giv_z~}~6;|5k!0rEBDelU3wL^7v?R5CPy%w%W+naR+^%mogI`ydkJ zHilIolNnZlOlE}iyudNYXads3Xads3Xads3s1Np$3D_<ZkX}Y<kX}Y<u--Kw8yQ<c zHZrz?UDXO!c?~SS988`8lPf?Z^8}DehV7t|jbSI4JPIaPgUL-`5**HqlA!vFVHSvF ztODs}lmn3ruR$g-GJ(lEAQ8q%AU4B45Xs057Rd&)&6%^n{oD?)ud~2p2dEDV7B^vr zh=cn0CZN2)m<iT<6HHD8n=l#72FEy~DVWU(7XJxm&jho#fyq*^hy|FP3uZflU48(} zP6m?>U@{*}>VZgRaGRFl8;H$l3nu$OBom}<EDK^YJ_eKGpjv<toWdDafkYU3KqO-g zh-7#HwpR~qqdJJq@D)rx1d)tVV3G|?GJ(`EEC$v13?YnB42%pHAZ&2Rhk(m=a4(yw z5Tu?7%x1I$u^D|pB*PyN$!G#Hhfx?z-Uo|FfJsd-=>js7u@vlPX0V7Ph-7F4t2YI) z88tv^7(Rg6Tuh+-0vw>yh7oj*G9x1g=v*^K4yL!DmIJdWh-5kmCLt<AL2PjUl!KX> zfsru-Y(gB^1Pi9m42+D*U=x%<;tU@_;tU_bExL~&afXi|lNmlTvx3%2fy`(42r{4H zBUt?#5XtBWAwjwr9YMMn9hpG8DjdP)>wrWUCV@?!1X9B=2_gbEc@o&<Ng$IM*+J$o zOahxf3Bm@snV}4%o>34iQU*4s49va*A{h(7qy<Qv(E_Z}0xAOXuMJq-24pXz4ai1D z8?aqAV3jr?yBO`5Y(YEnm_fZ!8<35R5c9u*&9MQI4F5sqF#HGUV)zdhc?=eL3>J9| z5@FN<kqr02<O8ty0}z{08%%0}-Jk__gBI8gS|BxyT3|P5LDhibOAF)%hJRp@W(Wzg zi?JE3z8P#nGspynpCDO=w-6GfhT$zp4Fl+$Z$^f<U^O4WvLC>*AHcF7z+oQ3bd!OR z=?`cXG{ap`?qE~^laTemc_22^4iL#03nnwbB&5y)kK`~Wfklpi$tW-hE~}X!BL-UF z6f4dQx{Trpm|X)R84iP09t5*}K<S)eC5U9y0g(*v!QxFIl5r83bO4j3U{V=GGCTt3 zzJp-$9>_F?PB3`{B+JOb^c%EG2+Xzt=bgLY{AI@s8fQ=gsbmZRlVDwpG9Z-<O(5GD zbwF%Jb1(@Wabma(5@$FHA{lRjZZcwg!N3G68$oIqLKrhZB`3H{I0+_ifXO*vGKEna zlxsk3W=3PMYdgWM%4!hF*Z_8e9Rq0TaRb<e(jbz-1Y{<YDq{cxBjYc|NCrlxcOaEa zy&#gI5!@y*2eaRRNJf1S$#C=kZw5w&qhPzh`wN)lnIsVF@ifnY*5hgEu^KTjG2CWA zJ#DU?!GxicVIqSS!(@gT49*O*8Rj#1g6}%<1E296!mycP2SX&oZif8~@eBtUE;6Jr zTw!?5P|EO%;T1zC!yAS-3|$QG7~U~-Gkjq9#L&a=mEkKxKf`y19}E*1elapKOk!kV z<YJh`$jd0iu#i!dQH^0aqXwfr!%jvIMo)%|;PdS+GlnxpGhAVeV~l6G$(Y2L&TyMC zlQE0oAtUIPqsQPII-Y=UIeH4do8viS8Dlxa3&uLedWKhwEsQM;Z@}mKzGa-uIGN!c z_`KftjI$YMGkjp2&$x);Bluk2uizUrzA>(2T*vU8aVz6?h98VO8Fw@M2A{wC4}9A0 zf6xiLj0~VNbQu{LZ!+FuWCER=%g6#cF_)1QbPg^f8{=EXcZ}?e9~eI{a)M5}W#nS~ z$;80O4L+Yw7`$Flgvp1=hfxA_)-0nW(`2S8j8dSJQyFELmM|@0lx14Uw31PdX$R9D zMtP?FOa~ZMK<mjF)j;QiGHQTM24&O)oe9dQ1v=N0QJd*6vnrzwvpTaGV<@vDvkPM? zvp;hPV<t1`CZT-Ls#nHB=6>dW#v<lb%xf5nnb$GzWh`Sp#C(#mjrlzD1;#$+%glEf zCotb<zRx(D`62Tg#yQOInLjYDVE)Sdm2nmGcNP}L)ht{re2hC;L|8-^_pykvh%@eI zkzr9_JjkNKqQQ8KMUO>~@i>bSixJ}q7E=~e#*-{IEH;d%Sln3L7*Df=u!Jz4VF_ai zV?4_e$r8<YjwO{PmGL4=HcK|+C6-#2TE@#P^(>8yS6EtDS{bjiEM{5Cc$4KA%Q40~ zEN58GFy3Xk#d4eR9?L_Phl~$cp0PY*e8}>G<u&6YmcK0j7@x6nvhpy#VC7@wV|>FZ z!79P{mQ{*XgYg}!7OMf{UshvQQzjNxOIAxJc2;Xv8zv4`J61a;E><U2cP4IDPgXA` zK~`VZU?ySKFxD_8DbQL_CTZ4W)?_9b)=bt+CRx@T)&eFu))LkdCS}$R)($2W)*jX# zCRNrx)(K2%tczF|F=?_cV_nXq#k!jH29pl!EjAmbD9{=EOnccv*dm#ZvL&%)GM#2C zVryl(3_4Mt={aa^Jkx8?Y57cV*+BO;ePw&j_L}K0+c&mf%uJxu@0r;_cQr9{gVxG3 z3$Qz|J2MM`)|WDiu_v)7GmEpQvZpgkvfpIC$t=TupZz|wENFcxvmE;?_BYJ(p!KE9 zN}%<n%&MUErOaBO^`*?(p!KE9I-vEX%z7LG9D>XS9HJaz%tjp2916@P9EKc*%vKzh z9G1-1ptYvVHXObj{>*luwWiFDptYvV&Y-oX%q|=;95KvppjD>K?x6Lh%-$Rg9Ied3 z9Niq#nIk!7aV%s`<5<kGm^qJQDaTgke2(oL`<NRz4se`i?%+7faglin$7PP&%riOe zaol5G#PNXR0rO&xM;vdMmw?X8WM0qlfs>DU18AKp^Lfw;Rp!f_x}18<S2+DR{g|(D zR&iD_Uk9x|Wxl~VgL4M+Ezs&y=G&mvr_6Ua*K@9Cz6V-y%6uQR;*|LTXvHb>L(p1N z=0}|OIiE2<=6uchni;e=;2QHA(CLZHZ$axznLmQomok6idd2mM`3u*3uJ_DeK`Tp{ zzk$}2GJj|OA+(39gxf)=jXQ{Q5$6i7D}vv6>Udg&ba;w{!h~J8^@Lq`MY!F#gSaDj zr8w7dZsB^yd4PKp=Nz61LNmB`2#at};h84HA;iHwOXwVThfs!a7<Uc#6z&Dw%Y;@5 zWpFofp5ffZd5QCw&>jTjcHj<zK*4Vi2(nAq1p>JvAdu@B_a+47nZUgR0zq~Nhe4oF z1_W|m;=ClZhkFwYb35=%K*mCCLNmC7xObpq&K2BKkTKU4o@o#l34RmefP@A2EFm4C zb0|1WC_@N}**^)p2#3KiuL$=780L24UWS5$gjR6}39S-Z#T_A(femx6<2=LljPnvG zj5&{S9)Mtw8A5y5f3W}KVBtE)(*=&PAkG6^SA<;zzwxxdVv0M6>lx=3!Eap8xYuxS zaR>;9@vIXL;|}6>1Nn%j4x*2z2&9i&4-^NSTevrI?*PRZ&jij(oV&Ppa4!SJCFdoO zOJEq}BF+|`BJK`Q=z>GgfoB5OGe}5*#6TD%4hjWOOmJQjngJ3c1%veC!yrEhae!>V zg<&BIaxV;n!kcpucLc~Kg5N+oVXBa6kSqv;QUXs4_biY)5C-wV@eEB_F!zDn0+I(| zcx*%C6qJHE4}eSqsRpS)#vpeI?E$4sAstYj0;vI+021d=;t=DW!XYEX!J)+SheJas zgXa$j3cGOVahUK#aaeJ7aX4_earkfqaYXP;;E3ayAjHA#!0jOH!aW5PTO27ISsVo% zWgIm;6F857cN=wZJmR>+(Z?}`V;08(j%6HcI5u(Y;Mm7;gyR&)1&(VRcQ_t#yx@4p zsm1Yy;}<6bX9Xu4Cl99(rv#@QXA7qarxvFHrx~XWrxSM^rw7j&PCw2N&M3|Vp$yJ6 z?i$WHLNh=m0QW5J8qOTfBF+lVI-V%b7S1lt37pfoN;v1hFxba!LNh@55E+B~kB&JP z35$S29u$)37@8|vxMzXl34~GdrH;@!P&x%+aJ)hDD2R<53m`VP9(N50gF+t}gK`@N zhUQ#QY64?uTqCC;UMcP-5C+8q7;oX+#kqxZ7bs?7IT=LrOcPqgd4}^6sH_s&!<EN* zgQtb_9@inRL!3`IpK!k7{J{B*^Bd<ME+#GxE<P?1E-8U|T-&%5xYT&1xOBLTxQw_g zcv>K(AB5zZ!4<=0$K}H1#TCF6#udYr#FYV(0hPC0d0Zu2Ra^~RZCpKElelJZ&Es0a zwTf#M*9NX_AbAMp+QW5->jc+1Q0XuDjq4WI1FmOW&$!-jed7AT^^co{n~Ph3TZ~(V zyMud^&?=s3pm66_;tt~0;AsItAstZ3&8^36!fgd&@k;T06AI&Y;As&G<96fr0i|N0 zJ={Tp-?$@$bcDjV<G53}v$zYmXMv#5IqouWoiT;GNoWS777@ze?&F@qJxeG9RFiP8 z0hLoi=Y%qZID|5|_i-QLK7|!t;J$_g?{MGYeuM?S;C=^(U%0;rt6@U!U);ZVKo{V! z@xUMtj}RQn@t{Kq9yuNrDAeLXKm#5KG~>YrZFrpE(1XVh4nue#Fp4LECyghK=MPT~ zPm!<-PX$jM&mW;ZJS{w3JQH}P35)Q|;aSA9f@dAi7M@)^2Y8P0oZ-2|bA#s|&l8?k zJRf+z@%-Us;^pAw6XM_%;g#Z5;8kN_WU2u5>6pNyQp}Kk;vP`niV-}L!^8~kONxNh zFa(3oR{@`p!yxq^bT$hsNQ5y4)U#vw59;eNegczrAd;aOB+e)cVl#S!NT!3J9wn2> z|33_jOrTpX8JX-D#26SEt}yX2Ffuv(2lZ70!M(XzVA2Ilo&l3rK_tU@Fu4RoGPZ(9 zhPNP+3B1Ra@ga!K2${8c4`$bZ$#yUq117<<JWK&#whzcvjQ(JErh~~Out)@m&14T| zmw`#hJkBkqRSb-brHncZj0_6k{kgVa*(~sUhZmR~2_hN(gZjdZwqP;~G<U=31!hNr zNCq2FpO;Y^tnxcpZzh;+#xRqCkuev{<^`KC0b(=ygVZoFgF=t-GbnbLz$-wQA>)CN z*>Lde7?TD_mT?^@zL+3mp0B{{5|B7k1egSmWHNzQJ1~6*i-6ZZFw210%;1$0%;52S z<|SZ}Enspjh-3t_8P|c>OyDt3Ch({N6L{SVGkAW189buPJQJjbc@~Id0*^^DTZ7ol z;BkIt@VGQn4oHNl3`~Mor!av>SD3)-E|}6m;>=(lF@xtIn87Pgn8D*S%wRV#9|Wt} z4kDT2!6bMs3Nv_glo>oPzyx*`<6@93BiJQOVAGg3gG88igGun*D6<EM%?z1w@BzgX zV-lDw1C!n$k^y}301J3Tm&p^Po(b$nX7Kne6L^&flORZ(8M1Oo0>owl&v!7)29K%E z2E{2ecwT^cA~<%yGX+fGk$*<;Xe~2j<Oe)o%%lxgBM6TDP7s@MJBVb`1?5df@Ju(8 z0$8LQG;7SP2_hN)fJVR=!DD5N??7xOBM`~B8ca?AkxbxGJ|<@no8dZ$WPAo98D4`( zrZZqN8bmUJWtmdI>{noNBbZzRBAFaOB;#`s$pjuFWxNYwGadz#r$HpcY{<wP!$Sr} z1`$x0FbIK2=5i3ppaF6}!(R}a3A}QHX)lP)cm^cSBo6XB6L^N5Q3k|j<O7jRkhMKA zps^#y2oTB83?iBAK{3Uc3npv8q%4SJxCI`Y+6l_t4B)vW27b^;6a)Cg6$Z$uO5inY z%-|UcW^k%z0*~@Bxq#hM2`V?3m6#;Jw|s-|G+?{LRL8)`A;iGLAjF`?V8P(UaEaj? zLkvS6Lk+_;hD8ip7!EL8VtB&vjgf=ViLr%&iGd9?F2!&SR0=SG&RBtpFp7ai<o-*- z)G)e$M3_P61whp>Ku(<b$iTqB%pk@f13D3a8FU&6GXn<$AA<-3BSQ;F4<q>G31-kK zFH8(V44@S>pi}5zrc4K`xxv5ywh44U$aW|jw1NqIvIsNi><6fNM)0XE%%Ia#pdt)k zK_)OEnaKz_O9pgK13QBN*lkJ-8Vq_2CJc;BTNyy}Df>VqlR4ug(7G28o6!cuW-|X@ z3mS|5U(3M3AjBZSAjhD>pv7RoV8&p>!1!<Pf6zJ^I}rKLfMGoYBLnll>kN#H=Rxd$ zdztt_E2%&v12a=0*gv3M&sUfv85o&D7#I*S#q|Uf?%Z4q3ZUC4Sku@)a_Dk|a2jyh za^`auf$er;P+<sRFk^6H2w_Nro3$0R6PRfp=mueE+(GsmH$z2)7#NwiF{Z-QGq!{F z1~czP643{naEy_M0ZZ60sxvS$>;sWZ=1icKr#nGxMjH^D$(%t36gCVpNdDLZ@&`{0 zC}bIgSku_=vp;74$PvQP#?j8v!O_Xl#c9B4%xTM+3i20EGS5VK%G|+lharX`i6M!h zhM|LD7Q-@z9So-!?l8P#)L_hlxu0PcxCD94^aCcsC<aRXOmd8m;Uab*5#~guQw&Vt zbcaZPe?a9DQwWG;He~wC084+jz-(~ZXOsrXGTwlSfYKjmB`P#+G0KC@t_7P7Q4d*> z1u7GuY8W8(+H)oexQq0`WyLWj39Mm!6dcA!85o(&na+dKALCI_xPsVB<_w^912zo1 zz-a(I{q11@t+cZRk^l4=n-~}wnEr#>0p~#Me|wl7F)%XPf=C7?W*3-i7%qZJ7ADXP zF8ItgP(Bs51I3&0JO<FIQwj{98NM)OqMVbloOuQFO2`QrtZCpl)kTg~j{UH-3XW3+ za4Jn;I0i0Vau_NYx}fC^L<CgcOkk8^T*RouXvFBi7{ZvqSj5=EIDv5y;}*tuOa?Hw zGTMW}n+dd53lzFG42%r#m>x1PF?xW?4HnRfCukfpgVt1n)PPC}NbUU#q>^D8NF^g^ z{Q^woS48YFJpq}|{0yuT9G^@^ptxd=0E>W2Cl=7kBdEzt;Mo<H4n*8CX@bgo7SJkM zs2WCpP#R@!K%`M7c91PBpq1TFH4LXfx){%Z)mSmeFfcQiFo=O-f_Wa(Lk7@Y?VvJ~ zk?9I3H8NL$)q!2Z<OX&b*sY8;AXz3Quq;G`A5^0;{$jELhoBGxBP*zt0=1P1JpaoI znxTM+fY*RAU&EF%nIpmJFbPC5-3F(|SP+}>0hoOooH8F^PnrMuKxvY(A4L9Z1f@oX z5KziwQUJ04^MTSNV?T&w2ti7jilCIq?9L>?z{ViJz{tP_D#;mm7^W~VGWsyRVPIkq zW6}VfEyg{KdnN-T_Z;p`46NKcxOX$ia3A76!l1x?g8L+cGWR*|iwvsVSGcb-XmVfY zzRsY{eT(}RgAVsy?z;@S+z+`QGU#!?=6=nf&;5b>1A_tgckVw7hTQ+T|1+5L2=E9p zSnwG0STb1g*znjeIP!S%crrMF`aBHIJoP;F;2ZDdXz30f76x-3HXdFETOJ`EH3nB6 z4IWK~C>|XiU4|GQeI7%GIIznS!EQ?83FV1lNaji4Nnpt4N#n_7$l=N7DP}0)Ddj0= zsN|{SX<(?~Y2s;TXkl<=U|^iZz`&>n#UM5aYe2<8Y#0_}U|^JCU|>{YU|@`6U|<BP zLDvW3!_;G=LGmDdAUO~lgh6J3_%I9-1F?M=7#Km=0qPczIuj_{ih+R<Chi7R2hsz> zK@1Fx5m0xgFfcG?LG^*;3m6zc=U9PGAOqtj1_n@@fw7N)0d!Ix<17Zo|I5MY415OZ zKXBWJ0dlq&_`I_JY+zaNId2SL_J8o%mH)w~TKxx~jrkvZV%UH1=~(~&gYq#00|V$> zO@{xVdrukugYH0O_<w<ck%8g=2?oZ0V7&~GQ!&9O?12u^WawfP1Km2x7{M69*vF{E zsKltjsK;o+XvOHj=*H;7xPvi>aSdYx;{wJw#uUaZ#sbDN#u~;Z#tz0l#wm=m7#A=u zV_d^{ig6R;4#s_qM;MPVK4Lt@c#81?<2A-RjE@*!Fur5_!uShx6FU<d6AzORlOK}= zlN{47CKV<vCMPBXCNrijOuLx2Fs%dK)yFh}$p%dC0+SQKa!yQjV4eq<^aGPEU=pM% zgei(CfoTHME~aTrX>idTI18j^7gG^a1ydc<Iwp|5E~Z^z^&pvPV7uoqEn?cmw1R0J z*gnv?FHE2l#Tc0Xn}BOL(5bFW|7E~osRfEX(5bErks$IP_|!%QrvHHqj0`OQWx;Ax zz`E2xt+W4@U^eKiM<&oYb_`75lf9V0XTmZ6&jrh-fmJGjO#q#@%J3he7i1&ze}AwV zkSr6_J*r^!pz@08Kd1y__zz)&Nap`iVD<S7j11!cK`J?*vI-20|Ns66_1768BLBXF zRZ1~1{s*5R`~Tm6aj*zzEt?=y4g({DDwt#fk^fgPX@E*dCLRXH|8t;hB?iX-yFelg z9H8^V7&t&G8LUC%e+F=fDS}RA`+pFu#up^c5C$e?z#=kWwh)+P1B-Bj**qZf|DAuY z7#ROQ1F;z-!6Y-7<OGvkV3L<92VQoARvPet+K3EB3<V5~3~bz+xwmj{<=)1<od<MY zHYX1o4-bzFPcBa(PZ>`YPaRJ)IDbhoFff45!vnSTL8qjF#&tk3#s<2%mw}NH6dMeT zptxmZ1jPfWd}93npW!wGCxZeQiZDolX%LT*A&d!h4>32G<YKxGcOMf2BLfe^1qMdO zHEb#jObiN)!Hgjc3@is(4l*#Yy<vO9z|8iJ?E?b~+i$kN4D9S2>|6|7>=Nu!47}{B z>}m`G>|yL-3_|R!>>UikT=%%{F^F?Fa!+B9;NHW1kU@w0DEDy&Bkt4OXBkYnFL7UD zumFwuF<5fn;J(RV#eJLm4ucK%J?;k#cHED+Uokjxzu|t*;Klus`x}EF_h0UR3?V!W zJp2q{JVHE%3~4-8Jk|_tJa#-D3>`e)JiZK*dHi{j7^d;0@zgP_;%VS%XE@Bj#GnFp zJp<c2ws#DSV0SZt-Ob7_#V*Cb0d_Yh*sa`PxAK79ssMJY8rZFxV7Kak-Kq<As~*^` z24J@ufyOBrOu3(NzhW>0yVx4+VtcTQU3nOISQy-S*m(FEe0YR-G#SFcwN@Uu)+zw| zp&0CkQm`Kyc>H;S7@EN~SUb1|>j&3h6L|7@${A*W>#l|1x@#rFX$A&nJq8A59|i_y z69xumD+UH;2PhvT<_2YFF)%O(F)%PkFfcI3F)%QvFfcHK#0nS~n9CR#m}?jqn41_F zSc*_Fa|Z(hO9DiUxsQQ?c?tsq^DG7imK=x}OB#gDynulL#AaRw6)$37U|xfycM}5x z^9u$B<{b!mkh!29D)SMj7|0$F2I;*6)eGW-FvwjXyFfHZ{uD$H^Bo2T5N0uh@G;zf z0U{1olf%Hkd=27Wkb009^CO76;rc*!fb0Wds9kV5BKcr{zJu6Hs$Qz9Ph((U{=&e( zBE-PJ{EGp+%!Y-Hfq{hwA_fv?kzin8kz-(BQDI<U(SnA#0Rsbz2Ll5rj96?K7+9R3 zd_M*T7Enn*C>%gx2f|U%FaWVZ;Yvs!E_qm5!zE9=I3pu?x8A?opxz7vc&{cy9f-{+ z3nmr8BzW|k5j;Z2s0S7Stx{rS1dSpwGJ;3<7`Z^=44@tl<NyDRpw+0W7!4U1|AAMU z{sXUrWH1A%`3D{mV_;;eVPO0R9v}Swhj9X^mIISVKx!DkJ1hRJV@d#tgGBy;cX#{; zj|elcfJ7J=K<XJ7!6XZqYz6mf6u~4nn3M*S>R?h0OiF@DRWK<BBLDwke8#}|58NC7 z2i|S?54`T}-&2q+|Nk-V0ksa8SQtQchdqb{i8FxLD>H!C=`w(JiL7T#U`%7oVXR=R zV=MxbEsR}^6Bws4&S6}{xPoyV;}*tUj0YHxF`i+(#CU`89^(_nCycKcKQMk{PGS7R z!~{O6P=pEjoWKkwJEm>0(*l`5X9dQ=&-5!{s$z~~YG7((>H(eMhje})==475`F+es zm~OGKG5uiP!NSJE#@qpdpcDN-eC97KY%B~+Z<s!T<u-vxrXS49K%&fjO#fKenEP1R zm|0lZm^U$Vfn-_Okk0wjV>V&70-xlEeu`fia}9G7^Csq5%pD+IAX8Y_n5Tf)Fw8uQ zc>(h>=4H%lm^U%+VBW`kg!vTn1?FqacbFeBzW}LXeg|?N^DmHVSQuE?7#RQE1?|9N zXaKL*RsoaZVA2XqYBR?$Ff#Ii*JxXSNQO3$I8!pCFF4Mwf@AR^I7R+tVEp$1)F);z zWBLbbJAv8Y-Kz|YOrY^y7SR5He`lCLt#U@D<>3CN7g*&FaH<9CV)g-xJOP`03TzAL ztYRi+c91wT=$u_9<^v3j|Br!fu?Le(OuxXgEMRgDn2Z6Fp!NzAGiW@Q8Dav{5>Shg z=>b?4)ShGj?b~2r0NtDX?+w!@u(&eVUXXhjKsz}YSU}{zFA!gV)c<<}TIbHd0NNqJ zzy@{$Gc)K^ekSlvXeMURIq1yHHDJ@6L2VsoCk93a31*Pn_?YK0Ff!<YNd_iR>kP7{ zn-#1^5KM}KNp=wV{~xn61LObWV7=BL5e8;vaGMNdBcmp04J<=DXnirmYtSlJMn};4 zR>mYSTMNwA1jPlTG*mrkm$p39evsQh>+b*m1C5aUdk%8>Kkz!re;=6cGBEyo2J+*- z=OB`Siy3sM0SDN2UogoBCd0s_9Ekk?2kb&_5SxJu>^m0F&Hx5BkjVdCU|BBEZj%3J zplncRD1ucAfYdPX{|EIrBth)||Cmb{7#TQ0Yz8i7CwR+|85EkJu?>bChH0RF0{2wz zS=_U^FEcPQXz(!dF!8YPu<@|-aPn~RaP#o+@bbv;$nnVYDDtR-`tm#xJlQ<CJo!8Y zJcT?(Jf%EkJmox<JXJh3JheP^JoP*cJWV{!JS{w}JZ(Jf;OY!?&kX3?Y0#Y<Dh!PO zXMn<sL5r!2f${%)rc4G#2GGol215`-1VbFd4u&j-YYZt2WehtQu7P<?438KJ7|Iy> z8168XG1M?LF*GrBF!V7@VVK3RfMFTK8iq|^mHQZuFq~qzz;KP>4#Ojc7YvLH8Q^d# z2ZvAwnA`*ok9IJ55R@($ioooPU@{j>R)TdkfZ0`GvH&b%4GMdP1~6M2OdbQvwt~qJ zaM;Iz*(qQ$02~7~V0H<ZtN@cqV3L7>i}4b(76Xfmn@<RXRB~le4ue~IQEE0rLr!8z z9>W@N*fIS7&maiy!!t3kFt9UlFmN&mFbFaTF$gn=Fo-gUF^DrrFi3({69)%5DKHo@ zAnRboqXVRlk%1q>7B)P3lo(i@9YYitynG!!6c|c;{e2V|=J`2>DKMM}^7mB$opq1o zUS>Qdfoun*3q}Se2ADc7tUkl0lACCiJVdMHC0ZpPR+UnTi8&<<^OB8<lNpvJCl;qN ztVvGJEnwILCJ%tgQ(*F1N?v|0!=tpK#AJpSIeDeI44-oHlXDn;fXRO#l92^Wa)C(! zFe#Q-np?yulb=_b%czuJl#<7&Q2=7=6_w=VGMW_UCKoVT6_+LzGddKP78EnOm4IY@ zs#1&c8A10kK}Llb7#JDA=YlhUPGn~VhdvtvGq~@{4CR3up`ch{W8h#wu)$}#Gk`{t zKr>{ZR0lE(e76n*`2GS09&kE^jC+Dl=4XJMEe}b*ka<8*-<Xj>8mtmj9xyUM&Zq~U zj?Mr{5yD_u$jFi^gBpV$LlQ#+!z|FMZN>u}A)G(Bgt%O|T)5J>KsQ-`;}+vK<M!g- z!lS~R6?!ehi!z*E4}!E=G<6E7EU0Ph6eYXSj6=Y;iyL3Ieo)u8)I*jToLYEYJw z4B&Iy!Q)<_Q4bCVX7-irtJqhAWEr`^`2##7DRcrfzXd8WxtJN4c#Obd#>Bt{9sviL z`h$Uy0W|&s3NwhA&ls2(L>NLqbuj}dthtzwM}TK@?`B{HkNv7~pWr^lpa~xP)#1L( zeU-t0`#Se624n6!+;<tw!DFSC3``6HkRCF)_sa<G{fdHnztZ5|uN=7ds|fD>DskWD zzRjQv?y0KqWb<S*sPh!?6ftOk%4-H~o*JGS23?*yo;n6So(7%<27R7po;C*1=oH8& z91KhhY77D#S{ynY+8jz8${aQv#;oZaCLE?5W*p`m795rwRvgw0j0^${J`BDL9~s{< zeqh#Mkz_Gt$zrKzxy|aw+Qd46bqecsMEJ4AF^Do4Fjz6TF!(WKG1M@$F-%~X#juUx z1mi8HH_ZQ71=y6>blA+;9N4_rKw-whaEyV88`REWVYmfneE_pS^I`%FV$3HPn7NlT z`Y^dOFmcajhNuV64={OxL>P_1su>wr7>>hPx8bahU>3}*lTfofKsuQr>LF%%fkYTh zz^Y{!tQedaycmKQq8O4GvKWdOsu-FWx)>%g%wkx?u!><5!!CwH45t__G2CK!#PEvY z6T>e?21YhU9<I*}%q+`UR&ad<(<`~Yfaz6SpTP8LuD=Y-EGt-+bNvU?E4UfK^h$09 zFujWFADCXv&B?&bvXW&vH#eAG!OaV%S90@!=~diZV0tw-GXpcrDwgHktYCTtH#?YK z$;}3)S8=m|>D64{8JJmCvn=QO38q(Y{RY!3xqgA^Ra`&7^lGj@3`{I5xH%Y@SXOc~ zF)*>L;`+wG#Il-!>=5DB2K#(Dw=S4o!L1LbS90rt=~di1V0tyT8QAB`xh=u;N^T1< zy^7l$Ot0p41p9b7w=<Yt!R-pBS8}_6=~diLV0tyTHQ2|?xoyGp3T}Hay^`AwOt0d$ z0n@9w4Z%KM&TR~)S8$tx>6P3jV0sm|5tv@ht;N8^vYgui93obr5aBkUFhr8UK3>k9 z3Z_?Zr-SL0+-YEX6?Y1lUd^2k_W5$|LNL98yBJKb<Sqi!tGElm^lI)Zu+Nus*MR92 z+_hkO6?Zk5Ud>$!_VIG=axlGuyAn*V<gNhItGLU+^lI)*u#cB>XM^b#+__+SC3g;( zUd5dSrdM+(fkPw@oKi}_A(BC1h`57&yqwz;Ot0Yf2Gc9Ky}<M;ZVxcMnmZWm<K^6; zV0s02IGA3^9R{XXafg8E)!eaQpD*W*2h%IK6T$RK?gTKsiaQQWujY;b`+PZf6qsJY z9Sx>eaz}#c)!e>dA1~+j2h%IK1Htr4?f@{oirWuNujY0Ghe!}OL}I`x#fQQW;b#Dq zBg?r3!So7lVKBXtTL?_A;uZkYtGOk?K3>i(4W?Ic%Yx~Z+%jN#6}J?aUd^ov_VIFV zbuhhxTN6yL<kkSwtGLy`^lEN-u+NusD}w12+{$2jCASioUd62drdM-|fPKE4TNF&M z;1&ndE4jtM^eS#XaEM5NLqr7}B66UV0;*X#)ESt#K7v`AaMlzCCLT|)*g81tAe{9Z z$`WNzVbEaEVK8E_U~pmZVDJI;CqU&b%Sx72EUP)xI5aqVIhJzl;P{NFlNcv4Ff;Hl z<S{aU*C@y^sKINf2=+b(7VZx2$=ogMy--#&`vfp+3U>>8Ka|zX-o?Pe-3?aP31v02 z_kdX-b=^=_GXoEU6oU$b9)ksg6N3*!7()U>7DEX`9YY8EG_YA9o2Eip&FnM4ERYG) zp{!>1B@8UwjocmFE$oY-tY-E_U{*JG3;RMStC@Wg*iMjn6QQhT_9<W%$h^r=7Cslv z1KSBQVJ?)_%)S830+}!$%4%j`0k#w5qUBIlGy5_y3*@4uP*yYhEU=v*^JYR>&Fpi) zERcD#p{!=m*aBlMV=LoCaGMF#?gF*2m_Th1P=6cL&Onk8W8mPB=4jxM;b`QL<!IrM z;{dIn<zP6*aGc>5!)=BS3?CVd89_5|4B)mKXiW_ZXrCbHP8Lv#Vw7i80PT50GLw&i zg+rdBi9>+{<Qf*xtuBmgjO^gn9w@CSFqnW_j7$tcpmkIXL14XnP_Z}$4F*PrIItK$ zR4fa;ehf7F$N*j$z{pUENP*yXCm*P-$p9KnVPORIh_K7+gIx?-TZUDh<2Xp3<1_;k zLnYXLQ0Or-u(5+$P>hVH;9^V+Mhs$HeO&!qSGaV!^tkl747d!rjJS-sOt?(B%(%{R z-sgP4`H=Gw=VQ(%oKHEQF)%WSF>o{RFz})EI;9w-8Dtm~7`z$$82lLm7y=oB8NwLC zk!(_7;OCmbHIwTqyDdXCLkUAElNOUUyB(7blP<eG*Lfz;EFnK$vq7_=kl15n*un|g znF`~vfyO@>7#W1%_AxR{;$j8I6e!gwF)%}A!8aQ*Ffd4h!kI}7E)KcjiGe{1v=5hy zk4qff^MITG{~K6bf=iKs0dA@aST|_y6Vh8@V2}Z&Oa>JO7qAS-H8N0hWI*~Egc%@w zP`E=<3n*s5X^|m^p$3dWEmRc-BL){}dIG6nWMG51fb%C8qW1=Bo9i)%aK7jK!1<B$ z6X$2nFPvXFzj1!&{K5H?^B3oD&Oe-gIsb9~=VIVu<N{BZLtMbc!o>?3Cjh$*Y6`aa z<YM7s<znMv=i=bv<l^Gu=HlVv1^I*XJ*F$Lsm1RK&i9-@kX^xO#W0I&C3ij7I<ECx z8#sD7W^v5sSje%IV?D<Pj;$O!I1Y0h<v7l9mg5}96^;iSk2oH4Jm+}F@tNZv$A3;1 zPF7AXuH{@SxR!D)<66z*#kG-R5=S4$1diz(6FH`G%;1>Ev4&$A$4ZV>9Q!zSa_r{V z!?B3tGRIAhs~p!k?s44UxWn;-;|s?(j_;go91l4rbM$je;h4iQpJOh^VvZ#oYdMy4 ztmat9v4Ue0$3~9L9NRgzaU9|}z_E+tAje*g{TxR)j&YpeIK^?2<21(^j`JKBIWBNq z;<(LmjpG)_eU2v_&p4iPyx@4v@rvUm$0v>t9Pc^)a{S`>%JGNeC&zD2MoxB44o+@P zUQQm4X&f^-?sB~0WaebxWa8xHTEn%LYZb>wj<-B^JoY>eJdQk0JkC5WJgz)$JnlRm z;QDYD0}BHuIPWMis4#$f%IXXn44MpD4B8Aj47v<@4EhWP42BFw48{y545kcb4CV|L z43-R54Au-b47Ln*3=Rx&3<(U$422Ah3{4Ep3@r?;3~dbU3>^%e3|$P}3_T3J41Em! z3=<e8GE8EajC2FmEQZ+(a~S3_%ww3(uz=wZ!x4sK3?~>)F`Q$#z;KD-3d1#q8w|G? z?l9bAc);+4;TgjVhF1)47~X;Rbbn^}%J7}x7sDThe~b)_OpGjyY>XU?+>E@8{EUK( z!i=Jf;*64v(u}f<@{Ed%%8aUv>WrF<+Kjr4`izE*#*C(n=8Tq%R*W``c8pGpE{txB z9*jR3e>47N{LduJB*G-hoWOpAyOFzzyP3O%yOq0*yPdm(yOX<%yBpk_gye4~MjM9N zh?ECPMP#KuM9O<hN!sJ&KuUcJu%^E)Xep2y=?`z}!;|)ABc(l1I&xufWpHC~XYgR~ zWbk6}29FBB>VzPMV1^KeP;ku<!4Sz1#SqO9!w`#<_7*ZMVpz<ugkdSeGKS?0D;QQX ztYTQru!dnR!#alb3>z3WGHhbljCAYTE{5F<dl>dI>|@x^aDd?;!(oP_496KxGMr{O z%W$6IBEw~ds|?o}ZZh0vxXW;#;UU9ghNleA8D28HW_Zi+p5Y_I7lv;PKNx;9{AKvh z$jHdd$jZph$jQjV$j2zaD8wkjD8?wkD8(qlD95P4sKlrOzPn9}QHN2F(SXs2(S*^A z(Sp&M(U#Gk(V5Ye(Vfwg@fYJC#(zu<(Dau`IQ20&G00&p!?Bg?Sj50%b>Nx>Tsv@q zOLq~ZnvjhVG)KtD1zJA>N%4#fY@8oJH6Vj90}GQI;{?#!XQnQu2nGfQZwSp8$6y4y z2at)Im5GIwg^7vf0Rz*22L=Xt4h9BI1_1^JcYl9((0aE2e_3?@-DY55(PaeP>k3{1 z#su1x0b2h8x>XNUV}VA&M8LHo1A_<y3zrlF10xqB0|Pq)Xq1qbp^njj(U38bF`2QR zaU$a+#`%m38P_pxV7$Y4m+?L02gaX_|Cz*@q?yc^%$XvYVwrX`?O{5~bdu>h(><n7 zOrM$lGxIR3GOIB=GP^P-F_$yfG1oJ9G50VpWIo4yjrk$-e-;K79u{5}f0h827?yaJ zCYFh;!mP5a>a6-wd!_ctbja?PeJ1-=j#185&R(uwu2pWf+<du@a{uK;<z?kv<$dMD z<s;>z<zwY*<+sQmk-sc|TmHTL7x{k*+zKfQg$nfwoeF&lQxxVXtWnsiuwUVWV!UFD zVy8~7PQC7bJ(p$0%W9X+UoN&>arwICTb6HM{%iUFm3phAR;T`E`^WH4|KI=rf5GeS z7#MgN6c`pT8ZsI&CNZWkHZV?NoXog@aS`Ks#*K`38SgQEVEhPP`zFC8!(`56!4$<5 z$FzrOFKA6K(+#HkOrM#)Ff%aoGOIDGGdnT6F()%uG1oJ<F!wO`F)wC5&wQQv0}JRL zK+r9K0W5(mu`CHJ%`B5xMOfunHKaC3?UQMcWsp4}`&{<79Fv@#oP%70T$|h+xdn2c z<Qe28<Q3#S<O85#5ij2$zfJy_{5AQz@}K0tDKIGTD5NQrC^RT^DNIn9s<1#|t->~i z4~h)nut?C!(|w@Fu*_ju$+EiT49mrrD=pu!eCzV>%YU!bTWzp9`ZvQrhJOnG{{R02 zx|RL^*Z+_IA7cCY{~+VP{|6Z#{a?@ckYPFFeTG?#ciFrd?=bi>-u~|ax^12DBI{Si z3k(d5Xa6^WZmDHF$-uyP=zlQdK?VknhW|_)^_-j>)eH=bUaY_Wp8tF9@7cegyKKKP zy!hwych*0Lzcc^-`#Xby;qM0qhQBWv7?{>DFno0SSpVkm^EV6(?<5%*p09es@_fbP z%IAw17@ov3Fg&+=Zu8vgxy5s{=cdn1o*O?mdT#Js|GD0Co#)!mHJ@ud0^Nwoz`*cw zEd#@|4KKYI7@o~%V0f0s!0`0Oi&?k+GBDf&tp{UBeaOI&Iz1tVfgvFklyMTp5?kUx z_f|6S88I+0TQk{%^D#(@7!0}(6QmBA2Hh135_<(Apcr%?I+V%40KV-U&I2(Rg&?Bf z6}F%g5J7?rpuIAn^`>ABsOdNXvi20l$3-*tFxfB#F)%QtfmASMF)%PSFfcGp0Pz?Y zm^~O6m@}9^K-Q`-FtAKwU|?Or3c7<Bw6$an>j4G^R?yANAR9dxJwg374owy<4mA#S zMlVKhmPw30jJ}M1jQ)%PjDd_nTpz)8eH*xr@8Ry?ZsKlaImq3~-OAn0lg(4aQw}<D z0X%0}##6)7z|+jr%G1VK##zl-&RN59h~+S61!pa1CCf=BdnN~#Q!J-h&M-ML`7?fH z{Kn(Y6TlP56T}nDRLoSuRLWGwRL)evRLNAu_?__w`w{k|?8n%Tv!7r;$?}5bCCe+8 z*PLRU_MGCJ4xAF4j+~O5PTbGApK*WS{>c4?`xW;)?)%&?x$p7Rb3fvK%l(4;J&z6d zYtZU4o)VrMo?M<ho_wAPo?@O-o;2{hYC2B_Pc2U-PZm!jPZLiyPdg7kXq`9r1MY`R zE=;Z*dK~)fH#k%n|1&W#`!feH2QmjS2Q!DTU*phazr|6<ew(A7U7cNnU6WmlYc|&$ zc5Nm`CMG6kb{!@bCRQdkCUzzcCQc?UCT@0JCLShUCO)oKu4t}CCVnn|uB}|#*!9@; z*$vnY*^StZnFP2lGYK*Yv72x$<XXVBn9BxK-*KfeiE=$*H|1*Kvg5MlGUp0l663Ps zvgV5CO5pO~s^ZGvvf#4hisH)Rvgc~xI>S}U70l($RmQc5%a6;8tBK2(%ZaOo-HdAs z*M6?;T)Vh-a_!*S&9#SXFV{YHb9M`MOLi-EYjzuUTTVWd-s4rSOI+tcy-Ox>t}9#@ zxz2(5wCt|zZtU(%5=@fp9_*e>QtV#r-du;7Lz$$x4sadeI?5i(ewFJO*FmmBT*rB) zaNXg$%h|_upX&jW4A(u*>0Gb59&xVbZ0Bs??B`t0xq`EUvyrotNtWv==L9A>_HeEb zoD(@GadvYyaZcuX!S#yk4c9v+d9L@IQ@EZnDR7<OZ02s@oWa@4*~VGVq{z9Fb0+63 z&K}O$oU6D_a-HV7z;%}EG1qObhn&kemvT<y?Bcr4b%X0B*EOzNoKra$axUUr%(;Z? zCD$pg=j;*ekz8-Nw79gnbl9WVqq$VLRJqi+)VVacG?|o`l$lhRRJoMcue0Ce>0-}j z&tcDH&tuPLFJLd^P~|Y<FyK(+Fy!gr>E`L-%Hhi93g;@|3gOD-O5sZ8a^;HPD&$J# z3ggP>a_5TU^5IJ73gmL;a^vdZa$zsx3gU|8n#EPg70Q*zRnAqx70Fe^HHm97*95MK zT+_Iwb4}rz%GJyj!&SoN$(70Zj;olfm#da5i7Sz-p38yDk*kiYnrj}{T&^~*cCHSt zPOkY}U0h3ert(bVl;ZT^l;-s1l;QN_l;!m2l;aHGl;;fORNxHaROAfiRN@TbROSrj zRN)NcROJlkRO5``ROgK3)ZmQb)Z~oj)Z&cc)aH!k)ZvWd)a8ul)Z<Lx)aOj(G~i6) zEaC~_N#;r6N#*M1+RRzZX~>z(X~dbrY0R0*X~LPtY08<-X~vnsY0jC+X~CJrX~~(* zX~mhtY0a6-X~UTZ-mPZGS-@GsS;`a7lfZt5!<PLnhaLMp4tw_d91iRcI2_p@ayYR+ z;&A40;c#Vt%;CoVgu|WvDTfF9GY(Jo=Nw+_FF3r}Uvl`czvA#^f6d{?{)WS!{VhiT z`#X+6_V*k?>>oISK|9LWKXHVzf942d|H2W@{*@zw{ToLl`*)5g_8%P4>_0hT*ne@v za<*``vj66YWB<bu&;FMqf&CvxBKv=iBn}3SWX|W{n{LxM(m66XGC8t1vN;$zayXbc zaygheSUB=HSUK`J*f<I}*f|P0I5>*f>)7kr8`vA!n>aW*iaGf?xHw8UxH(EeC*N?+ z;atZ#mvcSmJkAZA^Eo$iF5uk6!NXC;!OKz3!N*a-!Ov02A;3|^A;=-bA<QAd)5lTG zA;nR{DFC{8k)xKoj`J1h_C?OOJpDWqcqZ~p;=ISx%h3kfF9+T?*Uizx-pby_-p<~^ z{e=4|_hTMo9upo@9y1<u9t$2zo<ttVj!4j+3D6!N&}slJ2ha`W%uEc-sSHevj7*^n zOiYZiER2jyjJ^yE>};$o49pD7f~;(!+Qx#4f~tywigApy|IJ`D{dbz_)L%1WCeZE# zCI*lHpBO-Ux|tdH8RQ*gK$S@>10!RTHv<zBb36kxb2qb(in5>p3$KK>xiaXYWiw+V zF;NjVWhJ%6mXg^eDpHcFs*+MFCz%-k=BUcasj4U_FfcJVGM)gRgbs46ii08(BLf3t zEGsh$Gb3XYgAWS}0|N^?3p*Pt*p+O&qT0gD%FK$)%FGKy%taW*M9f8)PW}7G$OOWm z@jOWe2Jo(JX$E-)cLx_9E+!6s0bXWy1`hT#J~k!>R%RwfCI-e-ZbnYdCT|u-CZ=X4 z9|i__IawJ7X$EO2NeOW=Q4wK5IN+7lW@I-ORc2Q<2V+xXb7fOwb7OWe7G*UyS7v-5 z-#JawU9(@^O|7n5p>2|ehsFdASB?6A_5Jewj9V5sU%YtnqVoc$D_5>uaayndv@_Q3 z|1l;Ortb_&44MoE3<VA%23ji0Qj%h#oUB}oOo}W_42(=(-WzlSA{=B{7@3$EnVA^t z*ce$ESX)3X8dgSz3<hTA7G@u52So;ECgx@qMkYq4W`qhx(7hiF384N;K%@g70|SE+ zgOZ@2n5ZB-ucWrPshXO)shAjuHZ?IbH#HU&6BB1wQ&Sg}V-gi%V;5yMWn&W+5fe5u zGc{3D|7)J1pp<2t9Frd5U>dF>t>CNS6rr0D?_g%ETVriz;AoZ2D99fwAi;P_I>_BC zgf$|ZNrc~vSD5*vu(rLCTR2xf<L!+sT8bLl|1Ps>E2*i0mNGD~G8q2<#>~fbg@K!a zpFxsAl|i3jruPQPfCvX2UPd-PMh;dsjtmA?21ZU+#&jM=78ZthZe}iK28J$g22M_< zcy>qxN;{ZfD(7ZoW^VF^XaaSWx<F~Q8KjRro`Idco82eUL0er-T1rrWhli0tS6fYA zU0+dNN>y4_TvR|(P?CqAho6s^ot1%`k(-}aL|Yh^piNE8SQ(WWS(QK`WNc(=!ipp# z4i`P_>Feq1?(N1T`me;7vFBflnzFK*FJlH&*aysIx>A~zRbHNzRm%0(jOmm~U_e07 zqJ<A&g7yIcfq{!a`@fkPO#gply2W&rL7qXI!IYuOfnQCEjhT_fRFIFIg%zA!Kmn)B z4h|+ZMsOf8GBU+8g96E$ft3}?WCA4`bXBnoOiaxnMX=l)=^(DCz{p^1sGzN=Eh;R) z&&|ak&nVBqE27N^_Aa})nVGpU8yma0vN{_mpMw&TnwqJJ86*hA!AXZ1oQ#Z(%*+`L z0%8O04oWF<M+z9IO)3x9FA2`540cyAPYTvDRa3DxHV87a^|Et_R$#gs=xHCJvP(Wt z-!WISFGR~EEzT!SLnX-EGuT5%UBy;a$wpH}@81apCo^*=Ezr^#21Za>!T{Pe&&(j{ z0N$w{%ftvSM!<W~nFX1l#RsV9fRq}bCNv|1#{VQnKBiL)ybQt)0-#<!XoGqagO8Xn z6R()IunE|gq9S7A%4%vF?moe8?(VKZKJJV{Rga_N9+j8ekBhhu%4c%_lNedx=72Z4 zGcq<YLd{_mVPjVY2Z_3|m>46AyHAj-ySrO3$e_ym5pnlR${)o=KW1QL&}1}Y{KNEv zfseu1L4cQsn~ReJR8TWBG4e9FdvDMTh}g!+$iUFT;3K^S#3+e$;ACK6;A7xp0!0qH zxj4HxyE?nNxw^TyI-`@w0lppOVWGZ}rQ7)Si8B2-U>3taPk;S-{dxQ`W}sD$Mhy8( z98AFs+ze_C$_%UwOst_Sj10_-;DQpAY+@OhSebmmMWO&3uaq{Uv8b}BvZ=DEv8gem z<MHDj$B%n3wz!`==YAd(4x0b#84fU%FmQls3MP;X8ACxa!4M0Q^<@Cf5ORosV#8dV z4IC)uJK6aRBpmr<*-N-h>&+}XKwJJ8G#H*TN--KUFf+(GNHZ`nGBSjM>m87hpxO&u zCxDGHH5S$Q^wiG}5})9@ilLH$8yueu42<!h>w6e|RFpvRX>Nk3nLJb^CDqg<B~{E| zH4?~$Tnr41Sxl!G!08C&@Fs5t28Lz^AEb1|$f(T7nDy^l7$X;`ngs79HvIpMiJR#v z122Ol$ma~q%wU_r<qktU0|P@hgO7@esxqjKWE6pA8IWsOMZ}DaKn1O`5*rh@L1t)W zv3^EqMcKbP=fD70*MLB0#<Y?p5r@~7ED1ljZt|pwQzlKC1PTj=NCpN*(D}?f4AKq~ zT%4?+({;Td6*Jh$QUV}9D=G_`n3=P&v9qh28#5+{F);~93;XCD^kn?3Z6YHeCTM3p z4H9++3=B*hOjj7B8MGYK`FNR_8JQRvn3+IEGQ~46F?D$}Ff)T95)>Zlsv6)5Us2K2 z+>XiAL`_+Vja}Ug<ReiLkZq!hOdS6-cAI*uxmlR{Y3gg`x}=oo1({D}%=FxA77}1+ zZL6YSt!$VUVUd(zIvF(B#lXm*`~MpgGt(6YEd~b%TNOr7V}Oa7pOKM?kCB0q#Y>fu zh0&9NnSqIsnJJxtg@rYqft9t(8<akvfvl>csG_O`PN|@(ObpZx0Toc78V?%I%BD~T z+{KJ<1D!)dtt#U!5_EJk3=%>!<6Ye%qTOt)f;4p03Ox#PnUp<3`B=Cj-0c(01jT~H zCA^)St-ZZ$O)Qn9Y~*xO7#Ns9tNNHUn9eb9F^Di2I_U8;vM{qTGO#dtv9U6-FoE39 zkj4P2mlzlsn;Ct$xw%ESMFfQe1qDFWvMMNBDVj1G3mPkfN>gPvHg;wvjf~KLs~7{K zGxjnwv2aLmUt6?jkvHSRf65O{JQam?`2U@U#GO4j+)gpbF}OH5%7`;DGl+39F)?#8 zGBPoHf$9TLGccWjm6auyfrSO!bOR^QZblzQaMMRbh>wSzl|hbCjvZ8xfJ=F0B{igA z1BI5bqNpOfnHi&SZgX#)MS`MAy1sv6SgeePn2t1K+P|lQf>w;%@>25aMTCRJB)$FJ zyt!Cq7y|>&GlLdEGB7ci|Nq9s%XEc7jzNdP&cT|4k&%Iuk%>`|k%g7ni-C!Om4%5F zbR!Ko&vbdSF|x40DkTMZH5GXs1sxT2RTXw#32k8$J0@czF>z5fHDx6}CU#?3;!{>q zQxk_}DNr>nW-jgCkSb~{AfzYe8e=NK%qAY>5L;B8;gaY0$==V^Ro_ir$%WC-cWR{v z3#&I9n_fVMwX$JhMnQv1Pg`K9OMtqqymlyPWxWmq0~6?^b~Xl82PIa}Nx@#Aya;Iz zgDYE5lahgrflW}5g;z}5m{HMGQIxTRQT1Ov<Nbg4n5qI?{#JqFj2YDaVd4VkHx1Cd zl7q3avM4(XGZUj1D<cyVLp%$l8iy7o-Hbl6GN7i4jE1ZRKQDtcqcj^lrr6lQO*dsF zP!()u4z7vKAXTv#v@V7fGwSMWY>cvT6{>csIv&~$StZq)F6yQp76~>MZpt<)Chl(M zy?i}=gIolhT$!#!1Zz7O3X7Sz8AfHMMjLpV2+4&ynfYkAn;NMaDGP|%nHib>(=c&$ zH8*jUVw3d%<pZPt-@xSvG>!!rSy@2okb{wdk=YAW$1t)orNd(uCHKPOSXEsG9LJ0z zd`ys3Xl!K1WUh={aEhrzTU+3YRgH;5+PyJV#7scl&@I|bf{9H$$T6nSBHh&`&*77u zzq6YGIFe_ABAJ!dn^(_2(@M#(Fx@-H%C);KD8$uY-9|w>!~zsIus)9(12=;>xIAZM z1gFSeP&*QqAXQXUltAUVFjB1|h{T51FtD12@sXOGoSKS)!oLbcox{W+|G%E$5EIBQ zQ3fRkc@{=y1}0|EIZRMH8GU$octm+b#Z;77dBs7E5_tDY7+i_kF`2umNJ^=yN=d3@ zvGW>AIB|*yvLbBT#%<bQsu~drTCB{#$Y8`!!o<mx07|5rnHd-jv{hA2RYh4@nK=Jx zGG71J&RAJ;-srX8rg;oZ3>FMGnV1-#gIAa9JLoVlfVz(ipu=OBnZX@O=2%unW+rA| zY~3|eMNw8$MNtdJQ~%5uPcbfVcR%j#4hmmXH?T1?8fcrVs+zKjvKlc~{%dEv{!jDJ z+|7Qkjn0G86VLxoj5C<7fZM)S4(0-k42=AYObkq*YyzqanCn1U5LD5FYbcg@1{RiX z79R!%Ee#bV26+Z~Rc$t2F>P~G6Eiccbrm0zs3<#9#P#s2DyZoQ*;yO<D9dXk+Qetb znyM%%@x&@iORA_yN-Lj~Rx>x0QP9<rGcb`9bQ6_{3)HnYm6OrZk`bJYh*}0F2CM(e znLU|KG1xQsGE_SVi!pLCxpOcvbLum)G8izjv9fr1Z;%U!a8To5WM*V!U}a`VXJBPu zV`WQbU||9GRhmG(7I6LB%IPER0NSR^$;`x=j#~qZPox8%x2LMQs;ZiXs;UYXs2>Z> zNGLf-oSjWX6kINV+8>}A98_9@+Uug=jHYG^DPqMSZ4`5JP)W<i#_TDo<DjEw%El_> zX6hSc67Q%JD#695Dk-lm?%f<*G&gY0WZQgg?QF-g6_HkXbCWX`#r|VA(y=gPVdK>h zHMEgpOx25U*EUsBHA)L{Ota$WJT1h}Y^rD+ViMEj61OyK$~<WS7a{S8r6sZROC7@& ztar2wajs=yGw=qb2hd?qbxda&*ck*t^(-$V3lpOk8zTz?10xIQU?ip{Z%|hsRL62~ zFfeckatJc8Gq4K^3bONxYb&ZNvMVzhGcv2|F{y(ZDol!D|DJ|MGl~Z?ik%B!=Hq4I z5M&Fydzb0#Ut>ny8ys4?Jo46pH$dr@nIVyZ0eq^Q0D~lh5`(FO5j!I*6QdUc6B{F_ z>Pm&Rkea+1SXdZA)j~6)kCc>@l9Zy7ilUAJsHPW&WM6hjcUxIm7}^|B78HcC63?`D zxjE^2YPA;sj%tZ^4UR6G5bx!i%ycRs(LR`ml|#+VJTZ%LmbI0&@xM#|{(9M1xPsir z$YA^b8&f3H6$U8=a|aV~Mh0dHP9{bME=DFsW-m~E0xrA2MJ_l=bu;-eGKh(Qt9~g) zDOOO00xMO`;fVlLv4RE&7$+CER9EYTXljI7=T7qUpOse_qM{hc)H<uJb+!PHA3tBz zvVxqYv06U-pp%b4VKtZO6ay!NAh-+#jXpt|r-){xqN*ae3}qByQ&tu<HWCwKH&Fwz z6qx27-EjO)_WZE;-p0~IMkU6at=pMS1<Wb!Z~c1#l;J^f!T5pc6azPdrh^&-GXo1N zGfOIXj0<85D=R}Rr16gEz9=dRDvClO<A;BRjQ#&kGb;bP3%Vr5|GwXUP#++XfdOnT zCxfzs0?1qjRC5_X?O%|Q0<64}+Q!N-6L&{}O;h@JkLi^EUw^P0LGjVYbcI2I!P>z> zfRUL=kdc8|j**3xiII_o1=Mz6Wno|iB^z*jbb*r%s3_}Z^buB7Ra1s`x&&bv5)>EU z(HdoNJc0TS=BCPw)4yAldFRjY4_{VlldY$u7H{0pZ(f$5?<75!vGU&{VWG&Sh51XP z#YFrBBxkpVHhXfhUxE6SsTmxGiVkuNpq+~>jH#^P(G$>kTr-0Y$fJT_S12kADhetK zg2oDsnVO^iO^S+Q%!^|5{kNIv)W1vM2`>iFSudbdj<^{V9ArVgS5_8gCJxXQp^!KM zhn*;-W1?tkE~sv*D9SD<&Zzb8Uq}ceQ%H!3Fq5^2iHXSHqrwafjQ=<NcVYmYz9qn* z;h+ktdqB5WfVw0gnjx8iiKz*cgn4;5*cb#D1z31RwAtaURx?vUV?hx%76}zT0TmHR zU74t~ee7&LtgIr2dQ8XuI;MlhOc_Dxh$)-t6ayc~&3qipOe_pcj9#F|T$4Ab2iwfx zBPu8i>Fp>pvx9@c%*@=}n2{+v>#t{2l$@RX!OVY88ABcZy=FS~Z-J7F(v5#7!SN4D zOQ2IRS;0AgfuRZ1S7C|+84ezNWME}rg|-wy0V|pq1-fwcZyV?ou0#d~W>8p)GN?PK z2y-!l`*)B&DJu&zGdL(27#KtuL<NNf#n^Zyv=y0^#l%EG4iq*PHWmc;{FIp)=e%ZN zW(jV+!}6ddl9h?&9aE8frsY3>rc-~nGkN|sW87|%rtr5P=3b`hOs5!x7-Su!m>4kv zLR?S`8W4;ys~C;-m`nwYnWks{Yhqzy3(jDyWnpAy4q*yZ&M^O1$aL!OMga$JMaIdX zvKy2iE-+nX;AfBqhX}NBi`q|CRYUG4gPXLVk^xc+LAshu7rZBD=TG+Vm|T!O$@}k_ zqRPs`g378Q#<cvUF)_>XbC<?MFU@bCJ-fYq=1c}Aa2$ielAA$<!N@@$l+u}47*p98 zK}8WOBRFEf<H5W<j0^&NJR-azT$~(ipy5A6e+X1WDGQ2&$|+EV$tc&;U|O3U6ty+l zBP}J8=~P~db-Kg9S4^jT|2=c^cLkldX8S*k3FKc91_cIf2MuP>IvFoe#%p0<VS#0N zXdrYm`lzU?If{Tv70`&0xU#wusOyR&05a28H+He{vSAH{=ABBL7BXT41gPo+hXv?7 zQa%Q8P^rYu$i&JBYnP{j#|C3TJ;o+)W=3#FRe+z7L0FJqTtFN$AjQDP$cGpTWMdar zW>ywN4i~l7dM!tlBbjUaq7I~GhX;f(ok~r!^wHz_cYsm--%X}de*eC^I=fjjFfthb z|Hk-@=?a4;gPw!7G$S*U3_BAeGaIC<z|0H_A!zfvo54p_M-^OQsDh^#5ZzNXbx5ZN z)FWgEw^Kw##Kgqqm>9oxmD;2!sbpLF2g!Lk#Kc&oxmYD>%BV)@IlIWYSlNf`?vKwB z5(pEJvNKgS(2!6}2(}2)5fkzi5VO=*F;Ev3HwBf`;Iqh>6q!yju!F{1Kw-lK8aRaw z5D5xGW<V5`1(i(|MU@$yqwX__-(#xw2dzks`u~lo0i3TC9ppfLJjnD3xKq&$9$gY* z5K<Oa0X6U$1)&2(pwbIez?%v(HQ3a|wRJ_s#wCU`o%(0wRqi^mobk**!|;UkK*n>B zx*rs|pi^{t7!)1kK)o$SFJ>kt@I(!)tl(kbfs{6k%*xEh!eXG#p}8<q#Q&)OaSH`R zIoQPn7C^?d{+a3cDl7ZxGM)gJ3p@-A%%B@)<w0eUv=BRJ2*wLk3NbOk3rPkBP-9y{ zP*M<77J+Qg1Glh{t%BRhJau1mHzz+gBclj!d-UG8d>%noRuSI1*k(QvRu*BNJdiUO zugF;Ia`J}i{nKYU_0LByOievThjINsKfM?w)o^{r&0x2&FfcHI?&*WnIUKB5%5Nbd zA!xD1%&aWTY%DBn%*^DLw)a5%+j}YNH^kp%H2Zgov4k<~-wQ^Gf8Yfapp(BCm_X+j zb1=v_NU|}5MsU19l_IzdZ)Wrn5)x$Q71w4IHf9!77GySNl-`y2F6vzpV>V;_Kkt99 z83h@4g0i|A0|S#NxE-YFpvKO~$b=YHgC-zQAp~iu2nj+Rs%Q!sIZ$L0{U^v+@ox&_ z&3_$?O)gAk0WN<*YdRPi0{$m4g6<Dw2e}WlN*>faWnu!iI-8h46*t(=tjdDMqRN8C zj0O)Lbal;R&hYrF0S-6NSSP67!#CC`%&5$mz{nl`?;9vSm`;KGBl$mx0W`-5u@^L> z#hl2%$Oy>|jBtC!jRloWjRln@ySg4c2xGe8@n;4kovJZ%Gl?;sXJBJc-^iBY;vmDy z$jHD5E)|=>3AO~hP#ijfvXPC!MOfHaSecnw?TXPWzby+GxfzfAvtdmC2Rd$+f${&p z|7nc-{x4%-W>5f|CkmP+V+7sX1#15>GWZID+Z!N*SQSkb8Tb8jU_3C(0UQPzOmPe! znf`&szrlK?xj@rzj9%bzX3%0HkXyJJxJ5x@%^(wm<(R}pmGzj^O($|Is`6WlTN&~& z{Sz}c72*-Hv=riEU}Dhw|BdkhlOlsOgBnAr1D_-#3xfnBGYcCd8!NLHWQI$gfti7o zjhQu-0W__}&d9*f>&?K%#uyK(-kTYHq#cy8sbFMm1}OuVirq{;kq)Aa3@S=;GGZcv z{M?+ZETBOlSjjA?tPbz&nVUmuP0#?XFgt8k#*|SZDb7KVMcL2UGoh|I+R9YRM^Rci zVXl9Quer8!G?QXj+`q{_I$FL#0pTpn3eF~y0#3rx|DH0lSQzVSdHXRiF@*mA#$>?s zgF%!*kzvv{K^7)f7DlKaWkHo|i#NDJ0S(Uff@YW?KI~@nkp@p@I4EK&g3d+3h9{*R z6p@SoDF-Js@O%|aO=P5lh`1O7gPe?*qPQZcMI@r4&H)-L0nKM34-Bd+vnvY<E3t_} z`n{$mYnho?#RKe9Dyx$11H{>d?B9nuI!1W7GyT}9WG$$q*WK0Cqo?4)^yA-yq;!_# z%&e5tpgdsy{~P0eCM8Hei<^;&QJjZ~nMs0?ff+Q6$pm&-uQ#YS2p-1kX7FKT5ET~S z<78)m_On=(&?+p5i$J|DMjekld&eRV4POaq2?v>ZzCPM6GSZG*R#oxIHCBQ=o}2=w z6Vruw{rLDnZ7R^1st`C1lo+ON6XIlIVTH#50~1pVB-|Mo8GE6T!OX@4o{)gW0h%HP zX69z7N>DqXn*lUu$iT`9^Ar;kb1bHs$VdlqaWO_v94Lt^2@3G@v9mIWGKzxIBP)1Z z5>z!CtEriS3I$NwR5vm+g``VS5iv&Ll#0p}JAX+wCYA_yUKWV}!%$bJNOyO4uI{d` zUOh!#WBzM@IXsjMJY(YoQnCx<6F_GIgZ$3Oq{JZ3AjjbB;K0Mk#K_CY%)|t0&w$!B zkoHVG$R8+Ucajo}3^LLZa*}d_{9GJttPJ9e;vknY8i|RCiYkhMs$Ec2fX0$R<3{32 zZ0w3^YK$r_DW?A>GDQX$`)g?inERGET4%f2d$KbMGTJdI+2nefM#l>a`3eczRYgVA zxp)M6xP*ez3qJz`V>8oV262WM2Yx<A7G{2SCRP?U(1?$h_XgR32nT5f=umfyH>i?f zjb&hEZ3Xofz{P<is2|G8%#w~F7wNz$tg0#q>7p@$8e+<(prI~MXhR!9qQ;Cq?GbTt z(ZTX&!r}s2VmjLGj932YFkaD=lh26eW_M-fvbI--q$L3+AtogTIR<Tp1_ypIW+paP zUPfjX9!4f+SmC9_!N|(W5YNcY1TD58BW|G74lbG6L6gjC42+De-Vl}W@(ojAq=Te_ zJR^geioCXhwv>bbA2%l(cou>aG!$VZCT?sF>cS~2shfhL#1t|!0vYFqjBkU6OiWGG z7==wk<yd*#N~_Df{mWhbnHX8j3NlJdi=DkZ+ylLQn3Oa$-T&S%E>Q~93Um|Y(d7)B z)Z8%LD?2$kGdxi=Hvv*Ng)%TOf$nb=WeC^`ngs=AB+Y;b2XR(L7M2#!=r6d*)(dMJ zOFM`&fX3<>KtTag!x#%{88$QeL^^=Za}Z?^6;={f12u?*MMYq96ri3NsQ)ggEV!1H zO)AnPF*&><$~l7R$9)ZDO|97Ue~THz8&l){y=DS0H)Ub~O>b^xdI(-C(d)f|3*;p5 ztQTldr^Or8V{Zkyu9*o`vhtuvccDvzR+fn4((ND$G7{9;Oh+~~GBVOZ7_?Rbd1VA> z0s?tugt(xxV3UZvyoi{boMQyjLupxAX%Ig77j$y~0|V3llm7)6ApVeMSn9wfz{t$P z&&b3KTJHk#lQh^>3=9k{pn;H9NRk50hJezP_Xd!^;7Y(_B^XLTeuFE)W{QI(sN!c~ zU<Az{qG|{Ek)M}GLR>_MN19h!MF}))j6Ax}3?Ev6j4b%ci9vj8A*II4uOO_T72(0O z65?m+KP~L6?yRiJhD_OiXLy6AuNlCz*i5`kKNy4={2e$2_<2}ZKusS=T_^<VHn)K0 zn_Io%?EndoAR~A+Lo<q0qyy-r5Mc&kVIe6{uR&N)gpJ+Uj>+8Im|a*|on2H}a1Apf z3!8)_=j!yEFS2&ohBN*6S1e#6%poVwwe{)0v%gHiw`hWA*4HxqU|?p@fH;JkfsqmH zj%HBN58{CuQlL?<$Sshzl(Dj)@!E(8Q0fHxi8+i(i9wMecAK086C)ES(}0$vh=JBv zF~qa8aWI3k2Qw46KPl}X#lXha3SNQ55YNs5E=FJ@wvi6vj102Upp{sH{JcDz?5r#d zij0cT;s7+ZVFJxLFxprgT)~4#riIZ_;kka!;dy~}5$?RKlA#85YOK;ROiFQ}EEQQF z75482V`P0w9OzIuPc6+TJssqHuf!nC5U^DMv_cNrh!F<oZWdOs8=*NHl;>GkT0z;3 zAs#e#051(99RwK}`1zpD7G@L%Wf0Jc8Kevkb}nOhLQ;4|qzlNAQZVO*H^SWo>dJxV zO4XT^(AN}z>oIUC1X@#|3h54m#!n#)3XG8w_4Z=-e23N&j{^ID+1^2cK0ZM~UW~l0 zlN@U!TPHYHN5<#p$H(X7fb9(ZFTiBLq{JY~pv;gBYD*xs=OE26aN_~mmIJM=00jdm zcY@ZUF}EVM8!*)%S_sPG%BU>_P#{5i^q^rSP<I6spP*V7(kudxw}d2DR;JhminB4Y zMtJbBNcb5BxiC6L7&yqgGbwd-clPNi2$~4rVdn5uHt>px6HLxFjkAf#L$23E8I%|@ zK<!lIHWH|51Ilo)_7P~ghBv6C*$!D_$Hdf%(S|}+gJ^v!iGzYp6x#Y^h16D{F&tCy z^b)u>V>U8_HpfBDo?T4LY~q0qiIvq!_5l(sylx+Y+!$TLJ>1+rJQXq!R5Adyehid7 z|NLOokB$>c%PfqK&xWU2#y%!cnoWcBD!7>$85smvn3x!uyrdntnHia3yhw-wA#m}^ z%mPl$phaJxnp%v3nHk*BfXq09lUFmSbHNWu%N%SB!i>VK(E1qCq+kPOL`X{Z=U3w6 z=2hU;^w5s*;9`*sW>R8i4rFH04K(}rh)E(z2Xt&Vc${Yw6KKUF?s1-8P-_R4uyK#` z=rKV96XFz65y&vlrjjI!KzW5|eP<gnOH&gsjr=IXP*pi4FLg6ZDO+P>549<N0YU-+ z0@B(lA_@v3A{rJtE?y!6ZhRuja^iAw!onJskZ=|F|BW%2X)=QZXblP<BNGcVBLfp? zOF6R_12YSAGpIfSFQaJn2DK}}&0_F63Q-YZAwdD~ShNJA1Zdn))yNFqiWF2fH8(dF z1@*;6#27h5w0QYUB(&Uhe0-HnX3y5QV`Ft@=altuWVFyyE~<9<cgj#7;vWD1-<UwR z3kWm#Yy~d{g_TX9t!1F3$;`|U4;`qMb`Xb&GB7lQ>Q9*SAng%h24N*(703V$qK60? zXacpP1eruVcv+-kOcIkK%OhQxB>ui|*HVv7V~qc|ydfoy5w^3N0ek}&KWLnZoso%w z(F<gIt2bg)o1cMS8nXU@*%;Jg2GvW##>}Tr#}#o1@iDV73kY!(#hqc2_;*Ci%t%1M z*i?+s=I;xLf4LbLnc0~@^K0r3D$)Wh;Bjy8z(A`vD=6(kmSZU~C`n2QN`jhJs?2I? zpdJM~Y^VuhA835b*v!lvG}yx(!z;qV!Xm_%kTN;ii(7<+g_(zsJw73ZON@nuS(L{+ zYAPe6o~EC;xS!U)%S;mg9%)C&3L2S<G3x!hq#Ph48KK802XY~3r#u7rW+QIU2sS4h zGc!gA2nh)Zf>#DG8;hG8GlQyQ<tsZ=x;m409!{AwC7Dr(k?qu}f4}~{y9PRJo`I2p z@xK5g=mb<L(D<5^Fe@`NgAlk~1PNIN=ul`olaH#hBzRay7(A2)^&WV32s#*QZq682 z!Y{$b!Y0L+8~<E3!cRhlms?&?&q7d!N#fsmO%FxMKut!2zw>;eSef0JSWL7*lk$uV z>I@8w>zO1NI6?DinB!F7wi2u%fH_XZxc*-Sqr$(Bj0OLmFe+Oy&T?`3mj`NUG06N+ zVvJ!r#URU|<De-9u3SMo;aQm&L37;<tPG&JZt%op6N`_ups1iIc)E!d)KxT5Q#LgN zbra2uMcF{Tc6L!QP~TmgF(xL<-rCweFT%t+H`mH^9;08tWMgelMlGW$0e&7#+J+O- zh1rDDCmU)rGB7f{|Ig28f!f9aH!>L++d-o(+@L;MGXt0hYvX_>P~LlbGX3}iO7@^z z)&3_jf^Gtn1<fF^GqNx<dVzXn49qO)3=H7a*U6w^Fwj!DCeTF&46+Qef}(=b&{fx9 zZ<w2^f%?3lr~-Kd>M1q_h$o<4ng??JM6mNG8EP|nF!}{df_QCe0H`AXTEoF8&&0{N zh(Vme20U(M$j->bC=6OT;w8k$=)uOw><-%7$`lS73u9(vNQbV~XJYn;En5MNW-)?C z%Gpgp%l5=!onB=nHAY?)eK8q32?GxqD?Jr`QCT|)Lw8v#MtcEnB}qS19uW}jYX<7! zfY!=*F&tr10{2a193;W^GADwf9yF2J%itp@1YIZtuHhl2H@mU0@(~p&DHRZov69ST zyr(8B3tAkr19US2C_bY9e`9>gbcI2YA<scj0kncnkd=vvQ9_7`nF-VvH4KP=>_%W> zV9Ws7%EH8)&c?{f3Z8s#W&w?&fup6FfrXg`w6GUcR%I|SF*7rTBf<l+LSB(UQBYJx z6&zQL&;{|Jh9-D<1gH#W7gaU|S3PFtri_WIZjw?`PSQcX{ysX6^3qo7&eq`u2I1CB zR|1#>xIMW9($X{H1$ljW1sD@kIBg1}qD$;JK^H!O(h&m#lQ`351_cHW2Uk#)4O+i1 z#>mRT?8VN=#Ky?X!o-{kuBka7E5KM-z@q`ptUinkvNF<Apn8Unmxr5!jX{A?0opVM zP0)iJ460y2qh5-lir}0BTB&Pd#^`4rt}HL-rD17v=8TpUW5YjAZHt72e5T71f^K{g z)=u3{8Mf8}0bX2=PMJ<qB2yTc7_1o@82>XlF(@(^GT1s;@$s;+urM>oi|~USC?_K> z3fi3D#mmaX;2|r<#0W}oj7*Hl44~?Wfq|iy*+)%FUjtkYnwWtWBtzz|&BfV4t1?0B zltIN7XtA=Inz;x#^?{ms;B_cb!U9f)PU4md^5zoG1`hlp%J%xs;ui7_22KW!0>aKp z)+#F2%E};8i7`>mQrtz~K|okoz(L<x+*01&&`IAxKuFm^-$~q3Ud38T$y!Afq#Tqe z8Tc3&m~@y<F^Dl3I~Z^>GID_SSTLorgXW#UYY0F)2AV)EzGhY*lpP`5pp`Vj#>}8K z&>*Y~UI(pgYRqiRD7!i4WX<&H)5E4upI&u5mNAzx_Ftz%kV9jmLy!YwIRg_?8N|+D z1L<jrf}$C;NC7;w03KEYw>iOwH-h%6fRZU_u&tfhCvqEXiV0LQi5d$kzxVV6l~4bg z-~Vd{#n=DejQg4XG8i*BGK4q?^Dr_p*juQ{@G`S8Gpa(zZ$KkWU`s(GvMr$Y0eHv; zG;H7iwv-vvF^1MQ;8q}~mWHyTss?B=BWO81c+e7QY)M2+92A>u;5Y@fbU{TMs4Zcl zrlzjPWG*5m4q1Sxrq0IZY#^_qkrr>^t0t}NY+z$(;o}>oC#$SpoNf^$uMns2Wg%>C zZtA7(Z66>et|{$eC!@pIZD_|YY@uP}D$MW9CoC@`qb?)sZe#2yC~T_c>LV!N%`c@T zD=Z@?Dy(f~rfMw5!K<Srsi7*&t_N!OPh~1+T*q_^ypB&5TvCE&wHX;1!a)ORjPaoC z$B0rSBKP-^*Nab;77><_5f+i&BP|SK2um}qloA)0l9ZH`gwhP)HJ!}-Ojj6$8Ppi; z8B)PEMu8T3*%&ghv)MATurqssj%Q+JWCm4!Z0xLT=?v^#j4Vv-Ea{w#ObiT6;hf;r zpB$hgjQlm!MMW4HtSn57^mWzkHSFbOMbt#qxH&;ha&BG`ZBQZ3sst*RK#O5Pi$M99 z)Qvz3vh|phl|Uo?a!lsn(N{BLBXM?6-hy<fKt&qkf`GPUGqdEj0H4-WOAzf)5N4nk zT<8+j8=x0l6r7x-7v<<^C2OQ@?P%#FZYe2Y#063Y(*~kV^+JnW9gD&Y1N)*uR8)1U zW2&W`tyHL|ZxAn+12-=y9uyes8BLfjF-S6~Fno336K7;$5ChMNFtUM)a?yYY2SxZ? z$SP3Y12+iPf~Gqla|WBia|WPc1=!>XXaI*H9yGYP3SF5hxN3mRv7|GwF)=c-F@jnd zP~|M3K{B;~2nQvQZWd++mUQTJOF9D^BO_ZlY%Ye4(O=p@lYx<qiH(t|4x3^ouwqaq zVq@};1dSkxGBPMB$ViC@@$+(Vu(B{nGD>prLv|2}!gtFc&&Y@)W@I)xI_dL>SgPt< z%(Kvvl`$3*lXA!~u+fs0G}dLhWM+LwOH|s>D4C5(Oi_xT+mKga6BCoXw3L{>1_Kj= z27?9THAc|wCcF$BTX{HG8JP^Ug&{ja%psJ<3j3ZO`xVDl+4c0;tzzUmsj_;t>PcP* z1*&t67#f&37+V-vLF=1A0xh6j0E|qbpte0|W)Zx15V2~75j5Ft%EaORuaL3j-=Dqi z|Nq1GZZNTdPP$-V0j+a}=Lr)ATZT+<u87lTWaD7;;$dfEWn*GvWvT;Z3l>neU}50o zVBt(>;9y{5<zP$YhU5xvaIWA2=L&UjuCTQ>H&r)LH!(IcG|<vO&KbN!<_vYviZf7U zDXy%pEDGBepiC%VxHbg&7|4}`rPvv%>uBg063i3vj9VQY^v!t=tzG+CT|pUid@uO+ zgCC6FnXQPKw*<`vGk*Wa#`ufrhZ872nKLeAEM)@iofBuU1c#>{DEychnbTPz;RWiY zf_wB#4E~@+Ey994;=JOJ9c^sT@DT==43OoBpxGGkE)G!<v1=aTp<Zs0k*)^1`g(?X zdW;K0U0lM#TwFq}O|*1OOmwtN7(nN6F)(p5g@N0Bh7Nkj`-T`mF`CN2#>O7Y!N|_u z%nn+@%+JFw1X)eS$t#VzkqER#9=e(6-$%yljP2kJMSoYhyMxZFXI#K&%pA%f$zb51 z%g4yb#LEaul#r=52GH(EQ0`@54h6M5VnOrKzG9%=DLfL|j97Q4u(KO88vT1#tf|Sb zDko<sr7Y{J?cix`5qOw!fmwmNp_;h7qPU2jq@2CEp`WQQC>sbe&R}$84rS0_@OAJ~ zV`ODeWn^ImHPr+enV1C_8JSqYyDV5)7+BL8SQwa@SeVnnn{Gi3O;8t{iP4{tL0M5& zT1o=6)q+8TQG=ZaQYygrslZmcgIYn#XuDLFx_fBLsq2SEs5;2G*_jwvnOIsH>1)c$ zn@X$5yD*2^8E6`FF>@GcT6yyF+bd}*t0<bQ%gf7&3hIf04vGeyOOHO+4eq<+o9h<k zV`5h}wPP~>$ZKdRVa2DS#Le`N3pCHoBV=kW1{%-)FTf<l1UfrC#(|5Ok%gHHyyFEj z+$6!q$^;%;0!?y*I<n2)3`|Vm!9p3B6nK<^g#{EujNMRaNQ+#OK~hOjTm{l1hmTdL z!^Q^%ML?|#K_)48K2|Az3v~+-7AEG1h>A!TCW*f<JT$Z<MTBfM<}h>oTUHMpx?o^r zF#q4kq{{SzL5Lx6D;sFZwHKrzCcyw&P{_<s2Wl2HF)%WM2Y6aQeM4DPDHcXXmQd*8 zUKU2*$n9#%f`Z_cpse7@H{_L?icG3jX;%E|f_5>+<|&ShLiNn{e-1KEU}e$EiH^?$ z9dz>lKZ6IOEW;7TCCs4p%nXd$44fMo7<VvgZ!p$|jEC_37ht%;06P1}2%OW^h4|T+ zL6e)HeZmZ&t-lP63=GJde#KOjKr2%~jRSE|p9|3@Q#Ute7ZbbU<)t9RDaWUxW~!!b zC8c8AZD7i3&crM%DJ8)gX37CsAEL^*fZ+qP4+9TqOoE$>4LlA5S{T8AzNZhgtcaOM zR9hG{&TR(WZ1%yOnUPC^*G##I+2>yfqllE2D5of=x?v2c`~tNHBAG6M+hum%8$k87 z5a`N1CI(Rd0h}6Efd*eCV5J&Z5|o)h1B6Tr{*l|j{aAhpZOG6!v{wo4u0bZ186(3y zoFhU!-9vwA8yV{87#iv@X$D4y2M2|R1zTHLSzB9z2K<;9)EO)oucFtRjF3H)pj~t7 z=HiT3AvNalm7t1qB_rcW)zzz2PVz!11{MY-1_q`FOrU*(0u0g&Dhw&$n2O}(;b3NE zP-J9g6=7sy1MPlcW@HUzVr1ZEWM*SvPUU1|V+Wnw#*zk_UytSC-~t_^<tr%2!ot8H zD<dc^2;G)0EW{$fBESziAd^{;msb?r@G}N2V@KNEC&*~3Y$|LFTJ9sNZVVcHW@BTo zW8C8X&qtYw2@)5cjM;~edfM8asAXbgRp17#>S2^aikyET{~jfnFiQV>Ym+c1R@6d* z+fWdEn==E`4XFRL7?Qxg3y1g)w3=C#k(ot^k&%rBH1E#B%*v9%1lrgI_9M7O6$)xm z#d5K8Ff+0-Lj5PGC8(vTp`;)!iR3qKTz(VAw$G5A-B>gM>_I`e2k#VXXo1RgBS}cP zZet#J7^^q26!mtXQ|I|XM};$8W&oXoYtG=n@Ezn^?+wzRNqR{}7FN(=9wkO5W?L2} zMov(%VZg}2#pWgLAT0@+Z)0R;!6F+8F~A%&AOQ*>P?g2R3EIWT$;i$b%D~76TBgsC z&dbQf&B(#d#gWFq$;looz{AhX&dKg8?O?~i#>&FV#!|<?%0Pwzkq+9rx|*7d43-wU z=DOx)rl4k%zMiIzrjE9jthAVj03R<mJ1eMO5=68PptJ6vCHb({5ojAAxFZj0H;ID# ze$0%ZZEl!JZxYtJN*`L2Ie_Dhu^l-%GKyhVUPv_?Xl?CZGmPZ<|38Doe+R}TOiGOW z3`&d}m_!aRfJ#xO9ZVviQWSDJ7*ied1|~JoO$m(b49*Ob88$I+Y-D2GunD9DbUy}Y z0t{JA8N)3?Y8bs3@^Pq{2(ybZkRc9-njV-MhAajdh#Dqz_vFFUF#0pBV6X+bnQ4P9 z*v$+~AfGXtGM!@JXAowva4-e!9pYeRO9h>W%g(`=$_d&(2^v3W@@8OXXNU#WZp{on z!jN_tXgmS5IY?2_RGD3w5xOlxR8iGfP?_2EU(c`XSjJ$+$N!!&vj6+ZDD`*ib*sB~ znNETBN!$Z%!v6oC!S=rZXuk=Q8UyIAMs|iOMpp3MDWG-lAHd-a+7SQ>?>4ai7?Hvo zT}>IoSwd<Uy%>^kr~!o~#I7s`eux^Rump{5Yk}RP!@vNF|Ns9XYLMIm(ru2Uri{T2 zLk%+n17j#c4Wrlp+ZbwO85kJNm_hD=*p>Ca1EL1z9tI``IR*wM(B2r(esOyT8&*bU zHby3PW+rfqF~ouf{=iWN>T-cLGV$|)_UVA8m-r<4CBf$curcs5@^V3@<v_cfl|c)S zjRnP(!6Sgi3W6qTjJDbH!jEp;@8xwOZ_}nIMx{Q`4w|B^+ZnC=7@d5-`n_e8>2C$4 zeb8Q=>CB)sYX}M-#z2NS5H}-*4|wf9*zZOlH4M%Scfe|p^ANh4GDc-WY8bs3cHmG0 zau3)p#z2PUIMjf`8LWmOiy;-F1|H6g3=Rw}jK7%d7`PeOHnV{CIx-?1kidAtXw^!i z)yqv*t}<E4WM{l;mGR1z#;aDEtOoD)xBdT+=>pR$25|-j22BPV2TM_5CMHG|Wf^HP zPBvyH4mKuG7es`S(Sx0l!5y^m2sGZ^1sYTXHBvxZ{?&D~ltBZakS+g^p-#|pGSC1T zXdIMPL`<9!G$_MrqRuD=THyrV*ihhUU~8hI<1x9wBvMt+);R9(8F2|ldlSE^BF3x6 z{|-pGYE>43w>s#%Xm}a+gy$}ekq~v#bWh%Bm#oToSb1i9gV#SRA(7djxCO602gjQQ zG~Nz_!wESrqN^!m<RYYo(Tia|4mF@Sf!LMB-~&+uixW_Z#lXO126m4P*ga(o+aYRT zX$@(=1}}pcsPzdNonZklmj!R~1MTvHt-@AOQWO+aW&<5f0a`Y%3|c!NYHTFN3hEw! zDlR51liJ+CsHlXAb^j)MWG03uMKadeWZ2JUI#rYR)7jq@bTb-g9jh19Uj{RVPDtN{ zON5b;Nf>mpu9vg}l*7!(<OQ8c7h_;z0<UIkflOL6Gs0F>f<{=SkYquNDM6#WpaEC# zQc4L1(EcJ8rgZQcN+hYsNC$3HV|_g>4OM+sUNLP(5k6+{>^8WPg>()<NyO9yvf06o z8Pd=NPsW>o>trS_&>mD?HB~daj39jvDM?=qa|2CNdp&7QO=))vWn0w<s}%dha;8ir zV|4|2AwE%ANfmGFP(L97e*q~y4HZLWO=}H#9YqB-X)QHjeXYMcm^_^QmAyde2(qsE zACnq`3wVLHGiZA6|9|LuXP7uULm9(Xa2<fAp3w`mjRLOzKFl1(Ku|LrE`AFp&X5JV zSQ{qJYzP%+XY^-i1?Nw_|KFH(nUolG7>pV07^XRhs4%iIXel$XvI+4pLDqqSw%2L| zL^!B&GO}`kdg+X*yc|qyYz*=2Jm5tTpiw2}c+eWFZcukai-C)gl`E8!k(B||!NR9F z(m`2ImyyB7%FM*jK-XB$SVK)wUPcPE6dSytREJTA4^}v%EGR`Bm;+q~0$P;~DiF*- zJNDU)P1INs1A<J^j&a8SRx<f}8TzVe_!{-vDM$NyCRWr$*&1v3DoQJ-7}^DjN`;tO zIkWz-{P);9&cj&CK9otxCdb1pAzB!;6t&RA7P5j{$<ahY&`DTYQ&&^Z!{5s_<O+Nx zHz<D1{|kWDG&89&xG{iEm#t!01&&9^8e35OdO+g6hyjZ@DBgEs5ocrc0?k?e|IYw1 z2Nb^$bFx5}sQ>>DzD4~w(?2FP4$w*RYz)qzIr0Dh89;Z{^C5|sfzJB*|Ns9j1_nkQ zggB$uCa`<{e_>!?<Yl@AHHV!c>pv*ZLC%~|XZp*az@Wom=3p$w$i&F#r2v{pV`F7u z=4A9>U}OPL0fBNPtmIZw)KF8@QPNS>24zLiN&--M4X$V5>#W4h)zlzmx;kh{Ijrw_ zL%~tDSS{Hlysg9|-@ZP_BtU{iz)7n~tw2;yygJ0&Cpg&0kLfSJim0=hrNiFJ{yytm z?}7#yIVQ^lHE(kVK53qR7bAnc;u2%*BLDvft<PhUWCG=vKv4KI27=ZSLc>!X6b=kQ z;P`T8Xn}`6vUnNj=41@@Y>Zx@HLq}Skoh2U*cbyrTUX)Yp!fodGh~6Ldl{G+j2J4J zIGNnR<9^Z%jt+L9i<uahz?01EjLfV|%&82lEG(?ypyNj3IT%@3S^NdT=WL0K3P=k| zBd;Lk1Z5GVQAp5v0qjV8Mp<!1MR9Qjg@6ATufKt^8PCZoC`m~vDacNAcRvDSgUbZ- z|M!{HnamhO8RQt$Ky|i|0233VlA@HPFb5km6FV!o&K3kukt6DC=t_Oinl1((H62ax z4m0q$0wj9DgA3x|1OTeBg`qVzGpICXQg1Kzw9v59l~-&n@w8I6)|FNIm(9(4N5v$_ zi_uE_j*>~B4`{iaqJ_GhN{VCa1QiQ4d*x(1HAhKCBgve2FT;Q5#Pk3EXMn8p0p&B$ zS<;|3a~C*1(Z$OcE@Ba9WAp+Y5(_s66mKAN*ch@v6MNuOfdv@<fzwM2IK7lH%m#IN z7$9r@elSQfs4>KD6J~?%;Q%drmWON&fUZmjujqqh5KyfLUO6ETI;)}?wssv;MWh3# zs*1Xxgc7ud232Fqa!jJ&Y8td+6<lwDHhn9xu|uY&m<%eD?E@s(7@3(OBA6IiB>Wtb zE2G`Kf<cS@yj&T1K@0sj6qL9gFu!=fr^~Bk(9@Bc8WSU&oSh9S#~^EIK>i2$lZ_$k zF1UOE4W=>GF^9wZQ$7rh;C2Uc`xR1WErr%KY>Yl2BcN@TI_7C`y}k_fIP^Y&>-CL- z+WTLHsgCI^L@xt7V*ukuaQx~0S7H3gqzF|H64wEXgWAxM;P#Okq<zFF0k#Xdy$0ET z0!k~8vKn;HC&=$;dKtYK)?(EQE^oo*JR74I=vXdrU+x=IB=clQUyhNT(GS{o$F%n$ ztd3yxV>pb>&5Yg*tFhX96rBD*Zf0Zj2CZlQ{~x>`buQSypk5j~qc_70uwLYN0o5HF zn0_#+F@V-8u`{VNR)Ot>_M@2C7(i!YLCX3#rXLKl3|b7kw@Gm_ad0w1m&>U`I$4a& zOrTv?EudpHz~hX)-rS5F91QX7TqwI@v~enDU}tZJ>H)1305|H8RdaGe%>^Cd5sRrh zGSWd_UJkVBK~76v3wr9L5Oh7PETb$BuZT9<jv3G*4#?6$@UAY<Tn(h{2U<cZoLXL) zY!BW!bM<P3kB<jv_l$lBX!nep8&_9nPZwzSjDW3EWTeyI#h@KDF|mTF8F>i_ncy&l z^us~<0@MR$XY^qx0f#?YyfZN}f${~o?q>9X)*TSNpnL(*>&sAtLoX;_K=k^?LGlGe zFDSi1^!hQ}z@ZnE-avZU8T}ZJK-~Oa1$0IOlN#v60dSrI-Gd2=e-$Rsejw1vnvD*^ zs*J2GYWz%0tgMWnBRUuvKn+6B9I^%jI|B<VJ4-4DBO4odsu^5#f#z|Ux<Tm@9EIQ# zP8FsUym*8$8JiB!a#=xbEj?W=18oB(5j8bcMNVERZC2161!$%nvSGy>v_1m9n;Wss zh>wXGy1l4XS3rV;Pe#PlNK;Z?9lX299eKx*wm=eevr&mIE29MqtC5b9y*LYGvk`pv zk%ECHlNDs=5vWXngfnRD3lh%W3<)^G8C0Ht!kLZH8+70uv`pv&=S?k8S;XkYa14uH z0VW}Ez6Le$*%-Z`<sPUk>I0XV5WT((*Kp_sr45K)-z-SlfanFK4TxSpMjIS@L1_b| z7nC+GfYXNUe`TgVrYlTp450QTJ5MLWe6YCre-w8zX&OT80*!q%gZ-!t_2UwByZ+w> z?F(YQ$E3#K4N>!dGs6Q0j*X0r8y-N%;=uLG6Q)xP!jOISpdAg|Y)lMXj0~v^TnwNw z-Bi$qh$e3yPA2f2C<6m%n*)O|1LDkCUS3&k@V;47V^Kv@XqFdL6cki6RZL7w1Rc1M zm>BhM5@X)KNle@x<>lq&9)G`r&fs92lMlM78stu<9}IE~6Si_NGJ`i`f+})Fc1F-R zJZMP~CnE<3cw-DWvA2RZ2Q@Q;Rz!dn)FCN^olwx~4blQW=!qGm&_Nzy7y}an3o{dF z=?Vu2Tt#H01L%lXIR-gFL186fbwNQcUKwp+@VFGb$_K3=hi%~i6>n>=Tw!KnlZrA) zOakwP^6_E%5#<yvtS+Fb6_@^RJEJpbd&EDc7^kTJ|NnntU|>uHk5_@m*cb!<pTdYU z&`kywpmsB)U%?o-5E9>_3=E8R;C28+uh0Lt7<%6@FffXO$~&lDpCb^xphHQ#z-bzy z*Z2Qx485-y7#J0qo<RE{Y>d8#AbK_at1xOYonlgB0Bt&9V+??{Eu{aeFdSe4rDIU0 zV`B{X3{}s-z^DbTk0EyXF$AL9#h~%uiHVcx6a&Oeb_QL>U~pRZ`!B$_o#`Qi8u)Ah zb_QS2qJaPZLFcJ3nt;=~E;JqQLN^n%M+@4&g18gfzhX%IuM9dTg-MM;57ZCg=?sUs zGxWbQXipOp=#F6q1~#5f2~bu5?+aq$Ws+bJ0-Y_&%gVwC8u##GVP*pDrvOg}gB>Tt zAS5U(1lq#H2-~=1EDA{`=H`rlPv6em;1nCn!o<WW%DIn8;_bhypUn0!Npx%Yi112t zgRX1%|DPd%fr0Te*kAe}e=(FXXrTKG(w_w9VSlI^S=2lXYK#0=X8a6}Bhcb|Hl9xC z*o5PMWrl;`_6MkQ#KzOv2k|?oY=x_5WM@)m*!%wy1LsCY#vT7JfeHpl|BW#ap_fS= zG#?ML3*=^)U6A(HzdaDW|Mnp1wPO0gpvC}pGov5F>VHu4|AEbCU}WHhn8UyeT3-!1 zFcx&a1P5qe83P06fit`eyrQC@)qKXL?CR!%#)9VJ?4pXIiH6D{QJpGl87;oc*cdT3 z{@W60pK=EjtpC3=Ffej6{b1l`uy^1^F&{E_4%&wX+O!JY+y&x7H>@%GL^^OWFfhP2 z&>IWFtrYY#Rtb-AP)%i&Rj{^T?D)4)$J*uJDrkMhG#wn5;IU|iGA!+1$UaR_eFf?h zgT@{peIigAoemCDP~i?L`#Zt;0hC51nUt8+7(nXTcsfCs*+Jp}*)FW@R#dy#7`;Kq zqJ#QV3=B-5b5ztA0^JxG*qGS9LehQWe<$!6I`RzG4i;jJOw6!zi<rGYJH<U27+FC} z(vv}>PT-R`n?Xky$w-5i^>VO*4q67S&tZk{H8eA41ofCe$NDgW_D_R$Yp^limQ>>v zP!p3hkYoIp%fiIW3_f^=nI!~z#*wgr4pX48TY&1nTBcKfk0=yaFqZtaNddLn;OjO( zr{m>1@QE@qgZ8&DgXS_ptMNb$BpL7!CTK@1c-?<9sKdtq-bdXI+7zh30NnuxK7ke7 ze@0aj=^)I=ASD6X5yJ;MoJpQh9<(6_;VV-?Sm_VmM+91gBq+kh*d?LF%PT7+t5zQA z8Ub3`5owZ?<n{r&m6CCFLvq}|mrN3#TAH!x|AM?jp>324;PM<i55VZd0I3Vm`w!}% z@*F%?!p7(W9m4{(GcJJ3bBJDFh7;g&15@u4CN-#D-vUV9hv)^hs~~!V7#>6PB8_!} z`YlLfF^oYB-QYgA?f-AgDNI)x#2J(r&N^@@GBPtMfKHI}^4<Vi3kf>?0lXBMn}Z3o zy%=-_Aw!opCnGB>TRb}>8(T9QsKN$ClqqNr0_fy3NG}G|BLSbL1fDqpuVRMk0Cjkv zOg0~B2hiEd5G%mFA8h(!8Q9oR4^5ABkO%FYmzR}Ll2iii&jcOu1v#J^w1Xbhb73?R zhfgGmDuPycgO?10#x}v-dvifG#wH^@#-@J{8F~JFVU+*ZB4#9+KiR{5LRoc5NtLCR zCeu}J&>W*b3tP;x{QM=+{xjR#XA3eZ3V`w#GlSXxZ_IT}iVTVj8Vn{3kq%+<jO=Ve z+)PZ2JfNc}y%^XT7@61^Q+YVJnL$Inte}ovuQzB}Eqtg~OH)~ik-<Pu(?rWeO;t%l zSpzZ$4%=@ET0scflZY~M3tGYqJrDpfUaM%L20IKMv~G)OX==&8IZVO6CVm<kex@#& z^73g;Zn^f&MfNFjvhg-HZfuOKHvgRb-8}sLJUp2coAXQ)qQK*{7Ut#_j<u036D-V3 zEj|3aT!R_YQd2Y2QZmxPWf-WP8V@cPO`-MIWk`C1&6_~$nm%w_95hG>Di?bodjBhf z$|@!`22)7g4U+!<pTX|GGH4GWlNxB05ma}Bk|(rWM6!#~n;{LuE_Vh7#+h)t*ciR# zA^8G)rW4qmW>9zT1nWiaPlD8J0_zQfjzvM-2@?N?(97t>;733&8>5#Z%w7S|SyD`D z3}#?6t3YD{|Nk=>{}*7~1TOm_c2zMng2M#eF3^2@pfv%w-HGrc*MDWuSx`)B4CavV zRED@g;J-3x?;w*J1E_Jt#?$#2BF@FYz@!NFmpLd*7`+)>G5iJEcX$olP5|izwIx7j z|0B+w;%Crz&|w8FWoBS!WB~Uiz&i$-yrF9>85kJ&8Ti3VECmHYYb+r>3Gi`LpvJf0 zi>XuZF^S)wGKJCA>vcfDYp;L185kKr_Ja01Dlh~&_zQv7DIxYdva+!@b1*W4R;DvE zfe+JQ1C<J`-k|aV+(!oQd;|@qLk2QH!x-QlkP3_npats4J0PLEC7`<?K`sIfUm$iu zy0}30LQa~5*bQ0ZR0ZA+S>;p(-VgczKLcd05;Qjqo?~PTWaz^POURh%eI_*q@E9n3 z%p9T@G}Zyp>jS<O5!{c1uLFVT1&wt;^!ijl;t--2G}Zyp>&vhnhrOV>0;1Qq3|a4E zCN&0#UVrG?5=NxD0&FjMo(rTGG|#mVoK7M3g6bKF-XLiGhOSouoSz_igBT`)hbyA~ ztANiPQ)7s7;1ghE0v$QR#Lmdb0ITPvL5DmvflgZhcL&kNK0qybMh3=YG(}84pi@Vb zh1FES!@7`z`OwBWz$YYt2FSq-Hg+~P#!C{Sg5n%}vcjfD3Zm+I(DTe9b#!#JvSlT~ z;~6H#^0pG3$Y-1VGqTT8F>(cuYpXEHGD$$=6f|BM01hAU*<|3nWd+V#UW`l_@dDXX z3TiKc1}E4Vy%=_Z^@1v6q`7EDFW7uFQke=FPX_4)b=cS#y$m363egLi>xRr@XZ`;P zu@@;$!DoMg-DV4N8$%hxK8PA5^FgDY>%nmc+6e?Iv)jRLgXjf~fq?Zg`Z1iup%>J5 z1nXt=gO+&^y`VG$(d&(+?}%zI8>6=rsD1_a+ZeN${xa}0xI1w1FfuUlg09o{f*eyL z2%6~zCl&C_H~4TGeg-B`(~*e*baFf7=o3Z;@ZJZ0Mt;zdoQz7)IciX~$*!ix$f~Iy zkr*IvC?YPP#k9r5{_lP!$MjHc4p&xCcY%SCAr`zJ_6mbCgP#NFtQ7_Y(6)AYMrI~4 zMpkA~Miy39@S(eq*(T8Ofb5XHa;&W2Y58^*A4Uc_St$u&LFi67WkzM_0YAt)<pe>? zW8tUVgSKd!nwT+8mp2s_6VMdZ(P(cC4-i)2<yR2U(#Xt=V7kJ`?!wBgZ|myvPb<lf zmBo{VRmYm~nx>p%h979{ASg{VFz;jFVvu3bU@&Bec8FkPWMt42=V9hxV}hJ<#tGU2 z+sw_##?Hac#!<(>#K6wR#GcN;!N9`I!IH)WS~T6v>?0@3&CS4|rKzGMt0AYsEyFDX zS_>*9z`(`8CBnlis?BI>W@c^-TBFU#2pUxcHK9e=*cp}871@Ph=iiHph%p<htAUy> z?2P=P1_D`JdS+t(wlOjp+8XFPN{KNt#RmPm>Gw!dUS3i{K{o1L2)if;hX^~fEvJNw z3<sZ{9{-=iObOEVMiv%^@_M{~=N~x0)C4)>L`v>o-+3VmX=!Ug23E*A0I=W17?c?7 z7(5w{Iq=yqa<N%6a&l=h@^Hh-92-!AVr&Mj18(MHWa0-Mhsy%m4#mvI%A5hZjhTm& zJC%Wpi!EM&k&Ug_n}LUiJsw)gNjo@@tQ|C9&%ndO5zoNE(aqr#>7b*cEFsRr&A{O9 z>S%9iZepaTqoJ;Br(!23Bd#Q&BqYEs#v=v_Xi-5>K!d^<v=7;w-3U4ZYzn*S2C~Wo zBo1GqZfatNyuAncz_oo`77pU}q6PwT`VRI6hBh{a5(=`?lJW|oN%&4;W8TNcqoTsY zA#5tvYi?+4VPRxsZYU=uB?rRy37yi$@c%z(28-DgTn=?W$|1&1a9M)f&w;F`0F`qq z!0o3p=vWeX9TZsaET~=;uwG`QG7K{B3evj^q8B>nZ2VstsZGYl(+Rqb3fv}BMrxC> z@pS%$lsy_uag0?={}_ZAG(oFw9pv~pS-@vaGBYwUFg1fljX)>z2{8zXfd?@_OWZ*R z42UX2k~ny`!e1U;Lt#593u$RHDH|aJ9bWJOeZt%V`qI++0^Gu&tqYL-5SVsB_wq9_ zG=p|LVcG>c`Og?~ZYDVEE#}oR5VDc7kd(HNvJ*Dc<zb2wGcyt96*Q2RHW1_$HZc<e z?H>X8E0Ad^12gQ*L#*rQLD#Xt4jN<>RWxM`{CAycDeTOAkQwnzPZ)R^^uS@G3>xQQ zVgzmdWB_ky22IT}fd+~gco}$=guu52fC7jS+zSI;Jp?-2oiUy@(8J4{#f-&RPu-H0 z>4}Y(hn=RF1gJV-&|oTL=w||*Go=BxLzW42#|3P+Bm*M@Xd4S;6A?3PnJ`kGh=Cl3 zB!@Wq3A8VbDUOka*$}+e-x6#pHTNn_1?3`8ZjzK&kdRQ2Wr_pk4p1&J0_6@62F3M% z6^66mRkAu@7bvoU&zuw#U}a|T0N*bJYIB1cv<!^?s*0eCb3h>{jyi#32D-GUKo{1C zk<}J4P*Q@=#K@TI@bN2v`d5q$ptzdObe}<#!31odCIjfa0Pv<$$aX_UM&@|X9vDVu z(14;SgQ${{kdhMUczBS#!r)nPQ0obF%ND553A$cKgUwQ3&eT9uT$0_0%|c(s)KE-9 zf}JVO+ErFYR@T~8Mpjl98s7cjGcCm!Y{9l03G=cuvVczE24AlSTEhxvFoIVEGcx${ z@Gvm&i1COC34+J!*r1E>86gE9=;jRw%goH2v7cMdOw5Q+L5@2B%n~&egl!5EHZd22 zY!8wNQv$a?|1!lfb}*|la5LzFU7`d!rvP%;C%B=(03Io10Ienl+sw=X+1em%YR3er z+x~J2@v+3QNh=F6#qmivxkz)1IlIe%c6oI%l`|e>c4OcKpFY_RHXpRei;<0yfeCb? z7!T-T1vc=3tDr-Gn3))u>o{20K{Qi1n8C!H0KUE=K-xhSq!3&wf#h+ii*%3@7vtt) zV33st9W^P)&n3z&%D~CMDZ&YLIio1(UMo=545}2s%?t4I5k$qo*ezf#qs1-FUMwsv zEi57>^<GFuMo2_TirI}zAy}D5E<`B@WnZzRlsE&Z1ZQAis$=>9Sx3Og&KUfk=l^L2 z&W%irJN}=Bju(3{&SyBnq{ht7Pzf6RW#HJzz__7lgRwTG4q$R+`oN&Zpb^Rd*3Sgi z4;rh6>Id1vbOWqL8=o4+<zO{BwK&cB|BdM%SdEb$vKl0}g6+D<^npR0!3MMv1)GaN z?co1E7#NuOm_9P7F_x-=&HDHI{~5?Q`TsKzfB%2|{~MzRvm28dBWP_PJ7dWIM37!k zr2<}i!3J9Q4;|MCVLHXc#sIo+9enmX;}@n=46F>+-Wx#kU!aLArdUwfZvh(ev;^${ z69koepz|Z35)3|(4&0y{mKi{2w{wVTGm3%^#%KKU?|2l`ser$2pmTDW7(^Htm<+*Y z^D+21aDy&kU;)))pri@1N{E?}k<kKj3>s)k+!8df$`4Y)z{HRQmIMu;3$U><FtG8l z@qu>T3bAsCYBLIpDw+xlgATo6V`FD#^ndjC@xz<7{E{4;;(YZ?r~c-cnKMoP+sas? z8=|5Tto5%092TItiVtAFYk>LSJ_49;3+98ymzu$R(8e=n@Ldw1v&+F}wS&eUz<XDi zSXn^V6@ZFLMhDP_Jy5N~V9DSkBO@a(BQLC?s;11&Aqm=+g)-d@=_Eo{EwG9znt~TP zV>`RN@!uuz(q{C-%cH?}yn*)@Gl1vO*g*4W&^XNoyBjnw1v$4FG%nA>V6_!g6fk;0 zqLvpDtDrUnIH*C*NG1l*24^M<P>&WAr92Eg5`rwCC}mWJg|9LrV;m!6{5%0^P7W!7 zx$%q);O#_*^g>irLUjMxf?^w#KNBJ6^)s<C7{J35G#A0f0Gf+nW&j<q$qwE_#m>OZ zkP2~>0s|8}<irm)P)7mOi?aar##v)QXJ=Zn`bay-!j&+va4@p6uqGmuf)a=n2M1`o z7zZ~8Hy0<;m8qZ-Nf@-V7K+Uo_x#IY%=s6=_=Kt2-~ZobKfiyQ{Qbdh1&?JjU1efp z0Nt$)&KFFT;Iq!{Anp`mU}9o`xDM3pU<79jArO;^u>~#%3P#ZU59GQPRt|A(&}|W* zdm|E=BqIOK1_c~b_1{*oouKpvnh#}TFb3BZDhv!vN=%@;2SOb9K?gJNFoBLQ2aoAO z93ci;Ee&x5_+Sjs42mUa-L5d`fG%bx@Hx~FSAqft<PsqUA!kPsuuDvZnL!7$YcZ({ zGaH#1D~hVTdct`7Kc9gP_ruq!J{pWnUX0p|T;2hU|9=0w!RQZ)Q}DPQIBl7O-3;pA znSuE>;Pr;a|GzPPV7dajC5l0vAq(P6MHWT|ZcZjPb_TX|4n}s+Iil>0>0FGgtQMdQ zV+lHS4YwNb@y3>*kd#tTU|>*CS5Q|{l$Vo~U=U{z7Z(&17vuqDFYxU#d`#eDX_!HK z3d~JK`ItagRSFB5fkvX$826R5S;)GTMMg%KyU19!6$i#?q!mV%Bx}SoU5Q}wsPHg| z_cdV3%VRS1kJEFj@M4Ns!ekI)9+|<y*z~WCB|FSASpWZj2I%?fOl%C+;CM1+U|@RB zbc#Wm!4KkJ2{zCbJfPcm(mB{!nVFdpMJJLJ10y4IEL0v8JD|~gX$EOQL19rrK~YW) zX>CSP&@Dxx%BG+LEliC;(*=UYf<|VhrcCc;UCN^(qRO3RqS6W?ic*+Py*7^X(!P09 z$1B$8&8yg0#wGt^VxvLR_Mn|eOfQ+PGAJ^HZIfYxo<|8Pa3w&;wSgD=IDkuC##mNH zMo3Bqon^xc>X#=X)PRb4VMg%nctQf8c~8(ml<XWL+MwYK5ixPl0u9LA2I$O1@RT*= z%pDUo#`cN=uh@SNHaSH2Ov=of<P+|&@!x}3??R@w=Ax8E&K~}TOA3-E`1?&rDqLdd z@8Ps0xu_YG-U#j;0@a$}{S)}doj~(W5zMMgYK*m@@oC0j=o(P%|KAu7g2fvl;-E9v zK=TIw1(+nk;-Ev%*%*UC&A0#m8G^v|D##p2y&C*K4xA4_W0m^g`W7_K$i^4~G9P-s zUj&m9gEE5wLyiNt0w)tQn-usKNyxSe(8-^W^E{lrIY0v)3<)gkObnp<L)t-(fr-f) za+C)HBO``_NC!z(6-EXfEfoV*134KnQSb%5%8bgK94HIsK%*j{E4{$$v7w`qpwr>d zR?Q_<RpeOvOR=#EaJzDFvq=bY2(fS|2&n3Fvy1zh1i3jzc)GcAb$515(9#gp;1}}< zP`My4$-?Ns%AumCWZ)T}&X$sym71Cg8pi;wp#|+95ogc?jaacWva-oCvakv=GO{px zF|aYPvaqp&uIyrloIv6PD&oOas3oXGux0WAO{-~XsH-Z<OXx}J2@CP_f+o~JJ_8>~ zjdDf+c$YP3LXAyS5WHy!RI9?y3t)6{&an3LijHk8w(?hqu*-4QaM4uuHV*Sk@H5r+ z^YCN(s~zIt5yHd5l@y(xCnWIi2QzzMs)47BoBh8KA#opfe@`zjTRTws#>l|>Ux0~` z2~_kbImiogF@gFJ;G_7gL5t*U!3#LV8N@|J!F47xACssu=$194Q~#J4kMWD~#Pf>t z-;6Hg787D-VHOnS$YPTC`&BbKTuCM*R+CBS-*YJkYe4}U2Qfy_B7Y{(93PV!(+>tk z27S;eO`zI9h6QvZsTZgUhzA`}Yz5kA1s;jC1kImoYl;eUuz|8DD=3Q^AzzXNIid*E zjRF^x;7ltF-dqD2zG1X?O;T4=4Kdc$lCm<2PV6l7$hS9-x3+LekCu0|P!9C;33g`s zp(NreDyJzUp{^lqSQ=hYXOiOKkl-Ms;A7$H{%;kBjJ~_EQ%Gd6GiYv&fsp~6*O-(T zG#FeRoa7l<m_akVqKpinohS^Tl>n?>pcWMaBP#=FMJl*pat1XzVV+`OV9;PtSJhEf zRs^@GM3t35WulQ8G*N>+V+!^SbYK;tfHB!~dXSBIn4)}`U2&eGnS!o)akN#ihN7aY zer2#5qqRqjp@x)zub@bzvxc!eCo7+|p^KM@kQ={<kB%C+j5q&p%XkvJ?nK@}MjBKw zg2ygfKnL7`F0HU)^bw2~X66vrW`*Bv4BeH)WX{K=44QKS2QcV(D8`fO65$eR0>)<X zNe00=)~q^?N)?PH>9uyE8a`$|jv6`v^73BNd=jFZg6YZLwRst)jEqd4PT`$-Q5u%k zvfKioNi*;|bx9^A1`&put%9IqKcEYHAt!Ky&**gqr3p(=7}|o53j&q;po7(|LAeZa zSTCv)2We2-C7zX$fdMQ5KK8*HY5-^*8zX}NKOf{E9}z|oHb~L}uU!Le0st|<%NM{0 z`dZmVxJJ11vO-Su{kxgT-N6z0NM8nK@E#Z$roRll3?dBDpgY=NJKNYm*Mfld4?01Q z&}9G@iejSte3BBP(qhs=f_x(UBC5(9pvEa^ryF>e1nA&Cb;#mTW+O9mMKLiZ8N=*| zno@)8u$qc5KJKiHMy#GbeqPM~&N8_%R#vW!J-4-LUCim`_O`b6znv2%_D=$hPl3yM zCJ6=s$k~q!OdQ}be^84VoYNs?75G#`0R{n8Wl(k(7DO&#)l3Cfs%nVo^YWQV>A0#h zN&IUwFkoi^ooVXn$XE@kV;C4f>%o}V7=D7%*#BSuzcI-%{bNvPm=CEO1UMNP7?n5} znVA^G!L2@N2NWR|CT7_FJ8_T+kl+Hf1mjsjD_}rPK}d@Pw89GPWG8RX5*BbF3(j94 zB@S||j4Vu`BU<1WM>>Jh2TVaE<fv4226Z)2RTVZ432kLzbv00QvztQ3WyFQSLo=d~ zaW&9Ig5cYF*`7-asA&sHbMq<-89Qi+t`(N&5M<?+6;#z0@Z=B`WMScy6|7?7^VT+G zVRC0?Rq?X=CuhdO=*Y^hs&iJt+EQ3KK>goyP&)urpGYxDFtIU|gM13=$1?t6l3);I zFxtunI>r#%r{MwhX27W%+^XbZU}9iu0gEyCL~a$45@6;K*A|4G!D?y@x|&%zI-65m zl$FKKRVG|DIyjz5;@^D<CwmdQp2dloTbBoe(t{Z^|C@pOeLSGMiy2v%LANTfu(rS! zoPaiygIAeYG5E+RswxWFv2jRgn}f=KNTFnAZotL{E(#z=k%<`_i-L|Q6cGcJ3ZMp) zGNh%duBK+D?x1R9q3NulY%VXy7$s$y?cx;~92T9R<!9z&t0opKt|DM+nwWCV$k&*g zjnC0i-&U2Inc34t$;H^%+&RM0*hGq3z!FlF$nc4aatbE>hwM31XHsHf1Fzj-WZ?P# zjj0%1pRI(XInbmiE8>7(Y49v5RDhWQp57$cSeTfYSYsJjS*^W6Q%&Hb<G^i4NQ#q1 zR=~i_YzbAx0BvA`E)ry9U}mgCRtGT%q9hW0n+s^Y6zBkPZY~ZsPy>L2Lqr=sg>EVc zK4HexTv1e6P(+N$f=`TtLs>lkad@r=lYFL>9UF@)D@Oxk<Ud2k^J>bT|JEu?GcYmu z{}*6V1D_Ku58D4G4!@TgG^-1)Yn(yN5lBBCTJ(vENK1;yi^?mDE3t8igF5rzjXR*r zF+jJ<ff~H(N}{5$)(4YXL$0Qe3@a0JWMiI7ruCaiGCmsquC9zc9ev_rg6_I~Ow65a z4mCx`7D-BEq^75W(io_YIR$PjLT*N50G)%$<OMp>22{^5#Dk_9ok15=i;8ftF(@-C zvp|Yp*aRMCI?@B34Q<9~so^UlBW0%;?jz&v<_k_essbiv=8>jOB}__!Jf2+qd6_oO zYWxD0nmU1U@?M|>!^LHq8l2i>2kPR1%jrNSP<bc|I^`I&ZX2A`tw9wGB%~O8R8<94 zm039?wN;VIV`EWeG3bS<ppCkqTiw+x@`Li*-J2u7H>n%^%kc^h_R`jnPf7XaQR33k zlu~2ishVRKnv>@5P@oMO2x4Gl;QIfS@d%RygDB{ZQ4vN4&<)DaYdk@r!V(KA*1+`? zI07xfw@!;Ph>EBx3xY~4MsNxN4fjI|DK%5j+$Q6Zi>BIQ`T~6BvU+|76FiwDy#8IY z(qm(FV&jtbaAq`CS7u-Y??cmPl3);HP;pQM4MTy;7w}nPpmsVVgP;KTmJTsSF;G(r ze31=kO(LjF1bNvQykD8oI5Jry!p%3pM}R*<K%kz>x!9di_20d)q@oJ9JR9qLui2A9 z#TsbOh5*w%CM5=01`W{3x{Qo03`!hKj4YrFepy(+f$i)KatF8=w`KB?my^?w*ANsG z(*}7JbO$ncn>6I^WzaY)8@N{tN(-Q+S%RSB5Y^_osOswb>4kXNMHqNjdxUwFx>zx0 z$m$pe8Q3z(>KkUed4_ASGHOM~IhXq<);h2}f6ii6krdd@XJG&DDyw0vTU;{8y^R0A z{uf|UWRhl32JL-RWMpMfkP_!(XJKPv<p<YV4B*>QKzHnb0}it0%$C`QkwIQoQbJge zhl`CBl$t?ZNns-~adBZ|X87T|;4w3Gb!KRl1FCVNjP%7_X2xBL6;f5@73buZ6V<g; z6>wh{_aum4Nm7`POP)#E-g*tA^1qHZdU{MuF3hZowpzRX?PHvERYRA}m6;XnZg4rU zhv_?m9OxW1Sw_&z4yXajYUK@TPFte{F#`jG9D|&QprDvIxakQQQUFaMiGr#SQSe!~ z;7At}7i3%&nl2y^F2L_q;}hmn<{WMipvUz6uNQk-mTRe-OSw~etu^cO=gbxrDU4kI zK6{6$u`q3ixXI%GH>PV$5)7IQCJu(2;0+55(7KtOjS18p1Qk*Y37{&?65QL+WYAO= zRuxlL<KU3eW&|fSHf1GsWpKcOre2Lfr(S~7B4h{wJQJ?YG+SOqC)6%SiB%!jE>uTG zK1yEB#?Vkd#7Ni3Moykd!mYtCpvomG%B3p6ufff&!9OwC%VPR;JD21{{{}bE9O_q4 zUl_E%7&6Apko7+S(H91bgZsh^Swak8aZn#z9n_}=*F9{E!Jq*~@H&=nO!^>kMzDIu zV9-d&|Nji&umFosh4jrqXDCC)HyJ;G&l3Uf-(kQw-vnG<d}Er$bc#Wap#xH{ae+pi zKv4|33INJMI=?`Hft`Vgk)0_ObPqeYT2lj+{0#BTpzApFKqJvA*px6ct3%XrFfuUc zfksHg85tx&+gLznlkkJi2xgFDl;dOv^~fQo2Z2uS23_e3I-W#PR9O_XOhHtHjq!=0 zw5+z6uAG(Ij>u(<H*W4v$xGwn@#7LS&~vb|_%{<YMtL+g!Ywrc8U|lLbI^<lkU41d zvtk$^cOR!QU1gAEaC2~$Vq{>IW@KdGVPt0FWn^Jy0Uf&v>OiJ~26(`$mUTg8Ie4{* z9%!|QgqQ#yXy-l)GlMLnEIT`RoQGWzu>us<e}wk67^i17@iNBzTf|yhX&9m9S6DK| zEns$`W3r~IatKpvS&3hLrvQ(iY|PU9yrnS`BEEv4)l}fQZ}6R$Ol+Waide?8i5LR{ z?c)I5i_65upbM_A82^7|Zelvcpv<7d7zVj0hl_;~H1-HOBu?6U17ARd14x7oyi&(Y z+IxdARD>Na0$O>DtOllm1$6F3WMm}7Tu@7fospS6oq?4VJXWdZ4IYn<XMrUhh;7=S zOXJy4)G>gT<3f}{QW1pB09wloat<*@gB(ec(GD6|9Rlv@p(SR}@vY*Z5hgVi8EGjA zVL<_YNW%k^p2d+C<l8ZsD}v4r1ywenb9eZd*!h@LA%h9Z%1(mHyxcMZa!Mi`9OBaa zj3L&Ul_qtaOV)9Ti*fU+*)#63kBvx3G2mur^=4()_f*%IbSVGdC9@dI_#&oiuf<bL zTw)C(9GzV)z;O$jSC|3bS0)1;3*lg7WoKjs-vy=yI@pONj+2dvg+(27={@KO3h<UP zRYgHTK`wSFZDY{Q!h*&~?o||oyEQS2(J0yq<i3uj|IRV?+Q&pBBpX03bcQ+!w7bj~ z>>9{gR8To1%%JO_1wETD71VoXiv=x9R|Ai$s)Ji2;B9Tn!is_%>{8l{(2Osr3?8cj zHDpa0|9w4_k{xB?yeDcY)2V=?u@V0nzFL|8o5fTW0O|vQ<_EICd44lA&%Xh$J3(Hb z%l-cwQvve|CN;)AAT>;C4DzUI7#JB6|GR+aGPuF_c{4IFwt(ssHE^3Z4%CTJhu?C| zE~TxgEUIh@yXCs_Va)9)#?;6?dzeoBZDWG$IR-@vXg@c2P7!)mjS15!2GChGh7NiR zObn2#;29agQxRZ`m}5cr$*42?Fff3QT4Ug4;87G5WM>CYkccXR3Po^QQir9XM8*~W zHf3d(>6P`A=Oy@srN@H~-ZG80PRa+z1*A*_-9-<X%Y^VjbD2zRpfZwyiGl6E3;2vF zeg<)9x?x}pWME^1Of9K-vxDx^0Ow$Ego%p@3c%M7bFxcnGYW#%4}<y|#&*o+;>vu? z?21fI(P95Y!^~A2jTBfuE@F|_w^U&C1qIH(FMIYd^4QH<xynD#YSPR;GjJV<GUo`I zMrB}N4q-aQAP72Nn4gD}jhU610dxc@69a=9=ynfgW_4yCArKH`1Em{AV~{f#jTsr0 z83mOE8I>6o{=H6N6bxe&O!@bkQ7--8%dmeh(i!s^qyH^qjDp~Q6Bx7pO#@+2KSS;R zHzqOg9m5U|w#tmmEX<5d%#d?r85mi-R2W$pJsCiYjF`bY!(nv;Xi*^pxRO&<QB+aY z1Um<19M%-p{Xsvh)!0aki9N<QDsI9At4v+J0>gyll8m&(tjquhmk2Fwt!m%mN+xB` zP#zYZNVm8$L9rll2@fY1J8yRzGb<%&TY2pi21bS?1_q`D;CwIVAT7Yg1YMfMpa#BZ zMV-k<Tu@wqg<V2h7<5G*8@n;1F=S-eT$xchDTIlIk%h4{C4`xUk>y`m3e%~76-t4w z{7n9T%@|u$Lf!cPu7k8w!1jRi&_-w;%EQb<3=B+JOs5#QKzE*Su`_{!%L_C*qz1ks zU7f)Pd?N_Egtj6$o{R+<jTxD;a{ld&iej`*`*#Wy8BC|Z(X$kk4*q{-N(Rp%1v+r? zGctpYKV$|?jDw1IG1&R5YM>gLAs(7Xr5&V@Bp4XfL9*cPj~<gx<aTjYbwL4Ec1dl} z4b0#I8PsJp1;>$@IjF>DN{)KODF!ZrjiE)fvM{KK=3{J!m%HFH+0au(ZSs+PNce!t zU&uTqVqO<MPY&CE1e-gB<U>$-!^Fmz3(Esc5dFqrK4`8rlIaQ)8)&BL|9=LQIYtIX z25_0ibc#U=RK|+(b22kBfp+sTLAFjp+i8%L$-uxM#ULfDs=~@HuFc5I$As{*IV0kN z7^Z?J3XTyjGKL}|dXnZ&GXLHtA7|xbWntlFUBh(hAESG`J_mawC%1tulkmTCZaF?K z1un)OP#FxJ-(+HAECRb7eJ(KZzYEg^rc(^upm9UcqMRU5S6R&)w6uwtF^++eQ5`&J z$j!hF-j~46E~%|5sBEeTt&^B8g#CREDq;QqeT9~>e_I(CLHjwGBEe~2(?Jb1*vrJo z2pWH42Gu!?=@3g87#P$Ue1w&Sm0|G?9*_esI0tnA%ms~^BBQKQ?NaihY+bD5tfLrJ zMI>V*7-j!`u(A{tVFaxO2hVjguLk=;-a!VmYnu^LjWK}w;;3~4%-M>9#>}h#Y>Q%) zjbfCG1{HryRgn4tQr|Ft2A|0Ysv{Yg8G^uLK5E{O$xozELE5dsE{zf{Ad8qkhy6Jc z#V8idC?16rJZ(%>pb2y)1`Y-WrbaCF4g&*tsDfa<1G=3VG>{`I!ln$bch)WYUzk#2 zSk+ux5*`#4o5ob_4{Zj3s~u1jgX(3_{m<MC{0tJH5hVsj1~yhkwp7r_gc|7Vex^9c z>MC_6AJED&2{9o7$e}+B+zecbf}HGP;6@3|a*$_{8YFqiC5DwPjM-6t4;6+5MaF=N zE@<QE)L%0nCudjCo*O3cxgq}GcoSyOcF<s8WME-tWJzTJ)g%ne%uI2hBhbNBo3J1s z55#IVG^^E(jld0(#HhC9a+8MUi5=?`VyZ#SA8>2Tx5X9I24iH9`~Qu}n&}FI9H?&r zy0ej)k(EIheCjO|sBZx|lNH>z0FOI^`xdaW33S!0l%%+*03Qb%sCNYFTL>$$LC^gH z)%&2%EA)_Nb461{HMJK;L5ixHSr)}py?kbt*oA3ng&8z9v;14i7{%4d)GE&JEg}}P zFs*b|3?IKAw_s<3e{MBs9y0O&H|BdxR~bMnu)8693FR4B7#P`D7*iS8*_q>^O&?H0 z98|1=dU}u!qcU6>Gc&kR39AQ@l{%>7QpLoi4w|_IkJf<tg^>>2DyqtYf`Y=z9PBcn z0Ys1=)zm>Fn8t#}pxY{thRKwb*qHCR_ok<Hd$FvHidxO?)t;Q*>-qOdc5!KTR!K<? zctuditjvF#L2(n`>l?i^zkT-X_V$^xK+Q1*F$M-EIi^z#e6Y3-Gb^JP1L%Zy23BU! z>>4ynfc&fu8b=Tp7362&W8f212e-l)nU#gXgNMeTI#ducN^NY+Xt6W%gqpKfYukya zoUo{fm<&dZe>WH{c=Q4s64U>hF={Y|TDv%TfXhb6*+Gg-Y~b_pm>4Yoe`8W)`pY20 zpv_?Dpr_8q#Kg=9Iwl@;#0F@QA-EU=Cl@_NA30eSC0T7bZD~mnAy#$~ZO~;aW@hH% zd`#?Y;EO#${btBGqd0g-12m@2E+WTdnN{XkW5%x>5hm~G!^*~I=&F;EWtZ=f-}1{b zz}8LI-H?+{*`Cp9!x1SyF-~7LJ^?nqoC0ADnYQexa_d!zA!$MJ>PCWwF`l4hUkuEk zoq$ZB`{o1~BtY}84mzN7-63~ff%ch#4!VqGU}V$)<#cchSDnE}QCU${5xt3|4DND( z&UOOz`o%$qF^U=+nK7~0=o@;cmOOuE8EqPAZr54a%J1gV-0I}UIMK+0i-q0N#wC(T zLekn!O?mQsJ&iT$kg=jr1_s7COcD&Npf(67*1^m4G(bH`NF~DxUXRBv2I_5q+KO}D zM}S5+jhH|kZzcxe|87i%OcLO^H)96_(3~<-dsqX#JuD*3&&R;azzeRo(AvYGh*9Qa zVmD@d>=*j4J4D;nmGNjftD>p$S0;&nM*sfo+QrBc|GCE1Wnzl~s2&B41u=r>AwZE2 z+WyVJ^cp;$qvN0{z{|zX!pOz|N*$~$%*@Oh-b{>4;L44GfkBW#5Y!wN0#%)i%)-jd zg35x-%EHXX!h*(v!b~^ren@!{^djZM-GrM#x0odU{rUHXQQ+SPFcx8C`S%BcAz`Ta z{~P08@SL=lgS$E-D}x&J9s|&AbApVZdkmo07o>uRF+qbR`k=`%STcoNI4><JCc@7P zIxHGomVsA*vx8S8fpR&tBr`QZYZpI?$u-wgFjLGgvx<{fiZ!<hp6Oxb8f<T^<E$Yc z!ldNoY-q{L!l_`Y;T$a_=r16pX%yw5s;j0ZDCh=St_j+=&cMK=3l29~2Pqb2CI$vj zSixEuf((LUf?}Yy5a>2sHg<S}m{D191rswPqkK*fBNOAl$GJ=r|90}~8}c$9{QH8@ zM!>|F_n!kenLy_$b(y{~sWDyzjl(gifreP1cC#``K-<8q;5|=XpgIrk1VJGoaD$u? zbmt!^o{R+<CnY@nk@hJff=S}vzkjblu>y)CP``xz|4+uZ;PWw3y*DU<Mq{|aHxh6$ zGPp}SaKkx_o{<oFc)M5w)bhpFF4h38%0O!u3#lq2w~LWlh>DDFBj@qz`C9Tya<K8S zn>h+HIy0VMc|MMr#fgdO*%zpvr!Yw{2!l>x6$0G>z|80co@@nq8lFZ$eSJ|eK{j>? zZBQ@G#LQfoS(#Z`3_L$BEX*{;pP7kyz1J#cX8+QaYnUYdzF-U!)fN92$hclt|KD0B z2@neuCZMwxG?*k9xIy-TR@gByFsHJyGJ%R4P@@lAor6}2gL5S)OMuqG!uqLe8I>a< z{@smWlDL2W?~C{EnIvF&4|xs{WC!TZ3U<)U4tP}>XdNNQKd>wZUA_ovQwxF?FDeT% zNk#m79RYUu*?-0mb3-9_<ub7`K+ar&oXG~7YXqHd#t2&f%Xpkgf`Oeu+Cc(T@ql;h z!pbYq;oB^r@B-C{%BG5p$Jd9iW!%ho?w=9kIq-R_kbQ>|Ol%CGeTU2pX8%F+BtIC0 z8Kf8#L1(JSNK1(badR+ofL7&zHk5#tuQO<YsyEOGkR&s^s5T=cSHdSm%uNNs<KyDc zRf3FD{VSY$Yr;TeeN0G5OmI|4DASJ{9Ij=7RTH>gykK`N3#yvHb>rWC&h+T$3{J*r z|FXC;qN38kb)FCd_%2K)Hil^MJay=QH>Naj{3$yqfEtV7{(uJfkRe#Z7aVm0Z0u6n ztcZ?)F;iODKi3FG(FmViyFg_q<GFvJWwQ*73|<TjOakD1qVJ%?#>l|P4Zi0PnmW>1 zS-`1-fr$x{ID{C4lm$V}D+z63Q4!D-m?<cIK)ME|i_(pPv?C&P!)=43m?RFz$V)x@ z`{KQ<VweZ$xFAs7$E*W(zp8^08!Iz2EaAaA0}SBPOu$|Mr9MVMP~#Oe!e%VUtn*hh z;(5diP%44?z=-i2q}&bt@6KGwB*DNB>eGSxzd?}BfQC04coZfM?tIif0V8A#1~hsH z#?0D&5r0h}w)`{v_irbPMNAAr3=B+7OcD${45HwcIRiT*3o8Q)=)h#i5EQu91~-P( z8GZP885jilc}4j|89-Nfs3>xPl00ZI3e+b64cIe+YHrYKQby4!dD&?tuU9ipnK;GT z-HA!!j<2_OB;z@+f45d^7#M)|zeC#&Tft={s3m6RV2s>10Nah!HvsK#1hvDs`MCLb zc^J4DIN^N*m>WQ$3-20)Ms#PSB;_$`M*RIzZ|UJ|+rT7o$H&Jz5)|U@>V}3OM=*in z5VTL2fs;W9JmSZ|z|6wH3<?4b(3l@A8G?oh6hPwz!p24r?;110nkZ}MUrfvY9Kk5z zYBzPdg(suU43H)N+6|T0K<-A7{r`<|JChQmKOn-$!V2mOz|MJL0o@DA1U+aF+>F)- zHBunQr|2>GFfvGpK@QJA^al{5%a9HLXupp-WVH!s#%;Bpv#gw(j$aJxzaNb3?EXHo zuF{gS0UF-Lj;@tl{Cqw<LLt!}K|TUp?z}=4RVfM8ptZV8459zOG4n#_{^KDHea!xV z2BhT<DxyIB0hC6*255X4<_GjfKB$eaBq%5-qy%l_gDMy33EU93vx1ihfx1{qY|Ok3 zxoLIm9T5?oT(zmLSvLQgTz!3ATz!09LFc13RK91DxL;V|SRI*~nVFiJmd3!uAi%)D zc#BB_+^*Ml&|rej5Q3JVfKHiZ1*c?q(XFT~D5wrDS&Ws<;mvtdWpxoTV`EcgCAPA( znetXD8DSqHn(AHc>iTU>AF`|18TbVKeQ|Y(zRpHNSbf0=n$H963}awp0H0F~I$P~O z=&qw53|b7`4txq|XXb!bnrX2!vT`u8aDsM7Ffnn)GjMW(gBD!wL#G-*=O$=@rq?u( z6mv2#F>^AdGqAB?Q5xwW&B&mtECbs83!P{KpQxk7sKpJQW<zXWhMlMbI!_IJb{uGp zjWIYTL<L3I82iN)czC4*q*clzT_SyaA`Tp2W@MF$GD%8u`M}QV!NR6&z&O1jCGOup zrdY?QD8~q4H9<|S*z|wtKA<&?khT%y9ME2M#xu};o}jr((7j6E7|+7;188j{Xx>el zk(EV;k%1MQAHZkLfcg%gRa^Stz61C;A#i?>6c-f{782m&WQX;S!K(^Pz)puQ_duSC zV`DV)Dt2-y_0;r}m6CCm^$p-)WdHYrHNa2TO-|N@%d#>trN&Z#*MpnSFTf)*Oo+#a zj~^7b#{a*8_nopagn{cX(9zUenI1B+fhx`a|FNGT{r~^}lm7)6AnJTUJr4%<|38^Q zeI!s1=_({&ae<D`VDu7XWOSE?wAVl)>`)O0ZXre%2#3)VG?fNAyGN0Mfs>Jqg@FxJ zI6;QxHNeaNAghbi!D|>nEkkXvLUvSz4D9Ssb>LzRbeAP)v<-A&A;?%PszKd0b&z84 zQa3$jpU6lDF)2yVZZ=6pDMbbe1_>2qK@~+V(C9y`2v!zU28}C18};Uh8sT(JW@>8j z=~`Yr9}5M3Rz7w!Cqc&EoO~-!S9tBP@?0FdBO~+kFYsCdTs|{{8rwn)(x5v_SQ#0( z*qN9(7?@JQvsfD53>+Mcai9es>Wn@j!VC=JqQcT5(x6Shpn6wDksCBI0C%1#w7@YH zhSya;Dza0PO3uEEiu!OgufWRFg-PPBkGFT^dr%2;AMTX@{~0(T=Or?+F}wt)2T<FP z6`Z~=L(}(WusEbG_Kism+!kYFQhNli*O_F%?FCSKhn0b~1>|lGP>+cr4z#QSb^H)I zuOz4}s>~>`JAzR>Vp|xb2LJnFC#d}knlJgw9LvBCnjc|dWM*P+2BkO9l3Vb=FQlW3 z)IJ68JX17f{QGZT6r%@o>|@aGGtj~s@b*f^c}!av*csfsH;4yBIEaDTxoV&;0(jO3 z))JKlOCZ?<lZbQx?Vv%nOc`_tnJMGEj}f1lwmkT!!z3{WGDgnBz`#5eG*`y}K7)h7 znNbaO4&ncQ2DCFc7@QgIfx{MEZy94QLA_;+sz`c4dm$k9X2H&6K{|s2VlU`CXwY~s z8$(t)#Lb|9MLPR~!I_}|!(K?dgZ8z9+Td&q&d@uYH5i^Uo?!%?p(W=a4c<QtDj*qS z!L4vm5dquEZEmV4s=>JADR}c5BZCIRQ|1Fi>tmh))(2iA0M^F@ngwEDVEX^>zY^oz z|Ia`(+YDM8*-~5_<UyCfF@w$;WoBXm--QT@WM(E`a7Tkz0(@f_tD-6BUT8(ex&IOw z=Yw#R!x0At&@zdu42zjgF>o?SI*2jBcA`P9F$e8#W#$#t7FLJstY8;n<1>(O<P;HL zWxUF5(rBs{9tv7F#>0qY2B<dx3Ju04P{|73AqzG`9JD*uj>&uuJFlUHBbTrME7K`% z(*{%3h*0JK{~6@}*E4KmC}Cu0Vq*~gy9#uR6XTA*t3bOvK_)VQ>{tSGCrAXF9db<K zpyL3{i&zDOxf~@8_}H0FDThX=nl_knGcbY7V2oxu#lXQJ2wLF=YW6V3LiR&9G5BzD zGB9uoatiYDFmNz%2!pmlGlJX<@;qpjuQG_i7|kck9?#BeC|SWL!x6{Er!UEL%BtOj z+oZt^N`cA;gk78r!dUF$;$mRn66OL8G=TycY!@tmlwl0U5KvG#af%4CMzix7NH}l_ zAqN^P@E~bg{(n7V9yt8CVByDv9Da@r42%cBXE~ZX@N#p3j&5TFT^7X%y$6w-fswHV zR6;a^+9)6%c!aN+!6$Nys1RsdJfpg>vM{@;xVkaBIpcxVk|`1k7fPf^u4a6~|BsjP zGk-Y$nKS&Lm|*~&igSeN2ZJbsg9GRqZ6WYcETD~4Y|KoIf}l<-<N!&KHMlI55&>DN zYA&iSu51pn7EBnkg9I2QSFe^xl~}PtRCL7(iByTzt3^NZhw|;*$rsAs-_9S(ziStN zD1SSsT=ix6$f(Wul7X8+!U1&tD<k-PF3_MB1Ear+637nZi=TZR^z|Ga_4FMWbDb?M zot><#K=(BM|HO2jp^`z30klb2ok5V14O~YEGI4`x!~fq{GQfAT@-s*>s4^HbSTndX zY=yKAjD$D@nb|~mgqgWm*|;(oSQ!|(SsBv>7(pu>`I-5c85p{}c^SF6nc_LwnV6Ws zH*#BH*Tm1r%-rM+(aHxo_J)b68LXQ<o|BQCy_?-9(!tu+#@I+*O<D?a_ky#djjOGz zrMZ!{v9+$YnxVR(qP&!<w5qtMfTW-#WO@d2u!ATs>|RpHvDK`M%8a0Ol%S@(sR=81 zun!`mh{Q&yP*h?Qg{VF3>Feq1?(N1T`me;7vFBflnzFK*FJlH&m~k1D@$VBvf)`9N zT`A4VDlgB<D&_iX#&pUgFd!gk(ZUBXK^Scx5D*x+XaR%-#j!f$2}UOH{#s!N0Z<<s zv}L=A!ADR49KFWO$}6H4S}~pa_YaacLFzYw)w6^0CZq%apNPQF#OMRsQj4^i7L+%I zg^ih&g^ii7H%2Xd)flx9WZ%Dkj7(qyL1nuIBR}{aNw&?bjEwNRm!X#~GeU1$2ImcR z1`9?$s9I(QkXmt3(2bL+D?v9*GG2#VEeXlf9{)cvt^%L&%K}bkEDX;;X8Z@`We2eP zp=EXxG~<FsMi`g{LFFxI*#x7*KOb+#E#SSkr$B4s8RZ#cnbH~EL1&>d%4^P2OM{fN zjPmeuR+oW+i50v~!`#7yfrWvUk%cvtjggU^k%0qT$1sCx7p5lg(a_Btpo5DA89=Qw zFa{m>Yz#W>O%Qezq_L>7Xj5n?WAnc&jFSJJPoLi8$i(jd+dt2-3FH?BMFs{Y38u>o zpmWN?K_l~^wcwJB>}=pOQ@J2Jq&XQu`=8lZSX059z<3}@A9Tqu1Nb~`Mh0aiMFn{| zSs4j&F;OANQFPoKYz%6QYS056p?wKa@ZodDqRJv{pl*e!nHk8nU~FQ>7+?{uBror& zX=;D!)Tz^Ia*PfCIQ2~v6B85TnJ$Y9xblfuIYl`7IXOBD1bA_IxjFecI;EF_L*3{9 zH>RiHIY|))9fl_kd|HewtO|_mjEam*?6C8X^ch$=8QB;?_w=zaFtW2Srt>f|Gc&|; zGlI_R<z?jJY6bPWnG!e{Aq4^C?nq-|w1El&26lFi1P1U?N74>vyo_ATtjt`jb$GQi zplOVZbWjx)1zj91sw1kSsiCSYCj&a+Pk@g{gja-%lY@<cn~@uI!;vt!5MTo>gA)`7 zP5FR^i&(({3@QcSo7GH}P0h_OwU!x{q{)XjXiCLI2Qhl&<w?o&YjE>uE0{a@`pGk$ z%1g0HxBvHw>6GukXU=}EE)^Nh7gROaSRC27oZW6~sDQ5YVPIqs|No6ipXoA#0YidA zEG)f9GqSPDFtW3;c<C@QGkS6{GBGf-FoAAag{GS{aJm5na~EjHgFT)BTvjqN=;^Ae zC@U$*LHeQ4>-7v64R}D;)4;Y8vKxykvx$g`h=_yN_=83kA&cwPl}(M+OiWF{iO9^1 zvBoGuQ&q-WeTM3n-;9iRuL>pxsr$>zdmB`CiUoKuGJ5z6_b^=+lknh|+V99E#KkGf z?HC#>#OKE+)K%ulBf!fg#^zW(5p<rI<Nx1GUzpA_NPy21GhxVf$lzjRVio0P0-b~g zx_^$Fk(H5&5p;h#3kMqq3mfPjb<jH3bOz8}%<Rkz=?v@)oE+?+3)NZJIKmk?IN0JD z*f`kyB_$adj16_QL7Pq`Wh7;!rGy3fc{$h^Bp4-l*hI7;w}XRrm_c$4WH42fjg65} znNb+&WD|C9JVOR~-gqYI2N*~j2?+{n3umTF>5B*(GuD8M|AS#}j^UwhjuG$G^>o$M zb@kL4>;1!Ry_LAxP1rb#3OPAUIJp?TL1q5mn}Olsfk9zmK}HS^MkaQ4pt)_c|KAvo zF<oI$V$cGuyw{W$7U1V$V`O3U0&P8KVPvQS4GS?cv!t_v4uD|@2dzs4&A~G;`GaoT zkQC=&V_{}cVpL*dgY2p?1`i6WnVPb(ffm}EgEmw`r&&RF(FlXbQ$)=e&6Q19jaj@s zHQltu3?!uV#ANMFtXvJl%+!Nz)Fi5`bghCS6@45FIr!MD+4<SuYq)6mx;f_fi?<tD zGqJ=NYhG|*0B>YqKErg1L6kv}L6^ahA=V*^kCByunURBq$%~PZft8_|ft7)sjg>u} zhmnn&k%fbeC7pqTiHReWfrEo7mY0)@nT3gi$yZNLSxH=sfkDqu&(J_$Q(Z|{Sr>91 z4TC6ysGx)(9~)?0p)sSefgTfRHH<Q&vVk0vsGur%&mhEypxw~0``+126-DDy8C^ju z@BNehZD(O(VPa}?2nld^2n}-4QB%{=QCAORWMn$^cZ-^hgB0V0zh+E+>bB0(|4P~- zf`Y=sgMuQgEX^%UO)bnV54gI5x>gK=j9E;3nNBf?Fvu`CJ2>!jGcz->GJ0`waI!G7 zGO;qXfHtzRvND8%Xa?3;c1BhPR$oz31_n_XQRr|Ng9w9&n4pj#HydbX$yitzG?WjY zUJ?@pb$>yJItVhl#>FK@J1{Y_q?#tMFf#r7n##zP#+b#J@b4od|Gx!iIP`fyECCI+ zi;P-kX8*2%`;VYAP+o%8ZZU$&Zzcw#|L>VBn8X<*8I%}29o!V9nOIrGMERMSnV3Ke z$XQq!S<*r7WMXDzN@ZYXU|<esU}k0jjbt%1_)AGKGDs;&Dap$U3xYO{GDtE?a<GAx z!h<&HLe@NpDnfQmfoArFO(EL~MHx*)i>>v%WAr@BT>j;V`RRIwDJhFvhlB|Gm>9W> z3;NZ0cx1RJIM;-(6IZn{6H&EOWwdJ-QBxA{0JUXU85I8iV0s5`3kWc1GMF+rF?2cb zLGEX^RS{=qX9lgUlM9G&PzGHQ%goA_!3nN%K%Ji^Z*E4=P!bml2Qwq+vQcRVb!@6Q z7#SHs7vHe6v&3^Tvaocs_(VD=nVSiLZ>u(QGIui6mzNdP6w*Yx&lz-3A?7{R;B;sX z3KZ}NA843T473i08N445vibtD%0XQnJYAK?2)e~u6n@DwW1gp<u7bRxn!T!dfR>!P zez2pgs)~YwiiWh7hOQc~qLK!vy@P(W^(_YrO*26eZ8a4`DN}P*0|7xZO$%p6Hf}{3 zDOp)5DWzK~GO}vCtOA-cGAaxVtPFzxe=z-m-UaQ-5Xcb6XaK2<Wsq-zW?^LH0<EHt zbTDS-W&m9U4U%SqT@;Nj4ZRE6l!23hi;<HngMpC;bR#%JIs*#}YdjkxD=YW}Z}1>p zt2aL*H#d7c9|tcpxRE36y#W-DmYAB^*udwwf(PsI8Q@?`yk1^L(AkL~Cve2`F@l>E zk&zDC!9jZ9%cO(ig5!L>tu1w3^<1%CB`pBzN`TkR8$;Htz)mSZOmBeV9&wE{<lZAv zQpOa#mrTck(+y)c!PG+3&C?vDB_9S>2Ic=hm_Tdgm>HB9tQp)HLKqnxB&9@|SlC4v zIaxRunYq{*nV9{}G?}@XSiK-o3r-izpmf2^%gDqHx+9#8jUg6Pn}H)3+-&Iurwhh- zJ`NseriJW71*Z$bnt8x^86{q&9n3*D9dWX=q=V82E;F#`jEr>9^7K$qW@TYu@Cfk? zadtG+SGHEMhO`)k1bDbulvtG*Kto~zyrSCR;sBf|K#MdX%?5ULb7N6uc35e^NOA$u z4UTAY1vfEiS1DanQv)egetCh<J2`n|1)ZZp7>lu_09AR6@<NoC*I7VVT}MregGJ<m zc`g%+i-Vi*zsICSJS&62{~t`>z~wO)gEE6YgAKzh2OdsFCI)jkL1s{W4~c4RE=Cr1 z4t5rfItDfdb{01F3<l6KOQ1z1P2N0=92|`C+^n3?2zJoNr<xNSqaZ!3@!X88tlg|W zkq#<`20YxLo~(h5p^dhtq&T-Sk1`7v3l|4F8!I?McwrGD3_7xbne>291P8q5s=JKp zs~ERn31zIMa)FKEzt(>w7RL;X45>_UjJKG+G4O)cKR`~O!gKy5Xf_+%#8Wp0F9SDc zyu}_KpA^U%#HweiXUfX-&D+<<$4ph-1T=oIk12xD3_Lbww~ZaN#~wB)1{wti5A8Cx zfJViHK!dytjG?e$Qbq>fNR(?%5XUAmnz4w>@f)!5aWOOf;1n{m6yp)T^A+D~}k zzcPcv|HBOIJe~i+S8FmbGVElEW3&RF-H7A{>3|6ENIaQtu;LKpV-9DPP!wYN$0KNJ zAqI8`sNDd%oN57MD6>BU7lSZpy#POaZ#V-p3nOzV8!HnFXcHj|1Nh9hSO!LBMqh4j zZeea=K_Njw0d^iqZAS2h8B<|nL1Sgm{!2DCc6P>4Tkpe1J#9M}nOGIL1DF^R5=<DS z|GkYBwUFR86a=-k7#ZXkr!zV*`!L8cI62tMg03Ku5ny6w;b&xGW(I9cWq_QF0$J9Z z&Hx(L1~s-oEqZ1q(5abn400-}D$0UjS0I%hpjl|hx_@}#!l(@^34+jy2xcExE=S4w z42%pij0+f@nSB`87_=PJL07ggdVx=&Z2^t6GlqitlChw40~+pUV_*{$WZ@Ch7KY>_ ze@K2<zz9z0ps;3UkYt>}=*k?zAi<!*U<jJ|k{4oQW@cjaVqs)pWdv=hNM~SXWCXV; zK_ipQjQ&!RpxtPaDpD%E+@Nk58xL$~jvd@yf(*k!t4nbu(72m8xCzA$ZZ$zFUG)vn zCXy2@cd96;XvoQ`s_H2!X=sAlOUxmN)(}!rXAWu-@qimgJHSmMaO;SHL5xB3zbo@e zrl$<744e$S41x^e4AKnp4B8A<3|<Uj3~>xe3~3Dc48;uP47Ci+4DAfv4E@ZC-W#|A zA{_Xf7!@S-8HE+}7{#QSK|2w-QAGtnqUg#%Q>Gx@eWu(@j7+>tj0KvEs)}lAss)_v ztjsJ7TqYdGY(^}G988Q1OsV{gyn;Nup$vi|Jc8k(j3QDJe9SyDB0T8~lIDyG!jcN< za*Q&vjM8E<(y0t$0>WaU^5RUwW<pE?O2Pu^DvV0XjEbsCimCd#+8Qis>gsBt5SE&H zoQ;9Co|TTJmIbG}n!2B~_Xd#bTS#%0s0foF#6f}>4ib=&P6atd%uI-x47czFL^uq# z5OSCR3mo1$aGWWss?`l>q(w$XI!x~A>F8)_sjI7~NKe<*5fb9#<KhCHwAs_&)8E(I z(cRJA)!EYC(%#lu*Id`!)Ywp8QCm@4Q(aYAURGLCTvS+)o}ZqVm64X3lAM&75FZy4 z86Fz!@9X7ZYi(|3Y^Y<UYo(>3q68X_1TEbV5*Grm-ry7D6BOX*;^pGy;b!Ax<K$om zo$DzmXv-$1&1h;Ys%#3n_Ya0yK_a58%BIF(j5O%Nssx>pHwA5TXJdr)>e(SpX=U(| z7f6#^iH#Lp4k#;wjQ|-1T7?ZN8W}+xWm992z2N%>p|=i#Y&SMF7G*Y_V*2k<>%Tp% z|Ms-}TRSC=Im^`a&onbLMt3_WCp!>kG?L(F7vK_-ur>R4CdAZSTvSZXR4?_lf|8N~ z2s5fHhzn}V{tGcP18M%}q#_|CEcwp}!u^wA7ROW(Z~FHg$cA67OzX|2#F>FX+!V98 z{VgpmkDUSooty&#uCf|2F$qc@Ib@{EsHmVRp}@kzDy^ih)30l6tgCBm%r77yzpu5m z)y!K|NI>M@r<RtMf9s~io0`T?F$Dwg_^jst6y|+QKNy(7rIoXTgCHX#gAg<5G$|%8 zCPrq^oi+^342+;-wV7F%TUZ&HSXh`sK|4u6JH?q;{8?B)vC1OPBCo2;#wMoC$;=9> z^}wYrs8=HnYr}$yU{U0uF~-k~r~jG$Gh=)uqNuGM6QC8Lq@idhEiWgfsHVWHAuFw^ zDJ`qP^y8n|KQqQt)#8$}Qc8A)&R#Mi;eui^;<8Foa^~u?ZOZcU%Bpgp>rVfJ*QGFO zfz~T<K<?`E-T;|10L_yw2AxyM1e&7)?FIeDl)&_t!I;68A=DvAnvsP`ijj?l$%_*- zrq9H}$OJlnpM{N;B^_~(loM!>6x<87WcD#NVPvqjGO;za)zj5blb4kc<KqF{pUz;+ zXv_mWmjyg73R<oP-6{`CWAJ@0q9UN`37izw)WM6hAQhIFxSATHLSB`Sn*$@Gb!Du3 zpaP$u0FM;Au#lmYp$4CxMMNwkW9?Mu;0SGPgWOo&2&TWYT0FU>`7@Wq83*cW`bvrk zaj@EP@S3PA1?p-!O`gIn%f)07XJsGep`)g1t<0F752~j@gHq;97Z{`%)_ZTz0JW*O z7#SHPM7f!n7(u5b@Nh9QGQ+r$kje@)O9?rM27J~n=#VAIE_j3l{7g@Xp$G{i9iY`= zq72Z3F~NIe;F1oakOOZ}BpH1oBO@cX2}!7^v2aLeGuknkgOioGDEJsEK4x~%CIDkb zNl|@W0Z}6j0Wl69K|UE_ZZ#ck!RVJsjH!lsNpfmxEG%x!LW&}uF-dy=zB39k?gWja za{vFvYzaQ!MTfz~!BLcvfrX2ajhzW}aTXf`J6jzqXk9%^EGHuii?uiCx?9EsP+Qj$ zw3AOmO<74^RzjRXj6qCPSyhk=<UG)kH=rey&|^42`9ay#oK+Ft2muvQ(9RySWwn%S za%_$gi*j~MvXV@v8<Y6ICJ7a5BLjm_6CEQfX(kCz>uT%L;;5+NQtKKkHDyoJ8tYhh zFN?@XI~R`_>ng||OJ*(bdJ_ZCo>hi2#w3ij8=(vg%weFs0oqN-z|K&{SPdTYWBB(T zd?vIS=uBt^b|yE5SzvL!fA1L|!0+Q`V{!wZ_rk<r@$WsO=>NksI=_A?`22cD2LsUg z^^)>3|K3{|8CqHx8e162NlDAfN=wNxFoDiuW>#hT0d6}6ZWHGRT`CBjvX^3DVrl^` z>H@VH8N0l}<B^c7?4=!~K#2=<Jv*jk<aQMeQ8i^&UP*06$i5=bB_iPAels&uW6<&g z#5qQwV~oywP0lWu<l!;7AgD%DODvw9+tOb5@7cnts{FjF>O$}qEy$tG=7!upCfwF3 z;-NL5O<HZUW`WWjXb(&qQx=0bs7LIe!^_CRC<>a`1E2T=cO&>NL2wr7X7Ewb6jfD% zx)55XfKK0lyARcY>g~nu`3|imCQ%ByVsY$zmR15n|FS{HE%^imd4blLI@U(EPEb+g z^)upjjTKe!)q|}xWnf?g-7Cdx1im{wa2ppR6R2elS)TwpY_0{gIh~2A2^75Gu<Qn% z3@#4xS2HW<Y%mlV$O&??46@3?nj&nx655QSU^nwIu?s@N8Z;~qo;EQv6=Z5Kl#ONQ zQ1!62sEF_Ei;7Qh^jBdzmB{XE&8;73&i(JASGg<rP9%e{1pQ!B#`EAgQ_vZ#N0_o1 zWEmp2OL4G*yAqJa1`=$HtW2!UpbZnPAO|zVGcqzTfR}X0phz(=G=t<}9*T4TUC$xQ zAS<jStRe=wjDQie{|s@n3FyQhPzf$*4Dt`tUUm)@cN-607O80Cq~u7@m5m0{OxfJ} zA(m|alH9e_W78Pp{;g<8iDMMA=J2%w^@AAybu;<>cVISU5CQEO7GPv#;%8$5ol*jd zgBAuRMn>>u79ck<G5V{hi7@kuYcq?mv9p7g-axAqQ&2iRpr_1g#;&5RD#I(yA*5>K z#B@tdlfyKWO<Y_;fSJ*RO&6T6L2K$hfXfQV`8m!EZ5U+*WDooXCN)Oz9(V?4h6fmG zK<SqeT%LgTaIrBsL+_9Ttr^q>iG%kGu`@V>?we*{WRU#-o4F1=Z?Ep4qAJ0~jOC;Q z&}LNzHDO6v5zyWlaWyq2bz?bZabq(xb73_#C|k(fn2m{DS&vDb`JH*J4QnimqMoq? zE4w&<A}0@1EVF`vr3{OWCsQ0tQgncVovA=OYy2BVrlyz;3p<TveKZ-_O>zuE)-IW7 zQO;<&@9e++i6*U`*8iFr7{U7m{xV52C^Kj{sLG2Av4ijE1#LoOV$y(Z?Pp+MP-ak; z5fKpvIg%0N7Bw|>u!D@{n81!T2RjzRX8Ow>6&56CBU~1s<!8(i&Z=oH!^|ne>&3&% z6v3oqBFkdt$-yK!>EDlflV<Br&Z`e+#INZyk}{Q^YM5mhxM+q`*xN1!Ch*$KeDEG{ z83s!SGiF9ca7URTm4TIkgPoNlm4S%~yi=7ej+2p%O`Xk$fdP8@i4f@Q6WHk|h}F=r z4ae}^&WmQwj6QWLF0<08syjD1E+i&3gh|3DEzPIK40ITakCU?tX#X$7Ev`&041x?& z3=R&qoQ!M?Y|RWzT#W1-Ozf#F;01G_0b20lRpvMb@G%y`LZH(=grtO}7{H4olobU9 zL2FkbH(|qe9e|t%U%GmA<;v+d(@WlzmOksBXzlCH)RN;lWx97H<9W}Ve|f8+8!Z?a zK>K`anXWLXGAw~)c_AT2HfCW)7B(eDPDW)$22K_)X$LexCQcUE`I$=WptTf?oS<#N zY;5pt!JM27@l2rWGQs-*w2%~mHwCk?fhQMqLCag!y%|8NLF?H;`+Y%+)D`4Fvv#0; z&ai7i8B`fnp^MZ}xBfzAl);NH#6S%Lb5S-nrj@x9yqK3nMJ?wFo}6KkqOPLkZ<t-j z#~Ac)4NGmgeu$<<7+2`*^nc(zz;WIFQX)PA(iMe%b=?9ye*Ao(@+0*BH>Sf(N(?F( z;iAaM$*6=LE)YQi;i3WYDXwq<FYyP53wWa>mT-{=Uyq0qE-H*Fh;R{A7Bm)xY>7q= z7w7^5CL_0eXO_;0h%Qc-9A`}*X(=gtSzkYPMuva?IcMrP%E~x!ndF6omNo~a8wm1v zaP#>Fc&&phc?Z=`pms0hd;?L?NEQPdXuSj*V;VanE329}3lnS?ALQ~E@B$tV&;oEq zWoFQNUu9-xa4o<Nx<-t#kx?tY!!5|IGnP?Hn}uJ9LwTV(w-|V?&FkMbMz4R{m`+6q z=otw|B+7t}kOo~4&%nTB4qhWIjA6e9lKp}L@Qr<7`-PR&!9$De%EIExZ0sQW8H1lh zC7b%2Cq_NCVBr&$ZIP7#?frTE`t@rj32QwgFHc)g9RofafO#Rb>@WfioO6H<c3|LO zOy%ZeVrN$aZM$NIZugS}ZEq9?`x`{?u*+&QBK$543Ik(7W6&{!U^l2SRWNEMwYvnm zb|iEki;8+)r_C<Lr#xGQ2NDdv|2Bh8OafuhUUf#j2ss<&s04W{P+Wl8q-@~*l(L|+ z4!Ian-38tb26LB`Bm-z)CCE)640abHOw6In-p$38K_jl9$YD}`9GPP3Z;}#~+8Y+O zHr|4TO@v#zNrqbp6iDyEfJx%P18ZSRX?u4WBL*f0ZUzRXvrMNLWEm6~Y#pp*7@0WO z6&M*fm_Yl~L2IxWn9><IIM`!B%L>)JL4gR`W5B>5FUKIuAS)swC@3N##KjIeyq{f; zNnBi6U5^PAVoD%KDT9t(Q8#5(6ldx=<K&|w=oI6`CvR>PWn?PH=a}FmsO{}|g7MP7 zS(}+oIelQ!3$(a<Ti-<V-&)3CQ4{^!cg+L!Sw1)gUWddf8v_GV1(O8Wtu_vpGK@@Y ztP0Xh%#3VIUd)W35*f4v1a!U;C}kt3EU04z1x3WcDN7XLRM2e$Xil|QWM-qpZy#aL zE3K^-sHr8z>kw|wuViDkh_U(Kn=U2^<F%~n4jM}qDX9tm{w<)cv}lQjy*k@kV~^>e ztOh!lg84GjDF!75Rfe6Aogmzbj7;1ds*DWWppGVt$H~nEDvv=MJQO$?89=9mFoMFH zo0}t+hl`1WL(Lm>odP2$#HGDA=mtbMs4{ReFmZ-55K!!(#sE4^l?`MXCj%QJCtE55 zHd8_SKNuKPlo^y5loaFz1%)B;$qUK?NP#4%EC`KHGjmYn8Vf3dW0iU0X(u0TLFY(k zekC)*Xd^QPUdK2$Nx4f=QH;??7%%;s4T{*0EP4SJcW)b-2>n~f7$jz*d;4LaG^7xJ z;}m!U9A=<=;mst$pv<7gP~pI@#K^?OuExm0#l*<S0b4dF&j8CT=?q+4?6KgSjFe-f z9n_c^897)v7+LEWI6#dAR?sDam<l5uL>L%URX~jzK~VYyr3-NGLh>>qU6`3cvl`UT z+>6X?6a`?(Lrq5-lsq^^xTl7NG4d~jC6RTk>h>B-7AdL={Q4!JrnG2@ga*$CCW-rN zj6J3>Ffz#f*J2W34rK^s$aTo_WoBYz2=d}$Vc}qOV`S$LU}R%gWn^Jy^>SzAVDwaD zWN~NUU}R_HV6Wo@RnDLdzo1oJY^>~TX`nSv%q)qZ!}#JEn3(iHXW{$%xj5O`SX-JJ z>uD&<N{Df=F@!RPf+`$FBQbGN$f6VQ1OjxtkR5z;h_MlDkO4Hx0NTk9>JgelhPKdV z?m-PcQ_w|tc1(;*1f)3FggM1c#Uo-8{i0HurkY!bC>iP4St#ik@o||MDwx>nxcVxj zRwPAMq@*QfW(GLe#%bxOhM7v*i#kZyND6BiF^94;Su(L`De}ZQdxr6_@PxU?<+C#J zO9_a{Ff(&=gmMTnGbvju+qkkYJG!`9!_LZ*RArZQ6yp^zl@iwj4T^!zc(!BQ%QS-_ zgdx)*%?A{=fu3%hEUX-iu8iy){ETeuD&W9X#TTyW*uvG%*V)n5+RDPjNJm{sRzi%O zl_7*N1QD*F@jqy=$}xc!Ho$@v)M<qTyRs5w)eGTJtrU>t;1v-!6ti}<&{r^5%C9mp z7F5zRja5}t<K)&6SI#iEc9K#Ga`TAr^>YKCqoD4gAun&GC~79`AYmdStYyqJgO$mW zRZW)9*#vyFg1m`_b0i}ppE$pmI4d`&7Z*3HvZ<`1xiTZ8hLN5&?63u10aht%VbC3h z;ySY6J{0uM7bZ5w5-{KYzW~!7rteH_j8nkls-QEiAa_NHG00*)*IpOAu?^g@*JJRJ z5Em4XmJ*khkQEUY5EB$rQ|17j+X3ktgHj!+e<!A@#Kx|urUpCN{(|@9Z1{~)|8^7< zaWY=w%r7V^Wc#PfhCKcdy>w$^QBmXHeI1pRpaoKpbNAJml)z_yU_W<XAH{tb=k6;( z-3N_7P)`Ew+<n;SgGYh=PcJWaMhkXd4_`0Vf5%yT;X@CO)kT5+{(=9}qr$?XK<!mf z;Wv}%0)r$&kOQ|EBLkB-H)sOO3o@1{1lmEy6c0IP4RV&2w1XJ9+XIpT56$X=wic^{ zF4W;?WDpSo?dSk4I|Cgd!>Gi@&a4bM2h7aO+#I|B4V3+v(v?-j7N!6DAGcUgf|Em% zKR{gF(m+U&hev{`&B5&-1JkL0=7xT1>Jf&2-6J&EnY~y*!^og{Pw;*@Sq4dlG)RTS z4QfmZf-dI;*KDBqODHcAqCf~VPa4k*+T{-37N-un{YZ=fbjS%<2ILW#M5F^hBLgoF zHy1lA+z;Ra3zV^#m6_ounwi@%F@E3?;p7z+m*K50JswlfFT%vaBEan?sHo2*%fjTs zs-g9-ib>+%TrGP(QFrBACRX6HAi&`b8Vg`b0gW4S_cO&o`uRxv#XxE#LH&6q&{<OK z-2F_N&@?W<vY+WH1L!_9GX_@&Cq+gk7BfZ$CVfVB20cbLcF;aoX3)3=qb~T`0kF3q zDIT<*8?@|}O^eM(RasI9bW0j^&or!@1*drA{nDy%?+P0;vw@UBXMD`fndZ8-r=+(# zIkjh`w7LHMmztZGoRpWB%A^@v&M(f+!Ya*|9s5)!%2!f_ms?&?&r(nZX?PBPcG67k ze`nP_lq3T+7>)kU^NC_*c4J~O(RPQ-A(;OcU~y+sVo(F0U(C)B!{iAH8V1|{N11*w zvoJ_87=hLd=&Ca@Gb(~s+<Gy9Zu((lPG?|ZfZa&S!U#Hno{>RYQ(l&jhe3)_3Y0Qg zm7r4q&|z(GX$iU`9J1vdGKZ}W@iTZ3nQ^v^Dr<DDLw_f$s*JP>dw8w0Q%wZBY9fb< zl!OXrOl)inhl-?>3P*H&ytSk(U(&3kNe4w`B_w3|5@#eO%}nH%m249i78VzoTv9w) zSX@LzOk`ql$z)KQ-2DG(rWht41~CR*20aID1t}&bMr}1F2GDtPjG(KnnbMI~dV&tM z@YmAPQUM>{A;QKEju>#E4)u(f7`qARzGcud1hC`5Q$pfqW=t^!$sFb~a;)*Cg()m@ zGExex@g>pLcI>iJQgUn#R#uK|vQkDo_EwC%CB1?MV$uRVrNuphQlg^Lf;~lnDFPyb zf};E>!NF<#B7!RXp!KJ=|GzOGW#MEHWKd*?V5oDb78YV+XY$fz;$ZjGW8&hlwPNDr z3TI^FWc6ZTWd^NUtz+Qg;N;-q1l@hX%*w!=&cMje!o<YR$dnFRJj5OkTJops&CSTk z$rjHiz{JL;$L6D~#J~_1;^zapF4@M~!pz7(TT>}QIYL$%G_s+jB%~||I?4oc(Iw=X z2T&tJRNNG_830tCL#8L0p|u~VK4b@}fKCaSn;Jt_#X^VoAjtr<A{2DCHydMst*M%4 zkTDyFMTCotiKexLq?DM9qRqnC8e6-P05xwpB{37(ps2J|d2`7qNlS&)wCEsN6EP(@ zZ?%9DJKLI=40m_tetuIkSy?-28O!-9!qU8)yh6gl$};Mfe}2TT%+FmIDIw}CAUd<9 zeZGY7_3I+y^V?fyiV8T3N<=Qq&0iT0T7%BaVD<kSvmbLIgCK(fLnuRsL#w0&6FZZ; z783`%n>G^{hm|=KCl`xXFe4|UrvM{bi1RZtFhGNxg%K^p^<g0{2nlf|MPVUEhCqLB zPf%0N(!$hOUspp-F;pp3NI_VEkC%rFyqH~(QBVkUQnis7QmBKL;fNc7LXRC5{><P7 zb;JcbcqRvwfEnA=JOho{IV>ZbWlS7w(q$BFt*j&@(zC0JQj{zt<s>vkZ0&qqluTqI zWXzOYyzOm;wZ!EmEtOJ=YO*W>rDXz`3uR^OWn?V*O--|vWHl^#l|^KDIeCQC{(R_f z>6j$Q=giHUl^T~LAv<@DtYl7HY8EfIGoRq3j+TB6B_+^N8~^`9#!;Dznbep9KxH=j z6^4zFG8^gsY0$YRf0+(3sWAmIFfg*QUrA<U;MmB>xPcM0rHPTj=Dz?_8q*a9DF!PC zb5NBf0I9MVK&QNd2JXOPTA(Rj7Cq32xEAQRe$Ztc;G-6#7^T=Cp$r|L0q<`!GZ$1g zg(hgmITo3ks!G8|WgTU$k~$($H8b3T=Q3Rp6Y&)gpVihpi<SM~-e>VkvvZclf$Dwp z{{oDsn3NbKak&RPmW0bal8lnL-2>Xhz-X!AFC``AEE^mc6ri9Z5>emgTnfJVf>R(V zGc$pQ^WUy#E&VAqmJEyx#{d5@=`xit2s5y4<^t_SV-#l>XIBSfb#rxdaS&$QZynXE z)Sc3!*b`|J*`wH-+^f{XRH8IH&@ymMkY(U3#W{hNL2Co8f*2SWI2c6!r!b#n+QGof zAkCo3V9wym5X_Lskk4=vyjS_00%*#Wg^7`+PKldSk)4&5jfahur%pv!P??{VmrsO` zSENo&T2fV<Ra8tyOjM>$U7kr+P8PJG57azL=U@a)S+cUQr3*0f2r=^U^6;fgFp5Yq ziiwJdr8CIL$;pH>$jHdWGswxv`6nfXgt)m`SZHa<$nfzoFeK$C<>zK+W~8NrB!(o$ z$3{m*gt-N~1qb^3`gnO*xLUY6JKEdXSZSGSnVT9L8tCcBXv%1+t12rg$ni<@NrQF? z2?>DChEoz!(qI<{Z>?s8^qrK!r5Zc9$QJ``Lk2Zn6j9{ZQFy3wtVGK(HZvI+8JIGw zGpkC<DKLLS;r(5K!mUB!F&#qT{`E%TGV!5s{~bW#GDT{em}<%J^T~GW7(*B^`W=h` zqrboyFuDWA0FCE>(ufQ53Z@(2eb`c<lj|8w8SEI`8N$J7#7B^kfzjMlM_WT(Szb<B zTuhjkho70j#@fgbbk~y?3usCUwBH?cfin|xDCl5I(4ItQCSQAd1_pa~dv_OS$QEvP z$eq&gZQQJ4V&d%TYHI4Nph-@3b2BrLh$y?7n!2hOxPPo}$}SF41QHPkoxiAVW(HQQ zY-VQ8B*G=k>cYyUDERM|xQUnnhln~qQ-_jDfUE(dQs`e%%{UcywFot)4{8x=nsF*d z9Kss>iejc>|FS^x>PjjBjOrlW`dq@SOgEsav!UA6IY3GcID|Dm!fes!QWONKb$P<5 z6bjNVD+9JuQQX84WRYf^3b<XC_}_(@fq6CqFM}$BkAtT?BL^F3#Ugku3lkS30~-@V zIs*qcBPR!EBIK}k&?PeJEIxdEj0}9Le4s6Gic*pi;sX4j>xWpF8F(3ac|hf!k{WCQ ztuY&$usP@kATx7iMrF`+An0&PMq@!^X2$xY0M<ZOJyTsX)?l`XV&7n97ABT|r70oI z%%GE~laoA{nZ12Mt8Kk~|2XbpY?1Zxmi)V(>D0e+MPC<Drhvb#|GqMS&Mb0g>IARx zRbudU@RAi}BH$4XaBCap5hXtGCQ$HZMWQ?c^#|y*Kx1Y`PZVEx*n$rXe3TQ+!o(<_ z6JyU52U#%e?fd8DPDWc1BYpmV_DmB0PAGWGF&_M9^zSb?F2oram_TR33o}SF*g06U zGBSV;hhkt%h0hs*rmWOJN1ecCi$p~j7$n3*q(!A6!@jDDsIx`jfd$YmOH*T0VbDHH zQxmn_OO{0C)R{K*9QE?rla#hKf$3C~Uz=%+RZ>2al3&!n=70ZNT`Zt;X|hZ|7(i#P z*g053=C820ih+Xz<{tEuS5%PQBM52F!JPwLQMPu<lxg|d86|Ige4h19u<>zc`cdxu z?;q%-FeW?a^1r_zGg#p9dGOuX{0y?7J_KlG91AlG8|YM2(0OhQ%%CH*7#NwsHzR_w zHE5;}JP@j3q$sEkI<|{Zgbh?hK{lQm8;OBdxq)gb&~TNqGLxZ^rKPdEfpd7&k?zKS zUH+mff=1?!GIG1(8Gl%&$a`zY@`ARR=KW-1a?%PGu+;7Z^#!13`Y}i|=rU9}h-#>- zsq!+jv9d^sGcmAAf==;+jYi6{GqSKi`sUzs&A@xW81z8BaV^l&07V8SCdgqv;BGqd ziAPX1kq+XrGNAJ`WprhAg#<vmP8p;br8%I7Iw>1KZrcLiF9->4uz#7A*g%^Tjg4TQ zRbvdc3lwK#W{&XSWs&f6NUqe=G&iwVv<Pxxbc_%avNE?77jx&*Q&7_4xx>uiscg{I z9a5^~tr^0@_$Ve$FgcseThm`E$O3egDQIsk0|Vo4@VU~u4qB|BSy#}0QczzQcF6-| zdXs?xR0V>jCIq2(JSd79i-N|7K_^Qy{{DC1+_@+(FQ!ugpZxwEW3=*PbONti;9y{2 zoC`jO6}$Q1(=EYQC7_$H3^HF-8N5%Maqh<zD<XV+m?XRwy1abp@)~S*;(s=#bf!}b z3Jg^aLR{db<BSXvjBJeJjI3-d&`a}_8CcjEL3>8iLB~WR&ej1fcLPu4X@RcYRzRp? zV_;=tO-5A=8gUV2WRQ^(73SmN1ns9_VNhUH0L30?+y!)Qq^P1OxH^Zfh=catO-<Ao zMMcc{qZsG^+bwJ@y<o9RrLUg7th}94RBQvI6)V%fvrMM~elfGHnUXufjfW?Iho`!t zybA2U(Er?wKbRyK6d2qcTtLODEF&w6AR{xY03-MuYDNYY&=y6|xuxI>IlxI%9n_)) zEt_Z30^iJ`z@Q+kqN=J4jWuN)t^~DQM3n^@7kw2}5~^>vO0rS2loYd4ws#JV5Ks~X zovk~UiKTsVVx<K?pC7MaLRw}rGs|Np3DDUK|6Q0%!FydT985(7`M5b4*%&}aGBYwU zws5esF@vTaK_z7z3o{e!<VH~jQP4@Zf`USvpa~^L@Mcf&bpnjajL?f9n7#kKeh?pK z1ikh_9(EZ7=x%|35B`1G1G;DcjFE1H0JS$585sY&GxLD&RKw{e@cF4sc-+J+4%&dK z3_9Bt<qY2SNudU)r|_Qs_ircYv{x`jJ%{)Ie+ISxDxf<lnA8|S3pUxA;}}+h$5j>o ztANhuWm03j06Ob|IgX(j9Pf$$U6}frK<8T<I_U9oFfucMw>>j4wt%*1us{xViUSQ4 zLJq+Moop>6C@2IzqY^0~jRhH*V*Z`_n-gXYxtf9L)W4<w9zw$n6o`!AaY@iQo^s%G z_Y58MxH*`aS(rdaS3?fC0u^iwEG!Ihpe7jjG;PpH+CoBtLV_UsSWUs<V=M@|*UVUu z@s#$bh=`{NPq#<y+64*?(D}EFEdTzU`S%CnH|X6G!l2O?(4KBi4rV3}&}qn^VHZ#x z09weT<_)^6Tb<EISV&k<7<|Z|s<Np%=s+q#(C~q|F{3D>vO1%wt!-44t?j|Ie>WLT zb58cma``t6<S*x?Nmu@DQ~I~)A*lZaTC)Q>%U2k5@0gGv4>t!Z3nK>;E5wta{q(7j zfl1JDUXVk#Ky$IeLc)+B6*V_z7Zq1l7ezRMQ8hQ=?1Qu_2aAXZ3&!`sHP6j{cg8HS z|MvmpVTajZzw$!;Dg?Tbg`bC$nUxuI`w0UFI}>PR0KAd}be|F{t2!&_Pz@mlNRAK` z1Z4?fQ_!kx&{-Oc#-L63g35xtAyIL0kQ+8!K(}o$sriS7`fp>5`nUAoL{L6pQUm!H zbj}$oxc;|wu;PX;7GneLJ7HvEU`*v;WM^lN1sxBj4qDrx;SFXoLtP*UI)EDL0#FGI zide`w)S$I1YqxEC3OVE0$A{@hl;gkcpvZ+DbsXymI&+JWft`VYX$#XS1|HB!{oE{| z16&!sKsUF>f)5i`Lt1JCI*C+JNQjMHLYtEbocqDm3DdHFrx?LrT=I|68k}q){;LWA z-6{fI%ajd1`&8RO1Je%V<xEI+z>d@g9Yrh56!G*&>gTZ4jEv8iB>w&V_ZpO5883K% z4$%h3A1EK*1mEpo;GhdX3!jaHk)4e_5!3}y^9F6HhGYuVqg+`{Ax8-tLywGQYL5Ij zF)E5NKQe639>i*azit1%K+3Do|L#oX;4@=!*abiLl7Rt!aStS#SWQ8@`9RA`6iq>g z(=sUp{JR?w!KfVJzjNpN_n`BUp-olLeH9E0%<8cEo&j`TBO@zlyBFjr95rut_}x;d z^)bj`ML|f7uPB%p#hCYRNz}hdkVOvxf7?KQ0;N4rUb_sd?=j5Rz-PWF=)5w}IkbwR zp%IMAjLfSe{@n$+-s7La`}ZKX|7T<V1D<OycMxJ^WM+_NWMSrKWMTo`?*psvLER(J z5wM^e%h+Q<X&scsSXjVm9XwtSZLvX4iUkc{GcqwIGqAFvs0M9!2VK>t#GoW7C}g6l zYQzburUbFq@u1oh)Cdq%7Gz2gHkX{f$hq84-%d{6PAM|BG)L5uKl0y}D5g{Yb}%!q znVQ}2&dVLZ&0AAeQOV5m?;KO*-*%=`uynx9>;#@0_jGUr`B8?Eg;{`+iG`n$k%`3% zG&jKl8pnk>SHl~0sUM2xAS?I;1%*NL4q#V9>U_``A$a`S6ub({6nr2AyC@`~GO-IN z3c3|KnMPZvn2C#-C|KGA1oJ8LM*RC1!6fmViN&He6}j$bVSNZXy_!h^yk7Y_(*vff z48{!h41NyYvWzUua*S*&wwg?=?3^r2450Q30~aGB6Pp(U8v{El8$0MwK1K${WCj)% zaD!PFbVMk)(W1xXqpGf=s;Z)*2AeWr2RRqCW(+dMj+m4~9-o75i!(L?56D9g6)-bn zI-_i^z^@{%Cda2Ctna5JX|Iqm(QD>H1uG#59wYIR^oA^PF%4aVU=!nDqiB6yJv}`Y z12aZ%$D9BOb`EZTHa017PR^)B+3QbovHP%a&FP%j!)n;&9hPfsm=_n6nB?zXuBK28 ziaXHVrjMConUole80;9PI!J>?9$6Tf87&!^*p+0NSeP^!*;terS=rdUAjb-5GB9&8 zvNM5h%>#{0f^Jy^j|76}j`TpQyul4rJx~Kx4WX8Wft7_d89bPYriINX(m~q9n32KQ z&cqIK){BG~_>>nTMk6jz=N@*v4QMDHyc7>SmjUf?U=3$hq>YA*in5w~!dxmM0rv84 zmVy$@vMN!|j-i(7Typ$!(mcw7sxGpkMymd~7G;cFO3Esd3L5fEN*pZi%xrn7mQlv6 zjPemyF<~KbE{W1C9PDna%%VaZJYF^44YT)Kn;LqHig=qCTZ7w1w*OU_BAKo*se$fI zVqj;EW8?*koBvl~+|Q)Mq{aX`cOBH0+6OMHIsdybDT2>p(gGc6%*M#d!kP%G&td&Z z==v}CsxxTgMG#tc3NkuH-Di@xAGT)?Q)OV_-|OIlnUO){zdPeG@On0oxs0F{_KBdz z0&6Vj=mw<T3-YB$pbL4yO-DgxQ^sTK{KEp)GD&#;s{*wW&wGOX0csALf%fcxPR;|( zwf+}i`~xokWgVpWIhdG1C+2}RVt`wzS`0ptLITX};-FL15i_I0#>|Xb5tm{L_+?mG z*`x&WV=jGo$0YUdmPVwcWP~Q8+TUlOG8t+Yg8&1#eaFho2;PUv$jZpT#>kKcYDlVi zg9eGft7;hp7z7|~01kF3ZScunrp%zjSeQZ00nv9;ro2nqxhwJA<jJf2B^Y=9TgVvy z&s)ZS6(|odGB7~>Aqu)nTmaPO0PT1M-A~HEn8wb=#L5buD24P*KnK`>yObcC7@0wB z9dLgS$%l-8F1EEtbaq^f$rF@fXW@_*$_A}(b6&gFnMvZ`T|GZV#Xx;Vd2kwF`R~G% z0j{t09dvj&*_c^C4Os>jSYiOJAV#!e`5E}Z%`Ip`V1_jDg^ihojRl#6jP@Rge|s-w z{f79vyFgX8*}qeaC5&PJUNB1hTl4SMKhS*%LjT>EB*Afp%Rc002%3HB?4WBs#LbOC zy$r^~8k43ZAKIDH)tS7r-*4BhYu6Zs7}-vp`uFPusNu}W!1>>W*&XaRLkB$$Ms@~v z@Zqyev7p^*kQxr@)Oz&Bh$1wZgHBjC7G(PL_sLl%?k9{XQJ~8X!O0zTr4=~IGl0`P z(;=|GAa=2^2SIWY$Sz2QL%dx%|D0wq&RfeU5)rs_C*yff&wtgR)BeH7=z;G3`tQQD zp6L{WIB1Ww7$YkSJ0mkIlNST%zyelgXg!e#Zk4EkTC0#Ak2r(4vM}hvA#lP0ADgc% z2%bTPZ?!NNWQq(6i$b~(40LlBsPBV$Eg0zDf&Xqy@!)+@77nJOc-;X`zz}zcF^DOl zyTcUX4$v+r(4t;bCISEO0PqzA5uob`KqmnI`?Cvj3jz2_0&v<5U|;~>fhNgd?O-7x z%EZFT#>~XT!sNvOTBOec-mwSDO{wf`OyK3&u<}fjK~hjySWpzwzZV1r1L*z-B{gVB zn1Xh1fC3|0$xY2Rza+}R(^lPG0lbJ;nOBr`+N7vx&>d-(Rx6jX2=g$4j=lwj2Ll5W z2a^PY6oZX}rKA`W3o9pR|2i%QflDb!Ls*JIN>E5x815j@d0V2&rl6Jwc=xI}#6_k| z91$`W3f3%~K@k!0)^ZkdpnUyaScaprh%wy#)R9Q$iQQ~cBB0|BL2W;1UrHFXkA#Vl znNbk5X$P?g0dx--sOPB8gt#M4*cjSe1KpGeTII$rsGJyOm2CmPB97_QzuTZo;{H8m z6aZZk_iq{K+;C7ER+8xl6B`5Qj725}>HpuDMVYQLC^Kj=bU1MFFfuXnGJ+0?g6s~| zU|?luV`DAg0A0Mr$i&8&!NthV&JfSZ$iM)eX#_WM^q74>hjp{Ev$3<X)e%%1=^(4B z!pNYZuA-%?r6?~gB`Pe)54y1!beTAG5gO=3EKr>dJ9SSTdO`sBQfy;mGc$3}k}4B5 z#&=mw)_;GoS5-Ra>1pQ$%wNVOVeZDk=FQA*CMdk9J+YL7aak<W)u_4Y#?>tn0zm?z zn`hgbiYW1L$O#(GUlLGLUf9II#31(n8&evSB01pzZr2bQ4k&6-!a+e!N>YS~Z~!-I z%%Q<x#{>(84!2~}e-oI(0<3(rGy}{_x>+1uI6x=i8*_0vCR^Egu`vocFe%#PdYC3g z3JdxO3AWWUnwbm9bF<3`8v3Q^d-!>~g3p8or3n{snowiVW0>l|4N4QjyiABhq07L= z!OqTBz`(-H(!$Be&dLZ{1dxH)Z;O^P^g!x4*x2ic(-7$(uc6M!psl5@r=h2!Bqu8& z208*0oIunV)xZ-uMxY^OMR)=Ml?rNVxY7t}B2iCt4sY(_M5GddrI1vj7Qu8ia&D>_ zIGOMXGi}+<x4s88srckrgVTyTIIXBM=rFW9aLY5YGAJ-Gv9U6FK{AaF10x3$6Jr4b zI~#ioCnFPRw+|CT1{Wg-2RNC4htMGnVKyIW2VIbI4o0RrqSQw^2&=1UX{za{>j;V} zi72Uv3Ujl|XtS!RsiPzfaAyuB7qN>fgKm>iG*M$rippY!rw@U~NTkH0?5-200!c0` zd`<a?G*X-gPdtKrOdFXdK@!aW{|unrXj<U*o({PE2U;Bx0=|cJJ!qW+lNv)m#yST^ z2GG4>pu4F+djQ#()FL2lEYP@j3;6!;90wtJMrH;nMi$WNQ7k;*J|g67C{U9K+z$pf zy3|0u36x%<w1W)1GYQ&}$C!+y5Y+nxo$IX3pe!h?s;a68u4kdg4uDE>#PBw>TonYB zwdUqbvH#ABE?(qO<Ev*UCvUG2lImFOscaz{6(w#ad6DVVzfDW~^ZH%E<J?sh0qI5@ z?EmgDo%%Ol)=u^%sI40MpPPw~NrFM1!Pdb_l97o`gprwvgOQP$9W;m!>OV3uvoWQE zCNm+8Z_xY-c-fs6gO8vPsMD*=4!U0xvbq%EPEZ#ey!Xc3m|c#EiO)9GO2u4U%uLb7 zFE&ERKqNf;RpJBB0z28n3QK;_6)Oqp>GABW*O?^#{CUG@UJe=OW;()jib0+s$$?9l zk(EJ+kqMljMFS!nWWm)qV<H1HGg~YJ8yn<q%y?*>B<-MxDG$D3g_RYu@B<X&T%c=h zK_~5iJYdYM3|fDW<8s?Xk95B%oEO{%{QI&8*Cn@%4DA2inLL;z7(^K6K=zYy^MM8i zy?7a!7(JyOxIqV1GD3Kf5Ct+I-v`3H2AYCpU|@p#4YGa}JpjP75eP*X0RU-6fja*1 z0El$p5>f^=>jc122oC{e(D4_b#*`xCn;3^k+lcwRdOnu2f;{|Y4uXu%UjP2?WIVs} zTpXJ-^V83WJkrc`ia~-Q#es{Hk&zj4t1c{Wurh#_$AKDCETHjpW@hM(Ees5xE44s* z1F8blHDhD|<qeoZW}iq0P~MPWkO19ysj33b8_=D;u=FOV2p+3e6l7|4DfiR2m6x|u zj!u9K0WzKXTa(x4!o%aw!&6$t$O+jU!KepLAfWWG!6d;T0cr;#+|2-W4!9wVneZ7H zK-YQTb`JQ|OwgepOd8+;9x+i<c}v@W_aY(~m6#;{F(j6m@$>p|3q@pH2agxR!q5eL zzA-;!Cl>=V0~<4FW{{DAF_9gzpNtVaodfQ}2?`2;Mr>3eu?F+MF;jDrXSyG3^b?dY zK|_U*aYN9ZbN}6$TEKk`(6}cfXuld8Xssg)GfN_9a8<*bgPjRBK#tn2F%|^*2(%hO z*&Ni12MroV#5hFRL5B@NgPuFzzlZoG;J*vFeJu{!i3Mukv%&irOrU-SxCl%HjirI7 zSD;r7L-Qi&ns`BDXu}?~4gx%MB52GM86Fml)We9{!^m?L)Sd)2s6p*X$oz`ee>d=* zW@4Z|4#;jq9|y^9Sk%HA_Go<z@D4Dj6Oj8BVZO+Hi@z^+F|sTL-*ATLTY%Pzbc65T z)_2h1W(3`h=mp*J02*Nc4f?Xe_MtIIGf0Edk)SvzSt~Lt%P~Rj^)m*Y`2{YLOw2$x z{xQyZ&BDyg7~Fc7<$g<|M}~hS6EhRbJEkHjS7)(*ZcL~CZfEiYjc@N^JRsrhDEhY_ zn!eqcJ}^lzh%*>C=!)=gFtf2Rda*OIF)*+(BtlAcW+o=^<Ua!ggE)h@kf5j-WXf3; zYCGz6gT{hP9~`3WQ0^Qo1(yh67?%$IGXmGW=yfq@e54Lk7lY1-U}R@fV^9Rw#Y{+R z4k7oweq&Mt-}lPKqz0PK`u`u4xR~mgdzirIOE9uCWHHD<^dhY>hv)^}N#X*Ut7XXg z4qkH&(#>oNZWDBX-KF>c8?!F>-q00bK4dTZ3MMsh>jC605wO{eNbZ8{QwO;VyiXl8 z0R-|6gZ2M!OiP)rGKe$CgU;h+VFaD##>~J3ig;EQX3+WW3=H7maL8T_*x;+A1n2@| z33*9*Q4#PJ#^Q|PuxV~nXnP4#wW^td?k;9FGBZ~c5ffwdaVap+$@VXp>F3p-ncZZ~ z#LZEelvK&_FH|jpsZByKNI)!RX+i#y=;)>SN|kMY_t%zr=R(H%K=(BL1^Jsr6YOuW z+d*N<bP&`wV#s2!0{fc@DNG@HZZWBW^f0h7WPJdKDX6$+4gu+91h=agvKYF+dKr=Q zLfTcgnAE^$E3h$S1w-_z{r|=!2G+X)V(<UI7<v`KcdCN)g63@57_v@)k^m!v^8as4 zf=r-$+#Mb47#Ueu#Tgk`co><OxxsBRP*aNuG;Ytr0vS034`zVFUk`NB3g~8cAr(~> z=!7Huz&BWZ0=lct)LhY&@$=V)$$gWo(slLoj1m(HGo4?9HsQLzVNFhss}K|m5|{9D zcDD0;9KgWH!1rH(@d6X*G;ye#BpI0*_!ya(d9k|*TqA?sqy=`941)}4?*hzCkahc@ zL3qRjf-=}qTW`A*I60OFTE{6UCR+O@<VRcI{&$W^!r=>xX`-iluC0(@fPhq_Z-}?; zXV4VJ|Njh-GZ?~|pl2{J`Y@b>gf&vy2KB>rnU*p^?q+3Y^kL`)rw@qU9++N+EQU;o zUZgmN=mo_S_+D3rEa>`T@ZKd*95Wa};+P>7tQR?Lf%23(_)a*``c^iEEYN|C|Nn!| zWnsDiIzO5z3G5DtIwdAHrerW5Vs<@~8b}^o7NjyPW#HJz%(P)CXk{{JYyqO@7LyuC z4=CM&&YS%Ip8<U5ICDLd8sjmDyZ%1~>t#gJ3%Q37<R{Re2pdDzYDl`}{{M}sfEg5) zdq8>_eHc2yenKuMAa@%rh23ez=mT1w1G=XPtQQoPV7&}k3@Q-4NMQ-l3o5@rgF|c# zS$C1`1)ZY-*2@^gun31<_?=3OL7=Nc|NjS_(!&%9iU-C6AU88)F&JRzh1>-S^6vvk zxycF1Yl;6|z-yK`88jSJ86acw3=9k`v7q`x4Yd9RvY?2QffLj+0ZlG5ih_ndl}(ie zK}(fFL!+2Z1^m4RY9+(x-P{>xGf6ODH51a{0?&}6m<d`F2{v=~JOA(i*bqOH1mw&j zrk`N{9fA4}(!XOy3Rh4X-3m%)j87o`1KoG<|38EHe=CsxnADg+6H9E&@t~=B21W*s z|5o6&R5A=f4*sAehM;4pnY<V{nVFb5nd;a<r#vuovM{GIFmi!zolao@?T==F4!Ns? zHj9E5N`h|RgI<@x#lglP!zcq9PlDe`X=(}{g9kMk!2|xrq9SZ9!tz`kQUV5HK0b_| zA6KmKwLHeb;>pZr;`Y+zwbw$IrLB&js*Qn>f#tsyQzEz=baQYP72@Y%VPk<gmDvll zlbMB$sg413w+JIc3M9fn6A)n6iHnH{3kvY@f>xx0b`OCn9?<R~aCu{Ds*F5FFDk;u z_y+1Mq`~^#esHHTqK(;4gt<}ZzZH`>cx;ncH)?nj?MCEreUKX$LEWf32|mC-$rJ8I z#5n&sP!KYL7QryNf$w6`chCXV+PsXcETAp#(92!ZAT2h~<dZrpsKYBLC<JME3mZdb zX^cgcg~1`EY&-2h%KG&Q2WQT^p0@Wu!u7d~3mG;4J!h2scisQrI&km=f%@Fgdwm%M zL31{^>;#SbgWKVPLZC}PVRot;i<=vRF3S*CR%cf>SGH(g0Gh;D*w(Q&Wzv-7HEoP$ zk4~L><nrm-wNIcmm!QiXK=mXyg9xZ44oWf1jNmJ4L6aEF%nWg$4I1hUKD<1P45;^y zfNmdG1aECIGd3197F0JDgv|3YI`@?umnG*$?Mq3E4-ZX`vrMvK)cb4Z%gE?r=WGKm zn?dES5O_=%wBe13!S4SzW^JZZ3^EMrpdG9Nps6tMGIKTtMz(YgaPNj4)KCC#$kYQZ zrBhLomt~M)kWp1p7FLGb5f47p2|TI|9u$F${F|78+y`-+v5}dXDdTTT69cQrq(oa^ zamfJtyke`mG*`Fy#14IP38qtjUs#%2nZJA+n5Cnps*{oCm|(`J_pjN`yFuO%G(^M1 zVD|qTvoX^T1|<f4&^-mRjEroc_3}(!9H3dwW(MeK2%tsA;I6d3H;T(ulo=SbG?evK z^g%oSRaBLQmBDvD3Zl9mv<V0@fDGC>WUQuUYRU|r`vrBpO-<C8m~<S?O(Ij`jO<i- z<wCuaE6U<+y~Wv>m?yb8IlBcoGTJfy_<Pa8%fWHlyrg&uZ5a)V?#`}0Jw;w4fouOB zWTmF2#>5GxWP$qu%KyJH>oHwn&}NtjnGEOGU}R!rV`O1s^3r5v1C2w#L|CCB(3AA# zSs9r?=N+)5GO)2R#DlyF-WCfUHUy8dtAPd@*_c3=;4rW-f-Y#qsxZ<)6m(OFf}E_h zxTugI9}j~zqc*6>VT5d_5)~5@2ald2B3Mlg?Us8pM$X*QX!nq4Mn+?6OVfyqxaa~# z#_Ytz*u*4n8|yGNUClD4E6KiI;7#scZnoAA_nsvs#AkaudV1QJSt&``$ZLc0AgJwm zgGq@&1GJi7jggH(6%rh*%wA}|0o@!6@izFPVdew|W@bIm4kl$qS!pQ=QBVoW!=S;a z0rCpuz8=uVf3R1;K2cUu$Mnbbh#X@bLt7>$L(9zY{d?J%+}&L*9DS_K)m+tNHG-Lx zyqrOs%DJpebWIzk%{#Qv5Waz2Scn1C{&itWV!Fbh%;4qVt^iu(z{tYN<fRCTN(Lro zrWOWPtZ@+wSzgS@ASWXsCMqnz&&SKd4VvQsxgHS_qN3pWbkLd=w6h5q1q&+@ydn~! z!lKKwl2g+&{2lCKG;}qqz`-8M!@?8cUiI(Go>MM%o}M<Q*2<FL5Ql`hI}<09B7+Kp zn}f3wBO8Mv)ZNUW`9~IT%7L~nU<pVc+PYw5ke8Jd7ZZWhqYNsHDj@g5LR*drw|nnJ z<(cWqn<@l_1$g;-S-S;0SZMgDC}_nqDSEjWn)9-9%A0C>{rk6bik^X|s*akvuz(xr zY$;IB=Kojb2BuRCnhb4_AvZ2LMg~S%MkWT(c5E1j6@1PJ=*UY2(3*N?&^alfF-gz_ zKWJSgY~)T4v^QG|ixN=(5j?i12O6A%jNs`pK}Wqb88lVZMU;h<IoPGN8No#eI8lMh zKQ&X(?Xm2j3uIx}6q|t#g<%RbHOq;}jJAw4zQ`#m#?7m4ZzQP9$0IEurzFC`Auhwm zbn5SC2a9S@)=&#`XFTY&Xo{(GtN}Z#HygXYm%7HJg9Qwram#Pa+~EF<nS-$yBLkx- z*y|`>U|<995Mf|o0$mIPIw3<ySy@;~NC`Zm2rBWw&IJ{8Ajc{rMi!qKS)@kniA|f1 zJho`P$KJMn`n*H&DU4!B1B{R}x#oh~IZ@CdQZ`0V?aj!X3LdZmk0QatPTD~MLmIp! z4HOxWgFGS~K+DZQlc$1$BH#)}8XCT4U>8FI*c3Ld#*`1b$1EZv+A`AU-%`*KEKH~V zesQpblrL)G?vs&*6~W_d-<Vy%<1QHx_bM}hT93>Opsm5IObo0bXEQQ_yMQbSpm8ug z79Y^vFgR4Pv8jWm<5?0ISXlHxW7i<pi!+D|3WN3sLl&@#g4QFV1UaZtDY%wVBEr}r zHGEG@`o9+upap*amO9vi3YCOpv-eCA;Q9g7mpZ_7g+T$dc2<CqnMn||-5k_;WMyFh zuUG_+Sm=W0HNag~Jw_j4RaG@*@M2*`MaaHZ&}L-NR(r%sG0@1JnW-}4az@Gig*h|) zLza|V7wIagrI|E#nC6A}#AxhhEYe=IK5}VM-jWyz5nn;c*{z|?o|dVOV0VGq5xz`H z3^E{h@i8(p@ndzDKC-(&9eb#|K#gfo+A~3Q73i2nWyXNN)y-Xf)2CR)Xv!&s=%=Sg zrbPHIVU$&>YoF3sKSM;=l}{``A*L`0+ScX-_1&0GF^GZ2(ioV)J)t_p;w11L!mwJA z0d$bN7${+Zmu-VqD1l-IIc*6ln=(m1urN2Zh|G?*iZZ>ybSm(l3n)q5f0mR0%9jTM z!C@fs{~MD%c>Vx1W{Y93253qKRxL3wFo-aS2!mEhfEL=I+6xK^L1k0M{i{qYj7-D# zMQ5&LlJNNV*1^`saV9v^iF+_GGMN7V#-z%0g+T*Uw?J0aF)=DJGBZgsvM@`5+CW|m z(A(d@tK!%o6Yrq=br@J#^;mrv89-IMAU}A2uLh$As3i$H90##0-B=XTlmxYPL_t{% zbgZo@c(Rbu)+b-rKr_PD**7&cH6^1wG14|$H>|0nH#OQnx|ZpRy1avwf~C2QtCypT zi<5+Cuv&hcXB-y`cc`adDyYw|{r?-|LD2XXV<R~H82?)_ZDcydpanX&4m5WpB+1Ci zF2Tse3hOC>&+|hPWC7(u@Oge9<!TJ<%&g4ptaS{Gpji!u6xhU!8lu1kpS+=kM<K@K zlQ<)Tx|*trthAU2=u{34HU=$5El%kEBv=m&RAyop@SugkB5X`H(5_A-&Uq<B=M3xY z)ZaFEAC3LL6_Yg+=*IQukW;mUMHyL{Vd2B<B?6wSf>eVrDR3ykq#Qt^>@19o$fBT& z1wbco$)krHX!8~;E2O*z1s%j-WlUuZpl}0Cet=ur;4%_tD1!@Abx@qZ6h}rnh%++C z$;wDeiVF!qgISSL5nC`L2HMOa;mddo8om+pc=dcOc_lg6_}I-H1sR<Y!OD1k<@q>f z7AGdAXJ7v5fVz~RQRe>v$Y&sPFJ{UE_lflW2Oyn+%+9@-$r&sTY5>$RgT{MQKw}q7 z-b@AHu_z`+#5fjsOq<yhIv&o><jv%Ur1v6hJe)a<DGj0*X&g)M|2HOCW*wMb<}fBZ zta`(lk}>oO{r|@J07-8+vR)P*xV;ffi5Pl8_Odaj!|jb=azfb4A_3PMiQUbN4?yP< zfXDyYnIoBOA$mb!%e(=imq8s8wv6nIUJUu*@hoPf@k~f~gT}%@1-Kfcmlp#ADAd8n z7=q%7fq|)x`6I+EMt0^XrYvlJV0^&r1NTD|lOx0(XnLcWG70L9W^zF2Wf6wk8-r7C z9^Bp-CKn{VVsO2&IK!?Gt~VBDFF5R`LG&`HGx{>rGi+i6g-9OgT!D?K^Dm&Z`UGl* z8l!I%B<zepLxQ03w*LW)ADB)-=1Rb!8U}Ws`TqbWM(}!J(EJF<>~OF+WNrnN#zC_| zp!6FF4lhQeG!Clk8Nq2Bte44~31TKh??i}QjO>ho41wUdMjmek>t(vnq{aZ!%f=W8 zie!*G85o!hm_hjiY%g;dQy|1%r2GNWs|ilyAieC&VN70NH-k3!d_dA0&J={9SN{Ju zCPpN^;Y{9Oy@?DAOd{a?0kJoNDF8z+$X-rx{(#sU!Q=|l%lHADKOlM|nf$QoWd!FB zh~7w;UQoP%^9N`E9+W@AKxGP}7X#!BcjP#R#1$xifXq;1^zwtmaX2)NLAGmw%OxF< z8BE@cw;=97iZjqLq#r?X#-Rn;Q^({DI^!9%{^oxGBWV4N8UyI=1a|JljF-WF^ZOsb zu#M><lN$KudXV^9u(<920PtA98fcyXoSrs;#X)XV2d5{{+yEjyf!eDVz-|N2D=>L8 zZ3nvzxy%HGuR6$WkTaH;yqV&_W<vCW(gav9a~RV)h+ecbVFHd<u)WM-OrcoyhGWsI z`TrY}I+EUSWWC@t0kJoNX)RWJ)xl{3Vs8YBUQn3{(HqIM28Z5aCg_<`%#lnXP&WrK zT>z&EQ0@h#iA|vRWb|U#0nSIrWhNxNL1_YHKS(VoHLx*yB|zpNKxZPUg3Mw9-R1#0 z_g@)o25P$c#?%Hr;|r9o*cr+gPD9=CjTu?5Gs7K%dYu_sko1D)alrO6c{A;R_#Y`P zg5tXk6yHo>dy(UtS(SMrlN!X$jDZZxaoBsGNe!%*jWG~(2Nrk^Dgbmh07Nf1U9Jba z2NX_iOj%I%pmZ4l7LWQLz|;UP7eQxRgT$wS#r^*WFo`mO;t#Z_3?v=^7LWS>ji~{g zjx9iO!sN{~3+ylCauIYEy(q|E3}C%X-b}GDz05}7xbOt&Whi6h0{aWOTm<Po0*(t% zUk(%(7qRMfW;jexuQNjzl3q|;fbC`SW?F#N%|}3P29>1@pz=NeN$*4^HPCry;Ih0A zhrOV<0J)isF)$R8_M-j=fbI~0=mp2c9I%=G{{xtgFl95TLG0pQ%;XOiXZRn$1lseV z2Hx`l3Uf#u3YlL6#YGG#+_)Drt^NOnfpa4x<BtDdKof`1wt*QqUTi>Wn7o-hG5iQN z2joWv&^#T;k6I9W85o$%K<N!+FW8UP7=8rn1=m*~y=;tup!I{G`{VzoF~u?cU{Yha z44$KM0$u0E!1T|7DenJ$W*r7j1}RWan3a)*fq{{cMM{{DnTe6v3v``zGXo<F6AL3# z9cb>PiGhWsg~dliOi)A^bjJg;nVGq<C>tBQGU(hKbr8c8BqSy#zFk;aAVSPU%1GT% z(nLziMAA^wN<2o~Qi|!Mq?Q7gtg&dkfR?zpmH>~8lai7Xc#Qn-6{fiV#ZdP+I@mEY zGBI#4GBSw^aWgZ6-NVGt3|fN=a$Ox83llSQ6X+@pCLb;?1_mxEE-4XVem(|H22K%n zUQumEH8pip(1E9-;H#p+EM+y&2s!&dOBrLaZ~<vybxAAnSaB;!BZwoJ_KF(Iaw%v_ za>sMaIw~nS%5p<o397sx;dO*Lok5sEg`v<vP>7L<L6DP)l}S-X9PB{I4Ky+gpku@s z3qU(^n?T)ICRPR}&>h6AEuc+cvLI<zM&>%K@{tbWA|i|oA}S&(3i6T?0(>0o48n}U z9K0gh%#aan#H9-Cre^G*({G@jQU-<n96ciyDP~PpSvhG@aULFLaZ4$r2w+ZEQkIov zQ@3Xk5EA5N)?}_#a+2YJco7s43{3y*nBx9dFc&k3GN^*qoG8gkGP7}jF6?GtW(;Ql z-7Uz$!jR6%%EZjh$i~c;2wLph!r>z(#=sz^DyFKaAT0&D(^^SMNJ)vCS5g~t4kCDg zsWN2giLsHHIn>?oB-6oSpdq2FEhHksuFI;gA)&7=C@jk6B5oyRU~DR7Drq2TEzVr5 zZ!0b?E}>@!BD<9wWVsdf#We-EWgV0t@#D-C_dko-hk=tpoFRJ~8)T6b<WNglHbzEf z#%2ZvW)@}!mO9Xe3>Kyoc2*{4<`&SRekF)JNPvO48Itl@n3!0?8CY1D5*V0Rm;xdl z#K7rHoJ$-!J`YN19K531%-}o#3Vw0$>=Eqjoo!+!Qij4Z{N=1(Ztm_ZrY!o}Y9_4A zK3p=UBHWVN3fVU9Znl~dQtAv$|4`k|!3<vDMF+Pt!cveaD~j9yZQ+*|Hjp$GW8`J^ zba!`Sg}WY<Jw!}pxS+0=1hola?$2WoVNhaN3R+pq#K6f2nh1g{6_;lKO(ifEfM$A_ zSU{&-vobKVGADC@wko%<_&`s~RsbnuVPvhtsm?(Qq=gBz*u0K`k(G&&wFXHoGc#*A z11l?Nz>JmIKQhumQdE?YK~zap33Szgq&PS~i7<+A@``9fCnga2iH+UV3^fA(9ndvY zk!03nm64Mc;TC4&IgT0@c}mK%GHfEcto(w4yiAPhEWMBzg@y?vFBLL~GAJ{Ydv8z+ zh;UHiVg+prWMF1Y2WKXB7SIt-3~U^1$=slG?pxSF3*|vf1`Y;xjykN$+1NP388|rD z;z9cSBORo`xldV4Sy5hAMoI#n|9EibKU8mG&wcW!el66q0p&XgImz11#a2^HLLEF7 zCiY*2VG9ETgCK*80~aqN69Z@sJChe=j9LIRW8BQZ#Kf4uz{m*Rw=D`4f?XVw0J^>{ zAabj+sxWAKq_~M1C<lQ@f7MMvgS|P*igr>a#)k6Jf>J_~CDN7(BDUH(CL9*5;-EPo zp8r-1dl>>41R!(YpnMO$o(6RN2qOdh7#Q&K1n78;0HXltSW}b*6QFTj59sg=S0sFT zKL9eG_5VKu*M9*fMQ}UJ9MmsisA5<SuA2q^3or&VO=ePK0Jm!ysz6)bKxa7p|Hfnh zzMn{wVfQu>W+o0!&<(Yc0TH0<NtxLgnb?_`QbCm}=)wx<EzywOC5)hB!@C(k`9d=w z!a<vr5r=XH$QAD#91QVXj0_AQEhwrvIicn<u&^-3VycdebdZ;q108B4rzx)~DGr&u z;{u<L!ywBj%fl<84O+gT%nsVs4?E35gpJ)8T$+oDu&IM?Ed-6cii(Ia2HOXSvkTe# z_(WX2%FM(n9$=qRSykZSJ~7PEkxNg(g(=o4GSW#-$y!iJue+<O$1^MCbW%D?awY=< zGXoa`1Je?wQw(wp>I?=9Wn0A<*;zmV#uE_XpaxoHz`$6*z{0@J#=@QoT44a*7Tg3n z1c*77gOQoJ8B~(1LllC`czi0sXUZGsX=$jdD#|m+G02*VDsu9QYr_^KfSNR*<58je zszt#o=aiX2qx_(Se1b5>Mk#qYc4mK8Hdl{ltzB+Tx}I9C#S2@aU4x^`Cd7OBCU-K` zN{b81v2!c%IM@Xw+6VKna;UkPCuT9uvbM4|{(Iib#sc0)2c1F2#KyP^G<?SZx*G+Q zen9tcE!oD$%*4tD3Jy^EQG%Uq)B^H8cxbZM8?@qqA)b+)36c&VcVVevQwdH75apoK z6P#*P;N$OXY)}I+m4S*>(DG6_83|=cWnn>RLJ?;a2NkKXgrX=4n!CZDL>QO;+l)1Z z{P4=hmOL1kK=*wyc`$*_)R6?QJ7HpAVP;}UWng4%@@8XXW@d;7rHXC_9|3+w1~CzS zNdZYt(6!lojC|~nWDMR=2|8Z@mTuLQl|jiiG&av5sx76=q@pIkJt3yro692Ek@3gB zOBQjK@fE)A&aO5!evY7GE9n0>CSCBkEOHD<TR9n-!6_7!M`aio8Nt`OfhV<kQC$Su zjQ~E^4|D_>0}~UPbfkl*h%h6Aq`0u0h#VgeD+_}VqY%{1rh=e?2}fW$<uygHFtUmT z+NV_3<#@PH40Q_R>h5BEyAv%8*%BaYHb7%b)=XCzz-Ow1_FoFHFhL4T&>{sUP|eBQ z1)BU}hzHF=bu;)dGJp=u;$Q<Eb;imoq75%IK_>-+cH|(P{rlNER6{dJzqqBQ+8|I- zMKjAWcao>yWG+5_KOTWuZLPB<_<cmgqLvlpER6-V*%-m=F?TRWLDoz_=7m@pRk@g$ zSjD-Sn3*KN2mgaw(TuDNjOh$aOyJ`bz?bHM6KOY#52)}0o$3WXBmp!t1gYFX=cX$w ziGogv1eH$UnKI}J6woO}aZ?jBrW#>+E_PV~eHBGtgW{IzYP}FmjZo{{NuK_*@~o{) znWH#ZJ($@IjQ#i}Kpp^JTm<m|AD@F8xc&o;5hyYJWzb-#bdV5YWM&d(WMr1)W@2Gf zm11ILQDbCaW%csjAR7?jpa9B^plct%DFr;~*$bLnfE-2#&WE7e1DP3_nA1Tec02<k zBdUT(2Vq7AWkop|(Dg)|?4U&=(1Zf@l^DodpyfZ{`wO7{hn{~c%Erd1<B@0YSmYs~ zC}3&_^}Do$gUmc%A8i*IX-6)rs`%s@D;B0`M^{c3PiA(58c$Av(~0RqyncNA;QCIE zfq`i*c+LAHa5)7kx8H*If6FsOL)v?CkW#n+lp$foGz$xJEGV0Ti)nd?0+u=)D!^0e z^0HE(T?S&NqKfR;i)PSyD9Y-_3WA_A8L24#mOVf0=!V=xMy1}yQj`LCPSMuwzKk;c zt)TSB#9++8z{CbVOGgZJXBqso8zv@Z(6W{$$Rz`@pcL55;KR?y$RH%ZC&n)ZI{c4= z4RqipsBZ>e(q^h?s))3x&Az9>v^G2D-zLTt`%{C`QX(?qERt;Q`-9fCG1+?9+kwLm zlzxPmelUoGZo%3n!iiGOLmdJx=b`BZQgE}Nm-ARuGO(~TL#qX(a-Nxii4oNBU}J+C zh^Y)#&MU}Cs7R^^gZ8EJv9mHD%6U-LXe1^MUSbCFv$DD}8@MR|THXU*a>gi}Qc;;= z=P${|#PV-5<5G8C7Ks4EP*<l&cXxNL?yjz0Jw;w){vNNt93Dyrp0RNPDcObb;9~0k ze+IMv3QTHDN{sxB4NNgi5(gL<I5#pd?O>7sjioR!f=5A4gU<@lchG^(n}g0kVu@v7 zVQB*G`G%~~Zw8;r1wYt>S5n&;6h{y&Xv1jw?=GYAztfEU{{Q_z@dBFb1@Bz|uMs~8 z&fjMLzcHyXU188-@OJP3-La&CG;hrUy2^;r6JB|OPJscXmM+lh0dQf}4Vt_L<yYwB zwHBimD97TPyavr)gR-+Zbo$!Lt4vc{BgDbRCm}I0Eu$nc+PqLtH^(Wp(mXE8HK>H? zinhG1w33yXjk~v<i<5_(Sdf^QX{@zR6b}nesHblV$RGc|{ucnvuP}&$PB>*@WMl=+ zNP|x&U}a>1tjCCD0AHC2^F%wS_y<QmFAp~d8-pmLD9GtZRe&I<u2&RQ6cl086_V!W z662LuVC?^Qno;TBJ?{_fY(A{4%KFTb{(t@d)dhow-WVWd{&VIi23>||hX`gy&{3=G zj9OewOzhl@tV|M&%q+5uY|Lz4pp$VJ*+IoVD+3ENXbm6}6Sy@CF8)E?AGU5bA4UdE zbwzm@X(>rD5pWft%cu)#jDW%yxd?<GmtzV#o($aC1qHF0u@R`iWLhmO2ihX8ucQ`l z+|X}cmZ0w>J@>m+nRosS|L|p{9*$<Ps=(Aw0J5Hhi~UN#zeU1AkxL8nmqrT;f~y12 zRSN$Fn5@9%p%SQ_A}hqh#=^`9IyMf}QUM)02B}=z8GJx(DG4P>CD2k_Q0`)bv{OJU z7a#|yfbNwvW(FT6t8QcpzNP@wV39Sm(~y1>*DEN+&Mqa`6aPk0$<9nkKuDNh_=At7 zPKg;){J(A;Uu9)~9me9niyRA8Eqwhf#XXc&UB%=<J3&MLe`5}1`oW;gaAd0>BP%1U zVJXYN%-jO1|5@Te3y7d~4-=xSkq(G(P{dHg#MBI_#$d$^r17Z=QOL&D3Ney_g{2p? z<Oof%gDL}>5@u#_t%0fzbPc>HBZGoG=+a2g8bb~?@G3)Sm_bV((56jM5m<XkR0Pye z2krM`W2(=}F|`P?jfh}nlL#@0j;V9BjmYMTO`q%NkQ(>zHPes#8p@hlvFURgT>}_h z{_U&)-zN;dXO=OD=@f$i=qx<Y23tPp<r$1kAkRRqn+CU(xH;Ka83Y&wSRm~raB~yh z@c69bsu8KGA*RpEXU-+->Byw}_qKrnJBt@PC#WuBWKd#YU^D>VfoJYuA|%Mf$il$N z!_C1CYM~*Wzyn%04z{=%w4xb&_OP(9pfEeHq&A}{J7~SXptz`_sktENus?NEMy-&j zsE~jEKqS*C6Op%m{~3f$OoSO3{N9R~fcnp%dBtp|3k=c>D<Eqo_&}LbgrAR_g_Q|> z(>6$e1t9>vYg-t+Qip|=iGiUDytuxZ3DjW)b=Q!jnvkSGD;klc5V{=1A;mVR-~qJ= z;4+{%7i44r)j+&F9IT*5C@7D>GdCMMJEJoAc5TS!bvq^|O+g7xb{S4yImsoADp}wC zqoQOSWojhURhTZYu(&ZX^2;Ckm(3XF`|mr`sedz+y;Nq%%W{Ik0koHcm+3Eq97CLg zASehxWxbT77#|BW6X+CSPy<dJ>}QY@TR?H&>J1vk1-EEKL4u$?*q~M_Ts+c25WE>r zTvUXEjX{o44&-`a@EtjzK~YgbP*>WH$=uwSU07KibdsCNIbmf`NhB`8#KgiTA<4Nq z{pO3TUAEy&f5GiBY59L&1WbfE<m9=wKK*z0m+4Wa!X8lh&H&oc{F&(o12=;QB>RaW zUBL>va~hO#nn8oms1op+5^|j|N@WR3M1r7Qy2^r~Zf+57Zj4Dzj3NKlIQ^Rkvh)A1 z|KFJ8nEo;7fYJ`AXDTkt!^FU-E-E6#&C0^e#L2`U!w3pKX$NdFEKJOx8}+~%LFR$l zyWoIjjt8GJ+06_}QJ^wM8X*nZancM5eQ<9Dq{KlIw6vCqkpX<D0E%2B<Z5Rf1|2nV zRdvwS&dS2-?CR!_WB(!BA;rzb*@Zy^P0;><y1E_{s6E5Z%=TP7OE{TNNm@vnn^#%L z(orH;IBm1AJcl4Fx2&M5wty#xs2~dqr>tNV6Q6^Ey_|#)3zIuDtB#(cy}~~}GZsci zR(4gLvl7;p!pZ^a|DJ==S=9e;%%E`&Wro;o;LB@Zg|8U+y2d7N(18OC3G8ef%#fbE zgA`PPfsL&hCd&co=7U!w$;*N^1qloC@p5snvVaOjPz}fmKS0RH40@6fgaK}>DJy}z z$S9wh5M^Z_=@Vt;7~>x0&d(~7V%nn3DwoNXUd;IGADf$7N*v=65S^F7xZ$r^ke*(O zu^}ijK<x>p>EQ7iO9wM?M$kkNY^4@>x(L()Y6cbjkd8UHF(D=@A`EUyK(hjJQ^J_h zSdR(hP-RACF)>iqfHx^J7;8ZpEhO__6AKfY3!-81ccXxVw<6=@zh+E<${FVW<Uoy# z|Nj|6{|hilg4gRcgZnJt`FU`gE(zS`2F*F&WV*^A$DqX!vQ3sBv~CYNVkrTt_!#3s zn{>J$l^Qc_WK9M|ikTT&KQVx#Dbj%pv@t_PO_`ln8p{>!pphi#DMsw5_n>cZ?@rI^ zc6IH^@+?r7Pqj!%jIy#%Q#JkjCabV8Gqa#D8|gAAeH}gB^x}W4jKAI7Y-2h=_qDgr zoCzA6F#j*W^oL1_L5e{QG@r-A$igTI-g$&@HMBklIT*AGzMa`eMO8^ySyc(@Wcb01 zkozz|+r~|lmC&5a*O2R+Wm}({WiREU6%Y^};~=N}uf^5d+s)0}#|3_Onun@#T>9V5 zOrG^R4)O8%`SI~NIpDAd^%tv{PBBO_C@~~K`iqL7ju&WnkClO$g_SuK)Pe@L03aQ( zX3#ojC5RgEmJl3@A{_)77?c!bq!=U_BvnmC6*+h%aE$1&fextvwGGgEj@Q9mwjhtR zl*qVf<ep<*igmifzgLXOPX4Z-wO^on4+NMDnEo;-GZ->7Iq<`33~4?lHda`Dp$4um zK&_${a7PR}IK%+D!<m7Zxf_&6+Cjck0jU93AE53X4rP%JlAsf%bhK0qRSgvtB*ldV zA#ET~c1NmJKvx;U2WUZy5JCA#R9VmjbTAHNEC4imb6!{lH1;7T$;!wk;pdoKS(ySl zn4Xa};yw5%dIJY}cX+K6AZ#KCI*GosPftP6MEDMKV$Z+Z;M3?$<7{H`7?>Cm|IcBH z2e*^JC!q0z&N}mAXJlbyX#t%(!OF}Ax(S?tk(D78*3D=JZBhn#19V~`=nimDgt0P% z4@wXPWf&1QHg;h~SUfR)TiX{EmEBse<*0JxUrcs*KuAPd3ZwdArc)`YX_h{EJpT?b z7P~sTS%b=1{r}$>KQmnc-Dl%qqRz<7peD$~$SlMNp4<iv7lKB(z=K`j;O+)(u2fM1 z^&%KF88ty;U5udfmqggu`IsPq2CW}JH};s@F+m!f?CNG_jQq~&R<Y5Je#(IXmf0#w zX*Q)@MltqQE^;o;T7F70YLQG=bb>5`6BH#h43tgn<ORY6#j@fpyiCPK)eThiEyV<U zg~UK^*8TsDaSeD6k-3A38Y44<Di$~Qf(liHo0S#8Zq{Jb0J#~mfd$>o7*2kin4#fi zVqzs`<E$SeFCS=eP0hpD#74@}Ox{^ZPSuY|$;(B@LPJDEK|w@CTUsDMKxm$}rG~Ju zoSe9vvIw7>fC#9stir&+qy!oZV$28S?f*ai3ouS&`pY22pvsWzz{SPL$jr?MYD7ZD zzvMxO0y45Pr-LqIfDC$pdmCWSw1alD$g(j)w@IeM)G{zK!sH_z#AT!z8I%;HRb^B~ zg~9zjDMl&K7!1hY;Jqwn&{zU(Wr1up6bBDT7#o|KsFm8fLYm-aQaY~cx{g-j=9*%H zGNS4tOn=?n|9XHL<j(AzpyqkHy`hY;f`<gZm=LclXpb^<o`*pRw0BpCk(pJVksZFA znZ=8Ng_WflG{D6g%fQOo3c5iM?Cfq(Y%#=xdb8jvP*xgLS&NDY3V`keV^CsL0_9$K zMQCaYE`$Y@P0h`X!L4~v7sgyvgzX-5a!ORyS=+}~*<|+Y5D_h2%MXxkylJu?j*J#s z%0<;K|4t>av2Jm6VPFK$shBdIVi02RgOs$OmJ$mCV*#j6gO;%1_7tcLl7bD&VM;<; zQ9=wtf~q2lpjH%Uo<vjxd}9)5EJ#sQS@3U~Ep#S?ao#<qY6IA$hyP#DU>hTY!2fTI z!Qk?;#z7oZS}K6<^pa;}W)fj!VFn$l0vje(L@G5wAqsYAt2YC5J3u$64J!{XFtMuu zZw-`_MOvh-%%}`aD6p|%&{^2<{uw;jO-;-`h-vY1>WSIRo~^GcqQ%Q+BBAB3<KycI zEe%~lUH+Xi)MsM_B^5}pJ9&XFJA=&;G6*x6Iv5EsvM>rVGJyB~GBSca(FF1YQhNw~ z6@@UPFsN$)ZnJ?-!cj(~WyS&*=le|J_Y~xL#kjbYK6nK)o%8qqYo@Qv%Id?$4r+@r zsQ&-T^n&RWgCfXY88*=ID_)>xBx5Y-6n@bB2m=GGxy;BQEhQ--BmkPcVNhgL1P$q^ z8i6VVb<pM>@L&Uc1PbI9@TG{1?G*)Hv5X3v93p%sWoAwC33u4Ys1WO2=+a!2vdG!P z-*8Dm(gc6M2}y-Z4E;TvmLwN}u0mh{_n(;>n65C$GQ>J?i7+xV3WLvmgv<|%GcYqV z#<MVjhDI2em>@v|9tf8PPwX%-GlK{EyFd+oxGcC^B_RenPLY)bG*1Z+J7Y+=nSw@y zmDoT_e~gVlO-VM!thTPG*to=S1$hxQ0e&N?cAFZmiRFxE{uzcRqz5uOsVi`E`f&41 z__xEW+!a(7g9<b=rmGA>4B6frK$|vr1sItb__^3wSeTeW+x7UMY(_5uMn*=@NQf#S z22kC?2)aNO>}XK0*g*{D8OYENOagp?Dku%GfleY5f|lRn!otSj8!Ob5l|VkQQ7{l` z*qZn<YLTF%s1TPN(^Wa$e-A-t_Wpa#D99M9%;CubN(P`4zWxg^$${?xt%HOwxFahs zDa69U3_fp@*-IMSxn)5SgPsuv>D_{R^u3_=A-Mbk7t7KNEG*zb97q(P$but7S_)JN zaj-F{F{*(g1F44&Spx(;2F2VQGO%R~y5QE#jPa?UEH|qlhoCsuc9}>&3CQ)QR)SJD z;!5}>*jU(P_<Tfkn3ULA9T{1qWc~>uE<I(E`gdN_Ls2qN^QMI+D8AGf7??!C`|iyh zOxPG1nfMt&hZQozDiqL6AuJQJvVu=|Yi9F-Y=svT;NX?f7B)5q9bgaI3(u@5s%R># z?7w6BzbTA2=U1&<#n|*ua8oDa^v3}%@7@111h_DQ&i!CuWKd^dU|i26!5|1a?~aj? zjhzd8vMADRO>AtCgXvpAi5NVw&<s9_LXbfawD(_7P>2)MWe^2f%nq{HT-g-l6vh?# zsne?&mH$;V6)epC_mN4$X|CIP7pt9avq4SJ|Nj}FXVx&WF`WdL1E9GCRi;x6vY`F2 zoQ%v&e2grNp#4?SpnC#9XRU(nn*oiKBSHdF4}%A=Kv@dBM1mcZsXzq-EA+xdb5Pl! zC=S|V1sX{Ot>OaBCV3}TSvu#~UtrwxZ`)ZvqX7McM58=?-7GHKRBP{4W2V#Y0jC@l zoXuSWB*cTn1i@}p`Y*uv1iS~-&A}PwMo>>h3fYbDyM9_BoiI?B09=2Hiwg2X-6+E- zgTsx8_8Oytf03<Ify-^ing7n+wvNtE@U>1<R7m(>li}!}X2$Zx!RxcFZ<Kqult6$W z=okeCng0Te2bm-oG(h#d2qQBKKO+;XDkD3a8Y2Vf&R|vs(0wE642%qH?2K$6cY<df zz%@QnJrCN;EhY*Yxq|IkgVyt)wiv8M3p#s8Q514k5F5%o<@LZUd4I29r-W$h?f*V8 zcI{5j*G~_1P&YL(4wR7z{9tLPXs&4G;9z3A*vnxFOI)~Rgt3&EsH8Y=0551GCnJLa z`263$4Dt*G4&1UbOpNUO;DiZjih*{svVx|5nNk@*N8N(jCoP~%5B3bW$0Y|6V`pSw zPiJFfgsEj_hA99KJxED_eaOoLs!iA!<Qe5bzJxBWG8R>476qkeVP#We&^=$EF)wg~ znI$$yR5E44L&may8ut!-lohix(sx(YS5Ia7n-HqyujBUbEsL8cqllZHx0Z>Sl7h7| zc+3Ho-WlW=93AXnTgjNfXG$`%F{OeA*IA%bK%lWiP#Xc7u%Sn$@Ir>+Kn($SlM7V! zse!LX1I=bY*9WkDgq_f&A7E4y$9VeRTP^Q$SNjGo@DWWxK1L4mjNAbM2aS{5oinW& z7#SevN=Y!t5^x)|9EH109O^W1GLdDJ#c-Q4=rk8aQDsn%2sGo#cEc)8UMbexCfLU} z%7d}+mRv}JrePYFkf6VSl##Wm;a)GVQf(hY!%$ODxv2908{;CT$qdR24i2`$j7-Rf zxiGUZH-nA@2L~F&VF{p|1`agP-GXAGLeM@YD5s$W8R#HZQ+P8K)EERe9nM$;$}2?c zJKN|xYimTufZC~|Qm%d_TtWf?0@B(lGMX|%wsukAmTHWWJg1bitsm&<Z19+kG?Nm8 z0D}}m{5DZuCPwh$3eeyncua;No*6XO4DS7OgGy3x%_PIX%-jmTK?7bjF*3rWBOOGA z1Q{6wrG%uwGq&uY(Rq+ZpglV_(1<Je!ZuJJK@^-s8NF@d%;J`2M|kkCh<j=KI5bXj zXHqgs@r<7s#{LX^ADdT9+yjA*Y!}d>V~h--9iiu$elRdI7(m8dLG?n5H)sH<8FaH8 zh|3rY<wiPifesR5U>0QN71aivfzs&5^y4qb|NkI!7*{d<U{GV83tC^sJeT1%xV&^^ zU|>AV^n*c|Apv5(0tcu{0}U3WvN1BTGBUF;F{g4da&m%K6@arYP9@-oZU*i47Ghuk zPac5IR{~*fURiBMP=^DA1tH75KvM{enzLrjn&k%~n11~ANbvagm66*c!Q(He|L5`l z6T=Zu|Br!%0mM&aU|@O5yn#UtR4X#5GuSd+U;v#I<jt%IUayY4F9f8vlj$E^Z7kC* zkiQwdnZAI{GXMXLMGPztI`2iDA(jcWu#TP4o2d~jpZMR2=@8Q?1~mqKkUj=oCLXA` z0MkLHhj4LUsCejq0mkj1@MZ7^tM_GOf~t37UI#V@RK~C~fY$eb{qkRcc`eu+h`2A) z53u^s{{qbNU~$l1YIX)+rYx{HsJ&PR+B*R`gOJgiAs6gt<b5pQJ(r-p6W~3FY>eKZ zZ6*KzGcYnRFb0G5g7-x+djG!(){C_7;{Oc>21YHAUhuvsHb!sIRb>C6XPSZag7@(- zdNXVT>qWK~)Gkm5=>_jwVq^4<gYAC+?G*v-eFX0nVf1F03)YKdF9T@5hbUMtXs-wx zqc><e`Tu|LdaMR!(7A<Rz3e*}^B{VeH*5k=)Bpbu(aY?@qz2h%!oGvi6dYFIJt{1c zOlk}ez0ElFg7$iX>}6+fM$xMb*Sj63UeJC;h~DiedJ*=v;M9w-w*{&f62Bn-s({io z`wm7suz#75{0oj>uzx`fG<NnKjFMpgLiE0b?vrF^-^~cwFAGuc3f&XQ&b|wx9<*D8 z*%Wjh976}lE=C_l$k}ShaSAzW5_BHi3Q&4x^kMh~E-yf1-RHo2?IRpQL8C7`jO<L3 zjLe{wNX#r=4D1YStn6&59E_}-pygHIlbgURLBR9s@P(v|4B!i9734vCouv@VjkqE6 zt<cGHG1PMz%t5p3APg!*Kn=vXm2<qqmzCON>nW)v88`GmX4WT9p8MUw!6c6>YH@zS zvKTQDKLN?9-H}ahT<o6iegX5`8wABcLB;?Y)AnRiVo+f)WVr0WFT>2l#R<Bnou82f zJjM#D9CX-0C-yV5GBKy|FmiG-#4~a;F);LcGjMTn#4~VkfJf=Uok-9~E99sm1B4z1 zM#ffeh(>Nk-1;2!7`PZXn7BC7@o9w~1>Mf#6B+5CsHV!uprfT~sAi}jCm}8-DlEtc zX(zFQj+^0yE>tr%2T!npmLx#dJA+#Ppd|pxpkBDC3Fr<0NPE^4bQh(tX{a14kDIwY zUr|Pxw|}{XGtcrZj7%(M1sSEK#m-(H?txxDOiCJ>?tf3X+xjR4YAMRQI|{0C2X@z1 z_Iss7$0h|t3#38zdx6qQAGqw$0;e-?h9lrKjhz2MaUcZF|DXjNY>eKZECt;Mn+?{h z2i4n*t`}5hG4V3rV^U-AgQ)qxf#Ct<ruPT1GQ#%13i3Wp#sEeUaGo*$ufl}1Pm?i# zVINo=tR9><z<UoFeHc!G_8c<?Fe+f|Ifj@4$|In?hiZ&IMGOq=5R<@Wf%{J&KQK>$ zxR1dJ-4D<+IR7!JfiL)CWAv7Sl%1gZ>nXV20Ih>(htzeDaAE005(n37;C)!2`s@dT z8Uv_4V~5md(DEB84j9T94Kdt+8V8^~cSpf_J@Nl{<^b?qN)jYtD}cHs%#3WQpp%!u zqfM=#Mm=jh_(&V@$e9WjC7?^znjs1qSXsMSeZULwK+`C~kXu?n9aK<j1#|`@c*qPi zQ6~aUpr+u<<TRr6^$c{PqT(Z?|J{I`4gL3)u92A*WAVRUuax+J<^MK=);WOY2?Utp zn65GiGRQE5Y!~O_WM*WBO@Ru7TJQ|<OwiF2M#gqf3rZY3UjUJU58AhbZgdqE7Gh)& zk`a~x^#~aR83mzZ4`QMsd`!rVJ2p{4Nb9cEKFLBWeo+Nzu2U}F#w}2J#X=vZt0p<Y z&aDl_hR>LHC+gbyS*E-GTj6%OE*R9rgTyJ*bWl2BFa)I&24{wg7;ywzKOhOtCy;gl zY;Pl|AVX>waQ8E%fb}AuR|>8Nz<R;;26sP`Jve<r^g{R7GqQ6}gw;7HXA*p4e82?T zR}WGHsdEAu7#Kf*-3;1w&CcD=6b`l*`5awPyMPgFFQ_2`QsWG^7qUMdWG_VTM67A@ z8xtegUWnd_jItoT|4%V6FdBmG1-E6m`x!5w+xveZ0|O%m$X;+;hMl{g5we$pkAZ>F zl^J9&MDIj~RS>-h|Nei<z`!U2vKOj%BB<pI%}4M($&4VstOe^u4rh>m5$zz58st6L zAbY`WDDH`j?GU{Pdl?{lLE#K4OF-eQ5ArW)*9TKP*uS9e7Rbz4h+d>~LIoHY82do} zWdLnF1*s7O>jka7i3j@^qIV*rIYcj#y&+(|AbTNtCo+5h>jk%=!1jXMP~81YQ^0zW z;}^B<2C_FANiWF15WN#&?I0w3A$mdff(m?)e@($<j@o}8CQk6Ynu3F@l7u)f4<{o7 z2e?p!G$R;%K<lxYcty0uP3@Q=tJc`nktfWceQb8{t_n8w!fD}Zwx-HH8iMg<=5BV@ z3A);rl};Avj`k)>)~d-@w}i>sIQi(>xJmJfa=A(B+3Ffw$;;VlC@K3pSp~}q7<rk* zHYb4kx^n*oKzGYAs4=KID2XwGZeUXo<>mqnVjvvY%itp=AuI@SAh=Wj?L@@tKrwKC zTW-NJT@6`hRl$gO6-y%>UsXB1I772YQ+;h6`;6Tm{FQP;jjcs^L^z$q_05#@3`K<< zB*hdxZS*~*MSWeu%Y49L4vOn$aD0J|3uI^XhMl#G2=o6p85o!(!S$s#sGMQ+{=X5m zz69N)^k0Cfi|HzZC_}_nK}KdKSlJ0GB^lzuqqpFe9C+;oxcmf--h!8mLAuB&vf$BM z(6%4&(h*TcQD_MYp8E%l*MJzt;>LDNjFXF7qWq%r;z32Hm|GCj)!OdA&P;n_V=L4D zMKYa^k=K(1?c*{3KOMZbP>aEw!I{wmIu0$&$inU@!^X_Uslm*|$j0m?4Owi4D!~ep zaDa<(fE0toK&SqI+IpanV`yPuVc=wA;RFr6wZP}udcC<hxR^nUnn6<@-Hf1>L!bp` z=Ad+eUkiAhZZljz10y3uJ7`~rF<d(b78@BkIG~!j7(rG;wK^CxFf)NRk}^UZ3)afc z4jxbM^#<+n!lDbb)=kpVLPy))*23A++1OCqT*q8RRa`|wfQMH~n^oP+9CY~)sGSIE ze1q2bgV$dh3yO%FnTdmwDrmBwjSV!L1DTx`6%hk*Aoqldi7^=mata9wvam`A2c&?e z`@zezBluJjjcgsGG9=Um**N~afiBW!JY=CKEv;@L#HWzS8f48Y!);;=U$=ej?=@3d zJ3U55Yh4pB78Xg%PWTFLHdzTi2_1D776v8;tN-7aQo;AFXoGJdV`pMz0$n7`z{<kJ znhv^>t_iZlJ{~&iq^Q8ipsuQ*t*9+3BFN9f4H{7d4TwR<p+R?qg04;gt=58^sRtc9 zRR>RCK}Jv+ef?@Z{M@a~qdfimJ)<nFU4q?egX1G2<Kx1^xh!*hgOWuhl3gPrT)@aJ zNm4X9&^O1@FWlERB*fPjw2hB}iNWaqHzp;fAK*1Y$=f6$PKS)ZNHZ`Ye9(gG18MNJ zp-ha;Fg1v|?rsL3NC$BRdC-}q@>&X7P%p|c$`Rp36d!81=R3IA85l&^Iy%}$7#i5P zw-*Nb_yh%cd2?A6_=lv635EqU)&;n_2GlhMh6#$N1(!E@#svk&!~_OG`~GVGtw8f7 zOll0ap#CFsJR{`13HAS0p!p0YHr6U|xdmy1OosGb8QB^A7@mUb8MMAA;{)&+Ea3JF zqaVWou--(-nsX-b{Ra%}3{?z!!Q!CyEYg`i3{~*9{Wqoy;4uYoJ<PrXRu3cfvBC8) zxQ`94huL>9Lh4~qUj|w4PMBV#dKg{rP87W$KZ5OL^kdY+@FQ3+*pFa)8T}Zpf&Bup z7u2^0^+!N``*yH6SUoeSZx2?_z6)v>C_K%;{TJ{#5R5(yabUZU`&^K-96<dR$XO0P zptIf?m>AgqKVmEfj|IvxBsd7LF|shqFf%c;2r@D;F@qKzfr>2f96vK73uw5k#T(RY z1{XZtp!F4!pyI3<G|>-IfGij3z$YpqEh!==DyO0(!p1AEEv{~64!RTyvJeMSw3tKl z8M~O6m$QmiguJSwqpO3yqadfyY-wqEDS0_YSvk2xGp5A9k9Zu7O*~X&1$7+c#H6Go zRCFNi|Iq)BnC>$vF(@<WF)W1C4&r=_EDWIio;u7-Z0w4RjBG4kjEs!#(hd^XWx%to zpb9|&x+4_iS0?a5&#m4Z;G!5*3xLO8HQ-8^m|8(wL%<W^;9+FY?ie;Ua9IrQ^9ZV{ zXlttIsp_eys0a$Hazcw;Qxj0Z0$N!v4qa9y3Of7|yhRywq6AW)Fm88N(W;1aiE!s- zm5MP=NP+~Ao`W!_;4E25L;xLVOo{vVib=v#OEWILna9n@!a-S9P#YdpObmSgA2C)j zO=gg0Py?MP%*M#T#K*|aq5=vcNk%4CHWn|?&go{D_u#8GARz$rm8^`iqKulXnu?O3 zps)zYOW=4#4BX@LN}#if7G#}?f{KHai=&>SFsI;bh##0_K#|O73m(P)h3R@oJK!7h zRHh#ciVV6877SI}<%Kz!AzN5MiBu7Eb_X=7K=nYcH#a*MGb<}YJPRjeX)EZ$Eha{$ zW=M2mR}<-=sIRB2#K>S`q-UXTp{cH<tE>w;hnSy_ofWjW0lI-1v_=KCUkP<TrLvMb zWakpNjuaIUQv{`OP<)GsF{%{Q!uBx1_gU5#bXv%!*m`<{b~1S|p7jCky7YkUXtGk; zgV=jn>uBRy7@v{`+Sdf$>71I)zziDGVxG#R#GuHa!(hSS>)-_n7&cZ$FO&cP?dJux zC_w&(O=sxoLcFSLp=Y6?rlg~+gYH$x#2YB9LYDkNhLOPCbQ9247c(>PWzdl6M@7iG zD3E{Em{#EQ>c71ZFEScK{mi5UZ3b8=ojD2LXbSQy)RQnjgYr8w1H=EVOm$2@82A|^ z8Dtnr9k`^$g!s5wA)C8FNkIv8u}upX2NOFJBLg!#LmC4sD|jlS$(s{&RVsLWy%Olk z>}Cv=pxs_DMUf8TQc?^IQZiC9f&vVn%X&fc8iL%sQre2@%FN2b%IwOF;KS~P8I8^L zm_V(5admbk#jwfg&grh9(Tw7Oa$3$_SN{b#`?B!zFmnj9O*GKC`|k#$#$BefewA1L z-eJ_e$D^*!EpIJ&qY*Um1HL<bD^n!X4+aGWO@?X*F?nW2W>#hmbzTk@Rt8ovVLoO? z23X0g2)cZ_1+*R))M$X5Q~}y=3%$n&)arClWMgDuW@!ei1f5a|F0Hyi<rcVi)eRo@ zP*P-IP}Ee?WKdvGP#03w1g)E7WCst%sxyNcN03Eu>g>v(7Ly5Rv%a`FJ2<|@h1Hn! zg%xE~c%=CDsc1tsyc+39IV*;T`_IZ#RppB;ZZR|4Ey47|oRiOom7DeN9WL;`S1#{( zjrG+zOJi9zi)Xb(G$}DjfZBNG{|y-LgZ;0@Q0^ciEXc&lq%12T!otET2RiP81?GFu z*c4+6WKR(2q)zZgXa*J*_yPoJ&<0shyP1)Jv4sJv67c?4H5F+|0nq*wW(EaD1<-sR zs}ibLjnTt`Swvh|S&31{NYmRnRM*6@$b*mH*Ev8&M4C&Iw?kS5y!rKnS`EuQF5b%@ z6pF)=YpfWrKVs+dWZ_``*USUn{>mtlV9Xc?szJFJ7#Kf;_l@d0XrXMgXaX%)V2cAC z)!obny7hyXfft+%1vz-7K#P%0LBnXEd$JWp6T`!!R;^;RVDxckH2-(f{SSB^l<WUi zCPk(n3<3<o45<#BLISKzpbLb(AWcuu)>_6E@Tn0T?4S#VnPWk<IC#%vGlP$`gB$}m z`GA5Dn}SFO0r2Hu!XQTp2?>A(_7s&(*_DMs7feGgf?yUDHD`2=N_AU(pK-CMo2XuJ zw`<=`#sbDLe?2|_e^(i`r%VCmZAJ#;{{oCVnUol;LFJe%BQuj6CllzRJ{Cp`MphO} z@YZM8o@a243m%8>289tM*1=0U%uMw4loX^TdAS*^8Lc7lsiqFP=@YtY$kaqlO<j)( zT8hKZ{1p=i4<LY!69S*|4@&fm8A1LcLRNxeJm5)aF-ubuFOB>t!%$T@B`<X|ODS7p zV-L0LcrF1pUSmx`EnaT@4?clz0<4_u;OS}5f>#j@3mq3P5dk+o5oI}XIXPir4NGnB zKt=&oQ5_8qRu5M2ej)Y$0*t4bBp9R_oE+>ycQWyFGBJX-tTRe7GO)0CF+g|Zf~VD+ z(H8!R!A|IvW|Ri?xIz6J$eI3(pe0D4b9oS6YPRvw<>N85RAyB8_mQ#S-xDi4L2e5} z5qW-Ir4Pz(CfXWSjI&&v{^fD`IO=Gyv3jtwgHDoWU|^iXbcI2bLDxY`n30J=h?5Pp zF$3f<$fh;$;^$^YAJ8=&9PFU&ouHj-pp~>DZ0v%{kP%bp9{>I6WpNo%5t;VR%HeW~ z%Bf6O(jvXP4F7#(bdt$)GqINz^N^AQj}P%MFfdMFQbMwqiyhToa7uvM%gKRiFK9~M z6f`IUw>L{A$Soowsan}rPEk3UNhvK-H!e&eX@X3iTcxy^law3-GidJ<QwY;31`!5X z1})GH8$67l`UTW9Vq{@Y<zQsw1YHlpkP0eQpu1ez*qCFvAm@fKFlcFNsH>?!E|Opn zVGvdn6yyf&IS0*U2|^mc>^Khr0Zn!#1_hnLegMdnDXaY$F9zT^`XkH_w0DE+|2L+K zOs5z`8RS4KI9ZsO7&y7unOQj)SW`hQfF^I~i9et%uM7-wpq=re45BKaObl|Vw6-v~ zTmcQwgUS}j{cfhF;*ha=Q$?<TD8}N*z(AXtIOsaaf4z)JevDH8o`JAmnpe5&#PXDM z&|1he$ovMVpArw6+h@!GFIh<Zzm@R=xbGp(AkDDVdxKCwgo7S<>N=l`k)4;3gOi;D za-I!0BPeho<q~*mEfzE#0V<v#g_te_sCXh;lY=gzOhahGX-Q<HgBWOut2k)45omoM zIFtnVcxAN}l|eV$2r5F#Iq-TrV@6|hWkF+eWyTNBgdK%57-e}Jc??}Gvg;i${X1Q5 zv0lkB+1s1xl%AgdeZT*VzW4uL0o@tM!eIX2fQgam2ZKC=8iO{2KErYcF;+&<k=yKS zdIH?c%uJGEg3K(;EYN~m7u0}h;o)Fn=jCK#VrFMb1D_ZI@&#nh6}+etQa-YPj)4M& zQ8R`n9O@$-)OB>!)j`82I{G^L>e}ksD#{9KifR(#pz}MpI9ZrMLn(aVIY-caKcJhX zmD#a16+m}w8=I?v&S7F_7ZW#UR1p{Rb`CPPy=0@HW$dq|8DMUr<LMl1Xknf0X79;f zV9H%CDXFuSNmHLmiRIQ?nL<C)wY<VYzCwb0S3W3~21V4lcm#UPV4SJtEZpE0ZqN>{ zJN|F_@5IOszTef#!5p!cixqs<5%`1x$Q4t`puM4>b3S0}q<DEa*cc=jB|w8F>`L%^ zDol|!D~hnONT~1$sEA1F$^->P1qP<=18+tbG1Ox;^J8@Sx6kk2#dPpqu>U9j3o!g( zddR@dpv-X9flC0g;*S}0f)!Ulgo7$OBP$1Vq8zlQjDdjxd>R9IUX3Z9laYxDv=9lh zydI0<F3=G@7>ad?H`zfMRIIWwFoI4Igt-K~VVap4Za--7P@I<sI$JL(Aub}sqs*(U zqQu23rY#I=`-4WZm6g<(;Q<B?FH`6q12#W7F%fxr5ivOnDK%bx1z`m(Z|?|iZx5!G zveMGBveMFjTG(0LSy`108NJ<a-gN)B!y7am0dD^aFoWg;lo^Z|8XWi~n3>pEc^R2l zKvye(ntq^ex)Nwt6ZCK*aDYJ4IcOCZxP99WI=cz9S*i)VO$u^34i1ITT~OMZDn_bC zveH5V;BD8+jLOiJhvJ|Kb#S={Y7c-eC_warKm(_U`E)bTgt{7Iwr7zHE5AEreZ91h zvo|v%t4u*ae5<Q#dqPr9PI5%7ib@>Q6$`zXKhL47?!AK3g;fOgEMk`C=Pix)o7LVv z%Pii0mVLY#sMrMc@t8`Pl)(2rH8}7~5(<-EP$Iz<CZPU2sACLTF^EH9q=Tf2G9&bk zC^;Eum_YZIfYxH8hl!dgB2bW*7?^{W7>L*hNU-p_Nk|Gxv2&_O%9|-LF|&yWIwV$B z=Xtofc?G#Kx-cmjD0}{0V6Do|0@?-P>1ZcpAgE*j>KS?##>Zzz#|fong2RFfbVCYw z4~jHsWh6Uj$(k2CBP(coH7ipp=*}yKSkO{a&{lCq#%4wz&|yg8qQcT5(vZt}xVbnv z7=##wxWJ_sC?T`43oA1#LsqE?iYr5p<6=~UYz@m?+ZPocejp9BL5%4Xd}o;YznhHe zK8)dh|GvZah=JPieN0LWk_^?5ktl9qZYE|nMlT^o77u9$ZXre%R<H>8MjlW~mf>J$ zVrBx}#mWG>y^19sw0#EB!~;!LD<DgQ*UUr5mtbuk_{6#@r1A!}zZpf9ML}ykjZML8 z5kVvSkTxzmqjnuA6LPUg23zne@p1Di@LEK0G1|NT_~CvtNymYiIgptpz!@CQAi@m1 z{#^vrhF}A`(u<Rwm6@4?i3xPYP8z7^*#z2;1m1WjBFq3@fi5U0BqYcMUV*L*YWArs zn;NSdgR&7jySXxxPFh`5eX6x<lJgsvM7OKHGnu^no`ZHCy!LBoXlS@U5p@2_e*wmS z;5FT9pmTI21ew@a1Q=P_n7lZcnHV|QnON8uS<*m_fL2gS2aip)v-&7235uwKZVML_ z;Nq3l7KT^~S~d)x<$$*S&CQvWO^t=YTLzTH#l#rnocGzsJ6ncy%0|Gp3|I)tltkO@ zU}2Nu6X_J=mtd0ExN)QVziEj50*p%T|K3RkYFb!odVt4g1OI<#Vg{Wt!{EYXh;haY z6L`K?h)Ib-h(VqqeH%X~6ALSBs!fuCl@+o@xEDJ3&&-B2XayQ(1SwzupWX!8CyYJ^ z07~J~lEU&L@}Pq(*ulF$K-C#&{|~sZ28F&Lc%wFG+#f!f13y)Vh0hJ!i8`)-UwELL zrUTv!4LXNViIJa0ktrREKOyJL{bf*K&}LYRWgjaj)hi+G$O0AR-~nZDS;QO<+L;a7 zg%4R*4l2YU#W(7XSI|*J@NKW4(jNPcS2SgygNT$A85uOx6t$JKLEE{&TV53y6+neO z-Yu`}ko~UEl~X3?gjFG%U72C$5+*|qD|{d3=osOF*yw5jAJGCGSXkKe?{-ppT2@xd zX;8l(w8ncSlM>|oG|*kV5<E<h3xb)w!25nc%TvH-oWKTaKu6ID@Nu$(j;>(k710)k z>?s8e$w8AhByvH^;`g{^+c+1w={QKsIB5m=`}oT^OGrzEGAWst#V0q|^7Hxe3gx7v zB=GTg@__bIFfkbXKhO9Qygp7Jv|E80e3}juqlP>;Gb@V(BQq<Dmy9$MvpWM5JLm*f zmUM7Dg67)5TfxC~Z#T0KXrY?6rmDW0K6Dok=r%M+VGCK`Cn^Hn5f15CfJV;L&CJZf z)hTFGF{m90syQq*eI&rgiE*h$>U#v525K1uDsTyNICDw!t2q`$Dmp7@cxjj^F)0c1 zfX)>&QB~D&F*CB46_*s&mXuepl61&OuCe+zSy5e2MP66|)Hedn6}2$^U{GZ+VrX&T z7v*4LWm95gVN_;hfDbK!jyVLk%Ag4oltv*j0O@yws?K)M=!hC*VwQ~!zrsidNp&?w z20a}$BXuJMIcX_jL0)bKRYp}%vIF(iQL9f-i4B{J1qB7D;lXGGZeBn}l9?=&ER|W9 z*d+b!kmg-wWQFB8xYYlhgU+~qko9!5=Qrk61RahEIUnoVp99b;mQf!*!wPCYL&~R8 zCQ!cB1?5{|@J32TW?9eyV(@ksZ14pfIN%Tg4J?CN6G#V$flf97g&;UgkX15(vLh%g zAvqenFAscBB=~?j(C{>*h{I3@%eR{9in>a=GSZ-pMeMAg(K=`VLWgp|fo1{<KJZyh zY@+zeJT@klN?{eq;V9S&KVD;g{~k`zp>@*oe>*YCLCAgMN(}A{9!w?-3<nq-AmIoT z7iW+Ios1>Q$jl%D-l53A#K6J|I$;vDp%0#Cz=7Be8Z-gz<P{R&<K<)ro!tZ)wnEC> zkh|4EgBYe}X2Oc#!JOZ|C63nF?k*{&jQLEF0pPyWoqz8c?VRmBnUw6Rq9W>CY;rwK zqvOH7r+-^rLP1N({{Lq{y?+W4_u5QK3?dAg4E7E-Y>c3~33NXrGb;;dfhrSZbPe34 zggIDDl#xMI8FV~?sHT{v03U-0qX?+Zfy@~|M>jy_5^{SGR5-z|Yh?6I$uQTGHCHyu z2xx8XYm1MHjj{9g@zs)X4K`;|3Uo9v<z-=!G11iZ(wSD@JjE*`H6umeKu6U>(LhNL z)QR~I>1%;b^|N)b!rJEpx0yiwENGTxWDo)EFalpH$-xE+XpozcHll!feehn47;5L` z17?>6bQh}t6DZ&4GFUN8bpV}(1G?6dQJs;UO^}h9MSzis8MYBw8(dt1+NLeupmr*_ z-O>wMasq3sgSuc0kRG6eCP*Rpph`k&BORpm^%xmUjrFYbtyGoeWkrO*d)ajvb)jus z$QETb(AHJh1&5#_$IKMAC>WGyAh`#A#Ve>c!(;+IK1i~(8nimt)t`wGyfnBZ!z4_O zjfrKBi>Id>XoWEBIHA7{#U)CCT7hn&9P(0(fs>jWrfX;lnh1OJ{JWi<oSYe+D4Lr9 z+7AILhlN0AsxW}o0W*TyM~vT?lpuQznLt-3GO{p&#zBP{S(!ll;Tf2jz&BYl!V@F( z{7&$Zc%Tbg1^9Wm*jZtF4H=DKl?}KIHAOwBjnO&RPC>y<VVkdyim^=bX~TFck34&i zN+u;09|7UhiRo+{limM)5)y%&U<t|F4F81~|1e1~NP=n)2Ym$*q_fys!BgXq4Y{DY zL5h(<N>y4_Scrq2L6T7tG%70&K0v^n*;rgm473Ew+#G%w8)(y*I(TRFUlV&Z>C<s( zd`j#rY$9TuX>n)emF!LU#e{@~g~Z;un;OL!GoJjY5UL<zW+wXapSg96mWhX_q>PA@ zyu71`tRw@||G)pgGyY>zVlZH^0kxmyL2Y+Uac&kC7Dgs7a3X~5eS)4K3qGyF!c19F zNPvTl!GO^K<Xlik4^ob@LzcyWI=?1n=IUx{>T1x<&|>0Xzd*L%GJ`z;nGIyT!Yjhb zD#*bjDORbXU~gh#ucTBjAj!wc%gQOrC#NN2%O}Oj!X?9NE3UIuP(+NMUrd5YiG{_L zkws8$nW2-mv8}C%wu{~xaY4{-L1z{=IkgaBO(6jtp)dudf6K+SB_zZpB&2jfX({l( z0+T3{1S3C#H&Yg@P0ENk-x0Ew7F4$-Iq-q*-DGEEVg!xMfGRdnbp<+ZmmwZ976T2E zUT-$gf&SpxLeLUz&;}{cDXQRF4Iv3R<w}7;K}lExbnuO&wy_{`4TLy64s==%sQMNR zt%9GF!X_1Il9(J`5#`KO2&-%2Am_&YTg({Vm>T!*HTbSh(D_4u8MGOiVI>R`BQGNx zt284En-n863#^1u1xGHZtZM;Hia^~8KIn;og$3L`1CNa;gOosggqRlt9S4P^3Vh^& zrUs}ykq`r?3(#%IkTxf*JVElE2xyuZd@G=tsUU2Hg9+kL7>HLZq2-B0MU-;{^!x*9 zd6<{&dpKA<nc1Zj7!QKI4mpdAiGhoO0Xn88&afA9F@_>1JLr@pCT3P9&`xZ~PFZkc z13WkeiZ;j?8diniu^=Rcpz$-T3Q<jTkO!B^pusa_>!G7(&CEWaaWiQi9!3TpaUSS+ zF=#O)J1YY>BR8llhRlXQGLkT&pDx-287-6R0gaMHFg*m1k;%$R|BD0n&2K`+(1ZSe zWAtE>U}9s0oPhz_IV8g*!NAR+=AaDPGRwlu#K-|!rV8302wt7i%mA7-<YwR&1?>=I z6lE6_7gaVj7gRT8lnDs<5fH#6VIuUeSkS~okg-?D1XRv3GO#i*FqVSPn$K|H5#VKF zVUl5FVq^e~VS;AZ<v<s1GcYnSq%*LvF@mN>AZsVV%l<)k4q#UR*|pxx?gJULkY$in z5>ggY5>f^YS{MsL&QMbZPc91@3xb<C=Ek6&ySm^+PC*U{33g*POMPC!2u>jmaY=R~ zHVb_|LB=8$R#{nVS5`)we=|W$D>v4EI~kb%e`8=^3<1wC$T4JY<zZw8pGpaGKIpVy zc4kKQbkIyc3nK$7GXv<9JxB)=e4-r=6|AwK#%nWZJA;_02m^zZ1Za5^WZR>#sv;LJ zDDy+&7PJaR9D39pvoWaI1D-9PqGe|$s;|x~!xfQ{QLQW_DJ&q#sA;VyWgy4N$tduz z(&OKC#zJm!c@Z8V&;fjqyw7M0p1<`3--o~mIWH12^C1Wx^ar1G0a~vEs&^a&A<K9` zr(ZzDA{}@c8NkDuxK5LNtK+I3uA(WZ#>=Mx>NYSt{yAV^06vif)K3MSf0hrPo9J{9 zl3-+J<7Z@Ihux$Jn~~69U}s=rVP{H*thZoh2A_%03hFU|*A7FnDA<Vzr3h85tdPy5 zpz$QQQpl!oNmy0_?`ei^3I`nz4=rv$hi;jgD}q*3iNd$PGp1R`DJUjc`6lE?Ti^b7 zAR>Zs`R&41$BGX^f&l_jk-j0`wx9nQG5vVp@P$1;*F6`sZ1MkphQR;dn1jG|NdWV5 zEOklXe*us@g9?L*gCQ5_><&gRHWnt(VldDQ6+;3e=zKSCP+n+f2jx8#1{Gl?VI^S| zbx=BIg%6a14$K237SM5`pgI$YX8Og##4Z_XP^ZQ!9cvOD6`t$o9G(|w7xCd0zcH_t zW|W>zTsmXyzvcB&VT>C8me;4mF^c{90csP2_B}I)F)1<VFjP74YbrCbFmo|7v2Zdn zf=`_UjlRf(-5Jlp&dChgpavSnYX%LSgIo6PpkYU426lE>dxitFehPHLJg8R&x_JY1 zsv>x|j3j6vMM)8Kf+}c&fRBfrl|hG52UN^~Ist;%YXoo=s4NH_ngp$YV_Fy;RSByS zVLo-|WrYSn43kn^`aeX~aJW7y?B5N>$a?sSw<taET0qACk0IyuF(@$PI`GLdGJ=i* zViaa%26ya1UY7x96;{yb8e{|&lsXaBq5=c>s3lO=fp*Xs8NpNSFeTviB9Q(kCp#+( zs0Rw_sDZt!46XH`V;a!33GpanctTQmMWl<0t+JRZH-|j0ygYbCAJ}W4iasUo-$F-E zSx$Cm7Isx@rUXwdO-SBn>Sj`6P+&-O;FDxzW?}=aXacp3p{EN<GcYm1$BaQEgP<e; zjUZ_UdB`y<&}K5Y_tp$w`2&tZPSCnURV6mi%0%Qj4mCs-4vi==Q6@=wIX-y~ZZ%P5 z8x`a=i5}1)1zAr=#s$dh5J8Rs?NDJ-2A^jl>mUWn8?D|f%uGy7kn?6iBgSHatf0AZ zL1RYPfsJxZqJp70jG~OpOu@PT-hqz%lK2<EXJEj?X!iF7<9c3WQ~rNfA$1~nT`Z^{ z!2~*B7u=U+)?rd&kYG?@_yQS)=VD@HV&DfK-y#j!*9H<{<Y#2#1RcD?#l*+~<-j%u zC^IlKu&^?-q;hjGfl>n_=mwQuZ%#&5R<?NPVjQSZW@t)57pH(W4=^z?fv-qu1vSLL zMPW1iVwP@DC20)T$jAY`a0N2d2fA1Vhc3_sD^ik-3<`3RDpD$-BRY9Nvl;ActSro+ zIZV*{L`EZVQ}D?XqRQaYGC(yb_}*&Jp#<Pa1`Vwl#Mv<>{QLa(FC*{21#$s~?)eU_ zMXurAL4iI#OiGUCo|8RSd78;9I@U(EPVmZs9=XWC#9;CN8`CxLIJP>2vx5UCbgY0G zbf$J4=;9Q};0~n3U`zltM7kk!Fv^Ocg$BavBI>-{9BiN&7|`eq^y~y>&;g>L;?Tql zF{1`vlMm{>fn3~~J=HfUHb;q7AvZS3ds;zdgN>n~eu$B-kxdPgl3#yPc2iVTQ+8s% zzh_fXP>7dBbF-aGNKjD|B%PT37hsyl^n;0w@h*70o*&YdV&rECWt@wpoQKGR&Piy6 zq#|zk<rbhs1LrZr=W>L=Yf!;0aAp?h_&ubW7K6xuGbm^<0yL}(9{mDIKu++1RC`c~ zNbpHcpg{@_Hqec2ppjnC7=<~wQ3O7I6+D>l$FI!G%`4BR<*ps!!NnpK#08$g(G4_X z{P&k5NymYKk%9le0OKwu2?k}*x*`D%CMITnMn)zDMixdz$hsoXffulH9ejc$WLpn- zDV3azq=bkNFE{8ASWuS`ZLCrq$wA<h1s<u)RRj%AvPcD4AURJ~1~fPcZd}Ch8}mZv znm{g<gHQWG%8UO3jG#F*VTLqlxPykF1we<pfy14dk(mL?gPt=bga~qQ1_I5WJBWb~ z@&L<#0u?5KC163_T*&+wXuJ?wUi(9*$22{_AuAcoqy!7vzn_@FK?^CL!ERMxsDZdu z0OVF#Ay6|MG=K^^9!r3kk%a+S2<mFo)3KN!m!*I^XQ2I6;3+^zM1Y-+sDQvHK!f(r zbFhJ`A5g5I_y8UckQ3;@#Wy38ca${+<$3uOK?fFkfF=SF9{V>7+T;VBSC|M&J)pK< z5Ca3F2lyNnZ3hjI_dpY^uu%la4tH>2&d2~hi5J?!1kI&D0u{93%f!s=lZb&3H?Nws zvU0eF2a^&Pn>P!$j={e+#%f1TS&%(K{{<L1VD|7aGBNNnGBPnCR+oY6>vqsUDRN5| zJVXlWS%dbLfhIjN3_LW#m6fH{c)5iPn3QBa9sf>c$}-U5X7OeNO&x>lBY7qX20jLH z24e>UPSE9VUZ5-d7+IJ=>$w;}cgeJRgXTLylgA7UpcA4P_!#&E#i93xK-O=uv5PCK zD~m$sij|c^KZIqdSSrj+>uRdAb2YK+XOal=F|bo%e|YsOqnwVuD(Kh`(EJ$#1Cu<H z1fx5PF9QRUgac@63-WCgps+ChFTl8oNr}N6lxH;=Sy{A1nV4Bw85x*)7#SHrhsLvd zF+dLo09Whau?z6gkl-$VJCl#9j+&|pxMEiY?W<OYOk9ETCpcX|TPG$s7eX>_DoN5) zQqh1X7JTa;r}zc1xgZkHM$o}LxK=(gFo4@>jPgu>8DtqEATxx*EKJOdLX412C!hft zF?h}epCSeGm9&EhC^s`QgYI!a43>g(H8fAiGRlIAOHcxY3|@f-JwfBpu=z1T6Em}$ zuvQpSgG|Mi=`XbX^*0HrEjG`|Ll()+stn~0LSl@pEaIT~Fi^9ak%1A^2Ld@;fsK(F zG`7SHPD<cWUGRyd@D2@lK`jF-GYe}vl2TBe02*0nX8`T!Vq{QMke8E|6cGZ==5nyX zCW=5c05sk}qtqaOfQD^hz62jX$;PG$^P0CelIK924^ZDRI=C~6J9)@Lx&L0d|Jx7B zgpB_`{TE=A2amy;JD7m(-IL;GVrG<PWMF0n4I6@E0h-=G(EzR@B*eh;k<efl2OaQ+ zd|D#-=xtCH4oZ4Am2Fj|rMaazxRgZ{EfnSDdF45{RY4t2##4qG;H4ijF1G)Sp$$sV z5`_Pgz-|Ji6%Ej8f6$?00*s8{^;Dn`S7c#iWM^b$V`K%5L4aKd8C*vSa(U2wDxf(g zgj&#$I$TAhgEaKkIt_UZ&@ok@Di1P{4C>54hHZ_Fu>?3D6L>xy)Ta><Q*`im@ijsT zbVhYMU1<d!84a-yPA2BgOks#%|GV15$y3%tRfk_hMg(+a?f?G_V*gtiBf)D3!WlEM zv`JuR#!7(B-r{3qVqs=vU}6FH2S9g`HY1(A1?s=RI&_Q-khN~Gm2RLe3#$?G$+n2o zVZ|6Zkd7!-HkmzJe+76PK-R;NNe;Y0=kF6k&=E-tObnoPrU^`-`4>5coUQzfobV+L z@}M=RT-+SYtens_r>&sYFQW16po~ogcnA`D%V0NXphy(7I#pVdL7YJxvcN@9kcU@B z+uRg%d7vU>qDB<l76P>$O~pZ{+nOqJxkfUIf>yflvPi|4Bql|cN4oxd{n!1<l`B`= z|GI0b$ELa1H6%xa%38Vq-<a%~Bp4Jxt6U_wnHZTEy+E72m|{UEo`S~+ArlVZL(F8P zr6i$eox=ADfE(AKQ&vQg#=k+=1&b<!78fz<y5~EX88^K#@Bp>0l`Q1nG-b7O*%tbF z<*0kQij{^1B&P%z$GGsidWN=_w=pn+#vqu)!TGQd(yri=W@KRi4fKMWKp+OAmoy_2 zeBT=A!V89YCg_1?pfgZmTZchCMDQ34yfJ~01#i}d9BIo5KB5uYm=F_%>_r0~$^pu| zps66p;h~HTdM>K@C1tr0&K@4l@@jI@ylPBJG9HdoD=SND84dnjPW1MT?>ATF^k!iL zx3fWwx<BCYlt|Fr$pHoi&W#MPvHAc18My!dXJ%&-XXIyyVirPA^PrQ97#JA;GIKEU zGsJ+_1fl9@U<40X-eQtq;9yX8P+(^PRSk?@%%CG{k;Xqc7&wH4Sa`*>8JR_uO_`0w z%^7bUh&Yte)yX9B@95pTjMkq%fzI9p-^nS#Ai?0_;L5`Yx|W!cMU;`1Q4DgHBw`6Z z{QgS@(BwBLvw;gkP>&4MA!B0&^~S*Y3{)VRDw;yp;VUAlVaD6aR!aX)!`9$4DnonM zpwiIO3bFvd2VM{|F{uAv$t1!Aa+?x^wSxsWBj}hS24*HNMn(pA(8_dX7Ix5j4Dd;u zpd;*<nHge1w}><|_^2qUC<zG)iGWTe6$h=fP=>k(y8lpwO<9@E95e_8-nRD2nT>~S z2Fzu_nh|y;PF~T8LdLGT3Zi1tM&U97vV2e{2Kw1)nY*ZE$&2wx=z!*8`2GtpUI2|z zgU`2N`v2y?024El1cN$*5krzgyf`B>ix49dtCl<y8@s9!69XHI7Z)Qtqo*>+8;HX& z+1VJ`K_xm1=={Z2@N_A>yw=nJt)<W~(lnBk04>TE<mX^xP-j$!7S^C;B%q2BwCrC^ zSs8jLCVX!PJ7|xkx{*0}yjfI4Y^`3TRfL~)z9};cc%<+g<ILMG1x~y?!fL{nDvD}( zE{T@<;kx>2b{}Nzjm%tBRk$U1L4${04qsSI6Fp^3C4@A^%rq2~)I3d;<rReGLBsM4 zJm7r`e;Gs>JRNwz`z4_J79b0o1Q|f%3ZMa6a03CnwUi&k1&y<V;;|XDGm96rmjZK7 z19US5XgPqonc4nyv^@=fL3<iP75^P(%$CV@uapvZl9UJMMJWab#(7MC8QfVCLAQcJ z*DNqH@cjSAIDzR0gD8Wy0}nqB3p3~>PtdKmknStUtzcI&B!FB9?zswrc9b=PT4^vb z@F+7MFXUn`(9RD~dj;G<G&NCE7c>?G$H0EmXk|q?U*+nghzPeJl`JkfDJL=MO1C^2 z#)5zQ6vE<kBS9MmK=Z+Z;IlVm8O$9_K<AE0@h~wnLE9osOyIf=e_I5+`pu3BloTLc z5ztYypyCYF6k*h<sZ@83Hqr0}uP`!lS5<a31aI4VzxR02%xHD+iX!`-;?gc_@NO+o z+6M2<X1dCx#sJ=%%}@y%&w{q;ML}tv@ieH-!l3*A8<QfF5`!><Drh|gFCz;xqZf3z z2X#FKbX{LJqmQTv=p=esX(<U2RZ&%TR#3kmv^D`Wwg_s$gHp8_^a?j|B~UqHEUFBq zjm%6<7)6@8TU#rN6wMU$%!^Z+gI#5`W(M2Egfb~jYObFV?W|#{z{$dEY3S_b<*lpg z5oM&2mH}Ra1{yyLWCG1CP5>V@z`(-D&Zxl!y+2Kik(osre194w*=j&ng`mv2K`&AR z#|G>oHE9PmP>UApjcX_h!9)D&s`9dsh8K9@f;OWzbZ7`N(TIBQB-%{Au@U&nHBso5 zYuG0By-l^upp%UT&Df^*IpZ8%q4B5zj>l`DJu3ef{uhAGJIXMqGu(#EJgT#TwxqGK zGqa_0f(}{)4Yz{#m$rIyGlEX#;6ho$k4-uFIzLS1pjk+4%CVa6pvnN+g~S5Briht? z5$quFfG>(GKr@nxa<aTUpnE=L)#cP7*98a*^2qSYK;|VugNDN3r3sLpEi;4--B^gy z#BCCj1262)v+;KD)Q<4>W>mG-5|>evP!?t?gw0w0nGbI3O8k8RZsDqE3xZp?|Nk>^ z{#O8<h0Ms$;Kw9}Ue|-hQy3T+7cxmOurkOyfSSbMQ4pp$&~`7#a4{<btDqpL9)~zC zG~x@B#6Lq%#Jaw}pp_X6bq@UC-Ghvb%z`XTpc7vpsTb?2G|-$LwEqlRR|`7T&jHJ- zG)!fY4x;L64A51545|#Os#>5q9tmy491kedg32Gr(i+gzrWhnevxy2aBd_RV+=afJ zPuWsQTAhnSPFPmvBWyw6Gt||5CWzj|pWUbn`WWH$k1Ru~1CKN#8-tV(J2N92Xbu<D zZ<1zZWMlyq^Gu+1=a4a5a90~NbM7DsS|7m1$iSA4qzrr~Hh5Klgc#CFdgvL$keO{Z z@Rf1U<>#OZ7*R(l`#a?1L_|ji#>4jBLF=gi&m~Jd>ku^+BLl3S0=peF<|NJo8d8J1 z9dS-Ar~--yIToBpz=zd>-40&cfTWCt1$;`aC?f-?!5{>7J_j3UsSv1)1q~K}s|e7s z!=j)aZ;<u#;60+RVKr8KV03guPL6}Wau(BHc}XX6sY>@;nL3c`0~A9c6&Gkf{x{~C zOg|W;7}OcG7>yw9cOG#ePG%N%4e<31(%u_DIUO#>$^vTD^9wO@u#0mtv2bWGf>JDO zDnbD~b_|+MWQ+&x-Rt${Vq;=qVMt&FP0WGnVn`)zilmMKv{D?T1w6Lg3emwD$H2<k z3}G`QfQl}NW{_J<!L>O^2ee)VF$rsSFaYagU}uMFW8mO`>H^)a0kx2kjg285Y9FXL zk_BD5qNS;<D6KA|4vG#?Y_PL3NHI!5PXjfDE?QSp1I+*^iZUvSDuSj<!JTbz6IE11 zjFnLlzJ8sJi6w%u;olQR<$p%VH^VYoGRpsZ2s=%|nE%=Xr+;}aC~G-c7-xd(Z1evD z%$u2hFc>k|Gd4l)UJ%z(WCG81bBZu>f%@5?{x%~k=&UKwWFlCao1c*hUD^Rd27Eax zs~0FvL03N*GH^05ad9%GGqAI_cylu{Gqc5m&ZX$}=4IsKf=uW^=gfH+85lrw=8$w` zjiQl(n;WK&fhZ$Ds~>DJjbLD716Ro$u?!p>t)MB<X0SuR>l4AFiVX2EcQ_#QgKS6F z%FYgMu;bJZihpGjV@3vROJjQzdmU}iMp=1TNpaAsOz`x+5u*`kTOqdTeNYkrb@S0u z2lU)G)D_aJP-g!hf*1JOBkmB6U~B_5@*OMwsly6alw|=PkiAFXONFmJfHd^ob8S%y zS<s%%mCXN`elTb<m@!x~I5TEKVo6v=n2C*(jgghd9JIOw+(UzAdQ2(Mu{2^bj9i?; zjBH#`JsgnpYCy>al+ZL8*cf<N*?2(xp%!m0Movzqcos&`z3<$-9H2u^;@CllPa`(O z+GEiGJ{|$27aT#Y-UzJ>49!p;V*+&808&EPLsz#$EC=ZYkFWKDoj{B+4wfh;Fn}Bm zb_W9&7fe4RD=R}Bs00V484eC`K>|9J0<`8^Tg%D8#>&#d$Uw_X+YHo)6<1La;p3G7 z9baZ<4j=bHOAw66-CEFrW$^VkpfUsA&1Jd*>gV1Al@}-}qv78ZM4$JcH8g!diV{Y4 z<UTNJDslRk2ki}~!q?WbFwXo}0O=buFfj-)Ffg5Bn#`cWV8L)0a$p^|5IYkivoIqY z_-a%Tj}^*;&J)QqFflVVv$HZWGD0@TKs&?WJ#LUF2c1i&#lXY@RR$h*?)8Rf0o^-} zuERkctQawaz|P3X2rak41EVmrBO@K8)m0f8OiffR)Ga^*?4appHqd#)%An4+F=%i@ zP#HXWs0eA@fU8k(M+<VAg0ea!yrBm!n1gO2`U$@NNZEvuM_5Z7ag>6MiLr-TNz$8l z&Nh&97C70L>x1s0(gO`Uvaq3@py21`p`ZerWe`%}`*+1q9~8#_1(>9m{xaw?m@zDM zkW*u1XH#cnVV7iNW;JBwVCH9J;^1IpWMXGzU}X1#Gz*OxSlC(FSy<~B*xAAT7tjbY zGc)w&EiOh54sg1MmV^wD0@=Y3u8WO<orR4(os*G(o=68_eLZ6%Ju`hXRTV*D5mi-X zZctiAT`^(`ov??7t1;w)4|uzg*~rXX5EP1xD^zS$#MHPr<oM*}`3=RPfjS3Cbb*it z-yD5U#AJpK8wV&r!54ekAzF`rU$U_<_JA&-0u84zf$r8~Hes5~V8-Ccu+%|R4wOXY z895l$8M!$eOhlPkxj_>ikfI5cIvF?^xLG;4(?ORDGcw16uFz`r=H+4njg4{gK&QeT zOc<Ehn3_@4f<wF48=?=ARME6WIw)J1GcwrOm^)fH8X0J7Dl5uJfri<jDbbA43{+h} z<{cm@5j+tB?&pJ=-{6&a;8Y1}+<_CRC^W#?z(*T`+Y{_!utYg~wvMc!uZoyskq2~a zU>*yzN~k$_`;lF;qE;fKkEW@TmZqzs1{XXq!8dqm%9sTt*I0o^4o-`z3QDPhHzTQQ zDj0ZzwjU`;*nq+pJSWc#x*OP(ksVTFNGmdOuq#P1adJp9GI44%GJ{$h%%B_9*}SA3 z<iN6wTpUu2oLuM%*kM=RYBF$efG;#}@dmF)<BSKD*pOvh(8dJlL~=-D0@MXCLDRq$ z&%nji3)LLUz{!bKuY(Z-I|Bz7J4ZU$7NRsoMmoqcG8pJ-X{xEn%Yx1>2kpWDSNEoj zrl9^OQgshqMg`uE3(5n~#vnKf*+fOb6WXATs-OrPQ>}y&FQ1H%jCy&bOGHaVTwHXp zyqU1LfR>n!_Gvy=sR)y#BsciBjnxgwasOU2UiqiPctulAJ|mi&-IbNg+FsdHOEWh8 zUyyews1N%88&f9J4+b5E^^myXmu6&Tk>Oxs1~uZCK~wt3B8<%NJBLAw%fQR7p#?B_ z2OlG7tr?`D4QhQsTJwmKmjS%M47MW(q|O1f2n}WebPW;sEN%EKL}a9cFn9@&I5#I~ z<qv4l9dxK!T~rygbQYz^1@%9nfhQscJ>!P4*g8QwA|fR{%uUS71ahQJh#K@r8*n+? zz$GfG!1Uv9m8mwoki|H}#xDSro|(XN>LyG-Kv|n1&Vh@Sk%bX-6BDSv2J$rMwq<bt z47MPOiHSK5v<MO0*OUg$!Zb5LN7P$ED-q$c;MMiqTnwP`dqF|aDT1KG?G-^C0#!!E zP*|J464c6P`T=g-TSMFPpm~SCOs5!l7$h0|9K5+Xn3)-vBt!+6S=qs3=1lBN4D5B> zoE*&TtW4~zHC!yr%uGz6yX{z6n^}E$c^MdZC3z*q#Xw1wpO1lufk#MCNRWqDLYt9U zS)E-OG>c#?ZZ3?>Vf>ry9T@DH<mMFO7U1TJ%wan9Z^Q53i~)cD{`>c@4Z#MDU4$|) zF#cpZ#lX(M&tTx7%go5g#KOeLQpdo;z{bkLmI@jx1}#fy@&+B@)y(PxK7vFLdd-+3 zsJjhb?4@j~EUFyJxFq6B1ejnt^>X_3A1|j*XJ7=6!-MW}Q(}PL<;JbR$jTru2)cdB zOWFa%V}kOaEe{#++y)aU0fQG`fD>gm=va5~G8Ir#ZUrrFgCtB$CE#Q#D=j9%#|vH> zqQs~KN~Yi?8=~NeIzeMWWm6M#WpIrLnPg&PH#IXeP_|aCh;ojIU}ckxGD%93li`!& z;8J5!lCm+p)tDN`DEjBed&oA4`^vIx%&u%~py&qsv5`rML7!n3B<~9AGO{t~$T2ap z2{SUYihzzn^OAM|i87&yLjA1B0P3uQ(gx_t2S!F%7l;{hZy<PZNR<J!*cYr2)NTcP z98(>5phQbUSrN2Z5$u0`Mt$gD189N`lt)2Je!&%$C^!T_rzqeK45afDa0Ux}DV40J zqX$|5f$j`olxK`(dd}$106Gs^UUQaO8e}$(Q67AK0%(le9IWOQqdN=etYWNc{@-Ef zXDDO3$>`4D%fP@0SHrXe%^YT?bVhej>3~qfh^&T-QIg>*QzD}~iyN{zsA?Gc8Rfxh zJaDQ3xj7N6#t&HyihI}@6&Qbj)G$OLt3k1gk)fZ_2dpLrry3r{s|<@l=NK_T<~Mm5 zuQEo1`CQ;R7KA)2oWBGt&jCuK4025N4BgB^3_J{48`)A^9OOYG%}fl;=?qMaj7;Is zm3T~y{=$MH0xY}|+8fyzTp)vM%BG;9eq}jB8yiCj1q)^&3nL?QBUvf%nber(GBPkR zGcu(!Ff%YPhl494&^el@=0b*gMM0YrjJu)cGTB=g8Ct;1<zT$P$jkJPL4?5q>|R|i zMg~S<Q1Q;d<i*U$#K6?T02&4l2M@)8%5u<sNFod(s;Z!YTCnR)%^*E6&;l(`)h)ut zE*em&swQqKA|_^|=IUYP#CRb@+CxfRkk5oq*u_Z4Ma1O)e+Ci83yhLX{}|j^dKeX9 zdu;#zXOLu)VPs(zV&rF#XJBA_qyU{$W_$!n#VGQMnDPva48lxGjGWB83~UVQU_Z&Q zGBPqSdVw6@44yhJVPIfjV_<`fHG{oiZY-{>&MurQldB#Q!ld;2^`1SDb)~#aN{r0R zyo~%T6Bxx<^<neVtol&5XfTB^R51NuVB_u$0Oc<YrZ|R=O#hhJ7|Ouu1Hp&fm7ww8 zgrVa94MukEUWR}_4<PyT59E3iu>SwKjDi2RFtaoIFfjb(hsgis2iXQO@4pGd$NyIt z*tw@M^nj}8|Nj|4W<tcKL&O-E7(lly{9=*-9V^YC0}chyQW{7oFoN<d1NfFP21Z|I zW@c7qR`3mD;Gj@9W;PdJ7`bUvWF%v6_um)Y-JpQ;VC-dpm?s1>Z94-4Lx~_GuP7w_ zsv9qi+{D=14GA+1hQka~8K*I_vve_V{cC~v;a>~F4{-TztnzXU_ZU7f)-kek&t+(5 zTnEw5xDKu#AwLg?{3(VHjFTAIx#wfbGcYp9G4eAqF?usFGbn)k;vfo6;h~@u9t-lT zub==EuNc^8jLN3Qj7(3SF!F=i_Mot4vS#?g%+A2hy@#O>8vYCn6BxZf<qC@da=C(1 zKAAJhGZcW!W`ATgDCLtPqa>pgxJ(VhsfLG99$JP)<5VNaD9^~t1S;c`u&ZHU1mCa| z!nA>b6_z@X@;XvTFfcH{CuKmXLlJcDqadTu5k@wqJg2{)1|q^Nb_N4*c&aimF)}lO zTH6fFjNtp*nPXWPnVFb<K}Ux(unP);w*<h<Gd48_nRvvB@ubsIu#rgaBiBqpn3--) zj2d7=DRU=yRuXFFawo>)U?Y*-$;zMx_PaD>$Q)E;f`bu!0H~j!pa3(kI5<TqDuT}5 zV+{HC>xh#RQywV2`7tbG3}F&rK$yY6$k5Ed$i&15GJ`1&WG1p1!r*4IpVN_lznBF6 z-EacC17Zd{qTXR-W&p(jsFp|t)k3ih%nZ!Ff`WpGdIx--m#DIz(~%=iOafD!ra;RW zOUA1IH<>}VPB8qr4at9hZo~7R22(iWab{^ob|w}EEoM`YJkt(lQ@A`zoyow+pz&XT zvF!glM0pHKYmA_AQznLH(Ci<yJQf1a{ehB&xH|ac-R=GU5s`j<AMUla-GiI+24N1t za$Ou!u50uef=v2w=S~Z#HUqgwnL!*>1#D(!U^LKXRW~&kWfx~u{{4IL;t$z|*{*i> zQ2Su&SU~E;P0iI!#YHt1FaG_()z02E+Yl07jFJo=8Fw<XGdMFa{M!u*55^t;cEiI% zgHe*PpK&LnJ7W;z_J6y<>oUM=f-%)Fvoi+4%wR$><G(ZG^#4bg*%<>F82&tk*zxBn z+zt<hBMe8FelW8$$TBc6Tw>tZ$iTGW5=fenL4$EV!#SpvNa>4-k*S3NRNjG3Wdn8F z7#Mw#(x@<aj!a`&p()c!Q_w~nWHWTZ>0Akva9fxeL9LolP#l6xVPXX3aFhZAbWp6a z={eIvrj`G`T{Q)_ku(?>7|t<rA<7<*S;*-fWGs^(df5X?^v0qZriF}LAcGhg5atk> z_l!YTR2rXKR%mK^6&$|?jL-gGWngFU1o;WFp%`@8JQEXR9s?5t15+rdYZeFU!}&>r zw`N0bYCw^Q+=iT`SV5~66-|v9pZ#;PVDz=TurxUt<UNi55W83z+z@t&f%7;Pt3;uj zJW-txxfLmnAQnNaX}hpA8SEbc#&-<f44e#xAUhn?K&u@Y7`<4SnHW4c8JHN|G2&bl zR?$JSfTE}(I2W`q`v2S5@}bqR)!N0zmEr&Yf89)e{~ef3nb?@XTg(_ln6wy9GM!@J zWRP+Y2d(P@H7!8rbTTk7STOjA2s3erX$zZ~nH!6;v9T+Qh-vU<i90G=F_|d^C~>KU zN;5Dri2PS!IQjnw11E#tHdfHNV2sdxZ`=%w42+-);cY>u@NmL-C6QYo=8A)iGzA%c zQcRsMOUy~x$^&NX|Njg>{;M#y{6ECR#`YGRKcVL^F|o0zfcfVCzk${sF{v?t`X!({ zDlY#kVBp-y$hhNQ0VvD-|IYx^qXyCgzQ-c`Up`b#K2(kT|9XZ)OlnMQ44|`YnZb8} zNHBr!P}X8FWAJhCRAOXhF#(;x&*a6x%D~LP$_%=BfSrwjJ)MIQdJusVc<~!}Uc{2o zM@L&x!N@?{Ovg-3ML|nZOI1xxm5W0{TbPZF9Xz-RT5<|n?hYIKgbsF^E3tz&51E>% zffgP>2R+4>^6)T9M%db<+PV}tMrP`}R{DiTng(iV`&9<$iMc5n=;?U*`1v?m1WT}c zGU+JCnknfR$2*1Po5rjtuHO{zZlz}<BeU2~NK8>&QuS|Fbbzy$hEjDgxSjX^1p@=q z9;QzW>P!v{t>7Dog8#k#e-;u-|IdO#2^`*_30QOH9ws#=2aqa;tp7hCcUU0ZwSlsK zi-Ga~@Bac!4NOW5rVKYBeSI-mMrIZxH5maGHfAmsCeYdj(87Fa2ap6SrUbOdufo8_ z%*4#bRL217<AaXha|R7TS%NoYC4f5kpfu*a!7w1gK?AOqmDL(F5CR?&!KU6p3$7H@ z)M2b)K-0m<$N)1u5<FC3pr@g(C@&=ez989@(UgNjL>oRTYHA8uWX!0HcJGy_2pc=2 zF&`6XJs4<3w+I^(x3D}Hhm?SUix&sizjskWieje9d_wFJylRqup&VR{q7lN1Vul(# z5*(HvI9NQH*-X+|3ku|2<o_*FO)a+%ab;z8Wo0iYlXq5Nj01^8II=Qta&!Tm&B?&P zw2$cugAjupL&!F64kpM|d!V2Z2dzDaoPOc}3K>vo&u9tSOf15{3_52&5lI1)Po#q| z=+HVT31K-AIWA7nW<WL$5pC#ZK=7%};Kdh6djU<<80$0oJngHaqGGFT-1>7%^YThc zb8?uj__T)yHUw}qHgW_u1ctTw`d20;RaYk^Rf5W8&^a^eOiB#=4AS5o+n~7&&<bzR z@=MSr24}cyZ5e$81sEB`MFpe<r8zkm_!;?;+{7-b42cBPZIm7b_POy9QAMpqL0+Kc z-9Ai8_LY$p-FzQD@b*-+_IPE)$LHk4$7g`bcv}VrrbwnM4B`x74t$_>Eug(rpi8$v z=a7Ix5wY#X0h9>Ab8?oT<L!h&TNs#_6G2sSJjmG&j6UF9F9Q7FVRUguaaKt9K@PnE z&A=HO!<=l!*lC}tqo@{WUS3{ZEg#j_<2{AxikOI>phR&(V&Q{n+|#Q=n?1pGulavt z##2m644R<O0_{1LVqszgPep+CfUqz!Fft^=jsbIK_F-g@lMxr?U}Ml^)MQ0YvY;+M zbkYwo1p{)H802m#MomQrc{yVlQ*BXG1M^5zrxG6xUl|!GJH>Dx8E-dV+wik&94_pf zs`3)jf}C8YsllmDc7i;fT>N>NHqL7N0v?9W-k|%-7#J9zF`Z)IW@v+~&r@JvW?^Mv zE?{6{U||NGx5dZ^-qU5_&Bn;eYRL+^$qN+LiV$_Ib+{FRE)W2j#aai_+JvEy5p*vr z9|HpeHv_jI2(WX2j(|`E?YjjH$_gejw*I@yC>*wz>6HK9^9}zlf;xnZ3}OET7*~VO z?a*=1WB^_4z`{@mD*joRSyMqHK(P#rjMkuf)RqA<)+@lqA*C&@EC{~09c1*n2*%LA zx58RJFmrfbx;YVS?*HHazcJosy27B%;O5{g%*ezn#K^*=!N|&}30c3+!T>ra6=VZ= zKcNF+^wyHW2Xu{;oUFJgKOYA>gEpf!J0vzi=M|VMnuFGI!S>iehj7>x*<l+Vz$<F! zTex#@ss1}<#mXrq!Xd#aE~G4M=g7e&!)WHr$|)r#AjQEY&t+pNXrxeolwVCqLY$4& zgM;19T+m1%;R!^5*^>n{Ovb>-VDMjn@ek8q1|0@l2P+{)MkY-;Ss6i24t5S^MlF;b zNlu^~3@LhSLEcbT6%!HQ1I>y+6C}tB!mOskAU`N8Aq_&YtBNus%1$;m#`)e(0=%jJ z=A`fmYKuuq@M_4|*a{1~Gsczh@Uki>3rex`DtvIYkhM@_l=3lWWLCBp*X9PzA?e9^ z8t(G6`nO9-Q$|2m4qSvn;^Z3x0~0UPRR$Rbe+MC9Mg~S9(B+~W>};U*Tc8;dP+9~X zzX-139lSwJWlPZAN`jzH5U3T-0FeRh4dZ5DV31*uQPpGN5Ytu!`A3~y-CPY!Kv&v9 zBZsk2UXFR6`Yk@Gw;K0MJWRu+wA2&b7cQ5P_v!UeQ7mw7aMtd2X;P6BbCXqgan5yu z7X#z}t>8Nd{xV2|)?W&P_8)U{u(NS8c!1V9+k#J$k!Fx)0woNP-4J_Wq`Dk4xHeK! z(`IFPE%u39R_LktC7ap)GS$bry|h$6_-#v-F_ZFj@>g+|^LBCdNe~VaFI;(}E@_Vk zxIX^|uFtg@)<WuYL2gC{MlA&qE*546MMf6z%o|9I2`<Kpn0u1|m0j_m&F0|poF##c zm5GJLk_DF8L6a5W3<VBLXGl>1N;fPC3=nk=%503RkX`HyOiajXK@9__sz~sJ6oWQ{ zwyK(#sv0=E6R45FBm1Bf$mofuD&~_D@eTsj*Psd)dld|+ltI-rsJR3_6Iu;?R;Mv2 zw{tTxvVpI{1C@E{kj1K?#)~uPfLySrEkWM3g`Cc*ud8aTW(=B&1@E0uVN~IS)E=PL zBWQ!Dy0RQ-;XH`Tt_&{9L86eO{Mbc6ML1~w!bFYH&Lz{jAy?g0TY{Bc$wtrFRg#s} zmxWth(?8XqsnI8fiIGvm%f#Q+l}X8=CaR;4S6a-?ONviHnopFEhntI=)oEeh+QXim za|I-Mm8COM)6+on6wvlJ8>s#L|3CPgzpYFUnb<({4Y0denXWRiF(rZ9kD&JYS<o2} zjBmhv+5g`dw}bi5z<khMnOdMRc!mXFKIl9f1EwELY@p4zQ1c?0t}wANsDaWELm~qM zvljUNgn;b=Jj~3@44{q&D5HuoFoSF2I#7<V@MZ-a`~uFca0##wLo8Sx)Xw2$U|<ks z5ET;?7G&d)&^8t{7B&_(25nh3GgoF-W(M7?B$!zBoaJ?8BnvakJFnL)%q&c&{xvez z|21P=XO^Y>ub#0<F~|HL=-xL*(D^P*jZ6{@LJSex`M8nYCWY)aYf$^a(i_yCwPo>< zc7RKQI;>#VS;HhF9YAgrVh|D)6au?ZT+mn;=0tX7b<h^h(2RAPlS3F87kSKMWc<L) z@$U;G&)*k}K_bS&@7@ZVi2pqdYHP9l7hnRVNht<31}_J96-IUrAx36)CNCDy8Ey=0 zps_yaVj5>}E=G2Ca5ZEJ?%>$6`N&8!FeoWVtI4P_NHItWih<5@<_6V!;ITFEA}>&a z1RXS{3_3wiR9RFQG$W#>tRyNbCg$($-IxbXj+#C)tV~Rd2M<p0cXjohB;)fT#Emht zvkjaA#l-|1G~GfN^)ph_Q;#i@1Qi|s|1-${|HfnuN(0Op;4}i->j6p=YTz`%4&Unm zQ>O+}$H2x=nGNpiFxdVN1K&H&#$W>04{CQp&NmDJ^LZE;n5Tl%pcR<U%>W7+NhUVX zd=vxtj58({CMob7u#ST!4<iex!wkBrhlL5WBc|1xm6-{Ao;d>pCkF#Nc$5@8BQ9(# ztgNogZf>j&+S$a;b=~(`wBr%`=zdEsRwgz!CaJ&A7A|}>MN)=W5`3l<69WeW15+21 z1Oq>VFoT7IDFZ758wV>}Dgy@tBRdCUDkmcg3%Kdo3ObvT0e0JlFyitH(DB-eil(3& zu%ImxQDaqOQ)Q;Ee^*up#4@rm)?7Y%^zuL7cFBbcnIt@>y02OE6jUEFGZ_E>#%u&W zpU9lSj-k;(M2V4=O<j?Rg;j`~iIIVakqNw;S}7pHL79`01vJ~l!jK9o&>)*fK^LAf zGD0rr1K&ZY!N9`Fz!J*92~x_5M`@&kkgc_)g_((pvZ|7ZimI{_53j5?qX-*2JGA>G z4lb<K!6eFwtHwr<1B=<g{bo^7b2BqmWf9QSEwfQYO_Z&%hOeTua*Cl{pr}-cnPZ$W zV>pw)m!YqkhObetopQ9VXJWLmmVKyqoX5Y7KP+vnoEe(~!daM=98DwyorI+|bv5;D zay-luqQOVC7Mj@l2I^{g`x!8^SX8-&_%kqr&cI_bW0GJHV~}G|2kii4=Yk%^&dCfp zJ07xAj)9XC-1h>X$*!iNtSBcVEy*CpAf_lNs4B?KE2+&W3Mz9zd;UQA$`p2cvO4G( zEm315aVTxZWacI)l3McI+xwYiw5h1Dp}AdWWh=j%OLMD}8>5vgzqXu9<X6{wlGetu z+WbCh%9H2oX{=FaU}UiV|C8|rlRSeIXz!LdBLlMpClezB7b6oRvlpoI0?zi}d2^<C z1|}x(>64&)MIej7LEHBkK{Jw|@CCIjA!~kNy$D9Pz}T<|C0Au77rll?yNWRFU}Ys4 zC&uf!adEl4+#bB#Q|kk3T!jQ(c|pCVFwiLr;QgTjpjHj6bq(3r3tCjs%;2M_qNoI# zYGnik3g~_wc2F1y8XGZ5%<NpSp>9e{N{p}Fc}BsGP9}-A=9IX9W#Bqk1!^`Ag9L-U zgAL@a5q4(KEd(qqkXs1ASqK&(e7uYdqM((|{Jau;5}=Ku?5qqtj69&Vl+b-%kc|i8 z%HUlH#zvrnZ`f+5&g`0LVX?rYt<Ca$3X?=zV@KyxkJs)?Jm#H9iDO`70L=zX2lpp5 z9n_%v%s{Ptu)VF|RzB3HpiYXQ02{BAwxS}eGa+isIQ`##M#mSoZoPoyRZw45@&7l* zy-Z3B8laL~f{}$4v~yDlbVLK_Tn<JCMpiFSeZ$Di1UeNHy#4_kqmb1gkj>IkqQU}v zyxi=p3>u6YptT?3O3))?K=Z+%gMdM_5*ujE2c+G~#x4rJdH7L!h`ffgj<tQTtCh#h zU>oyTCHXk3vV8FVwp=b%H$g!)HSj(*O(_9?K_TP~ZlH38i6QL2Im2Ei3Gi)Bnht89 zBMX_Cn3Gu;85v<4gIQS^7`QoE_*wZu6CZ+XyrSB|g35x>$#(G6__hdb=$0oYiN7zH zB>wipb~%B<$mst!MlSGKBH9ibi1UXb>q9^zJkafuj11hMgK7B~`C$7ZO+jfJyuX^Q zL_~p4MMhFRB8ZEf&7E6bm$CO>MF4m*8WgXfIaNN;`Hs9ioS<Wcz|$fOD2HhAG4KhA zf@jP?BjBL3SdH0*mDSlr!y-lGIToegew4mbn@QrUq%z0G*Z(emQ9aMV$RNtVz&Mjh zf`OMo$3YW*b|hpPoS_vo`h~plikE>`R1`FyZ*0o0ZZ2pnYAz1i=@TZW>=>D#((-DJ zpp_b<>??B{uW6uR1W1}>YGRUL5M@wzPyscfm>6JAX9X{{g6!ymHH~;tnnotjrjdxK zU|9KFrUhjYjI1*~rn53KN&HJ=ob~qwqlbW^z`t_FPC+H%e|tf30m|o~{n3I9at_jb ztV{^&Ks~BvCLb|DG4Q;Wu(6<s2)i*OtPRGf92UaF%*dG#!otY>?@I!c#J@|DR^p5^ z{=Q(;mNk|52ddziKxG<}J(B_hAA=OQ9D#152cOOZ**?bLqoSgw3@t~HhhSO3H3cN8 zGuc}ghlCefSQdxZw)~3>2n`MJ4-E@o<PENIb*%{wu6CVP8=sd48Z-i>V@3w$|34Ye zGs!bZg33}sMkZ$XF;vW;!*LKrDWnGs&Uhk1;6WitMoCav3fhNj3LE@|^b(<&?VO&A zl8TE`U|dAFl9P<0TWUk2Z81AHuLn0Ts4x}gbyc2PA6V@MT6ZACAi`wF#KPFl5X2zE zWDQ%50h(uG+QDQEAIp_w@L+OfyujGb;KrcL#Ez_vaR(DSTpcrmFjEv0Gh;i$E(R4Q zD`a)dJD9BC>P-HBXRZOAiNPqsya~2vkQsKiFB601|DTNSm?XgWYFj#(39^8e&UwLa zC;*@111SPQ=lgau_$bITFsP}3F3e_-V~|sp(&XS3*JcEDUBM*}w4Y-R*?R<uIaA18 zm(VWHWo1o$i;5JxBui5RRn<sCQ!}FsM_XG*M>{(v2@gY8Q6Vpjhy?8*YfDdI5f42N z4?P<jJ&&K3rlvMFrlyvl`jL?rnwC@?6hW)~7+XNia`3=jFOv_bxZq%8VP@cC<O4+@ zsA^D#v^<ne%*+_=YFlDKi6+*vm`Pz;Eogi(xY`wD?*E@m_Kds?{0xc?a^M|vpnJoa zV9Aq#0d$S2lCU!9ygf$9B4{;JV~8JN2{RUyBw@)BloA=3!1c=^aQz~}VC10Bz`_7J zH3@Wr4fKow@T$#bCLho?Qvp665nd54P7XHEp|_w?A5x&QDJzRA3xaAkaGU*oUaD47 zh;ig(D{T)~TPBGRUtK@L(@YZX`*ltApy!K7FfcIg1edSkpmr)~StO$uJ0qk~jODN< z0e%JsVL^Uz0deqwO@iv6s0H243R*)9T5G7xE^KV73~pOU%#<~d%gkC35z#&0#ogM@ z<^`9Ejjm_F-xoJ-Fv@M#(A5PU*u=zu>;`KG3wB2IE9YAwceccV4uNQ9@Dbo=gt`GV z&cn$8ibT+|KSgkTVrCAy3dht~P#xwDM#-#9IRn|5Y26VK^W9w>Z0rI&b!}9*Ufj6x z_l5grLtS0aEGDG9-pwSzz|J7;AOYG42tDY5fq|hJJV*#G+!U2T>n}u=8AT(PGcH@k zc)`O1e4fZpCeRrSLa=lJsscd+;0z25-QZyzAqF93VHI$?5QHQQaODK4rI?uYlf1HW zBOM%_ZJ8wgEw_j=$&F?V|F_)M$-|s664L)sW6)>vVEoM3&Jf6;#^iyVpBZ;BdBF3t z5Q8?;H^z^Q?F{>v-u%DAzyLl03%r7hdB^`daCI90dzrkL`WV|80vI%y9;2D}7_JV~ zVZYBL!QjRa$-rRa1)4wtx0UxXflh!_cTkZNWCM5LL5Dyxw1T?q;Ek*d3=Ap^DuR+y zf^49=huK&d)TOjzG8cxh%*@P%jhU61^_bL^`Iy+5`&)yYxrLZnn7Me_!oov1MVMKb zg}Ge<+hapG__!EZ7`b>k!kHxgJ<^Gg5im9vW7PY1Nhv^9GD440?(YjmJsB%KE-rm5 znSYld;ib;Nz_=c?*Bm@J%?O@P1hrSBK(|^kGBUG>GchrN&ii5lje~*i|72!lW?}?S zQ^kV1_24=hDQinHNC~N^s)KyPC<;HY9d_y!s6h)l34?LPinQ?jGOIXwrC4*DU>{$P zmH+-SNjOd9jB;~J5EAqkkkT<X)ODQV!obL&^ZzH~OeWBcgw_rgf{ctz%F<E->}+hz zj4CK2u;AT;NcE4RJg7C#pvI^MD!w4+Cz*q<;Y1rk2A`N?44SxNWBV4H>*MQWsbipI zFDs_xrx6zI>KbU3uNfgP8^rGHX6Yhq%B7+rqa@7h&M)L)W$G+u$)Ty_EXC)~E6l*i zAn^Yi<4Y#ct;yOB8q%VCY%B~cBA_dvnY<WSSbDudO)<zNOWlk<8mg+QO02w++N$7s z5!%TBb)P|l3y{hZR0DvoItEt@Y>XD__JTq(Jlv{6qDD4qX2D8Asyu8;d=fSqCXU(x zW@i4{N@jCSHQ89)*w_UP4cv6t+1!{oG)>L(%=LYAbbR#HeL#5;)TWz-w8xz(7Suig zM<%$-hFDI;C<@vG!8q%6B$I^aKSL(aKm{WxZ8N?Am&J+>avacgiwv#a;B|`-6Tq_( z;KmwgXn<KwO`VOMnd?~a(by_>9#&5NDo|UNF+@aDOi%saI!IfM@h_}wVSw)i16{C% zR<;N#3o0`H4gVg=xSa9AKLe1P{{M&74@_*J7BvGSgDC?8QyBPMC~XG~HbxfE1!pV_ z=^UWT!<do0D$XD-C@3T%C@2Ct8W~gvvnh)zn}T+gnHr0VfHwIXnVK?%X{AJjhbL=A zc8B*cNo+QbwwyQ5GTLa%rbb5Ne`o8rfQkSHzW+ZM--6G_2?U*!&CJNd2s$0ddxKm+ zgac?^4Z7hFybu=ZGtjUYBO^=#v=0(Cv(?Sy6S-YXMNLouluAIQDjyR&Xj}lApUmx; z%oQ2mMowqv=jRZzv5=MLli?DT=4BHU=3=yBykJ(6r55ha<H63M<0Mx&0kl*Fbe0jL zJmY+(vyAQxpd(_iuABb<pHZGkjOjF^J3|NZ+B}rCg7%E^jLl#*s>o`X;cKfIm>GB( z<r(KOon`>tVXMZF<Gn!@G@`=B#KgeO$;HaT%)!8%3fj00nrd1Fx(^l{4C_E+g^CP} z4D1Yy?5Sv~IXGZyA|0gU<QN#_)Z|nZ<z=PCMHxT~eg#Do1$p=-L6^sZMioGp10aSP zV8;t7i;6<pQOdkVtzl8>fsR5VVcv_aLv3`-tbJnb42&EYU+8Yrw>FZ}<x!Whwm+$> zqNFUpb+(R*lIH*a3?RR62KyaUf?^Fruo@?@8V%&|L0L<w&M42Q1yaLYfKv@<UDisb zi;V6JS~%5!+_MN|4udvMHByZ742QsKba1Kpf0kh_Lm60&E-p1-e@z0b(Zj381*}FN zpBg4Uuo?q=Y8cmp)fi$|!@vl3&w8fQ41x@{TlqmpE_gwb86Rl$dllrI=XIdDEU*Am zEcoE(b)Zpk9tH;RZGmEfto-8I!h*)2_BVK395hv;%xrJY!pQvH#f+JW=_jK+<C>4M zZhVYM|3cm?+Kc|11L}`};^qp|B}RA9Sv-jS1t|GMjZu=Z599~t&&Ym2Q3Hy<43HWI zl>H7EYP!K{OmUjS#wgF21XcrDjt@_lC~3}$QIc^1Sd9g;IVfs480DElz-p{;s*z)q zWVB#PV{~V5K~{s}W>ZEfMt{&+VHO3PYE&2{86&`IK<nh;ZbmUjkx`OK1gz#AvN?#d z1~gy8D9M-uUc2mytOms#h+Uw)C!j@U*zE$T@deohTAhv~PC;v+y+LYNG;!J`!zjt9 z0#<`N--<FyGAe@Ad_gt`B`#DL`XS-N02+?L?q&;yWenBewcTHl%|Y=yXn#s5IDAm{ zr=Zwn&M42M4RQ}d0J1r#Y8d(%<3MT{d2p%$g<%><4Fl-pUhHlL`5h9cDD@DEU22R6 z7|WPa8QnpL3L@eHO^q~T8RG#)cUESc?lEWRXUqY+Cm2}`O89UwN;2GGTE*zj5RR+{ z#V%t;NyaEpI5R}xR3ph~%~-?a#puq;iBpX!qa>3C$Q%|MoN5>t<ss|!L467we&=G8 zWV8c^k04HSKz^?Ut+QvWLso<0cV>ouMqiM780(SMp!i*xQIatYlztc+kkz1=14^q~ zK;g_d374AxKN<QNlbDhi-5Dn%t3k2L`2PZiGR7!IHwGVu1+X1rOgsKB0QI;)dHw$a zMrMW=jBX5}2zf??yz&28j8P1a7~L2mu*x(1U%==EmXE|LZ~k9^v6eBL(T%YVA&+D~ z^M3(GUyy#rdW1ZR{Q`_>Aonr$BIJ?utNa&W+ya)LgiZec&;J69Neqt}-54h$<YDGB zFoDkIWX@+&Vo+z$W2kiCS778|;RAIuIKZ<TkfjSsT#TRu$jOk(0BSR_gAVKgZKg(< zw^D;EVq*kfQwQC?-RsTDh@>#mK}bVGLr+6bO<7o3SVdJyfSXrVTNQEzAhhEGnXy9Z zu^Wp**Mh2pPdQ{~&i^+Nc157MQ?jynq<dCwgrlRgO@x%2flG>M4EWYS_TSLE0`*KZ z&5*nJ7P6`~xt@Y9p@AL@3{3y;{|DUx`-4G`!HVIcgP^1sXyq&y6El+r8xuDdXfYe; zYyf=*HV#H+PBv!fjWD3ADnKV}HF+~|adE~oaB}u~GjMY=#DiK3-3*|m8lc_6#>8j? zZ<_|`g^XZGJ0R)8Y7Z+bBdDd@46zZSD>BkS$kN=%Kv!Ey2n3ZyU^9ZCUZ66&GRmDq zrpBV6;SlJRMBw%cIFQ**Km$sU8AK+7D(F41tXwRjqHM;jMw;B*5q#VtqU=VjhFU!Q zu=`@8CqgcYW#sAV0o@E+!Ndf*#)#Q6gB?V(azgHn?L3{7&XSy&1sV$m-)+Iv&IAf~ zYlasNg0fOfY%CglOw3H$oJ?HYuy8j72OuN1aOdU*F9m^|GsF-NKbrw|gcK;k@M~n? z<OChm!o>wS5(pG+4yFhTk-{Es7XvFRC+Ii<G<}hg4$21lj11<c`ql>4TAJ#r%1R2L z$x3li5g`F~Rt7yrJ<!n(;A0%YlYih-)F25Oa!nL!ln}V=lbweR8Y8^?5gf1>;pPG# z4P+SV>J;hj?v6MrDwvrC91+&pV4C^wE|g=VKz+{tKN%Ppo0<MH7%<FqkQ8TRWswkI zVq@iJWM)(5Vd7xYXJlezV1*tsqQ?n3qlXc6X%+_;BO^BlBPd(2#51t4w0N_Fj)00~ z09_=;!^q9u%<UuXpu@nzz{<wLnvSUvQ+cF=7y|=?0fT{_j+Umfps=bcC`I$~%4!=6 zg2!B-0j6wf4jKzJ5`!%h1<x}=20<WqZHeY`azMk0lN;qgh4yxKUUq0g7T{%MWd?;0 zb0^A43NAY!!Nzu#fr0t|M+OGwT}(>gvsfG$W;%!pax<}T+3HDhu<){IswptCFmS+9 zttK~!$H2mo&d<op#>l|R%aG2)$i>AN4<6ZQ_2%PbXJ!R$&6jr2#iEu0G_MEJ09hK) zi%=iwAZKpQz+moR?qF?cYGP!-pv#~uEG#G@rlKMuA^-{_Q_y|^Gc$A8jp?9ki9mBC zqM$7VY~ba{N^0uh<+SRe!ls}c&n_m$3TA+oo`gyZGaIuT=px-(WEveMA;x0FYM>u( z+u#`P&|v#F&&`qDs-AI1uAj4Q!w+_8Aw6r}d!EqioP61&MD=ZW&K>3Dxb=*am+>#7 zu1>wLJ0q{QUwu><J3Ai(6DT~HDw&iRlo+%bS{wxB85!9$)nr74__$d(Kvz3LY9D0= zMmEp|>2(Y&Ol&MnHK3El7~)yknLxKafd-qJL0vL+G*t|stB)W`LF>jq17_fR9U>hh zRg@VSl(khr7iLR{2=nrAurnwzDnU=^6h#UnJtj~Xp@gNl5*xdin7FA4=n5nWAr>PR zL+x<AggDIzX%Qx4R#PqTJxFSP?re^|uAu9X0$63m%&j?39cK|0WBvVyO-9VZiu0zY zmZq1lC%Y<#XCnjC|2zK$m<*Vd81xuy80I(#$nZ09aVzpLv4PhAF~Aa(KKM{9aE4>! zU}WTEV@zk@=7yc2#LLLV1)j9-_U2*a<ZS2kk#;b~q62(83O6@6pF)aFR9%q{QU>}A zphC>Xz=lDOK~G3XLq!FY`S^HcwZ%=5G9Tn>5m2%MpXaI$T7`jBrKqWk3LAl!Rf6W| zL`B3xc{!mOk&oMhheg8AF}bob#V$~sjggfbR(`SZu+21ZkazbAa$$7*P{|6)lPpXg zp2`MY-JN}U3W6rW-fSR2W=2m&ebYFbn7o)c!DP^!CgcCR3=H7=OLQ3KLz0e&ga8v0 zs{$h%lQyXAV)hbeWcHAD5CP5oqe`*Bj-64#5@@WfuwVuqLDLKhTMaDAz+nr&#saD| z(m{xUfkB5sM@dLkRYgQdND0*5F-8hVF>zQng_UA#qDbMWE*i=M3ph3&Ht;1U7KrOy zcwiyN$Ndx>T1-rDk`UL?D1rlvWh=OR2Zayl?h`ErZwC)iMiyo<MkW?nUM5y1O;GT# zc(K9DbXImoMh;d+&{_!Slq@GB2S+mp=mt0~1}%_hRF#C3xp-x?jZIPNFvu7)ygWuJ zi-TAY%?K_H4+9a{B`U}z@iS=C!OziC7J9Er4YU;A&H(nuum9f|A2TU2STLkJq$n`5 zvM4gLvq~_sv74ZrG{nHp%ErphR>#1`#LC80!vMNmlevb0nUR$lynzLp3BY?0K!^K+ z=b8--^mJ8~<zzsWHU}Gn1)~M1U_hC3R)e-em7#aMqnt<sx<A&)4AiP(2QBWqV-YA1 zzG_6=(uCJVR$h|Lm_<!RG(QS{PMD3Ku%VE&8mAGff~;T(mykdJ?79(Fepy)~2QE1s z7cb~pVM?0P?1G#^+Pe0<GHRgHY8d~&{{M|Jm}xSD2`DT?8Clq589A7>K+6RU8M&E^ z7#X-hcdM|mHgkfGl4oLOOlRQWVr1gxU`huyUconcwSrrb37|6(!C|1Us}5ReB@enC zKtfzZn4gb_o0FZvgwceTS410pHWhN5i;0`UPpT3{if=WfTUtQJQL(TyLkj>FP8J_u z&}9r9d>qgkm_vX=Uln{+1Lzd0EJj99CTC>q0G*ZQvJG?-rnM^zvxT8P`1S^HI$!|p z$y^Vf7YlUoV`SvuR0J)P5anlLf}V}S&dA8X$^^PggMp2aiJ5^Zoq>acGZvIJ!3(BX z;y`z}vw#w{3g}uxA!R{fB_S114h7FSfu?f6%?LGhc12M|Q*%V|Cnj#LXv(;vg-e(V z>H`r?kpf2Le-+&lqEK&&X^Z9k`^Y5Ww1)}YxZvTK?_za~1LQM4&bcn&b(5i>z6ba| z057I=*m?ja=z0K9|CCXlF&*5WzJ|Sz%fQ5-^&fOb9B6HdB7>8IJ?Ih_AwkeKXwbbz z?2Ig|j4YrVL78JACz*lI!GRq6CN9RnAS*4VD6R-v)GI8i0h(cE6lI6(X@RXBPzTMl z34^DZA!FT)KVCKX+qM_G=R1^TBpv<t;2LbVkc7ibU;TXl(gv$szw8P&XZbp>f77D; z{bOVO{XqwnFbMqL%G|{CgTb89A98n{q$nd3qXat>1Czd%rjQ^j7lWArBQrY}i<h*6 zq$ndBb~)&7H$4VM7A6)(raE3m9(JA<&^%&`HwQClD{DL(Bluh!E-v<1ZcZk4_GWfa zmrOb!!odhh7dKX2oQ&-3&7giAcRT|(cQ-dkC#W-L%fQIO#=^)3vJX`!^yoK;-LVWH zt1%66K+_LajYTKOAMCLh`a!3Sf@aOk8O%+LbhS0qm4uarL<I$fMZuLbXoe4T!k9UD zm%E^dI3s9blQA>6TL-?k3e>X`R2F0gcl;oS^r*9|L<tB%aw8ifOT=Q|vr?{VNlD?L z1B-48h=Q}Egov7;t#_fYl_n$jtUZ^1rzC_KJs54zo%{D!Oe{A2AL!D@)VP1I8KeHa zW(Q|cHg?e9Ab4g-hDm}!7_{C;NRW@0g_(teorReNbpI~sN@~z#8Z$#0_^59N=z?I- z9kjxN!oq^=koBFS;7fkN%RALgjYZi-8O6N=egt@f2u6GN&7kXkH@i<3GBFXF%)rRN z3b_l9K>}naCnIF3CX*Lv+yZn49SZ|!tp#}A4z^yCfq_ASK|)YaL{(XkgI5Z)rWDcx zhK@+6nSw1h7G;zwRMimE=jAg4-8km!8_sC&9%W#_&f?6@DeLJN?SAtnsBB_p;QBAX zl*XjQAj6={pv5rLflHE+k%NbkiwU$x1~i<Y#mUCR$j!>c#Kp*z$_u_TksWllV6QhH zBNrFA(gj~%2R@@o4_Pq-=r&V`4p5PeMMI>6x~httEF*)4x{8*nmZE~JvYfI6^x9c2 z4)FDgGK@0(yx=X0;>MzoBdZ`yIM9*{V@Q(()Y?^41MRRiR}>Rt42de`wlHI@NPslj zKsVBW+Hr<nu566T9By_l?kxXqFgr6T6&ATQG=m#!cNkx~-Skv8@bd9>5Bis$5fqgG z3Lns2J3pDc!0YMcKq~}5hwpK+Gcj^7F{Xl!QUjeP54%RPo54q1jFCZFN=!~%4s^IO zWF-pttT<6dQRsPluowdEM+a@^7hz)qpRWwMql8f-E7#pUl94gO-CaM)-P!ive+Ty5 zXeYPx=g*&abFqjrarZEHaj|uRx$PTMFt}dPX9#u(5Eo!#VrF6F<Y4k*XXIe?1Rd|l z#mK?Q$dL*vW0+$h4rbuwWQYeHNeHe(^|UqB73Ae)rKKb#Ks6{Y4<`qM3Zn|twQ8m& zpcG|h2EAy9T~Q5s#SSPoL_|f{R3Yn_OiUF;z*p|*nsFPbHoZ}?<zrOivsHf6<RxaN z)Bf)hXqV={W1#zYSeV^(-DNyo1-&gy1NdD%twY_InakV$fbQdAVPb~(E%d(?lL6BY z22}<_h8xgUvLXu;JDUt26EnLQBNMYIXkC(*w1Wg#n2(VKNmPcBh0zmQ_JR(TXJleb z2CZcWoiGYIX^a8XCu3k|hqRZwnPKIo257fCR<+=3ctIK%n3+N99bihqC%1yuxb=F2 zY+wN`2X6)~;D=-l(B<R~V$cKY4b=@n2iA)yt7@o&uB{eE3}=B@kn4XSQ{ASfprwrJ zpwTc;jRwi(rY37)16is{mNw8G$BJt90pjdJ_U|D>TTDN8LI$zAymOU{99<xrjor9F z!(BiAJ%9{srJQB}jcZvk*)jcKP+>4+cmWAxAx=hS4n;-=W=TfST{<kRpy~}O$c`ik z4O>;vy%el0$>2lK8KB1q_j+@&Gl9=mL_I3qfPopbjgY+#yCw$EEmWZ0DezOvr5!M| zz^!0lVgk1bo534e;95XOUaF}wGU#fn8mbv8$b-(o17$^a(BZi%*v>fz&w+vl9>L2` zOih(R$6bSSBxLEIh!~?V(gD^w@L*Q~onVYPfOYL}U>7{lF~+VK`2Smh$F9^ErZ|X$ z@3Z4)Vq@iFWM+dcWQL7gX)|yzGcj{8)qyXZ0B!$;_SsOztr)<g1=0?haMjS^E0`{% zK`V&bNC!zq24&C%(%{SPK!p?s8)(rGXgNH*17T_kDqpdTT7ko>2AcFt(MGCtERR8x z8zbsbpDzDOyTE(dLHl#QfX@oFVc6^-#mvaWsm{p7B*4hRA;-wh!q3ReE(^W*1XTMN zF)%T(axk%`GO#c(v#~IzgGLP);u*QYqbdwsTpaNX930)C{045Hw6pn0JD8zsf(}x_ z3cg-YiwRR>q=TZN0cc><z{b!<TT@Y9MoL@^bSE8X*C>NNqdurt0lM_l*c`Or8#0;z zOEuuUs}5OT4xUQ~WnWVhq>c2TJq5z1p>nJ|Zja!JXmV+FnYVwrt3MMXi&;TNX=$;u zmxp_xmk*PYhNk<!<Q=f2RBl&Xq7<kV=qAde%NaPSxna6jc5-rNc%o=-0%#pJsJs+p z`oW;YP~jjbFUP~e!pH!+MMe~~b-)WU&<t8&!otAF0^0M;#LScox>30WwA~prNdmsA zLD~Vdj|5!}3wYrh_zr8h%18&$rY0o@C1D{|Wg#h0V?<a`gpJ+Uj>+8Im|YySvH&Gl zv5P7Tu3=_mVUv*LT%CS%lNCG+<agPIGyV8iEMOwcAt%qZ_37(Qn6KS_nI2{O0rH>5 ze=CNYOg|We7-Ai`xEPt3L07AT+9;srt~3J!3o{D?GiXmasHg|EOA)tfV3P&!vubAc zfp5YSQdI`8NQA73Kz6y{T11{VW%}_q0O2~2)4*rfS}`{<DKQu@bUE;=$T4v-!}2n0 zP)wD9i<ya;3zU~Z%L*BiK~*t(JSPVesEq__R4{=@2Q|^vf!k0Z#T@aVfsJ0sF~QAD zKH#H$b#*k=l@(=Wq@~0~g@yQexw$yl7z`K<pp`eQCV^d%j-a8Z`NGCq!FigQ7n-R} zuv`Xj8Xbj{w<EFL1s|9G4-|2b@L+NUkC)au@QE?9V+4m113NPlGdn0aIAA9mu*QQn z`?q?7PS^*Bg&Kw;&`>D@=*9)`^#@Re;ERkv_jZbkz=HrfOp0`l2Zq<hM8TeBya4lb zMWjmvcu>?hAqmN=;m9|uLp%uHg9O>js>GnmV9e0#AfhD6#Lgzd#>C9d$H>GCx<eOs zRJSUqNx{Ozm=0+uuyR03E?85-n}LM|+;rB2sRNG)LllEXdwM}-8>B4(RU7Fbt*!>T zicQT}-57Ldsko?+05>OtDx)f>2nQ`&2c7hYQa*u7BUrN;9QmLoGo(=g+Gflcl3a<> zqJXxX!Q-EXpfSz}&}b)<QdhTk9%`dP&_wtSGY6>Y3?ASF4|RgiJhB3haVo*jgasYE zArHDwT9lE6RfLh51=O?<W@cn#Kof<wEfg7;7+D#aSV70}GP5uzgU7$vAj9bl&>0EP ziFPPzKvOwjg^<B?h`LAzQAP$iSs7`_rHdSFpmB82;&4!54mqP4biy^b2v<X78gMuv z-{Fi@4uFR@5qCIWLn;#_l5`wEZCFMI<NsFR`-H7PXAXh9uf@W|%BZO<%FMzFIyFaz zk%<|yor{?X>OIKK&d`d!o5cqd&FZS418O+f7_1nrpzHc!!<Hhjlcm{F9S>SE3H1pm zdZ7n`{DBYVN!c13d#K4u!OLJF4KdgVAmkE&d?qF6V4k>~oUpKlrM8-tNFuCUc41*b z8UPg0u+VYw0{i*@<NpGT6PW%o=rMGIYIjBk&{-=Yj4Z4wjO;AJjLhuH(Aphz6uKIy zqGmy=-N8`@O7$(ERFA09RX}2(m2T-6I-qw4w6j6(@YPULl$Vx*AO5Sys0SJs1>H1+ zxUd%`XDCBYYJ$YOnJMVHS@=E0kKq9{1?hrWq$`X6sqKM<Np&6Kz<(z2?Zu#ai{ZZ& z=>AOxImme$tVm@c10x%_@MB?wj>v<z7Bqu4qJVm1(1w{DqZ}yGk;^h>V_`WaaZpVw zY|IR*Xc-fsrP!X+aYY<Le9SD&0zw=`ahrPJ?J*{ae@Dd3j06OXO~n{({^_+lx`4*0 z8HD~@F@9%~V2}dcZ7V4z%*evP%FW2c%mo^q^I~9NWoAXxtL$t{pfVWb;AUpfXe4Oy zyAY@W0IL5PnX%OR%EF)nyBWXk>__At-bquE5AB590{ZJDqP2YO8lw;++o@Baa8v(p z1zM-iAO><5BO?o|Fe5Vq%weEq`Jj{7k{MW7SYsje2J|FFW{AVY7{tKtf(*<sDxzi& z@V%gnEB-xURE9_JzvG~rH<=`y{^hm8qSOs^+a@SoGcoA>w_?g;Qe@C(Fk|=*=_`pz zGP1BrFfy~SF|u)LFtUS=dtqnwl6F9sVA5n{U}pu*+z5kDvtnl8WMk$`Wnkw5E%;3V zc_N;Hm9^KKn}dms4YFDubTcKy8hr*fP7Y2s4p6HNmp(>NzX&u92hs*wltD-bXavif z8-B$G6X--8d0ibw1|tI<GhH(^6-5Pb*NLB-la&Q@EDf|t0qq|_wg#J<LpImJj&=dn zsIVbB@U=jYgKC)cjO=9vS(N>pJrnAhqpeJ}d=#aX6HJogbflzsq&T>g|E+S4_D}IO zKW6J?@1vvT8wA><t>A1TDc~e59TxZRPCN9taYkmy=4@kKEolFB0l1x^&#)R2SVGK< zOze`3tW5lj%&Y>?gU6wQ93Vk9h#<U<VFtAvQy|?=Mo@nh)Qx9ig0x1OS$#l#P$qT` zb|!H6voN!yfIAZq4WQe#dO`bqn?WnN!NpZKs2q|7P2Fl~DC?`}%gIQH!}}0Qj7p#` zD5%<j_90-w1qx6|*Av7-?>30o`Af3!xxs=<*vKB-`Ls`|s4Q@IclQW&bz)M|Q*`}j z2n#4%FMH@MIi7{_30dHaalq{sQ2(nGymrEtVVZ-uJR=LU0wWWPC<hZ8yDlRuqaGsz zE20O=!UAr2fJ<ZW7-TQ_&<OBqmTr*8;g`dLdZ5e<EKJNSpxd?ZtBrJ!HZo*nurxQc zHL}&#R8f+Z78c~?2DMy44H#DNMg$Wx*qQ9$6DY*Nt58fpb6Bu~96ay@8u$dKH51UB zhp31clfJx}0t*wHq`zGXXx`Rel8uR_QdotXT}Dt`LPAndik(yC-%aSklMk|<j`pb6 z`}y~9vUoDHOUwUtv{nTVn4(-i1x<I};4@&f8NwWbMHrcxlo=VAL6=3#FtW0+crmau zfSR|UPAe-5Yci-Y2M#jmh8<94*3IAp8mG|IP}Nq`R*(a=O!#;?*+D~Z@GyW(QHqI* zDne$fAni4fk3osVl#N-5ja^YqjnUG##L+t2-6h2ow%klwl1G}IOIg^^LC4$PlbunJ z(T>ZmDk`GR#U|I&6tvoG2{cF9df9O@J_+=20i8I?z{sHe-wJ$(swqRPLzFlpGm``( z3o|bxD~mQG8!HPVBbyN;JEJip13SAHbfHWt13L$(9D^TE)(b!Trkl}+k-<PuQ(Z*~ z)SDI+W@lwEWi$oV$e`s#;2|#1Jv-3*RY8#pZjOOGinwOg9K?leR$_c;=%8a!oTq4} zple<nZ56DcsOYL+8SG}^IX%e6JWLVl@naxwMLKI3%X6~wSsS`|i3qvzi}>iMdBhlM zNC_;1dY%Ea|H}%z-yZB=Sb?s{2pULbWYT72V>Du9XM_5e8GL>+!oMsm;6(}G{r9lU z5Av^;hKv;GEG%Ab4mOy7LA$ZQV+CfWCd!~K_@H(fXvKj#XpI$k^bdSdud))-Y~&;( zd+>I#nI2ZI!S>cV&KmL|=`p$HdJ1NW`DIpm(Bku$t=Dn^DNUm&4^>??H9<i)RWD~l zOI{XE1yc>@Xii(0Z$W3p82`6oTE(QqpvIsJieXSY9CTtC3nM#+G9w#kgBcsE7iiZ$ zsK`rYVB=(DU}H!Doy7!NG6lMW40`JzXgilWBZIb<x~_(<qP&bWsLchw>Ryde4OGg3 zVptH|5JfxF1GM2!5IOUL2JM-$jO_Vp<6Kfq8S~*wCcTTCn7yH%{5Qqkll|X&M!RFS zUW_7{Ww3<oD<o)B68di`)RT-Xu(Zs?;P>B((VFQY12=;_!)*sHQATD~F-8_<(5L}d zK!k%D0}~r78>oW;sRY1{Pf)+1#T&FB2fTY3R0BZHpT()V3tctnW=EW=(aZ*2c?xj^ z$YMq|21d}u0ibFI*$JQtHAx;GMg|^v9(g%gDGAU`Opse-L3s(X(HB%En3#zpSHd76 zQ)R&>5qWtLF*!L$9ccCtHgqs$dI&pG;Ur460oq#z%@-@d=Mj22xT`a=GN?&0v9NM8 zGJ>uVXB1{+W@7aMAIZd=44$V2ZNu#abx^?t9XR8wD9OoyE-&L`XV7HS1T|v8caT8# zxgw=<uq(kO9HKd~)jiMNvA|tGnit_@87Hj(A3uK?M+s>OKPDxss`%s@D<&ojXw2Js zE#MW(NJ&iR<MHGHEe`+x_`emSJkwtW4Td}iVNm}|4%Gh=V`OF$2e%9$*O1AAPVQi4 zL3F^tmkEJuFz`V)(jc!fvch8q<X@O9_^uTt1!+kUA$~p%HrRO^$lVPQ&^k7FYDe-b zN=HM<QURLIg^lbv<ay=gkvkb4wjT0Krf`3Du&}FIGwyKm039jKz{ueL-->B5lLUh@ z=qyIis3{+)vCPTH$P79AMcRQ+n30WvlM!^T59lx$XzK@b)-MwyQwj$=xLFVKCuBkd zd_j;J0|P4?D+5~{10(31bC@ayX69y40S{i4(#_%%=^((sz@W^atR$==tg0*s-cpE} z2tlLjAkPaL3qtOoVG>13jjFsXQZXipNs;A|uAr-hk(x2?TI#WBjPd`LH>AWdii7(s zivK}nwgiI)L!?6(s08C@WCGPqOmd8DjH-<6Y-)@Q>};T;eL=VEq$AZ&;Mp1Qj4GnM zR#ude5(6I(13tY$gHZ#Nr;y8QHTZ4z;616J!9hfo1S_L`eC-zf`^8wYI5j4}OvY3k z8lA^%y;ciI>6sX5I`(_I_b>;$d8e|nIzr<TvM1RbypJcH(VYdfQUv?n5zzh;Wu|mS zchK2vIQB-!GcqySfz+^|>`z49uK?cD@QTr$VH5H?4HPw?eJbERIvi@qYEbqoa52g= z`~mH;VDQAH27F)5LeQEJ7M%O<7(n|cAbX)u_ST`e$CP0vWDg395iYwJ*cjy*7Bbyr zbZ5c6SIiWC?~NHwbHo|t8Fqr*jC(&BTn%eFE_41LV(5puCkt5(>i#W;eui_P{Ut28 z_nb*FtYGK`sbMWdHU}jxK>M8d!EVmMt%ji=vd@Xd4yPJZhJNTC9a~&#{=a4DXN(1> zF?(EU7(n}PmV?!Rjs<|HXAC!|fZgnf(;QHm2k)t3aY9yu5{AkQ{ft{c;lqNm2M$Gz zB11pq{tgzLcYJ{S#jqQco>_431LI=ohwPVP?m~7Cioe(xB^kTH{sQedhx;8RJ&Q0( zGM0hW;NEv-$|%j~4^qR*fzvKiMoGpPa5&@Mn+4xf#S)3r97#rbMhTEzpfeG0lr56r zJxriGqd-@&!2N~dcS(lV(0mHIT?4KL#qXR9{fv4byIA6Jx(5{R@u0Y11YL#zHwPt7 zL1#;bFi&PuV*r(PptIs%GHhbt*vQPdVH0S}6lkw1c+DEq4<<DRSI`9~jD8G189?HR z{{xspm`*|TGO%+mW=aK%hyD)$tvP_H=U&W|0~SyGpNmk>$j<1)(E1m20}wOg4#*8a z|Nk?D{?9?^1*_@)%g4aEk%@7~Up|n7{{LsN{V%{2$-IFHa<-v5Llq+{_}uj}MhURn znULHL+K;uL=^x}Qd<J!fDuz`cKQNRr?0~q5fq`i**#DsY*zAmci~?B9_{Jmz_P;ww zFQXsBW{BSZ0Zenj{#OTyb1!CU0E?Ud4?tGWy%==Y!~g#bApgVFgZ;J~?0=Boz&9X) z*8ly_Md$^qDM3}kz{HUFUjSq;gFJ&eXvLE(KN~Y66DuPFBlxTc@RVE=Xixz%4hlL8 z3w+K-JA;p+j-r{05*x3SHY;fG7B*IEqNZ-Hq^t}&Iu5jmm7QHoOc-?HhM=(m<1Q~R z1tCs3J{2`nHBBDg<9D*>hfVL0RxxhrZ7fY>RO&V`Wi@AF7M7HfU=3qB6)>l0-eys2 z4h_b0{jGm5z{g62n0_#5F_<$Pau5{aWMX62R1jxoWsqlNVTH}@sY4b&K)Xed0TPCI zX3#tjc!&htPy$WyKqg_e(UgPRQ=kz7$WRKZ76)z6Spo6ji{%-Zn7~2Q3?72Pq8fB< zu#Pq(gR!BuxsExg^$lJ5pv9;K9aDgALIIDZ7(+JhfC3FXI*J^q=0;}FdjUn!)<3xO zvIrU|N~suod%}ZPjRQKGg4o7$?JtLigc_GAhlUzbIQw{l2ckeDQjmOT0ZI={Y7CHc zxP~zdnhq@xX^DZIdkrJxeuzW{2F4F8+DvMo8%7w}*|##9GwfpE*vQDVVHYTify^}j z|BZ=}xsXW>w3nFyq(%m8Cc55jjHU$jZex^%=>2cO_yL?>KxYDh+@k{)H~(+J#K@!s zvx|ETqb^uH@xL<@=*DTVI~mwmc4R`^dH>xJ>KQ>d#@POO2`MA~yaagwoYw>xKQK>& z_z|4%LC37IGx{<VVaeYDOpHuVp!TaV`o=LZurpLK>;ny1gUkYj7r2Z7?b!yEkr%;c zFd~&R{Gh#5%(Ix(!0kAYnzal&L1D_cVJFB*pfClU$=b&p&ZGuDj{#Kv-M}yhG_EMb zw3JB=v}c=vjnO9u;%87f-3)dE=$r;nIVFXmH;{pWNs{>>lN$JbM35Ua86H5~0J(q- z<d^>eOwHgp(1w;-v%p~uDzhcQWgKWB1E?%d0E<KH?O{>_pAW&#ki`%Pu@`Z-#s9Bh zdw0R?Wyt!U0I`>81KeKK{{l$%vN2SFx*-4mGl>5eU_#i-#!$rx_3wXoB=zjvYZ;5d z?g{<xj-(zWZVD3r|LMO6V<OmJI$(dT2bJ^SvzI*>EtnoMse#Uk0;QGz3;(==rj=Kq zi245?bfXZXEsG?R8Uv_(&CcG;unOWwW_TU=AGFI}ggKo_jYA7$K6^7mBgkIRS<6h> z;JOIZ?q+8wWB35ki<CB8L1(Bl-(yk(-%18jBMS*1Mnw34>e6hm`}IKPb1!Bp0h<qs z7hZ6@fDT~+#S7TK;JO4^{RYtef8hEvl?kpMTz_Q!Wry5Y@)vx19V87feg@kGI;ad} zCZxRZ`5(Zr1#B1S4n%hD#f+;#ZutNGe*oiWaJmJlSLa^L1ZtD9Gy42L5B4ikef$5- z{{Ti&=2j*(29O!*5HmpT_<scKXC@Rs7bDyOQp1q{R}|`2QAphUfAhb9Q55DTkedHT z{t7|W2tm{^B>uNRm;)~7z~##STmNksb&&LW{onNG9z^e-doaDAv$DbY3UrnTC|~7* z{e+yiLm3#D!@y|>bRQf@O&cg&7!v;nfX>-wQe!ZH#@Q>dc<BEC<}h%123F6#7<70O zDBKwsn5Kj60-Z?$GDjJkT}+Z-y8=OGGWs#x0h<X*AJf5c20GUb6lcr8;-K^)3APKY zo_jG<JXk#Oe<~APJt*85PGO{j(Eo`{aJ^tPwSSo*;q#Xn7C!$i5axixwgzl|=zj}@ zUa&YMU4qKE3oOb^YT$EC*x9!*wuAl1geWf<Ky`~cxNZTR2@X=D4>l8`mj$kOGfXcc zA42s8F{y#hhhSi5-^_@t7gVQ#`nI6@ss>yyF`?CI$o6h!)JC!wp?4c&BSF2}7&X9p zLFJ<sIRAsrUjgOqn-KpZ<$uT;7f>5C7!;=rWehKn^n%*nV7-if3{P<A1(jc5y^MYg zpW%Ar!D$K9PY0Ft_b~N>_s4<sf+i(EYNkW<{tsY^2d5>_ePy7uR0J0H{2u^XYr>=k zzCRO`|5t#;L1`}@oc}@U)fs&lsu(sgLiI6lY(&cc3=lIw^%uxaHAWv}22js{aRsa{ z{hyAo8=U5*{pEy|$A3AYX-?pOKEfR!HUD4y6@;csL8uzz|KAwDf&C8}g8=#M2-yF~ z`4*&R6F4u0fx?~9j{)L;<NpDS-<Ym2sWF&=#JLwULDB(8d=oe?fz@*_W|9K?A7suq za2$i}V&A|h3$Y6+O@q{I0{1h(df7KHd_d@B0i|h(-i^5QGK11IMDIqp-T;Ij8Ppm5 z7>+>09aML7Y(({I03*V$Ahpw>euMadi-Cbj5gcBi@fc8e?S=RODV>7OpbKV>WKsj4 zW5UMhXIBAfJydKk*8cyWf$M(&lOj0Xm_yBW0Q*tke*kDt8blu`wwWMxIM@F|gnCds zGDQ9rhQ^~XG#&;1=Q3t9f$~5&*vt(K*T80S{kK5q1(z>kU~z%}#*B3^^^EL{UJNXM zu0qWGa}{c4)c<cx4WRqD7(nL~f!ZuS5WgbDm;e87OrlJ;nAAY$u`sYPWC=su4ADE0 zNe!ZxF_57Tq8BNgAbRh^^fCs5lKTJu3{n3rm>R(0V*&Qd8b(OJ*Z;pMlPHq}L_OHe zX@9Om-FzL{&ERqq>}K{YjCl}y(cKI#HzPpivu|O9l(}FxgVPv9?<SaDq;eDNW^fvV z=-mX>3+kXnFoXPS$-n?=A2Ikt>_ze~Xt4MX6Ue`y!!p?z17#p##qd9XDFW<YOOSiG z7c+%|<A~vZ0BEfalN#v!8E|?~1B-+25CEq^&^R5a{;R;S3v#ajsLYB2naSwKXaLds zKLC9HfEt4pNSu2y(-*Kf_x}K9b|z5U4XmDfG1Eq{I12*<lN;DBYp}ok7*1o@CB(qM z_#a$8M1#y^^kZ0uq!-lY1?y$VVqk*kMT!fEUQqf0>1AWc0^JV)IwOyPfyoSPFX((8 zP(C{Z(TfzGjQ<4~|AFm|f!YhH%hebdm_)(l3~1aCl(vI0^nyky)`QC#(2?kDj9$>V zQ2QUiBnplTTacODi<yGKeo_A)09yaUqy`$30>=fUAE)*|7e)OBMmebZTom;i7+!<L zL;vTZw7WMz(+{M~0F_;!F*tUHtpD{4;8KHW1E|yh>jlRtvmuij_^t?cMt_E{U~zCb zfW<-Kz{cng+SdL5KZD-?Z;TI^g_zVp=Yuh@vve^$0;>;YU|`Gw^|2U1<DsCioq*BD z0<|q7nAw=r7;8b{!1jh=4_GgnIJ+9ddMx5>Y`>)-X&Y5N8#{+GR&lmJpo1&G<2K)z z5caaMb3*$!5OZRn?qOs5D~fCmT%4T?+V277$2xG`q{6_!$j0{O*f~h;`2QTJqyW1a zO%1ym4mFRNt}v)EsH8G5va|jEpYk7U64MTdNzk<V8lonbfsvh^<9{AjHRqVFg3Jj3 zne*p=!2k0g7cuSle;(u_s5$4Et}v*xbN&zbe~yujK_w7;A}+$N|IZm1m@YGGF{rcs z{a*-C6N*cX8iPtG$UR&jl~B9DzW@K9LHqwV#)IJaZG^_}L~xj(iL<LQv|tfuWBdIc z68@;_+1NRLU=?Tk^BSu-JLemSI4CY0!C|No2#&9vpipLHgoZNM4QOiE)iz^Sa|INJ z3>pkiKqVGD#u#Ct2saBH)(jd9_d)95v4*McDAc^GAa!h@m;>93VIGP)NF0J{Clqy{ zHmoE#+(Gx2va`KmI1P3WnmD@}Xw(GU4;5fSsApsQtqBP)RP}7^97b5h+5V_v6=&xJ zU2z0AAC!MU?qOs5tAeZ^F3!#c+CU3Uqvyc!r0tC_o~}dGgfK9&;fg2bSa7&%Gu#4) zD=cSWgg<i_RNZBeI^6NZoCa0*7^Dt&JTb>Z)!hN9!yZqde8!v)%4ZDP3~xZ{Ao&eF zKEdiTq3WK2)Um^I8>&D2KznP!@!rb7z`(}#h8+^l?x^DIYQG@jj40xuatAUez<??a z3Jq}j5kwVd`}02$EG~p9&d&KCd{@^0YhZCu{DR!W&i3~|c%170DO7QGuK$pGi$HCc z`QSL!Da8}-j58r>^6|twlRh~7bs)11c=D$LL><F7aJa%sRn%|=l?nC`b&R09EZA|! zI}?gJ-0{xj2#!x3(Cj`VC}ax3r8$bdpm=9;f~o_Z4bP4v-a+-2Iw;;D{cN^33|qkQ zf+o(c#;_cVI2+q<&@m{`dJ$m`8#{+IB%Pp|!^ZXpbPpvK^_-yXQK0<${~Oa!a6M$C ziHJug)N=O!H)bJ-8Ye_NVyaoebd^D!!HD4oD1PAa$b?efG5r6=ycntuHUDC&D}bt7 z05%U+&SI)7gsNKzQU{MmRCS<sx;`k}87D&Px1HedMH6RNV_1VloQ>@_wB3QKo{gPD z0TS+L;%tAQ{T?)Nc1{sw^`QI)at|BZUue8S)WgNux!54;LFpQF$F3Tv^kigbumPO| z4Z39m7E_?OWME+S2c>;bDe(UsNEM?b14?TE>^IO-^klF)NV|fa!3H!l05T0ZuKxdK zU|{ZH)?!d&=W2zR2I@zHZ=i>_bu9jWW4Z<o*Qt<jW#j_efhNwb#_$)5I2+sV42b_w z)w8j46k-);`;&rIoSicjSsWC8AosAb{Y}Oy&dvqu7DK~K0F<^FOF>6pvAx0G&R|x6 zs4+*Re~h*W^Bzz-WGrO>r)zlnM=2*jqczMspz6+p(<{z$jCm4N-Aa%;+~pYaWT?7T zAa(Hck76&Vd}8hgl~0VN49h_3aMWL*INu0WcLLN>2E{#cEe9=2BAEWb+TQF^pkte{ zh^y*C!XDfv1gi(NE!o(m9%2_)g^q84+cIGF;5G=m6trwZ7gx1`m?HzKqrmFHbr-wT z|8Q_x3qliD{a*nV2bYl`_24p?ja`ZbVh*~vDzvQx&XXYZ;Ie~_T?*Q#Ko?g9O=0~1 z&j3!#VD;cM$SwuyLSqqEt%ljFg4D-RhxDx&*x25}!xcpy(#OInuLkaGfpvr91X-^d zxW9!%4&3L$AqVbvp~`{s7^p9(2JU;|kOTL>U~-_ce=oQ_0BTgKGqy67f>Ic?FUAZj z$-r?0SIe%p=Nzb)1WL0I9iVawT`h|es62z_IcVREv6bl-YUu~c@BhEC=z!Eh%1DsO z`JnNcR;DLd)rx`CLi3&)gGwYs?K!M!B|vJS`A>~OB^sg@G*^#OX8wQv{~L=ED33w& zpc;cpJVY&qdl71(?Flsol>{7W8Nm4iTo!}!4JaL*2A9ci|EGh)6ipt|r(j@b!!EA| z?pr`*vFKL=_c8FugZmnI<iULoOnFec0=Zue-1opE5AK5?%QO7{#?k^#U)tde3~HdV zoROWel^Ik_aBf5^um68zse`BuWMF{Ss*J77ppk1VYFR}=am%0$nqgp2hom#G*=NBi z0i~>EVEF%yRRpU3IRgWOIwY-u)t|wko(rn}E=WBjy@AyqB1SzV&EZlHN<*x&p!CL| z4H^N5j}?Hz<|?N7VD-XK_0K@=fuudK`JfcVxe?XB7XMX{+5qZoZ$NDTQ27L}dr{;e zZ2_F}YTz~j8{6At(A++%*=pc+0S-BE+W?0gJ7*-QmxXL5D4l`IXEnCJaX96`?F5*a zp!C%bX$M#$r!UazB`j%%WiCXmI|G9{V=FUglnvJR#b`UTYC`L9&^16H)ek}C9VDG0 zkJNzkAt-%eRS!vLSk?1D&EE!!PsUc}BVhA!wy9b9q3U-+)gK3`hom!f^Fir_<v%FB zFqSf`gsR^TQV%WPI5(oS#X;&>xuEJnx3qx#dkCZ+lFrc02hUALfYy<K#~#@jeJfz~ z<~ODY(3%dYxK9;KT!jhIK2Za=PhfSB`G1g{60GflTE~LxY;c{%4r;p~^nmQS57WaK z2yHt;%z}xlfyH5ZK<)K9@H!dLcq3?iOg(ta8F@|*q6V~X1)|1>p%J?prbkR_;1j^u z*`@xw{eK1uA;um5pCOGC)*-1;{jZLx=KnY5<4k8k^&O~AU~FZo2X)~Y+?hfcwlcFZ z@H1#_WJ_^zkY`|I0NupE(89pP$jB59x)?Pcw3pRiMM#jDLtJ|!8-t55=+Y)7HOOi$ zQ4uzGB}veAm=e;)Od+aF(t_d=QesMyrZx<W3_t#>Ft+?Z#K6Z83Nn8y=qN!(FYgU} z0TB-3psVZ|nDQ7vJ6k|u%9y~w$iNsN?H~q{VQNN~h;#tmbkE1Yr>exlAqI8|t0-u> zK77v^qq7315F3Z0h>EtInHCG96*HTjq5c2=Fn=<!u|e(vFJy{ie86;{fsHZHlYxPm zp^Pbm@iub|gD8U{gDyiL$gK`;p!<MWm>F2o85p@(nV8ubnbSEL+1c3G!$D`tvBmRn za5J;9v-vA4ferxFR9Dhf)<qlxsH7yMq{Pc133jKj8E79BcsnKJBt+15Kz7h7e(?4| zQ4uy{qyQ~rGtrkdGY}P*;Lv3=)|W9g6qAr(R}hhw77><_Ss^SfEi5b}!}QYBRzgBr z%*0MYLQ1?*LP|<pTuMp;MuW==aGeiM*Wfx8+?GbJGa>Sz`~Z@N%D~0}k@SJu+E7^} zb3thzq>qg;5Of(eTpvta4J?kN57ZA~GG{u)pvIu#4jPM=`mY2TO@X!O{{R2~oq>VL z1fs?Tq(=2W2ZoxD3=B-?!D_U9Kx(A^>tLw)#=yXI6QU*zq(=2WJGvT%#Q)z|bRlY> z`2sZI#KbU%DTHx3IA2II^np?XWdAOoFe4*q%OL~kNJtiU@Xi%RW(m;UgI)}bEbh>q zEAk8sj4X@{EOnrhUYeL0!C47(#xiL45fh`ow1X-GGZP~dGx!Dwgh~d`X_H8KF4BQl zKtMoJKvGf#atAxM+-D9x@=Q!jTyzdBADS$eH4{kSkm5$ok1Wc83S3G-a-e<%0~5mr zrVz#hSo|o)$jBrJ_mVW|j1)#@gusCK(G+y#wkR7L`v$llJ-MYg5(La-(fr6G7o^0c z5D1!AVqjt@WAcLLYZV4>P&_&~h%vIcGqAC<wIF40PF5yn4o2p51`c+1j&KIZoeLc7 z{tOHXa?(-~;-F(a!TFgBe}0A)jF97;v1elskPl46WeidC?|el@Aub^aZ3!7o&{CxT z|Nn!|r1b#Td##``Vn*M?;QH<VTX4U2DU%u_MBL{nMEo8|oS6$M&dx6JKN*x)|KCOv zmqZ@h1<gN0%?VrtG3Pp1J*a&IGM}C819a?In1O-O3tXpx*Yh#@{(lXwuaMVEfW}<y zz-w9{YJC2`MOOnV0~q_jc}^z<lrOQB0nZs27^gziWP{YG{`-NNcNv%%CNYID&cKp> zg&7$cBp6wk(DNu%0G@uC7+DyZ;CU2W34vCWf$}H=G><YcGJx_ZvPuR9W<~~ZZ50oy zt^DD6R8&Yrm5oCJT--CNA!<iu5ixObcphbBuriUB09BWgN_jHo0txKW+)N>CHl~tF zVp0;~g3?UVh+K*{k1_}%`%#P$oJR-JkMKOoD2MQ)C_I;<_z{syapX}BcLol24)i?A z2Fjys?Cfme3~X%d@eJ&2pggLg3_1>i*gOi28qlJ05ixP>d32Ypp^UhRoS38%S|(*R z)Rd9XmJs3+Vg%(a2FMuD6DBpt*bt*{DL4;7#X)%#Jig25Q;sAK&ZA&)b_wXbHP{>^ zaY^`iumF=9xKG~;s-M{2FdT&1iz3dh2AxL$n*+|%VDlLRLy+wS<!P`u+Xv|QG^h<~ z2F}xvbrrr0G3fafG-k<U2F}wEH9ib+=xRWDn&~*xDF$^08-|JCwme3z2D$tH4`{s& zQa2N<PW3<M|FfVGM#dfg&w^UCAa!6n8^HZ@<1oaSD^kCSftjJ2DVgyYGZzCVxYd>d zF5S66=lO6lGJwyARSJl3kOVben^{07vNMH)&g5c<XJuw$U}Er>c2EGzF)%POGchpN zfz&WV)G)`hGBPtU`$sx(s;H=_i3qT9NI~+oGAtvj!`f}iN^0gvY{nohXs*64Eevj` zNxzp85t5M+5|LucQVv$&QVNli2es5BB_$=HG|0Q?{;7uehYfVzI3pu?S2)N&GN9IK zGb<xABO`M-=;SrVcn%hJCN_|WzqErgSegx_ftiu14kXG1QO5*Q$IQgc<R9rEB`(Iz z1v|1@ke^GGTa<y5fm4K&Llo>yPy^rG7*yS|BehDATd41$*}4GYMd>>tQea;)W%0;` zC~+wSE3?CW2lXYmzj2f)jxmVo4+AHI0@#0|oGeTXjEr8Oq7>AUED;f4f;R0L;bDyk zQ>Hj*;Rqia0Jl-W{dn*=E3B^$9yb834Pyk!fknaT3L*{~y9SA?v4h57!Lso2Sy0;* zBnR66uoOuTOkABYkYOpPJ_e0YfaL#w`2USr9@N)oF!Bf0RZ{=8!F3fpwf_GP>hm&v zfT&Rbse#pJ$ZB3NFfi+Z)G(I1g49U;R|cB{AG!MfA9U9TGZ#dSB1nzue<pM_3~K+s zv4la?fXWQUR%Xy7AS1(jrZ~nLaM-JX!(N&VbX@^xfjc8ZGw5`l63}1}1E&ZxMlyg! zH&YzE;AUj-WeQ=~4X%ITeMcsD&|YlNwm6hJQb<G<-gkr+zn~3Kpqg>FwK1$ZM6Cuv zc>;7Ue>H<BQwakzgPemjxIYM56v!CM1iIqZ7c`T_z%0njE2_;dXe=nI)ynkqFFR=Y z+2sG#47p6382CVUTZ4|O;^Y7wbj-}e$jjgXx>*Y3S3U+lCSDP3c0qP^b9HlZb8&WY zc6C8fJE@=`so-D`Y1hg$$4)vZP&znRIxtAu?r#bM<Nr7Rs~N5^ZDQbJFmW(sWdv>J zW@2Jw@?v3TV(<Vp_L(370C6cOWrG~b#lR&d$igcIa;dtxIJ>&JvY^O92kD#~X@|;I zru(+i+1XOIe?1`n*8X43AcC-i0dy_|69f40ZAOqC3`{Kyj0_Bnp`d09Xv-r5njOj@ z8^qa-1w|_zrE_zo9T&AS?XZ=~&X%_Qs|dD(L4(neNrbVBaM%i)DvD|_zF~Cq^aSO* z#{c|`%b0#Js4>J(Vqjonh|dF=$Hc(Hc$G1l=@bJ8gCK*7gCeMT%^1td%)-pb*u>z& z$;rULDaa|v%frCIz#+`WE2_;XCMFKb<mSpS2BRjsET5yKAuoFrhYX*Cq&^=T(<zg7 zE3<l2ZYTw^3l;}a46GoVaE3W61FIk`%te(2MPBT0WxB5p3u7H{7$eNU9;QfUD4QA! zitc~W%Ctir8ixOS{#P@UFl}UDXHaucW?^JzU`CD?(4p+$h+$`77Zel(U8K$~sBCI1 zsw^lP|02GXDe20Uzw<!p6=n`ML#X!#@qh>iDK<t{23C+^pgv(L3nK#qb1VZhb1Q?7 zw1YgVBm*-819K<?Gc!Xh3nK$FgKwk*D93{@d=?Z0pZ*TD7Id!@$m)X!!IrDHtAVU% zV1)Yv!+z+YG9dfGSEZrY50ykpi7cQa<8avzI;tD&^@9h&o>yyE2Ya7^nL(ZL1XB{z zDF$W+b_Q++eFq&5R(57KQ09tdVPaxnXkzr?;9y~4VBp~9;O63FVP|1yV`X4wU>4*A zB_v^EW<_CRW@BMvX2$l$sD=O18lx7LHAXFDJi*BH?;i|<+d}{UGW}ru&TPfR#sHdR zW?}I7|B2xU(^Up;25|;u27QKB*iJ}RE=Cr1R+e<ofk>cU0b`dpCnFOR_{^zR$O(Yl zjBIS(Y(C)Qr8pUxSecp98L(($038DeRU7G`tg0d@!OO$QpslH*uc|LEE1@i@EG)<) z&MOW%H3f8<nJ`k#i(CuiVz)`Ds7T?#Ouh;#s`B!xDhekRRMh0;)W8fh5l|fsPYe7E zF_5$<!@<bTz}~{Z3~F34Go^w~;$Vyg-A4#c4N7RzpjcyI3<b$E#DdQd^o5+l!Oy@i z2mzo|IKW8~k|dzXGCuxAe0+Q>Q<7S{IxJ~2Fu~meN*Uo07fFGdMc}N)%EHK+3My(K zXJ<fj2Q(joB^g*47+69XSXdZhnHU*ZK)FMJfq|QglY;@0E!la+wZV=NRTgA6HdO{? z&7@hgpt+N&qn&~AfA{}thEk@D46LAf2owwqX!Ry51FI0E9#S`E7Zgo+mC(x6ym|9q z&~P$b{}u)|23gR#li)gtiJ=)ZCIqU9*cjM^paqq(xwx{RXhLaeLMv0!_3PmL$pp23 z69XFq7lVw0Bo`+KD+@CdBL~<Z2B;B`$_!Mw2r`?ivx5y0wX?HpWh#!$&W`-s%QV#- zbmtjTe&=O~goLOxJ0lwdTMGj;OS6J1msl1?Mn-THE27G>fND_2P>?i3ENF)`C{Xzs z7#MgNcp(uCI&cvjrr-#M6nOFRov_$l2Pt=$7=#&TFnTeEFmN-7G8j7OF@SDoXJk$X zo$$oK6wb=X#KaH}y5oYuACzAN`FTWnMIm{FjYmWqIgh9tLGE1>5nJc$=N0Jd?-djh zqNAq9IHNcxtGFmTtJHqMeE+}z(Ed3l1`);?j6TdE3^ELw3|<cIpwo9ivC7TJ!o=(a zPTx$R6LpwC*S;{Zq%*KGGqZ*>fNrp2U}k0Z2VDxGsw}H1rwKYpi-V0phEawC<RBw4 zame{^Z0yG1Azx586O@zfn8d+1zkr5-O-<C^@F{b1D+!y~$uP0-nQ2;k1-ZJ!3h9UN z>KKY>$f@aTF^8})TeGlgx@n2Zx;eN7#3kuRd1znPm6Fhx)74YcW?*6vWSqh1$sEQY z&7cnQgAgM#($Sb~j0_;Rg9fiz8JL+^nbXk+^kroj8I%=e)MeGhMfpKzyGb)jL;WBs z!pEen1l@6sbkH5d3y{leR=C6p>4rF|N^vVlnAu7*vGN*gICurQs_3h0X&dV@hs7l8 zM|<k9+px0gxNC^Wc{n%*Tk2}7>FG*oS%ccm408WxF<LM@XLMt5LENc|awqF7MqkjK ztPCDF<<r3OL0IK^{x4wk0o@795RH(B-P!v8KZD}`S&ZHw{VbXYd6@kSjG!48CeZx9 z0z-g<FC>L9gU$kgoFkaZ%E-jY$i~jZmJVuz#d9$-Fm!<`1&&w-@D*R6t2QKIw}lAs z^MMZ^Q(#m89X<v+feCz+92@AQ9#ch8Wl#|)YAmW~3Q8<yX5UrZ6%@oxWvpFX0~p!1 zEMLCm->-@i!+54Ee1Z-<0x1zu;b)z?o#s0~Dy{b}1|8_j%wYWg7?UQ`cLr4kJq8no zY6o!%Q6_dK16_3m8A&!4c11R3Miy2U(76DhBf#W1*q9htLGwHe>?};|>7e8C7+6>{ zm>C%vTNr($9aNba8JQTHF_f{gvV?=q1WjOIWnm47bP!}<(9!_i8zCwwq$nZ62|5MA zTuq%_8FZ13i5cj)bt5rxb7Rnjf9z^%>dcU{@|4-tg~5{`%wp=oY;2+;jAFvt%HsC2 z7UpI7!uEn<T7p9MB7%wn%G~0-i5lK2D!!&dvdO|?T%ugDO5y%ij#klZ{004*T~U=Q zB%_0wn8K4-`Pe;Jxmf=x3GjIF^F^#+e96qj^v_hxld)B%Bq*7I5xh1>lIaSA9>WUn z4WRzCBq(AfK<7S#hSQWK`IuQ*R2bP<K?kMEfaF*hnHeP*nLq>JObBT<&~d3C{qmrx zDi&5y8NtZN5Dz*gt;?H%m6a`l0opQ9V_;%tVP*m!YtO<8I)D<ZLhvcj8fu`CEHM!Q zehxO!O{eImK#Pcp!|w|ehbACVGc)M?7ii4Uj4{qEPF+RT)4)E~*u~DBoms$IjYol> zi(igI+riLXLB+>3x!%sXndyp{hzGxfdrM^QWJ$&h3mIi*W^Wc27mKZo0^DBwd{N5^ za+ZR77NP$qF^4hDVo+n)1DOTkRb^yiP?8bk1C6J6Njre}>~Mah_Xg1Mb&3q227ev{ z3nNPlq#Wm9=VAt3Y{JIO1iqk18e*CPXy~CCpE?H>gc9h9^PqZxgA06CIm|fF`4Wr_ zD$1Y(mL(-bg$4L|c{tfwSwMG3fbtyZ;Bi6tLF=Z>%EFKeLr@tOq@YA<#<VaxDjamg ze23e)yg)n9q3e>N1|?e2ZuaU-N^zh&nj*pH%WwO4hjBMgeM;QFS4<L~TAIE`J3vQ` zu`+=61R61&VvuH#XHaHPXV3<nWGKhT%%Z`_&A{Zv$Ii;kEF{3e%*4pdlqSf}$;>Fs z!v#7$m?0H(=W~;{2rm=(wpRww@r7!tDxljiWMvqn89?VQgRdWgRI-ACf})^%N<m!; zP(2H}ZO9aSYZa(85maUdwP)0og^ksf#f>3!grA?EUq*bHKZwqZFJscuVquGubK2m{ z6d%vRti{Oa5ajUv`}gk-K@R`^Gcx}B&v?1!->0CUpntzWz3|}w-<TztPBHK?h%?wT zEOii*W@KlRVPs`zXJleEVB}=dXJq6A9ncCo=S_!!m4S(ql_`~*5!Ctum5@!K5|S;R zhmnn~*PDTplQ9+)4vgS)fei>K2bE0CAWe|FF*tl89h4;{1o#;lK-YLF$Vu2s+6xQv ziwlTDI{XZLjC`OwF(Bs;vat(;0z_F*Tv-rOgRvs+yA=@=mt%rd)zDhc)C77e`P1Ai zc|)n*_}b{usSZv~7B-BT+72=bGKMnBYN}?+X7WMNYEBYjUJ3>pDyGWj@)1m@QqwHG z^?CjsU{wEilj)Stzpt*&ZniOE0?q=0DzdVga>80x+HT<j0)YZjI?8gIaw3{mI!>VT zs@cJ5XA09P26+b18JJoOdJKjPCJY%4sY;B@ER0?Ppajh<CL+Ye0!}@m!h+l^j1uB} zJS+^nj0~w9pgYBzye0XWczK(7eHa)_j6pXs>S$|eYN&%!lA;2GJcB&wuz4wH-2_ic zQlO;7tZZtm4#LLb#^zwmZY-({!pzFd%FM#X>deOG;M62;Y_7~~uFPt#+^-wq!)Ir# z9iX1A7o(r8>aS^N!|NHM6=hJa8>B0~!G<TKH=fsJhAns4Ur|j)dl3zIDnm(UGyh!; z0)c-wr_P+oz{tSQz`$g~bc#Wk!NS2*h>@Lv(Myn#fzgwdnF(|>IXfdm8mItj@&?~i z(aa7CL16}An1{fZYJ#$cps^sRV`wa@EUe7TE|?Y`9xC-K>EF3cZ&ROGF#0($TKzi) z!cPA-G5Xp4`v5xMTbO}?NsH+egE)hkgRvOs?gl{te%KI_05b!l7pTtx?h!Y6voJD4 z&-E5(5Em4bWaSmtHa2BfHwR;7aP@93&MpeVpGs9`ma5FW&m?{i!denwnwb({nt9#d ze@UQeW@?~m<_*vo2Q!1je*xxYOjj7B8T1)!7&bWw2{1A-^D{CqSurwlYB92K!0vFc zVqjomWny3j-CV@U$jS-6r7)g>nYjyeND)f{8y6GkekdMB4i1)hD2JPooxPpiN7}(0 zzvehDMwnj6nS|a@oskamh6XY+3=HO`pbKea^kwul)D-0zq#2}C)f5GJc_p;Lp$@sc z7F^S-gImO)kv}$NR>+D7*y;AlY;5f8!bY%DXW7NX^s2+MIa9mct#UlI6IA3>@~l#( z2BvQ=VT=qCj13I%(TZ@BGUR1p;oy>=tm~|zq+!C#&8@-Y^p7iQc9LtlrI4Vzptw(0 zT*L0v4~(4w$w78{(Y7Ld;%2&ij5;pX${NNNCZJgsCI+7W0!%DSSHbs8IXgJW2?+}D za<eh9@PMZ1K|?sqUJTHarn|gBmlT0Ql_4I~MQUg8VPw$Jk`(9XV`Bjwj?Bg@qRk3w zIYWvVb#`@jWnpnhO9(P4%nmMSm|<7?ii(JdG3KeLtE<SV%NrL3+XYPs2-Q*bwX^dy zv(PmZ3bNA>m6McE<PrbC<t?D2CD~YhqOFRNbJFDK=|-VJKCZS4Bh%Z$<-7u!ynV{$ zbTqWg#63ahKd^wy5>P!P$Dquh&0xro<Pgup#l*<V$pju!;^Saq=4WSO5nyIXWe{Ls zWfNdc7h>e*W{4GJWMF6l9irLH=A$4FIx<*S2UL0}DatD=D9b|bGX-S|K~Sp}d<~u) zqnt3Wh&HHQYb*-7!WeYd4){JiWl_*gO~&B*NnBafSPevge9x{dYHVcoH>F6#+mVrx znbA2=yfo7yI6OQ&e5TuF71Jk=8JWc|7+d=>o$_)q;}qiLlj1eA4SdO1ZWd|Q=jGy3 z70E5ZCsOUAz`)F4`+oydAJcaRNd`p*b%rhnQIPML<oTIcnAPQ&IM_uQ8Nk^E)biEj zgmiUT7}z;j*i#u88NrwFfDcXW0-cl&iQ8t-Sb`>aiU>^uGcy*okq*KP44}H2L6SjI zSWr|@T#$=bN}JUbJm3wwL|jx1d^54Sps^t6&{amz?HGb=Y^=(l793;gj0K)O$qrT- zx=JdM1~-i}?EXE|P4LYqVw_p8Wg{8j&UtuI+MIZ9HDyLIVLt(Jrhi}lEfN%Ba%0@; zTjk5eWW_&i^HWCfei6`kAOi#FJO~DH1~~>L1{ViM9xf&ZUQQ-PCI-edJ`N@(c2*{4 z7AEFYenxKYCT~!?qZxF8E9l-W8EFP_2GnK-=sI*pVPjEc(6xHv%IeDO%AgzfKv-Cu zjg8${O-)>x-Iy^hVMe1|vQKDy@SmXgQ12w!`e_Mz9MXcBg3?Y?WzI33`uEi#$l>kV zw{IPS92mKiR6-35Lym%yD+40~_y2E9GE7$(EE(b)qNNyF*+A!An=x`TnlmzRvv{#H zvT%SdCT33MWMtxEWaMUIOa(Qx;~Ci4y1-ZO#)Iy01Ya9yVx*_5qYbL?z-RpO^YMbp zZU#$6OHhLVbo8?c$nAE_=8C3@pu~t&K7kIqHWpRpV+P$-3+kAFuCEnkV`Ee*t>e}Y z5q9yk;P`ijv5te?!Nn*{Nm4D>G_{nS-8@{-*~^BL(du6{2iro+Y!#&<uDEzLc?S_i zDPa}|J~?rHQz-#|A&KBnQB45{0Yw=RRtJ7*u@YHOI{<X{H7E^7GJR)|W>8|7<h=ou zRE4Bim>3z9WO!Ivm>79MbCO=t4rqd`Ow6Fp2uQgi=(d0s=)GB>UIqiWPYN2c1Wgr! zF9uWv59Kt2Rf0RN5H+l<&7iUkJhs`*>;t(!OPWDiP*7A*6ndE_Ed4?{9PHqW?;+=- zi-N*QOq_9<QH-jJoV%8Ww~Dfxp2O5Bt}O|1IjN~k-<6p}g?;$MnEoDQ6yWyb<zsYr z=Q_F|Wo{y;S}FguippuA5o<=!=|+rinNBfCfzqrbBNGQ?oQ;E#fujy|!&DOk3utPV zg*hEGdBw;S3c5OyF_s<FRRzs}NHItW3JD4-2?{B3vB_vFGMj>8PubL14RlJbqNuVM zC<U{zu?sWu>=pA5V?T6|E7V7fao%17&Xy+j#DM*bUOIX<4i-iZ4(iShUIE<yjxzlJ z|6h!88N*BF3`TYa-hXcy3K%#xGB9o^08Ll3Fc|*-#>~%jg+Z7>jlqz?iXp-wl$()_ z6?9uR=x}0Y7Di@J@0x?1g(IDToq?5&oi&vUG9k?c9^eAaS2OsltBZ;-GFVxd7^xeo z8ye_oX~@fpsEMj^b211s3iGgmDi%E^QxiKTV<S)<!pEd;WXEJ`qQ|7H1PXpRCUwwB zgP>NdvM9*gpyR5Q85ab!C7YQgw*~mLrdon%hk`Hzz2HKZsNMj*;3B&zK|W!2L2l|g zn%b^hAbFU25N)a#TIA|j6lNIM7Y(A~8MnH+IQz`Kt*WJ=%m5lP`2UA#7t<;5sJ$?Q z6~k)>VSYw7W^qOic5MkJE)Fe57A{aTg)1P!!IG7cnT@#_RHL#pGO*V%uyAp5v2fOL zgU%IaU}H?<VPt3E;9}=UXW(Mt<O*lt;^K^F;N;@;m-gPk4Z3g`%>b+#S-=`u;yFP! z`%6<`E2u*$D#F8~udAUZBPC)bYQ-bW18H6g@Uls1GlI)OVPj!sMq^fWHFZ!E2DMKC zKaUx9w7!~}I=DJu^mA3NRB>swSMX%)@cfse#LUjb%A~}2T-I7tP}o#NMoM1OO4H9p z#zs)sUQ$VmLqXX>!<w-uDTy)SuNl*+L-CvvJc6uoYk0X#Id}ww#YOl;3^a61xHxUO z_yokbg@k!T43srNyBQc6<QNzj<C!EFI2rUDv>BM0n43X&c`<;BPtd4%C<6ln_zFD+ z7Eli2WZ(p~pxM|Yw2e(6)wZ#q+>4txBRy7o-2M9k(l!<Q|AXltc)mt}!IEK_gA6YZ z2Qw#w79$6zCL=S4F(VthC?hMI2qPmao0s<nxqt`<Ee1{o4rWe{3<h=v(1r5p+>D?Z zD^NQce5@v8JP!*OGh}4IK@Xo|E=ESiX3!uDOFR!F3k&#^ZzUl?78V8uJsr?>RB|#B zVuF@JmMj7+0{nd7X(C=;QEg*JV@75{6EkyAOsSh23o<H!ZlQuS)zEG|04>)A)dZs8 zI80;Q;{DG@oQa7`g4ayB$&;}_)j`h9&cwjV#M078UsG1zR9Z#eRnOf+TTWd+gz3~@ zGe$W^5h*QEPEk&E!<c^||8DW}+bd}*t0<bQ%gf7&3hIeT+ZkvYb1`!mX<C8TAWQuJ z!K}-4nL(04mcflt4RWuyJTD^yBM&1tgE=D?w;3Z7Gm`@&2d5GvJBK2u*X<?k08+t% zM@1yWAR`8D1}-LUt_%hy9!6$PCgxO75VLb~q=P1X;z48i;NeTwcs@2>X2=C*kcczG zt&@eN33RR@baIS=ogG=XgB7tFc^Sdg4``B%4>F+^8R?)RFDE6%z+i1@Vx*^|t*NRk z=O*taB`YN>BMoX|iK%Mv^NMQ=o0@|PLA3gt-B^^F(U_5y*!Yzw*3{%zm6J1+QkHeq zcJQ>e2t3>kj&sI7Y%%Sk4~gkeMme(rbwf3Ac|~y%JxMuxb3;E<U%P*gKq-KOK#YUt z(iQ)IV_eCk#Kgt|+7rhJYO{mR++z@Buz{3ZjG%sgDmx=12O|R~BSR_!ILJ9Ux#GAP zxwx9SK%HJu22l}6xdh6&JfMC&xVa6QuLfNO3K?8h6io!Rt)o`0TD6MN;+}h?J0t7A z-|ms_eW2s?7{GQj{9yXQAk2^enE_P*mA6<N(BjR-$jRBv=_BocO9^KzR7s=*XeAp` zw_K1Lhx<aky}i97K*SG_+a&)zcaL=c!NB<cF9QQOOyn4x9PB~sJ_LD~m{>u#NHa39 zGBJWqIcH;JVrF1U2e%Ns8JL+_;uydehB7cP$T7$X34z)JLQ0?!0bxPV42(H=5x%;p zqNt)EIO!;ZyQboT=Xtn9MA?m44Yl|=8CCu@@o<WYvKg})X|nOKF`fFikX2UJ%8lhp z0AmQNtgN*wGb8AnJO(CEyn)9H_!(pvvLS6Z(B;F-EUe6_3~X%RBZHf~!4t`##tLJr zHz%lH&Eg~NpbUvzR#4=!GN9>zs)}?FWncjJyTpV982B0ZRTTvVxj<cP&;W-iq-V*d ztSkr}+ENBbd183@-kt{2+Uy{Yw3NuW=t+|p_xtTF%y!6j{P&O1-!;s~y#RFlCG_m$ zcyPN*nZeJ&o1Kw~O^A`1QJ9g1nc0hhje&`gjVTp$fiy!bXo$NB)CFz^hcxKkm39^% z8EHlad0A;?8D-F&8sOPgDMl&KRRy4NSI{jJ;Ndrvn<fxLag1uM^;(W9M>5y;DaiAR zadArvIeQ;S%MK3+0S~A7=s||ly7ZM<S$){py@Hu#{r-J-b#}7`)kiE0djAa=A29u3 zP+(AF&|@%VxCI$o78B=ZVq{X`V`5`8Hj-iHU<b_rgD!T&A_2M?3A#2|1Jtx>VPIom z=U`(`WngA*0r?l)>FWh`5Lx0`p|J<aC^`(_nG_uALE|S3930@wc)CGXcA+U}WQ3W_ z%nY8z24B|&Qyv-Vz#}XuDk36iswu$9E2}LIpI8B>T=1X@XyjT<6mnv+vapGoI=eYL zyAnIQnVBi8h?uxJ8{150M}4c*3?oiXEg$gpW<r84&K_|Nf-K6uE}I1fG`$%8+1c%! zBL6dp2$`E`UO30+ZRZinJ%<r|%NgU1XUa|gCj01UG3Ki1^ENTrGBX}!^!9a<R^eL5 zV+>kG${_IHfN>_%4+hZKWvhd*00$Ezv!aABGaIA4Boi|`8))VUbRDi10}~6V?^y>r z4!DJZg{8%tlY^C+krA@M1vKygTH4Fr&F%xb@Rx;&r5UtDvl*-$m&!;7K?MZ`B?To_ zB_TmUVNn54W*1i$R#P(-R5mp?Hx@NEH)a=AW(H?}QSik->cXPt8P0(`d_KO)CeiNB zUP9&K+9F!Kd?pg28iJf|7Z@3ZO_dofw3LggAH25w*CEHo>del`&dzw@-aYVI5{AV8 zE}%UoOl(ZuplXyM^uHTZ2Gb7)Nrr^2%#4gIjL^G{<si3|BtrXUEG*!o!+X6!-7@fX zCNKq{78&Hac92r$Sf~O>e+)FTpd=_LtOlB17Zw$P%`poa3o4t!r#1xFva(4<nj|Ls zhx=DVIY%)4xUZqCsTG_4@9n=oyBNb8Q{(=<hTaVfa(@leDFz|Xnj1lGHdYo^W=1d2 z@*)OShD6Z7W|KE)X<#!WsG<>K5E2y>hO8)31zE|=4w^j?Gy~1@F$y!)gsJ}ZjEa)8 zlRudG@9CR>e_!@6hC2Lv&2;MD0wouv8~;xJ+Y8FCjNoumX8OS(&JgdwDaHq~7amrU zFzX>1kA<Zb<^<@<Cy>(^n3=(=06`5wBo&aAPvQ*XQYh|J2VL)MjC=t!lXAG`F5B>M z#Fft*y#D>!1-{VvAL6d(mkf*yYX4oBM43)8a5I=X7;!Lyw(T%5GkLKxvM{i;FfcMg z3wQAPD+UIJX3&wy4BQOdLV`lzJDXWe!52CziZUt+GVcGE&RFqp3ggXx9gIzT_P8*a z1-Se@_3sM<BZK;XH_#bz`0Z&06}k+dL+bF^Bgz=@?>y+jNX7!tEt0!-fo_U)`j__) zGS2XgX)e<h1`!5jhV*Tm%%IC@yddSJBs*xPCZ3Uv2|RGX%E}ZETA~8J@K}+7k+I1e zq5!l)0yY*3Srowx9yAaa6J=zOkrGuFQ|99V54ngiiohl@L3av*udi1I-JoY;hPZqj ze44u&W433J3@g98J3p&TK|p+~t805gQcg~CM68NR9McsGy_mmdF?tp;OY`%VM*Gcb zZ=YorZ$HaE-VC&_A9PkWlPnV(6X+liMh4KCh<lkXFlaIqd2i4Nh;ZNqtr%67;%8<7 zE!C6;^I72hNQfd4(11uRD+?PlXaE%Ma|uvs53ceU;vqhVN`dcJl9vMwg>rGSvB2(; zVl)yL1((gt%FKF9pnf5<GPAL;n3%YknYl2WVP?kMFD=X~EG5o0A-au|pPP|Ugtt9< zZ(Kf)AS<f~Z(VF1p9m|f5Kj=l8Pf%JPgWK6zsDG_$XM%g@`mdD(`P#M&qpszO+7}3 zas59(y%;6caD7HsThKwI4BVjemzh9!euq2o$}loBXvqq)u|O^tlMIM(5CNxF&~P9_ zt2Y}fXnp|H7Lfog>;We+Xq5z$g5)a$1_NPLZ7J}ILeOeE&{b)6Oi(WwgGvENGYYZ< zh8^O4Wj-c$=6RE&y|_hKSeSYE*y9soxWrgkn1#8GxD8YVWw}`eIRvHHGty%?`MH@` zn7H^k6PP6aJ<^Gg5im9vW7PY1Nhv^9GD7dVo-#YDBO{BH%)b~$JsB%KE-rm5nSYlV z!0j)_KTPTjiVQjo9uBS=GE6M2Y>dn-petEH!}ko#Of1al42+De-t3I5tdMx@1&w$$ zGx;bhF)}FWDC<Z|N{ERF3kfnPGAe=+Eo9=5T^w<nt+F~Bo3g2q8Du~ovK&SnG-<KT z)XmOH2z<9K3%?LYdECX+Xc1Eh5fuS$K2Ku_5ngTwNhWn$Q;TpZ1H?VIe_woKVfJKX zlDCqyws%qFmDG`!cM_FhU<93i4ju<%XHW)hQvh!a1f^``$tK9^HSlDUps^sRDVrF@ zX!H*>%6iHF-)2aC#K6F~iAjP1Vh#fXBNGE-DyUd*h58+|LL9z0RT$JD7F8Bp3+fp} zFiG6^dJn#DjS0L5dmfV#gCv6rgRX-XI~x-dqZA(#XrBdii!fuaH)!`1c=%OGL0FJM zl2MWcJR1U9mS8Rp8ln;t6Bh*Ul>pBHslz&MqKwIX^Q?4SRCV?J^g_JsA`JZd42^>f zZ1rVzxY{NMwwvi2X1jTYsk1U_M8`Ro`zJO!db1kDxWy$K*n{d5CI$fpFGeTESK#%d z99!8~m>8H0w9VC3#TnIA#Z6Qg`BeUW0+D}Acbfj2YP!>ufr&wbsgaSFc@_gR13!a~ zgC+}T5~3MYK(;V2GchrTGeE}qnVI|<7{GULftH)HvWaRl3Mvb-gD(02`OMfzjFH#V zL|zoMr%P0xdDg$@jFR25($ccB(x8KDK=v3g+A_N{a5M0OVw0Z{wBdn|mx;lh0kq04 z98~ClZku87S5abO6Vnz(G8(jsdjibtwZ}}3GxI}@Ha%{99O8EkrZ`4NsQF6Z9o!7e zj11{uPlSRz5esTQ`3efDD6z0fpc=1es`vtCd?d)_aP!ZBF3w`mU<zh5V*1Iz%HReW zgA)h4Igf#Xi3xn1HB%gD0SuF$w1XIEr8_A6P$eLnwjryiL7p=;HZ>O2xN6E2Y+ASs zoJQT4JQx=+cQCLq2s;QcF)}bRG&3+VmIw+8GP8+m3yUj@Dw`^QUfb@|&fKxDsR^pb zig5vR6+{o%V$e7eT#q`)5@WC)CM$>@CI$~i4~8Smso?llaZm*70!25db;%6g>JA$H zW@7eNQ5F;chm<n2F*7*5f(BKU)D~MxW|yc)Nvf(!N~ti(GrIj#SCx}fRZ##P0tPZa zh~WrxJ~*D$9F#%V4>B+@*D)|NH-Q5UDWb8(1-r4Z@)4Nn(Uy|g%-T@XxBuG(8Y}Q% zSix|F=_oThdldu2|1i)wI7~bKhk*v6!C|7ocmcexl9PdLGb<ybfwr(ItEjrDx~aOT z24mW6(?lk&1D#ej#iB)O?VvesjsO1{`xreLK<b$o7!9;pRZYc3#X*>{@82xbMAO-f zT?a(kRf<K6Z2tde(D<*%SoQxLGw1|ehCdfT_A~DIa{+2U6N3js7{d`JHSjuW83##_ z|CyK}_ZBiRF!VC|sHmtYvG7W03nPUoq&$FxuZN1Hq?($fqza5?d<0pjtnlv+G$=sv zWyG+Jv6t~EIIkNx=rS;XaxMd7DrjPWi8&P1Vg@zIn3;S*BZ5dv#X)-xgiRG?nG{Wp z7^D9!Wvp{|Z*&JAcM!;MmthZhJX08aVIX8?pTS2^fQeU3+gzF17!>(;8Q(H6G6XVC zgsPQv5Cb)i!OgKIa0?k!M1Yn73o{!tXGvyDGEQW){0Gjb%>Rp-A{iz#r!#Odh%qQJ zWNib@U@(9>2r8iU{-79XW&@W&94zdh0tmDTjk$>lG|GY`3GpR(>laf3SS2%4K%|3| zgg6g31B0B5xRQjDkN~$Bj~D|N1D6OVuc$UNWM?KH6Fc(aabxgGL&(s9nYg7uCbyB5 zc(SmJ3~1Zh3NS4r!*q>D#$Aa^*w9d@7Wr@i&>;g149x#?nIai_;C{*hwI)HOoA(B= zpV&a7HJ~Da0kmU{8M+0HsfpP~+Cd3P65<VJrf@KWi8%q}NoGR60&RI$)?-pP6*mIa zC!p&akrI}viJ6vwrMR`Yl@Yfoc$b=t%sHs9nA4TqWq5=P4TZT@gM5PQGf<hz_^*b^ zn}L-Xv>r#rK@n+x3DU-21_n?7D+#l(iD@(1F&Tqy1ZHFZSEE!iBaD?bYqchmw{wuX zhdJob>Hoi(yct`-=BYcVfaa82m>EGuXgH|O1husonEV+TI6%wQxEQ%u*hI7ijUbz1 zl$A}xW|SzoX|B#<WoB0oa<(;x>=F7`54w|r={^H5gNB1DFC%DBpAmd80Rv+T0}}%S zxJU!_Gnp9tm4uW)g{hz#xLpbz?J`QYw6SD2=85&NNHb!p;}%wwakdra1|>EIrUb@D zrUwwSK_w+GBWU{(hy{vICh!husJmfigYQU%jI61t!_9WLOfzCS2r}EnMwlB^j)Bfu zi(t-zou|USrU25HMi*a;Lwp?$@%1R;9dPpxgO<mG&q4#8C5;e2RE%sdLi`{O^#>s0 z|KEf6$AjDt+8@uxzGgSr91QWbIK<cC5MK`!2k-A^2Dt}f{^9))^WT8&MTj2)U8MB? zKjb_BB=rZO>e0<P01=1mO$LWQcrP&fnq08?7~*Sjh_AyTz8*y!<W7kBhe6wH{{M%X zj}SiujW6(?Ww84p>JQ>je*hx>{~b6SHZVcYKVaX#3+#Rj@qJM7=V<Epf(|wM|Ns9B zH1RzU^-y<$%!jyVKlH9Bh&vJD`*4Wwg`T&Lu6_?lJp&^{(tjtWGNvn_-nRpv1ZYVE zBO^02BLgF7fDF_*lVkv`Z)0RgW?*813^l5Odj0C4VpA3_$HJlpYQuxa33Z|J;0~#r z3~2F&0O-PBWkzLocF@EzXr2o+@?~Z!2)>D)O%yb6BW?_uOcoOr6k%iBBB{zJpe81z zFYgoO5vUmD&d(}r@2y(cma~tY&4-m$*g%Kzc65MQy!}5vrc*KUdXgno{}fUZz+umj z_}_^MbT1AYBj|`8MuzDB-<a~5t}v)F)NGYxWMKwXb0Dv2F)*?)u`n`$T1Bk!Y>cd| zYM@)0;Y`rFUeExu9%%8629jd%;xk>a+E@m#E+nPkwbSylpvCrr{9GKYEDWlQsvHoH zo0=Mf*4V>ujAIiO1uyu8_#CwUj4`RPEh;iHCJ63vAqQ{Of_AHNu8HMbyx|Gyf%m~4 z_hL+f`1_StIq1qp24)5mrZC1B<}3z&@T}cz$S4Y*1S4o?3o|1dBO5cbm$U;&fJK-Q zCIB5}R%8GPFtgQxYCQ%9CPwfK1{>&F<0J+~MhDO$d<_oJK`aa`pa5fJV`OHl1BDJ2 zrI8M@Qc?l}j0}(&GSHzhq9WW}9PAAIjQm_2;H6oh`ApaeC1&E_>Jzk`U0hVrv{6FZ z#0qpUiQG;ODQ+!UGl3cZZZKzoPH|#Wg&yg|1v)wgwEy}rsLcfNdpz9l6QO<=gPtD4 zzzEuFfOL8cL;&h{1$IUj237_Z);du40d#|NIs+>QBO5DQ5(5K+11R>@VMpY^m4a$G z1|~){m5~mjAfF@r4LU4`lS4uqw2K|GodVLxF^4pRK$E|wilY8t--<~{8+&j|vnL3c z%bZ};VA=umEXe09u+7dn?(TTvnvp@6k(r4L5!Z~MxP}Tq{j12p%*4h7k84mjJRP*E zjFpWwiGhj90o2L^$2B7Z3u7HjF(|IFD8(AroE&VdsBsP2UM&h<Zh#us<)EIbq>`11 z^gecJZqRWB^FeV9J$^(Ld`<_pxQ@dX*Wgn~AW@5mYbJ;Q)bF6UW@2Rm$2DXG0uh5y zud6dKgSLJ#*0D3PK$L=dG~g{*uv7t7i4oVJLs!6Y4fZD_o<aT=M~Uk_nCFok2Sqm6 zt5APSBS$u9KI4BaQyjwvrhnl6Oq2r`J0mj#BO@dDWC>}|@ntNGj7*>_U>RDxK^q)E zok!*_P(+G@4`F~vBg;fO2=VeTFbIQpdV)rXML|6hW>Y&Rb7N6HCU$kC7P%z9vLa6w zkCBylDfHMcrZ^rUQ*$vcAxldk_|Xvz3{3ySnc^5$G6ykmGsrQdIdDq}FflNRf{G0< z$jrYO8+4?a6_mIb(m{ilEg*L)fEu=-A)z{$3MK|dNH3KY)D8EKbP(m?VPxQu<B^k; z5E5i(W#DGyX6F^r7DRF>JE#e63>n-6djslTM_wHRAsZ<RNoflyJ7Ggz9;WxOBOxY= znVAUl3K~dD8wm0Wo0x$v`(XZ`!<5S~73>dj25E+P$i74gb|yAv24+Uc4fiZ8tg)b3 z$R^NWr5v&p2O}#B3u`Dy5;TX#%HkX8AT0sjWh)^qAuS~d33Z4UK+}@UI64iW(l8Fx zPY{uoJ`bXWrKP?9U1tswmqI$RLsCk79;g%q^@04DVi`6=-L1>u?%=}A$jHsa#mvA0 z8iiry;bdZGWlv@RFTQ4EVQJ#<fx1gqLRVW;T~%30K~6>r*?pizip)qsjNDaYH3Kb8 z0oCQurZ%`m9|sL|*x7LZ?qNMb4iN>gsF;L&h8(Ex!}LFbDVAXcb0Gs4g92z39<-YZ z>J>DDW<MC48JIcQnOK=wQ$RD@@!&Q1Y(CuF3=G@~+#s(=h=~dd^7Ar)`k!2={ZFip zcNDV_1Sj={tcKds#=0V+LhMGY20D^PdZJ=NY)mhCWbBl<gmrX<dQe@<1Whv_xAHMW zZ{q~5mIO65Ks)Hg!7c{pLP+m3mVpV<^^`%9VPIlrW(s9sVq%U1cRKwd9fZJM;p67x z<pFsCoJoaYiCmQ3R8f>2)TzuAuoS<>sLAvI+>ZoxD2qWU0^Lo};De-QYPm@SQgE1= zK?)9#o8W=i3vv^@*A8|Q^Z#(BNQRZng$zOratvAwdE5CoL8n!Kf(q18mu3X@9-G-2 znOK;bIaxWFnVDJQL2H7+J#|H7SvJs$0G4o&By$2-DGR8hF0CLhBFw;`rXsJUpd}?C zEGHtzAjBXfCd$PtrVW||F-0yRa1BG~p%)ol&<qG4j<_vupfAiTs3t9~CdezSZy?@; z+;<1{<w0qf`F}Q3B*PSD7w}l0G-y2w$eR*upy>@JMh4JO5$b4!0$d8z7z3pV*jNN; z6b5`0kd&YRD+>dIxTt`%pfoop3qLDA18BGhG{Pb*s)%zGV#>ek=IG-OF7ECfAjcv* z9$e=@@*c?Dk_^@k77Pra9LK<z3d?fI$XSjBw6k53MN%A;-S~L8IM~1rf@C>GQS2qf zR7TBzN3iF!zwYh}P%;?P|7@l>hAGUh44^S`EeCb*#&1vnf`*mjL3LXbvkxd#xDnw2 zN}A~5G3DR&DI)UnB4ToK%&zY4<<hb;GBPr<;PLVQ+00A~Q<z^efb1}E(1qK<$_S1J zkS(CPm5JG(IGd)yY<h`o6AObLQxszo^9BY^=s6EF9JoNo2rz;UOY-vGpcW9}pb4(I zL8m4$H*<le@0%GwQ%LMAOc}f!Jj|do$3RDaT7XV`(8sM7`IOKEuts*afJg^5(76ww zgO`xceGnB965!+I66Y2NZE_Uh;}8WmT;azrsT;$`Rd8jSslqb+;bJCIjBcJB=In-A z%4VFF9D(z|c}GTOIW+MxJ90~ED{#q}iWIn7D2htRnY)!^NrRZ-G|PdDgOQn$nGrPW z4GkwQ@IWOuD-$yVb2DhQOfv%u0}~qyQwAt}*y6#SX0rgT;4s9k7OaY$oh=;9U}H}J z1spppq`0^kP)^9;66XS?bOAmdL`cCqF|3dk9nipH$5FX&<(CmQlrj-xWZ?*OL4?*m zXi%L6)6&w+8x*u9xkXH6xRT7>Ab~XzIje$#4OGssFqkk!F~)$?6fc7$gDOKCXjB(6 z0t!k`${f%UP-cc^PDW<X!WX7A&;%q)JU1H`GYg9aXg!cNHdSDG&|YgWgM~GoospG= z)j!ffNm`1Z4-__v@=~hOs-nVtlKhffyj;9I+~9EG;Skkk1fAWb3~B>|nlre@M;W!m zOr#8jW%xV)-7p7xT}B2G1fY}xN-@3e?rB&&kLmZ8ZJ@oJ;1O-G-@&yksQhAVW?<rE zWM*YzPDAnxs|BbLr-@A!$g3=@;Yb0+%0i^ig+V=9P+bG5sBtu+4oR7ah4ae@?`PEb z2=zE3n}FgS6#uF2?oFVWM)o`?&+9SyGbS;2fcyOF3=0wcUT9y4jRoB27lWS2$iN5^ zfcAydI2l=(*qB(@>Ocd(;JzILI~OA>J8KF96O)rS2O}FBxOCBl^@!ja;5{NN>LVQ_ zrKJP}R8^D|<YcAPrPaaxe<2Z7ZcslMci%`!4XJp+(n~T!<Pl4bK&ES0J4_QD%@qZN z<;)!!m>IB#4eUftNKhb7&1HfJ45qL_Bp;Np*@Wmp!cOgM#TPc9VG_`A3E{9ojF*6t z38D`Tx*CIp8MLzpG;Rh;Hw<hH;B><TUQ3n^Pd5w<sObiBurMbh3q%8a)C87pkkn&M zH=ywoaJqqy6+!ydNaH6cVS_o6VvZIxs?ZT6ltC3l(BMlq;M0~N$pn#Z24UFXNH=b< zQ6~72%su$R1~b`!W{&@VWnRa0ib0v7!Fz*fK!gLIl(>i>3o9cVXwaC+OBz{#g^>xA zW<bg$L2boYP;Xxiw0xBzmX(E_nSntaG`J><BnMjEqz+aAKE6Va!3R8^Dg#;$BPuKe zK8r$`Q5iIzDh^&y59)lHncFd$D+&u6GYcy-Gb;--gO>U5F*yk;^YKUv$SH|%aEQzB zF*2{;5P$bw+TH{4Z&!74ii&abs@rq3vwE|!>wBqdOgdQb?**g8zcv557|s5jVk}`~ z^I9~;)H&7wR4cIm|HUlMB*7rT&<I(c#V0Jl%f-yf$Oc*@0P;6KOaQbUn}yL6x{M3q zaSgDyuzOs?8;8e5MT7+T`FOZFIoMbjBp4+i9#=O7dE4Ar+#KX(b#`T8b!9##Q&~YC zelrKbcU_&yI}fK!nv#5|wTM^GhmFmd`RV6Vr~du=_wL#?Mxi|`&&7h~t`iv;n3b4L zF$gk9F<3d6bFwk9vhjebMlS|d@G(80)#41Xpni&)H#-Xx6O%fV4+DdggoqG>AcLT? zkcc7&yM(qeXs4H$DA*~?Y$yxA6Za++8&8gZ@-FR)VvZkl<u}tQqg0>A;BbzRQ~CFb zG1<xA)rElxbY~2+0+SMhG=nBXnFE&?IDpyMK^;R-u_MpG&JH>@i;00j!<&<ljg2`T zw0KC}8#K<M1!}vfgVtSWfKGJ)ZvxQ=Peg!~vVc~G>%mk;I*7^2Ffgbn$!N-If{wCK z77|tw<YJf9Mmo_1>>tn)D<w5`&?aJ0VaV=rWhFLtQ4z6F4;~f?KgY0Qz79}ePv9+% zv<nnxV`QCa;2`hr739L`_<@<jQ`sOh^8_g1FXiUzDF~Vf-(l1@jkAf#i-{9V2F=Yd zFfoWRFfa!&onjDUkO!Te#0bhuUZ52VY)s6l+>9IyY%CmXsho_AjNr+A$RtH97dsOx zt2(QXgg66(w4}Jaggk>7gBWPjg`l8154*Isu`p<do|&<tC>#ocPE8P0h8+HMLd{vL zwT<!DztfD$|L!uH{wwL(c{d{`EGmN0f=4gFAu-+mzTba;MvZ?r7)uyAtzDeJbG^(A z0{_1;+cGIJC^2X<7%}{Yv^j(X7&(~P85vpS8QB;W7#Y|=E8Econb=rh<$)FhI|C~t zJ8LQrBPRnR8z*Bb7b6D;Bt|trtF9U1nYfu47(jC`5DQE&bTA-k;Nrrr*TD>%HgIy# z_h#VY0x$E{0L@*gLu}<{1X~&z>7b~s#mJziqh+LRq@kvwEGsQ3AtJ=j3*KU`#Hhpz znbHAoKZXRfv8Wk%I80Pj5z<W(Wn*Jjgv@dqn?gdJsWiQXx3(*7ro5F(M%Y7XZ+({( z(|;40A_I*5wKN0nFbXo-Ioo?)E@`zjb>%9`z50+{#m>Md$Vx%QCfCz6I$l`FS4fc2 z+a)wGd!wPMz7Ax49#a;0-L$%cijbfHKObnz13MEp0~4bcq{s#3VRZ%{F+m{#7Iq13 zMP<;kF=IjSNu$P~lSbK@vU2|Ijf&bBW)v2Y)*fXXQOB6|ZyM97f6ERBn+7FL4>Jwz zV_*a=vtnXpl3);G&~#7}6666NWy-|K$iV2u$Ot-d6qGqZ>rm7geFTNXgh93mn}fCl zh_kD+3oA1VDhnD5nm^HTwN1$K(((-Yk@hJfqJi<PwySHomySp5zgLU`|2{BD{QENz zRPHkVXJbCgbc(@@;U%QL5!Yp8;!qG^Vq=nLWM%`cwgFW<pc9fXWLP0Gkr0iV3>*wh zY#dDK3@j|{u?+0&YM=-QpRUXdt#LRR85pz}e5AcM7zRW*=wi{p3{uL*2A=2C1DEIV zFij3xaP^F!@L)(rvIs>Vt50O4gCrw^p`MPmiju4}XqhKJ4+k5A8KW5&G*y{`QxIs^ ztP&f$pt2yOMpH(s@&v&N4m5AbbX4C?PTp2IGPX2F)RI4H9hbNmH?Nw#v7j<9w+yrn zl;&p)5jK~cvCz5V7cX}JH*ZZ@MI|%KzjI90UW=!gxWpQ;vwE|#>wBteOgfbRZwE8; znyK0S;5A9?|GAk(nIsr=7<NF;s*+G<Wa5wp#RDu3SiGbdnHfE$9Z;mekpPv7glNTx z32=tT8WWh2paCh=8L&nII9SzHl@#S=CB;F(3SKIy!>EHbSV4zDAe}8|t_aD$;NY}X zvy>FKQnGgri4;&4h^Xb&^RZCiXXRrza}s1!5KtDXZ?{VN%FpM=E0~a$nas@cnDN}o zb8+mBjLgr!JZEBQpPX1}0m_^I*;wSkcL!8E2r)3SGDtD9F!M1ovG6i7GJ!TDgHoD0 z0|x^uGY2bZQ7tnI1Ndeab~W%p2<o7s30#|Lfu>+p&{cvO)J%-Ypn(pk1{R-42R=1b zP(>hQqN-}h#V)JO2u~-76og1yir_q{C}_;o3{4%rsEO&{q^KyyyeP&dXo5gVJB+^n zHZz_2cZpFClwU*tb2ED}Nie8^`l6s?0EA>2nb-x878Zc(1zAQGG{H#7Ml~f)Ms^lP zCT4b~bWryb)Gh+&9nb*+pfj65+0#LtfrW{kC6tqqodK`PNC#;~21R-BUNz7%1YsdT zaDq@{RO7~$&p>NCK@;+#pcR|&AY&?0vy>FGQnq&vjSx^0jQAB1@hd`52{Q$}f6pXw z|2{nOL49QKo*dBKFx(7UAZN3I#!V6#pxcYoyg>~Xb=V#aP?QO>vrB5Tnkox|B1KWq zSdghQ^55(zCW){;dzh;Kwlbai+xG7ZXuJosZ^fM{lSzUBY&LwK3KJ7(&WT0C8+1gK zI(Q)<Hv>1^Z17eYsOe0Z;Q{|%M=&1s`uBGy<Jo`4OcMV<cXfl)tqaqBCeWEf77nI- zjEpRtjI5yZ23bI}si0YJcF_EIA_FrsvM0nD#D$dwg_Q+4*g>a_C_~nFfXXjpNKwVk zZY;=@8ENZc9cLXCWtDA_lF!H(wr3BcfR&}F$iHPwr~cg*k&KNvi|j|p9wsvfV}3?f z(DCf7P`eo!Vf8p{?=~bTK|7Pw1cj9lt^n1&phS+cONmJkydx<Bwn1s<PWW~t#2%%; z42%q6OtFlwm<}@VGDL3UVPs-tg|$efS(rcv#xvxzfgHvf3R*(K7z>&oWb~DGkOIpx zv|y2h>|lT%P%6aECaKLR$`0Pa4{A=Rn=-!A5RizL5RizH5Kw12C=g;~WMmW~z`)2L z^8YuJ9QaHwcLx{fQBaWTo{f*0k(Gss3AB2LiP?(*G@r`MR0rND16tje06I3g8FWUF zw3N6QFAsw}qdY6Gh&HGjCCF}SZVJN2qUNIPAS?)K>>7*kF<p|<;?K^`&JH_w?%cWP zEWI#g|2oj2S8l3~D*h^tDqE(xJG%S3J9zwi;$7$G+K^%enmYjP8DM(Gbc%tMffJPS zK?6)pp!$+2j*W$tnTe^H$%l=Nfq{*ajgy0&0lXrcomWg7l;S~~2aE+7L!uZh{#HkU z5_|w?2QwtiC;oR}hWOvi!B|#QkeP`EG^)qW2wJfO>Yp+)B!l;fG<kzsKFy#G3+R+F zK~Zs4Wp-XkZAL-xigw7!YNntK_@J}IltCL7K__zwGt0$=Now#3DhXw<Ftfb!e#*iY z)NqIS0munSaqO&~tgOY1=QRS<{!V4e@eBWF$#m-P8m16X*%$Ty8`C1DD-0S8(;!V& zVHsv7MpkJ?7DfR^W)^-%CT39Khc3#-3~m)ex;TssEX<6cGhSKY=Qn~@{)0{y=>|=c zD>GnG!NvyO{t8+G$<)mR=`pJ)E69O*YP{U6EDRcq8lXKSj7DPOkRzYiQ0oC@Q*&@L z6|@k`#LSE_Be_JP&pye*Brn`FMpH9UC)6t}%x;dbn^jCmq*Vpem2mIrOomYo_K6mP zVnN~(-YyQ#j!V=u(*yq*F`ff2Nc#Vuf${%erh2ARjQk8LAo>9G_GhM3;8og;47UHj zF)d)az@Q4+#R0m^N{Eqxk;#jZk;wzJlP#WsiK!K|qaQTk!raa5qo%5&!NMz{%_z>t z1Ujk;dRBrG8@nmEum&x>1$hj#6T*zq*V#AMQa`~nJvGi-QkO?SU)d+$)~PKmq08H& zn(2apLUc!xOMz2CL`(uJn;RRaeQAJigHQ0p*rKIbptQ!s!1ezdQw+EaR08eEkmqG* zW@ZHS?m>IcKzo+JSqi+#L`F(jh?9dsoKYN_rNBqgK=xUI_96)zGxIU2gEt(Tne#Dq zSy)-8JAO=DCLqnl!YRky6!%ig%`;M5Tv**e$uz`}%g)f)*NB^O#lKrJ=91FR;*7HY zghEqw3WcR4J>1#kL4`CEgBt?_qZ7FQuFT--;KTshmd_5F=7*kX0h&k#9n%gz(E@x3 z2}?7JkFXFUgPe@Cq?m}1vam7_7biOl==feiF3^!#%BJ8z1)o0w4o=V+d`4!bCJ2!R zQGP4$T%Vw@5cSXqQC<u0OcTpcKb0WH`C5i1k<R|kTCQ6E618;9LY)H~)txm#Vado~ z$jHe!f$1cJ0E4Q75(6{%Xn4?)Lde>7CLcxyeqJsPc2))fMgdR|GC~H+LF<@BMZ}Da zMHzkidn#))lh&2Ri86B5rl*z%ObHG*0_D5J{|-!z;CQlgu!glpnLvXWOw6oIsh}%r z89>DXXuSid_1Mhp!^i+?9fA+S<>p`m#TzJpK#pht4Xv1hYe-{3P<bT6#&}p#lTS!l z*u>)Bl&C1ioT!uo?5sX4Y*y}!_Ww3P8b5zMa#R2RXW;tJ&&&;O?}4_yu`^F$`1M~B zdZUaaG~FBi|HdTEbcI2Y!PCKwmywB4ScnhQk78z$V`O2LXJlYu@dE7;V`65hgKcQ) z0<AWLoCO9=aAG2Y0^D4n?O~8}-oUBeT;0?hl*!f9l-XHDLDe2O&6$IajPmU4x6iV_ z@W8#mP*E>JdpaXqa9T!UyivBHb_Q2|vU8X&YnyY0h?uLeQ1CKm7ds~p3GrYN&>is% zObp8ZzcF59I>R8uAj9D3U?<MV!Ys&$D0^AFK&y6`7+ILoKwBQbMLlS>H$yjrkFYQU zgRqRS3}_Bs7*q(0qZYzurl38k!lowR!=2fg883RfNT~Ai%kn9z{aeN8&%wgP!X_np zjOonZ>)~PStllhantJ+nj7R@@Ot&gk;nSA=_Xd3VhVuV!Oc_jP7{nM97_=ESL&oVu zq#0S5c^R2llvtP;In<e%7#Kl?ForNYBe)a>ZO_!=Vq;=qWn|)DVM=3QW^VH4X5`>t zh-c?yVqoY7RbntUXz)@Un__VPvKgWj)IaP74Pz=QDvFCUGAL>*YOAS=D~KxygZt_{ zpadw!C<Z!bA1MKX!US?&h6o!oJZxAYb7^)==8R6rVHDrp?ac!XC&uSInqo%m{JczD zD1qgz79PeZ2M@9mPSp@4E+KO(anL$=(78Jc5qEMjs4#&ptz!n=AI##zbd^DvL7qW_ zVd*v%Rwi~fSo=qjlaZZ+k%g6=C5?fNjWHh7?*|t)4Dp}}A9QFP<kS{b><Sqez$fRx z%6Dl8O(b>TLY9pUT&Z<|ssR+Wk&zDKpsh{{8VVYq6LuJc8HCl8A?Hw-vnw+ii=$Kt z>gM2l0PXXrt3eJ(+8mg~C>R?dHLopulDGHd?8Z4Venpj)g$0#WMd<--A{^`@Y=L*~ zFr9r}&>X!qKYwXVO;%U??Ah(@GiM&TD{Lbp=^%UuRK78R!io78(^>EsUdmP(@DLuP z5g^UL#=ywJ#t6$8pzZcd?4YCsN<Q*vvP?|Pkem)$ZV65{qKpg@;=)4wyr3e2m4!i! zQ4CgAsVjnfDlEqYDzpVbTgn-Yp?+g}lvks{`R@(bhg-!A+|8roq9*=Lc>?toS4x(| z9j3GYWG%c@lVcf=|1&D@XEcWT3AAB@fq}V}=`46`*w8^wjFA<zvx$ibyd9s3k(DV8 z)SLu0vKT=J<_ZZ3NeM}TIvEn6MwY4^6J&caqcYTuP-n3-$#IAXu&_vQt%?2jKRMvv zE3gBdp{@%2BW7zYBH=5?`1Y?EDB%CaK%I04a*iM~sGR3!5MhvGFn2H!Vq|2HWMpOm zxr&h;w6dEe4b-D*^5$S=f*yk^C(FkRTHXk9n;<8zq_!cWv9PhQF{3i*2w8S$cnL!s zY_7~Gm{@qRAokzy^!&@E`}F4NunTi=h<xS{6=7wU;!&E*bn4%JM#sNqjQ0NyGO^aw zf)l~NCt?l`qLKm1p!LF_v>?D-%XFGSmO%})W=)BajYWc&iJ1wsp$fE?5p;YY8v_$F z8>p}ZAECp{3@t1e7#P|aeB|Wh)a2A81qDS^lt8B|fY%d1TC8T~=Fkv@xLDYj5gxQT z%K35DT4IJ0W;T+dKL1_>fKvAcaCnPxuZ{g@62LCZ!7j=csF|3?AH>CBZf|n=4%6v> zhVVfD7Y`5a|Nj}}|9@k$X1c<p#+(6~<YK4{VPfFe$jG>X2~?3WGVn4mFlB?stxX&Z zg@yPz!RIwFfwm4YF|#r-r-IIL0-fgwKER2A0W>}>EF=nw3DBJz!r<MP#-iXOC(Vr+ z8I{2&IPiu<#l^`%PHkXR%lhjX#iZsR8tT6XdT2u~<P--rP`(52O@_Kb-9bf66kMW$ zX8yTB>mDHG5ws`*?JQv7mC#l-SBG^-&CQJ&!8a`_tFtq*C8r{ei%3t_Oq-cilvESg zfpTO-UyxDA#aV$6yJ7y91D(YnCksBrSb&*{5o|XLBNOQE5hlh|Hbzh#0?N<L3_fCD z?+c1>@XEkkqKp;}s4faY3Jx?^AqNY>VURHKV>-pa3EGPVI%b9iHV_OsAS;#uoM#~y zYzYd3?tu~(H3x6nW*6KY$!PU2Cz9#ZFXeyHevEUK83RCVEHUW0N4%i6I`}XV2Jj(4 zOibVcpDhk_LP#?k=wJ?Lw^k5*P$TFt5YT7}=++HI(L`_WsN7se3np#fpdjCWs-VN3 zz;z5WC(~61Q3e?Xa7&h*iHU)ei<Oy~gMm4fn-NsCFfcN9dGmk|4Q2qJvL+`By60F? zK~`B#SxQn&MqCDT=OTFUftOcOThP=Pyr}|w#3E>rPfbmoQHhORQB4hW=D(ttm>s)M zUuH_LKf9F`i(h|wcE2|ZqgO!@C*x&~!h+&L)_=OJ#f)BVNs|HtCMCP2&kPBjnZ{_- zR8rFPcVBmPHRy<y|NlXK80HnA`^^|YZE|*oA|_+-eG<0+zp=6*i5oG2&c6fg4QBwK z%_Phq!(iuN4H`0L;9yE+W@O+5H98nrQn?t}*&(fB(5Z6GtUjV53=9%tA~K>fph`|q zP*7D-kegRZ+uRg9^9CBOgdDLXs%$C@J5O975PI5DN=nq$XwZ>MrQS?jc`4TE4q@J< zrA!?Eo`GlGK=XjC3=B-w;4|V?7z(_>2j(j?urM+)vJ|j0g3iKWVPynu5MX171sxjI z1R9bApTeO6QN>h;Pg$gcH0X8@1vwcNSrsV>VIf}7>;xNwG@~@A%K<%+Nm&^*$PJ#D z0$sxey3X5J5Pmv3V?N~Qs3-w#b#`Vi7Iv=?ez311m`;KH^zRj;xTb=jGzXWQfK3Qv zhA-TE3{0SMidm5f)ZW%&xa7S-I3U7-i;EF-V+QC74d}`LP!V=U&|EHv!@|i3;zUM5 zbjWcqGP80rF|aW+q;s)@E>Q+AHtqsVA~dss<_tg`cnu^~;65y9L2{QjC{CGB)jO!6 zD`j9}f~W+K-@(p2j*N7W1)T|@rlO>!tfe3iIzv`Oh>wSz71Z<x-zA`=W(vMN0MZQ> z1^1UgbFz?>AqdVBpcaCO8lz57sI7n;FOQmdhnAiaG>JeGNCcyJTyS9k8;d&|$CMHa zBgPDP$TFrugOiED_J0V|1*WSEG7K7^lhDOPm>8I)CB#J;SwUMeWf(!1?t$(uV_^Xg zT6cl^Td<CRimJ4l6DzNzHmkY161<~sY$OKiX|ltLJ!Yhq8e>7485=KK{$vl2$pzVy zyff2vTg7wz8#St5!`f+u8V-yx%kp!V#zZg8Z{De-Z)&9-tZ&F91Z}4=fZS}(e1PdH zg9d{MsPDqe$jmA!Atubi%EZj%B@T8nGvo?8R#5MmjSW1k2);Q3)-+X7RdW>=lw#+V z)@Br8V;5J2oYQLxUfRdbF0QQ3u8!_0RwR!wUC2z=XXMaTR#vldGqwB|-l$S2lkI}+ zx4&m#USdpZ-l=4`O4d?QQPa%L)Y?mBakQ4Fj;lYyqcG2b$Cz~)7?^CCKxflyI;gQT zGBGoH!LCkX0Nr)K06IVhQsyfw3W_O!qCgn3j0Q9#2kP^K8pg`Hr@LDGoKK`}o?Edh zHZq6lR9c#6y!gL^j2hnUUZ7z;21W);1_tncK_yV^3-E*X$1pO28(=K#jG!$Ysi08@ z=y~s~tl$ajW(FTYK|wWD4M71=wWp|Ts)+rdL{UY^jS)=3q5rB^>$qxITd4V{8tNoD zMrXTw^e{4oCMS29&Qy1HQr9w;Q?ORm3h=kjv@2(xFu@x%_V@ogQv}mx1`&n`$Yv8R z5kWpS&}sCHUeXRQ4kIII4jQy+26SZzC}^Mqh0+e7tskI6O~73ba7!HAh>YAWBqS&) z017rnV`gzN=+QaM%IfUO=FG;zj6Rl=cmzZ_*u?}E#Q%@7urLc+9aqV8`JbPnj<2$^ zpDyExzh-lnePg`)Zw06><^M0h+|6`~K?ihaqnfM`FAEbh8zZQ_02;((Wnluf7Z7K= zL)wIr5`r=+N^GE0yg{o=;jKZ?dUiH8(D63#vwXk|@KHES#}umKEHpq(LPt@dCRQFU zW)=bV*0_C91w5i`tP*_1Q4L%otW4ZItQAq*<$0i1p_zjP;}v0JeF1SFsej^3r~U;P z#HlG~nlLW;XQ~n|FKB8e%y<nv#>>ILz@))+ia~@ymZ28X(bHsOWC86>Vgc3L91M(1 z9N=@Yn?N^ZFvNl;aA0S>NIR$_)Pf{9IT$#>s~wOufyap>9puFzCppQA$wK<fkUKTl zSs6qaML_*#NF|~SJKh~~?FjS)e`P^N&BnT}=7gxo$f(?0|JanU=y)cL%#_rk7N%%N zJv~R`e~at`T^;|uVqjuG_LHrH6$2XsCkGp6Dgy@>=saL{(A|fitB63W;8|H=H6a6o zthBf&g9wALBB*gID9Ft#t!)mv_XISf1gfS*A=yn?9a0V}EAP(D1-T~=>@tX(c9=Ul zI5;wS=4B<P7P;F7xi~Q}GU)ynV2TE}u~i%t#rV0oKsSeij>kj{M1s2v(xO7lyyDv8 z;-bo?=7Q>`#-i+?6ULyuuxtK5{QZCUv$3%7s)^r8{a3wI%+yqjSp`J?TWy#m&!`Zr z&A9ciHn=>|`Tvda8+aVs!NC@E*9S8r6EkSOl7|sAc%sb6!sv-qz<?*i!M8iW3VKx) zMHLlIP+A2QmXK3x;a7NpiyFu<BY3wE_{=~t#^?TS0U_OGRsLEjIzdTkNg<&rp*}Xo zAzCW>1upSrOv>(Ie9U~2ZmGGNfnt)bZVrw<PPS%N3JMOY>T#g<I1>Zw|8I=1nXWL% zFsOstRw9fnOv2#aA^6%TMkW>prgR2&c38#4!U8MZ<z!WqWYy)=g;kYRlm$SSMhP2( zS|H%kU0E4&AP6+anVKrAf(ArEZWk4m=no6<2#X1^iVhXl<q=R6G}P~Bl-Jf%lULGY z{AA*l;^*KK^fo<?jn$8Z)x!C?myVi@rjfZGIGj!Xe`AsZw@d6CtT`DO8MqjkKyz%2 z3{0$yOzEK0y}&L6*F@ktshh=zkpXn)fv7O71pwLs#|UmWfesgkoFxbve1=x5paY^z zP1G1wyuDqV<&8u{tuzzzEVI453LV#OZLRZR>WK1o_u=RE;N>q#_sF$#>B_u%$$R1E zqDDxV^8EkCbcX3FgCK((Lm*^bgaoKf1HNjH0o1u;W(EgnH)wiV47{rkeEcb>*8`J? zbPxrNI!lTR$qCB|@N=>=2r>$S9S%A>98{cvR|T6Zn=-<K7IYOwN6rKf&xv`t6FqHg zS20#r<`oxL6dI;8U1f|}l9#tMHg;KF%v629zXfwzJEzZWj%WleP-0|=`ESU0jOhx4 zC_@Eg2!;!E-kK0NaHSo%m>5|ZAROpgUn$7>^Gpn&gCIdyFfhckf)AYoc}Wgc5}fHl zlhL5q>0<N&Z*m7Mxd9bP>?{nTjG|zV8NqMF0`+{9A-6g)Hml0<@~R3%WxRP4=_0@? znZb00HIRuZH9O7gpFZ=<G(&q(d))H>Hzp~j(+u(qIt+Ra+G_kvER3LqJdBK>qhy$x zz-t9zc}-D4K}S(XQBYQmjaNdO5geefGwmQJgPDUbeNh6<m@tEbR~<Zk!WeHIqElb& zmSbxjV`Q!^Cfvlhd43|lHmAF<y`4^gQ3&I0Mb`-1+**?)SN9lG9%VC4Nfll%FB?%g zxqsflzV<$<+*)4N?*8EZj^+RVOj1l&82LGvnSz;4q0aUE|IZ-Fz`(c~JQt+T!~>IO z+yS2piurHN%*AwtL5?BLfm<BZm;`NtVuBt8D9ymk$lL-yfdJBtjb~tF1gAQAh%9&{ zg^3AP9DxoRho}JEoGvJ=s-g%gkyJ$>`2=)Aiy-*;0a0dX^nl7}rV3vXMFB-qA20ua z%ml_rS3Wk`T;J02`LXP5Ui{I%j3#kWm05qMF!#q9nbm>L#6YAsL538h^u~usZ_Er3 z4m7<9F)%TL+MA3F@ysls!wo^{Obj9juKGdg3pBh2PG9_t4A8VC$S4S&8iA)R(3G~g zv6&enU7@C`zxB{G#mE3U&m@TH6oVw_W=oK3_!*g*n7mk7Kv@HHf+M&t0~a^l%%EPp zB!i@?sv`KzNYItbkYoKpQzM|c1ZV;?H)oRWk81U-F4GCs)`&7spWvP8$5i9z=i44$ z-^b7A%g-M=GoxUJ2B<87j7xyV?>HIc9b}+e3ZS<pL5`{fRi5BxmLh2V19<j@sWIxG z6==$Z(T}kPyvT%+A@RRC({(1$dPy}0Wzd`hqZjD(e-_q6P)n}K8?@LB+~yP)6cYf& zxuP;7Xay%IxEPJ~m`p*JFbOI%H3a=@Vqszn&S0zsEhG(b2xkgZ&M^O1$aL!OMga$J zMaId08^Lo6jQ{`t|Hc%_bcI14G}Fex$OKyA#U#PV$|A|g#>(Qw3?B4iVP#+ewSU3o z9=Oy7pA^vz9u|>jkXKa_6jD(Z1Py*ML&hhdr3C0sEAWCZXyB?d+Oq{u$t(;}Q4BQ5 zZsI(5mb0o*FGN!#)OyxpE=Fsg<Y^&VKKv5x<vv9%0z7{FeCww{!kF>@H>UmIaR(O% zM_EQz7By8BIZ<IYCRPz}kC%awkCB;y6|}q*bkG6l(h_jZ0v=(9MZc=LvY@akDCC5} zC*^^1jH$Azq97ZavI1zW0C;Ex=5|%lxr~fW&Dzeg+Zg#x%iR90vT^2%nU-OdrLUkF zW02A&#w5zv!70euJ%gL;xZl66qGF7VymovcO`hVS-XdZd5e2s3`j8nqo+!$o?w}$n zB*@3h!_CUX$iU46SzX8g8g2$R357*Pg+XI^!lveepp_+{fjm`FcIXHqa~$;4*XNAV zKNuN7Lx_yRcA|_a{~!myp7ykL_%|OsjL4`7+NH?A!1NK^f0ANQ0*!AeDT<4U3JXEj zQL?fyG6-UEfv^zR1?Gar!XQVOi?Rzs1~A#pMa@AghQUeAl$|*)<K)JWsHn`VOM~M! zHny@bu`x~prMF&2e*wnqe<$P^rT%^4x8wiU!009Y?;@k3K%J84dF70MTR~~iS^_+` z1qz!?a9dl7L7u_h!A4G2OjJ;SA2cbz&B_A0+W<5Q&dR{Zz{;2kx*`fP^9vddZ)Wh3 zk(QR1k=Icb6chlB&nrT1p98P>0NsZv%Ff2duCA)4rmQS%WX>we6ra9&QdC^r&ax%} zcSk07=ZB13Jkef}X$v_2ePopV_n2`5uPxslKR+%zPM1W{BWfm^|6WzAfo^YLU}Rwa z|BXq3=?a5B=)N*;Mn(pGJtfd_C_Ib|T+ED$pq-Rn44^U&a@z}NeFFm%Q#X^3s;;U! zBv8Ta0nlPvXd)985ffJxWe1NGpe$WbQ&&@GXYNqfuu5~WO45{3jnH#;F>_`VSnJ^2 zRce!_q>^pvA0+4Hz|D1sF+ono*W4mVM@-09K+IC#z+BqePE9V*B|b|?AWT5Y&Q#ez zLxP3HfB`bL{TW<d#z02IK-bx@F|!na=4%+(K$~EhSz|$)!a!#dGk^}-m3B~KU}0us zW?`$tttirgTToC{R8bJt7Y0u>Lndkw17D!Ec%P&GO^Q;7k9ReI7T|%F+SO|+2*_}7 z$q8DAfXc)F{~6T&e*@hO%f!aG0o*UM{V%}O3_jDxn4!jjUzU%F6|_*63A7v*cC3#A z2O}#3XdWk>0k)7H+JpxURJVhsrc@A0z*Q?q9qb-A$l{cC(6pwcstRbCw2HB+v4Xs` zq_7|d8-p^VGU!+X<mpVv>@Kw80LQGcIcUa;8FX(9s80ZH!-_FR*jJic*Vv_|GcmD9 zX9UI9*t*t+`lZUU^7wVmwDi*x$%s!%(G33}A|56sm0ZFnW+bfd6fq+)Z)SkCe!}0( z36mMKG@OF9EJ`c-YP?FF8JPb6Vqjo$2j_1&hO(_ZjO<LH2~5!9L^UoB&`d8Q8w+DP zc&3+uogF;k2s#p<jg0}+u>j3tI;b!(L2jl+*8tjw08<v}AP$-?7m*W@l@y1J)e5OV z#%e(^4r+0jm>C<1gFA%EqRen#8#B58OEIytcQRIyh)Pe7l9iQ`R+01dWi+<eGqn)C z^l!e|Ge#L<BQ-g3Nm&MPdkYlNT;TSU9#b}Yd+Psx25trhCMB@EIg<{$JOk7JrT=Xh znZV_VJVUsHh!7(Sqcl4cGcyZlWd^esD<gxu_XY*f$s(YMH)inhPvBX7aBb1e;v?-K z3SWxE4BCkZTFueK<P*7FSy@O$6Es+2F2WAFWDK&00^FYx7c_<p@0o+TPsi9bq%HVG zM4a?3Tpi7{)S~W7O3P}pD=Nv$bF0W3x{GKkDXQ6+nA!+4?fLUeOh!{(Qjvjy@&D)l z0!%gFJM!8fOS^@Y7+Dz<rNsF;SlO6ZL5od6n;t=;jA)|JwcrvApksGoCrg8_qyiPl z?VvNQWFQOXK=R-wK4`ETvbCU{8PeL9mz9(d7Ubh$X9Hcb2WssLgO0}&2W>wBO~smv zg2y)1)tQwUnZe7dKm)TWO4_pgnOhUy^$JQ#3JFOoXlNLyNTfYaV3ey8l#=3<6yR52 zx+-p<{)aLCpToZm8d?HuPCQB;*1!HGG5%vz)6!#Mb7tcNg>>A10VXf-x*sV9Wd>^p z3wH2yGXpbdT_iIbsPqSqyMbqBz*7m$AYZgI`AADkD@!XWsVE9+D}W{zVWWW1F1oU) zvM_9b0)Bf#+~ux1F9)x{-jaz?QB7e!;bG-HVL@ThOsAqF9Rj&oc>H}c(mpVC*jQUz z{5${ehr6?_BP5O5{{O~&l<5i+8>a#!&Hh(ms$)9Kqz1apnt`1$fZ-%qJn_E@;|Hcw zOll0Eh9XFOBUoJHzY3!kSX_sJfq{)NKp$dm)PEJG2C%pV=)_>g0EUlX^@;xlKzDO6 zsWE^QvU4wEG>3_U&jSP9h{eDF5^n*wH>?;K7^}hSXZ0O)KntnC*APMPJpivfV~qtZ z>1<{NHHf)ki)eWz;g=DC+C)|_Zrysp=;*P=eX7Ub7ofEiplWa}lLQkRhZe~G|0n+o zFhKY^AU=cH|8GocOh1^|7#l#@n8Eb_H>TxG$_%OuIt&*fDVj@`k%f(ek%0v~8v*68 zf_KyL1VlJ!a5Aznv#_ugFmNz1vvDwkCyl@bS1+V<m;h=6cQb&dB}73-34qlzv$50> z)Zk!@Pc3LXIUZ~_XmW}HW({aHhPoO9gO;Y6j=B!${%sW{VO13s&@`1QXmlS`XM_9s z(4h(V78!7>*VF_yH^(%S&)!;ATR_!QzpB$J$0xJsUms&ckfEPGi=vc_3h%$u_Wo>) zl8g>3rId_Z1qB2ICp3lCyS3$;#3YJ}h{-7_NXjsp1qZsv{s-Nu{*7@jlM)jf;{|Yh zD}vj`N(>eZ(GC&jEKKYSW<uP|ER5`8JWNb%N{lS5ij2%GtX`lkt)KxI1~vv(7B<#Y z(7v;H(9BmaXlVwlkqo-&7jzepnu@%v03Rnig9W1nsL}&nJ83Qs_LsRh*w5;q)|Du% zXAT=jf-I>6o!1T8QKoM8$R|x8U65B!R76WyNWxZL(?L{~-9%l%OfkRADo$Q0*4!p| zriYblu)Vd8vxa;Klai5vw2h7uKaZ1un1vgsiVimmr-G@5bF`44zkrmcQIv<Ou9}*l zpquLd{|v_ezcKy=g#`;}K^_x`&n&>C#GuTe1G-V3n~{-8Q&B-uNRW$#gPBoFikTUF z*$OiQ?3Qq7>H@WRAP1IrGx&gNeia>69nhQ<KQE--hptUA1vMt&D>KE>14M)kd|w=^ zm>8o#Mk!+^Ux+h!|C4!Kvse~95Ns4Q97UCiD)p`8xc<FkwdZOrO>%Gr?|_Pnl7aYL zP}JO=qkD;mnysgjmo@mDXlVXpU<RGx3_fNZ+PwsypKGivXuLKef=L3L7p4EJFdSf# zU{V9+RZto88QhP8<yi)P25koo(78RJ#uJklWJxL`sOQcI9(QGA0I%QTXXFQ+FAiEu z4c)l|UXCLsrN+yzAgrJj;qif;)t!}9*^nvw?+kB{3mF+$K$RKzjw3(F)h&{2EKE$C zpnGAMz$XYZw}5Zy0k=Rf<RFu=%?zOJ9>PMPt}_S=aDi?#Qij~b1K!5~8Glk<8xa^7 zu__`UAYv7hhi7_vdb;P|9ZVje`3R6ZvY7rdh%>l22!f7(5El~`5)|NM<NysvdT)>o zh;R^qHP~A~RS9_Z7ko(~69XgYhTmo;pUAD^sw$u{Xkl}3T@R}ILAAW7sJS?3!38@@ zU}Av0p@_JEmWZZ)mO@vA5#uD?ru0y54p&w#6Z;@*3!i^cjAb(5yvobKzzA}aC_|V7 z7ds;pXl*MKXgCPu7%65(1~x{N-gXP9JZc7Q0+j$|Uj{~|bR<RK1t^gYT!MmvLaIsv zplLi|WAFkoSpNbVO1vJ72?$3{niS!|<eU})aiOipKUT)y49pCmu@rSCB?evwWd?l) z0|#9h0VY-!@T40gw=gk*M*w<3Zi0=|si<gbse<Qk7{LqBLCZCbjl{&*ML-whKn9{j z#KcV?VaE;=Fg7w{Qg1JI&v$4o@hGtGVU(2g2@3KO6SpzccQ99wkWi47mXv4YZJp#; z8`(O+vHC41hg67pd`?chNsu%fn~}M>k(8XAl$4wtD82u`^Iw3Wj4_JQjlq{;f#xi= zG|<V}OgsKB04?bO?=Seq=*9GxL5^V}WGqWSf{~d;oQsJWG+D~Y$lxyRfFuMOwT7Py zAO^bZ3bfG{oJJVH^#myGLQ;?vk}P=PA2gJ}LwFJlET9#y%%J<<z>O7f0CqF_L`H5` zRaI4ijtZ$OLkd)LaPbN~K9n6YgCNT2<;ue<?PtNK#LvYm&+ihdq9>vx$j@gX84ws} zXwJ+W%*>J$z!+|5#Kz*n#_1ag%6_1_`3I8(gE)h&gB2p5BQ6_bU}1r5+y?JQ*8=UI zz*suW4%tNk%lYsHJ%S=^@Dc!gofy0nVDxssdDH#h4)0I~rvD573owA(Aiy9EnpI_H zWCSH{&@KQbCJpeOWk`2Ri@`@okdZ-LR8U$-8Z;)z$pKj~%?WV;DCro3#<oP2LHBW* znL_uRYS?%?cxp#@do!w9Yl+LKNhk|{a56D>W|H{(!o$f^)<Z>GP((&VhT;GJ|92Sr z848#Z8QnQ_7#JAkVJU=h2ctYVg@Dw6!VI}RWBz{uLjl7hMmG*EM4Dnmq^Z#VR*cq6 z5)8tibyA#+jLgt^4$#Ot12gDaQYJ>G6jnxXNHa65gU26*8H9yZl?B<@CA5VFp{J%o zN5@P}l?B)8NK5iavvVm68#<UWN&GWxSC(aCc4cF;^|A*Q?VxgvjZvOqAyXm)H)!=f zcwa>*8)(j-k%5sJe0d$HZOp<5x@Z#KxMP>p78V5`@(*5>wb0Pg(9n|cp4QZ<TFuS> z|NlS4&<`?`(VfEtIeeH<!^eZs0#>ImvNHz$U-;)0WDWSASD;Y*|DPe?e*nV{m>LFl z?!}Dj!EHg%ntny55C%a8B?e~)2k10gI_BDbR#t2)`jv#0craJ=gF2d^S$<GUj16N& ze}{@}vZ;Zkt-rsVvzwTJjzN;Lnvah&W2k0;l9H3VrIMw}ztvhw8V*iQR>q)fBD5K1 zGA1yoGYF!u<p+((_cHk~G6?W-b8@hOW_eh7MYL5RSL}cfB?m7~5oOd%C^9Uoa&=st z9m+S;G|D0>*QeRr4KytV+GG4*fJu_+2ZItrp##R5#Gp&N6u|d%F+j)qdqHbVpz9|= zT{iIGg#wZ+3k!IcH>wiw#0_NcEqG6$5~C8RD~B{>A!q^`mH`bKgU+gjR2SgWiMvHL z_ylAH<W<YU-6JBH8Chi_Y$Fq0Kd`ZRu&}A?F)nIPN(5bMby3oaOHVUC^`Eb6C@4J& z{O4z8WtzgE%aHDnBFxLg#G=W_3L2H+039<X$H>ko%gDyg=EcCw!raWj3fk()k<Q5o z-O$(S4VooqOaRSkfQ~o-4=uN|`7knQYiXz{Dac5H&WaG=2e(Ib8FfKRgH(;oV0i|5 z@t7ib5KWAYO&xSGJZL;0k~qv21w|NHB(#KuP37JFeF79rm~7s4RWRC@1?L3n#p*^y zBo*2``)6#=&g#O><sHpvuBBL3ZS$MGJjE@~mQguSOeDe6GtlYFzq5%Tzv}%LU=m<b zW{?NX#Db=Bc(~a?!%d9hjLe{8RG3-37?>DA6U(3-YT!x|9FO4Pg?1Jn6%}PsVbCCg zD$X4Zknt2owflFyvkc|+gUqKea=4Wy#l#wA8-zABceET`>RcwN;3O>(eAp$x$;Dq( z++VXKH8mGB$0qz=fbjs6BzQlAorARmBl>;@&^CrvP<x*#4t*&G1A~Y#bTb1RFV@Wr z%%JIGb7eL*cE$r9K4Mz@f(n94n*UZZ2L1XES*6b;`FEmgC^W?c+A-e#r@t7qnL$eq z)c#~<5dJT~6vre9-OF$fvIb2=2I@>D4kktpb#~}p1_@MQM$lFUc1Cva5hS3+Zdwe? z%&p+v#7uFVtW2PVEug~|AeBlxWS0VL)DO}S)xfTUJ(hu;9jXDcDn;5sjf;_kft`T^ zd^I7OEwDBtXq+(8K|)cH0lND^LYx6~R)vtTsxmjPq_!g347;K!qp_f|s45~95!)UP zxcP#@Q&B)k0~DUoo&S`eLCUBO+xOt*8H*CK9v;xJ{TIur8m_`6VrdP!I>w)Yfw7KB zl7SO+6AlA2s0jj@z5y+e1^55Kvq*x1;MRw+vY?_U_$tfUK8*U0uU=)6bl71JO6wZ` ze=s&MDKV&nW@{xGnVF>6m>8KsE4@M6Fqxq}Cs<&B`c2x9yGK+(EkSsHP)!|jj0AN5 z4rDx3T#kvcAtNl#M#)dx*w)2d#a=DZU(;JvO4(Lk$6Ur#MblBW-@{+pgI`2PLrY7H zPt{u2#aB?kmY-isRaQxoUr^l=G-jmt{~NOflQM%5gDryx!xRTm1AZnZ27NvzMkW?U zPDXhNCN56Uc687pMpbr3Ms7w<E=JB&4o1+)=FGfI3=F;AJd9jiEb-vomCY<Z(hiz9 z)Pb%$1uKS}q~6Wy6X~GH$l&JUU}tS<Y79DO7If@^xR|gY4;MQtgAt<<AFqfuXc3R7 znYlTrKvH6Zwd$bvNGhqRgIDpInt&8Qddz0R?6B!6Q&j|$Q8ppjE;HK76neR2QXJ@F zNk3=v5Fu$}JDq@-pGgI(zUp#1adZ4bVodei!Wbj{Q+&;}ouiqQV-sCvnVA)wO(X@J zgr&pcz}HOb>M3dn1^F#zW0tQK6Lu06|M!HM*~nT)C(zS?nZ?3bSIgTEbZ*rDZ_JfU z$_$DOx(pVe{qLa7MEvrMtPFBYOf0N?jBFgBaY?ucJ46I}P>>P>3j+rm3r8xXi_ORd z4s%XM&=wfbye4@2r!GtpWB?PQ20T82O+9%3yP1iBo{pB9ij0&5IKWw%;R{EN#6bh` zpjH8Bd<N3CMh#%3C5-IIAzY{9ROwM*-(KvRtH3Ce<`xkL57ZytL4iJ?K1vKUbeWX> z{T!<!TPN9BS=ph4;;Z=l{P_5soK#q7g33N722fh{VNzz$U@&Gl3mFgNS7T&nP-SFe z7vpDQgoL6RBRdn42y|puiGhuQgOQB`GHAmD2_VqXIn43kg>|6&Eg>BnHMmOfHdA(X zhImi~+v|;{+CdAKLI%*b4UlTcy$IbbKA>CL85s=qwKbI$<z+$JZ$WdI9BiNsxS;wN zoWvnv3c4l})|~-&YET0nIj|FwEp5ZyBiwjdrJ`*e!bOzgZLlO_P}-g4;hdDfDE9Y- zhn9wsv8e${Xv5PisQv?mu?UkgXugwSA2f`G7@3#_85x<R8JR#Aq)I#R3o$aWAc;W3 zSb>3&fsLJ!4K&rw3_gRY7t|nOi05F1Zfk;sunJrq=u%ABx=?hr4jMR=alkI(2Dc7C zd!fMhH-m29W>8{Kl2TPtRsn73QG^B~N&te^azJ+bn+qE=G9v}&LSYuQ0DIpkc!D)d zNM>f{lH^^S`0r;cG|dL+Ya@qUY~pGQV_9oy#<0IH7}*i2l^L8?LE#A6>to1p6D1^B znB*9lI3cN3h>?j^kP$8d4M`0K76x_>7WPyIMg|T}Mh-}bf|?$^;1!ecT%dJGka1j4 z$*hB+o|Ux~G#~>WltkC#U_d}Q7bAG*I437k2#Yc>=<8~#t0*(bGRO)Fg8~^eii<mt zjm?c2)j@ll&6Pz(Ktr9NCccrGDI>OkE{RY4_tO9r+M1COxWZcG_}>@)vewd}u@<d} zum+V2e9WM*)@QI{SPltkailcM!LG%~#-Pc_$_73S2r2C%%fLgPk%66qksT7^pxGbr zWy+9%0Ilf<FC%IP4Jd(*Sb``;OU9s0p$y<Hq0qx<+CeoPkBYL23N&%bYO@kfoZxy5 zdZHUrjfOS+n3*|*c~>X?+b^hPZy>BBAgZLLDar~BNYPX*saDKDnla;Vsj{Uq8;dum zjuvA$B1oZQ<SW7B!k~-67#TPj7?^mOelSQgq&sj53G%Ul76?J_4VK|x2aR)qPO$-< z3<ti4x)pZJg0zDYsx%Y$v_EjC3$7^AL4bh)bl{qxprEjj6nG^JqbO*BCA+a5lexJu zyRfo4=z3hp5DH`RcEpvqFS2&ohWq%0GtG1SBw!-UAt%qZ_36K}pi6RN9iyT^{YJRk z{Tu{9hg^t?2y=t?V}U1pK|M!721Z8k=oH9F&{=zNP|GY9yh{v}mEbaw+r*><ML_Ej z%+0}$0!>%L+zH#Q@Z8lI<>Fq?4E3z4tU%XHvuM+!Oh5h=!=0KCXc)9G*OUPf#>x!o z4g$(b@^Z2=(!yYOf_K(}+$jy&KE}obUTDw)N|3GIpf);aJk3E7MV0}43wkq(nn(w3 zG4OUw$WAk;2Oyye+HohshSeiJKH=Muf*jQ|pio8%dW3&Kom*C>9}Mga)()I(tW1n7 zpv`xXo&*nQ4_*s+`z~lr0f@`M*bEtuk96Py&EGMw3xjW|6Ep{1WhuBeA|it6$G;<r z|CAYf6d8L#Z30FHeg+1HPfR};xEUNF28%+%06JxdumM#ZJP!&UBj*H-jthd9gBinz z$3>ObM!2~}xVkdMIYdQ)Mq3#Ee`6|PQeqHdkYrE*t@Xmb7Y)=H2Hl|m*^4GEB_hng zASWxOAgv%SCM+oe>Q@RW2?{9*g7%`BnnDH-#EnHEZ7el)bCj)U9$d{?DQ(;i_H1pr zF4;~j-#oq88O_*zJbZju{+(d)`QlVmRaN9v)nZ)~_$1WVH}qe6Vn_(+_(t&D;XWp1 z25AO01|x=9-Wxz08@RX_Sy;uuOP{4dTb<aUoJfeY66iQqW;Pb)bWl?hvTeK98&nxE zfTy`Y^^JoLToD5+D@+Y|^)x6((UeCz$ZM#Bc6sUPs2gb*DJjUP$*O_YD06`Kd4ZZf ztk6LqBhc;%Q0*)NYW1jt8lP;!MvykYsE8P3g(_&^MMR8I%Ol^ep~5^=p2tkn%2CEl zO-zv4D(|GVytIt0?4JN{p8#J!KQ5Qru!d>M^0KP#=8BdoY@%ZRl`f1~PO{1>GP0`w z9u&mJ=Vm6y<wNF<&HsO6-p8cGpvDjcPJ2O&)4}6zO#cO#449M{WEr#>8ny|sGI4T% z7Kwl=U3C^l2GC)PYz(ZR{Rte9z-R?k(BNvf8<g|f*?goOw2@ST=bgA2IXGb5KTb{* z)sYSo@^TCeYASNt^4j3KR#03;MNp83S6UmqLIkw16B16~#<`f7s3<7sDw_&J4xls! zozP>V#uN-Wh%&jdG6iw~<>@??J~;Mt}~YoZ!fm?x}3h)!o^r2ReSz+A$%4DI*4S z_$2rkN>E#n^}hhqWF}<>O$IZD3l0KmjI8W>%1j)r0$fat%%BBBTmcadI-tFNp!H9z z49twI%;{W=pcXS5=u8A~%?TN>2Nj^8bq3xWKq03K(!&V4FN!El4u<IJ8Q9oB!(Jep zK*uOxS_Il12Rf$F*ig$%+e}qONnT1)M35hJ-Iyk$CMey3n%v-|rlt-GLS;eliVV<t zGG*{0Gh-t$=%JsW8<a&s3pmV7O&Is27MW#u8T!gGF-C9+b0{jYvG}mCyZUenaeDHy zN(N~LyYT2+%XxV)Dfe~;*SSgYtLcj^VN_*gRF>zL;^USRvh`+UWSr%tuIm*aw~1YZ zKR5m?0}})1e*tEFCS?X~1`CEZNGJ($GcqvhN{Vr@urX+<GqJFNI*A}bCR9P_>S0+> zHNY6p%*4dV*b7Q};32ba&|DL^xuOW3ql7AA#HJ?Ffm_8?$W%o_RSmQsOI42vegB|1 zEKDE?5VW2}T~ygrO;{8>@d7#zMv0A4)6q*7x|^`w*pY?RlaEC**gCbyu_akVPdq}p zSkge)Gg6~J*hNIjCCM{4fQ`kCm1A;cK)5&~6XRhob=}_9(0X?!rdQwD82PU;Gn>WR zoQMaN_n<qT1(;I6`B96ZVVf`;69*@@{K&w`3Aq)%*Bg}AV2O~84KqK2#=yYJ8JL)` zs7B;REqN_SeiRf@QHSM6=m{K<F-K6E0j*wx<VSTQGw7IwsEF8~eN4=3;(-o{mDNf1 z0TL{{Zb3m2GlJY0UBW%w-1;3{gbW0g47$6zdJU94|ITzsOgtAICzO_17$2Vv%8Taz zd6+&hflgL6W9W5|6k%jx2j4oR!2;TP%E-jZz~aTk$m$MBI;x;CD^Nj>R4{`^44}<e zP!9^6CN$ydpnY9%K!LitY;34%BOS!FwHO$T3_$sbL6bpKP+UYsiwm@A)0AD^9KMPV zG}mM#CJqV(bI4vub?|N(Hg<MF&<1OGfzG(=pxO&2Ce$!VQkT`yV07>mR+nx601KQC zyY{I*=9A`z2hYEt1XWW7RZUYjOBG{%nbv>r8TAo?#mJz*z`%HsNs>VXbQUT0eUGi6 z(amN?9~Ja{kK)SepcxVH@(X2!{Ylw2Mw648OOh=dY^_q5Bm;b{yak`#bTra4fb5A< z|6j^v#-zlc06Hm|54<0W6|^mbg&B0t5CbbiBBFl_x{ro2mYos2%O14zQAHiH+LIN$ z^AQqUO0WSg5jJ*qQRo(V#>>I~mUkGt>8aY-7$^r>`$V|froRYDO78Bq>oy5DQq?xL zl2Z?GwoA0mWbW<t1mzV*28REO8UHZVGl+uL_VaSFvoNxPHjIEa6f}co)tW)gFUDBV zd4R3pvj#;OM5Tm;gg_fcjG=oNg^iiT#6VXtnhP5<pO&>U4VoAiTVXMUTa<-`SwM)R zDDI5k>Tito|Nb*({PUD|6BRHv6=Ssd`vSC9_P+qrZzf3wW6<oSu965d6Ehz>6C)EF zXrc|Y#Q>c3K*P?UmNqDZw=?({8)<5Q8onyZY`kLHte{R1bR^W=oLP@qU73#=zNrz^ zHxLHzY6SH&g^igKrK~)SR1|pS1@ugXxuRHjc(|CDnYnm*SR!I)M)~jxu`;vq@i50m zNAQX=F*6JEdquECr)hF}Fta!~Dl@9_=@@EqacUar@&CKeB>C@|VZ4mIoe?*q&cEwA zQR2c$mW&dhy@230!4C#S20ezdZPIMeHUX%8t_hhSW@lyst+s3NX5ippk7r<Khjzgs zy+TIDcF@iaP0&1NGm-|JY9k%^bhTB~!7Wt@CD2L^$XWq)WjQ9$O_8YWckucJ$U*_o zfz+Ut0tS`I_5l)XjLb|CzP=GVb}%urNccG<S4O*e1-mdhMtHe0@`4%y912QY519Sz zqoeKJ_;h)d40<|JQ)6O;le4qY{9U(Anh(j}T3qZ*%uJwo1y7)YJr3?EgFOzOk^-NQ z1opTV0}CrlGqMg24jhUj9Yh!ybhXt~7!(;4p&<a;M9B)9iv>*qv#~?k(a_lfaG)@P z7fL|=&zQOc<Zqa-;eo-Z0r!8jU3j=1bMOO*uRCFZ;mw!{4*>A`lhFSHOdL#-47?07 z4w4+8#etB6-@wxX?F>E=f`Xv6F5;l$LLnP1&4t0sidb0q<WHrsykX}2cU-|jg3-W` z(UXCZLGZr-<7Fmg1~t&yeO^W;W*$aHCUHgvM)1;AMiy4^vNPD38sL^9c=-spatF<a z>8L0xf;TX+LKcpJ3s`6w0%}CFiy|$KH#Rb3tT^WDv%||W#8*Kl&CWc^%*n|su)W4A zMl&WlG@5IgdzX!axud+IW{9Ipj36t2oKJ40s7Qc5s9DUw^#8+u0mf5I_rU#%T+j*} zNOlITzyaN7ro_O`4t81>^twR~HYRW`2Q7_)O${l41X&rG7@5KQE}`m?%791*L0K7P zMNnr!MOjc-MG3U^QXDiX0=gm=v=SA(jtR7A8stS~WpnWUHBj$B^r3f{umP{4qMl|& zMX0}TKuoZexsI-?f|91EUnCo=JExHa<4<>euOuG_=U{hDHCZ)73((Rx21W+c{{l?n z;Bk8V`z}!;fRO=oj4Y^6137sCG+B(;cL`bO2V2}I!Uo!Q394Hd6+ArM9Od+d8AUDB z6Y?yxyb4=ZZwxQ8@MpZ49uXYE&+X04S5*>N>9lnI`v)Grt9z=N86fR|WM&SgAD}&e zjB*a#oQy17j9xOJ^OYerzb*p{7c&<Na~%Ua13PGETnp$lb7-~B!^z6b%*+tS!N$!D z8RUR;{45D;Vv7SEyW8vyH6nqVkpXmun>3`;ZHmtT(6A+3J0oaN5vJL}6kHdfYUbjC zx)!ItNYEkdiVBPj8fu_Dj?&-*c3@|ova^B?Fa=#s$O<_s5Y!q`Q!_O+7Bn_D25qQf zS2kA$jlQYFuiF(B5n~iisi;h`^Os~}Vu`SDc5$wn>Fnx~z{?^LU>NG^6zT5n&IM|n z=_&FW^I!WLFlUa(+_@e~2A;8T0x8*r@d@CwAwg&FCNnN$`oX}<5DW<#c?Jen(7lkL z`%;-(7+6_byxBlEbjE>p%YxU-$$*YIZ3aykHZw3YF}I*dLl&nAg7@c0YpcRN09ww& zs10>yNrd$ikb9VZ{0#t&cl}Rhv1d|Z&}Qi0rlG{d1ivXki-Cz9w9BE6ft7`|g%jLU z<KgCI22J9DS^+F^TpVo7ETGdMr5*H86f<yhgZ4gvdR#n^SwA!l;Hf-yRYe7PIcZ7I z6dpflih~QZ4~dn9L7Pz<HkF4Q4e+HD@Ff+H-Ytk^S{NOr4U356TtDaVyg<7McV1S> zP=h)(R_PcfrMUEeufY+)7+D_`_U{H`WPM89zgJ8Wo?4nwdZ2|$459y%!RuLN8Jf0A zF(QIO9o2Ure{n)4g&_XoU}J)YgEp#4PR>@yqy}VC2vs$BVog#4!~3#~vamiUR__~w zJj@uLkYow-aA8HH3&^ulDBcWjg!>N^&kPKVZ<uy6NHENRoU*_JDpW;y*qA|EIHetU zSQ%LvVcbY>@On)d1|}x(;8zo9bqphD))w3qgm@NWj1;J(VFqoA2kqeo4cvh`ln@mT zpmhk~job|2DF(0#NN*CXATknkpoXZRC}=~Ov^JwLyO<bgEhVTq$p*X7+sw?IvE9)~ zR7aFk!c5Y^$==aB!i}3%F4&T3=X6FE=3rJ9#zXtL;@tl|WRi?CG+_p{XF>OXBr`o? z0^PNu$l&AP$q7ozUJOi}?3_&OkR;8($-vIY$)3u<#Rc{xXv~(8ktL2BmaG{V6y>EQ z86+4az>O(EaYY_p8OTi~pi^<!K^+%F86hfY45?U@LqCLNs8}jwn8D)5r>m*X&eg=O zpGhLf$G}d7eI_^|UcJgFr=zcG2ns>aeHcu8nI17HGw3l~b>Nd`WaJQJWMKqNK!8S1 zAR{5{TpV2N9CaWU$Aikn7H>9YCPqe%cpgqB4i3mllLXN9f}pYjveDj<fuJUEqnd?< zHGzSZwHq{9p%2povV)7Afh!%&8Z;fCA{MlaTU%2_PgPGr9(0VEm<T`kj3#zg&_*s$ z5epeaft=AK2;D#p8Wb>vt||g8)DVXRp%R;@D3ghucW9_w#LEEW1w}8tjSZFMWaJf@ z9=TiE2mI4zy!`KMBFcIqCHI7ry0YRbx+b6vSB?w}jJKGsf^P}6a4=N`WjHoA(D62) zDuW>&yhR!`;sjd92RemNL7tHTbmu4N#vBGYMmf;IsKVfGDdf~+@FBLKY8iA|4|wzk zH0C1$8j&<LQTr7fX`rfSD_2&qVY$1%wc%{bE?cjJKy7_9Gfg=?rmLYo<^gJQmM(e` zY5qnwhW7H)?Ap|WoZKT_^|W*>6x7XCG(cy7F@eWqm6)zEh%zXGdZq&WOpG23pbHBb znbSeF0i?&x;3F<3D=nrdt|+R?$}6VL2y4HARtB4^shf+5iHo6*haS__Z!Gu7x2vmc z3bm28)7MO@2=wyx_x1B;ysDM%R^4k~>{rsx6rG|XWu>O*_Rl>dG$1W8A}9ltuRwK{ z0An|kGJ_aH9i$P#&CJNiAk4x9TIMb7z|G9a3gbb?nn0VOpoby!f&v=0j*f+;9kc*M z0a7-A+I*mecbH1Ri|mA;$0KmCfzGZ3x0?`4Tp(kb(Blz&_>}lL`9Mo|&z=Pp6f(X{ z%HU<Y$^N%oKQME~8JdIk*BCG`Fexx|F!HmeGI=pcz|R_x0IjVAm6zcCHj)e|>;Kys zd<4Pk|3Qa}t&NEIz|0A%ib3uCY2bBgaSq&)JWMPMQlL}R;pYx8FhItaz!?<Wc|r7V z<iR6tpzXP!^NzrqLr_#iI&iA0s0a!xgLYD@f*b6Jk`XkrCW@4m7!O+82SX<LV@%_c z9R*|r_%y`aow|;AIL9ad{mIPfp{*XDdLDE@7dspHOuo?nkoXj0NJENGex&$hW`Odb z@hJpue1i`i07n*RsMZ0rY#TgI0?AP@iAV>2Mg~yb&cVhY#3%$>$O%3_95Q|iS{4be z%n)&g7Gr-uLL!WjLE-;5##2m^4C0{mzaoq*jH2L;O`s|je4qt5Y?013782kAw<j3H z8O1?a7}CIl9%I1<nO+7hYh=_dZ?rONWn5JlpA{CM>Y^a)#?@9CkT2`uk)0KuDkSJ9 zzz@2U{OA8b#xABG46+Ow3>6N1+>Gpu;*6{e5{zuDpz;~ipI2aD<YZ)LW@Jz2U<4gb z&C1M>&cMpR#>xiT)(F0!Tm#aE#iovpjWwKsl@-+eWo7e^bP$r4Q&pDJkk?RA78Dd# zQRD)R=?fyp4nd7NQDs5ULJ*KYl$F$&!K=NDjZIC|mK)mH+ZzkZ@<gPktLVteaq9~Q zx>`y}D$8lhaxog)=$V-Fa4`z}OE>#>QQwf$jg>u=NnS=wQd&??9hT2+m;#uT7{nPu zxAHK8u1tU=4@roRz}HWJmu<9qL$`-ZJIKQ1puIS7XRQ||4{DtXs|X51woWK2!`pM< z1`Q}FgO;-}{<DF%W{eY(%!3$_+A(qI|6cz42JTxk{Qt&y7Ce^b>fpr5$i&FS$bkPW z1#nLc6ufMZvlJ8<6jYT21yvzuDVUlX+cAO0WZ6M!6I88(N2#GftETSG;gap7;VU66 z;ULp3c=kMtuZOmajI^VkYmNiUcgLV~LqQ%-PJy|O5wSwNeti7qg^{34&A|L$fGM6y z3A``QjG@6n%0ORBQ&kR>C)gPoc-dG*8JSr`z}wj&0jvm`PlN1xKpB~b?r;a^HdXM% z5JVvZs6oyEJ#VfX)KCLY7PT|^L^=qltC<*qcGGDntBWXe@Jeb6n}bJuKus6$4Go}v zGGy6+DP(jSG*841I^<GSO-<aylocEbrpzXcCF!Q2a;!XVzP_c^W#0bfuKrAnEM^5U z1-urFG7g?BjH*0d(f@9+d)NhfF*0-2Pt(wJ-^^#v_wQnHiBh0epqnU<E@xn2Iv1lz zVx*ay-B~tPbHCv5L{O#1z`!KN^n*c)!OuZJN)mJ^IX@pe=*$L2(0Dy4ZwWFmFrcp$ z5C=7!uq_bSCL$^cK3KsVbf|&|8@sug8E9RAxhQyP0K2HNDLZpeI1?j3w>;PC^ouXg z9?9A)p%WhTSCH}5zhYh;A!dFx?yYwW?X~~?{lKbr^gPIVCeS8krZgrc1~CR%hDZk? zX(`ZIv7ne_VPxQCVgb1o)J_uyjh8}V5Y(mwA2uQF0CH_Bcw;5B%L-bL2io%Fz$+mx zE-N7`ET#e;O$9j`R4RgU1o%J*K~|81!Es`0!W`tz%PJLPoRH+}8~*QgL=dBUxGN*0 z=s$i&Zci=E*c4EZ-(wUqxp$7AQ8FRggbj2E6XXAX3=E7DnUol$7_uShOjt@%OjJae z2Q=Nt%*5!$&B)*(?H~-AM?#SS?-vDmOAyo>0Pn|X1*J03A~*+81_lQ3+99Z)K&#o9 zeImDstEzw_K;0ZtE5kDu8yhIuf&9Z1?!m<(8EnC?#K+C6z^mz@{p3kR_+KGL&LkZN zX68U<7F~Z+1ABu<7lc4{Ec5?wjCa8OR5J%-E=JITb<hDDBHZBfAQZqE2wX8i@&h!t zK+c0u1FgRhh8$#sa0zJo&K$gw9h3<`gGJDl?CR{y)hg=wQHG(aa!Ow6W|kK2*NT<Z zN|G!B<rSj!oo$Rg?=iAUh?;A=c!>zO@rfwQDX2?p*^7x<X!`{S2?Pj8Ypcj<%V>kz z8KAa-7t>!RHpV*8-YEvF|KFJXn3NgVID^3aFa`#usZ0`#{ESzbUcuTSj60ZM?GV%d z-<Xt`{xT?o_7I}oZvneEm4N{~*4NGKqk_2If*DfBL0Sx;&Euf-1okPYZD__=CGVYP zEA1EP<KylmrohdoCY4y$ROC}&>ot+-uenB6S-ELKSzMrh0tbs5D|=%>ahq#>SlYf> z3{3yO{TE<jWs+nNW{?H#ijn4KgWqWZP8yIlKs#uglZ2Q6KL<O5FrzT2?iUA-vVe{| z6caZ#1I-e!vB561U}H-%uqjSG5MM3M$;>ZQ9d|=BDpNv0Ohr}FGyH><v2TPR<FkKj zY>aG-8H4|x&dDee6_;_d<u--P$?N@BVSK=($fU;L0-BR&3}BcC4qx;CDol(_N=#}D zpmV)I`@%PY#g+dHFuY{?!KB9E4^q#)jG+}Q4vH^HuzJt|&7ghWHehkc9?yqNYT(Ha zHpT$Z9?$>(8T|jNFo}Zp;4nmh%wY^*SPfPWYBbzt`pclk0GeQ7XYgc<fQd7w!o}4g z;tcEzK1>_n^08Rt{Sop^yl{DT1|NjEOsq)a{vdI1JN6q>H`8ARb<iqUP&O8k<m6yx zWdhx@sKUt3sLIH|&f+EQfFaDx4!+9(q(upQXcl<C3~0QGH5Srj2aSU;bHG<?$_7L@ zs9{$LTH^{{U<}#-4PF_d0=iwDl?8M_9U~*mKy+oG9pQ`&N(!LE>cm9_1$epHSQ*qA z)j>O?!An6vJx5beeGP6Bn?R~IP;UU-ECO#^VKUTm*YWWcuvfP+i!hD0^34*PJzHN@ zM2nZtgiF@Lk<mg+IYC^;*6i<oCP$}WbxXr)mw%@W_1RdR**O^)!F!yInEo&XF$O@& zO-Ty{CMHftb2D)kRwgHH7G_3P9q5G+kjo-uzzWR8nOK+{&6rqOkko+I6rxIVp-M+W z+t{iM(3Ku7-dr3^tgMi8e|y2Bfei8Npi3=5!3Y_GF~CsF0Ggc!YXlzy4BAlv>R%!0 z1Fb68V_;_BWMbw_2df0P8hRntVb|rL4b#BD#RXLknjOZf8Z_=I3tF#lV`XY$pa)tE zs-!3<BPPPZ#t_691UhgNX`{1=8hqn|IJkHM71O8@2|o1&DZ)j-Lq(urd=W7*aq!0Y zJFo&;%+l1vOCvu@O@&X-N6#%%1Qg}M+Pr)w5?UVGKE8aqirRX-!jRHk%G%h(Q>}yx zT5yXfD2Rw?Sm?NTu_?%#dZ<_$ASDQRf{`}S5YZC{m;K^$vce)7mfC&+pfPt)n?!)g zj)9#)05p@z16s$1xYvY%fx*Wcbn_LXFQX3!2Ll6#0EYl*^j%O;keyRp+fdk87<7e~ zIqW?C8_8D_EN|QVHRljuW#i|tV6yvrXz}7V3q%bhB=jWaL&`p|oy<Z^Y8;@2Zfrb` z|B=NxAmWT{jK1C~3>+Jo7&oYFFxLJLQpCU{z^nw9_p-(!&-4}|&%nlG_ZZn+QMkCn zOC)iolMr!6Hbx&`H1qzeFbOc(LF5_O83PzCz-1o9{h<(XMm9$OGO#`-RClg`$TO(% zxP98f$j;-)Ai^+>L2x4z(~kdJHW+JfWMSMe4cvPN)w>WgnIU=@*%$-0z-B`H&cFm} z4>9#I*)Z@k2!m#vL1({lvaqIt)~>Q~GNf`aGBbnwl|G==F_4LPUq&C~J@TBg+N#Rz z%EHhMJ)*{<ih|0_#*ES1T@(K^PP}vO+?{`C)r)2>Ud&|UzS3>KJL8vsKSBP5*bR=a z8K5&scpO1z=P@udcrh?AJ_Nf-oIxIR>jN7jD-)v^J0lYVD+?2AD(JeXSkNGek2eP+ z3k!I}%9qhcQbJGwbRCm~yrjICh=91DI3EuKKO;XUr-(Lm<O({krz{8_U{h8Wg{+oS z7X4M6VPk0FSR8+Jhl6EfWLB+%OEQy<m%E7rCo`9=eNZry%@<vru=W4`rs^0SW?*E{ z`~QRSIg<;6B<Su8F-8VvaZV=4p`*-Rps_S?OC$iaVgg)31cG*QiVE{`v$HZtGD@;? zifAJyyhK66f6(j>8uw*%i%N=#S9MjikBls{cCHPJ@Rn6@V*FK+l2O9P=PT*e5*gOw zt>Vrv1W9jTw}8WOHY6N%k;OsjdN%0J2}bX9P~0;@;{N}C2EG3RAU`mvae(x(GgL7g z1H}u(Z{Yj@(dU(kt`Fq*&oF&F_6(ar$qW>4OyW#73>*vs;8O{in7~ov18OFNhn9UA zd=ym_l~_0>Kqr4IgU;6%V>eL)GnmBZO_;x~XL58}Qn1HmM%9T^m~8sHGcx~$fz9S& zU;vxV!yo|)aW+OqcF+mg44^~zU<nkoHx-m%eL<U^L5HM)t{7(EVc=F26y)R-*ETj) z2F++0f#*Nfjll~g!JE*$?CjR{OrAHv!{c&VQgBAbZ%0Ok{*H-LJa1-XZg&KSi5JLz zaF{HFgk=CIOb}%V)O}2B9E-sHa!?wc#AL(34H~rs-9OF3#0)+T6_mz(Kzkv<wY@JZ z=s0gdK>;>SDQ)B|<i+Um@zJA?|8_t#4Ja>z%>>8uQb;^MgTymL98|t71-s4H2;??I zJcACVm<4t_MBZBgRUV{&4wAf=5~lnlaQO$Z*X|Uuy{t@X93XSq8T}Y$Fd*p!hnoun z1LGuSP#Fo)>u>={FSv{Z>1AW|F+tM#|38D*e-*|_Og0d+L1&3nL&jr3Zk@<v1Knqc zbj&O~Tzy%<K`Ow?DTxSCWiLke4-X%PGugNwaDT_Z$e{oK2a^bsGlMkf%qP%Al)Q|r zOyZ2pETDck3uqk@3o|QZ2P0@Fb^tgHgO=L_GWdWFo>Wx=#h5B+-7O;LLTX`SBXdwf zV-nFxvuvH}(wda&Bj@g$nw61at2^O|w4l(m{?Jw*Wp~A@q`Z8AS?=I)0=tJ96o(ra zK$oofGBja?B}5G<KZDe;F?toDhME3<Wsskk)HpzA*Rk<*hCsp^td9wlUqRyRJoXHG z7#J8B!TFU3oJQ3hKn+n)hDrtP>Sy)=4_5ht+xYP9{G6Z*A3=wSDhnEmDw;C!g#UXJ z9?mGjWb^k3BkRB4Og8V{f%7Wp4vb(X8wOzpaZu=TvNN%Au`n@kvofUeFoG`9=4MI- z&5ZkagN7}A8GS@VM8rkF$3#MAm;?m{csXUY8A110fbMREtkqEj9ca&Pswm17>;|&h z%`Kc!B;0N8zc-9a|3x!0{AXZXY9A315iu`59vr5iJj(-4*IU8)!YdXjycif6R6)T5 z_P?5gGH8^Pg&BM{3ZoaeeF9pW?aS!HzyP}PNR*jVRNEMS9Jsiss-xpAM@J(8#_jwj zCj9?A1&qLUsxmMzYJv0U4oDtt2U+s}|8E8cMk8?i?f|>R_cD@O{{R2~nt_4Q7VK7t zyw_nAdC-^(qXIY&g3M*-vHPD5vyb6FGbj&2^!Zt$===W^WIZ_lLG(HNFNEm(pTu~K z$%a9V11!#Qm{A!d{{Jrn1EU%^Uqa0Dxr$;QgX(`3hX3Gv3DU>L7yug2`Tzg_Ed~Zg zPi9a%1EMc*6Ouj##{awjCowE$vSHu@-J`?I$jAg*_{8MJzz8}E2z(-*52%Xd2JK<x zW8`Du6wzjf@2Ur%|HG&w!7t0Frx_kLkIRLb)!2kd_wViau>b!-W-}^-!|pIRY`s>Z z*bPeOjK9EkD>%sVaC0!Rz>XjVr9dVoUnU=6AyENPwImEZ0TpzY1G}iQR|Kyd-_Dfl zH&Zt|L@?R>dnKSGc=^Y_J&ZiYH$edlDyNh|W;3aA90kXBCn!81d5duYlMMqas3rsr zaruB34#qJsf||Sx450N3f`TlZV&D@w6a^Rl|F??C<{z&o6QrLFD(4n}(-CMd5j&3~ zG~Phsi^1W498^v)sWasMcZQsY@!uKL>I0XZ${@R$)HpzT*?2lZMJGs{fdL`T2r92X z{F?#M`)>wFFX%i#1_s83Og0R>pb-roZZ1wXRu*OsCPqfkDWj0!0w2)`DI0hhctydU zP!&}KEol`r1|3i&%DC{~Dr41%@G`YIo}Lettj!s@@A_ILAA^Jo(^MuK2651N7ov;| z%#4slji3^TiGcxPtT=<Xpt!go8>pQis4Qq=W@fI;tjw$|A_l)&mT77bXff0VpAF1R zO#YQ?m{x;}p>z>LEv|nGj1RfAjl}=WWwQBa06MS?Qr4+3*)Rw)s5mI{Ff%bQF?z8u zGchswfR8wa*el2&C@3b#$_YA$KpE+nV>LBXK_->#fA?4zS^cva6+!wyh290}K!tyM zm~8$TiTWusdO+)FCKYhJodc%_M`&9NR2Hd#)95*H8uczfN~55-`LE2R0*+URzD{RQ zq%ksh{Z9gkGsuAMw-D!JVq)e+EC*#^1Qp}S;LW%`pgBcf@D;r>3^IZeD$0=H0}XY- z7K37VDcn~Vl&LhdSpJ;>`7M)C8RECw@nN7$rm4et8Rkb1QGaDdLxdkec^%@Hz(gd! zFfcI~{Qtq^!Q{%o&mav-{S2V>4h)Qp0U#HG=W7EQd{k7`ltCGe5wf2ET8D~>p<MLh zVO16#Rbg#i5fxo#_0J_FCMF~zB07{&KBCpjt1TkD#ml=TJf)&MHKVxv|9^<R;IO{} z3473z0=PJ+ZoL8yd*3>cdl79nuzSIA1CjU2!y*qZdm-l9frfkG=7QQb5OD`-NST0Y z9y?<Y!%T2p&IIYnfy!BkTfyZl$WC@fABF~q8c6FLJZEC?{}q!5QwW0ugCc_#gN=iw zm?&s#o0_Vu3=1<8CkGod12eOi2qU8hJ0pWT>~_-t<fe+ct`<@-!>8&&Th&3k?!e(K zE(|(ajMYR<of$Op%LEGbP%9+^Wes)fiYRR_Sv7riHPwGELLwhEZKA^&lSG8ys@ldv zqCweE$xyx6GrYx1M9@UuLaoqU-$#ZqRJI~n-$Ux(TA504IK5)>U>0Ul<G2F~e;!9; zMh1?J%#0ftA^mSic!AOrC@k0*1Htt)G=9M8=_k{ECT9i-231CO2QFnsW+o-*#IjOA zgo85ZxCvHfmQ-#ICeR5B;NANH-kgk}SwzrPYQEqrrPKo=96&=opku{gN<r(90=&Wc zpj{PoU3vi#4i;z@;LsJzz{chawidj6Igs5)+QAsE6@2X}L?<YwK*wVEc!MT7A=(%? zIbpg$Lq_sal8g)ra+0c2szS(%!k8H(7$tZ>y+<Q4aiktMxE%`4M37Y&pi{;L)zlcx z1@%N2O`l!A{_NigK^>8>QfuqdsEDwTNO4s@UNt6X7H0Pq?)%+Yn7vvf!dg5%OVct+ zS=kvEu(C5Sf$Pm9Og0Q$48ow3yg{eTGckh?#D?5h0V{GC8H5FSxxoi$aWQhS!Moew zJ7nO8puW$}D#&pU54UslwsZ7mvWW-@O}fcs^Y4kCttn{m8ety~gDB|Kat3AwRu*Q~ zRPfn;EDWg(pu!qbf2t@c3JQW-;Gjj_psQ+(LAS!N!j>K}9syhBm!0+RF2p`YrO>2o zCY!v7BT(zW>8b);P6;u{fo2t$85u#hJuxscv@o!+fV=JByab-p31kEnS&AZ{UIpkP z7SNst$SyokUP3Fjd_z0Ra`+J?ms?15Y;br)ObDZV%2b(24T!R<vLZF3xB?p1Uzltd zKz=uKFlGfEB?3B$A9N%?sE^A49q@p*m>C&Fg`o?J*cf;idDy{$4mw*2<S<cXL3psg zylZAXKe;nKJlx;P+10_F$!4*6fXKfWFPLopeKIgHN7PS@U%+K2XmE>-$MFlKECaPS zzJSZd$Dq8+=nd{$Lh~l1-eUX$uD2liIzdOQGcYn}{{O+ah{=UPok7n*8+?unI};-_ zWETSibXE#HJOZl_R6*ysa7t){&v60GLP6qE9lW#%6vm*D5l9CMG|b7kC@;yvU&%Md z#M#Bc!_OqrLN7#FM#Wvh&|2O~Q`c2vo>vSXUx13PmbRv(l$)!en~Dg(tAL=Ex}1`f zprD4$|Njs-g8lp)QeWJF_!;bWu%Dkp{458m4-oxn&Hn-*zcHzCfb}s{6=2urh1Sp3 z{I86xkEauK)iMJkgX{mFOgc<13@QxGkdbTw6;Tmp1{O|6CT0#sMkdf6HWVRNCh$3d zasd(GrG?B)49w{aEG*zjeQ+(y7|+Vc$QTIPF`)#iO3)OrvigGRVpzhCbl_4JR#8z= z5n$((hOeE1HX1+^XQ)@&gT|y8YvTg*5<|>n9M#O->?7s!c+IrzoFj_T!jps<ZCQie zy`qJDc(@I8jZ7@+6jXe@{>^60$SC#(9mxhdPn&TYlMMqqgQ9~RXw?LBEDIwuvkz#r z4AS9ZXJ8i;gfzrNL7hNlQ^swp!dEfbc>m*P{0O?AjGKW0Yz9ArzJm@cBO?pwsy3!n z(5N7LEC(YyJ9yLyJQBpf&%h7uS8{^<2RV2Ma+{PX*yQ5k@Zw^S>D!!t{BYjp4LS3h zaT_=dyo00xKGZb8{a*lNKa&~<sL#vBP*n&?1Kj_Wk;QpB-$L4PuK#~9@q^o+iVkw1 zekLQh^n|5;1_lN}20>+EWk}-_)M!%!U9ACH)?#KV$i!b!kXstz>E`0jWb<!EMoDo7 zW9Glvp1!{BjOpO^1lW9VS_VzXvGF)6K->cn2eo}ZgZ<%~gX#~6JZMY-BJUN0MIPMt zftYK@f^06R?E?`9Z$JP4p8;f^7&zQO=7HKi?cjC|BU0N3q=p~d_5rD3WAp*Hx1jk8 zoF7!cVWq-g<e<;b2s%}sfeCc(88fJ3&d9)+3|g}Y9z+9|(t%7q!pg#Gs*0c?8CIlq zV4$TOps`h?kTez)VP|7wEYKAc=irkSHZ{_dQq+J3ZG@YyPKhonqXi4Ak&cS9I0tel z|C>=@W$lU-)}V2Eko(yg{TZfX_anGJ3{u0!7zmzoK==_f0P`DM-YYuDu`yzFNEsLy zco=vjz?~~&W@BOaMR{k>Ce-lov#|<s)g+!{viaAgY%C^bDa%;)_XwynAjrVL^qI+q zK^|0tO7OEVgN_dX?{D?-W@Ul*u;dx!B_#wUKwShyX2=cR%*sgCfb6ql0?kY_{!ZfH zWoF^#NQ$2k7RD*W!p6rP9U99i$jZdU%^Vv#lQCP~MqJWT=HEmnn|~XW17&2KH5eoR zEm8827SPe;W6b+|<o|yLu)DzN6Exq##^YE4Nk<TIQ2P51>d!KIF>Hi{C9E9`(+f_6 z5WRLW$a+C(5F+jXx-y!95!@^K03O$ocaY&^V+Mx-Xfy}Ze+Bn3g@l9zLA?)ic4c8^ zV{vn1W?|(YQ<DyEOz7xH+<G|W`Ey2fM$uch{=N8j2a<n5@pThCwyo@-z|O)5HWM1I z3}7=E7#KJhID~{ieR@V_QDswRV_{*&n+GBfB=q(%+5B5{?-payzxRxS;4}m_4-_7p ztdP3r5yVdraU~`-kT@e7qZeA;1C|Gc1xOw=7j+C-pD08h0~?RS8Hl@O85kH3gU3tt z9dtm|FC+M_B9JlACMXjVr1*uDm7ENmLV`k|QpnhpQPEUUlu=PsmQnxT9mdK3E;5>1 zFz)uW{O9Hg_5;{#updBYPq6Vgf(M)aGeE>ae&7O^Io^t>WeP-|l}U{gBo7*gfb@@G zxdYV4G-i}%C}X<G=nfi#WQ3ociZ~k-R8BCmF??X$!@$iT0a}5>$jA^5@>e`43HYlh zG4YCN3&Zz2DXF>YtE=hjtEuZVChF_x=<Dm~{Qu8j#PE&j591HUb_N{=BPIm~h64-^ zppfI-$iTRRNdY`60&<Ty0|VnSrc(^=44O>rFjb(lz!?}A|A57fnPQQ|K?h1`f$xkk z0NqasSyzj6N0$b8oNyrnH-mzMEH@V?2P+FR69*`>f?N;Ux75twBMM$*tEdPbc2pE) z7ZhjI`u8s+gpnyE#6*}`NW{cM<j*@{P`>A9U|<PgI>n&Lpyi;hD#OnP%K2VQj7*?Q zIYDDM;M+GD7(m;QB?ToV1VLBVfG%%`6)cDxugt8+qz<_T6IwFx?@P+(h1_u&*}yFf zzT>hoa(`?qD=#+_vmkqGY$l%sE1M`!KE@T7LZ+s|jMx5}F+LPFHV}~Tk@_djxWp`7 zSuM`sUjPH+|JVObnCh57_fHu)=nFBjF$*%XvVq10!Dq~-f{u-6V@(D%64XHbT6Jb0 zRb_Bx&dx5aEiMSY*GdpHC=ISMA#Ij8&>dG%|E735h$(V&%Lv%mGPD1?$f*C<jM493 zxu*vQizf>k0}}(=e-~EJUCnX~Y7G7kKHMCv%&ZLH!CzL;`4pUt>}>3T3_LuHv3%T2 z%#4g`-n?8)Y;4T29H47O!AI+=sVc}b$T7%)meYfr1u9qs`PpT)83h#ujRhG&H?cE< z+$RdUyju~p!_kh}TpY9^SCPqy(IAS^I_=*nMwgs_d!znchz|QF8fLEIXr#dMaS@BW zzNG@AFVm@i#~CgEEn|%OxAfmc#;kwS7(IXf`?6;bBahvzm8<*%ttQRv1Ko$s%)s{F zfoVR|DFz7!1qO8nUk5K4UM6-9WmYCGb|x<db_Om6cCK`8HYOGZmLLWW4#rqenW*N? z$;`ySz#7NE%Bl{!oIzDZNl{K#3bx@xOk9wMT?!P`W@ezJW1z8E&`p>i?}0AE6E-t5 z7gT0rV^%a}V`pPy%MsHOa`6rgmsMk7R1XOMCwkQ^)Vz&Vp^8zhBwax503)ATlo4-) zH>1?Q&oLtL$0Gi={QGp^023>thzKK}`@(;_ZtE}`M1ayX<9`RHElj5v<QYZ1H}HUN znigVYVG;!2kt^-Jfe$Le1{VPx(8-0Q#sS2EN=Jg$lz<G8Wnf@o2m~Ez3tGRU=FJY; zwZs??>R72Wf-ZRgnJdT6$i~3H2HuGdo~Tm;P1zx<0@+2-DA0Wogp6`f!eSRAqdLe! z@GP<(vri;wOaOFjk356CxT?CKpdbgkl(wp%vZ=D5G9ME=Xva6G&jcF3F*CPgGFKF2 z+7ce}h*M0An^(==SWualTSh=mS(t-ET$+!u-S^+;15DLki>H{l#2T=(db6?{da9^R zK9c{pnSqf(>AyOYD$^+jC5CqI4WJPgArVGq7Jf!1Rz^lP24*h?MmBe82V_A;h+rf{ zxhx|iDD2o0L8p|*GO)0yfd)Fjv$X0=KGF`#3=E)~@(Ms_)iJUqVO16Bz!eI*;Z0E) zoY76qKwUscmrz*|S~G!;e245`R#swj=dv=BceA(FHM9SBim~NiH{;WP)u((S)WU=9 zQe_m}Z+fr`u&c-@DB3dF``P|IsN$q$?Zd_cN@p<tYJtv~1p8Nlk(G^~k%^O$kqhi! zE|7nb1i}90a)<g?ot=?|gOQn&g*g@Eb1nw1MA*QN8t7yjgwJ)MK1XO^04*BeO2Vo> z(m_B|MP61AbRmI|ilQ<%yR5c2T1c2Hn;M%dnktGi8;dHl;|`J4oqLyp!enE|o&{J# z<)6=2Kihx5eBMF>=Kp^N-T&VhKQLWkaAz=MQij!S42%q*J8g@>cR;B-sPKaJy0J1s zZ)Id)U{C{f<Cqc{n3(jKd{kA1nc2m);kW355-4;ndA0u!e>FW}6=6Ozai74sjE}@j zO~w8x8tSvNy0LTmGB7ekF)%Q-Fr8wMVX$;CljdY%1r5)zGJ_^&>zKfo1TwQSFsCtq zN@mbG5$Y_U$vPPZ8DV8nP+`o;F0CyLK6)H<xCr>f8c=T*w5v)P)HN`jx;|>{+QQPP z_}Hj~sHy@cS)as2pG4lC+-++UQWE1gZZGKMNd&b9K=+=5?ruHBAjqHq8Vi$?=4WPM z0aX?(pb<W1kb6KrhYoe?G5Uxpi>V2+vP)_+f{%lMq*IWs%+PC_Oik24=fJSBF}Cka zJX~JBWXY1n<>d#G4GStN^I~J75~CTnFpB+qB2XNfQd5%>S}edQ^N-Itz~9B*i@mLl z9dy(>BZDpEULXcDhG2&P4Q?hTMs*%07A9#4CT12>Mh4K)&@4>MOf1ZGY@j)}c+g22 zy56Av6GJ=)=)fU8(DGChBONVOWqDc9qI2;5D`t#lT<jv+;MIlD^@QLJ0k9n-rpA!d zYaro?v~`|MSxKFZF*vry)~+N#&09`M%tSUQDlJvsTrx`1QXw@hI!M+;Oi9jLEuh5C zwk9US-JL6bWq$6$NC{DA0nwQ)?eissuU{7tpWogxQ&hlNR3dU=ZvM)6J3DYZ{C~l~ zz*GRfk5-Ps*uj9Ek%1A^djj1i0$m&gy{l9gTz<wffX~2JQB+Y@QDS44(iVoCai9(= zPDDXthM<U1Hiwoh%8OME#ngnw^z1@Hv)h^)8&5}NhJ`1F#WP8m*mAJC3x=fDWG1EN z`#5;`n1BXWK=ZQ9+DxYyco+m2Btc{Jpv%qJnAlT6EzwvOMo`)W-3k}W$;!bDb_*{r z1B0ZvupqAhuK+(Eba@IFySO&Eg&+uBo(!s9Rbj4LyNZz|tF@`2i7`6r@14w$@VKx9 zrW(J0S3z!JI`!8K<{l=7L<R<CNpO2g9&`%>GiaR*3o9c_Dg!G67bh!MDzquZ$;lSS z&B4URrVhGLQyz558t4>h6-7|S5#(V9Z}5a2r2-EpL1jUBP$fo9j(YhrtF5u2>Ez_M zP)D6gNJ#K%1UpON?*o{#7{K|5c?;831~CRThHmc-po&o*bmtj6$Z4R1b3pFW1?>`M zjt8Yqb!Jdi3|e)o3|Gd)qy}1r1|Bm(Q|h3OOBFM-I>=P;W->ii(5bTAYHFY+q_8pv zy9}s<07?pC;-JeOz?CUzfu*Vmcr`63wS!75Gh3IgjJOtWj!jWf8(7@iQ!=_-nfUY) zyuA{1b>n@!<MkNRa+byV^~N#kff|`<v%_PT<%TtRcr=AYG<tY6fzvKHZq6{BW#DI! z1kL7&GBPqSdT}s<Zm3{pWJv>sfSNZaJLtRtaN|-?0BPuoi(MMD=T=Zr*q9MK87izO z$Y=~&AIZnWo*2cbRvF7E8dG_{I;Qd8PsZ|<+8k2s%>06EOsD>xV>JD1%;@!R8yL?h z5OI~@*U|ztCm@@IyP3{1FoRRDA!r9X8zT!d6H6Ls1QFck1~1)GXYc_fTu7qj<N)3D zDag()s?Er#$SBUn#?B6M3NyQ+I%655=-*OCNj-K6cDHNJ91{I*j5A%C&i*xKIvXPH zEVgZ%h|9mFJ9aQIgU%sjy2NyfL7YK~K?QU^pa3ru8;c+h6FVD|7pV2k!3Nsp%K=&{ zz`+1ICKJ5GRt>UpRGraBMp{8$T17@#NL(DW+aDA}s^I%3*p)$zAazqkQFb{dadBlv zb46h#Ha2!eJtk!)x3z(54Wr$J^u4T3F*%;L^3)gdi2nC;-M=MM4eT@p8Jh*v?F^PK zUApw1Re%oL9}|;5Y&w1xj~Ex+`S(!Jz);snn1LA{m)s1Z43eO8NCd&L$;QYCy48>! z9-R#A?98#8tW4mnFDAyoASNj$DIw0w16ou9&THK4lJJOCM2b{?CPj5vy#6gkja0Wd z#+flFF$<5?y?_3I+BCfX>zEcXoo0|@NO0g1V`K&G6J!OQI09PnEW^OWz|;b2@v?zt zpTNyC$Yup~(7}=l3`{Jj(hLmhpe7u6Bbz#lPox8vFsKzOA^>WfsVaj;kif&VpoD<j z7Bv?(W?B@RCn(0j8Og~m$)_fyYN02hz|Aejt*j!xK9-Tu!6;bc?*yg-oh027%Lrq3 z7B3c7J2xiq_=4zv7jXUW&bXiHDZKpw?zb^lFoEh~Nzg7x7DgsUra;gkZEUfi1H06` zIarw(8NvH&!TCs1LRg3aG`9~9Z!UHzZAMX0`3ma3C<_|HJ7mli|4v6??SlFKy^O8P z1?sB*|H?d#=@f%3L#P86KO-|EA82#37o>y}2jwAXnguOdjR$3Ma1cu)%YZ`{IwztB z+ONhb2x{aDfa)tnaG8XqfiJ4alpON_djp?o*T1t^oA;15Lm~qMiyJuK={jhMaxpT4 zR$+i<>7gA$&~O0*gF0yBg+ZJ_Tu@M0P>h{jQd^N3cJs5av9Ka&nXe$|3^QiNIj>om zS%O>dusmQ~@b5_^D-+8*rXu-F%YXh%r~Ynd^7Q@N%(&eoP2q1pI9_f4e`C79bd?Eo z$|eIN=mL9IMiU0kjZBO?7)`(<E-avS4M+`m4S_m?F@vXr8#^N_Hyal-3ll3#I>@!4 zb$PnppgTPvM{Vjc_-Ja#%77LD>+5P5YZ@yn%Bah#i;D_K3rcgcGw?I=^PskAKnp?H zKu4N@8wH{wV&>qDIgl#Y7>Ui;o<G^cV{$?EByaD@*_DL_l~qMWVq!8fV&by0E5&7G z#KmP~xMG&&=Pr$jUYei3G{$dsd;83p?d`K)Nz2GcBV)*To$dc`EdQCVGN^OvF@bJa zWa9)~*aA-5OmmrMFbFY7fm*!`tZb~!pq=H6feh^I@bnED$c6PP7#O4^A$MqqA=0-X zxG@18ZZI_#g^f5sMi`XsjTBi!=OTs}{<cAqx9!v=%Y5&jgO4eI_I(I2eq*}Apbc7e z3!cYg0Ucz?$N=gBLDQ@*sKpN{Kfrf7fv&LQU;~|B!V0-e2{J4LIxGOXy#Z9^qR!`u zim<W$vP)LeNH+J3)$`HQ3o*6Jbg)j)mecmrbPUq+&@qg(a47m9B^oB75D{qPZY3x0 zlH}!;Z7Cz}DJ~h}YwBhutLPr<5>#vrD*x2}e`A@>bcMl`q0>Q9pOJx!my4a5l|hP; zomG+%w5P{_k&Dq2vXw=dlaYagk(Hf+HI;#ji!m0|ie^*;wHlaX*;pV&x3q&cLKS4( zgn^5TJD!1?Th|*jdj)P~fM?+J*nA=#giMTdwbfNsR8<5)V?x3_pfMrPNx{gYLdu{7 z2-;~1z3mFL6xtNp=!4(dz{IYldJTD;Xs_#p309f9dIg3F$t4+SiCLKe4lWT|+FI4V z#g$CT25w@E#wg=OetKbXWrAWs;u0QCE_UATHfB~z(zf#2DGZDZ-V6-P7nn{lD1&xs zD2g$$Ftam(=A1!$H9(zo7RGdTQ0=7#TE3>v;3EvW*HlGG0MvF;R2Bryu0Zdk1q}>= z8cXJgi7RtKW9AD{c?m`V(hhZ|N!d~MZbo6|oKg19CP5}qjG6|rGE(x<(TvjnzFOM~ zi^wU1ST;7IqKuKCJtS=ZEt%7qPBG{+m@^!P%!~>tGP1F<LPjH)`4}0QKzp{(1lgHb zydojW4R{$D7(j!0>7YX!nOK<9K?l<?GBGAIaC39UGH`OLdGl~Fad3d^L3K7CX$MnG zE$oaO?4h7z%W&%hpX6h1W@w<NtD~*1rmCW>q#!3PDIo@08Nr~>sL#hPqOA&Ac@95Y z44x^#Cux8?QtYCT5kk=Tjj<UsWR&oOzP*)#va_>|wwR%`idK-inxCAkvc}(i;ikGi zCY+2hero4-u*qtf$T5b;s3{xqF|jL1s~U=!a`B1^Yv`!*3JXc?XXFjK_`K+E3m=n$ z!<1>=mZ07V1NVOcrn%t$wY7tVI3qiw7$X~Kq>z;Xd@MQWcnxqR3hvP((zh0yk20j6 zpae<<s-RRW2s#EDydK2}bgh#zXz3;>Au~B!+wY26#rW~<(UZZU26A%BT84jY4gXDJ zs`2wXy6xZb;8ZRjP77<Od%iJ=Go4}(0<Fg2Wn^aH1GPlGK$RvVXtDy_8U?pO^jLgA z#{q!**XDwddK^4e3b`-%;WZ|nJI1<#3WBnF!Eq-TxA^(}^K`djWAWhu-42lOUxVog z(<ug3h7()m7+Jsvg@XqDKzA%KF*7oOZw&!=HCW;pSXk6S1J95fBDKJ`zzPIJI4FW{ zMg*y4VCG;1wFg1F(2?}8fVwZBUJh}F5@8-_2vD4n0kjEFQbJr*kdF&=W1T9a3et^r zilT~;!w(c82grb0IUo!^4uwtmprL`jKBL{gb0PwQqKt-yE(VtWBBB@*qtrB%1Kn=u zsH*Cj3i9&ssc9?eF`fF`!c_A&SI|LI(*<;wFe3x^e>-seNP>EnY>X_7VvMXT;*1Qe zpmRIf7#Ua@l0j`saK!{3FI8vpVPpVp!T>EK<zNTxd|`*w2%v!wXuN^fbb!_?iL$X> z)wg#yyUE0J)678If=7yzSN@QNiG>R*Q=p&U-xghYOIBtZ7Etm8ts!9g25!TcJD6}Y zGBFA;vNFL(0n*voK!q3hAbOZf7#Ki}8D&9XWyt8Muqi0LLQ)N=x)cPjp)xh)X5zW& z>~0$G5EbQAWRj8tN~wFTZAHWwBmbH)YD!2%N1p}t=NLhK0H$oFD-3d=x)C%#4Z2&C zkCBB*hLM>CbSD=Bs3VmQ+l~zxc2Q^c5mpjbRfUd5fNMfgaBovo6g<c&3R>~1ti;AH zsGOJ*4!ZPISw!E`A=fA+H>%4d+BS;uUv4HFi$AEH91n8Mzo(4c6(VB)mV@fO#Qz%1 zKbTH2s54yIs)&@f)nMrrv>KmP&6|UfjSV?<gI1;?Qa7m24bBM+%v|8qjiC#kzKJ#1 zffzGEX<V9-0d#Z{=!8HCaWN4AUT`{BXH?^cG~Pg`<$}_=sEC*`S~3SGbx=z}nXwR@ z)VKVLR239s++yfrVD(ouicvP2Q8vmCp4R11k~%24=O~A1x@i3W4{Cq1vN2s{Vq^aZ zUaSpmvoi2Ah%iVp$U*K37Z+w?VpNo60$nZ5%*MpT%Fe{Xz{CO?k<<m%mwF67Qc?l} z3i48NQgULV0wMw;YRVk!VxZkmpqdlBV9ywIajBY`x~QVDDH|K;=r2=6Wl=LTrVHMa zv*FFoM2@)Fe;<#;#&fVH#53|AiBDim%U>D;YjQee)N_6O$X&NBuZ{cvf9_ULdHVnA ze*q?W@V#e_4t6YzjI5xw_Do(3%nWR-%xtNk@?8Vety71z-`E)$7{Ki}Vc01{T##di zI3RfzQel~bH-CVemx3Z}lENyyyfT8$5sb1C5&s^$d|+esWMPY9O1*#o@AEJJ4tvKi zFfj=KcVlT_l3<WwP-Y-zo=pRDo=sUvPL@H2K?ZZ44Kzk22)Zs_nHe-4f|zGBGXu@D zF|ivnK7JeVG~wxwv`-Ol{X+kBhiJRHG9E2wRWw!p$|Q0B{=YwrEdT!fd&Ma5@57IE z|NiXS#mEx>xyIFHVv7NIj8Evl8<Rcw%pK57p|69N6b}<ShawB!c{L66c{LSf1$h~1 zNznDEpwS9(QTV)?Iq0NC&@l{VX6B$I464tB&CE?fciw^11RI->uc~C7ud=#`1QVm6 zSKz<W`2pVLEP~O`HNy<KWp*(#o7zfD*|b{UjLV_d_n*PPzq@uZZeg~VW1anIm8>k2 ztOclSVE^yNWWglCAjYr+(mdh@6;_O1yo`*Dp3)8=9vhSgZ9kz-aA|mB%$0#=OE4z5 zG(bbIh`BP%2`=y@uxJxp4xsTUF$OUq@cb^y^cJFp44T)n2n?RbtLJOUE6Kse$8P2* z$ms0$pJ5l{`IYD6m|2{dn4W$42kJ{PgU$|P{Kw45AjhD|V9F5Y5DZ#1EXv5lD#^&k ztjoyG#^MFK3luaw3Ob;OiJ6rtoq?H?k&T_1EuDd#fq^}o0kpZFfq|XDUt3E--cVo5 zRNGWdMP5@uQ$s~fnTrj)UJ;a)O-;bZ=72_%mDJSP*g@x@85@Z)D}iz}c&j0d3F-zh z{?m$ZscF~r$#(MdkMq(FR}p7o;!sG?icVT#>?<h~sF0%Lr4%Y7;%@3>W;sRML;LWa zkO?_9zNQYY0({;f_QpaI!Hh2)P5Ah{6x9D|iVHcp8>(xAI+Ec0CctFKB*DPTAOhOc z3)*eN*uufc3Z6_)WoKamonR6Qq8UJSH3O3`=oUtPJ_cR}UT_zZlMS@h%NR6o4LX|& zDJNm(pMO#R<}phAd%>6#ey`BguCYu9ya&wu|2ObCE4&Pn3`!25p<ZStMo{ycfq^L; zWPd!Uh3~JTs;b1o2H6e@SxyTs4#BlDxJk^$I5FPe*iSn)(AZb!zq+=ThK9D5IwSAY zCbO#aDUD`Tsd)(rd3mw142%q*yQCeNlo+Hz=Zb^c1QI+<%uJGupjGnF>MtEUU>^=1 zj9~%QZ2pW4Vj|!v4{1heRyI6S9-?e)jJ3&=1A-<e*k&k5t3+s37Z>E($7`sBb6Hiz zC)ZdB@_6#_rl%yPgHJvJw-Nq>cAkYWonl}G?RNoX!WL#mCPqf4P|%(rP^*>+d}l5z z1FN7Q3!9iWBeSxwqA6oYLqzevW1yjZrUuXwT!v5v24)>52?lP^`VKZ$W{@c@44{Av z1%-Dk3nLRa9^u1^>}-<S#)9D8bD+k#U?|h|i02V6K=%|gp8ID6!l3iKE;BGNIWzqL zuRFGMFk@q6W?*6foy5(+4!sndm64G(6m)M%ECVAeqc0yX1A`zxuPC1=Xn~rbiXsP_ zIA{+OsBgmtI@QpWQCSq!^fytvHYG1Rt>pD;#wio0Si3th{rKnN>+K!Mw9f1Auhkj` z2Fjp292gjwT$z3{2r@{4?&#%YWMg1!W(Dm;1g(inWnkiBWaj`)r*JScbA&Q*aDaLw z9L&DLLJSOIB0^HaQjn27Wko?jZZ;`xVbEy_qQ*v`j0ozJ7@G>ioO5;M%IP=LOWu^0 zKI@-o?d#6;^P%UI>E4ly=RF_({RwdpC@dKa|9@jrXZpb)$RG=v>*Hr;0?qU>fi9VE z2A@C909q;zN<^R)w9JhDD&V<3P^Q6{>w}!Y0Zqz!v*UKfMpf0-RYk_^i1P^y^7ixd z_h<aT$no!~U}IcfXJ=kqgAk+i-|xOD$$kNG;JHUe2Ic>njHj6-8I%|<LXOrD0v(nm zfI1)Vy+Jo1!U0K!9XxXlo~cJDX9o@2FtWLW7DIqWg5?+)89>Vq*pgwh@Zc5!s90bE z&DBFpRc2skV_**lO>VO>;8N<K0y_AGje$KKGOv%_#K=eoZbcQ)%)TLb{slUp4H`HA z1(GsiY9ADXN}!!7#_Vb;5*oUS+V*<xPZ*W{ePS&C_n;;#Jk;MjKv6{Rx*I1CGqa4c zyowp)9Cyoq=^ie&?rdBPh;Y+l_y!F(Nk&#S0n~YYh!2GjGTh*B1JC#)34+6o3m$G7 zh#7oFMh0$1E(We-*c`qF%A7yMd|iZk25v3}Zg5!Q*5sgvq@Dq+0~D%Uj5utGjC2sv zR990`kb^E3FjNK&IAH`Xe6;}RU^jNiIHxjZAh%~u%L4^-Me3w#tO0$%VUD}yDu?yZ zpoXp$Si_{m;LZ@nc%K2fithh^hM@o77(JLInAjM>ZBho%;UJ(kDd9PN4e%|y>ZH!; zSNBKs^+hE|xOsZHMI=QqwmJp}JNj{^C1j*{I6Hf!WW=X&`GLx2CI+Sd-x!ZF{Q%AA zGnhFTt4j*7u`q*nIx;abHG_7hf-V|hh74@#G59DcGBPOYD(Q-f@NhFIFe<P?n#G{5 ztRAzeG3fXeJ0^2uJ|@TksnCICbv`Ch5wW+SF*bI(RtmfkLPmO8<}!@YjH>RYhGwFg z{#M#%21aVy0%ly{uI7nOnyxOsM&>>)I(m`CAz{shRytlTsyZIp&IX#={yORwI%3+! z42<yevrHI590JuinV1+=xtLg(B*mDRS(v?yK!=+%U`+h$V@&)T=xJ#vE6Pesh>HmE z@vyTpm@t|^C;mbGEX3KO;2PN+>$E>4qpGv9F)CzN7o{j!NXkiQirCuux+s~*M97#a zxp>>#3TuhWOIj+W6xC!|1WL;UaP_xzOcLaC=H|^xjmwdcojXTXGAAxIi<jG(PjFI4 zOTUJak_M<MVqjp(VNzxgWx$;8XJKVx0nf98W&-p<JsD8t0J`&DQCV3T*24xBA<zX1 zpb3AFYn8!cR-jsHgQBj8ilCUNiL8IXq)XX(j~SDk?7Dhw92qP0Oxamo**Sb;t!MZ~ zE;Cf#Y5=<XmxqCYnV(6Lft!J!K?3W1zlJyJd_NBl1A~N^kN^)q4?iC-12+SgqM#sb zz8|!03FIPBvmZX)zj^up=1V!bj0O>Z|I}JLJK0q;UUs`Z!#9#i^6zt3eM3V~Nz1?l zY7;R1XKDh6jhllr@(e#)EM#)un;m>W3TRjcTr43^`pd~kh%tyVhzf%0PC-E|ll~x= zf#&thA@#Yka%jZth~2w;FJ<T5oBg@N+S$dfg{dhsGt+h24Btq`EB|yLuKWKV+!lKe zDi0W+GrPdb1CX2le`ERsJ|k9(;jjarFe59o5F-n#3?m04==@Mo@P0lPPDVx!7Dmv% zC+2uIMrLLW@Qp+avFuD-%;1?RX~@XECIcr6BO_-40}BHo4GxCrYC*<=%ItUsHa2~4 zc19*9b%-_KyKD6rKx+l$K}Y7RsVHbEYDr6piwf{TMzYvgK?mkTXOm1#%pp6_6irP) z<2LHBVI25@`H&k^89(|)X`hIQVEh5SGTAU8*&cL(s=9lqe@Ki8SCF5r+D|6Q+sd_K zpo@`X6aUQ)(ARbg^fZKAGYLw6pazOC6X-l%UIszXxDzL+o#X}D9|0O{N@HMP&;X5B zGRA^t7}Xhl`1lwY_yqX`1^8J(OO=?xv-Y6#l^}};l-bxo6V2CCZv0z2NsdF3lbKzL z)6A4n&jgg%82JN5on<9mBqw)wgUU>1P_KvybeFm)cvq^AgC{=^6B~=T05>ye(w>Wv zoeg!;9yB2fo&nW>&)Q2#iiyd|N-9bzh)IY^h=FFU1h_%V8DUfRW@hH#se3Uo(A2%S zIXigjUQJDz@ky^opJt$~n3MBd#z*sPoMmhS|IO{}SfFVv&%<aVXl_?pT3Wiq$&F)& zuI>&Fdyl1zHx@3u!KWavtH8j_5W>K~4C=>2=Ip_(T?Q@=CT4a9<}^;wl(7bA&W8!q zmIgI`5VQ3{g79WFXtrKh)Y#luSP>lSte{RhJ81C(JG-L#nqRRgH~zkjyBERuh?!lI zOQb=RLo(Tc(b$nm^2Lk4&)>XZl5~)Cku)_Eb-uW26?pzwfJv4~l0k+c(SZ+io-d;p zCnKXUBLkBsq>nDm06iEE+^c4YXJ!OX7c((wftFIrBgulrSy&)zMnU_#w3vJ%9XN$m zGz3MUbIG9PBFe(b!eB?UvB6JR0L|0O#`y6Hv2rp;Fmtf+aVv_O7>P-63UEltaCaxX z(6AT%yM`%5%1kZY$BB`{gN4aRfwA@fe+JOlh%%D|gFEAWrV6BdF8tq(=_->XgCK(h zVvZhk>>XqdQ^T7b6yUJ(nt=gy6$OJJY@-%rj$RowEDIW?6$D*XWNgZ83|j2UboCRw z7kcQ^rxI30Qzl8bf93yR12>HS3>iCJCbodaz!=#7|73aw9s?_YEKcR(10O}k%gE>s z8S-F240(V?GbBLM4Xm+jjI6BS5JwuvkU^0`U!v>4$H2fK#vlS7zW_~_GeWkmp{$26 zHf4JDDFJo-;uFaEy2#@fjG(;62rA#1+ri^bx(-?*JRG3mJJ1d&mRRt;@fzOX{U_?+ zBTB>>L<L2~1O*WmgQw<|nU$eOCI}m=Gq->CXJ%qv@3o4V*}rt<8gWL5&0(Ut;{O5} z*X!#4TgxN?Vu2jM3@*dfnUokneRx#{I|pk9Rt`p#>32x6$q*0PcBjYSBQ3?x$H<^4 zFQqE2Dk{t;$uG&l#=y(S3!0&TE+qtY??pwRZ6ol|Kgy&#qpo|tLu(1>I`l%w&F9`> zVsdg~BJ%QFj<u1k6Tp|BdxCE|kB`rvBrPi|4Z;kdMctq?LY<hDq3$&V_1)MQS=bp_ znOIoiD+csIIRm_eOpn0_wCNEv1Ob}I0_`M53_-Ah8$6&ci4vPQ(k2pfBsP;%XT4vk zQ)hi(sr^5X(Ab#J@aV`0ZD}zvX%Jo|4PGH2Ce6s(Fx>@ov8GFHXk2~)=nl<jkh7(w zrKO=XcwL0~|8LBDnUomRITIMCF)%Q&aVCI@d(ikPlOfY%@Crjq2Q$Q)fFSsq01a;r z@S1=)1}0F^#lRpbAtcBkz#t$39?${jCzRX-UK1b+T03sc_!yF(99&rg%E}m_<D&nJ zAQ>w1LGjzuu8b}dn+-t27ux>?7&n3Mx72gcmS$vTlEK{9s}C7ZW&jT-tLmsKv9L>M zvm$NK01bgbR|0@Gld?mmKR^v=IVQ$U`RV52%4$(sj;;zeng(84MUna;TGDc^n$~u* z)_OW#Ix@+HB0@fbQm$6o)_RgM7QS|I$zl?Y!eUM)8pc|((iQ=b{G<5)8}n5rB?c2l zVMud9!kCep!HAKOTY#5~nT=77k%LW^k%fcROBzx(faIC5$~$06aYD3$deWd}iYi=; zjGT;Y9H6V*xw#o)nVEQ)!F4eQ2TLqFD-#QgI(W_mRL1Ba)PjbAG`tzOxf$Yl7#SEq zEp+gVBlKP~J<uTnh74?+jBKG?jEoGN9E_Z)g!M#5I><9JfExGes!9sdk|M%FpnDuZ zd-T~rO?+N<_>p+Vrp8Fa=irG##CWSI<bEtrd_h_^k79B~MHa(`(`SlF=a*T<$t%U0 z+XT<_uyPHyx7KmikPl%}@^Y5b<-dzOwEj!LNX0o?NYGzEO4BIHLseHzO;FHHm4Oj- zA}w<|lO%&Os23*4&cwjT0~&_%Vgn5WF*7kRXK=8yGBYx2fU>SS_=po_24!VoWzb|E zC%d#ZBWNxNbaNzVS^_kU2VIh2Y7CW7Gc{#mGBME!(2R)CiZ;vE&`+}piiiM<Ff&Q+ zHaC)$ef9VGJ2?e+MYXWt1G~+QWMoCe4}j7U6NAuyJEl)ek_>tbCNx`zpy5s4Is`O* z;B^Ql#s>PjI@($o>k#x9^+;KV0GW<3V+OSr6q)MvY%JuI9UN`dMGd5sH2u}oykunL z<o~G$soOcTG8$W}?wG(~VZ_K3s;Xqj!^9>prJ^Tn%*87rq^_mHBg7-Mi;*$%L+3v| zKE|5jV}T5e45I%9m?nbb-P6I1nURH2j**o`kdc{HfRPEbaGj9>JTwogTp)Y8!DCb4 z>3HzIUoBWt5ENEb1)X0HP70v8GtkI}C`y3?Daj$%p)+3Iq#SM$o9OB(s=+59D<H3G zX=NIq5V3_x(ru@tVodyd*HAV#4;D6cy$6m~@}k!mFN623i~j${co}@&v$lf<FXs9L zaKjt6J^|be0ImN=U!MS)EjYH3ao<)=Jt28PUPGZU@41X-Zf^gsS{SmhxU;ZDg38&@ z{~F+Swlc$stujdSxG2jBz+Fw`SzJ&93o(m}b&Ud&9&mXC>TwZgDE2i9DCU9YZ^aoI zptp+&L+5Ul8I?dS0LZD=#>S$`N}y~Bo)l3Ab$me>T=1|d?*&cUzPhU_D0;O>+xp*{ z2u7I*c{xRY7s%oSIUa#9Wi2MjfBK9U{|O0Ns6v(|fYXv4IBtkpn*eUtk-RoxzrMYT z$ri?aoAi;^CYZUhF|KfP`=_fbk64=^%D})h58Q4ubkO5qWMtq49Z%o|TA|L&$db;= z$iTo93)+qWPB7}sKA<%T!ph*qw2aE2ut6@%)J#p4O%)k0Z#FeGj?#~a&`Y%qj^50; zkMVLTqSy)xu5tsncS9K%m?XjHq#HXJz}6%PGJ*z$K=MosjG&Vuz}ZIwJQ}3V4B9-e z2^%c|uSbAZ$e`myptnSV)+B%iQbYxnL;c)j4FvfGRD_iknHgDS!fm4xBdlz~-6NQi z13fv}U0K<+RQ<$_xb!sRQ~#}Gbn|ddO#Jr|d<M4$a~6{%fwc)5WUWm=)&)=3sB06D zm4Ld)46(394>;B)AZuU&?ZI;(b8P}>ZvhB{miw_OGg=xN=;<>S{+pvJB+8g-Zlz=O zcVWcKh!+tyivF(fgs!6nPwAl4E~w-UPVA7e70|uwOl%yW10GqC+UY_JVhmCY@(fN6 z_M&`DObpVJ;I(}0%uGzI984^XOe|@j(iGf4(_{1z7Z(<0WRRB=mlBr}784c|5=6Au z8I8bM60{q_)Wi(5#NXIhO<j~-OxzT-{ZpCI0Mtk?1~u1%ZSC2b4)GNo<*0VDwPS5y zQUbTq!R>V)jsUlAp}C%Ogx0O&3J0w}W@0e=|BXqF=`VvUgBED#wiqMmN+(8Gn_Nvr zj|sG~KpZ@93c9xmQj&ufOPK31f$oK521`K(rI>iQGN$;;+8HNjbryQ$+owb-S}6K& za@6!S4D|E~_GkL*%Oj#7Cn75^X;2<lRAZX%6ICW9q++OS;-v9!6PHeay+>HMuNUZ; z+5i6;wEtH!yE9#3(C5@+VrS@($pWz$1Yj)2PR2<PF-C5f7(*Xitn+`ZOco<QryfHe zh{a&Dk%4(b2k1~{W+=_Zyn}HPNPrV6zylNDXWsF@79=ACWq|txtC_MHr!lKDu=B(* zDuS9L|Nk@aFv>G7WJ+XoXMD`SfNgI)D2*`6GbVu5JjJet0la>Rv5)B#13QBi<dQVd z#s&sPX7Eu<7NADFCFlehgeW5;cw)$c(I;{nXu|~yhXmx1PGdo1#=gr@QI{G0{%vA9 z_3sj>tYMI2U|@=4I>o@v-~};X3ba(8fsu)!4m3Cg?)+PT*2!3cb{$B<B^ekPnh**Y zL4z3JUW@=6hm<yGLA)qv!?&`jGGlhsDo-W{&s9t{4;c0SU3vgnTb;zfz_ftr6cZcc zMzA{){|hjsF`Z(NVn}lkmk{FRVPRtCW@Q2$u>@MiEXl|OS_y0z5aA%s!pO{Q0cvJi zg1is8N68lCI&lWrISJr^fy+cX2r@E=i-E4?;$&x#Vw7UzfXpF7R?C=~GqZ!H&;`vv z*F1o>`dAr+IS6Vd+lJ+deh-d{5>yrZmig~3qlCc+0Wo%8At~oT#!T0Lub58#n<Jtl z%qaNp%)hJ;V1NDJ^xuhL8^~V_phKI$;{%KdOp*-j3{H^n6#;pdiGeW{<PB@EH$a0# zaACL~tU)6~JfQhH2Jk^U9OBT4Q)R)mn>R-=Nj`A>!Xyc5hlnvSF#crvjW-Uh2*sfl z);I)>?3gMunnqUnFh2IFVA`_e_3I^|vlc*gm@atE=pwlP8~XnnlNZxp22lnb(8@#5 z#55zQbHEO|!kr1UbOW>{6yC!J6>MUli%vm18pU+Pb@+K1L>WavtC*mL8hj0|nkndT zNpU66EsO9DKkP1ZMi=J{Yd^2(*p~9L7P}m04Hr%S&|tp=KT~Z#4?m{A+93`eAv`Qx zNzv(fdFhNkfvE<bGH&+&_PP7`xchs0dD+_8fFgv!_Wuv2DyFLp-VE*zE>et4%<g(j zEKE#ZN|HjnJglI?kC_pCjcGGz<4rT@yfc<?1{M}@B4c6jXJl}8vbK_!l@Md_X7pwQ zFH!&(grH4M;&#mDa?Hj?=HlRiCN;=>6S$^fS2wa_GBp96pk=NEE;2#WtY+ropc8QF zEsEkTr6r|pt;Hk-ZCosPnfQdox%tF61oX7HI29Gx*(F8!_|3!gSy*J1C4~fe`1B1F zgQq34=vqs1EE3Y<&<ZT|4$LtTQkCZt=8=;ZXJJ><lIIrV5av-f5Mtz!mXNWrlh?La zW0l|%Rg=|nG7^*E<I{`rOX~6vk>eH3Wnl)TFUJ2ZOlz3VF{CnVa!~MKWMlOgWMW~` zWMpPhRFGf+rDH}0MpiFJ2McCa5AO|}0TJK<5O&r)1~zuKX4o7T12gFSry9^I4@QO> zP&2rRft`s7ynqL^7nz+YK-$3+JaR&+zDNggMuxBuS7%#mbyZnueqIhXhE&E>PH;4U zuNE{mGE>)M0`-zW*9@|;v+FS_gNCft?U+Hs8|I)(>Ot`ZiYX>zBhYdhP$yLl8du_W zEXGE1OyY7(>}+i8e9Vm3gf&G>_@#uEEtnYPZS~FF6!-+07@3&*S!GrD`4nYX`Pr1T zwfMy}MA_Me)Qr?rEo}5Th0RR(g%zbZ1#E*^g>5bQxn)IU#JMz$43t>ewXE!<xp-L_ z8O8anT$Olg_yk;qMH_qK(li)t&E>2mIThH~vdKycORx)asA|Z_i*j(N%1U{MMQO=9 zSQ{y_@JI^_OK|Wzn2Jhqa;qE5in-}=Dru@oF!P9u$b<w*I9KTyn23XpeP&Sj|AR?} z=>mfcgC|3TL#Utt6C;C_B@+`fBO@!5vJw*uD~p#RBP;kGZ)OHY<~jx@W>#h<);jQ6 ze=IeiecVk9ph_qlwDv9@w9(LCPF7c2)>F<?Sxs4uolQbpSRE2%h_Hl&xUiC%8t9Bm z&;*PeIHVz|-;T*#oQ+LX1bjON8#^D9u`rXcyq2-7l#Pv<u#lz_C$EPeAE%a?nW&15 z<}^WFO-@6B3>GeCW^OT6IU^GxE^ZZL10EhFB@RvwF)K3_J$c4Y9Gr%>mTF?MV&YQl zLgwZIq5`_Mf8U8pb7%|Hu<|JBDH#Xr>&6C3ajR)5a|lYQ3v$}{`^%`gIH^jCv4P7q ziT~f3y_v2u6fv|jR6A4zI54p?X=^btawNvbF|#uYFoLe6N#I~-W^iX<W#VLF<*Z}i zWMkrFt6^Z{U}EE_Vc=k7;$W;{0A1?JUc<o7z{Ji_1FAh4m|2*Z!x>nZn7{=IQ#=<V z6AP1nV|`^sW_nCiXo#N=C_U?`DRZ-lYm0+Jk6j(Sf=x`^%-oLI)C4RB4KqF_kR(V9 z)cP`qssmASOrp^K3aEt$3SUqWV=m6F4odXkaXofE(D@X?2B5pgK;x_6o;V+qxER|B zHW>we2?cQhF%ts;J_Q*LF>x*lF+N2TO9@e3IXN{sX?0a@0WK+F8C4l6MJ`?)TQyBj z6K-J(A46p|K^AUaZY6F`(<mcB9Ysl1eg$^EJ{Kl-IRi;m8#WdJ8DU9V+uKTJvYpa0 zTw*dJdRkJ9jBK17(qa-4{L<#C5^|ipBAkkLX@1`64l)uNlA;<K+7f~q>hc^yY?89P zlBz12G9vmKT)gTE+HM-Mu11ooYJ746lH4p361r-P0wSEvlSDWKORCtdw4H4P_!uR9 z0u&_74H-)WMA=*=l=#JEw54^7#dX2=OLG73XOd)QWC&wuacDGUWaCg#W@2Iy;o@Xw z1|5OT%;IHk#>DBtz{$bEna9A+#>K|YRmZ@^%ErZ7!@$bI#>!H|z{14F!c@b+#K^`3 zigac+M&=p@4hA-M4z>)Wb#2_xb!~n=j`nsoMh1F1YO0Fz5@P(k?5qr7jA1-%BHExk zXTY1@KoJWXEfE1Vw2h5GF%RnJv8#g<0f-42uTV3EWFS!Ff>rY|vqPg_gpcXGxQRHY z7@rhB55JDNtdKM#BeRUSfwQJKN0^(E5jT&%n7NUHniY?M0k4W8hqo{*r<%T+fuf;- zyq<wF3!9V-r-7XcpG90*Av>3qAup#emy9T%7{8p59KV`_x3|2jw*VWjBcqg#m7<Iy zD+{kMXATdmAO|-uyOxo<qp_+8Xh)j7zOiurq!7?5bHV=tOrSg4br`xGq#$=9GBb%X zGB9g1va={Kva!oEva+#3_h_ntH*m0k8oewG>})LT>7e6TK`RPcKoj}kedFz*+kjz% zB<YwM*w|poz?FrThKiD`w73{_Nreuh4(O&PVc32`#Kt#cV{G@XDJz?rKhDTX2~aYX zm6A{v*SB%<efg?(T7QpSy0&4qQBqu1iSrA_IkAz!aopSvoSfz+)>bwzSW?o`%0)zi z#U%plZM~d9M^iE|GKl}@W4zC_he4a6$3dEdk%^I$k(o(^k%3vBk&RuBk(G_rON)^O z)c#cg-&+OR&4Mj3n#c_dO?72O87VPQ&_*aW@TKOEz`$5u0tpK-HgzR6XxrJ;L=6-c z4^PDft60iNiK|KJ+Bm!JzS`Wz*i~&2tELmFA08Q>?RttaZACyF50@<`hq;NBp7{mF zw3yf;CP98bVL=y18#g=9G3cPC!T;~f&P-Pr<}tK7GzBoSa7+#f^iyVKV~J+u=E=)t zVqz3x<YHp=Vqjz8VPWH`W8mRt;o+`f;AUmvW(DOm7B&tRP;--=krlkkjfsnqDV>3f znSm=DDgSZug04vPXJqK=XlW`hO-u3da<sQFH#SsP1ziKj$<8p3aULHy6M|X=X2#%- zHan<+2C1|_%{n_Kb8zViY7l@+SUV<DutIPv5474CdV{4IC}*;RjbLM!V-^(wD*$zt z&CJCOK!$=cEw~K=KFtepb1JxPsLsa>ZgnutG`5lBGE`ELRuJQn6k-$6R<Vop))LdQ z&|;A{Q4%#%wk?d7;WxEZSCLlscF^Q!<&d}ZH!!hMx3Uzrm=mqxEF&ew!6Cq|uWlZs z&ZVj*rQ;E#E~~)9t!v05t12taD`hS%B+eti%BR4_A)?43AkHdcsLUy1XrrMh#>&qj z7hTUOr)MOhVa}+=uOPz=nx$cplo1K+j?fK`G-6ZL72#l$P!Zr%F?G|@)X)pcwHGvy zQ4Mg@HZjrmw9?>K)Z^ylVH0FxV-|7J(DyJAVir-c)Km|2)sj>Z;L@>C5)@H2kd~FO zG*t<(kdp(QGQi5o&(6xoX%%K9YAht>mt5*7s3GW(VaLGq|Ihz#ELKd<7$!0-W~g(h z4rgTJvD08;XSHQyVegEQW@coOV`Sng7Gh#%V)J6)0$seyRL8)<z{AGDlMY&F&CbRO zT0_pl$j$;<x(C`#lg`P=%)r1L&dJEk%n;8FOD6N?v^3S$l$WL^2l~4@TUqGqLX*iv z#)-(u1iZo)IhnArv8x-{F`0v|^)%pP2FENoQX#ER190;b)Dr-&KLHi)=Hh(J>}H?} z9$bpEfllmV=VKN%5Ch8^^Ra*pF*66J31&vHQcyMoHD}q_W?DMRu*1^`ySSmYg_o_W zeSjpNl%caGw}QXDypffOkf@}Xj2H{2sH2Z6pR9qEjg+Ja3!8v|6c?AFv$uyFmx8X6 zf{3b~f@(}klD3knNnW_HzLbovf*`9fzqWynBD)B?vc8G2sG^m&N-iT46TeMlB@;KF ztc<dstByE7m%fLtxE7bXnLek6xjHnh@UuwCiaD42iaW~iNo#2Gu{ihn%4llJNQ>}l zXe+QXvFiC7bE}$}tBQ)VaLI{_D{yeh8QIv&@G7|kBs+5RbGbBUJ18s4L?@|o2y)t$ zCA)A7OKa$;aByiD>dIO>Y1FtXv2w5&XUAGHE@I)~;dk)$loS*Yc6L_*EolU$7iLzb zs|<Dw{-8ZBE{trfx{Qo$ps@vJF9tSNMpn?xmn;mdY@oSlMh0+tKq@rYp_R6YF=+EB z=+<~nb_P2}I}W6B0-OLKbLOBtZ3Z0;0~G|KkZcT!eRXp&P$9t1$7F8DY!2>Gh>D1b z%`~-_2j_2PaT#$zBO@(WTRB}T8SA!u#|Cp_M*{-|Rv~sp2TfKXRvtx1Q`v;(8ad~H zL|aKKaod6jV`()mMI%E&aDdD6^7E+6nZ{aknL5i`<fLedi}N$vWKE3ZSLf%_b~jZJ zWE7MWv$Qhgv&wLKZ{5^Tq|M65A?ll)X`rG8y0eObnSuNNH)b`aD-3=NF$|duSq|wk zj4aHWvP?`Y&Wwyq2|m_bENon$J#;LLEKH1btc=W{r3cL5yu`)E$d%5+$pku~EgVF% zF@Wy^WMlA;i)Ca;Ns7&k%Zv>7^02ox&;xD56%^oLWAJ12;{^vMs6PxI<~B0}<s3a` zP-$vxB+jm;4DKb%F@bwzkWd6=1n}Lz;L;S7$3P1m&CEd|4OY)+1UeQE8Y{g@)~dFu zY9X2e3c@U$EKWh%{3g+*4!XK5x|&wWo*D{9=Hk+}E<Pc;+G_fth0el~?iz|V5{e#< zl7hmjW-c0Pt_s@Wj80BA^7cBC!m2h#2LCE_EJankG^9;bjJbqWrIZyEM5R2mxrDix zwXE$VO%uGeIfXd&{E|#{EEIT|Y-$UmOtj=x(sN=Yjh!Q91trv6blDXR4HYGYB!n5A zWK5;QQw_vqTnc;tb;!tyDTzso$$(a_fcCI5FfgqEj|YNQH<~&aF)*;RGJ&?VrGj>Q zGO~n%dS{HW9L!9NETC-*u#@SbH}`R}fetZ(Y&lRj7F1?57F14Ta$ozl!r7T|ma{W> zJHyn!*``bhe-|););2JL^GzJn6^2=$Qnrebox85O!k2}Uy+wkFk!czuD~l~74=bw| z11CEZJ10{e12+RZCpV~##=ywI#F);&!NA4B!3Emh&B_DHSv+hEJmC!Bl)(TxR*0FG ziGd9?gfgM8tRy=#DKR!W(BIk7+6r{!m6!-WH^VH(S)fuDvOWbIt?EjkHZQb+0%{S% zD@kP~95ptmpd_QlhOQKbRPfB8(wV71L4{vJQG$($Nr0O}N{pRbf}2OrK*<29o(x4P zhB?G^RqO(sG!??_kZWpd2VE6uB_9V(eilwhO|9!_k5W@xTIA<Si84z{a&hx0Dl3RF z39zde+i^)K3rf4X*^2TBu&{9P@v(7pFiYq;>&7V{Rgh|?dYtGLwR3HR^9&zEJ+0&D zhfz-lx*8(Y(~|xg4jED++DaU<UM^yMB2pTf@~oU(rnaChMF0OYK+jxd<Y&-iVrM#q zdWI_W-o`rSJSH{<&?QSu3`YOIF}X2aWsqV}0IgO6?EsRI78eraVPj^JAhZhzbe=wB z1Q&A`5NPd#AgIVS7iD8p7X%#(#aLG`+0%b!zCo;kR6wY<x4d1rW`vf0*oR<EuBc@N zIm=>sge{bW1^=Ft=jFEM;;3SL5<Jfz)Hw&A9R$v2pj;*lIy;Dok%1F*pB+1BcRC9j zBTFg=BO5a_TPSFkE^{mwD-$ytvo8Y!Y-1*3H6nPE4`^x$v{_485T5JSzFmuy?fxBO zwEAnt82xW4BI_}L&kAE&$#jZ=lYy5(%|V%$hntIonGJdrH4{TK_<o!c1_lOB22MdC zK~`|~Hf98^BVrU(HDwe{^kF=1>if@%@!mfrrc?jK7+?K0V|@Kj)HI3lUovR2f(5)s zItDxkz|SDe06r;6QiO?>1$4|R8*GOW10yplV=4nP6BBbN=yHHqc19*<(5^IO534G2 zu!(6K3o3%ELC{tskZ*;J1(k(CV-4zwjLZ!cPC2&870%8T-Q5RO^n;mB<wo1ZoBg|J z%9QZ$Jfq>i2Z9bZ*)pKZmu&xsF*7h-We{MHW>8|#WY7oI+A7LQGN8j$IG9;<_(9vC zycih4=g_8efbT9~WoAfcU}a(g?}uUnxssL1S4fb7L0?Z>OF~RgT1XnS=1NtNlTA#U z6>)U|xD6(*YGf`5YSXg|im8HJtftN=2pW|VV>AXGx6Ak-d-h!K$+_8+yrVs@{A>3$ zmvL0{voL35*VWV2``4?dtIL>D)4{93ziI);;bpl?V`7%(H7`~>azUEOgO^|S?=Bg8 z&F0Y1Ce42gjQ=#7f<v1BeXnBy?doM<1;@i~rc(@{Rl}kTG7O3gcOavw+%jTJY^)rN zOl(Y`Wl&rJ5e_gRPLL3s2T}l9>;xKuRbyaaX9S(AkqTOx#KgkJl+MA(!pO)H3b~Mz z3sOT!dv5@lr^CR+$->E0fTW3mlZlNJyB-H34Udd;kYHd?RFIPetsMnTz<~EJiYbDI zv|z~ubdMJ#Tfh^Fn!2ztGaEZNxj5V2KfGTXo?x^zUA)%I$upfwj&_O_|F??K_TOPp z(z(m1{O_Hhjg7e_@88vo!fZ^Sw9Ukz#=yYj56wU34kqB!wV9YaLEGdQm>3yA8?2d` znL<JPteIok7@3(sc^H(-Kp9F%P>3Crp`aU{6h&2y8I{!-^ZspO;`}$0G4`LVCu5eo zDN}-}>E8vWrr_$5frUYZfr05X)J{<bY0&;oX(7<?suu$TD<f#J6v$f821AgwjInHt zjNl!HqQU}vP^&o<+1W(3jfJ6I0A+P!VP((+g0Px0G%Kl0|Jyhzad-O>=SFAFkZAWp zL+`AO4$i7fr+OyV9Lfh5B1%ls%70%NFNjO90PW;V1g|>)?Q`S=-5kciz`_VRBON(Y zvqCd9CkHzl<oYu<NQM?w1Q%S1j642)tpGI>Ou=mg2GCp(WUlxM6B~08m=Cfi8$1UF z+T+W_AoKqxQ!3L9hD3%OhEj(jc{wI#77j*k))X5iF7BW}CUy?cCSOP<W?*JuVr2$T z7;$s4a;Nh$axt)Ra<Qc|a58YPb8>+CYp^}PJdB`CzsX6A44D~8ImtOu5q>^yE>;%C zM!MQ+Dw5&?d<=<<iJ*pqIA~DQ1adgC31s9Md;|e_xChiZ2iNXqppj@$zf4pFa!E62 zksTlCa(YnL3p7guk^~J6freNit7Q3@z}M5Nt0}RIi9NE7G;l3+=9RThaWJq^;*~H^ zRZ>ybWffMJ5>_%))DzM7)@GA7RhAJHwiH!%G?r5^meKIFS5uLewNYUgVqsFX*HBgF z)^LreR#4^U6lWKZXE%wlw#{(m;xp0G2{r|Fz9lR*HDzUOgt!C^<YgqyjdhI0%o3f& z1sOSo_yxE`M7hoUqV!Eoh2+gVG*s2iqD)wXRD?8~G?aDubp5Rq8GE@nc$k<Ng+UEU za37jEfaxZK1w%VSpF_8ZFcS--p#c*!Qwk#&a~UH8YZxOtcTFS{2fGg=CkMM%1|tKb zCj%EVD>D}>xTVLy&0Ys8fteUt(m{KHxR{vJ8CZE3L03tFB8!8Qog<xrlZ}ltoPm>* zEuNQ=jg!sa%94?xsj;XaD8S3p+0nAys$D}(URFv%ke`Q(!Gh5O6lucXh8(DaEXM>o zdjvF60&3hrdH|pXKd9XYp4|a;rkIsLy#y>HHK6_#xDO7RV&G$92X!bwrizJ*u#53A zv9p8v=SuACd`y=V)dg8)tYR91jUDv#lvI^@1qBrJ<<wl9-89T{Bh4h`IVFsAltq|W z`2_hSjlHt<tQGlLkUHDC>N;+=atanfV*Fw{t_CvZflj=tY7zpntb(jeyrMFKyj<?A zEZp2oegdLGOu`Dx@(Epxrc$aZT#7-#ZrXf;d_sI&GO98XiV}MMsb*@b+{)&THey^7 z;_R|Ein1<t>i-T(YicXAYaoxnNXv_v#aTPD8_M!XD#@@|rN--XsYIokvM{o-vdXJS z33AF=uq&x6vWdxPY4I_0g|>js;03qqS(=!xGE_4-IM{l)Yw@#y?hq`BHj`xmO?Waf zHZz0Hre_KVEoNm%U}a$gZ9EQOWJpW#^>%R*5n^X$sAjBYhxFJWZE;Z%(0B!?Giz*Q z#{?QB6X#=P=VM{FV+MsgXkLwvnVpYC6m$iM8FWI_)WnY2+>XVZ4?MdDnRf$aF;M3i zq?)<O&cj$kQCw4xmqSQX)jU2dT;IMXQ(1sTLP1$xjG0ATUS5Jl!qVPOM#omkB27+E z(7?<{h(}39UW7x)!cLk;(!oJWNkdXiN1caX-^4(fO)#X=-8NEL&R9fDgiXTTO~}AN zg-tOyGC)ovFg%^h%Sy{ySwviejggy^O-ar+way`IN|q!Sm#K%JyAcPMg<oiZj6`T! zin@)zicPSPfJAU|f|{gPNPwdNpPri%m$IjkxreNGfVYi+sHL5S1{;q{LrU0mQ(ap% z6JgP~896d;&PE&(QQ4X5X5|@<pm{U~E(Qjs*-WPx6c{uaj2P;-@`FleFUYa#8k~$Q z%<Rl81+0wV{V*(`>x9_YK)cf!n;=_a7?_xvnS7)jG@(k_>j<fhbdc22VqnlR(lXT5 z1`Pr;C@?4}>L{vm^Gaxg4w5%9Gd6~vcFQgbnmGbB=^*`RarlNaK|#nWA7xQiHdhb9 zGi{x2PP!i2t;N4Z<>lC!7q!H=1V@)mi1YSMe#))D<6y^V5Rhme%)`o|=4PIl#po|1 zAt=Yb)!NG1_}`^}f4yuhTtRg=<NtU6O&H&U&&r3Kjlu-lT*2hU#>fmh#0Rtio{<A| zlL%;6FL?h_6RQs+c$W&Or2(o*IoLpVv4IA&L6a%q6WmM%K^IJePDV5qRTdOs(-&3Z z;*t@xDJo*@`*-^AVed2Gs|Q>dbA1_oeg8A~{+sIQ0dfx$c+Yt~(-j6O27QKR2R=bY zHdb~<b~bTF7Ix6SIM51nMFutob{01FbkJt}c+eSiUEZ9Gpk)Hg983%h-JoSEns7Bt zOiiF2k)R#wj9sARZp~1|pvm!W2A@a=X&Gro1|2O>+(_%o=!=Pp2=nuTmauT~LYA<A zcD9<rM@x*&jRlp}mD#|1Hy~qzpqdT3oRiVgzuYN0(%-v2KdL~Eg~!iRm`x_vE32?L zFTg)MT~q677MDS+lY0~&Gk>6KaH_p_!e6sQEl2ON+M?22J0p81uO<&CL|A~%?h|3q z1oe*C7{T2I76z1k99`fYdyuUs-3&frqKpix%1R0{Qlgq-ntVJAB8(y&5PzU}1Jo1; z`@lp^9dc9_)B|FSPR)HF4>Z&lO!4%bUQ|<7S{WD&cDi9OS6(92=|O4HOY`!U#`;a` z>734JYijH0)ns5T16s@A_5T}FJ@_oWOb0GjMrI}j@cCtsy$8~u3XFw`IUTgJsmU9> zyn~G$7H{%svTSV45GA1IJ2<vPK@0U{WhBK#gar9{xEYigmEry~H`QY@g)DZ5`UaG1 zK>ajOd@))TG;A>o(2mY5mDjo&<&{|+=_$k}lf#u3Gf$40OUvJ-XF`OxSD&q8SuNu< zaIAphfrWwd|97S)rXLKTRfWn7+6>+f9;yP|%q;ATUJOhOtc*;op#Ca53nP0v0~<2~ zTR7;DJq8wL&`G`EBRRnXFADPDtJsA>gGHR|3_^@TprsSw#lWB`L@{w;V`gP@Ha2r( zVP!_>f)!AQn^_n<c`?f&P{}FT!Ni5zY3-u5V(LMQoO_)9&C?IGQ4}`RVc}xwi&U~z zli-Ywaca(WG}mA>`^%APVr9iN>2HQzQofObwHhPiKX1lmVxg5jde)$QLY)7<F@9lE zVi0FgV$fjF2klSO(vT71XJrB%Fwe+<eo_ewBLfp?2uVRsNPv@_L7Y(>G(>>#0jOSL z2M-Uhv#~LQ=DF3BKt2!$_43pig~6S?7kPzR!4-ZU3H|fb<@I#qbXj#GJ!MS9Rav-L z#I;4LRW$YgJ@T=16ywT`b*N3Zv$1nZ;oHY0C1x)y!f3+8$-0q|gQH(SS6*G{Uk+ow zX1j;F4Fd~<0s{kM7&9XS7x)}vS<tN?pc~9VXRUC876&k}Ffo8e$3QcisSIqatZbnS zY;3HtT%f5xUokNrZpe{xkYkAj`ME`TMA=yxxEQ&(*+jHqbC$3jTae<JS=g8v)M@yk zq-&(_V=d1wsURXMe6Da^QJ!;*GgE+?s+NtrvAL0in7p*0fZ*Rc(4ixSjGGu3SpMh# z7l6B0ok5o&+#y7Yk(FJNk%bj}>kBySAueYFxtxuGot2F}m4TIog*BXkm6au)i;;zu z#a|rka$OxIML8KUb#Zm#T+XNjZqk8Te8R@eeBfq-urjJY{#_K&5aH&N6_nQF>R4CF zu4*4^B4^H<>4f32KYi?+rfeL_`b_nI&ntL2Yx#;e+Aw|vukBz3%|SCsFo-fJf=<?y z660cF0VOZcJ#|bijG)mdhEULf8lY8>44{soj5KI7wxlR4o0zsZs6W97zV!>l0=bn* zR2Z}il37_?j`=dLIRC%@+w0f`nK(F^lPpj0i}CQYFbQ%mbjjzGk{0<O6B4V*B=qkG zpBfjFq%=Dt=ijfI(cwyrIy@$FjJ_{~&8-a?n89_<UnU6#Sq4Q06NYGq2q{K3(D4T> zpqp@57}-EOu-O>6*x9(!c^NsG7&ya0OH3HpnNoQfIXM|X8<sd3{8f|~8T52D)RatA zOcWL5<s`*HH6&!8G=nUoEU3B7ssx&lG#6H8Ru)z^H&<j=W(H>pV<XT^lPWmpo0*$~ zC!av7z>-X|Hi29$)*WeU(~4RAnJT$+tXWwk1f|5KM6CaLImb9Ndh^L~v9lP;b6Ppt zu(BI-xmt^uiZSy2lVyA?q-^@{lc+4W5I<K9JBKynrhl$XOsZNk;_8fi|6Yn{i>pD- zG?ZjuV3J_^!5{;=YmN=Hhzxw(PBSYD6B9F23j-rFGh;XdBO_>6EhDo(0|V$3GF35k zK{hrCP#+pJKLA}LFT%#otSk)b=z%gnvt&htGBYO&n@og3L;@3&dAJ#~u|-%gpAOTH z`@$*$8tOjYT;e?c7Bhwm3FsN7#{C0b0nZ@8z`&%&^n*c(p?E7VBO5!T7v!QFbq3I> zL3x}UOl<6IprgB47#Ua@8PXV7+1bGv19W>VE4#n6gBk<q_zHpwBOL@87#NfolvKpj zz^iMy*krVgg_X^X&5hZO*_GMZ*wOt3_LfAY2d9@%gwP4jQ-U1atSWJi9)Zj(&Jhl5 z@!?S-W=!#R(b0CnDjM3_&ThPNg8$Yt+RI9ZCMN#-2F@1}|GzQrWSq#L!C(Ta+c_AS z!Pi`ZPNidEVqs<i_xC`%Bp8_BCq=PB%U?r%RTV{fApr&rMh(y^NO5o{&deONh6OYU z241laZs&s<CZKh8#(YfbppG1<TenxuP==eEQ-_~JQIXBO-C0$UOWZ2WpPh?OP}VO< zSxi1KM%UU$TTn{UNLQ6hT2n_rMn%+=mrFy9-O*19q}a-cpI<{*SX$dhUn|sdzOaph z36Hdqw?An71LUvUj1xiU0)lSh;9z88<z!@GQWs|eZ@pk*1szsV#{gQW$_!ej$jZdX zn$8ZH4`v6?2eW}^0992O8O%)dbX2TWt!1PI_`vl%xDy1Pc`yep%wd8I53sSZ+cB9N zi-3KsZUmZ!0{4@|LDddu<qkWWsK{<r1BkB!<a~nEwe^H$O%w(64Mc>+v>ddgZDfTt zMc6o$gm~E%w59lDea*EIK95&%(v;N^kqe1ZWf9j?V^TL%XO@%~5))?T;S!N%Hq6&n z6^hFW)>CI-VE(`QUo}%8V=sd-gBwFAL%Kr>=qzD26>)B6Ru&&d@UcV8Ol(ZdY;_DQ zY@kWdI?x4NEKF?aoQ$jtjO?t8>0FHLpd;6~7(r*Qae&8PJlsu97#Z9{JwoklOx#S} zG&Pi!<Yfi;7>pT>q4kp*xPxa7U-e)PT6zXa4Wj18%%Bhg_rv*^g+VC-6gJ9Apgy7) zGl*}_$0{l!#;K?!Y#JONE~CVuVl1bk#w*I}$g6BEs~l&gsjgRcPTfh5ON7-p#LQ31 z$UUM$(aB0oKtV~=z))9NRA{M^siCAlyS|f~t(yG5Y9$>3MHfd)ZC-ayc?nGmF==x_ z2VON7B@Jsu4Tpa-#H~zudF(x9CL6kY7&1;U57ANbQqa(L4e+z(<zxTXr0Zuii$zky z*v3IY9<*>9e6E=l<9;S31~mpR2Y1k{hB)Y4GgU^=A|6%-IYt)H1Q{y>3oA=9Z0%An zsKpO&95XV2*5H7K06Ezi)EL#+Aq`_tnGBu?1@HHP<Qa3wW(&v)98*&^Mjekld&eRV z4POaq2?v>ZzCPM6GL|;N@?0EJ0tQlulg~H>PJ<3UTjc1%!Q#oxX5s+4D3p;Q^uHC8 zE7K1K6^2F!J_$y4W){$ew&3H#y*FrrmmIM(GcmJ+GCsV+)dF%o_$=*SZ_qF%xa+CL zfUc048LEbXl@)SoLo;YyyEr3*qJpe6XjB+<MKT8)g9@VxsGr6PTUlgksw@aPssnV? zs<EgDte_J#78PM*ya4r^Nkx=%L<B3FRHR8_vX133h_~Dr!y7?mHPes#8p@hlvFZO_ zcRIR&+RLDQEk(>cV80qNEO!u9VPaxuG!)`vW@DFPWMPtKWMyITf^-Iy7+4rsSy(}H zBrTv0Hn^A83tD2u6b~8@>1F~Q9}Ec|>?)a=n<2^>n3zDS9n@e(va`cXV_*O^E>IPM z&fikiP*D?6RTALfmC<H276l~%P`3y)BB{)-44IQKHUiCcfh%M)b8*<(cr|4uc1Dv3 zUBj$;%i<gxe=#;;d%wsq7i|pzGwJl2_VNh(P*)%I2v0*DrXTm%J!<`1CmAYwFm6_x zUYwH7>&D8_+dS!**2m1O%oOVwV-8TbF@o*^VQgml%OKBC?Z7Y0$i^xHTAafT?fS}s z5(yV46Eo-<5YRF`=xVzbZ_r!=dn^Mxdn;(OMl++2w1Wx*D+3!lD_c5}1`K794xmK{ z@(l8zeWT#IkDFIkn-OwcgQ>BpvZ*<^AQb}*$AX%?qQ;`iqKvi`5wWpxanZr@X2RkE zT4Fld?d^<#4*%XW3OY1v%E@O$bF;g$a#`CeyMXR*WMa_%|Bdk%lQO7n0qWC(d<N>% zOF}({y-(i@?WSYz)3?I=^xzzh*{7G2Wn|FP(biN|mNk+y0`<HE_!wjuWk7v;=t#9W zq)!iyc5yttdeAiI2a_msYiA>K^I&arS4|#f9#;opR?%?1C>QTw3mZo_J}Gw>4=x24 zQ)5qFRz5o&BYRm%4H56Z&wbSmJW^uCvf{MVH8h=!bhM4YdssksiZLlNDKW9Jq=WZH z*nq=Gl0lUr(}9bfk(o&eSD(2R(jmstXKn@M1}uGMP>WPrQdC$-fR~3sl~EPcXGRVO zM6Vg-UF2@_XX7A4LvurmV0&*K5oZfKlL)sEM`0GRFfL_RLt{4qR$eE^oHR#su;)@@ z8KuCU2A$Ev{QnzMB9jt>7K0&!1;b1SJ|0FUPIX3hMny&j_-Kwg7b_DJ4;vFBI}>9X zA0sCxb3ACbL$5bKHxsx5<b{n5Xd|m+U}0(XhA3xXX2wt*>7Z$Bq@&HqU}kD$VQitV zr){WXsH!XrIXG8<ivxVOhZduj0Hg{~S2l&T^29-%3u8VeQE*xZ=XYoU02)GIR$^mU zR8wO%GBZ~c6Jr#PuV6JX<19<I_LpK~72tN|;ARU5kPze$V&PB_P}S#V7xy)FbY*8$ zWpnWjaAW#+gV~j<vB9pmN=rjfgI~-eKt<55QC^aT(SemiMNi3q@lkL<K=8lx$oQBT zP?^BYp!fe9vp@Kp2@?iuP<ujxmx+m4Qiz9{kx8AAgPR#NHOtA($O*b4l$(Q%JC&7@ zi3t{ld|bTD92^Ys>^zV_G&9xKVq~ziFts+bHqg^D(Kb<4R*;vK784QV=i*>vVbEmM z<mUx1i#9beH%B_hNgOnzBPI?xp_^S1T024q!NBY4#8{2YOclk%Zu!Q4OUzUCQJ2+@ zGpx~&PjWF0($e!Y3XEiAEGo=!%92)2wsr~f3yL;n+~(r&?>SolSCHRgHfGgw5n%^W z@mL*0L17OOq2y?j{5CrqTU*4zPfT%cfx)0~VPdfQ|B|tR@h^ikgBpXpgN(AQw3IL_ z=pHFXMn-oA21bTv21dpbEoso@x8mxc6=I;_FLPsY&<as@b`j9jhMBpzx~RFCsfoHe zAJcPb78O@rPVtpu3Ct`!N0fE=Mbs5U&ZV%kxC!zxiSzREs=C_rN+<{v^|NrWvT;t4 zQ4!>o5qnbU#md6Omdhh332xVe(!4Q~GJ_(6I)jIUt0W^UixeXps87!cI^~lol^Jx< zBnvA8ODY#52M4T*Vr2za;oWRL%1Wv#O6tn$LaIuFqC%<y+`KZ{=Ef$Vp?YH@F>ysu zmrztuR1x9<QSd;%x-z2@BwbqN_@orE{F}tY#JJ>NGJlwZp`X51ut|EekR_u^wgMv; zm)#dGuCRKys(e0KK6W;92M0kB4-tX%1V*J?J$p#livRz{bc;!eL65<dVL9YP5MfzH zCI$^gHYQC`CU!P4Miyq!g%coARy5H_hz1Q#P``|cje!ZY3Y49hjXfRNzs$_w^8vb9 ze54(88CaPZSi?C%+rilwm_P{`kNQXlVFP_*BYjf?Qx(vWH6kiX;2;8@M*~TvVxp+Q zqoxj8ZU8CQpqZHwl99!OU8D@8dF$d`lTH6kWQq(h_Se!3F!wHUW|Gj7QqxyfRuSfu zk#uopd}{B>&M3%ecfi)2l_{svCfCz6I$l`FS4hyNB#engK|w~D%ZY{6g)t=1!v%B{ zE$Bi^1_s7ProRm03{ehTe2lCt{EW=tyQe@^xeRy^hk=Eq#T#^-5_n>q8Bu{tftoSQ zEUf96lHgmXKsUDuu=7eo_MIx58l%_Lj4t&N$;lz1a^_;9!s=p1s`ZSQ|LHPbcGFG? z<>YW-Ww$ZWW&oWH&CA5c1RB+}WN>Ae<RGRY&BVrPE6T*o&cVsb1S+T57#Z9l%{2`M zHt?>CbOz9^lZ+goJ_{?Xtbz1b7#JAZK?CqQ=t{u_HYX!1D`*6Pxz`(6d8C7cwUw@p zqrH`@wX3nAj-{@pu&TC-HaD-NwyFr|D0p`8nr+BR5>PfUGX-U5WpL_;WDQUa&aTA9 z&ITHQ1WAKSX;{QF@o91ivD&ckvdQ>bhsTBcnMcZUF)=dBhFPQ*8YX(=TC#9?zwq{A zWc2c|bdq(<HntNK^VT!cF<}pJ6wnqH=aJ<Wkz-`EGPkr;l2K7r6j2k^(Q2#qt#AqU zbWDzjO49W=5e!ZITPYzdr7SHWCeOeKTA9VTgy{!^5ksv5kBlS}E3*-(DGa)O4Ai96 zW?*AsW?=(u{bFZeXJV*hU}j)sXJ!PitY>0n&j2S51_p*^P=FGnnjsdVI?{n#MMGIw zMN=8vwl!7;ueSkZ0C8vo7n)iGjRnEw2<Q$@HB%GFN(fN<_kRShIJcrS2d_8}pNz4G z9=8;41g|)ctTYF>V-Yy)lmzFXKjL7G)7Wc|HjN9sw6i8%t(pMy`Jkn3<JC1mt+d z1!bHpEbN)VSL=b!(PCh%V*0^g&alKmOje4CnN^dIiG^K@mx+PN476$-)Fl9giV*`l zXuyD-C7qj*iGhI&H2BHI!^p_X#hA{(#s=;ZvBq;Sva*8ee_q~ZULR=(yqcJqVd@#! z*mz_47{Qt%9i$i-7|a>WO^po=^mH`Tgp`GpgjAG-l=*pOwT&Sm#;y#i_QXxW$q2@Q zhMBoBDD|m}f&&o7naCx~Eh5cs%wnS}B!tY4;1cE%m1Z|)wbl_9La;>y83oum<Wy{3 zxC+=f<y7rlxIX-Q&dwpP;^@eg&(5i!?BK-xf`OSqo`Hce3w#!{1cN+-xdX4PjF_k( zHwWadbVwJHn}LzB1+)hloM1pac!Jm>Apoigjg=v7UUTpPrl9e9brCUfQ#Lkn&?T&9 zW*;NWTwF~f%$!|JXQ|4{soYkPl~uXN8)|D4!gu!`Z<v)$DDOOFbKbK&=E}-uyk~gK zL1R9l|GzQMWKw3(XRu_L<RGXZ#l*;@rNP9-sH@Gy$pxFX1MM5);$-AX2lZsx;yKuv z*w|XVc|aqmv8>G8%+PeM11dN{${0Y>AhqoA9E|Mjz2H+injsqExf#I|eUT1|h6aob zrX~iKhL)gh&Y*h{ga!HexH;KaLDvoOLYjA=!U8%~Cy3PL1vU1JjUc3$sHm7YyP7)W zOg2;SoEXz-LkG_QQ!`KJh-`1i2v<H<@eqR=bvCI8lgMZz8DUvDYhP(m3uO^5b5mnm zmTEVrh{%687^CXrlmESBlJwBjjMUMMP4DI4HRECP53yC%7f}=!mSupR|CtVMCulRY zItU0cGBU|XF|jks%QCUS+s2@SYuMR9(@IQCEb*+2pf)e)AW4RJ<X#aY14Io-mVt>0 zyafy1D*~<kMa*<6D={*tt0`$KYm1A5MyJ8Sq{yfU>J_m<#-<U$WQ;pt7-xb4#?m&z zEy9hLRVv0NGSNszSYF!3M_R-}NfZ(s6&}t>3E*(h(uhyR2m~gE(EmJ4VNA*lstg7U z6%N9nTh(|O*%@`1nAq4=85!6>MFOaE2-?dA@ggD^7@!@0@M#&~8V7XqDMS^PfB>(7 zhSW`w4&2IGDuTi)Dk`9kKdTz_kStIGLR=8M84x@k0`t9~2)mfLstBlBF$aw%GRgCc z3OgBCxH_3>s$1EBgMdvc+9ooQO+)(MQ*l{YEp|m^dHFAzN{VVWCZ;yRj3sW4NFnjB zT3OD>JyAqaQ&UnNlooXUzhvCP_?JP7L6gDUflr8$fsxTmRz{eKk(JQ{a)Tl_10w?? zs5cGH+MF<6N#s^Vd08f2QEf(XP&El^+=A|&RMulsH#Gy-g6ih%kTM=LAg`{b#^}w@ z$0fxfre@@)DIv#b=&i%Y$;7-|S4W$bO+r|WS3rdEuME4G4zIG4hPHo@o-~J~6ptK> zjOuDpO%WNPf7ir>6s%PByu@Tx7#NrtbpBsr+|MM!pv0ifV8-C)AR{FqF3JM(fPuc6 zs){rRJLq0)9!3u_a4Q0I$`Ve$Y%?(u5@g~PfqKSVoE_X+2YU#zp;-;dSEA->poSf& z<YEVx!Db+@v2e25sCqEMyr-|MqpB(@3h`n*BQv9bNH{M?iKZ6FvmlRh3Ub6q<}txN zE5YL|Y01Wh@bH64Osq_@D(5A2gg1%`Dp)ZvG3fq(&cx58&!E7d0UEm(<mX~$U{q07 zkQL%&X5wIGkdb0yVrKRdU}W@=W@K^)_3uIZD8YFWeBw|ylaG$3swxX;1&ElqFr;=d z1y?oZpm+rLu0R{O)zs8gP1Kn|X|IYy^q!EOtcII{tgBw2xw^5ItU^<{x4F8prjRT< zW1ILLC9?qUI|g!$x_kmcf_?)08YZe1Dk-)N)74B>EksP^6Wt8|U6(F^j6axx_M9{6 zFuF5}FfcGFIWRz0l{4>PQUc#R37Q{v{{N7<n@O8NlfjU|lEHzY$RS@+oQIi-*<4pj zh?|v>$wEh+nVnNbi3zkpTa}TW(Nltv*+ZF;!JUDTfs>t)GZl2Y6Rd&A!@<oA>d~`u zK{}(>R>nq*40g6w4%QAvmd2I_dW!OpJ1AJe9Z^`>ZK{YAK#==H#L<FDosCUcL|hn@ zJ%vFxeyKBqdZXrwB4Ug(sU?glOu@dUz8adorY;!@@@Y=~R*EL-GV;wuF2?F6+Wg$I zaVw;6saX4Y%<y*;x}xxJi;WxGzuz|O&3UE?QQ$_tg}J$fhQ6|ea*|!k1O*FaJ3$ds zOGb%kBSuTPw0~#QQoZ#5T@YdP^Ku3EPgxk8|Nms}W>R5L1kbFSfp+%rFfuU<aWk<q z$wT_>><paT?3|#b1Kb>p+^M{5Oo)y<KNlak<IW20xEmU1X@UxG6JrB2Lo;0+O?@qW zHB}`IWerg09dvI8D+_}nqat)T!xT%Hfy)L^+m4M5+WUbHW3U>T;S4FaT>IuC`xH6Z zI7T~nPgX{4HV-#XH`aew**yQ9#u`eFwUI3oEX+(T86Sm(goOP|4-X3ug9Qu|=q?)O zGH{u12<jh$HVle!Gchx%fM$ZdK!;B=#<MUoGWL3NvN3_S2eLxf^r)*bGU#fn8LAtC z#_&N~iaFRo16k0j3p9og>KK4FFM%4mps8)B7^KOo&Snlu8K5L0Dk8?H>yvNWQYx(@ zEGw((8Q>SBVk@cGP~n?v-B_OP=<q7Q+b6)+&zsA=HmGeP2fI5@R&rdPl(<t=!!(!L z(1z*u_FmxgOOxaBL49UWKk5;48IvW09)mf914EQUxPrVa7Yh@ky`8a<7&jX;ldiUw z1`Fs07CA;n4^2h}cTm=V`;{HVua*{!3>FTS4rV6msxnegzv?mS5%eo&z7Q4x6)?(5 zY~XyMrp}DZ>!v!Q=Aya=jpY`4qUK_3Yz}_H!dqojeEsGGc#Dc|mBkhmg6eV_Dy6;+ z(}mULv{>2f8O8N|r5M9_GXGsDh&A$)`nQ}9G!z5c7zn!SKm>FH4k%hdlk3dP;B|Ga z;NEXDgO9SPs35pN6jcVd){I5P#6&@CP>ijJNQ#JnUS?2MRu!Mdb>RYMT0&KshYNJh zA^g7pQ!JAtgB*i8gNuWs3?ma8Xd;mvw8Fp()E8xDgWP8bUc}Z4npkNDHI=~wFzt*! z3i8T|^6Coeih`=DN*thmCwTQFbhyA2)G7iWZf*ohP3&S~%&^;381Fj?$O!POi@7`7 zdjv)tOv*IZ(=$GsoMLHb=d~b!jm4dvEx=`-kM~U`Nk3n6XJNqrM%Ud&dIs8{dI5B{ zF{3e)JOdAC=LzVHJVq}bMkWsi(Bw4(V;ut%6KqUCP*{MOS6rKsjSb#BF=sTk^N4Wc zW|g!vWIV@`<oWL*lVqHZF)Judg3m=_0-Xaa$50OGzw^mRGcz+Xh;Xui?#=O%b`WG{ zWMPnzW@2Q4h%tjM43!Lsa1a44t7C`<FGpqQ1&0>1kF<jXxEBwV11<lDNkuvcGcrhu zgZ8QLaIu3X<K#e-YmDHF6F~FjpjbpW9F$W*SGzIuDe-g3>57|4DOxF7DTBNs<EbN} zCLra=q|D45%+4z$qvLM^_LiwJJG;J&A_M47>~D<cn3Ne57_=B>L3}34%*e<f3tCyi z%gw~fEXc>i#tIst5(Rk>Lp&0qNuGh3ft8J!6>>8n3uqA<WbP>eHkqUhR{?HQGBPs6 zgBGHpsETwD1s$@aqzJmHPgIzLjX{A?0W`c0@jS@mkgf%&ZvmZ15(KxcnZcb5&{#WT zKc5mmC$9p(bC{8=Fuy#Xm8+qHM}QkQtBmh{3ndY8X%TREFta54`*QG_v2il{hk*MW zjLva}=Km@{-3nEGNZ2s7GAT3YFqko{hIms#OA{39N}MdrEQ~?|OzbRT!b}|Opx}{a zW@Kg1(qv*}MVDoTr36I=76x_>7WQ;Vth2FlGBbcu1P2Fm0s}L1Hz--C!qhN;dgV}s zoJ?ryA{`_d8BC1ybk$Wsvk_7f;FO}nr~@k2Ac2KQDd5f+Mu-_3iHVDvnK8lxj>!#@ zg2XH&6|IykmC-{FoS38?{*@pFA~aE9gd;S8!2=VVKSdcB7?YWl8Tc6N9eDUSm_WP2 zSQ(fYJiIq31VlIpfNq|OXJBA}<x*+z@#jp8pm9$|#%4yJ$gRr4ilB@u3~I%h3xehq zLA9`GSde0veuS8kn0AOJld=>q@87$Or&(BddBNkz-2cBZ#WN{0NHM4}<T?m|suCta z@RX1MHxm=ob~y%S1{PNEW?)8e!?G1LEe4<NmUaMLaL>pL+8+v2$jS<r2IpB&rx{wo zNHI!*@+_;8nyDaYXajWo1gIJn1kaFx_Hvj*dPm^C5aagXWQgOz9?`Tk^wQMy(KOZ2 zG%?jsH{;5PU=#)U#!ZTs_unUN9Zy{YAD!iz=9Ze87Uq!hN%;Ra#x$mX3=#~!4*Wul z%uK?FGqk-oNC!kXh=O*(#WOH5wSZ<e!F|1M&|sJ_XqhM@Xcq)1OEN&l0=B6tt15x3 z5Tp}tO$9-tT9A1Q(U;DiqH6pCG6GIL;%Yp6vI0&$P9bdU?ksEpjJKw6u!0y2j0^$) zzcGn0Niv8qL~UheWM%=aJpvVqvf$Df)D35d2Q67^29=9YCdgIN2zl^897q~mzjTAn zeFMd-2!n{Su!<lXuY@)uWDzl_K!DvCZEDIS0_sjj+eRivSlWiWGrIrV>7k{Okir=M zZxg6D4O+C##K7|Z8&eASTvd67;;np)oUAYxYOpXdF>rA(v2ud$@MdHL*K=&~?2O=b zw49t!CYz76gBn()u?$d^pk-9--RwS*4g&IWGE%}S;Q3)e0niMwu(7GJD66S4c#2FJ zG%p77g{d-RdfZ%<#V3MMBqAUH<`<tOmbQMLZ2#UeD*XJ(sG;F>7wjn&VGn12Kk(2X z6DZ7?-I$aaxEb^qTDNJdGO@rtrNY3<$im3VQU|&~HkO?kboEIq_);XM1WpcaX6VGf z4on$n84)-;GJr2I>Gfvd;An=a=Vk&ck93gd<zZyd(oj(bt(=sS6cZ5?;L+pNV`pXH zX5{7pbwa^aJ;-M!X3U^GVJrwv_}~S`ph6qAu2@+K6a<W?CFSKMCFSMUMMXuJm_vt* z-1u0<gAHob*(D>Gl%?h6rKIHLrT<=zO=pb%w;nWHRJgc4KABPc?{g0gtq2_*@ZPSF z|KFIsn3Ng$IWIHMVv>aP%$RmCNrHQ3j10p6zcB@Z?~aLb;Fe}&VU*$lU%d(`?!_5c zSQz6$tE*c<4IIcq3h>Gi&?On**<;uc5fc-*4Fn#M?`8&_FA2J*U09io7j%If#J31v zs)1WUN^I<=paS73D1k<}@v(x3J`@%BWcd_z?A#eA90es*u#Z3~_llMp8;cXK7sLPm z|G)khU{Yj~W@2NU3@YRPfBnzHRKcXcpva)hFat7eB*e$a#4I7o%g(~e$|T9i#>(s^ z?En&FK^26~GATfpIb?uNbcD_ugRiP&<Y0nMM8H-;fJW0>K}SJ?ecKCKZNd-_QwBb+ zNJmRe1$11IkN^)C8!M<c1zRkm&a5m9o<m{>chbeg#f6QTA#+WlB5Ww7;s+ZyIghPz zuVVS+goPyd6$A}+1pVg3TnH32QL^@MV^)=wQ&y3cRsG@?u>0R;#(A%mWY}C-*$nm0 zFsl6PJ|-sRVP@skEUTg-FR!A^!1({we*q><CItpPMlnbT2@5hZF>6Xo2yn47fDV#l zV`K*HR|Q>q2a;kzmvRsjWMqPiDlxJ$Dl>xGMxZUMJOL373ZSMYBSQgF2!hfMTLO51 zww(>M00(qdzamH-6QoHB>(OH<1i3;Lt`OX20`+OZGxTVR9Sm@&V_;;2Sq`erpd-cY zY~X{31sE6@^cZwiR76xXgakQxrM1~XD;K~A;F)7a5qMk(au>R~Iy)bevY3ScJD;dX zZ6cqXu%HCLypW-ep#Q9xbFqSIid>>R(&|>$QjU=lYI>?4cKOS2Dyx6|28kd8{qv0K z|JomGTQD)XaCy0H*luJ4+R7mG{~J>#lO%%}gAzl&g8(lVJ2MLtsI=k(CtgUFlm<=a zv9YjbfYvmDS7<QAGlH6%;L1!MO_q@nyrc=z)P(JMP?VRJ6cXU!=Hy^wWe{T&12r%~ z>$ud}m4%s&#m#Z0$9K9`qFIxZ_ijz?>`dO`!Dpgs?(E2-BqO5)!dw~(vu`pAGqRmI z^Y2%hw7j#inQN7dl9H^fGWd{P#{c#I1(+DX_k1ZkC~&edGlLpHpvtcml$XFYU^}yq zkdP3#^9FT_u(CR%@|B$_U7g8W52W_}yPC|%$H)z`<KNpW|7<|_ZGy}Noq-JsH)jV2 zb{5c{T~S6>CJu18f%a9fF|lSqZDL~s=h|-YHD2v3J`4<?@L&*Q5K~bR65`;M&}L^u z3k6UcOP!Gssm0RB7z%NcCnOZCbgf0w|6OD90)+$!e_;FvaV0b)G!&-&vtdj?36Dx> zct|j$IB<dvc4uK>0@c1e0TB*z;N89q1<+V#VQB`nT38c6cj$nRPLl^IU}0da!=WM) z+<25=5El{>6B1<Sl|)M2h@fTWV^VIn;AP{H5Wl`BwWmFK)85pcgR<Isil8*hqpHjp z$;f;D{J$Un-e3B6T;E0x+z0<J0B+MLf$nM(WMpAwXJlkz1Ft`1U}Xj^$E{;v2F*XS zuz*{V-5{rf8(i%Spo7shRFy!r0_yE0ppLpS_>2%l6ULZHq@>z9QA<0+($k%pk;S|U z+L(#p$gdO>2oe_ecjpw*5)DPPYQjM0H2?p`Bm`a$r3kJA&Hpc8jAhJVbYt*lSfDve zEscS5BLm}({|i8MAXxnZ#!#@lA1--@0<gS4E_o(xh8K)(3<0?08DqinfjHy^7<0h# z!3cRKg!>f#&tjAU%ZDN4k=)Pqe-^_XMr}qnhH$L%N(}vsF-+-<?i|SsNM~C!?qHM$ zg#b8=7#QUlA2OY0bZ2;htcD3$jWVMolPgmiWSkdqMm96D8gqtz##pB3jP4xv$mSs1 z_5VMkJfk2;4d*UgY8aRq<rzMK)UbfA*_KDd8j_oN7$q4SLH^>rirpLrMuvM#rHqNp zPZ-1**f#SpFdAqxih@?XfcHj%YG6|n@EUN&1Z8;<X%Qy}6?su9QAg${ChW2zf=Vjt zqPn(hMjW!jLdweOqS|(#wg&iYctNJC47?1Ip!Grw%*^1Hxh|*_11EDm&@D)+%AhNH z7)8L{AIQ1_G2|PNm;?<nLo18*GeRrM{?$1L2DrKg1UfUOl`M%kytZUX_`!9PCrz9( zY0@Nc-w||hv@+8v1|9}Q2RTj-7SPs8(7jb^-b{>)jF9`Pco=x3nAt_OjRj52K%1!9 z)y<6s&oVIyNDKSu9rR@UtZgDAASP&MJ?*a<C?BgaFfjgOy22pMVBnz3%LD3<GBPtU zfs93Xh&dLtgif6qv;|q3L0Vl^gOy!e+t?I#S`!;+Z8&7IpN*YeR8drIx2d<9n}w;L zroL9LOG=4ekoi=`OwYY$ApwTgwkitN%7$qX7D)-FlUbm53hDm;#>C8Yg+U9nuUQ3j zuL2_zGe09E6CWc3Ba4?R=qyFhIRlK$OrVPvz%5Q)c$uQd;G?RdsG_RH#xAAJD8dFk zh#FLsKvrCXCaaWT2T+4X1t6CRGQJIT4h^-cjJHV8(akVO2+fRlb&H60v#|=&&`~S& zD9B|}_6X%;;fip#PcRb{3lf*`c5=4%_OdmxRFblh(@9}qU}6B>OQFGZj)99o1axyC zKO++(Ga~~NlNaceCuT;_R<Se&Mn*MopFy3)hnt&$fm?)Ign^5JOHfcqkb_-9ThUa} zl+joav>;X;QVg=OGc#uYTg4a<oskjB#Kgj_z;$|&H{-*9%KnQ!J~Z)>5;o%hcM}wc z_TaEP#UKYBsT5~oW)S0IVq)e5tq2F5=gj~*z5=wn6+Bj{21=sfxYc9yVFceP0_w0s z2ie#~v>9O?6{Mh3Vq;eqRuolaH#1`t&Ta0kvq(@>N!RyJ42zZV5Yv%nO#Am#P|%8T zTV6_jy@+tIn54JAn>QD$3}axxd1ml>WF~N);bpqQAjhBsI!6dJI?2h%#0aWAn87<A zS(u<>XABGsy5P_PSJQgTJ__<`D)KrCIx6a_D(vhM+QN`);UWESJ|@^ob8tspO&m5b z4;r`^GnaO6NEI~}5YiKKjWLyAW)lx`h%KtlaLIH0Wbfzds_&+*<icp^JGIh-h1Hvl zO)ntBTG_BLqoBd1r!6qlB|zO)UOUtrv_y%4f$=%hDF!yso#UXzfsEj65(|!7P!$Zh zLy3)nO;C`9T}<1UQPEUUlre=-^<O>X{eSnEssdd8R)NMUm_hf<G6{q8od$y`Xs4us zC_6JV6KDr06F72o!Rr$k;#n9$EmR*ZO-2SyQ!P_vMSfleX+~+#CApxH70@;|@K$0_ z9||<40y+AM8MGe*JX9zmCI%g<R|ge(VvL%_HJZ*ECY}}vws94zcAz_O>$6PU-K@;r zm2DI~eS=&CoLs%QQlkw#O@!n^oy>eRB7(IY428u^+zg{K8JCIKnHiay8mSs7|C``y zZsID%ChK8t0vbwSfSeV@frw)PMpn?JqfDSJ2+Up#tPD(ytW4>k<Icb_i;{z3ajdGY z0*+%w$dQ}srr?u2%#jyoiHSis85@E9sm8=1?cSIwVkV$&=oW1z!NevW<QP+Ek?!h} z=kUqS-`ULo9LY05k<7~K&8z31X{BUXnC_ip<=WjA6yoBqW}~1TVgZU9(0+q&OrqfX z?!*{$9kgI&yS_JQDJ(oj`1u$aga!G;_{AWXdq86ZX_83Y2(;i8G&`gckY!R`=AUI+ zSw3~z6fZ9Ky5PF$ZnZ)6Gri88KAn;UvX_wobS{?}lQIJjgW+~APF7~ntT*JEYtY&o z4N&h}9W*Wk;(=G<s5AIPZjlmTViyBl-+(Ca8O<V@nK%_hlC`gTG1{qnOL58Yd0Ni~ zl@&S+42<8Hlo_N!_ucdHf{sfBZOrvzV1$hL>4OHDV1}!!Y6yTTFhxZiMZLHp<F|ii zM@;NxtnD;IHS~=09iuapoejGg8SUM>RgCl$bxahLY;{yaJ#}4dlu|*9w-^~@{|7VP zVNzz$0JR-e7+F}A8JSs_7@3%P85x;)z~|D#i+V<oqmvm}SimK|K8ufvs-lXj2Be&a z7W1H{xhc472A)<i1@+!QEKs2Z$`*{rx`X|?%IqW6RpLy7Bh#a8?F{@4JWP!}wKYQP zV@eszI~16?g1l1l#f1HZWxQPMth^*G4NNR#WNcicL2<*xAn`wfNrOolT*lWqaPc#; zFtdS5b}#P@pw*WW42%qnEexQmZ^3s+GQ@$_S%P8<5`ki1d5{zns6Jyz11SX!b;~m_ zvoJTqWf>S)LP4rnzy`1|_(euK@Nsj4Y75W}<N}~;C{>k3l}#bF1$b%;bU;2clSXdX zzs-!EQMreim|56m#dZts+-b#l<Dcmn9bZj81BHK}%Ptrhtp5iyF@pD|Iy%_N;H)c{ z7#Nut;b$~xfX-+DT|Nb^C}bIB*$Gw@jDp#%6KXBul=Wlv1LI?YWL*?Bq#3jRJr%NN zWKGW~ED{w75SR4uad+qB6lV<eyT}8oOD+C?V-jGx#sIp_$j-qUcGD6MBMU2`Izn1X zQC>=2T3tm&RS8l@n2CeN_d&D$%#hXi;Dr!sunIzrjZIucOw3~495G!n5k0AAd~6Dl zCMkIpIYBvQpPaqjoisdj8C6{v^{-uIWM%baWBqqiPro!hw<Vx&N@$RqKckwhvTg{t z9VPTXmhnB41ajQ~I*5e{d|N80hR|RDwVvR0gE*vaC}x!W*T#7F-_ylRdA=5ZYr$uV z82<mp#09MzOh9+{87Yghvw$iGL;<7=%BSFpLXXi$Rt7YeEu$f;fv6lHwKyB3I)IEi zLU%HniG!y*Kr0_iP1Kkn!*V8|WlM~*anKr}A*-ZX(?#9X!y>`P!cEym#l+q1yqB-1 zZ$OxnE7KL^3IVyIa0gcl0^l0rpN5I6tGThOqO1o46NCQ$5GD@r{DTIAwSxsOBP$DX zj}cNYFhFuUyn4`Q@sW{MQjpe=(NI-|M5-#diifsLQ0oNrDq-akX#>!<i;^Y|7P&CT z#9Z?<_rjLXE?(}=>h8KqE-x+}Vq^7WWAC42q*s{kk!>?4D9p#hSJ6g8H^>;Y76}v& z+~77o=$3R12UST4K>=_)z_tT|V*%FOS69Q{+!tpARm7|&>WpHbDH|qk<TgJuBfnBi zpfjT;%fGh*_QnCO$c=v;6-NHlNt4XA|Gl-cnh5TrDgFP(#K@$~z{dIW4d@&%PX-3Y z3^4zf5s0r3zE=h$|C<@aSN|W(_zW!nhYQ4K{T~6Y+t@h&z6J3O{s%LD1EpKee_ug- zjsM@67?`dxuyOwX0OBkBk7WeyWoP4J-~;hh|9=Db7udKMzkv8k|3esmgUsh*`V8U= z|Np`0#khr;o!yp!;a>u%m(8@}Ujn?3smj2>@PTm+Gdn{yOr8;0UV?#v(U@^7Gdn{G zR(ZkyKNuas@}*eixfmE2LmAgIvomSI<WcOG`~RKM0c1auHcTGHev$t_7=6I*w}Z)} z=m#B0;tbZW1CvM5FZurmqbo>1lP*l28QFgz_j@vKV`gW!hsmRuFU%;$=*4si5`O>d zAmR7F4iSEgatt4sPC>#CBG0%3NnV0cj?tLuG9>)4$O|%xF*<_fOBooj$a68uF@`do zg@hkOo(b7}IYv=N2ax@s@Po*s*e}8;#^?ifza7kc6#X)ca*WPk{h+xCu>Vl>OEQWv zx`OnB!Vh9TGqU|4_j@v3frKAK9>sjnXmKXfDF!tz(Eb^ACNV}$aJ~`#|AX0`=@Nq) zmjnX?gF2HKBWRx-JC`8nwB!H(8C3s&XZj7-2a*S!W5CWO%(xq@A9SaPJkw<cH7?LH zZ;(7_lPO64K3HDx|956vu)H+X{2b6VP+Y={f5Gxx|GzV<Fr8&k<C1~O$3f+vfaO*H z|6qELV!tL>UW9QmSYGb`4`vmxyew2d#Qh?SH^B0$3=GVoV0n<gLGFXNUzG6@ME?H| zW=1UX;*2}s^1m?T)w%c?d%^DDW7+^#C;Y#R(TmXtl0N_5gQU;D_Ympxe;vaIMlDGC zgvc}QK$4gEU&m<7Xbef8SmXu&moYkm<x3eDu*h@$uVV~l)P<x^h&&Ur`EvhD8680O zgVHBN9>so<|7DCmVE5a>%tz5L^S_SK8LVFiCXb?D@_!klD@Z>meL~D<Mz$a1eosad zNcx1xqnOYCznI0B=?8;411}>uCK-7DKl^9P$j-pa6!_1UL1-f*<Bor}p!@2WpbYSM zlNo~=lRA?G0|x^SsQ1ah1U@_`5!5#UwJutjeHa-)XM?b_GH@_*u=0v%tAf{G8$-^3 zWaLcGDa>XRU67otR?^&B_K(k*F&EU{ieOM<@`ReJ;-CmJmywAPWG=W9&<Yy!0h!Cd z!N9J_$}6gE4m~JV8FWf)1fytnVNUwP<YYC*Pi3vmC16uQO?LnPNsQV|rx>_F>yV(! z3Q`$ZK}W<Uf@)8Yqnns~z_Y*tY`jw1tjeIl4+v(|zH_Iyx3_m5bB61`EiV5yf!g4V z3=;p77(i*18+;Nb69Xdy6Jsg^0~;eN11reI;NguXkc;65`GUs0#Z5ui7elaAS5HsZ zgLxi|UT%yY9)D(l$JIps3ouqQ1TwIJ#?@FELA(AyUT9`!Vq#=$XGFByc*V5Eg^h)k z)!9WBXw0`MtNg&|_;3H0FAR(f0{;aV(-|i*urnw*$g_d>R6_;_kPKmGU<XZWh-r&6 z8Z(1T5nz<Go6%qspZ0;N>EE({H(tB|yGNYKjme8yf`OYs&Ow@um5~{|1CfydbaNA^ z$;ZIxuc8Fnej%)G20mXM)V^@kO5CwCQB$qa%d>&WO>EB|u>e0m$l6@-{{mq1)VM&c zQ8q5n2qwsbHVh1mN=!c(I2lYFxH#EZS(sTNT~5%x8Biy)1=QsPuMh|EzzL!m)a4Wv z1li6gtSroKDz0wK&d4tuDZF5TaHKHf_kWy>-+BD`&YXe7*+d2g##)#i>};$ojG%!N zFW3eX1_tm-4DcRvZjk3e=>oLMWs9f~$PQssbz^pOQFUSECBhLx%a#d6Fn%{ZbB52K z=N~5nC^4!3f6erb=^g_&gE)gSgFb^b!xirh;sFs3dYp{x0v!CzY>ez|>CB9bEQ}1? zj0~x~jNC#zg3Mg3++3gwX<EGb7+G1HSwXXspo?0F&;`-}zOJzabbF040~0$dQz%iE zvBvT-vNEyyMn*d585*dnNJ{YXFff>#8dw`zYip|LtLn?kN+?Sz3k&jy^NPdMurRNr zHmjhbDd;+1&>47)kc<r;rBg&=;}p;Mx1Z6`htcWZzJI@@6cj*f1{jw?8UNIv492-2 zhNOamC}W5ZWAMMVKL6G*Dak1)%E~G#$ie7;Ci3zi5qWtS&A<%m*f4oAonl~S;AfC# z@Q1{$ENB*z5p;As149$2vjUz50<WV0$2MrP6Lgyv=nySdggVF?CkIXy6%}PcaKXT6 z%&07E0_w*@dT*>qY$k5Ie-5^CGGgK~GU8$~j8QNa=tz9VD^hZDQc|+AN5Hh49H>oi z%An80&a{hxn?Z;{jv>fFNPv-%K}e9FkC&ARbUiu~XnT=dK!k%3Xh{$gb24a=2xvi8 z6X+NOP}_kKeAh1%Gc!{-c#fNanTgpya+`{_3V5*)vQwE&RaH${MUh>6RZ3J$N=i&r zY5`-*zZ%9%|ISH^h{%8hm|7)dWhEqJW&cT9uij<-SnZ3Hl$4aD45+=p#9+!O!FYh_ z8iOH&Ekl5VuM8t-JqrU1D?2L-=qy(@CN@T<I?z@(aC&C|4Y)BefI9Htakeg2AJE!( zOLHSzV_P*91vyD^UTzLH217<e&|*1Z@caj8F&wCa0^YobG>iorUIF!?L8GbS;8ovj z=19qIv!;bCKQEUspM;u>G9L%0fV;Jti4-qCrvN*Tq^O)aA18-^q~&T62?-GqNl8XY zMkys89zk9{Q6&u#JxOT`X;XV~c^(cS4mM7H1$hYrNeOdF1#8tkAkRxmNr9SM42%pd z3?@vSOqL7+3~ZaZ85s?<RoOthHO$4?#KalJ8O6lR8IQY`8yhRiEAjIwGq%Y6tI_n+ z43<|kHZFHn()7~=3n}yQGjKET|9il~%%TB3vs;qEl)-_)o57zUm?4}YjbXRL4h2RI zK1MHQCME`t=qM&}cTi#3%pfi%-on5r0XoZyDV2d$iV<`mMk)h410M%FUpg-j6Ndmd z6DL0hXDS0f1D7B_S2}~BG^3EPpinA<uneP!sIW*XgQzT{n7F7|sw5)|3v(<3GjkJ* zPe@2WfRB%un2-=3Uutqlcu06yXh3j4a8RI+zmLD4ua~E*v%RgQg^96|p{}-?s)~}F z4CGunAyFYw5n(<-K0#qEIbJbsMiX;UP&A2&iYP0oftE6`nktHdF)Y`xgZFwvvXZHZ zxuU6}nz@L$F?fOmJT3rQie#d$%m&s4)@>}xF2ZgKQUOgBj2H9F#Kd(a<z=@0JH&YG z?<S^e|H4(Iq*Pd#7<d13Wnuxb+5VX`o@A5d(c%&2;}Zs99&PS_+nBEX-Nbn8-=S?f zd8T4w5+YLa%=`SM6@+AXIeCQCmD{zoTeP+Nw6(pZR8*wcdD*ntc-bXYRVDd&w0Zah zIXMOSgoSf?L7wL1YzM1s(bn$ulYyEbETRl<r+WPV#595F3Ipi)76wqQ%<uzTUzq&= zz|6>WgMpoapFx~Ko<SXaJ{b!Ws1#@BKw1IV1zP6C8PCAU+0E&ruBrqDid>*0O-zi9 zKq1TurB%^rb|{}Q#?s2l^4|>*$rJ@5|6Kr)OkiS;jg9qx8yjnDDE@6@V*_@h>HiPR z9O!NY6{ny(ZkQQCOB6w)i(TLgz1ibA8QDQA>(JaN40Qv{Z7{boqKTV=-I4$z|D6Pr z*FhvxIG6;v5#+vaHa0f@{@OqQC_Bjh|HCB3_>(~#G<P8?A}qkq$HvUU$pIRtVq;`r z@&cXH30e-okP2Eq2CddbL<B`dz@@sXvN*fCsJbb;y1BBcvAVgqsj;ZJIJ+^E)TTP! zI{P}Ey3L#Gbn5Kub?P?V2Jsj-)$4+|^_w@>>(qmq3JeVYb}+yAzkq=q)W&3CW@2y$ z4Y@Heg@Wc{7-AVg$D4s}#%5<=7ZzmU71L%FRb&O($Nb`N?Z54k-+v{TY|(Eu0qspP z{r{CIlIbnH9pUNV#>&Xd#lp$V#Lf&lg`a@|)bwI(@n&FWXN%`%WMk`Q^MTaij0~8y zH`2O4q_PsU?hlp`k*jWGaYkooMf?^@GtK}pq~ztlgJ?+w1*Sqd1yGf&AouS8tYU`I z|Bk>|p!OFF1A2Pbb<kpAWMXAxW<yQuY;3R<ny`cp+RO+`EU?6bk(e+O_&IpGfu(Io zy8i#4!GmEl!x2U+W_AY95qSq0I5sjcZaBEXSo{Bf1`UR%j8croOl%CGC0UFNDgWge zF8=?-zymrf0JJh%1hm!#GLgvynIUTj)yR@a!i<dIDj5_{OpN}K4xo#ec^G&EMFd%S z#kIvv!7V0bQ_$>SilL2-p{%-@hJ}%lxw?{q6KL#8{{L6T6--wc`MI7j?qNC!StrT7 zgXtuwcMG~V<-Zh@*Z((+?x1$`zdx`&Zj3wr{Q;=}_tUBwMHus$K<D-IGjwlb0o~UC zT3yWtYEgjZ>ls==yOAJmAx2QEQvh^5Hbe$|6d|Zl0p7;L#ON;#*{mr9kw($zAPy14 z&=VOM=^)6?#=yYK&Bo8p&%nyS3hE6C3!00w3&QGcQIYCfQu~`gb+Rs_2%}YvUiQDo ztx{5wQpP5rbi>GC_kSJZHbylDW(Ky+OrSLrtc>Q2jFbMYQDqDcVhmCFw-&07F^F*+ zxa|j3$B0nJ2v^9+(7-6dSON|gAqRd|7G@?E2G9x3B?64RqT0gBAlGd*y(P6D>KbtV zMzneC9IW9j9?+@9bqt_147#%uRMw!ha~K&=+c%&k=p?sqh-=dj-IifsWLWl}pRttb z2jot1Q2$B<)O}@UWJ+ZK^?^X^E80QxK_Z~S6*M0Nnwd+53PX}IJLFDrP*N5IuM`km zW<F!aN9LFRR1E*A{Qu9O`QMW955sR}b_QMshJVbUu^Gl4|Ck|Z^Z$Q_J^w#3S};^H zurUahfZM|VKQR4=*E7lv3b3|r7x;8I=6F^{@P-U<iJ%DTq{GSs7!4~A7$ZQr{@+~? ziBxm^Jq@WaK-C&33>a9LJeXn`PO?6S9FT6C$H>YS8NtND>chy)(q?bR%*1Rc$;8WK zpu)t+yNr>aagB&DvykASJ#5Uvpj;r#<)zHU#LwuN&B*Q{4LQNnh73cp8QDSRFnLOQ zZx9ZMaG=;Bpnb?7hj5bU8)S!oR>dNl!9`?<Ae#YNFbgw-o7IOA*$h5Bp^7lW!9I_X z732*}?TiAvDvXQ**y4rT3vNOr7(gO8oP&{_jlG$Zk&TtDnVXA=g_W6=g}Dy2D4Um= ziMNh{mywy5u||NAk)N57zlMQdkeOewhCxt>Sx~5kK}eWcNVtZFk(oi5fms-|fCan) z9W)>TiTY>;4t9=a23lDGN*Oft0VvJHBYeQX#z}=R;^gcFt#U>RDA2x_RCH??Sy;JP zSwJ@-GqZ9r*N_<@n3jQh&e^yv<6-6EsbSz@X5}H#P6k*Sz_b#SZP2Y`;AG`&=0<pu za(h`>k%API%gWJw3APe+r7bHrOC19XD0FKWxR5PoU}ENDX5y=3;NxZH<E<ek?Zdr| zWW7Tz<u(g2^9s~32rx1WfG)hoo%gWV56<17MuyUf;|KR|+OTx-!UZ#@Po2`;Syxk4 zSyZ5*t*xyqqbDsO!Yiw7uB4`J2E9)cJiH(#Zm!1+JuFC$N!(nI$s9bgBL<RSf=Gyp zh>1flvM>VOo{iw3$bdMYgYH46&VY_O0BxjYV`n#lvOxz(!#SY)O%PIS{7hg~pc`n| z*}>P!fK-EyvlRpB1}~#xQ&PXlqs+oD!N<tY$HoQPy}-*Z!OYFe!N$SC!NkbH%_hjg z!N$nU!@|nQ&Mw5n$-%?M$-%@X$imCV!odt$xhTxS&C9~d#LU6N#LUUY%FM*X#4F6r zE5R!)!NtWQtHvkBEho#)#;q!=&%w{CtjWX0CZZ}Y!yzi9uE5F7CZX*n!mF#p&7){& zC?Lcvr>@GwEyT#EDl4q5$jT>aX28QEt0|zO!@?rWE1)DHY-h#8Cd|tzZYrYBqNu6L z$;Qja!=@m|EX}8>$|)ki&aKZa&8DWsFU&2(&8;LO!)+lY%gx6wVaO=Qr7a}N%g4<r zEg;OH%*Dma1G-v=Lt2EFLso=Yf{TewT$oXnlaqy0fRjUjS%Q~`or#T~gO!6xnTwl~ ziIb0mM}?oChfPS3pIMfXn}?H?olAt7iIbB_l!c3zmqUPu8MHkCG{65p2i#|42CbT9 z0xid93<a%T1r07UGJtw(%nZze%)FwY&YCIni$4+xCMN&?Gw}TX#B9rSg+ZM`iE$|d z0|Og_61bnw^ZyeQ=$=b85TAjafrW7vSX`5VkKq6#Gc%~4&%n?R>1*^u`WhY#jSNQ^ zIiURzBsm5q24zMO#tTfR7}yzv8PpwAKts%+Q^6U*Bfj99Kfnjg^|Jb?fR9WAw~CYn zl}!~zmBnFQJ7py`MiJ9JjBEF}OMnKwL_{PQMHm}RO_za(^k8FJ2)jT#p+SROpw2zS zCPvWIhyZB!AtTgusEH7Rz<muHMt{cb;I=nAgC^*Fa?lyTjESID8z?v#nwWi9SQr>s z*jd=wSQ$X!$_8p|3n~gS3MvZPFjoKD#VE+=&shDhmC+4!+$sYrgVFzw%)(4p;r(E1 zhIoe<VIcv276wLf5kWo{CT1~FUS`mlLRkC9T`2uy$S5}>mN9PZ{bNRTaQ8u+k&&Gd zvbqtnsu;Pij6BTEI8R1WQbtBv`rlSPMx%dc^%zYUP4)hrVKmbFclO^g2{}0lNjW*D zXei^~N~m(CI0YGL1qEptg};mR7|j?>_5Yn_G|~Ha`rk=CMl(KXd3hNbd3k9V{qGb^ zIk;bf7<Z6!kY<ORPQt(#3Ti#af|hqN_zDUNg0|=~s)81PDvL9p`M3RVE%S?C-~a8{ zVq((z|35?A|3)SsrXLLM3>8ex3=9Vt93XQw%Ktwxu4lT!;Le}~nt6tc>-?{0IM1ZU z;Lae&@D8S)fr+7lQG~Gu+^-U3aQEJz77*be1scz2flso5j&}tQ0{4OrN0fj_GBdC+ zfcE(^F@aA_Xkzk-+zv|60<64}pzau2YF=x~$a3BrDJ^d^ohc<LDJ3N-$-s!@21y4o z(4->5a?lJP_?Q7<WkE$zWw1GnB8=Ni!O9p#7%QM|AMn1iAiE%}ugoZ@ZZ0mUjx=t= z=yE{n)^<>L88n0~!YG@qSM%>!E2zUPB`GDvfTh1IuFfc)q{0}i8l=h?tO8CqcIbU& zVVD|5s3PQkvV?;uc=&+915`?at_x*lU<IFM1a=;y!vU#Vj4-Ew%}4Z?EgVciE2<co zSnC)-Q*JCZ3@o7KiVUFX3~*xT0<Hbl(Nb5HmIN*81I+}0ddiG!YM?P*$hG#Mb@IqN z7eF;Fcnz_!5%eH<5jHkPM|}%P0S-<+4LMN>9(DmveqJeKb!&G4P7XdL88r!hVJ=>N zSxZJqMpbJCNplHF0||Kreoi(HAr2mSaeGr~3u#F`5e+3#K3+i{9wks%Zu-y9*ofX& zW?*JyNCofQhIWdj9YjEfT7de>U^%ETdS96pe0P+wAfwle8Rj1u?-(-P`6CV*yI^8S zV_3>~ok^U5pFtk98kUt2GO3;nnp6iJu++-zBPakmhC)DIP@a>WfuE6|4KlF~3L{Xv z1>ELPhb}?}T{H-~M2%72IK;>(#Mn64$SBxY*+NOlLRr~TNy&1lQHZe#NYKPM#K=a; zQW-7>at{jw#<;J83aCP0h=+{!f|kXx#<MfBvUaojfcxZ1N{XOSI`pAl^xiqr5bxgw zXkB#B;4Y$@&dA`;aE*BmV=03;Xj~jL7tYMU$ea!uWnp3nX9FFn8qdPW#K7dw$RHvl zzz>>5Vqped-2+ZNMrM$M3-y>lX$O4Rp&S#_Q*#F%6Rv!3%c{x}V{->?6Ye~3>&o&X zMoD8;Uq2ZY8531se;E}S=$HiK1^Ac*xK?9;OcR2Jy%`u7x<KU_crh?66i`McU?Y_< znlTpSVaO0XqXvl01RB!@jZH$vB|&S}8ReNcnOqsT8I(ciZwWK9Fv~NtGBJ6{F)}lH z3Nf;RkKAHrU`%IVVPa(o2Q5wp-*?FZUX{$k>aPL<;0<la(|_XNSu@D?W^wS^Kgib6 z*OH>b($d1Bl1eK2Vls9T1|Bk2dMf&&vUU=N?y^=)JaW>~ax!u<>jk)#B>hZzL_oB! z884{JG-9Y_;$&O|p6#}GumN4FCd|mpz~sdSaVzK&Kn75L0AK9L0vhCHVFkGvi(46S zxQ`vV9m9*&js7q%FxrD0hvdjod1)CrSy`F?{~3(H;W~jyjUkbNfsvijpP}b}JOk%O zMy4JA<H4g+j0~WASDBb38CV%Wbq6D)odxS^2?~Pi4n<`_MN>s4Cg*>tjQ1F?IfKH% ziQygNUZy_`3JkgoJ`OxQjEoH0yv*QZXuLOodZ9v~SYl=Z6?otUnk}HwV(|PeI3huJ zB8NjygJ5RzkKCrJt)dBPZV8*fmdV1GR)BgM;LT#nN@{B6u=TQ_1+z{@>Y6G>p{9}k zlB#a1=2k8ydj1B{Uakp--p;<-TC%P-3hInqS`NZuW=i_jvPxD0f@-QtY9d1F7TV6f z+U_Pgw)!$sDoUbUdJN1A9{)cv90AWwgVr+mL;N8Nnui9*BQyhm?>K{H0BHx%ax>_Z zG&G|?Vj5IPM1tqhRa7uirYKSjBeOkJBqi0<BqdeQX{K^jIXP7o1%-c5`ZtsYjhp-Y zuV;A7P{PR1z|Y9@|1)IVoN>qh&!9N||DQpdfq`)q(+@^|CLYGuOg~`qOh2IVUjKhG zyk%Oz$j>0h$ipNLk!RY$BoCF>{BObdhv^5CI(S@@iIELNLq|86plUq+e`37AP|2Xq zAjk+B_h$poF)%Th{{PCb0@|LHcaQ<K(Ll5LUEa{Sd(eS1Dk_*$_ku_vWU3$~sh}VU zoqds%Qec`6nSPP`y9cxwLP1Uryl&eReLNMp^oDI=Kq<9wmE4dL8$1Q~@2Zuhr4_i) zf|u0{j11s4J^vpuaD(y<H#-|McsLz2^#NM31X&&gEpx=#l$F#S)Ed1!8r8JockM`$ z3-I#`5Zki{G}P?E@QBfai5+xl%w|T=IKR0ds|zE`BSuy5p2Pnu{tGgM|KHER&A_&q zla<jxTbx~-9dwkSy1BS{n6Zh9iHWkQv5Jv#vWc;Zv5AtYv5K)V0~>?$e<Ozd%o#}i z2zQ1&hinE$9!5y%#LUac1TI8b`50N)Sy|E<*!daRIN90K8Mp-*xwyHK88|sP;sqEv zIJ!A}Y;7Q23D6-M&W<+jw(hu=%|JR9LcEAe)<BgsJ5pngRCXh1CK@yaO(f0qL_tYa zR#sI>VV}G*h@mXMPhMF?PEJKxem_iPCro4?Ofjepz{ue9Z!e=1vmb*uC|(V@nLrD& zK)XXgXLXhs=u3jm<}??PV+M~=fR5xe0gb1C?qWiw&CEatjfsNQfi6P^spn(vkk!$V zHH)=&3<NRIF;)pyVQC3DU1cMC1!ik@9c3|DaakP|H63YTaS?GzVF_(jRc%QjDKQa{ zDkUR(c@~f=aT$mzDPai_5eZ>w9aU8=31QGe4i*Nh|BXxwn0_#*G1xG;G59dta!@lh zQRC)dW#eG<@^p2v7Z%{>;pF7dW@P8oVq{~Nml79YVPRwSf*jzZ$qL#U+QPuez|O|W zp25Y)#=yeN#sWUoyv3Uvb^?AAhmSO58In3w10y#h2PY%wR%2{BLA$E-!Ckjz-0E3a z*uuFO+1Nk>0BkJ&k&zBEcD5So&JMP2c5eE*>NXlSa<Vegk`h9S0z9CT)r>`rjX?9- zVxWV7)zk$cvBC~IvPqd4bcQx)N3glDk(s$E8ylp%VQvb(oKZ|ziCxgh7<92cyNI|T z8ylmOEwhoKt&+X0l#HRcytTBnm4dC2F{5o|zjKAJg=dM5X@OBeRiwLRyrp?rzl0bY zo4&U6*Ia!&rXLQhu0CvhoNnwqEdLI%3i3?g;%EE!hLMN$-%my^o~};+0w>OYYy!M$ zyu6Gt>CsujyqwmF&aRm@j2l`0tz%&fVA@+H!DuBK0xIhb|JO5|U?^d5XW(b}h|~{t zW|U=o!1M#0-yJuyrMNhliSlzYvw)T<39&ITdN46Ef-f9pO=V{U4N8QvgRWSOWoBex zVel1#06`9R8SRa13@(i7%I50E=HkZe;>zsi#_HzE>g>wm?8b~jF5RxK-7e1EZf@Pq zy2(aH$-25JMn)-&vZWQ3rKOb>I(}i{etzL$pt7xsL5r!LNgupsh#g$EsWYmnv8$Vb zMitrB&D7Lq$T7BP`e`a5Rt{;&F}5i4@hc$~4ng|m9*nXKM;Mndu`yJF$CuRqn=>3` z^k-)0n!&*EF92E_1%PTJu)OSlbB5hu`I(sV;4`6;7)=<Z7+4uVt8O9pOE58k2N9U! zm>HQEnLvv-Ss7Rb!7CTU!83jV4ol`S?)cYa1t}{e|0gm01)CuUY6dVdG=pb@LqT2t zIM4<zKWGC=7`*s(ufvlij8gwRtUzHCz$nCM0`7Z*+FcAx4A}eZ#-hp-K-z>DcYrKq zWRPSOV)zTzC+{EwT5f^Z;nW0L2ngy!L!2xO?s)B8@`O={vCirrXtozL&;LJ(@fp)8 z26YB*h8ql^aXfBtcuD?GV!RGof6l=38?;`Bhv71~PkWi+AhQS)GlL{U6=co_bj1)O z19*ipc%-bA8I;vQv*;3_mJ$OKb1DO<mIvLL02<q1VDyDeqk|1Z)#-p!Q)Fc1HbXN* zV^EurQACc(SW#3Fd@r6IgvrOm$f)TWq0cDy?~zGHh_O*<x+#dQAK|LWn53gG%xu6W z>tti&B+F*NEDYL~!*GS+AhRggFD($i2!j_dFfb>Ac3;MVPWyr{36NxfuWx2%2A%52 z0N#Ypzzp*ZR=p0mwSk80K@B5G2600(L(qZ<b5lj|wXk|j%8I6nMxYihACvMGMmdld zKwkUzh*1vawSyo}=%9ED9GvP5&l!#~{b%51kZ=H<Z3jLjk^vg<V!}*3V%ne<wJ~_y zKwMc(O<iBtN#D@Gz)9PX@pEviiCMR|XOFo-Cj%pcEW>k#-Egx&d%+-MYM}J$3pGoG z4RmZiXo;e+nAmPZZ6`y0LwzS*ea5dLZ6@Zup59$%CY}HPGw?G!Wz=AN#iRyWPr}H~ zkoCXnzW}uFAppvyptOj*<^(&R>jR@PgE|uvy8(FD1rsx9#W))i=*&p)nicj4h&;MD zqZAVxgF47=hRckQH7(%PE|)=8gU+sc#(07;1(XlC7BVab<pZvTpz!?vjPV4+LGYL^ zIA1a{w17s(o4||EK_j@Jk#S*RWoCy)tA$Lb{`~{pX#gsh)frDPGl23B6KHLXI+Gy7 zDzJ+M8ACw(H<$z&|AF!u$b2R?h`o%UE!*nI_W%FSVDrD8u^UvrGjTJnf|u{0wfFUm zvP^ai@}RZ%{EVy&;Jxpx%nYpQpw<1%h%54Zn0!P9Wd%hA*f^!ML1*NGjxzuqxB(hI z0w2OEA|@`Xqy{?uOI<?8$TLpcP+dn$(@;Ck(^T`nvRaj$Eu$l&tV*$-5eGB7p?;aN zETe;!Q<=I7_-r>u2I>Fxj0y<1Ft9R$c9t_Ru`+-<z2J?j(EDI~n33F~%El%tD#E4= zYBhn}1iHEal#oE{f<Tw_F)C=9c*klRYH4a}8EQxS8tUw2R8cQ;vT|UQRW8#vWM}5k zGb&b=V|28$t5Q>jl)*Ot>lr>WIWe#^$bd5+GZQ#BgU8=}po93F657I`i_48gm2I|W zXK!V4virNk7NTG5e?7x#xPE2^CT6BYP(lDLnDb$POag%PgDyZ<HZ^X{-nuoLiN)@( zEm%LOfAgJT9+M4t?KQN213D|v2j0I4WCqPt2(W+*F=kdq?%P`?@;eL5N{GwKic82c z+5F3`m64K?k(QGB|DVD1|98eTCL0EIu9XZY!0B@(c+SD>e<{O!#vPzlw4j|1pcx%z zMuv0-P=7iU)Sr$8dBImuNJR;3C3w3gWL}S1S=e~KyqK6A$U4?UerLvJISC0lX(_3{ z*BK`=FoH(m7#=b<Gq5qRZDwO;G|(0X^)*b*AEu>Qx3@DUc6Tc-<6Z#?H(3TPhWp5B z&CS)##YNd=Gcv5(+r7KHl~!;q2buZ5iQzGm69X@Uu!8`oUI2#<qmP&{6Q`KAxe0i{ z3e>?@R#TI=w)V5Ow6yZIw!WXb!_8}FLfmE_mo1>^U=aV`#BdvK4g<J^WAtHwnS)X+ z-L|pzu`)Ne_O-LTpSsoAXLDS_PA|`$;PB)9|D7?4$%R20vhtLd5!54MVqgNDe*ud9 zI*@Ik!N&m5nt4!DlO>SFM@3muMU$0N66|0*Ch+EV#F;Cg>#{^qVzxv^TUn9QMAOPu zUeiY}Aj-&4PSJ%A7S;Q8Y-J==6ou5(m2E}Dyqry46h*YOxnZ#ncBjPuT*ee|JZpef zcuF!du}DD<hGAg<9S#V(PnLlN6!Y+v@Cl%m@Bxyl+A2yQr!pc%J-Y}SJGfyBI{#A~ zHl_?3Wi~Zt<bk=1Us_vPiOWnw-d0&&!&lEA<ZJ~O#ujj@l$2bjYa=7Bq9m-UCMakt zD&py6>Y^yBr3I?g7#TzvPBUI%jAr14t_5XeVr*exU}6GqngK1hV_*VZGR(`st0bhv z$|kN28NmkCNT8ugQJE_Dpeiv_!AK{Q1kuyJ&hDO?`kMNXwqH5ZB*s(BtPJctv5eqj z%orFMME`$d@?o07AjdEX(lg-$9r+@{$i^Vb$j-*>CGCJDz{$o8I{p^45<?v{Ccqxg z#mU6Z-tEo7%EZ6`xni>wbkQDr0z`rxbmbz7BuE=)EEgjuC-^Wo_5=p-qF>0Bi*gKd z!m3K3y;Q=2paV6<1(i+B&5cEkL0D8-Py`h0qL5%0R2EG12~aSZsbD&5mcN9Sh={4Y zyZ=;1#w%Jf)z#J2ZvW0Eva`Cdb9qNIn*Y-U)xFjX3`{SXPBBO`7&{mUgUf!<h%_q; zsPJcD0af;lP2TLF^VOTdyRW1fqy@!91O-Jnc%`&KH%Xa+E{Kt1f}FptrY@)~2)Yr^ z+NL=tBBj+SQa{!$!Y4*+a^(y!_N!OfJ*HJno042+&id~+BP**#S>jYso@Zj<VqjpB zXFA0o#vl(mIf4r`UJu>_#>&XR!N|tWz?RAly6_LYtR6HE+05=EAucT`E-xVu3LF(h zK|ui?(6R($Q&4#dnOjs=hMYTUs?4Y?2wwcI#uXkOzNfjqyESC#(x`Ao?kM-@)R2>r zK8!J0scFTLjvXD0-~N5D@pEwk=ao<f2BvdNrx<t{WI*m>09~Zc$O<}Zn;{l7WDXv& zYXvRKVUA<xU}9zlhZ1PhhmZh29|JD~ud=WrC$G4+u^`Avpc`e>l$8aAL0mIa&{1e? zY|6@^&+{?@y<K`z13P?t1NBk@A26M&N&R=0(a6vLZ#9$1Kf^#@#^?Ve9n-BDSQy0q zZ)94|bcMl_A&4QGA)O)HAw!dqg;|S{fh93A%tu0ii=CU(!`053k(G^`#fz7Notc%5 zwS|e1iGhcYi<y&~ffIDj5@fE(n}M4fcAs2GFatwcN<wUKbVzidKZ7TOr;D?by{(O& zj=HLfsv19Nc_OH01&^OW?mU5zVxpqpEd=U%pmiRg*%R0`pdw;o;-J<i8@s5oDQK`t z6m+eSx~RH{IJl%^Ggo306%!LTH4zsTGd41Z2o`D?Dkw=SaWD($OF4-!2^xy&8Te}X zn;C}~+GaSLBpS-=1!>y(X}B9$gc}9=n<=SDN}4Iy$)xJ@2y$_(R#lX-Q!o=3(^is{ zk<s+86th;7)Df4G6*ZO-H?%QR^wu)b5D_-=v9<}-6%+Lk7O^tb)K?M~GxoM}OArb6 zWMOpkX6E5c{-7ewrK!rtXKBX4!))srX~@WCuOX)o8lx6uU|<pix8=AQydk^Nq+wN1 z6Zm)^@Q%e+P+bi^P*swF1+;9Qg*g?WjDd*>eETjZC<Q}yL<uX3D#BI;2r@DJV_?*c zy3E9Indy|@-xkIPetrzh41E8;GyP<`$e_hw%HYNj>Ja4S>f$Ub&BnnX$Hc_Up`{`N z+JK_U2)a9$frEjWg@YM%9Ts>1vCA8LcV0Z`_JmgC=@th&WffHw4OJx-RnRsFb9Hky zQ&Y&<5}?&Rir|aF!G~_Cv#YZs&3LFY!_Q4LW(A9zs;Ma}ii(Jvi<>V~jpP>cOZ2p` z^<`w_@Q>3nG*B`U6AbkV6N>hcw^y}vOR18PQInNaQq|UDtQAlakrHHd^!xXgIm;~| zPT$+oGtbe-+%-JIG}O?}Us+rzAjr$xo|D^8IM|<YhM~E<jI!okVR=Ip1BU`Ng(!b- zP?-k0Xv%|GpFtCJ5|ywZJ2NYjf~=Sb3kxfg7c*$6f`x&RB^7)DG6NG+lQ%mf0|Q4a z0|!SlhmW9|kfFGsqqqPkxGaIpn3{qw`~?s9h>NPJgOaea5;#GF<^+w6MHS8W`KA@- z+6W4Xi`i(ZYpUA_2uO<Cw0gEiwf=qYr!k|TafM`l8Y5#`E(f14AN%!tjLdh=n(Pg4 z_XQ0F{Qu7&_x~%?L#9)V{7eeW>`bR1<K&Dxm`;H^6^sn3|DQ2AFhw%RF(^35%1I0H zakH?2#t-=!!Lvf(`+iE~#F^N{v|06-)Y-sYDbTGq=3-*vc1)(md`zHAf0(${<Fd>g zOjtabo&D8370etbIjb8=`mv}u=;;M2N|`jYIY#abh`W-k?I~+wyDUsED^1h9rozxB zR}a+3U|{<H6;$>x@-uKU-GRB28R1SP1_s7gOs)(94B8GFuzoIRAc%z_l$nu%fhCre zk%fT;v=~T$K|n+ibi}!Ywy~)q=r$!~Q)5sv6Eqf8Vl;D&YHSQi(ebrq)Qs@5)lQ52 z_XylBW@cdj|CKQpJnj$LlNbhBNG}1(R`G1iOwegWrUcM|D&VZAz{beT+yp;R4m>{q zzV?<ifdM?>8tEXw!y_djBE-Yb!!N9&#Lg=Y>e+${Y;Z$Il~I|I8C>Xr`WQ-T9N>MH zQc?<xssEn)GD@n!`0&Yt;J;=}r~YO_rxF-IyHyw%w=lUv_p>lEGcdO>AbP>z;Ym=K zK-Yn@iEAr@=EX#n1sRq8Jz(7A%fw-8`xms$>HmKQ<^M++^%*jl*}2Zc#-EsW{DY29 zf!8^eGU_u)FfcQ)Gw3_$fCeH!vq-57pynKC1c^BoR1`7$qOTz05!Dt3?+Q~CRc7S* z_l1#%@u{(KfeCcq0LTBAjIK-p3{ni@4kF?_+>D@g{-7l?3=Ab=qKs@J+Tx~m%;3JC zx+!QkA*k&wuB<M?#?E2xqRJ~K8&Yp5!^LLpuAwR=?xAR)ZK^&;QGrXwMN`1tAxlJC z<nk6JI{|PyQ~v*#QJ=w(nVstr%#Vx+KQb~XGp=RSXM6~{l@hWt2HaGP1$C%bgJuH- zkp_xDOVJp8BOSQFGdawH%={34Dl_sht_6FJ6LiNi>n)}$3_J`13{ngl3}y^241Np| z3`q<ZA?Na1h;xZ?h_bRt2}truu=237@nkTtGBEP8GNy}wMmyt$nT41^X-|-!iI<lt zo|}`M8C<+eJ2>FhDhy7U5Ceo5LE1ref;ZR<_IPe^OA>Ujx_4A$fWMoog}IidjI@v- zFE1lQLR@50R8m-oe?&lpkC&^To1eX{xr>F1p}wY>mYK4mw1$j^gqWa|kQA=~uK+(E z2O9$qBabv_@&uI0zzN&b#0+%CGb?m+ps5KfTne-d0#WE=@(_9y;kps5!)7iDzP=uQ zUQAN|O1v3+{?(}|E35gi7@E2$`1qPS%X|NwVCt&i>uc&H@6EUpi3wKs=LeW#6arIB zSFBPTb4$vyvrD)@`O3o2-#=jf+&fmO4!I@Psg60t*jUcK0sj8;<|32eywk_1&#-`* zo$D%&bY;h=&lJD_J%@mi6}00Fw85MevP6Y7mJPhmn}Gqohnt;8Qd=0*=!Bkf-=C9X zl9R(|mv54vZ<7E2KLhA&&(%z)7~C23nRwviF3b#4{{@(Gm_X+!@G}U5ZaCuQ0NvQm z$jHhDuBaG2k+LoW8!HoADg!GsGixXVD=Tv>J0mkIvo8aKun_2WK9FmK1VuR5K%1c$ z8BG;M6@?iUMVZZ+m6;Vy6{Q$$|D9(t`#1UbzpIREKkEMQ=3{*FFZ6>wV-n*dbMt?N z|28dSJZZ?-`DY{OY?uEGuK&L?O=G&s$j_j~beZWiygoV&s*ezUV&n(;304ol>Z||% z!Hs$|P<_T=!(@g<UXwwNc?Hu(#&!l1rhoq*GB7wW8~|zJ+{nPR<NrgDEexRLYzqIs zGc94d#30Ea%b>yF$S}#FU!0MJO^lJ5g^iJs!-$ca%g&gIhufZ!m50rX4>V`Qzy>=0 zpM!}5v}>A`hm(huvyK;3^)NHAfCjYKIhfhg88{f2xHy>78Mqj@dAPXK8F*MZdBPcZ zcsS$v898}4{k1fuq!<}&tjtXf^fVo{9Mx15<fLS!WM!m9g!p;6I9OR2BpD?I*hI92 zjX;+V!EOcw-GmJ~vjNnF2j?wN4<53v0KU2r(l`c<OBsPD^ENs<>GOzKs_I+Jv(S>2 zF%}b(a$x-O&&Qi_%fI~wHd?Zh#=4ANu2HdWQYJzog4%+S+FDZjVj?<Bm&~m1Xo*T2 z8YQzai786)a~tvsaDej2Dkdg*X(=&%jlb`LBVvMhIL+C(i_NWgxoo(&Vc}X14_7~i zUWYE^aCKzl;datz;^NU`WaYvdu3Vg4xWbhi6t3J1JY3v7=?q+~oLu1yTwI*-44hn? z{-8j0bFsHIHUu4^k1a^~NeWWXUTElvVW9CoaC;5ZUS$Wbe*slMa!go*_bMcK@4<q% zu}|4tT-sJvRZUID$lTLX&QeB1%tS_BR$WKdK$?hv{uh$Q&1J#EFDW7}At<D+qp!`) zW5mTPDkv^0z%2pVZKv`77t>d8pFxH}pV7#B16M$VgD@{63$r926X@=5CRPPT22K_) zY3~i(P$@=qDXbbIBO!)aaWgV9GdA;bF>x?)GI4O$u`x1mvU4)9gYFB4-2TGK!^p(J z%9IY8X5|cL;N%4D4drA9?MDRJX@*5FGdB|>0}Cr7OF9Ei-3}C39U1AMA}h<v!oVP_ zFRQPkrJ=5(Brhi}DkQ+o$s)rl176<D$0n-H$f(SSGZV22g02?<^;n>%{F|CGRY4+o z20ZKN2tcxqur8B;Eeng9jf10ygP{iKNC~D>{}y9p6@`DBwGFKtZeDS>G+hfCi#PxO zjrlZq?|Bhu?43c45kxaFfUZVRV>-nk$)LdC>fi(}$>n54_*ht&MVXjbS(v>*lXlIZ zX)N%99~K5y(2j2gHU<W^P|%H|v7q@fUqN9(5kUz7b~b5kMMX$g8#KHEn`AZ@Wn&jr zWE2qt%^NeR{hQ?JALQobq$en@CuL}AWWuQP&q7B}PiH${PIOcjUrq*-m@zju<G)=@ zA||3t<%~fNL41tXyx?=0SwJhdSd*B}g4e^#GN>_JaS%`xVB+Qy=4ImH=3?aK0iBn@ z84%&1%fZUR#|%2$rG=9n)WqiEX5va^;AY_A<>pBRt=QoWW#Hun4UF?LfF`j(3EPl? ziH)flStk=40~a?N9*rbg6d4J+kwQ&X7Id4I1n9bI&|x2-dk+{O?Pz{bJ6c_tU0K*z zSW%go8Fc-Du&^;RBY43w=sY4-HPBKoc6MWCL$kAH30ER6FrG`VtxsGzGo`3Faq%D4 zNJj5Z*RL~vV-a9y=VLQpa^?)v*}uk&3jZE37BNQr`^?DuZ{@#Urc=KE8GLsOJ1NLI zi(dez69xw6V5D@S$(Rgo*MhoxTbUj*v4JO4nHfa>e`TD@bcI2fL6$+2!O6iMv;tFv znGv+j-HVlxk%@(gkp(>51YO$1!onO6>d$mD`^d{NFsLfaY07KL$}k8s2&=J!&TwKz zIk6iw7N>3oIm}xfc`N#4VKogogD8`@Tq#oxF-0L>UTF<gZf8?PDag$;ry2Kgidc$E zL<j3Sn#&3(8>{oo78R4%*OfA{Wd0j3C$AtYs{nEV69fDIZ_F}GKNwUQj2O}#Qe>E! zcz7ATK$kPHH8V3Z@-Xl)GSqRhGqG~9FfnnnGJ#SKFAq0wIzMQ|5KlNiBM%QlJRc(i z4}-rt<myHvbt6T2aFjxtH@u*8syNu$SQ%6qRY8?5tCAWhVg;2!&=hpZ5EzRpgJz=O z2ZE}D_Jf0JV^L8tF(wC#5J`3+yIJ8rKH;+>R<2}bVv!B7%q%LZcK7yj4|Zg9;?h;~ zWQzE^f+@m2F)=aGUP0DLR!6V1y=IDMYEnvKT0B<_Xa<LYkwNM|KjUMj%M6O3@fQU~ zHg-lv7Es~H%ngb&FL6c&Mo$KI1~yi9wp30ICRWf94=kYlz|d{(sQcL&7#I{86a@tZ zg;Z2jg}B&cv>8R&#l%F#Kx;=3)tj=iskx%5G3a0qQFg{tZ`$e@?JI%{{q*8>qa)J_ zobLR)pSk<r9me@*8PD3>WKK_TO0i^A3J?_zc5!reJmbB~#|Cu5wa))<Ol3@084?(Z z80r~X8TuF&Fl=&IZz?Uu&%(qg!^q5JZN|jS-rm|&S6deD$-%=rdukLr3qKF57Xup$ z7YiF#9Rn*DBNr=U9RoKzFFQAH9RnvnA3rBw9Rmjg7b^!B=-?mF2rg*Oj+q^_P?ep5 zmxrA<1GIpTpOc3_U5Js7fq^evh>?$vAzpxyfses|LVsy7Bg3p|{R<{6s3|Y5FRd>u z$jS%|aB{FR*Von3R997y19j3F5*QPN*+jHimB6*VIhZsvHwQ0F1~;nA;nNVHp-9lV zH((l+G}z6Ijf}*>T8zQd;%uOB76FZyfaYF611FGUaD+jpPlJsF9V9C*CdLjvO#qab z*xAL*K?_OM+0DTdAFxBX*_1%X>40a$8KrcL#MoI4JhU7Y1v!;ec=-9b`8l~5S-9Dh zH6#>xq+~?6I9Rxa`FNOxxtWzj*@d_m8I7E^-HkxKI{^uKE)`BiX?~?tTXrrnqbsaz zoZ`AF8me53EX>+T$2kQ#IJks31*Cb{<SR>56r&9`3z!)2@M=zwm1kw<5aJL}6JcVK z=Tfm!6p1wD(9~4Wlwh5}%Eu+h#Uacj#K@>AC#%3ME-oy>C&0|BAlSvsD622v9;)sX zpdn)-&Zi<OrX|nLF0UmnEvjn7Z5|=0Xr;oqPgql2#?eYu(@sIiNS0Y>8RIHOM#kmL z>=N<9A}sS}aVjaWu}jPSdtjo?8)nJK$Yc~Mu5H4=1gdA4;+ak{h%(4BcsaOpFfy_U z@$+!9FtIRsF)*^RuradKaj-HoGqHddoUpSou%|MxF|vRwM$j-S8)&>pR$4-gL6kvM zNJvOrk&{glI-)MZ#x8CST1%wPu57NZEGTGf1R6tDPTbkm;^S&*5t^0}?dTR#>^C`W zReVGw)2XyH*JSb7sA<cxvsc#tyTMr8?gd)@!T>r_iGgVXvjBq-gCv8yg9~WNtC@k3 zm5r5=t&W9}iIb6mgNXsub6{oUU`=OWV`m4ukUf?Qv=iEwfk9G2M3_N{K}b*pJfO=B zs>MO&lOU*U0-sO;x<G*ul(N*68F@3DRyj@T=$HgLXf8i3shFwjpBR&hk561~aw+2z z8^%aR_YiN;NplR$42l2qnc|r<8N?YB7}P-fw%D1NSQ%85<(b)7c^R461Q;2aS-e2! zys<ENurM+*gU%QL)f-Lhpa~RKHqa$+Y|PAH7lRHbVq^A|lwf3#mXc7AR1gyt5eC(r zpvC^2Y@llb#Ld;sS=q(FOA&>^RVS#V6;~ECHZU?4RueZ-2VEDSrmxO-hmVn8s#42Q z<w)kTH1mvvr~_%)Of0r?O$O$sMog#JY)wpD^0j&`ee`(#9blCC7tFZh-%X}de*eBR zGP5;$%Dv=v{0|!c{KjO>bcKnH@fZVWeBFXEnduSJWd>CSE0Ax+8JSs34K*b|W9ZC` zpnI%X8CjT^nOK<XxH#BB$8|HZG1P%tSCE_j*cceY!2^Pvj0}tn{%UHB3~E+tRwl-} zI@($q>M~NI!W`f$fw@7w4JFX^g`i>r)P(>~VZgf(pdJLcKrk0H76z}iGBGn(W>#ia z23<KjU&d5W*j!d!LqkKtP|4j|&RCRRSXa_jQ^UmEw<9htG1`HNktNkMfd#a3fs@0O zolja^Qi_jLTvOS=fSc2hlh;T}oKHww$Jm6OG2!1wM*e>b&T#1SfLH<=Y!^X&;Qycg ze`gV8l4tN^h+)WLs9*?k@Xt>S_Oi3kQIq6k;TGUxVP;@q@&eVcygW?YEUet=TpVo7 zpp|&xU<M0AJTns`XkVCrd1+>Pbflk;tFyYgx`v{Pii!dso2)h~q~t;&L8Gzo{k`CX z03O{p2L%x5j3hQt(FvM;L=`Y*2F>|^3R6&7$%>Q1BFbkTs-PHV1|q}F%)%5DgH8DO zl-$JSmHF)L>iAU^#5`4$+{NUT_-bqURpiCpRg_%C6cl;uYWY+Z#9Wm5esRjl$;ooc ztEwve_2rb6lau38R8^H{i~_M`ITch?6gWkNg+(D)NX(d@ODQ5+y*T%QdPJBiw*Wtv zVnmGkg9qwS;YuK$N_Z5U$0(%|=r1qtAE*)%6cmDk4H~gvW)Nh|VwQvME0tsjaR>z6 zz{$$Q#J~m`Gh$<4=U`(`<!0nyX66V5En{Vl<>%q!<Yi^%VD=Re;ox9k5Rnv-1hsZJ zggKDfJJ43F0GqfrXu+MLs4{q^AY@L{SkxHID@$eMN^843rK4ktlYKv97NeG#*}rRy z31(4dDQ1ib|E`&VRvj=f{Lg2KV9sO^1??sk72;=SVFjIq2~HLs;NxbRp&b;a5+NZ$ zX%;pKZANu-R%S8KS>oox#>}8)n!?P+!i@2^1Q>Z{@CdT8iwP`<{~sl5YaVhauAAwP zm9fv?<H`=Q%6__xC;plRJ!Z6FlK)o<&d)mk`I-5c+8O*AVi}wr96~%SIaolao-;Bs zcrY-rfVKhEff73tWQ>N9jhQi>fsK`wEu4Xk4YVVIjnzLUN=H*uMO~MjO-fr>99;E^ zo0&l-`^7{dH%dU3qkw99&{=85M&Q-c;2leBZ0w+=(x53U&}b&O*<i;cYy$29fah_H z%-#uT8Hw@oXhdoTb*tM4YpS?qxCX~)sY%FbNijQU7&~bQv$2Wksq1B#7(}UxnHtDD zn;H7+sYpvIYT7y|shCTPNvN5Li%B!Ji>do3I?9?znZ+o(**NDpnHR)b1Unm<NsB8; zGjnolYMKNDJ8|=wTWgu=Xr!2_d3t!NI|La7n;B&rXvdh^qz0J(Wz_T!)Yb`7)^}1? zWMKS1fngQHH^xQ=0R~g>DqJnl{W*+Yyr5E_6+8_DEA_zh3I2=>+?)&oi~`KOBH)#` zpdI_{>gMpR18hwij-p9In*@2~L`AfOg(PfNS-45M=-KEf@$)zdh=JzP{1_M*FEDLj z;9$^jP-SIg2CofcW@HEjjkGf(-5tciz#$|k2)a8+ky+W)SXj_lkg;!1%jRAuraY&A zx{R0q1|XZq2}*}-j7+!<<YeF!5)=X(D5$P%Y79E+m9cNtv~`o57>}%Snl$Ne00Scf z%zY%8XNqv)&i2i{PNx|cJN?_r=nZlrxn>H2%!IgcOApA6NsK&BH*SE9WME_fo&96T zw1I&Qv?ds|2AF}-iwTtWLCcLmVF|h7iH(6xgc)?j6SFxe1mE4VpY6;vOYENl;{%XM zpms7WKI9!_I6(bGWP=zOL03fyihynnF=jRgS6G7Y9@s8k;N-+KQygLp;{(th6KK4$ zGpIT!fsVyx^kQIS08b-=*C;bV?z;i?qlLlu-7uSj)+`9VzG8p0*okSD*xyM^c_1e- zFfwpM&EjN`b&%o&WovNR0xEYvOWo8NeFR0}E&?6cEBNlQ-HN$RPE0ez|A{lMw*MQz zxB=9sf}11bAj!$X3>tO^FGU2eLP0Uc9CX#F>ANd-+Zvplm}ZIoJyXTF`)>ef1t%ke z6!GQ=L!5KPZt-M@IsX_9F8>W+bO5DQ&|D@c&cI>B!UVebiP4K0Gt$@@*hRsS2HL!9 zYApEfjLrgQ#v{88Cr$*XQP4eHkUXU9pa41_3vx~_ID8OM&%wYU1~Nra5R^yFLD^@) z3EkNXoR~tV?bDw$4HVdnppy`w`BB9|k(CLQ(XlxRZWyz=xi}~#G4`!ItaB;PiSgJD zqhtTBGJXUZ$H2&-0S_x5@QG&N3kpFy&BegZV_;;|04-sF=XB7{Tt@IFM9_v^xG-dA zE++%0pfIEi5L8w-7dJKq`C`)+g9X!^7*B52U$7J<JUBsVj}4lJSun$ulYvtl93rM5 zUx1d$GxjYwp>Z|ashH7wpULsZjH^)0V`orukOw6{7Esj&c0VF`VUeUL$Y{<8E}Z_& z&||c6Vw7Wy)BAUY(FhbEj0{2y42&ejxhXWK-mzKWT+XB@@vnlh17s8f#4Jd>t2rpM zGBZKaGZv4+V@r@3l4%+H7TmE}u)v9l;a>%l0<xLn(D>p2tpk9@KblYBrl~WV3qwQg zy4}SLrv;2(rT>X#G6kc#j}vqn2IvSANV>%0KzK|*90-oq*$?ejOmkv9`tKAIyVSp0 zMxAi5u?&n1!caGIGUz&JF*7nUF?z8uGI@aJyP+Gs;pv8ffdMJem?}d{8OFYir>yrj zJ1u4WECD)d6l5|YJV|jcJF~eksIXw{TYS@IYqt}l$`U3CiGQALjE3N}1FGamif=(^ zeBZEG;LNDPn5q2l8zUFQeXw$e1GG+&#Q0ux$pjSN`u{#LW~#xAWME`ahK41P^@bum zaZj~kG<0I>SNwa6Ne$#Yl=4;GL50Zp28SOwz876GSu@3n@z@f^Ox1r6Cij92Wn=(_ zA1I9zpO+D-a_woWqm@oAYZ+se{;jTLJO?rnRPL%n{Y-ND5k#b)75gpLH99f6F}f@N zyTPadj%!36Eaf21#t1Gu!FdN-Z3=-aDRodUPcYd2mb0@M)2zP%j1NF=g5^7QP#wnz zPSc=vEh7UHXgwFGZW0t?23?#8(XJe3f90uDG1vg6JO+~LZFON#fvwE!ZTnz>lhXom zrkP0fHY|;Cg7QBn2egiaw<MrGfiwWrg+c8RWkK}_yDJNvoEpTKX3d!KH-K?B_?|lC zdK;7;Km{kb-e$#H5;H@b$JjS}mEGe7PDPCM{{+ODX8bE>?1aT1X>J627Usr&F{W9d zzVF`vM!AomaEG<;I2iOCwAmR!OP&}Q8JS{PKnE_VgU+yrv6(<;`ExLE2trz+#-_&L zh-CI_+2iB{YK8s{V7&ZK7o~lt@1Vm0x}_M@_`z)`sDu&%S8E_c!7T=6zt-JgQ#sQ9 z1~7L0D?@fGCur3Tspf(TDP>c3ziF$SoF+M~Vm$IU0DLtlwcOji)yb*HiLvnS-vCDG ze@{W~MbvfD4ifCp_6>4|5Qb(5u$TR8?l?P3Fa^2&4PXRa+5oxJ4wUyf7^ED;L8ldf zI|q>E^Pn67({64os4U1HYIl8slT(H?<JSd$1DJv{L49XLna%;)W5EHdBas~eOUvMN z!t7^r2b3u!m=yk1FfsfMVC;aHgQ#<yw}S4*1l_X?Ix0YvfteXJFU6n%TK@&!Ri@75 zBkdpzS_{O)zzoX0Fww|upb>b`rFh2b!l1T`vY@h`_0?rgPUR9z0v&$?7@vV025uvO z{0(XEf*cLbHz2uKv<wJ#v?+6d-Lq*<P7|e=g#VoYwNn!rLFo__{zRGss>YQC)%|R4 zE_QP2mSB=t!l?2$fYGoG6px5@D*kwcBr!joGtSP2yFm$TA|&iU`a$j>5Rae^fU+R7 zpU&ZxPEL77I~b4s4PgBE?<&aO3?#O3LA%UBeq!ufFkAP;0;g${^!H6;3I&xQjBv9! z8KfN~V0|2rEoh-@4r#r!`)Qn5;N+BTvX9Zb_-_E?s>dkpS}g~47G_946HCJty#WHg zQy0`~owmSW%O<Dgi}bfL9tSy#0pTuU+nMUncIGiX#tbJ$qkmWQ7~>e_K!!o;O-Q=~ zf1H66kf5K%4QFR%#!PUegRa3sY8T+G%NRjfFV>1N+sR3hssC>PlNwSxfapADa%qv1 z(*iZd%zvMl^#2Ahf)>&-K>PqoH-zFz*c@Ddum@P3UhCvksl*t&w&iaC<GD&Cw-Aw+ zK<P!<RNW8knklM`nM*(^uy-;jWiTMx49X4)kOBZp4oA-4pdO^4a=gX<6;4i#%8aR? zMgpT7C_kw}`vBlOV?ZNhSh^GNjykBfW-Rz_w%$o+rr<u~9iZrCV1&De9n{|kRaT&~ z5p={3BSR`^ZWrFY78V3`uN4KA!6}ciZ`vaLtzVe(78+~@w^{U{Z6V^@l+5hv;IRY7 zzO60lC#O3xnHL)E_@~8q4Qw9BZ;&`scTiy?s?HY#XI5|<aN%s-lM9>}_f79N+A$5> zBZY+*iDsH2T)C@F^CYMpak|2I$G>7mXuFaecOuN((60q@Cm*9_lktv!&lx4beh0Nn zKzWX+c!f6euh>m<W-1f==gxQ+ZU)G&gz^rgAEzw%?uzYn(BMm{#6M@y7!Ig42?{fi zTR`T3#}-H!e|dMs?qseLQ<>P`ZSe6Im|2iK1dmftzJQgO=;JR}Y-fSSUrHtZSyEyA zrA+Mad#vLxM2`nS2Vly?{wcx+U=U+_91PxDAx&=Y4WK%moS_MD!w@=}$gXT^3?5&p zJ-lp|6H`c@6R5|IY9C~%lOp>theMamI#uSx_{{0w3Q$r2#ShZ>rO!6#a47kfA`O`$ z4RNlWyt~zDI-|eSzyF9fI;>sI$v|CuAw#v`@s)LxK>bDColdhEmBD#O9$KyvReu`8 z23F2nO?95kI9=}F9mXC|;fmbuByKznF{pCga>g<zCZ>NijLVe&U1kKAV4%IJurXCd z2RT;I%qgg_Mje>NGN`iXtksEJr|FE7<^EM9Lk3kC89;YPg7P&dsN4q)%rh~vVW~c_ z4X7-*WI1zz6XTJ82N-85{ky^F5CQToXrC*z4&VfxWCzOLpsU1*Xv@F{Q&t_d+T7%{ zoUvK%-$HO}2H|I--3cBD0LSa>D^{zzofwT)F)ot(w`nG$EZCV0j0~VM8&tj$7r&sv zQbA+EcPI5`J2PsZG@Q){8>fKP<s1w|#xH1$6Vzr~a9qD<jT4jpzmKO4`ax|aNc$G# zE>N6sfbW1K5Wh%6jVE<4WjQe#oiMy~l1TyVNr)doW)UB5pu?2FmFI$!`m+`|F`iv; zQh(<BRy6a73NuAh`0&njJw|J%35=PB|Bf;qgqj6w$CH*$AS1Qlm_4z?VA3ilMz?Lc z3qh?RP(1}Q6BMUJ)tQ3KpwW+a6Ku{nGco;JDghp4MvP$*-xdO!!`N4{$NI!mro4Za z5R+i;0fj#a?IHMx#j-A&GYgy;KmXez!T1p}rUEX5L4E|82^m)eWiW7m4&OM4pgJ>T zbcM06d#TOI#ZHWyTK^SDFn(OJ1QZ#tar%u6B$#Q+40q|GK9Ea&88`e}CBgXVUo<1w zRAm2?U@q9Lpne2n-_+S4a~ZWh|LXz;!7oNOkZVEag7O^E@eDdb)mZS|aot(Y+l_l6 zMISW(gTj$e+X#|fl?C6O)UDm`<g~-6n91^Q0OLKFJ3wZD@BIPoSY-r_&ET7u0?jjk z%8}!`JDQxf8?Sⅈ&zyuw4Xl5V(u-?xb$hE|7~V86U>|4PXSH+W{`uKyD(&97rGg z-AUcq3qj`ePiNfsH((lSIYf*(p!foZQ1?cVISq^!jC_9s7(rV}z;z5Lu81l#j6s<a zG$}R1>XI|##edi380YwbVx5t}9$K$}%S9IGJPasDfVWXWCM4nYo*+B3x-hual)J)e z?II_p;N*YX<rqVzL(GEJdqkGmh@`$^qUEIpPK^8iy-;SH^)Hn1JH$Mg+d%CTX!o6> zb`fG;3RJd$%j2TOR$FH|F+TfuP>wMf)B*&hKbSv=E#Hwc?UJb=pF}X`|9b%P$_&OW zAg?fx(Ix;FF5nV*%2JS-jH-wJ9Z+VRc@Z*t0CFd+{l)>BM~5_yam=qEw|qf!)WV>e zGW+V%*-nfP{vCIMwg*tmgEW3|n+I#zf~V!d?gUSHFL!EZ0w*n$F-t86b<kKl-Zl$- zq94>W2aW15_LZJqJlBb_^N|yy0XT_KW+o_GGWJbc*R#cm(P_5Rent*x+<@8!B;+M_ zb&yHm-aez#Y{Qd`{7`d1_XvW@2qN<msE-bchpde{%NIB?DV*G3upBZ4$pGF^37z95 zs{MoL_O8v*TfV@FQNGe}$G;Cupb0KeZf1bFji~ZMQIJuc5u88%J=I}!cVc|>@2CM| zW&xT%iOwJ3!2@s_nO6()>)ff`20NxPZpSc>lyN%fXbC6;d;4@2EO%mbn{TiY)@Ct= z_A`iUv#^8PEUjj{otcd8sxq2^9END;k=$nKS#Gxd3sc^|7b*~=km?~4+AQp_S(deJ zW|yWrG07ZLVKjpd;(+56lx{)kjihz}JFLwzWxg3`d}LqG7ZpY`=s*s_&m@=$8pi=o zS26ajYXzBE#JJ%M$f=Nl9J0(5WQO~GNk7O;Mm|R6+aN<B136&-OOq1E(6OA$=5w4G zQ&j(bXEFfyM?q$Q!jMpVh1pyjJeBnQj`^fjPEL#|s_&RY{{}E#V_;-}nL&Je1==fn zciH@4HOQ=gJ0UF=gjodJ2OtMQ+AEjMCang$=qux@qQ3#43*A9yX#EG5AB4;iXNJs; zE1SN%Y`$WOlM^Fjs_MVTa~YTa4OoHXH$vusR~mtbQ{P=So3jw?o`1g><zX!rP<;hT zucVc;pf(F=RF(1O9HWyJpj-(v2NY+-<ZW<K1}Y=|?NwvUbYhD9_udGSDG}+CgSb8> zqKrLWV6=6M6XS(0-P`|SnBeVaL>~gYjs~=>jY{nm(6}(TJf1t>VB>Bl#%CLJkAhk& z2tN>A79)HC86fS1jgQU)=Lb+6fx?zp^B@Dm;IbV$Fl-4L7@p231Bxw12G9XR#K*ZQ zv{!LZcZ&10gGN(8^JI{AvlhIaN>qI)2<pOvTRC%1=rwI|Vq*I@@t|G}<1&z|KxqhO z9#Lrs-s7I5$7trn7@_y?8dC_!Bt{09IYfsS*kRzfTyR9MZG#iz)_>3T>NPT&mVv_` zVIHI|1TCDw(Fg-gNkdm!nSxhZfn#CA7K25ToR)9WoeOT&f&2<OpPQsO1us4Wo3`Mv z?z9C?jQZ2}8cmuC8Js~LBh_?JBf8xQZq|WvV&VDaGo6?eZaKl~YO>m$pk^#+Hi)rr z#f$?DPK<6&KOh4%2={@<KuBwMf@Y~0`^rwOpXkJRsK#k#KPZ+#VF+_0%uG<`#vL!P zwyY_lb-Z@++R0A++nr`H9s-;B|3AY-#wCm=n4U1GG5TgQFuF7PGGwxJfcL<2Ze(QK z!O{WV0nf~!`Tqw~IFmDj9D^2v2}895zYrS}6N|o{jHECt=o$k^A)~^;!U8_tCjhiq zk0G9!jfsIF&>M801dIbZ23ZkB8E6*>voBN;=q#&12A@a=aUE?&25l1^6EziSDKSxA zZU#9<ISx({ZE^51+u&u>pxsoUCD!8L6ZY7}mDSnUm?67bz-K{#jy+Zf?}K^~ndWG# ztQ_j;8t7;~L0veMS68+u*Iq5$LBmi}QAJZxPSZ(tfx3wuN2sTjkD{WThLx+Mt)ZK2 zud$Cwta84+k}6}Wn7*>SvZ}nS!M`KYQuD<0b@WWYdntwg|7PT5l4ay)Y-NmQ5{92q zAPfq-|Ns9NGnF&+Fx_HcXUO_@4^))=|Ns9iQz3&1(?bR}M!%&D|Ns9JW{UfFhv^>! zJA?Dz*AO-TmNUiuF93^`{R7=b%<%vJ|0bq5hL<2Q#-RUE5Ox0}nc^5$gT=i58$!jx znc^5$g2lZ5gN~R3=}l#dW0=GAgn^yW_doauoB#j+PhpB<C}1{ZU}yCIp99hRPn0R{ ze+$!n26o24zi%O8|8tn)7^X5UWngFY`R@V}V_^Jmz|72Wk=cuZnLz=(4jkibRzU$K zUNP{JZ)QbN#S4rJ|HU(Vxr5qG|8FpbFlaFSU|?oY2kVsq9gf0)>z*#qdCkJ2%BC6t zOh1+^VPIhT-@z2ZPyyD@&R_yIL6eo41$43xV<?!x$Pf!!SjB*GxD2l-#6_aYrp90+ zKY$Gd8Oy-*_ZL&#|3s#L3|tJN47Ol%jYRl3nOQ&!e!%83FrZt&&CS5TEy^t_z|X+N zz$F5@{|e+@(2fOBWj!W!Q)MwRaR}?HfT5_7xS1Z8Ka9cjPbo}>N66ArhzrgD`7ebj zj$t;l59sWD23N2<tk_vWs|OgE89}@085mg@z;}f(v4k_QurS3lFtITC^YAb-@JRAV zii-*h3GhLVspo*Mx)%m-`~vMuH!%|jpVOyq4B1G-C?YQ}A|@xdfi=*>%bUfF#aK_> zl9ky<T2@9zMn+aT!^X?QPE$-moq_Q`*j-Fd7<d_sz^($FHOR!+!omnX2ayFdvmDO= zxx0m%i-V1YnSqy)7j!u=C`g2j#9)UZDw~-xf$U;60@-EG8p!knVuy_vC{O+0$as~( zgXt6l8xt!ifBfIVc$L8$%x7BxO3NU5PcWYYEWeHMDuXYW&$bFAKj*&*L+bx4jO+}~ z|4aT{gPfiH=Nc$Sg3U+Bmto2?F#cchUyUK+{}Tpw1}(5Z<rzSy0D=yD25p>$t=3^= z0$sx;06M=J<Ue-M{yJq-<%m2dr@UCleU6}vg3T<@rnpcBW(EdEX7GhWpoQg3j3{P_ zD}xiLG1#n`aI^fFq8K(YD>1M#sDa%j4caXOIfWABIM5Oz29Tq`mzsdw#H=W)Y^u14 zF~#ZMJ!Yj7Jq(QhXEJFs<S_kU;9!sdo59P$zy#`?Gn5E}Zb}C0GBYzZ7H4B)PdB!S zW#@}aXUb%Eb><F8<6&U@fBL^Xg9Z5BY^1PZW@G{#YQn$_y72^b8wxXnzp$VPJZvDv zwy`LvU^cb@U5zcFVDZqx$k4(_R>~Y)CQSctz>xX>0s}ikFvwky>s=*T7?~LunDaoH z8NA(tfq^j&v_-&A+Cc&&$J~r51z83T+B6^tzLy*1C2`OZjHbq>iXN9d3{q@tQqb%S zMA!*7mw`DSRJk%j&W8rs!Qd<HAP%+?^UN%4HnM{b;}KOB1=)B3Y$N0U>HlRIGXFne z;AVi9S`3T~ETCeKfsK`knFX>C47Asj5wzD7beax$3nx3TB)HUKhiowy1tpG54<`=~ zr+GGRZZ>Xipfun2KZ&8}|2#%^mIeO{{slwQ{J&sGnrC4A@Bbfku(=ikAA<$RuMWDP zyZk_Vu0SWlGBSa0xq@t0OaO^81TZpy@-PD*BOeR25MhUHQ8Wb|ZoyV6EYHOuC1Buh z`Hq9dlbOwA#$-oO?ZC*u@Lz~wBLk9M+>A`1UG_|%t+q^zpaUovz&EKgfb3!d*~Q7u z%7SbcD8+zm0d4IQWn(L~vGg|(kmBHyf9K*jd4>ravnLA&C=UNE{2%b2^Zx+`ZdhFc zjyK4@OK`mTGcrKRLT0EpknjEbcU4qgUQ|?Gegmki1C@E8uq^%`$k6kD3nM$I&i-=? z6qZao{@j9uC8*x~FUwHy{{!g8q5nC5_Cw|O!{z7w7hnLTFgAv&2_W;<{a0ZKX5eRF zV+?2m@o)S$VbJ)0gPEPXmx1BW1CV~k9e*A`^fNI2_xrELunC+OJV9{<$qVA3MeIzB z`QSrmk+QqAgBVB#dKxW~1b8JOJPWdeiUMU(Wl&-8z{y}$ULK@;a{lknu<!q21~CR} zkX;T2tf2c-yx2j@a+sMww^cAOFo%P$e2iygWMF0hopLM2Af_m!D5RtWI)4?Mzd$E> zse`t)nHqx*gN2g&><rALq|6L$%*-X_4J;((ZLKYpm94Ct6qKA4!1rtZkN9uNu<HL! zMs`N;{|0|<L&M@WBrN{_{~!L}f??(V8;tCXUjOwWalwcj7b*YU8D{@K%Lr=M*!+13 z)&CN%Kjpt0!|eZOnAy1}GBEsk0hNCNm-qPZ!La@RNk(?Y!2b$=UP0ww!R2%Q=Q2zM zr3Fyi=Py4*{x3g7o`Lbd!v8de2mhBLwSy2R>VewrzKC{^xT2}zgMSW;2WB~d&;9*x z_+N?P!vALs>`W~GjX}}z|NnnKhVu-283VxXQTta2vF~3Y#J>Ok{~IteF<fNyWM=2? zVPN?CA0q$vKSZ8^@!t%FpNtud4h+ojIs&u>8I&v-K{F~0jGz<#U<bW}qE6LRQS{#o zrVBsaK|{3v^BJ}?Ok(U|U}j+3%*@DWpsj8$&OY&<>i^F(ZD%kr{<r#njNus5HwI<~ zd2o6W0kz%0Ted(q2qGIS%&5$G?B4+&MhB*Ee=abC4!Fl!e=snBYSnb`)e_;Lt0m$= z)f!6u!47G@n1W8yP&S3uAAA-@M&|JP17DuWV*s6k3_5g>Ar7>!+Ygp!n$aat^NhG6 z=rlD`<4ynOI6ZK>1<f-||DFHKFzo&R1bnt3q80=#(o01c7z?rqwN_U&Rb*8(Rowfp zjj`(AK1Qy8-=3V990!Vc#{XFIA*hQ7&4(?ZtjEj{&Inp@5f7RgVaA#d!Kbf_gS$q` zCdx`|q7ZT~dOqBUnhzNm|IcEwW=LaZ2j9cQ4vKeXb8&OnT})|OHrCoUHrm!UTGmW= zU@U7b8;IKvFgY+dFefpH!rM#W#tkFn)OJvt0MtZfX7U%55EO#7mq2G(f*S<jo$bot zxK&fLceRq0m63I^29eAH%KAoPe0(apQpyI#qI?2sI^g!30n;Cb^UMbrLHWjr8CpIw zBg*FlrZ@&oQ2&R)`M(9oiU0rqzrhs8u#o8=0~<rx1_lPk|7uJD47ZqmF|adGuZ)E> zw?vh1F>Z1CcfpD27bwMm^6Y;lhV#&TZUl8t$A1%s3UD3J2de{^kn4cO{{<Lg{~uvs zWpD%KGsw|eVhoH-SgQb0P?o`H2}W*(-p>Nc=j@;kk8<oTrwW@b6(C12{`X+AWZ2Hk z!XVG!53<9-MNF89g&B0V5+jqBs0b6Y2P+Gxamd2RzyhiQ7(i76=+tJ0cqTR`1{MZ? zMh00KDG5P-ULGzE26;w#c4!?dtfU5-a{-@!2D(qf*hmbNnjl>cV>UJ~VHH7DT@_A2 z4hac%V>U~DK}CKgT}5sYE^$eABQ^`Bcs@>BX=@f%Sy^jW9(GG<3s!az6Xb5@{~k<^ z4BMIKFmN*{GUzhIfZZO*1uA!085q;K*_l`wn3-5X=MA$kGI6jmrSmXyaB^~l^DuI7 zfR2FU;Ph8k0(F-()s=LWb$Jwd6y;^5CB?-=;2kGks7C}LB@cMm0n%{-9Vh_x66mxB zb7<KFRv|1SBP=W}eTvOOU&ho>4C*&IQv*?PN%mHdtcbKU^Bi$0Nl8gbDe+uuR~cE5 zw`65xWxHXbAU`twk6?;rSiu~_Aj+T(^C2kPLt}u6hlzocg(01Ri=B-toSPAHT~#^* z7Z+PR2QL#F7n{Gh7$bwGhMJ0!f}E_hq?oq2HmF<3!^OeQ%FH0jD9Q)*p|BEiEP=gb z%*Mu!Rn%S5NKaHuh|QSQP+QtqS432Z-H6pd2dgO4D+viHaZ_6f328ACyB=f?DE|fi zS7liL|1JY3g8?}HR6%!vg7@e#cyO{YF@h>1l)GL;gka+d?B=4zrsC|T%A)IQQ)+8d z>SOHeVq)y<K<&Z*OTqc(IRhJmD%e~p&|X<a(Al@mpe4_sN{WqvO$dDH5jb0zDw~Td ziz-;h#8`i*h>x#ekoY&7fq^j<d<PLX13!Z}gFJ&egFb^fgFS;ggExaeLpVb`Lpnn~ zLpeh|Lpwu1!*qtZ42v0-GhGMS2sx>B9vcf2gBd$BGb1MhV=4ouIfp5$2`dvnCsQgf zBex}w1v3{PH&?nKBfqtP6*HeOKVP~iqp+=r4YQE6uu!@zqqL)p1GAL8v{bqTqnN#< z9kaNcn0Trpqr9_%6SJJMyj;2}qq3`t3$v28vQoMxqq@6>8?&00x>~xvE|azgqn3`g zR=SY^6X+ziP!O%77w2v4W$4MOr=#a54Y^ZbsVS=oGZQxlHxox4x`P=x_&GV!2ZduD z)*!i4pHWv^O<PxOfE>&~Fl-|u9ZoJ;GH>3D851XTb~H9rRumRwW+Wy=Mg#`<_;`3Y zI#^m58fa=LDo9EQ3h?l-u`w_#S-xcXvZeDD&s)4`;ez=y=FXTqXZEa_QzuWDK5=?) zcSnC`e`|9?dt-ZTbwzz;eQ9w)d0}~Oc1C_?erj?;dSZHPbVPh)d}we$cwo4XzmLD4 zuZOpXx0k1@vxB>%yREf_y`{aWv4OdvxvsW`zNWsavVyvzx~#N>yrjISuz<LrIOx=A zK3+C%Hf}CX4tB^bTRzYMJVwx9nW!STN2_XTEXuBAY63cZMI1^iLunXa5z1$U(l9=| zsj(=iNu`P=j;;=!53>*59&vDo7-XWcC}{NM-|RVQO!4V+{`t)FmzI{cmX?-2E-fv6 zLt0w;-)d=TX+}Q~D^OZmdWRWPyy@SAAhCHMF*^{e62z(jvE-$trIVn#L8h!rH~nj7 zmR2>7NpFr>TAJCMG&8dTX=!O`0T2PgdLROX-9Q8gcg~$Vcdjvr1;Sw<0)*l6a2;SX z3e3{y&P_KnGXs^4Ync2P0+_`ZgcuA#C5nTZARjj?3oB?C64ctp7%3HE5RwGl+XO1l znB|znMfsT6l}*jS$Ag)A3Y%F-NLZK&a|`Lo$mobLi*ZRwu)DB{OK}<Vi1YD@^BRHr zq?ee28SI(W7};35w}>!+?~)RMbSju2<9^H@47?1I3{GGdnX@r7F|vY=;{+X;!o<qJ zl+M7)$if=Vz{<)J&knkv!k?dyfk9N5Pm*5}bOVKwkP-*4ICva`*$mV`g$)LRdio|H z0Z|dQInY5!$iQH#n4FxLh`c=0A=nsX3cQyNF6$TmS7eC)e;?6iWk+hW#%mcH>Kht? zNpCG9Lw!SIFv-C9f6adZhJgP!7=&PT4<vjcOQacLwGJq#!M!Eq;1vgr>Dn=w8;gQZ z9ugOIMGM{!n4t@HE5c6F<CL9HoN8dRl<B|6e*uQWV1KGIyamM@WF&!CQAQZ-V-8S? zl7?Irha||v2s->#I3U6SNsy7zOB$Ka#OM_XI`Ne!Ai_b6i4}BT3u7pVW@G^Ei)CO0 zpTWr&5aA%h02+s3VyFY1BncWoXJkZC1F{I03HUWfMn*aaFfgd9$jM5GgU&pYgpI#r zgqS!e&OpJa3_fayU3o29ylvu=l4N&b6O-aL=9T8+mgF_Y4Ah;%4o>3YP7cC6LQXO= z4kD=erVBOS{Qu9;%v8wm3Np_V4a(Qk{tGZ<{lCS)$Dj;~4F?H6_>qz<%uKwHVORm! zFf6+<JG=v?ZZ59u#%m|$W^5rZZ(-~vW~b(+<ius5<=~)Yz~!XG0NQ8v-{HRi!~Xw| z8H5?+8MGO^z;@U(F|so7fR6C=V&njw1_i!Vhn<myiHRi~bVVjpJTpkvpOHaJLq$nW zMna6Amz$H7nL(IQm=oGYWH*BJDMdh+r+`O!Ow7a~=ShR_-Czfk;A0QXf-R&aEkRv$ z3n>|MLmQiU@{-cB3K9|urXWIAT2fwCUESQs$U;K{)Pc{Gl9Q7{!k}<k$P~g5&&<!j z4r%#%ZvZ6`Iq=N^dEg`U^H~^~85x;FSs0m_K?B0fjDFIfq{7V93>tH3!IZ`}L@Ug$ zY_4o>%pR{~EL5#zEW~uC!nwkkf$?Ace<{YM|JN9JVdXD)fDJNO02-lShzCu1fO<cm zAyU+74sj(l*wl(K8ym}{lUi=dbLz8~YPuPpd=?*TW@m1j67m24zj`Kb#wKQVW_IrD z3=GVMkZ~MlLr9U!!1(V9Qz4@c(-LCFl^7LG6&ZCv2Zk~g8iVE&ul#Rj)cL=SnVtJ4 z1H)e-h}nOI;AYn|#WOZBJzx-rk3)ldc%VUO<SriQ=2XxqgE)h@vXYRp5^Nlr5j24W zK9v&E50Ya7jnXnU@x;1Yrl+J^+E}t1^Tc{sq@`ql*h~*xY=k6)6lI)k!4xR1GXAS$ zn#|b9>`&fY5;MwN5^5kb`@=#RRPO$p{9l!E+W)%@Yz%tf{GiMX+UN#8_K1ZMOD{tJ z*2`d31obf#|4n9UtF8T8l?du$fX5Aj8E!G%XJBVw+sqESLk_f_1=P27DpQ)+U8XdV zX^MlrkE4S<=p+CJ#{U^iAq-QP?lG{!`y))CE;S=)DiJg?7Yj<TzQS;8g~59;r}S4V zbuVO^?riT3s;}Vf0cc+dTzZ4|2Y|B!+UOo~eI-n&zGCttTz9+&g&c8p2Qx8s2QyOL zA<n|g$_x$-Fathw3$2!9Kv@|yQwFL%nBlbts73)-FrbnFrx6Z#b(2tgu!0&?ppZ0% zoJ41gUVAWBb4g3EyReGmt3D*rs}F|%|NmZP%K87F=@WxGqtCx}48EXo?7wRn_!tB> zGB9rN-C(S}k&$r&A1LcG{=3Ok%Baq?je!~4U8sTrpnG>16-5>Q-DL9nyNPL=Bj^lh zR;D6GJ*J(EY>Ym4QQCF?|NpzoRK&o>w3CsY(dXY*24q?0|ISS1411ZC8Mqll859|2 zKvIB4K!gLA5F;xiFC#Mplb1A@!vx_(dT#(p%QCPsFtdh(Z)1jB#Lmjf9M8eR&dkip z>@V%0&A`OS%E-i8#|}OVl9{EBft7)UHJkyoMU5GB-yf(2$pLObMmk7~i-E>zWTnLv z#T9u(c|-*TAd@+;nn@Tu?gF_ahaJ@91vkvtkwp7Nq@+ZIWMrOlOS2~kn9Gz3OG|?W zX_yX3NJ)u{OG!yM^T-7&bIFG&vw;R>K*KN$%>P4~${AL}{XYxI|00Ym4E&%AltKO% zVPs*3aG?H|<6vZA1XZofY@jJ#CI-fI4o1)%JuA4Dm3GiUcpMrMARVkM;h@zopeu5~ z>OrRsvatFS69Pt{15zPV`=HyEL_sYaQ{+*pS|J%3ArUF5D0XRXEg5qG9cTzJD}#Iw z3I#UhU^yP;5P2?VkncgE04_UYnBo}cGyP)_X3zxr-$9O#lZBaq33UD#69ZE-XfPTy zo+8X3EGEhV?JEi+H?mCan9Yq;ih8_l;<g5a?})P2O3#RqXZj~%WG2NWqAw$(FT%yA zrl~0at`q*NF(JxU7SMg#ptWk|;_T|46P3!kCn}XO1^d`LIQrOw=1;(7YaGKArWYt> zEA$9L$iyv1*$UdHGi717QguI5oVUF*1LJ?O{|XGR7#JAj88X3s<QCv(0?qcbFfw^T zTE?Iqql^ro^K(GQpl5)aA)r<OGiV&136!SgK=y!UP_ZkBbl?<H)>c*$VB?hncYu_Q z+0BfNz||(`uvc@)2)GioO6|7OQ&5)I*U*rXkdhY@)iAI!P%+ol6jRp~u+J3|l@XN| z6_aMu;j+}yHxri=*H8h)6?l!#3?|gDVDLby9zfIPNc9Y}vAH<AGPIsq3a)4VK4tRz z|DWk010QUC2NNTxSI5Y}lF9&TX@O={K^OKhf+rvN82FTglmyv$CBU9yG*yHT?<y;) z|9#4+%4VpcYRqcM>f@54%Cx{yPgzOd)h`GV*Evif3^UN`Ma=3LQ7<wZi?gdML*woq zIPU(<WRm=E!_37X$e;s`J4IGTP>+V6kA<1R9cdmI6g~bbim;lU(L`AZR8~Ms4RtfK zf8X?_g?VH}^eoJ@b(B<@<`}YDv#^@zX?V)<$wSsB9Qm)qVD$eQgF39uB+SUjBm_Dl zA9S-SBQp~t#tH~|6+QU$D`;p0yf_VX?5CM2$OY<VX2wRKBW*$FenM$+op32BDJf|g z32{*tE=2)jNl9^Wad8oeEArgj>?~|79Nb*ITx=X%(kz^E0s<UtEUcV7++3`zETGjl zptiFIQv$;lrdtex3})ak(Pm&~U}O$wVPpaY4(RA;W@g3&21aJa07eFWJ{~Sk4pz|V zv4U*Sw!W|usClPm3Yo$+Gd31oXB_0IW@_tTBI6Qa!ND|3R@Tgv-%?t}4%F7%$`r>K z#Pp9rozd%GFX$d$Mz0$T3@rb?GsQ7-F`F}RGl(-NGw3sff?WeT-;tS>kvW}{k%@(Y zDV&{=m79%=nT3Is1ti485YNHLz{KFMsv;@D%frZ^t*N4~sxL1qp)9E^EXX6yD-J3C zd7zF(zDxjd&c28kas{A>R2eWD+1VQ!I@lXYD=0}yDJmQgk(3k>m6T*$EGj7}Dk3Gt z6lZN@Xl-q1WNoM*Bdw??Eu+99DJ3HzB_$;ZqrrVG8KxM9C(LdP5)8@=dNAMVNb@tZ zu&{y)NOne67ADqoE;i5tG6t4#FoT659@GnC0r^l$66`-cRXurGNo6TzVL@JQc2))n zMhR}H??AICkjO9r4UmX~5)7!YXHzvY2M^kdiioj-=0w$$mDt6^iZ#r{M9o!<9PLe1 z&BR4aHI3|LjsN}8(-6^+mRC_?WRg==RnXQF)@F*)k(buBH#Kn9RFu(mFp)P^G<H=| z)R2|dVA7D4Rd-Q@lqpk~LKt$GJs1RG<$(Y%4=W2J_|8Aj=m^F@lpuqk1Z*HmSkw%B zuMj)9J_6mYDJiHUreq`_AYh~<p~KJYF@;NlU0K{jTG~WhnO%Zw3OHPgn93OXnQLI_ z2ApcZt^w7gER3Mr<3Nd-30C^5fPfOLWCcws!s<hIaG*gh%QHt}>wzO$L{u^X9QC3i zk_TZT%pTB~ZUxbjQc{vo8dPR6{WtnAz;GJ8)<m3vW2>kjCo7YIHaqAJX?Atc?a=0| zNNjmiQ)Lr#MRQYS6H`Vm6H{eVGet8~WfQYRQ&Z3t3g)KDrlyKWY>*v{{{;ShXZ-Mg z4}&9vGuZDIEKE!cTpUbnj0|k)%#4hztc>BHyLwsUdAK=22Pm_Gx@e9Jj&?Q{W-2O5 z!l3X_;pLTu6v}qY#zu0?pd*M8v>0?D9v`!^k{+|N5{zbJWB(_>#3yg2t)L|@B`Yc+ zEi9p_qM|7wA|)XzD<!X^U}7!JV#=<eCN3u_r=zT<BPlE)Dk>o?rK75>BPS&%F0Cxg z{O`Mzv%RLQmX<6swu#cW3PBbCv9+B*w;eP7KlNXV!R-HY22R+T1<--%;4QZdkQG?* z;1V2B8GyzNL92tr#LY}BT!lrRnP}(5OGz>@F#Rw2FU8OeHcJ>*D+qHkfvN>i$B%(A z6cl(2@!$@Gzp$t<yo3f784xoOrYjg*x(bV|HZgY<7KYhj3A2mw|E&K44517f49pCY z;JD%gIkTC8k+B4^=)>HYSy|4!#a!dx3vfNX<i9vr4=>EGpk)Hh=!+jjRN-s*%#Fb( z;6oaC#&YHn>@p&v(o(`2dw7JUWTa$dq(Nr@{x@byW;ny_49+)*nJ>^_Cb)MM3(BvI zz9M2G!blVF=EiKIBA|g-Rq*}drY4}Fi?fnaMg|5(Qj&}_VJxQ0@>b#o4t7T3R&oj! z;s*BiM&jW63RwQ_W=dvsf!Yfy=k>uMp~B9>%na(@hJqOkjIp4>1x5z23&cRK;NX>k zq)-#k`fW^Cu;LK^w;S%LP8jR&dl-v=ljX5^<NyEv#{Y#F&iwzx%+CFnf#J`3(0B>c zjz8-`oi6ZtgPBYj41LUQjP8uJ{~MTcWX?*?f~aB2fvZ`@l)+HLJcrSp!4;>PMNB*l zEzI*6-5Klt*WfT`5mN?30mv>E&Hvpv)Er^TVaNuV!{GUUFG3CD4kY(%Wy)cQ2f2rH z_y4Up)YLL*FjO(SF{CpD{cnP(VRYyI{eLx62iT87pnLGZW^QC++`$Cd4)Fi~|7A=( z3^gD(G6duF>tQAyhCq-TSpxoFfS3&mGZZ%#GHEd6GdnS)GerEaho}MDKObQ~R6S@r z0>b_{rW}TC%+8GN3{n3hA^I@PUdWWekitBH(Vb!0{~m-I7UVFR&6L4V1y-{I=6{em zC~6inWiaG})$D`05u}D0*_`Q&7Z@^`3Ygis=Q1!bEda?g?qFH~OB??lGoE0SV>-pa z&TjH=52%=DVEnh?zdED;|1%7N3>x4ZBMUB(S{Oj%nxGXkpq&)VjQ&c(D)17DRT)%T zgKmidHw45O{he4%e4}(+%l+6KxmM3kniP6dPE~}1M?*U_K*0lK5!3(J|6&Y_|9@u? zXV7Oz1ltq_Iwet<k(q&wk(F7Vk&RWGk)4IrON)_}(Nl<#(L;`r9o%>ZcgC3*m{~zr z=diJ`vxS3t>g=p6pnIG_7q+pn`-3YR6%{2|Zid`=$8HL4u0Xm2qM-3&T;e>EO8g=k zf=Wih8VV9Bd?Ffx%7!B93J_66V-(RqZgz1&D-{l2E)Fq4D`k!{E)EF+5TBb}T)<L= z9kQlwE>i|W1t{Gyp8DU6h<7IB_*%!5!%)HO#OTg=`hN#P4N9DzXUbtn0;@Ume<eZ< zG(AG>n!u#Q&<s{{_J2D<4T@cfOgRi2!D`O`PeiCeF=sV6t%Ayz3;*ZfP_vUMhoJ^+ z&gK7;aj5BL%4A3btGR;gFBH2vm~t4p!D_DlPr_l&LZ%#sVz8QP|EqDRf#e;Ko3A71 z9TdMWVaj1B0Go5;e;p2U;AIKpO`K)PcBTx5bWnN2c<cW>9OkTG%3;U>+jaZ@d>m?K zGvzQ;g4Nu^8HS6Plo%#})!aueqfx>T6h3)iH4l*8jG`uuDTiS#Sk1%#4mjMC!lcA- z4y@+Ue>)s%;Ni@aj2zC0w93Hv-+?KEVI{LOg9Jk>D2ID*&;^ati88V>i7_&;g2(9~ z9A;J~uSkfrGy`ayij|Qmm4TIkffY1y3)!W`%HS{Upv1t;%D~FZP=}<78K#Pjk%5)L zKhi;%kwI9H4{|9QxZlFgE&{I8p}jI=QP4_tWhKbGkhv%uTabaBgW(T<^=5ZvB@sCu z9$BUgQ%fsjxg9(F)wDQR<v~j!{{R0EDZ4>w>m+j7jh41D7>dAY;vCL;auHJwLlLN+ z;0(Z-{?;>PFyw&i$-w^$5OIKFS3Oe>Ljzci7jk|;QB%T{!LSzWp6CDlaF|oXl)<os z*`3jyGyJ~~4mI6OISd`3G{F@9zY~X=TBaO^X<#+c|EqAQDP+oFI0jY|``;V8nt$d0 znHU)U?_+dlZ~gz9i4(f>kAZOq6DO?h`v3bs6JyK&eT@9<tqcrIobdf^U=^TwtpD;1 z4FBgdv$MA|F#I_Hsr&vMfazyo_^-g&_<sQ-KS-YOKTJO(c*XMn{|sFJeHo4aPXUcV z|9|x7JyidDnEro{|NAoVK;-}J#v*_7zc-`${~l&`?#B!a=zDz_82?){MKc^@wqQ_Y zFb3rf2Mu=6op7KLzI4z^cV>ogR`8lyHqdqG{wjhZDk_2^@cK*`KFMhc>sEp;_5$r= z5i=H50uAOE8;MCO%E-toNEzDM8XMUt%E&0lOB>qS8X4L#otIZpl$KI7v^F%bwKSAd zQIe8UGO#w%x3vO|)l6n$VJKlXXOLsC1iMX-n~@PTSIfxcB_qwm<PO~=ln!3?2^xF@ zEu&>*0@b=ouvWY<V(Jx?!=T*+uwy}ET=vRR(sFVlqTFo4oVu2vu@7?#89An{+G<LY zGIH#OEX-Dh1~!iNc4_<ud>;S*|C|3`=6~$}>x}H&*Z%$ca{?M4Cm`{`!1OPhA)9du zqc;it3{_k$`D|FLommscx-E~rpTWd1gQ=ZyE7M!By|NBcpw<#2LpXTZK|H8u#|Q}( zP6=&cq`p3Kzki052xuxmMCyQ)C};#+RElYrv;?S=FDV7385kJ?nVK1PFnwm=W{`J~ zK^j8@SqPf(g^WKjbBbsqPd&84mZ~&M%gRcFFsLh7!c@u_$1KXg4LYx#3q0=#5^e@v z2mu;6=VstmVdfOoW&}?yfd<YQr6nC~gbI0e^n{p7g)J>aI7H0Ng}E6R8E!EZF)n5n zV&G<wbC6~S%@cs`Zh_vG0yRt&Y#3zqj~$aa<6>SzO9?AJ6(w$FAuiB1LmnYhbI`$+ z3``93nM#=ynMJ{S9St4ycpxXygM10wXcY_D^2q3mzQ>UhbaTC#nYl5@CS%B?3m+5v zd{ImBbRjcK(FPDh(9BYdsgzqXK!-!x!dM8-00lrDQxW4uW+4WC&{=rg?95o^6!;nV zMMPLQ#k3j0`zO`Z+1Qm$L4Jx6a!|IiHnA|b7Shq;V-}JRP~+COven@h26>H<0cN%U z$ZS4N&}Bi4Ud-S-N}3rM7(gK<z#t$Z3N{<$0r2n?cwWz(@uHxv7H@{IgR-@(g`}Ak zDAdd>#1sP5xWr9mY;-_wZ)YlCJkIo&ffsbY0_d1Fl>JQcpx!)aK!%rrS4l{Tl~Y_B zJX;Q56To<!J;cp3kkx|COh?n4o$0TWx0kc7th_cz<5H$b#vbUHsgeVD*CDo@bt)<< zN-Ufb+Kiy(7|<e!9Z?LJG4?nbnc6#;7&@6NOG+szN=qp*oiwqrF*dfbF_u<Pkd{$U z0F6a3<S~^n-e>y3AkLuUpb6@rGcdA%uDoJlU;u9{1rL+3fOq`B*8p%zLVN_BtN`^y zz!MqfjQ82?bVSuvl!S!k*-TjN^hDKFlm&(5*qMGfIf+V%Dk?iVi%N+qf!C@qGPE*P zF>Ynrk8>`QQ%qY}QB<*&(cs?&rv0v9_X_@3Vc5n1x~E>nL6Hrdp9BS1nW57h;UG2f zplJ?&RYfLFF>P_sP9#L3Xa>5GGg4PTf`d;+#MDSuTf{&~sYI8R(Sn85NJqw8hmT*u z6x`qc%k+csJF^uN8$%(O@4*zuu$>9KC;C4q<Us3w4lrpkSTLPpWM@+U&-DKiq@4VJ z2~tk}|NqaEsr-Kfvl|0DW60nApsv{e|Nn3PS7Au~e~5vd?d`vZATb8Uf2K@U|K~HE zVi0180@VSKb-A+O@t`~g(7EyX;HekTd=_&YXfmAHPufA6feEx_kAaz?1w$6H(nW|t zP)S7zba54EQ6MA@g03Yq21OWXO!VI*W>IlTQBkQo%wppDVp2>QViHo4QlerKM$)1T zjQ=c{lK(Gey2~I0pGN>qZh<RHCPvV5KhS1>W=2L9Mt?>I0Y1=RBr6Mp5Tg(qtPu#l zksh2Z*_6T8_?ewEwXv5Mm2nNX;9xTj@?^SeYAT>2CL?1nBMXYVe=C?`{wpwZG4L`# z_oni2gVstgFq9}UvO{)mGlB|t$YhDQsM^03N-_o#5>^#Vr)8AY1$jAnWrS2AX01dw ziy6G`f`Oq#3uYFx2)n77sfn5zXf><YgK{eg2?H5Lrqe;PszNfnoV<eS%AiOB?UVSy zu!Xq@X-)++-vbT{$kruBMy7bs?TSqPj0}9By%3<i&w`ACNMRvnY%Hn_UQ{otY-(cW zrsf%B%*J66;UZ(=V9Q)2D{ChsZOLy6TL1a)50e8U3)3P7UIysCROx^S2MH!dwB!J~ z_>7Smv@aDTg`5<iGLWPIDvFF0VM&3R4ZMn0(MVK8SxJ2>qnd^W<91CAbw;K<J$++i zeZBwx|8HT+V8{ZuW#9atjc7Zf^l#=f<uH_h+Lw$M|5xHrbDAlK!4oug!uTF}3<%M8 z`Tzg_bnsXbs4sr!|8yMYlrrTo^nvYq{680=2BrPo$dtn{8LZ~X|7e676uVY2<uH_k z)ja**f>48EP6AU7!v;{Ffhq8R1VRnM?+lFp!<Z&9tYl_o;DxOzB3KWwLsq^DgJ*L< zgRGzxQ!7~l!@~SnoLKZt42@WrS&bcR&6Jf@Km{b@|1PF%hEisA24My(P`Tis4;mO^ z^kQXUVq#`s0_`7XWMF2@0P&eXcS|rafHw9CGYBgSD+{u*L*`^bE8M_Vf_LnK_jZ_< zn%D_*%8RRN*z@xWEAk35tM74RVb|2x)DRWqWVd5rVE%8$P|a|X=@)|_g9L*-Y`#-U zQbI_8i=7d?XcRP>jD7Y8noosA!IRqTqROxf<VC?}afq9Xf~P^{gTuo@?1Cc0LJch} z4DAff&6$36vd8)O#IaAA!WIvrJNL3WnwvYb{Qk}AWNz;G|Ns9^rVNIKpmfO;`ac;* z+@vvCGt6XK#K6hm=Dk5SAi_Z$ykRyU+=d2~qD--%o9~!>r5(gTb#4oWM5F^Z1NcTL zB_$R%F>Pj00GgT@8;jboh>7Z3{$fg(5HU8fW?=lE&XmqDi|HJ)U7(drd0-nsi?YDC z*fB7H_8WrgrDhBXm|Yx-f?&H$jUjWw$}_LBY3fTTF{NAU7)i?f|Nr;azw1mY%q|T2 z4A%eB{<{i&0<jpL!&rZBGOUM){a^F%I)ueo0TcU|#>5X1V|Wf?G4eB5|6jvYDD;WJ zZ6gEYj{mNZg+@>Y6XOQZ;P*ym#vT8zgJf8s0<4TXm_S=!H?qMP?2J2@3c;1$|NsAd z{@rKX!mPuvoWYJ!>|Z|v1Gq=Yz}ODrGuJRM9AG#ADp7?%cK+)Jnas$zgSiH*pMmS| z<bN$pIm{glpj|h@4B`yZ4Dt-h4C)Np4EhYS!MR`p3n<ltTEC!S3nn&Jrc@3_Hc3ti zW_B(%_Ea85E@@sVW^O(%?o<IrK3PE-W_}?){!}p$CLv*Ap->PlBpfR)$|Ni#?5m(4 zBO@UpA|fEb!=tYUx|CN_LtRZ(ML}6XSxHewUPfL{Rzg}rT1rwxTtr+<R6tljSV$1G zQ9=m3FJC~8T^c;zAq*Yq0BuKLG*uK;G*uL3MW>Agl|dI_2pbD3i?+5Jw6+@jtz)!i zN@leFt&U3nGcqvv*Kc63!@z*)pFwA*LDxTZ0|Nttop7S=-%&<u2nMx9KywD!pnSoi zk3438D6{_m|9_4tham-2_OayrUx6*}{Qv*Imnny#9jxZ(|1=zGAY&>{jP5LsILF5} zfyYZgYJ72y-@wOs7+i3wiDk-Q*aq?!!;1eN2=}0r>E28k497rYDje?r&2Xs6W6EII z16JdSJYRrP?r&$}fs8RStVAATL{kGDhh%U=R)Z4Gpt+(v&{z?JJM!2diaAS|pkr4I z9yrIYAme`X7~L5*A&>hZoAd7(Qyill6Zrh5{S2U!eg5qOw^wH}@i63p!_5n)8<sNV zFcgB_>5V+Ef#R3XOgRjS%##`28GMlIZ)7+82f4G7xsTDEBM0Yr`wpfYhB~lae*Y&S z;t0jAZ%jE1a$q(7$abNqc@9p)p!qY0{|9lH1DYG!0rq>qe+wLHAbxjabY}>}>35L7 zs=(&J=f*&7d=$T*{P&6>?Eeu)cZSgai~qfnfzAsu?)Zl|Kj1P`4nqRioY4PkakytK zc%0ms(VZg-=h*dDrVNHeusLBk)7}B542B@)5=M81@c%n;*aex-a$<Bx%(;R5g%Zv) zm~t3uLFE8L<o_8s%z=-kGeqMYO9zz$;5ihASmd!)WV;v`808t;m`*dgGgyMynzPi> zAnsw5hg1s;j0~IqD>EAZKg__*V7CoA^a1H7@qk8BK&>B!cF@scLYU2xNC(h8LEH@7 zqM&sttl$nmcpSl)RYFn7fL(}>$(B>d+)|WB(9}}&|9^(0|KFGnF<oKgXU}5#&vXhh zUdgnB=@i&Cj10#A6&SxUU14x!$Z?R+1+8MzVPs^HV`O5KXJlq#@e*ZZWb}j_h$hd# zzyMmu$=c-&JM*j?bTk@h2_17gvk!R70s}J>b21i%j6RVLoVq%yYBtKC#Vw4G88pyR zF>yI2(79(yYU*NQ&})=ML2VQ@HFXFw1y8(zIyUN{ThT#1J~76Za{57<cE0NF1{UGQ z^=bSvyu8A!VkRnr%7Xj`VrCASf_zE}T%ugs=~ixGT0Ta`!G<;&&ZY@WD?NlotV}ia zm4wBNy{%%>SQ#0)rFkt?Ik>$!IF#))lyn7HI9S7kCG9NLjZ}n141KI^LUln`GBPOr zS71_PI>jK(Q0ySY23niP$jT(Z$jmCp$imFx1qmf(1||kp1}0X}MlwdwRWVK8Y>cd| z&E5>m%q;N?EG+FTKGF^fa8=9<EX*vR5JOhX>J#a}$H2fK%^;<!A|xmT+Nxk|3fdSU z3K~EH2MBZ|Kpix4!PxSzoAK$tN_#hXWp8B(LlI#c8UJ{B5hYDITPAxy+rN8R?96So zIk>zyIQ=Y?G(Zbf85ux>G!{(C42ldU4u&#pOiYZPphdgj`0Vv&0qxC?Wo2Y!Y-aRf zWRQk5m_eH<6&V#l8{-+l!^YyEqn$u~PH?<{mUDr3=7U?sj79-r4%T<tOzkam{rz$+ z?M>V6SUH6=K1vCWDHnHiRH}@2&a$`9a*nQ4a�gj|olzjY)xy*7(9C$-oV|j}f$g zla+;;3EWO+U}S6s-A~xe;3Ep&VqmOnYA&b_+QTU*{^f|Z^$}}p10KeOJO&0l|KfQV zm>Gf@7??gVNiuLT@G*!nm^m0TFfy>QGP0$DmXWl2GqAEU#er6HGch$Y`G7_S#Y6=8 zx%s&HczGDO7&sLLIeEpj6$On&A*(Bu6$On2&5eyfqvpztP7#wbV$$;dnMb^Dsj#(b zWs<xZ=;<HMB>DHbOT3{5s68J1{~I$ilM;grgEFHyWG^)rXmFC3k(r4Jv@cBwG`9s2 z0i8W4?EsQy;bjDIA|oL>lsOsMLFXH>GlH(X=U|9u<6>f9=!IlH&{;O!%s$X9(CWC9 zf|DQ<6F71q%0V`3Ba}0+v9)@0Gjeb+#B(u%O-I+_V93D9z{tYMn99J;z{14Nl1fky zXj6i`oGc@QqJpfloU)Xpgt(Zfh_E0(7Y7?FgAAh#XyqfLk(ijMD0I0kB=4&$iz<Tx z-ONl}iH%)RO^r#-CB^jLM5f39V}C8p0Q0;)w!Vmni9B`j_MYsFf{b=dN;bKkrqS`j zLcT(RjkRBxB=6@|djxv8go4@-40`{+F>5e^R`j?rzGad;z`)4P;K8U4>KTDr?@W^5 z@{oza|Nl4U6->$uvJ9FG<&e<lVq|1z;ALcC2hTh*GO{y6IMB$EVqj-yh~;2qW@cb$ z^=4xP4J3ie@oq*RX$LtDRwicVR&S7+cm`0Yq9}-T5Mp4^P*YN1kY$in7FJPJ;^dXo zW)uajHvnZNWhFIra9L<7Xe<bsyAedp;V7{&3EL(4=2<mVxaP=<C;6o(M_9T$2S$4b z26+1f`1mtPY8t!NhSblnw=i)|Oks@sx5d*nGLcc?-<P<e;@G&tY|t7-28RCvOhHVi zp#2nA2Pb|`W@ZLPE=E>1CNB<91przI%gVr($^tr_G?syhNez^9m}A))nVGeieHa*o zg#-ooc)>?}2?=tri))LTDl!U#Iu4ACqKu5n%*=}Fii~UjUH$!UGLzZA^NhCt<}-?U z|Cr49#PVP0hh>a@jQ!^3|K$G7vu8ZXxa(i}KP%Ar2h0qK|K~ADFzYiYF=#WGFwAt| z=jCGJWE5uP=8$G&;9~aj-T>OMsSg^hVr0tW0iBh>z?#p%!OO_a#lfA*z{$x7nonVD z@@C-TVu%HmZ_S{(RTrd#sTr(+wS|!SNC!C`9R>y+6CGoHU2RQOWd<b%B}F+|86ibM zK|wxVNo{dcWkJxMZDS*GQ)Ad*0i<RVRW?;pRu*M8hAeGW78HcC_Qu3Ga45+**gM)< zY1pg!yE(9Pm^;M;*?I^0Wdu99L?k6Kok~q*&L|S_ceM}ZVdYSBGxtjt%uY&T{9|op zZT#=jzrS8K7OvpC8J{vAV7$g42pY!+ot^^O-UmJ;D3%$t2-z1juF1~IAjl}l!YiU} z4x8;06=4$<5i?d4y%Qa&Eh(nNDyqTs<Y}n0u8yoSj~J)8q93Rn)A)aqQ3-lRCC64a z7A6KJ18re-b8*m~hG6Z@?&q~PyEDF%d!zN|x7-^o(D)~V#{YSYMhwdt_!+bvG(cw# zGBYxzf+hi(LqS7-v7pW`gD)ckANUjx27X3<RyNp~sNms6Q4wQf(Jd|>9xiot@$q#@ zE{=}QT2awj3``6f|J9*p3p3a_SRzbkV`N|f9Yn~&%E}VTz{0{B3tG9%>MJ0?$N=f_ zBCO-UVjZZXi?H%P10w_6+Hx%|a9N_kxSY|5X#+Dm&veinGNgTnm}6$pU^IZr&w$A@ z!RMesaRt%Oz|WxJpvu4mayw%xcyJsv$O68{n}HcL{Ke0}r>LaJ$|kNYYznG#z~j!~ zazO(W9(67rp3aQRK_LPP8PK{(nBCx61xp7rgz0RIkQ4bZ!-WAFE({RsIIvj9ZmK8> zv(m$*j*$UjD=1L@|A)mjBR|h{#wZ5Y0}PNo_n?6oNSHz5i<zBg4g<sA@1QVa+VS^0 zxYPxuFFytb#t^0r49pDt40;aQpxTbH1(a@?7{WnSHB&q*Bj_kjRu%>ZZcY|{R(=N1 z;aqHNqM+JY5V}MIJWtFh<a7x%e<><2&$Quh0F%8md=8Tdd^a4-9zzE`1_ow^W{71B z%uGz+10TUwF*Es-ZWZGOqz(PFwo8G^cBvy32xvh#0m&)`RDbC@Xo15JX`vr06AS3x z8(iUsJf)9j58N75J4j3q#)8Iz$dlQAP7^?<B8th$fs=y+=vYLMP2l{3obE_Z5BRNO z+<;^iTDVc&E(L1ag<L_P2H^y_RjBbqT3P^woiS3HL$w2L#sB}XJj=+>vYK%b(*{T% zmT?Eu22kmNBEJe#9(u<OD9^Kl?)n3DFq&BynVFc8hsr?{kf0J5ddw=DxHi~%qRN7P zPDhR~ZTNS?>EABU{!?grVdQ67f!8ip1{DWIuw57f@}S-^1FIl-gdAdvpVJYh4Sy#& zf!1e$PTVUmUw0s$-{va=J2PmNJ;P<bD)5=vSD!oY{X6+zHP;6gZBUa8T)Klspcxq$ zSakp0W?<l!_&fRU6s`|olRz?1%(H+g0(4j)1L&+}2<Bz*V4T6=$kfIl$z;P|$867F z$aIInl;I(RBcl$32BQvx9FsbOCX)?=5hD+SCZiLB1|tuH2V)5X7n2QxA(IV5B$Ew; z0h0}bE|U#|D3c9?J(CTCIg<^84pbhb&x4VNK^|lVlMRCfh-Tcxz{A|kV8v9=z`)eS zkjPZQV9RuiA(2UkA(4rlA(80@gDuljhD63-1`S5R|C^Yy84{Uz7!sMg84{U(FeEbb zGT1W3Lg_q)L?%@RV<sI2E~eWIQA`mG49trdc$mBxSeVQh7?^Ar)EGS(0zhtN^aT4; zjd9Qae~gk08cc=^dQ9mIp^Oh0%$d>|%$ZCW%o!sX_`&QfhEPT(20uoo|0kKm!0b@w z<qV-rSqz~}v0(L~Aa#s$8A2KFGZ-^wG6*nbG59n7VPF8QX<-s%5Muntz{q%wL7lON zL54}4;XPvwg96z9EDQ{cybK;pHVn~BHefMpCL0DmkQydN25Tl4hN;XB42F!U3<`{Y z88pCpoES8~VqgCMWn2ReV<W~m1_{PE20^f2@)<&z`WehY@x$<tfsZkcL6xxrtY4k! zC4&+(GlLj2F9Sd0CI$&`xP!tT4Ko-1{|}A}P&^=E#$X0@a9jj4Ff#@-$THnxkb}ho zC@zpOlMRCdh!2hnP<+5JC@x|d5}C>wLc!q<3VSrnoWP(5jtdh=T!7*Ogc+Is|6^og zaAjore~Xdn{}*uh!{PxH7s%L#!3Zkv4~h>M2E_$9uKzJGg8dJ&8{~gvx`M$LY(Fj< zltw}61myor26J$F1-TUzR`_U8dIsskML%UoWXk$~o9QV715*|QCu8vcPoQ+i7|bBb z$n^gWV=#j-Bh&v^;5cAnkOt9ADGVA+EDV02`~gx6&Ld(Bp`f(N#KRB(3NKKeVPs-( zgwmm4nuh^I^D;91{|<^naC-g$vY#Oo&fmbm2xEieGM2#{gI>U3%e;xfj46*H5tIj@ zv>lXo2jxjnzQ9TAFmQqM3_1-;FIf!!;PgWT4a)1_{03fTz`zL4^Vb;E!FeBvW^`i+ zWwK$2Wolqx0Hr}tdIzOBnBQP-Wn9Fd0mhXK8jS4>8VuXPWuXScR|XBn{R|q6+Zi;N z92x#HIWlyC(h?}oF*W}G&usPoKjVe}|Cz1-|7W`R|39<w{~wG||6haDfXg`^hD0V; z1`%+&v0+dHm2n`u!SM+y;}RGcU}YSWHiI%qj_Ec-BB-omvSLsHmvLqc49qGF#-Q?z zS%txrnU6sYoF_r~E1kid@iBuoC{7up7=oBm8T>(I6gbbZF|ablFqnbl8J{qigVZnu zGnj+bfzplK|6fd?GDMkyf$=Vb88{D3X5e9pW$*{9&tx!XywAV|HuEZjIg|STuS`D~ z{F(U}EEuEye+7$!@?k1O4D83J3|ycz$JESV3l3XQc^=Fl3re%hD;OA=_c0_gfz)I% zBr+8+aD)8{%9H5~iSY2+%)kaRi#d#ek;#WajhTTV351zIdX6wKg2U@6v@U>!6{tP| z)dvL(+_10$g*DSIh5!~`hC~)YhD4?<3>?f+42eul42evY400?o42ewj3}T>k!4%G5 z$>hKg%D9msl*yGr7*qx^y<=cxe8r&1)XWeH#)1r?Oim2!Os^P1nQEbRLIDG)P5{>@ zjCUABz-7)=1~X7yz*NBy%9P6B#oWXY%G|{e%Cv()h?$EalqrZIl<^&dEVBzkD3d;e z0N5^28L*Kdl<_?{%#ES-&=*iXXZB)XXD$QRL!3;44545<iWowfL>Lkow=jgl@&h^z z$`>HDj9(Zum|s9J<Lv+c7}hiJfblN|c@WK*&cFl43mD{?7#JLx7#M6AD;Rv4#Th(U zW-=HuykJmgoWY>ZXvUzyXvUz<_?f|jq3ORP<0NpM1ZoE;GIKEaFxfDOGn+CvGI21N zG5=wRXJ%y(1f^A`Yz8A{I|c=2I|e~!I|gNDI|c(LM}}0!FASiWz5|S37<MwoF>o=) zF~~8-F~~5+F-S6gVPF8Yg5Wf$nf8l;fnhngerI6hVPIf5!N34Mp&Z)UVSd4&3BsT- z1Nn<7pMj0ZhJk@Of`J>9X25khD6JVX=rQ^-STGAP*fM1>m^1S*L@_M_EfD<I4Ni9q z3`yWPX=Vszp3Wf2EXp9nw3vaDsfvM<c@{$=(=&!prqy6~urM!VNMr_;uQwUEnHm__ zm~Sv7G6yn*GI=w|F}`Bp0p(X{o&mLA%$ceg7?@ZX7+^HZ5e8cpcLsB2QwCe6$qeQ! zOboWn9Sl}*dz%?-7~e3MGbJ(@Gv8$pVcx?a!VF5wlfmguoGFNblPQ%U5me7H2{DK> zZerkI0=1b$8A6#m7#NvlAbAL!j^{BXGU<ZTQ6eZEF=a9EGKn#8GHqpGXUb;~V*1RG z$TX25lv$r4k?9dbD2oC^B6B=LC^I*MEmISNIjEfgt}AsII2ms<h=9}A9|i_EUCzM5 zq`@G>9LK=PbcI2LX+J{%Q!axSC=W8lf$2n08N?jOAk8$BK@3KN%0h5nf5*Vcw1Xj$ z$$%l0={y4?lQ#o9xO~WE2xa=hkjONRArx#DC{ImgNM!oV5DL-@E^CqMbXXY=ic4@j zK4!3HGGQ=gg3@rid>GWg;o`xd%=n3c4_wB8;v5wJAR5x<G-u`mw>{xBsO$jcUr_l4 z%3m1tE`|V5zGo6*NCcH9pfU%X?_u#%!N9_##vsJx#lQ?MC(;@0KxH;_8-pElGlLm( zCxbGxCW9JNC4(}PB!e3BEd~bWXa;uX?+l5|Q^9pMBtA|va57C`5MlCVFa?$IOeYu^ znY<X-(Zd6cFNDU|BbiUA?W4n>#$>`E4-bDd^`JOoWMZ&^(thAN4_kW!68@Sv<ze}k z0FB<RfaP~WG&o&@+Ky%n4B&W%(U}b93=IDq7#RLufzbc#7#RLJBVjv+vkYPk*BE#} z^*6Z9lg_}&bc=zD2~^LeGlVi#Gej|^Fa$AGFvx@R)prJSP#c?B3#`T%lpa8JH<J#7 z4LIHUFqkp1|Njc^Cs{EtFfU^WWsG5nVhm<bg0%w~nHcz)<o^F+VPlA5`tkoOIIr<B z*s>Ti*fRSvm@};fn;i;juQ91I=ri7D&;_+I7*qfM0K3rv(LQ75V^9T$8>o#T$G`}0 zS9vffGkGwWGk#)FV*JD)!(_!^zy!)auNV{=&oihnc{7AE1~4=-X)`o3u`oD-`aG;W z42dio7!p}*8O)hw84{WK7!sLS7(!V%84{U)F(fkQFoeR=AY(8CBO}xQ8?ZPByMc%{ zAxuAvhL-{8?Qc+<OAKK?+#Zl0J~8kiyA>SP=?vz~s~8fQK;;dr%?zzWV0JO-Fa(0j z8E86Z4E}$Lk?H?J#$X0MMyCHyVRpc1raXpl#<>gvpm<=M%MgO$M|%b}#t#f~;Py+_ z|C=D1IiA6sX+DD$QwW0*JT74FfZ7W)9~TWv&y2MU>`X5iLK#~a_?XWyn1e9bFR(rk z4+9rC-GchKpuX$@1}4TW3_MI-4E#*D8Mv4d7#LuA8`Q1?)wfLl8EnC6HjjaeaV`TF z%q}KFhCr}5D9uCq-HJ@Y3<gj!Q2*N)R5pXluLuTnuvzI0W?*?c26Ir~n(@#7zf4&S zfuQsZ!i-D|T%a~AqXUBqV-*7{W8D95pd$jnb=Pu+KxPOIWy<>h7^EKD4g-}h2@Iez z+=A&UgBjyo27V+Q3hK{*&5U9YV$uh<%OHJxrvHx_AsEy@o5YyOAO_AWSqxlEu?%ud zsSJT2H-h>jjQ<!I7;iB!fcpPTat!LAI)(Am|2K@Q7<d?0F-U>wtI&Rx14AfN1cNS< z9fLWjEeT35pt_iGGq_$i2diDhAjN!u!JPRjgE<QagE`Z81_8#u3<At?V9XD$52G1E z8UHdcF!?crG8O-S1Hzzo83;41_<x6y2RuH&#mK|J#hA@t&iEg6{}}@#sC@)#^MPnE zUdbTGu#!QJQIUazQISD}QI<gxiiH_v8F(2MFld0r96%V%2KAF9p<+%99*j;59t?XK zG(eb<k3kNO6&U0gWf(LVy%-o6Rxp?|nlmUeMlkR&iZTc?E@JS*gEbi6GiZR^$T*Kd z1B|^HG#Eh`WG|?H2Ew4e84rU8!#4&EWX#0JpaI5W3>pj!|GF9OGKhdMlOw|q#s>_F zpmq$H28~nsF&Hr&{Qm;fRs^LDrmp|LnSMj+e5O<eTX0`Uj3JTnG(#BU`~Sb0bQlbn z0{?$w;{3lAtTvXx9Bdxb|C6A$0=NzTjzJdOhXbYi3I<zH`;+M!xX-`>EAv4#IIn`r zm_-acpftq9&%niWl!1defx(!`fq{#Oi@};HgdvP6gh7SzB10Icy}~HW5XKnIV8q10 z5XQKZL6qqRLl{#QLl{#PgEbQugECVbgAh|3gCtWN12gj`1`{TA21~G96@xY7H-<1K zeg+8;X1vHC$`k_5S8B|{3_?s<3>u9085o#OFmQp=Fxaj$3_6TI83Y)2GK4XHV&DR~ z1#Ct)gDI0U10Umj1_mbE|Nj}6GuSgOXW(VL&0x=%3C8M-HyJb-A2HZ7++mPn*!ce} z<0J-q#!v=(CO-yy#&=Nshe3mJ3xgVCCxboX6tF%v#@h^5OtB0cjJXVQjJXV^j1w4C zn2Z<{87DB9gV>Du3@VKI42q0a3^I&%45A=0hRqBXjL{6{ObHC;ptR4}$)Lh$!63>w zhd~XDCoq^Zyk_8HoWx+vIEg`$DS^QhlqMNH7`PZ`F_<$3GB7iCGAJ`%XHa2G`~Q<+ z1%oj-?|{^MF)$#hF@~vOUIZ%J{@(|cRg4k;e=;xs|B)%{|3{|u|92SY{=dr@_5Uv8 z{r|rhGylJ4yvCpeYNIpGV=!m(V$fo~%pd?RzoHmenbN`G3Mx-P?E_GM5<Di!%)?*~ z8rxxHV$cBhT|nbspfYqWIG&6dSNwkrDtka}NKkx%`>~)gClCgWYrbXB2E`#$Cj+EE z2}<`&;4;x4Bn}Qw3kDU&NCtUuc-b?EGCW}50+$;T7*rT1F_<!}U|?f-$Y9EFfk7OM zSNuQ3u$;l1VFiN}qckME!C^V+|66dl&t@<On{}N*8I;!<R{Vbp4R<!iP6iRiW(HHn zK4{n*F-S6kZ~}uQqYDEkV>5#acnkoPXFy}b$TX~OLX5|O`aGaE6LuO}C&KC}oHRQ_ zB6$24)=mJA%P|Ck>RfQ$0p|bz%Jh^$l_`rs943x5UMCA4p9Hrn7+hg(F*wb`kjTiy z;KRi8|2t@04>UG{MBf6H3!u1#(XcT%Tr{}60M%uXwlT&S9lXo|jk|)v4|S~06WZp0 z(E{Kxn)?jK1Zng!J9Iv=V|Wn^4B#{ir7^}VKx6oV;Jy(HC|$$b5uh=Bq;WlX8wfOJ zpUEHqDm%e#bWq<OH0B4&1Nbng?F7P47#Nt|FfcHkXJBBuz`($C<^OXK2DL}9VWwIJ z1`uWzU|?Y8W&oWH3dZ2H0?MB_FtY&z1G6Oq1M_|c2Id_M49q7O7?=+-Ffd>I|D5^s z|L2TR41SDJ4DO6k42+CX3=E7>41$bN3_^@i|F1Dd{XfNcf`NhY5Ca2fu7k;wfq^Ll zinIPdXUhKn96WXh8jC~2%%HJDe+CBTrwk0t4;UDj*D^3LuVY{UkKKXB3UT3m3=GU5 zeaQ?A%su~qgU0;uVp9eNW>*FV=7$Un%y$_Wn72dyx#j<J=4=0dgUUz{hJ`6CycnYx zWI*jMxci~*f!PJpgAX&GXJ7zf783>r7CQz877Ydl7A*z_7L)(aK^SHaE}CT;0|Uzh z1_oAj1_o9&1_o9cDAxb~oK^MzbC^4D(JTuY7+BUbFtA!fu^a;ft0DsftKk3VtTO+f z<8wQ>%?(O}P|SRjfq{{U!5!R20nvdBg-k9CWmw%_{Qo&q!T;~fhW|e@TmS#YV$YDs z<j63G$&q0LQy@bbXbhS$>i=8DsQ>pEqyFCpr=h6-*TM4&pgD%^|DQAN`~RHDkzo={ z4@R1ZW$*`$1;fQbeL)aD{{K1C`TswdHZw3VIWkm3@lmW{6!ZTBv+n=r%)$R(Fzf&S z$dbXJ#^lKGo)Ns$>;G#8hX0cp82-OuVEC`iWW%7rq|VR?YV$$GKsROeFfjaw;1diC zOdbsCjG*~2P@4}X&hWpR0fBoM;~0b);}}H1^JJhl1gO0LY7c<go%sxkOg0QEOg0S0 zpt(s<*~?_Zz{Tvy5X>}>0W`<S&t$_O%zTPLg2{$~2P6mX2ZH)MstnwqzB-c)XsqzR z7-((|boM9XRt7m{I|ec4HU<e$o019C)@EYhV&eJ#mnoKk3smNU>Sl1i0W`)28q4ir zNCfvYVC5R9|HJ~$H=r>?ZpJ?hTuh=2iHsK*LYb~Im@@`32r!y5XfUQSXfS3nXfQ@G zXfQrwa06q|oGuJ+W6)p(VbFXAXx?@|gApSyLm&t<F#IoOVECWQ!0@kzf#Lse1_sET z%uM*oNznX4)c^O4s2G&4-aymWTX_0_=3R6?W7Pk5Q2d-R>i-LLd8UgDN#OeK7(+DU z1O^ji%)s!!mVx1aI0FOdZs-4g3=IDxpzIu|cs5iFL}xQFFz7+C2?GPzEIq~v3~<c6 ziGhJxlYyOCjUkcQh#{1@he3?F06dQi9usB$$iT>K#K6wt%#g^O%@E4;k|7a1_J0#R zZxG7l$PmV4!yv-+pCOUCo*|LxC_@xeJp&`tT?SjG7zQrJDh4;k!wiN@4;d1fofx>7 z{22^E^Eym<49eiLqLYDvC4_;2xt4)}xe-Ex`T<PY47}hr1BlK9&wYdDZW)+YFxav> zgZm-^OzRj}nL`+OL2Ur$eGIltUJSZSEew`SaSRO1vl#f9q8K>9IGjP3Ns~c`@fQOt zQy7B@GbaNN<8y`>reFq3Fy>*9WR_=$0gDwdn1kKQ0vbd9|DWkH10z!!12fZC24<!l z24*H@24?0U23{r~1~sM!47N;(|9>%MgZuy7jEeu4GPV8x%+$aT2O75qmuEVVIehS# z259c<2SXy`MFuNICeVEP|C8V`t$7Tlp!O%zeg;KQ-=A3k+&%@(y-S1p|11nFjBa4e z3#u2u?a}!RA|N?XKN-wt`hODKpJZYP1-DUG{67SdX9!?)WpHDh$e_)X$q>q{!63k- z%b>?3%plC<#~{o&pTVDTDT4-cCxagIdImk_g$#PkA_xr5zo0R3(EOMf11Do7$lVP5 zOeG8qOp6#ynZGiC)(tR$?$u&WW8ehm9nhT3Ed~W9CkA!K?+gsg91Ofn=?olT%)t=K z_@6<L@z(zjOx_Hp%n}UdjN2GOSppbBnH3m98NV@vGJRqQWje?pz{JPk$P~_C$~d1P zl*x<106eC#iXjxlXMDw=z&P>$Po^^P+6B<ob!H|h24<#A24=?j|Nk-GV_*i&`GMvR zm|ii2F$FM~GO_;u1}b+zV^fSw49cK-gpr9s8azL~fq|cS69h9wfzu8@(?W(&W)B8q zCIJQpqVewk|Cm6Sm%)pPi9ru5c4P2ja%13Og3^rp8N86N2ND}3?gHm8Ven!KV9>|F z;S63(;S8)y;S64k$_%Oy{Qozj^8eS2$_&g<{!=DCklg>jQ2s53*Z+5eFyk)<FEHND z;Khi6|9@tBz~IG{j{{dQ7%&lw-!K?3zGm<OVUQlie+*tI_$Y%WBNogQ&fv;;fPotw zGlej?5{nssGk^}i5(Lk|f#%_$nDHY6BO|DtfsXGp$TA{faR1ko!IfE$!H9{SL7XX; z!5Gv>0gc(S#53?S-TD8TwVHvAX)6O8(@6$KCPxM(Fji*JWo&2AVY<f<#mvcI3oaL? z{C~kbgMpdZn<0@!l);wy3_~JQHv<FHLk4~@docqq(^Cdf<_QdJ%<C8!SwbLlyCD6n z77Vsbn;F=clo^a!CNik87%+G<2QjcQJ!ar$Qexm{y2g+IqM5@OxS6&xB(iWbB!bFI z&^QcJ(f_L~Jq*H3!QlP`JCh!RDQIq&@dSf8QxbzYW843&pt)J5%Kw)^b9+n?42Fz1 z7?i>IK7%f}U7F7jz<8BGiMffvifJJOGjlQn53?adD6<QLIkOK#D3dU_EzQns#}L9K z%fQG~!63x+ib0s^Hv>EK9PoILIa4`<Df1TwbFkg_7_^yhGbl3eVenwO&LG6}kU^C3 z1%oJ4DMKij{>&f>F1J8)#-OqW)Si!FFk>!c&}MRCSj~8Zfrqh)!JKh7Ln2cRgB*Cy zDvco#JPzB=5XvOLAO^0lKx=LmFxWEPV_;;`XHZ}cWRPI;V&G;{VGv~e#~{w^#=yj) z^#2dj^Z$RC9T~WpIT;d}GyeYp$2F)uTESoh7UN<_Wa@{+2h%BrM5ZzZHKtn(Doh#- ziA<n52~fW`nn8xy;{P9(=Kp_~E;4X2asGeD^nk&T=?a4(I4?Ofa58>o;A9MEP+$!I zpU))Ez{<?YV91okAjy=@Ai;PJ+-{O!a{0d+G>!%;r<ffX%ox2H_?i3}w87(~{tPxu zsSM1_%?x^spmA_B24-e{2609|1}>%~22;kR|Nk>qF)%Q8GRQN}Vh9C=AqX>`X3zm) zCT9j+(ApK|0}Re!yqdw3c{PJ6Q$9lglM+KXc%4W9Lnz}T1_q`n3=E9h8AO>X7`PeJ z7=)QJ83LFx8CaNSF$6HrVhCYc#vlw{3*r9%E0g~JS4>3=LQF#c>zRuF*E0$IuLP?H zwVNg}n1jZoK=l-STnaRXWzL`o9uui!Py*YR&Y;XVk-?l%=>LC)Zw$<gI~jPG92xF` z=UH|#NHS?NsG+a7WBkv+!Q{(e1zIP@<jr8l*v}x&q{N`ZB*q}iWWgZ8q|ac@WY1v1 z#Kj;4UYBRjpbw^%8I+im89YIIVwjW})R~kSSecX=449M|K>K7An3Ne*nUoonplxVX z_<A$M{5+^#4Vj|{twV#fwF#?R&7jJh!XV5n$)L*Y#UQ~P%^(PBA2BmBs4|-|$T1r* zs4|N(sIq{{Y$DBMiDTers%J1`s%MB`bYbvkbYYNS>}L>REMpL0N(7f#;*6UaY#4Vk zOk|c}P-mLT;KO9iV9NM~!IbG8Lko)#13%L$1`lRa1`}pm22&;*23gRW1ExL(Q)V*; z6VMtT@cKtnh7`s<4Dw8|4C0Jm7^)e+FxWwH1K2EehGn2NlZ;;&_Ar}(&0YpFhdG>q zhiN$j4|p9V4}%Be7Y1c0wqbT<FaoJz>Su^!+|8iO@RmV`@e4x*6t_Ul2iXI&V-u4N zLmJ4P%r6-HnOGT2nXJKcL7@2{BPKTn6($A-C$O7&7<?G7GH5gMFnEF7#U#Pt!El$s z6zsM{264s$1{Vf~|0@_78Jro@7<`zHGFUNuWH4k>W^iVD$4~__gW(Z_I>RFdSw>%m z07hR1NjMD}ul>Uy$^3%hGV=?DD{ysA4E*4*a$?ZnmSE6eZ(z`1v}4d<-0}Yz!;AmV zm_-=WnWFwbV_d~x!)U=E!Dz`K&B)GR!^qAc#&DUzlyNqLDZ^z3Glu^R@(lkOWEp-k z$TR$85N9l8;AJdikYIepAj;&#P|gTyhebfgLC|OhGyone$z>2_)?%<`a%13PmS+fM zy2c>Jq{JY|%*(*X1Zw;KXOLq`WKd<g!@$HO#Gu4j!obdWih+qSh(Q^QD;PMybP$6w z8wZ0fjLpQuAjy=&AjNc<!4=E~jkBy^&}2HrpujYpL4i4zL4)}*gE^Beg92+ZLn3n) zXl{$aj}f$fBZz^WNs>W|sh>fIDTg7EX+1+SQwU^D60;VA1q=KCPs|z&g5Y+&3xf{B zxBnBE<QV*rF=IW02$LKGACnw|IFlR$H<KKL6bLicGbl01F{m-gfoTmWTaB@vL5N8X zj6v!_Y+(?e5w!jZqE{KDpGl5Ej0rS%1u-9_7Gwv=Y?vMB?f{tuQU{{JW<t#cxdVhj zZUJGiTcGwK+XXUPow1%l6=W}y9D^Q{9GDH_t1{L@`~`IzNG%MbyF(rt79d)NNsa+T z%OjZ!HV<mH5;UAZ?f|(Fjv4D2)S+r%G*};FJp;&0dnP#)8YG794v>0xCOHNa%vjH0 z3^f-d566u44AM+;m>BAAm^zUAz--2P23;mO1}!wKjhr^XY!8NsOz{kwjL{6j(0>0} z28Msa3=IF4Gcf#bVqo|m3GF|pGBEs~!ocuP6xy#gU|{%vgMs0H2Lr?3UknWYQ=oVw z1H=C<3=IE4^fm^D|8t;t1*j}xU;y{cXEHGSKh41Ke>xKGV_^6XI!um%f#Kgms9UBm zF#Mm#!0>+^)GkoJ-Wl4D2le$~c6vbFq`<)N-w<kt0RzLo84L{n^BEZaTS4uE=?9rH zi-7@jK<9suKN6tkt1&Qu`5g=l{}(eb{0Er}!l1E)Kn8~YOBonI_a*;d!@%(W64bvS zw}SW}3{rcEfq|i!f#Lr&28RC*3=IDlLdH)(V<`WwFff2|Jp;qPItGS+lNlKPgY3*; zU;vxx!NBnMDg(p6o8Vml4FB#jF#HF(HI#wjUks93F^GTvK84W#K;d}=;@*E-85sV3 zXJGg*1BtKyMGOr8jTjjI2{17HKgGcCzl4F|KgjP(7#RK=Gcf$y4Wa*o%vi?2@P82$ zgUs6s9ajN~A7)_q4>}<(4&tZ(v!P)$oq^%sV+MwQ8yFb=$1*Vdp9_tz^9&6CConMl zPh?>DznX#J|4!)G8z^i+VFuC<as$Zh?a;IV5(C)}l23z%FGvg;j{iaGLGtSv82;Bo z{Q~kIXj~68{!<7k+W(a!k6SS?FmOTLc$0zQzcpkm@!xy~hJV>iehf1hnHU10hb7)( za%Gs$<i}74#=Z>gOr{J=85kJyn5-FE8ICasg2ogf;S5SU|2!EO{@-L^_-D$%@XvyQ z;ok}dhJPy|^8fxo^TB**+5@HaQU-?qjSLL`S1~aB2c@|%28RD#3=IFx7#RL{GBErH z`8l0|;qNU5hJQW`41YoC1LWUx3=IE!q4BZ_8vdZL_GV!CpU1%P9~6Ed42nZg7|mv2 z`1g!~0UW1G85sV5W?%rv*A51T|KAuG{y%46_z#LlkQ^v{Pckt4zs$h!e=P&Ue^8ts zfW|E-|A6E-F}5)<Fn(d!%=Dij3Dm~|_u*0)v>BBd4473Jf|<=2f|;Wk^qB84_%r2z z*E}XNzW~qYL&hLM<B+)UA_hOa5E7{&3=9m&cnt#s!yg6)#w834Oluezm_Zlu{9s^U zHDF+1tzckaJ;1=gX28I}*1*8Pc7lNcbYeQY0|Ntl1p@>78wLgr8wLgr&}N|;1_sU_ z3=CWg7#O${7#O(MFfj16Ffj0XFfj0LVPN1>VPN13U|`@|!N9<Gg@J*80t17<69xvs z1O^5n1_lP99tH-XHw+BIE({F96Brmo3>X+h8W<QvE-)~NnlLbkPGDdVJ;T5t=D@%p zwt#^_T!Mi?d;$Z5gbD+L#0CZi$ruI($r%g`l7AQ&q#_s?q`ojPNc%7_NKav4kbc9! zAfv;;Ad|wtAhU#lLFNksgRB7qgKP@}gIoavgM0=9gZvi;289m{42m-t7?d0s7?eTy z1_Oi20tN=v1q=*oCJYQ}B@7Je77PsPXBZeX92gihB^Veq-!L#}&0t{Ac41)9zQVwu zlfuBDbA*9GH-&*gcLxK5o(%(o-V_D~eF+8z{TK!Y{TB=j1|?8@hJnFw2?K+X4+Dd- z3Il`j90msCKMV|}0t^hM4GausGZ+}m|1dCEI503+v@kGOOkrTKT*JU%`GSGLDu;o= z>Iwscbp!*0jSK^WO#uUgtpWps?G^?G+bawVc0LRYb}JYd>`fRL>?;@;93mJP99<X~ zoPIDcI7=`vINLBVI43YLIJYn`IImz}a6ZGp;QWDs!KHwK!KH(N!DS5tgUba52A3}k z46YIk46Zf|46X?b46ZE<46Z8}7~Cuv7~EnQ7~C2d7~GaHFu0vyU~qfGz~C;xz~Jt~ zz~CXmz~EuQz~B+Xz~Irqz~Hfjfx+Vh1B0gp1A|u#1A|uy1A|u&1B2HR1_rM^3=CdZ z7#O_XFfjPoFfjOpFfjP!FfjPEFfjPcVPNpt!oc8jhJnH72?K-A9|i_r5e5d|D+~<2 zZx|T-SQr@mWEdFyOc)sad>9z~QW!wf1q}Xs7#RGoFfjPPVPFVgVPFW5VPFU_VPFVk zVPFW9VPFV!U|<N0U|<NWVPFWH!oU!?hJhjQ2m?dl3kC+zZDT<y3=Baw3=BaD3=BaP z3=BbY7#M=KFfat&U|<OPzyLb(f+56&fgvP<fgz-TfgxlH14GCf28NIe3=APJ7#Kns z7#PAt7#PAV7#P9=7#PAz7#PBO7#PAfFffFjU|<M)!@v;E!oUzN!@v-3!oUz7!N3q+ z!@v;1z`zjkf`K8Dhk+qdg@GZ`gMlG3fq@~ig@GY*4g*8v76yjMGYkxo9~c;-I2ahB z6c`wyTo@RlVi*{rN*EZTdKeg@elRdZ%P=rRJ1{UrM=&r%7cekHPhnt)Uc<l;eSv`? z`U?X?Ob7!*%n=5Lm=_G-vv6W17#Ly=7#Lzh7#L!67#Lz(7#QL-7#QNdFfhbRFfha$ zFfhc2FfhdDFfhbVU|@(}!N8F4gn=RP4g*6H0|P^n1Or2o0Ruyl2LnS=4g*6{3j;&a z3I>Lx0}KpFPZ$`I{xC2ki!d-G>o71R2QV-sXD~1%H!v_H&tPCk-owC<e1(A_#e{(& z<p~2rDhC5YssaN;stW@{Y77HIY6Am9>I?>k)IAIgsaF^nQhzWoqzN!Eq}^d)Nc+OT zkS@W%kZ!=hkRHOoke<W9kUoKdA$<!2L;4v8hV%~%3>h2@3>i8M3>hvA3>haF7&03e z7_w>@7;+>S7;<bF7;-`w7;-8Y7;+{sFyyRYV8}VZz>urMz>qtGfg$$}14Etz14G^v z28MhI28R3&28IF_28MzO3=9Qt7#IrgFfbG`FfbI!FfbH3FfbIQFfbH#FfbIYVPGh_ z!oX1UgMpz~fPtY{fq|jefPta7hk>C)hJm3(hk>EQhJm5Phk>Ew2?ImP7Y2q>76yjW z8U}{a9tMWeISdSC77Pq!9t;d+YZw^H_AoG%2QV;{ConLS7cekXgfK8vq%bg4vM?}I z9$;Xoe8Rv`#lgT(rNF>YWx>Eu6~MqymBGMJ)xf||UBSRm6TrYwbAo}PwugbC&VzxW z?hXS({Q?Gt1`7s;hB*uj4Qm(}8c#4VG~QrfX!2lSXck~#XjWigXc1vxXj#L+(CWd! z(0YP_q0NPXp=}ETLwgDXLx&6lL&pXNhRzrUhRzZOhRz-ahR!7n44r!z7&@;oFm(Q4 zVCcHRz|i%FfuVZ|14H)~28Qk<3=G{b7#O;LFfjC9VPNP>U|{Gw!@$t@fq|i4gn^+y zg@K{Jgn^;|4Ff~}9|ndAJPZsICNMBeSirzAQG|hE;u!{pNhS;olOh-xCaqv#m>j~u zFgb;RVR8ur!{inQhRIVH7$z@aV3@pxfno9y28JmC3=C5e7#OA$FfdFx!N4%pgn?md z3j@Q{Hw+BZQWzMfy<uRO9>KscgMooz#t#ODnKcXyGkX{qX1Oph%(}zCFk6O!VfG9L zhS@6^80IK2FwA+tz%bW^fnjb81H;@k3=H!E7#QXqU|^Up!oV<Jg@Ivy4g<sd8U}{> zJq!%<&oD5|zr(<=pn!p4K?4KBf-4LR3mq637S3Q`Sj51<u&9B7VKEB>!{Q7Eh9v?F z3`;IBFf4UpU|1T%z_2WVfniw%1H*C!28QJZ3=GR17#NmEFfc67U|?9Wgn?nD0RzKI z2L^_f5ey6~3m6zy9$;Wt#lyg`s)B)G)f@(fRa+PsR^4D=SoMK{VYLVY!)hG{hSdQK z468F37*;P}U|4;FfnoIv28J~j3=C^(7#P+ZU|?ADfPrDH3IoI12@DMD7#JAVDKIds z>tSG6FTudDK8Ar|eGLP{hCd7p8+8~MHr6mOY+S*>u<;55!^SrZ44YIK7&dt@Fl^ev zz_7W2fnm!F28OK{7#OzQVPM!khk;>74+Fza0|thj4h#%Ce=soY4q#x|JAr{=e+L7@ zfd>o>2W=P_4z6KfICy}8;gA6X!=W_{42OLf7!J>2U^tRN9zLnSz;N;d1H&l}28L4- z3=F4b7#Pl|Ffg2X!N72qhk@a&3IoGg8wQ56Aq)&>Z!j>No5R3x-iCqU{2K;_3riRn zE{ZTPT(n?dxERC0aIu7e;bIR1!zBX-hD&D{7%oRJFkF7Yz;Km;f#I461H-i`3=G#f z7#OZEU|_gmz`$_h4g<r@76yh}I~W*luV7%f%fi5LFM@&LUIqihy$S|~`w|Qc_ct&w zJV;<*c&Na@@X&>U;b9B|!y_LChDRw343AA17#@FMV0fCq!0_w}1H-c)3=GdD7#N<f zU|@J*!ocvNgMs1I8U}{fe;63v9ARL1+rhx_PJ)5q-3kVV_YMpUA6yt1J}hBi_{hM( z@bLiy!>0}ghR-q#44+ppFnm71!0`D71H%^&28J(N7#O~4Ffe>wz`*cLf`Q@N3<id8 ze;63PConL4Kf}QA!-Rq1#}o#JA0HSPeqLc<_|?I{@OuIS!=F1041c~bF#P3VVECKD z!0>ku1H<1l3=IE3>pwyLO;A6n0lcn&ks*SCk>LmfBcl%kBjXtcMy41BMy59mjLZ=X zjLaDfj4TBVjI1>bjBIll7}?h_FtYDqU}S&5z{viCfsx}110!bv10$CO10%N#10(kp z21cF)21cGE42--w42--h7#R697#R7sFfj64Ffj6aFfj7xFfj7BFfj73U|{4wz`)3V zfq{|#0Ry7|3j?Eo3<IOU2?j=i8w`vBe;61AMHm<bEf^RD0~i<uOBfghdl(pn6c`wV z_AoFCJz!uI`oX{`EW^MkY{I}O9KpaST)@C6+`zynJb{5xcnbrg@EHb1;SUUqA{-2i zB03C=A}$P!A{h*fA`J|TB1;$;MfNZ-iacOo6#2oxDC)z&C_0CMQS<-<qv#C=M$r!p zjABz57{x6Z7{#wJFiLnZFiLD;V3gEhV3f3BV3d5pz$mqcfl>Ml1EWk21EZ`11EcH* z21dCL42<$M42%jo42%kQ7#I~x7#Nip7#Ni*7#NjaFfb~wU|>|<!N90;hk;Q|gMm@a zfq_vif`L)3hJjIS3In6s4hBZG3k-~EUl<tGc^DYg4Hy{JJs23(a~K%aTNoJCH!v`& zUtnNV|H8ni!Nb6)VZgws;laSDk;A~K(Zax}v4Vk7;{XGr#v2AkO&$hDO%(=4O&bPA z%>)KU%?bua%{dH=np+qcHE%F5YJOl~)DmG})Y4&K)Cyo=)XHFB)aqei)LO#8sC9vX zQR@Q(qqYbGqqYtMqqYkJqjn4fqjm`cqjnDiqxJ>{M(q;}jM{G)7<E_}7<Dul7<C*N z7<Ez@7<FnG7<CpfFzW1JVAOfSz^Kc@z^JRiz^LoMz^I$Tz^Gfpz^J=`fl+q{1EcO8 z21ea442*g_42*gz42*g<42*gq42*ge42*gc7#Q`oFfi(!VPMqzz`&@_!N91m!@#KT z!oaAX!N92Bz`&?KgMm?h0|TS}2?j>}Hw=sh0t}1>8VrmEJ`9WoDGZDTH4Kb~JPeG6 zYZw@fCNMA>`!Fz?a4;~MoM2!yy~Dt0c7%b^+=YSBd<_GmIf#9SfzgtOfzb+tWf&N( zbr=|}H!v{T6fiK_GB7aO9${d#+rq$TcZ7k_zJY<!frWw5QG$Wd(Sm`|aS8*Y;}Ql& zryK@Gr!NeQ&Q};1T`CwDU6(L0y74eDy0tJcy7w?JdiXFfdK_S2^!&oW=(UG|(K~{H z(K~~I(Yu0y(Z`2@(dP>TqwgLDM!z!*jQ%kUi~#}+i~$E27y~XaFb1w+U<^uNU<`W0 zz!-dific8`fidI>17qkM2F9=e2F7q72FCCs42%&D42%&042%(97#Jg`Ffc~NFfc|N zFfc}+VPK3&VPK5qVPK3s!oV2E!@wA)!oV1JfPpdY0s~{*9|p#F83x998wSSs5C+Eh z90tbt76!)nISh>PTNoJQ&oD5?e_&ut;9y`(&|zRqaA9Cf$Y5YhXkcJWSi-=Vu!n&$ z;Q<3<!Vd<<L>UIgL=y(a#0Un)!~zD!#3>AniE9`b6E83@Cca=`Ok!YQOp;(=OtN8M zObTIOOjclEOle?XOj*Lfn6ig~G35aRV=4;+W2y`TW2y-QV`>5eV`>8fW9kwH#?(Cw zjHy=`7*pRcFs89EFs5lRFs3;$Fs6NBU`$`Zz?go9fie9F17ijU17n5)17n5@17k)E z17k)517pSv2F8p%42&697#K5tFfe8cFfeACFfeBNFfe8oFfe9zFfeAWVPMQW!oZmM zf`KuMfq^kgg@G~4hJi6Ffq^lrf`Kt>4g+J>76!)b00zeFHw=t90t}2fCJc-@R~Q)c z9xyQG&tYIJs9|6%n8LtVu!MoJ-~<C>!2<@yf*%Zwg#rwWg(eJ)g+2_7g#`?Zg&hox zg=-iX3y&}`7T#fCEd0X2SR}!~SY*J!SoDQ~u|$S}vE&W|W9b(L#<C|2jO9ld7%Pr2 zFjnqhV5|~hV5~a8z*xP9fw3lrfwATa17ocR17jTr17qDB2FAKI42<=A7#JJ=FfcZ@ zFfcY9VPI^&!NAz^gn_ZOfq}8jhk>!}1OsDx4FhBQ90taY6b8nQ76!)74GfH34h)Rl zDh!N0HVlltISh=wEewo(8Vrnm77UDi9~c<>OBfg@tYKiBu!n(hVgdu>Bn}3~Nk14E zCrdCePQJmwIOPTd<J1QXj8i`_Fi!JeV4Tjwz&QO11LKSc2F95#42&~V7#L@<Ffh($ zU|^hmfq`+(6b8mQUl<tYvM?~tOJQJ~FT=n%Ux$Hlz7GTA0uctr1xFYd7ZxxuE(&2_ zT(pOQaj^;m<KiU@jEhe&FfLiaz_>JqfpM7w1LLwQ42;VUFfcBEz`(eofPrzP2?OKG z6AX;2rZ6zB+QPuN>IwtnsxJ(Ts~s2^SKnb^T+_n9xMm6i<Juku#&rq|jO#2I7}uR) zU|gTTz_`AGfpL8Y1LFo02F8sJ42&BC7#KG#U|`&IfPrz-9|p!PJPeFmPB1WTZDC;C zZo|O1vw(qd_W}mSy(|oj`#cyJ_pe}JJm|u}c&LJb@h}eq<KZ0)j7MY`7>_bAFdkE2 zU_9o+z<4Z!f$>-m1LLs`42;LFFfbnb!N7Q2hJo?80|Vpn6b8oQB@B$mTNoIRPhns@ zzJ!7C_#Xzw6Cn(YCoV8Bo_N8)c#?yG@uUU=<H-^R#*<SR7*8%?U_7~nf$`)K2F8<5 z7#L5nFfg8yU|>9@!@zi|hJo?a3I@hgCm0w{y<lKG&B4HUT7!Y{v<Cy@=@tgY(=!+t zPw!!1JpF)y@eB(C;~5PG#xp()jAv#rFrInCz<Abyf${7V2F9~D7#PpVFfg9WU|>Ag z!N7QK1q0)`9Sn@;85kJPdoVDbFJNFiKZAksJV@*g1LOHW42&0K7#J^DFfd-IVPL#) zf`Rd(00ZO200zd3dl(omDKIczs$pQf)Wg7d=>h}eWeo<#%N7ibmpvF5FXu2YUY@|f zczFQ><CO^vj8}FrFkZRCz<A{i1LL&>2FB|m42;(oFfiV5VPL%Rg@N(r3drW8TW1&; zZ%<)hyyL*YcxMj-<J~t5jQ373Fy6btz<BQk1LM6P42<_V7#QzMFfiWNU|_s&!N7Rm zgMsn>69&cy4GfGAZ5S9I`Y<p)OkrSrSi`{hu!n*1;T#6Whie!ZAMRmbe5Ap^_-F|O z<KsIFj88fk7@vwTFg`6{V0`+6f$^CS1LJcZ2FB+F42;h&FfhK*VPJgG!@&6B4+G=N z7zW0dR~Q&y#V|0wYGGh}wT6N56^Q?Zf${YU2F5oG42*9g7#QDNU|@V3!oc|U4g=#m z2?oY@H4KdJmM}2BJHx>E?h6CsdkzN1_iGp!-|t~y{1Cvv_~8!&<Hscoj31vcFn+RN zVEk0U!1$?yf$`HH2FA||7#KevVPO3Hf`Rc14+G<uCk%{VIT#qf>M$^V4PapWTEf8i zbp`|D*F6l3Umq|qeq&)^{HDRc_|1ob@mm1{<F_{qjNb(q7{8k^Fn*6<VEkUg!1#Ru z1LOB242<7jFfjh$VPO1Wz`*z;gn{u#1q0)cISh<H4lpqOc*4N=lY@crrw#+-&j1F- zpCt^8KW8v7{@laB`11h+<1ZEl#$Or?jK6#s7=INoF#ejt!1!wi1LLnd44{QOjK5VF z7=L>(F#gVAVEjFSf${ei2FBkv7#M&5VPO2Dz`*#&g@N%;3IpSx76!&YOBfjc9ARMm z^Mrx%FAD?XUlj(%zb*`ne^VG3|79>R{(r#0#8|+<#593{iTMEo6Uz$*CN={GCiVaZ zCJqS(CQcOwCN2dACaxz8Ox!FCOxzj_Ox#x(n0SvcF!5P1F!AkSVB){Sz$8$?z$6&L zz$CPTfk{M%fk{+{fk~`|fk`}tfk}dafk{$;fl2ZU1Cz7}1Cxvd1CuNd1CyK#1C!hy z1}6Cs1}6C@3`~j@3`|OE7?_klFfgf{VPI0-!@#7rhJi_a1_P7E6$U2F84OHX4Gc`$ z1q@6&4h&2>DGW?H9Slr5YZ#bxE-*0Zd|_bHm0)1fwP9e=O<-WsZDC;2UBSSldxn8Y z_X7iyo(Kbzo&^JwUJL`1UIPP@-Vz2Ty%P*fdT$t*^aU80^i3F;^dlIU^lKQH^cOHN z=^tTW(tp9gWWd9~WMII+WDvr@WKhAtWH5(;$>0D3lfe@PCPNMeCPN(tCc^**Cc_d2 zCc_yFOokg6m<&%aFd05zU@~H1U^3ERU@{6}U@|IUU^1G)z+|+6fyw9u1C!AM1}0+` z1}0+-1}5Vh3{0jL3{0j03{0jO3{0jy3{0j=7??~?Fff@uU|=%+!N6oDz`$gt!N6qZ z!@y)#!N6oTgMrCx4+E3g0|q8@76vAB83rbE2L>kd6b2^q8U`lwDGW^JI~bVE?=Uc# ze_>#<;9+30P+?%Quwh`b2w`Bds9<2Sn8Uziv4w%j;syhg#UBPHOA!VpOC1I#OBV(v z%M1o4%LWD}%NYzzmPZ(vEI%+XS&1+(Sy?bJSp_gKS(Pv_S<PTzvf9AFWOagp$?5?E zlQjzileG*3leGy0leG^6lXU?Dll2q^ChIi}Ox8yjn5<ub4q#$nvQc4Rvaw-cvPocI zvZ-KTvYErcWV3~V$>s(Flg$SPCR-5(CR-f_Cffi8Cff`KCff!ECfgYdOtwcDm~5Xg zFxhc1FxlxaFxdq#Fxh1=FxfRQFxkyuV6xl8z+`uYfywR%1CzY~1CzZC1CxCW1CxCN z1C#v%1}6I*3{3VH7?|w8FfcjrFfci&FfciIFfcjfFfci^Ffci+U|@1Mz`*2igMrE6 z4+E2<0t1ty1p|{~3<Hy62?LYk3<f604Gc_<Cm5I<A22XEu`n<>$uKZEnJ_Rp`7kg! z6)-S4buchFtzlqtI>Nx@^n!uOnSp`HS%rbg*@l71IfQ}9IfsGCxrKqrc@6`U^A-js z=Nk-6&L0?<TsRn*Tof3XTwEBKTw)lQTuK<2TzVLoT$V5}x$I$Ja=F65<no4r$(4nH z$yI}a$<>E}$+dui$#n_?lj{x!Cf7R*Ol}MeOl~R+Ol~#|Ol~0zOl~<0Ol~a<Om1@+ znB2B7FuC1eU~>Dzz~rvNz~t`3z~o-Sz~nxMfysRf1C#q11}66>3{38S7??an7??bC z7??a<7??a_7??a77??bkFfe(XVPNw3!ocJy!@%U}!ocL2!@%S@g@MU)4Fi+s5e6pD zI}A*oUl^FYco>+xR2Z1NY#5llLKv95Dj1l&<}fgM9bsVddc(lvEyBR$ZNtFiox;H6 z-NV4-y@Y|udk+JX_Z0>v?>7ugJ}eAOJ~9kUJ`M~_J}C@LK0ORfK3f==eC{wX`LZxD z`N}Xb`I;~=`T8(0`KB;1`PMKn`A%VA@?FEg<a>mH$@c{Vlb-+slb;0xlV1!2lV1q~ zlV1-5liv~sCcixlOnz4wnEc)_F!{4EF!{?cF!?($F!`r2F!}c|F!^s`VDi7iz!bp3 zz!advz!VU`z!Z?dz!cEHz!Wfpfhk}E15>~W2Bv@q3`_w(7?=VD7?=Vz7?=Vb7?=Vh z7?=VJ7?=V(7?=VVFfaukU|<S-z`zvvhk+?bf`KW>gn=n2fPpC}hk+@mgMle%3j<To z0|usG9tNgh3kIg(6b7c?2@FiZdl;C4UobF*a4;~1XfQB^crY-9WH2y=bTBZ5tYBaY zIl;gb@`8aWRD^*k)PaF1w19yrbO8fX=miF*Fa`#uFarjrumlFCum%REumucEVS5;u z!fr4yg?(XQ3Kw8t3fEy^3in`O3NK+`3SYp$6n+7+c__kwfhi(^fhl4F15?BS2BwG` z3``Lp7?>h?7?>he7?>g*7?>g>7?>hU7?>hwFfc{#VPJ}Uz`zv6!oU<I!@v|(!@v~H z!oU<Q!oU=*!oU>$hk+?ZhJh)@gn=n0gn=n0hk+@khk+?(0|Qgc6$Yl59}G;fG7L<y zCJan*5)4dne;AnJcQ7!;UtnO0f58CWOr9XYz?5LXz?9&@z?6`~z?3k7fhl1N15?5c z2ByS43`|K27?_e{7?_ev7?_fK7?_fmFfb+WVPHzW!oZaLhJh)Cg@GwWhJh)?fq^L{ zg@Gxhhk+?&3j<Ti4F;yv6%0&iJq%1~HyD`GzA!MQ^Dr=_YcMdSJ1{V%ConLjw=giJ zuV7$GKf}P3{(*rhgM)!7BZ7e`V-EvU#uo;r%m4<a%q<K|SrQCPSpf`8Ss4sWSyvdC zvLzUpvSS#SvX?M0<)|<)<$&lV3{1HK3{1HS3`}_j3`}_k7?|>17?|=~7?|>pFfbK} zFfbL&VPGm0VPGn}!N62BgMq1N2Ln?Pi2Z|ssaS%6sn~*nsrUs0Qz;JvQ<(z;Q+WXc zQ-u!$Q{@^4rm7tbOw}d~Ow~IWm}(Umm}-A8Fx5R_V5<MZz|^pUfvIr?15?u)2Bu~g z2Bzi&2Bwx62BtO!2Bx+U2B!8E3{0JK7??WOFfes#Ffer;U|{M_VPNV}VPNWc!obv* z!NAlX!@$&^!@x8_hJk6q6b7b=4h&2aS1>S55@2AO^n`(F(iaA%$zK?lraWO_n!1I7 zY3d&arfDt=Ow*n)Fin?WV4B{*z%=~<1Jeu}2BsMc7?@^UVPKlc!N4>#gMn%02?nN_ zFBq6+NiZ<Y@?c<^mBGL?>jDGQY!wEk*#QhpvpX1==EN{C%{5_Qn%ly_H1`Gr(>xOf zrg;+>nC7iuV48P;foUE{>;nVSd=3Vt`3ek7^DP*d<_9n^&Cg(9n%}^{G=ByI)BFt# zO!H4LFwK9!z%>5{1JeQl2BrlX3``3g7?>7BFfc7BU|?F%!N9a&0Rz*59SlqhE-)}H zc)`H5kb!||p#%fdLIVb-g&qt{3lkWa7FIAYEu6r>v?zsvX;BFS)8ZHgrX>*!OiMi& zn3lOPFfF%WU|Rlwfoa7G2Bwt>3{0yG7?@VwVPINaz`(SIg@I|!7Y3%aYZ#c;*)TAz zTfo4yo`ZpDg8>86#ux^sO%V)Cn*$h_w&XA{ZS7!S+O~v&Y5M^NrX4#Nn0EFsFzu3I zVA?Ihz_j}a1Jm9R2By6S7?}1MFfi@k!N7Fj0|V2+2MkPyJ}@vH_F-T;yn=!0hyerB zku3~NM;#cLj=o@EI_AK@bnFfT)A2V9Oec64m`<!=U^?l;z;yBl1JkJx2BuRF7?@78 zFfg6o!N7DThJoqq3<jpN8yJ|*-C$rkzk-43f(--Hg(nP57uPT_T|C3UbSZ#=>Czbn zrpqY|OqX{sFkKO0V7hXFf$7Q%2BxbF3`|$2Ffd&!VPLv`fPv{o2?Nv35C*232N;-c z#V|14W?^8ueTISQ&H@Iey9NwQcb70Q-HTyhy3fPFbbkW_(}NTSriUsFOb>4`Fg^Ui z!1PFhf$7l|2Byaq3`~#zFfcuFVPJZ)gMsO30t3@C83v|jQy7?@TQD#^f5O1@Vg&=! z%Mb>pmnjTPuSyu0UVAVwz0P1@dfmam^u~pO=`9GqVPJauhk@yx2m{kQ9R{X%E(}cX zVi=g-l`t^9>tSGe_k@Az-4_O?_bd!d??o7xKIAYkeW+nz`dGoh^s$41=~D^=)29gx zOrKUTFn#7>VER0Tf$56|1Jjoy3`}1e7?{2;U|{+-fr06}2m{mi1q@8zk1#NOzrw)u z{R;!rj~oW39|stiemr4d`pLn-^izR>>E{s!re7)yOuzmxF#UeP!1Sksf$6UX1Jl1A z2Bv>k7?}Q7FfcRRVPIxbVPIzNU|?psz`)FA!@$fwfq|KQ0RuBf3<EPK3j;If6$WN5 z3kGJc1q{sG0SwGM4GhdYGZ>h8r!X+{GcYg<XfQAfDljk$`7kgG?O<RQ4q;#xzQVvP zvW9_KbO{5q*a`+_u^$Y~;wu=KC0ZDmB{dkBrA{y~OZzY|%S13R%bGAS%c(Fh%Uxk$ zR?uN!R(Qg|tXRXqtmMMLtdzpQtTcgvS=onyS@{bCvq}jAvuX+hvl<Hnv)UR4X7vjU z%o-I8%$g<)%$i>qn6=h0Fl+5$VAeXrz^wI#fmyqOfmug{fmug~fmz3efmtVpfmx@7 zfmx@Afmvq>1GCN^24<Z*49q%z7?^cM7?^c+7?|~P7?=&_FfbbmFfbcxFfbc>Ffbct zFfbc7Ffbd=U|=@f!N6>Ifq~iZ0|T>>3Inr|3j?#!0S0EH8w||GH4Mxq91P4R3JlC9 z4h+mD5e&>G6%5QK6Bw9H-Y_tm<}fgu_AoG;&S79S{lLI%#=*dBroq5$=E1;hmchVm z*1*7QwuFJ%Y!3so*&POEvo8$H<~9t>=4Tj~%|9?OTL>^PTWByaTR1Q<Tg+f!wp3tX zwsc@%ww%JiY`KPk+42knv*jBGW-As3W-Ap2W-A*8W~&eeW~&kgW~&|sW~(&}%+?$X z%+?wV%+@Uo%+_-ln62+HFk63NV78H9V74(}V73WiV7A%9z-+6-z-&8(f!THq1GDWJ z24>qQ49s>F49s>j7?|w^7?|xf7?|xn7?|xd7?|xF7?|xr;(HjF?H@2OJFH+}b~wSn z?C^qt*(rg6+35lUv$F#Ov-24SW)~j@W|tHOW|tNQW|uh(%&ra$%&rj(%&rv-%&rp{ zm|Zt8FuR^$V0PQV!0h&if!RHVf!V!=f!Tcu1GD=Y24)W)24)Wx24)Wz24;^G24;^M z24;^b49p%o7??e7Ffe;oFfe=0VPN*$!@%q%!ocjchJo4Z2m`a%3kGIy1_owt2?l2G z90q3ZJq*m=R~VSRzc4WS@Gvm@s4y`5G%zsx%wS;l*}=f<a|e7<I<v0}1G8@q1G8@n z1GDcO24>$q49tE!49tE<7?}MX7?=YD7?=YdFfa#2Ffa#uFffNCFffNyFffNaU|<f- zVPFnpVPFpX!oVEf!oVCM!oVD{hJiV90|RrE3j=f10S4yi8V2SV83yK<2Mo+HKNy%} zTNs$*EEt&Mb}%r<-C<yk-^0Khe}#cLA%%fCQHFsz$%TPA=>Y?CiUR|4$^{1IR0{^? zv;qd^v_A~Y=?x6b86phKnK2B^nK=y1SqcoySt}Trvojc&a|{@ma}^kv^CTFU^X4!x z=L;|}=PzMk&VR$eT;RgMTyTVexiEr(x$p%8bFl&gb4d>abIB40=8`Q8%%u(t%%y)A zn9H6pFqfAwFjp`zFjvfBV6NE0z+Cx-fw`)Lfw|g%fw}q!19MFa19QzB2Ig872Ikra z49s;249xWc49xWx7?|r{FfcciFfcbNFfccrU|?>}U|?=Jz`)#Ez`)#khk?1Bhk?0$ z1_N`42m^D+1_tKN9}LW091P4|I~bU|Ll~I5pD-|Y|6pM5kzru&abRHXS-`;DE5X3r ztHHqB`+<SEkAZ=?FNJ}*?*;>N-wOuj{uK<&6OJ%2PnyEOJox|v^VBsA%+n<pm}jUk zFwZn#V4fYrz&y8yfqC8@2Id6;49p9TFfcEAz`(qifq{9k00Z;l9SqD%92l6FEMZ_? zYQw<1OoxGac>@FU@-Ga`D|#51S3F^0Uh##2c_j-2^Qr|5%xmT_Ft5{LU|w&-z`Xtp z1M`L(49puRFfeaQU|`;C!N9zQg@Jj?76#_6a~PPnIWRD9cVS@OeuaT~#})?WojnZ9 zyLuRycfVj@-m`>(dG7=U=6xp^nD;MWU_P*gf%(7@2Ihk^7?=;;VPHP|g@O4<4+Hbj z00!n`GZ>hU+b}Snn8Cn&atQ<TsV5A~ryCfU&un2}KD&c~`P=~p=JPcS%ohq6m@k?z zFkg&dV7@ekf%)<p2Iea*49r&-Ffd<ZVPL+thk^OJ1OxN+4-CvVc^H^)#V|16y28MG zdj|vaod5>rJ0BRB?;c@bzITCv`Q8Tx=KCucnD6gkV196if%#Dl1M}k;2Ij|G7?_{r zFfcz=VPJlGgn{|#6$a*~Ul^F5i7+rfGhtwU7Q(>%tb~F2*%SumXImJUpIu>Ke)fZb z`MC@O^K%CV=I1F4%+EU*n4hm<V19mqf%%0B1M`a+49qXyFfhL?VPJk`z`*=!0|WEx z2nOai5)905QW%)u+AuJ`V_;x@SHZyi?hXU<`veB&4<ZcAAMP+Pf2?6({$#?y{HcL~ z`Ev*Z^A`>V<}Y&?n7^!HVE(d)f%(fB2Ien!7?{7jVPO99hk^Mk4+Ha883yLBIt<KT zZ5Wup#xO8{tzlsPI){Pz>mCN?uXh-jzy4uh{wBk~{LO}e`CAMF^S2rX=5KQtn7{2| zVE%T8f%)4X2IlWF49wqc7?{7uFff0wVPO6~hk^O~9tP&`R~VSTe_>$$A;Q4?!-RqP zM+gJ+j}ivvA5$2Ze{5l3{&9tY`NtOq=ASYQ%s*`yn19AFF#oJ!VE#FWf%)ei2Iil4 z7?^+lVPO6x!@&H@hJpE43<LA8DGbcN-Y_u#c41)tJ%fSy_Xh^%KPC*!e;OE=|6E{T z{_}!?`L7QH^WQZL%>NA-nE&5lU}2cTz`_W^4;WaOk1((>-(g^3X<%SsRbgOZUBSS@ z`h|go?F<78djtaudkF&zdkX^#`ws>d4haSp4jTp*P8J3hP8|jo&JYF`&JG3^t`G(m zt{n_4TsIh4xLz=@a7Qq(2*)t62v1>P5njW<B7B5_Mfd>&i|`)?7Lg1F7LgSUEFymx zSi}?<Sj18oSj1`=Sj46<u!yZ;U=cT9U=hz?V39CkV3CMmV3DX{V3F9tz#`eez#{pA zfkmo<fko;M1B-MG1B>(r1{Rq)3@ox93@oxa3@oxs7+B;g7+B;D7+B;37+B;B7+B;d zFtEsPVPKIz!@#0o!N8)hgn>oj4FiiJ4+D#$3j>Q%4FijE3<Ha*3<Ha*2?LAj2?iE5 z5e61D6$TbH7X}u!6b2Tx6$~tDPZ(I#Wf)l0V;ES}OBh(xdl*<WA{baSbr@JQD;QWb zk1()kzF=U{{K3GY#lgU$rNO|W<-owAmBPTHRl~rdwS|F2>kb2p)*l8IZ4m|*?F9@h zIyww2Iynq1I%^nMbXgc!bY&P=bSoHGbeAx&=$>I<(Gy`{(TibV(Pv>`(ci+rVj#f4 zVxU4gHWXlBF`U7`Vr0O;VswOo#l(Yw#pDPBi|G>v7PA=)EM_|xSj-(5Sj-a`Sj>+w zuvk<uuvmcTCk!l>HViD5D;QWTPcX1pNieY3Ffg##C@`?tlrXT^^f0j4YA~?adN8oq zW-zeWb}+EmZeU=s=U`xQs9|7na$sO_ieO-I=3!uQ$zWh{X<%S+nZUr}8p6Qh=E1<? z&ceXrS;D~LCBwktwS$4hdj<oG_X-9UpDhe5ejyAjem@vk{3{q(0`@Sl1R5}~1U4|R z1YTfZ2~uHT394XV2|B>Q63oHC5*)(75<G{2CHM&gONb5wOGpU=OUMBRmQVo(me3Xk zme4B<EMW=^EMYkeEMW&2Si*T2Si(~nSi<)(utW$jutdZ#utaQNV2Sv{z!Dk2z!JHH zfhF<}151<z14~o|154Ba29{_Z2A1d)2A1dz3@kA$3@kA*3@kBQ7+7L?7+7L`7+7K_ zFtEhlU|@-JVPJ{tVPJ{7!oU(Q!oU(A!N3x~gn=dg1p`Zh4FgL;2LnsO2?mx#76z6? z9|o4h2@EWWHyBuwR2W#2G8kBrHZXuLZDUCeVPHvK!N8LIhk+%<fq^BZgMlUG3Ij{3 z3<FDQ3Ij{(8U~g$83vZL2@EXh91JY!H4H52M;KT#L>O2y5*S!AwlJ_{axk!D#xSsC zE@5EF{KCMJ<-ov_HGzR8>kb1;wgCf6b_oMZ_5lW#91#YVoD2q*oC6Fjxe^R4xiJhZ zxeFLra^Enp<e4zA<kc{+<Q-vP$>(8U$&X-Q$zQ?1QlP-VQc%LcQgDKSrBH%_r7(km zrEm`eOOXHrOHl*^OVJVrmZC2VEX58CEX5NTSc;!8u#{*pu#}WAu#}u&U@4VhU@0wN zU@1Mpz*5G;z)}{#z*07cfu-yT153FM150@Y155b<29^pA29}Bl29}C53@nv03@nv7 z3@nvf7+9)U7+9)&7+9+2FtAj8U|^|sU|^}9z`#=dfPtmPgn^}|gMp=H0|QI#83vZx zHw-Lw3=AxF0t_s5Dhw=j77Q$PJ`5~%2@EWCB@8Te9SkgWa~N3aHZZW%9bsUpXJBBd zcVJ+tpTWS=puoV=u!Di6QGtP_(SU)a(Sd=bF@S-kF@b@lv4Mf5aRCEM;~NH+CJP3Z zrWyv8racTS%_0me%^eIZ%|94eT0$6DT1psLS{^X4wE8fxv@T#^X+6Ne(hA~#U|?xu zU|?xeU|?zMVPI+JVPI+3VPI+ZVPI*`VPI(oiLGH^X+Oij(!s&N(jmjZ(qY5E(h<YJ z(lLjDrDG2ROUEAumd+3cmd*nVEL|cDEL~F=Sh_ASuyi{xuyiLduymI&uynUDuylW5 zVCi9DVChj|VCm^#VCi*WVChp~VCiRJVCj!xVCi4Lz%oUFfn~}R29_xs7+9uyFtAK* zU|^a0g@I*S1_R6VGYl-#Uofyt|G~gABZ7ftrUC=Y%moZAvsf5dW~nf+%t~Njnbp9+ zGMj~gWp)k&%j`W2EVJJ*u*@-FV41Uofn^>K1Is)M29|j_3@r0H7+B^lVPKiRgn?zj z3I>*i5)3R0Js4OP_Asz43SnSbG=qU<F$)9BVh;wE#akFymc%fyEIGr#vg8Q^%hDqZ zEX!6fuq>BhU|GI{fn|jZ1ItPQ29}jE3@j^8FtDur!@#mCgn?z%83vZs6Bt-lZ(v|q zeT9K#^$!M?H8Kn=YaAF@)}%17tm$B2S#yAaWz7=?mbDxVENdMYSk|U6u&nK1U|G9{ zfo1Il29~uS7+BV^FtDu4VPIJw!@#n>fq`ZH5(bv_Cm2}PzhPk6Ai%(~!GwWjLj(iM zh8hNz4GS1pHXLDK+3<paWg`y*%SHnRmW?3{EE_8rST@dKVA*(pfo0<p29`}63@n>; z7+5w1FtBV^U|`wo!oadQgMnpp4+G2Q4Gb)suQ0G|{=vYqMTUW8ivt78mJ|k-Egew2 zhJj_v83vXuZx~p%@-VP$)nQ=S>chaYHHU#^8wUf+wmA$e+x9T9Y&T(G**=GXW%~;T zmK`PxEIV2lSavcnu<VRsVA;8cfn}Ej1Iw-s2A16{3@p1X7+7{MU|`w3fq`ZB5eAms zPZ(Hs|6pL*!^6O`X9)w#UI_-4y(tVVdp9t!>{DQ1*|&g!W#177mVGZ6SoZTUu<SQr zVA&tSz_P!Bfo1<32A2H?7+4OdFt8k0z`%0Qf`R2=3<Jx-6AUbeOc+=W-C$rjEWyBX zB!GeCNC^YWkr@muNA@tV9C^UNa+HOE<){V&%TXT&mZJp>EJvp>upHgNz;g5s1IsZ6 z29{$g3@pby7+8)aFt8kJVPHA7f`R4O83vYP9~fAUi!iVpw_sp79>c(Lyn%t`_!0(| z<0lwcj=y1GIU&Hna>9gx<wOJn%ZVBWmJ<sYSWX;aU^(%Ef#oC*1ItMR29}d^7+6m3 zVPHA+f`R4q4hEJp0Sqi><}k3Fm0@5xw}gS^+!+Rz^9LAMF2pdfTx?-rx%h;E<&p#g z%cT+qmP;QPSS|}Nuv|7^V7c7Gz;cC!f#r${1IrZ`29_&p7+9`KFtA*0VPLuXfq~^( z2m{NtJq#?@H5gc~PhnuW{)d6(MgjxNjRppm8w(g%ZoFY&xyi%8a?^)_k%56{9m6RG z2}TJ9Q3fW484Rl!x<UKTd2<;!7&sUh82A}>F))KJPGn#J-@yW5GcpJ<#6iWG82A_p zploIaGln`Sn}tD%VGESa$}oZ98kEh(pum_0WwSH5Ft$P291L2Fr=V<31`Wn9P&OBX z6O#;-&COuP1hbci!G>uLRGgP-8q*CZn~%YSHIKoWA)ldup^~A9A(J7UA%mfWL4m=D z!GOV#!IVLP!JQ$WA)g_gA%`KAL4m=OA&()ML61R!!I2?{A%{VMAqcLk7|cs$C}v1y zC}JpMNM%T2&|~mp$Y&^F$Y)Ss2xdrR$YUsG2x3TONM|Tz$YDriC}J>T&|@$FLu&?i z27d;B26qN)xGtC;T{K;orh;68Y;G9X6~zph4EYRsU>{+#ClBg6U4~+Ye1=knB8Fs! zR0b=k|IHZ;7%aizV8vj`V8D>dkj7xiV98+2V9a2_V9t=tV8CF_pukYVP{NSLkjPL9 z_Gtz~CPN-L6g3#i7z`Qo7|a<oN$^o711KaD!R8h*Br>Ehq%!1!!z!CWfgztE4QH(Q zg8d2#?@EROXncWmrh{FSjud~GDqR>rF$YQkpwJ3oNM%T5$Ynql1;wWVLlQ$Jg8~Dl zevm6cp`8v6?GmVdJ#cy|VJKj*V$f&MXDA0lJ%)6oRHes|%#hEJ%b?GY&XCGb!l2Ip zixqtwx^vO|i7=s<p@gB7Aq5=kAq=3H0EML+SUo6R2ZO_}grOW9o}d&B3GEz)OmNJD z(kH~9r3`uCkS_wqLk2iBgBd&-d>9lM{K26M3MYigJ}4&WFeosfr$A6j0AU3NQd|b| z3&>ZX+?fPUVG#F0QZ*<?DKIE8I5Gr-V>g&Vi9vzE3C#Co2xb7eBb>pLA%ww$!Ji?N zA%sDJA)LXHA&4P}!I8m_A%ww`!4<66pCO1rfx(%<pTUp81+2pp%m$h3&)^2m@qP@E z3<?b144z;$AhS~$G8sx3K%oN)r&NXth608na7qG&dp-k5G?O8hp@0Dt!XTG}%8p=$ zRB(7gbb?9-V#8H|A&miIDok%NI4+VIG8xjqF`xi0SMtCmOd>c%f!qno4MhyO44~Ks zrBYDnfm{d*zZ3?L9*`?Reo15~0jC~NN>Bi&Q&34(#E=KiLzU2yLV=+aT-L$zB*;Y| z^A#8nsRBK{q34HUhGcNLTfk7lP|Tpm0CHOngC5wA=?wY|ppf@rV1jI%{6B}m2*N@o zdDbz2t^#FdU}j)pU}a!qU}xZ9;AG%p;AY@q;AP-r;AaqE5M&Ty5M~fz5M>Z!5ND8J zkYtczkY<o!kY$i#kY`X}P-IYIP-akJP-ReKP-oC!&}7hJ&}PtK&}GnL&<EEBMhwOb zCJd$wW(?*G77UgQRt(k*HVn25b`16m4h)VAP7KZrE)1>=ZVc`W9t@rgUJTw0J`BDL zehmH$0Sti*K@7nRAq=4mVGQ965e$(GQ4G-xF$}Q`aSZVc2@HvhEDVboS{Pax_A*Rk zc)`%d(8titFqdH^!xBbThAu`nhGvGzjO>gY484q;3>^%I8SXK1G0bCF#qgEk3&S^t zMGR{hPBI*4SjUjWkjyZFA%)>ILn^}whEojZ8O|`AWjM$1k|B-ZGQ$Oiiwx@-(iuK5 zOk}vmaE0M2Lk7cZhPMoz3|S1B4A~613^@#W3=0_Y84AH^xfq=1${5NSUNKZKR54UC zR5R2v)G+K|SjbS%P{+`~(8%zH;T^*+Ms7wPMqWlfMt(*ChQAE|7zG)H7=;-{82&Rd zFp4sYF^V%XGD<K?GD<N@Gs-Z^GRiT^Gb%7DGAc1DGyGuq$*97p%BaSu&Zxnt$*9Gs z&8Wku%c#ew&uGAC$Y{i9%<zlRgwd4IjM1FYg3*%EiqV?UhS8SMj?tdcfzgrCiQy5$ zV@7927e-e`H%50x4@OT$FGg=hA4XqBKSqDX0LDOu4GbF@gBXJuLl{FD!x+OEBN!tY zqZp$Zelz@Gc*5|MF@`agF^(~wF@Z6WF^MsmF@-UeF^w^uF@rIaF^e&qF^4giF^@5y zv4F9VVK-wDV=-e1V=2QkhUbiB49ghH87mko8LJqp8EY788S5BsFg#?q#c-S94#QoB z2MjkE?laaiHZV3aHZe9cwlKCbwlTIdb})7_b}@D{_AvG`_A&M|PGFqKIEisG;}piJ zjMEsWGtOX~$vBH~Hsc(|xs3A|=QA!~T*$bHaWUf(#-)tQ7?(4yU|h+#ig7jL8pgGZ z>loKFZeZNVxQTHy;}*uPjN2HuGt6e(!MKxg7vpZmJ&b!9_c88gJivI6@et!-#v_bJ z8ILg@XFS1plJOMdX~r{*XBp2io@czkc#-iE<7LJxj8_@2F<xi9!FZGL7UONkJB)W3 z?=jwIe8BjS@e$)=#wUzV8J{seXMDl<lJOPeYsNQ>ZyDb)zGwWv_>u7w<7dV%j9(eQ zF@9(K!T6K$7vpcnKa77F|1th&VqjuqVq#)uVqs!sVq;=w;$Y%r;$q@v;$h-t;$z}x z5?~T!5@Hf&5@8Z$5@Ql)l3<c#l46o(l3|i%l4Fu*0^Loo#H7rm!lcTi#-z@q!KBHg z#iY%!iD5I74wEj!HiqpCN0{^&wlHjEILgq(aEPIuVKu`sCVeIYhJ6hCnG6~BFc~p? zXEJ6oVKQYhV=`y5VCZ48WU^wiX0l<jWwK+kXL4Y2WO8D1W^!S2WpZP3XYye3Wb$J2 zX7XY3W%6V4X9{2nWC~&mW(r{nWeQ^oXNq8oWQt;nW{P2oWr|~pXG&m7WJ+R6W=dg7 zWlCd8XUbs8WXfX7X3Am8Wy)j9XDVPSWGZ4RW-4JSWh!GTXR2VTWU6ASW~yPTWvXMU zXKG+-WNKn+W@=$-Wolz;XX;?;Wa?t-X6j+;W$I(<XPUq?k!cdsWTq)hQ<<hQO=p_H zG?Qr-(`=?WOmms$G0kUMz_gHQ5z}I(B}_}1mN6}7TEVoEX%*9IrZr4!nbt9_XWGED zk!cgtW~MDnTbZ^oZD-oSw3BHU({83cOnaI3G3{qMz;ux55Yu6%BTPq`jxil)I>B_3 z=@ipxrZY@una(ktXS%?2k?9iCWu_}kSDCIcU1z$%bd%{8(`}|ZOm~^?G2Lf+!1R#m z5z}L)CrnS7o-sXVdcpLP=@rv!rZ-G)ncgwIXZpbOk?9lDXQnSqUzxr!eP{Z?^poiq z({H9fOn;gFG5u#|U<O@W$;`~c%*xEh%+Acg%*o8f%+1Wh%*)Kj%+D;qEXXXxEX*vz zEXpj#EY2*!EXgdzEX^##EXyp%EYGaKtjMgytjw&!tjes$tj?^#tjVm!tj(;$tjny& ztj}z~Y{+cHY|L!JY|3oLY|d=KY{_iJY|U)LY|CuNY|re#?8xlI?9A-K?8@xM?9S}L z?8)rK?9J@M?91%O?9Uv)9LOBR9LyZT9LgNV9L^lU9LXHT9L*fV9LpTX9M7D<oXDKS zoXniUoXVWWoX(uVoXMQUoXwoWoXecYoX=dqT*zF+T+Cd;T*_R=T+Up<T*+L;T+Lj= zT+3X?T+iIV+{oO-+|1m<+{)a>+|Jy=+{xU<+|As>+{@g@+|N9Lc_Q;9=E=-cn5QyN zW1h}DgLx+NEautFbC~Bc&tsm?ynuNj^CITO%uAS;GB0CZ&b)$oCG#rg)y!*{*D|kT zUeCONc_Z^C=FQAon71--W8TiZgLx<OF6Q0Ldzkk!?_=K2e1Q2N^C9NL%tx4yG9P0; z&U}LTB=afe)68d>&oZB5KF@rC`6BZr=F7}in6ENlW4_LOgZU=&E#}+IcbM-o-($Yd z{DAo(^CRZR%ukq~GCyN}&isP;CG#uh*UWF2-!i{re$V`Y`6Kfu=FiMun7=ZAWB$(k zgZU@(FXrFOf0+L=|6~5o!ob4F!o<SN!otGJ!p6eR!okAH!o|YP!o$ML!pFkTBETZZ zBE%xhBElldBE}-lBEcfbBE=%jBEurfBF7@nqQIiaqQs)iqQaueqQ;`mqQRocqQ#=k zqQj!gqQ|1oV!&d^V#H$1V!~p|V#Z?5V!>j`V#Q+3V#8v~V#i|7;=tm_;>6<2;=<y} z;>P06;=$s{;>F_4;=|(0;>Y6862KD362ubB62cP762=nF62TJ562%hD62lV962}tH zlE9M4lEjkClERY8lE#wGlEIS6lEsqElEaeAlE;$IQovHkQp8fsQo>ToQpQrwQo&Nm zQpHluQo~ZqQpZxy(!kQl(!|ot(!$cp(#F!x(!tWn(#6uv(!<ir(#O)zGJ$0x%OsY` zEK^vfvP@%{&N72#Cd(|A*(`Hd=CaIVna{F-Wg*KVmc=YfSeCLZV_D9!f@LMkDwfqO zYgpE@tYcZvvVmnI%O;l1EL&K%vTS47&a#7LC(ACD-7I@p_Ok3_+0Sx-<si!;mcuMZ zSdOwBV>!-pg5@O3DVEbLXIRd%oMSo9a)IR{%O#e}ELT{rvRq@i&T@n0Cd)0B+bnlj z?y}rtxzF-|<sr)>md7klSe~*xV|mW<g5@R4E0)(RZ&=>4ykmLK@`2?e%O{r4EMHi@ zvV3Ft&hmrhC(AFE-z<Mv{<8dI`OnJ0%E-#Z%FN2b%F4>d%FfEc%E`*b%FW8d%FD{f z%FimmD#$9tD$FXvD#|LxD$XjwD#<FvD$OdxD$6RzD$lCGs>rIus?4gws>-Uys?Msx zs>!Ows?Dmys>`a!s?Tb`YRGEDYRqcFYRYQHYR+oGYRPKFYRziHYRhWJYR~Gx>d5NE z>dflG>dNZI>dxxH>dETG>dorI>dWfK>dzX$8ps;N8q6BP8p;~R8qONQ8p#^P8qFHR z8p|5T8qb=*n#h{On#`KQn#!8Sn$DWRn#r2Qn$4QSn#-EUn$KFmTF6?&TFhF)TFP3+ zTFzR*TFF|)TFqL+TFY9;TF=_R+Q{0(+RWO*+REC-+Roa++R56*+RfU-+RNI<+Rr+H zbt3B|*2%0>Sf{d1W1Y@AgLNkBEY{hqb6DrH&SRa=x`1^d>mt_0tV>vzvMys?&boqi zCF?5I)vRk+*Rrl-UC+9KbtCI0*3GP2ShuonW8KcWgLNnCF4o<wdsz3f?ql80dVuvH z>mk;|tVdXnvL0hS&U%9NB<m^G)2wG$&$6ClJ<oc9^&;yf*2}C{Sg*2PW4+FLgY_os zE!NwtcUbST-ebMb`hfKz>m%03tWQ{<vOZ&d&iaD&CF?8J*Q{??-?F}Aeb4%V^&{&i z*3Yb8SiiD<WBtzhgY_rtFV^3ze^~#r{$u^m#=yqN#>B?V#=^$R#>U3Z#=*wP#>K|X z#>2+T#>d9bCcq}hCd4MpCc-AlCdMYtCc!4jCdDSrCc`GnCdVevrog7iro^VqroyJm zrpBhuropDkrp2bsro*PorpKnwX2531X2fR9X2NF5X2xdDX2E93X2oXBX2WL7X2)jF z=D_C2=EUaA=ECO6=EmmE=E3I4=EdgC=ELU8=EvsG7QhzB7Q`0J7Qz<F7RDCN7Qq(D z7R46L7Q+_H7RMIPmcW+Cmc*9Kmco|Gmd2LOmcf?Emc^FMmcy3ImdBRQR=`%sR>W4! zR>D@wR>oG&R>4-uR>fA$R>M}yR>xM)*1*=t*2LD#*231x*2dP(*1^`v*2UJ%*2C7z z*2mV*Hi2y-+a$KhY*W~#vQ1-~&NhQ>Cfh8w*=%#z=CaLWo6oj@Z6Vttw#95q*p{*_ zV_VL)f^8++Dz?>ZYuMJZtz%oywt;OU+a|WnY+Km2vTbAA&bEVXC)+Nz-E4c<_Ok6` z+s}4@?I7DBw!>^k*p9LtV>`}vg6$;RDYnyWXV}iNont%Cc7g38+a<QkY**N>vRz}l z&US<CCfhBx+iZ8(?y}uuyU+H3?IGJEw#RHw*q*XIV|&i_g6$>SE4J5cZ`j_ly<>aN z_JQpq+b6cqY+u;EvVCLw&h~@tC)+Q!-)w)_{<8gJ`_In6&dAQh&dkoj&dScl&d$!k z&dJWj&dtul&dbin&d)BuF32v#F3c{%F3K*(F3v8&F3B#%F3m2(F3T>*F3+yOuE?&$ zuFS5&uF9^)uFkH(uF0;&uFbB)uFI~+uFr13Zpd!LZp?1NZpv=PZq9DOZpm)NZq07P zZp&`RZqM$(?#S-M?#%AO?#k}Q?#}MP?#b@O?#=GQ?#u4S?#~{;9>^ZV9?TxX9?BlZ z9?l-Y9?2fX9?c%Z9?Krb9?zb@p2(iWp3I)Yp30uap3a`Zp2?oYp3R=ap39!cp3h#u zUdUd=Ud&#?Udmp^Ud~>@Uddj?Ud>*^Udvv`UeDgZ-pJm>-pt;@-pby_-p<~^-pSs@ z-p$^_-pk&{-p@XPeIolL_Q~v1*r&2jW1r4GgMB9ZEcV&#bJ*vy&tsp@zJPrp`y%$m z>`T~}vM*y_&c1?uCHpG&)$D87*Rro;U(ddQeIxrO_RZ{D*tfE8W8cocgMBCaF81B* zd)W7~?_=N3et`WT`yuwj>_^y-vL9nV&VGXZB>O4$)9h#1&$6FmKhJ)F{UZA%_RH*7 z*sro*W53RRgZ(D^E%w{&ci8W;-($be{($`<`y=+p>`&OAvOi;g&i;b^CHpJ(*X(cD z-?G1Bf6xAb{UiG)_Rs8J*uS!WWB<<ngZ(G_FZSQ;f7t)B|6~8p!N9@D!NkGL!NS4H z!N$SP!NI}F!NtMN!NbAJ!N<YRA;2NXA;clfA;KZbA;uxjA;BTZA;lrhA;TfdA;%%l zp}?WYp~Rugp~9icp~j)kp~0cap~a!ip~Ioep~s=mVZdR?VZ>p~VZvd`Va8$3VZmX^ zVZ~w1VZ&j|VaH+5;lSa@;lyyA!<oZ{!<EC0!=1x}!;`~{!<)m0!<WO4!=EF7BakDA zBbXzEBa|bIBb+0GBa$PEBbp<IBbFnMBc3CHBatJCBbg(GBb6hKBb_6IBa<VGBby_K zBbOtOBcG#yqmZMBqnM+Fqm-kJqnx9HqmrYFqne|Jqn4wNqn@LIqmiSDqnV?Hqm`qL zqn)FJqm!eHqno3LqnD$Pqn~2}$3%`v9FsYwa7^Wx#xb2^2FFZ}Ssb%D=5WmAn8z`n zV*$rPjzt`cIhJrN<ygkCoMQ#YN{&?=t2x$itmRn8v7TcC$3~7#9Gf||aBSt+#<87a z2ggp1T^zeP_HgXw*vGM-;{eA&jzb)WIgW4~<v7N1oZ|$?Nsdz-r#a4WoaH#jah~G> z$3>1y9G5w+a9riM#&MnF2FFc~TO7AJ?r_}YxW{py;{nG*jz=7iIi7Gl<#@*NoZ|(@ zOO96@uQ}duyybYu@t)%Y$48D&9G^M9aD3(X#_^rw2ggs2UmU+V{&4)|_{Z^|lYx_w zlZlg=lZBI&lZ}&|lY^6!lZ%s^lZTU+laG_1Q-D*DQ;1WTQ-o8LQ;bubQ-V{HQ;JiX zQ-)KPQ;t)fQ-M>FQ;AcVQ-xENQ;k!dQ-f2JQ;SoZQ-@QRQ;$=h(}2^E(}>fU(}dHM z(~Q%c(}L5I(~8rY(}vTQ(~i@g(}B~G(}~lW(}mNO(~Z-e(}UBK(~Hxa(}&ZS(~r}i zGk`OYGl(;oGlVmgGmJBwGlDacGm0~sGlnykGmbN!Gl4UaGl?^qGlesiGmSHyGlMge zGmA5uGlw&mGmkT$vw*XZvxu{pvxKvhvy8Kxvx2jdvx>8tvxc*lvyQW#vw^dbvx&2r zvxT#jvyHQzvxBpfvx~Evvxl>nvyZc%a{}i?&Pkk;Ij3+=<($SjopT1~OwL)HvpMH* z&gGoPIiGU@=R(d!oQpY^a4zLs#<`qx1?Ni6Rh+9i*Kn@oT*tYda|7o_&P|+~Ik#|b z<=n=(opT50PR?DNyE*r8?&aLaxu5d@=RwXxoQFA&a31A6#(A9c1m{W4Q=F$c&v2gQ zJjZ#S^8)8Z&P$w^Ij?YD<-Epuo%06gP0m}Kw>j@{-sQZ<d7twE=R?j%oR2x5a6aXH z#`&D{1?Nl7SDddo-*CR=e8>5o^8@Ec&QF}5Ilpjz<^0C^o%09hPtISQzd8SK{^k6~ z`Jan{i;;_oi<ygsi<OIwi=B&ui<66si<^swi<gU!i=Rt?OOQ*5OPEW9OO#8DOPouB zOOi{9OPWiDOO{KHOP))COOZ>7OPNcBOO;EFOPx!DOOs2BOPfoFOP5QJOP|Yt%aF^6 z%b3fA%aqHE%bd%C%aY5A%bLrE%a+TI%bv@D%aO~8%bClC%azNG%bm-E%ahBC%bUxG z%a_ZK%bzQNE08OQE0`;UE0imYE1WBWE0QaUE1D~YE0!ycE1oNXE0HUSE14^WE0rsa zE1fHYE0ZgWE1N5aE0-&eE1#=?tB|XRtC*{VtCXvZtDLKXtCFjVtD38ZtCp*dtDdWY ztC6dTtC_2XtCg#btDUQZtCOpXtDCEbtCy>ftDkEE*F>&KT$8z`a82c!#x<R52G>lk zSzNQZ=5WpBn#VPtYXR3nu0>pnxt4G(<yywIoNEQwO0HF0tGU*2t>s$BwVrDO*G8^Q zT${PJaBbz<#<iVm2iH!nU0l1l_Hgax+Q+q@>j2k5u0vdhxsGrh<vPZ7oa+SFNv=~| zr@78>o#i^mb)M@2*F~;NT$j17a9!oP#&w<R2G>olTU@uf?r`1Zy2o{&>jBq8u18#t zxt?%6<$A{Toa+VGORiU3uesiEz2$nx^`7ek*GH~TT%WnVaDC<a#`T@+2iH%oUtGVr z{&4-}`p5O3n}M5=n~9s5n}wT|n~j^Dn}eH^n~R&9n}?g1n~$5HTYy`TTZmhjTZCJb zTZ~(rTY_7XTZ&tnTZUVfTaH_vTY+1VTZvnlTZLPdTa8<tTZ3DZTZ>zpTZdbhTaR0x z+ko4U+lbqk+l1Sc+l<?s+k)GY+lt$o+lJeg+m73w+kxAW+lkwm+lAYe+l||u+k@Ma z+l$+q+lSki+mG9yJAgZoJBT}&JA^xwJB&M=JAylsJBmA+JBB-!JB~Y^JApfqJBd4) zJB2%yJB>S?JA*ruJBvG;JBK@$JC8e`yMVipyNJ7(yM()xyNtV>yMnutyNbJ-yN0`# zyN<h_yMeoryNSD*yM?=zyN$b@yMw!vyNkP<yNA1%yN|n{djj`F?n&H}xu<YX<(|ep zoqGoNOzv6Sv$^MR&*h%SJ)e64_d@PP+>5!Fa4+Rv#=V?-1@}tsRott&*Kn`pUdO$j zdjt1I?oHgAxwmj{<=)1<oqGrOPVQaYySevp@8#ady`TF4_d)JM+=sc3a3AG9#(kXo z1ouhqQ{1Py&v2jRKF58Y`vUhx?n~U4xvy|v<-W#!o%;s&P3~LVx4G|d-{ro?eV_XQ z_e1VS+>g1Ra6jdK#{Hc81@}wtSKP0;-*CU>e#iZu`vdn!?oZsGxxa9K<^IO~o%;v( zPwrpbzq$W#|K<M2{htSPh%^%qGY<<7D-RnFI}ZmBCl417HxCaFFApCNKaT*9Ade7_ zFpmh2D32JAIFAI6B##u2G>;6AERP(IJdXm8B99V}GLH(6DvuhEI*$gACXW`6HjfUE zE{`6MK92#9A&(J{F^>t4DUTVCIgbU8C65)4HIEICEsq_KJ&yyABaai0Gmi_8D~}tG zJC6sCCyy78H;)gGFOMIOKTiNpAWslaFi!|iC{GwqI8OvmBu^AiG*1jqEKeLyJWm2o zB2N-eGEWLmDo+|uI!^{qCQlYmHct*uE>9j$K2HHpAx{xcF;59kDNh+sIZp*oB~KMk zHBSvsEl(X!Jx>EqBTo}gGfxXoD^D9wJ5L8sCr=koH%|{wFHav&KhFf7i9C~dCi6_; znaVSbXFAUeo|!zecxLm=;hD=bk7qv50-l9Di+C3EEa6$ovy5js&kCNEJgazC^Q_@n z%d?JWJ<kT7jXaxpHuG%Z*~+txXFJaho}E0qcy{yb;n~Zxk7qy60iJ_Ahj<S29N{_2 z;uq@U!{i*qUYeI_VBlx~rCm7l%TkMqlk<yGAtFvjP}&$un?h+zcE{Yr<f8mM_CyHH z=9pBJT9(R|2&UK_^V9S5QnT3;AvCvha%NF-X>M9hY6W*Pg3aQbl3&7-oRVL{=8~LP zl%L0z0-;=zi}Djo*j*upv!_C6HdnCeY^h+1%QYpxBr!QTHLrv#70zULg;>d+3ZdCt zAtt7RDQ<U!<=p8AHj6vRA?YdkC14L48bLg6Xk^Uofl$Srfnc+EBqbKHWF#dPv3Ztc z=A@*uWr8VoPl#LDGa)paC)lxUnP7_D6XGiNObE^7>BW?p#qEVKmpco=hPcJp3`(2x zWTzLUrsm}&=A~pNv-u<@mzJcm<$x)u5+^9_%;p33FIx_nV(|faF9#IPeqiNnd0+~n z+5{4-h9+igeqiNnd0>h=FR>uMxTGk*AS0F8H7}hxH7}jrAL1?cd<f0v5B3*ZKA7V5 z&nV4HPb@0U%}FdR;mt?pu?2yxWGezw5SvY*UNbdk3j$loRs^Af!HU?5!4!8eB1pK4 z5p1v-Muv`1+KDX$qPYZ2v4nsks00*2q2QQdD+N>Rp%9m|mqKW^P_Wb4O2HIsXi;Wf zI%_G2<PAl3KW`~Ak3AF;f$XIanmZifF79$9b|ezJ62WGR0(*_E3QTcD!P75S6`aYM zlb@Gf%;gAj3q)BWoXPI!3Kn8dOa;^2{>3?o#TgKGK7tJm8w)6H$y$=1mtV}Al9^hR zTAW!7=5aaZ6l5fVnVgAv`6a12shNp9t_8)JIr({DVGh?4sJv%Bl*Qwpn+Yla!16qy zNQ$}qb5qkH$^|p?OA=A+Vg=j9;*?sF$m*V$o0|xBn6ae^yDP*9mZ;Q{L}u5LMAndu z)RIINkcF(?i3J6TY(9y(Nhyg;zNJilrA!f->`)gl1!OV@W#qF4XQt;SGKVCVvO`_N z9FUR8?3|Iw98jDIvBuGo2V@@1Mf^~OFg{lzJRo>dA<hANk0TZ8X-<%Tz(OLW5P6tZ zuplc~KT8tGbLk+@v8JTvlq9l4&0?tnc?~340`?lnFxG6a*K)vKgE-R3nJKT7sUnj- z9}>dMMH%^Q#URfavVtk*lEhMWsQt_Z8JWz<8JWxl#hI-6pn&HA84Gh3KU5iv&jU3R z>|7qGB$&em)(mEHCZ=U(8X6cF8N*nnhH#b<oQ04zfs2{KS!QsSIh<tyXIa8nW(IH; z+zvCC9fl@w^9<pp7{X04gqvanH^m5UiV@5d10%TmjNs-O!QE#BH`xeovJu>5W4Ou2 zaFdPUCL6=Uz!+|aG29MgxE;oDJB;CW7{l!_f!kpMx5ET(hY8#c6Sy6Sa4~_~VFI_q z1a5~3+zvCitIXg!%-}lA;5y9UI?Um&GKagx9PSo#xLeHOCY!@eHiw&R4ma5xZn6d3 zWDB@|E#P)o!0oVr+hGB>!vb!H1>6n`xE&U7J1pULSi<eFgxg^Wx5E-{hb7z&OSm1D za68NqroiklG=SM*XaJKnG=SM*XaKXr&;VwKp#jVeLj#x{h6XS@3=LrZFf@SMVF<Sa z;bv2qn1Q7QTX=d=Vp%F%IfM!aR{(6~V2U#wmhCyqVJx;th{8%R1<rd021W)Jyrp?5 z(57EXQVwrnX?{s6s6kg$oSMRxo0$h{X%wd>=jWwxrdA~9B<7|h<#6T|mlmWJW#$(_ z%(1XEF=Q)9Ed~|auBAo!U{NzeBWQXwGc<z6kC~ydDY$`UWN2i@nOa<unVVRWn!=f% zmztWHo>7v)Q<71X3S}2_r)8GG*gVClWvO`(Ma8_yl|`93Iho1enp*%|>nG>u<|aZK zh1^JNK_qq&s0sr&aE%PiO+aO?k%2k531nnoZVu&xD@-E;0|N-(zz|A9oNZtLZV($8 z7(n$KK=m7d8#G1+22gVhVCGmt^@AHLMg|7phMJqJn;S^n&CSgjO1pq)14B2ky#_{3 zU~vOu7qI;X#x7v{4UApD_87VvyMojkx*CJcF?5Bv*U%N>UIQaHV^;r+{GvS8d@vab zCQCsiOGbWvHb}F92}F~Di2=lN6H|!gCZ<rn8I*4ZafFE(#1STD;6N}iF#`vJfr%M7 z5DZMrz>YI8F#`vJfr%M75DZMrz=2?3Vg?Qb0~0fFAQ+gKK|E<<2D1<BDFYKTu%`@6 z%%S$1L+v+*+HVfE9}<ZMCXh%pFfoVP4~a|z6LYBj=1}|1q4t|Y?Kg+oZw|HJ9BRKg z)P4)7{T5LBEsP-HW&t(V0&1=W)LaXwxfW1!EuiLFK+Uy)nrjI)#}eufOK3P+Lc`G# z>JLk3I9fvOvxM4b3AN7>YM&+4K1-;5mQedFq4q)hc_xlfe>g(zcZAyS2({l4YQH1Y zen+VNj!^p@q4qmM?RSLQ?+CTu5o*69)P6^({Z7zubb{Is?OmEULG5>f+V2Fl-wA5J z6V!eusQpe*`<<ZnJ3;M-W)>4CsQpe*`<<ZnJ45YvhT888^}jRJerKrt&QSZEq4qmN z?RSRS?+mry8EU^X)P85E|DB=sJ45YvhT87}wI7<9O`w_C#06@<3)FrWsQr*kZeZd9 zwciD5zYElU7pVO%Q2Sk=_PapscY)gP0=3^2YQHPg|E^H~yF%@Eh1%~5wci!$e^;pe zu2B13q4v8%?RSOR?+Ufw6>7gL)P7f}{m??q#0_e{8`OR`sQqqG``w`SyFu-DgWB%~ zwcibDzZ=wkH>mxPLe#*-4Qjs|)P6`IYhVg3=S-o6qp1PJep5(cZeR*2%neK-g}H$# zq%b!yg%sunrjWwiz|;U@zo`Mlep5(cZeVHv@xLjw95pq7*l%h8@xQ4dB>YVcA^tZt zgxYTi_P?PkB!3#ZLh`4fD<pp!x<c}&p(`YR8oEOAr=cq(e;T?%@~5FIB!3#ZLh`4f zD<pp!x<c}&p(`YR8oEOAr=cq(e;T?%@~5FIB!3#ZLh`4fD<pp!x<c}&p(`YR8oEOA zr=cq(e;T?%@~5FIB!3#ZLh`4fD<pp!x<c}&p(`YR8oEOAr=cq(e;T?%@~5FIB!3#Z zLh`4fD<pp!x<c}&p(`YR8oEOAr=cq(e;T?%@~5FIB!3#ZLh`4ft1~!$3|%4l)6f-? zKMh?W`P0x9l0OYyA^FqL6_P&<T_O3?&=rzD4P7Dm)6f-?KMh?W`P0x9l0OYyA^FqL z6_P&<T_O3?&=rzD4P7Dm)6f-?KMh?W`P0x9l0OYyA^FqL6_P&<T_O3?&=rzD4P7Dm z)6f-?KMh?W`P0x9l0OYyA^FqL6_P&<T_O3?&=rzD4P7Dm)6f-?KMh?W`P0x9l0OYy zA^FqL6_P&<T_O3?&=ry&4P7Dm(a;r=9}Qh0`O(l7k{=CSA^FkBz|lz<F~9+CfhOnY zCLxJ|+ngXVA%r?mGXx}rqzpVv0}|r{H|*dhLAY>3c)_wT{SZmGRsmSs5G0?InpPqR z6G+NT2Z@D3GOHo9F=GgA%osu|dqZetZwRgI4K2W#)WFaJQb-wEKnf{C3rHblXaOmt z455tzLujMG5ZWj(gf<Ecp^XAVXrsUo+9)uDHVO<aA%%pYC8Urrgf<Qgp^XDWXyd>T z+Bh(THVzD-jRQky<G>KwI531Z4h*4<14C%zz!2IvFoZS^3?YS$fgz-@F))M_HU@@} z!p6W5QrH+6LJAuLLr7s`U<fH}3=AQKje#MgurV-%6gCEikiy2m5K`C}7(xmc14BsR zVqgd<Tnr2$g^Ph9q;N4XgcL3YhLFO=zz|Zn7#Knd7Xw2`;bLG2DO?N;A%%;9A*66I zFoYB?28NKr#lR3!xEL5h3Ks)INa12&2q|0)3?YS!fgz-DF))M_E(V5>!o|Q4Qn(lx zLJAiHLrCFbU<fH(3=AQKi-94ea4|516fOpakix~l5K_1p7(xmc149=`;co~jTnr2$ zg^Ph9q;N4XgcL3YhLFO=zz|Zn7#Knd7Xw2`;bLG2DO?N;A%%;9A*4_-FoYB;28NJA z#lR3!s2CVR3KauGNTFh22q{zy3?YSzfgz+&F))M_Dh7s-LdC!kQm7ahLJAcFLr9@w zU<fHx3=AQKih&`dP%$us6e<RWkix{k5L($ALMwYiNa12&2q|0)3?YS!fgz-DF))M_ zE(V5>!o|P{T6r5mD{muc<!uD5yp5ohw-L1RHiA~(M$pRJ2wHg?K`U<~Xyt7Lt-Ot( zmA4VJ@-~82-bT>M+Xz~D8$l~?BWUGq1gX3Yj0_>=f{~#qxN~4+2q_ng3?b!$ks+kK zF*1ZSPK*p8jRPY?NaMiB2$JuOj3D{m2s#5{WCSU9jEtb>8$r!Cf|_pxHQxx*I5IMV zG)|0+z?GkYkrAYEWMl-j-w0~I5!8MosQt!J`;DRY8$;5kkulVMW2pVcQ2U{?5=O>Q zdyS#?8bj?hhT3ZkwbvMGFQmt6U<B#08W@>C?KOegYXY^`1ZuAd)L!T$g^>x=-zHFh zn?UV1f!c2ZwciA4zX{ZS6R7<rP=A|1{SEEx8ks`vhYU;@7@0!tH-*}73iUU1%EHJL zYQHJeep9IZrcirLq4t<U?SXcpji8-qBWS&21g&?Bpq*$VXeZhT+KD!TcA|~UEWm@$ zK8Yond5{za?M53xyU|9_ZnP1!8*O9;bpf;+Z3OK`8$r9#M$m4w5wsg^1nou}LA%jL z=4RaBG2!H#{NmIUh@H@`v=Ou`Z3OK~8$r9$M$oRb5wt691no*2LA%mM(Aw1qTDuxS zYgZ#^?P>(AU5%i%s}ZzzHG<ZzM$p>T2wJ-uL2FkdXzgkQtzC_vwW|@db~S?5u13(> z)d*U<8bQ0@M$lT;2wKY;S(@^u=j4}^B<7Tq7UjWw53OsBpmnVgw5~OR*0n~^y4DC< z*BU|VS|ey(YXq%pjUWwu10zU7-@wSxh&81uCo>%q#*QYOU|w=*Q4VWKF+$v&vm`ku zGaV!jRS8XV(7M+MTK5`3>s}*h-D?D`dyOCseFGy%L*Kv%S{EBZ>tZ8lU2Fuci;bXl zu@ST`HiFj0M$o$02wE2#LF-~8XkBast%HrAb+8e%4mN_;!A8(J*a%t&8$s(}BWN9L z1g(ROpmnYhw5~OR*0n~^y4DC<*BU|VS|ey(YXq%pji7a{5wxx~g4VT0(7M(LTGtvu z>sljdU26obJB^@qrxCR7G=kQhM$o#`2wHa<LF-N<Xx(W9tvijNb*B-u?lgkdokp(E zG6Gt68bRw$BWT@e1g$%bAPsT@BWN9J1g%4jpmnDawC*&5)}2Psy3+_+cN)R!PH0Ej z2-;CLf_9XRpdDo+Xh+$|&5ak-G)_#)FG~frzrm@@7+NkEL(2tYXr4EQ=6Pdio;QZ( zd1GjvH-^T7F*MYTT})UCGBOPzhC+kd7}~5chBj-Aq0Jg&XtTx`+N?2#^qCBdq0Jj( zXcQSkqsSN<MaIw|GlmA4F*L}Gq0J6sXwVr$gU%QlbjHx2GlmA8F*N9mp+RR14LW0J z&>2I6&KMeW#;`F4=xns9AtZ`Tq1C=Aq}n%hfiz<bT_DXELl;Oh#?S@Qj4^b9G-C{1 zAk7#<7f3V4&;`<rF?4}6V+>s&%@{)$NHfOJ1=5T$bb&Nu3|%127(*9GGse&b(u^^5 zfiz<bT_DXELl;Oh#?S@Qj4^b9G-C{1Ak7#<7f3V4&;`<rF?2BmS9gXkhTx*d&;{aO zLl=mD4P7AqHFSab7t+izbb&N;3|%12977jKGsn;c(#$b*fi!arT_DXILl;Oh$Iu1R z%rSI<G;<7HAk7>@7f3V5&;`=WF?4}6a|~S|%^X7)NHfRK1=7qhbb&N;3|%12977jK zGsn;c(#$b*fi!arT_DXILl;Oh$Iu1R%rSH^23PlnE|BJqp$nwBW9R~D?ijj2nmdLr zkmin|3#7SY=mKf(7`i~3JBBWh=8mBYq`7100%`6Tx<HychAxoij-d;rxnt-8Y3>-h zK$<&-E|BJqp$nwBW9R~D?ijj2nmdLrkmin|3#7SY=mKf(7`i~3JBBWh=8mBYq`710 z0%`6Tx<HychAxoij-d;rxnt-8Y3>-hK$<&-E|BJqp$nwBW9VWEZf+U6K$<;<E~en- zmZ6I&xVdHM0%-;rx<HyihAxn1kf95t8D!`JX$Bd(K$<~@E|6xBp$nuLWat8E1{u0Q znn8vxkhugy7sy<Kp$nuDWat8E1R1(O8bO9GkVcT93#1Wb=mKd38M;6kL541nMv$Qk zq!DE30%-&px<Cp$Ll;P4XXpZ%OE7eS%q1AQKpH@XE|3P0p$nuiG<1O!hK4SX!qCtK zQWzS#Kng=c7f1uh&;`-}GIW77fDBzA4Io1oNCU{w1=0XAbb&O03|$}%AVU{O1IW+? z(f~4afi!>&T_6o0Ll;N`$j}AS05WueG=L0UAPpcx7f1uh&;`-}GIW77fDBzA4Io1o zNCU{w1=0XAbb$;r7`i})84O(@O&>!SNYlsA1=92}bb&N|3|$~iA43;N)5p*S()2NO zfi!&#T_8;#Ll;QX$Iu1R^f7dS3_BRQK!zO*T_D2_hAxm{2SXRgu!ErsWZ1#b1v2bl z=mHsbFm!=5eGFY7O&>!SNYlsA1=92}bb&N|3|$~iA43;N)5p*S()2NOfi!&#T_8;# zLl;QX$Iu1R^f7dSG<^(RAWa`b7f92`&;`=;F?4}6eGFY7O&>!S$S{PV3#18T=mKd1 z8M;84K!&c+^4=9%-n&A}dsk?A?+PvNU7_W@E3~|Kg_ifO(DL3DTHd=t%X?R7dG87> z?_Htgy(_f5cZHVsuF&${6<XfALd$zsXnF4nE$>~S<-IGkymy6`_pZ?L-W6KjyF$x* zS7>?f3N7zlq2;|Rw7hqPmiMmE^4=9%-n&A}dsk?A?+PvNU7_W@E3~|Kg_ifO(DL3D zTHd=t%X?R7dG87>?_Htgy(_f5cZHVsuF&${6<XfALd$zsXnF4nE$>~S<-IGkymy6` z_pZ?L-W6KjyF$x*S7>?f3N7DVq2;?Pw0w7kmhZ06^4%3$zPm!pcUNfn?g}m6U7_W> zE3|xfg_iHG(DL0CTE4qN%Xe33`R)oW-(8{QyDPMOcZHVkuF&$`6<WT#Ld$noX!-66 zE#F<C<-04ie0PPG@2=4D-4$BCyF$x%S7`a}3N7DVq2;?Pw0w7kmhZ06^4%3$zPm!p zcUNfn?g}m6U7_W>E3|xfg_iHG(DL0CTE4qN%Xe33`R)oW-(8{QyDPMOcZHVkuF&$` z6<WT#Ld$noX!-66E#F<C<-04ie0PPG@2=4D-4$BCyF$x%S7`a}3N5c)q2;wJw7hnO zme;P(^4b+zUb{leYgcG_?FucgU7_W*E3~|Jg_hT@(DK?9T3)+C%WGF?dF=`<uU(<# zwJWr|c7>MLuF&$@6<S`qLEEEl(DtYsw0v@dmQQZb^2rTaKDj~5CpT#M<OVID+@R%? z8?=0KgO*Qj(DKO*T0Xf!%O^Kz`Q!#IpWLA3lN+>ra)XvnZqV|{4O%|ALCYgIXnEuY zEsxxw<&hh-JaU7UM{dya#|>KkxIxPwH)#3e1}%TwpyiJnwES^{mOpOL^2ZHY{<uNQ zA2(?E;|48%+@R%;8?^j!gO)#T(DKI(TK>2}%O5vr`QrvHf83zuj~lf7af6mWZqV|_ z4O;%VLCYUEX!+v?Eq~mg<&PV*z3B#RZ@NLtBR6Py<OVH|+@R%=8?-!fgO*2b(DKI( zTK>2}%O5vr`QrvHf83zuj~leSaf6mOZqV|^4O-r~LCYIAXnErXEpObQ<&7J(oN<Ge zFK*Cs#SL1XxIxPkH)wg{1}#tApyi1hv^;TxmM3n|^27~Vp148F6E|pi;sz~G+@R%& z8?-!egO(?5(DK9$TAsK;%M&+fdEy2wPu!s8i5s*$af6m8ZqV|?4O*VKLCX_2XnEoW zEl=E_<%t`#JaL1TCvMR8p&PV)=mu>cx<T8AZqW9j8?=4s1}$&gpyiDlw7hYHmN#zD z^2QBX-nc=_8#icq;|48n+@R%+8??M}gO)dL(DKF&THd%p%NsXndE*8xZ``2ejT^MQ zaf6mOZqV|^4OZSj+lOw@_MscJedq>lAG$%?hi=gJp&PV)=mu>cx<T8AZqW9j8?=4s z25ld@LEDFJ(DtDlw0-CXZ6CTp+lOw@_MscJedq>lAG$%?hi=gJp&PV)=mu>cx<T8A zZqRn28?;^M25lF*LED9H&~~94v|Z>1Z5O&h+l6k>cA*=zUFZfG8Z&f*42>DOL59W* z-5^6_hHj9dF+(@Vw3MM6WLnD54KhS#=mwdVGIWCsl^ME0hRO`xAVXz_ZjhleLpR7! znV}nGsLap}GDu_S1{o?dbb}0)8M;9RZ4BKYLt}<+kntKrH^_L6p&Mkp#?TEiUSsG6 z8Lu&PgN)M{x<STi4Ba5(G=^@FaT-H6$T*Fm8)PWW&<!$_X6ObPN;7nW45b;mL59){ z-5^70hHj9dG($JYP@16|WGKzh4KkEw=mr@|GjxLtr5U<GhSCh(AVX<}Zjf;xLpR7c zkf9r79LUfOGX7)e1{wb`bc2ll7`j2me+=Cq<3EOOkntZw=t4b1H^}&pp&MlU$H)Lu z&lwp&>Nz6=NIhp{0IBDU3?TKIkpZNAZDasx9~&7!>JuXaNPS{t0I5%m3?TK1kpZMW zF*1PECq@R4`ohQnQqCJ0K-ynM29R>i$N*BV85uyzH6sH^xn^VlDc6h)Amy5o0i;|r zGJup<Mh1}b$;beb|BVbF`QOL@lK+hiAo<_O0FwWWpnHal3?TW`$N-X0jSL|9(#Qak zFO3W!`O(M#k{^u>Ao<S70Fv*F3?TW=$N-Y>j0_<8&d30g?~Du}`Oe4ylJATRApJ5U z14uqIGJuTt85u(QD@KNpddbKT(k?bKgw#VuhLC#52s(Xl<P51-jGQ6$ijgy<UNLfp z)GJ2Lkb1?)8B(tpIYa6dBWFmxV&n{|SB#t?^@@=*H2j^R;qMF$e`jd;J45OZBWFnc zVdM;{Ka89q^@ou&r2a5+hQ^08r2a61_QM>_A^vkThqQkj%^~d{M{`K~$I%?p{&6&i zw0|7UA?+VWb4Y*8(HxTA9L*u^AxCpad&tooQeQZlL+T4hb4Y#RXb!0_9L+79;d@2E z>oDLv@G=b0SgsqikLL#M<GDfmcy6vn#=@ZeWyVg12F3;^2F8ZQ2F3;kSi}s`#7wZL z1BoH4Gs9vYOblHeNH4OzAax)ym^w==?n4$sR|nDyGY_T?B!;XGWCyZ;LF!;)=;}av zk<A0C1BpS^Ih$g-4@C@J9Y`<KJh(cL7_vH$9Z2p2tAmN5s{`ppHV>o@BnDOI1oE2& z#EtOyMHWL>XNDBNFm)g?s5-D)(9A;-LstjVi|js-UXYj}hihKCUSduOoM8kLG3GAK z%LG-621d@7P_`?A?c##O28(lmw`G-<q;i1QnWyBJ@Im&E!B*Dug4Z6wcwAr}ILmND z)PXY$lnoY#XC<(Kd@!95)!bnH5H@E@ehGxd3uoyiCl>I7bC&@qcY%Cs0p%MQ7#kq- zLGqy3LY4>fA>MXz1*wO!T@Y+%Bynf3I4HLpm_mAF2Bwf6nSm*!Ml&#llu!nykP^zk z6jDMNm_kY*15-!|WMB#@fecI`C6IwBq(^383h9v<m_mAF2Bwf6nSm*!M`mDZWX=y; z!W9n+k0S8ubt404OUu>G5#&B2XGl$F=xXW89s-`b1#dAkbcM{^7`j4gY)5m5y^iJ( z`y9<7_BfhDdVG%Nkeb=i98x1YnnP-0M{`II&e0rF6FZtidTfs7ke-^OIi!c?Xb$O_ zIhsRiYDaTOPt4IA(gSlehxEK0%^@Q|j^>b_m!mnP$K_}a>1jEdLuxWdb4X9i(Hzpl zax{n3XpZKP9+jgxWaP%t95Q0#Xbu^vaWsdt5FE`RHJ+n6q-Jw8htz0}=8&4q(Hv5P zIhsRiE=O}nI69g`S_Y2hkRFSpIi#oJXbu_qa5RVXSRBnEJrzfDNDsx)9MUs!G>7yw z9L*su3rBNEi^9<y($aS{hqN3V%^@v&M{`Ka!O<Mja&R<<wBQ}hAuV`Eb4Ux`(Hzo} za5RUs930IdEeA()NXx;|9MW=dG>5bt9L=4~_@TR!^>Xr)bNn2`_@P^c;e6=kTsR-P z#}mdE@j==}0v7aj^bqmGA_z@+sREFlbcP1TDfuNisl~-`0aK^|7kGzY4s2D2ku#*j zXkY}1J_932huFXn(y%r#GIivG?j44j07~@+M$qvBS2rgR-_RA>&vJ$Ivy7b09l452 zlZumzG7CzwKuV09%`N#r1&EP}CDbGc*U|-r>tuw?HFksf1f&==eS;(mnk_)#I+~zz zEs(jcM#vU{R_Q`zL9TN&hZF~n=8$5*(Ht@$<7f^Uh;cNBG?*RDAq{0mb4UZ((Ht`1 z;%E*TaB(z;4750!Lk3tJ%^?BhXbu@raWsbvq&S*G22dQ$Ap<9l=8ypsM{~$PiK97W zfW*-pGEnPi4jCYEG=~g~IGRHmo{r{_fmla#$bg8WIivyVXbu?waWsbvd^nmz20k3k zAp;+d=8$-HG=~g)IGRHmrjF*22B@Psq~Ymk4ry>YnnN0zj^>aCrlUEeVee=TX=pl{ zLmHZn=8%S_qdBCZ>1YmVXgZoh8k&ygkcOtCIV9aVnnMOC9L*sE6OQJPflWtq$bf{S zIb<Nh(Ht@W;b;yScyKg_3?w+3LplzQ=8z79qdBC5;Ajr%z&n~lItY&DkPf?}IizFY zXbx%EJDNi})Q;wm;@r>`+R<`#bK{2|CIHTPpzI6X;Sb}B<Y38(B6(N@`Jp)orjsAK zi5tdePK!@tO^Z*9&jgb>%xUqN%xUpC?BEUA@g>Z~@g*#Ydd10{iQt+X%mS}Q)+<it zOGGSg)+<itf$x6?s{*$I^oo<YpywulnUF2ydd0~c;0@k-#mT&22EuS~Ye26!nKdyL ztOK&?Tdz2oB?&|%gUn8bD^CWuDD;Yxxs#JqQ!;aMKyKwjI(I>@IGG)E>H<h_ie7Ot zTWU!L$Oy>k3lJ9Q00xje$U;uIeVnN<e{w=jQ~>!n71hV7u%-;iBZ!k3K*pxR{SQ6T z0puH4JQgRjq=9Tq2dPYl*~5o)OoLu=GJiU9gcc{WgAQ!~=>mCx9dsT8$VEs8HGt$Y zK_+Ix!v}H}14vyas>?HxU7nc&J6-~;8|HG>%q)<jp~pLbneh1Igq#5ZvJ#|-59uri zkXSaz{n@DQXUPGXo&$Fq<iG}yLX_lOoXie7z5%Qjyt^M{UM`5p1BvCq^+S$s0O?0b z`Jgz?0|#s#EG@C*gLLM@bwZAC0EJ3EYN+HRg$idrEF1*#k<UW_g#+|R2(YOzH}WB! zMF5t7+0L2|N?0rfpj2N1+K>qHOCg9T0;z+X5&>qx5;K1hTF4cHq#;K{fLSnw+{H+# z9_hRYP@oi}n!%f%T9liZmy(nNGK{4Jlrca@Mu5zN93uhBMI{LH_)Aa?U?~MDErkan z<P-@|AeN#AVkvSUmcj!O<xmMwAVN>205jp~iUWKe1=uE7__36M!m1o32HEWkvJ>gx z2~g1k-}MVt1+$Z-0;IDNBvuJ?Hy_f`6d;|I$gYL#6$WXl0%?LASpjCjLYx<IXay() zOY<^~3}6)zWMtXV95TY@Xbx$iIGRJ+<BsN#0^ZRa)-H#wq=K}=4NM{Ja063FJKVq& z+735^wx`XY?P)V;d)f@zo;HKFr_G@4X)|bh+6>yBHiNdO&7kdRGiZC-4BDPHgSMy5 zpzUchNQ2DL4BDPHgSMy5AR|YPX3%!E8MIw(25ncHLEF`4&~~*Mw7qNwZ7-Wa+skIq z_OcmtM8?bl+@EnYgN)cann5dAGiW>74BC!1gN(>Inn6b59nByk@Q!AdVEqQBkOq^1 zDWt(<U<zq48JI#EOoq-7_ZT`uDtJR@h`S7(Ar-x$GsJy{&X5Y<&>7-RLuW|EZ|Dqj zFSNmA25m5zK^shF&<2wkw83NsZ7`WZ8%$=<29p`I!DI$)FquIcOlHsqlNq$ZWCm?8 znL!&&X3z$cnHhLxp`#gO1kce7+Hf+1Hk{0$4JR{b!^sTVa597V+tCc-Uq>^DKOM~= z{&O^gj*OW>N5ssa{TVZ8x;BG0c+8*;9y7?yh@%-~M#Rw!G85uxX6Ymd=?xf|nSmP` z!9n1*x}zDir)36hYMEI&gPXu+mM$RLz!)-CYhVm%(is>-nr;ThkTHA%V~9QjV@QrO zForbw42;dqLA`ndV;2L^7zl@RT2W$lNof&>2aMqfV+6q%QBVfRVn=gm(l&<#sgWTh z3mO?hCU=btA(OX8hK5Fb0huoOrAaxd!6ikd$&jA8p`jDB9|T?kZ)9lX#OsW#iq`{$ z=ZC@zK;Z?V@PbfyQOG=D&yvKP%w!j2F+uP_Gsq$!?-?0F=CF+nAtj8FA*AbIWC-ax z7#YI44$z!s4$WESkeuac4vi;sOIN6^Dadv}#Zs||p@bDwT^6!BLGbA@$Rd0ND4L)i z0I%>jGBj{whxF`Hp>#f!2Cpc0G=~-u=Fp^R4o#Zo(4=V&nVfSpha@;hbI4?zqZ1@R z9GxHm;^+h&`f!2<j1zPS#0lEBa)J(pI6?bZPS7C{Cum>G2|6_51nqM<L5E13pnWeV z=unB1F*w0EIzfwgCuq^)1TE^Fphb)mw8(dY7Bx=LqTdNx<TydA04HeC;{>e=oS;RJ z6SPWjf)+(i(5k@+S|m9^s|Y7((c}cJDx9E2loPbdaDo<9PSC2u30h=1L8}laXwl^a ztxB9ARfUlOBtDD`APLOK01_`o29ShiWB`dDBLhf+Gcth0laT=|;Xw)pM{~%em7_Ui z(#p{ZT2(ngi%2JEmE{B}DvdykqpDJi^7V4k*z)sIK~zaOh{pjwc0(^GjXf>Dv<OUR zmVxDqGb{9R(m24UZ-5x!6F5K$@-o2&aDdIrFHU0z83>|5CgvBXaYBsDFHYkCn+axs z3<aqHnF>+^F&4yvm<v(_HaNdHjSFltxH-uQW|n|tLAHPyAZtJjE{HuKCMVb;2n%Er zh{XZ23dG=o*ac=nECVq)!M1@}U^`Os@^d*rc7PcmJ3tJm9Uvye4hRcm2Z#l>1H^#Z z0cJw%05KtUfLUNW@=9}yz;=KcAUi+|s2w0C#104xWCw@^wgbd~+5u)l>;N$#c7Rxr z0s<Q0(9NhuhLD76WC%&9Muw1tYGepWs78j6glc36NvKAKkPKjC2+06OhL8+kWB_S1 z7#Tq7P9p<Io59EcQimEDK-vsO29Ubc$N<u2FfxGDsYV8nHiMA?q;54bfMjeV14xE6 zGJs@ABLhgrHZm|VV@*yhN(CRzXJh~=4vY*S#e<On)RoY+dq&W;dqxJ3Y+wXk>u3aB z+i2u!V8Ne~U!GT<Sd>znn3tSi1Rfzag07u3GJq5pM$k2dMh4JOF@T01bWNp^0i-2j zWB?641IQ4SkpZN5GJ>wPG;)QEh#MI|iVGv?nolF>+DRkm8crkV+E63tT2CYByqb}L zi7Q`z9;hFknwOKBn37r~1L1-WYb+_xPsuMSE-1}QE-6hc$;=0D4l{zT*EE8z12r;$ z6p2O#kRs6ty6({kx(?C^y57_Xy8hG%y1vxN08&&Nxf&V@!j1zfNGwWBFG?)P5P^vl zmZlbitj<k@4a!4GP$TF$G)B;KX^dPU%gBtN%SDZ#%T|q`%VCY6%Ug}0Q;9~d#s-qb zg{6r_sVSgLoswUI?ok6sS!`qgP14W>s7BBQs7BC*szwHovfl{0kkiNjQZ^bv7lax? z7kU~&7lIlYm>Tjy!WI_j;2dRO>L>#dE(V1@Bpxu_1gXD_3?O5eM$ko~M$ko~M$l7x zjG(9X7(o|{I+~dq^OhE1Buhx`Yh(avcp4d4xC$o~B_?NQB<7?g<(HJ?=YsaPi=c@W zWu|A82*3qP@(Vz!1d#>6N<=}zDd4tSL1IyAUP%V%TtG3f9ELDB44`Xtji76EjSL_& z0!9Xq837~c8et;?X!FnjvbN6%x@Oi0x`x&Wx>nZ60McwQGJrHAj0~Wqk^!U{VdQFJ z!~=46D#)dvAtf*e>_AYT7sSg>ErCk%L%A?%=xJ4;J}vy*Dv%jaPJTIP_(UMRG&83- zGcP>{YKkCS0A>ykIB=lSykHJYiW`>vLE{TBHmHDsc7+`+VO>^e02x4wWdleT*T?|U zMKdyhbYYAPVC@5FyW0TL4mUD@bS;exAYB?G14tLs$N<u{F*1O3NsSC3i<68DAd8NS z3?Pe+j0_-CDMkj6F;*i3$f6}91IQvJBLm2yBqKv;L1qYz5JO0N$;c2EQP66@5YoOg zGJteZjSL`T#zuzFh%<n6nT!k|Q%6Py@RSVhni?6HI|{*ykl@6;;^5Sx%rqa-;?mq) za7l*T{uO}i!-OkyM;Gux7l`Bt0w4615(rMUhLDx6Muw1;u11ECm8?dFkd>}RhLDx6 zMuyN5&k(Yr)yNRicr`LG2KUB{3?cKMMuw1y2_r)jXP$zhR7m?9R{R=5OB+MTq^FS~ zWSOjyp^<?^ab<2&eok6`QE_5!Vo_#EVqS56X;CudKr%zftf&#_;Mwxj%+#XdlA^@o zVvr9Ej9gsB!4o#Frtzq>B@S^j9O4!@#LdAo$VPDU(ap7l*^8zhCXP!zx;tR%(cJ+P zM|TIhelr~IfQh5K1165k9CUZU)T6rtU7ZCEcfiEa-2oFvcLz)ym-*=KFh>i2a~$q~ ziKDv%CXVh7m^ivSVB#=$z~adi>JB&`%^m3CFn6Gf!`y)`4s!>(IL!YraTty69+)_~ z`7m*G^I_uX{zTUgOCRX=!Nk%12@}U<4!S#F>e1bSt`3%N(A@zOM|THI9Nir-aa`u3 zy8}J^Vd(?i9WZfpcfiEa-2oFvcLz)y<_=i+!0dzb(b6BfILsaB;xKohi^JT3E{^UF zbaP?p2;F@!aa`)r-2qdN?hcqZEFNI)gVE^r!o<<-g^8n^4--fCC%QUVx<R)OCXQ|& zOdQ>xFmYVwqq_s$->~$7?hcqZx;tRv=<a}tqq_qp4s!?0y)gUWe6;k3E)H`Cx;V@o z=;APUpo^os1KnI$`apLdOdOYbba%kiqq_qpj_wY0{jhX|?hcqZx;tRvxXeL!2TVOI zJ;1^VMx)yc6GyifCXQ||OdQ>x==Q<V2fBSQadi7&;^_9l#L@i;6NkA2#)sJl=cA=R zba9wF(8XcyKo^I(16>^59q8u5(g(WxVB)ycqq_s99^D-<addZ}>xZQeba%kS(cJ+P z$7K$>J7DV3-GQzSmX6Tf0TV}e2TUB@9WZfR=EKq@EWThgy1g)QbbDdq==Q?I!Dl}> zI+;50B<2>R78NJvffkQ|IwwYkkUp$|F{GzsU~FK{37&8Uog=~to@NHIKxz$)A@fHD z#*od12F8%h6QHH1P<cmA@H97)4ij$BcuP)ZUTSh~5>yqqLuOzM*=k{64B2X7U<?^{ zFffJ;I~W*4wqAgii$nB5I&cQY&;_i<&;_i<&;_i<kl{K5W9R}_W5^PF17pY%djn(0 z5_<z<$P#-4W9R}`V@O}mz}Uz@0(6Q>L1IoKXfCQMH6<xEC%+uz88MJVQEEw1KIkSC zxG+yjehHidaU^6_s(~@2muX-O>5>~5Lxv0tjA6kInY%JDhR*I98-eEz4UD0)yvC5B z2LofsY_EYaWJ<@t7}6CtFosO&7#KsQbPSA*O~gPqeq^T>XXF=^fWtho66ADIh%jgc z6pAS5)(LdABH-bAG^G%KL3+~$#>U_w00U!4Z`;7w80v0FFWbNvG8JTC3>iu@Foulv z8W=;yehrKvQ#}U8ka0}|W5~FsfiYz4*1#Cj0XHy)3|$x)Lq?Vjj3Fb-2F8%79s^^@ zK&62(WDMEB7&6jr06m}Fz!);TXkZK(yEQO|i~$=MLxvj-j3HxH2F8#vU;|^wSh0aI zWCY#77&1m|U<?_HHZX<^I~o{6req9^Awxk1#*m>K17pY#kb$w85kEN7fb$TT#SJDQ z>%c)_Y3Kr3F=6Q9=Ee<PECrF11jSZnUS?i;a(-S`K9~iO5r@k__@L1&&>*4&nmBl1 z3namvl3!AingX^M$_BefGzC0fh*;nPJ~4<NEC*H$W^sc_h*b~=LpPhbIGaG%!GoO* zk#jcVhAejho5fX}mzbRj&J%parNsrQdBvIed9d+iLr2J>UqeU8qF+Nt$f93EN64aI zLr2J>UqeU8szO6Y$cUJsBV^>u&=E57W#|amMr7y+T}18(83{9Vgp8aSIzmRy3|*kx z&|IL~&|IL~&|IL~&|IL~&|IL~&|F->Ws{+cD|m+1&;_~;%>}Yu$<PIIJdvRbbX%GW zbX%GWbX%GW<ai=O7sz56Ll-x2_AqpTES52Hfo!KUa)E58Gjf4!r!#VaY^O7Ffo!KU za)E58Gjf4!r!#{3!wBjRBd9-&p#Cs|`ojq74<o2QjG+E7g8IV<>JKBRKa8OMFoOES z2<i_bs6UKcAlvMWTp){pj9efymPRg+8A~G<$ZAg`sDF*1{xyR87n0PCTp){s+@SLb zZqOlUH|Q}DZqQ>Q+@QxqxIvGJaDyHb;Rc@4DFUZ3_R?Z7&0Uq5UX_>*PHNzk=;-9i z4_o~N${2dZ$pY||fKUM*aMFfxz)2p;5k_7V2Ne@UWE7|f4|w4ej04UEP!2cfh94-K z2VMz+Myw$j2`a@4$tzGE52!>2U)u#8ayBrA%%~X{L*}9kj3HCy2F8#XH3Jh1$nl0I zkeNCI6Ua=RfeB=$&cFmRQ|D*~T~=%cT|R6ET|R6ES%cze23<aE23<aE2Hh=S23<yM z1{wNwG=mHcJDNe}3LMQK`+gnGAWIG0pyzhDfu^QQ^D-UHpi|pskToriW{|ZnhAz;3 zrY_L^r7kYur2>Y~j9>`O2nNQGp>snQ$Wd>GE|8<%3|*l6SzRFeSq+RKL+A#^kRfyf zW603Ep^J;9a9U1cNl7Yb;;kSxFA<zrzzM_!vgg&n*bG{SK~@?Wx<L2Fx<Jk>GjxG0 z**An{LIY#S$|FN)<~1;e4CfoVK=$k!7(-Tw8A3C$Av6;k7(-Sn85l!`{SAyE>xB%A zA#q~}%iPZ9E<8w!6T#<b8aYD((#RP)_uvd!Ead0}akQhkBe;Zebb?esj!uvY$k7Qh z=iumMW-N(yKwd95vj`L%Mh1{A%tp{7PK=;OoH&|82HYLZAp`D?=8)E&qd8>2-O(H} z;O=M+nG<w0hYZL&nnRZ5IGRIRl8)w(fp|xA$k7sx=8)E-qdBD2=x7eva_eXgX$Lx* zL)w9k=8$%vqdBA<=x7dU2RfQVj+SsVhqMD7%^~eTM{`3n1_lNOMh3?J{~7o}Op0+d zLo|aYgD2A|1_mYx28RCvEV}=0GhO-rjY;YMHwcdbuIB$YrXNfaFma}<|GzQ)MN${T z5X0cj;LT*izyLPI_5TkL%>Wk%$w1|7m|Xt<fXaI?cre&8*fC}>y!gKWVkekA3&LiA z$;*RGV+4~3H!(0Uy+Lw0$W3pM++@vQ&7i}e!>IMYjxmwp5!`&E|7DDc3@`q}?0~9c zI?cF%=>bzBqdY@D$Tp@2j5C-XFr_m}LiigPbQCr)ghseVDkymGVBib%-oO?d6|uoY zdV@w}LYhKXXv794)x@0~3>*x}$;wI6ii#T;A~!HNE4yrBU}SJkaMIntsIx&KAx(D! zla4})!iEC@8(7q$Hfb<&JG&;kZeVkbP~5<*8W|KFp{T4V-4&s*As`?^af3sow891p z5X&MmQeh)Rbz;f}jerP6C{JO70Z85er1C&OghHA^S68CK2CjgJ1Zjm03LrTy5X%h2 zGE0z7h>VPs-oT`~fy+6014B^61_p1CV#N(y&eGl+<Up$Ayc2f_FeHOi>|kX`ii}K2 zjNHJe9T};-fgvCwH8N6n1GA2TtHK5r)x;Eq4Xn-q5gS;Qoi?y3J4q`lMs8qC(A~hI zqpYZ`sF1ReHOWZ<Bm?rJG{nsyasy|AYw`wWEk%V5Y|aW@3SAo*wKp2Ds%~IW3yeqz zkWLJWj8KkLj8xd*5FD|AQCk`m7*P9lH?Zn#V0YHu#J~gzTQ;2y?8;8c3L6-dof0=N zC8cd(Oy0nluz^)ecLTeQ0?4U6&dEDi7?Kn~KG@)p5D8L}-~x6kht4K;9tLMuCoM&I zY;o$K#vT{M6h>|34Ghj3SX2`fT)UJLK`{vqVFqbXENoDa-rxWar47ug%84!zE4XoJ z-=Go@v4H`k3+gpwO+27Pr|h(en~T{sLQz^-F;YuWcLT4^MkXfL4JnB(x*Pa(HZUeA zC~V+$_5%4!VFRzSQ{)Cd5HBb~VFRDCQ>5+&ejSAk0^lG~*ulb(+@+kjf!{enp({aQ z1HZD9vhD@}9q*8cjVviHkt!Q_Q<W=pHwfx@2S#`YLj<K2BULsCgT;k(HZlk}hem7^ z5OChWnCP^DUmKLlH!!I>ZR7<Jd`zlN8yLlu6*e%d21INGCH)O7s!m;93LtCwv=o&$ zFebv-(hzx&7ADoi4SZUPksElO-8b+#yC-a5LX9eg4Ok;^BLjo5vdacW=M4;Yn;01x z!8uJ>hhZZFlWR!C1_5O!1=kHc%1#@&l{auEI4LA-U`*U9B*e(T&*0?Lm9SBWO?yM9 zwzN{D?gkN^jSRxtx*J4wHZp=}F`bP}AX;2!BQuDW(AmfWq9t`UvVv$SosDcDT3Tl# zJBXIi*~kH+Wpy@kf@n=0WmvdxU`y~0iBMM1+rStL${@NMw2&ml;F5(9No^!aHMnFE zL{d(NVFRx=8UB;k*~lQLt-C=%XCotsR@B+Z1frF6HZp@~Wu1*IAX-IdBP)nj)!E1f zqSbUZvV&-KosAqIT0>_eCy3V3QLq7LA6*@VgbhLo3eL(K7!#a9<sc~Q>44Izp3Vkc z?G3tGx*PO$6cpSQaKxf=qJ=W348^NLS}{_0g8?jRH}bH#f>PQBeXWhGjH0d)29e-c zG(-v>TxyJT6imS`H`ZZ*s&U0E95yg&V@-1)aR#w13?SfQr);s&h*1>eR}&ot8wGb9 zKHR|IoM@rD!Bj_2+(vhU86l+`7@d<XbT^pmY-C~(Q&rGYa0iua8<<o*ySm(!-IWs+ zB9f#*1+B8vCNoAxQ4uZO4Hh~Z7{oRRDm!gpQQg3z3Rbp}i@|9Fr?Ylym!7hMf=!o& z?gmR#WeR#=g$f&3l$~Iz6*eRUL?~=<2#nawq`<5oy_rdkSt`;ARK9XJD|9J?btt<f zY+!Ll&!!0q8yK}A6%dniLW=SRX6M9|fQSuD&WX|+n4J?MH!!QB>q%5rP}sntrtFqz zA+5ZD!`Tf~IdM3HZAnnTW{EP$7Uc~r&IzE%U{y`rz=~n&1{SqMPz42cgla-c0Hg#( z*cGX}!3q`@3JMAvSk<6mp=@KJyTKYRwShy~NkKuuP1$_|qqZ_A7Hwd1V9!88dILj9 zgr0(pvLd)@Wr7GQfZPR*iVbY48(396LD>V=On^ipX39%YhQ^jMD9PKxY*X04;S6=6 zLIS8QfgX^sD7Mp4R?q{5qT2>R=ZFC5pvVXXn@GKldW<64x*P0KWP_vwP-GoYWMO)d z%|qAWsI$RB8(9HN6U;&<oeh@ST?z^|pfuKHfm`VY7H7Q;dfK`hoWWiOC1W*6$R}># zQg(u*R8VqOR<KdfQ?`J5$VF!(2aBo`DD{K1sKUYsWF6ro?T#&hfzlVLsdxjEsvD>r zvr)E?RzylKijh!vy6S9Xu+WBuikr?x21{*4P@I7r3iS;<2;6lxFo;63{U!!b4j0zj zV4|hF!2^f5r_M$OMiJo+rdqliymU60Yw2$A28E)lvVxw1TbFVoEVO)dKnZ$-x3=yE zU!6@13@+Nb8~k)OF))G{{yLi&!6E@Vn;5|&fgm-m+PWKpKx#mYV2~ORBLt)d#0b^d z%)r3lsI9vpOlLC#BbXJgvzZYh7NN755h4}|QpX5V7X?xWW<`V4fmtyibzoL3$WCW% z-3@UdJ3)+ikewh#0?1AfBN3#=O<Q+E5=ae*kqlA;Vx)l7fEcMD`xu<GbvLAe>;tpX zLH2=J86f+>tW1zPMv%HJkUB6c8>9}*$^oeZvvPGdGFZVQFb~YzV5hCSAs@oD*Vf%o z019-FvJDQ}x*H00HZodigVhv4m>@O9U^O5KkeU*mjSSXsQ%k`-u#Pea6QrXY!UQR= z(Amgn4KcD3!UQR(f-pe}s&zIp*uc%J0rSA-)k2sc<#iAyNO`@^Mn)Tmc?}RINI@fn z2~yCcvys6TZeBB(2R5$-!UQRAg)l+N+jKTE+Ct20hcH13Iv`Atf=->yoGeVR7Oj)^ z29DGX%qjsDuxeZvRIz&RU^o#Jv5}FnFLDDTq`WNH$iVL8z`b>0FGGGjgR?#-gBDms zWCh0*js}i0<}|i2HZK-44i$Dz4ye>7ws~w_Y}Jf;EO9IWOb%?itY9etCr55A2UQ0J z2iXIX2Sg7D9pF8{>A=9rz^S#7LDI>Qo52ACf+d+5TsEpOb_7OjaOjBG$eiM`Q3J%= z!NCCHGPp!aZ&U{f?BZx(U}SOVNDhhE$jsmp2?7oxt2m}{G;vfgXRt-E`LI}UsIhbM zXzgJ9-@1XNcOw&n%VyRTHU<|L1_lPMl9IH<(vtj)%)Al>(8c$e`FRSq3Wj>-TwIAM zS*67#y2YhQ={c1J8N~{=3I<#$scDI&IVHM~#eNF53OR`-d0Z*wy2&}IiA5!q1*y8A zS(3bT-QtqeT;1}_l#&dv#<awooTS9$Y~AAI#GK5$baX+`sSFCX3VHc?say!PsAi<6 zWD>B)kSo0?u@Z+9K%UIZOV`cG%*`xOuvIWJFyMlT6(#1Sr|RY=Rw&pim|_TLf&#?? ztXMZWKer&iII|=b5+IrBdHF@Dx<#qQB}JLZpe5i?@x0`s)WqUc-3r|d@a_aqu$pic zrKY78rRF84D%dI@IJ%j6DXAc<4Y(j{Bf+5)65{5lo0*rE57Nq2o|%`DUtSEhKd~e; xDKjUtq!KIxvq85wFR`Ei<ipg8<kSL~Tey<*^Gb^Hb96KFia}e`;No0d3;>v1%-R3| literal 0 HcmV?d00001 diff --git a/tests/component_tests/font/__init__.py b/tests/component_tests/font/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/font/test_font.py b/tests/component_tests/font/test_font.py new file mode 100644 index 00000000000..55e27ae84ce --- /dev/null +++ b/tests/component_tests/font/test_font.py @@ -0,0 +1,337 @@ +"""Tests for the font component. + +Focuses on verifying that long multi-byte (Chinese/CJK) glyph strings +are correctly processed through the font configuration pipeline. +""" + +import functools +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.components.font import ( + CONF_BPP, + CONF_EXTRAS, + CONF_GLYPHSETS, + CONF_IGNORE_MISSING_GLYPHS, + CONF_RAW_GLYPH_ID, + FONT_CACHE, + flatten, + glyph_comparator, + to_code, + validate_font_config, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_GLYPHS, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_SIZE, + CONF_TYPE, +) + +FONT_DIR = Path(__file__).parent +FONT_PATH = FONT_DIR / "NotoSans-Regular.ttf" + +# 200 unique CJK Unified Ideograph characters (U+4E00..U+4EC7) +CHINESE_200 = "".join(chr(cp) for cp in range(0x4E00, 0x4EC8)) + + +def _file_conf() -> dict: + return {CONF_PATH: str(FONT_PATH), CONF_TYPE: "local"} + + +def _make_config( + glyphs: list[str], + *, + ignore_missing: bool = False, + size: int = 20, + bpp: int = 1, + extras: list | None = None, + glyphsets: list | None = None, +) -> dict: + """Build a config dict matching what FONT_SCHEMA produces.""" + return { + CONF_FILE: _file_conf(), + CONF_GLYPHS: glyphs, + CONF_GLYPHSETS: glyphsets or [], + CONF_IGNORE_MISSING_GLYPHS: ignore_missing, + CONF_SIZE: size, + CONF_BPP: bpp, + CONF_EXTRAS: extras or [], + } + + +@pytest.fixture(autouse=True) +def _load_font(): + """Load the test font into FONT_CACHE and clean up afterwards.""" + fc = _file_conf() + FONT_CACHE[fc] = FONT_PATH + yield + FONT_CACHE.store.clear() + + +# ---------- flatten / glyph_comparator helpers ---------- + + +def test_flatten_splits_chinese_string_into_chars(): + """A single string of 200 Chinese characters must become 200 individual chars.""" + result = flatten([CHINESE_200]) + assert len(result) == 200 + assert all(len(c) == 1 for c in result) + assert result[0] == "\u4e00" + assert result[-1] == "\u4ec7" + + +def test_flatten_multiple_chinese_strings(): + """Multiple glyph strings are concatenated then split correctly.""" + s1 = CHINESE_200[:100] + s2 = CHINESE_200[100:] + result = flatten([list(s1), list(s2)]) + assert len(result) == 200 + + +def test_glyph_comparator_orders_chinese_by_utf8(): + """glyph_comparator must order CJK characters by their UTF-8 byte sequence.""" + chars = list(CHINESE_200[:10]) + sorted_chars = sorted(chars, key=functools.cmp_to_key(glyph_comparator)) + # CJK block is contiguous and UTF-8 order matches codepoint order here + assert sorted_chars == chars + + +def test_glyph_comparator_mixed_ascii_and_chinese(): + """ASCII characters sort before CJK characters (lower UTF-8 bytes).""" + assert glyph_comparator("A", "\u4e00") == -1 + assert glyph_comparator("\u4e00", "A") == 1 + assert glyph_comparator("\u4e00", "\u4e00") == 0 + + +# ---------- validate_font_config ---------- + + +def test_long_chinese_glyphs_raises_missing_error(): + """200 Chinese chars not present in NotoSans must raise Invalid with the correct count.""" + config = _make_config([CHINESE_200]) + with pytest.raises(cv.Invalid, match=r"missing 200 glyphs"): + validate_font_config(config) + + +def test_long_chinese_glyphs_error_mentions_overflow(): + """When more than 10 glyphs are missing the error should mention the remainder.""" + config = _make_config([CHINESE_200]) + with pytest.raises(cv.Invalid, match=r"and 190 more"): + validate_font_config(config) + + +def test_duplicate_chinese_glyphs_detected(): + """Duplicate CJK characters within a single glyph string must be caught.""" + duped = "\u4e00\u4e01\u4e00" # first char repeated + config = _make_config([duped]) + with pytest.raises(cv.Invalid, match="duplicate"): + validate_font_config(config) + + +def test_duplicate_chinese_across_strings(): + """Duplicates across separate glyph strings are also caught.""" + config = _make_config(["\u4e00\u4e01", "\u4e01\u4e02"]) + with pytest.raises(cv.Invalid, match="duplicate"): + validate_font_config(config) + + +def test_no_false_duplicates_in_200_unique_chinese(): + """200 unique CJK characters must not trigger the duplicate check.""" + config = _make_config([CHINESE_200]) + # Should not raise duplicate error — it should reach the missing-glyph check instead + with pytest.raises(cv.Invalid, match="missing"): + validate_font_config(config) + + +def test_valid_latin_glyphs_pass_validation(): + """Latin characters present in NotoSans-Regular pass validation without error.""" + config = _make_config(["ABCabc123"]) + result = validate_font_config(config) + assert result is not None + assert result[CONF_SIZE] == 20 + + +def test_long_latin_glyphs_pass_validation(): + """A long string of supported Latin glyphs passes validation.""" + # 95 printable ASCII characters that NotoSans supports + latin = "".join(chr(cp) for cp in range(0x21, 0x7F)) + config = _make_config([latin]) + result = validate_font_config(config) + assert result is not None + + +def test_mixed_latin_and_chinese_glyphs_error(): + """Mixing valid Latin and invalid Chinese chars reports missing Chinese glyphs.""" + chinese_10 = CHINESE_200[:10] + config = _make_config(["ABC", chinese_10]) + with pytest.raises(cv.Invalid, match=r"missing 10 glyphs"): + validate_font_config(config) + + +def test_single_chinese_char_glyph(): + """A single Chinese character is correctly handled as one glyph.""" + config = _make_config(["\u4e00"]) + with pytest.raises(cv.Invalid, match=r"missing 1 glyph[^s]"): + validate_font_config(config) + + +def test_chinese_glyphs_as_individual_list_items(): + """Chinese chars provided as separate list items are handled the same as a single string.""" + chars_as_list = list(CHINESE_200[:50]) + config = _make_config(chars_as_list) + with pytest.raises(cv.Invalid, match=r"missing 50 glyphs"): + validate_font_config(config) + + +# ---------- YAML parsing ---------- + + +def test_yaml_long_latin_glyphs_parsed_and_validated(tmp_path): + """200 Latin Extended chars on a single YAML line are parsed intact and pass validation.""" + from esphome.yaml_util import load_yaml + + latin_long = "".join(chr(cp) for cp in range(0x100, 0x1C8)) + yaml_file = tmp_path / "font_test.yaml" + yaml_file.write_text( + f'font:\n - file: "NotoSans-Regular.ttf"\n glyphs: "{latin_long}"\n', + encoding="utf-8", + ) + + parsed = load_yaml(yaml_file) + raw_glyphs = parsed["font"][0]["glyphs"] + + # YAML must preserve every Unicode character on the single line + assert raw_glyphs == latin_long + assert len(raw_glyphs) == 200 + + # Feed through validate_font_config to confirm all glyphs are accepted + config = _make_config([raw_glyphs]) + result = validate_font_config(config) + assert result is not None + + +@pytest.mark.parametrize( + "glyphs_str", + [ + " ABC", # space at start + "AB CD", # space in middle + "ABC ", # space at end + ], + ids=["start", "middle", "end"], +) +def test_yaml_space_in_glyphs_preserved(tmp_path, glyphs_str): + """A space character in a glyphs string must survive YAML round-trip and validation.""" + from esphome.yaml_util import load_yaml + + yaml_file = tmp_path / "font_test.yaml" + yaml_file.write_text( + f'font:\n - file: "NotoSans-Regular.ttf"\n glyphs: "{glyphs_str}"\n', + encoding="utf-8", + ) + + parsed = load_yaml(yaml_file) + raw_glyphs = parsed["font"][0]["glyphs"] + + assert raw_glyphs == glyphs_str + assert " " in raw_glyphs + + # Space and ASCII letters are all in NotoSans — validation must pass + config = _make_config([raw_glyphs]) + result = validate_font_config(config) + assert result is not None + + +# ---------- to_code generation ---------- + + +# 200 unique Latin Extended characters (U+0100..U+01C7), all present in NotoSans +LATIN_LONG = "".join(chr(cp) for cp in range(0x100, 0x1C8)) + + +@pytest.fixture +def mock_cg(): + """Mock all cg codegen functions used by to_code.""" + with ( + patch("esphome.components.font.cg.add_define") as mock_define, + patch("esphome.components.font.cg.progmem_array") as mock_progmem, + patch("esphome.components.font.cg.static_const_array") as mock_static, + patch("esphome.components.font.cg.new_Pvariable") as mock_new_pvar, + ): + mock_progmem.return_value = MagicMock() + mock_static.return_value = MagicMock() + yield { + "add_define": mock_define, + "progmem_array": mock_progmem, + "static_const_array": mock_static, + "new_Pvariable": mock_new_pvar, + } + + +@pytest.mark.asyncio +async def test_to_code_long_latin_generates_all_glyphs(mock_cg): + """to_code must generate glyph data for every character in a long Latin string.""" + glyph_count = len(LATIN_LONG) # 200 + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + # USE_FONT define must be emitted + mock_cg["add_define"].assert_any_call("USE_FONT") + + # progmem_array receives the combined bitmap data (non-empty) + mock_cg["progmem_array"].assert_called_once() + bitmap_data = mock_cg["progmem_array"].call_args.args[1] + assert len(bitmap_data) > 0 + + # static_const_array receives one entry per unique glyph + mock_cg["static_const_array"].assert_called_once() + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + assert len(glyph_initializer) == glyph_count + + # new_Pvariable is called with the correct glyph count + mock_cg["new_Pvariable"].assert_called_once() + pvar_args = mock_cg["new_Pvariable"].call_args.args + assert pvar_args[2] == glyph_count # len(glyph_initializer) + assert pvar_args[8] == 1 # bpp + + +@pytest.mark.asyncio +async def test_to_code_glyph_entries_contain_expected_fields(mock_cg): + """Each glyph initializer entry must have 7 fields: codepoint, data ptr, advance, offset_x, offset_y, w, h.""" + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + for entry in glyph_initializer: + assert len(entry) == 7, f"Glyph entry should have 7 fields, got {len(entry)}" + codepoint = entry[0] + assert isinstance(codepoint, int) + assert 0x100 <= codepoint <= 0x1C7 + + +@pytest.mark.asyncio +async def test_to_code_glyphs_sorted_by_utf8(mock_cg): + """Glyphs in the initializer must be sorted by UTF-8 byte order.""" + config = _make_config([LATIN_LONG]) + config[CONF_ID] = MagicMock() + config[CONF_RAW_DATA_ID] = MagicMock() + config[CONF_RAW_GLYPH_ID] = MagicMock() + + await to_code(config) + + glyph_initializer = mock_cg["static_const_array"].call_args.args[1] + codepoints = [entry[0] for entry in glyph_initializer] + assert codepoints == sorted(codepoints) diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes index 63ab00e9f28..4df6726184f 100644 --- a/tests/components/font/.gitattributes +++ b/tests/components/font/.gitattributes @@ -1 +1,2 @@ -*.pcf -text +*.pcf -text +*.ttf -text From a008c27fcfc8e1f4fd1241ce4597eef4ea0be15b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 09:01:08 -1000 Subject: [PATCH 1008/2056] [climate] Avoid duplicate get_traits() in publish_state (#15181) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/climate/climate.cpp | 5 ++--- esphome/components/climate/climate.h | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 5cbe9a5dafb..32cac0961c6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -367,7 +367,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() { return recovered; } -void Climate::save_state_() { +void Climate::save_state_(const ClimateTraits &traits) { #if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" @@ -382,7 +382,6 @@ void Climate::save_state_() { #endif state.mode = this->mode; - auto traits = this->get_traits(); if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { state.target_temperature_low = this->target_temperature_low; @@ -480,7 +479,7 @@ void Climate::publish_state() { ControllerRegistry::notify_climate_update(this); #endif // Save state - this->save_state_(); + this->save_state_(traits); } ClimateTraits Climate::get_traits() { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index e2cb743c0a6..0251365dd8a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -335,7 +335,8 @@ class Climate : public EntityBase { /** Internal method to save the state of the climate device to recover memory. This is automatically * called from publish_state() */ - void save_state_(); + void save_state_(const ClimateTraits &traits); + void save_state_() { this->save_state_(this->traits()); } void dump_traits_(const char *tag); From 1e2c410abfae7a1c1a78cfff62c9507f307b582f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:47:18 -1000 Subject: [PATCH 1009/2056] Bump cryptography from 46.0.5 to 46.0.6 (#15193) 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 ce735f398ae..c74dd265c75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==46.0.5 +cryptography==46.0.6 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 3152642571a32108c87e90390722808c65533b4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:48:06 -1000 Subject: [PATCH 1010/2056] Bump codecov/codecov-action from 5.5.3 to 6.0.0 (#15194) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 965e23870d6..ab7a750388c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache From 81f0aa1168b8b993451b3673f28bd57763148cbe Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:54:50 +0100 Subject: [PATCH 1011/2056] [nextion] Replace `or`/`and` operators and missing `this->` (#15191) --- esphome/components/nextion/nextion.cpp | 2 +- .../components/nextion/nextion_upload_arduino.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index ac17e14312f..612bfbc968c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -90,7 +90,7 @@ bool Nextion::check_connect_() { #endif // NEXTION_PROTOCOL_LOG ESP_LOGW(TAG, "Not connected"); - comok_sent_ = 0; + this->comok_sent_ = 0; return false; } diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 6c454ab7459..f59b7080028 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -22,9 +22,9 @@ static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_size = this->tft_size_ - range_start; ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); - uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; + uint32_t range_end = ((this->upload_first_chunk_sent_ || this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); - if (range_size <= 0 or range_end <= range_start) { + if (range_size <= 0 || range_end <= range_start) { ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size); return -1; } @@ -34,7 +34,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGV(TAG, "Range: %s", range_header); http_client.addHeader("Range", range_header); int code = http_client.GET(); - if (code != HTTP_CODE_OK and code != HTTP_CODE_PARTIAL_CONTENT) { + if (code != HTTP_CODE_OK && code != HTTP_CODE_PARTIAL_CONTENT) { ESP_LOGW(TAG, "HTTP failed: %s", HTTPClient::errorToString(code).c_str()); return -1; } @@ -80,12 +80,12 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); - upload_first_chunk_sent_ = true; + this->upload_first_chunk_sent_ = true; if (recv_string.empty()) { ESP_LOGW(TAG, "No response from display during upload"); allocator.deallocate(buffer, 4096); @@ -112,7 +112,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { allocator.deallocate(buffer, 4096); buffer = nullptr; return range_end + 1; - } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" + } else if (recv_string[0] != 0x05 && recv_string[0] != 0x08) { // 0x05 == "ok" char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGE( TAG, "Invalid response: [%s]", @@ -214,7 +214,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ++tries; } - if (code != 200 and code != 206) { + if (code != 200 && code != 206) { ESP_LOGE(TAG, "HTTP request failed with status %d", code); return this->upload_end_(false); } From 6aafb521c15bf9a873f71392a445b1f7983234ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:59:21 +0000 Subject: [PATCH 1012/2056] Bump ruff from 0.15.7 to 0.15.8 (#15192) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e2bfe09cee..f4729f211c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.6 + rev: v0.15.8 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 1440b20333b..3b277e214dd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.7 # also change in .pre-commit-config.yaml when updating +ruff==0.15.8 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From fa8a609bcc1939ec4f9d7b54dc89b54bfc8ff23a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 13:50:50 -1000 Subject: [PATCH 1013/2056] [automation] Eliminate trigger trampolines with deduplicated forwarder structs (#15174) --- esphome/automation.py | 44 +++++++++++ esphome/components/binary_sensor/__init__.py | 61 +++++---------- esphome/components/button/__init__.py | 16 +--- esphome/components/event/__init__.py | 14 +--- esphome/components/number/__init__.py | 14 +--- esphome/components/sensor/__init__.py | 34 +++----- esphome/components/switch/__init__.py | 48 +++--------- esphome/components/text_sensor/__init__.py | 38 +++------ esphome/core/automation.h | 42 +++++++++- tests/unit_tests/test_automation.py | 81 +++++++++++++++++++- 10 files changed, 226 insertions(+), 166 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 17966dc782c..7b1d6ceca13 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -137,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action) ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") +TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder") +TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder") +TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) @@ -661,3 +664,44 @@ async def build_automation( actions = await build_action_list(config[CONF_THEN], templ, args) cg.add(obj.add_actions(actions)) return obj + + +async def build_callback_automation( + parent: MockObj, + callback_method: str, + args: TemplateArgsType, + config: ConfigType, + forwarder: MockObj | MockObjClass | None = None, +) -> None: + """Build an Automation and register it as a callback on the parent. + + Eliminates the need for a Trigger wrapper object by registering the + automation's trigger() directly as a callback on the parent component. + + Uses template forwarder structs so the compiler deduplicates the operator() + body across all call sites with the same signature. The forwarder must be + pointer-sized (single Automation* field) to fit inline in Callback::ctx_ + and avoid heap allocation. + + :param parent: The component object (e.g., button, sensor). + :param callback_method: Name of the callback method (e.g., "add_on_press_callback"). + :param args: Automation template args as list of (type, name) tuples. + :param config: The automation config dict. + :param forwarder: Optional forwarder type to use instead of the default + TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes + a single Automation pointer (e.g., TriggerOnTrueForwarder). + """ + arg_types = [arg[0] for arg in args] + templ = cg.TemplateArguments(*arg_types) + obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ) + actions = await build_action_list(config[CONF_THEN], templ, args) + cg.add(obj.add_actions(actions)) + # Use template forwarder structs for deduplication. The compiler generates + # one operator() per forwarder type; different automation pointers are just + # data in the struct. + if forwarder is None: + forwarder = TriggerForwarder.template(templ) + # RawExpression for aggregate init — both forwarder and obj are codegen + # MockObjs (not user input), and there's no Expression type for positional + # aggregate initialization (StructInitializer uses named fields). + cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 37cccc01be6..4705f1675d5 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -120,10 +120,6 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_( BinarySensorPtr = BinarySensor.operator("ptr") # Triggers -PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template()) -ReleaseTrigger = binary_sensor_ns.class_( - "ReleaseTrigger", automation.Trigger.template() -) ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template()) DoubleClickTrigger = binary_sensor_ns.class_( "DoubleClickTrigger", automation.Trigger.template() @@ -132,13 +128,6 @@ MultiClickTrigger = binary_sensor_ns.class_( "MultiClickTrigger", automation.Trigger.template(), cg.Component ) MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") -StateTrigger = binary_sensor_ns.class_( - "StateTrigger", automation.Trigger.template(bool) -) -StateChangeTrigger = binary_sensor_ns.class_( - "StateChangeTrigger", - automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)), -) BinarySensorPublishAction = binary_sensor_ns.class_( "BinarySensorPublishAction", automation.Action @@ -458,16 +447,8 @@ _BINARY_SENSOR_SCHEMA = ( ): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), - } - ), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), cv.Optional(CONF_ON_CLICK): cv.All( automation.validate_automation( { @@ -509,16 +490,8 @@ _BINARY_SENSOR_SCHEMA = ( ): cv.positive_time_period_milliseconds, } ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation({}), } ) ) @@ -556,13 +529,14 @@ def binary_sensor_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_binary_sensor_automations(var, config): - for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - - for conf in config.get(CONF_ON_RELEASE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, forwarder in ( + (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), + (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( @@ -593,13 +567,14 @@ async def _build_binary_sensor_automations(var, config): await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_STATE_CHANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_full_state_callback", [ (cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x"), diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 12d9ebaba62..f279b6ffe33 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_PRESS, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_IDENTIFY, @@ -41,10 +40,6 @@ ButtonPtr = Button.operator("ptr") PressAction = button_ns.class_("PressAction", automation.Action) -ButtonPressTrigger = button_ns.class_( - "ButtonPressTrigger", automation.Trigger.template() -) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -55,11 +50,7 @@ _BUTTON_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), } ) ) @@ -91,8 +82,9 @@ def button_schema( @setup_entity("button") async def setup_button_core_(var, config): for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_press_callback", [], conf + ) setup_device_class(config) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 300902b8cad..527bb4ebba9 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_EVENT, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_BUTTON, DEVICE_CLASS_DOORBELL, @@ -41,8 +40,6 @@ EventPtr = Event.operator("ptr") TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action) -EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") _EVENT_SCHEMA = ( @@ -53,11 +50,7 @@ _EVENT_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), cv.GenerateID(): cv.declare_id(Event), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_EVENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger), - } - ), + cv.Optional(CONF_ON_EVENT): automation.validate_automation({}), } ) ) @@ -92,8 +85,9 @@ def event_schema( @setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): for conf in config.get(CONF_ON_EVENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) + await automation.build_callback_automation( + var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf + ) cg.add(var.set_event_types(event_types)) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 0570ac0b1ec..90f9fe18356 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -155,9 +155,6 @@ Number = number_ns.class_("Number", cg.EntityBase) NumberPtr = Number.operator("ptr") # Triggers -NumberStateTrigger = number_ns.class_( - "NumberStateTrigger", automation.Trigger.template(cg.float_) -) ValueRangeTrigger = number_ns.class_( "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component ) @@ -198,11 +195,7 @@ _NUMBER_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), @@ -248,8 +241,9 @@ def number_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_number_automations(var, config): for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [(float, "x")], conf + ) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 9f3c1484b0c..19d03a0afc7 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -238,12 +238,6 @@ Sensor = sensor_ns.class_("Sensor", cg.EntityBase) SensorPtr = Sensor.operator("ptr") # Triggers -SensorStateTrigger = sensor_ns.class_( - "SensorStateTrigger", automation.Trigger.template(cg.float_) -) -SensorRawStateTrigger = sensor_ns.class_( - "SensorRawStateTrigger", automation.Trigger.template(cg.float_) -) ValueRangeTrigger = sensor_ns.class_( "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component ) @@ -316,18 +310,8 @@ _SENSOR_SCHEMA = ( cv.Any(None, cv.positive_time_period_milliseconds), ), cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SensorStateTrigger), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - SensorRawStateTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), @@ -897,12 +881,14 @@ async def build_filters(config): @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_sensor_automations(var, config): - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(float, "x")], conf) + for conf_key, callback in ( + (CONF_ON_VALUE, "add_on_state_callback"), + (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, callback, [(float, "x")], conf + ) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index bbafc54bd1e..c4dd4856e3f 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_ON_TURN_ON, CONF_RESTORE_MODE, CONF_STATE, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, @@ -61,17 +60,6 @@ TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchCondition = switch_ns.class_("SwitchCondition", Condition) -SwitchStateTrigger = switch_ns.class_( - "SwitchStateTrigger", automation.Trigger.template(bool) -) -SwitchTurnOnTrigger = switch_ns.class_( - "SwitchTurnOnTrigger", automation.Trigger.template() -) -SwitchTurnOffTrigger = switch_ns.class_( - "SwitchTurnOffTrigger", automation.Trigger.template() -) - - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True) @@ -86,21 +74,9 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger), - } - ), - cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), - } - ), - cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, } ) @@ -147,15 +123,15 @@ def switch_schema( @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_switch_automations(var, config): - for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) - for conf in config.get(CONF_ON_TURN_ON, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TURN_OFF, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, args, forwarder in ( + (CONF_ON_STATE, [(bool, "x")], None), + (CONF_ON_TURN_ON, [], automation.TriggerOnTrueForwarder), + (CONF_ON_TURN_OFF, [], automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", args, conf, forwarder=forwarder + ) @setup_entity("switch") diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 97f394ecf73..51eedf9a956 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_ON_VALUE, CONF_STATE, CONF_TO, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_DATE, DEVICE_CLASS_EMPTY, @@ -42,12 +41,6 @@ text_sensor_ns = cg.esphome_ns.namespace("text_sensor") TextSensor = text_sensor_ns.class_("TextSensor", cg.EntityBase) TextSensorPtr = TextSensor.operator("ptr") -TextSensorStateTrigger = text_sensor_ns.class_( - "TextSensorStateTrigger", automation.Trigger.template(cg.std_string) -) -TextSensorStateRawTrigger = text_sensor_ns.class_( - "TextSensorStateRawTrigger", automation.Trigger.template(cg.std_string) -) TextSensorPublishAction = text_sensor_ns.class_( "TextSensorPublishAction", automation.Action ) @@ -150,20 +143,8 @@ _TEXT_SENSOR_SCHEMA = ( cv.GenerateID(): cv.declare_id(TextSensor), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - TextSensorStateTrigger - ), - } - ), - cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - TextSensorStateRawTrigger - ), - } - ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), + cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), } ) ) @@ -203,13 +184,14 @@ async def build_filters(config): @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_text_sensor_automations(var, config): - for conf in config.get(CONF_ON_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - - for conf in config.get(CONF_ON_RAW_VALUE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + for conf_key, callback in ( + (CONF_ON_VALUE, "add_on_state_callback"), + (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, callback, [(cg.std_string, "x")], conf + ) @setup_entity("text_sensor") diff --git a/esphome/core/automation.h b/esphome/core/automation.h index ca4a2c8b6b0..fc2cad99be0 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -470,7 +470,9 @@ template<typename... Ts> class ActionList { template<typename... Ts> class Automation { public: - explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } + /// Default constructor for use with TriggerForwarder (no Trigger object needed). + Automation() = default; + explicit Automation(Trigger<Ts...> *trigger) { trigger->set_automation_parent(this); } void add_action(Action<Ts...> *action) { this->actions_.add_action(action); } void add_actions(const std::initializer_list<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); } @@ -487,8 +489,44 @@ template<typename... Ts> class Automation { int num_running() { return this->actions_.num_running(); } protected: - Trigger<Ts...> *trigger_; ActionList<Ts...> actions_; }; +/// Callback forwarder that triggers an Automation directly. +/// One operator() instantiation per Automation<Ts...> signature, shared across all call sites. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +template<typename... Ts> struct TriggerForwarder { + Automation<Ts...> *automation; + void operator()(const Ts &...args) const { this->automation->trigger(args...); } +}; + +/// Callback forwarder that triggers an Automation<> only when the bool arg is true. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +struct TriggerOnTrueForwarder { + Automation<> *automation; + void operator()(bool state) const { + if (state) + this->automation->trigger(); + } +}; + +/// Callback forwarder that triggers an Automation<> only when the bool arg is false. +/// Must stay pointer-sized to fit inline in Callback::ctx_ without heap allocation. +struct TriggerOnFalseForwarder { + Automation<> *automation; + void operator()(bool state) const { + if (!state) + this->automation->trigger(); + } +}; + +// Ensure forwarders fit in Callback::ctx_ (pointer-sized inline storage). +// If these fail, the forwarder would heap-allocate in Callback::create(). +static_assert(sizeof(TriggerForwarder<>) <= sizeof(void *)); +static_assert(sizeof(TriggerOnTrueForwarder) <= sizeof(void *)); +static_assert(sizeof(TriggerOnFalseForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<TriggerForwarder<>>); +static_assert(std::is_trivially_copyable_v<TriggerOnTrueForwarder>); +static_assert(std::is_trivially_copyable_v<TriggerOnFalseForwarder>); + } // namespace esphome diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 61fef8201d3..37779f23e68 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -5,7 +5,13 @@ from unittest.mock import patch import pytest -from esphome.automation import has_non_synchronous_actions +from esphome.automation import ( + TriggerForwarder, + TriggerOnFalseForwarder, + TriggerOnTrueForwarder, + has_non_synchronous_actions, +) +from esphome.cpp_generator import MockObj, RawExpression from esphome.util import RegistryEntry @@ -175,3 +181,76 @@ def test_has_non_synchronous_actions_dict_input( """Direct dict input (single action).""" assert has_non_synchronous_actions({"delay": "1s"}) is True assert has_non_synchronous_actions({"logger.log": "hello"}) is False + + +def _build_forwarder( + automation_name: str, + args: list[tuple[str, str]], + forwarder: MockObj | None = None, +) -> str: + """Build a trigger forwarder expression the same way build_callback_automation does. + + Mirrors the forwarder selection logic in automation.build_callback_automation. + """ + import esphome.codegen as cg + + obj = MockObj(automation_name, "->") + if forwarder is None: + arg_types = [RawExpression(t) for t, _ in args] + templ = ( + cg.TemplateArguments(*arg_types) if arg_types else cg.TemplateArguments() + ) + forwarder = TriggerForwarder.template(templ) + return f"{forwarder}{{{obj}}}" + + +def test_trigger_forwarder_no_args() -> None: + """Button on_press: TriggerForwarder<> with no args.""" + result = _build_forwarder("auto_1", []) + assert result == "TriggerForwarder<>{auto_1}" + + +def test_trigger_forwarder_single_float_arg() -> None: + """Sensor on_value: TriggerForwarder<float>.""" + result = _build_forwarder("auto_1", [("float", "x")]) + assert result == "TriggerForwarder<float>{auto_1}" + + +def test_trigger_forwarder_single_bool_arg() -> None: + """Switch on_state: TriggerForwarder<bool>.""" + result = _build_forwarder("auto_1", [("bool", "x")]) + assert result == "TriggerForwarder<bool>{auto_1}" + + +def test_trigger_forwarder_on_true() -> None: + """Binary_sensor on_press / switch on_turn_on: TriggerOnTrueForwarder.""" + result = _build_forwarder("auto_1", [], forwarder=TriggerOnTrueForwarder) + assert result == "TriggerOnTrueForwarder{auto_1}" + + +def test_trigger_forwarder_on_false() -> None: + """Binary_sensor on_release / switch on_turn_off: TriggerOnFalseForwarder.""" + result = _build_forwarder("auto_1", [], forwarder=TriggerOnFalseForwarder) + assert result == "TriggerOnFalseForwarder{auto_1}" + + +def test_trigger_forwarder_multiple_args() -> None: + """Binary_sensor on_state_change: TriggerForwarder with two args.""" + result = _build_forwarder( + "auto_1", + [("optional<bool>", "x_previous"), ("optional<bool>", "x")], + ) + assert result == "TriggerForwarder<optional<bool>, optional<bool>>{auto_1}" + + +def test_trigger_forwarder_string_arg() -> None: + """Text_sensor on_value: TriggerForwarder<std::string>.""" + result = _build_forwarder("auto_1", [("std::string", "x")]) + assert result == "TriggerForwarder<std::string>{auto_1}" + + +def test_trigger_forwarder_custom_type() -> None: + """Custom forwarder type passed directly.""" + custom = MockObj("MyForwarder", "") + result = _build_forwarder("auto_1", [], forwarder=custom) + assert result == "MyForwarder{auto_1}" From 240e53afce87155d12fe72a4f31117abe7ee4a13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 14:35:09 -1000 Subject: [PATCH 1014/2056] [fan] Add benchmarks for fan component (#15210) --- tests/benchmarks/components/fan/__init__.py | 5 + tests/benchmarks/components/fan/bench_fan.cpp | 122 ++++++++++++++++++ .../benchmarks/components/fan/benchmark.yaml | 1 + 3 files changed, 128 insertions(+) create mode 100644 tests/benchmarks/components/fan/__init__.py create mode 100644 tests/benchmarks/components/fan/bench_fan.cpp create mode 100644 tests/benchmarks/components/fan/benchmark.yaml diff --git a/tests/benchmarks/components/fan/__init__.py b/tests/benchmarks/components/fan/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/fan/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/fan/bench_fan.cpp b/tests/benchmarks/components/fan/bench_fan.cpp new file mode 100644 index 00000000000..c7966c78869 --- /dev/null +++ b/tests/benchmarks/components/fan/bench_fan.cpp @@ -0,0 +1,122 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/fan/fan.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Fan for benchmarking — control() is a no-op. +class BenchFan : public fan::Fan { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + fan::FanTraits get_traits() override { return this->traits_; } + + fan::FanTraits traits_; + + protected: + void control(const fan::FanCall & /*call*/) override {} +}; + +// Helper to create a typical fan device for benchmarks. +// Note: setup() is not called (no preferences backend), so save_state_() +// is effectively a no-op. This benchmarks the call/validation path, not persistence. +static void setup_fan(BenchFan &fan) { + fan.configure("test_fan"); + fan.traits_.set_oscillation(true); + fan.traits_.set_speed(true); + fan.traits_.set_supported_speed_count(6); + fan.traits_.set_direction(true); + fan.set_restore_mode(fan::FanRestoreMode::NO_RESTORE); + fan.traits_.set_supported_preset_modes({ + "auto", + "sleep", + "nature", + "turbo", + }); +} + +// --- Fan::publish_state() with speed update --- +// Measures the publish path for a fan reporting state — +// the hot path during fan operation. + +static void FanPublish_State(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + fan.direction = fan::FanDirection::FORWARD; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + fan.speed = (i % 6) + 1; + fan.publish_state(); + } + benchmark::DoNotOptimize(fan.speed); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanPublish_State); + +// --- Fan::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void FanPublish_WithCallback(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + + uint64_t callback_count = 0; + fan.add_on_state_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + fan.speed = (i % 6) + 1; + fan.publish_state(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanPublish_WithCallback); + +// --- FanCall::perform() set speed --- +// The most common fan call — adjusting the speed level. + +static void FanCall_SetSpeed(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + fan.state = true; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + int speed = (i % 6) + 1; + fan.make_call().set_speed(speed).perform(); + } + benchmark::DoNotOptimize(fan.speed); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanCall_SetSpeed); + +// --- FanCall::perform() with multiple fields --- +// Exercises the validation path with state, speed, oscillation, and direction. + +static void FanCall_MultiField(benchmark::State &state) { + BenchFan fan; + setup_fan(fan); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + auto dir = (i % 2 == 0) ? fan::FanDirection::FORWARD : fan::FanDirection::REVERSE; + int speed = (i % 6) + 1; + fan.make_call().set_state(true).set_speed(speed).set_oscillating(i % 2 == 0).set_direction(dir).perform(); + } + benchmark::DoNotOptimize(fan.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FanCall_MultiField); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/fan/benchmark.yaml b/tests/benchmarks/components/fan/benchmark.yaml new file mode 100644 index 00000000000..e9d59c12b2a --- /dev/null +++ b/tests/benchmarks/components/fan/benchmark.yaml @@ -0,0 +1 @@ +fan: From 90e6c0d7c7b2174309cd5309e0e082b6526b37e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 15:09:16 -1000 Subject: [PATCH 1015/2056] [core] Remove indirection from ControllerRegistry dispatch (#15173) --- esphome/core/controller_registry.cpp | 22 +++++++++++----------- esphome/core/controller_registry.h | 19 ++----------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 255efa86ba8..dd69de47d4c 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -10,24 +10,24 @@ StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controll void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -void ControllerRegistry::notify(void *obj, DispatchFunc dispatch) { - for (auto *controller : controllers) { - dispatch(controller, obj); - } -} - -// Macro for standard registry notification dispatch - calls on_<entity_name>_update() -// Each wrapper passes a small trampoline lambda that calls the correct virtual method. +// Each notify method directly iterates controllers and calls the virtual method. +// This avoids the overhead of a shared noinline dispatch loop with function pointer +// indirection. The loop is tiny (~20 bytes per entity type) so the flash cost of +// duplicating it is negligible compared to eliminating two levels of indirection +// (noinline call + function pointer) from every state publish. // NOLINTBEGIN(bugprone-macro-parentheses) #define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ - notify(obj, [](Controller *c, void *o) { c->on_##entity_name##_update(static_cast<entity_type *>(o)); }); \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ } -// Macro for entities where controller method has no "_update" suffix (Event, Update) #define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ - notify(obj, [](Controller *c, void *o) { c->on_##entity_name(static_cast<entity_type *>(o)); }); \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ } // NOLINTEND(bugprone-macro-parentheses) diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 15e3b4ba835..89b3069bcbf 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -146,8 +146,8 @@ class UpdateEntity; * entities call ControllerRegistry::notify_*_update() which iterates the small list * of registered controllers (typically 2: API and WebServer). * - * Controllers read state directly from entities using existing accessors (obj->state, etc.) - * rather than receiving it as callback parameters that were being ignored anyway. + * Each notify method directly iterates controllers and calls the virtual method, + * avoiding function pointer indirection for minimal dispatch overhead. * * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) * Typical config (25 entities): ~780 bytes saved @@ -247,21 +247,6 @@ class ControllerRegistry { #endif protected: - /** Type-erased dispatch function pointer. - * - * Each notify method passes a small trampoline that calls the - * correct virtual method on Controller. The shared notify() loop - * iterates controllers once, calling the trampoline for each. - */ - using DispatchFunc = void (*)(Controller *, void *); - - /** Shared dispatch loop - iterates controllers and calls dispatch for each. - * - * Marked noinline to ensure only one copy of the loop exists in flash, - * rather than being duplicated into each notify_*_update wrapper. - */ - static void __attribute__((noinline)) notify(void *obj, DispatchFunc dispatch); - static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers; }; From e77cdb59710c5db3a904a6054ebc63035954d40e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 15:13:44 -1000 Subject: [PATCH 1016/2056] [light] Validate effect names during config validation instead of codegen (#15107) --- esphome/components/light/__init__.py | 75 +++++ esphome/components/light/automation.py | 49 ++- tests/component_tests/light/__init__.py | 0 .../light/test_effect_validation.py | 280 ++++++++++++++++++ 4 files changed, 395 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/light/__init__.py create mode 100644 tests/component_tests/light/test_effect_validation.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4090ca57c25..5925afb472d 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_ID, CONF_INITIAL_STATE, CONF_MQTT_ID, + CONF_NAME, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, @@ -41,6 +42,8 @@ from esphome.const import ( from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass +import esphome.final_validate as fv +from esphome.types import ConfigType from .automation import LIGHT_STATE_SCHEMA from .effects import ( @@ -70,9 +73,19 @@ IS_PLATFORM_COMPONENT = True DOMAIN = "light" +@dataclass +class EffectRef: + """A pending effect name reference from a light action to validate.""" + + light_id: ID + effect_name: str + component_path: list[str | int] # path_context when the action was validated + + @dataclass class LightData: gamma_tables: dict = field(default_factory=dict) # gamma_value -> fwd_arr + effect_refs: list[EffectRef] = field(default_factory=list) def _get_data() -> LightData: @@ -115,6 +128,68 @@ def _get_or_create_gamma_table(gamma_correct): return fwd_arr +def find_effect_index(effects: list, effect_name: str) -> int | None: + """Find the 1-based index of an effect by name (case-insensitive). + + Returns the 1-based index if found, or None if not found. + """ + effect_name_lower = effect_name.lower() + for i, effect_conf in enumerate(effects): + key = next(iter(effect_conf)) + if effect_conf[key][CONF_NAME].lower() == effect_name_lower: + return i + 1 + return None + + +def available_effects_str(effects: list) -> str: + """Return a comma-separated string of available effect names.""" + available = [ + effect_conf[next(iter(effect_conf))][CONF_NAME] for effect_conf in effects + ] + return ", ".join(f"'{name}'" for name in available) if available else "none" + + +def _final_validate(config: ConfigType) -> ConfigType: + """Validate all recorded effect name references against their target lights. + + This runs once per light platform instance. If no light platform is configured, + this never runs — but the ID validator will catch the missing light ID separately. + """ + data = _get_data() + if not data.effect_refs: + return config + + # Drain the list so we only validate once even though + # FINAL_VALIDATE_SCHEMA runs for each light platform instance. + refs = data.effect_refs + data.effect_refs = [] + + fconf = fv.full_config.get() + + for ref in refs: + try: + light_path = fconf.get_path_for_id(ref.light_id)[:-1] + light_config = fconf.get_config_for_path(light_path) + except KeyError: + # Light ID not found — ID validation will have already reported this + continue + + effects = light_config.get(CONF_EFFECTS, []) + + if find_effect_index(effects, ref.effect_name) is None: + raise cv.FinalExternalInvalid( + f"Effect '{ref.effect_name}' not found for light " + f"'{ref.light_id}'. " + f"Available effects: {available_effects_str(effects)}", + path=[cv.ROOT_CONFIG_PATH] + ref.component_path, + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 55273003b95..16e7d72f6b0 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.config import path_context import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -17,7 +18,6 @@ from esphome.const import ( CONF_LIMIT_MODE, CONF_MAX_BRIGHTNESS, CONF_MIN_BRIGHTNESS, - CONF_NAME, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_RED, @@ -26,7 +26,7 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_WHITE, ) -from esphome.core import CORE, Lambda +from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression from esphome.types import ConfigType @@ -98,6 +98,31 @@ LIGHT_CONTROL_ACTION_SCHEMA = LIGHT_STATE_SCHEMA.extend( } ) + +def _record_effect_ref(config: ConfigType) -> ConfigType: + """Record a static effect name reference for later cross-component validation.""" + if CONF_EFFECT not in config: + return config + effect = config[CONF_EFFECT] + if isinstance(effect, Lambda): + return config # Lambda effects resolved at runtime + if effect.lower() == "none": + return config # "None" is always valid + + from . import EffectRef, _get_data + + _get_data().effect_refs.append( + EffectRef( + light_id=config[CONF_ID], + effect_name=effect, + component_path=path_context.get(), + ) + ) + return config + + +LIGHT_CONTROL_ACTION_SCHEMA.add_extra(_record_effect_ref) + LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(LightState), @@ -122,18 +147,24 @@ def _resolve_effect_index(config: ConfigType) -> int: Effect index 0 means "None" (no effect). Effects are 1-indexed matching the C++ convention in LightState. """ + from . import available_effects_str, find_effect_index + original_name = config[CONF_EFFECT] - effect_name = original_name.lower() - if effect_name == "none": + if original_name.lower() == "none": return 0 light_id = config[CONF_ID] light_path = CORE.config.get_path_for_id(light_id)[:-1] light_config = CORE.config.get_config_for_path(light_path) - for i, effect_conf in enumerate(light_config.get(CONF_EFFECTS, [])): - key = next(iter(effect_conf)) - if effect_conf[key][CONF_NAME].lower() == effect_name: - return i + 1 - raise ValueError(f"Effect '{original_name}' not found in light '{light_id}'") + effects = light_config.get(CONF_EFFECTS, []) + index = find_effect_index(effects, original_name) + if index is not None: + return index + # Should never reach here — effect names are validated during config + # validation in FINAL_VALIDATE_SCHEMA. This is a safety net. + raise EsphomeError( + f"Effect '{original_name}' not found for light '{light_id}'. " + f"Available effects: {available_effects_str(effects)}" + ) @automation.register_action( diff --git a/tests/component_tests/light/__init__.py b/tests/component_tests/light/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/light/test_effect_validation.py b/tests/component_tests/light/test_effect_validation.py new file mode 100644 index 00000000000..579e92c62a9 --- /dev/null +++ b/tests/component_tests/light/test_effect_validation.py @@ -0,0 +1,280 @@ +"""Tests for light effect name validation.""" + +from __future__ import annotations + +from collections.abc import Generator +from contextvars import Token + +import pytest + +from esphome import config_validation as cv +from esphome.components.light import ( + EffectRef, + _final_validate, + _get_data, + available_effects_str, + find_effect_index, +) +from esphome.components.light.automation import _record_effect_ref +from esphome.config import Config, path_context +from esphome.const import CONF_EFFECT, CONF_EFFECTS, CONF_ID, CONF_NAME +from esphome.core import ID, Lambda +import esphome.final_validate as fv +from esphome.types import ConfigType + + +def _make_effects(*names: str) -> list[dict[str, dict[str, str]]]: + """Create a list of effect config dicts from names.""" + return [{f"effect_{i}": {CONF_NAME: name}} for i, name in enumerate(names)] + + +# --- find_effect_index --- + + +def test_find_effect_index_found() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert find_effect_index(effects, "Fast Pulse") == 1 + assert find_effect_index(effects, "Slow Pulse") == 2 + + +def test_find_effect_index_case_insensitive() -> None: + effects = _make_effects("Fast Pulse") + assert find_effect_index(effects, "fast pulse") == 1 + assert find_effect_index(effects, "FAST PULSE") == 1 + + +def test_find_effect_index_not_found() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert find_effect_index(effects, "Missing") is None + + +def test_find_effect_index_empty() -> None: + assert find_effect_index([], "anything") is None + + +# --- available_effects_str --- + + +def test_available_effects_str_multiple() -> None: + effects = _make_effects("Fast Pulse", "Slow Pulse") + assert available_effects_str(effects) == "'Fast Pulse', 'Slow Pulse'" + + +def test_available_effects_str_single() -> None: + effects = _make_effects("Fast Pulse") + assert available_effects_str(effects) == "'Fast Pulse'" + + +def test_available_effects_str_empty() -> None: + assert available_effects_str([]) == "none" + + +# --- _final_validate --- + + +def _setup_final_validate( + effect_refs: list[EffectRef], + light_configs: list[ConfigType], + declare_ids: list[tuple[ID, list[str | int]]], +) -> Token: + """Set up CORE.data and fv.full_config for _final_validate tests.""" + data = _get_data() + data.effect_refs = effect_refs + + full_conf = Config() + full_conf["light"] = light_configs + for id_, path in declare_ids: + full_conf.declare_ids.append((id_, path)) + + return fv.full_config.set(full_conf) + + +def test_final_validate_valid_effect() -> None: + """Valid effect name should not raise.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Fast Pulse", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_invalid_effect_raises() -> None: + """Invalid effect name should raise FinalExternalInvalid.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Nonexistent", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="Nonexistent"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_lists_available_effects() -> None: + """Error message should list available effects.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Missing", component_path=["esphome"] + ), + ], + light_configs=[ + {CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse", "Slow Pulse")} + ], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="'Fast Pulse', 'Slow Pulse'"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_no_effects_on_light() -> None: + """Light with no effects should report 'none' as available.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Missing", component_path=["esphome"] + ), + ], + light_configs=[{CONF_ID: light_id}], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + with pytest.raises(cv.FinalExternalInvalid, match="Available effects: none"): + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_no_refs_is_noop() -> None: + """No stored refs should pass without error.""" + data = _get_data() + data.effect_refs = [] + _final_validate({}) + + +def test_final_validate_unknown_light_id_skipped() -> None: + """Refs to unknown light IDs should be silently skipped.""" + data = _get_data() + data.effect_refs = [ + EffectRef( + light_id=ID("nonexistent", is_declaration=True), + effect_name="Missing", + component_path=["esphome"], + ) + ] + + full_conf = Config() + token = fv.full_config.set(full_conf) + try: + _final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_final_validate_drains_refs() -> None: + """Refs should be drained after validation to avoid redundant runs.""" + light_id = ID("led1", is_declaration=True) + token = _setup_final_validate( + effect_refs=[ + EffectRef( + light_id=light_id, effect_name="Fast Pulse", component_path=["esphome"] + ), + ], + light_configs=[{CONF_ID: light_id, CONF_EFFECTS: _make_effects("Fast Pulse")}], + declare_ids=[(light_id, ["light", 0, CONF_ID])], + ) + try: + _final_validate({}) + assert _get_data().effect_refs == [] + finally: + fv.full_config.reset(token) + + +# --- _record_effect_ref --- + + +@pytest.fixture +def _path_ctx() -> Generator[None]: + """Set path_context for _record_effect_ref tests.""" + token = path_context.set(["esphome"]) + yield + path_context.reset(token) + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_static() -> None: + """Static effect name should be recorded.""" + light_id = ID("led1", is_declaration=True) + config: ConfigType = {CONF_ID: light_id, CONF_EFFECT: "Fast Pulse"} + result = _record_effect_ref(config) + assert result is config + data = _get_data() + assert len(data.effect_refs) == 1 + assert data.effect_refs[0].effect_name == "Fast Pulse" + assert data.effect_refs[0].light_id is light_id + assert data.effect_refs[0].component_path == ["esphome"] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_lambda() -> None: + """Lambda effect should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: Lambda("return effect;"), + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_none() -> None: + """Effect 'None' should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: "None", + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +@pytest.mark.usefixtures("_path_ctx") +def test_record_effect_ref_skips_none_case_insensitive() -> None: + """Effect 'none' (lowercase) should not be recorded.""" + config: ConfigType = { + CONF_ID: ID("led1", is_declaration=True), + CONF_EFFECT: "none", + } + _record_effect_ref(config) + assert _get_data().effect_refs == [] + + +def test_record_effect_ref_skips_no_effect_key() -> None: + """Config without effect key should be a no-op.""" + config: ConfigType = {CONF_ID: ID("led1", is_declaration=True)} + _record_effect_ref(config) + assert _get_data().effect_refs == [] From 90dafa3fa45bc5b279136f069030e1ea4edde780 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 26 Mar 2026 15:59:58 -1000 Subject: [PATCH 1017/2056] [logger] Warn when VERBOSE/VERY_VERBOSE logging is active (#15189) --- esphome/components/logger/logger.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index cd6543bfb84..23b69c36c60 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -243,6 +243,16 @@ void Logger::dump_config() { #endif #ifdef USE_ZEPHYR dump_crash_(); +#endif + // Warn users that VERBOSE/VERY_VERBOSE logging impacts performance. + // Only the compiled log level matters — all log calls up to this level + // are in the binary and will be formatted (vsnprintf) and block UART. +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + ESP_LOGW(TAG, "VERY_VERBOSE logging is active — significant performance impact, short-term debugging only\n" + " May cause connection instability. Set log level to DEBUG or lower for long-term use."); +#elif ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGI(TAG, "VERBOSE logging is active — performance impact, short-term debugging only\n" + " Set log level to DEBUG or lower for long-term use."); #endif } From 6feb2d04dfd61c3fe1d03980fe1516b846eacf6b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 27 Mar 2026 04:36:35 +0100 Subject: [PATCH 1018/2056] [nextion] Replace `static std::string COMMAND_DELIMITER` with `constexpr` (#15195) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 13 +++++++++---- esphome/components/nextion/nextion.h | 2 -- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 612bfbc968c..fa1582c209f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -10,6 +10,10 @@ namespace nextion { static const char *const TAG = "nextion"; +// Nextion command terminator: three consecutive 0xFF bytes (per Nextion Instruction Set v1.1). +static constexpr uint8_t COMMAND_DELIMITER[3] = {0xFF, 0xFF, 0xFF}; +static constexpr size_t DELIMITER_SIZE = sizeof(COMMAND_DELIMITER); + void Nextion::setup() { this->is_setup_ = false; this->connection_state_.ignore_is_setup_ = true; @@ -415,7 +419,8 @@ void Nextion::process_nextion_commands_() { #ifdef NEXTION_PROTOCOL_LOG this->print_queue_members_(); #endif - while ((to_process_length = this->command_data_.find(COMMAND_DELIMITER)) != std::string::npos) { + while ((to_process_length = this->command_data_.find(reinterpret_cast<const char *>(COMMAND_DELIMITER), 0, + DELIMITER_SIZE)) != std::string::npos) { #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP if (++commands_processed > this->max_commands_per_loop_) { ESP_LOGW(TAG, "Command processing limit exceeded"); @@ -423,8 +428,8 @@ void Nextion::process_nextion_commands_() { } #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP ESP_LOGN(TAG, "queue size: %zu", this->nextion_queue_.size()); - while (to_process_length + COMMAND_DELIMITER.length() < this->command_data_.length() && - static_cast<uint8_t>(this->command_data_[to_process_length + COMMAND_DELIMITER.length()]) == 0xFF) { + while (to_process_length + DELIMITER_SIZE < this->command_data_.length() && + static_cast<uint8_t>(this->command_data_[to_process_length + DELIMITER_SIZE]) == 0xFF) { ++to_process_length; ESP_LOGN(TAG, "Add 0xFF"); } @@ -829,7 +834,7 @@ void Nextion::process_nextion_commands_() { break; } - this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); + this->command_data_.erase(0, to_process_length + DELIMITER_SIZE + 1); } const uint32_t ms = App.get_loop_component_start_time(); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index bb5998cf5dd..217d2e605d2 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -29,8 +29,6 @@ class NextionComponentBase; using nextion_writer_t = display::DisplayWriter<Nextion>; -static const std::string COMMAND_DELIMITER{static_cast<char>(255), static_cast<char>(255), static_cast<char>(255)}; - #ifdef USE_NEXTION_COMMAND_SPACING class NextionCommandPacer { public: From 2d9922496cd94ed43a0a5eec7193ff9f581c48a8 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:02:45 +0100 Subject: [PATCH 1019/2056] [git] Add support for subpath to computed destination directory (#15135) --- esphome/git.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/git.py b/esphome/git.py index a45768b5cd6..096ff483a71 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -102,6 +102,7 @@ def clone_or_update( username: str = None, password: str = None, submodules: list[str] | None = None, + subpath: Path | None = None, _recover_broken: bool = True, ) -> tuple[Path, Callable[[], None] | None]: key = f"{url}@{ref}" @@ -112,6 +113,9 @@ def clone_or_update( ) repo_dir = _compute_destination_path(key, domain) + if subpath: + repo_dir = repo_dir / subpath + if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) From 73e939ffb5fa41be407812fa069be76f45d968e6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:13:24 -0400 Subject: [PATCH 1020/2056] [sgp4x] Fix NOx index_offset default (should be 1, not 100) (#15212) --- esphome/components/sgp4x/sensor.py | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index ab78ab59d9f..8d52ffb4f26 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -44,20 +44,27 @@ def validate_sensors(config): return config -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, - cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, - } - ) - } -) +def _gas_sensor_schema(index_offset_default: int): + return cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional( + CONF_INDEX_OFFSET, default=index_offset_default + ): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } + ) + + +VOC_SENSOR = _gas_sensor_schema(100) +NOX_SENSOR = _gas_sensor_schema(1) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -68,13 +75,13 @@ CONFIG_SCHEMA = cv.All( accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(VOC_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( From 1e65165e48274acfd30a8cfd18867608b6dff414 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:19:58 -1000 Subject: [PATCH 1021/2056] [safe_mode] Migrate SafeModeTrigger to callback automation (#15197) --- esphome/components/safe_mode/__init__.py | 13 ++++--------- esphome/components/safe_mode/automation.h | 10 ---------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index e868985054e..da36d21eb7f 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, - CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -20,7 +19,6 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) -SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) @@ -43,11 +41,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger), - } - ), + cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _remove_id_if_disabled, @@ -80,8 +74,9 @@ async def to_code(config): if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") for conf in on_safe_mode_config: - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_safe_mode_callback", [], conf + ) condition = var.should_enter_safe_mode( config[CONF_NUM_ATTEMPTS], diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index dee02c64a07..79b53c08812 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,19 +1,9 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "safe_mode.h" namespace esphome::safe_mode { -#ifdef USE_SAFE_MODE_CALLBACK -class SafeModeTrigger final : public Trigger<> { - public: - explicit SafeModeTrigger(SafeModeComponent *parent) { - parent->add_on_safe_mode_callback([this]() { trigger(); }); - } -}; -#endif // USE_SAFE_MODE_CALLBACK - template<typename... Ts> class MarkSuccessfulAction : public Action<Ts...>, public Parented<SafeModeComponent> { public: void play(const Ts &...x) override { this->parent_->mark_successful(); } From b0f6a94df51a40c163627aa7e12a1c3947369573 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:20:11 -1000 Subject: [PATCH 1022/2056] [sml] Migrate DataTrigger to callback automation (#15233) --- esphome/components/sml/__init__.py | 23 +++++------------------ esphome/components/sml/automation.h | 19 ------------------- 2 files changed, 5 insertions(+), 37 deletions(-) delete mode 100644 esphome/components/sml/automation.h diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index eaeddce3905..1bf0d97d65e 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_DATA, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA CODEOWNERS = ["@alengwenus"] @@ -18,24 +18,11 @@ CONF_SML_ID = "sml_id" CONF_OBIS_CODE = "obis_code" CONF_SERVER_ID = "server_id" -sml_ns = cg.esphome_ns.namespace("sml") - -DataTrigger = sml_ns.class_( - "DataTrigger", - automation.Trigger.template( - cg.std_vector.template(cg.uint8).operator("ref"), cg.bool_ - ), -) - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sml), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ).extend(uart.UART_DEVICE_SCHEMA) @@ -45,9 +32,9 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_data_callback", [ ( cg.std_vector.template(cg.uint8).operator("ref").operator("const"), diff --git a/esphome/components/sml/automation.h b/esphome/components/sml/automation.h deleted file mode 100644 index d51063065d1..00000000000 --- a/esphome/components/sml/automation.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "sml.h" - -#include <vector> - -namespace esphome { -namespace sml { - -class DataTrigger : public Trigger<const std::vector<uint8_t> &, bool> { - public: - explicit DataTrigger(Sml *sml) { - sml->add_on_data_callback([this](const std::vector<uint8_t> &data, bool valid) { this->trigger(data, valid); }); - } -}; - -} // namespace sml -} // namespace esphome From b41634e19af272b8c146d3912edf2cba3191b2eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:20:24 -1000 Subject: [PATCH 1023/2056] [alarm_control_panel] Migrate triggers to callback automation (#15198) --- .../alarm_control_panel/__init__.py | 162 +++++------------- .../alarm_control_panel.cpp | 4 +- .../alarm_control_panel/alarm_control_panel.h | 4 +- .../alarm_control_panel/automation.h | 69 ++------ .../mqtt/mqtt_alarm_control_panel.cpp | 3 +- 5 files changed, 68 insertions(+), 174 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index aefb18d25cc..4ee073a15b1 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_STATE, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready" alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) -StateTrigger = alarm_control_panel_ns.class_( - "StateTrigger", automation.Trigger.template() -) -TriggeredTrigger = alarm_control_panel_ns.class_( - "TriggeredTrigger", automation.Trigger.template() -) -ClearedTrigger = alarm_control_panel_ns.class_( - "ClearedTrigger", automation.Trigger.template() -) -ArmingTrigger = alarm_control_panel_ns.class_( - "ArmingTrigger", automation.Trigger.template() -) -PendingTrigger = alarm_control_panel_ns.class_( - "PendingTrigger", automation.Trigger.template() -) -ArmedHomeTrigger = alarm_control_panel_ns.class_( - "ArmedHomeTrigger", automation.Trigger.template() -) -ArmedNightTrigger = alarm_control_panel_ns.class_( - "ArmedNightTrigger", automation.Trigger.template() -) -ArmedAwayTrigger = alarm_control_panel_ns.class_( - "ArmedAwayTrigger", automation.Trigger.template() -) -DisarmedTrigger = alarm_control_panel_ns.class_( - "DisarmedTrigger", automation.Trigger.template() -) -ChimeTrigger = alarm_control_panel_ns.class_( - "ChimeTrigger", automation.Trigger.template() -) -ReadyTrigger = alarm_control_panel_ns.class_( - "ReadyTrigger", automation.Trigger.template() -) +StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder") +StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder") +AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState") ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) @@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTAlarmControlPanelComponent ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), - } - ), - cv.Optional(CONF_ON_ARMING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), - } - ), - cv.Optional(CONF_ON_PENDING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), - } - ), - cv.Optional(CONF_ON_DISARMED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), - } - ), - cv.Optional(CONF_ON_CLEARED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), - } - ), - cv.Optional(CONF_ON_CHIME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), - } - ), - cv.Optional(CONF_ON_READY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMING): automation.validate_automation({}), + cv.Optional(CONF_ON_PENDING): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}), + cv.Optional(CONF_ON_CHIME): automation.validate_automation({}), + cv.Optional(CONF_ON_READY): automation.validate_automation({}), } ) ) @@ -189,38 +114,39 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( @setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TRIGGERED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PENDING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_HOME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_NIGHT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_AWAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_DISARMED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder + ) + _STATE_ENTER_MAP = { + CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED, + CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING, + CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING, + CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME, + CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT, + CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY, + CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED, + } + for conf_key, state_enum in _STATE_ENTER_MAP.items(): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=StateEnterForwarder.template(state_enum), + ) for conf in config.get(CONF_ON_CLEARED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_cleared_callback", [], conf + ) for conf in config.get(CONF_ON_CHIME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_chime_callback", [], conf + ) for conf in config.get(CONF_ON_READY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_ready_callback", [], conf + ) if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) if mqtt_id := config.get(CONF_MQTT_ID): diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 623241851ae..fc72c13ce30 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -35,8 +35,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - // Single state callback - triggers check get_state() for specific states - this->state_callback_.call(); + // Single state callback - listeners receive the new state as an argument + this->state_callback_.call(state); #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index cf99d359e75..e748b8621b9 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -145,8 +145,8 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // state callback - triggers check get_state() for specific state - LazyCallbackManager<void()> state_callback_{}; + // state callback - passes the new state to listeners + LazyCallbackManager<void(AlarmControlPanelState)> state_callback_{}; // clear callback - fires when leaving TRIGGERED state LazyCallbackManager<void()> cleared_callback_{}; // chime callback diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 4ff34de0d5e..022d2650d2d 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -5,60 +5,27 @@ namespace esphome::alarm_control_panel { -/// Trigger on any state change -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); +/// Callback forwarder that triggers an Automation<> on any state change. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +struct StateAnyForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template<AlarmControlPanelState State> struct StateEnterForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState state) const { + if (state == State) + this->automation->trigger(); } }; -/// Template trigger that fires when entering a specific state -template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> { - public: - explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == State) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -// Type aliases for state-specific triggers -using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>; -using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>; -using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>; -using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>; -using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>; -using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>; -using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>; - -/// Trigger when leaving TRIGGERED state (alarm cleared) -class ClearedTrigger : public Trigger<> { - public: - explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on chime event (zone opened while disarmed) -class ChimeTrigger : public Trigger<> { - public: - explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on ready state change -class ReadyTrigger : public Trigger<> { - public: - explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); - } -}; +static_assert(sizeof(StateAnyForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<StateAnyForwarder>); +static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>); template<typename... Ts> class ArmAwayAction : public Action<Ts...> { public: diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 74a60b3624b..f059360e231 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -48,7 +48,8 @@ static bool apply_command(AlarmControlPanelCall &call, const char *state) { } void MQTTAlarmControlPanelComponent::setup() { - this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->alarm_control_panel_->add_on_state_callback( + [this](AlarmControlPanelState /*state*/) { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); if (!payload.empty() && payload[0] == '{') { From dea8fdd906a7fc79648d05dfeb74d6aaadad55e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:20:35 -1000 Subject: [PATCH 1024/2056] [lock] Migrate LockStateTrigger to callback automation (#15199) --- esphome/components/copy/lock/copy_lock.cpp | 2 +- esphome/components/lock/__init__.py | 34 ++++++++++------------ esphome/components/lock/automation.h | 22 ++++++-------- esphome/components/lock/lock.cpp | 2 +- esphome/components/lock/lock.h | 4 +-- esphome/components/mqtt/mqtt_lock.cpp | 3 +- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 25bd8c33ef6..c846954510f 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.lock"; void CopyLock::setup() { - source_->add_on_state_callback([this]() { this->publish_state(source_->state); }); + source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); }); traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index fe4db23ae3a..0df4b20cbae 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -31,8 +30,7 @@ OpenAction = lock_ns.class_("OpenAction", automation.Action) LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action) LockCondition = lock_ns.class_("LockCondition", Condition) -LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) -LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) +LockStateForwarder = lock_ns.class_("LockStateForwarder") LockState = lock_ns.enum("LockState") @@ -52,16 +50,8 @@ _LOCK_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), - cv.Optional(CONF_ON_LOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), - } - ), - cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), - } - ), + cv.Optional(CONF_ON_LOCK): automation.validate_automation({}), + cv.Optional(CONF_ON_UNLOCK): automation.validate_automation({}), } ) ) @@ -93,12 +83,18 @@ def lock_schema( @setup_entity("lock") async def _setup_lock_core(var, config): - for conf in config.get(CONF_ON_LOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_UNLOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, state_enum in ( + (CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED), + (CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=LockStateForwarder.template(state_enum), + ) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 6f3c4226933..c140bc568fb 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,21 +49,17 @@ template<typename... Ts> class LockCondition : public Condition<Ts...> { bool state_; }; -template<LockState State> class LockStateTrigger : public Trigger<> { - public: - explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { - a_lock->add_on_state_callback([this]() { - if (this->lock_->state == State) { - this->trigger(); - } - }); +/// Callback forwarder that triggers an Automation<> only when a specific lock state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template<LockState State> struct LockStateForwarder { + Automation<> *automation; + void operator()(LockState state) const { + if (state == State) + this->automation->trigger(); } - - protected: - Lock *lock_; }; -using LockLockTrigger = LockStateTrigger<LockState::LOCK_STATE_LOCKED>; -using LockUnlockTrigger = LockStateTrigger<LockState::LOCK_STATE_UNLOCKED>; +static_assert(sizeof(LockStateForwarder<LockState::LOCK_STATE_LOCKED>) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<LockStateForwarder<LockState::LOCK_STATE_LOCKED>>); } // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 90937485b9e..3ff131af3d5 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -42,7 +42,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); - this->state_callback_.call(); + this->state_callback_.call(state); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); #endif diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 707431d5433..543a4b51a89 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -148,7 +148,7 @@ class Lock : public EntityBase { /** Set callback for state changes. * - * @param callback The void(bool) callback. + * @param callback The void(LockState) callback. */ template<typename F> void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward<F>(callback)); @@ -178,7 +178,7 @@ class Lock : public EntityBase { */ virtual void control(const LockCall &call) = 0; - LazyCallbackManager<void()> state_callback_{}; + LazyCallbackManager<void(LockState)> state_callback_{}; Deduplicator<LockState> publish_dedup_; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 45d8e4698f5..7920187f922 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -28,7 +28,8 @@ void MQTTLockComponent::setup() { this->status_momentary_warning("state", 5000); } }); - this->lock_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); + this->lock_->add_on_state_callback( + [this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTLockComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str()); From 2e42547d32edce07150f925e7bc8fd0c03f7b814 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:20:46 -1000 Subject: [PATCH 1025/2056] [media_player] Migrate triggers to callback automation (#15200) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/media_player/__init__.py | 42 ++++++++++--------- esphome/components/media_player/automation.h | 41 ++++++++---------- .../components/media_player/media_player.cpp | 2 +- .../components/media_player/media_player.h | 2 +- .../voice_assistant/voice_assistant.cpp | 4 +- 5 files changed, 44 insertions(+), 47 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index a5baca29940..767916ad88d 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_TRIGGER_ID, CONF_VOLUME, ) from esphome.core import CORE @@ -65,15 +64,19 @@ _COMMAND_ACTIONS = [ "clear_playlist", ] -# State triggers: (config_key, C++ class name) +StateAnyForwarder = media_player_ns.class_("StateAnyForwarder") +StateEnterForwarder = media_player_ns.class_("StateEnterForwarder") +MediaPlayerState = media_player_ns.enum("MediaPlayerState") + +# State triggers: (config_key, state enum or None for any-state) _STATE_TRIGGERS = [ - (CONF_ON_STATE, "StateTrigger"), - (CONF_ON_IDLE, "IdleTrigger"), - (CONF_ON_PLAY, "PlayTrigger"), - (CONF_ON_PAUSE, "PauseTrigger"), - (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), - (CONF_ON_TURN_ON, "OnTrigger"), - (CONF_ON_TURN_OFF, "OffTrigger"), + (CONF_ON_STATE, None), + (CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE), + (CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING), + (CONF_ON_PAUSE, MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED), + (CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING), + (CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON), + (CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF), ] # State conditions that all share the same schema and codegen handler @@ -98,10 +101,15 @@ VolumeSetAction = media_player_ns.class_( @setup_entity("media_player") async def setup_media_player_core_(var, config): - for conf_key, _ in _STATE_TRIGGERS: + for conf_key, state_enum in _STATE_TRIGGERS: for conf in config.get(conf_key, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if state_enum is None: + forwarder = StateAnyForwarder + else: + forwarder = StateEnterForwarder.template(state_enum) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) async def register_media_player(var, config): @@ -120,14 +128,8 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(conf_key): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - media_player_ns.class_(class_name, automation.Trigger.template()) - ), - } - ) - for conf_key, class_name in _STATE_TRIGGERS + cv.Optional(conf_key): automation.validate_automation({}) + for conf_key, _ in _STATE_TRIGGERS } ) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 031f6657f49..658381ef90d 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -71,32 +71,27 @@ template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Pa void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this]() { this->trigger(); }); +/// Callback forwarder that triggers an Automation<> on any state change. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +struct StateAnyForwarder { + Automation<> *automation; + void operator()(MediaPlayerState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when a specific media player state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template<MediaPlayerState State> struct StateEnterForwarder { + Automation<> *automation; + void operator()(MediaPlayerState state) const { + if (state == State) + this->automation->trigger(); } }; -template<MediaPlayerState State> class MediaPlayerStateTrigger : public Trigger<> { - public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { - player->add_on_state_callback([this]() { - if (this->player_->state == State) - this->trigger(); - }); - } - - protected: - MediaPlayer *player_; -}; - -using IdleTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>; -using PlayTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING>; -using PauseTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED>; -using AnnouncementTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING>; -using OnTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_ON>; -using OffTrigger = MediaPlayerStateTrigger<MediaPlayerState::MEDIA_PLAYER_STATE_OFF>; +static_assert(sizeof(StateAnyForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<StateAnyForwarder>); +static_assert(sizeof(StateEnterForwarder<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v<StateEnterForwarder<MediaPlayerState::MEDIA_PLAYER_STATE_IDLE>>); template<typename... Ts> class IsIdleCondition : public Condition<Ts...>, public Parented<MediaPlayer> { public: diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a0eb7b55007..48d23fa0b12 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -199,7 +199,7 @@ MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { } void MediaPlayer::publish_state() { - this->state_callback_.call(); + this->state_callback_.call(this->state); #if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_media_player_update(this); #endif diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 26eca469e7f..d5d00207970 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -168,7 +168,7 @@ class MediaPlayer : public EntityBase { virtual void control(const MediaPlayerCall &call) = 0; - LazyCallbackManager<void()> state_callback_{}; + LazyCallbackManager<void(MediaPlayerState)> state_callback_{}; }; } // namespace media_player diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 15124e422ff..ddce606b2c4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -39,8 +39,8 @@ void VoiceAssistant::setup() { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - this->media_player_->add_on_state_callback([this]() { - switch (this->media_player_->state) { + this->media_player_->add_on_state_callback([this](media_player::MediaPlayerState state) { + switch (state) { case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING: if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) { // State changed to announcing after receiving the url From a2d452684a0cc6620e0a3e1bce746b0ef6a80ff3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:21:03 -1000 Subject: [PATCH 1026/2056] [ld2450] Migrate LD2450DataTrigger to callback automation (#15201) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/ld2450/__init__.py | 14 +++++--------- esphome/components/ld2450/ld2450.h | 8 -------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 5854a5794c4..37bf12bafc9 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -12,7 +12,6 @@ MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) -LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template()) CONF_LD2450_ID = "ld2450_id" @@ -23,11 +22,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_THROTTLE): cv.invalid( f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -54,5 +49,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_data_callback", [], conf + ) diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e774dd9c756..cbcdec10b31 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR @@ -201,11 +200,4 @@ class LD2450Component : public Component, public uart::UARTDevice { LazyCallbackManager<void()> data_callback_; }; -class LD2450DataTrigger : public Trigger<> { - public: - explicit LD2450DataTrigger(LD2450Component *parent) { - parent->add_on_data_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::ld2450 From 83b3187126be87ac2d7a97db9dd340d8679180e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:21:16 -1000 Subject: [PATCH 1027/2056] [rtttl] Migrate FinishedPlaybackTrigger to callback automation (#15202) --- esphome/components/rtttl/__init__.py | 25 +++++-------------------- esphome/components/rtttl/rtttl.h | 7 ------- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 35667342006..638e950ba63 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -5,14 +5,7 @@ import esphome.codegen as cg from esphome.components.output import FloatOutput from esphome.components.speaker import Speaker import esphome.config_validation as cv -from esphome.const import ( - CONF_GAIN, - CONF_ID, - CONF_OUTPUT, - CONF_PLATFORM, - CONF_SPEAKER, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_GAIN, CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_SPEAKER import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -26,9 +19,6 @@ rtttl_ns = cg.esphome_ns.namespace("rtttl") Rtttl = rtttl_ns.class_("Rtttl", cg.Component) PlayAction = rtttl_ns.class_("PlayAction", automation.Action) StopAction = rtttl_ns.class_("StopAction", automation.Action) -FinishedPlaybackTrigger = rtttl_ns.class_( - "FinishedPlaybackTrigger", automation.Trigger.template() -) IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) MULTI_CONF = True @@ -40,13 +30,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput), cv.Optional(CONF_SPEAKER): cv.use_id(Speaker), cv.Optional(CONF_GAIN, default="0.6"): cv.percentage, - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER), @@ -103,8 +87,9 @@ async def to_code(config): cg.add(var.set_gain(config[CONF_GAIN])) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_playback_callback", [], conf + ) @automation.register_action( diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bff43d2edd8..98ed9ba1bf4 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -131,11 +131,4 @@ template<typename... Ts> class IsPlayingCondition : public Condition<Ts...>, pub bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; -class FinishedPlaybackTrigger : public Trigger<> { - public: - explicit FinishedPlaybackTrigger(Rtttl *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::rtttl From 4493d2efb6582f020b6ed73879ab56566c088779 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:21:27 -1000 Subject: [PATCH 1028/2056] [online_image] Migrate triggers to callback automation (#15216) --- esphome/components/online_image/__init__.py | 41 ++++--------------- .../components/online_image/online_image.h | 14 ------- 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 292e2bb3bb2..5b8294c70ee 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -7,14 +7,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv -from esphome.const import ( - CONF_BUFFER_SIZE, - CONF_ID, - CONF_ON_ERROR, - CONF_TRIGGER_ID, - CONF_TYPE, - CONF_URL, -) +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL from esphome.core import Lambda AUTO_LOAD = ["image", "runtime_image"] @@ -41,14 +34,6 @@ ReleaseImageAction = online_image_ns.class_( "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) ) -# Triggers -DownloadFinishedTrigger = online_image_ns.class_( - "DownloadFinishedTrigger", automation.Trigger.template() -) -DownloadErrorTrigger = online_image_ns.class_( - "DownloadErrorTrigger", automation.Trigger.template() -) - ONLINE_IMAGE_SCHEMA = ( runtime_image.runtime_image_schema(OnlineImage) @@ -61,18 +46,8 @@ ONLINE_IMAGE_SCHEMA = ( cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DownloadFinishedTrigger - ), - } - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), - } - ), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}), + cv.Optional(CONF_ON_ERROR): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("never")) @@ -165,9 +140,11 @@ async def to_code(config): cg.add(var.add_request_header(key, value)) for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "cached")], conf) + await automation.build_callback_automation( + var, "add_on_finished_callback", [(bool, "cached")], conf + ) for conf in config.get(CONF_ON_ERROR, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_error_callback", [], conf + ) diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3a348cbb07a..816d6525eaa 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -129,18 +129,4 @@ template<typename... Ts> class OnlineImageReleaseAction : public Action<Ts...> { OnlineImage *parent_; }; -class DownloadFinishedTrigger : public Trigger<bool> { - public: - explicit DownloadFinishedTrigger(OnlineImage *parent) { - parent->add_on_finished_callback([this](bool cached) { this->trigger(cached); }); - } -}; - -class DownloadErrorTrigger : public Trigger<> { - public: - explicit DownloadErrorTrigger(OnlineImage *parent) { - parent->add_on_error_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::online_image From 54283a2599cf5f6958bbb329745afd0dbb800f2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:21:41 -1000 Subject: [PATCH 1029/2056] [rotary_encoder] Migrate triggers to callback automation (#15217) --- .../rotary_encoder/rotary_encoder.h | 14 -------- esphome/components/rotary_encoder/sensor.py | 34 +++++-------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4b776fe55e9..6f4a4fd83ce 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -118,19 +118,5 @@ template<typename... Ts> class RotaryEncoderSetValueAction : public Action<Ts... RotaryEncoderSensor *encoder_; }; -class RotaryEncoderClockwiseTrigger : public Trigger<> { - public: - explicit RotaryEncoderClockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_clockwise_callback([this]() { this->trigger(); }); - } -}; - -class RotaryEncoderAnticlockwiseTrigger : public Trigger<> { - public: - explicit RotaryEncoderAnticlockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_anticlockwise_callback([this]() { this->trigger(); }); - } -}; - } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index be315db55d2..e64e44f7c13 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_PIN_B, CONF_RESOLUTION, CONF_RESTORE_MODE, - CONF_TRIGGER_ID, CONF_VALUE, ICON_ROTATE_RIGHT, UNIT_STEPS, @@ -43,13 +42,6 @@ RotaryEncoderSetValueAction = rotary_encoder_ns.class_( "RotaryEncoderSetValueAction", automation.Action ) -RotaryEncoderClockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderClockwiseTrigger", automation.Trigger -) -RotaryEncoderAnticlockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderAnticlockwiseTrigger", automation.Trigger -) - def validate_min_max_value(config): if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: @@ -81,20 +73,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_ZERO"): cv.enum( RESTORE_MODES, upper=True, space="_" ), - cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderClockwiseTrigger - ), - } - ), - cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderAnticlockwiseTrigger - ), - } - ), + cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation({}), + cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation({}), } ) .extend(cv.COMPONENT_SCHEMA), @@ -123,11 +103,13 @@ async def to_code(config): cg.add(var.set_max_value(config[CONF_MAX_VALUE])) for conf in config.get(CONF_ON_CLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_clockwise_callback", [], conf + ) for conf in config.get(CONF_ON_ANTICLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_anticlockwise_callback", [], conf + ) @automation.register_action( From 514df6c99af94915523d26e45ad489d4a5ff60d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:21:52 -1000 Subject: [PATCH 1030/2056] [dfplayer] Migrate FinishedPlaybackTrigger to callback automation (#15218) --- esphome/components/dfplayer/__init__.py | 18 +++++------------- esphome/components/dfplayer/dfplayer.h | 7 ------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 9df108c9c0a..c49420f0605 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -2,16 +2,13 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_TRIGGER_ID, CONF_VOLUME +from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] dfplayer_ns = cg.esphome_ns.namespace("dfplayer") DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component) -DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_( - "DFPlayerFinishedPlaybackTrigger", automation.Trigger.template() -) DFPlayerIsPlayingCondition = dfplayer_ns.class_( "DFPlayerIsPlayingCondition", automation.Condition ) @@ -58,13 +55,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(DFPlayer), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DFPlayerFinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(uart.UART_DEVICE_SCHEMA) ) @@ -79,8 +70,9 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_playback_callback", [], conf + ) @automation.register_action( diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 2c4ee03470d..0d240566c3d 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -171,12 +171,5 @@ template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts. bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; -class DFPlayerFinishedPlaybackTrigger : public Trigger<> { - public: - explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace dfplayer } // namespace esphome From 623408bbfe2bff8c41964b73afa2a23fbd208e10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:02 -1000 Subject: [PATCH 1031/2056] [hlk_fm22x] Migrate triggers to callback automation (#15219) --- esphome/components/hlk_fm22x/__init__.py | 109 ++++++----------------- esphome/components/hlk_fm22x/hlk_fm22x.h | 46 ---------- 2 files changed, 28 insertions(+), 127 deletions(-) diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index cb6d5cdfd6d..c0349319d1f 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_NAME, CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_FAILED, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund"] @@ -28,33 +27,6 @@ HlkFm22xComponent = hlk_fm22x_ns.class_( "HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice ) -FaceScanMatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string) -) - -FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanUnmatchedTrigger", automation.Trigger.template() -) - -FaceScanInvalidTrigger = hlk_fm22x_ns.class_( - "FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8) -) - -FaceInfoTrigger = hlk_fm22x_ns.class_( - "FaceInfoTrigger", - automation.Trigger.template( - cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16 - ), -) - -EnrollmentDoneTrigger = hlk_fm22x_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8) -) - -EnrollmentFailedTrigger = hlk_fm22x_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8) -) - EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action) DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action) DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action) @@ -65,46 +37,14 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(HlkFm22xComponent), - cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanMatchedTrigger - ), - } - ), + cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation({}), cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanUnmatchedTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation({}), + cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("50ms")) @@ -118,23 +58,27 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf + await automation.build_callback_automation( + var, + "add_on_face_scan_matched_callback", + [(cg.int16, "face_id"), (cg.std_string, "name")], + conf, ) for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf + ) for conf in config.get(CONF_ON_FACE_INFO, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_face_info_callback", [ (cg.int16, "status"), (cg.int16, "left"), @@ -149,14 +93,17 @@ async def to_code(config): ) for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_done_callback", + [(cg.int16, "face_id"), (cg.uint8, "direction")], + conf, ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf + ) @automation.register_action( diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index d897d51881b..fd8257b435a 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -141,52 +141,6 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { CallbackManager<void(uint8_t)> enrollment_failed_callback_; }; -class FaceScanMatchedTrigger : public Trigger<int16_t, std::string> { - public: - explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_matched_callback( - [this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); }); - } -}; - -class FaceScanUnmatchedTrigger : public Trigger<> { - public: - explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FaceScanInvalidTrigger : public Trigger<uint8_t> { - public: - explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - -class FaceInfoTrigger : public Trigger<int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t> { - public: - explicit FaceInfoTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_info_callback( - [this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch, - int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger<int16_t, uint8_t> { - public: - explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_done_callback( - [this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger<uint8_t> { - public: - explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<HlkFm22xComponent> { public: TEMPLATABLE_VALUE(std::string, name) From a4a8fa3027088c2a9c3ef8cc96c004cfabfe36b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:14 -1000 Subject: [PATCH 1032/2056] [pn532] Migrate PN532OnFinishedWriteTrigger to callback automation (#15220) --- esphome/components/pn532/__init__.py | 17 ++++------------- esphome/components/pn532/pn532.h | 7 ------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 6f679ed10a9..4ccda49a727 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -19,10 +19,6 @@ CONF_PN532_ID = "pn532_id" pn532_ns = cg.esphome_ns.namespace("pn532") PN532 = pn532_ns.class_("PN532", cg.PollingComponent) -PN532OnFinishedWriteTrigger = pn532_ns.class_( - "PN532OnFinishedWriteTrigger", automation.Trigger.template() -) - PN532IsWritingCondition = pn532_ns.class_( "PN532IsWritingCondition", automation.Condition ) @@ -35,13 +31,7 @@ PN532_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), } ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN532OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -77,8 +67,9 @@ async def setup_pn532(var, config): ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 1f6a6b3bc3f..b76cbb19462 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -133,13 +133,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN532OnFinishedWriteTrigger(PN532 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template<typename... Ts> class PN532IsWritingCondition : public Condition<Ts...>, public Parented<PN532> { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } From 985477f2cfa40cc31a97f3169364b73db4d34a91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:25 -1000 Subject: [PATCH 1033/2056] [pn7150][pn7160] Migrate triggers to callback automation (#15221) --- esphome/components/pn7150/__init__.py | 34 ++++++-------------------- esphome/components/pn7150/automation.h | 14 ----------- esphome/components/pn7160/__init__.py | 34 ++++++-------------------- esphome/components/pn7160/automation.h | 14 ----------- 4 files changed, 16 insertions(+), 80 deletions(-) diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 6af14128818..c8723dc31cd 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -50,14 +50,6 @@ SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) -PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( - "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7150OnFinishedWriteTrigger = pn7150_ns.class_( - "PN7150OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7150IsWritingCondition = pn7150_ns.class_( "PN7150IsWritingCondition", automation.Condition ) @@ -83,20 +75,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7150_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7150), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -215,12 +195,14 @@ async def setup_pn7150(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index 21329a998a2..a8c65ae633a 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7150 { -class PN7150OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7150OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template<typename... Ts> class PN7150IsWritingCondition : public Condition<Ts...>, public Parented<PN7150> { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 54e4b74796b..e382594b938 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -52,14 +52,6 @@ SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) -PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( - "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7160OnFinishedWriteTrigger = pn7160_ns.class_( - "PN7160OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7160IsWritingCondition = pn7160_ns.class_( "PN7160IsWritingCondition", automation.Condition ) @@ -85,20 +77,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7160_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7160), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -227,12 +207,14 @@ async def setup_pn7160(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 08148c23114..7759da8f532 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7160 { -class PN7160OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7160OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template<typename... Ts> class PN7160IsWritingCondition : public Condition<Ts...>, public Parented<PN7160> { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } From a5416df6155172ff80869caa8e183cd58e552e18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:36 -1000 Subject: [PATCH 1034/2056] [sim800l] Migrate triggers to callback automation (#15222) --- esphome/components/sim800l/__init__.py | 93 +++++++------------------- esphome/components/sim800l/sim800l.h | 35 ---------- 2 files changed, 23 insertions(+), 105 deletions(-) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index ebb74302a9c..91771047e1c 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MESSAGE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_MESSAGE DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] @@ -11,28 +11,6 @@ MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace("sim800l") Sim800LComponent = sim800l_ns.class_("Sim800LComponent", cg.Component) -Sim800LReceivedMessageTrigger = sim800l_ns.class_( - "Sim800LReceivedMessageTrigger", - automation.Trigger.template(cg.std_string, cg.std_string), -) -Sim800LIncomingCallTrigger = sim800l_ns.class_( - "Sim800LIncomingCallTrigger", - automation.Trigger.template(cg.std_string), -) -Sim800LCallConnectedTrigger = sim800l_ns.class_( - "Sim800LCallConnectedTrigger", - automation.Trigger.template(), -) -Sim800LCallDisconnectedTrigger = sim800l_ns.class_( - "Sim800LCallDisconnectedTrigger", - automation.Trigger.template(), -) - -Sim800LReceivedUssdTrigger = sim800l_ns.class_( - "Sim800LReceivedUssdTrigger", - automation.Trigger.template(cg.std_string), -) - # Actions Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) Sim800LSendUssdAction = sim800l_ns.class_("Sim800LSendUssdAction", automation.Action) @@ -55,41 +33,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(Sim800LComponent), - cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedMessageTrigger - ), - } - ), - cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LIncomingCallTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallConnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallDisconnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedUssdTrigger - ), - } - ), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({}), + cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("5s")) @@ -106,23 +54,28 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_SMS_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf + await automation.build_callback_automation( + var, + "add_on_sms_received_callback", + [(cg.std_string, "message"), (cg.std_string, "sender")], + conf, ) for conf in config.get(CONF_ON_INCOMING_CALL, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "caller_id")], conf) + await automation.build_callback_automation( + var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf + ) for conf in config.get(CONF_ON_CALL_CONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_call_connected_callback", [], conf + ) for conf in config.get(CONF_ON_CALL_DISCONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_call_disconnected_callback", [], conf + ) for conf in config.get(CONF_ON_USSD_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "ussd")], conf) + await automation.build_callback_automation( + var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf + ) SIM800L_SEND_SMS_SCHEMA = cv.Schema( diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index d79279ea720..d0da1230398 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -121,41 +121,6 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { CallbackManager<void(std::string)> ussd_received_callback_; }; -class Sim800LReceivedMessageTrigger : public Trigger<std::string, std::string> { - public: - explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { - parent->add_on_sms_received_callback( - [this](const std::string &message, const std::string &sender) { this->trigger(message, sender); }); - } -}; - -class Sim800LIncomingCallTrigger : public Trigger<std::string> { - public: - explicit Sim800LIncomingCallTrigger(Sim800LComponent *parent) { - parent->add_on_incoming_call_callback([this](const std::string &caller_id) { this->trigger(caller_id); }); - } -}; - -class Sim800LCallConnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallConnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_connected_callback([this]() { this->trigger(); }); - } -}; - -class Sim800LCallDisconnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallDisconnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_disconnected_callback([this]() { this->trigger(); }); - } -}; -class Sim800LReceivedUssdTrigger : public Trigger<std::string> { - public: - explicit Sim800LReceivedUssdTrigger(Sim800LComponent *parent) { - parent->add_on_ussd_received_callback([this](const std::string &ussd) { this->trigger(ussd); }); - } -}; - template<typename... Ts> class Sim800LSendSmsAction : public Action<Ts...> { public: Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} From 6ffb5af60ced80ff47720fc572c4476475954f97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:47 -1000 Subject: [PATCH 1035/2056] [fingerprint_grow] Migrate triggers to callback automation (#15223) --- .../components/fingerprint_grow/__init__.py | 142 +++++------------- .../fingerprint_grow/fingerprint_grow.h | 58 ------- 2 files changed, 36 insertions(+), 164 deletions(-) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 2637097be81..0b01ba7cabd 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -21,7 +21,6 @@ from esphome.const import ( CONF_SENSING_PIN, CONF_SPEED, CONF_STATE, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] @@ -38,38 +37,6 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_( "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice ) -FingerScanStartTrigger = fingerprint_grow_ns.class_( - "FingerScanStartTrigger", automation.Trigger.template() -) - -FingerScanMatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) -) - -FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanUnmatchedTrigger", automation.Trigger.template() -) - -FingerScanMisplacedTrigger = fingerprint_grow_ns.class_( - "FingerScanMisplacedTrigger", automation.Trigger.template() -) - -FingerScanInvalidTrigger = fingerprint_grow_ns.class_( - "FingerScanInvalidTrigger", automation.Trigger.template() -) - -EnrollmentScanTrigger = fingerprint_grow_ns.class_( - "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) -) - -EnrollmentDoneTrigger = fingerprint_grow_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) -) - -EnrollmentFailedTrigger = fingerprint_grow_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) -) - EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) CancelEnrollmentAction = fingerprint_grow_ns.class_( "CancelEnrollmentAction", automation.Action @@ -125,62 +92,22 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, - cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanStartTrigger - ), - } - ), + cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation({}), cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanUnmatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMisplacedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentScanTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("500ms")) @@ -214,40 +141,43 @@ async def to_code(config): cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) for conf in config.get(CONF_ON_FINGER_SCAN_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_start_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + await automation.build_callback_automation( + var, + "add_on_finger_scan_matched_callback", + [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], + conf, ) - for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_misplaced_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_invalid_callback", [], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_scan_callback", + [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], + conf, ) - for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) - + await automation.build_callback_automation( + var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf + ) @automation.register_action( diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 63839534f64..947c701c98a 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -210,64 +210,6 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic CallbackManager<void(uint16_t)> enrollment_failed_callback_; }; -class FingerScanStartTrigger : public Trigger<> { - public: - explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_start_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> { - public: - explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_matched_callback( - [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); - } -}; - -class FingerScanUnmatchedTrigger : public Trigger<> { - public: - explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMisplacedTrigger : public Trigger<> { - public: - explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanInvalidTrigger : public Trigger<> { - public: - explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); }); - } -}; - -class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> { - public: - explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_scan_callback( - [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger<uint16_t> { - public: - explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger<uint16_t> { - public: - explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<FingerprintGrowComponent> { public: TEMPLATABLE_VALUE(uint16_t, finger_id) From a95f9f41fb418a66d9d3d550b69aa29d1fc303ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:22:58 -1000 Subject: [PATCH 1036/2056] [ltr_als_ps] Migrate triggers to callback automation (#15224) --- esphome/components/ltr_als_ps/ltr_als_ps.h | 36 +++++----------------- esphome/components/ltr_als_ps/sensor.py | 33 ++++++-------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2e24a14283d..8aa5c9f24b1 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -58,6 +58,14 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template<typename F> void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); + } + + template<typename F> void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; - - template<typename F> void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); - } - - template<typename F> void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr_als_ps } // namespace esphome diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 0dbcff1bfbb..57503772a12 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, @@ -93,11 +92,6 @@ PS_GAINS = { "64X": PsGain.PS_GAIN_64, } -LTRPsHighTrigger = ltr_als_ps_ns.class_( - "LTRPsHighTrigger", automation.Trigger.template() -) -LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -143,16 +137,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -244,13 +230,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) From a73c67e4763c971573e89c0d5b4f7e6971ef2341 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:23:17 -1000 Subject: [PATCH 1037/2056] [ltr501] Migrate triggers to callback automation (#15225) --- esphome/components/ltr501/ltr501.h | 36 +++++++---------------------- esphome/components/ltr501/sensor.py | 31 ++++++++----------------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 2bd838a0fe2..2b914631087 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -58,6 +58,14 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template<typename F> void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); + } + + template<typename F> void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; - - template<typename F> void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward<F>(callback)); - } - - template<typename F> void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward<F>(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr501 } // namespace esphome diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index adaf669a729..712810222c8 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, @@ -87,9 +86,6 @@ PS_GAINS = { "16X": PsGain.PS_GAIN_16, } -LTRPsHighTrigger = ltr501_ns.class_("LTRPsHighTrigger", automation.Trigger.template()) -LTRPsLowTrigger = ltr501_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -146,16 +142,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -252,13 +240,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) From f5cd1e5e76831637ecf987439e205243a32c1fb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:23:26 -1000 Subject: [PATCH 1038/2056] [ld2450] Fix flaky integration test race condition (#15226) --- tests/integration/test_uart_mock_ld2450.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py index b1aa2f6952b..2469273e0ab 100644 --- a/tests/integration/test_uart_mock_ld2450.py +++ b/tests/integration/test_uart_mock_ld2450.py @@ -83,11 +83,18 @@ async def test_uart_mock_ld2450( ], ) - # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + # Signal when we see all recovery frame values + # Must wait for ALL values to avoid race where some arrive after the waiter fires recovery_received = collector.add_waiter( lambda: ( pytest.approx(500.0, abs=1.0) in collector.sensor_states["target_1_distance"] + and pytest.approx(300.0) in collector.sensor_states["target_1_x"] + and pytest.approx(400.0) in collector.sensor_states["target_1_y"] + and pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + and pytest.approx(1.0) in collector.sensor_states["target_count"] + and pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + and pytest.approx(0.0) in collector.sensor_states["still_target_count"] ) ) From d77bf23c76b7257d20cd0c9541eabcf2613adf69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:23:37 -1000 Subject: [PATCH 1039/2056] [nextion] Migrate triggers to callback automation (#15227) --- esphome/components/nextion/automation.h | 44 ------------ esphome/components/nextion/display.py | 90 +++++++------------------ 2 files changed, 25 insertions(+), 109 deletions(-) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 8e85e15823e..9f52507d676 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -5,50 +5,6 @@ namespace esphome { namespace nextion { -class BufferOverflowTrigger : public Trigger<> { - public: - explicit BufferOverflowTrigger(Nextion *nextion) { - nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); }); - } -}; - -class SetupTrigger : public Trigger<> { - public: - explicit SetupTrigger(Nextion *nextion) { - nextion->add_setup_state_callback([this]() { this->trigger(); }); - } -}; - -class SleepTrigger : public Trigger<> { - public: - explicit SleepTrigger(Nextion *nextion) { - nextion->add_sleep_state_callback([this]() { this->trigger(); }); - } -}; - -class WakeTrigger : public Trigger<> { - public: - explicit WakeTrigger(Nextion *nextion) { - nextion->add_wake_state_callback([this]() { this->trigger(); }); - } -}; - -class PageTrigger : public Trigger<uint8_t> { - public: - explicit PageTrigger(Nextion *nextion) { - nextion->add_new_page_callback([this](const uint8_t page_id) { this->trigger(page_id); }); - } -}; - -class TouchTrigger : public Trigger<uint8_t, uint8_t, bool> { - public: - explicit TouchTrigger(Nextion *nextion) { - nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { - this->trigger(page_id, component_id, touch_event); - }); - } -}; - template<typename... Ts> class NextionSetBrightnessAction : public Action<Ts...> { public: explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {} diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 5b2dfc488d2..506eb1202b7 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,13 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart import esphome.config_validation as cv -from esphome.const import ( - CONF_BRIGHTNESS, - CONF_ID, - CONF_LAMBDA, - CONF_ON_TOUCH, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, CONF_ON_TOUCH from esphome.core import CORE, TimePeriod from . import ( # noqa: F401 pylint: disable=unused-import @@ -55,14 +49,6 @@ def AUTO_LOAD() -> list[str]: NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action ) -SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) -SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) -WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) -PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) -TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) -BufferOverflowTrigger = nextion_ns.class_( - "BufferOverflowTrigger", automation.Trigger.template() -) def _validate_tft_upload(config): @@ -101,38 +87,12 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, - cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BufferOverflowTrigger - ), - } - ), - cv.Optional(CONF_ON_PAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), - } - ), - cv.Optional(CONF_ON_SETUP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), - } - ), - cv.Optional(CONF_ON_SLEEP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), - } - ), - cv.Optional(CONF_ON_TOUCH): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), - } - ), - cv.Optional(CONF_ON_WAKE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), - } - ), + cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation({}), + cv.Optional(CONF_ON_PAGE): automation.validate_automation({}), + cv.Optional(CONF_ON_SETUP): automation.validate_automation({}), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation({}), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation({}), + cv.Optional(CONF_ON_WAKE): automation.validate_automation({}), cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All( cv.positive_time_period_milliseconds, @@ -273,25 +233,25 @@ async def to_code(config): await display.register_display(var, config) for conf in config.get(CONF_ON_SETUP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_setup_state_callback", [], conf + ) for conf in config.get(CONF_ON_SLEEP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_sleep_state_callback", [], conf + ) for conf in config.get(CONF_ON_WAKE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_wake_state_callback", [], conf + ) for conf in config.get(CONF_ON_PAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "x")], conf) - + await automation.build_callback_automation( + var, "add_new_page_callback", [(cg.uint8, "x")], conf + ) for conf in config.get(CONF_ON_TOUCH, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_touch_event_callback", [ (cg.uint8, "page_id"), (cg.uint8, "component_id"), @@ -299,7 +259,7 @@ async def to_code(config): ], conf, ) - for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_buffer_overflow_event_callback", [], conf + ) From 2f3c21c7c16b37d2ad1f57e4d90883129a50c86c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:23:50 -1000 Subject: [PATCH 1040/2056] [ezo] Migrate triggers to callback automation (#15228) --- esphome/components/ezo/automation.h | 53 ---------------- esphome/components/ezo/sensor.py | 94 ++++++++--------------------- 2 files changed, 25 insertions(+), 122 deletions(-) delete mode 100644 esphome/components/ezo/automation.h diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h deleted file mode 100644 index a4a6fa30146..00000000000 --- a/esphome/components/ezo/automation.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include <utility> - -#include "esphome/core/automation.h" -#include "ezo.h" - -namespace esphome { -namespace ezo { - -class LedTrigger : public Trigger<bool> { - public: - explicit LedTrigger(EZOSensor *ezo) { - ezo->add_led_state_callback([this](bool value) { this->trigger(value); }); - } -}; - -class CustomTrigger : public Trigger<std::string> { - public: - explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class TTrigger : public Trigger<std::string> { - public: - explicit TTrigger(EZOSensor *ezo) { - ezo->add_t_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class CalibrationTrigger : public Trigger<std::string> { - public: - explicit CalibrationTrigger(EZOSensor *ezo) { - ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class SlopeTrigger : public Trigger<std::string> { - public: - explicit SlopeTrigger(EZOSensor *ezo) { - ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class DeviceInformationTrigger : public Trigger<std::string> { - public: - explicit DeviceInformationTrigger(EZOSensor *ezo) { - ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -} // namespace ezo -} // namespace esphome diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index cf240faec32..7c81f9c848e 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID CODEOWNERS = ["@ssieb"] @@ -21,61 +21,16 @@ EZOSensor = ezo_ns.class_( "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) -CustomTrigger = ezo_ns.class_( - "CustomTrigger", automation.Trigger.template(cg.std_string) -) - - -TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string)) - -SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string)) - -CalibrationTrigger = ezo_ns.class_( - "CalibrationTrigger", automation.Trigger.template(cg.std_string) -) - -DeviceInformationTrigger = ezo_ns.class_( - "DeviceInformationTrigger", automation.Trigger.template(cg.std_string) -) - -LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) - CONFIG_SCHEMA = ( sensor.sensor_schema(EZOSensor) .extend( { - cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), - } - ), - cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger), - } - ), - cv.Optional(CONF_ON_SLOPE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger), - } - ), - cv.Optional(CONF_ON_T): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger), - } - ), - cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DeviceInformationTrigger - ), - } - ), - cv.Optional(CONF_ON_LED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger), - } - ), + cv.Optional(CONF_ON_CUSTOM): automation.validate_automation({}), + cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation({}), + cv.Optional(CONF_ON_SLOPE): automation.validate_automation({}), + cv.Optional(CONF_ON_T): automation.validate_automation({}), + cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation({}), + cv.Optional(CONF_ON_LED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -90,25 +45,26 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for conf in config.get(CONF_ON_CUSTOM, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_custom_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_LED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) - + await automation.build_callback_automation( + var, "add_led_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_device_infomation_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_SLOPE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_slope_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_CALIBRATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_calibration_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_T, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_callback_automation( + var, "add_t_callback", [(cg.std_string, "x")], conf + ) From 39509265bc76a0eadce17c9e28a4b032fc3597fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:24:03 -1000 Subject: [PATCH 1041/2056] [haier] Migrate triggers to callback automation (#15229) --- esphome/components/haier/climate.py | 62 ++++++++------------------ esphome/components/haier/haier_base.h | 7 --- esphome/components/haier/hon_climate.h | 16 ------- 3 files changed, 18 insertions(+), 67 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index caaaa18dd68..9c2c999f252 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, - CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, ) @@ -122,21 +121,6 @@ SUPPORTED_HON_CONTROL_METHODS = { "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, } -HaierAlarmStartTrigger = haier_ns.class_( - "HaierAlarmStartTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -HaierAlarmEndTrigger = haier_ns.class_( - "HaierAlarmEndTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -StatusMessageTrigger = haier_ns.class_( - "StatusMessageTrigger", - automation.Trigger.template(cg.const_char_ptr, cg.size_t), -) - def validate_visual(config): if CONF_VISUAL in config: @@ -203,13 +187,7 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema: cv.Optional( CONF_ANSWER_TIMEOUT, ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - StatusMessageTrigger - ), - } - ), + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -264,19 +242,9 @@ CONFIG_SCHEMA = cv.All( f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead" ), cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmStartTrigger - ), - } - ), - cv.Optional(CONF_ON_ALARM_END): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmEndTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ALARM_END): automation.validate_automation({}), } ), }, @@ -530,19 +498,25 @@ async def to_code(config): var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE]) ) for conf in config.get(CONF_ON_ALARM_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_start_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_ALARM_END, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_end_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_STATUS_MESSAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf + await automation.build_callback_automation( + var, + "add_status_message_callback", + [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], + conf, ) # https://github.com/paveldn/HaierProtocol cg.add_library("pavlodn/HaierProtocol", "0.9.31") diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 87aa1d65eff..0c416623c08 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -177,12 +177,5 @@ class HaierClimateBase : public esphome::Component, ESPPreferenceObject base_rtc_; }; -class StatusMessageTrigger : public Trigger<const char *, size_t> { - public: - explicit StatusMessageTrigger(HaierClimateBase *parent) { - parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); }); - } -}; - } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 7c48a3748b8..7a87f27b668 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -200,21 +200,5 @@ class HonClimate : public HaierClimateBase { SwitchState quiet_mode_state_{SwitchState::OFF}; }; -class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> { - public: - explicit HaierAlarmStartTrigger(HonClimate *parent) { - parent->add_alarm_start_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - -class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> { - public: - explicit HaierAlarmEndTrigger(HonClimate *parent) { - parent->add_alarm_end_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - } // namespace haier } // namespace esphome From f9d41bd36adf4923f0509db82da47c321ffafcac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:24:15 -1000 Subject: [PATCH 1042/2056] [modbus_controller] Migrate triggers to callback automation (#15230) --- .../components/modbus_controller/__init__.py | 64 ++++++------------- .../components/modbus_controller/automation.h | 35 ---------- 2 files changed, 19 insertions(+), 80 deletions(-) delete mode 100644 esphome/components/modbus_controller/automation.h diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index aea79b20536..dfc43bf23b2 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -5,14 +5,7 @@ import esphome.codegen as cg from esphome.components import modbus from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_LAMBDA, - CONF_NAME, - CONF_OFFSET, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET from esphome.cpp_helpers import logging from .const import ( @@ -135,17 +128,6 @@ CPP_TYPE_REGISTER_MAP = { "FP32_R": cg.float_, } -ModbusCommandSentTrigger = modbus_controller_ns.class_( - "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOnlineTrigger = modbus_controller_ns.class_( - "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOfflineTrigger = modbus_controller_ns.class_( - "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) _LOGGER = logging.getLogger(__name__) @@ -182,23 +164,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_SERVER_REGISTERS, ): cv.ensure_list(ModbusServerRegisterSchema), - cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ModbusCommandSentTrigger - ), - } - ), - cv.Optional(CONF_ON_ONLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), - } - ), - cv.Optional(CONF_ON_OFFLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger), - } - ), + cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}), + cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}), + cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -363,19 +331,25 @@ async def to_code(config): cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_command_sent_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_ONLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_online_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_OFFLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_offline_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h deleted file mode 100644 index b3338192cca..00000000000 --- a/esphome/components/modbus_controller/automation.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/modbus_controller/modbus_controller.h" - -namespace esphome { -namespace modbus_controller { - -class ModbusCommandSentTrigger : public Trigger<int, int> { - public: - ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_command_sent_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOnlineTrigger : public Trigger<int, int> { - public: - ModbusOnlineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_online_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOfflineTrigger : public Trigger<int, int> { - public: - ModbusOfflineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_offline_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -} // namespace modbus_controller -} // namespace esphome From 0d67f91facbe654bbb634d95aa16c791ff52a3f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:24:25 -1000 Subject: [PATCH 1043/2056] [rf_bridge] Migrate triggers to callback automation (#15231) --- esphome/components/rf_bridge/__init__.py | 37 +++++++----------------- esphome/components/rf_bridge/rf_bridge.h | 14 --------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 934f24b7890..c6eb1749c3a 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_PROTOCOL, CONF_RAW, CONF_SYNC, - CONF_TRIGGER_ID, ) DEPENDENCIES = ["uart"] @@ -26,14 +25,6 @@ RFBridgeComponent = rf_bridge_ns.class_( RFBridgeData = rf_bridge_ns.struct("RFBridgeData") RFBridgeAdvancedData = rf_bridge_ns.struct("RFBridgeAdvancedData") -RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedCodeTrigger", automation.Trigger.template(RFBridgeData) -) -RFBridgeReceivedAdvancedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedAdvancedCodeTrigger", - automation.Trigger.template(RFBridgeAdvancedData), -) - RFBridgeSendCodeAction = rf_bridge_ns.class_( "RFBridgeSendCodeAction", automation.Action ) @@ -65,19 +56,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(RFBridgeComponent), - cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedCodeTrigger - ), - } - ), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({}), cv.Optional(CONF_ON_ADVANCED_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedAdvancedCodeTrigger - ), - } + {} ), } ) @@ -92,13 +73,15 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(RFBridgeData, "data")], conf) - + await automation.build_callback_automation( + var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf + ) for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(RFBridgeAdvancedData, "data")], conf + await automation.build_callback_automation( + var, + "add_on_advanced_code_received_callback", + [(RFBridgeAdvancedData, "data")], + conf, ) diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index e5780c9ebe5..571ac6c385e 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -77,20 +77,6 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { CallbackManager<void(RFBridgeAdvancedData)> advanced_data_callback_; }; -class RFBridgeReceivedCodeTrigger : public Trigger<RFBridgeData> { - public: - explicit RFBridgeReceivedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_code_received_callback([this](RFBridgeData data) { this->trigger(data); }); - } -}; - -class RFBridgeReceivedAdvancedCodeTrigger : public Trigger<RFBridgeAdvancedData> { - public: - explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_advanced_code_received_callback([this](const RFBridgeAdvancedData &data) { this->trigger(data); }); - } -}; - template<typename... Ts> class RFBridgeSendCodeAction : public Action<Ts...> { public: RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} From 5a8d6931a8d8a0f1fe05727e2e5d00098aa2dbb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 27 Mar 2026 08:24:35 -1000 Subject: [PATCH 1044/2056] [factory_reset] Migrate FastBootTrigger to callback automation (#15232) --- esphome/components/factory_reset/__init__.py | 21 ++++++------------- .../components/factory_reset/factory_reset.h | 6 ------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index 5784d09ce6f..20b191a2b7a 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -1,10 +1,9 @@ -from esphome.automation import Trigger, build_automation, validate_automation +from esphome import automation import esphome.codegen as cg from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 import esphome.config_validation as cv from esphome.const import ( CONF_ID, - CONF_TRIGGER_ID, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -18,7 +17,6 @@ CODEOWNERS = ["@anatoly-savchenkov"] factory_reset_ns = cg.esphome_ns.namespace("factory_reset") FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) -FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) CONF_MAX_DELAY = "max_delay" CONF_RESETS_REQUIRED = "resets_required" @@ -55,11 +53,7 @@ CONFIG_SCHEMA = cv.All( ), ), cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, - cv.Optional(CONF_ON_INCREMENT): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), - } - ), + cv.Optional(CONF_ON_INCREMENT): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _validate, @@ -88,12 +82,9 @@ async def to_code(config): ) await cg.register_component(var, config) for conf in config.get(CONF_ON_INCREMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await build_automation( - trigger, - [ - (cg.uint8, "x"), - (cg.uint8, "target"), - ], + await automation.build_callback_automation( + var, + "add_increment_callback", + [(cg.uint8, "x"), (cg.uint8, "target")], conf, ) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 34f89d73b60..41ee627c4b3 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -30,12 +30,6 @@ class FactoryResetComponent : public Component { uint8_t required_count_; // The number of boot attempts before fast boot is enabled }; -class FastBootTrigger : public Trigger<uint8_t, uint8_t> { - public: - explicit FastBootTrigger(FactoryResetComponent *parent) { - parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); - } -}; } // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) From 810c046cc68dba48b3748f4a60de2049146beee7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:25:38 -0400 Subject: [PATCH 1045/2056] [multiple] Fix misc hardware register bugs (#15208) --- esphome/components/mcp23008/mcp23008.cpp | 4 +++- esphome/components/mcp23017/mcp23017.cpp | 6 ++++-- esphome/components/mcp23s08/mcp23s08.cpp | 15 ++++++++++----- esphome/components/mcp23s17/mcp23s17.cpp | 22 +++++++++++++--------- esphome/components/mmc5603/mmc5603.cpp | 6 +++--- esphome/components/sx1509/sx1509.cpp | 4 ++-- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 0c34e4971a7..64b120daa49 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -6,6 +6,8 @@ namespace mcp23008 { static const char *const TAG = "mcp23008"; +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23008::setup() { uint8_t iocon; if (!this->read_reg(mcp23x08_base::MCP23X08_IOCON, &iocon)) { @@ -18,7 +20,7 @@ void MCP23008::setup() { if (this->open_drain_ints_) { // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 1ad2036939a..e14e317d440 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -6,6 +6,8 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23017::setup() { uint8_t iocon; if (!this->read_reg(mcp23x17_base::MCP23X17_IOCONA, &iocon)) { @@ -19,8 +21,8 @@ void MCP23017::setup() { if (this->open_drain_ints_) { // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); } } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 3d944b45d55..1c17b66637f 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -6,6 +6,11 @@ namespace mcp23s08 { static const char *const TAG = "mcp23s08"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S08::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0x03) << 1); @@ -15,19 +20,19 @@ void MCP23S08::set_device_address(uint8_t device_addr) { void MCP23S08::setup() { this->spi_setup(); + // Enable HAEN (broadcast to all chips since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x08_base::MCP23X08_IOCON); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state this->read_reg(mcp23x08_base::MCP23X08_OLAT, &this->olat_); if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) + this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } } diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 1624eda9e41..c6abd7ad594 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -6,6 +6,11 @@ namespace mcp23s17 { static const char *const TAG = "mcp23s17"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S17::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0b111) << 1); @@ -15,18 +20,17 @@ void MCP23S17::set_device_address(uint8_t device_addr) { void MCP23S17::setup() { this->spi_setup(); + // Enable HAEN (broadcast to addresses 0 and 4 since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); this->enable(); - cmd = 0b01001000; - this->transfer_byte(cmd); + this->transfer_byte(0b01001000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state @@ -34,9 +38,9 @@ void MCP23S17::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 1cbc84191f1..51b94eb7671 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -126,21 +126,21 @@ void MMC5603Component::update() { int32_t raw_x = 0; raw_x |= buffer[0] << 12; raw_x |= buffer[1] << 4; - raw_x |= buffer[2] << 0; + raw_x |= buffer[2] & 0x0F; const float x = 0.00625 * (raw_x - 524288); int32_t raw_y = 0; raw_y |= buffer[3] << 12; raw_y |= buffer[4] << 4; - raw_y |= buffer[5] << 0; + raw_y |= buffer[5] & 0x0F; const float y = 0.00625 * (raw_y - 524288); int32_t raw_z = 0; raw_z |= buffer[6] << 12; raw_z |= buffer[7] << 4; - raw_z |= buffer[8] << 0; + raw_z |= buffer[8] & 0x0F; const float z = 0.00625 * (raw_z - 524288); diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index dfe1277297a..1cdae76eaf4 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -309,8 +309,8 @@ void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8 set_debounce_time_(time); for (uint16_t i = 0; i < num_rows; i++) set_debounce_pin_(i); - for (uint16_t i = 0; i < (8 + num_cols); i++) - set_debounce_pin_(i); + for (uint16_t i = 0; i < num_cols; i++) + set_debounce_pin_(i + 8); } } // namespace sx1509 From 0a607b9c93c0a1c8504a73de09b564c723a258b2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:36:16 -0400 Subject: [PATCH 1046/2056] [esp32_ble_server] Fix wrong union member in STOP_EVT handler (#15239) --- esphome/components/esp32_ble_server/ble_service.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 96fedf23466..8956c87b3e8 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -159,7 +159,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g break; } case ESP_GATTS_STOP_EVT: { - if (param->start.service_handle == this->handle_) { + if (param->stop.service_handle == this->handle_) { this->state_ = STOPPED; } break; From 4b9467cd0cd03bf33aa24ec8be48bc9395976a6a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:37:33 -0400 Subject: [PATCH 1047/2056] [esp32_ble_client] Fix wrong union member in OPEN_EVT handler (#15236) --- esphome/components/esp32_ble_client/ble_client_base.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 9d6e079d926..7f0f2c624d3 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -350,7 +350,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // For V3_WITHOUT_CACHE, we already set fast params before connecting // No need to update them again here this->log_event_("Searching for services"); - esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + esp_ble_gattc_search_service(esp_gattc_if, param->open.conn_id, nullptr); break; } case ESP_GATTC_CONNECT_EVT: { From 53bd57f3c2557d75fade7864b7371a2de27cabc5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:37:54 -0400 Subject: [PATCH 1048/2056] [pid] Fix inverted debug log conditions and broken smoothing formula (#15240) --- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pid/pid_simulator.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index e1ddd1d7c67..3b971e6559d 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -101,10 +101,10 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway - if (zc_symmetrical) { + if (!zc_symmetrical) { ESP_LOGVV(TAG, "%s: ZC is not symmetrical", this->id_.c_str()); } - if (amplitude_convergent) { + if (!amplitude_convergent) { ESP_LOGVV(TAG, "%s: Amplitude is not convergent", this->id_.c_str()); } uint32_t phase = this->relay_function_.phase_count; diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h index 30222f2f7a1..629784cea51 100644 --- a/esphome/components/pid/pid_simulator.h +++ b/esphome/components/pid/pid_simulator.h @@ -59,7 +59,7 @@ class PIDSimulator : public PollingComponent, public output::FloatOutput { delayed_temps.erase(delayed_temps.begin()); float prev_temp = this->delayed_temps[0]; float alpha = 0.1f; - float ret = (1 - alpha) * prev_temp + alpha * prev_temp; + float ret = (1 - alpha) * prev_temp + alpha * temperature; return ret; } From 951ad91cb259262675dbfbf7f71d1d6fb27d596a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:39:30 -0400 Subject: [PATCH 1049/2056] [atm90e32] Fix phase angle precision loss and remove unused member (#15238) --- esphome/components/atm90e32/atm90e32.cpp | 4 ++-- esphome/components/atm90e32/atm90e32.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index ee7fe5ce75e..db29702c54a 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -550,8 +550,8 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { } float ATM90E32Component::get_phase_angle_(uint8_t phase) { - uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; - return (val > 180) ? (float) (val - 360.0f) : (float) val; + float val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0f; + return (val > 180.0f) ? val - 360.0f : val; } float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 2524616470a..c44a11e3ed4 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -134,7 +134,6 @@ class ATM90E32Component : public PollingComponent, void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; } #endif uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier); - int32_t last_periodic_millis = millis(); protected: #ifdef USE_NUMBER From 05c15f4241d20c78d3f8eb40a19d3046723aeaf7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:44:40 -0400 Subject: [PATCH 1050/2056] [remote_base] Fix gobox uint64_t format specifier (#15237) --- esphome/components/remote_base/gobox_protocol.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 4f6de5e59ea..0e1617659d8 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -1,5 +1,6 @@ #include "gobox_protocol.h" #include "esphome/core/log.h" +#include <cinttypes> namespace esphome { namespace remote_base { @@ -25,7 +26,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { dst->set_carrier_frequency(38000); dst->reserve((HEADER_SIZE + CODE_SIZE + 1) * 2); uint64_t code = (HEADER << CODE_SIZE) | (data.code & ((1UL << CODE_SIZE) - 1)); - ESP_LOGI(TAG, "Send Gobox: code=0x%Lx", code); + ESP_LOGI(TAG, "Send Gobox: code=0x%016" PRIx64, code); for (int16_t i = (HEADER_SIZE + CODE_SIZE - 1); i >= 0; i--) { if (code & ((uint64_t) 1 << i)) { dst->item(BIT_MARK_US, BIT_ONE_SPACE_US); From f0db0c105424e31022e9521b44eb577cfa18d2d5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:48:08 -0400 Subject: [PATCH 1051/2056] [esp32] Add ESP-IDF 5.5.4 and 6.0.0 version mappings (#15241) --- esphome/components/esp32/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 91eb913e3df..0ce1117262f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -690,9 +690,15 @@ ARDUINO_IDF_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(5, 5, 3, "1"), "latest": cv.Version(5, 5, 3, "1"), - "dev": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version( + 6, 0, 0 + ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", + cv.Version( + 5, 5, 4 + ): "https://github.com/pioarduino/platform-espressif32.git#develop", cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), From 7532e1f957499ca880c71c0e8c1f693c585b4cf3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:58:41 -0400 Subject: [PATCH 1052/2056] [multiple] Fix uninitialized members and error constant types (#15235) --- esphome/components/max44009/max44009.cpp | 18 +++++++++--------- esphome/components/max44009/max44009.h | 4 ++-- .../modbus_controller/output/modbus_output.h | 4 ++-- esphome/components/tuya/climate/tuya_climate.h | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 8b8e38c1eae..cbce0535196 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -8,17 +8,17 @@ namespace max44009 { static const char *const TAG = "max44009.sensor"; // REGISTERS -static const uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; -static const uint8_t MAX44009_LUX_READING_HIGH = 0x03; -static const uint8_t MAX44009_LUX_READING_LOW = 0x04; +static constexpr uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; +static constexpr uint8_t MAX44009_LUX_READING_HIGH = 0x03; +static constexpr uint8_t MAX44009_LUX_READING_LOW = 0x04; // CONFIGURATION MASKS -static const uint8_t MAX44009_CFG_CONTINUOUS = 0x80; +static constexpr uint8_t MAX44009_CFG_CONTINUOUS = 0x80; // ERROR CODES -static const uint8_t MAX44009_OK = 0; -static const uint8_t MAX44009_ERROR_WIRE_REQUEST = -10; -static const uint8_t MAX44009_ERROR_OVERFLOW = -20; -static const uint8_t MAX44009_ERROR_HIGH_BYTE = -30; -static const uint8_t MAX44009_ERROR_LOW_BYTE = -31; +static constexpr int8_t MAX44009_OK = 0; +static constexpr int8_t MAX44009_ERROR_WIRE_REQUEST = -10; +static constexpr int8_t MAX44009_ERROR_OVERFLOW = -20; +static constexpr int8_t MAX44009_ERROR_HIGH_BYTE = -30; +static constexpr int8_t MAX44009_ERROR_LOW_BYTE = -31; void MAX44009Sensor::setup() { bool state_ok = false; diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index 59eea66ed98..d0ffd7bc706 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -28,8 +28,8 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 uint8_t read_(uint8_t reg); void write_(uint8_t reg, uint8_t value); - int error_; - MAX44009Mode mode_; + int8_t error_{0}; + MAX44009Mode mode_{MAX44009_MODE_AUTO}; }; } // namespace max44009 diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 0fb4bb89ea4..3f3cadfe2fb 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -15,7 +15,7 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; this->offset = offset; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->register_count = register_count; this->sensor_value_type = value_type; this->skip_updates = 0; @@ -47,7 +47,7 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public ModbusBinaryOutput(uint16_t start_address, uint8_t offset) { this->register_type = ModbusRegisterType::COIL; this->start_address = start_address; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::BIT; this->skip_updates = 0; this->register_count = 1; diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index 31bef576391..09f3fd30c39 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -105,8 +105,8 @@ class TuyaClimate : public climate::Climate, public Component { optional<uint8_t> sleep_id_{}; optional<float> eco_temperature_{}; TuyaDatapointType eco_type_{}; - uint8_t active_state_; - uint8_t fan_state_; + uint8_t active_state_{0}; + uint8_t fan_state_{0}; optional<uint8_t> swing_vertical_id_{}; optional<uint8_t> swing_horizontal_id_{}; optional<uint8_t> fan_speed_id_{}; @@ -119,9 +119,9 @@ class TuyaClimate : public climate::Climate, public Component { bool swing_horizontal_{false}; bool heating_state_{false}; bool cooling_state_{false}; - float manual_temperature_; - bool eco_; - bool sleep_; + float manual_temperature_{NAN}; + bool eco_{false}; + bool sleep_{false}; bool reports_fahrenheit_{false}; }; From 3016cd363617d080e7eb6ed6532e5668cb07bdbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:29:08 -1000 Subject: [PATCH 1053/2056] Bump github/codeql-action from 4.34.1 to 4.35.1 (#15245) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6baab70b427..67f4690ac9f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:${{matrix.language}}" From a2dee21e8e8f43c9ba68ab5a7b99908d7cd22eaf Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:24:19 +0100 Subject: [PATCH 1054/2056] [nextion] Replace `std::deque` queues with `std::list` (#15211) --- esphome/components/nextion/nextion.cpp | 14 ++++++-------- esphome/components/nextion/nextion.h | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fa1582c209f..964dbfb660e 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -841,10 +841,10 @@ void Nextion::process_nextion_commands_() { if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { - for (size_t i = 0; i < this->nextion_queue_.size(); i++) { - NextionComponentBase *component = this->nextion_queue_[i]->component; - if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) { - if (this->nextion_queue_[i]->queue_time == 0) { + for (auto it = this->nextion_queue_.begin(); it != this->nextion_queue_.end();) { + NextionComponentBase *component = (*it)->component; + if (ms - (*it)->queue_time > this->max_q_age_ms_) { + if ((*it)->queue_time == 0) { ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); } @@ -863,10 +863,8 @@ void Nextion::process_nextion_commands_() { delete component; // NOLINT(cppcoreguidelines-owning-memory) } - delete this->nextion_queue_[i]; // NOLINT(cppcoreguidelines-owning-memory) - - this->nextion_queue_.erase(this->nextion_queue_.begin() + i); - i--; + delete *it; // NOLINT(cppcoreguidelines-owning-memory) + it = this->nextion_queue_.erase(it); } else { break; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 217d2e605d2..b5aaecd667d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,16 +1,16 @@ #pragma once -#include <deque> +#include <list> #include <vector> +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/components/uart/uart.h" #include "esphome/core/defines.h" #include "esphome/core/time.h" -#include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" -#include "esphome/components/display/display.h" -#include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP32 @@ -1391,8 +1391,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING - std::deque<NextionQueue *> nextion_queue_; - std::deque<NextionQueue *> waveform_queue_; + std::list<NextionQueue *> nextion_queue_; + std::list<NextionQueue *> waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; From d245b9f123e37618e51f027412f9cc860309478a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:24:03 -0400 Subject: [PATCH 1055/2056] [sm2135] Fix copy-paste error in setup pin mode (#15248) --- esphome/components/sm2135/sm2135.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index 1293c3f3213..c3d10e70c28 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -25,7 +25,7 @@ void SM2135::setup() { this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); this->clock_pin_->setup(); this->clock_pin_->digital_write(false); - this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->clock_pin_->pin_mode(gpio::FLAG_OUTPUT); this->data_pin_->pin_mode(gpio::FLAG_PULLUP); this->clock_pin_->pin_mode(gpio::FLAG_PULLUP); From 24b8a95340d79ad00157c261e0e3c9f92998439e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:24:15 -0400 Subject: [PATCH 1056/2056] [pid] Remove unused PIDSimulator class (#15247) --- esphome/components/pid/pid_autotuner.h | 1 - esphome/components/pid/pid_simulator.h | 77 -------------------------- 2 files changed, 78 deletions(-) delete mode 100644 esphome/components/pid/pid_simulator.h diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 98dc02bcc44..1db9ca71385 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -3,7 +3,6 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" #include "pid_controller.h" -#include "pid_simulator.h" #include <vector> diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h deleted file mode 100644 index 629784cea51..00000000000 --- a/esphome/components/pid/pid_simulator.h +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/output/float_output.h" - -#include <vector> - -namespace esphome { -namespace pid { - -class PIDSimulator : public PollingComponent, public output::FloatOutput { - public: - PIDSimulator() : PollingComponent(1000) {} - - float surface = 1; /// surface area in m² - float mass = 3; /// mass of simulated object in kg - float temperature = 21; /// current temperature of object in °C - float efficiency = 0.98; /// heating efficiency, 1 is 100% efficient - float thermal_conductivity = 15; /// thermal conductivity of surface are in W/(m*K), here: steel - float specific_heat_capacity = 4.182; /// specific heat capacity of mass in kJ/(kg*K), here: water - float heat_power = 500; /// Heating power in W - float ambient_temperature = 20; /// Ambient temperature in °C - float update_interval = 1; /// The simulated updated interval in seconds - std::vector<float> delayed_temps; /// storage of past temperatures for delaying temperature reading - size_t delay_cycles = 15; /// how many update cycles to delay the output - float output_value = 0.0; /// Current output value of heating element - sensor::Sensor *sensor = new sensor::Sensor(); - - float delta_t(float power) { - // P = Q / t - // Q = c * m * 𝚫t - // 𝚫t = (P*t) / (c*m) - float c = this->specific_heat_capacity; - float t = this->update_interval; - float p = power / 1000; // in kW - float m = this->mass; - return (p * t) / (c * m); - } - - float update_temp() { - float value = clamp(output_value, 0.0f, 1.0f); - - // Heat - float power = value * heat_power * efficiency; - temperature += this->delta_t(power); - - // Cool - // Q = k_w * A * (T_mass - T_ambient) - // P = Q / t - float dt = temperature - ambient_temperature; - float cool_power = (thermal_conductivity * surface * dt) / update_interval; - temperature -= this->delta_t(cool_power); - - // Delay temperature readings - delayed_temps.push_back(temperature); - if (delayed_temps.size() > delay_cycles) - delayed_temps.erase(delayed_temps.begin()); - float prev_temp = this->delayed_temps[0]; - float alpha = 0.1f; - float ret = (1 - alpha) * prev_temp + alpha * temperature; - return ret; - } - - void setup() override { sensor->publish_state(this->temperature); } - void update() override { - float new_temp = this->update_temp(); - sensor->publish_state(new_temp); - } - - protected: - void write_state(float state) override { this->output_value = state; } -}; - -} // namespace pid -} // namespace esphome From 68d9f657adf9d2a65621486627596b5f6275a317 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:32:37 -0400 Subject: [PATCH 1057/2056] [bl0940] Fix energy reference default using wrong constant in legacy mode (#15249) --- esphome/components/bl0940/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index d2e0ea435d0..f36250ecdfa 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -124,7 +124,7 @@ def set_reference_values(config): config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) - config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF) else: vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) From 76d75850a3bd100fb056d5a82c8792abb3f23154 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:35:12 -0400 Subject: [PATCH 1058/2056] [sgp4x] Remove dead voc_baseline config option (#15250) --- esphome/components/sgp4x/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 8d52ffb4f26..1e58a0f26a9 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, CONF_VOC, - CONF_VOC_BASELINE, DEVICE_CLASS_AQI, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, @@ -83,7 +82,6 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( { cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), @@ -112,9 +110,6 @@ async def to_code(config): cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - if CONF_VOC_BASELINE in config: - cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) - if CONF_VOC in config: sens = await sensor.new_sensor(config[CONF_VOC]) cg.add(var.set_voc_sensor(sens)) From f6c63c62e43b88b8933a3f02f866c7e0936dd290 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Fri, 27 Mar 2026 17:59:26 -0500 Subject: [PATCH 1059/2056] [tmp117] Code clean-up (#15260) --- esphome/components/tmp117/tmp117.cpp | 17 +++++++---------- esphome/components/tmp117/tmp117.h | 6 ++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index f8f52266e06..b3e900f5b65 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -4,8 +4,7 @@ #include "tmp117.h" #include "esphome/core/log.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { static const char *const TAG = "tmp117"; @@ -18,11 +17,10 @@ void TMP117Component::update() { if ((uint16_t) data != 0x8000) { float temperature = data * 0.0078125f; - ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); this->publish_state(temperature); this->status_clear_warning(); } else { - ESP_LOGD(TAG, "TMP117 not ready"); + ESP_LOGD(TAG, "Not ready"); } } void TMP117Component::setup() { @@ -38,7 +36,7 @@ void TMP117Component::setup() { } } void TMP117Component::dump_config() { - ESP_LOGD(TAG, "TMP117:"); + ESP_LOGCONFIG(TAG, "TMP117:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -48,7 +46,7 @@ void TMP117Component::dump_config() { bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { - ESP_LOGW(TAG, "Updating TMP117 failed!"); + ESP_LOGW(TAG, "Updating failed"); return false; } return true; @@ -56,7 +54,7 @@ bool TMP117Component::read_data_(int16_t *data) { bool TMP117Component::read_config_(uint16_t *config) { if (!this->read_byte_16(1, (uint16_t *) config)) { - ESP_LOGW(TAG, "Reading TMP117 config failed!"); + ESP_LOGW(TAG, "Reading config failed"); return false; } return true; @@ -64,11 +62,10 @@ bool TMP117Component::read_config_(uint16_t *config) { bool TMP117Component::write_config_(uint16_t config) { if (!this->write_byte_16(1, config)) { - ESP_LOGE(TAG, "Writing TMP117 config failed!"); + ESP_LOGE(TAG, "Writing config failed"); return false; } return true; } -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index f501ee270c3..a8fe7ac7cef 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { class TMP117Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: @@ -22,5 +21,4 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s uint16_t config_; }; -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 From a99f051e19759c70e2007deb4b873327c9127699 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:49:00 +0100 Subject: [PATCH 1060/2056] [nextion] Replace queue name string literals with short Nextion-native identifiers (#15215) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 13 +- .../components/nextion/nextion_commands.cpp | 115 ++++++++---------- 2 files changed, 62 insertions(+), 66 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 964dbfb660e..97d9b36e4c3 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -241,7 +241,7 @@ bool Nextion::send_command(const char *command) { return false; if (this->send_command_(command)) { - this->add_no_result_to_queue_("send_command"); + this->add_no_result_to_queue_("command"); return true; } return false; @@ -262,7 +262,7 @@ bool Nextion::send_command_printf(const char *format, ...) { } if (this->send_command_(buffer)) { - this->add_no_result_to_queue_("send_command_printf"); + this->add_no_result_to_queue_("command_printf"); return true; } return false; @@ -853,8 +853,13 @@ void Nextion::process_nextion_commands_() { this->is_sleeping_ = false; } - ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str()); + if ((*it)->pending_command.empty()) { + ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + } else { + ESP_LOGD(TAG, "Remove old queue '%s':'%s' cmd:'%s'", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str(), (*it)->pending_command.c_str()); + } if (component->get_queue_type() == NextionQueueType::NO_RESULT) { if (component->get_variable_name() == "sleep_wake") { diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 4ddbfbee6a9..6718646efa3 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -12,7 +12,7 @@ void Nextion::soft_reset() { this->send_command_("rest"); } void Nextion::set_wake_up_page(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; - this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); + this->add_no_result_to_queue_with_set_internal_("wup", "wup", wake_up_page, true); } void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { @@ -23,7 +23,7 @@ void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { this->touch_sleep_timeout_ = touch_sleep_timeout; } - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); + this->add_no_result_to_queue_with_set_internal_("thsp", "thsp", this->touch_sleep_timeout_, true); } void Nextion::sleep(bool sleep) { @@ -58,115 +58,107 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%s", component, color); } void Nextion::set_component_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Background (pressed) void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%s", component, color); } void Nextion::set_component_pressed_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground void Nextion::set_component_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground (pressed) void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font void Nextion::set_component_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font (pressed) void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set picture void Nextion::set_component_pic(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".pic", "%s.pic=%" PRIu16, component, pic_id); } void Nextion::set_component_picc(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".picc", "%s.picc=%" PRIu16, component, pic_id); } // Set video void Nextion::set_component_vid(const char *component, uint8_t vid_id) { - this->add_no_result_to_queue_with_printf_("set_component_vid", "%s.vid=%" PRIu8, component, vid_id); + this->add_no_result_to_queue_with_printf_(".vid", "%s.vid=%" PRIu8, component, vid_id); } void Nextion::set_component_drag(const char *component, bool drag) { - this->add_no_result_to_queue_with_printf_("set_component_drag", "%s.drag=%i", component, drag ? 1 : 0); + this->add_no_result_to_queue_with_printf_(".drag", "%s.drag=%i", component, drag ? 1 : 0); } void Nextion::set_component_aph(const char *component, uint8_t aph) { - this->add_no_result_to_queue_with_printf_("set_component_aph", "%s.aph=%" PRIu8, component, aph); + this->add_no_result_to_queue_with_printf_(".aph", "%s.aph=%" PRIu8, component, aph); } void Nextion::set_component_position(const char *component, uint32_t x, uint32_t y) { - this->add_no_result_to_queue_with_printf_("set_component_position_x", "%s.x=%" PRIu32, component, x); - this->add_no_result_to_queue_with_printf_("set_component_position_y", "%s.y=%" PRIu32, component, y); + this->add_no_result_to_queue_with_printf_(".x", "%s.x=%" PRIu32, component, x); + this->add_no_result_to_queue_with_printf_(".y", "%s.y=%" PRIu32, component, y); } void Nextion::set_component_text_printf(const char *component, const char *format, ...) { @@ -180,29 +172,29 @@ void Nextion::set_component_text_printf(const char *component, const char *forma } // General Nextion -void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } -void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %i", page); } +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("page", "page %s", page); } +void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); } void Nextion::set_backlight_brightness(float brightness) { if (brightness < 0 || brightness > 1.0) { ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)"); return; } - this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100)); + this->add_no_result_to_queue_with_printf_("dim", "dim=%d", static_cast<int>(brightness * 100)); } void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch; - this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); + this->add_no_result_to_queue_with_set("thup", "thup", auto_wake_on_touch ? 1 : 0); } // General Component void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); + this->add_no_result_to_queue_with_printf_(".font", "%s.font=%" PRIu8, component, font_id); } void Nextion::set_component_visibility(const char *component, bool show) { - this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); + this->add_no_result_to_queue_with_printf_("vis", "vis %s,%d", component, show ? 1 : 0); } void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } @@ -210,56 +202,55 @@ void Nextion::hide_component(const char *component) { this->set_component_visibi void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } void Nextion::enable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,1", component); } void Nextion::disable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,0", component); } void Nextion::set_component_text(const char *component, const char *text) { - this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); + this->add_no_result_to_queue_with_printf_(".txt", "%s.txt=\"%s\"", component, text); } void Nextion::set_component_value(const char *component, int32_t value) { - this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%" PRId32, component, value); + this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value); } void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%" PRIu16, component, x); - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%" PRIu16, component, y); + this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x); + this->add_no_result_to_queue_with_printf_(".ycen", "%s.ycen=%" PRIu16, component, y); } // Drawing void Nextion::display_picture(uint16_t picture_id, uint16_t x_start, uint16_t y_start) { - this->add_no_result_to_queue_with_printf_("display_picture", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, - y_start, picture_id); + this->add_no_result_to_queue_with_printf_("pic", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, y_start, + picture_id); } void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color) { - this->add_no_result_to_queue_with_printf_( - "fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); -} - -void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { - this->add_no_result_to_queue_with_printf_("fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); } +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, y1, + width, height, color); +} + void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color) { - this->add_no_result_to_queue_with_printf_("fill_area", - "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, - width, height, display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, width, height, display::ColorUtil::color_to_565(color)); } void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { From 34410e92b7e1f9ddd15629ecb5903932554e9077 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:55:40 -0400 Subject: [PATCH 1061/2056] [as5600] Remove dead angle/position sensor code (#15254) --- esphome/components/as5600/sensor/__init__.py | 15 ----------- .../as5600/sensor/as5600_sensor.cpp | 25 +++---------------- .../components/as5600/sensor/as5600_sensor.h | 6 ----- 3 files changed, 4 insertions(+), 42 deletions(-) diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index e84733a4849..cf67a3f2035 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -2,11 +2,9 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( - CONF_ANGLE, CONF_GAIN, CONF_ID, CONF_MAGNITUDE, - CONF_POSITION, CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_MAGNET, @@ -21,7 +19,6 @@ DEPENDENCIES = ["as5600"] AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) -CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" @@ -89,18 +86,6 @@ async def to_code(config): if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) - if angle_config := config.get(CONF_ANGLE): - sens = await sensor.new_sensor(angle_config) - cg.add(var.set_angle_sensor(sens)) - - if raw_angle_config := config.get(CONF_RAW_ANGLE): - sens = await sensor.new_sensor(raw_angle_config) - cg.add(var.set_raw_angle_sensor(sens)) - - if position_config := config.get(CONF_POSITION): - sens = await sensor.new_sensor(position_config) - cg.add(var.set_position_sensor(sens)) - if raw_position_config := config.get(CONF_RAW_POSITION): sens = await sensor.new_sensor(raw_position_config) cg.add(var.set_raw_position_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index 1c0f4bad2c3..4e549d24d52 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); - if (this->angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); - } - if (this->raw_angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); - } - if (this->position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); - } - if (this->raw_position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); - } - if (this->gain_sensor_ != nullptr) { - LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); - } - if (this->magnitude_sensor_ != nullptr) { - LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); - } - if (this->status_sensor_ != nullptr) { - LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); - } + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index d471be49b58..77593f4b12c 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, void update() override; void dump_config() override; - void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } - void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } - void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { this->raw_position_sensor_ = raw_position_sensor; } @@ -28,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } protected: - sensor::Sensor *angle_sensor_{nullptr}; - sensor::Sensor *raw_angle_sensor_{nullptr}; - sensor::Sensor *position_sensor_{nullptr}; sensor::Sensor *raw_position_sensor_{nullptr}; sensor::Sensor *gain_sensor_{nullptr}; sensor::Sensor *magnitude_sensor_{nullptr}; From 47774fb644a162c5e38941a1b392ab7374aecb2e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:55:57 -0400 Subject: [PATCH 1062/2056] [modbus_controller] Fix wrong enum in function_code_to_register (#15253) --- esphome/components/modbus_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index dfc43bf23b2..cb0969913ac 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -362,7 +362,7 @@ async def register_modbus_device(var, config): def function_code_to_register(function_code): FUNCTION_CODE_TYPE_MAP = { "read_coils": ModbusRegisterType.COIL, - "read_discrete_inputs": ModbusRegisterType.DISCRETE, + "read_discrete_inputs": ModbusRegisterType.DISCRETE_INPUT, "read_holding_registers": ModbusRegisterType.HOLDING, "read_input_registers": ModbusRegisterType.READ, "write_single_coil": ModbusRegisterType.COIL, From b6abfec82e4e51bac2f0a927ca3180b174d70c02 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:22:24 -0400 Subject: [PATCH 1063/2056] [core] Fix area/device hash collision validation not running (#15259) --- esphome/config.py | 18 ++++++++++++++++++ esphome/core/config.py | 15 ++++++--------- script/ci-custom.py | 12 ++++++++++++ tests/unit_tests/core/test_config.py | 18 ++++++++++++++++++ .../config/area_singular_hash_collision.yaml | 10 ++++++++++ 5 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml diff --git a/esphome/config.py b/esphome/config.py index 7a6feea3d31..641b6ec1b48 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -958,6 +958,23 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) +class CoreFinalValidateStep(ConfigValidationStep): + """Run final validation on core esphome config (area/device hash collisions).""" + + # Same priority as component final validate steps + priority = -20.0 + + def run(self, result: Config) -> None: + if result.errors: + return + + token = fv.full_config.set(result) + with result.catch_error([CONF_ESPHOME]): + if CONF_ESPHOME in result: + core_config.validate_ids_and_references(result[CONF_ESPHOME]) + fv.full_config.reset(token) + + class PinUseValidationCheck(ConfigValidationStep): """Check for pin reuse""" @@ -1085,6 +1102,7 @@ def validate_config( for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(CoreFinalValidateStep()) result.add_validation_step(PinUseValidationCheck()) result.add_validation_step(RemoveReferenceValidationStep()) diff --git a/esphome/core/config.py b/esphome/core/config.py index e02c6ec75f2..c47693c783e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -156,22 +156,22 @@ def validate_ids_and_references(config: ConfigType) -> ConfigType: hash_dict[hash_val] = id_obj.id # Collect all areas - all_areas: list[dict[str, str | core.ID]] = [] + all_areas: list[tuple[dict[str, str | core.ID], str]] = [] if CONF_AREA in config: - all_areas.append(config[CONF_AREA]) - all_areas.extend(config[CONF_AREAS]) + all_areas.append((config[CONF_AREA], CONF_AREA)) + all_areas.extend((area, CONF_AREAS) for area in config.get(CONF_AREAS, [])) # Validate area hash collisions and collect IDs area_hashes: dict[int, str] = {} area_ids: set[str] = set() - for area in all_areas: + for area, key in all_areas: area_id: core.ID = area[CONF_ID] - check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + check_hash_collision(area_id, area_hashes, "Area", [key, area_id.id]) area_ids.add(area_id.id) # Validate device hash collisions and area references device_hashes: dict[int, str] = {} - for device in config[CONF_DEVICES]: + for device in config.get(CONF_DEVICES, []): device_id: core.ID = device[CONF_ID] check_hash_collision( device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] @@ -329,9 +329,6 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) - - PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, diff --git a/script/ci-custom.py b/script/ci-custom.py index 7d0680a4919..ad39f92005a 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1006,6 +1006,18 @@ def lint_log_in_header(fname, line, col, content): ) +@lint_content_find_check( + "FINAL_VALIDATE_SCHEMA", + include=["esphome/core/*.py"], + exclude=["esphome/core/entity_helpers.py"], +) +def lint_final_validate_in_core(fname, line, col, content): + return ( + "FINAL_VALIDATE_SCHEMA in esphome/core/ is not picked up by the component loader. " + "Use CoreFinalValidateStep in esphome/config.py instead." + ) + + def main(): colorama.init() diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 474d31a90af..6fa8f7ed43a 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -248,6 +248,24 @@ def test_area_id_hash_collision( ) +def test_area_singular_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area hash collisions between singular area: and areas: list are detected.""" + result = load_config_from_fixture( + yaml_file, "area_singular_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + captured = capsys.readouterr() + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + # Error path should point to 'areas' (where the colliding entry is), not 'area' + assert "areas" in captured.out + + def test_device_duplicate_id( yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml new file mode 100644 index 00000000000..6e137f5f6e0 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + area: + id: test_2258 + name: "Area 1" + areas: + - id: d6ka + name: "Area 2" + +host: From 7a7c33fdb16f2b32d95d194ed5130471e6bb99e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 28 Mar 2026 15:38:06 -1000 Subject: [PATCH 1064/2056] [esp32_ble_server] Fix set_value action with static data lists (#15285) --- .../components/esp32_ble_server/ble_server_automations.h | 2 ++ tests/components/esp32_ble_server/common.yaml | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index fe186002803..0bbfdffd5bd 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -70,6 +70,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T public: BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) + void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { // If the listener is already set, do nothing @@ -115,6 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts... public: BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) + void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 7fe0b2eb5f1..4e34049038a 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -69,3 +69,11 @@ esp32_ble_server: - ble_server.descriptor.set_value: id: test_change_descriptor value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data(); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: + data: [0xfc, 0xef, 0xfe, 0x86] + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: + data: [0x01, 0x02, 0x03] From d9adb078aa2fa4ba1db4912672d7904ea1038818 Mon Sep 17 00:00:00 2001 From: Tobias Stanzel <tobi.stanzel@gmail.com> Date: Sun, 29 Mar 2026 19:41:00 +0200 Subject: [PATCH 1065/2056] [tm1637] Add buffer manipulation methods (#13686) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/tm1637/tm1637.cpp | 6 ++++++ esphome/components/tm1637/tm1637.h | 3 +++ tests/components/tm1637/common.yaml | 2 ++ 3 files changed, 11 insertions(+) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index f9c876f40c5..da9adb59a4f 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -348,6 +348,12 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { return pos - start_pos; } uint8_t TM1637Display::print(const char *str) { return this->print(0, str); } + +void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) { + uint8_t len = std::min(length, (uint8_t) sizeof(this->buffer_)); + memcpy(this->buffer_, data, len); +} + uint8_t TM1637Display::printf(uint8_t pos, const char *format, ...) { va_list arg; va_start(arg, format); diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index b9e96119e9b..c1fbabb21b5 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -47,6 +47,9 @@ class TM1637Display : public PollingComponent { /// Print `str` at position 0. uint8_t print(const char *str); + /// Set raw buffer bytes from data array up to length bytes. + void set_buffer(const uint8_t *data, uint8_t length); + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_length(uint8_t length) { this->length_ = length; } diff --git a/tests/components/tm1637/common.yaml b/tests/components/tm1637/common.yaml index 8d01e29877c..b6debc055dc 100644 --- a/tests/components/tm1637/common.yaml +++ b/tests/components/tm1637/common.yaml @@ -5,3 +5,5 @@ display: intensity: 3 lambda: |- it.print("1234"); + static const uint8_t buf[] = {0x3f, 0x06, 0x5b, 0x4f | 0x80}; + it.set_buffer(buf, sizeof(buf)); From a91e6d92f6e922d37283c6b9679e7ce458785716 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:32:43 -1000 Subject: [PATCH 1066/2056] [core] Remove dead get_loop_priority code (#15242) --- esphome/components/ch422g/ch422g.cpp | 6 ------ esphome/components/ch422g/ch422g.h | 3 --- esphome/components/ch423/ch423.cpp | 6 ------ esphome/components/ch423/ch423.h | 3 --- esphome/components/deep_sleep/deep_sleep_component.cpp | 6 ------ esphome/components/deep_sleep/deep_sleep_component.h | 3 --- esphome/components/pca9554/pca9554.cpp | 5 ----- esphome/components/pca9554/pca9554.h | 4 ---- esphome/components/pcf8574/pcf8574.cpp | 5 ----- esphome/components/pcf8574/pcf8574.h | 3 --- esphome/components/status_led/light/status_led_light.h | 3 --- esphome/components/status_led/status_led.cpp | 3 --- esphome/components/status_led/status_led.h | 3 --- esphome/components/wifi/wifi_component.cpp | 6 ------ esphome/components/wifi/wifi_component.h | 4 ---- esphome/core/application.cpp | 6 ------ esphome/core/component.cpp | 4 ---- esphome/core/component.h | 10 ---------- esphome/core/defines.h | 1 - 19 files changed, 84 deletions(-) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 5f5e848c76e..fc856cd5636 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,12 +124,6 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method very early in the loop, so that we cache read values -// before other components call our digital_read() method. -float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 1b965682093..6e6bdad64a0 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,9 +23,6 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 805d8df8774..8424d130b4b 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,12 +129,6 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method very early in the loop, so that we cache read values -// before other components call our digital_read() method. -float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index d85648a8f96..d384971a72e 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,9 +22,6 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; protected: diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 0511518419d..3dd1b709308 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,12 +40,6 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } -#ifdef USE_LOOP_PRIORITY -float DeepSleepComponent::get_loop_priority() const { - return -100.0f; // run after everything else is ready -} -#endif - void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 14713d51a1d..9090f91876a 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,9 +113,6 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index d94767ef070..adc7bc0fb5c 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -122,11 +122,6 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method early to invalidate cache before any other components access the pins -float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 6dd15ccb4bf..1d877f9ce29 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -23,10 +23,6 @@ class PCA9554Component : public Component, float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif - void dump_config() override; void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index fa9496e7e42..d3ec31436de 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -99,11 +99,6 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method early to invalidate cache before any other components access the pins -float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 23bccc26c9d..b039173789f 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -26,9 +26,6 @@ class PCF8574Component : public Component, void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index a5c98d90d46..3a745e0017a 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,9 +30,6 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override { return 50.0f; } -#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 93a8d4b38e9..a792110eebb 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,9 +28,6 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } -#ifdef USE_LOOP_PRIORITY -float StatusLED::get_loop_priority() const { return 50.0f; } -#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index f262eb260c4..a4b5db93d7e 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,9 +14,6 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif protected: GPIOPin *pin_; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 620d1a083dc..db203326679 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -970,12 +970,6 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP -#ifdef USE_LOOP_PRIORITY -float WiFiComponent::get_loop_priority() const { - return 10.0f; // before other loop components -} -#endif - void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::clear_sta() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 55e532c37d2..8dfe5fa7afa 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -463,10 +463,6 @@ class WiFiComponent final : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif - /// Reconnect WiFi if required. void loop() override; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index ce15aed1e28..5cb8a5bb244 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -99,12 +99,6 @@ void Application::setup() { if (component->can_proceed()) continue; -#ifdef USE_LOOP_PRIORITY - // Sort components 0 through i by loop priority - insertion_sort_by_priority<decltype(this->components_.begin()), &Component::get_loop_priority>( - this->components_.begin(), this->components_.begin() + i + 1); -#endif - do { uint8_t new_app_state = STATUS_LED_WARNING; uint32_t now = millis(); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index caaea891432..2ad82e1172b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -85,10 +85,6 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again -#ifdef USE_LOOP_PRIORITY -float Component::get_loop_priority() const { return 0.0f; } -#endif - float Component::get_setup_priority() const { return setup_priority::DATA; } void Component::setup() {} diff --git a/esphome/core/component.h b/esphome/core/component.h index 46cd77b0343..d08b1abfcde 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -115,16 +115,6 @@ class Component { void set_setup_priority(float priority); - /** priority of loop(). higher -> executed earlier - * - * Defaults to 0. - * - * @return The loop priority of this component - */ -#ifdef USE_LOOP_PRIORITY - virtual float get_loop_priority() const; -#endif - void call(); virtual void on_shutdown() {} diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8cf331c4d60..7259167a527 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -357,7 +357,6 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) -#define USE_LOOP_PRIORITY #define USE_RP2040_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C From 8a802ca666b608426644bfa2fc4caf7e0ab72577 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:54:07 -1000 Subject: [PATCH 1067/2056] [benchmark] Add BLE raw advertisement proto encode benchmarks (#15289) --- script/cpp_benchmark.py | 5 ++ tests/benchmarks/components/api/__init__.py | 14 +++ .../components/api/bench_proto_encode.cpp | 89 +++++++++++++++++++ .../bluetooth_proxy/bluetooth_proxy.h | 38 ++++++++ 4 files changed, 146 insertions(+) create mode 100644 tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index a54d3752df6..92faa05819a 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -21,6 +21,10 @@ BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components" # Path to /tests/benchmarks/core (always included, not a component) CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core" +# Stub headers for ESP32-only components (e.g. bluetooth_proxy) that +# allow benchmarks to compile on the host platform. +STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs" + PLATFORMIO_OPTIONS = { "build_unflags": [ "-Os", # remove default size-opt @@ -29,6 +33,7 @@ PLATFORMIO_OPTIONS = { "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() + f"-I{STUBS_DIR}", # stub headers for ESP32-only components ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark # library dependency from nested includes. diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py index 0687c3f87fb..eb86492964d 100644 --- a/tests/benchmarks/components/api/__init__.py +++ b/tests/benchmarks/components/api/__init__.py @@ -1,3 +1,4 @@ +import esphome.codegen as cg from tests.testing_helpers import ComponentManifestOverride @@ -5,3 +6,16 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # api must run its to_code to define USE_API, USE_API_PLAINTEXT, # and add the noise-c library dependency. manifest.enable_codegen() + + original_to_code = manifest.to_code + + async def to_code(config): + await original_to_code(config) + # Enable BLE proto message types for benchmarks. The real + # bluetooth_proxy component is ESP32-only; a lightweight stub + # header in tests/benchmarks/stubs/ satisfies the include. + cg.add_define("USE_BLUETOOTH_PROXY") + cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3) + cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 656c1e17dba..1e2efcd2813 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -295,4 +295,93 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { } BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh); +// --- BluetoothLERawAdvertisementsResponse (12 adverts, highest-volume BLE message) --- + +#ifdef USE_BLUETOOTH_PROXY + +static BluetoothLERawAdvertisementsResponse make_ble_raw_advs_12() { + static const uint8_t fake_adv_data[] = { + 0x02, 0x01, 0x06, 0x03, 0x03, 0x9F, 0xFE, 0x17, 0x16, 0x9F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + BluetoothLERawAdvertisementsResponse msg; + msg.advertisements_len = 12; + for (int i = 0; i < 12; i++) { + auto &adv = msg.advertisements[i]; + adv.address = 0xAABBCCDD0000ULL + i; + adv.rssi = -60 - i; + adv.address_type = 1; + memcpy(adv.data, fake_adv_data, sizeof(fake_adv_data)); + adv.data_len = sizeof(fake_adv_data); + } + return msg; +} + +static void CalculateSize_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_BLERawAdvs12); + +static void Encode_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + APIBuffer buffer; + uint32_t total_size = msg.calculate_size(); + buffer.resize(total_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_BLERawAdvs12); + +static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_BLERawAdvs12); + +static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh); + +#endif // USE_BLUETOOTH_PROXY + } // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h new file mode 100644 index 00000000000..0934e0d4ed6 --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -0,0 +1,38 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp needs when USE_BLUETOOTH_PROXY is defined, +// without pulling in ESP32 BLE dependencies. +#pragma once + +#include "esphome/components/api/api_pb2.h" + +namespace esphome { +namespace api { +class APIConnection; +} // namespace api + +namespace bluetooth_proxy { + +class BluetoothProxy { + public: + api::APIConnection *get_api_connection() const { return nullptr; } + void subscribe_api_connection(api::APIConnection *conn, uint32_t flags) {} + void unsubscribe_api_connection(api::APIConnection *conn) {} + void bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {} + void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {} + void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {} + void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {} + void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {} + void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {} + void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {} + void send_connections_free(api::APIConnection *conn) {} + void bluetooth_scanner_set_mode(bool active) {} + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {} + uint32_t get_feature_flags() const { return 0; } + void get_bluetooth_mac_address_pretty(char *buf) const { buf[0] = '\0'; } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern BluetoothProxy *global_bluetooth_proxy; + +} // namespace bluetooth_proxy +} // namespace esphome From 1f3fd60d294eed3162328520077119541666101f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:55:39 -1000 Subject: [PATCH 1068/2056] [version] Remove duplicate build_info_data.h include (#15288) --- esphome/components/version/version_text_sensor.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 8aec98d2da3..34c7aae6bca 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,6 +1,5 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" -#include "esphome/core/build_info_data.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" @@ -36,7 +35,9 @@ void VersionTextSensor::setup() { if (!this->hide_timestamp_) { size_t len = strlen(version_str); ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1); - ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1); + char build_time_buf[Application::BUILD_TIME_STR_SIZE]; + App.get_build_time_string(build_time_buf); + strncat(version_str, build_time_buf, sizeof(version_str) - strlen(version_str) - 1); } // The closing parenthesis is part of the config-hash suffix and must From 2a97eca00b82e8ef10c5203f9b1865ac6c5cc6de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:55:52 -1000 Subject: [PATCH 1069/2056] [sensor] Use std::array in ValueList/FilterOut/ThrottleWithPriority filters (#15265) --- esphome/components/sensor/__init__.py | 6 ++-- esphome/components/sensor/filter.cpp | 38 ++++++-------------- esphome/components/sensor/filter.h | 51 +++++++++++++++++++-------- esphome/core/helpers.h | 18 ++++++++++ 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 19d03a0afc7..5569567de12 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -381,7 +381,7 @@ async def filter_out_filter_to_code(config, filter_id): if not isinstance(config, list): config = [config] template_ = [await cg.templatable(x, [], float) for x in config] - return cg.new_Pvariable(filter_id, template_) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_) QUANTILE_SCHEMA = cv.All( @@ -650,7 +650,9 @@ async def throttle_with_priority_filter_to_code(config, filter_id): if not isinstance(config[CONF_VALUE], list): config[CONF_VALUE] = [config[CONF_VALUE]] template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]] - return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_ + ) HEARTBEAT_SCHEMA = cv.Schema( diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index d995ee4111c..66a9e9555bc 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -222,16 +222,14 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_ optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } -// ValueListFilter (base class) -ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {} - -bool ValueListFilter::value_matches_any_(float sensor_value) { - int8_t accuracy = this->parent_->get_accuracy_decimals(); +// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations) +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count) { + int8_t accuracy = parent->get_accuracy_decimals(); float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); - for (auto &filter_value : this->values_) { - float fv = filter_value.value(); + for (size_t i = 0; i < count; i++) { + float fv = values[i].value(); // Handle NaN comparison if (std::isnan(fv)) { @@ -248,16 +246,6 @@ bool ValueListFilter::value_matches_any_(float sensor_value) { return false; } -// FilterOutValueFilter -FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out) - : ValueListFilter(values_to_filter_out) {} - -optional<float> FilterOutValueFilter::new_value(float value) { - if (this->value_matches_any_(value)) - return {}; // Filter out - return value; // Pass through -} - // ThrottleFilter ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {} optional<float> ThrottleFilter::new_value(float value) { @@ -269,17 +257,13 @@ optional<float> ThrottleFilter::new_value(float value) { return {}; } -// ThrottleWithPriorityFilter -ThrottleWithPriorityFilter::ThrottleWithPriorityFilter( - uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values) - : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} - -optional<float> ThrottleWithPriorityFilter::new_value(float value) { +// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp) +optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, + size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) { const uint32_t now = App.get_loop_component_start_time(); - // Allow value through if: no previous input, time expired, or is prioritized - if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || - this->value_matches_any_(value)) { - this->last_input_ = now; + if (last_input == 0 || now - last_input >= min_time_between_inputs || + value_list_matches_any(parent, value, values, count)) { + last_input = now; return value; } return {}; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 6a76bd373e3..80fa14742c2 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #ifdef USE_SENSOR_FILTER +#include <array> #include <utility> #include <vector> #include "esphome/core/automation.h" @@ -328,28 +329,42 @@ class MultiplyFilter : public Filter { TemplatableValue<float> multiplier_; }; -/** Base class for filters that compare sensor values against a list of configured values. +/// Non-template helper for value matching (implementation in filter.cpp) +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count); + +/** Base class for filters that compare sensor values against a fixed list of configured values. * - * This base class provides common functionality for filters that need to check if a sensor - * value matches any value in a configured list, with proper handling of NaN values and - * accuracy-based rounding for comparisons. + * Templated on N (the number of values) so the list is stored inline in a std::array, + * avoiding heap allocation and the overhead of FixedVector. + * + * @tparam N Number of values in the filter list, set by code generation to match + * the exact number of values configured in YAML. */ -class ValueListFilter : public Filter { +template<size_t N> class ValueListFilter : public Filter { protected: - explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values); + explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) { + init_array_from(this->values_, values); + } /// Check if sensor value matches any configured value (with accuracy rounding) - bool value_matches_any_(float sensor_value); + bool value_matches_any_(float sensor_value) { + return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N); + } - FixedVector<TemplatableValue<float>> values_; + std::array<TemplatableValue<float>, N> values_{}; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. -class FilterOutValueFilter : public ValueListFilter { +template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> { public: - explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out); + explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out) + : ValueListFilter<N>(values_to_filter_out) {} - optional<float> new_value(float value) override; + optional<float> new_value(float value) override { + if (this->value_matches_any_(value)) + return {}; // Filter out + return value; // Pass through + } }; class ThrottleFilter : public Filter { @@ -363,13 +378,21 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp) +optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, + size_t count, uint32_t &last_input, uint32_t min_time_between_inputs); + /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. -class ThrottleWithPriorityFilter : public ValueListFilter { +template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> { public: explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::initializer_list<TemplatableValue<float>> prioritized_values); + std::initializer_list<TemplatableValue<float>> prioritized_values) + : ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} - optional<float> new_value(float value) override; + optional<float> new_value(float value) override { + return throttle_with_priority_new_value(this->parent_, value, this->values_.data(), N, this->last_input_, + this->min_time_between_inputs_); + } protected: uint32_t last_input_{0}; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 82c6b3833ce..913614f5641 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2,6 +2,7 @@ #include <algorithm> #include <array> +#include <cassert> #include <cmath> #include <cstdarg> #include <cstdint> @@ -497,6 +498,23 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> index_type capacity_{0}; }; +/// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen), +/// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). +/// N is set by code generation; assert catches mismatches in debug/integration tests. +template<typename T, size_t N> inline void init_array_from(std::array<T, N> &dest, std::initializer_list<T> src) { +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif + if constexpr (std::is_trivially_copyable_v<T>) { + __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); + } else { + size_t i = 0; + for (const auto &v : src) { + dest[i++] = v; + } + } +} + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time From 5da3253f4b67128b991c7631b9730488072f9a6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:57:52 -1000 Subject: [PATCH 1070/2056] [esp8266] Add enable_scanf_float option (#15284) --- esphome/components/esp8266/__init__.py | 62 +++++++++++++++---- .../components/esp8266/test.esp8266-ard.yaml | 3 + tests/unit_tests/components/test_esp8266.py | 62 +++++++++++++++++++ 3 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/components/test_esp8266.py diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 16043b6d69a..2081145096e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +import re import esphome.codegen as cg import esphome.config_validation as cv @@ -18,8 +19,9 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.helpers import copy_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( @@ -40,12 +42,42 @@ from .const import ( ) from .gpio import PinInitialState, add_pin_initial_states_array +CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float" +# Heuristically matches scanf/sscanf calls with float format specifiers. +# Standard scanf float conversions: %f %F %e %E %g %G %a %A +# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length) +# Also matches non-standard patterns like %.2f as a heuristic — these are +# invalid in scanf but users may write them by analogy with printf. +# Uses [^;]*? to stay within a single statement, preventing false positives +# from e.g. sscanf(buf, "%d", &x); printf("%f", val); +_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]") + CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def lambdas_use_scanf_float(config: ConfigType) -> bool: + """Check if any lambda in the config uses scanf with a float format specifier. + + Comments are stripped before matching to avoid false positives from + commented-out code. The cost of a false positive is only ~8KB flash. + """ + stack: list = [config] + while stack: + obj = stack.pop() + if isinstance(obj, Lambda): + src = obj.comment_remover(obj.value) + if _SCANF_FLOAT_RE.search(src): + return True + elif isinstance(obj, dict): + stack.extend(obj.values()) + elif isinstance(obj, list): + stack.extend(obj) + return False + + def set_core_data(config): CORE.data[KEY_ESP8266] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 @@ -181,6 +213,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean, } ), set_core_data, @@ -201,16 +234,23 @@ async def to_code(config): cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option( - "extra_scripts", - [ - "pre:testing_mode.py", - "pre:exclude_updater.py", - "pre:exclude_waveform.py", - "pre:remove_float_scanf.py", - "post:post_build.py", - ], - ) + enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT) + if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config): + enable_scanf_float = True + _LOGGER.warning( + "Lambda uses scanf with a float format specifier; " + "enabling scanf float support (~8KB flash)" + ) + + extra_scripts = [ + "pre:testing_mode.py", + "pre:exclude_updater.py", + "pre:exclude_waveform.py", + ] + if not enable_scanf_float: + extra_scripts.append("pre:remove_float_scanf.py") + extra_scripts.append("post:post_build.py") + cg.add_platformio_option("extra_scripts", extra_scripts) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index c77218f7a3c..ba70c1a6a4f 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -14,3 +14,6 @@ esphome: assert(x == 95); x = clamp_at_most(x, 40); assert(x == 40); + - lambda: |- + float value = 0.0f; + sscanf("3.14", "%f", &value); diff --git a/tests/unit_tests/components/test_esp8266.py b/tests/unit_tests/components/test_esp8266.py new file mode 100644 index 00000000000..318fd2d889f --- /dev/null +++ b/tests/unit_tests/components/test_esp8266.py @@ -0,0 +1,62 @@ +"""Tests for ESP8266 component.""" + +import pytest + +from esphome.components.esp8266 import lambdas_use_scanf_float +from esphome.core import Lambda +from esphome.types import ConfigType + + +@pytest.mark.parametrize( + ("src", "expected"), + [ + # Basic float formats + ('sscanf(buf, "%f", &v)', True), + ('sscanf(buf, "%F", &v)', True), + ('sscanf(buf, "%e", &v)', True), + ('sscanf(buf, "%E", &v)', True), + ('sscanf(buf, "%g", &v)', True), + ('sscanf(buf, "%G", &v)', True), + ('sscanf(buf, "%a", &v)', True), + ('sscanf(buf, "%A", &v)', True), + # With modifiers + ('sscanf(buf, "%lf", &v)', True), + ('sscanf(buf, "%Lf", &v)', True), + ('sscanf(buf, "%8lf", &v)', True), + ('sscanf(buf, "%*f")', True), + ('sscanf(buf, "%.2f", &v)', True), + # Mixed formats + ('sscanf(buf, "%d,%f", &a, &b)', True), + # fscanf and std::sscanf + ('fscanf(fp, "%f", &v)', True), + ('std::sscanf(buf, "%f", &v)', True), + # Multi-line + ('sscanf(buf,\n"%f", &v)', True), + # No float format + ('sscanf(buf, "%d", &v)', False), + ('sscanf(buf, "%s", s)', False), + # printf not scanf + ('printf("%f", val)', False), + # %f in a different statement after scanf + ('sscanf(buf, "%d", &x); printf("%f", val);', False), + # scanf %f in comment only + ('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False), + ('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False), + ], +) +def test_lambdas_use_scanf_float(src: str, expected: bool) -> None: + """Test scanf float detection in lambda source.""" + config: ConfigType = {"test": [Lambda(src)]} + assert lambdas_use_scanf_float(config) is expected + + +def test_lambdas_use_scanf_float_no_lambdas() -> None: + """Test with config containing no lambdas.""" + config: ConfigType = {"key": "value", "list": [1, 2]} + assert lambdas_use_scanf_float(config) is False + + +def test_lambdas_use_scanf_float_nested() -> None: + """Test detection in deeply nested config.""" + config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}} + assert lambdas_use_scanf_float(config) is True From 584807b03900ea488f46fde1cd3a1cd13234bed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:58:03 -1000 Subject: [PATCH 1071/2056] [ld2410] Fix flaky integration test race condition (#15299) --- tests/integration/test_uart_mock_ld2410.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py index ce0e1bb7ec2..88d6f2cbacd 100644 --- a/tests/integration/test_uart_mock_ld2410.py +++ b/tests/integration/test_uart_mock_ld2410.py @@ -73,9 +73,16 @@ async def test_uart_mock_ld2410( ], ) - # Signal when we see recovery frame values + # Signal when we see ALL recovery frame values to avoid race where some + # arrive after the waiter fires but before we index into the lists recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["moving_distance"] + and pytest.approx(75.0) in collector.sensor_states["still_distance"] + and pytest.approx(100.0) in collector.sensor_states["moving_energy"] + and pytest.approx(80.0) in collector.sensor_states["still_energy"] + and pytest.approx(127.0) in collector.sensor_states["detection_distance"] + ) ) async with ( From c2b8ea33610be67f2ea7cf043e2276552d1c558a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 12:02:29 -1000 Subject: [PATCH 1072/2056] [web_server_base] Reduce sizeof(WebServerBase) by 4 bytes (#15251) --- esphome/components/web_server_base/web_server_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 48e13ad71e2..2aa3ae215c4 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -135,7 +135,7 @@ class WebServerBase { uint16_t get_port() const { return port_; } protected: - int initialized_{0}; + uint8_t initialized_{0}; uint16_t port_{80}; AsyncWebServer *server_{nullptr}; std::vector<AsyncWebHandler *> handlers_; From 38fa8925da52981021c334e6f88ba4e521208fc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 12:02:47 -1000 Subject: [PATCH 1073/2056] [ai] Add automation, callback manager, and test grouping docs (#15243) --- .ai/instructions.md | 174 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 2 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index a7e08f9c4d5..86f554e9cec 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -239,6 +239,123 @@ This document provides essential context for AI models interacting with this pro var = await switch.new_switch(config) ``` +* **Automations (Triggers, Actions, Conditions):** + + Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true). + + * **Triggers -- Callback method (preferred):** + + Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema. + + **Python:** + ```python + from esphome import automation + + CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + }).extend(cv.COMPONENT_SCHEMA) + + async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for conf in config.get(CONF_ON_STATE, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [(bool, "x")], conf + ) + ``` + + `build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`). + + For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`: + ```python + for conf_key, forwarder in ( + (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), + (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) + ``` + + **C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation): + ```cpp + class MyComponent : public Component { + public: + // Must be a template -- accepts both std::function and pointer-sized forwarder structs + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } + protected: + // Use CallbackManager when callbacks are always registered (e.g. core components) + CallbackManager<void(bool)> state_callback_; + // Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes + // (nullptr vs empty std::vector) per instance when no callbacks are added + // LazyCallbackManager<void(bool)> state_callback_; + }; + ``` + + * **Triggers -- Trigger class method:** + + Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic). + + **Python:** + ```python + TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template()) + + CONFIG_SCHEMA = cv.Schema({ + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} + ), + }) + + async def to_code(config): + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + ``` + + **C++:** + ```cpp + class TurnOnTrigger : public Trigger<> { + public: + explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} { + parent->add_on_state_callback([this](bool state) { + if (state && !this->last_on_) + this->trigger(); + this->last_on_ = state; + }); + } + protected: + bool last_on_; + }; + ``` + + * **Actions:** + ```cpp + template<typename... Ts> class MyAction : public Action<Ts...> { + public: + explicit MyAction(MyComponent *parent) : parent_(parent) {} + void play(const Ts &...) override { this->parent_->do_something(); } + protected: + MyComponent *parent_; + }; + ``` + Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use. + + * **Conditions:** + ```cpp + template<typename... Ts> class MyCondition : public Condition<Ts...> { + public: + explicit MyCondition(MyComponent *parent) : parent_(parent) {} + bool check(const Ts &...) override { return this->parent_->is_active(); } + protected: + MyComponent *parent_; + }; + ``` + Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`. + * **Configuration Validation:** * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. @@ -274,10 +391,39 @@ This document provides essential context for AI models interacting with this pro * **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows: ``` tests/ - ├── test_build_components/ # Base test configurations - └── components/[component]/ # Component-specific tests + ├── test_build_components/ + │ └── common/ # Shared bus packages (uart, i2c, spi, etc.) + │ ├── uart/ # UART at default baud rate + │ ├── uart_115200/ # UART at 115200 baud + │ ├── i2c/ # I2C bus + │ └── spi/ # SPI bus + └── components/[component]/ + ├── common.yaml # Component-only config (no bus definitions) + ├── test.esp32-idf.yaml + ├── test.esp8266-ard.yaml + └── test.rp2040-ard.yaml ``` Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms. + + * **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`: + ```yaml + # test.esp32-idf.yaml — use packages for buses + packages: + uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml + + <<: !include common.yaml + ``` + ```yaml + # common.yaml — component config only, NO bus definitions + my_component: + id: my_instance + + sensor: + - platform: my_component + name: My Sensor + ``` + Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time. + * **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use: ```bash ./script/test_component_grouping.py -e config --all @@ -417,6 +563,30 @@ This document provides essential context for AI models interacting with this pro Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`). + **Callback Managers:** + + ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method. + + | Type | Idle overhead (32-bit) | When to use | + |------|----------------------|-------------| + | `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered | + | `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) | + + `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers. + + **Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature: + ```cpp + // Bad -- forces heap allocation for forwarder structs + void add_on_state_callback(std::function<void(bool)> &&callback) { + this->state_callback_.add(std::move(callback)); + } + + // Good -- accepts any callable without forcing std::function wrapping + template<typename F> void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward<F>(callback)); + } + ``` + * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. **Bad Pattern (Module-Level Globals):** From a9aaf29d837b9228437f3954c1b2fb2396035ce6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 12:09:21 -1000 Subject: [PATCH 1074/2056] [core] Shrink Component from 12 to 8 bytes per instance (#15103) --- esphome/core/component.cpp | 24 ++++-- esphome/core/component.h | 37 +++++--- esphome/cpp_helpers.py | 123 ++++++++++++++++++++++++++- tests/unit_tests/test_cpp_helpers.py | 72 +++++++++++++++- 4 files changed, 230 insertions(+), 26 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2ad82e1172b..00a36fce3d8 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -84,6 +84,8 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again +// Threshold in ms (computed from centiseconds constant in component.h) +static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U; float Component::get_setup_priority() const { return setup_priority::DATA; } @@ -268,15 +270,18 @@ void Component::call() { break; } } +const LogString *Component::get_component_log_str() const { + return component_source_lookup(this->component_source_index_); +} bool Component::should_warn_of_blocking(uint32_t blocking_time) { - if (blocking_time > this->warn_if_blocking_over_) { - // Prevent overflow when adding increment - if we're about to overflow, just max out - if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time || - blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) { - this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max(); - } else { - this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS); - } + // Convert centisecond threshold to milliseconds for comparison + uint32_t threshold_ms = static_cast<uint32_t>(this->warn_if_blocking_over_) * 10U; + if (blocking_time > threshold_ms) { + // Set new threshold: blocking_time + increment, converted back to centiseconds + uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; + uint32_t new_cs = new_threshold_ms / 10U; + // Saturate at uint8_t max (255 = 2550ms) + this->warn_if_blocking_over_ = static_cast<uint8_t>(new_cs > 255U ? 255U : new_cs); return true; } return false; @@ -537,4 +542,7 @@ void clear_setup_priority_overrides() { } #endif +// Weak default for component_source_lookup - overridden by generated code +__attribute__((weak)) const LogString *component_source_lookup(uint8_t) { return LOG_STR("<unknown>"); } + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index d08b1abfcde..c390a205f0f 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -11,6 +11,10 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) + namespace esphome { // Forward declaration for LogString @@ -79,11 +83,14 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; - // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; +inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms) + +/// Lookup component source name by index (1-based). Generated by Python codegen. +/// Weak default returns "<unknown>" so builds without codegen still link. +const LogString *component_source_lookup(uint8_t index); class Component { public: @@ -275,23 +282,25 @@ class Component { bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } - /** Set where this component was loaded from for some debug messages. - * - * This is set by the ESPHome core, and should not be called manually. - */ - void set_component_source(const LogString *source) { component_source_ = source; } /** Get the integration where this component was declared as a LogString for logging. * * Returns LOG_STR("<unknown>") if source not set */ - const LogString *get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_; - } + const LogString *get_component_log_str() const; bool should_warn_of_blocking(uint32_t blocking_time); protected: friend class Application; + friend void ::setup(); + friend void ::original_setup(); + + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core during setup, and should not be called manually. + * @param index 1-based index into the component source lookup table (0 = not set) + */ + void set_component_source_(uint8_t index) { this->component_source_index_ = index; } virtual void call_setup(); void call_dump_config_(); @@ -509,9 +518,9 @@ class Component { void status_clear_warning_slow_path_(); void status_clear_error_slow_path_(); - // Ordered for optimal packing on 32-bit systems - const LogString *component_source_{nullptr}; - uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) + // Ordered for optimal packing on 32-bit systems (8 bytes total with vtable) + uint8_t component_source_index_{0}; ///< Index into component source PROGMEM lookup table (0 = not set) + uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING @@ -588,6 +597,8 @@ class WarnIfComponentBlockingGuard { this->record_runtime_stats_(); #endif #ifndef USE_BENCHMARK + // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) + static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U; if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8f8c6931403..e7ff2965c83 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import logging from esphome.const import ( @@ -7,15 +8,130 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, ID, coroutine +from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import ( + RawStatement, + add, + add_define, + add_global, + get_variable, +) from esphome.cpp_types import App +from esphome.helpers import cpp_string_escape from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry _LOGGER = logging.getLogger(__name__) +_COMPONENT_SOURCE_DOMAIN = "component_source_pool" + +# Maximum unique component source names (8-bit index, 0 = not set) +_MAX_COMPONENT_SOURCES = 0xFF # 255 + + +@dataclass +class ComponentSourcePool: + """Pool of component source names for PROGMEM lookup table. + + Source names are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (returns LOG_STR("<unknown>")). At render time, + the pool generates a C++ PROGMEM table + lookup function. + """ + + sources: dict[str, int] = field(default_factory=dict) + table_registered: bool = False + + +def _get_source_pool() -> ComponentSourcePool: + """Get or create the component source pool from CORE.data.""" + if _COMPONENT_SOURCE_DOMAIN not in CORE.data: + CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool() + return CORE.data[_COMPONENT_SOURCE_DOMAIN] + + +def _ensure_source_table_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_source_pool() + if pool.table_registered: + return + pool.table_registered = True + CORE.add_job(_generate_component_source_table) + + +def register_component_source(name: str) -> int: + """Register a component source name and return its 1-based index. + + Deduplicates: multiple components from the same source share one index. + """ + if not name: + return 0 + pool = _get_source_pool() + if name in pool.sources: + return pool.sources[name] + idx = len(pool.sources) + 1 + if idx > _MAX_COMPONENT_SOURCES: + _LOGGER.warning( + "Too many unique component source names (max %d), '%s' will show as '<unknown>'", + _MAX_COMPONENT_SOURCES, + name, + ) + return 0 + pool.sources[name] = idx + _ensure_source_table_registered() + return idx + + +def _generate_source_table_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ PROGMEM table + LogString* lookup for component sources. + + Same pattern as entity_helpers._generate_category_code but returns + const LogString* instead of const char* (needed for LOG_STR_ARG). + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + count = len(sorted_strings) + + # Emit individual PROGMEM char arrays so string data lives in flash on ESP8266 + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + + entries = ", ".join(var_names) + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{") + lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");') + lines.append(" return reinterpret_cast<const LogString *>(") + lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") + lines.append("}") + return "\n".join(lines) + "\n" + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_component_source_table() -> None: + """Generate the component source lookup table as a FINAL-priority job. + + Runs after all component to_code() calls have registered their sources. + """ + pool = _get_source_pool() + if code := _generate_source_table_code( + "COMP_SRC_TABLE", "component_source_lookup", pool.sources + ): + add_global( + RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome") + ) + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -77,7 +193,8 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(LogStringLiteral(name))) + idx = register_component_source(name) + add(var.set_component_source_(idx)) add(App.register_component_(var)) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 5b6eed156fa..52424a7cb22 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -1,8 +1,10 @@ +import logging from unittest.mock import Mock import pytest from esphome import const, cpp_helpers as ch +from esphome.cpp_helpers import ComponentSourcePool, register_component_source @pytest.mark.asyncio @@ -23,7 +25,7 @@ async def test_register_component(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() @@ -59,7 +61,7 @@ async def test_register_component__with_setup_priority(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() @@ -78,3 +80,69 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] + + +def test_register_component_source_empty_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ch, "CORE", Mock(data={})) + assert register_component_source("") == 0 + + +def test_register_component_source_deduplicates( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(ch, "CORE", Mock(data={})) + idx1 = register_component_source("wifi") + idx2 = register_component_source("api") + idx3 = register_component_source("wifi") + assert idx1 == 1 + assert idx2 == 2 + assert idx3 == 1 # deduplicated + + +def test_generate_source_table_code_empty() -> None: + from esphome.cpp_helpers import _generate_source_table_code + + assert _generate_source_table_code("TBL", "lookup", {}) == "" + + +def test_generate_source_table_code_non_empty() -> None: + from esphome.cpp_helpers import _generate_source_table_code + + code = _generate_source_table_code("TBL", "lookup", {"wifi": 1, "api": 2}) + assert "PROGMEM" in code + assert "wifi" in code + assert "api" in code + assert "lookup" in code + assert "index == 0" in code + assert "progmem_read_ptr" in code + assert "index > 2" in code + + +@pytest.mark.asyncio +async def test_generate_component_source_table_empty_pool( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that _generate_component_source_table does nothing with an empty pool.""" + from esphome.cpp_helpers import _generate_component_source_table + + monkeypatch.setattr(ch, "CORE", Mock(data={})) + add_global_mock = Mock() + monkeypatch.setattr(ch, "add_global", add_global_mock) + await _generate_component_source_table() + add_global_mock.assert_not_called() + + +def test_register_component_source_overflow_warns( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Pre-fill pool to max + pool = ComponentSourcePool( + sources={f"comp_{i}": i + 1 for i in range(0xFF)}, + table_registered=True, + ) + monkeypatch.setattr(ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool})) + with caplog.at_level(logging.WARNING): + idx = register_component_source("overflow_component") + assert idx == 0 + assert "Too many unique component source names" in caplog.text + assert "overflow_component" in caplog.text From d6475eaeed764bb30589323f832cf305d94bd69f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 12:15:18 -1000 Subject: [PATCH 1075/2056] [binary_sensor] Remove redundant `optional<bool>` state_, save 8 bytes per instance (#15095) --- .../binary_sensor/binary_sensor.cpp | 7 -- .../components/binary_sensor/binary_sensor.h | 21 +++-- esphome/core/entity_base.h | 88 +++++++++++++------ 3 files changed, 77 insertions(+), 39 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 8ace7eafd14..7596975a684 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -32,13 +32,6 @@ void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); this->publish_state(new_state); } -void BinarySensor::send_state_internal(bool new_state) { - // copy the new state to the visible property for backwards compatibility, before any callbacks - this->state = new_state; - // Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed - this->set_new_state(new_state); -} - bool BinarySensor::set_new_state(const optional<bool> &new_state) { if (StatefulEntityBase::set_new_state(new_state)) { // weirdly, this file could be compiled even without USE_BINARY_SENSOR defined diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 6ae5d04bcbf..28c156763a8 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -32,7 +32,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi */ class BinarySensor : public StatefulEntityBase<bool> { public: - explicit BinarySensor(){}; + explicit BinarySensor() = default; + + const bool &get_state() const override { return this->state; } + void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; } /** Publish a new state to the front-end. * @@ -54,16 +57,24 @@ class BinarySensor : public StatefulEntityBase<bool> { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool new_state); + void send_state_internal(bool new_state) { + // Fast path: skip virtual dispatch when state hasn't changed + if (this->flags_.has_state && this->state == new_state) + return; + this->set_new_state(new_state); + } /// Return whether this binary sensor has outputted a state. virtual bool is_status_binary_sensor() const; - // For backward compatibility, provide an accessible property - + /// The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase. bool state{}; protected: + bool get_trigger_on_initial_state() const override { return this->trigger_on_initial_state_; } + void set_state_value(const bool &value) override { this->state = value; } + + bool trigger_on_initial_state_{true}; #ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; #endif @@ -73,7 +84,7 @@ class BinarySensor : public StatefulEntityBase<bool> { class BinarySensorInitiallyOff : public BinarySensor { public: - bool has_state() const override { return true; } + BinarySensorInitiallyOff() { this->set_has_state(true); } }; } // namespace esphome::binary_sensor diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 8c1f1a213e5..5a69c9dd09b 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -296,15 +296,36 @@ void log_entity_device_class(const char *tag, const char *prefix, const EntityBa #define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj) void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj); -/** - * An entity that has a state. - * @tparam T The type of the state +/** Base class for entities that track a typed state value with change-detection and callbacks. + * + * This class does not store the state value — subclasses own their storage. Whether a state + * has been set is tracked by EntityBase::has_state(). + * + * Subclasses must implement: + * - get_state(): return a const reference to the current value + * - set_state_value(): store a new value (called only when the state actually changes) + * - get_trigger_on_initial_state(): return whether callbacks should fire on the first state + * + * Subclasses may override set_new_state() to add behavior (logging, notifications) after calling + * the base implementation. Since set_new_state() is virtual, callers like invalidate_state() + * dispatch through the vtable to the subclass override in the .cpp, avoiding template code + * bloat at inline call sites. Subclasses may also add a fast-path dedup check before calling + * set_new_state() to skip virtual dispatch entirely when the state hasn't changed. + * + * Callback behavior: + * - full_state_callbacks_: fired on every change, receives optional<T> previous and current + * - state_callbacks_: fired only when the new state has a value, and either this is not the + * first state (had_state) or trigger_on_initial_state is set + * + * @tparam T The type of the state value */ template<typename T> class StatefulEntityBase : public EntityBase { public: - virtual bool has_state() const { return this->state_.has_value(); } - virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access) - virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } + /// Return the current state value. Only valid when has_state() is true. + virtual const T &get_state() const = 0; + /// Return the current state if available, otherwise return the provided default. + T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; } + /// Clear the state — sets has_state() to false and fires callbacks with nullopt. void invalidate_state() { this->set_new_state({}); } template<typename F> void add_full_state_callback(F &&callback) { @@ -314,33 +335,46 @@ template<typename T> class StatefulEntityBase : public EntityBase { this->state_callbacks_.add(std::forward<F>(callback)); } - void set_trigger_on_initial_state(bool trigger_on_initial_state) { - this->trigger_on_initial_state_ = trigger_on_initial_state; - } - protected: - optional<T> state_{}; - /** - * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. + /// Subclasses return whether callbacks should fire on the very first state. + virtual bool get_trigger_on_initial_state() const = 0; + + /** Apply a new state, de-duplicating and firing callbacks as needed. * - * @param new_state The new state. - * @return True if the state was changed, false if it was the same as before. + * Pass nullopt to invalidate (clear) the state. Pass a value to set it. + * Returns true if the state actually changed, false if it was the same. + * Subclasses may override to add logging/notifications after calling the base. */ virtual bool set_new_state(const optional<T> &new_state) { - if (this->state_ != new_state) { - // call the full state callbacks with the previous and new state - this->full_state_callbacks_.call(this->state_, new_state); - // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or - // the previous state was valid - auto had_state = this->has_state(); - this->state_ = new_state; - if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) - this->state_callbacks_.call(new_state.value()); - return true; + // Access flags_ directly to avoid function call overhead in this hot path + bool had_state = this->flags_.has_state; + // Use pointer to avoid requiring T to be default-constructible + const T *current = had_state ? &this->get_state() : nullptr; + if (new_state.has_value()) { + if (current != nullptr && *current == new_state.value()) + return false; // same value, no change + } else if (!had_state) { + return false; // already invalidated, no change } - return false; + // Capture old_state before set_state_value — current pointer aliases subclass storage + bool has_full_cbs = !this->full_state_callbacks_.empty(); + optional<T> old_state; + if (has_full_cbs) + old_state = current != nullptr ? optional<T>(*current) : nullopt; + // Update storage before firing callbacks so callback code can inspect current state + this->flags_.has_state = new_state.has_value(); + if (new_state.has_value()) { + this->set_state_value(new_state.value()); + } + if (has_full_cbs) + this->full_state_callbacks_.call(old_state, new_state); + // had_state first: on every change except the first, skips the virtual call + if (new_state.has_value() && (had_state || this->get_trigger_on_initial_state())) + this->state_callbacks_.call(new_state.value()); + return true; } - bool trigger_on_initial_state_{true}; + /// Subclasses implement this to store the actual value into their own storage. + virtual void set_state_value(const T &value) = 0; LazyCallbackManager<void(optional<T> previous, optional<T> current)> full_state_callbacks_; LazyCallbackManager<void(T)> state_callbacks_; }; From 3520ef74809b0d6f1c6e9d3abd067c36536ce9a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 12:38:04 -1000 Subject: [PATCH 1076/2056] [text_sensor] Use std::array in MapFilter (#15269) --- esphome/components/text_sensor/__init__.py | 2 +- esphome/components/text_sensor/filter.cpp | 14 ++++++-------- esphome/components/text_sensor/filter.h | 17 +++++++++++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 51eedf9a956..78a7a3a41b3 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -129,7 +129,7 @@ async def map_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, mappings) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(mappings)), mappings) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index f7c6a695fba..bc044f3a737 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -93,17 +93,15 @@ bool SubstituteFilter::new_value(std::string &value) { return true; } -// Map -MapFilter::MapFilter(const std::initializer_list<Substitution> &mappings) : mappings_(mappings) {} - -bool MapFilter::new_value(std::string &value) { - for (const auto &mapping : this->mappings_) { - if (value == mapping.from) { - value.assign(mapping.to); +// Map — non-template helper +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + if (value == mappings[i].from) { + value.assign(mappings[i].to); return true; } } - return true; // Pass through if no match + return true; } } // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 8a8bc55c8e1..07832af9e2b 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_TEXT_SENSOR_FILTER +#include <array> + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -131,6 +133,9 @@ class SubstituteFilter : public Filter { FixedVector<Substitution> substitutions_; }; +/// Non-template helper (implementation in filter.cpp) +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value); + /** A filter that maps values from one set to another * * Uses linear search instead of std::map for typical small datasets (2-20 mappings). @@ -154,14 +159,18 @@ class SubstituteFilter : public Filter { * - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare) * * Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20 + * + * N is set by code generation to match the exact number of mappings configured in YAML. */ -class MapFilter : public Filter { +template<size_t N> class MapFilter : public Filter { public: - explicit MapFilter(const std::initializer_list<Substitution> &mappings); - bool new_value(std::string &value) override; + explicit MapFilter(const std::initializer_list<Substitution> &mappings) { + init_array_from(this->mappings_, mappings); + } + bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } protected: - FixedVector<Substitution> mappings_; + std::array<Substitution, N> mappings_{}; }; } // namespace esphome::text_sensor From 29419d9d97557af72cc75d351b80797e9cefe83d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 13:36:08 -1000 Subject: [PATCH 1077/2056] [automation] Use std::array in And/Or/Xor conditions (#15282) --- esphome/automation.py | 20 +++++++++++++++----- esphome/core/base_automation.h | 25 ++++++++++++++++--------- esphome/core/helpers.h | 3 ++- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 7b1d6ceca13..94d64086ec0 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -250,7 +250,9 @@ async def and_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("or", OrCondition, validate_condition_list) @@ -261,7 +263,9 @@ async def or_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("all", AndCondition, validate_condition_list) @@ -272,7 +276,9 @@ async def all_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("any", OrCondition, validate_condition_list) @@ -283,7 +289,9 @@ async def any_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("not", NotCondition, validate_potentially_and_condition) @@ -305,7 +313,9 @@ async def xor_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("lambda", LambdaCondition, cv.returning_lambda) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index efcffa8824d..11133d39739 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,14 +9,17 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include <array> #include <list> #include <vector> namespace esphome { -template<typename... Ts> class AndCondition : public Condition<Ts...> { +template<size_t N, typename... Ts> class AndCondition : public Condition<Ts...> { public: - explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {} + explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) @@ -27,12 +30,14 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> { } protected: - FixedVector<Condition<Ts...> *> conditions_; + std::array<Condition<Ts...> *, N> conditions_{}; }; -template<typename... Ts> class OrCondition : public Condition<Ts...> { +template<size_t N, typename... Ts> class OrCondition : public Condition<Ts...> { public: - explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {} + explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) @@ -43,7 +48,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> { } protected: - FixedVector<Condition<Ts...> *> conditions_; + std::array<Condition<Ts...> *, N> conditions_{}; }; template<typename... Ts> class NotCondition : public Condition<Ts...> { @@ -55,9 +60,11 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> { Condition<Ts...> *condition_; }; -template<typename... Ts> class XorCondition : public Condition<Ts...> { +template<size_t N, typename... Ts> class XorCondition : public Condition<Ts...> { public: - explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {} + explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { @@ -68,7 +75,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> { } protected: - FixedVector<Condition<Ts...> *> conditions_; + std::array<Condition<Ts...> *, N> conditions_{}; }; template<typename... Ts> class LambdaCondition : public Condition<Ts...> { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 913614f5641..66ba166445f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -500,7 +500,8 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> /// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen), /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). -/// N is set by code generation; assert catches mismatches in debug/integration tests. +/// N is always set by code generation — the caller is responsible for ensuring src.size() == N. +/// The debug assert is a safety net for development, not a runtime check. template<typename T, size_t N> inline void init_array_from(std::array<T, N> &dest, std::initializer_list<T> src) { #ifdef ESPHOME_DEBUG assert(src.size() == N); From 4da7f5ecc2e82e185c0dea21bf7f40caa1a88148 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 13:50:46 -1000 Subject: [PATCH 1078/2056] [binary_sensor] Use std::array in AutorepeatFilter (#15268) --- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/binary_sensor/filter.cpp | 27 ++++++------------ esphome/components/binary_sensor/filter.h | 29 ++++++++++++++++---- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4705f1675d5..8d072904b07 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -255,6 +255,7 @@ async def delayed_off_filter_to_code(config, filter_id): ): cv.positive_time_period_milliseconds, } ), + cv.Length(max=254), ), ) async def autorepeat_filter_to_code(config, filter_id): @@ -283,7 +284,7 @@ async def autorepeat_filter_to_code(config, filter_id): ), ) ] - var = cg.new_Pvariable(filter_id, timings) + var = cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings) await cg.register_component(var, {}) return var diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 5d525e967db..914060ce138 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -76,14 +76,11 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional<bool> InvertFilter::new_value(bool value) { return !value; } -AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {} - -optional<bool> AutorepeatFilter::new_value(bool value) { +// AutorepeatFilterBase +optional<bool> AutorepeatFilterBase::new_value(bool value) { if (value) { - // Ignore if already running if (this->active_timing_ != 0) return {}; - this->next_timing_(); return true; } else { @@ -94,34 +91,26 @@ optional<bool> AutorepeatFilter::new_value(bool value) { } } -void AutorepeatFilter::next_timing_() { - // Entering this method - // 1st time: starts waiting the first delay - // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on - // last time: no delay to start but have to bump the index to reflect the last - if (this->active_timing_ < this->timings_.size()) { +void AutorepeatFilterBase::next_timing_() { + if (this->active_timing_ < this->timings_count_) { this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); } - - if (this->active_timing_ <= this->timings_.size()) { + if (this->active_timing_ <= this->timings_count_) { this->active_timing_++; } - if (this->active_timing_ == 2) this->next_value_(false); - - // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals } -void AutorepeatFilter::next_value_(bool val) { +void AutorepeatFilterBase::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val); // This is at least the second one so not initial + this->output(val); this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } -float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +float AutorepeatFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 0813847ca21..37c6bf0092c 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_BINARY_SENSOR_FILTER +#include <array> + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -86,22 +88,39 @@ struct AutorepeatFilterTiming { uint32_t time_on; }; -class AutorepeatFilter : public Filter, public Component { +/// Non-template base for AutorepeatFilter — all methods in filter.cpp. +/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once. +class AutorepeatFilterBase : public Filter, public Component { public: - explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings); - optional<bool> new_value(bool value) override; - float get_setup_priority() const override; + AutorepeatFilterBase(const AutorepeatFilterBase &) = delete; + AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete; protected: + AutorepeatFilterBase() = default; void next_timing_(); void next_value_(bool val); - FixedVector<AutorepeatFilterTiming> timings_; + const AutorepeatFilterTiming *timings_{nullptr}; + uint8_t timings_count_{0}; uint8_t active_timing_{0}; }; +/// Template wrapper that provides inline std::array storage for timings. +/// N is set by code generation to match the exact number of timings configured in YAML. +template<size_t N> class AutorepeatFilter : public AutorepeatFilterBase { + public: + explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) { + init_array_from(this->timings_storage_, timings); + this->timings_ = this->timings_storage_.data(); + this->timings_count_ = N; + } + + protected: + std::array<AutorepeatFilterTiming, N> timings_storage_{}; +}; + class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function<optional<bool>(bool)> f); From 66754fa376b8495885c7718acacaf66b0872f7f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 14:24:32 -1000 Subject: [PATCH 1079/2056] [text_sensor] Use std::array in SubstituteFilter (#15266) --- esphome/components/text_sensor/__init__.py | 4 +++- esphome/components/text_sensor/filter.cpp | 20 +++++++------------- esphome/components/text_sensor/filter.h | 16 +++++++++++----- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 78a7a3a41b3..5b07dd29156 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -116,7 +116,9 @@ async def substitute_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, substitutions) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(substitutions)), substitutions + ) @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index bc044f3a737..d4e6b5b9bbf 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -73,20 +73,14 @@ bool PrependFilter::new_value(std::string &value) { return true; } -// Substitute -SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &substitutions) - : substitutions_(substitutions) {} - -bool SubstituteFilter::new_value(std::string &value) { - for (const auto &sub : this->substitutions_) { - // Compute lengths once per substitution (strlen is fast, called infrequently) - const size_t from_len = strlen(sub.from); - const size_t to_len = strlen(sub.to); +// Substitute — non-template helper +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + const size_t from_len = strlen(substitutions[i].from); + const size_t to_len = strlen(substitutions[i].to); std::size_t pos = 0; - while ((pos = value.find(sub.from, pos, from_len)) != std::string::npos) { - value.replace(pos, from_len, sub.to, to_len); - // Advance past the replacement to avoid infinite loop when - // the replacement contains the search pattern (e.g., f -> foo) + while ((pos = value.find(substitutions[i].from, pos, from_len)) != std::string::npos) { + value.replace(pos, from_len, substitutions[i].to, to_len); pos += to_len; } } diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 07832af9e2b..6db76dcb640 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -123,14 +123,20 @@ struct Substitution { const char *to; }; -/// A simple filter that replaces a substring with another substring -class SubstituteFilter : public Filter { +/// Non-template helper (implementation in filter.cpp) +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value); + +/// A simple filter that replaces a substring with another substring. +/// N is set by code generation to match the exact number of substitutions configured in YAML. +template<size_t N> class SubstituteFilter : public Filter { public: - explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions); - bool new_value(std::string &value) override; + explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions) { + init_array_from(this->substitutions_, substitutions); + } + bool new_value(std::string &value) override { return substitute_filter_apply(this->substitutions_.data(), N, value); } protected: - FixedVector<Substitution> substitutions_; + std::array<Substitution, N> substitutions_{}; }; /// Non-template helper (implementation in filter.cpp) From 508ec295a4d9b8b920b27ce4d3cee6818997c39d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 14:55:46 -1000 Subject: [PATCH 1080/2056] [sensor] Use std::array in OrFilter (#15262) --- esphome/components/sensor/__init__.py | 2 +- esphome/components/sensor/filter.cpp | 30 ++++++++----------------- esphome/components/sensor/filter.h | 32 ++++++++++++++++++++------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 5569567de12..8bbaa73e2ed 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -620,7 +620,7 @@ async def delta_filter_to_code(config, filter_id): @FILTER_REGISTRY.register("or", OrFilter, validate_filters) async def or_filter_to_code(config, filter_id): filters = await build_filters(config) - return cg.new_Pvariable(filter_id, filters) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(filters)), filters) @FILTER_REGISTRY.register( diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 66a9e9555bc..dad09ff0217 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -295,32 +295,20 @@ optional<float> DeltaFilter::new_value(float value) { return {}; } -// OrFilter -OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {} -OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} - -optional<float> OrFilter::PhiNode::new_value(float value) { - if (!this->or_parent_->has_value_) { - this->or_parent_->output(value); - this->or_parent_->has_value_ = true; +// OrFilter helpers +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi) { + for (size_t i = 0; i < count; i++) { + filters[i]->initialize(parent, phi); } - - return {}; + phi->initialize(parent, nullptr); } -optional<float> OrFilter::new_value(float value) { - this->has_value_ = false; - for (auto *filter : this->filters_) - filter->input(value); +optional<float> or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value) { + has_value = false; + for (size_t i = 0; i < count; i++) + filters[i]->input(value); return {}; } -void OrFilter::initialize(Sensor *parent, Filter *next) { - Filter::initialize(parent, next); - for (auto *filter : this->filters_) { - filter->initialize(parent, &this->phi_); - } - this->phi_.initialize(parent, nullptr); -} // TimeoutFilterBase - shared loop logic void TimeoutFilterBase::loop() { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 80fa14742c2..deaaa27f19b 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -489,26 +489,42 @@ class DeltaFilter : public Filter { float last_value_{NAN}; }; -class OrFilter : public Filter { +/// Non-template helpers for OrFilter (implementation in filter.cpp) +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi); +optional<float> or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value); + +/// N is set by code generation to match the exact number of filters configured in YAML. +template<size_t N> class OrFilter : public Filter { public: - explicit OrFilter(std::initializer_list<Filter *> filters); + explicit OrFilter(std::initializer_list<Filter *> filters) { init_array_from(this->filters_, filters); } - void initialize(Sensor *parent, Filter *next) override; + void initialize(Sensor *parent, Filter *next) override { + Filter::initialize(parent, next); + or_filter_initialize(this->filters_.data(), N, parent, &this->phi_); + } - optional<float> new_value(float value) override; + optional<float> new_value(float value) override { + return or_filter_new_value(this->filters_.data(), N, value, this->has_value_); + } protected: class PhiNode : public Filter { public: - PhiNode(OrFilter *or_parent); - optional<float> new_value(float value) override; + PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} + optional<float> new_value(float value) override { + if (!this->or_parent_->has_value_) { + this->or_parent_->output(value); + this->or_parent_->has_value_ = true; + } + return {}; + } protected: OrFilter *or_parent_; }; - FixedVector<Filter *> filters_; - PhiNode phi_; + std::array<Filter *, N> filters_{}; + PhiNode phi_{this}; bool has_value_{false}; }; From d51b047f6381c407cb8c879a96d73c4b5c36f528 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 14:56:04 -1000 Subject: [PATCH 1081/2056] [sensor] Use std::array in CalibratePolynomialFilter (#15264) --- esphome/components/sensor/__init__.py | 2 +- esphome/components/sensor/filter.cpp | 9 +++------ esphome/components/sensor/filter.h | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 8bbaa73e2ed..8abba17ff9f 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -808,7 +808,7 @@ async def calibrate_polynomial_filter_to_code(config, filter_id): # Column vector b = [[v] for v in y] res = [v[0] for v in _lstsq(a, b)] - return cg.new_Pvariable(filter_id, res) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(res)), res) def validate_clamp(config): diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index dad09ff0217..7b7a968f48f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -396,14 +396,11 @@ optional<float> CalibrateLinearFilter::new_value(float value) { return NAN; } -CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients) - : coefficients_(coefficients) {} - -optional<float> CalibratePolynomialFilter::new_value(float value) { +optional<float> calibrate_polynomial_compute(const float *coefficients, size_t count, float value) { float res = 0.0f; float x = 1.0f; - for (const auto &coefficient : this->coefficients_) { - res += x * coefficient; + for (size_t i = 0; i < count; i++) { + res += x * coefficients[i]; x *= value; } return res; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index deaaa27f19b..26a03acde57 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -537,13 +537,21 @@ class CalibrateLinearFilter : public Filter { FixedVector<std::array<float, 3>> linear_functions_; }; -class CalibratePolynomialFilter : public Filter { +/// Non-template helper for polynomial calibration (implementation in filter.cpp) +optional<float> calibrate_polynomial_compute(const float *coefficients, size_t count, float value); + +/// N is set by code generation to match the exact number of polynomial coefficients. +template<size_t N> class CalibratePolynomialFilter : public Filter { public: - explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients); - optional<float> new_value(float value) override; + explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients) { + init_array_from(this->coefficients_, coefficients); + } + optional<float> new_value(float value) override { + return calibrate_polynomial_compute(this->coefficients_.data(), N, value); + } protected: - FixedVector<float> coefficients_; + std::array<float, N> coefficients_{}; }; class ClampFilter : public Filter { From 17afbeb87b32c8b30a983c5926060d09ed78846d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 14:57:15 -1000 Subject: [PATCH 1082/2056] [binary_sensor] Use std::array in MultiClickTrigger (#15267) --- esphome/components/binary_sensor/__init__.py | 13 ++++++--- .../components/binary_sensor/automation.cpp | 18 ++++++------- esphome/components/binary_sensor/automation.h | 27 ++++++++++++++++--- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8d072904b07..660f75ccd9e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -124,9 +124,10 @@ ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.templa DoubleClickTrigger = binary_sensor_ns.class_( "DoubleClickTrigger", automation.Trigger.template() ) -MultiClickTrigger = binary_sensor_ns.class_( - "MultiClickTrigger", automation.Trigger.template(), cg.Component +MultiClickTriggerBase = binary_sensor_ns.class_( + "MultiClickTriggerBase", automation.Trigger.template(), cg.Component ) +MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase) MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") BinarySensorPublishAction = binary_sensor_ns.class_( @@ -484,7 +485,9 @@ _BINARY_SENSOR_SCHEMA = ( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), cv.Required(CONF_TIMING): cv.All( - [parse_multi_click_timing_str], validate_multi_click_timing + [parse_multi_click_timing_str], + validate_multi_click_timing, + cv.Length(min=1, max=255), ), cv.Optional( CONF_INVALID_COOLDOWN, default="1s" @@ -561,7 +564,9 @@ async def _build_binary_sensor_automations(var, config): ) for tim in conf[CONF_TIMING] ] - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], cg.TemplateArguments(len(timings)), var, timings + ) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) await cg.register_component(trigger, conf) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 7e43d42357c..eb68abce3b7 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -13,7 +13,7 @@ constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1; constexpr uint32_t MULTICLICK_IS_VALID_ID = 2; constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3; -void MultiClickTrigger::on_state_(bool state) { +void MultiClickTriggerBase::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -32,7 +32,7 @@ void MultiClickTrigger::on_state_(bool state) { ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; - if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { + if (this->timing_count_ == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } else { this->schedule_is_valid_(evt.min_length); @@ -50,7 +50,7 @@ void MultiClickTrigger::on_state_(bool state) { return; } - if (*this->at_index_ == this->timing_.size()) { + if (*this->at_index_ == this->timing_count_) { this->trigger_(); return; } @@ -61,7 +61,7 @@ void MultiClickTrigger::on_state_(bool state) { ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); - } else if (*this->at_index_ + 1 != this->timing_.size()) { + } else if (*this->at_index_ + 1 != this->timing_count_) { ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); @@ -74,7 +74,7 @@ void MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void MultiClickTrigger::schedule_cooldown_() { +void MultiClickTriggerBase::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { @@ -86,7 +86,7 @@ void MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout(MULTICLICK_IS_VALID_ID); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); } -void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -97,19 +97,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void MultiClickTrigger::cancel() { +void MultiClickTriggerBase::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void MultiClickTrigger::trigger_() { +void MultiClickTriggerBase::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout(MULTICLICK_TRIGGER_ID); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f30f9d32792..1875910affd 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -1,5 +1,6 @@ #pragma once +#include <array> #include <cinttypes> #include <utility> @@ -89,10 +90,10 @@ class DoubleClickTrigger : public Trigger<> { uint32_t max_length_; /// Maximum length of click. 0 means no maximum. }; -class MultiClickTrigger : public Trigger<>, public Component { +/// Non-template base for MultiClickTrigger (keeps large method bodies out of the header). +class MultiClickTriggerBase : public Trigger<>, public Component { public: - explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) - : parent_(parent), timing_(timing) {} + explicit MultiClickTriggerBase(BinarySensor *parent) : parent_(parent) {} void setup() override { this->last_state_ = this->parent_->get_state_default(false); @@ -104,6 +105,8 @@ class MultiClickTrigger : public Trigger<>, public Component { void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; } void cancel(); + MultiClickTriggerBase(const MultiClickTriggerBase &) = delete; + MultiClickTriggerBase &operator=(const MultiClickTriggerBase &) = delete; protected: void on_state_(bool state); @@ -113,14 +116,30 @@ class MultiClickTrigger : public Trigger<>, public Component { void trigger_(); BinarySensor *parent_; - FixedVector<MultiClickTriggerEvent> timing_; + const MultiClickTriggerEvent *timing_{nullptr}; uint32_t invalid_cooldown_{1000}; optional<size_t> at_index_{}; + uint8_t timing_count_{0}; bool last_state_{false}; bool is_in_cooldown_{false}; bool is_valid_{false}; }; +/// Template wrapper that provides inline std::array storage for timing events. +/// N is set by code generation to match the exact number of timing events configured in YAML. +template<size_t N> class MultiClickTrigger : public MultiClickTriggerBase { + public: + MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) + : MultiClickTriggerBase(parent) { + init_array_from(this->timing_storage_, timing); + this->timing_ = this->timing_storage_.data(); + this->timing_count_ = N; + } + + protected: + std::array<MultiClickTriggerEvent, N> timing_storage_{}; +}; + class StateTrigger : public Trigger<bool> { public: explicit StateTrigger(BinarySensor *parent) { From 18168ad7fda9cb7f93a6d5b5c183038954318076 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 15:07:15 -1000 Subject: [PATCH 1083/2056] [sensor] Use std::array in CalibrateLinearFilter (#15263) --- esphome/components/sensor/__init__.py | 4 +++- esphome/components/sensor/filter.cpp | 11 ++++------- esphome/components/sensor/filter.h | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 8abba17ff9f..650f5ed826a 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -770,7 +770,9 @@ async def calibrate_linear_filter_to_code(config, filter_id): linear_functions = [[k, b, float("NaN")]] elif config[CONF_METHOD] == "exact": linear_functions = map_linear(x, y) - return cg.new_Pvariable(filter_id, linear_functions) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(linear_functions)), linear_functions + ) CONF_DEGREE = "degree" diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 7b7a968f48f..6a90a5af66b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -385,13 +385,10 @@ void HeartbeatFilter::setup() { float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions) - : linear_functions_(linear_functions) {} - -optional<float> CalibrateLinearFilter::new_value(float value) { - for (const auto &f : this->linear_functions_) { - if (!std::isfinite(f[2]) || value < f[2]) - return (value * f[0]) + f[1]; +optional<float> calibrate_linear_compute(const std::array<float, 3> *functions, size_t count, float value) { + for (size_t i = 0; i < count; i++) { + if (!std::isfinite(functions[i][2]) || value < functions[i][2]) + return (value * functions[i][0]) + functions[i][1]; } return NAN; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 26a03acde57..cb4abd154ad 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -528,13 +528,21 @@ template<size_t N> class OrFilter : public Filter { bool has_value_{false}; }; -class CalibrateLinearFilter : public Filter { +/// Non-template helper for linear calibration (implementation in filter.cpp) +optional<float> calibrate_linear_compute(const std::array<float, 3> *functions, size_t count, float value); + +/// N is set by code generation to match the exact number of calibration segments. +template<size_t N> class CalibrateLinearFilter : public Filter { public: - explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions); - optional<float> new_value(float value) override; + explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions) { + init_array_from(this->linear_functions_, linear_functions); + } + optional<float> new_value(float value) override { + return calibrate_linear_compute(this->linear_functions_.data(), N, value); + } protected: - FixedVector<std::array<float, 3>> linear_functions_; + std::array<std::array<float, 3>, N> linear_functions_{}; }; /// Non-template helper for polynomial calibration (implementation in filter.cpp) From ffbbe5eab33e9d18e8e209fa55b3d24de21f765f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:55:40 +0200 Subject: [PATCH 1084/2056] [nextion] Fix log level for command processing limit message (#15302) --- esphome/components/nextion/nextion.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 97d9b36e4c3..b0d8ba92f76 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -423,7 +423,7 @@ void Nextion::process_nextion_commands_() { DELIMITER_SIZE)) != std::string::npos) { #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP if (++commands_processed > this->max_commands_per_loop_) { - ESP_LOGW(TAG, "Command processing limit exceeded"); + ESP_LOGV(TAG, "Command limit reached, deferring"); break; } #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP From 95b0e6061795f6e4ba2d7474674cbbd1a07a7347 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:20:04 +0200 Subject: [PATCH 1085/2056] [nextion] Add accessor const qualifiers, return by ref, and deprecate `get_wave_chan_id()` (#15204) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../binary_sensor/nextion_binarysensor.h | 6 ++-- esphome/components/nextion/nextion.cpp | 30 +++++----------- .../nextion/nextion_component_base.h | 34 +++++++++---------- .../nextion/sensor/nextion_sensor.h | 5 ++- .../nextion/switch/nextion_switch.h | 2 +- .../nextion/text_sensor/nextion_textsensor.h | 2 +- 6 files changed, 32 insertions(+), 47 deletions(-) diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h index b6b23ada85d..baab47851c7 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -21,15 +21,15 @@ class NextionBinarySensor : public NextionComponent, void process_touch(uint8_t page_id, uint8_t component_id, bool state) override; // Set the components page id for Nextion Touch Component - void set_page_id(uint8_t page_id) { page_id_ = page_id; } + void set_page_id(uint8_t page_id) { this->page_id_ = page_id; } // Set the components component id for Nextion Touch Component - void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } void set_state(bool state) override { this->set_state(state, true, true); } void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } void set_state(bool state, bool publish, bool send_to_nextion) override; - NextionQueueType get_queue_type() override { return NextionQueueType::BINARY_SENSOR; } + NextionQueueType get_queue_type() const override { return NextionQueueType::BINARY_SENSOR; } void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value != 0, publish, send_to_nextion); diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b0d8ba92f76..22fb3ce937f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -281,7 +281,7 @@ void Nextion::print_queue_members_() { ESP_LOGN(TAG, "Queue null"); } else { ESP_LOGN(TAG, "Queue type: %d:%s, name: %s", i->component->get_queue_type(), - i->component->get_queue_type_string().c_str(), i->component->get_variable_name().c_str()); + i->component->get_queue_type_string(), i->component->get_variable_name().c_str()); } } ESP_LOGN(TAG, "*******************************************"); @@ -607,7 +607,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGE(TAG, "String return but '%s' not text sensor", component->get_variable_name().c_str()); } else { ESP_LOGN(TAG, "String resp: '%s' id: %s type: %s", to_process.c_str(), component->get_variable_name().c_str(), - component->get_queue_type_string().c_str()); + component->get_queue_type_string()); } delete nb; // NOLINT(cppcoreguidelines-owning-memory) @@ -649,7 +649,7 @@ void Nextion::process_nextion_commands_() { component->get_queue_type()); } else { ESP_LOGN(TAG, "Numeric: %s type %d:%s val %d", component->get_variable_name().c_str(), - component->get_queue_type(), component->get_queue_type_string().c_str(), value); + component->get_queue_type(), component->get_queue_type_string(), value); component->set_state_from_int(value, true, false); } @@ -842,24 +842,10 @@ void Nextion::process_nextion_commands_() { if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { for (auto it = this->nextion_queue_.begin(); it != this->nextion_queue_.end();) { - NextionComponentBase *component = (*it)->component; if (ms - (*it)->queue_time > this->max_q_age_ms_) { - if ((*it)->queue_time == 0) { - ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str()); - } - - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - - if ((*it)->pending_command.empty()) { - ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str()); - } else { - ESP_LOGD(TAG, "Remove old queue '%s':'%s' cmd:'%s'", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str(), (*it)->pending_command.c_str()); - } + NextionComponentBase *component = (*it)->component; + ESP_LOGV(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string(), + component->get_variable_name().c_str()); if (component->get_queue_type() == NextionQueueType::NO_RESULT) { if (component->get_variable_name() == "sleep_wake") { @@ -940,7 +926,7 @@ void Nextion::all_components_send_state_(bool force_update) { binarysensortype->send_state_to_nextion(); } for (auto *sensortype : this->sensortype_) { - if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_chan_id() == 0) + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == 0) sensortype->send_state_to_nextion(); } for (auto *switchtype : this->switchtype_) { @@ -1236,7 +1222,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); - ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); + ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string(), component->get_variable_name().c_str()); std::string command = "get " + component->get_variable_name_to_send(); diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index fe0692b8753..4d5550d4060 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -1,4 +1,6 @@ #pragma once + +#include <string> #include <utility> #include <vector> #include "esphome/core/defines.h" @@ -35,12 +37,8 @@ class NextionComponentBase { virtual ~NextionComponentBase() = default; void set_variable_name(const std::string &variable_name, const std::string &variable_name_to_send = "") { - variable_name_ = variable_name; - if (variable_name_to_send.empty()) { - variable_name_to_send_ = variable_name; - } else { - variable_name_to_send_ = variable_name_to_send; - } + this->variable_name_ = variable_name; + this->variable_name_to_send_ = variable_name_to_send.empty() ? variable_name : variable_name_to_send; } virtual void update_component_settings(){}; @@ -64,14 +62,14 @@ class NextionComponentBase { virtual void set_state(const std::string &state, bool publish) {} virtual void set_state(const std::string &state, bool publish, bool send_to_nextion){}; - uint8_t get_component_id() { return this->component_id_; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } + uint8_t get_component_id() const { return this->component_id_; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } - uint8_t get_wave_channel_id() { return this->wave_chan_id_; } + uint8_t get_wave_channel_id() const { return this->wave_chan_id_; } void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } - std::vector<uint8_t> get_wave_buffer() { return this->wave_buffer_; } - size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + const std::vector<uint8_t> &get_wave_buffer() const { return this->wave_buffer_; } + size_t get_wave_buffer_size() const { return this->wave_buffer_.size(); } void clear_wave_buffer(size_t buffer_sent) { if (this->wave_buffer_.size() <= buffer_sent) { this->wave_buffer_.clear(); @@ -80,15 +78,17 @@ class NextionComponentBase { } } - std::string get_variable_name() { return this->variable_name_; } - std::string get_variable_name_to_send() { return this->variable_name_to_send_; } - virtual NextionQueueType get_queue_type() { return NextionQueueType::NO_RESULT; } - virtual std::string get_queue_type_string() { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } + const std::string &get_variable_name() const { return this->variable_name_; } + const std::string &get_variable_name_to_send() const { return this->variable_name_to_send_; } + virtual NextionQueueType get_queue_type() const { return NextionQueueType::NO_RESULT; } + virtual const char *get_queue_type_string() const { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } virtual void set_state_from_int(int state_value, bool publish, bool send_to_nextion){}; virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; virtual void send_state_to_nextion(){}; - bool get_needs_to_send_update() { return this->needs_to_send_update_; } - uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + bool get_needs_to_send_update() const { return this->needs_to_send_update_; } + // Remove before 2026.10.0 + ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0") + uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); } void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } protected: diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index e4dde9a5130..b1902f9b1be 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -17,7 +17,7 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol void update() override; void add_to_wave_buffer(float state); void set_precision(uint8_t precision) { this->precision_ = precision; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } void process_sensor(const std::string &variable_name, int state) override; @@ -27,9 +27,8 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol void set_state(float state, bool publish, bool send_to_nextion) override; void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } - uint8_t get_wave_chan_id() { return this->wave_chan_id_; } void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } - NextionQueueType get_queue_type() override { + NextionQueueType get_queue_type() const override { return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; } void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h index 15482874736..c371ea3fc63 100644 --- a/esphome/components/nextion/switch/nextion_switch.h +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -21,7 +21,7 @@ class NextionSwitch : public NextionComponent, public switch_::Switch, public Po void set_state(bool state, bool publish, bool send_to_nextion) override; void send_state_to_nextion() override { this->set_state(this->state, false, true); }; - NextionQueueType get_queue_type() override { return NextionQueueType::SWITCH; } + NextionQueueType get_queue_type() const override { return NextionQueueType::SWITCH; } void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value != 0, publish, send_to_nextion); diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h index 5716d0a0083..7c08e471891 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.h +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -22,7 +22,7 @@ class NextionTextSensor : public NextionComponent, public text_sensor::TextSenso void set_state(const std::string &state, bool publish, bool send_to_nextion) override; void send_state_to_nextion() override { this->set_state(this->state, false, true); }; - NextionQueueType get_queue_type() override { return NextionQueueType::TEXT_SENSOR; } + NextionQueueType get_queue_type() const override { return NextionQueueType::TEXT_SENSOR; } void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {} void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value, publish, send_to_nextion); From cd3c2ae77e0e88291d28372eb9a1a54d39e52b16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:45:46 -1000 Subject: [PATCH 1086/2056] Bump aioesphomeapi from 44.8.0 to 44.8.1 (#15309) 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 c74dd265c75..0df5caf1818 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.8.0 +aioesphomeapi==44.8.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From d420e7bc236984f9e711e8f7669025f5f5eb090c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 02:57:27 -1000 Subject: [PATCH 1087/2056] [modbus_controller] Fix off-by-one bounds check in byte_from_hex_str (#15301) --- esphome/components/modbus_controller/modbus_controller.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 78c3b959654..693908dca41 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -81,15 +81,15 @@ inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_ inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } /** Get a byte from a hex string - * hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 - * hex_byte_from_str("1122",0) returns 0x11 + * byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 + * byte_from_hex_str("1122", 0) returns 0x11 * @param value string containing hex encoding * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in * the hex string is byte_pos * 2 * @return byte value */ inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { - if (value.length() < pos * 2 + 1) + if (value.length() < pos * 2 + 2) return 0; return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); } From 1bc6a8d95698f2913f202c11b1af13986f6c453a Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:54:09 +0200 Subject: [PATCH 1088/2056] [nextion] Fix queue age check using inconsistent time sources (#15317) --- esphome/components/nextion/nextion.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 22fb3ce937f..bb3e12be508 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1034,7 +1034,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->nextion_queue_.push_back(nextion_queue); From 31574a427bf721f429d6fad82f25b360ce255aad Mon Sep 17 00:00:00 2001 From: Bonne Eggleston <bonne@exciton.com.au> Date: Mon, 30 Mar 2026 09:56:47 -0700 Subject: [PATCH 1089/2056] [modbus] Share helper functions across modbus components - part A (#15291) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/modbus/helpers.py | 83 +++++++++++++ esphome/components/modbus/modbus_helpers.h | 106 +++++++++++++++++ .../components/modbus_controller/__init__.py | 87 ++------------ .../binary_sensor/__init__.py | 2 +- .../modbus_controller/modbus_controller.cpp | 4 +- .../modbus_controller/modbus_controller.h | 109 ++++-------------- .../modbus_controller/number/__init__.py | 6 +- .../modbus_controller/output/__init__.py | 2 +- .../modbus_controller/select/__init__.py | 9 +- .../modbus_controller/sensor/__init__.py | 3 +- .../modbus_controller/switch/__init__.py | 2 +- .../modbus_controller/text_sensor/__init__.py | 2 +- 12 files changed, 231 insertions(+), 184 deletions(-) create mode 100644 esphome/components/modbus/helpers.py create mode 100644 esphome/components/modbus/modbus_helpers.h diff --git a/esphome/components/modbus/helpers.py b/esphome/components/modbus/helpers.py new file mode 100644 index 00000000000..6f97f1e6051 --- /dev/null +++ b/esphome/components/modbus/helpers.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg + +modbus_ns = cg.esphome_ns.namespace("modbus") +modbus_helpers_ns = modbus_ns.namespace("helpers") + +ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") + +MODBUS_FUNCTION_CODE = { + "read_coils": ModbusFunctionCode.READ_COILS, + "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, + "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, + "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, + "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, + "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, + "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, + "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, +} + +ModbusRegisterType_ns = modbus_ns.namespace("ModbusRegisterType") +ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") + +MODBUS_WRITE_REGISTER_TYPE = { + "custom": ModbusRegisterType.CUSTOM, + "coil": ModbusRegisterType.COIL, + "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, + "read": ModbusRegisterType.READ, +} + +SensorValueType_ns = modbus_helpers_ns.namespace("SensorValueType") +SensorValueType = SensorValueType_ns.enum("SensorValueType") +SENSOR_VALUE_TYPE = { + "RAW": SensorValueType.RAW, + "U_WORD": SensorValueType.U_WORD, + "S_WORD": SensorValueType.S_WORD, + "U_DWORD": SensorValueType.U_DWORD, + "U_DWORD_R": SensorValueType.U_DWORD_R, + "S_DWORD": SensorValueType.S_DWORD, + "S_DWORD_R": SensorValueType.S_DWORD_R, + "U_QWORD": SensorValueType.U_QWORD, + "U_QWORD_R": SensorValueType.U_QWORD_R, + "S_QWORD": SensorValueType.S_QWORD, + "S_QWORD_R": SensorValueType.S_QWORD_R, + "FP32": SensorValueType.FP32, + "FP32_R": SensorValueType.FP32_R, +} + +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORD_R": 4, + "S_QWORD": 4, + "S_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + +CPP_TYPE_REGISTER_MAP = { + "RAW": cg.uint16, + "U_WORD": cg.uint16, + "S_WORD": cg.int16, + "U_DWORD": cg.uint32, + "U_DWORD_R": cg.uint32, + "S_DWORD": cg.int32, + "S_DWORD_R": cg.int32, + "U_QWORD": cg.uint64, + "U_QWORD_R": cg.uint64, + "S_QWORD": cg.int64, + "S_QWORD_R": cg.int64, + "FP32": cg.float_, + "FP32_R": cg.float_, +} diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h new file mode 100644 index 00000000000..9f78de1c21a --- /dev/null +++ b/esphome/components/modbus/modbus_helpers.h @@ -0,0 +1,106 @@ +#pragma once + +#include <string> + +#include "esphome/core/helpers.h" +#include "esphome/components/modbus/modbus_definitions.h" + +namespace esphome::modbus::helpers { + +enum class SensorValueType : uint8_t { + RAW = 0x00, // variable length + U_WORD = 0x1, // 1 Register unsigned + U_DWORD = 0x2, // 2 Registers unsigned + S_WORD = 0x3, // 1 Register signed + S_DWORD = 0x4, // 2 Registers signed + BIT = 0x5, + U_DWORD_R = 0x6, // 2 Registers unsigned + S_DWORD_R = 0x7, // 2 Registers unsigned + U_QWORD = 0x8, + S_QWORD = 0x9, + U_QWORD_R = 0xA, + S_QWORD_R = 0xB, + FP32 = 0xC, + FP32_R = 0xD +}; + +inline bool value_type_is_float(SensorValueType v) { + return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; +} + +inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::READ_COILS; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::READ_DISCRETE_INPUTS; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_HOLDING_REGISTERS; + case ModbusRegisterType::READ: + return ModbusFunctionCode::READ_INPUT_REGISTERS; + default: + return ModbusFunctionCode::CUSTOM; + } +} + +inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::WRITE_SINGLE_COIL; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::CUSTOM; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; + case ModbusRegisterType::READ: + default: + return ModbusFunctionCode::CUSTOM; + } +} + +inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } + +/** Get a byte from a hex string + * byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 + * byte_from_hex_str("1122", 0) returns 0x11 + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return byte value + */ +inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { + if (value.length() < pos * 2 + 2) + return 0; + return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); +} + +/** Get a word from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return word value + */ +inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { + return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); +} + +/** Get a dword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return dword value + */ +inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { + return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); +} + +/** Get a qword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return qword value + */ +inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { + return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); +} + +} // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index cb0969913ac..9e332425a68 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -4,6 +4,13 @@ from esphome import automation import esphome.codegen as cg from esphome.components import modbus from esphome.components.const import CONF_ENABLED +from esphome.components.modbus.helpers import ( + CPP_TYPE_REGISTER_MAP, + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, + ModbusRegisterType, +) import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET from esphome.cpp_helpers import logging @@ -41,7 +48,6 @@ CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") -modbus_ns = cg.esphome_ns.namespace("modbus") ModbusController = modbus_controller_ns.class_( "ModbusController", cg.PollingComponent, modbus.ModbusDevice ) @@ -50,85 +56,6 @@ SensorItem = modbus_controller_ns.struct("SensorItem") ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") -ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") -ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") -MODBUS_FUNCTION_CODE = { - "read_coils": ModbusFunctionCode.READ_COILS, - "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, - "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, - "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, - "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, - "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, - "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, - "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, -} - -ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") -ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") - -MODBUS_WRITE_REGISTER_TYPE = { - "custom": ModbusRegisterType.CUSTOM, - "coil": ModbusRegisterType.COIL, - "holding": ModbusRegisterType.HOLDING, -} - -MODBUS_REGISTER_TYPE = { - **MODBUS_WRITE_REGISTER_TYPE, - "discrete_input": ModbusRegisterType.DISCRETE_INPUT, - "read": ModbusRegisterType.READ, -} - -SensorValueType_ns = modbus_controller_ns.namespace("SensorValueType") -SensorValueType = SensorValueType_ns.enum("SensorValueType") -SENSOR_VALUE_TYPE = { - "RAW": SensorValueType.RAW, - "U_WORD": SensorValueType.U_WORD, - "S_WORD": SensorValueType.S_WORD, - "U_DWORD": SensorValueType.U_DWORD, - "U_DWORD_R": SensorValueType.U_DWORD_R, - "S_DWORD": SensorValueType.S_DWORD, - "S_DWORD_R": SensorValueType.S_DWORD_R, - "U_QWORD": SensorValueType.U_QWORD, - "U_QWORD_R": SensorValueType.U_QWORD_R, - "S_QWORD": SensorValueType.S_QWORD, - "S_QWORD_R": SensorValueType.S_QWORD_R, - "FP32": SensorValueType.FP32, - "FP32_R": SensorValueType.FP32_R, -} - -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORD_R": 4, - "S_QWORD": 4, - "S_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - -CPP_TYPE_REGISTER_MAP = { - "RAW": cg.uint16, - "U_WORD": cg.uint16, - "S_WORD": cg.int16, - "U_DWORD": cg.uint32, - "U_DWORD_R": cg.uint32, - "S_DWORD": cg.int32, - "S_DWORD_R": cg.int32, - "U_QWORD": cg.uint64, - "U_QWORD_R": cg.uint64, - "S_QWORD": cg.int64, - "S_QWORD_R": cg.int64, - "FP32": cg.float_, - "FP32_R": cg.float_, -} - - _LOGGER = logging.getLogger(__name__) SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 2ae008f6300..18d017e13f5 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index ea6ba9d0859..38eaea2d1c6 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -535,7 +535,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command( ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; cmd.register_type = register_type; - cmd.function_code = modbus_register_read_function(register_type); + cmd.function_code = modbus::helpers::modbus_register_read_function(register_type); cmd.register_address = start_address; cmd.register_count = register_count; cmd.on_data_func = std::move(handler); @@ -548,7 +548,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbu ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; cmd.register_type = register_type; - cmd.function_code = modbus_register_read_function(register_type); + cmd.function_code = modbus::helpers::modbus_register_read_function(register_type); cmd.register_address = start_address; cmd.register_count = register_count; cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address, diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 693908dca41..438eb12c2a9 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/modbus/modbus.h" +#include "esphome/components/modbus/modbus_helpers.h" #include "esphome/core/automation.h" #include <list> @@ -19,109 +20,43 @@ class ModbusController; using modbus::ModbusFunctionCode; using modbus::ModbusRegisterType; using modbus::ModbusExceptionCode; +using modbus::helpers::SensorValueType; -enum class SensorValueType : uint8_t { - RAW = 0x00, // variable length - U_WORD = 0x1, // 1 Register unsigned - U_DWORD = 0x2, // 2 Registers unsigned - S_WORD = 0x3, // 1 Register signed - S_DWORD = 0x4, // 2 Registers signed - BIT = 0x5, - U_DWORD_R = 0x6, // 2 Registers unsigned - S_DWORD_R = 0x7, // 2 Registers unsigned - U_QWORD = 0x8, - S_QWORD = 0x9, - U_QWORD_R = 0xA, - S_QWORD_R = 0xB, - FP32 = 0xC, - FP32_R = 0xD -}; - -inline bool value_type_is_float(SensorValueType v) { - return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; -} +// Remove before 2026.10.0 — these helpers have moved to modbus::helpers +ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0") +inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); } +ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0") inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { - switch (reg_type) { - case ModbusRegisterType::COIL: - return ModbusFunctionCode::READ_COILS; - break; - case ModbusRegisterType::DISCRETE_INPUT: - return ModbusFunctionCode::READ_DISCRETE_INPUTS; - break; - case ModbusRegisterType::HOLDING: - return ModbusFunctionCode::READ_HOLDING_REGISTERS; - break; - case ModbusRegisterType::READ: - return ModbusFunctionCode::READ_INPUT_REGISTERS; - break; - default: - return ModbusFunctionCode::CUSTOM; - break; - } + return modbus::helpers::modbus_register_read_function(reg_type); } + +ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0") inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { - switch (reg_type) { - case ModbusRegisterType::COIL: - return ModbusFunctionCode::WRITE_SINGLE_COIL; - break; - case ModbusRegisterType::DISCRETE_INPUT: - return ModbusFunctionCode::CUSTOM; - break; - case ModbusRegisterType::HOLDING: - return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; - break; - case ModbusRegisterType::READ: - default: - return ModbusFunctionCode::CUSTOM; - break; - } + return modbus::helpers::modbus_register_write_function(reg_type); } -inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } +ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0") +inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); } -/** Get a byte from a hex string - * byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 - * byte_from_hex_str("1122", 0) returns 0x11 - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return byte value - */ +ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { - if (value.length() < pos * 2 + 2) - return 0; - return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); + return modbus::helpers::byte_from_hex_str(value, pos); } -/** Get a word from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return word value - */ +ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { - return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); + return modbus::helpers::word_from_hex_str(value, pos); } -/** Get a dword from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return dword value - */ +ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { - return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); + return modbus::helpers::dword_from_hex_str(value, pos); } -/** Get a qword from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return qword value - */ +ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { - return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); + return modbus::helpers::qword_from_hex_str(value, pos); } // Extract data from modbus response buffer @@ -585,7 +520,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; - if (value_type_is_float(item.sensor_value_type)) { + if (modbus::helpers::value_type_is_float(item.sensor_value_type)) { float_value = bit_cast<float>(static_cast<uint32_t>(number)); } else { float_value = static_cast<float>(number); @@ -597,7 +532,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) { int64_t val; - if (value_type_is_float(value_type)) { + if (modbus::helpers::value_type_is_float(value_type)) { val = bit_cast<uint32_t>(value); } else { val = llroundf(value); diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index b5efd7abf00..7563adfad99 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,5 +1,9 @@ import esphome.codegen as cg from esphome.components import number +from esphome.components.modbus.helpers import ( + MODBUS_WRITE_REGISTER_TYPE, + SENSOR_VALUE_TYPE, +) import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -11,8 +15,6 @@ from esphome.const import ( ) from .. import ( - MODBUS_WRITE_REGISTER_TYPE, - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 1800a90d57e..1ec4afd9979 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import output +from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY from .. import ( - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, modbus_calc_properties, diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index c94532da516..334a4dfd768 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,15 +1,10 @@ import esphome.codegen as cg from esphome.components import select +from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, TYPE_REGISTER_MAP import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC -from .. import ( - SENSOR_VALUE_TYPE, - TYPE_REGISTER_MAP, - ModbusController, - SensorItem, - modbus_controller_ns, -) +from .. import ModbusController, SensorItem, modbus_controller_ns from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index d8fce54ecee..5b72586c66d 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -1,11 +1,10 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index e325e6198e9..a40c15ab92f 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import switch +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 35cae645e1e..995357143e4 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, From 1a86e88373996828d5927c08ad53318e6340cb04 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 30 Mar 2026 13:15:02 -0500 Subject: [PATCH 1090/2056] [thermostat] Fix stale `max_runtime_exceeded` causing spurious supplemental heating/cooling (#15274) --- .../components/thermostat/thermostat_climate.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d52a22f880d..eb3e756bc2a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -606,6 +606,16 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off, + // even if supplemental_action_ is already idle (early-return path). This prevents a stale + // heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle + // when HEATING_MAX_RUN_TIME fires while the main action is already IDLE. + if (action == climate::CLIMATE_ACTION_OFF || action == climate::CLIMATE_ACTION_IDLE) { + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + } // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->supplemental_action_) && this->setup_complete_) { // already in target mode @@ -975,8 +985,10 @@ void ThermostatClimate::cooling_on_timer_callback_() { void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); - if (this->supports_fan_only_action_uses_fan_mode_timer_) + if (this->supports_fan_only_action_uses_fan_mode_timer_) { this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); + } } void ThermostatClimate::fanning_off_timer_callback_() { From ddb188e8f03d7c113c690154d9f386af188d0f4f Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 30 Mar 2026 13:15:13 -0500 Subject: [PATCH 1091/2056] [bme68x_bsec2] Fix warning spam, code clean-up (#15258) --- .../components/bme68x_bsec2/bme68x_bsec2.cpp | 67 +++++++++---------- .../components/bme68x_bsec2/bme68x_bsec2.h | 62 ++++++++--------- 2 files changed, 62 insertions(+), 67 deletions(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index cf516f6ca68..d9e00e65b27 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -6,10 +6,7 @@ #ifdef USE_BSEC2 #include "bme68x_bsec2.h" -#include <string> - -namespace esphome { -namespace bme68x_bsec2 { +namespace esphome::bme68x_bsec2 { #define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") #define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") @@ -18,9 +15,19 @@ namespace bme68x_bsec2 { static const char *const TAG = "bme68x_bsec2.sensor"; -static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; +static constexpr const char *const IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +static bool is_no_new_data_warning(int8_t status) { +#ifdef BME68X_W_NO_NEW_DATA + return status == BME68X_W_NO_NEW_DATA; +#else + return status == 2; +#endif +} void BME68xBSEC2Component::setup() { + this->warn_if_blocking_over_ = 60; // initial reads may block for up to 60ms + this->bsec_status_ = bsec_init_m(&this->bsec_instance_); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); @@ -114,7 +121,8 @@ void BME68xBSEC2Component::loop() { } else { this->status_clear_error(); } - if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { + const bool has_bme68x_warning = this->bme68x_status_ > BME68X_OK && !is_no_new_data_warning(this->bme68x_status_); + if (this->bsec_status_ > BSEC_OK || has_bme68x_warning) { this->status_set_warning(); } else { this->status_clear_warning(); @@ -130,7 +138,7 @@ void BME68xBSEC2Component::loop() { void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { - ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); + ESP_LOGE(TAG, "Configuration blob too large"); this->mark_failed(); return; } @@ -212,14 +220,12 @@ void BME68xBSEC2Component::run_() { if (curr_time_ns < this->bsec_settings_.next_call) { return; } - uint8_t status; - ESP_LOGV(TAG, "Performing sensor run"); struct bme68x_conf bme68x_conf; this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); if (this->bsec_status_ < BSEC_OK) { - ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); + ESP_LOGW(TAG, "Fetching control settings failed (BSEC2 error code %d)", this->bsec_status_); return; } @@ -235,9 +241,9 @@ void BME68xBSEC2Component::run_() { this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; - // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); - status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + // this->bme68x_status_ = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + this->bme68x_status_ = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + this->bme68x_status_ = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); this->op_mode_ = BME68X_FORCED_MODE; ESP_LOGV(TAG, "Using forced mode"); @@ -259,9 +265,8 @@ void BME68xBSEC2Component::run_() { BSEC_TOTAL_HEAT_DUR - (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); - status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - - status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->bme68x_status_ = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + this->bme68x_status_ = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); this->op_mode_ = BME68X_PARALLEL_MODE; ESP_LOGV(TAG, "Using parallel mode"); } @@ -282,24 +287,15 @@ void BME68xBSEC2Component::run_() { this->trigger_time_ns_ = curr_time_ns; this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { - ESP_LOGV(TAG, "Measurement not required"); - this->read_(curr_time_ns); + ESP_LOGV(TAG, "Measurement not required, queueing immediate read"); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", 0, [this]() { this->read_(this->trigger_time_ns_); }); } } void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { ESP_LOGV(TAG, "Reading data"); - if (this->bsec_settings_.trigger_measurement) { - uint8_t current_op_mode; - this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); - - if (current_op_mode == BME68X_SLEEP_MODE) { - ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); - return; - } - } - if (!this->bsec_settings_.process_data) { ESP_LOGV(TAG, "Data processing not required"); return; @@ -309,12 +305,16 @@ void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { uint8_t nFields = 0; this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); + if (is_no_new_data_warning(this->bme68x_status_)) { + ESP_LOGV(TAG, "BME68X did not provide new data"); + return; + } if (this->bme68x_status_ != BME68X_OK) { - ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); + ESP_LOGW(TAG, "Fetching data failed (BME68X error code %d)", this->bme68x_status_); return; } if (nFields < 1) { - ESP_LOGD(TAG, "BME68X did not provide new data"); + ESP_LOGV(TAG, "BME68X did not provide new fields"); return; } @@ -373,7 +373,7 @@ void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); if (this->bsec_status_ != BSEC_OK) { - ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); + ESP_LOGW(TAG, "Signal processing failed (BSEC2 error code %d)", this->bsec_status_); return; } if (num_outputs < 1) { @@ -474,7 +474,7 @@ void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, #endif #ifdef USE_TEXT_SENSOR -void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { +void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const char *value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } @@ -526,6 +526,5 @@ void BME68xBSEC2Component::save_state_(uint8_t accuracy) { ESP_LOGI(TAG, "Saved state"); } -} // namespace bme68x_bsec2 -} // namespace esphome +} // namespace esphome::bme68x_bsec2 #endif diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 1ed72eee037..9317229a1fb 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -19,8 +19,7 @@ #include <bsec2.h> -namespace esphome { -namespace bme68x_bsec2 { +namespace esphome::bme68x_bsec2 { enum AlgorithmOutput { ALGORITHM_OUTPUT_IAQ, @@ -97,7 +96,7 @@ class BME68xBSEC2Component : public Component { void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); #endif #ifdef USE_TEXT_SENSOR - void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); + void publish_sensor_(text_sensor::TextSensor *sensor, const char *value); #endif void load_state_(); @@ -108,39 +107,12 @@ class BME68xBSEC2Component : public Component { struct bme68x_dev bme68x_; bsec_bme_settings_t bsec_settings_; bsec_version_t version_; - uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; - struct bme68x_heatr_conf bme68x_heatr_conf_; - uint8_t op_mode_; // operating mode of sensor - bsec_library_return_t bsec_status_{BSEC_OK}; - int8_t bme68x_status_{BME68X_OK}; - - int64_t last_time_ms_{0}; - int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit - // toolchains with small std::function SBO - uint32_t millis_overflow_counter_{0}; std::queue<std::function<void()>> queue_; + ESPPreferenceObject bsec_state_; uint8_t const *bsec2_configuration_{nullptr}; - uint32_t bsec2_configuration_length_{0}; - bool bsec2_blob_configured_{false}; - - ESPPreferenceObject bsec_state_; - uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day - uint32_t last_state_save_ms_ = 0; - - float temperature_offset_{0}; - - AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; - OperatingAge operating_age_{OPERATING_AGE_28D}; - Voltage voltage_{VOLTAGE_3_3V}; - - SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate - SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; - SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; - SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; - #ifdef USE_SENSOR sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; @@ -155,8 +127,32 @@ class BME68xBSEC2Component : public Component { #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr}; #endif + + int64_t last_time_ms_{0}; + int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit + // toolchains with small std::function SBO + + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_{0}; + uint32_t millis_overflow_counter_{0}; + uint32_t bsec2_configuration_length_{0}; + bsec_library_return_t bsec_status_{BSEC_OK}; + + float temperature_offset_{0}; + + AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; + OperatingAge operating_age_{OPERATING_AGE_28D}; + Voltage voltage_{VOLTAGE_3_3V}; + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + + uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; + uint8_t op_mode_; // operating mode of sensor + int8_t bme68x_status_{BME68X_OK}; + bool bsec2_blob_configured_{false}; }; -} // namespace bme68x_bsec2 -} // namespace esphome +} // namespace esphome::bme68x_bsec2 #endif From 45e6d49d36ebbb1d2d3a19d4cd6704ac2112e01a Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 30 Mar 2026 13:15:27 -0500 Subject: [PATCH 1092/2056] [shtcx] Code clean-up (#15261) --- esphome/components/shtcx/shtcx.cpp | 18 +++++++----------- esphome/components/shtcx/shtcx.h | 16 +++++++++------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index ec12a5babdd..9ec0a2cdb7b 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -2,16 +2,15 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace shtcx { +namespace esphome::shtcx { static const char *const TAG = "shtcx"; -static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; -static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; -static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; -static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; -static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; +static constexpr uint16_t SHTCX_COMMAND_SLEEP = 0xB098; +static constexpr uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; +static constexpr uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; +static constexpr uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; +static constexpr uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; static const LogString *shtcx_type_to_string(SHTCXType type) { switch (type) { @@ -91,8 +90,6 @@ void SHTCXComponent::update() { } else { temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; humidity = 100.0f * float(raw_data[1]) / 65536.0f; - - ESP_LOGD(TAG, "Temperature=%.2f°C Humidity=%.2f%%", temperature, humidity); } if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); @@ -117,5 +114,4 @@ void SHTCXComponent::wake_up() { delayMicroseconds(200); } -} // namespace shtcx -} // namespace esphome +} // namespace esphome::shtcx diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index f9778dce8d7..a86b204e2b7 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -4,10 +4,13 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace shtcx { +namespace esphome::shtcx { -enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; +enum SHTCXType : uint8_t { + SHTCX_TYPE_SHTC3 = 0, + SHTCX_TYPE_SHTC1, + SHTCX_TYPE_UNKNOWN, +}; /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. class SHTCXComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { @@ -23,11 +26,10 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri void wake_up(); protected: - SHTCXType type_; - uint16_t sensor_id_; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + uint16_t sensor_id_; + SHTCXType type_; }; -} // namespace shtcx -} // namespace esphome +} // namespace esphome::shtcx From b579758c469eebbcb23fecefa3043530f493e913 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 30 Mar 2026 13:15:37 -0500 Subject: [PATCH 1093/2056] [dht] Code clean-up (#15271) --- esphome/components/dht/dht.cpp | 28 +++++++++++----------------- esphome/components/dht/dht.h | 13 +++++-------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index fef247f1681..5b7b6a268fa 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace dht { +namespace esphome::dht { static const char *const TAG = "dht"; @@ -45,16 +44,13 @@ void DHT::update() { } if (success) { - ESP_LOGD(TAG, "Temperature %.1f°C Humidity %.1f%%", temperature, humidity); - if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->humidity_sensor_ != nullptr) this->humidity_sensor_->publish_state(humidity); this->status_clear_warning(); } else { - ESP_LOGW(TAG, "Invalid readings! Check pin number and pull-up resistor%s.", - this->is_auto_detect_ ? " and try manually specifying the model" : ""); + ESP_LOGW(TAG, "Invalid readings"); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(NAN); if (this->humidity_sensor_ != nullptr) @@ -73,8 +69,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r *temperature = NAN; int error_code = 0; - int8_t i = 0; - uint8_t data[5] = {0, 0, 0, 0, 0}; + uint8_t data[5] = {}; #ifndef USE_ESP32 this->pin_.pin_mode(gpio::FLAG_OUTPUT); @@ -107,7 +102,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint8_t bit = 7; uint8_t byte = 0; - for (i = -1; i < 40; i++) { + // On 32-bit Xtensa/RISC-V cores, int8_t would require masking/sign-extension for comparisons + // vs. native int. Using int i is native word size — small win in the timing-critical section. + for (int i = -1; i < 40; i++) { uint32_t start_time = micros(); // Wait for rising edge @@ -156,11 +153,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r } } } - if (!report_errors && error_code != 0) - return false; - - if (error_code) { - ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + if (error_code != 0) { + if (report_errors) + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); return false; } @@ -177,7 +172,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r if (checksum_a != data[4] && checksum_b != data[4]) { if (report_errors) { - ESP_LOGW(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); + ESP_LOGW(TAG, "Invalid checksum"); } return false; } @@ -234,5 +229,4 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r return true; } -} // namespace dht -} // namespace esphome +} // namespace esphome::dht diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 4671ee6f272..0c535f7cf6e 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -4,10 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace dht { +namespace esphome::dht { -enum DHTModel { +enum DHTModel : uint8_t { DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_DHT11, DHT_MODEL_DHT22, @@ -42,7 +41,6 @@ class DHT : public PollingComponent { this->t_pin_ = pin; this->pin_ = pin->to_isr(); } - void set_model(DHTModel model) { model_ = model; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -55,13 +53,12 @@ class DHT : public PollingComponent { protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; InternalGPIOPin *t_pin_; ISRInternalGPIOPin pin_; DHTModel model_{DHT_MODEL_AUTO_DETECT}; bool is_auto_detect_{false}; - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace dht -} // namespace esphome +} // namespace esphome::dht From ad3f6ae3139b1ad8cb60bc27bb9c4ddc45336f2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:20:52 -1000 Subject: [PATCH 1094/2056] [automation] Remove actions_end_ pointer from ActionList to save RAM (#15283) --- esphome/core/automation.h | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index fc2cad99be0..05c7f195885 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -419,44 +419,48 @@ template<typename... Ts> class Action { template<typename... Ts> class ActionList { public: void add_action(Action<Ts...> *action) { - if (this->actions_end_ == nullptr) { - this->actions_begin_ = action; - } else { - this->actions_end_->next_ = action; - } - this->actions_end_ = action; + // Walk to end of chain - action lists are short and only built during setup() + Action<Ts...> **tail = &this->actions_; + while (*tail != nullptr) + tail = &(*tail)->next_; + *tail = action; } void add_actions(const std::initializer_list<Action<Ts...> *> &actions) { + // Find tail once, then append all actions in a single pass + Action<Ts...> **tail = &this->actions_; + while (*tail != nullptr) + tail = &(*tail)->next_; for (auto *action : actions) { - this->add_action(action); + *tail = action; + tail = &action->next_; } } // Force-inline: part of the Trigger→Automation→ActionList forwarding // chain collapsed to reduce automation call stack depth. inline void play(const Ts &...x) ESPHOME_ALWAYS_INLINE { - if (this->actions_begin_ != nullptr) - this->actions_begin_->play_complex(x...); + if (this->actions_ != nullptr) + this->actions_->play_complex(x...); } void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{}); } void stop() { - if (this->actions_begin_ != nullptr) - this->actions_begin_->stop_complex(); + if (this->actions_ != nullptr) + this->actions_->stop_complex(); } - bool empty() const { return this->actions_begin_ == nullptr; } + bool empty() const { return this->actions_ == nullptr; } /// Check if any action in this action list is currently running. bool is_running() { - if (this->actions_begin_ == nullptr) + if (this->actions_ == nullptr) return false; - return this->actions_begin_->is_running(); + return this->actions_->is_running(); } /// Return the number of actions in this action list that are currently running. int num_running() { - if (this->actions_begin_ == nullptr) + if (this->actions_ == nullptr) return 0; - return this->actions_begin_->num_running_total(); + return this->actions_->num_running_total(); } protected: @@ -464,8 +468,7 @@ template<typename... Ts> class ActionList { this->play(std::get<S>(tuple)...); } - Action<Ts...> *actions_begin_{nullptr}; - Action<Ts...> *actions_end_{nullptr}; + Action<Ts...> *actions_{nullptr}; }; template<typename... Ts> class Automation { From ffee4c22b3416d774f7cf398ffbe1b03523b168e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:21:58 -1000 Subject: [PATCH 1095/2056] [esp32_ble] Devirtualize BLE event handler dispatch (#15310) --- esphome/components/esp32_ble/__init__.py | 66 +++++++++++++++--- esphome/components/esp32_ble/ble.cpp | 21 ++---- esphome/components/esp32_ble/ble.h | 69 +++++++------------ .../components/esp32_ble_beacon/__init__.py | 1 - .../esp32_ble_beacon/esp32_ble_beacon.h | 4 +- .../components/esp32_ble_server/__init__.py | 1 - .../components/esp32_ble_server/ble_server.h | 7 +- .../components/esp32_ble_tracker/__init__.py | 2 - .../esp32_ble_tracker/esp32_ble_tracker.h | 13 ++-- esphome/core/helpers.h | 32 +++++++++ 10 files changed, 128 insertions(+), 88 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 43208eb87eb..2e5e3587536 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -134,10 +134,38 @@ class HandlerCounts: _handler_counts = HandlerCounts() +def _add_callback( + parent_var: cg.MockObj, + method: str, + handler_var: cg.MockObj, + params: str, + call_args: str, +) -> None: + """Generate a lambda callback that forwards to a handler method. + + Uses a braced scope with a local pointer variable so the generated C++ + lambda captures only that pointer, avoiding GCC warnings about capturing + variables with static storage duration. + """ + cg.add( + cg.RawStatement( + f"{{ auto *h = {handler_var}; " + f"{parent_var}->{method}(" + f"[h]({params}) {{ h->{call_args}; }}); }}" + ) + ) + + def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: """Register a GAP event handler and track the count.""" _handler_counts.gap_event += 1 - cg.add(parent_var.register_gap_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gap_event_callback", + handler_var, + "esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param", + "gap_event_handler(event, param)", + ) def register_gap_scan_event_handler( @@ -145,7 +173,13 @@ def register_gap_scan_event_handler( ) -> None: """Register a GAP scan event handler and track the count.""" _handler_counts.gap_scan_event += 1 - cg.add(parent_var.register_gap_scan_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gap_scan_event_callback", + handler_var, + "const esphome::esp32_ble::BLEScanResult &scan_result", + "gap_scan_event_handler(scan_result)", + ) def register_gattc_event_handler( @@ -153,7 +187,13 @@ def register_gattc_event_handler( ) -> None: """Register a GATTc event handler and track the count.""" _handler_counts.gattc_event += 1 - cg.add(parent_var.register_gattc_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gattc_event_callback", + handler_var, + "esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param", + "gattc_event_handler(event, gattc_if, param)", + ) def register_gatts_event_handler( @@ -161,7 +201,13 @@ def register_gatts_event_handler( ) -> None: """Register a GATTs event handler and track the count.""" _handler_counts.gatts_event += 1 - cg.add(parent_var.register_gatts_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gatts_event_callback", + handler_var, + "esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param", + "gatts_event_handler(event, gatts_if, param)", + ) def register_ble_status_event_handler( @@ -169,7 +215,13 @@ def register_ble_status_event_handler( ) -> None: """Register a BLE status event handler and track the count.""" _handler_counts.ble_status_event += 1 - cg.add(parent_var.register_ble_status_event_handler(handler_var)) + _add_callback( + parent_var, + "add_ble_status_event_callback", + handler_var, + "", + "ble_before_disabled_event_handler()", + ) def register_bt_logger(*loggers: BTLoggers) -> None: @@ -225,10 +277,6 @@ NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) -GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") -GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") -GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") - BLEEnabledCondition = esp32_ble_ns.class_("BLEEnabledCondition", automation.Condition) BLEEnableAction = esp32_ble_ns.class_("BLEEnableAction", automation.Action) BLEDisableAction = esp32_ble_ns.class_("BLEDisableAction", automation.Action) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 317f8fd11b0..2cd2ec67f7a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -408,9 +408,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param; ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); - for (auto *gatts_handler : this->gatts_event_handlers_) { - gatts_handler->gatts_event_handler(event, gatts_if, param); - } + this->gatts_event_callbacks_.call(event, gatts_if, param); break; } #endif @@ -420,9 +418,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param; ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); - for (auto *gattc_handler : this->gattc_event_handlers_) { - gattc_handler->gattc_event_handler(event, gattc_if, param); - } + this->gattc_event_callbacks_.call(event, gattc_if, param); break; } #endif @@ -431,10 +427,7 @@ void ESP32BLE::loop() { switch (gap_event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } + this->gap_scan_event_callbacks_.call(ble_event->scan_result()); #endif break; @@ -478,9 +471,7 @@ void ESP32BLE::loop() { } // clang-format on // Dispatch to all registered handlers - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(gap_event, param); - } + this->gap_event_callbacks_.call(gap_event, param); } #endif break; @@ -518,9 +509,7 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { ESP_LOGD(TAG, "Disabling"); #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - for (auto *ble_event_handler : this->ble_status_event_handlers_) { - ble_event_handler->ble_before_disabled_event_handler(); - } + this->ble_status_event_callbacks_.call(); #endif if (!ble_dismantle_()) { diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 82b27894611..de8c8c23432 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -87,37 +87,6 @@ enum BLEComponentState : uint8_t { BLE_COMPONENT_STATE_ACTIVE, }; -class GAPEventHandler { - public: - virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; -}; - -class GAPScanEventHandler { - public: - virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; -}; - -#ifdef USE_ESP32_BLE_CLIENT -class GATTcEventHandler { - public: - virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) = 0; -}; -#endif - -#ifdef USE_ESP32_BLE_SERVER -class GATTsEventHandler { - public: - virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) = 0; -}; -#endif - -class BLEStatusEventHandler { - public: - virtual void ble_before_disabled_event_handler() = 0; -}; - class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } @@ -154,22 +123,28 @@ class ESP32BLE : public Component { #endif #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + template<typename F> void add_gap_event_callback(F &&callback) { + this->gap_event_callbacks_.add(std::forward<F>(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - void register_gap_scan_event_handler(GAPScanEventHandler *handler) { - this->gap_scan_event_handlers_.push_back(handler); + template<typename F> void add_gap_scan_event_callback(F &&callback) { + this->gap_scan_event_callbacks_.add(std::forward<F>(callback)); } #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + template<typename F> void add_gattc_event_callback(F &&callback) { + this->gattc_event_callbacks_.add(std::forward<F>(callback)); + } #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + template<typename F> void add_gatts_event_callback(F &&callback) { + this->gatts_event_callbacks_.add(std::forward<F>(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - void register_ble_status_event_handler(BLEStatusEventHandler *handler) { - this->ble_status_event_handlers_.push_back(handler); + template<typename F> void add_ble_status_event_callback(F &&callback) { + this->ble_status_event_callbacks_.add(std::forward<F>(callback)); } #endif void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } @@ -202,21 +177,27 @@ class ESP32BLE : public Component { private: template<typename... Args> friend void enqueue_ble_event(Args... args); - // Handler vectors - use StaticVector when counts are known at compile time #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - StaticVector<GAPEventHandler *, ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT> gap_event_handlers_; + StaticCallbackManager<ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT, + void(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *)> + gap_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - StaticVector<GAPScanEventHandler *, ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT> gap_scan_event_handlers_; + StaticCallbackManager<ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT, void(const BLEScanResult &)> + gap_scan_event_callbacks_; #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - StaticVector<GATTcEventHandler *, ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT> gattc_event_handlers_; + StaticCallbackManager<ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT, + void(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *)> + gattc_event_callbacks_; #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - StaticVector<GATTsEventHandler *, ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT> gatts_event_handlers_; + StaticCallbackManager<ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT, + void(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *)> + gatts_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - StaticVector<BLEStatusEventHandler *, ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT> ble_status_event_handlers_; + StaticCallbackManager<ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT, void()> ble_status_event_callbacks_; #endif // Large objects (size depends on template parameters, but typically aligned to 4 bytes) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 04c783980d3..e2e790164ee 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -13,7 +13,6 @@ esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") ESP32BLEBeacon = esp32_ble_beacon_ns.class_( "ESP32BLEBeacon", cg.Component, - esp32_ble.GAPEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) CONF_MAJOR = "major" diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 7a0424f3aa2..e16c413179f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct { using namespace esp32_ble; -class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented<ESP32BLE> { +class ESP32BLEBeacon : public Component, public Parented<ESP32BLE> { public: explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {} @@ -51,7 +51,7 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } #endif - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); protected: void on_advertise_(); diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 827ddba9554..57106cd93be 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -72,7 +72,6 @@ BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic") BLEServer = esp32_ble_server_ns.class_( "BLEServer", cg.Component, - esp32_ble.GATTsEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace( diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 1b419d2ee45..9708ed40c8b 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -24,7 +24,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> { +class BLEServer : public Component, public Parented<ESP32BLE> { public: void setup() override; void loop() override; @@ -53,10 +53,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv const uint16_t *get_clients() const { return this->clients_; } uint8_t get_client_count() const { return this->client_count_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) override; + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void ble_before_disabled_event_handler() override; + void ble_before_disabled_event_handler(); // Direct callback registration - supports multiple callbacks void on_connect(std::function<void(uint16_t)> &&callback) { diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index c5e8f3178d0..b9c4c28ccfd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -90,8 +90,6 @@ esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", cg.Component, - esp32_ble.GAPEventHandler, - esp32_ble.GATTcEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f50ed107b6d..ff69a4dcd2d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -291,10 +291,6 @@ class ESPBTClient : public ESPBTDeviceListener { }; class ESP32BLETracker : public Component, - public GAPEventHandler, - public GAPScanEventHandler, - public GATTcEventHandler, - public BLEStatusEventHandler, #ifdef USE_OTA_STATE_LISTENER public ota::OTAGlobalStateListener, #endif @@ -325,11 +321,10 @@ class ESP32BLETracker : public Component, void start_scan(); void stop_scan(); - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; - void gap_scan_event_handler(const BLEScanResult &scan_result) override; - void ble_before_disabled_event_handler() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void gap_scan_event_handler(const BLEScanResult &scan_result); + void ble_before_disabled_event_handler(); #ifdef USE_OTA_STATE_LISTENER void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 66ba166445f..f96b888e287 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1830,6 +1830,38 @@ template<typename... Ts> class CallbackManager<void(Ts...)> { std::vector<Callback<void(Ts...)>> callbacks_; }; +/** CallbackManager backed by StaticVector for compile-time-known callback counts. + * + * Drop-in replacement for CallbackManager that avoids std::vector template bloat + * (_M_realloc_insert, etc.) when the maximum number of callbacks is known at compile time. + * + * @tparam N Maximum number of callbacks (compile-time constant, typically from cg.add_define()) + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template<size_t N, typename... X> class StaticCallbackManager; + +template<size_t N, typename... Ts> class StaticCallbackManager<N, void(Ts...)> { + public: + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline without heap allocation. + template<typename F> void add(F &&callback) { this->add_(Callback<void(Ts...)>::create(std::forward<F>(callback))); } + + /// Call all callbacks in this manager. + void call(Ts... args) { + for (auto &cb : this->callbacks_) + cb.call(args...); + } + size_t size() const { return this->callbacks_.size(); } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { call(args...); } + + protected: + /// Non-template core to avoid code duplication per lambda type. + void add_(Callback<void(Ts...)> cb) { this->callbacks_.push_back(cb); } + StaticVector<Callback<void(Ts...)>, N> callbacks_; +}; + template<typename... X> class LazyCallbackManager; /** Lazy-allocating callback manager that only allocates memory when callbacks are registered. From 8969eb76e9bdd12734f96af0200c89a18b70bc75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:24:17 -1000 Subject: [PATCH 1096/2056] [wifi] Avoid redundant SDK calls in WiFi loop on ESP8266 (#15303) --- esphome/components/wifi/wifi_component.cpp | 8 +--- esphome/components/wifi/wifi_component.h | 13 +++++- .../wifi/wifi_component_esp8266.cpp | 44 ++++++++++--------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index db203326679..7b31a22ed5b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -784,7 +784,8 @@ void WiFiComponent::loop() { } case WIFI_COMPONENT_STATE_STA_CONNECTED: { - if (!this->is_connected_()) { + // Use cached connected_ set unconditionally at the top of loop() + if (!this->connected_) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); @@ -2129,11 +2130,6 @@ void WiFiComponent::retry_connect() { } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected_() const { - return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && - this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; -} -void WiFiComponent::update_connected_state_() { this->connected_ = this->is_connected_(); } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 8dfe5fa7afa..073341fe79a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -670,8 +670,11 @@ class WiFiComponent final : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_() const; - bool is_connected_() const; - void update_connected_state_(); + bool is_connected_() const { + return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && + this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; + } + void update_connected_state_() { this->connected_ = this->is_connected_(); } bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP @@ -811,6 +814,12 @@ class WiFiComponent final : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; +#ifdef USE_ESP8266 + // ESP8266WiFiSTAState enum, defined in wifi_component_esp8266.cpp. + // Written from SDK system context (wifi_event_callback) — uint8_t writes + // are atomic on Xtensa LX106 so no synchronization is needed. + uint8_t sta_state_{0}; +#endif RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 03800cc3a92..cb53d3ac1bd 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -44,11 +44,14 @@ namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; -static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +enum class ESP8266WiFiSTAState : uint8_t { + IDLE, // Not connecting + CONNECTING, // Connection in progress + ASSOCIATED, // Associated to AP, waiting for IP + CONNECTED, // Successfully connected with IP + ERROR_NOT_FOUND, // AP not found (probe failed) + ERROR_FAILED, // Connection failed (auth, timeout, etc.) +}; bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { uint8_t current_mode = wifi_get_opmode(); @@ -359,11 +362,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // Reset flags, do this _before_ wifi_station_connect as the callback method // may be called from wifi_station_connect - s_sta_connecting = true; - s_sta_connected = false; - s_sta_got_ip = false; - s_sta_connect_error = false; - s_sta_connect_not_found = false; + this->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::CONNECTING); ETS_UART_INTR_DISABLE(); ret = wifi_station_connect(); @@ -493,7 +492,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf, it.channel); #endif - s_sta_connected = true; + global_wifi_component->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::ASSOCIATED); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Defer listener notification until state machine reaches STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations @@ -506,16 +505,14 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { if (it.reason == REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, (const char *) it.ssid); - s_sta_connect_not_found = true; + global_wifi_component->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::ERROR_NOT_FOUND); } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason))); - s_sta_connect_error = true; + global_wifi_component->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::ERROR_FAILED); } - s_sta_connected = false; - s_sta_connecting = false; global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS global_wifi_component->pending_.disconnect = true; @@ -541,7 +538,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { mask_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); - s_sta_got_ip = true; + global_wifi_component->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS // Defer listener callbacks to main loop - system context has limited stack global_wifi_component->pending_.got_ip = true; @@ -636,17 +633,22 @@ void WiFiComponent::wifi_pre_setup_() { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { - station_status_t status = wifi_station_get_connect_status(); - if (status == STATION_GOT_IP) + // Use cached state from wifi_event_callback() instead of calling + // wifi_station_get_connect_status() which queries the SDK every time. + // Use if statements with early returns instead of switch to avoid GCC + // generating a CSWTCH lookup table in .rodata (flash) on ESP8266. + auto state = static_cast<ESP8266WiFiSTAState>(this->sta_state_); + if (state == ESP8266WiFiSTAState::CONNECTED) return WiFiSTAConnectStatus::CONNECTED; - if (status == STATION_NO_AP_FOUND) + if (state == ESP8266WiFiSTAState::ERROR_NOT_FOUND) return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD) + if (state == ESP8266WiFiSTAState::ERROR_FAILED) return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - if (status == STATION_CONNECTING) + if (state == ESP8266WiFiSTAState::CONNECTING || state == ESP8266WiFiSTAState::ASSOCIATED) return WiFiSTAConnectStatus::CONNECTING; return WiFiSTAConnectStatus::IDLE; } + bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) From 46ea61666e97061d32a115b29a38328553067e33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:24:34 -1000 Subject: [PATCH 1097/2056] [wifi] Replace FreeRTOS queue with LockFreeQueue on ESP-IDF (#15306) --- esphome/components/wifi/wifi_component.h | 11 ++++++++ .../wifi/wifi_component_esp_idf.cpp | 26 ++++++------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 073341fe79a..9a08902d47b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -6,6 +6,9 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#ifdef USE_ESP32 +#include "esphome/core/lock_free_queue.h" +#endif #include "esphome/core/string_ref.h" #include <span> @@ -727,6 +730,7 @@ class WiFiComponent final : public Component { #ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); + friend void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif #ifdef USE_RP2040 @@ -871,6 +875,13 @@ class WiFiComponent final : public Component { bool is_high_performance_mode_{false}; #endif +#ifdef USE_ESP32 + // Lock-free SPSC queue for WiFi events from ESP-IDF event handler. + // 17 slots = 16 usable (ring buffer reserves one slot). WiFi events are rare. + // Placed at end of class to avoid padding between smaller fields. + LockFreeQueue<IDFWiFiEvent, 17> event_queue_; +#endif + private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d8b3db96679..4097df80afd 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -47,7 +47,6 @@ namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #ifdef USE_WIFI_AP static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -132,11 +131,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi return; } - // copy to heap to keep queue object small + // copy to heap — WiFi events are rare so heap alloc is fine auto *to_send = new IDFWiFiEvent; // NOLINT(cppcoreguidelines-owning-memory) memcpy(to_send, &event, sizeof(IDFWiFiEvent)); - // don't block, we may miss events but the core can handle that - if (xQueueSend(s_event_queue, &to_send, 0L) != pdPASS) { + if (!global_wifi_component->event_queue_.push(to_send)) { delete to_send; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -157,12 +155,6 @@ void WiFiComponent::wifi_pre_setup_() { ESP_LOGE(TAG, "xEventGroupCreate failed"); return; } - // NOLINTNEXTLINE(bugprone-sizeof-expression) - s_event_queue = xQueueCreate(64, sizeof(IDFWiFiEvent *)); - if (s_event_queue == nullptr) { - ESP_LOGE(TAG, "xQueueCreate failed"); - return; - } err = esp_event_loop_create_default(); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); @@ -724,16 +716,14 @@ const char *get_disconnect_reason_str(uint8_t reason) { } void WiFiComponent::wifi_loop_() { - while (true) { - IDFWiFiEvent *data; - if (xQueueReceive(s_event_queue, &data, 0L) != pdTRUE) { - // no event ready - break; - } + uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); + } - // process event + IDFWiFiEvent *data; + while ((data = this->event_queue_.pop()) != nullptr) { wifi_process_event_(data); - delete data; // NOLINT(cppcoreguidelines-owning-memory) } } From 8688ef7125cd7eac012ec26e90b0e4c63e67ebb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:24:48 -1000 Subject: [PATCH 1098/2056] [benchmark] Fix decode benchmarks being optimized away by compiler (#15293) --- .../components/api/bench_proto_decode.cpp | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 113201dd8a1..961c629f2a9 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -10,10 +10,9 @@ namespace esphome::api::benchmarks { // sub-microsecond benchmarks. static constexpr int kInnerIterations = 2000; -// Helper: encode a message into a buffer and return it. -// Benchmarks encode once in setup, then decode the resulting bytes in a loop. -// This keeps decode benchmarks in sync with the actual protobuf schema — -// hand-encoded byte arrays would silently break when fields change. +// Helper: encode a message into an APIBuffer for reuse in decode benchmarks. +// Optimization barriers are applied to the decode target objects via +// DoNotOptimize/ClobberMemory, not to this buffer. template<typename T> static APIBuffer encode_message(const T &msg) { APIBuffer buffer; uint32_t size = msg.calculate_size(); @@ -23,6 +22,12 @@ template<typename T> static APIBuffer encode_message(const T &msg) { return buffer; } +/// Force a pointer through an asm barrier so the compiler cannot +/// prove its contents are unchanged across iterations. +/// benchmark::DoNotOptimize/ClobberMemory are insufficient under +/// CodSpeed's valgrind-based instrumentation. +static void escape(void *p) { asm volatile("" : : "g"(p) : "memory"); } + // --- HelloRequest decode (string + varint fields) --- static void Decode_HelloRequest(benchmark::State &state) { @@ -31,13 +36,18 @@ static void Decode_HelloRequest(benchmark::State &state) { source.api_version_major = 1; source.api_version_minor = 10; auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - HelloRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + HelloRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.api_version_major); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } @@ -50,13 +60,18 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) { source.key = 0x12345678; source.state = true; auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - SwitchCommandRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + SwitchCommandRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.state); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } @@ -78,13 +93,18 @@ static void Decode_LightCommandRequest(benchmark::State &state) { source.has_effect = true; source.effect = StringRef::from_lit("rainbow"); auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - LightCommandRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + LightCommandRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.brightness); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } From 8561a8c495afdf7caaffb3e043c644dbed7474e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 08:48:04 -1000 Subject: [PATCH 1099/2056] [core] Suppress component source overflow warnings in testing mode (#15320) --- esphome/cpp_helpers.py | 12 +++++++----- tests/unit_tests/test_cpp_helpers.py | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index e7ff2965c83..479090016f8 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -71,11 +71,13 @@ def register_component_source(name: str) -> int: return pool.sources[name] idx = len(pool.sources) + 1 if idx > _MAX_COMPONENT_SOURCES: - _LOGGER.warning( - "Too many unique component source names (max %d), '%s' will show as '<unknown>'", - _MAX_COMPONENT_SOURCES, - name, - ) + if not CORE.testing_mode: + _LOGGER.warning( + "Too many unique component source names (max %d), " + "'%s' will show as '<unknown>'", + _MAX_COMPONENT_SOURCES, + name, + ) return 0 pool.sources[name] = idx _ensure_source_table_registered() diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 52424a7cb22..a76ea21c23b 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -140,9 +140,28 @@ def test_register_component_source_overflow_warns( sources={f"comp_{i}": i + 1 for i in range(0xFF)}, table_registered=True, ) - monkeypatch.setattr(ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool})) + monkeypatch.setattr( + ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}, testing_mode=False) + ) with caplog.at_level(logging.WARNING): idx = register_component_source("overflow_component") assert idx == 0 assert "Too many unique component source names" in caplog.text assert "overflow_component" in caplog.text + + +def test_register_component_source_overflow_suppressed_in_testing_mode( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Pre-fill pool to max + pool = ComponentSourcePool( + sources={f"comp_{i}": i + 1 for i in range(0xFF)}, + table_registered=True, + ) + monkeypatch.setattr( + ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}, testing_mode=True) + ) + with caplog.at_level(logging.WARNING): + idx = register_component_source("overflow_component") + assert idx == 0 + assert "Too many unique component source names" not in caplog.text From f25fa7123599fe3a33fff4c652486b6fe6c23e47 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 31 Mar 2026 06:25:15 +1000 Subject: [PATCH 1100/2056] [lvgl] Fix align_to directives (#15311) --- esphome/components/lvgl/__init__.py | 15 +++++++++++++-- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/lvgl_esphome.h | 15 ++++++++++++--- esphome/components/lvgl/trigger.py | 25 +++++++++++++++++++++++-- esphome/components/lvgl/types.py | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 4 +++- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a6afa12afa4..6377183ef4f 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -48,6 +48,7 @@ from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets +from .defines import CONF_ALIGN_TO_LAMBDA_ID from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -69,8 +70,16 @@ from .schemas import ( ) from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code -from .trigger import add_on_boot_triggers, generate_triggers -from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns +from .trigger import add_on_boot_triggers, generate_align_tos, generate_triggers +from .types import ( + IdleTrigger, + PlainTrigger, + lv_font_t, + lv_group_t, + lv_lambda_t, + lv_style_t, + lvgl_ns, +) from .widgets import ( LvScrActType, Widget, @@ -345,6 +354,7 @@ async def to_code(configs): Widget.widgets_completed = True async with LvContext(): await generate_triggers() + await generate_align_tos(configs[0]) for config in configs: lv_component = await cg.get_variable(config[CONF_ID]) await generate_page_triggers(config) @@ -458,6 +468,7 @@ LVGL_SCHEMA = cv.All( .extend( { cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), + cv.GenerateID(CONF_ALIGN_TO_LAMBDA_ID): cv.declare_id(lv_lambda_t), cv.GenerateID(df.CONF_DISPLAYS): display_schema, cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16), cv.Optional( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 0a53d886693..72345ca98e1 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -504,6 +504,7 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" +CONF_ALIGN_TO_LAMBDA_ID = "align_to_lambda_id" CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8de82d50c01..21d1e0d4176 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -128,10 +128,19 @@ class LvPageType : public Parented<LvglComponent> { bool skip; }; -using LvLambdaType = std::function<void(lv_obj_t *)>; -using set_value_lambda_t = std::function<void(float)>; using event_callback_t = void(lv_event_t *); -using text_lambda_t = std::function<const char *()>; + +class LvLambdaComponent : public Component { + public: + LvLambdaComponent(void (*callback)()) : callback_(callback) {} + + void setup() override { this->callback_(); } + // execute after the LvglComponent is setup + float get_setup_priority() const override { return setup_priority::PROCESSOR - 5; } + + protected: + void (*callback_)(); +}; template<typename... Ts> class ObjUpdateAction : public Action<Ts...> { public: diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 077ff06bb75..54309cdf891 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -8,10 +8,13 @@ from esphome.const import ( CONF_X, CONF_Y, ) +from esphome.cpp_generator import new_Pvariable +from esphome.cpp_helpers import register_component from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, + CONF_ALIGN_TO_LAMBDA_ID, DIRECTIONS, LV_EVENT_MAP, LV_EVENT_TRIGGERS, @@ -89,14 +92,32 @@ async def generate_triggers(): await add_on_boot_triggers(w.config.get(CONF_ON_BOOT, ())) - # Generate align to directives while we're here - if align_to := w.config.get(CONF_ALIGN_TO): + +async def generate_align_tos(config: dict): + """ + Called once, with a full lvgl configuration to emit deferred align_to actions as a component + that executes after the LVGL setup. This is required since align_to actions are not recalculated on layout changes + and so must be applied after the display is properly laid out. + :param config: + :return: + """ + align_tos = tuple( + w for w in widget_map.values() if w.config and CONF_ALIGN_TO in w.config + ) + if align_tos: + async with LambdaContext(where="align_to") as context: + for w in align_tos: + align_to = w.config[CONF_ALIGN_TO] target = widget_map[align_to[CONF_ID]].obj align = literal(align_to[CONF_ALIGN]) x = align_to[CONF_X] y = align_to[CONF_Y] lv.obj_align_to(w.obj, target, align, x, y) + action_id = config[CONF_ALIGN_TO_LAMBDA_ID] + var = new_Pvariable(action_id, await context.get_lambda()) + await register_component(var, {}) + async def add_trigger(conf, w, *events, is_selected=None): is_selected = is_selected or w.is_selected() diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 8343a542a9f..686e4292679 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,7 +1,7 @@ from esphome import automation, codegen as cg from esphome.const import CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj -from esphome.cpp_types import esphome_ns +from esphome.cpp_types import Component, esphome_ns from .defines import lvgl_ns @@ -51,7 +51,7 @@ IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglAction = lvgl_ns.class_("LvglAction", automation.Action) -lv_lambda_t = lvgl_ns.class_("LvLambdaType") +lv_lambda_t = lvgl_ns.class_("LvLambdaComponent", Component) LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b168578a98c..821476a72b8 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -288,7 +288,9 @@ lvgl: - label: text: "Hello shiny day" text_color: 0xFFFFFF - align: bottom_mid + align_to: + id: hello_label + align: OUT_LEFT_TOP - label: id: setup_lambda_label # Test lambda in widget property during setup (LvContext) From c5eb0eb984deae5f95edeb9b9f873feeb6aec785 Mon Sep 17 00:00:00 2001 From: Ardumine <61353807+Ardumine@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:50:11 +0100 Subject: [PATCH 1101/2056] [internal_temperature] Add nRF52 Zephyr support (#15297) --- .../internal_temperature.cpp | 46 +++++++++++++++++++ .../components/internal_temperature/sensor.py | 9 +++- .../test.nrf52-adafruit.yaml | 1 + 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/components/internal_temperature/test.nrf52-adafruit.yaml diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 34d7baf8803..567ae6170e1 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -22,11 +22,18 @@ extern "C" { uint32_t temp_single_get_current_temperature(uint32_t *temp_value); } #endif // USE_BK72XX +#if defined(USE_ZEPHYR) && defined(USE_NRF52) +#include <zephyr/device.h> +#include <zephyr/drivers/sensor.h> +#endif // USE_ZEPHYR && USE_NRF52 namespace esphome { namespace internal_temperature { static const char *const TAG = "internal_temperature"; +#if defined(USE_ZEPHYR) && defined(USE_NRF52) +static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp); +#endif // USE_ZEPHYR && USE_NRF52 #ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ @@ -36,6 +43,37 @@ static temperature_sensor_handle_t tsensNew = NULL; #endif // USE_ESP32 void InternalTemperatureSensor::update() { +#if defined(USE_ZEPHYR) && defined(USE_NRF52) + struct sensor_value value; + int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR); + if (result != 0) { + ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value); + if (result != 0) { + ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + const float temperature = value.val1 + (value.val2 / 1000000.0f); + if (std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +#else + float temperature = NAN; bool success = false; #ifdef USE_ESP32 @@ -79,9 +117,17 @@ void InternalTemperatureSensor::update() { this->publish_state(NAN); } } +#endif // USE_ZEPHYR && USE_NRF52 } void InternalTemperatureSensor::setup() { +#if defined(USE_ZEPHYR) && defined(USE_NRF52) + if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) { + ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name); + this->mark_failed(); + return; + } +#endif // USE_ZEPHYR && USE_NRF52 #ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 93b98a30f4b..965e7f05205 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,15 +1,18 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, PLATFORM_BK72XX, PLATFORM_ESP32, + PLATFORM_NRF52, PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) +from esphome.core import CORE internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") InternalTemperatureSensor = internal_temperature_ns.class_( @@ -25,10 +28,14 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), - cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX]), + cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_NRF52]), ) async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) + + if CORE.using_zephyr and CORE.is_nrf52: + zephyr_add_prj_conf("SENSOR", True) + zephyr_add_prj_conf("TEMP_NRF5", True) diff --git a/tests/components/internal_temperature/test.nrf52-adafruit.yaml b/tests/components/internal_temperature/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/internal_temperature/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 58df755d8bfe43123d59033b82c4dc3f82b0197e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:27:30 -1000 Subject: [PATCH 1102/2056] Bump requests from 2.33.0 to 2.33.1 (#15324) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0df5caf1818..8ad5528c958 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.33.0 +requests==2.33.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From 53b2a03c80d22a99fb13aa5f09d665773e414402 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:56:05 -0400 Subject: [PATCH 1103/2056] [multiple] Fix -Wformat and -Wextra warnings across 33 component files (#15321) --- esphome/components/adc/adc_sensor_esp32.cpp | 10 ++++-- esphome/components/api/api_connection.cpp | 12 +++---- .../media_source/audio_file_media_source.cpp | 3 +- esphome/components/bm8563/bm8563.cpp | 7 ++-- .../components/bme68x_bsec2/bme68x_bsec2.cpp | 4 +-- esphome/components/dlms_meter/dlms_meter.cpp | 4 ++- .../components/esp32_touch/esp32_touch.cpp | 2 +- .../components/espnow/espnow_component.cpp | 4 ++- .../components/http_request/http_request.cpp | 2 +- esphome/components/hub75/hub75.cpp | 4 ++- esphome/components/infrared/infrared.cpp | 11 +++--- esphome/components/inkplate/inkplate.cpp | 34 ++++++++++--------- esphome/components/ld2450/ld2450.cpp | 3 +- .../components/max7219digit/max7219digit.cpp | 5 ++- esphome/components/modbus/modbus.cpp | 2 +- .../components/nextion/nextion_commands.cpp | 4 +-- esphome/components/qmp6988/qmp6988.cpp | 6 ++-- esphome/components/rd03d/rd03d.cpp | 4 ++- .../remote_base/symphony_protocol.cpp | 10 +++--- .../components/runtime_image/bmp_decoder.cpp | 4 ++- .../components/serial_proxy/serial_proxy.cpp | 24 +++++++------ esphome/components/spa06_base/spa06_base.cpp | 4 ++- esphome/components/sps30/sps30.cpp | 8 +++-- .../thermostat/thermostat_climate.cpp | 8 +++-- .../components/tormatic/tormatic_cover.cpp | 11 +++--- .../components/tormatic/tormatic_protocol.h | 4 ++- .../uart/uart_component_esp_idf.cpp | 2 +- .../climate/uponor_smatrix_climate.cpp | 4 ++- .../sensor/uponor_smatrix_sensor.cpp | 4 ++- .../uponor_smatrix/uponor_smatrix.cpp | 14 ++++---- esphome/components/vl53l0x/vl53l0x_sensor.cpp | 6 ++-- .../components/water_heater/water_heater.cpp | 5 ++- .../components/zwave_proxy/zwave_proxy.cpp | 4 ++- 33 files changed, 145 insertions(+), 88 deletions(-) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 1d3138623ec..fc707013a89 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -2,6 +2,7 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#include <cinttypes> namespace esphome { namespace adc { @@ -346,7 +347,8 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange summary:"); ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); - ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); + ESP_LOGVV(TAG, " Coefficients: c12=%" PRIu32 ", c6=%" PRIu32 ", c2=%" PRIu32 ", c0=%" PRIu32 ", sum=%" PRIu32, c12, + c6, c2, c0, csum); if (csum == 0) { ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); @@ -354,8 +356,10 @@ float ADCSensor::sample_autorange_() { } const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; - ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, - c0, csum, final_result); + ESP_LOGV(TAG, + "Autorange final: (%.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 ")/%" PRIu32 + " = %.6fV", + mv12, c12, mv6, c6, mv2, c2, mv0, c0, csum, final_result); return final_result; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0a99adcacfb..79df85ada35 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1465,7 +1465,7 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance, + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance, static_cast<uint32_t>(proxies.size())); return; } @@ -1476,7 +1476,7 @@ void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigure void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } proxies[msg.instance]->write_from_client(msg.data, msg.data_len); @@ -1485,7 +1485,7 @@ void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } proxies[msg.instance]->set_modem_pins(msg.line_states); @@ -1494,7 +1494,7 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } SerialProxyGetModemPinsResponse resp{}; @@ -1506,7 +1506,7 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } switch (msg.type) { @@ -1536,7 +1536,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { break; } default: - ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type)); + ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type)); break; } } diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp index 120f871d2ff..fbb5ecd88d4 100644 --- a/esphome/components/audio_file/media_source/audio_file_media_source.cpp +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -4,6 +4,7 @@ #include "esphome/components/audio/audio_decoder.h" +#include <cinttypes> #include <cstring> namespace esphome::audio_file { @@ -249,7 +250,7 @@ void AudioFileMediaSource::decode_task(void *params) { audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); - ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(), + ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %" PRIu32, stream_info.get_bits_per_sample(), stream_info.get_channels(), stream_info.get_sample_rate()); if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 269acfea447..062094c036f 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -1,4 +1,7 @@ #include "bm8563.h" + +#include <cinttypes> + #include "esphome/core/log.h" namespace esphome::bm8563 { @@ -146,10 +149,10 @@ optional<uint8_t> BM8563::read_register_(uint8_t reg) { } void BM8563::set_timer_irq_(uint32_t duration_s) { - ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + ESP_LOGI(TAG, "Timer Duration: %" PRIu32 " s", duration_s); if (duration_s > MAX_TIMER_DURATION_S) { - ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + ESP_LOGW(TAG, "Timer duration %" PRIu32 " s exceeds maximum %" PRIu32 " s", duration_s, MAX_TIMER_DURATION_S); return; } diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index d9e00e65b27..d4ac57d7500 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -89,7 +89,7 @@ void BME68xBSEC2Component::dump_config() { " Operating age: %s\n" " Sample rate: %s\n" " Voltage: %s\n" - " State save interval: %ims\n" + " State save interval: %" PRIu32 "ms\n" " Temperature offset: %.2f", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_), BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_), BME68X_BSEC2_VOLTAGE_LOG(this->voltage_), this->state_save_interval_ms_, this->temperature_offset_); @@ -283,7 +283,7 @@ void BME68xBSEC2Component::run_() { if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { bme68x_get_conf(&bme68x_conf, &this->bme68x_); uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); - ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); + ESP_LOGV(TAG, "Queueing read in %" PRIu32 "us", meas_dur); this->trigger_time_ns_ = curr_time_ns; this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 052a0f4d01f..b732e71d24b 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -1,5 +1,7 @@ #include "dlms_meter.h" +#include <cinttypes> + #if defined(USE_ESP8266_FRAMEWORK_ARDUINO) #include <bearssl/bearssl.h> #elif defined(USE_ESP32) @@ -21,7 +23,7 @@ void DlmsMeterComponent::dump_config() { ESP_LOGCONFIG(TAG, "DLMS Meter:\n" " Provider: %s\n" - " Read Timeout: %u ms", + " Read Timeout: %" PRIu32 " ms", provider_name, this->read_timeout_); #define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_); DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, ) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 0d331b29d60..e44bc807e9a 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -217,7 +217,7 @@ void ESP32TouchComponent::setup() { for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); if (err != ESP_OK) { - ESP_LOGW(TAG, "Oneshot scan %d failed: %s", i, esp_err_to_name(err)); + ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err)); } } diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 78916891f4d..0dc0f12e7e6 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,8 @@ #include "espnow_err.h" +#include <cinttypes> + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -266,7 +268,7 @@ void ESPNowComponent::loop() { if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) { int32_t new_channel = wifi::global_wifi_component->get_wifi_channel(); if (new_channel != this->wifi_channel_) { - ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel); + ESP_LOGI(TAG, "Wifi Channel is changed from %d to %" PRId32 ".", this->wifi_channel_, new_channel); this->wifi_channel_ = new_channel; } } diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 6590d2018ee..2c74638f12c 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "http_request"; void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "HTTP Request:\n" - " Timeout: %ums\n" + " Timeout: %" PRIu32 "ms\n" " User-Agent: %s\n" " Follow redirects: %s\n" " Redirect limit: %d", diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index cf8661b2b33..ba652d427d9 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -1,6 +1,8 @@ #include "hub75_component.h" #include "esphome/core/application.h" +#include <cinttypes> + #ifdef USE_ESP32 namespace esphome::hub75 { @@ -58,7 +60,7 @@ void HUB75Display::dump_config() { config_.pins.oe, config_.pins.clk); ESP_LOGCONFIG(TAG, - " Clock Speed: %u MHz\n" + " Clock Speed: %" PRIu32 " MHz\n" " Latch Blanking: %i\n" " Clock Phase: %s\n" " Min Refresh Rate: %i Hz\n" diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 658c9fd0df5..9b97995a964 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -1,4 +1,7 @@ #include "infrared.h" + +#include <cinttypes> + #include "esphome/core/log.h" #ifdef USE_API @@ -100,7 +103,7 @@ void Infrared::control(const InfraredCall &call) { // Zero-copy from packed protobuf data transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(), call.get_packed_count()); - ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), + ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(), call.get_repeat_count()); } else if (call.is_base64url()) { // Decode base64url (URL-safe) into transmit buffer @@ -113,16 +116,16 @@ void Infrared::control(const InfraredCall &call) { for (int32_t timing : transmit_data->get_data()) { int32_t abs_timing = timing < 0 ? -timing : timing; if (abs_timing > max_timing_us) { - ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us); + ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us); return; } } - ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(), call.get_repeat_count()); } else { // From vector (lambdas/automations) transmit_data->set_data(call.get_raw_timings()); - ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%u", call.get_raw_timings().size(), + ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(), call.get_repeat_count()); } diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 3b4b1a63d57..0511b451a83 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cinttypes> + #include <hal/gpio_hal.h> namespace esphome { @@ -193,7 +195,7 @@ void Inkplate::dump_config() { ESP_LOGCONFIG(TAG, " Greyscale: %s\n" " Partial Updating: %s\n" - " Full Update Every: %d", + " Full Update Every: %" PRIu32, YESNO(this->greyscale_), YESNO(this->partial_updating_), this->full_update_every_); // Log pins LOG_PIN(" CKV Pin: ", this->ckv_pin_); @@ -306,7 +308,7 @@ void Inkplate::fill(Color color) { // If clipping is active, fall back to base implementation if (this->get_clipping().is_set()) { Display::fill(color); - ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time); return; } @@ -329,12 +331,12 @@ void Inkplate::display() { this->display3b_(); } else { if (this->partial_updating_ && this->partial_update_()) { - ESP_LOGV(TAG, "Display finished (partial) (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (partial) (%" PRIu32 "ms)", millis() - start_time); return; } this->display1b_(); } - ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (full) (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display1b_() { @@ -409,7 +411,7 @@ void Inkplate::display1b_() { uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); - ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b start loops (%" PRIu32 "ms)", millis() - start_time); for (uint8_t k = 0; k < rep; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; @@ -440,7 +442,7 @@ void Inkplate::display1b_() { } delayMicroseconds(230); } - ESP_LOGV(TAG, "Display1b first loop x %d (%ums)", 4, millis() - start_time); + ESP_LOGV(TAG, "Display1b first loop x %d (%" PRIu32 "ms)", 4, millis() - start_time); buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); @@ -469,7 +471,7 @@ void Inkplate::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b second loop (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b second loop (%" PRIu32 "ms)", millis() - start_time); if (this->model_ == INKPLATE_6_PLUS) { clean_fast_(2, 2); @@ -495,13 +497,13 @@ void Inkplate::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b third loop (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b third loop (%" PRIu32 "ms)", millis() - start_time); } vscan_start_(); eink_off_(); this->block_partial_ = false; this->partial_updates_ = 0; - ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display3b_() { @@ -614,7 +616,7 @@ void Inkplate::display3b_() { clean_fast_(3, 1); vscan_start_(); eink_off_(); - ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display3b finished (%" PRIu32 "ms)", millis() - start_time); } bool Inkplate::partial_update_() { @@ -641,7 +643,7 @@ bool Inkplate::partial_update_() { this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; } } - ESP_LOGV(TAG, "Partial update buffer built after (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update buffer built after (%" PRIu32 "ms)", millis() - start_time); int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; @@ -667,7 +669,7 @@ bool Inkplate::partial_update_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Partial update loop k=%d (%ums)", k, millis() - start_time); + ESP_LOGV(TAG, "Partial update loop k=%d (%" PRIu32 "ms)", k, millis() - start_time); } clean_fast_(2, 2); clean_fast_(3, 1); @@ -675,7 +677,7 @@ bool Inkplate::partial_update_() { eink_off_(); memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); - ESP_LOGV(TAG, "Partial update finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update finished (%" PRIu32 "ms)", millis() - start_time); return true; } @@ -730,7 +732,7 @@ void Inkplate::clean() { clean_fast_(0, 8); // Black to Black clean_fast_(2, 1); // Black to White clean_fast_(1, 10); // White to White - ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Clean finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { @@ -773,9 +775,9 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Clean fast rep loop %d finished (%ums)", k, millis() - start_time); + ESP_LOGV(TAG, "Clean fast rep loop %d finished (%" PRIu32 "ms)", k, millis() - start_time); } - ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Clean fast finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::pins_z_state_() { diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 6230a8c30bb..58c3cac42d8 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -10,6 +10,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include <cinttypes> #include <cmath> #include <numbers> @@ -575,7 +576,7 @@ void LD2450Component::handle_periodic_data_() { if (this->get_timeout_status_(this->presence_millis_)) { this->target_binary_sensor_->publish_state(false); } else { - ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_); + ESP_LOGV(TAG, "Clear presence waiting timeout: %" PRIu32, this->timeout_); } } } diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index cdceafad501..f9b46cf797d 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -6,6 +6,7 @@ #include "max7219font.h" #include <algorithm> +#include <cinttypes> namespace esphome { namespace max7219digit { @@ -92,7 +93,9 @@ void MAX7219Component::loop() { if (this->scroll_mode_ == ScrollMode::STOP) { if (static_cast<size_t>(this->stepsleft_ + get_width_internal()) == first_line_size + 1) { if (millis_since_last_scroll < this->scroll_dwell_) { - ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", + ESP_LOGVV(TAG, + "Dwell time at end of string in case of stop at end. Step %d, since last scroll %" PRIu32 + ", dwell %d.", this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); return; } diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 4146a54c877..3b1a038be34 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -313,7 +313,7 @@ void Modbus::send_next_frame_() { this->last_send_ = millis(); this->tx_buffer_.pop_front(); if (!this->tx_buffer_.empty()) { - ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size()); + ESP_LOGV(TAG, "Write queue contains %zu items.", this->tx_buffer_.size()); } } diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 6718646efa3..6c8e0f18bc8 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -319,14 +319,14 @@ void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radiu void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, uint16_t background_color, uint16_t foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( - "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, + "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRId32 ",%" PRIu8 ",\"%s\"", x1, y1, size, background_color, foreground_color, logo_pic, border_width, content); } void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, Color background_color, Color foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( - "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, + "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRId32 ",%" PRIu8 ",\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color), display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content); } diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 17d91c36330..976efe79103 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -1,4 +1,6 @@ #include "qmp6988.h" + +#include <cinttypes> #include <cmath> namespace esphome { @@ -129,7 +131,7 @@ bool QMP6988Component::get_calibration_data_() { ESP_LOGV(TAG, "Calibration data:\n" - " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_a0[%" PRId32 "] COE_a1[%d] COE_a2[%d] COE_b00[%" PRId32 "]\n" " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, @@ -153,7 +155,7 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, "Int calibration data:\n" - " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " a0[%" PRId32 "] a1[%" PRId32 "] a2[%" PRId32 "] b00[%" PRId32 "]\n" " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index d47347fcfa6..c9c6a546ab8 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -1,6 +1,8 @@ #include "rd03d.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" + +#include <cinttypes> #include <cmath> namespace esphome::rd03d { @@ -56,7 +58,7 @@ void RD03DComponent::dump_config() { *this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi"); } if (this->throttle_ > 0) { - ESP_LOGCONFIG(TAG, " Throttle: %ums", this->throttle_); + ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_); } #ifdef USE_SENSOR LOG_SENSOR(" ", "Target Count", this->target_count_sensor_); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index f30a980d91e..6844e449ed0 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -1,6 +1,8 @@ #include "symphony_protocol.h" #include "esphome/core/log.h" +#include <cinttypes> + namespace esphome { namespace remote_base { @@ -26,8 +28,8 @@ static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); - ESP_LOGD(TAG, "Sending Symphony: data=0x%0*X nbits=%u repeats=%u", (data.nbits + 3) / 4, (uint32_t) data.data, - data.nbits, data.repeats); + ESP_LOGD(TAG, "Sending Symphony: data=0x%0*" PRIX32 " nbits=%" PRIu8 " repeats=%" PRIu8, (data.nbits + 3) / 4, + (uint32_t) data.data, data.nbits, data.repeats); // Each bit produces a mark+space (2 entries). We fold the inter-frame/footer gap // into the last bit's space of each frame to avoid over-length gaps. dst->reserve(data.nbits * 2u * data.repeats); @@ -112,8 +114,8 @@ optional<SymphonyData> SymphonyProtocol::decode(RemoteReceiveData src) { } void SymphonyProtocol::dump(const SymphonyData &data) { - const int32_t hex_width = (data.nbits + 3) / 4; // pad to nibble width - ESP_LOGI(TAG, "Received Symphony: data=0x%0*X, nbits=%d", hex_width, (uint32_t) data.data, data.nbits); + const int hex_width = (data.nbits + 3) / 4; // pad to nibble width + ESP_LOGI(TAG, "Received Symphony: data=0x%0*" PRIX32 ", nbits=%" PRIu8, hex_width, (uint32_t) data.data, data.nbits); } } // namespace remote_base diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 174f924b285..6a1bd61d862 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -6,6 +6,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cinttypes> + namespace esphome::runtime_image { static const char *const TAG = "image_decoder.bmp"; @@ -107,7 +109,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } if (this->compression_method_ != 0) { - ESP_LOGE(TAG, "Unsupported compression method: %d", this->compression_method_); + ESP_LOGE(TAG, "Unsupported compression method: %" PRIu32, this->compression_method_); return DECODE_ERROR_UNSUPPORTED_FORMAT; } diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index f3c256c62a7..04c94e9292d 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -3,6 +3,8 @@ #ifdef USE_SERIAL_PROXY #include "esphome/core/log.h" + +#include <cinttypes> #include "esphome/core/util.h" #ifdef USE_API @@ -74,7 +76,7 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) { void SerialProxy::dump_config() { ESP_LOGCONFIG(TAG, - "Serial Proxy [%u]:\n" + "Serial Proxy [%" PRIu32 "]:\n" " Name: %s\n" " Port Type: %s\n" " RTS Pin: %s\n" @@ -89,7 +91,9 @@ void SerialProxy::dump_config() { void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size) { - ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u", + ESP_LOGD(TAG, + "Configuring serial proxy [%" PRIu32 "]: baud=%" PRIu32 ", flow_ctrl=%s, parity=%" PRIu8 ", stop=%" PRIu8 + ", data=%" PRIu8, this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size); auto *uart_comp = this->parent_; @@ -148,7 +152,7 @@ void SerialProxy::write_from_client(const uint8_t *data, size_t len) { void SerialProxy::set_modem_pins(uint32_t line_states) { const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; - ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); + ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); if (this->rts_pin_ != nullptr) { this->rts_state_ = rts; @@ -161,12 +165,12 @@ void SerialProxy::set_modem_pins(uint32_t line_states) { } uint32_t SerialProxy::get_modem_pins() const { - return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) | - (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); + return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) | + (this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u); } uart::UARTFlushResult SerialProxy::flush_port() { - ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "Flushing serial proxy [%" PRIu32 "]", this->instance_index_); return this->flush(); } @@ -180,19 +184,19 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: } this->api_connection_ = api_connection; this->enable_loop(); - ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); break; case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: if (this->api_connection_ != api_connection) { - ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); return; } this->api_connection_ = nullptr; this->disable_loop(); - ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_); break; default: - ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(type)); + ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(type)); break; } } diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp index 36268aa9a26..b0490628cbc 100644 --- a/esphome/components/spa06_base/spa06_base.cpp +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -1,5 +1,7 @@ #include "spa06_base.h" +#include <cinttypes> + #include "esphome/core/helpers.h" namespace esphome::spa06_base { @@ -195,7 +197,7 @@ bool SPA06Component::read_coefficients_() { ESP_LOGV(TAG, "Coefficients:\n" " c0: %i, c1: %i,\n" - " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c00: %" PRIi32 ", c10: %" PRIi32 ", c20: %i, c30: %i, c40: %i,\n" " c01: %i, c11: %i, c21: %i, c31: %i", this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, this->c21_, this->c31_); diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index dbb44743d25..e4fc4ffd31d 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #include "sps30.h" +#include <cinttypes> + namespace esphome { namespace sps30 { @@ -105,7 +107,7 @@ void SPS30Component::dump_config() { " Firmware version v%0d.%0d", this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); if (this->idle_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Idle interval: %us", this->idle_interval_.value() / 1000); + ESP_LOGCONFIG(TAG, " Idle interval: %" PRIu32 "s", this->idle_interval_.value() / 1000); } LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); @@ -142,8 +144,8 @@ void SPS30Component::update() { // If its not time to take an action, do nothing. const uint32_t update_start_ms = millis(); if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) { - ESP_LOGD(TAG, "Sensor waiting for %ums before transitioning to state %d.", (this->next_state_ms_ - update_start_ms), - this->next_state_); + ESP_LOGD(TAG, "Sensor waiting for %" PRIu32 "ms before transitioning to state %d.", + (this->next_state_ms_ - update_start_ms), this->next_state_); return; } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index eb3e756bc2a..d979359c1f8 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cinttypes> namespace esphome::thermostat { @@ -1346,15 +1347,16 @@ void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex t if (elapsed >= new_duration_ms) { // Timer should complete immediately (including when new_duration_ms is 0) - ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms); + ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %" PRIu32 " >= new %" PRIu32 ")", timer_index, elapsed, + new_duration_ms); this->timer_[timer_index].active = false; // Trigger the timer callback immediately this->call_timer_callback_(timer_index); return; } else { // Adjust timer to run for remaining time - keep original start time - ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms, - new_duration_ms - elapsed); + ESP_LOGVV(TAG, "timer %d adjusted: elapsed %" PRIu32 ", new total %" PRIu32 ", remaining %" PRIu32, timer_index, + elapsed, new_duration_ms, new_duration_ms - elapsed); this->timer_[timer_index].time = new_duration_ms; return; } diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index 37a269088e6..77c2e87717e 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -1,3 +1,4 @@ +#include <cinttypes> #include <vector> #include "tormatic_cover.h" @@ -120,11 +121,11 @@ void Tormatic::recalibrate_duration_(GateStatus s) { if (s == OPENED) { this->open_duration_ = now - this->direction_start_time_; - ESP_LOGI(TAG, "Recalibrated the gate's open duration to %dms", this->open_duration_); + ESP_LOGI(TAG, "Recalibrated the gate's open duration to %" PRIu32 "ms", this->open_duration_); } if (s == CLOSED) { this->close_duration_ = now - this->direction_start_time_; - ESP_LOGI(TAG, "Recalibrated the gate's close duration to %dms", this->close_duration_); + ESP_LOGI(TAG, "Recalibrated the gate's close duration to %" PRIu32 "ms", this->close_duration_); } this->direction_start_time_ = 0; @@ -269,7 +270,7 @@ optional<GateStatus> Tormatic::read_gate_status_() { switch (hdr.type) { case STATUS: { if (hdr.payload_size() != sizeof(StatusReply)) { - ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(), + ESP_LOGE(TAG, "Header specifies payload size %" PRIu32 " but size of StatusReply is %zu", hdr.payload_size(), sizeof(StatusReply)); } @@ -294,7 +295,7 @@ optional<GateStatus> Tormatic::read_gate_status_() { default: // Unknown message type, drain the remaining amount of bytes specified in // the header. - ESP_LOGE(TAG, "Reading remaining %d payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type); + ESP_LOGE(TAG, "Reading remaining %" PRIu32 " payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type); break; } @@ -339,7 +340,7 @@ template<typename T> optional<T> Tormatic::read_data_() { } obj.byteswap(); - ESP_LOGV(TAG, "Read %s in %d ms", obj.print().c_str(), millis() - start); + ESP_LOGV(TAG, "Read %s in %" PRIu32 " ms", obj.print().c_str(), millis() - start); return obj; } diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 26a634b6306..269b63ff783 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -1,5 +1,7 @@ #pragma once +#include <cinttypes> + #include "esphome/components/cover/cover.h" /** @@ -86,7 +88,7 @@ struct MessageHeader { std::string print() { // 64 bytes: "MessageHeader: seq " + uint16 + ", len " + uint32 + ", type " + type + safety margin char buf[64]; - buf_append_printf(buf, sizeof(buf), 0, "MessageHeader: seq %d, len %d, type %s", this->seq, this->len, + buf_append_printf(buf, sizeof(buf), 0, "MessageHeader: seq %d, len %" PRIu32 ", type %s", this->seq, this->len, message_type_to_str(this->type)); return buf; } diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index cd77cd11897..6d9d44e97fc 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -296,7 +296,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { int32_t write_len = uart_write_bytes(this->uart_num_, data, len); if (write_len != (int32_t) len) { - ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + ESP_LOGW(TAG, "uart_write_bytes failed: %" PRId32 " != %zu", write_len, len); this->mark_failed(); } #ifdef USE_UART_DEBUGGER diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 3eae4d2d966..512a2581229 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cinttypes> + namespace esphome { namespace uponor_smatrix { @@ -10,7 +12,7 @@ static const char *const TAG = "uponor_smatrix.climate"; void UponorSmatrixClimate::dump_config() { LOG_CLIMATE("", "Uponor Smatrix Climate", this); - ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_); + ESP_LOGCONFIG(TAG, " Device address: 0x%08" PRIX32, this->address_); } void UponorSmatrixClimate::loop() { diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 7ee12edcdb1..5f690a6879a 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -1,6 +1,8 @@ #include "uponor_smatrix_sensor.h" #include "esphome/core/log.h" +#include <cinttypes> + namespace esphome { namespace uponor_smatrix { @@ -9,7 +11,7 @@ static const char *const TAG = "uponor_smatrix.sensor"; void UponorSmatrixSensor::dump_config() { ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor\n" - " Device address: 0x%08X", + " Device address: 0x%08" PRIX32, this->address_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_); diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 4c3a4b05dfa..1fd53955a0e 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include <cinttypes> + namespace esphome { namespace uponor_smatrix { @@ -24,7 +26,7 @@ void UponorSmatrixComponent::dump_config() { #ifdef USE_TIME if (this->time_id_ != nullptr) { ESP_LOGCONFIG(TAG, " Time synchronization: YES"); - ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_); + ESP_LOGCONFIG(TAG, " Time master device address: 0x%08" PRIX32 "", this->time_device_address_); } #endif @@ -33,7 +35,7 @@ void UponorSmatrixComponent::dump_config() { if (!this->unknown_devices_.empty()) { ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); for (auto device_address : this->unknown_devices_) { - ESP_LOGCONFIG(TAG, " 0x%08X", device_address); + ESP_LOGCONFIG(TAG, " 0x%08" PRIX32 "", device_address); } } } @@ -103,14 +105,14 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)]; #endif - ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, + ESP_LOGV(TAG, "Received packet: addr=%08" PRIX32 ", data=%s, crc=%04X", device_address, format_hex_to(hex_buf, &packet[4], packet_len - 6), crc); // Handle packet size_t data_len = (packet_len - 6) / 3; if (data_len == 0) { if (packet[4] == UPONOR_ID_REQUEST) - ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address); + ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08" PRIX32 "", device_address); return true; } @@ -135,7 +137,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { if (data[i].id == UPONOR_ID_DATETIME1) found_time = true; if (found_temperature && found_time) { - ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address); + ESP_LOGI(TAG, "Using detected time device address 0x%08" PRIX32 "", device_address); this->time_device_address_ = device_address; break; } @@ -154,7 +156,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Log unknown device addresses if (!found && !this->unknown_devices_.count(device_address)) { - ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address); + ESP_LOGI(TAG, "Received packet for unknown device address 0x%08" PRIX32 " ", device_address); this->unknown_devices_.insert(device_address); } diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 8a76ed77605..58b5a426754 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -1,6 +1,8 @@ #include "vl53l0x_sensor.h" #include "esphome/core/log.h" +#include <cinttypes> + /* * Most of the code in this integration is based on the VL53L0x library * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X @@ -28,8 +30,8 @@ void VL53L0XSensor::dump_config() { LOG_PIN(" Enable Pin: ", this->enable_pin_); } ESP_LOGCONFIG(TAG, - " Timeout: %u%s\n" - " Timing Budget %uus ", + " Timeout: %" PRIu32 "%s\n" + " Timing Budget %" PRIu32 "us ", this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)", this->measurement_timing_budget_us_); } diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9a74877f0a6..9ee8faadee2 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -1,5 +1,7 @@ #include "water_heater.h" #include "esphome/core/log.h" + +#include <cinttypes> #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" #include "esphome/core/progmem.h" @@ -110,7 +112,8 @@ void WaterHeaterCall::validate_() { auto traits = this->parent_->get_traits(); if (this->mode_.has_value()) { if (!traits.supports_mode(*this->mode_)) { - ESP_LOGW(TAG, "'%s' - Mode %d not supported", this->parent_->get_name().c_str(), *this->mode_); + ESP_LOGW(TAG, "'%s' - Mode %" PRIu32 " not supported", this->parent_->get_name().c_str(), + static_cast<uint32_t>(*this->mode_)); this->mode_.reset(); } } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index ad4357663ff..7653d2b678e 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -3,6 +3,8 @@ #ifdef USE_API #include "esphome/components/api/api_server.h" + +#include <cinttypes> #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -160,7 +162,7 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en break; default: - ESP_LOGW(TAG, "Unknown request type: %d", type); + ESP_LOGW(TAG, "Unknown request type: %" PRIu32, static_cast<uint32_t>(type)); break; } } From ef65e47bc58ef6df76506be39060ce9114d9e3ca Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino <glm.net@gmail.com> Date: Mon, 30 Mar 2026 21:08:50 -0300 Subject: [PATCH 1104/2056] [schema] generator fixes (#15276) --- esphome/components/sensor/__init__.py | 4 + esphome/config_validation.py | 4 + script/build_language_schema.py | 206 ++++++++++++++++++-------- 3 files changed, 151 insertions(+), 63 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 650f5ed826a..626466eefa0 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -113,6 +113,7 @@ from esphome.core.entity_helpers import ( setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj, MockObjClass +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -229,7 +230,10 @@ _SENSOR_ENTITY_CATEGORIES = { } +@schema_extractor("enum") def sensor_entity_category(value): + if value == SCHEMA_EXTRACT: + return _SENSOR_ENTITY_CATEGORIES return cv.enum(_SENSOR_ENTITY_CATEGORIES, lower=True)(value) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 56f255a076f..45d2cd81175 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -417,10 +417,14 @@ def icon(value): return value +@schema_extractor("use_id") def sub_device_id(value: str | None) -> core.ID | None: # Lazy import to avoid circular imports from esphome.core.config import Device + if value == SCHEMA_EXTRACT: + return Device + if not value: return None diff --git a/script/build_language_schema.py b/script/build_language_schema.py index bea540dc632..09ff9999013 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -67,19 +67,16 @@ def get_component_names(): # pylint: disable-next=redefined-outer-name,reimported from esphome.loader import CORE_COMPONENTS_PATH - component_names = ["esphome", "sensor", "esp32", "esp8266"] - skip_components = [] - - for d in CORE_COMPONENTS_PATH.iterdir(): - if ( - not d.name.startswith("__") - and d.is_dir() - and d.name not in component_names - and d.name not in skip_components - ): - component_names.append(d.name) - - return sorted(component_names) + # return sorted( + # ["esphome", "sensor", "esp32", "esp8266", "adc", "touchscreen", "xpt2046"] + # ) + return sorted( + [ + d.name + for d in CORE_COMPONENTS_PATH.iterdir() + if not d.name.startswith("__") and d.is_dir() + ] + ) def load_components(): @@ -120,39 +117,57 @@ from esphome.util import Registry # noqa: E402 # pylint: enable=wrong-import-position +def sort_obj(obj): + if isinstance(obj, dict): + return {k: sort_obj(v) for k, v in sorted(obj.items(), key=lambda x: str(x[0]))} + if isinstance(obj, list): + return [sort_obj(item) for item in obj] + return obj + + def write_file(name, obj): full_path = Path(args.output_path) / f"{name}.json" + sorted_obj = sort_obj(obj) if JSON_DUMP_PRETTY: - json_str = json.dumps(obj, indent=2) + json_str = json.dumps(sorted_obj, indent=2) else: - json_str = json.dumps(obj, separators=(",", ":")) + json_str = json.dumps(sorted_obj, separators=(",", ":")) write_file_if_changed(full_path, json_str) - print(f"Wrote {full_path}") def delete_extra_files(keep_names): output_path = Path(args.output_path) + count = 0 for d in output_path.iterdir(): if d.suffix == ".json" and d.stem not in keep_names: + count += 1 d.unlink() - print(f"Deleted {d}") + return count def register_module_schemas(key, module, manifest=None): + count = 0 for name, schema in module_schemas(module): + count += 1 register_known_schema(key, name, schema) - if manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: + if ( + manifest + and manifest.multi_conf + and key in output + and S_CONFIG_SCHEMA in output[key][S_SCHEMAS] + ): # Multi conf should allow list of components # not sure about 2nd part of the if, might be useless config (e.g. as3935) output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + return count def register_known_schema(module, name, schema): if module not in output: output[module] = {S_SCHEMAS: {}} config = convert_config(schema, f"{module}/{name}") - if S_TYPE not in config: + if S_TYPE not in config and name != "FINAL_VALIDATE_SCHEMA" and module != "core": print(f"Config var without type: {module}.{name}") output[module][S_SCHEMAS][name] = config @@ -175,14 +190,23 @@ def module_schemas(module): except OSError: # some empty __init__ files module_str = "" - schemas = {} + schemas = [] for m_attr_name in dir(module): m_attr_obj = getattr(module, m_attr_name) if is_convertible_schema(m_attr_obj): - schemas[module_str.find(m_attr_name)] = [m_attr_name, m_attr_obj] + # Find where the name is assigned in the module source to preserve + # definition order. Using ^NAME\s*= (multiline) targets assignments + # at column 0, so "CONFIG_SCHEMA" won't collide with "CONFIG_SCHEMA_BASE". + match = re.search( + r"^" + re.escape(m_attr_name) + r"\s*=", + module_str, + re.MULTILINE, + ) + pos = match.start() if match else -1 + schemas.append((pos, m_attr_name, m_attr_obj)) - for pos in sorted(schemas.keys()): - yield schemas[pos] + for _, name, obj in sorted(schemas, key=lambda x: x[0]): + yield name, obj found_registries = {} @@ -240,9 +264,16 @@ def add_module_registries(domain, module): if len(parts) == 2: reg_domain = parts[0] reg_entry_name = parts[1] - else: - reg_domain = ".".join([parts[1], parts[0]]) - reg_entry_name = parts[2] + elif len(parts) == 3: + # is a platform or a component? + if parts[0] in schema_core[S_PLATFORMS]: + reg_domain = ".".join([parts[1], parts[0]]) + reg_entry_name = parts[2] + elif parts[0] in schema_core[S_COMPONENTS]: + reg_domain = parts[0] + reg_entry_name = ".".join([parts[1], parts[2]]) + else: + print(f"registry {name} is unknown") if reg_domain not in output: output[reg_domain] = {} @@ -252,8 +283,6 @@ def add_module_registries(domain, module): attr_obj[name].schema, f"{reg_domain}/{reg_type}/{reg_entry_name}" ) - # print(f"{domain} - {attr_name} - {name}") - def do_pins(): # do pin registries @@ -330,6 +359,35 @@ def fix_font(): ) +def fix_globals(): + if "globals" not in output: + return + from esphome.components.globals import _NON_RESTORING_SCHEMA + + config = convert_config(_NON_RESTORING_SCHEMA, "globals/CONFIG_SCHEMA") + config["is_list"] = True + output["globals"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + +def fix_mapping(): + if "mapping" not in output: + return + from esphome.components.mapping import BASE_SCHEMA + + config = convert_config(BASE_SCHEMA, "mapping/CONFIG_SCHEMA") + output["mapping"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + +def fix_image(): + if "image" not in output: + return + from esphome.components.image import IMAGE_SCHEMA + + config = convert_config(IMAGE_SCHEMA, "image/CONFIG_SCHEMA") + config["is_list"] = True + output["image"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + def fix_menu(): if "display_menu_base" not in output: return @@ -355,7 +413,7 @@ def fix_menu(): # 4. Configure menu items inside as recursive menu = schemas["MENU_TYPES"][S_SCHEMA][S_CONFIG_VARS]["items"]["types"]["menu"] menu[S_CONFIG_VARS].pop("items") - menu[S_EXTENDS] = ["display_menu_base.MENU_TYPES"] + menu[S_EXTENDS].append("display_menu_base.MENU_TYPES") def get_logger_tags(): @@ -531,7 +589,6 @@ def shrink(): else: arr_s.pop(S_EXTENDS) arr_s |= key_s[S_SCHEMA] - print(x) # simple types should be spread on each component, # for enums so far these are logger.is_log_level, cover.validate_cover_state and pulse_counter.sensor.COUNT_MODE_SCHEMA @@ -580,6 +637,10 @@ def shrink(): domain_schemas[S_SCHEMAS].pop(schema_name) +def is_cv_invalid(schema): + return repr(schema).startswith("<function invalid.<locals>.validator") + + def build_schema(): print("Building schema") @@ -610,7 +671,8 @@ def build_schema(): output[domain] = {S_COMPONENTS: {}, S_SCHEMAS: {}} platforms[domain] = {} elif manifest.config_schema is not None: - # e.g. dallas + if is_cv_invalid(manifest.config_schema): + continue output[domain] = {S_SCHEMAS: {S_CONFIG_SCHEMA: {}}} # Generate platforms (e.g. sensor, binary_sensor, climate ) @@ -621,7 +683,9 @@ def build_schema(): # Generate components for domain, manifest in components.items(): if domain not in platforms: - if manifest.config_schema is not None: + if manifest.config_schema is not None and not is_cv_invalid( + manifest.config_schema + ): core_components[domain] = {} if len(manifest.dependencies) > 0: core_components[domain]["dependencies"] = manifest.dependencies @@ -630,14 +694,15 @@ def build_schema(): for platform in platforms: platform_manifest = get_platform(domain=platform, platform=domain) if platform_manifest is not None: - output[platform][S_COMPONENTS][domain] = {} - if len(platform_manifest.dependencies) > 0: - output[platform][S_COMPONENTS][domain]["dependencies"] = ( - platform_manifest.dependencies - ) - register_module_schemas( + count = register_module_schemas( f"{domain}.{platform}", platform_manifest.module, platform_manifest ) + if count > 0: + output[platform][S_COMPONENTS].setdefault(domain, {}) + if len(platform_manifest.dependencies) > 0: + output[platform][S_COMPONENTS][domain]["dependencies"] = ( + platform_manifest.dependencies + ) # Do registries add_module_registries("core", automation) @@ -657,6 +722,9 @@ def build_schema(): fix_remote_receiver() fix_script() fix_font() + fix_globals() + fix_mapping() + fix_image() add_logger_tags() shrink() fix_menu() @@ -677,12 +745,19 @@ def build_schema(): # bundle core inside esphome data["esphome"]["core"] = data.pop("core")["core"] + if GENERATED_ID_TYPES: + print( + "Unconsumed id_type matchers:", + [id_type for _, id_type in GENERATED_ID_TYPES], + ) + if args.check: # do not gen files return for c, s in data.items(): write_file(c, s) - delete_extra_files(data.keys()) + deleted = delete_extra_files(data.keys()) + print(f"Written {len(data.items())} deleted {deleted} files.") def is_convertible_schema(schema): @@ -711,6 +786,30 @@ def convert_config(schema, path): return converted +GENERATED_ID_TYPES = [ + ( + lambda p: p.startswith("i2c/CONFIG_SCHEMA/") and p.endswith("/id"), + {"class": "i2c::I2CBus", "parents": ["Component"]}, + ), + ( + lambda p: p == "uart/CONFIG_SCHEMA/val 1/ext0/all/id", + {"class": "uart::UARTComponent", "parents": ["Component"]}, + ), + ( + lambda p: p == "http_request/CONFIG_SCHEMA/val 1/ext0/all/id", + {"class": "http_request::HttpRequestComponent", "parents": ["Component"]}, + ), + ( + lambda p: ( + p + == "uptime.sensor/CONFIG_SCHEMA/type_timestamp/ext0/ext1/all/time_id/val 1" + ), + {}, + ), + (lambda p: p == "esp_ldo/action/voltage.adjust/all/all/id", {}), +] + + def convert(schema, config_var, path): """config_var can be a config_var or a schema: both are dicts config_var has a S_TYPE property, if this is S_SCHEMA, then it has a S_SCHEMA property @@ -718,9 +817,6 @@ def convert(schema, config_var, path): """ repr_schema = repr(schema) - if path.startswith("ads1115.sensor") and path.endswith("gain"): - print(path) - if repr_schema in known_schemas: schema_info = known_schemas[(repr_schema)] for schema_instance, name in schema_info: @@ -841,8 +937,6 @@ def convert(schema, config_var, path): schema({"delay": "1s"}) except cv.Invalid: config_var["has_required_var"] = True - else: - print("figure out " + path) elif schema_type == "effects": config_var[S_TYPE] = "registry" config_var["registry"] = "light.effects" @@ -879,8 +973,6 @@ def convert(schema, config_var, path): "id" ]["id_type"]["class"] config_var[S_TYPE] = "use_id" - else: - print("TODO deferred?") elif isinstance(data, str): # TODO: Figure out why pipsolar does this config_var["use_id_type"] = data @@ -890,23 +982,11 @@ def convert(schema, config_var, path): else: raise TypeError("Unknown extracted schema type") elif config_var.get("key") == "GeneratedID": - if path.startswith("i2c/CONFIG_SCHEMA/") and path.endswith("/id"): - config_var["id_type"] = { - "class": "i2c::I2CBus", - "parents": ["Component"], - } - elif path == "uart/CONFIG_SCHEMA/val 1/ext0/all/id": - config_var["id_type"] = { - "class": "uart::UARTComponent", - "parents": ["Component"], - } - elif path == "http_request/CONFIG_SCHEMA/val 1/ext0/all/id": - config_var["id_type"] = { - "class": "http_request::HttpRequestComponent", - "parents": ["Component"], - } - elif path == "pins/esp32/val 1/id": - config_var["id_type"] = "pin" + for i, (matcher, id_type) in enumerate(GENERATED_ID_TYPES): + if matcher(path): + config_var["id_type"] = id_type + GENERATED_ID_TYPES.pop(i) + break else: print("Cannot determine id_type for " + path) From a3913b98ba4d41263d9022a09fb91d21b4747384 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 30 Mar 2026 17:05:48 -1000 Subject: [PATCH 1105/2056] [wifi] Move LibreTiny WiFi STA state to member variable (#15305) --- esphome/components/wifi/wifi_component.h | 8 +++--- .../wifi/wifi_component_libretiny.cpp | 25 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 9a08902d47b..665dec37d5a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -818,10 +818,10 @@ class WiFiComponent final : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; -#ifdef USE_ESP8266 - // ESP8266WiFiSTAState enum, defined in wifi_component_esp8266.cpp. - // Written from SDK system context (wifi_event_callback) — uint8_t writes - // are atomic on Xtensa LX106 so no synchronization is needed. +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + // Platform-specific STA state enum, defined in platform cpp file. + // On ESP8266, written from SDK system context (wifi_event_callback) — + // uint8_t writes are atomic on Xtensa LX106 so no synchronization is needed. uint8_t sta_state_{0}; #endif RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b049a0413c1..9565ffa7473 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -97,8 +97,6 @@ enum class LTWiFiSTAState : uint8_t { ERROR_FAILED, // Connection failed (auth, timeout, etc.) }; -static LTWiFiSTAState s_sta_state = LTWiFiSTAState::IDLE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - // Count of ignored disconnect events during connection - too many indicates real failure static uint8_t s_ignored_disconnect_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Threshold for ignored disconnect events before treating as connection failure @@ -223,7 +221,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { this->wifi_apply_hostname_(); // Reset state machine and disconnect counter before connecting - s_sta_state = LTWiFiSTAState::CONNECTING; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTING); s_ignored_disconnect_count = 0; WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), @@ -459,7 +457,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); - s_sta_state = LTWiFiSTAState::IDLE; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::IDLE); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -479,7 +477,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - s_sta_state = LTWiFiSTAState::CONNECTED; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS this->notify_ip_state_listeners_(); #endif @@ -501,12 +499,13 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed. // However, if we get too many of these events (IGNORED_DISCONNECT_THRESHOLD), treat it // as a real connection failure to avoid waiting the full timeout for a failing connection. - if (it.ssid_len == 0 && s_sta_state == LTWiFiSTAState::CONNECTING && it.reason != WIFI_REASON_NO_AP_FOUND) { + if (it.ssid_len == 0 && this->sta_state_ == static_cast<uint8_t>(LTWiFiSTAState::CONNECTING) && + it.reason != WIFI_REASON_NO_AP_FOUND) { s_ignored_disconnect_count++; if (s_ignored_disconnect_count >= IGNORED_DISCONNECT_THRESHOLD) { ESP_LOGW(TAG, "Too many disconnect events (%u) while connecting, treating as failure (reason=%s)", s_ignored_disconnect_count, get_disconnect_reason_str(it.reason)); - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED); WiFi.disconnect(); this->error_from_callback_ = true; // Don't break - fall through to notify listeners @@ -520,13 +519,13 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, (const char *) it.ssid); - s_sta_state = LTWiFiSTAState::ERROR_NOT_FOUND; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_NOT_FOUND); } else { char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(it.bssid, bssid_s); ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED); } uint8_t reason = it.reason; @@ -551,7 +550,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); WiFi.disconnect(); this->error_from_callback_ = true; - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED); } break; } @@ -559,7 +558,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGV(TAG, "static_ip=%s gateway=%s", network::IPAddress(WiFi.localIP()).str_to(ip_buf), network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); - s_sta_state = LTWiFiSTAState::CONNECTED; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS this->notify_ip_state_listeners_(); #endif @@ -637,7 +636,7 @@ void WiFiComponent::wifi_pre_setup_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use state machine instead of querying WiFi.status() directly // State is updated in main loop from queued events, ensuring thread safety - switch (s_sta_state) { + switch (static_cast<LTWiFiSTAState>(this->sta_state_)) { case LTWiFiSTAState::CONNECTED: return WiFiSTAConnectStatus::CONNECTED; case LTWiFiSTAState::ERROR_NOT_FOUND: @@ -758,7 +757,7 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; bool WiFiComponent::wifi_disconnect_() { // Reset state first so disconnect events aren't ignored // and wifi_sta_connect_status_() returns IDLE instead of CONNECTING - s_sta_state = LTWiFiSTAState::IDLE; + this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::IDLE); return WiFi.disconnect(); } From ceb3cb2ae797611e73f14f3887df812778600ed6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:22:29 -0400 Subject: [PATCH 1106/2056] [haier] Fix hOn half-degree temperature setting (#15312) --- esphome/components/haier/hon_climate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 1cee95bf16b..1e9cb42f382 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -675,7 +675,6 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); } out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0; - control_out_buffer[4] = 0; // This byte should be cleared before setting values out_data->display_status = this->get_display_state() ? 1 : 0; this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); out_data->health_mode = this->get_health_mode() ? 1 : 0; From c64bc2496093dfd0f107e15473adff8437574cc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 07:34:54 -1000 Subject: [PATCH 1107/2056] [preferences] Reduce log verbosity for unchanged NVS/FDB writes (#15332) --- esphome/components/esp32/preferences.cpp | 12 ++++++++---- esphome/components/libretiny/preferences.cpp | 13 +++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index e88ace3e6be..bc0a34ebe86 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -129,11 +129,15 @@ bool ESP32Preferences::sync() { } s_pending_save.clear(); - ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, - failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), - last_key); + ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%s for key=%" PRIu32, + cached + written + failed, cached, written, failed, esp_err_to_name(last_err), last_key); + } else if (written > 0) { + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + } else { + ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 344ca4a8b3e..fba67172949 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -108,16 +108,21 @@ bool LibreTinyPreferences::sync() { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); + ESP_LOGV(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } s_pending_save.clear(); - ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, - failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%" PRIu32, failed, last_err, last_key); + ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%d for key=%" PRIu32, + cached + written + failed, cached, written, failed, last_err, last_key); + } else if (written > 0) { + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + } else { + ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); } return failed == 0; From 9b97e95cf3620dc3aad8715e011ec1b89cdbf112 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 07:42:12 -1000 Subject: [PATCH 1108/2056] [binary_sensor] Add on_multi_click integration test (#15329) --- .../fixtures/multi_click_trigger.yaml | 105 ++++++++++++++++++ tests/integration/test_multi_click_trigger.py | 82 ++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 tests/integration/fixtures/multi_click_trigger.yaml create mode 100644 tests/integration/test_multi_click_trigger.py diff --git a/tests/integration/fixtures/multi_click_trigger.yaml b/tests/integration/fixtures/multi_click_trigger.yaml new file mode 100644 index 00000000000..3bd53d594c9 --- /dev/null +++ b/tests/integration/fixtures/multi_click_trigger.yaml @@ -0,0 +1,105 @@ +esphome: + name: test-multi-click + +host: +api: + batch_delay: 0ms + services: + - service: run_all_tests + then: + # Prime the binary sensor with an initial OFF state. + # trigger_on_initial_state defaults to false, so the first + # state change from unknown won't fire callbacks. + - binary_sensor.template.publish: + id: test_button + state: false + - delay: 50ms + + # Test 1: Single click (ON < 50ms, OFF >= 30ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + # Wait for single click trigger (30ms) + cooldown (100ms) + margin + - delay: 200ms + + # Test 2: Double click (ON < 50ms, OFF < 25ms, ON < 50ms, OFF >= 25ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + - delay: 15ms + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + # Wait for double click trigger (25ms) + cooldown (100ms) + margin + - delay: 200ms + + # Test 3: Long press (ON >= 80ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 100ms + - binary_sensor.template.publish: + id: test_button + state: false + +logger: + level: VERBOSE + +globals: + - id: single_click_count + type: int + initial_value: "0" + - id: double_click_count + type: int + initial_value: "0" + - id: long_press_count + type: int + initial_value: "0" + +binary_sensor: + - platform: template + name: "Test Button" + id: test_button + on_multi_click: + # Single press + - timing: + - ON for at most 50ms + - OFF for at least 30ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(single_click_count) += 1; + ESP_LOGI("multi_click_test", "SINGLE_CLICK count=%d", id(single_click_count)); + + # Double press + - timing: + - ON for at most 50ms + - OFF for at most 25ms + - ON for at most 50ms + - OFF for at least 25ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(double_click_count) += 1; + ESP_LOGI("multi_click_test", "DOUBLE_CLICK count=%d", id(double_click_count)); + + # Long press + - timing: + - ON for at least 80ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(long_press_count) += 1; + ESP_LOGI("multi_click_test", "LONG_PRESS count=%d", id(long_press_count)); diff --git a/tests/integration/test_multi_click_trigger.py b/tests/integration/test_multi_click_trigger.py new file mode 100644 index 00000000000..8a020dd18b3 --- /dev/null +++ b/tests/integration/test_multi_click_trigger.py @@ -0,0 +1,82 @@ +"""Integration test for on_multi_click binary sensor automation. + +Tests that on_multi_click correctly triggers for single click, double click, +and long press patterns using a template binary sensor with timing +orchestrated entirely in YAML. + +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_multi_click_trigger( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that on_multi_click triggers for single, double, and long press patterns.""" + loop = asyncio.get_running_loop() + + single_click_pattern = re.compile(r"SINGLE_CLICK count=(\d+)") + double_click_pattern = re.compile(r"DOUBLE_CLICK count=(\d+)") + long_press_pattern = re.compile(r"LONG_PRESS count=(\d+)") + + single_click_future: asyncio.Future[int] = loop.create_future() + double_click_future: asyncio.Future[int] = loop.create_future() + long_press_future: asyncio.Future[int] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for multi-click trigger messages.""" + if m := single_click_pattern.search(line): + if not single_click_future.done(): + single_click_future.set_result(int(m.group(1))) + elif m := double_click_pattern.search(line): + if not double_click_future.done(): + double_click_future.set_result(int(m.group(1))) + elif (m := long_press_pattern.search(line)) and not long_press_future.done(): + long_press_future.set_result(int(m.group(1))) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + _entities, services = await client.list_entities_services() + + test_service = next((s for s in services if s.name == "run_all_tests"), None) + assert test_service is not None, "run_all_tests service not found" + + # Kick off the entire test sequence (runs in YAML with delays) + await client.execute_service(test_service, {}) + + # Wait for all three triggers + try: + count = await asyncio.wait_for(single_click_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for SINGLE_CLICK - on_multi_click did not trigger." + ) + assert count == 1, f"Expected single click count=1, got {count}" + + try: + count = await asyncio.wait_for(double_click_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for DOUBLE_CLICK - on_multi_click did not trigger." + ) + assert count == 1, f"Expected double click count=1, got {count}" + + try: + count = await asyncio.wait_for(long_press_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for LONG_PRESS - on_multi_click did not trigger." + ) + assert count == 1, f"Expected long press count=1, got {count}" From 2c9a3051d6e90e6a89da2a08d52d93160288c300 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 07:43:18 -1000 Subject: [PATCH 1109/2056] [api] Use memcpy for fixed32 decode on little-endian platforms (#15292) --- esphome/components/api/proto.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 4f5b3f0918f..d9fe0fe461a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -257,7 +257,13 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); return; } - uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); + uint32_t val; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian — direct load on LE platforms + memcpy(&val, ptr, 4); +#else + val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); +#endif if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); } From 2449aa75af91ba01b3b812d5fde43d73eb918d5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 07:45:23 -1000 Subject: [PATCH 1110/2056] [http_request] Fix crash when esp_http_client_init fails (#15328) --- .../http_request/http_request_idf.cpp | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index dda61e24007..30f53eecdc7 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -17,6 +17,7 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; +static constexpr uint32_t ERROR_DURATION_MS = 1000; struct UserData { const std::vector<std::string> &lower_case_collect_headers; @@ -57,7 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c const std::vector<Header> &request_headers, const std::vector<std::string> &lower_case_collect_headers) { if (!network::is_connected()) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); return nullptr; } @@ -74,7 +75,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c } else if (method == "PATCH") { method_idf = HTTP_METHOD_PATCH; } else { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Unsupported method"); return nullptr; } @@ -112,6 +113,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c config.event_handler = http_event_handler; esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == nullptr) { + this->status_momentary_error("failed", ERROR_DURATION_MS); + ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized"); + return nullptr; + } std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client); container->set_parent(this); @@ -129,7 +135,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c esp_err_t err = esp_http_client_open(client, body_len); if (err != ESP_OK) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -151,7 +157,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c } if (err != ESP_OK) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -176,7 +182,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c err = esp_http_client_set_redirection(client); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } @@ -189,7 +195,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c err = esp_http_client_open(client, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } @@ -214,7 +220,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c } ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); return container; } From 26b426bbffd28611d0ff4880b4196c8a0bde4d13 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 31 Mar 2026 14:34:16 -0500 Subject: [PATCH 1111/2056] [zwave_proxy] Clear Home ID on USB modem disconnect (#15327) --- esphome/components/uart/uart_component.h | 4 + esphome/components/usb_uart/usb_uart.h | 1 + .../components/zwave_proxy/zwave_proxy.cpp | 76 +++++++++++++++++-- esphome/components/zwave_proxy/zwave_proxy.h | 6 ++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index abc77fbae82..afd3ad57774 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -85,6 +85,10 @@ class UARTComponent { // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. virtual UARTFlushResult flush() = 0; + // Returns true if the underlying transport is connected and operational. + // Hardware UARTs always return true. USB-backed UARTs override to reflect actual connection state. + virtual bool is_connected() { return true; } + // Sets the maximum time to wait for TX to drain during flush(). // Only meaningful on ESP32 (IDF). Other platforms ignore this value. // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 8a47f0cf4be..8e8e65032d6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -140,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override { return this->input_buffer_.get_available(); } + bool is_connected() override { return this->initialised_.load(); } uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 7653d2b678e..ecb38b25e7f 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -22,6 +22,8 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup +static constexpr uint32_t RECONNECT_DELAY_MS = 500; // Delay between home ID query attempts after reconnect +static constexpr uint8_t MAX_QUERY_RETRIES = 5; // Max attempts to query home ID after reconnect static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { // Calculate Z-Wave frame checksum @@ -38,7 +40,10 @@ ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; } void ZWaveProxy::setup() { this->setup_time_ = App.get_loop_component_start_time(); - this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + this->was_connected_ = this->parent_->is_connected(); + if (this->was_connected_) { + this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + } } float ZWaveProxy::get_setup_priority() const { @@ -84,6 +89,14 @@ void ZWaveProxy::loop() { this->api_connection_ = nullptr; // Unsubscribe if disconnected } + const bool connected = this->parent_->is_connected(); + if (this->was_connected_ != connected) { + this->on_connection_changed_(connected); + } + if (this->reconnect_time_ != 0) { + this->retry_home_id_query_(); + } + this->process_uart_(); this->status_clear_warning(); } @@ -167,6 +180,55 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en } } +void ZWaveProxy::on_connection_changed_(bool connected) { + this->was_connected_ = connected; + if (connected) { + ESP_LOGD(TAG, "Modem reconnected"); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->buffer_index_ = 0; + this->last_response_ = 0; + this->in_bootloader_ = false; + // Defer the query — the modem needs time to initialize after power is applied + this->reconnect_time_ = App.get_loop_component_start_time(); + this->query_retries_ = 0; + } else { + ESP_LOGW(TAG, "Modem disconnected"); + this->clear_home_id_(); + } +} + +void ZWaveProxy::retry_home_id_query_() { + if (this->home_id_ready_) { + // Got the home ID, cancel remaining retries + this->reconnect_time_ = 0; + return; + } + if (App.get_loop_component_start_time() - this->reconnect_time_ <= RECONNECT_DELAY_MS) { + return; // Not yet time for next attempt + } + this->reconnect_time_ = App.get_loop_component_start_time(); // Reset timer for next retry + this->query_retries_++; + if (this->query_retries_ <= MAX_QUERY_RETRIES) { + ESP_LOGD(TAG, "Querying Home ID (attempt %u)", this->query_retries_); + this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + } else { + ESP_LOGW(TAG, "Failed to read Home ID after %u attempts", MAX_QUERY_RETRIES); + this->reconnect_time_ = 0; + } +} + +void ZWaveProxy::clear_home_id_() { + static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {}; + if (this->set_home_id_(ZERO_HOME_ID)) { + this->send_homeid_changed_msg_(); + } + this->home_id_ready_ = false; + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->buffer_index_ = 0; + this->last_response_ = 0; + this->in_bootloader_ = false; +} + bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) { if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { ESP_LOGV(TAG, "Home ID unchanged"); @@ -309,7 +371,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; switch (byte) { case ZWAVE_FRAME_TYPE_START: - ESP_LOGVV(TAG, "Received START"); + ESP_LOGV(TAG, "Received START"); if (this->in_bootloader_) { ESP_LOGD(TAG, "Exited bootloader mode"); this->in_bootloader_ = false; @@ -318,7 +380,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH; return; case ZWAVE_FRAME_TYPE_BL_MENU: - ESP_LOGVV(TAG, "Received BL_MENU"); + ESP_LOGV(TAG, "Received BL_MENU"); if (!this->in_bootloader_) { ESP_LOGD(TAG, "Entered bootloader mode"); this->in_bootloader_ = true; @@ -327,16 +389,16 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU; return; case ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD: - ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD"); + ESP_LOGV(TAG, "Received BL_BEGIN_UPLOAD"); break; case ZWAVE_FRAME_TYPE_ACK: - ESP_LOGVV(TAG, "Received ACK"); + ESP_LOGV(TAG, "Received ACK"); break; case ZWAVE_FRAME_TYPE_NAK: - ESP_LOGW(TAG, "Received NAK"); + ESP_LOGV(TAG, "Received NAK"); break; case ZWAVE_FRAME_TYPE_CAN: - ESP_LOGW(TAG, "Received CAN"); + ESP_LOGV(TAG, "Received CAN"); break; default: ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte); diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 12cb9a90a1e..0b810de29fe 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -65,6 +65,9 @@ class ZWaveProxy : public uart::UARTDevice, public Component { protected: bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. + void clear_home_id_(); // Clear home ID and notify API clients + void on_connection_changed_(bool connected); // Handle modem connect/disconnect transitions + void retry_home_id_query_(); // Retry home ID query after reconnect void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) @@ -80,14 +83,17 @@ class ZWaveProxy : public uart::UARTDevice, public Component { // Pointers and 32-bit values (aligned together) api::APIConnection *api_connection_{nullptr}; // Current subscribed client uint32_t setup_time_{0}; // Time when setup() was called + uint32_t reconnect_time_{0}; // Timestamp of reconnect detection (0 = no pending query) // Small values (grouped by size to minimize padding) uint16_t buffer_index_{0}; // Index for populating the data buffer uint16_t end_frame_after_{0}; // Payload reception ends after this index uint8_t last_response_{0}; // Last response type sent + uint8_t query_retries_{0}; // Number of home ID query attempts after reconnect ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module + bool was_connected_{false}; // Previous UART connection state for edge detection }; extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From da6c4e20fef3f92dfc25ecd5f34df76d2c8baf1a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:29:57 +1000 Subject: [PATCH 1112/2056] [lvgl] Fixes #2 (#15161) --- esphome/components/lvgl/automation.py | 4 +- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/lvcode.py | 5 -- esphome/components/lvgl/lvgl_esphome.cpp | 26 ++++---- esphome/components/lvgl/number/__init__.py | 4 +- esphome/components/lvgl/schemas.py | 67 ++++++++++++++++----- esphome/components/lvgl/switch/__init__.py | 4 +- esphome/components/lvgl/text/__init__.py | 4 +- esphome/components/lvgl/trigger.py | 3 + esphome/components/lvgl/widgets/meter.py | 63 ++++++++++--------- esphome/components/lvgl/widgets/tileview.py | 2 +- tests/components/lvgl/lvgl-package.yaml | 34 ++++++++++- 12 files changed, 145 insertions(+), 72 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 24579e5be80..50e6db74b8f 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -136,7 +136,7 @@ async def update_to_code(config, action_id, template_arg, args): widget.type.w_type.value_property is not None and widget.type.w_type.value_property in config ): - lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr) widgets = await get_widgets(config[CONF_ID]) return await action_to_code( @@ -455,6 +455,6 @@ async def obj_refresh_to_code(config, action_id, template_arg, args): widget.type.w_type.value_property is not None and widget.type.w_type.value_property in config ): - lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr) return await action_to_code(widget, do_refresh, action_id, template_arg, args) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 72345ca98e1..de5835d7a69 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -541,6 +541,7 @@ CONF_END_ANGLE = "end_angle" CONF_END_VALUE = "end_value" CONF_ENTER_BUTTON = "enter_button" CONF_ENTRIES = "entries" +CONF_EXT_CLICK_AREA = "ext_click_area" CONF_FLAGS = "flags" CONF_FLEX_FLOW = "flex_flow" CONF_FLEX_ALIGN_MAIN = "flex_align_main" diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 146b261f260..eb8f7d4437c 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -253,14 +253,10 @@ class MockLv: A mock object that can be used to generate LVGL calls. """ - # Mapping for LVGL 9 - ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"} - def __init__(self, base): self.base = base def __getattr__(self, attr: str) -> "MockLv": - attr = MockLv.ATTR_MAP.get(attr, attr) return MockLv(f"{self.base}{attr}") def append(self, expression): @@ -314,7 +310,6 @@ class ReturnStatement(ExpressionStatement): class LvExpr(MockLv): def __getattr__(self, attr: str) -> "MockLv": - attr = MockLv.ATTR_MAP.get(attr, attr) return LvExpr(f"{self.base}{attr}") def append(self, expression): diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index bf86a4e9ee0..a5075cb6148 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -343,26 +343,26 @@ void IndicatorLine::set_value(int value) { } void IndicatorLine::update_length_() { - uint32_t actual_needle_length; - auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto cx = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto cy = lv_obj_get_height(lv_obj_get_parent(this->obj)) / 2; + auto radius = clamp_at_most(cx, cy); auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN); auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN); if (LV_COORD_IS_PCT(radial_offset)) { radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100; } if (LV_COORD_IS_PCT(length)) { - actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100; + length = radius * LV_COORD_GET_PCT(length) / 100; } else if (length < 0) { - actual_needle_length = radius + length; - } else { - actual_needle_length = length; + length += radius; } auto x = lv_trigo_cos(this->angle_) / 32768.0f; auto y = lv_trigo_sin(this->angle_) / 32768.0f; + // radius here also represents the offset of the scale center from top left this->points_[0].x = radius + radial_offset * x; this->points_[0].y = radius + radial_offset * y; - this->points_[1].x = x * actual_needle_length + radius; - this->points_[1].y = y * actual_needle_length + radius; + this->points_[1].x = radius + x * (radial_offset + length); + this->points_[1].y = radius + y * (radial_offset + length); lv_obj_refresh_self_size(this->obj); lv_obj_invalidate(this->obj); } @@ -682,15 +682,15 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task)); int tick = line_dsc->base.id2; if (tick >= range_start && tick <= range_end) { - unsigned range = range_end - range_start; + int ratio; if (local) { + int range = range_end - range_start; tick -= range_start; + ratio = range == 0 ? 0 : (tick * 255) / range; } else { - range = lv_scale_get_total_tick_count(scale) - 1; + // total tick count is guaranteed to be at least 2. + ratio = (line_dsc->base.id1 * 255) / (lv_scale_get_total_tick_count(scale) - 1); } - if (range == 0) - range = 1; - auto ratio = (tick * 255) / range; line_dsc->color = lv_color_mix(color_end, color_start, ratio); line_dsc->width += width; } diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index c48e051eac3..d80e93708bf 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -12,7 +12,7 @@ from ..lvcode import ( UPDATE_EVENT, LambdaContext, ReturnStatement, - lv, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LvNumber, lvgl_ns @@ -40,7 +40,7 @@ async def to_code(config): await widget.set_property( "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) event_code = ( LV_EVENT.VALUE_CHANGED if not config[CONF_UPDATE_ON_RELEASE] diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index bcbb193ce34..9c9504f05f1 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -146,26 +146,41 @@ def point_schema(value): # All LVGL styles and their validators -STYLE_PROPS = { +BASE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, - "arc_opa": lvalid.opacity, + "anim_duration": lvalid.lv_milliseconds, "arc_color": lvalid.lv_color, + "arc_opa": lvalid.opacity, "arc_rounded": lvalid.lv_bool, "arc_width": lvalid.pixels, - "anim_time": lvalid.lv_milliseconds, + "base_dir": df.LvConstant("LV_BASE_DIR_", "LTR", "RTL", "AUTO").one_of, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, "bg_grad_color": lvalid.lv_color, - "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, "bg_grad_dir": LV_GRAD_DIR.one_of, + "bg_grad_opa": lvalid.opacity, "bg_grad_stop": lvalid.stop_value, "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, "bg_image_src": lvalid.lv_image, "bg_image_tiled": lvalid.lv_bool, + "bg_main_opa": lvalid.opacity, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, + "blend_mode": df.LvConstant( + "LV_BLEND_MODE_", + "NORMAL", + "ADDITIVE", + "SUBTRACTIVE", + "MULTIPLY", + "DIFFERENCE", + ).one_of, + "blur_backdrop": lvalid.lv_bool, + "blur_quality": df.LvConstant( + "LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION" + ).one_of, + "blur_radius": lvalid.lv_positive_int, "border_color": lvalid.lv_color, "border_opa": lvalid.opacity, "border_post": lvalid.lv_bool, @@ -175,33 +190,53 @@ STYLE_PROPS = { "border_width": lvalid.lv_positive_int, "clip_corner": lvalid.lv_bool, "color_filter_opa": lvalid.opacity, + "drop_shadow_color": lvalid.lv_color, + "drop_shadow_offset_x": lvalid.lv_int, + "drop_shadow_offset_y": lvalid.lv_int, + "drop_shadow_opa": lvalid.opacity, + "drop_shadow_quality": df.LvConstant( + "LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION" + ).one_of, + "drop_shadow_radius": lvalid.lv_positive_int, "height": lvalid.size, + "image_opa": lvalid.opacity, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, + "length": lvalid.pixels_or_percent, "line_color": lvalid.lv_color, "line_dash_gap": lvalid.lv_positive_int, "line_dash_width": lvalid.lv_positive_int, "line_opa": lvalid.opacity, "line_rounded": lvalid.lv_bool, "line_width": lvalid.lv_positive_int, + "margin_bottom": lvalid.padding, + "margin_left": lvalid.padding, + "margin_right": lvalid.padding, + "margin_top": lvalid.padding, + "max_height": lvalid.pixels_or_percent, + "max_width": lvalid.pixels_or_percent, + "min_height": lvalid.pixels_or_percent, + "min_width": lvalid.pixels_or_percent, "opa": lvalid.opacity, "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, "outline_pad": lvalid.padding, "outline_width": lvalid.pixels, - "length": lvalid.pixels_or_percent, "pad_all": lvalid.padding, "pad_bottom": lvalid.padding, "pad_left": lvalid.padding, + "pad_radial": lvalid.padding, "pad_right": lvalid.padding, "pad_top": lvalid.padding, "radial_offset": lvalid.size, + "radius": lvalid.lv_fraction, + "recolor": lvalid.lv_color, + "recolor_opa": lvalid.opacity, + "rotary_sensitivity": lvalid.lv_positive_int, "shadow_color": lvalid.lv_color, "shadow_offset_x": lvalid.lv_int, "shadow_offset_y": lvalid.lv_int, - "shadow_ofs_x": lvalid.lv_int, - "shadow_ofs_y": lvalid.lv_int, "shadow_opa": lvalid.opacity, "shadow_spread": lvalid.lv_int, "shadow_width": lvalid.lv_positive_int, @@ -216,7 +251,9 @@ STYLE_PROPS = { "text_letter_space": lvalid.lv_positive_int, "text_line_space": lvalid.lv_positive_int, "text_opa": lvalid.opacity, - "transform_angle": lvalid.lv_angle, + "text_outline_stroke_color": lvalid.lv_color, + "text_outline_stroke_opa": lvalid.opacity, + "text_outline_stroke_width": lvalid.lv_positive_int, "transform_height": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent, @@ -226,20 +263,17 @@ STYLE_PROPS = { "transform_scale_y": lvalid.scale, "transform_skew_x": lvalid.lv_angle, "transform_skew_y": lvalid.lv_angle, - "transform_zoom": lvalid.scale, + "transform_width": lvalid.pixels_or_percent, + "translate_radial": lvalid.lv_int, "translate_x": lvalid.pixels_or_percent, "translate_y": lvalid.pixels_or_percent, - "max_height": lvalid.pixels_or_percent, - "max_width": lvalid.pixels_or_percent, - "min_height": lvalid.pixels_or_percent, - "min_width": lvalid.pixels_or_percent, - "radius": lvalid.lv_fraction, "width": lvalid.size, "x": lvalid.pixels_or_percent, "y": lvalid.pixels_or_percent, } STYLE_REMAP = { + "anim_time": "anim_duration", "transform_angle": "transform_rotation", "transform_zoom": "transform_scale", "zoom": "scale", @@ -249,6 +283,10 @@ STYLE_REMAP = { "r_mod": "length", } +STYLE_PROPS = BASE_PROPS | { + p: BASE_PROPS[v] for p, v in STYLE_REMAP.items() if v in BASE_PROPS +} + def remap_property(prop, record=True): """ @@ -394,6 +432,7 @@ def obj_schema(widget_type: WidgetType): return ( part_schema(widget_type.parts) .extend(ALIGN_TO_SCHEMA) + .extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels}) .extend(automation_schema(widget_type.w_type)) .extend( { diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 6d10a70d853..a43851b4a39 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -13,8 +13,8 @@ from ..lvcode import ( LambdaContext, LvConditional, LvContext, - lv, lv_add, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns @@ -39,7 +39,7 @@ async def to_code(config): widget.add_state(LV_STATE.CHECKED) cond.else_() widget.clear_state(LV_STATE.CHECKED) - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) control.add(switch_id.publish_state(v)) switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda()) await cg.register_component(switch, config) diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index eb56cdb7a77..190ecacda52 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -10,8 +10,8 @@ from ..lvcode import ( UPDATE_EVENT, LambdaContext, LvContext, - lv, lv_add, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LvText, lvgl_ns @@ -33,7 +33,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext([(cg.std_string, "text_value")]) as control: await widget.set_property("text", "text_value.c_str()") - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) control.add(textvar.publish_state(widget.get_value())) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 54309cdf891..c52d213e15a 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -15,6 +15,7 @@ from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, CONF_ALIGN_TO_LAMBDA_ID, + CONF_EXT_CLICK_AREA, DIRECTIONS, LV_EVENT_MAP, LV_EVENT_TRIGGERS, @@ -113,6 +114,8 @@ async def generate_align_tos(config: dict): x = align_to[CONF_X] y = align_to[CONF_Y] lv.obj_align_to(w.obj, target, align, x, y) + if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA): + lv.obj_set_ext_click_area(w.obj, ext_click_area) action_id = config[CONF_ALIGN_TO_LAMBDA_ID] var = new_Pvariable(action_id, await context.get_lambda()) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 494f811a8eb..ab65a7c47d5 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -56,11 +56,11 @@ from ..lv_validation import ( lv_float, lv_image, lv_int, + lv_positive_int, opacity, padding, pixels, pixels_or_percent, - pixels_or_percent_validator, requires_component, size, ) @@ -88,7 +88,10 @@ CONF_COLOR_START = "color_start" CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top" CONF_IMAGE_ID = "image_id" CONF_INDICATORS = "indicators" +CONF_DASH_GAP = "dash_gap" +CONF_DASH_WIDTH = "dash_width" CONF_LINE_ID = "line_id" +CONF_ROUNDED = "rounded" CONF_LABEL_GAP = "label_gap" CONF_MAJOR = "major" CONF_METER = "meter" @@ -135,9 +138,12 @@ INDICATOR_LINE_SCHEMA = cv.Schema( { cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_ROUNDED, default=True): lv_bool, + cv.Optional(CONF_DASH_GAP): lv_positive_int, + cv.Optional(CONF_DASH_WIDTH): lv_positive_int, cv.Optional(CONF_R_MOD): padding, - cv.Optional(CONF_LENGTH): pixels_or_percent_validator, - cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator, + cv.Optional(CONF_LENGTH): pixels_or_percent, + cv.Optional(CONF_RADIAL_OFFSET): pixels_or_percent, cv.Optional(CONF_VALUE, default=0.0): lv_float, cv.Optional(CONF_OPA, default=1.0): opacity, } @@ -249,17 +255,17 @@ SCALE_SCHEMA = cv.Schema( { cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2), cv.Optional(CONF_WIDTH, default=2): cv.positive_int, - cv.Optional(CONF_LENGTH, default=10): size, - cv.Optional(CONF_RADIAL_OFFSET, default=0): size, + cv.Optional(CONF_LENGTH, default=10): cv.positive_int, + cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int, cv.Optional(CONF_COLOR, default=0x808080): lv_color, cv.Optional(CONF_MAJOR): cv.Schema( { cv.Optional(CONF_STRIDE, default=3): cv.positive_int, cv.Optional(CONF_WIDTH, default=5): size, - cv.Optional(CONF_LENGTH, default="15%"): size, - cv.Optional(CONF_RADIAL_OFFSET, default=0): size, + cv.Optional(CONF_LENGTH, default=12): cv.positive_int, + cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_LABEL_GAP, default=4): size, + cv.Optional(CONF_LABEL_GAP, default=4): cv.int_, } ), } @@ -466,11 +472,15 @@ class MeterType(WidgetType): CONF_OPA: v[CONF_OPA], CONF_LINE_WIDTH: v[CONF_WIDTH], "line_color": v[CONF_COLOR], - "line_rounded": True, + "line_rounded": v[CONF_ROUNDED], CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT, CONF_LENGTH: length, - CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET], } + if radial_offset := v.get(CONF_RADIAL_OFFSET): + props[CONF_RADIAL_OFFSET] = radial_offset + for option in (CONF_DASH_WIDTH, CONF_DASH_GAP): + if option in v: + props["line_" + option] = v[option] lw = await widget_to_code(props, line_indicator_type, scale_var) await set_indicator_values(lw, v) @@ -478,10 +488,8 @@ class MeterType(WidgetType): add_lv_use(CONF_IMAGE) src = v[CONF_SRC] src_data = get_image_metadata(src.id) - pivot_x = await pixels.process(v[CONF_PIVOT_X]) - pivot_y = await pixels.process( - v.get(CONF_PIVOT_Y, src_data.height // 2) - ) + pivot_x = v[CONF_PIVOT_X] + pivot_y = v.get(CONF_PIVOT_Y, src_data.height // 2) props = { CONF_X: src_data.width // 2 - pivot_x, "transform_pivot_x": pivot_x, @@ -511,11 +519,12 @@ class MeterType(WidgetType): lv_obj.set_style_line_width( scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS ) - lv_obj.set_style_radial_offset( - scale_var, - await size.process(ticks[CONF_RADIAL_OFFSET]), - LV_PART.ITEMS, - ) + if radial_offset := ticks.get(CONF_RADIAL_OFFSET): + lv_obj.set_style_radial_offset( + scale_var, + -radial_offset, + LV_PART.ITEMS, + ) lv_obj.set_style_line_color( scale_var, await lv_color.process(ticks[CONF_COLOR]), @@ -536,11 +545,12 @@ class MeterType(WidgetType): await size.process(major[CONF_LENGTH]), LV_PART.INDICATOR, ) - lv_obj.set_style_radial_offset( - scale_var, - await size.process(ticks[CONF_RADIAL_OFFSET]), - LV_PART.INDICATOR, - ) + if radial_offset := major.get(CONF_RADIAL_OFFSET): + lv_obj.set_style_radial_offset( + scale_var, + -radial_offset, + LV_PART.INDICATOR, + ) lv_obj.set_style_line_width( scale_var, await size.process(major[CONF_WIDTH]), @@ -553,12 +563,9 @@ class MeterType(WidgetType): ) # Set label gap (padding) - label_gap = await size.process(major[CONF_LABEL_GAP]) - if isinstance(label_gap, int): - label_gap -= DEFAULT_LABEL_GAP lv_obj.set_style_pad_radial( scale_var, - label_gap, + major[CONF_LABEL_GAP] - DEFAULT_LABEL_GAP, LV_PART.INDICATOR, ) else: diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 8e9d95f3495..4657d628de6 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -129,6 +129,6 @@ async def tileview_select(config, action_id, template_arg, args): lv.tileview_set_tile_by_index( widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) ) - lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + lv_obj.send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) return await action_to_code(widgets, do_select, action_id, template_arg, args) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 821476a72b8..b8c9a1809ea 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -232,7 +232,7 @@ lvgl: - roller: id: lv_roller visible_row_count: 2 - anim_time: 500ms + anim_duration: 500ms options: - Nov - Dec @@ -317,20 +317,27 @@ lvgl: align: top_left - container: align: center + anim_duration: 1s arc_opa: COVER arc_color: 0xFF0000 arc_rounded: false arc_width: 3 - anim_time: 1s + base_dir: auto bg_color: light_blue bg_grad_color: light_blue bg_grad_dir: hor + bg_grad_opa: cover bg_grad_stop: 128 bg_image_opa: transp bg_image_recolor: light_blue bg_image_recolor_opa: 50% + bg_main_opa: cover bg_main_stop: 0 bg_opa: 20% + blend_mode: normal + blur_backdrop: false + blur_quality: auto + blur_radius: 0 border_color: 0x00FF00 border_opa: cover border_post: true @@ -338,7 +345,15 @@ lvgl: border_width: 4 clip_corner: false color_filter_opa: transp + drop_shadow_color: 0x000000 + drop_shadow_offset_x: 5 + drop_shadow_offset_y: 5 + drop_shadow_opa: cover + drop_shadow_quality: precision + drop_shadow_radius: 10 + ext_click_area: 100px height: 50% + image_opa: cover image_recolor: light_blue image_recolor_opa: cover line_width: 10 @@ -346,6 +361,10 @@ lvgl: line_dash_gap: 10 line_rounded: false line_color: light_blue + margin_bottom: 4 + margin_left: 4 + margin_right: 4 + margin_top: 4 opa: cover opa_layered: cover outline_color: light_blue @@ -355,8 +374,12 @@ lvgl: pad_all: 10px pad_bottom: 10px pad_left: 10px + pad_radial: 0 pad_right: 10px pad_top: 10px + recolor: 0xFF0000 + recolor_opa: transp + rotary_sensitivity: 256 shadow_color: light_blue shadow_opa: cover shadow_spread: 5 @@ -368,6 +391,9 @@ lvgl: text_letter_space: 4 text_line_space: 4 text_opa: cover + text_outline_stroke_color: 0x000000 + text_outline_stroke_opa: cover + text_outline_stroke_width: 2 transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% @@ -377,8 +403,10 @@ lvgl: transform_scale_y: 0.8 transform_skew_x: 10 transform_skew_y: 20 + transform_width: 100 shadow_offset_x: 3 shadow_offset_y: 3 + translate_radial: 0 translate_x: 10 translate_y: 10 max_height: 100 @@ -1053,7 +1081,7 @@ lvgl: - ticks: width: 1 count: 61 - length: 20% + length: 20 radial_offset: 5 color: 0xFFFFFF major: From 2cb987095da9ca82aaa6f4412184f8cda9fc8090 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston <bonne@exciton.com.au> Date: Tue, 31 Mar 2026 13:48:16 -0700 Subject: [PATCH 1113/2056] [modbus] Share helper functions across modbus components - part B (#14172) Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/modbus/modbus_helpers.cpp | 139 ++++++++++++++++++ esphome/components/modbus/modbus_helpers.h | 101 +++++++++++++ .../binary_sensor/modbus_binarysensor.cpp | 4 +- .../modbus_controller/modbus_controller.cpp | 134 +---------------- .../modbus_controller/modbus_controller.h | 109 ++++---------- .../number/modbus_number.cpp | 2 +- .../output/modbus_output.cpp | 2 +- .../select/modbus_select.cpp | 4 +- .../switch/modbus_switch.cpp | 4 +- 9 files changed, 277 insertions(+), 222 deletions(-) create mode 100644 esphome/components/modbus/modbus_helpers.cpp diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp new file mode 100644 index 00000000000..77190b28463 --- /dev/null +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -0,0 +1,139 @@ +#include "modbus_helpers.h" +#include "esphome/core/log.h" + +namespace esphome::modbus::helpers { + +static const char *const TAG = "modbus_helpers"; + +void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) { + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::FP32: + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + data.push_back((value & 0xFFFF000000000000) >> 48); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF000000000000) >> 48); + break; + default: + ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type)); + break; + } +} + +int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits + + if (offset > data.size()) { + ESP_LOGE(TAG, "not enough data for value"); + return value; + } + + size_t size = data.size() - offset; + bool error = false; + switch (sensor_value_type) { + case SensorValueType::U_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), + bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::U_DWORD: + case SensorValueType::FP32: + if (size >= 4) { + value = get_data<uint32_t>(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::FP32_R: + if (size >= 4) { + value = get_data<uint32_t>(data, offset); + value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), + bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::S_DWORD: + if (size >= 4) { + value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_DWORD_R: { + if (size >= 4) { + value = get_data<uint32_t>(data, offset); + // Currently the high word is at the low position + // the sign bit is therefore at low before the switch + uint32_t sign_bit = (value & 0x8000) << 16; + value = mask_and_shift_by_rightbit( + static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); + } else { + error = true; + } + } break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + // Ignore bitmask for QWORD + if (size >= 8) { + value = get_data<uint64_t>(data, offset); + } else { + error = true; + } + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: { + // Ignore bitmask for QWORD + if (size >= 8) { + uint64_t tmp = get_data<uint64_t>(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); + } else { + error = true; + } + } break; + case SensorValueType::RAW: + default: + break; + } + if (error) + ESP_LOGE(TAG, "not enough data for value"); + return value; +} +} // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index 9f78de1c21a..84897bcad35 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -1,6 +1,8 @@ #pragma once #include <string> +#include <vector> +#include <cmath> #include "esphome/core/helpers.h" #include "esphome/components/modbus/modbus_definitions.h" @@ -103,4 +105,103 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); } +// Extract data from modbus response buffer +/** Extract data from modbus response buffer + * @param T one of supported integer data types int_8,int_16,int_32,int_64 + * @param data modbus response buffer (uint8_t) + * @param buffer_offset offset in bytes. + * @return value of type T extracted from buffer + */ +template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) { + if (sizeof(T) == sizeof(uint8_t)) { + return T(data[buffer_offset]); + } + if (sizeof(T) == sizeof(uint16_t)) { + return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); + } + + if (sizeof(T) == sizeof(uint32_t)) { + return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 | + static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2)); + } + + if (sizeof(T) == sizeof(uint64_t)) { + return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 | + (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4))); + } + + static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) || + sizeof(T) == sizeof(uint64_t), + "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported."); + + return T{}; +} + +/** Extract coil data from modbus response buffer + * Responses for coil are packed into bytes . + * coil 3 is bit 3 of the first response byte + * coil 9 is bit 2 of the second response byte + * @param coil number of the cil + * @param data modbus response buffer (uint8_t) + * @return content of coil register + */ +inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) { + auto data_byte = coil / 8; + return (data[data_byte] & (1 << (coil % 8))) > 0; +} + +/** Extract bits from value and shift right according to the bitmask + * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. + * the result is then shifted right by the position if the first right set bit in the mask + * Useful for modbus data where more than one value is packed in a 16 bit register + * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as + * D15 - D8 = hour, D7 - D0 = minute + * To get the hours use mask 0xFF00 and 0x00FF for the minute + * @param data an integral value between 16 aand 32 bits, + * @param bitmask the bitmask to apply + */ +template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) { + auto result = (mask & data); + if (result == 0 || mask == 0xFFFFFFFF) { + return result; + } + for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { + if (pos < 32 && (mask & (1UL << pos)) != 0) + return result >> pos; + } + return 0; +} + +/** Convert float value to vector<uint16_t> suitable for sending + * @param data target for payload + * @param value float value to convert + * @param value_type defines if 16/32 or FP32 is used + * @return vector containing the modbus register words in correct order + */ +void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type); + +/** Convert vector<uint8_t> response payload to number. + * @param data payload with the data to convert + * @param sensor_value_type defines if 16/32/64 bits or FP32 is used + * @param offset offset to the data in data + * @param bitmask bitmask used for masking and shifting + * @return 64-bit number of the payload + */ +int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); + +inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) { + int64_t val; + + if (value_type_is_float(value_type)) { + val = bit_cast<uint32_t>(value); + } else { + val = llroundf(value); + } + + std::vector<uint16_t> data; + number_to_payload(data, val, value_type); + return data; +} + } // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp index c3eb3d4411e..1ea3041b4d5 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -15,10 +15,10 @@ void ModbusBinarySensor::parse_and_publish(const std::vector<uint8_t> &data) { case ModbusRegisterType::DISCRETE_INPUT: case ModbusRegisterType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = coil_from_vector(this->offset, data); + value = modbus::helpers::coil_from_vector(this->offset, data); break; default: - value = get_data<uint16_t>(data, this->offset) & this->bitmask; + value = modbus::helpers::get_data<uint16_t>(data, this->offset) & this->bitmask; break; } // Is there a lambda registered diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 38eaea2d1c6..3c4ceaf62de 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -140,7 +140,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t std::vector<uint16_t> payload; payload.reserve(server_register->register_count * 2); - number_to_payload(payload, value, server_register->value_type); + modbus::helpers::number_to_payload(payload, value, server_register->value_type); sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); current_address += server_register->register_count; found = true; @@ -258,7 +258,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st // Actually write to the registers: if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { - int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); + int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); return server_register->write_lambda(number); })) { this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); @@ -517,7 +517,8 @@ void ModbusController::loop() { void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) { - ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data<uint16_t>(data, 0), get_data<int16_t>(data, 1)); + ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data<uint16_t>(data, 0), + modbus::helpers::get_data<int16_t>(data, 1)); } void ModbusController::dump_sensors_() { @@ -710,132 +711,5 @@ bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { other.register_type == this->register_type && other.function_code == this->function_code; } -void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) { - switch (value_type) { - case SensorValueType::U_WORD: - case SensorValueType::S_WORD: - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD: - case SensorValueType::S_DWORD: - case SensorValueType::FP32: - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD_R: - case SensorValueType::S_DWORD_R: - case SensorValueType::FP32_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - break; - case SensorValueType::U_QWORD: - case SensorValueType::S_QWORD: - data.push_back((value & 0xFFFF000000000000) >> 48); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF000000000000) >> 48); - break; - default: - ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d", - static_cast<uint16_t>(value_type)); - break; - } -} - -int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask) { - int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits - - size_t size = data.size() - offset; - bool error = false; - switch (sensor_value_type) { - case SensorValueType::U_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ; - } else { - error = true; - } - break; - case SensorValueType::U_DWORD: - case SensorValueType::FP32: - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } - break; - case SensorValueType::U_DWORD_R: - case SensorValueType::FP32_R: - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } - break; - case SensorValueType::S_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), - bitmask); // default is 0xFFFF ; - } else { - error = true; - } - break; - case SensorValueType::S_DWORD: - if (size >= 4) { - value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask); - } else { - error = true; - } - break; - case SensorValueType::S_DWORD_R: { - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - // Currently the high word is at the low position - // the sign bit is therefore at low before the switch - uint32_t sign_bit = (value & 0x8000) << 16; - value = mask_and_shift_by_rightbit( - static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); - } else { - error = true; - } - } break; - case SensorValueType::U_QWORD: - case SensorValueType::S_QWORD: - // Ignore bitmask for QWORD - if (size >= 8) { - value = get_data<uint64_t>(data, offset); - } else { - error = true; - } - break; - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD_R: { - // Ignore bitmask for QWORD - if (size >= 8) { - uint64_t tmp = get_data<uint64_t>(data, offset); - value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); - } else { - error = true; - } - } break; - case SensorValueType::RAW: - default: - break; - } - if (error) - ESP_LOGE(TAG, "not enough data for value"); - return value; -} - } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 438eb12c2a9..6c6c748b73f 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -59,83 +59,38 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { return modbus::helpers::qword_from_hex_str(value, pos); } -// Extract data from modbus response buffer -/** Extract data from modbus response buffer - * @param T one of supported integer data types int_8,int_16,int_32,int_64 - * @param data modbus response buffer (uint8_t) - * @param buffer_offset offset in bytes. - * @return value of type T extracted from buffer - */ -template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) { - if (sizeof(T) == sizeof(uint8_t)) { - return T(data[buffer_offset]); - } - if (sizeof(T) == sizeof(uint16_t)) { - return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); - } - - if (sizeof(T) == sizeof(uint32_t)) { - return get_data<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2)); - } - - if (sizeof(T) == sizeof(uint64_t)) { - return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 | - (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4))); - } +template<typename T> +ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0") +T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) { + return modbus::helpers::get_data<T>(data, buffer_offset); } -/** Extract coil data from modbus response buffer - * Responses for coil are packed into bytes . - * coil 3 is bit 3 of the first response byte - * coil 9 is bit 2 of the second response byte - * @param coil number of the cil - * @param data modbus response buffer (uint8_t) - * @return content of coil register - */ +ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0") inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) { - auto data_byte = coil / 8; - return (data[data_byte] & (1 << (coil % 8))) > 0; + return modbus::helpers::coil_from_vector(coil, data); } -/** Extract bits from value and shift right according to the bitmask - * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. - * the result is then shifted right by the position if the first right set bit in the mask - * Useful for modbus data where more than one value is packed in a 16 bit register - * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as - * D15 - D8 = hour, D7 - D0 = minute - * To get the hours use mask 0xFF00 and 0x00FF for the minute - * @param data an integral value between 16 aand 32 bits, - * @param bitmask the bitmask to apply - */ -template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) { - auto result = (mask & data); - if (result == 0 || mask == 0xFFFFFFFF) { - return result; - } - for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { - if ((mask & (1UL << pos)) != 0) - return result >> pos; - } - return 0; +template<typename N> +ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0") +N mask_and_shift_by_rightbit(N data, uint32_t mask) { + return modbus::helpers::mask_and_shift_by_rightbit(data, mask); } -/** Convert float value to vector<uint16_t> suitable for sending - * @param data target for payload - * @param value float value to convert - * @param value_type defines if 16/32 or FP32 is used - * @return vector containing the modbus register words in correct order - */ -void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type); +ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0") +inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) { + modbus::helpers::number_to_payload(data, value, value_type); +} -/** Convert vector<uint8_t> response payload to number. - * @param data payload with the data to convert - * @param sensor_value_type defines if 16/32/64 bits or FP32 is used - * @param offset offset to the data in data - * @param bitmask bitmask used for masking and shifting - * @return 64-bit number of the payload - */ -int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask); +ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0") +inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask); +} + +ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0") +inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) { + return modbus::helpers::float_to_payload(value, value_type); +} class ModbusController; @@ -517,7 +472,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { * @return float value of data */ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) { - int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); + int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; if (modbus::helpers::value_type_is_float(item.sensor_value_type)) { @@ -529,19 +484,5 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem return float_value; } -inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) { - int64_t val; - - if (modbus::helpers::value_type_is_float(value_type)) { - val = bit_cast<uint32_t>(value); - } else { - val = llroundf(value); - } - - std::vector<uint16_t> data; - number_to_payload(data, val, value_type); - return data; -} - } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index 4a3ec1fc41a..ed5d91ec5b6 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -62,7 +62,7 @@ void ModbusNumber::control(float value) { this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); }); } else { - data = float_to_payload(write_value, this->sensor_value_type); + data = modbus::helpers::float_to_payload(write_value, this->sensor_value_type); ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index f02d9397ca1..e7f1a39716e 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -34,7 +34,7 @@ void ModbusFloatOutput::write_state(float value) { } // lambda didn't set payload if (data.empty()) { - data = float_to_payload(value, this->sensor_value_type); + data = modbus::helpers::float_to_payload(value, this->sensor_value_type); } ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index e2a54d3f602..2cff7e89ee8 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "modbus_controller.select"; void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); } void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) { - int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); + int64_t value = modbus::helpers::payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); ESP_LOGD(TAG, "New select value %lld from payload", value); @@ -61,7 +61,7 @@ void ModbusSelect::control(size_t index) { } if (data.empty()) { - number_to_payload(data, *mapval, this->sensor_value_type); + modbus::helpers::number_to_payload(data, *mapval, this->sensor_value_type); } else { ESP_LOGV(TAG, "Using payload from write lambda"); } diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index 68aa37c9ed8..dbaff04cc69 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -33,10 +33,10 @@ void ModbusSwitch::parse_and_publish(const std::vector<uint8_t> &data) { case ModbusRegisterType::DISCRETE_INPUT: case ModbusRegisterType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = coil_from_vector(this->offset, data); + value = modbus::helpers::coil_from_vector(this->offset, data); break; default: - value = get_data<uint16_t>(data, this->offset) & this->bitmask; + value = modbus::helpers::get_data<uint16_t>(data, this->offset) & this->bitmask; break; } From 64e836f9c8da7cb68ade3cb430f9dbb7b4cecc9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:49:17 -1000 Subject: [PATCH 1114/2056] Bump CodSpeedHQ/action from 4.12.1 to 4.13.0 (#15340) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab7a750388c..71703652e81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4 + uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation From 2064eef273c878191cd29799329c049693fa60d4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:53:12 -0400 Subject: [PATCH 1115/2056] [esp32_hosted] Guard against empty firmware URL in perform() (#15338) --- .../components/esp32_hosted/update/esp32_hosted_update.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index dcd6e643c2e..af35d328881 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -448,6 +448,13 @@ void Esp32HostedUpdate::perform(bool force) { return; } +#ifdef USE_ESP32_HOSTED_HTTP_UPDATE + if (this->firmware_url_.empty()) { + ESP_LOGW(TAG, "No firmware URL available, run check first"); + return; + } +#endif + update::UpdateState prev_state = this->state_; this->state_ = update::UPDATE_STATE_INSTALLING; this->update_info_.has_progress = false; From 66b6d36a260dd1900c1786f9edc98c47157c8255 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:04:10 +1000 Subject: [PATCH 1116/2056] [lvgl] Fixes #3 (#15304) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 10 ++----- esphome/components/lvgl/defines.py | 4 --- esphome/components/lvgl/styles.py | 31 +++------------------ esphome/components/lvgl/widgets/__init__.py | 7 +++-- esphome/components/lvgl/widgets/canvas.py | 2 -- esphome/components/lvgl/widgets/keyboard.py | 26 +++++++++++++---- esphome/components/lvgl/widgets/label.py | 2 +- esphome/components/lvgl/widgets/line.py | 19 ++++++------- esphome/components/lvgl/widgets/msgbox.py | 8 ++++-- esphome/components/lvgl/widgets/qrcode.py | 3 +- esphome/components/lvgl/widgets/tabview.py | 4 +-- 11 files changed, 48 insertions(+), 68 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 6377183ef4f..736fba759f3 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -380,7 +380,8 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - lv_image_formats = df.get_color_formats().copy() + # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending + lv_image_formats = {"RGB565", "ARGB8888"} if { "transform_rotation", "transform_scale", @@ -388,10 +389,6 @@ async def to_code(configs): "transform_scale_y", } & styles_used: df.add_define("LV_COLOR_SCREEN_TRANSP", "1") - lv_image_formats.add("ARGB8888") - lv_image_formats.add( - "RGB565" - ) # Currently always need RGB565 for the display buffer for use in helpers.lv_uses: df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") @@ -401,9 +398,6 @@ async def to_code(configs): metadata = get_image_metadata(image_id.id) image_type = IMAGE_TYPE[metadata.image_type] transparent = metadata.transparency != CONF_OPAQUE - if transparent: - # Internal draw layer will use ARGB8888 - lv_image_formats.add("ARGB8888") if image_type == ImageBinary: lv_image_formats.add("I1") if image_type == ImageGrayscale: diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index de5835d7a69..dd51a2f5199 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -52,10 +52,6 @@ def get_remapped_uses(): return get_data(KEY_REMAPPED_USES, set()) -def get_color_formats(): - return get_data(KEY_COLOR_FORMATS, set()) - - def add_warning(msg: str): get_warnings().add(msg) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 6f43e78f90e..793290de73a 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -4,26 +4,12 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from .defines import ( - CONF_STYLE_DEFINITIONS, - CONF_THEME, - CONF_TOP_LAYER, - LValidator, - literal, -) +from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv +from .lvcode import LambdaContext, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property -from .types import ObjUpdateAction, lv_obj_t, lv_style_t -from .widgets import ( - Widget, - add_widgets, - collect_parts, - set_obj_properties, - theme_widget_map, - wait_for_widgets, -) -from .widgets.obj import obj_spec +from .types import ObjUpdateAction, lv_style_t +from .widgets import collect_parts, theme_widget_map, wait_for_widgets def has_style_props(config) -> bool: @@ -112,12 +98,3 @@ async def theme_to_code(config): for state, props in states.items() } theme_widget_map[w_name] = styles - - -async def add_top_layer(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.var.get_disp()) - if top_conf := config.get(CONF_TOP_LAYER): - with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: - top_w = Widget(top_layer_obj, obj_spec, top_conf) - await set_obj_properties(top_w, top_conf) - await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index b383196963c..0ac4062106d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,5 +1,4 @@ import sys -from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.automation import register_action @@ -405,7 +404,11 @@ class Widget: # Map of widgets to their config, used for trigger generation -widget_map: dict[Any, Widget] = {} +widget_map: dict[ID, Widget] = {} + + +def is_widget_completed(name: ID) -> bool: + return name in widget_map class LvScrActType(WidgetType): diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 0e40d0dfbef..f12766bae12 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -42,7 +42,6 @@ from ..defines import ( CONF_SRC, CONF_START_ANGLE, addr, - get_color_formats, literal, ) from ..lv_validation import ( @@ -99,7 +98,6 @@ class CanvasType(WidgetType): # RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel) if config[CONF_TRANSPARENT]: color_format = "LV_COLOR_FORMAT_ARGB8888" - get_color_formats().add("ARGB8888") else: color_format = "LV_COLOR_FORMAT_NATIVE" diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index d4a71078d0b..029ca5f6846 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -1,12 +1,15 @@ from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv from esphome.const import CONF_ITEMS, CONF_MODE +from esphome.core import CORE from esphome.cpp_types import std_string +from .. import LvContext from ..defines import CONF_MAIN, KEYBOARD_MODES, literal -from ..helpers import add_lv_use, lvgl_components_required +from ..helpers import lvgl_components_required from ..types import LvCompound, LvType -from . import Widget, WidgetType, get_widgets +from . import Widget, WidgetType, get_widgets, is_widget_completed +from .buttonmatrix import CONF_BUTTONMATRIX from .textarea import CONF_TEXTAREA, lv_textarea_t CONF_KEYBOARD = "keyboard" @@ -41,16 +44,27 @@ class KeyboardType(WidgetType): ) def get_uses(self): - return CONF_KEYBOARD, CONF_TEXTAREA + return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX async def to_code(self, w: Widget, config: dict): lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add(CONF_KEYBOARD) - add_lv_use("btnmatrix") if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) - if ta := await get_widgets(config, CONF_TEXTAREA): - await w.set_property(CONF_TEXTAREA, ta[0].obj) + if textarea := config.get(CONF_TEXTAREA): + # If a textarea is configured, it must be generated before the keyboard can attach it. + # If not yet configured, defer the attachment code. + + async def add_textarea(): + async with LvContext(): + await w.set_property( + CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj + ) + + if is_widget_completed(textarea): + await add_textarea() + else: + CORE.add_job(add_textarea) keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index bb5900b8c92..5ac92f2717a 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -35,7 +35,7 @@ class LabelType(WidgetType): if (value := config.get(CONF_TEXT)) is not None: await w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_LONG_MODE, config) - await w.set_property(CONF_RECOLOR, config) + await w.set_property(CONF_RECOLOR, config, processor=lv_bool) label_spec = LabelType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index a9b202163f9..3112cc28d0c 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -17,11 +17,6 @@ lv_point_t = cg.global_ns.struct("lv_point_t") lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") -LINE_SCHEMA = { - cv.Required(CONF_POINTS): cv.ensure_list(point_schema), -} - - async def process_coord(coord): if isinstance(coord, Lambda): return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) @@ -34,15 +29,17 @@ class LineType(WidgetType): CONF_LINE, LvType("LvLineType", parents=(LvCompound,)), (CONF_MAIN,), - LINE_SCHEMA, + schema={cv.Required(CONF_POINTS): cv.ensure_list(point_schema)}, + modify_schema={cv.Optional(CONF_POINTS): cv.ensure_list(point_schema)}, ) async def to_code(self, w: Widget, config): - points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] - for p in config[CONF_POINTS] - ] - lv_add(w.var.set_points(points)) + if CONF_POINTS in config: + points = [ + [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + for p in config[CONF_POINTS] + ] + lv_add(w.var.set_points(points)) line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index af27ee75531..d0e6bfa3a2b 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -33,6 +33,7 @@ from ..styles import LVStyle from ..types import LV_EVENT, lv_obj_t from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code from .button import button_spec, lv_button_t +from .img import CONF_IMAGE from .label import CONF_LABEL from .obj import obj_spec @@ -41,7 +42,7 @@ CONF_MSGBOX = "msgbox" OUTER_STYLE = LVStyle( "msgbox_outer", { - "bg_opa": 128, + "bg_opa": 0.5, "bg_color": "black", "border_width": 0, "pad_all": 0, @@ -119,6 +120,7 @@ async def msgbox_to_code(top_layer, conf): CONF_BUTTON, CONF_LABEL, CONF_MSGBOX, + CONF_IMAGE, *button_spec.get_uses(), ) if CONF_BUTTON_STYLE in conf: @@ -156,7 +158,7 @@ async def msgbox_to_code(top_layer, conf): with LocalVariable( "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) ) as close_btn: - lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.remove_event(close_btn, 0) lv_obj.add_event_cb( close_btn, await close_action.get_lambda(), @@ -170,6 +172,6 @@ async def msgbox_to_code(top_layer, conf): async def msgboxes_to_code(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + top_layer = lv_expr.disp_get_layer_top(lv_component.get_disp()) for conf in config.get(CONF_MSGBOXES, ()): await msgbox_to_code(top_layer, conf) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 82c43705433..df76ab6bb0d 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT -from ..defines import CONF_MAIN, get_color_formats +from ..defines import CONF_MAIN from ..lv_validation import color, lv_color, lv_int, lv_text from ..lvcode import LocalVariable, lv from ..schemas import TEXT_SCHEMA @@ -44,7 +44,6 @@ class QrCodeType(WidgetType): return CONF_CANVAS, CONF_IMAGE async def to_code(self, w: Widget, config): - get_color_formats().add("ARGB8888") await w.set_property( CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) ) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 60ba664f047..7629b03e9db 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -26,7 +26,7 @@ from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .button import button_spec -from .buttonmatrix import buttonmatrix_spec +from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec from .obj import obj_spec CONF_TABVIEW = "tabview" @@ -73,7 +73,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return "btnmatrix", TYPE_FLEX + return CONF_BUTTONMATRIX, TYPE_FLEX async def to_code(self, w: Widget, config: dict): await w.set_property( From 9dca7e0daf015db9a2dbe1cad391a000df335ae6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:01:33 -0400 Subject: [PATCH 1117/2056] [tormatic] Fix UART stream desync on ESP32 (#15337) --- .../components/tormatic/tormatic_cover.cpp | 67 ++++++++++++++----- esphome/components/tormatic/tormatic_cover.h | 1 + 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index 77c2e87717e..a58228a219f 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -10,6 +10,10 @@ namespace tormatic { static const char *const TAG = "tormatic.cover"; +// Time to poll the UART when flushing after desync. At 9600 baud, a full +// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived. +static constexpr uint32_t DRAIN_TIMEOUT_MS = 15; + using namespace esphome::cover; void Tormatic::setup() { @@ -256,32 +260,51 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional<GateStatus> Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (!this->pending_hdr_) { + if (this->available() < sizeof(MessageHeader)) { + return {}; + } + + this->pending_hdr_ = this->read_data_<MessageHeader>(); + if (!this->pending_hdr_) { + return {}; + } + + // Sanity check: valid messages have small payloads (3-4 bytes). A large + // or impossible payload_size means the stream is out of sync (corrupted + // byte, dropped data, etc.). Flush the buffer so we can resync on the + // next request/response cycle. + if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) { + ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size()); + this->pending_hdr_.reset(); + this->drain_rx_(); + return {}; + } + } + + // Wait for all payload bytes to arrive before processing. + if (this->available() < this->pending_hdr_->payload_size()) { return {}; } - auto o_hdr = this->read_data_<MessageHeader>(); - if (!o_hdr) { - ESP_LOGE(TAG, "Timeout reading message header"); - return {}; - } - auto hdr = o_hdr.value(); + auto hdr = *this->pending_hdr_; + this->pending_hdr_.reset(); switch (hdr.type) { case STATUS: { if (hdr.payload_size() != sizeof(StatusReply)) { ESP_LOGE(TAG, "Header specifies payload size %" PRIu32 " but size of StatusReply is %zu", hdr.payload_size(), sizeof(StatusReply)); + this->drain_rx_(hdr.payload_size()); + return {}; } - // Read a StatusReply requested by update(). auto o_status = this->read_data_<StatusReply>(); if (!o_status) { return {}; } - auto status = o_status.value(); - return status.state; + return o_status->state; } case COMMAND: @@ -344,16 +367,24 @@ template<typename T> optional<T> Tormatic::read_data_() { return obj; } -// Drain up to n amount of bytes from the uart rx buffer. +// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes +// (caller must ensure they are available). When n == 0, poll for 15ms to +// guarantee a full packet time at 9600 baud has elapsed, consuming any +// bytes still in transit. void Tormatic::drain_rx_(uint16_t n) { uint8_t data; - uint16_t count = 0; - while (this->available()) { - this->read_byte(&data); - count++; - - if (n > 0 && count >= n) { - return; + if (n > 0) { + for (uint16_t i = 0; i < n; i++) { + if (!this->read_byte(&data)) { + return; + } + } + } else { + uint32_t start = millis(); + while (millis() - start < DRAIN_TIMEOUT_MS) { + if (this->available()) { + this->read_byte(&data); + } } } } diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 534d4bef141..34483ed6a35 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -43,6 +43,7 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void handle_gate_status_(GateStatus s); uint32_t seq_tx_{0}; + optional<MessageHeader> pending_hdr_{}; GateStatus current_status_{PAUSED}; From 23dcc5389d12cf4bbfccc0238c959ba84aa77f13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 12:59:45 -1000 Subject: [PATCH 1118/2056] [time] Fix strftime %Z and %z returning wrong timezone (#15330) --- esphome/components/time/posix_tz.cpp | 13 +++++++ esphome/components/time/posix_tz.h | 3 ++ esphome/core/time.cpp | 54 ++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp index 4d1f0c74c2c..f388267abd2 100644 --- a/esphome/components/time/posix_tz.cpp +++ b/esphome/components/time/posix_tz.cpp @@ -4,6 +4,7 @@ #include "posix_tz.h" #include <cctype> +#include <cstdio> namespace esphome::time { @@ -442,6 +443,18 @@ bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) { return internal::parse_dst_rule(p, result.dst_end); } +// Format a POSIX offset (positive = west) as "+HHMM" / "-HHMM" for display. +// Convention: negate POSIX sign so east-of-UTC is positive (ISO 8601 / RFC 2822). +void format_designation(int32_t posix_offset, char *buf, size_t buf_size) { + int32_t display = -posix_offset; + char sign = display >= 0 ? '+' : '-'; + if (display < 0) + display = -display; + int h = display / 3600; + int m = (display % 3600) / 60; + snprintf(buf, buf_size, "%c%02d%02d", sign, h, m); +} + bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) { if (!out_tm) { return false; diff --git a/esphome/components/time/posix_tz.h b/esphome/components/time/posix_tz.h index c71ba15cd14..be1ddfd689c 100644 --- a/esphome/components/time/posix_tz.h +++ b/esphome/components/time/posix_tz.h @@ -36,6 +36,9 @@ struct ParsedTimezone { bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; } }; +/// Format a POSIX offset as "+HHMM"/"-HHMM" into buf (must be >= 6 bytes). +void format_designation(int32_t posix_offset, char *buf, size_t buf_size); + /// Parse a POSIX TZ string into a ParsedTimezone struct. /// /// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility). diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 650c61d37bb..b6fc9b90ad9 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -2,6 +2,9 @@ #include "helpers.h" #include <algorithm> +#ifdef USE_TIME_TIMEZONE +#include "esphome/components/time/posix_tz.h" +#endif namespace esphome { @@ -14,12 +17,59 @@ uint8_t days_in_month(uint8_t month, uint16_t year) { size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); +#ifdef USE_TIME_TIMEZONE + // ::strftime uses libc's internal timezone state for %Z and %z, but we + // eliminated setenv("TZ")/tzset() on embedded platforms to save flash. + // Substitute %Z and %z with correct values from our parsed timezone. + // Quick scan: does format contain %Z or %z (but not %%Z/%%z)? + bool needs_subst = false; + for (const char *p = format; *p; p++) { + if (*p == '%' && *(p + 1)) { + p++; + if (*p == '%') + continue; // %% is a literal %, skip + if (*p == 'Z' || *p == 'z') { + needs_subst = true; + break; + } + } + } + if (needs_subst) { + const auto &tz = time::get_global_tz(); + char designation[6]; // "+HHMM" + null + int32_t offset = c_tm.tm_isdst > 0 ? tz.dst_offset_seconds : tz.std_offset_seconds; + time::format_designation(offset, designation, sizeof(designation)); + + char modified[STRFTIME_BUFFER_SIZE]; + char *out = modified; + char *out_end = modified + sizeof(modified) - 1; + for (const char *p = format; *p && out < out_end; p++) { + if (*p == '%') { + if (*(p + 1) == '%') { + // %% → copy both percent signs (literal %) + *out++ = *p++; + if (out < out_end) + *out++ = *p; + } else if (*(p + 1) == 'Z' || *(p + 1) == 'z') { + p++; // skip the Z/z + for (const char *d = designation; *d && out < out_end; d++) + *out++ = *d; + } else { + *out++ = *p; + } + } else { + *out++ = *p; + } + } + *out = '\0'; + return ::strftime(buffer, buffer_len, modified, &c_tm); + } +#endif return ::strftime(buffer, buffer_len, format, &c_tm); } size_t ESPTime::strftime_to(std::span<char, STRFTIME_BUFFER_SIZE> buffer, const char *format) { - struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(buffer.data(), buffer.size(), format, &c_tm); + size_t len = this->strftime(buffer.data(), buffer.size(), format); if (len > 0) { return len; } From 15bcd62f222ce4e24be1ea3f6e37b0d1c0b04cab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:59:53 +1300 Subject: [PATCH 1119/2056] [internal_temperature] Move code into platform specific files (#15339) --- .../internal_temperature.h | 10 +- .../internal_temperature_bk72xx.cpp | 41 ++++++++ .../internal_temperature_common.cpp | 10 ++ ...ure.cpp => internal_temperature_esp32.cpp} | 96 ++----------------- .../internal_temperature_rp2040.cpp | 31 ++++++ .../internal_temperature_zephyr.cpp | 56 +++++++++++ .../components/internal_temperature/sensor.py | 17 ++++ 7 files changed, 170 insertions(+), 91 deletions(-) create mode 100644 esphome/components/internal_temperature/internal_temperature_bk72xx.cpp create mode 100644 esphome/components/internal_temperature/internal_temperature_common.cpp rename esphome/components/internal_temperature/{internal_temperature.cpp => internal_temperature_esp32.cpp} (54%) create mode 100644 esphome/components/internal_temperature/internal_temperature_rp2040.cpp create mode 100644 esphome/components/internal_temperature/internal_temperature_zephyr.cpp diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h index 78e3bcef7de..4810e8478de 100644 --- a/esphome/components/internal_temperature/internal_temperature.h +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -1,18 +1,18 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" -namespace esphome { -namespace internal_temperature { +namespace esphome::internal_temperature { class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent { public: +#if defined(USE_ESP32) || (defined(USE_ZEPHYR) && defined(USE_NRF52)) void setup() override; +#endif // USE_ESP32 || (USE_ZEPHYR && USE_NRF52) void dump_config() override; void update() override; }; -} // namespace internal_temperature -} // namespace esphome +} // namespace esphome::internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp new file mode 100644 index 00000000000..31a92f90a51 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp @@ -0,0 +1,41 @@ +#ifdef USE_BK72XX + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +extern "C" { +uint32_t temp_single_get_current_temperature(uint32_t *temp_value); +} + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.bk72xx"; + +void InternalTemperatureSensor::update() { + float temperature = NAN; + bool success = false; + + uint32_t raw, result; + result = temp_single_get_current_temperature(&raw); + success = (result == 0); +#if defined(USE_LIBRETINY_VARIANT_BK7231N) + temperature = raw * -0.38f + 156.0f; +#elif defined(USE_LIBRETINY_VARIANT_BK7231T) + temperature = raw * 0.04f; +#else // USE_LIBRETINY_VARIANT + temperature = raw * 0.128f; +#endif // USE_LIBRETINY_VARIANT + + if (success && std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_BK72XX diff --git a/esphome/components/internal_temperature/internal_temperature_common.cpp b/esphome/components/internal_temperature/internal_temperature_common.cpp new file mode 100644 index 00000000000..89a7d34333b --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_common.cpp @@ -0,0 +1,10 @@ +#include "esphome/core/log.h" +#include "internal_temperature.h" + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature"; + +void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } + +} // namespace esphome::internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature_esp32.cpp similarity index 54% rename from esphome/components/internal_temperature/internal_temperature.cpp rename to esphome/components/internal_temperature/internal_temperature_esp32.cpp index 567ae6170e1..09121fa9c91 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature_esp32.cpp @@ -1,7 +1,8 @@ -#include "internal_temperature.h" -#include "esphome/core/log.h" - #ifdef USE_ESP32 + +#include "esphome/core/log.h" +#include "internal_temperature.h" + #if defined(USE_ESP32_VARIANT_ESP32) // there is no official API available on the original ESP32 extern "C" { @@ -13,70 +14,20 @@ uint8_t temprature_sens_read(); defined(USE_ESP32_VARIANT_ESP32S3) #include "driver/temperature_sensor.h" #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 -#ifdef USE_RP2040 -#include "Arduino.h" -#endif // USE_RP2040 -#ifdef USE_BK72XX -extern "C" { -uint32_t temp_single_get_current_temperature(uint32_t *temp_value); -} -#endif // USE_BK72XX -#if defined(USE_ZEPHYR) && defined(USE_NRF52) -#include <zephyr/device.h> -#include <zephyr/drivers/sensor.h> -#endif // USE_ZEPHYR && USE_NRF52 -namespace esphome { -namespace internal_temperature { +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.esp32"; -static const char *const TAG = "internal_temperature"; -#if defined(USE_ZEPHYR) && defined(USE_NRF52) -static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp); -#endif // USE_ZEPHYR && USE_NRF52 -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) static temperature_sensor_handle_t tsensNew = NULL; #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 void InternalTemperatureSensor::update() { -#if defined(USE_ZEPHYR) && defined(USE_NRF52) - struct sensor_value value; - int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR); - if (result != 0) { - ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result); - if (!this->has_state()) { - this->publish_state(NAN); - } - return; - } - - result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value); - if (result != 0) { - ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result); - if (!this->has_state()) { - this->publish_state(NAN); - } - return; - } - - const float temperature = value.val1 + (value.val2 / 1000000.0f); - if (std::isfinite(temperature)) { - this->publish_state(temperature); - } else { - ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature); - if (!this->has_state()) { - this->publish_state(NAN); - } - } -#else - float temperature = NAN; bool success = false; -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32) uint8_t raw = temprature_sens_read(); ESP_LOGV(TAG, "Raw temperature value: %d", raw); @@ -92,23 +43,7 @@ void InternalTemperatureSensor::update() { ESP_LOGE(TAG, "Reading failed (%d)", result); } #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 -#ifdef USE_RP2040 - temperature = analogReadTemp(); - success = (temperature != 0.0f); -#endif // USE_RP2040 -#ifdef USE_BK72XX - uint32_t raw, result; - result = temp_single_get_current_temperature(&raw); - success = (result == 0); -#if defined(USE_LIBRETINY_VARIANT_BK7231N) - temperature = raw * -0.38f + 156.0f; -#elif defined(USE_LIBRETINY_VARIANT_BK7231T) - temperature = raw * 0.04f; -#else // USE_LIBRETINY_VARIANT - temperature = raw * 0.128f; -#endif // USE_LIBRETINY_VARIANT -#endif // USE_BK72XX + if (success && std::isfinite(temperature)) { this->publish_state(temperature); } else { @@ -117,18 +52,9 @@ void InternalTemperatureSensor::update() { this->publish_state(NAN); } } -#endif // USE_ZEPHYR && USE_NRF52 } void InternalTemperatureSensor::setup() { -#if defined(USE_ZEPHYR) && defined(USE_NRF52) - if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) { - ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name); - this->mark_failed(); - return; - } -#endif // USE_ZEPHYR && USE_NRF52 -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) @@ -148,10 +74,8 @@ void InternalTemperatureSensor::setup() { return; } #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 } -void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } +} // namespace esphome::internal_temperature -} // namespace internal_temperature -} // namespace esphome +#endif // USE_ESP32 diff --git a/esphome/components/internal_temperature/internal_temperature_rp2040.cpp b/esphome/components/internal_temperature/internal_temperature_rp2040.cpp new file mode 100644 index 00000000000..66dee9faf78 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_rp2040.cpp @@ -0,0 +1,31 @@ +#ifdef USE_RP2040 + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +#include "Arduino.h" + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.rp2040"; + +void InternalTemperatureSensor::update() { + float temperature = NAN; + bool success = false; + + temperature = analogReadTemp(); + success = (temperature != 0.0f); + + if (success && std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_RP2040 diff --git a/esphome/components/internal_temperature/internal_temperature_zephyr.cpp b/esphome/components/internal_temperature/internal_temperature_zephyr.cpp new file mode 100644 index 00000000000..be72ab6f517 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_zephyr.cpp @@ -0,0 +1,56 @@ +#if defined(USE_ZEPHYR) && defined(USE_NRF52) + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +#include <zephyr/device.h> +#include <zephyr/drivers/sensor.h> + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.zephyr"; + +static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp); + +void InternalTemperatureSensor::update() { + struct sensor_value value; + int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR); + if (result != 0) { + ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value); + if (result != 0) { + ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + const float temperature = value.val1 + (value.val2 / 1000000.0f); + if (std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +void InternalTemperatureSensor::setup() { + if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) { + ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name); + this->mark_failed(); + return; + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_ZEPHYR && USE_NRF52 diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 965e7f05205..6d79e086757 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import sensor from esphome.components.zephyr import zephyr_add_prj_conf +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_TEMPERATURE, @@ -11,6 +12,7 @@ from esphome.const import ( PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + PlatformFramework, ) from esphome.core import CORE @@ -39,3 +41,18 @@ async def to_code(config): if CORE.using_zephyr and CORE.is_nrf52: zephyr_add_prj_conf("SENSOR", True) zephyr_add_prj_conf("TEMP_NRF5", True) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "internal_temperature_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "internal_temperature_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "internal_temperature_bk72xx.cpp": { + PlatformFramework.BK72XX_ARDUINO, + }, + "internal_temperature_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, + } +) From b71c406e704f1d751484404737a91c2b8035ddbb Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:04:07 +0200 Subject: [PATCH 1120/2056] [uart] fix baud rate not applied on `load_settings()` for ESP32 (IDF) (#15341) --- .../uart/uart_component_esp_idf.cpp | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6d9d44e97fc..93e43e03726 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -147,6 +147,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // uart_param_config must be called after uart_driver_install and before any + // other uart_set_*() calls. The driver installation resets the UART peripheral + // registers to their default state, overwriting any previously configured baud + // rate or framing settings. Calling uart_param_config here ensures the requested + // settings are applied after the reset and before pin routing, inversion, and + // threshold configuration. + uart_config_t uart_config = this->get_config_(); + err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; @@ -214,22 +228,15 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install(). auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() + err = uart_set_mode(this->uart_num_, mode); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - uart_config_t uart_config = this->get_config_(); - err = uart_param_config(this->uart_num_, &uart_config); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - #ifdef USE_UART_WAKE_LOOP_ON_RX // Register ISR callback to wake the main loop when UART data arrives. // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to From 4a23ba7d8a2b28f5245674fc8337227e6f50ed08 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 31 Mar 2026 19:06:48 -0400 Subject: [PATCH 1121/2056] [mixer] Fix memory leak in mixer task on stop/start cycles (#15185) --- .../mixer/speaker/mixer_speaker.cpp | 274 +++++++++--------- 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 9d11abb3277..0fabc68c705 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -597,173 +597,173 @@ void MixerSpeaker::audio_mixer_task(void *params) { xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING); - std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( - this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); + { // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task + std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( + this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - if (output_transfer_buffer == nullptr) { - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); + if (output_transfer_buffer == nullptr) { + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); - vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it - } - - output_transfer_buffer->set_sink(this_mixer->output_speaker_); - - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - - bool sent_finished = false; - - // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) - FixedVector<SourceSpeaker *> speakers_with_data; - FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; - speakers_with_data.init(this_mixer->source_speakers_.size()); - transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - - while (true) { - uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); - if (event_group_bits & MIXER_TASK_COMMAND_STOP) { - break; + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } - // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves - output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + output_transfer_buffer->set_sink(this_mixer->output_speaker_); - const uint32_t output_frames_free = - this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - speakers_with_data.clear(); - transfer_buffers_with_data.clear(); + bool sent_finished = false; - for (auto &speaker : this_mixer->source_speakers_) { - if (speaker->is_running() && !speaker->get_pause_state()) { - // Speaker is running and not paused, so it possibly can provide audio data - std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); - if (transfer_buffer.use_count() == 0) { - // No transfer buffer allocated, so skip processing this speaker - continue; - } - speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) + FixedVector<SourceSpeaker *> speakers_with_data; + FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; + speakers_with_data.init(this_mixer->source_speakers_.size()); + transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - if (transfer_buffer->available() > 0) { - // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop - transfer_buffers_with_data.push_back(transfer_buffer); - speakers_with_data.push_back(speaker); + while (true) { + uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + break; + } + + // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves + output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + + const uint32_t output_frames_free = + this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + + speakers_with_data.clear(); + transfer_buffers_with_data.clear(); + + for (auto &speaker : this_mixer->source_speakers_) { + if (speaker->is_running() && !speaker->get_pause_state()) { + // Speaker is running and not paused, so it possibly can provide audio data + std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); + if (transfer_buffer.use_count() == 0) { + // No transfer buffer allocated, so skip processing this speaker + continue; + } + speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + + if (transfer_buffer->available() > 0) { + // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop + transfer_buffers_with_data.push_back(transfer_buffer); + speakers_with_data.push_back(speaker); + } } } - } - if (transfer_buffers_with_data.empty()) { - // No audio available for transferring, block task temporarily - delay(TASK_DELAY_MS); - continue; - } + if (transfer_buffers_with_data.empty()) { + // No audio available for transferring, block task temporarily + delay(TASK_DELAY_MS); + continue; + } - uint32_t frames_to_mix = output_frames_free; + uint32_t frames_to_mix = output_frames_free; - if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { - // Only one speaker has audio data, just copy samples over + if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { + // Only one speaker has audio data, just copy samples over - audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); + audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); - if (active_stream_info.get_sample_rate() == - this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { - // Speaker's sample rate matches the output speaker's, copy directly + if (active_stream_info.get_sample_rate() == + this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { + // Speaker's sample rate matches the output speaker's, copy directly - const uint32_t frames_available_in_buffer = - active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info, - reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); + const uint32_t frames_available_in_buffer = + active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), + active_stream_info, reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); - // Set playback delay for newly contributing source - if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[0]->playback_delay_frames_.store( - this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); - speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + // Set playback delay for newly contributing source + if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[0]->playback_delay_frames_.store( + this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); + speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + } + + // Update source speaker pending frames + speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + + // Update output transfer buffer length and pipeline frame count + output_transfer_buffer->increase_buffer_length( + this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); + this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); + } else { + // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker + if (!this_mixer->output_speaker_->is_stopped()) { + if (!sent_finished) { + this_mixer->output_speaker_->finish(); + sent_finished = true; // Avoid repeatedly sending the finish command + } + } else { + // Speaker has finished writing the current audio, update the stream information and restart the speaker + this_mixer->audio_stream_info_ = + audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, + active_stream_info.get_sample_rate()); + this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); + this_mixer->output_speaker_->start(); + // Reset pipeline frame count since we're starting fresh with a new sample rate + this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); + sent_finished = false; + } + } + } else { + // Determine how many frames to mix + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames( + transfer_buffers_with_data[i]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + } + int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()); + audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); + + // Mix two streams together + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { + mix_audio_samples(primary_buffer, primary_stream_info, + reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), + speakers_with_data[i]->get_audio_stream_info(), + reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); + + if (i != transfer_buffers_with_data.size() - 1) { + // Need to mix more streams together, point primary buffer and stream info to the already mixed output + primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()); + primary_stream_info = this_mixer->audio_stream_info_.value(); + } } - // Update source speaker pending frames - speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + // Get current pipeline depth for delay calculation (before incrementing) + uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - // Update output transfer buffer length and pipeline frame count + // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + // Set playback delay for newly contributing sources + if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); + speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); + } + + speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[i]->decrease_buffer_length( + speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); + } + + // Update output transfer buffer length and pipeline frame count (once, not per source) output_transfer_buffer->increase_buffer_length( this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); - } else { - // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker - if (!this_mixer->output_speaker_->is_stopped()) { - if (!sent_finished) { - this_mixer->output_speaker_->finish(); - sent_finished = true; // Avoid repeatedly sending the finish command - } - } else { - // Speaker has finished writing the current audio, update the stream information and restart the speaker - this_mixer->audio_stream_info_ = - audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, - active_stream_info.get_sample_rate()); - this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); - this_mixer->output_speaker_->start(); - // Reset pipeline frame count since we're starting fresh with a new sample rate - this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - sent_finished = false; - } } - } else { - // Determine how many frames to mix - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - const uint32_t frames_available_in_buffer = - speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - } - int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()); - audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); - - // Mix two streams together - for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { - mix_audio_samples(primary_buffer, primary_stream_info, - reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), - speakers_with_data[i]->get_audio_stream_info(), - reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); - - if (i != transfer_buffers_with_data.size() - 1) { - // Need to mix more streams together, point primary buffer and stream info to the already mixed output - primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()); - primary_stream_info = this_mixer->audio_stream_info_.value(); - } - } - - // Get current pipeline depth for delay calculation (before incrementing) - uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - - // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - // Set playback delay for newly contributing sources - if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); - speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); - } - - speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[i]->decrease_buffer_length( - speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); - } - - // Update output transfer buffer length and pipeline frame count (once, not per source) - output_transfer_buffer->increase_buffer_length( - this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); - this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); } - } - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + } // Reset pipeline frame count since the task is stopping this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - output_transfer_buffer.reset(); - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it From 954227b2031962cf074615981b471613206be47b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 13:26:26 -1000 Subject: [PATCH 1122/2056] [esp32_ble_tracker] Restart BLE scan after OTA failure (#15308) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 6 ++++++ esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 6dce70f8396..f2d60be6413 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -88,12 +88,18 @@ void ESP32BLETracker::setup() { #ifdef USE_OTA_STATE_LISTENER void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { if (state == ota::OTA_STARTED) { + this->scan_continuous_before_ota_ = this->scan_continuous_; this->stop_scan(); #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->disconnect(); } #endif + } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { + this->scan_continuous_before_ota_ = false; + this->scan_continuous_ = true; + // Do not restart scanning immediately here; allow loop() to + // safely restart scanning once the scanner and all clients are idle. } } #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index ff69a4dcd2d..43405b02b7f 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -431,6 +431,9 @@ class ESP32BLETracker : public Component, ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; +#ifdef USE_OTA_STATE_LISTENER + bool scan_continuous_before_ota_{false}; +#endif bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; From e261b5de655dbcda6a1fbda83aec1fbdfd8c5c1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:22:25 -1000 Subject: [PATCH 1123/2056] [time] Point to valid IANA timezone list on validation failure (#15110) --- esphome/components/time/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 9821046a737..c31ccbc7eac 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -284,13 +284,23 @@ def validate_tz(value: str) -> str: tzfile = _load_tzdata(value) if tzfile is not None: value = _extract_tz_string(tzfile) + is_iana = True + else: + is_iana = False # Validate that the POSIX TZ string is parseable (skip empty strings) if value: try: parse_posix_tz_python(value) except ValueError as e: - raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + if is_iana: + raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e + raise cv.Invalid( + f"Invalid POSIX timezone string '{value}': {e}. " + f"If you meant to use an IANA timezone, check the list of valid " + f"timezones at " + f"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + ) from e return value From f7b410fd0c14e0bf00bddab3f03227087957d7f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 08:47:15 -1000 Subject: [PATCH 1124/2056] [wifi] Fix roaming attempt counter reset on disconnect during scan (#15099) --- esphome/components/wifi/wifi_component.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 09f883ed617..aa4e691cd09 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -269,11 +269,11 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ │ /// │ ┌──────────────┼──────────────┐ │ /// │ ↓ ↓ ↓ │ -/// │ scan error no better AP +10 dB better AP │ +/// │ disconnect no better AP +10 dB better AP │ /// │ │ │ │ │ /// │ ↓ ↓ ↓ │ /// │ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ -/// │ │ → IDLE │ │ CONNECTING │ │ +/// │ │ → RECONNECTING │ │ CONNECTING │ │ /// │ │ (counter preserved) │ │ (process_roaming_scan_) │ │ /// │ └──────────────────────────────┘ └────────────┬─────────────┘ │ /// │ │ │ @@ -296,7 +296,7 @@ bool CompactString::operator==(const StringRef &other) const { /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Scan error (SCANNING→IDLE): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ @@ -2075,9 +2075,10 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::SCANNING) { - // Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter - ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); - this->roaming_state_ = RoamingState::IDLE; + // Disconnected during roam scan - transition to RECONNECTING so the attempts + // counter is preserved when reconnection succeeds (IDLE would reset it) + ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { // Not a roaming-triggered reconnect, reset state this->clear_roaming_state_(); From d9788aaefc337faaa20e4cde5681368ae3e18a4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 23 Mar 2026 13:58:36 -1000 Subject: [PATCH 1125/2056] [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) --- .../components/wifi/wifi_component_esp8266.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 5514f1c6be5..517b59da37a 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -664,11 +664,22 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + // Use shorter dwell times for roaming scans - we only need to detect strong + // nearby APs, not do a thorough survey. This also reduces off-channel time + // which can cause Beacon Timeout disconnects on some APs. + // Roaming times match the ESP32 IDF scan defaults. + static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300; + static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400; + static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500; + static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100; + static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300; + bool roaming = this->roaming_state_ == RoamingState::SCANNING; if (passive) { - config.scan_time.passive = 500; + config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS; } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; + config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } #endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); From 2f2c7ac393b23f52e660b928ace0fcd0becf9176 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:04:27 -0400 Subject: [PATCH 1126/2056] [sx127x] Fix FIFO read corruption (#15114) --- esphome/components/sx127x/sx127x.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 66957a73424..0fddfdccdb7 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -38,14 +38,18 @@ void SX127x::write_register_(uint8_t reg, uint8_t value) { void SX127x::read_fifo_(std::vector<uint8_t> &packet) { this->enable(); this->write_byte(REG_FIFO & 0x7F); - this->read_array(packet.data(), packet.size()); + for (auto &byte : packet) { + byte = this->transfer_byte(0x00); + } this->disable(); } void SX127x::write_fifo_(const std::vector<uint8_t> &packet) { this->enable(); this->write_byte(REG_FIFO | 0x80); - this->write_array(packet.data(), packet.size()); + for (const auto &byte : packet) { + this->transfer_byte(byte); + } this->disable(); } From cb15e98765e69377bf7369cf95fb21386dd21d57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:03:56 -1000 Subject: [PATCH 1127/2056] [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) --- .../components/datetime/datetime_entity.cpp | 3 + esphome/core/time.cpp | 2 +- esphome/core/time.h | 18 ++++-- tests/components/time/posix_tz_parser.cpp | 56 ++++++++++++++++++- 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 730abb3ca8b..fa50271f044 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const { obj.year = this->year_; obj.month = this->month_; obj.day_of_month = this->day_; + obj.day_of_week = 0; + obj.day_of_year = 0; + obj.is_dst = false; obj.hour = this->hour_; obj.minute = this->minute_; obj.second = this->second_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 6add82e7d14..650c61d37bb 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -231,7 +231,7 @@ void ESPTime::increment_day() { void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { time_t res = 0; - if (!this->fields_in_range()) { + if (!this->fields_in_range(false, use_day_of_year)) { this->timestamp = -1; return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index 1716c51ffd5..ed474320381 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -79,11 +79,19 @@ struct ESPTime { /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_year > 0 && this->day_of_year < 367 && this->month > 0 && - this->month < 13 && this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + /// Check if time fields are in range. + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool fields_in_range(bool check_day_of_week = true, bool check_day_of_year = true) const { + bool valid = this->second < 61 && this->minute < 60 && this->hour < 24 && this->month > 0 && this->month < 13 && + this->day_of_month > 0 && this->day_of_month <= days_in_month(this->month, this->year); + if (check_day_of_week) { + valid = valid && this->day_of_week > 0 && this->day_of_week < 8; + } + if (check_day_of_year) { + valid = valid && this->day_of_year > 0 && this->day_of_year < 367; + } + return valid; } /** Convert a string to ESPTime struct as specified by the format argument. diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index d1747ef5b19..b7cf2a4afad 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -1036,8 +1036,6 @@ static time_t esptime_recalc_local(int year, int month, int day, int hour, int m t.hour = hour; t.minute = min; t.second = sec; - t.day_of_week = 1; // Placeholder for fields_in_range() - t.day_of_year = 1; t.recalc_timestamp_local(); return t.timestamp; } @@ -1187,6 +1185,60 @@ TEST(RecalcTimestampLocal, NonDefaultTransitionTime) { EXPECT_EQ(esp_result, libc_result); } +TEST(RecalcTimestampLocal, MinimalFieldsWithoutDayOfWeekOrYear) { + // Regression test for issue #15115: DateTimeEntity::state_as_esptime() constructs + // an ESPTime with only year/month/day/hour/minute/second set (no day_of_week or + // day_of_year). recalc_timestamp_local() must work without those fields. + const char *tz_str = "CET-1CEST,M3.5.0,M10.5.0"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + // Construct ESPTime with only date/time fields (like state_as_esptime does) + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 20; + t.hour = 23; + t.minute = 14; + t.second = 55; + // day_of_week and day_of_year are deliberately left as 0 + t.recalc_timestamp_local(); + + // Must NOT return -1 (the bug: fields_in_range() rejected valid times) + EXPECT_NE(t.timestamp, -1); + + // Verify against libc + time_t libc_result = libc_mktime(2026, 3, 20, 23, 14, 55); + EXPECT_EQ(t.timestamp, libc_result); +} + +TEST(RecalcTimestampLocal, MinimalFieldsNoDST) { + // Same test but with a timezone that has no DST + const char *tz_str = "IST-5:30"; + setenv("TZ", tz_str, 1); + tzset(); + time::ParsedTimezone tz{}; + ASSERT_TRUE(parse_posix_tz(tz_str, tz)); + set_global_tz(tz); + + ESPTime t{}; + t.year = 2026; + t.month = 3; + t.day_of_month = 23; + t.hour = 10; + t.minute = 0; + t.second = 0; + t.recalc_timestamp_local(); + + EXPECT_NE(t.timestamp, -1); + + time_t libc_result = libc_mktime(2026, 3, 23, 10, 0, 0); + EXPECT_EQ(t.timestamp, libc_result); +} + TEST(RecalcTimestampLocal, YearBoundaryDST) { // Test southern hemisphere DST across year boundary // Australia/Sydney: DST active from October to April (spans Jan 1) From f5f99071fb51f9cd61ed3bcf2e2f63e3a1555c77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 24 Mar 2026 14:04:17 -1000 Subject: [PATCH 1128/2056] [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) --- esphome/components/wifi/wifi_component.cpp | 66 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 6 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index aa4e691cd09..8656df7f4dc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -287,18 +287,25 @@ bool CompactString::operator==(const StringRef &other) const { /// │ │ (counter reset to 0) │ │ (retry_connect called) │ /// │ └──────────────────────────────────┘ └───────────┬─────────────┘ /// │ │ │ -/// │ ↓ │ -/// │ ┌───────────────────────┐ │ -/// │ │ → IDLE │ │ -/// │ │ (counter preserved!) │ │ -/// │ └───────────────────────┘ │ +/// │ ┌─────────┴─────────┐ │ +/// │ ↓ ↓ │ +/// │ on target BSSID on other AP │ +/// │ │ │ │ +/// │ ↓ ↓ │ +/// │ ┌──────────────────┐ ┌────────────┐│ +/// │ │ → IDLE │ │ → IDLE ││ +/// │ │ (counter reset) │ │ (counter ││ +/// │ │ (roam worked!) │ │ preserved)││ +/// │ └──────────────────┘ └────────────┘│ /// │ │ /// │ Key behaviors: │ /// │ - After 3 checks: attempts >= 3, stop checking │ /// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect during scan (SCANNING→RECONNECTING): counter preserved │ +/// │ - Disconnect after scan (within grace period): counter preserved │ /// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ -/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ +/// │ - Roaming success via retry (on target BSSID): counter reset │ +/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │ /// └──────────────────────────────────────────────────────────────────────┘ // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) @@ -1583,17 +1590,33 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Only preserve attempts if reconnecting after a failed roam attempt // This prevents ping-pong between APs when a roam target is unreachable if (this->roaming_state_ == RoamingState::CONNECTING) { - // Successful roam to better AP - reset attempts so we can roam again later + // Successful roam to better AP on first try - reset attempts so we can roam again later ESP_LOGD(TAG, "Roam successful"); this->roaming_attempts_ = 0; } else if (this->roaming_state_ == RoamingState::RECONNECTING) { - // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong - ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + // Check if we ended up on the roam target despite needing a retry + // (e.g., first connect failed but scan-based retry found and connected to the same better AP) + bssid_t current_bssid = this->wifi_bssid(); + if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) { + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(current_bssid.data(), bssid_buf); + ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS, + bssid_buf); + this->roaming_attempts_ = 0; + } else if (this->roaming_target_bssid_ != bssid_t{}) { + // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong + ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } else { + // Reconnected after scan-induced disconnect (no roam target) - keep attempts + ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } } else { // Normal connection (boot, credentials changed, etc.) this->roaming_attempts_ = 0; } this->roaming_state_ = RoamingState::IDLE; + this->roaming_target_bssid_ = {}; + this->roaming_scan_end_ = 0; // Clear all priority penalties - the next reconnect will happen when an AP disconnects, // which means the landscape has likely changed and previous tracked failures are stale @@ -2080,8 +2103,16 @@ void WiFiComponent::retry_connect() { ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; } else if (this->roaming_state_ == RoamingState::IDLE) { - // Not a roaming-triggered reconnect, reset state - this->clear_roaming_state_(); + // Check if a roaming scan recently completed - on ESP8266, going off-channel + // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes. + // Transition to RECONNECTING so the attempts counter is preserved on reconnect. + if (this->roaming_scan_end_ != 0 && millis() - this->roaming_scan_end_ < ROAMING_SCAN_GRACE_PERIOD) { + ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; + } else { + // Not a roaming-triggered reconnect, reset state + this->clear_roaming_state_(); + } } // RECONNECTING: keep state and counter, still trying to reconnect @@ -2316,6 +2347,8 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> void WiFiComponent::clear_roaming_state_() { this->roaming_attempts_ = 0; this->roaming_last_check_ = 0; + this->roaming_scan_end_ = 0; + this->roaming_target_bssid_ = {}; this->roaming_state_ = RoamingState::IDLE; } @@ -2383,7 +2416,7 @@ void WiFiComponent::check_roaming_(uint32_t now) { // Guard: skip scan if signal is already good (no meaningful improvement possible) int8_t rssi = this->wifi_rssi(); if (rssi > ROAMING_GOOD_RSSI) { - ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, + ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); return; } @@ -2397,6 +2430,9 @@ void WiFiComponent::process_roaming_scan_() { this->scan_done_ = false; // Default to IDLE - will be set to CONNECTING if we find a better AP this->roaming_state_ = RoamingState::IDLE; + // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout) + // can be attributed to the scan and avoid resetting the attempts counter + this->roaming_scan_end_ = millis(); // Get current connection info int8_t current_rssi = this->wifi_rssi(); @@ -2445,10 +2481,12 @@ void WiFiComponent::process_roaming_scan_() { WiFiAP roam_params = *selected; apply_scan_result_to_params(roam_params, *best); - this->release_scan_results_(); // Mark as roaming attempt - affects retry behavior if connection fails this->roaming_state_ = RoamingState::CONNECTING; + this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results + + this->release_scan_results_(); // Connect directly - wifi_sta_connect_ handles disconnect internally this->start_connecting(roam_params); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 883cc1344b4..27a46a8e034 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -779,6 +779,10 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; + // Grace period after roaming scan completes. If WiFi disconnects within this + // window (e.g., ESP8266 Beacon Timeout caused by going off-channel during scan), + // the disconnect is treated as roaming-related and the attempts counter is preserved. + static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD = 30 * 1000; // 30 seconds // 4-byte members float output_power_{NAN}; @@ -786,6 +790,7 @@ class WiFiComponent : public Component { uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t roaming_last_check_{0}; + uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif @@ -810,6 +815,7 @@ class WiFiComponent : public Component { bool error_from_callback_{false}; RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; + bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; #endif From 92642df419ad4ca8a7dae98ddb85cef050e8e5c2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:21:04 -0400 Subject: [PATCH 1129/2056] [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) --- esphome/components/wifi/wifi_component.cpp | 8 ++++++++ esphome/components/wifi/wifi_component.h | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8656df7f4dc..6163571bc9a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2229,6 +2229,14 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); +#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) + if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) || + (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) { + ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel); + this->selected_sta_index_ = -1; + return false; + } +#endif return true; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 27a46a8e034..c88fffc5126 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -774,6 +774,8 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif + static constexpr uint8_t FIRST_5GHZ_CHANNEL = 36; + // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index eca3f192490..0280becc7e4 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -961,6 +961,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = 100; config.scan_time.active.max = 300; } + // When scanning while connected (roaming), return to home channel between + // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) + if (this->roaming_state_ == RoamingState::SCANNING) { + config.coex_background_scan = true; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { From 7b5a4b466a135ab658317e928830a65c05f9d19a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:50:37 -0400 Subject: [PATCH 1130/2056] [uart] Fix debug callback missing peeked byte and reading past end (#15169) --- esphome/components/uart/uart_component_esp_idf.cpp | 6 ++++-- esphome/components/uart/uart_component_host.cpp | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8168e49805a..82120b1a5f9 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -324,6 +324,9 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { } bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return false; + } size_t length_to_read = len; int32_t read_len = 0; if (!this->check_read_timeout_(len)) @@ -331,11 +334,10 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; - data++; this->has_peek_ = false; } if (length_to_read > 0) - read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 66026f3ccdd..e9c101816ef 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -235,16 +235,14 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { } if (!this->check_read_timeout_(len)) return false; - uint8_t *data_ptr = data; size_t length_to_read = len; if (this->has_peek_) { length_to_read--; - *data_ptr = this->peek_byte_; - data_ptr++; + *data = this->peek_byte_; this->has_peek_ = false; } if (length_to_read > 0) { - int sz = ::read(this->file_descriptor_, data_ptr, length_to_read); + int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read); if (sz == -1) { this->update_error_(strerror(errno)); return false; From 3fd3dcc7e575e52f0924ebdd4fa2fa0356762c0c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:13:24 -0400 Subject: [PATCH 1131/2056] [sgp4x] Fix NOx index_offset default (should be 1, not 100) (#15212) --- esphome/components/sgp4x/sensor.py | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index ab78ab59d9f..8d52ffb4f26 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -44,20 +44,27 @@ def validate_sensors(config): return config -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, - cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, - } - ) - } -) +def _gas_sensor_schema(index_offset_default: int): + return cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional( + CONF_INDEX_OFFSET, default=index_offset_default + ): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } + ) + + +VOC_SENSOR = _gas_sensor_schema(100) +NOX_SENSOR = _gas_sensor_schema(1) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -68,13 +75,13 @@ CONFIG_SCHEMA = cv.All( accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(VOC_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( From 3d8a3a91f25cc2e620c4d04e3b5aa9fd1773921b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 28 Mar 2026 15:38:06 -1000 Subject: [PATCH 1132/2056] [esp32_ble_server] Fix set_value action with static data lists (#15285) --- .../components/esp32_ble_server/ble_server_automations.h | 2 ++ tests/components/esp32_ble_server/common.yaml | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index fe186002803..0bbfdffd5bd 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -70,6 +70,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T public: BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) + void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { // If the listener is already set, do nothing @@ -115,6 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts... public: BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) + void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 7fe0b2eb5f1..4e34049038a 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -69,3 +69,11 @@ esp32_ble_server: - ble_server.descriptor.set_value: id: test_change_descriptor value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data(); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: + data: [0xfc, 0xef, 0xfe, 0x86] + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: + data: [0x01, 0x02, 0x03] From d79cf1d7183ed1b72a825a116be798120bbd775e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 29 Mar 2026 11:57:52 -1000 Subject: [PATCH 1133/2056] [esp8266] Add enable_scanf_float option (#15284) --- esphome/components/esp8266/__init__.py | 62 +++++++++++++++---- .../components/esp8266/test.esp8266-ard.yaml | 3 + tests/unit_tests/components/test_esp8266.py | 62 +++++++++++++++++++ 3 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/components/test_esp8266.py diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 16043b6d69a..2081145096e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +import re import esphome.codegen as cg import esphome.config_validation as cv @@ -18,8 +19,9 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.helpers import copy_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( @@ -40,12 +42,42 @@ from .const import ( ) from .gpio import PinInitialState, add_pin_initial_states_array +CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float" +# Heuristically matches scanf/sscanf calls with float format specifiers. +# Standard scanf float conversions: %f %F %e %E %g %G %a %A +# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length) +# Also matches non-standard patterns like %.2f as a heuristic — these are +# invalid in scanf but users may write them by analogy with printf. +# Uses [^;]*? to stay within a single statement, preventing false positives +# from e.g. sscanf(buf, "%d", &x); printf("%f", val); +_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]") + CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def lambdas_use_scanf_float(config: ConfigType) -> bool: + """Check if any lambda in the config uses scanf with a float format specifier. + + Comments are stripped before matching to avoid false positives from + commented-out code. The cost of a false positive is only ~8KB flash. + """ + stack: list = [config] + while stack: + obj = stack.pop() + if isinstance(obj, Lambda): + src = obj.comment_remover(obj.value) + if _SCANF_FLOAT_RE.search(src): + return True + elif isinstance(obj, dict): + stack.extend(obj.values()) + elif isinstance(obj, list): + stack.extend(obj) + return False + + def set_core_data(config): CORE.data[KEY_ESP8266] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 @@ -181,6 +213,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean, } ), set_core_data, @@ -201,16 +234,23 @@ async def to_code(config): cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option( - "extra_scripts", - [ - "pre:testing_mode.py", - "pre:exclude_updater.py", - "pre:exclude_waveform.py", - "pre:remove_float_scanf.py", - "post:post_build.py", - ], - ) + enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT) + if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config): + enable_scanf_float = True + _LOGGER.warning( + "Lambda uses scanf with a float format specifier; " + "enabling scanf float support (~8KB flash)" + ) + + extra_scripts = [ + "pre:testing_mode.py", + "pre:exclude_updater.py", + "pre:exclude_waveform.py", + ] + if not enable_scanf_float: + extra_scripts.append("pre:remove_float_scanf.py") + extra_scripts.append("post:post_build.py") + cg.add_platformio_option("extra_scripts", extra_scripts) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index c77218f7a3c..ba70c1a6a4f 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -14,3 +14,6 @@ esphome: assert(x == 95); x = clamp_at_most(x, 40); assert(x == 40); + - lambda: |- + float value = 0.0f; + sscanf("3.14", "%f", &value); diff --git a/tests/unit_tests/components/test_esp8266.py b/tests/unit_tests/components/test_esp8266.py new file mode 100644 index 00000000000..318fd2d889f --- /dev/null +++ b/tests/unit_tests/components/test_esp8266.py @@ -0,0 +1,62 @@ +"""Tests for ESP8266 component.""" + +import pytest + +from esphome.components.esp8266 import lambdas_use_scanf_float +from esphome.core import Lambda +from esphome.types import ConfigType + + +@pytest.mark.parametrize( + ("src", "expected"), + [ + # Basic float formats + ('sscanf(buf, "%f", &v)', True), + ('sscanf(buf, "%F", &v)', True), + ('sscanf(buf, "%e", &v)', True), + ('sscanf(buf, "%E", &v)', True), + ('sscanf(buf, "%g", &v)', True), + ('sscanf(buf, "%G", &v)', True), + ('sscanf(buf, "%a", &v)', True), + ('sscanf(buf, "%A", &v)', True), + # With modifiers + ('sscanf(buf, "%lf", &v)', True), + ('sscanf(buf, "%Lf", &v)', True), + ('sscanf(buf, "%8lf", &v)', True), + ('sscanf(buf, "%*f")', True), + ('sscanf(buf, "%.2f", &v)', True), + # Mixed formats + ('sscanf(buf, "%d,%f", &a, &b)', True), + # fscanf and std::sscanf + ('fscanf(fp, "%f", &v)', True), + ('std::sscanf(buf, "%f", &v)', True), + # Multi-line + ('sscanf(buf,\n"%f", &v)', True), + # No float format + ('sscanf(buf, "%d", &v)', False), + ('sscanf(buf, "%s", s)', False), + # printf not scanf + ('printf("%f", val)', False), + # %f in a different statement after scanf + ('sscanf(buf, "%d", &x); printf("%f", val);', False), + # scanf %f in comment only + ('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False), + ('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False), + ], +) +def test_lambdas_use_scanf_float(src: str, expected: bool) -> None: + """Test scanf float detection in lambda source.""" + config: ConfigType = {"test": [Lambda(src)]} + assert lambdas_use_scanf_float(config) is expected + + +def test_lambdas_use_scanf_float_no_lambdas() -> None: + """Test with config containing no lambdas.""" + config: ConfigType = {"key": "value", "list": [1, 2]} + assert lambdas_use_scanf_float(config) is False + + +def test_lambdas_use_scanf_float_nested() -> None: + """Test detection in deeply nested config.""" + config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}} + assert lambdas_use_scanf_float(config) is True From 9cd7c5e700f954272742250467b2ad703d6f5e60 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 30 Mar 2026 13:15:02 -0500 Subject: [PATCH 1134/2056] [thermostat] Fix stale `max_runtime_exceeded` causing spurious supplemental heating/cooling (#15274) --- .../components/thermostat/thermostat_climate.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d52a22f880d..eb3e756bc2a 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -606,6 +606,16 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off, + // even if supplemental_action_ is already idle (early-return path). This prevents a stale + // heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle + // when HEATING_MAX_RUN_TIME fires while the main action is already IDLE. + if (action == climate::CLIMATE_ACTION_OFF || action == climate::CLIMATE_ACTION_IDLE) { + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + } // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->supplemental_action_) && this->setup_complete_) { // already in target mode @@ -975,8 +985,10 @@ void ThermostatClimate::cooling_on_timer_callback_() { void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); - if (this->supports_fan_only_action_uses_fan_mode_timer_) + if (this->supports_fan_only_action_uses_fan_mode_timer_) { this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); + } } void ThermostatClimate::fanning_off_timer_callback_() { From 3bf45d8fe026309f8a8e60f49bdb15c564788529 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:22:29 -0400 Subject: [PATCH 1135/2056] [haier] Fix hOn half-degree temperature setting (#15312) --- esphome/components/haier/hon_climate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b8889ef2bdc..b027b0f295e 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -677,7 +677,6 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); } out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0; - control_out_buffer[4] = 0; // This byte should be cleared before setting values out_data->display_status = this->get_display_state() ? 1 : 0; this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); out_data->health_mode = this->get_health_mode() ? 1 : 0; From 66a4acafd061b37f71f49cd7c6348df9a7c9eb9e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:01:33 -0400 Subject: [PATCH 1136/2056] [tormatic] Fix UART stream desync on ESP32 (#15337) --- .../components/tormatic/tormatic_cover.cpp | 67 ++++++++++++++----- esphome/components/tormatic/tormatic_cover.h | 1 + 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index 37a269088e6..a48dece8405 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -9,6 +9,10 @@ namespace tormatic { static const char *const TAG = "tormatic.cover"; +// Time to poll the UART when flushing after desync. At 9600 baud, a full +// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived. +static constexpr uint32_t DRAIN_TIMEOUT_MS = 15; + using namespace esphome::cover; void Tormatic::setup() { @@ -255,32 +259,51 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional<GateStatus> Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (!this->pending_hdr_) { + if (this->available() < sizeof(MessageHeader)) { + return {}; + } + + this->pending_hdr_ = this->read_data_<MessageHeader>(); + if (!this->pending_hdr_) { + return {}; + } + + // Sanity check: valid messages have small payloads (3-4 bytes). A large + // or impossible payload_size means the stream is out of sync (corrupted + // byte, dropped data, etc.). Flush the buffer so we can resync on the + // next request/response cycle. + if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) { + ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size()); + this->pending_hdr_.reset(); + this->drain_rx_(); + return {}; + } + } + + // Wait for all payload bytes to arrive before processing. + if (this->available() < this->pending_hdr_->payload_size()) { return {}; } - auto o_hdr = this->read_data_<MessageHeader>(); - if (!o_hdr) { - ESP_LOGE(TAG, "Timeout reading message header"); - return {}; - } - auto hdr = o_hdr.value(); + auto hdr = *this->pending_hdr_; + this->pending_hdr_.reset(); switch (hdr.type) { case STATUS: { if (hdr.payload_size() != sizeof(StatusReply)) { ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(), sizeof(StatusReply)); + this->drain_rx_(hdr.payload_size()); + return {}; } - // Read a StatusReply requested by update(). auto o_status = this->read_data_<StatusReply>(); if (!o_status) { return {}; } - auto status = o_status.value(); - return status.state; + return o_status->state; } case COMMAND: @@ -343,16 +366,24 @@ template<typename T> optional<T> Tormatic::read_data_() { return obj; } -// Drain up to n amount of bytes from the uart rx buffer. +// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes +// (caller must ensure they are available). When n == 0, poll for 15ms to +// guarantee a full packet time at 9600 baud has elapsed, consuming any +// bytes still in transit. void Tormatic::drain_rx_(uint16_t n) { uint8_t data; - uint16_t count = 0; - while (this->available()) { - this->read_byte(&data); - count++; - - if (n > 0 && count >= n) { - return; + if (n > 0) { + for (uint16_t i = 0; i < n; i++) { + if (!this->read_byte(&data)) { + return; + } + } + } else { + uint32_t start = millis(); + while (millis() - start < DRAIN_TIMEOUT_MS) { + if (this->available()) { + this->read_byte(&data); + } } } } diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 534d4bef141..34483ed6a35 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -43,6 +43,7 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void handle_gate_status_(GateStatus s); uint32_t seq_tx_{0}; + optional<MessageHeader> pending_hdr_{}; GateStatus current_status_{PAUSED}; From dc634b8c7b5218b9b5b0a9c561ffa4a80254bb57 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:04:07 +0200 Subject: [PATCH 1137/2056] [uart] fix baud rate not applied on `load_settings()` for ESP32 (IDF) (#15341) --- .../uart/uart_component_esp_idf.cpp | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 82120b1a5f9..7d02f54b47f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -147,6 +147,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // uart_param_config must be called after uart_driver_install and before any + // other uart_set_*() calls. The driver installation resets the UART peripheral + // registers to their default state, overwriting any previously configured baud + // rate or framing settings. Calling uart_param_config here ensures the requested + // settings are applied after the reset and before pin routing, inversion, and + // threshold configuration. + uart_config_t uart_config = this->get_config_(); + err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; @@ -214,22 +228,15 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install(). auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() + err = uart_set_mode(this->uart_num_, mode); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - uart_config_t uart_config = this->get_config_(); - err = uart_param_config(this->uart_num_, &uart_config); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - #ifdef USE_UART_WAKE_LOOP_ON_RX // Register ISR callback to wake the main loop when UART data arrives. // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to From 514c0c8331c6eebc7e116b85c7d8abe2ecc6526b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 31 Mar 2026 19:06:48 -0400 Subject: [PATCH 1138/2056] [mixer] Fix memory leak in mixer task on stop/start cycles (#15185) --- .../mixer/speaker/mixer_speaker.cpp | 274 +++++++++--------- 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 9d11abb3277..0fabc68c705 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -597,173 +597,173 @@ void MixerSpeaker::audio_mixer_task(void *params) { xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING); - std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( - this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); + { // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task + std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( + this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - if (output_transfer_buffer == nullptr) { - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); + if (output_transfer_buffer == nullptr) { + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); - vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it - } - - output_transfer_buffer->set_sink(this_mixer->output_speaker_); - - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - - bool sent_finished = false; - - // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) - FixedVector<SourceSpeaker *> speakers_with_data; - FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; - speakers_with_data.init(this_mixer->source_speakers_.size()); - transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - - while (true) { - uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); - if (event_group_bits & MIXER_TASK_COMMAND_STOP) { - break; + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } - // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves - output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + output_transfer_buffer->set_sink(this_mixer->output_speaker_); - const uint32_t output_frames_free = - this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - speakers_with_data.clear(); - transfer_buffers_with_data.clear(); + bool sent_finished = false; - for (auto &speaker : this_mixer->source_speakers_) { - if (speaker->is_running() && !speaker->get_pause_state()) { - // Speaker is running and not paused, so it possibly can provide audio data - std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); - if (transfer_buffer.use_count() == 0) { - // No transfer buffer allocated, so skip processing this speaker - continue; - } - speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) + FixedVector<SourceSpeaker *> speakers_with_data; + FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; + speakers_with_data.init(this_mixer->source_speakers_.size()); + transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - if (transfer_buffer->available() > 0) { - // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop - transfer_buffers_with_data.push_back(transfer_buffer); - speakers_with_data.push_back(speaker); + while (true) { + uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + break; + } + + // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves + output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + + const uint32_t output_frames_free = + this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + + speakers_with_data.clear(); + transfer_buffers_with_data.clear(); + + for (auto &speaker : this_mixer->source_speakers_) { + if (speaker->is_running() && !speaker->get_pause_state()) { + // Speaker is running and not paused, so it possibly can provide audio data + std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); + if (transfer_buffer.use_count() == 0) { + // No transfer buffer allocated, so skip processing this speaker + continue; + } + speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + + if (transfer_buffer->available() > 0) { + // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop + transfer_buffers_with_data.push_back(transfer_buffer); + speakers_with_data.push_back(speaker); + } } } - } - if (transfer_buffers_with_data.empty()) { - // No audio available for transferring, block task temporarily - delay(TASK_DELAY_MS); - continue; - } + if (transfer_buffers_with_data.empty()) { + // No audio available for transferring, block task temporarily + delay(TASK_DELAY_MS); + continue; + } - uint32_t frames_to_mix = output_frames_free; + uint32_t frames_to_mix = output_frames_free; - if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { - // Only one speaker has audio data, just copy samples over + if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { + // Only one speaker has audio data, just copy samples over - audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); + audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); - if (active_stream_info.get_sample_rate() == - this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { - // Speaker's sample rate matches the output speaker's, copy directly + if (active_stream_info.get_sample_rate() == + this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { + // Speaker's sample rate matches the output speaker's, copy directly - const uint32_t frames_available_in_buffer = - active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info, - reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); + const uint32_t frames_available_in_buffer = + active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), + active_stream_info, reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); - // Set playback delay for newly contributing source - if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[0]->playback_delay_frames_.store( - this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); - speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + // Set playback delay for newly contributing source + if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[0]->playback_delay_frames_.store( + this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); + speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + } + + // Update source speaker pending frames + speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + + // Update output transfer buffer length and pipeline frame count + output_transfer_buffer->increase_buffer_length( + this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); + this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); + } else { + // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker + if (!this_mixer->output_speaker_->is_stopped()) { + if (!sent_finished) { + this_mixer->output_speaker_->finish(); + sent_finished = true; // Avoid repeatedly sending the finish command + } + } else { + // Speaker has finished writing the current audio, update the stream information and restart the speaker + this_mixer->audio_stream_info_ = + audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, + active_stream_info.get_sample_rate()); + this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); + this_mixer->output_speaker_->start(); + // Reset pipeline frame count since we're starting fresh with a new sample rate + this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); + sent_finished = false; + } + } + } else { + // Determine how many frames to mix + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames( + transfer_buffers_with_data[i]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + } + int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()); + audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); + + // Mix two streams together + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { + mix_audio_samples(primary_buffer, primary_stream_info, + reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), + speakers_with_data[i]->get_audio_stream_info(), + reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); + + if (i != transfer_buffers_with_data.size() - 1) { + // Need to mix more streams together, point primary buffer and stream info to the already mixed output + primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()); + primary_stream_info = this_mixer->audio_stream_info_.value(); + } } - // Update source speaker pending frames - speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + // Get current pipeline depth for delay calculation (before incrementing) + uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - // Update output transfer buffer length and pipeline frame count + // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + // Set playback delay for newly contributing sources + if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); + speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); + } + + speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[i]->decrease_buffer_length( + speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); + } + + // Update output transfer buffer length and pipeline frame count (once, not per source) output_transfer_buffer->increase_buffer_length( this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); - } else { - // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker - if (!this_mixer->output_speaker_->is_stopped()) { - if (!sent_finished) { - this_mixer->output_speaker_->finish(); - sent_finished = true; // Avoid repeatedly sending the finish command - } - } else { - // Speaker has finished writing the current audio, update the stream information and restart the speaker - this_mixer->audio_stream_info_ = - audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, - active_stream_info.get_sample_rate()); - this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); - this_mixer->output_speaker_->start(); - // Reset pipeline frame count since we're starting fresh with a new sample rate - this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - sent_finished = false; - } } - } else { - // Determine how many frames to mix - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - const uint32_t frames_available_in_buffer = - speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - } - int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()); - audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); - - // Mix two streams together - for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { - mix_audio_samples(primary_buffer, primary_stream_info, - reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), - speakers_with_data[i]->get_audio_stream_info(), - reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); - - if (i != transfer_buffers_with_data.size() - 1) { - // Need to mix more streams together, point primary buffer and stream info to the already mixed output - primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()); - primary_stream_info = this_mixer->audio_stream_info_.value(); - } - } - - // Get current pipeline depth for delay calculation (before incrementing) - uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - - // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - // Set playback delay for newly contributing sources - if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); - speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); - } - - speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[i]->decrease_buffer_length( - speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); - } - - // Update output transfer buffer length and pipeline frame count (once, not per source) - output_transfer_buffer->increase_buffer_length( - this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); - this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); } - } - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + } // Reset pipeline frame count since the task is stopping this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - output_transfer_buffer.reset(); - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it From 65051153ac0f7559bd8e271d82fb9685b611164e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 31 Mar 2026 13:26:26 -1000 Subject: [PATCH 1139/2056] [esp32_ble_tracker] Restart BLE scan after OTA failure (#15308) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 6 ++++++ esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 3 +++ 2 files changed, 9 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5a43cf7e49b..6a2834a869b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -82,12 +82,18 @@ void ESP32BLETracker::setup() { #ifdef USE_OTA_STATE_LISTENER void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { if (state == ota::OTA_STARTED) { + this->scan_continuous_before_ota_ = this->scan_continuous_; this->stop_scan(); #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->disconnect(); } #endif + } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { + this->scan_continuous_before_ota_ = false; + this->scan_continuous_ = true; + // Do not restart scanning immediately here; allow loop() to + // safely restart scanning once the scanner and all clients are idle. } } #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 7f1c2b0f7c8..e0e25aca200 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -429,6 +429,9 @@ class ESP32BLETracker : public Component, ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; +#ifdef USE_OTA_STATE_LISTENER + bool scan_continuous_before_ota_{false}; +#endif bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; From 600ca01fd3904fffd3d6092a45c8a3f6a1899c1b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:18:24 +1300 Subject: [PATCH 1140/2056] Bump version to 2026.3.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index d86894435f8..97201d1c445 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.1 +PROJECT_NUMBER = 2026.3.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 52ac7acd228..ebab56193cb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.1" +__version__ = "2026.3.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8f2cf8b8a75ed710559eff51a37097bc2100959a Mon Sep 17 00:00:00 2001 From: Christian H <28529536+nytaros@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:39:41 +0200 Subject: [PATCH 1141/2056] [bmp581_base] Add support for BMP585 (#15277) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/bmp581_base/bmp581_base.cpp | 2 +- esphome/components/bmp581_base/bmp581_base.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c9d250545bc..7a627eee03f 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -126,7 +126,7 @@ void BMP581Component::setup() { } // verify id - if (chip_id != BMP581_ASIC_ID) { + if (chip_id != BMP581_ASIC_ID && chip_id != BMP585_ASIC_ID) { ESP_LOGE(TAG, "Unknown chip ID"); this->error_code_ = ERROR_WRONG_CHIP_ID; diff --git a/esphome/components/bmp581_base/bmp581_base.h b/esphome/components/bmp581_base/bmp581_base.h index c3920512e0d..1a73a91558c 100644 --- a/esphome/components/bmp581_base/bmp581_base.h +++ b/esphome/components/bmp581_base/bmp581_base.h @@ -8,7 +8,8 @@ namespace esphome::bmp581_base { static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) -static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command +static const uint8_t BMP585_ASIC_ID = 0x51; +static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command // BMP581 Register Addresses enum { From 31a70ab29911d646dc602a0df012d73ba6c85f9b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 31 Mar 2026 21:44:54 -0400 Subject: [PATCH 1142/2056] [resampler] Future-proof resampler task to avoid potential memory leaks (#15186) --- .../resampler/speaker/resampler_speaker.cpp | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 1303bc459e5..b737a2d39a0 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -317,57 +317,59 @@ void ResamplerSpeaker::resample_task(void *params) { xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); - std::unique_ptr<audio::AudioResampler> resampler = - make_unique<audio::AudioResampler>(this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS), - this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); + { // Ensure C++ objects fall out of scope for proper cleanup before stopping the task + std::unique_ptr<audio::AudioResampler> resampler = make_unique<audio::AudioResampler>( + this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS), + this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_, - this_resampler->taps_, this_resampler->filters_); + esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_, + this_resampler->taps_, this_resampler->filters_); - if (err == ESP_OK) { - std::shared_ptr<RingBuffer> temp_ring_buffer = - RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); + if (err == ESP_OK) { + std::shared_ptr<RingBuffer> temp_ring_buffer = + RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (!temp_ring_buffer) { - err = ESP_ERR_NO_MEM; - } else { - this_resampler->ring_buffer_ = temp_ring_buffer; - resampler->add_source(this_resampler->ring_buffer_); + if (!temp_ring_buffer) { + err = ESP_ERR_NO_MEM; + } else { + this_resampler->ring_buffer_ = temp_ring_buffer; + resampler->add_source(this_resampler->ring_buffer_); - this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_); - resampler->add_sink(this_resampler->output_speaker_); - } - } - - if (err == ESP_OK) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING); - } else if (err == ESP_ERR_NO_MEM) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); - } else if (err == ESP_ERR_NOT_SUPPORTED) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); - } - - while (err == ESP_OK) { - uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); - - if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { - break; + this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_); + resampler->add_sink(this_resampler->output_speaker_); + } } - // Stop gracefully if the decoder is done - int32_t ms_differential = 0; - audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential); - - if (resampler_state == audio::AudioResamplerState::FINISHED) { - break; - } else if (resampler_state == audio::AudioResamplerState::FAILED) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); - break; + if (err == ESP_OK) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING); + } else if (err == ESP_ERR_NO_MEM) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); + } else if (err == ESP_ERR_NOT_SUPPORTED) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); } + + while (err == ESP_OK) { + uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); + + if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { + break; + } + + // Stop gracefully if the decoder is done + int32_t ms_differential = 0; + audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential); + + if (resampler_state == audio::AudioResamplerState::FINISHED) { + break; + } else if (resampler_state == audio::AudioResamplerState::FAILED) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); + break; + } + } + + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); - resampler.reset(); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it From 212b3e16880808ab7e3af1b6a5f513a9f622b2fb Mon Sep 17 00:00:00 2001 From: Rene Guca <45061891+rguca@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:59:24 +0200 Subject: [PATCH 1143/2056] [cover] move time_based_cover to its own subdirectory (#15313) Co-authored-by: Rene <rene@guca.at> --- esphome/components/time_based/__init__.py | 3 +++ esphome/components/time_based/{cover.py => cover/__init__.py} | 3 ++- esphome/components/time_based/{ => cover}/time_based_cover.cpp | 0 esphome/components/time_based/{ => cover}/time_based_cover.h | 0 4 files changed, 5 insertions(+), 1 deletion(-) rename esphome/components/time_based/{cover.py => cover/__init__.py} (97%) rename esphome/components/time_based/{ => cover}/time_based_cover.cpp (100%) rename esphome/components/time_based/{ => cover}/time_based_cover.h (100%) diff --git a/esphome/components/time_based/__init__.py b/esphome/components/time_based/__init__.py index e69de29bb2d..ce2f453bda7 100644 --- a/esphome/components/time_based/__init__.py +++ b/esphome/components/time_based/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +time_based_ns = cg.esphome_ns.namespace("time_based") diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover/__init__.py similarity index 97% rename from esphome/components/time_based/cover.py rename to esphome/components/time_based/cover/__init__.py index d14332d453f..022b48d2497 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_STOP_ACTION, ) -time_based_ns = cg.esphome_ns.namespace("time_based") +from .. import time_based_ns + TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component) CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/cover/time_based_cover.cpp similarity index 100% rename from esphome/components/time_based/time_based_cover.cpp rename to esphome/components/time_based/cover/time_based_cover.cpp diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/cover/time_based_cover.h similarity index 100% rename from esphome/components/time_based/time_based_cover.h rename to esphome/components/time_based/cover/time_based_cover.h From fbfb5d401f99cf40d60bdced6db36ba00262f27e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:34:29 +0200 Subject: [PATCH 1144/2056] [nextion] Fix memory leak in `reset_()` (#15344) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index bb3e12be508..d141ef79062 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -143,8 +143,17 @@ void Nextion::reset_(bool reset_nextion) { while (this->available()) { // Clear receive buffer this->read_byte(&d); - }; + } + for (auto *entry : this->nextion_queue_) { + if (entry->component != nullptr && entry->component->get_queue_type() == NextionQueueType::NO_RESULT) { + delete entry->component; // NOLINT(cppcoreguidelines-owning-memory) + } + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } this->nextion_queue_.clear(); + for (auto *entry : this->waveform_queue_) { + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } this->waveform_queue_.clear(); } From cc8889628010840dcbc9167e07adf6ec342e00b9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 1 Apr 2026 17:04:22 +0200 Subject: [PATCH 1145/2056] [debug] add peripherals status (#12053) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/debug/debug_zephyr.cpp | 47 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bf87b7ae3dc..d1580dae80d 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -91,6 +91,49 @@ void DebugComponent::log_partition_info_() { flash_area_foreach(fa_cb, nullptr); } +#ifdef ESPHOME_LOG_HAS_VERBOSE +// Check if an nRF peripheral's ENABLE register indicates it is enabled. +// periph: peripheral register prefix (e.g. USBD, UARTE, SPI) +// reg: register block pointer (e.g. NRF_USBD, NRF_UARTE0) +#define NRF_PERIPH_ENABLED(periph, reg) \ + YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos)) + +static void log_peripherals_info() { + // most peripherals are enabled only when in use so ESP_LOGV is enough + ESP_LOGV(TAG, "Peripherals status:"); + ESP_LOGV(TAG, " USBD: %-3s| UARTE0: %-3s| UARTE1: %-3s| UART0: %-3s", // + NRF_PERIPH_ENABLED(USBD, NRF_USBD), NRF_PERIPH_ENABLED(UARTE, NRF_UARTE0), + NRF_PERIPH_ENABLED(UARTE, NRF_UARTE1), NRF_PERIPH_ENABLED(UART, NRF_UART0)); + ESP_LOGV(TAG, " TWIS0: %-3s| TWIS1: %-3s| TWIM0: %-3s| TWIM1: %-3s", // + NRF_PERIPH_ENABLED(TWIS, NRF_TWIS0), NRF_PERIPH_ENABLED(TWIS, NRF_TWIS1), + NRF_PERIPH_ENABLED(TWIM, NRF_TWIM0), NRF_PERIPH_ENABLED(TWIM, NRF_TWIM1)); + ESP_LOGV(TAG, " TWI0: %-3s| TWI1: %-3s| COMP: %-3s| CCM: %-3s", // + NRF_PERIPH_ENABLED(TWI, NRF_TWI0), NRF_PERIPH_ENABLED(TWI, NRF_TWI1), NRF_PERIPH_ENABLED(COMP, NRF_COMP), + NRF_PERIPH_ENABLED(CCM, NRF_CCM)); + ESP_LOGV(TAG, " PDM: %-3s| SPIS0: %-3s| SPIS1: %-3s| SPIS2: %-3s", // + NRF_PERIPH_ENABLED(PDM, NRF_PDM), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS0), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS1), + NRF_PERIPH_ENABLED(SPIS, NRF_SPIS2)); + ESP_LOGV(TAG, " SPIM0: %-3s| SPIM1: %-3s| SPIM2: %-3s| SPIM3: %-3s", // + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM0), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM1), + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM2), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM3)); + ESP_LOGV(TAG, " SPI0: %-3s| SPI1: %-3s| SPI2: %-3s| SAADC: %-3s", // + NRF_PERIPH_ENABLED(SPI, NRF_SPI0), NRF_PERIPH_ENABLED(SPI, NRF_SPI1), NRF_PERIPH_ENABLED(SPI, NRF_SPI2), + NRF_PERIPH_ENABLED(SAADC, NRF_SAADC)); + ESP_LOGV(TAG, " QSPI: %-3s| QDEC: %-3s| LPCOMP: %-3s| I2S: %-3s", // + NRF_PERIPH_ENABLED(QSPI, NRF_QSPI), NRF_PERIPH_ENABLED(QDEC, NRF_QDEC), + NRF_PERIPH_ENABLED(LPCOMP, NRF_LPCOMP), NRF_PERIPH_ENABLED(I2S, NRF_I2S)); + ESP_LOGV(TAG, " PWM0: %-3s| PWM1: %-3s| PWM2: %-3s| PWM3: %-3s", // + NRF_PERIPH_ENABLED(PWM, NRF_PWM0), NRF_PERIPH_ENABLED(PWM, NRF_PWM1), NRF_PERIPH_ENABLED(PWM, NRF_PWM2), + NRF_PERIPH_ENABLED(PWM, NRF_PWM3)); + ESP_LOGV(TAG, " AAR: %-3s| QSPI deep power-down:%-3s| CRYPTOCELL: %-3s", NRF_PERIPH_ENABLED(AAR, NRF_AAR), + YESNO((NRF_QSPI->IFCONFIG0 & QSPI_IFCONFIG0_DPMENABLE_Msk) == + (QSPI_IFCONFIG0_DPMENABLE_Enable << QSPI_IFCONFIG0_DPMENABLE_Pos)), + YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) == + (CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos))); +} +#undef NRF_PERIPH_ENABLED +#endif + static const char *regout0_to_str(uint32_t value) { switch (value) { case (UICR_REGOUT0_VOUT_DEFAULT): @@ -354,7 +397,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> }; ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); - +#ifdef ESPHOME_LOG_HAS_VERBOSE + log_peripherals_info(); +#endif return pos; } From f33fd047ee5589fa7d21445ed42bb541fe06e92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Pereira?= <goncalo_pereira@outlook.pt> Date: Wed, 1 Apr 2026 17:09:22 +0100 Subject: [PATCH 1146/2056] [hdc2080] Add support for HDC2080 sensor (#9331) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Big Mike <mikelawrence@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc2080/__init__.py | 1 + esphome/components/hdc2080/hdc2080.cpp | 71 +++++++++++++++++++ esphome/components/hdc2080/hdc2080.h | 24 +++++++ esphome/components/hdc2080/sensor.py | 57 +++++++++++++++ tests/components/hdc2080/common.yaml | 7 ++ tests/components/hdc2080/test.esp32-idf.yaml | 4 ++ .../components/hdc2080/test.esp8266-ard.yaml | 4 ++ tests/components/hdc2080/test.rp2040-ard.yaml | 4 ++ 9 files changed, 173 insertions(+) create mode 100644 esphome/components/hdc2080/__init__.py create mode 100644 esphome/components/hdc2080/hdc2080.cpp create mode 100644 esphome/components/hdc2080/hdc2080.h create mode 100644 esphome/components/hdc2080/sensor.py create mode 100644 tests/components/hdc2080/common.yaml create mode 100644 tests/components/hdc2080/test.esp32-idf.yaml create mode 100644 tests/components/hdc2080/test.esp8266-ard.yaml create mode 100644 tests/components/hdc2080/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8d297d7b072..03f41618af1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -217,6 +217,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc2080/* @G-Pereira @jesserockz esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch diff --git a/esphome/components/hdc2080/__init__.py b/esphome/components/hdc2080/__init__.py new file mode 100644 index 00000000000..341ea610489 --- /dev/null +++ b/esphome/components/hdc2080/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@G-Pereira", "@jesserockz"] diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp new file mode 100644 index 00000000000..dcb207e099b --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -0,0 +1,71 @@ +#include "hdc2080.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::hdc2080 { + +static const char *const TAG = "hdc2080"; + +// Register map (Table 8-6) +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; // Temperature [7:0] +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; // Temperature [15:8] +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; // Humidity [7:0] +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; // Humidity [15:8] +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; // Soft Reset and Interrupt Configuration +static constexpr uint8_t REG_MEASUREMENT_CONFIGURATION = 0x0F; + +// Measurement register (0x0F) bit fields +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: start measurement +static constexpr uint8_t MEAS_CONF_TEMP = 0x02; // Bits 2:1 = 01: temperature only +static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only + +void HDC2080Component::setup() { + const uint8_t data = 0x00; // automatic measurement mode disabled, heater off + if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } +} + +void HDC2080Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2080:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void HDC2080Component::update() { + uint8_t data = MEAS_TRIG; // 14-bit resolution, measure both, start + if (this->temperature_sensor_ != nullptr && this->humidity_sensor_ == nullptr) { + data = MEAS_TRIG | MEAS_CONF_TEMP; + } else if (this->temperature_sensor_ == nullptr && this->humidity_sensor_ != nullptr) { + data = MEAS_TRIG | MEAS_CONF_HUM; + } + if (this->write_register(REG_MEASUREMENT_CONFIGURATION, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + // wait for conversion to complete 2ms should be enough, more is fine + this->set_timeout(5, [this]() { + uint8_t raw_data[4]; + if (this->read_register(REG_TEMPERATURE_LOW, raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + float temp = encode_uint16(raw_data[1], raw_data[0]) * (165.0f / 65536.0f) - 40.5f; + this->temperature_sensor_->publish_state(temp); + } + if (this->humidity_sensor_ != nullptr) { + float humidity = encode_uint16(raw_data[3], raw_data[2]) * (100.0f / 65536.0f); + this->humidity_sensor_->publish_state(humidity); + } + }); +} + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/hdc2080.h b/esphome/components/hdc2080/hdc2080.h new file mode 100644 index 00000000000..daa10d371d6 --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome::hdc2080 { + +class HDC2080Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + void update() override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/sensor.py b/esphome/components/hdc2080/sensor.py new file mode 100644 index 00000000000..777fc51cbaf --- /dev/null +++ b/esphome/components/hdc2080/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2080_ns = cg.esphome_ns.namespace("hdc2080") +HDC2080Component = hdc2080_ns.class_( + "HDC2080Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2080Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) + .add_extra(cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_HUMIDITY)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) diff --git a/tests/components/hdc2080/common.yaml b/tests/components/hdc2080/common.yaml new file mode 100644 index 00000000000..cb14cb183b5 --- /dev/null +++ b/tests/components/hdc2080/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc2080/test.esp32-idf.yaml b/tests/components/hdc2080/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/hdc2080/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.esp8266-ard.yaml b/tests/components/hdc2080/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/hdc2080/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.rp2040-ard.yaml b/tests/components/hdc2080/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/hdc2080/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From ea609d3552fe0685a68a97362abc5658f203837b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 07:09:04 -1000 Subject: [PATCH 1147/2056] [runtime_stats] Store stats inline on Component to eliminate std::map lookup (#15345) --- .../runtime_stats/runtime_stats.cpp | 76 +++++++++---------- .../components/runtime_stats/runtime_stats.h | 75 +----------------- esphome/core/application.h | 9 +++ esphome/core/component.cpp | 12 +-- esphome/core/component.h | 42 ++++++++++ esphome/core/defines.h | 1 + 6 files changed, 92 insertions(+), 123 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index cb28acc96c6..06714b5a446 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -2,6 +2,7 @@ #ifdef USE_RUNTIME_STATS +#include "esphome/core/application.h" #include "esphome/core/component.h" #include <algorithm> @@ -13,20 +14,16 @@ RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_ global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) { - if (component == nullptr) - return; - - // Record stats using component pointer as key - this->component_stats_[component].record_time(duration_us); -} - void RuntimeStatsCollector::log_stats_() { - // First pass: count active components + auto &components = App.components_; + + // Single pass: collect active components into stack buffer + SmallBufferWithHeapFallback<256, Component *> buffer(components.size()); + Component **sorted = buffer.get(); size_t count = 0; - for (const auto &it : this->component_stats_) { - if (it.second.get_period_count() > 0) { - count++; + for (auto *component : components) { + if (component->runtime_stats_.period_count > 0) { + sorted[count++] = component; } } @@ -39,61 +36,58 @@ void RuntimeStatsCollector::log_stats_() { return; } - // Stack buffer sized to actual active count (up to 256 components), heap fallback for larger - SmallBufferWithHeapFallback<256, Component *> buffer(count); - Component **sorted = buffer.get(); - - // Second pass: fill buffer with active components - size_t idx = 0; - for (const auto &it : this->component_stats_) { - if (it.second.get_period_count() > 0) { - sorted[idx++] = it.first; - } - } - // Sort by period runtime (descending) - std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_period_time_us() > this->component_stats_[b].get_period_time_us(); - }); + std::sort(sorted, sorted + count, compare_period_time); // Log top components by period runtime for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), - stats.get_period_avg_time_us() / 1000.0f, stats.get_period_max_time_us() / 1000.0f, - stats.get_period_time_us() / 1000.0f); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, + stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, + stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); } // Log total stats since boot (only for active components - idle ones haven't changed) ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); // Re-sort by total runtime for all-time stats - std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_total_time_us() > this->component_stats_[b].get_total_time_us(); - }); + std::sort(sorted, sorted + count, compare_total_time); for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), - stats.get_total_avg_time_us() / 1000.0f, stats.get_total_max_time_us() / 1000.0f, - stats.get_total_time_us() / 1000.0); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, + stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); } + + // Reset period stats + for (auto *component : components) { + component->runtime_stats_.reset_period(); + } +} + +bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) { + return a->runtime_stats_.period_time_us > b->runtime_stats_.period_time_us; +} + +bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) { + return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us; } void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { if ((int32_t) (current_time - this->next_log_time_) >= 0) { 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) +runtime_stats::RuntimeStatsCollector + *global_runtime_stats = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; } // namespace esphome diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 303d895985f..3c2c9f78add 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -4,11 +4,8 @@ #ifdef USE_RUNTIME_STATS -#include <map> #include <cstdint> -#include <cstring> #include "esphome/core/hal.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -19,64 +16,6 @@ namespace runtime_stats { static const char *const TAG = "runtime_stats"; -class ComponentRuntimeStats { - public: - ComponentRuntimeStats() - : period_count_(0), - period_time_us_(0), - period_max_time_us_(0), - total_count_(0), - total_time_us_(0), - total_max_time_us_(0) {} - - void record_time(uint32_t duration_us) { - // Update period counters - this->period_count_++; - this->period_time_us_ += duration_us; - if (duration_us > this->period_max_time_us_) - this->period_max_time_us_ = duration_us; - - // Update total counters (uint64_t to avoid overflow — uint32_t would overflow after ~10 hours) - this->total_count_++; - this->total_time_us_ += duration_us; - if (duration_us > this->total_max_time_us_) - this->total_max_time_us_ = duration_us; - } - - void reset_period_stats() { - this->period_count_ = 0; - this->period_time_us_ = 0; - this->period_max_time_us_ = 0; - } - - // Period stats (reset each logging interval) - uint32_t get_period_count() const { return this->period_count_; } - uint32_t get_period_time_us() const { return this->period_time_us_; } - uint32_t get_period_max_time_us() const { return this->period_max_time_us_; } - float get_period_avg_time_us() const { - return this->period_count_ > 0 ? this->period_time_us_ / static_cast<float>(this->period_count_) : 0.0f; - } - - // Total stats (persistent until reboot, uint64_t to avoid overflow) - uint32_t get_total_count() const { return this->total_count_; } - uint64_t get_total_time_us() const { return this->total_time_us_; } - uint32_t get_total_max_time_us() const { return this->total_max_time_us_; } - float get_total_avg_time_us() const { - return this->total_count_ > 0 ? this->total_time_us_ / static_cast<float>(this->total_count_) : 0.0f; - } - - protected: - // Period stats (reset each logging interval) - uint32_t period_count_; - uint32_t period_time_us_; - uint32_t period_max_time_us_; - - // Total stats (persistent until reboot) - uint32_t total_count_; - uint64_t total_time_us_; - uint32_t total_max_time_us_; -}; - class RuntimeStatsCollector { public: RuntimeStatsCollector(); @@ -87,23 +26,15 @@ class RuntimeStatsCollector { } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_us); - // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); protected: void log_stats_(); + // Static comparators — member functions have friend access, lambdas do not + static bool compare_period_time(Component *a, Component *b); + static bool compare_total_time(Component *a, Component *b); - void reset_stats_() { - for (auto &it : this->component_stats_) { - it.second.reset_period_stats(); - } - } - - // Map from component to its stats - // We use Component* as the key since each component is unique - std::map<Component *, ComponentRuntimeStats> component_stats_; uint32_t log_interval_; uint32_t next_log_time_{0}; }; diff --git a/esphome/core/application.h b/esphome/core/application.h index 06ff30e81f6..6cc61bc9547 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -130,6 +130,12 @@ bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redund #endif } // namespace esphome::socket +#ifdef USE_RUNTIME_STATS +namespace esphome::runtime_stats { +class RuntimeStatsCollector; +} // namespace esphome::runtime_stats +#endif + // Forward declarations for friend access from codegen-generated setup() void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests @@ -590,6 +596,9 @@ class Application { friend Component; #if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) friend bool socket::socket_ready_fd(int fd, bool loop_monitored); +#endif +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; #endif friend void ::setup(); friend void ::original_setup(); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00a36fce3d8..955596ce956 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,9 +9,6 @@ #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 { @@ -524,13 +521,8 @@ WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t block #ifdef USE_RUNTIME_STATS void WarnIfComponentBlockingGuard::record_runtime_stats_() { - // Use micros() for accurate sub-millisecond timing. millis() has insufficient - // resolution — most components complete in microseconds but millis() only has - // 1ms granularity, so results were essentially random noise. - if (global_runtime_stats != nullptr) { - uint32_t duration_us = micros() - this->started_us_; - global_runtime_stats->record_component_time(this->component_, duration_us); - } + uint32_t duration_us = micros() - this->started_us_; + this->component_->runtime_stats_.record_time(duration_us); } #endif diff --git a/esphome/core/component.h b/esphome/core/component.h index c390a205f0f..c5a331ee298 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -20,6 +20,12 @@ namespace esphome { // Forward declaration for LogString struct LogString; +#ifdef USE_RUNTIME_STATS +namespace runtime_stats { +class RuntimeStatsCollector; +} // namespace runtime_stats +#endif + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -92,6 +98,37 @@ inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds /// Weak default returns "<unknown>" so builds without codegen still link. const LogString *component_source_lookup(uint8_t index); +#ifdef USE_RUNTIME_STATS +/// Inline runtime statistics — eliminates std::map lookup on every loop iteration. +/// Only present when USE_RUNTIME_STATS is defined (profiling builds). +struct ComponentRuntimeStats { + // Period stats (reset each logging interval) + uint32_t period_count{0}; + uint32_t period_time_us{0}; + uint32_t period_max_time_us{0}; + // Total stats (persistent until reboot, uint64_t to avoid overflow) + uint32_t total_count{0}; + uint64_t total_time_us{0}; + uint32_t total_max_time_us{0}; + + void record_time(uint32_t duration_us) { + this->period_count++; + this->period_time_us += duration_us; + if (duration_us > this->period_max_time_us) + this->period_max_time_us = duration_us; + this->total_count++; + this->total_time_us += duration_us; + if (duration_us > this->total_max_time_us) + this->total_max_time_us = duration_us; + } + void reset_period() { + this->period_count = 0; + this->period_time_us = 0; + this->period_max_time_us = 0; + } +}; +#endif + class Component { public: /** Where the component's initialization should happen. @@ -529,6 +566,11 @@ class Component { /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; + friend class WarnIfComponentBlockingGuard; + ComponentRuntimeStats runtime_stats_; +#endif }; /** This class simplifies creating components that periodically check a state. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7259167a527..23e65f55bc0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -180,6 +180,7 @@ #define USE_RUNTIME_IMAGE_BMP #define USE_RUNTIME_IMAGE_PNG #define USE_RUNTIME_IMAGE_JPEG +#define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER From 2e3ea2152d103a21b0cfbe5018bd3a47aabe285a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:13:23 -0400 Subject: [PATCH 1148/2056] [esp32_camera] Bump esp32-camera to v2.1.6 (#15349) --- esphome/components/camera_encoder/__init__.py | 2 +- esphome/components/esp32_camera/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index a0c59a517a7..3bbeae78356 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None: buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: - add_idf_component(name="espressif/esp32-camera", ref="2.1.5") + add_idf_component(name="espressif/esp32-camera", ref="2.1.6") cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER") var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index afab849a7c6..66af321e4e2 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -400,7 +400,7 @@ async def to_code(config): if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") - add_idf_component(name="espressif/esp32-camera", ref="2.1.5") + add_idf_component(name="espressif/esp32-camera", ref="2.1.6") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c44853969ed..462af5d1e70 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.1.5 + version: 2.1.6 espressif/mdns: version: 1.10.0 espressif/esp_wifi_remote: From bdce47e764497cd75051f885ffd2916fdf980459 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:39:51 +1000 Subject: [PATCH 1149/2056] [lvgl] Fixes #4 (#15334) --- esphome/components/lvgl/__init__.py | 22 +++++++++++++++++----- esphome/components/lvgl/lvgl_esphome.h | 8 +++++--- esphome/components/lvgl/schemas.py | 1 + tests/components/lvgl/lvgl-package.yaml | 8 ++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 736fba759f3..3b4f150699b 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -380,8 +380,10 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending - lv_image_formats = {"RGB565", "ARGB8888"} + for use in helpers.lv_uses: + df.add_define(f"LV_USE_{use.upper()}") + cg.add_define(f"USE_LVGL_{use.upper()}") + if { "transform_rotation", "transform_scale", @@ -389,9 +391,18 @@ async def to_code(configs): "transform_scale_y", } & styles_used: df.add_define("LV_COLOR_SCREEN_TRANSP", "1") - for use in helpers.lv_uses: - df.add_define(f"LV_USE_{use.upper()}") - cg.add_define(f"USE_LVGL_{use.upper()}") + + # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending + lv_image_formats = {"RGB565", "ARGB8888"} + if { + "drop_shadow_color", + "drop_shadow_offset_x", + "drop_shadow_offset_y", + "drop_shadow_opa", + "drop_shadow_quality", + "drop_shadow_radius", + } & styles_used: + lv_image_formats.add("A8") for image_id in lv_images_used: await cg.get_variable(image_id) @@ -410,6 +421,7 @@ async def to_code(configs): lv_image_formats.add("RGB888") for fmt in lv_image_formats: df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") + lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 21d1e0d4176..8d139b23cb1 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -74,11 +74,13 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { #if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. -inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { - lv_image_set_src(obj, image->get_lv_image_dsc()); +inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); } + +inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); } -inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { +inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } #endif // USE_LVGL_IMAGE diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 9c9504f05f1..4f1473b6521 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -168,6 +168,7 @@ BASE_PROPS = { "bg_main_opa": lvalid.opacity, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, + "bitmap_mask_src": lvalid.lv_image, "blend_mode": df.LvConstant( "LV_BLEND_MODE_", "NORMAL", diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b8c9a1809ea..abc66ef587b 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -511,6 +511,7 @@ lvgl: image: src: cat_image align: top_left + bitmap_mask_src: alert on_click: - lvgl.widget.focus: spin_up - lvgl.widget.focus: next @@ -1189,6 +1190,13 @@ image: type: BINARY transparency: chroma_key + - id: alert + file: $component_dir/logo-text.svg + type: grayscale + resize: 100x100 + invert_alpha: true + transparency: alpha_channel + color: - id: light_blue hex: "3340FF" From 5cdbbd48873b0cfed261b2c19081fd42e99967a0 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Wed, 1 Apr 2026 23:48:47 +0200 Subject: [PATCH 1150/2056] [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 1) (#15315) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- CODEOWNERS | 1 + .../components/mitsubishi_cn105/__init__.py | 0 .../components/mitsubishi_cn105/climate.py | 41 +++++++++++++++++++ .../mitsubishi_cn105/mitsubishi_cn105.cpp | 7 ++++ .../mitsubishi_cn105/mitsubishi_cn105.h | 19 +++++++++ .../mitsubishi_cn105_climate.cpp | 28 +++++++++++++ .../mitsubishi_cn105_climate.h | 27 ++++++++++++ tests/components/mitsubishi_cn105/common.yaml | 4 ++ .../mitsubishi_cn105/test.esp32-idf.yaml | 4 ++ .../mitsubishi_cn105/test.esp8266-ard.yaml | 4 ++ .../mitsubishi_cn105/test.rp2040-ard.yaml | 4 ++ 11 files changed, 139 insertions(+) create mode 100644 esphome/components/mitsubishi_cn105/__init__.py create mode 100644 esphome/components/mitsubishi_cn105/climate.py create mode 100644 esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp create mode 100644 esphome/components/mitsubishi_cn105/mitsubishi_cn105.h create mode 100644 esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp create mode 100644 esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h create mode 100644 tests/components/mitsubishi_cn105/common.yaml create mode 100644 tests/components/mitsubishi_cn105/test.esp32-idf.yaml create mode 100644 tests/components/mitsubishi_cn105/test.esp8266-ard.yaml create mode 100644 tests/components/mitsubishi_cn105/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 03f41618af1..fffe5ce91c9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -331,6 +331,7 @@ esphome/components/mipi_dsi/* @clydebarrow esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey +esphome/components/mitsubishi_cn105/* @crnjan esphome/components/mixer/speaker/* @kahrendt esphome/components/mlx90393/* @functionpointer esphome/components/mlx90614/* @jesserockz diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/mitsubishi_cn105/climate.py b/esphome/components/mitsubishi_cn105/climate.py new file mode 100644 index 00000000000..5ea72d4cd22 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/climate.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import climate, uart +import esphome.config_validation as cv +from esphome.const import CONF_UPDATE_INTERVAL +from esphome.types import ConfigType + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["climate"] +CODEOWNERS = ["@crnjan"] + +mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105") + +MitsubishiCN105Climate = mitsubishi_ns.class_( + "MitsubishiCN105Climate", + climate.Climate, + cg.Component, + uart.UARTDevice, +) + +CONFIG_SCHEMA = ( + climate.climate_schema(MitsubishiCN105Climate) + .extend(uart.UART_DEVICE_SCHEMA) + .extend({cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval}) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + uart.final_validate_device_schema( + "mitsubishi_cn105", + require_rx=True, + require_tx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, + ) +) + + +async def to_code(config: ConfigType) -> None: + var = await climate.new_climate(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp new file mode 100644 index 00000000000..35ab405b2d2 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -0,0 +1,7 @@ +#include "mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105 { + +static const char *const TAG = "mitsubishi_cn105.driver"; + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h new file mode 100644 index 00000000000..6018dddbffa --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/uart/uart.h" + +namespace esphome::mitsubishi_cn105 { + +class MitsubishiCN105 { + public: + explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} + + uint32_t get_update_interval() const { return this->update_interval_ms_; } + void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } + + protected: + uart::UARTDevice &device_; + uint32_t update_interval_ms_{1000}; +}; + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp new file mode 100644 index 00000000000..6d50296c8fd --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -0,0 +1,28 @@ +#include "mitsubishi_cn105_climate.h" +#include "esphome/core/log.h" + +namespace esphome::mitsubishi_cn105 { + +static const char *const TAG = "mitsubishi_cn105.climate"; + +void MitsubishiCN105Climate::dump_config() { + LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); + ESP_LOGCONFIG(TAG, + " Update interval: %" PRIu32 " ms\n" + " UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u", + this->hp_.get_update_interval(), this->parent_->get_baud_rate(), this->parent_->get_data_bits(), + LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits()); +} + +void MitsubishiCN105Climate::setup() {} + +void MitsubishiCN105Climate::loop() {} + +climate::ClimateTraits MitsubishiCN105Climate::traits() { + climate::ClimateTraits traits; + return traits; +} + +void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {} + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h new file mode 100644 index 00000000000..08b482025ff --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105 { + +class MitsubishiCN105Climate : public climate::Climate, public Component, public uart::UARTDevice { + public: + explicit MitsubishiCN105Climate() : hp_(*this) {} + + void setup() override; + void loop() override; + void dump_config() override; + + climate::ClimateTraits traits() override; + void control(const climate::ClimateCall &call) override; + + void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } + + protected: + MitsubishiCN105 hp_; +}; + +} // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml new file mode 100644 index 00000000000..e885ceef817 --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -0,0 +1,4 @@ +climate: + - platform: mitsubishi_cn105 + name: "AC Test" + uart_id: uart_bus diff --git a/tests/components/mitsubishi_cn105/test.esp32-idf.yaml b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml new file mode 100644 index 00000000000..ac63cf987f3 --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml new file mode 100644 index 00000000000..9f2f350b46d --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml new file mode 100644 index 00000000000..4363d6eee82 --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml + +<<: !include common.yaml From b5c4449a161c2fc83b4e7b5150b50dcc6ef1ea71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:11:44 -1000 Subject: [PATCH 1151/2056] Bump pillow from 12.1.1 to 12.2.0 (#15361) 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 8ad5528c958..dd206000976 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==12.1.1 +pillow==12.2.0 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 From eefbb42be477d1ed2e80c251859a4588df61c47b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:16:56 +1000 Subject: [PATCH 1152/2056] [lvgl] Add missing event names (#15362) --- esphome/components/lvgl/defines.py | 75 +++++++++++++--- tests/components/lvgl/lvgl-package.yaml | 108 ++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 11 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index dd51a2f5199..500ccb608a7 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -251,22 +251,75 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ ] LV_EVENT_MAP = { - "PRESS": "PRESSED", - "SHORT_CLICK": "SHORT_CLICKED", + "ALL_EVENTS": "ALL", + "CANCEL": "CANCEL", + "CHANGE": "VALUE_CHANGED", + "CHILD_CHANGE": "CHILD_CHANGED", + "CHILD_CREATE": "CHILD_CREATED", + "CHILD_DELETE": "CHILD_DELETED", + "CLICK": "CLICKED", + "COLOR_FORMAT_CHANGE": "COLOR_FORMAT_CHANGED", + "COVER_CHECK": "COVER_CHECK", + "CREATE": "CREATE", + "DEFOCUS": "DEFOCUSED", + "DELETE": "DELETE", + "DOUBLE_CLICK": "DOUBLE_CLICKED", + "DRAW_MAIN": "DRAW_MAIN", + "DRAW_MAIN_BEGIN": "DRAW_MAIN_BEGIN", + "DRAW_MAIN_END": "DRAW_MAIN_END", + "DRAW_POST": "DRAW_POST", + "DRAW_POST_BEGIN": "DRAW_POST_BEGIN", + "DRAW_POST_END": "DRAW_POST_END", + "DRAW_TASK_ADD": "DRAW_TASK_ADDED", + "FLUSH_FINISH": "FLUSH_FINISH", + "FLUSH_START": "FLUSH_START", + "FLUSH_WAIT_FINISH": "FLUSH_WAIT_FINISH", + "FLUSH_WAIT_START": "FLUSH_WAIT_START", + "FOCUS": "FOCUSED", + "GESTURE": "GESTURE", + "GET_SELF_SIZE": "GET_SELF_SIZE", + "HIT_TEST": "HIT_TEST", + "HOVER_LEAVE": "HOVER_LEAVE", + "HOVER_OVER": "HOVER_OVER", + "INDEV_RESET": "INDEV_RESET", + "INSERT": "INSERT", + "INVALIDATE_AREA": "INVALIDATE_AREA", + "KEY": "KEY", + "LAYOUT_CHANGE": "LAYOUT_CHANGED", + "LEAVE": "LEAVE", "LONG_PRESS": "LONG_PRESSED", "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT", - "CLICK": "CLICKED", + "PRESS": "PRESSED", + "PRESS_LOST": "PRESS_LOST", + "PRESSING": "PRESSING", + "READY": "READY", + "REFRESH": "REFRESH", + "REFR_EXT_DRAW_SIZE": "REFR_EXT_DRAW_SIZE", + "REFR_READY": "REFR_READY", + "REFR_REQUEST": "REFR_REQUEST", + "REFR_START": "REFR_START", "RELEASE": "RELEASED", + "RENDER_READY": "RENDER_READY", + "RENDER_START": "RENDER_START", + "RESOLUTION_CHANGE": "RESOLUTION_CHANGED", + "ROTARY": "ROTARY", + "SCREEN_LOAD": "SCREEN_LOADED", + "SCREEN_LOAD_START": "SCREEN_LOAD_START", + "SCREEN_UNLOAD": "SCREEN_UNLOADED", + "SCREEN_UNLOAD_START": "SCREEN_UNLOAD_START", + "SCROLL": "SCROLL", "SCROLL_BEGIN": "SCROLL_BEGIN", "SCROLL_END": "SCROLL_END", - "SCROLL": "SCROLL", - "FOCUS": "FOCUSED", - "DEFOCUS": "DEFOCUSED", - "READY": "READY", - "CANCEL": "CANCEL", - "ALL_EVENTS": "ALL", - "CHANGE": "VALUE_CHANGED", - "GESTURE": "GESTURE", + "SCROLL_THROW_BEGIN": "SCROLL_THROW_BEGIN", + "SHORT_CLICK": "SHORT_CLICKED", + "SINGLE_CLICK": "SINGLE_CLICKED", + "SIZE_CHANGE": "SIZE_CHANGED", + "STATE_CHANGE": "STATE_CHANGED", + "STYLE_CHANGE": "STYLE_CHANGED", + "TRIPLE_CLICK": "TRIPLE_CLICKED", + "UPDATE_LAYOUT_COMPLETE": "UPDATE_LAYOUT_COMPLETED", + "VSYNC": "VSYNC", + "VSYNC_REQUEST": "VSYNC_REQUEST", } LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index abc66ef587b..3a6af93b640 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -590,6 +590,114 @@ lvgl: logger.log: Button clicked on_long_press_repeat: logger.log: Button clicked + on_pressing: + logger.log: Button pressing + on_press_lost: + logger.log: Button press lost + on_single_click: + logger.log: Button single clicked + on_double_click: + logger.log: Button double clicked + on_triple_click: + logger.log: Button triple clicked + on_scroll_throw_begin: + logger.log: Scroll throw begin + on_gesture: + logger.log: Gesture detected + on_key: + logger.log: Key event + on_rotary: + logger.log: Rotary event + on_leave: + logger.log: Leave event + on_hit_test: + logger.log: Hit test + on_indev_reset: + logger.log: Indev reset + on_hover_over: + logger.log: Hover over + on_hover_leave: + logger.log: Hover leave + on_cover_check: + logger.log: Cover check + on_refr_ext_draw_size: + logger.log: Refr ext draw size + on_draw_main_begin: + logger.log: Draw main begin + on_draw_main: + logger.log: Draw main + on_draw_main_end: + logger.log: Draw main end + on_draw_post_begin: + logger.log: Draw post begin + on_draw_post: + logger.log: Draw post + on_draw_post_end: + logger.log: Draw post end + on_draw_task_add: + logger.log: Draw task add + on_insert: + logger.log: Insert event + on_refresh: + logger.log: Refresh event + on_state_change: + logger.log: State changed + on_create: + logger.log: Create event + on_delete: + logger.log: Delete event + on_child_change: + logger.log: Child changed + on_child_create: + logger.log: Child created + on_child_delete: + logger.log: Child deleted + on_screen_unload_start: + logger.log: Screen unload start + on_screen_load_start: + logger.log: Screen load start + on_screen_load: + logger.log: Screen loaded + on_screen_unload: + logger.log: Screen unloaded + on_size_change: + logger.log: Size changed + on_style_change: + logger.log: Style changed + on_layout_change: + logger.log: Layout changed + on_get_self_size: + logger.log: Get self size + on_invalidate_area: + logger.log: Invalidate area + on_resolution_change: + logger.log: Resolution changed + on_color_format_change: + logger.log: Color format changed + on_refr_request: + logger.log: Refresh request + on_refr_start: + logger.log: Refresh start + on_refr_ready: + logger.log: Refresh ready + on_render_start: + logger.log: Render start + on_render_ready: + logger.log: Render ready + on_flush_start: + logger.log: Flush start + on_flush_finish: + logger.log: Flush finish + on_flush_wait_start: + logger.log: Flush wait start + on_flush_wait_finish: + logger.log: Flush wait finish + on_update_layout_complete: + logger.log: Update layout complete + on_vsync: + logger.log: Vsync + on_vsync_request: + logger.log: Vsync request - led: id: lv_led color: 0x00FF00 From 27c662e73faf6583179084d6a179816751d8d170 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 16:11:50 -1000 Subject: [PATCH 1153/2056] [bluetooth_proxy] Replace loop() with set_interval for advertisement flushing (#15347) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 48 ++++++------------- .../bluetooth_proxy/bluetooth_proxy.h | 17 +++++-- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 87206996b27..c69163b1f74 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -30,6 +30,19 @@ void BluetoothProxy::setup() { this->configured_scan_active_ = this->parent_->get_scan_active(); this->parent_->add_scanner_state_listener(this); + + this->set_interval(100, [this]() { + if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) { + this->flush_pending_advertisements_(); + return; + } + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; + if (connection->get_address() != 0 && !connection->disconnect_pending()) { + connection->disconnect(); + } + } + }); } void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { @@ -101,25 +114,15 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) { - this->flush_pending_advertisements(); + this->flush_pending_advertisements_(); } } return true; } -void BluetoothProxy::flush_pending_advertisements() { - if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() || - this->api_connection_ == nullptr) - return; - - // Send the message - this->api_connection_->send_message(this->response_); - +void BluetoothProxy::log_advertisement_flush_() { ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); - - // Reset the length for the next batch - this->response_.advertisements_len = 0; } void BluetoothProxy::dump_config() { @@ -130,27 +133,6 @@ void BluetoothProxy::dump_config() { YESNO(this->active_), this->connection_count_); } -void BluetoothProxy::loop() { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { - for (uint8_t i = 0; i < this->connection_count_; i++) { - auto *connection = this->connections_[i]; - if (connection->get_address() != 0 && !connection->disconnect_pending()) { - connection->disconnect(); - } - } - return; - } - - // Flush any pending BLE advertisements that have been accumulated but not yet sent - uint32_t now = App.get_loop_component_start_time(); - - // Flush accumulated advertisements every 100ms - if (now - this->last_advertisement_flush_time_ >= 100) { - this->flush_pending_advertisements(); - this->last_advertisement_flush_time_ = now; - } -} - esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f1b723e7192..6680ab0e840 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -65,8 +65,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; - void loop() override; - void flush_pending_advertisements(); esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { @@ -150,6 +148,18 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, protected: void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); + /// Caller must ensure api_connection_ is non-null and API server is connected. + void flush_pending_advertisements_() { + if (this->response_.advertisements_len == 0) + return; + this->api_connection_->send_message(this->response_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->log_advertisement_flush_(); +#endif + this->response_.advertisements_len = 0; + } + void log_advertisement_flush_(); + BluetoothConnection *get_connection_(uint64_t address, bool reserve); void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state); void log_connection_info_(BluetoothConnection *connection, const char *message); @@ -166,9 +176,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, // BLE advertisement batching api::BluetoothLERawAdvertisementsResponse response_; - // Group 3: 4-byte types - uint32_t last_advertisement_flush_time_{0}; - // Pre-allocated response message - always ready to send api::BluetoothConnectionsFreeResponse connections_free_response_; From bcc7b8f490cae830be31bb3e891c5a39a43fac37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 16:12:02 -1000 Subject: [PATCH 1154/2056] [api] Add send_sensor_state benchmarks (#15352) --- esphome/components/api/api_connection.h | 12 ++ .../benchmarks/components/api/bench_helpers.h | 67 ++++++ .../components/api/bench_plaintext_frame.cpp | 58 +----- .../api/bench_send_sensor_state.cpp | 191 ++++++++++++++++++ 4 files changed, 272 insertions(+), 56 deletions(-) create mode 100644 tests/benchmarks/components/api/bench_helpers.h create mode 100644 tests/benchmarks/components/api/bench_send_sensor_state.cpp diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3d8563b1ae5..4ce13356502 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -44,10 +44,22 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); +#ifdef USE_BENCHMARK +class APIConnection; +void bench_enable_immediate_send(APIConnection *conn); +void bench_clear_batch(APIConnection *conn); +void bench_process_batch(APIConnection *conn); +#endif + class APIConnection final : public APIServerConnectionBase { public: friend class APIServer; friend class ListEntitiesIterator; +#ifdef USE_BENCHMARK + friend void bench_enable_immediate_send(APIConnection *conn); + friend void bench_clear_batch(APIConnection *conn); + friend void bench_process_batch(APIConnection *conn); +#endif APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); ~APIConnection(); diff --git a/tests/benchmarks/components/api/bench_helpers.h b/tests/benchmarks/components/api/bench_helpers.h new file mode 100644 index 00000000000..73e51bce3d3 --- /dev/null +++ b/tests/benchmarks/components/api/bench_helpers.h @@ -0,0 +1,67 @@ +#pragma once + +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <sys/socket.h> +#include <unistd.h> + +#include <memory> +#include <utility> + +#include "esphome/components/socket/socket.h" + +namespace esphome::api::benchmarks { + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +inline void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Create a TCP loopback socket pair. Returns the write-side Socket +// (wrapped for ESPHome) and the raw read-side fd for draining. +// Both ends are non-blocking with 16MB buffers. +inline std::pair<std::unique_ptr<socket::Socket>, int> create_tcp_loopback() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Use large socket buffers so benchmarks never hit WOULD_BLOCK + // during a single outer iteration (2000 × ~15B messages = ~30KB). + int bufsize = 16 * 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + return {std::make_unique<socket::Socket>(write_fd), read_fd}; +} + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp index 79bffaf9534..0caa50c748f 100644 --- a/tests/benchmarks/components/api/bench_plaintext_frame.cpp +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -2,12 +2,9 @@ #ifdef USE_API_PLAINTEXT #include <benchmark/benchmark.h> -#include <fcntl.h> -#include <netinet/in.h> -#include <netinet/tcp.h> -#include <sys/socket.h> #include <unistd.h> +#include "bench_helpers.h" #include "esphome/components/api/api_frame_helper_plaintext.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_buffer.h" @@ -16,57 +13,12 @@ namespace esphome::api::benchmarks { static constexpr int kInnerIterations = 2000; -// Helper to drain accumulated data from the read side of a socket -// to prevent the write side from blocking. -static void drain_socket(int fd) { - char buf[65536]; - while (::read(fd, buf, sizeof(buf)) > 0) { - } -} - // Helper to create a TCP loopback connection with an APIPlaintextFrameHelper // on the write end. Returns the helper and the read-side fd. -// Uses real TCP sockets so TCP_NODELAY succeeds during init(). static std::pair<std::unique_ptr<APIPlaintextFrameHelper>, int> create_plaintext_helper() { - // Create a TCP listener on loopback - int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); - int opt = 1; - ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - struct sockaddr_in addr {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; // OS-assigned port - ::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); - ::listen(listen_fd, 1); - - // Get the assigned port - socklen_t addr_len = sizeof(addr); - ::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len); - - // Connect from client side - int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); - ::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)); - - // Accept on server side (this is our read fd) - int read_fd = ::accept(listen_fd, nullptr, nullptr); - ::close(listen_fd); - - // Make both ends non-blocking - int flags = ::fcntl(write_fd, F_GETFL, 0); - ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); - flags = ::fcntl(read_fd, F_GETFL, 0); - ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); - - // Increase socket buffer sizes to reduce drain frequency - int bufsize = 1024 * 1024; - ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); - ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); - - auto sock = std::make_unique<socket::Socket>(write_fd); + auto [sock, read_fd] = create_tcp_loopback(); auto helper = std::make_unique<APIPlaintextFrameHelper>(std::move(sock)); helper->init(); - return {std::move(helper), read_fd}; } @@ -97,9 +49,6 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) { msg.encode(writer); helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); - - if ((i & 0xFF) == 0) - drain_socket(read_fd); } drain_socket(read_fd); benchmark::DoNotOptimize(helper.get()); @@ -144,9 +93,6 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { } helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5)); - - if ((i & 0xFF) == 0) - drain_socket(read_fd); } drain_socket(read_fd); benchmark::DoNotOptimize(helper.get()); diff --git a/tests/benchmarks/components/api/bench_send_sensor_state.cpp b/tests/benchmarks/components/api/bench_send_sensor_state.cpp new file mode 100644 index 00000000000..815081374a5 --- /dev/null +++ b/tests/benchmarks/components/api/bench_send_sensor_state.cpp @@ -0,0 +1,191 @@ +#include "esphome/core/defines.h" +#if defined(USE_API_PLAINTEXT) && defined(USE_SENSOR) + +#include <benchmark/benchmark.h> +#include <unistd.h> + +#include "bench_helpers.h" +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_server.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::api { + +// Friend functions declared in APIConnection for benchmark access. +void bench_enable_immediate_send(APIConnection *conn) { conn->flags_.should_try_send_immediately = true; } +void bench_clear_batch(APIConnection *conn) { conn->clear_batch_(); } +void bench_process_batch(APIConnection *conn) { conn->process_batch_(); } + +} // namespace esphome::api + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create a TCP loopback connection with an APIConnection. +// Returns the connection and the read-side fd for draining. +static std::pair<std::unique_ptr<APIConnection>, int> create_api_connection() { + auto [sock, read_fd] = create_tcp_loopback(); + auto conn = std::make_unique<APIConnection>(std::move(sock), global_api_server); + conn->start(); + return {std::move(conn), read_fd}; +} + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestSensor : public sensor::Sensor { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// --- send_sensor_state: immediate send path --- +// Measures: send_message_smart_ → prepare buffer → dispatch_message_ → +// try_send_sensor_state → fill key/device_id + proto encode → frame write → +// TCP send. This is the per-client cost when batch_delay=0 and initial states +// have been sent. + +static void SendSensorState_Immediate(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + bench_enable_immediate_send(conn.get()); + // batch_delay must be 0 for should_send_immediately_ to return true + uint16_t saved_delay = global_api_server->get_batch_delay(); + global_api_server->set_batch_delay(0); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + global_api_server->set_batch_delay(saved_delay); + ::close(read_fd); +} +BENCHMARK(SendSensorState_Immediate); + +// --- send_sensor_state: batch path (cold — first call allocates) --- +// Measures: send_message_smart_ → schedule_message_ → deferred batch add. +// Includes one-time vector allocation cost. + +static void SendSensorState_Batch_Cold(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(SendSensorState_Batch_Cold); + +// --- send_sensor_state: batch path (warm — buffer already allocated) --- +// Measures steady-state batch cost after the vector has been allocated +// and cleared at least once. This is the typical path during normal +// operation after the first batch has been processed. + +static void SendSensorState_Batch_Warm(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + // Warm up: send once to allocate, then clear to keep capacity + conn->send_sensor_state(&sensor); + bench_clear_batch(conn.get()); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(SendSensorState_Batch_Warm); + +// --- process_batch_: single sensor state (encode + frame + write) --- +// Measures the deferred batch processing path: dispatch_message_ → +// try_send_sensor_state → fill + proto encode → send_buffer → frame write. +// This is the cost paid on the next loop() after batching. + +static void ProcessBatch_SingleSensor(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + // Warm up batch vector + conn->send_sensor_state(&sensor); + bench_process_batch(conn.get()); + drain_socket(read_fd); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + bench_process_batch(conn.get()); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(ProcessBatch_SingleSensor); + +// --- process_batch_: 5 different sensors --- +// Measures batch processing with multiple items queued. +// This exercises the multi-message path in process_batch_. + +static void ProcessBatch_5Sensors(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensors[5]; + for (int i = 0; i < 5; i++) { + char name[20]; + snprintf(name, sizeof(name), "sensor_%d", i); + sensors[i].configure(name); + sensors[i].publish_state(23.5f + static_cast<float>(i)); + } + + // Warm up batch vector + for (auto &s : sensors) + conn->send_sensor_state(&s); + bench_process_batch(conn.get()); + drain_socket(read_fd); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + for (auto &s : sensors) + conn->send_sensor_state(&s); + bench_process_batch(conn.get()); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(ProcessBatch_5Sensors); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT && USE_SENSOR From be56be5201f23d41e060f3cee458607f0bf97729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 16:14:45 -1000 Subject: [PATCH 1155/2056] [core] Reduce runtime_stats measurement overhead (#15359) --- esphome/core/component.cpp | 7 ------- esphome/core/component.h | 9 ++++----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 955596ce956..288c3f01a3d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -519,13 +519,6 @@ WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t block } } -#ifdef USE_RUNTIME_STATS -void WarnIfComponentBlockingGuard::record_runtime_stats_() { - uint32_t duration_us = micros() - this->started_us_; - this->component_->runtime_stats_.record_time(duration_us); -} -#endif - #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely diff --git a/esphome/core/component.h b/esphome/core/component.h index c5a331ee298..d09b42b9368 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -630,17 +630,17 @@ class WarnIfComponentBlockingGuard { { } - // Finish the timing operation and return the current time + // Finish the timing operation and return the current time (millis) // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { - uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS - this->record_runtime_stats_(); + this->component_->runtime_stats_.record_time(micros() - this->started_us_); #endif + uint32_t curr_time = millis(); #ifndef USE_BENCHMARK // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U; + uint32_t blocking_time = curr_time - this->started_; if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } @@ -655,7 +655,6 @@ class WarnIfComponentBlockingGuard { Component *component_; #ifdef USE_RUNTIME_STATS uint32_t started_us_; - void record_runtime_stats_(); #endif private: From f36d78e09c866136111c260d03c99820ff71fcee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 16:15:00 -1000 Subject: [PATCH 1156/2056] [core] Force inline Component::get_component_log_str() (#15363) --- esphome/core/component.cpp | 3 --- esphome/core/component.h | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 288c3f01a3d..2b5aba2a7b3 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -267,9 +267,6 @@ void Component::call() { break; } } -const LogString *Component::get_component_log_str() const { - return component_source_lookup(this->component_source_index_); -} bool Component::should_warn_of_blocking(uint32_t blocking_time) { // Convert centisecond threshold to milliseconds for comparison uint32_t threshold_ms = static_cast<uint32_t>(this->warn_if_blocking_over_) * 10U; diff --git a/esphome/core/component.h b/esphome/core/component.h index d09b42b9368..f091f9434c9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -323,7 +323,9 @@ class Component { * * Returns LOG_STR("<unknown>") if source not set */ - const LogString *get_component_log_str() const; + inline const LogString *get_component_log_str() const ESPHOME_ALWAYS_INLINE { + return component_source_lookup(this->component_source_index_); + } bool should_warn_of_blocking(uint32_t blocking_time); From 08c7b3afbdf332ff65e1648047ba3ff90f7e2d2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 16:53:53 -1000 Subject: [PATCH 1157/2056] [esp32_ble_tracker] Reduce scan cycle log spam (#15365) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index f2d60be6413..c7f2319d69d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -249,7 +249,7 @@ void ESP32BLETracker::start_scan_(bool first) { return; } this->set_scanner_state_(ScannerState::STARTING); - ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING."); + ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING."); if (!first) { #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) @@ -855,7 +855,7 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { } void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { - ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); + ESP_LOGV(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); #ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); #endif From 1436d034bf699531d81b2545804ebb996288d15f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 17:11:47 -1000 Subject: [PATCH 1158/2056] [api] Inline DeferredBatch::add_item to eliminate push_back call barrier (#15353) --- esphome/components/api/api_connection.cpp | 36 ++--------------------- esphome/components/api/api_connection.h | 33 +++++++++++++++------ 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 79df85ada35..aa64ced64c1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -132,8 +132,6 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa #endif } -uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } - void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); @@ -2072,37 +2070,9 @@ void APIConnection::on_fatal_error() { this->flags_.remove = true; } -void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); } - -void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index) { - // Check if we already have a message of this type for this entity - // This provides deduplication per entity/message_type combination - // O(n) but optimized for RAM and not performance. - // Skip deduplication for events - they are edge-triggered, every occurrence matters -#ifdef USE_EVENT - if (message_type != EventResponse::MESSAGE_TYPE) -#endif - { - for (const auto &item : items) { - if (item.entity == entity && item.message_type == message_type) - return; // Already queued - } - } - // No existing item found (or event), add new one - this->push_item({entity, message_type, estimated_size, aux_data_index}); -} - -void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t 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 - this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED}); - if (items.size() > 1) { - // Swap the new high-priority item to the front - std::swap(items.front(), items.back()); - } +bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { + this->deferred_batch_.add_item_front(entity, message_type, estimated_size); + return this->schedule_batch_(); } bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4ce13356502..13d5273ecbb 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -644,11 +644,28 @@ class APIConnection final : public APIServerConnectionBase { // Add item to the batch (with deduplication) void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = AUX_DATA_UNUSED); + uint8_t aux_data_index = AUX_DATA_UNUSED) { + // Dedup: O(n) scan but optimized for RAM over performance + // Skip deduplication for events - they are edge-triggered, every occurrence matters +#ifdef USE_EVENT + if (message_type != EventResponse::MESSAGE_TYPE) +#endif + { + for (const auto &item : this->items) { + if (item.entity == entity && item.message_type == message_type) + return; // Already queued + } + } + this->items.push_back({entity, message_type, estimated_size, aux_data_index}); + } // Add item to the front of the batch (for high priority messages like ping) - void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); - // Single push_back site to avoid duplicate _M_realloc_insert instantiation - void push_item(const BatchItem &item); + void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { + // Swap to front avoids expensive vector::insert which shifts all elements + this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); + if (this->items.size() > 1) { + std::swap(this->items.front(), this->items.back()); + } + } // Clear all items void clear() { @@ -713,7 +730,7 @@ class APIConnection final : public APIServerConnectionBase { ActiveIterator active_iterator_{ActiveIterator::NONE}; // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary - uint32_t get_batch_delay_ms_() const; + uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. // If its IPv6 the header is 40 bytes, and if its IPv4 @@ -780,10 +797,8 @@ class APIConnection final : public APIServerConnectionBase { } // Helper function to schedule a high priority message at the front of the batch - bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { - this->deferred_batch_.add_item_front(entity, message_type, estimated_size); - return this->schedule_batch_(); - } + // Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths + bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); // Helper function to log client messages with name and peername void log_client_(int level, const LogString *message); From 3fbf0f0c019da645c9e68cb217789711e58aa6c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 1 Apr 2026 17:13:09 -1000 Subject: [PATCH 1159/2056] [api] Simplify encode_to_buffer to single resize call (#15355) --- esphome/components/api/api_connection.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index aa64ced64c1..0f456ecd0c7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2021,24 +2021,23 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + size_t to_add; if (conn->flags_.batch_first_message) { // First message - buffer already prepared by caller, just clear flag conn->flags_.batch_first_message = false; + to_add = calculated_size; } else { // Batch message second or later - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding); + // Reserve for full message, resize to include footer gap + header padding + payload + to_add = total_calculated_size; } - // Pre-resize buffer to include payload, then encode through raw pointer - size_t write_start = shared_buf.size(); - shared_buf.resize(write_start + calculated_size); - ProtoWriteBuffer buffer{&shared_buf, write_start}; + shared_buf.resize(shared_buf.size() + to_add); + ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; encode_fn(msg, buffer); // Return total size (header + payload + footer) - return static_cast<uint16_t>(header_padding + calculated_size + footer_size); + return static_cast<uint16_t>(total_calculated_size); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); From 34295fbd6985cdea1017ee188439c3fa66e59a59 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:25:54 +0200 Subject: [PATCH 1160/2056] [nextion] Collapse nested namespace to esphome::nextion (#15367) --- esphome/components/nextion/automation.h | 6 ++---- .../nextion/binary_sensor/nextion_binarysensor.cpp | 6 ++---- .../nextion/binary_sensor/nextion_binarysensor.h | 7 +++---- esphome/components/nextion/nextion.cpp | 6 ++---- esphome/components/nextion/nextion.h | 6 ++---- esphome/components/nextion/nextion_base.h | 7 +++---- esphome/components/nextion/nextion_commands.cpp | 7 +++---- esphome/components/nextion/nextion_component.cpp | 6 ++---- esphome/components/nextion/nextion_component.h | 7 +++---- esphome/components/nextion/nextion_component_base.h | 6 ++---- esphome/components/nextion/nextion_upload.cpp | 7 +++---- esphome/components/nextion/nextion_upload_arduino.cpp | 7 +++---- esphome/components/nextion/nextion_upload_esp32.cpp | 7 +++---- esphome/components/nextion/sensor/nextion_sensor.cpp | 6 ++---- esphome/components/nextion/sensor/nextion_sensor.h | 7 +++---- esphome/components/nextion/switch/nextion_switch.cpp | 6 ++---- esphome/components/nextion/switch/nextion_switch.h | 7 +++---- .../components/nextion/text_sensor/nextion_textsensor.cpp | 7 +++---- .../components/nextion/text_sensor/nextion_textsensor.h | 7 +++---- 19 files changed, 49 insertions(+), 76 deletions(-) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 9f52507d676..17f6c77e17a 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -2,8 +2,7 @@ #include "esphome/core/automation.h" #include "nextion.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { template<typename... Ts> class NextionSetBrightnessAction : public Action<Ts...> { public: @@ -91,5 +90,4 @@ template<typename... Ts> class NextionPublishBoolAction : public Action<Ts...> { NextionComponent *component_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index 3628ac2f634..08e7c58ef10 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_binarysensor"; @@ -64,5 +63,4 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti ESP_LOGN(TAG, "Write: %s=%s", this->variable_name_.c_str(), ONOFF(this->state)); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h index baab47851c7..7637957222d 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionBinarySensor; class NextionBinarySensor : public NextionComponent, @@ -38,5 +38,4 @@ class NextionBinarySensor : public NextionComponent, protected: uint8_t page_id_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index d141ef79062..ab268fed7f2 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion"; @@ -1290,5 +1289,4 @@ void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = write bool Nextion::is_updating() { return this->connection_state_.is_updating_; } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index b5aaecd667d..b3ecbf46b1c 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -21,8 +21,7 @@ #endif // USE_ESP32 vs USE_ESP8266 #endif // USE_NEXTION_TFT_UPLOAD -namespace esphome { -namespace nextion { +namespace esphome::nextion { class Nextion; class NextionComponentBase; @@ -1547,5 +1546,4 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index d46cd9a1852..2c516fc80f2 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -2,8 +2,8 @@ #include "esphome/core/defines.h" #include "esphome/core/color.h" #include "nextion_component_base.h" -namespace esphome { -namespace nextion { + +namespace esphome::nextion { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #define NEXTION_PROTOCOL_LOG @@ -61,5 +61,4 @@ class NextionBase { bool is_detected_ = false; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 6c8e0f18bc8..a7e65b5ddf9 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -3,8 +3,8 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion"; // Sleep safe commands @@ -340,5 +340,4 @@ void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 30c8b80524f..f4571256165 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -1,7 +1,6 @@ #include "nextion_component.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { void NextionComponent::set_background_color(Color bco) { if (this->variable_name_ == this->variable_name_to_send_) { @@ -110,5 +109,4 @@ void NextionComponent::update_component_settings(bool force_update) { this->component_flags_.font_id_needs_update = false; } } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h index add9e11cf1f..068cf513617 100644 --- a/esphome/components/nextion/nextion_component.h +++ b/esphome/components/nextion/nextion_component.h @@ -3,8 +3,8 @@ #include "esphome/core/color.h" #include "nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionComponent; class NextionComponent : public NextionComponentBase { @@ -80,5 +80,4 @@ class NextionComponent : public NextionComponentBase { uint16_t reserved : 3; } component_flags_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 4d5550d4060..c1d0ae8ed11 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -5,8 +5,7 @@ #include <vector> #include "esphome/core/defines.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { enum NextionQueueType { NO_RESULT = 0, @@ -102,5 +101,4 @@ class NextionComponentBase { bool needs_to_send_update_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index 7ddd7a2f088..a49e7f18d6c 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -4,8 +4,8 @@ #include "esphome/core/application.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload"; bool Nextion::upload_end_(bool successful) { @@ -33,7 +33,6 @@ bool Nextion::upload_end_(bool successful) { return successful; } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index f59b7080028..c79c68552ea 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -11,8 +11,8 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload.arduino"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; @@ -342,8 +342,7 @@ WiFiClient *Nextion::get_wifi_client_() { } #endif // USE_ESP8266 -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // NOT USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 166bbcc86a8..40a284dc46b 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -14,8 +14,8 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload.esp32"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; @@ -344,8 +344,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(true); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 9ea12cf808a..d4fad86286a 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_sensor"; @@ -108,5 +107,4 @@ void NextionSensor::wave_update_() { this->nextion_->add_addt_command_to_queue(this); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index b1902f9b1be..f1a3ff72ec0 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionSensor; class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent { @@ -44,5 +44,4 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol bool send_last_value_ = true; void wave_update_(); }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index 21636f2bfab..0018cff0052 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_switch"; @@ -48,5 +47,4 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { void NextionSwitch::write_state(bool state) { this->set_state(state); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h index c371ea3fc63..7e0593d217c 100644 --- a/esphome/components/nextion/switch/nextion_switch.h +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionSwitch; class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { @@ -30,5 +30,4 @@ class NextionSwitch : public NextionComponent, public switch_::Switch, public Po protected: void write_state(bool state) override; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index 9b6deeda87b..45e46914236 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -2,8 +2,8 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion_textsensor"; void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) { @@ -45,5 +45,4 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s ESP_LOGN(TAG, "Write: %s='%s'", this->variable_name_.c_str(), state.c_str()); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h index 7c08e471891..42cd5dcef4f 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.h +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionTextSensor; class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { @@ -28,5 +28,4 @@ class NextionTextSensor : public NextionComponent, public text_sensor::TextSenso this->set_state(state_value, publish, send_to_nextion); } }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion From c21c7dd29289f2553a16fa63da60f66e715c8fe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 2 Apr 2026 03:12:38 -1000 Subject: [PATCH 1161/2056] [mitsubishi_cn105] Fix test grouping conflict with uart package (#15366) --- tests/components/mitsubishi_cn105/test.esp32-idf.yaml | 2 +- tests/components/mitsubishi_cn105/test.esp8266-ard.yaml | 2 +- tests/components/mitsubishi_cn105/test.rp2040-ard.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/mitsubishi_cn105/test.esp32-idf.yaml b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml index ac63cf987f3..5ce18619027 100644 --- a/tests/components/mitsubishi_cn105/test.esp32-idf.yaml +++ b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml index 9f2f350b46d..a3f8cf43d49 100644 --- a/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml +++ b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml index 4363d6eee82..7c1f4f41e22 100644 --- a/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml +++ b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml <<: !include common.yaml From a359ecaaf40384c8f31934e9662fdad84b4db3bc Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Thu, 2 Apr 2026 16:12:20 +0200 Subject: [PATCH 1162/2056] [zigbee] print logs after reporting info update (#13916) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/zigbee/zigbee_zephyr.cpp | 60 +++++++++++++++------ esphome/components/zigbee/zigbee_zephyr.h | 1 + esphome/components/zigbee/zigbee_zephyr.py | 2 + 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index c103363b4a4..047c30300e2 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -255,26 +255,25 @@ void ZigbeeComponent::factory_reset() { ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); } +static void log_reporting_info(zb_zcl_reporting_info_t *rep_info) { + auto now = millis(); + ESP_LOGD(TAG, "Reporting: endpoint %d, cluster_id 0x%04X, attr_id 0x%04X, flags 0x%02X, report in %ums", rep_info->ep, + rep_info->cluster_id, rep_info->attr_id, rep_info->flags, + ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED) + ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now + : 0); + ESP_LOGD(TAG, " min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds", + rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval, + rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval); +} + void ZigbeeComponent::dump_reporting_() { #ifdef ESPHOME_LOG_HAS_VERBOSE - auto now = millis(); - bool first = true; for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) { if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) { zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info; for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) { - if (!first) { - ESP_LOGV(TAG, ""); - } - first = false; - ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep, - rep_info->cluster_id, rep_info->attr_id, rep_info->flags, - ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED) - ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now - : 0); - ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds", - rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval, - rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval); + log_reporting_info(rep_info); rep_info++; } } @@ -282,9 +281,38 @@ void ZigbeeComponent::dump_reporting_() { #endif } +void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info) { +#ifdef ESPHOME_LOG_HAS_DEBUG + auto *rep_info = + zb_zcl_find_reporting_info_manuf(attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, + config_rep_req->attr_id, attr_addr_info->manuf_code); + if (rep_info == nullptr) { + ESP_LOGE(TAG, + "Failed to resolve reporting info (src_ep=%u cluster_id=0x%04x role=%u attr_id=0x%04x manuf_code=0x%04x)", + attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, config_rep_req->attr_id, + attr_addr_info->manuf_code); + return; + } + log_reporting_info(rep_info); +#endif +} + } // namespace esphome::zigbee -extern "C" void zboss_signal_handler(zb_uint8_t param) { - esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); +extern "C" { +void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); } + +// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) +extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info); + +zb_ret_t __wrap_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info) { + zb_ret_t ret = __real_zb_zcl_put_reporting_info_from_req(config_rep_req, attr_addr_info); + esphome::zigbee::global_zigbee->after_reporting_info(config_rep_req, attr_addr_info); + return ret; +} +// NOLINTEND(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) } #endif diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 3fa5818ec57..eeb142eff17 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -76,6 +76,7 @@ class ZigbeeComponent : public Component { } template<typename F> void add_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } void zboss_signal_handler_esphome(zb_bufid_t bufid); + void after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, zb_zcl_attr_addr_info_t *attr_addr_info); void factory_reset(); Trigger<> *get_join_trigger() { return &this->join_trigger_; }; void force_report(); diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index a1e6ad3097e..3288d924833 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -166,6 +166,8 @@ async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) zephyr_add_prj_conf("NET_UDP", False) + cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req") + if CONF_IEEE802154_VENDOR_OUI in config: zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True) random_number = config[CONF_IEEE802154_VENDOR_OUI] From b8a9d327f05700a6df463202cdc5cfaefbfdc8dd Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 2 Apr 2026 09:40:19 -0500 Subject: [PATCH 1163/2056] [media_player] Add enqueue action (#14775) --- esphome/components/media_player/__init__.py | 41 +++++++++++++------- esphome/components/media_player/automation.h | 16 +++++--- tests/components/media_player/common.yaml | 5 +++ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 767916ad88d..842f620dae3 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -94,6 +94,9 @@ _STATE_CONDITIONS = [ PlayMediaAction = media_player_ns.class_( "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) +EnqueueMediaAction = media_player_ns.class_( + "EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer) +) VolumeSetAction = media_player_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) @@ -168,20 +171,17 @@ MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( - "media_player.play_media", - PlayMediaAction, - cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(MediaPlayer), - cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), - cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), - }, - key=CONF_MEDIA_URL, - ), - synchronous=True, +_MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MediaPlayer), + cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), + }, + key=CONF_MEDIA_URL, ) -async def media_player_play_media_action(config, action_id, template_arg, args): + + +async def _media_action_handler(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) @@ -191,6 +191,21 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var +automation.register_action( + "media_player.play_media", + PlayMediaAction, + _MEDIA_URL_ACTION_SCHEMA, + synchronous=True, +)(_media_action_handler) + +automation.register_action( + "media_player.enqueue", + EnqueueMediaAction, + _MEDIA_URL_ACTION_SCHEMA, + synchronous=True, +)(_media_action_handler) + + def _snake_to_camel(name): return "".join(word.capitalize() for word in name.split("_")) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 658381ef90d..14ce3c6aedb 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -55,17 +55,23 @@ using GroupJoinAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYE template<typename... Ts> using ClearPlaylistAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST, Ts...>; -template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> { +template<MediaPlayerCommand Command, typename... Ts> +class MediaPlayerMediaAction : public Action<Ts...>, public Parented<MediaPlayer> { TEMPLATABLE_VALUE(std::string, media_url) TEMPLATABLE_VALUE(bool, announcement) void play(const Ts &...x) override { - this->parent_->make_call() - .set_media_url(this->media_url_.value(x...)) - .set_announcement(this->announcement_.value(x...)) - .perform(); + auto call = this->parent_->make_call(); + if constexpr (Command != MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY) + call.set_command(Command); + call.set_media_url(this->media_url_.value(x...)).set_announcement(this->announcement_.value(x...)).perform(); } }; +template<typename... Ts> +using PlayMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY, Ts...>; +template<typename... Ts> +using EnqueueMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_ENQUEUE, Ts...>; + template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> { TEMPLATABLE_VALUE(float, volume) void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 89600f70f60..88d04d0ff06 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -61,3 +61,8 @@ media_player: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% + - media_player.enqueue: http://localhost/media.mp3 + - media_player.enqueue: !lambda 'return "http://localhost/media.mp3";' + - media_player.enqueue: + media_url: http://localhost/media.mp3 + announcement: true From da8d9d9c2d1f1ba41837beea471ebcfd50116618 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 2 Apr 2026 10:37:14 -0500 Subject: [PATCH 1164/2056] [audio] use microFLAC library for decoding (#15372) --- esphome/components/audio/__init__.py | 1 + esphome/components/audio/audio_decoder.cpp | 83 +++++++++------------- esphome/components/audio/audio_decoder.h | 10 +-- esphome/idf_component.yml | 2 + 4 files changed, 42 insertions(+), 54 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index acc3b5d351e..8f2102de6a7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -210,6 +210,7 @@ async def to_code(config): data = _get_data() if data.flac_support: cg.add_define("USE_AUDIO_FLAC_SUPPORT") + add_idf_component(name="esphome/micro-flac", ref="0.1.1") if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") if data.opus_support: diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index bc05bc0006a..baa4c41c064 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -84,13 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { switch (this->audio_file_type_) { #ifdef USE_AUDIO_FLAC_SUPPORT case AudioFileType::FLAC: - this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>(); - // CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source - // or built into the firmware, so the data integrity is already verified by the time it gets to the decoder, - // making the CRC check unnecessary. - this->flac_decoder_->set_crc_check_enabled(false); + this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>(); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_MP3_SUPPORT @@ -268,59 +265,45 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { #ifdef USE_AUDIO_FLAC_SUPPORT FileDecoderState AudioDecoder::decode_flac_() { - if (!this->audio_stream_info_.has_value()) { - // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available()); + size_t bytes_consumed, samples_decoded; - if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - // Serrious error reading FLAC header, there is no recovery - return FileDecoderState::FAILED; + micro_flac::FLACDecoderResult result = this->flac_decoder_->decode( + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); + + if (result == micro_flac::FLAC_DECODER_SUCCESS) { + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().samples_to_bytes(samples_decoded)); } - - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); this->input_buffer_->consume(bytes_consumed); + } else if (result == micro_flac::FLAC_DECODER_HEADER_READY) { + // Header just parsed, stream info now available + const auto &info = this->flac_decoder_->get_stream_info(); + this->audio_stream_info_ = audio::AudioStreamInfo(info.bits_per_sample(), info.num_channels(), info.sample_rate()); - if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - return FileDecoderState::MORE_TO_PROCESS; - } - - // Reallocate the output transfer buffer to the smallest necessary size - this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); + // Reallocate the output transfer buffer to the required size + this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample(); if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { - // Couldn't reallocate output buffer return FileDecoderState::FAILED; } - - this->audio_stream_info_ = - audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(), - this->flac_decoder_->get_sample_rate()); - - return FileDecoderState::MORE_TO_PROCESS; - } - - uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(), - this->output_transfer_buffer_->get_buffer_end(), &output_samples); - - if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Not an issue, just needs more data that we'll get next time. - return FileDecoderState::POTENTIALLY_FAILED; - } - - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_buffer_->consume(bytes_consumed); - - if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Corrupted frame, don't retry with current buffer content, wait for new sync - return FileDecoderState::POTENTIALLY_FAILED; - } - - // We have successfully decoded some input data and have new output data - this->output_transfer_buffer_->increase_buffer_length( - this->audio_stream_info_.value().samples_to_bytes(output_samples)); - - if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) { + this->input_buffer_->consume(bytes_consumed); + } else if (result == micro_flac::FLAC_DECODER_END_OF_STREAM) { + this->input_buffer_->consume(bytes_consumed); return FileDecoderState::END_OF_FILE; + } else if (result == micro_flac::FLAC_DECODER_NEED_MORE_DATA) { + this->input_buffer_->consume(bytes_consumed); + return FileDecoderState::MORE_TO_PROCESS; + } else if (result == micro_flac::FLAC_DECODER_ERROR_OUTPUT_TOO_SMALL) { + // Reallocate to decode the frame on the next call + const auto &info = this->flac_decoder_->get_stream_info(); + this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "FLAC decoder failed: %d", static_cast<int>(result)); + return FileDecoderState::POTENTIALLY_FAILED; } return FileDecoderState::MORE_TO_PROCESS; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 726baa289e2..6e3a228a686 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -16,14 +16,16 @@ #include "esp_err.h" // esp-audio-libs -#ifdef USE_AUDIO_FLAC_SUPPORT -#include <flac_decoder.h> -#endif #ifdef USE_AUDIO_MP3_SUPPORT #include <mp3_decoder.h> #endif #include <wav_decoder.h> +// micro-flac +#ifdef USE_AUDIO_FLAC_SUPPORT +#include <micro_flac/flac_decoder.h> +#endif + // micro-opus #ifdef USE_AUDIO_OPUS_SUPPORT #include <micro_opus/ogg_opus_decoder.h> @@ -119,7 +121,7 @@ class AudioDecoder { std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_; #ifdef USE_AUDIO_FLAC_SUPPORT FileDecoderState decode_flac_(); - std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_; + std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_; #endif #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 462af5d1e70..1e40fef2dc3 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.4 + esphome/micro-flac: + version: 0.1.1 esphome/micro-opus: version: 0.3.6 espressif/esp-dsp: From e7e590b36f97deea2b9dac5af69be9505c335970 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:08:43 -0400 Subject: [PATCH 1165/2056] [thermostat] Fix on_boot_restore_from DEFAULT_PRESET validation bypass (#15383) --- esphome/components/thermostat/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index f7c1298d688..ec115296d71 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -503,7 +503,7 @@ def validate_thermostat(config): # If restoring default preset on boot is true then ensure we have a default preset if ( CONF_ON_BOOT_RESTORE_FROM in config - and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + and config[CONF_ON_BOOT_RESTORE_FROM] == "DEFAULT_PRESET" and CONF_DEFAULT_PRESET not in config ): raise cv.Invalid( From da09e1e1ce336ff7f7d2c3bede9c4bdad7bbedf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 2 Apr 2026 09:19:47 -1000 Subject: [PATCH 1166/2056] [time] Use O(1) closed-form leap year math for epoch-to-year conversion (#15368) --- esphome/components/time/posix_tz.cpp | 63 +++++++------ tests/components/time/posix_tz_parser.cpp | 109 ++++++++++++++++++++++ 2 files changed, 144 insertions(+), 28 deletions(-) diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp index f388267abd2..c25248e4574 100644 --- a/esphome/components/time/posix_tz.cpp +++ b/esphome/components/time/posix_tz.cpp @@ -34,25 +34,43 @@ bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year // Get days in year (avoids duplicate is_leap_year calls) static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; } -// Convert days since epoch to year, updating days to remainder -static int __attribute__((noinline)) days_to_year(int64_t &days) { - int year = 1970; - int diy; - while (days >= (diy = days_in_year(year)) && year < 2200) { - days -= diy; +// Count leap years in [1, year] (i.e. up to and including year) +static constexpr int count_leap_years_up_to(int year) { return year / 4 - year / 100 + year / 400; } + +constexpr int EPOCH_YEAR = 1970; +constexpr int LEAP_YEARS_BEFORE_EPOCH = count_leap_years_up_to(EPOCH_YEAR - 1); +constexpr int DAYS_PER_YEAR = 365; +constexpr int SECONDS_PER_DAY = 86400; + +// Days from epoch (Jan 1 1970) to Jan 1 of given year — O(1) +static inline int64_t days_to_year_start(int year) { + return static_cast<int64_t>(DAYS_PER_YEAR) * (year - EPOCH_YEAR) + + (count_leap_years_up_to(year - 1) - LEAP_YEARS_BEFORE_EPOCH); +} + +// Convert days since epoch to year, updating days to day-of-year remainder. +// The initial estimate from days/365 can overshoot by multiple years for +// far-future dates (e.g., year 5000+) due to accumulated leap days, +// so we use loops rather than single-step correction. +static int days_to_year(int64_t &days) { + int year = static_cast<int>(EPOCH_YEAR + days / DAYS_PER_YEAR); + int64_t year_start = days_to_year_start(year); + while (days < year_start) { + year--; + year_start = days_to_year_start(year); + } + while (days >= year_start + days_in_year(year)) { + year_start += days_in_year(year); year++; } - while (days < 0 && year > 1900) { - year--; - days += days_in_year(year); - } + days -= year_start; return year; } -// Extract just the year from a UTC epoch +// Extract just the year from a UTC epoch — O(1) static int epoch_to_year(time_t epoch) { - int64_t days = epoch / 86400; - if (epoch < 0 && epoch % 86400 != 0) + int64_t days = epoch / SECONDS_PER_DAY; + if (epoch < 0 && epoch % SECONDS_PER_DAY != 0) days--; return days_to_year(days); } @@ -87,11 +105,11 @@ int __attribute__((noinline)) day_of_week(int year, int month, int day) { void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) { // Days since epoch - int64_t days = epoch / 86400; - int32_t remaining_secs = epoch % 86400; + int64_t days = epoch / SECONDS_PER_DAY; + int32_t remaining_secs = epoch % SECONDS_PER_DAY; if (remaining_secs < 0) { days--; - remaining_secs += 86400; + remaining_secs += SECONDS_PER_DAY; } out_tm->tm_sec = remaining_secs % 60; @@ -280,17 +298,6 @@ static int __attribute__((noinline)) days_from_year_start(int year, int month, i return days; } -// Calculate days from epoch to Jan 1 of given year (for DST transition calculations) -// Only supports years >= 1970. Timezone is either compiled in from YAML or set by -// Home Assistant, so pre-1970 dates are not a concern. -static int64_t __attribute__((noinline)) days_to_year_start(int year) { - int64_t days = 0; - for (int y = 1970; y < year; y++) { - days += days_in_year(y); - } - return days; -} - time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) { int month, day; @@ -339,7 +346,7 @@ time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRul int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day); // Convert to epoch and add transition time and base offset - return days * 86400 + rule.time_seconds + base_offset_seconds; + return days * SECONDS_PER_DAY + rule.time_seconds + base_offset_seconds; } } // namespace internal diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index b7cf2a4afad..440eea608d9 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -758,6 +758,115 @@ TEST(PosixTzParser, EpochToLocalDstTransition) { EXPECT_EQ(local.tm_isdst, 1); } +// ============================================================================ +// Leap year edge cases for closed-form year arithmetic +// ============================================================================ + +TEST(PosixTzParser, EpochToLocalLeapYear2000) { + // 2000 is a leap year (divisible by 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + // Feb 29, 2000 12:00:00 UTC + time_t epoch = make_utc(2000, 2, 29, 12); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 100); // 2000 + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 29); + EXPECT_EQ(local.tm_hour, 12); +} + +TEST(PosixTzParser, EpochToLocalNonLeapYear2100) { + // 2100 is NOT a leap year (divisible by 100 but not 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + // Mar 1, 2100 00:00:00 UTC — the day after what would be Feb 29 + time_t epoch = make_utc(2100, 3, 1); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); // 2100 + EXPECT_EQ(local.tm_mon, 2); // March + EXPECT_EQ(local.tm_mday, 1); + + // Feb 28, 2100 23:59:59 UTC — last second of February (no Feb 29) + epoch = make_utc(2100, 2, 28, 23, 59, 59); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 28); +} + +TEST(PosixTzParser, EpochToLocalLeapYear2400) { + // 2400 is a leap year (divisible by 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + time_t epoch = make_utc(2400, 2, 29, 6); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 500); // 2400 + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 29); + EXPECT_EQ(local.tm_hour, 6); +} + +TEST(PosixTzParser, EpochToLocalNewYearBoundaries) { + // Test year boundary — last second of 2099 and first second of 2100 + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + struct tm local; + + // Dec 31, 2099 23:59:59 UTC + time_t epoch = make_utc(2099, 12, 31, 23, 59, 59); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 199); // 2099 + EXPECT_EQ(local.tm_mon, 11); // December + EXPECT_EQ(local.tm_mday, 31); + + // Jan 1, 2100 00:00:00 UTC + epoch = make_utc(2100, 1, 1); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); // 2100 + EXPECT_EQ(local.tm_mon, 0); // January + EXPECT_EQ(local.tm_mday, 1); +} + +TEST(PosixTzParser, EpochToLocalDstAcrossCenturyBoundary) { + // DST transition in year 2100 (non-leap) with US Eastern rules + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz)); + + // July 4, 2100 16:00 UTC = 12:00 EDT + time_t epoch = make_utc(2100, 7, 4, 16); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 12); + EXPECT_EQ(local.tm_isdst, 1); + + // Jan 15, 2100 10:00 UTC = 05:00 EST + epoch = make_utc(2100, 1, 15, 10); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 5); + EXPECT_EQ(local.tm_isdst, 0); +} + +TEST(PosixTzParser, EpochToLocalFarFutureYear5000) { + // Year 5000 — days/365 estimate overshoots by ~2 years due to leap days, + // requiring multiple correction steps in days_to_year. + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + time_t epoch = make_utc(5000, 6, 15, 12); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 3100); // 5000 + EXPECT_EQ(local.tm_mon, 5); // June + EXPECT_EQ(local.tm_mday, 15); + EXPECT_EQ(local.tm_hour, 12); +} + // ============================================================================ // Verification against libc // ============================================================================ From 0343121e9b81ba09d1951831b396df32a3ff2850 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:21:18 -0400 Subject: [PATCH 1167/2056] [ble_client] Fix descriptor_uuid ignored for text sensors (#15374) --- esphome/components/ble_client/text_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index a6b8956f939..0f53cccdad0 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -88,7 +88,7 @@ async def to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) - if descriptor_uuid := config: + if descriptor_uuid := config.get(CONF_DESCRIPTOR_UUID): if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format): cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid))) elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format): From 5dcae1a133762ad49b4fb57b408d7651bc6bd470 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:22:07 -0400 Subject: [PATCH 1168/2056] [climate] Fix MQTT target_temperature_low_state_topic calling wrong setter (#15376) --- esphome/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8cf5fa9b0c2..13dd7aa0075 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -400,7 +400,7 @@ async def setup_climate_core_(var, config): ) ) is not None: cg.add( - mqtt_.set_custom_target_temperature_state_topic( + mqtt_.set_custom_target_temperature_low_state_topic( target_temperature_low_state_topic ) ) From 12a0f5959f0a63a63356aabb05419056db97861f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:23:04 -0400 Subject: [PATCH 1169/2056] [bl0940] Fix reference_voltage config ignored in non-legacy mode (#15375) --- esphome/components/bl0940/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index f36250ecdfa..992064943b8 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -126,7 +126,7 @@ def set_reference_values(config): config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF) else: - vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) + vref = config.get(CONF_REFERENCE_VOLTAGE, DEFAULT_BL0940_VREF) r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) From 67ee727e382c0c13ad8d2918a33e491cac0d037a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:24:26 -1000 Subject: [PATCH 1170/2056] Bump docker/login-action from 4.0.0 to 4.1.0 in the docker-actions group (#15386) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aa63f6a168..ba6db99b84c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,12 +102,12 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -182,13 +182,13 @@ jobs: - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} From 2f405fd96f39819dfcfa4f1f069cbf5ffee501c3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:25:15 -0400 Subject: [PATCH 1171/2056] [espnow] Fix enable_on_boot config option not passed to C++ (#15377) --- esphome/components/espnow/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index d1a85ae8fd8..00703bc2284 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -132,6 +132,7 @@ async def to_code(config): if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER])) for peer in config.get(CONF_PEERS, []): From 37b33f62de49c667c88b5a60b38ef46365bbd9ff Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:25:54 -0400 Subject: [PATCH 1172/2056] [htu21d] Fix set_heater action reading wrong config key (#15378) --- esphome/components/htu21d/sensor.py | 2 +- tests/components/htu21d/common.yaml | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 92c088a22fa..ed4fb5968a7 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -118,6 +118,6 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): async def set_heater_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - status_ = await cg.templatable(config[CONF_LEVEL], args, bool) + status_ = await cg.templatable(config[CONF_STATUS], args, bool) cg.add(var.set_status(status_)) return var diff --git a/tests/components/htu21d/common.yaml b/tests/components/htu21d/common.yaml index ad4b23d460e..126360b7755 100644 --- a/tests/components/htu21d/common.yaml +++ b/tests/components/htu21d/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: htu21d + id: htu21d_sensor i2c_id: i2c_bus model: htu21d temperature: @@ -9,3 +10,14 @@ sensor: heater: name: Heater update_interval: 15s + +button: + - platform: template + name: "Test HTU21D Actions" + on_press: + - htu21d.set_heater: + id: htu21d_sensor + status: true + - htu21d.set_heater_level: + id: htu21d_sensor + level: 5 From 0262d20bbe4bdbac88666e90e231d25f94ff72ea Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:26:47 -0400 Subject: [PATCH 1173/2056] [mlx90393] Remove call to non-existent set_drdy_pin method (#15381) --- esphome/components/mlx90393/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index d93c379506c..293a133c3d9 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -130,9 +130,6 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_DRDY_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) - cg.add(var.set_drdy_pin(pin)) cg.add(var.set_gain(GAIN[config[CONF_GAIN]])) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) cg.add(var.set_filter(config[CONF_FILTER])) From f7222a0e6cc53b5b5d92959c97b73b99e7c18ef3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:28:30 +0000 Subject: [PATCH 1174/2056] Bump ruff from 0.15.8 to 0.15.9 (#15385) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4729f211c2..4ff1685ea7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.8 + rev: v0.15.9 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 3b277e214dd..a191378dd7f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.8 # also change in .pre-commit-config.yaml when updating +ruff==0.15.9 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From dde472b0cf24d2f8f8964b6e816516a9e4d31b33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:28:44 -0400 Subject: [PATCH 1175/2056] [pipsolar] Fix set_level action passing string to cv.use_id (#15380) --- esphome/components/pipsolar/output/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 81e99e15a29..4ae8d9d487e 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -94,7 +94,7 @@ async def to_code(config): SetOutputAction, cv.Schema( { - cv.Required(CONF_ID): cv.use_id(CONF_ID), + cv.Required(CONF_ID): cv.use_id(PipsolarOutput), cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), } ), From 6b89998b6058b444c9de20ab7d8ac023538f77dc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:29:33 -0400 Subject: [PATCH 1176/2056] [template] Fix cover position_action overridden by has_position default (#15379) --- esphome/components/template/cover/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index cfc19c00cdf..ea4da4e73cc 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -108,7 +108,6 @@ async def to_code(config): cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) - cg.add(var.set_has_position(config[CONF_HAS_POSITION])) @automation.register_action( From 90624e6eca424a7b98afdbdbf924aa862c35702a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:34:27 -0400 Subject: [PATCH 1177/2056] [deep_sleep] Fix wakeup_pin_mode rejecting lowercase on ESP32/BK72XX (#15384) --- esphome/components/deep_sleep/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 4098fd3fb8b..16329bb0fa0 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -266,8 +266,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin, cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]), - cv.enum(WAKEUP_PIN_MODES), - upper=True, + cv.enum(WAKEUP_PIN_MODES, upper=True), ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, From c82166e5f35acfad339c7a16f952efc8ee0de151 Mon Sep 17 00:00:00 2001 From: Thom Wiggers <thom@thomwiggers.nl> Date: Thu, 2 Apr 2026 22:06:49 +0200 Subject: [PATCH 1178/2056] [dsmr] Allow setting MBUS id for thermal sensors in DSMR component (#7519) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 2 +- esphome/components/dsmr/__init__.py | 5 ++++- tests/components/dsmr/common.yaml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fffe5ce91c9..c466204b66c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -142,7 +142,7 @@ esphome/components/dlms_meter/* @SimonFischer04 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its -esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk +esphome/components/dsmr/* @glmnet @PolarGoose esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/touchscreen/* @jesserockz diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 7d76856f281..b1ff9794a38 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -4,7 +4,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID -CODEOWNERS = ["@glmnet", "@zuidwijk", "@PolarGoose"] +CODEOWNERS = ["@glmnet", "@PolarGoose"] MULTI_CONF = True @@ -16,6 +16,7 @@ CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" CONF_WATER_MBUS_ID = "water_mbus_id" +CONF_THERMAL_MBUS_ID = "thermal_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, + cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -64,6 +66,7 @@ async def to_code(config): cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) + cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID])) # DSMR Parser cg.add_library("esphome/dsmr_parser", "1.1.0") diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index d11ce37d598..962800d7b01 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -13,3 +13,4 @@ dsmr: request_pin: ${request_pin} request_interval: 20s receive_timeout: 100ms + thermal_mbus_id: 3 From 63710a4cb70c3071e5664eeed57e374b91b5c1da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 2 Apr 2026 10:10:16 -1000 Subject: [PATCH 1179/2056] [spi] Add spi0 and spi1 to reserved IDs for RP2040 compatibility (#15388) --- esphome/config_validation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 45d2cd81175..09f460f46b5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -244,6 +244,8 @@ RESERVED_IDS = [ "open", "setup", "loop", + "spi0", + "spi1", "uart0", "uart1", "uart2", From 1e72f0ee5aa347214d6e5476d26864758e912d83 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:17:20 +0200 Subject: [PATCH 1180/2056] [nextion] Gate waveform code behind `USE_NEXTION_WAVEFORM`, use `StaticRingBuffer` (#15273) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/nextion/nextion.cpp | 53 ++++++++----- esphome/components/nextion/nextion.h | 16 +++- esphome/components/nextion/nextion_base.h | 2 + .../components/nextion/nextion_commands.cpp | 2 + .../nextion/nextion_component_base.h | 6 ++ esphome/components/nextion/sensor/__init__.py | 18 ++--- .../nextion/sensor/nextion_sensor.cpp | 78 ++++++++++--------- .../nextion/sensor/nextion_sensor.h | 23 ++++-- esphome/core/defines.h | 1 + 9 files changed, 125 insertions(+), 74 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index ab268fed7f2..4a15cbe64fa 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -150,10 +150,12 @@ void Nextion::reset_(bool reset_nextion) { delete entry; // NOLINT(cppcoreguidelines-owning-memory) } this->nextion_queue_.clear(); +#ifdef USE_NEXTION_WAVEFORM for (auto *entry : this->waveform_queue_) { delete entry; // NOLINT(cppcoreguidelines-owning-memory) } this->waveform_queue_.clear(); +#endif // USE_NEXTION_WAVEFORM } void Nextion::dump_config() { @@ -496,20 +498,21 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Invalid baud rate"); break; case 0x12: // invalid Waveform ID or Channel # was used +#ifdef USE_NEXTION_WAVEFORM if (this->waveform_queue_.empty()) { ESP_LOGW(TAG, "Waveform ID/ch used but no sensor queued"); } else { auto &nb = this->waveform_queue_.front(); NextionComponentBase *component = nb->component; - ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); } +#else // USE_NEXTION_WAVEFORM + ESP_LOGW(TAG, "Waveform ID/ch error but waveform not enabled"); +#endif // USE_NEXTION_WAVEFORM break; case 0x1A: // variable name invalid ESP_LOGW(TAG, "Invalid variable name"); @@ -812,29 +815,30 @@ void Nextion::process_nextion_commands_() { } case 0xFD: { // data transparent transmit finished ESP_LOGVV(TAG, "Data transmit done"); +#ifdef USE_NEXTION_WAVEFORM this->check_pending_waveform_(); +#endif // USE_NEXTION_WAVEFORM break; } case 0xFE: { // data transparent transmit ready ESP_LOGVV(TAG, "Ready for transmit"); +#ifdef USE_NEXTION_WAVEFORM if (this->waveform_queue_.empty()) { ESP_LOGE(TAG, "No waveforms queued"); break; } - auto &nb = this->waveform_queue_.front(); auto *component = nb->component; - size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() - : 255; // ADDT command can only send 255 - + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; this->write_array(component->get_wave_buffer().data(), static_cast<int>(buffer_to_send)); - ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); - component->clear_wave_buffer(buffer_to_send); delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); +#else // USE_NEXTION_WAVEFORM + ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled"); +#endif // USE_NEXTION_WAVEFORM break; } default: @@ -934,8 +938,13 @@ void Nextion::all_components_send_state_(bool force_update) { binarysensortype->send_state_to_nextion(); } for (auto *sensortype : this->sensortype_) { - if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == 0) +#ifdef USE_NEXTION_WAVEFORM + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == UINT8_MAX) { +#else // USE_NEXTION_WAVEFORM + if (force_update || sensortype->get_needs_to_send_update()) { +#endif // USE_NEXTION_WAVEFORM sensortype->send_state_to_nextion(); + } } for (auto *switchtype : this->switchtype_) { if (force_update || switchtype->get_needs_to_send_update()) @@ -1239,13 +1248,11 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { } } +#ifdef USE_NEXTION_WAVEFORM /** - * @brief Add addt command to the queue + * @brief Add addt command to the waveform queue. * - * @param component_id The waveform component id - * @param wave_chan_id The waveform channel to send it to - * @param buffer_to_send The buffer size - * @param buffer_size The buffer data + * @param component Pointer to the Nextion component with waveform data to send. */ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) @@ -1262,7 +1269,11 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); - this->waveform_queue_.push_back(nextion_queue); + if (!this->waveform_queue_.push(nextion_queue)) { + ESP_LOGW(TAG, "Waveform queue full, drop"); + delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + return; + } if (this->waveform_queue_.size() == 1) this->check_pending_waveform_(); } @@ -1273,17 +1284,17 @@ void Nextion::check_pending_waveform_() { auto *nb = this->waveform_queue_.front(); auto *component = nb->component; - size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() - : 255; // ADDT command can only send 255 + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); if (!this->send_command_(command)) { delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); } } +#endif // USE_NEXTION_WAVEFORM void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index b3ecbf46b1c..d9103892891 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -9,6 +9,10 @@ #include "esphome/core/defines.h" #include "esphome/core/time.h" +#ifdef USE_NEXTION_WAVEFORM +#include "esphome/core/helpers.h" +#endif // USE_NEXTION_WAVEFORM + #include "nextion_base.h" #include "nextion_component.h" @@ -602,6 +606,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void disable_component_touch(const char *component); +#ifdef USE_NEXTION_WAVEFORM /** * Add waveform data to a waveform component * @param component_id The integer component id. @@ -611,6 +616,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value); void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value); +#endif // USE_NEXTION_WAVEFORM /** * Display a picture at coordinates. @@ -1205,7 +1211,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void add_to_get_queue(NextionComponentBase *component) override; +#ifdef USE_NEXTION_WAVEFORM void add_addt_command_to_queue(NextionComponentBase *component) override; +#endif // USE_NEXTION_WAVEFORM void update_components_by_prefix(const std::string &prefix); @@ -1391,7 +1399,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #endif // USE_NEXTION_COMMAND_SPACING std::list<NextionQueue *> nextion_queue_; - std::list<NextionQueue *> waveform_queue_; +#ifdef USE_NEXTION_WAVEFORM + /// Fixed-size ring buffer for waveform queue. Nextion supports at most 4 waveform + /// channels (IDs 0-3), so 4 entries is both the correct maximum and a safe default. + StaticRingBuffer<NextionQueue *, 4> waveform_queue_; +#endif // USE_NEXTION_WAVEFORM uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; @@ -1460,7 +1472,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe const std::string &variable_name_to_send, const std::string &state_value, bool is_sleep_safe = false); +#ifdef USE_NEXTION_WAVEFORM void check_pending_waveform_(); +#endif // USE_NEXTION_WAVEFORM #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP8266 diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index 2c516fc80f2..4a2dc90d40e 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -33,7 +33,9 @@ class NextionBase { const std::string &variable_name_to_send, const std::string &state_value) = 0; +#ifdef USE_NEXTION_WAVEFORM virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; +#endif // USE_NEXTION_WAVEFORM virtual void add_to_get_queue(NextionComponentBase *component) = 0; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index a7e65b5ddf9..a332d342ee5 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -217,6 +217,7 @@ void Nextion::set_component_value(const char *component, int32_t value) { this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value); } +#ifdef USE_NEXTION_WAVEFORM void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, value); @@ -226,6 +227,7 @@ void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, value); } +#endif // USE_NEXTION_WAVEFORM void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x); diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index c1d0ae8ed11..6676d019201 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -64,6 +64,7 @@ class NextionComponentBase { uint8_t get_component_id() const { return this->component_id_; } void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } +#ifdef USE_NEXTION_WAVEFORM uint8_t get_wave_channel_id() const { return this->wave_chan_id_; } void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } @@ -76,6 +77,7 @@ class NextionComponentBase { this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent); } } +#endif // USE_NEXTION_WAVEFORM const std::string &get_variable_name() const { return this->variable_name_; } const std::string &get_variable_name_to_send() const { return this->variable_name_to_send_; } @@ -85,19 +87,23 @@ class NextionComponentBase { virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; virtual void send_state_to_nextion(){}; bool get_needs_to_send_update() const { return this->needs_to_send_update_; } +#ifdef USE_NEXTION_WAVEFORM // Remove before 2026.10.0 ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0") uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); } void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } +#endif // USE_NEXTION_WAVEFORM protected: std::string variable_name_; std::string variable_name_to_send_; uint8_t component_id_ = 0; +#ifdef USE_NEXTION_WAVEFORM uint8_t wave_chan_id_ = UINT8_MAX; std::vector<uint8_t> wave_buffer_; int wave_max_length_ = 255; +#endif // USE_NEXTION_WAVEFORM bool needs_to_send_update_; }; diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index cab531f1db6..7351d8f1d54 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -85,16 +85,16 @@ async def to_code(config): cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) if CONF_WAVE_CHANNEL_ID in config: + cg.add_define("USE_NEXTION_WAVEFORM") cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID])) - - if CONF_WAVEFORM_SEND_LAST_VALUE in config: - cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])) - - if CONF_WAVE_MAX_VALUE in config: - cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) - - if CONF_WAVE_MAX_LENGTH in config: - cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) + if CONF_WAVEFORM_SEND_LAST_VALUE in config: + cg.add( + var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE]) + ) + if CONF_WAVE_MAX_VALUE in config: + cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) + if CONF_WAVE_MAX_LENGTH in config: + cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) @automation.register_action( diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index d4fad86286a..ca657522f96 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -10,37 +10,44 @@ void NextionSensor::process_sensor(const std::string &variable_name, int state) if (!this->nextion_->is_setup()) return; - if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) { +#ifdef USE_NEXTION_WAVEFORM + if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) +#else // USE_NEXTION_WAVEFORM + if (this->variable_name_ == variable_name) +#endif // USE_NEXTION_WAVEFORM + { this->publish_state(state); ESP_LOGD(TAG, "Sensor: %s=%d", variable_name.c_str(), state); } } +#ifdef USE_NEXTION_WAVEFORM void NextionSensor::add_to_wave_buffer(float state) { this->needs_to_send_update_ = true; - int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100); - - wave_buffer_.push_back(wave_state); - + this->wave_buffer_.push_back(wave_state); if (this->wave_buffer_.size() > (size_t) this->wave_max_length_) { this->wave_buffer_.erase(this->wave_buffer_.begin()); } } +#endif // USE_NEXTION_WAVEFORM void NextionSensor::update() { if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; +#ifdef USE_NEXTION_WAVEFORM if (this->wave_chan_id_ == UINT8_MAX) { this->nextion_->add_to_get_queue(this); } else { if (this->send_last_value_) { this->add_to_wave_buffer(this->last_value_); } - this->wave_update_(); } +#else // USE_NEXTION_WAVEFORM + this->nextion_->add_to_get_queue(this); +#endif // USE_NEXTION_WAVEFORM } void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { @@ -50,61 +57,60 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { if (std::isnan(state)) return; - if (this->wave_chan_id_ == UINT8_MAX) { - if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { - this->needs_to_send_update_ = true; - } else { - this->needs_to_send_update_ = false; - - if (this->precision_ > 0) { - double to_multiply = pow(10, this->precision_); - int state_value = (int) (state * to_multiply); - - this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); - } else { - this->nextion_->add_no_result_to_queue_with_set(this, (int) state); - } - } - } - } else { +#ifdef USE_NEXTION_WAVEFORM + if (this->wave_chan_id_ != UINT8_MAX) { + // Waveform sensor — buffer the value, don't send directly. if (this->send_last_value_) { this->last_value_ = state; // Update will handle setting the buffer } else { this->add_to_wave_buffer(state); } + this->update_component_settings(); + return; + } +#endif // USE_NEXTION_WAVEFORM + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + if (this->precision_ > 0) { + double to_multiply = pow(10, this->precision_); + int state_value = (int) (state * to_multiply); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } } float published_state = state; - if (this->wave_chan_id_ == UINT8_MAX) { - if (publish) { - if (this->precision_ > 0) { - double to_multiply = pow(10, -this->precision_); - published_state = (float) (state * to_multiply); - } - - this->publish_state(published_state); + if (publish) { + if (this->precision_ > 0) { + double to_multiply = pow(10, -this->precision_); + published_state = (float) (state * to_multiply); } + this->publish_state(published_state); } this->update_component_settings(); ESP_LOGN(TAG, "Write: %s=%lf", this->variable_name_.c_str(), published_state); } +#ifdef USE_NEXTION_WAVEFORM void NextionSensor::wave_update_() { if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) { return; } - #ifdef NEXTION_PROTOCOL_LOG size_t buffer_to_send = this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255 - ESP_LOGN(TAG, "Wave update: %zu/%zu vals to comp %d ch %d", buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); -#endif - +#endif // NEXTION_PROTOCOL_LOG this->nextion_->add_addt_command_to_queue(this); } +#endif // USE_NEXTION_WAVEFORM } // namespace esphome::nextion diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index f1a3ff72ec0..72e3982b3a1 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -15,22 +15,30 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol void update_component() override { this->update(); } void update() override; - void add_to_wave_buffer(float state); void set_precision(uint8_t precision) { this->precision_ = precision; } void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } - void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } - void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } void process_sensor(const std::string &variable_name, int state) override; void set_state(float state) override { this->set_state(state, true, true); } void set_state(float state, bool publish) override { this->set_state(state, publish, true); } void set_state(float state, bool publish, bool send_to_nextion) override; + NextionQueueType get_queue_type() const override { +#ifdef USE_NEXTION_WAVEFORM + return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; +#else // USE_NEXTION_WAVEFORM + return NextionQueueType::SENSOR; +#endif // USE_NEXTION_WAVEFORM + } + +#ifdef USE_NEXTION_WAVEFORM + void add_to_wave_buffer(float state); + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } - NextionQueueType get_queue_type() const override { - return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; - } +#endif // USE_NEXTION_WAVEFORM + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value, publish, send_to_nextion); @@ -38,10 +46,11 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol protected: uint8_t precision_ = 0; +#ifdef USE_NEXTION_WAVEFORM uint32_t wave_maxvalue_ = 255; - float last_value_ = 0; bool send_last_value_ = true; void wave_update_(); +#endif // USE_NEXTION_WAVEFORM }; } // namespace esphome::nextion diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 23e65f55bc0..faa8c6d4b0e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -123,6 +123,7 @@ #define USE_NEXTION_MAX_COMMANDS_PER_LOOP #define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD +#define USE_NEXTION_WAVEFORM #define USE_NUMBER #define USE_OUTPUT #define USE_POWER_SUPPLY From 4134763f3455929a837465c037fca0ab55faa817 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:32:10 -0400 Subject: [PATCH 1181/2056] [at581x][canbus] Fix walrus operator skipping falsy config values (#15390) --- esphome/components/at581x/__init__.py | 4 ++-- esphome/components/canbus/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 0780814ea6e..34e65706288 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -177,7 +177,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): template_ = int(template_ / 1000000) cg.add(var.set_frequency(template_)) - if sens_dist := config.get(CONF_SENSING_DISTANCE): + if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: template_ = await cg.templatable(sens_dist, args, int) cg.add(var.set_sensing_distance(template_)) @@ -209,7 +209,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): template_ = int(template_) cg.add(var.set_trigger_keep(template_)) - if stage_gain := config.get(CONF_STAGE_GAIN): + if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: template_ = await cg.templatable(stage_gain, args, int) cg.add(var.set_stage_gain(template_)) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index c94c8647a95..7d3bf78f492 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -161,7 +161,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) - if can_id := config.get(CONF_CAN_ID): + if (can_id := config.get(CONF_CAN_ID)) is not None: can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) From 4d0d3cc271754930b6c33cf0e02b439173af6e3a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:53:53 -0400 Subject: [PATCH 1182/2056] [sen5x] Remove dead voc_baseline config option (#15391) --- esphome/components/sen5x/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 9fe51121f16..ce35cf5bf15 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -25,7 +25,6 @@ from esphome.const import ( CONF_TEMPERATURE_COMPENSATION, CONF_TIME_CONSTANT, CONF_VOC, - CONF_VOC_BASELINE, DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PM1, @@ -165,7 +164,6 @@ CONFIG_SCHEMA = ( gain_factor=230, ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, From be3e0c27bfc0910f1581ab9fb837ecff113cbb50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 2 Apr 2026 11:28:12 -1000 Subject: [PATCH 1183/2056] [core] Inline fast path for enable_loop (#15392) --- esphome/core/component.cpp | 10 ++++------ esphome/core/component.h | 7 ++++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2b5aba2a7b3..0f68f0c8e02 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -294,12 +294,10 @@ void Component::disable_loop() { App.disable_component_loop_(this); } } -void Component::enable_loop() { - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); - this->set_component_state_(COMPONENT_STATE_LOOP); - App.enable_component_loop_(this); - } +void Component::enable_loop_slow_path_() { + ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); + this->set_component_state_(COMPONENT_STATE_LOOP); + App.enable_component_loop_(this); } void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: diff --git a/esphome/core/component.h b/esphome/core/component.h index f091f9434c9..e2b7aa85d3a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -242,7 +242,10 @@ class Component { * @note Components should call this->enable_loop() on themselves, not on other components. * This ensures the component's state is properly updated along with the loop partition. */ - void enable_loop(); + void enable_loop() { + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) + this->enable_loop_slow_path_(); + } /** Thread and ISR-safe version of enable_loop() that can be called from any context. * @@ -344,6 +347,8 @@ class Component { virtual void call_setup(); void call_dump_config_(); + void enable_loop_slow_path_(); + /// Helper to set component state (clears state bits and sets new state) inline void set_component_state_(uint8_t state) { this->component_state_ &= ~COMPONENT_STATE_MASK; From 710186998baa241b360f30ad08e90218eaea8a3a Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 2 Apr 2026 18:12:05 -0500 Subject: [PATCH 1184/2056] [ota] Use modernized namespace syntax (#15398) --- esphome/components/ota/automation.h | 6 ++---- esphome/components/ota/ota_backend.cpp | 6 ++---- esphome/components/ota/ota_backend.h | 6 ++---- esphome/components/ota/ota_backend_arduino_libretiny.cpp | 7 ++----- esphome/components/ota/ota_backend_arduino_libretiny.h | 7 ++----- esphome/components/ota/ota_backend_arduino_rp2040.cpp | 7 ++----- esphome/components/ota/ota_backend_arduino_rp2040.h | 7 ++----- esphome/components/ota/ota_backend_esp_idf.cpp | 6 ++---- esphome/components/ota/ota_backend_esp_idf.h | 6 ++---- 9 files changed, 18 insertions(+), 40 deletions(-) diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 92c0050ba01..29a8878136e 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class OTAStateChangeTrigger final : public Trigger<OTAState>, public OTAStateListener { public: @@ -67,6 +66,5 @@ class OTAErrorTrigger final : public Trigger<uint8_t>, public OTAStateListener { OTAComponent *parent_; }; -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 01a18a58efe..17949de642f 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -1,7 +1,6 @@ #include "ota_backend.h" -namespace esphome { -namespace ota { +namespace esphome::ota { #ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -34,5 +33,4 @@ void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) } #endif -} // namespace ota -} // namespace esphome +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index ab0ec58e8a8..db79370bb38 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -8,8 +8,7 @@ #include <vector> #endif -namespace esphome { -namespace ota { +namespace esphome::ota { enum OTAResponseTypes { OTA_RESPONSE_OK = 0x00, @@ -117,5 +116,4 @@ OTAGlobalCallback *get_global_ota_callback(); // - notify_state_deferred_() when in separate task (e.g., web_server OTA) // This ensures proper listener execution in all contexts. #endif -} // namespace ota -} // namespace esphome +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index d364f750074..dcd71e92dda 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -7,8 +7,7 @@ #include <Update.h> -namespace esphome { -namespace ota { +namespace esphome::ota { static const char *const TAG = "ota.arduino_libretiny"; @@ -66,7 +65,5 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() { void ArduinoLibreTinyOTABackend::abort() { Update.abort(); } -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 4514bf84bda..3d426e67599 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -4,8 +4,7 @@ #include "esphome/core/defines.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class ArduinoLibreTinyOTABackend final { public: @@ -22,7 +21,5 @@ class ArduinoLibreTinyOTABackend final { std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend(); -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index e2a57ec665b..bc8ef812e6e 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -9,8 +9,7 @@ #include <Updater.h> -namespace esphome { -namespace ota { +namespace esphome::ota { static const char *const TAG = "ota.arduino_rp2040"; @@ -75,8 +74,6 @@ void ArduinoRP2040OTABackend::abort() { rp2040::preferences_prevent_write(false); } -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_RP2040 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 0956cb4b4b9..05bd2f5cc4e 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -6,8 +6,7 @@ #include "esphome/core/defines.h" #include "esphome/core/macros.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class ArduinoRP2040OTABackend final { public: @@ -24,8 +23,6 @@ class ArduinoRP2040OTABackend final { std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend(); -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_RP2040 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 925bb396454..efaf810ca30 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -8,8 +8,7 @@ #include <esp_task_wdt.h> #include <spi_flash_mmap.h> -namespace esphome { -namespace ota { +namespace esphome::ota { std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); } @@ -112,6 +111,5 @@ void IDFOTABackend::abort() { this->update_handle_ = 0; } -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index a0f538afc02..d007bcd1281 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -7,8 +7,7 @@ #include <esp_ota_ops.h> -namespace esphome { -namespace ota { +namespace esphome::ota { class IDFOTABackend final { public: @@ -29,6 +28,5 @@ class IDFOTABackend final { std::unique_ptr<IDFOTABackend> make_ota_backend(); -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif // USE_ESP32 From af662da90d92f4e802cb6ae25372701b6cc22cd0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:28:45 +1000 Subject: [PATCH 1185/2056] [mipi_spi] Rotation and buffer size changes (#15047) --- esphome/components/const/__init__.py | 2 + esphome/components/display/__init__.py | 38 ++++ esphome/components/display/display.h | 2 +- esphome/components/image/__init__.py | 3 +- esphome/components/mipi/__init__.py | 70 ++++-- esphome/components/mipi_dsi/display.py | 3 +- esphome/components/mipi_dsi/mipi_dsi.cpp | 12 +- esphome/components/mipi_dsi/mipi_dsi.h | 2 - esphome/components/mipi_rgb/display.py | 3 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 10 +- esphome/components/mipi_rgb/mipi_rgb.h | 2 - esphome/components/mipi_spi/display.py | 122 +++------- esphome/components/mipi_spi/mipi_spi.cpp | 10 +- esphome/components/mipi_spi/mipi_spi.h | 212 ++++++++++++------ tests/component_tests/display/__init__.py | 0 .../display/test_display_metadata.py | 81 +++++++ .../mipi_spi/test_display_metadata.py | 200 +++++++++++++++++ tests/component_tests/mipi_spi/test_init.py | 9 +- 18 files changed, 557 insertions(+), 224 deletions(-) create mode 100644 tests/component_tests/display/__init__.py create mode 100644 tests/component_tests/display/test_display_metadata.py create mode 100644 tests/component_tests/mipi_spi/test_display_metadata.py diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 0eb37e3029e..846d3fd883b 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -32,4 +32,6 @@ ICON_CURRENT_DC = "mdi:current-dc" ICON_SOLAR_PANEL = "mdi:solar-panel" ICON_SOLAR_POWER = "mdi:solar-power" +KEY_METADATA = "metadata" + UNIT_AMPERE_HOUR = "Ah" diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 6367f88acc4..4d79a0a31bb 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,6 +1,9 @@ +from dataclasses import dataclass + from esphome import automation, core from esphome.automation import maybe_simple_id import esphome.codegen as cg +from esphome.components.const import KEY_METADATA import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, @@ -16,7 +19,9 @@ from esphome.const import ( SCHEDULER_DONT_RUN, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj +DOMAIN = "display" IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") @@ -146,6 +151,39 @@ async def setup_display_core_(var, config): cg.add(var.show_test_card()) +# Storage of display metadata in a central location, accessible via the id + + +@dataclass(frozen=True) +class DisplayMetaData: + width: int = 0 + height: int = 0 + has_writer: bool = False + has_hardware_rotation: bool = False + + +def get_all_display_metadata() -> dict[str, DisplayMetaData]: + """Get all display metadata.""" + return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + + +def get_display_metadata(display_id: str) -> DisplayMetaData | None: + """Get display metadata by ID for use by other components.""" + return get_all_display_metadata().get(display_id, DisplayMetaData()) + + +def add_metadata( + id: str | MockObj, + width: int, + height: int, + has_writer: bool, + has_hardware_rotation: bool = False, +): + get_all_display_metadata()[str(id)] = DisplayMetaData( + width, height, has_writer, has_hardware_rotation + ) + + async def register_display(var, config): await cg.register_component(var, config) await setup_display_core_(var, config) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index e40f6ec963b..6e38300d0e5 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -704,7 +704,7 @@ class Display : public PollingComponent { void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } /// Internal method to set the display rotation with. - void set_rotation(DisplayRotation rotation); + virtual void set_rotation(DisplayRotation rotation); // Internal method to set display auto clearing. void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 6fb0e46d937..4a5fcc385e8 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA import esphome.config_validation as cv from esphome.const import ( CONF_DEFAULTS, @@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key" CONF_ALPHA_CHANNEL = "alpha_channel" CONF_INVERT_ALPHA = "invert_alpha" CONF_IMAGES = "images" -KEY_METADATA = "metadata" TRANSPARENCY_TYPES = ( CONF_OPAQUE, diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 4dbc81caa2b..ccd43c72cf2 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left MADCTL_XFLIP = 0x02 # Mirror the display horizontally MADCTL_YFLIP = 0x01 # Mirror the display vertically +MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips + # Special constant for delays in command sequences DELAY_FLAG = 0xFFF # Special flag to indicate a delay @@ -329,7 +331,13 @@ class DriverChip: return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms - def get_dimensions(self, config) -> tuple[int, int, int, int]: + def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]: + """ + Return the dimensions of the current model. + :param config: The current configuration + :param swap: If width/height should be swapped when axes are swapped. + :return: + """ if CONF_DIMENSIONS in config: # Explicit dimensions, just use as is dimensions = config[CONF_DIMENSIONS] @@ -361,13 +369,12 @@ class DriverChip: ) offset_height = native_height - height - offset_height # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer - if transform.get(CONF_SWAP_XY) is True: + if swap and transform.get(CONF_SWAP_XY) is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height - def get_transform(self, config) -> dict[str, bool]: - can_transform = self.rotation_as_transform(config) + def get_base_transform(self, config): transform = config.get( CONF_TRANSFORM, { @@ -376,14 +383,20 @@ class DriverChip: CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) - if not isinstance(transform, dict): - # Presumably disabled - return { - CONF_MIRROR_X: False, - CONF_MIRROR_Y: False, - CONF_SWAP_XY: False, - CONF_TRANSFORM: False, - } + if isinstance(transform, dict): + return transform + + # Transform is disabled + return { + CONF_MIRROR_X: False, + CONF_MIRROR_Y: False, + CONF_SWAP_XY: False, + CONF_TRANSFORM: False, + } + + def get_transform(self, config) -> dict[str, bool]: + transform = self.get_base_transform(config) + can_transform = self.rotation_as_transform(config) # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] @@ -411,11 +424,15 @@ class DriverChip: return {cv.Required(CONF_SWAP_XY): cv.boolean} return {cv.Optional(CONF_SWAP_XY, default=False): validator} - def add_madctl(self, sequence: list, config: dict): - # Add the MADCTL command to the sequence based on the configuration. - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) + def get_madctl(self, transform: dict, config: dict) -> int: + """ + Convert a transform to MADCTL bits + :param transform: The transform dict + :param use_flip: Whether to use axis flips + :return: MADCTL value + """ + use_flip = config.get(CONF_USE_AXIS_FLIPS, False) + madctl = MADCTL_FLIP_FLAG if use_flip else 0 if transform[CONF_MIRROR_X]: madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX if transform[CONF_MIRROR_Y]: @@ -424,22 +441,28 @@ class DriverChip: madctl |= MADCTL_MV if config[CONF_COLOR_ORDER] == MODE_BGR: madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) return madctl + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + # This takes into account rotation if it can be implemented in the transform + transform = self.get_transform(config) + madctl = self.get_madctl(transform, config) + sequence.append((MADCTL, madctl & 0xFF)) + def skip_command(self, command: str): """ Allow suppressing a standard command in the init sequence. """ return self.get_default(f"no_{command.lower()}", False) - def get_sequence(self, config) -> tuple[tuple[int, ...], int]: + def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]: """ Create the init sequence for the display. Use the default sequence from the model, if any, and append any custom sequence provided in the config. Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence - Pixel format, color order, and orientation will be set. - Returns a tuple of the init sequence and the computed MADCTL value. + MADCTL will be set if add_madctl is True + Returns the init sequence """ sequence = list(self.initsequence or ()) custom_sequence = config.get(CONF_INIT_SEQUENCE, []) @@ -457,7 +480,8 @@ class DriverChip: if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - madctl = self.add_madctl(sequence, config) + if add_madctl: + self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: @@ -471,7 +495,7 @@ class DriverChip: # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed - return flatten_sequence(sequence), madctl + return flatten_sequence(sequence) def requires_buffer(config) -> bool: diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 85bfad7f1a8..026c2145692 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -192,10 +192,9 @@ async def to_code(config): width, height, _offset_width, _offset_height = model.get_dimensions(config) var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode) - sequence, madctl = model.get_sequence(config) + sequence = model.get_sequence(config) cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_init_sequence(sequence)) - cg.add(var.set_madctl(madctl)) cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index e8e9ca2bfbf..fc59aeffe89 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() { "\n Model: %s" "\n Width: %u" "\n Height: %u" - "\n Mirror X: %s" - "\n Mirror Y: %s" - "\n Swap X/Y: %s" "\n Rotation: %d degrees" "\n DSI Lanes: %u" "\n Lane Bit Rate: %.0fMbps" @@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() { "\n VSync Front Porch: %u" "\n Buffer Color Depth: %d bit" "\n Display Pixel Mode: %d bit" - "\n Color Order: %s" "\n Invert Colors: %s" "\n Pixel Clock: %.1fMHz", - this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), - YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, - this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, - this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_, - (3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", + this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_, + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_, YESNO(this->invert_colors_), this->pclk_frequency_); LOG_PIN(" Reset Pin ", this->reset_pin_); } diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 6e27912aa51..c27c9ccc6ef 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display { void set_model(const char *model) { this->model_ = model; } void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } - void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } void smark_failed(const LogString *message, esp_err_t err); @@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display { std::vector<GPIOPin *> enable_pins_{}; size_t width_{}; size_t height_{}; - uint8_t madctl_{}; uint16_t hsync_pulse_width_ = 10; uint16_t hsync_back_porch_ = 10; uint16_t hsync_front_porch_ = 20; diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 0aa8c56719b..4952bda95f0 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -265,9 +265,8 @@ async def to_code(config): if CONF_SPI_ID in config: await spi.register_spi_device(var, config, write_only=True) - sequence, madctl = model.get_sequence(config) + sequence = model.get_sequence(config) cg.add(var.set_init_sequence(sequence)) - cg.add(var.set_madctl(madctl)) cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 0b0a5344e47..6f5e2f2490c 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() { MipiRgb::dump_config(); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); - ESP_LOGCONFIG(TAG, - " SPI Data rate: %uMHz" - "\n Mirror X: %s" - "\n Mirror Y: %s" - "\n Swap X/Y: %s" - "\n Color Order: %s", - (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), - YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); + ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); } #endif // USE_SPI diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index 76b48bb2493..accc251a184 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -38,7 +38,6 @@ class MipiRgb : public display::Display { display::ColorOrder get_color_mode() { return this->color_mode_; } void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } - void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } @@ -84,7 +83,6 @@ class MipiRgb : public display::Display { uint16_t vsync_front_porch_ = 10; uint32_t pclk_frequency_ = 16 * 1000 * 1000; bool pclk_inverted_{true}; - uint8_t madctl_{}; const char *model_{"Unknown"}; bool invert_colors_{}; display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 8dccfa3a92a..6aa98e3f668 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -10,7 +10,7 @@ from esphome.components.const import ( CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING, ) -from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS +from esphome.components.display import CONF_SHOW_TEST_CARD from esphome.components.mipi import ( CONF_PIXEL_MODE, CONF_USE_AXIS_FLIPS, @@ -47,12 +47,10 @@ from esphome.const import ( CONF_MIRROR_Y, CONF_MODEL, CONF_RESET_PIN, - CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import CORE from esphome.cpp_generator import TemplateArguments from esphome.final_validate import full_config @@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = { def denominator(config): """ Calculate the best denominator for a buffer size fraction. - The denominator must be a number between 2 and 16 that divides the display height evenly, + The denominator should be a number between 2 and 16 that divides the display height evenly, and the fraction represented by the denominator must be less than or equal to the given fraction. :config: The configuration dictionary containing the buffer size fraction and display dimensions :return: The denominator to use for the buffer size fraction """ model = MODELS[config[CONF_MODEL]] frac = config.get(CONF_BUFFER_SIZE) - if frac is None or frac > 0.75: + _width, height, _offset_width, _offset_height = model.get_dimensions(config) + if frac is None or frac > 0.75 or height < 32: return 1 - height, _width, _offset_width, _offset_height = model.get_dimensions(config) try: return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) except StopIteration: - raise cv.Invalid( - f"Buffer size fraction {frac} is not compatible with display height {height}" - ) from StopIteration + # No exact divisor, just use the closest. + return next(x for x in range(2, 17) if frac >= 1 / x) def model_schema(config): @@ -287,30 +284,19 @@ def _final_validate(config): config[CONF_SHOW_TEST_CARD] = True if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config: - if not requires_buffer(config): - return config # No buffer needed, so no need to set a buffer size # If PSRAM is not enabled, choose a small buffer size by default if not requires_buffer(config): - # not our problem. - return config + return config # No buffer needed, so no need to set a buffer size color_depth = get_color_depth(config) frac = denominator(config) - height, width, _offset_width, _offset_height = model.get_dimensions(config) + width, height, _offset_width, _offset_height = model.get_dimensions(config) buffer_size = color_depth // 8 * width * height // frac - # Target a buffer size of 20kB - fraction = 20000.0 / buffer_size - try: - config[CONF_BUFFER_SIZE] = 1.0 / next( - x for x in range(2, 17) if fraction >= 1 / x and height % x == 0 - ) - except StopIteration: - # Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0 - # PSRAM will be needed. - if CORE.is_esp32: - raise cv.Invalid( - "PSRAM is required for this display" - ) from StopIteration + # Target a buffer size of 20kB, except for large displays, which shouldn't end up here + fraction = min(20000.0, buffer_size // 16) / buffer_size + config[CONF_BUFFER_SIZE] = 1.0 / next( + x for x in range(2, 17) if fraction >= 1 / x + ) return config @@ -318,39 +304,6 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -def get_transform(config): - """ - Get the transformation configuration for the display. - :param config: - :return: - """ - model = MODELS[config[CONF_MODEL]] - can_transform = model.rotation_as_transform(config) - transform = config.get( - CONF_TRANSFORM, - { - CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False), - }, - ) - - # Can we use the MADCTL register to set the rotation? - if can_transform and CONF_TRANSFORM not in config: - rotation = config[CONF_ROTATION] - if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] - elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - else: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] - transform[CONF_TRANSFORM] = True - return transform - - def get_instance(config): """ Get the type of MipiSpi instance to create based on the configuration, @@ -359,7 +312,16 @@ def get_instance(config): :return: type, template arguments """ model = MODELS[config[CONF_MODEL]] - width, height, offset_width, offset_height = model.get_dimensions(config) + has_hardware_transform = config.get( + CONF_TRANSFORM + ) != CONF_DISABLED and model.transforms == { + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_SWAP_XY, + } + width, height, offset_width, offset_height = model.get_dimensions( + config, not has_hardware_transform + ) color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) bufferpixels = COLOR_DEPTHS[color_depth] @@ -373,57 +335,43 @@ def get_instance(config): bus_type = BusTypes[bus_type] buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) - rotation = ( - 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) - ) + madctl = model.get_madctl(model.get_base_transform(config), config) + has_writer = requires_buffer(config) templateargs = [ buffer_type, bufferpixels, config[CONF_BYTE_ORDER] == "big_endian", display_pixel_mode, bus_type, + width, + height, + offset_width, + offset_height, + madctl, + has_hardware_transform, ] + display.add_metadata( + config[CONF_ID], width, height, has_writer, has_hardware_transform + ) # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi if requires_buffer(config): templateargs.extend( [ - width, - height, - offset_width, - offset_height, - DISPLAY_ROTATIONS[rotation], frac, config[CONF_DRAW_ROUNDING], ] ) return MipiSpiBuffer, templateargs - # Swap height and width if the display is rotated 90 or 270 degrees in software - if rotation in (90, 270): - width, height = height, width - offset_width, offset_height = offset_height, offset_width - templateargs.extend( - [ - width, - height, - offset_width, - offset_height, - ] - ) return MipiSpi, templateargs async def to_code(config): model = MODELS[config[CONF_MODEL]] var_id = config[CONF_ID] + init_sequence = model.get_sequence(config, False) var_id.type, templateargs = get_instance(config) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) - init_sequence, _madctl = model.get_sequence(config) cg.add(var.set_init_sequence(init_sequence)) - if model.rotation_as_transform(config): - if CONF_TRANSFORM in config: - LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") - else: - config[CONF_ROTATION] = 0 cg.add(var.set_model(config[CONF_MODEL])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 90f63245113..2eec3b12d17 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -5,7 +5,8 @@ namespace esphome::mipi_spi { void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness, - GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width, + bool has_hardware_rotation) { ESP_LOGCONFIG(TAG, "MIPI_SPI Display\n" " Model: %s\n" @@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w " Swap X/Y: %s\n" " Mirror X: %s\n" " Mirror Y: %s\n" + " Hardware rotation: %s\n" " Invert colors: %s\n" " Color order: %s\n" " Display pixels: %d bits\n" @@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w " SPI Data rate: %uMHz\n" " SPI Bus width: %d", model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", - display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000), - bus_width); + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors), + (madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode, + static_cast<unsigned>(data_rate / 1000000), bus_width); LOG_PIN(" CS Pin: ", cs); LOG_PIN(" Reset Pin: ", reset); LOG_PIN(" DC Pin: ", dc); diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 083ff9507fe..423226b1d70 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A; static constexpr uint8_t SWIRE2 = 0x5B; static constexpr uint8_t PAGESEL = 0xFE; -static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top -static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left -static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes -static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order -static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order -static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally -static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically +static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically +static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits static constexpr uint8_t DELAY_FLAG = 0xFF; // store a 16 bit value in a buffer, big endian. @@ -66,7 +67,8 @@ enum BusType { // Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness, - GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width, + bool has_hardware_rotation); /** * Base class for MIPI SPI displays. @@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w * buffer */ template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, - int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT> + int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION> class MipiSpi : public display::Display, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { @@ -103,10 +105,39 @@ class MipiSpi : public display::Display, this->brightness_ = brightness; this->reset_params_(); } + void set_rotation(display::DisplayRotation rotation) override { + this->rotation_ = rotation; + if constexpr (HAS_HARDWARE_ROTATION) { + this->reset_params_(); + } + } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } - int get_width_internal() override { return WIDTH; } - int get_height_internal() override { return HEIGHT; } + int get_width() override { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return HEIGHT; + return WIDTH; + } + + int get_height() override { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return WIDTH; + return HEIGHT; + } + + // If hardware rotation is in use, the actual display width/height changes with rotation + int get_width_internal() override { + if constexpr (HAS_HARDWARE_ROTATION) + return get_width(); + return WIDTH; + } + int get_height_internal() override { + if constexpr (HAS_HARDWARE_ROTATION) + return get_height(); + return HEIGHT; + } void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } // reset the display, and write the init sequence @@ -166,9 +197,6 @@ class MipiSpi : public display::Display, case INVERT_ON: this->invert_colors_ = true; break; - case MADCTL_CMD: - this->madctl_ = arg_byte; - break; case BRIGHTNESS: this->brightness_ = arg_byte; break; @@ -177,13 +205,13 @@ class MipiSpi : public display::Display, break; } const auto *ptr = vec.data() + index; - esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); this->write_command_(cmd, ptr, num_args); index += num_args; if (cmd == SLEEP_OUT) delay(10); } } + this->reset_params_(); // init sequence no longer needed this->init_sequence_.clear(); } @@ -206,9 +234,10 @@ class MipiSpi : public display::Display, } void dump_config() override { - internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, - DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, - this->mode_, this->data_rate_, BUS_TYPE); + internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, + this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, + this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, + HAS_HARDWARE_ROTATION); } protected: @@ -219,10 +248,13 @@ class MipiSpi : public display::Display, // Writes a command to the display, with the given bytes. void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)]; - esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); -#endif + // Don't spam the log after setup + if (this->init_sequence_.empty()) { + esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); + } else { + esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); + } if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); @@ -271,16 +303,60 @@ class MipiSpi : public display::Display, this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); if (this->brightness_.has_value()) this->write_command_(BRIGHTNESS, this->brightness_.value()); + + // calculate new madctl value from base value adjusted for rotation + uint8_t madctl = MADCTL; // lower 8 bits only + constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0; + constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX; + constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY; + if constexpr (HAS_HARDWARE_ROTATION) { + switch (this->rotation_) { + default: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + madctl ^= x_mask; // flip X axis + madctl ^= MADCTL_MV; // swap X and Y axes + break; + case display::DISPLAY_ROTATION_180_DEGREES: + madctl ^= x_mask; // flip X axis + madctl ^= y_mask; // flip Y axis + break; + case display::DISPLAY_ROTATION_270_DEGREES: + madctl ^= y_mask; // flip Y axis + madctl ^= MADCTL_MV; // swap X and Y axes + break; + } + } + esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl); + this->write_command_(MADCTL_CMD, madctl); + } + + uint16_t get_offset_width_() { + if constexpr (HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return OFFSET_HEIGHT; + } + return OFFSET_WIDTH; + } + + uint16_t get_offset_height_() { + if constexpr (HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return OFFSET_WIDTH; + } + return OFFSET_HEIGHT; } // set the address window for the next data write void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); uint8_t buf[4]; - x1 += OFFSET_WIDTH; - x2 += OFFSET_WIDTH; - y1 += OFFSET_HEIGHT; - y2 += OFFSET_HEIGHT; + x1 += get_offset_width_(); + x2 += get_offset_width_(); + y1 += get_offset_height_(); + y2 += get_offset_height_(); put16_be(buf, y1); put16_be(buf + 2, y2); this->write_command_(RASET, buf, sizeof buf); @@ -408,7 +484,6 @@ class MipiSpi : public display::Display, optional<uint8_t> brightness_{}; const char *model_{"Unknown"}; std::vector<uint8_t> init_sequence_{}; - uint8_t madctl_{}; }; /** @@ -427,22 +502,21 @@ class MipiSpi : public display::Display, * @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even) */ template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE, - uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, - int FRACTION, unsigned ROUNDING> + uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, + bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING> class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, - OFFSET_WIDTH, OFFSET_HEIGHT> { + OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> { public: // these values define the buffer size needed to write in accordance with the chip pixel alignment // requirements. If the required rounding does not divide the width and height, we round up to the next multiple and // ignore the extra columns and rows when drawing, but use them to write to the display. - static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING; - static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING; + static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; } - MipiSpiBuffer() { this->rotation_ = ROTATION; } + MipiSpiBuffer() = default; void dump_config() override { - MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, - OFFSET_HEIGHT>::dump_config(); + MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, + MADCTL, HAS_HARDWARE_ROTATION>::dump_config(); esph_log_config(TAG, " Rotation: %d°\n" " Buffer pixels: %d bits\n" @@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS " Buffer bytes: %zu\n" " Draw rounding: %u", this->rotation_, BUFFERPIXEL * 8, FRACTION, - sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING); + sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING); } void setup() override { - MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, - OFFSET_HEIGHT>::setup(); + MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, + MADCTL, HAS_HARDWARE_ROTATION>::setup(); RAMAllocator<BUFFERTYPE> allocator{}; - this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); + this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION); if (this->buffer_ == nullptr) { this->mark_failed(LOG_STR("Buffer allocation failed")); } @@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS } // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of // the display height, - for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) { + for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); + this->start_line_ += this->get_height_internal() / FRACTION) { #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE auto lap = millis(); #endif - this->end_line_ = this->start_line_ + HEIGHT / FRACTION; + this->end_line_ = + clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal()); if (this->auto_clear_enabled_) { this->clear(); } @@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS int w = this->x_high_ - this->x_low_ + 1; int h = this->y_high_ - this->y_low_ + 1; this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, - this->y_low_ - this->start_line_, BUFFER_WIDTH - w); + this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w); // invalidate watermarks - this->x_low_ = WIDTH; - this->y_low_ = HEIGHT; + this->x_low_ = this->get_width_internal(); + this->y_low_ = this->get_height_internal(); this->x_high_ = 0; this->y_high_ = 0; #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE @@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS void draw_pixel_at(int x, int y, Color color) override { if (!this->get_clipping().inside(x, y)) return; - rotate_coordinates(x, y); - if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) + if constexpr (not HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = WIDTH - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = HEIGHT - x - 1; + x = tmp; + } + } + if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_) return; - this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color); + this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color); if (x < this->x_low_) { this->x_low_ = x; } @@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS this->x_low_ = 0; this->y_low_ = this->start_line_; - this->x_high_ = WIDTH - 1; + this->x_high_ = this->get_width_internal() - 1; this->y_high_ = this->end_line_ - 1; - std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color)); - } - - int get_width() override { - if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) - return HEIGHT; - return WIDTH; - } - - int get_height() override { - if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) - return WIDTH; - return HEIGHT; + std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()), + convert_color(color)); } protected: // Rotate the coordinates to match the display orientation. - static void rotate_coordinates(int &x, int &y) { - if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) { - x = WIDTH - x - 1; - y = HEIGHT - y - 1; - } else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) { - auto tmp = x; - x = WIDTH - y - 1; - y = tmp; - } else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) { - auto tmp = y; - y = HEIGHT - x - 1; - x = tmp; - } - } // Convert a color to the buffer pixel format. static BUFFERTYPE convert_color(const Color &color) { diff --git a/tests/component_tests/display/__init__.py b/tests/component_tests/display/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/display/test_display_metadata.py b/tests/component_tests/display/test_display_metadata.py new file mode 100644 index 00000000000..e5697544942 --- /dev/null +++ b/tests/component_tests/display/test_display_metadata.py @@ -0,0 +1,81 @@ +"""Tests for display component metadata functions.""" + +from unittest.mock import patch + +from esphome.components.display import ( + DisplayMetaData, + add_metadata, + get_all_display_metadata, + get_display_metadata, +) +from esphome.cpp_generator import MockObj + + +def test_add_metadata_with_string_id(): + """Test adding metadata with a plain string ID.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("my_display", 320, 240, True) + meta = get_display_metadata("my_display") + assert meta == DisplayMetaData( + width=320, height=240, has_writer=True, has_hardware_rotation=False + ) + + +def test_add_metadata_with_mockobj_id(): + """Test adding metadata with a MockObj ID (converted via str()).""" + with patch("esphome.components.display.CORE.data", {}): + mock_id = MockObj("my_display_obj") + add_metadata(mock_id, 480, 320, False, has_hardware_rotation=True) + meta = get_display_metadata("my_display_obj") + assert meta == DisplayMetaData( + width=480, height=320, has_writer=False, has_hardware_rotation=True + ) + + +def test_add_metadata_hardware_rotation_default(): + """Test that has_hardware_rotation defaults to False.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp", 128, 64, False) + meta = get_display_metadata("disp") + assert meta.has_hardware_rotation is False + + +def test_get_display_metadata_missing_returns_none(): + """Test that querying a non-existent ID returns None.""" + with patch("esphome.components.display.CORE.data", {}): + data = get_display_metadata("no_such_display") + assert data.width == 0 + assert data.height == 0 + assert data.has_writer is False + assert data.has_hardware_rotation is False + + +def test_add_multiple_displays(): + """Test adding metadata for multiple displays.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp_a", 320, 240, True) + add_metadata("disp_b", 128, 64, False, has_hardware_rotation=True) + + all_meta = get_all_display_metadata() + assert len(all_meta) == 2 + assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, False) + assert all_meta["disp_b"] == DisplayMetaData(128, 64, False, True) + + +def test_add_metadata_overwrites_existing(): + """Test that adding metadata for the same ID overwrites the previous entry.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp", 320, 240, True) + add_metadata("disp", 640, 480, False, has_hardware_rotation=True) + meta = get_display_metadata("disp") + assert meta == DisplayMetaData(640, 480, False, True) + + +def test_metadata_is_frozen(): + """Test that DisplayMetaData instances are immutable (frozen dataclass).""" + meta = DisplayMetaData(320, 240, True, False) + try: + meta.width = 640 + assert False, "Expected FrozenInstanceError" + except AttributeError: + pass diff --git a/tests/component_tests/mipi_spi/test_display_metadata.py b/tests/component_tests/mipi_spi/test_display_metadata.py new file mode 100644 index 00000000000..ab42a756943 --- /dev/null +++ b/tests/component_tests/mipi_spi/test_display_metadata.py @@ -0,0 +1,200 @@ +"""Tests for display metadata created by mipi_spi component.""" + +from collections.abc import Callable +from pathlib import Path + +from esphome.components.display import ( + DisplayMetaData, + get_all_display_metadata, + get_display_metadata, +) +from esphome.components.esp32 import ( + KEY_BOARD, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S3, +) +from esphome.components.mipi_spi.display import ( + CONFIG_SCHEMA, + FINAL_VALIDATE_SCHEMA, + get_instance, +) +from esphome.const import PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + + +def validated_config(config): + """Run schema + final validation and return the validated config.""" + return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +def test_metadata_native_quad_default_test_card( + set_core_config: SetCoreConfigCallable, +) -> None: + """A quad-mode display with no explicit drawing gets a test card from final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + config = validated_config({"model": "JC3636W518"}) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 360 + assert meta.height == 360 + # final validation auto-enables show_test_card when no drawing methods are configured + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_single_mode_with_dc_pin( + set_core_config: SetCoreConfigCallable, +) -> None: + """A single-mode display with no explicit drawing gets a test card from final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "ST7735", + "dc_pin": 18, + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 128 + assert meta.height == 160 + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_custom_dimensions( + set_core_config: SetCoreConfigCallable, +) -> None: + """A custom model picks up explicit dimensions.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 480, "height": 320}, + "init_sequence": [[0xA0, 0x01]], + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 480 + assert meta.height == 320 + # final validation auto-enables show_test_card + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_with_test_card_has_writer( + set_core_config: SetCoreConfigCallable, +) -> None: + """When show_test_card is enabled, has_writer should be True.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 240, "height": 240}, + "init_sequence": [[0xA0, 0x01]], + "show_test_card": True, + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.has_writer is True + + +def test_metadata_no_swap_xy_not_full_hardware_rotation( + set_core_config: SetCoreConfigCallable, +) -> None: + """A model that disables swap_xy should report has_hardware_rotation=False.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + # JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only + config = validated_config({"model": "JC3248W535"}) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.has_hardware_rotation is False + + +def test_metadata_multiple_displays_independent( + set_core_config: SetCoreConfigCallable, +) -> None: + """Multiple displays each get their own metadata entry.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config_a = validated_config( + { + "id": "disp_a", + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 320, "height": 240}, + "init_sequence": [[0xA0, 0x01]], + } + ) + config_b = validated_config( + { + "id": "disp_b", + "model": "custom", + "dc_pin": 19, + "dimensions": {"width": 128, "height": 64}, + "init_sequence": [[0xA0, 0x01]], + } + ) + get_instance(config_a) + get_instance(config_b) + + all_meta = get_all_display_metadata() + # final validation auto-enables show_test_card for both + assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, True) + assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, True) + + +def test_metadata_via_code_generation_native( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Full code generation for native.yaml should produce correct metadata.""" + generate_main(component_fixture_path("native.yaml")) + all_meta = get_all_display_metadata() + # native.yaml: model JC3636W518 -> 360x360, no writer, full hardware rotation + assert len(all_meta) == 1 + meta = next(iter(all_meta.values())) + assert meta == DisplayMetaData( + width=360, height=360, has_writer=True, has_hardware_rotation=True + ) + + +def test_metadata_via_code_generation_lvgl( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Full code generation for lvgl.yaml should produce correct metadata.""" + generate_main(component_fixture_path("lvgl.yaml")) + all_meta = get_all_display_metadata() + # lvgl.yaml: model ST7735 -> 128x160, no writer (lvgl draws directly), full hw rotation + assert len(all_meta) == 1 + meta = next(iter(all_meta.values())) + assert meta == DisplayMetaData( + width=128, height=160, has_writer=False, has_hardware_rotation=True + ) diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index bae39d3879f..4873892a8d8 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -204,11 +204,6 @@ def test_transform_and_init_sequence_errors( r"extra keys not allowed @ data\['brightness'\]", id="brightness_not_supported", ), - pytest.param( - {"model": "T-DISPLAY-S3-PRO"}, - "PSRAM is required for this display", - id="psram_required", - ), ], ) def test_esp32s3_specific_errors( @@ -319,7 +314,7 @@ def test_native_generation( main_cpp = generate_main(component_fixture_path("native.yaml")) assert ( - "mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()" + "mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, true, 1, 1>()" in main_cpp ) assert "set_init_sequence({240, 1, 8, 242" in main_cpp @@ -335,7 +330,7 @@ def test_lvgl_generation( main_cpp = generate_main(component_fixture_path("lvgl.yaml")) assert ( - "mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();" + "mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, true>();" in main_cpp ) assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp From bcd8ddeabe46f4ed2509554b3ed51066b891d572 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:44:54 -0400 Subject: [PATCH 1186/2056] [lvgl] Fix ext_click_area property application (#15394) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/schemas.py | 4 +++- esphome/components/lvgl/trigger.py | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 4f1473b6521..2c57452a55c 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -23,6 +23,7 @@ from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid from .defines import ( + CONF_EXT_CLICK_AREA, CONF_SCROLL_DIR, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, @@ -311,6 +312,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, + cv.Optional(CONF_EXT_CLICK_AREA): lvalid.pixels, cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of, cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of, cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of, @@ -318,6 +320,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex ) OBJ_PROPERTIES = { + CONF_EXT_CLICK_AREA, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, CONF_SCROLL_DIR, @@ -433,7 +436,6 @@ def obj_schema(widget_type: WidgetType): return ( part_schema(widget_type.parts) .extend(ALIGN_TO_SCHEMA) - .extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels}) .extend(automation_schema(widget_type.w_type)) .extend( { diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index c52d213e15a..54309cdf891 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -15,7 +15,6 @@ from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, CONF_ALIGN_TO_LAMBDA_ID, - CONF_EXT_CLICK_AREA, DIRECTIONS, LV_EVENT_MAP, LV_EVENT_TRIGGERS, @@ -114,8 +113,6 @@ async def generate_align_tos(config: dict): x = align_to[CONF_X] y = align_to[CONF_Y] lv.obj_align_to(w.obj, target, align, x, y) - if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA): - lv.obj_set_ext_click_area(w.obj, ext_click_area) action_id = config[CONF_ALIGN_TO_LAMBDA_ID] var = new_Pvariable(action_id, await context.get_lambda()) From 6f05e3d20494d6cf5c9f7cd1a0b362e1491cdf7b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:54:44 +1000 Subject: [PATCH 1187/2056] [ci] Run ci-custom.py as a pre-commit check (#15411) --- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 4 ++++ script/run-in-env.py | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71703652e81..06e8189f54b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -723,7 +723,7 @@ jobs: cache-key: ${{ needs.common.outputs.cache-key }} - uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: - SKIP: pylint,clang-tidy-hash + SKIP: pylint,clang-tidy-hash,ci-custom - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ff1685ea7c..f8c21aad367 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,3 +65,7 @@ repos: files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$ pass_filenames: false additional_dependencies: [] + - id: ci-custom + name: ci-custom + entry: python3 script/run-in-env.py script/ci-custom.py + language: system diff --git a/script/run-in-env.py b/script/run-in-env.py index 886e65db276..9283ba9940f 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -44,7 +44,8 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - subprocess.run(sys.argv[1:], check=False, close_fds=False) + result = subprocess.run(sys.argv[1:], check=False, close_fds=False) + sys.exit(result.returncode) else: print( "No command provided to run in the virtual environment.", From 5548a3277118c7c051592a25061c0f29bab63061 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 3 Apr 2026 06:15:51 -0400 Subject: [PATCH 1188/2056] [ili9xxx] Fix SPI MOSI pin validation never executing (#15399) --- esphome/components/ili9xxx/display.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index bfb2300f4f6..185a74fa41d 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -210,8 +210,8 @@ def final_validate(config): ): LOGGER.info("Consider enabling PSRAM if available for the display buffer") - return spi.final_validate_device_schema( - "ili9xxx", require_miso=False, require_mosi=True + spi.final_validate_device_schema("ili9xxx", require_miso=False, require_mosi=True)( + config ) From 8360502a94ea7ee3787a6f38d5e44ed4a7074fed Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:01:04 +1000 Subject: [PATCH 1189/2056] [ci] Fix deprecated-component matcher (#15417) --- .github/scripts/auto-label-pr/detectors.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index fc631980193..fb9dadc6a09 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -235,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) { } } - // Get PR head to fetch files from the PR branch - const prNumber = context.payload.pull_request.number; + // Get base branch ref to check if deprecation already exists for the component + // This prevents flagging a PR that simply adds deprecation + const baseRef = context.payload.pull_request.base.ref; // Check each component's __init__.py for DEPRECATED_COMPONENT constant for (const component of components) { const initFile = `esphome/components/${component}/__init__.py`; try { - // Fetch file content from PR head using GitHub API + // Fetch file content from base branch using GitHub API const { data: fileData } = await github.rest.repos.getContent({ owner, repo, path: initFile, - ref: `refs/pull/${prNumber}/head` + ref: baseRef }); // Decode base64 content From 6fecd720490ddffff1305b86340639ee95b62a61 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 3 Apr 2026 08:35:16 -0400 Subject: [PATCH 1190/2056] [ezo_pmp] Fix change_i2c_address action using wrong template type (#15393) --- esphome/components/ezo_pmp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index 1538e303f12..3de796dd258 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -286,7 +286,7 @@ async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, ar paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.int_) cg.add(var.set_address(template_)) return var From 2a5933e4f728580b4b95de13c4783c4d8c9396a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:27:13 -1000 Subject: [PATCH 1191/2056] [host] Add graceful shutdown on SIGINT/SIGTERM (#15387) --- esphome/components/host/core.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index a662e842ee7..0ade4274feb 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -5,11 +5,17 @@ #include "esphome/core/helpers.h" #include "preferences.h" +#include <csignal> #include <sched.h> #include <time.h> #include <cmath> #include <cstdlib> +namespace { +volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void signal_handler(int signal) { s_signal_received = signal; } +} // namespace + namespace esphome { void HOT yield() { ::sched_yield(); } @@ -72,11 +78,17 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } void setup(); void loop(); int main() { + // Install signal handlers for graceful shutdown (flushes preferences to disk) + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + esphome::host::setup_preferences(); setup(); - while (true) { + while (s_signal_received == 0) { loop(); } + esphome::App.run_safe_shutdown_hooks(); + return 0; } #endif // USE_HOST From 5a236697473e554d7b27b29d25f7b188163d3b49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:27:29 -1000 Subject: [PATCH 1192/2056] [scheduler] Fix unrealistic scheduler benchmarks missing periodic drain (#15396) --- tests/benchmarks/core/bench_scheduler.cpp | 124 +++++++++++++++++++--- 1 file changed, 107 insertions(+), 17 deletions(-) diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 9357734cc8e..214fe0e4b87 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -8,7 +8,24 @@ namespace esphome::benchmarks { // Inner iteration count to amortize CodSpeed instrumentation overhead. // Without this, the ~60ns per-iteration valgrind start/stop cost dominates // sub-microsecond benchmarks. -static constexpr int kInnerIterations = 2000; +// Must be divisible by all batch sizes used below (3, 10) to avoid +// pool imbalance at iteration boundaries that causes spurious malloc. +static constexpr int kInnerIterations = 2100; + +// Warm the scheduler pool by registering and replacing items twice. +// The first batch allocates fresh items; the second batch cancels them and +// populates the recycling pool with the cancelled items from the first batch. +static void warm_pool(Scheduler &scheduler, Component *component, int batch_size, uint32_t delay) { + uint32_t now = millis(); + for (int i = 0; i < batch_size; i++) { + scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {}); + } + scheduler.call(++now); + for (int i = 0; i < batch_size; i++) { + scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {}); + } + scheduler.call(++now); +} // --- Scheduler fast path: no work to do --- @@ -83,11 +100,21 @@ static void Scheduler_SetTimeout(benchmark::State &state) { Scheduler scheduler; Component dummy_component; + // Register 3 timeouts then call() — realistic worst case where multiple + // components schedule in the same loop iteration. Keeps item count within + // the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {}); + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -99,22 +126,22 @@ BENCHMARK(Scheduler_SetTimeout); static void Scheduler_SetInterval(benchmark::State &state) { Scheduler scheduler; Component dummy_component; - // Number of distinct interval keys; controls how many unique timers exist - // simultaneously and the drain cadence for process_to_add(). - static constexpr int kKeyCount = 5; + // Register 3 intervals then call() — realistic worst case where multiple + // components schedule in the same loop iteration. Keeps item count within + // the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kKeyCount), 1000, []() {}); - // Drain to_add_ periodically to reflect production behavior where - // process_to_add() runs each main loop iteration. Without this, - // cancelled items accumulate in to_add_ causing O(n²) scan cost. - if ((i + 1) % kKeyCount == 0) { - scheduler.process_to_add(); + scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); } } - // Final drain in case kInnerIterations is not a multiple of 5 - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -128,16 +155,79 @@ static void Scheduler_Defer(benchmark::State &state) { Component dummy_component; // defer() is Component::defer which calls set_timeout(delay=0). - // Call set_timeout directly since defer() is protected. + // Component::defer(func) passes nullptr as the name, which skips + // cancel_item_locked_ entirely — matching production behavior where + // defers are anonymous fire-and-forget callbacks. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 0); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 0, []() {}); + scheduler.set_timeout(&dummy_component, static_cast<const char *>(nullptr), 0, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_Defer); +// --- Scheduler: defer with same ID (cancel-and-replace pattern) --- + +static void Scheduler_Defer_SameID(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // Measures defer with a fixed numeric ID — each call cancels the previous + // pending defer before adding the new one. This is the pattern used by + // components that defer work but want to coalesce rapid updates. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 0); + for (auto _ : state) { + uint32_t now = millis(); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(0), 0, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } + } + scheduler.call(++now); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Defer_SameID); + +// --- Scheduler: set_timeout with batch size exceeding pool (cliff test) --- + +static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure + // the performance cliff when the recycling pool is exhausted and items + // must be malloc'd/freed. + static constexpr int kBatchSize = 10; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); + for (auto _ : state) { + uint32_t now = millis(); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } + } + scheduler.call(++now); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_SetTimeout_ExceedPool); + } // namespace esphome::benchmarks From ea0227a20668de45b993cc745c5b2f86316d8732 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:27:44 -1000 Subject: [PATCH 1193/2056] [benchmarks] Add host platform benchmarks for number, select, and switch (#15405) --- .../benchmarks/components/number/__init__.py | 5 + .../components/number/bench_number.cpp | 121 ++++++++++++++ .../components/number/benchmark.yaml | 1 + .../benchmarks/components/select/__init__.py | 5 + .../components/select/bench_select.cpp | 157 ++++++++++++++++++ .../components/select/benchmark.yaml | 1 + .../benchmarks/components/switch/__init__.py | 5 + .../components/switch/bench_switch.cpp | 137 +++++++++++++++ .../components/switch/benchmark.yaml | 1 + 9 files changed, 433 insertions(+) create mode 100644 tests/benchmarks/components/number/__init__.py create mode 100644 tests/benchmarks/components/number/bench_number.cpp create mode 100644 tests/benchmarks/components/number/benchmark.yaml create mode 100644 tests/benchmarks/components/select/__init__.py create mode 100644 tests/benchmarks/components/select/bench_select.cpp create mode 100644 tests/benchmarks/components/select/benchmark.yaml create mode 100644 tests/benchmarks/components/switch/__init__.py create mode 100644 tests/benchmarks/components/switch/bench_switch.cpp create mode 100644 tests/benchmarks/components/switch/benchmark.yaml diff --git a/tests/benchmarks/components/number/__init__.py b/tests/benchmarks/components/number/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/number/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/number/bench_number.cpp b/tests/benchmarks/components/number/bench_number.cpp new file mode 100644 index 00000000000..57a73930b29 --- /dev/null +++ b/tests/benchmarks/components/number/bench_number.cpp @@ -0,0 +1,121 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/number/number.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Number for benchmarking — control() publishes the value back. +class BenchNumber : public number::Number { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(float value) override { this->publish_state(value); } +}; + +// Helper to create a typical number entity for benchmarks. +static void setup_number(BenchNumber &number) { + number.configure("test_number"); + number.traits.set_min_value(0.0f); + number.traits.set_max_value(100.0f); + number.traits.set_step(1.0f); + number.traits.set_mode(number::NUMBER_MODE_SLIDER); +} + +// --- Number::publish_state() --- +// Measures the publish path: set_has_state, store value, callback dispatch. + +static void NumberPublish_State(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast<float>(i % 100)); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_State); + +// --- Number::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void NumberPublish_WithCallback(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + uint64_t callback_count = 0; + number.add_on_state_callback([&callback_count](float) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast<float>(i % 100)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_WithCallback); + +// --- NumberCall::perform() set value --- +// The most common number call — setting an absolute value. +// Exercises: validation against min/max, control() dispatch. + +static void NumberCall_SetValue(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(50.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float val = static_cast<float>(i % 100); + number.make_call().set_value(val).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_SetValue); + +// --- NumberCall::perform() increment --- +// Exercises: state read, step arithmetic, max clamping. + +static void NumberCall_Increment(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(0.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_increment(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Increment); + +// --- NumberCall::perform() decrement --- +// Exercises: state read, step arithmetic, min clamping. + +static void NumberCall_Decrement(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(100.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_decrement(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Decrement); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/number/benchmark.yaml b/tests/benchmarks/components/number/benchmark.yaml new file mode 100644 index 00000000000..f4356612706 --- /dev/null +++ b/tests/benchmarks/components/number/benchmark.yaml @@ -0,0 +1 @@ +number: diff --git a/tests/benchmarks/components/select/__init__.py b/tests/benchmarks/components/select/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/select/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/select/bench_select.cpp b/tests/benchmarks/components/select/bench_select.cpp new file mode 100644 index 00000000000..8e047d9151c --- /dev/null +++ b/tests/benchmarks/components/select/bench_select.cpp @@ -0,0 +1,157 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/select/select.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Select for benchmarking — control() publishes directly by index. +class BenchSelect : public select::Select { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(size_t index) override { this->publish_state(index); } +}; + +// Helper to create a select with the given options. +static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) { + select.configure(name); + select.traits.set_options(options); + select.publish_state(size_t(0)); +} + +// --- Select::publish_state(size_t) --- +// The fast path: publish by index, no string lookup. + +static void SelectPublish_ByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast<size_t>(i % 4)); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByIndex); + +// --- Select::publish_state(const char *) --- +// The string path: requires index_of() lookup via strncmp. + +static void SelectPublish_ByString(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(options[i % 4]); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByString); + +// --- Select::publish_state() with callback --- +// Measures callback dispatch overhead on the index path. + +static void SelectPublish_WithCallback(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + uint64_t callback_count = 0; + select.add_on_state_callback([&callback_count](size_t) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast<size_t>(i % 4)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_WithCallback); + +// --- SelectCall::perform() set by index --- +// The fast call path — no string matching needed. + +static void SelectCall_SetByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_index(i % 4).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByIndex); + +// --- SelectCall::perform() set by option string --- +// Exercises the string lookup path through index_of(). + +static void SelectCall_SetByOption(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(options[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption); + +// --- SelectCall::perform() next with cycling --- +// Exercises the navigation path through active_index_. + +static void SelectCall_NextCycle(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().select_next(true).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_NextCycle); + +// --- SelectCall with 10 options (string lookup) --- +// Worst-case string matching with more options. + +static void SelectCall_SetByOption_10Options(benchmark::State &state) { + BenchSelect select; + setup_select( + select, "test_select", + {"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"}); + + // Pick options spread across the list to exercise different search depths + const char *picks[] = {"off", "custom3", "custom6", "move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(picks[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption_10Options); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/select/benchmark.yaml b/tests/benchmarks/components/select/benchmark.yaml new file mode 100644 index 00000000000..d336a348a03 --- /dev/null +++ b/tests/benchmarks/components/select/benchmark.yaml @@ -0,0 +1 @@ +select: diff --git a/tests/benchmarks/components/switch/__init__.py b/tests/benchmarks/components/switch/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/switch/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/switch/bench_switch.cpp b/tests/benchmarks/components/switch/bench_switch.cpp new file mode 100644 index 00000000000..d948f080adf --- /dev/null +++ b/tests/benchmarks/components/switch/bench_switch.cpp @@ -0,0 +1,137 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/switch/switch.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Switch for benchmarking — write_state() publishes directly. +class BenchSwitch : public switch_::Switch { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +// --- Switch::publish_state() alternating --- +// Forces state change every call, exercising the full publish path. + +static void SwitchPublish_Alternating(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Alternating); + +// --- Switch::publish_state() no change --- +// Tests the deduplication fast path in publish_dedup_. + +static void SwitchPublish_NoChange(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(true); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_NoChange); + +// --- Switch::publish_state() with callback --- +// Measures callback dispatch overhead on state changes. + +static void SwitchPublish_WithCallback(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + + uint64_t callback_count = 0; + sw.add_on_state_callback([&callback_count](bool) { callback_count++; }); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_WithCallback); + +// --- Switch::turn_on() / turn_off() --- +// The front-end call path: turn_on → write_state → publish_state. + +static void SwitchTurnOn(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.turn_on(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchTurnOn); + +// --- Switch::toggle() alternating --- +// Exercises the toggle path which reads current state to determine target. + +static void SwitchToggle(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.toggle(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchToggle); + +// --- Switch::publish_state() inverted --- +// Verifies the inversion path doesn't add significant overhead. + +static void SwitchPublish_Inverted(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.set_inverted(true); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Inverted); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/switch/benchmark.yaml b/tests/benchmarks/components/switch/benchmark.yaml new file mode 100644 index 00000000000..c637b3dc890 --- /dev/null +++ b/tests/benchmarks/components/switch/benchmark.yaml @@ -0,0 +1 @@ +switch: From f2a0d9943d5a207e7f3b099509e2f04df5d651e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:27:55 -1000 Subject: [PATCH 1194/2056] [benchmarks] Add host platform benchmarks for text_sensor and button (#15407) --- .../benchmarks/components/button/__init__.py | 5 + .../components/button/bench_button.cpp | 55 +++++++++ .../components/button/benchmark.yaml | 1 + .../components/text_sensor/__init__.py | 5 + .../text_sensor/bench_text_sensor.cpp | 108 ++++++++++++++++++ .../components/text_sensor/benchmark.yaml | 1 + 6 files changed, 175 insertions(+) create mode 100644 tests/benchmarks/components/button/__init__.py create mode 100644 tests/benchmarks/components/button/bench_button.cpp create mode 100644 tests/benchmarks/components/button/benchmark.yaml create mode 100644 tests/benchmarks/components/text_sensor/__init__.py create mode 100644 tests/benchmarks/components/text_sensor/bench_text_sensor.cpp create mode 100644 tests/benchmarks/components/text_sensor/benchmark.yaml diff --git a/tests/benchmarks/components/button/__init__.py b/tests/benchmarks/components/button/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/button/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/button/bench_button.cpp b/tests/benchmarks/components/button/bench_button.cpp new file mode 100644 index 00000000000..82f76961c92 --- /dev/null +++ b/tests/benchmarks/components/button/bench_button.cpp @@ -0,0 +1,55 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/button/button.h" + +namespace esphome::button::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Minimal Button for benchmarking — press_action() is a no-op. +class BenchButton : public Button { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void press_action() override {} +}; + +// --- Button::press() --- +// Measures: ESP_LOGD + press_action() + callback dispatch. + +static void ButtonPress(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(&button); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress); + +// --- Button::press() with callback --- +// Measures callback dispatch overhead. + +static void ButtonPress_WithCallback(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + uint64_t callback_count = 0; + button.add_on_press_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress_WithCallback); + +} // namespace esphome::button::benchmarks diff --git a/tests/benchmarks/components/button/benchmark.yaml b/tests/benchmarks/components/button/benchmark.yaml new file mode 100644 index 00000000000..75f089f7937 --- /dev/null +++ b/tests/benchmarks/components/button/benchmark.yaml @@ -0,0 +1 @@ +button: diff --git a/tests/benchmarks/components/text_sensor/__init__.py b/tests/benchmarks/components/text_sensor/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp new file mode 100644 index 00000000000..0ac88f79c1e --- /dev/null +++ b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp @@ -0,0 +1,108 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// --- publish_state(const char *) with short string, value changes each time --- +// Exercises: memcmp check (mismatch), string assign, callback dispatch. + +static void TextSensorPublish_Short_Changing(benchmark::State &state) { + TextSensor sensor; + + // Pre-populate with different short strings + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_Changing); + +// --- publish_state(const char *) with short string, same value (dedup path) --- +// Exercises: memcmp check (match), skips string assign. + +static void TextSensorPublish_Short_NoChange(benchmark::State &state) { + TextSensor sensor; + sensor.publish_state("192.168.1.100"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state("192.168.1.100"); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_NoChange); + +// --- publish_state with longer string (firmware version, MAC address) --- +// Exercises: memcmp on longer strings, string assign with potential realloc. + +static void TextSensorPublish_Long_Changing(benchmark::State &state) { + TextSensor sensor; + + const char *values[] = { + "2025.12.0-dev (Jan 15 2025, 10:30:00)", + "2025.12.1-dev (Feb 20 2025, 14:45:00)", + "2025.12.2-dev (Mar 10 2025, 08:15:00)", + "2025.12.3-dev (Apr 5 2025, 16:00:00)", + }; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Long_Changing); + +// --- publish_state with callback --- +// Measures callback dispatch overhead for text sensors. + +static void TextSensorPublish_WithCallback(benchmark::State &state) { + TextSensor sensor; + + uint64_t callback_count = 0; + sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; }); + + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithCallback); + +// --- publish_state(const char *, size_t) direct --- +// The lowest-level overload, avoids strlen. + +static void TextSensorPublish_WithLen(benchmark::State &state) { + TextSensor sensor; + + static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + static constexpr size_t lens[] = {11, 11, 11, 11}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4], lens[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithLen); + +} // namespace esphome::text_sensor::benchmarks diff --git a/tests/benchmarks/components/text_sensor/benchmark.yaml b/tests/benchmarks/components/text_sensor/benchmark.yaml new file mode 100644 index 00000000000..7bcb7de1c84 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/benchmark.yaml @@ -0,0 +1 @@ +text_sensor: From 38f4dc32170f1882bf8eb07de61cf2198d53278e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:28:07 -1000 Subject: [PATCH 1195/2056] [uptime] Pass known length to publish_state to avoid redundant strlen (#15410) --- esphome/components/uptime/text_sensor/uptime_text_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index c89d23672e9..7a56654804b 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -70,7 +70,7 @@ void UptimeTextSensor::update() { if (show_seconds) append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_); - this->publish_state(buf); + this->publish_state(buf, pos); } float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } From 95683b7416a4973733cdc5a695d82cafe55fc691 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:28:29 -1000 Subject: [PATCH 1196/2056] [light] Pass LightTraits to avoid redundant virtual get_traits() calls (#15403) --- esphome/components/light/light_call.cpp | 12 +++++------- esphome/components/light/light_call.h | 5 +++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 7c936b51b76..a749cd7305a 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -198,13 +198,13 @@ LightColorValues LightCall::validate_() { // Ensure there is always a color mode set if (!this->has_color_mode()) { - this->color_mode_ = this->compute_color_mode_(); + this->color_mode_ = this->compute_color_mode_(traits); this->set_flag_(FLAG_HAS_COLOR_MODE); } auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. - this->transform_parameters_(); + this->transform_parameters_(traits); // Business logic adjustments before validation // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. @@ -366,9 +366,7 @@ LightColorValues LightCall::validate_() { return v; } -void LightCall::transform_parameters_() { - auto traits = this->parent_->get_traits(); - +void LightCall::transform_parameters_(const LightTraits &traits) { // Allow CWWW modes to be set with a white value and/or color temperature. // This is used in three cases in HA: // - CW/WW lights, which set the "brightness" and "color_temperature" @@ -407,8 +405,8 @@ void LightCall::transform_parameters_() { } } } -ColorMode LightCall::compute_color_mode_() { - auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); +ColorMode LightCall::compute_color_mode_(const LightTraits &traits) { + auto supported_modes = traits.get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 0eb1785239c..88d29bd3490 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -10,6 +10,7 @@ struct LogString; namespace light { class LightState; +class LightTraits; /** This class represents a requested change in a light state. * @@ -188,11 +189,11 @@ class LightCall { LightColorValues validate_(); //// Compute the color mode that should be used for this call. - ColorMode compute_color_mode_(); + ColorMode compute_color_mode_(const LightTraits &traits); /// Get potential color modes bitmask for this light call. color_mode_bitmask_t get_suitable_color_modes_mask_(); /// Some color modes also can be set using non-native parameters, transform those calls. - void transform_parameters_(); + void transform_parameters_(const LightTraits &traits); // Bitfield flags - each flag indicates whether a corresponding value has been set. enum FieldFlags : uint16_t { From 4969fd6e99c40209cebe9d1d385aacee3d950abe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:28:41 -1000 Subject: [PATCH 1197/2056] [light] Use reciprocal multiply in normalize_color (#15401) --- esphome/components/light/light_color_values.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index a2c2dbca465..fa286a3941b 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -104,9 +104,10 @@ class LightColorValues { this->green_ = 1.0f; this->blue_ = 1.0f; } else { - this->red_ /= max_value; - this->green_ /= max_value; - this->blue_ /= max_value; + float inv = 1.0f / max_value; + this->red_ *= inv; + this->green_ *= inv; + this->blue_ *= inv; } } } From d90e2a6a9aa9241478366f110a5ab2eddaba2b65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 08:28:54 -1000 Subject: [PATCH 1198/2056] [core] Use __builtin_ctz for FiniteSetMask bit scanning (#15400) --- esphome/core/finite_set_mask.h | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index f9cd0377c73..616c69353d7 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -119,7 +119,7 @@ template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16 constexpr ValueType operator*() const { // Return value for the first set bit - return BitPolicy::from_bit(find_next_set_bit(mask_, 0)); + return BitPolicy::from_bit(find_lowest_set_bit(mask_)); } constexpr Iterator &operator++() { @@ -151,17 +151,32 @@ template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16 /// Get the first value from a raw bitmask /// Used for optimizing intersection logic (e.g., "pick first suitable mode") static constexpr ValueType first_value_from_mask(bitmask_t mask) { - return BitPolicy::from_bit(find_next_set_bit(mask, 0)); + return BitPolicy::from_bit(find_lowest_set_bit(mask)); } - /// Find the next set bit in a bitmask starting from a given position - /// Returns the bit position, or MAX_BITS if no more bits are set - static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) { - int bit = start_bit; + /// Find the lowest set bit in a bitmask + /// Returns the bit position, or MAX_BITS if no bits are set + static constexpr int find_lowest_set_bit(bitmask_t mask) { + if (mask == 0) { + return BitPolicy::MAX_BITS; + } +#if defined(__GNUC__) || defined(__clang__) + int bit; + if constexpr (sizeof(bitmask_t) <= sizeof(unsigned int)) { + bit = __builtin_ctz(static_cast<unsigned int>(mask)); + } else if constexpr (sizeof(bitmask_t) <= sizeof(uint32_t)) { + bit = __builtin_ctzl(static_cast<uint32_t>(mask)); + } else { + bit = __builtin_ctzll(static_cast<uint64_t>(mask)); + } + return bit < BitPolicy::MAX_BITS ? bit : BitPolicy::MAX_BITS; +#else + int bit = 0; while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast<bitmask_t>(1) << bit))) { ++bit; } return bit; +#endif } protected: From f8f65c1a7b8a96c7dca602294cd7a9aa919a6bfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:42:37 -1000 Subject: [PATCH 1199/2056] Bump click from 8.3.1 to 8.3.2 (#15421) 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 dd206000976..a8ec413b23b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 -click==8.3.1 +click==8.3.2 esphome-dashboard==20260210.0 aioesphomeapi==44.8.1 zeroconf==0.148.0 From c6bb1fe1415bcb1cbd9120265307a377bb1b0be3 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston <bonne@exciton.com.au> Date: Fri, 3 Apr 2026 13:24:02 -0700 Subject: [PATCH 1200/2056] [modbus] Add integration tests for server and server via controller (#14845) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- tests/integration/conftest.py | 7 + .../fixtures/uart_mock_modbus.yaml | 6 +- .../uart_mock_modbus_no_threshold.yaml | 7 +- .../fixtures/uart_mock_modbus_server.yaml | 124 +++++ .../uart_mock_modbus_server_controller.yaml | 180 +++++++ ...ock_modbus_server_controller_multiple.yaml | 118 +++++ ...t_mock_modbus_server_controller_write.yaml | 330 ++++++++++++ .../fixtures/uart_mock_modbus_timing.yaml | 4 +- tests/integration/state_utils.py | 106 ++++ tests/integration/test_uart_mock_modbus.py | 483 ++++++++++-------- 10 files changed, 1136 insertions(+), 229 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_server.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b652b4174cc..7c85bf753ca 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -198,6 +198,13 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s ' - "-g" # Add debug symbols', ) + # Replace external component path placeholder if present + if "EXTERNAL_COMPONENT_PATH" in content: + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path) + return content diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 3ff7ab01bdf..da36da4de18 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -22,6 +22,8 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: responses: @@ -46,7 +48,7 @@ modbus: modbus_controller: - address: 1 id: modbus_controller_ok - max_cmd_retries: 0 + max_cmd_retries: 2 update_interval: 1s - address: 2 id: modbus_controller_slow @@ -89,4 +91,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml index e3e8c8c8da7..9bc4dc50e92 100644 --- a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -22,6 +22,8 @@ uart: uart_mock: - id: virtual_uart_dev baud_rate: 9600 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: on_tx: @@ -40,7 +42,8 @@ uart_mock: 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency) delay: 40ms - data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + data: + !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; @@ -61,4 +64,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server.yaml b/tests/integration/fixtures/uart_mock_modbus_server.yaml new file mode 100644 index 00000000000..b657a6fd21b --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server.yaml @@ -0,0 +1,124 @@ +esphome: + name: uart-mock-modbus-server-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + auto_start: false + debug: + injections: + - delay: 100ms + inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read) + - delay: 100ms + # Read holding register 7 on device 2 + # Reply from device 2 + # Read holding register 5 on device 1 (read_after_peer_response) + inject_rx: + [ + 0x02, + 0x03, + 0x00, + 0x07, + 0x00, + 0x01, + 0x35, + 0xF8, + 0x02, + 0x03, + 0x02, + 0x00, + 0xF0, + 0xFC, + 0x00, + 0x01, + 0x03, + 0x00, + 0x05, + 0x00, + 0x01, + 0x94, + 0x0B, + ] + - delay: 100ms + inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response + - delay: 100ms + # Read holding register 7 on device 2, with no response + # Read holding register A on device 1 (read_after_peer_timeout) + inject_rx: + [ + 0x02, + 0x03, + 0x00, + 0x07, + 0x00, + 0x01, + 0x35, + 0xF8, + 0x01, + 0x03, + 0x00, + 0x0A, + 0x00, + 0x01, + 0xA4, + 0x08, + ] + +modbus: + uart_id: virtual_uart_dev + role: server + +modbus_controller: + - address: 1 + server_registers: + - address: 0x03 + value_type: U_WORD + read_lambda: |- + id(basic_read).publish_state(1); + return 1; + - address: 0x05 + value_type: U_WORD + read_lambda: |- + id(read_after_peer_response).publish_state(1); + return 1; + - address: 0x0A + value_type: U_WORD + read_lambda: |- + id(read_after_peer_timeout).publish_state(1); + return 1; + +sensor: + - platform: template + name: "basic_read" + id: basic_read + - platform: template + name: "read_after_peer_response" + id: read_after_peer_response + - platform: template + name: "read_after_peer_timeout" + id: read_after_peer_timeout + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml new file mode 100644 index 00000000000..f0f2c56a36d --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml @@ -0,0 +1,180 @@ +esphome: + name: uart-mock-modbus-server-contro + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + update_interval: 1s + id: modbus_controller_1 + + - address: 1 + modbus_id: virtual_modbus_server + id: modbus_server_1 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 99; + - address: 0x03 + value_type: S_WORD + read_lambda: return -99; + - address: 0x05 + value_type: U_DWORD + read_lambda: return 16909060; + - address: 0x08 + value_type: S_DWORD + read_lambda: return -16909060; + - address: 0x0B + value_type: U_DWORD_R + read_lambda: return 67305985; + - address: 0x0E + value_type: S_DWORD_R + read_lambda: return -67305985; + - address: 0x11 + value_type: U_QWORD + read_lambda: return 72623859790382856; + - address: 0x16 + value_type: S_QWORD + read_lambda: return -72623859790382856; + - address: 0x1B + value_type: U_QWORD_R + read_lambda: return 578437695752307201; + - address: 0x20 + value_type: S_QWORD_R + read_lambda: return -578437695752307201; + - address: 0x25 + value_type: FP32 + read_lambda: return 3.14; + - address: 0x28 + value_type: FP32_R + read_lambda: return 3.14; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml new file mode 100644 index 00000000000..7ec67b03db4 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml @@ -0,0 +1,118 @@ +esphome: + name: uart-mock-modbus-server-mult + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + - id: virtual_uart_server_2 + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_server_2 + id: virtual_modbus_server_2 + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_client + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_client + update_interval: 1s + id: modbus_controller_1 + - address: 2 + modbus_id: virtual_modbus_client + update_interval: 1s + id: modbus_controller_2 + + - address: 1 + modbus_id: virtual_modbus_server + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 919; + - address: 2 + modbus_id: virtual_modbus_server_2 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 929; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_2 + name: "reg_u_word_2" + address: 0x01 + register_type: holding + value_type: U_WORD + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_server_2).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml new file mode 100644 index 00000000000..3edcc73f07f --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml @@ -0,0 +1,330 @@ +esphome: + name: uart-mock-modbus-srv-write + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +globals: + - id: stored_u_word + type: uint16_t + initial_value: "11" + - id: stored_s_word + type: int16_t + initial_value: "-11" + - id: stored_u_dword + type: uint32_t + initial_value: "1001" + - id: stored_s_dword + type: int32_t + initial_value: "-1001" + - id: stored_u_dword_r + type: uint32_t + initial_value: "3003" + - id: stored_s_dword_r + type: int32_t + initial_value: "-3003" + - id: stored_u_qword + type: uint64_t + initial_value: "5005" + - id: stored_s_qword + type: int64_t + initial_value: "-5005" + - id: stored_u_qword_r + type: uint64_t + initial_value: "7007" + - id: stored_s_qword_r + type: int64_t + initial_value: "-7007" + - id: stored_fp32 + type: float + initial_value: "1.5" + - id: stored_fp32_r + type: float + initial_value: "2.5" + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + update_interval: 2s + id: modbus_controller_1 + + - address: 1 + modbus_id: virtual_modbus_server + id: modbus_server_1 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return id(stored_u_word); + write_lambda: id(stored_u_word) = x; return true; + - address: 0x03 + value_type: S_WORD + read_lambda: return id(stored_s_word); + write_lambda: id(stored_s_word) = x; return true; + - address: 0x05 + value_type: U_DWORD + read_lambda: return id(stored_u_dword); + write_lambda: id(stored_u_dword) = x; return true; + - address: 0x08 + value_type: S_DWORD + read_lambda: return id(stored_s_dword); + write_lambda: id(stored_s_dword) = x; return true; + - address: 0x0B + value_type: U_DWORD_R + read_lambda: return id(stored_u_dword_r); + write_lambda: id(stored_u_dword_r) = x; return true; + - address: 0x0E + value_type: S_DWORD_R + read_lambda: return id(stored_s_dword_r); + write_lambda: id(stored_s_dword_r) = x; return true; + - address: 0x11 + value_type: U_QWORD + read_lambda: return id(stored_u_qword); + write_lambda: id(stored_u_qword) = x; return true; + - address: 0x16 + value_type: S_QWORD + read_lambda: return id(stored_s_qword); + write_lambda: id(stored_s_qword) = x; return true; + - address: 0x1B + value_type: U_QWORD_R + read_lambda: return id(stored_u_qword_r); + write_lambda: id(stored_u_qword_r) = x; return true; + - address: 0x20 + value_type: S_QWORD_R + read_lambda: return id(stored_s_qword_r); + write_lambda: id(stored_s_qword_r) = x; return true; + - address: 0x25 + value_type: FP32 + read_lambda: return id(stored_fp32); + write_lambda: id(stored_fp32) = x; return true; + - address: 0x28 + value_type: FP32_R + read_lambda: return id(stored_fp32_r); + write_lambda: id(stored_fp32_r) = x; return true; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + +number: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + min_value: 0 + max_value: 65535 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + min_value: -16777215 + max_value: 16777215 + step: 0.01 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + min_value: -16777215 + max_value: 16777215 + step: 0.01 + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index f4cf0bde37e..c670864085b 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -22,6 +22,8 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: on_tx: @@ -61,4 +63,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index 5792a8e8046..d42b50ecdb4 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -346,3 +346,109 @@ class SensorStateCollector: else: self._waiters.append((condition, future)) return future + + +class SensorTracker: + """Data-driven sensor state tracker with expected-value futures. + + Tracks sensor state updates and resolves futures when sensors report + specific expected values. Eliminates per-sensor future boilerplate. + + Usage:: + + tracker = SensorTracker(["reg_u_word", "reg_s_word"]) + futures = tracker.expect_all({"reg_u_word": 99, "reg_s_word": -99}) + # ... subscribe_states with tracker.on_state, start scenario ... + await tracker.await_all(futures) + """ + + def __init__(self, sensor_names: list[str]) -> None: + self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} + self.key_to_sensor: dict[int, str] = {} + self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {} + + _ANY = object() # Sentinel: match any value + + def expect(self, name: str, value: object) -> asyncio.Future: + """Register an expected value for *name* and return a future for it.""" + future: asyncio.Future = asyncio.get_running_loop().create_future() + self._expectations.setdefault(name, []).append((value, future)) + return future + + def expect_any(self, name: str) -> asyncio.Future: + """Register a future that resolves on *any* state update for *name*.""" + return self.expect(name, self._ANY) + + def expect_all(self, expected: dict[str, object]) -> dict[str, asyncio.Future]: + """Call ``expect`` for every entry and return a dict of futures.""" + return {name: self.expect(name, value) for name, value in expected.items()} + + def on_state(self, state: EntityState) -> None: + """State callback suitable for ``subscribe_states``.""" + if not isinstance(state, SensorState) or state.missing_state: + return + sensor_name = self.key_to_sensor.get(state.key) + if not sensor_name or sensor_name not in self.sensor_states: + return + self.sensor_states[sensor_name].append(state.state) + for expected_value, future in self._expectations.get(sensor_name, []): + if not future.done() and ( + expected_value is self._ANY or state.state == expected_value + ): + future.set_result(True) + break + + async def await_change( + self, future: asyncio.Future, name: str, timeout: float = 2.0 + ) -> None: + """Wait for a sensor future to resolve; fail the test on timeout.""" + try: + await asyncio.wait_for(future, timeout=timeout) + except TimeoutError: + import pytest + + pytest.fail( + f"Timeout waiting for {name} change. Received sensor states:\n" + f" {name}: {self.sensor_states[name]}\n" + ) + + async def await_must_not_change( + self, future: asyncio.Future, name: str, timeout: float = 2.0 + ) -> None: + """Assert a sensor future does NOT resolve within the timeout.""" + try: + await asyncio.wait_for(future, timeout=timeout) + except TimeoutError: + return # Expected + import pytest + + pytest.fail( + f"{name} change should not have been triggered, but was. " + f"Received sensor states:\n {name}: {self.sensor_states[name]}\n" + ) + + async def await_all( + self, futures: dict[str, asyncio.Future], timeout: float = 2.0 + ) -> None: + """Await every future in *futures*, failing with per-sensor diagnostics.""" + for name, future in futures.items(): + await self.await_change(future, name, timeout=timeout) + + async def setup_and_start_scenario(self, client) -> list: + """Wire up subscriptions, wait for initial states, press Start Scenario.""" + entities, _ = await client.list_entities_services() + self.key_to_sensor.update( + build_key_to_entity_mapping(entities, list(self.sensor_states.keys())) + ) + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state)) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + import pytest + + pytest.fail("Timeout waiting for initial states") + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + return entities diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index e341d86f53f..e8dfa1b8226 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -14,15 +14,67 @@ test_uart_mock_modbus_no_threshold : from __future__ import annotations import asyncio -from pathlib import Path +from collections.abc import Callable +from dataclasses import dataclass -from aioesphomeapi import ButtonInfo, EntityState, SensorState +from aioesphomeapi import NumberInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import SensorTracker, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction +@dataclass +class RegisterTestCase: + """Test parameters for a single modbus register write/read round-trip.""" + + initial_value: object + write_number_name: str + write_value: float + post_write_value: object + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_modbus_line_callback() -> tuple[Callable[[str], None], list[str], list[str]]: + """Return a (callback, error_lines, warning_lines) tuple for tracking modbus log output. + + Only captures bus-level modbus messages ([modbus:]), not modbus_controller + scheduling noise (e.g. "Duplicate modbus command found"). + """ + error_log_lines: list[str] = [] + warning_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "[E][modbus:" in line: + error_log_lines.append(line) + if "[W][modbus:" in line: + warning_log_lines.append(line) + + return line_callback, error_log_lines, warning_log_lines + + +def _assert_no_modbus_errors( + error_log_lines: list[str], warning_log_lines: list[str] +) -> None: + assert len(error_log_lines) == 0, ( + "Expect no errors logged by the modbus mock, but got:\n" + + "\n".join(error_log_lines) + ) + assert len(warning_log_lines) == 0, ( + "Expect no warnings logged by the modbus mock, but got:\n" + + "\n".join(warning_log_lines) + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + @pytest.mark.asyncio async def test_uart_mock_modbus( yaml_config: str, @@ -30,127 +82,41 @@ async def test_uart_mock_modbus( api_client_connected: APIClientConnectedFactory, ) -> None: """Test basic modbus data parsing.""" - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" + + tracker = SensorTracker( + [ + "basic_register", + "delayed_response", + "late_response", + "no_response", + "exception_response", + ] ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "basic_register": [], - "delayed_response": [], - "late_response": [], - "no_response": [], - "exception_response": [], - } - - basic_register_changed = loop.create_future() - delayed_response_changed = loop.create_future() - late_response_changed = loop.create_future() - no_response_changed = loop.create_future() - exception_response_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "basic_register" - and state.state == 259.0 - and not basic_register_changed.done() - ): - basic_register_changed.set_result(True) - elif ( - sensor_name == "delayed_response" - and state.state == 255.0 - and not delayed_response_changed.done() - ): - delayed_response_changed.set_result(True) - elif ( - sensor_name == "late_response" and not late_response_changed.done() - ): - late_response_changed.set_result(True) - elif sensor_name == "no_response" and not no_response_changed.done(): - no_response_changed.set_result(True) - elif ( - sensor_name == "exception_response" - and not exception_response_changed.done() - ): - exception_response_changed.set_result(True) + basic_register_changed = tracker.expect("basic_register", 259.0) + delayed_response_changed = tracker.expect("delayed_response", 255.0) + # late_response / no_response / exception_response: expect *any* value + # (these should never fire, so we use a permissive match via expect_any) + late_response_changed = tracker.expect_any("late_response") + no_response_changed = tracker.expect_any("no_response") + exception_response_changed = tracker.expect_any("exception_response") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() + await tracker.setup_and_start_scenario(client) - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) - - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") - - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) - - try: - await asyncio.wait_for(delayed_response_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for delayed_response change. Received sensor states:\n" - f" delayed_response: {sensor_states['delayed_response']}\n" - ) - - try: - await asyncio.wait_for(late_response_changed, timeout=2.0) - pytest.fail( - f"late_response change should not have been triggered, but was. Received sensor states:\n" - f" late_response: {sensor_states['late_response']}\n" - ) - except TimeoutError: - pass # Expected timeout since we never inject a response for late_response - - try: - await asyncio.wait_for(no_response_changed, timeout=2.0) - pytest.fail( - f"no_response change should not have been triggered, but was. Received sensor states:\n" - f" no_response: {sensor_states['no_response']}\n" - ) - except TimeoutError: - pass # Expected timeout since we never inject a response for no_response - - # Wait for basic register to be updated with successful parse - try: - await asyncio.wait_for(basic_register_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for Basic Register change. Received sensor states:\n" - f" basic_register: {sensor_states['basic_register']}\n" - ) - - try: - await asyncio.wait_for(exception_response_changed, timeout=2.0) - pytest.fail( - f"exception_response change should not have been triggered, but was. Received sensor states:\n" - f" exception_response: {sensor_states['exception_response']}\n" - ) - except TimeoutError: - pass + await tracker.await_change(delayed_response_changed, "delayed_response") + await tracker.await_change(basic_register_changed, "basic_register") + # Run all "must not change" checks concurrently — each waits the full + # timeout, so sequential execution would multiply the wall time. + await asyncio.gather( + tracker.await_must_not_change(late_response_changed, "late_response"), + tracker.await_must_not_change(no_response_changed, "no_response"), + tracker.await_must_not_change( + exception_response_changed, "exception_response" + ), + ) @pytest.mark.asyncio @@ -159,69 +125,17 @@ async def test_uart_mock_modbus_timing( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test basic modbus data parsing.""" - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) + """Test modbus timing with multi-register SDM meter response.""" - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "sdm_voltage": [], - } - - voltage_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is a good voltage reading (243V) - if ( - sensor_name == "sdm_voltage" - and state.state > 200.0 - and not voltage_changed.done() - ): - voltage_changed.set_result(True) + tracker = SensorTracker(["sdm_voltage"]) + voltage_changed = tracker.expect_any("sdm_voltage") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) - - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") - - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) - - # Wait for voltage to be updated with successful parse - try: - await asyncio.wait_for(voltage_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for SDM voltage change. Received sensor states:\n" - f" sdm_voltage: {sensor_states['sdm_voltage']}\n" - ) + await tracker.setup_and_start_scenario(client) + await tracker.await_change(voltage_changed, "sdm_voltage") @pytest.mark.asyncio @@ -234,66 +148,187 @@ async def test_uart_mock_modbus_no_threshold( Without the 50ms fallback timeout, the chunked response with a 40ms gap between USB packets would cause a false timeout and CRC failure cascade. + Bus-level warnings (CRC failures, buffer clears) are expected during + chunked reassembly — the test only verifies the final value arrives. """ - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "sdm_voltage": [], - } - - voltage_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is a good voltage reading (243V) - if ( - sensor_name == "sdm_voltage" - and state.state > 200.0 - and not voltage_changed.done() - ): - voltage_changed.set_result(True) + tracker = SensorTracker(["sdm_voltage"]) + voltage_changed = tracker.expect_any("sdm_voltage") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() + await tracker.setup_and_start_scenario(client) + await tracker.await_change(voltage_changed, "sdm_voltage") - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.", + strict=True, +) +async def test_uart_mock_modbus_server( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test modbus server parsing with peer traffic on a shared bus.""" - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) + tracker = SensorTracker( + ["basic_read", "read_after_peer_response", "read_after_peer_timeout"] + ) + futures = tracker.expect_all( + { + "basic_read": 1, + "read_after_peer_response": 1, + "read_after_peer_timeout": 1, + } + ) - # Wait for voltage to be updated with successful parse - try: - await asyncio.wait_for(voltage_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for SDM voltage change. Received sensor states:\n" - f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_server_controller( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller functionality for all read register types.""" + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + expected_values = { + "reg_u_word": 99, + "reg_s_word": -99, + "reg_u_dword": 16909060, + "reg_s_dword": -16909060, + "reg_u_dword_r": pytest.approx(67305985), + "reg_s_dword_r": pytest.approx(-67305985), + "reg_u_qword": pytest.approx(72623859790382856), + "reg_s_qword": pytest.approx(-72623859790382856), + "reg_u_qword_r": pytest.approx(578437695752307201), + "reg_s_qword_r": pytest.approx(-578437695752307201), + "reg_fp32": pytest.approx(3.14), + "reg_fp32_r": pytest.approx(3.14), + } + tracker = SensorTracker(list(expected_values.keys())) + futures = tracker.expect_all(expected_values) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_server_controller_write( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller write functionality for all register value types. + + Verifies that writing to modbus server registers via the controller updates + the server's stored values, which are then read back correctly on the next poll. + All 12 value types are tested: U/S_WORD, U/S_DWORD(_R), U/S_QWORD(_R), FP32(_R). + """ + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + register_test_cases: dict[str, RegisterTestCase] = { + "reg_u_word": RegisterTestCase(11, "write_u_word", 42, 42), + "reg_s_word": RegisterTestCase(-11, "write_s_word", -42, -42), + "reg_u_dword": RegisterTestCase(1001, "write_u_dword", 2002, 2002), + "reg_s_dword": RegisterTestCase(-1001, "write_s_dword", -2002, -2002), + "reg_u_dword_r": RegisterTestCase(3003, "write_u_dword_r", 4004, 4004), + "reg_s_dword_r": RegisterTestCase(-3003, "write_s_dword_r", -4004, -4004), + "reg_u_qword": RegisterTestCase(5005, "write_u_qword", 6006, 6006), + "reg_s_qword": RegisterTestCase(-5005, "write_s_qword", -6006, -6006), + "reg_u_qword_r": RegisterTestCase(7007, "write_u_qword_r", 8008, 8008), + "reg_s_qword_r": RegisterTestCase(-7007, "write_s_qword_r", -8008, -8008), + "reg_fp32": RegisterTestCase( + pytest.approx(1.5, abs=0.01), + "write_fp32", + 3.14, + pytest.approx(3.14, abs=0.01), + ), + "reg_fp32_r": RegisterTestCase( + pytest.approx(2.5, abs=0.01), + "write_fp32_r", + 6.28, + pytest.approx(6.28, abs=0.01), + ), + } + + tracker = SensorTracker(list(register_test_cases.keys())) + + # Phase 1: expect initial baseline values + initial_futures = tracker.expect_all( + {name: case.initial_value for name, case in register_test_cases.items()} + ) + # Phase 2: expect post-write values (registered now so on_state can match them) + written_futures = tracker.expect_all( + {name: case.post_write_value for name, case in register_test_cases.items()} + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities = await tracker.setup_and_start_scenario(client) + + # Wait for initial baseline values to confirm the controller <-> server + # connection is working before issuing writes + await tracker.await_all(initial_futures, timeout=4.0) + + # Issue write commands for all register types + for case in register_test_cases.values(): + entity = find_entity(entities, case.write_number_name, NumberInfo) + assert entity is not None, ( + f"{case.write_number_name} number entity not found" ) + client.number_command(entity.key, case.write_value) + + # Wait for sensors to reflect the written values (round-trip write+read) + await tracker.await_all(written_futures, timeout=4.0) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.", + strict=True, +) +async def test_uart_mock_modbus_server_controller_multiple( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller functionality with multiple servers.""" + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + expected_values = {"reg_u_word": 919, "reg_u_word_2": 929} + tracker = SensorTracker(list(expected_values.keys())) + futures = tracker.expect_all(expected_values) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) From 533eeabf1d349bfb5912d6bcae012c5db8ff46c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:17:49 +0000 Subject: [PATCH 1201/2056] Bump aioesphomeapi from 44.8.1 to 44.9.0 (#15425) 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 a8ec413b23b..9c63cdee27b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260210.0 -aioesphomeapi==44.8.1 +aioesphomeapi==44.9.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 7ab26a4fe0b5ef4770e76e65a39409be02557375 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:21:58 +1000 Subject: [PATCH 1202/2056] [ili9xxx][st7735] Add deprecation warnings (#15416) --- esphome/components/ili9xxx/__init__.py | 4 ++++ esphome/components/ili9xxx/display.py | 3 +++ esphome/components/st7735/__init__.py | 5 +++++ esphome/components/st7735/display.py | 6 ++++++ 4 files changed, 18 insertions(+) diff --git a/esphome/components/ili9xxx/__init__.py b/esphome/components/ili9xxx/__init__.py index e69de29bb2d..84888bbabcb 100644 --- a/esphome/components/ili9xxx/__init__.py +++ b/esphome/components/ili9xxx/__init__.py @@ -0,0 +1,4 @@ +DEPRECATED_COMPONENT = """ +The 'ili9xxx' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 185a74fa41d..1f20b21a0e8 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate async def to_code(config): + LOGGER.warning( + "The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) rhs = MODELS[config[CONF_MODEL]].new() var = cg.Pvariable(config[CONF_ID], rhs) diff --git a/esphome/components/st7735/__init__.py b/esphome/components/st7735/__init__.py index ba854bb0ae0..62dfa6f4136 100644 --- a/esphome/components/st7735/__init__.py +++ b/esphome/components/st7735/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg st7735_ns = cg.esphome_ns.namespace("st7735") + +DEPRECATED_COMPONENT = """ +The 'st7735' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 9dc69f27ff1..766370c21f2 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import display, spi @@ -15,6 +17,7 @@ from esphome.const import ( from . import st7735_ns CODEOWNERS = ["@SenexCrenshaw"] +LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["spi"] @@ -87,6 +90,9 @@ async def setup_st7735(var, config): async def to_code(config): + LOGGER.warning( + "The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) var = cg.new_Pvariable( config[CONF_ID], config[CONF_MODEL], From 4f2290d5480afc7e97494ececb7334060e247030 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 16:37:20 -1000 Subject: [PATCH 1203/2056] [web_server] Disable loop when no SSE clients are connected (#15428) --- esphome/components/web_server/web_server.cpp | 17 +++++++++++++++-- esphome/components/web_server/web_server.h | 3 ++- .../web_server_idf/web_server_idf.cpp | 6 +++++- .../components/web_server_idf/web_server_idf.h | 3 ++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1dda6204fee..a57a8d26ffd 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char this->send(message, event, id, reconnect); } -void DeferredUpdateEventSourceList::loop() { +bool DeferredUpdateEventSourceList::loop() { for (DeferredUpdateEventSource *dues : *this) { dues->loop(); } + return !this->empty(); } void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, @@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); + ws->enable_loop_soon_any_context(); } void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { @@ -413,13 +415,24 @@ void WebServer::setup() { // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events this->set_interval(10000, [this]() { + if (this->events_.empty()) + return; char buf[32]; auto uptime = static_cast<uint32_t>(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); } -void WebServer::loop() { this->events_.loop(); } +void WebServer::loop() { + // No SSE clients connected; stop looping until a new client connects via + // enable_loop_soon_any_context(). This is safe because: + // - set_interval/set_timeout/defer run via the Scheduler, independent of loop() + // - deferrable_send_state early-outs when no clients are connected + // - try_send_nodefer (log, ping) iterates sessions which are empty + // - REST API handlers use defer() which runs via the Scheduler + if (!this->events_.loop()) + this->disable_loop(); +} #ifdef USE_LOGGER void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6152dfbfd32..8e8b1de8c4b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent void on_client_disconnect_(DeferredUpdateEventSource *source); public: - void loop(); + /// Returns true if there are event sources remaining (including pending cleanup). + bool loop(); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 38ccfccb76d..8f464ae9121 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -484,9 +484,12 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { this->on_connect_(rsp); } this->sessions_.push_back(rsp); + // Wake up WebServer::loop() to drain deferred event queues for this client. + // Safe from httpd task context via the pending_enable_loop_ flag. + this->web_server_->enable_loop_soon_any_context(); } -void AsyncEventSource::loop() { +bool AsyncEventSource::loop() { // Clean up dead sessions safely // This follows the ESP-IDF pattern where free_ctx marks resources as dead // and the main loop handles the actual cleanup to avoid race conditions @@ -504,6 +507,7 @@ void AsyncEventSource::loop() { ++i; } } + return !this->sessions_.empty(); } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 81683e8d852..f2931fb5079 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler { void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); - void loop(); + /// Returns true if there are sessions remaining (including pending cleanup). + bool loop(); bool empty() { return this->count() == 0; } size_t count() const { return this->sessions_.size(); } From 2337767c38e09c7e98e0289f2f3cb93a0234c3f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 3 Apr 2026 16:37:31 -1000 Subject: [PATCH 1204/2056] [modbus_controller] Fix format specifier warnings (#15429) --- .../components/modbus_controller/modbus_controller.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 3c4ceaf62de..5c3b39c9548 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() { while (ix != this->sensorset_.end()) { SensorItem *curr = *ix; - ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, + ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr); if (r.register_count == 0) { @@ -484,18 +484,18 @@ void ModbusController::dump_config() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : this->sensorset_) { - ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d", + ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu", static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count, it->get_register_size()); } ESP_LOGCONFIG(TAG, "ranges"); for (auto &it : this->register_ranges_) { - ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type), + ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type), it.start_address, it.register_count, it.skip_updates); } ESP_LOGCONFIG(TAG, "server registers"); for (auto &r : this->server_registers_) { - ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address, + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address, static_cast<uint8_t>(r->value_type), r->register_count); } #endif @@ -524,7 +524,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty void ModbusController::dump_sensors_() { ESP_LOGV(TAG, "sensors"); for (auto &it : this->sensorset_) { - ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count, + ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count, it->get_register_size(), it->offset); } } From 16ae753317b68022ce2763c72622280df8160a8d Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Sat, 4 Apr 2026 07:44:04 +0200 Subject: [PATCH 1205/2056] [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 2) (#15358) --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 188 ++++++++++++++++++ .../mitsubishi_cn105/mitsubishi_cn105.h | 26 +++ .../mitsubishi_cn105_climate.cpp | 5 +- .../mitsubishi_cn105_time.cpp | 7 + .../climate/mitsubishi_cn105_tests.cpp | 173 ++++++++++++++++ tests/components/mitsubishi_cn105/common.cpp | 7 + tests/components/mitsubishi_cn105/common.h | 53 +++++ 7 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp create mode 100644 tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp create mode 100644 tests/components/mitsubishi_cn105/common.cpp create mode 100644 tests/components/mitsubishi_cn105/common.h diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 35ab405b2d2..e3923bb0b81 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -1,7 +1,195 @@ +#include <array> +#include <numeric> #include "mitsubishi_cn105.h" namespace esphome::mitsubishi_cn105 { static const char *const TAG = "mitsubishi_cn105.driver"; +static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; + +static constexpr size_t HEADER_LEN = 5; +static constexpr uint8_t PREAMBLE = 0xFC; +static constexpr uint8_t HEADER_BYTE_1 = 0x01; +static constexpr uint8_t HEADER_BYTE_2 = 0x30; + +static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A; +static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A; +static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}}; + +static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { + return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); +} + +template<std::size_t PayloadSize> +static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) { + const size_t full_len = PayloadSize + HEADER_LEN + 1; + std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)}; + std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN); + packet.back() = checksum(packet.data(), packet.size() - 1); + return packet; +} + +static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD); + +void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } + +void MitsubishiCN105::update() { + if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { + this->write_timeout_start_ms_.reset(); + this->read_pos_ = 0; + this->set_state_(State::READ_TIMEOUT); + return; + } + + this->read_incoming_bytes_(); +} + +void MitsubishiCN105::set_state_(State new_state) { + if (should_transition(this->state_, new_state)) { + ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)), + LOG_STR_ARG(state_to_string(new_state))); + this->state_ = new_state; + this->did_transition_(new_state); + } else { + ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)), + LOG_STR_ARG(state_to_string(new_state))); + } +} + +bool MitsubishiCN105::should_transition(State from, State to) { + switch (to) { + case State::CONNECTING: + return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT; + + case State::CONNECTED: + case State::READ_TIMEOUT: + return from == State::CONNECTING; + + default: + return false; + } +} + +void MitsubishiCN105::did_transition_(State to) { + switch (to) { + case State::CONNECTING: + this->send_packet_(CONNECT_PACKET); + break; + + case State::CONNECTED: + this->write_timeout_start_ms_.reset(); + // TODO: read AC status after connected, next PR + break; + + case State::READ_TIMEOUT: + this->set_state_(State::CONNECTING); + break; + + default: + break; + } +} + +void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { + dump_buffer_vv("TX", packet, len); + this->device_.write_array(packet, len); + this->write_timeout_start_ms_ = get_loop_time_ms(); +} + +void MitsubishiCN105::read_incoming_bytes_() { + uint8_t watchdog = 64; + while (this->device_.available() > 0 && watchdog-- > 0) { + uint8_t &value = this->read_buffer_[this->read_pos_]; + if (!this->device_.read_byte(&value)) { + ESP_LOGW(TAG, "UART read failed while data available"); + return; + } + + switch (++this->read_pos_) { + case 1: + if (value != PREAMBLE) { + this->reset_read_position_and_dump_buffer_("RX ignoring preamble"); + } + continue; + + case 2: + continue; + + case 3: + if (value != HEADER_BYTE_1) { + this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch"); + } + continue; + + case 4: + if (value != HEADER_BYTE_2) { + this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch"); + } + continue; + + case HEADER_LEN: + static_assert(READ_BUFFER_SIZE > HEADER_LEN); + if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) { + this->reset_read_position_and_dump_buffer_("RX invalid: payload too large"); + } + continue; + + default: + break; + } + + const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]); + if (this->read_pos_ <= len_without_checksum) { + continue; + } + + if (checksum(this->read_buffer_, len_without_checksum) != value) { + this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch"); + continue; + } + + this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN); + this->reset_read_position_and_dump_buffer_("RX"); + } +} + +void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { + switch (type) { + case PACKET_TYPE_CONNECT_RESPONSE: + this->set_state_(State::CONNECTED); + break; + + default: + ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type); + break; + } +} + +void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) { + dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); + this->read_pos_ = 0; +} + +void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char buf[format_hex_pretty_size(READ_BUFFER_SIZE)]; + ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len)); +#endif +} + +const LogString *MitsubishiCN105::state_to_string(State state) { + switch (state) { + case State::NOT_CONNECTED: + return LOG_STR("Not connected"); + case State::CONNECTING: + return LOG_STR("Connecting"); + case State::CONNECTED: + return LOG_STR("Connected"); + case State::READ_TIMEOUT: + return LOG_STR("ReadTimeout"); + } + return LOG_STR("Unknown"); +} + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 6018dddbffa..fc09b3bed24 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -1,19 +1,45 @@ #pragma once +#include <optional> #include "esphome/components/uart/uart.h" namespace esphome::mitsubishi_cn105 { +uint32_t get_loop_time_ms(); + class MitsubishiCN105 { public: explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} + void initialize(); + void update(); + uint32_t get_update_interval() const { return this->update_interval_ms_; } void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } protected: + enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT }; + + void set_state_(State new_state); + void did_transition_(State to); + void read_incoming_bytes_(); + void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + void reset_read_position_and_dump_buffer_(const char *prefix); + void send_packet_(const uint8_t *packet, size_t len); + template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } + static bool should_transition(State from, State to); + static const LogString *state_to_string(State state); + static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len); + uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; + std::optional<uint32_t> write_timeout_start_ms_; + State state_{State::NOT_CONNECTED}; + + private: + static constexpr size_t READ_BUFFER_SIZE = 32; + uint8_t read_buffer_[READ_BUFFER_SIZE]; + uint8_t read_pos_{0}; }; } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 6d50296c8fd..cce6bef5e46 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -1,3 +1,4 @@ +#include <cinttypes> #include "mitsubishi_cn105_climate.h" #include "esphome/core/log.h" @@ -14,9 +15,9 @@ void MitsubishiCN105Climate::dump_config() { LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits()); } -void MitsubishiCN105Climate::setup() {} +void MitsubishiCN105Climate::setup() { this->hp_.initialize(); } -void MitsubishiCN105Climate::loop() {} +void MitsubishiCN105Climate::loop() { this->hp_.update(); } climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp new file mode 100644 index 00000000000..55a0a2328fe --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp @@ -0,0 +1,7 @@ +#include "esphome/core/application.h" + +namespace esphome::mitsubishi_cn105 { + +uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); }; + +} // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp new file mode 100644 index 00000000000..e01d9e69ff1 --- /dev/null +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -0,0 +1,173 @@ +#include "../common.h" + +namespace esphome::mitsubishi_cn105::testing { + +struct TestContext { + MockUARTComponent uart; + uart::UARTDevice device{&uart}; + TestableMitsubishiCN105 sut{device}; + + TestContext() { this->sut.set_current_time(0); } +}; + +TEST(MitsubishiCN105Tests, InitSendsConnectPacket) { + auto ctx = TestContext{}; + + ctx.sut.set_current_time(123); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + + ctx.sut.initialize(); + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123}); +} + +TEST(MitsubishiCN105Tests, SuccessfullyConnects) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.sut.write_timeout_start_ms_.has_value()); + + // Connect response + ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55}); + + ctx.sut.update(); + + // All bytes from UART should be consumed and state = CONNECTED + EXPECT_TRUE(ctx.uart.rx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTED); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + + // Nothing should be send to UART + EXPECT_TRUE(ctx.uart.tx.empty()); +} + +TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // No response (no RX data), no retry yet + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); + + // Still no response after 1999ms, no retry yet + ctx.sut.set_current_time(1999); + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); + + // Stop waiting after 2s and retry connect + ctx.sut.set_current_time(2000); + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000}); +} + +TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // RX noise/unexpected traffic + ctx.uart.push_rx({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46}); + + // Make sure we have enough bytes in buffer. + ASSERT_GT(ctx.uart.rx.size(), 64); + + // No valid response, no state change expected + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + + // Watchdog interrupts reading (max. 64 bytes at once) so we do not spend the whole loop draining UART + EXPECT_FALSE(ctx.uart.rx.empty()); + + // Next update will read remaining bytes, no state change expected + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_TRUE(ctx.uart.rx.empty()); +} + +TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // Mixed RX stream with partial, malformed, and oversized frames to test parser robustness + ctx.uart.push_rx({// ───────────────────────────── + // Noise (no 0xFC) -> should be ignored via preamble reset + // ──────────────────────────── + 0x01, 0x02, 0x03, 0x04, 0x05, + + // ───────────────────────────── + // Partial frame (declares payload len=5, but we cut it short) + // Later bytes will eventually force checksum mismatch and reset + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x05, 0xAA, 0xBB, + + // ───────────────────────────── + // Invalid header (header byte 3 should be 0x01, header byte 4 should be 0x30) + // Should reset quickly on header mismatch + // ───────────────────────────── + 0xFC, 0x62, 0xFF, 0xFF, 0x02, 0x01, 0x02, 0x00, + + // ───────────────────────────── + // Oversized length field (rejected by payload-too-large check at HEADER_LEN) + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0xFE, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, + 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + + // ───────────────────────────── + // Valid unknown-type frame (type=0x62), should be parsed successfully then ignored + // Frame: FC 62 01 30 02 AA BB 30 + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x02, 0xAA, 0xBB, 0x30, + + // ───────────────────────────── + // Invalid checksum (should be rejected at checksum check) + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x02, 0x10, 0x20, 0xFF, + + // ───────────────────────────── + // Back-to-back VALID frames (unknown type=0x62) to stress boundary handling. + // Frame A: FC 62 01 30 01 02 6C + // Frame B: FC 62 01 30 01 03 6B + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x01, 0x02, 0x6C, 0xFC, 0x62, 0x01, 0x30, 0x01, 0x03, 0x6B, + + // ───────────────────────────── + // Trailing noise + // ───────────────────────────── + 0x55, 0x66, 0x77, 0x88}); + + // Drain RX - no valid response, no state change expected + int iterations = 0; + while (!ctx.uart.rx.empty() && iterations++ < 10) { + ctx.sut.update(); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + } + + EXPECT_TRUE(ctx.uart.rx.empty()); +} + +} // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.cpp b/tests/components/mitsubishi_cn105/common.cpp new file mode 100644 index 00000000000..ea13d7676c4 --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.cpp @@ -0,0 +1,7 @@ +#include "common.h" + +namespace esphome::mitsubishi_cn105 { + +uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; }; + +} // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h new file mode 100644 index 00000000000..c41268d7233 --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.h @@ -0,0 +1,53 @@ +#pragma once + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <algorithm> +#include <cstdint> +#include <initializer_list> +#include <vector> +#include "esphome/components/uart/uart_component.h" +#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105::testing { + +class MockUARTComponent : public uart::UARTComponent { + public: + std::vector<uint8_t> tx; + std::vector<uint8_t> rx; + + void push_rx(std::initializer_list<uint8_t> data) { this->rx.insert(this->rx.end(), data.begin(), data.end()); } + + // UARTComponent + void write_array(const uint8_t *data, size_t len) override { this->tx.insert(this->tx.end(), data, data + len); } + + bool read_array(uint8_t *data, size_t len) override { + if (this->rx.size() < len) { + return false; + } + + std::copy(this->rx.begin(), this->rx.begin() + len, data); + this->rx.erase(this->rx.begin(), this->rx.begin() + len); + return true; + } + + size_t available() override { return this->rx.size(); } + + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +class TestableMitsubishiCN105 : public MitsubishiCN105 { + public: + using MitsubishiCN105::MitsubishiCN105; + using MitsubishiCN105::State; + using MitsubishiCN105::state_; + using MitsubishiCN105::write_timeout_start_ms_; + + static inline uint32_t test_loop_time_ms = 0; + + void set_current_time(uint32_t ms) { test_loop_time_ms = ms; } +}; + +} // namespace esphome::mitsubishi_cn105::testing From 53b6528cc5dd88161c364b53518b557c38e2f31a Mon Sep 17 00:00:00 2001 From: alorente <gitmaster@passific.fr> Date: Sat, 4 Apr 2026 08:02:15 +0200 Subject: [PATCH 1206/2056] [epaper_spi] Allow runtime rotation change (#15419) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/display/__init__.py | 5 ++-- esphome/components/epaper_spi/display.py | 14 +---------- esphome/components/epaper_spi/epaper_spi.cpp | 23 ++++++++++++++++--- esphome/components/epaper_spi/epaper_spi.h | 17 ++++++++++---- .../mipi_dsi/test_mipi_dsi_config.py | 2 +- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 4d79a0a31bb..67d76a59d9d 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -117,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) async def setup_display_core_(var, config): - if CONF_ROTATION in config: - cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) + if rotation := config.get(CONF_ROTATION, 0): + # Default initialised value for rotation is 0 + cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation])) if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None: # Default to true if pages or lambda is specified. Ideally this would be done during validation, but diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 2657071f45a..658f9e2c4a7 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -175,9 +175,7 @@ async def to_code(config): *model.get_constructor_args(config), ) - # Rotation is handled by setting the transform - display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} - await display.register_display(var, display_config) + await display.register_display(var, config) await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) @@ -201,16 +199,6 @@ async def to_code(config): transform[CONF_SWAP_XY] = False else: transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} - rotation = config[CONF_ROTATION] - if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] - elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - elif rotation == 270: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] transform_str = "|".join( { str(getattr(Transform, x.upper())) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index ae1923a9162..a2ca311b305 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -97,6 +97,23 @@ bool EPaperBase::reset() { return true; } +void EPaperBase::update_effective_transform_() { + switch (this->rotation_) { + case DISPLAY_ROTATION_90_DEGREES: + this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X); + break; + case DISPLAY_ROTATION_180_DEGREES: + this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X); + break; + case DISPLAY_ROTATION_270_DEGREES: + this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y); + break; + default: + this->effective_transform_ = this->transform_; + break; + } +} + void EPaperBase::update() { if (this->state_ != EPaperState::IDLE) { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); @@ -280,11 +297,11 @@ bool EPaperBase::initialise(bool partial) { bool EPaperBase::rotate_coordinates_(int &x, int &y) { if (!this->get_clipping().inside(x, y)) return false; - if (this->transform_ & SWAP_XY) + if (this->effective_transform_ & SWAP_XY) std::swap(x, y); - if (this->transform_ & MIRROR_X) + if (this->effective_transform_ & MIRROR_X) x = this->width_ - x - 1; - if (this->transform_ & MIRROR_Y) + if (this->effective_transform_ & MIRROR_Y) y = this->height_ - y - 1; if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return false; diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index a7439855185..47b4f9f72df 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/components/spi/spi.h" #include "esphome/components/split_buffer/split_buffer.h" #include "esphome/core/component.h" @@ -51,7 +51,14 @@ class EPaperBase : public Display, void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } - void set_transform(uint8_t transform) { this->transform_ = transform; } + void set_transform(uint8_t transform) { + this->transform_ = transform; + this->update_effective_transform_(); + } + void set_rotation(DisplayRotation rotation) override { + Display::set_rotation(rotation); + this->update_effective_transform_(); + } void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } void dump_config() override; @@ -106,8 +113,8 @@ class EPaperBase : public Display, protected: int get_height_internal() override { return this->height_; }; int get_width_internal() override { return this->width_; }; - int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } - int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; } void draw_pixel_at(int x, int y, Color color) override; void process_state_(); @@ -119,6 +126,7 @@ class EPaperBase : public Display, void send_init_sequence_(const uint8_t *sequence, size_t length); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); + void update_effective_transform_(); bool rotate_coordinates_(int &x, int &y); /** @@ -171,6 +179,7 @@ class EPaperBase : public Display, uint32_t delay_until_{}; // timestamp until which to delay processing uint16_t next_delay_{}; // milliseconds to delay before next state uint8_t transform_{}; + uint8_t effective_transform_{}; uint8_t update_count_{}; // these values represent the bounds of the updated buffer. Note that x_high and y_high // point to the pixel past the last one updated, i.e. may range up to width/height. diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 1ae8cc644e0..119bbf7feab 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -133,6 +133,6 @@ def test_code_generation( assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp - assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp From 89de00e7ce485c982a454e9fc26ece9a144d9a6a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:04:01 +1000 Subject: [PATCH 1207/2056] [online_image] Clear LVGL dsc when image size changes. (#15360) --- esphome/components/runtime_image/runtime_image.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 2ebe67c3a55..fa42b53496b 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -248,6 +248,9 @@ void RuntimeImage::release_buffer_() { this->height_ = 0; this->buffer_width_ = 0; this->buffer_height_ = 0; +#ifdef USE_LVGL + memset(&this->dsc_, 0, sizeof(this->dsc_)); +#endif } } From b0d39aedd33be6f8380d90728d220e0e4fdf0bad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 00:30:29 -1000 Subject: [PATCH 1208/2056] [hlw8012] Change periodic sensor reading logs to LOGV (#15431) --- esphome/components/hlw8012/hlw8012.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index d0fd697d8ff..22f292e47e1 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -73,13 +73,13 @@ void HLW8012Component::update() { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { float current = cf1_hz * this->current_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current); + ESP_LOGV(TAG, "Got power=%.1fW, current=%.1fA", power, current); if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); } } else { float voltage = cf1_hz * this->voltage_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); + ESP_LOGV(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); } From 9ee5089891f54ef3e986db448b52b1dfaf9bafbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 00:30:41 -1000 Subject: [PATCH 1209/2056] [time] Support */N syntax in cron expressions (#15434) --- esphome/components/time/__init__.py | 4 +- tests/unit_tests/components/test_time.py | 80 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/components/test_time.py diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index c31ccbc7eac..7ac0abeee0f 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -123,8 +123,8 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): f"Can't have more than two '/' in one time expression, got {part}" ) offset, repeat = data - offset_n = 0 - if offset: + offset_n = min_value + if offset and offset not in ("*", "?"): offset_n = _parse_cron_int( offset, special_mapping, diff --git a/tests/unit_tests/components/test_time.py b/tests/unit_tests/components/test_time.py new file mode 100644 index 00000000000..48988fb03f9 --- /dev/null +++ b/tests/unit_tests/components/test_time.py @@ -0,0 +1,80 @@ +"""Tests for time component cron expression parsing.""" + +from esphome.components.time import _parse_cron_part + + +def test_star_slash_seconds() -> None: + assert _parse_cron_part("*/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_star_slash_minutes() -> None: + assert _parse_cron_part("*/5", 0, 59, {}) == { + 0, + 5, + 10, + 15, + 20, + 25, + 30, + 35, + 40, + 45, + 50, + 55, + } + + +def test_star_slash_hours() -> None: + assert _parse_cron_part("*/2", 0, 23, {}) == { + 0, + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + } + + +def test_star_slash_days_of_month() -> None: + """days_of_month starts at 1, not 0.""" + assert _parse_cron_part("*/5", 1, 31, {}) == {1, 6, 11, 16, 21, 26, 31} + + +def test_question_slash() -> None: + assert _parse_cron_part("?/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_empty_offset_slash() -> None: + """Empty offset defaults to min_value.""" + assert _parse_cron_part("/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_empty_offset_slash_nonzero_min() -> None: + """Empty offset defaults to min_value, not 0.""" + assert _parse_cron_part("/5", 1, 31, {}) == {1, 6, 11, 16, 21, 26, 31} + + +def test_numeric_offset_slash() -> None: + assert _parse_cron_part("5/10", 0, 60, {}) == {5, 15, 25, 35, 45, 55} + + +def test_star() -> None: + assert _parse_cron_part("*", 0, 59, {}) == set(range(0, 60)) + + +def test_question() -> None: + assert _parse_cron_part("?", 0, 59, {}) == set(range(0, 60)) + + +def test_range() -> None: + assert _parse_cron_part("1-5", 0, 59, {}) == {1, 2, 3, 4, 5} + + +def test_single_value() -> None: + assert _parse_cron_part("30", 0, 59, {}) == {30} From f51871fa6b577216d80d9a2699c757a0d109becd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 00:37:50 -1000 Subject: [PATCH 1210/2056] [total_daily_energy] Replace loop() with timeout-based midnight reset (#15432) --- .../total_daily_energy/total_daily_energy.cpp | 67 ++++++++++++++----- .../total_daily_energy/total_daily_energy.h | 8 +-- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index e7a45a5edfa..161c712cc16 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -1,10 +1,21 @@ #include "total_daily_energy.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { static const char *const TAG = "total_daily_energy"; +static constexpr uint32_t TIMEOUT_ID_MIDNIGHT = 1; +static constexpr uint8_t SECONDS_PER_MINUTE = 60; +static constexpr uint8_t MINUTES_PER_HOUR = 60; +static constexpr uint8_t HOURS_PER_DAY = 24; +static constexpr uint32_t SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; +static constexpr uint16_t MILLIS_PER_SECOND = 1000; +// Wake up 90 minutes before midnight to recalculate, ensuring DST transitions +// (which shift wall clock by 1 hour but don't change millis()) don't cause +// the midnight reset to fire late. DST transitions don't trigger the time sync +// callback since they change local time interpretation, not the epoch. +static constexpr uint32_t PRE_MIDNIGHT_SECONDS = 90 * SECONDS_PER_MINUTE; void TotalDailyEnergy::setup() { float initial_value = 0; @@ -15,28 +26,55 @@ void TotalDailyEnergy::setup() { } this->publish_state_and_save(initial_value); - this->last_update_ = millis(); + this->last_update_ = App.get_loop_component_start_time(); this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); + + // Schedule initial midnight reset if time is already valid, otherwise + // the time sync callback will handle it once time becomes available. + this->schedule_midnight_reset_(); + // Re-schedule on every NTP sync in case the clock jumped across midnight. + this->time_->add_on_time_sync_callback([this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } -void TotalDailyEnergy::loop() { +void TotalDailyEnergy::schedule_midnight_reset_() { auto t = this->time_->now(); if (!t.is_valid()) return; - if (this->last_day_of_year_ == 0) { + // Check if the day changed (time sync moved us past midnight, or first call) + if (this->last_day_of_year_ != t.day_of_year) { + if (this->last_day_of_year_ != 0) { + // Day actually changed — reset energy + this->total_energy_ = 0; + this->publish_state_and_save(0); + } this->last_day_of_year_ = t.day_of_year; - return; } - if (t.day_of_year != this->last_day_of_year_) { - this->last_day_of_year_ = t.day_of_year; - this->total_energy_ = 0; - this->publish_state_and_save(0); + // Calculate seconds until next midnight. + // Uses the same TIMEOUT_ID_MIDNIGHT ID so re-scheduling (e.g. from time sync) cancels + // any previously pending timeout. + uint32_t seconds_until_midnight = + ((HOURS_PER_DAY - 1 - t.hour) * MINUTES_PER_HOUR + (MINUTES_PER_HOUR - 1 - t.minute)) * SECONDS_PER_MINUTE + + (SECONDS_PER_MINUTE - t.second); + + // set_timeout counts real elapsed millis, but DST shifts wall clock by up to 1 hour + // without changing millis. To avoid firing up to 1 hour late/early, we use two stages: + // 1) Wake up 90 minutes before midnight to recalculate with current wall clock + // 2) From there, schedule the precise midnight reset + uint32_t timeout_seconds; + if (seconds_until_midnight > PRE_MIDNIGHT_SECONDS) { + timeout_seconds = seconds_until_midnight - PRE_MIDNIGHT_SECONDS; + } else { + timeout_seconds = seconds_until_midnight + 1; } + + ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, + [this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::publish_state_and_save(float state) { @@ -50,14 +88,14 @@ void TotalDailyEnergy::publish_state_and_save(float state) { void TotalDailyEnergy::process_new_state_(float state) { if (std::isnan(state)) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const float old_state = this->last_power_state_; const float new_state = state; - float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_hours = (now - this->last_update_) / static_cast<float>(MILLIS_PER_SECOND) / SECONDS_PER_HOUR; float delta_energy = 0.0f; switch (this->method_) { case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: - delta_energy = delta_hours * (old_state + new_state) / 2.0; + delta_energy = delta_hours * (old_state + new_state) / 2.0f; break; case TOTAL_DAILY_ENERGY_METHOD_LEFT: delta_energy = delta_hours * old_state; @@ -71,5 +109,4 @@ void TotalDailyEnergy::process_new_state_(float state) { this->publish_state_and_save(this->total_energy_ + delta_energy); } -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1145f54f957..9a20ecea017 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { enum TotalDailyEnergyMethod { TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, @@ -23,12 +22,12 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - void loop() override; void publish_state_and_save(float state); protected: void process_new_state_(float state); + void schedule_midnight_reset_(); ESPPreferenceObject pref_; time::RealTimeClock *time_; @@ -41,5 +40,4 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { float last_power_state_{0.0f}; }; -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy From 297f9c134f3b1000d8dddefcac5acf5240c50d45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 01:07:16 -1000 Subject: [PATCH 1211/2056] [time] Use set_interval for CronTrigger instead of loop() (#15433) --- esphome/components/time/automation.cpp | 7 ++++++- esphome/components/time/automation.h | 3 ++- tests/components/time/common.yaml | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 8bc87878d1e..7eb99cfe743 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) { return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] && this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week]; } -void CronTrigger::loop() { +void CronTrigger::setup() { + // Cron resolution is 1 second — check once per second instead of every loop iteration + this->set_interval(1000, [this]() { this->check_time_(); }); +} + +void CronTrigger::check_time_() { ESPTime time = this->rtc_->now(); if (!time.is_valid()) return; diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 4ccfc641d6c..546c4a10de2 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component { void add_day_of_week(uint8_t day_of_week); void add_days_of_week(const std::vector<uint8_t> &days_of_week); bool matches(const ESPTime &time); - void loop() override; + void setup() override; float get_setup_priority() const override; protected: + void check_time_(); std::bitset<61> seconds_; std::bitset<60> minutes_; std::bitset<24> hours_; diff --git a/tests/components/time/common.yaml b/tests/components/time/common.yaml index 465be045dbd..cd258c7aa6d 100644 --- a/tests/components/time/common.yaml +++ b/tests/components/time/common.yaml @@ -6,5 +6,9 @@ api: time: - platform: homeassistant + on_time: + - seconds: "0,10,20,30,40,50" + then: + - logger.log: "CronTrigger fired (every 10 seconds)" - platform: sntp id: sntp_time From 1a1725f958a285f7018c48d89c8f509c58ddad0b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:11:29 +1000 Subject: [PATCH 1212/2056] [esp32] Clean build when sdkconfig options change (#15439) --- esphome/components/esp32/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0ce1117262f..5cae67db132 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -48,7 +48,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache +from esphome.writer import clean_build, clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa @@ -2195,6 +2195,7 @@ def _write_sdkconfig(): if write_file_if_changed(internal_path, contents): # internal changed, update real one write_file_if_changed(sdk_path, contents) + clean_build(clear_pio_cache=False) def _write_idf_component_yml(): From 830517a98f4db08749bcb5e36182d3d6ebb958ab Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Sun, 5 Apr 2026 00:40:05 +0200 Subject: [PATCH 1213/2056] [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 3) (#15437) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 160 ++++++++++++++++-- .../mitsubishi_cn105/mitsubishi_cn105.h | 37 +++- .../mitsubishi_cn105_climate.cpp | 23 ++- .../mitsubishi_cn105_climate.h | 2 + .../mitsubishi_cn105_time.cpp | 2 +- .../climate/mitsubishi_cn105_tests.cpp | 156 +++++++++++++++-- tests/components/mitsubishi_cn105/common.cpp | 2 +- tests/components/mitsubishi_cn105/common.h | 3 + 8 files changed, 353 insertions(+), 32 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index e3923bb0b81..0bce8da1ad4 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -8,6 +8,7 @@ static const char *const TAG = "mitsubishi_cn105.driver"; static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; +static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10; static constexpr size_t HEADER_LEN = 5; static constexpr uint8_t PREAMBLE = 0xFC; static constexpr uint8_t HEADER_BYTE_1 = 0x01; @@ -15,7 +16,13 @@ static constexpr uint8_t HEADER_BYTE_2 = 0x30; static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A; static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A; -static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}}; +static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01}; + +static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42; +static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62; +static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02; +static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; +static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP}; static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); @@ -30,19 +37,29 @@ static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, Payloa return packet; } +static float decode_temperature(int temp_a, int temp_b, int delta) { + return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a; +} + static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD); void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } -void MitsubishiCN105::update() { +bool MitsubishiCN105::update() { + if (const auto start = this->status_update_start_ms_; + start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) { + this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); + return false; + } + if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { this->write_timeout_start_ms_.reset(); this->read_pos_ = 0; this->set_state_(State::READ_TIMEOUT); - return; + return false; } - this->read_incoming_bytes_(); + return this->read_incoming_bytes_(); } void MitsubishiCN105::set_state_(State new_state) { @@ -63,9 +80,24 @@ bool MitsubishiCN105::should_transition(State from, State to) { return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT; case State::CONNECTED: - case State::READ_TIMEOUT: return from == State::CONNECTING; + case State::UPDATING_STATUS: + return from == State::CONNECTED || from == State::STATUS_UPDATED || + from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE; + + case State::STATUS_UPDATED: + return from == State::UPDATING_STATUS; + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return from == State::STATUS_UPDATED; + + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return from == State::SCHEDULE_NEXT_STATUS_UPDATE; + + case State::READ_TIMEOUT: + return from == State::UPDATING_STATUS || from == State::CONNECTING; + default: return false; } @@ -79,7 +111,30 @@ void MitsubishiCN105::did_transition_(State to) { case State::CONNECTED: this->write_timeout_start_ms_.reset(); - // TODO: read AC status after connected, next PR + this->status_msg_index_ = 0; + this->set_state_(State::UPDATING_STATUS); + break; + + case State::UPDATING_STATUS: + this->update_status_(); + break; + + case State::STATUS_UPDATED: { + this->write_timeout_start_ms_.reset(); + if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) { + this->status_msg_index_ = 0; + } + if (this->status_msg_index_ != 0) { + this->set_state_(State::UPDATING_STATUS); + } else { + this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); + } + break; + } + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + this->status_update_start_ms_ = get_loop_time_ms(); + this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); break; case State::READ_TIMEOUT: @@ -97,13 +152,24 @@ void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { this->write_timeout_start_ms_ = get_loop_time_ms(); } -void MitsubishiCN105::read_incoming_bytes_() { +void MitsubishiCN105::update_status_() { + ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_); + std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]}; + this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload)); +} + +void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) { + this->status_update_start_ms_.reset(); + this->set_state_(state); +} + +bool MitsubishiCN105::read_incoming_bytes_() { uint8_t watchdog = 64; while (this->device_.available() > 0 && watchdog-- > 0) { uint8_t &value = this->read_buffer_[this->read_pos_]; if (!this->device_.read_byte(&value)) { ESP_LOGW(TAG, "UART read failed while data available"); - return; + return false; } switch (++this->read_pos_) { @@ -149,23 +215,85 @@ void MitsubishiCN105::read_incoming_bytes_() { continue; } - this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN); + bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, + len_without_checksum - HEADER_LEN); this->reset_read_position_and_dump_buffer_("RX"); + return processed; } + + return false; } -void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { +bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { switch (type) { case PACKET_TYPE_CONNECT_RESPONSE: this->set_state_(State::CONNECTED); - break; + return false; + + case PACKET_TYPE_STATUS_RESPONSE: + return this->process_status_packet_(payload, len); default: ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type); - break; + return false; } } +bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) { + if (len == 0) { + ESP_LOGVV(TAG, "RX status packet too short"); + return false; + } + + const auto previous = this->status_; + const auto msg_type = payload[0]; + if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) { + return false; + } + + if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) { + this->set_state_(State::STATUS_UPDATED); + } + + return previous != this->status_ && this->is_status_initialized(); +} + +bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) { + switch (msg_type) { + case STATUS_MSG_SETTINGS: + return this->parse_status_settings_(payload, len); + + case STATUS_MSG_ROOM_TEMP: + return this->parse_status_room_temperature_(payload, len); + + default: + ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type); + return false; + } +} + +bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) { + if (len <= 10) { + ESP_LOGVV(TAG, "RX settings payload too short"); + return false; + } + + this->status_.power_on = payload[2] != 0; + this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31); + + return true; +} + +bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) { + if (len <= 5) { + ESP_LOGVV(TAG, "RX room temperature payload too short"); + return false; + } + + this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10); + return true; +} + void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) { dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); this->read_pos_ = 0; @@ -186,6 +314,14 @@ const LogString *MitsubishiCN105::state_to_string(State state) { return LOG_STR("Connecting"); case State::CONNECTED: return LOG_STR("Connected"); + case State::UPDATING_STATUS: + return LOG_STR("UpdatingStatus"); + case State::STATUS_UPDATED: + return LOG_STR("StatusUpdated"); + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return LOG_STR("ScheduleNextStatusUpdate"); + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return LOG_STR("WaitingForScheduledStatusUpdate"); case State::READ_TIMEOUT: return LOG_STR("ReadTimeout"); } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index fc09b3bed24..d43904b3133 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -9,23 +9,49 @@ uint32_t get_loop_time_ms(); class MitsubishiCN105 { public: + struct Status { + bool operator==(const Status &) const = default; + + bool power_on{false}; + float target_temperature{NAN}; + float room_temperature{NAN}; + }; + explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} void initialize(); - void update(); + bool update(); uint32_t get_update_interval() const { return this->update_interval_ms_; } void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } + const Status &status() const { return this->status_; } + bool is_status_initialized() const { return !std::isnan(status_.room_temperature); } + protected: - enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT }; + enum class State : uint8_t { + NOT_CONNECTED, + CONNECTING, + CONNECTED, + UPDATING_STATUS, + STATUS_UPDATED, + SCHEDULE_NEXT_STATUS_UPDATE, + WAITING_FOR_SCHEDULED_STATUS_UPDATE, + READ_TIMEOUT + }; void set_state_(State new_state); void did_transition_(State to); - void read_incoming_bytes_(); - void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + bool read_incoming_bytes_(); + bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + bool process_status_packet_(const uint8_t *payload, size_t len); + bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len); + bool parse_status_settings_(const uint8_t *payload, size_t len); + bool parse_status_room_temperature_(const uint8_t *payload, size_t len); void reset_read_position_and_dump_buffer_(const char *prefix); void send_packet_(const uint8_t *packet, size_t len); + void update_status_(); + void cancel_waiting_and_transition_to_(State state); template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); static const LogString *state_to_string(State state); @@ -34,7 +60,10 @@ class MitsubishiCN105 { uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; std::optional<uint32_t> write_timeout_start_ms_; + std::optional<uint32_t> status_update_start_ms_; + Status status_{}; State state_{State::NOT_CONNECTED}; + uint8_t status_msg_index_{0}; private: static constexpr size_t READ_BUFFER_SIZE = 32; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index cce6bef5e46..55fc23c4499 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -17,13 +17,34 @@ void MitsubishiCN105Climate::dump_config() { void MitsubishiCN105Climate::setup() { this->hp_.initialize(); } -void MitsubishiCN105Climate::loop() { this->hp_.update(); } +void MitsubishiCN105Climate::loop() { + if (this->hp_.update()) { + this->apply_values_(); + } +} climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; + + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + + traits.set_visual_min_temperature(16.0f); + traits.set_visual_max_temperature(31.0f); + traits.set_visual_temperature_step(1.0f); + traits.set_visual_current_temperature_step(0.5f); + return traits; } void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {} +void MitsubishiCN105Climate::apply_values_() { + const auto &status = this->hp_.status(); + + this->target_temperature = status.target_temperature; + this->current_temperature = status.room_temperature; + + this->publish_state(); +} + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h index 08b482025ff..da8f8d8d0af 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -21,6 +21,8 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } protected: + void apply_values_(); + MitsubishiCN105 hp_; }; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp index 55a0a2328fe..0f3fcb56489 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp @@ -2,6 +2,6 @@ namespace esphome::mitsubishi_cn105 { -uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); }; +uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); } } // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index e01d9e69ff1..5b4f84623ef 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -25,27 +25,80 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) { EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123}); } -TEST(MitsubishiCN105Tests, SuccessfullyConnects) { +TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { auto ctx = TestContext{}; ctx.sut.initialize(); ctx.uart.tx.clear(); // Remove first connect packet bytes EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); - EXPECT_TRUE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); // Connect response ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55}); - ctx.sut.update(); + ctx.sut.set_current_time(200); + ASSERT_FALSE(ctx.sut.update()); - // All bytes from UART should be consumed and state = CONNECTED + // All bytes from UART should be consumed EXPECT_TRUE(ctx.uart.rx.empty()); - EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTED); - EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + // After successful connect we request status, first settings (0x02) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{200}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // Settings response + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99}); + + // Settings should still have initial values + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); + + ctx.sut.set_current_time(300); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + + // Check settings that we just read from received package + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); + + // Now fetch room temperature (0x03) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{300}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // Room temperature response + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x0B, 0x00, 0x00, + 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5}); + + // Room temperature should still have initial value + EXPECT_THAT(ctx.sut.status().room_temperature, ::testing::IsNan()); + + ctx.sut.set_current_time(400); + EXPECT_FALSE(ctx.sut.is_status_initialized()); + ASSERT_TRUE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + EXPECT_TRUE(ctx.sut.is_status_initialized()); + + // Check room temperature we just read from received package + EXPECT_EQ(ctx.sut.status().room_temperature, 21.0f); - // Nothing should be send to UART EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{400}); } TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { @@ -55,21 +108,21 @@ TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { ctx.uart.tx.clear(); // Remove first connect packet bytes // No response (no RX data), no retry yet - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); // Still no response after 1999ms, no retry yet ctx.sut.set_current_time(1999); - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); // Stop waiting after 2s and retry connect ctx.sut.set_current_time(2000); - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000}); @@ -92,7 +145,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { ASSERT_GT(ctx.uart.rx.size(), 64); // No valid response, no state change expected - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); @@ -100,7 +153,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { EXPECT_FALSE(ctx.uart.rx.empty()); // Next update will read remaining bytes, no state change expected - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_TRUE(ctx.uart.rx.empty()); @@ -162,7 +215,7 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { // Drain RX - no valid response, no state change expected int iterations = 0; while (!ctx.uart.rx.empty() && iterations++ < 10) { - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); } @@ -170,4 +223,81 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { EXPECT_TRUE(ctx.uart.rx.empty()); } +TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) { + auto ctx = TestContext{}; + + ctx.sut.set_update_interval(2000); + ctx.sut.set_current_time(80000); + + // No scheduled status update + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Status update completed, schedule next status update + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{80000}); + + // Wait for update_interval (ms) before doing another status update + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(81999); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(82000); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_FALSE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56}); + + ctx.sut.update(); + + EXPECT_TRUE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7}); + + ctx.sut.update(); + + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 16.0f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f); +} + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.cpp b/tests/components/mitsubishi_cn105/common.cpp index ea13d7676c4..50993c5c2c9 100644 --- a/tests/components/mitsubishi_cn105/common.cpp +++ b/tests/components/mitsubishi_cn105/common.cpp @@ -2,6 +2,6 @@ namespace esphome::mitsubishi_cn105 { -uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; }; +uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; } } // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index c41268d7233..ed55c3dc0ce 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -44,6 +44,9 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::State; using MitsubishiCN105::state_; using MitsubishiCN105::write_timeout_start_ms_; + using MitsubishiCN105::status_update_start_ms_; + + void set_state(State s) { this->set_state_(s); } static inline uint32_t test_loop_time_ms = 0; From 2d9a42e4bad94c4c608cb18c40d83d2939391a0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 13:56:21 -1000 Subject: [PATCH 1214/2056] [pcf8574][pca9554] Add optional interrupt pin to eliminate polling (#15444) --- .../components/gpio_expander/cached_gpio.h | 20 +++++++++++++++---- esphome/components/pca9554/__init__.py | 4 ++++ esphome/components/pca9554/pca9554.cpp | 19 +++++++++++++++--- esphome/components/pca9554/pca9554.h | 5 ++++- esphome/components/pcf8574/__init__.py | 4 ++++ esphome/components/pcf8574/pcf8574.cpp | 17 +++++++++++++++- esphome/components/pcf8574/pcf8574.h | 5 ++++- tests/components/pca9554/common.yaml | 5 +++++ tests/components/pca9554/test.esp32-idf.yaml | 3 +++ .../components/pca9554/test.esp8266-ard.yaml | 3 +++ tests/components/pca9554/test.rp2040-ard.yaml | 3 +++ tests/components/pcf8574/common.yaml | 5 +++++ tests/components/pcf8574/test.esp32-idf.yaml | 3 +++ .../components/pcf8574/test.esp8266-ard.yaml | 3 +++ tests/components/pcf8574/test.rp2040-ard.yaml | 3 +++ 15 files changed, 92 insertions(+), 10 deletions(-) diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index eeff98cb6e3..ddb9e63686f 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -28,7 +28,10 @@ namespace esphome::gpio_expander { template<typename T, uint16_t N, typename P = typename std::conditional<(N > 256), uint16_t, uint8_t>::type> class CachedGpioExpander { public: - /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. + /// @brief Read the state of the given pin. + /// By default, each read invalidates the pin's cache entry so the next read + /// of the same pin triggers a fresh hardware read. When invalidate_on_read + /// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_(). /// @param pin Pin number to read /// @return Pin state bool digital_read(P pin) { @@ -36,14 +39,17 @@ class CachedGpioExpander { const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { - // Invalidate pin - this->read_cache_valid_[bank] &= ~pin_mask; + if (this->invalidate_on_read_) { + // Invalidate pin so next read triggers hardware read + this->read_cache_valid_[bank] &= ~pin_mask; + } } else { // Read whole bank from hardware if (!this->digital_read_hw(pin)) return false; // Mark bank cache as valid except the pin that is being returned now - this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask; + // (when not invalidating on read, mark all pins including this one as valid) + this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~(this->invalidate_on_read_ ? pin_mask : 0); } return this->digital_read_cache(pin); } @@ -71,12 +77,18 @@ class CachedGpioExpander { /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } + /// @brief Control whether digital_read() invalidates the pin's cache entry after reading. + /// When enabled (default), each read self-invalidates so the next read triggers a hardware read. + /// When disabled, cache stays valid until reset_pin_cache_() is explicitly called. + void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; } + static constexpr uint16_t BITS_PER_BYTE = 8; static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; static constexpr size_t BANKS = N / BANK_SIZE; static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); T read_cache_valid_[BANKS]{0}; + bool invalidate_on_read_{true}; }; } // namespace esphome::gpio_expander diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 626b08a3781..99b812b33bd 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -29,6 +30,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PCA9554Component), cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -43,6 +45,8 @@ async def to_code(config): cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index adc7bc0fb5c..9b300eaac28 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -34,12 +34,24 @@ void PCA9554Component::setup() { this->read_inputs_(); ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); -} + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); + // With interrupt pin, only run loop when interrupt fires + this->disable_loop(); + } +} +void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); } void PCA9554Component::loop() { - // Invalidate the cache at the start of each loop. - // The actual read will happen on demand when digital_read() is called + // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + // Interrupt-driven: disable loop until next interrupt fires + this->disable_loop(); + } } void PCA9554Component::dump_config() { @@ -47,6 +59,7 @@ void PCA9554Component::dump_config() { "PCA9554:\n" " I/O Pins: %d", this->pin_count_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 1d877f9ce29..f33f9d4592c 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -16,7 +16,6 @@ class PCA9554Component : public Component, /// Check i2c availability and setup masks void setup() override; - /// Invalidate cache at start of each loop void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -26,8 +25,11 @@ class PCA9554Component : public Component, void dump_config() override; void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + static void IRAM_ATTR gpio_intr(PCA9554Component *arg); + bool read_inputs_(); bool write_register_(uint8_t reg, uint16_t value); @@ -48,6 +50,7 @@ class PCA9554Component : public Component, uint16_t input_mask_{0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCA9554 pin as an internal input GPIO pin. diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index f387d0a610f..902efd22798 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PCF8574Component), cv.Optional(CONF_PCF8575, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -39,6 +41,8 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) cg.add(var.set_pcf8575(config[CONF_PCF8575])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index d3ec31436de..1eeef663b0d 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -15,16 +15,31 @@ void PCF8574Component::setup() { this->write_gpio_(); this->read_gpio_(); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); + // With interrupt pin, only run loop when interrupt fires + this->disable_loop(); + } } +void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); } void PCF8574Component::loop() { - // Invalidate the cache at the start of each loop + // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + // Interrupt-driven: disable loop until next interrupt fires + this->disable_loop(); + } } void PCF8574Component::dump_config() { ESP_LOGCONFIG(TAG, "PCF8574:\n" " Is PCF8575: %s", YESNO(this->pcf8575_)); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index b039173789f..cae2e930b74 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -17,10 +17,10 @@ class PCF8574Component : public Component, PCF8574Component() = default; void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } /// Check i2c availability and setup masks void setup() override; - /// Invalidate cache at start of each loop void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -30,6 +30,8 @@ class PCF8574Component : public Component, void dump_config() override; protected: + static void IRAM_ATTR gpio_intr(PCF8574Component *arg); + bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -44,6 +46,7 @@ class PCF8574Component : public Component, /// The state read in read_gpio_ - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574 + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCF8574 pin as an internal input GPIO pin. diff --git a/tests/components/pca9554/common.yaml b/tests/components/pca9554/common.yaml index 9e5e7f3342b..82a88b90aae 100644 --- a/tests/components/pca9554/common.yaml +++ b/tests/components/pca9554/common.yaml @@ -3,6 +3,11 @@ pca9554: i2c_id: i2c_bus pin_count: 8 address: 0x3F + - id: pca9554_hub_int + i2c_id: i2c_bus + pin_count: 8 + address: 0x3E + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pca9554/test.esp32-idf.yaml b/tests/components/pca9554/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pca9554/test.esp32-idf.yaml +++ b/tests/components/pca9554/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pca9554/test.esp8266-ard.yaml b/tests/components/pca9554/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pca9554/test.esp8266-ard.yaml +++ b/tests/components/pca9554/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pca9554/test.rp2040-ard.yaml b/tests/components/pca9554/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pca9554/test.rp2040-ard.yaml +++ b/tests/components/pca9554/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/pcf8574/common.yaml b/tests/components/pcf8574/common.yaml index 09fa33164e3..8a26b930152 100644 --- a/tests/components/pcf8574/common.yaml +++ b/tests/components/pcf8574/common.yaml @@ -3,6 +3,11 @@ pcf8574: i2c_id: i2c_bus address: 0x21 pcf8575: false + - id: pcf8574_hub_int + i2c_id: i2c_bus + address: 0x22 + pcf8575: false + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pcf8574/test.esp32-idf.yaml b/tests/components/pcf8574/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pcf8574/test.esp32-idf.yaml +++ b/tests/components/pcf8574/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pcf8574/test.esp8266-ard.yaml b/tests/components/pcf8574/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pcf8574/test.esp8266-ard.yaml +++ b/tests/components/pcf8574/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pcf8574/test.rp2040-ard.yaml b/tests/components/pcf8574/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pcf8574/test.rp2040-ard.yaml +++ b/tests/components/pcf8574/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 4d2062282ed68f7f6ca793b4ffb22c73bd130d5c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:11:49 +1000 Subject: [PATCH 1215/2056] [mipi_spi] Run spi final validation (#15418) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/mipi_spi/display.py | 8 +++++--- tests/component_tests/mipi_spi/conftest.py | 11 +++++++++++ .../component_tests/mipi_spi/test_display_metadata.py | 4 +++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 6aa98e3f668..42c7ec22249 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -279,6 +279,10 @@ def _final_validate(config): from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + if config[CONF_BUS_MODE] == TYPE_SINGLE: + spi.final_validate_device_schema(DOMAIN, require_miso=False, require_mosi=True)( + config + ) if not requires_buffer(config) and LVGL_DOMAIN not in global_config: # If no drawing methods are configured, and LVGL is not enabled, show a test card config[CONF_SHOW_TEST_CARD] = True @@ -286,7 +290,7 @@ def _final_validate(config): if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config: # If PSRAM is not enabled, choose a small buffer size by default if not requires_buffer(config): - return config # No buffer needed, so no need to set a buffer size + return # No need to pick a size color_depth = get_color_depth(config) frac = denominator(config) width, height, _offset_width, _offset_height = model.get_dimensions(config) @@ -298,8 +302,6 @@ def _final_validate(config): x for x in range(2, 17) if fraction >= 1 / x ) - return config - FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/tests/component_tests/mipi_spi/conftest.py b/tests/component_tests/mipi_spi/conftest.py index eddf0987d0a..082a9e55f2a 100644 --- a/tests/component_tests/mipi_spi/conftest.py +++ b/tests/component_tests/mipi_spi/conftest.py @@ -1,6 +1,7 @@ """Tests for mpip_spi configuration validation.""" from collections.abc import Callable, Generator +from unittest import mock import pytest @@ -12,6 +13,16 @@ from esphome.core import CORE from esphome.pins import gpio_pin_schema +@pytest.fixture(autouse=True) +def mock_spi_final_validate(): + """Mock spi.final_validate_device_schema since unit tests have no real SPI bus config.""" + with mock.patch( + "esphome.components.spi.final_validate_device_schema", + return_value=lambda config: None, + ): + yield + + @pytest.fixture def choose_variant_with_pins() -> Generator[Callable[[list], None]]: """ diff --git a/tests/component_tests/mipi_spi/test_display_metadata.py b/tests/component_tests/mipi_spi/test_display_metadata.py index ab42a756943..c11c7816e4e 100644 --- a/tests/component_tests/mipi_spi/test_display_metadata.py +++ b/tests/component_tests/mipi_spi/test_display_metadata.py @@ -25,7 +25,9 @@ from tests.component_tests.types import SetCoreConfigCallable def validated_config(config): """Run schema + final validation and return the validated config.""" - return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config def test_metadata_native_quad_default_test_card( From 9ea27e68ee0ed94b6b693c14fab1ac10db31aba2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 4 Apr 2026 22:52:40 -1000 Subject: [PATCH 1216/2056] [pcf8574][pca9554] Disable loop when all pins are outputs (#15455) --- esphome/components/pca9554/pca9554.cpp | 11 +++++++++-- esphome/components/pcf8574/pcf8574.cpp | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 9b300eaac28..ac4f119dfee 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -40,9 +40,11 @@ void PCA9554Component::setup() { this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); // Don't invalidate cache on read — only invalidate when interrupt fires this->set_invalidate_on_read_(false); - // With interrupt pin, only run loop when interrupt fires - this->disable_loop(); } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); } void PCA9554Component::loop() { @@ -89,6 +91,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { // Clear mode mask bit this->config_mask_ &= ~(1 << pin); + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Set mode mask bit this->config_mask_ |= 1 << pin; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 1eeef663b0d..bf4a9442a26 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -21,9 +21,11 @@ void PCF8574Component::setup() { this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); // Don't invalidate cache on read — only invalidate when interrupt fires this->set_invalidate_on_read_(false); - // With interrupt pin, only run loop when interrupt fires - this->disable_loop(); } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); } void PCF8574Component::loop() { @@ -66,6 +68,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->mode_mask_ &= ~(1 << pin); // Write GPIO to enable input mode this->write_gpio_(); + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Set mode mask bit this->mode_mask_ |= 1 << pin; From 2d7eb116f20064cdd5774441627294e36669c98e Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:11:49 +0200 Subject: [PATCH 1217/2056] [spi] Enable host-platform builds for unit testing (#15188) --- esphome/components/spi/spi.cpp | 10 +++++++++- esphome/components/spi/spi.h | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 36344a6d38d..20359135ba0 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -68,7 +68,7 @@ void SPIComponent::dump_config() { LOG_PIN(" SDI Pin: ", this->sdi_pin_); LOG_PIN(" SDO Pin: ", this->sdo_pin_); for (size_t i = 0; i != this->data_pins_.size(); i++) { - ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); + ESP_LOGCONFIG(TAG, " Data pin %zu: GPIO%d", i, this->data_pins_[i]); } if (this->spi_bus_->is_hw()) { ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); @@ -118,4 +118,12 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { return out_data; } +#if !defined(USE_ESP32) && !defined(USE_ARDUINO) +// Stub for unsupported platforms (host, Zephyr, etc.) - hardware SPI is unavailable +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector<uint8_t> &data_pins) { + return nullptr; +} +#endif + } // namespace esphome::spi diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 84c8bca2671..dc538f4c41f 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -23,9 +23,9 @@ using SPIInterface = SPIClassRP2040 *; using SPIInterface = SPIClass *; #endif -#elif defined(CLANG_TIDY) +#elif defined(USE_HOST) || defined(CLANG_TIDY) -using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) +using SPIInterface = void *; // Stub for platforms without SPI (e.g., host, Zephyr) #endif // USE_ESP32 / USE_ARDUINO From dae8ea1b043bacb0a26def16b3ce4929324e6064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 08:26:39 -1000 Subject: [PATCH 1218/2056] [mcp23xxx][pi4ioe5v6408] Add optional interrupt pin to eliminate polling (#15445) --- esphome/components/mcp23008/mcp23008.cpp | 7 ++++- esphome/components/mcp23017/mcp23017.cpp | 23 ++++++++++++---- esphome/components/mcp23s08/mcp23s08.cpp | 3 +++ esphome/components/mcp23s17/mcp23s17.cpp | 23 +++++++++++----- .../mcp23x08_base/mcp23x08_base.cpp | 5 ++++ .../mcp23x17_base/mcp23x17_base.cpp | 5 ++++ esphome/components/mcp23xxx_base/__init__.py | 4 +++ .../mcp23xxx_base/mcp23xxx_base.cpp | 7 ++++- .../components/mcp23xxx_base/mcp23xxx_base.h | 23 +++++++++++++++- esphome/components/pi4ioe5v6408/__init__.py | 4 +++ .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 26 ++++++++++++++++++- .../components/pi4ioe5v6408/pi4ioe5v6408.h | 4 +++ tests/components/mcp23008/common.yaml | 14 ++++++++-- tests/components/mcp23008/test.esp32-idf.yaml | 3 +++ .../components/mcp23008/test.esp8266-ard.yaml | 3 +++ .../components/mcp23008/test.rp2040-ard.yaml | 3 +++ tests/components/mcp23017/common.yaml | 14 ++++++++-- tests/components/mcp23017/test.esp32-idf.yaml | 3 +++ .../components/mcp23017/test.esp8266-ard.yaml | 3 +++ .../components/mcp23017/test.rp2040-ard.yaml | 3 +++ tests/components/mcp23s08/common.yaml | 1 + tests/components/mcp23s08/test.esp32-idf.yaml | 1 + .../components/mcp23s08/test.esp8266-ard.yaml | 1 + .../components/mcp23s08/test.rp2040-ard.yaml | 1 + tests/components/mcp23s17/common.yaml | 1 + tests/components/mcp23s17/test.esp32-idf.yaml | 1 + .../components/mcp23s17/test.esp8266-ard.yaml | 1 + .../components/mcp23s17/test.rp2040-ard.yaml | 1 + tests/components/pi4ioe5v6408/common.yaml | 15 ++++++++--- .../pi4ioe5v6408/test.esp32-idf.yaml | 1 + .../pi4ioe5v6408/test.rp2040-ard.yaml | 1 + 31 files changed, 183 insertions(+), 22 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 64b120daa49..5f73e03f6f6 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -22,9 +22,14 @@ void MCP23008::setup() { // enable open-drain interrupt pins, 3.3V-safe this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } + + this->setup_interrupt_pin_(); } -void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } +void MCP23008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23008:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23008::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index e14e317d440..212c15ccf27 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -6,7 +6,8 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; -static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin void MCP23017::setup() { uint8_t iocon; @@ -19,14 +20,26 @@ void MCP23017::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = 0; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); + iocon_flags |= IOCON_ODR; } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (iocon_flags != 0) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags); + } + + this->setup_interrupt_pin_(); } -void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } +void MCP23017::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23017:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23017::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 1c17b66637f..983c1aa6008 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -34,11 +34,14 @@ void MCP23S08::setup() { // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23S08::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S08:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index c6abd7ad594..db9a34e2302 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -7,9 +7,10 @@ namespace mcp23s17 { static const char *const TAG = "mcp23s17"; // IOCON register bits -static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode -static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable -static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin void MCP23S17::set_device_address(uint8_t device_addr) { if (device_addr != 0) { @@ -37,16 +38,26 @@ void MCP23S17::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = IOCON_SEQOP | IOCON_HAEN; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); + iocon_flags |= IOCON_ODR; } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (this->open_drain_ints_ || this->interrupt_pin_ != nullptr) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags); + } + + this->setup_interrupt_pin_(); } void MCP23S17::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S17:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 92228be62c9..e4f4d50aae1 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -32,6 +32,11 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } } void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 6f95ee98fdb..42613053de7 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -44,6 +44,11 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } } void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index d6e82101adf..cd952099c05 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, CONF_INPUT, CONF_INTERRUPT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -32,6 +33,7 @@ MCP23XXX_INTERRUPT_MODES = { MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ).extend(cv.COMPONENT_SCHEMA) @@ -43,6 +45,8 @@ async def register_mcp23xxx(config, num_pins): await cg.register_component(var, config) CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) return var diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 535119fc5c1..4c1daac5623 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -7,7 +7,12 @@ namespace mcp23xxx_base { template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() { this->pin_mode(flags_); - this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + // When interrupt_pin is configured, pin_mode() already auto-enables CHANGE + // interrupt for input pins, so skip the explicit call if the user didn't + // override the default (NO_INTERRUPT) + if (this->interrupt_mode_ != MCP23XXX_NO_INTERRUPT || this->parent_->get_interrupt_pin() == nullptr) { + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + } } template<uint8_t N> void MCP23XXXGPIOPin<N>::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } template<uint8_t N> bool MCP23XXXGPIOPin<N>::digital_read() { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index fb992466d5d..e77eac87e74 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -15,11 +15,31 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander: virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; } float get_setup_priority() const override { return setup_priority::IO; } - void loop() override { this->reset_pin_cache_(); } + void loop() override { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } + } protected: + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the GPIO read clears the latch. One harmless extra read at most. + void setup_interrupt_pin_() { + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + this->disable_loop(); + } + } + static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } + // read a given register virtual bool read_reg(uint8_t reg, uint8_t *value) = 0; // write a value to a given register @@ -28,6 +48,7 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander: virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0; bool open_drain_ints_; + InternalGPIOPin *interrupt_pin_{nullptr}; }; template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin { diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py index c64f923823e..d5b19dab1c8 100644 --- a/esphome/components/pi4ioe5v6408/__init__.py +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -33,6 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), cv.Optional(CONF_RESET, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -46,6 +48,8 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_reset(config[CONF_RESET])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 9247e114f04..8e38e7fa1d6 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -33,9 +33,21 @@ void PI4IOE5V6408Component::setup() { return; } } + + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the read clears the latch. One harmless extra read at most. + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + this->disable_loop(); + } } +void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); } void PI4IOE5V6408Component::dump_config() { ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -60,7 +72,12 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->write_gpio_modes_(); } -void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } +void PI4IOE5V6408Component::loop() { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } +} bool PI4IOE5V6408Component::read_gpio_outputs_() { if (this->is_failed()) @@ -142,6 +159,13 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } + // Enable interrupts for input pins when interrupt pin is configured + // (input pins have mode_mask_ bit cleared) + if (this->interrupt_pin_ != nullptr && + !this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, static_cast<uint8_t>(~this->mode_mask_))) { + this->status_set_warning(LOG_STR("Failed to write interrupt enable mask")); + return false; + } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Wrote GPIO config:\n" diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index 4dc31201ce4..ff2474fe994 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -22,8 +22,11 @@ class PI4IOE5V6408Component : public Component, /// Indicate if the component should reset the state during setup void set_reset(bool reset) { this->reset_ = reset; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg); + bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -40,6 +43,7 @@ class PI4IOE5V6408Component : public Component, uint8_t pull_up_down_mask_{0x00}; bool reset_{true}; + InternalGPIOPin *interrupt_pin_{nullptr}; bool read_gpio_modes_(); bool write_gpio_modes_(); diff --git a/tests/components/mcp23008/common.yaml b/tests/components/mcp23008/common.yaml index 4a407adfd82..7eeee409fff 100644 --- a/tests/components/mcp23008/common.yaml +++ b/tests/components/mcp23008/common.yaml @@ -1,6 +1,10 @@ mcp23008: - i2c_id: i2c_bus - id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio @@ -9,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23008_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23008_binary_sensor_int + pin: + mcp23xxx: mcp23008_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/mcp23008/test.esp32-idf.yaml b/tests/components/mcp23008/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23008/test.esp32-idf.yaml +++ b/tests/components/mcp23008/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23008/test.esp8266-ard.yaml b/tests/components/mcp23008/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23008/test.esp8266-ard.yaml +++ b/tests/components/mcp23008/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23008/test.rp2040-ard.yaml b/tests/components/mcp23008/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23008/test.rp2040-ard.yaml +++ b/tests/components/mcp23008/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23017/common.yaml b/tests/components/mcp23017/common.yaml index 54a97e911fe..8b1e32600ef 100644 --- a/tests/components/mcp23017/common.yaml +++ b/tests/components/mcp23017/common.yaml @@ -1,6 +1,10 @@ mcp23017: - i2c_id: i2c_bus - id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio @@ -9,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23017_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23017_binary_sensor_int + pin: + mcp23xxx: mcp23017_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/mcp23017/test.esp32-idf.yaml b/tests/components/mcp23017/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23017/test.esp32-idf.yaml +++ b/tests/components/mcp23017/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23017/test.esp8266-ard.yaml b/tests/components/mcp23017/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23017/test.esp8266-ard.yaml +++ b/tests/components/mcp23017/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23017/test.rp2040-ard.yaml b/tests/components/mcp23017/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23017/test.rp2040-ard.yaml +++ b/tests/components/mcp23017/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23s08/common.yaml b/tests/components/mcp23s08/common.yaml index 2170ae04596..327bf02ca9b 100644 --- a/tests/components/mcp23s08/common.yaml +++ b/tests/components/mcp23s08/common.yaml @@ -2,3 +2,4 @@ mcp23s08: - id: mcp23s08_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s08/test.esp32-idf.yaml b/tests/components/mcp23s08/test.esp32-idf.yaml index a3352cf880d..eeb69416453 100644 --- a/tests/components/mcp23s08/test.esp32-idf.yaml +++ b/tests/components/mcp23s08/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml index 595f31046ab..ffc40b35953 100644 --- a/tests/components/mcp23s08/test.esp8266-ard.yaml +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s08/test.rp2040-ard.yaml b/tests/components/mcp23s08/test.rp2040-ard.yaml index 79ea6ce90b0..09b87ca3f85 100644 --- a/tests/components/mcp23s08/test.rp2040-ard.yaml +++ b/tests/components/mcp23s08/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/mcp23s17/common.yaml b/tests/components/mcp23s17/common.yaml index a89beeb16b7..150ecca325d 100644 --- a/tests/components/mcp23s17/common.yaml +++ b/tests/components/mcp23s17/common.yaml @@ -2,3 +2,4 @@ mcp23s17: - id: mcp23s17_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s17/test.esp32-idf.yaml b/tests/components/mcp23s17/test.esp32-idf.yaml index a3352cf880d..eeb69416453 100644 --- a/tests/components/mcp23s17/test.esp32-idf.yaml +++ b/tests/components/mcp23s17/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml index 595f31046ab..ffc40b35953 100644 --- a/tests/components/mcp23s17/test.esp8266-ard.yaml +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s17/test.rp2040-ard.yaml b/tests/components/mcp23s17/test.rp2040-ard.yaml index 79ea6ce90b0..09b87ca3f85 100644 --- a/tests/components/mcp23s17/test.rp2040-ard.yaml +++ b/tests/components/mcp23s17/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml index 2344622081a..77a77fa3e4f 100644 --- a/tests/components/pi4ioe5v6408/common.yaml +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -1,7 +1,11 @@ pi4ioe5v6408: - i2c_id: i2c_bus - id: pi4ioe1 - address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1 + address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1_int + address: 0x45 + interrupt_pin: ${interrupt_pin} switch: - platform: gpio @@ -16,3 +20,8 @@ binary_sensor: pin: pi4ioe5v6408: pi4ioe1 number: 1 + - platform: gpio + id: sensor1_int + pin: + pi4ioe5v6408: pi4ioe1_int + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml index 9a4779d822a..a6eb3c1cb14 100644 --- a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO21 i2c_scl: GPIO22 + interrupt_pin: GPIO15 packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml index 3429a2952a3..cd6fef30421 100644 --- a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO4 i2c_scl: GPIO5 + interrupt_pin: GPIO2 packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From ae9068a4c43e6db15daa45092bb47613382ea6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= <efilistovic@gmail.com> Date: Sun, 5 Apr 2026 22:17:12 +0300 Subject: [PATCH 1219/2056] [internal_temperature] Add support for LN882X (Lightning LN882H) (#15370) Co-authored-by: Bl00d-B0b <Bl00d-B0b@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../internal_temperature_ln882x.cpp | 24 +++++++++++++++++++ .../components/internal_temperature/sensor.py | 14 ++++++++++- .../internal_temperature/test.ln882x-ard.yaml | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 esphome/components/internal_temperature/internal_temperature_ln882x.cpp create mode 100644 tests/components/internal_temperature/test.ln882x-ard.yaml diff --git a/esphome/components/internal_temperature/internal_temperature_ln882x.cpp b/esphome/components/internal_temperature/internal_temperature_ln882x.cpp new file mode 100644 index 00000000000..621fbed0302 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_ln882x.cpp @@ -0,0 +1,24 @@ +#ifdef USE_LN882X + +#include "internal_temperature.h" + +extern "C" { +uint16_t hal_adc_get_data(uint32_t adc_base, uint32_t ch); +} + +namespace esphome::internal_temperature { + +void InternalTemperatureSensor::update() { + static constexpr uint32_t ADC_BASE = 0x40000800U; + static constexpr uint32_t ADC_CH0 = 1U; + static constexpr uint16_t ADC_MASK = 0xFFF; + static constexpr float ADC_TEMP_SCALE = 2.54f; + static constexpr float ADC_TEMP_OFFSET = 278.15f; + uint16_t raw = hal_adc_get_data(ADC_BASE, ADC_CH0); + float temperature = (raw & ADC_MASK) / ADC_TEMP_SCALE - ADC_TEMP_OFFSET; + this->publish_state(temperature); +} + +} // namespace esphome::internal_temperature + +#endif // USE_LN882X diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 6d79e086757..02730b68624 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, PLATFORM_BK72XX, PLATFORM_ESP32, + PLATFORM_LN882X, PLATFORM_NRF52, PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, @@ -30,7 +31,15 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), - cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_NRF52]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_RP2040, + PLATFORM_BK72XX, + PLATFORM_NRF52, + PLATFORM_LN882X, + ] + ), ) @@ -53,6 +62,9 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "internal_temperature_bk72xx.cpp": { PlatformFramework.BK72XX_ARDUINO, }, + "internal_temperature_ln882x.cpp": { + PlatformFramework.LN882X_ARDUINO, + }, "internal_temperature_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/tests/components/internal_temperature/test.ln882x-ard.yaml b/tests/components/internal_temperature/test.ln882x-ard.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/internal_temperature/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From c7a163441e2297c37b91c693c4a86713625a6296 Mon Sep 17 00:00:00 2001 From: Ross Tyler <rossetyler@gmail.com> Date: Sun, 5 Apr 2026 13:57:41 -0700 Subject: [PATCH 1220/2056] [ethernet] Add `interface` configuration variable for esp-idf (#10285) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/ethernet/__init__.py | 61 ++++++++++++------- .../components/ethernet/ethernet_component.h | 5 ++ .../ethernet/ethernet_component_esp32.cpp | 13 ++-- .../ethernet/test-w5500.esp32-idf.yaml | 21 ++++++- 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 17459cabb65..d9f51c677e4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -104,6 +104,8 @@ CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" CONF_PHY_REGISTERS = "phy_registers" +CONF_INTERFACE = "interface" + CONF_CLOCK_SPEED = "clock_speed" EthernetType = ethernet_ns.enum("EthernetType") @@ -191,6 +193,13 @@ CLK_MODES_DEPRECATED = { "GPIO17_OUT": ("CLK_OUT", 17), } +spi_host_device_t = cg.global_ns.enum("spi_host_device_t") + +SPI_INTERFACE_MAP = { + "spi2": spi_host_device_t.SPI2_HOST, + "spi3": spi_host_device_t.SPI3_HOST, +} + MANUAL_IP_SCHEMA = cv.Schema( { cv.Required(CONF_STATIC_IP): cv.ipv4address, @@ -225,6 +234,24 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False +def _validate_spi_interface(config: ConfigType) -> ConfigType: + """Set default SPI interface or validate user choice against the variant.""" + if not CORE.is_esp32: + return config + from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant + from esphome.components.spi import get_hw_interface_list + + has_spi3 = "spi3" in sum(get_hw_interface_list(), []) + if CONF_INTERFACE not in config: + # Only classic ESP32 defaults to spi3; all others default to spi2 + config[CONF_INTERFACE] = ( + "spi3" if get_esp32_variant() == VARIANT_ESP32 else "spi2" + ) + elif config[CONF_INTERFACE] == "spi3" and not has_spi3: + raise cv.Invalid("Interface 'spi3' is not available on this variant.") + return config + + def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: @@ -368,6 +395,10 @@ SPI_SCHEMA = cv.All( cv.frequency, cv.int_range(int(8e6), int(80e6)), ), + cv.Optional(CONF_INTERFACE): cv.All( + cv.only_on_esp32, + cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True), + ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( cv.only_on_esp32, @@ -378,6 +409,7 @@ SPI_SCHEMA = cv.All( ), ), cv.only_on([Platform.ESP32, Platform.RP2040]), + _validate_spi_interface, ) CONFIG_SCHEMA = cv.All( @@ -408,37 +440,18 @@ def _final_validate_spi(config): return # SPI interface validation is ESP32-only if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return - from esphome.components.esp32 import ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - get_esp32_variant, - ) from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface if spi_configs := fv.full_config.get().get(CONF_SPI): - variant = get_esp32_variant() - if variant in ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - ): - spi_host = "SPI2_HOST" - else: - spi_host = "SPI3_HOST" + # get_spi_interface() returns strings like "SPI2_HOST" + spi_host = f"{config[CONF_INTERFACE].upper()}_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) if interface == spi_host: raise cv.Invalid( - f"`spi` component is using interface '{interface}'. " - f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.", + f"The `ethernet` and `spi` components are both using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, change the `interface` on either `ethernet:` or `spi:`." ) @@ -528,6 +541,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") + + cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 # ENC28J60 was never built-in to IDF, so it has no Kconfig option diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c6e37d01ea2..b760ba2af76 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,6 +11,9 @@ #ifdef USE_ESP32 #include "esp_eth.h" +#ifdef USE_ETHERNET_SPI +#include "hal/spi_types.h" +#endif #include "esp_eth_mac.h" #include "esp_eth_mac_esp.h" #include "esp_netif.h" @@ -135,6 +138,7 @@ class EthernetComponent final : public Component { void set_interrupt_pin(uint8_t interrupt_pin); void set_reset_pin(uint8_t reset_pin); void set_clock_speed(int clock_speed); + void set_interface(spi_host_device_t interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void set_polling_interval(uint32_t polling_interval); #endif @@ -201,6 +205,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; + spi_host_device_t interface_{SPI3_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index a170239e032..d4585bf100b 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -158,12 +158,7 @@ void EthernetComponent::setup() { .intr_flags = 0, }; -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - auto host = SPI2_HOST; -#else - auto host = SPI3_HOST; -#endif + auto host = this->interface_; err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); @@ -458,6 +453,11 @@ void EthernetComponent::dump_config() { " MOSI Pin: %u\n" " CS Pin: %u", this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); + const char *spi_interface = "spi3"; + if (this->interface_ == SPI2_HOST) { + spi_interface = "spi2"; + } + ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT if (this->polling_interval_ != 0) { ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_); @@ -760,6 +760,7 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } #endif diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index 36f1b5365f1..f1551fef603 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -1 +1,20 @@ -<<: !include common-w5500.yaml +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + interface: spi2 + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" From f23843130e79f0d4149c3fbf8c9d994211a34705 Mon Sep 17 00:00:00 2001 From: Andrew Rankin <andrew@eiknet.com> Date: Sun, 5 Apr 2026 19:07:42 -0400 Subject: [PATCH 1221/2056] [lvgl] option to enable LVGL's built-in dark theme (#15389) --- esphome/components/lvgl/__init__.py | 10 ++++++++-- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/hello_world.yaml | 2 +- esphome/components/lvgl/styles.py | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 3b4f150699b..a9d31d42d8b 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -392,6 +392,9 @@ async def to_code(configs): } & styles_used: df.add_define("LV_COLOR_SCREEN_TRANSP", "1") + if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE): + df.add_define("LV_THEME_DEFAULT_DARK", "1") + # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending lv_image_formats = {"RGB565", "ARGB8888"} if { @@ -459,8 +462,11 @@ def add_hello_world(config): def _theme_schema(value): return cv.Schema( { - cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) - for name, w in WIDGET_TYPES.items() + cv.Optional(df.CONF_DARK_MODE, default=False): cv.boolean, + **{ + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) + for name, w in WIDGET_TYPES.items() + }, } )(value) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 500ccb608a7..668bb465151 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -598,6 +598,7 @@ CONF_FLEX_ALIGN_CROSS = "flex_align_cross" CONF_FLEX_ALIGN_TRACK = "flex_align_track" CONF_FLEX_GROW = "flex_grow" CONF_FREEZE = "freeze" +CONF_DARK_MODE = "dark_mode" CONF_FULL_REFRESH = "full_refresh" CONF_GRADIENTS = "gradients" CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index 4af179a5890..bbbd34e30a9 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -3,7 +3,7 @@ - obj: id: hello_world_card_ pad_all: 12 - bg_color: white + bg_opa: cover height: 100% width: 100% scrollable: false diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 793290de73a..c17f30383bb 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -7,7 +7,7 @@ from esphome.core import ID from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal from .helpers import add_lv_use from .lvcode import LambdaContext, lv -from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property +from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property from .types import ObjUpdateAction, lv_style_t from .widgets import collect_parts, theme_widget_map, wait_for_widgets @@ -85,7 +85,7 @@ async def style_update_to_code(config, action_id, template_arg, args): async def theme_to_code(config): if theme := config.get(CONF_THEME): add_lv_use(CONF_THEME) - for w_name, style in theme.items(): + for w_name, style in ((k, v) for k, v in theme.items() if k in WIDGET_TYPES): # 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 diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 3a6af93b640..3c5c730e6cb 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -49,6 +49,7 @@ lvgl: bg_color: 0x000000 bg_opa: cover theme: + dark_mode: true obj: border_width: 1 From f01762ea44a8b57488e080d2a52bdc05b4af484f Mon Sep 17 00:00:00 2001 From: Tomer27cz <85194189+Tomer27cz@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:17:52 +0200 Subject: [PATCH 1222/2056] [ci] move import to function (#15440) --- script/helpers.py | 4 +--- tests/script/test_helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 290dcadf0b9..c9c550d8893 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -15,8 +15,6 @@ from typing import Any import colorama -from esphome.loader import get_platform - root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -644,7 +642,7 @@ def get_all_dependencies( PLATFORM_HOST, ) from esphome.core import CORE - from esphome.loader import get_component + from esphome.loader import get_component, get_platform all_components: set[str] = set(component_names) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 28f111d7587..948aabaa669 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1037,7 +1037,7 @@ def test_get_all_dependencies_platform_component() -> None: with ( patch("esphome.loader.get_component") as mock_get_component, - patch("helpers.get_platform") as mock_get_platform, + patch("esphome.loader.get_platform") as mock_get_platform, ): mock_get_platform.return_value = platform_comp mock_get_component.return_value = None @@ -1061,7 +1061,7 @@ def test_get_all_dependencies_platform_component_with_dependencies() -> None: with ( patch("esphome.loader.get_component") as mock_get_component, - patch("helpers.get_platform") as mock_get_platform, + patch("esphome.loader.get_platform") as mock_get_platform, ): mock_get_platform.return_value = platform_comp mock_get_component.side_effect = lambda name: ( From f193bab60b17a981f1e2648c9768daa4fe806a34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:25:50 -1000 Subject: [PATCH 1223/2056] [api] Add ListEntities benchmarks for sensor, binary_sensor, and light (#15427) --- .../components/api/bench_list_entities.cpp | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 tests/benchmarks/components/api/bench_list_entities.cpp diff --git a/tests/benchmarks/components/api/bench_list_entities.cpp b/tests/benchmarks/components/api/bench_list_entities.cpp new file mode 100644 index 00000000000..02cef50d705 --- /dev/null +++ b/tests/benchmarks/components/api/bench_list_entities.cpp @@ -0,0 +1,235 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" +#include "esphome/components/light/color_mode.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// --- ListEntitiesSensorResponse --- + +static ListEntitiesSensorResponse make_sensor_response() { + ListEntitiesSensorResponse msg; + msg.object_id = StringRef::from_lit("living_room_temperature"); + msg.key = 0x12345678; + msg.name = StringRef::from_lit("Living Room Temperature"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:thermometer"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.unit_of_measurement = StringRef::from_lit("°C"); + msg.accuracy_decimals = 1; + msg.force_update = false; + msg.device_class = StringRef::from_lit("temperature"); + msg.state_class = enums::STATE_CLASS_MEASUREMENT; +#ifdef USE_DEVICES + msg.device_id = 1; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesSensorResponse); + +static void Encode_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesSensorResponse); + +static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesSensorResponse); + +// --- ListEntitiesBinarySensorResponse --- + +static ListEntitiesBinarySensorResponse make_binary_sensor_response() { + ListEntitiesBinarySensorResponse msg; + msg.object_id = StringRef::from_lit("front_door_contact"); + msg.key = 0xAABBCCDD; + msg.name = StringRef::from_lit("Front Door Contact"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:door"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.device_class = StringRef::from_lit("door"); + msg.is_status_binary_sensor = false; +#ifdef USE_DEVICES + msg.device_id = 2; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesBinarySensorResponse); + +static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesBinarySensorResponse); + +static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse); + +// --- ListEntitiesLightResponse --- + +static light::ColorModeMask light_color_modes; +static FixedVector<const char *> light_effects; + +static ListEntitiesLightResponse make_light_response() { + // Initialize static data on first call + static bool initialized = false; + if (!initialized) { + light_color_modes.insert(light::ColorMode::RGB_WHITE); + light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE); + light_effects.init(3); + light_effects.push_back("None"); + light_effects.push_back("Rainbow"); + light_effects.push_back("Strobe"); + initialized = true; + } + + ListEntitiesLightResponse msg; + msg.object_id = StringRef::from_lit("kitchen_ceiling_light"); + msg.key = 0x55667788; + msg.name = StringRef::from_lit("Kitchen Ceiling Light"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:ceiling-light"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.supported_color_modes = &light_color_modes; + msg.min_mireds = 153.0f; + msg.max_mireds = 500.0f; + msg.effects = &light_effects; +#ifdef USE_DEVICES + msg.device_id = 3; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesLightResponse); + +static void Encode_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesLightResponse); + +static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesLightResponse); + +} // namespace esphome::api::benchmarks From 83a4edbea14c4f854d3d30ab3423ae6975f8747f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:26:08 -1000 Subject: [PATCH 1224/2056] [select] [switch] Downgrade control path logging from DEBUG to VERBOSE (#15406) --- esphome/components/select/select_call.cpp | 2 +- esphome/components/switch/switch.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 0e14371d004..9d2fb725f31 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -116,7 +116,7 @@ void SelectCall::perform() { auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + ESP_LOGV(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); parent->control(idx); } diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 11840db3a31..abc7338a628 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -18,15 +18,15 @@ void Switch::control(bool target_state) { } } void Switch::turn_on() { - ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); } void Switch::turn_off() { - ESP_LOGD(TAG, "'%s' Turning OFF.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning OFF.", this->get_name().c_str()); this->write_state(this->inverted_); } void Switch::toggle() { - ESP_LOGD(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); + ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); this->write_state(this->inverted_ == this->state); } optional<bool> Switch::get_initial_state() { From 30d1230a174df2d4220ddce7b5dc47185202bced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:26:21 -1000 Subject: [PATCH 1225/2056] [button] Downgrade press logging from DEBUG to VERBOSE (#15408) --- esphome/components/button/button.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index b1c491805e0..2a2a6451325 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -16,7 +16,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o } void Button::press() { - ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Pressed.", this->get_name().c_str()); this->press_action(); this->press_callback_.call(); } From 0f2d8656adc64ece1e0f2e3ebcc85a66320e322d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:26:40 -1000 Subject: [PATCH 1226/2056] [esp32_ble] Skip dropped count memw when queue is empty (#15422) --- esphome/components/esp32_ble/ble.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 2cd2ec67f7a..68e5fffe2bc 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -399,8 +399,17 @@ void ESP32BLE::loop() { return; } +#ifdef USE_ESP32_BLE_ADVERTISING + if (this->advertising_ != nullptr) { + this->advertising_->loop(); + } +#endif + BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { + if (ble_event == nullptr) + return; + + do { switch (ble_event->type_) { #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) case BLEEvent::GATTS: { @@ -488,15 +497,11 @@ void ESP32BLE::loop() { } // Return the event to the pool this->ble_event_pool_.release(ble_event); - ble_event = this->ble_events_.pop(); - } -#ifdef USE_ESP32_BLE_ADVERTISING - if (this->advertising_ != nullptr) { - this->advertising_->loop(); - } -#endif + } while ((ble_event = this->ble_events_.pop()) != nullptr); - // Log dropped events periodically + // Log dropped events - only reachable when events were processed. + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check (saves a memw). uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); From 155657f1ccda9d6529ce07a4ffb41996e3641f7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:26:55 -1000 Subject: [PATCH 1227/2056] [mcp23xxx][pi4ioe5v6408] Disable loop when all pins are outputs (#15460) --- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 5 +++++ esphome/components/mcp23x17_base/mcp23x17_base.cpp | 5 +++++ esphome/components/mcp23xxx_base/mcp23xxx_base.h | 5 ++++- esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 10 +++++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index e4f4d50aae1..197b7392042 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -37,6 +37,11 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) { + this->enable_loop(); + } } void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 42613053de7..efed7f5f17e 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -49,6 +49,11 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) { + this->enable_loop(); + } } void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index e77eac87e74..6efd04e2463 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -35,8 +35,11 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander: this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); this->set_invalidate_on_read_(false); - this->disable_loop(); } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 8e38e7fa1d6..6e8631022a7 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -41,8 +41,11 @@ void PI4IOE5V6408Component::setup() { this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); this->set_invalidate_on_read_(false); - this->disable_loop(); } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); } void PI4IOE5V6408Component::dump_config() { @@ -67,6 +70,11 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->pull_up_down_mask_ &= ~(1 << pin); this->pull_enable_mask_ |= 1 << pin; } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } // Write GPIO to enable input mode this->write_gpio_modes_(); From ea0ce710a8cf5d93a364826f6581f10e7e8c5d5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 13:55:06 -1000 Subject: [PATCH 1228/2056] [api] Split Noise handshake state_action_ to reduce stack pressure (#15464) --- .../components/api/api_frame_helper_noise.cpp | 250 +++++++++--------- .../components/api/api_frame_helper_noise.h | 5 + 2 files changed, 136 insertions(+), 119 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 78e87793fc8..162f4ef6056 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -244,132 +244,144 @@ APIError APINoiseFrameHelper::try_read_frame_() { * If an error occurred, returns that error. Only returns OK if the transport is ready for data * traffic. */ +// Split into per-state methods so the compiler doesn't allocate stack space +// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB +// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack, +// so every byte saved in the caller matters. APIError APINoiseFrameHelper::state_action_() { - int err; - APIError aerr; - if (state_ == State::INITIALIZE) { - HELPER_LOG("Bad state for method: %d", (int) state_); - return APIError::BAD_STATE; + switch (this->state_) { + case State::INITIALIZE: + HELPER_LOG("Bad state for method: %d", (int) this->state_); + return APIError::BAD_STATE; + case State::CLIENT_HELLO: + return this->state_action_client_hello_(); + case State::SERVER_HELLO: + return this->state_action_server_hello_(); + case State::HANDSHAKE: + return this->state_action_handshake_(); + case State::CLOSED: + case State::FAILED: + return APIError::BAD_STATE; + default: + return APIError::OK; } - if (state_ == State::CLIENT_HELLO) { - // waiting for client hello - aerr = this->try_read_frame_(); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - // ignore contents, may be used in future for flags - // Resize for: existing prologue + 2 size bytes + frame data - size_t old_size = this->prologue_.size(); - size_t rx_size = this->rx_buf_.size(); - this->prologue_.resize(old_size + 2 + rx_size); - this->prologue_[old_size] = (uint8_t) (rx_size >> 8); - this->prologue_[old_size + 1] = (uint8_t) rx_size; - if (rx_size > 0) { - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); - } - - state_ = State::SERVER_HELLO; +} +APIError APINoiseFrameHelper::state_action_client_hello_() { + // waiting for client hello + APIError aerr = this->try_read_frame_(); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); } - if (state_ == State::SERVER_HELLO) { - // send server hello - const auto &name = App.get_name(); - char mac[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(mac); - - // Calculate positions and sizes - size_t name_len = name.size() + 1; // including null terminator - size_t name_offset = 1; - size_t mac_offset = name_offset + name_len; - size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE; - - // 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null) - // + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null) - constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE; - uint8_t msg[max_msg_size]; - - // chosen proto - msg[0] = 0x01; - - // node name, terminated by null byte - std::memcpy(msg + name_offset, name.c_str(), name_len); - // node mac, terminated by null byte - std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); - - aerr = write_frame_(msg, total_size); - if (aerr != APIError::OK) - return aerr; - - // start handshake - aerr = init_handshake_(); - if (aerr != APIError::OK) - return aerr; - - state_ = State::HANDSHAKE; + // ignore contents, may be used in future for flags + // Resize for: existing prologue + 2 size bytes + frame data + size_t old_size = this->prologue_.size(); + size_t rx_size = this->rx_buf_.size(); + this->prologue_.resize(old_size + 2 + rx_size); + this->prologue_[old_size] = (uint8_t) (rx_size >> 8); + this->prologue_[old_size + 1] = (uint8_t) rx_size; + if (rx_size > 0) { + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); } - if (state_ == State::HANDSHAKE) { - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE) { - // waiting for handshake msg - aerr = this->try_read_frame_(); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - if (this->rx_buf_.empty()) { - send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (this->rx_buf_[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); - send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); - err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); - if (err != 0) { - // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") - : LOG_STR("Handshake error")); - return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), - APIError::HANDSHAKESTATE_READ_FAILED); - } - - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else if (action == NOISE_ACTION_WRITE_MESSAGE) { - uint8_t buffer[65]; - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); - - err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), - APIError::HANDSHAKESTATE_WRITE_FAILED); - if (aerr_write != APIError::OK) - return aerr_write; - buffer[0] = 0x00; // success - - aerr = write_frame_(buffer, mbuf.size + 1); - if (aerr != APIError::OK) - return aerr; - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else { - // bad state for action - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - } - if (state_ == State::CLOSED || state_ == State::FAILED) { - return APIError::BAD_STATE; - } + state_ = State::SERVER_HELLO; return APIError::OK; } +APIError APINoiseFrameHelper::state_action_server_hello_() { + // send server hello + const auto &name = App.get_name(); + char mac[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac); + + // Calculate positions and sizes + size_t name_len = name.size() + 1; // including null terminator + size_t name_offset = 1; + size_t mac_offset = name_offset + name_len; + size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE; + + // 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null) + // + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null) + constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE; + uint8_t msg[max_msg_size]; + + // chosen proto + msg[0] = 0x01; + + // node name, terminated by null byte + std::memcpy(msg + name_offset, name.c_str(), name_len); + // node mac, terminated by null byte + std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); + + APIError aerr = write_frame_(msg, total_size); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + return APIError::OK; +} +APIError APINoiseFrameHelper::state_action_handshake_() { + int action = noise_handshakestate_get_action(this->handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + return this->state_action_handshake_read_(); + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + return this->state_action_handshake_write_(); + } + // bad state for action + this->state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; +} +APIError APINoiseFrameHelper::state_action_handshake_read_() { + APIError aerr = this->try_read_frame_(); + if (aerr != APIError::OK) { + return this->handle_handshake_frame_error_(aerr); + } + + if (this->rx_buf_.empty()) { + this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } else if (this->rx_buf_[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); + this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); + int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr); + if (err != 0) { + // Special handling for MAC failure + this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") + : LOG_STR("Handshake error")); + return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), + APIError::HANDSHAKESTATE_READ_FAILED); + } + + return this->check_handshake_finished_(); +} +APIError APINoiseFrameHelper::state_action_handshake_write_() { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr); + APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), + APIError::HANDSHAKESTATE_WRITE_FAILED); + if (aerr != APIError::OK) + return aerr; + buffer[0] = 0x00; // success + + aerr = this->write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + return this->check_handshake_finished_(); +} void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { // Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes uint8_t data[32]; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index a6b17ff3b92..f44bde07556 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -26,6 +26,11 @@ class APINoiseFrameHelper final : public APIFrameHelper { protected: APIError state_action_(); + APIError state_action_client_hello_(); + APIError state_action_server_hello_(); + APIError state_action_handshake_(); + APIError state_action_handshake_read_(); + APIError state_action_handshake_write_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); From 07f6be679fcfe4edf700b496de86803cce219227 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sun, 5 Apr 2026 21:20:48 -0500 Subject: [PATCH 1229/2056] [esp32] Add signed app verification without hardware secure boot (#15357) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- esphome/components/esp32/__init__.py | 104 ++++++++++++++++++ esphome/components/esp32/post_build.py.script | 96 +++++++++++++++- esphome/components/ota/ota_backend.h | 1 + .../components/ota/ota_backend_esp_idf.cpp | 8 ++ esphome/core/defines.h | 1 + esphome/espota2.py | 8 ++ tests/components/esp32/dummy_signing_key.pem | 41 +++++++ .../esp32/test-signed_ota.esp32-s3-idf.yaml | 10 ++ 8 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 tests/components/esp32/dummy_signing_key.pem create mode 100644 tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 5cae67db132..0d8a2215245 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -97,8 +97,12 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification" +CONF_SIGNING_KEY = "signing_key" +CONF_SIGNING_SCHEME = "signing_scheme" CONF_SRAM1_AS_IRAM = "sram1_as_iram" CONF_SUBTYPE = "subtype" +CONF_VERIFICATION_KEY = "verification_key" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" @@ -120,6 +124,27 @@ ASSERTION_LEVELS = { "SILENT": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT", } +SIGNING_SCHEMES = { + "rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME", + "ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME", +} + +# Chip variants that only support one signing scheme for Secure Boot V2. +# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h. +# Variants not listed in either set support both RSA and ECDSA +# (e.g. C5, C6, H2, P4). New variants should be added to the +# appropriate set if they only support one scheme. +SIGNED_OTA_RSA_ONLY_VARIANTS = { + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, +} +SIGNED_OTA_ECC_ONLY_VARIANTS = { + VARIANT_ESP32C2, + VARIANT_ESP32C61, +} + COMPILER_OPTIMIZATIONS = { "DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG", "NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE", @@ -962,6 +987,47 @@ def final_validate(config): ) # disable the rollback feature anyway since it can't be used. advanced[CONF_ENABLE_OTA_ROLLBACK] = False + if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): + scheme = signed_ota[CONF_SIGNING_SCHEME] + variant = config[CONF_VARIANT] + scheme_variant_conflicts = { + "ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"), + "rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"), + } + if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[ + 0 + ]: + errs.append( + cv.Invalid( + f"Signing scheme '{scheme}' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", + path=[ + CONF_FRAMEWORK, + CONF_ADVANCED, + CONF_SIGNED_OTA_VERIFICATION, + CONF_SIGNING_SCHEME, + ], + ) + ) + if CONF_OTA not in full_config: + _LOGGER.warning( + "Signed OTA verification is enabled but no OTA component is configured. " + "The initial firmware will be signed but OTA updates won't be possible " + "until an OTA component is added." + ) + if CONF_SIGNING_KEY in signed_ota: + _LOGGER.info( + "Signed OTA verification is enabled. Keep your signing key safe! " + "If you lose the signing key, you will NOT be able to OTA update " + "devices running firmware signed with this key. " + "Without the key, you'll need to reflash via serial." + ) + else: + _LOGGER.info( + "Signed OTA verification is configured with a public verification key. " + "Binaries will NOT be signed automatically during build. " + "You must sign them externally before flashing." + ) if errs: raise cv.MultipleInvalid(errs) @@ -1173,6 +1239,18 @@ FRAMEWORK_SCHEMA = cv.Schema( min=8192, max=32768 ), cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, + cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All( + cv.Schema( + { + cv.Optional(CONF_SIGNING_KEY): cv.file_, + cv.Optional(CONF_VERIFICATION_KEY): cv.file_, + cv.Optional( + CONF_SIGNING_SCHEME, default="rsa3072" + ): cv.one_of(*SIGNING_SCHEMES, lower=True), + } + ), + cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY), + ), cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False ): cv.boolean, @@ -1878,6 +1956,32 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True) cg.add_define("USE_OTA_ROLLBACK") + # Enable signed app verification without hardware secure boot + if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): + add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True) + add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT", True) + + scheme = signed_ota[CONF_SIGNING_SCHEME] + for key, flag in SIGNING_SCHEMES.items(): + add_idf_sdkconfig_option(flag, scheme == key) + + if CONF_SIGNING_KEY in signed_ota: + # Private key mode — auto-sign binaries during build + add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True) + add_idf_sdkconfig_option( + "CONFIG_SECURE_BOOT_SIGNING_KEY", + str(signed_ota[CONF_SIGNING_KEY].resolve()), + ) + else: + # Public key mode — verification only, external signing required + add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False) + add_idf_sdkconfig_option( + "CONFIG_SECURE_BOOT_VERIFICATION_KEY", + str(signed_ota[CONF_VERIFICATION_KEY].resolve()), + ) + + cg.add_define("USE_OTA_SIGNED_VERIFICATION") + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 5ef5860687c..8d13214259a 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -8,6 +8,99 @@ import shutil # noqa: E402 from glob import glob # noqa: E402 +def _parse_sdkconfig(sdkconfig_path): + """Parse sdkconfig file and return a dict of CONFIG_ options.""" + options = {} + try: + for line in sdkconfig_path.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + # Strip surrounding quotes from string values + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + options[key] = value + except FileNotFoundError: + pass + return options + + +def sign_firmware(source, target, env): + """ + Sign the firmware binary using espsecure.py if signed OTA verification is enabled. + Reads signing configuration from sdkconfig. + """ + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + project_dir = pathlib.Path(env.subst("$PROJECT_DIR")) + pioenv = env.subst("$PIOENV") + sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}") + + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT") != "y": + return + + if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") != "y": + print("Signed OTA verification enabled but build-time signing disabled.") + print("You must sign the firmware externally before flashing.") + return + + signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY") + if not signing_key: + print("Error: CONFIG_SECURE_BOOT_SIGNING_KEY not set in sdkconfig") + env.Exit(1) + return + + signing_key_path = pathlib.Path(signing_key) + if not signing_key_path.exists(): + print(f"Error: Signing key not found: {signing_key_path}") + env.Exit(1) + return + + # ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes), + # so the espsecure signature version is always 2. + sign_version = "2" + + firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" + firmware_path = build_dir / firmware_name + + if not firmware_path.exists(): + print(f"Error: Firmware binary not found: {firmware_path}") + env.Exit(1) + return + + python_exe = f'"{env.subst("$PYTHONEXE")}"' + unsigned_path = firmware_path.with_suffix(".unsigned.bin") + + # Keep a copy of the unsigned binary + shutil.copyfile(str(firmware_path), str(unsigned_path)) + + cmd = [ + python_exe, + "-m", + "espsecure", + "sign-data", + "--version", + sign_version, + "--keyfile", + str(signing_key_path), + "--output", + str(firmware_path), + str(unsigned_path), + ] + + print(f"Signing firmware with key: {signing_key_path.name}") + result = env.Execute( + env.VerboseAction(" ".join(cmd), "Signing firmware with espsecure") + ) + + if result == 0: + print("Successfully signed firmware") + else: + print(f"Error: espsecure sign_data failed with code {result}") + # Restore unsigned binary on failure + shutil.copyfile(str(unsigned_path), str(firmware_path)) + env.Exit(1) + + def merge_factory_bin(source, target, env): """ Merges all flash sections into a single .factory.bin using esptool. @@ -124,7 +217,8 @@ def esp32_copy_ota_bin(source, target, env): print(f"Copied firmware to {new_file_name}") -# Run merge first, then ota copy second +# Run signing first, then merge, then ota copy +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index db79370bb38..bd9c4819010 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -37,6 +37,7 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, + OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index efaf810ca30..598fce1562a 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -3,6 +3,7 @@ #include "esphome/components/md5/md5.h" #include "esphome/core/defines.h" +#include "esphome/core/log.h" #include <esp_ota_ops.h> #include <esp_task_wdt.h> @@ -10,6 +11,8 @@ namespace esphome::ota { +static const char *const TAG = "ota.idf"; + std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); } OTAResponseTypes IDFOTABackend::begin(size_t image_size) { @@ -98,7 +101,12 @@ OTAResponseTypes IDFOTABackend::end() { } } if (err == ESP_ERR_OTA_VALIDATE_FAILED) { +#ifdef USE_OTA_SIGNED_VERIFICATION + ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err); + return OTA_RESPONSE_ERROR_SIGNATURE_INVALID; +#else return OTA_RESPONSE_ERROR_UPDATE_END; +#endif } if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index faa8c6d4b0e..141f6d2be4a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -211,6 +211,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK +#define USE_OTA_SIGNED_VERIFICATION #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_SRAM1_AS_IRAM diff --git a/esphome/espota2.py b/esphome/espota2.py index c412bb51ffd..4b813e4060c 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -40,6 +40,8 @@ RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88 RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89 RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A RESPONSE_ERROR_MD5_MISMATCH = 0x8B +RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C +RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -192,6 +194,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None "Error: Application MD5 code mismatch. Please try again " "or flash over USB with a good quality cable." ) + if dat == RESPONSE_ERROR_SIGNATURE_INVALID: + raise OTAError( + "Error: Firmware signature verification failed. The firmware was not signed " + "with the correct key. Ensure the signing key matches the one used to build " + "the firmware currently running on the device." + ) if dat == RESPONSE_ERROR_UNKNOWN: raise OTAError("Unknown error from ESP") if not isinstance(expect, (list, tuple)): diff --git a/tests/components/esp32/dummy_signing_key.pem b/tests/components/esp32/dummy_signing_key.pem new file mode 100644 index 00000000000..a74231d5903 --- /dev/null +++ b/tests/components/esp32/dummy_signing_key.pem @@ -0,0 +1,41 @@ +*** DO NOT USE THIS KEY...EVER *** +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEA0J665DlxzUzzouzH96fxqXybEfFU7H1oSf2fUHwoNMgUG7Vc +SHxuFJkpsUnxg9br09/v5THOXfUj5t/Arog6FGiL7i0HXCYDMnSn2EzQWR+DY2Qj +C3YzTLvcOQ40gFjDWfzAheAMCQmc5xQeB3YmaXQf+fUWH/PfFs9Pm+L92YTv2XC1 +B2q5s8K8hUWghO472A+UMrreuDcltNJ+TbuSRHK0NQzKpKo0Vkl4HycczGDgpa8D +h68JL/BKVeJAjKxWd/xcj/FCk661ODXi0esB/mGQP3hAthWpwi+gdkWczWs1Ocr6 +VxKje1zFm9SEq+SmCViPY/Pu8Xs7steqz3b3JtRGtKQE0r3B+hBKI7aRudOZyz0s +kqoL1zYrAWmoTWBqa2tj1ACPqtr2LyHGt2aVrHRQGJf21mPYIy9GIOv+3v3GzIAK +az2B8Z93Bw1biwNZDr1SLNYfQVaJT1hQavmdlvwW8vqLUGDcQlk42yOF6nAmvAPu +Wzxf+QFEtJT6Am65AgMBAAECggGAB0d+mG+LscDtYGI4MQNGaqZLJ+NelfjjPm+v +0yhd48eWcggQPgQ/eA8HFiVRHMtPQ7+U2I+2Fm+zDr+AcuaUdjlWppsiHlxCMMzC +vYiinXV8yWdJVMFNVXBZpRECknbmbBmmYxV3/gm8lJCOYq7D9NqFMhzT5o4FGv/l +VHhlaKVblB/7ZRSbgbL6DoFpMjI42tdiUanVEyLzeR1+JDq3BhXlhVNar8ezl04t +d5LPDa+UrxtN+XpJTQeqpFgGbhImSxjzCjo0kbGiEx/DwWuFJxguIcDU25sM4g2+ +ivtn7N11U0oaqNwsz7p4cKAm8toJYxxXWZvKdj1kZvCZ+BtyH0/MtOa2Q6v91HOh +zY4KEl5wxQYnxJrgqevSm8rrC51tLOCidZ16cHba8sjrK69xysEazk43roHLFXDp +JpH7Zd8LETjFWGVfUz6vppzkt6mrJk0DNuMLk/UwpPHzW2pu1qDiHPCb9+ra1S9U +t55hT2TBFDcG/NmZZnyHQoh8METhAoHBAPIG0G8Cd4fmkvbgCRLzCIWsH6zGHS9o +80Rj9Gu93B+m/F9GtgyYuX+DKSdMdw3IJamUsBwofT2wynmkuJFhLtD1FmYtlsXf +TWp8g8CfFGrIXDvin5E3heyhvtFiOjXlw0Q8yMmQXr5LF0i3WyFCPQM20ugClB7N +CQBOVAfpVoRU1fA6UjjHibFRwi1b4bLV69QiERPCJfcny/DPkZpu7I1fiINmwzEb +O5mIFo5F4TQADEreWplXEmhEXIzDMFIwEQKBwQDcqimCcO3RSysZMQhhUfk8G19I +yRNwvi2fK5LiGCZMYjeYKqg1rBN4yCf9PTwaqBNRqXTg13Fc7zrOkSI+0oDa4FWI +/kMEztaUK+Kwd2NKc96aXHMBGF+1Sx7Ygnr9e2dyqDqRij2/qlQYY1EDz7cYldaX +YNrXcQQeNJbqydjRDYi+9bI+wDkrK/5PxE1sGmqS1RMKxoJCZmxNiQT3PmXM/oNR +Ev6N9CDklFtClWNcD0Uum+mxNJ53ldZDx4UI/CkCgcEA6R6BI3vX0FHaGureMp9f +BQoulEdbEzBeqPAyHJkKbn50Nf0xGt78RYL7X7v6LI8tH7N1Eho5z/L6g8KSeI2H +/4MiqRaeVEdrFPeMHDvd+aC1noUBt2komS2OU7XuZb3CoHZ/3A4wA9DmQ4dAwr8/ +b1oeOZVKQISzd9T6gYhSajIgwzwZuFESInaitvf6ZDxC49hQZJyr3u05NeFo2Lyh +Iuby4cZYmnMlrBN1zmImseSd8ntL/sjslPvLvVXAtFlRAoHBAIDuG5rPiOTE2sW5 +VIAoeUuZYq8QbX9uXxGlUAkyuw3eRUVvhyD1DduAd30Ljla05bTNIjFNMDtwvBd9 +zViPfiJk+RU2GspwYAfrLGSXHTifQu1GHxwAtcsjvT4b3ujEdckUakQnVbTrPH+T +Z/6mGwEOa3e/a559tj4/0/4TOc/L7J5GyILJpZ2H8uuAcww60xI/1QRywCEz3wve +hzw/BRQlkWyJgJpIjf+Af2IEDy327iExj/WuHPkaXzrzFNQPIQKBwAM6qeNOxrO3 +V91wg4+44FAsOda62fZ0GlCM7ETnEjLbFamtCKEcDNfijwTa54LcZ6yObyutD1RN +dhj4Z6QKuYnsE02agv9CtXdFEVEXaqj4pshdgVOwGK34OidT4yIJQGLrRAQ/JiGH +x6CoGUCNIAq5J08VdosLTD9qdn1zv8USCAP0ReKnRMndTzENLYz9G3nQyHgt5GzI +YoSRtrWnXrQp2Yn3epk74gFAJtKozWNV4Du35FJBjmSeMuRivonNMQ== +-----END RSA PRIVATE KEY----- +*** DO NOT USE THIS KEY...EVER *** diff --git a/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml b/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml new file mode 100644 index 00000000000..cf1a54cfa15 --- /dev/null +++ b/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml @@ -0,0 +1,10 @@ +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + signed_ota_verification: + signing_key: ../../components/esp32/dummy_signing_key.pem + signing_scheme: rsa3072 + +<<: !include common.yaml From aac74f4c947d55726129f1914e78a85f84bee375 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:48:00 -0400 Subject: [PATCH 1230/2056] [ags10] Fix wrong type passed to cg.templatable for set_zero_point mode (#15467) --- esphome/components/ags10/sensor.py | 10 +++++++--- tests/components/ags10/common.yaml | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 4cfa9e67ec1..90fe067b32c 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = { AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(AGS10Component), - cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True), + cv.Required(CONF_MODE): cv.templatable( + cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True) + ), cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t), }, ) @@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( async def ags10setzeropoint_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - mode = await cg.templatable(config.get(CONF_MODE), args, enumerate) + mode = await cg.templatable( + config.get(CONF_MODE), args, AGS10SetZeroPointActionMode + ) cg.add(var.set_mode(mode)) - value = await cg.templatable(config[CONF_VALUE], args, int) + value = await cg.templatable(config[CONF_VALUE], args, cg.uint16) cg.add(var.set_value(value)) return var diff --git a/tests/components/ags10/common.yaml b/tests/components/ags10/common.yaml index 0551871e594..8009c562955 100644 --- a/tests/components/ags10/common.yaml +++ b/tests/components/ags10/common.yaml @@ -4,3 +4,14 @@ sensor: tvoc: name: AGS10 TVOC update_interval: 60s + +button: + - platform: template + name: "Test AGS10 Actions" + on_press: + - ags10.set_zero_point: + id: ags10_1 + mode: CURRENT_VALUE + - ags10.new_i2c_address: + id: ags10_1 + address: 0x1A From 10f08e080299296db5afb8ca8a25f9d423401bca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 17:30:56 -1000 Subject: [PATCH 1231/2056] [esp8266] Add crash handler for post-mortem diagnostics (#15465) --- esphome/components/api/api_connection.h | 6 + esphome/components/esp8266/__init__.py | 1 + esphome/components/esp8266/crash_handler.cpp | 235 +++++++++++++++++++ esphome/components/esp8266/crash_handler.h | 20 ++ esphome/components/esp8266/preferences.cpp | 11 +- esphome/components/logger/logger_esp8266.cpp | 7 + esphome/core/defines.h | 1 + 7 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 esphome/components/esp8266/crash_handler.cpp create mode 100644 esphome/components/esp8266/crash_handler.h diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 13d5273ecbb..5a86240ab56 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -20,6 +20,9 @@ #ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" #endif +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -276,6 +279,9 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 2081145096e..fcd3499b151 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -233,6 +233,7 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) + cg.add_define("USE_ESP8266_CRASH_HANDLER") enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT) if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config): diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp new file mode 100644 index 00000000000..91b0cf90827 --- /dev/null +++ b/esphome/components/esp8266/crash_handler.cpp @@ -0,0 +1,235 @@ +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include <cinttypes> + +extern "C" { +#include <user_interface.h> + +// Global reset info struct populated by SDK/Arduino core at boot +extern struct rst_info resetInfo; +} + +// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01, +// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address. +static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF; +static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000; + +// ESP8266 memory map boundaries for code regions +static constexpr uint32_t IRAM_START = 0x40100000; +static constexpr uint32_t IRAM_END = 0x40108000; // 32KB + +// Linker symbols for the actual firmware IROM section. +// Using these instead of a conservative upper bound (0x40400000) prevents +// false positives from stale stack values beyond the actual flash mapping. +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration) +extern void _irom0_text_start(void); +extern void _irom0_text_end(void); +// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration) +} + +// Check if a value looks like a code address in IRAM or flash-mapped IROM. +// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but +// ensures correctness if the compiler ever chooses not to inline. +static inline bool IRAM_ATTR is_code_addr(uint32_t val) { + uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; + return (addr >= IRAM_START && addr < IRAM_END) || + (addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end); +} + +// Recover the actual code address from a windowed-ABI return address on the stack. +static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } + +// RTC user memory layout for crash backtrace data. +// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). +// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts. +// Store 16 raw candidates, filter to real return addresses at log time. +static constexpr uint8_t RTC_CRASH_BASE = 174; +static constexpr size_t MAX_BACKTRACE = 16; + +// Magic word packs sentinel, version, and count into one uint32_t: +// bits[31:16] = sentinel +// bits[15:8] = version +// bits[7:0] = backtrace count +static constexpr uint8_t CRASH_SENTINEL_BITS = 16; +static constexpr uint8_t CRASH_VERSION_BITS = 8; + +static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD; +static constexpr uint8_t CRASH_VERSION_VALUE = 1; + +static constexpr uint32_t CRASH_SENTINEL = static_cast<uint32_t>(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION = static_cast<uint32_t>(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast<uint32_t>(0xFFFF) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION_MASK = static_cast<uint32_t>(0xFF) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_COUNT_MASK = 0xFF; + +// Struct layout: 18 RTC blocks (72 bytes): +// [0] = magic (sentinel | version | count) +// [1..16] = up to 16 code addresses from stack scanning +// [17] = epc1 at crash time (to skip duplicates at log time) +struct RtcCrashData { + uint32_t magic; + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t epc1; // Fault PC, used to filter duplicates +}; +static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks"); + +namespace esphome::esp8266 { + +static const char *const TAG = "esp8266"; + +static inline bool is_crash_reason(uint32_t reason) { + return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST; +} + +bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); } + +// Xtensa exception cause names for the LX106 core (ESP8266). +// Only includes causes that can actually occur on the LX106 — it has no MMU, +// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are +// impossible and omitted. The numeric cause is always logged as fallback. +// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266). +static const LogString *get_exception_cause(uint32_t cause) { + if (cause == 0) + return LOG_STR("IllegalInst"); + if (cause == 2) + return LOG_STR("InstFetchErr"); + if (cause == 3) + return LOG_STR("LoadStoreErr"); + if (cause == 4) + return LOG_STR("Level1Int"); + if (cause == 6) + return LOG_STR("DivByZero"); + if (cause == 9) + return LOG_STR("Alignment"); + if (cause == 20) + return LOG_STR("InstFetchProhibit"); + if (cause == 28) + return LOG_STR("LoadProhibit"); + if (cause == 29) + return LOG_STR("StoreProhibit"); + return nullptr; +} + +static const LogString *get_reset_reason(uint32_t reason) { + if (reason == REASON_WDT_RST) + return LOG_STR("Hardware WDT"); + if (reason == REASON_EXCEPTION_RST) + return LOG_STR("Exception"); + if (reason == REASON_SOFT_WDT_RST) + return LOG_STR("Soft WDT"); + return LOG_STR("Unknown"); +} + +// Read backtrace from RTC user memory into caller-provided buffer. +// Returns the number of valid backtrace entries (0 if no data found). +static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { + RtcCrashData rtc_data; + if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data))) + return 0; + uint32_t magic = rtc_data.magic; + if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION) + return 0; + uint8_t raw_count = magic & CRASH_COUNT_MASK; + if (raw_count > MAX_BACKTRACE) + raw_count = MAX_BACKTRACE; + // Skip any that match epc1 (already reported as the fault PC). + // Note: we cannot verify CALL instructions at addr-3 on ESP8266 because + // reading from IROM causes LoadStoreError due to flash cache conflicts + // (the reading code and target can share a direct-mapped cache line). + // The linker-symbol IROM bounds already eliminate most false positives. + uint8_t out = 0; + for (uint8_t i = 0; i < raw_count && out < max_entries; i++) { + uint32_t addr = rtc_data.backtrace[i]; + if (addr != rtc_data.epc1) + backtrace[out++] = addr; + } + return out; +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console, making it possible to see partial output if the device +// crashes again during boot, and allowing the CLI's process_stacktrace to match +// and decode each address individually. +void crash_handler_log() { + if (!is_crash_reason(resetInfo.reason)) + return; + + // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost). + // Both resetInfo and RTC data survive until the next reset, so this can be + // called multiple times (logger init + API subscribe) with the same result. + uint32_t backtrace[MAX_BACKTRACE]; + uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific + // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match + // the Arduino core's postmortem handler behavior. + static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0; + static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d; + uint32_t exccause = resetInfo.exccause; + if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION && + (resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) { + exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO; + } + const LogString *cause = get_exception_cause(exccause); + if (cause != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), + LOG_STR_ARG(cause), exccause); + } else { + ESP_LOGE(TAG, " Reason: %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), exccause); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); + if (resetInfo.reason == REASON_EXCEPTION_RST) { + ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr); + } + for (uint8_t i = 0; i < bt_count; i++) { + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); + } +} + +} // namespace esphome::esp8266 + +// --- Custom crash callback --- +// Overrides the weak custom_crash_callback() from Arduino core's +// core_esp8266_postmortem.cpp. Called during exception handling before +// the device restarts. We scan the full stack for code addresses and store +// them in RTC user memory (which survives software reset). +extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end) { + // No zero-init — only magic, epc1, and backtrace[0..count-1] are read. + // Saves the IRAM cost of a 72-byte zero-init loop. + RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init) + uint8_t count = 0; + + // Stack pointer from the Xtensa exception frame is always 4-byte aligned. + auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr) + auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr) + uint32_t epc1 = rst_info->epc1; + + for (; scan < end && count < MAX_BACKTRACE; scan++) { + uint32_t val = *scan; + if (is_code_addr(val)) { + uint32_t addr = recover_code_addr(val); + // Skip epc1 — already reported as the fault PC + if (addr != epc1) + data.backtrace[count++] = addr; + } + } + + data.epc1 = epc1; + data.magic = CRASH_SENTINEL | CRASH_VERSION | count; + + system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data)); +} + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/crash_handler.h b/esphome/components/esp8266/crash_handler.h new file mode 100644 index 00000000000..ea3683b834e --- /dev/null +++ b/esphome/components/esp8266/crash_handler.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" + +#ifdef USE_ESP8266_CRASH_HANDLER + +namespace esphome::esp8266 { + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if the previous boot was a crash (exception, WDT, or soft WDT). +bool crash_handler_has_data(); + +} // namespace esphome::esp8266 + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 906fed2b29a..f444f035553 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -19,12 +19,13 @@ static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; -// RTC memory layout for preferences: -// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127) -// - Normal region: RTC words 32-127 (mapped from preference offset 0-95) +// RTC memory layout: +// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109) +// - Normal region: RTC words 32-109 (mapped from preference offset 0-77) +// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data) static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot -static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs -static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128 +static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs +static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110 // Maximum preference size in words (limited by uint8_t length_words field) static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index b9507e707a7..5797b03ba72 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -1,5 +1,9 @@ #ifdef USE_ESP8266 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,6 +30,9 @@ void Logger::pre_setup() { global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); +#endif } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 141f6d2be4a..d92fb6e98a9 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -331,6 +331,7 @@ // ESP8266-specific feature flags #ifdef USE_ESP8266 #define USE_ADC_SENSOR_VCC +#define USE_ESP8266_CRASH_HANDLER #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL #define USE_ESP8266_LOGGER_SERIAL From 1de94c1a8489ca1ed5cf229e6f0e41bbbb6e065d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 5 Apr 2026 18:02:06 -1000 Subject: [PATCH 1232/2056] [api] Add max_value proto option for constant-size varint codegen (#15424) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 6 +++ esphome/components/api/api_pb2.cpp | 6 +-- script/api_protobuf/api_protobuf.py | 68 ++++++++++++++++++++---- 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 96ee2fb9209..1e03675999b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1606,7 +1606,7 @@ message BluetoothLEAdvertisementResponse { message BluetoothLERawAdvertisement { uint64 address = 1 [(force) = true]; sint32 rssi = 2 [(force) = true]; - uint32 address_type = 3; + uint32 address_type = 3 [(max_value) = 4]; bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 02600f0977f..0aa9e814cf8 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -96,4 +96,10 @@ extend google.protobuf.FieldOptions { // variant of the calc_ method. Use on fields that are almost always non-default // to eliminate dead branches on hot paths. optional bool force = 50016 [default=false]; + + // max_value: Maximum value a field can have. + // When max_value < 128, the code generator emits constant-size calculations + // and direct byte writes instead of varint branching, since the encoded varint + // is guaranteed to be 1 byte. + optional uint32 max_value = 50017; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ae2cd2bae8d..f25d269e8f3 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2255,15 +2255,15 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); buffer.write_raw_byte(34); - buffer.encode_varint_raw(this->data_len); + buffer.write_raw_byte(static_cast<uint8_t>(this->data_len)); buffer.encode_raw(this->data, this->data_len); } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint64_force(1, this->address); size += ProtoSize::calc_sint32_force(1, this->rssi); - size += ProtoSize::calc_uint32(1, this->address_type); - size += ProtoSize::calc_length_force(1, this->data_len); + size += this->address_type ? 2 : 0; + size += 2 + this->data_len; return size; } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f2a11141af0..c17f16412cc 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -156,6 +156,11 @@ class TypeInfo(ABC): """Check if this field should always be encoded (skip zero/empty check).""" return get_field_opt(self._field, pb.force, False) + @property + def max_value(self) -> int | None: + """Get the max_value option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_value, None) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -235,37 +240,56 @@ class TypeInfo(ABC): "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", } + # When max_value < 128, the varint is always 1 byte — use a direct byte write + RAW_ENCODE_SMALL_MAP: dict[str, str] = { + "encode_uint32": "buffer.write_raw_byte(static_cast<uint8_t>({value}));", + "encode_uint64": "buffer.write_raw_byte(static_cast<uint8_t>({value}));", + } + def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: """Try to emit a precomputed-tag encode for a forced field. Returns the raw encode string if the tag is a single byte and the encode_func has a known raw equivalent, or None otherwise. + When max_value < 128, uses direct byte write instead of varint encoding. """ if not self.force: return None tag = self.calculate_tag() if tag >= 128: return None - raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + max_val = self.max_value + raw_expr = None + if max_val is not None and max_val < 128: + raw_expr = self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) + if raw_expr is None: + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" def _encode_bytes_with_precomputed_tag( - self, data_expr: str, len_expr: str + self, data_expr: str, len_expr: str, max_len: int | None = None ) -> str | None: """Try to emit a precomputed-tag encode for a forced bytes/string field. Returns the raw encode string if the tag is a single byte, or None. + When max_len < 128, uses direct byte write for the length varint. """ if not self.force: return None tag = self.calculate_tag() if tag >= 128: return None + # When max_len < 128, length varint is always 1 byte + len_encode = ( + f"buffer.write_raw_byte(static_cast<uint8_t>({len_expr}));" + if max_len is not None and max_len < 128 + else f"buffer.encode_varint_raw({len_expr});" + ) return ( f"buffer.write_raw_byte({tag});\n" - f"buffer.encode_varint_raw({len_expr});\n" + f"{len_encode}\n" f"buffer.encode_raw({data_expr}, {len_expr});" ) @@ -346,6 +370,25 @@ class TypeInfo(ABC): value = value_expr or name return f"size += ProtoSize::{method}({field_id_size}, {value});" + def _get_single_byte_varint_size( + self, name: str, force: bool, extra_expr: str | None = None + ) -> str: + """Size calculation when the varint is guaranteed to be 1 byte. + + Used when max_value < 128 or fixed_array_size < 128. + The fixed part is field_id_size + 1 (tag + 1-byte varint). + + Args: + name: Expression to check for zero (non-force only) + force: Whether to skip the zero check + extra_expr: Additional variable expression to add (e.g., data length) + """ + fixed = self.calculate_field_id_size() + 1 + size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed) + if force: + return f"size += {size_expr};" + return f"size += {name} ? {size_expr} : 0;" + @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -1191,8 +1234,9 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + max_len = self.array_size if isinstance(self.array_size, int) else None if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}", f"this->{self.field_name}_len" + f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len ): return result if self.force: @@ -1212,13 +1256,14 @@ class FixedArrayBytesType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # Use the actual length stored in the _len field length_field = f"this->{self.field_name}_len" - field_id_size = self.calculate_field_id_size() - if force: - # For repeated fields, always calculate size (no zero check) - return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" - # For non-repeated fields, length already checks for zero - return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" + # When array_size < 128, length varint is always 1 byte + if isinstance(self.array_size, int) and self.array_size < 128: + return self._get_single_byte_varint_size( + length_field, force, extra_expr=length_field + ) + + return self._get_simple_size_calculation(length_field, force, "length") def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -1245,6 +1290,9 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: + max_val = self.max_value + if max_val is not None and max_val < 128: + return self._get_single_byte_varint_size(name, force) return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: From 7644f17cf61f08a36304be3309dd26e229c1312d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:05:04 -0400 Subject: [PATCH 1233/2056] [at581x] Fix codegen crash when using lambdas for frequency/time/power (#15468) --- esphome/components/at581x/__init__.py | 32 ++++++++++----------------- tests/components/at581x/common.yaml | 5 +++++ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 34e65706288..4923491f0c2 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -173,8 +173,10 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): - template_ = await cg.templatable(freq, args, float) - template_ = int(template_ / 1000000) + if cg.is_template(freq): + template_ = await cg.templatable(freq, args, cg.int32) + else: + template_ = int(freq / 1000000) cg.add(var.set_frequency(template_)) if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: @@ -182,31 +184,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_sensing_distance(template_)) if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): - template_ = await cg.templatable(selfcheck, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(selfcheck, args, cg.int32) cg.add(var.set_poweron_selfcheck_time(template_)) if protect := config.get(CONF_PROTECT_TIME): - template_ = await cg.templatable(protect, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(protect, args, cg.int32) cg.add(var.set_protect_time(template_)) if trig_base := config.get(CONF_TRIGGER_BASE): - template_ = await cg.templatable(trig_base, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(trig_base, args, cg.int32) cg.add(var.set_trigger_base(template_)) if trig_keep := config.get(CONF_TRIGGER_KEEP): - template_ = await cg.templatable(trig_keep, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(trig_keep, args, cg.int32) cg.add(var.set_trigger_keep(template_)) if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: @@ -214,8 +204,10 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_stage_gain(template_)) if power := config.get(CONF_POWER_CONSUMPTION): - template_ = await cg.templatable(power, args, float) - template_ = int(template_ * 1000000) + if cg.is_template(power): + template_ = await cg.templatable(power, args, cg.int32) + else: + template_ = int(power * 1000000) cg.add(var.set_power_consumption(template_)) return var diff --git a/tests/components/at581x/common.yaml b/tests/components/at581x/common.yaml index 425be47c423..dfb3a9a05eb 100644 --- a/tests/components/at581x/common.yaml +++ b/tests/components/at581x/common.yaml @@ -12,6 +12,11 @@ esphome: trigger_keep: 10s stage_gain: 3 power_consumption: 70uA + - at581x.settings: + id: waveradar + frequency: !lambda "return 5800;" + poweron_selfcheck_time: !lambda "return 2000;" + power_consumption: !lambda "return 70;" - at581x.reset: id: waveradar From 859ea23bdefdb8dd209c071394d32f12f4779fae Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Mon, 6 Apr 2026 06:33:02 +0200 Subject: [PATCH 1234/2056] [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 4) (#15462) --- .../components/mitsubishi_cn105/climate.py | 16 +- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 225 +++++++++++------- .../mitsubishi_cn105/mitsubishi_cn105.h | 63 ++++- .../mitsubishi_cn105_climate.cpp | 78 +++++- .../mitsubishi_cn105_climate.h | 1 + .../climate/mitsubishi_cn105_tests.cpp | 12 +- 6 files changed, 291 insertions(+), 104 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/climate.py b/esphome/components/mitsubishi_cn105/climate.py index 5ea72d4cd22..7fa6825ea60 100644 --- a/esphome/components/mitsubishi_cn105/climate.py +++ b/esphome/components/mitsubishi_cn105/climate.py @@ -8,6 +8,8 @@ DEPENDENCIES = ["uart"] AUTO_LOAD = ["climate"] CODEOWNERS = ["@crnjan"] +CONF_CURRENT_TEMPERATURE_MIN_INTERVAL = "current_temperature_min_interval" + mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105") MitsubishiCN105Climate = mitsubishi_ns.class_( @@ -20,7 +22,14 @@ MitsubishiCN105Climate = mitsubishi_ns.class_( CONFIG_SCHEMA = ( climate.climate_schema(MitsubishiCN105Climate) .extend(uart.UART_DEVICE_SCHEMA) - .extend({cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval}) + .extend( + { + cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, + cv.Optional( + CONF_CURRENT_TEMPERATURE_MIN_INTERVAL, default="60s" + ): cv.update_interval, + } + ) ) FINAL_VALIDATE_SCHEMA = cv.All( @@ -39,3 +48,8 @@ async def to_code(config: ConfigType) -> None: var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) + cg.add( + var.set_current_temperature_min_interval( + config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL] + ) + ) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 0bce8da1ad4..3b29f66bf1f 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -22,7 +22,33 @@ static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42; static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62; static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02; static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; -static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP}; + +static constexpr std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = { + std::nullopt, // 0x00 + MitsubishiCN105::Mode::HEAT, // 0x01 + MitsubishiCN105::Mode::DRY, // 0x02 + MitsubishiCN105::Mode::COOL, // 0x03 + std::nullopt, // 0x04 + std::nullopt, // 0x05 + std::nullopt, // 0x06 + MitsubishiCN105::Mode::FAN_ONLY, // 0x07 + MitsubishiCN105::Mode::AUTO // 0x08 +}; + +static constexpr std::array<std::optional<MitsubishiCN105::FanMode>, 7> PROTOCOL_FAN_MODE_MAP = { + MitsubishiCN105::FanMode::AUTO, // 0x00 + MitsubishiCN105::FanMode::QUIET, // 0x01 + MitsubishiCN105::FanMode::SPEED_1, // 0x02 + MitsubishiCN105::FanMode::SPEED_2, // 0x03 + std::nullopt, // 0x04 + MitsubishiCN105::FanMode::SPEED_3, // 0x05 + MitsubishiCN105::FanMode::SPEED_4 // 0x06 +}; + +template<typename T, size_t N> +static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> &table, uint8_t value) { + return (value < N) ? table[value] : std::nullopt; +} static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); @@ -54,12 +80,14 @@ bool MitsubishiCN105::update() { if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { this->write_timeout_start_ms_.reset(); - this->read_pos_ = 0; + this->frame_parser_.reset(); this->set_state_(State::READ_TIMEOUT); return false; } - return this->read_incoming_bytes_(); + return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) { + return this->process_rx_packet_(type, payload, len); + }); } void MitsubishiCN105::set_state_(State new_state) { @@ -111,7 +139,7 @@ void MitsubishiCN105::did_transition_(State to) { case State::CONNECTED: this->write_timeout_start_ms_.reset(); - this->status_msg_index_ = 0; + this->current_status_msg_type_ = STATUS_MSG_SETTINGS; this->set_state_(State::UPDATING_STATUS); break; @@ -121,10 +149,8 @@ void MitsubishiCN105::did_transition_(State to) { case State::STATUS_UPDATED: { this->write_timeout_start_ms_.reset(); - if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) { - this->status_msg_index_ = 0; - } - if (this->status_msg_index_ != 0) { + if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) { + this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP; this->set_state_(State::UPDATING_STATUS); } else { this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); @@ -134,6 +160,7 @@ void MitsubishiCN105::did_transition_(State to) { case State::SCHEDULE_NEXT_STATUS_UPDATE: this->status_update_start_ms_ = get_loop_time_ms(); + this->current_status_msg_type_ = STATUS_MSG_SETTINGS; this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); break; @@ -146,15 +173,26 @@ void MitsubishiCN105::did_transition_(State to) { } } +bool MitsubishiCN105::should_request_room_temperature_() const { + if (!this->is_room_temperature_enabled()) { + return false; + } + + if (!this->last_room_temperature_update_ms_.has_value()) { + return true; + } + + return (get_loop_time_ms() - *this->last_room_temperature_update_ms_) >= this->room_temperature_min_interval_ms_; +} + void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { - dump_buffer_vv("TX", packet, len); + FrameParser::dump_buffer_vv("TX", packet, len); this->device_.write_array(packet, len); this->write_timeout_start_ms_ = get_loop_time_ms(); } void MitsubishiCN105::update_status_() { - ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_); - std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]}; + std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {this->current_status_msg_type_}; this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload)); } @@ -163,67 +201,6 @@ void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) { this->set_state_(state); } -bool MitsubishiCN105::read_incoming_bytes_() { - uint8_t watchdog = 64; - while (this->device_.available() > 0 && watchdog-- > 0) { - uint8_t &value = this->read_buffer_[this->read_pos_]; - if (!this->device_.read_byte(&value)) { - ESP_LOGW(TAG, "UART read failed while data available"); - return false; - } - - switch (++this->read_pos_) { - case 1: - if (value != PREAMBLE) { - this->reset_read_position_and_dump_buffer_("RX ignoring preamble"); - } - continue; - - case 2: - continue; - - case 3: - if (value != HEADER_BYTE_1) { - this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch"); - } - continue; - - case 4: - if (value != HEADER_BYTE_2) { - this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch"); - } - continue; - - case HEADER_LEN: - static_assert(READ_BUFFER_SIZE > HEADER_LEN); - if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) { - this->reset_read_position_and_dump_buffer_("RX invalid: payload too large"); - } - continue; - - default: - break; - } - - const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]); - if (this->read_pos_ <= len_without_checksum) { - continue; - } - - if (checksum(this->read_buffer_, len_without_checksum) != value) { - this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch"); - continue; - } - - bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, - len_without_checksum - HEADER_LEN); - this->reset_read_position_and_dump_buffer_("RX"); - return processed; - } - - return false; -} - bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { switch (type) { case PACKET_TYPE_CONNECT_RESPONSE: @@ -251,11 +228,19 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) return false; } - if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) { + if (msg_type == this->current_status_msg_type_) { this->set_state_(State::STATUS_UPDATED); } - return previous != this->status_ && this->is_status_initialized(); + bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode || + previous.fan_mode != this->status_.fan_mode || + previous.target_temperature != this->status_.target_temperature; + + if (this->is_room_temperature_enabled()) { + changed |= previous.room_temperature != this->status_.room_temperature; + } + + return changed && this->is_status_initialized(); } bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) { @@ -278,6 +263,9 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) return false; } + const bool i_see = payload[3] > 0x08; + this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); + this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); this->status_.power_on = payload[2] != 0; this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31); @@ -291,21 +279,11 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz } this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10); + this->last_room_temperature_update_ms_ = get_loop_time_ms(); + return true; } -void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) { - dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); - this->read_pos_ = 0; -} - -void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char buf[format_hex_pretty_size(READ_BUFFER_SIZE)]; - ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len)); -#endif -} - const LogString *MitsubishiCN105::state_to_string(State state) { switch (state) { case State::NOT_CONNECTED: @@ -328,4 +306,79 @@ const LogString *MitsubishiCN105::state_to_string(State state) { return LOG_STR("Unknown"); } +template<typename Callback> +bool MitsubishiCN105::FrameParser::read_and_parse(uart::UARTDevice &device, Callback &&callback) { + uint8_t watchdog = 64; + while (device.available() > 0 && watchdog-- > 0) { + uint8_t &value = this->read_buffer_[this->read_pos_]; + if (!device.read_byte(&value)) { + ESP_LOGW(TAG, "UART read failed while data available"); + return false; + } + + switch (++this->read_pos_) { + case 1: + if (value != PREAMBLE) { + this->reset_and_dump_buffer_("RX ignoring preamble"); + } + continue; + + case 2: + continue; + + case 3: + if (value != HEADER_BYTE_1) { + this->reset_and_dump_buffer_("RX invalid: header 1 mismatch"); + } + continue; + + case 4: + if (value != HEADER_BYTE_2) { + this->reset_and_dump_buffer_("RX invalid: header 2 mismatch"); + } + continue; + + case HEADER_LEN: + static_assert(READ_BUFFER_SIZE > HEADER_LEN); + if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) { + this->reset_and_dump_buffer_("RX invalid: payload too large"); + } + continue; + + default: + break; + } + + const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]); + if (this->read_pos_ <= len_without_checksum) { + continue; + } + + if (checksum(this->read_buffer_, len_without_checksum) != value) { + this->reset_and_dump_buffer_("RX invalid: checksum mismatch"); + continue; + } + + dump_buffer_vv("RX", this->read_buffer_, this->read_pos_); + const bool processed = + callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN); + this->read_pos_ = 0; + return processed; + } + + return false; +} + +void MitsubishiCN105::FrameParser::reset_and_dump_buffer_(const char *prefix) { + dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); + this->read_pos_ = 0; +} + +void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char buf[format_hex_pretty_size(READ_BUFFER_SIZE)]; + ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len)); +#endif +} + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index d43904b3133..6a29763696f 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -9,11 +9,30 @@ uint32_t get_loop_time_ms(); class MitsubishiCN105 { public: - struct Status { - bool operator==(const Status &) const = default; + enum class Mode : uint8_t { + HEAT, + DRY, + COOL, + FAN_ONLY, + AUTO, + UNKNOWN, + }; + enum class FanMode : uint8_t { + AUTO, + QUIET, + SPEED_1, + SPEED_2, + SPEED_3, + SPEED_4, + UNKNOWN, + }; + + struct Status { bool power_on{false}; float target_temperature{NAN}; + Mode mode{Mode::UNKNOWN}; + FanMode fan_mode{FanMode::UNKNOWN}; float room_temperature{NAN}; }; @@ -25,8 +44,17 @@ class MitsubishiCN105 { uint32_t get_update_interval() const { return this->update_interval_ms_; } void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } + uint32_t get_room_temperature_min_interval() const { return this->room_temperature_min_interval_ms_; } + bool is_room_temperature_enabled() const { return this->room_temperature_min_interval_ms_ != SCHEDULER_DONT_RUN; } + void set_room_temperature_min_interval(uint32_t interval_ms) { + this->room_temperature_min_interval_ms_ = interval_ms; + } + const Status &status() const { return this->status_; } - bool is_status_initialized() const { return !std::isnan(status_.room_temperature); } + bool is_status_initialized() const { + return this->is_room_temperature_enabled() ? !std::isnan(this->status_.room_temperature) + : !std::isnan(this->status_.target_temperature); + } protected: enum class State : uint8_t { @@ -40,35 +68,46 @@ class MitsubishiCN105 { READ_TIMEOUT }; + class FrameParser { + public: + template<typename Callback> bool read_and_parse(uart::UARTDevice &device, Callback &&callback); + void reset() { read_pos_ = 0; } + static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len); + + protected: + void reset_and_dump_buffer_(const char *prefix); + + private: + static constexpr size_t READ_BUFFER_SIZE = 32; + uint8_t read_buffer_[READ_BUFFER_SIZE]; + uint8_t read_pos_{0}; + }; + void set_state_(State new_state); void did_transition_(State to); - bool read_incoming_bytes_(); bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); bool process_status_packet_(const uint8_t *payload, size_t len); bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len); bool parse_status_settings_(const uint8_t *payload, size_t len); bool parse_status_room_temperature_(const uint8_t *payload, size_t len); - void reset_read_position_and_dump_buffer_(const char *prefix); void send_packet_(const uint8_t *packet, size_t len); void update_status_(); void cancel_waiting_and_transition_to_(State state); + bool should_request_room_temperature_() const; template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); static const LogString *state_to_string(State state); - static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len); uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; + uint32_t room_temperature_min_interval_ms_{60000}; std::optional<uint32_t> write_timeout_start_ms_; std::optional<uint32_t> status_update_start_ms_; + std::optional<uint32_t> last_room_temperature_update_ms_; Status status_{}; State state_{State::NOT_CONNECTED}; - uint8_t status_msg_index_{0}; - - private: - static constexpr size_t READ_BUFFER_SIZE = 32; - uint8_t read_buffer_[READ_BUFFER_SIZE]; - uint8_t read_pos_{0}; + uint8_t current_status_msg_type_{0}; + FrameParser frame_parser_; }; } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 55fc23c4499..21ce272993a 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -6,8 +6,42 @@ namespace esphome::mitsubishi_cn105 { static const char *const TAG = "mitsubishi_cn105.climate"; +static constexpr std::array MODE_MAP{ + std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO}, + std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT}, + std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY}, + std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL}, + std::pair{MitsubishiCN105::Mode::FAN_ONLY, climate::CLIMATE_MODE_FAN_ONLY}, +}; + +static constexpr std::array FAN_MODE_MAP{ + std::pair{MitsubishiCN105::FanMode::AUTO, climate::CLIMATE_FAN_AUTO}, + std::pair{MitsubishiCN105::FanMode::QUIET, climate::CLIMATE_FAN_QUIET}, + std::pair{MitsubishiCN105::FanMode::SPEED_1, climate::CLIMATE_FAN_LOW}, + std::pair{MitsubishiCN105::FanMode::SPEED_2, climate::CLIMATE_FAN_MEDIUM}, + std::pair{MitsubishiCN105::FanMode::SPEED_3, climate::CLIMATE_FAN_MIDDLE}, + std::pair{MitsubishiCN105::FanMode::SPEED_4, climate::CLIMATE_FAN_HIGH}, +}; + +template<typename A, typename B, std::size_t N> +static bool map_lookup(const std::array<std::pair<A, B>, N> &map, A key, B &out) { + for (const auto &[from, to] : map) { + if (from == key) { + out = to; + return true; + } + } + return false; +} + void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); + if (this->hp_.is_room_temperature_enabled()) { + ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms", + this->hp_.get_room_temperature_min_interval()); + } else { + ESP_LOGCONFIG(TAG, " Current temperature: disabled"); + } ESP_LOGCONFIG(TAG, " Update interval: %" PRIu32 " ms\n" " UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u", @@ -26,12 +60,32 @@ void MitsubishiCN105Climate::loop() { climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; - traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + climate::CLIMATE_MODE_AUTO, + }); + + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_QUIET, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_HIGH, + }); traits.set_visual_min_temperature(16.0f); traits.set_visual_max_temperature(31.0f); traits.set_visual_temperature_step(1.0f); - traits.set_visual_current_temperature_step(0.5f); + + if (this->hp_.is_room_temperature_enabled()) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + traits.set_visual_current_temperature_step(0.5f); + } return traits; } @@ -42,7 +96,25 @@ void MitsubishiCN105Climate::apply_values_() { const auto &status = this->hp_.status(); this->target_temperature = status.target_temperature; - this->current_temperature = status.room_temperature; + + if (this->hp_.is_room_temperature_enabled()) { + this->current_temperature = status.room_temperature; + } + + if (status.power_on) { + if (!map_lookup(MODE_MAP, status.mode, this->mode)) { + ESP_LOGD(TAG, "Unable to map mode"); + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + + climate::ClimateFanMode fan_mode; + if (map_lookup(FAN_MODE_MAP, status.fan_mode, fan_mode)) { + this->fan_mode = fan_mode; + } else { + ESP_LOGD(TAG, "Unable to map fan mode"); + } this->publish_state(); } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h index da8f8d8d0af..eee4c209669 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -19,6 +19,7 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public void control(const climate::ClimateCall &call) override; void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } + void set_current_temperature_min_interval(uint32_t ms) { hp_.set_room_temperature_min_interval(ms); } protected: void apply_values_(); diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index 5b4f84623ef..f26b0d82b62 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -60,6 +60,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { // Settings should still have initial values EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); + EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::UNKNOWN); ctx.sut.set_current_time(300); ASSERT_FALSE(ctx.sut.update()); @@ -68,6 +70,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { // Check settings that we just read from received package EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); + EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::AUTO); + EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::AUTO); // Now fetch room temperature (0x03) EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); @@ -260,24 +264,28 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { auto ctx = TestContext{}; ctx.uart.push_rx( - {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56}); + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55}); ctx.sut.update(); EXPECT_TRUE(ctx.sut.status().power_on); EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f); + EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::COOL); + EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::QUIET); } TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { auto ctx = TestContext{}; ctx.uart.push_rx( - {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7}); + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD}); ctx.sut.update(); EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f); + EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::FAN_ONLY); + EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::SPEED_4); } TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) { From 1c97954b4734781097ca1aced2dbf3c1e1ec6887 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:42:17 -1000 Subject: [PATCH 1235/2056] Bump aioesphomeapi from 44.9.0 to 44.9.1 (#15470) 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 9c63cdee27b..7e6a2fe4b58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260210.0 -aioesphomeapi==44.9.0 +aioesphomeapi==44.9.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 2f2b7e42ba1dca11bde1f07fa9e52b175d07b85f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:15:02 -1000 Subject: [PATCH 1236/2056] Bump aioesphomeapi from 44.9.1 to 44.11.1 (#15471) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7e6a2fe4b58..db4a2b19e03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260210.0 -aioesphomeapi==44.9.1 +aioesphomeapi==44.11.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 02185fb4f4b227319c5408a981f0f68f1a5f8d57 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Mon, 6 Apr 2026 21:59:18 +0200 Subject: [PATCH 1237/2056] [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 5) (#15483) --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 150 ++++++++++++++++-- .../mitsubishi_cn105/mitsubishi_cn105.h | 27 ++++ .../mitsubishi_cn105_climate.cpp | 39 ++++- .../climate/mitsubishi_cn105_tests.cpp | 101 +++++++++++- tests/components/mitsubishi_cn105/common.h | 2 + 5 files changed, 298 insertions(+), 21 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 3b29f66bf1f..1a354956183 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -1,4 +1,5 @@ #include <array> +#include <cmath> #include <numeric> #include "mitsubishi_cn105.h" @@ -8,6 +9,8 @@ static const char *const TAG = "mitsubishi_cn105.driver"; static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; +static constexpr uint8_t TARGET_TEMPERATURE_ENC_A_OFFSET = 31; + static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10; static constexpr size_t HEADER_LEN = 5; static constexpr uint8_t PREAMBLE = 0xFC; @@ -23,6 +26,9 @@ static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62; static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02; static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; +static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41; +static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61; + static constexpr std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = { std::nullopt, // 0x00 MitsubishiCN105::Mode::HEAT, // 0x01 @@ -50,6 +56,18 @@ static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> & return (value < N) ? table[value] : std::nullopt; } +template<typename T, size_t N> +static constexpr bool reverse_lookup(const std::array<std::optional<T>, N> &table, T value, uint8_t &placeholder) { + for (size_t i = 0; i < N; ++i) { + const auto &table_value = table[i]; + if (table_value.has_value() && table_value == value) { + placeholder = i; + return true; + } + } + return false; +} + static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); } @@ -72,10 +90,16 @@ static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } bool MitsubishiCN105::update() { - if (const auto start = this->status_update_start_ms_; - start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) { - this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); - return false; + if (const auto start = this->status_update_start_ms_) { + if (this->pending_updates_.any()) { + this->cancel_waiting_and_transition_to_(State::APPLYING_SETTINGS); + return false; + } + + if ((get_loop_time_ms() - *start) >= this->update_interval_ms_) { + this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); + return false; + } } if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { @@ -118,13 +142,19 @@ bool MitsubishiCN105::should_transition(State from, State to) { return from == State::UPDATING_STATUS; case State::SCHEDULE_NEXT_STATUS_UPDATE: - return from == State::STATUS_UPDATED; + return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED; case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: return from == State::SCHEDULE_NEXT_STATUS_UPDATE; + case State::APPLYING_SETTINGS: + return from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE || from == State::STATUS_UPDATED; + + case State::SETTINGS_APPLIED: + return from == State::APPLYING_SETTINGS; + case State::READ_TIMEOUT: - return from == State::UPDATING_STATUS || from == State::CONNECTING; + return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING; default: return false; @@ -149,7 +179,9 @@ void MitsubishiCN105::did_transition_(State to) { case State::STATUS_UPDATED: { this->write_timeout_start_ms_.reset(); - if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) { + if (this->pending_updates_.any() && this->is_status_initialized()) { + this->set_state_(State::APPLYING_SETTINGS); + } else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) { this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP; this->set_state_(State::UPDATING_STATUS); } else { @@ -164,6 +196,16 @@ void MitsubishiCN105::did_transition_(State to) { this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); break; + case State::APPLYING_SETTINGS: + this->apply_settings_(); + this->pending_updates_.clear(); + break; + + case State::SETTINGS_APPLIED: + this->write_timeout_start_ms_.reset(); + this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); + break; + case State::READ_TIMEOUT: this->set_state_(State::CONNECTING); break; @@ -210,6 +252,10 @@ bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, s case PACKET_TYPE_STATUS_RESPONSE: return this->process_status_packet_(payload, len); + case PACKET_TYPE_WRITE_SETTINGS_RESPONSE: + this->set_state_(State::SETTINGS_APPLIED); + return false; + default: ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type); return false; @@ -263,11 +309,23 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) return false; } - const bool i_see = payload[3] > 0x08; - this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); - this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); - this->status_.power_on = payload[2] != 0; - this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31); + if (!this->pending_updates_.has(UpdateFlag::POWER)) { + this->status_.power_on = payload[2] != 0; + } + + this->use_temperature_encoding_b_ = payload[10] != 0; + if (!this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { + this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET); + } + + if (!this->pending_updates_.has(UpdateFlag::MODE)) { + const bool i_see = payload[3] > 0x08; + this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); + } + + if (!this->pending_updates_.has(UpdateFlag::FAN)) { + this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); + } return true; } @@ -284,6 +342,70 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz return true; } +void MitsubishiCN105::set_power(bool power_on) { + this->status_.power_on = power_on; + this->pending_updates_.set(UpdateFlag::POWER); +} + +void MitsubishiCN105::set_target_temperature(float target_temperature) { + if (target_temperature < 16 || target_temperature > 31) { + ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature); + return; + } + this->status_.target_temperature = std::round(target_temperature); + this->pending_updates_.set(UpdateFlag::TEMPERATURE); +} + +void MitsubishiCN105::set_mode(Mode mode) { + uint8_t placeholder; + if (!reverse_lookup(PROTOCOL_MODE_MAP, mode, placeholder)) { + ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode)); + return; + } + this->status_.mode = mode; + this->pending_updates_.set(UpdateFlag::MODE); +} + +void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { + uint8_t placeholder; + if (!reverse_lookup(PROTOCOL_FAN_MODE_MAP, fan_mode, placeholder)) { + ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode)); + return; + } + this->status_.fan_mode = fan_mode; + this->pending_updates_.set(UpdateFlag::FAN); +} + +void MitsubishiCN105::apply_settings_() { + std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {0x01}; + + if (this->pending_updates_.has(UpdateFlag::POWER)) { + payload[1] |= 0x01; + payload[3] = this->status_.power_on ? 0x01 : 0x00; + } + + if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { + payload[1] |= 0x04; + if (this->use_temperature_encoding_b_) { + payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f); + } else { + payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature); + } + } + + if (this->pending_updates_.has(UpdateFlag::MODE) && + reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) { + payload[1] |= 0x02; + } + + if (this->pending_updates_.has(UpdateFlag::FAN) && + reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) { + payload[1] |= 0x08; + } + + this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload)); +} + const LogString *MitsubishiCN105::state_to_string(State state) { switch (state) { case State::NOT_CONNECTED: @@ -300,6 +422,10 @@ const LogString *MitsubishiCN105::state_to_string(State state) { return LOG_STR("ScheduleNextStatusUpdate"); case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: return LOG_STR("WaitingForScheduledStatusUpdate"); + case State::APPLYING_SETTINGS: + return LOG_STR("ApplyingSettings"); + case State::SETTINGS_APPLIED: + return LOG_STR("SettingsApplied"); case State::READ_TIMEOUT: return LOG_STR("ReadTimeout"); } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 6a29763696f..68d98bf6d9d 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -56,6 +56,11 @@ class MitsubishiCN105 { : !std::isnan(this->status_.target_temperature); } + void set_power(bool power_on); + void set_target_temperature(float target_temperature); + void set_mode(Mode mode); + void set_fan_mode(FanMode fan_mode); + protected: enum class State : uint8_t { NOT_CONNECTED, @@ -65,6 +70,8 @@ class MitsubishiCN105 { STATUS_UPDATED, SCHEDULE_NEXT_STATUS_UPDATE, WAITING_FOR_SCHEDULED_STATUS_UPDATE, + APPLYING_SETTINGS, + SETTINGS_APPLIED, READ_TIMEOUT }; @@ -83,6 +90,23 @@ class MitsubishiCN105 { uint8_t read_pos_{0}; }; + enum class UpdateFlag : uint8_t { + TEMPERATURE = 1 << 0, + POWER = 1 << 1, + MODE = 1 << 2, + FAN = 1 << 3, + }; + + struct UpdateFlags { + void set(UpdateFlag f) { flags_ |= static_cast<uint8_t>(f); } + void clear() { flags_ = 0; } + bool any() const { return flags_ != 0; } + bool has(UpdateFlag f) const { return (flags_ & static_cast<uint8_t>(f)) != 0; } + + protected: + uint8_t flags_{0}; + }; + void set_state_(State new_state); void did_transition_(State to); bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); @@ -94,6 +118,7 @@ class MitsubishiCN105 { void update_status_(); void cancel_waiting_and_transition_to_(State state); bool should_request_room_temperature_() const; + void apply_settings_(); template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); static const LogString *state_to_string(State state); @@ -106,6 +131,8 @@ class MitsubishiCN105 { std::optional<uint32_t> last_room_temperature_update_ms_; Status status_{}; State state_{State::NOT_CONNECTED}; + UpdateFlags pending_updates_; + bool use_temperature_encoding_b_{false}; uint8_t current_status_msg_type_{0}; FrameParser frame_parser_; }; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 21ce272993a..40ddb88a795 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -34,6 +34,22 @@ static bool map_lookup(const std::array<std::pair<A, B>, N> &map, A key, B &out) return false; } +template<typename Left, typename Right, std::size_t N> +static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pair<Left, Right>, N> &map, Right key) { + for (const auto &entry : map) { + if (entry.second == key) { + return entry.first; + } + } + return std::nullopt; +} + +template<typename Left, typename Right, std::size_t N> +static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pair<Left, Right>, N> &map, + const std::optional<Right> &key) { + return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt; +} + void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); if (this->hp_.is_room_temperature_enabled()) { @@ -90,7 +106,28 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() { return traits; } -void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {} +void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { + if (const auto target_temperature = call.get_target_temperature()) { + this->hp_.set_target_temperature(*target_temperature); + } + + if (const auto mode = call.get_mode()) { + if (*mode == climate::CLIMATE_MODE_OFF) { + this->hp_.set_power(false); + } else if (const auto mapped = reverse_map_lookup(MODE_MAP, *mode)) { + this->hp_.set_power(true); + this->hp_.set_mode(*mapped); + } + } + + if (const auto fan_mode = reverse_map_lookup(FAN_MODE_MAP, call.get_fan_mode())) { + this->hp_.set_fan_mode(*fan_mode); + } + + if (this->hp_.is_status_initialized()) { + this->apply_values_(); + } +} void MitsubishiCN105Climate::apply_values_() { const auto &status = this->hp_.status(); diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index f26b0d82b62..7846a31193f 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -60,8 +60,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { // Settings should still have initial values EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); - EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::UNKNOWN); - EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::UNKNOWN); ctx.sut.set_current_time(300); ASSERT_FALSE(ctx.sut.update()); @@ -70,8 +70,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { // Check settings that we just read from received package EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); - EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::AUTO); - EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::AUTO); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::AUTO); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::AUTO); // Now fetch room temperature (0x03) EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); @@ -269,9 +269,10 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { ctx.sut.update(); EXPECT_TRUE(ctx.sut.status().power_on); + EXPECT_FALSE(ctx.sut.use_temperature_encoding_b_); EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f); - EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::COOL); - EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::QUIET); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::COOL); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::QUIET); } TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { @@ -283,9 +284,10 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { ctx.sut.update(); EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_TRUE(ctx.sut.use_temperature_encoding_b_); EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f); - EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::FAN_ONLY); - EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::SPEED_4); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::FAN_ONLY); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::SPEED_4); } TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) { @@ -308,4 +310,87 @@ TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) { EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f); } +TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) { + auto ctx = TestContext{}; + + ctx.sut.set_power(true); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); +} + +TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedA) { + auto ctx = TestContext{}; + + ctx.sut.set_target_temperature(23.0f); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71)); +} + +TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) { + auto ctx = TestContext{}; + + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_target_temperature(26.0f); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0xC5)); +} + +TEST(MitsubishiCN105Tests, ApplyModeCool) { + auto ctx = TestContext{}; + + ctx.sut.set_mode(MitsubishiCN105::Mode::COOL); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x02, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78)); +} + +TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) { + auto ctx = TestContext{}; + + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::SPEED_1); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73)); +} + +TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { + auto ctx = TestContext{}; + + // Waiting for next scheduled status update + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + // Nothing to do in update (rx empty, no timeout) + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + + // Write new values + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_power(false); + ctx.sut.set_target_temperature(25.0f); + ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT); + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); + + // Waiting for next status update must be interrupted and new values send to AC + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB)); + + // Write ACK response + ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); +} + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index ed55c3dc0ce..0862d64fa7e 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -45,8 +45,10 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::state_; using MitsubishiCN105::write_timeout_start_ms_; using MitsubishiCN105::status_update_start_ms_; + using MitsubishiCN105::use_temperature_encoding_b_; void set_state(State s) { this->set_state_(s); } + void apply_settings() { this->apply_settings_(); } static inline uint32_t test_loop_time_ms = 0; From dbd4e77d611cf259d21d04e830499902d7c3e22a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:23:10 -0400 Subject: [PATCH 1238/2056] [pylontech] Remove unnecessary Component inheritance from sensor/text_sensor (#15482) --- esphome/components/pylontech/sensor/__init__.py | 2 +- esphome/components/pylontech/sensor/pylontech_sensor.h | 2 +- esphome/components/pylontech/text_sensor/__init__.py | 2 +- .../components/pylontech/text_sensor/pylontech_text_sensor.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 716cc1001a3..52f2679b707 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -18,7 +18,7 @@ from esphome.const import ( from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns -PylontechSensor = pylontech_ns.class_("PylontechSensor", cg.Component) +PylontechSensor = pylontech_ns.class_("PylontechSensor") CONF_COULOMB = "coulomb" CONF_TEMPERATURE_LOW = "temperature_low" diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h index 8986adc26cd..25e71606a4d 100644 --- a/esphome/components/pylontech/sensor/pylontech_sensor.h +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -6,7 +6,7 @@ namespace esphome { namespace pylontech { -class PylontechSensor : public PylontechListener, public Component { +class PylontechSensor : public PylontechListener { public: PylontechSensor(int8_t bat_num); void dump_config() override; diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py index 15741ea9d18..f68ca103741 100644 --- a/esphome/components/pylontech/text_sensor/__init__.py +++ b/esphome/components/pylontech/text_sensor/__init__.py @@ -5,7 +5,7 @@ from esphome.const import CONF_ID from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns -PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor", cg.Component) +PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor") CONF_BASE_STATE = "base_state" CONF_VOLTAGE_STATE = "voltage_state" diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h index a685512ed55..27a3993b3e9 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -6,7 +6,7 @@ namespace esphome { namespace pylontech { -class PylontechTextSensor : public PylontechListener, public Component { +class PylontechTextSensor : public PylontechListener { public: PylontechTextSensor(int8_t bat_num); void dump_config() override; From a64f09a43f18af2627e7c9b6313e4869278b03d9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:29:59 -0400 Subject: [PATCH 1239/2056] [sprinkler][dfplayer][max6956][rf_bridge] Fix cg.templatable type mismatches (#15480) --- esphome/components/dfplayer/__init__.py | 14 +++++++------- esphome/components/max6956/__init__.py | 6 ++++-- esphome/components/rf_bridge/__init__.py | 4 ++-- esphome/components/sprinkler/__init__.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index c49420f0605..adc19137914 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -122,7 +122,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) return var @@ -143,10 +143,10 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): async def dfplayer_play_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = await cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_) cg.add(var.set_loop(template_)) return var @@ -167,13 +167,13 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args): async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_FOLDER], args, float) + template_ = await cg.templatable(config[CONF_FOLDER], args, cg.uint16) cg.add(var.set_folder(template_)) if CONF_FILE in config: - template_ = await cg.templatable(config[CONF_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = await cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_) cg.add(var.set_loop(template_)) return var @@ -213,7 +213,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args): async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_VOLUME], args, float) + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.uint8) cg.add(var.set_volume(template_)) return var diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index be6390fc17f..e9fae4cceb7 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -117,7 +117,7 @@ async def max6956_pin_to_code(config): async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, float) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, cg.uint8) cg.add(var.set_brightness_global(template_)) return var @@ -139,6 +139,8 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_BRIGHTNESS_MODE], args, float) + template_ = await cg.templatable( + config[CONF_BRIGHTNESS_MODE], args, MAX6956_CURRENTMODE + ) cg.add(var.set_brightness_mode(template_)) return var diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index c6eb1749c3a..4ee1e7891f8 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -185,9 +185,9 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) - template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint16) + template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint8) cg.add(var.set_length(template_)) - template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint16) + template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint8) cg.add(var.set_protocol(template_)) template_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) cg.add(var.set_code(template_)) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 6e2ff4ee2e1..9dc695cafc8 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -427,7 +427,7 @@ CONFIG_SCHEMA = cv.All( async def sprinkler_set_divider_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.float_) + template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.uint32) cg.add(var.set_divider(template_)) return var @@ -471,7 +471,7 @@ async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, ar async def sprinkler_set_repeat_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_REPEAT], args, cg.float_) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.uint32) cg.add(var.set_repeat(template_)) return var From 6044f41db55e61c4178d88366880cf9c0869615e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:30:15 -0400 Subject: [PATCH 1240/2056] [multiple] Add missing cv.COMPONENT_SCHEMA to CONFIG_SCHEMA (#15475) --- esphome/components/ads1118/__init__.py | 14 +- esphome/components/as3935/sensor.py | 2 +- esphome/components/cse7766/sensor.py | 96 ++++++------- .../cst226/binary_sensor/__init__.py | 12 +- esphome/components/dsmr/__init__.py | 4 +- esphome/components/e131/__init__.py | 2 +- esphome/components/gcja5/sensor.py | 128 +++++++++--------- esphome/components/hlw8032/sensor.py | 76 ++++++----- esphome/components/sml/__init__.py | 16 ++- esphome/components/sml/sensor/__init__.py | 18 ++- .../components/sml/text_sensor/__init__.py | 18 ++- esphome/components/sun_gtil2/__init__.py | 14 +- esphome/components/tm1651/__init__.py | 2 +- .../tt21100/binary_sensor/__init__.py | 14 +- esphome/components/wiegand/__init__.py | 2 +- 15 files changed, 230 insertions(+), 188 deletions(-) diff --git a/esphome/components/ads1118/__init__.py b/esphome/components/ads1118/__init__.py index 128e0d07013..45d47a329e7 100644 --- a/esphome/components/ads1118/__init__.py +++ b/esphome/components/ads1118/__init__.py @@ -12,11 +12,15 @@ CONF_ADS1118_ID = "ads1118_id" ads1118_ns = cg.esphome_ns.namespace("ads1118") ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ADS1118), - } -).extend(spi.spi_device_schema(cs_pin_required=True)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1118), + } + ) + .extend(spi.spi_device_schema(cs_pin_required=True)) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 79bc7af4a9e..1e549c5d82a 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema( accuracy_decimals=1, ), } -).extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 6572a914aa4..94ed66d7ccf 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -32,52 +32,56 @@ DEPENDENCIES = ["uart"] cse7766_ns = cg.esphome_ns.namespace("cse7766") CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CSE7766Component), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=3, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_APPARENT_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, - accuracy_decimals=1, - device_class=DEVICE_CLASS_REACTIVE_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7766Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "cse7766", baud_rate=4800, parity="EVEN", require_rx=True ) diff --git a/esphome/components/cst226/binary_sensor/__init__.py b/esphome/components/cst226/binary_sensor/__init__.py index d95f0d2b4d1..324d7947726 100644 --- a/esphome/components/cst226/binary_sensor/__init__.py +++ b/esphome/components/cst226/binary_sensor/__init__.py @@ -15,10 +15,14 @@ CST226Button = cst226_ns.class_( cg.Parented.template(CST226Touchscreen), ) -CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend( - { - cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen), - } +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(CST226Button) + .extend( + { + cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index b1ff9794a38..dd7f2b9f566 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -46,7 +46,9 @@ CONFIG_SCHEMA = cv.All( CONF_RECEIVE_TIMEOUT, default="200ms" ): cv.positive_time_period_milliseconds, } - ).extend(uart.UART_DEVICE_SCHEMA), + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), ) diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index 301812e314e..a1a8e0aec59 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(E131Component), cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), } -) +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py index ec26447ccb8..a522b1f50f0 100644 --- a/esphome/components/gcja5/sensor.py +++ b/esphome/components/gcja5/sensor.py @@ -29,68 +29,72 @@ GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UAR CONF_PMC_0_3 = "pmc_0_3" CONF_PMC_5_0 = "pmc_5_0" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(GCJA5Component), - cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM1, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GCJA5Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "gcja5", baud_rate=9600, require_rx=True, parity="EVEN" ) diff --git a/esphome/components/hlw8032/sensor.py b/esphome/components/hlw8032/sensor.py index 96800e46f4e..846c9a398b5 100644 --- a/esphome/components/hlw8032/sensor.py +++ b/esphome/components/hlw8032/sensor.py @@ -27,42 +27,46 @@ DEPENDENCIES = ["uart"] hlw8032_ns = cg.esphome_ns.namespace("hlw8032") HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(HLW8032Component), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_APPARENT_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, - cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HLW8032Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, + cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN" diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index 1bf0d97d65e..1b7f9da4fb4 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -19,12 +19,16 @@ CONF_OBIS_CODE = "obis_code" CONF_SERVER_ID = "server_id" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(Sml), - cv.Optional(CONF_ON_DATA): automation.validate_automation({}), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sml), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/sml/sensor/__init__.py b/esphome/components/sml/sensor/__init__.py index 164a31f8a71..e6d7180f177 100644 --- a/esphome/components/sml/sensor/__init__.py +++ b/esphome/components/sml/sensor/__init__.py @@ -10,13 +10,17 @@ AUTO_LOAD = ["sml"] SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema().extend( - { - cv.GenerateID(): cv.declare_id(SmlSensor), - cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), - cv.Required(CONF_OBIS_CODE): obis_code, - cv.Optional(CONF_SERVER_ID, default=""): cv.string, - } +CONFIG_SCHEMA = ( + sensor.sensor_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(SmlSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py index 9c9da26c3aa..5a5ab658c4b 100644 --- a/esphome/components/sml/text_sensor/__init__.py +++ b/esphome/components/sml/text_sensor/__init__.py @@ -19,13 +19,17 @@ SML_TYPES = { SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) -CONFIG_SCHEMA = text_sensor.text_sensor_schema(SmlTextSensor).extend( - { - cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), - cv.Required(CONF_OBIS_CODE): obis_code, - cv.Optional(CONF_SERVER_ID, default=""): cv.string, - cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), - } +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(SmlTextSensor) + .extend( + { + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/sun_gtil2/__init__.py b/esphome/components/sun_gtil2/__init__.py index d073c16e4e8..c7082794db7 100644 --- a/esphome/components/sun_gtil2/__init__.py +++ b/esphome/components/sun_gtil2/__init__.py @@ -13,11 +13,15 @@ sun_gtil2_ns = cg.esphome_ns.namespace("sun_gtil2") SunGTIL2Component = sun_gtil2_ns.class_("SunGTIL2", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(SunGTIL2Component), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SunGTIL2Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index fb35eb21b51..7d957df3beb 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, } - ), + ).extend(cv.COMPONENT_SCHEMA), ) diff --git a/esphome/components/tt21100/binary_sensor/__init__.py b/esphome/components/tt21100/binary_sensor/__init__.py index f79eff0e017..081bd17c20b 100644 --- a/esphome/components/tt21100/binary_sensor/__init__.py +++ b/esphome/components/tt21100/binary_sensor/__init__.py @@ -16,11 +16,15 @@ TT21100Button = tt21100_ns.class_( cg.Parented.template(TT21100Touchscreen), ) -CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TT21100Button).extend( - { - cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), - cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), - } +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(TT21100Button) + .extend( + { + cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), + cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py index 962ac4c3739..36ec7bd43fd 100644 --- a/esphome/components/wiegand/__init__.py +++ b/esphome/components/wiegand/__init__.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA = cv.Schema( } ), } -) +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): From e86978f0dad99790effc0e5abebdece6baeb99a3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:30:46 -0400 Subject: [PATCH 1241/2056] [rpi_dpi_rgb][st7701s][ags10] Fix Optional config keys accessed unconditionally (#15474) --- esphome/components/ags10/sensor.py | 2 +- esphome/components/rpi_dpi_rgb/display.py | 2 +- esphome/components/st7701s/display.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 90fe067b32c..e94504ff1af 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -35,7 +35,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(AGS10Component), - cv.Optional(CONF_TVOC): sensor.sensor_schema( + cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index e92eee7c0c0..ee462686e47 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.All( } ), ), - cv.Optional(CONF_COLOR_ORDER): cv.one_of( + cv.Optional(CONF_COLOR_ORDER, default="BGR"): cv.one_of( *COLOR_ORDERS.keys(), upper=True ), cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index a8b12dfa28a..7f6492812fd 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -138,7 +138,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INIT_SEQUENCE, default=1): cv.ensure_list( map_sequence ), - cv.Optional(CONF_COLOR_ORDER): cv.one_of( + cv.Optional(CONF_COLOR_ORDER, default="BGR"): cv.one_of( *COLOR_ORDERS.keys(), upper=True ), cv.Optional(CONF_PCLK_FREQUENCY, default="16MHz"): cv.All( From a7963bee98e0c468f8e75436567bb5be70023014 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:31:40 -0400 Subject: [PATCH 1242/2056] [gcja5][cd74hc4067][openthread_info] Fix PollingComponent mismatches (#15476) --- esphome/components/cd74hc4067/__init__.py | 4 +--- esphome/components/gcja5/sensor.py | 2 +- esphome/components/openthread_info/text_sensor.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py index 9b69576b43c..af6866df789 100644 --- a/esphome/components/cd74hc4067/__init__.py +++ b/esphome/components/cd74hc4067/__init__.py @@ -9,9 +9,7 @@ MULTI_CONF = True cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067") -CD74HC4067Component = cd74hc4067_ns.class_( - "CD74HC4067Component", cg.Component, cg.PollingComponent -) +CD74HC4067Component = cd74hc4067_ns.class_("CD74HC4067Component", cg.Component) CONF_PIN_S0 = "pin_s0" CONF_PIN_S1 = "pin_s1" diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py index a522b1f50f0..e4de7721c65 100644 --- a/esphome/components/gcja5/sensor.py +++ b/esphome/components/gcja5/sensor.py @@ -24,7 +24,7 @@ DEPENDENCIES = ["uart"] gcja5_ns = cg.esphome_ns.namespace("gcja5") -GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UARTDevice) +GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.Component, uart.UARTDevice) CONF_PMC_0_3 = "pmc_0_3" CONF_PMC_5_0 = "pmc_5_0" diff --git a/esphome/components/openthread_info/text_sensor.py b/esphome/components/openthread_info/text_sensor.py index ddec8f264ce..b672831bf09 100644 --- a/esphome/components/openthread_info/text_sensor.py +++ b/esphome/components/openthread_info/text_sensor.py @@ -54,7 +54,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ), + ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_ROLE): text_sensor.text_sensor_schema( RoleOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), From 62b4b250c7fd085b356b8eff34f9a9d493007ee9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:35:50 -0400 Subject: [PATCH 1243/2056] [opentherm] Fix step=0 default overriding entity step (#15484) --- esphome/components/opentherm/input.py | 33 +++++++++---------- .../components/opentherm/number/__init__.py | 14 +++++--- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/esphome/components/opentherm/input.py b/esphome/components/opentherm/input.py index c5814f74e2c..e711e4df8eb 100644 --- a/esphome/components/opentherm/input.py +++ b/esphome/components/opentherm/input.py @@ -2,51 +2,48 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE from . import generate, schema -CONF_min_value = "min_value" -CONF_max_value = "max_value" -CONF_auto_min_value = "auto_min_value" -CONF_auto_max_value = "auto_max_value" -CONF_step = "step" +CONF_AUTO_MIN_VALUE = "auto_min_value" +CONF_AUTO_MAX_VALUE = "auto_max_value" OpenthermInput = generate.opentherm_ns.class_("OpenthermInput") def validate_min_value_less_than_max_value(conf): if ( - CONF_min_value in conf - and CONF_max_value in conf - and conf[CONF_min_value] > conf[CONF_max_value] + CONF_MIN_VALUE in conf + and CONF_MAX_VALUE in conf + and conf[CONF_MIN_VALUE] > conf[CONF_MAX_VALUE] ): - raise cv.Invalid(f"{CONF_min_value} must be less than {CONF_max_value}") + raise cv.Invalid(f"{CONF_MIN_VALUE} must be less than {CONF_MAX_VALUE}") return conf def input_schema(entity: schema.InputSchema) -> cv.Schema: result = cv.Schema( { - cv.Optional(CONF_min_value, entity.range[0]): cv.float_range( + cv.Optional(CONF_MIN_VALUE, entity.range[0]): cv.float_range( entity.range[0], entity.range[1] ), - cv.Optional(CONF_max_value, entity.range[1]): cv.float_range( + cv.Optional(CONF_MAX_VALUE, entity.range[1]): cv.float_range( entity.range[0], entity.range[1] ), } ) result = result.add_extra(validate_min_value_less_than_max_value) - result = result.extend({cv.Optional(CONF_step, False): cv.float_}) if entity.auto_min_value is not None: - result = result.extend({cv.Optional(CONF_auto_min_value, False): cv.boolean}) + result = result.extend({cv.Optional(CONF_AUTO_MIN_VALUE, False): cv.boolean}) if entity.auto_max_value is not None: - result = result.extend({cv.Optional(CONF_auto_max_value, False): cv.boolean}) + result = result.extend({cv.Optional(CONF_AUTO_MAX_VALUE, False): cv.boolean}) return result def generate_setters(entity: cg.MockObj, conf: dict[str, Any]) -> None: - generate.add_property_set(entity, CONF_min_value, conf) - generate.add_property_set(entity, CONF_max_value, conf) - generate.add_property_set(entity, CONF_auto_min_value, conf) - generate.add_property_set(entity, CONF_auto_max_value, conf) + generate.add_property_set(entity, CONF_MIN_VALUE, conf) + generate.add_property_set(entity, CONF_MAX_VALUE, conf) + generate.add_property_set(entity, CONF_AUTO_MIN_VALUE, conf) + generate.add_property_set(entity, CONF_AUTO_MAX_VALUE, conf) diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py index 6dbc45f49be..17ec12a61ab 100644 --- a/esphome/components/opentherm/number/__init__.py +++ b/esphome/components/opentherm/number/__init__.py @@ -3,7 +3,13 @@ from typing import Any import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_STEP +from esphome.const import ( + CONF_INITIAL_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_RESTORE_VALUE, + CONF_STEP, +) from .. import const, generate, input, schema, validate @@ -18,9 +24,9 @@ OpenthermNumber = generate.opentherm_ns.class_( async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: var = await number.new_number( config, - min_value=config[input.CONF_min_value], - max_value=config[input.CONF_max_value], - step=config[input.CONF_step], + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], ) await cg.register_component(var, config) input.generate_setters(var, config) From ab455915079d9a51fda55e59bc7dafe7f864997c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 11:01:03 -1000 Subject: [PATCH 1244/2056] [core] Move wake_loop out of socket component into core (#15446) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32_ble/__init__.py | 6 - esphome/components/esp32_ble/ble.cpp | 6 - esphome/components/esp32_camera/__init__.py | 5 +- .../components/esp32_camera/esp32_camera.cpp | 2 - .../components/esphome/ota/ota_esphome.cpp | 2 +- esphome/components/espnow/__init__.py | 8 +- .../components/espnow/espnow_component.cpp | 8 +- .../components/micro_wake_word/__init__.py | 8 +- .../micro_wake_word/micro_wake_word.cpp | 2 - esphome/components/mixer/speaker/__init__.py | 5 +- .../mixer/speaker/mixer_speaker.cpp | 4 - esphome/components/mqtt/__init__.py | 6 - .../components/mqtt/mqtt_backend_esp32.cpp | 4 +- .../components/resampler/speaker/__init__.py | 5 +- .../resampler/speaker/resampler_speaker.cpp | 2 - esphome/components/socket/__init__.py | 48 ++----- .../components/socket/lwip_raw_tcp_impl.cpp | 111 +-------------- esphome/components/socket/lwip_raw_tcp_impl.h | 2 +- esphome/components/socket/socket.cpp | 2 +- esphome/components/socket/socket.h | 16 +-- esphome/components/uart/__init__.py | 14 -- .../components/usb_cdc_acm/usb_cdc_acm.cpp | 6 - esphome/components/usb_host/__init__.py | 8 +- .../components/usb_host/usb_host_client.cpp | 4 +- esphome/components/usb_uart/__init__.py | 7 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- esphome/core/application.cpp | 91 ++++--------- esphome/core/application.h | 128 ++++++------------ esphome/core/component.cpp | 10 +- esphome/core/config.py | 14 ++ esphome/core/defines.h | 5 +- esphome/core/lwip_fast_select.c | 57 ++------ esphome/core/lwip_fast_select.h | 20 --- esphome/core/main_task.c | 5 + esphome/core/main_task.h | 55 ++++++++ esphome/core/wake.cpp | 82 +++++++++++ esphome/core/wake.h | 126 +++++++++++++++++ .../socket/test_wake_loop_threadsafe.py | 127 ----------------- 38 files changed, 397 insertions(+), 618 deletions(-) create mode 100644 esphome/core/main_task.c create mode 100644 esphome/core/main_task.h create mode 100644 esphome/core/wake.cpp create mode 100644 esphome/core/wake.h delete mode 100644 tests/components/socket/test_wake_loop_threadsafe.py diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 2e5e3587536..974611c9b18 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,7 +7,6 @@ from typing import Any from esphome import automation import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv @@ -592,11 +591,6 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks - # This enables low-latency (~12μs) BLE event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 68e5fffe2bc..0280439731c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -599,9 +599,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif return; // Ignore these GAP events as they are not relevant for our use case @@ -622,9 +620,7 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif @@ -633,9 +629,7 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 66af321e4e2..5165956806f 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c, socket +from esphome.components import i2c from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv @@ -29,7 +29,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera", "socket"] +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -370,7 +370,6 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") - socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 085feb8c8a7..a7546476d89 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -521,11 +521,9 @@ void ESP32Camera::framebuffer_task(void *pv) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // Only wake the main loop if there's a pending request to consume the frame -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (that->has_requested_image_()) { App.wake_loop_threadsafe(); } -#endif // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 972d2b2b8d5..af9b8ee19a1 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -262,7 +262,7 @@ void ESPHomeOTAComponent::handle_data_() { /// BSD sockets (ESP32): setblocking(true) makes read/write block /// lwip sockets (LT): setblocking(true) makes read/write block /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses - /// socket_delay()/socket_wake() in read(); + /// wakeable_delay() in read(); /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 00703bc2284..1c8d262810d 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, core import esphome.codegen as cg -from esphome.components import socket, wifi +from esphome.components import wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,7 +17,7 @@ from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] -AUTO_LOAD = ["socket"] + byte_vector = cg.std_vector.template(cg.uint8) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -124,10 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks - # This enables low-latency event processing instead of waiting for select() timeout - socket.require_wake_loop_threadsafe() - cg.add_define("USE_ESPNOW") if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 0dc0f12e7e6..282287ca83d 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -92,10 +92,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. - // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process ESP-NOW send event App.wake_loop_threadsafe(); -#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -115,10 +113,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. - // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process ESP-NOW receive event App.wake_loop_threadsafe(); -#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 372eb4c3b00..fae48630b55 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, ota, socket +from esphome.components import esp32, microphone, ota import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] -AUTO_LOAD = ["socket"] + DOMAIN = "micro_wake_word" @@ -444,10 +444,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Enable wake_loop_threadsafe() for low-latency wake word detection - # The inference task queues detection events that need immediate processing - socket.require_wake_loop_threadsafe() - mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index b93bf1b556c..f1aac875f13 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -431,9 +431,7 @@ void MicroWakeWord::process_probabilities_() { xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); // Wake main loop immediately to process wake word detection -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 63b419cc98e..59a80d9297c 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 0fabc68c705..741239a2dd9 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -245,11 +245,9 @@ void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } @@ -533,9 +531,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } return ESP_OK; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 817f99375ea..33a88c49cc7 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -69,9 +69,6 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): if CORE.is_esp8266 or CORE.is_libretiny: return ["async_tcp", "json"] - # ESP32 needs socket for wake_loop_threadsafe() - if CORE.is_esp32: - return ["json", "socket"] return ["json"] @@ -348,10 +345,7 @@ async def to_code(config): # https://github.com/heman/async-mqtt-client/blob/master/library.json cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") - # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler - # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: - socket.require_wake_loop_threadsafe() # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) # IDF 6.0 moved esp-mqtt to an external component if idf_version() >= cv.Version(6, 0, 0): diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index ab067c44182..499a3307306 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -202,10 +202,8 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b // allocate() returned non-null, the queue cannot be full. instance->mqtt_event_queue_.push(event); - // Wake main loop immediately to process MQTT event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process MQTT event App.wake_loop_threadsafe(); -#endif } } diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 4e4705a8892..3134cf7646a 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -77,9 +77,6 @@ FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await speaker.register_speaker(var, config) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index b737a2d39a0..3b50353ddc8 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -245,11 +245,9 @@ void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 08cf3ea33c3..abbbb0f056f 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -31,9 +31,6 @@ MIN_UDP_SOCKETS = 6 # Minimum listening sockets — at least api + ota baseline. MIN_TCP_LISTEN_SOCKETS = 2 -# Wake loop threadsafe support tracking -KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" - class SocketType(StrEnum): TCP = "tcp" @@ -123,37 +120,22 @@ def get_socket_counts() -> SocketCounts: def require_wake_loop_threadsafe() -> None: - """Mark that wake_loop_threadsafe support is required by a component. + """Deprecated: wake loop support is now always available on all platforms. - Call this from components that need to wake the main event loop from background threads. - This enables the shared UDP loopback socket mechanism (~208 bytes RAM). - The socket is shared across all components that use this feature. - - This call is a no-op if networking is not enabled in the configuration. - - IMPORTANT: This is for background thread context only, NOT ISR context. - Socket operations are not safe to call from ISR handlers. - - On ESP32, FreeRTOS task notifications are used instead (no socket needed). - - Example: - from esphome.components import socket - - async def to_code(config): - socket.require_wake_loop_threadsafe() + This function adds backward-compatible defines so external components + that check #ifdef USE_WAKE_LOOP_THREADSAFE / USE_SOCKET_SELECT_SUPPORT + continue to compile. Remove before 2026.12.0. """ - - # Only set up once (idempotent - multiple components can call this) - if CORE.has_networking and not CORE.data.get( - KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False - ): - CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - cg.add_define("USE_WAKE_LOOP_THREADSAFE") - if not CORE.is_esp32 and not CORE.is_libretiny: - # Only platforms without fast select need a UDP socket for wake - # notifications. ESP32 and LibreTiny use FreeRTOS task notifications - # instead (no socket needed). - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) + # Remove before 2026.12.0 + _LOGGER.warning( + "require_wake_loop_threadsafe() is deprecated and no longer needed. " + "Wake loop support is now always available. Remove this call and any " + "#ifdef USE_SOCKET_SELECT_SUPPORT / USE_WAKE_LOOP_THREADSAFE guards. " + "This will be removed in 2026.12.0." + ) + # Add deprecated defines for backward compat with external component C++ code + cg.add_define("USE_WAKE_LOOP_THREADSAFE") + cg.add_define("USE_SOCKET_SELECT_SUPPORT") CONFIG_SCHEMA = cv.Schema( @@ -184,10 +166,8 @@ async def to_code(config): cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") elif impl == IMPLEMENTATION_LWIP_SOCKETS: cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() # and FreeRTOS task notifications — enable fast select to bypass lwip_select(). # Only when not using lwip_tcp, which does not provide select() support. diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 3bcbd880850..86131d3ddb9 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -8,6 +8,7 @@ #include <sys/time.h> #include "esphome/core/helpers.h" +#include "esphome/core/wake.h" #include "esphome/core/log.h" #ifdef USE_ESP8266 @@ -19,102 +20,6 @@ namespace esphome::socket { -#ifdef USE_ESP8266 -// Flag to signal socket activity - checked by socket_delay() to exit early -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; - -void socket_delay(uint32_t ms) { - // Use esp_delay with a callback that checks if socket data arrived. - // This allows the delay to exit early when socket_wake() is called by - // lwip recv_fn/accept_fn callbacks, reducing socket latency. - // - // When ms is 0, we must use delay(0) because esp_delay(0, callback) - // exits immediately without yielding, which can cause watchdog timeouts - // when the main loop runs in high-frequency mode (e.g., during light effects). - if (ms == 0) { - delay(0); - return; - } - s_socket_woke = false; - esp_delay(ms, []() { return !s_socket_woke; }); -} - -void IRAM_ATTR socket_wake() { - s_socket_woke = true; - esp_schedule(); -} -#elif defined(USE_RP2040) -// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. -// -// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, -// then sleep with __wfe(). Wake on either: -// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout -// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake -// -// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, -// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept -// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_delay_expired = false; - -static int64_t alarm_callback(alarm_id_t id, void *user_data) { - (void) id; - (void) user_data; - s_delay_expired = true; - // Wake the main loop from __wfe() sleep — timeout expired. - __sev(); - // Return 0 = don't reschedule (one-shot) - return 0; -} - -void socket_delay(uint32_t ms) { - if (ms == 0) { - yield(); - return; - } - // If a wake was already signalled, consume it and return immediately - // instead of going to sleep. This avoids losing a wake that arrived - // between loop iterations. - if (s_socket_woke) { - s_socket_woke = false; - return; - } - // Don't clear s_socket_woke here — if an IRQ fires between the check above - // and the while loop below, the while condition sees it immediately. Clearing - // here would lose that wake and sleep until the timer fires. - s_delay_expired = false; - // Set a one-shot timer to wake us after the timeout. - // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - // Sleep until woken by either the timer alarm or socket_wake(). - // __wfe() may return spuriously (stale event register, other interrupts), - // so we loop checking both flags. - while (!s_socket_woke && !s_delay_expired) { - __wfe(); - } - // Cancel timer if we woke early (socket data arrived before timeout) - if (!s_delay_expired) - cancel_alarm(alarm); - s_socket_woke = false; // consume the wake for next call -} - -// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP -// callbacks via pendsv (not hard IRQ), so they execute from flash safely. -void socket_wake() { - s_socket_woke = true; - // Wake the main loop from __wfe() sleep. __sev() is a global event that - // wakes any core sleeping in __wfe(). This is ISR-safe. - __sev(); -} -#endif - // ---- LWIP thread safety ---- // // On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. @@ -543,10 +448,8 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } @@ -555,15 +458,15 @@ void LWIPRawImpl::wait_for_data_() { // (needs async_context lock). // // Loop until data arrives, connection closes, or the full timeout elapses. - // socket_delay() may return early due to other sockets waking the global - // socket_wake() flag, so we re-enter for the remaining time. + // wakeable_delay() may return early due to any wake source, + // so we re-enter for the remaining time. uint32_t timeout_ms = this->recv_timeout_cs_ * 10; uint32_t start = millis(); while (this->waiting_for_data_()) { uint32_t elapsed = millis() - start; if (elapsed >= timeout_ms) break; - socket_delay(timeout_ms - elapsed); + esphome::internal::wakeable_delay(timeout_ms - elapsed); } } @@ -951,10 +854,8 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn); tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 3c27d71062f..e2dcb80d32b 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -109,7 +109,7 @@ class LWIPRawImpl : public LWIPRawCommon { return -1; } // Raw TCP doesn't use a blocking flag directly. Blocking behavior - // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). + // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay(). return 0; } int loop() { return 0; } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index bfb6ae8e130..bc43b2746ee 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 226a669e31a..9ea71321e0b 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -45,7 +45,7 @@ using ListenSocket = LWIPRawListenImpl; inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Shared ready() helper for fd-based socket implementations. /// Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored); @@ -120,19 +120,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -/// Delay that can be woken early by socket activity. -/// On ESP8266, uses esp_delay() with a callback that checks socket activity. -/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt -/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) - -/// Signal socket/IO activity and wake the main loop early. -/// On ESP8266: sets flag + esp_schedule(). -/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). -/// ISR-safe on both platforms. -void socket_wake(); // NOLINT(readability-redundant-declaration) -#endif - } // namespace esphome::socket #endif diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 83649cc2092..70752287436 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -42,16 +42,6 @@ CODEOWNERS = ["@esphome/core"] DOMAIN = "uart" -def AUTO_LOAD() -> list[str]: - """Ideally, we would only auto-load socket only when wake_loop_on_rx is requested; - however, AUTO_LOAD is examined before wake_loop_on_rx is set, so instead, since ESP32 - always uses socket select support in the main app, we'll just ensure it's loaded here. - """ - if CORE.is_esp32: - return ["socket"] - return [] - - uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -527,10 +517,6 @@ async def final_step(): # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer # registration) — enable by default to reduce RX buffer overflow risk # by waking the main loop immediately when data arrives. - # Requires networking for the wake_loop_isrsafe() infrastructure. - from esphome.components import socket - - socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index 253626f0a3e..40f7f2e28bb 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -29,10 +29,7 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,10 +50,7 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 5eb0371e5c5..338bd8d5728 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -14,7 +13,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer", "socket"] +AUTO_LOAD = ["bytebuffer"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -76,11 +75,6 @@ async def to_code(config: ConfigType) -> None: max_requests = config[CONF_MAX_TRANSFER_REQUESTS] cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) - # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task - # This enables low-latency (~12μs) USB event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 18d938344c7..c34c7ef67d9 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -200,10 +200,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Re-enable component loop to process the queued event client->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB event App.wake_loop_threadsafe(); -#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 2d85723d722..0e8994a3ed2 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema @@ -14,7 +13,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -117,10 +116,6 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): - # Enable wake_loop_threadsafe for low-latency USB data processing - # The USB task queues data events that need immediate processing - socket.require_wake_loop_threadsafe() - for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 0b8589f6713..30ec61fdc4b 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -325,10 +325,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Re-enable component loop to process the queued data this->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB data instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB data App.wake_loop_threadsafe(); -#endif } // On success, restart input immediately from USB task for performance diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 5cb8a5bb244..cd758598801 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -28,22 +28,8 @@ #include "esphome/components/socket/socket.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #include <cerrno> - -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS -// LWIP sockets implementation -#include <lwip/sockets.h> -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) -// BSD sockets implementation -#ifdef USE_ESP32 -// ESP32 "BSD sockets" are actually LWIP under the hood -#include <lwip/sockets.h> -#else -// True BSD sockets (e.g., host platform) -#include <sys/select.h> -#endif -#endif #endif namespace esphome { @@ -128,13 +114,11 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. - // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally - // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). - esphome_lwip_fast_select_init(); +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Save main loop task handle for wake_loop_*() / fast select FreeRTOS notifications. + esphome_main_task_handle = xTaskGetCurrentTaskHandle(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -490,23 +474,17 @@ void Application::unregister_socket(struct lwip_sock *sock) { return; } } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking if (fd < 0) return false; -#ifndef USE_ESP32 - // Only check on non-ESP32 platforms - // On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design - // (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h) - // Other platforms may not have this guarantee if (fd >= FD_SETSIZE) { ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); return false; } -#endif this->socket_fds_.push_back(fd); this->socket_fds_changed_ = true; @@ -547,7 +525,7 @@ void Application::unregister_socket_fd(int fd) { #endif // Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST void Application::yield_with_select_(uint32_t delay_ms) { // Fallback select() path (host platform and any future platforms without fast select). if (!this->socket_fds_.empty()) [[likely]] { @@ -570,11 +548,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; // Call select with timeout -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS - int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#else int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#endif // Process select() result: // ret > 0: socket(s) have data ready - normal and expected @@ -597,7 +571,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered or select() failed - use regular delay delay(delay_ms); } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_HOST // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. @@ -618,18 +592,13 @@ alignas(Application) char app_storage[sizeof(Application)] asm( #undef ESPHOME_STRINGIFY_ #undef ESPHOME_STRINGIFY_IMPL_ -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - -#ifdef USE_LWIP_FAST_SELECT -void Application::wake_loop_threadsafe() { - // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) - esphome_lwip_wake_main_loop(); -} -#else // !USE_LWIP_FAST_SELECT +// Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ +// ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp +#ifdef USE_HOST void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications - this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + this->wake_socket_fd_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (this->wake_socket_fd_ < 0) { ESP_LOGW(TAG, "Wake socket create failed: %d", errno); return; @@ -638,12 +607,12 @@ void Application::setup_wake_loop_threadsafe_() { // Bind to loopback with auto-assigned port struct sockaddr_in addr = {}; addr.sin_family = AF_INET; - addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; // Auto-assign port - if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + if (::bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } @@ -652,50 +621,36 @@ void Application::setup_wake_loop_threadsafe_() { // Connecting a UDP socket allows using send() instead of sendto() for better performance struct sockaddr_in wake_addr; socklen_t len = sizeof(wake_addr); - if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + if (::getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { ESP_LOGW(TAG, "Wake socket address failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Connect to self (loopback) - allows using send() instead of sendto() // After connect(), no need to store wake_addr - the socket remembers it - if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + if (::connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Set non-blocking mode - int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); - lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + int flags = ::fcntl(this->wake_socket_fd_, F_GETFL, 0); + ::fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); // Register with application's select() loop if (!this->register_socket_fd(this->wake_socket_fd_)) { ESP_LOGW(TAG, "Wake socket register failed"); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } } -void Application::wake_loop_threadsafe() { - // Called from FreeRTOS task context when events need immediate processing - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->wake_socket_fd_ >= 0) { - const char dummy = 1; - // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway - // No error checking needed: we control both ends of this loopback socket. - // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->wake_socket_fd_, &dummy, 1, 0); - } -} -#endif // USE_LWIP_FAST_SELECT - -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#endif // USE_HOST void Application::get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); diff --git a/esphome/core/application.h b/esphome/core/application.h index 6cc61bc9547..6b2969b4907 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,32 +24,21 @@ #include "esphome/core/area.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include <freertos/FreeRTOS.h> -#include <freertos/task.h> -#else -#include <FreeRTOS.h> -#include <task.h> #endif -#else +#ifdef USE_HOST #include <sys/select.h> -#ifdef USE_WAKE_LOOP_THREADSAFE -#include <lwip/sockets.h> +#include <sys/socket.h> +#include <unistd.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <arpa/inet.h> #endif -#endif -#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) -} // namespace esphome::socket -#endif +#include "esphome/core/wake.h" #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -124,7 +113,7 @@ void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) #endif namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST /// Shared ready() helper for fd-based socket implementations. bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) #endif @@ -550,7 +539,7 @@ class Application { /// @return true if registration was successful, false if sock is null bool register_socket(struct lwip_sock *sock); void unregister_socket(struct lwip_sock *sock); -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Fallback select() path: monitors file descriptors. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. /// @return true if registration was successful, false if fd exceeds limits @@ -558,43 +547,21 @@ class Application { void unregister_socket_fd(int fd); #endif -#ifdef USE_WAKE_LOOP_THREADSAFE - /// Wake the main event loop from another FreeRTOS task. - /// Thread-safe, but must only be called from task context (NOT ISR-safe). - /// On ESP32: uses xTaskNotifyGive (<1 us) - /// On other platforms: uses UDP loopback socket - void wake_loop_threadsafe(); -#endif - -#ifdef USE_LWIP_FAST_SELECT - /// Wake the main event loop from an ISR. - /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. - /// Only available on platforms with fast select (ESP32, LibreTiny). - /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. - static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { - esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); - } + /// Wake the main event loop from another thread or callback. + /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. + void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } #ifdef USE_ESP32 - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Detects the calling context and uses the appropriate FreeRTOS API. - static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } -#endif + /// Wake from ISR (ESP32 only). + static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); } #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. - static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } -#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context. - /// Sets the socket wake flag and calls __sev() to exit __wfe() early. - static void wake_loop_any_context() { socket::socket_wake(); } -#endif + /// Wake from any context (ISR, thread, callback). + static void IRAM_ATTR wake_loop_any_context() { esphome::wake_loop_any_context(); } protected: friend Component; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif #ifdef USE_RUNTIME_STATS @@ -602,8 +569,11 @@ class Application { #endif friend void ::setup(); friend void ::original_setup(); +#ifdef USE_HOST + friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ +#endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif @@ -648,14 +618,14 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); #else inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -685,13 +655,11 @@ class Application { FixedVector<Component *> looping_components_{}; #ifdef USE_LWIP_FAST_SELECT std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors #endif -#ifdef USE_SOCKET_SELECT_SUPPORT -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif #endif // StringRef members (8 bytes each: pointer + size) @@ -702,7 +670,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -718,11 +686,11 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes @@ -815,7 +783,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -832,15 +800,15 @@ inline void Application::drain_wake_notifications_() { // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK // We control both ends of this loopback socket (always write 1 byte per wake), // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + while (::recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { // Just draining, no action needed - wake has already occurred } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -908,21 +876,17 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { #endif // Use the last component's end time instead of calling millis() again + uint32_t delay_time = 0; auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; + if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) { + delay_time = this->loop_interval_ - elapsed; 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); delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); } + this->yield_with_select_(delay_time); this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { @@ -931,9 +895,9 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } // Inline yield_with_select_ for all paths except the select() fallback -#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#ifndef USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { @@ -953,20 +917,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay } // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); + // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. #endif + esphome::internal::wakeable_delay(delay_ms); } -#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#endif // !USE_HOST } // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0f68f0c8e02..deda42b0a7d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -299,7 +299,7 @@ void Component::enable_loop_slow_path_() { this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } -void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { +void IRAM_ATTR Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted @@ -311,15 +311,9 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ - ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). - // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. - // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. - // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. - Application::wake_loop_any_context(); -#endif + wake_loop_any_context(); } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { diff --git a/esphome/core/config.py b/esphome/core/config.py index c47693c783e..62c41b254c4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -753,6 +753,20 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "main_task.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "lwip_fast_select.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, "time_64.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d92fb6e98a9..9c90790f3a8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -252,9 +252,8 @@ #define USE_SENDSPIN #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT -#define USE_WAKE_LOOP_THREADSAFE + #define USE_SPEAKER #define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI @@ -379,7 +378,6 @@ #ifdef USE_LIBRETINY #define USE_CAPTIVE_PORTAL #define USE_SOCKET_IMPL_LWIP_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH @@ -391,7 +389,6 @@ #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index a695fa396bc..bb3acbafcb1 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -63,12 +63,12 @@ // // Shared state and safety rationale: // -// s_main_loop_task (TaskHandle_t, 4 bytes): -// Written once by main loop in init(). Read by TCP/IP thread (in callback) -// and background tasks (in wake). -// Safe: write-once-then-read pattern. Socket hooks may run before init(), -// but the NULL check on s_main_loop_task in the callback provides correct -// degraded behavior — notifications are simply skipped until init() completes. +// esphome_main_task_handle (TaskHandle_t, 4 bytes, defined in main_task.c): +// Written once by main loop in Application::setup(). Read by TCP/IP thread +// (in callback) and background tasks (in wake). +// Safe: write-once-then-read pattern. Socket hooks may run before setup(), +// but the NULL check on esphome_main_task_handle in the callback provides correct +// degraded behavior — notifications are simply skipped until setup() completes. // // s_original_callback (netconn_callback, 4-byte function pointer): // Written by main loop in hook_socket() (only when NULL — set once). @@ -123,15 +123,10 @@ #endif #include "esphome/core/lwip_fast_select.h" +#include "esphome/core/main_task.h" #include <stddef.h> -// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. -// On LibreTiny it's not defined — provide a no-op fallback. -#ifndef IRAM_ATTR -#define IRAM_ATTR -#endif - // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -157,8 +152,7 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); -// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. -static TaskHandle_t s_main_loop_task = NULL; +// Task handle is in main_task.c (esphome_main_task_handle) — shared with wake.h. // Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. static netconn_callback s_original_callback = NULL; @@ -177,15 +171,13 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt // (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions // already wake the main loop through the RCVPLUS path. if (evt == NETCONN_EVT_RCVPLUS) { - TaskHandle_t task = s_main_loop_task; + TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); } } } -void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); } - // lwip_socket_dbg_get_socket() is a thin wrapper around the static // tryget_socket_unconn_nouse() — a direct array lookup without the refcount // that get_socket()/done_socket() uses. This is safe because: @@ -232,35 +224,4 @@ bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { return true; } -// Wake the main loop from another FreeRTOS task. NOT ISR-safe. -void esphome_lwip_wake_main_loop(void) { - TaskHandle_t task = s_main_loop_task; - if (task != NULL) { - xTaskNotifyGive(task); - } -} - -// Wake the main loop from an ISR. ISR-safe variant. -void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { - TaskHandle_t task = s_main_loop_task; - if (task != NULL) { - vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); - } -} - -// Wake the main loop from any context (ISR, thread, or main loop). -// ESP32-only: uses xPortInIsrContext() to detect ISR context. -// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_lwip_wake_main_loop(); - } -} -#endif - #endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 50706ba9f69..20ac191673f 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -20,10 +20,6 @@ enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 }; extern "C" { #endif -/// Initialize fast select — must be called from the main loop task during setup(). -/// Saves the current task handle for xTaskNotifyGive() wake notifications. -void esphome_lwip_fast_select_init(void); - /// Look up a LwIP socket struct from a file descriptor. /// Returns NULL if fd is invalid or the socket/netconn is not initialized. /// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data(). @@ -57,15 +53,6 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { /// The sock pointer must have been obtained from esphome_lwip_get_sock(). void esphome_lwip_hook_socket(struct lwip_sock *sock); -/// Wake the main loop task from another FreeRTOS task — costs <1 us. -/// NOT ISR-safe — must only be called from task context. -void esphome_lwip_wake_main_loop(void); - -/// Wake the main loop task from an ISR — costs <1 us. -/// ISR-safe variant using vTaskNotifyGiveFromISR(). -/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. -void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); - /// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. /// Must be called with the TCPIP core lock held (LwIPLock in C++). /// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, @@ -73,13 +60,6 @@ void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); /// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); -/// Wake the main loop task from any context (ISR, thread, or main loop). -/// ESP32-only: uses xPortInIsrContext() to detect ISR context. -/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void esphome_lwip_wake_main_loop_any_context(void); -#endif - #ifdef __cplusplus } #endif diff --git a/esphome/core/main_task.c b/esphome/core/main_task.c new file mode 100644 index 00000000000..52d9c2951a6 --- /dev/null +++ b/esphome/core/main_task.c @@ -0,0 +1,5 @@ +#include "esphome/core/main_task.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +TaskHandle_t esphome_main_task_handle = NULL; +#endif diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h new file mode 100644 index 00000000000..ed2885d2e25 --- /dev/null +++ b/esphome/core/main_task.h @@ -0,0 +1,55 @@ +#pragma once + +/// Main loop task handle and wake helpers — shared between wake.h (C++) and lwip_fast_select.c (C). +/// esphome_main_task_handle is set once during Application::setup() via xTaskGetCurrentTaskHandle(). + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#ifdef USE_ESP32 +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#else +#include <FreeRTOS.h> +#include <task.h> +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +extern TaskHandle_t esphome_main_task_handle; + +/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. +static inline void esphome_main_task_notify() { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +/// Wake the main loop task from an ISR. ISR-safe. +static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken); + } +} + +#ifdef USE_ESP32 +/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). +static inline void esphome_main_task_notify_any_context() { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_main_task_notify(); + } +} +#endif + +#ifdef __cplusplus +} +#endif + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp new file mode 100644 index 00000000000..b6b59b59909 --- /dev/null +++ b/esphome/core/wake.cpp @@ -0,0 +1,82 @@ +#include "esphome/core/wake.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include <coredecls.h> +#endif + +#ifdef USE_HOST +#include "esphome/core/application.h" +#include <sys/socket.h> +#endif + +namespace esphome { + +// === ESP32 — IRAM_ATTR entry points === +#ifdef USE_ESP32 +void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { + esphome_main_task_notify_from_isr(px_higher_priority_task_woken); +} +void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } +#endif + +// === ESP8266 / RP2040 === +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile bool g_main_loop_woke = false; +#endif + +#ifdef USE_ESP8266 +void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); } +#endif + +// === RP2040 — wakeable_delay (needs file-scope state for alarm callback) === +#ifdef USE_RP2040 +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback_(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + __sev(); + return 0; +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} +} // namespace internal +#endif // USE_RP2040 + +// === Host (UDP loopback socket) === +#ifdef USE_HOST +void wake_loop_threadsafe() { + if (App.wake_socket_fd_ >= 0) { + const char dummy = 1; + ::send(App.wake_socket_fd_, &dummy, 1, 0); + } +} +#endif + +} // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h new file mode 100644 index 00000000000..a8c9b7ad08b --- /dev/null +++ b/esphome/core/wake.h @@ -0,0 +1,126 @@ +#pragma once + +/// @file wake.h +/// Platform-specific main loop wake primitives. +/// Always available on all platforms — no opt-in needed. + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "esphome/core/main_task.h" +#endif +#ifdef USE_ESP8266 +#include <coredecls.h> +#elif defined(USE_RP2040) +#include <hardware/sync.h> +#include <pico/time.h> +#endif + +namespace esphome { + +// === Wake flag for ESP8266/RP2040 === +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern volatile bool g_main_loop_woke; +#endif + +// === ESP32 / LibreTiny (FreeRTOS) === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#ifdef USE_ESP32 +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_any_context(); +#else +/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not +/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake +/// is not possible. xTaskNotifyGive is used as the best available option. +inline void wake_loop_any_context() { esphome_main_task_notify(); } +#endif + +inline void wake_loop_threadsafe() { esphome_main_task_notify(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} +} // namespace internal + +// === ESP8266 === +#elif defined(USE_ESP8266) + +/// Inline implementation — IRAM callers inline this directly. +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { + g_main_loop_woke = true; + esp_schedule(); +} + +/// IRAM_ATTR entry point for ISR callers — defined in wake.cpp. +void wake_loop_any_context(); + +/// Non-ISR: always inline. +inline void wake_loop_threadsafe() { wake_loop_impl(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + delay(0); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + esp_delay(ms, []() { return !g_main_loop_woke; }); +} +} // namespace internal + +// === RP2040 === +#elif defined(USE_RP2040) + +inline void wake_loop_any_context() { + g_main_loop_woke = true; + __sev(); +} + +inline void wake_loop_threadsafe() { wake_loop_any_context(); } + +/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp. +namespace internal { +void wakeable_delay(uint32_t ms); +} // namespace internal + +// === Host / Zephyr / other === +#else + +#ifdef USE_HOST +/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. +void wake_loop_threadsafe(); +#else +/// Zephyr is currently the only platform without a wake mechanism. +/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). +/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. +inline void wake_loop_threadsafe() {} +#endif + +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + delay(ms); +} +} // namespace internal + +#endif + +} // namespace esphome diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py deleted file mode 100644 index 0434b3e1b54..00000000000 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ /dev/null @@ -1,127 +0,0 @@ -import pytest - -from esphome.components import socket -from esphome.const import ( - KEY_CORE, - KEY_TARGET_PLATFORM, - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_LN882X, - PLATFORM_RTL87XX, -) -from esphome.core import CORE - - -def _setup_platform(platform=PLATFORM_ESP8266) -> None: - """Set up CORE.data with a platform for testing.""" - CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - _setup_platform() - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - CORE.config = {"ethernet": True} - - # Call again - should not raise or fail - socket.require_wake_loop_threadsafe() - - # Verify state is still True - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Define should not be added since flag was already True - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - _setup_platform() - # Call three times - CORE.config = {"openthread": True} - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added (only once, but we can just check it exists) - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking() -> None: - """Test that wake loop is NOT configured when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"esphome": {"name": "test"}, "logger": {}} - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify CORE.data flag was NOT set (since has_networking returns False) - assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data - - # Verify the define was NOT added - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None: - """Test that no socket is consumed when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"logger": {}} - - # Track initial socket consumer state - initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify no socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - assert udp_consumers == initial_udp - - -@pytest.mark.parametrize( - "platform", - [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X], -) -def test_require_wake_loop_threadsafe__fast_select_no_udp_socket( - platform: str, -) -> None: - """Test that fast select platforms use task notifications instead of UDP socket.""" - _setup_platform(platform) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify the define was added - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - # Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications) - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - - -def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None: - """Test that platforms without fast select consume a UDP socket for wake notifications.""" - _setup_platform(PLATFORM_ESP8266) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify UDP socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert udp_consumers.get("socket.wake_loop_threadsafe") == 1 From 95e2b0a8b051e989e7d01e26dffbff2a65411354 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:02:20 -0400 Subject: [PATCH 1245/2056] [multiple] Add missing device_class to sensor schemas (#15479) --- esphome/components/alpha3/sensor.py | 13 +++++++++++++ esphome/components/atm90e26/sensor.py | 2 ++ esphome/components/atm90e32/sensor.py | 2 ++ esphome/components/growatt_solar/sensor.py | 6 ++++++ esphome/components/havells_solar/sensor.py | 5 +++++ esphome/components/hydreon_rgxx/sensor.py | 2 ++ esphome/components/mlx90393/sensor.py | 2 ++ esphome/components/pipsolar/sensor/__init__.py | 13 +++++++++++++ esphome/components/pzemac/sensor.py | 2 ++ esphome/components/sdm_meter/sensor.py | 9 ++++++++- esphome/components/selec_meter/sensor.py | 5 +++++ esphome/components/teleinfo/sensor/__init__.py | 10 +++++++++- 12 files changed, 69 insertions(+), 2 deletions(-) diff --git a/esphome/components/alpha3/sensor.py b/esphome/components/alpha3/sensor.py index 361e1d101f0..279ab214cf0 100644 --- a/esphome/components/alpha3/sensor.py +++ b/esphome/components/alpha3/sensor.py @@ -9,6 +9,10 @@ from esphome.const import ( CONF_POWER, CONF_SPEED, CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CUBIC_METER_PER_HOUR, UNIT_METER, @@ -27,26 +31,35 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FLOW): sensor.sensor_schema( unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HEAD): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index 4522e94846f..5941cb35b42 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_REACTIVE_POWER, @@ -103,6 +104,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index a510095217a..09449504320 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -20,6 +20,7 @@ from esphome.const import ( DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_REACTIVE_POWER, @@ -166,6 +167,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 19f3adfd0e1..7458b88b724 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -12,7 +12,9 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -53,6 +55,7 @@ PHASE_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -72,6 +75,7 @@ PV_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -118,6 +122,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( @@ -147,6 +152,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 532315a1d16..a876acd79b6 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, @@ -64,6 +65,7 @@ PHASE_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -77,6 +79,7 @@ PV_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -123,6 +126,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( @@ -171,6 +175,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index f270b72e24d..fdb606182f5 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, + DEVICE_CLASS_TEMPERATURE, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -117,6 +118,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=0, icon=ICON_THERMOMETER, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DISABLE_LED): cv.boolean, diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index 293a133c3d9..a6330b1cc00 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_RESOLUTION, CONF_TEMPERATURE, CONF_TEMPERATURE_COMPENSATION, + DEVICE_CLASS_TEMPERATURE, ICON_MAGNET, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, @@ -107,6 +108,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, icon=ICON_THERMOMETER, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( cv.Schema( diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 8d3ba10d62d..88c6566d63e 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -102,46 +102,56 @@ TYPES = { unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=0, device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_TYPE: sensor.sensor_schema( accuracy_decimals=0, @@ -150,11 +160,13 @@ TYPES = { unit_of_measurement=UNIT_AMPERE, accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( accuracy_decimals=0, @@ -294,6 +306,7 @@ TYPES = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_EEPROM_VERSION: sensor.sensor_schema( accuracy_decimals=0, diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index fa1c3961d0f..c134bc19c1a 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, @@ -66,6 +67,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index affbc0409e2..9687357f5eb 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -19,8 +19,10 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_TOTAL_POWER, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, @@ -67,6 +69,7 @@ PHASE_SENSORS = { CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( @@ -80,7 +83,10 @@ PHASE_SENSORS = { state_class=STATE_CLASS_MEASUREMENT, ), CONF_PHASE_ANGLE: sensor.sensor_schema( - unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 + unit_of_measurement=UNIT_DEGREES, + icon=ICON_FLASH, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -99,6 +105,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=3, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema( diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 069b61af5a1..eda65bf9f5e 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -14,8 +14,10 @@ from esphome.const import ( CONF_POWER_FACTOR, CONF_REACTIVE_POWER, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, @@ -102,6 +104,7 @@ SENSORS = { CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE: sensor.sensor_schema( @@ -125,6 +128,7 @@ SENSORS = { unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( @@ -141,6 +145,7 @@ SENSORS = { CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index b436c701207..150484d97ad 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -1,6 +1,12 @@ import esphome.codegen as cg from esphome.components import sensor -from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_ENERGY, + ICON_FLASH, + STATE_CLASS_TOTAL_INCREASING, + UNIT_WATT_HOURS, +) from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns @@ -11,6 +17,8 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ).extend(TELEINFO_LISTENER_SCHEMA) From 50518918130b4a58e0860e0732bbb81fc415a826 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:02:28 -0400 Subject: [PATCH 1246/2056] [esp32] Fix ESP32-C6 pin validator rejecting GPIO 24-30 with wrong error (#15477) --- esphome/components/esp32/gpio_esp32_c6.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index 993606d9dee..bd7bb9e2208 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) def esp32_c6_validate_gpio_pin(value: int) -> int: - if value < 0 or value > 23: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") + if value < 0 or value > 30: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)") if value in _ESP32C6_SPI_PSRAM_PINS: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" @@ -47,8 +47,8 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: mode = value[CONF_MODE] is_input = mode[CONF_INPUT] - if num < 0 or num > 23: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)") + if num < 0 or num > 30: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)") if is_input: # All ESP32 pins support input mode pass From 8650c5b013a8a4c0d07fb021c8ba95ded0150194 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:19:20 -0400 Subject: [PATCH 1247/2056] [multiple] Add missing state_class to sensor schemas (#15478) --- esphome/components/dsmr/sensor.py | 10 ++++++++++ esphome/components/ltr390/sensor.py | 5 +++++ esphome/components/mopeka_pro_check/sensor.py | 1 + esphome/components/rotary_encoder/sensor.py | 2 ++ esphome/components/seeed_mr24hpc1/sensor.py | 4 ++++ esphome/components/xiaomi_cgpr1/binary_sensor.py | 4 ++++ esphome/components/xiaomi_mjyd02yla/binary_sensor.py | 1 + 7 files changed, 27 insertions(+) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 863af42d1b7..c49614eaa98 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -122,42 +122,52 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("total_imported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff4"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff4"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("power_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py index 579adb90516..37fceaf9844 100644 --- a/esphome/components/ltr390/sensor.py +++ b/esphome/components/ltr390/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, + STATE_CLASS_MEASUREMENT, UNIT_LUX, ) @@ -57,24 +58,28 @@ CONFIG_SCHEMA = cv.All( icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AMBIENT_LIGHT): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( unit_of_measurement=UNIT_UVI, icon=ICON_BRIGHTNESS_5, accuracy_decimals=5, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_UV): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_GAIN, default="X18"): cv.Any( cv.enum(GAIN_OPTIONS), diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py index 4e84fb708cd..323175917dc 100644 --- a/esphome/components/mopeka_pro_check/sensor.py +++ b/esphome/components/mopeka_pro_check/sensor.py @@ -115,6 +115,7 @@ CONFIG_SCHEMA = ( icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MINIMUM_SIGNAL_QUALITY, default="MEDIUM"): cv.enum( SIGNAL_QUALITIES, upper=True diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index e64e44f7c13..fc4202556d0 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_RESTORE_MODE, CONF_VALUE, ICON_ROTATE_RIGHT, + STATE_CLASS_MEASUREMENT, UNIT_STEPS, ) @@ -60,6 +61,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_STEPS, icon=ICON_ROTATE_RIGHT, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/seeed_mr24hpc1/sensor.py b/esphome/components/seeed_mr24hpc1/sensor.py index 7b20941aa47..ca15fd5be68 100644 --- a/esphome/components/seeed_mr24hpc1/sensor.py +++ b/esphome/components/seeed_mr24hpc1/sensor.py @@ -5,6 +5,7 @@ from esphome.const import ( DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ENERGY, DEVICE_CLASS_SPEED, + STATE_CLASS_MEASUREMENT, UNIT_METER, ) @@ -26,6 +27,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, # Specify the number of decimal places icon="mdi:signal-distance-variant", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOVEMENT_SIGNS): sensor.sensor_schema( icon="mdi:human-greeting-variant", @@ -34,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, icon="mdi:signal-distance-variant", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CUSTOM_SPATIAL_STATIC_VALUE): sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, @@ -48,6 +51,7 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_SPEED, accuracy_decimals=2, icon="mdi:run-fast", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CUSTOM_MODE_NUM): sensor.sensor_schema( icon="mdi:counter", diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py index 2f71a11b28f..0606c93dbec 100644 --- a/esphome/components/xiaomi_cgpr1/binary_sensor.py +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMELAPSE, + STATE_CLASS_MEASUREMENT, UNIT_LUX, UNIT_MINUTE, UNIT_PERCENT, @@ -38,6 +39,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( @@ -45,11 +47,13 @@ CONFIG_SCHEMA = cv.All( icon=ICON_TIMELAPSE, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( unit_of_measurement=UNIT_LUX, accuracy_decimals=0, device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 312f8b43b19..312abc82cb3 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -43,6 +43,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MINUTE, icon=ICON_TIMELAPSE, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, From c78fb964a2ba058c1909df95a6e0cd6a1885ce4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:15:42 -0400 Subject: [PATCH 1248/2056] [multiple] Add missing state_class to remaining sensor schemas (#15486) --- esphome/components/am43/sensor/__init__.py | 3 +++ esphome/components/as3935/sensor.py | 2 ++ esphome/components/cs5460a/sensor.py | 4 ++++ esphome/components/debug/sensor.py | 8 ++++++++ esphome/components/hmc5883l/sensor.py | 1 + esphome/components/ld2420/sensor/__init__.py | 5 ++++- esphome/components/max31855/sensor.py | 1 + esphome/components/mmc5603/sensor.py | 1 + esphome/components/pylontech/sensor/__init__.py | 10 ++++++++++ esphome/components/qmc5883l/sensor.py | 1 + esphome/components/shelly_dimmer/light.py | 4 ++++ esphome/components/sun/sensor/__init__.py | 8 +++++++- esphome/components/sun_gtil2/sensor.py | 7 +++++++ esphome/components/tx20/sensor.py | 1 + 14 files changed, 54 insertions(+), 2 deletions(-) diff --git a/esphome/components/am43/sensor/__init__.py b/esphome/components/am43/sensor/__init__.py index 4b3e1716a47..2697d364ad8 100644 --- a/esphome/components/am43/sensor/__init__.py +++ b/esphome/components/am43/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, ENTITY_CATEGORY_DIAGNOSTIC, ICON_BRIGHTNESS_5, + STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ) @@ -26,11 +27,13 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_BATTERY, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, icon=ICON_BRIGHTNESS_5, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 1e549c5d82a..9b43155563d 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_LIGHTNING_ENERGY, ICON_FLASH, ICON_SIGNAL_DISTANCE_VARIANT, + STATE_CLASS_MEASUREMENT, UNIT_KILOMETER, ) @@ -20,6 +21,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_KILOMETER, icon=ICON_SIGNAL_DISTANCE_VARIANT, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( icon=ICON_FLASH, diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index d2383bd01b8..0c6ae0d8217 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_VOLT, UNIT_WATT, @@ -82,16 +83,19 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 0a716d666e7..af1b83190d8 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -14,6 +14,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, + STATE_CLASS_MEASUREMENT, UNIT_BYTES, UNIT_HERTZ, UNIT_MILLISECOND, @@ -38,12 +39,14 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BLOCK): sensor.sensor_schema( unit_of_measurement=UNIT_BYTES, icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Any( @@ -59,6 +62,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=1, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_MIN_FREE): cv.All( @@ -72,6 +76,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( @@ -79,6 +84,7 @@ CONFIG_SCHEMA = { icon=ICON_TIMER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PSRAM): cv.All( cv.only_on_esp32, @@ -88,6 +94,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_CPU_FREQUENCY): cv.All( @@ -96,6 +103,7 @@ CONFIG_SCHEMA = { icon="mdi:speedometer", accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), } diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 96d03130081..cf3c594f360 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -87,6 +87,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index 6dde35753a7..97acdabd7ba 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, CONF_MOVING_DISTANCE, DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, UNIT_CENTIMETER, ) @@ -20,7 +21,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(LD2420Sensor), cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + state_class=STATE_CLASS_MEASUREMENT, ), } ), diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index 93e48beee02..35ae28d04ca 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -19,6 +19,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/mmc5603/sensor.py b/esphome/components/mmc5603/sensor.py index 5b3982cee68..6d2bafdd0e6 100644 --- a/esphome/components/mmc5603/sensor.py +++ b/esphome/components/mmc5603/sensor.py @@ -45,6 +45,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 52f2679b707..450f663274f 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,46 +33,55 @@ TYPES: dict[str, cv.Schema] = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=3, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE_LOW: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE_HIGH: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_LOW: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_HIGH: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_COULOMB: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MOS_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index b79e370a052..fe34381ad87 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -100,6 +100,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) temperature_schema = sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index c96bc380d77..c1e9cad3587 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -22,6 +22,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_VOLT, UNIT_WATT, @@ -163,16 +164,19 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_WATT, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, device_class=DEVICE_CLASS_CURRENT, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), # Change the default gamma_correct setting. cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index a356d9cca82..a1ced8ff5b5 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import CONF_TYPE, ICON_WEATHER_SUNSET, UNIT_DEGREES +from esphome.const import ( + CONF_TYPE, + ICON_WEATHER_SUNSET, + STATE_CLASS_MEASUREMENT, + UNIT_DEGREES, +) from .. import CONF_SUN_ID, Sun, sun_ns @@ -20,6 +25,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_DEGREES, icon=ICON_WEATHER_SUNSET, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sun_gtil2/sensor.py b/esphome/components/sun_gtil2/sensor.py index d8c59bf1de0..55c8195391f 100644 --- a/esphome/components/sun_gtil2/sensor.py +++ b/esphome/components/sun_gtil2/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_FLASH, ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_VOLT, UNIT_WATT, @@ -30,36 +31,42 @@ CONFIG_SCHEMA = cv.All( icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DC_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AC_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DC_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIMITER_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 4f1582072e6..87bc4283b73 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -30,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SIGN_DIRECTION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } From 6f62b2f18c702a9a8752ffaa9f0161d5b782a85d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:20:38 -0400 Subject: [PATCH 1249/2056] [thermostat] Remove non-functional cv.templatable from preset fields (#15481) --- esphome/components/thermostat/climate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index ec115296d71..d609e22ac26 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -118,10 +118,8 @@ PRESET_CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MODE): validate_climate_mode, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode), - cv.Optional(CONF_SWING_MODE): cv.templatable( - climate.validate_climate_swing_mode - ), + cv.Optional(CONF_FAN_MODE): climate.validate_climate_fan_mode, + cv.Optional(CONF_SWING_MODE): climate.validate_climate_swing_mode, } ) @@ -631,7 +629,7 @@ CONFIG_SCHEMA = cv.All( ): automation.validate_automation(single=True), cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage, cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, - cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), + cv.Optional(CONF_DEFAULT_PRESET): cv.string, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( From 5a14d6a4add88e80facb2890a7cf58fab22ea453 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:38:47 -0400 Subject: [PATCH 1250/2056] [multiple] Add missing device_class to sensor schemas (batch 2) (#15487) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/debug/sensor.py | 2 ++ esphome/components/havells_solar/sensor.py | 6 ++++++ esphome/components/sdm_meter/sensor.py | 2 ++ esphome/components/selec_meter/sensor.py | 3 +++ esphome/components/tx20/sensor.py | 2 ++ esphome/components/xiaomi_miscale/sensor.py | 2 ++ 6 files changed, 17 insertions(+) diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index af1b83190d8..a018ce5c3bd 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_FRAGMENTATION, CONF_FREE, CONF_LOOP_TIME, + DEVICE_CLASS_FREQUENCY, ENTITY_CATEGORY_DIAGNOSTIC, ICON_COUNTER, ICON_TIMER, @@ -102,6 +103,7 @@ CONFIG_SCHEMA = { unit_of_measurement=UNIT_HERTZ, icon="mdi:speedometer", accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index a876acd79b6..f0683e1d9c1 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -138,6 +139,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( @@ -186,21 +188,25 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 9687357f5eb..8006d0b4ba8 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -25,6 +25,7 @@ from esphome.const import ( DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_FLASH, @@ -75,6 +76,7 @@ PHASE_SENSORS = { CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index eda65bf9f5e..1a53eb5c373 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -20,6 +20,7 @@ from esphome.const import ( DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -99,6 +100,7 @@ SENSORS = { CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( @@ -140,6 +142,7 @@ SENSORS = { CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 87bc4283b73..1bb5ab07064 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_PIN, CONF_WIND_DIRECTION_DEGREES, CONF_WIND_SPEED, + DEVICE_CLASS_WIND_SPEED, ICON_SIGN_DIRECTION, ICON_WEATHER_WINDY, STATE_CLASS_MEASUREMENT, @@ -24,6 +25,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, icon=ICON_WEATHER_WINDY, accuracy_decimals=1, + device_class=DEVICE_CLASS_WIND_SPEED, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema( diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 4aa8d029f8d..14e5c1d376c 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_IMPEDANCE, CONF_MAC_ADDRESS, CONF_WEIGHT, + DEVICE_CLASS_WEIGHT, ICON_OMEGA, ICON_SCALE_BATHROOM, STATE_CLASS_MEASUREMENT, @@ -31,6 +32,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOGRAM, icon=ICON_SCALE_BATHROOM, accuracy_decimals=2, + device_class=DEVICE_CLASS_WEIGHT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( From 2b5ee69eb27a2f1aa0d6d473caaab7dc71d6e626 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 12:42:18 -1000 Subject: [PATCH 1251/2056] [api] Speed up protobuf encode 17-20% with register-optimized write path (#15290) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 4 +- esphome/components/api/api_connection.h | 6 +- esphome/components/api/api_pb2.cpp | 1455 ++++++++++++--------- esphome/components/api/api_pb2.h | 186 +-- esphome/components/api/proto.cpp | 17 +- esphome/components/api/proto.h | 407 +++--- script/api_protobuf/api_protobuf.py | 121 +- 7 files changed, 1257 insertions(+), 939 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0f456ecd0c7..feb16e4f4c4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1993,7 +1993,7 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M size_t write_start = shared_buf.size(); shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; - encode_fn(msg, buffer); + encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } // Encodes a message to the buffer and returns the total number of bytes used, @@ -2034,7 +2034,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode shared_buf.resize(shared_buf.size() + to_add); ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; - encode_fn(msg, buffer); + encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); // Return total size (header + payload + footer) return static_cast<uint16_t>(total_calculated_size); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5a86240ab56..2f685b0b8a5 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -324,7 +324,7 @@ class APIConnection final : public APIServerConnectionBase { void on_no_setup_connection(); // Function pointer type for type-erased message encoding - using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + using MessageEncodeFn = uint8_t *(*) (const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM); // Function pointer type for type-erased size calculation using CalculateSizeFn = uint32_t (*)(const void *); @@ -403,7 +403,9 @@ class APIConnection final : public APIServerConnectionBase { } // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) - static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} + static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) { + return buf.get_pos(); + } // Non-template buffer management for send_message bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f25d269e8f3..ed4711bfaac 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,11 +31,13 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->api_version_major); - buffer.encode_uint32(2, this->api_version_minor); - buffer.encode_string(3, this->server_info); - buffer.encode_string(4, this->name); +uint8_t *HelloResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->api_version_major); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->api_version_minor); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->server_info); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->name); + return pos; } uint32_t HelloResponse::calculate_size() const { uint32_t size = 0; @@ -46,9 +48,11 @@ uint32_t HelloResponse::calculate_size() const { return size; } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->area_id); - buffer.encode_string(2, this->name); +uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->area_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + return pos; } uint32_t AreaInfo::calculate_size() const { uint32_t size = 0; @@ -58,10 +62,12 @@ uint32_t AreaInfo::calculate_size() const { } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->device_id); - buffer.encode_string(2, this->name); - buffer.encode_uint32(3, this->area_id); +uint8_t *DeviceInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->area_id); + return pos; } uint32_t DeviceInfo::calculate_size() const { uint32_t size = 0; @@ -72,9 +78,11 @@ uint32_t DeviceInfo::calculate_size() const { } #endif #ifdef USE_SERIAL_PROXY -void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->name); - buffer.encode_uint32(2, static_cast<uint32_t>(this->port_type)); +uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->port_type)); + return pos; } uint32_t SerialProxyInfo::calculate_size() const { uint32_t size = 0; @@ -83,65 +91,67 @@ uint32_t SerialProxyInfo::calculate_size() const { return size; } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(2, this->name); - buffer.encode_string(3, this->mac_address); - buffer.encode_string(4, this->esphome_version); - buffer.encode_string(5, this->compilation_time); - buffer.encode_string(6, this->model); +uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mac_address); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->esphome_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->compilation_time); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->model); #ifdef USE_DEEP_SLEEP - buffer.encode_bool(7, this->has_deep_sleep); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(8, this->project_name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(9, this->project_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->project_version); #endif #ifdef USE_WEBSERVER - buffer.encode_uint32(10, this->webserver_port); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, this->bluetooth_proxy_feature_flags); #endif - buffer.encode_string(12, this->manufacturer); - buffer.encode_string(13, this->friendly_name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, this->manufacturer); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->friendly_name); #ifdef USE_VOICE_ASSISTANT - buffer.encode_uint32(17, this->voice_assistant_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 17, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - buffer.encode_string(16, this->suggested_area); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_string(18, this->bluetooth_mac_address); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - buffer.encode_bool(19, this->api_encryption_supported); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 19, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_sub_message(20, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_sub_message(21, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 21, it); } #endif #ifdef USE_AREAS - buffer.encode_optional_sub_message(22, this->area); + ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 22, this->area); #endif #ifdef USE_ZWAVE_PROXY - buffer.encode_uint32(23, this->zwave_proxy_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 23, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - buffer.encode_uint32(24, this->zwave_home_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 24, this->zwave_home_id); #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - buffer.encode_sub_message(25, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it); } #endif + return pos; } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -206,20 +216,22 @@ uint32_t DeviceInfoResponse::calculate_size() const { return size; } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(5, this->device_class); - buffer.encode_bool(6, this->is_status_binary_sensor); - buffer.encode_bool(7, this->disabled_by_default); +uint8_t *ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->is_status_binary_sensor); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(8, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->icon); #endif - buffer.encode_uint32(9, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; @@ -238,13 +250,15 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { #endif return size; } -void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t BinarySensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -258,23 +272,25 @@ uint32_t BinarySensorStateResponse::calculate_size() const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_bool(5, this->assumed_state); - buffer.encode_bool(6, this->supports_position); - buffer.encode_bool(7, this->supports_tilt); - buffer.encode_string(8, this->device_class); - buffer.encode_bool(9, this->disabled_by_default); +uint8_t *ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_position); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_tilt); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->icon); #endif - buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); - buffer.encode_bool(12, this->supports_stop); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supports_stop); #ifdef USE_DEVICES - buffer.encode_uint32(13, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_id); #endif + return pos; } uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; @@ -296,14 +312,16 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { #endif return size; } -void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_float(3, this->position); - buffer.encode_float(4, this->tilt); - buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation)); +uint8_t *CoverStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->position); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->tilt); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } uint32_t CoverStateResponse::calculate_size() const { uint32_t size = 0; @@ -355,25 +373,27 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_bool(5, this->supports_oscillation); - buffer.encode_bool(6, this->supports_speed); - buffer.encode_bool(7, this->supports_direction); - buffer.encode_int32(8, this->supported_speed_count); - buffer.encode_bool(9, this->disabled_by_default); +uint8_t *ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_oscillation); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_speed); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_direction); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supported_speed_count); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->icon); #endif - buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(this->entity_category)); for (const char *it : *this->supported_preset_modes) { - buffer.encode_string(12, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, it, strlen(it), true); } #ifdef USE_DEVICES - buffer.encode_uint32(13, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_id); #endif + return pos; } uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; @@ -399,16 +419,18 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { #endif return size; } -void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); - buffer.encode_bool(3, this->oscillating); - buffer.encode_uint32(5, static_cast<uint32_t>(this->direction)); - buffer.encode_int32(6, this->speed_level); - buffer.encode_string(7, this->preset_mode); +uint8_t *FanStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->oscillating); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, static_cast<uint32_t>(this->direction)); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->speed_level); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, this->preset_mode); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t FanStateResponse::calculate_size() const { uint32_t size = 0; @@ -485,26 +507,28 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); for (const auto &it : *this->supported_color_modes) { - buffer.encode_uint32(12, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast<uint32_t>(it), true); } - buffer.encode_float(9, this->min_mireds); - buffer.encode_float(10, this->max_mireds); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->min_mireds); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->max_mireds); for (const char *it : *this->effects) { - buffer.encode_string(11, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it), true); } - buffer.encode_bool(13, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(14, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 14, this->icon); #endif - buffer.encode_uint32(15, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(16, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, this->device_id); #endif + return pos; } uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; @@ -533,23 +557,25 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { #endif return size; } -void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); - buffer.encode_float(3, this->brightness); - buffer.encode_uint32(11, static_cast<uint32_t>(this->color_mode)); - buffer.encode_float(10, this->color_brightness); - buffer.encode_float(4, this->red); - buffer.encode_float(5, this->green); - buffer.encode_float(6, this->blue); - buffer.encode_float(7, this->white); - buffer.encode_float(8, this->color_temperature); - buffer.encode_float(12, this->cold_white); - buffer.encode_float(13, this->warm_white); - buffer.encode_string(9, this->effect); +uint8_t *LightStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->brightness); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(this->color_mode)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->color_brightness); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->red); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->green); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->blue); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->white); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->color_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 12, this->cold_white); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 13, this->warm_white); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->effect); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } uint32_t LightStateResponse::calculate_size() const { uint32_t size = 0; @@ -681,23 +707,25 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_string(6, this->unit_of_measurement); - buffer.encode_int32(7, this->accuracy_decimals); - buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class); - buffer.encode_uint32(10, static_cast<uint32_t>(this->state_class)); - buffer.encode_bool(12, this->disabled_by_default); - buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->unit_of_measurement); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->accuracy_decimals); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->force_update); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_class); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast<uint32_t>(this->state_class)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; @@ -719,13 +747,15 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { #endif return size; } -void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_float(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t SensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -739,20 +769,22 @@ uint32_t SensorStateResponse::calculate_size() const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->assumed_state); - buffer.encode_bool(7, this->disabled_by_default); - buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(9, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; @@ -771,12 +803,14 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { #endif return size; } -void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); +uint8_t *SwitchStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t SwitchStateResponse::calculate_size() const { uint32_t size = 0; @@ -814,19 +848,21 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; @@ -844,13 +880,15 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { #endif return size; } -void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_string(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *TextSensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t TextSensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -876,9 +914,11 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, static_cast<uint32_t>(this->level)); - buffer.encode_bytes(3, this->message_ptr_, this->message_len_); +uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level)); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_); + return pos; } uint32_t SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; @@ -899,7 +939,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint8_t *NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->success); + return pos; +} uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_bool(1, this->success); @@ -907,9 +951,11 @@ uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->key); - buffer.encode_string(2, this->value); +uint8_t *HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->value); + return pos; } uint32_t HomeassistantServiceMap::calculate_size() const { uint32_t size = 0; @@ -917,27 +963,29 @@ uint32_t HomeassistantServiceMap::calculate_size() const { size += ProtoSize::calc_length(1, this->value.size()); return size; } -void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->service); +uint8_t *HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->service); for (auto &it : this->data) { - buffer.encode_sub_message(2, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 2, it); } for (auto &it : this->data_template) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } for (auto &it : this->variables) { - buffer.encode_sub_message(4, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, it); } - buffer.encode_bool(5, this->is_event); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - buffer.encode_uint32(6, this->call_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - buffer.encode_bool(7, this->wants_response); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - buffer.encode_string(8, this->response_template); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->response_template); #endif + return pos; } uint32_t HomeassistantActionRequest::calculate_size() const { uint32_t size = 0; @@ -1004,10 +1052,12 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->entity_id); - buffer.encode_string(2, this->attribute); - buffer.encode_bool(3, this->once); +uint8_t *SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->entity_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->attribute); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->once); + return pos; } uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { uint32_t size = 0; @@ -1112,9 +1162,11 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->name); - buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); +uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type)); + return pos; } uint32_t ListEntitiesServicesArgument::calculate_size() const { uint32_t size = 0; @@ -1122,13 +1174,15 @@ uint32_t ListEntitiesServicesArgument::calculate_size() const { size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); return size; } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->name); - buffer.write_tag_and_fixed32(21, this->key); +uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); for (auto &it : this->args) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } - buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response)); + return pos; } uint32_t ListEntitiesServicesResponse::calculate_size() const { uint32_t size = 0; @@ -1247,13 +1301,15 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->call_id); - buffer.encode_bool(2, this->success); - buffer.encode_string(3, this->error_message); +uint8_t *ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->call_id); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - buffer.encode_bytes(4, this->response_data, this->response_data_len); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 4, this->response_data, this->response_data_len); #endif + return pos; } uint32_t ExecuteServiceResponse::calculate_size() const { uint32_t size = 0; @@ -1267,18 +1323,20 @@ uint32_t ExecuteServiceResponse::calculate_size() const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_bool(5, this->disabled_by_default); +uint8_t *ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(6, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->icon); #endif - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; @@ -1295,13 +1353,15 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { #endif return size; } -void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bytes(2, this->data_ptr_, this->data_len_); - buffer.encode_bool(3, this->done); +uint8_t *CameraImageResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->done); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t CameraImageResponse::calculate_size() const { uint32_t size = 0; @@ -1328,48 +1388,50 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_bool(5, this->supports_current_temperature); - buffer.encode_bool(6, this->supports_two_point_target_temperature); +uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_current_temperature); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - buffer.encode_uint32(7, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(it), true); } - buffer.encode_float(8, this->visual_min_temperature); - buffer.encode_float(9, this->visual_max_temperature); - buffer.encode_float(10, this->visual_target_temperature_step); - buffer.encode_bool(12, this->supports_action); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->visual_min_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->visual_max_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->visual_target_temperature_step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supports_action); for (const auto &it : *this->supported_fan_modes) { - buffer.encode_uint32(13, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(it), true); } for (const auto &it : *this->supported_swing_modes) { - buffer.encode_uint32(14, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, static_cast<uint32_t>(it), true); } for (const char *it : *this->supported_custom_fan_modes) { - buffer.encode_string(15, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 15, it, strlen(it), true); } for (const auto &it : *this->supported_presets) { - buffer.encode_uint32(16, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, static_cast<uint32_t>(it), true); } for (const char *it : *this->supported_custom_presets) { - buffer.encode_string(17, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 17, it, strlen(it), true); } - buffer.encode_bool(18, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 18, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(19, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 19, this->icon); #endif - buffer.encode_uint32(20, static_cast<uint32_t>(this->entity_category)); - buffer.encode_float(21, this->visual_current_temperature_step); - buffer.encode_bool(22, this->supports_current_humidity); - buffer.encode_bool(23, this->supports_target_humidity); - buffer.encode_float(24, this->visual_min_humidity); - buffer.encode_float(25, this->visual_max_humidity); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 20, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 21, this->visual_current_temperature_step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 22, this->supports_current_humidity); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 23, this->supports_target_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 24, this->visual_min_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 25, this->visual_max_humidity); #ifdef USE_DEVICES - buffer.encode_uint32(26, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id); #endif - buffer.encode_uint32(27, this->feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags); + return pos; } uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; @@ -1428,24 +1490,26 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { size += ProtoSize::calc_uint32(2, this->feature_flags); return size; } -void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); - buffer.encode_float(3, this->current_temperature); - buffer.encode_float(4, this->target_temperature); - buffer.encode_float(5, this->target_temperature_low); - buffer.encode_float(6, this->target_temperature_high); - buffer.encode_uint32(8, static_cast<uint32_t>(this->action)); - buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode)); - buffer.encode_uint32(10, static_cast<uint32_t>(this->swing_mode)); - buffer.encode_string(11, this->custom_fan_mode); - buffer.encode_uint32(12, static_cast<uint32_t>(this->preset)); - buffer.encode_string(13, this->custom_preset); - buffer.encode_float(14, this->current_humidity); - buffer.encode_float(15, this->target_humidity); +uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->mode)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->current_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->target_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->target_temperature_low); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->target_temperature_high); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast<uint32_t>(this->action)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, static_cast<uint32_t>(this->fan_mode)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast<uint32_t>(this->swing_mode)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->custom_fan_mode); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast<uint32_t>(this->preset)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->custom_preset); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 14, this->current_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 15, this->target_humidity); #ifdef USE_DEVICES - buffer.encode_uint32(16, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, this->device_id); #endif + return pos; } uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; @@ -1561,25 +1625,27 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(4, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif - buffer.encode_bool(5, this->disabled_by_default); - buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(7, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id); #endif - buffer.encode_float(8, this->min_temperature); - buffer.encode_float(9, this->max_temperature); - buffer.encode_float(10, this->target_temperature_step); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->min_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->max_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->target_temperature_step); for (const auto &it : *this->supported_modes) { - buffer.encode_uint32(11, static_cast<uint32_t>(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true); } - buffer.encode_uint32(12, this->supported_features); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features); + return pos; } uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; @@ -1605,17 +1671,19 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->supported_features); return size; } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_float(2, this->current_temperature); - buffer.encode_float(3, this->target_temperature); - buffer.encode_uint32(4, static_cast<uint32_t>(this->mode)); +uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->current_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->target_temperature); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - buffer.encode_uint32(5, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_id); #endif - buffer.encode_uint32(6, this->state); - buffer.encode_float(7, this->target_temperature_low); - buffer.encode_float(8, this->target_temperature_high); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->state); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->target_temperature_low); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->target_temperature_high); + return pos; } uint32_t WaterHeaterStateResponse::calculate_size() const { uint32_t size = 0; @@ -1673,24 +1741,26 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_float(6, this->min_value); - buffer.encode_float(7, this->max_value); - buffer.encode_float(8, this->step); - buffer.encode_bool(9, this->disabled_by_default); - buffer.encode_uint32(10, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(11, this->unit_of_measurement); - buffer.encode_uint32(12, static_cast<uint32_t>(this->mode)); - buffer.encode_string(13, this->device_class); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->min_value); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->max_value); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->unit_of_measurement); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast<uint32_t>(this->mode)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; @@ -1713,13 +1783,15 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { #endif return size; } -void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_float(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *NumberStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t NumberStateResponse::calculate_size() const { uint32_t size = 0; @@ -1758,21 +1830,23 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif for (const char *it : *this->options) { - buffer.encode_string(6, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, it, strlen(it), true); } - buffer.encode_bool(7, this->disabled_by_default); - buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; @@ -1794,13 +1868,15 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { #endif return size; } -void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_string(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *SelectStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t SelectStateResponse::calculate_size() const { uint32_t size = 0; @@ -1847,23 +1923,25 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); for (const char *it : *this->tones) { - buffer.encode_string(7, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, it, strlen(it), true); } - buffer.encode_bool(8, this->supports_duration); - buffer.encode_bool(9, this->supports_volume); - buffer.encode_uint32(10, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_duration); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->supports_volume); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; @@ -1887,12 +1965,14 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { #endif return size; } -void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); +uint8_t *SirenStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t SirenStateResponse::calculate_size() const { uint32_t size = 0; @@ -1959,22 +2039,24 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_bool(8, this->assumed_state); - buffer.encode_bool(9, this->supports_open); - buffer.encode_bool(10, this->requires_code); - buffer.encode_string(11, this->code_format); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->supports_open); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->requires_code); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->code_format); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; @@ -1995,12 +2077,14 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { #endif return size; } -void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); +uint8_t *LockStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; @@ -2052,19 +2136,21 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; @@ -2106,12 +2192,14 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->format); - buffer.encode_uint32(2, this->sample_rate); - buffer.encode_uint32(3, this->num_channels); - buffer.encode_uint32(4, static_cast<uint32_t>(this->purpose)); - buffer.encode_uint32(5, this->sample_bytes); +uint8_t *MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->format); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->sample_rate); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->num_channels); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->purpose)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->sample_bytes); + return pos; } uint32_t MediaPlayerSupportedFormat::calculate_size() const { uint32_t size = 0; @@ -2122,23 +2210,25 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { size += ProtoSize::calc_uint32(1, this->sample_bytes); return size; } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_bool(8, this->supports_pause); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_sub_message(9, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 9, it); } #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif - buffer.encode_uint32(11, this->feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->feature_flags); + return pos; } uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; @@ -2162,14 +2252,16 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->feature_flags); return size; } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); - buffer.encode_float(3, this->volume); - buffer.encode_bool(4, this->muted); +uint8_t *MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->state)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->volume); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 4, this->muted); #ifdef USE_DEVICES - buffer.encode_uint32(5, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_id); #endif + return pos; } uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; @@ -2248,15 +2340,20 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { - buffer.write_raw_byte(8); - buffer.encode_varint_raw_64(this->address); - buffer.write_raw_byte(16); - buffer.encode_varint_raw(encode_zigzag32(this->rssi)); - buffer.encode_uint32(3, this->address_type); - buffer.write_raw_byte(34); - buffer.write_raw_byte(static_cast<uint8_t>(this->data_len)); - buffer.encode_raw(this->data, this->data_len); +uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); + ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); + ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi)); + if (this->address_type) { + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->address_type)); + } + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len)); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len); + return pos; } uint32_t BluetoothLERawAdvertisement::calculate_size() const { uint32_t size = 0; @@ -2266,10 +2363,12 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const { size += 2 + this->data_len; return size; } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_sub_message(1, this->advertisements[i]); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]); } + return pos; } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; @@ -2297,11 +2396,13 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, proto_varint_value } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->connected); - buffer.encode_uint32(3, this->mtu); - buffer.encode_int32(4, this->error); +uint8_t *BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->connected); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mtu); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->error); + return pos; } uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { uint32_t size = 0; @@ -2321,13 +2422,15 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, proto_var } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); if (this->uuid[0] != 0 || this->uuid[1] != 0) { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->short_uuid); + return pos; } uint32_t BluetoothGATTDescriptor::calculate_size() const { uint32_t size = 0; @@ -2339,17 +2442,19 @@ uint32_t BluetoothGATTDescriptor::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); if (this->uuid[0] != 0 || this->uuid[1] != 0) { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->properties); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_sub_message(4, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, it); } - buffer.encode_uint32(5, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->short_uuid); + return pos; } uint32_t BluetoothGATTCharacteristic::calculate_size() const { uint32_t size = 0; @@ -2367,16 +2472,18 @@ uint32_t BluetoothGATTCharacteristic::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTService::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); if (this->uuid[0] != 0 || this->uuid[1] != 0) { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } - buffer.encode_uint32(4, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->short_uuid); + return pos; } uint32_t BluetoothGATTService::calculate_size() const { uint32_t size = 0; @@ -2393,11 +2500,13 @@ uint32_t BluetoothGATTService::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); +uint8_t *BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); for (auto &it : this->services) { - buffer.encode_sub_message(2, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 2, it); } + return pos; } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { uint32_t size = 0; @@ -2409,8 +2518,10 @@ uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { } return size; } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); +uint8_t *BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + return pos; } uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { uint32_t size = 0; @@ -2430,10 +2541,12 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, proto_varint_val } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, this->data_ptr_, this->data_len_); +uint8_t *BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->data_ptr_, this->data_len_); + return pos; } uint32_t BluetoothGATTReadResponse::calculate_size() const { uint32_t size = 0; @@ -2524,10 +2637,12 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, proto_varint_v } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, this->data_ptr_, this->data_len_); +uint8_t *BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->data_ptr_, this->data_len_); + return pos; } uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { uint32_t size = 0; @@ -2536,14 +2651,16 @@ uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->free); - buffer.encode_uint32(2, this->limit); +uint8_t *BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->free); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - buffer.encode_uint64(3, it, true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } } + return pos; } uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { uint32_t size = 0; @@ -2556,10 +2673,12 @@ uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { } return size; } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothGATTErrorResponse::calculate_size() const { uint32_t size = 0; @@ -2568,9 +2687,11 @@ uint32_t BluetoothGATTErrorResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); +uint8_t *BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + return pos; } uint32_t BluetoothGATTWriteResponse::calculate_size() const { uint32_t size = 0; @@ -2578,9 +2699,11 @@ uint32_t BluetoothGATTWriteResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->handle); return size; } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); +uint8_t *BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + return pos; } uint32_t BluetoothGATTNotifyResponse::calculate_size() const { uint32_t size = 0; @@ -2588,10 +2711,12 @@ uint32_t BluetoothGATTNotifyResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->handle); return size; } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->paired); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->paired); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDevicePairingResponse::calculate_size() const { uint32_t size = 0; @@ -2600,10 +2725,12 @@ uint32_t BluetoothDevicePairingResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->success); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { uint32_t size = 0; @@ -2612,10 +2739,12 @@ uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->success); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { uint32_t size = 0; @@ -2624,10 +2753,12 @@ uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); - buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); - buffer.encode_uint32(3, static_cast<uint32_t>(this->configured_mode)); +uint8_t *BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->state)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->mode)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->configured_mode)); + return pos; } uint32_t BluetoothScannerStateResponse::calculate_size() const { uint32_t size = 0; @@ -2661,10 +2792,12 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, proto_vari } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->noise_suppression_level); - buffer.encode_uint32(2, this->auto_gain); - buffer.encode_float(3, this->volume_multiplier); +uint8_t *VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->noise_suppression_level); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->auto_gain); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->volume_multiplier); + return pos; } uint32_t VoiceAssistantAudioSettings::calculate_size() const { uint32_t size = 0; @@ -2673,12 +2806,14 @@ uint32_t VoiceAssistantAudioSettings::calculate_size() const { size += ProtoSize::calc_float(1, this->volume_multiplier); return size; } -void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_bool(1, this->start); - buffer.encode_string(2, this->conversation_id); - buffer.encode_uint32(3, this->flags); - buffer.encode_optional_sub_message(4, this->audio_settings); - buffer.encode_string(5, this->wake_word_phrase); +uint8_t *VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->start); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->conversation_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->flags); + ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, this->audio_settings); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->wake_word_phrase); + return pos; } uint32_t VoiceAssistantRequest::calculate_size() const { uint32_t size = 0; @@ -2760,9 +2895,11 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_bytes(1, this->data, this->data_len); - buffer.encode_bool(2, this->end); +uint8_t *VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->end); + return pos; } uint32_t VoiceAssistantAudio::calculate_size() const { uint32_t size = 0; @@ -2833,18 +2970,24 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint8_t *VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->success); + return pos; +} uint32_t VoiceAssistantAnnounceFinished::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_bool(1, this->success); return size; } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->id); - buffer.encode_string(2, this->wake_word); +uint8_t *VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->wake_word); for (auto &it : this->trained_languages) { - buffer.encode_string(3, it, true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } + return pos; } uint32_t VoiceAssistantWakeWord::calculate_size() const { uint32_t size = 0; @@ -2908,14 +3051,16 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { +uint8_t *VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); for (auto &it : this->available_wake_words) { - buffer.encode_sub_message(1, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, it); } for (const auto &it : *this->active_wake_words) { - buffer.encode_string(2, it, true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, it, true); } - buffer.encode_uint32(3, this->max_active_wake_words); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->max_active_wake_words); + return pos; } uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { uint32_t size = 0; @@ -2944,21 +3089,23 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_uint32(8, this->supported_features); - buffer.encode_bool(9, this->requires_code); - buffer.encode_bool(10, this->requires_code_to_arm); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supported_features); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->requires_code); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->requires_code_to_arm); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; @@ -2978,12 +3125,14 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { #endif return size; } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast<uint32_t>(this->state)); +uint8_t *AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->state)); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; @@ -3032,22 +3181,24 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_uint32(8, this->min_length); - buffer.encode_uint32(9, this->max_length); - buffer.encode_string(10, this->pattern); - buffer.encode_uint32(11, static_cast<uint32_t>(this->mode)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->min_length); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->max_length); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->pattern); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(this->mode)); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; @@ -3068,13 +3219,15 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { #endif return size; } -void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_string(2, this->state); - buffer.encode_bool(3, this->missing_state); +uint8_t *TextStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t TextStateResponse::calculate_size() const { uint32_t size = 0; @@ -3121,18 +3274,20 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; @@ -3149,15 +3304,17 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { #endif return size; } -void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->missing_state); - buffer.encode_uint32(3, this->year); - buffer.encode_uint32(4, this->month); - buffer.encode_uint32(5, this->day); +uint8_t *DateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->year); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->month); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->day); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } uint32_t DateStateResponse::calculate_size() const { uint32_t size = 0; @@ -3204,18 +3361,20 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; @@ -3232,15 +3391,17 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { #endif return size; } -void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->missing_state); - buffer.encode_uint32(3, this->hour); - buffer.encode_uint32(4, this->minute); - buffer.encode_uint32(5, this->second); +uint8_t *TimeStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->hour); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->minute); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->second); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } uint32_t TimeStateResponse::calculate_size() const { uint32_t size = 0; @@ -3287,22 +3448,24 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); for (const char *it : *this->event_types) { - buffer.encode_string(9, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, it, strlen(it), true); } #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; @@ -3325,12 +3488,14 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { #endif return size; } -void EventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_string(2, this->event_type); +uint8_t *EventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->event_type); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t EventResponse::calculate_size() const { uint32_t size = 0; @@ -3343,22 +3508,24 @@ uint32_t EventResponse::calculate_size() const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(8, this->device_class); - buffer.encode_bool(9, this->assumed_state); - buffer.encode_bool(10, this->supports_position); - buffer.encode_bool(11, this->supports_stop); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->supports_position); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supports_stop); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; @@ -3379,13 +3546,15 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { #endif return size; } -void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_float(2, this->position); - buffer.encode_uint32(3, static_cast<uint32_t>(this->current_operation)); +uint8_t *ValveStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->position); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->current_operation)); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t ValveStateResponse::calculate_size() const { uint32_t size = 0; @@ -3430,18 +3599,20 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; @@ -3458,13 +3629,15 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { #endif return size; } -void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->missing_state); - buffer.encode_fixed32(3, this->epoch_seconds); +uint8_t *DateTimeStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->epoch_seconds); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t DateTimeStateResponse::calculate_size() const { uint32_t size = 0; @@ -3503,19 +3676,21 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; @@ -3533,20 +3708,22 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { #endif return size; } -void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->missing_state); - buffer.encode_bool(3, this->in_progress); - buffer.encode_bool(4, this->has_progress); - buffer.encode_float(5, this->progress); - buffer.encode_string(6, this->current_version); - buffer.encode_string(7, this->latest_version); - buffer.encode_string(8, this->title); - buffer.encode_string(9, this->release_summary); - buffer.encode_string(10, this->release_url); +uint8_t *UpdateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->in_progress); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 4, this->has_progress); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->progress); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->current_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, this->latest_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->title); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->release_summary); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->release_url); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t UpdateStateResponse::calculate_size() const { uint32_t size = 0; @@ -3604,7 +3781,11 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +uint8_t *ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); + return pos; +} uint32_t ZWaveProxyFrame::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->data_len); @@ -3632,9 +3813,11 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, static_cast<uint32_t>(this->type)); - buffer.encode_bytes(2, this->data, this->data_len); +uint8_t *ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->type)); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data, this->data_len); + return pos; } uint32_t ZWaveProxyRequest::calculate_size() const { uint32_t size = 0; @@ -3644,20 +3827,22 @@ uint32_t ZWaveProxyRequest::calculate_size() const { } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(4, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif - buffer.encode_bool(5, this->disabled_by_default); - buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(7, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id); #endif - buffer.encode_uint32(8, this->capabilities); - buffer.encode_uint32(9, this->receiver_frequency); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->receiver_frequency); + return pos; } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; @@ -3719,14 +3904,16 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { +uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); #ifdef USE_DEVICES - buffer.encode_uint32(1, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); #endif - buffer.write_tag_and_fixed32(21, this->key); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); for (const auto &it : *this->timings) { - buffer.encode_sint32(3, it, true); + ProtoEncode::encode_sint32(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } + return pos; } uint32_t InfraredRFReceiveEvent::calculate_size() const { uint32_t size = 0; @@ -3768,9 +3955,11 @@ bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_ } return true; } -void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->instance); - buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +uint8_t *SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); + return pos; } uint32_t SerialProxyDataReceived::calculate_size() const { uint32_t size = 0; @@ -3823,9 +4012,11 @@ bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, proto_vari } return true; } -void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->instance); - buffer.encode_uint32(2, this->line_states); +uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states); + return pos; } uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { uint32_t size = 0; @@ -3846,11 +4037,13 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t v } return true; } -void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->instance); - buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); - buffer.encode_uint32(3, static_cast<uint32_t>(this->status)); - buffer.encode_string(4, this->error_message); +uint8_t *SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->status)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->error_message); + return pos; } uint32_t SerialProxyRequestResponse::calculate_size() const { uint32_t size = 0; @@ -3884,9 +4077,11 @@ bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto } return true; } -void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_int32(2, this->error); +uint8_t *BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->error); + return pos; } uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { uint32_t size = 0; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 14f6c704ae4..3b239db36c5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -412,7 +412,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -477,7 +477,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -492,7 +492,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -506,7 +506,7 @@ class SerialProxyInfo final : public ProtoMessage { public: StringRef name{}; enums::SerialProxyPortType port_type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -574,7 +574,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_SERIAL_PROXY std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -605,7 +605,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -622,7 +622,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -644,7 +644,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -662,7 +662,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -704,7 +704,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector<const char *> *supported_preset_modes{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -771,7 +771,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector<const char *> *effects{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -798,7 +798,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -862,7 +862,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -879,7 +879,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -898,7 +898,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -914,7 +914,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -948,7 +948,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -965,7 +965,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1037,7 +1037,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1051,7 +1051,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1080,7 +1080,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1124,7 +1124,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1215,7 +1215,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1234,7 +1234,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector<ListEntitiesServicesArgument> args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1304,7 +1304,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1321,7 +1321,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1343,7 +1343,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1394,7 +1394,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1422,7 +1422,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1480,7 +1480,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1501,7 +1501,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1545,7 +1545,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1562,7 +1562,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1596,7 +1596,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector<const char *> *options{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1613,7 +1613,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector<const char *> *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1666,7 +1666,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1711,7 +1711,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1727,7 +1727,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1764,7 +1764,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1796,7 +1796,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1814,7 +1814,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector<MediaPlayerSupportedFormat> supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1832,7 +1832,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1888,7 +1888,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1905,7 +1905,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1942,7 +1942,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1970,7 +1970,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array<uint64_t, 2> uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1985,7 +1985,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector<BluetoothGATTDescriptor> descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1999,7 +1999,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector<BluetoothGATTCharacteristic> characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2016,7 +2016,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector<BluetoothGATTService> services{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2032,7 +2032,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2071,7 +2071,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2166,7 +2166,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2184,7 +2184,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2202,7 +2202,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2219,7 +2219,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2236,7 +2236,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2254,7 +2254,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2272,7 +2272,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2290,7 +2290,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2308,7 +2308,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2354,7 +2354,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2374,7 +2374,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2436,7 +2436,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2494,7 +2494,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2507,7 +2507,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector<std::string> trained_languages{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2557,7 +2557,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector<VoiceAssistantWakeWord> available_wake_words{}; const std::vector<std::string> *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2592,7 +2592,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2608,7 +2608,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2647,7 +2647,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2664,7 +2664,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2698,7 +2698,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2717,7 +2717,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2752,7 +2752,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2771,7 +2771,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2808,7 +2808,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector<const char *> *event_types{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2824,7 +2824,7 @@ class EventResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2845,7 +2845,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2862,7 +2862,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2897,7 +2897,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2914,7 +2914,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2948,7 +2948,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2972,7 +2972,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3007,7 +3007,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3026,7 +3026,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3047,7 +3047,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { #endif uint32_t capabilities{0}; uint32_t receiver_frequency{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3094,7 +3094,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector<int32_t> *timings{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3138,7 +3138,7 @@ class SerialProxyDataReceived final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3204,7 +3204,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage { #endif uint32_t instance{0}; uint32_t line_states{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3239,7 +3239,7 @@ class SerialProxyRequestResponse final : public ProtoMessage { enums::SerialProxyRequestType type{}; enums::SerialProxyStatus status{}; StringRef error_message{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3277,7 +3277,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { #endif uint64_t address{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index d9fe0fe461a..236e4a474a1 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -145,14 +145,15 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size // [tag][v1][v2][body ..... body] // ^-- pos_ = element end, within buffer void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + uint8_t *(*encode_fn)(const void *, + ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) { this->encode_field_raw(field_id, 2); // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) uint8_t *len_pos = this->pos_; this->debug_check_bounds_(1); this->pos_++; uint8_t *body_start = this->pos_; - encode_fn(value, *this); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start); if (body_size < 128) [[likely]] { // Common case: 1-byte varint, just backpatch @@ -173,22 +174,27 @@ void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, // Non-template core for encode_optional_sub_message. void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + uint8_t *(*encode_fn)(const void *, + ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) { if (nested_size == 0) return; this->encode_field_raw(field_id, 2); this->encode_varint_raw(nested_size); #ifdef ESPHOME_DEBUG_API uint8_t *start = this->pos_; - encode_fn(value, *this); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); if (static_cast<uint32_t>(this->pos_ - start) != nested_size) this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); #else - encode_fn(value, *this); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); #endif } #ifdef ESPHOME_DEBUG_API +void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller) { + ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos); + abort(); +} void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, @@ -201,6 +207,7 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe expected, actual); abort(); } + #endif void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b629018a919..902d4c0f5c4 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -195,6 +195,26 @@ class Proto32Bit { // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported +// Debug bounds checking for proto encode functions. +// In debug mode (ESPHOME_DEBUG_API), an extra end-of-buffer pointer is threaded +// through the entire encode chain. In production, these expand to nothing. +#ifdef ESPHOME_DEBUG_API +#define PROTO_ENCODE_DEBUG_PARAM , uint8_t *proto_debug_end_ +#define PROTO_ENCODE_DEBUG_ARG , proto_debug_end_ +#define PROTO_ENCODE_DEBUG_INIT(buf) , (buf)->data() + (buf)->size() +#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) \ + do { \ + if ((pos) + (n) > proto_debug_end_) \ + proto_check_bounds_failed(pos, n, proto_debug_end_, __builtin_FUNCTION()); \ + } while (0) +void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller); +#else +#define PROTO_ENCODE_DEBUG_PARAM +#define PROTO_ENCODE_DEBUG_ARG +#define PROTO_ENCODE_DEBUG_INIT(buf) +#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) +#endif + class ProtoWriteBuffer { public: ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} @@ -207,15 +227,6 @@ class ProtoWriteBuffer { } this->encode_varint_raw_slow_(value); } - void encode_varint_raw_64(uint64_t value) { - while (value > 0x7F) { - this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value | 0x80); - value >>= 7; - } - this->debug_check_bounds_(1); - *this->pos_++ = static_cast<uint8_t>(value); - } /** * Encode a field key (tag/wire type combination). * @@ -229,123 +240,6 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } - /// Write a single precomputed tag byte. Tag must be < 128. - inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { - this->debug_check_bounds_(1); - *this->pos_++ = b; - } - /// Write raw bytes to the buffer (no tag, no length prefix). - inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { - this->debug_check_bounds_(len); - std::memcpy(this->pos_, data, len); - this->pos_ += len; - } - /// Write a precomputed tag byte + 32-bit value in one operation. - /// Tag must be a single-byte varint (< 128). No zero check. - inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { - this->debug_check_bounds_(5); - this->pos_[0] = tag; -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - std::memcpy(this->pos_ + 1, &value, 4); -#else - this->pos_[1] = static_cast<uint8_t>(value & 0xFF); - this->pos_[2] = static_cast<uint8_t>((value >> 8) & 0xFF); - this->pos_[3] = static_cast<uint8_t>((value >> 16) & 0xFF); - this->pos_[4] = static_cast<uint8_t>((value >> 24) & 0xFF); -#endif - this->pos_ += 5; - } - void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { - if (len == 0 && !force) - return; - - this->encode_field_raw(field_id, 2); // type 2: Length-delimited string - this->encode_varint_raw(len); - // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks - // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. - this->debug_check_bounds_(len); - std::memcpy(this->pos_, string, len); - this->pos_ += len; - } - void encode_string(uint32_t field_id, const std::string &value, bool force = false) { - this->encode_string(field_id, value.data(), value.size(), force); - } - void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) { - this->encode_string(field_id, ref.c_str(), ref.size(), force); - } - void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { - this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); - } - void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { - if (value == 0 && !force) - return; - this->encode_field_raw(field_id, 0); // type 0: Varint - uint32 - this->encode_varint_raw(value); - } - void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { - if (value == 0 && !force) - return; - this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - this->encode_varint_raw_64(value); - } - void encode_bool(uint32_t field_id, bool value, bool force = false) { - if (!value && !force) - return; - this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->debug_check_bounds_(1); - *this->pos_++ = value ? 0x01 : 0x00; - } - void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->debug_check_bounds_(4); -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - // Protobuf fixed32 is little-endian, so direct copy works - std::memcpy(this->pos_, &value, 4); - this->pos_ += 4; -#else - *this->pos_++ = (value >> 0) & 0xFF; - *this->pos_++ = (value >> 8) & 0xFF; - *this->pos_++ = (value >> 16) & 0xFF; - *this->pos_++ = (value >> 24) & 0xFF; -#endif - } - // 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; - - union { - float value; - uint32_t raw; - } val{}; - val.value = value; - this->encode_fixed32(field_id, val.raw); - } - void encode_int32(uint32_t field_id, int32_t value, bool force = false) { - if (value < 0) { - // negative int32 is always 10 byte long - this->encode_int64(field_id, value, force); - return; - } - this->encode_uint32(field_id, static_cast<uint32_t>(value), force); - } - void encode_int64(uint32_t field_id, int64_t value, bool force = false) { - this->encode_uint64(field_id, static_cast<uint64_t>(value), force); - } - void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { - this->encode_uint32(field_id, encode_zigzag32(value), force); - } - void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { - this->encode_uint64(field_id, encode_zigzag64(value), force); - } - /// Encode a packed repeated sint32 field (zero-copy from vector) - void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); /// Single-pass encode for repeated submessage elements. /// Thin template wrapper; all buffer work is in the non-template core. template<typename T> void encode_sub_message(uint32_t field_id, const T &value); @@ -353,12 +247,17 @@ class ProtoWriteBuffer { /// Thin template wrapper; all buffer work is in the non-template core. template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value); + // NOLINTBEGIN(readability-identifier-naming) // Non-template core for encode_sub_message — backpatch approach. - void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + void encode_sub_message(uint32_t field_id, const void *value, + uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)); // Non-template core for encode_optional_sub_message. void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &)); + uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)); + // NOLINTEND(readability-identifier-naming) APIBuffer *get_buffer() const { return buffer_; } + uint8_t *get_pos() const { return pos_; } + void set_pos(uint8_t *pos) { pos_ = pos; } protected: // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small @@ -375,6 +274,211 @@ class ProtoWriteBuffer { uint8_t *pos_; }; +// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize. +constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128 +constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384 + +/// Static encode helpers for generated encode() functions. +/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos, +/// then calls these methods which take pos by reference. No struct, no overhead. +/// For sub-messages, pos is synced back to buffer before the call and reloaded after. +class ProtoEncode { + public: + /// Write a multi-byte varint directly through a pos pointer. + template<typename T> + static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) { + do { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + } while (value > 0x7F); + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast<uint8_t>(value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast<uint8_t>(value); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + /// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast<uint8_t>(value); + return; + } + if (value < VARINT_MAX_2_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 2); + *pos++ = static_cast<uint8_t>(value | 0x80); + *pos++ = static_cast<uint8_t>(value >> 7); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint64_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast<uint8_t>(value); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t field_id, uint32_t type) { + encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type); + } + /// Write a single precomputed tag byte. Tag must be < 128. + static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint8_t b) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + const void *data, size_t len) { + PROTO_ENCODE_CHECK_BOUNDS(pos, len); + std::memcpy(pos, data, len); + pos += len; + } + /// Write a precomputed tag byte + 32-bit value in one operation. + static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint8_t tag, uint32_t value) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 5); + pos[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(pos + 1, &value, 4); +#else + pos[1] = static_cast<uint8_t>(value & 0xFF); + pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF); + pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF); + pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF); +#endif + pos += 5; + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const char *string, size_t len, bool force = false) { + if (len == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string + if (len < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len); + *pos++ = static_cast<uint8_t>(len); + } else { + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len); + PROTO_ENCODE_CHECK_BOUNDS(pos, len); + } + std::memcpy(pos, string, len); + pos += len; + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const std::string &value, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force); + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const StringRef &ref, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force); + } + static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const uint8_t *data, size_t len, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force); + } + static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint64_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value, + bool force = false) { + if (!value && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = value ? 0x01 : 0x00; + } + static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5); + PROTO_ENCODE_CHECK_BOUNDS(pos, 4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(pos, &value, 4); + pos += 4; +#else + *pos++ = (value >> 0) & 0xFF; + *pos++ = (value >> 8) & 0xFF; + *pos++ = (value >> 16) & 0xFF; + *pos++ = (value >> 24) & 0xFF; +#endif + } + // 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. + static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value, + bool force = false) { + if (value == 0.0f && !force) + return; + union { + float value; + uint32_t raw; + } val{}; + val.value = value; + encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, val.raw); + } + static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value, + bool force = false) { + if (value < 0) { + // negative int32 is always 10 byte long + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); + return; + } + encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force); + } + static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value, + bool force = false) { + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force); + } + static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + int32_t value, bool force = false) { + encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force); + } + static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + int64_t value, bool force = false) { + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force); + } + /// Sub-message encoding: sync pos to buffer, delegate, get pos from return value. + template<typename T> + static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer, + uint32_t field_id, const T &value) { + buffer.set_pos(pos); + buffer.encode_sub_message(field_id, value); + pos = buffer.get_pos(); + } + template<typename T> + static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) { + buffer.set_pos(pos); + buffer.encode_optional_sub_message(field_id, value); + pos = buffer.get_pos(); + } +}; + #ifdef HAS_PROTO_MESSAGE_DUMP /** * Fixed-size buffer for message dumps - avoids heap allocation. @@ -470,7 +574,7 @@ class ProtoMessage { // All call sites use templates to preserve the concrete type, so virtual // dispatch is not needed. This eliminates per-message vtable entries for // encode/calculate_size, saving ~1.3 KB of flash across all message types. - void encode(ProtoWriteBuffer &buffer) const {} + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); } uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; @@ -512,9 +616,10 @@ class ProtoDecodableMessage : public ProtoMessage { class ProtoSize { public: - // Varint encoding thresholds: values below each threshold fit in N bytes - static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128 - static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384 + // Varint encoding thresholds — use namespace-level constants for 1/2 byte, + // class-level for 3/4 byte (only used within ProtoSize). + static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = VARINT_MAX_1_BYTE; + static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = VARINT_MAX_2_BYTE; static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 @@ -531,6 +636,17 @@ class ProtoSize { return varint_wide(value); return varint_slow(value); } + /// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + /// Inlines both checks; falls back to slow path for 3+ bytes. + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) { + if (value < VARINT_THRESHOLD_1_BYTE) [[likely]] + return 1; + if (value < VARINT_THRESHOLD_2_BYTE) [[likely]] + return 2; + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); + } private: // Slow path for varint >= 128, outlined to keep fast path small @@ -645,10 +761,10 @@ class ProtoSize { return value ? field_id_size + 4 : 0; } static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { - return value ? field_id_size + varint(encode_zigzag32(value)) : 0; + return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0; } static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) { - return field_id_size + varint(encode_zigzag32(value)); + return field_id_size + varint_short(encode_zigzag32(value)); } static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { return value ? field_id_size + varint(value) : 0; @@ -691,28 +807,9 @@ class ProtoSize { // Implementation of methods that depend on ProtoSize being fully defined -// Implementation of encode_packed_sint32 - must be after ProtoSize is defined -inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) { - if (values.empty()) - return; - - // Calculate packed size - size_t packed_size = 0; - for (int value : values) { - packed_size += ProtoSize::varint(encode_zigzag32(value)); - } - - // Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values - this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED); - this->encode_varint_raw(packed_size); - for (int value : values) { - this->encode_varint_raw(encode_zigzag32(value)); - } -} - // Encode thunk — converts void* back to concrete type for direct encode() call -template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) { - static_cast<const T *>(msg)->encode(buf); +template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) { + return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG); } // Thin template wrapper; delegates to non-template core in proto.cpp. diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index c17f16412cc..3833f279ce7 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -232,29 +232,31 @@ class TypeInfo(ABC): # eliminating the zero-check branch and encode_field_raw indirection. # {value} is replaced with the actual field expression. RAW_ENCODE_MAP: dict[str, str] = { - "encode_uint32": "buffer.encode_varint_raw({value});", - "encode_uint64": "buffer.encode_varint_raw_64({value});", - "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", - "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", - "encode_int64": "buffer.encode_varint_raw_64(static_cast<uint64_t>({value}));", - "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", + "encode_uint32": "ProtoEncode::encode_varint_raw(pos, {value});", + "encode_uint64": "ProtoEncode::encode_varint_raw_64(pos, {value});", + "encode_sint32": "ProtoEncode::encode_varint_raw_short(pos, encode_zigzag32({value}));", + "encode_sint64": "ProtoEncode::encode_varint_raw_64(pos, encode_zigzag64({value}));", + "encode_int64": "ProtoEncode::encode_varint_raw_64(pos, static_cast<uint64_t>({value}));", + "encode_bool": "ProtoEncode::write_raw_byte(pos, {value} ? 0x01 : 0x00);", } # When max_value < 128, the varint is always 1 byte — use a direct byte write RAW_ENCODE_SMALL_MAP: dict[str, str] = { - "encode_uint32": "buffer.write_raw_byte(static_cast<uint8_t>({value}));", - "encode_uint64": "buffer.write_raw_byte(static_cast<uint8_t>({value}));", + "encode_uint32": "ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({value}));", + "encode_uint64": "ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({value}));", } def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: - """Try to emit a precomputed-tag encode for a forced field. + """Try to emit a precomputed-tag encode for a field. + + For forced fields: emits raw tag + value unconditionally. + For non-forced fields with single-byte tag: emits inline zero-check + + raw tag + value, avoiding an outlined function call. Returns the raw encode string if the tag is a single byte and the encode_func has a known raw equivalent, or None otherwise. When max_value < 128, uses direct byte write instead of varint encoding. """ - if not self.force: - return None tag = self.calculate_tag() if tag >= 128: return None @@ -263,10 +265,17 @@ class TypeInfo(ABC): if max_val is not None and max_val < 128: raw_expr = self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) if raw_expr is None: + # Only use RAW_ENCODE_MAP for forced fields or fields with max_value + if not self.force and max_val is None: + return None raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None - return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" + body = f"ProtoEncode::write_raw_byte(pos, {tag});\n{raw_expr.format(value=value_expr)}" + if self.force: + return body + # Non-forced with max_value: inline zero-check + raw encode + return f"if ({value_expr}) {{\n {body}\n}}" def _encode_bytes_with_precomputed_tag( self, data_expr: str, len_expr: str, max_len: int | None = None @@ -283,14 +292,14 @@ class TypeInfo(ABC): return None # When max_len < 128, length varint is always 1 byte len_encode = ( - f"buffer.write_raw_byte(static_cast<uint8_t>({len_expr}));" + f"ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({len_expr}));" if max_len is not None and max_len < 128 - else f"buffer.encode_varint_raw({len_expr});" + else f"ProtoEncode::encode_varint_raw(pos, {len_expr});" ) return ( - f"buffer.write_raw_byte({tag});\n" + f"ProtoEncode::write_raw_byte(pos, {tag});\n" f"{len_encode}\n" - f"buffer.encode_raw({data_expr}, {len_expr});" + f"ProtoEncode::encode_raw(pos, {data_expr}, {len_expr});" ) @property @@ -298,8 +307,8 @@ class TypeInfo(ABC): if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): return result if self.force: - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" encode_func = None @@ -657,10 +666,10 @@ class Fixed32Type(TypeInfo): tag = self.calculate_tag() if self.force and tag < 128: # Emit combined tag+value write: precomputed tag + direct memcpy - return f"buffer.write_tag_and_fixed32({tag}, this->{self.field_name});" + return f"ProtoEncode::write_tag_and_fixed32(pos, {tag}, this->{self.field_name});" if self.force: - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() @@ -734,8 +743,8 @@ class StringType(TypeInfo): ): return result if self.force: - return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" - return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_, true);" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_);" def dump(self, name): # If name is 'it', this is a repeated field element - always use string @@ -822,8 +831,8 @@ class MessageType(TypeInfo): @property def encode_content(self) -> str: - # encode_sub_message always encodes (uses backpatch), no force needed - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + # Sub-message encoding needs buffer for backpatch/sync + return f"ProtoEncode::{self.encode_func}(pos, buffer, {self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -904,8 +913,8 @@ class BytesType(TypeInfo): ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" @@ -1015,8 +1024,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" @property def decode_length_content(self) -> str | None: @@ -1068,10 +1077,10 @@ class PointerToStringBufferType(PointerToBufferTypeBase): ): return result if self.force: - return ( - f"buffer.encode_string({self.number}, this->{self.field_name}, true);" - ) - return f"buffer.encode_string({self.number}, this->{self.field_name});" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}, true);" + return ( + f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name});" + ) @property def decode_length_content(self) -> str | None: @@ -1240,8 +1249,8 @@ class FixedArrayBytesType(TypeInfo): ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: return f"out.append(format_hex_pretty({name}, {name}_len));" @@ -1323,8 +1332,8 @@ class EnumType(TypeInfo): ): return result if self.force: - return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" - return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, static_cast<uint32_t>(this->{self.field_name}));" def dump(self, name: str) -> str: return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" @@ -1487,11 +1496,13 @@ class FixedArrayRepeatedType(TypeInfo): def _encode_element(self, element: str) -> str: """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): - return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" + return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.encode_sub_message({self.number}, {element});" - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" + return ( + f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" + ) @property def cpp_type(self) -> str: @@ -1815,11 +1826,13 @@ class RepeatedTypeInfo(TypeInfo): def _encode_element_call(self, element: str) -> str: """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): - return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" + return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.encode_sub_message({self.number}, {element});" - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" + return ( + f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" + ) @property def encode_content(self) -> str: @@ -1828,7 +1841,7 @@ class RepeatedTypeInfo(TypeInfo): # Special handling for const char* elements (when container_no_template contains "const char") if "const char" in self._container_no_template: o = f"for (const char *it : *this->{self.field_name}) {{\n" - o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" + o += f" ProtoEncode::{self._ti.encode_func}(pos, {self.number}, it, strlen(it), true);\n" else: o = f"for (const auto &it : *this->{self.field_name}) {{\n" o += f" {self._encode_element_call('it')}\n" @@ -2403,15 +2416,19 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" - if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: - o += f" {encode[0]} }}\n" - else: - o += "\n" - o += indent("\n".join(encode)) + "\n" - o += "}\n" + # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls + encode_debug = [ + line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode + ] + o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" + o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" + o += indent("\n".join(encode_debug)) + "\n" + o += " return pos;\n" + o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer &buffer) const;" + prot = ( + "uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;" + ) public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used From ce0d36079064fe6d228d29fb01e4693e62c1e9f1 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:46:42 +1000 Subject: [PATCH 1252/2056] [lvgl] Implement rotation directly (#14955) --- esphome/components/lvgl/__init__.py | 26 +++- esphome/components/lvgl/automation.py | 31 ++++- esphome/components/lvgl/defines.py | 5 + esphome/components/lvgl/lvgl_esphome.cpp | 154 ++++++++++++++++------- esphome/components/lvgl/lvgl_esphome.h | 14 ++- esphome/components/lvgl/types.py | 1 + tests/components/lvgl/lvgl-package.yaml | 11 +- 7 files changed, 189 insertions(+), 53 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a9d31d42d8b..b429e1e322a 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -9,7 +9,7 @@ from esphome.components.const import ( CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING, ) -from esphome.components.display import Display +from esphome.components.display import Display, get_display_metadata, validate_rotation from esphome.components.esp32 import ( VARIANT_ESP32P4, add_idf_component, @@ -37,6 +37,7 @@ from esphome.const import ( CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, + CONF_ROTATION, CONF_TIMEOUT, CONF_TRIGGER_ID, ) @@ -74,6 +75,7 @@ from .trigger import add_on_boot_triggers, generate_align_tos, generate_triggers from .types import ( IdleTrigger, PlainTrigger, + RotationType, lv_font_t, lv_group_t, lv_lambda_t, @@ -185,6 +187,7 @@ def final_validation(config_list): for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") + uses_rotation = CONF_ROTATION in config for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) @@ -192,6 +195,11 @@ def final_validation(config_list): raise cv.Invalid( "Using lambda: or pages: in display config is not compatible with LVGL" ) + # treating 0 as false is intended here. + if uses_rotation and display.get(CONF_ROTATION): + df.LOGGER.warning( + "use of 'rotation' in both LVGL and the display config is not recommended" + ) if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( "Using auto_clear_enabled: true in display config not compatible with LVGL" @@ -322,6 +330,18 @@ async def to_code(configs): displays = [ await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] ] + rotation_type = RotationType.ROTATION_UNUSED + # options will have CONF_ROTATION true if rotation is changed in an automation. + if CONF_ROTATION in config or df.get_options().get(CONF_ROTATION) is True: + if all( + get_display_metadata(str(disp)).has_hardware_rotation + for disp in displays + ): + rotation_type = RotationType.ROTATION_HARDWARE + df.LOGGER.info("LVGL will use hardware rotation via display driver") + else: + rotation_type = RotationType.ROTATION_SOFTWARE + df.LOGGER.info("LVGL will use software rotation") lv_component = cg.new_Pvariable( config[CONF_ID], displays, @@ -330,8 +350,11 @@ async def to_code(configs): config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], + rotation_type, ) await cg.register_component(lv_component, config) + if rotation := config.get(CONF_ROTATION): + cg.add(lv_component.set_rotation(rotation)) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) lv_scr_act = get_screen_active(lv_component) @@ -492,6 +515,7 @@ LVGL_SCHEMA = cv.All( ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, + cv.Optional(CONF_ROTATION): validate_rotation, cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 50e6db74b8f..b825320a407 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -3,8 +3,9 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components.display import validate_rotation import esphome.config_validation as cv -from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_ROTATION, CONF_TIMEOUT from esphome.core import Lambda from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr @@ -23,6 +24,7 @@ from .defines import ( PARTS, StaticCastExpression, add_warning, + get_options, ) from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( @@ -191,6 +193,33 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): return var +def _validate_rotation(value): + # Note that we need rotation + get_options()[CONF_ROTATION] = True + return validate_rotation(value) + + +@automation.register_action( + "lvgl.display.set_rotation", + ObjUpdateAction, + cv.maybe_simple_value( + LVGL_SCHEMA.extend( + { + cv.Required(CONF_ROTATION): _validate_rotation, + } + ), + key=CONF_ROTATION, + ), + synchronous=True, +) +async def lvgl_set_rotation(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + async with LambdaContext() as context: + add_line_marks(where=action_id) + lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + + @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 668bb465151..ae8387bccac 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -29,6 +29,7 @@ KEY_COLOR_FORMATS = "color_formats" KEY_LV_DEFINES = "lv_defines" KEY_REMAPPED_USES = "remapped_uses" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_OPTIONS = "options" KEY_WARNINGS = "warnings" @@ -56,6 +57,10 @@ def add_warning(msg: str): get_warnings().add(msg) +def get_options(): + return get_data(KEY_OPTIONS) + + class StaticCastExpression(Expression): __slots__ = ("type", "exp") diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index a5075cb6148..0ab49d0a101 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -83,6 +83,44 @@ std::string lv_event_code_name_for(lv_event_t *event) { return buf; } +void LvglComponent::set_rotation(display::DisplayRotation rotation) { + if (this->rotation_type_ == RotationType::ROTATION_UNUSED) { + ESP_LOGW(TAG, "Display rotation cannot be changed unless rotation was enabled during setup."); + return; + } + this->rotation_ = rotation; + if (this->is_ready()) { + this->set_resolution_(); + lv_obj_update_layout(this->get_screen_active()); + lv_obj_invalidate(this->get_screen_active()); + } +} + +void LvglComponent::rotate_coordinates(int32_t &x, int32_t &y) const { + switch (this->rotation_) { + default: + break; + + case display::DISPLAY_ROTATION_180_DEGREES: { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + } + case display::DISPLAY_ROTATION_270_DEGREES: { + auto tmp = x; + x = this->height_ - y - 1; + y = tmp; + break; + } + case display::DISPLAY_ROTATION_90_DEGREES: { + auto tmp = y; + y = this->width_ - x - 1; + x = tmp; + break; + } + } +} + static void rounder_cb(lv_event_t *event) { auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event)); auto *area = static_cast<lv_area_t *>(lv_event_get_param(event)); @@ -118,7 +156,11 @@ void LvglComponent::dump_config() { " Buffer size: %zu%%\n" " Rotation: %d\n" " Draw rounding: %d", - this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); + this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); + if (this->rotation_type_ != ROTATION_UNUSED) { + ESP_LOGCONFIG(TAG, " Rotation type: %s", + this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); + } } void LvglComponent::set_paused(bool paused, bool show_snow) { @@ -216,48 +258,51 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_); - switch (this->rotation) { - case display::DISPLAY_ROTATION_90_DEGREES: - for (lv_coord_t x = height; x-- != 0;) { - for (lv_coord_t y = 0; y != width; y++) { - dst[y * height_rounded + x] = *ptr++; + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_); + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + for (lv_coord_t x = height; x-- != 0;) { + for (lv_coord_t y = 0; y != width; y++) { + dst[y * height_rounded + x] = *ptr++; + } } - } - y1 = x1; - x1 = this->height_ - area->y1 - height; - height = width; - width = height_rounded; - break; + y1 = x1; + x1 = this->width_ - area->y1 - height; + height = width; + width = height_rounded; + break; - case display::DISPLAY_ROTATION_180_DEGREES: - for (lv_coord_t y = height; y-- != 0;) { - for (lv_coord_t x = width; x-- != 0;) { - dst[y * width + x] = *ptr++; + case display::DISPLAY_ROTATION_180_DEGREES: + for (lv_coord_t y = height; y-- != 0;) { + for (lv_coord_t x = width; x-- != 0;) { + dst[y * width + x] = *ptr++; + } } - } - x1 = this->width_ - x1 - width; - y1 = this->height_ - y1 - height; - break; + x1 = this->width_ - x1 - width; + y1 = this->height_ - y1 - height; + break; - case display::DISPLAY_ROTATION_270_DEGREES: - for (lv_coord_t x = 0; x != height; x++) { - for (lv_coord_t y = width; y-- != 0;) { - dst[y * height_rounded + x] = *ptr++; + case display::DISPLAY_ROTATION_270_DEGREES: + for (lv_coord_t x = 0; x != height; x++) { + for (lv_coord_t y = width; y-- != 0;) { + dst[y * height_rounded + x] = *ptr++; + } } - } - x1 = y1; - y1 = this->width_ - area->x1 - width; - height = width; - width = height_rounded; - break; + x1 = y1; + y1 = this->height_ - area->x1 - width; + height = width; + width = height_rounded; + break; - default: - dst = ptr; - break; + default: + dst = ptr; + break; + } + ptr = dst; } for (auto *display : this->displays_) { - display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, + display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) ptr, display::COLOR_ORDER_RGB, LV_BITNESS, this->big_endian_); } } @@ -297,6 +342,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r if (l->touch_pressed_) { data->point.x = l->touch_point_.x; data->point.y = l->touch_point_.y; + l->parent_->rotate_coordinates(data->point.x, data->point.y); data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; @@ -543,26 +589,44 @@ void LvglComponent::write_random_() { * multiple of 2, and so on. * @param resume_on_input if true, this component will resume rendering when the user * presses a key or clicks on the screen. + * @param rotation_type What rotation type to use, if any */ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input, bool update_when_display_idle) + int draw_rounding, bool resume_on_input, bool update_when_display_idle, + RotationType rotation_type) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), resume_on_input_(resume_on_input), - update_when_display_idle_(update_when_display_idle) { + update_when_display_idle_(update_when_display_idle), + rotation_type_(rotation_type) { this->disp_ = lv_display_create(240, 240); } +void LvglComponent::set_resolution_() const { + int32_t width = this->width_; + int32_t height = this->height_; + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + std::swap(width, height); + } + ESP_LOGD(TAG, "Setting resolution to %u x %u (rotation %d)", (unsigned) width, (unsigned) height, + (int) this->rotation_); + if (this->rotation_type_ == RotationType::ROTATION_HARDWARE) { + for (auto *display : this->displays_) + display->set_rotation(this->rotation_); + } + lv_display_set_resolution(this->disp_, width, height); +} void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; + this->width_ = display->get_native_width(); + this->height_ = display->get_native_height(); // cater for displays with dimensions that don't divide by the required rounding - this->width_ = display->get_width(); - this->height_ = display->get_height(); - auto width = (display->get_width() + rounding - 1) / rounding * rounding; - auto height = (display->get_height() + rounding - 1) / rounding * rounding; + auto width = (this->width_ + rounding - 1) / rounding * rounding; + auto height = (this->height_ + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -586,15 +650,14 @@ void LvglComponent::setup() { return; } this->draw_buf_ = static_cast<uint8_t *>(buffer); - lv_display_set_resolution(this->disp_, this->width_, this->height_); + this->set_resolution_(); lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); lv_display_set_flush_cb(this->disp_, static_flush_cb); lv_display_set_user_data(this->disp_, this); lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); - this->rotation = display->get_rotation(); - if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { this->rotate_buf_ = static_cast<lv_color_t *>(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); @@ -620,9 +683,6 @@ void LvglComponent::setup() { esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); }); #endif - // Rotation will be handled by our drawing function, so reset the display rotation. - for (auto *disp : this->displays_) - disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8d139b23cb1..4a4c11d3834 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -156,13 +156,18 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> { #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG +enum RotationType : uint8_t { + ROTATION_UNUSED, + ROTATION_SOFTWARE, + ROTATION_HARDWARE, +}; class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; public: LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input, bool update_when_display_idle); + bool resume_on_input, bool update_when_display_idle, RotationType rotation_type); static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -216,13 +221,16 @@ class LvglComponent : public PollingComponent { // rounding factor to align bounds of update area when drawing size_t draw_rounding{2}; - display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; } void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } + void set_rotation(display::DisplayRotation rotation); + display::DisplayRotation get_rotation() const { return this->rotation_; } + void rotate_coordinates(int32_t &x, int32_t &y) const; protected: + void set_resolution_() const; void draw_end_(); // Not checking for non-null callback since the // LVGL callback that calls it is not set in that case @@ -256,6 +264,8 @@ class LvglComponent : public PollingComponent { Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; void *rotate_buf_{}; + display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; + RotationType rotation_type_; }; class IdleTrigger : public Trigger<> { diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 686e4292679..0c8ddfbfbd0 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -69,6 +69,7 @@ lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") +RotationType = lvgl_ns.enum("RotationType") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 3c5c730e6cb..4d44c62000c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -30,12 +30,19 @@ binary_sensor: return y; lvgl: + id: lvgl_id + rotation: 90 log_level: debug resume_on_input: true on_pause: - logger.log: LVGL is Paused + - logger.log: LVGL is Paused + - lvgl.display.set_rotation: 90 on_resume: - logger.log: LVGL has resumed + - logger.log: LVGL has resumed + - lvgl.display.set_rotation: + rotation: 0 + lvgl_id: lvgl_id + on_boot: - logger.log: LVGL has started - lvgl.indicator.update: From c98bb9060f7d45e9ef57103fd3add9578caa2e59 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:48:14 +1000 Subject: [PATCH 1253/2056] [lvgl] Fix setting triggers on display (#15364) --- esphome/components/lvgl/__init__.py | 20 +++++-- esphome/components/lvgl/defines.py | 36 +++++++----- esphome/components/lvgl/trigger.py | 54 +++++++++++++----- tests/components/lvgl/lvgl-package.yaml | 76 ++++++++++++------------- 4 files changed, 114 insertions(+), 72 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b429e1e322a..b69f8ef57bf 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -2,7 +2,7 @@ import importlib from pathlib import Path import pkgutil -from esphome.automation import build_automation, validate_automation +from esphome.automation import Trigger, build_automation, validate_automation import esphome.codegen as cg from esphome.components.const import ( CONF_BYTE_ORDER, @@ -34,7 +34,6 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_LOG_LEVEL, - CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, CONF_ROTATION, @@ -59,7 +58,7 @@ from .encoders import ( from .gradient import GRADIENT_SCHEMA, gradients_to_code from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool, lv_images_used -from .lvcode import LvContext, LvglComponent, lvgl_static +from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, @@ -71,7 +70,7 @@ from .schemas import ( ) from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code -from .trigger import add_on_boot_triggers, generate_align_tos, generate_triggers +from .trigger import generate_align_tos, generate_triggers from .types import ( IdleTrigger, PlainTrigger, @@ -79,6 +78,7 @@ from .types import ( lv_font_t, lv_group_t, lv_lambda_t, + lv_obj_t_ptr, lv_style_t, lvgl_ns, ) @@ -398,7 +398,6 @@ async def to_code(configs): f"set_{trigger_name.removeprefix('on_')}_trigger", )(trigger_var) ) - await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) # This must be done after all widgets are created for comp in helpers.lvgl_components_required: @@ -502,6 +501,17 @@ LVGL_SCHEMA = cv.All( cv.polling_component_schema("1s") .extend( { + **{ + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(lv_obj_t_ptr, lv_event_t_ptr) + ), + } + ) + for event in df.LV_SCREEN_EVENT_TRIGGERS + + df.LV_DISPLAY_EVENT_TRIGGERS + }, cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), cv.GenerateID(CONF_ALIGN_TO_LAMBDA_ID): cv.declare_id(lv_lambda_t), cv.GenerateID(df.CONF_DISPLAYS): display_schema, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index ae8387bccac..ef29a99ddd4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -276,10 +276,6 @@ LV_EVENT_MAP = { "DRAW_POST_BEGIN": "DRAW_POST_BEGIN", "DRAW_POST_END": "DRAW_POST_END", "DRAW_TASK_ADD": "DRAW_TASK_ADDED", - "FLUSH_FINISH": "FLUSH_FINISH", - "FLUSH_START": "FLUSH_START", - "FLUSH_WAIT_FINISH": "FLUSH_WAIT_FINISH", - "FLUSH_WAIT_START": "FLUSH_WAIT_START", "FOCUS": "FOCUSED", "GESTURE": "GESTURE", "GET_SELF_SIZE": "GET_SELF_SIZE", @@ -300,18 +296,8 @@ LV_EVENT_MAP = { "READY": "READY", "REFRESH": "REFRESH", "REFR_EXT_DRAW_SIZE": "REFR_EXT_DRAW_SIZE", - "REFR_READY": "REFR_READY", - "REFR_REQUEST": "REFR_REQUEST", - "REFR_START": "REFR_START", "RELEASE": "RELEASED", - "RENDER_READY": "RENDER_READY", - "RENDER_START": "RENDER_START", - "RESOLUTION_CHANGE": "RESOLUTION_CHANGED", "ROTARY": "ROTARY", - "SCREEN_LOAD": "SCREEN_LOADED", - "SCREEN_LOAD_START": "SCREEN_LOAD_START", - "SCREEN_UNLOAD": "SCREEN_UNLOADED", - "SCREEN_UNLOAD_START": "SCREEN_UNLOAD_START", "SCROLL": "SCROLL", "SCROLL_BEGIN": "SCROLL_BEGIN", "SCROLL_END": "SCROLL_END", @@ -322,12 +308,34 @@ LV_EVENT_MAP = { "STATE_CHANGE": "STATE_CHANGED", "STYLE_CHANGE": "STYLE_CHANGED", "TRIPLE_CLICK": "TRIPLE_CLICKED", +} +LV_SCREEN_EVENT_MAP = { + "SCREEN_LOAD": "SCREEN_LOADED", + "SCREEN_LOAD_START": "SCREEN_LOAD_START", + "SCREEN_UNLOAD": "SCREEN_UNLOADED", + "SCREEN_UNLOAD_START": "SCREEN_UNLOAD_START", +} + +LV_DISPLAY_EVENT_MAP = { + "FLUSH_FINISH": "FLUSH_FINISH", + "FLUSH_START": "FLUSH_START", + "FLUSH_WAIT_FINISH": "FLUSH_WAIT_FINISH", + "FLUSH_WAIT_START": "FLUSH_WAIT_START", + "REFR_READY": "REFR_READY", + "REFR_REQUEST": "REFR_REQUEST", + "REFR_START": "REFR_START", + "RENDER_READY": "RENDER_READY", + "RENDER_START": "RENDER_START", + "RESOLUTION_CHANGE": "RESOLUTION_CHANGED", "UPDATE_LAYOUT_COMPLETE": "UPDATE_LAYOUT_COMPLETED", "VSYNC": "VSYNC", "VSYNC_REQUEST": "VSYNC_REQUEST", } LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) +LV_DISPLAY_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_DISPLAY_EVENT_MAP) +LV_SCREEN_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_SCREEN_EVENT_MAP) + SWIPE_TRIGGERS = tuple( f"on_swipe_{x.lower()}" for x in DIRECTIONS.choices + ("up", "down") ) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 54309cdf891..f825999e8a3 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -8,16 +8,21 @@ from esphome.const import ( CONF_X, CONF_Y, ) -from esphome.cpp_generator import new_Pvariable +from esphome.cpp_generator import MockObj, new_Pvariable from esphome.cpp_helpers import register_component +from esphome.cpp_types import nullptr from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, CONF_ALIGN_TO_LAMBDA_ID, DIRECTIONS, + LV_DISPLAY_EVENT_MAP, + LV_DISPLAY_EVENT_TRIGGERS, LV_EVENT_MAP, LV_EVENT_TRIGGERS, + LV_SCREEN_EVENT_MAP, + LV_SCREEN_EVENT_TRIGGERS, SWIPE_TRIGGERS, literal, ) @@ -30,6 +35,7 @@ from .lvcode import ( lv, lv_add, lv_event_t_ptr, + lv_expr, lvgl_static, ) from .types import LV_EVENT @@ -49,25 +55,24 @@ async def generate_triggers(): Must be done after all widgets completed """ + all_triggers = ( + LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS + ) for w in widget_map.values(): + config = w.config if isinstance(w.type, LvScrActType): w = get_screen_active(w.var) - if w.config: + if config: for event, conf in { - event: conf - for event, conf in w.config.items() - if event in LV_EVENT_TRIGGERS + event: conf for event, conf in config.items() if event in all_triggers }.items(): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") - event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) await add_trigger(conf, w, event) for event, conf in { - event: conf - for event, conf in w.config.items() - if event in SWIPE_TRIGGERS + event: conf for event, conf in config.items() if event in SWIPE_TRIGGERS }.items(): conf = conf[0] dir = event[9:].upper() @@ -77,11 +82,9 @@ async def generate_triggers(): selected = literal( f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) - await add_trigger( - conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected - ) + await add_trigger(conf, w, "GESTURE", is_selected=selected) - for conf in w.config.get(CONF_ON_VALUE, ()): + for conf in config.get(CONF_ON_VALUE, ()): await add_trigger( conf, w, @@ -90,7 +93,7 @@ async def generate_triggers(): UPDATE_EVENT, ) - await add_on_boot_triggers(w.config.get(CONF_ON_BOOT, ())) + await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) async def generate_align_tos(config: dict): @@ -119,6 +122,17 @@ async def generate_align_tos(config: dict): await register_component(var, {}) +TRIGGER_MAP = LV_EVENT_MAP | LV_DISPLAY_EVENT_MAP | LV_SCREEN_EVENT_MAP +DISPLAY_TRIGGERS = set(LV_DISPLAY_EVENT_TRIGGERS) + + +def _get_event_literal(trigger: str | MockObj) -> MockObj: + if isinstance(trigger, MockObj): + return trigger + trigger = trigger.removeprefix("on_") + return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()]) + + async def add_trigger(conf, w, *events, is_selected=None): is_selected = is_selected or w.is_selected() tid = conf[CONF_TRIGGER_ID] @@ -129,4 +143,14 @@ async def add_trigger(conf, w, *events, is_selected=None): async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(is_selected): lv_add(trigger.trigger(*value, literal("event"))) - lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events)) + callback = await context.get_lambda() + event_literals = [_get_event_literal(event) for event in events] + if isinstance(events[0], str) and events[0] in DISPLAY_TRIGGERS: + assert len(events) == 1 + lv.display_add_event_cb( + lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr + ) + else: + lv_add( + lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *event_literals) + ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 4d44c62000c..967fe515927 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -49,6 +49,44 @@ lvgl: id: meter_arc_indicator start_value: 0 end_value: 180 + on_invalidate_area: + logger.log: Invalidate area + on_resolution_change: + logger.log: Resolution changed + on_color_format_change: + logger.log: Color format changed + on_refr_request: + logger.log: Refresh request + on_refr_start: + logger.log: Refresh start + on_refr_ready: + logger.log: Refresh ready + on_render_start: + logger.log: Render start + on_render_ready: + logger.log: Render ready + on_flush_start: + logger.log: Flush start + on_flush_finish: + logger.log: Flush finish + on_flush_wait_start: + logger.log: Flush wait start + on_flush_wait_finish: + logger.log: Flush wait finish + on_update_layout_complete: + logger.log: Update layout complete + on_vsync: + logger.log: Vsync + on_vsync_request: + logger.log: Vsync request + on_screen_load_start: + logger.log: Screen load start + on_screen_load: + logger.log: Screen loaded + on_screen_unload: + logger.log: Screen unloaded + on_screen_unload_start: + logger.log: Screen unload start bg_color: light_blue bottom_layer: widgets: @@ -660,14 +698,6 @@ lvgl: logger.log: Child created on_child_delete: logger.log: Child deleted - on_screen_unload_start: - logger.log: Screen unload start - on_screen_load_start: - logger.log: Screen load start - on_screen_load: - logger.log: Screen loaded - on_screen_unload: - logger.log: Screen unloaded on_size_change: logger.log: Size changed on_style_change: @@ -676,36 +706,6 @@ lvgl: logger.log: Layout changed on_get_self_size: logger.log: Get self size - on_invalidate_area: - logger.log: Invalidate area - on_resolution_change: - logger.log: Resolution changed - on_color_format_change: - logger.log: Color format changed - on_refr_request: - logger.log: Refresh request - on_refr_start: - logger.log: Refresh start - on_refr_ready: - logger.log: Refresh ready - on_render_start: - logger.log: Render start - on_render_ready: - logger.log: Render ready - on_flush_start: - logger.log: Flush start - on_flush_finish: - logger.log: Flush finish - on_flush_wait_start: - logger.log: Flush wait start - on_flush_wait_finish: - logger.log: Flush wait finish - on_update_layout_complete: - logger.log: Update layout complete - on_vsync: - logger.log: Vsync - on_vsync_request: - logger.log: Vsync request - led: id: lv_led color: 0x00FF00 From 9bd936112d6947555d3864ef39b3f33e85bff49b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:54:09 +0200 Subject: [PATCH 1254/2056] [nextion] Fix queue age check using inconsistent time sources (#15317) --- esphome/components/nextion/nextion.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 01ceb3d765e..d0e87d4fdf4 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1064,7 +1064,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->nextion_queue_.push_back(nextion_queue); From 9036c29c8a1122faac1ff145de79dd303d741044 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:04:01 +1000 Subject: [PATCH 1255/2056] [online_image] Clear LVGL dsc when image size changes. (#15360) --- esphome/components/runtime_image/runtime_image.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 5a4a2ea7d69..25cf7c8ab4f 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -247,6 +247,9 @@ void RuntimeImage::release_buffer_() { this->height_ = 0; this->buffer_width_ = 0; this->buffer_height_ = 0; +#ifdef USE_LVGL + memset(&this->dsc_, 0, sizeof(this->dsc_)); +#endif } } From 162c8810db89d3dc09e2f5f1c84bb890d476ca18 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:11:29 +1000 Subject: [PATCH 1256/2056] [esp32] Clean build when sdkconfig options change (#15439) --- esphome/components/esp32/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 475de6aa3e4..72d0e42971e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -47,7 +47,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache +from esphome.writer import clean_build, clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa @@ -1911,6 +1911,7 @@ def _write_sdkconfig(): if write_file_if_changed(internal_path, contents): # internal changed, update real one write_file_if_changed(sdk_path, contents) + clean_build(clear_pio_cache=False) def _write_idf_component_yml(): From 1c67e4ce4c5d50419d0d610a4c50e99554e92337 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:50:41 +1200 Subject: [PATCH 1257/2056] Bump version to 2026.3.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 97201d1c445..7d0fcb9a277 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.3.2 +PROJECT_NUMBER = 2026.3.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index ebab56193cb..6037065a21e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.3.2" +__version__ = "2026.3.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 62d0c25a2bfd3da6afcfdd3c9f869c18432cbe18 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:14:59 +1200 Subject: [PATCH 1258/2056] [CI] Add branches-ignore for release and beta in PR title check (#15491) --- .github/workflows/pr-title-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 2ad023ed1b0..0021654def6 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -3,6 +3,9 @@ name: PR Title Check on: pull_request: types: [opened, edited, synchronize, reopened] + branches-ignore: + - release + - beta permissions: contents: read From 29ca7bc8f930335201d3827b95c1efbbfe131fe0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:57:16 -0400 Subject: [PATCH 1259/2056] [espnow] Fix string data generating invalid C++ char literals (#15493) --- esphome/components/espnow/__init__.py | 2 +- tests/components/espnow/common.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1c8d262810d..49b96eeb6f7 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -245,7 +245,7 @@ async def send_action( data = config.get(CONF_DATA, []) if isinstance(data, str): - data = [cg.RawExpression(f"'{c}'") for c in data] + data = list(data.encode()) templ = await cg.templatable(data, args, byte_vector, byte_vector) cg.add(var.set_data(templ)) diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index b724af54e05..bdc478ea036 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -29,6 +29,8 @@ espnow: data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};' - espnow.broadcast: data: "Hello, World!" + - espnow.broadcast: + data: "it's a test" - espnow.broadcast: data: [0x01, 0x02, 0x03, 0x04, 0x05] - espnow.broadcast: From d9da91efbe2de7c967efa2fce6e6462c9afd9862 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:14:17 -0400 Subject: [PATCH 1260/2056] [bl0940] Fix restore_value reading from wrong config dict (#15492) --- esphome/components/bl0940/number/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py index a640c2ae08f..92ab2837b34 100644 --- a/esphome/components/bl0940/number/__init__.py +++ b/esphome/components/bl0940/number/__init__.py @@ -89,6 +89,6 @@ async def to_code(config): ) await cg.register_component(var, conf) - if restore_value := config.get(CONF_RESTORE_VALUE): + if restore_value := conf.get(CONF_RESTORE_VALUE): cg.add(var.set_restore_value(restore_value)) cg.add(getattr(bl0940, setter_method)(var)) From 14bcd9db59411348a22d8da887ca2fcd6380d87e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:18:59 -0400 Subject: [PATCH 1261/2056] [neopixelbus] Fix SPI pin validation accepting one wrong pin on ESP8266 (#15494) --- esphome/components/neopixelbus/_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index 9072f78035f..e1c327a2e0b 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -344,7 +344,7 @@ def _spi_extra_validate(config): if CORE.is_esp32: return - if config[CONF_DATA_PIN] != 13 and config[CONF_CLOCK_PIN] != 14: + if config[CONF_DATA_PIN] != 13 or config[CONF_CLOCK_PIN] != 14: raise cv.Invalid( "SPI only supports pins GPIO13 for data and GPIO14 for clock on ESP8266" ) From c6e683cc33c35e789c40bf93645e52876391680d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:19:53 -0400 Subject: [PATCH 1262/2056] [pmsx003] Connect model-specific sensor validation to schema (#15495) --- esphome/components/pmsx003/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index cdcedc85acc..0a11120bf03 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -185,7 +185,7 @@ def validate_update_interval(value): return value -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(PMSX003Component), @@ -290,7 +290,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + validate_pmsx003_sensors, ) From 0816579fa93b5573ac6b4ed68c0628dfa84b4b0b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:20:46 -0400 Subject: [PATCH 1263/2056] [prometheus] Fix relabel validation not checking for required keys (#15496) --- esphome/components/prometheus/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index 26a9e70f7c7..cc1541ce804 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -10,12 +10,14 @@ AUTO_LOAD = ["web_server_base"] prometheus_ns = cg.esphome_ns.namespace("prometheus") PrometheusHandler = prometheus_ns.class_("PrometheusHandler", cg.Component) -CUSTOMIZED_ENTITY = cv.Schema( - { - cv.Optional(CONF_ID): cv.string_strict, - cv.Optional(CONF_NAME): cv.string_strict, - }, - cv.has_at_least_one_key, +CUSTOMIZED_ENTITY = cv.All( + cv.Schema( + { + cv.Optional(CONF_ID): cv.string_strict, + cv.Optional(CONF_NAME): cv.string_strict, + }, + ), + cv.has_at_least_one_key(CONF_ID, CONF_NAME), ) CONFIG_SCHEMA = cv.Schema( From b155c13117b63a50ddacf70c34bc99f59309aaf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 14:25:53 -1000 Subject: [PATCH 1264/2056] [api] Use integer comparison for float zero checks in protobuf encoding (#15490) --- esphome/components/api/proto.h | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 902d4c0f5c4..48da1f22268 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -25,6 +25,19 @@ constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded me constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag +// Reinterpret float bits as uint32_t without floating-point comparison. +// Used by both encode_float() and calc_float() to ensure identical zero checks. +// Uses union type-punning which is a GCC/Clang extension (not standard C++), +// but bit_cast/memcpy don't optimize to a no-op on xtensa-gcc (ESP8266). +inline uint32_t float_to_raw(float value) { + union { + float f; + uint32_t u; + } v; + v.f = value; + return v.u; +} + // Helper functions for ZigZag encoding/decoding inline constexpr uint32_t encode_zigzag32(int32_t value) { return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); @@ -432,14 +445,10 @@ class ProtoEncode { // is needed in the future, the necessary encoding/decoding functions must be added. static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value, bool force = false) { - if (value == 0.0f && !force) + uint32_t raw = float_to_raw(value); + if (raw == 0 && !force) return; - union { - float value; - uint32_t raw; - } val{}; - val.value = value; - encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, val.raw); + encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw); } static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value, bool force = false) { @@ -751,8 +760,8 @@ class ProtoSize { } static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } - static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { - return value != 0.0f ? field_id_size + 4 : 0; + static uint32_t calc_float(uint32_t field_id_size, float value) { + return float_to_raw(value) != 0 ? field_id_size + 4 : 0; } static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { return value ? field_id_size + 4 : 0; From 094e0440c68e52c0eb74664e4174bfc85fb89dfb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:30:36 -0400 Subject: [PATCH 1265/2056] [config] Fix unfilled placeholder in dimensions() error message (#15498) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 09f460f46b5..c6b67e9f35b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1667,7 +1667,7 @@ def dimensions(value): match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: raise Invalid( - "Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed." + f"Invalid value '{value}' for dimensions. Only WIDTHxHEIGHT is allowed." ) return dimensions([match.group(1), match.group(2)]) From 4fa3e48d3353b1cb50fd68a978b35d2dd8f6b517 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:34:07 -0400 Subject: [PATCH 1266/2056] [remote_base] Fix misc protocol schema and codegen bugs (#15497) --- esphome/components/remote_base/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a0594d7f679..99eda76f81a 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -470,7 +470,7 @@ CANALSATLD_SCHEMA = cv.Schema( ) -@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSAT_SCHEMA) +@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSATLD_SCHEMA) def canalsatld_binary_sensor(var, config): cg.add( var.set_data( @@ -1130,7 +1130,7 @@ def sony_dumper(var, config): async def sony_action(var, config, args): template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) @@ -1174,7 +1174,7 @@ def symphony_dumper(var, config): async def symphony_action(var, config, args): template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint8) cg.add(var.set_repeats(template_)) @@ -1188,7 +1188,7 @@ def validate_raw_alternating(value): this_negative = val < 0 if i != 0 and this_negative == last_negative: raise cv.Invalid( - f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", + f"Values must alternate between being positive and negative, please see index {i - 1} and {i}", [i], ) last_negative = this_negative @@ -2105,12 +2105,12 @@ async def abbwelcome_action(var, config, args): ) cg.add( var.set_source_address( - await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint16) + await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint32) ) ) cg.add( var.set_destination_address( - await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint16) + await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint32) ) ) cg.add( From d15fa84f4f5a33e9fd48597a278e3f8bb3645193 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 14:39:55 -1000 Subject: [PATCH 1267/2056] [api] Auto-derive max_value for enum fields in protobuf codegen (#15469) --- esphome/components/api/api_pb2.cpp | 130 +++++++++++++--------------- script/api_protobuf/api_protobuf.py | 62 ++++++++----- 2 files changed, 98 insertions(+), 94 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ed4711bfaac..ba9ebd1f40d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -87,7 +87,7 @@ uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PAR uint32_t SerialProxyInfo::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->port_type)); + size += this->port_type ? 2 : 0; return size; } #endif @@ -244,7 +244,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -305,7 +305,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -328,7 +328,7 @@ uint32_t CoverStateResponse::calculate_size() const { size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_float(1, this->tilt); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); + size += this->current_operation ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -408,7 +408,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { size += ProtoSize::calc_length_force(1, strlen(it)); @@ -437,7 +437,7 @@ uint32_t FanStateResponse::calculate_size() const { size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->oscillating); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->direction)); + size += this->direction ? 2 : 0; size += ProtoSize::calc_int32(1, this->speed_level); size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES @@ -536,9 +536,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { size += 5; size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { - for (const auto &it : *this->supported_color_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); - } + size += this->supported_color_modes->size() * 2; } size += ProtoSize::calc_float(1, this->min_mireds); size += ProtoSize::calc_float(1, this->max_mireds); @@ -551,7 +549,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(2, this->device_id); #endif @@ -582,7 +580,7 @@ uint32_t LightStateResponse::calculate_size() const { size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_float(1, this->brightness); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->color_mode)); + size += this->color_mode ? 2 : 0; size += ProtoSize::calc_float(1, this->color_brightness); size += ProtoSize::calc_float(1, this->red); size += ProtoSize::calc_float(1, this->green); @@ -739,9 +737,9 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->accuracy_decimals); size += ProtoSize::calc_bool(1, this->force_update); size += ProtoSize::calc_length(1, this->device_class.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state_class)); + size += this->state_class ? 2 : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -796,7 +794,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { #endif size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -873,7 +871,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -922,7 +920,7 @@ uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEB } uint32_t SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->level)); + size += this->level ? 2 : 0; size += ProtoSize::calc_length(1, this->message_len_); return size; } @@ -1171,7 +1169,7 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC uint32_t ListEntitiesServicesArgument::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += this->type ? 2 : 0; return size; } uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { @@ -1193,7 +1191,7 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const { size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->supports_response)); + size += this->supports_response ? 2 : 0; return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { @@ -1347,7 +1345,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); #endif - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1441,23 +1439,17 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { - for (const auto &it : *this->supported_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); - } + size += this->supported_modes->size() * 2; } size += ProtoSize::calc_float(1, this->visual_min_temperature); size += ProtoSize::calc_float(1, this->visual_max_temperature); size += ProtoSize::calc_float(1, this->visual_target_temperature_step); size += ProtoSize::calc_bool(1, this->supports_action); if (!this->supported_fan_modes->empty()) { - for (const auto &it : *this->supported_fan_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); - } + size += this->supported_fan_modes->size() * 2; } if (!this->supported_swing_modes->empty()) { - for (const auto &it : *this->supported_swing_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); - } + size += this->supported_swing_modes->size() * 2; } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { @@ -1465,9 +1457,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } } if (!this->supported_presets->empty()) { - for (const auto &it : *this->supported_presets) { - size += ProtoSize::calc_uint32_force(2, static_cast<uint32_t>(it)); - } + size += this->supported_presets->size() * 3; } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { @@ -1478,7 +1468,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(2, this->icon.size()); #endif - size += ProtoSize::calc_uint32(2, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 3 : 0; size += ProtoSize::calc_float(2, this->visual_current_temperature_step); size += ProtoSize::calc_bool(2, this->supports_current_humidity); size += ProtoSize::calc_bool(2, this->supports_target_humidity); @@ -1514,16 +1504,16 @@ uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBU uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += this->mode ? 2 : 0; size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); size += ProtoSize::calc_float(1, this->target_temperature_low); size += ProtoSize::calc_float(1, this->target_temperature_high); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->action)); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->fan_mode)); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->swing_mode)); + size += this->action ? 2 : 0; + size += this->fan_mode ? 2 : 0; + size += this->swing_mode ? 2 : 0; size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->preset)); + size += this->preset ? 2 : 0; size += ProtoSize::calc_length(1, this->custom_preset.size()); size += ProtoSize::calc_float(1, this->current_humidity); size += ProtoSize::calc_float(1, this->target_humidity); @@ -1656,7 +1646,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1664,9 +1654,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += ProtoSize::calc_float(1, this->max_temperature); size += ProtoSize::calc_float(1, this->target_temperature_step); if (!this->supported_modes->empty()) { - for (const auto &it : *this->supported_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast<uint32_t>(it)); - } + size += this->supported_modes->size() * 2; } size += ProtoSize::calc_uint32(1, this->supported_features); return size; @@ -1690,7 +1678,7 @@ uint32_t WaterHeaterStateResponse::calculate_size() const { size += 5; size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += this->mode ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1774,9 +1762,9 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { size += ProtoSize::calc_float(1, this->max_value); size += ProtoSize::calc_float(1, this->step); size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += this->mode ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -1862,7 +1850,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { } } size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1959,7 +1947,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { } size += ProtoSize::calc_bool(1, this->supports_duration); size += ProtoSize::calc_bool(1, this->supports_volume); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2067,7 +2055,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_open); size += ProtoSize::calc_bool(1, this->requires_code); @@ -2089,7 +2077,7 @@ uint8_t *LockStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_P uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += this->state ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2161,7 +2149,7 @@ uint32_t ListEntitiesButtonResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -2206,7 +2194,7 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { size += ProtoSize::calc_length(1, this->format.size()); size += ProtoSize::calc_uint32(1, this->sample_rate); size += ProtoSize::calc_uint32(1, this->num_channels); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->purpose)); + size += this->purpose ? 2 : 0; size += ProtoSize::calc_uint32(1, this->sample_bytes); return size; } @@ -2239,7 +2227,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->supports_pause); if (!this->supported_formats.empty()) { for (const auto &it : this->supported_formats) { @@ -2266,7 +2254,7 @@ uint8_t *MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_ uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += this->state ? 2 : 0; size += ProtoSize::calc_float(1, this->volume); size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES @@ -2348,7 +2336,7 @@ uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCO ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi)); if (this->address_type) { ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->address_type)); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type); } ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len)); @@ -2762,9 +2750,9 @@ uint8_t *BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_EN } uint32_t BluetoothScannerStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->configured_mode)); + size += this->state ? 2 : 0; + size += this->mode ? 2 : 0; + size += this->configured_mode ? 2 : 0; return size; } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { @@ -3116,7 +3104,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_uint32(1, this->supported_features); size += ProtoSize::calc_bool(1, this->requires_code); size += ProtoSize::calc_bool(1, this->requires_code_to_arm); @@ -3137,7 +3125,7 @@ uint8_t *AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer PROTO_E uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->state)); + size += this->state ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3209,11 +3197,11 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_uint32(1, this->min_length); size += ProtoSize::calc_uint32(1, this->max_length); size += ProtoSize::calc_length(1, this->pattern.size()); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->mode)); + size += this->mode ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3298,7 +3286,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3385,7 +3373,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3476,7 +3464,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { @@ -3536,7 +3524,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); @@ -3560,7 +3548,7 @@ uint32_t ValveStateResponse::calculate_size() const { uint32_t size = 0; size += 5; size += ProtoSize::calc_float(1, this->position); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->current_operation)); + size += this->current_operation ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3623,7 +3611,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3701,7 +3689,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3821,7 +3809,7 @@ uint8_t *ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_P } uint32_t ZWaveProxyRequest::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); + size += this->type ? 2 : 0; size += ProtoSize::calc_length(1, this->data_len); return size; } @@ -3853,7 +3841,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->icon.size()); #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -4048,8 +4036,8 @@ uint8_t *SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD uint32_t SerialProxyRequestResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->instance); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type)); - size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->status)); + size += this->type ? 2 : 0; + size += this->status ? 2 : 0; size += ProtoSize::calc_length(1, this->error_message.size()); return size; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3833f279ce7..6de5c2b1c7d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -56,6 +56,10 @@ FILE_HEADER = """// This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py """ +# Populated by main() before any TypeInfo creation. +# Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value. +_enum_max_values: dict[str, int] = {} + def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" @@ -240,12 +244,6 @@ class TypeInfo(ABC): "encode_bool": "ProtoEncode::write_raw_byte(pos, {value} ? 0x01 : 0x00);", } - # When max_value < 128, the varint is always 1 byte — use a direct byte write - RAW_ENCODE_SMALL_MAP: dict[str, str] = { - "encode_uint32": "ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({value}));", - "encode_uint64": "ProtoEncode::write_raw_byte(pos, static_cast<uint8_t>({value}));", - } - def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: """Try to emit a precomputed-tag encode for a field. @@ -255,19 +253,14 @@ class TypeInfo(ABC): Returns the raw encode string if the tag is a single byte and the encode_func has a known raw equivalent, or None otherwise. - When max_value < 128, uses direct byte write instead of varint encoding. """ tag = self.calculate_tag() if tag >= 128: return None max_val = self.max_value + # Only use RAW_ENCODE_MAP for forced fields or fields with max_value raw_expr = None - if max_val is not None and max_val < 128: - raw_expr = self.RAW_ENCODE_SMALL_MAP.get(self.encode_func) - if raw_expr is None: - # Only use RAW_ENCODE_MAP for forced fields or fields with max_value - if not self.force and max_val is None: - return None + if self.force or max_val is not None: raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None @@ -1321,19 +1314,24 @@ class EnumType(TypeInfo): default_value = "" wire_type = WireType.VARINT # Uses wire type 0 + @property + def max_value(self) -> int | None: + """Get max_value from explicit annotation or auto-derive from enum definition.""" + explicit = super().max_value + if explicit is not None: + return explicit + return _enum_max_values.get(self._field.type_name) + @property def encode_func(self) -> str: return "encode_uint32" @property def encode_content(self) -> str: - if result := self._encode_with_precomputed_tag( - f"static_cast<uint32_t>(this->{self.field_name})" - ): - return result + value_expr = f"static_cast<uint32_t>(this->{self.field_name})" if self.force: - return f"ProtoEncode::{self.encode_func}(pos, {self.number}, static_cast<uint32_t>(this->{self.field_name}), true);" - return f"ProtoEncode::{self.encode_func}(pos, {self.number}, static_cast<uint32_t>(this->{self.field_name}));" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr});" def dump(self, name: str) -> str: return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" @@ -1343,6 +1341,9 @@ class EnumType(TypeInfo): return f"static_cast<{self.cpp_type}>({value})" def get_size_calculation(self, name: str, force: bool = False) -> str: + max_val = self.max_value + if max_val is not None and max_val < 128: + return self._get_single_byte_varint_size(name, force) return self._get_simple_size_calculation( name, force, "uint32", f"static_cast<uint32_t>({name})" ) @@ -1905,17 +1906,27 @@ class RepeatedTypeInfo(TypeInfo): size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" o += f" size += {size_expr} * {bytes_per_element};\n" else: - # Other types need the actual value + # Check if inner type produces a constant size (doesn't depend on value) + inner_size = self._ti.get_size_calculation("it", True) + if "it" not in inner_size: + # Constant size per element — use multiply instead of loop + # Extract the constant from "size += N;" + const_val = ( + inner_size.strip().removeprefix("size += ").removesuffix(";") + ) + size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" + o += f" size += {size_expr} * {const_val};\n" # Special handling for const char* elements - if self._use_pointer and "const char" in self._container_no_template: + elif self._use_pointer and "const char" in self._container_no_template: field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" o += f" size += ProtoSize::calc_length_force({field_id_size}, strlen(it));\n" + o += " }\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" - o += " }\n" + o += f" {inner_size}\n" + o += " }\n" o += "}" return o @@ -2788,6 +2799,11 @@ def main() -> None: file = d.file[0] + # Build enum max value map so EnumType can auto-derive max_value + for enum in file.enum_type: + if not enum.options.deprecated and enum.value: + _enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value) + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( build_type_usage_map(file) From 82dc80a4138df4005ca6469e27d1bc71753b3abc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 15:26:40 -1000 Subject: [PATCH 1268/2056] [scheduler] Skip cancel for anonymous items, add empty-container fast path (#15397) --- esphome/core/scheduler.cpp | 24 +++++++++++++++++++++--- esphome/core/scheduler.h | 30 +++++++++++++++--------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9ee3b2fdd2a..71b29390d6b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -214,8 +214,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif /* ESPHOME_DEBUG_SCHEDULER */ } - // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) - if (!skip_cancel) { + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous) + // Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan. + if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, /* find_first= */ true); } @@ -742,6 +743,23 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const // When find_first=false, cancels ALL matches across all containers (needed for // public cancel path where DelayAction parallel mode can create duplicates). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id +size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vector<SchedulerItem *> &container, + Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, + SchedulerItem::Type type, bool match_retry, + bool find_first) { + size_t count = 0; + for (auto *item : container) { + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + this->set_item_removed_(item, true); + if (find_first) + return 1; + count++; + } + } + return count; +} + bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first) { @@ -767,7 +785,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // The main loop may be executing an item's callback right now, and recycling // would destroy the callback while it's running (use-after-free). // Only the main loop in call() should recycle items after execution completes. - if (!this->items_.empty()) { + { size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry, find_first); total_cancelled += heap_cancelled; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1e44f41da84..43a3ec7049b 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -495,23 +495,23 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal. // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, - Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry, - bool find_first = false) { - size_t count = 0; - for (auto *item : container) { - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { - this->set_item_removed_(item, true); - if (find_first) - return 1; - count++; - } - } - return count; + // Inlined: the fast path (empty container) avoids calling the out-of-line scan. + inline size_t HOT mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component, + NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, + bool find_first = false) { + if (container.empty()) + return 0; + return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id, + type, match_retry, find_first); } + // Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty. + // IMPORTANT: Must be called with scheduler lock held + __attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_( + std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first); + Mutex lock_; std::vector<SchedulerItem *> items_; std::vector<SchedulerItem *> to_add_; From b8b8d1bb15eceaf389cf0e06b4da13ae35b655d3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:31:57 -0400 Subject: [PATCH 1269/2056] [core] Replace deprecated datetime.utcfromtimestamp() (#15503) --- esphome/external_files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/external_files.py b/esphome/external_files.py index 72a3f33fdc8..18b68fba088 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime import logging from pathlib import Path @@ -27,8 +27,8 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) try: local_modification_time = local_file_path.stat().st_mtime - local_modification_time_str = datetime.utcfromtimestamp( - local_modification_time + local_modification_time_str = datetime.fromtimestamp( + local_modification_time, tz=UTC ).strftime("%a, %d %b %Y %H:%M:%S GMT") headers = { From e428cb5092afcd78e8565bb992fc75da20b7f11c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:33:22 -0400 Subject: [PATCH 1270/2056] [multiple] Fix misc cosmetic bugs (batch 2) (#15502) --- esphome/components/atm90e32/sensor.py | 1 - esphome/components/esp8266/gpio.py | 2 +- esphome/components/espnow/__init__.py | 6 +++--- esphome/components/kamstrup_kmp/sensor.py | 3 ++- esphome/components/stepper/__init__.py | 2 +- esphome/zeroconf.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 09449504320..7e5d85c57a4 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -132,7 +132,6 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema( diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 43508afaf92..64be4a64952 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -155,7 +155,7 @@ ESP8266_PIN_SCHEMA = cv.All( @dataclass class PinInitialState: - mode = 255 + mode: int = 255 level: int = 255 diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 49b96eeb6f7..a9624734d02 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -158,15 +158,15 @@ def validate_peer(value): def _validate_raw_data(value): if isinstance(value, str): - if len(value) >= MAX_ESPNOW_PACKET_SIZE: + if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( - f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}" + f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}" ) return value if isinstance(value, list): if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( - f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}" + f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}" ) return cv.Schema([cv.hex_uint8_t])(value) raise cv.Invalid( diff --git a/esphome/components/kamstrup_kmp/sensor.py b/esphome/components/kamstrup_kmp/sensor.py index fb37ac2c8da..134ac245bf3 100644 --- a/esphome/components/kamstrup_kmp/sensor.py +++ b/esphome/components/kamstrup_kmp/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, @@ -75,7 +76,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_FLOW): sensor.sensor_schema( accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLUME, + device_class=DEVICE_CLASS_VOLUME_FLOW_RATE, state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=UNIT_LITRE_PER_HOUR, ), diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 27d4fc276d9..8acacc3b492 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -46,7 +46,7 @@ def validate_acceleration(value): def validate_speed(value): value = cv.string(value) - for suffix in ("steps/s", "steps/s"): + for suffix in ("steps/s",): value = value.removesuffix(suffix) if value == "inf": diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index dc4ca77eb45..dd45b58a6cb 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -25,7 +25,7 @@ _BACKGROUND_TASKS: set[asyncio.Task] = set() class DashboardStatus: - def __init__(self, on_update: Callable[[dict[str, bool | None], []]]) -> None: + def __init__(self, on_update: Callable[[dict[str, bool | None]], None]) -> None: """Initialize the dashboard status.""" self.on_update = on_update From e62c78ad46dc41f231fbe258147387d2c5262ebb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:41:57 -0400 Subject: [PATCH 1271/2056] [multiple] Fix misc cosmetic bugs (error messages, types, defaults) (#15499) --- esphome/components/binary_sensor/__init__.py | 2 +- esphome/components/lc709203f/sensor.py | 2 +- esphome/components/micro_wake_word/__init__.py | 2 +- esphome/components/rotary_encoder/sensor.py | 2 +- esphome/components/sprinkler/__init__.py | 2 +- esphome/components/st7789v/display.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 660f75ccd9e..d8cdaa5d58e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -390,7 +390,7 @@ def validate_multi_click_timing(value): new_state = v_.get(CONF_STATE, not state) if new_state == state: raise cv.Invalid( - f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}" + f"Timings must have alternating state. Indices {i - 1} and {i} have the same state {state}" ) if max_length is not None and max_length < min_length: raise cv.Invalid( diff --git a/esphome/components/lc709203f/sensor.py b/esphome/components/lc709203f/sensor.py index eb08a522e51..75ae7036380 100644 --- a/esphome/components/lc709203f/sensor.py +++ b/esphome/components/lc709203f/sensor.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(lc709203f), - cv.Optional(CONF_SIZE, default="500"): cv.int_range(100, 3000), + cv.Optional(CONF_SIZE, default=500): cv.int_range(100, 3000), cv.Optional(CONF_VOLTAGE, default="3.7"): cv.enum( BATTERY_VOLTAGE_OPTIONS, upper=True ), diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index fae48630b55..ff27dec6df0 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -405,7 +405,7 @@ def _model_config_to_manifest_data(model_config): file = _compute_local_file_path(model_config) / "manifest.json" else: - raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}") + raise ValueError(f"Unsupported config type: {model_config[CONF_TYPE]}") return _load_model_data(file) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index fc4202556d0..d88657e7152 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -50,7 +50,7 @@ def validate_min_max_value(config): max_val = config[CONF_MAX_VALUE] if min_val >= max_val: raise cv.Invalid( - f"Max value {max_val} must be smaller than min value {min_val}" + f"Max value {max_val} must be greater than min value {min_val}" ) return config diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 9dc695cafc8..fb2beb5b16f 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -272,7 +272,7 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( ), cv.Optional( CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND - ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), + ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower=True), } ) .extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index c9f41996160..85414237cfe 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -127,7 +127,7 @@ def validate_st7789v(config): if model_data[REQUIRE_PS] and CONF_POWER_SUPPLY not in config: raise cv.Invalid( - f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}"' + f"{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}" ) if ( From 96c398648178086ceaf9f28c99e37acfaed227c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 15:58:17 -1000 Subject: [PATCH 1272/2056] [core] Replace std::vector in CallbackManager with trivial-copy container (#15272) --- esphome/core/helpers.cpp | 13 ++++++++ esphome/core/helpers.h | 68 +++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1732fc72e8b..5940f6ec985 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -22,6 +22,19 @@ namespace esphome { static const char *const TAG = "helpers"; +__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, + size_t elem_size) { + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); + uint16_t new_cap = size + 1; + auto *new_data = ::operator new(new_cap *elem_size); + if (data) { + __builtin_memcpy(new_data, data, size * elem_size); + ::operator delete(data); + } + capacity = new_cap; + return new_data; +} + static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f96b888e287..c26bbe17b75 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1801,33 +1801,77 @@ template<typename... Ts> struct Callback<void(Ts...)> { } }; +/// Grow a CallbackManager's backing array to exactly size+1. Defined in helpers.cpp. +void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size); + template<typename... X> class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. + * + * Uses a trivial-copyable-specialized container instead of std::vector to avoid + * template bloat (_M_realloc_insert, exception-safe copies). Since Callback is + * trivially copyable (just {fn_ptr, ctx_ptr}), reallocation is a plain memcpy. + * Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector). + * Grows to exact size on each add — callbacks are registered during setup() + * and most instances have only 1-2 callbacks, so slack capacity is wasteful. * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ template<typename... Ts> class CallbackManager<void(Ts...)> { + using CbType = Callback<void(Ts...)>; + static_assert(std::is_trivially_copyable_v<CbType>, "Callback must be trivially copyable"); + public: + CallbackManager() = default; + ~CallbackManager() { ::operator delete(this->data_); } + + // Non-copyable (would alias data_), movable (for std::map support) + CallbackManager(const CallbackManager &) = delete; + CallbackManager &operator=(const CallbackManager &) = delete; + CallbackManager(CallbackManager &&other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + CallbackManager &operator=(CallbackManager &&other) noexcept { + std::swap(this->data_, other.data_); + std::swap(this->size_, other.size_); + std::swap(this->capacity_, other.capacity_); + return *this; + } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) /// are stored inline without heap allocation or std::function. - template<typename F> void add(F &&callback) { this->add_(Callback<void(Ts...)>::create(std::forward<F>(callback))); } - - /// Call all callbacks in this manager. No null check on invoke. - void call(Ts... args) { - for (auto &cb : this->callbacks_) - cb.call(args...); - } - size_t size() const { return this->callbacks_.size(); } + template<typename F> void add(F &&callback) { this->add_(CbType::create(std::forward<F>(callback))); } /// Call all callbacks in this manager. - void operator()(Ts... args) { call(args...); } + inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { + if (this->size_ != 0) { + for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { + it->call(args...); + } + } + } + uint16_t size() const { return this->size_; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } protected: template<typename...> friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. - void add_(Callback<void(Ts...)> cb) { this->callbacks_.push_back(cb); } - std::vector<Callback<void(Ts...)>> callbacks_; + /// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow(). + void add_(CbType cb) { + if (this->size_ == this->capacity_) { + this->data_ = + static_cast<CbType *>(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType))); + } + this->data_[this->size_++] = cb; + } + CbType *data_{nullptr}; + uint16_t size_{0}; + uint16_t capacity_{0}; }; /** CallbackManager backed by StaticVector for compile-time-known callback counts. @@ -1871,7 +1915,7 @@ template<typename... X> class LazyCallbackManager; * from API and web_server components). * * Memory overhead comparison (32-bit systems): - * - CallbackManager: 12 bytes (empty std::vector) + * - CallbackManager: 8 bytes (pointer + uint16 size + uint16 capacity) * - LazyCallbackManager: 4 bytes (nullptr pointer) * * Uses plain pointer instead of unique_ptr to avoid template instantiation overhead. From 517d0390d0affbac28159910df9013f139d39347 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:17:25 -0400 Subject: [PATCH 1273/2056] [ota] Fix check_error skipping validation for RESPONSE_OK (#15501) --- esphome/espota2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index 4b813e4060c..39f51e02e93 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -130,7 +130,7 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None :param expect: Expected response code(s), None to skip validation. :raises OTAError: If an error code is detected or response doesn't match expected. """ - if not expect: + if expect is None: return if not data: raise OTAError( @@ -278,7 +278,7 @@ def perform_ota( raise OTAError("ESP requests password, but no password given!") nonce_bytes = receive_exactly( - sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False + sock, nonce_size, f"{hash_name} authentication nonce", None, decode=False ) assert isinstance(nonce_bytes, bytes) nonce = nonce_bytes.decode() From 99ee405f4e427a639ed813bae611a9d42c12fe7d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:17:34 -0400 Subject: [PATCH 1274/2056] [esp32_ble][esp32_ble_server][esp32_ble_beacon] Fix UUID regex, IndexError, and unused inheritance (#15504) --- esphome/components/esp32_ble/__init__.py | 6 +++--- esphome/components/esp32_ble_beacon/__init__.py | 6 +----- .../components/esp32_ble_beacon/esp32_ble_beacon.h | 2 +- esphome/components/esp32_ble_server/__init__.py | 14 ++++++++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 974611c9b18..79d05049bfb 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -373,14 +373,14 @@ def bt_uuid(value): value = in_value.upper() if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F|0-9]{4,}$") + pattern = re.compile("^[A-F0-9]{4,}$") if not pattern.match(value): raise cv.Invalid( f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" ) return value if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F|0-9]{8,}$") + pattern = re.compile("^[A-F0-9]{8,}$") if not pattern.match(value): raise cv.Invalid( f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" @@ -388,7 +388,7 @@ def bt_uuid(value): return value if len(value) == len(bt_uuid128_format): pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + "^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$" ) if not pattern.match(value): raise cv.Invalid( diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index e2e790164ee..8052c13596b 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -10,11 +10,7 @@ AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") -ESP32BLEBeacon = esp32_ble_beacon_ns.class_( - "ESP32BLEBeacon", - cg.Component, - cg.Parented.template(esp32_ble.ESP32BLE), -) +ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) CONF_MAJOR = "major" CONF_MINOR = "minor" CONF_MIN_INTERVAL = "min_interval" diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index e16c413179f..44a71334543 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct { using namespace esp32_ble; -class ESP32BLEBeacon : public Component, public Parented<ESP32BLE> { +class ESP32BLEBeacon : public Component { public: explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {} diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 57106cd93be..7bf3092a4e8 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -307,24 +307,30 @@ def final_validate_config(config): # Check if all characteristics that require notifications have the notify property set for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): # Look for the characteristic in the configuration - char_config = [ + matches = [ char_conf for service_conf in config[CONF_SERVICES] for char_conf in service_conf[CONF_CHARACTERISTICS] if char_conf[CONF_ID] == char_id - ][0] + ] + if not matches: + continue + char_config = matches[0] if not char_config[CONF_NOTIFY]: raise cv.Invalid( f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set" ) for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()): # Look for the characteristic in the configuration - char_config = [ + matches = [ char_conf for service_conf in config[CONF_SERVICES] for char_conf in service_conf[CONF_CHARACTERISTICS] if char_conf[CONF_ID] == char_id - ][0] + ] + if not matches: + continue + char_config = matches[0] if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda): raise cv.Invalid( f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value" From 9894bdc0f1ee40f33537d31e10ed63a1f2cc4a0d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:03:57 -0400 Subject: [PATCH 1275/2056] [multiple] Fix misc low-priority bugs (batch 3) (#15506) --- esphome/components/display_menu_base/__init__.py | 2 +- esphome/components/ens160_base/__init__.py | 1 - esphome/components/font/__init__.py | 2 +- esphome/components/shelly_dimmer/light.py | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index c9a0c7ee93e..9125c43f0c7 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -119,7 +119,7 @@ DisplayMenuOnPrevTrigger = display_menu_base_ns.class_( def validate_format(format): - if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None: + if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None: raise cv.Invalid( f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided" ) diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py index 3b6ad8a4ee0..46c53c3b10c 100644 --- a/esphome/components/ens160_base/__init__.py +++ b/esphome/components/ens160_base/__init__.py @@ -24,7 +24,6 @@ CODEOWNERS = ["@vincentscode", "@latonita"] ens160_ns = cg.esphome_ns.namespace("ens160_base") CONF_AQI = "aqi" -UNIT_INDEX = "index" CONFIG_SCHEMA_BASE = cv.Schema( { diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index c8813bf1bc3..a1339a4bc11 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -238,7 +238,7 @@ def validate_font_config(config): return config -FONT_EXTENSIONS = (".ttf", ".woff", ".otf", "bdf", ".pcf") +FONT_EXTENSIONS = (".ttf", ".woff", ".otf", ".bdf", ".pcf") def validate_truetype_file(value): diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index c1e9cad3587..1688f9d6a60 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -67,7 +67,7 @@ KNOWN_FIRMWARE = { def parse_firmware_version(value): - match = re.match(r"(\d+).(\d+)", value) + match = re.fullmatch(r"(\d+)\.(\d+)", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) @@ -129,7 +129,7 @@ def validate_firmware(value): def validate_sha256(value): value = cv.string(value) - if not value.isalnum() or not len(value) == 64: + if not re.fullmatch(r"[0-9a-fA-F]{64}", value): raise ValueError(f"Not a valid SHA256 hex string: {value}") return value From b6ef1a58fb0c92dcc93388ab816cc6cd76b879af Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:17:35 -0400 Subject: [PATCH 1276/2056] [multiple] Fix validation ranges and error messages (#15508) --- esphome/components/bmp581_spi/sensor.py | 2 +- esphome/components/mcp23s08/__init__.py | 2 +- esphome/components/mcp23s17/__init__.py | 2 +- esphome/components/pcd8544/display.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/bmp581_spi/sensor.py b/esphome/components/bmp581_spi/sensor.py index 75f60b24604..db0d0cd529c 100644 --- a/esphome/components/bmp581_spi/sensor.py +++ b/esphome/components/bmp581_spi/sensor.py @@ -31,7 +31,7 @@ BMP581SPIComponent = bmp581_ns.class_( def check_spi_mode(config): spi_mode = config.get(CONF_SPI_MODE) if spi_mode not in VALID_SPI_MODES: - raise cv.Invalid("BMP581 only supports SPI mode 3") + raise cv.Invalid("BMP581 only supports SPI mode 0 or mode 3") return config diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py index 3d4e304f9bb..312da79b75c 100644 --- a/esphome/components/mcp23s08/__init__.py +++ b/esphome/components/mcp23s08/__init__.py @@ -18,7 +18,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(mcp23S08), - cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.int_range(min=0, max=3), } ) .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py index ea8433af2e1..599bfa08515 100644 --- a/esphome/components/mcp23s17/__init__.py +++ b/esphome/components/mcp23s17/__init__.py @@ -18,7 +18,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(mcp23S17), - cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.int_range(min=0, max=7), } ) .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index 9d993c21059..2f6dcc56ed3 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE - cv.Optional(CONF_CONTRAST, default=0x7F): cv.int_, + cv.Optional(CONF_CONTRAST, default=0x7F): cv.int_range(min=0, max=127), } ) .extend(cv.polling_component_schema("1s")) From 10b38e158844120956b7eba6caff8c980eafe43d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 17:31:01 -1000 Subject: [PATCH 1277/2056] [api] Add max_data_length proto option and optimize entity name/object_id (#15426) --- esphome/components/api/api.proto | 180 ++++++------ esphome/components/api/api_options.proto | 6 + esphome/components/api/api_pb2.cpp | 274 +++++++++---------- esphome/components/api/proto.h | 13 + esphome/components/number/__init__.py | 7 +- esphome/components/sensor/__init__.py | 7 +- esphome/core/config.py | 3 + esphome/core/entity_helpers.py | 11 +- script/api_protobuf/api_protobuf.py | 34 ++- tests/unit_tests/core/test_entity_helpers.py | 17 ++ 10 files changed, 321 insertions(+), 231 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1e03675999b..07705baff60 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -308,6 +308,12 @@ enum EntityCategory { ENTITY_CATEGORY_DIAGNOSTIC = 2; } +// Entity field max_data_length values match Python validation constants: +// name/object_id = 120 (config_validation.NAME_MAX_LENGTH) +// icon = 63 (core/config.ICON_MAX_LENGTH) +// device_class = 47 (core/config.DEVICE_CLASS_MAX_LENGTH) +// unit_of_measurement = 63 (core/config.UNIT_OF_MEASUREMENT_MAX_LENGTH) + // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; @@ -315,15 +321,15 @@ message ListEntitiesBinarySensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string device_class = 5; + string device_class = 5 [(max_data_length) = 47]; bool is_status_binary_sensor = 6; bool disabled_by_default = 7; - string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 9; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } @@ -349,17 +355,17 @@ message ListEntitiesCoverResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool assumed_state = 5; bool supports_position = 6; bool supports_tilt = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; bool supports_stop = 12; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -433,9 +439,9 @@ message ListEntitiesFanResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_oscillation = 5; @@ -443,7 +449,7 @@ message ListEntitiesFanResponse { bool supports_direction = 7; int32 supported_speed_count = 8; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -521,9 +527,9 @@ message ListEntitiesLightResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"]; @@ -540,7 +546,7 @@ message ListEntitiesLightResponse { float max_mireds = 10; repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"]; bool disabled_by_default = 13; - string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 15; uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } @@ -626,16 +632,16 @@ message ListEntitiesSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - string unit_of_measurement = 6; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; + string unit_of_measurement = 6 [(max_data_length) = 63]; int32 accuracy_decimals = 7; bool force_update = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; SensorStateClass state_class = 10; // Last reset type removed in 2021.9.0 // Deprecated in API version 1.5 @@ -666,16 +672,16 @@ message ListEntitiesSwitchResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message SwitchStateResponse { @@ -708,15 +714,15 @@ message ListEntitiesTextSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message TextSensorStateResponse { @@ -971,12 +977,12 @@ message ListEntitiesCameraResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; - string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } @@ -1056,9 +1062,9 @@ message ListEntitiesClimateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_current_temperature = 5; // Deprecated: use feature_flags @@ -1078,7 +1084,7 @@ message ListEntitiesClimateResponse { repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; bool disabled_by_default = 18; - string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; bool supports_current_humidity = 22; // Deprecated: use feature_flags @@ -1167,10 +1173,10 @@ message ListEntitiesWaterHeaterResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_WATER_HEATER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string name = 3 [(max_data_length) = 120, (force) = true]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; @@ -1243,20 +1249,20 @@ message ListEntitiesNumberResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; float min_value = 6; float max_value = 7; float step = 8; bool disabled_by_default = 9; EntityCategory entity_category = 10; - string unit_of_measurement = 11; + string unit_of_measurement = 11 [(max_data_length) = 63]; NumberMode mode = 12; - string device_class = 13; + string device_class = 13 [(max_data_length) = 47]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message NumberStateResponse { @@ -1292,12 +1298,12 @@ message ListEntitiesSelectResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; @@ -1336,12 +1342,12 @@ message ListEntitiesSirenResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"]; bool supports_duration = 8; @@ -1399,12 +1405,12 @@ message ListEntitiesLockResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; bool assumed_state = 8; @@ -1448,15 +1454,15 @@ message ListEntitiesButtonResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BUTTON"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message ButtonCommandRequest { @@ -1515,12 +1521,12 @@ message ListEntitiesMediaPlayerResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -2103,11 +2109,11 @@ message ListEntitiesAlarmControlPanelResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 supported_features = 8; @@ -2150,11 +2156,11 @@ message ListEntitiesTextResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -2198,12 +2204,12 @@ message ListEntitiesDateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2245,12 +2251,12 @@ message ListEntitiesTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2292,15 +2298,15 @@ message ListEntitiesEventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; @@ -2323,15 +2329,15 @@ message ListEntitiesValveResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool assumed_state = 9; bool supports_position = 10; @@ -2378,12 +2384,12 @@ message ListEntitiesDateTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2421,15 +2427,15 @@ message ListEntitiesUpdateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message UpdateStateResponse { @@ -2504,10 +2510,10 @@ message ListEntitiesInfraredResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_INFRARED"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string name = 3 [(max_data_length) = 120, (force) = true]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 0aa9e814cf8..0f71268d700 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -102,4 +102,10 @@ extend google.protobuf.FieldOptions { // and direct byte writes instead of varint branching, since the encoded varint // is guaranteed to be 1 byte. optional uint32 max_value = 50017; + + // max_data_length: Maximum length of a string or bytes field. + // When max_data_length < 128, the code generator emits constant-size + // length varint calculations and direct byte writes, since the length + // varint is guaranteed to be 1 byte. + optional uint32 max_data_length = 50018; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ba9ebd1f40d..f7c68b95a77 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -218,9 +218,9 @@ uint32_t DeviceInfoResponse::calculate_size() const { #ifdef USE_BINARY_SENSOR uint8_t *ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_class); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->is_status_binary_sensor); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); @@ -235,14 +235,14 @@ uint8_t *ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer PROTO } uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += 2 + this->name.size(); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES @@ -274,9 +274,9 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #ifdef USE_COVER uint8_t *ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->assumed_state); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_position); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_tilt); @@ -294,16 +294,16 @@ uint8_t *ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE } uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_tilt); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->supports_stop); @@ -375,9 +375,9 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_FAN uint8_t *ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_oscillation); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_speed); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_direction); @@ -397,16 +397,16 @@ uint8_t *ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_D } uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->supports_oscillation); size += ProtoSize::calc_bool(1, this->supports_speed); size += ProtoSize::calc_bool(1, this->supports_direction); size += ProtoSize::calc_int32(1, this->supported_speed_count); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += this->entity_category ? 2 : 0; if (!this->supported_preset_modes->empty()) { @@ -509,9 +509,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LIGHT uint8_t *ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); for (const auto &it : *this->supported_color_modes) { ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast<uint32_t>(it), true); } @@ -532,9 +532,9 @@ uint8_t *ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE } uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); if (!this->supported_color_modes->empty()) { size += this->supported_color_modes->size() * 2; } @@ -547,7 +547,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { } size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES @@ -707,9 +707,9 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SENSOR uint8_t *ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -727,16 +727,16 @@ uint8_t *ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += !this->unit_of_measurement.empty() ? 2 + this->unit_of_measurement.size() : 0; size += ProtoSize::calc_int32(1, this->accuracy_decimals); size += ProtoSize::calc_bool(1, this->force_update); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += this->state_class ? 2 : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -769,9 +769,9 @@ uint32_t SensorStateResponse::calculate_size() const { #ifdef USE_SWITCH uint8_t *ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -786,16 +786,16 @@ uint8_t *ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -848,9 +848,9 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_TEXT_SENSOR uint8_t *ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -864,15 +864,15 @@ uint8_t *ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_E } uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1323,9 +1323,9 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #ifdef USE_CAMERA uint8_t *ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->icon); @@ -1338,12 +1338,12 @@ uint8_t *ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES @@ -1388,9 +1388,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #ifdef USE_CLIMATE uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_current_temperature); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1433,9 +1433,9 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO } uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { @@ -1466,7 +1466,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(2, this->icon.size()); + size += !this->icon.empty() ? 3 + this->icon.size() : 0; #endif size += this->entity_category ? 3 : 0; size += ProtoSize::calc_float(2, this->visual_current_temperature_step); @@ -1617,9 +1617,9 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_WATER_HEATER uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif @@ -1639,11 +1639,11 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_ } uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -1731,9 +1731,9 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #ifdef USE_NUMBER uint8_t *ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -1752,20 +1752,20 @@ uint8_t *ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_float(1, this->min_value); size += ProtoSize::calc_float(1, this->max_value); size += ProtoSize::calc_float(1, this->step); size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += !this->unit_of_measurement.empty() ? 2 + this->unit_of_measurement.size() : 0; size += this->mode ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1820,9 +1820,9 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SELECT uint8_t *ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -1838,11 +1838,11 @@ uint8_t *ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif if (!this->options->empty()) { for (const char *it : *this->options) { @@ -1913,9 +1913,9 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SIREN uint8_t *ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -1933,11 +1933,11 @@ uint8_t *ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE } uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { @@ -2029,9 +2029,9 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LOCK uint8_t *ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -2048,11 +2048,11 @@ uint8_t *ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_ } uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -2126,9 +2126,9 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_BUTTON uint8_t *ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -2142,15 +2142,15 @@ uint8_t *ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2200,9 +2200,9 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { } uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -2220,11 +2220,11 @@ uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_ } uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3079,9 +3079,9 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #ifdef USE_ALARM_CONTROL_PANEL uint8_t *ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3097,11 +3097,11 @@ uint8_t *ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer } uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3171,9 +3171,9 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #ifdef USE_TEXT uint8_t *ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3190,11 +3190,11 @@ uint8_t *ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_ } uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3264,9 +3264,9 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATE uint8_t *ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3279,11 +3279,11 @@ uint8_t *ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_ } uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3351,9 +3351,9 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_TIME uint8_t *ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3366,11 +3366,11 @@ uint8_t *ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_ } uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3438,9 +3438,9 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_EVENT uint8_t *ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3457,15 +3457,15 @@ uint8_t *ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE } uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; if (!this->event_types->empty()) { for (const char *it : *this->event_types) { size += ProtoSize::calc_length_force(1, strlen(it)); @@ -3498,9 +3498,9 @@ uint32_t EventResponse::calculate_size() const { #ifdef USE_VALVE uint8_t *ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3517,15 +3517,15 @@ uint8_t *ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE } uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_stop); @@ -3589,9 +3589,9 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATETIME uint8_t *ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3604,11 +3604,11 @@ uint8_t *ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC } uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; @@ -3666,9 +3666,9 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_UPDATE uint8_t *ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif @@ -3682,15 +3682,15 @@ uint8_t *ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD } uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; - size += ProtoSize::calc_length(1, this->device_class.size()); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3817,9 +3817,9 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #ifdef USE_INFRARED uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->object_id); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif @@ -3834,11 +3834,11 @@ uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); size += this->entity_category ? 2 : 0; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 48da1f22268..ff7c5232b66 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -359,6 +359,19 @@ class ProtoEncode { std::memcpy(pos, data, len); pos += len; } + /// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128. + /// Tag must be a single-byte varint (< 128). Always encodes (no zero check). + static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, + const StringRef &ref) { +#ifdef ESPHOME_DEBUG_API + assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128"); +#endif + PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size()); + pos[0] = tag; + pos[1] = static_cast<uint8_t>(ref.size()); + std::memcpy(pos + 2, ref.c_str(), ref.size()); + pos += 2 + ref.size(); + } /// Write a precomputed tag byte + 32-bit value in one operation. static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 90f9fe18356..26d2602ba45 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -79,6 +79,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -186,7 +187,11 @@ NUMBER_OPERATION_OPTIONS = { } validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) _NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 626466eefa0..275c4542fb9 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -106,6 +106,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -290,7 +291,11 @@ ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) validate_accuracy_decimals = cv.int_ validate_icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") diff --git a/esphome/core/config.py b/esphome/core/config.py index 62c41b254c4..31cfd00ef7a 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -233,6 +233,9 @@ DEVICE_CLASS_MAX_LENGTH = 47 # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 +# Max unit of measurement string length +UNIT_OF_MEASUREMENT_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 0589b92364a..fc931c2baa1 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,11 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH +from esphome.core.config import ( + DEVICE_CLASS_MAX_LENGTH, + ICON_MAX_LENGTH, + UNIT_OF_MEASUREMENT_MAX_LENGTH, +) from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -200,6 +204,11 @@ def register_device_class(value: str) -> int: def register_unit_of_measurement(value: str) -> int: """Register a unit_of_measurement string and return its 1-based index.""" + if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH: + raise ValueError( + f"Unit of measurement string too long ({len(value)} chars, " + f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 6de5c2b1c7d..526644842da 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -165,6 +165,11 @@ class TypeInfo(ABC): """Get the max_value option for this field, or None if not set.""" return get_field_opt(self._field, pb.max_value, None) + @property + def max_data_length(self) -> int | None: + """Get the max_data_length option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_data_length, None) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -373,7 +378,11 @@ class TypeInfo(ABC): return f"size += ProtoSize::{method}({field_id_size}, {value});" def _get_single_byte_varint_size( - self, name: str, force: bool, extra_expr: str | None = None + self, + name: str, + force: bool, + extra_expr: str | None = None, + zero_check: str | None = None, ) -> str: """Size calculation when the varint is guaranteed to be 1 byte. @@ -384,12 +393,14 @@ class TypeInfo(ABC): name: Expression to check for zero (non-force only) force: Whether to skip the zero check extra_expr: Additional variable expression to add (e.g., data length) + zero_check: Override expression for the zero check (e.g., "!x.empty()") """ fixed = self.calculate_field_id_size() + 1 size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed) if force: return f"size += {size_expr};" - return f"size += {name} ? {size_expr} : 0;" + check = zero_check or name + return f"size += {check} ? {size_expr} : 0;" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -1065,8 +1076,14 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + max_len = self.max_data_length + if max_len is not None and max_len < 128 and self.force: + tag = self.calculate_tag() + if tag < 128: + return f"ProtoEncode::encode_short_string_force(pos, {tag}, this->{self.field_name});" if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + f"this->{self.field_name}.c_str()", + f"this->{self.field_name}.size()", ): return result if self.force: @@ -1091,7 +1108,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + size_field = f"this->{self.field_name}.size()" + max_len = self.max_data_length + if max_len is not None and max_len < 128: + return self._get_single_byte_varint_size( + size_field, + force, + extra_expr=size_field, + zero_check=f"!this->{self.field_name}.empty()", + ) + return self._get_simple_size_calculation(size_field, force, "length") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index d6cbb8c6be0..e79ff850f91 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -28,6 +28,7 @@ from esphome.core.entity_helpers import ( get_base_entity_object_id, register_device_class, register_icon, + register_unit_of_measurement, setup_device_class, setup_entity, setup_unit_of_measurement, @@ -925,6 +926,22 @@ def test_register_device_class_max_length() -> None: assert register_device_class("") == 0 +def test_register_unit_of_measurement_max_length() -> None: + """Test register_unit_of_measurement rejects units exceeding 63 characters.""" + # 63 chars should succeed + max_uom = "a" * 63 + idx = register_unit_of_measurement(max_uom) + assert idx > 0 + + # 64 chars should fail + too_long = "a" * 64 + with pytest.raises(ValueError, match="Unit of measurement string too long"): + register_unit_of_measurement(too_long) + + # Empty string returns 0 + assert register_unit_of_measurement("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], From e49384cd573b5ddf29947d18433eafbd05d388c8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:42:39 -0400 Subject: [PATCH 1278/2056] [dfrobot_sen0395] Fix list.index() on mutated list in range validator (#15511) --- esphome/components/dfrobot_sen0395/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index ba77e56abb0..0becaf35430 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -97,7 +97,7 @@ def range_segment_list(input): ) largest_distance = -1 - for distance in input: + for i, distance in enumerate(input): if isinstance(distance, cv.Lambda): continue m = cv.distance(distance) @@ -112,7 +112,7 @@ def range_segment_list(input): ) largest_distance = m # Replace distance object with meters float - input[input.index(distance)] = m + input[i] = m return input From f94e1dfab6c6a4890aedf6c203dd8f26591ac919 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 6 Apr 2026 18:12:01 -1000 Subject: [PATCH 1279/2056] [core] Move ControllerRegistry notify methods inline into header (#15505) --- esphome/core/controller_registry.cpp | 110 ------------------------- esphome/core/controller_registry.h | 117 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 110 deletions(-) diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index dd69de47d4c..92f23f56427 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -2,122 +2,12 @@ #ifdef USE_CONTROLLER_REGISTRY -#include "esphome/core/controller.h" - namespace esphome { StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers; void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -// Each notify method directly iterates controllers and calls the virtual method. -// This avoids the overhead of a shared noinline dispatch loop with function pointer -// indirection. The loop is tiny (~20 bytes per entity type) so the flash cost of -// duplicating it is negligible compared to eliminating two levels of indirection -// (noinline call + function pointer) from every state publish. -// NOLINTBEGIN(bugprone-macro-parentheses) -#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##entity_name##_update(obj); \ - } \ - } - -#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##entity_name(obj); \ - } \ - } -// NOLINTEND(bugprone-macro-parentheses) - -#ifdef USE_BINARY_SENSOR -CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) -#endif - -#ifdef USE_FAN -CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) -#endif - -#ifdef USE_LIGHT -CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) -#endif - -#ifdef USE_SENSOR -CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) -#endif - -#ifdef USE_SWITCH -CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) -#endif - -#ifdef USE_COVER -CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) -#endif - -#ifdef USE_TEXT_SENSOR -CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) -#endif - -#ifdef USE_CLIMATE -CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) -#endif - -#ifdef USE_NUMBER -CONTROLLER_REGISTRY_NOTIFY(number::Number, number) -#endif - -#ifdef USE_DATETIME_DATE -CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) -#endif - -#ifdef USE_DATETIME_TIME -CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) -#endif - -#ifdef USE_DATETIME_DATETIME -CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) -#endif - -#ifdef USE_TEXT -CONTROLLER_REGISTRY_NOTIFY(text::Text, text) -#endif - -#ifdef USE_SELECT -CONTROLLER_REGISTRY_NOTIFY(select::Select, select) -#endif - -#ifdef USE_LOCK -CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) -#endif - -#ifdef USE_VALVE -CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) -#endif - -#ifdef USE_MEDIA_PLAYER -CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) -#endif - -#ifdef USE_ALARM_CONTROL_PANEL -CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) -#endif - -#ifdef USE_WATER_HEATER -CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) -#endif - -#ifdef USE_EVENT -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) -#endif - -#ifdef USE_UPDATE -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) -#endif - -#undef CONTROLLER_REGISTRY_NOTIFY -#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX - } // namespace esphome #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 89b3069bcbf..846642da292 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -252,4 +252,121 @@ class ControllerRegistry { } // namespace esphome +// Include controller.h AFTER the class definition so notify methods can be +// defined inline. This is safe because controller_registry.h is only ever +// included from .cpp files, never from other headers. +#include "esphome/core/controller.h" + +namespace esphome { + +// Inline notify methods — each is a tiny loop over 1-2 controllers. +// Defining them here (rather than in controller_registry.cpp) allows the +// compiler to inline them into the single call site in each entity's +// notify_frontend_(), eliminating an unnecessary function-call frame. + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + inline void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ + } + +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + inline void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } +// NOLINTEND(bugprone-macro-parentheses) + +#ifdef USE_BINARY_SENSOR +CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) +#endif + +#ifdef USE_FAN +CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) +#endif + +#ifdef USE_LIGHT +CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) +#endif + +#ifdef USE_SENSOR +CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) +#endif + +#ifdef USE_SWITCH +CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) +#endif + +#ifdef USE_COVER +CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) +#endif + +#ifdef USE_TEXT_SENSOR +CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) +#endif + +#ifdef USE_CLIMATE +CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) +#endif + +#ifdef USE_NUMBER +CONTROLLER_REGISTRY_NOTIFY(number::Number, number) +#endif + +#ifdef USE_DATETIME_DATE +CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) +#endif + +#ifdef USE_DATETIME_TIME +CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) +#endif + +#ifdef USE_DATETIME_DATETIME +CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) +#endif + +#ifdef USE_TEXT +CONTROLLER_REGISTRY_NOTIFY(text::Text, text) +#endif + +#ifdef USE_SELECT +CONTROLLER_REGISTRY_NOTIFY(select::Select, select) +#endif + +#ifdef USE_LOCK +CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) +#endif + +#ifdef USE_VALVE +CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) +#endif + +#ifdef USE_MEDIA_PLAYER +CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) +#endif + +#ifdef USE_WATER_HEATER +CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) +#endif + +#ifdef USE_EVENT +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) +#endif + +#ifdef USE_UPDATE +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) +#endif + +#undef CONTROLLER_REGISTRY_NOTIFY +#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX + +} // namespace esphome + #endif // USE_CONTROLLER_REGISTRY From 488a6a1c4090209a726cef53b2042d588b203126 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:15:03 +0000 Subject: [PATCH 1280/2056] Bump aioesphomeapi from 44.11.1 to 44.12.0 (#15515) 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 db4a2b19e03..5c798819a86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260210.0 -aioesphomeapi==44.11.1 +aioesphomeapi==44.12.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 7ab7538220372b85deb233dc40f198b9940d744c Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:59:05 +0200 Subject: [PATCH 1281/2056] [hdc2080] Fix tests (#15518) --- tests/components/hdc2080/common.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/hdc2080/common.yaml b/tests/components/hdc2080/common.yaml index cb14cb183b5..48c6de2cd5e 100644 --- a/tests/components/hdc2080/common.yaml +++ b/tests/components/hdc2080/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: hdc2080 + i2c_id: i2c_bus temperature: name: Temperature humidity: From 674d030cbb47507a83c57b63a816088591a5e8a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 07:36:55 -1000 Subject: [PATCH 1282/2056] [core] Reschedule fired intervals directly into heap (#15516) --- esphome/core/scheduler.cpp | 24 ++- .../scheduler_interval_reschedule.yaml | 113 ++++++++++++ .../test_scheduler_interval_reschedule.py | 165 ++++++++++++++++++ 3 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_interval_reschedule.yaml create mode 100644 tests/integration/test_scheduler_interval_reschedule.py diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 71b29390d6b..dff50b03ef0 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -529,7 +529,7 @@ void HOT Scheduler::call(uint32_t now) { const auto now_64 = this->millis_64_from_(now); this->process_to_add(); - // Track if any items were added to to_add_ during this call (intervals or from callbacks) + // Track if any items were added to to_add_ during callbacks bool has_added_items = false; #ifdef ESPHOME_DEBUG_SCHEDULER @@ -578,6 +578,12 @@ void HOT Scheduler::call(uint32_t now) { if (this->to_remove_count_() >= MAX_LOGICALLY_DELETED_ITEMS) { this->full_cleanup_removed_items_(); } + // IMPORTANT: This loop uses index-based access (items_[0]), NOT iterators. + // This is intentional — fired intervals are pushed back into items_ via + // push_back() + push_heap() below, which may reallocate the vector's storage. + // Index-based access is safe across reallocations because we re-read items_[0] + // at the top of each iteration. Do NOT convert this to a range-based for loop + // or iterator-based loop, as that would break when items are added. while (!this->items_.empty()) { // Don't copy-by value yet SchedulerItem *item = this->items_[0]; @@ -646,10 +652,18 @@ void HOT Scheduler::call(uint32_t now) { if (executed_item->type == SchedulerItem::INTERVAL) { executed_item->set_next_execution(now_64 + executed_item->interval); - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(executed_item); - this->to_add_count_increment_(); + // Push directly back into the heap instead of routing through to_add_. + // This is safe because: + // 1. We're on the main loop and already hold the lock + // 2. The item was already popped from items_ via pop_raw_locked_() above + // 3. The while loop uses index-based access (items_[0]), not iterators, + // so push_back() reallocation cannot invalidate our iteration + // 4. push_heap() restores the heap invariant before the next iteration + // peeks at items_[0] + // This avoids the to_add_ detour and the overhead of + // process_to_add_slow_path_() (lock acquisition, vector iteration, clear). + this->items_.push_back(executed_item); + std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } else { // Timeout completed - recycle it this->recycle_item_main_loop_(executed_item); diff --git a/tests/integration/fixtures/scheduler_interval_reschedule.yaml b/tests/integration/fixtures/scheduler_interval_reschedule.yaml new file mode 100644 index 00000000000..a0f882deeec --- /dev/null +++ b/tests/integration/fixtures/scheduler_interval_reschedule.yaml @@ -0,0 +1,113 @@ +esphome: + name: sched-interval-resched + +host: +api: +logger: + level: DEBUG + +globals: + # Counters for each interval + - id: fast_count + type: int + initial_value: "0" + - id: medium_count + type: int + initial_value: "0" + - id: slow_count + type: int + initial_value: "0" + # Track interval that cancels itself + - id: self_cancel_count + type: int + initial_value: "0" + # Track interval that schedules a timeout from its callback + - id: callback_timeout_fired + type: bool + initial_value: "false" + # Track set_interval replacing itself from within callback + - id: replace_count + type: int + initial_value: "0" + - id: replaced_count + type: int + initial_value: "0" + +interval: + # Fast interval: 50ms + - interval: 50ms + then: + - lambda: |- + id(fast_count) += 1; + if (id(fast_count) == 10) { + ESP_LOGI("test", "FAST_10_REACHED"); + } + + # Medium interval: 100ms + - interval: 100ms + then: + - lambda: |- + id(medium_count) += 1; + if (id(medium_count) == 5) { + ESP_LOGI("test", "MEDIUM_5_REACHED fast_count=%d", id(fast_count)); + } + + # Slow interval: 200ms + - interval: 200ms + then: + - lambda: |- + id(slow_count) += 1; + if (id(slow_count) == 3) { + ESP_LOGI("test", "SLOW_3_REACHED fast_count=%d medium_count=%d", id(fast_count), id(medium_count)); + } + + # Interval that cancels itself after 3 fires + - interval: 75ms + id: self_cancelling + then: + - lambda: |- + id(self_cancel_count) += 1; + ESP_LOGI("test", "SELF_CANCEL_FIRE count=%d", id(self_cancel_count)); + if (id(self_cancel_count) >= 3) { + id(self_cancelling)->stop_poller(); + ESP_LOGI("test", "SELF_CANCEL_STOPPED"); + } + + # Interval that schedules a timeout from its callback (tests to_add_ path) + - interval: 150ms + id: timeout_creator + then: + - lambda: |- + if (!id(callback_timeout_fired)) { + // Schedule a one-shot timeout from within an interval callback + // This goes through to_add_ (not the direct push_heap path) + id(timeout_creator)->set_timeout("test_timeout", 10, []() { + id(callback_timeout_fired) = true; + ESP_LOGI("test", "CALLBACK_TIMEOUT_FIRED"); + }); + // Stop this interval after scheduling the timeout + id(timeout_creator)->stop_poller(); + } + + # Interval that calls set_interval with the same name from within its callback, + # replacing itself. Tests that the old item (marked removed) is not rescheduled + # via push_heap, and the new item goes through to_add_ correctly. + - interval: 60ms + id: replace_test + then: + - lambda: |- + id(replace_count) += 1; + if (id(replace_count) == 1) { + ESP_LOGI("test", "REPLACE_ORIGINAL_FIRE"); + // Replace the polling interval with a named interval on the same component + id(replace_test)->set_interval("replaced_interval", 80, []() { + id(replaced_count) += 1; + ESP_LOGI("test", "REPLACED_FIRE count=%d", id(replaced_count)); + if (id(replaced_count) >= 3) { + id(replace_test)->cancel_interval("replaced_interval"); + ESP_LOGI("test", "REPLACED_STOPPED"); + } + }); + // Stop the original polling interval + id(replace_test)->stop_poller(); + } diff --git a/tests/integration/test_scheduler_interval_reschedule.py b/tests/integration/test_scheduler_interval_reschedule.py new file mode 100644 index 00000000000..d141bd9f157 --- /dev/null +++ b/tests/integration/test_scheduler_interval_reschedule.py @@ -0,0 +1,165 @@ +"""Test that intervals are correctly rescheduled after firing. + +This test verifies the optimization where fired intervals are pushed directly +back into the scheduler's heap (items_) via push_back() + push_heap(), instead +of routing through the to_add_ staging vector and process_to_add_slow_path_(). + +Key scenarios tested: +1. Multiple intervals at different periods all fire at correct rates +2. Heap ordering is preserved — faster intervals fire proportionally more often +3. An interval that cancels itself mid-callback is not rescheduled +4. A timeout scheduled from within an interval callback (to_add_ path) still works +5. An interval that replaces itself via set_interval from within its callback +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_interval_reschedule( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that intervals are correctly rescheduled via direct heap insertion.""" + loop = asyncio.get_running_loop() + + # Futures for each milestone + fast_10_future: asyncio.Future[None] = loop.create_future() + medium_5_future: asyncio.Future[tuple[int]] = loop.create_future() + slow_3_future: asyncio.Future[tuple[int, int]] = loop.create_future() + self_cancel_stopped_future: asyncio.Future[None] = loop.create_future() + callback_timeout_future: asyncio.Future[None] = loop.create_future() + replace_original_future: asyncio.Future[None] = loop.create_future() + replaced_stopped_future: asyncio.Future[None] = loop.create_future() + + self_cancel_fire_count = 0 + replaced_fire_count = 0 + + def on_log_line(line: str) -> None: + nonlocal self_cancel_fire_count, replaced_fire_count + + if "FAST_10_REACHED" in line and not fast_10_future.done(): + fast_10_future.set_result(None) + + match = re.search(r"MEDIUM_5_REACHED fast_count=(\d+)", line) + if match and not medium_5_future.done(): + medium_5_future.set_result((int(match.group(1)),)) + + match = re.search(r"SLOW_3_REACHED fast_count=(\d+) medium_count=(\d+)", line) + if match and not slow_3_future.done(): + slow_3_future.set_result((int(match.group(1)), int(match.group(2)))) + + match = re.search(r"SELF_CANCEL_FIRE count=(\d+)", line) + if match: + self_cancel_fire_count = int(match.group(1)) + + if "SELF_CANCEL_STOPPED" in line and not self_cancel_stopped_future.done(): + self_cancel_stopped_future.set_result(None) + + if "CALLBACK_TIMEOUT_FIRED" in line and not callback_timeout_future.done(): + callback_timeout_future.set_result(None) + + if "REPLACE_ORIGINAL_FIRE" in line and not replace_original_future.done(): + replace_original_future.set_result(None) + + match = re.search(r"REPLACED_FIRE count=(\d+)", line) + if match: + replaced_fire_count = int(match.group(1)) + + if "REPLACED_STOPPED" in line and not replaced_stopped_future.done(): + replaced_stopped_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-interval-resched" + + # 1. Fast interval (50ms) should reach 10 fires within ~600ms + try: + await asyncio.wait_for(fast_10_future, timeout=5.0) + except TimeoutError: + pytest.fail("Fast interval (50ms) did not fire 10 times") + + # 2. Medium interval (100ms) should reach 5 fires + # At that point, fast_count should be roughly 2x medium_count + try: + result = await asyncio.wait_for(medium_5_future, timeout=5.0) + except TimeoutError: + pytest.fail("Medium interval (100ms) did not fire 5 times") + + fast_at_medium_5 = result[0] + # Fast runs at 50ms, medium at 100ms, so fast should be ~2x medium + # Allow some slack for scheduling jitter + assert fast_at_medium_5 >= 7, ( + f"Fast interval should have fired at least 7 times when medium hit 5, " + f"but only fired {fast_at_medium_5} times" + ) + + # 3. Slow interval (200ms) should reach 3 fires + # At that point, both fast and medium should have proportionally more fires + try: + result = await asyncio.wait_for(slow_3_future, timeout=5.0) + except TimeoutError: + pytest.fail("Slow interval (200ms) did not fire 3 times") + + fast_at_slow_3, medium_at_slow_3 = result + # At 600ms: fast ~12, medium ~6, slow 3 + assert fast_at_slow_3 >= 8, ( + f"Fast should have fired at least 8 times when slow hit 3, " + f"but only fired {fast_at_slow_3}" + ) + assert medium_at_slow_3 >= 4, ( + f"Medium should have fired at least 4 times when slow hit 3, " + f"but only fired {medium_at_slow_3}" + ) + + # 4. Self-cancelling interval should have stopped after exactly 3 fires + try: + await asyncio.wait_for(self_cancel_stopped_future, timeout=5.0) + except TimeoutError: + pytest.fail("Self-cancelling interval did not stop") + + # Wait a bit to ensure it doesn't fire again + await asyncio.sleep(0.3) + assert self_cancel_fire_count == 3, ( + f"Self-cancelling interval fired {self_cancel_fire_count} times, " + f"expected exactly 3" + ) + + # 5. Timeout scheduled from interval callback should have fired + try: + await asyncio.wait_for(callback_timeout_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout scheduled from interval callback did not fire") + + # 6. Interval that replaces itself via set_interval from within callback + # The original fires once, sets up a new named interval, then stops itself. + # The replacement interval should fire 3 times then cancel itself. + try: + await asyncio.wait_for(replace_original_future, timeout=5.0) + except TimeoutError: + pytest.fail("Replace-test original interval did not fire") + + try: + await asyncio.wait_for(replaced_stopped_future, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Replaced interval did not stop. Fired {replaced_fire_count} times" + ) + + # Wait to ensure replacement doesn't fire again after cancellation + await asyncio.sleep(0.3) + assert replaced_fire_count == 3, ( + f"Replaced interval fired {replaced_fire_count} times, expected exactly 3" + ) From 0d809a748102808a2e03fe69a9206d5162f6326e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 10:09:27 -1000 Subject: [PATCH 1283/2056] [automation] Add CallbackAutomation dataclass and build_callback_automations helper (#15246) --- esphome/automation.py | 33 +++ .../alarm_control_panel/__init__.py | 92 +++++--- esphome/components/binary_sensor/__init__.py | 47 ++-- esphome/components/button/__init__.py | 10 +- esphome/components/dfplayer/__init__.py | 12 +- esphome/components/event/__init__.py | 12 +- esphome/components/ezo/sensor.py | 45 ++-- esphome/components/factory_reset/__init__.py | 17 +- .../components/fingerprint_grow/__init__.py | 77 +++--- esphome/components/haier/climate.py | 41 ++-- esphome/components/hlk_fm22x/__init__.py | 89 ++++--- esphome/components/ld2450/__init__.py | 10 +- esphome/components/lock/__init__.py | 27 ++- esphome/components/ltr501/sensor.py | 19 +- esphome/components/ltr_als_ps/sensor.py | 19 +- esphome/components/media_player/__init__.py | 59 ++++- .../components/modbus_controller/__init__.py | 41 ++-- esphome/components/nextion/display.py | 54 ++--- esphome/components/number/__init__.py | 12 +- esphome/components/online_image/__init__.py | 18 +- esphome/components/pn532/__init__.py | 12 +- esphome/components/pn7150/__init__.py | 20 +- esphome/components/pn7160/__init__.py | 20 +- esphome/components/rf_bridge/__init__.py | 26 +- esphome/components/rotary_encoder/sensor.py | 17 +- esphome/components/rtttl/__init__.py | 12 +- esphome/components/safe_mode/__init__.py | 14 +- esphome/components/sensor/__init__.py | 19 +- esphome/components/sim800l/__init__.py | 49 ++-- esphome/components/sml/__init__.py | 29 ++- esphome/components/switch/__init__.py | 27 ++- esphome/components/text_sensor/__init__.py | 19 +- tests/unit_tests/test_automation.py | 223 +++++++++++++++++- 33 files changed, 794 insertions(+), 427 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 94d64086ec0..b4dcc419950 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import logging import esphome.codegen as cg @@ -715,3 +716,35 @@ async def build_callback_automation( # MockObjs (not user input), and there's no Expression type for positional # aggregate initialization (StructInitializer uses named fields). cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) + + +@dataclass(frozen=True, slots=True) +class CallbackAutomation: + """A single callback automation entry for build_callback_automations.""" + + conf_key: str + callback_method: str + args: TemplateArgsType = field(default_factory=list) + forwarder: MockObj | MockObjClass | None = None + + +async def build_callback_automations( + parent: MockObj, + config: ConfigType, + entries: tuple[CallbackAutomation, ...], +) -> None: + """Build multiple callback automations from a tuple of entries. + + :param parent: The component object (e.g., button, sensor). + :param config: The full component config dict. + :param entries: Tuple of CallbackAutomation entries to process. + """ + for entry in entries: + for conf in config.get(entry.conf_key, []): + await build_callback_automation( + parent, + entry.callback_method, + entry.args, + conf, + forwarder=entry.forwarder, + ) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 4ee073a15b1..9fcdf42ecb4 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -111,42 +111,66 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder + ), + automation.CallbackAutomation( + CONF_ON_TRIGGERED, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_TRIGGERED + ), + ), + automation.CallbackAutomation( + CONF_ON_ARMING, + "add_on_state_callback", + forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING), + ), + automation.CallbackAutomation( + CONF_ON_PENDING, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_PENDING + ), + ), + automation.CallbackAutomation( + CONF_ON_ARMED_HOME, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_ARMED_HOME + ), + ), + automation.CallbackAutomation( + CONF_ON_ARMED_NIGHT, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_ARMED_NIGHT + ), + ), + automation.CallbackAutomation( + CONF_ON_ARMED_AWAY, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_ARMED_AWAY + ), + ), + automation.CallbackAutomation( + CONF_ON_DISARMED, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + AlarmControlPanelState.ACP_STATE_DISARMED + ), + ), + automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"), + automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"), + automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"), +) + + @setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): - for conf in config.get(CONF_ON_STATE, []): - await automation.build_callback_automation( - var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder - ) - _STATE_ENTER_MAP = { - CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED, - CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING, - CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING, - CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME, - CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT, - CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY, - CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED, - } - for conf_key, state_enum in _STATE_ENTER_MAP.items(): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, - "add_on_state_callback", - [], - conf, - forwarder=StateEnterForwarder.template(state_enum), - ) - for conf in config.get(CONF_ON_CLEARED, []): - await automation.build_callback_automation( - var, "add_on_cleared_callback", [], conf - ) - for conf in config.get(CONF_ON_CHIME, []): - await automation.build_callback_automation( - var, "add_on_chime_callback", [], conf - ) - for conf in config.get(CONF_ON_READY, []): - await automation.build_callback_automation( - var, "add_on_ready_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) if mqtt_id := config.get(CONF_MQTT_ID): diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index d8cdaa5d58e..0b36c299f6a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -531,16 +531,31 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_PRESS, + "add_on_state_callback", + forwarder=automation.TriggerOnTrueForwarder, + ), + automation.CallbackAutomation( + CONF_ON_RELEASE, + "add_on_state_callback", + forwarder=automation.TriggerOnFalseForwarder, + ), + automation.CallbackAutomation( + CONF_ON_STATE, "add_on_state_callback", [(bool, "x")] + ), + automation.CallbackAutomation( + CONF_ON_STATE_CHANGE, + "add_full_state_callback", + [(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")], + ), +) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_binary_sensor_automations(var, config): - for conf_key, forwarder in ( - (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), - (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), - ): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, "add_on_state_callback", [], conf, forwarder=forwarder - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( @@ -572,22 +587,6 @@ async def _build_binary_sensor_automations(var, config): await cg.register_component(trigger, conf) await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_STATE, []): - await automation.build_callback_automation( - var, "add_on_state_callback", [(bool, "x")], conf - ) - - for conf in config.get(CONF_ON_STATE_CHANGE, []): - await automation.build_callback_automation( - var, - "add_full_state_callback", - [ - (cg.optional.template(bool), "x_previous"), - (cg.optional.template(bool), "x"), - ], - conf, - ) - @setup_entity("binary_sensor") async def setup_binary_sensor_core_(var, config): diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index f279b6ffe33..2c19ea69b1a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -79,12 +79,14 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"), +) + + @setup_entity("button") async def setup_button_core_(var, config): - for conf in config.get(CONF_ON_PRESS, []): - await automation.build_callback_automation( - var, "add_on_press_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) setup_device_class(config) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index adc19137914..7796f5d891d 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -64,15 +64,19 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback" + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - await automation.build_callback_automation( - var, "add_on_finished_playback_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 527bb4ebba9..9c9dd025b18 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -82,12 +82,16 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_EVENT, "add_on_event_callback", [(cg.StringRef, "event_type")] + ), +) + + @setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): - for conf in config.get(CONF_ON_EVENT, []): - await automation.build_callback_automation( - var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) cg.add(var.set_event_types(event_types)) diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index 7c81f9c848e..b931885149f 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -38,33 +38,30 @@ CONFIG_SCHEMA = ( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_CUSTOM, "add_custom_callback", [(cg.std_string, "x")] + ), + automation.CallbackAutomation(CONF_ON_LED, "add_led_state_callback", [(bool, "x")]), + automation.CallbackAutomation( + CONF_ON_DEVICE_INFORMATION, + "add_device_infomation_callback", + [(cg.std_string, "x")], + ), + automation.CallbackAutomation( + CONF_ON_SLOPE, "add_slope_callback", [(cg.std_string, "x")] + ), + automation.CallbackAutomation( + CONF_ON_CALIBRATION, "add_calibration_callback", [(cg.std_string, "x")] + ), + automation.CallbackAutomation(CONF_ON_T, "add_t_callback", [(cg.std_string, "x")]), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) - for conf in config.get(CONF_ON_CUSTOM, []): - await automation.build_callback_automation( - var, "add_custom_callback", [(cg.std_string, "x")], conf - ) - for conf in config.get(CONF_ON_LED, []): - await automation.build_callback_automation( - var, "add_led_state_callback", [(bool, "x")], conf - ) - for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): - await automation.build_callback_automation( - var, "add_device_infomation_callback", [(cg.std_string, "x")], conf - ) - for conf in config.get(CONF_ON_SLOPE, []): - await automation.build_callback_automation( - var, "add_slope_callback", [(cg.std_string, "x")], conf - ) - for conf in config.get(CONF_ON_CALIBRATION, []): - await automation.build_callback_automation( - var, "add_calibration_callback", [(cg.std_string, "x")], conf - ) - for conf in config.get(CONF_ON_T, []): - await automation.build_callback_automation( - var, "add_t_callback", [(cg.std_string, "x")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index 20b191a2b7a..818a53c0ed2 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -73,6 +73,15 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_INCREMENT, + "add_increment_callback", + [(cg.uint8, "x"), (cg.uint8, "target")], + ), +) + + async def to_code(config): if reset_count := config.get(CONF_RESETS_REQUIRED): var = cg.new_Pvariable( @@ -81,10 +90,4 @@ async def to_code(config): config[CONF_MAX_DELAY].total_seconds, ) await cg.register_component(var, config) - for conf in config.get(CONF_ON_INCREMENT, []): - await automation.build_callback_automation( - var, - "add_increment_callback", - [(cg.uint8, "x"), (cg.uint8, "target")], - conf, - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 0b01ba7cabd..8d935a3c9ed 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -116,6 +116,44 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_FINGER_SCAN_START, "add_on_finger_scan_start_callback" + ), + automation.CallbackAutomation( + CONF_ON_FINGER_SCAN_MATCHED, + "add_on_finger_scan_matched_callback", + [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], + ), + automation.CallbackAutomation( + CONF_ON_FINGER_SCAN_UNMATCHED, + "add_on_finger_scan_unmatched_callback", + ), + automation.CallbackAutomation( + CONF_ON_FINGER_SCAN_MISPLACED, + "add_on_finger_scan_misplaced_callback", + ), + automation.CallbackAutomation( + CONF_ON_FINGER_SCAN_INVALID, "add_on_finger_scan_invalid_callback" + ), + automation.CallbackAutomation( + CONF_ON_ENROLLMENT_SCAN, + "add_on_enrollment_scan_callback", + [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], + ), + automation.CallbackAutomation( + CONF_ON_ENROLLMENT_DONE, + "add_on_enrollment_done_callback", + [(cg.uint16, "finger_id")], + ), + automation.CallbackAutomation( + CONF_ON_ENROLLMENT_FAILED, + "add_on_enrollment_failed_callback", + [(cg.uint16, "finger_id")], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -140,44 +178,7 @@ async def to_code(config): idle_period_to_sleep_ms = config[CONF_IDLE_PERIOD_TO_SLEEP] cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) - for conf in config.get(CONF_ON_FINGER_SCAN_START, []): - await automation.build_callback_automation( - var, "add_on_finger_scan_start_callback", [], conf - ) - for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): - await automation.build_callback_automation( - var, - "add_on_finger_scan_matched_callback", - [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], - conf, - ) - for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): - await automation.build_callback_automation( - var, "add_on_finger_scan_unmatched_callback", [], conf - ) - for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): - await automation.build_callback_automation( - var, "add_on_finger_scan_misplaced_callback", [], conf - ) - for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): - await automation.build_callback_automation( - var, "add_on_finger_scan_invalid_callback", [], conf - ) - for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): - await automation.build_callback_automation( - var, - "add_on_enrollment_scan_callback", - [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], - conf, - ) - for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - await automation.build_callback_automation( - var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf - ) - for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - await automation.build_callback_automation( - var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 9c2c999f252..d485c1d5d44 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -456,6 +456,25 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_ALARM_START, + "add_alarm_start_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + ), + automation.CallbackAutomation( + CONF_ON_ALARM_END, + "add_alarm_end_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + ), + automation.CallbackAutomation( + CONF_ON_STATUS_MESSAGE, + "add_status_message_callback", + [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], + ), +) + + async def to_code(config): cg.add(haier_ns.init_haier_protocol_logging()) var = await climate.new_climate(config) @@ -497,26 +516,6 @@ async def to_code(config): cg.add( var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE]) ) - for conf in config.get(CONF_ON_ALARM_START, []): - await automation.build_callback_automation( - var, - "add_alarm_start_callback", - [(cg.uint8, "code"), (cg.const_char_ptr, "message")], - conf, - ) - for conf in config.get(CONF_ON_ALARM_END, []): - await automation.build_callback_automation( - var, - "add_alarm_end_callback", - [(cg.uint8, "code"), (cg.const_char_ptr, "message")], - conf, - ) - for conf in config.get(CONF_ON_STATUS_MESSAGE, []): - await automation.build_callback_automation( - var, - "add_status_message_callback", - [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], - conf, - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) # https://github.com/paveldn/HaierProtocol cg.add_library("pavlodn/HaierProtocol", "0.9.31") diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index c0349319d1f..8f55d5dc087 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -52,58 +52,53 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_FACE_SCAN_MATCHED, + "add_on_face_scan_matched_callback", + [(cg.int16, "face_id"), (cg.std_string, "name")], + ), + automation.CallbackAutomation( + CONF_ON_FACE_SCAN_UNMATCHED, "add_on_face_scan_unmatched_callback" + ), + automation.CallbackAutomation( + CONF_ON_FACE_SCAN_INVALID, + "add_on_face_scan_invalid_callback", + [(cg.uint8, "error")], + ), + automation.CallbackAutomation( + CONF_ON_FACE_INFO, + "add_on_face_info_callback", + [ + (cg.int16, "status"), + (cg.int16, "left"), + (cg.int16, "top"), + (cg.int16, "right"), + (cg.int16, "bottom"), + (cg.int16, "yaw"), + (cg.int16, "pitch"), + (cg.int16, "roll"), + ], + ), + automation.CallbackAutomation( + CONF_ON_ENROLLMENT_DONE, + "add_on_enrollment_done_callback", + [(cg.int16, "face_id"), (cg.uint8, "direction")], + ), + automation.CallbackAutomation( + CONF_ON_ENROLLMENT_FAILED, + "add_on_enrollment_failed_callback", + [(cg.uint8, "error")], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): - await automation.build_callback_automation( - var, - "add_on_face_scan_matched_callback", - [(cg.int16, "face_id"), (cg.std_string, "name")], - conf, - ) - - for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): - await automation.build_callback_automation( - var, "add_on_face_scan_unmatched_callback", [], conf - ) - - for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): - await automation.build_callback_automation( - var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf - ) - - for conf in config.get(CONF_ON_FACE_INFO, []): - await automation.build_callback_automation( - var, - "add_on_face_info_callback", - [ - (cg.int16, "status"), - (cg.int16, "left"), - (cg.int16, "top"), - (cg.int16, "right"), - (cg.int16, "bottom"), - (cg.int16, "yaw"), - (cg.int16, "pitch"), - (cg.int16, "roll"), - ], - conf, - ) - - for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - await automation.build_callback_automation( - var, - "add_on_enrollment_done_callback", - [(cg.int16, "face_id"), (cg.uint8, "direction")], - conf, - ) - - for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - await automation.build_callback_automation( - var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 37bf12bafc9..585c9f7bf54 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -44,11 +44,13 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_DATA, []): - await automation.build_callback_automation( - var, "add_on_data_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 0df4b20cbae..1a45896ac1f 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -81,20 +81,23 @@ def lock_schema( return _LOCK_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_LOCK, + "add_on_state_callback", + forwarder=LockStateForwarder.template(LockState.LOCK_STATE_LOCKED), + ), + automation.CallbackAutomation( + CONF_ON_UNLOCK, + "add_on_state_callback", + forwarder=LockStateForwarder.template(LockState.LOCK_STATE_UNLOCKED), + ), +) + + @setup_entity("lock") async def _setup_lock_core(var, config): - for conf_key, state_enum in ( - (CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED), - (CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED), - ): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, - "add_on_state_callback", - [], - conf, - forwarder=LockStateForwarder.template(state_enum), - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index 712810222c8..cca9330e76b 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -211,6 +211,16 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback" + ), + automation.CallbackAutomation( + CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback" + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -240,14 +250,7 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - await automation.build_callback_automation( - var, "add_on_ps_high_trigger_callback", [], conf - ) - for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - await automation.build_callback_automation( - var, "add_on_ps_low_trigger_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) cg.add(var.set_ltr_type(config[CONF_TYPE])) diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 57503772a12..893415f0280 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -201,6 +201,16 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback" + ), + automation.CallbackAutomation( + CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback" + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -230,14 +240,7 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - await automation.build_callback_automation( - var, "add_on_ps_high_trigger_callback", [], conf - ) - for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - await automation.build_callback_automation( - var, "add_on_ps_low_trigger_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) cg.add(var.set_ltr_type(config[CONF_TYPE])) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 842f620dae3..3c2e9029d63 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -69,7 +69,7 @@ StateEnterForwarder = media_player_ns.class_("StateEnterForwarder") MediaPlayerState = media_player_ns.enum("MediaPlayerState") # State triggers: (config_key, state enum or None for any-state) -_STATE_TRIGGERS = [ +_STATE_TRIGGERS = ( (CONF_ON_STATE, None), (CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE), (CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING), @@ -77,7 +77,7 @@ _STATE_TRIGGERS = [ (CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING), (CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON), (CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF), -] +) # State conditions that all share the same schema and codegen handler _STATE_CONDITIONS = [ @@ -102,17 +102,54 @@ VolumeSetAction = media_player_ns.class_( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder + ), + automation.CallbackAutomation( + CONF_ON_IDLE, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + MediaPlayerState.MEDIA_PLAYER_STATE_IDLE + ), + ), + automation.CallbackAutomation( + CONF_ON_PLAY, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING + ), + ), + automation.CallbackAutomation( + CONF_ON_PAUSE, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED + ), + ), + automation.CallbackAutomation( + CONF_ON_ANNOUNCEMENT, + "add_on_state_callback", + forwarder=StateEnterForwarder.template( + MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING + ), + ), + automation.CallbackAutomation( + CONF_ON_TURN_ON, + "add_on_state_callback", + forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_ON), + ), + automation.CallbackAutomation( + CONF_ON_TURN_OFF, + "add_on_state_callback", + forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_OFF), + ), +) + + @setup_entity("media_player") async def setup_media_player_core_(var, config): - for conf_key, state_enum in _STATE_TRIGGERS: - for conf in config.get(conf_key, []): - if state_enum is None: - forwarder = StateAnyForwarder - else: - forwarder = StateEnterForwarder.template(state_enum) - await automation.build_callback_automation( - var, "add_on_state_callback", [], conf, forwarder=forwarder - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) async def register_media_player(var, config): diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 9e332425a68..2af58a96beb 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -205,6 +205,25 @@ async def add_modbus_base_properties( cg.add(var.set_template(template_)) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_COMMAND_SENT, + "add_on_command_sent_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + ), + automation.CallbackAutomation( + CONF_ON_ONLINE, + "add_on_online_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + ), + automation.CallbackAutomation( + CONF_ON_OFFLINE, + "add_on_offline_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) @@ -257,27 +276,7 @@ async def to_code(config): ) cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) - for conf in config.get(CONF_ON_COMMAND_SENT, []): - await automation.build_callback_automation( - var, - "add_on_command_sent_callback", - [(cg.int_, "function_code"), (cg.int_, "address")], - conf, - ) - for conf in config.get(CONF_ON_ONLINE, []): - await automation.build_callback_automation( - var, - "add_on_online_callback", - [(cg.int_, "function_code"), (cg.int_, "address")], - conf, - ) - for conf in config.get(CONF_ON_OFFLINE, []): - await automation.build_callback_automation( - var, - "add_on_offline_callback", - [(cg.int_, "function_code"), (cg.int_, "address")], - conf, - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) async def register_modbus_device(var, config): diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 506eb1202b7..e477ab71827 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -144,6 +144,28 @@ async def nextion_set_brightness_to_code(config, action_id, template_arg, args): return var +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation(CONF_ON_SETUP, "add_setup_state_callback"), + automation.CallbackAutomation(CONF_ON_SLEEP, "add_sleep_state_callback"), + automation.CallbackAutomation(CONF_ON_WAKE, "add_wake_state_callback"), + automation.CallbackAutomation( + CONF_ON_PAGE, "add_new_page_callback", [(cg.uint8, "x")] + ), + automation.CallbackAutomation( + CONF_ON_TOUCH, + "add_touch_event_callback", + [ + (cg.uint8, "page_id"), + (cg.uint8, "component_id"), + (cg.bool_, "touch_event"), + ], + ), + automation.CallbackAutomation( + CONF_ON_BUFFER_OVERFLOW, "add_buffer_overflow_event_callback" + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await uart.register_uart_device(var, config) @@ -232,34 +254,4 @@ async def to_code(config): await display.register_display(var, config) - for conf in config.get(CONF_ON_SETUP, []): - await automation.build_callback_automation( - var, "add_setup_state_callback", [], conf - ) - for conf in config.get(CONF_ON_SLEEP, []): - await automation.build_callback_automation( - var, "add_sleep_state_callback", [], conf - ) - for conf in config.get(CONF_ON_WAKE, []): - await automation.build_callback_automation( - var, "add_wake_state_callback", [], conf - ) - for conf in config.get(CONF_ON_PAGE, []): - await automation.build_callback_automation( - var, "add_new_page_callback", [(cg.uint8, "x")], conf - ) - for conf in config.get(CONF_ON_TOUCH, []): - await automation.build_callback_automation( - var, - "add_touch_event_callback", - [ - (cg.uint8, "page_id"), - (cg.uint8, "component_id"), - (cg.bool_, "touch_event"), - ], - conf, - ) - for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []): - await automation.build_callback_automation( - var, "add_buffer_overflow_event_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 26d2602ba45..a223b346f28 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -243,12 +243,16 @@ def number_schema( return _NUMBER_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_VALUE, "add_on_state_callback", [(float, "x")] + ), +) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_number_automations(var, config): - for conf in config.get(CONF_ON_VALUE, []): - await automation.build_callback_automation( - var, "add_on_state_callback", [(float, "x")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 5b8294c70ee..518d787d8a9 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -105,6 +105,14 @@ async def online_image_action_to_code(config, action_id, template_arg, args): return var +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")] + ), + automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"), +) + + async def to_code(config): # Use the enhanced helper function to get all runtime image parameters settings = await runtime_image.process_runtime_image_config(config) @@ -139,12 +147,4 @@ async def to_code(config): else: cg.add(var.add_request_header(key, value)) - for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): - await automation.build_callback_automation( - var, "add_on_finished_callback", [(bool, "cached")], conf - ) - - for conf in config.get(CONF_ON_ERROR, []): - await automation.build_callback_automation( - var, "add_on_error_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 4ccda49a727..f34df216475 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -49,6 +49,13 @@ def CONFIG_SCHEMA(conf): ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback" + ), +) + + async def setup_pn532(var, config): await cg.register_component(var, config) @@ -66,10 +73,7 @@ async def setup_pn532(var, config): trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf ) - for conf in config.get(CONF_ON_FINISHED_WRITE, []): - await automation.build_callback_automation( - var, "add_on_finished_write_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_condition( diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index c8723dc31cd..9dd3e8c5b07 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -164,6 +164,16 @@ async def pn7150_simple_action_to_code(config, action_id, template_arg, args): return var +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback" + ), + automation.CallbackAutomation( + CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback" + ), +) + + async def setup_pn7150(var, config): await cg.register_component(var, config) @@ -194,15 +204,7 @@ async def setup_pn7150(var, config): trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf ) - for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - await automation.build_callback_automation( - var, "add_on_emulated_tag_scan_callback", [], conf - ) - - for conf in config.get(CONF_ON_FINISHED_WRITE, []): - await automation.build_callback_automation( - var, "add_on_finished_write_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_condition( diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index e382594b938..ef14a29099d 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -168,6 +168,16 @@ async def pn7160_simple_action_to_code(config, action_id, template_arg, args): return var +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback" + ), + automation.CallbackAutomation( + CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback" + ), +) + + async def setup_pn7160(var, config): await cg.register_component(var, config) @@ -206,15 +216,7 @@ async def setup_pn7160(var, config): trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf ) - for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - await automation.build_callback_automation( - var, "add_on_emulated_tag_scan_callback", [], conf - ) - - for conf in config.get(CONF_ON_FINISHED_WRITE, []): - await automation.build_callback_automation( - var, "add_on_finished_write_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_condition( diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 4ee1e7891f8..9ca47fe8621 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -67,22 +67,26 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_CODE_RECEIVED, + "add_on_code_received_callback", + [(RFBridgeData, "data")], + ), + automation.CallbackAutomation( + CONF_ON_ADVANCED_CODE_RECEIVED, + "add_on_advanced_code_received_callback", + [(RFBridgeAdvancedData, "data")], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_CODE_RECEIVED, []): - await automation.build_callback_automation( - var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf - ) - for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): - await automation.build_callback_automation( - var, - "add_on_advanced_code_received_callback", - [(RFBridgeAdvancedData, "data")], - conf, - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema( diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index d88657e7152..20c757f093c 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -84,6 +84,14 @@ CONFIG_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation(CONF_ON_CLOCKWISE, "add_on_clockwise_callback"), + automation.CallbackAutomation( + CONF_ON_ANTICLOCKWISE, "add_on_anticlockwise_callback" + ), +) + + async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) @@ -104,14 +112,7 @@ async def to_code(config): if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) - for conf in config.get(CONF_ON_CLOCKWISE, []): - await automation.build_callback_automation( - var, "add_on_clockwise_callback", [], conf - ) - for conf in config.get(CONF_ON_ANTICLOCKWISE, []): - await automation.build_callback_automation( - var, "add_on_anticlockwise_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 638e950ba63..c661aad972a 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -71,6 +71,13 @@ FINAL_VALIDATE_SCHEMA = cv.Schema( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback" + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -86,10 +93,7 @@ async def to_code(config): cg.add(var.set_gain(config[CONF_GAIN])) - for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - await automation.build_callback_automation( - var, "add_on_finished_playback_callback", [], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index da36d21eb7f..6df0ba78b1f 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -65,18 +65,22 @@ async def safe_mode_mark_successful_to_code(config, action_id, template_arg, arg return var +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation(CONF_ON_SAFE_MODE, "add_on_safe_mode_callback"), +) + + @coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE): + if config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") - for conf in on_safe_mode_config: - await automation.build_callback_automation( - var, "add_on_safe_mode_callback", [], conf - ) + await automation.build_callback_automations( + var, config, _CALLBACK_AUTOMATIONS + ) condition = var.should_enter_safe_mode( config[CONF_NUM_ATTEMPTS], diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 275c4542fb9..3a54e97f68a 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -892,16 +892,19 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_VALUE, "add_on_state_callback", [(float, "x")] + ), + automation.CallbackAutomation( + CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(float, "x")] + ), +) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_sensor_automations(var, config): - for conf_key, callback in ( - (CONF_ON_VALUE, "add_on_state_callback"), - (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), - ): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, callback, [(float, "x")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) for conf in config.get(CONF_ON_VALUE_RANGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 91771047e1c..ae7ee6fa59f 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -48,34 +48,37 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_SMS_RECEIVED, + "add_on_sms_received_callback", + [(cg.std_string, "message"), (cg.std_string, "sender")], + ), + automation.CallbackAutomation( + CONF_ON_INCOMING_CALL, + "add_on_incoming_call_callback", + [(cg.std_string, "caller_id")], + ), + automation.CallbackAutomation( + CONF_ON_CALL_CONNECTED, "add_on_call_connected_callback" + ), + automation.CallbackAutomation( + CONF_ON_CALL_DISCONNECTED, "add_on_call_disconnected_callback" + ), + automation.CallbackAutomation( + CONF_ON_USSD_RECEIVED, + "add_on_ussd_received_callback", + [(cg.std_string, "ussd")], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_SMS_RECEIVED, []): - await automation.build_callback_automation( - var, - "add_on_sms_received_callback", - [(cg.std_string, "message"), (cg.std_string, "sender")], - conf, - ) - for conf in config.get(CONF_ON_INCOMING_CALL, []): - await automation.build_callback_automation( - var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf - ) - for conf in config.get(CONF_ON_CALL_CONNECTED, []): - await automation.build_callback_automation( - var, "add_on_call_connected_callback", [], conf - ) - for conf in config.get(CONF_ON_CALL_DISCONNECTED, []): - await automation.build_callback_automation( - var, "add_on_call_disconnected_callback", [], conf - ) - for conf in config.get(CONF_ON_USSD_RECEIVED, []): - await automation.build_callback_automation( - var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) SIM800L_SEND_SMS_SCHEMA = cv.Schema( diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index 1b7f9da4fb4..d25e883fa18 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -31,23 +31,26 @@ CONFIG_SCHEMA = ( ) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_DATA, + "add_on_data_callback", + [ + ( + cg.std_vector.template(cg.uint8).operator("ref").operator("const"), + "bytes", + ), + (cg.bool_, "valid"), + ], + ), +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for conf in config.get(CONF_ON_DATA, []): - await automation.build_callback_automation( - var, - "add_on_data_callback", - [ - ( - cg.std_vector.template(cg.uint8).operator("ref").operator("const"), - "bytes", - ), - (cg.bool_, "valid"), - ], - conf, - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) def obis_code(value): diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index c4dd4856e3f..5a63cbfb9f2 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -121,17 +121,26 @@ def switch_schema( return _SWITCH_SCHEMA.extend(schema) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_STATE, "add_on_state_callback", [(bool, "x")] + ), + automation.CallbackAutomation( + CONF_ON_TURN_ON, + "add_on_state_callback", + forwarder=automation.TriggerOnTrueForwarder, + ), + automation.CallbackAutomation( + CONF_ON_TURN_OFF, + "add_on_state_callback", + forwarder=automation.TriggerOnFalseForwarder, + ), +) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_switch_automations(var, config): - for conf_key, args, forwarder in ( - (CONF_ON_STATE, [(bool, "x")], None), - (CONF_ON_TURN_ON, [], automation.TriggerOnTrueForwarder), - (CONF_ON_TURN_OFF, [], automation.TriggerOnFalseForwarder), - ): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, "add_on_state_callback", args, conf, forwarder=forwarder - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @setup_entity("switch") diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 5b07dd29156..94014e8d206 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -184,16 +184,19 @@ async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_VALUE, "add_on_state_callback", [(cg.std_string, "x")] + ), + automation.CallbackAutomation( + CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(cg.std_string, "x")] + ), +) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _build_text_sensor_automations(var, config): - for conf_key, callback in ( - (CONF_ON_VALUE, "add_on_state_callback"), - (CONF_ON_RAW_VALUE, "add_on_raw_state_callback"), - ): - for conf in config.get(conf_key, []): - await automation.build_callback_automation( - var, callback, [(cg.std_string, "x")], conf - ) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @setup_entity("text_sensor") diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 37779f23e68..a377cf185a8 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -1,14 +1,16 @@ """Tests for esphome.automation module.""" from collections.abc import Generator -from unittest.mock import patch +from unittest.mock import AsyncMock, call, patch import pytest from esphome.automation import ( + CallbackAutomation, TriggerForwarder, TriggerOnFalseForwarder, TriggerOnTrueForwarder, + build_callback_automations, has_non_synchronous_actions, ) from esphome.cpp_generator import MockObj, RawExpression @@ -254,3 +256,222 @@ def test_trigger_forwarder_custom_type() -> None: custom = MockObj("MyForwarder", "") result = _build_forwarder("auto_1", [], forwarder=custom) assert result == "MyForwarder{auto_1}" + + +@pytest.fixture +def mock_build_callback() -> Generator[AsyncMock]: + """Patch build_callback_automation to capture calls.""" + with patch( + "esphome.automation.build_callback_automation", new_callable=AsyncMock + ) as mock: + yield mock + + +@pytest.mark.asyncio +async def test_build_callback_automations_empty_entries( + mock_build_callback: AsyncMock, +) -> None: + """No entries means no calls.""" + parent = MockObj("var", "->") + await build_callback_automations(parent, {}, ()) + mock_build_callback.assert_not_called() + + +@pytest.mark.asyncio +async def test_build_callback_automations_missing_config_key( + mock_build_callback: AsyncMock, +) -> None: + """Entry present but config key missing -- no calls.""" + parent = MockObj("var", "->") + await build_callback_automations( + parent, + {}, + (CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),), + ) + mock_build_callback.assert_not_called() + + +@pytest.mark.asyncio +async def test_build_callback_automations_single_entry( + mock_build_callback: AsyncMock, +) -> None: + """Single entry with one config triggers one call.""" + parent = MockObj("var", "->") + conf: dict[str, object] = {"automation_id": "auto_1", "then": []} + config: dict[str, list[dict[str, object]]] = {"on_state": [conf]} + await build_callback_automations( + parent, + config, + (CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),), + ) + mock_build_callback.assert_called_once_with( + parent, "add_on_state_callback", [(bool, "x")], conf, forwarder=None + ) + + +@pytest.mark.asyncio +async def test_build_callback_automations_multiple_configs( + mock_build_callback: AsyncMock, +) -> None: + """Single entry with multiple configs triggers multiple calls.""" + parent = MockObj("var", "->") + conf1: dict[str, object] = {"automation_id": "auto_1", "then": []} + conf2: dict[str, object] = {"automation_id": "auto_2", "then": []} + config: dict[str, list[dict[str, object]]] = {"on_state": [conf1, conf2]} + await build_callback_automations( + parent, + config, + (CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]),), + ) + assert mock_build_callback.call_count == 2 + mock_build_callback.assert_any_call( + parent, "add_on_state_callback", [(bool, "x")], conf1, forwarder=None + ) + mock_build_callback.assert_any_call( + parent, "add_on_state_callback", [(bool, "x")], conf2, forwarder=None + ) + + +@pytest.mark.asyncio +async def test_build_callback_automations_multiple_entries( + mock_build_callback: AsyncMock, +) -> None: + """Multiple entries each with one config.""" + parent = MockObj("var", "->") + conf_a: dict[str, object] = {"automation_id": "auto_a", "then": []} + conf_b: dict[str, object] = {"automation_id": "auto_b", "then": []} + config: dict[str, list[dict[str, object]]] = { + "on_value": [conf_a], + "on_raw_value": [conf_b], + } + await build_callback_automations( + parent, + config, + ( + CallbackAutomation("on_value", "add_on_value_callback", [(float, "x")]), + CallbackAutomation( + "on_raw_value", "add_on_raw_value_callback", [(float, "x")] + ), + ), + ) + assert mock_build_callback.call_count == 2 + assert mock_build_callback.call_args_list == [ + call(parent, "add_on_value_callback", [(float, "x")], conf_a, forwarder=None), + call( + parent, "add_on_raw_value_callback", [(float, "x")], conf_b, forwarder=None + ), + ] + + +@pytest.mark.asyncio +async def test_build_callback_automations_with_forwarder( + mock_build_callback: AsyncMock, +) -> None: + """Entry with forwarder passes it through.""" + parent = MockObj("var", "->") + conf: dict[str, object] = {"automation_id": "auto_1", "then": []} + config: dict[str, list[dict[str, object]]] = {"on_press": [conf]} + await build_callback_automations( + parent, + config, + ( + CallbackAutomation( + "on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder + ), + ), + ) + mock_build_callback.assert_called_once_with( + parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder + ) + + +@pytest.mark.asyncio +async def test_build_callback_automations_mixed_entries( + mock_build_callback: AsyncMock, +) -> None: + """Mix of entries with args, forwarders, and defaults.""" + parent = MockObj("var", "->") + conf_state: dict[str, object] = {"automation_id": "auto_1", "then": []} + conf_press: dict[str, object] = {"automation_id": "auto_2", "then": []} + conf_release: dict[str, object] = {"automation_id": "auto_3", "then": []} + config: dict[str, list[dict[str, object]]] = { + "on_state": [conf_state], + "on_press": [conf_press], + "on_release": [conf_release], + } + await build_callback_automations( + parent, + config, + ( + CallbackAutomation("on_state", "add_on_state_callback", [(bool, "x")]), + CallbackAutomation( + "on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder + ), + CallbackAutomation( + "on_release", "add_on_state_callback", forwarder=TriggerOnFalseForwarder + ), + ), + ) + assert mock_build_callback.call_count == 3 + assert mock_build_callback.call_args_list == [ + call( + parent, "add_on_state_callback", [(bool, "x")], conf_state, forwarder=None + ), + call( + parent, + "add_on_state_callback", + [], + conf_press, + forwarder=TriggerOnTrueForwarder, + ), + call( + parent, + "add_on_state_callback", + [], + conf_release, + forwarder=TriggerOnFalseForwarder, + ), + ] + + +@pytest.mark.asyncio +async def test_build_callback_automations_skips_missing_keys( + mock_build_callback: AsyncMock, +) -> None: + """Entries whose config keys are absent are silently skipped.""" + parent = MockObj("var", "->") + conf: dict[str, object] = {"automation_id": "auto_1", "then": []} + config: dict[str, list[dict[str, object]]] = {"on_press": [conf]} + await build_callback_automations( + parent, + config, + ( + CallbackAutomation( + "on_press", "add_on_state_callback", forwarder=TriggerOnTrueForwarder + ), + CallbackAutomation( + "on_release", "add_on_state_callback", forwarder=TriggerOnFalseForwarder + ), + ), + ) + mock_build_callback.assert_called_once_with( + parent, "add_on_state_callback", [], conf, forwarder=TriggerOnTrueForwarder + ) + + +@pytest.mark.asyncio +async def test_build_callback_automations_defaults( + mock_build_callback: AsyncMock, +) -> None: + """Verify CallbackAutomation with only required fields defaults args=[] and forwarder=None.""" + parent = MockObj("var", "->") + conf: dict[str, object] = {"automation_id": "auto_1", "then": []} + config: dict[str, list[dict[str, object]]] = {"on_press": [conf]} + await build_callback_automations( + parent, + config, + (CallbackAutomation("on_press", "add_on_press_callback"),), + ) + mock_build_callback.assert_called_once_with( + parent, "add_on_press_callback", [], conf, forwarder=None + ) From 6460f3a757777f805af3a94d8521db2af837dba1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 10:24:36 -1000 Subject: [PATCH 1284/2056] [api] Add max_data_length and force to DeviceInfoResponse/HelloResponse proto fields (#15514) --- esphome/components/api/api.proto | 51 +++++++++++------- esphome/components/api/api_connection.cpp | 9 ++++ esphome/components/api/api_pb2.cpp | 60 +++++++++++----------- esphome/components/esp32/__init__.py | 5 +- esphome/components/esp8266/__init__.py | 5 +- esphome/components/libretiny/__init__.py | 5 +- esphome/components/nrf52/__init__.py | 5 +- esphome/components/number/__init__.py | 2 +- esphome/components/rp2040/__init__.py | 5 +- esphome/components/sensor/__init__.py | 2 +- esphome/config_validation.py | 34 +++++++++--- esphome/core/config.py | 20 ++++++-- esphome/core/entity_helpers.py | 15 +++--- tests/unit_tests/test_config_validation.py | 44 +++++++++++++--- 14 files changed, 182 insertions(+), 80 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 07705baff60..33d16f0339b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -129,11 +129,12 @@ message HelloResponse { // A string identifying the server (ESP); like client info this may be empty // and only exists for debugging/logging purposes. - // For example "ESPHome v1.10.0 on ESP8266" - string server_info = 3; + // Currently set to ESPHOME_VERSION string literal. + string server_info = 3 [(max_data_length) = 32, (force) = true]; - // The name of the server (App.get_name()) - string name = 4; + // The name of the server (App.get_name() - device hostname) + // max_data_length matches ESPHOME_DEVICE_NAME_MAX_LEN (validated by validate_hostname) + string name = 4 [(max_data_length) = 31, (force) = true]; } // DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported. @@ -196,12 +197,14 @@ message DeviceInfoRequest { message AreaInfo { uint32 area_id = 1; - string name = 2; + // max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA + string name = 2 [(max_data_length) = 120, (force) = true]; } message DeviceInfo { uint32 device_id = 1; - string name = 2; + // max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via DEVICE_SCHEMA + string name = 2 [(max_data_length) = 120, (force) = true]; uint32 area_id = 3; } @@ -216,6 +219,16 @@ message SerialProxyInfo { SerialProxyPortType port_type = 2; // Port type (RS232, RS485) } +// DeviceInfoResponse max_data_length values: +// name = 31 (ESPHOME_DEVICE_NAME_MAX_LEN, validated by validate_hostname) +// friendly_name = 120 (core/config.FRIENDLY_NAME_MAX_LEN) +// mac_address/bluetooth_mac_address = 17 (MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1, constexpr) +// esphome_version = 32 (ESPHOME_VERSION string literal) +// compilation_time = 25 (Application::BUILD_TIME_STR_SIZE - 1, constexpr) +// manufacturer = 20 (longest hardcoded literal: "Nordic Semiconductor") +// model = 127 (core/config.BOARD_MAX_LENGTH, validated in platform schemas) +// project_name/project_version = 127 (core/config.PROJECT_MAX_LENGTH) +// suggested_area = 120 (core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA) message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -224,28 +237,30 @@ message DeviceInfoResponse { // with older ESPHome versions that still send this field. bool uses_password = 1 [deprecated = true]; - // The name of the node, given by "App.set_name()" - string name = 2; + // The name of the node, given by "App.set_name()" - device hostname + string name = 2 [(max_data_length) = 31, (force) = true]; // The mac address of the device. For example "AC:BC:32:89:0E:A9" - string mac_address = 3; + string mac_address = 3 [(max_data_length) = 17, (force) = true]; // A string describing the ESPHome version. For example "1.10.0" - string esphome_version = 4; + string esphome_version = 4 [(max_data_length) = 32, (force) = true]; // A string describing the date of compilation, this is generated by the compiler // and therefore may not be in the same format all the time. // If the user isn't using ESPHome, this will also not be set. - string compilation_time = 5; + string compilation_time = 5 [(max_data_length) = 25, (force) = true]; // The model of the board. For example NodeMCU - string model = 6; + // max_data_length matches core/config.BOARD_MAX_LENGTH (validated in platform schemas) + string model = 6 [(max_data_length) = 127, (force) = true]; bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"]; // The esphome project details if set - string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"]; - string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"]; + // max_data_length matches core/config.PROJECT_MAX_LENGTH + string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"]; + string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"]; uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; @@ -253,18 +268,18 @@ message DeviceInfoResponse { uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"]; uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; - string manufacturer = 12; + string manufacturer = 12 [(max_data_length) = 20, (force) = true]; - string friendly_name = 13; + string friendly_name = 13 [(max_data_length) = 120, (force) = true]; // Deprecated in API version 1.10 uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"]; uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; - string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; + string suggested_area = 16 [(max_data_length) = 120, (force) = true, (field_ifdef) = "USE_AREAS"]; // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" - string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; + string bluetooth_mac_address = 18 [(max_data_length) = 17, (force) = true, (field_ifdef) = "USE_BLUETOOTH_PROXY"]; // Supports receiving and saving api encryption key bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index feb16e4f4c4..bfb3ec291c5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -72,6 +72,14 @@ static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); +// Cross-validate C++ constants against proto max_data_length annotations in api.proto +static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17, + "Update max_data_length for mac_address/bluetooth_mac_address in api.proto"); +static_assert(Application::BUILD_TIME_STR_SIZE - 1 == 25, "Update max_data_length for compilation_time in api.proto"); +static_assert(sizeof(ESPHOME_VERSION) - 1 <= 32, "Update max_data_length for esphome_version in api.proto"); +static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for name in api.proto"); +static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto"); + static const char *const TAG = "api.connection"; #ifdef USE_CAMERA static const int CAMERA_STOP_STREAM = 5000; @@ -1716,6 +1724,7 @@ bool APIConnection::send_device_info_response_() { static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER); resp.manufacturer = MANUFACTURER; #endif + static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto"); #undef ESPHOME_MANUFACTURER #ifdef USE_ESP8266 diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f7c68b95a77..d27cfa57cf9 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -35,29 +35,29 @@ uint8_t *HelloResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->api_version_major); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->api_version_minor); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->server_info); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->server_info); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, this->name); return pos; } uint32_t HelloResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->api_version_major); size += ProtoSize::calc_uint32(1, this->api_version_minor); - size += ProtoSize::calc_length(1, this->server_info.size()); - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->server_info.size(); + size += 2 + this->name.size(); return size; } #ifdef USE_AREAS uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->area_id); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name); return pos; } uint32_t AreaInfo::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->area_id); - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); return size; } #endif @@ -65,14 +65,14 @@ uint32_t AreaInfo::calculate_size() const { uint8_t *DeviceInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->area_id); return pos; } uint32_t DeviceInfo::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->device_id); - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_uint32(1, this->area_id); return size; } @@ -93,19 +93,19 @@ uint32_t SerialProxyInfo::calculate_size() const { #endif uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mac_address); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->esphome_version); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->compilation_time); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->model); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->mac_address); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, this->esphome_version); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 42, this->compilation_time); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 50, this->model); #ifdef USE_DEEP_SLEEP ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->project_name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 66, this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->project_version); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 74, this->project_version); #endif #ifdef USE_WEBSERVER ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->webserver_port); @@ -113,16 +113,16 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_ #ifdef USE_BLUETOOTH_PROXY ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, this->bluetooth_proxy_feature_flags); #endif - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, this->manufacturer); - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->friendly_name); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 98, this->manufacturer); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 106, this->friendly_name); #ifdef USE_VOICE_ASSISTANT ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 17, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area, true); #endif #ifdef USE_BLUETOOTH_PROXY - ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address, true); #endif #ifdef USE_API_NOISE ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 19, this->api_encryption_supported); @@ -155,19 +155,19 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_ } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_length(1, this->mac_address.size()); - size += ProtoSize::calc_length(1, this->esphome_version.size()); - size += ProtoSize::calc_length(1, this->compilation_time.size()); - size += ProtoSize::calc_length(1, this->model.size()); + size += 2 + this->name.size(); + size += 2 + this->mac_address.size(); + size += 2 + this->esphome_version.size(); + size += 2 + this->compilation_time.size(); + size += 2 + this->model.size(); #ifdef USE_DEEP_SLEEP size += ProtoSize::calc_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size += ProtoSize::calc_length(1, this->project_name.size()); + size += 2 + this->project_name.size(); #endif #ifdef ESPHOME_PROJECT_NAME - size += ProtoSize::calc_length(1, this->project_version.size()); + size += 2 + this->project_version.size(); #endif #ifdef USE_WEBSERVER size += ProtoSize::calc_uint32(1, this->webserver_port); @@ -175,16 +175,16 @@ uint32_t DeviceInfoResponse::calculate_size() const { #ifdef USE_BLUETOOTH_PROXY size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size += ProtoSize::calc_length(1, this->manufacturer.size()); - size += ProtoSize::calc_length(1, this->friendly_name.size()); + size += 2 + this->manufacturer.size(); + size += 2 + this->friendly_name.size(); #ifdef USE_VOICE_ASSISTANT size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size += ProtoSize::calc_length(2, this->suggested_area.size()); + size += 3 + this->suggested_area.size(); #endif #ifdef USE_BLUETOOTH_PROXY - size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size()); + size += 3 + this->bluetooth_mac_address.size(); #endif #ifdef USE_API_NOISE size += ProtoSize::calc_bool(2, this->api_encryption_supported); diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0d8a2215245..f27690c97b4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -44,6 +44,7 @@ from esphome.const import ( __version__, ) from esphome.core import CORE, HexInt +from esphome.core.config import BOARD_MAX_LENGTH from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed @@ -1403,7 +1404,9 @@ CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.Optional(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_BOARD): cv.All( + cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) + ), cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index fcd3499b151..bef7e36470a 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed from esphome.types import ConfigType @@ -203,7 +204,9 @@ BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"] CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.Required(CONF_BOARD): cv.string_strict, + cv.Required(CONF_BOARD): cv.All( + cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) + ), cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean, diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 8f991246040..656eee6d7bf 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -23,6 +23,7 @@ from esphome.const import ( __version__, ) from esphome.core import CORE +from esphome.core.config import BOARD_MAX_LENGTH from esphome.storage_json import StorageJSON from . import gpio # noqa @@ -266,7 +267,9 @@ CONFIG_SCHEMA = cv.All(_notify_old_style) BASE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LTComponent), - cv.Required(CONF_BOARD): cv.string_strict, + cv.Required(CONF_BOARD): cv.All( + cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) + ), cv.Optional(CONF_FAMILY): cv.one_of(*FAMILIES, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, }, diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 5054e5e0df4..5d92a4fa801 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -46,6 +46,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +from esphome.core.config import BOARD_MAX_LENGTH import esphome.final_validate as fv from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -145,7 +146,9 @@ CONFIG_SCHEMA = cv.All( set_core_data, cv.Schema( { - cv.Required(CONF_BOARD): cv.string_strict, + cv.Required(CONF_BOARD): cv.All( + cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) + ), cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), cv.Optional(CONF_DFU): cv.Schema( { diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index a223b346f28..9fbaff68601 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -190,7 +190,7 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_unit_of_measurement = cv.All( cv.string_strict, # Keep in sync with max_data_length in api.proto - cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), + cv.ByteLength(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), ) _NUMBER_SCHEMA = ( diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 0bb18110693..e452780d412 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( ThreadModel, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority +from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed from . import boards @@ -168,7 +169,9 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.Required(CONF_BOARD): cv.string_strict, + cv.Required(CONF_BOARD): cv.All( + cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) + ), cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, cv.Optional(CONF_WATCHDOG_TIMEOUT, default="8388ms"): cv.All( cv.positive_time_period_milliseconds, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 3a54e97f68a..ecf51d54884 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -294,7 +294,7 @@ RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) validate_unit_of_measurement = cv.All( cv.string_strict, # Keep in sync with max_data_length in api.proto - cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), + cv.ByteLength(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), ) validate_accuracy_decimals = cv.int_ validate_icon = cv.icon diff --git a/esphome/config_validation.py b/esphome/config_validation.py index c6b67e9f35b..7805de98db7 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -130,6 +130,26 @@ RequiredFieldInvalid = vol.RequiredFieldInvalid # the rest of the error path is relative to the root config path ROOT_CONFIG_PATH = object() + +def ByteLength(*, max: int) -> Callable[[str], str]: + """Validate that the UTF-8 byte length of a string does not exceed max. + + Use instead of Length() when the limit must apply to encoded bytes, + not characters (e.g. for protobuf length-varint constraints). + """ + + def validator(value: str) -> str: + byte_len = len(str(value).encode("utf-8")) + if byte_len > max: + raise Invalid( + f"String is too long ({byte_len} bytes, max {max}). " + f"Multibyte characters count as multiple bytes." + ) + return value + + return validator + + RESERVED_IDS = [ # C++ keywords https://en.cppreference.com/w/cpp/keyword "alarm", @@ -411,9 +431,10 @@ def icon(value): raise Invalid( 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' ) - if len(value) > ICON_MAX_LENGTH: + byte_len = len(value.encode("utf-8")) + if byte_len > ICON_MAX_LENGTH: raise Invalid( - f"Icon string is too long ({len(value)} chars, max {ICON_MAX_LENGTH}). " + f"Icon string is too long ({byte_len} bytes, max {ICON_MAX_LENGTH}). " "Icons are stored in PROGMEM with a 64-byte buffer limit." ) return value @@ -2067,11 +2088,12 @@ def _validate_entity_name(value): "Name cannot be None when esphome->friendly_name is not set!" )(value) if value is not None: - # Validate length for web server URL compatibility - if len(value) > NAME_MAX_LENGTH: + # Validate byte length for web server URL and proto encoding compatibility + byte_len = len(value.encode("utf-8")) + if byte_len > NAME_MAX_LENGTH: raise Invalid( - f"Name is too long ({len(value)} chars). " - f"Maximum length is {NAME_MAX_LENGTH} characters." + f"Name is too long ({byte_len} bytes). " + f"Maximum length is {NAME_MAX_LENGTH} bytes." ) # Validate no '/' in name for web server URL compatibility value = _validate_no_slash(value) diff --git a/esphome/core/config.py b/esphome/core/config.py index 31cfd00ef7a..bf210876dfb 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -236,11 +236,17 @@ ICON_MAX_LENGTH = 63 # Max unit of measurement string length UNIT_OF_MEASUREMENT_MAX_LENGTH = 63 +# Max project name/version string length (must fit in single-byte varint for proto encoding) +PROJECT_MAX_LENGTH = 127 + +# Max board/model string length (must fit in single-byte varint for proto encoding) +BOARD_MAX_LENGTH = 127 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), cv.Required(CONF_NAME): cv.All( - cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN) ), } ) @@ -249,7 +255,7 @@ DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Device), cv.Required(CONF_NAME): cv.All( - cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN) ), cv.Optional(CONF_AREA_ID): cv.use_id(Area), } @@ -266,7 +272,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NAME): cv.valid_name, # Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( - cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN) ), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), @@ -306,9 +312,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PROJECT): cv.Schema( { cv.Required(CONF_NAME): cv.All( - cv.string_strict, valid_project_name + cv.string_strict, + valid_project_name, + cv.ByteLength(max=PROJECT_MAX_LENGTH), + ), + cv.Required(CONF_VERSION): cv.All( + cv.string_strict, cv.ByteLength(max=PROJECT_MAX_LENGTH) ), - cv.Required(CONF_VERSION): cv.string_strict, cv.Optional(CONF_ON_UPDATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index fc931c2baa1..f09dd013fe2 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -193,9 +193,10 @@ def _register_string( def register_device_class(value: str) -> int: """Register a device_class string and return its 1-based index.""" - if value and len(value) > DEVICE_CLASS_MAX_LENGTH: + byte_len = len(value.encode("utf-8")) if value else 0 + if byte_len > DEVICE_CLASS_MAX_LENGTH: raise ValueError( - f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'" + f"Device class string too long ({byte_len} bytes, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'" ) return _register_string( value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class" @@ -204,9 +205,10 @@ def register_device_class(value: str) -> int: def register_unit_of_measurement(value: str) -> int: """Register a unit_of_measurement string and return its 1-based index.""" - if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH: + byte_len = len(value.encode("utf-8")) if value else 0 + if byte_len > UNIT_OF_MEASUREMENT_MAX_LENGTH: raise ValueError( - f"Unit of measurement string too long ({len(value)} chars, " + f"Unit of measurement string too long ({byte_len} bytes, " f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'" ) return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") @@ -214,9 +216,10 @@ def register_unit_of_measurement(value: str) -> int: def register_icon(value: str) -> int: """Register an icon string and return its 1-based index.""" - if value and len(value) > ICON_MAX_LENGTH: + byte_len = len(value.encode("utf-8")) if value else 0 + if byte_len > ICON_MAX_LENGTH: raise ValueError( - f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'" + f"Icon string too long ({byte_len} bytes, max {ICON_MAX_LENGTH}): '{value}'" ) return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index c1849daf4ba..ce941b40dc5 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -149,17 +149,36 @@ def test_icon__invalid(): def test_icon__max_length(): - """Test that icons exceeding 63 characters are rejected.""" - # Exactly 63 chars should pass - max_icon = "mdi:" + "a" * 59 # 63 chars total + """Test that icons exceeding 63 bytes are rejected.""" + # Exactly 63 bytes should pass + max_icon = "mdi:" + "a" * 59 # 63 bytes total assert config_validation.icon(max_icon) == max_icon - # 64 chars should fail - too_long = "mdi:" + "a" * 60 # 64 chars total + # 64 bytes should fail + too_long = "mdi:" + "a" * 60 # 64 bytes total with pytest.raises(Invalid, match="Icon string is too long"): config_validation.icon(too_long) +def test_byte_length() -> None: + """Test ByteLength validator checks UTF-8 byte length, not char count.""" + validator = config_validation.ByteLength(max=10) # pylint: disable=no-member + + # ASCII: 10 chars = 10 bytes, should pass + assert validator("a" * 10) == "a" * 10 + + # ASCII: 11 chars = 11 bytes, should fail + with pytest.raises(Invalid, match="too long.*11 bytes.*max 10"): + validator("a" * 11) + + # Multibyte: 3 chars × 3 bytes = 9 bytes, should pass + assert validator("温度传") == "温度传" + + # Multibyte: 4 chars × 3 bytes = 12 bytes, should fail + with pytest.raises(Invalid, match="too long.*12 bytes.*max 10"): + validator("温度传感") + + @pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True)) def test_boolean__valid_true(value): assert config_validation.boolean(value) is True @@ -567,14 +586,23 @@ def test_validate_entity_name__slash_replaced_with_warning( def test_validate_entity_name__max_length() -> None: - # 120 chars should pass + # 120 bytes should pass assert config_validation._validate_entity_name("x" * 120) == "x" * 120 - # 121 chars should fail - with pytest.raises(Invalid, match="too long.*121 chars.*Maximum.*120"): + # 121 bytes should fail + with pytest.raises(Invalid, match="too long.*121 bytes.*Maximum.*120"): config_validation._validate_entity_name("x" * 121) +def test_validate_entity_name__multibyte_byte_length() -> None: + # 40 chars of 3-byte UTF-8 = 120 bytes, should pass + assert config_validation._validate_entity_name("温" * 40) == "温" * 40 + + # 41 chars of 3-byte UTF-8 = 123 bytes, should fail (over 120 byte limit) + with pytest.raises(Invalid, match="too long.*123 bytes.*Maximum.*120"): + config_validation._validate_entity_name("温" * 41) + + def test_validate_entity_name__none_without_friendly_name() -> None: # When name is "None" and friendly_name is not set, it should fail CORE.friendly_name = None From c6c743e2bb0b7b686bd2d727480d004b879d3948 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:26:11 -1000 Subject: [PATCH 1285/2056] Bump pytest from 9.0.2 to 9.0.3 (#15540) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index a191378dd7f..eeee3434ce0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==9.0.2 +pytest==9.0.3 pytest-cov==7.1.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 From ef6c65c7ecb5cfeefab06035f68e9b1c6ae80f22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 10:37:19 -1000 Subject: [PATCH 1286/2056] [cli] Add config bundle CLI command for remote compilation (#13791) --- esphome/__main__.py | 61 + esphome/bundle.py | 699 ++++++++++ esphome/components/wifi/wpa2_eap.py | 6 +- esphome/yaml_util.py | 33 +- .../fixtures/bundle/assets/certs/ca_cert.pem | 18 + .../bundle/assets/certs/client_cert.pem | 18 + .../bundle/assets/certs/client_key.pem | 27 + .../bundle/assets/fonts/test_font.ttf | Bin 0 -> 202764 bytes .../bundle/assets/images/animation.gif | Bin 0 -> 9735 bytes .../fixtures/bundle/assets/images/logo.png | Bin 0 -> 685 bytes .../fixtures/bundle/assets/web/custom.css | 2 + .../fixtures/bundle/assets/web/custom.js | 2 + .../fixtures/bundle/bundle_test.yaml | 60 + .../fixtures/bundle/common/base.yaml | 1 + .../fixtures/bundle/includes/custom_sensor.h | 3 + .../local_components/my_component/__init__.py | 1 + .../my_component/my_component.h | 2 + tests/unit_tests/fixtures/bundle/secrets.yaml | 4 + tests/unit_tests/test_bundle.py | 1210 +++++++++++++++++ tests/unit_tests/test_main.py | 196 +++ tests/unit_tests/test_yaml_util.py | 54 + 21 files changed, 2390 insertions(+), 7 deletions(-) create mode 100644 esphome/bundle.py create mode 100644 tests/unit_tests/fixtures/bundle/assets/certs/ca_cert.pem create mode 100644 tests/unit_tests/fixtures/bundle/assets/certs/client_cert.pem create mode 100644 tests/unit_tests/fixtures/bundle/assets/certs/client_key.pem create mode 100644 tests/unit_tests/fixtures/bundle/assets/fonts/test_font.ttf create mode 100644 tests/unit_tests/fixtures/bundle/assets/images/animation.gif create mode 100644 tests/unit_tests/fixtures/bundle/assets/images/logo.png create mode 100644 tests/unit_tests/fixtures/bundle/assets/web/custom.css create mode 100644 tests/unit_tests/fixtures/bundle/assets/web/custom.js create mode 100644 tests/unit_tests/fixtures/bundle/bundle_test.yaml create mode 100644 tests/unit_tests/fixtures/bundle/common/base.yaml create mode 100644 tests/unit_tests/fixtures/bundle/includes/custom_sensor.h create mode 100644 tests/unit_tests/fixtures/bundle/local_components/my_component/__init__.py create mode 100644 tests/unit_tests/fixtures/bundle/local_components/my_component/my_component.h create mode 100644 tests/unit_tests/fixtures/bundle/secrets.yaml create mode 100644 tests/unit_tests/test_bundle.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 87abd7f7965..a696cceffb0 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1242,6 +1242,38 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None: return 0 +def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None: + from esphome.bundle import BUNDLE_EXTENSION, ConfigBundleCreator + + creator = ConfigBundleCreator(config) + + if args.list_only: + files = creator.discover_files() + for bf in sorted(files, key=lambda f: f.path): + safe_print(f" {bf.path}") + _LOGGER.info("Found %d files", len(files)) + return 0 + + result = creator.create_bundle() + + if args.output: + output_path = Path(args.output) + else: + stem = CORE.config_path.stem + output_path = CORE.config_dir / f"{stem}{BUNDLE_EXTENSION}" + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_bytes(result.data) + + _LOGGER.info( + "Bundle created: %s (%d files, %.1f KB)", + output_path, + len(result.files), + len(result.data) / 1024, + ) + return 0 + + def command_dashboard(args: ArgsProtocol) -> int | None: from esphome.dashboard import dashboard @@ -1517,6 +1549,7 @@ POST_CONFIG_ACTIONS = { "rename": command_rename, "discover": command_discover, "analyze-memory": command_analyze_memory, + "bundle": command_bundle, } SIMPLE_CONFIG_ACTIONS = [ @@ -1818,6 +1851,24 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs="+" ) + parser_bundle = subparsers.add_parser( + "bundle", + help="Create a self-contained config bundle for remote compilation.", + ) + parser_bundle.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_bundle.add_argument( + "-o", + "--output", + help="Output path for the bundle archive.", + ) + parser_bundle.add_argument( + "--list-only", + help="List discovered files without creating the archive.", + action="store_true", + ) + # Keep backward compatibility with the old command line format of # esphome <config> <command>. # @@ -1896,6 +1947,16 @@ def run_esphome(argv): _LOGGER.warning("Skipping secrets file %s", conf_path) return 0 + # Bundle support: if the configuration is a .esphomebundle, extract it + # and rewrite conf_path to the extracted YAML config. + from esphome.bundle import is_bundle_path, prepare_bundle_for_compile + + if is_bundle_path(conf_path): + _LOGGER.info("Extracting config bundle %s...", conf_path) + conf_path = prepare_bundle_for_compile(conf_path) + # Update the argument so downstream code sees the extracted path + args.configuration[0] = str(conf_path) + CORE.config_path = conf_path CORE.dashboard = args.dashboard diff --git a/esphome/bundle.py b/esphome/bundle.py new file mode 100644 index 00000000000..b6816c7c95e --- /dev/null +++ b/esphome/bundle.py @@ -0,0 +1,699 @@ +"""Config bundle creator and extractor for ESPHome. + +A bundle is a self-contained .tar.gz archive containing a YAML config +and every local file it depends on. Bundles can be created from a config +and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz`` +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +import io +import json +import logging +from pathlib import Path +import re +import shutil +import tarfile +from typing import Any + +from esphome import const, yaml_util +from esphome.const import ( + CONF_ESPHOME, + CONF_EXTERNAL_COMPONENTS, + CONF_INCLUDES, + CONF_INCLUDES_C, + CONF_PATH, + CONF_SOURCE, + CONF_TYPE, +) +from esphome.core import CORE, EsphomeError + +_LOGGER = logging.getLogger(__name__) + +BUNDLE_EXTENSION = ".esphomebundle.tar.gz" +MANIFEST_FILENAME = "manifest.json" +CURRENT_MANIFEST_VERSION = 1 +MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB +MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB + +# Directories preserved across bundle extractions (build caches) +_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio") +_BUNDLE_STAGING_DIR = ".bundle_staging" + + +class ManifestKey(StrEnum): + """Keys used in bundle manifest.json.""" + + MANIFEST_VERSION = "manifest_version" + ESPHOME_VERSION = "esphome_version" + CONFIG_FILENAME = "config_filename" + FILES = "files" + HAS_SECRETS = "has_secrets" + + +# String prefixes that are never local file paths +_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<") + +# File extensions recognized when resolving relative path strings. +# A relative string with one of these extensions is resolved against the +# config directory and included if the file exists. +_KNOWN_FILE_EXTENSIONS = frozenset( + { + # Fonts + ".ttf", + ".otf", + ".woff", + ".woff2", + ".pcf", + ".bdf", + # Images + ".png", + ".jpg", + ".jpeg", + ".bmp", + ".gif", + ".svg", + ".ico", + ".webp", + # Certificates + ".pem", + ".crt", + ".key", + ".der", + ".p12", + ".pfx", + # C/C++ includes + ".h", + ".hpp", + ".c", + ".cpp", + ".ino", + # Web assets + ".css", + ".js", + ".html", + } +) + + +# Matches !secret references in YAML text. This is intentionally a simple +# regex scan rather than a YAML parse — it may match inside comments or +# multi-line strings, which is the conservative direction (include more +# secrets rather than fewer). +_SECRET_RE = re.compile(r"!secret\s+(\S+)") + + +def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]: + """Scan YAML files for ``!secret <key>`` references.""" + keys: set[str] = set() + for fpath in yaml_files: + try: + text = fpath.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + for match in _SECRET_RE.finditer(text): + keys.add(match.group(1)) + return keys + + +@dataclass +class BundleFile: + """A file to include in the bundle.""" + + path: str # Relative path inside the archive + source: Path # Absolute path on disk + + +@dataclass +class BundleResult: + """Result of creating a bundle.""" + + data: bytes + manifest: dict[str, Any] + files: list[BundleFile] + + +@dataclass +class BundleManifest: + """Parsed and validated bundle manifest.""" + + manifest_version: int + esphome_version: str + config_filename: str + files: list[str] + has_secrets: bool + + +class ConfigBundleCreator: + """Creates a self-contained bundle from an ESPHome config.""" + + def __init__(self, config: dict[str, Any]) -> None: + self._config = config + self._config_dir = CORE.config_dir + self._config_path = CORE.config_path + self._files: list[BundleFile] = [] + self._seen_paths: set[Path] = set() + self._secrets_paths: set[Path] = set() + + def discover_files(self) -> list[BundleFile]: + """Discover all files needed for the bundle.""" + self._files = [] + self._seen_paths = set() + self._secrets_paths = set() + + # The main config file + self._add_file(self._config_path) + + # Phase 1: YAML includes (tracked during config loading) + self._discover_yaml_includes() + + # Phase 2: Component-referenced files from validated config + self._discover_component_files() + + return list(self._files) + + def create_bundle(self) -> BundleResult: + """Create the bundle archive.""" + files = self.discover_files() + + # Determine which secret keys are actually referenced by the + # bundled YAML files so we only ship those, not the entire + # secrets.yaml which may contain secrets for other devices. + yaml_sources = [ + bf.source for bf in files if bf.source.suffix in (".yaml", ".yml") + ] + used_secret_keys = _find_used_secret_keys(yaml_sources) + filtered_secrets = self._build_filtered_secrets(used_secret_keys) + + has_secrets = bool(filtered_secrets) + if has_secrets: + _LOGGER.warning( + "Bundle contains secrets (e.g. Wi-Fi passwords). " + "Do not share it with untrusted parties." + ) + + manifest = self._build_manifest(files, has_secrets=has_secrets) + + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + # Add manifest first + manifest_data = json.dumps(manifest, indent=2).encode("utf-8") + _add_bytes_to_tar(tar, MANIFEST_FILENAME, manifest_data) + + # Add filtered secrets files + for rel_path, data in sorted(filtered_secrets.items()): + _add_bytes_to_tar(tar, rel_path, data) + + # Add files in sorted order for determinism, skipping secrets + # files which were already added above with filtered content + for bf in sorted(files, key=lambda f: f.path): + if bf.source in self._secrets_paths: + continue + self._add_to_tar(tar, bf) + + return BundleResult(data=buf.getvalue(), manifest=manifest, files=files) + + def _add_file(self, abs_path: Path) -> bool: + """Add a file to the bundle. Returns False if already added.""" + abs_path = abs_path.resolve() + if abs_path in self._seen_paths: + return False + if not abs_path.is_file(): + _LOGGER.warning("Bundle: skipping missing file %s", abs_path) + return False + + rel_path = self._relative_to_config_dir(abs_path) + if rel_path is None: + _LOGGER.warning( + "Bundle: skipping file outside config directory: %s", abs_path + ) + return False + + self._seen_paths.add(abs_path) + self._files.append(BundleFile(path=rel_path, source=abs_path)) + return True + + def _add_directory(self, abs_path: Path) -> None: + """Recursively add all files in a directory.""" + abs_path = abs_path.resolve() + if not abs_path.is_dir(): + _LOGGER.warning("Bundle: skipping missing directory %s", abs_path) + return + for child in sorted(abs_path.rglob("*")): + if child.is_file() and "__pycache__" not in child.parts: + self._add_file(child) + + def _relative_to_config_dir(self, abs_path: Path) -> str | None: + """Get a path relative to the config directory. Returns None if outside. + + Always uses forward slashes for consistency in tar archives. + """ + try: + return abs_path.relative_to(self._config_dir).as_posix() + except ValueError: + return None + + def _discover_yaml_includes(self) -> None: + """Discover YAML files loaded during config parsing. + + We track files by wrapping _load_yaml_internal. The config has already + been loaded at this point (bundle is a POST_CONFIG_ACTION), so we + re-load just to discover the file list. + + Secrets files are tracked separately so we can filter them to + only include the keys this config actually references. + """ + with yaml_util.track_yaml_loads() as loaded_files: + try: + yaml_util.load_yaml(self._config_path) + except EsphomeError: + _LOGGER.debug( + "Bundle: re-loading YAML for include discovery failed, " + "proceeding with partial file list" + ) + + for fpath in loaded_files: + if fpath == self._config_path.resolve(): + continue # Already added as config + if fpath.name in const.SECRETS_FILES: + self._secrets_paths.add(fpath) + self._add_file(fpath) + + def _discover_component_files(self) -> None: + """Walk the validated config for file references. + + Uses a generic recursive walk to find file paths instead of + hardcoding per-component knowledge about config dict formats. + After validation, components typically resolve paths to absolute + using CORE.relative_config_path() or cv.file_(). Relative paths + with known file extensions are also resolved and checked. + + Core ESPHome concepts that use relative paths or directories + are handled explicitly. + """ + config = self._config + + # Generic walk: find all file paths in the validated config + self._walk_config_for_files(config) + + # --- Core ESPHome concepts needing explicit handling --- + + # esphome.includes / includes_c - can be relative paths and directories + esphome_conf = config.get(CONF_ESPHOME, {}) + for include_path in esphome_conf.get(CONF_INCLUDES, []): + resolved = _resolve_include_path(include_path) + if resolved is None: + continue + if resolved.is_dir(): + self._add_directory(resolved) + else: + self._add_file(resolved) + for include_path in esphome_conf.get(CONF_INCLUDES_C, []): + resolved = _resolve_include_path(include_path) + if resolved is not None: + self._add_file(resolved) + + # external_components with source: local - directories + for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []): + source = ext_conf.get(CONF_SOURCE, {}) + if not isinstance(source, dict): + continue + if source.get(CONF_TYPE) != "local": + continue + path = source.get(CONF_PATH) + if not path: + continue + p = Path(path) + if not p.is_absolute(): + p = CORE.relative_config_path(p) + self._add_directory(p) + + def _walk_config_for_files(self, obj: Any) -> None: + """Recursively walk the config dict looking for file path references.""" + if isinstance(obj, dict): + for value in obj.values(): + self._walk_config_for_files(value) + elif isinstance(obj, (list, tuple)): + for item in obj: + self._walk_config_for_files(item) + elif isinstance(obj, Path): + if obj.is_absolute() and obj.is_file(): + self._add_file(obj) + elif isinstance(obj, str): + self._check_string_path(obj) + + def _check_string_path(self, value: str) -> None: + """Check if a string value is a local file reference.""" + # Fast exits for strings that cannot be file paths + if len(value) < 2 or "\n" in value: + return + if value.startswith(_NON_PATH_PREFIXES): + return + # File paths must contain a path separator or a dot (for extension) + if "/" not in value and "\\" not in value and "." not in value: + return + + p = Path(value) + + # Absolute path - check if it points to an existing file + if p.is_absolute(): + if p.is_file(): + self._add_file(p) + return + + # Relative path with a known file extension - likely a component + # validator that forgot to resolve to absolute via cv.file_() or + # CORE.relative_config_path(). Warn and try to resolve. + if p.suffix.lower() in _KNOWN_FILE_EXTENSIONS: + _LOGGER.warning( + "Bundle: non-absolute path in validated config: %s " + "(component validator should return absolute paths)", + value, + ) + resolved = CORE.relative_config_path(p) + if resolved.is_file(): + self._add_file(resolved) + + def _build_filtered_secrets(self, used_keys: set[str]) -> dict[str, bytes]: + """Build filtered secrets files containing only the referenced keys. + + Returns a dict mapping relative archive path to YAML bytes. + """ + if not used_keys or not self._secrets_paths: + return {} + + result: dict[str, bytes] = {} + for secrets_path in self._secrets_paths: + rel_path = self._relative_to_config_dir(secrets_path) + if rel_path is None: + continue + try: + all_secrets = yaml_util.load_yaml(secrets_path, clear_secrets=False) + except EsphomeError: + _LOGGER.warning("Bundle: failed to load secrets file %s", secrets_path) + continue + if not isinstance(all_secrets, dict): + continue + filtered = {k: v for k, v in all_secrets.items() if k in used_keys} + if filtered: + data = yaml_util.dump(filtered, show_secrets=True).encode("utf-8") + result[rel_path] = data + return result + + def _build_manifest( + self, files: list[BundleFile], *, has_secrets: bool + ) -> dict[str, Any]: + """Build the manifest.json content.""" + return { + ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION, + ManifestKey.ESPHOME_VERSION: const.__version__, + ManifestKey.CONFIG_FILENAME: self._config_path.name, + ManifestKey.FILES: [f.path for f in files], + ManifestKey.HAS_SECRETS: has_secrets, + } + + @staticmethod + def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None: + """Add a BundleFile to the tar archive with deterministic metadata.""" + with open(bf.source, "rb") as f: + _add_bytes_to_tar(tar, bf.path, f.read()) + + +def extract_bundle( + bundle_path: Path, + target_dir: Path | None = None, +) -> Path: + """Extract a bundle archive and return the path to the config YAML. + + Sanity checks reject path traversal, symlinks, absolute paths, and + oversized archives to prevent accidental file overwrites or extraction + outside the target directory. These are **not** a security boundary — + bundles are assumed to come from the user's own machine or a trusted + build pipeline. + + Args: + bundle_path: Path to the .tar.gz bundle file. + target_dir: Directory to extract into. If None, extracts next to + the bundle file in a directory named after it. + + Returns: + Absolute path to the extracted config YAML file. + + Raises: + EsphomeError: If the bundle is invalid or extraction fails. + """ + + bundle_path = bundle_path.resolve() + if not bundle_path.is_file(): + raise EsphomeError(f"Bundle file not found: {bundle_path}") + + if target_dir is None: + target_dir = _default_target_dir(bundle_path) + + target_dir = target_dir.resolve() + target_dir.mkdir(parents=True, exist_ok=True) + + # Read and validate the archive + try: + with tarfile.open(bundle_path, "r:gz") as tar: + manifest = _read_manifest_from_tar(tar) + _validate_tar_members(tar, target_dir) + tar.extractall(path=target_dir, filter="data") + except tarfile.TarError as err: + raise EsphomeError(f"Failed to extract bundle: {err}") from err + + config_filename = manifest[ManifestKey.CONFIG_FILENAME] + config_path = target_dir / config_filename + if not config_path.is_file(): + raise EsphomeError( + f"Bundle manifest references config '{config_filename}' " + f"but it was not found in the archive" + ) + + return config_path + + +def read_bundle_manifest(bundle_path: Path) -> BundleManifest: + """Read and validate the manifest from a bundle without full extraction. + + Args: + bundle_path: Path to the .tar.gz bundle file. + + Returns: + Parsed BundleManifest. + + Raises: + EsphomeError: If the manifest is missing, invalid, or version unsupported. + """ + + try: + with tarfile.open(bundle_path, "r:gz") as tar: + manifest = _read_manifest_from_tar(tar) + except tarfile.TarError as err: + raise EsphomeError(f"Failed to read bundle: {err}") from err + + return BundleManifest( + manifest_version=manifest[ManifestKey.MANIFEST_VERSION], + esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"), + config_filename=manifest[ManifestKey.CONFIG_FILENAME], + files=manifest.get(ManifestKey.FILES, []), + has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False), + ) + + +def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]: + """Read and validate manifest.json from an open tar archive.""" + + try: + member = tar.getmember(MANIFEST_FILENAME) + except KeyError: + raise EsphomeError("Invalid bundle: missing manifest.json") from None + + f = tar.extractfile(member) + if f is None: + raise EsphomeError("Invalid bundle: manifest.json is not a regular file") + + if member.size > MAX_MANIFEST_SIZE: + raise EsphomeError( + f"Invalid bundle: manifest.json too large " + f"({member.size} bytes, max {MAX_MANIFEST_SIZE})" + ) + + try: + manifest = json.loads(f.read()) + except (json.JSONDecodeError, UnicodeDecodeError) as err: + raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err + + # Version check + version = manifest.get(ManifestKey.MANIFEST_VERSION) + if version is None: + raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'") + if not isinstance(version, int) or version < 1: + raise EsphomeError( + f"Invalid bundle: manifest_version must be a positive integer, got {version!r}" + ) + if version > CURRENT_MANIFEST_VERSION: + raise EsphomeError( + f"Bundle manifest version {version} is newer than this ESPHome " + f"version supports (max {CURRENT_MANIFEST_VERSION}). " + f"Please upgrade ESPHome to compile this bundle." + ) + + # Required fields + if ManifestKey.CONFIG_FILENAME not in manifest: + raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'") + + return manifest + + +def _validate_tar_members(tar: tarfile.TarFile, target_dir: Path) -> None: + """Sanity-check tar members to prevent mistakes and accidental overwrites. + + This is not a security boundary — bundles are created locally or come + from a trusted build pipeline. The checks catch malformed archives + and common mistakes (stray absolute paths, ``..`` components) that + could silently overwrite unrelated files. + """ + + total_size = 0 + for member in tar.getmembers(): + # Reject absolute paths (Unix and Windows) + if member.name.startswith(("/", "\\")): + raise EsphomeError( + f"Invalid bundle: absolute path in archive: {member.name}" + ) + + # Reject path traversal (split on both / and \ for cross-platform) + parts = re.split(r"[/\\]", member.name) + if ".." in parts: + raise EsphomeError( + f"Invalid bundle: path traversal in archive: {member.name}" + ) + + # Reject symlinks + if member.issym() or member.islnk(): + raise EsphomeError(f"Invalid bundle: symlink in archive: {member.name}") + + # Ensure extraction stays within target_dir + target_path = (target_dir / member.name).resolve() + if not target_path.is_relative_to(target_dir): + raise EsphomeError( + f"Invalid bundle: file would extract outside target: {member.name}" + ) + + # Track total decompressed size + total_size += member.size + if total_size > MAX_DECOMPRESSED_SIZE: + raise EsphomeError( + f"Invalid bundle: decompressed size exceeds " + f"{MAX_DECOMPRESSED_SIZE // (1024 * 1024)}MB limit" + ) + + +def is_bundle_path(path: Path) -> bool: + """Check if a path looks like a bundle file.""" + return path.name.lower().endswith(BUNDLE_EXTENSION) + + +def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None: + """Add in-memory bytes to a tar archive with deterministic metadata.""" + info = tarfile.TarInfo(name=name) + info.size = len(data) + info.mtime = 0 + info.uid = 0 + info.gid = 0 + info.mode = 0o644 + tar.addfile(info, io.BytesIO(data)) + + +def _resolve_include_path(include_path: Any) -> Path | None: + """Resolve an include path to absolute, skipping system includes.""" + if isinstance(include_path, str) and include_path.startswith("<"): + return None # System include, not a local file + p = Path(include_path) + if not p.is_absolute(): + p = CORE.relative_config_path(p) + return p + + +def _default_target_dir(bundle_path: Path) -> Path: + """Compute the default extraction directory for a bundle.""" + name = bundle_path.name + if name.lower().endswith(BUNDLE_EXTENSION): + name = name[: -len(BUNDLE_EXTENSION)] + return bundle_path.parent / name + + +def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None: + """Move preserved build cache directories back into target_dir. + + If the bundle contained entries under a preserved directory name, + the extracted copy is removed so the original cache always wins. + """ + for dirname, src in preserved.items(): + dst = target_dir / dirname + if dst.exists(): + shutil.rmtree(dst) + shutil.move(str(src), str(dst)) + + +def prepare_bundle_for_compile( + bundle_path: Path, + target_dir: Path | None = None, +) -> Path: + """Extract a bundle for compilation, preserving build caches. + + Unlike extract_bundle(), this preserves .esphome/ and .pioenvs/ + directories in the target if they already exist (for incremental builds). + + Args: + bundle_path: Path to the .tar.gz bundle file. + target_dir: Directory to extract into. Must be specified for + build server use. + + Returns: + Absolute path to the extracted config YAML file. + """ + + bundle_path = bundle_path.resolve() + if not bundle_path.is_file(): + raise EsphomeError(f"Bundle file not found: {bundle_path}") + + if target_dir is None: + target_dir = _default_target_dir(bundle_path) + + target_dir = target_dir.resolve() + target_dir.mkdir(parents=True, exist_ok=True) + + preserved: dict[str, Path] = {} + + # Temporarily move preserved dirs out of the way + staging = target_dir / _BUNDLE_STAGING_DIR + for dirname in _PRESERVE_DIRS: + src = target_dir / dirname + if src.is_dir(): + dst = staging / dirname + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(src), str(dst)) + preserved[dirname] = dst + + try: + # Clean non-preserved content and extract fresh + for item in target_dir.iterdir(): + if item.name == _BUNDLE_STAGING_DIR: + continue + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + + config_path = extract_bundle(bundle_path, target_dir) + finally: + # Restore preserved dirs (idempotent) and clean staging + _restore_preserved_dirs(preserved, target_dir) + if staging.is_dir(): + shutil.rmtree(staging) + + return config_path diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 5d5bd8dca3e..9da3494329a 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -71,9 +71,11 @@ def _validate_load_certificate(value): def validate_certificate(value): + # _validate_load_certificate already calls cv.file_() internally, + # but returns the parsed certificate object. We re-call cv.file_() + # to get the resolved path string that the bundle walker can discover. _validate_load_certificate(value) - # Validation result should be the path, not the loaded certificate - return value + return str(cv.file_(value)) def _validate_load_private_key(key, cert_pw): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index e001316a222..a24c1ebccbc 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Callable -from contextlib import suppress +from collections.abc import Callable, Generator +from contextlib import contextmanager, suppress import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper @@ -44,6 +44,27 @@ _LOGGER = logging.getLogger(__name__) SECRET_YAML = "secrets.yaml" _SECRET_CACHE = {} _SECRET_VALUES = {} +# Not thread-safe — config processing is single-threaded today. +_load_listeners: list[Callable[[Path], None]] = [] + + +@contextmanager +def track_yaml_loads() -> Generator[list[Path]]: + """Context manager that records every file loaded by the YAML loader. + + Yields a list that is populated with resolved Path objects for every + file loaded through ``_load_yaml_internal`` while the context is active. + """ + loaded: list[Path] = [] + + def _on_load(fname: Path) -> None: + loaded.append(Path(fname).resolve()) + + _load_listeners.append(_on_load) + try: + yield loaded + finally: + _load_listeners.remove(_on_load) class ESPHomeDataBase: @@ -466,6 +487,8 @@ def load_yaml(fname: Path, clear_secrets: bool = True) -> Any: def _load_yaml_internal(fname: Path) -> Any: """Load a YAML file.""" + for listener in _load_listeners: + listener(fname) try: with fname.open(encoding="utf-8") as f_handle: return parse_yaml(fname, f_handle) @@ -473,10 +496,10 @@ def _load_yaml_internal(fname: Path) -> Any: raise EsphomeError(f"Error reading file {fname}: {err}") from err -def parse_yaml( - file_name: Path, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal -) -> Any: +def parse_yaml(file_name: Path, file_handle: TextIOWrapper, yaml_loader=None) -> Any: """Parse a YAML file.""" + if yaml_loader is None: + yaml_loader = _load_yaml_internal try: return _load_yaml_internal_with_type( ESPHomeLoader, file_name, file_handle, yaml_loader diff --git a/tests/unit_tests/fixtures/bundle/assets/certs/ca_cert.pem b/tests/unit_tests/fixtures/bundle/assets/certs/ca_cert.pem new file mode 100644 index 00000000000..6d200b15ef5 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/assets/certs/ca_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzjCCAbagAwIBAgIUW3BzjtekVgMj12/oeXawSswGyXMwDQYJKoZIhvcNAQEL +BQAwITEfMB0GA1UEAwwWRVNQSG9tZSBCdW5kbGUgVGVzdCBDQTAeFw0yNjAyMDYx +MzMxMTZaFw0yNzAyMDYxMzMxMTZaMCExHzAdBgNVBAMMFkVTUEhvbWUgQnVuZGxl +IFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG62vBFkGn +hEu54gh2A7b1ZwesVadZ6u0iaVO7GSWiI0o4nb6xv7ULZbGrgsKNIO6qCV4VSR3p +BfMhF5dFy8kkMzA8dKZMk16tygzocdNum2QQ8BHyIsATL7SGZ33si9Alp30gXv6h +XSlEKYDKHFavkDhWPFNa5+oeHbMS/MxjpOUXIpq32VaFpJr427d9Y9wGjuK8B7Gp +CI5Ub1g2dpC9xSHqQKD3JZokmtc70+mD74AcNWbyxWp0bkW9wOfNJJnAoiwhJxQ8 +yfE37UsUIVc8014NhdhU1K/S0iQuOKfGX1L/GAshv8syQIcDfzJuJdX+5E/leAYD +UEKqRkcLT+D5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF1HpJ6d+W5WrzOQrGej +41pxCDeJ9tSiSj/KtvJfjEVIpg0hMRTY7nSL7OAg9KGESfx4u1jMwVnyOv34br5B +DTlRl+wF2k7Ip8CNnyZfCC+1SVQZpUt1mVNz8BhIZZ9/a830wCILNQQrVKkSeNBk +SEc1qTt4mIhQZ+M422qAswluv4fz/FW1f4oB9KhCpzUCANjmyERnqTnImjnJu8h0 +jbPNnNsN+G+Roju8UD/7atWYfAUmDjHx72Ci/5G9SzoM5fhgxxu43XYd5RW5wBzt +j4KdKdYlDtOL62mRPKWd40uGnJcieUjisU7noRn0ErMgbUlhLdbXT9X7aNborZcu +x6I= +-----END CERTIFICATE----- diff --git a/tests/unit_tests/fixtures/bundle/assets/certs/client_cert.pem b/tests/unit_tests/fixtures/bundle/assets/certs/client_cert.pem new file mode 100644 index 00000000000..6d200b15ef5 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/assets/certs/client_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzjCCAbagAwIBAgIUW3BzjtekVgMj12/oeXawSswGyXMwDQYJKoZIhvcNAQEL +BQAwITEfMB0GA1UEAwwWRVNQSG9tZSBCdW5kbGUgVGVzdCBDQTAeFw0yNjAyMDYx +MzMxMTZaFw0yNzAyMDYxMzMxMTZaMCExHzAdBgNVBAMMFkVTUEhvbWUgQnVuZGxl +IFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG62vBFkGn +hEu54gh2A7b1ZwesVadZ6u0iaVO7GSWiI0o4nb6xv7ULZbGrgsKNIO6qCV4VSR3p +BfMhF5dFy8kkMzA8dKZMk16tygzocdNum2QQ8BHyIsATL7SGZ33si9Alp30gXv6h +XSlEKYDKHFavkDhWPFNa5+oeHbMS/MxjpOUXIpq32VaFpJr427d9Y9wGjuK8B7Gp +CI5Ub1g2dpC9xSHqQKD3JZokmtc70+mD74AcNWbyxWp0bkW9wOfNJJnAoiwhJxQ8 +yfE37UsUIVc8014NhdhU1K/S0iQuOKfGX1L/GAshv8syQIcDfzJuJdX+5E/leAYD +UEKqRkcLT+D5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF1HpJ6d+W5WrzOQrGej +41pxCDeJ9tSiSj/KtvJfjEVIpg0hMRTY7nSL7OAg9KGESfx4u1jMwVnyOv34br5B +DTlRl+wF2k7Ip8CNnyZfCC+1SVQZpUt1mVNz8BhIZZ9/a830wCILNQQrVKkSeNBk +SEc1qTt4mIhQZ+M422qAswluv4fz/FW1f4oB9KhCpzUCANjmyERnqTnImjnJu8h0 +jbPNnNsN+G+Roju8UD/7atWYfAUmDjHx72Ci/5G9SzoM5fhgxxu43XYd5RW5wBzt +j4KdKdYlDtOL62mRPKWd40uGnJcieUjisU7noRn0ErMgbUlhLdbXT9X7aNborZcu +x6I= +-----END CERTIFICATE----- diff --git a/tests/unit_tests/fixtures/bundle/assets/certs/client_key.pem b/tests/unit_tests/fixtures/bundle/assets/certs/client_key.pem new file mode 100644 index 00000000000..6182f45d8be --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/assets/certs/client_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxutrwRZBp4RLueIIdgO29WcHrFWnWertImlTuxkloiNKOJ2+ +sb+1C2Wxq4LCjSDuqgleFUkd6QXzIReXRcvJJDMwPHSmTJNercoM6HHTbptkEPAR +8iLAEy+0hmd97IvQJad9IF7+oV0pRCmAyhxWr5A4VjxTWufqHh2zEvzMY6TlFyKa +t9lWhaSa+Nu3fWPcBo7ivAexqQiOVG9YNnaQvcUh6kCg9yWaJJrXO9Ppg++AHDVm +8sVqdG5FvcDnzSSZwKIsIScUPMnxN+1LFCFXPNNeDYXYVNSv0tIkLjinxl9S/xgL +Ib/LMkCHA38ybiXV/uRP5XgGA1BCqkZHC0/g+QIDAQABAoIBAEpsFwcJNCwf95MG +qcK5lhCPaRQFgdTG68ylmoGUIXvddy3ies+W2X33oLb5958ElLaCRbRyBCJEKxgU +8vBWk50bF69uty9MLa6YuyaWO5QUyCX8I8KzVKh4/zIP81F2Z7xGwy5CzEKED+Xk +Hz6+xoHt094TuN34iaOV2gM/GJsok4Wp/lzsuT3X6i3Nad9YGrV2yL/wv5c542bw +vrFDtYQ/+ADZZPW4+xK0ShiarSqV3iXB2cEjc4JX7yLX1hB4LY8VHRzl+Byjdl0/ +lheiIesl5htl82SFxquZDimDsbilTm7TLW2bbm3b3/oC7DchTx6COBjp90VJqk3R +QrO5dicCgYEA80pyA7tCB0bGnJ7KWkteKddyOdakeYeM7Bpfv17qbCm9ciMw9nqt +KJVZPtAuqZGTpfSJseOCIyz9zloB79hVJ3mdWpGJVvmNM5H+BJyCciXpwfqp64QG +1gMqGlSy/MwsZHqNCsOIvrzH09GFN0LSPNKeXN7GNAtU1vI5s7Xf158CgYEA0U+Y +Qe1qJY4m597spHNFfkGznoFXAjHOoWYHv95902cH6JD4GnYPfwFXxgFsrJhFaFMC +jXlT0fRFAIe4NuUJhGD6TYSJqsFkH3xJkAepvKpfjM5qJ7+PQHRnED/E5OS2Nj0R ++cxBhTEWTw9YiOFBRbj6hlphkj8izVGJZ2pL4GcCgYEApsjiYKx/F33tqnExR7Vj +WEvagswi9S137mQmP4tSKdRzi0uUxWRUUP4RsH4HfzfNgHej7c+J55Nwa4ZIzaQA +vI8i0HP1MyrhIflzqrWgt6BGIDU3R7268fw5YNOv4J4X0Moy5q4lkJzaYNvB96BX +gFrjNceDGSqrfq+P3yNP0QECgYBNQfHTM8ygPA4EO/Zg5ONbrOidsuPovXWlgUGP +ApKy+y6iGxBYxAcIO/in71KrijDkRu+ERKo5rs3hWjcWnAedQyZggnFGA8fvDzMf +5JQ0PTazhGUOcthvVAfOqZsFWZ4f+v6tk0UD4pB3chSdwXcUQyjFeorVLlSsMFJl +R4jmNQKBgG38YFR2bqIc7jJItr+34POXdJ4te8Dm1jJHbo8xXsnjVSaxjc5PGs3p +OuJpwuMwzEuFEnE7XLkQxTJw54OBLMmDgK0XUOPDq6eLzrKkW5NlpejqaQV9Piyo +q1kqbJan20jfJQUGTcX7FXHMUThzqJltHILR1GTW6I9z4k8xdsDY +-----END RSA PRIVATE KEY----- diff --git a/tests/unit_tests/fixtures/bundle/assets/fonts/test_font.ttf b/tests/unit_tests/fixtures/bundle/assets/fonts/test_font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4066b0a9889c2505d31b953487ac1d48fafaf9e9 GIT binary patch literal 202764 zcmZQzWME(rU}RumVPJ4`3-O)5@W3htX7LXU3=A^vF0O8DJGes`n7+3#FfgdN2lxly zl)ov*z$|`%fq}umJvh|q{=9uB7?{4VU|>)X^AFZH(h2;i!oa}T!N9<fker)X5N+$Q zkAXq@2Ll5)PjXp_0)q<E8U_Y+76t|emh{Bpg8%;+m>HP9*Dx?JaHQu{rZuez?`B|d zlwn|ERLMw9OsOi<5M^Ls<Y8c7Fw4kDO^g?bTgt$|=mK(2Ms7((^OndL3=E7f7#O4k za`KZCSxW+^FfgcdFfd4o<R(@Wu$^a8Wnf^;U|?WS$V<#k4e~y{kb%j`fq{W3q9DJx zM0`S~7Xy>a3<icx55Zx<%)sDsGEXF)-{va=H;V-W0|Uc@RfT;}dLjeEe}O0lmJJ~H zGBAO4Fn~yw4gUog5~CO(@*okA4j5)@0g14+Fn(d+1dA~-fOLVv-hn|6EW*NcfpG!@ zD+2@5KBg}W3=GpDG!q+xDWf<80}}%)GguwN69&Ki9vlyTGcYVL0Eq;oxu!5QFev>0 z%Y5K}2SXyu28Is_42<R=Ii?6^50K9p6c~FLR6ru2kYKvNz`%5Y;RDkJh6*Sf#D`&+ zI7kkJLE<nPBnRRXi$UsPdO>=S*&seXOp01!%t1B}nT@R11}QvXY!D5?$m-D9gyhk~ z0mMf)1DhDS8gxFgKgiLKZXP-xWIi%Rm!~x!6t2ja*7nicd{Eee{D+Ok<`$4zm^h4$ zi-xJAH4U?$+V+6Lgcvi4Rfo?kP~L%=17ahq1&Nd5M^e-x+e5BCkQpF1g6N@!LGA`& zn7fhLFfnS-Aag;O8fFn=1}F`oV?z39?LK;#Pis2}g(0c(1MOa9`$731gh}-SrRtI0 zK&e?HNk1suuwin-0Avn1`lzJ`R94|~1G*fDkIi0?I4(1=sRyY=#@N)1ib48n<c0$* ze96%d(u=MSBnHCB;^=&2Ib=3W9gN17USM)CeK0<{T9`OU9WoolhhdO72*da=8k-nQ z4n%|O0?{xGQjabMlS8IK>X7;9Y+}qn*Mo~ssGcAskIP(`IHmew=74Bqvj~L|NIfA8 zG6RHR>d@IBJ_y6)k!f^sWIj3@#)r|!=D^q>IZ(WUXb^^pgT!ELbQ+`%9fQ<>#6UDY z3{r!R@u?q`!(D!Y!jT@~LykQlJs=EoA2J&zhA)l5)Zn5)=7BISc}m5JH5*+Wh!4V) z+CQovGu^||3ydZ=jEIdNkUhw1k=Y=-(fJIl3m6zU3K)=RMjh4#APf<M$%E8zJ1{Wt z2{14SJYZlD-oU^h*1*6Z8Nk3Gqrku*_kn>y@c;vZ$^-@m^#le6EdvGyT?PgQ{R<2X zMhh4iObZwoEF2gZtOXbtY#%T%IBZ~GaBg5=a0_5y@Kj)6@czKS;CFz5A#ef%Lr4Mx zL%0D0188J_)ZXD6&akva?{qa#_A)Rqfaa4xSOzpV#lXPH#J~(@$uO{RJY!&BT*1h| zz|OFOp^jk<BR``mqb{QXqaC9MV<=-HV=7}IV?ARh<3z^!jH?(oG45nM%y^RV3gcbI zmyCaz6q(eS^qI_<?3j|6GMGx4YM2_C`k59ntzp{DbcX3Ba~*R#^Frp8%qN*|v&6F` zvy`&TW?9U#k!3f_A(qQ5_gTKO{A6Wd4P)(K?PBd`-N$;2^%d(E_7eg}1&#}x75Fdu zQ1q9Wh}Z(LEn+`p`DK-4%Vj6ZG0SnwDa)zL8OpiJMam`0Wy{sdwaZPEn<+P6Zm-;D zx&QJT<#)<ol)oweQ2wQYnu584t%8$6kV3dZp+cQPpTaVQbqf0x#TAtljTBuJy%hr$ z!xiHdI~AuW9#Ooa_*IETiAPCTNlr;m$y_N!DM_haX@$~WrDMuW%F@c`mESA>Rb8mM zUG<>qdDR=L&(zq|IMhVc6x7VsoYd0Q^3__^rmHPgJE3-7?S(p%I;T3Hx`eumx{rE_ z`VGxk%@bM%T0vSzv;(vwv|sC_=@ja!>PhM8=~?JG>UkRo8LBSVTyD7BdU^En%H@sA zS1#Z1oAtN+Z|&cvzpa0}{Pz3L{r~U(|KNGP1q|yLg%~v$4H%6W9T|NXBN$T{GZ;%4 z8yWi;XE82k+{Cz@aW~^h#<Prf86PpeW0GJ}VbWtVVzOd#WJ+PmVk&2<V`^cVh!Rs1 znHMu}WWLS9z>>sL#4?Fx5z9K3T`UJ!F0fo<dBpOAm4VfeH4++AhgmPMeqcW#umBuW z7vM3qNNk%dgRBHNrkLco<mBYk<P7BO<U-^U<g(;y<Qn9<<fhBbmfIuuN$v+qObIF& zC|D{uDR?M^D-<Y{C^RT6P*|z30UA?|ir$I=iouGJik*rR6ptvLSNy8Ppv0*ps3fhV zs$`-RqLiT2q_jwBx6)x{24xB5^U5!kKdUZO-K4rl^%yv&7@#p_pk}F-pq8oDpf*8m zq1q9om=XcUR5CoK0<{i<W9pSos!pyhgPw$*o}MW@rqq@jEVo=9wY+k9!}1l&*Z*et zE&E&hw;?#DydW|4|MCAr{}2A(|9|iQ-T!y}-|>Iz|IPn5{$KxpEd#^<B@7JzC;#vH z-|@fwf7Ac^|K*@r+yB1*z5l!ZcmD72-|oNdf1Cf-|E>O8{5Suv_g~?^z<-v13I8Jg z`Tz6#=l;*^pU^*ne@qMvfA9Rg^!L=?V}E!4-Trs$-_3v5{;mC6{kQ6G*<TL^hQF47 zO@1eS>iX3AspV7C+l6mC-xj=i`sT@-#~}Njl|GAk`sV4gr&pd{dU}e1;pw`kE1oWR zI^k*G)1Ig8PuremJvC!scvAnQ;z{C@s3*Zsd>9y>2tN^e!pFeygz53C$1ff~e|+un zmB*JK*E}wIoXo)R*!QvLV~@w~kL4aqJbL-){-gbmwlXk0TJvZr$i7FF3=EI_7#JSO zgLn)K4@DUm9(=za0^!})zAty5^*+nJpZDI~yU)OIZ|}W5_trq<?&aJIxo3UP^qwID z!`**(zu$d$H}CeV+YPrRZ~eU0dGp`R?>EzKyk%gx5zfGHJ&=Lny5n_=t3_85Ai5bC z;>2Mr1_rKT1_rJ)t~dq;wmIPWd4v=ii?NS^ff1wzNt|&8<1!>Jkxa%(_!NTIQQ%O( zSc0yXaRxS}sHzyJK-4hqfY7KU4$)O0Rg7yG_c1Uq9%5i%Ji@@hc#iQ3;|&G|#yeoS zCyehHe}GsB%=ibwX5wHHW0HfgK_rt5h{TSWxEL6i1Q-~YgkbVa0!(5|5+FTHGE8zz z3QS5&It&a<;58FWMhpy077PrGpvD~oqaLF^BX~swqamXiWc>r91*0WnC}SANG{$ho z2*yapD8^{U7{*wT2m=FS9Ai8K17iYXB4ZLrgfW>hg{gw662xYzVyXtyHB7b4>>w61 z2Q%2W3=GV7neQ<$FyCijV1B^-koge<1M_1B2IeQsPZ=1PpD{mYegRU?z`*>H`4#hP z=0D7Tng20^N@xZa1{OvZEf#H%Jc|yCE{h(EK8qOx1B*FitqB7IizSN{i!}oSiw%n{ ziyZ?4i#>}2iz7%q0|Sc_OCU=SOE3#KXRw5_gfTF%gtJ7jOasZYOlR4}z`(MbWe>{{ z1_qX+EXP=mGcd560Lz_ZImL3Cfq~@=%Q*%Hmh&vv7#LWtv)lm7-DJ57rth)bXJBA? z!19oRf#ng)W0t2Z&%ol(SzfTbWckMO9nAZ|@{{Ek%Wp9I56fSce=K01F@P4YFfcH% zGO{wUinB_vN-{98N`X|dN;5F9%CH)-8Z$7kny{LJ#mrd2D^wU5SS?sBK_UzctX8ZJ zP?jTW2m=FaC<CY!P|Lu;TF2S|l3{IRZDVa`?O^R>?PBd_?O|YG?Pcv_?Ps0HI*D~M z>lD_htkYPhv(8|h$vTUHfps?P90mr~xvcX+dO>{%*2Sz#SeG&|ur6a=!N3MeZJ=d1 z;8qVr34~;10Ikmfv0*fbjSYiV@PJxUEDWp+Yz*uS91NTcTnyX{JPf=Hd<^^y0t|u- zLJYzTA`GGoVhrL85)6_IQVh}zG7Pc|at!he3Ji)2N({;jDh#R&Y7FWO8Vs5YS`69@ zIt;oDdJOsu1`LJ_MhwObCJd$wW(?*G77UgQRt(k*HVn25b`16m4h)VAP7KZrE)1>= zZVc`W9t@rgUJTw0J`BDLehmH$0Sti*K@7nRAq=4mVGQ965e$(GQ4G-xF$}Q`aSZVc z2@Hu03=FLdZ44a@T?~^LrZdcCSj4c9VKKu}h9wNk7*;T>Vpz$rnqdvYI)=3j>lrpM zY+~5Lu$f^i!*+&k47(Y2G3;U3%VN(^$dJO2%8<$8$WYF(fFYkDn`H_^DMJ>^IfizI znJkAHiok)F&QQj(on<cr0~0&T35FboYR2yj%NceuG%zGH<bhRyh+Kvm7AJ-zhDL@K zhG~rKj2xi60nQDUELIGla_a!2Dgy&c1B*9{4=Ao#92hECtXb?>oEe%Kx*3`ndKfwx z`WX5dCNT6eOktSHz`!t>VK&1YhFKuv85kG}7#J8qDIT29K`Y5XYtq1SpnQdlLF>{M zFfcHDVPIf%U|?XJz`(%xfq{XEhk=2~gMopmfq{W(2Ll7s15hGhU|^PEU|=?3U|`N+ zU|`<EzyK--SWG}}Wnf_0!@$7uhk*f<##u`k7+4Q5FtD*OFtF(`Ft9~1Ft9CPU|>7J zz`!oRz`!2Cz`#C-fr0%B0|SQ$0|Q3@0|Q3`0|Unz1_q8R3=EtC3=Et$3=EtZ3=Eu8 z7#KKjFfefbU|`@fVPN1YU|`@{!N9<EgMootfq{WLhJk^52?GQ72?hr4FANMk1`G^5 zISdRuCm0xbSr{02Js22xa~K$SmoPB!o?u|$V_;z5lVM=s1NE?L7#R4rFfj0aU|`_a zU|`@+VPN2&!N9<Og@HkUgMmT7hJit#gn>a|3j>3|9R>zL5e5c98wLi!5(Wmr4Gau| zZx|SaWEdEPA{ZEidKeglwlFXVePCb^R$yQdu7LF1h2JnRh^R0yh&V7Xh*U5zh-`r3 z8w?DhDhv#wDGUsvI~W+m1Q-~^Y#12CrZ6yw?O<RKXJKFvw_#uqU%<csT8}8<z`!68 z!@wXhfq_Bd1OtO42V_J*vVeg>asdN_<P8P}$uA5HQZft-QXUKpQXLEoQg0X-q(c}O zq_;3I$mlRI$doWJ$Q)r{kQHHIkj-FVkUhe{ASc4WAeY0yAh(8rLGA|wgS-s`gM0%6 zgZvo=1_coY289p?28AgM3<`G`7!(y47!(sAqYp|v3=B#U3=B$Z7#Nf}7#Nfz7#Nf{ zFfb^8VPH_PU|>+GVPH@>!N8y@z`&rIz`&q7g@Hl!4g-Ul4g-T)4FiMP6$S=%69xwL z4h9DG7Yqy<CJYQ34Gat#7Z?~c6&M&aD;O9w&oD4(aWF7wxiBzjbuch!onT<lmSJGf z&S7BC-oe12!@|Iz6T!fsvxR{{mxX~r*MfmTw}yd1cMk)D?jHsQJr@QBy$%Kjy(<h1 z`Wy@l`VkBa`U@Bs^xrTr7`QMn7|dZ{FnGeiU}(U=VA#UIV0eRp!N`Px!Ds>lgV7xZ z24f8d2ICe62IB_|3?@1Z3??}Y3??fW7)-t}Fqk?pFqqC@U@*PHz+fiBz+e``z+kq5 zfx+wx1B1B_1B3Yz1_tv#3=9?_3=9@)7#J+RFfdr!FfdqlFfdr&U|_IPVPLQ-VPLR2 z0vV~X_F-VKZed`sKEuFZBf!95<H5jS)55@DbB2MzR)m4UHim)0b_)Z89R~x0od*Mh zT>}Gy-5v%8yEhCB_6iIP_6ZCO_DdKT?C&rzILI(CI21reejFJX7#wXF7#wFXFgQM7 zU~tl4U~np6U~oFZz~C&wz~EfLz~Fp?fx$(Efx)GOfx+bj1B0so1A}W01B2@x1_n0? z1_rkr1_rka3=Hl%3=HlK3=HmP7#KVZ7#KWy7#KXxFfe%XFfe!~Ffe#-U|{fk!@%IB z!oc8Fz`)?OgMq=Dhk?O6hJnF*3j>1>2Lppo0t1831_lOS4h9C_7zPI44Gau^JPZte z2@DK=TNoJp85kJ+Qy3Wh4=^zJKVV=8&|zQ*NMT?ISi-;%@PL6KP=SFV&<8Sl6?lh% zA&7^8A;^J&A!q^vL(m=uhM+$T48bl848aWy48cbj7{JAy1p`A!4Ff~S0S1Op1_p-E z5C(?O84L`e7Z@1A1Q-~?0vH&=au^uGrZ6ysonc@I`@+Bw?!v$jK8JxJ{0jp^gbf2j zL<0ju#1#gHNDT&t$PxyI$UO`Ukv|w1qC6NFqLwf)MEzl4hz?<3h~B}#5F^3B5L3ax z5OalrAy$TgAvTABA$AV~LmUSKLtG34L);t&hPW>b4Dk^R4DlNn7~=mhFeGR&FeKzK zFeL0@U`XU(U`PyMU`SlSz>s)_fgwqQfg!1Yfgx!F14Gge28Lt@28QGo28QGl3=Am( z3=Am|3=Anf3=An37#LCo7#LDR7#LDlFfgQkU|>j7U|>i~U|>j_!N8Dqfq@}ifq@}C zgMlG^2?Im=69$G16$XZk1O|qT6$}g+Zx|Rd4Hy_QOBfh3w=ghd{$OCp@?c=dYGGi= z+QYz*^@V{U+k$~1yM%!u`v3z&4hsWAjtc`rP6Go&&K?GaTm}Y)TpI?4+zJMU+&v87 zN-xiZfg!Jkfgx`P14G^i28MhC28R3$28R443=H{i7#Ipv7#Ioy7#IqsFfbHcVPGiu z!N5>x!N5>hz`#(rhJm5*4Ff}w0s}))2m?dW1O|qp3k(d!5)2H*84L`?a~K$kFEB8a z@GvlxI504j<S;Ol%wS+Bxxm0s%E7=;>chZLTEW0jx`%<G^aBG!nF<3#SqcL~*%AhZ zvKtHx<su9W<t_{i<uwcp<vSP{Dg+o9Dgqc7Di$y>RNP=-s1#sesPtf9sO(^1s64~K zP{qT*P!+(yP&I{tq3QqwLp28jL$wbBLv;=VL-hs*hUy;-3^fi63^fxN7;4rqFw`7j zV5oV(z);J<z)-8gz)%~*z)(AffuVK}14Hc(28KEn28Ox}28OyR3=DNQ7#QkV7#Qk3 z7#QkX7#QjgFfi2rVPI%5VPI&;VPI&Oz`)ROfPtZrg@K__gMpzjg@K`Q3j;%w00Tpl z4+BHf1O|qtD+~<H3JeU*1q=+$2N)Qd|1dDL1TZkP)G#o#>|tPNdBDKXs=&a|n!v!& zI)Q<q^#lV$n*swvTLc3`TMGk2+YSbXwm%FE?IsKi?FkGF?Nb;S+V3zhbeJ$Obd)eK zbR1w{=wx7E=yYLV=<H!&=sd!}(D{dfq051Rp{s*|p=%2RL)Q-mhHe!GhVBdohVB&% z4Bc-S7<xPy7<%R~F!X$2VCYp~VCaotVCbE|z|ebvfuZ*U14Ew<14CZ~14G{u28O;5 z3=I7`3=I7p3=I8e7#RA0FfdFoVPKe$z`!tJ1_Q%{Jq!#J-Y_ss)L>wk=)=G;v4w$Q z;uQvlNd^oIlM)yhCQV>qm~?=FVbTu<hRHe%43je$7$(nPV3>S|fnkaQ1H+UU28Jm! z7#OD9U|^Uk!N4%pf`MUb0RzL-H4F?>zc4UNb6{YYR>QzB?FIwGbO{EA=`IWm(;FBV zrmtaOnEr%;VFm{S!weS&h8YbE3^SH6FwD5az%WyVfnjD11H;TU3=A`GFfh#GU|^W# zz`!u8fPrDw3I>K*Hy9XZ%P=s^PGDe|J%fQ^_6r7vIW`Oob2=Cp=Imi$n9IPxFxQ5G zVQv8f!`uxF40B&FFwE0nV3?P}z%XwG0|TgmKR<+lVLpg`gMndz1Ovl@2nL1)GZ+{a z++bi>D8s<8FoA(#;RFVTg?ktn7MU<GENWq3SagAbVX*`Q!{Pu2hQ%!m42usiFf9JV zz_7%Ffnmu628Jbj7#Nm(U|?7(!@#h#gMne`2L^^^9t;f27BDa@`@q1k+=YQ*`2+@r z<u4c*R_HJ=tjJ(sSaF1bVWk8E!^#>4hLv|17*@G3Fsxd^z_98I1H)<u28PvB7#LQ+ zU|?8d!N9PlgMneq2?mC>91IL=6BroQ?qFb8C&Iw6u7H7I-3|tZ^$ZLQ>jM}V)-PaS z0Bt$gkifvOVFd%jhBpih8yy%JHcnw+*m#72VG{!b!zL33hD{|544c+4Fl>6jz_3|? zfnl=;1H<MD28PXh7#KGHVPM#z!N9P^gMnd70|UdB3k(cf85kJ0S}-td?O|Zpx`%;b z>lX%wZ7K{5+X@&Mwk=>_*!F~hVY>hW!}bUUhV4BJ4BM|TFzn!9VAx^8z_25MfnmoQ z28JCs7#Mb{Ffi=&U|`regMne^4F-l?0t^hh5*QeEwJ<R3TEf7v>j(qGt|tr(yEzyb zb{jA-><(aH*xkXvu=@Z5!|p!}40{|H81|GfFzngDz_8~71H)bg28O*c3=Df$Ffi<W z!oaXkf`MV51p~vr8U}`a7Z@1!b1*RMw_#w|-^0MLe-8u00Tu>^0|pEX2Vxi)4s<Xu z95}<kaFBt4;h+Np!@(K`hJyzf7!H16U^pbhz;Gylf#FaG1H+*$3=D@pFfbezU|={L zz`$^L1_Q(43k(cLG#D6;Brq@>nZv+v<NyQ1Q4R)%qbdvxM|~I=j+QVm9NogeaP$EK z!!Zd4hGRAi497AU7>>0tFdSRKz;NsW1H*9^28QDb3=GE|7#NP{Ffbfnz`$_)3<JXn z9tMUJ8Vn340vH%hbTBZSIKjYh;s*o6NdpFklQ|3wCs!~qoP5H-aEgP0;gkad!>IxW zhEp>b7)~8vU^w-Jf#Ea@1H)+(28PoG3=F5&Ffg2c!oYBbhk@Zt2m`~J3I>KVD;OBg zykTHCtHHo<)`NlJYy$(s*%J&5=Xe+x&N(nJoSVSFaP9~L!?_;}4Cf6P7|zEqFr1&l zz;ONs1H%Oc28IhE3=9_<7#J>WU|_iLfq~(o3<JZ(7zT!ma~K#d-e6$3q`<&%DTIOH z(gp^GOHUXWE(<U)T=rmKxLm`)aCr*@!xaVwhASQn3|D#>7_J;(V7SV_z;M-pf#K>L z28OG57#OZ;Ffd#zU|_hmg@NJP2L^`gCJYSM=P)o_f5O0ULxX|gMhOGMjROn}H(3}M zZYD4=+?>L|aPtWR!z}{_hFcRD7;c?mV7RToz;HW(f#LQB28P>T7#Qw2FfiPyVPLrP zfPvwz3j@R53I>L|I~W-5aWF94D`8-`cY=Z8J_iHC{TK#@`&$?o9`G<QJP2W6crb;5 z;lT|ChKDi?3=e%67#=n-Fg!fL!0<?hf#FdI1H+>h28KsR7#JQ4FfcrhU|@LM!@%(P z1Ovkp76yhV9t;dmDi|1^Y++z{@`8cksSX3f(+mcNr)wA(p0O}6JhNe7cvix|@azl& z!*dY^hUW<k49{mUFg!oO!0`MB1H%gg28I_M3=A(mFfhE-VPJTfz`*cw2?N8+KMV}7 zG#D6Or7$qOn!v#D>I?(Js}~FmuSFObURN+MygtCd@cIt}!y63-hBq+`3~%NzFub|J z!0=Xqf#Gck1H;<_28Op&7#QAOU|@L1!@%&)f`Q>(0RzLkJq!%*zA!Mnw_sp+-@w4| zeh&k~2Mz{?4=xN0A2JviK1^U>_;7-O;iCit!^aK=hL0B*7(O{LFnnrZVEA;0f#I_Z z1H<PK28Pdj7#P0DFfe=xVPN=Dz`*e33<JYg9tMW5F$@e}OBfiw&S7BqdW3=D>mLS& zZy^i}-<B{ie0#va@Lhy~;d=!G!}lc&4By`{F#IrKVEB>3!0=-Z1H+FC3=BVc7#M!q zFfjbgU|{$;fq~)Y9tMV=e;62kSuimCDqvvvwSa-)*Bu51(16o#9R`Nq84L`+moPB= ze!#%+M}~pnPX+_SpB)Sge;F7U{@O4w{B2=i_`8RJ;qM;?hJPLm4F9GuF#Nl~!0=y$ zf#H7yc!V7^YQxA7!NAC{hJlgc4+A5k2LmJH1O`UNI}D6WA`Fa7F$|1MYZw@r-Y_sS zn=mjkH!v_VA7Ef)5n*6tNnl`PnZUrva)E)7)qsJKbpit;>k|e>HXQ~=wgv`9wi67D z>@p0D>?sV4>>C&u**`Eaa>Ot&avWh`<g{R5<XpkP$oYYRk;{aEk!uPABi9=SMs5oR zM(zm=jNES+7<ptE7<pnC7<r~JF!EesVB}R{VC1b}VC22Qz{sb;z{pp^z{q!nfsx;U zfswz2fsua$1ET-~1EYWs1EatK21bDw42*&j42*&y42*&^7#IZ~Ffa=KVPF)JU|<w7 zVPF&rVPF(0VPF)R!oVnWgn?1$3j?FD3In5X00X0N2Lq$<0tQCmGYpI(8VrmgJq(N@ zYZw?s?l3Tl$}ljBIxsMb7BDc1ZeU;(eZjyerog}`c7lOXoP~i=+=qct`~d@_gaZSk z!~zCJi8Bn0k_-%tk`@e%k_ilql1CUAr8pQEr6L#@rFs|`rM56IO1)rUlvZG1l#XFw zl<r|*l)l2iC?mkYC}YFGC=<cJDAT~eD6@uvQRW5%qbv^tqpSr3qih8OqwE0&M%gb6 zjB+{*jB*(ajB+y=808)?Fv`m?Fv`0yFv=$|Fv@o@Fv@RXV3dErz^Gurz^IUe1)pGG zRJ37WRIFiORJ_2zsHDNbs8qthsI-NFQJIH<Q8|WzQF#pmqly3nqe=n;qskTrMwK57 zjH)3FjH(?BjH*`{7}XRQ7}a_h7}ZWNFsgGfFsjEeFsg50VARlHVALpKVAPnyz^HM9 zfl*U{fl;%7fl>1e1EZD>1Ebay21acb21e}+21e~G42(J+42(J}7#MX$7#MY17#Q_f z7#Q^$7#Q`wFfi)dFfi(GVPMq%z`$sr!@y{e!N6#+gn`lE4FjX01p}jD4+Ep&2?j<Z z76wKm2L?u?8U{wA3k;0L5)6#SF$|2xGZ+|+pD-|*h%hjk#4s?L>|tOuWno}6O<`a( zUBSR;dW3<|^bZ50SpWm0*&GH&vpWoo<|+(~<~|IJ<}D12=6e_z&0jDuT8J<(TI4V= zS}b5-w79~+Xeq$JXz9biXxYNRXnBEw(TazG(aM8?(P{w$qty=vMr#uWM(Y9wM(ZgI zjMh6C7_ILxFxqf1Fxmt#Fxt#uV6=I_z-Y_Ez-a5jz-W7ffzeKYfzfUS1Ebv=21a`a z21fe|21ffG42<@F7#JNq7#JO<Ffck?VPJGrU|@9gVPJGDVPJHe!@%fxf`QTT0|TQI z4+Eo<1p}kg6b4489}JAnA`FbqHVll;DGZFx3m6!kPcSgLurM&X7%(up<S;P0%wS-2 zxx&Ec%EQ3u8pFWoI)#DJjfH{HO^1QeErfy5t%ZTnU5A0uy@G+!{Q?7{hXVtn#})=g zj}Huto*@j3o=X@Qy+jxoy)qaWz2-15dfj1Q^p;^@^iE-5^j^Zi=>3L)(Z_^=(Wixh z(dP&Qqb~;oqi+TSqwf|5M&B<CjD9){jD9H$jD9;982vdI82v3682vjK82yhhF#7*t zU<`0!U<^oMU<{bRz!-3XfiaMSficj8fiW<Lfids^17qMD2F4%-2F9Qm2F9Qj42(g4 z7#M>S7#M@sFfax`VPFgiU|<Y+0l}dL42+=-42+={7#PD$7#PDk7#PF8FffKkFffKM zU|@_8U|@_0U|@`x!N3@)!N3^Vz`z)Jgn==NgMl$hhk-FFf`Kt=4g+H}3j<@c4+CTL z6b8oVI}D65Dh!M<ISh<3M;I7m1sE7(V;C4?=P)qFK44&slVD(s3t(W3o58>scZ2~v z*%EKVz!=}cz!<-QfieCK17m^;17ku017ku517pGg2F8Rx42+3342+2-42+3u7#I_8 zFfb<ZFfb;0Ffb++Ffb-<VPH)9!oZm9!oZkZz`&S1gMl&m1OsCV0|R4<0Rv-73Ik)x z3I@iMCk%|KDh!ONF$|2U8yFZ<pD-|{2{16GxiB!Ml`t@-tzlqH`@+DOZo|Nsp2NVH zzJ-A?{SE_T1_uLUh5`d)Mg{|8#vBI5j0+5mnH&s^nH~&`nNt`TGw(1kX0b3ZW;rl0 zW=&vV%zDDWn61OWn4QAFn7x95G5Z7qWA+;c#vB(0#+(iY#+)+@jJW~~jJYukjJY!y z7;|qhFy`qnFy`ejFy>8QV9Yzfz?jd$z?kpAz?h%Gz?i>)fieF917m>%17m>$17krA z17pDv2F8Ln42*>#42*>}42*>v7#IsbFfbOmFfbN%FfbNfU|=kkVPGuIU|=lX!oXOf z!N6Ej!oXN^fPt};hk>y)f`PGg2?Jy42L{G68wSR*4hF`uD-4X~3Ji?pISh>DI~W)% zSQr>90vH%8<}ff;ykKCgG+|(@Y+zulJj1|PCBeX0mBPSSwSj@L>JI~BwFd)Z^%Mri z>IV#rH98E8H5CkuHAfg2YXuk>YhxG~YgaHZ)_!4NtaD&stm|Q5th>R$Sg*psSYN=v zSigsXv4Mkuu_1(kv0(uNW5XK;#zqSU#>N%~#>NW_j7>5Oj7=E~j7?h@7@HXw7@K_< z7@KD>Fg8D7U~Dm9U~H*jU~D<Tz}PCnz}T9=z}UKmfwA=m17n*D17q6+2FA8K42<m> z42<n142<mu7#KTv7#KSu7#KU2Ffev}U|{UDVPNd+U|{UL!ob+2z`)p*!@$_JgMqP| zg@Lg<fPt}l4g+KN3kJp>69&ef1_s8SGYpKq5)6#JDGZFg8yFaS|1dE2c`z{cO<`c{ zd%(chufxFDU%|lGe}sW?f&c^Kgct_K2`d;FCwyUGoan&7II)L;apDaI#z`s+jFSo& z7$@yvV4Tdsz&JUCfpPKz2FA&67#OEmFfdMOVPKqcfq`+V3<KlT3<k!jTNoIpF)%Pr z^I>3|HiLn2+7kxG=>`mp(`y(Qr=MV8oc@FXG~vrQLxzEIh6MxTj1UIK83hcCGkO>p zXRKggoN<JKamE7%#u<MY7-vc_FwQh#V4NAiz&JC9fpKOB1LMpk42&}mFfh)%!@xN6 z2Lt0Q5eCLt1`Ld|d>9yKWiT+#YGGiUwSa+f)*c4NSvMFMXMJH{obAKFIQs|#<D3`< z#<?O4jC0>GFwT3zz&QU81LHyy2F67_42(-y7#Nq%VPISy!oavvhk<dm3IpR>8wSSp zB@B!k&oD4<abaNGc87s+=M)CUJvj`F`%@Sg57jU*9^Jygc#?;K@k|Q?;{_Q8#>-b2 z7_YBkV7$GAf${bO2F5!&42*Yr7#Q#TVPL%5z`%G<gn{wi9tOtyB@B!Y92giM$}liK z;$dKX%)`L=M1g_vsS5++vl<4*=SLVAUn(##zUpCMe8a=Q_;wBh<9i1N#t&Z@7(eY{ zVEi(Lf$>`d1LKbx2F71K42*x)Ffjgm06KqwfeEx{p3#Nr0s}JxJA>XvMh1PGP3$ZT zZ;dvxvEH%S#LUF-)`(#vGf0>fB)o~8jq#7oCPt9Bv7n-$y0M_L3ZufG8#kCP{54Qv z>Huxz{{T9VfY}3VCWES=vY@!BvM6IlMMcF2rii~QK2%hIrlOb_DwrY|AHdWxnktJb zgVa5!sHk9y`0(KaXc`Kv5VXginL(a`LD<w-l-bl+)Y#P6R2hsRdXz<tMM3yO1u6j9 z`ESOD4<K=nfgl$^=0d^e21qk7D4IgdQZ!XG1=+3)Hb+rZQPddX;(s$3AAq!1FlPKy z05cF)K&HePz-EEMPa5PxWl@k%Am)P276q9J!XR%|{F?#S5Ax*S6(2rSfFPJaa+^BX zZK6<rio$&h@;lT-V^BzlLPHjADue-Y>4%C65Cs+h2LWg*_ybb}V+J_P89?r2%m9Zq zD3}=;Di|2R@gWSdTiH}u5bl0KV^Da2Vg_tlMMVWDXe+?J!O#oRWh|%+4o_oIV?mJL z;SNF7%E(Z`_yA@nD0UJ41;wYbC_*12lI`Gds$hy>y1>BAz@Q3C2_QFSfWi|Lpdgw7 z;-?IT4-D)K3?O$Qy9KfWhA{(_3PEcH8BG;MLFp6ZHLx5w9Wzvb<P<?E6O@8MVaNbF zTZ7qyK^S5t$XsJk>V?J&EQmm<4HN?)*MQA~>4T&@L1j}=Is>^#Sr8J|#-_%CilQK& z{+j`E1;nI(Gb)%Oz)Z%Bzbh)hp^jo6H#ok);S36GrU+1|eW+l%P*L$u0i4%B{Xs}N z1>3DG3QM0LHz<o5n<_%x&J+QP(+^-fDi||hNeE;h*efXZg3W@)7}x}eImV)(&@u-5 z18&d<aKZ-#1uR%V4nw#Ll;*%b0J#Pn4yMYYp!fh=4RRDHnSev#-;4@S*?_9o6qMpX zxmZz9Q4nE2DEt0h!T11dJ2*)H%>c8J<53(GejtB9LIn~dAiu!kq5_<K7&AaNgPl<E zPvOG{P<8<&esKQhXJAkS<rhIkSZE4@toyqHoKqMxz=8Rp;@=E#+C_>Bki8%biUEk7 zpb!A13{X0Ng&)YxQ2Rk~0!rK<2Oz=#>IZNrfXXm%+5(vgEgxV8FlK<_5o9!^Sgc@R zVgRRerU*!R4Js9cVW}OY?w<m@y#C(-$}J4cpj-!1CkV=U@Dd257@R&UAbtcz7y~$s zfXf1KNsGuQpb`a^Mj&B|L4xWM22d@5aGRniESG|F>H|plK=L^_9;6X@5@IW;90Az{ zj#Z@c1j9B^dIYbA0h=StAPy;6kmCkq2D~nVgeb@uP>djGP?BI|VEEqwE}w)M7+6g~ zX%bYPDuVO8vY@djQ^Y?7Q2JoZ03~g3jrVWH2T*FQsQ9}A<P=C4g5AQ;zyPXiA#tk+ z%3XrsR0S#|{>`XheDF^Jl-DaN{(@GAA<_gWPl56UI7fg&9+E#m@dVNe3Oi7(2CCX% z#m%1`NcKYXgYqFbRfEe4K}ejz^n<(&EuBElM%WE5lZ*wG!FDSO!tDH~PyvcKricpA zh!&DvVD+FJq-?5aswfBwpAR1xA3*aDNHxN2PEdG&<Hb}_umYkCWC2JOa=sP>=W9V_ zQBde9f=Vce>p|HN6lkEj=t0H58KBY?<a%Vi+#ov?1wrMhqNy@0yuo43?D63PIGjKQ z0I2-}b~~s9R|MB~Ag6#52RK}?n5zhh6H`#iM5w*+*PsHN(GdO=2e|=MgMwmS8JvGW zDG-#BK$#dE_7xv07(spjB_@yn$Ysd>U<HMNqM$N3L@O$oE}(@ws15_w?xx0Izk=IS zApe5Q0%J(vFnfSFIP{8w^+IZV488wmFnfSQ1Ed+@H&A&A3I|B6fKmy#z5uBNmvazn zL1_h)84&t-L16+ar$KcXD5W5Bf&wToD=I*#0HGdIE<)N-u-qr82(D?MK?boB5}+Ux z{;mKeK5$eZrB6Yyy`V4xmwAw!1hyNNjX)_1q#xn}<T!x10c1NUZG%c@QS>?rRLFw; z4Qp9}Ohr!*?2tMJl*$-0KKxzr0hH^}@;o>#Lh>D`oB=rq<iQG1^BjbsWiz;3f#z?J zYH+-RS|Q*z6ezAC_yfpSpd1Ej{381alx{#NQWTUvl|gYY3Tnkbbiqhw4^Vx;6ak8Q zq_9*5g(bN43Jy<{lnu@mpw_&yAj}j54T>>H$%7F42U-)1h;NXeK=m-BJqB-sf^<Ul zfKnEO`F91htcJGd!0`l1TaXqVs163_H&JD9c@5G8YD<7<ForZuKqtvT+L@rT02D{y zmNTf#`v7Y7V3u_tH-cOS!JrTXV}!#%4R>&*0%>Ew${CQ^ppsZnSrpWw`S5|+16o-y zf?DpdI2DG)3)Fm&AHePf6QC3UW`I%`w4Q?Om0^Xpe?b!9b{?pI2I?!YGl2RE;Ftl2 zBgCOdVaF7~04i%iDO?m1YLJ!;xHSn$)*upLHIiOt53pWvSpY7nLHSEi8C+w4!w*zS zXMnQ9hYH3FaJdU^M>9o$+TWnqQU>KFa1K`#MWlI<OTj50mZSd7Ku-G*_d?UHF{lIt zw>Ci`UjeEgAcYn<v3-E+gZdMci%mhbA*3#b*5;5>7-A(jWq>RMhocPx1CtE12Lq_g z69m;;pt=|oE5?G1I?#S8ENuQMK*I*yegKCnE4WtxYZZdR7gXXafcyn=AE@YHil~4z z=fL)X!v|uYF(@xUav3y#g3}eK;Q9wHFTp{|z{CJ^BQpapIJQB(Sa8j0EXt?@YP@~` zRUV)w2Bf|LwHv{09B^NfAEHMP6pPT-H#m`iLIYGt{n-Jo_CZZn21dADp!yBmhXsWR zQYj#)EVu=hM8UlU7>DrzIR0U7<pkAj%1~D_$$%T_Oc$V80#@EY;tU+akU9=jG9cAb z9~d8e_y7ru3UGk{ElZdpKqnc3S`MJvR}tC*Pym&Oj1NF65$Ztk3i3Iwx(t#OK{)_g zmqFuJn1Mmu6r6&=c?-&d*aS=Z;2skwIl;<#gkDxqEe7%pcuc?;oby1T4vt|^$^iEO zA$=E66CaeYm?D@WK<N;iF2H#PQu0I7Jt$p(1N-j^r0@fmSD-io*LslpOBB{O0tE;- zDJp<9f(fKJ28Ru(Z3G!z0mTE849Gtqg`my|D6@fr1<5{8yn<Vfpxg{DLqJ0(pg00` zXA$8Jbr-(+3*5E^J8uT4Mn~x71=Xd9dJB|?K`w$=1gY>q#V$O|Ky?;MxeBTyKzSJ4 z%K(?(ARfdF{3IlgLgSGi)IR|E0Mtf<#386C0A(C-+q{A)0u+@XK8kz5eg~x(kUKzS z0?2l7Jp~$I`nv*@=8&8Lu@h8FK*lY=qg^2XfYT4CB?@WRgS3G15j5FB>K{<z0o4P{ zkT7G+s9^R0SLsOoE?9d2TIaxeTpu9G4{R)`i3Ktj>Xr}Sv?$ELU=A$<z@;Kczo@b( zNCO0eTmvx#JRbM~+y{k>SAuFKQDIPB0UDVBCH=n!pcW&zodOQu3!wfdE7YHgpt=)e zKE%)9G9MgFpa6zg1|`9wkbxF>+0X0&>Jfr+04OJb#vman8WeVra0i#bkfaUrCb;B+ z*Buv_B0wXUppgwwp90+a0k;#t`42qu2@<b>mg8W}AlHCP4QLez;zKGGXgX&0U{D5? zfnXPbM?}G42r>;^qJY{_5X_hXt!F@HfGvSI6vBm)p#C-@u0gfFpt2x5d>J2r+yd&h zf}32h{L1VB@)sy2f=Y03?+sKALPlhvqY#j?0#t`XN()Gagrsk%-B1ctb%D-qg^p2z z+yII#*mwk}E&=6ZCUCyI!0f@m4vH03u-}yhK{X|)URD+amD><oKY$_=R7ZlW26_75 zj1SPB87QrQ>oL$MB(nztH>hM}1h)x5Inh`Y)FNVhPysHBL9S)Y02u~}8}JUA3Z@H8 z7Z`*Y)InnfpppQT8zDInoCCnA4OI-{1+aHPt^qT!6VQADYK1)jZ9P&I1m$y(qZuE7 z3TjYPfx>~=gV}?@7?KV^brmQ@f!bdn7l6_m$X#FzY8`>v0-*W=oaaE}YY@A^<qL!f zBSAt???STzm;xmNh!TkVm@a_Zoy?#X0LZ+*D?mPk$T4Ps$AG}C15g?Tg$yX=L&l^) z^%N*}z%;0&0i{1s0sxKogX02uWEUJ4kkMVn49KuKq;LTR0VqFw0NVu~wE?9>RQo_S zLk99ewn1VL;tLQ7PRkJe(x8|G=>=hsE{J=LL3s+^H-eY}Awl|K7*rlY$Jg0Ga{-Xt z2#RJ<;Zy-GOTgt)1ZV^eT=zrkIZ%8;M}k1<l<@($x(B<9feDn(!R=L0KN}RYV0Vcc zgGU%aL%g8zqYHlxKmiYqBL+r>51_Ql_y9C^4H~Hf4?u!)4zz4!1dWMvgW4Yue}YO0 zkUK$TG}t#_bs$r~`GP^!6x7awly%TV0Umg$K*U=GIQ;oRt#U?C8y=KzK`BjC8Iq4O zAZ?5fkc<Sf2vlG~CWAov86_`)^9(5Wz-v}`oWe^sP%jAVRuBPpJcxlEgOU{}p@8!W zGbF!)@;AsHkl(?*SdgnB<KUo{71U->`vcsxftKkI^TBZsCcvo*8W!Nd0Hs9+us!PV zu}ElX2=W6MW7q<5KG;Br1t1b62oeHeP^%kM4MOt>*nDONeQ;_Oh175u{s5PXpqVVN zj46l&)l|?F1NRRoqeE(0Pz*sW15+SJfH2r!Oz<&HW>A?6N>!kqCTJW66pElU2}&QJ zHXW#>2k8T~rodeWkip<g2a5-$2$l_yvYQba%gln{(KbkpVX6#C+t6$QE_0X<fQ<a( z0LmYbln%)wptQq)Y#t=nQNcWJQ0oCUD+ThOBqORDK@kM1%Rv4`a$`jWx+@WG<OkK3 z5HrE<1m_a$=3+G&(`?WQ7a;#zfXDB}LFF*022d77Hx<;bBqBh;=@%TH@}O}Vh>yXk z7VK6?c?g=Dfsc!V;suPsVStDquvi5s0f4d|)Ldo{22qf?#-M&8v^NY&qu^0ZP)D<( z;sdC<1`oA@oeMJNuK}nm{4W5mYe4&M(b{y7o<AssgG(n!iw>Ny!4s?C3LiYO4k-f> z@xcx$uRv`%W6<0J*hQdmMX))bvI^80_`3p@`XDX>wSF<o0LLe}W^jY@6eL}M+ya_K zMvR6-)7b;itP^NV9O4*IQb0HdRMLaSu~;DKKp2q@7)=o-LDB%4L4O>OjA3A60ChAT zfb%EB6mW?v3~sH0#vUQHH>dyy1u(=de;mL;3>xjm&<_b0GW25!6HreQ;s$7#fJcTZ zDj==^hX|;u1-XM6>JKd8fiMLU9w>$&c?Fc_K&??&p9#`W1J#(|*#uEzL6BvjPyi(q zP%Rt*?RkBu01xCK(g;7Or4ET-aCm_8k|ZOzjRtCyfm)rQl28!hFHo9?Vwl%Jo`YBi z8Jhx)b0f?Lw}WA2sWAFz1gL)mpVa*~17alHqo5`wq#X<@N8sf$*qx#jnuSq@GNQQ+ z;$Kj1gN7s2bs+CT(-gY<u$H0N&4rbrAR6XxP`IM`9cLMeZYoN80Tp*xgA$x>AnOy9 z!TnosYa5hql%eGoxP%7hD^Qt*+}{Pc4hM#`$slvb;IV2@Yad*8g4$W26a*f@2e}c{ z$$?h7pfm%H`wyUYGip4e<Znpo293FZVgoaOgIkz?4M2l6$mtGj22xr9&8~pv*^ts4 zB9O3}fhC_K+=7}uASOZ529jB*@dtJhID|#P@dtAeXaxz#YoLS%F$k0lkc<KM>0o(4 z8dQQp{RJ9D1?N~uKMK@Z1Q~=BPcZX9p$!oR4JIP$JkT62WUNmVG=>1LJ3%!Sd|3f_ z@(wyC4;gO|7X;1WgGL4*JvmSs0@Zb(iW1a(1~>OBDnQe7AYVZo1*)B(?L?*s<^v4u z44`G|pwb1@gTWZ32WtXPM1vAEB&Wa{SfI27@eV9(5avO`2Gm~$^-IC-1h?KuHjkS@ z7TjBcjYo=Nnh8w>h%t7MQz4E7wJ(sIiR4ax21S^;V7G!}5SQ5%pcXbXHo&8XFw@b^ zho&{~xHl|h!M#IFlfljc4c5;9ITvTh!sCj?f&uJi^!^hTlR>!)6fs!i4J7f`0Bk#m z3GR1-+6GJ!ppXWQ{ejXEsI?AD&!E;Vq-6(Ml?K)XCm>-D<$}u~P&<q%0+gyCJrDF5 z7*Lr8P6eRw1T~I9MG?5a0TzXHbU|%GXgPvNQ=qmFxE%&wj|WaypmqmjEgq<nMG8$w z1c9c_!7c=ijA57uj(1R-j8gM3(<sI)5ZD#qW-EpxL1_pS29WHEzHShbJ0WQZ!&GoA zg4)y=hNAc!l!hSlBB1;(2r9c^VF~KFgT~z%b-?3@pspS~=)i`8>I5{suy7+uFE^-V z4{76o>;|=AVfrESIiRT)$TSitHT^Y!WVs4ZctGrjtQm#49vU7n6Bw6(T>k)@>%nOf zl)m9(k%FLl7E%L%(jq8jBb85}dH^&A3W*JHW(T<h<R)+@6~q9AC#bUuDnI!_DIMZ3 zaJYcW5qR$k-eLo#ZP-d2kP)C92%5A2O%{QS0w;Koc_g*f5K#qgrGg6*q%sa_7T8?` z+iC>NBBQMaF%g`+!QO`ybV%+ayR8N@8N+PU@)xPU3$E)yW6wyVzu=Ayq-Oj#17aj7 z*&zibxZeb6tAW<=a)WaYc*KeoGU^O!H8Fyg$$~R5WV{MmuQNq}dTrqKJf`q=7%1IB z$}I5eG*GDk!eEzxlNV@F09ty0q<C<9kP$i}2wi^+F5h8!1~k0^N>QL`Fr+YM2hE^^ z>kQCZJks@J=3BTKV3&h32Ph07;RlKk&<Yt)nFMMJqQ(crC!jC@)#4~EFHq%;)hFP* z1{xOy*8|`fMM}?5GeC6#D2c(^rJ%|H;vn$kGN`5jxe;8~fz1NXw}Z!6K`YoG<tk)u z6{Z$6e*B^0!ygAw8i48nr2()DAf*R5O(DAgRCa)z0kR9H6JT)z?u&z4ql#EYV?fRT zRgT~+4QfB4j~ao)1zN5#gUVLW+D9yHfd}AqgCOIet^%iYQ1JsQhCmox^neM_xEna1 zL(>a5&0&m{JpiY5aFT@@3vwUGB;0O1zyPizk<G>CR<Oz7R1Nkwxb%g@BPd~kYderV zka&cQ_lSe$NR^@eM(COgP@ITDXKkTQ1UnbHOaWpRXdDUD7ej8ZAejT2$p*JrLA`Kr z!yRD~r~~x@+$IFO21Fq8EO=}Lxqkp2Ifl(Ig4*ZM+zak^!IBFk2>&>M#|c0Nf<`G2 zaSW=rNj4J_a-fy~*!Q611j>`pb`Z-325!W7Bdksa<t<dVLUTE^R0JD>JfZ>_#sv?b zA;N$e6yMN28IaZ_sJ<u8Y>2BtaRjQ<!A)6E>kS?j;Cutkh0rl(=-e$lY%xL>XSjme z6eO4havP!WL{8&4+y{zZ3^#%Vak>*4p5&Mfax=(myzT~v0VsYz>A)D$RsgNLG*yI- zwL?l)P@x7%sgPcF1ZX`ZVyp=?7sU*zZ<Ixu!Sf`b*;Y^w73>Poyuk-hAL8!{W)INX z4sb397d>Di0vxyC8A0$`Zp3&ixD-Pk;Y1pigr;Y3p9^$bswr46dYc|<0=P{Jwidhq z1{5gZ1P)1J@Uj_{H_*ZaeWVmr0D!jrfEOYo1qvufgMtKR7C0}+f?5uU@MVP5IN<UH z)Z2%wuYj6}=0sTB!JP^9FCwf!c?)S27qp!KM^M4t1WNbdumbBv4=acXXzoA>D$qCt zr2GZ1VF$IS5iS6=c0eU2%n^`TA4nPWX9swY9jYJJPXy~na|Nv21+{4^U}Z13p9roc zAO#V6d4?~}z(onDN>M-_wFf6;GUE(kB3fACj5AQWLLbin)m<p7(7`bc3NCOKc>t=Y zkkc2asRRmMPz?c{cY~!dNIwLWhe7cQ+AoB>G7xSq$V5;c0A*)z#sn`*1`Vh}hK&$q zE!4ee<qmjN9cYy@w8Vw92taWIYHWZ^C(^y><|6uG;1(UsT#$*xxfe8^4^7wLv1ycW z1*b#QFa;GO*uoPje}K$Ea~Eiv47!`Z4Nd3*9dMr$)UIIm0QC;V!Q;B1QV_iI1kwuu z&-#L51mPe^x&|jDX#9fn3L=i7BTC?M1YAaAxC>+iy30U|-f_jPIBX0Il4hX&R`4zo zklR3`Qs9h+h)+<<9=gc^QQw2x65t(<h_z3WjG$3T&?vMrcy%enJWxII0eMOW)Y?NV zHU-ZQf#(<b5%a5H|APC~jG(>~s5d2u*!>5}yP&2m%!%L(4Gll|+SLOLSo`T9^FZs0 zjYT125TG_4Vh{nEi@_060m>7Qxi3%}gV#~Wvqhjn3LIMC_7Q0KI0DfwLX2U6*U*CM zD8%RmDBHm71$99|NfK1FLFU`Q;Q<~8N6jzDc7q}w6t>7Af#d=v=(<`^I|7_PU||Xx zPeZr^Yzk;O7_2J>ZiYbv030`r50L8}w0S#ln+6iOD0YJq3TWL9GiVJJc+D87%mdBw ziW-Z8!W<H6pcO&?6hJ!~;rm@!K<f&?YeztRFK+03EVu^=G6%e-3$iN`l)NBG0aBuY z7UP5VC7{m_fx{8KoK%Lo1!M?3&4XhRlm<Y4fw~Bh7fCP&;vi@?1g}m4)x*&8K^)Wu z2aUji*6)BuT;b~_OhJ32!SN&tidm3LK=A`|BREDtz6A+>__G5f22L-G3^xA-!0XB7 zLH#1IS>Tl(5OYAgFhJv7pm8r{(AsT~egp;^1LlGjQ-gVswOXLHp^)(mh;DG39$cz{ z#)cuY^I&~o0<00j0?l*4+AE;BLvY%MxCPXng0AEM)kDzG28T6ljsl!#!2K!k$N@|r zJiQ{uEFqa6tQow-8`{2L_5iI|hm1pm*3g2=OHi==Q-D;jpkxkiks!hivgQ`lX9L%A zppXKMje>F~I2OQb$YE<<K>crU*@ti+xX+B_H&Al=rvUXGq-=t0vx1dPVEwrBB*>2- zGe8c2_!4Re%%31P!1hUiR$78XNe~=Hpq(k86~B<U0C%VUDO5oAq=3T~vfctR<`2~e z-lYa^dqGxWLn0GIf=X(TY2c<hm;j9zu!G8JXt^ZH3|==2PRXDdN6@Sitp5OZLj|O8 z0Qmq^LZfW|0_6#?UqF3bM0kqA#~+|3LE;aRr$ECP;PxRjok8=IFvv~dJSB?i7EnzL zO0h7<fbtWh1B>JokY7QW36$3b!0V7XL46uf-3_YE6b0d?K%4?n52_WxEB`?pa9sf| z^T2T{kJ|PpB8*_6L@1oVV{_232d6vqumU9<&^~WSXn_iIxI;np8N`jC`V2Y_1s>6Y zv`;|2Kk!Z>uqS7LOoGgjgEASoUW1gZaC5-@FCxtWxd&9wK?fBeoi9W=fiVUJYJ*{{ z-33P;sJ;U=4Uw}3lIx&mg4@T4@e^=Z5jGR#N>EBcb18I86I?EVN3fwW06B*NG|CF9 z-B3aiJhY2k(LsU|WH=~}L9Me2P*)n%qz8>R!t(+7=EEEhigZw62kSzC+ooXiJ;1pH zwE6(tssi=FAmf^_K09a#6gm|GaVaP%fLL&+g2vB4W<ll-!F2<;wgI&!!K-3Htu{e$ z9ux(c2F@KI6CfDM1+|+XNdQ&`fWsC%^93%I;AH@a2c98EDhohu2T%n9Ndb@{Na#o* ztR4ZUbM*0JX3)GnY*rb*rVv!dBM%cox_IDO8faPq=V`S5JS^X#yAPCWKwbs=PyymX zP=rHsAxH|{jTmVkhdaTkAMR36EF)qEo+rWKh$w%+eFo%J_@F)<Xr>pe72J9QxeVlL zP`-k=7t}?9h5|?sG#`ivPeh&qnGM?a1{&D_jh%{udvG9)sOG|Vqk}vT!64({^$9m( zEfgpfLHe=S+zd%PAeVz$&#+Dpa?uG+_8=?J-Hm<R6XbGGt`!BX?^lGL27qQh)b*g? z1{n;&kTw#u?vaJIyFe=`Kqf-M0@}U=w<bVgg6d>Y&l$AX9c&!fv7nA0v<(9rBZ0K# zz^NZ%COCwl>+O)t105XzG7xMSn7|BQXo-bf4q~_!<a0>5g=F{);=>os4(w?IoQjdm z#}&TFc@JF2fN~lreHe=h3xZa?L)W{59Q=312S~~V<up(+_}2i|)?z-uzzpj{K>98q zGax-cCK<3}Kq1C-;ll?=q5lEYJ4dO{ar8^k*Cc`R5U6BAgcry)NIR|Jd6XGf-xO`0 z4P1SI5(mgjAXkDCA@W=sEQ~<)IlO<0a2IG-FepwzMu5TxRFA`#H$nA+>u_)zT@hu5 z4irP+g&82bktZ6V`oLu(+SnVoeg)0MfYy7#MsmQ0fNTeO0L2B!;epYvL7Ml3m;`N$ zf}8^~3E4?VbqF}F!08ZF4}sRfi7JE2DR{_&!ULoqRO*An1$>eLNCY&T0}maz+rVpZ zVD%D`nMm#f<!+DxXf8yHEYvmxQYi|pg)!Hrfr>j&wFY)3c(pso7PKHlYdgSP3ffTs z>d}DOCJ4WSW-CFaf;KVC0QH1G27)k3Xd?U$8Vf|5(*d3F0_rP(QW-cW2`a-^Jb+As zxA(z`AH+l2^bbut;I^40>NpfAJYnV;!%AY1dqLA<7a%4>TnQS=1<!cnbtkBd0GAk` zkppn4XAE^GC;%YlAyOr%YXT7ibt^#W2|WJ`8aqK7;{vyrp=lgc-$6_Rr7BP~fm$W7 z6bedDNSifb@ej(Y&>j?|?S;*yplAoBP>7qLUCBQVXwC$=6Vw(%uk%534WPOMgh6fu z^?yJZ<Vx^pILKJgXbFgh1Oj*(3RW6|x{ENg!EGp{)QT`0VKU5IP*)MU;29DKpa}$! z9Uu&@?!g3P&IfEJsIG_fV6nIr6qq2L$S%e0PSE%*xI98O6UB|7P6KHB0c0~Q$c4Dv z2TJ$KsC_tuzd>~mXjB0dqR_e+RP=+IBcQQAaMVF#43-C=QHd6=;5G@ktOBJ`&{|CJ zicr*$0EaEue2mZq6)>RifnZS3qMSPc+S3ghhXoDrLp%#<yMy-%NrLwWf=dQa_=9%s zK-aLF8ViC{gW9j4+6=VZ1!Msz@Stu1g&}zV1#~SuXfGPLtO2)Fz;l4GmGB^4Al)E0 zfONz6k%Q(9MWJp0<xWAcE0FYpH(7$Tf}3=p>30<Ups_{po@a3X9Mn1jovH*f3H96< zNLL>eFQ8*AAnSBMcEj|7b{Rq1DIhZlt>1^}2iXleR|T~H5;S52J}Cl}NI?e&FnfS2 zJn(u2P~L=%w;;Jw6f|!JN|~Uf0xLB@`4V=p+Xv7gg`n{Wgni(1vq0q;IGjM^;UHIo z+ziGLlRki3lpqtq0S9dof+%QvwFA6&kQcnq4?fchT5S$0Q$c65fR^@vM=e2pM)24- z<lG3zcoAqU5!82p%w2-*0G&4kIR_1r{y+^+Q22oM3V}uoLA&NaV?v<x0iwZkAfPre z$X196u>K}^su(6z0UfgfpD6)3dw~_yz6P)50##q&;XI~@3Q)L%%WOng!QvJ%*9vAq z!U&Qg!6c|O{Qx>Q2Ryci&<|Nl4_YMzNtIyTh<Pt)&Hx?#0Lqe}ya{nNq(cie2X-C_ zC{KXKPC@YvDm6fBR6v`iL1Q$a_8CMggoNZz@SY~H8$fXl(hKejfkF~gtAj@Pz#9-5 zL8oCsG=fM_MFc8fKz)b~uo;kfTF|H*D2;&k`+-KtAm;&sv>;WjkO~#*cG%b=Xf_8_ zCxhm(KyeSsfgpV#tH7bD06jnoVW%j#F9cq13Gy+hosBqs4;)#rj1AgI4O(#t@(VQW zfa_fFelXDZEoc=n$oHUf$r!X(8Je@<X%*xYkTXCBw7}0b1ew7U0a_geH3OXTpk{!| zSjZ|o@Dx7C08sY<j6sP2;eUBh*#s&Tjlpx&V0VB+5fq1@*>1?r$r<3%37q4htv^uM zK+OeFACTe{QQm=jUdEtvYTzXnWH$ylUBXfnXkj3zR|v|B;Cux+y9AUfK(huAKY>CS z6bNwrpzs8H2$X$60SitG;PeFY1-P{dX~%+76m(ZS$gQxQcCZWyF2kVVDGn|(Ksg$o zi=ie7f-5)B34{<cKwMCf13G<<5ptFwxR1dA8jVLfD*`&V1eODzWCTiKkaK>(=Sd*W zlmPo3zUK-)+5{?zKo~T61+pG`q5){s98`Ef&oP9VkMrDFX3%^SxK0F}V+e0SfLsIK z5eynv2geeqWCsN<xY&R+p8pzvyLX^`1UkqTl!RdICUE}<+{OWw+TdISAJYQG8E6d% zEI)y0P^t2722#5LG%N$Eh(WbHI2yqOXuU8o>*^qV5^$P?q$v;yF%D!PD8)dc3o_rx z2-@EVp5H=lk3jm%ko|QaH$svXsD6Po8o|vQP;h_-F^QXRM4sC~I0sbifJ#qj{{UOq zf%}%Au^Mm<pa|Ik0B*2?+yn9*hMORx)lf6ReGatvAZ#YG6Ol%)p=N^J2~IJHI0l;u z?H}L{Gt7P_*nQ}8Lg4lpd<Pb|TmbnN;VPuT0BG3-FC##;C3w6D(q09{IB4DkJP!ss zB@&cEK!p`JS%C>iyByqu1g%fSwj&o@V?oY=1-ExWVGbF8g%pI4rXeg3fb*Urs2vTS z`v#Z9pd1S-y}&fML<O}5K;}Yr13*$Ghy?i()IbM062u2%)IJ~Dm@GK#(bs=~w1LVa z(0~TW%^)X(#6W|bph<T``UST&!0~{^Tu`|W2}w}yg~u$&L{Q=b^;JL)2Ad2z-3q=g z1Uaps^(nyfLZIEA;4%%IhCn?5=tws>af8eQF%Ygsp9ls=9b~Q_oF38b0FCxT#=Ie= zH>eE(IwNNZ#AqZEVFx0}L9@c(FbCBGVDrK833e;ER0M~)DI{c&YI{)X0=XO1@&-jL zDA~fy|9t|K-$8bOLJ!o=1<kvG+AN^{Hm3RD&;f-yD9u1y6WGj03V%?9L#zj-MUdOU z@rpgKg8c!HQE+9BWE7|r0UcC^h)0YyIbeSag6d;<UIjS-HO1jhZy4rbNo$}D#Gw2S zt|p*Err-evcsfJ0!K4v&HMqtHx4A(*K`;+`jv~041Vuh59zacRkm;ZT9g?2FbuM^Z z0bFLn>MzL14|MDY)DnQFRgh7j(H`(P6=b9WX@V7G9N1t`{RQ2h05cPEq5y2wDJULb zyJpbmdO`guu$vJ16Eyz}8fgdjW<aSBJO>4H6<SFJ@*b#K#adQ@=AczUWfeGWgU-9d zS6+eAAE@C0^){G-BpNJ57N`vfohy)s#|@}_2DbyjsT$-5aC;28$_bWsKxGTaa8N9P zf(FC}<t0#W7ThibnadObn%4u(QG><~z`X@fO$nXV0F|#G?V#yE@XA6^Sp`a;AhX~Y z$&HY6V4!2{Aoqa!NEIKzyGsA;0OeQ2+6j2O5VSH06r-TL0J2e8bPMQw5~%qg`$22* zz+>z1HZ3?`dVuyIg4*h!)Ct0{)DIe80ht7^>p}hog#pwMcnSu!I3ZyHI%63e7TC?h zVj5($AL>dd1upNPX$_-I1fBN-ua^Tk2BZf>W3=61E9Ma82KISANcj(5F9$LQWFE*v zcvj58{mSeCI+GAfznIyh;?EAm*}nff!0lV;JQZl|4(y~mP=63S_XX}9LJ}~<c2EKY zy9IpW9AqR2Bm<5-L|Fqqdjr%X0i`@p{sYAxDCR&Xf`D2lATvOHbPx^g@<EIN9TfyM z2i9MMnFA^7L8gFCkOD<JsOJdIau1-ILB_%7CBf^{z;i^PHR#at1l0C{&hdi66<lya z+SiZ<3&gW1XU2frE8sSmGH5OY*6#<O;0{iOSlZK|G79V%P>zJfHE4_%>K;h_2p&Cv ztgisABn8(opsWq55<vs{Sk^Cq#xsdB1LP6N)D<X)g9c+jbsjkVg4_YF^Ay2zW{{E% z*7`CQ1hu+AITPZ3h+9C-AqfBP3XnO_JPMsd1GVEor9Y@I2TnKW>qWp-6S!i61~@dm zRQ&G%ujzxXwSuHU(CPvB2r9%<(BZ(Kkc6gjW)E1}0F?|NmxE##G=2uk%OIbF#!=wq zdBq1%LID?YpkfMCrh(S6LH5prQWwYsut}hnCwMggn*Cr`zzPM3epvq*l){xkttsfa zq2N&fq?1FzgLdHLiKxFI<8Gk1fbT^GjVpm$Vi5D-X&vdT2CySQ;Rj}*5a8ihn7bhJ z#9)7cSE7J?26h#=6$u%o0~Pe3(Nh#tKqCvF>KM{z0F8%&&c6hYK7;zbpfa540=RJo z?(%_BD(I>Ln7z<3O-P*!&K=+pBJgY*D6fGUec*Hj>U4lcXF%g*(6K3KSU|^&kw!&9 zZBkGc2W59~%?dFIbUy>kUU=UITr-188c>M?jX|hBNUINgBLp}NLC2^e<ENnh6htqG zgq*wp9>E2rdQkj<>;wfo*zaI(L&FWShz=ZH;Ib4lo&pX5&?ph8tOBih0qvs#nFT5@ zK{~-kfGa+5ngVlC(gf(7OmI2`=?0}vkV(jAU4t9X3XoI9z-P#S-2hIL;PE7I>_bu| zxHVx6JuM$(JGeXmEgA%KKvDSzcAh`z{7KNcOQxXPIY48SptV+zwi9S%7F05T3p-dP z3yw}u7abZGpfm#=X9cA?kgLJ{Md%0{$U;zrfO00-=a4)NY3GCV!p{r?oe_*~0we@M zgM-jX%0D|m27v`YO>vML{zJyH!Q+j}pi?ox;{srJfI>r*=>qs%GjQ00TmwEE8&)BK z3I|Bt0}2PoenQaP17utbtRIvwjX|b>6BMY)0ZJR-Wm~Xz4RZPdg(YY%0X*^vN)h1R zfT*z`ct{?U96+ru(1bpy6Ap3^C|`gMcZS3#)E$sA0aPwR!vuWJ9%Mcg(ntqug%F?s zhPnZsCP8Tl)Gi0LPe5fHI8TB{)xc|Vz@7p1z91%mLKNx-P#Xc#js&f+hS(13Yk^v) zpj-wD4^V9k(g?B`)Lnq(6mZCZB)}MvzG3+ll)gZ94~SL-uMGg{L<9<08z{OEWfJII zVo(_Yixtp{GH`nqls3R20lB9ElEgs+Z;)_=>Vt%%A}DV|R_1|vJ>XIWWK9LA`~#&Z z(8K{$AGAyZs|J;SkbDJ>MWnOG!5J8&vjSA6egF-np}HBGCqTIgyblN3t_Kx_piqP8 z2O9wLJ}6f}(jmy*(%`XkP{{=Dk%C*(@LnZ!2PrtHpr(M?AOaNrAk#7P1x7m$HAO!F z9Tfx8362lsz3iYo3MyYfB^+qP0F*OO;sBhIK-mIR34_xXD61jHoMCYfDdEApbU`gL z=omiO2S{Bph}9tXgX#wiz2G_qGExo7K3K=F!0`xfFF?v>P%Z}L1ZXP`l7>O?03POs z+6_Ny2V8F>*$<9Kq%;8TLqq0nK=Tja`T-R5P>ho1!FoYC2!uiT29$>&t_QVNASF6f zBPa#H^Dtz+J;YY%t{SlG5&A(X9~1(hnMQ;O&^8vRlMPBoQ2#^5=b<f6a9IF%19)vO zY$67f5J4#jb{;9?1Eve0T^o@8B(yys3hD=fY7j_;01iH|wV*JAmc`(56gulH3agz# z{ztVJl-5Au2TC}gsRmHh0tr7*TN4uZpfU)<1dt|B!hw{eSkAbI=mm!#sH}kJ9dN$` z)S&^H0E%glcR`gIIL(09dn1P#sPzPiZ*Z>&q!>KE01j}FYoXx)jyJHI!QsUUnkfMH z<3Mo-nE?mI7c`GSf(-qvWLW(V>ji@Aa8UgZ)(O@Fs)xbbr69!(w7h_{X+hxxE+?S+ zK%oz6i-FQP=nztfhe5kcA&aQ~8h}j$)qlwIFW|Lf;B``<90N`(AYX$$42q_|2B4+9 zpjj1=pFx@Y0TcMlTF@*8C}+X!1eGP=(hU^4AhqB$32`fA4jEJrfOaZCYie+90JaB8 zfWj9%Yy;{e!OK=~-vJy);8YE&6+o^8#}8;s3glm?0bmN;UIMc~XYhi~2nU}H&A<w( zPeFEqTAHAUfsJQC#(oiVkDw7v&>Cq_%L1Ghz&3z_6I6?UBL{Tr4@eY#rZ*_>34+gf z1?{MS%)5eH^`JI5*fF5h2#_tZ@cCA7`T^fv0Zu>Qauzg-2yO$2f@(Xc&I(YA98vZ_ z^Bu@ea2p6z&VttyK}u;*Jq9jo!R0jA2#{OB-6L?}0o4y`cR<$zfO=NSrl2|#RJVgl zD$w?k2cY5x)P{t{1t_mGML_o0fNB?zE>N8gN+#fP8LSCx58RK?G98lE!J}NDJ|wuc zi`0LD*a{tP0587=xe=ikcCHGzE&-2DLCO_yc>*rcVAC3)qysLT!1)`gP5`xrK;v$p z*<{ch4JfohGfAMFwE`45keC9ceTb>x)+RU|gX0Ntemp3zgX%`G39!}k$aOS0$U$L| z0Wt~11JR)T0V(UC{sP~J06ISooF~BbKFD@xtT9D^G=RMV3k%qK44ir(m+ZjxK;sLx z766oUz_vis7ieW2I5mUkFd??V>m;zB!S_RmgW8plSp-n&C<t~RIIBXJ=t0I&z~K!p zPa*eZfpvgtAXu3IPJ@uMFrnobq#OZ{VS#fr=&mnt{4qk#*a7t=VSa~{@8I$VJSPBc zXG2bLfmCCl+y!zMSR1N7NWB1RgM!K+P{{>y4JcMXT0t=cu8sb#fW<ec4FsvvKy5Nm zY=cU7P|X4kL6B~UE?BUEL_kFZIMtx~4^-}ebb;@B0~e;Cz5qCrLDB;#o*?BUbnFst zAE+G(8tnk53s6M_awf=Pn02uD0*|pk>TZx3pxg;+$%0xBV7(wc;PpCS|3doXU_FpD z1u7dsEf#Re3@Rg`Q431MphNFLEoX>Z85qEM3e-LXg$#H^1FQ?$P6L$%;5Y<jX>das zwEhcZJxo71jeyH@kd2`D0QEy5G}yf$yFsl>P;CuKY+xf$%34U?0hhC&om!w$6BHz1 z-+^ofF~Bt_QaXT?L7=uFq{fBjUy$1%7!*>FGy&3%;$HAwW01NXRN5+n`V`<E6r@E5 z-V6p#JK($xZWDmq1y&2KpP~5x>?%l824pKJ$)UOtJi-NW8@NUW<#v$EK&nCU0ja(~ zmV>Ul0_lb3RYjye1gH*!l(wKe3rc+;8q`CFXb0_;g&GLSGEfeP0`;iCeu0_`sbj#o z4%DIp#Sy4?2pMAnm-evcB%}xh8HlVOKK2aCz2KG|$Sq(Wf=VLj+3lc#Vn_oM<Oax5 z&5#rTOXINi1gLLl3R#~7HVss7L1ukGnGsY+gM%6rhbVV!g7kq#f#4+*xb=_$?xllt zR)EgQ2U!m48GynS6qoSzP@uF1X%&D&6O!7%ok@^wA3$SOV22{|KDgWkrEAd05GbvK zN<&DfK~fpmQc#qFQam(^fyP~-_Cm*9!EH07nhQJ{4eoV;5<1)@P;&vcQwvcR!}=ql zh_VlSqBVFW6ev}J^n=PRkiS7Q_{jI5!TTf-+re!iSnnEE|AO>^)*iy^UyuN(1qg8i zv>b($zmQc=pb!Dg5J2-RD5rx16C4_#<|^o_Sy1GG*C2ty0#Z+aQV*!D2`aroK@KWq zK0wcx0VNI4S|0G&81ncMsEh*D*WmgET#|yr8kDd;fbYx$?FWXezXUlPlIKC=)l3&a z=ea@eRRYI2NH@4VHU*8IgH*#X=yV$x8+n|LDFSp?2PnK@cP=tMfaQ0jG9OafgYpE_ zm5|YOP=rE?Kgd7_=wMe+bq&g1u=_s1{)MdP1?Nv_Dg&3rkWv|(9l)UpIVTq6PLK)U zfh=gd4iv_avn#;y1YTVVHwhB!q9Ah+IR%taK^W{lNPB|`WEj{zyr3CL#9AWIct08s z5(n_HC{P~_<g<S>Ktrt%7U<pz==~Ys`$WJay`aN{K-Mrafbaf++{X+(n-4VX%g6w} z(;9q-Irv5(Mo2>fq=yNj2Ye3(^e!31-L|0hJqn<kghA!j2hjC4;Cp6aW`NQs$P7rD z2CZ=go#6`|^8=NMNNEyOw1ZFn1lNS1m2e<4K?xM350W22r4VF&DL7w(?gs=#5mJ5x znF8X1k7_|Gi&#PD<%3dG1>*zo^(x>Eg$Q?n&4<+fkd`T^ZUwJb1Vuc^l8O(Ys03&G z8K9N62)&?n%}D3Ng9c1NeGaVmu7c}tP-+2{2cVn@Dv3cU4rCXkJOLHx5KBRQW-trV z?tp~_q@4(=i$Nonkd_>%7<uqd0Xl(-NK=%Qh2TCb<n9tsJb_#bDn~$(3NCX%84a|G z4LP1b_tAlNw}V^<%UO`IFvyxmh<89K7!;CVUIqBbW~8_RwKu@?MWA*AEKWcp>=5M@ zpqPM6Jb>y?)UW~R1=$bQ4-QvQTL9YX0O^M~8dN5L)@(z{EoeT5=@kXfD?sWkl)e^7 zFUkT9=>8P&94I^JJ{s`pf{>H7Aorky&(i`YI8d<*#wd2e+hgE5P7qY%!NM1ml)+=? zu-XYZoI!W7gYyyORy~jne+|IPKN0B?-1Y*cHIz0#DBQvQQ&4EY=Qcoj3@L4b`kA0{ zTu@C93Pn(w2B`;S9uN)1paKy!{K2bIAt?pt{i>k62I?DuTQVS<KrJK0&O~sp3TgqQ zON*2?Aahcno;qlh02H^N)CKClgT}2v{Zg1t2n`BENO=JEH~0oS<ZuMtNe6NZB+gM& zETZ;8N<R=|K`jVSp#U)&CI%j;LWDoEKS6$lm<MTbL9~DiaPS&jaP^1ePf$KG1>O7% zsySijErM!((3m24)EHzvsDJ>qi$S$NsAU4WW*lliybS_swLxl1kQ<;O1#>s3&HzOs zgbxiOaQJ}Qu*~4S@6hu<!C?a$Hv`q@pmYJ6&jz<AK&}880xlFlEd+??K7i(>;buYZ zZUgtRKy6TvTR=58But?7H@L2XXhx7Alc79NcLphp!0XLH^(`n3L0U&3w}C<o)IJ8a z20(cU*7XA!0&1axXb=X~!zg_tP<%n=b0IwsXblcZ`=Ih1QaXclLNFvBL&FJrjts;l zpt2n_n*l4^L7@d{t%E%S%Y9%D#4H$zFq2OEH$Y{422%tBDBptm+n_!yxTgb3R-hqI zNO&`X#xB8W666<1n1g)@%6%whB(!A%_Rt4VqYauKA^mia9)t<7)PyM8!Epl05Ksn6 zc!1Yffk(7J<52K=58M#|g(>=Y3OGJNHiAkpP|SeK8I&>tVioe(17ytsB%OlV{tyh( z3B#aR1*I!UZUGfAV9&y`0)zu4A?^p&FOWM!!LA3bGXu}OBKiVgQ$U#zVkju!q2UH? z!-MlJq+A6h8&KN<GRg=Z7((=&_(5ysz-2yo76eppgB=Dn7t%chZQudLD=6(k$_coC zL8S%AeV`E=&^}mDsRF*(9+bI34g_U(kmo?I0GR<=D}eAD$c3Qx6RaKs?T;elH;@}a z`atWdKn3F86`<k_)Uup`8V>Nf5;Q^sDt#b*LTEt<avXHN0^}}Kzk$XRK)S#)GoYFS z(W3`>28^KwWWa(75_eE@ltJMNDOE61KfDY>q+_sw5CT@jLU^FyLoO4ceO>U_E~vkS z#ciNFn;~ujha|kd1&=2|>O<%lE!c;kk`+?kL-H~xq9D-@b^*kfpq?i<+`w)DrBCQy z5pW9x<U??K1U&l)D*qt5K(!ud9~OuQ)(z^xK--$&HKvfYSK#mfo$UlR2X;0pXa)*Y zjAFY73fhhWuStX5CIzY;K(`Kn`~oVE!86L>4gxMyko_VEYVU(;1JHN}C_jL9WPl14 z&?Gc?%pIJ_K!ex@$ZHqbL8J80UKeQZ3C#82ZUbnw7ic{!(*^L!rO5Vy+fJZ52xJ$i z><9ZDG-eK-QGhg8p<xCpBEhi&ZahQtIi!4qmJcBRg6c$+-A>TN4eNA6A^~b7IIV)p za?t(h;Qk0`Z7Imlka`D{d!cPfP@baRy-uKZBiKGr3Py}>LF|K`1_ui#(3l2j%?G$` z4vH60ZUl#uAUNlM!vx$!W{LnW&I6_T3eXV~sC`I~8KCkIwG{}ig&;;iYXopef!a!- zStn4L08QtRdJOCePz?lG`wOZgV7uMH8bLV~GJpVS()?Y4nof|;i^0Dq0q&*`(49)4 zwUkhkL0J;g{eg{Nf%6gS`f*cGSb=(8AUA@-3>Nhu-AL=lLBgOTgF)U0-SUp?UW|3L z;5C-udKA=7g49Hyq6(6rK$#Vi+Ck|ZG>rzil>pLS0+or7ej#`+6>17-1`^V~V)g)C zqzRcOL<)Cu`~I+UA5=Dhat0_*LhB_^o&eQ_8Q`P>?$IIg6Qus9!I%Nm|AL@80hGtU zBhQf1TNG5=f>J*u)|n6doACj(z5m|~c)1N8>jCE(kUntg69kp9kYWv_AI%@q;Ia&q z;=#RYP$>gRx8N2DTHgfJZi8UZE(cJH`JVz(96;&|$cigSJr5y~*4)5S0H|{fPD;>Z z2F{uAu|-II0iHXD<Rnl_5H#0=<Q|L!iKr_u_VI(yZ$dK()XIW{Bh(3qB#B`(k{?0& z8#D$04lU5!H#o#V^$GD~5TG#@uxmiG5unluvY7-_Fo9AxsHg=MO5nJJjT?Z~gYzh; zUy2kCpm76e#)MQHh%^ea2i%Vb_0g5#ZCX$aLdS9tYGL^R6l2)hj}U7>dv8ExJVGBW z#tI;L3p9&`da67$UqaFlyeAK;=uq4PO8KCigL8}k=2vLh0vam>r3O&W0+mfDB@(!P zf#ylrdN6Qq1l29z_yFe}P;LPw8)#aBQlO9qnGBj%1hGL)8l<#DlkowlTe(5=PN4D} zgdyt$LF-Q{J}`l|I%1TcpfUqont{dvkmh_rcNv14hG^ptQ1u|wK^T-gK(z(9aE27n zpd<|GJAmqD@H%slN*GpztbBt+Bh*z8jVS3K5?7#hCuG(DbgMFG=?tjw0Tn`^A`Q|v zgXVwmco}q736wuUYcPz#y-&~rB}l}8#<HNA!9%r3aROd<4a(Etz8Po@G&I$LQU}OV zMo{gCNN<p~8Eh^FlzTwE8%Qe^tP|9(g_d@ZK!VK2fzuUIn*mm)z+2m({b8VWM-Uf- zNXCp0;QR+}Ykfd-hajjN2e17Awe7(DGf0jHjVyp85R?ERLzCcc9n8;=IebO14?sBq zG{OO*L1#KZ>Sb_?1(J+G&Va@%)a{VG3epX#Jwa_!P)!PLmx8PW`5!cZ4DR?tYAbj- z4?B|q7Tz!#%h)@}W)S`hoeKjU2nJef10Q>b+*|zsGN=bhcOZF0J&F{vphN?XPmqZf zOcy}YT^~R*4bXUnh6%_=;CUD53K#H6576zNAnl;GHh9VxR3svWC#X&Z&1i!$EDeIv zA*hr9mG@w4p#*r80F)R(i3_P7hphdDwD>_~4QONl)RP67foKau%0kHG0n|V^1#Y*4 z$MqrM3>vLKZ|%VqgBZ{iyr3-&AZ?(C2H#JJS)ZDM%>vbd@R}KvKv3#a$W#kxMJ_mu zz=<DfG*VcB>QhiSfyV|wVFgM9;IKlePZ1^~kNZH!FTiypYF!E{$3g8LL|Fn215iE# zs|T0eXmu&51cwF!JRO13Im{kVt%6#YBF%Lo(hj7|0_6#`{D71fKy6CUj4ZqyfQ*xY zd;!9s6bhQF`KJKN@}TenmFo~cz{gC%B@HNTKyrEow3vZ($Uz|nE*KH+g5*`g^(rVB zzzGXvFVz2_F+A`ZFVI*zX#XK7zQF^_h`a_a<H3D3@JIuw6alM(tOZj5t>=Pz8&nY@ z^ugBjK<icT=m0ov!Pjp=avLa(f{HSb`(Oqkhaadu1*J^T8VgX&g4m#%1JpVLm&l+# zJ9_>D8H$M^c?TNiIO|eS`39b~0A&?KI78}2(EJ%FhCpiuLGEYF0L_$s0594^R}1zz zsAdG`5^z9)j<Zn!84fiYG&c<y3j?hS0;z>y)Or-+BJhCa3Y4@DNt2+^0M(<Q&;XBl zfJz^T??G))SU(Xw&x%=xnu2D`Astqb6G7u|ppGp33@K1~i&}qz_WOZrK~S_o?E#hG z2>+8(e}dv2VhA+-K+`6y41uH>P|qFYPf)BQwUa@GHN+KQW5C4-Qu-DItrG?1Z&*1C zqCvS3UVnnJI@tCP@V+njEH?1@44^&}=p27gs{u4Z1FJE>c7X~bq-q0FFv7PbgGvGD z7!|nf0^NNME(1YhRN(V&KzRu~mI<y|!6Q}pO@!wQ<^!Oaf5_Mm$V_l~0cs<FW~M-4 z1u+r4^Z@KVkf$M$3>qv2xenxCaD5B*FZgUqNGUDJ2%7bV`w`|b$igf1I0ltF;IyX* zF6+U$9Ben*_!YzvpbjD250G*XQWt_k7!)^%kp)mc9U4oZ<6xlr;pdHl_HBaQ3_6Dn zw5k(47OVqac>zB)1{|0b49t*o8=&z3J^>vRW+*zBfcmXC?zx2Qrvi;zf$v#hG*uP_ z?Wcm>Z2->Qpt1lIQs7OQkgEuw=@7K`7c~9^uAe|PEXZz9d?})jVS!Q<*q<NJ*Yra4 zgW5WvJOB=FkSU;+B&bG!Xo9dlfQRa#c7xmu$wy$@LFpM(8-r=kt|_oaNFo8XmOyb1 zHsvq4J;nqoq`++jcF<i4@G(vtH9JT*C_h3Bt@r@G2MW|u02>Jwh2&3UbCJibz$H6) zCLPiK05>qfdO-vzgh3`^wmo2R1UlOSY$iA@gGxk{b4fut4pe)gj$1*D1&yJA*F{4L z6mWYMxvoIefrxP{P-_HKoFlarLH#1wnoY=>6Igt~Z?1=004ip|1I*CXV353Zf$0M1 zTrF_EAgCV{C}0e#v?2OIaSmw{fZHn2G7H>y0QnqN#e&@cO1hx*0-nx-r5Sj>B&;72 z<)E|=3IK@xApb+ke^8nOrB}#mXmINRUiX6?4PKW4YLkElvOw!3VPyj-&%n$8g&rh5 zfctx(xj=}uP$R%e4m>muatA2MBHRI9&j8MAkUkvfY*KJ^ftHhiG=hQ+$=@LLAp1al z1W;)TIw2L3D?w=(RLg_gH_Qi^Js9-CeR1>_38;Por#f(}1T=~O3I$O6RM1!yHW&qW zIh+AY1~3{FOBJ9BA5vn1!v)$_0M8qP<|bh&9F&_uwt?rGz<Cpvp5exWN`6qI0JN?d zM8ncHD6YWcA0YEUB?H(@kRL!U7X_86aI+v;22xmnQtqD}(Bc&oe$adZKBoX=7POp$ zm;`ncXjc~~5aDiuazL$4@M=!D9}(-MA+2&y%Nepq6coXrxc>k;M;c^5yqtuF4Y;ie zstv%kD(L)1@OT7VA7}|Nq;Q6$5m;UU*P8IY1E{5myygOwOCZjMl#Adr0A5=Snv27) z7gX<pA{D$n7t;O&wQnHnBtT<YD4`BdIiU79#7Za$@-~zSF8rYB92)N6I0U70q;N-= zg))B)GX&}=P|zd70x`A-YM-EtErRA*QP%!|$L66uPe?9@?^gitegYLX;Nc9=-XQ4t z0H8KBXk{-bW(2X$GePSH@EJ(z@N;6oeqjcsNiYV58Myufr7zH$3rMRM<~0}%%Wk0A z6Hs0PVOXCWl<}c<fzK>Lw+obJ!5G6XaO(z|cwsg{S{xwWUjtD70&P72(co?Yto;r4 z6I$H_J?8{mI)l<ID9}J=gQ6Q$24I|b0?CfxE+EWYu=^2b^?<{QqB~%r?N;dAD)?jz zNN7WEF+*`JD1n2grr=J61UHgHq2@u`UyznHIOeds5>z%r{SF?af|&#g8&D8~x(P79 zgToZ;LU73i@jKGWN{}8%-9WXw2|?qO;4VLCw;;@&5H~`aCZPBO7u$$@1!)g}>r?1R z9JtpFnL~q=PT(14&=p+~AU~p?O$!^l0<B5_wP8T5Z4eFeA1GPFbc50jsIUiB$FMWY zq45Y#A&__kpRNt55kYZ@<Q8abLi`5~eMlyPo}L2Q)H?&*Sil{ppxy__R8eF%Ld*sC ze!-?f!V5fQ3#v;%r4Z;ode8tB<a|7K&}|i<mNuw%lVk+73PBhnH-d9MD8vvL=3+#z z87b4Fx*L3o8zf8w!MPp0eiYOK0H;ro-JqHk6v_zmK`fa2L75ySvqQ~?hB=b?kT3_8 z(cpd#B=nKz(7;g#N;lv>3@A~+JOU0pkgcGm5+tZ#aSP2;sP;hD-67cn9kl@a1JfRu zXCPL<NKpR}5g*_@20pJ4)Jg~U$-(s{cq|>{Sg1CToBud~?0`;RL8<^)eg(T1Y8Lp6 zdvNOrY!Y}T5Og92)Feom3hGc-Ku7Oj<BX_b3@%Y2aR|N{5yM2dd7!Bb=)q|i4uqNs zb|1>Q@z`C7Y$j6hp*j?5CNwR8eGPFZBrSmZ%J6Y@a3q5Q64WpUg(74c9qGs-v^Xcl zJh&4<{)U!W;Pw`@Eiy3u1=R3Cibq)Zp!Z#%VFU^xa0xX7C7j?L8K}9KaY?kfDB%Uq zbx?Dm@d;kdffk?8-5`*i0MNB&kRS)S7c!EI;$~1E1XN$3#VxsJqq!TBN?~~q>~8RS zE5heXfgA}c^DjUX3wWa%td9*Edm+*skb}TOB2aU{bvJ4_qNiiD`CDk5gTn70^r9zp zhk?%)2G6U5?y@AsJdpD+y8W=Y0I#dTeNPy=J3-M1Dqx{`80uC~6yS0%=qyC?&4;)h zY(D5DRHOh#4PS6fLDMPHTrkL~kd_uGXhHD}DsWIHhN0#_^AV~!&>98Of<c%AaTMqV z5$MDh3#g;Q1gq1zLAUn8YIVq6B#?F+WG)xxCQwi!71rnzyHGQs?!sp#bfg;OKB$=p z2O<r0qx8!_W8I)|0`>pF?HKT!DJYP@8#y3@k)S>=O4$h>{RE8=L1uUn;~X%1BmS;{ zoMMe!?t=PIps@y|6%mk8V8~il&^k^~KkYB_JrIzx8Z;IHYC}L*bwJhvKwA%>qyW+i zD!9O9C43JXsJ{=JGk~;4!TP}+Tu8b_=!5hz5N%kPKJW@dP+tX7zQM+6LF1<xkfnDB zJ>YgPd@lm1bcBorfo4xYt^<WCxVZ|BdC<O0P>%_rpA&RGJm@YpkbRJ`HprMTcn1(d z9VpH~H&cLP0aUMo)j|RhJh2XHnM30Y6b7I%QSg})pw>93g$L@#f=2j3b8ZhnBORb% z{|7pFAKbbFO^kqzKr#ao58$<?&^?xjx*Q%6pwbK6^#{iTN;<>c*9NVg1}*Ue`wx63 z2P-5TKqpLrq6u;4Bxo%)WZx{Pbqs2~g3};qClY9u1nd(~iULj6fmeUS%>|d2vLJIo zeF{Xo8hi%;XiNii!ZWz93u--siW%^@1IR;Q&IeE`1IHmWOh98Pp#Hre=tcpw^(IUg zKoj{hz^B`xrGL;G1xTwH)Sm&bQGqS2VSE5d79c*T7y*X|*xlfL93VG?+zxH$LQ@tj zMj`hvfI<sY0fU_kNuuxxRd7ohVkRg}fYT6Y?-<NHP*DNZ56U2*o69iW4vKS74hH8% za7h7*X%GenJH&33{0p`Rv<eQqR|mA>8q}i%Sp-U{pv5wO4Z!1b(DDVc{tm584-P9x zZx_776Qmt9Pzh~6Lsp|eri_uzg4hiitpShjfdUwm)Ike&E+Cyh0<{}D1`CQ&(8v;K zr2;6ofMOS<6J#rN{0<tnpl}9HE<wT-nkK*_c+m0&oPa?6T=0q|aHc}21@Er~$18Y+ z1}H8;u>tBIfJ!(}Sqicf6sn*dvT$#LqY^0{g32n0eV{oL@R$)eZa~fhPfLKBVvHH^ zcmcT+W;aY9xGaI3)B>^_lKMd|0F7>e^Al*B5NLNeVh<H49f8Vp(E2=3FA`!FWE>5g zS3v8uAV*T5*bT}zpnfy7O#usAaD@TZg5)=lT2TE2j(xDpAVI?H0Xmffwfuyq9q`UP zSeSy==!11b>;Z))#0U@xO*Bvn)m~$idJ0m!f^rEs7LoG?++Ik|hQuQ%CP66(+GaqM zk>C*lh=rh51E@_5u@_wSLDnL|#v?(CjvjzZe=PPv%07^tpcDhzRRUTAji`&k(GMa( z`4i-AP@I6+;ASbrPH_1GTAvBY)u31ew*o*l5GYH7Zh?f2cY%xo7qQsuN@(nZQa{Ac zpzsA-2P#jn?a_pl$Dp|q(98n34Fbw>pmGkhMhFyR&;b*W<)Hc#Tn>ZMCCD@2jKIiH z0oqp$Ig5!AYz{OGK!dp8raWj&7NJHM<V)z;V^AfG3>DBl;q2fW<UnNxXf^~?Mu9AW z*a6x*fGq!^;sZ#31?WB&B>gCE0Qnsj-XM2^%-{yw0m*StcYtyU*jFI47(k=#%pRaU z{-7JepgW&ICZVpa5(KT6gVchM5CiRqhXfR8uIT~jOeSy;{sm2NLdzY<*cy2D1hirU z<Wo@igYMOZX$3VWAahipfi;xzf6%%e&|DqJR?v76*hbLM6R2GS@&E{f%>gG8h+gRY z7I+U8O1%Y{{|0#slnX$ISArV@;6f8J^#W<jLFYSoL32>xwliqVQxINzLew*6fR7A8 z_A6u!wjyY(3uG^7B@-xpfNCM|Oe-Y+fwhCSL4ax^a6W_D2R@%f8Ke^wN}#e8ycPz! zS_PC9z*z&*Ttam(I4^_O+`!s%pgaLeyr3+d0otJr%GRjiD+ux*XqE)jx`Bl*C^BGy z3vwyQ4A2a!0kn;Pm=l4VrvxesK(Pxdg^*JPJYql$aH;@D6S!P}#TBA`1#$(*1)$a? zBu9f|36`Zn$sRI=4zB({WeLa~khv_dJ3wP*pimQp&e}t51&!W<vhg1WP@V*xp$0k2 z0<s<m?3fDB8gfY65fl%QGc=)QLe4+~wfDf|XCNPd3<lAV={ZnnflOh{_~QUE16sF( z+fd-L5|aBNBzQIjH17vWiQqtiCJ9h_gzVJ-nGLQDK`8_&z94=DpRWdX3n)imoX-bZ z_y#Jdpm7Ct3%Je%wZWnBgA_)f+7>j@UI7_Mgyvah@Ok#obPPIy2V9?lT1B8(2GumM zoxza)0wkJ1sRrz5kQczs9%wv)@*_BHg4=(PKG6kGvlEoQK)pxMsy(!P1Zf+A!XHvo zfbtTwPXP*VP(}bBkqxmB9F(B!2s&~F9uJ`Q0=Pa0<zdMFM9_L>kZYMPRDcdW2hDIo z$_&stEU+6vwH#utC`c_Rzk$26;JgY=vtV<<ZA@@W4|G!Rj1M4{;B&K~>+3;j6n6GE zXblc1J%B<FQcpw5S&(l)wt#CcaL7RjP|*ttKTsS(*06zEwa7UYlxCn|4oT+l^_Nh4 zLG!2J&;#H91uaY9wj!bqUgg2;MADB^W`puMXr>wDLI}oxo*yZG24w`8{m6cXgcqpQ z3TiEaQVb{uf;<dLVxY6q;qeWMLr|X<)P4ikG0;{iIFo`>3n)CHc^q7}gH{7V(gmc9 zg~S8sNJVJkfaX8wSz+LIFC+#)x*+XzP!kmt^PoxzR3<=XVIXdTh9f5fgE*+o3Tm-H zTC1SGZv@B>kYiP#?gF_VTqc8Bf{>gB%5$KS9Nc*aB@s|J1eDC6-A0H#p!r`&xdvKW z2J6RymSRBULFXbv>LyljYaO(P0OWU&4?yD#;Hm{oK!OT1&;arexFG?_AD}!e3Ep1? zO;MoI8hq9^XskrkSo8x(11K?rA`+4~U?eD|!SW8skDzb_wa3A|I!I{`%D|A&htyY~ z{Ebo8!_EZ*rAg?>04S7@TX4{p7(7Kood5|rP-X!atsqfQm_zfjAZQ&UcvKlw_d-io z&`=F1rGeCf=8V9-1!#Hzr6I^V9#9(tl&itDAav9Y<QGuq1>_%49tAB60X6C%aRFM= z3R$-dX={P<6DWW{K?e(OQ2K|iVF1+|U<?XxxNku%SIFoA$O2GW0A&U+8%lt~8+1wl zcwH(3=o}PK<p6RUXxa&KryW=h;|w9t;!{wk3+!Idf>_X51E72bURwYffdeh{0MGk^ z3UQ>m2C{b@Qc8eA1v2^wayclDpurCgR1g74D4;C_(6|7l4N$oXN>QL*fgrg51xpH` z00Pzi;5rKAGtiCb;C?ftWecvOK=A;MF-Y?W6f)od0s95yagZJGb=lxO6QKPSpkP5- zvjEOZARmKoM+N0|NZ5fo))AnELmA-n*g%a%&{+-446u6vLCPTE0Sh;{UEnn*pu`Oh zUKICmg4)%feJHRJ4rB#*JPA~ygZ&F?p@YsdhOJ2j<r+}#1vwnl+lCGhK!*!KXHJ36 zfCI-b{OqR;a8QH78d@%~GBB`$eF$<k;{#CF9c5iII4(eK21U@QBsjf+$}I3~IH;8Y zANzou*9dN>gXdg9Wj3UR08$GcLIKs&kOQ(2{sD(Ds15~{THvx8RJwr%S-@>;lrRFV z(FBc~flifyw8tRv4PF%k9jO2{W@doe6QC3T-FpQ-Ukefrptchz5rMk=V6$Oy1aSw* zJjkk<51`Hn#DidUuyjLKxrJ0tfXXdU+6Jvx69u1s2XZK^L;{tckiI&&7(j|AP`d<N zcYyK~xEBIi&Bq8n5)4$x`~`KlK<Yu|2BfV3NhhGb04Pm?YY<SP0-X^EijoZQ@Bk<` zfy)VKdH}T-Kw%7OM}ubQK_LR#I|lB%K%5GRMo?7&4PRK;z{a9MdO>|2&?+x*iv!$_ z2W9t)4<J`V$|+EB2%YZ&pTi4UHxHWm2IYG2YD9L>%y$Op08~&42W4T95un|8plpid zS8)CUjV(jN1v07$QiMonAk#r@0<aC>xf<{Zrr<CDc?(qOfeLKM!eE3x(0WB^eF<9S zt}F_2Jh-feWKNI)pm}^)Is(m)flC5V-3oR$C}V*zs7!#KRSiyy;9foWejY(kJp-y= zK7eMoK{3h106M=BROW)sgU{!HvJq(05-1mg?#TWCs!2fS#F&HX2WY<@G@cG}4g&+j z2XGn5&%j^~>W@OyfMQ1!<Yf>BhbX8J2B!f~+JUqYz-1~}CFo8HaN7~IvJ+CofV>aV z3{D8(jeyYbg6IX60g&-b@R%5=)eb2)K$pyd8_^(RKwbg0Xdv+jzV`tXN8r2xF#$9h zqbLYEj}Vl=K|MI|4pUIV0}XS7?F8!s<z3LZOyJoMW{(fx&FA3lX`oIfSRHuG3DTbc zl^me{45%LlavNxT8SK6a=s+u|p#W-Qg1iW;co1a(WXuP2mI~yC60l_;N5bs_y9qQ7 z49er+5kZK`3Xlchcm-EUsOrIa2UO02>Q+!n1IGZU_PYQ|`JlsR!M8nt3NLWU2<i%h znj&c72e%V6HV1MgDDQz1HMm%T^r5khi-G1pkm>=@Eh7fVX#!O4fyy6H{SOW?aJ>wQ z7*KG5k|8*wg4%1)w8sDr2Sk{QLQ)dA2V)9q-Gat7!M*^s)Ihaj1}Kg}t$DC0D8E4B z4%CN(h7Y*(1C6et#vG{g2y+W4>OjdFG-v@T-5_Rw#-G7?8Cu7JTCmV|_zcDeVEy1e zFw$Odu=^q7te|liP;NkJv4Ywepez82A5bv>vlyDXL7@!FDR6&)%>$(ikSX9W1dRfK zLIHH%DyZWK8L$U+9O3B=<adxBP}v2l_dw+-=w5qpf?&*m%qD|Ju~6*+=OvK&pb&%Y zqX3sj;J^iibOmU>1-#S@l7_%}1XACF_MCuwPaqwjfUN)>FbW^5f|e7YGy$3u1eYtI zejcd&1)W6$sy9K^I4DFwxd>54fWrV1myi}4C`}>7B`D&cVF|VtT$6)B5)vqgxP+uD za7aQ<RRN`Ul(A@te(*WbprinG(+tq?J(7Pxet_l~aIFBX>p<=R543_B?x30kIWHj3 zD~f{VGhyTOps6MBNIk-xpzwsmognDMSa1sm6!?&c`v;w{L#T(&d4gI*ptc=&eGcec zBv6YHG$jJgVBm5Pl<&aJX?R)zrEz4pflglml^3AA1*xr|bCaOD9#U3=&V&V(5};H9 z9(NOk=4+@vPz*AH8yTSSzZIZX3D~XRcm%l>9(G{e;8YDRao~{%E^<I=4(whOw}R&Y zKx0^-^EN>J1V|bJ6$cROL1_rut^=(hL5>4R{DV>tC?|vaLg1DTJgmS?DR7|&%8$_I z3ph=nxB-;6Kqi1w5~P(6OTnOg4RSPOSPYbApy7t~+$qp~WzbLrw_cF;OM>e>kdHul z0hF@94GmC30^~W=d<#mCko*Lm2?FI{SP22v4O*BFnMDN011!&i%2TlIC~*!7NkrZQ ztA&;epb!Vu(I}%_pjJ01u)wpWpj8+kKO^OJP&x*?4N@~h(=seIqtwe_yFh*cwIxCN zz@-Ve{{za1AX_rPkpl`~P;Cy4TX4F9l)2#fSWv0}jr%Bqd*z^BC1_1MsKElBWCqy@ z3P(`TfEp7Z8XlftH$c(}xI6>5D!{Eva9Rb|`OuO9G++a|+YRIwkQrb%qNEeBIUp0j z^)lELQ27tSkftNJ`U5v=zy?608DR!w4g_QZc;p3Cj)U9Qpb{3m=ob`Opo9Tx7bD^p zQdWXm)1dSKPFawVL$E$@EdUNja5)E>;R6@wkntK&KNmco07@C)_A#b@&_*kW0Z6@N zP`rcEI#UE_jW8?7c2I19YC%v=1Em3QZ3yx`s8<i^TY&uyCm`t^5)5EL5EB|VP_sdO z4N$%VjRAq%ZYZe{>MGDI6-Ym*DuuOop!$*aWPp4J8W{ogI>Dnj&~gAAk<g|!$P|zx zKs2=AM1(Uu%|T)ZWERM0pwbCMgF_pv5ki16F1Qs3N{Z053+|tT*1dq&$3ok3pm2ip zAHZ{a5Z&P959&sPxS%A6)J}o)6``Zmpb!U@aiAJf6x>UI_Y}bu0Vt56dZA$g-4hPV zGq5oi)bM~<2_m6k0ixjX0zSV^8#E^b4hKlD0ptouZ4K%XLNK`X49bt-eM9g(2r?Ip zAtE5Nq3H@#nu26NX$xAWfb$e6oIoR-AlHKHa8S-b<V{cx2h}BzN(I~;2VJ;|$XAfL z8c1IkRD*-Y5x{v=5RxiE{Y6j#3_9H&6kH!FK$if40u5mX<je?An1kaR)XoB@OOSb> z_BiC;I*{w37}N&?)g>SU!AT5a7AOuNV|Ji1K}Z}x(lp_82?;7hx&)a88A}75=cNb^ zb+GF|ZD&}?0dfP#Ne~*cAO|!ahgyb!_xD3@{6x*akdswGcU%dg`Vr(Yu%Y1a2fGSn z52%cVrWI%$g35SkSRs!`fdd6nn*N&s8f*cN6M_8%yL$%Q4+EDVklT+y{sPw$$axLy z22lP5+m76f0Hq_ad9ZtuLFR!&36gg~b6t?I$6_A5oQ9Svi1Jb$l=ea8B?v>~ADsTd zrb1i_wgoh14t5k00ggjhLI;aOL||@(wz)vz1*^}%V`UJtKtT=hFQ{<{%jht@u(Ntm z^CdXffXWzfeuSiWu*FaU6pvsBA@U<M{-9w8D}$jYxPxnU(6|(|y$Om!P#7^qfU6pi zccA*A@ei5f#0-0AO@m%`g8cXay21hEM9^3T<or6QdEoLBw66u!LILGtkPpFg46xoB z*a(O|kO`o6DdxO1BL9JW1j69iWpFu)NDq+E28R#GC{TkRlEM-70;r7(i7!wngGzFE z`2@-ppcDyije`<AxHAZgFJ=!0Xj%fLB=9^cxHJX%2aKUDXHdxi87Bo5b0B9xF(T@q z0u|uZN8r*AnqCohK*}1BO`w_(VkW3`2KgML{0G%IP%~j60OcYmc%25BbB45AK<#5h z>jR`0+<XI7OrTwP&^!)ahk#ZOLEHiIH)3`M8t$MT7AQJV%PdH`0<}7z7-RwrqvmVy z4k1wSj&MDc1&Ryk{Ejww9VXN)a5#Z;3@Ckq!Ur{_f#zeNMt~_$4gfPy31}k#(lrIy z1<h0PpgA&7Sq+L6kX@kofbc;qP?-nHNU$6Z>SMtS1|@D-BM?!iz~+xZW<kOq6cR|G z0Ghu54-bJFkzlhy1UL>sZbCjk8Ds`HHbFH3sN@HwXIOm#b`q$lfK_nNID$A2-ArXj zSqU-`gh8<jE+xSxgM0}|fslj@HVG6}5buG^f|v?+GNcTEx)p3LH0;3lL4(UYaC{-m z2gMQ05Ri*e3<GmOO*7bCkg&EHxLgng^)o>(2DLjtF#;)nA=v`dECZ!%kP%SxVe7fT zck4mgXRMGuBdBEz3O{hY1?mSt{EDRi187SXTDujTH=zE5wS7T*`9xuU0_g|mERZ9> z#SGYo@c4wa3&7z5+UW}(Wdx-O&^l?*`UY^C0vQ7yKmmt6gaGXUtN^cGd%yr1TL!NS zQx*l?JHl=XJ;xqY=!0zrWl7MQ8PGT~q#RHMwf&)S0vgqX_!pG)pkW1?t-^38IIKY- z22L5MV;U&^bdX;`HiHa<)-#|q0jv8!=77QtQZIw@E;v1a%2jBc1!`=9!wQ^|AzlU< z2oeXEoM5+s>Kc?e5>UMW9=ib54WM8Hhd#)2phN*_EF$Uz@R$y`4++)-ig!?ufi^Ex zU>mCf)ghpDE8tZx;PeLWKO*$Obwh0fxfoQQfZPdbPeO7mhy;~Xp!yMH4mgiN+sdF* zlUIPE9Taw8k3+%%oOr=*2bar`Gu9xpAdt9(<SP&xG<E>88jc}GLAaoyBP2Hog61nh zCq+QYHE>D<t%nCI1`Sexvib+mXe!ula9<oUe+RY?R5Czf6TF%atP4cIb$%dIFQ{}u z*DDF0+lRE>KxqY(YeBU+$j@*$gKY;p8bW}a4)HHI+(7H6AfX0L{mS5T)<9(pB$N>0 z16B_i+k&<nKt2Uws2-3TAzeVom;-o-2At0rz<Oa}3#qLj_CaS~z-uf)PDQ#)2doF= zN6=UcD2IXk1;U^<7>GvoBPfYOO#lz7f#U~k1}uKS_JT)VpmxKRgBPE|c(Ahyz<QDG z2lo}h{>9Lb?0%3rFf+jELm6C0!*oOSBJw&Y&49`{P__i=#e%`38DMeHnpV(61ZeFm zBred+g@-6uJ(vKG^nvEpAn^dx2b!~mgexdSQTp-FmO7}Q1cxKcA0W4aaw<3!;pqmv zt`pjuKpGze+YKQgX$dwS1Tq$q2Epb+@-Aqt87Njky%1P00_-k`2?!D#(eQQ=#2j#3 z$%69(Xm%Ey6F}|+)!pFo6tt!WCA~lsE6iBN3{b`Zg;53geq~_>233S9V0VB@1+d*P zJ#ZSy4Uqf<cLQj)6s8Ok28<72X#?zLka|%1hWH%pcbIypU7!qzhyzgEp{fVPAX1z| zlgbCs$_a?Oz+nvTD}!SXoL)e+4LA-#HiBD8;823kpTgV#Zp45}T~GrKgdvp;!adsH zFbAaskUPL;gK`L}+2Gm?9EM2dA~V4zgGTuvu>uMywDKL4he5W0!U^nN@c0Ev$quO( zzy%N_F2VEnkop}o-VC~<5wuDZ948<vpks@m?GG4hM8W+vP<VlICTQIxD8GVy1}W=6 zj)#|#(98mgC`j8DTE8LXdw6(*%V)5e;P3$1fF<99T?Y+&kX4|`9Z=eY<X><-4R#AC z%wgxvfSR`8NJnu4xQ>R_WuROI4m;?0HP~cO)e5SoK~{n%gi!1OhZ`s#2r5I<7}&Mo z0tKf%2tUHy2C@mH5fo&gMirzD1YTnV-irxdS-_Z40otz#k%N_Sg#F3{(!d0oPZS3A zOtIx!=uPP$e}Hl(IOIXj0XYj4%1|1V&apA!Z7W2Z1)RR2X%XC7fz-n2=jDI`6_TVu z)*zb$i5HLwpd1K_8BqC&5?-Le97r(-U5|niCh#&Bc8&!!6++b_rG3!t&*0Po9?gLE zQ$a2RO~6!uk4XRpAvjNi%OptJ2j^vQE(5s?5~t8K01hOO3E<V5AReqt1D7?ZegpXr z6zZscgXjjWn}OI3^BZ`)8r13nxfoo6gJTO+I>E|ccs>VNjEs@P7c_nc4pmTY2blv} zPY$vaG#?!S8hi&0o`MoQBd8YunsEm0-UjU~X3PL>E&(lr1GQZkGr(a2SxE>A1#r6* zG;{|(AsAxk2bjM>{RnW{0M)IKyoQ`6Kn{S9|KrU+pqvf5!xB2d2ij!^KHdf7-@ni` zQLr!r=MhjU1eddj^nk1uWEr&00BUVRQV&=?$R-pwg7(LOw0;1!HDLO{@dAnyP}v13 z&A|Qyw`LTfaSQeyqLPPj!3|!JF$^&Gg6c4ESq>>v!RDC4$``QBAOe(QKrKj=bP0Al zC`G~bD}&a&AlnbhGT<ZvCO{@Y;s{c9!rB^;)D3MBf?WVI6}^Q3bq9z7m+oM<z~UNG zHiFkogVHFdbV1S&8rX*(WCE&<pl$%`1-SuIhl9c%w7UbO9~_dXy#|oIka7y#2Lo}@ z%_1hRfhKf7B`+wogWLg&2k`nb(6|e@J^(KO{Qx;<5qli~k0X%Z(AwB&aSHMWD5XG> z0LbCs*Z_?tLe_XOfOd&7T>za93C%O0@B@bfq_zUpjG$Z(9YX{effFOObM(RO5pe8+ zay_&p0mUu2e+F_BC>0}22aOHjG#IM{I4#4<S5RDn(;~>N;1&kh51>>D3Ij9)!0jAn z572M`I9q_j8C-Wk+zsl*f!z+$4{|4{{R+z2=wSj9Lxe4wEjYL^cZ27mL332#ky>!t z2aVQ1N-I#A1FCpHF$FdUTuFdg;1wXC^aJxdwc9rgpuGsp9-upY!SRFSK5)BD6x6qX z_!>e&-3F<`Q1d1zB*7RI3y{(d>IoPHUepg6pM$p7AZ=MtZ3m76NXURw5jg#T;sPAY z;JO~<7MM{OG{|`%3@XFG7#8;6{I3p9qo7m+%KxB!KwuhXJ}B-X7_{FD!)&Mk^yq#l z554{chaXP=gGU0uTT;RMEy3+D#tcyX2O0?n)qmjH4>PVI?G<pmfN~Zn6@u#!Xv+fH zwt%_`iGmbjAOX-I6lk>#)NF9w3R(*QsjETl1W1_-nyVCL1RbaV(gixw!vo3yZ6$-W z8DQxJKBf+K56F$+dK#2xK;Zz&^PrRhjSx`g1GV(Pc@f&@0J#X17e9b%dr&y2nu5w5 zP&hF#F?;~6T7|7`1+7^{+(`_bPJ+3of+>RO0_bj5P?-jbPmtZ9hB;`Q4m=qEty972 z0JLTW7M5WDgXZ?YK?Qay$jy+s6SOuRq%Q|gr=YX~Nvojx6l5bPm10YuApd}j2Gd{$ zO1gyB@!;MLsJ{#jMey7;BsLIj5lB>k1{1-~1y5gtVipz-3}F9(>I6_d2aQ#5c!2lk zL9~N<9pI`7#6YU2!FnP6VvtTqOA6A{1G^d2FNTaUf*V5Md=D<u)WPE{pnL_cOTeK3 zKA{ejE<mXP98wU!K*~6fLqP5Z(cpXvVL&_uVnY)f*bZ=7g|)RnBjb>=5|oaZA}YY+ z;h=~Dr6UG#dlDR;AUok0lFz|45zf2<vKEpaVdX5`9B3?qOKPy6Ksg-J9);)vk)SZb z-seDsDJXnE7(CVo$_(ILFCReLXp!ntQ2c>H6BG+z)x?yeknv|os{%4-2igA)UP}l{ zF%^&<2%zbFm|c*$2T0ovb=4jyu0eyx@ahN>HK37eaGZeWKR|H;9+3pClmqX3fh0%; za8nz+su+>ZA$K>xXG%;VYdb-10(A&Lv##K~*C2Z<E`a9}7*qw71;xQ70w{n%i{2sI zJ&@dwa057YAnpc@MS%9^fua*hEv$b9u4O=J101iQItCgaAX~tba3BUKU4jxi!oQ$+ z0hb%N+Mb|50xh`*X$NUVvK!Pd0)-W*#)ppE!`upv9Eesh6D7<*Ym8y5DnK(Huvu%! zQbw3XP&Y!(l>yZyAPmZfp!5J;feBIzG9NVT0+}QM4+kQ-5v5%UTD%T+p#q9~5aA8V ziQqf{&d4AWK^Qz|3#!)<X$xGgfkPRz0u?R)fm{SChd_ov>n3p80);g!4?s#H@E$sl zClHt^0;CO80-(4N)HVl)8K?~bnv(<NS@0fJ(D*obZWCb!N*cMq0IC~6HJK<VrGl|2 zXzwE^AAp<+_9Ce81Q`I1Vo>;j_U<uzfKJK+?E#0UD)1~2sP_zV1GrWJWqI)8YEa06 zM8G>e!Snp!IX%dj3gql9=&5M1JN%%z6r2Vi8esVoloUXxf&FoS<vWn$zy&+9d7_}R zd6gk&tAp|iWQ7J~PYkFm1@+%5z_%BI4FdTXG!c(s4nLAPp!q7$O??@l${pk$$boO5 z<I^HQGgqKq6Bajsdi#)C3EY<h&9#A6Z^A<d;v2}o3dmIO;3u*<(0c?x_sN0QbwK*T zkg^1#7ZkoA%Rxms<SqfkS$fK%!r)pK8c(1V+B%?u8{AVu(hi#PQvmJ70T}{`6L1(p z&*B4}wj>DJkpfE9prx+hSb^@AgNNe-=uw!ENqy-4B=Fc9l3Ad+H%Lwer4*=HphW`U zB$ol+{#^k+ln<KpK=nOxctGO>dNwg=&nUQO4k=WiZUH4-=yh|L=L;%}g2ED9u7O$~ zkW>MU519T8&~C2>pv($R6VS8fz~PE=pDXmtVTdb0^*xd^KquBAIRw=VXo?4=4Dh+2 zpdLERH6U|9iR=R;iGs~Rbr38(HZY*xeE=%4Atr&!6Oeh}vIyiTum?ez1;hZwJlI%J zltE-Gz!ftn`k`qE-E43>0McIsl{MgU1{8KnAcn$8nAwmv0!SD<9RZIAkh$Q~G$Cmc zeAg&=)hF0Qa2o*TB9LZKvmAOXDriUmWw--oCispJ@X5^>W`e>1bj}`fyYd65Gy?TB zK`j!{ngwW$jGF$TF+|XPApibx!0k5Bd6eMv4^DTW7BwV>kYWbxPq3>%`{q%Pfr0rE zoc562BnnQ!pk5JXm?7K*8q@(*9FPJ6nr^^(5?-!Acl980kpd`uKrR7?4XEUY>I0uS z3HA#toY6{ZP-6tv2!kFm2|8~Z>_1ROf|UuNvn7$!0JsJN<ps!kB2a%Ck_JE|Xu<(B zHhBSL%%2_LMhhr${WU-d8|cj`%#gAkQl}yO1iDrZ>>{l00mTt4Y;m{+%_X?q0S+Hf zTtmVY6jI>2748;LZG))CKqUmY1P2dxf%6}T0PRO%2JO%W^##G@6{IDHNHw5(40Z}J z$Z??1L5?d624>JLJRtqxdKFZgf?8zYH8;@pPq4ZS+<*iXz>pFLS}B4|1g9NXID^v- zG@QVvG9jA@so5ZvJj^_3N&*=K!62`J3v@^w!ws!p!96JO2q9>n-v?;&y#UIiuse0Y z=^uK^CnIPl9Vpd<`Z}P~fxtKGKx}~M1<g2tPd2Q8xt(PL13zd*zA&g91GjoW=?&aQ z0JT*?ePi(cNl+UGl)6Bs!DvXRfJ8urCTJobw44wWb06U5GasPDd{B;qm=79gg2V)b zL~=ju<Y!QeMwA&;!h?EENG(QKGJu!_uFJp<29=>6psoi~1Sl86%Q<kl2}!SDGZAeS za6JzyGhs$T90)o(73OtNBM!M-K*`UbF%j@cC8&G?w`O4mk?uB74ub6U1;-eqEl${N zsP!$_b&#<O@R`}@AqTE=p>D$+a8NVB`G6gC4-P1Ifn0?aav;;7nHqE|0H}U301YRA znr)ynb70{H%A?TyL@+*~2^(izg32&x8HnZ@NP83HIZ!O(ibIfF!1qjm$}VvHLFy?` zIs?_BpppliN<e)rWpHT*G7<$t+z5(la8njZ4m4_kNOvH&!t4T>4~h*?eg?5I?LwY8 zLKq403xW@0fr=MsJb}v$b_Q)oDFccpa9ayfb3(=dF>Qm*ZNbcg(jc#aG80bELG6U5 z4J13^=?$L`K`91m6M_PH5mW{uL|`nmauk||Ky7_UZh?%Pf^s0py)a$igbit{K!l+6 z1!$ch*xk_b6<l9HPkRKn(xLSPs6C9-KnKM!O8o%7??M^%v^>x$Eui)y2!qT5xALI7 z?Lkwdpr$`!{}&{bLE#K`Afy2P04oE*cZta3G9O_&L>s7Y0m^wRz(D{BOVrSW-Mt1) zC(uv?r4x)AW(lZi0~;!axDrxJLTed#+Gep};Aa5eW&v(1gK8-7SUyIc1bYxvPk|XQ z=YbrETvC9|1;r28T=2>3sODmcE3ko}4g@H2KrIb~;oy!Ha=d}(>A?Pn*2SP40-BKr z_3%Ex5)7m!hK^Z)$0#A=OOSdL)WZbNNP$WQ=w29b2NTrQ0Hqm_gTSL%;ISL9e$bc) zNFT%mP$+>hc+?oA9J=-v!~{(pfHF0B%oemp2ecj(Jfi^`%La{Yf!32kXUoAIS#X&P z@q7hn&k|_d0yItoS<4M>vqJMCC^dpy4W3~DwG2S57;s*MdH`hJ3{Y5tTmtGjgZu<b zkKi_iG%P)Wa~`-nVFaCb2wGQcst8}r0B$XOV0`d*1tj%=%!8yLXu}U$CxF`#idfAA zoiYku{R$fGgoZOXS%TZ?uvkSzHmE59&I+KROe8m>v?B<X8L-9=u5ts^mWSjVaP5FK zJ%U?>pp*b9JwQH2xyugfcW`*Z%MV0&Vsj(dW1w+MP(Xq)a=3!JV_-qZnrcM91AL-A zdK(aY^8!+N2QwQ~a6pm~%-tX}(ee;DO@q@WXucEFmIQ?Z=)5vm+6J{y!D$L)7RUr} zt_O{tfH<J`0N4yzzJTq=1C1qu!vLfo)ZT$6Lx`(DJ_HZ6fc1jymj}=Jfo4O%sSsWV zK=LNI6oUB-WHA`SS}CAp4H5w7anKkaWM3a>{~Y+9Gf=Mq+5!OQ6;aR_Bd8|>idDF& z(A#D~27w1$q45f?Tfi+aMbMZWs3!x;`ylfm<A&gKgTT9_Kw3dP8)$DBl)S-Sho)|{ zdJkM*f$n{Q^tC`W04Q&ON`6Qvf!olagaPXI!6FfKdI+do1($tbw}Hz(klVm3c|<|_ zL4E>dCD1KEpzaI<^!yocSqUD40*#G9&QAcHodD7Uu@jPEz~h3TiVdlq47LmEW^kH= zrW9~%`vJ&{An$`x3Cw0lXBuih$n%hX6F9#qg2(p2DFhUfAhST@dk^5@2x=-G0M(_S zv;s02<Q&*~BSz332JpFNFqcEdl^HXbE+FJU+Ydp$g2^!-K*%v4fLx^mmIE(mf}HOH z?ioUBc<}5hXg(6BJZRPu<R;MBq6{BE=kb76(t*llPz-=eTyUBM9ZUpSXNjJBAwC1O zB|r;MKt%$myeCFING(VYNd5l~#s>@^Kxc%BgX#fD-Ua6)nE4<YR6l`|4ZJ=A%@Kgo zGFT<32MgYk1wOa|a^*e9Y-rsB9;*k{-XIL}A*kj7%@Kf21rZ=GgDW0zomattZLS#H z&Sdsr0NotR4w?rB_2ofr77zxn`v92&ax4P_3&@?|c}vKC6>!@Le6uVl#fXAxLQuO5 znufsT1nBfeNTmld0)#=C8gwlo$Y-D;2s8l0zyLb!n1KO&*CfPjXzv<fI><es+8K)t z-~m05Ss?R4V>=O`MM2;Y0GSRd0KntmgwJ_qV2S{joBWtHCe&=C9h0EA0L`95)_*|L z5okmX6nij?vOXD+b3mqn(kbHHX`JUqW4H+t5};XgSkxfQ#pNcL9uSSF>p`wS=mqgW zZUXH|V!8ktEyA`k6;Z^4)Wa~UKF}Fh;8Y1IMZhTm=2n;vFb#`$4{%uqN`L$e&XE2U zs4omkiy#^r@1TAisI-T+wm>NXl<vX(aF7^89&8)vYzk1L4|FEf1Eiz@wiSs0m5iVg z5>oddNuV(qSQtPV6|^1^mOjCAte_Gev||etuHcXX6>k+EKohPY4E8jvtpwh4EDl<w z4|O?c9U!QkYHEx$R|{D`0j@wng$XG0D1ckLp#5U7^&_Bl!`7g1f~^q%hXHt&o)KK$ zD8ow}P^kdf^NK14b|}P`NK8mrfQ7-{0kc38-%th$1u71Z%LH)-Y4GYe&?zI3zBgoq z5KKaLwt-@l*#qK7kX<0FA*Yvs!wl?gFaZfU<T6K?0cJiHvq7`Apm+e84^GiA!@+9= zLH2=50Z<VQS^*5Y7aZKA0Oc3xd9C32Vo=P0>U~hU0yQ(iC*XrB6;J^JX&ZsoZ9~>2 zfc7AR!Wp#h585(^UVH-yJ5b92w6+3r_M0&5>^IbS0DBZ1tOyq9s&WR%e0~IEJ|C0| zP}cB(icnDaL)#JHw51L!wNPz<luW2%NOpnk1`!ZPgIK5$&cMuooc4u5w!q3|&>3Q& z{vBvdA}B7wx<Q!=+*<&xBl<T3c8)OUc5O(X3F0D9+67}+SqY<2%1n@n;1~rl;20cl zA7Ev*Dzu#pTloWW2`IilfTm$VSsUDN1Fwtv0AA+;%ERE3IE6uTPM{_s$jzYRhCm}r zpm;`@4I0Y@mFkc>4!Xh%Wbg;Dvp_WrxXl7S;}6oNfUczijc0>Sas!Vzfi8tn0J#Ga z^q}+!ia&&1;Qc$0QWIQ*f?D39pfz2fRtwZOVE==(f|`M-VE`#}LHiLwIUW=rpbhk3 zec<#9UN;AGqco%xQU<49P+A9#&>?70_(5_ZSU<$YV7oyClD!Z&Lq=sGVFW3IGN8Ia zB^bmoutsn=fzRE8tT_YUvJQ%M&>1nHSO7KuK`OyZ3qW-WsNDr#$A)NIJ^)L?&SiwI zcLANK%W4Wbbph1wg{&k2mzt3Lm%;1-YTKwnM*2a$1aO>yivy7Rz+GC9eV{fHM6EeA zO@UhDkN^P1IVdr~!XDJdV|)N=<AM7%Ab)^L1W@k-=6{HPAWj0U@2G(40i7WVnjZj_ z{-8Jp_vb*p6VQQAXy&8rQAWfOhy`vBgK9U310f*?CP8rowF~8J9*{0bT!T+?fQLIM zp22Yj){P*L%mLjujWaJmnnj4b0JR&W2a?9H=LLvGpac!g3s8NKvoJt)I!HGtzQFxW zP*j1E6eK}|G=sFFg%>|~&p)_sh4>UHFMt+Lg2D>ahhTu(g(G}m{Zf#f;E(~eq(K;D zEgU0-55&#*@&Y6^K=eZV3ocg~GeBt*)OrTBB0%jrh@B9HASDdoec}usAnQCKsT@*$ z!TOQlwXp1<T&*aIoDU%J3dwh%J`v*skX`V#Ht?|kP}&3c4ncv*$N);G84$JN82JF= z4M>&&`5)RJ0k3I=_ySalfl79We?UHhU?%XYBS>gMeTPPY%ma;2LeA(yIv*XB1_VJp zBCrNf9Kt%9ptThl;Ee*{@d0T61Ulx$462(!X$XwLtt3!B1(#CBqM)ONA;k_T6M;+y zh5Wx6;4}tKWFU*c^%t_)+MpT`)L)1B6I9}YM#sT?P^ty3;DF|BV?j{K4_ZD2G9QA$ zsTS15M6PH-Rzsvgg$yWtK<#9TVBiF`#zAcyQ2PdSYZ<7V2c0DhZA*bS!2DBy4E0uk z79xN~svzT3p!SL+Xbf2u(X#~CWsnpOy88l@V!)LdxIlz1vw}38Aua-^duVwI9yeh2 z0L^}aV;i~)1nhT6>m1wyhlUAwof{|=K_LvLAts`d;66F@d=Sw71o+Amu&Lk}gQO*6 zL9kQ6x<RIa&iDu28Uqpd0O=%y>Qu;jUdR|4cy0z%*MUw3grr1JKN+M4v{nyHg9vcK zM4VFr@gw-03Rvoa)^Sj`fLq!ie<HV>;0A#c5vUUi6M^&tpy^W*mWIG#2sR(QmI;(< zASnjCEf#!UEG(sfJPa`m)JlPc8E9@Fa+eawpWqk(g&L@&hqZ^`zC{EcB&I>V3(#7I z8PGZv*1uo|#jY@@K1Izxu<>G$E5R6?wqYq3TF-%;2oi$jBM={qK^X}gH{dV;uMq~X zWdM&vLs#B{Pm~4CnuG4L0F@VDlN3NB-Jr%3*c>DRR9+yLKcMzIs67fTeIO|ZS}Vgs z0ZBWE39<l0!(5LXuAq4dkZw?@!g3GTOk-$!AEXf!J)j&7p5p*b;K0{?g8hlI_8sg# zP+bV_6M^y`I4wceih~UU4djAmu^`b8Nk7nt2T`Dy2bT-rF<NF&X%1@pFhbWnfI6Lw z8K6`RYS4mK-GGW1aQP3*x8QIGjpjn!0$V)?O2?pZ2d8E55f-5E23ZMq7My^a1u9pd z=?>gpMJfkCX%Au^*fh}6JLHrHHWWmF$E9Ft4ty>UBsW0LSc8^Nkh}p2b#N;Hl!HO% zvw*`K)^Y+hc42OWRC*vD$P#Euhr1he79(W72o$r>S!s}qk@v2E{0_k&W5D+dfWi;t zXV9Dx)J-6NgHtegg+8=40jDDcXjuZvU<#1E&@lbbdJ;5B3u>Q0VgXz;fyzxmP`H88 zKFD5V4C+h40ukhMgnuA?D!5sqAQ!=01~nIUrX0v^pxOnN=0Sxr*b-1cfye*BX$zcg znHh8;BMhJsSkT#kAhSTJ1=4bamo4DlIH;A3afUwFWRMl0G!06Jkd^~P7~~X?KR}zS zA!ROO26+4rR5F5M23op;Oa|EuDj!gC9r)yYP#l4x8<Nf-$Co0N383@HK`UCo^9`WW zcs~470IlMNG@ig^5jY<~)-J=&cLbea4n4CFw3`C7Hdq1Nzy(_g4kjc!KxqjS2B6U= zP~8u<4|6XIC}%?a2`#Undp|(+5~REahbzbgNb3Zg)<Llf=>vk2F({TGp$8>FUI#H@ zG_>pjhodTZ{s!tYa9)C}MFXWW&>9+W`3*{KAX7mY92+3hKo}$dD||qK3p&gc99}rg zhOU!?<RcIXDoemEZ%_gN4Zwg*g<w!O0hAQLu7(*7OV42QA^X~(ZU?y$l1o6R--A*% z$b4l;Uj!UhAjg7w$)MbZ+|q)GgDgd{3gHjf9!5}F1Dk}!9#E|c9>)ZgI}jhC+XXfq z>`#zg;H(d_8YB!l6arzNCT5&~;vZb<VUHJZmPd;jklCQ*0I~=cd<Z*q3EF|hN8rW^ zB9=fd2bqp+6NnFL6C<ZFLjrbzZ34Brpm7NHBgjxx`#}B!dlb|^LkU6{2a?~w<q}f5 z2A`%5E{k!d3UD<FF$GM52K7J}M1g$>3p{w*1C2u?r9BMu;r;>VC%A7ws|CQOgA*Xc zZ=mD=o;w5E1P(k9foRts0FQZVgZga7qM%l$F*FZB$^mdn1lxd|(m`XT3ZT3LG9DC= zFl)i|2T+p>!~)w1_Z_56huCLK&_1{yk>d=jy@+T7+lgX1NCbpI=^xpCL*ng6gfR}= zp#crH2F-GC(4gC`OVDm`8wQkKA$c8_y&xMw7&W{>DHv2tLeeUDk2I(+11iP97+h*$ zl)NBwpcq^(f=<Z=moLcq%osVJLE{AyPoUHfN+sZW0~TVCS_6Ao1+@#5exOWHVgX}N zpn<{+RF**Yg@VWT!23jC_JC?R(AXKM;)Uu*P|$pY&<pCvfm{Y|y@6eaveFOU41rYK zX!e4007y5u_6GHiVCq3_2ZXKQz6&<Jps)sy$AIc0m|mDZm<M2PW<J0KJ`YzFR2M+v z3v4&IeGaWF;e`n(q(CJwga$=Cs6_!v=M~^GiW{^e95%WMnoj_&TLbmoLFo$AG6$bd z2O6dXt)m4^RDqoZ%1|H%iksv?^9G>)J~%wUtvj%rKqsj~>MT)k_aD^20%dtn`wr?N zkXdjHNl%El0gaGBdS0OP1C7@Ypv6BBU%@qEhOarKeFh32Sj-}y#RXoR1kn!a@`3XM zioM|W4p=8BY~cMe=>B$yP7nzT2hc1lXaoc_t`61<ZDV1HSGWgYdcpB356StU5CrQ7 z*$u*=cm?-qL2iJ08%%*#Du7&oia||yZ~+eL2Y}-ke3mWPBv1<(bZ#}Mv<CG~z&gQZ zft!w?#S)O2Cvb3r!vtlF0@Th0tu}%5TtTTF+U|oS3UKoM02<i=t(b<?B%m}8IU@wN z763F#2I{MVTm&8410@ACXMnr_)(dX?g8PGzwQryj3RHH0YzMXUL41hC&?8bnU0hIi z7vwqc=5VlQ5N3hbFM#3{f<bLwP+|aK=vo4h-OLQ&^E<)yA9UTbF=z!kXyg)lwmztg z16lR~)O7&Ob%AUJZQTHs`4FSPW4O?^G^9-c&KsaO2DhoeaSJjBwEqL-E0AwMRVTPj z4{qy#3tW(2AQ%!%U>P)jLC29maSZB*fNBVk4?$;qg2M;Adlxhi3EBY#vIAlus8E2W zLr7e!Lic+Mf?Npd)qqR^*Y_aTB8@IVOa*0SFbh;XgG+2M4{kW92tqL*>`qW?5|YY6 zd+b1Y1mttj*dRzFXe0$hLovwDpiIihzyP{B2(caq9CsPuZ~&ES5ce~KW;?*+4WRk} zl*+&-rGhqwg4T0Dmsx>R#s|=vC|KW~AGCJ_)Q<%D8x(q=cma=DK+9*4Ge8Ob15*TK zHzmTOpi|2cXFh@5f>g$Wd)}b(&{!1NJV;6XfiVMS8n|Qxoe>SbYZekQpmbZo1iDKT zGAj?xIpCHg#Ao0h7&sMzJO{3nK_LzbIZ!eL=iv(QIsMT2G?1?#=?@gQpiqaTJBS&e zObd!g7>1jJn8O9nOM=FELF)>@P6nMX2Ew3x1930tyavczGN`5kyBXwvkZm9np%~P6 z1805c7z=v&3o7$L<u|n4hw1`R5S^fg49Gr6yd&RbgY%vp(6~Kh?hG_%01ih%P#Xji zc%WJja%~YP+@NY<_qTvj6S(aKN*^GffG{NNKw%0>9T2C1NJt#O#NZ;3^$`$%g2EBJ z3J)?*4XSy;XM}+AHfW$5G{6Q<+MpQ~=(#>fdcf*U!D|vhH}=48-2v$W#RCh70M|(n zJ>d0wkadlqd3um;P|I-!sNjdJKL9Tr2e}lyhUx=2%^>N8jCX-X;XyIU4BlJ=A;D<? zob$o+K%fc;8vmfaFgUbO+X0|Bhqi^mAp{z<hr}bO&H}ebLE}S;;MOC^Hc$x!%8{U! z38(^|0UBrmw<R*5=78J{!=Si>mbu_M2V7%;#xp_b3DnvXWdhIfz*<+ZvJjNF(e*;> z97z2uh*p+@VhX&l6;uYIjGHKe+rpwCUx4x&q@IGL6>zQtkD!6<1=$HU1{@GzAAqtR zm;oJig0zVsX<QLo*$hpG;F16oTCidX(E<e-4iyD4L2(5tPr>CBxPIjZm6@vG+qpn1 z%RzlT(76Jjh88G6fy!g}%rYpppyejAUQoRQx%Cs2MnGu-q8n63qv-|3H6#zPgWHXu z7M>vF;2cor8ni_N91n0cpjjc%Eq9PnV{lUxG?|8C4rtz7SrC*Tz%?eQI)}^?!p_9v zhS>#*70^9g;7hJR-UEmB-xc7w4~U(hbvz89wlTPb0L?^!M^_->0u4!!F3{lwNL*0B zf|5T2%uZ%V8ymFF78LuCb`vO$Kyd~N8IU0mjL7m3K6Z1kxd~(rk|Qt;0)-PK{eap& zpt(}$%5QKifd->MO-T?2&;Nr??t`4~0J#SeoP$AO0B*^H6A;*m2o@~ggX=s*KM>SI z0p}TTZI9HR0MCAaX9Gat0Ks4*K^Y0=6>!#oW-o{&st6)^!{Qlohajk}2&%EcH3!IS za1I33Az)vlobZG^e+V8I0qq?Drvr#t%nbIB@nz^<HE<pSnTMF00F_DL7=_vgif6DG zsI>%E52{%q9>76@d<^j$4h4{=060%T+yy$@2^5c@85&SO2R!csy44F@gRKBJ13(vB zg3<!$qy|uP5@}2r98cgeewaT%r7I+Lg4@;5xnW3H{n-I>7Tl5G>;)>MKusu6R~#H( zkbDC!N5HE@!L1X}dOA=!0@7cBoJ=8Q2q;~G+BO$Jxdgm62ohJIkOb)m(MV_iflYxV zI`G^NI6UC`!F^)z9vN^63o7eDaSj>>0BeL0A3*&ZP$LJ#1yzw?H!yoZ`qfBofS3ew z0Vw4o&3%D1f-xuvz<20C90P6Rg4$Z(J~}w>fcyh;1$Z17oJv5ZVeu2fU67*!p<#M~ z3AA<yG{ON}uZVCL_+CfQy=!3CfI_zdw5Sv68j!yrc^;G>5N#JwxPbaEpuIZao*1YM z01cUfa~d=iLc9ht7o2E7>yseoH*+(9R+NIw0Qm|$Zw^ugN@nmh3SI{SSvv&|Pf!?| zBA-JE>SluD?gRKN7?7C|HDGh0W<lZ^R30%fAgKf8JxE#wrAN@f1WX~gyoA{eYPWz~ z3hIMHcQ=9p2!vtr1Z^8Js0xD4Dgv#Ei~v<Upm2kwWd_KaJxGlR3MWYLfNTb@ECJV_ z;Jm~PT2l(z?*{5WfNDZza6Ca;9^ldrw2BG7$QNP`sJjSigMg|asG0Cc0LMQhuY>2g zA!j8aS|d<1!E41pH6yrX0yQ6QI(V@Rc=j85^*X3{1+_XLH0aK6P>%}S$3;IY800rd zIDo^_Tu@O^Tu>2wKGEM5U^BrP8XS)m&~^ayY;RDR3#x}fDFajzfg0;b>OiR(9^atS z5_AFrLOrB41kwX)CW4wNP`#kM0ItJ9X#iAiDGS0>foy`_Lk=kqV0{meJ>Xgkv}*;X z9+bL4IzSk9j1|-l&^iThdIZ^ln$92-#7OP|sRjE5t`?G~Kx^f|_fSLED?v4aLIR%l zpng$>lndaz#B>4d8&G>3<S=;I!~{|cvIn6S+%5)<#X$9d0t%)N9JO$JpzUz@m=vf~ z1=UNS)CgTv1hNc)Vc|g4GejZjh#x$@1)IMV1f56*%Tu6S085u(Hne>S&0FAo)5f5E zRNy)fI=TTa5yANYG(Qfy5fHSK5$Y>Q;{cHp5PVQh0|gR@28lw{K>Q89{~8+qkkT47 z<_4`Uz{v)@mJPC28k{dctAs)M0z72{G7>ZqI|Cg4puH@hJGmkL2B$>u=rS~YfWsVG zcvVy|aDv90!1Icr^%9`=r!<2)gFb^X19%NRs3wK9MM1qHQ2PS3k{p7;<0#-*1>e00 zO7Ecl7NpDvmC>MJhO9jRU(^gvD$oiCyp9UA9R+;U2-r<f@4|Ypa3@0gqTuoo>`v%- z0yrmvOK!05Ky5zoSU9-t0BtLPcBF#RKgfJgVg+^F!C}PkzXKe{^5DCHK)0Jg^AM=V z1SuC3L2WrmdIg0qD7wIz7F<+<vl!fkkZ}}ne+g<X_-;Z_c?=38aQK1S@entI#6jgW z_>?NpeR86p))UB$pezffK?Hbt6Ub^12V7Hv+yF{Cps_ws+mi*9S;77Qos=XE*?kYT z4Kim2&2_L{UhJTl0gHi3Kj;W5)B=!SzzGd>g);nTYDipwA_gP}4n0sjK<Wo@9~ZW= z50Yv@z5=&8p~k`H{yu<K=77TodL}GneqR*4LLQnzkz)}wt^qa?a-I-4HbH$Ha7aK1 z$RHL>5ac{`J3#kGfcGzhQZ2|tMNmlwDZ@bH`=Fj5*eqxeRQ%ZiiwA@VD1SoZR2Q69 zKz&ANID*F}LE#CGTcnf&YHJ}<4#*Brcz_EUP{SWmRDjYWNDAf!a12A*9gy&a>@fwo z7u=cy)o!48gVe>4wgIRm4ABQ0L;;uOAlsm~#DVJ#&^|UuSqLgSz+*p<m9U^5GpKEX zz@WGWxd=QKgRBQSjsuDnkS-7gHA2BVQy|(wo&m)JB)@{vCWd~Ht&mnSL_esD@B!3# z1Tnx?g9%WwW<~1DvNM3%_u$=}Fx@a3)XE2&045;DfXx6C43IrCSlxke3&i;#y<kJY zIzd4Q%3|QXcc}VN+<|NgST~X}C<fv14?;hvOa+B5#5G{Ezy#P7D8azMz=kDUz<X3- zA%aRn;|OdLn1IF-+(6W{f#f$q@ajfTod@1822Q=8HWFx>1zZ4uOakqUM2tCrN+Iyh z5s*JXBL<LhKghjh;BpgGf}t4zjUz~!2c=J8aGTZ?R0@M;K0r5ug7#=Bi-JZ@!R`Ug z-asZ3Ag+QYepG`Y9WH1;0#x3E=J=uO*})|+B-et*3BV-~Xf6Pf|3PsNawyn?;5rQC zZ*Yi#+6SOPhX=6!5vU#pojRfnD#yX2YtS8pU;{v!L9J|X<qYYlFd@YsKWJpgSX3M| zHVEn)i7G;RXyE(SL9T#|%Yr9>!R0Qv?*J}$!DAy}cZ16XP~8q~g@bZ07=s9CnG4nj z?sr2<4@eq;tg?fUpjI8UMgV&oPC$x6kZ(XN5RJ`DkV`-%A-FXFsq;YL333}q8{9Mq z1LQT3>tLk?!f)X7Od)=Qj0b?z9%yO;q#jf~f_m@>^<Y0i)PvF-$Uaotq547gf^7vc zz(zp`5Et1$;IPFABh<b;#6ysfz@`^9EKv1=-2n0es9ph$6+`;}ptcpr-C#e1N__C_ z1;{s0_k+e!KyC$NP#F(qfKn8wj|`dTWMu&Nr64*$Bjn(+9~xGWxd)`XLm=+@0NSVZ zq2lifgj#TW0u&FRJyxLH0jfhG`*uEn`(I!^|7L*Zv;HZ7^?}kfEdPS*2}MyxkS<Y0 z(8!l4V+N>g&6okcWDsOw#ora6yo%&TaGrv?5#)bRFBZ1@43tVhyR$RE2WlbD2SIlt z3q$I4kbB`X?Vv~kPmX=~YXDke3hCE^`-|W{GbrCOgF+cxmxIC{nkPW5SW##mh6OUX zwFhduLmDEWv0{)E#7uB_fyyn=xGl)vpj8~88+9N~fXz>V%2`+#f$B2`kPATjQ9-SE zNHO+j2WSieTwXwQf$C6XJy8FGcFlt^s9y|n2<VUxkXBGgfleau__G6CZ=vc1m+g>J z6=W}{Ol1X+K~z8*CE%e5h#Npt9AJYX?f}&}=w^ZI9EfRPw?NBlgoz;iC`LkE2io=i zX9wu?0ccwg6n2pM9khoP9ExDK!AykFXl{ix#zDudfxHebouLHMy+fdKAGB^wl7T@S zG*$^JX+eECP&oymO%(;9#R@1}fm-tLLKoCMi2xny0BVmw@)2mBNDw^70!pQzK7*+u zc$O99Iq;D)FgN@IuWSaF5771_q)z}!Z=hZa$Tc7~xYPu->p;03sa_I=_zDz?=oq?l z6DABQ8^A>hq)Y*)M`y4b!SkOWvq7z2Fb#1vsC^3RFM;d_xdqGzm7kz88>AM(#$2Zh zvyv1V(%%A=b&&g(K%*8Q3?7>Y`yS+TP{{|H4+Z5ii0Pp017X2PP=15Xje^fx1mz%D z-3#i6fW}QgB^;=|57h&%NnzO+G-in$xuEa@oxcKc7kHl{w9f}x6#-tu4QhD{g4&p% zo)EOf4(*qL+y`n0A$7>W_Z@@#sPH?L!6ggammq(G;{g=nqM+GD`1ljJXaNzR$_~_S z0NV)i2FO|v29@@(^aN_lK<@8_#v}NIQc!vX-<1mrQ4j`c2Um2U-Me58ga9oZ0htfx zf!Z{%e8LU6a|`ThP#%GdeL&kbka2otaG4G=9qLMuA&{YZa0ef3A9zj!oPR(=rJw;v zaIpqS3;#P1VFbPj1XN>!%3UxG?gxT+;L#~iNuVqWI#veOkB7J$6#5m=rCp#(6&gU0 zFk%8_Sn$|7Xgq_#9GvUHEk}@_Kp_ShQ-mfAs0YACg4m$e9%xbuls=$kls>p_1-TD| zL9PSGDJU*MsSO%3Ad|ta289lI90F_{8iB9}O#)&ccx@zjtqLTyfXo8dO_25~q~3<> z1g*6I`5jyUAj&B4TsFvcpqK=i12zS00*D1Gq+oFcqQRk90j?{-=77r)d+@w3q#ObH z4CH%|FTuVCx4Xb88P*d9l^-DUK{kSH2eEN_7w!kRYamgJ%muN*X&$=&4YFSdRBwab z1abjnyaPJ20*V==du3n-Lac)N5M(CApWt)?ZWDmh34F8?>?cUPBJK%*m;nnZC=GQR zIIIwH1};TFWhl694H^jm@5BM+Zip$6Fab3o!F^0{c!S*mIx8Qv_ZdE)3`&P+Y9Mo| z@DX^BS)e^lpiBlj(hj3P#SNNc2i5qXmIga?JRPJ9vi#!D4iFpCLjm<Yp|vL{Y(V4d z2N;+^WddmJ26QeGZ^(nqf_M@^g0ceW%mai7gayi`$o7ES6w090Zb&f<N<(Np6Nq`> z`WjsC;xGq1Cjw2Wpx6MVP89b+%mG*bAj81n3~r|}LC${S2d%3F`5bi09B925C_X_W z7vNe1vdjP!?2v=zK*v{sXFfn_1H5*I0o@$%iEW@Y9fF{KKX^|NXaH9Mv}gbn_MmbO z)fh;b08U?sJPnQkP#y!VrUBJ@pgJ3rx<SDTtMWi>kQk_RfSzvvX~Tj0Owc|KXdV|l z8wXmQ2_C@$wfLcJ4{-W~mXRPfB<x@;5Dh8Mp=Bgwz7JH^g4+~mV|$=hF~mt=M}dM3 zlo4UWc;LZ1a6W+f6;$%0nE@(!KzR#f79=x)%mSGTGX~Ufg$y==(iH=E{Tegq^jOfW zD7XbE3aSS{;RVXy;IOGcEagGeXpnFR=Sk3=!JzaA&T-(<2ArlKW`N=v<Nz23hY@5^ z7qn~un*rKG1?i=LLK_rD&{hQ~{Z@cZ4hN56fL2B+K*s>V`au0KP&*Y==YrQQfcCdT z+DM>2RRw4>9ODCU-a^C$Qv~SF2uS-1S~EfH2A}5(nOQ(C89{*vicPSIpiBUc2iP2` zB4~9EIK_eHD<KUD@Ln&li$UQ8YC}Qyq=Q<Np!pH7O`!G-xD^XF9CWf8DAqykC6EB9 z%Lkf!0LPIaWNa1G8UURy2WrJZTU`*Vz^w;H@X7ocAkD~O3n_O&=>jyW3#kE+?m~ft z7N~~>S*`|-DX1eLVFqebg7=X#u!3fZz<MDu!3bX62I{T;1zpkzZr_2!1>|1v`Ug<C z0%|RS+F#(f2PJCI&=aV10EZCc1DIZD9flOA;1xllpt~$Vt^|cYc)}FqI>>37aDPJ9 z#ei}x$R@C@pfE<B-v%vqg4hby51p4}0J|5m*AIMFCMXs`lQN*Yu|P@xp8_-vLGy~B zGbun}07~nEpqVmoEPz5B<T%C*@Nu7@6|JC72S^{NT@Q5|I6Oe(NZ?)!s1AjUW`U9y zbnzT020&FI!foKQc^Dw)FoMzxcw8H7Gbk^DyaTfw(zpaKCI(G{L(?eaOc`U)8IWLi zLrQ7rSR<%d1uyRcnFXyIq52@{71Txno&5+}Q3}GKem}^$;4}hS6bv!}gkgCSv>p~z z|3cC@X#N5;cLUza1W^uZJA%RmlqZ-y7<d^#X$q8kL9HiIMYw9v?YIX(0SOvR0M%bm zcfs!3fyOeVEdUztgiIQMN@MWyv5F6%{mY;OEg@+b6c5ZEpxHdo3Rw^aud@dQ9>`&k zW&|Srg3nC>r9M#m5L6O^N(*p02CY#9AM6C`+knFXQdEF~1vHonH3JfFtl)JHppXOA zjNs6O)*TR?%m*N@1P39cZ4T8BKl2h)x`EoIAd^7lDmWE^Tm;Qc5QD%ZNCcd;VTk}* zR)O*_WbB9)+^Yt)7(l0CfQsP{pk(s_G{S`lCrDn0ry20t0`U5DP;vvE4S{eY=qwsY zTnmEsqk~F?8K9yBTJVC3C5X8oH$d*ng7hOHF#t-@ptJ{y2G9yGaKQjQQvhE6f$|+U zXnX=x@`3gffz}8@8WNzBTNOYj9R6{DrV*$;pmYT)mq9bopj-`)HIR!yNgj0W11P#c zX%U(>Kx?-kp$lrogX(Aq52Z%|iYbtV;9e-Gs719`5ado!+5};6DTK5_2^7|#AOvAh z(Px0r3(7Z;Ghje{RM^Y{`1~hO4FRiFKuHx8)u6-%@(E_xg3~NKMnLKzsRE=66fvOm z0rNLBtYH0Qa2*D2HG%RX$Q__C0Ao-R2d`fO<u7<ngNj0ohWZ5*mXLS`g#c*O0i60F zp#X9=xTJ%`TLw~Ef}~Gz&?rC1W{`TA!$AR1@c|UW&@upGKD<l>%}zu65TK9+r6p+j z2AYtCrd5dfp!^6+d5E+M%Z1>HIjDMw9gv-s;2u4wO#(>?U<<&Pse=L&T+l<}0pxdP z@O_@3lnO58!8I;oEd<C*(A_ZLkOUb3D&Sxl7Q9vpyuSrJe*$gqgWBmJGeG$k()$Ou z_d%fv342fof!jEsyaej;g0@b8RuF;bH6Y`M@OdY2`y5osFg^gy#eqx!b&etJK~P?U z_VqwLM$pbNP<s$m-WZF*`g$N=Kmq~Ox&u1{+)M)%H{f&9D$veJgWV+uI=&N{Y?1OE zsJ?)<i$L~(mym-z0m^FN?lx#W6zKdR@HwLlpm{)$3t;y(gWG|Swl!q*4rCUnqy)Eg zK&?<viw@L(0hf0m2FT9{3?APB#T_`zp|ii>{UG3$2dEMP^&i233_ide(G~~W4Q{W1 zX7WI-4`onGQV^Q9Kp6y@P+)UZpgaeulfmU6Xgw#mJq4bX09B5VB>|8dqrf2mNn22T zkTL?)mILok1hxM`EedeFgM0*Pu!A_z@pVM~2OVRTgtj9==@NuNH8gl80u)Z**}Z=X zAkTta0Wu5HivU$ppmrNHk3;Kj6th5S6O`jY<CI{tKx-sGaSbYTK(Pr*=HT;9K+Z$l zLkq2Iz;Om@NmqdGrGT8U2yPa_(g~yu1*wHWWgNsFMEwB{Do`gKG>8cb2~cecE-#o5 zK<ZMEUT|9m6mp<GCU_(e)TaZtI>35B`4N<|L5(?(!$IvacyR}9Yl6)KpIri)c?HKg zs4a)xM0hm;avEqsHz;7iR)Gj;c!JU%<Q_ZFJQpb6fl6Uez61FW)GG(KRX{NSsfNJ@ zfeZj)Q2v9Y5$IWzpuQ9|CxG;W@)UH28RR{Xau5b-MYSK%PXPG`lzTw+A82e7v{ng} z)j@hdGXS8h02;vrRlT6f1RT!bya1kK1MM~d_0~ZrELK3zu>|E`rVF514NxBfx()_3 z0E%*+KX_~pvX=tXQUUcvq5DHXp#UlSKz(|se?Y}1xZ4Es5GdZj<vz%jU<?vPN<Wg| zu{u!60O}`$d-k9f87O=pxex3m@GvS!C(Of${Ub~fp!Lw;em6XPL8IWHv<NPdLAHXN z0bb$*&io)3W`K%Um^&c#11Nr>btAY13-KLjJ^@mYf()vt_y9g_1d<kEbrWJN0hHT7 z`5!dK2Oj?f#TdwUqTq9`z~vk`<AItxpqPRf3ic!{y@2CPkbwbW7A#+ZVh5B}VGS`* z-hxLtBF%v2dO+vdfKmyl?EtE~!6^r-AJk<7l_4PeXF#rpf?5tvFW@n5@ERnD?VuA3 zz`YYtYZuuZcpgIb4d}cw6c2&T1p5c#CeTPWsLlrU)Ico?(9Sb(w1V6)1Kdx8M-?cI zV!8JiR^Ng0EXX6^vrIwjts&>Cf@=y$$Rq013~(L;2R`CHCeR%-qM$RoAa@6WOCnHA zf!rnvYV(2%evo%SMKI)698fw0&7^?B8(KGm&$0xsi2$uS1dj!PW{^c8yZ%8IgF9NF zel95NLH2_h(V(@Cpf&)5xv8=!s09R807__}ObRz2yv7o|M-!wDww46c#|2jcSd0hF zH-XA7u)737t4QH%P(Wibpi^)lx3z$8rUe~D47T~-3{V*YvKJa>@V+J}S3z3rp!sdk zTs3&~4_@#=d<CknLH+`r%L(=uXarJ{5poX@ECBz4#v`EN23|)En|BB0H&9s(b{%LF zJ;+c{9)suvmmr}2GqPUr%rN-OK2RST)ZPU3Ex>2<gIXe>);VbAA9lqUXdGq+$XHN= z2;oOqI|mf=p!RA8q&)&!p9b0Q3az7=88ks*X)Fqwy91RJpc(<(mjKmR(7p+%#DJ7u zAQym=4!A)HFSI}b2dX9@NghOka0Mt$FfdfGYJlU69khoA<S#{0R*hdbKp0dzgYpG< ze71u92e?hd4m&9fl+Hnz{l^au7zULLpu_|zC;xY_YC!wGppqMmL2KYaH4$j6gy{mP z(fLoI0+h-@14p2C0XPUCW3c}_*ncn|0PXl?1?vTuNuV`_q7V}xB=Z3li$4yaQ~#L{ z{Mk|Q`vjEf0N&jV3Jp+x1(m=IOpH4JJDC4~&m;%$)CGkmI8?zs8&Eikn<|Pj{{iJ$ z2nIK$L56@ZsPM)x1FRn$6CnMdwht&&m}DUOKYaKA9!~&ihhUH)ure1MKH&B$$R!~E zflULu22>Y;Oa#ROi^cB~5ZxFgJntaP0+|6GtAM)-6jC6UfyT_CCj36ZVgY5sDI&}Q znFPYHc}9@lKMpJ#eg`0#4H5)ZQsDH?{)5?r0W>EFDS1J&#HJt@f_w~0$>4Pn;Cujb zA*2lk^*5^q#3T^;>jsFds9@*#@dLubMnc<V90ibhYDnpd92OwAgRrtFI|s~wA3wn9 z7tV)@L(SrLAZ!*V)NvIR-!!;Re6>LEQO)8LAZ!-T3aFln&k?*cK4l>IP;scc1VCrV zgVPQuG_m`UAF8FI;zNf(#`_rvK2!o~mhc9`?h^8V>Zy3UL)hZY2?QT14mC@xfv{Pk z5>P!AuU?38y!?USL&c$HNd^!$OZ){?PsIxfi5t%~5PYaO)GQeV!e&W<chQ0LJ@t@Y z@gxGl2Z@2Y5s<c<+y}yD$#y{XR6MGXt9aOf;6ufs?otG;PaqJF3ZNboSlj&-iXQiN zAoyTGcwSSPK-gW%@Jw~*hKj`P7YIJ8S?URd%~FMD#akR|FK$X8_^4)S84xy0175;i zx6s^i%>%(lHA|O)uvyxm>JF^!N`_9y<q8BJEC>%TeNelVK-lZSJAD^t=to>wf#9Q> zWwd~>yA0t)?zt028fR}H_^4)?5>bAcz{{)CKTLj{=0NaK&9We({4#@=Une!pPn@to z@KMdOCZhbZgqL5(BCKW{%|P%`&9Wt;{IY>+si-*IVViMi27(WjfaW0wBFZm&c=>f; zhl9ob69_)4S<Xb1UrzAyYwrtZjy*pRd{ndCh$z2Y;N{nD3D+CDG!T4Lvpk6?zue*F z*A5Sl726{ad{nc%i73Ck;N{oW3h#<79SA<ES$;&6U%v42YtsrpkBvJJd{nani73AU z;N{o)8-Wt*ULg3WW`z(@eg(nHueBV(FV;vP_^4)u6H$JJ!ppB!7GXP9dLa0yW<?QE zenr5`ujLt$9m^^Zd{na#?Htft2&nxCYVm;DQ6LOzMS(|0*g3#Wbhu}shCnH39zwKp z@S4SW<EsVNiEr>ofEog&pk^W3Ie5+DS@9`@cgAOU;(!_grJ!aZ+Bta5;;(o=Lm=Y= zJkdf8fl^Sj5bYeiW(j$`IU#KE7M_`)hCnH(S%`KHUb93cUj7i{cm>boP(z>;)GS0h z2d`P;FP>{i+;{;me4vIvDX3Y9b`D;%q;@=skY4c=UMNBhfl^Sj5bYeiX32Iu?2xN? z1l5a9LCr$6bMTs_5OHsZqQ`xBC4y=i)GS0h2d`Pm8n<7lNZf%}ZdlDiv~%#9rTXKh zgxZT+@Jbh}S%`KHUb8e#T=US}aUI?mz-ktvorBjb?HQLVbULoU8!1@LLbP-6nx&U< zVTFFgMR?(Z)ht9i2d`O%7H4l5X`F)>idfA;v~%#9Wx{cq!{o<l{N)#-orBjbvl}NY z%uk%eUw$FlIe5*oTyZqRYQ{1A<rkuzgV!vZibFGOGY;b~zYy&lyk^;Z>_6dPaR7h$ zg=pvCHOon2&ktvgz4*&7L^}tsSuQVjX}I3ljlcXtv~%#9<-TKkgvW{<_{%RuI|r{> zUL9LHyeqb%CQ&#KTIL|yIe5+Tjo7%u&tnt*@(a<<!E084#<~}Q66^7oUx;=NUbBLJ ztdR(Qu@-;%g=pvCH7oSQN{_G|tMHd!h;|NMvm$0JtBCAaj=%guv~$qMIzTH;AagE= z@tleu4eSl@=z}ugV_S%J4sNrM#&5oE;0k~m0jJ<*A=){(%|aT#`SgGnX-X1e9Na8K zI|sK}NaHsj1o$DNxDaE&B-|`SI|sK}NaHte9fTlPUxH165OA{)?Ht@@A&uX>DiECj zF#$os%|f(uaGQlRe)D{RL;}JT2n%i&qMd`=ETr+9Cl{m*ASNJ4xLJsH4sNrM#%~@m z$T1*Hfw16aA=){(%|aT#xo@C=e5?c5Ft}NWb`EZ{kj8KBBq%Qc8-PT>%|f(uaGQlR zesgnzS^<(VU?$uwL^}t!SxDnI*A8epfDJ$*;ASD(Ik?S28o#;xK}P_|7%&rV7NVVl z+bpE<n~Ms1pv5m>GhhVVEJQm8w^>NzH|GKjH^9t*(Qva6?Ht@@A&uXhZZN@Jej(a9 zxXnTuzd5nN9C!JJXy@QI3u*l3=mRU<<rkuzgWD{m@teZ}Hn__#L^}t!SxDnI2OR8i zmtTl>4sNrM#&7l(IN>h85bYe?W+9E=>{{T8yZl15b8wr5G=8)Ff(P#M3(?NOZ5Gn_ z%~l3)+~pUdorBvfr16_g2EMq<FGM>Bw^>NzH|rAuaF<_*b`EZ{kj8J;ObEtZej(a9 zxXnTuzgc-840rj3Xy@QI3u*jj*@sBn<rkuz1DYKJjpu;O1dZi@Fnm0xg1zAfMgajW zbl_zUqMd`^EUtjB7zG!)S%`KHezSNLK4BE7=w>0>Irz=u|L_4LpwP`iv~%#AC3N5| zMg*dpg=pvCH%oNFD~yDNZWf}QgWoKPgy$Fu9^EWNI|sj6(gsg3G9tQJh;|Nsv*Z{a zVH6PPW+B=+_{~zda37=KLN^Q1&cSb%@`5`U1uD8(h;|Nsv(yT1Vife~W+B=+_|4LE zxQ0;yp__$h=ioO>N8mC>#fWYeqMd`^EWHO8F)D0yvk>hZ{AL+$IET@YKsO7~&cSb% zNyBM^<rkuzgWoLkfD;7EFGM>BzgboaM+uf+h;|Nsvur*bCRlzU+Bx{mvOjQuVEKh; z=ioQXX~JHD<rkuzgWoLIgk1#7FGM>BzgZpz+X<Fmh;|Nsv%DF$5-h(E?Hv4O`CiyW zu>3-_bMTuLuwXsG@(a<<!EaV@!5V_)7owem->fi)l?2N#L^}t+S&;(E2$Wxp85{+O z`yn_AzE1$HTZ62LV$9%nK*(`Bd^vzD$0vZ0;}iG@KGOiEPv8MUPT;|N@V+dVobU#O zobZM>0mx>GH6Y}~8eTRa%Si?x<Rk;0Z9tZjQ9#JaC_H|EEGPE?At(3Yp#ZX+;sJ!5 z;(>b($Z{$Z5OOLLZWkcSsV5-h)DvzjK$g=oK*(tsT)luSr^|ql(`C5KfGnqf0U@V< z;er9OoY4Y=oY8`_3CMD$NbzS{a0(^<ERf>Q!r?ed{8=N#pS8ddl=!nnia*;2hfw0r z0V)0*Hta`<KWC))b8gs!5`S(;@#hw>6D9sUk>bx&VH-;Pc_YQ2_lGSg@#lvWe|`rx zqQqYyQv3x@ScejSAxQBTlCT;j{=$*sFWg`Sa{N7D1+6Pl2CWzdt?>lyQHIf=d<g0n zz-dv?>R9j!T{r`@dJtMm!zs`jCdLQgzJ@V2b8(svUQY<J5oSD!U0@D2`(PGB>;?Br zoblO9(0;7$09%jjFt7kV_n^89+{Xzf#$80a52ri9PD6JqSdbX^qPiR0pGqdh-6Xpo zuRp-f#^x8W1S$SO^%uBrS4@t-$o3yWe}cV)%dcQba{P<xZ*V`fnG%0f?0=%d0qk9T zVF6Y^NqC@!3%E}`nHu3jweTT2oWMarD6GJWs1aVM;RYT*SWL}uqk8y}6pr9vA|@=s z3aJ^MsNo78OW90|aHU!Jk{r(9AS5=d!HQ`S-l*XY9(OuS%W$W8_>&b6;NT@CF2EXS z86T+e0v^M=Opkb>Tl|n6PvD>@HLk##=n-G2@dh3*eN4}IqkH^O6p!F&Atx@u8tECI zsPPIOyZ$^7@j77fOL07dqm10R25TOO_(qL)&^!Yp^FYS?fX6>o(g8R+DM<@p0|qiZ zpr#AZoDrk&U`Q8(CVfyXoq(g7(zF6LWH6)`)N})yA7sQj{s@c5fuiB#p0IcvXc|8L z3W`cfFle_rWHUU4B={OuSbPo^8Zr(Hnk#0+JN`US=?Z4=pi5t{^i8#N21=t;OKV`m z22=Tkp57tp4z#X-k*N6xSUMOedeEjnnxsQ;@}Wi@3Ni?W2W9z(Jzav<s4$W=|1?nP z5@zq9PoFePr=a9T)p8SLAP5hM^a@MAkn$9?9*U8y`A1kf8Yp_mq+gn*V{o#gcDV~O z6oiLVdPYsxptX676wN;mRJw-QJLJ+g&C@wJNzy8<gAE_d?O#~>#zjNQbI>|cMyjko zfThEMqT%TYmac{xP1SxLD0!k|nzUh%Eg6#KKYIRv<O|T6Xhy28KN+Ze0ke0==MPwZ zrg=UAC25-H6|f~kx&FYOZ$Rt+8ELZq2$qfqiXJxkhpzbuoXlz21_N0D!^5Kf!Je-` zdtn%9y8djS@)gY9VVA$?p3gwpfG+hG$RZFPmiY~q{~+x^(7rK7x~xBh<%5Buhi(2F zkbDTv9`vgBK$d~<u+ERD`4Y4Tlaa3LPX{Vr!t5RP`E$VXDLCuUJFkK*9Zvl}SpLLC zL+Vq|erLu3S$_=6M*~HVnEX4S`52t71|lzmEgnhv8J5o>`5JWg0ONqJKOd-k4YPN| z<?jK{=in?iP<b6}`AE#~$oU?8pVS2gVbHBrpc_NM_tJoFwglb5Cd!z>6al*6>F){< z3A)1#bW0NWVo%UrL6AG9q+xnd^@HyE1Ko)PHUn-9+$6{aT3{nV<qtyz*9oQz49du6 zVVZ|-CbF?eCd17JF(7A(FjVl)V7kDdjcz`6JFwV;W)+HUAZtJY1ICcMuox-?GMFwf z7-O>!x1HGS#k3mDb`<+T9GIVlEtoDaSmSa7es|z>3s&b~xCrD7aCpE7Sh$FBFkN79 z#^)x&?!xCbT+YMdLJT*81Ylt&af9gsgEt{}66aO|?#1I|tgZ$*6Bev+8Wz6NE0``Y z1QX+K;@wWz{rJ6r!xvcn0TP78fm{XC1%_y1{X&|5i1HIbZ{hSA$Q$suL1w_>NzsGp z0z)z>ek0v~#QKpKZ{qPOPQQXAU~#D;!E}Kkn^gaj=Vuc9O{CZH`X1y><S0gA!{S@* z1=9tFVsiXXei)Dz4#WoqfzZGk9w13rn$X<Abb+Co+%TamTu2TZl7fd|2!VnDC7qyi zVChDugXsc8GbLd}c{q_1R-^_O5g|q}%s>iYX-hwX=>kJHrQt@Mup=}4$OuBBLJ<^9 z=*bO(4@-YW8cY`$CQ~CEsTY>yhbMVKN@S=K6|Nvfurz7{x}$eCwZfLV;Y(2%lO4=N zhcqY{G14qH0a!XV2lX!(Q!}ioAKsLPIVHhPLdX*x_8^6@v~C6Jf3K!?_|qm1s1Oen zMg>W+0SabpNgt~iEI-(S#wRw@B0gvrC)9}-%A<zF*dZx?K#E~`#sM^bvzu0NMB8|x zT3k^jx=4;OP&8o8Kez;8`N|nIzIB+EaYp-iqh{PuEBeTYL6YMTqyd)qTtVZ1r)eFJ z^odI<$0rq|l&n|<MH8-Uh*KPvKRrO>qnGIsuk?#s>c=m2qnONCCM%vnnqYa@8#I1= zn_h8E-}t6UoKrp8$&PtYG~&$5_#|NY+z&Lq{+OO|PyhI*RT`j0GN2$OkewDl8ewHY zAZY&KHNDfrz@!NprVAP+4T@3+D4OvV33%mT<wY=Pe&+K)q>q71BeYE?v`Z=!rWT6Q z3rI7pObG+cU;Q4aG&8X2hURI9rpbrm6a-2Jc*_++lHgTTd<^TrtDiu(K$<e-K-r88 zoDAEbY$gVFh6_+OGlMFl3Y5*lz{wZ~WwSDHGd4ikYz&-?3mBLg7+E+O7#Ok`7@%xM z1|fz;P&N|-AHxACo0-9j;Rck=!XU)x1!c1`h%shB*=!6#j6DpV3@Hpn45<vM48;rz z3?2-L48;t^42cYR3_1)73>ge13?&Q&48;sq4EhZE4CxG+3?&R145bW740;U74EYSX z4EiW$crxTMBs1tU_%h@(<T2zkBr_B-Br>EilrS)OrWB>77Atrp78fVx=_q8BloS+O z>FcLwmSmJB=_Tjq>O)j`<|XU<=I7-n7bT{ZFkmwYNj8Wfl_8y>lp%*9k)eozAt*Jy zG$*l$fx!)Ie+if_Vn}C5Wl&%+V$fqSU{GMNVo<<hDn#6n!GJ-R!Hhwd!HB_t!3eb0 z%Pl{z#4W!lJypR-&p^RS0dBj3m4cyxu9>cpfsrvGcZM;fG88csGh~9@rNCeeb_+vT zYEf}!ex8D{o&g~fm_W&if#Lr`1|tXym1JuHmtRZ_%#2Ko%#19Itc;*k!_LUT$jQLV z$i=|VAiyBVAjBZdAi^NZAjTlhAi*HXAjKffAj2TbAjcrjpunKWpv0ie$j!*Z$jivb z$j>OiD99+pD9k9rD9R|tD9$LsD9I?rD9tFtD9b3vD9@<CsK}_qsLZIssLEi^sK%(y z;KZoGsL80ssLiOusLQCwsLyD?Xvk>9Xv}EBXv%2DXwGQCXvt{BXw7KDXv=8FXwT@t z=*Z~A=*;NC=*sBE=+5ZD=*j5C=*{TE=*#HG=+79y7|0mJ7|a;LaEmdNF^n;sF@iCY zF^VyoF^1tDV=QAFV?1L5V<KY`V=`k3V=7}BV>)97V<ux3V>V+BV=iMJV?JX6V<BS^ zV=-e1V<}@9V>x35V<lr1V>M$9V=ZGHV?AR7V<Te|V>4q5V=H4DV>@F9V<%%5V>e?D zV=rSLV?W~r#)*uR7$-AMVVuf1jd42T491y^vlwSH&S9L(IFE5Y;{wKojEfi-GcI9V z%D9YiIpYe(m5i$xS2M0*T+6tQaXsS(#*K`d7&kL+Vcg2Njd45U4#u5~yBK#f?qS@^ zxQ}r^;{nEljE5KxGag|)%6N?NIO7S%lZ>YrPcxojJj-~F@jT-N#*2)X7%wwkVZ6$C zjqy6;4aS>{w-|3T-eJ7Uc#rWu;{(QrjE@)}Gd^K_%J_`&IpYh)myE9%Uo*a8e9QQb z@jc@Q#*d7j7(X+9Vf@PYjqy9<55}L2zZicr{$c#f_>b{F69W??6B83N6AKe76B`pd z69*F~6BiRV6Au$F6CV>llK_(-lMs_IlL(V2lNggYlLV6_lN6IQlMItAlN^&glLC_> zlM<6MlM0h6lNysclLnI}lNOUUlMa(ElOB^klL3<<lM$0KlL?b4lNpmalLeC{lNFOS zlMRzClO2;ilLM0@lM|COlM9n8lN*yelLwP0lNXaWlMj<GlOK~mQvg#SQxH=yQwUQi zQy5b?Qv_2aQxsD)Qw&oqQyfz~Qvy>WQxa1$QwmcmQyNn`QwCEeQx;P;Qw~!uQyx=3 zQvp*UQxQ`!QwdWkQyEh^Qw38cQx#J+Qw>usQyo)1Qv*{YQxj7&QwvioQyWt|QwLKg zQx{V=Qx8)wQy)`5(*&l8Op}-<GfiQd$~28>I@1iMnM|{oW;4xUn#(kgX+F~eriDz4 zm=-fFVOq+xjA=R33Z|7ztC&_ZtzlZrw2o;#(*~xEOq-ZCGi_no%CwDXJJSxPolLu! zb~Ei^+RL<$X+P5erh`m}m<}@?VLHlmjOjSj38s@wr<hJNonbo5bdKpf(*>rBOqZB0 zGhJc2%5;tCI@1lNn@qQuZZq9sy32Hr=|0l~riV<Am>x4dVS38+jOjVk3#OM$ub5sl zy<vLG^p5F0(+8%HOrMxOGkszD%JhxtJJS!QpG?1)elz`H`pfi>=|3|AGb1w-Gcz*_ zGb=M2GdnW}Gbb|_GdD92GcPkAGe5Hcvmmn&voNy=vnaC|vpBN^vm~<=vox~|vn;b5 zvpll`vm&z+vof;^vnsP1vpTZ|vnI0^vo^C1vo5n9vp%x{vmvt)voW&?vnjI~vpKT` zvn8_?vo*5~vn{h7vpur|vm>(;voo^`vn#V3vpcf~vnR6`vp2I3voEtBvp;hHb0BjN zb1-uVb0~8db2xJZb0l*Vb2M`db1ZWlb3Ahbb0TvRb24)Zb1HKhb2@Vdb0%{Zb2f7h zb1ripb3Stcb0KpPb1`!Xb18Efb2)Pbb0u>Xb2W1fb1icnb3Jndb0c#Tb2D=bb1QQj zb31bfb0>2bb2oDjb1!orb3gM0=84Rcm?tw&VV=r7jd?or4Ca~4vzTWy&taa+Jdb%k z^8)6D%!`;8GcRFY%DjwuIr9qUmCUP{S2M3+Udz0Wc|G$6=8epom^U+TVcyETjd?rs z4(6TAyO?(~?_u7{ypMT5^8x0A%!il{Gaq3-%6yFZIP(eSlgy`>PcxrkKFfTL`8@Ll z=8Mdim@hM5VZO?IjrltB4d$E7x0r7;-(kMXe2@7)^8@CG%#WBKGe2Q|%KVJ^Ir9tV zm&~u2Uo*d9e#`uh`91Ro=8w#um_IXrVgAbejrlwC59XiDznFhB|6%^i{EzuR3j+%y z3lj@73kwS?3mXeN3kM4)3l|GF3l9q~3m*$VivWutix7)2iwKJ-ix`VIiv)`#ixi7A ziwuh_iyVtQivo)xixP`6iwcV>iyDhMiw27(ix!JEiw=t}iyn(Uivf!vixG=4iwTP< ziy4bKiv^1%ixrDCiw%n{iyezSivx=zixZ18iwlb@iyMnOiwBD*ix-PGiw}!0iyw<W zO8`qCOAt#iO9)FSOBhQyO9V?KOB72qOAJdaOB_o)O9D$GOA<>mOA1RWOBzc$O9o3O zOBPEuOAbpeOCC!;O94wEOA$*kO9@LUOBqW!O9e|MOBG8sOASjcOC3u+O9M+IOA|{o zOAAXYOB+i&O9x9QOBYKwOAkvgOCL)=%LJB*ER$F!vrJ)`$}){*I?D`}nJlwdX0yy; znaeVdWj@OSmW3>fSQfJ^VOh$ujAc2?3YL{Dt5{aEtYKNpvW{gv%LbN>ESp$1vut76 z%Ce1RJIfB1oh-XpcC+kZ*~_wzWk1USmV+#ZSPrusVL8fjjO94X36_&Ar&vz2oMAc3 za*pLZ%LSH;ESFd=vs_`h%5sh6I?D}~n=H3jZnNBBxyy2o<vz;;mWM2lSRS)HVR_2( zjO97Y3znBGuUKBQykU9E@{Z*_%LkT^ET33DvwUIs%JPlnJIfE2pDe#vezW{x`OET; z<v%L}D<dltD>Ew#D=RA-D?2L(D<>-#D>o|-D=#Y_D?h6Mt01cot1znwt0=1&t2nC! zt0b!wt2C<&t1PP=t30a$t0F@as}e&yt1_z!t17D+t2(O&t0t=!t2V0+t1hb^t3Im% zt0Aiqt1+tyt0}7)t2wI$t0k)yt2L_)t1YV?t39g&t0Suut23($t1GJ;t2?U)t0$`$ zt2e6;t1qh`t3PW1YanY7YcOjFYba|NYdC8JYb0wFYcy*NYb<LVYdmWLYa(kBYcgvJ zYbt9RYdUKNYbI+JYc^{RYc6XZYd&iMYawe9YcXpHYbk3PYdLELYb9$HYc*>PYb|RX zYdvcNYa?qDYcp#LYb$FTYddQPYbR?LYd32TYcFdbYd`A*)`_f>SSPbiVV%l4jdeQf z4Az;fvsh=d&S9O)I*)Ze>jKt=tczF|GfZM#!n%}o8S8R}Ijk#KSF)~RUCp|NbuH^U z*7d9#SU0k6V%^NTg>@_IHrDN|J6LzJ?qc1|x`%Zy>ps@~tOr;RvL0eR%zA|NDC;rS z<E$rGPqLn3J<WQC^(^Z-*7K|vSTC|(V!h0Ih4m`yHP-8_H&}17-eSGYdWZEc>pj-{ ztPfZpvOZ#c%=(1&DeE)V=d3SSU$VYpea-rY^)2f=*7vL*SU<9UV*Skeh4m}zH`ed0 zKUjaV{$l;j`iJ!|>p#~2Yz%CSY)ov-Y%FZ7Y;0`oY#eNyY+P*IY&>kdY<z6|Yyxb8 zY(i|pY$9x;Y+`KUY!YmeY*K8}Y%*-JY;tV!Yzl0OY)Wj(Y$|N3Y-()kY#MBuY+7vE zY&vYZY<g_^YzAzGY({LxY$j}`Y-VicY!+;mY*uX6Y&LARY<6t+Yz}OWY))*>Y%XlB zY;J7sY#wZ$Y+h{MY(8whY<_J1YyoV6Y(Z?nY$0r+Y+-ESY!PgcY*B2{Y%y%HY;kPy zYzb_MY)Nd%Y$<H1Y-w!iY#D5sY*}pCY&mSXY<X<?Yz1tEY(;FvY$a@^Y-McaY!z&k zY*lR4Y&C4PY;|n)Yz=IUY)x#<Y%Of9Y;A1qY#nT!Y+Y>KY&~qfY<+C~Y!lcfvQ1)} z%r=E>D%&)+>1;FDX0pv<o6R<dZ7$n9w)t!e*cP%aVq46%gl#F?GPdPxE7(@Dtzuiv zwuWsj+d8)OY#Z1%vTb79%(jJXE88}<?QA>PcCzha+s(FzZ7<tCw*71e*bcHCVmr)s zgzYHXF}CAuC)iH1onkx9c82XN+c~!LY!}!rvRz`k%yxzCD%&-->ufjJZnE8CyUlio z?JnCrw)<=k*dDSyVtdT?gzYKYGq&e!FW6qPy<&UK_J-{(+dH=RY#-P@vVCIv%=U%t zE891=?`%KVezN^y`_1-;?JwIuw*Tx5?2PP8?9A*e?5yl;?Ck6u?40ae?A+`;?7ZxJ z?ELHk?1Jn<?859K?4s;q?BeVa?2_zK?9%Kq?6T}~?DFgi?27D4?8@va?5gZ)?CR_q z?3(Oa?Aq))?7HlF?E35m?1t<{?8fXS?56By?B?ti?3V0S?AGiy?6&N7?Dp&q?2hbC z?9S{i?5^x??C$Iy?4Imi?B47??7r-N?EdTl?1Ah-?7{3I?4j&o?BVPY?2+tI?9uEo z?6K@|?D6ag?1}720;v_rIf=Q6C7Jno@wtgb*?fhi`6UQ0cV203Qfg6rab|iRcS?Rv zP9lWOQ;=GeoSIhxmf$JQ%uCNnjR$E>705|VD~T^iEK1EQ$w)2EEEX)vOwT|O;Vj50 zEe7ip%SkNB%!^M>EXXWL%!$uQEh$MYiciEYoP=FC8M|-_cHva)!fDur)3FO@U>DBB zE}Vs3I2*fg4tC*O?8152h4Zls7ho4I#4cQfUAP##a0zzdQtZNI*oDin3s+zluEZ`} zB@9l2#f7DbMXB*gMTyDTsU;$iRER3bomiZlnHis)Sd?1AlU|fqmKvX!3`*7_kaSs` zo0x-0mDmK6un8t(6HLJ-n2Jp>4Vz#(Ho**Rf|=L^v#<$fV-w85CYXy&Fb|txJ~qJu zY=VW@1dFf<7Go1E!6sOWO|T4`U^zCy3T%Rv*aWK}#awb}QBEZ|iKX(Cr52TBCMV{^ zCnXj^%RD4$Xi_W5%t=Y*%gfhIDoRbvjxR1qOiq=|%me97O^HV@j5$GR5nNDm737!Z zrGS~d$)!a_sd>qjU>-+hY95%ul~k0Uotg)-ou@c88C;-)N^Q=R%*50pP(jR>oL^80 z%?6q2c~WVqxtV#Hd8zUFMJbtii8=9^c`2F6i6!|(n&1pnke``Xl9`ttpORmil#?2t z2ruLj^#ey<KBNxdFG@|%EG|hcN`<(RJ0&%}C^Z%AG=6Y-pPQKnE%o_}OAAtqij#{n z3rgZk%JT(L_!&j1sXU3vr6sV;$d#L!T$B$siKielC%GuU9L(X$%}hzjNsUj*FF`Re zKQC1}H?cUmGzY8av>-u1jPIn&N^{Z^i{jIY5+MPap9iYt5{pVwizKkgWhCaL$)QV> zfP54WDve4~i;BhIv0Yf2S`3aRP`#@NDRIzi!uaycl8kt8(S%n8IK0&GnUS1Xlw6vd zmXlh6*C22LRmEozJU!vHt_YNH5|i@FQpK?wma2?ZJQ?ES<kXbRoSa1LPECcV!4oP_ z6?j4gssa%z*!>Ju2KO^|2W1j;5L5*o2SHU3au8G*+(8mpLou}^1G`J}AT9;RGIkaD z5EXa=7pek}U!W=we!*)M*fH3HA8HWXx5BxlIVG6|IiO++)M5n{a7tK10IIXN1XL<w zx4x91PoXOC_!O!F;ZyAXfGUIg1G|GNAr1mtCxz8lB^miec~B{o#z}ETVo^bSu{=Z< zic})RRB(jikq6tVf?Geri+BtHM<q04V{-&N0Pt9ZC^<y2xh54Fb|}W9mKxX`2+hbi zLjfv}GZdimh)}@hOQ<5aFR?iUnt^dT1S*fyAy9ez4uL9yI|SNLMhUajk_>F_LoOk( z$wM<1PWMCQak?KWk8nR8OK>J9s3CCw@up;!Wr8LL!3_fiEa?WS15zGfvl5y`arz4? zkJDdJd4#{PB@n10xVy1A1i2J|mhLF=1}+1z84lG5HynGx2rUkB6O)Vb^RR^!iUMe6 z!`>`ODoQO&#pVU55ny?|uE<Z%&r8k5;|hoZ?5==jdT@+ja|N<I_O=RC1tQL{6-yY3 zuov-AWhwb3@!)0;wirTJgV$fliADK&c>D#H$8JIjq{s)Wz-9unJofm3mSnh735o*j zu7D~5rvhxQK$gcIrcf1l!xUW&UjIOn6SnjQH3p&puYYicD%1$DJofaK4viOhQyg1* z!%&3XHBe<ZLl;>dd+0({;0;|IYS799s4CP(J4&I2sselHWDpZN7>clm4zvOUS0LEZ z9<n_46a!U(I|raB!0QT}aRfC2ERQ{opeo>TBnhe7;NhQ{o>)>^lp3Fz1?^@*LmeTG zJ*=~c32O{R&@v5fJ7l;v8)_CxmksI;Y$~udMzV`=^!0KehT|;ck>#;F52^xB;>4-~ z&66-SxC&)tdF%-asshpi!BzyJD8Qbepp7$}p@A%qJv5*y@Pr0d6=<OWQ-doskma$5 z2DCkfJA0uhz#bY<CE)CXEt4b5YoKKuNWz4wfDLeg2A!b!1ttArQHCoc=EKH2pa#G) zVm_?jf)<iR1kyLMJoZq4ssL9z*g^qW9=i#}1l)iu4-FnPUqDshZ%AP%!k$85%5WDa zSXJO~5v~*pGXo@#Jyc2{p@K79k>#<Q0B!B#tb~x|vF8$~3f#E_MFCz{;LIgZBf#?5 zV;8Cd(J{x?Y{5{3J;y<n5z2A6m7&FRIW(5=HmNWaVfQDr1BE{oV<^I2aYL2CJ7d`6 zrV8SEoaH&PJa&7aD&P$SXr6+WK9EulT@7Bh;7spOQ^4}j(htR!ocz4>_+n_qh89an z5PxRm=V!}eEr?4%BkRSP719t{6ob*^(2UM6NX?7SmxJg-lS@o1%1lnoi%)@;i)hlI z?rTvoXx;@nq=qJ6oSadf2z3#f6xh~O?4C`-@GOdVpw)a*Vi7b(QDqZzlTx7lXGl{4 znvYOaLF-p=kVB?3a#GWw3ejB(aSe2?1Y8#3iGq^ye5i*}>;p$ZNqIhWJOa&%)Zz?i z_(Lb~5dmAA5ucNvUw}QJ${<byr3xGdSK%1OPOZv_hgkxRTNL*q%gaEL2GnUK<<QC; z61Y%zqj?DI-L%ZIRH$AQv*D6@C<eo)b<%V4OF)Ymic9hfARdHRC5WQDyfQBjnv0+= zOG(WGX-m$@OwNX;N2p9rVo7RIVop3*3K~67sl=SJ)O^sQ46q#3xlp;H)RLln(1Z^} zBev1hoGk2R0W<-E^E$ThMV7~I0<`0T(*$IB>?T0t38x9j^4Lv)Rw6h}K$eFFD{4%a z7Uh9T2WXj!mPitz{e4K~40QpDeyl3cOo4TbLG2K1A&o2#wFAurSoaH;3CQx;eFDlY z;CV)HNI`8u@d>I5Z0>~h6+y0mnt<j`s84X#b;$D2oPlNnD9>Ow8(JRX9K1!A$KKL{ z#TD3j(BcXu2v8KLL-I13`(SO#{M_8c_@vaF{Bmq1Jf<q_roq~m_)Wu9g+1&l^V2g> zhX^2T1CTiOFolh@fMOI|j3diyVGR#xsH4_W&=MCVY+>V(rA0Z=phpW?Xxjr+Q()7N zBn=I5H2u)>3U4b3T@7}VprsC8lhD;*PjS!|BBc0$dK)EFP!wSEHnb%HT4jPw8XEk# zq+uziv?vF=1EB!|slBi(fevUxlwk7%iUMdVLW>)yFOm|Aw6Ufls5?_K6VvlSt5hJC zVNbKjbuZL5lpsZx$L=F&OBG@Xv>HZpa4Bew4URSxq{b-2ZZ@>SfS8Rfv``daHv?ME z;x+?C0rn7u1W6&JaRSOy@g<3wIncTRA_;9mpsT^|Cg=zQZa1MQz-|UKvEw!aMFF1L zIuY8GMDa&rPC-T@wggcEi7>D{wmbw)4xm7Sma8b{g1Z5*&IgJNG~wgQe9#gP6z<rp zg-V00g(fr<!(mO|_*8731$SGZp^TypEP;r1oE}QWrWtN0*w3jtki3dwKAIAQO`2E~ zC+FmYx?X5{pnVt=V^i`=a#D+n<55yJworq%AV8spErCE6oTa0-k|9>XvN%i<A_~m~ zMTxn^@t~C#*i40{AXpCwMI*F|1UU#=UZF~-<{>#5TY&&=B|~xrK6htAM>J5(hnB3! zT|aEW233}nnp1);_@UB>-UP@y*qRbKsZ|+xLMRtoDu9MwE+nmhTM9X;>DXKeRRXdc zTS*12?7#~bL29u11gZwrC-_1&51V(A5{p2K9P_aiC(uNlpPQNvTf~kMBLx|m*o=gR zLJ^)2f)=5$5P~*NPz;Be0trh{AYjeM5NVh}c$B1N7H5D=fmR|YcEPhP)Lk&O_(Bsl z-T}>35c6Oe58iz)GQ?6?U@ZdT6Z2ByK@P+g7@&pTCGatV_|)PIZ22p-7@9>v<usm{ z!J09k4HMMlgsngZHyEI835c(uK~P#yoSBoKhb_4yjg8=OR~a6;ay)XC_~NM&n|q*5 zY*091o4-q~!XCa=NZ|{uZ=ilhZi+!OD^#W+BeAG5FFiFeC%!B{GdVRS9z2X!l350; zn4k*bX$ILWXaqo&6qgjGmLzASro<;hg99p&l$n#5lnEPD!517!*b)vbqk{$?Q=w%q z)MQ8u!_qZU0gum;45%3p%}|q|6<%^tele)31T_|*o<LCs&G*^(%+J9Vr%0_kXcmH6 zj2xbj;sR0w=|Qq4R294oz@;1-a8Tv&5C?4?DlIMs4X@?qr(~vOrWV0={y@Ew4C?bL zqD$uFm&bz^%7Yi#gVuI)!h2!Dpta`N@hB^*1t4OHdHL}rsX3{ciBb>&Sj<431#bFD zVwFltEy1BFJux>Ihg3>xPD!E+R`XIJ*&VxORjDP364<rjaz#l-YDpq?(=zjMY06G4 zD8S+LoW$IeL~*Qc$SuV#o`*xcA`^QM<b&7p=V3RjAQO846lLUNkGtZ`^jsW)R+3nX z-38Dx7rRXb891Dfj7y@RI1{_uz-<7iEhyO)oZz4VfFc1(ZrC({6B{-Oa8iTXj$#rt zp<%NGl+45+QH-hup88OP!AT68IiLiF%}wCsg-rsSxJ0qooLh=bG!Lt21uU>p90^TS z*o-O2#OB$ejC?sPE=dFLf<aDHDAs@_uo+j9Sc)wcpeYTTj)DxV?n=fkTu=;4l_>53 zr!Z*tN0BHl0p$zT<jSJVoSe*L^zD_I&|wDfXsHxVRr%7mq*Ag{amuA<rK;f4SDIav zn3<QEm=~Xk+klj+R0CWFBvq#7<QIYJj>NopB-3zuqB0Yw6dph1;E~JAN|nLoyprM! z-2TeO6EM(j9ymheahYD6kzZ62Uy9TAl&XwWNnDx}afU-uDo&{~+`dWAz^yF>w-lau zufk(u6>g7Z5(qEc2_PGHn55?543pGc+){ZsgFhdqrUKl7m0E;TS86d%sS?~_Rf<zn zY8p<dj6|Gyy`&g-0!hY`6^b(wWpO3c;*8{sL`7V(8Hq)Z?S(jVL1k)DDNgOh`Du{d ziZ~+*cWhMR4!z1makzs~3gScw?BYqOI3&uDA`iu+^bA~DQgBIR;xelWr&(30I6RVx zh%XdpLCT~|9KOlM<^R+i96C~SaY^Lia9KVM4F$M@CAA2Lj?`it5+%4or4)yT)HECt z8HtF5hZ2q@#ke9X8M#nL(Nm0^Gtp#{GZGQyB8nV52O%;%iaaR);Lr-sJvgEQSBO{Q z3a83MW$gI_rG<hcEHm?Qgk?$=uCPqcN>##cAyQicsRTd?7*O@9hg~<WW(f|LSK`Vq zI31pYQzkD9mwQ2t5gZ|ujWZmO8zOkKd?^m2Q>t)R36+T@ICNDemf$SV(=tntDr%G{ z0ZAd!Dq6ojvnnIAq!_89LD348Rfk6?n(p)rXd@Xi@PTMcqnLuGN(+~1RWQ?1Qj3#Q z^HNfaG~h~648&BZ30H{b!ffdDEZj(hUr>z1P>0jRROt9PArmpwAu<)38;eVd62XVR zlq$fzf#NuX0z}rrX$JC0lnx>DkQL)hTg4d}xY8C#3Xy-IV?_mtIr+&M*@<d!+tC~g zk3aBgT0}<(#nZSHp_v7ZU$9At5JoWxSqUOVVK)c95*6WiY-YigA(A|rd9cD6>@Wfa zI9Ls$AVD(;JRgZ@U7|P#EQg3?Y&s!P40kXh_hA}@Qz@Fc@Th^CiU?K|Q!y3d%vM#< zk_2ufj*N<-4tESdyaQP|f-44))gU~9=3i)e4|f`lw1KV+r+KiIFR&v=aF~a#3{g6u zxeqqiCI`=OD6s&O)`v^u&<_a=xHom->aiM!rW&Vj(IXazZ_$<Ej92J-M7;5eq6VkS zpsL^=#^Ew_W!ms?K?_HiLg+y-pnWok^o|mSSX3f1CYq_q@M;m3UJxEaF%(lFPM3n( zuW0dy!=+eM;%rQ0=jUd|r=&_J7Uiab2e8n5l31C9Q!Xhr52svuX08-2GgA_AYD-Pd zz^Sb&5vNotZe6J*ICYie<8((=2JRrq%*&L)<+Ys5a@=83fybH2xWgnnH4mp3GV*as zWhLTtWqL+p3Qo!7jMNOA;gXBTEtQ!#wdLiP<CH7Tz#Xq;xML_e19ylOq-NmsURDO4 zSSZ46W-*>nDN8NKX<rGx6j6l7Ey?*f&8*BU&%-HMkXc@YGqAJqSeTDH+S5`q)Zs}U zWA+ksEF$Q9sCdvmDq*;~#DaWKAEO{6RSYhcnp=>OSe#i5K9NxpA&h=jBU}bNVT#a? zG7pMK`iV)P_E;w3^ezRs0Vu^SbQU(TG6`WfiUQm+NvU}_^rUCzN?^AxB@u^~)Z`2t zTB;IpNTlM@ky?U7M@c>o+d;$W*d3gimx%~elz;&hY&eXo!0EJPT>j5a&BNj7jC>pt zSxBV>ipxNS84gE-iZUFI&c*41%1j(u^76}Z$P{PbinTIaagdyWD{KmovnYyBK}8pi z&?&-YT5)Oy4o8=zmg5LEP?3d0QxWdCMyiQWTn#F$a99K?s&H78jnkrhTpmhG&A`a^ zuuaOT$r<q=hp1pr_m$9o0I0mmPUVLy%BU>JNX-+1^HYivbHNAkNMxiY6{VKP7nJ5D zmz2WFox;R487vaT`DyV5iN&Rba#$quO7r3~64OevGZKZM($GG6VoqwBAgWMOYKbta zNP1>&Y7UxMN@7lGi2$m)j8rtE$`Z@av{fZmX6B)(%1A9i6Dh$EsmxCiN3{$x-JG3> z?)>b;G&DPN5_40r>&{KhMYB0KH5aQrd8O#C&MVCmL3L4aVs2`726_NjX6A{Zs)M+^ zAQdh23Q{GpsVgZ?Ovyy^5m*e(<%RiaXjT`c7NfhSI0GZ}OA^bJVD`dNa%E;pN@j5e z?C8p}#4>qYO3D(;;w$r02pLhCp91q8a&k@wovoRHEj_2F78PeCR%Yh%=Vj-|r(~vQ zmc*xmM#H4iQ}c_`LDS&qy@)(%TvADixaHFE$fe}x;j}Ilw~=L;xaF$w=qpLYZ5<vX zv+?*KC$Si3sN~|&mzP+C)93lPeOHiJB!epwsxlIbaJr=^F%_p=F&=l8;BjYZ9!|G_ z7D?ii%+A2$i}VaUt|`Q$5tQ|CTA7@ICw`Oh*qD`>gEQQcGjs4*h$k{C5^;xI7C!%H zB&Om_H5s@gI3p9cwsJh$@OU93AGeW}xYKW9YMu~0{lcoqJYjU9q(n@ybSz>i`IyF~ zVrnhR#1yN-qOK$n(>N?zv$41&C$SjA7r9u}<s}wj1VBEf>k1N!M9>2Tl)o|TC`wGl z5G%%FZwVHAOY<=70OeZ@K~R3h5Cr8@44VtFI2)8NF?56SBZlr|Ebh+A%)wNbnS;f^ z#3BrZ6^WQW1m!Obg&B#d7_pLp8L%0dn99nrD8u5EjC@S3X_*+7R_2MKM|)~cW?Cj9 z*F!T6v=fXiQzT|0l`aK|MTzM}i3J()#i<~6Nq&(Gdfy*zV;&ZVC#L2}!WBR*htJX@ zibkjmbk#cO;9I%m#Jt25P&=ZyG6y7{TnswOz6i9Qx+EhJpL8~81rBcAS*6lNrFogj zuw~h4sTrm5X{q@c@oA~?X<{gHrA4Jx@ufxarTi$ul|_80d{l0^B8qHDMrl!Iabi_G zh*2C5KH^dUMMY{!h6sv4VqS4+ReYi#ieP?md_Jm3Vluj##MI(sITT&sy)a2diFwHx z@%hEY@%geS(vU?iCFPmv5Lr~8<fHmKKNXFihiX_+VrEKyd{JUbd{Js<N_>$JidET( zrFn^{9#2lq6UI;zpN*<0H7{KPMNxWcQht1TWm;)`I;zU_#3D%)mF1;*S^4pxt@ZKc zq9`&MiRn3sRq<%yo{^ZEo)e#e8h^+kn30$cx-}s~0mU$+K!hnkjfA|!(v-}q_`J%L zcn~u_4^<jTKSUbUttt5>C8;U#c~U5rf|FEcaeQWRaeO9fw56h^rmWPW#Ju<{w6IDo zLUjbPHxrBE6H{eStV41mNEX$FndOO9@u~S4s9FkAi%K%`;|o&ri%K%$3s565F(*9} zq$ek%0@bYI{PN74{P<!ORO>U-Q*+{x-B_HT6JLy){*c@VmPHMfWK?bys$)yQekjQ- ziZ7`wiZ8)wFjyAVq!d*YrzWKqWhdq(=EWza<`*T$CuJpq`0+`osS3%ha22Ql0gd(o z)Eoldp;{o0Vq<CsIQ%o>Q&E$DZem_uVpV)@VqSc%Jc<tFfXq!yDT>cUO$<n`gGi%k zPb|$&tcuS`OfQYk$xkne&%x^S98{-g=B1Y>R-q=t%)E3|H>V_~CsxI$B&Mdvr=Z4A zeo01RPGVksYDq?FPJBM9IjM<7r~;Y!MX0$Txg4AhQA^O|#GGW*QYtw$F((;SQE^FP z9%`L|9DJ2UsQD5p=qk~Qqs-*goJ7z)6!GXaNFi#9s(|Jd=-op|eR8N>(4*p0kw>4P zX#%{X3RxT)35i8HiKXfB#g(~9`LO6JDa*`FOerlw6-`RbNe9(ZP*GH;WusOB#Tlsl ze6;!{ttc@s8&q`{XQ!6L=c8p<RM||tvZ)!Uc@kA`Vlrw9MUzb}PR6%`ED@h1UY#lU zbf)6j(Uyr%V-`M1ynEqt@#)ORCs_d9c?@lfK?ZPPgE-IzV-Y?BO7KaR;*%`HCs~e9 zvI<WKr$djPf%pXKEu_N-z*}rV`wpS|17UuJs)U}70N;iO4IUKJ(A8i!32G-^lhD;* zHwk+B8eWso)j&5vqxlZHbrZ521G<+H#cL=Eu)7Aj{S~ik(AD5K3AR)MTUf(Pf~kQX zI)LV{Y{>cu@JUD5HnAhiV>bc17ayky$nw~I166@HT+r2EHwm_AF(1#l>*#8*`wDtm z8%|##%VT#7R0UpNp{v1e5_DTVUX#$(U^fYR+!S7u(A8i!3A*1MuSw`?u!l5sb36Dj zLTo7zSsuFy&=Wl&JEkDJl2NiIvOHcBaK<*&1h72z@PMj-FS5dxBhl61Hwj-FhM5FY zgWaSGNZ!U9N9bzsn*=LMu=x&V5=;$tlb|Q};B^za8Z}6qqLo~*LkRF5_KKkho6BM8 z6y|blC)z?o9JE{ma-{<{MNo$$6hY52KuL|@wlI1ogKnnBtq}FVE9enxIF)B)rf0-M zW@e$S0kpt_UJ(R69|6T2@J3?n*#x?H5ocCKmd9=aG=y+wRb+X*Cg5}f)C90RG_ukB z4m|>=JT<2{Bfbo33rY~6s({w^Xr@5z0Uc<H?KlM_X%#HHi^2Q8A=wksq)>w>LbC)z znKDEfifci;uaS*Xg{VO@3SAY{LKKsro0O3a)4*yN7KOrj`FZi+iwr@Ry=8z`0kEg$ z#it||XRxQ{f@vx6i9FCw8t8&p*eZacY-z~Jwb<pLqM$P$EWsDcBJ@Fwgq|Rc)ky4e zP*H3~@<QcO64TRDi-a@si!!V7!N;+t=Hz4+6lWIm6{Hr$=Vs>Qq=HvAh=RB!sd=EG z%F<%cK{TKdS=NH0%-mESs9_LJNl|7&PHH@u!v{Yy1I!hLbCGo*M3J>>!U6=ej0}Ev zA+{?SwV)~?p^H~3Ur}mWD(EiBRPahOai}xUuc745%qvMPD#}kv%uDA;1YPo&S)3~g zUu1@^ToPFTt_pO?p+HGyQ9gM6EGV?%Q%ewjMZVaP2NXq6d$NV#fmEEDo0*)Slb@#m zI>Z+yUmTy9lbH@WrxkWKNKU0FXgyqNQ8MU!7o=+^3riDIib~<JA%J}CWL0WWz5=oU zqyj?8E14)d((+4-Pz5r}Qc*6VEY7SDL^iTGwJbFcMFhMa0L2i{g`A?un!tBwf`SW0 zBiL8s$m&uHOA~X7!HH7_Ssa}Ikgj8vMV5r75yWN9e8r_n&~ws2A;OE`=I5m%@k+|` z`4Mv98!nNwf`SH_n^}gew>Yx`NiR51koe#rLDCEg5MhK>@Zdnw0uBsOgc_uvkV1%} z2ZS_23L_YVQcH{S<54bJ<pL)?aQN~j7nN3}W<u{I6-)!=pbXF@p?S$*dCr{7qC^M_ zRAPWxTzQEwL5}i#$d#@9d8y^`#TluEsX1UZoGIWV)HBobIKlRVVx1#9vj8l~T~eA} zl$i}?3#O+QfkrF~QuFd4*T8ZXq!vT3NKG$IEK1DF1S{l8ECp?`f*8+}QB+x$mkD(l z=mrq58sVJGvZB=ZlKj%5y!=c^3gRs;NG;9<k1=Pa=W&&!=A}c*JwebNv59%{MWqGM z%UuOaN{g~VOKsqu7RUo#D4Pr}QouHF<tFAOLN;D=6@jmk1^J9KC$R|X%B0Ma<orxX zphy;jZ@Mi?O$M2gmmZ%CyS0|Tq$n{3Qc{796@ZSoz-qlTX{bPPVqS54acWU!8vN*s zbUbpEFoP0vLD$kI=A~f0_Z6H_OEM(UuXhEVT%VVjheM_$BeN()3SAGV00YYuV>K=< zGp!^8t0u5au?&WJ;45Y!l2}bF&a5cO!0;C6i1T=`R56BaAa8;5Vumbs+aQvdZUGw# zmBi2r4n)W@sElGU_`SM$rMam^pqq!|L6`TU+XpI%;vvDs56Z>miA5>J@rgx6`Q^N& z1xP#ra7IQF;7`df&qLxvZfypug6LNTvyoK9=a+$xqe?455>!S~1+62&wnJJ?kVdx{ z!ez)xL|_g?78J)}o*2TpD9S-b$Ak627PNt_*Fq=<^W#D4AkNNDi-%bZmSRuKFN#mh z$>B=Q&n?JF%}vcK5h+M4Dgo7yDVb^DRse{@Q<9Ndlp3E}ky%_)ECS)A<fj(LgYJ0- ziwc5RpeY83De?JfLdl?}Qf6Lpd}48YZfb5)YLOIN4^$2z#h(|SSX3EbT$-B;Zqjjq zOK)(77J@MIi{jy>JZExgQfhoretrozs8T7(0JHfEit-Eci$IqrC+5VL<a2{2-g9zN z!R(y;bnppbiFqkpFm`?s54a5jy%<{n<f{DAqIi&CdQoByC)lTH`9-PhNvX++rNyaS z2rGCXB|oHA;Q@Ic%#1I|7XWcGK?nDPZ7j*>1l1{^LHsmsNHq&)^MjNn#>3?xK^Tvu z0$h24)yC%+#lwt;*6$eNT#%$tl9-&$2}%O-AQpRR0hnefDJo56uE;N9$xF;jVb04h z;)aBHX<lZ29&d3;Vo`j40VKEarer4Ork11@frFnlBfqpbRjeQ<F*!9pKMfo>MXAM* zf}6EGu_QTzGb6vWC_N{!xR@t3D>b<UI{l%Nl#`g84Nal&7AG_bmlS1!o2kkWMW8eq zk4udiL=D7$xD-Kh9jvtnI*i#Aq7ARnn7XiA26HY%J=71#mPy0I0HP!<C%+uj{{i&^ zpnl9vP01|H1!qTuS78Q20}HAK&8sj)iVz<`6_plX8iQ&TI2phL3}%Wj7UzpXOa`5g zS(F+NDi%N&Rft1GL08}8Ktv%GFi$~#IVgF6XT*ddocz4_{IoP^)bqo|AhTvX<r$eJ zsqqlwr68tabG{Tr9X2Uhh%Ou<BMZ@pLskZ&7khBXK(ykJgy=*~L$H8^>clR|3(*P6 zOZj;zyiiLr6La(PQlP;B30ID!(wv;))XG#Ihz^J-ANUx{;?#JMY-$Nq2ZSRGQCM6G z9l<V2tSn|t&dD!L;V8{bDoV}CNn|h1%P-GO%wtW?&rL1if@&y9EaHMV1eD=<N-|18 zrw$aC<QL^iz;r^AFX%|F_>`jjf?|>4?9!YZ@RSdDf;1^VFSS^xq$o2Tw4NW7QZiB# z%Tn_MkcDy*vr|z7(lU!ug^^W2?M6{lo|%^-h^(j_bj67XvPf=zX<kWUW*&;N)Wo8a z3}J}J(u(rS^FSFdEipM&2qFe*v%-a-L6Mu80~bomEGjOEFDy+g0u_3>`T2R=X{ou1 z&`v!koFxF&TL5Zs7MEt02tdrw068EYBmlJ#8tGsmeu$>zoYEvPAF8JqVF*M4k|7WQ z6hj~u!42VoIW{l9B$XS|tO8ZWslrLApj`pbwiH;XSPUu#YSN{q=9OeXg!ywzi!+ns z({iAVa3Kgk4|H{8BBXC101<+Y5rW$MDVeG9ppcC><bpGe5KLnP(*(gZMKH~v=`SrO zF&&yJK}<H#Dy#e=K5z<4O^Giq&o4?TW>3sX%}vTo76gxCmgVP^BtknJBH%ay4X_|Z zln6A<gHv#RX-PbYF9uC$@!<RcPR5|L%~Ft=oDD7Xz?Q~?dS4ltsW~Zpkl-&)&MYp@ zFDm9p%_~dI$uCG1$^jR8&>|)^FF7+u0F?AA<C8N|le57c8$O6YQesX{eo1@<C&&e9 zpv=Gpu>>^Sp$bt0iErdV5s+kjeo<Z`s2wH<Q3Y`eSS@c(Vo`c(JhVbbYUP10dC$*> zmIX<PMM=;qH?1f?7gP`OL5kfJXz{BAT>_DroCs<v#e;fX;2?{KkHt&DT1cP}F3&6h z?U;a0q)Ec0^3f-nRNyTc<kkwB$!bW=8$8McA?Y1j(SZvqNO(XBM@f(f#AI;c3*JBp z4k{^x3?!luQqa*`l(8G=Q5lf7I@WWxA-14hGa&^T27+}lq4VD0F<R)s{Ag0pE@&dQ zxq>o?6;S8q=Vwd6*L|Y86=^{zibPT(E}3+kGAa3aIIKZlcZy<7Stc%Ps&MKlNyKFh zPQ$Wsx;ZDY7_lT4#c8=X_2eZM;qYfZF5eU&Z@oaV2DB0!hYN}lQ*p=?<8)gIPPdgJ z?UzAu0cf2#VofrN6lk?L4k^%@aU8BI#OYbk`f(iQfmV^@FfSRWZ?iIUaOug+!D&%q z5e`ii$a{)V0s^$s9EYZi#8e!qAp=+RWn|*gQjSv#PDf|tr;32vb)Z$Lpk5|)_*@h+ zO9Ckf;4{Z!MX6<};F}21hRaLA7m&dRzImag3uKxCF`EDy?oQ-|P8UEVxk1~?6LY|I zm2gsiL4Iz2T0U$&cWPcb^e}d`(kml1FCE*}4oKo?I!dZUAw?ug%#>6?=OCd4CPWgZ zH4UF+F*frO%di#MiO>QSX}TsUu?RXf2eS<7f};4M{F2hV6xcF0h>OrAvFpvuD=r1y zv6q6k$j`yE&MF5wdyD2M=#+4NQch-JX)1K~7TW|eR)zRNu@J9Qvr*$1wrMgg4TpFp z4)Gk+$z+5%nNaJYp$wCN&V!f0wo5{TzcL@Xv@0XCD6>!%*G5f{G7v6EEJ@6OZ|8(u zV+Jb2AepK-GY{0fk%gH8kxxp6&G;v#rxs_R$%3|kLK;8VWU~{~AV#NvWzig80G$Dc zNTL~B0!yM0Sv0M1Ycuk55_6?-xVR`WEmaCt4rV7vN;s`FC#O6!r6dC}Y9?Bg3LeD) zH810nGZKpulS@*Ipp%>m;NhiWXmJ7Mfrg|MAfpAyB9I~lDk2KHg-4+zBQZ}QzX&3x zP?8Ti>l@U0!;s(tHP{ruqmSI+CJ%%y2w|6GWF}`rL^z8J5|dMlGcpV0G2|fD6f1z5 zt_p?(<c$c(8xxQ>As}x`K;Ddiyg31R3j*?%3IxK(fRF-1LJEusDKI9az=V(jQ$h;N z2q`cpq`-oZ0zx5aL?|SU2!*5(p^!8p6p}`SLehv(NE#6eNh3lbX+$U_jR=LLv4I@8 z96-wNsCgQ{v=M%3WBk%4_@zzpOPk@BHpee*fnVBE9)GYH5Rf+{Aa6uK-k5;A2?2Ri z0`g`A<jo1lTM&>Z5Pn7k!q136_!$uhKO+L+XG9?Uj0l9E5rObCA`pH?1j5gVK=>IO z2t(=`aQ&?So|scG#3E*dMa&qBm<bj!Q!HX;Sj5b+h*@9}vlPMNW&>=3hS&s+un8Jt z6EwjlXo^kH44a@iHbDz)g4ldygw01r*nDJ!%|}Mqd}M^pM@HCuWQ5H}M%a90gw01r z*nDJcAOtQ9QPLWwkP)VkF{Y3SrjRM7kQt_sIi`>WrjVsDrc({Dh#6uLGr}Tfj77`@ zi<l`EF*7V;=2*lmu!v#thY=Qk7-8{;5f*<KVey9%7JnFF@rMx>e;8r$hY=Qk7-8{; zu>minyn>KMC_G~no(T%i6oqGo!ZSzVS)lMN`5-nyNJ9fut|2Pd2$gG$$~8gdnxb;e zP`T!)Tnkh#s;i7pU1fyoDkD@^8KJt$2-Q_asID?Xb(ImStBg=xWrXS~V*}pg#G=#^ z1qf+~!ZSkQ8Kdw_P<W;&JTnxYISS7Lg=fhJu@^!b8lZ9wQMpE_Tw_$O2`bkVm1~B| zHAm%IpmI@NWrXS~BUD!zp}NWl)m28Qt};S(l@Y3|j8I)=gz73IR96`raD&$9D1Zq= zB(@O}+Zc&$g2XmOVw)kc&5_s^NNh_Uur*-9&;XfZh|DoU<`^S$OprOI$Q(0djyW>N z0-1yC0wZJ>7$LjB2-yWj$SyEKc7YMH3yhFmV1(=fBV-pCA-llXfB_5`|Nm!TVqj!o zVBle3V31*8U|?ckVqj)n!NAD6lC7D6iGc+y#sF5s#bCf-$H0`5mQ%vOmy=kM#~=lk zXZZggEDur(V>9MtrYACRFhF!NGO&PEGBR*5Fv{;@U}q3v&|qoc%@SKB9U+~>z{J4D zz{k3Vftf*pL4kEO0~3P+gB*h$Lmk5&h98VZj9H8e7_Ts~F_|%?Fil|E#q@}ogIR~! zk2#OIhj|6_A?91mA6Qse#8}i=OjsOPyjX(Rni=F7_{fKBO$>4r=x$_?C093F1A`2C zdfDn3q{-3AR>vSkx<0mA21!zNvDGk0kfw*Nnn9ct9c)z$V#KRwt7H%*RyA7%g9veI z*~%G&iBZW`#vnvk9a|}bAR$$3B@6=i)vy&a@Z(d#R)kl!kbxJMJX-++4|X}Wd<Jf8 zGHiJaT$ti)xeT1>qHH+~9H?S!*$nK+B5YZpk^?5fmdU^hXJ;_5K-g^Q49p-FTN(os z10RD8LlwgwhChrpj8%*~7{4$XFl8|<VY<UC!tBM|z`TX|1&b7m4@(Wp8kSqE9IPg+ z39MbLTUhU~v9M{e1+bN{&0yQbc8Bc`y9~P(djxw4`vmq?>_^z|uz%s;;!xl);qc&y z;>h8s;ppL*#j%283&#PDQyf<~?r=O|<6%%>5TF1mvbi(JQ=ngfjhiC<ifnESa^&03 z#zlc{MK)IkS#s@W<0M<JBAW|?40-mlageD~k<FPwnjAaX*vZnT$mYZ#MY?@#Y-H$C zWOHPYB-Ji9R+9B7vN<qFkY*1X3yC@u+3Xp_NwI^CnI!d!Y<3J{#GB8?M1pEXHd_W! zV$Eh_BwDQ^n+<~qaptly5UEm;&6+`&7&BS_6Qxd(&5A*Yuz9Tih)|`-X2~E($Sl^s z1l1_ASr9bk4*?a5Y~~F7_|&le#w)MLX2!sWTR-bBJhF;xrVPBe^s@fMDW}M0!oY)F zAL|bsGKy@*4BXiCuztrXuE=J@z=f%v^&1vZMK(hQPIR@bUopfK*$fysP}Q-1K@(AA z(`R5uR>S%kg|En_$H0b=XZ?i4Q)JU+V1>!CeuT3X*>o6KAmXeape#i;Z3bqL80&j5 zt;o88fr&wp!JeUk;SeJWqYGmT;~^#nCL5*-rcF$5m{pi#m?trxVE)5mz>>r=h2;p# z7gi<K0M;7T6|9$7|FEgB`LGqS&0yQZ_JEy<U4`9+J%zoAeF6I(_8aV<ICwZzIBYnA zI5IeDI3{o`;@HA*gyRawBaROoe>gcfg*at6RXBAxO*pOCv?y%v2(Z2b^#>`zY?>6c zcm!DAGRRY8zXk<u9s$-j407b#&ZbUwt4DzKHG?d<HnXXb+3pcweZ?R{o~>-EWVL(* zSYI+olVc;B3K?x50oE4`Ql#6)rc83{M}YM?gCwaou_=+*{t;k(#vnnOEo_P;wSWXz zpE8J(Vgs8332h(&)+Y>N#GB40Pjo9tfb}thD6uB9$r0HO5@3DAAVQp}Y_dePgalY0 zG6)l6BAW~mZ6N{H2Mj`lO=FWL*cuXGz0V*>$Rsv`eHPYx3<CH~VUxt$A`)P|%fOG% z1UArk7S29B>m3F@+`8Gsakh#CSZ_1%;?l_`hNE31z<P@TG{&HSsgq3<Ys*N0^(F&1 zrfLQSHW4gsBLUVM3|yG1*@Q7#M*^(Z8932ZvI(KJj|5n+F>s)&ViQDZAqlWvWnf2E z!6tyzMiOAX!oY@*W#fmpk_1>UGqA#B*!ZCBBmve-3@i{)HePT`Nr3etbj*1Zm{w$6 z&%neWz`7PZ0<Fm4!qCQWjFE-Wfw78l6XOde874obI;J&DH<+22wU`5#OPHrIZ)3i~ z{DFmwMTy0N#fK%1C6A?srGsS>%Pf{fEGt+qK<p!%RAfC*Q42(o^&ACl2u0SjWVZtp zS<jGJn=7)OCaYFfWIaVj{i?`%lH|Hlk@W<LHJ>8uagu5>Mb={^)J=-4M~SX)6j_fD zS&JyL9ww@GP-HzsM7giXdXQk5uE=_TK#8r$x*u<etjM|#PnoO8x)*1;s>r$rN9n1^ zx*KaLsmQtuOL?fsx)Y=9Q)JzNR?;c5ZbvEE6j`?+m1T;oTjAxGBI_1tsinxe8CpuM z0@Dht8$ta%)|KGWijjeZbsd;R+00oggFJ&H`H(GzL5>35$qcgO>Sjw~kReYmTOxxr zIXc-A7^Fzo#}>~ZNvbZkI0gyQ^svP;h?Am&Ervmic=c@245GxUW{YAFAx<q@B!e(9 zD%m0!gb1r+3uh1{q>3$!K>)uRwonFsd@9&N82E6@vjsEo;*w<xV&K6p#}>%IjZKCv zfPo8BoXwws6J3<ekAVYKjLnyU9a)6Uhk*^jXY*!Yh4I+D7+4@|Hctj-5R1)&fr-J9 zfsuh7Jf|iAZjmT5I6`Olxfpa9^ib9XNPuViL2CgRkXYa)2jG<g4EKEQc7eDLRu%R^ G>4^Y6@-El_ literal 0 HcmV?d00001 diff --git a/tests/unit_tests/fixtures/bundle/assets/images/animation.gif b/tests/unit_tests/fixtures/bundle/assets/images/animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..9932e774483eb3516bec26187591b997e6d41859 GIT binary patch literal 9735 zcmZ?wbhEHbOkqf2_|5<VTnvn2jLcF@%xX+bT1-qP%q&(cEUql9zAUVPEUeM2tN|<> z$!u&{Y;0K^?Ah#`rR=QLZ0r>r?Dgy%bsQYc?CgE)>`ff(ogD0~>>Mo|9G#pTv)S3F zaj?(kV4us*Ih~VZ4hP3V4z|^t>`OVhmT+(`;^bJ(!MTouV?8J5I!?}Q9PHb;*fw)= zZs+9K#>KIXi)$+v*G^8(9bBBdxVZLkaqZ&bJj}&$gq!mSH|Gg%&ePl+r?@yzb90^J z<~+;IeU6L!BoEhlZl2TJTo<{yE^%>P<>tJ^!+nj1>moPLC2sD!++6p$x$g0B-{t1H z%gyzWoAVJj=R<Dp$2^=5xgPLvKjPtf%)|ABm-`tH_hTNeH{6`Bc(~v4aJ}Q@e$C7C zl8@&tFYh}Z-nYDbZ+Lk=aB+R)=KjFL`GJS~D=+sq9`294JRf*?Kk@K<=H>m)%kzns z=MN9}4<4SMyu818d4KWq{N?5O&&%_NkLN!h&p$ri|9rgv`Fa2H^8MlC`^C!xih@x* zKp~*`pWDwhB-q(8z|~04fSHkjfkE*n3j>(`&+Y5(ZQ<nUYhYktAOm8ssDhm-a7DJ~ zW<SFwmX{5jGv54gs$)?6&+X~#9OUlgZfIa=^q+x&DI_O3x0uC%fgv}qq$tSUNg*OK zN`d_a0}lfu12Y2)gHd8~ae-q%fDhP2ti-o#V0SQF)&-e`RS9lE?v&KxWCjLC4h9AW zyX1nR5(WlF4+aJXpYoCd5PJ#(1A|C%Mq&yB1LFz?1_qs?h{z}g2F5EO@gxZQ1&Eyv zVgCWKi$a2&85o!Z7#J87(vjFnNbKZ{qW`%J3`_<L3=Ar{Ii<-^H-dtcCp9lLn1O*o ziGhJZhM|}th{2t~i9vzElfjk2nZb;~kin9{jKPdSm%)fZkHL_E0f&B&*)j}%4EYQS z49N`n3<V683`Go?4CxFR3?&RY3<?aT48;to3<?Zs3`Gp745<t`43(e=fVc$_u*GS{ zMqn{U9w!C{)_ec|e^zB+V4uvu@OQ%h|GyXh|NnOp0|WCN28IK92>tmB7#J+RGB7Yt zK!~Z#XJFuOVPM#HI3hAEN&##rL(+5xhVAVP3=%gB5{nYSV$2K-3`}Wh3=E%^GcfQ* zGB5~VU|`^iPR=OGh08N6zRti9Qq92NbC!W&<}3yVwKN6>{ul-wkP1+_&A{=WfssST zW5a@j%^bp7F()=GJlrmz>@~+@<D#S862@6~PFzfA=vQ#=lJVTM<m6-x-!LA{1xrs) zH%LA;$8+<tv$K?>n{q4@7#ioZaB;|TZCP<~vBzYs*k8pOfeUP-n^JxRI-Fm?wn^^B zsjaK8uTMDKCF?yc`x=uyyGLxq)pSN?b^(<cs~4=h%To6F)!ozE*Wce?`&`K)fnn{f zelF$+UMcf~O{{!M-t&BRZhCrphH>`1KNbfQ4l^;SY>QG@*K|TyBjUmWMdoa7F@qEH ze0OhqdwWOm{kF4FhqJgvHC)0z%yf8h#Ew_N!uH@HR(>Uy88hVe?)v)r#^&t%=l1^k z^8TWJ#Qzx&8g?);WeID^1z0sUKa=xW(s3}Ut@gL!r`Pw-@8AFbKLb<6{aV8%2~8Z5 z6Q&<vWR+=~ki??FK9f~$f&nw1^_zktZIWd#9=0pAX*}vsDckX|)B1sdGnZ1tg9A() zbIR{H3P?S;!6H7xpt;S*=i%di9~R9g6I{eHpG@?S`}ug1kIcryJZdou7W6E-sNvA^ zF7SaPmxYX>t3=`kMYS0z(=wmU%viSa*{qZ<%QT5#nMP+WMUL$Y3PY<(o-!_vRB+<4 zHV}L!(<1ch*}@LBRWBCxm{nygEbcjZgwJqG#689eI}L}-<(VIz^Tlss%v_k{6s5It zsa@8qm5bJCy;|9J=)YCg!UDTYCbsE|rl&i06~1a<v#PrJYRwU`>^B=vsCj422}mo* zY~C$&MSDJ9iohF@!&<A~Zo4CNnr*XM3PU3k51+tWhs#<Fj2nzOZol3AA?x>BLF1l+ zgPWgay;{B8x^Cv1MMqa@z24s;$F06sEn~v#o~mE3()Tx?d-dXoNVv_0g<H=(XzYF> zcEDhL^*7B!eNA1n+E<G$U$bepP`hE~Qt2nN)=y;#=svhywSU*AMTzgutT?9^ti`nM zNt)N@vli|>tIqob^L#n&tY5RvK|qN&<D7>1+O3QHxqVHR$$Lw0Vm6OoTe8Ob`ySH+ z%h+OHUUz#P`8hL{z3SVo3iVrGZwF=nw=%!!nLhjb1Ln;S%dWV#_rAMPn!MNKVfJ=j z(>q<kXDc2=I<GTb)gEkT$`ESIu;b>YEV-Re;_N5<cpiFLcE^Kd`R9H<sg-|g_0sD2 zy)XAVn$P}vx+8sE^{GA8@3vnpJ8ZY-{OYo}-FHq{%f0;MbN=1e&kFzVR-Eq-Ucc#U z&HB1OZ%^{Sw*9=(N`K$m#nbQq{pq(@j+sN|w_V-uONamO{qrSRf7`bi!t?&!m}a-) zs(#+h?t3?vnIGshJ>T#zT85$d`#v_IfFLL92WJ+z+r2cr(A3K`f9bmr%L|P9l}s!P zS9z4~V4k+2^vCYmT+SL!;{WoRG<+WN$XD$BT4>OAYPRC_|1KFe-*WXkoOC1vl`|}5 zrk$zTm3{eCg@oO2$&aO0DN*OoY_S)+bgrHAjly@W6{ZGzSNg>EO!>9u$6@7Xc?G## zlO>l&9FW^&*pu>^f%W1vbIo__`f8F7$o^k`Ag8t<H!JzDG|%(nKPoR~KbBg;!rggf zt?H+q%#g=VlsX-px;B;+)LfRAkJOaD_r7E5k=rsC-MzdUFHUwivq<dX4&!?>KXq02 z#3~zST5#+unlyikD#ywfUY=$fC*=D);#z*)Np04J&g3VHjkvF$iI@94d8N-H-MO63 z=3JMXK53_^H*y}y$r7IMsOBN(XLDcGZJ(y)@+>g9z0)__Zt|r2SMDnv=KSw(Q#HBA zIOfSo9#htMsn7FnoO%8sL(^h=>4J`rDswHrrXMrV5$ej8d2X7#GT^TxmruM+hWS&= z^O>I}&rbe)S<YMQNL5!!@Rcfw6=EkYB{Jo5`FlNo|E+Ybfkt=gim1>fSzIA@=Qc;M zH%~fbwajncHty(;U8_P$+14$cmc86??=;2br$Y12a(jLKED`p5$<=kUu7><iy2i1F zOSkssn&ol2cmBndZd`j#YbEF5>%2d|Fid?mVMS_8s_uQw&3kt(XHt4FX}ZwKWyh?( zZH%{?Jo|via?7hH8M%d4-_O~?z!j10?*8K9B(7OYF3tM3#qF11>g*J4&BSS5vt19g z2vsZc{$IGlWyjgId5$GYd@DOxrZoxX2s+JT-jU8EIl+&Mtwdgo<;Bhmz3b{{SU3NX z>0sv43g={Wka}uxoJo?Sv1FMsld!{b>+6h+RoA;^zec6AJZ;UdI2Jrl;$@aePb6Pf z$mGU<|I(Ou9puen^LfB@c$pcS+J+pqH?yz)Xk$9GQoq?`m&JAyHlxXn2hz;WJmkwy zW1Kg0L-Uo*a%)W3^cy@2yPh9P5?wvxXtO3ymD6X-lBp8f1)AM`<!6%R=Bw(OtTVj2 zDoJ#{NUKIhV%MbUG9qf`M>SnLxq9U{3D5ZAaw=<Q=d|r-Qs<@IR8?H);dRXCnbGV@ z6<N>Cv#y_cX14n$$Jv6u&2t{h2;gY0{RcJInBRgbOa=i4P{W6jfeA)~<vxMsxEL7z zg8>5rNR)*QEXvLRYBn)5Lc~C91_n^0hmnDS$pM0)>UqKHSr`}?D!}?h;Cx0fUlPuj z1M?NZd|w6z21ZbWh@ZicA%!8IAqm`A31G-z$Y&^F$Y&@9vkDj(7znF?x)QZ%st9kJ zg00m6TMO!HFxh_qvl&L)tcbSkXqz>X=55w?P-ljRfk~8+QHGIGfss*%k<pZq(UOtD zoteRviNTqH!GnRxgMrb5iNTkF!H1C{fRQ1Xks*+YDUgXFn3*AjfgzNEF_eKJgpnbP zfiawcF^rKZoQWZjg&~5GDT0wPnvo%nfgzreA(n|LhKV78fgzEBF_D2GiGeYRks+Cp zDT$FWfr%lJi7|<pA%&44oq-{Ri6MiLA)ApQmw_RVfgzuPF`t1kkC7ptkuisfA(x3M zmzgn-nK6fjp@4y*kb$9qk+G1Gsepl@h=HMqk+GPGp_GB4gpsL~fuWp{p_GZKgo&Yy ziLru#p^|~2nt`E;k+Fh_p^}NIl9{1~fuWXxp^kyEj)9?`fuW9(v7V8khKaG3nW2G! zp^<^1fsvt+k)er!p_zfPiIJh1k)eT!p^=H9k(sH1iK&5^xsjQniHV_^nW>qHp@o65 zg@K`!fuV(wp^br|oq@5PfuW6&p`DSjg^8h+iK&&Dv7L#bjhUgHnW2e=v4w@9gMp!w zfuVzuv6GRpi-DnoiJ_B;v6GppiwRmHj8e1=0m6N@jf;=>^D}oq`)t9hVon+zaGAuE z1nINQwkUpe#M5Yr$4urXAqg*7pY2ph#)Soj?pA*^9WokMM{Lfz3+c0Mw~|$Gcy(xg z7`KE$!Gg87w--D<HP?Ily1U!5c|{Z$LRAhk7_tilR68_2Wbcrdh&!`m<KyEKl)dA6 zetdYuTs3!E=}dvQ3EMgpJLG(KZFzZlMeyo4-zdXpdsF_F_5J*LfSE&~X!VV=)#Z5t zdrLq6<E^pyaDQ*Jdi**j%g=WNMD{$34pU4x$jbFNOmXq<9;F9+yZ_7k@7we9^NXX+ z{P*`(eqr9Q`n(vA)Q|5<{-0jQL;7rN8VVJ^-iEjEoVEQBm2)k0=`^MtwbSFJM8YB- zv`VC{c-SVB6rtGqQ<lT2q{`6ZcEYW~_WSKR+g?2CHaMp7xZ7W5#i8ozlkEZt8uyOf z6&7VvdeQ^xv$Z?1F+CFIc;wO0$Nq7~+<qg5nQoJu<yJnO=8?piH2Kog6F2*q7H~Z5 z7fG6_Iz2~d<+4ecS1J}aC7In&X3<(xv2=RwH%sODg<^*4HU52)sWXEgMW#;3{`GQ2 zcf+ZdDpS}zXC_SzGyC;oS?r;i@-te3q9!a&_d4-%rOP#rthwrG;j8AYIQIY5qE+T? zkt^n|)8bmaW=~Vb<0WljT`!jI{-&k9_P`^rHB*m$>RPjQk&sJP%TcrHRjZVpq~EMK zyzkVStt;%LUv0Zow)*w<2Sr+Xt5arG=Iz?Jt~z`5=|j@**5A^4y>{>QN7n21z5ld( z<(`XtCTqK1aNSWm_(Up4?+}-LSMH`S@1!@*=V9jgc(gVxrtpBw=9h)Lcjn0$O&5Ot z+mK_~yq-<_-^jhvJAHN69IZ2-C#{h^%X!=8@d=~pcQ&1OW71u-TTQt3^D0aB*!7oO zrOxVJa(32TeC6r6n5|b0lHP8(W?Fo9(RC)-JzH<+PU3C2nYj4vYMz__eivTK>+dZ- zm}*~Bb~pd}f35B31U{YlzNXxKj`<DS&uhPJY2wc_zg)3CH}7#@bi|Hh)!KDG@65cu zcgMZl-E%+P_lu6Hyf4D+m-}d5{yM9t%j^A0Uu5*leZIK!P~6TpJInQUy<WP$Zr9TZ z(t1DNtoa{j^K47^+25BB+viukIq;az_G`g%Ih(tE-uy=23h&!(y>PZTxBBum|NYfp zAIDoI&MHm+x$^#_^tl?dGSyCQofUtnzwWo)CE@=IzNy9k)GuN`%s+u^g$dg!PL`5i z^Zy2^I5-HNZER|CU=g&?(em}E&zmeinJ>d;o9g;9r9%@tPna~YuYDIS+qIA<eT5=- zt41}Kh9G-%1_P_j0Xvpm7wV5YnJiF}GIVg-BFJLsJXa_zHSNwzzY4x7^R%Kqc0Gyg zFTaz#T2zk9dci^8!fD2C&t`12OjlW4aH+!Wg@%T4+?p$;S&yBs-T2s7ab|J$gB>$B zhp_hKTKc4_IiGNn`eZm&QY9`ySo-nW+`i^O?&x<;C%vL3P0HTM6)*Ku{J@`y8i|}s zHQYVD7EM^%&nxMfFz?couw9>~uB++tNqH>n|L)1O9cPy6rC*xdvE|A113b$NyqP8? zNqwGi!e^P$^pj`OtUk}Y(Bk1RIzr08ATl~aI=Dwj+rd37ZblX{MrKJSb~Pq8EhY{f zW)4$kP8$|(R~8;07TzFMzDQR76gK`WR)J(T!E6qpTsGliHj!#}(Pj?O7Iv{tPO%OS z$$oap=^PRh*`;Q4NYCSxTEr={hD&ZMxBPA%g}vPJ2l(XAaw{I?QaZ~6Lg#pu&+#ao z<yN}Dt#pY;`7)QvWgg|Le5%)ZlrHfq-{4lg$E9|kSM@Hx>Rn#d`#fs*c~tN7sy^gV zeZ;Hwm`n8ux7t%~wdY*w&v{gz@TfoIQGLv#{)%7yHILdGUiG)U>hE~f-|%X@=Fxb| zr}>6g^F4^I{)tchGmrWg9<{H$>fiX(Kl5sQ<k9@dtMQpf^D~deS6+>;e41Z)wZ8CZ zeB;&p#;f_AU*j8}<~Kf#@BA9yc{RRpYyRZc{LQ2BgIDt>zvd4<jh{T)KlrqM@M`?# z*Zjq!^@~sI7r*vzKFvS;ntyn-{_t!4<<tDfqy3*(`#-<de?Fc6e44)n)IWoe#xDV_ z-vZkI1hxJPX#W?``bWzq)u`DxLx6B=6Lp3l*o#MVLF#EngXB|FUT#`;7S!5YV`-e` zJAvPRpNzwTjEhVjleK)k8dhFh9<bPJu2yMg<1+t5E{0tJ8a_;Hd@>FW+t%FNoN;+o z?CEK_O)RNLl~fHDq%yQIvIyuJm^C)BHZXI``Rv&6@NkDPcNoL>16Oy+i`J=RF*r7{ z^2j+f2qb<^W1r{T4QXvIQFo~L(7>3?(!bs7TJ(WWP3*!&;R_NLZfE5>(9A9Gw`a%4 z$H&V*zmv5oZ)Po6f68yh^aG8jcW7sv*s!qq`+Hfl4p?jRa{qHXxjp3{rL=r182&Qc zJ;A>E`aau(PUZaaEecE=EMh+nHY&)hc+kXS_ToXafL6qV7WS$QjjW$_8KhG<o|+yg z<?#tP$R^j|(8Q^~WW^#*cx$uAc-0OiPK5}MgDgBM3HM9nI1}bF?Z~{q$f0&bfSKRN z?B<e5HaaVxO!4>IsWLe%C^B_gcvR=pseW#f&72A)3l7$B{iz7Ab5dHsz#^RTfss@H zLxPJ~McT#|K{uU;&*wL=X}ws`BK9kNe%-gu<sub&2M=?r)J$m1lDc<2aW=Pufzl+y zJC(<lFL<VzCDzlI^=jpcWwW$aEYvf6#x5Q5pTTjdgII_x6Ss?lBfH|2nXIc1uxab8 zuK#2BX5$IAXq^p_9~{!yMb2<VuVrC1I8Y}LaiNi8pH_9-#*<aj?{+*f`|UJS%)^^u zD|2M`+HDFyxb!w0)#F~Z_luCV{?0#}vh;Rc5<9TY;oT#ygRJglx8EP)`L2C*ajM(u zg$J^$O4c6yaBXMHhTnC+vJWfBTfg5gyTtnA0S1vh4a~cbWj|kjtS{%4q<nwWs}=vw z&H21?Ay;<G=G0SO*+;j<&B;8icH^hv=2d33t3=GL_h!uNF?zj<a|2iJfn6@)*(NL^ zISnfO-a2m$&ju}DyY2EnsgGG~LixR$P9@}jaJ-aiy!YF^n(qI%zTGY?zWeZc;(b}? zn}*eQzCWt|-)nm3)j8iCcRiK%89$z3{&vTbM(@7dhf`;-t$4QN^_m^G`^)c@?w%|? zZ~MJW_t;;nmNU!!e8s~3!SZR!?A$kR%ub*C{%it&?aO!NlXYD;90=wueYP;I(dzkj z`Fw*H?cMkOED9*s-*Y9*ntS!DYx2I8-!@EtxA%K(d;eFqC!0HM?%guqZ}+?RclFmF zS2uee_`0V$_De(g{&&3o_3NAB|2KWH|E%iYuy}SX<No7jEc^89cvv1VzfRx(d+Yh8 zKgRR^n@H^X;`yOcWKF{ABg-8G(=Ifs?wQUq`?!sJY(e9;Lm_u#o?CC}d{ue-bo}c7 zLLCQX%nI5KTW0b%cUZ0#6l}kGN%5P4gri!WK#T4a>32bDM?VJ^c81v~h;>UGRc=~W z?!}TUarA|ubX!38bfd-M_H64+%>wInRFZd>SSZQWt?nyJxg)V~$8qIvhMx0&g^4on z+~&CJLI<05%5s<GZlOyJIxgOh7h^R)<l|{HaeDHSTShb8ot~YWkmRhwzCGQ+QOj>~ zl})P3dG=GTZ2TP?58oG+{(8iH+Q+J*prxGkJNCuAn$%nMN>#?1b6bL6(TqEv6E3Um zuyEJ=+<Mj|MpxTYE6eZlY^NoXI=&}ejjf7jub9Ln&-G#pXWXQ++z>VOjh>CgK|vJ{ zwxrm(@BDx0duHyu313un-)FA+x>k7l%A)7im1ZvWlRk$%dn{rltaY~RU2y9op9l8O zj~x%OGoIofo#CjzN=xToF++6Aie*77O)`WgEU~y9v*t?cC5v-KIl{4vt!Ho3f0_AZ zq3qRXT8CEzR8+l5Pnoq+pWD)V;jSrD4X(-sIWD-8%_h?Mf_bGVcSS&mVfLy})))GT zGp;h;+BoC*m+0FKVqtx!F82GsRp5$Vxw-1+m4)6@RU`LnoXQisyt@9+;n<bCE>0;E zTo-jW%k_0t$TqQS%c_^A#Q*(uZoQS*iu|SWpVguR+S<5ETaM~*UX%zw_snymyKHdA z=P17ftF2p>on_j$#9;5t|5^uDCA%tR3QI=^p53;sKlPc2>h0T;+PbzGN;qC~k&dh{ z+`OaWlX%g`uG_YCddxzq8YQ;3BVKh)EuHTzakC}VvCU{=rG53mBL8VYhdRqEpFGvu z^rKSeKG*BS=c}}ivVGV47-W>fz!(0=iFG4ij6%Y^!#Ap#&pfdIw7SpDLVSzBoqJ3% zYSI5(bDO2x)+aV<r#0yo+~Eqi{6~N_TsULqp+lmVQ(UjFd2;v7-n2jMU)*{Q{b5Zg ztIUr2b8ve^`O<qwymB_O*)1qgi3qysa&)<x_050)wJDRA?^&+-`DI#7l?bPe!AkZ! z-*zvzVVJ14pixLw(<lCu(9|7Mp6TVT{O=ukXwr-ucb3d|eaU+M5W}o1BJKj69WH84 z6MHjUUN}ihxhW@1><vm`k|~U|vz^qJIQ!8uwlB}^8D|AFDIIy~XWZd5%TJ(Dz(tY2 zM1fi2lMxfo0Y$Ue*H(WP9w^)Ocph)cgKInXZLKOh`#SFOE$i*a8fsoVR`xDE5tXo! zH?QQe^n(xw%M+btMe}^$W<~$Lb$;K0f+FWPq0N>3)f;`&%q~k_neqG13qSL024UG< z3wjddvb^H|Et*kv`}e(uh2IlJKFng4IB@sJLccvvZKrLM`163ne@En>zxn0+cgH$i z<PPlHB(f^v&m)QRJ3`k5iOerx)mOOLdBWZA)1>z<+GcW`7A`@DK26*G=c&rejx*_g WpJ(0v^UUJ>&U5AKJ~uEhSOWldK6Y9F literal 0 HcmV?d00001 diff --git a/tests/unit_tests/fixtures/bundle/assets/images/logo.png b/tests/unit_tests/fixtures/bundle/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2fd547833635ab465a796c9a936dcfdf4b3086 GIT binary patch literal 685 zcmeAS@N?(olHy`uVBq!ia0y~yU{GLSV36QoW?*1g)637sz`*c4z$e5tBjbR_9FGGv z1|4%&yeKd*FzIM$5D>`Wn31BfV2MXV%Z!ME28jTL1v3O94kRpCap1!ohXw<Mgr1Cu zn2rToDil;2Dms32%s8OYqcCAkKtW1_L4$%vM#KaWfrN~Lf{Km<2^kVwBo?fwn4zQ4 zF=fS!1shhZvB;<>Sdn8;&?68Kv!P?kf*ErpDk3B@as&?KR3xO#*r1~@qh-Q@mW~4< z5-TJmG!AG~)KpC9$>`{j2#}G;@G$5o5r{}QP?1tl(ehx!o(U@^EO=q#kTIvBpkhVD zoP>gw2@xw24(MnEgaj;DBGD0{prE3_<}P)Qfq}8n)5S4F<NVr5vBAQQ0<Pw60qUza zxvBQe&J<mwl)@^Qyi9ceqe<P*|NobhH**R~ovQ!s^qpB&rTL-Xb$=We`Ohk#@qUi8 z*opYXSJhvfdU*X@@5{2Ed;UCmpC49i*Hcq%e5iKSGMTBp;Zx;5d<m<pR%YVq^l&oW zczJ=S!a|QpK0+I-eSSR_(9!Ypf4gpf_3e8X?`{4N@c8|=(o1a#bDev3+~z)i@YkuS zX_ocb>3z2@FlsK$s7;x*#cS2xIVzl27!7YGlpAJTWL~5zX7qB=y4|@V4$`mB_usE@ zUhls6(dk0vySo<zO6!O?bX0BGYC3oNV!7#3a<?Zpe|p~E@4ilCt%K$)h9$eE9yZ_- z$x&mwCU)XX6l;*zcYfz?owIR*!LQ!SMRlzX)mEJ7vTIu5U$Mtr<=>2gKXL9{DD`+3 z+ozL}c3Gb`eezyEZD-}ln?~oKXCFT$IrT}8!ehU46+6WYTU`7UEZ7~JSSndHJRIM2 u*M?6J3sPw9wJQkST)Ot_rLO&F^NY`J=VxKsf0u!Qfx*+&&t;ucLK6VvyDrWE literal 0 HcmV?d00001 diff --git a/tests/unit_tests/fixtures/bundle/assets/web/custom.css b/tests/unit_tests/fixtures/bundle/assets/web/custom.css new file mode 100644 index 00000000000..992b81c80e4 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/assets/web/custom.css @@ -0,0 +1,2 @@ +/* Dummy CSS for bundle testing */ +body { color: red; } diff --git a/tests/unit_tests/fixtures/bundle/assets/web/custom.js b/tests/unit_tests/fixtures/bundle/assets/web/custom.js new file mode 100644 index 00000000000..9be8a6b2dc9 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/assets/web/custom.js @@ -0,0 +1,2 @@ +// Dummy JS for bundle testing +console.log("test"); diff --git a/tests/unit_tests/fixtures/bundle/bundle_test.yaml b/tests/unit_tests/fixtures/bundle/bundle_test.yaml new file mode 100644 index 00000000000..f834a8d867f --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/bundle_test.yaml @@ -0,0 +1,60 @@ +esphome: + name: bundle-test + includes: + - includes/custom_sensor.h + +esp32: + board: esp32dev + framework: + type: esp-idf + +logger: + <<: !include common/base.yaml + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +api: + +ota: + - platform: esphome + password: !secret ota_password + +web_server: + port: 80 + css_include: assets/web/custom.css + js_include: assets/web/custom.js + +i2c: + sda: GPIO21 + scl: GPIO22 + +font: + - id: test_font + file: assets/fonts/test_font.ttf + size: 16 + +image: + - id: test_image + file: assets/images/logo.png + type: BINARY + resize: 16x16 + +animation: + - id: test_animation + file: assets/images/animation.gif + type: BINARY + resize: 16x16 + +display: + - platform: ssd1306_i2c + model: SSD1306_128X64 + address: 0x3C + lambda: |- + it.image(0, 0, id(test_image)); + +external_components: + - source: + type: local + path: local_components diff --git a/tests/unit_tests/fixtures/bundle/common/base.yaml b/tests/unit_tests/fixtures/bundle/common/base.yaml new file mode 100644 index 00000000000..58e1083e82d --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/common/base.yaml @@ -0,0 +1 @@ +level: DEBUG diff --git a/tests/unit_tests/fixtures/bundle/includes/custom_sensor.h b/tests/unit_tests/fixtures/bundle/includes/custom_sensor.h new file mode 100644 index 00000000000..7f0ff474ee2 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/includes/custom_sensor.h @@ -0,0 +1,3 @@ +// Dummy custom sensor header for bundle testing +#pragma once +#include "esphome/core/component.h" diff --git a/tests/unit_tests/fixtures/bundle/local_components/my_component/__init__.py b/tests/unit_tests/fixtures/bundle/local_components/my_component/__init__.py new file mode 100644 index 00000000000..aa9fc1474b1 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/local_components/my_component/__init__.py @@ -0,0 +1 @@ +# Dummy local external component for bundle testing diff --git a/tests/unit_tests/fixtures/bundle/local_components/my_component/my_component.h b/tests/unit_tests/fixtures/bundle/local_components/my_component/my_component.h new file mode 100644 index 00000000000..19b89ecc820 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/local_components/my_component/my_component.h @@ -0,0 +1,2 @@ +// Dummy component header for bundle testing +#pragma once diff --git a/tests/unit_tests/fixtures/bundle/secrets.yaml b/tests/unit_tests/fixtures/bundle/secrets.yaml new file mode 100644 index 00000000000..47acddb4d9f --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/secrets.yaml @@ -0,0 +1,4 @@ +wifi_ssid: "TestNetwork" +wifi_password: "TestPassword123" +api_key: "unused_secret_should_not_appear" +ota_password: "ota_test_password" diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py new file mode 100644 index 00000000000..b8b2d0ffd1a --- /dev/null +++ b/tests/unit_tests/test_bundle.py @@ -0,0 +1,1210 @@ +"""Tests for esphome.bundle module.""" + +from __future__ import annotations + +import io +import json +from pathlib import Path +import tarfile +from typing import Any + +import pytest + +from esphome.bundle import ( + BUNDLE_EXTENSION, + CURRENT_MANIFEST_VERSION, + MANIFEST_FILENAME, + BundleManifest, + ConfigBundleCreator, + ManifestKey, + _add_bytes_to_tar, + _default_target_dir, + _find_used_secret_keys, + extract_bundle, + is_bundle_path, + prepare_bundle_for_compile, + read_bundle_manifest, +) +from esphome.core import CORE, EsphomeError + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_bundle( + tmp_path: Path, + config_filename: str = "test.yaml", + config_content: str = "esphome:\n name: test\n", + manifest_overrides: dict[str, Any] | None = None, + extra_files: dict[str, bytes] | None = None, + *, + include_manifest: bool = True, + raw_members: list[tarfile.TarInfo] | None = None, +) -> Path: + """Create a minimal bundle tar.gz for testing.""" + bundle_path = tmp_path / f"device{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + if include_manifest: + manifest: dict[str, Any] = { + ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION, + ManifestKey.ESPHOME_VERSION: "2026.2.0-test", + ManifestKey.CONFIG_FILENAME: config_filename, + ManifestKey.FILES: [config_filename], + ManifestKey.HAS_SECRETS: False, + } + if manifest_overrides: + manifest.update(manifest_overrides) + _add_bytes_to_tar(tar, MANIFEST_FILENAME, json.dumps(manifest).encode()) + + _add_bytes_to_tar(tar, config_filename, config_content.encode()) + + if extra_files: + for name, data in extra_files.items(): + _add_bytes_to_tar(tar, name, data) + + if raw_members: + for info in raw_members: + tar.addfile(info, io.BytesIO(b"")) + + bundle_path.write_bytes(buf.getvalue()) + return bundle_path + + +def _setup_config_dir( + tmp_path: Path, + files: dict[str, str] | None = None, +) -> Path: + """Set up a fake config directory with files and configure CORE.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + config_yaml = "esphome:\n name: test\n" + (config_dir / "test.yaml").write_text(config_yaml) + + if files: + for rel_path, content in files.items(): + p = config_dir / rel_path + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content) + + CORE.config_path = config_dir / "test.yaml" + return config_dir + + +# --------------------------------------------------------------------------- +# is_bundle_path +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("filename", "expected"), + [ + (f"my_device{BUNDLE_EXTENSION}", True), + (f"MY_DEVICE{BUNDLE_EXTENSION.upper()}", True), + ("my_device.yaml", False), + ("my_device.tar.gz", False), + ("my_device.zip", False), + ("", False), + ], +) +def test_is_bundle_path(filename: str, expected: bool) -> None: + assert is_bundle_path(Path(filename)) is expected + + +# --------------------------------------------------------------------------- +# _default_target_dir +# --------------------------------------------------------------------------- + + +def test_default_target_dir_strips_extension() -> None: + p = Path(f"/builds/device{BUNDLE_EXTENSION}") + result = _default_target_dir(p) + assert result == Path("/builds/device") + + +def test_default_target_dir_no_extension() -> None: + p = Path("/builds/device.other") + result = _default_target_dir(p) + assert result == Path("/builds/device.other") + + +# --------------------------------------------------------------------------- +# _find_used_secret_keys +# --------------------------------------------------------------------------- + + +def test_find_used_secret_keys(tmp_path: Path) -> None: + yaml1 = tmp_path / "a.yaml" + yaml1.write_text("wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_pw\n") + yaml2 = tmp_path / "b.yaml" + yaml2.write_text("api:\n key: !secret api_key\n") + + keys = _find_used_secret_keys([yaml1, yaml2]) + assert keys == {"wifi_ssid", "wifi_pw", "api_key"} + + +def test_find_used_secret_keys_no_secrets(tmp_path: Path) -> None: + yaml1 = tmp_path / "a.yaml" + yaml1.write_text("esphome:\n name: test\n") + + keys = _find_used_secret_keys([yaml1]) + assert keys == set() + + +def test_find_used_secret_keys_missing_file(tmp_path: Path) -> None: + missing = tmp_path / "does_not_exist.yaml" + keys = _find_used_secret_keys([missing]) + assert keys == set() + + +def test_find_used_secret_keys_deduplicates(tmp_path: Path) -> None: + yaml1 = tmp_path / "a.yaml" + yaml1.write_text("a: !secret key1\nb: !secret key1\n") + + keys = _find_used_secret_keys([yaml1]) + assert keys == {"key1"} + + +# --------------------------------------------------------------------------- +# _add_bytes_to_tar +# --------------------------------------------------------------------------- + + +def test_add_bytes_to_tar_deterministic_metadata() -> None: + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + _add_bytes_to_tar(tar, "hello.txt", b"world") + + buf.seek(0) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + member = tar.getmember("hello.txt") + assert member.size == 5 + assert member.mtime == 0 + assert member.uid == 0 + assert member.gid == 0 + assert member.mode == 0o644 + assert tar.extractfile(member).read() == b"world" + + +# --------------------------------------------------------------------------- +# ManifestKey +# --------------------------------------------------------------------------- + + +def test_manifest_key_values() -> None: + assert ManifestKey.MANIFEST_VERSION == "manifest_version" + assert ManifestKey.ESPHOME_VERSION == "esphome_version" + assert ManifestKey.CONFIG_FILENAME == "config_filename" + assert ManifestKey.FILES == "files" + assert ManifestKey.HAS_SECRETS == "has_secrets" + + +def test_manifest_key_is_str() -> None: + """Verify ManifestKey values work as dict keys and JSON keys.""" + d: dict[str, int] = {ManifestKey.MANIFEST_VERSION: 1} + assert d["manifest_version"] == 1 + + +# --------------------------------------------------------------------------- +# extract_bundle +# --------------------------------------------------------------------------- + + +def test_extract_bundle_basic(tmp_path: Path) -> None: + bundle_path = _make_bundle(tmp_path) + target = tmp_path / "output" + + config_path = extract_bundle(bundle_path, target) + + assert config_path.is_file() + assert config_path.name == "test.yaml" + assert config_path.read_text().startswith("esphome:") + assert (target / MANIFEST_FILENAME).is_file() + + +def test_extract_bundle_default_target_dir(tmp_path: Path) -> None: + bundle_path = _make_bundle(tmp_path) + + config_path = extract_bundle(bundle_path) + + expected_dir = tmp_path / "device" + assert config_path.parent == expected_dir + + +def test_extract_bundle_missing_file(tmp_path: Path) -> None: + missing = tmp_path / f"missing{BUNDLE_EXTENSION}" + with pytest.raises(EsphomeError, match="Bundle file not found"): + extract_bundle(missing) + + +def test_extract_bundle_missing_manifest(tmp_path: Path) -> None: + bundle_path = _make_bundle(tmp_path, include_manifest=False) + with pytest.raises(EsphomeError, match="missing manifest.json"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_future_manifest_version(tmp_path: Path) -> None: + bundle_path = _make_bundle( + tmp_path, + manifest_overrides={ManifestKey.MANIFEST_VERSION: 999}, + ) + with pytest.raises(EsphomeError, match="newer than this ESPHome"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_missing_config_filename_in_manifest(tmp_path: Path) -> None: + """Manifest exists but is missing config_filename key.""" + bundle_path = tmp_path / f"bad{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + manifest = {ManifestKey.MANIFEST_VERSION: 1} + _add_bytes_to_tar(tar, MANIFEST_FILENAME, json.dumps(manifest).encode()) + _add_bytes_to_tar(tar, "test.yaml", b"esphome:\n name: test\n") + bundle_path.write_bytes(buf.getvalue()) + + with pytest.raises(EsphomeError, match="missing 'config_filename'"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_config_not_in_archive(tmp_path: Path) -> None: + """Manifest references a config file that isn't in the archive.""" + bundle_path = _make_bundle( + tmp_path, + config_filename="test.yaml", + manifest_overrides={ManifestKey.CONFIG_FILENAME: "missing.yaml"}, + ) + with pytest.raises(EsphomeError, match="was not found in the archive"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_with_extra_files(tmp_path: Path) -> None: + bundle_path = _make_bundle( + tmp_path, + extra_files={ + "common/base.yaml": b"level: DEBUG\n", + "includes/sensor.h": b"#pragma once\n", + }, + ) + target = tmp_path / "out" + extract_bundle(bundle_path, target) + + assert (target / "common" / "base.yaml").read_text() == "level: DEBUG\n" + assert (target / "includes" / "sensor.h").read_text() == "#pragma once\n" + + +# --------------------------------------------------------------------------- +# extract_bundle - security validation +# --------------------------------------------------------------------------- + + +def test_extract_bundle_rejects_absolute_path(tmp_path: Path) -> None: + info = tarfile.TarInfo(name="/etc/passwd") + info.size = 0 + bundle_path = _make_bundle(tmp_path, raw_members=[info]) + + with pytest.raises(EsphomeError, match="absolute path"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_rejects_path_traversal(tmp_path: Path) -> None: + info = tarfile.TarInfo(name="../../../etc/passwd") + info.size = 0 + bundle_path = _make_bundle(tmp_path, raw_members=[info]) + + with pytest.raises(EsphomeError, match="path traversal"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_rejects_backslash_path_traversal(tmp_path: Path) -> None: + info = tarfile.TarInfo(name="foo\\..\\..\\etc\\passwd") + info.size = 0 + bundle_path = _make_bundle(tmp_path, raw_members=[info]) + + with pytest.raises(EsphomeError, match="path traversal"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_rejects_symlink(tmp_path: Path) -> None: + info = tarfile.TarInfo(name="evil_link") + info.type = tarfile.SYMTYPE + info.linkname = "/etc/passwd" + info.size = 0 + bundle_path = _make_bundle(tmp_path, raw_members=[info]) + + with pytest.raises(EsphomeError, match="symlink"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_rejects_oversized( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Archive whose total decompressed size exceeds the limit is rejected.""" + # Lower the limit so we don't need huge test data + monkeypatch.setattr("esphome.bundle.MAX_DECOMPRESSED_SIZE", 100) + + bundle_path = _make_bundle( + tmp_path, + extra_files={"big.bin": b"\x00" * 200}, + ) + + with pytest.raises(EsphomeError, match="decompressed size exceeds"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_corrupted_tar(tmp_path: Path) -> None: + """Corrupted tar file raises EsphomeError.""" + bundle_path = tmp_path / f"bad{BUNDLE_EXTENSION}" + bundle_path.write_bytes(b"not a tar file at all") + + with pytest.raises(EsphomeError, match="Failed to extract bundle"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_malformed_manifest_json(tmp_path: Path) -> None: + """Invalid JSON in manifest.json raises EsphomeError.""" + bundle_path = tmp_path / f"badjson{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + _add_bytes_to_tar(tar, MANIFEST_FILENAME, b"{invalid json") + _add_bytes_to_tar(tar, "test.yaml", b"esphome:\n name: test\n") + bundle_path.write_bytes(buf.getvalue()) + + with pytest.raises(EsphomeError, match="malformed manifest.json"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_missing_manifest_version(tmp_path: Path) -> None: + """Manifest without manifest_version raises EsphomeError.""" + bundle_path = tmp_path / f"nover{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + manifest = {ManifestKey.CONFIG_FILENAME: "test.yaml"} + _add_bytes_to_tar(tar, MANIFEST_FILENAME, json.dumps(manifest).encode()) + _add_bytes_to_tar(tar, "test.yaml", b"esphome:\n name: test\n") + bundle_path.write_bytes(buf.getvalue()) + + with pytest.raises(EsphomeError, match="missing 'manifest_version'"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_invalid_manifest_version_type(tmp_path: Path) -> None: + """Non-integer manifest_version raises EsphomeError.""" + bundle_path = _make_bundle( + tmp_path, + manifest_overrides={ManifestKey.MANIFEST_VERSION: "not_an_int"}, + ) + + with pytest.raises(EsphomeError, match="must be a positive integer"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_manifest_version_zero(tmp_path: Path) -> None: + """manifest_version of 0 is rejected.""" + bundle_path = _make_bundle( + tmp_path, + manifest_overrides={ManifestKey.MANIFEST_VERSION: 0}, + ) + + with pytest.raises(EsphomeError, match="must be a positive integer"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_manifest_too_large( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Oversized manifest.json is rejected.""" + monkeypatch.setattr("esphome.bundle.MAX_MANIFEST_SIZE", 50) + + bundle_path = _make_bundle(tmp_path) + + with pytest.raises(EsphomeError, match="manifest.json too large"): + extract_bundle(bundle_path, tmp_path / "out") + + +def test_extract_bundle_manifest_not_regular_file(tmp_path: Path) -> None: + """manifest.json that is a directory entry raises EsphomeError.""" + bundle_path = tmp_path / f"dirmanifest{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + # Add manifest.json as a directory instead of a file + dir_info = tarfile.TarInfo(name=MANIFEST_FILENAME) + dir_info.type = tarfile.DIRTYPE + dir_info.size = 0 + tar.addfile(dir_info) + _add_bytes_to_tar(tar, "test.yaml", b"esphome:\n name: test\n") + bundle_path.write_bytes(buf.getvalue()) + + with pytest.raises(EsphomeError, match="not a regular file"): + extract_bundle(bundle_path, tmp_path / "out") + + +# --------------------------------------------------------------------------- +# read_bundle_manifest +# --------------------------------------------------------------------------- + + +def test_read_bundle_manifest_corrupted_tar(tmp_path: Path) -> None: + """Corrupted tar file raises EsphomeError via read_bundle_manifest.""" + bundle_path = tmp_path / f"bad{BUNDLE_EXTENSION}" + bundle_path.write_bytes(b"not a tar file") + + with pytest.raises(EsphomeError, match="Failed to read bundle"): + read_bundle_manifest(bundle_path) + + +def test_read_bundle_manifest(tmp_path: Path) -> None: + bundle_path = _make_bundle( + tmp_path, + manifest_overrides={ManifestKey.HAS_SECRETS: True}, + extra_files={"secrets.yaml": b"wifi: test\n"}, + ) + + manifest = read_bundle_manifest(bundle_path) + + assert isinstance(manifest, BundleManifest) + assert manifest.manifest_version == CURRENT_MANIFEST_VERSION + assert manifest.esphome_version == "2026.2.0-test" + assert manifest.config_filename == "test.yaml" + assert manifest.has_secrets is True + + +def test_read_bundle_manifest_minimal(tmp_path: Path) -> None: + """Manifest with only required fields.""" + bundle_path = tmp_path / f"min{BUNDLE_EXTENSION}" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tar: + manifest = { + ManifestKey.MANIFEST_VERSION: 1, + ManifestKey.CONFIG_FILENAME: "cfg.yaml", + } + _add_bytes_to_tar(tar, MANIFEST_FILENAME, json.dumps(manifest).encode()) + _add_bytes_to_tar(tar, "cfg.yaml", b"") + bundle_path.write_bytes(buf.getvalue()) + + result = read_bundle_manifest(bundle_path) + assert result.esphome_version == "unknown" + assert result.files == [] + assert result.has_secrets is False + + +# --------------------------------------------------------------------------- +# prepare_bundle_for_compile +# --------------------------------------------------------------------------- + + +def test_prepare_bundle_preserves_build_cache(tmp_path: Path) -> None: + bundle_path = _make_bundle(tmp_path) + target = tmp_path / "work" + target.mkdir() + + # Pre-existing build cache + esphome_dir = target / ".esphome" + esphome_dir.mkdir() + (esphome_dir / "build_state.json").write_text('{"cached": true}') + + pio_dir = target / ".pioenvs" + pio_dir.mkdir() + (pio_dir / "firmware.bin").write_bytes(b"\x00" * 100) + + config_path = prepare_bundle_for_compile(bundle_path, target) + + assert config_path.is_file() + # Build caches should be preserved + assert (target / ".esphome" / "build_state.json").read_text() == '{"cached": true}' + assert (target / ".pioenvs" / "firmware.bin").read_bytes() == b"\x00" * 100 + + +def test_prepare_bundle_cleans_old_config(tmp_path: Path) -> None: + bundle_path = _make_bundle(tmp_path) + target = tmp_path / "work" + target.mkdir() + + # Old config from previous extraction + (target / "old_config.yaml").write_text("old: true") + old_dir = target / "old_includes" + old_dir.mkdir() + (old_dir / "old.h").write_text("// old") + + prepare_bundle_for_compile(bundle_path, target) + + # Old files should be cleaned + assert not (target / "old_config.yaml").exists() + assert not (target / "old_includes").exists() + # New config should exist + assert (target / "test.yaml").is_file() + + +def test_prepare_bundle_missing_file(tmp_path: Path) -> None: + missing = tmp_path / f"missing{BUNDLE_EXTENSION}" + with pytest.raises(EsphomeError, match="Bundle file not found"): + prepare_bundle_for_compile(missing) + + +def test_prepare_bundle_cache_wins_over_bundle_content(tmp_path: Path) -> None: + """Pre-existing build cache is restored even if the bundle contains those dirs.""" + bundle_path = _make_bundle( + tmp_path, + extra_files={ + ".esphome/from_bundle.json": b'{"from": "bundle"}', + }, + ) + target = tmp_path / "work" + target.mkdir() + + # Pre-existing build cache + esphome_dir = target / ".esphome" + esphome_dir.mkdir() + (esphome_dir / "local_cache.json").write_text('{"from": "local"}') + + prepare_bundle_for_compile(bundle_path, target) + + # Local cache should win over bundle content + assert (target / ".esphome" / "local_cache.json").read_text() == '{"from": "local"}' + assert not (target / ".esphome" / "from_bundle.json").exists() + + +def test_prepare_bundle_default_target_dir(tmp_path: Path) -> None: + """prepare_bundle_for_compile uses default dir when target_dir is None.""" + bundle_path = _make_bundle(tmp_path) + + config_path = prepare_bundle_for_compile(bundle_path) + + expected_dir = tmp_path / "device" + assert config_path.parent == expected_dir + assert config_path.is_file() + + +# --------------------------------------------------------------------------- +# ConfigBundleCreator - file discovery +# --------------------------------------------------------------------------- + + +def test_discover_files_includes_config(tmp_path: Path) -> None: + _setup_config_dir(tmp_path) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + + +def test_discover_files_finds_path_objects(tmp_path: Path) -> None: + """Path objects in validated config are discovered.""" + config_dir = _setup_config_dir( + tmp_path, + files={"assets/font.ttf": "fake font data"}, + ) + + config: dict[str, Any] = {"font": [{"file": config_dir / "assets" / "font.ttf"}]} + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "assets/font.ttf" in paths + + +def test_discover_files_finds_absolute_string_paths(tmp_path: Path) -> None: + """Absolute string paths in validated config are discovered.""" + config_dir = _setup_config_dir( + tmp_path, + files={"assets/logo.png": "fake png data"}, + ) + + abs_path = str(config_dir / "assets" / "logo.png") + config: dict[str, Any] = {"image": [{"file": abs_path}]} + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "assets/logo.png" in paths + + +def test_discover_files_skips_non_path_prefixes(tmp_path: Path) -> None: + """Remote URLs and special prefixes are not treated as file paths.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "font": [ + {"file": "https://example.com/font.ttf"}, + {"file": "mdi:home"}, + {"file": "http://example.com/icon.png"}, + ] + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + # Only the config file itself + assert len(files) == 1 + assert files[0].path == "test.yaml" + + +def test_discover_files_skips_multiline_strings(tmp_path: Path) -> None: + """Lambda/template strings are not treated as file paths.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "sensor": [{"lambda": "auto val = id(sensor1);\nreturn val;"}] + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + assert len(files) == 1 + + +def test_discover_files_deduplicates(tmp_path: Path) -> None: + """Same file referenced twice is only included once.""" + config_dir = _setup_config_dir( + tmp_path, + files={"cert.pem": "fake cert"}, + ) + + abs_path = str(config_dir / "cert.pem") + config: dict[str, Any] = { + "a": {"cert": abs_path}, + "b": {"cert": abs_path}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + cert_files = [f for f in files if f.path == "cert.pem"] + assert len(cert_files) == 1 + + +def test_discover_files_skips_outside_config_dir(tmp_path: Path) -> None: + """Files outside the config directory are skipped.""" + _setup_config_dir(tmp_path) + + outside_file = tmp_path / "outside.pem" + outside_file.write_text("outside cert") + + config: dict[str, Any] = {"cert": str(outside_file)} + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "outside.pem" not in paths + + +def test_discover_files_esphome_includes(tmp_path: Path) -> None: + """Paths listed in esphome.includes are discovered.""" + _setup_config_dir( + tmp_path, + files={"my_sensor.h": "#pragma once\n"}, + ) + + config: dict[str, Any] = { + "esphome": {"includes": ["my_sensor.h"]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "my_sensor.h" in paths + + +def test_discover_files_esphome_includes_directory(tmp_path: Path) -> None: + """esphome.includes pointing to a directory adds all files.""" + _setup_config_dir( + tmp_path, + files={ + "my_lib/a.h": "// a", + "my_lib/b.cpp": "// b", + }, + ) + + config: dict[str, Any] = { + "esphome": {"includes": ["my_lib"]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "my_lib/a.h" in paths + assert "my_lib/b.cpp" in paths + + +def test_discover_files_esphome_includes_skips_system(tmp_path: Path) -> None: + """System includes like <Arduino.h> are not added.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "esphome": {"includes": ["<Arduino.h>"]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert len(paths) == 1 # Just test.yaml + + +def test_discover_files_external_components_local(tmp_path: Path) -> None: + """external_components with type: local adds the directory.""" + _setup_config_dir( + tmp_path, + files={ + "components/my_comp/__init__.py": "# comp", + "components/my_comp/sensor.py": "# sensor", + }, + ) + + config: dict[str, Any] = { + "external_components": [{"source": {"type": "local", "path": "components"}}], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "components/my_comp/__init__.py" in paths + assert "components/my_comp/sensor.py" in paths + + +def test_discover_files_external_components_skips_pycache(tmp_path: Path) -> None: + """__pycache__ directories inside local external_components are excluded.""" + _setup_config_dir( + tmp_path, + files={ + "components/my_comp/__init__.py": "# comp", + "components/my_comp/__pycache__/module.cpython-313.pyc": "bytecode", + }, + ) + + config: dict[str, Any] = { + "external_components": [{"source": {"type": "local", "path": "components"}}], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "components/my_comp/__init__.py" in paths + assert not any("__pycache__" in p for p in paths) + + +def test_discover_files_external_components_non_dict_source(tmp_path: Path) -> None: + """external_components with string source (e.g. github shorthand) is skipped.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "external_components": [{"source": "github://user/repo@main"}], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + # Only the config file itself - no crash from non-dict source + assert len(files) == 1 + assert files[0].path == "test.yaml" + + +def test_discover_files_nested_config_values(tmp_path: Path) -> None: + """Deeply nested Path objects in lists/dicts are found.""" + config_dir = _setup_config_dir( + tmp_path, + files={"deep/file.pem": "cert data"}, + ) + + config: dict[str, Any] = { + "level1": {"level2": [{"level3": config_dir / "deep" / "file.pem"}]} + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "deep/file.pem" in paths + + +def test_discover_files_idempotent_secrets(tmp_path: Path) -> None: + """Calling discover_files twice does not accumulate secrets paths.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "secrets.yaml").write_text("k: v\n") + (config_dir / "test.yaml").write_text("a: !secret k\n") + + creator = ConfigBundleCreator({}) + files1 = creator.discover_files() + files2 = creator.discover_files() + + # Both calls should return the same result (secrets not accumulated) + paths1 = [f.path for f in files1] + paths2 = [f.path for f in files2] + assert "secrets.yaml" in paths1 + assert paths1 == paths2 + + +def test_discover_files_skips_missing_file(tmp_path: Path) -> None: + """_add_file logs warning for non-existent files via includes.""" + _setup_config_dir(tmp_path) + + # Include references a file that doesn't exist on disk + config: dict[str, Any] = { + "esphome": {"includes": ["nonexistent.h"]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "nonexistent.h" not in paths + + +def test_discover_files_skips_missing_directory(tmp_path: Path) -> None: + """_add_directory logs warning for non-existent directories.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "external_components": [ + {"source": {"type": "local", "path": "nonexistent_dir"}} + ], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + # Only the config file + assert len(files) == 1 + + +def test_discover_files_yaml_reload_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """YAML reload failure during include discovery is handled gracefully.""" + _setup_config_dir(tmp_path) + + def _raise_error(*args, **kwargs): + raise EsphomeError("parse error") + + monkeypatch.setattr("esphome.yaml_util.load_yaml", _raise_error) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + # Should still have the config file at minimum + paths = [f.path for f in files] + assert "test.yaml" in paths + + +def test_discover_files_esphome_includes_c(tmp_path: Path) -> None: + """Paths listed in esphome.includes_c are discovered.""" + _setup_config_dir( + tmp_path, + files={"my_code.c": "// c code"}, + ) + + config: dict[str, Any] = { + "esphome": {"includes_c": ["my_code.c"]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "my_code.c" in paths + + +def test_discover_files_external_components_non_local_type(tmp_path: Path) -> None: + """external_components with type != 'local' are skipped.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "external_components": [ + {"source": {"type": "git", "url": "https://github.com/user/repo"}} + ], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + assert len(files) == 1 + + +def test_discover_files_external_components_no_path(tmp_path: Path) -> None: + """external_components with local type but missing path are skipped.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "external_components": [{"source": {"type": "local"}}], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + assert len(files) == 1 + + +def test_discover_files_external_components_absolute_path(tmp_path: Path) -> None: + """external_components with absolute path are resolved correctly.""" + config_dir = _setup_config_dir( + tmp_path, + files={"ext/comp/__init__.py": "# comp"}, + ) + + abs_path = str(config_dir / "ext") + config: dict[str, Any] = { + "external_components": [{"source": {"type": "local", "path": abs_path}}], + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "ext/comp/__init__.py" in paths + + +def test_discover_files_relative_string_with_known_extension(tmp_path: Path) -> None: + """Relative strings with known extensions are resolved and warned.""" + _setup_config_dir( + tmp_path, + files={"my_cert.pem": "cert data"}, + ) + + config: dict[str, Any] = { + "component": {"cert": "my_cert.pem"}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "my_cert.pem" in paths + + +def test_discover_files_relative_string_missing_file(tmp_path: Path) -> None: + """Relative strings with known extensions that don't exist are skipped.""" + _setup_config_dir(tmp_path) + + config: dict[str, Any] = { + "component": {"cert": "nonexistent.pem"}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + assert len(files) == 1 + + +def test_discover_files_esphome_includes_absolute_path(tmp_path: Path) -> None: + """esphome.includes with absolute path is handled.""" + config_dir = _setup_config_dir( + tmp_path, + files={"my_code.h": "#pragma once"}, + ) + + config: dict[str, Any] = { + "esphome": {"includes": [str(config_dir / "my_code.h")]}, + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "my_code.h" in paths + + +def test_discover_files_walk_tuple_values(tmp_path: Path) -> None: + """Tuples in config are walked like lists.""" + config_dir = _setup_config_dir( + tmp_path, + files={"a.pem": "cert"}, + ) + + config: dict[str, Any] = { + "items": (config_dir / "a.pem",), + } + creator = ConfigBundleCreator(config) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "a.pem" in paths + + +# --------------------------------------------------------------------------- +# ConfigBundleCreator - create_bundle +# --------------------------------------------------------------------------- + + +def test_create_bundle_produces_valid_archive(tmp_path: Path) -> None: + _setup_config_dir(tmp_path) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert isinstance(result.data, bytes) + assert len(result.data) > 0 + + # Verify it's a valid tar.gz + buf = io.BytesIO(result.data) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + names = tar.getnames() + assert MANIFEST_FILENAME in names + assert "test.yaml" in names + + +def test_create_bundle_manifest_content(tmp_path: Path) -> None: + _setup_config_dir(tmp_path) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + manifest = result.manifest + assert manifest[ManifestKey.MANIFEST_VERSION] == CURRENT_MANIFEST_VERSION + assert manifest[ManifestKey.CONFIG_FILENAME] == "test.yaml" + assert "test.yaml" in manifest[ManifestKey.FILES] + + +def test_create_bundle_filters_secrets(tmp_path: Path) -> None: + config_dir = _setup_config_dir(tmp_path) + + # Create secrets.yaml with multiple secrets + secrets = config_dir / "secrets.yaml" + secrets.write_text( + "wifi_ssid: MyNetwork\nwifi_pw: secret123\nunused: should_not_appear\n" + ) + + # Config that references only some secrets + config_yaml = "wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_pw\n" + (config_dir / "test.yaml").write_text(config_yaml) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + # Extract and check secrets + buf = io.BytesIO(result.data) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + secrets_data = tar.extractfile("secrets.yaml").read().decode() + + assert "wifi_ssid" in secrets_data + assert "wifi_pw" in secrets_data + assert "unused" not in secrets_data + assert "should_not_appear" not in secrets_data + + +def test_create_bundle_no_secrets(tmp_path: Path) -> None: + _setup_config_dir(tmp_path) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert result.manifest[ManifestKey.HAS_SECRETS] is False + + +def test_create_bundle_secrets_load_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Secrets file that fails to load during filtering is skipped gracefully.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "secrets.yaml").write_text("k: v\n") + (config_dir / "test.yaml").write_text("a: !secret k\n") + + from esphome import yaml_util as yu + + original_load = yu.load_yaml + + def _failing_on_filter(fname, *args, clear_secrets=True, **kwargs): + # Fail only when _build_filtered_secrets calls with clear_secrets=False + if not clear_secrets and "secrets" in str(fname): + raise EsphomeError("corrupt secrets") + return original_load(fname, *args, clear_secrets=clear_secrets, **kwargs) + + monkeypatch.setattr(yu, "load_yaml", _failing_on_filter) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + # Should succeed without secrets since the filtered load failed + assert result.manifest[ManifestKey.HAS_SECRETS] is False + + +def test_create_bundle_secrets_non_dict(tmp_path: Path) -> None: + """Secrets file that parses to non-dict is skipped.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "secrets.yaml").write_text("- item1\n- item2\n") + (config_dir / "test.yaml").write_text("a: !secret k\n") + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert result.manifest[ManifestKey.HAS_SECRETS] is False + + +def test_create_bundle_secrets_no_matching_keys(tmp_path: Path) -> None: + """Secrets with no matching keys produces empty filtered result.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "secrets.yaml").write_text("other_key: value\n") + (config_dir / "test.yaml").write_text("a: !secret nonexistent\n") + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert result.manifest[ManifestKey.HAS_SECRETS] is False + + +def test_create_bundle_deterministic_order(tmp_path: Path) -> None: + """Files are added in sorted order for reproducibility.""" + _setup_config_dir( + tmp_path, + files={ + "z_last.h": "// z", + "a_first.h": "// a", + "m_middle.h": "// m", + }, + ) + + config: dict[str, Any] = { + "esphome": {"includes": ["z_last.h", "a_first.h", "m_middle.h"]}, + } + creator = ConfigBundleCreator(config) + result = creator.create_bundle() + + buf = io.BytesIO(result.data) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + names = tar.getnames() + + # manifest.json is always first, then files in sorted order + assert names[0] == MANIFEST_FILENAME + file_names = [n for n in names if n != MANIFEST_FILENAME] + assert file_names == sorted(file_names) + + +# --------------------------------------------------------------------------- +# Round-trip: create then extract +# --------------------------------------------------------------------------- + + +def test_bundle_round_trip(tmp_path: Path) -> None: + """A bundle created by ConfigBundleCreator can be extracted.""" + _setup_config_dir( + tmp_path, + files={"include.h": "#pragma once\n"}, + ) + config: dict[str, Any] = {"esphome": {"includes": ["include.h"]}} + + creator = ConfigBundleCreator(config) + result = creator.create_bundle() + + bundle_path = tmp_path / f"roundtrip{BUNDLE_EXTENSION}" + bundle_path.write_bytes(result.data) + + target = tmp_path / "extracted" + config_path = extract_bundle(bundle_path, target) + + assert config_path.is_file() + assert (target / "include.h").read_text() == "#pragma once\n" + + manifest = read_bundle_manifest(bundle_path) + assert manifest.config_filename == "test.yaml" + assert "include.h" in manifest.files + + +def test_bundle_round_trip_with_secrets(tmp_path: Path) -> None: + """Secrets survive round-trip with correct filtering.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "secrets.yaml").write_text("key1: val1\nkey2: val2\nunused: nope\n") + (config_dir / "test.yaml").write_text("a: !secret key1\nb: !secret key2\n") + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + bundle_path = tmp_path / f"secrets{BUNDLE_EXTENSION}" + bundle_path.write_bytes(result.data) + + target = tmp_path / "extracted" + extract_bundle(bundle_path, target) + + secrets_content = (target / "secrets.yaml").read_text() + assert "key1" in secrets_content + assert "key2" in secrets_content + assert "unused" not in secrets_content + + manifest = read_bundle_manifest(bundle_path) + assert manifest.has_secrets is True diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 115ce38c938..85536d2f1ce 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -24,6 +24,7 @@ from esphome.__main__ import ( _make_crystal_freq_callback, choose_upload_log_host, command_analyze_memory, + command_bundle, command_clean_all, command_rename, command_update_all, @@ -47,6 +48,7 @@ from esphome.__main__ import ( upload_using_picotool, upload_using_platformio, ) +from esphome.bundle import BUNDLE_EXTENSION, BundleFile, BundleResult from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( CONF_API, @@ -1101,6 +1103,8 @@ class MockArgs: name: str | None = None dashboard: bool = False reset: bool = False + list_only: bool = False + output: str | None = None def test_upload_program_serial_esp32( @@ -3765,6 +3769,198 @@ esp32: assert "secrets.yaml" not in summary_section +# --- command_bundle tests --- + + +def test_command_bundle_list_only( + tmp_path: Path, + capsys: CaptureFixture[str], +) -> None: + """Test command_bundle with --list-only prints files and returns 0.""" + mock_files = [ + BundleFile(path="device.yaml", source=tmp_path / "device.yaml"), + BundleFile(path="secrets.yaml", source=tmp_path / "secrets.yaml"), + BundleFile(path="common/base.yaml", source=tmp_path / "common" / "base.yaml"), + ] + + args = MockArgs(list_only=True) + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.discover_files.return_value = mock_files + + with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator): + result = command_bundle(args, config) + + assert result == 0 + captured = capsys.readouterr() + # Files should be printed in sorted order + assert "common/base.yaml" in captured.out + assert "device.yaml" in captured.out + assert "secrets.yaml" in captured.out + + +def test_command_bundle_list_only_empty( + tmp_path: Path, + capsys: CaptureFixture[str], +) -> None: + """Test command_bundle --list-only with no files discovered.""" + args = MockArgs(list_only=True) + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.discover_files.return_value = [] + + with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator): + result = command_bundle(args, config) + + assert result == 0 + + +def test_command_bundle_creates_archive(tmp_path: Path) -> None: + """Test command_bundle creates archive at default output path.""" + CORE.config_path = tmp_path / "mydevice.yaml" + + mock_result = BundleResult( + data=b"fake-tar-gz-data", + manifest={"manifest_version": 1}, + files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")], + ) + + args = MockArgs() + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.create_bundle.return_value = mock_result + + with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator): + result = command_bundle(args, config) + + assert result == 0 + output_path = tmp_path / f"mydevice{BUNDLE_EXTENSION}" + assert output_path.exists() + assert output_path.read_bytes() == b"fake-tar-gz-data" + + +def test_command_bundle_custom_output(tmp_path: Path) -> None: + """Test command_bundle with -o custom output path.""" + custom_output = tmp_path / "output" / "custom.esphomebundle.tar.gz" + mock_result = BundleResult( + data=b"custom-output-data", + manifest={"manifest_version": 1}, + files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")], + ) + + args = MockArgs(output=str(custom_output)) + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.create_bundle.return_value = mock_result + + with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator): + result = command_bundle(args, config) + + assert result == 0 + assert custom_output.exists() + assert custom_output.read_bytes() == b"custom-output-data" + + +def test_command_bundle_creates_parent_dirs(tmp_path: Path) -> None: + """Test command_bundle creates parent directories for output path.""" + nested_output = tmp_path / "deep" / "nested" / "dir" / "out.tar.gz" + mock_result = BundleResult( + data=b"data", + manifest={"manifest_version": 1}, + files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")], + ) + + args = MockArgs(output=str(nested_output)) + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.create_bundle.return_value = mock_result + + with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator): + result = command_bundle(args, config) + + assert result == 0 + assert nested_output.exists() + + +def test_command_bundle_logs_info( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test command_bundle logs bundle creation info.""" + CORE.config_path = tmp_path / "mydevice.yaml" + + mock_result = BundleResult( + data=b"x" * 2048, + manifest={"manifest_version": 1}, + files=[ + BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml"), + BundleFile(path="secrets.yaml", source=tmp_path / "secrets.yaml"), + ], + ) + + args = MockArgs() + config: dict[str, Any] = {} + + mock_creator = MagicMock() + mock_creator.create_bundle.return_value = mock_result + + with ( + patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator), + caplog.at_level(logging.INFO), + ): + result = command_bundle(args, config) + + assert result == 0 + assert "Bundle created" in caplog.text + assert "2 files" in caplog.text + assert "2.0 KB" in caplog.text + + +def test_run_esphome_bundle_detection(tmp_path: Path) -> None: + """Test run_esphome detects .esphomebundle.tar.gz and extracts it.""" + bundle_path = tmp_path / f"device{BUNDLE_EXTENSION}" + bundle_path.write_bytes(b"fake-bundle") + + extracted_yaml = tmp_path / "extracted" / "device.yaml" + + with ( + patch("esphome.bundle.is_bundle_path", return_value=True) as mock_is_bundle, + patch( + "esphome.bundle.prepare_bundle_for_compile", + return_value=extracted_yaml, + ) as mock_prepare, + patch("esphome.__main__.read_config", return_value=None), + ): + result = run_esphome(["esphome", "compile", str(bundle_path)]) + + mock_is_bundle.assert_called_once() + mock_prepare.assert_called_once_with(bundle_path) + # read_config returns None → exit code 2 + assert result == 2 + + +def test_run_esphome_non_bundle_skips_extraction(tmp_path: Path) -> None: + """Test run_esphome does not extract for regular .yaml files.""" + yaml_file = tmp_path / "device.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + with ( + patch("esphome.bundle.is_bundle_path", return_value=False) as mock_is_bundle, + patch("esphome.bundle.prepare_bundle_for_compile") as mock_prepare, + patch("esphome.__main__.read_config", return_value=None), + ): + result = run_esphome(["esphome", "compile", str(yaml_file)]) + + mock_is_bundle.assert_called_once() + mock_prepare.assert_not_called() + assert result == 2 + + def test_get_configured_xtal_freq_reads_sdkconfig(tmp_path: Path) -> None: """Test reading XTAL_FREQ from sdkconfig.""" CORE.name = "test-device" diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 667b5938190..0342d12540e 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -323,6 +323,60 @@ def test_dump_sort_keys() -> None: assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:") +# --------------------------------------------------------------------------- +# track_yaml_loads +# --------------------------------------------------------------------------- + + +def test_track_yaml_loads_records_files(tmp_path: Path) -> None: + """track_yaml_loads records every file loaded inside the context.""" + yaml_file = tmp_path / "test.yaml" + yaml_file.write_text("key: value\n") + + with yaml_util.track_yaml_loads() as loaded: + yaml_util.load_yaml(yaml_file) + + assert len(loaded) == 1 + assert loaded[0] == yaml_file.resolve() + + +def test_track_yaml_loads_records_includes(tmp_path: Path) -> None: + """track_yaml_loads records nested !include files.""" + inc = tmp_path / "included.yaml" + inc.write_text("included_key: 42\n") + main = tmp_path / "main.yaml" + main.write_text("child: !include included.yaml\n") + + with yaml_util.track_yaml_loads() as loaded: + yaml_util.load_yaml(main) + + resolved = [p.name for p in loaded] + assert "main.yaml" in resolved + assert "included.yaml" in resolved + + +def test_track_yaml_loads_empty_outside_context(tmp_path: Path) -> None: + """Files loaded outside the context are not recorded.""" + yaml_file = tmp_path / "test.yaml" + yaml_file.write_text("key: value\n") + + with yaml_util.track_yaml_loads() as loaded: + pass # load nothing inside + + yaml_util.load_yaml(yaml_file) + assert loaded == [] + + +def test_track_yaml_loads_cleanup_on_exception(tmp_path: Path) -> None: + """Listener is removed even if the body raises.""" + before = len(yaml_util._load_listeners) + + with pytest.raises(RuntimeError), yaml_util.track_yaml_loads(): + raise RuntimeError("boom") + + assert len(yaml_util._load_listeners) == before + + @pytest.mark.parametrize( "data", [ From ac14b9e5584d8c2bea962529522aaa46df8a41e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:40:21 -1000 Subject: [PATCH 1287/2056] Bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#15541) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba6db99b84c..9e8a040888d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: skip-existing: true From 9d396cea5a3d5ad5d1ae50f29c81825b9d5c3120 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:56:25 -0400 Subject: [PATCH 1288/2056] [grove_tb6612fng] Move direction logic from Python to C++ to fix lambda crash (#15513) --- esphome/components/grove_tb6612fng/__init__.py | 4 +--- esphome/components/grove_tb6612fng/grove_tb6612fng.h | 10 +++++++++- tests/components/grove_tb6612fng/common.yaml | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 210e2f7babb..27a47953b34 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -80,11 +80,9 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) - template_speed = ( - template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed - ) cg.add(var.set_channel(template_channel)) cg.add(var.set_speed(template_speed)) + cg.add(var.set_direction(config[CONF_DIRECTION] == "FORWARD")) return var diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index a36cb85cffc..bf471632267 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -168,11 +168,19 @@ class GROVETB6612FNGMotorRunAction : public Action<Ts...>, public Parented<Grove TEMPLATABLE_VALUE(uint8_t, channel) TEMPLATABLE_VALUE(uint16_t, speed) + void set_direction(bool forward) { this->forward_ = forward; } + void play(const Ts &...x) override { auto channel = this->channel_.value(x...); - auto speed = this->speed_.value(x...); + int16_t speed = this->speed_.value(x...); + if (!this->forward_) { + speed = -speed; + } this->parent_->dc_motor_run(channel, speed); } + + protected: + bool forward_{true}; }; template<typename... Ts> diff --git a/tests/components/grove_tb6612fng/common.yaml b/tests/components/grove_tb6612fng/common.yaml index 52d5ead96e3..7c6d65e9a6c 100644 --- a/tests/components/grove_tb6612fng/common.yaml +++ b/tests/components/grove_tb6612fng/common.yaml @@ -6,6 +6,11 @@ esphome: speed: 255 direction: BACKWARD id: test_motor + - grove_tb6612fng.run: + channel: 0 + speed: !lambda "return 200;" + direction: BACKWARD + id: test_motor - grove_tb6612fng.stop: channel: 1 id: test_motor From 186525e77d127234887b6a9c05aa3a4b4136134c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:57:26 -0400 Subject: [PATCH 1289/2056] [ld2420] Fix select options wrapped in extra list (#15524) --- esphome/components/ld2420/select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py index 6ccc00b41c1..3d078eba681 100644 --- a/esphome/components/ld2420/select/__init__.py +++ b/esphome/components/ld2420/select/__init__.py @@ -28,7 +28,7 @@ async def to_code(config): if operating_mode_config := config.get(CONF_OPERATING_MODE): sel = await select.new_select( operating_mode_config, - options=[CONF_SELECTS], + options=CONF_SELECTS, ) await cg.register_parented(sel, config[CONF_LD2420_ID]) cg.add(LD2420_component.set_operating_mode_select(sel)) From 687753b0bebe95b0510f32f932042d7571f7a080 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:03:55 -0400 Subject: [PATCH 1290/2056] [lightwaverf] Fix write pin using input schema instead of output (#15525) --- esphome/components/lightwaverf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index acbbbb4de97..46c400cb0e4 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LIGHTWAVERFComponent), cv.Optional(CONF_READ_PIN, default=13): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_WRITE_PIN, default=14): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_WRITE_PIN, default=14): pins.internal_gpio_output_pin_schema, } ).extend(cv.polling_component_schema("1s")) From 17ec5389d88e9562eee2fcc80acdbe44b8e73cf7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:07:28 -0400 Subject: [PATCH 1291/2056] [mcp4461] Fix terminal disable passing string where C++ expects char (#15528) --- esphome/components/mcp4461/output/__init__.py | 6 +++--- tests/components/mcp4461/common.yaml | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index 02bdbefed5d..0d145d81d30 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -48,11 +48,11 @@ async def to_code(config): config[CONF_CHANNEL], ) if not config[CONF_TERMINAL_A]: - cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "a")) + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("a"))) if not config[CONF_TERMINAL_B]: - cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "b")) + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("b"))) if not config[CONF_TERMINAL_W]: - cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "w")) + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("w"))) if CONF_INITIAL_VALUE in config: cg.add( parent.set_initial_value(config[CONF_CHANNEL], config[CONF_INITIAL_VALUE]) diff --git a/tests/components/mcp4461/common.yaml b/tests/components/mcp4461/common.yaml index 92fd789dcb9..71e2528aa49 100644 --- a/tests/components/mcp4461/common.yaml +++ b/tests/components/mcp4461/common.yaml @@ -22,3 +22,11 @@ output: id: digipot_wiper_4 mcp4461_id: mcp4461_digipot_01 channel: D + + - platform: mcp4461 + id: digipot_wiper_5 + mcp4461_id: mcp4461_digipot_01 + channel: A + terminal_a: false + terminal_b: false + terminal_w: false From d354747da041f4189c6fd17416429d9735d119c7 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:10:56 +0200 Subject: [PATCH 1292/2056] [nextion] Fix format specifiers and error message typos in command handlers (#15542) --- esphome/components/nextion/nextion.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 4a15cbe64fa..6b806e0988f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -706,7 +706,7 @@ void Nextion::process_nextion_commands_() { auto index = to_process.find('\0'); if (index == std::string::npos || (to_process_length - index - 1) < 1) { ESP_LOGE(TAG, "Bad switch data (0x90)"); - ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index); + ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index); break; } @@ -732,7 +732,7 @@ void Nextion::process_nextion_commands_() { auto index = to_process.find('\0'); if (index == std::string::npos || (to_process_length - index - 1) != 4) { ESP_LOGE(TAG, "Bad sensor data (0x91)"); - ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index); + ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index); break; } @@ -765,7 +765,7 @@ void Nextion::process_nextion_commands_() { auto index = to_process.find('\0'); if (index == std::string::npos || (to_process_length - index - 1) < 1) { ESP_LOGE(TAG, "Bad text data (0x92)"); - ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index); + ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index); break; } @@ -798,8 +798,8 @@ void Nextion::process_nextion_commands_() { // Get variable name auto index = to_process.find('\0'); if (index == std::string::npos || (to_process_length - index - 1) < 1) { - ESP_LOGE(TAG, "Bad binary data (0x92)"); - ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index); + ESP_LOGE(TAG, "Bad binary data (0x93)"); + ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index); break; } From 2fe6cb392bd687c21f9967f042d7927391ba8217 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:40:43 -0400 Subject: [PATCH 1293/2056] [rotary_encoder] Fix set_value action accepting any sensor ID (#15535) --- esphome/components/rotary_encoder/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 20c757f093c..246db023f45 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -120,7 +120,7 @@ async def to_code(config): RotaryEncoderSetValueAction, cv.Schema( { - cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_ID): cv.use_id(RotaryEncoderSensor), cv.Required(CONF_VALUE): cv.templatable(cv.int_), } ), From 4ebfe71b8fa5cc9eb8c2ce2a1db31a86182dbc3b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:42:33 -0400 Subject: [PATCH 1294/2056] [seeed_mr24hpc1] Move baud rate validation to FINAL_VALIDATE_SCHEMA (#15536) --- esphome/components/seeed_mr24hpc1/__init__.py | 1 + esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/seeed_mr24hpc1/__init__.py b/esphome/components/seeed_mr24hpc1/__init__.py index e80470bde11..f71239d18cc 100644 --- a/esphome/components/seeed_mr24hpc1/__init__.py +++ b/esphome/components/seeed_mr24hpc1/__init__.py @@ -33,6 +33,7 @@ CONFIG_SCHEMA = ( # This authentication mode requires that the device must have transmit and receive functionality, a parity mode of "NONE", and a stop bit of one. FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "seeed_mr24hpc1", + baud_rate=115200, require_tx=True, require_rx=True, parity="NONE", diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index c9fe3a2e6ef..b44c5ce83d3 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -62,8 +62,6 @@ void MR24HPC1Component::dump_config() { // Initialisation functions void MR24HPC1Component::setup() { - this->check_uart_settings(115200); - #ifdef USE_NUMBER if (this->custom_mode_number_ != nullptr) { this->custom_mode_number_->publish_state(0); // Zero out the custom mode From 3ca3cdc5e20b17b1506eff4b8dda26a453ee80cd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:44:28 -0400 Subject: [PATCH 1295/2056] [multiple] Fix missing entity base classes in Python class declarations (#15534) --- esphome/components/bh1900nux/sensor.py | 2 +- esphome/components/gl_r01_i2c/sensor.py | 2 +- esphome/components/ld2420/select/__init__.py | 2 +- esphome/components/sdp3x/sensor.py | 5 ++++- esphome/components/sen0321/sensor.py | 2 +- esphome/components/sen21231/sensor.py | 2 +- esphome/components/tc74/sensor.py | 4 +++- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/bh1900nux/sensor.py b/esphome/components/bh1900nux/sensor.py index 5e1c0395aff..a70db3555ad 100644 --- a/esphome/components/bh1900nux/sensor.py +++ b/esphome/components/bh1900nux/sensor.py @@ -12,7 +12,7 @@ CODEOWNERS = ["@B48D81EFCC"] sensor_ns = cg.esphome_ns.namespace("bh1900nux") BH1900NUXSensor = sensor_ns.class_( - "BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice + "BH1900NUXSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) CONFIG_SCHEMA = ( diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py index 58db72540ed..6a8d47213cb 100644 --- a/esphome/components/gl_r01_i2c/sensor.py +++ b/esphome/components/gl_r01_i2c/sensor.py @@ -13,7 +13,7 @@ DEPENDENCIES = ["i2c"] gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c") GLR01I2CComponent = gl_r01_i2c_ns.class_( - "GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent + "GLR01I2CComponent", sensor.Sensor, i2c.I2CDevice, cg.PollingComponent ) CONFIG_SCHEMA = ( diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py index 3d078eba681..b9059c120fc 100644 --- a/esphome/components/ld2420/select/__init__.py +++ b/esphome/components/ld2420/select/__init__.py @@ -12,7 +12,7 @@ CONF_SELECTS = [ "Simple", ] -LD2420Select = ld2420_ns.class_("LD2420Select", cg.Component) +LD2420Select = ld2420_ns.class_("LD2420Select", select.Select, cg.Component) CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 169ed374ed0..be2eec7bafe 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -14,7 +14,10 @@ CODEOWNERS = ["@Azimath"] sdp3x_ns = cg.esphome_ns.namespace("sdp3x") SDP3XComponent = sdp3x_ns.class_( - "SDP3XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice + "SDP3XComponent", + sensor.Sensor, + cg.PollingComponent, + sensirion_common.SensirionI2CDevice, ) diff --git a/esphome/components/sen0321/sensor.py b/esphome/components/sen0321/sensor.py index e1c1d4e94be..3910e6e4c91 100644 --- a/esphome/components/sen0321/sensor.py +++ b/esphome/components/sen0321/sensor.py @@ -12,7 +12,7 @@ DEPENDENCIES = ["i2c"] sen0321_sensor_ns = cg.esphome_ns.namespace("sen0321_sensor") Sen0321Sensor = sen0321_sensor_ns.class_( - "Sen0321Sensor", cg.PollingComponent, i2c.I2CDevice + "Sen0321Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) CONFIG_SCHEMA = ( diff --git a/esphome/components/sen21231/sensor.py b/esphome/components/sen21231/sensor.py index 52cecbfb693..781a1213ac5 100644 --- a/esphome/components/sen21231/sensor.py +++ b/esphome/components/sen21231/sensor.py @@ -8,7 +8,7 @@ DEPENDENCIES = ["i2c"] sen21231_sensor_ns = cg.esphome_ns.namespace("sen21231_sensor") Sen21231Sensor = sen21231_sensor_ns.class_( - "Sen21231Sensor", cg.PollingComponent, i2c.I2CDevice + "Sen21231Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) CONFIG_SCHEMA = ( diff --git a/esphome/components/tc74/sensor.py b/esphome/components/tc74/sensor.py index 18fc2d9a420..18a94016fb2 100644 --- a/esphome/components/tc74/sensor.py +++ b/esphome/components/tc74/sensor.py @@ -11,7 +11,9 @@ CODEOWNERS = ["@sethgirvan"] DEPENDENCIES = ["i2c"] tc74_ns = cg.esphome_ns.namespace("tc74") -TC74Component = tc74_ns.class_("TC74Component", cg.PollingComponent, i2c.I2CDevice) +TC74Component = tc74_ns.class_( + "TC74Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) CONFIG_SCHEMA = ( sensor.sensor_schema( From 5a52936f7281a1e044c8f43833b648f8d4c826f7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:52:33 -0400 Subject: [PATCH 1296/2056] [graph] Fix legend config incorrectly accepting a list (#15522) --- esphome/components/graph/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index d72fe40dd27..0749d7e2a3f 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -110,7 +110,7 @@ GRAPH_SCHEMA = cv.Schema( cv.Optional(CONF_MIN_RANGE): cv.float_range(min=0, min_included=False), cv.Optional(CONF_MAX_RANGE): cv.float_range(min=0, min_included=False), cv.Optional(CONF_TRACES): cv.ensure_list(GRAPH_TRACE_SCHEMA), - cv.Optional(CONF_LEGEND): cv.ensure_list(GRAPH_LEGEND_SCHEMA), + cv.Optional(CONF_LEGEND): GRAPH_LEGEND_SCHEMA, } ) @@ -192,7 +192,7 @@ async def to_code(config): cg.add(var.add_trace(tr)) # Add legend if CONF_LEGEND in config: - lgd = config[CONF_LEGEND][0] + lgd = config[CONF_LEGEND] legend = cg.new_Pvariable(lgd[CONF_ID], GraphLegend()) if CONF_NAME_FONT in lgd: font = await cg.get_variable(lgd[CONF_NAME_FONT]) From 3073f3ec5c09cfff14867d140faa590000aae270 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:53:16 -0400 Subject: [PATCH 1297/2056] [haier] Fix control_method schema incorrectly using ensure_list (#15523) --- esphome/components/haier/climate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index d485c1d5d44..424ef46392e 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -215,9 +215,7 @@ CONFIG_SCHEMA = cv.All( { cv.Optional( CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" - ): cv.ensure_list( - cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) - ), + ): cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True), cv.Optional(CONF_BEEPER): cv.invalid( f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead" ), From cbcf80081b9cf5c5adf6cbcf06e930c363cecb9e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:54:12 -0400 Subject: [PATCH 1298/2056] [pcf8563] Fix default I2C address from 8-bit (0xA3) to 7-bit (0x51) (#15526) --- esphome/components/pcf8563/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/pcf8563/time.py b/esphome/components/pcf8563/time.py index 0d4de3cb73d..1502158c290 100644 --- a/esphome/components/pcf8563/time.py +++ b/esphome/components/pcf8563/time.py @@ -21,7 +21,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(pcf8563Component), } -).extend(i2c.i2c_device_schema(0xA3)) +).extend(i2c.i2c_device_schema(0x51)) @automation.register_action( From e7ddc6f6d39c720d5ea667f7cd76bba2858ddc34 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:54:57 -0400 Subject: [PATCH 1299/2056] [multiple] Fix validation ranges (batch 2) (#15533) --- esphome/components/dsmr/__init__.py | 2 +- esphome/components/hlk_fm22x/__init__.py | 2 +- esphome/components/micronova/button/__init__.py | 2 +- esphome/components/micronova/switch/__init__.py | 8 ++++++-- esphome/components/pca6416a/__init__.py | 2 +- esphome/components/pcf8574/__init__.py | 2 +- esphome/components/xiaomi_mue4094rt/binary_sensor.py | 8 +++++--- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index dd7f2b9f566..9c493bfcff1 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -37,7 +37,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_, - cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, + cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_range(min=1), cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( CONF_REQUEST_INTERVAL, default="0ms" diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index 8f55d5dc087..c1aa81f6d42 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -131,7 +131,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(HlkFm22xComponent), - cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t), + cv.Required(CONF_FACE_ID): cv.templatable(cv.int_range(min=0, max=32767)), }, key=CONF_FACE_ID, ), diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 1ef359ea6cf..63b127e63d8 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -28,7 +28,7 @@ CONFIG_SCHEMA = cv.Schema( is_polling_component=False, ) ) - .extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range()}), + .extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range(min=0x00, max=0xFF)}), } ) diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index d9722b5d488..e149ee3ce39 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -37,8 +37,12 @@ CONFIG_SCHEMA = cv.Schema( ) .extend( { - cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range(), - cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range(), + cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range( + min=0x00, max=0xFF + ), + cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range( + min=0x00, max=0xFF + ), } ), } diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index e540edb91f2..b6e156e7ff4 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -51,7 +51,7 @@ PCA6416A_PIN_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PCA6416AGPIOPin), cv.Required(CONF_PCA6416A): cv.use_id(PCA6416AComponent), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=16), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), cv.Optional(CONF_MODE, default={}): cv.All( { cv.Optional(CONF_INPUT, default=False): cv.boolean, diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index 902efd22798..d8a1e20db6f 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -55,7 +55,7 @@ def validate_mode(value): PCF8574_PIN_SCHEMA = pins.gpio_base_schema( PCF8574GPIOPin, - cv.int_range(min=0, max=17), + cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, invertible=True, diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py index 911d179d8b0..c5d93384c91 100644 --- a/esphome/components/xiaomi_mue4094rt/binary_sensor.py +++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py @@ -1,3 +1,4 @@ +from esphome import core import esphome.codegen as cg from esphome.components import binary_sensor, esp32_ble_tracker import esphome.config_validation as cv @@ -21,9 +22,10 @@ CONFIG_SCHEMA = cv.All( .extend( { cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional( - CONF_TIMEOUT, default="5s" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=65535)), + ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) From 97ad5ab35fd0432851fdcdb0ebfa474f176fbc12 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:56:01 -0400 Subject: [PATCH 1300/2056] [udp] Fix on_receive only processing first automation (#15538) --- esphome/components/udp/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 17bbf19c9ea..5dfd188f0fd 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -130,12 +130,9 @@ async def to_code(config): if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": cg.add(var.set_listen_address(listen_address)) cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) - if on_receive := config.get(CONF_ON_RECEIVE): - on_receive = on_receive[0] - trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) - trigger = await automation.build_automation( - trigger_id, trigger_argtype, on_receive - ) + for conf in config.get(CONF_ON_RECEIVE, []): + trigger_id = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + trigger = await automation.build_automation(trigger_id, trigger_argtype, conf) trigger_lambda = await cg.process_lambda( trigger.trigger( cg.std_vector.template(cg.uint8)( @@ -146,6 +143,7 @@ async def to_code(config): listener_argtype, ) cg.add(var.add_listener(trigger_lambda)) + if config.get(CONF_ON_RECEIVE): cg.add(var.set_should_listen()) From 9fe4d5c63db19b0166de6be7eb1dabea4761ccef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:56:50 -0400 Subject: [PATCH 1301/2056] [rp2040_pio_led_strip][rp2040_pio] Fix CUSTOM chipset crash and improve error message (#15537) --- esphome/components/rp2040_pio/__init__.py | 9 ++++++++- esphome/components/rp2040_pio_led_strip/light.py | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/rp2040_pio/__init__.py b/esphome/components/rp2040_pio/__init__.py index 4bd46731dfe..eecfedaa759 100644 --- a/esphome/components/rp2040_pio/__init__.py +++ b/esphome/components/rp2040_pio/__init__.py @@ -1,6 +1,7 @@ import platform import esphome.codegen as cg +import esphome.config_validation as cv DEPENDENCIES = ["rp2040"] @@ -31,7 +32,13 @@ async def to_code(config): # "earlephilhower/tool-pioasm-rp2040-earlephilhower", # ], # ) - file = PIOASM_DOWNLOADS[platform.system().lower()][platform.machine().lower()] + os_name = platform.system().lower() + arch = platform.machine().lower() + if os_name not in PIOASM_DOWNLOADS or arch not in PIOASM_DOWNLOADS[os_name]: + raise cv.Invalid( + f"pioasm is not available for {platform.system()} {platform.machine()}" + ) + file = PIOASM_DOWNLOADS[os_name][arch] cg.add_platformio_option( "platform_packages", [f"earlephilhower/tool-pioasm-rp2040-earlephilhower@{PIOASM_REPO_BASE}/{file}"], diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 62f7fffdc97..274f059bd5c 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -148,7 +148,6 @@ CHIPSETS = { "WS2812B": Chipset.CHIPSET_WS2812B, "SK6812": Chipset.CHIPSET_SK6812, "SM16703": Chipset.CHIPSET_SM16703, - "CUSTOM": Chipset.CHIPSET_CUSTOM, } From 5d31f4aeba5376e09ae7ad42025030b28404e0f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 12:00:17 -1000 Subject: [PATCH 1302/2056] [light] Use function-pointer fields in LightControlAction (#15132) --- esphome/components/light/automation.h | 61 ++++---- esphome/components/light/automation.py | 97 ++++++------ .../fixtures/light_control_action.yaml | 139 ++++++++++++++++++ .../integration/test_light_control_action.py | 95 ++++++++++++ 4 files changed, 320 insertions(+), 72 deletions(-) create mode 100644 tests/integration/fixtures/light_control_action.yaml create mode 100644 tests/integration/test_light_control_action.py diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 2854bc62d9c..a5c9220a237 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,46 +24,51 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { LightState *state_; }; +/// Compact light control action — each field is a function pointer (nullptr = unset). +/// Codegen wraps constants in stateless lambdas. 72 bytes vs 128 with TemplatableValue. template<typename... Ts> class LightControlAction : public Action<Ts...> { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(ColorMode, color_mode) - TEMPLATABLE_VALUE(bool, state) - TEMPLATABLE_VALUE(uint32_t, transition_length) - TEMPLATABLE_VALUE(uint32_t, flash_length) - TEMPLATABLE_VALUE(float, brightness) - TEMPLATABLE_VALUE(float, color_brightness) - TEMPLATABLE_VALUE(float, red) - TEMPLATABLE_VALUE(float, green) - TEMPLATABLE_VALUE(float, blue) - TEMPLATABLE_VALUE(float, white) - TEMPLATABLE_VALUE(float, color_temperature) - TEMPLATABLE_VALUE(float, cold_white) - TEMPLATABLE_VALUE(float, warm_white) - TEMPLATABLE_VALUE(uint32_t, effect) +#define LIGHT_CONTROL_FIELDS(X) \ + X(ColorMode, color_mode) \ + X(bool, state) \ + X(uint32_t, transition_length) \ + X(uint32_t, flash_length) \ + X(float, brightness) \ + X(float, color_brightness) \ + X(float, red) \ + X(float, green) \ + X(float, blue) \ + X(float, white) \ + X(float, color_temperature) \ + X(float, cold_white) \ + X(float, warm_white) \ + X(uint32_t, effect) + +#define LIGHT_FIELD_SETTER_(type, name) \ + void set_##name(type (*f)(Ts...)) { this->name##_ = f; } +#define LIGHT_FIELD_APPLY_(type, name) \ + if (this->name##_) \ + call.set_##name(this->name##_(x...)); +#define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; + + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - call.set_color_mode(this->color_mode_.optional_value(x...)); - call.set_state(this->state_.optional_value(x...)); - call.set_brightness(this->brightness_.optional_value(x...)); - call.set_color_brightness(this->color_brightness_.optional_value(x...)); - call.set_red(this->red_.optional_value(x...)); - call.set_green(this->green_.optional_value(x...)); - call.set_blue(this->blue_.optional_value(x...)); - call.set_white(this->white_.optional_value(x...)); - call.set_color_temperature(this->color_temperature_.optional_value(x...)); - call.set_cold_white(this->cold_white_.optional_value(x...)); - call.set_warm_white(this->warm_white_.optional_value(x...)); - call.set_effect(this->effect_.optional_value(x...)); - call.set_flash_length(this->flash_length_.optional_value(x...)); - call.set_transition_length(this->transition_length_.optional_value(x...)); + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) call.perform(); } protected: LightState *parent_; + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) + +#undef LIGHT_FIELD_DECL_ +#undef LIGHT_FIELD_APPLY_ +#undef LIGHT_FIELD_SETTER_ +#undef LIGHT_CONTROL_FIELDS }; template<typename... Ts> class DimRelativeAction : public Action<Ts...> { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 16e7d72f6b0..365a64584c4 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.config import path_context @@ -28,7 +30,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression -from esphome.types import ConfigType +from esphome.types import ConfigType, SafeExpType from .types import ( COLOR_MODES, @@ -141,6 +143,28 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) +async def _as_lambda( + value: Any, + args: list[tuple[SafeExpType, str]], + output_type: SafeExpType, +) -> LambdaExpression: + """Return a stateless lambda expression for a templatable value. + + If value is already a lambda, process it normally. Otherwise wrap + the constant in a ``[](...) -> T { return <value>; }`` expression + so that LightControlAction can store every field as a plain + function pointer. + """ + if cg.is_template(value): + return await cg.process_lambda(value, args, return_type=output_type) + return LambdaExpression( + f"return {cg.safe_exp(value)};", + args, + capture="", + return_type=output_type, + ) + + def _resolve_effect_index(config: ConfigType) -> int: """Resolve a static effect name to its 1-based index at codegen time. @@ -179,47 +203,29 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_COLOR_MODE in config: - template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) - cg.add(var.set_color_mode(template_)) - if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, bool) - cg.add(var.set_state(template_)) - if CONF_TRANSITION_LENGTH in config: - template_ = await cg.templatable( - config[CONF_TRANSITION_LENGTH], args, cg.uint32 - ) - cg.add(var.set_transition_length(template_)) - if CONF_FLASH_LENGTH in config: - template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) - cg.add(var.set_flash_length(template_)) - if CONF_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) - cg.add(var.set_brightness(template_)) - if CONF_COLOR_BRIGHTNESS in config: - template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) - cg.add(var.set_color_brightness(template_)) - if CONF_RED in config: - template_ = await cg.templatable(config[CONF_RED], args, float) - cg.add(var.set_red(template_)) - if CONF_GREEN in config: - template_ = await cg.templatable(config[CONF_GREEN], args, float) - cg.add(var.set_green(template_)) - if CONF_BLUE in config: - template_ = await cg.templatable(config[CONF_BLUE], args, float) - cg.add(var.set_blue(template_)) - if CONF_WHITE in config: - template_ = await cg.templatable(config[CONF_WHITE], args, float) - cg.add(var.set_white(template_)) - if CONF_COLOR_TEMPERATURE in config: - template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) - cg.add(var.set_color_temperature(template_)) - if CONF_COLD_WHITE in config: - template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) - cg.add(var.set_cold_white(template_)) - if CONF_WARM_WHITE in config: - template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) - cg.add(var.set_warm_white(template_)) + + # (config_key, setter_name, c++ type) + FIELDS = ( + (CONF_COLOR_MODE, "set_color_mode", ColorMode), + (CONF_STATE, "set_state", bool), + (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), + (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), + (CONF_BRIGHTNESS, "set_brightness", float), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", float), + (CONF_RED, "set_red", float), + (CONF_GREEN, "set_green", float), + (CONF_BLUE, "set_blue", float), + (CONF_WHITE, "set_white", float), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", float), + (CONF_COLD_WHITE, "set_cold_white", float), + (CONF_WARM_WHITE, "set_warm_white", float), + ) + for conf_key, setter, type_ in FIELDS: + if conf_key in config: + cg.add( + getattr(var, setter)(await _as_lambda(config[conf_key], args, type_)) + ) + if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): # Lambda returns a string — wrap in a C++ lambda that resolves @@ -242,8 +248,11 @@ async def light_control_to_code(config, action_id, template_arg, args): cg.add(var.set_effect(wrapper)) else: # Static string — resolve effect name to index at codegen time - effect_index = _resolve_effect_index(config) - cg.add(var.set_effect(effect_index)) + cg.add( + var.set_effect( + await _as_lambda(_resolve_effect_index(config), args, cg.uint32) + ) + ) return var diff --git a/tests/integration/fixtures/light_control_action.yaml b/tests/integration/fixtures/light_control_action.yaml new file mode 100644 index 00000000000..66f0cf1873b --- /dev/null +++ b/tests/integration/fixtures/light_control_action.yaml @@ -0,0 +1,139 @@ +esphome: + name: light-control-action-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +globals: + - id: test_brightness + type: float + initial_value: "0.75" + +output: + - platform: template + id: test_red + type: float + write_action: + - lambda: "" + - platform: template + id: test_green + type: float + write_action: + - lambda: "" + - platform: template + id: test_blue + type: float + write_action: + - lambda: "" + - platform: template + id: test_cold_white + type: float + write_action: + - lambda: "" + - platform: template + id: test_warm_white + type: float + write_action: + - lambda: "" + +light: + - platform: rgbww + name: "Test Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + cold_white: test_cold_white + warm_white: test_warm_white + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + effects: + - random: + name: "Test Effect" + transition_length: 10ms + update_interval: 10ms + +button: + # Test 1: light.turn_on with RGB constants + - platform: template + id: btn_turn_on_rgb + name: "Turn On RGB" + on_press: + - light.turn_on: + id: test_light + brightness: 1.0 + red: 0.0 + green: 0.0 + blue: 1.0 + + # Test 2: light.turn_off + - platform: template + id: btn_turn_off + name: "Turn Off" + on_press: + - light.turn_off: + id: test_light + + # Test 3: light.turn_on with color_temperature + - platform: template + id: btn_turn_on_ct + name: "Turn On CT" + on_press: + - light.turn_on: + id: test_light + color_temperature: 4000 K + brightness: 0.8 + + # Test 4: light.turn_on with effect + - platform: template + id: btn_turn_on_effect + name: "Turn On Effect" + on_press: + - light.turn_on: + id: test_light + effect: "Test Effect" + + # Test 5: light.turn_on with effect none to clear it + - platform: template + id: btn_clear_effect + name: "Clear Effect" + on_press: + - light.turn_on: + id: test_light + effect: "None" + + # Test 6: light.control with cold/warm white + - platform: template + id: btn_control_cw + name: "Control CW" + on_press: + - light.control: + id: test_light + cold_white: 0.9 + warm_white: 0.1 + + # Test 7: light.turn_on with lambda brightness (tests lambda path) + - platform: template + id: btn_lambda_brightness + name: "Lambda Brightness" + on_press: + - light.turn_on: + id: test_light + brightness: !lambda "return id(test_brightness);" + red: 1.0 + green: 0.0 + blue: 0.0 + + # Test 8: light.turn_on with transition_length + - platform: template + id: btn_turn_on_transition + name: "Turn On Transition" + on_press: + - light.turn_on: + id: test_light + brightness: 0.5 + transition_length: 0s + red: 0.5 + green: 0.5 + blue: 0.0 diff --git a/tests/integration/test_light_control_action.py b/tests/integration/test_light_control_action.py new file mode 100644 index 00000000000..9a5c16a04d4 --- /dev/null +++ b/tests/integration/test_light_control_action.py @@ -0,0 +1,95 @@ +"""Integration test for LightControlAction. + +Tests that light.turn_on, light.turn_off, and light.control automation actions +work correctly with the compact per-field union storage. Exercises both constant +value and lambda paths. +""" + +import asyncio +from typing import Any + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_control_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test LightControlAction with constants and lambdas.""" + async with run_compiled(yaml_config), api_client_connected() as client: + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + client.subscribe_states(on_state) + + # Get entities + entities = await client.list_entities_services() + light = next(e for e in entities[0] if e.object_id == "test_light") + buttons = {e.name: e for e in entities[0] if hasattr(e, "name")} + + async def wait_for_state(key: int, timeout: float = 5.0) -> Any: + """Wait for a state change for the given entity key.""" + loop = asyncio.get_running_loop() + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + async def press_and_wait(button_name: str) -> Any: + """Press a button and wait for light state change.""" + btn = buttons[button_name] + client.button_command(btn.key) + return await wait_for_state(light.key) + + # Test 1: light.turn_on with RGB constants + state = await press_and_wait("Turn On RGB") + assert state.state is True + assert state.brightness == pytest.approx(1.0) + assert state.red == pytest.approx(0.0, abs=0.01) + assert state.green == pytest.approx(0.0, abs=0.01) + assert state.blue == pytest.approx(1.0, abs=0.01) + + # Test 2: light.turn_off + state = await press_and_wait("Turn Off") + assert state.state is False + + # Test 3: light.turn_on with color_temperature + state = await press_and_wait("Turn On CT") + assert state.state is True + assert state.brightness == pytest.approx(0.8) + assert state.color_temperature == pytest.approx(250.0) # 4000K = 250 mireds + + # Test 4: light.turn_on with effect + state = await press_and_wait("Turn On Effect") + assert state.effect == "Test Effect" + + # Test 5: Clear effect + state = await press_and_wait("Clear Effect") + assert state.effect == "None" + + # Test 6: light.control with cold/warm white + state = await press_and_wait("Control CW") + assert state.cold_white == pytest.approx(0.9, abs=0.1) + assert state.warm_white == pytest.approx(0.1, abs=0.1) + + # Test 7: light.turn_on with lambda brightness + # The global test_brightness is 0.75 + state = await press_and_wait("Lambda Brightness") + assert state.state is True + assert state.brightness == pytest.approx(0.75, abs=0.05) + assert state.red == pytest.approx(1.0, abs=0.01) + assert state.green == pytest.approx(0.0, abs=0.01) + assert state.blue == pytest.approx(0.0, abs=0.01) + + # Test 8: light.turn_on with transition_length and brightness + state = await press_and_wait("Turn On Transition") + assert state.state is True + assert state.brightness == pytest.approx(0.5) From ee7b38504b936b5bae5a9666c25a50e3329fb92b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:13:58 +0200 Subject: [PATCH 1303/2056] [nextion] Expose custom protocol frames as automation triggers (#13248) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/nextion/automation.h | 3 + esphome/components/nextion/base_component.py | 4 ++ esphome/components/nextion/display.py | 41 ++++++++++++ esphome/components/nextion/nextion.cpp | 20 ++++++ esphome/components/nextion/nextion.h | 66 ++++++++++++++++++++ esphome/core/defines.h | 4 ++ tests/components/nextion/common.yaml | 27 +++++++- 7 files changed, 164 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 17f6c77e17a..e039dae6157 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -1,5 +1,8 @@ #pragma once + #include "esphome/core/automation.h" +#include "esphome/core/string_ref.h" + #include "nextion.h" namespace esphome::nextion { diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 7705b21b0b4..74a50a95d4c 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -19,6 +19,10 @@ CONF_MAX_COMMANDS_PER_LOOP = "max_commands_per_loop" CONF_MAX_QUEUE_AGE = "max_queue_age" CONF_MAX_QUEUE_SIZE = "max_queue_size" CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow" +CONF_ON_CUSTOM_BINARY_SENSOR = "on_custom_binary_sensor" +CONF_ON_CUSTOM_SENSOR = "on_custom_sensor" +CONF_ON_CUSTOM_SWITCH = "on_custom_switch" +CONF_ON_CUSTOM_TEXT_SENSOR = "on_custom_text_sensor" CONF_ON_PAGE = "on_page" CONF_ON_SETUP = "on_setup" CONF_ON_SLEEP = "on_sleep" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index e477ab71827..4d42898a10f 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -20,6 +20,10 @@ from .base_component import ( CONF_MAX_QUEUE_AGE, CONF_MAX_QUEUE_SIZE, CONF_ON_BUFFER_OVERFLOW, + CONF_ON_CUSTOM_BINARY_SENSOR, + CONF_ON_CUSTOM_SENSOR, + CONF_ON_CUSTOM_SWITCH, + CONF_ON_CUSTOM_TEXT_SENSOR, CONF_ON_PAGE, CONF_ON_SETUP, CONF_ON_SLEEP, @@ -88,6 +92,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation({}), + cv.Optional(CONF_ON_CUSTOM_BINARY_SENSOR): automation.validate_automation( + {} + ), + cv.Optional(CONF_ON_CUSTOM_SENSOR): automation.validate_automation({}), + cv.Optional(CONF_ON_CUSTOM_SWITCH): automation.validate_automation({}), + cv.Optional(CONF_ON_CUSTOM_TEXT_SENSOR): automation.validate_automation({}), cv.Optional(CONF_ON_PAGE): automation.validate_automation({}), cv.Optional(CONF_ON_SETUP): automation.validate_automation({}), cv.Optional(CONF_ON_SLEEP): automation.validate_automation({}), @@ -163,8 +173,36 @@ _CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation( CONF_ON_BUFFER_OVERFLOW, "add_buffer_overflow_event_callback" ), + automation.CallbackAutomation( + CONF_ON_CUSTOM_BINARY_SENSOR, + "add_custom_binary_sensor_callback", + [(cg.StringRef, "key"), (cg.bool_, "value")], + ), + automation.CallbackAutomation( + CONF_ON_CUSTOM_SENSOR, + "add_custom_sensor_callback", + [(cg.StringRef, "key"), (cg.int32, "value")], + ), + automation.CallbackAutomation( + CONF_ON_CUSTOM_SWITCH, + "add_custom_switch_callback", + [(cg.StringRef, "key"), (cg.bool_, "value")], + ), + automation.CallbackAutomation( + CONF_ON_CUSTOM_TEXT_SENSOR, + "add_custom_text_sensor_callback", + [(cg.StringRef, "key"), (cg.StringRef, "value")], + ), ) +# Map custom trigger config keys to their conditional defines +_CUSTOM_TRIGGER_DEFINES = { + CONF_ON_CUSTOM_BINARY_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR", + CONF_ON_CUSTOM_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_SENSOR", + CONF_ON_CUSTOM_SWITCH: "USE_NEXTION_TRIGGER_CUSTOM_SWITCH", + CONF_ON_CUSTOM_TEXT_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR", +} + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -253,5 +291,8 @@ async def to_code(config): cg.add(var.set_max_commands_per_loop(max_commands_per_loop)) await display.register_display(var, config) + for conf_key, define_name in _CUSTOM_TRIGGER_DEFINES.items(): + if config.get(conf_key): + cg.add_define(define_name) await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 6b806e0988f..b0e14b5ea3f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,8 +1,11 @@ #include "nextion.h" + #include <cinttypes> + #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include "esphome/core/util.h" namespace esphome::nextion { @@ -715,6 +718,10 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Switch %s: %s", ONOFF(to_process[index] != 0), variable_name.c_str()); +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH + this->custom_switch_callback_.call(StringRef(variable_name), to_process[index] != 0); +#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH + for (auto *switchtype : this->switchtype_) { switchtype->process_bool(variable_name, to_process[index] != 0); } @@ -744,6 +751,10 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value); +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR + this->custom_sensor_callback_.call(StringRef(variable_name), value); +#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR + for (auto *sensor : this->sensortype_) { sensor->process_sensor(variable_name, value); } @@ -781,6 +792,11 @@ void Nextion::process_nextion_commands_() { // nq->variable_name = variable_name; // nq->state = text_value; // this->textsensorq_.push_back(nq); + +#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR + this->custom_text_sensor_callback_.call(StringRef(variable_name), StringRef(text_value)); +#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR + for (auto *textsensortype : this->textsensortype_) { textsensortype->process_text(variable_name, text_value); } @@ -808,6 +824,10 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Binary sensor: %s=%s", variable_name.c_str(), ONOFF(to_process[index] != 0)); +#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR + this->custom_binary_sensor_callback_.call(StringRef(variable_name), to_process[index] != 0); +#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR + for (auto *binarysensortype : this->binarysensortype_) { binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index d9103892891..c84a5cd49c2 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -7,6 +7,7 @@ #include "esphome/components/display/display_color_utils.h" #include "esphome/components/uart/uart.h" #include "esphome/core/defines.h" +#include "esphome/core/string_ref.h" #include "esphome/core/time.h" #ifdef USE_NEXTION_WAVEFORM @@ -1183,6 +1184,59 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe this->buffer_overflow_callback_.add(std::forward<F>(callback)); } + // Callbacks for Nextion "custom protocol" frames (0x90..0x93) +#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR + /** Add a callback to be notified when Nextion sends a custom binary sensor protocol frame (0x93). + * + * This callback is invoked when a Nextion custom binary sensor frame is received, + * providing the component name as the key and the decoded boolean value. + * + * @param callback The void(const StringRef &key, bool value) callback. + */ + template<typename F> void add_custom_binary_sensor_callback(F &&callback) { + this->custom_binary_sensor_callback_.add(std::forward<F>(callback)); + } +#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR + +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR + /** Add a callback to be notified when Nextion sends a custom sensor protocol frame (0x91). + * + * This callback is invoked when a Nextion custom sensor frame is received, + * providing the component name as the key and the decoded integer value. + * + * @param callback The void(StringRef key, int32_t value) callback. + */ + template<typename F> void add_custom_sensor_callback(F &&callback) { + this->custom_sensor_callback_.add(std::forward<F>(callback)); + } +#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR + +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH + /** Add a callback to be notified when Nextion sends a custom switch protocol frame (0x90). + * + * This callback is invoked when a Nextion custom switch frame is received, + * providing the component name as the key and the decoded boolean value. + * + * @param callback The void(const StringRef &key, bool value) callback. + */ + template<typename F> void add_custom_switch_callback(F &&callback) { + this->custom_switch_callback_.add(std::forward<F>(callback)); + } +#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH + +#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR + /** Add a callback to be notified when Nextion sends a custom text sensor protocol frame (0x92). + * + * This callback is invoked when a Nextion custom text sensor frame is received, + * providing the component name as the key and the decoded text value. + * + * @param callback The void(const StringRef &key, const StringRef &value) callback. + */ + template<typename F> void add_custom_text_sensor_callback(F &&callback) { + this->custom_text_sensor_callback_.add(std::forward<F>(callback)); + } +#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR + void update_all_components(); /** @@ -1535,6 +1589,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager<void(uint8_t)> page_callback_{}; CallbackManager<void(uint8_t, uint8_t, bool)> touch_callback_{}; CallbackManager<void()> buffer_overflow_callback_{}; +#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR + CallbackManager<void(StringRef, bool)> custom_binary_sensor_callback_{}; +#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR + CallbackManager<void(StringRef, int32_t)> custom_sensor_callback_{}; +#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR +#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH + CallbackManager<void(StringRef, bool)> custom_switch_callback_{}; +#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH +#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR + CallbackManager<void(StringRef, StringRef)> custom_text_sensor_callback_{}; +#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR nextion_writer_t writer_; optional<float> brightness_; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9c90790f3a8..4939c194e3b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -123,6 +123,10 @@ #define USE_NEXTION_MAX_COMMANDS_PER_LOOP #define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD +#define USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR +#define USE_NEXTION_TRIGGER_CUSTOM_SENSOR +#define USE_NEXTION_TRIGGER_CUSTOM_SWITCH +#define USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR #define USE_NEXTION_WAVEFORM #define USE_NUMBER #define USE_OUTPUT diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index d9493db50c9..0616b9a41a8 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -286,6 +286,31 @@ display: on_buffer_overflow: then: logger.log: "Nextion reported a buffer overflow!" + on_custom_text_sensor: + then: + - lambda: |- + // key: StringRef, value: StringRef + if (key == "csv") { + // parse value here, or forward to your own component + ESP_LOGD("nextion.csv", "Got CSV: %s", value.c_str()); + } + on_custom_sensor: + then: + - lambda: |- + // key: StringRef, value: int32_t + if (key == "temperature_raw") { + ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value); + } + on_custom_binary_sensor: + then: + - lambda: |- + if (key == "btn1") { + ESP_LOGD("nextion.btn", "btn1=%s", ONOFF(value)); + } + on_custom_switch: + then: + - lambda: |- + ESP_LOGD("nextion.sw", "%s=%s", key.c_str(), ONOFF(value)); on_page: then: lambda: 'ESP_LOGD("display","Display shows new page %u", x);' @@ -304,8 +329,8 @@ display: on_wake: then: lambda: 'ESP_LOGD("display","Display woke up");' - update_interval: 5s start_up_page: 1 startup_override_ms: 10000ms # Wait 10s for display ready touch_sleep_timeout: 3 + update_interval: 5s wake_up_page: 2 From 0d7f2f05b914bd7b1e3fc066af49386371a10227 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:16:37 -0400 Subject: [PATCH 1304/2056] [libretiny] Fix board pin alias resolution TypeError (#15527) --- esphome/components/libretiny/gpio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 9bad400eb70..9f8d96de249 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -41,7 +41,7 @@ def _lookup_board_pins(board): board_pins = component.board_pins.get(board, {}) # Resolve aliased board pins (shorthand when two boards have the same pin configuration) while isinstance(board_pins, str): - board_pins = board_pins[board_pins] + board_pins = component.board_pins[board_pins] return board_pins From 14bcdfe7004a6ea77425c0f4e5ab77af21e70827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:29:55 +0200 Subject: [PATCH 1305/2056] [emontx] emonTx component (#9027) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- CODEOWNERS | 1 + esphome/components/emontx/__init__.py | 152 ++++++++++++++++++ esphome/components/emontx/emontx.cpp | 116 +++++++++++++ esphome/components/emontx/emontx.h | 69 ++++++++ esphome/components/emontx/sensor/__init__.py | 133 +++++++++++++++ .../emontx/sensor/emontx_sensor.cpp | 10 ++ .../components/emontx/sensor/emontx_sensor.h | 13 ++ tests/components/emontx/common.yaml | 25 +++ tests/components/emontx/test.esp32-idf.yaml | 4 + tests/components/emontx/test.esp8266-ard.yaml | 4 + tests/components/emontx/test.rp2040-ard.yaml | 4 + .../common/uart_115200/esp32-ard.yaml | 1 + .../common/uart_115200/esp32-c3-ard.yaml | 1 + .../common/uart_115200/esp32-c3-idf.yaml | 1 + .../common/uart_115200/esp32-idf.yaml | 1 + .../common/uart_115200/esp8266-ard.yaml | 1 + .../common/uart_115200/rp2040-ard.yaml | 1 + 17 files changed, 537 insertions(+) create mode 100644 esphome/components/emontx/__init__.py create mode 100644 esphome/components/emontx/emontx.cpp create mode 100644 esphome/components/emontx/emontx.h create mode 100644 esphome/components/emontx/sensor/__init__.py create mode 100644 esphome/components/emontx/sensor/emontx_sensor.cpp create mode 100644 esphome/components/emontx/sensor/emontx_sensor.h create mode 100644 tests/components/emontx/common.yaml create mode 100644 tests/components/emontx/test.esp32-idf.yaml create mode 100644 tests/components/emontx/test.esp8266-ard.yaml create mode 100644 tests/components/emontx/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c466204b66c..5b1ae65f1bf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -148,6 +148,7 @@ esphome/components/ee895/* @Stock-M esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/emc2101/* @ellull esphome/components/emmeti/* @E440QF +esphome/components/emontx/* @FredM67 @glynhudson @TrystanLea esphome/components/ens160/* @latonita esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita diff --git a/esphome/components/emontx/__init__.py b/esphome/components/emontx/__init__.py new file mode 100644 index 00000000000..a2d4349698a --- /dev/null +++ b/esphome/components/emontx/__init__.py @@ -0,0 +1,152 @@ +from dataclasses import dataclass, field + +from esphome import automation +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_COMMAND, + CONF_ID, + CONF_ON_DATA, + CONF_RX_BUFFER_SIZE, + CONF_UART_ID, +) +from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +AUTO_LOAD = ["json"] +CODEOWNERS = ["@FredM67", "@TrystanLea", "@glynhudson"] +DEPENDENCIES = ["uart"] + +emontx_ns = cg.esphome_ns.namespace("emontx") +EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice) + +# Action to send command to emonTx +EmonTxSendCommandAction = emontx_ns.class_("EmonTxSendCommandAction", automation.Action) + +CONF_EMONTX_ID = "emontx_id" +CONF_TAG_NAME = "tag_name" +CONF_ON_JSON = "on_json" + +DOMAIN = "emontx" + +MINIMUM_RX_BUFFER_SIZE = 2048 + + +@dataclass +class EmonTxData: + sensor_counts: dict[str, int] = field(default_factory=dict) + + +def _get_data() -> EmonTxData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = EmonTxData() + return CORE.data[DOMAIN] + + +# Main configuration schema +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EmonTx), + cv.Optional(CONF_ON_JSON): automation.validate_automation({}), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + + # Count sensors registered to this hub (IDs are resolved at final_validate stage) + hub_id = str(config[CONF_ID]) + sensor_count = sum( + 1 + for s in full_config.get("sensor", []) + if s.get("platform") == "emontx" and str(s.get(CONF_EMONTX_ID)) == hub_id + ) + _get_data().sensor_counts[hub_id] = sensor_count + + # Ensure UART RX buffer size is large enough to handle data bursts from firmware + for uart_conf in full_config["uart"]: + if uart_conf[CONF_ID] == config[CONF_UART_ID]: + current_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE] + if current_buffer_size < MINIMUM_RX_BUFFER_SIZE: + raise cv.Invalid( + f"Component emontx requires UART '{config[CONF_UART_ID]}' to have " + f"rx_buffer_size of at least {MINIMUM_RX_BUFFER_SIZE} bytes " + f"(currently set to {current_buffer_size} bytes). " + f"Please add 'rx_buffer_size: {MINIMUM_RX_BUFFER_SIZE}' to your uart configuration.", + path=[CONF_UART_ID], + ) + break + + # Validate UART settings + schema = uart.final_validate_device_schema( + "emontx", + baud_rate=115200, + require_tx=False, + require_rx=True, + data_bits=8, + parity="NONE", + stop_bits=1, + ) + return schema(config) + + +FINAL_VALIDATE_SCHEMA = final_validate + + +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_JSON, + "add_on_json_callback", + [(cg.JsonObject, "json"), (cg.std_string, "raw_json")], + ), + automation.CallbackAutomation( + CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")] + ), +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + # Initialize sensor storage with count from final_validate + sensor_count = _get_data().sensor_counts.get(str(config[CONF_ID]), 0) + if sensor_count > 0: + cg.add(var.init_sensors(sensor_count)) + + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + + +# Action: emontx.send_command + +EMONTX_SEND_COMMAND_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EmonTx), + cv.Required(CONF_COMMAND): cv.templatable(cv.string), + } +) + + +@automation.register_action( + "emontx.send_command", + EmonTxSendCommandAction, + EMONTX_SEND_COMMAND_ACTION_SCHEMA, + synchronous=True, +) +async def emontx_send_command_action_to_code( + config: ConfigType, action_id, template_arg, args +) -> None: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string) + cg.add(var.set_command(template_)) + return var diff --git a/esphome/components/emontx/emontx.cpp b/esphome/components/emontx/emontx.cpp new file mode 100644 index 00000000000..7a1b084fe08 --- /dev/null +++ b/esphome/components/emontx/emontx.cpp @@ -0,0 +1,116 @@ +#include "emontx.h" +#include "esphome/core/log.h" +#include "esphome/components/json/json_util.h" + +namespace esphome::emontx { + +static const char *const TAG = "emontx"; + +void EmonTx::setup() { this->buffer_pos_ = 0; } + +/** + * @brief Implements the main loop for parsing data from the serial port. + * + * @details Continuously processes incoming UART data line-by-line: + * 1. Fire on_data callbacks for all received lines + * 2. If line starts with '{', parse as JSON and update sensors/callbacks + */ +void EmonTx::loop() { + // Read all available data to prevent UART buffer overflow + while (this->available() > 0) { + uint8_t received = this->read(); + + if (received == '\r') { + continue; // Ignore CR + } else if (received == '\n') { + // End of line - process the buffer + if (this->buffer_pos_ > 0) { + // Null-terminate for safe logging and c_str() use + size_t len = this->buffer_pos_; + this->buffer_[len] = '\0'; + this->buffer_pos_ = 0; + + StringRef line(this->buffer_.data(), len); + ESP_LOGD(TAG, "Received line: %s", line.c_str()); + + // Fire data callbacks for all received lines + this->data_callbacks_.call(line); + + // Check if this line is JSON (starts with '{') + if (this->buffer_[0] == '{') { + ESP_LOGV(TAG, "Line is JSON, parsing..."); + this->parse_json_(this->buffer_.data(), len); + } + } + } else if (this->buffer_pos_ >= MAX_LINE_LENGTH) { + ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding buffer", MAX_LINE_LENGTH); + this->buffer_pos_ = 0; + } else { + this->buffer_[this->buffer_pos_++] = static_cast<char>(received); + } + } +} + +void EmonTx::parse_json_(const char *data, size_t len) { + bool success = json::parse_json(reinterpret_cast<const uint8_t *>(data), len, [this, data, len](JsonObject root) { +#ifdef USE_SENSOR + for (auto &sensor_pair : this->sensors_) { + auto val = root[sensor_pair.first]; + if (val.is<JsonVariant>()) { + float value = val; + ESP_LOGV(TAG, "Updating sensor '%s' with value: %.2f", sensor_pair.first, value); + sensor_pair.second->publish_state(value); + } + } +#endif + + this->json_callbacks_.call(root, StringRef(data, len)); + return true; + }); + + if (!success) { + ESP_LOGW(TAG, "Failed to parse JSON"); + } +} + +/** + * @brief Logs the EmonTx component configuration details. + */ +void EmonTx::dump_config() { + ESP_LOGCONFIG(TAG, "EmonTx:"); + +#ifdef USE_SENSOR + ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size()); + for (const auto &sensor_pair : this->sensors_) { + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor_pair.first); + } +#else + ESP_LOGCONFIG(TAG, " Sensor support: DISABLED"); +#endif +} + +/** + * @brief Sends a command string to the emonTx device via UART. + * + * @param command The command string to send (LF will be appended automatically). + */ +void EmonTx::send_command(const std::string &command) { + ESP_LOGD(TAG, "Sending command to emonTx: %s", command.c_str()); + this->write_str(command.c_str()); + this->write_byte('\n'); +} + +#ifdef USE_SENSOR +/** + * @brief Registers a sensor to receive updates for a specific JSON tag. + * + * @param tag_name The JSON key to monitor for this sensor (must be a string literal). + * @param sensor Pointer to the sensor that will receive value updates. + */ +void EmonTx::register_sensor(const char *tag_name, sensor::Sensor *sensor) { + ESP_LOGCONFIG(TAG, "Registering sensor for tag: %s", tag_name); + this->sensors_.emplace_back(tag_name, sensor); +} +#endif + +} // namespace esphome::emontx diff --git a/esphome/components/emontx/emontx.h b/esphome/components/emontx/emontx.h new file mode 100644 index 00000000000..67e7f5bffce --- /dev/null +++ b/esphome/components/emontx/emontx.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/json/json_util.h" + +#include <array> + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +namespace esphome::emontx { + +/// Maximum line length in bytes (plus one byte reserved for null terminator) +static constexpr size_t MAX_LINE_LENGTH = 1024; + +/** + * @class EmonTx + * @brief Main class for the EmonTx component. + * + * The EmonTx processes incoming data frames via UART, + * extracts tags and values, and publishes them to registered sensors. + */ +class EmonTx : public Component, public uart::UARTDevice { + public: + EmonTx() = default; + + void loop() override; + void setup() override; + void dump_config() override; + + template<typename F> void add_on_json_callback(F &&callback) { this->json_callbacks_.add(std::forward<F>(callback)); } + + template<typename F> void add_on_data_callback(F &&callback) { this->data_callbacks_.add(std::forward<F>(callback)); } + + // Send command to emonTx via UART + void send_command(const std::string &command); + +#ifdef USE_SENSOR + void init_sensors(size_t count) { this->sensors_.init(count); } + void register_sensor(const char *tag_name, sensor::Sensor *sensor); +#endif + + protected: + void parse_json_(const char *data, size_t len); + +#ifdef USE_SENSOR + FixedVector<std::pair<const char *, sensor::Sensor *>> sensors_{}; +#endif + LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_; + LazyCallbackManager<void(StringRef)> data_callbacks_; + uint16_t buffer_pos_{0}; + std::array<char, MAX_LINE_LENGTH + 1> buffer_{}; +}; + +// Action to send command to emonTx +template<typename... Ts> class EmonTxSendCommandAction : public Action<Ts...>, public Parented<EmonTx> { + public: + TEMPLATABLE_VALUE(std::string, command) + + void play(const Ts &...x) override { this->parent_->send_command(this->command_.value(x...)); } +}; + +} // namespace esphome::emontx diff --git a/esphome/components/emontx/sensor/__init__.py b/esphome/components/emontx/sensor/__init__.py new file mode 100644 index 00000000000..83a972c5e0e --- /dev/null +++ b/esphome/components/emontx/sensor/__init__.py @@ -0,0 +1,133 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_PULSES, + UNIT_VOLT, + UNIT_WATT, + UNIT_WATT_HOURS, +) +from esphome.types import ConfigType + +from .. import CONF_EMONTX_ID, CONF_TAG_NAME, EmonTx, emontx_ns + +EmonTxSensor = emontx_ns.class_("EmonTxSensor", sensor.Sensor, cg.Component) + +# Define sensor type configurations by prefix +SENSOR_CONFIGS = { + "P": { + CONF_UNIT_OF_MEASUREMENT: UNIT_WATT, + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 0, + }, + "E": { + CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + CONF_ACCURACY_DECIMALS: 0, + }, + "V": { + CONF_UNIT_OF_MEASUREMENT: UNIT_VOLT, + CONF_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 2, + }, + "I": { + CONF_UNIT_OF_MEASUREMENT: UNIT_AMPERE, + CONF_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 2, + }, + "T": { + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 2, + }, +} + +# Pattern-based configurations +PATTERN_CONFIGS = { + "PULSE": { + CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES, + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_ACCURACY_DECIMALS: 0, + }, + "PF": { + CONF_UNIT_OF_MEASUREMENT: UNIT_EMPTY, + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER_FACTOR, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_ACCURACY_DECIMALS: 2, + }, +} + +# Create a base schema that's flexible for any tag +BASE_SCHEMA = sensor.sensor_schema( + EmonTxSensor, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=0, +).extend( + { + cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx), + cv.Required(CONF_TAG_NAME): cv.string, + } +) + + +def apply_tag_defaults(config: ConfigType) -> ConfigType: + """Apply defaults based on tag prefix if applicable, but don't restrict any tags.""" + tag = config[CONF_TAG_NAME] + + # Skip if tag is too short + if len(tag) < 2: + return config + + # Check if this tag starts with a known prefix + tag_upper = tag.upper() + + for pattern, pattern_config in PATTERN_CONFIGS.items(): + if tag_upper.startswith(pattern): + # Apply pattern defaults if not overridden by user + for key, value in pattern_config.items(): + if key not in config: + config[key] = value + return config + + # Only apply defaults for known prefixes with numeric indices + prefix = tag_upper[0] + if prefix in SENSOR_CONFIGS and len(tag) > 1 and tag[1:].isdigit(): + # Apply defaults for known tag types, but only if not overridden by user + defaults = SENSOR_CONFIGS[prefix] + for key, value in defaults.items(): + if key not in config: + config[key] = value + + return config + + +CONFIG_SCHEMA = cv.All(BASE_SCHEMA, apply_tag_defaults) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + hub = await cg.get_variable(config[CONF_EMONTX_ID]) + cg.add(hub.register_sensor(config[CONF_TAG_NAME], var)) diff --git a/esphome/components/emontx/sensor/emontx_sensor.cpp b/esphome/components/emontx/sensor/emontx_sensor.cpp new file mode 100644 index 00000000000..142df0150e7 --- /dev/null +++ b/esphome/components/emontx/sensor/emontx_sensor.cpp @@ -0,0 +1,10 @@ +#include "emontx_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::emontx { + +static const char *const TAG = "emontx_sensor"; + +void EmonTxSensor::dump_config() { LOG_SENSOR(" ", "EmonTx Sensor", this); } + +} // namespace esphome::emontx diff --git a/esphome/components/emontx/sensor/emontx_sensor.h b/esphome/components/emontx/sensor/emontx_sensor.h new file mode 100644 index 00000000000..9714acdf0dc --- /dev/null +++ b/esphome/components/emontx/sensor/emontx_sensor.h @@ -0,0 +1,13 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome::emontx { + +class EmonTxSensor : public sensor::Sensor, public Component { + public: + void dump_config() override; +}; + +} // namespace esphome::emontx diff --git a/tests/components/emontx/common.yaml b/tests/components/emontx/common.yaml new file mode 100644 index 00000000000..5c25e37abb5 --- /dev/null +++ b/tests/components/emontx/common.yaml @@ -0,0 +1,25 @@ +button: + - platform: template + name: Send command test + on_press: + - emontx.send_command: + id: test_emontx + command: "v" + +emontx: + id: test_emontx + on_json: + - then: + - logger.log: "Got JSON" + on_data: + - then: + - logger.log: + format: "Got data: %s" + args: [data.c_str()] + +sensor: + - platform: emontx + name: Power + tag_name: P1 + emontx_id: test_emontx + unit_of_measurement: W diff --git a/tests/components/emontx/test.esp32-idf.yaml b/tests/components/emontx/test.esp32-idf.yaml new file mode 100644 index 00000000000..3a3747f3a5b --- /dev/null +++ b/tests/components/emontx/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/emontx/test.esp8266-ard.yaml b/tests/components/emontx/test.esp8266-ard.yaml new file mode 100644 index 00000000000..31c5731589a --- /dev/null +++ b/tests/components/emontx/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/emontx/test.rp2040-ard.yaml b/tests/components/emontx/test.rp2040-ard.yaml new file mode 100644 index 00000000000..ff55e8263d7 --- /dev/null +++ b/tests/components/emontx/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/test_build_components/common/uart_115200/esp32-ard.yaml b/tests/test_build_components/common/uart_115200/esp32-ard.yaml index 9102910f310..108f12110d7 100644 --- a/tests/test_build_components/common/uart_115200/esp32-ard.yaml +++ b/tests/test_build_components/common/uart_115200/esp32-ard.yaml @@ -9,3 +9,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 diff --git a/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml index 87a969c6a37..5176a4e8e22 100644 --- a/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml +++ b/tests/test_build_components/common/uart_115200/esp32-c3-ard.yaml @@ -9,3 +9,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 diff --git a/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml index f3768592e5b..f61d01d2064 100644 --- a/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml +++ b/tests/test_build_components/common/uart_115200/esp32-c3-idf.yaml @@ -10,3 +10,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 diff --git a/tests/test_build_components/common/uart_115200/esp32-idf.yaml b/tests/test_build_components/common/uart_115200/esp32-idf.yaml index e405f74fe7b..b432d31a7e4 100644 --- a/tests/test_build_components/common/uart_115200/esp32-idf.yaml +++ b/tests/test_build_components/common/uart_115200/esp32-idf.yaml @@ -10,3 +10,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 diff --git a/tests/test_build_components/common/uart_115200/esp8266-ard.yaml b/tests/test_build_components/common/uart_115200/esp8266-ard.yaml index 2dcf1c4a5d3..c4b9170c2f9 100644 --- a/tests/test_build_components/common/uart_115200/esp8266-ard.yaml +++ b/tests/test_build_components/common/uart_115200/esp8266-ard.yaml @@ -9,3 +9,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 diff --git a/tests/test_build_components/common/uart_115200/rp2040-ard.yaml b/tests/test_build_components/common/uart_115200/rp2040-ard.yaml index 62a7b5aed20..874b09217b1 100644 --- a/tests/test_build_components/common/uart_115200/rp2040-ard.yaml +++ b/tests/test_build_components/common/uart_115200/rp2040-ard.yaml @@ -9,3 +9,4 @@ uart: tx_pin: ${tx_pin} rx_pin: ${rx_pin} baud_rate: 115200 + rx_buffer_size: 2048 From aad898503d8200600f1ec285b0444f7d38906ba2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:37:17 -0400 Subject: [PATCH 1306/2056] [multiple] Fix channel/pin range validation and widen channel types (#15529) --- esphome/components/bp1658cj/output.py | 2 +- esphome/components/mcp3008/sensor/__init__.py | 2 +- esphome/components/my9231/my9231.cpp | 4 ++-- esphome/components/my9231/my9231.h | 6 +++--- esphome/components/sm16716/output.py | 2 +- esphome/components/sm2135/output.py | 2 +- esphome/components/sm2235/output.py | 2 +- esphome/components/sm2335/output.py | 2 +- esphome/components/tlc5947/output/__init__.py | 2 +- esphome/components/tlc5947/output/tlc5947_output.h | 4 ++-- esphome/components/tlc5971/output/__init__.py | 2 +- esphome/components/tlc5971/output/tlc5971_output.h | 4 ++-- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/esphome/components/bp1658cj/output.py b/esphome/components/bp1658cj/output.py index 023b6ecd1ec..78cf717aba2 100644 --- a/esphome/components/bp1658cj/output.py +++ b/esphome/components/bp1658cj/output.py @@ -14,7 +14,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ), cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/mcp3008/sensor/__init__.py b/esphome/components/mcp3008/sensor/__init__.py index e85ce2955d6..2576ef50e51 100644 --- a/esphome/components/mcp3008/sensor/__init__.py +++ b/esphome/components/mcp3008/sensor/__init__.py @@ -35,7 +35,7 @@ CONFIG_SCHEMA = ( .extend( { cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), - cv.Required(CONF_NUMBER): cv.int_, + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, } ) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index 5b77a49e726..25f7e6925d3 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -81,9 +81,9 @@ void MY9231OutputComponent::loop() { } this->update_ = false; } -void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { +void MY9231OutputComponent::set_channel_value_(uint16_t channel, uint16_t value) { ESP_LOGV(TAG, "set channels %u to %u", channel, value); - uint8_t index = this->num_channels_ - channel - 1; + uint16_t index = this->num_channels_ - channel - 1; if (this->pwm_amounts_[index] != value) { this->update_ = true; } diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index 77c12598537..dff68d247c3 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -30,7 +30,7 @@ class MY9231OutputComponent : public Component { class Channel : public output::FloatOutput { public: void set_parent(MY9231OutputComponent *parent) { parent_ = parent; } - void set_channel(uint8_t channel) { channel_ = channel; } + void set_channel(uint16_t channel) { channel_ = channel; } protected: void write_state(float state) override { @@ -39,13 +39,13 @@ class MY9231OutputComponent : public Component { } MY9231OutputComponent *parent_; - uint8_t channel_; + uint16_t channel_; }; protected: uint16_t get_max_amount_() const { return (uint32_t(1) << this->bit_depth_) - 1; } - void set_channel_value_(uint8_t channel, uint16_t value); + void set_channel_value_(uint16_t channel, uint16_t value); void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py index 50f6ec759f3..2cfc38f5cc3 100644 --- a/esphome/components/sm16716/output.py +++ b/esphome/components/sm16716/output.py @@ -14,7 +14,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716), cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=254), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/sm2135/output.py b/esphome/components/sm2135/output.py index 71c4af2253a..a4ac7fc7da2 100644 --- a/esphome/components/sm2135/output.py +++ b/esphome/components/sm2135/output.py @@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_SM2135_ID): cv.use_id(SM2135), cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/sm2235/output.py b/esphome/components/sm2235/output.py index 2a9698d645e..b17af2b1e00 100644 --- a/esphome/components/sm2235/output.py +++ b/esphome/components/sm2235/output.py @@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235), cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/sm2335/output.py b/esphome/components/sm2335/output.py index ef7fec7307a..7fd00917bda 100644 --- a/esphome/components/sm2335/output.py +++ b/esphome/components/sm2335/output.py @@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335), cv.Required(CONF_ID): cv.declare_id(Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/tlc5947/output/__init__.py b/esphome/components/tlc5947/output/__init__.py index a1290add816..6bea1546d30 100644 --- a/esphome/components/tlc5947/output/__init__.py +++ b/esphome/components/tlc5947/output/__init__.py @@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947), cv.Required(CONF_ID): cv.declare_id(TLC5947Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.uint16_t, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/tlc5947/output/tlc5947_output.h b/esphome/components/tlc5947/output/tlc5947_output.h index 5b2c51020c7..0faec96acbf 100644 --- a/esphome/components/tlc5947/output/tlc5947_output.h +++ b/esphome/components/tlc5947/output/tlc5947_output.h @@ -11,11 +11,11 @@ namespace tlc5947 { class TLC5947Channel : public output::FloatOutput, public Parented<TLC5947> { public: - void set_channel(uint8_t channel) { this->channel_ = channel; } + void set_channel(uint16_t channel) { this->channel_ = channel; } protected: void write_state(float state) override; - uint8_t channel_; + uint16_t channel_; }; } // namespace tlc5947 diff --git a/esphome/components/tlc5971/output/__init__.py b/esphome/components/tlc5971/output/__init__.py index ae000ae0a9b..854fbbd8103 100644 --- a/esphome/components/tlc5971/output/__init__.py +++ b/esphome/components/tlc5971/output/__init__.py @@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(CONF_TLC5971_ID): cv.use_id(TLC5971), cv.Required(CONF_ID): cv.declare_id(TLC5971Channel), - cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + cv.Required(CONF_CHANNEL): cv.uint16_t, } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/tlc5971/output/tlc5971_output.h b/esphome/components/tlc5971/output/tlc5971_output.h index 944ee19b2d6..ca3099e7b28 100644 --- a/esphome/components/tlc5971/output/tlc5971_output.h +++ b/esphome/components/tlc5971/output/tlc5971_output.h @@ -11,11 +11,11 @@ namespace tlc5971 { class TLC5971Channel : public output::FloatOutput, public Parented<TLC5971> { public: - void set_channel(uint8_t channel) { this->channel_ = channel; } + void set_channel(uint16_t channel) { this->channel_ = channel; } protected: void write_state(float state) override; - uint8_t channel_; + uint16_t channel_; }; } // namespace tlc5971 From b307c7c74ca18922b4580e7344f5b6a4a354a371 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:44:52 +1200 Subject: [PATCH 1307/2056] [config_validation] Add unbounded percentage validators (#15500) --- esphome/config_validation.py | 65 ++++++--- tests/unit_tests/test_config_validation.py | 149 +++++++++++++++++++++ 2 files changed, 197 insertions(+), 17 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 7805de98db7..b0bd9e6231d 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1468,17 +1468,53 @@ hex_uint64_t = hex_int_range(min=0, max=18446744073709551615) i2c_address = hex_uint8_t -def percentage(value): +def percentage(value: object) -> float: """Validate that the value is a percentage. - The resulting value is an integer in the range 0.0 to 1.0. + The resulting value is a float in the range 0.0 to 1.0. """ - value = possibly_negative_percentage(value) + value = _parse_percentage(value) return zero_to_one_float(value) -def possibly_negative_percentage(value): - has_percent_sign = False +def possibly_negative_percentage(value: object) -> float: + """Validate that the value is a possibly negative percentage. + + The resulting value is a float in the range -1.0 to 1.0. + """ + value = _parse_percentage(value) + return negative_one_to_one_float(value) + + +def unbounded_percentage(value: object) -> float: + """Validate that the value is a percentage, allowing values above 100%. + + The resulting value is a non-negative float with no upper bound. + For example, "150%" returns 1.5 and "50%" returns 0.5. + """ + value = _parse_percentage(value) + if value < 0: + raise Invalid("Percentage must not be negative") + return value + + +def unbounded_possibly_negative_percentage(value: object) -> float: + """Validate that the value is a possibly negative percentage without bounds. + + The resulting value is an unbounded float. + For example, "200%" returns 2.0 and "-150%" returns -1.5. + """ + return _parse_percentage(value) + + +def _parse_percentage(value: object) -> float: + """Parse a percentage string or number into a float. + + Handles both "50%" style strings and raw float values. + Values without a percent sign above 1.0 or below -1.0 are rejected + to prevent user mistakes (e.g. writing 50 instead of 50%). + """ + has_percent_sign: bool = False if isinstance(value, str): try: if value.endswith("%"): @@ -1490,21 +1526,16 @@ def possibly_negative_percentage(value): # pylint: disable=raise-missing-from raise Invalid("invalid number") try: - if value > 1: - msg = "Percentage must not be higher than 100%." - if not has_percent_sign: - msg += " Please put a percent sign after the number!" - raise Invalid(msg) - if value < -1: - msg = "Percentage must not be smaller than -100%." - if not has_percent_sign: - msg += " Please put a percent sign after the number!" - raise Invalid(msg) + if not has_percent_sign and (value > 1 or value < -1): + raise Invalid( + "Percentage value must use a percent sign for values " + "outside -1.0 to 1.0. Please put a percent sign after the number!" + ) except TypeError: raise Invalid( # pylint: disable=raise-missing-from - "Expected percentage or float between -1.0 and 1.0" + "Expected percentage or float" ) - return negative_one_to_one_float(value) + return float(value) def percentage_int(value): diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index ce941b40dc5..ac84ce7cc89 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -616,3 +616,152 @@ def test_validate_entity_name__none_with_friendly_name() -> None: result = config_validation._validate_entity_name("None") assert result is None CORE.friendly_name = None # Reset + + +# --- percentage validators --- + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("0%", 0.0), + ("50%", 0.5), + ("100%", 1.0), + (0.0, 0.0), + (0.5, 0.5), + (1.0, 1.0), + ("0.0", 0.0), + ("0.5", 0.5), + ("1.0", 1.0), + ), +) +def test_percentage__valid(value: object, expected: float) -> None: + assert config_validation.percentage(value) == expected + + +@pytest.mark.parametrize( + "value", + ( + "150%", + "-10%", + "-0.1", + "1.1", + 2, + -1, + "foo", + None, + ), +) +def test_percentage__invalid(value: object) -> None: + with pytest.raises(Invalid): + config_validation.percentage(value) + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("0%", 0.0), + ("50%", 0.5), + ("100%", 1.0), + ("-50%", -0.5), + ("-100%", -1.0), + (0.0, 0.0), + (0.5, 0.5), + (-0.5, -0.5), + (1.0, 1.0), + (-1.0, -1.0), + ), +) +def test_possibly_negative_percentage__valid(value: object, expected: float) -> None: + assert config_validation.possibly_negative_percentage(value) == expected + + +@pytest.mark.parametrize( + "value", + ( + "150%", + "-150%", + 2, + -2, + "foo", + None, + ), +) +def test_possibly_negative_percentage__invalid(value: object) -> None: + with pytest.raises(Invalid): + config_validation.possibly_negative_percentage(value) + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("0%", 0.0), + ("50%", 0.5), + ("100%", 1.0), + ("150%", 1.5), + ("200%", 2.0), + (0.0, 0.0), + (0.5, 0.5), + (1.0, 1.0), + ), +) +def test_unbounded_percentage__valid(value: object, expected: float) -> None: + assert config_validation.unbounded_percentage(value) == expected + + +@pytest.mark.parametrize( + "value", + ( + "-10%", + "-0.5", + -1, + "foo", + None, + ), +) +def test_unbounded_percentage__invalid(value: object) -> None: + with pytest.raises(Invalid): + config_validation.unbounded_percentage(value) + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("0%", 0.0), + ("50%", 0.5), + ("150%", 1.5), + ("-50%", -0.5), + ("-150%", -1.5), + ("200%", 2.0), + ("-200%", -2.0), + (0.0, 0.0), + (0.5, 0.5), + (-0.5, -0.5), + (1.0, 1.0), + (-1.0, -1.0), + ), +) +def test_unbounded_possibly_negative_percentage__valid( + value: object, expected: float +) -> None: + assert config_validation.unbounded_possibly_negative_percentage(value) == expected + + +@pytest.mark.parametrize("value", ("foo", None)) +def test_unbounded_possibly_negative_percentage__invalid(value: object) -> None: + with pytest.raises(Invalid): + config_validation.unbounded_possibly_negative_percentage(value) + + +@pytest.mark.parametrize( + "value", + (50, -50, 2, -2), +) +def test_percentage_validators__raw_number_above_one_without_percent_sign( + value: object, +) -> None: + """Raw numeric values outside [-1, 1] must use a percent sign.""" + with pytest.raises(Invalid, match="percent sign"): + config_validation.unbounded_percentage(value) + with pytest.raises(Invalid, match="percent sign"): + config_validation.unbounded_possibly_negative_percentage(value) From 801f3fadaa266acac82e29dcc3060d576431f947 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:00:39 +1000 Subject: [PATCH 1308/2056] [epaper_spi] Fix deep sleep command (#15544) --- esphome/components/epaper_spi/epaper_spi.h | 8 +++++--- esphome/components/epaper_spi/epaper_spi_mono.cpp | 14 +++++++++++++- tests/components/epaper_spi/test.esp32-s3-idf.yaml | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 47b4f9f72df..2992ca5afda 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -110,12 +110,14 @@ class EPaperBase : public Display, this->fill(COLOR_ON); } - protected: - int get_height_internal() override { return this->height_; }; - int get_width_internal() override { return this->width_; }; int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; } int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; } void draw_pixel_at(int x, int y, Color color) override; + + protected: + int get_height_internal() override { return this->height_; }; + int get_width_internal() override { return this->width_; }; + bool is_using_partial_update_() const { return this->full_update_every_ > 1; } void process_state_(); const char *epaper_state_to_string_(); diff --git a/esphome/components/epaper_spi/epaper_spi_mono.cpp b/esphome/components/epaper_spi/epaper_spi_mono.cpp index d10022c4ac9..ee117304c4f 100644 --- a/esphome/components/epaper_spi/epaper_spi_mono.cpp +++ b/esphome/components/epaper_spi/epaper_spi_mono.cpp @@ -15,7 +15,11 @@ void EPaperMono::refresh_screen(bool partial) { void EPaperMono::deep_sleep() { ESP_LOGV(TAG, "Deep sleep"); - this->command(0x10); + if (this->is_using_partial_update_()) { + this->cmd_data(0x10, {0x00}); // sleep in power on mode + } else { + this->cmd_data(0x10, {0x03}); // deep sleep + } } bool EPaperMono::reset() { @@ -27,6 +31,14 @@ bool EPaperMono::reset() { } void EPaperMono::set_window() { + // if not using partial update, the display will go into deep sleep, so must rewrite entire + // buffer since the display RAM will not retain contents + if (!this->is_using_partial_update_()) { + this->x_low_ = 0; + this->x_high_ = this->width_; + this->y_low_ = 0; + this->y_high_ = this->height_; + } // round x-coordinates to byte boundaries this->x_low_ &= ~7; this->x_high_ += 7; diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 9593d0f6f07..bf6053c78bc 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -78,6 +78,7 @@ display: model: seeed-reterminal-e1002 - platform: epaper_spi model: seeed-ee04-mono-4.26 + full_update_every: 10 # Override pins to avoid conflict with other display configs busy_pin: 43 dc_pin: 42 From d20d613c1da39c5c0bcddb1b426fbac9a52a68fd Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Wed, 8 Apr 2026 03:12:55 +0200 Subject: [PATCH 1309/2056] [substitutions] `!include ${filename}`, Substitutions in include filename paths (package refactor part 5) (#12213) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/packages/__init__.py | 61 +++++-- esphome/components/substitutions/__init__.py | 58 ++++++ esphome/components/substitutions/jinja.py | 16 +- esphome/config_validation.py | 7 +- esphome/expression.py | 25 +++ esphome/yaml_util.py | 148 ++++++++++++--- .../11-include_path.approved.yaml | 15 ++ .../substitutions/11-include_path.input.yaml | 21 +++ .../substitutions/12-yaml-merge.approved.yaml | 9 + .../substitutions/12-yaml-merge.input.yaml | 10 ++ .../fixtures/substitutions/inc2.yaml | 6 + .../fixtures/substitutions/inc3.yaml | 3 + tests/unit_tests/test_substitutions.py | 68 ++++++- tests/unit_tests/test_yaml_util.py | 168 +++++++++++++++++- 14 files changed, 562 insertions(+), 53 deletions(-) create mode 100644 esphome/expression.py create mode 100644 tests/unit_tests/fixtures/substitutions/11-include_path.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/11-include_path.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/12-yaml-merge.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/12-yaml-merge.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/inc2.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/inc3.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 1a6df84fe07..04db690c6f4 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -6,7 +6,12 @@ from pathlib import Path from typing import Any from esphome import git, yaml_util -from esphome.components.substitutions import ContextVars, push_context, substitute +from esphome.components.substitutions import ( + ContextVars, + push_context, + resolve_include, + substitute, +) from esphome.components.substitutions.jinja import has_jinja from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv @@ -31,6 +36,8 @@ from esphome.core import EsphomeError _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_PACKAGES +# Guard against infinite include chains (e.g. A includes B includes A). +MAX_INCLUDE_DEPTH = 20 def is_remote_package(package_config: dict) -> bool: @@ -59,8 +66,8 @@ def valid_package_contents(package_config: dict) -> dict: for k, v in package_config.items(): if not isinstance(k, str): raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list, Remove)): - continue # e.g. script: [], psram: !remove, logger: {level: debug} + if isinstance(v, (dict, list, Remove, yaml_util.IncludeFile)): + continue # e.g. script: [], psram: !remove, logger: {level: debug}, switch: !include switches.yaml if v is None: continue # e.g. web_server: if isinstance(v, str) and has_jinja(v): @@ -160,6 +167,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All( PACKAGE_SCHEMA = cv.Any( # A package definition is either: validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or + yaml_util.IncludeFile, # isinstance check — passes IncludeFile objects through unchanged, or: valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} # which will have to be fully validated later as per each component's schema. ) @@ -396,16 +404,49 @@ class _PackageProcessor: self.skip_update = skip_update def resolve_package( - self, package_config: dict | str, context_vars: ContextVars | None + self, + package_config: dict | str | yaml_util.IncludeFile, + context_vars: ContextVars | None, ) -> dict: - """Substitute variables in the definition and fetch remote packages. + """Resolve a package definition to a concrete ``dict`` and fetch remote packages. - The input may be a ``str`` (git shorthand or Jinja expression) or a - ``dict`` (remote or local package). After ``PACKAGE_SCHEMA`` validation - the result is always a ``dict``. + The input may be a ``str`` (git shorthand or Jinja expression), a + ``dict`` (remote or local package), or an ``IncludeFile`` whose filename + may itself contain substitution expressions. + + The loop handles the case where loading an ``IncludeFile`` yields another + ``IncludeFile`` (e.g. a chain of deferred includes). Each iteration: + + 1. If the current value is an ``IncludeFile``, load it — resolving any + substitutions in its filename first. + 2. Substitute variables in the resulting value (for strings and remote + package dicts). + 3. Validate against ``PACKAGE_SCHEMA``. If the result is a ``dict``, + the loop exits; otherwise another iteration is needed. + + Raises ``cv.Invalid`` if the chain has not resolved to a ``dict`` after + ``MAX_INCLUDE_DEPTH`` iterations. """ - package_config = _substitute_package_definition(package_config, context_vars) - package_config = PACKAGE_SCHEMA(package_config) + for _ in range(MAX_INCLUDE_DEPTH): + if isinstance(package_config, yaml_util.IncludeFile): + package_config, _ = resolve_include( + package_config, + [], + context_vars or ContextVars(), + strict_undefined=False, + ) + + package_config = _substitute_package_definition( + package_config, context_vars + ) + package_config = PACKAGE_SCHEMA(package_config) + if isinstance(package_config, dict): + break + else: + raise cv.Invalid( + f"Maximum include nesting depth ({MAX_INCLUDE_DEPTH}) exceeded" + ) + if is_remote_package(package_config): package_config = _process_remote_package(package_config, self.skip_update) return package_config diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index aab1712b652..c0bd9d7be97 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -2,6 +2,7 @@ from collections import ChainMap import logging from typing import Any +import esphome from esphome import core from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered import esphome.config_validation as cv @@ -12,6 +13,7 @@ from esphome.yaml_util import ( ConfigContext, ESPHomeDataBase, ESPLiteralValue, + IncludeFile, make_data_base, ) @@ -291,6 +293,59 @@ def push_context( return parent_context +def resolve_include( + include: IncludeFile, + path: list[int | str], + context_vars: ContextVars, + strict_undefined: bool = True, + errors: ErrList | None = None, +) -> tuple[Any, str]: + """Resolve an include, substituting the filename if needed. + + Returns the loaded content and the resolved filename. + + Note: no path-traversal validation is performed on the resolved filename. + A substitution that resolves to an absolute path will bypass the parent + directory (Path.__truediv__ ignores the left operand for absolute paths). + ESPHome's trust model assumes the config author controls all substitution + values (including command-line substitutions), so path restrictions are + an explicit non-goal here. + """ + original = str(include.file) + filename = str( + _expand_substitutions( + original, path + ["file"], context_vars, strict_undefined, errors + ) + ) + if filename != original: + include = IncludeFile( + include.parent_file, filename, include.vars, include.yaml_loader + ) + try: + return include.load(), filename + except esphome.core.EsphomeError as err: + raise cv.Invalid( + f"Error including file '{filename}': {err}", + path + [f"<{filename}>"], + ) from err + + +def _substitute_include( + include: IncludeFile, + path: list[int | str], + context_vars: ContextVars, + strict_undefined: bool, + errors: ErrList | None, +) -> Any: + """Resolve an include and substitute its content.""" + content, filename = resolve_include( + include, path, context_vars, strict_undefined, errors + ) + return substitute( + content, path + [f"<{filename}>"], context_vars, strict_undefined, errors + ) + + def substitute( item: Any, path: SubstitutionPath, @@ -333,6 +388,9 @@ def substitute( if item.value != value: result = type(item)(value) + elif isinstance(item, IncludeFile): + result = _substitute_include(item, path, context_vars, strict_undefined, errors) + if isinstance(item, ESPHomeDataBase): result = make_data_base(result, item) return result diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index 37e9fa4d2db..36a7425a69f 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -2,7 +2,6 @@ from ast import literal_eval from collections.abc import Iterator, Mapping from itertools import chain, islice import math -import re from types import GeneratorType from typing import Any @@ -10,6 +9,9 @@ import jinja2 as jinja from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate from jinja2.runtime import missing as Missing +# Re-exported for backward compatibility — consumers import has_jinja from here +from esphome.expression import has_jinja # noqa: F401 # pylint: disable=unused-import + TemplateError = jinja.TemplateError TemplateSyntaxError = jinja.TemplateSyntaxError TemplateRuntimeError = jinja.TemplateRuntimeError @@ -20,18 +22,6 @@ Undefined = jinja.Undefined Resolver = ".resolver" -DETECT_JINJA = r"(\$\{)" -detect_jinja_re = re.compile( - r"<%.+?%>" # Block form expression: <% ... %> - r"|\$\{[^}]+\}", # Braced form expression: ${ ... } - flags=re.MULTILINE, -) - - -def has_jinja(st: str) -> bool: - return detect_jinja_re.search(st) is not None - - # SAFE_GLOBALS defines a allowlist of built-in functions or modules that are considered safe to expose # in Jinja templates or other sandboxed evaluation contexts. Only functions that do not allow # arbitrary code execution, file access, or other security risks are included. diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b0bd9e6231d..31cfb41a6d9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -75,7 +75,6 @@ from esphome.const import ( SCHEDULER_DONT_RUN, TYPE_GIT, TYPE_LOCAL, - VALID_SUBSTITUTIONS_CHARACTERS, Framework, __version__ as ESPHOME_VERSION, ) @@ -90,6 +89,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.expression import SUBSTITUTION_VARIABLE_PROG as VARIABLE_PROG from esphome.helpers import add_class_to_obj, docs_url, list_starts_with from esphome.schema_extractors import ( SCHEMA_EXTRACT, @@ -104,11 +104,6 @@ from esphome.yaml_util import make_data_base _LOGGER = logging.getLogger(__name__) -# pylint: disable=consider-using-f-string -VARIABLE_PROG = re.compile( - f"\\$([{VALID_SUBSTITUTIONS_CHARACTERS}]+|\\{{[{VALID_SUBSTITUTIONS_CHARACTERS}]*\\}})" -) - # pylint: disable=invalid-name Schema = _Schema diff --git a/esphome/expression.py b/esphome/expression.py new file mode 100644 index 00000000000..d425d822a46 --- /dev/null +++ b/esphome/expression.py @@ -0,0 +1,25 @@ +"""Helpers for detecting substitution variables and Jinja expressions.""" + +import re + +from esphome.const import VALID_SUBSTITUTIONS_CHARACTERS + +SUBSTITUTION_VARIABLE_PROG = re.compile( + rf"\$([{VALID_SUBSTITUTIONS_CHARACTERS}]+|\{{[{VALID_SUBSTITUTIONS_CHARACTERS}]*\}})" +) + +_JINJA_RE = re.compile( + r"<%.+?%>" # Block: <% ... %> + r"|\$\{[^}]+\}", # Braced: ${ ... } + flags=re.MULTILINE, +) + + +def has_jinja(value: str) -> bool: + """Check if a string contains Jinja expressions.""" + return _JINJA_RE.search(value) is not None + + +def has_substitution_or_expression(value: str) -> bool: + """Check if a string contains substitution variables ($name, ${name}) or Jinja expressions.""" + return SUBSTITUTION_VARIABLE_PROG.search(value) is not None or has_jinja(value) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index a24c1ebccbc..c621428196f 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -33,6 +33,7 @@ from esphome.core import ( MACAddress, TimePeriod, ) +from esphome.expression import has_substitution_or_expression from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files @@ -110,24 +111,6 @@ def make_data_base( return value -class ConfigContext: - """This is a mixin class that holds substitution vars that should be applied - to the tagged node and its children. During configuration loading, context vars can - be added to nodes using `add_context` function, which applies the mixin storing - the captured values and unevaluated expressions. - The substitution pass then recreates the effective context by merging the context vars - from this node and parent nodes. - """ - - @property - def vars(self) -> dict[str, Any]: - return self._context_vars - - def set_context(self, vars: dict[str, Any]) -> None: - # pylint: disable=attribute-defined-outside-init - self._context_vars = vars - - def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: """Tags a list/string/dict value with context vars that must be applied to it and its children during the substitution pass. If no vars are given, no tagging is done. @@ -151,6 +134,94 @@ def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: return value +class ConfigContext: + """This is a mixin class that holds substitution vars that should be applied + to the tagged node and its children. During configuration loading, context vars can + be added to nodes using `add_context` function, which applies the mixin storing + the captured values and unevaluated expressions. + The substitution pass then recreates the effective context by merging the context vars + from this node and parent nodes. + """ + + @property + def vars(self) -> dict[str, Any]: + return self._context_vars + + def set_context(self, vars: dict[str, Any]) -> None: + # pylint: disable=attribute-defined-outside-init + self._context_vars = vars + + def copy_context_to_children(self) -> None: + """Propagate context to children. + + isinstance(self, dict/list) works because ConfigContext is dynamically + mixed into dict/list subclasses via add_class_to_obj in add_context(). + """ + if isinstance(self, dict): + # pylint: disable=no-member + tagged = { + add_context(k, self.vars): add_context(v, self.vars) + for k, v in self.items() + } + self.clear() + self.update(tagged) + elif isinstance(self, list): + for i, item in enumerate(self): + # pylint: disable=unsupported-assignment-operation + self[i] = add_context(item, self.vars) + + +_UNSET = object() + + +class IncludeFile: + """Deferred !include that is resolved during the substitution pass. + + Created during YAML parsing instead of loading the file immediately, + allowing substitution variables to appear in the filename path + (e.g. ``!include device-${platform}.yaml``). The actual file is + loaded on the first call to ``load()``, and the result is cached. + """ + + def __init__( + self, + parent_file: Path, + file: Path | str, + vars: dict[str, Any] | None, + yaml_loader: Callable[[Path], Any], + ) -> None: + self.parent_file = parent_file + self.file = Path(file) + self.vars = vars + self.yaml_loader = yaml_loader + self._content: Any = _UNSET + + def __repr__(self) -> str: + return f"IncludeFile({self.file.as_posix()})" + + def load(self) -> Any: + """Load and cache the included file content. + + Note: returns the cached mutable object on subsequent calls. + Callers that need to modify the result should copy it first. + """ + if self._content is not _UNSET: + return self._content + if self.has_unresolved_expressions(): + from esphome.config_validation import Invalid + + raise Invalid( + f"Cannot load include with unresolved substitutions: {self.file}" + ) + self._content = self.yaml_loader(Path(self.parent_file.parent / self.file)) + self._content = add_context(self._content, self.vars) + return self._content + + def has_unresolved_expressions(self) -> bool: + """Check if the filename contains substitution variables or Jinja expressions.""" + return has_substitution_or_expression(str(self.file)) + + def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): @@ -170,6 +241,36 @@ def _add_data_ref(fn): return wrapped +_MAX_MERGE_INCLUDE_DEPTH = 10 + + +def _resolve_merge_include(value: Any, node: yaml.Node, value_node: yaml.Node) -> Any: + """Resolve an IncludeFile (and chains) and propagate context for merge key handling.""" + for _ in range(_MAX_MERGE_INCLUDE_DEPTH): + if not isinstance(value, IncludeFile): + break + if value.has_unresolved_expressions(): + raise yaml.constructor.ConstructorError( + "While constructing a mapping", + node.start_mark, + "Substitution in include filename with merge keys is not supported yet.", + value_node.start_mark, + ) + value = value.load() + else: + raise yaml.constructor.ConstructorError( + "While constructing a mapping", + node.start_mark, + f"Maximum include chain depth ({_MAX_MERGE_INCLUDE_DEPTH}) exceeded in merge key", + value_node.start_mark, + ) + if isinstance(value, ConfigContext): + # Since the parent dict/list will disappear, propagate + # context to children now to retain context vars + value.copy_context_to_children() + return value + + class ESPHomeLoaderMixin: """Loader class that keeps track of line numbers.""" @@ -261,6 +362,9 @@ class ESPHomeLoaderMixin: # This is a merge key, resolve value and add to merge_pairs value = self.construct_object(value_node) + + value = _resolve_merge_include(value, node, value_node) + if isinstance(value, dict): # base case, copy directly to merge_pairs # direct merge, like "<<: {some_key: some_value}" @@ -268,6 +372,7 @@ class ESPHomeLoaderMixin: elif isinstance(value, list): # sequence merge, like "<<: [{some_key: some_value}, {other_key: some_value}]" for item in value: + item = _resolve_merge_include(item, node, value_node) if not isinstance(item, dict): raise yaml.constructor.ConstructorError( "While constructing a mapping", @@ -362,8 +467,11 @@ class ESPHomeLoaderMixin: else: file, vars = node.value, None - result = self.yaml_loader(self._rel_path(file)) - return add_context(result, vars) + return IncludeFile(self.name, file, vars, self.yaml_loader) + + # Directory includes (!include_dir_*) load eagerly during YAML parsing + # because their paths are directory names, not individual files, and + # substitutions in directory paths are not supported. @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: diff --git a/tests/unit_tests/fixtures/substitutions/11-include_path.approved.yaml b/tests/unit_tests/fixtures/substitutions/11-include_path.approved.yaml new file mode 100644 index 00000000000..d758a832a49 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/11-include_path.approved.yaml @@ -0,0 +1,15 @@ +values: + - var1: 4 + - a: 5 + - b: 6 + - c: The value of C is 7 + - This value comes from inc2.yaml. x is 3, y is 4 + - From main config, x is 3, y is 2 + - $a $b $c are out of scope here + - keys_in_inc3: + x: 3 + y: 2 +substitutions: + x: 3 + y: 2 + include_file: inc1 diff --git a/tests/unit_tests/fixtures/substitutions/11-include_path.input.yaml b/tests/unit_tests/fixtures/substitutions/11-include_path.input.yaml new file mode 100644 index 00000000000..78b1cb4fb9f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/11-include_path.input.yaml @@ -0,0 +1,21 @@ +substitutions: + include_file: inc1 + x: 3 # override x from inc2.yaml + +packages: + my_package: !include + file: ${include_file + ".yaml"} # includes inc1.yaml + vars: + var1: 4 + a: ${x+2} + b: ${a+1} + c: 7 + other_package: !include + file: inc${1+1}.yaml # includes inc2.yaml + vars: + y: 4 + +values: + - From main config, x is $x, y is $y + - $a $b $c are out of scope here + - !include ${"inc" + "3.yaml"} # includes inc3.yaml here (not a package) diff --git a/tests/unit_tests/fixtures/substitutions/12-yaml-merge.approved.yaml b/tests/unit_tests/fixtures/substitutions/12-yaml-merge.approved.yaml new file mode 100644 index 00000000000..02d85124982 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/12-yaml-merge.approved.yaml @@ -0,0 +1,9 @@ +substitutions: + x: 7 +test_list: + - content: + before: Content before + after: Content after + keys_in_inc3: + x: 7 + y: 8 diff --git a/tests/unit_tests/fixtures/substitutions/12-yaml-merge.input.yaml b/tests/unit_tests/fixtures/substitutions/12-yaml-merge.input.yaml new file mode 100644 index 00000000000..a03e66e393c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/12-yaml-merge.input.yaml @@ -0,0 +1,10 @@ +substitutions: + x: 7 +test_list: + - content: + before: Content before + <<: !include + file: inc3.yaml + vars: + y: 8 + after: Content after diff --git a/tests/unit_tests/fixtures/substitutions/inc2.yaml b/tests/unit_tests/fixtures/substitutions/inc2.yaml new file mode 100644 index 00000000000..29a1833efc6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/inc2.yaml @@ -0,0 +1,6 @@ +substitutions: + x: 1 + y: 2 + +values: + - This value comes from inc2.yaml. x is $x, y is $y diff --git a/tests/unit_tests/fixtures/substitutions/inc3.yaml b/tests/unit_tests/fixtures/substitutions/inc3.yaml new file mode 100644 index 00000000000..03d459dc974 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/inc3.yaml @@ -0,0 +1,3 @@ +keys_in_inc3: + x: ${x} + y: ${y} diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index c7b0bbcf7ca..01c669e5420 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -8,12 +8,17 @@ import pytest from esphome import config as config_module, yaml_util from esphome.components import substitutions -from esphome.components.packages import do_packages_pass, merge_packages +from esphome.components.packages import ( + MAX_INCLUDE_DEPTH, + _PackageProcessor, + do_packages_pass, + merge_packages, +) from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, merge_config import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS -from esphome.core import CORE, Lambda +from esphome.core import CORE, EsphomeError, Lambda from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -630,3 +635,62 @@ def test_do_substitution_pass_substitutions_must_be_mapping_from_config() -> Non cv.Invalid, match="Substitutions must be a key to value mapping" ): substitutions.do_substitution_pass(config) + + +# ── IncludeFile / package loading tests ──────────────────────────────────── + + +def test_resolve_package_max_depth_exceeded(tmp_path: Path) -> None: + """A yaml_loader that always returns another IncludeFile triggers the depth guard.""" + parent = tmp_path / "main.yaml" + parent.write_text("") + + # Each call to the loader returns a fresh IncludeFile pointing at itself, + # so PACKAGE_SCHEMA always sees an IncludeFile and never a dict. + def always_returns_include(path: Path) -> yaml_util.IncludeFile: + return yaml_util.IncludeFile(parent, path.name, None, always_returns_include) + + package_config = yaml_util.IncludeFile( + parent, "test.yaml", None, always_returns_include + ) + processor = _PackageProcessor({}, None, False) + with pytest.raises( + cv.Invalid, + match=f"Maximum include nesting depth \\({MAX_INCLUDE_DEPTH}\\) exceeded", + ): + processor.resolve_package(package_config, substitutions.ContextVars()) + + +def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: + """!include with an undefined substitution variable raises cv.Invalid. + + The error message must reference the unresolved filename template so the + user knows which include failed, rather than seeing a bare file-not-found. + """ + main_file = tmp_path / "main.yaml" + main_file.write_text("result: !include ${undefined_var}.yaml\n") + + config = yaml_util.load_yaml(main_file) + with pytest.raises(cv.Invalid, match=r"\$\{undefined_var\}"): + substitutions.do_substitution_pass(config) + + +def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> None: + """An undefined substitution in a package include filename raises cv.Invalid. + + Previously this would raise an unhandled UndefinedError. With + strict_undefined=False, the unresolved filename passes through to + file loading which produces a clean cv.Invalid error. + """ + parent = tmp_path / "main.yaml" + parent.write_text("") + + def loader(path: Path): + raise EsphomeError(f"Error reading file {path}: No such file") + + package_config = yaml_util.IncludeFile( + parent, "${undefined_var}.yaml", None, loader + ) + processor = _PackageProcessor({}, None, False) + with pytest.raises(cv.Invalid, match="unresolved substitutions"): + processor.resolve_package(package_config, substitutions.ContextVars()) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 0342d12540e..0bd7c9453b5 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1,3 +1,4 @@ +import io from pathlib import Path import shutil from unittest.mock import patch @@ -7,6 +8,7 @@ import pytest from esphome import core, yaml_util from esphome.components import substitutions from esphome.config_helpers import Extend, Remove +import esphome.config_validation as cv from esphome.core import EsphomeError from esphome.util import OrderedDict @@ -74,7 +76,9 @@ def test_parsing_with_custom_loader(fixture_path): loader_calls.append(fname) with yaml_file.open(encoding="utf-8") as f_handle: - yaml_util.parse_yaml(yaml_file, f_handle, custom_loader) + config = yaml_util.parse_yaml(yaml_file, f_handle, custom_loader) + # substitute config to expand includes: + substitutions.substitute(config, [], substitutions.ContextVars(), False) assert len(loader_calls) == 3 assert loader_calls[0].parts[-2:] == ("includes", "included.yaml") @@ -348,7 +352,9 @@ def test_track_yaml_loads_records_includes(tmp_path: Path) -> None: main.write_text("child: !include included.yaml\n") with yaml_util.track_yaml_loads() as loaded: - yaml_util.load_yaml(main) + result = yaml_util.load_yaml(main) + # !include is deferred; resolve it to trigger the nested load + result["child"].load() resolved = [p.name for p in loaded] assert "main.yaml" in resolved @@ -500,3 +506,161 @@ def test_represent_extend() -> None: def test_represent_remove() -> None: """Test that Remove objects are dumped as plain !remove scalars.""" assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n" + + +# ── IncludeFile unit tests ────────────────────────────────────────────────── + + +def test_include_file_repr(tmp_path: Path) -> None: + """repr() includes the filename so it appears usefully in error messages.""" + parent = tmp_path / "main.yaml" + include = yaml_util.IncludeFile(parent, "some/nested.yaml", None, lambda _: {}) + assert repr(include) == "IncludeFile(some/nested.yaml)" + + +def test_include_file_load_caches_result(tmp_path: Path) -> None: + """load() invokes the yaml_loader only once; subsequent calls return the cached object.""" + parent = tmp_path / "main.yaml" + content = {"key": "value"} + call_count = 0 + + def counting_loader(_): + nonlocal call_count + call_count += 1 + return content + + include = yaml_util.IncludeFile(parent, "child.yaml", None, counting_loader) + first = include.load() + second = include.load() + + assert call_count == 1 + assert first is second + + +def test_include_file_load_caches_none_result(tmp_path: Path) -> None: + """load() caches None content (empty YAML files) and does not re-invoke the loader.""" + parent = tmp_path / "main.yaml" + call_count = 0 + + def counting_loader(_): + nonlocal call_count + call_count += 1 + + include = yaml_util.IncludeFile(parent, "empty.yaml", None, counting_loader) + first = include.load() + second = include.load() + + assert call_count == 1 + assert first is None + assert second is None + + +def test_include_file_load_raises_on_unresolved_expressions(tmp_path: Path) -> None: + """load() raises if the filename contains unresolved substitutions or expressions.""" + parent = tmp_path / "main.yaml" + include = yaml_util.IncludeFile(parent, "${undefined_var}.yaml", None, lambda _: {}) + with pytest.raises(cv.Invalid, match="unresolved"): + include.load() + + +@pytest.mark.parametrize( + ("filename", "expected"), + [ + ("device-${platform}.yaml", True), + ("$platform.yaml", True), + ("${a + b}.yaml", True), # Jinja expression + ("device.yaml", False), + ("path/to/device.yaml", False), + ("my$file.yaml", True), # $file is a valid substitution + ("price-100$.yaml", False), # $ at end, not followed by valid substitution + ], +) +def test_include_file_has_unresolved_expressions( + tmp_path: Path, filename: str, expected: bool +) -> None: + """has_unresolved_expressions() detects substitution patterns in the filename.""" + parent = tmp_path / "main.yaml" + include = yaml_util.IncludeFile(parent, filename, None, lambda _: {}) + assert include.has_unresolved_expressions() == expected + + +def test_include_in_list_context() -> None: + """!include of a file returning a list is handled correctly, + including when that list itself contains a nested IncludeFile.""" + parent = Path("/fake/main.yaml") + + # The nested IncludeFile resolves to a plain string value + inner = yaml_util.IncludeFile(parent, "inner.yaml", None, lambda _: "gamma") + + # The outer IncludeFile returns a list whose last element is itself an IncludeFile, + # exercising the substitution pass's ability to recurse into loaded content. + outer = yaml_util.IncludeFile( + parent, "items.yaml", None, lambda _: ["alpha", "beta", inner] + ) + + config = OrderedDict({"values": outer}) + config = substitutions.do_substitution_pass(config) + + assert config["values"] == ["alpha", "beta", "gamma"] + + +def test_include_plain_filename_loads_after_deferred_refactor() -> None: + """!include with a plain filename (no $ expressions) still loads correctly. + + Regression guard: the deferred-loading refactor must not break the simple case. + """ + parent = Path("/fake/main.yaml") + include = yaml_util.IncludeFile( + parent, "child.yaml", None, lambda _: {"answer": 42} + ) + + config = OrderedDict({"result": include}) + config = substitutions.do_substitution_pass(config) + + assert config["result"]["answer"] == 42 + + +def test_yaml_merge_include_with_filename_substitution_raises() -> None: + """<<: !include ${expr} raises a clear error — substitutions in merge-key filenames + are not yet supported, and the error message must say so.""" + yaml_text = "base:\n existing: value\n <<: !include ${filename}.yaml\n" + with pytest.raises(EsphomeError, match="not supported yet"): + yaml_util.parse_yaml( + Path("/fake/main.yaml"), io.StringIO(yaml_text), lambda _: {} + ) + + +def test_yaml_merge_list_include_with_filename_substitution_raises() -> None: + """Substitutions in include filenames within merge-key lists raise a clear error.""" + yaml_text = "base:\n existing: value\n <<:\n - !include ${filename}.yaml\n" + with pytest.raises(EsphomeError, match="not supported yet"): + yaml_util.parse_yaml( + Path("/fake/main.yaml"), io.StringIO(yaml_text), lambda _: {} + ) + + +def test_yaml_merge_chain_include_resolves() -> None: + """Chained includes in merge keys resolve through multiple IncludeFile layers.""" + parent = Path("/fake/main.yaml") + + inner = yaml_util.IncludeFile(parent, "inner.yaml", None, lambda _: {"x": 1}) + outer = yaml_util.IncludeFile(parent, "outer.yaml", None, lambda _: inner) + + yaml_text = "base:\n existing: value\n <<: !include outer.yaml\n" + config = yaml_util.parse_yaml(parent, io.StringIO(yaml_text), lambda _: outer) + config = substitutions.do_substitution_pass(config) + + assert config["base"]["x"] == 1 + assert config["base"]["existing"] == "value" + + +def test_yaml_merge_chain_include_depth_exceeded() -> None: + """Chain includes in merge keys exceeding depth limit raise a clear error.""" + parent = Path("/fake/main.yaml") + + def self_referencing_loader(path: Path) -> yaml_util.IncludeFile: + return yaml_util.IncludeFile(parent, path.name, None, self_referencing_loader) + + yaml_text = "base:\n <<: !include loop.yaml\n" + with pytest.raises(EsphomeError, match="Maximum include chain depth"): + yaml_util.parse_yaml(parent, io.StringIO(yaml_text), self_referencing_loader) From 88f4067dd6bbaae97e679e734626dea5d761d26a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:19:29 +1000 Subject: [PATCH 1310/2056] [lvgl] Implement rotation with PPA (#15453) --- esphome/components/lvgl/__init__.py | 13 +- esphome/components/lvgl/lvgl_esphome.cpp | 144 ++++++++++++++++--- esphome/components/lvgl/lvgl_esphome.h | 14 ++ tests/components/lvgl/test.esp32-idf.yaml | 4 +- tests/components/lvgl/test.esp32-p4-idf.yaml | 12 ++ 5 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 tests/components/lvgl/test.esp32-p4-idf.yaml diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b69f8ef57bf..f6f6204f4c4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -187,7 +187,6 @@ def final_validation(config_list): for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") - uses_rotation = CONF_ROTATION in config for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) @@ -196,9 +195,9 @@ def final_validation(config_list): "Using lambda: or pages: in display config is not compatible with LVGL" ) # treating 0 as false is intended here. - if uses_rotation and display.get(CONF_ROTATION): - df.LOGGER.warning( - "use of 'rotation' in both LVGL and the display config is not recommended" + if display.get(CONF_ROTATION): + raise cv.Invalid( + "use of 'rotation' in the display config is not compatible with LVGL, please set rotation in the LVGL config instead" ) if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( @@ -262,6 +261,7 @@ async def to_code(configs): df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") + df.add_define("LV_DEF_REFR_PERIOD", "16") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets # cg.add_define("LV_KCONFIG_PRESENT") @@ -341,7 +341,10 @@ async def to_code(configs): df.LOGGER.info("LVGL will use hardware rotation via display driver") else: rotation_type = RotationType.ROTATION_SOFTWARE - df.LOGGER.info("LVGL will use software rotation") + if get_esp32_variant() == VARIANT_ESP32P4: + df.LOGGER.info("LVGL will use software rotation (PPA accelerated)") + else: + df.LOGGER.info("LVGL will use software rotation") lv_component = cg.new_Pvariable( config[CONF_ID], displays, diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 0ab49d0a101..0c4e7a3425d 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -158,8 +158,15 @@ void LvglComponent::dump_config() { " Draw rounding: %d", this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); if (this->rotation_type_ != ROTATION_UNUSED) { - ESP_LOGCONFIG(TAG, " Rotation type: %s", - this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); + const char *rot_type = "hardware via display driver"; + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { +#ifdef USE_ESP32_VARIANT_ESP32P4 + rot_type = this->ppa_client_ != nullptr ? "software (PPA accelerated)" : "software"; +#else + rot_type = "software"; +#endif + } + ESP_LOGCONFIG(TAG, " Rotation type: %s", rot_type); } } @@ -252,21 +259,120 @@ void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) { size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } +#ifdef USE_ESP32_VARIANT_ESP32P4 +bool LvglComponent::ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height, + uint32_t height_rounded) { + ppa_srm_rotation_angle_t angle; + uint16_t out_w, out_h; + + // Map ESPHome clockwise display rotation to PPA counter-clockwise angles + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + angle = PPA_SRM_ROTATION_ANGLE_270; // 270° CCW = 90° CW + out_w = height_rounded; + out_h = width; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + angle = PPA_SRM_ROTATION_ANGLE_180; + out_w = width; + out_h = height; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + angle = PPA_SRM_ROTATION_ANGLE_90; // 90° CCW = 270° CW + out_w = height_rounded; + out_h = width; + break; + default: + return false; // No rotation needed + } + + // Align buffer size to cache line (LV_DRAW_BUF_ALIGN) as required by PPA DMA + // the underlying buffer will be large enough as the size is also padded when allocating. + size_t out_buf_size = out_w * out_h * sizeof(lv_color_data); + out_buf_size = LV_ROUND_UP(out_buf_size, LV_DRAW_BUF_ALIGN); + + ppa_srm_oper_config_t srm_config{}; + srm_config.in.buffer = src; + srm_config.in.pic_w = width; + srm_config.in.pic_h = height; + srm_config.in.block_w = width; + srm_config.in.block_h = height; +#if LV_COLOR_DEPTH == 16 + srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565; +#elif LV_COLOR_DEPTH == 32 + srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888; +#endif + srm_config.out.buffer = dst; + srm_config.out.buffer_size = out_buf_size; + srm_config.out.pic_w = out_w; + srm_config.out.pic_h = out_h; +#if LV_COLOR_DEPTH == 16 + srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565; +#elif LV_COLOR_DEPTH == 32 + srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888; +#endif + srm_config.rotation_angle = angle; + srm_config.scale_x = 1.0f; + srm_config.scale_y = 1.0f; + srm_config.mode = PPA_TRANS_MODE_BLOCKING; + + esp_err_t ret = ppa_do_scale_rotate_mirror(this->ppa_client_, &srm_config); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "PPA rotation failed: %s", esp_err_to_name(ret)); + ESP_LOGW(TAG, "PPA SRM: in=%ux%u src=%p, out=%ux%u dst=%p size=%zu, angle=%d", width, height, src, out_w, out_h, + dst, out_buf_size, (int) angle); + return false; + } + return true; +} +#endif // USE_ESP32_VARIANT_ESP32P4 + void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + if (this->rotation_type_ == ROTATION_SOFTWARE) { lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_); +#ifdef USE_ESP32_VARIANT_ESP32P4 + bool ppa_done = this->ppa_client_ != nullptr && this->ppa_rotate_(ptr, dst, width, height, height_rounded); + if (!ppa_done) +#endif + { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + for (lv_coord_t x = height; x-- != 0;) { + for (lv_coord_t y = 0; y != width; y++) { + dst[y * height_rounded + x] = *ptr++; + } + } + break; + + case display::DISPLAY_ROTATION_180_DEGREES: + for (lv_coord_t y = height; y-- != 0;) { + for (lv_coord_t x = width; x-- != 0;) { + dst[y * width + x] = *ptr++; + } + } + break; + + case display::DISPLAY_ROTATION_270_DEGREES: + for (lv_coord_t x = 0; x != height; x++) { + for (lv_coord_t y = width; y-- != 0;) { + dst[y * height_rounded + x] = *ptr++; + } + } + break; + + default: + dst = ptr; + break; + } + } + // Coordinate adjustments apply regardless of PPA or SW rotation switch (this->rotation_) { case display::DISPLAY_ROTATION_90_DEGREES: - for (lv_coord_t x = height; x-- != 0;) { - for (lv_coord_t y = 0; y != width; y++) { - dst[y * height_rounded + x] = *ptr++; - } - } y1 = x1; x1 = this->width_ - area->y1 - height; height = width; @@ -274,21 +380,11 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { break; case display::DISPLAY_ROTATION_180_DEGREES: - for (lv_coord_t y = height; y-- != 0;) { - for (lv_coord_t x = width; x-- != 0;) { - dst[y * width + x] = *ptr++; - } - } x1 = this->width_ - x1 - width; y1 = this->height_ - y1 - height; break; case display::DISPLAY_ROTATION_270_DEGREES: - for (lv_coord_t x = 0; x != height; x++) { - for (lv_coord_t y = width; y-- != 0;) { - dst[y * height_rounded + x] = *ptr++; - } - } x1 = y1; y1 = this->height_ - area->x1 - width; height = width; @@ -296,7 +392,6 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { break; default: - dst = ptr; break; } ptr = dst; @@ -664,6 +759,15 @@ void LvglComponent::setup() { this->mark_failed(); return; } +#ifdef USE_ESP32_VARIANT_ESP32P4 + ppa_client_config_t ppa_config{}; + ppa_config.oper_type = PPA_OPERATION_SRM; + ppa_config.max_pending_trans_num = 1; + if (ppa_register_client(&ppa_config, &this->ppa_client_) != ESP_OK) { + ESP_LOGW(TAG, "PPA client registration failed, using software rotation"); + this->ppa_client_ = nullptr; + } +#endif } if (this->draw_start_callback_ != nullptr) { lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this); @@ -804,7 +908,7 @@ static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT static void *lv_alloc_draw_buf(size_t size, bool internal) { void *buffer; - size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN; + size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN); buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT if (buffer == nullptr) ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 4a4c11d3834..3433aaa5272 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -26,6 +26,10 @@ #include <utility> #include <vector> +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include "driver/ppa.h" +#endif + #ifdef USE_FONT #include "esphome/components/font/font.h" #endif // USE_LVGL_FONT @@ -229,6 +233,9 @@ class LvglComponent : public PollingComponent { display::DisplayRotation get_rotation() const { return this->rotation_; } void rotate_coordinates(int32_t &x, int32_t &y) const; + uint16_t get_width() const { return lv_display_get_horizontal_resolution(this->disp_); } + uint16_t get_height() const { return lv_display_get_vertical_resolution(this->disp_); } + protected: void set_resolution_() const; void draw_end_(); @@ -238,6 +245,10 @@ class LvglComponent : public PollingComponent { void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_data *ptr); +#ifdef USE_ESP32_VARIANT_ESP32P4 + bool ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height, + uint32_t height_rounded); +#endif void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); std::vector<display::Display *> displays_{}; @@ -266,6 +277,9 @@ class LvglComponent : public PollingComponent { void *rotate_buf_{}; display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; RotationType rotation_type_; +#ifdef USE_ESP32_VARIANT_ESP32P4 + ppa_client_handle_t ppa_client_{}; +#endif }; class IdleTrigger : public Trigger<> { diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index e6025e17fc6..79ea06f16af 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -21,7 +21,7 @@ binary_sensor: ignore_strapping_warning: true display: - - platform: ili9xxx + - platform: mipi_spi spi_id: spi_bus model: st7789v id: second_display @@ -41,7 +41,7 @@ display: invert_colors: false update_interval: never - - platform: ili9xxx + - platform: mipi_spi spi_id: spi_bus model: st7789v id: tft_display diff --git a/tests/components/lvgl/test.esp32-p4-idf.yaml b/tests/components/lvgl/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..5fd93702554 --- /dev/null +++ b/tests/components/lvgl/test.esp32-p4-idf.yaml @@ -0,0 +1,12 @@ +display: + - platform: mipi_dsi + model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C +lvgl: + byte_order: little_endian + rotation: 90 + +psram: + +esp_ldo: + - channel: 3 + voltage: 2.5V From de7f081799d1788d6403cbc01579c4651bb2be14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 15:52:37 -1000 Subject: [PATCH 1311/2056] [emontx] Fix uart package name in tests (#15546) --- tests/components/emontx/test.esp32-idf.yaml | 2 +- tests/components/emontx/test.esp8266-ard.yaml | 2 +- tests/components/emontx/test.rp2040-ard.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/emontx/test.esp32-idf.yaml b/tests/components/emontx/test.esp32-idf.yaml index 3a3747f3a5b..a0784fcd53f 100644 --- a/tests/components/emontx/test.esp32-idf.yaml +++ b/tests/components/emontx/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/emontx/test.esp8266-ard.yaml b/tests/components/emontx/test.esp8266-ard.yaml index 31c5731589a..80a2cb2fc06 100644 --- a/tests/components/emontx/test.esp8266-ard.yaml +++ b/tests/components/emontx/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/emontx/test.rp2040-ard.yaml b/tests/components/emontx/test.rp2040-ard.yaml index ff55e8263d7..410c579d4bd 100644 --- a/tests/components/emontx/test.rp2040-ard.yaml +++ b/tests/components/emontx/test.rp2040-ard.yaml @@ -1,4 +1,4 @@ packages: - uart: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml <<: !include common.yaml From c7513b926219bc668e935829371559c182c9cb5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 16:01:18 -1000 Subject: [PATCH 1312/2056] [ci] Add lint check for test package key matching bus directory (#15547) --- script/ci-custom.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index ad39f92005a..1ec3eab3a94 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1006,6 +1006,38 @@ def lint_log_in_header(fname, line, col, content): ) +PACKAGE_BUS_RE = re.compile( + r"^\s+(\w+):\s*!include\s+\S*test_build_components/common/(\w+)/", + re.MULTILINE, +) + + +@lint_content_check(include=["tests/components/*/test.*.yaml"]) +def lint_test_package_key_matches_bus(fname, content): + """Ensure package keys match the common bus directory name. + + For example, a package using uart_115200 includes must use + 'uart_115200' as the key, not 'uart'. + """ + errs: list[tuple[int, int, str]] = [] + for match in PACKAGE_BUS_RE.finditer(content): + pkg_key = match.group(1) + bus_dir = match.group(2) + if pkg_key != bus_dir: + lineno = content.count("\n", 0, match.start()) + 1 + errs.append( + ( + lineno, + 1, + f"Package key {highlight(pkg_key)} does not match bus directory " + f"{highlight(bus_dir)}. The package key must match the directory " + f"name under tests/test_build_components/common/. " + f"Change {highlight(pkg_key)} to {highlight(bus_dir)}.", + ) + ) + return errs + + @lint_content_find_check( "FINAL_VALIDATE_SCHEMA", include=["esphome/core/*.py"], From 8ffe0f5e31d9816eea26046e05f7b7b3b05a4748 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:02:36 -0400 Subject: [PATCH 1313/2056] [core] Fix ANSI codes for secret text hiding (#15521) --- esphome/__main__.py | 2 +- esphome/core/log.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index a696cceffb0..25b404ae45c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1083,7 +1083,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: # add the console decoration so the front-end can hide the secrets if not args.show_secrets: output = re.sub( - r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output + r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output ) if not CORE.quiet: safe_print(output) diff --git a/esphome/core/log.h b/esphome/core/log.h index ff39633142e..72e06cabacb 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -54,8 +54,8 @@ namespace esphome { #define ESPHOME_LOG_COLOR_CYAN "36" // DEBUG #define ESPHOME_LOG_COLOR_GRAY "37" // VERBOSE #define ESPHOME_LOG_COLOR_WHITE "38" -#define ESPHOME_LOG_SECRET_BEGIN "\033[5m" -#define ESPHOME_LOG_SECRET_END "\033[6m" +#define ESPHOME_LOG_SECRET_BEGIN "\033[8m" +#define ESPHOME_LOG_SECRET_END "\033[28m" #define LOG_SECRET(x) ESPHOME_LOG_SECRET_BEGIN x ESPHOME_LOG_SECRET_END #define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m" From 2e3ff4e215a2cc831ba570d4c9448be39adbcd99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:11:51 +0000 Subject: [PATCH 1314/2056] Bump cryptography from 46.0.6 to 46.0.7 (#15550) 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 5c798819a86..31f33ce7ee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==46.0.6 +cryptography==46.0.7 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From 4db82877af35f9f06d1f7c658e01accd6642b0d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 16:27:11 -1000 Subject: [PATCH 1315/2056] [yaml] Add IncludeFile representer to ESPHomeDumper (#15549) --- esphome/yaml_util.py | 9 ++++++++ tests/unit_tests/test_yaml_util.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index c621428196f..520379e51d2 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -754,6 +754,14 @@ class ESPHomeDumper(yaml.SafeDumper): def represent_remove(self, value): return self.represent_scalar(tag="!remove", value=value.value) + def represent_include_file(self, value): + if value.vars: + mapping = {"file": value.file.as_posix(), "vars": value.vars} + return self.represent_mapping( + tag="!include", mapping=mapping, flow_style=False + ) + return self.represent_scalar(tag="!include", value=value.file.as_posix()) + def represent_id(self, value): if is_secret(value.id): return self.represent_secret(value.id) @@ -785,3 +793,4 @@ ESPHomeDumper.add_multi_representer(Remove, ESPHomeDumper.represent_remove) ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id) ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(IncludeFile, ESPHomeDumper.represent_include_file) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 0bd7c9453b5..2c01019abdf 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -508,6 +508,42 @@ def test_represent_remove() -> None: assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n" +def test_represent_include_file() -> None: + """Test that IncludeFile objects are dumped as !include scalars.""" + include = yaml_util.IncludeFile( + Path("/fake/main.yaml"), "path/to/file.yaml", None, lambda _: {} + ) + assert yaml_util.dump({"key": include}) == "key: !include 'path/to/file.yaml'\n" + + +def test_represent_include_file_with_vars() -> None: + """Test that IncludeFile with vars is dumped as !include mapping form.""" + include = yaml_util.IncludeFile( + Path("/fake/main.yaml"), + "path/to/file.yaml", + {"key": "value"}, + lambda _: {}, + ) + result = yaml_util.dump({"key": include}) + assert "!include" in result + assert "file: path/to/file.yaml" in result + assert "key: value" in result + + +def test_represent_include_file_with_data_base_mixin() -> None: + """Test that IncludeFile wrapped with ESPHomeDataBase mixin is also dumped correctly. + + The YAML loader wraps IncludeFile via add_class_to_obj, creating a dynamic + subclass. add_multi_representer must match this subclass through the MRO. + """ + include = yaml_util.IncludeFile( + Path("/fake/main.yaml"), "common/spi.yaml", None, lambda _: {} + ) + wrapped = yaml_util.make_data_base(include) + assert isinstance(wrapped, yaml_util.ESPHomeDataBase) + assert yaml_util.dump({"pkg": wrapped}) == "pkg: !include 'common/spi.yaml'\n" + + # ── IncludeFile unit tests ────────────────────────────────────────────────── From e658a8559ebafc94deface09ea4e14cdc43611be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 7 Apr 2026 16:57:05 -1000 Subject: [PATCH 1316/2056] [ethernet] Add W6100 and W6300 support for RP2040 (#15543) --- esphome/components/ethernet/__init__.py | 16 +++++++++---- .../components/ethernet/ethernet_component.h | 22 +++++++++++++++++ .../ethernet/ethernet_component_rp2040.cpp | 24 +++++++++++++++++++ esphome/core/defines.h | 2 ++ .../ethernet/common-w6100-rp2040.yaml | 18 ++++++++++++++ .../ethernet/common-w6300-rp2040.yaml | 18 ++++++++++++++ .../ethernet/test-w6100.rp2040-ard.yaml | 1 + .../ethernet/test-w6300.rp2040-ard.yaml | 1 + 8 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 tests/components/ethernet/common-w6100-rp2040.yaml create mode 100644 tests/components/ethernet/common-w6300-rp2040.yaml create mode 100644 tests/components/ethernet/test-w6100.rp2040-ard.yaml create mode 100644 tests/components/ethernet/test-w6300.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index d9f51c677e4..10f9a73863d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -123,6 +123,8 @@ ETHERNET_TYPES = { "DM9051": EthernetType.ETHERNET_TYPE_DM9051, "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, "ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60, + "W6100": EthernetType.ETHERNET_TYPE_W6100, + "W6300": EthernetType.ETHERNET_TYPE_W6300, } # PHY types that need compile-time defines for conditional compilation @@ -140,6 +142,8 @@ _PHY_TYPE_TO_DEFINE = { "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", "ENC28J60": "USE_ETHERNET_ENC28J60", + "W6100": "USE_ETHERNET_W6100", + "W6300": "USE_ETHERNET_W6300", } @@ -170,12 +174,14 @@ _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} # ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} -# RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"} +# RP2040-supported ethernet types (SPI and PIO QSPI) +RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"} _RP2040_SPI_LIBRARIES = { "W5100": "lwIP_w5100", "W5500": "lwIP_w5500", "ENC28J60": "lwIP_enc28j60", + "W6100": "lwIP_w6100", + "W6300": "lwIP_w6300", } SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) @@ -328,9 +334,9 @@ def _validate(config): f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " f"on ESP32 classic and ESP32-P4, not {variant}" ) - elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: + elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES: raise cv.Invalid( - f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, " + f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, " f"not {config[CONF_TYPE]}" ) return config @@ -427,6 +433,8 @@ CONFIG_SCHEMA = cv.All( "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, "ENC28J60": SPI_SCHEMA, + "W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), + "W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), "LAN8670": RMII_SCHEMA, }, upper=True, diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b760ba2af76..3a878423156 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -30,6 +30,20 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #include <W5500lwIP.h> #elif defined(USE_ETHERNET_W5100) #include <W5100lwIP.h> +#elif defined(USE_ETHERNET_W6100) +#include <W6100lwIP.h> +#elif defined(USE_ETHERNET_W6300) +#include <W6300lwIP.h> +// W6300 uses PIO QSPI, not Arduino SPI. The upstream Wiznet6300 class +// incorrectly returns needsSPI()=true, causing LwipIntfDev::begin() to +// call SPI.begin() which claims GPIOs that PIO QSPI needs. +// This wrapper hides needsSPI() with a version returning false. +class Wiznet6300NoSPI : public Wiznet6300 { + public: + using Wiznet6300::Wiznet6300; + constexpr bool needsSPI() const { return false; } +}; +using Wiznet6300lwIPFixed = LwipIntfDev<Wiznet6300NoSPI>; #elif defined(USE_ETHERNET_ENC28J60) #include <ENC28J60lwIP.h> #else @@ -70,6 +84,8 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_DM9051, ETHERNET_TYPE_LAN8670, ETHERNET_TYPE_ENC28J60, + ETHERNET_TYPE_W6100, + ETHERNET_TYPE_W6300, }; struct ManualIP { @@ -232,6 +248,8 @@ class EthernetComponent final : public Component { static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls #if defined(USE_ETHERNET_W5100) static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time +#elif defined(USE_ETHERNET_W6300) + static constexpr uint32_t RESET_DELAY_MS = 100; // W6300 needs 100ms after hardware reset #else static constexpr uint32_t RESET_DELAY_MS = 10; #endif @@ -239,6 +257,10 @@ class EthernetComponent final : public Component { Wiznet5500lwIP *eth_{nullptr}; #elif defined(USE_ETHERNET_W5100) Wiznet5100lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W6100) + Wiznet6100lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W6300) + Wiznet6300lwIPFixed *eth_{nullptr}; #elif defined(USE_ETHERNET_ENC28J60) ENC28J60lwIP *eth_{nullptr}; #else diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index 9771bc59d5a..ef7bd463328 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -18,9 +18,14 @@ static const char *const TAG = "ethernet"; void EthernetComponent::setup() { // Configure SPI pins +#if !defined(USE_ETHERNET_W6300) SPI.setRX(this->miso_pin_); SPI.setTX(this->mosi_pin_); SPI.setSCK(this->clk_pin_); +#endif + // W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI. + // SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false) + // prevents LwipIntfDev::begin() from calling SPI.begin(). // Toggle reset pin if configured if (this->reset_pin_ >= 0) { @@ -40,6 +45,10 @@ void EthernetComponent::setup() { this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #elif defined(USE_ETHERNET_W5100) this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W6100) + this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W6300) + this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #elif defined(USE_ETHERNET_ENC28J60) this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT #endif @@ -183,9 +192,23 @@ void EthernetComponent::dump_config() { type_str = "W5500"; #elif defined(USE_ETHERNET_W5100) type_str = "W5100"; +#elif defined(USE_ETHERNET_W6100) + type_str = "W6100"; +#elif defined(USE_ETHERNET_W6300) + type_str = "W6300"; #elif defined(USE_ETHERNET_ENC28J60) type_str = "ENC28J60"; #endif +#if defined(USE_ETHERNET_W6300) + // W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Type: %s (PIO QSPI)\n" + " Connected: %s\n" + " IRQ Pin: %d\n" + " Reset Pin: %d", + type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_); +#else ESP_LOGCONFIG(TAG, "Ethernet:\n" " Type: %s\n" @@ -198,6 +221,7 @@ void EthernetComponent::dump_config() { " Reset Pin: %d", type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, this->interrupt_pin_, this->reset_pin_); +#endif this->dump_connect_params_(); } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 4939c194e3b..d8b4faced9e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -300,6 +300,8 @@ #define USE_ETHERNET_OPENETH #define USE_ETHERNET_W5100 #define USE_ETHERNET_W5500 +#define USE_ETHERNET_W6100 +#define USE_ETHERNET_W6300 #define USE_ETHERNET_DM9051 #define CONFIG_ETH_SPI_ETHERNET_W5500 1 #define CONFIG_ETH_SPI_ETHERNET_DM9051 1 diff --git a/tests/components/ethernet/common-w6100-rp2040.yaml b/tests/components/ethernet/common-w6100-rp2040.yaml new file mode 100644 index 00000000000..8afbd2d7cd6 --- /dev/null +++ b/tests/components/ethernet/common-w6100-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W6100 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-w6300-rp2040.yaml b/tests/components/ethernet/common-w6300-rp2040.yaml new file mode 100644 index 00000000000..c248bc9810c --- /dev/null +++ b/tests/components/ethernet/common-w6300-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: W6300 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-w6100.rp2040-ard.yaml b/tests/components/ethernet/test-w6100.rp2040-ard.yaml new file mode 100644 index 00000000000..bf119e97c40 --- /dev/null +++ b/tests/components/ethernet/test-w6100.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w6100-rp2040.yaml diff --git a/tests/components/ethernet/test-w6300.rp2040-ard.yaml b/tests/components/ethernet/test-w6300.rp2040-ard.yaml new file mode 100644 index 00000000000..4fa1bb76f44 --- /dev/null +++ b/tests/components/ethernet/test-w6300.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-w6300-rp2040.yaml From 313b9fd5bf9503da438e6368892fb99426aaf072 Mon Sep 17 00:00:00 2001 From: Szewcson <szewcson@gmail.com> Date: Wed, 8 Apr 2026 05:05:18 +0200 Subject: [PATCH 1317/2056] [gdk101] Retry reset on interval for slow-booting sensor MCU (#11750) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/gdk101/gdk101.cpp | 60 ++++++++++++++++++---------- esphome/components/gdk101/gdk101.h | 3 ++ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 8b381564b2b..149973ba8a2 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -6,9 +6,15 @@ namespace esphome { namespace gdk101 { static const char *const TAG = "gdk101"; -static const uint8_t NUMBER_OF_READ_RETRIES = 5; +static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5; +static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10; +static constexpr uint32_t RESET_INTERVAL_ID = 0; +static constexpr uint32_t RESET_INTERVAL_MS = 1000; void GDK101Component::update() { + if (!this->reset_complete_) + return; + uint8_t data[2]; if (!this->read_dose_1m_(data)) { this->status_set_warning(LOG_STR("Failed to read dose 1m")); @@ -33,26 +39,45 @@ void GDK101Component::update() { } void GDK101Component::setup() { - uint8_t data[2]; - // first, reset the sensor + if (!this->try_reset_()) { + // Sensor MCU boots slowly after power cycle — retry on a short interval + this->reset_retries_remaining_ = NUMBER_OF_RESET_RETRIES; + this->set_interval(RESET_INTERVAL_ID, RESET_INTERVAL_MS, [this]() { + if (this->try_reset_()) { + if (this->reset_complete_) { + this->update(); + } + return; + } + if (--this->reset_retries_remaining_ == 0) { + this->cancel_interval(RESET_INTERVAL_ID); + this->mark_failed(LOG_STR("Reset failed after retries")); + } + }); + } +} + +/// Attempt to reset the sensor and read firmware version. Returns true on success or hard failure. +bool GDK101Component::try_reset_() { + uint8_t data[2] = {0}; if (!this->reset_sensor_(data)) { - this->status_set_error(LOG_STR("Reset failed!")); - this->mark_failed(); - return; + this->status_set_warning(LOG_STR("Sensor not answering reset, will retry")); + return false; } - // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error(LOG_STR("Reset not acknowledged!")); - this->mark_failed(); - return; + this->status_set_warning(LOG_STR("Reset not acknowledged, will retry")); + return false; } delay(10); - // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error(LOG_STR("Failed to read firmware version")); - this->mark_failed(); - return; + this->cancel_interval(RESET_INTERVAL_ID); + this->mark_failed(LOG_STR("Failed to read firmware version")); + return true; } + this->reset_complete_ = true; + this->status_clear_warning(); + this->cancel_interval(RESET_INTERVAL_ID); + return true; } void GDK101Component::dump_config() { @@ -92,12 +117,7 @@ bool GDK101Component::reset_sensor_(uint8_t *data) { // After sending reset command it looks that sensor start performing reset and is unresponsible during read // after a while we can send another reset command and read "0x01" as confirmation // Documentation not going in to such details unfortunately - if (!this->read_bytes_with_retry_(GDK101_REG_RESET, data, 2)) { - ESP_LOGE(TAG, "Updating GDK101 failed!"); - return false; - } - - return true; + return this->read_bytes_with_retry_(GDK101_REG_RESET, data, 2); } bool GDK101Component::read_dose_1m_(uint8_t *data) { diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index abe417e0f93..abe3fd60d83 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -44,12 +44,15 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { protected: bool read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len); + bool try_reset_(); bool reset_sensor_(uint8_t *data); bool read_dose_1m_(uint8_t *data); bool read_dose_10m_(uint8_t *data); bool read_status_(uint8_t *data); bool read_fw_version_(uint8_t *data); bool read_measurement_duration_(uint8_t *data); + bool reset_complete_{false}; + uint8_t reset_retries_remaining_{0}; }; } // namespace gdk101 From 51f3f5c774a708b232296bc41e53151e3801531b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 03:08:28 +0000 Subject: [PATCH 1318/2056] Bump esphome-dashboard from 20260210.0 to 20260408.1 (#15552) 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 31f33ce7ee5..c4b90b5ca94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.2.0 click==8.3.2 -esphome-dashboard==20260210.0 +esphome-dashboard==20260408.1 aioesphomeapi==44.12.0 zeroconf==0.148.0 puremagic==1.30 From 9bf53e0ab83e39e594c9151c0bc617972825098a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:17:58 -0400 Subject: [PATCH 1319/2056] [esp32_hosted] Add SPI transport and SDIO 1-bit bus width support (#15551) --- esphome/components/esp32_hosted/__init__.py | 264 ++++++++++++++---- .../test-sdio-1bit.esp32-p4-idf.yaml | 13 + .../esp32_hosted/test-spi.esp32-p4-idf.yaml | 15 + 3 files changed, 235 insertions(+), 57 deletions(-) create mode 100644 tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 3f9185745dd..1619a845d84 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -4,95 +4,245 @@ from pathlib import Path from esphome import pins from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT +from esphome.const import ( + CONF_CLK_PIN, + CONF_CS_PIN, + CONF_FREQUENCY, + CONF_MISO_PIN, + CONF_MOSI_PIN, + CONF_RESET_PIN, + CONF_TYPE, + CONF_VARIANT, +) from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] CONF_ACTIVE_HIGH = "active_high" +CONF_BUS_WIDTH = "bus_width" CONF_CMD_PIN = "cmd_pin" CONF_D0_PIN = "d0_pin" CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" -CONF_SLOT = "slot" +CONF_DATA_READY_ACTIVE_HIGH = "data_ready_active_high" +CONF_DATA_READY_PIN = "data_ready_pin" +CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high" +CONF_HANDSHAKE_PIN = "handshake_pin" CONF_SDIO_FREQUENCY = "sdio_frequency" +CONF_SLOT = "slot" +CONF_SPI_MODE = "spi_mode" -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), - cv.Required(CONF_ACTIVE_HIGH): cv.boolean, - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), - cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( - cv.frequency, cv.Range(min=400e3, max=50e6) - ), - } - ), +# Shared fields for both transport modes +BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), + cv.Required(CONF_ACTIVE_HIGH): cv.boolean, + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + } +) + +SDIO_SCHEMA = BASE_SCHEMA.extend( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D1_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D2_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D3_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_BUS_WIDTH, default=4): cv.one_of(1, 4, int=True), + cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), + } ) -async def to_code(config): - add_define("USE_ESP32_HOSTED") - if config[CONF_ACTIVE_HIGH]: - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", - True, +def _validate_sdio(config): + if config[CONF_BUS_WIDTH] == 4: + for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN): + if pin not in config: + raise cv.Invalid( + f"{pin} is required when bus_width is 4", + path=[pin], + ) + return config + + +# SPI variant-dependent defaults and limits +_SPI_VARIANT_DEFAULTS = { + "ESP32": {"spi_mode": 2, "frequency": 10, "max_frequency": 10}, + "ESP32C6": {"spi_mode": 3, "frequency": 26, "max_frequency": 40}, +} +_SPI_DEFAULT = {"spi_mode": 3, "frequency": 40, "max_frequency": 40} + +SPI_SCHEMA = BASE_SCHEMA.extend( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_HANDSHAKE_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_DATA_READY_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_SPI_MODE): cv.int_range(min=0, max=3), + cv.Optional(CONF_FREQUENCY): cv.All(cv.frequency, cv.Range(min=1e6, max=40e6)), + cv.Optional(CONF_HANDSHAKE_ACTIVE_HIGH, default=True): cv.boolean, + cv.Optional(CONF_DATA_READY_ACTIVE_HIGH, default=True): cv.boolean, + } +) + + +def _validate_spi(config): + variant = config[CONF_VARIANT] + defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT) + + if CONF_SPI_MODE not in config: + config[CONF_SPI_MODE] = defaults["spi_mode"] + + if CONF_FREQUENCY not in config: + config[CONF_FREQUENCY] = float(defaults["frequency"] * 1e6) + + freq_mhz = int(config[CONF_FREQUENCY] // 1e6) + if freq_mhz > defaults["max_frequency"]: + raise cv.Invalid( + f"SPI frequency {freq_mhz}MHz exceeds maximum {defaults['max_frequency']}MHz for {variant}", + path=[CONF_FREQUENCY], ) + return config + + +CONFIG_SCHEMA = cv.typed_schema( + { + "sdio": cv.All(SDIO_SCHEMA, _validate_sdio), + "spi": cv.All(SPI_SCHEMA, _validate_spi), + }, + default_type="sdio", +) + + +def _configure_sdio(config): + slot = config[CONF_SLOT] + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}", + True, + ) + if config[CONF_BUS_WIDTH] == 1: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS", True) else: - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", - True, - ) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS", True) esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT - config[CONF_RESET_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT - True, - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", - True, - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{slot}", config[CONF_CLK_PIN], ) esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{slot}", config[CONF_CMD_PIN], ) esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{slot}", config[CONF_D0_PIN], ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D1_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D2_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D3_PIN], - ) + if config[CONF_BUS_WIDTH] == 4: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{slot}", + config[CONF_D1_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{slot}", + config[CONF_D2_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{slot}", + config[CONF_D3_PIN], + ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", int(config[CONF_SDIO_FREQUENCY] // 1000), ) + +def _configure_spi(config): + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True) + # SPI mode is set via per-variant choice options + variant = config[CONF_VARIANT] + mode = config[CONF_SPI_MODE] + suffix = "ESP32" if variant == "ESP32" else "ESP32XX" + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SPI_PRIV_MODE_{mode}_{suffix}", + True, + ) + # Frequency is set via per-variant options + freq_mhz = int(config[CONF_FREQUENCY] // 1e6) + if variant == "ESP32": + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32", freq_mhz) + elif variant == "ESP32C6": + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32C6", freq_mhz) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32XX", freq_mhz) + # Pin configuration (use HSPI variant as P4/H2 hosts don't have VSPI) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MOSI", config[CONF_MOSI_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MISO", config[CONF_MISO_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CLK", config[CONF_CLK_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CS", config[CONF_CS_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_GPIO_HANDSHAKE", config[CONF_HANDSHAKE_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_GPIO_DATA_READY", config[CONF_DATA_READY_PIN] + ) + # Handshake and data_ready polarity + if config[CONF_HANDSHAKE_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_HIGH", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_LOW", True) + if config[CONF_DATA_READY_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_HIGH", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True) + + +async def to_code(config): + add_define("USE_ESP32_HOSTED") + transport = config[CONF_TYPE] + transport_prefix = "SDIO" if transport == "sdio" else "SPI" + + # Reset polarity + if config[CONF_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_HIGH", True + ) + else: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_LOW", True + ) + # Reset GPIO + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_GPIO_RESET_SLAVE", # NOLINT + config[CONF_RESET_PIN], + ) + # Slave variant # NOLINT + esp32.add_idf_sdkconfig_option( + f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT + True, + ) + + # Transport-specific configuration + if transport == "sdio": + _configure_sdio(config) + else: + _configure_spi(config) + + # Library versions idf_ver = esp32.idf_version() os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}" if idf_ver >= cv.Version(5, 5, 0): diff --git a/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml new file mode 100644 index 00000000000..80f166c0575 --- /dev/null +++ b/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml @@ -0,0 +1,13 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + bus_width: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml new file mode 100644 index 00000000000..a4423140de0 --- /dev/null +++ b/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml @@ -0,0 +1,15 @@ +esp32_hosted: + type: spi + variant: ESP32C6 + active_high: true + reset_pin: GPIO15 + handshake_pin: GPIO54 + data_ready_pin: GPIO14 + miso_pin: GPIO10 + mosi_pin: GPIO11 + clk_pin: GPIO9 + cs_pin: GPIO53 + +wifi: + ssid: MySSID + password: password1 From a8b7c7a4ac28cda44ed6499768a4acf9fa67e65e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 02:38:00 -1000 Subject: [PATCH 1320/2056] [core] Add TemplatableFn for 4-byte function-pointer templatable storage (#15545) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../analog_threshold_binary_sensor.h | 4 +- esphome/components/api/user_services.h | 2 +- esphome/components/binary_sensor/filter.h | 12 +- esphome/components/cc1101/__init__.py | 9 +- esphome/components/datetime/__init__.py | 9 +- esphome/components/display/__init__.py | 3 +- .../components/esp32_ble_tracker/__init__.py | 3 +- esphome/components/globals/__init__.py | 2 +- esphome/components/http_request/__init__.py | 6 +- .../components/http_request/http_request.h | 4 +- esphome/components/light/automation.h | 75 +++-- esphome/components/light/automation.py | 38 +-- esphome/components/lightwaverf/__init__.py | 16 +- esphome/components/lightwaverf/lightwaverf.h | 6 +- esphome/components/lvgl/lvgl_esphome.cpp | 2 +- esphome/components/lvgl/lvgl_esphome.h | 4 +- esphome/components/max7219digit/display.py | 9 +- esphome/components/mdns/__init__.py | 7 + esphome/components/mdns/mdns_component.cpp | 12 +- esphome/components/mdns/mdns_component.h | 2 +- esphome/components/mdns/mdns_esp32.cpp | 2 +- esphome/components/mdns/mdns_esp8266.cpp | 2 +- esphome/components/mdns/mdns_libretiny.cpp | 2 +- esphome/components/mdns/mdns_rp2040.cpp | 2 +- esphome/components/number/__init__.py | 8 +- esphome/components/number/automation.h | 4 +- esphome/components/openthread/openthread.cpp | 2 +- esphome/components/remote_base/__init__.py | 9 +- .../components/remote_base/toto_protocol.h | 2 - esphome/components/script/script.h | 2 +- esphome/components/select/__init__.py | 8 +- esphome/components/sensor/automation.h | 4 +- esphome/components/sensor/filter.cpp | 8 +- esphome/components/sensor/filter.h | 26 +- .../speaker/media_player/__init__.py | 3 +- .../components/speaker_source/media_player.py | 3 +- esphome/components/sprinkler/__init__.py | 6 +- esphome/components/sprinkler/automation.h | 4 +- esphome/core/automation.h | 307 +++++++++++++----- esphome/cpp_generator.py | 40 ++- .../components/sensor/bench_sensor_filter.cpp | 4 +- tests/unit_tests/test_cpp_generator.py | 15 +- 42 files changed, 432 insertions(+), 256 deletions(-) diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index dd707681054..55a822b9b09 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina protected: sensor::Sensor *sensor_{nullptr}; - TemplatableValue<float> upper_threshold_{}; - TemplatableValue<float> lower_threshold_{}; + TemplatableFn<float> upper_threshold_{}; + TemplatableFn<float> lower_threshold_{}; bool raw_state_{false}; // Pre-filter state for hysteresis logic }; diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index d1b8a6ef0d0..29eadda927a 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> { protected: APIServer *parent_; - TemplatableValue<bool, Ts...> success_{true}; + TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }}; TemplatableValue<std::string, Ts...> error_message_{""}; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON std::function<void(Ts..., JsonObject)> json_builder_; diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 37c6bf0092c..2e45554f810 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -36,7 +36,7 @@ class TimeoutFilter : public Filter, public Component { template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } protected: - TemplatableValue<uint32_t> timeout_delay_{}; + TemplatableFn<uint32_t> timeout_delay_{}; }; class DelayedOnOffFilter final : public Filter, public Component { @@ -49,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component { template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; } protected: - TemplatableValue<uint32_t> on_delay_{}; - TemplatableValue<uint32_t> off_delay_{}; + TemplatableFn<uint32_t> on_delay_{}; + TemplatableFn<uint32_t> off_delay_{}; }; class DelayedOnFilter : public Filter, public Component { @@ -62,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component { template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue<uint32_t> delay_{}; + TemplatableFn<uint32_t> delay_{}; }; class DelayedOffFilter : public Filter, public Component { @@ -74,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component { template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue<uint32_t> delay_{}; + TemplatableFn<uint32_t> delay_{}; }; class InvertFilter : public Filter { @@ -155,7 +155,7 @@ class SettleFilter : public Filter, public Component { template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: - TemplatableValue<uint32_t> delay_{}; + TemplatableFn<uint32_t> delay_{}; bool steady_{true}; }; diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 27092908621..0feb384ac23 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -423,11 +423,10 @@ def _register_setter_actions(): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) data = config[CONF_VALUE] - if cg.is_template(data): - templ_ = await cg.templatable(data, args, _type) - cg.add(getattr(var, _setter)(templ_)) - else: - cg.add(getattr(var, _setter)(_map[data] if _map else data)) + if _map and not cg.is_template(data): + data = _map[data] + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) return var automation.register_action( diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 90835624bf1..895ac4e243e 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ("month", date_config[CONF_MONTH]), ("year", date_config[CONF_YEAR]), ) - cg.add(action_var.set_date(date_struct)) + template_ = await cg.templatable(date_struct, args, cg.ESPTime) + cg.add(action_var.set_date(template_)) return action_var @@ -236,7 +237,8 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ("minute", time_config[CONF_MINUTE]), ("hour", time_config[CONF_HOUR]), ) - cg.add(action_var.set_time(time_struct)) + template_ = await cg.templatable(time_struct, args, cg.ESPTime) + cg.add(action_var.set_time(template_)) return action_var @@ -271,5 +273,6 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args): ("month", datetime_config[CONF_MONTH]), ("year", datetime_config[CONF_YEAR]), ) - cg.add(action_var.set_datetime(datetime_struct)) + template_ = await cg.templatable(datetime_struct, args, cg.ESPTime) + cg.add(action_var.set_datetime(template_)) return action_var diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 67d76a59d9d..744b5d16c49 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -207,7 +207,8 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cg.add(var.set_page(template_)) else: paren = await cg.get_variable(config[CONF_ID]) - cg.add(var.set_page(paren)) + template_ = await cg.templatable(paren, args, DisplayPagePtr) + cg.add(var.set_page(template_)) return var diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index b9c4c28ccfd..d758b400c4f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code( ): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - cg.add(var.set_continuous(config[CONF_CONTINUOUS])) + template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_) + cg.add(var.set_continuous(template_)) return var diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fe83b1ea7c3..ec6730a41c5 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -109,7 +109,7 @@ async def globals_set_to_code(config, action_id, template_arg, args): template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) templ = await cg.templatable( - config[CONF_VALUE], args, None, to_exp=cg.RawExpression + config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True ) cg.add(var.set_value(templ)) return var diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 416432cfc44..90879c459eb 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -302,11 +302,13 @@ async def http_request_action_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) - cg.add(var.set_method(config[CONF_METHOD])) + template_ = await cg.templatable(config[CONF_METHOD], args, cg.const_char_ptr) + cg.add(var.set_method(template_)) capture_response = config[CONF_CAPTURE_RESPONSE] if capture_response: - cg.add(var.set_capture_response(capture_response)) + template_ = await cg.templatable(capture_response, args, cg.bool_) + cg.add(var.set_capture_response(template_)) cg.add_define("USE_HTTP_REQUEST_RESPONSE") cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 73dbda86943..ae73983bab2 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -457,7 +457,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { #endif void init_request_headers(size_t count) { this->request_headers_.init(count); } - void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) { + void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) { this->request_headers_.push_back({key, value}); } @@ -560,7 +560,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { } } HttpRequestComponent *parent_; - FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{}; + FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{}; std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"}; FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{}; std::function<void(Ts..., JsonObject)> json_func_{nullptr}; diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index a5c9220a237..f6a2ca52d45 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,51 +24,60 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { LightState *state_; }; -/// Compact light control action — each field is a function pointer (nullptr = unset). -/// Codegen wraps constants in stateless lambdas. 72 bytes vs 128 with TemplatableValue. template<typename... Ts> class LightControlAction : public Action<Ts...> { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} -#define LIGHT_CONTROL_FIELDS(X) \ - X(ColorMode, color_mode) \ - X(bool, state) \ - X(uint32_t, transition_length) \ - X(uint32_t, flash_length) \ - X(float, brightness) \ - X(float, color_brightness) \ - X(float, red) \ - X(float, green) \ - X(float, blue) \ - X(float, white) \ - X(float, color_temperature) \ - X(float, cold_white) \ - X(float, warm_white) \ - X(uint32_t, effect) - -#define LIGHT_FIELD_SETTER_(type, name) \ - void set_##name(type (*f)(Ts...)) { this->name##_ = f; } -#define LIGHT_FIELD_APPLY_(type, name) \ - if (this->name##_) \ - call.set_##name(this->name##_(x...)); -#define LIGHT_FIELD_DECL_(type, name) type (*name##_)(Ts...){nullptr}; - - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) + TEMPLATABLE_VALUE(ColorMode, color_mode) + TEMPLATABLE_VALUE(bool, state) + TEMPLATABLE_VALUE(uint32_t, transition_length) + TEMPLATABLE_VALUE(uint32_t, flash_length) + TEMPLATABLE_VALUE(float, brightness) + TEMPLATABLE_VALUE(float, color_brightness) + TEMPLATABLE_VALUE(float, red) + TEMPLATABLE_VALUE(float, green) + TEMPLATABLE_VALUE(float, blue) + TEMPLATABLE_VALUE(float, white) + TEMPLATABLE_VALUE(float, color_temperature) + TEMPLATABLE_VALUE(float, cold_white) + TEMPLATABLE_VALUE(float, warm_white) + TEMPLATABLE_VALUE(uint32_t, effect) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) + if (this->color_mode_.has_value()) + call.set_color_mode(this->color_mode_.value(x...)); + if (this->state_.has_value()) + call.set_state(this->state_.value(x...)); + if (this->transition_length_.has_value()) + call.set_transition_length(this->transition_length_.value(x...)); + if (this->flash_length_.has_value()) + call.set_flash_length(this->flash_length_.value(x...)); + if (this->brightness_.has_value()) + call.set_brightness(this->brightness_.value(x...)); + if (this->color_brightness_.has_value()) + call.set_color_brightness(this->color_brightness_.value(x...)); + if (this->red_.has_value()) + call.set_red(this->red_.value(x...)); + if (this->green_.has_value()) + call.set_green(this->green_.value(x...)); + if (this->blue_.has_value()) + call.set_blue(this->blue_.value(x...)); + if (this->white_.has_value()) + call.set_white(this->white_.value(x...)); + if (this->color_temperature_.has_value()) + call.set_color_temperature(this->color_temperature_.value(x...)); + if (this->cold_white_.has_value()) + call.set_cold_white(this->cold_white_.value(x...)); + if (this->warm_white_.has_value()) + call.set_warm_white(this->warm_white_.value(x...)); + if (this->effect_.has_value()) + call.set_effect(this->effect_.value(x...)); call.perform(); } protected: LightState *parent_; - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) - -#undef LIGHT_FIELD_DECL_ -#undef LIGHT_FIELD_APPLY_ -#undef LIGHT_FIELD_SETTER_ -#undef LIGHT_CONTROL_FIELDS }; template<typename... Ts> class DimRelativeAction : public Action<Ts...> { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 365a64584c4..2400822b312 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -1,5 +1,3 @@ -from typing import Any - from esphome import automation import esphome.codegen as cg from esphome.config import path_context @@ -30,7 +28,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, Lambda from esphome.cpp_generator import LambdaExpression -from esphome.types import ConfigType, SafeExpType +from esphome.types import ConfigType from .types import ( COLOR_MODES, @@ -143,28 +141,6 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( ) -async def _as_lambda( - value: Any, - args: list[tuple[SafeExpType, str]], - output_type: SafeExpType, -) -> LambdaExpression: - """Return a stateless lambda expression for a templatable value. - - If value is already a lambda, process it normally. Otherwise wrap - the constant in a ``[](...) -> T { return <value>; }`` expression - so that LightControlAction can store every field as a plain - function pointer. - """ - if cg.is_template(value): - return await cg.process_lambda(value, args, return_type=output_type) - return LambdaExpression( - f"return {cg.safe_exp(value)};", - args, - capture="", - return_type=output_type, - ) - - def _resolve_effect_index(config: ConfigType) -> int: """Resolve a static effect name to its 1-based index at codegen time. @@ -222,9 +198,8 @@ async def light_control_to_code(config, action_id, template_arg, args): ) for conf_key, setter, type_ in FIELDS: if conf_key in config: - cg.add( - getattr(var, setter)(await _as_lambda(config[conf_key], args, type_)) - ) + template_ = await cg.templatable(config[conf_key], args, type_) + cg.add(getattr(var, setter)(template_)) if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): @@ -248,11 +223,10 @@ async def light_control_to_code(config, action_id, template_arg, args): cg.add(var.set_effect(wrapper)) else: # Static string — resolve effect name to index at codegen time - cg.add( - var.set_effect( - await _as_lambda(_resolve_effect_index(config), args, cg.uint32) - ) + template_ = await cg.templatable( + _resolve_effect_index(config), args, cg.uint32 ) + cg.add(var.set_effect(template_)) return var diff --git a/esphome/components/lightwaverf/__init__.py b/esphome/components/lightwaverf/__init__.py index 46c400cb0e4..76eabc2b712 100644 --- a/esphome/components/lightwaverf/__init__.py +++ b/esphome/components/lightwaverf/__init__.py @@ -61,15 +61,13 @@ async def send_raw_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - repeats = await cg.templatable(config[CONF_REPEAT], args, int) - inverted = await cg.templatable(config[CONF_INVERTED], args, bool) - pulse_length = await cg.templatable(config[CONF_PULSE_LENGTH], args, int) - code = config[CONF_CODE] - - cg.add(var.set_repeats(repeats)) - cg.add(var.set_inverted(inverted)) - cg.add(var.set_pulse_length(pulse_length)) - cg.add(var.set_data(code)) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.int_) + cg.add(var.set_repeat(template_)) + template_ = await cg.templatable(config[CONF_INVERTED], args, cg.int_) + cg.add(var.set_inverted(template_)) + template_ = await cg.templatable(config[CONF_PULSE_LENGTH], args, cg.int_) + cg.add(var.set_pulse_length(template_)) + cg.add(var.set_code(config[CONF_CODE])) return var diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index ee4e91e9d1d..6210e6b5d47 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -45,11 +45,7 @@ template<typename... Ts> class SendRawAction : public Action<Ts...> { TEMPLATABLE_VALUE(int, inverted); TEMPLATABLE_VALUE(int, pulse_length); TEMPLATABLE_VALUE(std::vector<uint8_t>, code); - - void set_repeats(const int &data) { repeat_ = data; } - void set_inverted(const int &data) { inverted_ = data; } - void set_pulse_length(const int &data) { pulse_length_ = data; } - void set_data(const std::vector<uint8_t> &data) { code_ = data; } + void set_code(std::initializer_list<uint8_t> data) { this->code_ = std::vector<uint8_t>(data); } void play(const Ts &...x) { int repeats = this->repeat_.value(x...); diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 0c4e7a3425d..ce9b013dcf1 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -412,7 +412,7 @@ void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uin lv_display_flush_ready(disp_drv); } -IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) { +IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout) : timeout_(timeout) { parent->add_on_idle_callback([this](uint32_t idle_time) { if (!this->is_idle_ && idle_time > this->timeout_.value()) { this->is_idle_ = true; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3433aaa5272..3ba258b1a2b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -284,10 +284,10 @@ class LvglComponent : public PollingComponent { class IdleTrigger : public Trigger<> { public: - explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout); + explicit IdleTrigger(LvglComponent *parent, TemplatableFn<uint32_t> timeout); protected: - TemplatableValue<uint32_t> timeout_; + TemplatableFn<uint32_t> timeout_; bool is_idle_{}; }; diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index eb751b995d1..df2423b0d0e 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -147,7 +147,8 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -166,7 +167,8 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -185,7 +187,8 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 0d535d69709..79d355e8ae9 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( ) from esphome.core import CORE, Lambda, coroutine_with_priority from esphome.coroutine import CoroPriority +from esphome.cpp_generator import LambdaExpression from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -131,6 +132,12 @@ def mdns_service( Returns: A StructInitializer representing a MDNSService struct """ + # Wrap port in a stateless lambda for TemplatableFn storage. + # Can't use cg.templatable() here because this is a sync function. + if not isinstance(port, LambdaExpression): + port = LambdaExpression( + f"return {cg.safe_exp(port)};", [], capture="", return_type=cg.uint16 + ) return cg.StructInitializer( MDNSService, ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")), diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 342a6e6c645..e05373ac5d3 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -57,7 +57,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN auto &service = services.emplace_next(); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.proto = MDNS_STR(SERVICE_TCP); - service.port = api::global_api_server->get_port(); + service.port = []() -> uint16_t { return api::global_api_server->get_port(); }; const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); @@ -151,7 +151,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN auto &prom_service = services.emplace_next(); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.proto = MDNS_STR(SERVICE_TCP); - prom_service.port = USE_WEBSERVER_PORT; + prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; }; #endif #ifdef USE_SENDSPIN @@ -162,7 +162,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN auto &sendspin_service = services.emplace_next(); sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN); sendspin_service.proto = MDNS_STR(SERVICE_TCP); - sendspin_service.port = USE_SENDSPIN_PORT; + sendspin_service.port = []() -> uint16_t { return USE_SENDSPIN_PORT; }; sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}}; #endif @@ -172,7 +172,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN auto &web_service = services.emplace_next(); web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.proto = MDNS_STR(SERVICE_TCP); - web_service.port = USE_WEBSERVER_PORT; + web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; }; #endif #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \ @@ -185,7 +185,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN auto &fallback_service = services.emplace_next(); fallback_service.service_type = MDNS_STR(SERVICE_HTTP); fallback_service.proto = MDNS_STR(SERVICE_TCP); - fallback_service.port = USE_WEBSERVER_PORT; + fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; }; fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}}; #endif } @@ -199,7 +199,7 @@ void MDNSComponent::dump_config() { ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), - const_cast<TemplatableValue<uint16_t> &>(service.port).value()); + service.port.value()); for (const auto &record : service.txt_records) { ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 47cad4bf71b..adf88a9cf16 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -36,7 +36,7 @@ struct MDNSService { // second label indicating protocol _including_ underscore character prefix // as defined in RFC6763 Section 7, like "_tcp" or "_udp" const MDNSString *proto; - TemplatableValue<uint16_t> port; + TemplatableFn<uint16_t> port; FixedVector<MDNSTXTRecord> txt_records; }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 3e997402bc6..17000a2bd76 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -37,7 +37,7 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S txt_records.get()[i].key = MDNS_STR_ARG(record.key); txt_records.get()[i].value = MDNS_STR_ARG(record.value); } - uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); + uint16_t port = service.port.value(); err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, txt_records.get(), service.txt_records.size()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 295a408cbd4..70c614f8d34 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -27,7 +27,7 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER while (progmem_read_byte((const uint8_t *) service_type) == '_') { service_type++; } - uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); + uint16_t port = service.port.value(); MDNS.addService(FPSTR(service_type), FPSTR(proto), port); for (const auto &record : service.txt_records) { MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 986099fa1f7..a543a3809ae 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -27,7 +27,7 @@ static void register_libretiny(MDNSComponent *, StaticVector<MDNSService, MDNS_S while (*service_type == '_') { service_type++; } - uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); + uint16_t port_ = service.port.value(); MDNS.addService(service_type, proto, port_); for (const auto &record : service.txt_records) { MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 88f707afd37..64b603030c8 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -32,7 +32,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV while (*service_type == '_') { service_type++; } - uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); + uint16_t port = service.port.value(); MDNS.addService(service_type, proto, port); for (const auto &record : service.txt_records) { MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 9fbaff68601..c8441002586 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -448,7 +448,11 @@ async def number_to_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + NUMBER_OPERATION_OPTIONS[mode], args, NumberOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index a7cd04f0838..2843aa6bf5e 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -63,8 +63,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component { Number *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue<float, float> min_{NAN}; - TemplatableValue<float, float> max_{NAN}; + TemplatableFn<float, float> min_{[](float) -> float { return NAN; }}; + TemplatableFn<float, float> max_{[](float) -> float { return NAN; }}; }; template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...> { diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 7c9a3083034..21dad4f8670 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -181,7 +181,7 @@ void OpenThreadSrpComponent::setup() { memcpy(string, host_name.c_str(), host_name_len); // Set port - entry->mService.mPort = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); + entry->mService.mPort = service.port.value(); otDnsTxtEntry *txt_entries = reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size())); diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 99eda76f81a..042ac9d46ab 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -2123,7 +2123,8 @@ async def abbwelcome_action(var, config, args): await cg.templatable(config[CONF_MESSAGE_TYPE], args, cg.uint8) ) ) - cg.add(var.set_auto_message_id(CONF_MESSAGE_ID not in config)) + template_ = await cg.templatable(CONF_MESSAGE_ID not in config, args, cg.bool_) + cg.add(var.set_auto_message_id(template_)) if CONF_MESSAGE_ID in config: cg.add( var.set_message_id( @@ -2231,3 +2232,9 @@ async def Toto_action(var, config, args): cg.add(var.set_rc_code_2(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) cg.add(var.set_command(template_)) + # Set toto-specific defaults (only if user didn't configure repeat) + if CONF_REPEAT not in config: + template_ = await cg.templatable(3, args, cg.uint32) + cg.add(var.set_send_times(template_)) + template_ = await cg.templatable(36000, args, cg.uint32) + cg.add(var.set_send_wait(template_)) diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index 6a635b0f7c4..53d453f7e3d 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -35,8 +35,6 @@ template<typename... Ts> class TotoAction : public RemoteTransmitterActionBase<T data.rc_code_1 = this->rc_code_1_.value(x...); data.rc_code_2 = this->rc_code_2_.value(x...); data.command = this->command_.value(x...); - this->set_send_times(this->send_times_.value_or(x..., 3)); - this->set_send_wait(this->send_wait_.value_or(x..., 36000)); TotoProtocol().encode(dst, data); } }; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index cd1a084f16c..a0dffe26bf2 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -211,7 +211,7 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T public: ScriptExecuteAction(Script<As...> *script) : script_(script) {} - using Args = std::tuple<TemplatableValue<As, Ts...>...>; + using Args = std::tuple<TemplatableFn<As, Ts...>...>; template<typename... F> void set_args(F... x) { args_ = Args{x...}; } diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index b2c17f59ac1..8c7c8f00fa1 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -282,7 +282,11 @@ async def select_operation_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + SELECT_OPERATION_OPTIONS[mode], args, SelectOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index b4de712727f..37578f5320d 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -79,8 +79,8 @@ class ValueRangeTrigger : public Trigger<float>, public Component { Sensor *parent_; ESPPreferenceObject rtc_; bool previous_in_range_{false}; - TemplatableValue<float, float> min_{NAN}; - TemplatableValue<float, float> max_{NAN}; + TemplatableFn<float, float> min_{[](float) -> float { return NAN; }}; + TemplatableFn<float, float> max_{[](float) -> float { return NAN; }}; }; template<typename... Ts> class SensorInRangeCondition : public Condition<Ts...> { diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 6a90a5af66b..fbac7d35358 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -213,17 +213,17 @@ optional<float> LambdaFilter::new_value(float value) { } // OffsetFilter -OffsetFilter::OffsetFilter(TemplatableValue<float> offset) : offset_(std::move(offset)) {} +OffsetFilter::OffsetFilter(TemplatableFn<float> offset) : offset_(offset) {} optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); } // MultiplyFilter -MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_(std::move(multiplier)) {} +MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(multiplier) {} optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } // ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations) -bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count) { +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count) { int8_t accuracy = parent->get_accuracy_decimals(); float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); @@ -258,7 +258,7 @@ optional<float> ThrottleFilter::new_value(float value) { } // ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp) -optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, +optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values, size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) { const uint32_t now = App.get_loop_component_start_time(); if (last_input == 0 || now - last_input >= min_time_between_inputs || diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index cb4abd154ad..0dbbc33ab34 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -311,26 +311,26 @@ class StatelessLambdaFilter : public Filter { /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: - explicit OffsetFilter(TemplatableValue<float> offset); + explicit OffsetFilter(TemplatableFn<float> offset); optional<float> new_value(float value) override; protected: - TemplatableValue<float> offset_; + TemplatableFn<float> offset_; }; /// A simple filter that multiplies to each value it receives by `multiplier`. class MultiplyFilter : public Filter { public: - explicit MultiplyFilter(TemplatableValue<float> multiplier); + explicit MultiplyFilter(TemplatableFn<float> multiplier); optional<float> new_value(float value) override; protected: - TemplatableValue<float> multiplier_; + TemplatableFn<float> multiplier_; }; /// Non-template helper for value matching (implementation in filter.cpp) -bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count); +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count); /** Base class for filters that compare sensor values against a fixed list of configured values. * @@ -342,7 +342,7 @@ bool value_list_matches_any(Sensor *parent, float sensor_value, const Templatabl */ template<size_t N> class ValueListFilter : public Filter { protected: - explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) { + explicit ValueListFilter(std::initializer_list<TemplatableFn<float>> values) { init_array_from(this->values_, values); } @@ -351,13 +351,13 @@ template<size_t N> class ValueListFilter : public Filter { return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N); } - std::array<TemplatableValue<float>, N> values_{}; + std::array<TemplatableFn<float>, N> values_{}; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> { public: - explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out) + explicit FilterOutValueFilter(std::initializer_list<TemplatableFn<float>> values_to_filter_out) : ValueListFilter<N>(values_to_filter_out) {} optional<float> new_value(float value) override { @@ -379,14 +379,14 @@ class ThrottleFilter : public Filter { }; /// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp) -optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue<float> *values, +optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values, size_t count, uint32_t &last_input, uint32_t min_time_between_inputs); /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> { public: explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::initializer_list<TemplatableValue<float>> prioritized_values) + std::initializer_list<TemplatableFn<float>> prioritized_values) : ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} optional<float> new_value(float value) override { @@ -430,15 +430,15 @@ class TimeoutFilterLast : public TimeoutFilterBase { // Timeout filter with configured value - evaluates TemplatableValue after timeout class TimeoutFilterConfigured : public TimeoutFilterBase { public: - explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue<float> &new_value) + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableFn<float> &new_value) : TimeoutFilterBase(time_period), value_(new_value) {} optional<float> new_value(float value) override; protected: float get_output_value() override { return this->value_.value(); } - TemplatableValue<float> value_; // 16 bytes (configured output value, can be lambda) - // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead + TemplatableFn<float> value_; // 4 bytes (configured output value, can be lambda) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter, public Component { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b16f882cbad..320e96c8979 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -516,7 +516,8 @@ async def play_on_device_media_media_action(config, action_id, template_arg, arg announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_) - cg.add(var.set_audio_file(media_file)) + template_ = await cg.templatable(media_file, args, audio.AudioFile.operator("ptr")) + cg.add(var.set_audio_file(template_)) cg.add(var.set_announcement(announcement)) cg.add(var.set_enqueue(enqueue)) return var diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index 7f0f776ee5f..70feeac3180 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -312,7 +312,8 @@ async def set_playlist_delay_action_to_code( parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) - cg.add(var.set_pipeline(config[CONF_PIPELINE])) + template_ = await cg.templatable(config[CONF_PIPELINE], args, cg.uint8) + cg.add(var.set_pipeline(template_)) template_ = await cg.templatable(config[CONF_DELAY], args, cg.uint32) cg.add(var.set_delay(template_)) diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index fb2beb5b16f..efa5b0bf150 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -455,7 +455,7 @@ async def sprinkler_set_multiplier_to_code(config, action_id, template_arg, args async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t) cg.add(var.set_valve_number(template_)) template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) cg.add(var.set_valve_run_duration(template_)) @@ -487,7 +487,7 @@ async def sprinkler_set_valve_run_duration_to_code( ): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t) cg.add(var.set_valve_number(template_)) template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) cg.add(var.set_valve_run_duration(template_)) @@ -525,7 +525,7 @@ async def sprinkler_start_full_cycle_to_code(config, action_id, template_arg, ar async def sprinkler_start_single_valve_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.uint8) + template_ = await cg.templatable(config[CONF_VALVE_NUMBER], args, cg.size_t) cg.add(var.set_valve_to_start(template_)) if CONF_RUN_DURATION in config: template_ = await cg.templatable(config[CONF_RUN_DURATION], args, cg.uint32) diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index b3f030805dd..c6fe2e4e022 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -108,7 +108,8 @@ template<typename... Ts> class StartSingleValveAction : public Action<Ts...> { public: explicit StartSingleValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - TEMPLATABLE_VALUE(size_t, valve_to_start) + // TemplatableValue (not TemplatableFn) — also set from C++ with raw values in sprinkler.cpp + template<typename V> void set_valve_to_start(V valve_to_start) { this->valve_to_start_ = valve_to_start; } TEMPLATABLE_VALUE(uint32_t, valve_run_duration) void play(const Ts &...x) override { @@ -118,6 +119,7 @@ template<typename... Ts> class StartSingleValveAction : public Action<Ts...> { protected: Sprinkler *sprinkler_; + TemplatableValue<size_t, Ts...> valve_to_start_{}; }; template<typename... Ts> class ShutdownAction : public Action<Ts...> { diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 05c7f195885..eb270bfee26 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -34,70 +34,236 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; }; #endif // NOLINTEND(readability-identifier-naming) +/// Function-pointer-only templatable storage (4 bytes on 32-bit). +/// Used by the TEMPLATABLE_VALUE macro for codegen-managed fields. +/// Codegen wraps constants in stateless lambdas so only a function pointer is needed. +template<typename T, typename... X> class TemplatableFn { + public: + TemplatableFn() = default; + TemplatableFn(std::nullptr_t) = delete; + + // Exact return type match — direct function pointer storage + template<typename F> TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {} + + // Convertible return type (e.g., int -> uint8_t) — casting trampoline. + // Stateless lambdas are default-constructible in C++20, so F{} recreates the lambda inside + // the trampoline without capturing. This compiles to the same code as a direct call + cast. + // Deprecated: codegen should use the correct output type to avoid the trampoline. + template<typename F> + [[deprecated("Lambda return type does not match TemplatableFn<T> — use the correct type in " + "codegen")]] TemplatableFn(F) requires(!std::convertible_to<F, T (*)(X...)>) && + std::invocable<F, X...> &&std::convertible_to<std::invoke_result_t<F, X...>, T> &&std::is_empty_v<F> + &&std::default_initializable<F> : f_([](X... x) -> T { return static_cast<T>(F{}(x...)); }) {} + + // Reject any callable that didn't match the above (stateful lambdas or inconvertible return types) + template<typename F> + TemplatableFn(F) requires std::invocable<F, X...> && + (!std::convertible_to<F, T (*)(X...)>) &&(!std::is_empty_v<F> || + !std::convertible_to<std::invoke_result_t<F, X...>, T> || + !std::default_initializable<F>) = delete; + + bool has_value() const { return this->f_ != nullptr; } + + T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; } + + optional<T> optional_value(X... x) const { + if (!this->f_) + return {}; + return this->f_(x...); + } + + T value_or(X... x, T default_value) const { return this->f_ ? this->f_(x...) : default_value; } + + protected: + T (*f_)(X...){nullptr}; +}; + +// Forward declaration for TemplatableValue (string specialization needs it) +template<typename T, typename... X> class TemplatableValue; + +/// Selects TemplatableFn (4 bytes) for trivially copyable types, TemplatableValue (8 bytes) otherwise. +/// Non-trivial types (std::string, std::vector<uint8_t>, etc.) need TemplatableValue for raw value +/// storage, PROGMEM/FlashStringHelper support (strings), and proper copy/move/destruction. +template<typename T, typename... X> +using TemplatableStorage = + std::conditional_t<std::is_trivially_copyable_v<T>, TemplatableFn<T, X...>, TemplatableValue<T, X...>>; + #define TEMPLATABLE_VALUE_(type, name) \ protected: \ - TemplatableValue<type, Ts...> name##_{}; \ + TemplatableStorage<type, Ts...> name##_{}; \ \ public: \ template<typename V> void set_##name(V name) { this->name##_ = name; } #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) +/// Primary TemplatableValue: stores either a constant value or a function pointer. +/// No std::function, no string-specific paths. 8 bytes on 32-bit. +/// Accepts raw constants for backward compatibility with direct C++ usage. template<typename T, typename... X> class TemplatableValue { - // For std::string, store pointer to heap-allocated string to keep union pointer-sized. - // For other types, store value inline. - static constexpr bool USE_HEAP_STORAGE = std::same_as<T, std::string>; + public: + TemplatableValue() = default; + TemplatableValue(std::nullptr_t) = delete; + // Accept raw constants + template<typename V> TemplatableValue(V value) requires(!std::invocable<V, X...>) : tag_(VALUE) { + new (&this->storage_.value_) T(static_cast<T>(std::move(value))); + } + + // Accept stateless lambdas (convertible to function pointer) + template<typename F> TemplatableValue(F f) requires std::convertible_to<F, T (*)(X...)> : tag_(FN) { + this->storage_.f_ = f; + } + + // Convertible return type (e.g., int -> uint8_t) — casting trampoline + template<typename F> + [[deprecated("Lambda return type does not match TemplatableValue<T> — use the correct type in " + "codegen")]] TemplatableValue(F) requires(!std::convertible_to<F, T (*)(X...)>) && + std::invocable<F, X...> &&std::convertible_to<std::invoke_result_t<F, X...>, T> &&std::is_empty_v<F> + &&std::default_initializable<F> : tag_(FN) { + this->storage_.f_ = [](X... x) -> T { return static_cast<T>(F{}(x...)); }; + } + + // Reject any callable that didn't match the above + template<typename F> + TemplatableValue(F) requires std::invocable<F, X...> && + (!std::convertible_to<F, T (*)(X...)>) &&(!std::is_empty_v<F> || + !std::convertible_to<std::invoke_result_t<F, X...>, T> || + !std::default_initializable<F>) = delete; + + TemplatableValue(const TemplatableValue &other) : tag_(other.tag_) { + if (this->tag_ == VALUE) { + new (&this->storage_.value_) T(other.storage_.value_); + } else if (this->tag_ == FN) { + this->storage_.f_ = other.storage_.f_; + } + } + + TemplatableValue(TemplatableValue &&other) noexcept : tag_(other.tag_) { + if (this->tag_ == VALUE) { + new (&this->storage_.value_) T(std::move(other.storage_.value_)); + other.destroy_(); + } else if (this->tag_ == FN) { + this->storage_.f_ = other.storage_.f_; + } + other.tag_ = NONE; + } + + TemplatableValue &operator=(const TemplatableValue &other) { + if (this != &other) { + this->destroy_(); + this->tag_ = other.tag_; + if (this->tag_ == VALUE) { + new (&this->storage_.value_) T(other.storage_.value_); + } else if (this->tag_ == FN) { + this->storage_.f_ = other.storage_.f_; + } + } + return *this; + } + + TemplatableValue &operator=(TemplatableValue &&other) noexcept { + if (this != &other) { + this->destroy_(); + this->tag_ = other.tag_; + if (this->tag_ == VALUE) { + new (&this->storage_.value_) T(std::move(other.storage_.value_)); + other.destroy_(); + } else if (this->tag_ == FN) { + this->storage_.f_ = other.storage_.f_; + } + other.tag_ = NONE; + } + return *this; + } + + ~TemplatableValue() { this->destroy_(); } + + bool has_value() const { return this->tag_ != NONE; } + + T value(X... x) const { + if (this->tag_ == FN) + return this->storage_.f_(x...); + if (this->tag_ == VALUE) + return this->storage_.value_; + return T{}; + } + + optional<T> optional_value(X... x) const { + if (this->tag_ == NONE) + return {}; + return this->value(x...); + } + + T value_or(X... x, T default_value) const { + if (this->tag_ == NONE) + return default_value; + return this->value(x...); + } + + protected: + void destroy_() { + if constexpr (!std::is_trivially_destructible_v<T>) { + if (this->tag_ == VALUE) + this->storage_.value_.~T(); + } + } + + enum Tag : uint8_t { NONE, VALUE, FN } tag_{NONE}; + // Union with explicit ctor/dtor to support non-trivially-constructible/destructible T + // (e.g., std::vector<uint8_t>). Lifetime of value_ is managed externally via + // placement new and destroy_(). + union Storage { + constexpr Storage() : f_(nullptr) {} + constexpr ~Storage() {} + T value_; + T (*f_)(X...); + } storage_; +}; + +/// Specialization for std::string: supports VALUE, STATIC_STRING, FLASH_STRING, +/// stateless lambdas, and stateful lambdas (std::function). +template<typename... X> class TemplatableValue<std::string, X...> { public: TemplatableValue() : type_(NONE) {} - // For const char* when T is std::string: store pointer directly, no heap allocation - // String remains in flash and is only converted to std::string when value() is called - TemplatableValue(const char *str) requires std::same_as<T, std::string> : type_(STATIC_STRING) { - this->static_str_ = str; - } + // For const char*: store pointer directly, no heap allocation. + // String remains in flash and is only converted to std::string when value() is called. + TemplatableValue(const char *str) : type_(STATIC_STRING) { this->static_str_ = str; } #ifdef USE_ESP8266 // On ESP8266, __FlashStringHelper* is a distinct type from const char*. // ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM. - // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions - // to access the PROGMEM pointer safely. - TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) { + // Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions. + TemplatableValue(const __FlashStringHelper *str) : type_(FLASH_STRING) { this->static_str_ = reinterpret_cast<const char *>(str); } #endif template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = new T(std::move(value)); - } else { - new (&this->value_) T(std::move(value)); - } + this->value_ = new std::string(std::move(value)); } // For stateless lambdas (convertible to function pointer): use function pointer template<typename F> - TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, T (*)(X...)> + TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, std::string (*)(X...)> : type_(STATELESS_LAMBDA) { this->stateless_f_ = f; // Implicit conversion to function pointer } // For stateful lambdas (not convertible to function pointer): use std::function template<typename F> - TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) : type_(LAMBDA) { - this->f_ = new std::function<T(X...)>(std::move(f)); + TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, std::string (*)(X...)>) + : type_(LAMBDA) { + this->f_ = new std::function<std::string(X...)>(std::move(f)); } // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = new T(*other.value_); - } else { - new (&this->value_) T(other.value_); - } + this->value_ = new std::string(*other.value_); } else if (this->type_ == LAMBDA) { - this->f_ = new std::function<T(X...)>(*other.f_); + this->f_ = new std::function<std::string(X...)>(*other.f_); } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; } else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) { @@ -108,12 +274,8 @@ template<typename T, typename... X> class TemplatableValue { // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - this->value_ = other.value_; - other.value_ = nullptr; - } else { - new (&this->value_) T(std::move(other.value_)); - } + this->value_ = other.value_; + other.value_ = nullptr; } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; @@ -144,11 +306,7 @@ template<typename T, typename... X> class TemplatableValue { ~TemplatableValue() { if (this->type_ == VALUE) { - if constexpr (USE_HEAP_STORAGE) { - delete this->value_; - } else { - this->value_.~T(); - } + delete this->value_; } else if (this->type_ == LAMBDA) { delete this->f_; } @@ -157,53 +315,40 @@ template<typename T, typename... X> class TemplatableValue { bool has_value() const { return this->type_ != NONE; } - T value(X... x) const { + std::string value(X... x) const { switch (this->type_) { case STATELESS_LAMBDA: return this->stateless_f_(x...); // Direct function pointer call case LAMBDA: return (*this->f_)(x...); // std::function call case VALUE: - if constexpr (USE_HEAP_STORAGE) { - return *this->value_; - } else { - return this->value_; - } + return *this->value_; case STATIC_STRING: - // if constexpr required: code must compile for all T, but STATIC_STRING - // can only be set when T is std::string (enforced by constructor constraint) - if constexpr (std::same_as<T, std::string>) { - return std::string(this->static_str_); - } - __builtin_unreachable(); + return std::string(this->static_str_); #ifdef USE_ESP8266 - case FLASH_STRING: + case FLASH_STRING: { // PROGMEM pointer — must use _P functions to access on ESP8266 - if constexpr (std::same_as<T, std::string>) { - size_t len = strlen_P(this->static_str_); - std::string result(len, '\0'); - memcpy_P(result.data(), this->static_str_, len); - return result; - } - __builtin_unreachable(); + size_t len = strlen_P(this->static_str_); + std::string result(len, '\0'); + memcpy_P(result.data(), this->static_str_, len); + return result; + } #endif case NONE: default: - return T{}; + return {}; } } - optional<T> optional_value(X... x) { - if (!this->has_value()) { + optional<std::string> optional_value(X... x) const { + if (!this->has_value()) return {}; - } return this->value(x...); } - T value_or(X... x, T default_value) { - if (!this->has_value()) { + std::string value_or(X... x, std::string default_value) const { + if (!this->has_value()) return default_value; - } return this->value(x...); } @@ -216,10 +361,10 @@ template<typename T, typename... X> class TemplatableValue { /// The pointer is always directly readable — FLASH_STRING uses a separate type. const char *get_static_string() const { return this->static_str_; } - /// Check if the string value is empty without allocating (for std::string specialization). + /// Check if the string value is empty without allocating. /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation. /// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate. - bool is_empty() const requires std::same_as<T, std::string> { + bool is_empty() const { switch (this->type_) { case NONE: return true; @@ -245,7 +390,7 @@ template<typename T, typename... X> class TemplatableValue { /// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used). /// @param lambda_buf_size Size of the buffer. /// @return StringRef pointing to the string data. - StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> { + StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const { switch (this->type_) { case NONE: return StringRef(); @@ -278,22 +423,20 @@ template<typename T, typename... X> class TemplatableValue { } } - protected : enum : uint8_t { - NONE, - VALUE, - LAMBDA, - STATELESS_LAMBDA, - STATIC_STRING, // For const char* when T is std::string - avoids heap allocation - FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms - } type_; - // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). - // For other types, store value inline as before. - using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>; + protected: + enum : uint8_t { + NONE, + VALUE, + LAMBDA, + STATELESS_LAMBDA, + STATIC_STRING, // For const char* — avoids heap allocation + FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms + } type_; union { - ValueStorage value_; // T for inline storage, T* for heap storage - std::function<T(X...)> *f_; - T (*stateless_f_)(X...); - const char *static_str_; // For STATIC_STRING and FLASH_STRING types + std::string *value_; // Heap-allocated string (VALUE) + std::function<std::string(X...)> *f_; // Heap-allocated std::function (LAMBDA) + std::string (*stateless_f_)(X...); // Function pointer (STATELESS_LAMBDA) + const char *static_str_; // For STATIC_STRING and FLASH_STRING types }; }; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index a8efe96cce3..cf90b878e1f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -819,11 +819,17 @@ async def templatable( args: list[tuple[SafeExpType, str]], output_type: SafeExpType | None, to_exp: Callable | dict = None, + *, + wrap_constant: bool = False, ): """Generate code for a templatable config option. If `value` is a templated value, the lambda expression is returned. - Otherwise the value is returned as-is (optionally process with to_exp). + For std::string output, constants are returned as-is (with PROGMEM wrapping), + using the std::string-specific TemplatableValue specialization. + For all other output types, constants are wrapped in stateless lambdas + so that TemplatableFn-backed macro-generated fields can store them as + function pointers. :param value: The value to process. :param args: The arguments for the lambda expression. @@ -833,20 +839,28 @@ async def templatable( """ if is_template(value): return await process_lambda(value, args, return_type=output_type) - if to_exp is None: + # Late import to avoid circular dependency (cpp_generator <-> cpp_types). + from esphome.cpp_types import std_string + + if to_exp is not None: + value = to_exp[value] if isinstance(to_exp, dict) else to_exp(value) + elif ( + isinstance(value, str) and output_type is not None and output_type is std_string + ): # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. # On other platforms ESPHOME_F() is a no-op returning const char*. - # Lazy import to avoid circular dependency (cpp_generator <-> cpp_types). - # Identity check (is) avoids brittle string comparison. - if isinstance(value, str) and output_type is not None: - from esphome.cpp_types import std_string - - if output_type is std_string: - return FlashStringLiteral(value) - return value - if isinstance(to_exp, dict): - return to_exp[value] - return to_exp(value) + return FlashStringLiteral(value) + # Wrap non-string constants in stateless lambdas so that TemplatableFn + # (used by TEMPLATABLE_VALUE macro) stores them as function pointers. + # wrap_constant=True forces wrapping even with output_type=None (compiler deduces type). + if (output_type is not None or wrap_constant) and output_type is not std_string: + return LambdaExpression( + f"return {safe_exp(value)};", + args, + capture="", + return_type=output_type, + ) + return value class MockObj(Expression): diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp index e4aa3976901..e6dc7835672 100644 --- a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -56,8 +56,8 @@ static void SensorFilter_Chain3(benchmark::State &state) { Sensor sensor; sensor.add_filters({ - new OffsetFilter(1.0f), - new MultiplyFilter(2.0f), + new OffsetFilter([]() -> float { return 1.0f; }), + new MultiplyFilter([]() -> float { return 2.0f; }), new SlidingWindowMovingAverageFilter(5, 1, 1), }); diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index bdc31cdef8e..81ae586e23b 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -669,11 +669,11 @@ async def test_templatable__int_with_std_string() -> None: @pytest.mark.asyncio async def test_templatable__string_with_non_string_output_type() -> None: - """Static string with non-std::string output_type returns raw string.""" + """Static string with non-std::string output_type returns stateless lambda.""" result = await cg.templatable("hello", [], ct.bool_) - assert isinstance(result, str) - assert result == "hello" + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio @@ -684,6 +684,15 @@ async def test_templatable__with_to_exp_callable() -> None: assert result == 84 +@pytest.mark.asyncio +async def test_templatable__with_to_exp_callable_and_output_type() -> None: + """When to_exp is provided with non-string output_type, result is lambda-wrapped.""" + result = await cg.templatable(42, [], ct.int_, to_exp=lambda x: x * 2) + + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" + + @pytest.mark.asyncio async def test_templatable__with_to_exp_dict() -> None: """When to_exp is a dict, value is looked up.""" From a72609e6408e2a2be900eda2f26a5a715861cbb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 02:39:14 -1000 Subject: [PATCH 1321/2056] [yaml] Resolve top-level IncludeFile in load_yaml (#15557) --- esphome/yaml_util.py | 7 ++++++- tests/unit_tests/test_yaml_util.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 520379e51d2..59d851c02e7 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -599,9 +599,14 @@ def _load_yaml_internal(fname: Path) -> Any: listener(fname) try: with fname.open(encoding="utf-8") as f_handle: - return parse_yaml(fname, f_handle) + res = parse_yaml(fname, f_handle) except (UnicodeDecodeError, OSError) as err: raise EsphomeError(f"Error reading file {fname}: {err}") from err + # Top-level !include returns a deferred IncludeFile; resolve it so + # callers always receive the final content. + if isinstance(res, IncludeFile): + res = res.load() + return res def parse_yaml(file_name: Path, file_handle: TextIOWrapper, yaml_loader=None) -> Any: diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 2c01019abdf..bfd60de44d9 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -640,6 +640,18 @@ def test_include_in_list_context() -> None: assert config["values"] == ["alpha", "beta", "gamma"] +def test_top_level_include_resolved_by_load_yaml(tmp_path: Path) -> None: + """load_yaml resolves a top-level !include so callers always get a dict.""" + child = tmp_path / "child.yaml" + child.write_text("key: value\n") + main = tmp_path / "main.yaml" + main.write_text("!include child.yaml\n") + + result = yaml_util.load_yaml(main) + assert isinstance(result, dict) + assert result["key"] == "value" + + def test_include_plain_filename_loads_after_deferred_refactor() -> None: """!include with a plain filename (no $ expressions) still loads correctly. From e1aa92b9831a605c91997953eed3864e879ef943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 08:13:37 -1000 Subject: [PATCH 1322/2056] [rotary_encoder] Fix templatable value type to use cg.int32 (#15567) --- esphome/components/rotary_encoder/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 246db023f45..21239863e45 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -129,6 +129,6 @@ async def to_code(config): async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.int32) cg.add(var.set_value(template_)) return var From b83edf6c175120fbc0319bf58cf9e1a836038c97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 08:57:56 -1000 Subject: [PATCH 1323/2056] [script] Resolve IncludeFile objects in component config merge (#15575) --- script/merge_component_configs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 41bbafcd021..df7ad4a28c9 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -288,6 +288,9 @@ def merge_component_configs( for pkg_name, pkg_value in list(packages_value.items()): if pkg_name in common_bus_packages: continue + # Resolve deferred !include files before checking type + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() if not isinstance(pkg_value, dict): continue # Component-specific package - expand its content into top level @@ -295,6 +298,9 @@ def merge_component_configs( elif isinstance(packages_value, list): # List format - expand all package includes for pkg_value in packages_value: + # Resolve deferred !include files before checking type + if isinstance(pkg_value, yaml_util.IncludeFile): + pkg_value = pkg_value.load() if not isinstance(pkg_value, dict): continue comp_data = merge_config(comp_data, pkg_value) From 869cace2f3ce753bbd1ddd0120aa0a31d838466c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 08:59:49 -1000 Subject: [PATCH 1324/2056] [web_server] Truncate update entity summary to 256 characters (#15570) --- esphome/components/web_server/web_server.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a57a8d26ffd..1daec1786d9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2207,7 +2207,11 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; root[ESPHOME_F("title")] = obj->update_info.title; - root[ESPHOME_F("summary")] = obj->update_info.summary; + // Truncate long changelogs — full text available via release_url + constexpr size_t max_summary_len = 256; + root[ESPHOME_F("summary")] = obj->update_info.summary.size() <= max_summary_len + ? obj->update_info.summary + : obj->update_info.summary.substr(0, max_summary_len); root[ESPHOME_F("release_url")] = obj->update_info.release_url; this->add_sorting_info_(root, obj); } From a2bd83382b585de1b0e19647cfe712d485670191 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:00:59 -1000 Subject: [PATCH 1325/2056] [codegen] Fix templatable uint8 type to use cg.uint8 (#15572) --- esphome/components/ags10/sensor.py | 2 +- esphome/components/aic3204/audio_dac.py | 2 +- esphome/components/grove_tb6612fng/__init__.py | 8 ++++---- esphome/components/htu21d/sensor.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index e94504ff1af..6491d7d810d 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( async def ags10newi2caddress_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - address = await cg.templatable(config[CONF_ADDRESS], args, int) + address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) cg.add(var.set_new_address(address)) return var diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index a644638f696..b478b573a35 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -43,7 +43,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config.get(CONF_MODE), args, int) + template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8) cg.add(var.set_auto_mute_mode(template_)) return var diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 27a47953b34..ae64c049f53 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -78,7 +78,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16) cg.add(var.set_channel(template_channel)) cg.add(var.set_speed(template_speed)) @@ -101,7 +101,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) cg.add(var.set_channel(template_channel)) return var @@ -121,7 +121,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_CHANNEL], args, int) + template_channel = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) cg.add(var.set_channel(template_channel)) return var @@ -175,6 +175,6 @@ async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_channel = await cg.templatable(config[CONF_ADDRESS], args, int) + template_channel = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8) cg.add(var.set_address(template_channel)) return var diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index ed4fb5968a7..942a28475a3 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -98,7 +98,7 @@ async def to_code(config): async def set_heater_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - level_ = await cg.templatable(config[CONF_LEVEL], args, int) + level_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8) cg.add(var.set_level(level_)) return var From 063a8ce666d0cd79d145670a311efdd48aed838a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:03:25 -1000 Subject: [PATCH 1326/2056] [codegen] Fix templatable uint32 type to use cg.uint32 (#15574) --- esphome/components/pulse_counter/sensor.py | 2 +- esphome/components/pulse_meter/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index c09d778eda2..3326745846b 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -160,6 +160,6 @@ async def to_code(config): async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) cg.add(var.set_total_pulses(template_)) return var diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 499b7309c88..ab3dd2a2499 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -110,6 +110,6 @@ async def to_code(config): async def set_total_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, int) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint32) cg.add(var.set_total_pulses(template_)) return var From 0a42a11f1cc8b1c6334dc281604165f09846434b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:10:46 -1000 Subject: [PATCH 1327/2056] [at581x] Fix non-templated frequency/power_consumption constants for TemplatableFn (#15576) --- esphome/components/at581x/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 4923491f0c2..0bd26bbe5f8 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -173,10 +173,9 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): - if cg.is_template(freq): - template_ = await cg.templatable(freq, args, cg.int32) - else: - template_ = int(freq / 1000000) + if not cg.is_template(freq): + freq = int(freq / 1000000) + template_ = await cg.templatable(freq, args, cg.int_) cg.add(var.set_frequency(template_)) if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: @@ -204,10 +203,9 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_stage_gain(template_)) if power := config.get(CONF_POWER_CONSUMPTION): - if cg.is_template(power): - template_ = await cg.templatable(power, args, cg.int32) - else: - template_ = int(power * 1000000) + if not cg.is_template(power): + power = int(power * 1000000) + template_ = await cg.templatable(power, args, cg.int_) cg.add(var.set_power_consumption(template_)) return var From cfa41b34677a682b7464de1607240c4b0aa74f52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:20:16 -1000 Subject: [PATCH 1328/2056] [codegen] Add cg.int8 type and fix templatable int8 types (#15573) --- esphome/codegen.py | 1 + esphome/components/at581x/__init__.py | 2 +- esphome/components/dfrobot_sen0395/__init__.py | 4 ++-- esphome/cpp_types.py | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 30e31353607..a5b5abe4479 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -79,6 +79,7 @@ from esphome.cpp_types import ( # noqa: F401 float_, global_ns, gpio_Flags, + int8, int16, int32, int64, diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 0bd26bbe5f8..acd2bcc608d 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -169,7 +169,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): # Radar configuration if frontend_reset := config.get(CONF_HW_FRONTEND_RESET): - template_ = await cg.templatable(frontend_reset, args, int) + template_ = await cg.templatable(frontend_reset, args, cg.int8) cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 0becaf35430..feb79eeacf6 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -159,7 +159,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args await cg.register_parented(var, config[CONF_ID]) if factory_reset_config := config.get(CONF_FACTORY_RESET): - template_ = await cg.templatable(factory_reset_config, args, int) + template_ = await cg.templatable(factory_reset_config, args, cg.int8) cg.add(var.set_factory_reset(template_)) if CONF_DETECTION_SEGMENTS in config: @@ -200,7 +200,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args template_ = template_.total_milliseconds / 1000 cg.add(var.set_delay_after_disappear(template_)) if CONF_SENSITIVITY in config: - template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int) + template_ = await cg.templatable(config[CONF_SENSITIVITY], args, cg.int8) cg.add(var.set_sensitivity(template_)) return var diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 8dd77de8434..aeaa4480a8c 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -13,6 +13,7 @@ std_string = std_ns.class_("string") std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") std_span = std_ns.class_("span") +int8 = global_ns.namespace("int8_t") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") From 7de060ed554bd0a03c3f0037d6e152a1ab2c176b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:22:24 +1000 Subject: [PATCH 1329/2056] [lvgl] Fix args for lambda in set_rotation action (#15555) --- esphome/components/lvgl/automation.py | 6 +++--- esphome/components/lvgl/widgets/tabview.py | 3 ++- tests/components/lvgl/lvgl-package.yaml | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index b825320a407..977f1af9b49 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -2,6 +2,7 @@ from collections.abc import Callable from typing import Any from esphome import automation +from esphome.automation import StatelessLambdaAction import esphome.codegen as cg from esphome.components.display import validate_rotation import esphome.config_validation as cv @@ -201,7 +202,7 @@ def _validate_rotation(value): @automation.register_action( "lvgl.display.set_rotation", - ObjUpdateAction, + StatelessLambdaAction, cv.maybe_simple_value( LVGL_SCHEMA.extend( { @@ -214,8 +215,7 @@ def _validate_rotation(value): ) async def lvgl_set_rotation(config, action_id, template_arg, args): lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - async with LambdaContext() as context: - add_line_marks(where=action_id) + async with LambdaContext(args, where=action_id) as context: lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 7629b03e9db..108bb38df58 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_BUTTON, CONF_ID, CONF_INDEX, CONF_ITEMS, @@ -73,7 +74,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return CONF_BUTTONMATRIX, TYPE_FLEX + return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON async def to_code(self, w: Widget, config: dict): await w.set_property( diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 967fe515927..d3565c6c59a 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -194,6 +194,7 @@ lvgl: text: "Close" on_click: then: + - lvgl.display.set_rotation: 0 - lvgl.widget.hide: message_box - lvgl.style.update: id: style_test From 019d415bbd80ee99bcaa72293a7a2148545b51b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:37:11 -1000 Subject: [PATCH 1330/2056] [codegen] Fix templatable int type to use cg.int_ (#15571) --- esphome/components/at581x/__init__.py | 4 ++-- esphome/components/ezo_pmp/__init__.py | 4 ++-- esphome/components/fan/__init__.py | 2 +- esphome/components/pmwcs3/sensor.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index acd2bcc608d..94b68db4b33 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -179,7 +179,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_frequency(template_)) if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: - template_ = await cg.templatable(sens_dist, args, int) + template_ = await cg.templatable(sens_dist, args, cg.int_) cg.add(var.set_sensing_distance(template_)) if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): @@ -199,7 +199,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_trigger_keep(template_)) if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: - template_ = await cg.templatable(stage_gain, args, int) + template_ = await cg.templatable(stage_gain, args, cg.int_) cg.add(var.set_stage_gain(template_)) if power := config.get(CONF_POWER_CONSUMPTION): diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index 3de796dd258..0793495e1ac 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -202,7 +202,7 @@ async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) cg.add(var.set_volume(template_)) - template_ = await cg.templatable(config[CONF_DURATION], args, int) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.int_) cg.add(var.set_duration(template_)) return var @@ -236,7 +236,7 @@ async def ezo_pmp_dose_with_constant_flow_rate_to_code( template_ = await cg.templatable(config[CONF_VOLUME_PER_MINUTE], args, cg.double) cg.add(var.set_volume(template_)) - template_ = await cg.templatable(config[CONF_DURATION], args, int) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.int_) cg.add(var.set_duration(template_)) return var diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index df71c6ab3fd..f906d2c945f 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -348,7 +348,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(oscillating, args, bool) cg.add(var.set_oscillating(template_)) if (speed := config.get(CONF_SPEED)) is not None: - template_ = await cg.templatable(speed, args, int) + template_ = await cg.templatable(speed, args, cg.int_) cg.add(var.set_speed(template_)) if (direction := config.get(CONF_DIRECTION)) is not None: template_ = await cg.templatable(direction, args, FanDirection) diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index bb40f3e4995..c0bc54c5baf 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -137,6 +137,6 @@ PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( async def pmwcs3newi2caddress_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) - address = await cg.templatable(config[CONF_ADDRESS], args, int) + address = await cg.templatable(config[CONF_ADDRESS], args, cg.int_) cg.add(var.set_new_address(address)) return var From 62d84db5a401fe5218df2b252993dbada32688fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:38:17 -1000 Subject: [PATCH 1331/2056] Bump CodSpeedHQ/action from 4.13.0 to 4.13.1 (#15577) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06e8189f54b..dddf21f57ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4 + uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation From 5b840c1662014c8cb30f6630923428791ced3dbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:39:12 -1000 Subject: [PATCH 1332/2056] [codegen] Fix templatable bool type to use cg.bool_ (#15569) --- esphome/components/cover/__init__.py | 2 +- esphome/components/fan/__init__.py | 4 ++-- esphome/components/htu21d/sensor.py | 2 +- esphome/components/mqtt/__init__.py | 4 ++-- esphome/components/nextion/binary_sensor/__init__.py | 6 +++--- esphome/components/nextion/sensor/__init__.py | 4 ++-- esphome/components/nextion/switch/__init__.py | 6 +++--- esphome/components/number/__init__.py | 2 +- esphome/components/online_image/__init__.py | 2 +- esphome/components/remote_base/__init__.py | 10 +++++----- esphome/components/remote_transmitter/__init__.py | 2 +- esphome/components/select/__init__.py | 2 +- esphome/components/switch/__init__.py | 2 +- esphome/components/template/binary_sensor/__init__.py | 2 +- esphome/components/template/switch/__init__.py | 2 +- esphome/components/template/water_heater/__init__.py | 4 ++-- esphome/components/valve/__init__.py | 2 +- 17 files changed, 29 insertions(+), 29 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index c330241f4dc..cd82de456d9 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -300,7 +300,7 @@ async def cover_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if (stop := config.get(CONF_STOP)) is not None: - template_ = await cg.templatable(stop, args, bool) + template_ = await cg.templatable(stop, args, cg.bool_) cg.add(var.set_stop(template_)) if (state := config.get(CONF_STATE)) is not None: template_ = await cg.templatable(state, args, float) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index f906d2c945f..ce1e55d36b1 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -345,7 +345,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if (oscillating := config.get(CONF_OSCILLATING)) is not None: - template_ = await cg.templatable(oscillating, args, bool) + template_ = await cg.templatable(oscillating, args, cg.bool_) cg.add(var.set_oscillating(template_)) if (speed := config.get(CONF_SPEED)) is not None: template_ = await cg.templatable(speed, args, cg.int_) @@ -370,7 +370,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): async def fan_cycle_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, bool) + template_ = await cg.templatable(config[CONF_OFF_SPEED_CYCLE], args, cg.bool_) cg.add(var.set_no_off_cycle(template_)) return var diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 942a28475a3..8808dc70f5f 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -118,6 +118,6 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): async def set_heater_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - status_ = await cg.templatable(config[CONF_STATUS], args, bool) + status_ = await cg.templatable(config[CONF_STATUS], args, cg.bool_) cg.add(var.set_status(status_)) return var diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 33a88c49cc7..cb6b9d144fd 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -504,7 +504,7 @@ async def mqtt_publish_action_to_code(config, action_id, template_arg, args): cg.add(var.set_payload(template_)) template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) cg.add(var.set_qos(template_)) - template_ = await cg.templatable(config[CONF_RETAIN], args, bool) + template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_) cg.add(var.set_retain(template_)) return var @@ -537,7 +537,7 @@ async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args cg.add(var.set_payload(lambda_)) template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8) cg.add(var.set_qos(template_)) - template_ = await cg.templatable(config[CONF_RETAIN], args, bool) + template_ = await cg.templatable(config[CONF_RETAIN], args, cg.bool_) cg.add(var.set_retain(template_)) return var diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py index 5b5922887ce..29f5bdaea78 100644 --- a/esphome/components/nextion/binary_sensor/__init__.py +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -76,13 +76,13 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) - template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool) + template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_) cg.add(var.set_publish_state(template_)) - template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool) + template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_) cg.add(var.set_send_to_nextion(template_)) return var diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index 7351d8f1d54..ba551056c63 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -119,10 +119,10 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_STATE], args, float) cg.add(var.set_state(template_)) - template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool) + template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_) cg.add(var.set_publish_state(template_)) - template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool) + template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_) cg.add(var.set_send_to_nextion(template_)) return var diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py index 81e6721d0f9..29749ecab02 100644 --- a/esphome/components/nextion/switch/__init__.py +++ b/esphome/components/nextion/switch/__init__.py @@ -58,13 +58,13 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) - template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool) + template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_) cg.add(var.set_publish_state(template_)) - template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool) + template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_) cg.add(var.set_send_to_nextion(template_)) return var diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index c8441002586..65c131aeabb 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -445,7 +445,7 @@ async def number_to_to_code(config, action_id, template_arg, args): to_ = await cg.templatable(operation, args, NumberOperation) cg.add(var.set_operation(to_)) if (cycle := config.get(CONF_CYCLE)) is not None: - template_ = await cg.templatable(cycle, args, bool) + template_ = await cg.templatable(cycle, args, cg.bool_) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: template_ = await cg.templatable( diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 518d787d8a9..ee4d5abb1cc 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -100,7 +100,7 @@ async def online_image_action_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) if CONF_UPDATE in config: - template_ = await cg.templatable(config[CONF_UPDATE], args, bool) + template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_) cg.add(var.set_update(template_)) return var diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 042ac9d46ab..cbf82e6f440 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -868,7 +868,7 @@ async def keeloq_action(var, config, args): cg.add(var.set_encrypted(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) cg.add(var.set_command(template_)) - template_ = await cg.templatable(config[CONF_LEVEL], args, bool) + template_ = await cg.templatable(config[CONF_LEVEL], args, cg.bool_) cg.add(var.set_vlow(template_)) @@ -1580,7 +1580,7 @@ async def rc_switch_type_a_action(var, config, args): cg.add( var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.std_string)) ) - cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_))) @register_binary_sensor( @@ -1605,7 +1605,7 @@ async def rc_switch_type_b_action(var, config, args): cg.add(var.set_protocol(proto)) cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint8))) cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) - cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_))) @register_binary_sensor( @@ -1638,7 +1638,7 @@ async def rc_switch_type_c_action(var, config, args): ) cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8))) cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8))) - cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_))) @register_binary_sensor( @@ -1663,7 +1663,7 @@ async def rc_switch_type_d_action(var, config, args): cg.add(var.set_protocol(proto)) cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string))) cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8))) - cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_))) @register_trigger("rc_switch", RCSwitchTrigger, RCSwitchData) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 89019e296e5..1163fc86eb8 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -128,7 +128,7 @@ DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( async def digital_write_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_TRANSMITTER_ID]) - template_ = await cg.templatable(config[CONF_VALUE], args, bool) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.bool_) cg.add(var.set_value(template_)) return var diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 8c7c8f00fa1..ba5214e550a 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -279,7 +279,7 @@ async def select_operation_to_code(config, action_id, template_arg, args): op_ = await cg.templatable(operation, args, SelectOperation) cg.add(var.set_operation(op_)) if (cycle := config.get(CONF_CYCLE)) is not None: - template_ = await cg.templatable(cycle, args, bool) + template_ = await cg.templatable(cycle, args, cg.bool_) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: template_ = await cg.templatable( diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 5a63cbfb9f2..9fa4a013ff8 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -196,7 +196,7 @@ SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( async def switch_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) return var diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index e537e1f97cf..8f57df91c51 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -64,6 +64,6 @@ async def to_code(config): async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) return var diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index eb6f0f46de7..ca986365ede 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -85,6 +85,6 @@ async def to_code(config): async def switch_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, bool) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) return var diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 814aa401935..2dbc952bf14 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -158,11 +158,11 @@ async def water_heater_template_publish_to_code( cg.add(var.set_mode(template_)) if CONF_AWAY in config: - template_ = await cg.templatable(config[CONF_AWAY], args, bool) + template_ = await cg.templatable(config[CONF_AWAY], args, cg.bool_) cg.add(var.set_away(template_)) if CONF_IS_ON in config: - template_ = await cg.templatable(config[CONF_IS_ON], args, bool) + template_ = await cg.templatable(config[CONF_IS_ON], args, cg.bool_) cg.add(var.set_is_on(template_)) return var diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 0319ff50e7f..d3b9c14a1f8 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -229,7 +229,7 @@ async def valve_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if stop_config := config.get(CONF_STOP): - template_ = await cg.templatable(stop_config, args, bool) + template_ = await cg.templatable(stop_config, args, cg.bool_) cg.add(var.set_stop(template_)) if state_config := config.get(CONF_STATE): template_ = await cg.templatable(state_config, args, float) From 4a764ae1e388ab713f25c5b982653a004d292a65 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:42:47 -0400 Subject: [PATCH 1333/2056] [spi] Fix IndexError on invalid RP2040 CLK pin (#15562) --- esphome/components/spi/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 931882be8d1..33ccfbb5ee8 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -246,11 +246,15 @@ def validate_hw_pins(spi, index=-1): return clk_pin_no >= 0 if target_platform == PLATFORM_RP2040: - pin_set = ( - list(filter(lambda s: clk_pin_no in s[CONF_CLK_PIN], RP_SPI_PINSETS))[0] - if index == -1 - else RP_SPI_PINSETS[index] - ) + if index == -1: + matches = list( + filter(lambda s: clk_pin_no in s[CONF_CLK_PIN], RP_SPI_PINSETS) + ) + if not matches: + return False + pin_set = matches[0] + else: + pin_set = RP_SPI_PINSETS[index] if pin_set is None: return False if sdo_pin_no not in pin_set[CONF_MOSI_PIN]: From 4b8f99ed102cbe6ef5b53098b455c6c71e2c06e8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:44:19 -0400 Subject: [PATCH 1334/2056] [modbus_controller] Fix output missing address validation and text_sensor division (#15561) --- esphome/components/modbus_controller/output/__init__.py | 9 +++++++++ .../components/modbus_controller/text_sensor/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 1ec4afd9979..27d212d58d2 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -11,6 +11,7 @@ from .. import ( modbus_controller_ns, ) from ..const import ( + CONF_CUSTOM_COMMAND, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, CONF_USE_WRITE_MULTIPLE, @@ -35,6 +36,10 @@ CONFIG_SCHEMA = cv.typed_schema( "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( { cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( + "custom_command is not supported for outputs" + ), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } @@ -42,6 +47,10 @@ CONFIG_SCHEMA = cv.typed_schema( "holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( { cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( + "custom_command is not supported for outputs" + ), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( SENSOR_VALUE_TYPE ), diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 995357143e4..93ecd311686 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -61,7 +61,7 @@ async def to_code(config): response_size = config[CONF_RESPONSE_SIZE] reg_count = config[CONF_REGISTER_COUNT] if reg_count == 0: - reg_count = response_size / 2 + reg_count = response_size // 2 var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], From fb0033947c18ac02cea29976d8155c4e3618d7d6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:45:43 -0400 Subject: [PATCH 1335/2056] [qspi_dbi] Connect _validate to CONFIG_SCHEMA (#15563) --- esphome/components/qspi_dbi/display.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 48d1f6d12e6..595067e94c4 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -154,6 +154,7 @@ CONFIG_SCHEMA = cv.All( upper=True, key=CONF_MODEL, ), + _validate, cv.only_on_esp32, ) From 312dea7ddba55312f356f6907d2c9b5093e0b123 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 09:46:16 -1000 Subject: [PATCH 1336/2056] [json] Fix heap buffer overflow in SerializationBuffer truncation path (#15566) --- esphome/components/json/json_util.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 6c60a04d20e..edcd23f922e 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -140,8 +140,11 @@ SerializationBuffer<> JsonBuilder::serialize() { heap_size *= 2; } // Payload exceeds 5120 bytes - return truncated result - ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", size); - result.set_size_(size); + // heap_size was doubled after the last iteration, so the actual allocated + // buffer capacity is heap_size/2. Clamp to avoid writing past the buffer. + size_t max_content = heap_size / 2 - 1; + ESP_LOGW(TAG, "JSON payload too large, truncated to %zu bytes", max_content); + result.set_size_(max_content); return result; } From 19c8f0ac7a42f96dddeb08a9213763867e82b879 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:46:36 -0400 Subject: [PATCH 1337/2056] [zephyr] Fix user overlay only emitting first property (#15560) --- esphome/components/zephyr/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 348e7a3cf2d..d3cc6b2cf45 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -199,11 +199,14 @@ def zephyr_add_user(key, value): def copy_files(): user = zephyr_data()[KEY_USER] if user: + entries = " ".join( + f"{key} = {', '.join(value)};" for key, value in user.items() + ) zephyr_add_overlay( f""" / {{ zephyr,user {{ - {[f"{key} = {', '.join(value)};" for key, value in user.items()][0]} + {entries} }}; }}; """ From 94f1e48d959029d737c1ce27b098b73f32d9fb17 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 10:09:43 -1000 Subject: [PATCH 1338/2056] [esp32] Preserve crash data across OTA rollback reboots (#15578) --- esphome/components/api/api_connection.h | 1 + esphome/components/esp32/crash_handler.cpp | 11 +++++++++-- esphome/components/esp32/crash_handler.h | 8 +++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2f685b0b8a5..4be5a73e810 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -276,6 +276,7 @@ class APIConnection final : public APIServerConnectionBase { App.schedule_dump_config(); #ifdef USE_ESP32_CRASH_HANDLER esp32::crash_handler_log(); + esp32::crash_handler_clear(); #endif #ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index ecf30d78781..55cf92a703b 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -101,12 +101,19 @@ void crash_handler_read_and_clear() { if (s_raw_crash_data.pseudo_excause > 1) s_raw_crash_data.pseudo_excause = 0; } - // Clear magic regardless so we don't re-report on next normal reboot - s_raw_crash_data.magic = 0; + // Don't clear magic here — crash data must survive OTA rollback reboots. + // Magic is cleared by crash_handler_clear() after an API client receives the data. } bool crash_handler_has_data() { return s_crash_data_valid; } +void crash_handler_clear() { + // Only clear the magic so data doesn't survive the next reboot. + // Keep s_crash_data_valid so crash_handler_log() still works for + // additional API clients connecting during this boot session. + s_raw_crash_data.magic = 0; +} + // Look up the exception cause as a human-readable string. // Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays // not exposed via any public API. diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h index 97a4d4e1162..c5e7d145ece 100644 --- a/esphome/components/esp32/crash_handler.h +++ b/esphome/components/esp32/crash_handler.h @@ -4,12 +4,18 @@ namespace esphome::esp32 { -/// Read crash data from NOINIT memory and clear the magic marker. +/// Read and validate crash data from NOINIT memory. +/// Does not clear the magic marker — call crash_handler_clear() after +/// the data has been delivered to an API client so it survives OTA rollback reboots. void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); +/// Clear the magic marker and mark crash data as consumed. +/// Call after the data has been delivered to an API client. +void crash_handler_clear(); + /// Returns true if crash data was found this boot. bool crash_handler_has_data(); From 2cd92a311b323df121fdb49c1a755c1c464c4960 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:14:18 -0400 Subject: [PATCH 1339/2056] [esp32] Capture both cores' backtraces in crash handler (#15559) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/esp32/crash_handler.cpp | 219 ++++++++++++++------- 1 file changed, 149 insertions(+), 70 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index 55cf92a703b..ed61b619362 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -59,6 +59,59 @@ static inline bool is_return_addr(uint32_t addr) { } #endif +// --- Architecture-specific backtrace helpers --- +// These run from IRAM during panic (no flash access). + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +// Walk Xtensa backtrace from an exception frame, writing PCs to out[]. +// Returns number of entries written. +static uint8_t IRAM_ATTR walk_xtensa_backtrace(XtExcFrame *frame, uint32_t *out, uint8_t max) { + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) frame->pc, + .sp = (uint32_t) frame->a1, + .next_pc = (uint32_t) frame->a0, + .exc_frame = frame, + }; + uint8_t count = 0; + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + out[count++] = first_pc; + } + while (count < max && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) + break; + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + out[count++] = pc; + } + } + return count; +} +#endif + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Capture RISC-V backtrace: MEPC + RA from registers, then stack scan. +// Returns total count; *reg_count receives number of register-sourced entries. +static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *out, uint8_t max, uint8_t *reg_count) { + uint8_t count = 0; + if (is_code_addr(frame->mepc)) { + out[count++] = frame->mepc; + } + if (is_code_addr(frame->ra) && frame->ra != frame->mepc) { + out[count++] = frame->ra; + } + *reg_count = count; + auto *scan_start = (uint32_t *) frame->sp; + for (uint32_t i = 0; i < 64 && count < max; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != frame->mepc && val != frame->ra) { + out[count++] = val; + } + } + return count; +} +#endif + // Raw crash data written by the panic handler wrapper. // Lives in .noinit so it survives software reset but contains garbage after power cycle. // Validated by magic marker. Static linkage since it's only used within this file. @@ -66,7 +119,7 @@ static inline bool is_return_addr(uint32_t addr) { // Magic is second to validate the data. Remaining fields can change between versions. // Version is uint32_t because it would be padded to 4 bytes anyway before the next // uint32_t field, so we use the full width rather than wasting 3 bytes of padding. -static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_DATA_VERSION = 2; struct RawCrashData { uint32_t version; uint32_t magic; @@ -77,6 +130,13 @@ struct RawCrashData { uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) uint32_t backtrace[MAX_BACKTRACE]; uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) + uint8_t crashed_core; +#if SOC_CPU_CORES_NUM > 1 + static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores"); + uint8_t other_backtrace_count; + uint8_t other_reg_frame_count; + uint32_t other_backtrace[MAX_BACKTRACE]; +#endif }; static RawCrashData __attribute__((section(".noinit"))) s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -100,6 +160,14 @@ void crash_handler_read_and_clear() { s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT if (s_raw_crash_data.pseudo_excause > 1) s_raw_crash_data.pseudo_excause = 0; + if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM) + s_raw_crash_data.crashed_core = 0; +#if SOC_CPU_CORES_NUM > 1 + if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count) + s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count; +#endif } // Don't clear magic here — crash data must survive OTA rollback reboots. // Magic is cleared by crash_handler_clear() after an API client receives the data. @@ -219,6 +287,36 @@ static const char *get_exception_type() { return "Unknown"; } +// Log backtrace entries, filtering stack-scanned addresses on RISC-V. +static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) { + uint8_t bt_num = 0; + for (uint8_t i = 0; i < count; i++) { + uint32_t addr = addrs[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= reg_frame_count && !is_return_addr(addr)) + continue; + const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } +} + +// Append backtrace addresses to the addr2line hint buffer. +static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count, + uint8_t reg_frame_count) { + for (uint8_t i = 0; i < count && pos < size - 12; i++) { + uint32_t addr = addrs[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr); + } + return pos; +} + // Intentionally uses separate ESP_LOGE calls per line instead of combining into // one multi-line log message. This ensures each address appears as its own line // on the serial console, making it possible to see partial output if the device @@ -235,33 +333,28 @@ void crash_handler_log() { } else { ESP_LOGE(TAG, " Reason: %s", get_exception_type()); } + ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core); ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); - uint8_t bt_num = 0; - for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { - uint32_t addr = s_raw_crash_data.backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. - if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) - continue; -#endif -#if CONFIG_IDF_TARGET_ARCH_RISCV - const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; -#else - const char *source = "backtrace"; -#endif - ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count); + +#if SOC_CPU_CORES_NUM > 1 + if (s_raw_crash_data.other_backtrace_count > 0) { + int other_core = 1 - s_raw_crash_data.crashed_core; + ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core); + log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count, + s_raw_crash_data.other_reg_frame_count); } +#endif + // Build addr2line hint with all captured addresses for easy copy-paste char hint[256]; int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); - for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { - uint32_t addr = s_raw_crash_data.backtrace[i]; -#if CONFIG_IDF_TARGET_ARCH_RISCV - if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) - continue; + pos = append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, + s_raw_crash_data.reg_frame_count); +#if SOC_CPU_CORES_NUM > 1 + append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace, + s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count); #endif - pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); - } ESP_LOGE(TAG, "%s", hint); } @@ -283,68 +376,54 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { s_raw_crash_data.reg_frame_count = 0; s_raw_crash_data.exception = (uint8_t) info->exception; s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + s_raw_crash_data.crashed_core = (uint8_t) info->core; +#if SOC_CPU_CORES_NUM > 1 + s_raw_crash_data.other_backtrace_count = 0; + s_raw_crash_data.other_reg_frame_count = 0; +#endif #if CONFIG_IDF_TARGET_ARCH_XTENSA // Xtensa: walk the backtrace using the public API if (info->frame != nullptr) { auto *xt_frame = (XtExcFrame *) info->frame; s_raw_crash_data.cause = xt_frame->exccause; - esp_backtrace_frame_t bt_frame = { - .pc = (uint32_t) xt_frame->pc, - .sp = (uint32_t) xt_frame->a1, - .next_pc = (uint32_t) xt_frame->a0, - .exc_frame = xt_frame, - }; - - uint8_t count = 0; - // First frame PC - uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(first_pc)) { - s_raw_crash_data.backtrace[count++] = first_pc; - } - // Walk remaining frames - while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { - if (!esp_backtrace_get_next_frame(&bt_frame)) { - break; - } - uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); - if (is_code_addr(pc)) { - s_raw_crash_data.backtrace[count++] = pc; - } - } - s_raw_crash_data.backtrace_count = count; + s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE); } +#if SOC_CPU_CORES_NUM > 1 + // Capture the other core's backtrace from the global frame array. + // Both cores save their frames to g_exc_frames[] before esp_panic_handler + // is called, so the other core's frame is available here. + if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) { + int other_core = 1 - info->core; + auto *other_frame = (XtExcFrame *) g_exc_frames[other_core]; + if (other_frame != nullptr) { + s_raw_crash_data.other_backtrace_count = + walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE); + } + } +#endif + #elif CONFIG_IDF_TARGET_ARCH_RISCV // RISC-V: capture MEPC + RA, then scan stack for code addresses if (info->frame != nullptr) { auto *rv_frame = (RvExcFrame *) info->frame; s_raw_crash_data.cause = rv_frame->mcause; - uint8_t count = 0; - - // Save MEPC (fault PC) and RA (return address) - if (is_code_addr(rv_frame->mepc)) { - s_raw_crash_data.backtrace[count++] = rv_frame->mepc; - } - if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { - s_raw_crash_data.backtrace[count++] = rv_frame->ra; - } - - // Track how many entries came from registers (MEPC/RA) so we can - // skip return-address validation for them at log time. - s_raw_crash_data.reg_frame_count = count; - - // Scan stack for code addresses — captures broadly during panic, - // filtered by is_return_addr() at log time when flash is accessible. - auto *scan_start = (uint32_t *) rv_frame->sp; - for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { - uint32_t val = scan_start[i]; - if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { - s_raw_crash_data.backtrace[count++] = val; - } - } - s_raw_crash_data.backtrace_count = count; + s_raw_crash_data.backtrace_count = + capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count); } + +#if SOC_CPU_CORES_NUM > 1 + // Capture the other core's backtrace from the global frame array. + if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) { + int other_core = 1 - info->core; + auto *other_frame = (RvExcFrame *) g_exc_frames[other_core]; + if (other_frame != nullptr) { + s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace( + other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count); + } + } +#endif #endif // Write version and magic last — ensures all data is written before we mark it valid From 4a18ef87d7b8e28862b1a0c73be07ca123bf06f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 10:23:36 -1000 Subject: [PATCH 1340/2056] [codegen] Fix templatable float type to use cg.float_ (#15568) --- .../analog_threshold/binary_sensor.py | 6 ++--- esphome/components/audio_adc/__init__.py | 2 +- esphome/components/audio_dac/__init__.py | 2 +- esphome/components/climate/__init__.py | 8 +++---- esphome/components/cover/__init__.py | 6 ++--- .../components/dfrobot_sen0395/__init__.py | 16 +++++++------- esphome/components/esp8266_pwm/output.py | 2 +- esphome/components/integration/sensor.py | 2 +- esphome/components/ledc/output.py | 2 +- esphome/components/libretiny_pwm/output.py | 2 +- esphome/components/light/automation.py | 22 +++++++++---------- esphome/components/media_player/__init__.py | 2 +- esphome/components/nextion/display.py | 2 +- esphome/components/nextion/sensor/__init__.py | 2 +- esphome/components/number/__init__.py | 10 ++++++--- esphome/components/output/__init__.py | 6 ++--- esphome/components/pid/climate.py | 6 ++--- .../components/pipsolar/output/__init__.py | 2 +- esphome/components/rp2040_pwm/output.py | 2 +- esphome/components/sensor/__init__.py | 14 ++++++------ esphome/components/servo/__init__.py | 2 +- esphome/components/speaker/__init__.py | 2 +- esphome/components/template/cover/__init__.py | 6 ++--- .../components/template/sensor/__init__.py | 2 +- esphome/components/template/valve/__init__.py | 4 ++-- .../template/water_heater/__init__.py | 4 ++-- esphome/components/ufire_ec/sensor.py | 4 ++-- esphome/components/ufire_ise/sensor.py | 4 ++-- esphome/components/valve/__init__.py | 4 ++-- 29 files changed, 76 insertions(+), 72 deletions(-) diff --git a/esphome/components/analog_threshold/binary_sensor.py b/esphome/components/analog_threshold/binary_sensor.py index b5f87b9b5c2..8c137277551 100644 --- a/esphome/components/analog_threshold/binary_sensor.py +++ b/esphome/components/analog_threshold/binary_sensor.py @@ -40,10 +40,10 @@ async def to_code(config): cg.add(var.set_sensor(sens)) if isinstance(config[CONF_THRESHOLD], dict): - lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float) - upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float) + lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], cg.float_) + upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], cg.float_) else: - lower = await cg.templatable(config[CONF_THRESHOLD], [], float) + lower = await cg.templatable(config[CONF_THRESHOLD], [], cg.float_) upper = lower cg.add(var.set_upper_threshold(upper)) cg.add(var.set_lower_threshold(lower)) diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 3c9b32e6107..3c3a4988b5b 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -32,7 +32,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, float) + template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_) cg.add(var.set_mic_gain(template_)) return var diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index a950c1967bf..46c277ce510 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -52,7 +52,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config.get(CONF_VOLUME), args, float) + template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_) cg.add(var.set_volume(template_)) return var diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 13dd7aa0075..df77fa5c1c9 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -488,16 +488,16 @@ async def climate_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(mode, args, ClimateMode) cg.add(var.set_mode(template_)) if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None: - template_ = await cg.templatable(target_temp, args, float) + template_ = await cg.templatable(target_temp, args, cg.float_) cg.add(var.set_target_temperature(template_)) if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: - template_ = await cg.templatable(target_temp_low, args, float) + template_ = await cg.templatable(target_temp_low, args, cg.float_) cg.add(var.set_target_temperature_low(template_)) if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: - template_ = await cg.templatable(target_temp_high, args, float) + template_ = await cg.templatable(target_temp_high, args, cg.float_) cg.add(var.set_target_temperature_high(template_)) if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None: - template_ = await cg.templatable(target_humidity, args, float) + template_ = await cg.templatable(target_humidity, args, cg.float_) cg.add(var.set_target_humidity(template_)) if (fan_mode := config.get(CONF_FAN_MODE)) is not None: template_ = await cg.templatable(fan_mode, args, ClimateFanMode) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index cd82de456d9..fdfca55f0f8 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -303,13 +303,13 @@ async def cover_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(stop, args, cg.bool_) cg.add(var.set_stop(template_)) if (state := config.get(CONF_STATE)) is not None: - template_ = await cg.templatable(state, args, float) + template_ = await cg.templatable(state, args, cg.float_) cg.add(var.set_position(template_)) if (position := config.get(CONF_POSITION)) is not None: - template_ = await cg.templatable(position, args, float) + template_ = await cg.templatable(position, args, cg.float_) cg.add(var.set_position(template_)) if (tilt := config.get(CONF_TILT)) is not None: - template_ = await cg.templatable(tilt, args, float) + template_ = await cg.templatable(tilt, args, cg.float_) cg.add(var.set_tilt(template_)) return var diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index feb79eeacf6..943c510279e 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -166,24 +166,24 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args segments = config[CONF_DETECTION_SEGMENTS] if len(segments) >= 2: - template_ = await cg.templatable(segments[0], args, float) + template_ = await cg.templatable(segments[0], args, cg.float_) cg.add(var.set_det_min1(template_)) - template_ = await cg.templatable(segments[1], args, float) + template_ = await cg.templatable(segments[1], args, cg.float_) cg.add(var.set_det_max1(template_)) if len(segments) >= 4: - template_ = await cg.templatable(segments[2], args, float) + template_ = await cg.templatable(segments[2], args, cg.float_) cg.add(var.set_det_min2(template_)) - template_ = await cg.templatable(segments[3], args, float) + template_ = await cg.templatable(segments[3], args, cg.float_) cg.add(var.set_det_max2(template_)) if len(segments) >= 6: - template_ = await cg.templatable(segments[4], args, float) + template_ = await cg.templatable(segments[4], args, cg.float_) cg.add(var.set_det_min3(template_)) - template_ = await cg.templatable(segments[5], args, float) + template_ = await cg.templatable(segments[5], args, cg.float_) cg.add(var.set_det_max3(template_)) if len(segments) >= 8: - template_ = await cg.templatable(segments[6], args, float) + template_ = await cg.templatable(segments[6], args, cg.float_) cg.add(var.set_det_min4(template_)) - template_ = await cg.templatable(segments[7], args, float) + template_ = await cg.templatable(segments[7], args, cg.float_) cg.add(var.set_det_max4(template_)) if CONF_OUTPUT_LATENCY in config: template_ = await cg.templatable( diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index b9b6dcc95a3..f119a6ba9f3 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -62,6 +62,6 @@ async def to_code(config) -> None: async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) cg.add(var.set_frequency(template_)) return var diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index d0aae4201e4..8d784df6720 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -133,6 +133,6 @@ async def sensor_integration_reset_to_code(config, action_id, template_arg, args async def sensor_integration_set_value_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_VALUE], args, float) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_) cg.add(var.set_value(template_)) return var diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 62ff5ad30ac..95df1fba237 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -82,6 +82,6 @@ async def to_code(config): async def ledc_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) cg.add(var.set_frequency(template_)) return var diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index e812b6a8f25..6f71530aaf7 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -43,6 +43,6 @@ async def to_code(config): async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) cg.add(var.set_frequency(template_)) return var diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 2400822b312..46d37239e5e 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -183,18 +183,18 @@ async def light_control_to_code(config, action_id, template_arg, args): # (config_key, setter_name, c++ type) FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), - (CONF_STATE, "set_state", bool), + (CONF_STATE, "set_state", cg.bool_), (CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32), (CONF_FLASH_LENGTH, "set_flash_length", cg.uint32), - (CONF_BRIGHTNESS, "set_brightness", float), - (CONF_COLOR_BRIGHTNESS, "set_color_brightness", float), - (CONF_RED, "set_red", float), - (CONF_GREEN, "set_green", float), - (CONF_BLUE, "set_blue", float), - (CONF_WHITE, "set_white", float), - (CONF_COLOR_TEMPERATURE, "set_color_temperature", float), - (CONF_COLD_WHITE, "set_cold_white", float), - (CONF_WARM_WHITE, "set_warm_white", float), + (CONF_BRIGHTNESS, "set_brightness", cg.float_), + (CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_), + (CONF_RED, "set_red", cg.float_), + (CONF_GREEN, "set_green", cg.float_), + (CONF_BLUE, "set_blue", cg.float_), + (CONF_WHITE, "set_white", cg.float_), + (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), + (CONF_COLD_WHITE, "set_cold_white", cg.float_), + (CONF_WARM_WHITE, "set_warm_white", cg.float_), ) for conf_key, setter, type_ in FIELDS: if conf_key in config: @@ -262,7 +262,7 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( async def light_dim_relative_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float) + templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_) cg.add(var.set_relative_brightness(templ)) if CONF_TRANSITION_LENGTH in config: templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 3c2e9029d63..1c2c4746451 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -305,7 +305,7 @@ _register_state_conditions() async def media_player_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - volume = await cg.templatable(config[CONF_VOLUME], args, float) + volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_) cg.add(var.set_volume(volume)) return var diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 4d42898a10f..dc3d5c6d091 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -148,7 +148,7 @@ async def nextion_set_brightness_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_) cg.add(var.set_brightness(template_)) return var diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index ba551056c63..61cb42e62cb 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -116,7 +116,7 @@ async def sensor_nextion_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, cg.float_) cg.add(var.set_state(template_)) template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 65c131aeabb..f13ccc4c36d 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -257,10 +257,14 @@ async def _build_number_automations(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) if CONF_ABOVE in conf: - template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + template_ = await cg.templatable( + conf[CONF_ABOVE], [(float, "x")], cg.float_ + ) cg.add(trigger.set_min(template_)) if CONF_BELOW in conf: - template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + template_ = await cg.templatable( + conf[CONF_BELOW], [(float, "x")], cg.float_ + ) cg.add(trigger.set_max(template_)) await automation.build_automation(trigger, [(float, "x")], conf) @@ -362,7 +366,7 @@ OPERATION_BASE_SCHEMA = cv.Schema( async def number_set_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, float) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_) cg.add(var.set_value(template_)) return var diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index a4ce2b2d1a2..36798f2d7f0 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -104,7 +104,7 @@ async def output_turn_off_to_code(config, action_id, template_arg, args): async def output_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_LEVEL], args, float) + template_ = await cg.templatable(config[CONF_LEVEL], args, cg.float_) cg.add(var.set_level(template_)) return var @@ -123,7 +123,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args): async def output_set_min_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_MIN_POWER], args, float) + template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_) cg.add(var.set_min_power(template_)) return var @@ -142,7 +142,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args): async def output_set_max_power_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_MAX_POWER], args, float) + template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_) cg.add(var.set_max_power(template_)) return var diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 18e33b80391..3e4ff754c95 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -189,13 +189,13 @@ async def set_control_parameters(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - kp_template_ = await cg.templatable(config[CONF_KP], args, float) + kp_template_ = await cg.templatable(config[CONF_KP], args, cg.float_) cg.add(var.set_kp(kp_template_)) - ki_template_ = await cg.templatable(config[CONF_KI], args, float) + ki_template_ = await cg.templatable(config[CONF_KI], args, cg.float_) cg.add(var.set_ki(ki_template_)) - kd_template_ = await cg.templatable(config[CONF_KD], args, float) + kd_template_ = await cg.templatable(config[CONF_KD], args, cg.float_) cg.add(var.set_kd(kd_template_)) return var diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 4ae8d9d487e..d1ea981589f 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -103,6 +103,6 @@ async def to_code(config): async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, float) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.float_) cg.add(var.set_level(template_)) return var diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index 4ea488a6cd1..ad37926954e 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -47,6 +47,6 @@ async def to_code(config): async def rp2040_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_FREQUENCY], args, float) + template_ = await cg.templatable(config[CONF_FREQUENCY], args, cg.float_) cg.add(var.set_frequency(template_)) return var diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ecf51d54884..b658ff7056d 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -371,13 +371,13 @@ def sensor_schema( @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) async def offset_filter_to_code(config, filter_id): - template_ = await cg.templatable(config, [], float) + template_ = await cg.templatable(config, [], cg.float_) return cg.new_Pvariable(filter_id, template_) @FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_)) async def multiply_filter_to_code(config, filter_id): - template_ = await cg.templatable(config, [], float) + template_ = await cg.templatable(config, [], cg.float_) return cg.new_Pvariable(filter_id, template_) @@ -389,7 +389,7 @@ async def multiply_filter_to_code(config, filter_id): async def filter_out_filter_to_code(config, filter_id): if not isinstance(config, list): config = [config] - template_ = [await cg.templatable(x, [], float) for x in config] + template_ = [await cg.templatable(x, [], cg.float_) for x in config] return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_) @@ -658,7 +658,7 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( async def throttle_with_priority_filter_to_code(config, filter_id): if not isinstance(config[CONF_VALUE], list): config[CONF_VALUE] = [config[CONF_VALUE]] - template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]] + template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]] return cg.new_Pvariable( filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_ ) @@ -713,7 +713,7 @@ async def timeout_filter_to_code(config, filter_id): else: # Use TimeoutFilterConfigured for configured value mode filter_id.type = TimeoutFilterConfigured - template_ = await cg.templatable(config[CONF_VALUE], [], float) + template_ = await cg.templatable(config[CONF_VALUE], [], cg.float_) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var @@ -909,10 +909,10 @@ async def _build_sensor_automations(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await cg.register_component(trigger, conf) if (above := conf.get(CONF_ABOVE)) is not None: - template_ = await cg.templatable(above, [(float, "x")], float) + template_ = await cg.templatable(above, [(float, "x")], cg.float_) cg.add(trigger.set_min(template_)) if (below := conf.get(CONF_BELOW)) is not None: - template_ = await cg.templatable(below, [(float, "x")], float) + template_ = await cg.templatable(below, [(float, "x")], cg.float_) cg.add(trigger.set_max(template_)) await automation.build_automation(trigger, [(float, "x")], conf) diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index a23bb53536a..c2eaefe455b 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -67,7 +67,7 @@ async def to_code(config): async def servo_write_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_LEVEL], args, float) + template_ = await cg.templatable(config[CONF_LEVEL], args, cg.float_) cg.add(var.set_value(template_)) return var diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 8480eebcdb2..98b5abe58cb 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -127,7 +127,7 @@ automation.register_condition( async def speaker_volume_set_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - volume = await cg.templatable(config[CONF_VOLUME], args, float) + volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_) cg.add(var.set_volume(volume)) return var diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index ea4da4e73cc..a30c0af3131 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -130,13 +130,13 @@ async def cover_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, cg.float_) cg.add(var.set_position(template_)) if CONF_POSITION in config: - template_ = await cg.templatable(config[CONF_POSITION], args, float) + template_ = await cg.templatable(config[CONF_POSITION], args, cg.float_) cg.add(var.set_position(template_)) if CONF_TILT in config: - template_ = await cg.templatable(config[CONF_TILT], args, float) + template_ = await cg.templatable(config[CONF_TILT], args, cg.float_) cg.add(var.set_tilt(template_)) if CONF_CURRENT_OPERATION in config: template_ = await cg.templatable( diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index b0f48ade465..0c875bba0fb 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -49,6 +49,6 @@ async def to_code(config): async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_STATE], args, float) + template_ = await cg.templatable(config[CONF_STATE], args, cg.float_) cg.add(var.set_state(template_)) return var diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index 3e8fd816030..a2d0c198805 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -118,10 +118,10 @@ async def valve_template_publish_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) if state_config := config.get(CONF_STATE): - template_ = await cg.templatable(state_config, args, float) + template_ = await cg.templatable(state_config, args, cg.float_) cg.add(var.set_position(template_)) if (position_config := config.get(CONF_POSITION)) is not None: - template_ = await cg.templatable(position_config, args, float) + template_ = await cg.templatable(position_config, args, cg.float_) cg.add(var.set_position(template_)) if current_operation_config := config.get(CONF_CURRENT_OPERATION): template_ = await cg.templatable( diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 2dbc952bf14..7f8a82c916d 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -146,11 +146,11 @@ async def water_heater_template_publish_to_code( await cg.register_parented(var, config[CONF_ID]) if current_temp := config.get(CONF_CURRENT_TEMPERATURE): - template_ = await cg.templatable(current_temp, args, float) + template_ = await cg.templatable(current_temp, args, cg.float_) cg.add(var.set_current_temperature(template_)) if target_temp := config.get(CONF_TARGET_TEMPERATURE): - template_ = await cg.templatable(target_temp, args, float) + template_ = await cg.templatable(target_temp, args, cg.float_) cg.add(var.set_target_temperature(template_)) if mode := config.get(CONF_MODE): diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 10b4ece6141..1d8775ccf0a 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -102,8 +102,8 @@ UFIRE_EC_CALIBRATE_PROBE_SCHEMA = cv.Schema( async def ufire_ec_calibrate_probe_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - solution_ = await cg.templatable(config[CONF_SOLUTION], args, float) - temperature_ = await cg.templatable(config[CONF_TEMPERATURE], args, float) + solution_ = await cg.templatable(config[CONF_SOLUTION], args, cg.float_) + temperature_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) cg.add(var.set_solution(solution_)) cg.add(var.set_temperature(temperature_)) return var diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index a116012d055..23254b2f477 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -96,7 +96,7 @@ UFIRE_ISE_CALIBRATE_PROBE_SCHEMA = cv.Schema( async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + template_ = await cg.templatable(config[CONF_SOLUTION], args, cg.float_) cg.add(var.set_solution(template_)) return var @@ -110,7 +110,7 @@ async def ufire_ise_calibrate_probe_low_to_code(config, action_id, template_arg, async def ufire_ise_calibrate_probe_high_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_SOLUTION], args, float) + template_ = await cg.templatable(config[CONF_SOLUTION], args, cg.float_) cg.add(var.set_solution(template_)) return var diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index d3b9c14a1f8..1930a7ad0c9 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -232,10 +232,10 @@ async def valve_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(stop_config, args, cg.bool_) cg.add(var.set_stop(template_)) if state_config := config.get(CONF_STATE): - template_ = await cg.templatable(state_config, args, float) + template_ = await cg.templatable(state_config, args, cg.float_) cg.add(var.set_position(template_)) if (position_config := config.get(CONF_POSITION)) is not None: - template_ = await cg.templatable(position_config, args, float) + template_ = await cg.templatable(position_config, args, cg.float_) cg.add(var.set_position(template_)) return var From 576d89a82a79a9c8acd688716fef81ee3119172b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 11:05:53 -1000 Subject: [PATCH 1341/2056] [api] Peel first write iteration, inline socket writes, zero-gap batch encoding (#15063) --- esphome/components/api/api_connection.cpp | 64 ++----- esphome/components/api/api_connection.h | 64 ++++++- esphome/components/api/api_frame_helper.cpp | 78 +++++---- esphome/components/api/api_frame_helper.h | 85 ++++++--- .../components/api/api_frame_helper_noise.cpp | 127 +++++++------- .../components/api/api_frame_helper_noise.h | 17 +- .../api/api_frame_helper_plaintext.cpp | 162 ++++++++++-------- .../api/api_frame_helper_plaintext.h | 15 +- esphome/components/api/proto.h | 11 ++ .../components/api/bench_plaintext_frame.cpp | 4 +- 10 files changed, 374 insertions(+), 253 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bfb3ec291c5..17605345fdb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -406,7 +406,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); + return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, @@ -2005,48 +2005,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } -// Encodes a message to the buffer and returns the total number of bytes used, -// including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - if (conn->flags_.log_only_mode) { - auto *proto_msg = static_cast<const ProtoMessage *>(msg); - DumpBuffer dump_buf; - conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); - return 1; - } -#endif - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); - const uint8_t footer_size = conn->helper_->frame_footer_size(); +// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) - return 0; // Doesn't fit - - auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - - size_t to_add; - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller, just clear flag - conn->flags_.batch_first_message = false; - to_add = calculated_size; - } else { - // Batch message second or later - // Reserve for full message, resize to include footer gap + header padding + payload - to_add = total_calculated_size; - } - - shared_buf.resize(shared_buf.size() + to_add); - ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; - encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); - - // Return total size (header + payload + footer) - return static_cast<uint16_t>(total_calculated_size); +// Noinline version for cold paths — single shared copy +uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { + return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -2173,17 +2137,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items "MessageInfo must remain trivially destructible with this placement-new approach"); const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); - const uint8_t frame_overhead = header_padding + footer_size; // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage); size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits<uint16_t>::max(); - // Track where each message's header padding begins in the buffer - // For plaintext: this is where the 6-byte header padding starts - // For noise: this is where the 7-byte header padding starts - // The actual message data follows after the header padding + // Track where each message's header begins in the buffer + // First message: offset 0 (max padding, may have unused leading bytes) + // Subsequent messages: offset points to exact header start (no gaps) uint32_t current_offset = 0; // Process items and encode directly to buffer (up to our limit) @@ -2199,13 +2161,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items } // Message was encoded successfully - // payload_size is header_padding + actual payload size + footer_size - uint16_t proto_payload_size = payload_size - frame_overhead; + // payload_size = header_size + proto_payload_size + footer_size + uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size; // Use placement new to construct MessageInfo in pre-allocated stack array // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); + new (&message_info[items_processed++]) + MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2255,6 +2218,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first) { this->flags_.batch_first_message = batch_first; + this->batch_message_type_ = item.message_type; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 4be5a73e810..f227dbe2de1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -411,16 +411,59 @@ class APIConnection final : public APIServerConnectionBase { // Non-template buffer management for send_message bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); - // Non-template buffer management for batch encoding - static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size); + // Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes. + // ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites. + static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, + const void *msg, APIConnection *conn, + uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + auto *proto_msg = static_cast<const ProtoMessage *>(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + return 1; + } +#endif + const uint8_t footer_size = conn->helper_->frame_footer_size(); - // Thin template wrapper — computes size, delegates buffer work to non-template helper + // First message uses max padding (already in buffer), subsequent use exact header size + size_t to_add; + if (conn->flags_.batch_first_message) { + conn->flags_.batch_first_message = false; + conn->batch_header_size_ = conn->helper_->frame_header_padding(); + to_add = calculated_size; + } else { + conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + to_add = calculated_size + conn->batch_header_size_ + footer_size; + } + + // Check if it fits (using actual header size, not max padding) + uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size; + if (total_calculated_size > remaining_size) + return 0; + + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + shared_buf.resize(shared_buf.size() + to_add); + ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; + encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); + + return total_calculated_size; + } + + // Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages). + // All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion. + static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); + + // Thin template wrapper — uses noinline encode_to_buffer_slow since + // encode_message_to_buffer callers are cold paths (zero-payload control messages). + // Hot paths (state/info) go through fill_and_encode_entity_state/info instead. + // batch_message_type_ is already set by dispatch_message_ before reaching here. template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { if constexpr (T::ESTIMATED_SIZE == 0) { - return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size); + return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size); } else { - return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size); + return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size); } } @@ -735,9 +778,14 @@ class APIConnection final : public APIServerConnectionBase { // 2-byte types immediately after flags_ (no padding between them) uint16_t client_api_version_major_{0}; uint16_t client_api_version_minor_{0}; - // 1-byte type to fill padding + // 1-byte types to fill remaining space before next 4-byte boundary ActiveIterator active_iterator_{ActiveIterator::NONE}; - // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary + uint8_t batch_message_type_{0}; // Current message type during batch encoding + // Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary + + // Actual header size used by encode_to_buffer for the current message. + // Read by process_batch_multi_ to pass into MessageInfo. + uint8_t batch_header_size_{0}; uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } // Message will use 8 more bytes than the minimum size, and typical diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 6d3bd51b586..90353b64026 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -100,10 +100,17 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("UNKNOWN"); } +#ifdef HELPER_LOG_PACKETS +void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) { + LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len); +} +#endif + APIError APIFrameHelper::drain_overflow_and_handle_errors_() { if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) { int err = errno; - if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + if (err != EWOULDBLOCK && err != EAGAIN) { + this->state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", err); return APIError::SOCKET_WRITE_FAILED; } @@ -111,45 +118,58 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() { return APIError::OK; } -// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full. -// Returns OK if all data was sent or successfully queued. -// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED). -APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { +// Single-buffer write path: wraps in iovec and delegates. +APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) { + struct iovec iov = {const_cast<void *>(data), len}; + APIError err = this->write_raw_iov_(&iov, 1, len, sent); #ifdef HELPER_LOG_PACKETS - for (int i = 0; i < iovcnt; i++) { - LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len); - } + // Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent + if (err == APIError::OK) + LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len); #endif + return err; +} - uint16_t skip = 0; - - // Drain any existing backlog first - if (!this->overflow_buf_.empty()) [[unlikely]] { - APIError err = this->drain_overflow_and_handle_errors_(); - if (err != APIError::OK) - return err; - } - - // If backlog is clear, try direct send - if (this->overflow_buf_.empty()) [[likely]] { - ssize_t sent = - (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); - - if (sent == -1) [[unlikely]] { +// Handles partial writes, errors, and overflow buffering. +// Called when the inline fast path couldn't complete the write, +// or directly from cold paths (handshake, error handling). +APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) { + if (sent <= 0) { + if (sent == WRITE_NOT_ATTEMPTED) { + // Cold path: no write attempted yet, drain overflow and try + if (!this->overflow_buf_.empty()) { + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; + } + if (this->overflow_buf_.empty()) { + sent = this->write_iov_to_socket_(iov, iovcnt); + if (sent == static_cast<ssize_t>(total_write_len)) + return APIError::OK; + // Partial write or -1: fall through to error check / enqueue below + } else { + // Overflow backlog remains after drain; skip socket write, enqueue everything + sent = 0; + } + } + // WRITE_FAILED (-1): fast path or retry write returned -1, check errno + if (sent == WRITE_FAILED) { int err = errno; - if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + if (err != EWOULDBLOCK && err != EAGAIN) { + this->state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", err); return APIError::SOCKET_WRITE_FAILED; } - } else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] { - return APIError::OK; - } else { - skip = static_cast<uint16_t>(sent); + sent = 0; // Treat WOULD_BLOCK as zero bytes sent } } + // Full write completed (possible when called directly, not via write_raw_fast_buf_) + if (sent == static_cast<ssize_t>(total_write_len)) + return APIError::OK; + // Queue unsent data into overflow buffer - if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) { + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) { HELPER_LOG("Overflow buffer full, dropping connection"); this->state_ = State::FAILED; return APIError::SOCKET_WRITE_FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 72ccf8aa562..d1215388d2c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -49,12 +49,17 @@ struct ReadPacketBuffer { }; // Packed message info structure to minimize memory usage +// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits. +// The noise wire format encodes types as 16-bit, but the high byte is always 0. +// If message types ever exceed 255, this and encrypt_noise_message_ must be updated. struct MessageInfo { uint16_t offset; // Offset in buffer where message starts uint16_t payload_size; // Size of the message payload uint8_t message_type; // Message type (0-255) + uint8_t header_size; // Actual header size used (avoids recomputation in write path) - MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} + MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr) + : offset(off), payload_size(size), message_type(type), header_size(hdr) {} }; enum class APIError : uint16_t { @@ -161,20 +166,33 @@ class APIFrameHelper { this->nodelay_counter_ = 0; } } - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - // Resize buffer to include footer space if needed (e.g. Noise MAC) - if (frame_footer_size_) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - MessageInfo msg{type, 0, - static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); - } - // Write multiple protobuf messages in a single operation - // messages contains (message_type, offset, length) for each message in the buffer - // The buffer contains all messages with appropriate padding before each + // Write a single protobuf message - the hot path (87-100% of all writes). + // Caller must ensure state is DATA before calling. + virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; + // Write multiple protobuf messages in a single batched operation. + // Caller must ensure state is DATA and messages is not empty. + // messages contains (message_type, offset, length) for each message in the buffer. + // The buffer contains all messages with appropriate padding before each. virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0; - // Get the frame header padding required by this protocol + // Get the maximum frame header padding required by this protocol (worst case) uint8_t frame_header_padding() const { return frame_header_padding_; } + // Get the actual frame header size for a specific message. + // For noise: always returns frame_header_padding_ (fixed 7-byte header). + // For plaintext: computes actual size from varint lengths (3-6 bytes). + // Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC + // footer, plaintext has footer=0). If a protocol with a plaintext footer is ever + // added, this should become a virtual method. + uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const { +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + return this->frame_footer_size_ + ? this->frame_header_padding_ + : static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type)); +#elif defined(USE_API_NOISE) + return this->frame_header_padding_; +#else // USE_API_PLAINTEXT only + return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type)); +#endif + } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read @@ -196,18 +214,41 @@ class APIFrameHelper { // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. APIError drain_overflow_and_handle_errors_(); - // Common implementation for writing raw data to socket - APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); + // Sentinel values for the sent parameter in write_raw_ methods + static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1 + static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet - // Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN). - // Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors. - APIError check_socket_write_err_(int err) { - if (err == EWOULDBLOCK || err == EAGAIN) - return APIError::WOULD_BLOCK; - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; + // Dispatch to write() or writev() based on iovec count + inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) { + return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); } + // Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages) + // These inline the fast path (overflow empty + full write) and tail-call the out-of-line + // slow path only on failure/partial write. + inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) { + if (this->overflow_buf_.empty()) [[likely]] { + ssize_t sent = this->socket_->write(data, len); + if (sent == static_cast<ssize_t>(len)) [[likely]] { +#ifdef HELPER_LOG_PACKETS + this->log_packet_sending_(data, len); +#endif + return APIError::OK; + } + // sent is -1 (WRITE_FAILED) or partial write count + return this->write_raw_buf_(data, len, sent); + } + return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED); + } + // Out-of-line write paths: handle partial writes, errors, overflow buffering + // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write) + APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED); + APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, + ssize_t sent = WRITE_NOT_ATTEMPTED); +#ifdef HELPER_LOG_PACKETS + void log_packet_sending_(const void *data, uint16_t len); +#endif + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr<socket::Socket> socket_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 162f4ef6056..6dba64a7f88 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Convert a noise error code to a readable error @@ -464,65 +457,83 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - APIError aerr = this->check_data_state_(); +// Encrypt a single noise message in place and return the encrypted frame length. +// Returns APIError::OK on success. +APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type, + uint16_t &encrypted_len_out) { + // Write noise header + buf_start[0] = 0x01; // indicator + // buf_start[1], buf_start[2] to be set after encryption + + // Write message header (to be encrypted) + constexpr uint8_t msg_offset = 3; + buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte + buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte + // payload data is already in the buffer starting at offset + 7 + + // Encrypt the message in place + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_); + + int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf); + APIError aerr = + this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); if (aerr != APIError::OK) return aerr; - if (messages.empty()) { - return APIError::OK; - } + // Fill in the encrypted size + buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8); + buf_start[2] = static_cast<uint8_t>(mbuf.size); + encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data + return APIError::OK; +} + +APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); +#endif + + // Resize buffer to include footer space for Noise MAC + if (this->frame_footer_size_) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_); + + uint16_t payload_size = + static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_); + uint8_t *buf_start = buffer.get_buffer()->data(); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len); + if (aerr != APIError::OK) + return aerr; + return this->write_raw_fast_buf_(buf_start, encrypted_len); +} + +APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); + assert(!messages.empty()); +#endif + + // Noise messages are already contiguous in the buffer: + // HEADER_PADDING (7) exactly matches the fixed header size, and + // footer space (16) is consumed by the encryption MAC. uint8_t *buffer_data = buffer.get_buffer()->data(); - - // Stack-allocated iovec array - no heap allocation - StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs; + uint8_t *write_start = buffer_data + messages[0].offset; uint16_t total_write_len = 0; - // We need to encrypt each message in place for (const auto &msg : messages) { - // The buffer already has padding at offset uint8_t *buf_start = buffer_data + msg.offset; - - // Write noise header - buf_start[0] = 0x01; // indicator - // buf_start[1], buf_start[2] to be set after encryption - - // Write message header (to be encrypted) - constexpr uint8_t msg_offset = 3; - buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte - buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte - buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte - buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte - // payload data is already in the buffer starting at offset + 7 - - // Make sure we have space for MAC - // The buffer should already have been sized appropriately - - // Encrypt the message in place - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size, - 4 + msg.payload_size + frame_footer_size_); - - int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - APIError aerr = - handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len); if (aerr != APIError::OK) return aerr; - - // Fill in the encrypted size - buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8); - buf_start[2] = static_cast<uint8_t>(mbuf.size); - - // Add iovec for this encrypted message - size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data - iovs.push_back({buf_start, msg_len}); - total_write_len += msg_len; + total_write_len += encrypted_len; } - // Send all encrypted messages in one writev call - return this->write_raw_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, total_write_len); } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { @@ -531,16 +542,16 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { header[1] = (uint8_t) (len >> 8); header[2] = (uint8_t) len; + if (len == 0) { + return this->write_raw_buf_(header, 3); + } struct iovec iov[2]; iov[0].iov_base = header; iov[0].iov_len = 3; - if (len == 0) { - return this->write_raw_(iov, 1, 3); // Just header - } iov[1].iov_base = const_cast<uint8_t *>(data); iov[1].iov_len = len; - return this->write_raw_(iov, 2, 3 + len); // Header + data + return this->write_raw_iov_(iov, 2, 3 + len); } /** Initiate the data structures for the handshake. @@ -606,7 +617,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { if (aerr != APIError::OK) return aerr; - frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); + this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); HELPER_LOG("Handshake complete!"); noise_handshakestate_free(handshake_); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index f44bde07556..0676eab78d6 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,19 +9,22 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len + APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx) : APIFrameHelper(std::move(socket)), ctx_(ctx) { - // Noise header structure: - // Pos 0: indicator (0x01) - // Pos 1-2: encrypted payload size (16-bit big-endian) - // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) - // Pos 7+: actual payload data - frame_header_padding_ = 7; + frame_header_padding_ = HEADER_PADDING; } ~APINoiseFrameHelper() override; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; protected: @@ -33,6 +36,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError state_action_handshake_write_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); + APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type, + uint16_t &encrypted_len_out); APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const LogString *reason); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 9e669b31ee2..fa611a6e333 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Initialize the frame helper, returns OK if successful. @@ -205,7 +198,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { // Make sure to tell the remote that we don't // understand the indicator byte so it knows // we do not support it. - struct iovec iov[1]; // The \x00 first byte is the marker for plaintext. // // The remote will know how to handle the indicator byte, @@ -220,14 +212,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { "Bad indicator byte"; char msg[INDICATOR_MSG_SIZE]; memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); - iov[0].iov_base = (void *) msg; + this->write_raw_buf_(msg, INDICATOR_MSG_SIZE); #else static const char MSG[] = "\x00" "Bad indicator byte"; - iov[0].iov_base = (void *) MSG; + this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE); #endif - iov[0].iov_len = INDICATOR_MSG_SIZE; - this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); } return aerr; } @@ -237,73 +227,101 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } + +// Encode a 16-bit varint (1-3 bytes) using pre-computed length. +ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) { + if (varint_len >= 2) { + *p++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + if (varint_len == 3) { + *p++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + } + } + *p = static_cast<uint8_t>(value); +} + +// Encode an 8-bit varint (1-2 bytes) using pre-computed length. +ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) { + if (varint_len == 2) { + *p++ = static_cast<uint8_t>(value | 0x80); + *p = static_cast<uint8_t>(value >> 7); + } else { + *p = value; + } +} + +// Write plaintext header into pre-allocated padding before payload. +// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg, +// actual header size for contiguous batch messages). +// Returns the total header length (indicator + varints). +ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size, + uint8_t message_type, uint8_t padding_size) { + uint8_t size_varint_len = ProtoSize::varint16(payload_size); + uint8_t type_varint_len = ProtoSize::varint8(message_type); + uint8_t total_header_len = 1 + size_varint_len + type_varint_len; + + // The header is right-justified within the padding so it sits immediately before payload. + // + // Single/first message (padding_size = HEADER_PADDING = 6): + // Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload + // Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload + // Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload + // + // Batch messages 2+ (padding_size = actual header size, no unused bytes): + // Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload + // Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload +#ifdef ESPHOME_DEBUG_API + assert(padding_size >= total_header_len); +#endif + uint32_t header_offset = padding_size - total_header_len; + + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator + + // Encode varints directly into buffer using pre-computed lengths + encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1); + encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len); + + return total_header_len; +} + +APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); +#endif + + uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING); + uint8_t *buffer_data = buffer.get_buffer()->data(); + uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING); + return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len, + static_cast<uint16_t>(header_len + payload_size)); +} + APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { - APIError aerr = this->check_data_state_(); - if (aerr != APIError::OK) - return aerr; - - if (messages.empty()) { - return APIError::OK; - } - +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); + assert(!messages.empty()); +#endif uint8_t *buffer_data = buffer.get_buffer()->data(); - // Stack-allocated iovec array - no heap allocation - StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs; - uint16_t total_write_len = 0; + // First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes. + // Subsequent messages were encoded with exact header sizes (header_size = actual header len). + // write_plaintext_header right-justifies the header within header_size bytes of padding. + const auto &first = messages[0]; + uint8_t *first_start = buffer_data + first.offset; + uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING); + uint8_t *write_start = first_start + HEADER_PADDING - header_len; + uint16_t total_len = header_len + first.payload_size; - for (const auto &msg : messages) { - // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead - uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE - ? 1 - : (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); - uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; - uint8_t total_header_len = 1 + size_varint_len + type_varint_len; - - // Calculate where to start writing the header - // The header starts at the latest possible position to minimize unused padding - // - // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 - // [0-2] - Unused padding - // [3] - 0x00 indicator byte - // [4] - Payload size varint (1 byte, for sizes 0-127) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 - // [0-1] - Unused padding - // [2] - 0x00 indicator byte - // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 - // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-65535) - // [4-5] - Message type varint (2 bytes, for types 128-16383) - // [6...] - Actual payload data - // - // The message starts at offset + frame_header_padding_ - // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = buffer_data + msg.offset; - uint32_t header_offset = frame_header_padding_ - total_header_len; - - // Write the plaintext header - buf_start[header_offset] = 0x00; // indicator - - // Encode varints directly into buffer - encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); - encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); - - // Add iovec for this message (header + payload) - size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size); - iovs.push_back({buf_start + header_offset, msg_len}); - total_write_len += msg_len; + for (size_t i = 1; i < messages.size(); i++) { + const auto &msg = messages[i]; + header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); + total_len += header_len + msg.payload_size; } - // Send all messages in one writev call - return write_raw_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, total_len); } } // namespace esphome::api diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index f8161c039d3..8314754715f 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,18 +7,21 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint + explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) { - // Plaintext header structure (worst case): - // Pos 0: indicator (0x00) - // Pos 1-3: payload size varint (up to 3 bytes) - // Pos 4-5: message type varint (up to 2 bytes) - // Pos 6+: actual payload data - frame_header_padding_ = 6; + frame_header_padding_ = HEADER_PADDING; } ~APIPlaintextFrameHelper() override = default; APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; protected: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index ff7c5232b66..e0a4e03189e 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -645,6 +645,17 @@ class ProtoSize { static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 + // Varint encoded length for a 16-bit value (1, 2, or 3 bytes). + // Fully inline — no slow path call for values >= 128. + static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) { + return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3); + } + + // Varint encoded length for an 8-bit value (1 or 2 bytes). + static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) { + return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2; + } + /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp index 0caa50c748f..74c640a093c 100644 --- a/tests/benchmarks/components/api/bench_plaintext_frame.cpp +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -75,7 +75,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { buffer.clear(); - MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + MessageInfo messages[5] = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; for (int j = 0; j < 5; j++) { uint16_t offset = buffer.size(); @@ -89,7 +89,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { ProtoWriteBuffer writer(&buffer, offset + padding); msg.encode(writer); - messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size, padding); } helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5)); From d4cce142c5352e94772777dfeaf668fbc1533cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 11:11:31 -1000 Subject: [PATCH 1342/2056] [api] Fix batch messages stuck in Nagle buffer (#15581) --- esphome/components/api/api_connection.cpp | 33 ++++++++++++++++------- esphome/components/api/api_connection.h | 1 + 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 17605345fdb..7db423141c1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -315,6 +315,8 @@ void APIConnection::process_active_iterator_() { this->destroy_active_iterator_(); if (this->flags_.state_subscription) { this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } else { + this->finalize_iterator_sync_(); } } else { this->process_iterator_batch_(this->iterator_storage_.list_entities); @@ -322,21 +324,27 @@ void APIConnection::process_active_iterator_() { } else { // INITIAL_STATE if (this->iterator_storage_.initial_state.completed()) { this->destroy_active_iterator_(); - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); - } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); + this->finalize_iterator_sync_(); } else { this->process_iterator_batch_(this->iterator_storage_.initial_state); } } } +void APIConnection::finalize_iterator_sync_() { + // Flush any remaining batched messages immediately so clients + // receive completion responses (e.g. ListEntitiesDoneResponse) + // without waiting for the batch timer. + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); +} + void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { size_t initial_size = this->deferred_batch_.size(); size_t max_batch = this->get_max_batch_size_(); @@ -2185,6 +2193,13 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items shared_buf.resize(shared_buf.size() + footer_size); } + // Ensure TCP_NODELAY is on before writing batch data. + // Log messages enable Nagle (NODELAY off) to coalesce small packets. + // Without this, batch data written to the socket sits in LWIP's Nagle + // buffer — the remote won't ACK until it sends its own data (e.g. a + // ping), which can take 20+ seconds. + this->helper_->set_nodelay_for_message(false); + // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, std::span<const MessageInfo>(message_info, items_processed)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f227dbe2de1..284c4475deb 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -662,6 +662,7 @@ class APIConnection final : public APIServerConnectionBase { // Helper methods for iterator lifecycle management void destroy_active_iterator_(); void begin_iterator_(ActiveIterator type); + void finalize_iterator_sync_(); #ifdef USE_CAMERA std::unique_ptr<camera::CameraImageReader> image_reader_; #endif From faa05031a71508cc2690d971bf5bbda3224522ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 12:25:29 -1000 Subject: [PATCH 1343/2056] [climate] Store custom mode vectors on Climate entity to eliminate heap allocation (#15206) --- .../bedjet/climate/bedjet_climate.cpp | 9 ++ .../bedjet/climate/bedjet_climate.h | 11 +-- esphome/components/climate/climate.cpp | 21 +++- esphome/components/climate/climate.h | 47 ++++++++- esphome/components/climate/climate_traits.cpp | 27 ++++++ esphome/components/climate/climate_traits.h | 90 ++++++++++++----- esphome/components/demo/demo_climate.h | 18 +++- esphome/components/midea/ac_adapter.cpp | 4 +- esphome/components/midea/air_conditioner.cpp | 27 +++++- esphome/components/midea/air_conditioner.h | 7 +- .../thermostat/thermostat_climate.cpp | 17 ++-- .../legacy_climate_component/__init__.py | 4 + .../climate/__init__.py | 16 ++++ .../climate/legacy_climate.h | 55 +++++++++++ .../fixtures/legacy_climate_compat.yaml | 18 ++++ .../integration/test_legacy_climate_compat.py | 96 +++++++++++++++++++ 16 files changed, 404 insertions(+), 63 deletions(-) create mode 100644 tests/integration/fixtures/external_components/legacy_climate_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/legacy_climate_component/climate/__init__.py create mode 100644 tests/integration/fixtures/external_components/legacy_climate_component/climate/legacy_climate.h create mode 100644 tests/integration/fixtures/legacy_climate_compat.yaml create mode 100644 tests/integration/test_legacy_climate_compat.py diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index a17407f08ff..88ed902a112 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -61,6 +61,15 @@ void BedJetClimate::dump_config() { } void BedJetClimate::setup() { + // Set custom modes once during setup — stored on Climate base class, wired via get_traits() + this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); + this->set_supported_custom_presets({ + this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", + "M1", + "M2", + "M3", + }); + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index 05f4a849e07..d12c2a8255c 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -42,21 +42,14 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli climate::CLIMATE_MODE_DRY, }); - // It would be better if we had a slider for the fan modes. - traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_presets({ // If we support NONE, then have to decide what happens if the user switches to it (turn off?) // climate::CLIMATE_PRESET_NONE, // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. climate::CLIMATE_PRESET_BOOST, }); - // String literals are stored in rodata and valid for program lifetime - traits.set_supported_custom_presets({ - this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", - "M1", - "M2", - "M3", - }); + // Custom fan modes and presets are set once in setup(), stored on Climate base class, + // and wired automatically via get_traits() traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 32cac0961c6..e1324971407 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -484,6 +484,11 @@ void Climate::publish_state() { ClimateTraits Climate::get_traits() { auto traits = this->traits(); + // Wire custom mode pointers from Climate-owned storage + if (this->supported_custom_fan_modes_) + traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_); + if (this->supported_custom_presets_) + traits.set_supported_custom_presets_(this->supported_custom_presets_); #ifdef USE_CLIMATE_VISUAL_OVERRIDES if (!std::isnan(this->visual_min_temperature_override_)) { traits.set_visual_min_temperature(this->visual_min_temperature_override_); @@ -681,9 +686,8 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { } bool Climate::set_custom_fan_mode_(const char *mode, size_t len) { - auto traits = this->get_traits(); - return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, - traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode()); + return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, this->find_custom_fan_mode_(mode, len), + this->has_custom_fan_mode()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } @@ -691,8 +695,7 @@ void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const char *preset, size_t len) { - auto traits = this->get_traits(); - return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len), + return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, this->find_custom_preset_(preset, len), this->has_custom_preset()); } @@ -703,6 +706,10 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) { + if (this->supported_custom_fan_modes_) { + return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len); + } + // Fallback for deprecated path: external components may set modes on ClimateTraits directly return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len); } @@ -711,6 +718,10 @@ const char *Climate::find_custom_preset_(const char *custom_preset) { } const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) { + if (this->supported_custom_presets_) { + return vector_find(*this->supported_custom_presets_, custom_preset, len); + } + // Fallback for deprecated path: external components may set modes on ClimateTraits directly return this->get_traits().find_custom_preset_(custom_preset, len); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 0251365dd8a..04f653a2b0f 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -1,5 +1,6 @@ #pragma once +#include <vector> #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -234,6 +235,28 @@ class Climate : public EntityBase { void set_visual_max_humidity_override(float visual_max_humidity_override); #endif + /// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits). + void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) { + this->ensure_custom_fan_modes_().assign(modes.begin(), modes.end()); + } + void set_supported_custom_fan_modes(const std::vector<const char *> &modes) { + this->ensure_custom_fan_modes_() = modes; + } + template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->ensure_custom_fan_modes_().assign(modes, modes + N); + } + + /// Set the supported custom presets (stored on Climate, referenced by ClimateTraits). + void set_supported_custom_presets(std::initializer_list<const char *> presets) { + this->ensure_custom_presets_().assign(presets.begin(), presets.end()); + } + void set_supported_custom_presets(const std::vector<const char *> &presets) { + this->ensure_custom_presets_() = presets; + } + template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { + this->ensure_custom_presets_().assign(presets, presets + N); + } + /// Check if a custom fan mode is currently active. bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } @@ -336,13 +359,14 @@ class Climate : public EntityBase { * called from publish_state() */ void save_state_(const ClimateTraits &traits); - void save_state_() { this->save_state_(this->traits()); } + void save_state_() { this->save_state_(this->get_traits()); } void dump_traits_(const char *tag); LazyCallbackManager<void(Climate &)> state_callback_{}; LazyCallbackManager<void(ClimateCall &)> control_callback_{}; ESPPreferenceObject rtc_; + #ifdef USE_CLIMATE_VISUAL_OVERRIDES float visual_min_temperature_override_{NAN}; float visual_max_temperature_override_{NAN}; @@ -353,16 +377,33 @@ class Climate : public EntityBase { #endif private: + /// Lazy-allocate custom mode vectors (never freed — entity lives forever). + std::vector<const char *> &ensure_custom_fan_modes_() { + if (!this->supported_custom_fan_modes_) { + this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT + } + return *this->supported_custom_fan_modes_; + } + std::vector<const char *> &ensure_custom_presets_() { + if (!this->supported_custom_presets_) { + this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT + } + return *this->supported_custom_presets_; + } + + std::vector<const char *> *supported_custom_fan_modes_{nullptr}; + std::vector<const char *> *supported_custom_presets_{nullptr}; + /** The active custom fan mode (private - enforces use of safe setters). * - * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. + * Points to an entry in supported_custom_fan_modes_ or nullptr. * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. */ const char *custom_fan_mode_{nullptr}; /** The active custom preset (private - enforces use of safe setters). * - * Points to an entry in traits.supported_custom_presets_ or nullptr. + * Points to an entry in supported_custom_presets_ or nullptr. * Use get_custom_preset() to read, set_custom_preset_() to modify. */ const char *custom_preset_{nullptr}; diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 9bf2d9acd3a..398e25f69e3 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -2,6 +2,33 @@ namespace esphome::climate { +// Compat: shared empty vector for getters when no custom modes are set. +// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed +// and getters can return const vector * instead of const vector &. +static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT + +const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const { + if (this->supported_custom_fan_modes_) { + return *this->supported_custom_fan_modes_; + } + // Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0. + if (!this->compat_custom_fan_modes_.empty()) { + return this->compat_custom_fan_modes_; + } + return EMPTY_CUSTOM_MODES; +} + +const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const { + if (this->supported_custom_presets_) { + return *this->supported_custom_presets_; + } + // Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0. + if (!this->compat_custom_presets_.empty()) { + return this->compat_custom_presets_; + } + return EMPTY_CUSTOM_MODES; +} + int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_target_temperature_step_); } diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 80ef0854d59..082b2127a9f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -147,27 +147,45 @@ class ClimateTraits { void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool get_supports_fan_modes() const { - return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); + if (!this->supported_fan_modes_.empty()) { + return true; + } + // Same precedence as get_supported_custom_fan_modes() getter + if (this->supported_custom_fan_modes_) { + return !this->supported_custom_fan_modes_->empty(); + } + return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0 } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) { - this->supported_custom_fan_modes_ = modes; + // Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector). + this->compat_custom_fan_modes_ = modes; } + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_fan_modes(const std::vector<const char *> &modes) { - this->supported_custom_fan_modes_ = modes; + this->compat_custom_fan_modes_ = modes; } - template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { - this->supported_custom_fan_modes_.assign(modes, modes + N); + // Remove before 2026.11.0 + template<size_t N> + ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") + void set_supported_custom_fan_modes(const char *const (&modes)[N]) { + this->compat_custom_fan_modes_.assign(modes, modes + N); } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete; void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete; - const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } + // Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *. + const std::vector<const char *> &get_supported_custom_fan_modes() const; bool supports_custom_fan_mode(const char *custom_fan_mode) const { - return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); + return (this->supported_custom_fan_modes_ && + vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode)) || + vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0 } bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { return this->supports_custom_fan_mode(custom_fan_mode.c_str()); @@ -179,23 +197,32 @@ class ClimateTraits { bool get_supports_presets() const { return !this->supported_presets_.empty(); } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_presets(std::initializer_list<const char *> presets) { - this->supported_custom_presets_ = presets; + this->compat_custom_presets_ = presets; } + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_custom_presets(const std::vector<const char *> &presets) { - this->supported_custom_presets_ = presets; + this->compat_custom_presets_ = presets; } - template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { - this->supported_custom_presets_.assign(presets, presets + N); + // Remove before 2026.11.0 + template<size_t N> + ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0") + void set_supported_custom_presets(const char *const (&presets)[N]) { + this->compat_custom_presets_.assign(presets, presets + N); } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_supported_custom_presets(const std::vector<std::string> &presets) = delete; void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete; - const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; } + // Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *. + const std::vector<const char *> &get_supported_custom_presets() const; bool supports_custom_preset(const char *custom_preset) const { - return vector_contains(this->supported_custom_presets_, custom_preset); + return (this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset)) || + vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0 } bool supports_custom_preset(const std::string &custom_preset) const { return this->supports_custom_preset(custom_preset.c_str()); @@ -258,13 +285,25 @@ class ClimateTraits { } } + /// Set custom mode pointers (only Climate::get_traits() should call these). + void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) { + this->supported_custom_fan_modes_ = modes; + } + void set_supported_custom_presets_(const std::vector<const char *> *presets) { + this->supported_custom_presets_ = presets; + } + /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead const char *find_custom_fan_mode_(const char *custom_fan_mode) const { return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode)); } const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const { - return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len); + if (this->supported_custom_fan_modes_) { + return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len); + } + // Compat: check owned vector from deprecated setters. Remove in 2026.11.0. + return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len); } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found @@ -273,7 +312,11 @@ class ClimateTraits { return this->find_custom_preset_(custom_preset, strlen(custom_preset)); } const char *find_custom_preset_(const char *custom_preset, size_t len) const { - return vector_find(this->supported_custom_presets_, custom_preset, len); + if (this->supported_custom_presets_) { + return vector_find(*this->supported_custom_presets_, custom_preset, len); + } + // Compat: check owned vector from deprecated setters. Remove in 2026.11.0. + return vector_find(this->compat_custom_presets_, custom_preset, len); } uint32_t feature_flags_{0}; @@ -289,16 +332,17 @@ class ClimateTraits { climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimatePresetMask supported_presets_; - /** Custom mode storage using const char* pointers to eliminate std::string overhead. + /** Custom mode storage - pointers to vectors owned by the Climate base class. * - * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: - * - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) - * - Static const data: static const char* MODE = "Eco"; - * - * Climate class setters validate pointers are from these vectors before storing. + * ClimateTraits does not own this data; Climate stores the vectors and + * get_traits() wires these pointers automatically. */ - std::vector<const char *> supported_custom_fan_modes_; - std::vector<const char *> supported_custom_presets_; + const std::vector<const char *> *supported_custom_fan_modes_{nullptr}; + const std::vector<const char *> *supported_custom_presets_{nullptr}; + // Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector). + // Remove in 2026.11.0. + std::vector<const char *> compat_custom_fan_modes_; + std::vector<const char *> compat_custom_presets_; }; } // namespace esphome::climate diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index c5f07ac1145..c6d328b1bc5 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -16,6 +16,19 @@ class DemoClimate : public climate::Climate, public Component { public: void set_type(DemoClimateType type) { type_ = type; } void setup() override { + // Set custom modes once during setup — stored on Climate base class, wired via get_traits() + switch (type_) { + case DemoClimateType::TYPE_1: + break; + case DemoClimateType::TYPE_2: + this->set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + this->set_supported_custom_presets({"My Preset"}); + break; + case DemoClimateType::TYPE_3: + this->set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + break; + } + // Set initial state switch (type_) { case DemoClimateType::TYPE_1: this->current_temperature = 20.0; @@ -105,14 +118,13 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_FAN_DIFFUSE, climate::CLIMATE_FAN_QUIET, }); - traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + // Custom fan modes and presets are set once in setup() traits.set_supported_swing_modes({ climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, }); - traits.set_supported_custom_presets({"My Preset"}); break; case DemoClimateType::TYPE_3: traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | @@ -123,7 +135,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_HEAT_COOL, }); - traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + // Custom fan modes are set once in setup() traits.set_supported_swing_modes({ climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index d903db4a1b8..8b20a562c8b 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -168,8 +168,8 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); if (capabilities.supportEcoPreset()) traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); - if (capabilities.supportFrostProtectionPreset()) - traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); + // Frost protection custom preset is handled by AirConditioner directly + // since custom presets are stored on the Climate base class } } // namespace ac diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 4d59a4fbbca..50521cf238b 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -24,6 +24,25 @@ template<typename T> void update_property(T &property, const T &value, bool &fla } void AirConditioner::on_status_change() { + // Add frost protection custom preset once when autoconf completes + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK && + this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) { + // Read existing presets (set by codegen), append frost protection, write back + const auto &existing = this->get_traits().get_supported_custom_presets(); + bool found = false; + for (const char *p : existing) { + if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) { + found = true; + break; + } + } + if (!found) { + std::vector<const char *> merged(existing.begin(), existing.end()); + merged.push_back(Constants::FREEZE_PROTECTION); + this->set_supported_custom_presets(merged); + } + this->frost_protection_set_ = true; + } bool need_publish = false; update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish); update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish); @@ -91,17 +110,15 @@ ClimateTraits AirConditioner::traits() { traits.set_supported_modes(this->supported_modes_); traits.set_supported_swing_modes(this->supported_swing_modes_); traits.set_supported_presets(this->supported_presets_); - if (!this->supported_custom_presets_.empty()) - traits.set_supported_custom_presets(this->supported_custom_presets_); - if (!this->supported_custom_fan_modes_.empty()) - traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); + // Custom fan modes and presets are stored on Climate base class and wired via get_traits() /* + MINIMAL SET OF CAPABILITIES */ traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); - if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) + if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { Converters::to_climate_traits(traits, this->base_.getCapabilities()); + } if (!traits.get_supported_modes().empty()) traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); if (!traits.get_supported_swing_modes().empty()) diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 70833b8bcca..8dbc71b4226 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } - void set_custom_presets(std::initializer_list<const char *> presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; } + void set_custom_presets(std::initializer_list<const char *> presets) { this->set_supported_custom_presets(presets); } + void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); } protected: void control(const ClimateCall &call) override; @@ -55,8 +55,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, ClimateModeMask supported_modes_{}; ClimateSwingModeMask supported_swing_modes_{}; ClimatePresetMask supported_presets_{}; - std::vector<const char *> supported_custom_presets_{}; - std::vector<const char *> supported_custom_fan_modes_{}; + bool frost_protection_set_{false}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d979359c1f8..d8478d2648d 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -333,15 +333,7 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ vector - if (!this->custom_preset_config_.empty()) { - std::vector<const char *> custom_preset_names; - custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &entry : this->custom_preset_config_) { - custom_preset_names.push_back(entry.name); - } - traits.set_supported_custom_presets(custom_preset_names); - } + // Custom presets are stored on Climate base class and wired via get_traits() return traits; } @@ -1306,6 +1298,13 @@ void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> pre void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) { this->custom_preset_config_ = presets; + // Populate Climate base class custom presets vector + std::vector<const char *> names; + names.reserve(presets.size()); + for (const auto &entry : this->custom_preset_config_) { + names.push_back(entry.name); + } + this->set_supported_custom_presets(names); } ThermostatClimate::ThermostatClimate() = default; diff --git a/tests/integration/fixtures/external_components/legacy_climate_component/__init__.py b/tests/integration/fixtures/external_components/legacy_climate_component/__init__.py new file mode 100644 index 00000000000..ba9eff2d892 --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_climate_component/__init__.py @@ -0,0 +1,4 @@ +"""Legacy climate component — tests deprecated ClimateTraits setters backward compat. + +Remove this entire directory in 2026.11.0 when the deprecated setters are removed. +""" diff --git a/tests/integration/fixtures/external_components/legacy_climate_component/climate/__init__.py b/tests/integration/fixtures/external_components/legacy_climate_component/climate/__init__.py new file mode 100644 index 00000000000..0810ae02a16 --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_climate_component/climate/__init__.py @@ -0,0 +1,16 @@ +"""Legacy climate platform that uses deprecated ClimateTraits setters.""" + +import esphome.codegen as cg +from esphome.components import climate +import esphome.config_validation as cv +from esphome.types import ConfigType + +legacy_climate_ns = cg.esphome_ns.namespace("legacy_climate_test") +LegacyClimate = legacy_climate_ns.class_("LegacyClimate", climate.Climate, cg.Component) + +CONFIG_SCHEMA = climate.climate_schema(LegacyClimate).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = await climate.new_climate(config) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/legacy_climate_component/climate/legacy_climate.h b/tests/integration/fixtures/external_components/legacy_climate_component/climate/legacy_climate.h new file mode 100644 index 00000000000..bdf5179fa5b --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_climate_component/climate/legacy_climate.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/core/component.h" + +namespace esphome::legacy_climate_test { + +/// Test climate that uses the DEPRECATED ClimateTraits setters for custom modes. +/// This validates backward compatibility for external components that haven't migrated. +class LegacyClimate : public climate::Climate, public Component { + public: + void setup() override { + this->mode = climate::CLIMATE_MODE_OFF; + this->target_temperature = 22.0f; + this->current_temperature = 20.0f; + this->publish_state(); + } + + protected: + climate::ClimateTraits traits() override { + auto traits = climate::ClimateTraits(); + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_COOL}); + traits.set_visual_min_temperature(16.0f); + traits.set_visual_max_temperature(30.0f); + traits.set_visual_temperature_step(0.5f); + + // DEPRECATED API: setting custom modes directly on ClimateTraits. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Auto"}); + traits.set_supported_custom_presets({"Eco Mode", "Night Mode"}); +#pragma GCC diagnostic pop + + return traits; + } + + void control(const climate::ClimateCall &call) override { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + } + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + } + if (call.has_custom_fan_mode()) { + this->set_custom_fan_mode_(call.get_custom_fan_mode()); + } + if (call.has_custom_preset()) { + this->set_custom_preset_(call.get_custom_preset()); + } + this->publish_state(); + } +}; + +} // namespace esphome::legacy_climate_test diff --git a/tests/integration/fixtures/legacy_climate_compat.yaml b/tests/integration/fixtures/legacy_climate_compat.yaml new file mode 100644 index 00000000000..112e50a4685 --- /dev/null +++ b/tests/integration/fixtures/legacy_climate_compat.yaml @@ -0,0 +1,18 @@ +esphome: + name: legacy-climate-compat + +host: +api: +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [legacy_climate_component] + +climate: + - platform: legacy_climate_component + name: "Legacy Climate" + id: legacy_climate diff --git a/tests/integration/test_legacy_climate_compat.py b/tests/integration/test_legacy_climate_compat.py new file mode 100644 index 00000000000..aad71dd04ae --- /dev/null +++ b/tests/integration/test_legacy_climate_compat.py @@ -0,0 +1,96 @@ +"""Integration test for backward compatibility of deprecated ClimateTraits setters. + +Verifies that external components using the old traits.set_supported_custom_fan_modes() +and traits.set_supported_custom_presets() API still work correctly during the +deprecation period. + +Remove this entire test file and the legacy_climate_component external component +in 2026.11.0 when the deprecated ClimateTraits setters are removed. +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +import aioesphomeapi +from aioesphomeapi import ClimateInfo +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_legacy_climate_compat( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that deprecated ClimateTraits custom mode setters still work end-to-end.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, ( + f"Expected 1 climate entity, got {len(climate_infos)}" + ) + + test_climate = climate_infos[0] + + # Verify custom fan modes set via deprecated ClimateTraits setter are exposed + assert set(test_climate.supported_custom_fan_modes) == { + "Turbo", + "Silent", + "Auto", + }, ( + f"Expected custom fan modes {{Turbo, Silent, Auto}}, " + f"got {test_climate.supported_custom_fan_modes}" + ) + + # Verify custom presets set via deprecated ClimateTraits setter are exposed + assert set(test_climate.supported_custom_presets) == { + "Eco Mode", + "Night Mode", + }, ( + f"Expected custom presets {{Eco Mode, Night Mode}}, " + f"got {test_climate.supported_custom_presets}" + ) + + # Set up state tracking with InitialStateHelper + turbo_future: asyncio.Future[aioesphomeapi.ClimateState] = loop.create_future() + + def on_state(state: aioesphomeapi.EntityState) -> None: + if ( + isinstance(state, aioesphomeapi.ClimateState) + and state.custom_fan_mode == "Turbo" + and not turbo_future.done() + ): + turbo_future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Verify we can set a custom fan mode via API (tests find_custom_fan_mode_ compat path) + client.climate_command(test_climate.key, custom_fan_mode="Turbo") + + try: + turbo_state = await asyncio.wait_for(turbo_future, timeout=5.0) + except TimeoutError: + pytest.fail("Custom fan mode 'Turbo' not received within 5 seconds") + + assert turbo_state.custom_fan_mode == "Turbo" From 8e02d0a20e0c46ecc578b2d5d405f406cb599068 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 12:25:37 -1000 Subject: [PATCH 1344/2056] [fan] Store preset mode vector on Fan entity to eliminate heap allocation (#15209) --- esphome/components/copy/fan/copy_fan.cpp | 9 +- esphome/components/fan/fan.cpp | 55 +++++++++++-- esphome/components/fan/fan.h | 24 ++++++ esphome/components/fan/fan_traits.h | 49 ++++++++--- .../components/hbridge/fan/hbridge_fan.cpp | 1 - esphome/components/hbridge/fan/hbridge_fan.h | 8 +- esphome/components/speed/fan/speed_fan.cpp | 1 - esphome/components/speed/fan/speed_fan.h | 8 +- .../components/template/fan/template_fan.cpp | 1 - .../components/template/fan/template_fan.h | 8 +- .../legacy_fan_component/__init__.py | 4 + .../legacy_fan_component/fan/__init__.py | 16 ++++ .../legacy_fan_component/fan/legacy_fan.h | 45 ++++++++++ .../fixtures/legacy_fan_compat.yaml | 18 ++++ tests/integration/test_legacy_fan_compat.py | 82 +++++++++++++++++++ 15 files changed, 297 insertions(+), 32 deletions(-) create mode 100644 tests/integration/fixtures/external_components/legacy_fan_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/legacy_fan_component/fan/__init__.py create mode 100644 tests/integration/fixtures/external_components/legacy_fan_component/fan/legacy_fan.h create mode 100644 tests/integration/fixtures/legacy_fan_compat.yaml create mode 100644 tests/integration/test_legacy_fan_compat.py diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 14c600d71f4..bdaa35c467b 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -7,6 +7,12 @@ namespace copy { static const char *const TAG = "copy.fan"; void CopyFan::setup() { + // Copy preset modes once from source fan — stored on Fan base class + auto source_traits = source_->get_traits(); + if (source_traits.supports_preset_modes()) { + this->set_supported_preset_modes(source_traits.supported_preset_modes()); + } + source_->add_on_state_callback([this]() { this->copy_state_from_source_(); this->publish_state(); @@ -39,7 +45,8 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); - traits.set_supported_preset_modes(base.supported_preset_modes()); + // Preset modes are set once in setup() and wired via wire_preset_modes_() + this->wire_preset_modes_(traits); return traits; } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index dc7a75018cb..9301e0cea41 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -9,6 +9,22 @@ namespace fan { static const char *const TAG = "fan"; +// Compat: shared empty vector for getter when no preset modes are set. +// Remove in 2026.11.0 when deprecated FanTraits setters are removed +// and getter can return const vector * instead of const vector &. +static const std::vector<const char *> EMPTY_PRESET_MODES; // NOLINT + +const std::vector<const char *> &FanTraits::supported_preset_modes() const { + if (this->preset_modes_) { + return *this->preset_modes_; + } + // Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0 (change return to const vector *). + if (!this->compat_preset_modes_.empty()) { + return this->compat_preset_modes_; + } + return EMPTY_PRESET_MODES; +} + // Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN"); @@ -148,6 +164,18 @@ const char *Fan::find_preset_mode_(const char *preset_mode) { } const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) { + if (preset_mode == nullptr || len == 0) { + return nullptr; + } + if (this->supported_preset_modes_) { + for (const char *mode : *this->supported_preset_modes_) { + if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') { + return mode; + } + } + return nullptr; + } + // Fallback for deprecated path: external components may set modes on FanTraits directly return this->get_traits().find_preset_mode(preset_mode, len); } @@ -261,8 +289,6 @@ void Fan::save_state_() { return; } - auto traits = this->get_traits(); - FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; @@ -271,12 +297,25 @@ void Fan::save_state_() { state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { - const auto &preset_modes = traits.supported_preset_modes(); - // Find index of current preset mode (pointer comparison is safe since preset is from traits) - for (size_t i = 0; i < preset_modes.size(); i++) { - if (preset_modes[i] == this->preset_mode_) { - state.preset_mode = i; - break; + if (this->supported_preset_modes_) { + // New path: search Fan-owned vector directly + for (size_t i = 0; i < this->supported_preset_modes_->size(); i++) { + if ((*this->supported_preset_modes_)[i] == this->preset_mode_) { + state.preset_mode = i; + break; + } + } + } else { + // Compat: fall back to traits for deprecated path. Remove in 2026.11.0. + // Pointer comparison works because preset_mode_ and the compat vector both + // hold pointers to string literals in .rodata (stable addresses). + auto traits = this->get_traits(); + const auto &preset_modes = traits.supported_preset_modes(); + for (size_t i = 0; i < preset_modes.size(); i++) { + if (preset_modes[i] == this->preset_mode_) { + state.preset_mode = i; + break; + } } } } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index e7b3681e32e..d5763edf2f7 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -130,6 +130,14 @@ class Fan : public EntityBase { virtual FanTraits get_traits() = 0; + /// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer). + void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) { + this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end()); + } + void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { + this->ensure_preset_modes_() = preset_modes; + } + /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } @@ -167,11 +175,27 @@ class Fan : public EntityBase { const char *find_preset_mode_(const char *preset_mode); const char *find_preset_mode_(const char *preset_mode, size_t len); + /// Wire the Fan-owned preset modes pointer into the given traits object. + void wire_preset_modes_(FanTraits &traits) { + if (this->supported_preset_modes_) { + traits.set_supported_preset_modes_(this->supported_preset_modes_); + } + } + LazyCallbackManager<void()> state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; private: + /// Lazy-allocate preset modes vector (never freed — entity lives forever). + std::vector<const char *> &ensure_preset_modes_() { + if (!this->supported_preset_modes_) { + this->supported_preset_modes_ = new std::vector<const char *>(); // NOLINT + } + return *this->supported_preset_modes_; + } + + std::vector<const char *> *supported_preset_modes_{nullptr}; const char *preset_mode_{nullptr}; }; diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c0c5f34c504..a2b2633af17 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -3,12 +3,17 @@ #include <cstring> #include <vector> #include <initializer_list> +#include "esphome/core/helpers.h" namespace esphome { namespace fan { +class Fan; // Forward declaration + class FanTraits { + friend class Fan; // Allow Fan to access protected pointer setter + public: FanTraits() = default; FanTraits(bool oscillation, bool speed, bool direction, int speed_count) @@ -30,42 +35,64 @@ class FanTraits { bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } - /// Return the preset modes supported by the fan. - const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan (from initializer list). + // Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *. + const std::vector<const char *> &supported_preset_modes() const; + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0") void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) { - this->preset_modes_ = preset_modes; + // Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector). + this->compat_preset_modes_ = preset_modes; + } + // Remove before 2026.11.0 + ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0") + void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { + this->compat_preset_modes_ = preset_modes; } - /// Set the preset modes supported by the fan (from vector). - void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; } // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete; void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete; /// Return if preset modes are supported - bool supports_preset_modes() const { return !this->preset_modes_.empty(); } + bool supports_preset_modes() const { + // Same precedence as supported_preset_modes() getter + if (this->preset_modes_) { + return !this->preset_modes_->empty(); + } + return !this->compat_preset_modes_.empty(); + } /// Find and return the matching preset mode pointer from supported modes, or nullptr if not found. const char *find_preset_mode(const char *preset_mode) const { return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0); } const char *find_preset_mode(const char *preset_mode, size_t len) const { - if (preset_mode == nullptr || len == 0) + if (preset_mode == nullptr || len == 0) { return nullptr; - for (const char *mode : this->preset_modes_) { + } + // Check pointer-based storage (new path) then compat owned vector (deprecated path) + const auto &modes = this->preset_modes_ ? *this->preset_modes_ : this->compat_preset_modes_; + for (const char *mode : modes) { if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') { - return mode; // Return pointer from traits + return mode; } } return nullptr; } protected: + /// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this). + void set_supported_preset_modes_(const std::vector<const char *> *preset_modes) { + this->preset_modes_ = preset_modes; + } + bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::vector<const char *> preset_modes_{}; + const std::vector<const char *> *preset_modes_{nullptr}; + // Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector). + // Remove in 2026.11.0. + std::vector<const char *> compat_preset_modes_; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 89c162eebfc..d548128b99b 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -30,7 +30,6 @@ fan::FanCall HBridgeFan::brake() { void HBridgeFan::setup() { // Construct traits before restore so preset modes can be looked up by index this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index ec1e8ada0e1..997f66ae486 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -20,11 +20,14 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; } + void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override { return this->traits_; } + fan::FanTraits get_traits() override { + this->wire_preset_modes_(this->traits_); + return this->traits_; + } fan::FanCall brake(); @@ -36,7 +39,6 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::vector<const char *> preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index d45237c4677..eaa8a55858d 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -9,7 +9,6 @@ static const char *const TAG = "speed.fan"; void SpeedFan::setup() { // Construct traits before restore so preset modes can be looked up by index this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index e9a389e0f3f..db96039a135 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,8 +16,11 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); } + fan::FanTraits get_traits() override { + this->wire_preset_modes_(this->traits_); + return this->traits_; + } protected: void control(const fan::FanCall &call) override; @@ -28,7 +31,6 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::vector<const char *> preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 46a5cba9bb3..431be846544 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -9,7 +9,6 @@ void TemplateFan::setup() { // Construct traits before restore so preset modes can be looked up by index this->traits_ = fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b7e1d4ab5a6..5ab6ae8c655 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -13,8 +13,11 @@ class TemplateFan final : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); } + fan::FanTraits get_traits() override { + this->wire_preset_modes_(this->traits_); + return this->traits_; + } protected: void control(const fan::FanCall &call) override; @@ -23,7 +26,6 @@ class TemplateFan final : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::vector<const char *> preset_modes_{}; }; } // namespace esphome::template_ diff --git a/tests/integration/fixtures/external_components/legacy_fan_component/__init__.py b/tests/integration/fixtures/external_components/legacy_fan_component/__init__.py new file mode 100644 index 00000000000..714be181fef --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_fan_component/__init__.py @@ -0,0 +1,4 @@ +"""Legacy fan component — tests deprecated FanTraits setters backward compat. + +Remove this entire directory in 2026.11.0 when the deprecated setters are removed. +""" diff --git a/tests/integration/fixtures/external_components/legacy_fan_component/fan/__init__.py b/tests/integration/fixtures/external_components/legacy_fan_component/fan/__init__.py new file mode 100644 index 00000000000..985d97d081e --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_fan_component/fan/__init__.py @@ -0,0 +1,16 @@ +"""Legacy fan platform that uses deprecated FanTraits setters.""" + +import esphome.codegen as cg +from esphome.components import fan +import esphome.config_validation as cv +from esphome.types import ConfigType + +legacy_fan_ns = cg.esphome_ns.namespace("legacy_fan_test") +LegacyFan = legacy_fan_ns.class_("LegacyFan", fan.Fan, cg.Component) + +CONFIG_SCHEMA = fan.fan_schema(LegacyFan).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + var = await fan.new_fan(config) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/legacy_fan_component/fan/legacy_fan.h b/tests/integration/fixtures/external_components/legacy_fan_component/fan/legacy_fan.h new file mode 100644 index 00000000000..ac378b59c5e --- /dev/null +++ b/tests/integration/fixtures/external_components/legacy_fan_component/fan/legacy_fan.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/components/fan/fan.h" +#include "esphome/core/component.h" + +namespace esphome::legacy_fan_test { + +/// Test fan that uses the DEPRECATED FanTraits setters for preset modes. +/// This validates backward compatibility for external components that haven't migrated. +class LegacyFan : public fan::Fan, public Component { + public: + void setup() override { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(*this); + } + this->publish_state(); + } + + fan::FanTraits get_traits() override { + auto traits = fan::FanTraits(false, true, false, 3); + + // DEPRECATED API: setting preset modes directly on FanTraits. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + traits.set_supported_preset_modes({"Turbo", "Silent", "Eco"}); +#pragma GCC diagnostic pop + + return traits; + } + + protected: + void control(const fan::FanCall &call) override { + if (call.get_state().has_value()) { + this->state = *call.get_state(); + } + if (call.get_speed().has_value()) { + this->speed = *call.get_speed(); + } + this->apply_preset_mode_(call); + this->publish_state(); + } +}; + +} // namespace esphome::legacy_fan_test diff --git a/tests/integration/fixtures/legacy_fan_compat.yaml b/tests/integration/fixtures/legacy_fan_compat.yaml new file mode 100644 index 00000000000..256fd4e4c15 --- /dev/null +++ b/tests/integration/fixtures/legacy_fan_compat.yaml @@ -0,0 +1,18 @@ +esphome: + name: legacy-fan-compat + +host: +api: +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [legacy_fan_component] + +fan: + - platform: legacy_fan_component + name: "Legacy Fan" + id: legacy_fan diff --git a/tests/integration/test_legacy_fan_compat.py b/tests/integration/test_legacy_fan_compat.py new file mode 100644 index 00000000000..5ee41772f3d --- /dev/null +++ b/tests/integration/test_legacy_fan_compat.py @@ -0,0 +1,82 @@ +"""Integration test for backward compatibility of deprecated FanTraits setters. + +Verifies that external components using the old traits.set_supported_preset_modes() +API still work correctly during the deprecation period. + +Remove this entire test file and the legacy_fan_component external component +in 2026.11.0 when the deprecated FanTraits setters are removed. +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import FanInfo, FanState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_legacy_fan_compat( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that deprecated FanTraits preset mode setters still work end-to-end.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + + fan_infos = [e for e in entities if isinstance(e, FanInfo)] + assert len(fan_infos) == 1, f"Expected 1 fan entity, got {len(fan_infos)}" + + test_fan = fan_infos[0] + + # Verify preset modes set via deprecated FanTraits setter are exposed + assert set(test_fan.supported_preset_modes) == { + "Turbo", + "Silent", + "Eco", + }, ( + f"Expected preset modes {{Turbo, Silent, Eco}}, " + f"got {test_fan.supported_preset_modes}" + ) + + # Verify speed support + assert test_fan.supports_speed is True + assert test_fan.supported_speed_count == 3 + + # Subscribe and wait for initial states + states: dict[int, FanState] = {} + state_event = asyncio.Event() + + def on_state(state: FanState) -> None: + if isinstance(state, FanState): + states[state.key] = state + state_event.set() + + client.subscribe_states(on_state) + + # Wait for initial state + await asyncio.wait_for(state_event.wait(), timeout=5.0) + + # Turn on fan with preset mode (tests find_preset_mode_ compat path) + state_event.clear() + client.fan_command( + key=test_fan.key, + state=True, + preset_mode="Turbo", + ) + await asyncio.wait_for(state_event.wait(), timeout=5.0) + + fan_state = states[test_fan.key] + assert fan_state.state is True + assert fan_state.preset_mode == "Turbo" From 14ec82084b026b752f838247bdfce2310fbb17a2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:35:09 +1000 Subject: [PATCH 1345/2056] [rpi_dpi_rgb][qspi_dbi] Add deprecation warnings (#15583) --- esphome/components/qspi_dbi/__init__.py | 5 +++++ esphome/components/qspi_dbi/display.py | 6 ++++++ esphome/components/rpi_dpi_rgb/__init__.py | 5 +++++ esphome/components/rpi_dpi_rgb/display.py | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/esphome/components/qspi_dbi/__init__.py b/esphome/components/qspi_dbi/__init__.py index 290a8643358..aebbe2fcc82 100644 --- a/esphome/components/qspi_dbi/__init__.py +++ b/esphome/components/qspi_dbi/__init__.py @@ -1,3 +1,8 @@ CODEOWNERS = ["@clydebarrow"] CONF_DRAW_FROM_ORIGIN = "draw_from_origin" + +DEPRECATED_COMPONENT = """ +The 'qspi_dbi' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 595067e94c4..48cd72ecdfa 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import display, spi @@ -29,6 +31,7 @@ from . import CONF_DRAW_FROM_ORIGIN from .models import DriverChip DEPENDENCIES = ["spi"] +LOGGER = logging.getLogger(__name__) qspi_dbi_ns = cg.esphome_ns.namespace("qspi_dbi") QSPI_DBI = qspi_dbi_ns.class_( @@ -160,6 +163,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + LOGGER.warning( + "The 'qspi_dbi' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) await spi.register_spi_device(var, config, write_only=True) diff --git a/esphome/components/rpi_dpi_rgb/__init__.py b/esphome/components/rpi_dpi_rgb/__init__.py index c58ce8a01e8..8628f0e0639 100644 --- a/esphome/components/rpi_dpi_rgb/__init__.py +++ b/esphome/components/rpi_dpi_rgb/__init__.py @@ -1 +1,6 @@ CODEOWNERS = ["@clydebarrow"] + +DEPRECATED_COMPONENT = """ +The 'rpi_dpi_rgb' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_rgb' component. +""" diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index ee462686e47..314852832c0 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import display @@ -38,6 +40,7 @@ from esphome.const import ( ) DEPENDENCIES = ["esp32"] +LOGGER = logging.getLogger(__name__) rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb") RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component) @@ -126,6 +129,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + LOGGER.warning( + "The 'rpi_dpi_rgb' component is deprecated, it is recommended to use 'mipi_rgb' instead." + ) var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) From ff5ba99d16fedcc416cf24d4d255c07c45704ca0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:39:13 +1200 Subject: [PATCH 1346/2056] Bump version to 2026.4.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index cfdb74bd196..e60c994ea94 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0-dev +PROJECT_NUMBER = 2026.4.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 29ce0303297..576a4b31d55 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0-dev" +__version__ = "2026.4.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 0a8130858c347b4f0b9ebce9c032dbe9d8139ee4 Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias <git@angelnu.com> Date: Wed, 8 Apr 2026 23:57:53 +0100 Subject: [PATCH 1347/2056] [ade7953_spi] Fix SPI mode on esp-idf (#14824) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/ade7953_base/ade7953_base.cpp | 10 ++++++- .../components/ade7953_base/ade7953_base.h | 30 +++++++++++-------- .../components/ade7953_spi/ade7953_spi.cpp | 6 ++++ esphome/components/ade7953_spi/ade7953_spi.h | 2 +- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/esphome/components/ade7953_base/ade7953_base.cpp b/esphome/components/ade7953_base/ade7953_base.cpp index 821e4a3105e..2dfab8ff853 100644 --- a/esphome/components/ade7953_base/ade7953_base.cpp +++ b/esphome/components/ade7953_base/ade7953_base.cpp @@ -8,6 +8,9 @@ namespace ade7953_base { static const char *const TAG = "ade7953"; +constexpr uint16_t CONFIG_DEFAULT = 0x8004u; +constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u; + static const float ADE_POWER_FACTOR = 154.0f; static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600; @@ -18,7 +21,12 @@ void ADE7953::setup() { // The chip might take up to 100ms to initialise this->set_timeout(100, [this]() { - // this->ade_write_8(0x0010, 0x04); + // Lock communication interface (SPI or I2C) + uint16_t config_v = CONFIG_DEFAULT; + this->ade_read_16(CONFIG_16, &config_v); + config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit + this->ade_write_16(CONFIG_16, config_v); + // Configure optimum settings according to datasheet this->ade_write_8(0x00FE, 0xAD); this->ade_write_16(0x0120, 0x0030); // Set gains diff --git a/esphome/components/ade7953_base/ade7953_base.h b/esphome/components/ade7953_base/ade7953_base.h index bcafddca4eb..b58f95b2304 100644 --- a/esphome/components/ade7953_base/ade7953_base.h +++ b/esphome/components/ade7953_base/ade7953_base.h @@ -9,31 +9,35 @@ namespace esphome { namespace ade7953_base { -static const uint8_t PGA_V_8 = +static constexpr uint8_t PGA_V_8 = 0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0]) -static const uint8_t PGA_IA_8 = +static constexpr uint8_t PGA_IA_8 = 0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0]) -static const uint8_t PGA_IB_8 = +static constexpr uint8_t PGA_IB_8 = 0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0]) -static const uint32_t AIGAIN_32 = +static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register + +static constexpr uint16_t AIGAIN_32 = 0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit) -static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) -static const uint32_t AWGAIN_32 = +static constexpr uint16_t AVGAIN_32 = + 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) +static constexpr uint16_t AWGAIN_32 = 0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit) -static const uint32_t AVARGAIN_32 = +static constexpr uint16_t AVARGAIN_32 = 0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit) -static const uint32_t AVAGAIN_32 = +static constexpr uint16_t AVAGAIN_32 = 0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit) -static const uint32_t BIGAIN_32 = +static constexpr uint16_t BIGAIN_32 = 0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit) -static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) -static const uint32_t BWGAIN_32 = +static constexpr uint16_t BVGAIN_32 = + 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit) +static constexpr uint16_t BWGAIN_32 = 0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit) -static const uint32_t BVARGAIN_32 = +static constexpr uint16_t BVARGAIN_32 = 0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit) -static const uint32_t BVAGAIN_32 = +static constexpr uint16_t BVAGAIN_32 = 0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit) class ADE7953 : public PollingComponent, public sensor::Sensor { diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp index 6b16d933a29..a69b7d19fb5 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.cpp +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -7,6 +7,9 @@ namespace ade7953_spi { static const char *const TAG = "ade7953"; +// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS +constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2; + void AdE7953Spi::setup() { this->spi_setup(); ade7953_base::ADE7953::setup(); @@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) { this->write_byte16(reg); this->transfer_byte(0); this->write_byte16(value); + if (reg == ade7953_base::CONFIG_16) { + delayMicroseconds(CONFIG_LOCK_SETTLE_US); + } this->disable(); return false; } diff --git a/esphome/components/ade7953_spi/ade7953_spi.h b/esphome/components/ade7953_spi/ade7953_spi.h index d96852b9bbe..27f6025d983 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.h +++ b/esphome/components/ade7953_spi/ade7953_spi.h @@ -12,7 +12,7 @@ namespace esphome { namespace ade7953_spi { class AdE7953Spi : public ade7953_base::ADE7953, - public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> { public: void setup() override; From 76490e45bc3c164d4ffe5111a7f61b8af1f66b90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 13:08:29 -1000 Subject: [PATCH 1348/2056] [ci] Fix status-check-labels workflow flooding CI queue (#15585) --- .github/workflows/status-check-labels.yml | 25 +++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index cca70815b93..6483bbe789d 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -2,30 +2,29 @@ name: Status check labels on: pull_request: - types: [labeled, unlabeled] + types: [opened, reopened, labeled, unlabeled, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: check: - name: Check ${{ matrix.label }} + name: Check blocking labels runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - label: - - needs-docs - - merge-after-release - - chained-pr steps: - - name: Check for ${{ matrix.label }} label + - name: Check for blocking labels uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | + const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr']; const { data: labels } = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); - const hasLabel = labels.find(label => label.name === '${{ matrix.label }}'); - if (hasLabel) { - core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}'); + const labelNames = labels.map(l => l.name); + const found = blockingLabels.filter(bl => labelNames.includes(bl)); + if (found.length > 0) { + core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`); } From 52c35ec09cc9982a8c9adf31f2cd63c95611f9aa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:39:14 +1200 Subject: [PATCH 1349/2056] Bump version to 2026.5.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index cfdb74bd196..7fce941c9b4 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0-dev +PROJECT_NUMBER = 2026.5.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 29ce0303297..c2bf86d5321 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0-dev" +__version__ = "2026.5.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From abdbbf4dd27699abc35efb647f593e3ae191077e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 16:14:01 -1000 Subject: [PATCH 1350/2056] [api] Fix ListEntitiesRequest not read due to LWIP rcvevent tracking (#15589) --- esphome/components/api/api_connection.cpp | 36 +++++++++++++++-------- esphome/components/api/api_connection.h | 1 + 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7db423141c1..4663456da62 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -52,11 +52,11 @@ namespace esphome::api { -// Read a maximum of 5 messages per loop iteration to prevent starving other components. +// Maximum messages to read per loop iteration to prevent starving other components. // This is a balance between API responsiveness and allowing other components to run. // Since each message could contain multiple protobuf messages when using packet batching, // this limits the number of messages processed, not the number of TCP packets. -static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; +static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; @@ -220,10 +220,17 @@ void APIConnection::loop() { } const uint32_t now = App.get_loop_component_start_time(); - // Check if socket has data ready before attempting to read - if (this->helper_->is_socket_ready()) { + // Check if socket has data ready before attempting to read. + // Also try reading if we hit the message limit last time — LWIP's rcvevent + // (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple + // messages share a TCP segment, the last message's data stays in LWIP's + // lastdata cache after rcvevent hits 0, making is_socket_ready() return false + // even though data remains. + if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) { + this->flags_.may_have_remaining_data = false; // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput - for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { + uint8_t message_count = 0; + for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { ReadPacketBuffer buffer; err = this->helper_->read_packet(&buffer); if (err == APIError::WOULD_BLOCK) { @@ -245,6 +252,11 @@ void APIConnection::loop() { return; } } + // If we hit the limit, there may be more data remaining in LWIP's + // lastdata cache that rcvevent doesn't account for. + if (message_count == MAX_MESSAGES_PER_LOOP) { + this->flags_.may_have_remaining_data = true; + } } // Process deferred batch if scheduled and timer has expired @@ -2086,6 +2098,13 @@ void APIConnection::process_batch_() { return; } + // Ensure TCP_NODELAY is on before draining overflow and writing batch data. + // Log messages enable Nagle (NODELAY off) to coalesce small packets. + // If Nagle is still on when we try to drain, LWIP holds data in the + // Nagle buffer, the TCP send buffer stays full, and the overflow + // buffer can never drain — blocking the batch write indefinitely. + this->helper_->set_nodelay_for_message(false); + // Try to clear buffer first if (!this->try_to_clear_buffer(true)) { // Can't write now, we'll try again later @@ -2193,13 +2212,6 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items shared_buf.resize(shared_buf.size() + footer_size); } - // Ensure TCP_NODELAY is on before writing batch data. - // Log messages enable Nagle (NODELAY off) to coalesce small packets. - // Without this, batch data written to the socket sits in LWIP's Nagle - // buffer — the remote won't ACK until it sends its own data (e.g. a - // ping), which can take 20+ seconds. - this->helper_->set_nodelay_for_message(false); - // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, std::span<const MessageInfo>(message_info, items_processed)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284c4475deb..7d087970903 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -771,6 +771,7 @@ class APIConnection final : public APIServerConnectionBase { uint8_t batch_scheduled : 1; uint8_t batch_first_message : 1; // For batch buffer allocation uint8_t should_try_send_immediately : 1; // True after initial states are sent + uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check #ifdef HAS_PROTO_MESSAGE_DUMP uint8_t log_only_mode : 1; #endif From 46d0c29be5fdec7d4a03c3252a53b7c1651c2f67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 19:20:32 -1000 Subject: [PATCH 1351/2056] [safe_mode] Use loop component start time instead of millis() (#15591) --- esphome/components/safe_mode/safe_mode.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 40fa03392b3..5b0f7c8827e 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -86,7 +86,8 @@ void SafeModeComponent::mark_successful() { } void SafeModeComponent::loop() { - if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { + if (!this->boot_successful_ && + (App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) { // successful boot, reset counter ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->mark_successful(); From eafc5df3f29d6e4d895d8947db95bd77a891506f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 19:30:39 -1000 Subject: [PATCH 1352/2056] [safe_mode] Combine related OTA rollback log messages (#15592) --- esphome/components/safe_mode/safe_mode.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 5b0f7c8827e..bae5e42b9b5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() { #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition(); if (last_invalid != nullptr) { - ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); - ESP_LOGW(TAG, "The device reset before the boot was marked successful"); + ESP_LOGW(TAG, + "OTA rollback detected! Rolled back from partition '%s'\n" + " The device reset before the boot was marked successful", + last_invalid->label); if (esp_reset_reason() == ESP_RST_BROWNOUT) { - ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!"); - ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); + ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n" + " See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } #endif From 2721f08bcc5e0c0238dd4a77bda50fe5d7fc7676 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:03:58 +0000 Subject: [PATCH 1353/2056] Bump aioesphomeapi from 44.12.0 to 44.13.1 (#15600) 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 c4b90b5ca94..d7db44454cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.12.0 +aioesphomeapi==44.13.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 6f5d642a310623bddf175eaaf3bbc99084e263ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 05:48:10 -1000 Subject: [PATCH 1354/2056] [gdk101] Increase reset retries for slow-booting sensor MCU (#15584) --- esphome/components/gdk101/gdk101.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 149973ba8a2..0ee718cd20e 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -7,7 +7,7 @@ namespace gdk101 { static const char *const TAG = "gdk101"; static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5; -static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10; +static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30; static constexpr uint32_t RESET_INTERVAL_ID = 0; static constexpr uint32_t RESET_INTERVAL_MS = 1000; From dd07fba943f27a9105ede153217bb62d4910d58c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 05:48:18 -1000 Subject: [PATCH 1355/2056] [socket] Document ready() contract: callers must drain or track (#15590) --- esphome/components/api/api_frame_helper.h | 5 ++++- esphome/components/socket/bsd_sockets_impl.h | 2 ++ esphome/components/socket/lwip_raw_tcp_impl.h | 2 ++ esphome/components/socket/lwip_sockets_impl.h | 2 ++ esphome/components/socket/socket.h | 13 +++++++++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index d1215388d2c..f98eca80766 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -195,7 +195,10 @@ class APIFrameHelper { } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } - // Check if socket has data ready to read + // Check if socket has buffered data ready to read. + // Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK) + // or track that they stopped early and retry without this check. + // See Socket::ready() for details. bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } // Release excess memory from internal buffers after initial sync void release_buffers() { diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index 339a699bc97..e520784702b 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -112,6 +112,8 @@ class BSDSocketImpl { int setblocking(bool blocking); int loop() { return 0; } + /// Check if the socket has buffered data ready to read. + /// See the ready() contract in socket.h — callers must drain or track remaining data. bool ready() const; int get_fd() const { return this->fd_; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index e2dcb80d32b..917b5b2f7a2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon { errno = ENOSYS; return -1; } + // Check if the socket has buffered data ready to read. + // See the ready() contract in socket.h — callers must drain or track remaining data. // Intentionally unlocked — this is a polling check called every loop iteration. // A stale read at worst delays processing by one loop tick; the actual I/O in // read() holds the lwip lock and re-checks properly. See esphome#10681. diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index bfc4da9926a..942d0ccf857 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -78,6 +78,8 @@ class LwIPSocketImpl { int setblocking(bool blocking); int loop() { return 0; } + /// Check if the socket has buffered data ready to read. + /// See the ready() contract in socket.h — callers must drain or track remaining data. bool ready() const; int get_fd() const { return this->fd_; } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 9ea71321e0b..ad55e889e80 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored); // Inline ready() — defined here because it depends on socket_ready/socket_ready_fd // declared above, while the impl headers are included before those declarations. +// +// Contract (applies to ALL socket implementations — each platform implements +// ready() differently, but this contract holds regardless of the mechanism): +// ready() checks if the socket has buffered data ready to read. When it returns +// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until +// read() returns 0 to indicate EOF / connection closed, or track that it stopped +// early and retry without calling ready(). The next call to ready() will only +// report new data correctly if all callers fulfill this contract. Failing to +// drain the socket may cause ready() to return false while data remains readable. +// +// In practice each socket is owned by a single component, so this contract is +// straightforward to fulfill — but the owning component must be aware of it, +// especially if it limits how many messages it processes per loop iteration. #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) inline bool Socket::ready() const { #ifdef USE_LWIP_FAST_SELECT From 8f6d489a9a0bb4e5b35ee387446e1fb5f0610526 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 05:48:33 -1000 Subject: [PATCH 1356/2056] [ci] Use --base-only for memory impact builds (#15598) --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dddf21f57ed..cf9fa8e7c05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -868,7 +868,8 @@ jobs: python script/test_build_components.py \ -e compile \ -c "$component_list" \ - -t "$platform" 2>&1 | \ + -t "$platform" \ + --base-only 2>&1 | \ tee /dev/stderr | \ python script/ci_memory_impact_extract.py \ --output-env \ @@ -954,7 +955,8 @@ jobs: python script/test_build_components.py \ -e compile \ -c "$component_list" \ - -t "$platform" 2>&1 | \ + -t "$platform" \ + --base-only 2>&1 | \ tee /dev/stderr | \ python script/ci_memory_impact_extract.py \ --output-env \ From 03db32d045081b6716ebd4260be9ba6cebbfa08c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 07:48:32 -1000 Subject: [PATCH 1357/2056] [core] Add CodSpeed benchmarks for hot helper functions (#15593) --- tests/benchmarks/core/bench_helpers.cpp | 271 +++++++++++++++++++++++- 1 file changed, 270 insertions(+), 1 deletion(-) diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index c6e1e6930eb..d9a9d158a3e 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -10,7 +10,6 @@ namespace esphome::benchmarks { static constexpr int kInnerIterations = 2000; // --- random_float() --- -// Ported from ol.yaml:148 "Random Float Benchmark" static void RandomFloat(benchmark::State &state) { for (auto _ : state) { @@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) { } BENCHMARK(RandomUint32); +// --- format_hex_to() - 6 bytes (MAC address sized) --- + +static void FormatHexTo_6Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45}; + char buffer[13]; // 6 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 6); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_6Bytes); + +// --- format_hex_to() - 16 bytes (UUID sized) --- + +static void FormatHexTo_16Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; + char buffer[33]; // 16 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 16); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_16Bytes); + +// --- format_hex_to() - 100 bytes (large payload) --- + +static void FormatHexTo_100Bytes(benchmark::State &state) { + uint8_t data[100]; + for (int i = 0; i < 100; i++) { + data[i] = static_cast<uint8_t>(i); + } + char buffer[201]; // 100 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 100); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_100Bytes); + +// --- format_hex_pretty_to() - 6 bytes with ':' separator --- + +static void FormatHexPrettyTo_6Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45}; + char buffer[18]; // 6 * 3 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_pretty_to(buffer, data, 6); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexPrettyTo_6Bytes); + +// --- format_mac_addr_upper() --- + +static void FormatMacAddrUpper(benchmark::State &state) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_mac_addr_upper(mac, buffer); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatMacAddrUpper); + +// --- fnv1_hash() - short string --- + +static void Fnv1Hash_Short(benchmark::State &state) { + const char *str = "sensor.temperature"; + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash(str); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1Hash_Short); + +// --- fnv1_hash() - long string --- + +static void Fnv1Hash_Long(benchmark::State &state) { + const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected"; + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash(str); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1Hash_Long); + +// --- fnv1a_hash() - short string --- +// Use DoNotOptimize on the input pointer to prevent constexpr evaluation + +static void Fnv1aHash_Short(benchmark::State &state) { + const char *str = "sensor.temperature"; + benchmark::DoNotOptimize(str); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1a_hash(str); + benchmark::ClobberMemory(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1aHash_Short); + +// --- fnv1a_hash() - long string --- + +static void Fnv1aHash_Long(benchmark::State &state) { + const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected"; + benchmark::DoNotOptimize(str); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1a_hash(str); + benchmark::ClobberMemory(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1aHash_Long); + +// --- fnv1_hash_object_id() - typical entity name --- + +static void Fnv1HashObjectId(benchmark::State &state) { + char name[] = "Living Room Temperature Sensor"; + size_t len = sizeof(name) - 1; + benchmark::DoNotOptimize(name); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash_object_id(name, len); + benchmark::ClobberMemory(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1HashObjectId); + +// --- parse_hex() - 6 bytes from string --- + +static void ParseHex_6Bytes(benchmark::State &state) { + const char *hex_str = "ABCDEF012345"; + uint8_t data[6]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + parse_hex(hex_str, data, 6); + } + benchmark::DoNotOptimize(data); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ParseHex_6Bytes); + +// --- parse_hex() - 16 bytes from string --- + +static void ParseHex_16Bytes(benchmark::State &state) { + const char *hex_str = "ABCDEF0123456789FEDCBA9876543210"; + uint8_t data[16]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + parse_hex(hex_str, data, 16); + } + benchmark::DoNotOptimize(data); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ParseHex_16Bytes); + +// --- crc8() - 8 bytes --- + +static void CRC8_8Bytes(benchmark::State &state) { + const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + for (auto _ : state) { + uint8_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= crc8(data, 8); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CRC8_8Bytes); + +// --- crc16() - 8 bytes --- + +static void CRC16_8Bytes(benchmark::State &state) { + const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + for (auto _ : state) { + uint16_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= crc16(data, 8); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CRC16_8Bytes); + +// --- value_accuracy_to_buf() - typical sensor value --- + +static void ValueAccuracyToBuf(benchmark::State &state) { + char raw_buf[VALUE_ACCURACY_MAX_LEN] = {}; + std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf); + float value = 23.456f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + value_accuracy_to_buf(buf, value, 2); + } + benchmark::DoNotOptimize(raw_buf); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ValueAccuracyToBuf); + +// --- int8_to_str() --- + +static void Int8ToStr(benchmark::State &state) { + char buffer[5] = {}; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + int8_to_str(buffer, static_cast<int8_t>(i & 0xFF)); + benchmark::DoNotOptimize(buffer); + benchmark::ClobberMemory(); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Int8ToStr); + +// --- base64_decode() - into pre-allocated buffer --- + +static void Base64Decode_32Bytes(benchmark::State &state) { + // 32 bytes encoded = 44 base64 chars + const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw=="; + size_t encoded_len = 44; + uint8_t output[32]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + base64_decode(encoded, encoded_len, output, sizeof(output)); + } + benchmark::DoNotOptimize(output); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Base64Decode_32Bytes); + } // namespace esphome::benchmarks From d062f62656a3632384df7c81f70afe845d28aeab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:00:52 -0400 Subject: [PATCH 1358/2056] [sx127x][cc1101] Disable loop when packet mode is inactive (#15606) --- esphome/components/cc1101/cc1101.cpp | 9 +++++++++ esphome/components/sx127x/sx127x.cpp | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 51aa88b8f72..f2b7451721a 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -150,6 +150,10 @@ void CC1101Component::setup() { if (this->gdo0_pin_ != nullptr) { this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); }); } + + if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { + this->disable_loop(); + } } void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) { @@ -669,6 +673,11 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { + if (value) { + this->enable_loop(); + } else { + this->disable_loop(); + } this->write_(Register::PKTCTRL0); this->write_(Register::PKTCTRL1); this->write_(Register::IOCFG0); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 0fddfdccdb7..83be96767a4 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -383,9 +383,14 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { if (millis() - start > 20) { ESP_LOGE(TAG, "Set mode failure"); this->mark_failed(); - break; + return; } } + if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) { + this->enable_loop(); + } else { + this->disable_loop(); + } } void SX127x::set_mode_rx() { From ab71f5276f8a0b454b22f852ed8ed2882e8dbf8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:36:25 +0000 Subject: [PATCH 1359/2056] Bump ruff from 0.15.9 to 0.15.10 (#15609) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8c21aad367..ac4f0049f83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.9 + rev: v0.15.10 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index eeee3434ce0..18d0461e830 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.9 # also change in .pre-commit-config.yaml when updating +ruff==0.15.10 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 668007707da498e12fd9904a152761245821f258 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:13:22 +1200 Subject: [PATCH 1360/2056] [CI] Add org fork detection warning to auto-label PR workflow (#15588) --- .github/scripts/auto-label-pr/constants.js | 1 + .github/scripts/auto-label-pr/detectors.js | 19 +++++++ .github/scripts/auto-label-pr/index.js | 16 ++++-- .github/scripts/auto-label-pr/reviews.js | 62 +++++++++++++++++++++- 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index 1c33772c4c3..e02b450bf0e 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -4,6 +4,7 @@ module.exports = { CODEOWNERS_MARKER: '<!-- codeowners-request -->', TOO_BIG_MARKER: '<!-- too-big-request -->', DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->', + ORG_FORK_MARKER: '<!-- maintainer-access-warning -->', MANAGED_LABELS: [ 'new-component', diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index fb9dadc6a09..25c0ba49afc 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) { return { labels, deprecatedInfo }; } +// Strategy: Detect when maintainers cannot modify the PR branch +function detectMaintainerAccess(context) { + const pr = context.payload.pull_request; + + // Only relevant for cross-repo PRs (forks) + if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) { + return null; + } + + if (pr.maintainer_can_modify) { + return null; + } + + const isOrgFork = pr.head.repo.owner.type === 'Organization'; + console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`); + return { isOrgFork, orgName: pr.head.repo.owner.login }; +} + // Strategy: Requirements detection async function detectRequirements(allLabels, prFiles, context) { const labels = new Set(); @@ -329,5 +347,6 @@ module.exports = { detectTests, detectPRTemplateCheckboxes, detectDeprecatedComponents, + detectMaintainerAccess, detectRequirements }; diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 42588c0bc87..021e91a9ee1 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -12,9 +12,10 @@ const { detectTests, detectPRTemplateCheckboxes, detectDeprecatedComponents, + detectMaintainerAccess, detectRequirements } = require('./detectors'); -const { handleReviews } = require('./reviews'); +const { handleReviews, handleMaintainerAccessComment } = require('./reviews'); const { applyLabels, removeOldLabels } = require('./labels'); // Fetch API data @@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => { codeOwnerLabels, testLabels, checkboxLabels, - deprecatedResult + deprecatedResult, + maintainerAccess ] = await Promise.all([ detectMergeBranch(context), detectComponentPlatforms(changedFiles, apiData), @@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => { detectCodeOwner(github, context, changedFiles), detectTests(changedFiles), detectPRTemplateCheckboxes(context), - detectDeprecatedComponents(github, context, changedFiles) + detectDeprecatedComponents(github, context, changedFiles), + detectMaintainerAccess(context) ]); // Extract deprecated component info @@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => { console.log('Computed labels:', finalLabels.join(', ')); - // Handle reviews - await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD); + // Handle reviews and org fork comment + await Promise.all([ + handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD), + handleMaintainerAccessComment(github, context, maintainerAccess) + ]); // Apply labels await applyLabels(github, context, finalLabels); diff --git a/.github/scripts/auto-label-pr/reviews.js b/.github/scripts/auto-label-pr/reviews.js index 906e2c456ab..7ac136515de 100644 --- a/.github/scripts/auto-label-pr/reviews.js +++ b/.github/scripts/auto-label-pr/reviews.js @@ -2,7 +2,8 @@ const { BOT_COMMENT_MARKER, CODEOWNERS_MARKER, TOO_BIG_MARKER, - DEPRECATED_COMPONENT_MARKER + DEPRECATED_COMPONENT_MARKER, + ORG_FORK_MARKER } = require('./constants'); // Generate review messages @@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d } } +// Handle maintainer access warning comment +async function handleMaintainerAccessComment(github, context, maintainerAccess) { + if (!maintainerAccess) { + return; + } + + const { owner, repo } = context.repo; + const pr_number = context.issue.number; + const prAuthor = context.payload.pull_request.user.login; + + // Check if we already posted the warning (iterate pages to exit early) + let existingComment; + for await (const { data: comments } of github.paginate.iterator( + github.rest.issues.listComments, + { owner, repo, issue_number: pr_number } + )) { + existingComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body && comment.body.includes(ORG_FORK_MARKER) + ); + if (existingComment) { + break; + } + } + + if (existingComment) { + console.log('Maintainer access warning comment already exists, skipping'); + return; + } + + let body; + if (maintainerAccess.isOrgFork) { + body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` + + `Hey there @${prAuthor},\n` + + `It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` + + `GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` + + `This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` + + `To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` + + `See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`; + } else { + body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` + + `Hey there @${prAuthor},\n` + + `It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` + + `This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` + + `Please enable this option in the PR sidebar to allow maintainer collaboration.`; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body + }); + console.log('Created maintainer access warning comment'); +} + module.exports = { - handleReviews + handleReviews, + handleMaintainerAccessComment }; From c04dfa922ecb5b71782ef250e7ba59bf7b63caa7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:02:49 +1200 Subject: [PATCH 1361/2056] [hbridge] Move light pin switching to loop (#15615) --- esphome/components/hbridge/light/__init__.py | 2 +- .../hbridge/light/hbridge_light_output.h | 52 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/esphome/components/hbridge/light/__init__.py b/esphome/components/hbridge/light/__init__.py index 65dd3196dff..ccb47237b64 100644 --- a/esphome/components/hbridge/light/__init__.py +++ b/esphome/components/hbridge/light/__init__.py @@ -8,7 +8,7 @@ from .. import hbridge_ns CODEOWNERS = ["@DotNetDann"] HBridgeLightOutput = hbridge_ns.class_( - "HBridgeLightOutput", cg.PollingComponent, light.LightOutput + "HBridgeLightOutput", cg.Component, light.LightOutput ) CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( diff --git a/esphome/components/hbridge/light/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h index c3091548523..4e064d53522 100644 --- a/esphome/components/hbridge/light/hbridge_light_output.h +++ b/esphome/components/hbridge/light/hbridge_light_output.h @@ -1,20 +1,17 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -#include "esphome/core/log.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace hbridge { -// Using PollingComponent as the updates are more consistent and reduces flickering -class HBridgeLightOutput : public PollingComponent, public light::LightOutput { +class HBridgeLightOutput : public Component, public light::LightOutput { public: - HBridgeLightOutput() : PollingComponent(1) {} - - void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; } - void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; } + void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; } + void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); @@ -24,16 +21,16 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { return traits; } - void setup() override { this->forward_direction_ = false; } + void setup() override { this->disable_loop(); } - void update() override { - // This method runs around 60 times per second - // We cannot do the PWM ourselves so we are reliant on the hardware PWM - if (!this->forward_direction_) { // First LED Direction + void loop() override { + // Only called when both channels are active — alternate H-bridge direction + // each iteration to multiplex cold and warm white. + if (!this->forward_direction_) { this->pina_pin_->set_level(this->pina_duty_); this->pinb_pin_->set_level(0); this->forward_direction_ = true; - } else { // Second LED Direction + } else { this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; @@ -43,15 +40,32 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); + float new_pina, new_pinb; + state->current_values_as_cwww(&new_pina, &new_pinb, false); + + this->pina_duty_ = new_pina; + this->pinb_duty_ = new_pinb; + + if (new_pina != 0.0f && new_pinb != 0.0f) { + // Both channels active — need loop to alternate H-bridge direction + this->high_freq_.start(); + this->enable_loop(); + } else { + // Zero or one channel active — drive pins directly, no multiplexing needed + this->high_freq_.stop(); + this->disable_loop(); + this->pina_pin_->set_level(new_pina); + this->pinb_pin_->set_level(new_pinb); + } } protected: output::FloatOutput *pina_pin_; output::FloatOutput *pinb_pin_; - float pina_duty_ = 0; - float pinb_duty_ = 0; - bool forward_direction_ = false; + float pina_duty_{0}; + float pinb_duty_{0}; + bool forward_direction_{false}; + HighFrequencyLoopRequester high_freq_; }; } // namespace hbridge From c90fa2378ab1fa8c5b30c118ff1f6ca5d10c21ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:00 -1000 Subject: [PATCH 1362/2056] [tca9555] Add interrupt pin support (#15613) --- esphome/components/tca9555/__init__.py | 4 ++++ esphome/components/tca9555/tca9555.cpp | 19 ++++++++++++++++++- esphome/components/tca9555/tca9555.h | 5 +++++ tests/components/tca9555/common.yaml | 4 ++++ tests/components/tca9555/test.esp32-idf.yaml | 3 +++ .../components/tca9555/test.esp8266-ard.yaml | 3 +++ tests/components/tca9555/test.rp2040-ard.yaml | 3 +++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py index f42e0fe398b..5f571fcea6e 100644 --- a/esphome/components/tca9555/__init__.py +++ b/esphome/components/tca9555/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(TCA9555Component), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -38,6 +40,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 79c52538983..3eb794df441 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -24,9 +24,18 @@ void TCA9555Component::setup() { this->mark_failed(); return; } + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&TCA9555Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR TCA9555Component::gpio_intr(TCA9555Component *arg) { arg->enable_loop_soon_any_context(); } void TCA9555Component::dump_config() { ESP_LOGCONFIG(TAG, "TCA9555:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -36,6 +45,9 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { // Set mode mask bit this->mode_mask_ |= 1 << pin; + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Clear mode mask bit this->mode_mask_ &= ~(1 << pin); @@ -43,7 +55,12 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { // Write GPIO to enable input mode this->write_gpio_modes_(); } -void TCA9555Component::loop() { this->reset_pin_cache_(); } +void TCA9555Component::loop() { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } +} bool TCA9555Component::read_gpio_outputs_() { if (this->is_failed()) diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index 9f7273b1e7e..d4d070013c7 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -24,7 +24,10 @@ class TCA9555Component : public Component, void loop() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(TCA9555Component *arg); bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -39,6 +42,8 @@ class TCA9555Component : public Component, bool read_gpio_modes_(); bool write_gpio_modes_(); bool read_gpio_outputs_(); + + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a TCA9555 pin as an internal input GPIO pin. diff --git a/tests/components/tca9555/common.yaml b/tests/components/tca9555/common.yaml index 82b4c959d81..d1a68c575a8 100644 --- a/tests/components/tca9555/common.yaml +++ b/tests/components/tca9555/common.yaml @@ -2,6 +2,10 @@ tca9555: - id: tca9555_hub i2c_id: i2c_bus address: 0x21 + - id: tca9555_hub_int + i2c_id: i2c_bus + address: 0x22 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/tca9555/test.esp32-idf.yaml b/tests/components/tca9555/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/tca9555/test.esp32-idf.yaml +++ b/tests/components/tca9555/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/tca9555/test.esp8266-ard.yaml b/tests/components/tca9555/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/tca9555/test.esp8266-ard.yaml +++ b/tests/components/tca9555/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/tca9555/test.rp2040-ard.yaml b/tests/components/tca9555/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/tca9555/test.rp2040-ard.yaml +++ b/tests/components/tca9555/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 9cf9b02ba2a3f1fe634859da93ee38c1ca25d30e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:26 -1000 Subject: [PATCH 1363/2056] [pca6416a] Add interrupt pin support (#15614) --- esphome/components/pca6416a/__init__.py | 10 +++++++++- esphome/components/pca6416a/pca6416a.cpp | 18 ++++++++++++++++++ esphome/components/pca6416a/pca6416a.h | 4 ++++ tests/components/pca6416a/common.yaml | 4 ++++ tests/components/pca6416a/test.esp32-idf.yaml | 3 +++ .../components/pca6416a/test.esp8266-ard.yaml | 3 +++ tests/components/pca6416a/test.rp2040-ard.yaml | 3 +++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index b6e156e7ff4..813bb35c482 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -25,7 +26,12 @@ PCA6416AGPIOPin = pca6416a_ns.class_( CONF_PCA6416A = "pca6416a" CONFIG_SCHEMA = ( - cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)}) + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } + ) .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x21)) ) @@ -35,6 +41,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index f393af88cea..dc7463b01ba 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -49,11 +49,22 @@ void PCA6416AComponent::setup() { ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); } void PCA6416AComponent::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } } void PCA6416AComponent::dump_config() { @@ -62,6 +73,7 @@ void PCA6416AComponent::dump_config() { } else { ESP_LOGCONFIG(TAG, "PCA6416A:"); } + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -101,6 +113,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { this->update_register_(pin, true, pull_dir); this->update_register_(pin, false, pull_en); } + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { this->update_register_(pin, true, io_dir); if (has_pullup_) { @@ -109,6 +124,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { } else { ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors"); } + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { this->update_register_(pin, false, io_dir); } diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 138a51cc208..4d2e6b219e4 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -24,7 +24,10 @@ class PCA6416AComponent : public Component, void dump_config() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg); // Virtual methods from CachedGpioExpander bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; @@ -43,6 +46,7 @@ class PCA6416AComponent : public Component, esphome::i2c::ErrorCode last_error_; /// Only the PCAL6416A has pull-up resistors bool has_pullup_{false}; + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCA6416A pin as an internal input GPIO pin. diff --git a/tests/components/pca6416a/common.yaml b/tests/components/pca6416a/common.yaml index 9ad6e2fb158..09083c6c156 100644 --- a/tests/components/pca6416a/common.yaml +++ b/tests/components/pca6416a/common.yaml @@ -2,6 +2,10 @@ pca6416a: - id: pca6416a_hub i2c_id: i2c_bus address: 0x21 + - id: pca6416a_hub_int + i2c_id: i2c_bus + address: 0x22 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pca6416a/test.esp32-idf.yaml b/tests/components/pca6416a/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pca6416a/test.esp32-idf.yaml +++ b/tests/components/pca6416a/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pca6416a/test.esp8266-ard.yaml b/tests/components/pca6416a/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pca6416a/test.esp8266-ard.yaml +++ b/tests/components/pca6416a/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pca6416a/test.rp2040-ard.yaml b/tests/components/pca6416a/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pca6416a/test.rp2040-ard.yaml +++ b/tests/components/pca6416a/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 17209df7b5f97ad3dc6d4442366959661362bf32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:52 -1000 Subject: [PATCH 1364/2056] [mcp23016] Add interrupt pin support (#15616) --- esphome/components/mcp23016/__init__.py | 4 ++++ esphome/components/mcp23016/mcp23016.cpp | 14 ++++++++++++++ esphome/components/mcp23016/mcp23016.h | 4 ++++ tests/components/mcp23016/common.yaml | 8 ++++++-- tests/components/mcp23016/test.esp32-idf.yaml | 3 +++ tests/components/mcp23016/test.esp8266-ard.yaml | 3 +++ tests/components/mcp23016/test.rp2040-ard.yaml | 3 +++ 7 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 5a1f011617a..b71d57498a2 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -24,6 +25,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(MCP23016), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -35,6 +37,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index fbdb6903b84..118a77ce378 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -24,11 +24,22 @@ void MCP23016::setup() { // all pins input this->write_reg_(MCP23016_IODIR1, 0xFFFF); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); } void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } } bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); } @@ -37,6 +48,9 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { this->update_reg_(pin, true, MCP23016_IODIR1); + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg_(pin, false, MCP23016_IODIR1); } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 494bc9c197a..32149ba3e24 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -35,7 +35,10 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: float get_setup_priority() const override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(MCP23016 *arg); // Virtual methods from CachedGpioExpander bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; @@ -51,6 +54,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: uint16_t olat_{0x0000}; // Cache for input values (16-bit combined for both banks) uint16_t input_mask_{0x0000}; + InternalGPIOPin *interrupt_pin_{nullptr}; }; class MCP23016GPIOPin : public GPIOPin { diff --git a/tests/components/mcp23016/common.yaml b/tests/components/mcp23016/common.yaml index e8e3ad9d08a..81f38b3f52e 100644 --- a/tests/components/mcp23016/common.yaml +++ b/tests/components/mcp23016/common.yaml @@ -1,6 +1,10 @@ mcp23016: - i2c_id: i2c_bus - id: mcp23016_hub + - i2c_id: i2c_bus + id: mcp23016_hub + - i2c_id: i2c_bus + id: mcp23016_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/mcp23016/test.esp32-idf.yaml b/tests/components/mcp23016/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23016/test.esp32-idf.yaml +++ b/tests/components/mcp23016/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23016/test.esp8266-ard.yaml b/tests/components/mcp23016/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23016/test.esp8266-ard.yaml +++ b/tests/components/mcp23016/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23016/test.rp2040-ard.yaml b/tests/components/mcp23016/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23016/test.rp2040-ard.yaml +++ b/tests/components/mcp23016/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From ec420d57923bb6c38bf780e1bfcf854e48b51896 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:33:56 -1000 Subject: [PATCH 1365/2056] [api] Add (inline_encode) proto option for sub-message inlining (#15599) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 47 ++-- esphome/components/api/api_pb2.h | 2 - esphome/components/api/proto.h | 6 + script/api_protobuf/api_protobuf.py | 293 +++++++++++++++++++++-- 6 files changed, 304 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 33d16f0339b..e4d0c2d16df 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1625,6 +1625,7 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { + option (inline_encode) = true; uint64 address = 1 [(force) = true]; sint32 rssi = 2 [(force) = true]; uint32 address_type = 3 [(max_value) = 4]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 0f71268d700..dacc290e31e 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -22,6 +22,7 @@ extend google.protobuf.MessageOptions { optional bool log = 1039 [default=true]; optional bool no_delay = 1040 [default=false]; optional string base_class = 1041; + optional bool inline_encode = 1042 [default=false]; } extend google.protobuf.FieldOptions { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d27cfa57cf9..c2d513f0d33 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2328,40 +2328,37 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { - uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); - ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); - ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi)); - if (this->address_type) { - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); - ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type); - } - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len)); - ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len); - return pos; -} -uint32_t BluetoothLERawAdvertisement::calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_uint64_force(1, this->address); - size += ProtoSize::calc_sint32_force(1, this->rssi); - size += this->address_type ? 2 : 0; - size += 2 + this->data_len; - return size; -} uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { - ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]); + auto &sub_msg = this->advertisements[i]; + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10); + uint8_t *len_pos = pos; + ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); + ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); + ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi)); + if (sub_msg.address_type) { + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type); + } + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len)); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len); + *len_pos = static_cast<uint8_t>(pos - len_pos - 1); } return pos; } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); + auto &sub_msg = this->advertisements[i]; + size += 2; + size += ProtoSize::calc_uint64_force(1, sub_msg.address); + size += ProtoSize::calc_sint32_force(1, sub_msg.rssi); + size += sub_msg.address_type ? 2 : 0; + size += 2 + sub_msg.data_len; } return size; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3b239db36c5..5827a8728e6 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1888,8 +1888,6 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; - uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e0a4e03189e..8cac7fff3bf 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -352,6 +352,12 @@ class ProtoEncode { PROTO_ENCODE_CHECK_BOUNDS(pos, 1); *pos++ = b; } + /// Reserve one byte for later backpatch (e.g., sub-message length). + /// Advances pos past the reserved byte without writing a value. + static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + pos++; + } /// Write raw bytes to the buffer (no tag, no length prefix). static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 526644842da..39bfc865d02 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -60,6 +60,10 @@ FILE_HEADER = """// This file was automatically generated with a tool. # Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value. _enum_max_values: dict[str, int] = {} +# Populated by main() before message generation. +# Maps message name (e.g. "BluetoothLERawAdvertisement") to its descriptor. +_message_desc_map: dict[str, Any] = {} + def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" @@ -427,6 +431,23 @@ class TypeInfo(ABC): Estimated size in bytes including field ID and typical data """ + def get_max_encoded_size(self) -> int | None: + """Get the maximum possible encoded size in bytes for this field. + + Returns the worst-case encoded size including field ID and maximum + possible value encoding. Returns None if the size is unbounded + (e.g., variable-length strings without max_data_length). + + Used by (inline_encode) validation to ensure sub-messages fit in a + single-byte length varint (< 128 bytes). + """ + return None # Unbounded by default + + +def _varint_max_size(bits: int) -> int: + """Return the maximum varint encoding size for a value with the given number of bits.""" + return (max(bits, 1) + 6) // 7 # ceil(bits / 7), min 1 byte for varint(0) + TYPE_INFO: dict[int, TypeInfo] = {} @@ -514,8 +535,30 @@ def register_type(name: int): return func +class FixedSizeTypeMixin: + """Mixin for types with a known fixed encoded size (float, double, fixed32, fixed64).""" + + def get_max_encoded_size(self) -> int: + return self.calculate_field_id_size() + self.get_fixed_size_bytes() + + +class VarintTypeMixin: + """Mixin for varint types. Subclasses set _varint_max_bits.""" + + _varint_max_bits: int = 64 # Default to worst case + + def get_max_encoded_size(self) -> int: + max_val = self.max_value + if max_val is not None: + return self.calculate_field_id_size() + _varint_max_size( + max_val.bit_length() if max_val > 0 else 1 + ) + return self.calculate_field_id_size() + _varint_max_size(self._varint_max_bits) + + @register_type(1) -class DoubleType(TypeInfo): +class DoubleType(FixedSizeTypeMixin, TypeInfo): + # Unsupported but defined for completeness cpp_type = "double" default_value = "0.0" decode_64bit = "value.as_double()" @@ -541,7 +584,7 @@ class DoubleType(TypeInfo): @register_type(2) -class FloatType(TypeInfo): +class FloatType(FixedSizeTypeMixin, TypeInfo): cpp_type = "float" default_value = "0.0f" decode_32bit = "value.as_float()" @@ -567,8 +610,9 @@ class FloatType(TypeInfo): @register_type(3) -class Int64Type(TypeInfo): +class Int64Type(VarintTypeMixin, TypeInfo): cpp_type = "int64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "static_cast<int64_t>(value)" encode_func = "encode_int64" @@ -587,8 +631,9 @@ class Int64Type(TypeInfo): @register_type(4) -class UInt64Type(TypeInfo): +class UInt64Type(VarintTypeMixin, TypeInfo): cpp_type = "uint64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "value" encode_func = "encode_uint64" @@ -607,8 +652,9 @@ class UInt64Type(TypeInfo): @register_type(5) -class Int32Type(TypeInfo): +class Int32Type(VarintTypeMixin, TypeInfo): cpp_type = "int32_t" + _varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf default_value = "0" decode_varint = "static_cast<int32_t>(value)" encode_func = "encode_int32" @@ -627,7 +673,7 @@ class Int32Type(TypeInfo): @register_type(6) -class Fixed64Type(TypeInfo): +class Fixed64Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "uint64_t" default_value = "0" decode_64bit = "value.as_fixed64()" @@ -653,7 +699,7 @@ class Fixed64Type(TypeInfo): @register_type(7) -class Fixed32Type(TypeInfo): +class Fixed32Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "uint32_t" default_value = "0" decode_32bit = "value.as_fixed32()" @@ -689,7 +735,8 @@ class Fixed32Type(TypeInfo): @register_type(8) -class BoolType(TypeInfo): +class BoolType(VarintTypeMixin, TypeInfo): + _varint_max_bits = 1 cpp_type = "bool" default_value = "false" decode_varint = "value != 0" @@ -807,6 +854,16 @@ class StringType(TypeInfo): def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string + def get_max_encoded_size(self) -> int | None: + max_len = self.max_data_length + if max_len is not None: + return ( + self.calculate_field_id_size() + + _varint_max_size(max_len.bit_length()) + + max_len + ) + return None # Unbounded + @register_type(11) class MessageType(TypeInfo): @@ -1122,6 +1179,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase): def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string + def get_max_encoded_size(self) -> int | None: + max_len = self.max_data_length + if max_len is not None: + return ( + self.calculate_field_id_size() + + _varint_max_size(max_len.bit_length()) + + max_len + ) + return None + class PackedBufferTypeInfo(TypeInfo): """Type for packed repeated fields that expose raw buffer instead of decoding. @@ -1299,14 +1366,23 @@ class FixedArrayBytesType(TypeInfo): self.calculate_field_id_size() + 1 + 31 ) # field ID + length byte + typical 31 bytes + def get_max_encoded_size(self) -> int: + # field_id + varint(array_size) + array_size + return ( + self.calculate_field_id_size() + + _varint_max_size(self.array_size.bit_length()) + + self.array_size + ) + @property def wire_type(self) -> WireType: return WireType.LENGTH_DELIMITED @register_type(13) -class UInt32Type(TypeInfo): +class UInt32Type(VarintTypeMixin, TypeInfo): cpp_type = "uint32_t" + _varint_max_bits = 32 default_value = "0" decode_varint = "value" encode_func = "encode_uint32" @@ -1328,7 +1404,9 @@ class UInt32Type(TypeInfo): @register_type(14) -class EnumType(TypeInfo): +class EnumType(VarintTypeMixin, TypeInfo): + _varint_max_bits = 32 + @property def cpp_type(self) -> str: return f"enums::{self._field.type_name[1:]}" @@ -1379,7 +1457,7 @@ class EnumType(TypeInfo): @register_type(15) -class SFixed32Type(TypeInfo): +class SFixed32Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "int32_t" default_value = "0" decode_32bit = "value.as_sfixed32()" @@ -1405,7 +1483,7 @@ class SFixed32Type(TypeInfo): @register_type(16) -class SFixed64Type(TypeInfo): +class SFixed64Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "int64_t" default_value = "0" decode_64bit = "value.as_sfixed64()" @@ -1431,8 +1509,9 @@ class SFixed64Type(TypeInfo): @register_type(17) -class SInt32Type(TypeInfo): +class SInt32Type(VarintTypeMixin, TypeInfo): cpp_type = "int32_t" + _varint_max_bits = 32 # zigzag encoding keeps it 32-bit default_value = "0" decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" encode_func = "encode_sint32" @@ -1451,8 +1530,9 @@ class SInt32Type(TypeInfo): @register_type(18) -class SInt64Type(TypeInfo): +class SInt64Type(VarintTypeMixin, TypeInfo): cpp_type = "int64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "decode_zigzag64(value)" encode_func = "encode_sint64" @@ -1500,6 +1580,91 @@ def _generate_array_dump_content( return o +def _is_inline_encode(sub_msg_name: str) -> bool: + """Check if a sub-message type has the (inline_encode) option set.""" + sub_desc = _message_desc_map.get(sub_msg_name) + if not sub_desc: + return False + inline_opt = getattr(pb, "inline_encode", None) + if inline_opt is None: + return False + return get_opt(sub_desc, inline_opt, False) + + +def _generate_inline_encode_block( + field_number: int, sub_msg_name: str, element: str +) -> str: + """Generate inline encode code for a sub-message with (inline_encode) = true. + + Instead of calling encode_sub_message (function pointer indirection), + this inlines the sub-message's field encoding directly. Uses 1-byte + backpatch for the length (validated to be < 128 at generation time). + + Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement + on complex element expressions. + + Args: + field_number: The parent field number for this sub-message + sub_msg_name: The sub-message type name + element: C++ expression for the element (e.g., "it" or "this->field[i]") + """ + sub_desc = _message_desc_map[sub_msg_name] + tag = (field_number << 3) | 2 # wire type 2 = LENGTH_DELIMITED + assert tag < 128, f"inline_encode requires single-byte tag, got {tag}" + + lines = [] + lines.append(f"auto &sub_msg = {element};") + lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});") + lines.append("uint8_t *len_pos = pos;") + lines.append("ProtoEncode::reserve_byte(pos);") + + # Generate inline field encoding for each sub-message field + for field in sub_desc.field: + if field.options.deprecated: + continue + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + encode_line = ti.encode_content + # Replace this-> with sub_msg reference for the sub-message fields + encode_line = encode_line.replace("this->", "sub_msg.") + lines.extend(wrap_with_ifdef(encode_line, get_field_opt(field, pb.field_ifdef))) + + lines.append("*len_pos = static_cast<uint8_t>(pos - len_pos - 1);") + return "\n".join(lines) + + +def _generate_inline_size_block( + field_number: int, sub_msg_name: str, element: str +) -> str: + """Generate inline size calculation for a sub-message with (inline_encode) = true. + + Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement + on complex element expressions like 'this->advertisements[i]'. + + Args: + field_number: The parent field number for this sub-message + sub_msg_name: The sub-message type name + element: C++ expression for the element + """ + sub_desc = _message_desc_map[sub_msg_name] + + lines = [] + lines.append(f"auto &sub_msg = {element};") + # 1 byte tag + 1 byte length (guaranteed < 128 by validation) + lines.append("size += 2;") + + for field in sub_desc.field: + if field.options.deprecated: + continue + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + force = get_field_opt(field, pb.force, False) + size_line = ti.get_size_calculation(f"sub_msg.{ti.field_name}", force) + # Replace hardcoded this-> references (e.g., FixedArrayBytesType uses this->field_len) + size_line = size_line.replace("this->", "sub_msg.") + lines.extend(wrap_with_ifdef(size_line, get_field_opt(field, pb.field_ifdef))) + + return "\n".join(lines) + + class FixedArrayRepeatedType(TypeInfo): """Special type for fixed-size repeated fields using std::array. @@ -1526,6 +1691,10 @@ class FixedArrayRepeatedType(TypeInfo): return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): + if _is_inline_encode(self._ti.cpp_type): + return _generate_inline_encode_block( + self.number, self._ti.cpp_type, element + ) return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" return ( f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" @@ -1633,8 +1802,19 @@ class FixedArrayRepeatedType(TypeInfo): ] return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}" + is_inline = isinstance(self._ti, MessageType) and _is_inline_encode( + self._ti.cpp_type + ) + # When using a define, always use loop-based approach if self.is_define: + if is_inline: + o = f"for (const auto &it : {name}) {{\n" + o += indent( + _generate_inline_size_block(self.number, self._ti.cpp_type, "it") + ) + o += "\n}" + return o o = f"for (const auto &it : {name}) {{\n" o += f" {self._ti.get_size_calculation('it', True)}\n" o += "}" @@ -1642,6 +1822,14 @@ class FixedArrayRepeatedType(TypeInfo): # For fixed arrays, we always encode all elements + if is_inline: + o = f"for (const auto &it : {name}) {{\n" + o += indent( + _generate_inline_size_block(self.number, self._ti.cpp_type, "it") + ) + o += "\n}" + return o + # Special case for single-element arrays - no loop needed if self.array_size == 1: return self._ti.get_size_calculation(f"{name}[0]", True) @@ -1714,6 +1902,15 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): def get_size_calculation(self, name: str, force: bool = False) -> str: # Calculate size only for active elements + if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type): + o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" + o += indent( + _generate_inline_size_block( + self.number, self._ti.cpp_type, f"{name}[i]" + ) + ) + o += "\n}" + return o o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n" o += "}" @@ -2222,6 +2419,28 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: return total_size +def calculate_message_max_size(desc: descriptor.DescriptorProto) -> int | None: + """Calculate the maximum possible encoded size for a message. + + Returns None if any field has unbounded size (e.g., variable-length strings). + Used to validate that (inline_encode) messages fit in a single-byte length varint. + """ + total_size = 0 + + for field in desc.field: + if field.options.deprecated: + continue + + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + max_size = ti.get_max_encoded_size() + if max_size is None: + return None + + total_size += max_size + + return total_size + + def build_message_type( desc: descriptor.DescriptorProto, base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]], @@ -2451,11 +2670,23 @@ def build_message_type( prot = "void decode(const uint8_t *buffer, size_t length);" public_content.append(prot) + # Check if this message uses inline_encode — if so, skip generating standalone + # encode/calculate_size methods since the encoding is inlined into the parent. + inline_opt = getattr(pb, "inline_encode", None) + is_inline_only = ( + message_id is None # Not a service message (no id) + and inline_opt is not None + and get_opt(desc, inline_opt, False) + ) + # Only generate encode method if this message needs encoding and has fields - if needs_encode and encode: + if needs_encode and encode and not is_inline_only: # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls encode_debug = [ - line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode + line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,").replace( + "(pos)", "(pos PROTO_ENCODE_DEBUG_ARG)" + ) + for line in encode ] o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" @@ -2470,7 +2701,7 @@ def build_message_type( # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # Add calculate_size method only if this message needs encoding and has fields - if needs_encode and size_calc: + if needs_encode and size_calc and not is_inline_only: o = f"uint32_t {desc.name}::calculate_size() const {{\n" o += " uint32_t size = 0;\n" o += indent("\n".join(size_calc)) + "\n" @@ -2830,6 +3061,32 @@ def main() -> None: if not enum.options.deprecated and enum.value: _enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value) + # Build message descriptor map for inline_encode lookups + mt = file.message_type + _message_desc_map.update({m.name: m for m in mt if not m.options.deprecated}) + + # Validate inline_encode messages fit in single-byte length varint + inline_encode_opt = getattr(pb, "inline_encode", None) + if inline_encode_opt is not None: + for m in mt: + if m.options.deprecated: + continue + if not get_opt(m, inline_encode_opt, False): + continue + max_size = calculate_message_max_size(m) + if max_size is None: + raise ValueError( + f"Message '{m.name}' has (inline_encode) = true but contains " + f"fields with unbounded size. Inline encoding requires all " + f"fields to have bounded maximum size." + ) + if max_size >= 128: + raise ValueError( + f"Message '{m.name}' has (inline_encode) = true but max " + f"encoded size is {max_size} bytes (>= 128). Inline encoding " + f"requires sub-messages that fit in a single-byte length varint." + ) + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( build_type_usage_map(file) @@ -3048,8 +3305,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint content += "\n} // namespace enums\n\n" - mt = file.message_type - # Identify empty SOURCE_CLIENT messages that don't need class generation for m in mt: if m.options.deprecated: From d3591c8d9e3724e21a508c58d4f4fe7459e17aa9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 10 Apr 2026 21:21:26 +0200 Subject: [PATCH 1366/2056] [micro_wake_word] Pin esp-nn version (#15628) --- .../components/micro_wake_word/__init__.py | 2 + .../micro_wake_word/streaming_model.cpp | 92 +++++++++++++++++-- .../micro_wake_word/streaming_model.h | 5 + 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index ff27dec6df0..de95e4961b9 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -451,6 +451,8 @@ async def to_code(config): ota.request_ota_state_listeners() esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") + # Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn) + esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 0ab6cd3772d..e761e4866fd 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -29,14 +29,6 @@ void VADModel::log_model_config() { bool StreamingModel::load_model_() { RAMAllocator<uint8_t> arena_allocator; - if (this->tensor_arena_ == nullptr) { - this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); - if (this->tensor_arena_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); - return false; - } - } - if (this->var_arena_ == nullptr) { this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE); if (this->var_arena_ == nullptr) { @@ -53,6 +45,26 @@ bool StreamingModel::load_model_() { return false; } + // Probe for the actual required tensor arena size if not yet determined + if (!this->tensor_arena_size_probed_) { + size_t probed_size = this->probe_arena_size_(); + if (probed_size > 0) { + ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size); + this->tensor_arena_size_ = probed_size; + } else { + ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_); + } + this->tensor_arena_size_probed_ = true; + } + + if (this->tensor_arena_ == nullptr) { + this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); + if (this->tensor_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); + return false; + } + } + if (this->interpreter_ == nullptr) { this->interpreter_ = make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_, @@ -94,6 +106,70 @@ bool StreamingModel::load_model_() { return true; } +size_t StreamingModel::probe_arena_size_() { + RAMAllocator<uint8_t> arena_allocator; + + // Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different + // versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct, + // and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16 + // bytes. + size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15, + (this->tensor_arena_size_ * 2 + 15) & ~15}; + + for (size_t attempt_size : attempt_sizes) { + uint8_t *probe_arena = arena_allocator.allocate(attempt_size); + if (probe_arena == nullptr) { + continue; + } + + // Verify the model works at all with this arena size + auto probe_interpreter = make_unique<tflite::MicroInterpreter>( + tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_); + + if (probe_interpreter->AllocateTensors() != kTfLiteOk) { + probe_interpreter.reset(); + arena_allocator.deallocate(probe_arena, attempt_size); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + continue; + } + + // Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment). + // If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds. + size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15; + probe_interpreter.reset(); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + + size_t upper = attempt_size; + + while (lower < upper) { + auto test_interpreter = make_unique<tflite::MicroInterpreter>( + tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_); + + bool ok = test_interpreter->AllocateTensors() == kTfLiteOk; + + test_interpreter.reset(); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + + if (ok) { + // Found a working size smaller than the full arena + upper = lower + 16; // Pad by 16 bytes to be safe for future allocations + break; + } + + // Try the midpoint between current attempt and full size + lower = ((lower + upper) / 2 + 15) & ~15; + } + + arena_allocator.deallocate(probe_arena, attempt_size); + return upper; + } + + return 0; +} + void StreamingModel::unload_model() { this->interpreter_.reset(); diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index 0811bfb19b8..fc9eeb5e2d8 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -63,6 +63,10 @@ class StreamingModel { /// @brief Allocates tensor and variable arenas and sets up the model interpreter /// @return True if successful, false otherwise bool load_model_(); + /// @brief Probes the actual required tensor arena size by trial allocation. + /// Tries the manifest size first, then 2x if that fails. + /// @return The required arena size rounded up to 16-byte alignment, or 0 on failure. + size_t probe_arena_size_(); /// @brief Returns true if successfully registered the streaming model's TensorFlow operations bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver); @@ -70,6 +74,7 @@ class StreamingModel { bool loaded_{false}; bool enabled_{true}; + bool tensor_arena_size_probed_{false}; bool unprocessed_probability_status_{false}; uint8_t current_stride_step_{0}; int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION}; From 2c610abcd010bfa0fbdae5e52f7ca83cca8b1bfb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:19:52 -1000 Subject: [PATCH 1367/2056] Bump resvg-py from 0.2.6 to 0.3.0 (#15629) 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 d7db44454cd..8f8ada561a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==12.2.0 -resvg-py==0.2.6 +resvg-py==0.3.0 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 From ae96f82b824682da5d855b8883fc76520b9f0276 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:20:04 -1000 Subject: [PATCH 1368/2056] Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#15631) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 6d200956e9d..677032b7fab 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf9fa8e7c05..2240879bd24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -904,7 +904,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: memory-analysis-target path: memory-analysis-target.json @@ -969,7 +969,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: memory-analysis-pr path: memory-analysis-pr.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e8a040888d..12d2ce30aa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests From 395610c117bd9358c77f0903890f27c0c65ccf65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:20:17 -1000 Subject: [PATCH 1369/2056] Bump docker/build-push-action from 7.0.0 to 7.1.0 in /.github/actions/build-image (#15633) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index a8952260303..52d72544d3e 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 1dfeef0265c7fe7a6bf322a3d79b08aeb1926c2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:20:43 -1000 Subject: [PATCH 1370/2056] Bump actions/github-script from 8.0.0 to 9.0.0 (#15632) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 4 ++-- .github/workflows/ci-clang-tidy-hash.yml | 4 ++-- .github/workflows/codeowner-approved-label-update.yml | 2 +- .github/workflows/codeowner-review-request.yml | 2 +- .github/workflows/external-component-bot.yml | 2 +- .github/workflows/issue-codeowner-notify.yml | 2 +- .github/workflows/pr-title-check.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/status-check-labels.yml | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 3b5e9f0d158..27ddfe59118 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -33,7 +33,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - name: Auto Label PR - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 677032b7fab..e5143911d94 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -47,7 +47,7 @@ jobs: fi - if: failure() name: Review PR - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | await github.rest.pulls.createReview({ @@ -70,7 +70,7 @@ jobs: esphome/components/api/api_pb2_service.* - if: success() name: Dismiss review - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 7905739b15e..40cdff0cba7 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -42,7 +42,7 @@ jobs: - if: failure() && github.event.pull_request.head.repo.full_name == github.repository name: Request changes - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | await github.rest.pulls.createReview({ @@ -55,7 +55,7 @@ jobs: - if: success() && github.event.pull_request.head.repo.full_name == github.repository name: Dismiss review - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 34ff934b773..49653b6fb3b 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -34,7 +34,7 @@ jobs: CODEOWNERS - name: Check codeowner approval and update label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: PR_NUMBER: ${{ github.event.pull_request.number }} with: diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index a89c03ba042..76be6ecd7bf 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -33,7 +33,7 @@ jobs: ref: ${{ github.event.pull_request.base.sha }} - name: Request reviews from component codeowners - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 4fa020f63d5..3165b170780 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add external component comment - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 6faf956c876..b211c13985d 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify codeowners for component issues - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 0021654def6..87009962719 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12d2ce30aa7..c92581b49b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -229,7 +229,7 @@ jobs: repositories: home-assistant-addon - name: Trigger Workflow - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | @@ -264,7 +264,7 @@ jobs: repositories: esphome-schema - name: Trigger Workflow - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | @@ -295,7 +295,7 @@ jobs: repositories: version-notifier - name: Trigger Workflow - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index 6483bbe789d..709342e5ae6 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check for blocking labels - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr']; From e1a813e11fc26d9ebc86d6e7fe0e6a7d38f00f30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:21:01 -1000 Subject: [PATCH 1371/2056] Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 (#15630) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index a71e5ef4cad..be1457387d9 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -41,7 +41,7 @@ jobs: python script/run-in-env.py pre-commit run --all-files - name: Commit changes - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot <esphome@openhomefoundation.org> From a7c5b0ab466d6f75279cd8228036856fb118cb2a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:26:09 -0400 Subject: [PATCH 1372/2056] [sx127x][cc1101][sx126x] Use GPIO interrupt to wake loop (#15627) --- esphome/components/cc1101/cc1101.cpp | 25 ++++++++++++++++--------- esphome/components/cc1101/cc1101.h | 1 + esphome/components/sx126x/sx126x.cpp | 9 +++++++++ esphome/components/sx126x/sx126x.h | 2 ++ esphome/components/sx127x/sx127x.cpp | 9 ++++----- esphome/components/sx127x/sx127x.h | 2 ++ 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index f2b7451721a..c231f314cc2 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -102,6 +102,8 @@ CC1101Component::CC1101Component() { memset(this->pa_table_, 0, sizeof(this->pa_table_)); } +void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); } + void CC1101Component::setup() { this->spi_setup(); this->cs_->digital_write(true); @@ -148,11 +150,12 @@ void CC1101Component::setup() { // Defer pin mode setup until after all components have completed setup() // This handles the case where remote_transmitter runs after CC1101 and changes pin mode if (this->gdo0_pin_ != nullptr) { - this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); }); - } - - if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { - this->disable_loop(); + this->defer([this]() { + this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); + if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } + }); } } @@ -164,6 +167,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float } void CC1101Component::loop() { + this->disable_loop(); if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr || !this->gdo0_pin_->digital_read()) { return; @@ -244,6 +248,7 @@ void CC1101Component::begin_tx() { this->write_(Register::PKTCTRL0, 0x32); ESP_LOGV(TAG, "Beginning TX sequence"); if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->detach_interrupt(); this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can @@ -673,10 +678,12 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { - if (value) { - this->enable_loop(); - } else { - this->disable_loop(); + if (this->gdo0_pin_ != nullptr) { + if (value) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } else { + this->gdo0_pin_->detach_interrupt(); + } } this->write_(Register::PKTCTRL0); this->write_(Register::PKTCTRL1); diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 2efd9e082d5..68d81ac8f3c 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -93,6 +93,7 @@ class CC1101Component : public Component, // GDO pin for packet reception InternalGPIOPin *gdo0_pin_{nullptr}; + static void IRAM_ATTR gpio_intr(CC1101Component *arg); // Packet handling void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index ec62fad10af..6ea09e3a9e8 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -104,11 +104,17 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) { delayMicroseconds(SWITCHING_DELAY_US); } +void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); } + void SX126x::setup() { // setup pins this->busy_pin_->setup(); this->rst_pin_->setup(); this->dio1_pin_->setup(); + if (this->dio1_pin_->is_internal()) { + static_cast<InternalGPIOPin *>(this->dio1_pin_) + ->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } // start spi this->spi_setup(); @@ -348,6 +354,9 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo } void SX126x::loop() { + if (this->dio1_pin_->is_internal()) { + this->disable_loop(); + } if (!this->dio1_pin_->digital_read()) { return; } diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index a758d637955..edc00e3727e 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -3,6 +3,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "sx126x_reg.h" #include <utility> #include <vector> @@ -100,6 +101,7 @@ class SX126x : public Component, Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: + static void IRAM_ATTR gpio_intr(SX126x *arg); void configure_fsk_ook_(); void configure_lora_(); void set_packet_params_(uint8_t payload_length); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 83be96767a4..2b13efb38da 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -53,6 +53,8 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) { this->disable(); } +void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); } + void SX127x::setup() { // setup reset this->rst_pin_->setup(); @@ -60,6 +62,7 @@ void SX127x::setup() { // setup dio0 if (this->dio0_pin_) { this->dio0_pin_->setup(); + this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); } // start spi @@ -313,6 +316,7 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo } void SX127x::loop() { + this->disable_loop(); if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) { return; } @@ -386,11 +390,6 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { return; } } - if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) { - this->enable_loop(); - } else { - this->disable_loop(); - } } void SX127x::set_mode_rx() { diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index be7b6d8d9f8..76f942fddac 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -4,6 +4,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include <vector> namespace esphome { @@ -86,6 +87,7 @@ class SX127x : public Component, Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: + static void IRAM_ATTR gpio_intr(SX127x *arg); void configure_fsk_ook_(); void configure_lora_(); void set_mode_(uint8_t modulation, uint8_t mode); From 40081e5ae723e15870d56f5464a8e559dd71599c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 10 Apr 2026 13:13:05 -1000 Subject: [PATCH 1373/2056] [rp2040] Fix W5500 Ethernet pbuf corruption by mirroring LWIPMutex semantics (#15624) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/rp2040/helpers.cpp | 31 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index 8cb5f7c18df..6e5ddad2364 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -9,7 +9,7 @@ #include <WiFi.h> #include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) -#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock) +#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below) #include "esphome/components/ethernet/ethernet_component.h" #endif #include <hardware/structs/rosc.h> @@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } // main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat // assertion failures). See esphome#10681. // -// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end. -// Both acquire the async_context recursive mutex to prevent IRQ callbacks from -// firing during critical sections. +// WiFi uses cyw43_arch_lwip_begin/end. +// +// For wired Ethernet, taking only the async_context lock is NOT enough. The +// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP` +// counter to decide whether to defer packet processing. If we hold the +// async_context lock without bumping `__inLWIP`, an interrupt-driven packet +// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat` +// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's +// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the +// lock, and on release re-unmask any GPIO IRQs that were deferred while we +// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because +// pulling lwip_wrap.h there poisons many TUs with lwIP types. // // When neither WiFi nor Ethernet is configured, this is a no-op since // there's no network stack and no lwip callbacks to race with. @@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); } LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); } #elif defined(USE_ETHERNET) -LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); } -LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); } +LwIPLock::LwIPLock() { + __inLWIP++; + ethernet_arch_lwip_begin(); +} +LwIPLock::~LwIPLock() { + ethernet_arch_lwip_end(); + __inLWIP--; + if (__needsIRQEN && !__inLWIP) { + __needsIRQEN = false; + ethernet_arch_lwip_gpio_unmask(); + } +} #else LwIPLock::LwIPLock() {} LwIPLock::~LwIPLock() {} From 5460ee7edde319ab3c3c2a79a4438321fdd86592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:55:15 -1000 Subject: [PATCH 1374/2056] Bump aioesphomeapi from 44.13.1 to 44.13.2 (#15637) 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 8f8ada561a3..12e7658e432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.1 +aioesphomeapi==44.13.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 2001b912807e72df923eb7c6eccb15c41c6f0e65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:57:39 +0000 Subject: [PATCH 1375/2056] Bump resvg-py from 0.3.0 to 0.3.1 (#15640) 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 12e7658e432..361a628919c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==12.2.0 -resvg-py==0.3.0 +resvg-py==0.3.1 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 From c2af4874f952d3685c33f42e4bda7156497d2075 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:58:20 +0000 Subject: [PATCH 1376/2056] Bump aioesphomeapi from 44.13.2 to 44.13.3 (#15641) 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 361a628919c..1466eccc9b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.2 +aioesphomeapi==44.13.3 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 6e6786451061b190bdf5a8ade02ac113ffd30c05 Mon Sep 17 00:00:00 2001 From: Farmer-shin <olaf.wut@web.de> Date: Sat, 11 Apr 2026 13:27:25 +0200 Subject: [PATCH 1377/2056] [epaper_spi] Add Waveshare 3.97inch E-Paper Display (#15466) --- esphome/components/epaper_spi/models/ssd1677.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py index f7e012f1620..bad33a6a023 100644 --- a/esphome/components/epaper_spi/models/ssd1677.py +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -43,3 +43,11 @@ wave_4_26.extend( }, }, ) + + +ssd1677.extend( + "waveshare-3.97in", + width=800, + height=480, + mirror_x=True, +) From bef4c8a86c275c0c64bfe230c96a77fe5dd87835 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:36:27 -0400 Subject: [PATCH 1378/2056] [cc1101] Extract chip configuration into configure() method (#15635) --- esphome/components/cc1101/cc1101.cpp | 43 +++++++++++++++++----------- esphome/components/cc1101/cc1101.h | 1 + 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index c231f314cc2..ea0138e1dd8 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -106,6 +106,30 @@ void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_lo void CC1101Component::setup() { this->spi_setup(); + + if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->setup(); + } + + this->configure(); + if (this->is_failed()) { + return; + } + + // Defer pin mode setup until after all components have completed setup() + // This handles the case where remote_transmitter runs after CC1101 and changes pin mode + if (this->gdo0_pin_ != nullptr) { + this->defer([this]() { + this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); + if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } + }); + } +} + +void CC1101Component::configure() { + // Manual reset sequence per CC1101 datasheet section 19.1.2 this->cs_->digital_write(true); delayMicroseconds(1); this->cs_->digital_write(false); @@ -128,11 +152,6 @@ void CC1101Component::setup() { return; } - // Setup GDO0 pin if configured - if (this->gdo0_pin_ != nullptr) { - this->gdo0_pin_->setup(); - } - this->initialized_ = true; for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) { @@ -142,21 +161,11 @@ void CC1101Component::setup() { this->write_(static_cast<Register>(i)); } this->set_output_power(this->output_power_requested_); + if (!this->enter_rx_()) { this->mark_failed(); return; } - - // Defer pin mode setup until after all components have completed setup() - // This handles the case where remote_transmitter runs after CC1101 and changes pin mode - if (this->gdo0_pin_ != nullptr) { - this->defer([this]() { - this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); - if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { - this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); - } - }); - } } void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) { @@ -273,7 +282,7 @@ void CC1101Component::begin_rx() { void CC1101Component::reset() { this->strobe_(Command::RES); - this->setup(); + this->configure(); } void CC1101Component::set_idle() { diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 68d81ac8f3c..000a13d586c 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -25,6 +25,7 @@ class CC1101Component : public Component, void setup() override; void loop() override; void dump_config() override; + void configure(); // Actions void begin_tx(); From e6318a2d1690ffee8b2729b8412f970f5452aba3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:54:30 -0400 Subject: [PATCH 1379/2056] [mdns] Bump espressif/mdns to 1.11.0 (#15670) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 79d355e8ae9..7c36295e8d9 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -170,7 +170,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.10.0") + add_idf_component(name="espressif/mdns", ref="1.11.0") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1e40fef2dc3..bf42730e673 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -14,7 +14,7 @@ dependencies: espressif/esp32-camera: version: 2.1.6 espressif/mdns: - version: 1.10.0 + version: 1.11.0 espressif/esp_wifi_remote: version: 1.4.0 rules: From 45af21bf3813e10350e249513635fd5661d35954 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:58:51 -0400 Subject: [PATCH 1380/2056] [canbus] Fix canbus.send can_id compile error (#15668) --- esphome/components/canbus/__init__.py | 1 - tests/components/canbus/common.yaml | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 7d3bf78f492..fcd342ad389 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -162,7 +162,6 @@ async def canbus_action_to_code(config, action_id, template_arg, args): await cg.register_parented(var, config[CONF_CANBUS_ID]) if (can_id := config.get(CONF_CAN_ID)) is not None: - can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index 8bddeb74094..e779f7f078b 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -50,6 +50,13 @@ button: - platform: template name: Canbus Actions on_press: + - canbus.send: + can_id: 0x601 + data: [0, 1, 2] + - canbus.send: + can_id: 0x1FFFFFFF + use_extended_id: true + data: [0, 1, 2] - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; From 2f684bf4f3ac7d6e41d4bd7942ee3a3c73cdf59f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:07:04 -0400 Subject: [PATCH 1381/2056] [esp32] Bump platform to 55.03.38, Arduino to 3.3.8, ESP-IDF to 5.5.4 (#15666) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 20 ++-- esphome/components/esp32/boards.py | 16 +++ esphome/platformio_api.py | 129 +----------------------- esphome/platformio_runner.py | 114 +++++++++++++++++++++ platformio.ini | 10 +- pyproject.toml | 1 - tests/unit_tests/conftest.py | 6 +- tests/unit_tests/test_platformio_api.py | 119 +++++++--------------- 9 files changed, 192 insertions(+), 225 deletions(-) create mode 100644 esphome/platformio_runner.py diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 5c7eab517bd..cd61d9ec480 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6 +d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f27690c97b4..cd38c82dd89 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -671,11 +671,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 7), - "latest": cv.Version(3, 3, 7), - "dev": cv.Version(3, 3, 7), + "recommended": cv.Version(3, 3, 8), + "latest": cv.Version(3, 3, 8), + "dev": cv.Version(3, 3, 8), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 8): cv.Version(55, 3, 38), cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), @@ -695,6 +696,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 8): cv.Version(5, 5, 4), cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), @@ -714,17 +716,15 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3, "1"), - "latest": cv.Version(5, 5, 3, "1"), + "recommended": cv.Version(5, 5, 4), + "latest": cv.Version(5, 5, 4), "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", - cv.Version( - 5, 5, 4 - ): "https://github.com/pioarduino/platform-espressif32.git#develop", + cv.Version(5, 5, 4): cv.Version(55, 3, 38), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), @@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 37), - "latest": cv.Version(55, 3, 37), + "recommended": cv.Version(55, 3, 38), + "latest": cv.Version(55, 3, 38), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 2bd08e7c392..2c73fe7d08d 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1960,6 +1960,10 @@ BOARDS = { "name": "Hornbill ESP32 Minima", "variant": VARIANT_ESP32, }, + "huidu_hd_wf1": { + "name": "Huidu HD-WF1", + "variant": VARIANT_ESP32S2, + }, "huidu_hd_wf2": { "name": "Huidu HD-WF2", "variant": VARIANT_ESP32S3, @@ -2028,6 +2032,10 @@ BOARDS = { "name": "LilyGo T-Display-S3", "variant": VARIANT_ESP32S3, }, + "lilygo-t-energy-s3": { + "name": "LilyGo T-Energy-S3", + "variant": VARIANT_ESP32S3, + }, "lilygo-t3-s3": { "name": "LilyGo T3-S3", "variant": VARIANT_ESP32S3, @@ -2289,10 +2297,18 @@ BOARDS = { "name": "S.ODI Ultra v1", "variant": VARIANT_ESP32, }, + "seeed_xiao_esp32_s3_plus": { + "name": "Seeed Studio XIAO ESP32S3 Plus", + "variant": VARIANT_ESP32S3, + }, "seeed_xiao_esp32c3": { "name": "Seeed Studio XIAO ESP32C3", "variant": VARIANT_ESP32C3, }, + "seeed_xiao_esp32c5": { + "name": "Seeed Studio XIAO ESP32C5", + "variant": VARIANT_ESP32C5, + }, "seeed_xiao_esp32c6": { "name": "Seeed Studio XIAO ESP32C6", "variant": VARIANT_ESP32C6, diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index cb080b2a953..e9719f7dcd8 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,104 +5,15 @@ import os from pathlib import Path import re import subprocess -import time -from typing import Any +import sys from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError -from esphome.util import run_external_command, run_external_process +from esphome.util import run_external_process _LOGGER = logging.getLogger(__name__) -def patch_structhash(): - # Patch platformio's structhash to not recompile the entire project when files are - # removed/added. This might have unintended consequences, but this improves compile - # times greatly when adding/removing components and a simple clean build solves - # all issues - from platformio.run import cli, helpers - - def patched_clean_build_dir(build_dir, *args): - from platformio import fs - from platformio.project.helpers import get_project_dir - - platformio_ini = Path(get_project_dir()) / "platformio.ini" - - build_dir = Path(build_dir) - - # if project's config is modified - if ( - build_dir.is_dir() - and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime - ): - fs.rmtree(build_dir) - - if not build_dir.is_dir(): - build_dir.mkdir(parents=True) - - helpers.clean_build_dir = patched_clean_build_dir - cli.clean_build_dir = patched_clean_build_dir - - -def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors. - - PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry - for 502/503 errors. We add retries with exponential backoff and close the - session between attempts to force a fresh TCP connection, which may route - to a different CDN edge node. - """ - from platformio.package.download import FileDownloader - from platformio.package.exception import PackageException - - if getattr(FileDownloader.__init__, "_esphome_patched", False): - return - - original_init = FileDownloader.__init__ - - def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 5 - - for attempt in range(max_retries): - try: - original_init(self, *args, **kwargs) - return - except PackageException as e: - if attempt < max_retries - 1: - # Exponential backoff: 2, 4, 8, 16 seconds - delay = 2 ** (attempt + 1) - _LOGGER.warning( - "Package download failed: %s. " - "Retrying in %d seconds... (attempt %d/%d)", - str(e), - delay, - attempt + 1, - max_retries, - ) - # Close the response and session to free resources - # and force a new TCP connection on retry, which may - # route to a different CDN edge node - # pylint: disable=protected-access,broad-except - try: - if ( - hasattr(self, "_http_response") - and self._http_response is not None - ): - self._http_response.close() - if hasattr(self, "_http_session"): - self._http_session.close() - except Exception: - pass - # pylint: enable=protected-access,broad-except - time.sleep(delay) - else: - # Final attempt - re-raise - raise - - patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access - FileDownloader.__init__ = patched_init - - IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" FILTER_PLATFORMIO_LINES = [ r"Verbose mode can be enabled via `-v, --verbose` option.*", @@ -142,20 +53,6 @@ FILTER_PLATFORMIO_LINES = [ ] -class PlatformioLogFilter(logging.Filter): - """Filter to suppress noisy platformio log messages.""" - - _PATTERN = re.compile( - r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES) - ) - - def filter(self, record: logging.LogRecord) -> bool: - # Only filter messages from platformio-related loggers - if "platformio" not in record.name.lower(): - return True - return self._PATTERN.match(record.getMessage()) is None - - def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -166,30 +63,12 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") # Increase uv retry count to handle transient network errors (default is 3) os.environ.setdefault("UV_HTTP_RETRIES", "10") - cmd = ["platformio"] + list(args) + cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) if not CORE.verbose: kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES - if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None: - return run_external_process(*cmd, **kwargs) - - import platformio.__main__ - - patch_structhash() - patch_file_downloader() - - # Add log filter to suppress noisy platformio messages - log_filter = PlatformioLogFilter() if not CORE.verbose else None - if log_filter: - for handler in logging.getLogger().handlers: - handler.addFilter(log_filter) - try: - return run_external_command(platformio.__main__.main, *cmd, **kwargs) - finally: - if log_filter: - for handler in logging.getLogger().handlers: - handler.removeFilter(log_filter) + return run_external_process(*cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py new file mode 100644 index 00000000000..408d49d1a6f --- /dev/null +++ b/esphome/platformio_runner.py @@ -0,0 +1,114 @@ +"""Subprocess entry point that applies ESPHome's PlatformIO patches. + +Invoked via ``python -m esphome.platformio_runner`` instead of +``python -m platformio`` so that the patches (incremental rebuild +preservation, download retries) apply inside the subprocess. Running +PlatformIO in a subprocess keeps its ``sys.path`` mutations and other +global state from leaking into the ESPHome process. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +import sys +import time +from typing import Any + +_LOGGER = logging.getLogger(__name__) + + +def patch_structhash() -> None: + """Avoid full rebuilds when files are added or removed. + + PlatformIO clears the build dir whenever its structure hash changes. + We replace that with an mtime check against ``platformio.ini`` so + incremental builds are preserved unless the project config changed. + """ + from platformio.run import cli, helpers + + def patched_clean_build_dir(build_dir, *_args): + from platformio import fs + from platformio.project.helpers import get_project_dir + + platformio_ini = Path(get_project_dir()) / "platformio.ini" + build_dir = Path(build_dir) + + if ( + build_dir.is_dir() + and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime + ): + fs.rmtree(build_dir) + + if not build_dir.is_dir(): + build_dir.mkdir(parents=True) + + helpers.clean_build_dir = patched_clean_build_dir + cli.clean_build_dir = patched_clean_build_dir + + +def patch_file_downloader() -> None: + """Retry PlatformIO package downloads with exponential backoff. + + PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in + retry for 502/503 errors. We wrap ``__init__`` to retry on + ``PackageException`` and close the session between attempts so a new + TCP connection can route to a different CDN edge node. + """ + from platformio.package.download import FileDownloader + from platformio.package.exception import PackageException + + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + + original_init = FileDownloader.__init__ + + def patched_init(self, *args: Any, **kwargs: Any) -> None: + max_retries = 5 + + for attempt in range(max_retries): + try: + original_init(self, *args, **kwargs) + return + except PackageException as e: + if attempt < max_retries - 1: + delay = 2 ** (attempt + 1) + _LOGGER.warning( + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", + str(e), + delay, + attempt + 1, + max_retries, + ) + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) + else: + raise + + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access + FileDownloader.__init__ = patched_init + + +def main() -> int: + patch_structhash() + patch_file_downloader() + + import platformio.__main__ + + return platformio.__main__.main() or 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/platformio.ini b/platformio.ini index e0f7c7d443a..7d17628a8ff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,10 +133,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -169,9 +169,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz framework = espidf lib_deps = diff --git a/pyproject.toml b/pyproject.toml index 2e3a247768b..a744286e888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ classifiers = [ "Topic :: Home Automation", ] -# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76 requires-python = ">=3.11.0,<3.15" dynamic = ["dependencies", "optional-dependencies", "version"] diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 1a1bfffd03d..dfd4305c4d4 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -84,9 +84,9 @@ def mock_decode_pc() -> Generator[Mock, None, None]: @pytest.fixture -def mock_run_external_command() -> Generator[Mock, None, None]: - """Mock run_external_command for platformio_api.""" - with patch("esphome.platformio_api.run_external_command") as mock: +def mock_run_external_process() -> Generator[Mock, None, None]: + """Mock run_external_process for platformio_api.""" + with patch("esphome.platformio_api.run_external_process") as mock: yield mock diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index e1b3908c249..ddc4e45c844 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -1,7 +1,8 @@ """Tests for platformio_api.py path functions.""" +# pylint: disable=protected-access + import json -import logging import os from pathlib import Path import shutil @@ -10,7 +11,7 @@ from unittest.mock import MagicMock, Mock, call, patch import pytest -from esphome import platformio_api +from esphome import platformio_api, platformio_runner from esphome.core import CORE, EsphomeError @@ -281,13 +282,13 @@ def test_run_idedata_raises_on_invalid_json( def test_run_platformio_cli_sets_environment_variables( - setup_core: Path, mock_run_external_command: Mock + setup_core: Path, mock_run_external_process: Mock ) -> None: """Test run_platformio_cli sets correct environment variables.""" CORE.build_path = str(setup_core / "build" / "test") with patch.dict(os.environ, {}, clear=False): - mock_run_external_command.return_value = 0 + mock_run_external_process.return_value = 0 platformio_api.run_platformio_cli("test", "arg") # Check environment variables were set @@ -300,10 +301,12 @@ def test_run_platformio_cli_sets_environment_variables( assert "PLATFORMIO_LIBDEPS_DIR" in os.environ assert "PYTHONWARNINGS" in os.environ - # Check command was called correctly - mock_run_external_command.assert_called_once() - args = mock_run_external_command.call_args[0] - assert "platformio" in args + # Check command was called correctly — runs PlatformIO as a subprocess + # via the esphome.platformio_runner entry point. + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert "-m" in args + assert "esphome.platformio_runner" in args assert "test" in args assert "arg" in args @@ -444,7 +447,7 @@ def test_patch_structhash(setup_core: Path) -> None: }, ): # Call patch_structhash - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Verify both modules had clean_build_dir patched # Check that clean_build_dir was set on both modules @@ -496,7 +499,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -546,7 +549,7 @@ def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -594,7 +597,7 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -719,7 +722,7 @@ def test_patch_file_downloader_succeeds_first_try() -> None: ), }, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -758,7 +761,7 @@ def test_patch_file_downloader_retries_on_failure() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -799,7 +802,7 @@ def test_patch_file_downloader_raises_after_max_retries() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -847,7 +850,7 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() -> ), patch("time.sleep"), ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -882,9 +885,9 @@ def test_patch_file_downloader_idempotent() -> None: }, ): # Patch multiple times - platformio_api.patch_file_downloader() - platformio_api.patch_file_downloader() - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() + platformio_runner.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -895,19 +898,18 @@ def test_patch_file_downloader_idempotent() -> None: assert call_count == 1 -def test_platformio_log_filter_allows_non_platformio_messages() -> None: - """Test that non-platformio logger messages are allowed through.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="esphome.core", - level=logging.INFO, - pathname="", - lineno=0, - msg="Some esphome message", - args=(), - exc_info=None, +def _filter_through_redirect(line: str) -> str: + """Write a line through RedirectText with FILTER_PLATFORMIO_LINES and return what passes.""" + import io + + from esphome.util import RedirectText + + captured = io.StringIO() + redirect = RedirectText( + captured, filter_lines=platformio_api.FILTER_PLATFORMIO_LINES ) - assert log_filter.filter(record) is True + redirect.write(line + "\n") + return captured.getvalue() @pytest.mark.parametrize( @@ -930,19 +932,9 @@ def test_platformio_log_filter_allows_non_platformio_messages() -> None: "Memory Usage -> https://bit.ly/pio-memory-usage", ], ) -def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: - """Test that noisy platformio messages are filtered out.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="platformio.builder", - level=logging.INFO, - pathname="", - lineno=0, - msg=msg, - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is False +def test_filter_platformio_lines_blocks_noisy_messages(msg: str) -> None: + """Test that noisy platformio output lines are filtered out by RedirectText.""" + assert _filter_through_redirect(msg) == "" @pytest.mark.parametrize( @@ -954,39 +946,6 @@ def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: "warning: unused variable", ], ) -def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None: - """Test that non-noisy platformio messages are allowed through.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="platformio.builder", - level=logging.INFO, - pathname="", - lineno=0, - msg=msg, - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is True - - -@pytest.mark.parametrize( - "logger_name", - [ - "PLATFORMIO.builder", - "PlatformIO.core", - "platformio.run", - ], -) -def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None: - """Test that platformio logger name matching is case insensitive.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name=logger_name, - level=logging.INFO, - pathname="", - lineno=0, - msg="Found 5 compatible libraries", - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is False +def test_filter_platformio_lines_allows_other_messages(msg: str) -> None: + """Test that non-noisy platformio output lines pass through RedirectText.""" + assert _filter_through_redirect(msg) == msg + "\n" From 6d92cc3d2b8d24845587d6f551d9fb1fa7169010 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 10:24:23 -1000 Subject: [PATCH 1382/2056] [packages] Fix false deprecation warning and wrong error paths in nested packages (#15605) --- esphome/components/packages/__init__.py | 34 +++- .../component_tests/packages/test_packages.py | 177 +++++++++++++++++- 2 files changed, 205 insertions(+), 6 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 04db690c6f4..3a15b5b95a4 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -45,6 +45,18 @@ def is_remote_package(package_config: dict) -> bool: return CONF_URL in package_config +def is_package_definition(value: object) -> bool: + """Returns True if the value looks like a package definition rather than a config fragment. + + Package definitions are IncludeFile objects, git URL shorthand strings, or + remote package dicts (containing a ``url:`` key). Config fragments are + plain dicts that represent component configuration. + """ + return isinstance(value, (yaml_util.IncludeFile, str)) or ( + isinstance(value, dict) and is_remote_package(value) + ) + + def valid_package_contents(package_config: dict) -> dict: """Validate that a package looks like a plausible ESPHome config fragment. @@ -318,11 +330,11 @@ def _walk_packages( if not isinstance(packages, dict): _walk_package_list(packages, callback, context) elif (result := _walk_package_dict(packages, callback, context)) is not None: - if not validate_deprecated: + if not validate_deprecated or any( + is_package_definition(v) for v in packages.values() + ): raise result # Fallback: treat the dict as a single deprecated package. - # Note: this catches *any* cv.Invalid from the callback, which may - # mask real validation errors in named package dicts. # This block can be removed once the single-package # deprecation period (2026.7.0) is over. config[CONF_PACKAGES] = [packages] @@ -461,6 +473,9 @@ class _PackageProcessor: self, package_config: dict | str, context_vars: ContextVars | None ) -> dict: """Resolve a single package and recurse into any nested packages.""" + from_remote = isinstance(package_config, dict) and is_remote_package( + package_config + ) package_config = self.resolve_package(package_config, context_vars) self.collect_substitutions(package_config) @@ -470,7 +485,18 @@ class _PackageProcessor: # Push context from !include vars on the package root and on the packages key context_vars = push_context(package_config, context_vars) context_vars = push_context(package_config[CONF_PACKAGES], context_vars) - return _walk_packages(package_config, self.process_package, context_vars) + # Disable the deprecated single-package fallback for remote + # packages. _process_remote_package returns dicts with + # already-resolved values that is_package_definition cannot + # distinguish from config fragments, so the fallback would + # always fire and mask real errors with wrong paths + # (packages->0 instead of packages-><name>). + return _walk_packages( + package_config, + self.process_package, + context_vars, + validate_deprecated=not from_remote, + ) def do_packages_pass( diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0893c7dcbbc..0b828d757ed 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1,11 +1,18 @@ """Tests for the packages component.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +from esphome.components.packages import ( + CONFIG_SCHEMA, + _walk_packages, + do_packages_pass, + is_package_definition, + merge_packages, +) from esphome.components.substitutions import do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove @@ -37,7 +44,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict -from esphome.yaml_util import add_context +from esphome.yaml_util import IncludeFile, add_context # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -79,6 +86,44 @@ def packages_pass(config): return config +_INCLUDE_FILE = "INCLUDE_FILE" + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + # IncludeFile objects are package definitions + (_INCLUDE_FILE, True), + # Git URL shorthand strings are package definitions + ("github://esphome/firmware/base.yaml@main", True), + # Remote package dicts (with url key) are package definitions + ({"url": "https://github.com/esphome/firmware", "file": "base.yaml"}, True), + # Plain config dicts are NOT package definitions (they are config fragments) + ({"wifi": {"ssid": "test"}}, False), + # None is not a package definition + (None, False), + # Lists are not package definitions + ([{"wifi": {"ssid": "test"}}], False), + # Empty dicts are not package definitions + ({}, False), + ], + ids=[ + "include_file", + "git_shorthand", + "remote_package", + "config_fragment", + "none", + "list", + "empty_dict", + ], +) +def test_is_package_definition(value: object, expected: bool) -> None: + """Test that is_package_definition correctly identifies package definitions.""" + if value is _INCLUDE_FILE: + value = MagicMock(spec=IncludeFile) + assert is_package_definition(value) is expected + + def test_package_unused(basic_esphome, basic_wifi) -> None: """ Ensures do_package_pass does not change a config if packages aren't used. @@ -1107,6 +1152,134 @@ def test_invalid_package_contents_masked_by_deprecation( do_packages_pass(config) +def test_named_dict_with_include_files_no_false_deprecation_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Package errors in named dicts must not trigger the deprecated fallback.""" + good_include = MagicMock(spec=IncludeFile) + bad_include = MagicMock(spec=IncludeFile) + + config = { + CONF_PACKAGES: { + "good_pkg": good_include, + "bad_pkg": bad_include, + }, + } + + call_count = 0 + + def failing_callback(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + if call_count == 1: + # First package processes fine + return {CONF_WIFI: {CONF_SSID: "test"}} + # Second package has an error (e.g. jinja syntax error) + raise cv.Invalid("simulated jinja error in bad_pkg") + + with ( + caplog.at_level(logging.WARNING), + pytest.raises(cv.Invalid, match="simulated jinja error"), + ): + _walk_packages(config, failing_callback) + + # Must NOT emit the deprecated single-package warning + assert "deprecated" not in caplog.text.lower() + + +def test_validate_deprecated_false_raises_directly( + caplog: pytest.LogCaptureFixture, +) -> None: + """With validate_deprecated=False, errors raise directly without fallback. + + This is the codepath used for remote packages where _process_remote_package + returns already-resolved dicts that is_package_definition cannot detect. + """ + config = { + CONF_PACKAGES: { + "pkg_a": {CONF_WIFI: {CONF_SSID: "test"}}, + "pkg_b": {CONF_WIFI: {CONF_SSID: "test2"}}, + }, + } + + call_count = 0 + + def failing_callback(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + if call_count == 1: + return package_config + raise cv.Invalid("nested error") + + with ( + caplog.at_level(logging.WARNING), + pytest.raises(cv.Invalid, match="nested error"), + ): + _walk_packages(config, failing_callback, validate_deprecated=False) + + assert "deprecated" not in caplog.text.lower() + + +def test_error_on_first_declared_package_still_detected() -> None: + """When the first declared package errors, it's the last processed in reverse. + + All other entries are already resolved to dicts, but the failing entry + retains its original IncludeFile value since assignment was skipped. + """ + config = { + CONF_PACKAGES: { + "first_pkg": MagicMock(spec=IncludeFile), + "second_pkg": MagicMock(spec=IncludeFile), + "third_pkg": MagicMock(spec=IncludeFile), + }, + } + + call_count = 0 + + def fail_on_last(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + # Reverse iteration: third_pkg (1), second_pkg (2), first_pkg (3) + if call_count < 3: + return {CONF_WIFI: {CONF_SSID: "test"}} + raise cv.Invalid("error in first_pkg") + + with pytest.raises(cv.Invalid, match="error in first_pkg"): + _walk_packages(config, fail_on_last) + + +def test_deprecated_single_package_fallback_still_works( + caplog: pytest.LogCaptureFixture, +) -> None: + """The deprecated single-package form still falls back at the top level. + + When a dict's values are plain config fragments (not package definitions) + and the callback fails, the deprecated fallback wraps the dict in a list + and retries with a deprecation warning. + """ + config = { + CONF_PACKAGES: { + CONF_WIFI: {CONF_SSID: "test", CONF_PASSWORD: "secret"}, + }, + } + + attempt = 0 + + def fail_then_succeed(package_config: dict, context: object) -> dict: + nonlocal attempt + attempt += 1 + if attempt == 1: + # First attempt: treating as named dict fails + raise cv.Invalid("not a valid package") + # Second attempt: after fallback wraps as list, succeeds + return package_config + + with caplog.at_level(logging.WARNING): + _walk_packages(config, fail_then_succeed) + + assert "deprecated" in caplog.text.lower() + + def test_merge_packages_invalid_nested_type_raises() -> None: """Invalid nested packages type during merge raises cv.Invalid.""" config = { From 8754bbfa89f241f15bb239066ef98c9dbde61e50 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:29:11 +1000 Subject: [PATCH 1383/2056] [lvgl] Fix use of rotation on host SDL (#15611) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 2 +- tests/components/lvgl/test.host.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index f6f6204f4c4..b6421dc43d7 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -341,7 +341,7 @@ async def to_code(configs): df.LOGGER.info("LVGL will use hardware rotation via display driver") else: rotation_type = RotationType.ROTATION_SOFTWARE - if get_esp32_variant() == VARIANT_ESP32P4: + if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4: df.LOGGER.info("LVGL will use software rotation (PPA accelerated)") else: df.LOGGER.info("LVGL will use software rotation") diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index f84156c9d82..6328648fe34 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -20,6 +20,7 @@ lvgl: - id: lvgl_0 default_font: space16 displays: sdl0 + rotation: 180 top_layer: - id: lvgl_1 From 20b516ff11881be4c7d6d15f984943403683ea8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 8 Apr 2026 16:14:01 -1000 Subject: [PATCH 1384/2056] [api] Fix ListEntitiesRequest not read due to LWIP rcvevent tracking (#15589) --- esphome/components/api/api_connection.cpp | 36 +++++++++++++++-------- esphome/components/api/api_connection.h | 1 + 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7db423141c1..4663456da62 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -52,11 +52,11 @@ namespace esphome::api { -// Read a maximum of 5 messages per loop iteration to prevent starving other components. +// Maximum messages to read per loop iteration to prevent starving other components. // This is a balance between API responsiveness and allowing other components to run. // Since each message could contain multiple protobuf messages when using packet batching, // this limits the number of messages processed, not the number of TCP packets. -static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; +static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; @@ -220,10 +220,17 @@ void APIConnection::loop() { } const uint32_t now = App.get_loop_component_start_time(); - // Check if socket has data ready before attempting to read - if (this->helper_->is_socket_ready()) { + // Check if socket has data ready before attempting to read. + // Also try reading if we hit the message limit last time — LWIP's rcvevent + // (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple + // messages share a TCP segment, the last message's data stays in LWIP's + // lastdata cache after rcvevent hits 0, making is_socket_ready() return false + // even though data remains. + if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) { + this->flags_.may_have_remaining_data = false; // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput - for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { + uint8_t message_count = 0; + for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { ReadPacketBuffer buffer; err = this->helper_->read_packet(&buffer); if (err == APIError::WOULD_BLOCK) { @@ -245,6 +252,11 @@ void APIConnection::loop() { return; } } + // If we hit the limit, there may be more data remaining in LWIP's + // lastdata cache that rcvevent doesn't account for. + if (message_count == MAX_MESSAGES_PER_LOOP) { + this->flags_.may_have_remaining_data = true; + } } // Process deferred batch if scheduled and timer has expired @@ -2086,6 +2098,13 @@ void APIConnection::process_batch_() { return; } + // Ensure TCP_NODELAY is on before draining overflow and writing batch data. + // Log messages enable Nagle (NODELAY off) to coalesce small packets. + // If Nagle is still on when we try to drain, LWIP holds data in the + // Nagle buffer, the TCP send buffer stays full, and the overflow + // buffer can never drain — blocking the batch write indefinitely. + this->helper_->set_nodelay_for_message(false); + // Try to clear buffer first if (!this->try_to_clear_buffer(true)) { // Can't write now, we'll try again later @@ -2193,13 +2212,6 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items shared_buf.resize(shared_buf.size() + footer_size); } - // Ensure TCP_NODELAY is on before writing batch data. - // Log messages enable Nagle (NODELAY off) to coalesce small packets. - // Without this, batch data written to the socket sits in LWIP's Nagle - // buffer — the remote won't ACK until it sends its own data (e.g. a - // ping), which can take 20+ seconds. - this->helper_->set_nodelay_for_message(false); - // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, std::span<const MessageInfo>(message_info, items_processed)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284c4475deb..7d087970903 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -771,6 +771,7 @@ class APIConnection final : public APIServerConnectionBase { uint8_t batch_scheduled : 1; uint8_t batch_first_message : 1; // For batch buffer allocation uint8_t should_try_send_immediately : 1; // True after initial states are sent + uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check #ifdef HAS_PROTO_MESSAGE_DUMP uint8_t log_only_mode : 1; #endif From 4f00ad409e5d806ecd6ba0ddca8b4e61098d1a56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:03:58 +0000 Subject: [PATCH 1385/2056] Bump aioesphomeapi from 44.12.0 to 44.13.1 (#15600) 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 c4b90b5ca94..d7db44454cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.12.0 +aioesphomeapi==44.13.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 873378fa1f4546de5cf3dd110adab7bb8bb74dfd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 05:48:10 -1000 Subject: [PATCH 1386/2056] [gdk101] Increase reset retries for slow-booting sensor MCU (#15584) --- esphome/components/gdk101/gdk101.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 149973ba8a2..0ee718cd20e 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -7,7 +7,7 @@ namespace gdk101 { static const char *const TAG = "gdk101"; static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5; -static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 10; +static constexpr uint8_t NUMBER_OF_RESET_RETRIES = 30; static constexpr uint32_t RESET_INTERVAL_ID = 0; static constexpr uint32_t RESET_INTERVAL_MS = 1000; From c85a062e2348eb0ded069b4629b68f233b80a459 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:00:52 -0400 Subject: [PATCH 1387/2056] [sx127x][cc1101] Disable loop when packet mode is inactive (#15606) --- esphome/components/cc1101/cc1101.cpp | 9 +++++++++ esphome/components/sx127x/sx127x.cpp | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 51aa88b8f72..f2b7451721a 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -150,6 +150,10 @@ void CC1101Component::setup() { if (this->gdo0_pin_ != nullptr) { this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); }); } + + if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { + this->disable_loop(); + } } void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) { @@ -669,6 +673,11 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { + if (value) { + this->enable_loop(); + } else { + this->disable_loop(); + } this->write_(Register::PKTCTRL0); this->write_(Register::PKTCTRL1); this->write_(Register::IOCFG0); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 0fddfdccdb7..83be96767a4 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -383,9 +383,14 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { if (millis() - start > 20) { ESP_LOGE(TAG, "Set mode failure"); this->mark_failed(); - break; + return; } } + if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) { + this->enable_loop(); + } else { + this->disable_loop(); + } } void SX127x::set_mode_rx() { From e4ee2b7c045f4c177c962390a572a4725f718449 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:02:49 +1200 Subject: [PATCH 1388/2056] [hbridge] Move light pin switching to loop (#15615) --- esphome/components/hbridge/light/__init__.py | 2 +- .../hbridge/light/hbridge_light_output.h | 52 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/esphome/components/hbridge/light/__init__.py b/esphome/components/hbridge/light/__init__.py index 65dd3196dff..ccb47237b64 100644 --- a/esphome/components/hbridge/light/__init__.py +++ b/esphome/components/hbridge/light/__init__.py @@ -8,7 +8,7 @@ from .. import hbridge_ns CODEOWNERS = ["@DotNetDann"] HBridgeLightOutput = hbridge_ns.class_( - "HBridgeLightOutput", cg.PollingComponent, light.LightOutput + "HBridgeLightOutput", cg.Component, light.LightOutput ) CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( diff --git a/esphome/components/hbridge/light/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h index c3091548523..4e064d53522 100644 --- a/esphome/components/hbridge/light/hbridge_light_output.h +++ b/esphome/components/hbridge/light/hbridge_light_output.h @@ -1,20 +1,17 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -#include "esphome/core/log.h" +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace hbridge { -// Using PollingComponent as the updates are more consistent and reduces flickering -class HBridgeLightOutput : public PollingComponent, public light::LightOutput { +class HBridgeLightOutput : public Component, public light::LightOutput { public: - HBridgeLightOutput() : PollingComponent(1) {} - - void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; } - void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; } + void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; } + void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); @@ -24,16 +21,16 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { return traits; } - void setup() override { this->forward_direction_ = false; } + void setup() override { this->disable_loop(); } - void update() override { - // This method runs around 60 times per second - // We cannot do the PWM ourselves so we are reliant on the hardware PWM - if (!this->forward_direction_) { // First LED Direction + void loop() override { + // Only called when both channels are active — alternate H-bridge direction + // each iteration to multiplex cold and warm white. + if (!this->forward_direction_) { this->pina_pin_->set_level(this->pina_duty_); this->pinb_pin_->set_level(0); this->forward_direction_ = true; - } else { // Second LED Direction + } else { this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; @@ -43,15 +40,32 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); + float new_pina, new_pinb; + state->current_values_as_cwww(&new_pina, &new_pinb, false); + + this->pina_duty_ = new_pina; + this->pinb_duty_ = new_pinb; + + if (new_pina != 0.0f && new_pinb != 0.0f) { + // Both channels active — need loop to alternate H-bridge direction + this->high_freq_.start(); + this->enable_loop(); + } else { + // Zero or one channel active — drive pins directly, no multiplexing needed + this->high_freq_.stop(); + this->disable_loop(); + this->pina_pin_->set_level(new_pina); + this->pinb_pin_->set_level(new_pinb); + } } protected: output::FloatOutput *pina_pin_; output::FloatOutput *pinb_pin_; - float pina_duty_ = 0; - float pinb_duty_ = 0; - bool forward_direction_ = false; + float pina_duty_{0}; + float pinb_duty_{0}; + bool forward_direction_{false}; + HighFrequencyLoopRequester high_freq_; }; } // namespace hbridge From 171a429526bc156b490bbe547d447858139f3b98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:00 -1000 Subject: [PATCH 1389/2056] [tca9555] Add interrupt pin support (#15613) --- esphome/components/tca9555/__init__.py | 4 ++++ esphome/components/tca9555/tca9555.cpp | 19 ++++++++++++++++++- esphome/components/tca9555/tca9555.h | 5 +++++ tests/components/tca9555/common.yaml | 4 ++++ tests/components/tca9555/test.esp32-idf.yaml | 3 +++ .../components/tca9555/test.esp8266-ard.yaml | 3 +++ tests/components/tca9555/test.rp2040-ard.yaml | 3 +++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py index f42e0fe398b..5f571fcea6e 100644 --- a/esphome/components/tca9555/__init__.py +++ b/esphome/components/tca9555/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(TCA9555Component), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -38,6 +40,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 79c52538983..3eb794df441 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -24,9 +24,18 @@ void TCA9555Component::setup() { this->mark_failed(); return; } + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&TCA9555Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR TCA9555Component::gpio_intr(TCA9555Component *arg) { arg->enable_loop_soon_any_context(); } void TCA9555Component::dump_config() { ESP_LOGCONFIG(TAG, "TCA9555:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -36,6 +45,9 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { // Set mode mask bit this->mode_mask_ |= 1 << pin; + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Clear mode mask bit this->mode_mask_ &= ~(1 << pin); @@ -43,7 +55,12 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { // Write GPIO to enable input mode this->write_gpio_modes_(); } -void TCA9555Component::loop() { this->reset_pin_cache_(); } +void TCA9555Component::loop() { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } +} bool TCA9555Component::read_gpio_outputs_() { if (this->is_failed()) diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index 9f7273b1e7e..d4d070013c7 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -24,7 +24,10 @@ class TCA9555Component : public Component, void loop() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(TCA9555Component *arg); bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -39,6 +42,8 @@ class TCA9555Component : public Component, bool read_gpio_modes_(); bool write_gpio_modes_(); bool read_gpio_outputs_(); + + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a TCA9555 pin as an internal input GPIO pin. diff --git a/tests/components/tca9555/common.yaml b/tests/components/tca9555/common.yaml index 82b4c959d81..d1a68c575a8 100644 --- a/tests/components/tca9555/common.yaml +++ b/tests/components/tca9555/common.yaml @@ -2,6 +2,10 @@ tca9555: - id: tca9555_hub i2c_id: i2c_bus address: 0x21 + - id: tca9555_hub_int + i2c_id: i2c_bus + address: 0x22 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/tca9555/test.esp32-idf.yaml b/tests/components/tca9555/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/tca9555/test.esp32-idf.yaml +++ b/tests/components/tca9555/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/tca9555/test.esp8266-ard.yaml b/tests/components/tca9555/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/tca9555/test.esp8266-ard.yaml +++ b/tests/components/tca9555/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/tca9555/test.rp2040-ard.yaml b/tests/components/tca9555/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/tca9555/test.rp2040-ard.yaml +++ b/tests/components/tca9555/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 17f3b7dbd5b48d6e57c928eb7c4f08284eb1c574 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:26 -1000 Subject: [PATCH 1390/2056] [pca6416a] Add interrupt pin support (#15614) --- esphome/components/pca6416a/__init__.py | 10 +++++++++- esphome/components/pca6416a/pca6416a.cpp | 18 ++++++++++++++++++ esphome/components/pca6416a/pca6416a.h | 4 ++++ tests/components/pca6416a/common.yaml | 4 ++++ tests/components/pca6416a/test.esp32-idf.yaml | 3 +++ .../components/pca6416a/test.esp8266-ard.yaml | 3 +++ tests/components/pca6416a/test.rp2040-ard.yaml | 3 +++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index b6e156e7ff4..813bb35c482 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -25,7 +26,12 @@ PCA6416AGPIOPin = pca6416a_ns.class_( CONF_PCA6416A = "pca6416a" CONFIG_SCHEMA = ( - cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)}) + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } + ) .extend(cv.COMPONENT_SCHEMA) .extend(i2c.i2c_device_schema(0x21)) ) @@ -35,6 +41,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index f393af88cea..dc7463b01ba 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -49,11 +49,22 @@ void PCA6416AComponent::setup() { ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); } void PCA6416AComponent::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } } void PCA6416AComponent::dump_config() { @@ -62,6 +73,7 @@ void PCA6416AComponent::dump_config() { } else { ESP_LOGCONFIG(TAG, "PCA6416A:"); } + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -101,6 +113,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { this->update_register_(pin, true, pull_dir); this->update_register_(pin, false, pull_en); } + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { this->update_register_(pin, true, io_dir); if (has_pullup_) { @@ -109,6 +124,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { } else { ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors"); } + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { this->update_register_(pin, false, io_dir); } diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 138a51cc208..4d2e6b219e4 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -24,7 +24,10 @@ class PCA6416AComponent : public Component, void dump_config() override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg); // Virtual methods from CachedGpioExpander bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; @@ -43,6 +46,7 @@ class PCA6416AComponent : public Component, esphome::i2c::ErrorCode last_error_; /// Only the PCAL6416A has pull-up resistors bool has_pullup_{false}; + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCA6416A pin as an internal input GPIO pin. diff --git a/tests/components/pca6416a/common.yaml b/tests/components/pca6416a/common.yaml index 9ad6e2fb158..09083c6c156 100644 --- a/tests/components/pca6416a/common.yaml +++ b/tests/components/pca6416a/common.yaml @@ -2,6 +2,10 @@ pca6416a: - id: pca6416a_hub i2c_id: i2c_bus address: 0x21 + - id: pca6416a_hub_int + i2c_id: i2c_bus + address: 0x22 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pca6416a/test.esp32-idf.yaml b/tests/components/pca6416a/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pca6416a/test.esp32-idf.yaml +++ b/tests/components/pca6416a/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pca6416a/test.esp8266-ard.yaml b/tests/components/pca6416a/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pca6416a/test.esp8266-ard.yaml +++ b/tests/components/pca6416a/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pca6416a/test.rp2040-ard.yaml b/tests/components/pca6416a/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pca6416a/test.rp2040-ard.yaml +++ b/tests/components/pca6416a/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 835ee456a53817a0858805834bc521057be68e8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:29:52 -1000 Subject: [PATCH 1391/2056] [mcp23016] Add interrupt pin support (#15616) --- esphome/components/mcp23016/__init__.py | 4 ++++ esphome/components/mcp23016/mcp23016.cpp | 14 ++++++++++++++ esphome/components/mcp23016/mcp23016.h | 4 ++++ tests/components/mcp23016/common.yaml | 8 ++++++-- tests/components/mcp23016/test.esp32-idf.yaml | 3 +++ tests/components/mcp23016/test.esp8266-ard.yaml | 3 +++ tests/components/mcp23016/test.rp2040-ard.yaml | 3 +++ 7 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 5a1f011617a..b71d57498a2 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -24,6 +25,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(MCP23016), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -35,6 +37,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index fbdb6903b84..118a77ce378 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -24,11 +24,22 @@ void MCP23016::setup() { // all pins input this->write_reg_(MCP23016_IODIR1, 0xFFFF); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + this->disable_loop(); } +void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); } void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } } bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); } @@ -37,6 +48,9 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { this->update_reg_(pin, true, MCP23016_IODIR1); + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg_(pin, false, MCP23016_IODIR1); } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 494bc9c197a..32149ba3e24 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -35,7 +35,10 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: float get_setup_priority() const override; + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + protected: + static void IRAM_ATTR gpio_intr(MCP23016 *arg); // Virtual methods from CachedGpioExpander bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; @@ -51,6 +54,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander:: uint16_t olat_{0x0000}; // Cache for input values (16-bit combined for both banks) uint16_t input_mask_{0x0000}; + InternalGPIOPin *interrupt_pin_{nullptr}; }; class MCP23016GPIOPin : public GPIOPin { diff --git a/tests/components/mcp23016/common.yaml b/tests/components/mcp23016/common.yaml index e8e3ad9d08a..81f38b3f52e 100644 --- a/tests/components/mcp23016/common.yaml +++ b/tests/components/mcp23016/common.yaml @@ -1,6 +1,10 @@ mcp23016: - i2c_id: i2c_bus - id: mcp23016_hub + - i2c_id: i2c_bus + id: mcp23016_hub + - i2c_id: i2c_bus + id: mcp23016_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/mcp23016/test.esp32-idf.yaml b/tests/components/mcp23016/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23016/test.esp32-idf.yaml +++ b/tests/components/mcp23016/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23016/test.esp8266-ard.yaml b/tests/components/mcp23016/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23016/test.esp8266-ard.yaml +++ b/tests/components/mcp23016/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23016/test.rp2040-ard.yaml b/tests/components/mcp23016/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23016/test.rp2040-ard.yaml +++ b/tests/components/mcp23016/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 0f16d27a72cc2fb4db0413bbf5099a34caaf78f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 9 Apr 2026 17:33:56 -1000 Subject: [PATCH 1392/2056] [api] Add (inline_encode) proto option for sub-message inlining (#15599) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 47 ++-- esphome/components/api/api_pb2.h | 2 - esphome/components/api/proto.h | 6 + script/api_protobuf/api_protobuf.py | 293 +++++++++++++++++++++-- 6 files changed, 304 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 33d16f0339b..e4d0c2d16df 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1625,6 +1625,7 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { + option (inline_encode) = true; uint64 address = 1 [(force) = true]; sint32 rssi = 2 [(force) = true]; uint32 address_type = 3 [(max_value) = 4]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 0f71268d700..dacc290e31e 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -22,6 +22,7 @@ extend google.protobuf.MessageOptions { optional bool log = 1039 [default=true]; optional bool no_delay = 1040 [default=false]; optional string base_class = 1041; + optional bool inline_encode = 1042 [default=false]; } extend google.protobuf.FieldOptions { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d27cfa57cf9..c2d513f0d33 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2328,40 +2328,37 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { - uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); - ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); - ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi)); - if (this->address_type) { - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); - ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type); - } - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); - ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len)); - ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len); - return pos; -} -uint32_t BluetoothLERawAdvertisement::calculate_size() const { - uint32_t size = 0; - size += ProtoSize::calc_uint64_force(1, this->address); - size += ProtoSize::calc_sint32_force(1, this->rssi); - size += this->address_type ? 2 : 0; - size += 2 + this->data_len; - return size; -} uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { - ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]); + auto &sub_msg = this->advertisements[i]; + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10); + uint8_t *len_pos = pos; + ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); + ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); + ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi)); + if (sub_msg.address_type) { + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type); + } + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len)); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len); + *len_pos = static_cast<uint8_t>(pos - len_pos - 1); } return pos; } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); + auto &sub_msg = this->advertisements[i]; + size += 2; + size += ProtoSize::calc_uint64_force(1, sub_msg.address); + size += ProtoSize::calc_sint32_force(1, sub_msg.rssi); + size += sub_msg.address_type ? 2 : 0; + size += 2 + sub_msg.data_len; } return size; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3b239db36c5..5827a8728e6 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1888,8 +1888,6 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; - uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e0a4e03189e..8cac7fff3bf 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -352,6 +352,12 @@ class ProtoEncode { PROTO_ENCODE_CHECK_BOUNDS(pos, 1); *pos++ = b; } + /// Reserve one byte for later backpatch (e.g., sub-message length). + /// Advances pos past the reserved byte without writing a value. + static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + pos++; + } /// Write raw bytes to the buffer (no tag, no length prefix). static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 526644842da..39bfc865d02 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -60,6 +60,10 @@ FILE_HEADER = """// This file was automatically generated with a tool. # Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value. _enum_max_values: dict[str, int] = {} +# Populated by main() before message generation. +# Maps message name (e.g. "BluetoothLERawAdvertisement") to its descriptor. +_message_desc_map: dict[str, Any] = {} + def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" @@ -427,6 +431,23 @@ class TypeInfo(ABC): Estimated size in bytes including field ID and typical data """ + def get_max_encoded_size(self) -> int | None: + """Get the maximum possible encoded size in bytes for this field. + + Returns the worst-case encoded size including field ID and maximum + possible value encoding. Returns None if the size is unbounded + (e.g., variable-length strings without max_data_length). + + Used by (inline_encode) validation to ensure sub-messages fit in a + single-byte length varint (< 128 bytes). + """ + return None # Unbounded by default + + +def _varint_max_size(bits: int) -> int: + """Return the maximum varint encoding size for a value with the given number of bits.""" + return (max(bits, 1) + 6) // 7 # ceil(bits / 7), min 1 byte for varint(0) + TYPE_INFO: dict[int, TypeInfo] = {} @@ -514,8 +535,30 @@ def register_type(name: int): return func +class FixedSizeTypeMixin: + """Mixin for types with a known fixed encoded size (float, double, fixed32, fixed64).""" + + def get_max_encoded_size(self) -> int: + return self.calculate_field_id_size() + self.get_fixed_size_bytes() + + +class VarintTypeMixin: + """Mixin for varint types. Subclasses set _varint_max_bits.""" + + _varint_max_bits: int = 64 # Default to worst case + + def get_max_encoded_size(self) -> int: + max_val = self.max_value + if max_val is not None: + return self.calculate_field_id_size() + _varint_max_size( + max_val.bit_length() if max_val > 0 else 1 + ) + return self.calculate_field_id_size() + _varint_max_size(self._varint_max_bits) + + @register_type(1) -class DoubleType(TypeInfo): +class DoubleType(FixedSizeTypeMixin, TypeInfo): + # Unsupported but defined for completeness cpp_type = "double" default_value = "0.0" decode_64bit = "value.as_double()" @@ -541,7 +584,7 @@ class DoubleType(TypeInfo): @register_type(2) -class FloatType(TypeInfo): +class FloatType(FixedSizeTypeMixin, TypeInfo): cpp_type = "float" default_value = "0.0f" decode_32bit = "value.as_float()" @@ -567,8 +610,9 @@ class FloatType(TypeInfo): @register_type(3) -class Int64Type(TypeInfo): +class Int64Type(VarintTypeMixin, TypeInfo): cpp_type = "int64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "static_cast<int64_t>(value)" encode_func = "encode_int64" @@ -587,8 +631,9 @@ class Int64Type(TypeInfo): @register_type(4) -class UInt64Type(TypeInfo): +class UInt64Type(VarintTypeMixin, TypeInfo): cpp_type = "uint64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "value" encode_func = "encode_uint64" @@ -607,8 +652,9 @@ class UInt64Type(TypeInfo): @register_type(5) -class Int32Type(TypeInfo): +class Int32Type(VarintTypeMixin, TypeInfo): cpp_type = "int32_t" + _varint_max_bits = 64 # int32 is sign-extended to 64 bits in protobuf default_value = "0" decode_varint = "static_cast<int32_t>(value)" encode_func = "encode_int32" @@ -627,7 +673,7 @@ class Int32Type(TypeInfo): @register_type(6) -class Fixed64Type(TypeInfo): +class Fixed64Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "uint64_t" default_value = "0" decode_64bit = "value.as_fixed64()" @@ -653,7 +699,7 @@ class Fixed64Type(TypeInfo): @register_type(7) -class Fixed32Type(TypeInfo): +class Fixed32Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "uint32_t" default_value = "0" decode_32bit = "value.as_fixed32()" @@ -689,7 +735,8 @@ class Fixed32Type(TypeInfo): @register_type(8) -class BoolType(TypeInfo): +class BoolType(VarintTypeMixin, TypeInfo): + _varint_max_bits = 1 cpp_type = "bool" default_value = "false" decode_varint = "value != 0" @@ -807,6 +854,16 @@ class StringType(TypeInfo): def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string + def get_max_encoded_size(self) -> int | None: + max_len = self.max_data_length + if max_len is not None: + return ( + self.calculate_field_id_size() + + _varint_max_size(max_len.bit_length()) + + max_len + ) + return None # Unbounded + @register_type(11) class MessageType(TypeInfo): @@ -1122,6 +1179,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase): def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string + def get_max_encoded_size(self) -> int | None: + max_len = self.max_data_length + if max_len is not None: + return ( + self.calculate_field_id_size() + + _varint_max_size(max_len.bit_length()) + + max_len + ) + return None + class PackedBufferTypeInfo(TypeInfo): """Type for packed repeated fields that expose raw buffer instead of decoding. @@ -1299,14 +1366,23 @@ class FixedArrayBytesType(TypeInfo): self.calculate_field_id_size() + 1 + 31 ) # field ID + length byte + typical 31 bytes + def get_max_encoded_size(self) -> int: + # field_id + varint(array_size) + array_size + return ( + self.calculate_field_id_size() + + _varint_max_size(self.array_size.bit_length()) + + self.array_size + ) + @property def wire_type(self) -> WireType: return WireType.LENGTH_DELIMITED @register_type(13) -class UInt32Type(TypeInfo): +class UInt32Type(VarintTypeMixin, TypeInfo): cpp_type = "uint32_t" + _varint_max_bits = 32 default_value = "0" decode_varint = "value" encode_func = "encode_uint32" @@ -1328,7 +1404,9 @@ class UInt32Type(TypeInfo): @register_type(14) -class EnumType(TypeInfo): +class EnumType(VarintTypeMixin, TypeInfo): + _varint_max_bits = 32 + @property def cpp_type(self) -> str: return f"enums::{self._field.type_name[1:]}" @@ -1379,7 +1457,7 @@ class EnumType(TypeInfo): @register_type(15) -class SFixed32Type(TypeInfo): +class SFixed32Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "int32_t" default_value = "0" decode_32bit = "value.as_sfixed32()" @@ -1405,7 +1483,7 @@ class SFixed32Type(TypeInfo): @register_type(16) -class SFixed64Type(TypeInfo): +class SFixed64Type(FixedSizeTypeMixin, TypeInfo): cpp_type = "int64_t" default_value = "0" decode_64bit = "value.as_sfixed64()" @@ -1431,8 +1509,9 @@ class SFixed64Type(TypeInfo): @register_type(17) -class SInt32Type(TypeInfo): +class SInt32Type(VarintTypeMixin, TypeInfo): cpp_type = "int32_t" + _varint_max_bits = 32 # zigzag encoding keeps it 32-bit default_value = "0" decode_varint = "decode_zigzag32(static_cast<uint32_t>(value))" encode_func = "encode_sint32" @@ -1451,8 +1530,9 @@ class SInt32Type(TypeInfo): @register_type(18) -class SInt64Type(TypeInfo): +class SInt64Type(VarintTypeMixin, TypeInfo): cpp_type = "int64_t" + _varint_max_bits = 64 default_value = "0" decode_varint = "decode_zigzag64(value)" encode_func = "encode_sint64" @@ -1500,6 +1580,91 @@ def _generate_array_dump_content( return o +def _is_inline_encode(sub_msg_name: str) -> bool: + """Check if a sub-message type has the (inline_encode) option set.""" + sub_desc = _message_desc_map.get(sub_msg_name) + if not sub_desc: + return False + inline_opt = getattr(pb, "inline_encode", None) + if inline_opt is None: + return False + return get_opt(sub_desc, inline_opt, False) + + +def _generate_inline_encode_block( + field_number: int, sub_msg_name: str, element: str +) -> str: + """Generate inline encode code for a sub-message with (inline_encode) = true. + + Instead of calling encode_sub_message (function pointer indirection), + this inlines the sub-message's field encoding directly. Uses 1-byte + backpatch for the length (validated to be < 128 at generation time). + + Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement + on complex element expressions. + + Args: + field_number: The parent field number for this sub-message + sub_msg_name: The sub-message type name + element: C++ expression for the element (e.g., "it" or "this->field[i]") + """ + sub_desc = _message_desc_map[sub_msg_name] + tag = (field_number << 3) | 2 # wire type 2 = LENGTH_DELIMITED + assert tag < 128, f"inline_encode requires single-byte tag, got {tag}" + + lines = [] + lines.append(f"auto &sub_msg = {element};") + lines.append(f"ProtoEncode::write_raw_byte(pos, {tag});") + lines.append("uint8_t *len_pos = pos;") + lines.append("ProtoEncode::reserve_byte(pos);") + + # Generate inline field encoding for each sub-message field + for field in sub_desc.field: + if field.options.deprecated: + continue + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + encode_line = ti.encode_content + # Replace this-> with sub_msg reference for the sub-message fields + encode_line = encode_line.replace("this->", "sub_msg.") + lines.extend(wrap_with_ifdef(encode_line, get_field_opt(field, pb.field_ifdef))) + + lines.append("*len_pos = static_cast<uint8_t>(pos - len_pos - 1);") + return "\n".join(lines) + + +def _generate_inline_size_block( + field_number: int, sub_msg_name: str, element: str +) -> str: + """Generate inline size calculation for a sub-message with (inline_encode) = true. + + Uses a local reference alias 'sub_msg' to avoid issues with this-> replacement + on complex element expressions like 'this->advertisements[i]'. + + Args: + field_number: The parent field number for this sub-message + sub_msg_name: The sub-message type name + element: C++ expression for the element + """ + sub_desc = _message_desc_map[sub_msg_name] + + lines = [] + lines.append(f"auto &sub_msg = {element};") + # 1 byte tag + 1 byte length (guaranteed < 128 by validation) + lines.append("size += 2;") + + for field in sub_desc.field: + if field.options.deprecated: + continue + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + force = get_field_opt(field, pb.force, False) + size_line = ti.get_size_calculation(f"sub_msg.{ti.field_name}", force) + # Replace hardcoded this-> references (e.g., FixedArrayBytesType uses this->field_len) + size_line = size_line.replace("this->", "sub_msg.") + lines.extend(wrap_with_ifdef(size_line, get_field_opt(field, pb.field_ifdef))) + + return "\n".join(lines) + + class FixedArrayRepeatedType(TypeInfo): """Special type for fixed-size repeated fields using std::array. @@ -1526,6 +1691,10 @@ class FixedArrayRepeatedType(TypeInfo): return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast<uint32_t>({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): + if _is_inline_encode(self._ti.cpp_type): + return _generate_inline_encode_block( + self.number, self._ti.cpp_type, element + ) return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" return ( f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" @@ -1633,8 +1802,19 @@ class FixedArrayRepeatedType(TypeInfo): ] return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}" + is_inline = isinstance(self._ti, MessageType) and _is_inline_encode( + self._ti.cpp_type + ) + # When using a define, always use loop-based approach if self.is_define: + if is_inline: + o = f"for (const auto &it : {name}) {{\n" + o += indent( + _generate_inline_size_block(self.number, self._ti.cpp_type, "it") + ) + o += "\n}" + return o o = f"for (const auto &it : {name}) {{\n" o += f" {self._ti.get_size_calculation('it', True)}\n" o += "}" @@ -1642,6 +1822,14 @@ class FixedArrayRepeatedType(TypeInfo): # For fixed arrays, we always encode all elements + if is_inline: + o = f"for (const auto &it : {name}) {{\n" + o += indent( + _generate_inline_size_block(self.number, self._ti.cpp_type, "it") + ) + o += "\n}" + return o + # Special case for single-element arrays - no loop needed if self.array_size == 1: return self._ti.get_size_calculation(f"{name}[0]", True) @@ -1714,6 +1902,15 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): def get_size_calculation(self, name: str, force: bool = False) -> str: # Calculate size only for active elements + if isinstance(self._ti, MessageType) and _is_inline_encode(self._ti.cpp_type): + o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" + o += indent( + _generate_inline_size_block( + self.number, self._ti.cpp_type, f"{name}[i]" + ) + ) + o += "\n}" + return o o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n" o += "}" @@ -2222,6 +2419,28 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: return total_size +def calculate_message_max_size(desc: descriptor.DescriptorProto) -> int | None: + """Calculate the maximum possible encoded size for a message. + + Returns None if any field has unbounded size (e.g., variable-length strings). + Used to validate that (inline_encode) messages fit in a single-byte length varint. + """ + total_size = 0 + + for field in desc.field: + if field.options.deprecated: + continue + + ti = create_field_type_info(field, needs_decode=False, needs_encode=True) + max_size = ti.get_max_encoded_size() + if max_size is None: + return None + + total_size += max_size + + return total_size + + def build_message_type( desc: descriptor.DescriptorProto, base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]], @@ -2451,11 +2670,23 @@ def build_message_type( prot = "void decode(const uint8_t *buffer, size_t length);" public_content.append(prot) + # Check if this message uses inline_encode — if so, skip generating standalone + # encode/calculate_size methods since the encoding is inlined into the parent. + inline_opt = getattr(pb, "inline_encode", None) + is_inline_only = ( + message_id is None # Not a service message (no id) + and inline_opt is not None + and get_opt(desc, inline_opt, False) + ) + # Only generate encode method if this message needs encoding and has fields - if needs_encode and encode: + if needs_encode and encode and not is_inline_only: # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls encode_debug = [ - line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode + line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,").replace( + "(pos)", "(pos PROTO_ENCODE_DEBUG_ARG)" + ) + for line in encode ] o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" @@ -2470,7 +2701,7 @@ def build_message_type( # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used # Add calculate_size method only if this message needs encoding and has fields - if needs_encode and size_calc: + if needs_encode and size_calc and not is_inline_only: o = f"uint32_t {desc.name}::calculate_size() const {{\n" o += " uint32_t size = 0;\n" o += indent("\n".join(size_calc)) + "\n" @@ -2830,6 +3061,32 @@ def main() -> None: if not enum.options.deprecated and enum.value: _enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value) + # Build message descriptor map for inline_encode lookups + mt = file.message_type + _message_desc_map.update({m.name: m for m in mt if not m.options.deprecated}) + + # Validate inline_encode messages fit in single-byte length varint + inline_encode_opt = getattr(pb, "inline_encode", None) + if inline_encode_opt is not None: + for m in mt: + if m.options.deprecated: + continue + if not get_opt(m, inline_encode_opt, False): + continue + max_size = calculate_message_max_size(m) + if max_size is None: + raise ValueError( + f"Message '{m.name}' has (inline_encode) = true but contains " + f"fields with unbounded size. Inline encoding requires all " + f"fields to have bounded maximum size." + ) + if max_size >= 128: + raise ValueError( + f"Message '{m.name}' has (inline_encode) = true but max " + f"encoded size is {max_size} bytes (>= 128). Inline encoding " + f"requires sub-messages that fit in a single-byte length varint." + ) + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( build_type_usage_map(file) @@ -3048,8 +3305,6 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint content += "\n} // namespace enums\n\n" - mt = file.message_type - # Identify empty SOURCE_CLIENT messages that don't need class generation for m in mt: if m.options.deprecated: From 0faa641c8a3021d0b7d8d617af1de7604b7c1912 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 10 Apr 2026 21:21:26 +0200 Subject: [PATCH 1393/2056] [micro_wake_word] Pin esp-nn version (#15628) --- .../components/micro_wake_word/__init__.py | 2 + .../micro_wake_word/streaming_model.cpp | 92 +++++++++++++++++-- .../micro_wake_word/streaming_model.h | 5 + 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index ff27dec6df0..de95e4961b9 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -451,6 +451,8 @@ async def to_code(config): ota.request_ota_state_listeners() esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") + # Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn) + esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 0ab6cd3772d..e761e4866fd 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -29,14 +29,6 @@ void VADModel::log_model_config() { bool StreamingModel::load_model_() { RAMAllocator<uint8_t> arena_allocator; - if (this->tensor_arena_ == nullptr) { - this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); - if (this->tensor_arena_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); - return false; - } - } - if (this->var_arena_ == nullptr) { this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE); if (this->var_arena_ == nullptr) { @@ -53,6 +45,26 @@ bool StreamingModel::load_model_() { return false; } + // Probe for the actual required tensor arena size if not yet determined + if (!this->tensor_arena_size_probed_) { + size_t probed_size = this->probe_arena_size_(); + if (probed_size > 0) { + ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size); + this->tensor_arena_size_ = probed_size; + } else { + ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_); + } + this->tensor_arena_size_probed_ = true; + } + + if (this->tensor_arena_ == nullptr) { + this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); + if (this->tensor_arena_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena."); + return false; + } + } + if (this->interpreter_ == nullptr) { this->interpreter_ = make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_, @@ -94,6 +106,70 @@ bool StreamingModel::load_model_() { return true; } +size_t StreamingModel::probe_arena_size_() { + RAMAllocator<uint8_t> arena_allocator; + + // Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different + // versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct, + // and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16 + // bytes. + size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15, + (this->tensor_arena_size_ * 2 + 15) & ~15}; + + for (size_t attempt_size : attempt_sizes) { + uint8_t *probe_arena = arena_allocator.allocate(attempt_size); + if (probe_arena == nullptr) { + continue; + } + + // Verify the model works at all with this arena size + auto probe_interpreter = make_unique<tflite::MicroInterpreter>( + tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_); + + if (probe_interpreter->AllocateTensors() != kTfLiteOk) { + probe_interpreter.reset(); + arena_allocator.deallocate(probe_arena, attempt_size); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + continue; + } + + // Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment). + // If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds. + size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15; + probe_interpreter.reset(); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + + size_t upper = attempt_size; + + while (lower < upper) { + auto test_interpreter = make_unique<tflite::MicroInterpreter>( + tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_); + + bool ok = test_interpreter->AllocateTensors() == kTfLiteOk; + + test_interpreter.reset(); + this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE); + this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20); + + if (ok) { + // Found a working size smaller than the full arena + upper = lower + 16; // Pad by 16 bytes to be safe for future allocations + break; + } + + // Try the midpoint between current attempt and full size + lower = ((lower + upper) / 2 + 15) & ~15; + } + + arena_allocator.deallocate(probe_arena, attempt_size); + return upper; + } + + return 0; +} + void StreamingModel::unload_model() { this->interpreter_.reset(); diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index 0811bfb19b8..fc9eeb5e2d8 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -63,6 +63,10 @@ class StreamingModel { /// @brief Allocates tensor and variable arenas and sets up the model interpreter /// @return True if successful, false otherwise bool load_model_(); + /// @brief Probes the actual required tensor arena size by trial allocation. + /// Tries the manifest size first, then 2x if that fails. + /// @return The required arena size rounded up to 16-byte alignment, or 0 on failure. + size_t probe_arena_size_(); /// @brief Returns true if successfully registered the streaming model's TensorFlow operations bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver); @@ -70,6 +74,7 @@ class StreamingModel { bool loaded_{false}; bool enabled_{true}; + bool tensor_arena_size_probed_{false}; bool unprocessed_probability_status_{false}; uint8_t current_stride_step_{0}; int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION}; From 4d4f78de813c3bedf9915c2c28b976f28db96523 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:26:09 -0400 Subject: [PATCH 1394/2056] [sx127x][cc1101][sx126x] Use GPIO interrupt to wake loop (#15627) --- esphome/components/cc1101/cc1101.cpp | 25 ++++++++++++++++--------- esphome/components/cc1101/cc1101.h | 1 + esphome/components/sx126x/sx126x.cpp | 9 +++++++++ esphome/components/sx126x/sx126x.h | 2 ++ esphome/components/sx127x/sx127x.cpp | 9 ++++----- esphome/components/sx127x/sx127x.h | 2 ++ 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index f2b7451721a..c231f314cc2 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -102,6 +102,8 @@ CC1101Component::CC1101Component() { memset(this->pa_table_, 0, sizeof(this->pa_table_)); } +void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); } + void CC1101Component::setup() { this->spi_setup(); this->cs_->digital_write(true); @@ -148,11 +150,12 @@ void CC1101Component::setup() { // Defer pin mode setup until after all components have completed setup() // This handles the case where remote_transmitter runs after CC1101 and changes pin mode if (this->gdo0_pin_ != nullptr) { - this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); }); - } - - if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { - this->disable_loop(); + this->defer([this]() { + this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); + if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } + }); } } @@ -164,6 +167,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float } void CC1101Component::loop() { + this->disable_loop(); if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr || !this->gdo0_pin_->digital_read()) { return; @@ -244,6 +248,7 @@ void CC1101Component::begin_tx() { this->write_(Register::PKTCTRL0, 0x32); ESP_LOGV(TAG, "Beginning TX sequence"); if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->detach_interrupt(); this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can @@ -673,10 +678,12 @@ void CC1101Component::set_packet_mode(bool value) { this->state_.GDO0_CFG = 0x0D; } if (this->initialized_) { - if (value) { - this->enable_loop(); - } else { - this->disable_loop(); + if (this->gdo0_pin_ != nullptr) { + if (value) { + this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } else { + this->gdo0_pin_->detach_interrupt(); + } } this->write_(Register::PKTCTRL0); this->write_(Register::PKTCTRL1); diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 2efd9e082d5..68d81ac8f3c 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -93,6 +93,7 @@ class CC1101Component : public Component, // GDO pin for packet reception InternalGPIOPin *gdo0_pin_{nullptr}; + static void IRAM_ATTR gpio_intr(CC1101Component *arg); // Packet handling void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index ec62fad10af..6ea09e3a9e8 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -104,11 +104,17 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) { delayMicroseconds(SWITCHING_DELAY_US); } +void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); } + void SX126x::setup() { // setup pins this->busy_pin_->setup(); this->rst_pin_->setup(); this->dio1_pin_->setup(); + if (this->dio1_pin_->is_internal()) { + static_cast<InternalGPIOPin *>(this->dio1_pin_) + ->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + } // start spi this->spi_setup(); @@ -348,6 +354,9 @@ void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo } void SX126x::loop() { + if (this->dio1_pin_->is_internal()) { + this->disable_loop(); + } if (!this->dio1_pin_->digital_read()) { return; } diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index a758d637955..edc00e3727e 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -3,6 +3,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "sx126x_reg.h" #include <utility> #include <vector> @@ -100,6 +101,7 @@ class SX126x : public Component, Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: + static void IRAM_ATTR gpio_intr(SX126x *arg); void configure_fsk_ook_(); void configure_lora_(); void set_packet_params_(uint8_t payload_length); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 83be96767a4..2b13efb38da 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -53,6 +53,8 @@ void SX127x::write_fifo_(const std::vector<uint8_t> &packet) { this->disable(); } +void IRAM_ATTR SX127x::gpio_intr(SX127x *arg) { arg->enable_loop_soon_any_context(); } + void SX127x::setup() { // setup reset this->rst_pin_->setup(); @@ -60,6 +62,7 @@ void SX127x::setup() { // setup dio0 if (this->dio0_pin_) { this->dio0_pin_->setup(); + this->dio0_pin_->attach_interrupt(&SX127x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); } // start spi @@ -313,6 +316,7 @@ void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, flo } void SX127x::loop() { + this->disable_loop(); if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) { return; } @@ -386,11 +390,6 @@ void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { return; } } - if (mode == MODE_RX && (modulation == MOD_LORA || this->packet_mode_)) { - this->enable_loop(); - } else { - this->disable_loop(); - } } void SX127x::set_mode_rx() { diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index be7b6d8d9f8..76f942fddac 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -4,6 +4,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include <vector> namespace esphome { @@ -86,6 +87,7 @@ class SX127x : public Component, Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: + static void IRAM_ATTR gpio_intr(SX127x *arg); void configure_fsk_ook_(); void configure_lora_(); void set_mode_(uint8_t modulation, uint8_t mode); From 66a4752e133b3c84114150711c13195f03f2f1e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 10 Apr 2026 13:13:05 -1000 Subject: [PATCH 1395/2056] [rp2040] Fix W5500 Ethernet pbuf corruption by mirroring LWIPMutex semantics (#15624) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/rp2040/helpers.cpp | 31 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index 8cb5f7c18df..6e5ddad2364 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -9,7 +9,7 @@ #include <WiFi.h> #include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) -#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock) +#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below) #include "esphome/components/ethernet/ethernet_component.h" #endif #include <hardware/structs/rosc.h> @@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } // main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat // assertion failures). See esphome#10681. // -// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end. -// Both acquire the async_context recursive mutex to prevent IRQ callbacks from -// firing during critical sections. +// WiFi uses cyw43_arch_lwip_begin/end. +// +// For wired Ethernet, taking only the async_context lock is NOT enough. The +// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP` +// counter to decide whether to defer packet processing. If we hold the +// async_context lock without bumping `__inLWIP`, an interrupt-driven packet +// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat` +// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's +// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the +// lock, and on release re-unmask any GPIO IRQs that were deferred while we +// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because +// pulling lwip_wrap.h there poisons many TUs with lwIP types. // // When neither WiFi nor Ethernet is configured, this is a no-op since // there's no network stack and no lwip callbacks to race with. @@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); } LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); } #elif defined(USE_ETHERNET) -LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); } -LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); } +LwIPLock::LwIPLock() { + __inLWIP++; + ethernet_arch_lwip_begin(); +} +LwIPLock::~LwIPLock() { + ethernet_arch_lwip_end(); + __inLWIP--; + if (__needsIRQEN && !__inLWIP) { + __needsIRQEN = false; + ethernet_arch_lwip_gpio_unmask(); + } +} #else LwIPLock::LwIPLock() {} LwIPLock::~LwIPLock() {} From b45f94d511cbab6bc6e095c213c4da8dfbaf6f9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:55:15 -1000 Subject: [PATCH 1396/2056] Bump aioesphomeapi from 44.13.1 to 44.13.2 (#15637) 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 d7db44454cd..a02235ae4c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.1 +aioesphomeapi==44.13.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 5084c610166324dda50549fc74a26eb5f790a33c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:58:20 +0000 Subject: [PATCH 1397/2056] Bump aioesphomeapi from 44.13.2 to 44.13.3 (#15641) 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 a02235ae4c2..e4a019a4900 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.2 +aioesphomeapi==44.13.3 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 2a89d4835f79bf6952c616433bc371e78161de84 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:54:30 -0400 Subject: [PATCH 1398/2056] [mdns] Bump espressif/mdns to 1.11.0 (#15670) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 79d355e8ae9..7c36295e8d9 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -170,7 +170,7 @@ async def to_code(config): cg.add_library("LEAmDNS", None) if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.10.0") + add_idf_component(name="espressif/mdns", ref="1.11.0") cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1e40fef2dc3..bf42730e673 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -14,7 +14,7 @@ dependencies: espressif/esp32-camera: version: 2.1.6 espressif/mdns: - version: 1.10.0 + version: 1.11.0 espressif/esp_wifi_remote: version: 1.4.0 rules: From 0578e433528f4a6f88e80404aa88846814b3c49c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:58:51 -0400 Subject: [PATCH 1399/2056] [canbus] Fix canbus.send can_id compile error (#15668) --- esphome/components/canbus/__init__.py | 1 - tests/components/canbus/common.yaml | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 7d3bf78f492..fcd342ad389 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -162,7 +162,6 @@ async def canbus_action_to_code(config, action_id, template_arg, args): await cg.register_parented(var, config[CONF_CANBUS_ID]) if (can_id := config.get(CONF_CAN_ID)) is not None: - can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index 8bddeb74094..e779f7f078b 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -50,6 +50,13 @@ button: - platform: template name: Canbus Actions on_press: + - canbus.send: + can_id: 0x601 + data: [0, 1, 2] + - canbus.send: + can_id: 0x1FFFFFFF + use_extended_id: true + data: [0, 1, 2] - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; From 6480868e6ef7d2ea3e5c80a7f63b379d969a391c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:07:04 -0400 Subject: [PATCH 1400/2056] [esp32] Bump platform to 55.03.38, Arduino to 3.3.8, ESP-IDF to 5.5.4 (#15666) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 20 ++-- esphome/components/esp32/boards.py | 16 +++ esphome/platformio_api.py | 129 +----------------------- esphome/platformio_runner.py | 114 +++++++++++++++++++++ platformio.ini | 10 +- pyproject.toml | 1 - tests/unit_tests/conftest.py | 6 +- tests/unit_tests/test_platformio_api.py | 119 +++++++--------------- 9 files changed, 192 insertions(+), 225 deletions(-) create mode 100644 esphome/platformio_runner.py diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 5c7eab517bd..cd61d9ec480 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6 +d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f27690c97b4..cd38c82dd89 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -671,11 +671,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 7), - "latest": cv.Version(3, 3, 7), - "dev": cv.Version(3, 3, 7), + "recommended": cv.Version(3, 3, 8), + "latest": cv.Version(3, 3, 8), + "dev": cv.Version(3, 3, 8), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 8): cv.Version(55, 3, 38), cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), @@ -695,6 +696,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 8): cv.Version(5, 5, 4), cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), @@ -714,17 +716,15 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 3, "1"), - "latest": cv.Version(5, 5, 3, "1"), + "recommended": cv.Version(5, 5, 4), + "latest": cv.Version(5, 5, 4), "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", - cv.Version( - 5, 5, 4 - ): "https://github.com/pioarduino/platform-espressif32.git#develop", + cv.Version(5, 5, 4): cv.Version(55, 3, 38), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), @@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 37), - "latest": cv.Version(55, 3, 37), + "recommended": cv.Version(55, 3, 38), + "latest": cv.Version(55, 3, 38), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 2bd08e7c392..2c73fe7d08d 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1960,6 +1960,10 @@ BOARDS = { "name": "Hornbill ESP32 Minima", "variant": VARIANT_ESP32, }, + "huidu_hd_wf1": { + "name": "Huidu HD-WF1", + "variant": VARIANT_ESP32S2, + }, "huidu_hd_wf2": { "name": "Huidu HD-WF2", "variant": VARIANT_ESP32S3, @@ -2028,6 +2032,10 @@ BOARDS = { "name": "LilyGo T-Display-S3", "variant": VARIANT_ESP32S3, }, + "lilygo-t-energy-s3": { + "name": "LilyGo T-Energy-S3", + "variant": VARIANT_ESP32S3, + }, "lilygo-t3-s3": { "name": "LilyGo T3-S3", "variant": VARIANT_ESP32S3, @@ -2289,10 +2297,18 @@ BOARDS = { "name": "S.ODI Ultra v1", "variant": VARIANT_ESP32, }, + "seeed_xiao_esp32_s3_plus": { + "name": "Seeed Studio XIAO ESP32S3 Plus", + "variant": VARIANT_ESP32S3, + }, "seeed_xiao_esp32c3": { "name": "Seeed Studio XIAO ESP32C3", "variant": VARIANT_ESP32C3, }, + "seeed_xiao_esp32c5": { + "name": "Seeed Studio XIAO ESP32C5", + "variant": VARIANT_ESP32C5, + }, "seeed_xiao_esp32c6": { "name": "Seeed Studio XIAO ESP32C6", "variant": VARIANT_ESP32C6, diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index cb080b2a953..e9719f7dcd8 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -5,104 +5,15 @@ import os from pathlib import Path import re import subprocess -import time -from typing import Any +import sys from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError -from esphome.util import run_external_command, run_external_process +from esphome.util import run_external_process _LOGGER = logging.getLogger(__name__) -def patch_structhash(): - # Patch platformio's structhash to not recompile the entire project when files are - # removed/added. This might have unintended consequences, but this improves compile - # times greatly when adding/removing components and a simple clean build solves - # all issues - from platformio.run import cli, helpers - - def patched_clean_build_dir(build_dir, *args): - from platformio import fs - from platformio.project.helpers import get_project_dir - - platformio_ini = Path(get_project_dir()) / "platformio.ini" - - build_dir = Path(build_dir) - - # if project's config is modified - if ( - build_dir.is_dir() - and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime - ): - fs.rmtree(build_dir) - - if not build_dir.is_dir(): - build_dir.mkdir(parents=True) - - helpers.clean_build_dir = patched_clean_build_dir - cli.clean_build_dir = patched_clean_build_dir - - -def patch_file_downloader(): - """Patch PlatformIO's FileDownloader to retry on PackageException errors. - - PlatformIO's FileDownloader uses HTTPSession which lacks built-in retry - for 502/503 errors. We add retries with exponential backoff and close the - session between attempts to force a fresh TCP connection, which may route - to a different CDN edge node. - """ - from platformio.package.download import FileDownloader - from platformio.package.exception import PackageException - - if getattr(FileDownloader.__init__, "_esphome_patched", False): - return - - original_init = FileDownloader.__init__ - - def patched_init(self, *args: Any, **kwargs: Any) -> None: - max_retries = 5 - - for attempt in range(max_retries): - try: - original_init(self, *args, **kwargs) - return - except PackageException as e: - if attempt < max_retries - 1: - # Exponential backoff: 2, 4, 8, 16 seconds - delay = 2 ** (attempt + 1) - _LOGGER.warning( - "Package download failed: %s. " - "Retrying in %d seconds... (attempt %d/%d)", - str(e), - delay, - attempt + 1, - max_retries, - ) - # Close the response and session to free resources - # and force a new TCP connection on retry, which may - # route to a different CDN edge node - # pylint: disable=protected-access,broad-except - try: - if ( - hasattr(self, "_http_response") - and self._http_response is not None - ): - self._http_response.close() - if hasattr(self, "_http_session"): - self._http_session.close() - except Exception: - pass - # pylint: enable=protected-access,broad-except - time.sleep(delay) - else: - # Final attempt - re-raise - raise - - patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access - FileDownloader.__init__ = patched_init - - IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" FILTER_PLATFORMIO_LINES = [ r"Verbose mode can be enabled via `-v, --verbose` option.*", @@ -142,20 +53,6 @@ FILTER_PLATFORMIO_LINES = [ ] -class PlatformioLogFilter(logging.Filter): - """Filter to suppress noisy platformio log messages.""" - - _PATTERN = re.compile( - r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES) - ) - - def filter(self, record: logging.LogRecord) -> bool: - # Only filter messages from platformio-related loggers - if "platformio" not in record.name.lower(): - return True - return self._PATTERN.match(record.getMessage()) is None - - def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -166,30 +63,12 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") # Increase uv retry count to handle transient network errors (default is 3) os.environ.setdefault("UV_HTTP_RETRIES", "10") - cmd = ["platformio"] + list(args) + cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) if not CORE.verbose: kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES - if os.environ.get("ESPHOME_USE_SUBPROCESS") is not None: - return run_external_process(*cmd, **kwargs) - - import platformio.__main__ - - patch_structhash() - patch_file_downloader() - - # Add log filter to suppress noisy platformio messages - log_filter = PlatformioLogFilter() if not CORE.verbose else None - if log_filter: - for handler in logging.getLogger().handlers: - handler.addFilter(log_filter) - try: - return run_external_command(platformio.__main__.main, *cmd, **kwargs) - finally: - if log_filter: - for handler in logging.getLogger().handlers: - handler.removeFilter(log_filter) + return run_external_process(*cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py new file mode 100644 index 00000000000..408d49d1a6f --- /dev/null +++ b/esphome/platformio_runner.py @@ -0,0 +1,114 @@ +"""Subprocess entry point that applies ESPHome's PlatformIO patches. + +Invoked via ``python -m esphome.platformio_runner`` instead of +``python -m platformio`` so that the patches (incremental rebuild +preservation, download retries) apply inside the subprocess. Running +PlatformIO in a subprocess keeps its ``sys.path`` mutations and other +global state from leaking into the ESPHome process. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +import sys +import time +from typing import Any + +_LOGGER = logging.getLogger(__name__) + + +def patch_structhash() -> None: + """Avoid full rebuilds when files are added or removed. + + PlatformIO clears the build dir whenever its structure hash changes. + We replace that with an mtime check against ``platformio.ini`` so + incremental builds are preserved unless the project config changed. + """ + from platformio.run import cli, helpers + + def patched_clean_build_dir(build_dir, *_args): + from platformio import fs + from platformio.project.helpers import get_project_dir + + platformio_ini = Path(get_project_dir()) / "platformio.ini" + build_dir = Path(build_dir) + + if ( + build_dir.is_dir() + and platformio_ini.stat().st_mtime > build_dir.stat().st_mtime + ): + fs.rmtree(build_dir) + + if not build_dir.is_dir(): + build_dir.mkdir(parents=True) + + helpers.clean_build_dir = patched_clean_build_dir + cli.clean_build_dir = patched_clean_build_dir + + +def patch_file_downloader() -> None: + """Retry PlatformIO package downloads with exponential backoff. + + PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in + retry for 502/503 errors. We wrap ``__init__`` to retry on + ``PackageException`` and close the session between attempts so a new + TCP connection can route to a different CDN edge node. + """ + from platformio.package.download import FileDownloader + from platformio.package.exception import PackageException + + if getattr(FileDownloader.__init__, "_esphome_patched", False): + return + + original_init = FileDownloader.__init__ + + def patched_init(self, *args: Any, **kwargs: Any) -> None: + max_retries = 5 + + for attempt in range(max_retries): + try: + original_init(self, *args, **kwargs) + return + except PackageException as e: + if attempt < max_retries - 1: + delay = 2 ** (attempt + 1) + _LOGGER.warning( + "Package download failed: %s. " + "Retrying in %d seconds... (attempt %d/%d)", + str(e), + delay, + attempt + 1, + max_retries, + ) + # pylint: disable=protected-access,broad-except + try: + if ( + hasattr(self, "_http_response") + and self._http_response is not None + ): + self._http_response.close() + if hasattr(self, "_http_session"): + self._http_session.close() + except Exception: + pass + # pylint: enable=protected-access,broad-except + time.sleep(delay) + else: + raise + + patched_init._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access + FileDownloader.__init__ = patched_init + + +def main() -> int: + patch_structhash() + patch_file_downloader() + + import platformio.__main__ + + return platformio.__main__.main() or 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/platformio.ini b/platformio.ini index e0f7c7d443a..7d17628a8ff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,10 +133,10 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -169,9 +169,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.3.1/esp-idf-v5.5.3.1.tar.xz + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz framework = espidf lib_deps = diff --git a/pyproject.toml b/pyproject.toml index 2e3a247768b..a744286e888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ classifiers = [ "Topic :: Home Automation", ] -# Python 3.14 is not supported on Windows, see https://github.com/zephyrproject-rtos/windows-curses/issues/76 requires-python = ">=3.11.0,<3.15" dynamic = ["dependencies", "optional-dependencies", "version"] diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 1a1bfffd03d..dfd4305c4d4 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -84,9 +84,9 @@ def mock_decode_pc() -> Generator[Mock, None, None]: @pytest.fixture -def mock_run_external_command() -> Generator[Mock, None, None]: - """Mock run_external_command for platformio_api.""" - with patch("esphome.platformio_api.run_external_command") as mock: +def mock_run_external_process() -> Generator[Mock, None, None]: + """Mock run_external_process for platformio_api.""" + with patch("esphome.platformio_api.run_external_process") as mock: yield mock diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index e1b3908c249..ddc4e45c844 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -1,7 +1,8 @@ """Tests for platformio_api.py path functions.""" +# pylint: disable=protected-access + import json -import logging import os from pathlib import Path import shutil @@ -10,7 +11,7 @@ from unittest.mock import MagicMock, Mock, call, patch import pytest -from esphome import platformio_api +from esphome import platformio_api, platformio_runner from esphome.core import CORE, EsphomeError @@ -281,13 +282,13 @@ def test_run_idedata_raises_on_invalid_json( def test_run_platformio_cli_sets_environment_variables( - setup_core: Path, mock_run_external_command: Mock + setup_core: Path, mock_run_external_process: Mock ) -> None: """Test run_platformio_cli sets correct environment variables.""" CORE.build_path = str(setup_core / "build" / "test") with patch.dict(os.environ, {}, clear=False): - mock_run_external_command.return_value = 0 + mock_run_external_process.return_value = 0 platformio_api.run_platformio_cli("test", "arg") # Check environment variables were set @@ -300,10 +301,12 @@ def test_run_platformio_cli_sets_environment_variables( assert "PLATFORMIO_LIBDEPS_DIR" in os.environ assert "PYTHONWARNINGS" in os.environ - # Check command was called correctly - mock_run_external_command.assert_called_once() - args = mock_run_external_command.call_args[0] - assert "platformio" in args + # Check command was called correctly — runs PlatformIO as a subprocess + # via the esphome.platformio_runner entry point. + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert "-m" in args + assert "esphome.platformio_runner" in args assert "test" in args assert "arg" in args @@ -444,7 +447,7 @@ def test_patch_structhash(setup_core: Path) -> None: }, ): # Call patch_structhash - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Verify both modules had clean_build_dir patched # Check that clean_build_dir was set on both modules @@ -496,7 +499,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -546,7 +549,7 @@ def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -594,7 +597,7 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_api.patch_structhash() + platformio_runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -719,7 +722,7 @@ def test_patch_file_downloader_succeeds_first_try() -> None: ), }, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -758,7 +761,7 @@ def test_patch_file_downloader_retries_on_failure() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -799,7 +802,7 @@ def test_patch_file_downloader_raises_after_max_retries() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -847,7 +850,7 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() -> ), patch("time.sleep"), ): - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -882,9 +885,9 @@ def test_patch_file_downloader_idempotent() -> None: }, ): # Patch multiple times - platformio_api.patch_file_downloader() - platformio_api.patch_file_downloader() - platformio_api.patch_file_downloader() + platformio_runner.patch_file_downloader() + platformio_runner.patch_file_downloader() + platformio_runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -895,19 +898,18 @@ def test_patch_file_downloader_idempotent() -> None: assert call_count == 1 -def test_platformio_log_filter_allows_non_platformio_messages() -> None: - """Test that non-platformio logger messages are allowed through.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="esphome.core", - level=logging.INFO, - pathname="", - lineno=0, - msg="Some esphome message", - args=(), - exc_info=None, +def _filter_through_redirect(line: str) -> str: + """Write a line through RedirectText with FILTER_PLATFORMIO_LINES and return what passes.""" + import io + + from esphome.util import RedirectText + + captured = io.StringIO() + redirect = RedirectText( + captured, filter_lines=platformio_api.FILTER_PLATFORMIO_LINES ) - assert log_filter.filter(record) is True + redirect.write(line + "\n") + return captured.getvalue() @pytest.mark.parametrize( @@ -930,19 +932,9 @@ def test_platformio_log_filter_allows_non_platformio_messages() -> None: "Memory Usage -> https://bit.ly/pio-memory-usage", ], ) -def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: - """Test that noisy platformio messages are filtered out.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="platformio.builder", - level=logging.INFO, - pathname="", - lineno=0, - msg=msg, - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is False +def test_filter_platformio_lines_blocks_noisy_messages(msg: str) -> None: + """Test that noisy platformio output lines are filtered out by RedirectText.""" + assert _filter_through_redirect(msg) == "" @pytest.mark.parametrize( @@ -954,39 +946,6 @@ def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: "warning: unused variable", ], ) -def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None: - """Test that non-noisy platformio messages are allowed through.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name="platformio.builder", - level=logging.INFO, - pathname="", - lineno=0, - msg=msg, - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is True - - -@pytest.mark.parametrize( - "logger_name", - [ - "PLATFORMIO.builder", - "PlatformIO.core", - "platformio.run", - ], -) -def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None: - """Test that platformio logger name matching is case insensitive.""" - log_filter = platformio_api.PlatformioLogFilter() - record = logging.LogRecord( - name=logger_name, - level=logging.INFO, - pathname="", - lineno=0, - msg="Found 5 compatible libraries", - args=(), - exc_info=None, - ) - assert log_filter.filter(record) is False +def test_filter_platformio_lines_allows_other_messages(msg: str) -> None: + """Test that non-noisy platformio output lines pass through RedirectText.""" + assert _filter_through_redirect(msg) == msg + "\n" From 8790dec13756be30c59e2ae2544ada9b6e2b8a06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 10:24:23 -1000 Subject: [PATCH 1401/2056] [packages] Fix false deprecation warning and wrong error paths in nested packages (#15605) --- esphome/components/packages/__init__.py | 34 +++- .../component_tests/packages/test_packages.py | 177 +++++++++++++++++- 2 files changed, 205 insertions(+), 6 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 04db690c6f4..3a15b5b95a4 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -45,6 +45,18 @@ def is_remote_package(package_config: dict) -> bool: return CONF_URL in package_config +def is_package_definition(value: object) -> bool: + """Returns True if the value looks like a package definition rather than a config fragment. + + Package definitions are IncludeFile objects, git URL shorthand strings, or + remote package dicts (containing a ``url:`` key). Config fragments are + plain dicts that represent component configuration. + """ + return isinstance(value, (yaml_util.IncludeFile, str)) or ( + isinstance(value, dict) and is_remote_package(value) + ) + + def valid_package_contents(package_config: dict) -> dict: """Validate that a package looks like a plausible ESPHome config fragment. @@ -318,11 +330,11 @@ def _walk_packages( if not isinstance(packages, dict): _walk_package_list(packages, callback, context) elif (result := _walk_package_dict(packages, callback, context)) is not None: - if not validate_deprecated: + if not validate_deprecated or any( + is_package_definition(v) for v in packages.values() + ): raise result # Fallback: treat the dict as a single deprecated package. - # Note: this catches *any* cv.Invalid from the callback, which may - # mask real validation errors in named package dicts. # This block can be removed once the single-package # deprecation period (2026.7.0) is over. config[CONF_PACKAGES] = [packages] @@ -461,6 +473,9 @@ class _PackageProcessor: self, package_config: dict | str, context_vars: ContextVars | None ) -> dict: """Resolve a single package and recurse into any nested packages.""" + from_remote = isinstance(package_config, dict) and is_remote_package( + package_config + ) package_config = self.resolve_package(package_config, context_vars) self.collect_substitutions(package_config) @@ -470,7 +485,18 @@ class _PackageProcessor: # Push context from !include vars on the package root and on the packages key context_vars = push_context(package_config, context_vars) context_vars = push_context(package_config[CONF_PACKAGES], context_vars) - return _walk_packages(package_config, self.process_package, context_vars) + # Disable the deprecated single-package fallback for remote + # packages. _process_remote_package returns dicts with + # already-resolved values that is_package_definition cannot + # distinguish from config fragments, so the fallback would + # always fire and mask real errors with wrong paths + # (packages->0 instead of packages-><name>). + return _walk_packages( + package_config, + self.process_package, + context_vars, + validate_deprecated=not from_remote, + ) def do_packages_pass( diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0893c7dcbbc..0b828d757ed 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1,11 +1,18 @@ """Tests for the packages component.""" +import logging from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +from esphome.components.packages import ( + CONFIG_SCHEMA, + _walk_packages, + do_packages_pass, + is_package_definition, + merge_packages, +) from esphome.components.substitutions import do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove @@ -37,7 +44,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict -from esphome.yaml_util import add_context +from esphome.yaml_util import IncludeFile, add_context # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -79,6 +86,44 @@ def packages_pass(config): return config +_INCLUDE_FILE = "INCLUDE_FILE" + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + # IncludeFile objects are package definitions + (_INCLUDE_FILE, True), + # Git URL shorthand strings are package definitions + ("github://esphome/firmware/base.yaml@main", True), + # Remote package dicts (with url key) are package definitions + ({"url": "https://github.com/esphome/firmware", "file": "base.yaml"}, True), + # Plain config dicts are NOT package definitions (they are config fragments) + ({"wifi": {"ssid": "test"}}, False), + # None is not a package definition + (None, False), + # Lists are not package definitions + ([{"wifi": {"ssid": "test"}}], False), + # Empty dicts are not package definitions + ({}, False), + ], + ids=[ + "include_file", + "git_shorthand", + "remote_package", + "config_fragment", + "none", + "list", + "empty_dict", + ], +) +def test_is_package_definition(value: object, expected: bool) -> None: + """Test that is_package_definition correctly identifies package definitions.""" + if value is _INCLUDE_FILE: + value = MagicMock(spec=IncludeFile) + assert is_package_definition(value) is expected + + def test_package_unused(basic_esphome, basic_wifi) -> None: """ Ensures do_package_pass does not change a config if packages aren't used. @@ -1107,6 +1152,134 @@ def test_invalid_package_contents_masked_by_deprecation( do_packages_pass(config) +def test_named_dict_with_include_files_no_false_deprecation_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Package errors in named dicts must not trigger the deprecated fallback.""" + good_include = MagicMock(spec=IncludeFile) + bad_include = MagicMock(spec=IncludeFile) + + config = { + CONF_PACKAGES: { + "good_pkg": good_include, + "bad_pkg": bad_include, + }, + } + + call_count = 0 + + def failing_callback(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + if call_count == 1: + # First package processes fine + return {CONF_WIFI: {CONF_SSID: "test"}} + # Second package has an error (e.g. jinja syntax error) + raise cv.Invalid("simulated jinja error in bad_pkg") + + with ( + caplog.at_level(logging.WARNING), + pytest.raises(cv.Invalid, match="simulated jinja error"), + ): + _walk_packages(config, failing_callback) + + # Must NOT emit the deprecated single-package warning + assert "deprecated" not in caplog.text.lower() + + +def test_validate_deprecated_false_raises_directly( + caplog: pytest.LogCaptureFixture, +) -> None: + """With validate_deprecated=False, errors raise directly without fallback. + + This is the codepath used for remote packages where _process_remote_package + returns already-resolved dicts that is_package_definition cannot detect. + """ + config = { + CONF_PACKAGES: { + "pkg_a": {CONF_WIFI: {CONF_SSID: "test"}}, + "pkg_b": {CONF_WIFI: {CONF_SSID: "test2"}}, + }, + } + + call_count = 0 + + def failing_callback(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + if call_count == 1: + return package_config + raise cv.Invalid("nested error") + + with ( + caplog.at_level(logging.WARNING), + pytest.raises(cv.Invalid, match="nested error"), + ): + _walk_packages(config, failing_callback, validate_deprecated=False) + + assert "deprecated" not in caplog.text.lower() + + +def test_error_on_first_declared_package_still_detected() -> None: + """When the first declared package errors, it's the last processed in reverse. + + All other entries are already resolved to dicts, but the failing entry + retains its original IncludeFile value since assignment was skipped. + """ + config = { + CONF_PACKAGES: { + "first_pkg": MagicMock(spec=IncludeFile), + "second_pkg": MagicMock(spec=IncludeFile), + "third_pkg": MagicMock(spec=IncludeFile), + }, + } + + call_count = 0 + + def fail_on_last(package_config: dict, context: object) -> dict: + nonlocal call_count + call_count += 1 + # Reverse iteration: third_pkg (1), second_pkg (2), first_pkg (3) + if call_count < 3: + return {CONF_WIFI: {CONF_SSID: "test"}} + raise cv.Invalid("error in first_pkg") + + with pytest.raises(cv.Invalid, match="error in first_pkg"): + _walk_packages(config, fail_on_last) + + +def test_deprecated_single_package_fallback_still_works( + caplog: pytest.LogCaptureFixture, +) -> None: + """The deprecated single-package form still falls back at the top level. + + When a dict's values are plain config fragments (not package definitions) + and the callback fails, the deprecated fallback wraps the dict in a list + and retries with a deprecation warning. + """ + config = { + CONF_PACKAGES: { + CONF_WIFI: {CONF_SSID: "test", CONF_PASSWORD: "secret"}, + }, + } + + attempt = 0 + + def fail_then_succeed(package_config: dict, context: object) -> dict: + nonlocal attempt + attempt += 1 + if attempt == 1: + # First attempt: treating as named dict fails + raise cv.Invalid("not a valid package") + # Second attempt: after fallback wraps as list, succeeds + return package_config + + with caplog.at_level(logging.WARNING): + _walk_packages(config, fail_then_succeed) + + assert "deprecated" in caplog.text.lower() + + def test_merge_packages_invalid_nested_type_raises() -> None: """Invalid nested packages type during merge raises cv.Invalid.""" config = { From e264c9745438957c5b02c77d33980012b13e0dbf Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:29:11 +1000 Subject: [PATCH 1402/2056] [lvgl] Fix use of rotation on host SDL (#15611) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 2 +- tests/components/lvgl/test.host.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index f6f6204f4c4..b6421dc43d7 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -341,7 +341,7 @@ async def to_code(configs): df.LOGGER.info("LVGL will use hardware rotation via display driver") else: rotation_type = RotationType.ROTATION_SOFTWARE - if get_esp32_variant() == VARIANT_ESP32P4: + if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4: df.LOGGER.info("LVGL will use software rotation (PPA accelerated)") else: df.LOGGER.info("LVGL will use software rotation") diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index f84156c9d82..6328648fe34 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -20,6 +20,7 @@ lvgl: - id: lvgl_0 default_font: space16 displays: sdl0 + rotation: 180 top_layer: - id: lvgl_1 From a408b5a4fe4278cbbf6a033dff7e98c3e0bcf009 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:48:19 +1200 Subject: [PATCH 1403/2056] Bump version to 2026.4.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index e60c994ea94..599109b43f3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0b1 +PROJECT_NUMBER = 2026.4.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 576a4b31d55..b4cd7583302 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0b1" +__version__ = "2026.4.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From daa68a2a603b8954974f331ef619adcbdfead8d4 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:48:30 +0200 Subject: [PATCH 1404/2056] [packages] fix support `packages: !include mypackages.yaml` (#15677) --- esphome/components/packages/__init__.py | 13 +++--- .../component_tests/packages/test_packages.py | 45 +++++++++++++++++++ ...13-packages_as_included_list.approved.yaml | 3 ++ .../13-packages_as_included_list.input.yaml | 4 ++ .../substitutions/13-packages_list.yaml | 2 + ...14-packages_as_included_dict.approved.yaml | 3 ++ .../14-packages_as_included_dict.input.yaml | 4 ++ .../substitutions/14-packages_dict.yaml | 3 ++ 8 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_list.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 3a15b5b95a4..3f3df753512 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -321,12 +321,15 @@ def _walk_packages( return config packages = config[CONF_PACKAGES] - if not isinstance(packages, (dict, list)): - raise cv.Invalid( - f"Packages must be a key to value mapping or list, got {type(packages)} instead" - ) - with cv.prepend_path(CONF_PACKAGES): + if isinstance(packages, yaml_util.IncludeFile): + # If the packages key is an IncludeFile, resolve it first before processing. + packages, _ = resolve_include(packages, [], context, strict_undefined=False) + if not isinstance(packages, (dict, list)): + raise cv.Invalid( + f"Packages must be a key to value mapping or list, got {type(packages)} instead" + ) + if not isinstance(packages, dict): _walk_package_list(packages, callback, context) elif (result := _walk_package_dict(packages, callback, context)) is not None: diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0b828d757ed..cd91c4d8cb1 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1106,6 +1106,51 @@ def test_packages_invalid_type_raises() -> None: do_packages_pass(config) +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_list(mock_resolve_include) -> None: + """When packages: is an IncludeFile that resolves to a list, it is processed correctly.""" + include_file = MagicMock(spec=IncludeFile) + package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + mock_resolve_include.return_value = ([package_content], None) + + config = {CONF_PACKAGES: include_file} + result = do_packages_pass(config) + result = merge_packages(result) + + assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + + +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_dict(mock_resolve_include) -> None: + """When packages: is an IncludeFile that resolves to a dict, it is processed correctly.""" + include_file = MagicMock(spec=IncludeFile) + package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + mock_resolve_include.return_value = ({"network": package_content}, None) + + config = {CONF_PACKAGES: include_file} + result = do_packages_pass(config) + result = merge_packages(result) + + assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + + +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_invalid_type_raises( + mock_resolve_include, +) -> None: + """When packages: is an IncludeFile that resolves to an invalid type, cv.Invalid is raised.""" + include_file = MagicMock(spec=IncludeFile) + mock_resolve_include.return_value = ("not_a_dict_or_list", None) + + config = {CONF_PACKAGES: include_file} + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ) as exc_info: + do_packages_pass(config) + + assert exc_info.value.path == [CONF_PACKAGES] + + @pytest.mark.parametrize( "invalid_package", [ diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml new file mode 100644 index 00000000000..7863def1906 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml @@ -0,0 +1,3 @@ +wifi: + password: pkg_password + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml new file mode 100644 index 00000000000..7a3b4970db6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml @@ -0,0 +1,4 @@ +packages: !include 13-packages_list.yaml + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml new file mode 100644 index 00000000000..23161db3d3c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml @@ -0,0 +1,2 @@ +- wifi: + password: pkg_password diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml new file mode 100644 index 00000000000..7863def1906 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml @@ -0,0 +1,3 @@ +wifi: + password: pkg_password + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml new file mode 100644 index 00000000000..8b9fc5ec3aa --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml @@ -0,0 +1,4 @@ +packages: !include 14-packages_dict.yaml + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml new file mode 100644 index 00000000000..55e8b38a435 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml @@ -0,0 +1,3 @@ +network: + wifi: + password: pkg_password From 5608aa10a51842d4234a040d2d2519dd90dc706f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:46:49 +1200 Subject: [PATCH 1405/2056] [CI] Don't run label workflow on closed/merged PRs (#15678) --- .github/workflows/auto-label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 27ddfe59118..f5214a5ce0c 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -20,7 +20,7 @@ env: jobs: label: runs-on: ubuntu-latest - if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' + if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot') steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 41c9ed28cd2c8ebc9712c670f8a1f0b34b808c25 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:23:01 +0200 Subject: [PATCH 1406/2056] [esp32] Use static stack memory for loop task instead of heap (#15659) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/esp32/core.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 313818e6019..add50dcf4da 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -61,6 +61,9 @@ uint32_t arch_get_cpu_freq_hz() { } TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static StackType_t + loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void loop_task(void *pv_params) { setup(); @@ -73,9 +76,11 @@ extern "C" void app_main() { initArduino(); esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE - xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); + loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack, + &loop_task_tcb); #else - xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); + loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, + loop_task_stack, &loop_task_tcb, 1); #endif } From ac8a2467a5004908ed06b13f1bbc5a3c30341f89 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:51:55 -0400 Subject: [PATCH 1407/2056] [core] Fix PlatformIO progress bar rendering in subprocess mode (#15681) --- esphome/platformio_api.py | 3 --- esphome/platformio_runner.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e9719f7dcd8..fc21977fdd1 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -65,9 +65,6 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("UV_HTTP_RETRIES", "10") cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) - if not CORE.verbose: - kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES - return run_external_process(*cmd, **kwargs) diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py index 408d49d1a6f..92700d5c42d 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio_runner.py @@ -105,6 +105,36 @@ def main() -> int: patch_structhash() patch_file_downloader() + # Wrap stdout/stderr with RedirectText before PlatformIO runs: + # + # 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and + # PlatformIO's own progress-bar code check ``stream.isatty()`` to + # decide whether to emit TTY-format output (``\r`` cursor moves, ANSI + # colors, fancy progress bars). With the wrapper in place they always + # emit TTY format, even when our real stdout is a pipe to the parent + # process. Downstream consumers (local terminals and the Home + # Assistant dashboard log viewer) render the TTY control sequences + # correctly, so the user sees real progress bars. + # + # 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in + # this subprocess, so noisy PlatformIO output is dropped before it + # ever leaves the runner. This replaces the parent-side filtering + # that was lost when we switched from in-process to subprocess — the + # parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and + # bypasses its ``write()`` path entirely. + # + # Filtering is disabled when the user passed -v / --verbose to + # ``esphome compile``, preserving the previous in-process behavior where + # verbose mode let all PlatformIO output through unfiltered. + from esphome.platformio_api import FILTER_PLATFORMIO_LINES + from esphome.util import RedirectText + + is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:]) + filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES + + sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + import platformio.__main__ return platformio.__main__.main() or 0 From d4e9c62d92d24054c57a5915570f90b3b9aedade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:23:49 +0000 Subject: [PATCH 1408/2056] Bump aioesphomeapi from 44.13.3 to 44.14.0 (#15695) 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 1466eccc9b7..fa642b579ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.3 +aioesphomeapi==44.14.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From c62a75ee17e0ef038e0dc32e7dc097a1a446e755 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 20:40:33 -1000 Subject: [PATCH 1409/2056] [benchmark] Use -Os to match firmware optimization level (#15688) --- script/cpp_benchmark.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index 92faa05819a..5080a9fec74 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -26,12 +26,11 @@ CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core" STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs" PLATFORMIO_OPTIONS = { - "build_unflags": [ - "-Os", # remove default size-opt - ], "build_flags": [ - "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) + "-Os", # match firmware optimization level (detects inlining regressions) "-g", # debug symbols for profiling + "-ffunction-sections", # required for dead-code stripping with -Os + "-fdata-sections", # required for dead-code stripping with -Os "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() f"-I{STUBS_DIR}", # stub headers for ESP32-only components ], From 4f69c3b850ebb7943b1599881f13c0255f078da7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 21:03:53 -1000 Subject: [PATCH 1410/2056] [benchmark] Add SubscribeLogsResponse encode benchmarks (#15696) --- .../components/api/bench_log_response.cpp | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/benchmarks/components/api/bench_log_response.cpp diff --git a/tests/benchmarks/components/api/bench_log_response.cpp b/tests/benchmarks/components/api/bench_log_response.cpp new file mode 100644 index 00000000000..4ef57987beb --- /dev/null +++ b/tests/benchmarks/components/api/bench_log_response.cpp @@ -0,0 +1,118 @@ +#include <benchmark/benchmark.h> + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Typical log line: "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy" +static constexpr const char *kTypicalLogLine = + "[12:34:56][D][sensor:094]: 'Temperature': Sending state 23.50000 with 1 decimals of accuracy"; + +// Short log line: "[12:34:56][I][app:029]: Running..." +static constexpr const char *kShortLogLine = "[12:34:56][I][app:029]: Running..."; + +// --- Encode --- + +static void Encode_LogResponse_Typical(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine)); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_LogResponse_Typical); + +static void Encode_LogResponse_Short(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_INFO; + msg.set_message(reinterpret_cast<const uint8_t *>(kShortLogLine), strlen(kShortLogLine)); + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_LogResponse_Short); + +// --- Calculate Size --- + +static void CalculateSize_LogResponse_Typical(benchmark::State &state) { + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_LogResponse_Typical); + +// --- Calc + Encode (steady state) --- + +static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) { + APIBuffer buffer; + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_LogResponse_Typical); + +// --- Calc + Encode (fresh allocation each time) --- + +static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) { + SubscribeLogsResponse msg; + msg.level = enums::LOG_LEVEL_DEBUG; + msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine)); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_LogResponse_Typical_Fresh); + +} // namespace esphome::api::benchmarks From 5b4385a08497b5f2eda32dea77e206d99e5d3f78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 21:42:31 -1000 Subject: [PATCH 1411/2056] [api] Add speed_optimized proto option for hot encode paths (#15691) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/api/api.proto | 2 ++ esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 16 ++++++++++++---- script/api_protobuf/api_protobuf.py | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e4d0c2d16df..4a9a6f90512 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -671,6 +671,7 @@ message SensorStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; option (no_delay) = true; + option (speed_optimized) = true; fixed32 key = 1 [(force) = true]; float state = 2; @@ -1638,6 +1639,7 @@ message BluetoothLERawAdvertisementsResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BLUETOOTH_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"]; } diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index dacc290e31e..d5d0b37e8df 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -23,6 +23,7 @@ extend google.protobuf.MessageOptions { optional bool no_delay = 1040 [default=false]; optional string base_class = 1041; optional bool inline_encode = 1042 [default=false]; + optional bool speed_optimized = 1043 [default=false]; } extend google.protobuf.FieldOptions { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c2d513f0d33..481510e407b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -745,7 +745,9 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { #endif return size; } -uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); @@ -755,7 +757,9 @@ uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG #endif return pos; } -uint32_t SensorStateResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SensorStateResponse::calculate_size() const { uint32_t size = 0; size += 5; size += ProtoSize::calc_float(1, this->state); @@ -2328,7 +2332,9 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { auto &sub_msg = this->advertisements[i]; @@ -2350,7 +2356,9 @@ uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer P } return pos; } -uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { auto &sub_msg = this->advertisements[i]; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 39bfc865d02..1743d2bb340 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2679,6 +2679,16 @@ def build_message_type( and get_opt(desc, inline_opt, False) ) + # Check if this message wants speed-optimized encode/calculate_size. + # When set, __attribute__((optimize("O2"))) is added to the definitions + # so GCC inlines the small ProtoEncode helpers even under -Os. + is_speed_optimized = get_opt(desc, pb.speed_optimized, False) + speed_attr = ( + '__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n' + if is_speed_optimized + else "" + ) + # Only generate encode method if this message needs encoding and has fields if needs_encode and encode and not is_inline_only: # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls @@ -2688,7 +2698,7 @@ def build_message_type( ) for line in encode ] - o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" + o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" o += indent("\n".join(encode_debug)) + "\n" o += " return pos;\n" @@ -2702,7 +2712,7 @@ def build_message_type( # Add calculate_size method only if this message needs encoding and has fields if needs_encode and size_calc and not is_inline_only: - o = f"uint32_t {desc.name}::calculate_size() const {{\n" + o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n" o += " uint32_t size = 0;\n" o += indent("\n".join(size_calc)) + "\n" o += " return size;\n" From 6db787d5e4cf637182b7b82e1e23b82971a1a46a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:12:57 +0000 Subject: [PATCH 1412/2056] Bump aioesphomeapi from 44.14.0 to 44.15.0 (#15699) 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 fa642b579ba..cd3aa5bd868 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.14.0 +aioesphomeapi==44.15.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From fe6ecb24b4bfc16f096837e39a74664c97e382de Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:49:13 +0200 Subject: [PATCH 1413/2056] [bme68x_bsec2] use esphome-libs wrappers for ESP32 (#15697) --- esphome/components/bme68x_bsec2/__init__.py | 36 +++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index 5f0afa9c9f2..b63443c5f30 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -17,6 +17,7 @@ CODEOWNERS = ["@neffs", "@kbx81"] DOMAIN = "bme68x_bsec2" BSEC2_LIBRARY_VERSION = "1.10.2610" +BME68x_LIBRARY_VERSION = "v1.3.40408" CONF_ALGORITHM_OUTPUT = "algorithm_output" CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id" @@ -184,16 +185,31 @@ async def to_code_base(config): if core.CORE.using_arduino: cg.add_library("Wire", None) cg.add_library("SPI", None) - cg.add_library( - "BME68x Sensor library", - None, - "https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408", - ) - cg.add_library( - "BSEC2 Software Library", - None, - f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}", - ) + + if core.CORE.is_esp32: + from esphome.components.esp32 import add_idf_component + + add_idf_component( + name="boschsensortec/Bosch-BME68x-Library", + repo="https://github.com/esphome-libs/Bosch-BME68x-Library", + ref=BME68x_LIBRARY_VERSION, + ) + add_idf_component( + name="boschsensortec/Bosch-BSEC2-Library", + repo="https://github.com/esphome-libs/Bosch-BSEC2-Library", + ref=BSEC2_LIBRARY_VERSION, + ) + else: + cg.add_library( + "BME68x Sensor library", + None, + f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}", + ) + cg.add_library( + "BSEC2 Software Library", + None, + f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}", + ) cg.add_define("USE_BSEC2") From 7918a93a7feeb37f8cb957444e326e4e9867f970 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:40:49 +0200 Subject: [PATCH 1414/2056] [esp32] Fix some compiler warnings & bugs (#15610) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/atm90e32/atm90e32.h | 4 ++-- esphome/components/midea/air_conditioner.cpp | 3 ++- esphome/components/mipi_spi/mipi_spi.h | 8 ++++---- esphome/components/tcs34725/tcs34725.cpp | 2 +- esphome/cpp_helpers.py | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index c44a11e3ed4..95154812cb4 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent, #endif float get_reference_voltage(uint8_t phase) { #ifdef USE_NUMBER - return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage + return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage #else return 120.0; // Default voltage #endif } float get_reference_current(uint8_t phase) { #ifdef USE_NUMBER - return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current + return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current #else return 5.0f; // Default current #endif diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 50521cf238b..69e0d46d2d8 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -28,7 +28,8 @@ void AirConditioner::on_status_change() { if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK && this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) { // Read existing presets (set by codegen), append frost protection, write back - const auto &existing = this->get_traits().get_supported_custom_presets(); + auto traits = this->get_traits(); + const auto &existing = traits.get_supported_custom_presets(); bool found = false; for (const char *p : existing) { if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) { diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 423226b1d70..2242be6c17b 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -234,9 +234,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, - this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, - this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, + internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, + (uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, + this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, HAS_HARDWARE_ROTATION); } @@ -305,7 +305,7 @@ class MipiSpi : public display::Display, this->write_command_(BRIGHTNESS, this->brightness_.value()); // calculate new madctl value from base value adjusted for rotation - uint8_t madctl = MADCTL; // lower 8 bits only + uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0; constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX; constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY; diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 4fe87de0ca2..1098d8de5fe 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration my_integration_time_regval = integration_time; this->integration_time_auto_ = false; } - this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f; + this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f; ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_); } void TCS34725Component::set_gain(TCS34725Gain gain) { diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 479090016f8..f2bd3b92a31 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -113,7 +113,8 @@ def _generate_source_table_code( entries = ", ".join(var_names) lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{") - lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");') + cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}" + lines.append(f' if ({cond}) return LOG_STR("<unknown>");') lines.append(" return reinterpret_cast<const LogString *>(") lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") lines.append("}") From 6aa538a61d7559531b446763abae20eda918572b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 13 Apr 2026 15:42:02 +0200 Subject: [PATCH 1415/2056] [micro_wake_word] Bugfix: Use es-nn v1.1.2 (last known working version) (#15703) --- esphome/components/micro_wake_word/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index de95e4961b9..5ab1e4bb805 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -452,7 +452,7 @@ async def to_code(config): esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") # Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn) - esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1") + esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") From f30f0a0edcbc7ed89fff03f10b14f90326d0bbab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 03:43:17 -1000 Subject: [PATCH 1416/2056] [zephyr] Remove redundant yield() from main loop (#15694) --- esphome/components/zephyr/core.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index a3b0471ebcc..93a9a1ae8ee 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -99,7 +99,6 @@ int main() { setup(); while (true) { loop(); - esphome::yield(); } return 0; } From 5d0cfc31fa92c1038b328419f61d33f1eaadbe68 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:18:44 -0400 Subject: [PATCH 1417/2056] [core] Move FILTER_PLATFORMIO_LINES into platformio_runner (#15707) --- esphome/platformio_api.py | 39 --------------------- esphome/platformio_runner.py | 45 ++++++++++++++++++++++++- tests/unit_tests/test_platformio_api.py | 2 +- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index fc21977fdd1..dec541985f6 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -14,45 +14,6 @@ from esphome.util import run_external_process _LOGGER = logging.getLogger(__name__) -IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" -FILTER_PLATFORMIO_LINES = [ - r"Verbose mode can be enabled via `-v, --verbose` option.*", - r"CONFIGURATION: https://docs.platformio.org/.*", - r"DEBUG: Current.*", - r"LDF Modes:.*", - r"LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf.*", - f"Looking for {IGNORE_LIB_WARNINGS} library in registry", - f"Warning! Library `.*'{IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.", - f"You can ignore this message, if `.*{IGNORE_LIB_WARNINGS}.*` is a built-in library.*", - r"Scanning dependencies...", - r"Found \d+ compatible libraries", - r"Memory Usage -> https://bit.ly/pio-memory-usage", - r"Found: https://platformio.org/lib/show/.*", - r"Using cache: .*", - r"Installing dependencies", - r"Library Manager: Already installed, built-in library", - r"Building in .* mode", - r"Advanced Memory Usage is available via .*", - r"Merged .* ELF section", - r"esptool.py v.*", - r"esptool v.*", - r"Checking size .*", - r"Retrieving maximum program size .*", - r"PLATFORM: .*", - r"PACKAGES:.*", - r" - framework-arduinoespressif.* \(.*\)", - r" - tool-esptool.* \(.*\)", - r" - toolchain-.* \(.*\)", - r"Creating BIN file .*", - r"Warning! Could not find file \".*.crt\"", - r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.", - r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", - r"Warning: esp-idf-size exited with code 2", - r"esp_idf_size: error: unrecognized arguments: --ng", - r"Package configuration completed successfully", -] - - def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py index 92700d5c42d..599c9408a49 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio_runner.py @@ -101,6 +101,50 @@ def patch_file_downloader() -> None: FileDownloader.__init__ = patched_init +_IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" +# Regex patterns matched against each line of PlatformIO output. Lines that +# match are dropped by RedirectText before they reach the parent process. +# Patterns are anchored at the start of the line (RedirectText uses +# ``re.match``). Disabled when the user passes ``-v`` / ``--verbose`` to +# ``esphome compile``. +FILTER_PLATFORMIO_LINES = [ + r"Verbose mode can be enabled via `-v, --verbose` option.*", + r"CONFIGURATION: https://docs.platformio.org/.*", + r"DEBUG: Current.*", + r"LDF Modes:.*", + r"LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf.*", + f"Looking for {_IGNORE_LIB_WARNINGS} library in registry", + f"Warning! Library `.*'{_IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.", + f"You can ignore this message, if `.*{_IGNORE_LIB_WARNINGS}.*` is a built-in library.*", + r"Scanning dependencies...", + r"Found \d+ compatible libraries", + r"Memory Usage -> https://bit.ly/pio-memory-usage", + r"Found: https://platformio.org/lib/show/.*", + r"Using cache: .*", + r"Installing dependencies", + r"Library Manager: Already installed, built-in library", + r"Building in .* mode", + r"Advanced Memory Usage is available via .*", + r"Merged .* ELF section", + r"esptool.py v.*", + r"esptool v.*", + r"Checking size .*", + r"Retrieving maximum program size .*", + r"PLATFORM: .*", + r"PACKAGES:.*", + r" - framework-arduinoespressif.* \(.*\)", + r" - tool-esptool.* \(.*\)", + r" - toolchain-.* \(.*\)", + r"Creating BIN file .*", + r"Warning! Could not find file \".*.crt\"", + r"Warning! Arduino framework as an ESP-IDF component doesn't handle the `variant` field! The default `esp32` variant will be used.", + r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", + r"Warning: esp-idf-size exited with code 2", + r"esp_idf_size: error: unrecognized arguments: --ng", + r"Package configuration completed successfully", +] + + def main() -> int: patch_structhash() patch_file_downloader() @@ -126,7 +170,6 @@ def main() -> int: # Filtering is disabled when the user passed -v / --verbose to # ``esphome compile``, preserving the previous in-process behavior where # verbose mode let all PlatformIO output through unfiltered. - from esphome.platformio_api import FILTER_PLATFORMIO_LINES from esphome.util import RedirectText is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:]) diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index ddc4e45c844..67e64e5f612 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -906,7 +906,7 @@ def _filter_through_redirect(line: str) -> str: captured = io.StringIO() redirect = RedirectText( - captured, filter_lines=platformio_api.FILTER_PLATFORMIO_LINES + captured, filter_lines=platformio_runner.FILTER_PLATFORMIO_LINES ) redirect.write(line + "\n") return captured.getvalue() From fb0283e0ee04830275fe6e857f4a8955ac137c7b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:18:52 -0400 Subject: [PATCH 1418/2056] [esp32] Update the recommended platform to 55.03.38-1 (#15705) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 8 ++++---- platformio.ini | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index cd61d9ec480..60c3776aa84 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf +10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cd38c82dd89..2974028b503 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -676,7 +676,7 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(3, 3, 8), } ARDUINO_PLATFORM_VERSION_LOOKUP = { - cv.Version(3, 3, 8): cv.Version(55, 3, 38), + cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"), cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), @@ -724,7 +724,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", - cv.Version(5, 5, 4): cv.Version(55, 3, 38), + cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), @@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 38), - "latest": cv.Version(55, 3, 38), + "recommended": cv.Version(55, 3, 38, "1"), + "latest": cv.Version(55, 3, 38, "1"), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/platformio.ini b/platformio.ini index 7d17628a8ff..708d62afdc7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,7 +133,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz @@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip platform_packages = pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz From 53ce2a2f7fed1a142f43eec73c681c0106d14552 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 08:25:05 -1000 Subject: [PATCH 1419/2056] [api] Add speed_optimized to SubscribeLogsResponse (#15698) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/api/api.proto | 5 +++-- esphome/components/api/api_pb2.cpp | 18 ++++++++++++------ script/api_protobuf/api_protobuf.py | 6 ++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4a9a6f90512..f906cfb8d78 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -778,9 +778,10 @@ message SubscribeLogsResponse { option (source) = SOURCE_SERVER; option (log) = false; option (no_delay) = false; + option (speed_optimized) = true; - LogLevel level = 1; - bytes message = 3; + LogLevel level = 1 [(force) = true]; + bytes message = 3 [(force) = true]; } // ==================== NOISE ENCRYPTION ==================== diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 481510e407b..f304c852822 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -916,16 +916,22 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t } return true; } -uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level)); - ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_); return pos; } -uint32_t SubscribeLogsResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; - size += this->level ? 2 : 0; - size += ProtoSize::calc_length(1, this->message_len_); + size += 2; + size += ProtoSize::calc_length_force(1, this->message_len_); return size; } #ifdef USE_API_NOISE diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 1743d2bb340..73e0859d5eb 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1028,7 +1028,8 @@ class BytesType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" + calc_fn = "calc_length_force" if force else "calc_length" + return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -1109,7 +1110,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + calc_fn = "calc_length_force" if force else "calc_length" + return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);" class PointerToStringBufferType(PointerToBufferTypeBase): From af7cb1d81ecefbdc8240a03f05ac926c1d21a96b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 08:40:25 -1000 Subject: [PATCH 1420/2056] [scheduler] Force-inline process_defer_queue_() fast path (#15686) --- esphome/core/scheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 43a3ec7049b..ea6c2a675ec 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -407,7 +407,7 @@ class Scheduler { // Process defer queue for FIFO execution of deferred items. // IMPORTANT: This method should only be called from the main thread (loop task). // Inlined: the fast path (nothing deferred) is just an atomic load check. - inline void HOT process_defer_queue_(uint32_t &now) { + inline void ESPHOME_ALWAYS_INLINE HOT process_defer_queue_(uint32_t &now) { // Fast path: nothing to process, avoid lock entirely. // Worst case is a one-loop-iteration delay before newly deferred items are processed. if (this->defer_empty_()) From 9f7e310526cb51126aaef85e64f76b8c152e620c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 08:40:39 -1000 Subject: [PATCH 1421/2056] [scheduler] Force-inline cleanup_() fast path (#15683) --- esphome/core/scheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ea6c2a675ec..e678ea0987c 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -302,7 +302,7 @@ class Scheduler { // loop thread structurally modifies items_ (push/pop/erase). Other threads may // iterate items_ and mark items removed under lock_, but never change the // vector's size or data pointer. - inline bool HOT cleanup_() { + inline bool ESPHOME_ALWAYS_INLINE HOT cleanup_() { if (this->to_remove_empty_()) return !this->items_.empty(); return this->cleanup_slow_path_(); From b85a7ef3177d2fe171922e2f67ad3056b4b3f34f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 08:40:58 -1000 Subject: [PATCH 1422/2056] [scheduler] Force-inline process_to_add() fast path (#15685) --- esphome/core/scheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index e678ea0987c..21af94ea4e3 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -138,7 +138,7 @@ class Scheduler { // (single-threaded). This is safe because the main loop is the only thread // that reads to_add_ without holding lock_; other threads may read it only // while holding the mutex (e.g. cancel_item_locked_). - inline void HOT process_to_add() { + inline void ESPHOME_ALWAYS_INLINE HOT process_to_add() { if (this->to_add_empty_()) return; this->process_to_add_slow_path_(); From e8bc4bedb4ebe191bb10a6f125979002d0935a14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:00:11 -1000 Subject: [PATCH 1423/2056] Bump actions/cache from 5.0.4 to 5.0.5 in /.github/actions/restore-python (#15714) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index af54175c01e..21393f2aba3 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: venv # yamllint disable-line rule:line-length From ce6bffb65cba8f3a9d954b00b2d4e77049343df0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:00:24 -1000 Subject: [PATCH 1424/2056] Bump actions/cache from 5.0.4 to 5.0.5 (#15713) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2240879bd24..6aa5b2a5472 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: venv # yamllint disable-line rule:line-length @@ -159,7 +159,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -198,7 +198,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -231,7 +231,7 @@ jobs: echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -253,7 +253,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -387,14 +387,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -466,14 +466,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -555,14 +555,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -817,7 +817,7 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -841,7 +841,7 @@ jobs: - name: Cache platformio if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -883,7 +883,7 @@ jobs: - name: Save memory analysis to cache if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -930,7 +930,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} From 40348092818c1df90de5a7795b45787dce8e36c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:00:46 -1000 Subject: [PATCH 1425/2056] Bump actions/create-github-app-token from 3.0.0 to 3.1.1 (#15712) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index f5214a5ce0c..0e5ceb9346c 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -27,7 +27,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c92581b49b0..35b9e065e1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,7 +221,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -256,7 +256,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -287,7 +287,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From 8cdffef82a44ec58c43a290ca8b22f9e55071b8c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:06:56 -0400 Subject: [PATCH 1426/2056] [heatpumpir] Bump tonia/HeatpumpIR to 1.0.41 (#15711) --- .clang-tidy.hash | 2 +- esphome/components/heatpumpir/climate.py | 2 +- platformio.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 60c3776aa84..ab526134f80 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90 +dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815 diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 7743da77abd..b7e04374807 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -127,6 +127,6 @@ async def to_code(config): cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) cg.add_build_flag("-Wno-error=overloaded-virtual") - cg.add_library("tonia/HeatpumpIR", "1.0.40") + cg.add_library("tonia/HeatpumpIR", "1.0.41") if CORE.is_libretiny or CORE.is_esp32: CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"]) diff --git a/platformio.ini b/platformio.ini index 708d62afdc7..3897db83e13 100644 --- a/platformio.ini +++ b/platformio.ini @@ -83,7 +83,7 @@ lib_deps = fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 dudanov/MideaUART@1.1.9 ; midea - tonia/HeatpumpIR@1.0.40 ; heatpumpir + tonia/HeatpumpIR@1.0.41 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO @@ -178,7 +178,7 @@ lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.4 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word - tonia/HeatpumpIR@1.0.40 ; heatpumpir + tonia/HeatpumpIR@1.0.41 ; heatpumpir build_flags = ${common:idf.build_flags} -Wno-nonnull-compare From 73c972a604e4ebb735b40d41f249dbd9deff0f9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 13:59:32 -1000 Subject: [PATCH 1427/2056] [adc] Place ADC oneshot control functions in IRAM for cache safety (#15717) --- esphome/components/adc/sensor.py | 15 ++++++++++++++- esphome/components/esp32/__init__.py | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index bab2762f00e..09e09f0dc1b 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,11 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler -from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component +from esphome.components.esp32 import ( + get_esp32_variant, + include_builtin_idf_component, + require_adc_oneshot_iram, +) from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.zephyr import ( zephyr_add_overlay, @@ -24,6 +28,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE +from esphome.types import ConfigType from . import ( ATTENUATION_MODES, @@ -65,6 +70,13 @@ def validate_config(config): return config +def _require_adc_iram(config: ConfigType) -> ConfigType: + """Register ADC oneshot IRAM requirement during config validation.""" + if CORE.is_esp32: + require_adc_oneshot_iram() + return config + + ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) @@ -95,6 +107,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("60s")), validate_config, + _require_adc_iram, ) CONF_ADC_CHANNEL_ID = "adc_channel_id" diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 2974028b503..7b3f9da3da6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1058,6 +1058,7 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert" CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7" CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram" CONF_DISABLE_FATFS = "disable_fatfs" +CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram" # VFS requirement tracking # Components that need VFS features can call require_vfs_*() functions @@ -1071,6 +1072,7 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" KEY_FATFS_REQUIRED = "fatfs_required" KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" +KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required" def require_vfs_select() -> None: @@ -1168,6 +1170,17 @@ def require_fatfs() -> None: CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True +def require_adc_oneshot_iram() -> None: + """Mark that ADC oneshot IRAM safety is required by a component. + + Call this from components that use the ADC oneshot driver. When flash cache is + disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management), + the ADC oneshot read function must be in IRAM to avoid crashes. + This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM. + """ + CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -1268,6 +1281,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean, cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, } ), @@ -2068,6 +2082,16 @@ async def to_code(config): if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False) + # Place ADC oneshot control functions in IRAM for cache safety + # When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread, + # power management, etc.), ADC reads will crash if these functions are in flash. + # Components using ADC call require_adc_oneshot_iram() to force this. + if ( + CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False) + or advanced[CONF_ADC_ONESHOT_IN_IRAM] + ): + add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True) + # Disable FATFS support # Components that need FATFS (SD card, etc.) can call require_fatfs() if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False): From 21df5d9bf6162f5d14529560f1c8b98c704a13f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 13:59:45 -1000 Subject: [PATCH 1428/2056] [web_server] Reset OTA backend on new upload to avoid brick after interrupted OTA (#15720) --- .../web_server/ota/ota_web_server.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 95b166901ad..9812714ec03 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -114,7 +114,25 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf uint8_t *data, size_t len, bool final) { ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; - if (index == 0 && !this->ota_backend_) { + // First byte of a new upload: index==0 with actual data. (web_server_idf + // fires a separate start-marker call with data==nullptr/len==0 before the + // first real chunk; gate on len>0 so we only trigger once per upload.) + if (index == 0 && len > 0) { + // If a previous upload was interrupted (e.g. client closed the tab, TCP + // reset) the backend from that session may still be open. Tear it down + // so flash state doesn't get concatenated with the new image (which can + // produce a technically-valid-sized but corrupted firmware that bricks + // the device once it reboots). + if (this->ota_backend_) { + ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session"); + this->ota_backend_->abort(); +#ifdef USE_OTA_STATE_LISTENER + // Notify listeners that the previous session was aborted before the new one starts. + this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0); +#endif + this->ota_backend_.reset(); + } + // Initialize OTA on first call this->ota_init_(filename.c_str()); From edb16a27d374c55c86e8679cae43a36afc343b6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 16:58:48 -1000 Subject: [PATCH 1429/2056] [esphome] Skip missing extra flash images in upload_using_esptool (#15723) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/__main__.py | 9 +++++++- tests/unit_tests/test_main.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 25b404ae45c..7879cdad0ca 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -750,8 +750,15 @@ def upload_using_esptool( platformio_api.FlashImage( path=idedata.firmware_bin_path, offset=firmware_offset ), - *idedata.extra_flash_images, ] + for image in idedata.extra_flash_images: + if not image.path.is_file(): + _LOGGER.warning( + "Skipping missing flash image declared by platform: %s", + image.path, + ) + continue + flash_images.append(image) mcu = "esp8266" if CORE.is_esp32: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 85536d2f1ce..e07b4accf23 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1231,6 +1231,48 @@ def test_upload_using_esptool_path_conversion( assert partitions_path.endswith("partitions.bin") +def test_upload_using_esptool_skips_missing_extra_flash_images( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """A non-existent path in extra_flash_images must be filtered out with a + warning, and must not appear in the esptool command line. Only the valid + images are flashed. Regression test for + https://github.com/esphome/esphome/issues/15634. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin" + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [ + platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + platformio_api.FlashImage(path=missing_path, offset="0x2d0000"), + ] + mock_get_idedata.return_value = mock_idedata + + (tmp_path / "firmware.bin").touch() + (tmp_path / "bootloader.bin").touch() + # Intentionally do NOT create missing_path + + config = {CONF_ESPHOME: {"platformio_options": {}}} + + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + result = upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + assert result == 0 + assert "Skipping missing flash image" in caplog.text + assert str(missing_path) in caplog.text + + cmd_list = list(mock_run_external_command_main.call_args[0][1:]) + assert str(missing_path) not in cmd_list + assert "0x2d0000" not in cmd_list + + def test_upload_using_esptool_with_file_path( tmp_path: Path, mock_run_external_command_main: Mock, From 6b4b65346240c1cb93084141b28d07119fdc1d43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 03:18:38 -1000 Subject: [PATCH 1430/2056] [globals] Fix TemplatableFn deprecation warning for globals.set (#15733) --- esphome/components/globals/__init__.py | 7 ++++++- tests/components/globals/common.yaml | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index ec6730a41c5..46725fe6ddd 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -108,8 +108,13 @@ async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) + # Use the global's value_type alias as the lambda return type so + # TemplatableFn stores a direct function pointer instead of going through + # the deprecated converting trampoline when the value expression deduces + # to a different type (e.g. int literal assigned to a float global). + value_type = cg.RawExpression(f"{full_id.type}::value_type") templ = await cg.templatable( - config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True + config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression ) cg.add(var.set_value(templ)) return var diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml index 35dca0624f3..6d5721d3be8 100644 --- a/tests/components/globals/common.yaml +++ b/tests/components/globals/common.yaml @@ -4,6 +4,14 @@ esphome: - globals.set: id: glob_int value: "10" + # Set a float global with an integer literal - must emit the correct + # return type so TemplatableFn stores a direct function pointer. + - globals.set: + id: glob_float + value: "102" + - globals.set: + id: glob_float + value: !lambda "return 42;" globals: - id: glob_int From 2a530a4bf4618c08999db8b3fdfa52b1e2f09271 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:48:33 -1000 Subject: [PATCH 1431/2056] [core] Optimize format_hex_internal by splitting separator loop (#15594) --- esphome/core/helpers.cpp | 17 ++-- esphome/core/helpers.h | 6 +- tests/components/core/test_helpers.cpp | 120 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 tests/components/core/test_helpers.cpp diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 5940f6ec985..cbe22dd09aa 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -347,17 +347,18 @@ std::string format_mac_address_pretty(const uint8_t *mac) { return std::string(buf); } -// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase +// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. +// When separator is set, it is written unconditionally after each byte and the last +// one is overwritten with '\0', eliminating the per-byte `i < length - 1` check. static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, char base) { - if (length == 0) { - buffer[0] = '\0'; + if (length == 0 || buffer_size == 0) { + if (buffer_size > 0) + buffer[0] = '\0'; return buffer; } - // With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator) - // Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator) uint8_t stride = separator ? 3 : 2; - size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); + size_t max_bytes = separator ? (buffer_size / 3) : ((buffer_size - 1) / 2); if (max_bytes == 0) { buffer[0] = '\0'; return buffer; @@ -369,10 +370,12 @@ static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t size_t pos = i * stride; buffer[pos] = format_hex_char(data[i] >> 4, base); buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); - if (separator && i < length - 1) { + if (separator) { buffer[pos + 2] = separator; } } + // With separator: overwrite last separator with '\0' + // Without: write '\0' after last hex char buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c26bbe17b75..3c42d7df076 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1263,13 +1263,13 @@ constexpr uint8_t parse_hex_char(char c) { } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) -inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } +ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } /// Convert a nibble (0-15) to lowercase hex char -inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } +ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } +ESPHOME_ALWAYS_INLINE inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp new file mode 100644 index 00000000000..00169621c34 --- /dev/null +++ b/tests/components/core/test_helpers.cpp @@ -0,0 +1,120 @@ +#include <gtest/gtest.h> +#include <cstring> + +#include "esphome/core/helpers.h" + +namespace esphome::core::testing { + +// --- format_hex_to() --- + +TEST(FormatHexTo, Basic) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[7]; // 3 * 2 + 1 + format_hex_to(buffer, data, 3); + EXPECT_STREQ(buffer, "abcdef"); +} + +TEST(FormatHexTo, SingleByte) { + const uint8_t data[] = {0x0F}; + char buffer[3]; + format_hex_to(buffer, data, 1); + EXPECT_STREQ(buffer, "0f"); +} + +TEST(FormatHexTo, ZeroLength) { + char buffer[4] = "xxx"; + format_hex_to(buffer, static_cast<size_t>(sizeof(buffer)), static_cast<const uint8_t *>(nullptr), 0); + EXPECT_STREQ(buffer, ""); +} + +TEST(FormatHexTo, ZeroBufferSize) { + char buffer[4] = "xxx"; + const uint8_t data[] = {0xAB}; + format_hex_to(buffer, static_cast<size_t>(0), data, 1); + // Should not crash, buffer unchanged + EXPECT_EQ(buffer[0], 'x'); +} + +TEST(FormatHexTo, BufferTooSmall) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[5]; // only room for 2 bytes + format_hex_to(buffer, data, 3); + EXPECT_STREQ(buffer, "abcd"); +} + +TEST(FormatHexTo, MacAddress) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[13]; + format_hex_to(buffer, mac, 6); + EXPECT_STREQ(buffer, "aabbccddeeff"); +} + +// --- format_hex_pretty_to() --- + +TEST(FormatHexPrettyTo, BasicColon) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[9]; // 3 * 3 + format_hex_pretty_to(buffer, data, 3); + EXPECT_STREQ(buffer, "AB:CD:EF"); +} + +TEST(FormatHexPrettyTo, SingleByte) { + const uint8_t data[] = {0x0F}; + char buffer[3]; + format_hex_pretty_to(buffer, data, 1); + EXPECT_STREQ(buffer, "0F"); +} + +TEST(FormatHexPrettyTo, ZeroLength) { + char buffer[4] = "xxx"; + format_hex_pretty_to(buffer, static_cast<size_t>(sizeof(buffer)), static_cast<const uint8_t *>(nullptr), 0); + EXPECT_STREQ(buffer, ""); +} + +TEST(FormatHexPrettyTo, ZeroBufferSize) { + char buffer[4] = "xxx"; + const uint8_t data[] = {0xAB}; + format_hex_pretty_to(buffer, static_cast<size_t>(0), data, 1); + EXPECT_EQ(buffer[0], 'x'); +} + +TEST(FormatHexPrettyTo, CustomSeparator) { + const uint8_t data[] = {0xAA, 0xBB, 0xCC}; + char buffer[9]; + format_hex_pretty_to(buffer, data, 3, '-'); + EXPECT_STREQ(buffer, "AA-BB-CC"); +} + +// --- format_mac_addr_upper() --- + +TEST(FormatMacAddrUpper, Basic) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac, buffer); + EXPECT_STREQ(buffer, "AA:BB:CC:DD:EE:FF"); +} + +TEST(FormatMacAddrUpper, AllZeros) { + const uint8_t mac[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac, buffer); + EXPECT_STREQ(buffer, "00:00:00:00:00:00"); +} + +// --- format_hex_char() --- + +TEST(FormatHexChar, LowercaseDigits) { + EXPECT_EQ(format_hex_char(0), '0'); + EXPECT_EQ(format_hex_char(9), '9'); + EXPECT_EQ(format_hex_char(10), 'a'); + EXPECT_EQ(format_hex_char(15), 'f'); +} + +TEST(FormatHexChar, UppercaseDigits) { + EXPECT_EQ(format_hex_pretty_char(0), '0'); + EXPECT_EQ(format_hex_pretty_char(9), '9'); + EXPECT_EQ(format_hex_pretty_char(10), 'A'); + EXPECT_EQ(format_hex_pretty_char(15), 'F'); +} + +} // namespace esphome::core::testing From c833ff4a8480338355011792ffea6aa581b774e2 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 14 Apr 2026 13:49:18 -0400 Subject: [PATCH 1432/2056] [audio] Add/configure microDecoder library in preparation for use in future PRs (#15679) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/audio/__init__.py | 25 ++++++++++++++++++++++++- esphome/idf_component.yml | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 8f2102de6a7..fee582ca25c 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,7 +1,11 @@ from dataclasses import dataclass import esphome.codegen as cg -from esphome.components.esp32 import add_idf_component, include_builtin_idf_component +from esphome.components.esp32 import ( + add_idf_component, + add_idf_sdkconfig_option, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE from esphome.core import CORE @@ -27,6 +31,7 @@ class AudioData: flac_support: bool = False mp3_support: bool = False opus_support: bool = False + micro_decoder_support: bool = False def _get_data() -> AudioData: @@ -50,6 +55,11 @@ def request_opus_support() -> None: _get_data().opus_support = True +def request_micro_decoder_support() -> None: + """Request micro-decoder library support for audio decoding.""" + _get_data().micro_decoder_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -208,6 +218,19 @@ async def to_code(config): ) data = _get_data() + + if data.micro_decoder_support: + add_idf_component(name="esphome/micro-decoder", ref="0.1.1") + + # All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash + if not data.flac_support: + add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False) + if not data.mp3_support: + add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False) + if not data.opus_support: + add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False) + + # Legacy audio_decoder.cpp support defines and components if data.flac_support: cg.add_define("USE_AUDIO_FLAC_SUPPORT") add_idf_component(name="esphome/micro-flac", ref="0.1.1") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index bf42730e673..f4e3e751ec0 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.4 + esphome/micro-decoder: + version: 0.1.1 esphome/micro-flac: version: 0.1.1 esphome/micro-opus: From 5ba8c644e4f0d47ffcd524be544323ee92d2b343 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:49:27 -1000 Subject: [PATCH 1433/2056] [ld24xx] Replace heap-allocated SensorWithDedup with inline SensorWithDedup (#15676) --- esphome/components/ld2410/ld2410.cpp | 21 +++++++------ esphome/components/ld2410/ld2410.h | 4 +-- esphome/components/ld2412/ld2412.cpp | 29 +++++++++--------- esphome/components/ld2412/ld2412.h | 4 +-- esphome/components/ld2450/ld2450.cpp | 20 ++++++------- esphome/components/ld2450/ld2450.h | 18 ++++++------ esphome/components/ld24xx/ld24xx.h | 44 ++++++++++++++-------------- 7 files changed, 69 insertions(+), 71 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index f10e7ec0aae..32e49c643f5 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -360,8 +360,8 @@ void LD2410Component::handle_periodic_data_() { */ #ifdef USE_SENSOR SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, - encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) - SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])); + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]); SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])); SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); @@ -375,26 +375,26 @@ void LD2410Component::handle_periodic_data_() { Moving energy: 20~28th bytes */ for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]); } /* Still energy: 29~37th bytes */ for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]); } /* Light sensor: 38th bytes */ - SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]); } else { for (auto &gate_move_sensor : this->gate_move_sensors_) { - SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor); } for (auto &gate_still_sensor : this->gate_still_sensors_) { - SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor); } - SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_); } #endif #ifdef USE_BINARY_SENSOR @@ -786,13 +786,12 @@ void LD2410Component::set_light_out_control() { } #ifdef USE_SENSOR -// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { - this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s); + this->gate_move_sensors_[gate].set_sensor(s); } void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { - this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s); + this->gate_still_sensors_[gate].set_sensor(s); } #endif diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 687ed21d1d2..31186b135f2 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -129,8 +129,8 @@ class LD2410Component : public Component, public uart::UARTDevice { std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{}; #endif #ifdef USE_SENSOR - std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{}; - std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{}; + std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{}; + std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{}; #endif }; diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 38e1a59abab..a502ae3c109 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -397,12 +397,12 @@ void LD2412Component::handle_periodic_data_() { */ #ifdef USE_SENSOR SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_, - encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])) - SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW])); + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]); SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_, - encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])) - SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) - if (this->detection_distance_sensor_ != nullptr) { + encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW])); + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); + if (this->detection_distance_sensor_.has_sensor()) { int new_detect_distance = 0; if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { new_detect_distance = @@ -410,7 +410,7 @@ void LD2412Component::handle_periodic_data_() { } else if (target_state != 0x00) { new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]); } - this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); + this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance); } if (engineering_mode) { // Engineering mode needs at least LIGHT_SENSOR + 1 bytes @@ -423,27 +423,27 @@ void LD2412Component::handle_periodic_data_() { Moving energy: 20~28th bytes */ for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]); } /* Still energy: 29~37th bytes */ for (uint8_t i = 0; i < TOTAL_GATES; i++) { - SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]); } /* Light sensor value */ - SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]); } } else { for (auto &gate_move_sensor : this->gate_move_sensors_) { - SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor); } for (auto &gate_still_sensor : this->gate_still_sensors_) { - SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor); } - SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_); } #endif // the radar module won't tell us when it's done, so we just have to keep polling... @@ -846,12 +846,11 @@ void LD2412Component::set_light_out_control() { } #ifdef USE_SENSOR -// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { - this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s); + this->gate_move_sensors_[gate].set_sensor(s); } void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { - this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s); + this->gate_still_sensors_[gate].set_sensor(s); } #endif diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 7fd22459787..306e7ae31d2 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -133,8 +133,8 @@ class LD2412Component : public Component, public uart::UARTDevice { std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{}; #endif #ifdef USE_SENSOR - std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{}; - std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{}; + std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{}; + std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{}; #endif }; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 58c3cac42d8..0dc2638aad9 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -565,6 +565,7 @@ void LD2450Component::handle_periodic_data_() { SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count); // Moving Target Count SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count); + #endif #ifdef USE_BINARY_SENSOR @@ -872,33 +873,32 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); } #ifdef USE_SENSOR -// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { - this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s); + this->move_x_sensors_[target].set_sensor(s); } void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { - this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s); + this->move_y_sensors_[target].set_sensor(s); } void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) { - this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s); + this->move_speed_sensors_[target].set_sensor(s); } void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) { - this->move_angle_sensors_[target] = new SensorWithDedup<float>(s); + this->move_angle_sensors_[target].set_sensor(s); } void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) { - this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s); + this->move_distance_sensors_[target].set_sensor(s); } void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) { - this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s); + this->move_resolution_sensors_[target].set_sensor(s); } void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s); + this->zone_target_count_sensors_[zone].set_sensor(s); } void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s); + this->zone_still_target_count_sensors_[zone].set_sensor(s); } void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s); + this->zone_moving_target_count_sensors_[zone].set_sensor(s); } #endif #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index cbcdec10b31..10f9bb874a4 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -182,15 +182,15 @@ class LD2450Component : public Component, public uart::UARTDevice { ZoneOfNumbers zone_numbers_[MAX_ZONES]; #endif #ifdef USE_SENSOR - std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_x_sensors_{}; - std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_y_sensors_{}; - std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_speed_sensors_{}; - std::array<SensorWithDedup<float> *, MAX_TARGETS> move_angle_sensors_{}; - std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_distance_sensors_{}; - std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_resolution_sensors_{}; - std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_target_count_sensors_{}; - std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_still_target_count_sensors_{}; - std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{}; + std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_x_sensors_{}; + std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_y_sensors_{}; + std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_speed_sensors_{}; + std::array<SensorWithDedup<float>, MAX_TARGETS> move_angle_sensors_{}; + std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_distance_sensors_{}; + std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_resolution_sensors_{}; + std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_target_count_sensors_{}; + std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_still_target_count_sensors_{}; + std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{}; diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h index fd55167974b..cba1b68a15f 100644 --- a/esphome/components/ld24xx/ld24xx.h +++ b/esphome/components/ld24xx/ld24xx.h @@ -11,28 +11,20 @@ #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ protected: \ - ld24xx::SensorWithDedup<dedup_type> *name##_sensor_{nullptr}; \ + ld24xx::SensorWithDedup<dedup_type> name##_sensor_{}; \ \ public: \ - void set_##name##_sensor(sensor::Sensor *sensor) { \ - this->name##_sensor_ = new ld24xx::SensorWithDedup<dedup_type>(sensor); \ - } + void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_.set_sensor(sensor); } #endif #define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \ - if ((sensor) != nullptr) { \ - LOG_SENSOR(tag, name, (sensor)->sens); \ + if ((sensor).has_sensor()) { \ + LOG_SENSOR(tag, name, (sensor).get_sensor()); \ } -#define SAFE_PUBLISH_SENSOR(sensor, value) \ - if ((sensor) != nullptr) { \ - (sensor)->publish_state_if_not_dup(value); \ - } +#define SAFE_PUBLISH_SENSOR(sensor, value) (sensor).publish_state_if_not_dup(value) -#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \ - if ((sensor) != nullptr) { \ - (sensor)->publish_state_unknown(); \ - } +#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) (sensor).publish_state_unknown() #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -70,25 +62,33 @@ inline void format_version_str(const uint8_t *version, std::span<char, 20> buffe } #ifdef USE_SENSOR -// Helper class to store a sensor with a deduplicator & publish state only when the value changes +/// Sensor with deduplication — sensor may be null, null check is internal. +/// Stored inline, no heap allocation. Does nothing when no sensor is set. template<typename T> class SensorWithDedup { public: - SensorWithDedup(sensor::Sensor *sens) : sens(sens) {} + void set_sensor(sensor::Sensor *sens) { + this->sens_ = sens; + this->dedup_ = {}; + } void publish_state_if_not_dup(T state) { - if (this->publish_dedup.next(state)) { - this->sens->publish_state(static_cast<float>(state)); + if (this->sens_ != nullptr && this->dedup_.next(state)) { + this->sens_->publish_state(static_cast<float>(state)); } } void publish_state_unknown() { - if (this->publish_dedup.next_unknown()) { - this->sens->publish_state(NAN); + if (this->sens_ != nullptr && this->dedup_.next_unknown()) { + this->sens_->publish_state(NAN); } } - sensor::Sensor *sens; - Deduplicator<T> publish_dedup; + bool has_sensor() const { return this->sens_ != nullptr; } + sensor::Sensor *get_sensor() const { return this->sens_; } + + protected: + sensor::Sensor *sens_{nullptr}; + Deduplicator<T> dedup_; }; #endif } // namespace esphome::ld24xx From cf01163c8cdc6ad7c8ad68b37c30804349eac499 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:49:44 -1000 Subject: [PATCH 1434/2056] [core] Add uint32_to_str helper and use in preferences (#15597) --- esphome/components/esp32/preferences.cpp | 12 +-- esphome/components/libretiny/preferences.cpp | 12 +-- esphome/core/helpers.cpp | 14 ++++ esphome/core/helpers.h | 15 ++++ tests/benchmarks/core/bench_helpers.cpp | 56 ++++++++++++++ tests/components/core/test_uint32_to_str.cpp | 77 ++++++++++++++++++++ 6 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 tests/components/core/test_uint32_to_str.cpp diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index bc0a34ebe86..925c4e76624 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -4,7 +4,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include <nvs_flash.h> -#include <cinttypes> #include <cstring> #include <vector> @@ -12,9 +11,6 @@ namespace esphome::esp32 { static const char *const TAG = "preferences"; -// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding -static constexpr size_t KEY_BUFFER_SIZE = 12; - struct NVSData { uint32_t key; SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) @@ -51,8 +47,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { } } - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + char key_str[UINT32_MAX_STR_SIZE]; + uint32_to_str(key_str, this->key); size_t actual_len; esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { @@ -108,8 +104,8 @@ bool ESP32Preferences::sync() { uint32_t last_key = 0; for (const auto &save : s_pending_save) { - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + char key_str[UINT32_MAX_STR_SIZE]; + uint32_to_str(key_str, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index fba67172949..313b36d31ed 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -3,7 +3,6 @@ #include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include <cinttypes> #include <cstring> #include <vector> @@ -11,9 +10,6 @@ namespace esphome::libretiny { static const char *const TAG = "preferences"; -// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding -static constexpr size_t KEY_BUFFER_SIZE = 12; - struct NVSData { uint32_t key; SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) @@ -50,8 +46,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) { } } - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + char key_str[UINT32_MAX_STR_SIZE]; + uint32_to_str(key_str, this->key); fdb_blob_make(this->blob, data, len); size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); if (actual_len != len) { @@ -92,8 +88,8 @@ bool LibreTinyPreferences::sync() { uint32_t last_key = 0; for (const auto &save : s_pending_save) { - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + char key_str[UINT32_MAX_STR_SIZE]; + uint32_to_str(key_str, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index cbe22dd09aa..34ecaf137f0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -380,6 +380,20 @@ static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t return buffer; } +char *uint32_to_str_unchecked(char *buf, uint32_t val) { + if (val == 0) { + *buf++ = '0'; + return buf; + } + char *start = buf; + while (val > 0) { + *buf++ = '0' + (val % 10); + val /= 10; + } + std::reverse(start, buf); + return buf; +} + char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 3c42d7df076..54bc32a5a58 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1295,6 +1295,21 @@ inline char *int8_to_str(char *buf, int8_t val) { return buf; } +/// Minimum buffer size for uint32_to_str: 10 digits + null terminator. +static constexpr size_t UINT32_MAX_STR_SIZE = 11; + +/// Write unsigned 32-bit integer to buffer (internal, no size check). +/// Buffer must have at least 10 bytes free. Returns pointer past last char written. +char *uint32_to_str_unchecked(char *buf, uint32_t val); + +/// Write unsigned 32-bit integer to buffer with compile-time size check. +/// Null-terminates the output. Returns number of chars written (excluding null). +inline size_t uint32_to_str(std::span<char, UINT32_MAX_STR_SIZE> buf, uint32_t val) { + char *end = uint32_to_str_unchecked(buf.data(), val); + *end = '\0'; + return static_cast<size_t>(end - buf.data()); +} + /// Format byte array as lowercase hex to buffer (base implementation). char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index d9a9d158a3e..1ce9101ff6f 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -1,4 +1,6 @@ #include <benchmark/benchmark.h> +#include <cinttypes> +#include <cstdio> #include "esphome/core/helpers.h" @@ -307,4 +309,58 @@ static void Base64Decode_32Bytes(benchmark::State &state) { } BENCHMARK(Base64Decode_32Bytes); +// --- uint32_to_str() vs snprintf --- + +static void Uint32ToStr_Small(benchmark::State &state) { + char buf[UINT32_MAX_STR_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_to_str(buf, 12345); + benchmark::DoNotOptimize(buf); + benchmark::ClobberMemory(); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Uint32ToStr_Small); + +static void Snprintf_Uint32_Small(benchmark::State &state) { + char buf[UINT32_MAX_STR_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(12345)); + benchmark::DoNotOptimize(buf); + benchmark::ClobberMemory(); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Snprintf_Uint32_Small); + +static void Uint32ToStr_Large(benchmark::State &state) { + char buf[UINT32_MAX_STR_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_to_str(buf, 4294967295u); + benchmark::DoNotOptimize(buf); + benchmark::ClobberMemory(); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Uint32ToStr_Large); + +static void Snprintf_Uint32_Large(benchmark::State &state) { + char buf[UINT32_MAX_STR_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(4294967295u)); + benchmark::DoNotOptimize(buf); + benchmark::ClobberMemory(); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Snprintf_Uint32_Large); + } // namespace esphome::benchmarks diff --git a/tests/components/core/test_uint32_to_str.cpp b/tests/components/core/test_uint32_to_str.cpp new file mode 100644 index 00000000000..fc754429ecd --- /dev/null +++ b/tests/components/core/test_uint32_to_str.cpp @@ -0,0 +1,77 @@ +#include <gtest/gtest.h> + +#include "esphome/core/helpers.h" + +namespace esphome::core::testing { + +// --- uint32_to_str_unchecked() (internal, raw pointer) --- + +TEST(Uint32ToStr, InternalZero) { + char buf[UINT32_MAX_STR_SIZE]; + char *end = uint32_to_str_unchecked(buf, 0); + *end = '\0'; + EXPECT_STREQ(buf, "0"); + EXPECT_EQ(end - buf, 1); +} + +TEST(Uint32ToStr, InternalSingleDigit) { + char buf[UINT32_MAX_STR_SIZE]; + char *end = uint32_to_str_unchecked(buf, 7); + *end = '\0'; + EXPECT_STREQ(buf, "7"); +} + +TEST(Uint32ToStr, InternalMultiDigit) { + char buf[UINT32_MAX_STR_SIZE]; + char *end = uint32_to_str_unchecked(buf, 12345); + *end = '\0'; + EXPECT_STREQ(buf, "12345"); + EXPECT_EQ(end - buf, 5); +} + +TEST(Uint32ToStr, InternalMaxValue) { + char buf[UINT32_MAX_STR_SIZE]; + char *end = uint32_to_str_unchecked(buf, 4294967295u); + *end = '\0'; + EXPECT_STREQ(buf, "4294967295"); + EXPECT_EQ(end - buf, 10); +} + +TEST(Uint32ToStr, InternalPowersOfTen) { + char buf[UINT32_MAX_STR_SIZE]; + char *end; + + end = uint32_to_str_unchecked(buf, 10); + *end = '\0'; + EXPECT_STREQ(buf, "10"); + + end = uint32_to_str_unchecked(buf, 100); + *end = '\0'; + EXPECT_STREQ(buf, "100"); + + end = uint32_to_str_unchecked(buf, 1000000); + *end = '\0'; + EXPECT_STREQ(buf, "1000000"); +} + +// --- uint32_to_str() (public, span API) --- + +TEST(Uint32ToStr, SpanZero) { + char buf[UINT32_MAX_STR_SIZE]; + EXPECT_EQ(uint32_to_str(buf, 0), 1u); + EXPECT_STREQ(buf, "0"); +} + +TEST(Uint32ToStr, SpanMultiDigit) { + char buf[UINT32_MAX_STR_SIZE]; + EXPECT_EQ(uint32_to_str(buf, 12345), 5u); + EXPECT_STREQ(buf, "12345"); +} + +TEST(Uint32ToStr, SpanMaxValue) { + char buf[UINT32_MAX_STR_SIZE]; + EXPECT_EQ(uint32_to_str(buf, 4294967295u), 10u); + EXPECT_STREQ(buf, "4294967295"); +} + +} // namespace esphome::core::testing From da9fbb8044c1766ceda1b48e68bcdfafc89c9ccc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:50:11 -1000 Subject: [PATCH 1435/2056] [core] Fix app_state_ status bits clobbered for non-looping components (#15658) --- esphome/core/application.cpp | 34 ++- esphome/core/application.h | 24 ++- esphome/core/component.cpp | 13 ++ esphome/core/component.h | 5 + tests/integration/fixtures/status_flags.yaml | 141 +++++++++++++ tests/integration/test_status_flags.py | 209 +++++++++++++++++++ 6 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 tests/integration/fixtures/status_flags.yaml create mode 100644 tests/integration/test_status_flags.py diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index cd758598801..0c17c701615 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -85,8 +85,12 @@ void Application::setup() { if (component->can_proceed()) continue; + // Force the status LED to blink WARNING while we wait for a slow + // component to come up. Cleared after setup() finishes if no real + // component has warning set. + this->app_state_ |= STATUS_LED_WARNING; + do { - uint8_t new_app_state = STATUS_LED_WARNING; uint32_t now = millis(); // Process pending loop enables to handle GPIO interrupts during setup @@ -96,17 +100,26 @@ void Application::setup() { // Update loop_component_start_time_ right before calling each component this->loop_component_start_time_ = millis(); this->components_[j]->call(); - new_app_state |= this->components_[j]->get_component_state(); - this->app_state_ |= new_app_state; this->feed_wdt(); } this->after_loop_tasks_(); - this->app_state_ = new_app_state; yield(); } while (!component->can_proceed() && !component->is_failed()); } + // Setup is complete. Reconcile STATUS_LED_WARNING: the slow-setup path + // above may have forced it on, and any status_clear_warning() calls + // from components during setup were intentional no-ops (gated by + // APP_STATE_SETUP_COMPLETE). Walk components once here to pick up the + // real state. STATUS_LED_ERROR is never artificially forced, so its + // clear path always works and needs no reconciliation. Finally, set + // APP_STATE_SETUP_COMPLETE so subsequent warning clears go through + // the normal walk-and-clear path. + if (!this->any_component_has_status_flag_(STATUS_LED_WARNING)) + this->app_state_ &= ~STATUS_LED_WARNING; + this->app_state_ |= APP_STATE_SETUP_COMPLETE; + ESP_LOGI(TAG, "setup() finished successfully!"); #ifdef USE_SETUP_PRIORITY_OVERRIDE @@ -211,6 +224,19 @@ void HOT Application::feed_wdt(uint32_t time) { #endif } } +bool Application::any_component_has_status_flag_(uint8_t flag) const { + // Walk all components (not just looping ones) so non-looping components' + // status bits are respected. Only called from the slow-path clear helpers + // (status_clear_warning_slow_path_ / status_clear_error_slow_path_) on an + // actual set→clear transition, so walking O(N) here is paid once per + // transition — not once per loop iteration. + for (auto *component : this->components_) { + if ((component->get_component_state() & flag) != 0) + return true; + } + return false; +} + void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot"); for (auto &component : std::ranges::reverse_view(this->components_)) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 6b2969b4907..0150bb6646a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -401,7 +401,18 @@ class Application { */ void teardown_components(uint32_t timeout_ms); - uint8_t get_app_state() const { return this->app_state_; } + /// Return the public app state status bits (STATUS_LED_* only). + /// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked + /// out so external readers (status_led components, etc.) never see them. + uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; } + + /// True once Application::setup() has finished walking all components + /// and finalized the initial status flags. Before this point, the + /// slow-setup busy-wait may be forcing STATUS_LED_WARNING on, and + /// status_clear_* intentionally skips its walk-and-clear step so the + /// forced bit doesn't get wiped. Stored as a free bit on app_state_ + /// (bit 6) to avoid costing additional RAM. + bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; } // Helper macro for entity getter method declarations #ifdef USE_DEVICES @@ -577,6 +588,12 @@ class Application { bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif + /// Walk all registered components looking for any whose component_state_ + /// has the given flag set. Used by Component::status_clear_*_slow_path_() + /// (which is a friend) to decide whether to clear the corresponding bit on + /// this->app_state_ (the app-wide "any component has this status" indicator). + bool any_component_has_status_flag_(uint8_t flag) const; + /// Register a component, detecting loop() override at compile time. /// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance. template<typename T> void register_component_(T *comp) { @@ -838,8 +855,6 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ } inline void ESPHOME_ALWAYS_INLINE Application::loop() { - uint8_t new_app_state = 0; - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); @@ -859,13 +874,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; this->feed_wdt(last_op_end_time); } this->after_loop_tasks_(); - this->app_state_ = new_app_state; #ifdef USE_RUNTIME_STATS // Process any pending runtime stats printing after all components have run diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index deda42b0a7d..8949b4b76dc 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -411,10 +411,23 @@ void Component::status_set_error(const LogString *message) { } void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; + // Clear the app-wide STATUS_LED_WARNING bit only if setup has finished + // AND no other component still has it set. During setup the forced + // STATUS_LED_WARNING (from the slow-setup busy-wait) must not be wiped + // by a transient component clear — Application::setup() reconciles + // the warning bit once at the end before setting APP_STATE_SETUP_COMPLETE. + // The set path is unchanged (set_status_flag_ still writes directly). + if (App.is_setup_complete() && !App.any_component_has_status_flag_(STATUS_LED_WARNING)) + App.app_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; + // STATUS_LED_ERROR is never artificially forced — it only ever lands + // in app_state_ via a real set_status_flag_ call. So the walk-and-clear + // path is always safe, including during setup. + if (!App.any_component_has_status_flag_(STATUS_LED_ERROR)) + App.app_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_momentary_warning(const char *name, uint32_t length) { diff --git a/esphome/core/component.h b/esphome/core/component.h index e2b7aa85d3a..3307c5ae76e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -89,6 +89,11 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; +// Bit 6 on Application::app_state_ (ONLY) — set at the end of +// Application::setup(). Component::status_clear_*_slow_path_() uses this to +// decide whether to propagate clears to App.app_state_. Never set on a +// Component's component_state_. +inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; diff --git a/tests/integration/fixtures/status_flags.yaml b/tests/integration/fixtures/status_flags.yaml new file mode 100644 index 00000000000..cb118dcc84c --- /dev/null +++ b/tests/integration/fixtures/status_flags.yaml @@ -0,0 +1,141 @@ +esphome: + name: status-flags-test + +host: +api: + actions: + # Warning flag services for sensor_a + - action: set_warning_a + then: + - lambda: "id(sensor_a)->status_set_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_warning_a + then: + - lambda: "id(sensor_a)->status_clear_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Warning flag services for sensor_b + - action: set_warning_b + then: + - lambda: "id(sensor_b)->status_set_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_warning_b + then: + - lambda: "id(sensor_b)->status_clear_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Error flag services for sensor_a + - action: set_error_a + then: + - lambda: "id(sensor_a)->status_set_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_error_a + then: + - lambda: "id(sensor_a)->status_clear_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Error flag services for sensor_b + - action: set_error_b + then: + - lambda: "id(sensor_b)->status_set_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_error_b + then: + - lambda: "id(sensor_b)->status_clear_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Snapshot of the status_led_light's output state for observation. + - action: snapshot_led + then: + - component.update: status_led_writes + - component.update: status_led_last_state + +logger: + +# Tracks each write to the fake status_led output. +globals: + - id: status_led_write_count + type: uint32_t + restore_value: no + initial_value: "0" + - id: status_led_last_write + type: bool + restore_value: no + initial_value: "false" + +# Fake binary output — status_led_light writes to this instead of a pin. +# Every write bumps a counter and records the last value, both of which +# are exposed below so the test can verify status_led_light's loop is +# actually reading App.get_app_state() and responding. +output: + - platform: template + id: fake_status_led + type: binary + write_action: + - globals.set: + id: status_led_write_count + value: !lambda "return id(status_led_write_count) + 1;" + - globals.set: + id: status_led_last_write + value: !lambda "return state;" + +# Actual status_led_light component under test. +light: + - platform: status_led + name: Status LED + id: status_led_light_id + output: fake_status_led + +sensor: + # Two components that the test will toggle warning/error flags on. + - platform: template + name: Sensor A + id: sensor_a + update_interval: 24h + lambda: return 1.0; + - platform: template + name: Sensor B + id: sensor_b + update_interval: 24h + lambda: return 2.0; + + # Expose App.app_state_'s STATUS_LED_WARNING / STATUS_LED_ERROR bits + # as 0.0 / 1.0. force_update ensures every manual component.update + # publishes even if the value is unchanged. + - platform: template + name: App Warning Bit + id: app_warning_bit + update_interval: 24h + force_update: true + lambda: |- + return (App.get_app_state() & STATUS_LED_WARNING) != 0 ? 1.0 : 0.0; + - platform: template + name: App Error Bit + id: app_error_bit + update_interval: 24h + force_update: true + lambda: |- + return (App.get_app_state() & STATUS_LED_ERROR) != 0 ? 1.0 : 0.0; + + # Observables for the fake status_led output. + - platform: template + name: Status LED Writes + id: status_led_writes + update_interval: 24h + force_update: true + lambda: return id(status_led_write_count); + - platform: template + name: Status LED Last State + id: status_led_last_state + update_interval: 24h + force_update: true + lambda: |- + return id(status_led_last_write) ? 1.0 : 0.0; diff --git a/tests/integration/test_status_flags.py b/tests/integration/test_status_flags.py new file mode 100644 index 00000000000..ffbc7c7f634 --- /dev/null +++ b/tests/integration/test_status_flags.py @@ -0,0 +1,209 @@ +"""Integration tests for Component::status_set/clear_warning/error propagation. + +Verifies that toggling STATUS_LED_WARNING / STATUS_LED_ERROR on individual +components correctly updates the app-wide bits on Application::app_state_, +AND that the status_led_light component actually responds to those bits +by writing to its output (the full chain from component.status_set_warning +→ App.app_state_ → status_led_light.loop() reading get_app_state()). + +Exercises the multi-component OR semantics (the app bit stays set while +any component still has the flag, and only clears when the last component +clears its bit), the independence of warning and error, and the actual +status_led_light read of the bits via a fake template output that counts +writes. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .state_utils import InitialStateHelper, SensorTracker, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Time to let the host-mode main loop run so status_led_light.loop() can +# execute enough iterations to produce measurable write-count changes on +# the fake template output. 300 ms is well above the minimum needed. +STATUS_LED_SETTLE_S = 0.3 + + +@pytest.mark.asyncio +async def test_status_flags( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + async with run_compiled(yaml_config), api_client_connected() as client: + entities, services = await client.list_entities_services() + + # Map every custom API service by name for the test to execute. + svc = {s.name: s for s in services} + for name in ( + "set_warning_a", + "clear_warning_a", + "set_warning_b", + "clear_warning_b", + "set_error_a", + "clear_error_a", + "set_error_b", + "clear_error_b", + "snapshot_led", + ): + assert name in svc, f"service {name} not registered" + + # Track every sensor we care about. SensorTracker gives us + # expect(value) / expect_any() futures that resolve when a + # matching state arrives; much simpler than manual bookkeeping. + tracker = SensorTracker( + [ + "app_warning_bit", + "app_error_bit", + "status_led_writes", + "status_led_last_state", + ] + ) + tracker.key_to_sensor.update( + build_key_to_entity_mapping(entities, list(tracker.sensor_states.keys())) + ) + + # Swallow initial state broadcasts so the test only reacts to + # state changes triggered by our service calls. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(tracker.on_state)) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def call(name: str) -> None: + await client.execute_service(svc[name], {}) + + async def call_and_expect_bits( + service_name: str, *, warning: float, error: float + ) -> None: + """Execute a service and wait for both app bit sensors to match. + + Each bit-toggling service calls component.update on both + app_warning_bit and app_error_bit, so both sensors publish. + """ + futures = tracker.expect_all( + {"app_warning_bit": warning, "app_error_bit": error} + ) + await call(service_name) + await tracker.await_all(futures) + + async def snapshot_led_writes() -> int: + """Trigger a publish of the fake status_led output counter and return it.""" + future = tracker.expect_any("status_led_writes") + await call("snapshot_led") + await tracker.await_change(future, "status_led_writes") + return int(tracker.sensor_states["status_led_writes"][-1]) + + # ---- Baseline: everything clean ---- + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # ================================================================ + # Part 1 — STATUS_LED_WARNING propagation to App.app_state_ + # ================================================================ + + # Single component set/clear + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # Multi-component OR: both set, clear A, bit stays (B still has it), clear B, gone + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_b", warning=0.0, error=0.0) + + # Opposite clear order + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # ================================================================ + # Part 2 — STATUS_LED_ERROR propagation (same scenarios) + # ================================================================ + + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0) + + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("set_error_b", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0) + + # ================================================================ + # Part 3 — warning and error are independent + # ================================================================ + + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_error_b", warning=1.0, error=1.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0) + + # ================================================================ + # Part 4 — status_led_light actually reads App.app_state_ + # ================================================================ + # The fake status_led_light output increments status_led_write_count + # on every write. status_led_light::loop() writes its output on every + # iteration while an error/warning bit is set, so after holding a + # warning for ~300 ms we should see the counter move significantly. + # This is the end-to-end proof that the bits we set above actually + # reach status_led_light and drive its behavior. + + count_before_warning = await snapshot_led_writes() + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + # Let status_led_light's loop run long enough to toggle the pin + # several times (it reads get_app_state() every main loop iteration). + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_warning = await snapshot_led_writes() + assert count_after_warning > count_before_warning, ( + "status_led_light did not respond to STATUS_LED_WARNING being set: " + f"write count stayed at {count_before_warning} → {count_after_warning}. " + "The full chain Component::status_set_warning → App.app_state_ → " + "status_led_light::loop reading get_app_state() is broken." + ) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # Same check for ERROR + count_before_error = await snapshot_led_writes() + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_error = await snapshot_led_writes() + assert count_after_error > count_before_error, ( + "status_led_light did not respond to STATUS_LED_ERROR being set: " + f"write count stayed at {count_before_error} → {count_after_error}. " + ) + await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0) + + # ---- Set → clear → re-set round-trip ---- + # After clearing, status_led_light stops writing (steady state). + # Re-setting the flag must make it resume. This guards against a + # future idle optimization (e.g. #15642) where status_led disables + # its own loop when idle: if the re-enable path were broken, the + # second set would not produce writes. + # + # Snapshot AFTER the clear to avoid counting writes that were still + # in-flight from the error-set phase. + count_after_clear = await snapshot_led_writes() + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_idle = await snapshot_led_writes() + assert count_after_idle - count_after_clear <= 5, ( + "status_led_light kept writing after warning/error was cleared: " + f"count grew from {count_after_clear} to {count_after_idle}. " + "Expected it to stop writing once all status bits were clear." + ) + # Re-set warning — writes must resume. + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_reset = await snapshot_led_writes() + assert count_after_reset > count_after_idle + 5, ( + "status_led_light did not resume writing after re-setting " + f"STATUS_LED_WARNING: count went from {count_after_idle} to " + f"{count_after_reset}. If an idle optimization disabled the " + "loop, the re-enable path may be broken." + ) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) From 4729efbd0478aa8ecaa0840b382f7703b8a933d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:50:28 -1000 Subject: [PATCH 1436/2056] [light] Deduplicate color_uncorrect channel math via shared helper (#15727) --- .../components/light/esp_color_correction.cpp | 16 +++++++++ .../components/light/esp_color_correction.h | 33 +++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 9d731a2bd56..e793226bb1e 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -22,4 +22,20 @@ uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const { return (target - a <= b - target) ? lo : lo + 1; } +Color ESPColorCorrection::color_uncorrect(Color color) const { + // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) + return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green), + this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white)); +} + +uint8_t ESPColorCorrection::color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const { + if (max_brightness == 0 || this->local_brightness_ == 0) + return 0; + // Use 32-bit intermediates: when max_brightness and local_brightness_ are small but non-zero, + // (uncorrected / max_brightness) * 255 can exceed 65535 before the std::min(255) clamp runs. + uint32_t uncorrected = this->gamma_uncorrect_(value) * 255UL; + uint32_t res = ((uncorrected / max_brightness) * 255UL) / this->local_brightness_; + return static_cast<uint8_t>(std::min(res, uint32_t(255))); +} + } // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 48ecc463648..4eb5208c96e 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -46,38 +46,18 @@ class ESPColorCorrection { uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); return this->gamma_correct_(res); } - inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { - // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) - return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green), - this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white)); - } + Color color_uncorrect(Color color) const; inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { - if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL; - uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return (uint8_t) std::min(res, uint16_t(255)); + return this->color_uncorrect_channel_(red, this->max_brightness_.red); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { - if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL; - uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return (uint8_t) std::min(res, uint16_t(255)); + return this->color_uncorrect_channel_(green, this->max_brightness_.green); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { - if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL; - uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return (uint8_t) std::min(res, uint16_t(255)); + return this->color_uncorrect_channel_(blue, this->max_brightness_.blue); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { - if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL; - uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return (uint8_t) std::min(res, uint16_t(255)); + return this->color_uncorrect_channel_(white, this->max_brightness_.white); } protected: @@ -85,6 +65,9 @@ class ESPColorCorrection { uint8_t gamma_correct_(uint8_t value) const; /// Reverse gamma: binary search the forward PROGMEM table uint8_t gamma_uncorrect_(uint8_t value) const; + /// Shared body of color_uncorrect_{red,green,blue,white}. Kept out-of-line + /// to avoid duplicating two 16-bit divides at every call site. + uint8_t color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const; const uint16_t *gamma_table_{nullptr}; Color max_brightness_{255, 255, 255, 255}; From 9f5ed938e5722aab1a69affe4ce58dc11e3fbc68 Mon Sep 17 00:00:00 2001 From: Alexey Spirkov <extmail@alsp.net> Date: Tue, 14 Apr 2026 21:07:16 +0300 Subject: [PATCH 1437/2056] [i2s_audio] Add PDM mics support for ESP32-P4 (#15333) Co-authored-by: Alexey Spirkov <dev@alsp.net> --- esphome/components/i2s_audio/microphone/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 761cbb7f48b..1392d1d4ec1 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_( ) INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32] -PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3] +PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4] def _validate_esp32_variant(config): From 79cee864cbc0934af3f4e4c895c11b024660a72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 08:20:14 -1000 Subject: [PATCH 1438/2056] [esphome][ota] Disable loop while idle, wake on listening-socket activity (#15636) --- esphome/components/esphome/ota/__init__.py | 10 ++ .../components/esphome/ota/ota_esphome.cpp | 39 ++++++- .../components/socket/lwip_raw_tcp_impl.cpp | 8 ++ esphome/core/lwip_fast_select.c | 18 +++ esphome/core/lwip_fast_select.h | 6 + tests/component_tests/ota/test_esphome_ota.py | 105 ++++++++++++++++++ 6 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/ota/test_esphome_ota.py diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 337064dd271..5d35910fbdf 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -78,6 +78,14 @@ def ota_esphome_final_validate(config): else: new_ota_conf.append(ota_conf) + if len(merged_ota_esphome_configs_by_port) > 1: + raise cv.Invalid( + f"Only a single port is supported for '{CONF_OTA}' " + f"'{CONF_PLATFORM}: {CONF_ESPHOME}'. Got ports " + f"{sorted(merged_ota_esphome_configs_by_port.keys())}. Consolidate " + f"onto a single port; configs sharing a port are merged automatically." + ) + new_ota_conf.extend(merged_ota_esphome_configs_by_port.values()) full_conf[CONF_OTA] = new_ota_conf @@ -147,6 +155,8 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) + # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. + cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") await cg.register_component(var, config) await ota_to_code(var, config) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index af9b8ee19a1..47f661a8eaa 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -15,6 +15,9 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif #include <cerrno> #include <cstdio> @@ -28,6 +31,17 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer +// Single-instance pointer — multi-port configs are rejected in final_validate. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static ESPHomeOTAComponent *global_esphome_ota_component = nullptr; + +// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ). +extern "C" void esphome_wake_ota_component_any_context() { + if (global_esphome_ota_component != nullptr) { + global_esphome_ota_component->enable_loop_soon_any_context(); + } +} + void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { @@ -65,6 +79,14 @@ void ESPHomeOTAComponent::setup() { this->server_failed_(LOG_STR("listen")); return; } + + // loop() self-disables on its first idle tick; no explicit disable_loop() needed here. + global_esphome_ota_component = this; +#ifdef USE_LWIP_FAST_SELECT + // Filter fast-select wakes to this listener only. If the sock lookup returns nullptr, + // no wakes fire and loop() falls back to the self-disable safety net. + esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd())); +#endif } void ESPHomeOTAComponent::dump_config() { @@ -81,13 +103,15 @@ void ESPHomeOTAComponent::dump_config() { } void ESPHomeOTAComponent::loop() { - // Skip handle_handshake_() call if no client connected and no incoming connections - // This optimization reduces idle loop overhead when OTA is not active - // Note: No need to check server_ for null as the component is marked failed in setup() - // if server_ creation fails - if (this->client_ != nullptr || this->server_->ready()) { - this->handle_handshake_(); + // Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select + // listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back + // to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more + // iteration so a connection queued mid-session is still caught here. + if (this->client_ == nullptr && !this->server_->ready()) { + this->disable_loop(); + return; } + this->handle_handshake_(); } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; @@ -566,6 +590,9 @@ void ESPHomeOTAComponent::cleanup_connection_() { #ifdef USE_OTA_PASSWORD this->cleanup_auth_(); #endif + // Intentionally no disable_loop() — letting loop() run one more iteration catches + // any connection that queued on the listener mid-session (otherwise the wake flag, + // set while we were in LOOP state, would be lost to enable_pending_loops_()). } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 86131d3ddb9..c6692b01654 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,6 +11,10 @@ #include "esphome/core/wake.h" #include "esphome/core/log.h" +#ifdef USE_OTA_PLATFORM_ESPHOME +extern "C" void esphome_wake_ota_component_any_context(); +#endif + #ifdef USE_ESP8266 #include <coredecls.h> // For esp_schedule() #elif defined(USE_RP2040) @@ -854,6 +858,10 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn); tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); +#ifdef USE_OTA_PLATFORM_ESPHOME + // Must run before wake_loop_any_context() so flags are visible when the main task wakes. + esphome_wake_ota_component_any_context(); +#endif // Wake the main loop immediately so it can accept the new connection. esphome::wake_loop_any_context(); return ERR_OK; diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index bb3acbafcb1..36000d4e777 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -157,6 +157,17 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVEN // Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. static netconn_callback s_original_callback = NULL; +#ifdef USE_OTA_PLATFORM_ESPHOME +static struct netconn *s_ota_listener_conn = NULL; +extern void esphome_wake_ota_component_any_context(void); + +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { + s_ota_listener_conn = (sock != NULL) ? sock->conn : NULL; +} +#else +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { (void) sock; } +#endif + // Wrapper callback: calls original event_callback + notifies main loop task. // Called from LwIP's TCP/IP thread when socket events occur (task context, not ISR). static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt evt, u16_t len) { @@ -171,6 +182,13 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt // (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions // already wake the main loop through the RCVPLUS path. if (evt == NETCONN_EVT_RCVPLUS) { +#ifdef USE_OTA_PLATFORM_ESPHOME + // Mark OTA pending-enable only for events on its listen socket. MUST happen + // before xTaskNotifyGive so the flags are visible when the main task wakes. + if (conn == s_ota_listener_conn) { + esphome_wake_ota_component_any_context(); + } +#endif TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 20ac191673f..3b5e449148d 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -53,6 +53,12 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { /// The sock pointer must have been obtained from esphome_lwip_get_sock(). void esphome_lwip_hook_socket(struct lwip_sock *sock); +/// Set the listener netconn that the fast-select callback filters OTA wakes against. +/// After this is called, the OTA wake hook only fires for RCVPLUS events whose `conn` +/// matches this listener. Passing NULL disables OTA wakes (no event matches a NULL +/// listener) — correct behavior before install and after teardown. +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock); + /// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. /// Must be called with the TCPIP core lock held (LwIPLock in C++). /// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py new file mode 100644 index 00000000000..cdac430ff73 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -0,0 +1,105 @@ +"""Tests for the esphome OTA platform final_validate logic.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.esphome.ota import ota_esphome_final_validate +from esphome.const import ( + CONF_ESPHOME, + CONF_ID, + CONF_OTA, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_VERSION, +) +from esphome.core import ID +import esphome.final_validate as fv + + +def _make_ota_config(port: int = 3232, **kwargs: Any) -> dict[str, Any]: + config: dict[str, Any] = { + CONF_PLATFORM: CONF_ESPHOME, + CONF_ID: ID(f"ota_esphome_{port}", is_manual=False), + CONF_VERSION: 2, + CONF_PORT: port, + } + config.update(kwargs) + return config + + +def test_single_esphome_ota_instance_accepted() -> None: + """A single ESPHome OTA config passes final_validate untouched.""" + full_conf = {CONF_OTA: [_make_ota_config(port=3232)]} + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert len(updated[CONF_OTA]) == 1 + assert updated[CONF_OTA][0][CONF_PORT] == 3232 + finally: + fv.full_config.reset(token) + + +def test_same_port_configs_merge(caplog: pytest.LogCaptureFixture) -> None: + """Two ESPHome OTA configs on the same port merge into one instance.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_PASSWORD: "pw"}), + _make_ota_config(port=3232), + ] + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert len(updated[CONF_OTA]) == 1 + assert updated[CONF_OTA][0][CONF_PORT] == 3232 + assert any("Found and merged" in record.message for record in caplog.records), ( + "Expected merge warning not found in log" + ) + finally: + fv.full_config.reset(token) + + +def test_multiple_ports_rejected() -> None: + """Two ESPHome OTA configs on different ports raise cv.Invalid.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232), + _make_ota_config(port=3233), + ] + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match=r"Only a single port is supported for 'ota' 'platform: esphome'", + ): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_non_esphome_ota_unaffected() -> None: + """Non-esphome OTA platforms are not subject to the single-instance rule.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + {CONF_PLATFORM: "http_request", CONF_ID: ID("ota_hr", is_manual=False)}, + ] + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert len(updated[CONF_OTA]) == 3 + finally: + fv.full_config.reset(token) From 3f82a3a519545f93f82e55bae1745f3c527b543b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 08:20:31 -1000 Subject: [PATCH 1439/2056] [core] Inline Millis64Impl::compute() on single-threaded platforms (#15684) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/core/scheduler.h | 2 +- esphome/core/time_64.cpp | 42 ++++++++++++---------------------------- esphome/core/time_64.h | 32 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 21af94ea4e3..7634b3bd087 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -286,7 +286,7 @@ class Scheduler { // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. // On platforms with native 64-bit time, ignores now and uses millis_64() directly. // On other platforms, extends now to 64-bit using rollover tracking. - uint64_t millis_64_from_(uint32_t now) { + uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) { #ifdef USE_NATIVE_64BIT_TIME (void) now; return millis_64(); diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index db5df25eb93..b8a299ff7eb 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -20,6 +20,12 @@ namespace esphome { static const char *const TAG = "time_64"; #endif +#ifdef ESPHOME_THREAD_SINGLE +// Storage for Millis64Impl inline compute() — defined here so all TUs share one copy. +uint32_t Millis64Impl::last_millis_{0}; +uint16_t Millis64Impl::millis_major_{0}; +#else + uint64_t Millis64Impl::compute(uint32_t now) { // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2; @@ -44,51 +50,25 @@ uint64_t Millis64Impl::compute(uint32_t now) { * to last_millis is provided by its release store and the corresponding acquire loads. */ static std::atomic<uint16_t> millis_major{0}; -#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */ +#else /* ESPHOME_THREAD_MULTI_NO_ATOMICS */ static Mutex lock; static uint32_t last_millis{0}; static uint16_t millis_major{0}; -#else /* ESPHOME_THREAD_SINGLE */ - static uint32_t last_millis{0}; - static uint16_t millis_major{0}; #endif // THREAD SAFETY NOTE: - // This function has three implementations, based on the precompiler flags - // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.) + // This function has two out-of-line implementations, based on the preprocessor flags: // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx) // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.) // + // The ESPHOME_THREAD_SINGLE path is inlined in time_64.h. // Make sure all changes are synchronized if you edit this function. // // IMPORTANT: Always pass fresh millis() values to this function. The implementation // handles out-of-order timestamps between threads, but minimizing time differences // helps maintain accuracy. -#ifdef ESPHOME_THREAD_SINGLE - // Single-core platforms have no concurrency, so this is a simple implementation - // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. - - uint16_t major = millis_major; - uint32_t last = last_millis; - - // Check for rollover - if (now < last && (last - now) > HALF_MAX_UINT32) { - millis_major++; - major++; - last_millis = now; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } else if (now > last) { - // Only update if time moved forward - last_millis = now; - } - - // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast<uint64_t>(major) << 32); - -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) +#if defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) // Without atomics, this implementation uses locks more aggressively: // 1. Always locks when near the rollover boundary (within 10 seconds) // 2. Always locks when detecting a large backwards jump @@ -202,6 +182,8 @@ uint64_t Millis64Impl::compute(uint32_t now) { #endif } +#endif // !ESPHOME_THREAD_SINGLE + } // namespace esphome #endif // !USE_NATIVE_64BIT_TIME diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h index 42d4b041e51..592e645d41a 100644 --- a/esphome/core/time_64.h +++ b/esphome/core/time_64.h @@ -4,6 +4,9 @@ #ifndef USE_NATIVE_64BIT_TIME #include <cstdint> +#include <limits> + +#include "esphome/core/helpers.h" namespace esphome { @@ -16,7 +19,36 @@ class Millis64Impl { friend uint64_t millis_64(); friend class Scheduler; +#ifdef ESPHOME_THREAD_SINGLE + // Storage defined in time_64.cpp — declared here so the inline body can access them. + static uint32_t last_millis_; + static uint16_t millis_major_; + + static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) { + // Half the 32-bit range - used to detect rollovers vs normal time progression + static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2; + + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. + uint16_t major = millis_major_; + uint32_t last = last_millis_; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { + millis_major_++; + major++; + last_millis_ = now; + } else if (now > last) { + // Only update if time moved forward + last_millis_ = now; + } + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast<uint64_t>(major) << 32); + } +#else static uint64_t compute(uint32_t now); +#endif }; } // namespace esphome From 506edaadd5d4355a175e8cc46e76d0f280f2e24d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 09:08:30 -1000 Subject: [PATCH 1440/2056] [core] Inline feed_wdt hot path with out-of-line slow path (#15656) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/application.cpp | 34 ++++++++++++++++++++-------------- esphome/core/application.h | 36 +++++++++++++++++++++++++++++------- esphome/core/scheduler.cpp | 8 +++++++- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0c17c701615..1c732307054 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -209,21 +209,27 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void HOT Application::feed_wdt(uint32_t time) { - static uint32_t last_feed = 0; - // Use provided time if available, otherwise get current time - uint32_t now = time ? time : millis(); - // Compare in milliseconds (3ms threshold) - if (now - last_feed > 3) { - arch_feed_wdt(); - last_feed = now; -#ifdef USE_STATUS_LED - if (status_led::global_status_led != nullptr) { - status_led::global_status_led->call(); - } -#endif +void Application::feed_wdt() { + // Cold entry: callers without a millis() timestamp in hand. Fetches the + // time and takes the same rate-limit path as feed_wdt_with_time(). + uint32_t now = millis(); + if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { + this->feed_wdt_slow_(now); } } + +void HOT Application::feed_wdt_slow_(uint32_t time) { + // Callers (both feed_wdt() and feed_wdt_with_time()) have already + // confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded. + arch_feed_wdt(); + this->last_wdt_feed_ = time; +#ifdef USE_STATUS_LED + if (status_led::global_status_led != nullptr) { + status_led::global_status_led->call(); + } +#endif +} + bool Application::any_component_has_status_flag_(uint8_t flag) const { // Walk all components (not just looping ones) so non-looping components' // status bits are respected. Only called from the slow-path clear helpers @@ -325,7 +331,7 @@ void Application::teardown_components(uint32_t timeout_ms) { while (pending_count > 0 && (now - start_time) < timeout_ms) { // Feed watchdog during teardown to prevent triggering - this->feed_wdt(now); + this->feed_wdt_with_time(now); // Process components and compact the array, keeping only those still pending size_t still_pending = 0; diff --git a/esphome/core/application.h b/esphome/core/application.h index 0150bb6646a..60087d527d2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -385,7 +385,24 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - void feed_wdt(uint32_t time = 0); + /// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the + /// rate of HAL pokes low while still being small enough that any plausible + /// watchdog timeout (seconds) has orders of magnitude of safety margin. + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3; + + /// Feed the task watchdog. Cold entry — callers without a millis() + /// timestamp in hand. Out of line to keep call sites tiny. + void feed_wdt(); + + /// Feed the task watchdog, hot entry. Callers that already have a + /// millis() timestamp pay only a load + sub + branch on the common + /// (no-op) path. The actual arch feed + status LED update live in + /// feed_wdt_slow_. + void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) { + if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] { + this->feed_wdt_slow_(time); + } + } void reboot(); @@ -632,7 +649,10 @@ class Application { /// Caller must ensure dump_config_at_ < components_.size(). void __attribute__((noinline)) process_dump_config_(); - void feed_wdt_arch_(); + /// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates + /// last_wdt_feed_, and re-dispatches the status LED. Out of line so the + /// inline wrapper stays tiny. + void feed_wdt_slow_(uint32_t time); /// Perform a delay while also monitoring socket file descriptors for readiness #ifdef USE_HOST @@ -686,6 +706,7 @@ class Application { // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; + uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path #ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() @@ -830,12 +851,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ this->drain_wake_notifications_(); #endif - // Process scheduled tasks + // Process scheduled tasks. Scheduler::call now feeds the watchdog itself + // after each scheduled item that actually runs, so we no longer need an + // unconditional feed here — when Scheduler::call has no work to do, the + // only elapsed time is a sleep wake + a few instructions, and when it does + // have work, it fed the wdt as it went. this->scheduler.call(loop_start_time); - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions if (this->has_pending_enable_loop_requests_) { @@ -874,7 +896,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } - this->feed_wdt(last_op_end_time); + this->feed_wdt_with_time(last_op_end_time); } this->after_loop_tasks_(); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index dff50b03ef0..3e75a680643 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -739,7 +739,13 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); - return guard.finish(); + uint32_t end = guard.finish(); + // Feed the watchdog after each scheduled item (both main heap and defer + // queue paths go through here). A run of back-to-back callbacks cannot + // starve the wdt. The inline fast path is a load + sub + branch — nearly + // free when the 3 ms rate limit hasn't elapsed. + App.feed_wdt_with_time(end); + return end; } // Common implementation for cancel operations - handles locking From e48c7165c59baea515a4a6f592710203971d16ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 09:45:42 -1000 Subject: [PATCH 1441/2056] [light] Avoid addressable transition stall at low gamma-corrected values (#15726) --- .../components/light/addressable_light.cpp | 63 +++++++++- esphome/components/light/addressable_light.h | 3 + .../addressable_light_transition.yaml | 29 +++++ .../mock_addressable_light/__init__.py | 1 + .../mock_addressable_light/light.py | 23 ++++ .../mock_addressable_light.h | 52 ++++++++ .../test_addressable_light_transition.py | 119 ++++++++++++++++++ 7 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/addressable_light_transition.yaml create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/__init__.py create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/light.py create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h create mode 100644 tests/integration/test_addressable_light_transition.py diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 2f6ffc9a38a..d2f5913f4b5 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -58,6 +58,12 @@ void AddressableLightTransformer::start() { // our transition will handle brightness, disable brightness in correction. this->light_.correction_.set_local_brightness(255); this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); + + // Uniformity scan is deferred to the first apply() call. start() can run before the underlying + // LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE + // triggering a transition), and reading through ESPColorView would deref a null buffer. + this->uniform_start_scanned_ = false; + this->uniform_start_is_uniform_ = false; } inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { @@ -97,12 +103,57 @@ optional<LightColorValues> AddressableLightTransformer::apply() { // non-linear when applying small deltas. if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { - int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); - for (auto led : this->light_) { - led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), - subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), - subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), - subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); + // Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the + // frame buffer is valid. When every LED already has the same color (the common case: plain + // turn_on/turn_off on a uniform strip), interpolate math-only against a single start color. + // Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip + // quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27 + // round to stored 0, freezing progress). + if (!this->uniform_start_scanned_) { + this->uniform_start_scanned_ = true; + if (this->light_.size() > 0) { + Color first = this->light_[0].get(); + bool uniform = true; + for (int32_t i = 1; i < this->light_.size(); i++) { + if (this->light_[i].get() != first) { + uniform = false; + break; + } + } + if (uniform) { + this->uniform_start_color_ = first; + this->uniform_start_is_uniform_ = true; + } + } + } + if (this->uniform_start_is_uniform_) { + // All LEDs started at the same color: compute the interpolated value once and write it to + // every LED. No read-back, so each LED's stored byte advances through every gamma threshold + // as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values. + // + // Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be + // overwritten on the next apply() here. The fallback path below would have respected them + // via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we + // support, so this is acceptable. + // lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress). + const Color &start = this->uniform_start_color_; + int32_t remaining = int32_t(256.f * (1.f - smoothed_progress)); + uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining); + uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining); + uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining); + uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining); + for (auto led : this->light_) { + led.set_rgbw(r, g, b, w); + } + } else { + int32_t scale = + int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); + for (auto led : this->light_) { + led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), + subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), + subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), + subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); + } } this->last_transition_progress_ = smoothed_progress; this->light_.schedule_show(); diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 17cdb7d6f6c..0202ad380a8 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -115,6 +115,9 @@ class AddressableLightTransformer : public LightTransformer { AddressableLight &light_; float last_transition_progress_{0.0f}; Color target_color_{}; + Color uniform_start_color_{}; + bool uniform_start_scanned_{false}; + bool uniform_start_is_uniform_{false}; }; } // namespace esphome::light diff --git a/tests/integration/fixtures/addressable_light_transition.yaml b/tests/integration/fixtures/addressable_light_transition.yaml new file mode 100644 index 00000000000..7b847dd8038 --- /dev/null +++ b/tests/integration/fixtures/addressable_light_transition.yaml @@ -0,0 +1,29 @@ +esphome: + name: addr-light-transition +host: +api: +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +light: + - platform: mock_addressable_light + output_id: strip_output + id: strip + name: "Test Strip" + num_leds: 4 + gamma_correct: 2.8 + default_transition_length: 0s + +sensor: + - platform: template + name: "led0_red_raw" + id: led0_red_raw + update_interval: 10ms + accuracy_decimals: 0 + lambda: |- + return (float) id(strip_output).get_raw_red(0); diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py b/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py new file mode 100644 index 00000000000..e8cfff8e1fb --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/tests"] diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/light.py b/tests/integration/fixtures/external_components/mock_addressable_light/light.py new file mode 100644 index 00000000000..293d2854f45 --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/light.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import light +import esphome.config_validation as cv +from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID +from esphome.types import ConfigType + +mock_addressable_light_ns = cg.esphome_ns.namespace("mock_addressable_light") +MockAddressableLight = mock_addressable_light_ns.class_( + "MockAddressableLight", light.AddressableLight +) + +CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(MockAddressableLight), + cv.Optional(CONF_NUM_LEDS, default=4): cv.positive_not_null_int, + } +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS]) + await light.register_light(var, config) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h b/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h new file mode 100644 index 00000000000..c6b0d10601b --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h @@ -0,0 +1,52 @@ +#pragma once + +#include <cstddef> +#include <cstdint> +#include <memory> + +#include "esphome/components/light/addressable_light.h" +#include "esphome/core/component.h" + +namespace esphome::mock_addressable_light { + +// In-memory addressable light for host-mode integration tests. Exposes the raw +// per-LED byte buffer (post-gamma-correction, as the hardware would see it) +// so tests can observe transition behavior without real hardware. +class MockAddressableLight : public light::AddressableLight { + public: + explicit MockAddressableLight(uint16_t num_leds) + : num_leds_(num_leds), buf_(new uint8_t[num_leds * 4]()), effect_data_(new uint8_t[num_leds]()) {} + + void setup() override {} + void write_state(light::LightState *state) override {} + int32_t size() const override { return this->num_leds_; } + void clear_effect_data() override { + for (uint16_t i = 0; i < this->num_leds_; i++) + this->effect_data_[i] = 0; + } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + + // Accessors for tests: return the raw stored byte (post gamma correction), + // which is what actual LED hardware would receive. + uint8_t get_raw_red(uint16_t index) const { return this->buf_[index * 4 + 0]; } + uint8_t get_raw_green(uint16_t index) const { return this->buf_[index * 4 + 1]; } + uint8_t get_raw_blue(uint16_t index) const { return this->buf_[index * 4 + 2]; } + uint8_t get_raw_white(uint16_t index) const { return this->buf_[index * 4 + 3]; } + + protected: + light::ESPColorView get_view_internal(int32_t index) const override { + size_t pos = index * 4; + return {this->buf_.get() + pos + 0, this->buf_.get() + pos + 1, this->buf_.get() + pos + 2, + this->buf_.get() + pos + 3, this->effect_data_.get() + index, &this->correction_}; + } + + uint16_t num_leds_; + std::unique_ptr<uint8_t[]> buf_; + std::unique_ptr<uint8_t[]> effect_data_; +}; + +} // namespace esphome::mock_addressable_light diff --git a/tests/integration/test_addressable_light_transition.py b/tests/integration/test_addressable_light_transition.py new file mode 100644 index 00000000000..37fecde5952 --- /dev/null +++ b/tests/integration/test_addressable_light_transition.py @@ -0,0 +1,119 @@ +"""Integration test for addressable light transitions with gamma correction. + +Regression test for a bug where a long turn-on transition on an addressable +light with gamma correction (e.g. gamma_correct: 2.8) produced no visible +output for ~90% of the transition duration, then jumped to the target in the +final ~10%. Root cause: the transition algorithm read each LED's current value +back through the 8-bit stored byte every step; at gamma 2.8 any pre-gamma value +below ~27 rounds to stored byte 0, so the stored byte stalled at 0 until +progress was high enough for a single step to produce a large-enough pre-gamma +value to clear the gamma threshold. + +The fix interpolates against a cached start color when all LEDs started at the +same value (the common case for plain turn_on/turn_off), avoiding the round-trip. + +This test uses a host-only mock addressable light that exposes the raw stored +byte of each LED, so we can observe the transition directly. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import LightInfo, SensorInfo, SensorState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_addressable_light_transition( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """With gamma 2.8, the stored raw byte must rise visibly well before the end.""" + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + light = require_entity(entities, "test_strip", LightInfo) + sensor = require_entity(entities, "led0_red_raw", SensorInfo) + + # Track the raw-byte sensor. It polls every 10ms in the fixture, and + # ESPHome sensors publish on every change, so we collect a time series. + # Samples are stored as absolute (loop_time, value); we rebase to the + # command-issue time after the run so pre-command samples are strictly + # negative and reliably excluded. + loop = asyncio.get_running_loop() + samples: list[tuple[float, float]] = [] + + def on_state(state: object) -> None: + if not isinstance(state, SensorState) or state.key != sensor.key: + return + samples.append((loop.time(), state.state)) + + # InitialStateHelper swallows the first state ESPHome sends per entity + # on subscribe, so on_state only sees real post-subscribe updates. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + # Start transition: off -> full white over 1 second. This is the + # scenario from the bug report, compressed in time. + transition_s = 1.0 + command_time = loop.time() + client.light_command( + key=light.key, + state=True, + rgb=(1.0, 1.0, 1.0), + brightness=1.0, + transition_length=transition_s, + ) + + # Let the full transition run, plus margin for the final sample. + await asyncio.sleep(transition_s + 0.2) + + # Rebase to command-issue time. Pre-command samples have t < 0 and are + # excluded; everything else is in seconds since the command was issued. + post_command = [ + (t - command_time, v) for (t, v) in samples if t >= command_time + ] + assert post_command, "no sensor samples received after command was issued" + + # Assertion 1: the transition is not stalled. With the bug, the raw + # byte stays at 0 until ~90% of the transition duration. With the fix, + # it becomes nonzero in the first ~30% (for gamma 2.8, pre-gamma 76 + # clears the gamma threshold at progress ~0.30). Require the first + # nonzero sample to land well before 50% of the transition duration, + # measured from the command-issue time. The 50% bound (rather than + # 70%) leaves headroom for assertion 2's mid-window check. + first_nonzero = next(((t, v) for (t, v) in post_command if v > 0), None) + assert first_nonzero is not None, ( + "raw byte never rose above 0 during the transition — the fade stalled" + ) + assert first_nonzero[0] < transition_s * 0.5, ( + f"raw byte only rose above 0 at t={first_nonzero[0]:.3f}s " + f"(>{transition_s * 0.5:.3f}s after command) — transition is stalling" + ) + + # Assertion 2: by mid-late transition, the raw byte should have reached + # a substantial fraction of its final value. Bound the window to + # [50%, 90%] of the transition so the post-transition settled value + # (which always reaches 255) can't satisfy this assertion — that would + # let "stays at 0 then jumps at 99%" regressions slip through. + mid_window = [ + v + for (t, v) in post_command + if transition_s * 0.5 <= t <= transition_s * 0.9 + ] + assert mid_window, "no samples captured in mid-transition window" + assert max(mid_window) >= 100, ( + f"raw byte peaked at only {max(mid_window)} between 50%–90% of " + "transition (expected >= 100 for white target at gamma 2.8)" + ) + + # Assertion 3: final value reaches target. Gamma 2.8 of 255 is 255. + final_samples = [v for (_, v) in post_command[-5:]] + assert max(final_samples) >= 250, ( + f"final raw byte was {max(final_samples)}, expected >= 250" + ) From 2db2b89eb1648c95034024c22f2dc64d70681b8f Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:47:44 +0200 Subject: [PATCH 1442/2056] [nextion] Fix command spacing pacer never throttling sends (#15664) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 108 +++++++++++++++++-------- esphome/components/nextion/nextion.h | 15 ++-- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b0e14b5ea3f..e42f7ca2169 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -36,8 +36,9 @@ bool Nextion::send_command_(const std::string &command) { } #ifdef USE_NEXTION_COMMAND_SPACING - if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) { - ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); + const uint32_t now = App.get_loop_component_start_time(); + if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) { + ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str()); return false; } #endif // USE_NEXTION_COMMAND_SPACING @@ -48,6 +49,16 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); +#ifdef USE_NEXTION_COMMAND_SPACING + // Mark sent immediately after writing to UART. The pacer enforces inter-command + // spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_ + // at zero indefinitely, making can_send() always return true and spacing a no-op. + // ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally. + if (!this->connection_state_.ignore_is_setup_) { + this->command_pacer_.mark_sent(now); + } +#endif // USE_NEXTION_COMMAND_SPACING + return true; } @@ -253,11 +264,8 @@ bool Nextion::send_command(const char *command) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return false; - if (this->send_command_(command)) { - this->add_no_result_to_queue_("command"); - return true; - } - return false; + this->add_no_result_to_queue_with_command_("command", command); + return true; } bool Nextion::send_command_printf(const char *format, ...) { @@ -274,11 +282,8 @@ bool Nextion::send_command_printf(const char *format, ...) { return false; } - if (this->send_command_(buffer)) { - this->add_no_result_to_queue_("command_printf"); - return true; - } - return false; + this->add_no_result_to_queue_with_command_("command_printf", buffer); + return true; } #ifdef NEXTION_PROTOCOL_LOG @@ -349,25 +354,43 @@ void Nextion::loop() { } #ifdef USE_NEXTION_COMMAND_SPACING - // Try to send any pending commands if spacing allows this->process_pending_in_queue_(); +#ifdef USE_NEXTION_WAVEFORM + if (!this->waveform_queue_.empty()) { + this->check_pending_waveform_(); + } +#endif // USE_NEXTION_WAVEFORM #endif // USE_NEXTION_COMMAND_SPACING } #ifdef USE_NEXTION_COMMAND_SPACING void Nextion::process_pending_in_queue_() { - if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { - return; - } +#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP + size_t commands_sent = 0; +#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP - // Check if first item in queue has a pending command - auto *front_item = this->nextion_queue_.front(); - if (front_item && !front_item->pending_command.empty()) { - if (this->send_command_(front_item->pending_command)) { - // Command sent successfully, clear the pending command - front_item->pending_command.clear(); - ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); + for (auto *item : this->nextion_queue_) { + if (item == nullptr || item->pending_command.empty()) { + continue; // Already sent, waiting for ACK — skip, don't stop } + +#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP + if (++commands_sent > this->max_commands_per_loop_) { + ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring"); + break; + } +#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP + + const uint32_t now = App.get_loop_component_start_time(); + if (!this->command_pacer_.can_send(now)) { + break; // Spacing not elapsed, stop for this loop iteration + } + + if (!this->send_command_(item->pending_command)) { + break; // Unexpected send failure, stop + } + item->pending_command.clear(); + ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str()); } } #endif // USE_NEXTION_COMMAND_SPACING @@ -470,10 +493,6 @@ void Nextion::process_nextion_commands_() { this->setup_callback_.call(); } } -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent - ESP_LOGN(TAG, "Command spacing: marked command sent"); -#endif break; case 0x02: // invalid Component ID or name was used ESP_LOGW(TAG, "Invalid component ID/name"); @@ -1079,10 +1098,18 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { } /** - * @brief + * @brief Send a command and enqueue it for response tracking. * - * @param variable_name Variable name for the queue - * @param command + * Callers are responsible for checking is_sleeping() before calling this + * method. The sleep guard is deliberately absent here because some callers + * (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly + * sleep-safe and must bypass it. + * + * If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready, + * the command is saved in the queue entry for retry rather than dropped. + * + * @param variable_name Name of the variable or component associated with the command. + * @param command The raw command string to send. */ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty()) @@ -1263,9 +1290,22 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { std::string command = "get " + component->get_variable_name_to_send(); +#ifdef USE_NEXTION_COMMAND_SPACING + // Always enqueue first so the response handler is present when the command + // is eventually sent. Store the command for retry if spacing blocked it; + // process_pending_in_queue_() will transmit it when the pacer allows. + nextion_queue->pending_command = command; + this->nextion_queue_.push_back(nextion_queue); + if (this->send_command_(command)) { + nextion_queue->pending_command.clear(); + } +#else // USE_NEXTION_COMMAND_SPACING if (this->send_command_(command)) { this->nextion_queue_.push_back(nextion_queue); + } else { + delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) } +#endif // USE_NEXTION_COMMAND_SPACING } #ifdef USE_NEXTION_WAVEFORM @@ -1309,10 +1349,10 @@ void Nextion::check_pending_waveform_() { char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); - if (!this->send_command_(command)) { - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop(); - } + // If spacing or setup state blocks the send, leave the entry at the front + // of waveform_queue_ for retry on the next loop iteration via + // check_pending_waveform_(). Only pop on a successful send. + this->send_command_(command); } #endif // USE_NEXTION_WAVEFORM diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c84a5cd49c2..c62772ac757 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -55,15 +55,20 @@ class NextionCommandPacer { uint8_t get_spacing() const { return spacing_ms_; } /** - * @brief Check if enough time has passed to send next command - * @return true if enough time has passed since last command + * @brief Check if enough time has passed to send the next command. + * @param now Current timestamp in milliseconds (use App.get_loop_component_start_time() + * for consistency with the rest of the queue timing). + * @return true if the spacing interval has elapsed since the last command was sent. */ - bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; } + bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; } /** - * @brief Mark a command as sent, updating the timing + * @brief Record the transmit timestamp for the most recently sent command. + * @param now Current timestamp in milliseconds, as returned by + * App.get_loop_component_start_time(). Must use the same clock + * source as can_send() to avoid unsigned underflow. */ - void mark_sent() { last_command_time_ = millis(); } + void mark_sent(uint32_t now) { last_command_time_ = now; } private: uint8_t spacing_ms_; From 6d894dd6ee8e65467861681cdc8aeea8c7328962 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:48:30 +0200 Subject: [PATCH 1443/2056] [packages] fix support `packages: !include mypackages.yaml` (#15677) --- esphome/components/packages/__init__.py | 13 +++--- .../component_tests/packages/test_packages.py | 45 +++++++++++++++++++ ...13-packages_as_included_list.approved.yaml | 3 ++ .../13-packages_as_included_list.input.yaml | 4 ++ .../substitutions/13-packages_list.yaml | 2 + ...14-packages_as_included_dict.approved.yaml | 3 ++ .../14-packages_as_included_dict.input.yaml | 4 ++ .../substitutions/14-packages_dict.yaml | 3 ++ 8 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/13-packages_list.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 3a15b5b95a4..3f3df753512 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -321,12 +321,15 @@ def _walk_packages( return config packages = config[CONF_PACKAGES] - if not isinstance(packages, (dict, list)): - raise cv.Invalid( - f"Packages must be a key to value mapping or list, got {type(packages)} instead" - ) - with cv.prepend_path(CONF_PACKAGES): + if isinstance(packages, yaml_util.IncludeFile): + # If the packages key is an IncludeFile, resolve it first before processing. + packages, _ = resolve_include(packages, [], context, strict_undefined=False) + if not isinstance(packages, (dict, list)): + raise cv.Invalid( + f"Packages must be a key to value mapping or list, got {type(packages)} instead" + ) + if not isinstance(packages, dict): _walk_package_list(packages, callback, context) elif (result := _walk_package_dict(packages, callback, context)) is not None: diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0b828d757ed..cd91c4d8cb1 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1106,6 +1106,51 @@ def test_packages_invalid_type_raises() -> None: do_packages_pass(config) +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_list(mock_resolve_include) -> None: + """When packages: is an IncludeFile that resolves to a list, it is processed correctly.""" + include_file = MagicMock(spec=IncludeFile) + package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + mock_resolve_include.return_value = ([package_content], None) + + config = {CONF_PACKAGES: include_file} + result = do_packages_pass(config) + result = merge_packages(result) + + assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + + +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_dict(mock_resolve_include) -> None: + """When packages: is an IncludeFile that resolves to a dict, it is processed correctly.""" + include_file = MagicMock(spec=IncludeFile) + package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + mock_resolve_include.return_value = ({"network": package_content}, None) + + config = {CONF_PACKAGES: include_file} + result = do_packages_pass(config) + result = merge_packages(result) + + assert result == {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} + + +@patch("esphome.components.packages.resolve_include") +def test_packages_include_file_resolves_to_invalid_type_raises( + mock_resolve_include, +) -> None: + """When packages: is an IncludeFile that resolves to an invalid type, cv.Invalid is raised.""" + include_file = MagicMock(spec=IncludeFile) + mock_resolve_include.return_value = ("not_a_dict_or_list", None) + + config = {CONF_PACKAGES: include_file} + with pytest.raises( + cv.Invalid, match="Packages must be a key to value mapping or list" + ) as exc_info: + do_packages_pass(config) + + assert exc_info.value.path == [CONF_PACKAGES] + + @pytest.mark.parametrize( "invalid_package", [ diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml new file mode 100644 index 00000000000..7863def1906 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.approved.yaml @@ -0,0 +1,3 @@ +wifi: + password: pkg_password + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml new file mode 100644 index 00000000000..7a3b4970db6 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_as_included_list.input.yaml @@ -0,0 +1,4 @@ +packages: !include 13-packages_list.yaml + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml b/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml new file mode 100644 index 00000000000..23161db3d3c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/13-packages_list.yaml @@ -0,0 +1,2 @@ +- wifi: + password: pkg_password diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml new file mode 100644 index 00000000000..7863def1906 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.approved.yaml @@ -0,0 +1,3 @@ +wifi: + password: pkg_password + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml new file mode 100644 index 00000000000..8b9fc5ec3aa --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_as_included_dict.input.yaml @@ -0,0 +1,4 @@ +packages: !include 14-packages_dict.yaml + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml b/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml new file mode 100644 index 00000000000..55e8b38a435 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/14-packages_dict.yaml @@ -0,0 +1,3 @@ +network: + wifi: + password: pkg_password From c3769e4fcea761dc264d323e688d5cc51d22e57f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:51:55 -0400 Subject: [PATCH 1444/2056] [core] Fix PlatformIO progress bar rendering in subprocess mode (#15681) --- esphome/platformio_api.py | 3 --- esphome/platformio_runner.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e9719f7dcd8..fc21977fdd1 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -65,9 +65,6 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("UV_HTTP_RETRIES", "10") cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) - if not CORE.verbose: - kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES - return run_external_process(*cmd, **kwargs) diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py index 408d49d1a6f..92700d5c42d 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio_runner.py @@ -105,6 +105,36 @@ def main() -> int: patch_structhash() patch_file_downloader() + # Wrap stdout/stderr with RedirectText before PlatformIO runs: + # + # 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and + # PlatformIO's own progress-bar code check ``stream.isatty()`` to + # decide whether to emit TTY-format output (``\r`` cursor moves, ANSI + # colors, fancy progress bars). With the wrapper in place they always + # emit TTY format, even when our real stdout is a pipe to the parent + # process. Downstream consumers (local terminals and the Home + # Assistant dashboard log viewer) render the TTY control sequences + # correctly, so the user sees real progress bars. + # + # 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in + # this subprocess, so noisy PlatformIO output is dropped before it + # ever leaves the runner. This replaces the parent-side filtering + # that was lost when we switched from in-process to subprocess — the + # parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and + # bypasses its ``write()`` path entirely. + # + # Filtering is disabled when the user passed -v / --verbose to + # ``esphome compile``, preserving the previous in-process behavior where + # verbose mode let all PlatformIO output through unfiltered. + from esphome.platformio_api import FILTER_PLATFORMIO_LINES + from esphome.util import RedirectText + + is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:]) + filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES + + sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) + sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) + import platformio.__main__ return platformio.__main__.main() or 0 From 3b2caa1f5b11456dcdd93823d8040e63e1bb5609 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:23:49 +0000 Subject: [PATCH 1445/2056] Bump aioesphomeapi from 44.13.3 to 44.14.0 (#15695) 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 e4a019a4900..339b5d29191 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.13.3 +aioesphomeapi==44.14.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 20d49f9a7c6e94f5f31f8a74827cd108b2d79d5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 12 Apr 2026 21:42:31 -1000 Subject: [PATCH 1446/2056] [api] Add speed_optimized proto option for hot encode paths (#15691) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/api/api.proto | 2 ++ esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 16 ++++++++++++---- script/api_protobuf/api_protobuf.py | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e4d0c2d16df..4a9a6f90512 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -671,6 +671,7 @@ message SensorStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; option (no_delay) = true; + option (speed_optimized) = true; fixed32 key = 1 [(force) = true]; float state = 2; @@ -1638,6 +1639,7 @@ message BluetoothLERawAdvertisementsResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BLUETOOTH_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"]; } diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index dacc290e31e..d5d0b37e8df 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -23,6 +23,7 @@ extend google.protobuf.MessageOptions { optional bool no_delay = 1040 [default=false]; optional string base_class = 1041; optional bool inline_encode = 1042 [default=false]; + optional bool speed_optimized = 1043 [default=false]; } extend google.protobuf.FieldOptions { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c2d513f0d33..481510e407b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -745,7 +745,9 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { #endif return size; } -uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); @@ -755,7 +757,9 @@ uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG #endif return pos; } -uint32_t SensorStateResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SensorStateResponse::calculate_size() const { uint32_t size = 0; size += 5; size += ProtoSize::calc_float(1, this->state); @@ -2328,7 +2332,9 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { auto &sub_msg = this->advertisements[i]; @@ -2350,7 +2356,9 @@ uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer P } return pos; } -uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { auto &sub_msg = this->advertisements[i]; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 39bfc865d02..1743d2bb340 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2679,6 +2679,16 @@ def build_message_type( and get_opt(desc, inline_opt, False) ) + # Check if this message wants speed-optimized encode/calculate_size. + # When set, __attribute__((optimize("O2"))) is added to the definitions + # so GCC inlines the small ProtoEncode helpers even under -Os. + is_speed_optimized = get_opt(desc, pb.speed_optimized, False) + speed_attr = ( + '__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n' + if is_speed_optimized + else "" + ) + # Only generate encode method if this message needs encoding and has fields if needs_encode and encode and not is_inline_only: # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls @@ -2688,7 +2698,7 @@ def build_message_type( ) for line in encode ] - o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" + o = f"{speed_attr}uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" o += indent("\n".join(encode_debug)) + "\n" o += " return pos;\n" @@ -2702,7 +2712,7 @@ def build_message_type( # Add calculate_size method only if this message needs encoding and has fields if needs_encode and size_calc and not is_inline_only: - o = f"uint32_t {desc.name}::calculate_size() const {{\n" + o = f"{speed_attr}uint32_t {desc.name}::calculate_size() const {{\n" o += " uint32_t size = 0;\n" o += indent("\n".join(size_calc)) + "\n" o += " return size;\n" From 033e144e06a073489109ff5695ea9b3c646f6f48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:12:57 +0000 Subject: [PATCH 1447/2056] Bump aioesphomeapi from 44.14.0 to 44.15.0 (#15699) 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 339b5d29191..726a0a221ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.14.0 +aioesphomeapi==44.15.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From bd63f63b369aefc5dedff8f50551ea9ee5c9c42e Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:40:49 +0200 Subject: [PATCH 1448/2056] [esp32] Fix some compiler warnings & bugs (#15610) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/atm90e32/atm90e32.h | 4 ++-- esphome/components/midea/air_conditioner.cpp | 3 ++- esphome/components/mipi_spi/mipi_spi.h | 8 ++++---- esphome/components/tcs34725/tcs34725.cpp | 2 +- esphome/cpp_helpers.py | 3 ++- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index c44a11e3ed4..95154812cb4 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent, #endif float get_reference_voltage(uint8_t phase) { #ifdef USE_NUMBER - return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage + return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage #else return 120.0; // Default voltage #endif } float get_reference_current(uint8_t phase) { #ifdef USE_NUMBER - return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current + return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current #else return 5.0f; // Default current #endif diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 50521cf238b..69e0d46d2d8 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -28,7 +28,8 @@ void AirConditioner::on_status_change() { if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK && this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) { // Read existing presets (set by codegen), append frost protection, write back - const auto &existing = this->get_traits().get_supported_custom_presets(); + auto traits = this->get_traits(); + const auto &existing = traits.get_supported_custom_presets(); bool found = false; for (const char *p : existing) { if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) { diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 423226b1d70..2242be6c17b 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -234,9 +234,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, - this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, - this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, + internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, + (uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, + this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, HAS_HARDWARE_ROTATION); } @@ -305,7 +305,7 @@ class MipiSpi : public display::Display, this->write_command_(BRIGHTNESS, this->brightness_.value()); // calculate new madctl value from base value adjusted for rotation - uint8_t madctl = MADCTL; // lower 8 bits only + uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0; constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX; constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY; diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 4fe87de0ca2..1098d8de5fe 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration my_integration_time_regval = integration_time; this->integration_time_auto_ = false; } - this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f; + this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f; ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_); } void TCS34725Component::set_gain(TCS34725Gain gain) { diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 479090016f8..f2bd3b92a31 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -113,7 +113,8 @@ def _generate_source_table_code( entries = ", ".join(var_names) lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{") - lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");') + cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}" + lines.append(f' if ({cond}) return LOG_STR("<unknown>");') lines.append(" return reinterpret_cast<const LogString *>(") lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") lines.append("}") From 8843c36ec62902869384bb2c3434633e12278d6e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 13 Apr 2026 15:42:02 +0200 Subject: [PATCH 1449/2056] [micro_wake_word] Bugfix: Use es-nn v1.1.2 (last known working version) (#15703) --- esphome/components/micro_wake_word/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index de95e4961b9..5ab1e4bb805 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -452,7 +452,7 @@ async def to_code(config): esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") # Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn) - esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1") + esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") From 445715b9fd3f2b2579d23388aeba03a0eb931424 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:18:52 -0400 Subject: [PATCH 1450/2056] [esp32] Update the recommended platform to 55.03.38-1 (#15705) --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 8 ++++---- platformio.ini | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index cd61d9ec480..60c3776aa84 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf +10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cd38c82dd89..2974028b503 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -676,7 +676,7 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(3, 3, 8), } ARDUINO_PLATFORM_VERSION_LOOKUP = { - cv.Version(3, 3, 8): cv.Version(55, 3, 38), + cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"), cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), @@ -724,7 +724,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", - cv.Version(5, 5, 4): cv.Version(55, 3, 38), + cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 37), @@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 38), - "latest": cv.Version(55, 3, 38), + "recommended": cv.Version(55, 3, 38, "1"), + "latest": cv.Version(55, 3, 38, "1"), "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } diff --git a/platformio.ini b/platformio.ini index 7d17628a8ff..708d62afdc7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -133,7 +133,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip platform_packages = pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz @@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip platform_packages = pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz From 28f3bcdba3197e0751169ae23260b0e309ad7e39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 08:25:05 -1000 Subject: [PATCH 1451/2056] [api] Add speed_optimized to SubscribeLogsResponse (#15698) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/api/api.proto | 5 +++-- esphome/components/api/api_pb2.cpp | 18 ++++++++++++------ script/api_protobuf/api_protobuf.py | 6 ++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4a9a6f90512..f906cfb8d78 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -778,9 +778,10 @@ message SubscribeLogsResponse { option (source) = SOURCE_SERVER; option (log) = false; option (no_delay) = false; + option (speed_optimized) = true; - LogLevel level = 1; - bytes message = 3; + LogLevel level = 1 [(force) = true]; + bytes message = 3 [(force) = true]; } // ==================== NOISE ENCRYPTION ==================== diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 481510e407b..f304c852822 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -916,16 +916,22 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t } return true; } -uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); - ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level)); - ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_); return pos; } -uint32_t SubscribeLogsResponse::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; - size += this->level ? 2 : 0; - size += ProtoSize::calc_length(1, this->message_len_); + size += 2; + size += ProtoSize::calc_length_force(1, this->message_len_); return size; } #ifdef USE_API_NOISE diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 1743d2bb340..73e0859d5eb 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1028,7 +1028,8 @@ class BytesType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" + calc_fn = "calc_length_force" if force else "calc_length" + return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes @@ -1109,7 +1110,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + calc_fn = "calc_length_force" if force else "calc_length" + return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);" class PointerToStringBufferType(PointerToBufferTypeBase): From 89fbfc6f7107fefeb44f94223475830b7f0b6652 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 13:59:32 -1000 Subject: [PATCH 1452/2056] [adc] Place ADC oneshot control functions in IRAM for cache safety (#15717) --- esphome/components/adc/sensor.py | 15 ++++++++++++++- esphome/components/esp32/__init__.py | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index bab2762f00e..09e09f0dc1b 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,11 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler -from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component +from esphome.components.esp32 import ( + get_esp32_variant, + include_builtin_idf_component, + require_adc_oneshot_iram, +) from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.zephyr import ( zephyr_add_overlay, @@ -24,6 +28,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE +from esphome.types import ConfigType from . import ( ATTENUATION_MODES, @@ -65,6 +70,13 @@ def validate_config(config): return config +def _require_adc_iram(config: ConfigType) -> ConfigType: + """Register ADC oneshot IRAM requirement during config validation.""" + if CORE.is_esp32: + require_adc_oneshot_iram() + return config + + ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) @@ -95,6 +107,7 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("60s")), validate_config, + _require_adc_iram, ) CONF_ADC_CHANNEL_ID = "adc_channel_id" diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 2974028b503..7b3f9da3da6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1058,6 +1058,7 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert" CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7" CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram" CONF_DISABLE_FATFS = "disable_fatfs" +CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram" # VFS requirement tracking # Components that need VFS features can call require_vfs_*() functions @@ -1071,6 +1072,7 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" KEY_FATFS_REQUIRED = "fatfs_required" KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" +KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required" def require_vfs_select() -> None: @@ -1168,6 +1170,17 @@ def require_fatfs() -> None: CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True +def require_adc_oneshot_iram() -> None: + """Mark that ADC oneshot IRAM safety is required by a component. + + Call this from components that use the ADC oneshot driver. When flash cache is + disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management), + the ADC oneshot read function must be in IRAM to avoid crashes. + This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM. + """ + CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -1268,6 +1281,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean, cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, } ), @@ -2068,6 +2082,16 @@ async def to_code(config): if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False) + # Place ADC oneshot control functions in IRAM for cache safety + # When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread, + # power management, etc.), ADC reads will crash if these functions are in flash. + # Components using ADC call require_adc_oneshot_iram() to force this. + if ( + CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False) + or advanced[CONF_ADC_ONESHOT_IN_IRAM] + ): + add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True) + # Disable FATFS support # Components that need FATFS (SD card, etc.) can call require_fatfs() if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False): From a77ab59436985333112d4f42a3e670cd77f3d6f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 13:59:45 -1000 Subject: [PATCH 1453/2056] [web_server] Reset OTA backend on new upload to avoid brick after interrupted OTA (#15720) --- .../web_server/ota/ota_web_server.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 95b166901ad..9812714ec03 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -114,7 +114,25 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf uint8_t *data, size_t len, bool final) { ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; - if (index == 0 && !this->ota_backend_) { + // First byte of a new upload: index==0 with actual data. (web_server_idf + // fires a separate start-marker call with data==nullptr/len==0 before the + // first real chunk; gate on len>0 so we only trigger once per upload.) + if (index == 0 && len > 0) { + // If a previous upload was interrupted (e.g. client closed the tab, TCP + // reset) the backend from that session may still be open. Tear it down + // so flash state doesn't get concatenated with the new image (which can + // produce a technically-valid-sized but corrupted firmware that bricks + // the device once it reboots). + if (this->ota_backend_) { + ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session"); + this->ota_backend_->abort(); +#ifdef USE_OTA_STATE_LISTENER + // Notify listeners that the previous session was aborted before the new one starts. + this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0); +#endif + this->ota_backend_.reset(); + } + // Initialize OTA on first call this->ota_init_(filename.c_str()); From 311812c8cc838cf12af9f8605ff7145a9164dcb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 13 Apr 2026 16:58:48 -1000 Subject: [PATCH 1454/2056] [esphome] Skip missing extra flash images in upload_using_esptool (#15723) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/__main__.py | 9 +++++++- tests/unit_tests/test_main.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 25b404ae45c..7879cdad0ca 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -750,8 +750,15 @@ def upload_using_esptool( platformio_api.FlashImage( path=idedata.firmware_bin_path, offset=firmware_offset ), - *idedata.extra_flash_images, ] + for image in idedata.extra_flash_images: + if not image.path.is_file(): + _LOGGER.warning( + "Skipping missing flash image declared by platform: %s", + image.path, + ) + continue + flash_images.append(image) mcu = "esp8266" if CORE.is_esp32: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 85536d2f1ce..e07b4accf23 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1231,6 +1231,48 @@ def test_upload_using_esptool_path_conversion( assert partitions_path.endswith("partitions.bin") +def test_upload_using_esptool_skips_missing_extra_flash_images( + tmp_path: Path, + mock_run_external_command_main: Mock, + mock_get_idedata: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """A non-existent path in extra_flash_images must be filtered out with a + warning, and must not appear in the esptool command line. Only the valid + images are flashed. Regression test for + https://github.com/esphome/esphome/issues/15634. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test") + CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} + + missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin" + + mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" + mock_idedata.extra_flash_images = [ + platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + platformio_api.FlashImage(path=missing_path, offset="0x2d0000"), + ] + mock_get_idedata.return_value = mock_idedata + + (tmp_path / "firmware.bin").touch() + (tmp_path / "bootloader.bin").touch() + # Intentionally do NOT create missing_path + + config = {CONF_ESPHOME: {"platformio_options": {}}} + + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + result = upload_using_esptool(config, "/dev/ttyUSB0", None, None) + + assert result == 0 + assert "Skipping missing flash image" in caplog.text + assert str(missing_path) in caplog.text + + cmd_list = list(mock_run_external_command_main.call_args[0][1:]) + assert str(missing_path) not in cmd_list + assert "0x2d0000" not in cmd_list + + def test_upload_using_esptool_with_file_path( tmp_path: Path, mock_run_external_command_main: Mock, From 089a2c99e2c0cfecd4a77094de63d4e164f2a8da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 03:18:38 -1000 Subject: [PATCH 1455/2056] [globals] Fix TemplatableFn deprecation warning for globals.set (#15733) --- esphome/components/globals/__init__.py | 7 ++++++- tests/components/globals/common.yaml | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index ec6730a41c5..46725fe6ddd 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -108,8 +108,13 @@ async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) + # Use the global's value_type alias as the lambda return type so + # TemplatableFn stores a direct function pointer instead of going through + # the deprecated converting trampoline when the value expression deduces + # to a different type (e.g. int literal assigned to a float global). + value_type = cg.RawExpression(f"{full_id.type}::value_type") templ = await cg.templatable( - config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True + config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression ) cg.add(var.set_value(templ)) return var diff --git a/tests/components/globals/common.yaml b/tests/components/globals/common.yaml index 35dca0624f3..6d5721d3be8 100644 --- a/tests/components/globals/common.yaml +++ b/tests/components/globals/common.yaml @@ -4,6 +4,14 @@ esphome: - globals.set: id: glob_int value: "10" + # Set a float global with an integer literal - must emit the correct + # return type so TemplatableFn stores a direct function pointer. + - globals.set: + id: glob_float + value: "102" + - globals.set: + id: glob_float + value: !lambda "return 42;" globals: - id: glob_int From ef44491c695dc03acb80a736121221c986a6b384 Mon Sep 17 00:00:00 2001 From: Alexey Spirkov <extmail@alsp.net> Date: Tue, 14 Apr 2026 21:07:16 +0300 Subject: [PATCH 1456/2056] [i2s_audio] Add PDM mics support for ESP32-P4 (#15333) Co-authored-by: Alexey Spirkov <dev@alsp.net> --- esphome/components/i2s_audio/microphone/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 761cbb7f48b..1392d1d4ec1 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_( ) INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32] -PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3] +PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4] def _validate_esp32_variant(config): From 0a568a3e1eaf6a10c7d954a9873888a800b00537 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 09:45:42 -1000 Subject: [PATCH 1457/2056] [light] Avoid addressable transition stall at low gamma-corrected values (#15726) --- .../components/light/addressable_light.cpp | 63 +++++++++- esphome/components/light/addressable_light.h | 3 + .../addressable_light_transition.yaml | 29 +++++ .../mock_addressable_light/__init__.py | 1 + .../mock_addressable_light/light.py | 23 ++++ .../mock_addressable_light.h | 52 ++++++++ .../test_addressable_light_transition.py | 119 ++++++++++++++++++ 7 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 tests/integration/fixtures/addressable_light_transition.yaml create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/__init__.py create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/light.py create mode 100644 tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h create mode 100644 tests/integration/test_addressable_light_transition.py diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 2f6ffc9a38a..d2f5913f4b5 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -58,6 +58,12 @@ void AddressableLightTransformer::start() { // our transition will handle brightness, disable brightness in correction. this->light_.correction_.set_local_brightness(255); this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); + + // Uniformity scan is deferred to the first apply() call. start() can run before the underlying + // LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE + // triggering a transition), and reading through ESPColorView would deref a null buffer. + this->uniform_start_scanned_ = false; + this->uniform_start_is_uniform_ = false; } inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { @@ -97,12 +103,57 @@ optional<LightColorValues> AddressableLightTransformer::apply() { // non-linear when applying small deltas. if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { - int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); - for (auto led : this->light_) { - led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), - subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), - subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), - subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); + // Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the + // frame buffer is valid. When every LED already has the same color (the common case: plain + // turn_on/turn_off on a uniform strip), interpolate math-only against a single start color. + // Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip + // quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27 + // round to stored 0, freezing progress). + if (!this->uniform_start_scanned_) { + this->uniform_start_scanned_ = true; + if (this->light_.size() > 0) { + Color first = this->light_[0].get(); + bool uniform = true; + for (int32_t i = 1; i < this->light_.size(); i++) { + if (this->light_[i].get() != first) { + uniform = false; + break; + } + } + if (uniform) { + this->uniform_start_color_ = first; + this->uniform_start_is_uniform_ = true; + } + } + } + if (this->uniform_start_is_uniform_) { + // All LEDs started at the same color: compute the interpolated value once and write it to + // every LED. No read-back, so each LED's stored byte advances through every gamma threshold + // as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values. + // + // Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be + // overwritten on the next apply() here. The fallback path below would have respected them + // via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we + // support, so this is acceptable. + // lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress). + const Color &start = this->uniform_start_color_; + int32_t remaining = int32_t(256.f * (1.f - smoothed_progress)); + uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining); + uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining); + uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining); + uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining); + for (auto led : this->light_) { + led.set_rgbw(r, g, b, w); + } + } else { + int32_t scale = + int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); + for (auto led : this->light_) { + led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), + subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), + subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), + subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); + } } this->last_transition_progress_ = smoothed_progress; this->light_.schedule_show(); diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 17cdb7d6f6c..0202ad380a8 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -115,6 +115,9 @@ class AddressableLightTransformer : public LightTransformer { AddressableLight &light_; float last_transition_progress_{0.0f}; Color target_color_{}; + Color uniform_start_color_{}; + bool uniform_start_scanned_{false}; + bool uniform_start_is_uniform_{false}; }; } // namespace esphome::light diff --git a/tests/integration/fixtures/addressable_light_transition.yaml b/tests/integration/fixtures/addressable_light_transition.yaml new file mode 100644 index 00000000000..7b847dd8038 --- /dev/null +++ b/tests/integration/fixtures/addressable_light_transition.yaml @@ -0,0 +1,29 @@ +esphome: + name: addr-light-transition +host: +api: +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +light: + - platform: mock_addressable_light + output_id: strip_output + id: strip + name: "Test Strip" + num_leds: 4 + gamma_correct: 2.8 + default_transition_length: 0s + +sensor: + - platform: template + name: "led0_red_raw" + id: led0_red_raw + update_interval: 10ms + accuracy_decimals: 0 + lambda: |- + return (float) id(strip_output).get_raw_red(0); diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py b/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py new file mode 100644 index 00000000000..e8cfff8e1fb --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/tests"] diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/light.py b/tests/integration/fixtures/external_components/mock_addressable_light/light.py new file mode 100644 index 00000000000..293d2854f45 --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/light.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import light +import esphome.config_validation as cv +from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID +from esphome.types import ConfigType + +mock_addressable_light_ns = cg.esphome_ns.namespace("mock_addressable_light") +MockAddressableLight = mock_addressable_light_ns.class_( + "MockAddressableLight", light.AddressableLight +) + +CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(MockAddressableLight), + cv.Optional(CONF_NUM_LEDS, default=4): cv.positive_not_null_int, + } +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS]) + await light.register_light(var, config) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h b/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h new file mode 100644 index 00000000000..c6b0d10601b --- /dev/null +++ b/tests/integration/fixtures/external_components/mock_addressable_light/mock_addressable_light.h @@ -0,0 +1,52 @@ +#pragma once + +#include <cstddef> +#include <cstdint> +#include <memory> + +#include "esphome/components/light/addressable_light.h" +#include "esphome/core/component.h" + +namespace esphome::mock_addressable_light { + +// In-memory addressable light for host-mode integration tests. Exposes the raw +// per-LED byte buffer (post-gamma-correction, as the hardware would see it) +// so tests can observe transition behavior without real hardware. +class MockAddressableLight : public light::AddressableLight { + public: + explicit MockAddressableLight(uint16_t num_leds) + : num_leds_(num_leds), buf_(new uint8_t[num_leds * 4]()), effect_data_(new uint8_t[num_leds]()) {} + + void setup() override {} + void write_state(light::LightState *state) override {} + int32_t size() const override { return this->num_leds_; } + void clear_effect_data() override { + for (uint16_t i = 0; i < this->num_leds_; i++) + this->effect_data_[i] = 0; + } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + + // Accessors for tests: return the raw stored byte (post gamma correction), + // which is what actual LED hardware would receive. + uint8_t get_raw_red(uint16_t index) const { return this->buf_[index * 4 + 0]; } + uint8_t get_raw_green(uint16_t index) const { return this->buf_[index * 4 + 1]; } + uint8_t get_raw_blue(uint16_t index) const { return this->buf_[index * 4 + 2]; } + uint8_t get_raw_white(uint16_t index) const { return this->buf_[index * 4 + 3]; } + + protected: + light::ESPColorView get_view_internal(int32_t index) const override { + size_t pos = index * 4; + return {this->buf_.get() + pos + 0, this->buf_.get() + pos + 1, this->buf_.get() + pos + 2, + this->buf_.get() + pos + 3, this->effect_data_.get() + index, &this->correction_}; + } + + uint16_t num_leds_; + std::unique_ptr<uint8_t[]> buf_; + std::unique_ptr<uint8_t[]> effect_data_; +}; + +} // namespace esphome::mock_addressable_light diff --git a/tests/integration/test_addressable_light_transition.py b/tests/integration/test_addressable_light_transition.py new file mode 100644 index 00000000000..37fecde5952 --- /dev/null +++ b/tests/integration/test_addressable_light_transition.py @@ -0,0 +1,119 @@ +"""Integration test for addressable light transitions with gamma correction. + +Regression test for a bug where a long turn-on transition on an addressable +light with gamma correction (e.g. gamma_correct: 2.8) produced no visible +output for ~90% of the transition duration, then jumped to the target in the +final ~10%. Root cause: the transition algorithm read each LED's current value +back through the 8-bit stored byte every step; at gamma 2.8 any pre-gamma value +below ~27 rounds to stored byte 0, so the stored byte stalled at 0 until +progress was high enough for a single step to produce a large-enough pre-gamma +value to clear the gamma threshold. + +The fix interpolates against a cached start color when all LEDs started at the +same value (the common case for plain turn_on/turn_off), avoiding the round-trip. + +This test uses a host-only mock addressable light that exposes the raw stored +byte of each LED, so we can observe the transition directly. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import LightInfo, SensorInfo, SensorState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_addressable_light_transition( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """With gamma 2.8, the stored raw byte must rise visibly well before the end.""" + async with run_compiled(yaml_config), api_client_connected() as client: + entities, _ = await client.list_entities_services() + light = require_entity(entities, "test_strip", LightInfo) + sensor = require_entity(entities, "led0_red_raw", SensorInfo) + + # Track the raw-byte sensor. It polls every 10ms in the fixture, and + # ESPHome sensors publish on every change, so we collect a time series. + # Samples are stored as absolute (loop_time, value); we rebase to the + # command-issue time after the run so pre-command samples are strictly + # negative and reliably excluded. + loop = asyncio.get_running_loop() + samples: list[tuple[float, float]] = [] + + def on_state(state: object) -> None: + if not isinstance(state, SensorState) or state.key != sensor.key: + return + samples.append((loop.time(), state.state)) + + # InitialStateHelper swallows the first state ESPHome sends per entity + # on subscribe, so on_state only sees real post-subscribe updates. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + # Start transition: off -> full white over 1 second. This is the + # scenario from the bug report, compressed in time. + transition_s = 1.0 + command_time = loop.time() + client.light_command( + key=light.key, + state=True, + rgb=(1.0, 1.0, 1.0), + brightness=1.0, + transition_length=transition_s, + ) + + # Let the full transition run, plus margin for the final sample. + await asyncio.sleep(transition_s + 0.2) + + # Rebase to command-issue time. Pre-command samples have t < 0 and are + # excluded; everything else is in seconds since the command was issued. + post_command = [ + (t - command_time, v) for (t, v) in samples if t >= command_time + ] + assert post_command, "no sensor samples received after command was issued" + + # Assertion 1: the transition is not stalled. With the bug, the raw + # byte stays at 0 until ~90% of the transition duration. With the fix, + # it becomes nonzero in the first ~30% (for gamma 2.8, pre-gamma 76 + # clears the gamma threshold at progress ~0.30). Require the first + # nonzero sample to land well before 50% of the transition duration, + # measured from the command-issue time. The 50% bound (rather than + # 70%) leaves headroom for assertion 2's mid-window check. + first_nonzero = next(((t, v) for (t, v) in post_command if v > 0), None) + assert first_nonzero is not None, ( + "raw byte never rose above 0 during the transition — the fade stalled" + ) + assert first_nonzero[0] < transition_s * 0.5, ( + f"raw byte only rose above 0 at t={first_nonzero[0]:.3f}s " + f"(>{transition_s * 0.5:.3f}s after command) — transition is stalling" + ) + + # Assertion 2: by mid-late transition, the raw byte should have reached + # a substantial fraction of its final value. Bound the window to + # [50%, 90%] of the transition so the post-transition settled value + # (which always reaches 255) can't satisfy this assertion — that would + # let "stays at 0 then jumps at 99%" regressions slip through. + mid_window = [ + v + for (t, v) in post_command + if transition_s * 0.5 <= t <= transition_s * 0.9 + ] + assert mid_window, "no samples captured in mid-transition window" + assert max(mid_window) >= 100, ( + f"raw byte peaked at only {max(mid_window)} between 50%–90% of " + "transition (expected >= 100 for white target at gamma 2.8)" + ) + + # Assertion 3: final value reaches target. Gamma 2.8 of 255 is 255. + final_samples = [v for (_, v) in post_command[-5:]] + assert max(final_samples) >= 250, ( + f"final raw byte was {max(final_samples)}, expected >= 250" + ) From 3ef140e25d1c39016c1ac528c53e81466e74ff04 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:47:44 +0200 Subject: [PATCH 1458/2056] [nextion] Fix command spacing pacer never throttling sends (#15664) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/nextion.cpp | 108 +++++++++++++++++-------- esphome/components/nextion/nextion.h | 15 ++-- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b0e14b5ea3f..e42f7ca2169 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -36,8 +36,9 @@ bool Nextion::send_command_(const std::string &command) { } #ifdef USE_NEXTION_COMMAND_SPACING - if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) { - ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); + const uint32_t now = App.get_loop_component_start_time(); + if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) { + ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str()); return false; } #endif // USE_NEXTION_COMMAND_SPACING @@ -48,6 +49,16 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); +#ifdef USE_NEXTION_COMMAND_SPACING + // Mark sent immediately after writing to UART. The pacer enforces inter-command + // spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_ + // at zero indefinitely, making can_send() always return true and spacing a no-op. + // ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally. + if (!this->connection_state_.ignore_is_setup_) { + this->command_pacer_.mark_sent(now); + } +#endif // USE_NEXTION_COMMAND_SPACING + return true; } @@ -253,11 +264,8 @@ bool Nextion::send_command(const char *command) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return false; - if (this->send_command_(command)) { - this->add_no_result_to_queue_("command"); - return true; - } - return false; + this->add_no_result_to_queue_with_command_("command", command); + return true; } bool Nextion::send_command_printf(const char *format, ...) { @@ -274,11 +282,8 @@ bool Nextion::send_command_printf(const char *format, ...) { return false; } - if (this->send_command_(buffer)) { - this->add_no_result_to_queue_("command_printf"); - return true; - } - return false; + this->add_no_result_to_queue_with_command_("command_printf", buffer); + return true; } #ifdef NEXTION_PROTOCOL_LOG @@ -349,25 +354,43 @@ void Nextion::loop() { } #ifdef USE_NEXTION_COMMAND_SPACING - // Try to send any pending commands if spacing allows this->process_pending_in_queue_(); +#ifdef USE_NEXTION_WAVEFORM + if (!this->waveform_queue_.empty()) { + this->check_pending_waveform_(); + } +#endif // USE_NEXTION_WAVEFORM #endif // USE_NEXTION_COMMAND_SPACING } #ifdef USE_NEXTION_COMMAND_SPACING void Nextion::process_pending_in_queue_() { - if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { - return; - } +#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP + size_t commands_sent = 0; +#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP - // Check if first item in queue has a pending command - auto *front_item = this->nextion_queue_.front(); - if (front_item && !front_item->pending_command.empty()) { - if (this->send_command_(front_item->pending_command)) { - // Command sent successfully, clear the pending command - front_item->pending_command.clear(); - ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); + for (auto *item : this->nextion_queue_) { + if (item == nullptr || item->pending_command.empty()) { + continue; // Already sent, waiting for ACK — skip, don't stop } + +#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP + if (++commands_sent > this->max_commands_per_loop_) { + ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring"); + break; + } +#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP + + const uint32_t now = App.get_loop_component_start_time(); + if (!this->command_pacer_.can_send(now)) { + break; // Spacing not elapsed, stop for this loop iteration + } + + if (!this->send_command_(item->pending_command)) { + break; // Unexpected send failure, stop + } + item->pending_command.clear(); + ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str()); } } #endif // USE_NEXTION_COMMAND_SPACING @@ -470,10 +493,6 @@ void Nextion::process_nextion_commands_() { this->setup_callback_.call(); } } -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent - ESP_LOGN(TAG, "Command spacing: marked command sent"); -#endif break; case 0x02: // invalid Component ID or name was used ESP_LOGW(TAG, "Invalid component ID/name"); @@ -1079,10 +1098,18 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { } /** - * @brief + * @brief Send a command and enqueue it for response tracking. * - * @param variable_name Variable name for the queue - * @param command + * Callers are responsible for checking is_sleeping() before calling this + * method. The sleep guard is deliberately absent here because some callers + * (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly + * sleep-safe and must bypass it. + * + * If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready, + * the command is saved in the queue entry for retry rather than dropped. + * + * @param variable_name Name of the variable or component associated with the command. + * @param command The raw command string to send. */ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty()) @@ -1263,9 +1290,22 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { std::string command = "get " + component->get_variable_name_to_send(); +#ifdef USE_NEXTION_COMMAND_SPACING + // Always enqueue first so the response handler is present when the command + // is eventually sent. Store the command for retry if spacing blocked it; + // process_pending_in_queue_() will transmit it when the pacer allows. + nextion_queue->pending_command = command; + this->nextion_queue_.push_back(nextion_queue); + if (this->send_command_(command)) { + nextion_queue->pending_command.clear(); + } +#else // USE_NEXTION_COMMAND_SPACING if (this->send_command_(command)) { this->nextion_queue_.push_back(nextion_queue); + } else { + delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) } +#endif // USE_NEXTION_COMMAND_SPACING } #ifdef USE_NEXTION_WAVEFORM @@ -1309,10 +1349,10 @@ void Nextion::check_pending_waveform_() { char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); - if (!this->send_command_(command)) { - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop(); - } + // If spacing or setup state blocks the send, leave the entry at the front + // of waveform_queue_ for retry on the next loop iteration via + // check_pending_waveform_(). Only pop on a successful send. + this->send_command_(command); } #endif // USE_NEXTION_WAVEFORM diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c84a5cd49c2..c62772ac757 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -55,15 +55,20 @@ class NextionCommandPacer { uint8_t get_spacing() const { return spacing_ms_; } /** - * @brief Check if enough time has passed to send next command - * @return true if enough time has passed since last command + * @brief Check if enough time has passed to send the next command. + * @param now Current timestamp in milliseconds (use App.get_loop_component_start_time() + * for consistency with the rest of the queue timing). + * @return true if the spacing interval has elapsed since the last command was sent. */ - bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; } + bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; } /** - * @brief Mark a command as sent, updating the timing + * @brief Record the transmit timestamp for the most recently sent command. + * @param now Current timestamp in milliseconds, as returned by + * App.get_loop_component_start_time(). Must use the same clock + * source as can_send() to avoid unsigned underflow. */ - void mark_sent() { last_command_time_ = millis(); } + void mark_sent(uint32_t now) { last_command_time_ = now; } private: uint8_t spacing_ms_; From 4c43f7e9d06dbf5df989aab28c494eef93f225a9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:58:30 +1200 Subject: [PATCH 1459/2056] Bump version to 2026.4.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 599109b43f3..fb85e8028c7 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0b2 +PROJECT_NUMBER = 2026.4.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index b4cd7583302..d5d726d0db1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0b2" +__version__ = "2026.4.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 193e7d476d5b36efd599154a0c98f8144d10066a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:12:03 +1200 Subject: [PATCH 1460/2056] Pin GitHub Actions to commit SHAs Replace mutable tag references with immutable commit SHAs to prevent supply-chain attacks via compromised tags. Version comments are preserved for readability. --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 8806a89748f..20f9a74ea97 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -8,4 +8,4 @@ on: jobs: lock: - uses: esphome/workflows/.github/workflows/lock.yml@main + uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0 From 2bdd9f6217a664e16a7a5f3270a9aae000b7e434 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:44:30 +1200 Subject: [PATCH 1461/2056] Bump version to 2026.4.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index fb85e8028c7..6f1f3d0e7e1 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0b3 +PROJECT_NUMBER = 2026.4.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index d5d726d0db1..95fd488dca6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0b3" +__version__ = "2026.4.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 3b82c6e38be2156b2446d19de51b7be78c7e4b86 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:32:11 -0400 Subject: [PATCH 1462/2056] [st7789v] Fix swapped offset_width/offset_height in model presets (#15755) --- esphome/components/st7789v/display.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 85414237cfe..745c37f47dd 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -45,8 +45,8 @@ MODELS = { presets={ CONF_HEIGHT: 240, CONF_WIDTH: 135, - CONF_OFFSET_HEIGHT: 52, - CONF_OFFSET_WIDTH: 40, + CONF_OFFSET_HEIGHT: 40, + CONF_OFFSET_WIDTH: 52, CONF_CS_PIN: "GPIO5", CONF_DC_PIN: "GPIO16", CONF_RESET_PIN: "GPIO23", @@ -68,8 +68,8 @@ MODELS = { presets={ CONF_HEIGHT: 280, CONF_WIDTH: 240, - CONF_OFFSET_HEIGHT: 0, - CONF_OFFSET_WIDTH: 20, + CONF_OFFSET_HEIGHT: 20, + CONF_OFFSET_WIDTH: 0, } ), "ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec( @@ -77,8 +77,8 @@ MODELS = { presets={ CONF_HEIGHT: 240, CONF_WIDTH: 135, - CONF_OFFSET_HEIGHT: 52, - CONF_OFFSET_WIDTH: 40, + CONF_OFFSET_HEIGHT: 40, + CONF_OFFSET_WIDTH: 52, CONF_CS_PIN: "GPIO7", CONF_DC_PIN: "GPIO39", CONF_RESET_PIN: "GPIO40", @@ -89,8 +89,8 @@ MODELS = { presets={ CONF_HEIGHT: 320, CONF_WIDTH: 170, - CONF_OFFSET_HEIGHT: 35, - CONF_OFFSET_WIDTH: 0, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 35, CONF_ROTATION: 270, CONF_CS_PIN: "GPIO10", CONF_DC_PIN: "GPIO13", @@ -102,8 +102,8 @@ MODELS = { presets={ CONF_HEIGHT: 320, CONF_WIDTH: 172, - CONF_OFFSET_HEIGHT: 34, - CONF_OFFSET_WIDTH: 0, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 34, CONF_ROTATION: 90, CONF_CS_PIN: "GPIO21", CONF_DC_PIN: "GPIO22", From 274c01ca74c5ee5d65d85d128ce852ef28f0de3b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:32:33 -0400 Subject: [PATCH 1463/2056] [sx126x][sx127x] Fix frequency precision loss from float32 codegen (#15753) --- esphome/components/sx126x/__init__.py | 4 ++-- esphome/components/sx127x/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 08f4c0fb882..b8696158fe5 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -200,11 +200,11 @@ CONFIG_SCHEMA = ( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( - cv.frequency, cv.float_range(min=0, max=100000) + cv.frequency, cv.int_range(min=0, max=100000) ), cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.All( - cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6)) ), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 7f554fbf841..8fa72471927 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -197,11 +197,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( - cv.frequency, cv.float_range(min=0, max=100000) + cv.frequency, cv.int_range(min=0, max=100000) ), cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.All( - cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6)) ), cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), From 10f52f2056890183a9b79d3036b219afed4274d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:07:49 -1000 Subject: [PATCH 1464/2056] Bump aioesphomeapi from 44.15.0 to 44.16.0 (#15757) 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 cd3aa5bd868..36c81e25bf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.15.0 +aioesphomeapi==44.16.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 403a9f7b7eef07e89e68c6b8c10da2a4b0041fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:12:30 -1000 Subject: [PATCH 1465/2056] Bump github/codeql-action from 4.35.1 to 4.35.2 (#15759) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 67f4690ac9f..246a8656934 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: category: "/language:${{matrix.language}}" From 01b5bef37fafaf4a4a1bb4db1240c5ea43dac717 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 15 Apr 2026 17:44:42 -1000 Subject: [PATCH 1466/2056] [status_led] Disable loop when idle (#15642) --- esphome/components/status_led/status_led.cpp | 19 +++++++++++++++---- esphome/core/application.cpp | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index a792110eebb..48762a73334 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -7,6 +7,11 @@ namespace status_led { static const char *const TAG = "status_led"; +static constexpr uint32_t ERROR_PERIOD_MS = 250; +static constexpr uint32_t ERROR_ON_MS = 150; +static constexpr uint32_t WARNING_PERIOD_MS = 1500; +static constexpr uint32_t WARNING_ON_MS = 250; + StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; } @@ -19,12 +24,18 @@ void StatusLED::dump_config() { LOG_PIN(" Pin: ", this->pin_); } void StatusLED::loop() { - if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) { - this->pin_->digital_write(millis() % 250u < 150u); - } else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) { - this->pin_->digital_write(millis() % 1500u < 250u); + const uint32_t app_state = App.get_app_state(); + // Use millis() rather than App.get_loop_component_start_time() because this loop is also + // dispatched from Application::feed_wdt() during long blocking operations, where the cached + // per-component timestamp doesn't advance and would freeze the blink pattern. + const uint32_t now = millis(); + if ((app_state & STATUS_LED_ERROR) != 0u) { + this->pin_->digital_write(now % ERROR_PERIOD_MS < ERROR_ON_MS); + } else if ((app_state & STATUS_LED_WARNING) != 0u) { + this->pin_->digital_write(now % WARNING_PERIOD_MS < WARNING_ON_MS); } else { this->pin_->digital_write(false); + this->disable_loop(); } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1c732307054..866edebbf60 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -225,7 +225,21 @@ void HOT Application::feed_wdt_slow_(uint32_t time) { this->last_wdt_feed_ = time; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { - status_led::global_status_led->call(); + auto *sl = status_led::global_status_led; + uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK; + if (sl_state == COMPONENT_STATE_LOOP_DONE) { + // status_led only transitions to LOOP_DONE from inside its own loop() (after the + // first idle-path dispatch), so its pin is already initialized by pre_setup() and + // its setup() has already run. Re-dispatch only if an error or warning bit has been + // set since; otherwise skip entirely. + if ((this->app_state_ & STATUS_LED_MASK) == 0) + return; + sl->enable_loop(); + } else if (sl_state != COMPONENT_STATE_LOOP) { + // CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle. + return; + } + sl->loop(); } #endif } From e7194dce75e3c7c9be03c6aaf78b2769ffb7c178 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 15 Apr 2026 17:45:01 -1000 Subject: [PATCH 1467/2056] [core] Deduplicate entity type boilerplate with X-macro pattern (#15618) --- esphome/components/web_server/list_entities.h | 3 + esphome/core/application.h | 366 ++---------------- esphome/core/component_iterator.cpp | 152 +------- esphome/core/component_iterator.h | 156 +------- esphome/core/controller.h | 139 +------ esphome/core/controller_registry.cpp | 2 - esphome/core/controller_registry.h | 326 +--------------- esphome/core/entity_includes.h | 79 ++++ esphome/core/entity_types.h | 98 +++++ esphome/writer.py | 6 +- script/ci-custom.py | 2 +- script/helpers.py | 7 +- 12 files changed, 281 insertions(+), 1055 deletions(-) create mode 100644 esphome/core/entity_includes.h create mode 100644 esphome/core/entity_types.h diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 6a840661097..8c22d757b67 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -75,6 +75,9 @@ class ListEntitiesIterator final : public ComponentIterator { #ifdef USE_VALVE bool on_valve(valve::Valve *obj) override; #endif +#ifdef USE_MEDIA_PLAYER + bool on_media_player(media_player::MediaPlayer *obj) override { return true; } +#endif #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override; #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index 60087d527d2..b4bb8a1eec9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -39,78 +39,7 @@ #include "esphome/components/runtime_stats/runtime_stats.h" #endif #include "esphome/core/wake.h" -#ifdef USE_BINARY_SENSOR -#include "esphome/components/binary_sensor/binary_sensor.h" -#endif -#ifdef USE_SENSOR -#include "esphome/components/sensor/sensor.h" -#endif -#ifdef USE_SWITCH -#include "esphome/components/switch/switch.h" -#endif -#ifdef USE_BUTTON -#include "esphome/components/button/button.h" -#endif -#ifdef USE_TEXT_SENSOR -#include "esphome/components/text_sensor/text_sensor.h" -#endif -#ifdef USE_FAN -#include "esphome/components/fan/fan.h" -#endif -#ifdef USE_CLIMATE -#include "esphome/components/climate/climate.h" -#endif -#ifdef USE_LIGHT -#include "esphome/components/light/light_state.h" -#endif -#ifdef USE_COVER -#include "esphome/components/cover/cover.h" -#endif -#ifdef USE_NUMBER -#include "esphome/components/number/number.h" -#endif -#ifdef USE_DATETIME_DATE -#include "esphome/components/datetime/date_entity.h" -#endif -#ifdef USE_DATETIME_TIME -#include "esphome/components/datetime/time_entity.h" -#endif -#ifdef USE_DATETIME_DATETIME -#include "esphome/components/datetime/datetime_entity.h" -#endif -#ifdef USE_TEXT -#include "esphome/components/text/text.h" -#endif -#ifdef USE_SELECT -#include "esphome/components/select/select.h" -#endif -#ifdef USE_LOCK -#include "esphome/components/lock/lock.h" -#endif -#ifdef USE_VALVE -#include "esphome/components/valve/valve.h" -#endif -#ifdef USE_MEDIA_PLAYER -#include "esphome/components/media_player/media_player.h" -#endif -#ifdef USE_ALARM_CONTROL_PANEL -#include "esphome/components/alarm_control_panel/alarm_control_panel.h" -#endif -#ifdef USE_WATER_HEATER -#include "esphome/components/water_heater/water_heater.h" -#endif -#ifdef USE_INFRARED -#include "esphome/components/infrared/infrared.h" -#endif -#ifdef USE_SERIAL_PROXY -#include "esphome/components/serial_proxy/serial_proxy.h" -#endif -#ifdef USE_EVENT -#include "esphome/components/event/event.h" -#endif -#ifdef USE_UPDATE -#include "esphome/components/update/update_entity.h" -#endif +#include "esphome/core/entity_includes.h" namespace esphome::socket { #ifdef USE_HOST @@ -190,93 +119,16 @@ class Application { void set_current_component(Component *component) { this->current_component_ = component; } Component *get_current_component() { return this->current_component_; } -#ifdef USE_BINARY_SENSOR - void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - this->binary_sensors_.push_back(binary_sensor); - } -#endif - -#ifdef USE_SENSOR - void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); } -#endif - -#ifdef USE_SWITCH - void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); } -#endif - -#ifdef USE_BUTTON - void register_button(button::Button *button) { this->buttons_.push_back(button); } -#endif - -#ifdef USE_TEXT_SENSOR - void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); } -#endif - -#ifdef USE_FAN - void register_fan(fan::Fan *state) { this->fans_.push_back(state); } -#endif - -#ifdef USE_COVER - void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); } -#endif - -#ifdef USE_CLIMATE - void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); } -#endif - -#ifdef USE_LIGHT - void register_light(light::LightState *light) { this->lights_.push_back(light); } -#endif - -#ifdef USE_NUMBER - void register_number(number::Number *number) { this->numbers_.push_back(number); } -#endif - -#ifdef USE_DATETIME_DATE - void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); } -#endif - -#ifdef USE_DATETIME_TIME - void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); } -#endif - -#ifdef USE_DATETIME_DATETIME - void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); } -#endif - -#ifdef USE_TEXT - void register_text(text::Text *text) { this->texts_.push_back(text); } -#endif - -#ifdef USE_SELECT - void register_select(select::Select *select) { this->selects_.push_back(select); } -#endif - -#ifdef USE_LOCK - void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); } -#endif - -#ifdef USE_VALVE - void register_valve(valve::Valve *valve) { this->valves_.push_back(valve); } -#endif - -#ifdef USE_MEDIA_PLAYER - void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); } -#endif - -#ifdef USE_ALARM_CONTROL_PANEL - void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->alarm_control_panels_.push_back(a_alarm_control_panel); - } -#endif - -#ifdef USE_WATER_HEATER - void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); } -#endif - -#ifdef USE_INFRARED - void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } -#endif +// Entity register methods (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) \ + void register_##singular(type *obj) { this->plural##_.push_back(obj); } +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) #ifdef USE_SERIAL_PROXY void register_serial_proxy(serial_proxy::SerialProxy *proxy) { @@ -285,14 +137,6 @@ class Application { } #endif -#ifdef USE_EVENT - void register_event(event::Event *event) { this->events_.push_back(event); } -#endif - -#ifdef USE_UPDATE - void register_update(update::UpdateEntity *update) { this->updates_.push_back(update); } -#endif - /// Reserve space for components to avoid memory fragmentation /// Set up all the registered components. Call this at the end of your setup() function. @@ -456,108 +300,22 @@ class Application { #ifdef USE_AREAS const auto &get_areas() { return this->areas_; } #endif -#ifdef USE_BINARY_SENSOR - auto &get_binary_sensors() const { return this->binary_sensors_; } - GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors) -#endif -#ifdef USE_SWITCH - auto &get_switches() const { return this->switches_; } - GET_ENTITY_METHOD(switch_::Switch, switch, switches) -#endif -#ifdef USE_BUTTON - auto &get_buttons() const { return this->buttons_; } - GET_ENTITY_METHOD(button::Button, button, buttons) -#endif -#ifdef USE_SENSOR - auto &get_sensors() const { return this->sensors_; } - GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors) -#endif -#ifdef USE_TEXT_SENSOR - auto &get_text_sensors() const { return this->text_sensors_; } - GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors) -#endif -#ifdef USE_FAN - auto &get_fans() const { return this->fans_; } - GET_ENTITY_METHOD(fan::Fan, fan, fans) -#endif -#ifdef USE_COVER - auto &get_covers() const { return this->covers_; } - GET_ENTITY_METHOD(cover::Cover, cover, covers) -#endif -#ifdef USE_LIGHT - auto &get_lights() const { return this->lights_; } - GET_ENTITY_METHOD(light::LightState, light, lights) -#endif -#ifdef USE_CLIMATE - auto &get_climates() const { return this->climates_; } - GET_ENTITY_METHOD(climate::Climate, climate, climates) -#endif -#ifdef USE_NUMBER - auto &get_numbers() const { return this->numbers_; } - GET_ENTITY_METHOD(number::Number, number, numbers) -#endif -#ifdef USE_DATETIME_DATE - auto &get_dates() const { return this->dates_; } - GET_ENTITY_METHOD(datetime::DateEntity, date, dates) -#endif -#ifdef USE_DATETIME_TIME - auto &get_times() const { return this->times_; } - GET_ENTITY_METHOD(datetime::TimeEntity, time, times) -#endif -#ifdef USE_DATETIME_DATETIME - auto &get_datetimes() const { return this->datetimes_; } - GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes) -#endif -#ifdef USE_TEXT - auto &get_texts() const { return this->texts_; } - GET_ENTITY_METHOD(text::Text, text, texts) -#endif -#ifdef USE_SELECT - auto &get_selects() const { return this->selects_; } - GET_ENTITY_METHOD(select::Select, select, selects) -#endif -#ifdef USE_LOCK - auto &get_locks() const { return this->locks_; } - GET_ENTITY_METHOD(lock::Lock, lock, locks) -#endif -#ifdef USE_VALVE - auto &get_valves() const { return this->valves_; } - GET_ENTITY_METHOD(valve::Valve, valve, valves) -#endif -#ifdef USE_MEDIA_PLAYER - auto &get_media_players() const { return this->media_players_; } - GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players) -#endif - -#ifdef USE_ALARM_CONTROL_PANEL - auto &get_alarm_control_panels() const { return this->alarm_control_panels_; } - GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) -#endif - -#ifdef USE_WATER_HEATER - auto &get_water_heaters() const { return this->water_heaters_; } - GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters) -#endif - -#ifdef USE_INFRARED - auto &get_infrareds() const { return this->infrareds_; } - GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) -#endif +// Entity getter methods (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) \ + auto &get_##plural() const { return this->plural##_; } \ + GET_ENTITY_METHOD(type, singular, plural) +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) #ifdef USE_SERIAL_PROXY auto &get_serial_proxies() const { return this->serial_proxies_; } #endif -#ifdef USE_EVENT - auto &get_events() const { return this->events_; } - GET_ENTITY_METHOD(event::Event, event, events) -#endif - -#ifdef USE_UPDATE - auto &get_updates() const { return this->updates_; } - GET_ENTITY_METHOD(update::UpdateEntity, update, updates) -#endif - Scheduler scheduler; /// Register/unregister a socket to be monitored for read events. @@ -743,79 +501,19 @@ class Application { #ifdef USE_AREAS StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{}; #endif -#ifdef USE_BINARY_SENSOR - StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{}; -#endif -#ifdef USE_SWITCH - StaticVector<switch_::Switch *, ESPHOME_ENTITY_SWITCH_COUNT> switches_{}; -#endif -#ifdef USE_BUTTON - StaticVector<button::Button *, ESPHOME_ENTITY_BUTTON_COUNT> buttons_{}; -#endif -#ifdef USE_EVENT - StaticVector<event::Event *, ESPHOME_ENTITY_EVENT_COUNT> events_{}; -#endif -#ifdef USE_SENSOR - StaticVector<sensor::Sensor *, ESPHOME_ENTITY_SENSOR_COUNT> sensors_{}; -#endif -#ifdef USE_TEXT_SENSOR - StaticVector<text_sensor::TextSensor *, ESPHOME_ENTITY_TEXT_SENSOR_COUNT> text_sensors_{}; -#endif -#ifdef USE_FAN - StaticVector<fan::Fan *, ESPHOME_ENTITY_FAN_COUNT> fans_{}; -#endif -#ifdef USE_COVER - StaticVector<cover::Cover *, ESPHOME_ENTITY_COVER_COUNT> covers_{}; -#endif -#ifdef USE_CLIMATE - StaticVector<climate::Climate *, ESPHOME_ENTITY_CLIMATE_COUNT> climates_{}; -#endif -#ifdef USE_LIGHT - StaticVector<light::LightState *, ESPHOME_ENTITY_LIGHT_COUNT> lights_{}; -#endif -#ifdef USE_NUMBER - StaticVector<number::Number *, ESPHOME_ENTITY_NUMBER_COUNT> numbers_{}; -#endif -#ifdef USE_DATETIME_DATE - StaticVector<datetime::DateEntity *, ESPHOME_ENTITY_DATE_COUNT> dates_{}; -#endif -#ifdef USE_DATETIME_TIME - StaticVector<datetime::TimeEntity *, ESPHOME_ENTITY_TIME_COUNT> times_{}; -#endif -#ifdef USE_DATETIME_DATETIME - StaticVector<datetime::DateTimeEntity *, ESPHOME_ENTITY_DATETIME_COUNT> datetimes_{}; -#endif -#ifdef USE_SELECT - StaticVector<select::Select *, ESPHOME_ENTITY_SELECT_COUNT> selects_{}; -#endif -#ifdef USE_TEXT - StaticVector<text::Text *, ESPHOME_ENTITY_TEXT_COUNT> texts_{}; -#endif -#ifdef USE_LOCK - StaticVector<lock::Lock *, ESPHOME_ENTITY_LOCK_COUNT> locks_{}; -#endif -#ifdef USE_VALVE - StaticVector<valve::Valve *, ESPHOME_ENTITY_VALVE_COUNT> valves_{}; -#endif -#ifdef USE_MEDIA_PLAYER - StaticVector<media_player::MediaPlayer *, ESPHOME_ENTITY_MEDIA_PLAYER_COUNT> media_players_{}; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - StaticVector<alarm_control_panel::AlarmControlPanel *, ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT> - alarm_control_panels_{}; -#endif -#ifdef USE_WATER_HEATER - StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{}; -#endif -#ifdef USE_INFRARED - StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{}; -#endif +// Entity StaticVector fields (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) StaticVector<type *, count> plural##_{}; +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) + #ifdef USE_SERIAL_PROXY StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{}; #endif -#ifdef USE_UPDATE - StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{}; -#endif }; /// Global storage of Application pointer - only one Application can exist. diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index ff76b2b81bf..f4d3c05e190 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -33,53 +33,18 @@ void ComponentIterator::advance() { } break; -#ifdef USE_BINARY_SENSOR - case IteratorState::BINARY_SENSOR: - this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); - break; -#endif - -#ifdef USE_COVER - case IteratorState::COVER: - this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); - break; -#endif - -#ifdef USE_FAN - case IteratorState::FAN: - this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); - break; -#endif - -#ifdef USE_LIGHT - case IteratorState::LIGHT: - this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); - break; -#endif - -#ifdef USE_SENSOR - case IteratorState::SENSOR: - this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); - break; -#endif - -#ifdef USE_SWITCH - case IteratorState::SWITCH: - this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); - break; -#endif - -#ifdef USE_BUTTON - case IteratorState::BUTTON: - this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); - break; -#endif - -#ifdef USE_TEXT_SENSOR - case IteratorState::TEXT_SENSOR: - this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); - break; -#endif +// Entity iterator cases (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) \ + case IteratorState::upper: \ + this->process_platform_item_(App.get_##plural(), &ComponentIterator::on_##singular); \ + break; +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) #ifdef USE_API_USER_DEFINED_ACTIONS case IteratorState::SERVICE: @@ -97,96 +62,6 @@ void ComponentIterator::advance() { } break; #endif -#ifdef USE_CLIMATE - case IteratorState::CLIMATE: - this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); - break; -#endif - -#ifdef USE_NUMBER - case IteratorState::NUMBER: - this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); - break; -#endif - -#ifdef USE_DATETIME_DATE - case IteratorState::DATETIME_DATE: - this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); - break; -#endif - -#ifdef USE_DATETIME_TIME - case IteratorState::DATETIME_TIME: - this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); - break; -#endif - -#ifdef USE_DATETIME_DATETIME - case IteratorState::DATETIME_DATETIME: - this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); - break; -#endif - -#ifdef USE_TEXT - case IteratorState::TEXT: - this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); - break; -#endif - -#ifdef USE_SELECT - case IteratorState::SELECT: - this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); - break; -#endif - -#ifdef USE_LOCK - case IteratorState::LOCK: - this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); - break; -#endif - -#ifdef USE_VALVE - case IteratorState::VALVE: - this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); - break; -#endif - -#ifdef USE_MEDIA_PLAYER - case IteratorState::MEDIA_PLAYER: - this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); - break; -#endif - -#ifdef USE_ALARM_CONTROL_PANEL - case IteratorState::ALARM_CONTROL_PANEL: - this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); - break; -#endif - -#ifdef USE_WATER_HEATER - case IteratorState::WATER_HEATER: - this->process_platform_item_(App.get_water_heaters(), &ComponentIterator::on_water_heater); - break; -#endif - -#ifdef USE_INFRARED - case IteratorState::INFRARED: - this->process_platform_item_(App.get_infrareds(), &ComponentIterator::on_infrared); - break; -#endif - -#ifdef USE_EVENT - case IteratorState::EVENT: - this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); - break; -#endif - -#ifdef USE_UPDATE - case IteratorState::UPDATE: - this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); - break; -#endif - case IteratorState::MAX: if (this->on_end()) { this->state_ = IteratorState::NONE; @@ -203,7 +78,4 @@ bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return #ifdef USE_CAMERA bool ComponentIterator::on_camera(camera::Camera *camera) { return true; } #endif -#ifdef USE_MEDIA_PLAYER -bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; } -#endif } // namespace esphome diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 6c03b74a17c..9a1e5da3518 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -28,80 +28,21 @@ class ComponentIterator { void advance(); bool completed() const { return this->state_ == IteratorState::NONE; } virtual bool on_begin(); -#ifdef USE_BINARY_SENSOR - virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0; -#endif -#ifdef USE_COVER - virtual bool on_cover(cover::Cover *cover) = 0; -#endif -#ifdef USE_FAN - virtual bool on_fan(fan::Fan *fan) = 0; -#endif -#ifdef USE_LIGHT - virtual bool on_light(light::LightState *light) = 0; -#endif -#ifdef USE_SENSOR - virtual bool on_sensor(sensor::Sensor *sensor) = 0; -#endif -#ifdef USE_SWITCH - virtual bool on_switch(switch_::Switch *a_switch) = 0; -#endif -#ifdef USE_BUTTON - virtual bool on_button(button::Button *button) = 0; -#endif -#ifdef USE_TEXT_SENSOR - virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; -#endif +// Pure virtual entity callbacks (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) virtual bool on_##singular(type *obj) = 0; +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ +// NOLINTEND(bugprone-macro-parentheses) +// Non-entity and non-pure-virtual callbacks (have default implementations) #ifdef USE_API_USER_DEFINED_ACTIONS virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA virtual bool on_camera(camera::Camera *camera); -#endif -#ifdef USE_CLIMATE - virtual bool on_climate(climate::Climate *climate) = 0; -#endif -#ifdef USE_NUMBER - virtual bool on_number(number::Number *number) = 0; -#endif -#ifdef USE_DATETIME_DATE - virtual bool on_date(datetime::DateEntity *date) = 0; -#endif -#ifdef USE_DATETIME_TIME - virtual bool on_time(datetime::TimeEntity *time) = 0; -#endif -#ifdef USE_DATETIME_DATETIME - virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 0; -#endif -#ifdef USE_TEXT - virtual bool on_text(text::Text *text) = 0; -#endif -#ifdef USE_SELECT - virtual bool on_select(select::Select *select) = 0; -#endif -#ifdef USE_LOCK - virtual bool on_lock(lock::Lock *a_lock) = 0; -#endif -#ifdef USE_VALVE - virtual bool on_valve(valve::Valve *valve) = 0; -#endif -#ifdef USE_MEDIA_PLAYER - virtual bool on_media_player(media_player::MediaPlayer *media_player); -#endif -#ifdef USE_ALARM_CONTROL_PANEL - virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; -#endif -#ifdef USE_WATER_HEATER - virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0; -#endif -#ifdef USE_INFRARED - virtual bool on_infrared(infrared::Infrared *infrared) = 0; -#endif -#ifdef USE_EVENT - virtual bool on_event(event::Event *event) = 0; -#endif -#ifdef USE_UPDATE - virtual bool on_update(update::UpdateEntity *update) = 0; #endif virtual bool on_end(); @@ -111,80 +52,19 @@ class ComponentIterator { enum class IteratorState : uint8_t { NONE = 0, BEGIN, -#ifdef USE_BINARY_SENSOR - BINARY_SENSOR, -#endif -#ifdef USE_COVER - COVER, -#endif -#ifdef USE_FAN - FAN, -#endif -#ifdef USE_LIGHT - LIGHT, -#endif -#ifdef USE_SENSOR - SENSOR, -#endif -#ifdef USE_SWITCH - SWITCH, -#endif -#ifdef USE_BUTTON - BUTTON, -#endif -#ifdef USE_TEXT_SENSOR - TEXT_SENSOR, -#endif +// Entity iterator states (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) upper, +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) upper, +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ +// NOLINTEND(bugprone-macro-parentheses) #ifdef USE_API_USER_DEFINED_ACTIONS SERVICE, #endif #ifdef USE_CAMERA CAMERA, -#endif -#ifdef USE_CLIMATE - CLIMATE, -#endif -#ifdef USE_NUMBER - NUMBER, -#endif -#ifdef USE_DATETIME_DATE - DATETIME_DATE, -#endif -#ifdef USE_DATETIME_TIME - DATETIME_TIME, -#endif -#ifdef USE_DATETIME_DATETIME - DATETIME_DATETIME, -#endif -#ifdef USE_TEXT - TEXT, -#endif -#ifdef USE_SELECT - SELECT, -#endif -#ifdef USE_LOCK - LOCK, -#endif -#ifdef USE_VALVE - VALVE, -#endif -#ifdef USE_MEDIA_PLAYER - MEDIA_PLAYER, -#endif -#ifdef USE_ALARM_CONTROL_PANEL - ALARM_CONTROL_PANEL, -#endif -#ifdef USE_WATER_HEATER - WATER_HEATER, -#endif -#ifdef USE_INFRARED - INFRARED, -#endif -#ifdef USE_EVENT - EVENT, -#endif -#ifdef USE_UPDATE - UPDATE, #endif MAX, }; diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 632b46c8937..09975b465f8 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -1,140 +1,19 @@ #pragma once -#include "esphome/core/defines.h" -#ifdef USE_BINARY_SENSOR -#include "esphome/components/binary_sensor/binary_sensor.h" -#endif -#ifdef USE_FAN -#include "esphome/components/fan/fan.h" -#endif -#ifdef USE_LIGHT -#include "esphome/components/light/light_state.h" -#endif -#ifdef USE_COVER -#include "esphome/components/cover/cover.h" -#endif -#ifdef USE_SENSOR -#include "esphome/components/sensor/sensor.h" -#endif -#ifdef USE_TEXT_SENSOR -#include "esphome/components/text_sensor/text_sensor.h" -#endif -#ifdef USE_SWITCH -#include "esphome/components/switch/switch.h" -#endif -#ifdef USE_BUTTON -#include "esphome/components/button/button.h" -#endif -#ifdef USE_CLIMATE -#include "esphome/components/climate/climate.h" -#endif -#ifdef USE_NUMBER -#include "esphome/components/number/number.h" -#endif -#ifdef USE_DATETIME_DATE -#include "esphome/components/datetime/date_entity.h" -#endif -#ifdef USE_DATETIME_TIME -#include "esphome/components/datetime/time_entity.h" -#endif -#ifdef USE_DATETIME_DATETIME -#include "esphome/components/datetime/datetime_entity.h" -#endif -#ifdef USE_TEXT -#include "esphome/components/text/text.h" -#endif -#ifdef USE_SELECT -#include "esphome/components/select/select.h" -#endif -#ifdef USE_LOCK -#include "esphome/components/lock/lock.h" -#endif -#ifdef USE_VALVE -#include "esphome/components/valve/valve.h" -#endif -#ifdef USE_MEDIA_PLAYER -#include "esphome/components/media_player/media_player.h" -#endif -#ifdef USE_ALARM_CONTROL_PANEL -#include "esphome/components/alarm_control_panel/alarm_control_panel.h" -#endif -#ifdef USE_WATER_HEATER -#include "esphome/components/water_heater/water_heater.h" -#endif -#ifdef USE_EVENT -#include "esphome/components/event/event.h" -#endif -#ifdef USE_UPDATE -#include "esphome/components/update/update_entity.h" -#endif +#include "esphome/core/entity_includes.h" namespace esphome { class Controller { public: -#ifdef USE_BINARY_SENSOR - virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; -#endif -#ifdef USE_FAN - virtual void on_fan_update(fan::Fan *obj){}; -#endif -#ifdef USE_LIGHT - virtual void on_light_update(light::LightState *obj){}; -#endif -#ifdef USE_SENSOR - virtual void on_sensor_update(sensor::Sensor *obj){}; -#endif -#ifdef USE_SWITCH - virtual void on_switch_update(switch_::Switch *obj){}; -#endif -#ifdef USE_COVER - virtual void on_cover_update(cover::Cover *obj){}; -#endif -#ifdef USE_TEXT_SENSOR - virtual void on_text_sensor_update(text_sensor::TextSensor *obj){}; -#endif -#ifdef USE_CLIMATE - virtual void on_climate_update(climate::Climate *obj){}; -#endif -#ifdef USE_NUMBER - virtual void on_number_update(number::Number *obj){}; -#endif -#ifdef USE_DATETIME_DATE - virtual void on_date_update(datetime::DateEntity *obj){}; -#endif -#ifdef USE_DATETIME_TIME - virtual void on_time_update(datetime::TimeEntity *obj){}; -#endif -#ifdef USE_DATETIME_DATETIME - virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; -#endif -#ifdef USE_TEXT - virtual void on_text_update(text::Text *obj){}; -#endif -#ifdef USE_SELECT - virtual void on_select_update(select::Select *obj){}; -#endif -#ifdef USE_LOCK - virtual void on_lock_update(lock::Lock *obj){}; -#endif -#ifdef USE_VALVE - virtual void on_valve_update(valve::Valve *obj){}; -#endif -#ifdef USE_MEDIA_PLAYER - virtual void on_media_player_update(media_player::MediaPlayer *obj){}; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; -#endif -#ifdef USE_WATER_HEATER - virtual void on_water_heater_update(water_heater::WaterHeater *obj){}; -#endif -#ifdef USE_EVENT - virtual void on_event(event::Event *obj){}; -#endif -#ifdef USE_UPDATE - virtual void on_update(update::UpdateEntity *obj){}; -#endif +// Controller virtual methods (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) virtual void on_##callback(type *obj){}; +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) }; } // namespace esphome diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 92f23f56427..907e0f923d6 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -6,8 +6,6 @@ namespace esphome { StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers; -void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } - } // namespace esphome #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 846642da292..c6113116ffd 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -4,139 +4,13 @@ #ifdef USE_CONTROLLER_REGISTRY +#include "esphome/core/entity_includes.h" #include "esphome/core/helpers.h" -// Forward declarations namespace esphome { class Controller; -#ifdef USE_BINARY_SENSOR -namespace binary_sensor { -class BinarySensor; -} -#endif - -#ifdef USE_FAN -namespace fan { -class Fan; -} -#endif - -#ifdef USE_LIGHT -namespace light { -class LightState; -} -#endif - -#ifdef USE_SENSOR -namespace sensor { -class Sensor; -} -#endif - -#ifdef USE_SWITCH -namespace switch_ { -class Switch; -} -#endif - -#ifdef USE_COVER -namespace cover { -class Cover; -} -#endif - -#ifdef USE_TEXT_SENSOR -namespace text_sensor { -class TextSensor; -} -#endif - -#ifdef USE_CLIMATE -namespace climate { -class Climate; -} -#endif - -#ifdef USE_NUMBER -namespace number { -class Number; -} -#endif - -#ifdef USE_DATETIME_DATE -namespace datetime { -class DateEntity; -} -#endif - -#ifdef USE_DATETIME_TIME -namespace datetime { -class TimeEntity; -} -#endif - -#ifdef USE_DATETIME_DATETIME -namespace datetime { -class DateTimeEntity; -} -#endif - -#ifdef USE_TEXT -namespace text { -class Text; -} -#endif - -#ifdef USE_SELECT -namespace select { -class Select; -} -#endif - -#ifdef USE_LOCK -namespace lock { -class Lock; -} -#endif - -#ifdef USE_VALVE -namespace valve { -class Valve; -} -#endif - -#ifdef USE_MEDIA_PLAYER -namespace media_player { -class MediaPlayer; -} -#endif - -#ifdef USE_ALARM_CONTROL_PANEL -namespace alarm_control_panel { -class AlarmControlPanel; -} -#endif - -#ifdef USE_WATER_HEATER -namespace water_heater { -class WaterHeater; -} -#endif - -#ifdef USE_EVENT -namespace event { -class Event; -} -#endif - -#ifdef USE_UPDATE -namespace update { -class UpdateEntity; -} -#endif - /** Global registry for Controllers to receive entity state updates. * * This singleton registry allows Controllers (APIServer, WebServer) to receive @@ -160,91 +34,17 @@ class ControllerRegistry { * Controllers should call this in their setup() method. * Typically only APIServer and WebServer register. */ - static void register_controller(Controller *controller); + static void register_controller(Controller *controller) { controllers.push_back(controller); } -#ifdef USE_BINARY_SENSOR - static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); -#endif - -#ifdef USE_FAN - static void notify_fan_update(fan::Fan *obj); -#endif - -#ifdef USE_LIGHT - static void notify_light_update(light::LightState *obj); -#endif - -#ifdef USE_SENSOR - static void notify_sensor_update(sensor::Sensor *obj); -#endif - -#ifdef USE_SWITCH - static void notify_switch_update(switch_::Switch *obj); -#endif - -#ifdef USE_COVER - static void notify_cover_update(cover::Cover *obj); -#endif - -#ifdef USE_TEXT_SENSOR - static void notify_text_sensor_update(text_sensor::TextSensor *obj); -#endif - -#ifdef USE_CLIMATE - static void notify_climate_update(climate::Climate *obj); -#endif - -#ifdef USE_NUMBER - static void notify_number_update(number::Number *obj); -#endif - -#ifdef USE_DATETIME_DATE - static void notify_date_update(datetime::DateEntity *obj); -#endif - -#ifdef USE_DATETIME_TIME - static void notify_time_update(datetime::TimeEntity *obj); -#endif - -#ifdef USE_DATETIME_DATETIME - static void notify_datetime_update(datetime::DateTimeEntity *obj); -#endif - -#ifdef USE_TEXT - static void notify_text_update(text::Text *obj); -#endif - -#ifdef USE_SELECT - static void notify_select_update(select::Select *obj); -#endif - -#ifdef USE_LOCK - static void notify_lock_update(lock::Lock *obj); -#endif - -#ifdef USE_VALVE - static void notify_valve_update(valve::Valve *obj); -#endif - -#ifdef USE_MEDIA_PLAYER - static void notify_media_player_update(media_player::MediaPlayer *obj); -#endif - -#ifdef USE_ALARM_CONTROL_PANEL - static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); -#endif - -#ifdef USE_WATER_HEATER - static void notify_water_heater_update(water_heater::WaterHeater *obj); -#endif - -#ifdef USE_EVENT - static void notify_event(event::Event *obj); -#endif - -#ifdef USE_UPDATE - static void notify_update(update::UpdateEntity *obj); -#endif +// Notify method declarations (generated from entity_types.h) +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + static void notify_##callback(type *obj); +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) protected: static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers; @@ -265,108 +65,18 @@ namespace esphome { // notify_frontend_(), eliminating an unnecessary function-call frame. // NOLINTBEGIN(bugprone-macro-parentheses) -#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ - inline void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ +#define ENTITY_TYPE_(type, singular, plural, count, upper) // no controller callback +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + inline void ControllerRegistry::notify_##callback(type *obj) { \ for (auto *controller : controllers) { \ - controller->on_##entity_name##_update(obj); \ - } \ - } - -#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ - inline void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##entity_name(obj); \ + controller->on_##callback(obj); \ } \ } +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ // NOLINTEND(bugprone-macro-parentheses) -#ifdef USE_BINARY_SENSOR -CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) -#endif - -#ifdef USE_FAN -CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) -#endif - -#ifdef USE_LIGHT -CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) -#endif - -#ifdef USE_SENSOR -CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) -#endif - -#ifdef USE_SWITCH -CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) -#endif - -#ifdef USE_COVER -CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) -#endif - -#ifdef USE_TEXT_SENSOR -CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) -#endif - -#ifdef USE_CLIMATE -CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) -#endif - -#ifdef USE_NUMBER -CONTROLLER_REGISTRY_NOTIFY(number::Number, number) -#endif - -#ifdef USE_DATETIME_DATE -CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) -#endif - -#ifdef USE_DATETIME_TIME -CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) -#endif - -#ifdef USE_DATETIME_DATETIME -CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) -#endif - -#ifdef USE_TEXT -CONTROLLER_REGISTRY_NOTIFY(text::Text, text) -#endif - -#ifdef USE_SELECT -CONTROLLER_REGISTRY_NOTIFY(select::Select, select) -#endif - -#ifdef USE_LOCK -CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) -#endif - -#ifdef USE_VALVE -CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) -#endif - -#ifdef USE_MEDIA_PLAYER -CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) -#endif - -#ifdef USE_ALARM_CONTROL_PANEL -CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) -#endif - -#ifdef USE_WATER_HEATER -CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) -#endif - -#ifdef USE_EVENT -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) -#endif - -#ifdef USE_UPDATE -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) -#endif - -#undef CONTROLLER_REGISTRY_NOTIFY -#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX - } // namespace esphome #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/entity_includes.h b/esphome/core/entity_includes.h new file mode 100644 index 00000000000..f67887b30be --- /dev/null +++ b/esphome/core/entity_includes.h @@ -0,0 +1,79 @@ +#pragma once + +// Shared entity component includes. +// Conditionally includes headers for all entity types based on USE_* defines. + +#include "esphome/core/defines.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_COVER +#include "esphome/components/cover/cover.h" +#endif +#ifdef USE_FAN +#include "esphome/components/fan/fan.h" +#endif +#ifdef USE_LIGHT +#include "esphome/components/light/light_state.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_DATETIME_DATE +#include "esphome/components/datetime/date_entity.h" +#endif +#ifdef USE_DATETIME_TIME +#include "esphome/components/datetime/time_entity.h" +#endif +#ifdef USE_DATETIME_DATETIME +#include "esphome/components/datetime/datetime_entity.h" +#endif +#ifdef USE_TEXT +#include "esphome/components/text/text.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_LOCK +#include "esphome/components/lock/lock.h" +#endif +#ifdef USE_VALVE +#include "esphome/components/valve/valve.h" +#endif +#ifdef USE_MEDIA_PLAYER +#include "esphome/components/media_player/media_player.h" +#endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif +#ifdef USE_SERIAL_PROXY +#include "esphome/components/serial_proxy/serial_proxy.h" +#endif +#ifdef USE_EVENT +#include "esphome/components/event/event.h" +#endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif diff --git a/esphome/core/entity_types.h b/esphome/core/entity_types.h new file mode 100644 index 00000000000..04b490e10ea --- /dev/null +++ b/esphome/core/entity_types.h @@ -0,0 +1,98 @@ +// X-macro include file for entity type declarations. +// This file is included multiple times with different macro definitions. +// +// Both macros must be defined before including this file: +// +// ENTITY_TYPE_(type, singular, plural, count, upper) +// — entities without controller callbacks (button, infrared) +// +// ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) +// — entities with controller callbacks +// +// Excluded from this list (handled manually): +// - devices/areas: not entities +// - serial_proxy: custom register logic, no by-key lookup + +#ifndef ENTITY_TYPE_ +#error "ENTITY_TYPE_(type, singular, plural, count, upper) must be defined before including entity_types.h" +#endif +#ifndef ENTITY_CONTROLLER_TYPE_ +#error \ + "ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) must be defined before including entity_types.h" +#endif + +#ifdef USE_BINARY_SENSOR +ENTITY_CONTROLLER_TYPE_(binary_sensor::BinarySensor, binary_sensor, binary_sensors, ESPHOME_ENTITY_BINARY_SENSOR_COUNT, + BINARY_SENSOR, binary_sensor_update) +#endif +#ifdef USE_COVER +ENTITY_CONTROLLER_TYPE_(cover::Cover, cover, covers, ESPHOME_ENTITY_COVER_COUNT, COVER, cover_update) +#endif +#ifdef USE_FAN +ENTITY_CONTROLLER_TYPE_(fan::Fan, fan, fans, ESPHOME_ENTITY_FAN_COUNT, FAN, fan_update) +#endif +#ifdef USE_LIGHT +ENTITY_CONTROLLER_TYPE_(light::LightState, light, lights, ESPHOME_ENTITY_LIGHT_COUNT, LIGHT, light_update) +#endif +#ifdef USE_SENSOR +ENTITY_CONTROLLER_TYPE_(sensor::Sensor, sensor, sensors, ESPHOME_ENTITY_SENSOR_COUNT, SENSOR, sensor_update) +#endif +#ifdef USE_SWITCH +ENTITY_CONTROLLER_TYPE_(switch_::Switch, switch, switches, ESPHOME_ENTITY_SWITCH_COUNT, SWITCH, switch_update) +#endif +#ifdef USE_BUTTON +ENTITY_TYPE_(button::Button, button, buttons, ESPHOME_ENTITY_BUTTON_COUNT, BUTTON) +#endif +#ifdef USE_TEXT_SENSOR +ENTITY_CONTROLLER_TYPE_(text_sensor::TextSensor, text_sensor, text_sensors, ESPHOME_ENTITY_TEXT_SENSOR_COUNT, + TEXT_SENSOR, text_sensor_update) +#endif +#ifdef USE_CLIMATE +ENTITY_CONTROLLER_TYPE_(climate::Climate, climate, climates, ESPHOME_ENTITY_CLIMATE_COUNT, CLIMATE, climate_update) +#endif +#ifdef USE_NUMBER +ENTITY_CONTROLLER_TYPE_(number::Number, number, numbers, ESPHOME_ENTITY_NUMBER_COUNT, NUMBER, number_update) +#endif +#ifdef USE_DATETIME_DATE +ENTITY_CONTROLLER_TYPE_(datetime::DateEntity, date, dates, ESPHOME_ENTITY_DATE_COUNT, DATETIME_DATE, date_update) +#endif +#ifdef USE_DATETIME_TIME +ENTITY_CONTROLLER_TYPE_(datetime::TimeEntity, time, times, ESPHOME_ENTITY_TIME_COUNT, DATETIME_TIME, time_update) +#endif +#ifdef USE_DATETIME_DATETIME +ENTITY_CONTROLLER_TYPE_(datetime::DateTimeEntity, datetime, datetimes, ESPHOME_ENTITY_DATETIME_COUNT, DATETIME_DATETIME, + datetime_update) +#endif +#ifdef USE_TEXT +ENTITY_CONTROLLER_TYPE_(text::Text, text, texts, ESPHOME_ENTITY_TEXT_COUNT, TEXT, text_update) +#endif +#ifdef USE_SELECT +ENTITY_CONTROLLER_TYPE_(select::Select, select, selects, ESPHOME_ENTITY_SELECT_COUNT, SELECT, select_update) +#endif +#ifdef USE_LOCK +ENTITY_CONTROLLER_TYPE_(lock::Lock, lock, locks, ESPHOME_ENTITY_LOCK_COUNT, LOCK, lock_update) +#endif +#ifdef USE_VALVE +ENTITY_CONTROLLER_TYPE_(valve::Valve, valve, valves, ESPHOME_ENTITY_VALVE_COUNT, VALVE, valve_update) +#endif +#ifdef USE_MEDIA_PLAYER +ENTITY_CONTROLLER_TYPE_(media_player::MediaPlayer, media_player, media_players, ESPHOME_ENTITY_MEDIA_PLAYER_COUNT, + MEDIA_PLAYER, media_player_update) +#endif +#ifdef USE_ALARM_CONTROL_PANEL +ENTITY_CONTROLLER_TYPE_(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels, + ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT, ALARM_CONTROL_PANEL, alarm_control_panel_update) +#endif +#ifdef USE_WATER_HEATER +ENTITY_CONTROLLER_TYPE_(water_heater::WaterHeater, water_heater, water_heaters, ESPHOME_ENTITY_WATER_HEATER_COUNT, + WATER_HEATER, water_heater_update) +#endif +#ifdef USE_INFRARED +ENTITY_TYPE_(infrared::Infrared, infrared, infrareds, ESPHOME_ENTITY_INFRARED_COUNT, INFRARED) +#endif +#ifdef USE_EVENT +ENTITY_CONTROLLER_TYPE_(event::Event, event, events, ESPHOME_ENTITY_EVENT_COUNT, EVENT, event) +#endif +#ifdef USE_UPDATE +ENTITY_CONTROLLER_TYPE_(update::UpdateEntity, update, updates, ESPHOME_ENTITY_UPDATE_COUNT, UPDATE, update) +#endif diff --git a/esphome/writer.py b/esphome/writer.py index 06a22301184..787ecac6f6e 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -171,6 +171,7 @@ VERSION_H_FORMAT = """\ DEFINES_H_TARGET = "esphome/core/defines.h" VERSION_H_TARGET = "esphome/core/version.h" BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" +ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -196,9 +197,12 @@ def copy_src_tree(): source_files_l.sort() # Build #include list for esphome.h + # X-macro files are included multiple times with different macro definitions + # and must not be included bare in esphome.h + esphome_h_exclude = {Path(ENTITY_TYPES_H_TARGET)} include_l = [] for target, _ in source_files_l: - if target.suffix in HEADER_FILE_EXTENSIONS: + if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude: include_l.append(f'#include "{target}"') include_l.append("") include_s = "\n".join(include_l) diff --git a/script/ci-custom.py b/script/ci-custom.py index 1ec3eab3a94..6dce86924e0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -672,7 +672,7 @@ def lint_using_esp_idf_deprecated(fname, line, col, content): ) -@lint_content_check(include=["*.h"]) +@lint_content_check(include=["*.h"], exclude=["esphome/core/entity_types.h"]) def lint_pragma_once(fname, content): if "#pragma once" not in content: return ( diff --git a/script/helpers.py b/script/helpers.py index c9c550d8893..9f5ea7894cd 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -174,7 +174,12 @@ def build_all_include(header_files: list[str] | None = None) -> None: if line ] - headers = [f'#include "{h}"' for h in header_files] + from esphome.writer import ENTITY_TYPES_H_TARGET + + # X-macro files are included multiple times with different macro definitions + # and must not be included bare in the all-include header + exclude = {ENTITY_TYPES_H_TARGET} + headers = [f'#include "{h}"' for h in header_files if h not in exclude] headers.sort() headers.append("") content = "\n".join(headers) From e0118dd8ebb8ed9dae2002dd1243ef539f5c82aa Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:19:42 +1000 Subject: [PATCH 1468/2056] [lvgl] Clean the build if lv_conf.h changes (#15777) --- esphome/components/lvgl/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b6421dc43d7..ac0363ca696 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -44,6 +44,7 @@ from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed +from esphome.writer import clean_build from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets @@ -451,7 +452,8 @@ async def to_code(configs): df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) - write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) + if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()): + clean_build(clear_pio_cache=False) cg.add_build_flag("-DLV_CONF_H=1") # handle windows paths in a way that doesn't break the generated C++ lv_conf_h_path = Path(lv_conf_h_file).as_posix() From b40ffacb8db789afaab8cc5be537c2f51638a06a Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Thu, 16 Apr 2026 15:35:24 +0200 Subject: [PATCH 1469/2056] [mitsubishi_cn105] use HEAT_COOL mode to enable temperature slider (#15748) --- .../mitsubishi_cn105_climate.cpp | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 40ddb88a795..284339e57f9 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 { static const char *const TAG = "mitsubishi_cn105.climate"; static constexpr std::array MODE_MAP{ - std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO}, + std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL}, std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT}, std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY}, std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL}, @@ -76,23 +76,13 @@ void MitsubishiCN105Climate::loop() { climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; - traits.set_supported_modes({ - climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_FAN_ONLY, - climate::CLIMATE_MODE_AUTO, - }); + for (const auto &p : MODE_MAP) { + traits.add_supported_mode(p.second); + } - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_QUIET, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_MIDDLE, - climate::CLIMATE_FAN_HIGH, - }); + for (const auto &p : FAN_MODE_MAP) { + traits.add_supported_fan_mode(p.second); + } traits.set_visual_min_temperature(16.0f); traits.set_visual_max_temperature(31.0f); From c8e21802db45fd78c1909adbb4850e73881c8839 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 03:36:55 -1000 Subject: [PATCH 1470/2056] [core] Diagnose missing cg.templatable in codegen for TEMPLATABLE_VALUE fields (#15758) --- esphome/core/automation.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index eb270bfee26..468ea3b382c 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -62,6 +62,18 @@ template<typename T, typename... X> class TemplatableFn { !std::convertible_to<std::invoke_result_t<F, X...>, T> || !std::default_initializable<F>) = delete; + // Reject raw (non-callable) values with a helpful diagnostic pointing at the Python-side fix. + // TemplatableFn stores only a function pointer (4 bytes), so constants must be wrapped in a + // stateless lambda by codegen. External components hitting this error should use + // `cg.templatable(value, args, type)` in their Python __init__.py before passing to the setter. + template<typename V> TemplatableFn(V) requires(!std::invocable<V, X...>) && (!std::convertible_to<V, T (*)(X...)>) { + static_assert(sizeof(V) == 0, "Missing cg.templatable(...) in Python codegen for this TEMPLATABLE_VALUE " + "field. The wrapper was always required; it worked by accident because the old " + "TemplatableValue implicitly converted raw constants. TemplatableFn cannot. See " + "https://developers.esphome.io/blog/2026/04/09/" + "templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/"); + } + bool has_value() const { return this->f_ != nullptr; } T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; } From 4c758fa1da1b1353b98c85a0394a46ee510f3541 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 03:40:22 -1000 Subject: [PATCH 1471/2056] [time] Fix RTC is_valid() rejecting valid times after day_of_year cleanup (#15763) --- esphome/components/bm8563/bm8563.cpp | 2 +- esphome/components/ds1307/ds1307.cpp | 2 +- esphome/components/pcf85063/pcf85063.cpp | 2 +- esphome/components/pcf8563/pcf8563.cpp | 2 +- esphome/components/rx8130/rx8130.cpp | 2 +- esphome/core/time.h | 8 ++- tests/components/time/is_valid.cpp | 72 ++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/components/time/is_valid.cpp diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 062094c036f..d911301c9d3 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -63,7 +63,7 @@ void BM8563::read_time() { rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 8fff4213b4c..ba2ad6032f4 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -44,7 +44,7 @@ void DS1307Component::read_time() { .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 1cf28a49557..000de1433cb 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -44,7 +44,7 @@ void PCF85063Component::read_time() { .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index b748f0156aa..50003ca378b 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -44,7 +44,7 @@ void PCF8563Component::read_time() { .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 3b704d2551f..0aa6e86d31e 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -81,7 +81,7 @@ void RX8130Component::read_time() { .year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index ed474320381..0b67b7b3fcd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -76,8 +76,12 @@ struct ESPTime { /// @copydoc strftime(const std::string &format) std::string strftime(const char *format); - /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) - bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } + /// Check if this ESPTime is valid (year >= 2019 and the requested fields are in range). + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool is_valid(bool check_day_of_week = true, bool check_day_of_year = true) const { + return this->year >= 2019 && this->fields_in_range(check_day_of_week, check_day_of_year); + } /// Check if time fields are in range. /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) diff --git a/tests/components/time/is_valid.cpp b/tests/components/time/is_valid.cpp new file mode 100644 index 00000000000..9148c0e8d6b --- /dev/null +++ b/tests/components/time/is_valid.cpp @@ -0,0 +1,72 @@ +// Regression tests for ESPTime::is_valid() optional checks. +// +// The RTC components (ds1307, bm8563, pcf85063, pcf8563, rx8130) read date/time +// fields from hardware but do not populate day_of_year. They call +// recalc_timestamp_utc(false) -- which skips day_of_year -- and then is_valid(). +// These tests ensure the is_valid() overload can skip day_of_year validation so +// RTCs don't log "Invalid RTC time, not syncing to system clock." for valid times. + +#include <gtest/gtest.h> +#include "esphome/core/time.h" + +namespace esphome::testing { + +// Build an ESPTime that mirrors what the RTC components construct: all fields +// populated from hardware except day_of_year (left zero-initialized). +static ESPTime make_rtc_like_time() { + ESPTime t{}; + t.second = 30; + t.minute = 15; + t.hour = 12; + t.day_of_week = 4; // thursday + t.day_of_month = 15; + t.month = 4; + t.year = 2026; + // day_of_year intentionally left at 0 -- RTCs don't compute it. + return t; +} + +TEST(ESPTimeIsValid, DefaultRejectsZeroDayOfYear) { + // Default is_valid() checks day_of_year; zero-init is out of range. + ESPTime t = make_rtc_like_time(); + EXPECT_FALSE(t.is_valid()); +} + +TEST(ESPTimeIsValid, SkipDayOfYearAcceptsRTCLikeTime) { + // RTC code path: skip day_of_year validation. + ESPTime t = make_rtc_like_time(); + EXPECT_TRUE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipDayOfYearStillRejectsOutOfRangeFields) { + ESPTime t = make_rtc_like_time(); + t.hour = 25; + EXPECT_FALSE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipDayOfYearStillRejectsYearBefore2019) { + ESPTime t = make_rtc_like_time(); + t.year = 2000; + EXPECT_FALSE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipBothDayChecksAcceptsGPSLikeTime) { + // GPS path (gps_time.cpp) populates neither day_of_week nor day_of_year. + ESPTime t{}; + t.second = 30; + t.minute = 15; + t.hour = 12; + t.day_of_month = 15; + t.month = 4; + t.year = 2026; + EXPECT_TRUE(t.is_valid(/*check_day_of_week=*/false, /*check_day_of_year=*/false)); + EXPECT_FALSE(t.is_valid()); // default still rejects +} + +TEST(ESPTimeIsValid, FullyPopulatedAcceptsWithDefaults) { + ESPTime t = make_rtc_like_time(); + t.day_of_year = 105; + EXPECT_TRUE(t.is_valid()); +} + +} // namespace esphome::testing From 04a58159d0fa8462d4150cba30e086f9119869d6 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Thu, 16 Apr 2026 15:43:03 +0200 Subject: [PATCH 1472/2056] [zephyr_ble_server] add support for on_numeric_comparison_request (#14400) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/zephyr_ble_server/__init__.py | 61 +++- .../zephyr_ble_server/ble_server.cpp | 295 +++++++++++++++--- .../components/zephyr_ble_server/ble_server.h | 26 +- .../test.nrf52-xiao-ble.yaml | 10 + 4 files changed, 341 insertions(+), 51 deletions(-) create mode 100644 tests/components/zephyr_ble_server/test.nrf52-xiao-ble.yaml diff --git a/esphome/components/zephyr_ble_server/__init__.py b/esphome/components/zephyr_ble_server/__init__.py index 211941e9846..658137d1a22 100644 --- a/esphome/components/zephyr_ble_server/__init__.py +++ b/esphome/components/zephyr_ble_server/__init__.py @@ -1,28 +1,35 @@ +from esphome import automation import esphome.codegen as cg from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework -import esphome.final_validate as fv +from esphome.const import CONF_ID, Framework +from esphome.core import CORE zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server") BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component) +CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" +CONF_ACCEPT = "accept" + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), + cv.Optional( + CONF_ON_NUMERIC_COMPARISON_REQUEST + ): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_framework(Framework.ZEPHYR), ) - -def _final_validate(_): - full_config = fv.full_config.get() - zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME]) - - -FINAL_VALIDATE_SCHEMA = _final_validate +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_NUMERIC_COMPARISON_REQUEST, + "add_passkey_callback", + [(cg.uint32, "passkey")], + ), +) async def to_code(config): @@ -30,5 +37,39 @@ async def to_code(config): zephyr_add_prj_conf("BT", True) zephyr_add_prj_conf("BT_PERIPHERAL", True) zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536) - # zephyr_add_prj_conf("BT_LL_SW_SPLIT", True) + zephyr_add_prj_conf("BT_DEVICE_NAME", CORE.name) await cg.register_component(var, config) + if config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST): + zephyr_add_prj_conf("BT_SMP", True) + zephyr_add_prj_conf("BT_SETTINGS", True) + zephyr_add_prj_conf("BT_SMP_SC_ONLY", True) + zephyr_add_prj_conf("BT_KEYS_OVERWRITE_OLDEST", True) + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + + +BLENumericComparisonReplyAction = zephyr_ble_server_ns.class_( + "BLENumericComparisonReplyAction", automation.Action +) + +BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEServer), + cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "ble_server.numeric_comparison_reply", + BLENumericComparisonReplyAction, + BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, + synchronous=True, +) +async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + templ = await cg.templatable(config[CONF_ACCEPT], args, cg.bool_) + cg.add(var.set_accept(templ)) + + return var diff --git a/esphome/components/zephyr_ble_server/ble_server.cpp b/esphome/components/zephyr_ble_server/ble_server.cpp index 9f7e606a90c..15993abcce4 100644 --- a/esphome/components/zephyr_ble_server/ble_server.cpp +++ b/esphome/components/zephyr_ble_server/ble_server.cpp @@ -3,32 +3,34 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" #include <zephyr/bluetooth/bluetooth.h> -#include <zephyr/bluetooth/conn.h> +#include <zephyr/settings/settings.h> namespace esphome::zephyr_ble_server { static const char *const TAG = "zephyr_ble_server"; -static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +BLEServer *global_ble_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #define DEVICE_NAME CONFIG_BT_DEVICE_NAME #define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1) -static const struct bt_data AD[] = { +static const bt_data AD[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), }; -static const struct bt_data SD[] = { +static const bt_data SD[] = { #ifdef USE_OTA BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d, 0xdc, 0x53, 0x8d), #endif }; -const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN; +const bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN; -static void advertise(struct k_work *work) { +static void advertise(k_work *work) { int rc = bt_le_adv_stop(); if (rc) { ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc); @@ -42,57 +44,276 @@ static void advertise(struct k_work *work) { ESP_LOGI(TAG, "Advertising successfully started"); } -static void connected(struct bt_conn *conn, uint8_t err) { +void BLEServer::connected(bt_conn *conn, uint8_t err) { + char addr[BT_ADDR_LE_STR_LEN]; + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); if (err) { - ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err); - } else { - ESP_LOGI(TAG, "Connected"); + ESP_LOGE(TAG, "Failed to connect to %s (%u)", addr, err); + return; } + ESP_LOGI(TAG, "Connected %s", addr); +#ifdef CONFIG_BT_SMP + if (bt_conn_set_security(conn, BT_SECURITY_L4)) { + ESP_LOGE(TAG, "Failed to set security"); + } +#endif + conn = bt_conn_ref(conn); + global_ble_server->defer([conn]() { global_ble_server->conn_ = conn; }); } -static void disconnected(struct bt_conn *conn, uint8_t reason) { - ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason); +void BLEServer::disconnected(bt_conn *conn, uint8_t reason) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + ESP_LOGI(TAG, "Disconnected from %s (reason 0x%02x)", addr, reason); + global_ble_server->defer([]() { + if (global_ble_server->conn_) { + bt_conn_unref(global_ble_server->conn_); + global_ble_server->conn_ = nullptr; + } + }); k_work_submit(&advertise_work); } -static void bt_ready(int err) { - if (err != 0) { - ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err); +#ifdef CONFIG_BT_SMP +static void identity_resolved(bt_conn *conn, const bt_addr_le_t *rpa, const bt_addr_le_t *identity) { + char addr_identity[BT_ADDR_LE_STR_LEN]; + char addr_rpa[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(identity, addr_identity, sizeof(addr_identity)); + bt_addr_le_to_str(rpa, addr_rpa, sizeof(addr_rpa)); + + ESP_LOGD(TAG, "Identity resolved %s -> %s", addr_rpa, addr_identity); +} + +static void security_changed(bt_conn *conn, bt_security_t level, bt_security_err err) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (!err) { + ESP_LOGD(TAG, "Security changed: %s level %u", addr, level); } else { - k_work_submit(&advertise_work); + ESP_LOGE(TAG, "Security failed: %s level %u err %d", addr, level, err); } } -BT_CONN_CB_DEFINE(conn_callbacks) = { - .connected = connected, - .disconnected = disconnected, -}; +static void pairing_complete(bt_conn *conn, bool bonded) { + char addr[BT_ADDR_LE_STR_LEN]; -void BLEServer::setup() { - k_work_init(&advertise_work, advertise); - resume_(); + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + ESP_LOGD(TAG, "Pairing completed: %s, bonded: %d", addr, bonded); } -void BLEServer::loop() { - if (this->suspended_) { - resume_(); - this->suspended_ = false; - } +static void pairing_failed(bt_conn *conn, bt_security_err reason) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + ESP_LOGE(TAG, "Pairing failed conn: %s, reason %d", addr, reason); + + bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); } -void BLEServer::resume_() { - int rc = bt_enable(bt_ready); - if (rc != 0) { - ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc); +static void bond_deleted(uint8_t id, const bt_addr_le_t *peer) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(peer, addr, sizeof(addr)); + ESP_LOGD(TAG, "Bond deleted for %s, id %u", addr, id); +} + +static void auth_passkey_display(bt_conn *conn, unsigned int passkey) { + char addr[BT_ADDR_LE_STR_LEN]; + char passkey_str[7]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + snprintk(passkey_str, 7, "%06u", passkey); + + ESP_LOGI(TAG, "Passkey for %s: %s", addr, passkey_str); +} + +static void conn_addr_str(bt_conn *conn, char *addr, size_t len) { + struct bt_conn_info info; + + if (bt_conn_get_info(conn, &info) < 0) { + addr[0] = '\0'; return; } + + switch (info.type) { + case BT_CONN_TYPE_LE: + bt_addr_le_to_str(info.le.dst, addr, len); + break; + default: + ESP_LOGE(TAG, "Not implemented"); + addr[0] = '\0'; + break; + } } -void BLEServer::on_shutdown() { - struct k_work_sync sync; - k_work_cancel_sync(&advertise_work, &sync); - bt_disable(); - this->suspended_ = true; +static void auth_cancel(bt_conn *conn) { + char addr[BT_ADDR_LE_STR_LEN]; + + conn_addr_str(conn, addr, sizeof(addr)); + + ESP_LOGI(TAG, "Pairing cancelled: %s", addr); +} + +void BLEServer::auth_passkey_confirm(bt_conn *conn, unsigned int passkey) { + char addr[BT_ADDR_LE_STR_LEN]; + char passkey_str[7]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + snprintk(passkey_str, 7, "%06u", passkey); + + ESP_LOGI(TAG, "Confirm passkey for %s: %s", addr, passkey_str); + global_ble_server->defer([passkey]() { global_ble_server->passkey_cb_(passkey); }); +} + +static void auth_pairing_confirm(bt_conn *conn) { + /* Automatically confirm pairing request from the device side. */ + auto err = bt_conn_auth_pairing_confirm(conn); + if (err) { + ESP_LOGE(TAG, "Can't confirm pairing (err: %d)", err); + return; + } + + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + ESP_LOGI(TAG, "Pairing confirmed: %s", addr); +} + +#endif + +void BLEServer::setup() { + global_ble_server = this; + int err = 0; + k_work_init(&advertise_work, advertise); + + static bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +#ifdef CONFIG_BT_SMP + .identity_resolved = identity_resolved, + .security_changed = security_changed, +#endif + }; + + bt_conn_cb_register(&conn_callbacks); +#ifdef CONFIG_BT_SMP + static struct bt_conn_auth_info_cb conn_auth_info_callbacks = { + .pairing_complete = pairing_complete, .pairing_failed = pairing_failed, .bond_deleted = bond_deleted}; + err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks); + if (err) { + ESP_LOGE(TAG, "Failed to register authorization info callbacks."); + } + static struct bt_conn_auth_cb auth_cb = { + .passkey_display = auth_passkey_display, + .passkey_confirm = auth_passkey_confirm, + .cancel = auth_cancel, + .pairing_confirm = auth_pairing_confirm, + }; + err = bt_conn_auth_cb_register(&auth_cb); + if (err) { + ESP_LOGE(TAG, "Failed to set auth handlers (%d)", err); + } +#endif + // callback cannot be used to start scanning due to race conditions with BT_SETTINGS + err = bt_enable(nullptr); + if (err) { + ESP_LOGE(TAG, "Bluetooth enable failed: %d", err); + return; + } +#ifdef CONFIG_BT_SETTINGS + err = settings_load(); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + } +#endif + k_work_submit(&advertise_work); +} + +#ifdef ESPHOME_LOG_HAS_DEBUG +static const char *role_str(uint8_t role) { + switch (role) { + case BT_CONN_ROLE_CENTRAL: + return "Central"; + case BT_CONN_ROLE_PERIPHERAL: + return "Peripheral"; + } + + return "Unknown"; +} + +static void connection_info(bt_conn *conn, void *user_data) { + char addr[BT_ADDR_LE_STR_LEN]; + struct bt_conn_info info; + + if (bt_conn_get_info(conn, &info) < 0) { + ESP_LOGE(TAG, "Unable to get info: conn %p", conn); + return; + } + + switch (info.type) { + case BT_CONN_TYPE_LE: + bt_addr_le_to_str(info.le.dst, addr, sizeof(addr)); + ESP_LOGD(TAG, " %u [LE][%s] %s: Interval %u latency %u timeout %u security L%u", info.id, role_str(info.role), + addr, info.le.interval, info.le.latency, info.le.timeout, info.security.level); + break; + default: + ESP_LOGE(TAG, "Not implemented"); + break; + } +} +#ifdef CONFIG_BT_BONDABLE +static void bond_info(const struct bt_bond_info *info, void *user_data) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(&info->addr, addr, sizeof(addr)); + ESP_LOGD(TAG, " Bond remote identity: %s", addr); +} +#endif +#endif + +void BLEServer::dump_config() { + ESP_LOGCONFIG(TAG, + "ble server:\n" + " connected: %s\n" + " name: %s\n" + " appearance: %u\n" + " ready: %s\n" +#ifdef CONFIG_BT_SMP + " security manager: YES", +#else + " security manager: NO", +#endif + YESNO(this->conn_), bt_get_name(), bt_get_appearance(), YESNO(bt_is_ready())); + +#ifdef ESPHOME_LOG_HAS_DEBUG + bt_conn_foreach(BT_CONN_TYPE_ALL, connection_info, nullptr); +#ifdef CONFIG_BT_BONDABLE + bt_foreach_bond(BT_ID_DEFAULT, bond_info, nullptr); +#endif +#endif +} + +void BLEServer::numeric_comparison_reply(bool accept) { + if (this->conn_ == nullptr) { + ESP_LOGE(TAG, "Not connected"); + return; + } + ESP_LOGD(TAG, "Numeric comparison %s", accept ? "accepted" : "rejected"); + if (accept) { + bt_conn_auth_passkey_confirm(this->conn_); + } else { + bt_conn_auth_cancel(this->conn_); + } } } // namespace esphome::zephyr_ble_server diff --git a/esphome/components/zephyr_ble_server/ble_server.h b/esphome/components/zephyr_ble_server/ble_server.h index 1b32e9b58ca..bf69c52b126 100644 --- a/esphome/components/zephyr_ble_server/ble_server.h +++ b/esphome/components/zephyr_ble_server/ble_server.h @@ -1,18 +1,36 @@ #pragma once #ifdef USE_ZEPHYR #include "esphome/core/component.h" +#include <zephyr/bluetooth/conn.h> +#include "esphome/core/automation.h" namespace esphome::zephyr_ble_server { class BLEServer : public Component { public: void setup() override; - void loop() override; - void on_shutdown() override; + void dump_config() override; + template<typename F> void add_passkey_callback(F &&callback) { this->passkey_cb_.add(std::forward<F>(callback)); } + void numeric_comparison_reply(bool accept); protected: - void resume_(); - bool suspended_ = false; + static void connected(bt_conn *conn, uint8_t err); + static void disconnected(bt_conn *conn, uint8_t reason); + static void auth_passkey_confirm(bt_conn *conn, unsigned int passkey); + bt_conn *conn_{}; + CallbackManager<void(uint32_t)> passkey_cb_; +}; + +template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> { + public: + explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(bool, accept) + + void play(const Ts &...x) override { this->parent_->numeric_comparison_reply(this->accept_.value(x...)); } + + protected: + BLEServer *parent_; }; } // namespace esphome::zephyr_ble_server diff --git a/tests/components/zephyr_ble_server/test.nrf52-xiao-ble.yaml b/tests/components/zephyr_ble_server/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..2b440102dbc --- /dev/null +++ b/tests/components/zephyr_ble_server/test.nrf52-xiao-ble.yaml @@ -0,0 +1,10 @@ +zephyr_ble_server: + on_numeric_comparison_request: + then: + - logger.log: + format: "Compare this passkey with the one on your BLE device: %06d" + args: [passkey] + - ble_server.numeric_comparison_reply: + accept: True + - ble_server.numeric_comparison_reply: + accept: !lambda "return true;" From ee70a4aa72d6f7aaffca86960b74a1a88796606a Mon Sep 17 00:00:00 2001 From: guillempages <guillempages@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:46:27 +0200 Subject: [PATCH 1473/2056] [tm1637] Add set_brightness method (#15322) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/tm1637/tm1637.cpp | 7 +++++++ esphome/components/tm1637/tm1637.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index da9adb59a4f..4814d5b1c49 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -347,6 +347,13 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { } return pos - start_pos; } + +void TM1637Display::set_brightness(float brightness) { + auto intensity = clamp(brightness, 0.f, 1.f) * 7; + this->set_on(intensity > 0); + this->set_intensity(intensity); +} + uint8_t TM1637Display::print(const char *str) { return this->print(0, str); } void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) { diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index c1fbabb21b5..1738d37107a 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -50,6 +50,9 @@ class TM1637Display : public PollingComponent { /// Set raw buffer bytes from data array up to length bytes. void set_buffer(const uint8_t *data, uint8_t length); + /// Set the display brightness. Accepts a value between 0.0 and 1.0; 0 will turn off + /// the display and 1.0 will set it to the maximum brightness. + void set_brightness(float brightness); void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_length(uint8_t length) { this->length_ = length; } From d8329dba228733282176ab053d49b85ad6839ad6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:17:51 +1000 Subject: [PATCH 1474/2056] [mipi_spi] Add Waveshare C6 LCD 1.47 (#15776) --- esphome/components/mipi_spi/models/waveshare.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index cc86101f5ee..ee8bd067004 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -15,7 +15,7 @@ from esphome.components.mipi import ( import esphome.config_validation as cv from .amoled import CO5300 -from .ili import ILI9488_A +from .ili import ILI9488_A, ST7789V from .jc import AXS15231 DriverChip( @@ -243,3 +243,15 @@ ST7789P.extend( ), ), ) + +ST7789V.extend( + "WAVESHARE-ESP32-C6-LCD-1.47", + width=172, + height=320, + offset_width=34, + invert_colors=True, + data_rate="40MHz", + reset_pin=21, + cs_pin=14, + dc_pin={"number": 15, "ignore_strapping_warning": True}, +) From 0b051289f5c66f783e596570c42ee39bc540dbe3 Mon Sep 17 00:00:00 2001 From: SaVi <215243861+SaVi456@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:49:33 +0530 Subject: [PATCH 1475/2056] [core] Add missing exception chaining (raise from) across codebase (#15648) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/automation.py | 7 ++-- esphome/components/as5600/__init__.py | 4 +-- esphome/components/audio_file/__init__.py | 2 +- esphome/components/binary_sensor/__init__.py | 17 +++++++--- esphome/components/bme68x_bsec2/__init__.py | 4 ++- esphome/components/font/__init__.py | 2 +- esphome/components/ili9xxx/display.py | 2 +- esphome/components/shelly_dimmer/light.py | 2 +- .../speaker/media_player/__init__.py | 2 +- esphome/components/stepper/__init__.py | 10 +++--- esphome/components/substitutions/__init__.py | 2 +- esphome/components/time/__init__.py | 6 ++-- esphome/components/wifi/wpa2_eap.py | 6 ++-- esphome/components/wireguard/__init__.py | 2 +- esphome/config_validation.py | 32 ++++++++----------- esphome/external_files.py | 2 +- esphome/voluptuous_schema.py | 3 +- esphome/yaml_util.py | 3 +- 18 files changed, 54 insertions(+), 54 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index b4dcc419950..97d9a0a47a8 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -199,11 +199,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): return cv.Schema([schema])(value) except cv.Invalid as err2: if "extra keys not allowed" in str(err2) and len(err2.path) == 2: - # pylint: disable=raise-missing-from - raise err + raise err from None if "Unable to find action" in str(err): - raise err2 - raise cv.MultipleInvalid([err, err2]) + raise err2 from None + raise cv.MultipleInvalid([err, err2]) from None elif isinstance(value, dict): if CONF_THEN in value: return [schema(value)] diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index b141329e945..444306cec3a 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360): value = angle(min=min, max=max)(value) return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION except cv.Invalid as e: - raise cv.Invalid(f"When using angle, {e.error_message}") + raise cv.Invalid(f"When using angle, {e.error_message}") from e def percent_to_position(value): @@ -164,7 +164,7 @@ def has_valid_range_config(): except cv.Invalid as e: raise cv.Invalid( f"The range between start and end position is invalid. It was was {range} but {e.error_message}" - ) + ) from e return validator diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py index 3ed6c1cd928..bb1ce257db4 100644 --- a/esphome/components/audio_file/__init__.py +++ b/esphome/components/audio_file/__init__.py @@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]: raise cv.Invalid( f"Unable to determine audio file type of '{path}'. " f"Try re-encoding the file into a supported format. Details: {e}" - ) + ) from e media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type == "wav": diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 0b36c299f6a..29ddbab02cd 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -332,8 +332,9 @@ def parse_multi_click_timing_str(value): try: state = cv.boolean(parts[0]) except cv.Invalid: - # pylint: disable=raise-missing-from - raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}") + raise cv.Invalid( + f"First word must either be ON or OFF, not {parts[0]}" + ) from None if parts[1] != "for": raise cv.Invalid(f"Second word must be 'for', got {parts[1]}") @@ -350,7 +351,9 @@ def parse_multi_click_timing_str(value): try: length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}") + raise cv.Invalid( + f"Multi Click Grammar Parsing length failed: {err}" + ) from err return {CONF_STATE: state, key: str(length)} if parts[3] != "to": @@ -359,12 +362,16 @@ def parse_multi_click_timing_str(value): try: min_length = cv.positive_time_period_milliseconds(parts[2]) except cv.Invalid as err: - raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") + raise cv.Invalid( + f"Multi Click Grammar Parsing minimum length failed: {err}" + ) from err try: max_length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") + raise cv.Invalid( + f"Multi Click Grammar Parsing maximum length failed: {err}" + ) from err return { CONF_STATE: state, diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index b63443c5f30..b56217fac18 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -171,7 +171,9 @@ async def to_code_base(config): with open(path, encoding="utf-8") as f: bsec2_iaq_config = f.read() except Exception as e: - raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}") + raise core.EsphomeError( + f"Could not open binary configuration file {path}: {e}" + ) from e # Convert retrieved BSEC2 config to an array of ints rhs = [int(x) for x in bsec2_iaq_config.split(",")] diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index a1339a4bc11..a10c45a9d78 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -325,7 +325,7 @@ def download_gfont(value): raise cv.Invalid( f"Could not download font at {url}, please check the fonts exists " f"at google fonts ({e})" - ) + ) from e match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) if match is None: raise cv.Invalid( diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 1f20b21a0e8..b1d332c1e59 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -283,7 +283,7 @@ async def to_code(config): try: return Image.open(path) except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") + raise core.EsphomeError(f"Could not load image file {path}: {e}") from e # make a wide horizontal combined image. images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]] diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index 1688f9d6a60..97538e13c9b 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -84,7 +84,7 @@ def get_firmware(value): req = requests.get(url, timeout=30) req.raise_for_status() except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download firmware file ({url}): {e}") + raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e h = hashlib.new("sha256") h.update(req.content) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 320e96c8979..9b496637da1 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -173,7 +173,7 @@ def _read_audio_file_and_type(file_config): raise cv.Invalid( f"Unable to determine audio file type of '{path}'. " f"Try re-encoding the file into a supported format. Details: {e}" - ) + ) from e media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type in ("wav"): diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 8acacc3b492..8e801876628 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -35,8 +35,9 @@ def validate_acceleration(value): try: value = float(value) except ValueError: - # pylint: disable=raise-missing-from - raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") + raise cv.Invalid( + f"Expected acceleration as floating point number, got {value}" + ) from None if value <= 0: raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!") @@ -55,8 +56,9 @@ def validate_speed(value): try: value = float(value) except ValueError: - # pylint: disable=raise-missing-from - raise cv.Invalid(f"Expected speed as floating point number, got {value}") + raise cv.Invalid( + f"Expected speed as floating point number, got {value}" + ) from None if value <= 0: raise cv.Invalid("Speed must be larger than 0 steps/s!") diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index c0bd9d7be97..94aebbbfe35 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -188,7 +188,7 @@ def _expand_substitutions( f"\nRelevant context:\n{err.context_trace_str()}" f"\nSee {'->'.join(str(x) for x in path)}", path, - ) + ) from err else: if isinstance(orig_value, ESPHomeDataBase): value = _restore_data_base(value, orig_value) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ac0abeee0f..37c08b3a126 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -109,8 +109,7 @@ def _parse_cron_int(value, special_mapping, message): try: return int(value) except ValueError: - # pylint: disable=raise-missing-from - raise cv.Invalid(message.format(value)) + raise cv.Invalid(message.format(value)) from None def _parse_cron_part(part, min_value, max_value, special_mapping): @@ -134,10 +133,9 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): try: repeat_n = int(repeat) except ValueError: - # pylint: disable=raise-missing-from raise cv.Invalid( f"Repeat for '/' time expression must be an integer, got {repeat}" - ) + ) from None return set(range(offset_n, max_value + 1, repeat_n)) if "-" in part: data = part.split("-") diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 9da3494329a..51971a12207 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -67,7 +67,7 @@ def _validate_load_certificate(value): contents = read_relative_config_path(value) return wrapped_load_pem_x509_certificate(contents) except ValueError as err: - raise cv.Invalid(f"Invalid certificate: {err}") + raise cv.Invalid(f"Invalid certificate: {err}") from err def validate_certificate(value): @@ -86,9 +86,9 @@ def _validate_load_private_key(key, cert_pw): except ValueError as e: raise cv.Invalid( f"There was an error with the EAP 'password:' provided for 'key' {e}" - ) + ) from e except TypeError as e: - raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") + raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") from e def _check_private_key_cert_match(key, cert): diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 1b543913766..4fdb256d0cc 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -53,7 +53,7 @@ def _cidr_network(value): try: ipaddress.ip_network(value, strict=False) except ValueError as err: - raise cv.Invalid(f"Invalid network in CIDR notation: {err}") + raise cv.Invalid(f"Invalid network in CIDR notation: {err}") from err return value diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 31cfb41a6d9..e6b0cb7ee23 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -544,8 +544,9 @@ def int_(value): try: return int(value, base) except ValueError: - # pylint: disable=raise-missing-from - raise Invalid(f"Expected integer, but cannot parse {value} as an integer") + raise Invalid( + f"Expected integer, but cannot parse {value} as an integer" + ) from None def int_range(min=None, max=None, min_included=True, max_included=True): @@ -844,8 +845,7 @@ def time_period_str_colon(value): try: parsed = [int(x) for x in value.split(":")] except ValueError: - # pylint: disable=raise-missing-from - raise Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) from None if len(parsed) == 2: hour, minute = parsed @@ -1047,8 +1047,7 @@ def date_time(date: bool, time: bool): try: date_obj = datetime.strptime(value, format) except ValueError as err: - # pylint: disable=raise-missing-from - raise Invalid(f"Invalid {exc_message}: {err}") + raise Invalid(f"Invalid {exc_message}: {err}") from err return_value = {} if date: @@ -1078,8 +1077,9 @@ def mac_address(value): try: parts_int.append(int(part, 16)) except ValueError: - # pylint: disable=raise-missing-from - raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF") + raise Invalid( + "MAC Address parts must be hexadecimal values from 00 to FF" + ) from None return core.MACAddress(*parts_int) @@ -1096,8 +1096,7 @@ def bind_key(value, *, name="Bind key"): try: parts_int.append(int(part, 16)) except ValueError: - # pylint: disable=raise-missing-from - raise Invalid(f"{name} must be hex values from 00 to FF") + raise Invalid(f"{name} must be hex values from 00 to FF") from None return "".join(f"{part:02X}" for part in parts_int) @@ -1425,8 +1424,7 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - # pylint: disable=raise-missing-from - raise Invalid(f"MQTT Quality of Service must be integer, got {value}") + raise Invalid(f"MQTT Quality of Service must be integer, got {value}") from None return one_of(0, 1, 2)(value) @@ -1518,8 +1516,7 @@ def _parse_percentage(value: object) -> float: else: value = float(value) except ValueError: - # pylint: disable=raise-missing-from - raise Invalid("invalid number") + raise Invalid("invalid number") from None try: if not has_percent_sign and (value > 1 or value < -1): raise Invalid( @@ -1527,9 +1524,7 @@ def _parse_percentage(value: object) -> float: "outside -1.0 to 1.0. Please put a percent sign after the number!" ) except TypeError: - raise Invalid( # pylint: disable=raise-missing-from - "Expected percentage or float" - ) + raise Invalid("Expected percentage or float") from None return float(value) @@ -1702,8 +1697,7 @@ def dimensions(value): try: width, height = int(value[0]), int(value[1]) except ValueError: - # pylint: disable=raise-missing-from - raise Invalid("Width and height dimensions must be integers") + raise Invalid("Width and height dimensions must be integers") from None if width <= 0 or height <= 0: raise Invalid("Width and height must at least be 1") return [width, height] diff --git a/esphome/external_files.py b/esphome/external_files.py index 18b68fba088..55711e1b790 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -107,7 +107,7 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: e, ) return path.read_bytes() - raise cv.Invalid(f"Could not download from {url}: {e}") + raise cv.Invalid(f"Could not download from {url}: {e}") from e path.parent.mkdir(parents=True, exist_ok=True) data = req.content diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 0703c54a7a8..904963ba4e8 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -39,8 +39,7 @@ class _Schema(vol.Schema): try: res = extra(res) except vol.Invalid as err: - # pylint: disable=raise-missing-from - raise ensure_multiple_invalid(err) + raise ensure_multiple_invalid(err) from err return res def _compile_mapping(self, schema, invalid_msg=None): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 59d851c02e7..e15adff935c 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -338,10 +338,9 @@ class ESPHomeLoaderMixin: try: hash(key) except TypeError: - # pylint: disable=raise-missing-from raise yaml.constructor.ConstructorError( f'Invalid key "{key}" (not hashable)', key_node.start_mark - ) + ) from None key = make_data_base(str(key)) key.from_node(key_node) From 6af7a9ed8fe8059f7378c37affbb091140c01165 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:36:06 -0400 Subject: [PATCH 1476/2056] [qmc5883l] Move per-update log line from DEBUG to VERBOSE (#15781) --- esphome/components/qmc5883l/qmc5883l.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index bc2adb5cfe5..d0488d0c9f7 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -100,7 +100,7 @@ void QMC5883LComponent::update() { // ROL_PNT in setup and reading 7 bytes starting at the status register. // If status and all three axes are desired, using ROL_PNT saves you 3 bytes. // But simply not reading status saves you 4 bytes always and is much simpler. - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) { + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1); if (err != i2c::ERROR_OK) { char buf[32]; @@ -165,7 +165,7 @@ void QMC5883LComponent::update() { temp = int16_t(raw_temp) * 0.01f; } - ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading, + ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading, temp, status); if (this->x_sensor_ != nullptr) From c6ad23fbc05047745522c5ec0e0996e85f958d00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 08:45:33 -1000 Subject: [PATCH 1477/2056] [bundle] Force-resolve nested IncludeFile during file discovery (#15762) --- esphome/bundle.py | 78 ++++++++- .../fixtures/bundle/bundle_test.yaml | 6 +- .../fixtures/bundle/common/wifi.yaml | 2 + tests/unit_tests/test_bundle.py | 150 +++++++++++++++++- 4 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 tests/unit_tests/fixtures/bundle/common/wifi.yaml diff --git a/esphome/bundle.py b/esphome/bundle.py index b6816c7c95e..efa80acc8cc 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -151,8 +151,8 @@ class ConfigBundleCreator: def __init__(self, config: dict[str, Any]) -> None: self._config = config - self._config_dir = CORE.config_dir - self._config_path = CORE.config_path + self._config_dir = Path(CORE.config_dir).resolve() + self._config_path = Path(CORE.config_path).resolve() self._files: list[BundleFile] = [] self._seen_paths: set[Path] = set() self._secrets_paths: set[Path] = set() @@ -258,21 +258,36 @@ class ConfigBundleCreator: def _discover_yaml_includes(self) -> None: """Discover YAML files loaded during config parsing. - We track files by wrapping _load_yaml_internal. The config has already - been loaded at this point (bundle is a POST_CONFIG_ACTION), so we - re-load just to discover the file list. + Deliberately uses a fresh re-parse and force-loads every deferred + ``IncludeFile`` to include *all* potentially-reachable includes, + even branches not selected by the local substitutions. Bundles are + meant to be compiled on another system where command-line + substitution overrides may choose a different branch — e.g. + ``!include network/${eth_model}/config.yaml`` must ship every + candidate so the remote build can pick any one. + + Entries with unresolved substitution variables in the filename + path are skipped with a warning (they cannot be resolved without + the substitution pass). Secrets files are tracked separately so we can filter them to only include the keys this config actually references. """ + # Must be a fresh parse: IncludeFile.load() caches its result in + # _content, and we discover files by listening for loader calls. On + # an already-parsed tree the cache is populated, .load() returns + # without calling the loader, the listener never fires, and the + # referenced files would be silently dropped from the bundle. with yaml_util.track_yaml_loads() as loaded_files: try: - yaml_util.load_yaml(self._config_path) + data = yaml_util.load_yaml(self._config_path) except EsphomeError: _LOGGER.debug( "Bundle: re-loading YAML for include discovery failed, " "proceeding with partial file list" ) + else: + _force_load_include_files(data) for fpath in loaded_files: if fpath == self._config_path.resolve(): @@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None: tar.addfile(info, io.BytesIO(data)) +def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None: + """Recursively resolve any ``IncludeFile`` instances in a YAML tree. + + Nested ``!include`` returns a deferred ``IncludeFile`` that is only + resolved during the substitution pass. During bundle discovery we need + the referenced files to actually load so the ``track_yaml_loads`` + listener fires for them. + + ``IncludeFile`` instances with unresolved substitution variables in the + filename cannot be loaded — we skip and warn about those. + """ + if _seen is None: + _seen = set() + + if isinstance(obj, yaml_util.IncludeFile): + if id(obj) in _seen: + return + _seen.add(id(obj)) + if obj.has_unresolved_expressions(): + _LOGGER.warning( + "Bundle: cannot resolve !include %s (referenced from %s) " + "with substitutions in path", + obj.file, + obj.parent_file, + ) + return + try: + loaded = obj.load() + except EsphomeError as err: + _LOGGER.warning( + "Bundle: failed to load !include %s (referenced from %s): %s", + obj.file, + obj.parent_file, + err, + ) + return + _force_load_include_files(loaded, _seen) + elif isinstance(obj, dict): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for value in obj.values(): + _force_load_include_files(value, _seen) + elif isinstance(obj, (list, tuple)): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for item in obj: + _force_load_include_files(item, _seen) + + def _resolve_include_path(include_path: Any) -> Path | None: """Resolve an include path to absolute, skipping system includes.""" if isinstance(include_path, str) and include_path.startswith("<"): diff --git a/tests/unit_tests/fixtures/bundle/bundle_test.yaml b/tests/unit_tests/fixtures/bundle/bundle_test.yaml index f834a8d867f..247f5cc8bb1 100644 --- a/tests/unit_tests/fixtures/bundle/bundle_test.yaml +++ b/tests/unit_tests/fixtures/bundle/bundle_test.yaml @@ -11,9 +11,9 @@ esp32: logger: <<: !include common/base.yaml -wifi: - ssid: !secret wifi_ssid - password: !secret wifi_password +# Plain nested !include — deferred as an IncludeFile until the substitution +# pass. The bundle must force-resolve it to pick up common/wifi.yaml. +wifi: !include common/wifi.yaml api: diff --git a/tests/unit_tests/fixtures/bundle/common/wifi.yaml b/tests/unit_tests/fixtures/bundle/common/wifi.yaml new file mode 100644 index 00000000000..d7e7b3cd450 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/common/wifi.yaml @@ -0,0 +1,2 @@ +ssid: !secret wifi_ssid +password: !secret wifi_password diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index b8b2d0ffd1a..89bf1a33b3e 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -5,8 +5,10 @@ from __future__ import annotations import io import json from pathlib import Path +import shutil import tarfile from typing import Any +from unittest.mock import patch import pytest @@ -20,6 +22,7 @@ from esphome.bundle import ( _add_bytes_to_tar, _default_target_dir, _find_used_secret_keys, + _force_load_include_files, extract_bundle, is_bundle_path, prepare_bundle_for_compile, @@ -485,7 +488,7 @@ def test_read_bundle_manifest_minimal(tmp_path: Path) -> None: result = read_bundle_manifest(bundle_path) assert result.esphome_version == "unknown" - assert result.files == [] + assert not result.files assert result.has_secrets is False @@ -862,6 +865,117 @@ def test_discover_files_skips_missing_directory(tmp_path: Path) -> None: assert len(files) == 1 +def test_discover_files_nested_include(tmp_path: Path) -> None: + """Nested !include files (e.g. wifi: !include wifi.yaml) are bundled.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include wifi.yaml\n" + ) + (config_dir / "wifi.yaml").write_text('ssid: "a"\npassword: "b"\n') + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + assert "wifi.yaml" in paths + + +def test_discover_files_deeply_nested_include(tmp_path: Path) -> None: + """Chains of !include (a includes b includes c) are fully resolved.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include level1.yaml\n" + ) + (config_dir / "level1.yaml").write_text("nested: !include level2.yaml\n") + (config_dir / "level2.yaml").write_text('value: "leaf"\n') + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "level1.yaml" in paths + assert "level2.yaml" in paths + + +def test_discover_files_nested_include_unresolved_substitution( + tmp_path: Path, +) -> None: + """!include with substitution vars in path cannot be resolved; skipped gracefully.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include ${platform}.yaml\n" + ) + + creator = ConfigBundleCreator({}) + # Should not raise + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + + +def test_discover_files_nested_include_load_failure( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A nested !include pointing at a missing file is logged and skipped.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include missing.yaml\n" + ) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + assert any( + "failed to load !include" in r.message and "missing.yaml" in r.message + for r in caplog.records + ) + + +def test_force_load_skips_duplicate_include_file() -> None: + """The same IncludeFile referenced twice is only loaded once.""" + + class _StubInclude: + """Mimics yaml_util.IncludeFile minimally for _force_load testing.""" + + def __init__(self) -> None: + self.file = Path("dup.yaml") + self.parent_file = Path("root.yaml") + self.load_calls = 0 + + def has_unresolved_expressions(self) -> bool: + return False + + def load(self) -> dict[str, Any]: + self.load_calls += 1 + return {} + + stub = _StubInclude() + # Same instance appears twice — second visit must hit the _seen guard. + tree = {"a": stub, "b": [stub]} + + with patch("esphome.bundle.yaml_util.IncludeFile", _StubInclude): + _force_load_include_files(tree) + + assert stub.load_calls == 1 + + +def test_force_load_handles_cyclic_containers() -> None: + """Cyclic dict/list references don't cause infinite recursion.""" + cyclic_dict: dict[str, Any] = {} + cyclic_dict["self"] = cyclic_dict + + cyclic_list: list[Any] = [] + cyclic_list.append(cyclic_list) + + # Should return without recursing forever + _force_load_include_files(cyclic_dict) + _force_load_include_files(cyclic_list) + + def test_discover_files_yaml_reload_failure( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1008,6 +1122,40 @@ def test_discover_files_walk_tuple_values(tmp_path: Path) -> None: assert "a.pem" in paths +# --------------------------------------------------------------------------- +# ConfigBundleCreator - fixture-based end-to-end +# --------------------------------------------------------------------------- + + +def test_discover_files_fixture_config(fixture_path: Path, tmp_path: Path) -> None: + """Use the real ``fixtures/bundle/`` tree as an end-to-end reproducer. + + The fixture config uses ``wifi: !include common/wifi.yaml`` — a plain + nested !include that is returned as a deferred ``IncludeFile`` and only + resolved during the substitution pass. Before this fix, bundle discovery + never ran substitutions, so ``common/wifi.yaml`` was silently missing + from the bundle. + """ + # Copy the fixture tree into a tmp dir so the test doesn't rely on the + # source repo being writable and so we can set CORE.config_path freely. + src = fixture_path / "bundle" + dst = tmp_path / "bundle" + shutil.copytree(src, dst) + + CORE.config_path = dst / "bundle_test.yaml" + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + paths = {f.path for f in files} + + # Root and top-level !secret-referenced files + assert "bundle_test.yaml" in paths + assert "secrets.yaml" in paths + # The nested !include — this is what regressed when IncludeFile became + # deferred (PR #12213). + assert "common/wifi.yaml" in paths + + # --------------------------------------------------------------------------- # ConfigBundleCreator - create_bundle # --------------------------------------------------------------------------- From 90943928702b23b5e0d452eb7d7de1b84a6c70c3 Mon Sep 17 00:00:00 2001 From: rwalker777 <49888088+rwalker777@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:01:32 -0500 Subject: [PATCH 1478/2056] [gpio] Keep interrupts enabled for gpio binary_sensor shared with deep_sleep wakeup pin (#15020) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../components/gpio/binary_sensor/__init__.py | 88 +++++++++++++------ .../gpio/binary_sensor/gpio_binary_sensor.cpp | 5 -- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 3c2021d40e8..390b26ba1d4 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -60,20 +60,35 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): - var = await binary_sensor.new_binary_sensor(config) - await cg.register_component(var, config) +def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool: + """Check if pin is shared exclusively with deep_sleep (wakeup pin).""" + pin_key = (CORE.target_platform, CORE.target_platform, pin_num) + pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, []) + if len(pin_users) != 2: + return False + return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users) - pin = await cg.gpio_pin_expression(config[CONF_PIN]) - cg.add(var.set_pin(pin)) - # Check for ESP8266 GPIO16 interrupt limitation - # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through - # the Arduino attachInterrupt() function. This is the only known GPIO pin - # across all supported platforms that has this limitation, so we handle it - # here instead of in the platform-specific code. +def _final_validate(config): use_interrupt = config[CONF_USE_INTERRUPT] - if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: + if not use_interrupt: + return config + + pin_num = config[CONF_PIN][CONF_NUMBER] + + # Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt + # attachment — only internal/native GPIO pins do. + if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform: + _LOGGER.info( + "GPIO binary_sensor '%s': Pin is not an internal GPIO, " + "falling back to polling mode.", + config.get(CONF_NAME, config[CONF_ID]), + ) + config[CONF_USE_INTERRUPT] = False + return config + + # GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt(). + if CORE.is_esp8266 and pin_num == 16: _LOGGER.warning( "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " "Falling back to polling mode (same as in ESPHome <2025.7). " @@ -81,22 +96,45 @@ async def to_code(config): "performance with interrupts.", config.get(CONF_NAME, config[CONF_ID]), ) - use_interrupt = False + config[CONF_USE_INTERRUPT] = False + return config - # Check if pin is shared with other components (allow_other_uses) # When a pin is shared, interrupts can interfere with other components - # (e.g., duty_cycle sensor) that need to monitor the pin's state changes - if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): - _LOGGER.info( - "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " - "The sensor will use polling mode for compatibility with other pin uses.", - config.get(CONF_NAME, config[CONF_ID]), - config[CONF_PIN][CONF_NUMBER], - ) - use_interrupt = False + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes. + # Exception: deep_sleep wakeup pins are compatible with interrupts when + # the pin is only shared between this sensor and deep_sleep (count == 2). + if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + if not _pin_shared_only_with_deep_sleep(pin_num): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared " + "with other components. The sensor will use polling mode for " + "compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + pin_num, + ) + config[CONF_USE_INTERRUPT] = False + else: + _LOGGER.debug( + "GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, " + "keeping interrupts enabled.", + config.get(CONF_NAME, config[CONF_ID]), + pin_num, + ) - if use_interrupt: + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + if config[CONF_USE_INTERRUPT]: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) else: - # Only generate call when disabling interrupts (default is true) - cg.add(var.set_use_interrupt(use_interrupt)) + cg.add(var.set_use_interrupt(False)) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 39b1a2f7132..1f0154c70b7 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -46,11 +46,6 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { } void GPIOBinarySensor::setup() { - if (this->store_.use_interrupt_ && !this->pin_->is_internal()) { - ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); - this->store_.use_interrupt_ = false; - } - if (this->store_.use_interrupt_) { auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_); this->store_.setup(internal_pin, this); From 7d8add70a7b86afbbf93e9f65e34d9f72532f1eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 09:01:55 -1000 Subject: [PATCH 1479/2056] [ili9xxx] Guard against null buffer in display_() when allocation fails (#15786) --- esphome/components/ili9xxx/ili9xxx_defines.h | 2 ++ esphome/components/ili9xxx/ili9xxx_display.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/ili9xxx/ili9xxx_defines.h b/esphome/components/ili9xxx/ili9xxx_defines.h index f4c5aad957c..70e0937f790 100644 --- a/esphome/components/ili9xxx/ili9xxx_defines.h +++ b/esphome/components/ili9xxx/ili9xxx_defines.h @@ -1,5 +1,7 @@ #pragma once +#include <cstdint> + namespace esphome { namespace ili9xxx { diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index a3eff901d30..11acb8a73a4 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -229,6 +229,10 @@ void ILI9XXXDisplay::update() { } void ILI9XXXDisplay::display_() { + // buffer may be null if allocation failed + if (this->buffer_ == nullptr) { + return; + } // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { return; From 6bb90a126838e8ce5703421e18037e93a2335218 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:07:04 -0400 Subject: [PATCH 1480/2056] [esp32] Accept unquoted minimum_chip_revision values (#15785) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7b3f9da3da6..a68614cb436 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1222,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( - *ESP32_CHIP_REVISIONS + *ESP32_CHIP_REVISIONS, string=True ), cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, From 627e440bd60212b065ab24cc2fbae0ece8d2abda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 09:38:49 -1000 Subject: [PATCH 1481/2056] [libretiny] Make IRAM_ATTR functional on RTL87xx and LN882H (#15766) --- esphome/components/bk72xx/__init__.py | 5 + esphome/components/libretiny/__init__.py | 17 ++ .../libretiny/generate_components.py | 5 + .../libretiny/patch_linker.py.script | 171 ++++++++++++++++++ esphome/components/ln882x/__init__.py | 5 + esphome/components/rtl87xx/__init__.py | 5 + esphome/core/application.h | 4 +- esphome/core/hal.h | 72 ++++++++ esphome/core/main_task.h | 19 +- esphome/core/wake.cpp | 6 +- esphome/core/wake.h | 28 ++- 11 files changed, 308 insertions(+), 29 deletions(-) create mode 100644 esphome/components/libretiny/patch_linker.py.script diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py index 7fed742d2e2..3ffab0f3a5e 100644 --- a/esphome/components/bk72xx/__init__.py +++ b/esphome/components/bk72xx/__init__.py @@ -65,3 +65,8 @@ async def to_code(config): @pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA) async def pin_to_code(config): return await libretiny.gpio.component_pin_to_code(config) + + +# Called by writer.py; delegates to the shared libretiny implementation. +def copy_files() -> None: + libretiny.copy_files() diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 656eee6d7bf..4f42f40478c 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -1,5 +1,6 @@ import json import logging +from pathlib import Path import esphome.codegen as cg import esphome.config_validation as cv @@ -24,6 +25,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.config import BOARD_MAX_LENGTH +from esphome.helpers import copy_file_if_changed from esphome.storage_json import StorageJSON from . import gpio # noqa @@ -465,6 +467,11 @@ async def component_to_code(config): # it for project source files only. GCC uses the last -O flag. build_src_flags += " -Os" cg.add_platformio_option("build_src_flags", build_src_flags) + # IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops). + # On other families, patch_linker.py routes .sram.text into the right + # RAM-executable output section and prints a post-link placement summary. + if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX: + cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"]) # dummy version code cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)")) # decrease web server stack size (16k words -> 4k words) @@ -549,3 +556,13 @@ async def component_to_code(config): _configure_lwip(config) await cg.register_component(var, config) + + +# Called by writer.py +def copy_files() -> None: + script_dir = Path(__file__).parent + patch_linker_file = script_dir / "patch_linker.py.script" + copy_file_if_changed( + patch_linker_file, + CORE.relative_build_path("patch_linker.py"), + ) diff --git a/esphome/components/libretiny/generate_components.py b/esphome/components/libretiny/generate_components.py index 41b43894465..d5437895a69 100644 --- a/esphome/components/libretiny/generate_components.py +++ b/esphome/components/libretiny/generate_components.py @@ -79,6 +79,11 @@ async def to_code(config): @pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA) async def pin_to_code(config): return await libretiny.gpio.component_pin_to_code(config) + + +# Called by writer.py; delegates to the shared libretiny implementation. +def copy_files() -> None: + libretiny.copy_files() ''' BASE_CODE_BOARDS = ''' diff --git a/esphome/components/libretiny/patch_linker.py.script b/esphome/components/libretiny/patch_linker.py.script new file mode 100644 index 00000000000..282a31d3f2f --- /dev/null +++ b/esphome/components/libretiny/patch_linker.py.script @@ -0,0 +1,171 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os +import re +import subprocess + +# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family +# section routed into RAM-executable memory (see esphome/core/hal.h). +# +# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK +# masks FIQ+IRQ around flash writes). On the remaining families: +# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it. +# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it. +# - LN882H: stock linker has no glob for ".sram.text", so we inject +# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH). +# +# All families also get a post-link summary showing where IRAM_ATTR landed. + + +_MARKER = "/* esphome .sram.text */" +# Strong assignments (not PROVIDE) so the symbols are always emitted in the +# ELF; PROVIDE symbols with no references can be garbage-collected. +_KEEP_LINE = ( + " __esphome_sram_text_start = .; " + "KEEP(*(.sram.text*)) " + "__esphome_sram_text_end = .; " + + _MARKER + "\n" +) +_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)") + + +def _detect(env): + prefix = "USE_LIBRETINY_VARIANT_" + # CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds + # the raw "-DNAME" strings. PlatformIO populates both, but the exact order + # vs. extra_scripts varies, so check both to be robust. + for token in env.get("CPPDEFINES", []): + if isinstance(token, (list, tuple)): + token = token[0] + if isinstance(token, str) and token.startswith(prefix): + return token[len(prefix):] + for flag in env.get("BUILD_FLAGS", []): + if isinstance(flag, str) and "-D" + prefix in flag: + name = flag.split("-D", 1)[1].split("=", 1)[0].strip() + if name.startswith(prefix): + return name[len(prefix):] + return None + + +KNOWN_VARIANTS = frozenset({ + "LN882H", + "RTL8710B", + "RTL8720C", +}) + + +def _inject_keep(host_section): + """Return a patcher that injects _KEEP_LINE at the top of `host_section`.""" + def patch(content): + if _MARKER in content: + return content + return host_section.sub(r"\1" + _KEEP_LINE, content, count=1) + return patch + + +# Variants not listed here intentionally have no .ld patcher: +# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker +# already routes into .ram_image2.text (> BD_RAM). +# - RTL8720C: stock linker already consumes *(.sram.text*). +# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op. +_PATCHERS_BY_VARIANT = { + "LN882H": (_inject_keep(_LN_COPY),), +} + + +def _patchers_for(variant): + return _PATCHERS_BY_VARIANT.get(variant, ()) + + +def _pre_link(target, source, env): + build_dir = env.subst("$BUILD_DIR") + ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")] + patched = 0 + for name in ld_files: + path = os.path.join(build_dir, name) + with open(path, "r", encoding="utf-8") as fh: + original = fh.read() + if _MARKER in original: + patched += 1 + continue + content = original + for fn in _patchers: + content = fn(content) + if content != original: + with open(path, "w", encoding="utf-8") as fh: + fh.write(content) + print("ESPHome: patched {} for IRAM_ATTR placement".format(name)) + patched += 1 + if not patched: + raise RuntimeError( + "ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the " + "regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format( + build_dir + ) + ) + + +# Substrings matched against demangled names as a fallback on RTL8720C, +# where we cannot inject __esphome_sram_text_start/end markers. +_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe", + "enable_loop_soon_any_context") + + +def _post_link(target, source, env): + """Print where IRAM_ATTR ended up so users can confirm at a glance.""" + elf = env.subst("$BUILD_DIR/${PROGNAME}.elf") + if not os.path.isfile(elf): + return + nm = env.subst("$NM") + try: + out = subprocess.check_output( + [nm, "--defined-only", "--demangle", elf], text=True + ) + except (OSError, subprocess.CalledProcessError) as exc: + print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc)) + return + start = end = None + fallback = [] + for line in out.splitlines(): + parts = line.split(maxsplit=2) + if len(parts) != 3: + continue + addr_str, _kind, name = parts + if name == "__esphome_sram_text_start": + start = int(addr_str, 16) + elif name == "__esphome_sram_text_end": + end = int(addr_str, 16) + elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS): + fallback.append(int(addr_str, 16)) + print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant)) + if start is not None and end is not None: + print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end)) + elif fallback: + lo, hi = min(fallback), max(fallback) + print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo)) + else: + print(" no IRAM_ATTR symbols found") + + +if (_variant := _detect(env)) is None: + raise RuntimeError( + "ESPHome: could not determine LibreTiny variant from build flags. " + "patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR " + "into SRAM; without it, ISR handlers would silently end up in flash." + ) +if _variant not in KNOWN_VARIANTS: + raise RuntimeError( + "ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not " + "know how to route IRAM_ATTR into SRAM for this family. Update " + "patch_linker.py.script before shipping firmware.".format(_variant) + ) + +if _patchers := _patchers_for(_variant): + # LibreTiny writes the processed .ld templates into $BUILD_DIR during its + # own builder setup, which may run after this script. Register the patch + # as a pre-link action so it executes once the linker scripts exist. + env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link) + +# Post-link summary for every family that reaches this script. +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link) diff --git a/esphome/components/ln882x/__init__.py b/esphome/components/ln882x/__init__.py index 5c637bdf629..9c918275227 100644 --- a/esphome/components/ln882x/__init__.py +++ b/esphome/components/ln882x/__init__.py @@ -65,3 +65,8 @@ async def to_code(config): @pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA) async def pin_to_code(config): return await libretiny.gpio.component_pin_to_code(config) + + +# Called by writer.py; delegates to the shared libretiny implementation. +def copy_files() -> None: + libretiny.copy_files() diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 6fd750d51ed..a3b1dba4f22 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -65,3 +65,8 @@ async def to_code(config): @pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA) async def pin_to_code(config): return await libretiny.gpio.component_pin_to_code(config) + + +# Called by writer.py; delegates to the shared libretiny implementation. +def copy_files() -> None: + libretiny.copy_files() diff --git a/esphome/core/application.h b/esphome/core/application.h index b4bb8a1eec9..7356263c557 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -337,8 +337,8 @@ class Application { /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } -#ifdef USE_ESP32 - /// Wake from ISR (ESP32 only). +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + /// Wake from ISR (ESP32 and LibreTiny). static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); } #endif diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 03a30b7459f..e4083622b98 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -21,6 +21,35 @@ #define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) #define PROGMEM +#elif defined(USE_LIBRETINY) + +// IRAM_ATTR places a function in executable RAM so it is callable from an +// ISR even while flash is busy (XIP stall, OTA, logger flash write). +// Each family uses a section its stock linker already routes to RAM: +// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the +// exception: its stock linker has no matching glob, so patch_linker.py +// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. +// +// BK72xx (all variants) are left as a no-op: their SDK wraps flash +// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for +// the duration of every write, so no ISR fires while flash is stalled and +// the race IRAM_ATTR guards against cannot occur. The trade-off is that +// interrupts are delayed (not dropped) by up to ~20 ms during a sector +// erase, but that is an SDK-level choice and cannot be changed from this +// layer. +#if defined(USE_BK72XX) +#define IRAM_ATTR +#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) +// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). +#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#else +// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. +// LN882H: patch_linker.py.script injects *(.sram.text*) into +// .flash_copysection (> RAM0 AT> FLASH). +#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#endif +#define PROGMEM + #else #define IRAM_ATTR @@ -28,8 +57,51 @@ #endif +#ifdef USE_ESP32 +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#endif + +#ifdef USE_BK72XX +// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so +// it is callable from Thumb code via interworking. The MRS CPSR instruction +// is ARM-only and user code here may be built in Thumb, so in_isr_context() +// defers to this port helper on BK72xx instead of reading CPSR inline. +extern "C" uint32_t platform_is_in_interrupt_context(void); +#endif + namespace esphome { +/// Returns true when executing inside an interrupt handler. +/// always_inline so callers placed in IRAM keep the detection in IRAM. +__attribute__((always_inline)) inline bool in_isr_context() { +#if defined(USE_ESP32) + return xPortInIsrContext() != 0; +#elif defined(USE_ESP8266) + // ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is + // non-zero both in a real ISR and when user code masks interrupts. The + // ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule + // which is ISR-safe) so this helper is unused on this platform. + return false; +#elif defined(USE_RP2040) + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#elif defined(USE_BK72XX) + // BK72xx is ARM968E-S (ARM9); see extern declaration above. + return platform_is_in_interrupt_context() != 0; +#elif defined(USE_LIBRETINY) + // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; + // non-zero means we're in a handler. + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#else + // Host and any future platform without an ISR concept. + return false; +#endif +} + void yield(); uint32_t millis(); uint64_t millis_64(); diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index ed2885d2e25..3aa8669e445 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -20,7 +20,8 @@ extern "C" { extern TaskHandle_t esphome_main_task_handle; /// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. -static inline void esphome_main_task_notify() { +/// always_inline so callers placed in IRAM do not reference a flash-resident copy. +__attribute__((always_inline)) static inline void esphome_main_task_notify() { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); @@ -28,26 +29,14 @@ static inline void esphome_main_task_notify() { } /// Wake the main loop task from an ISR. ISR-safe. -static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) { +__attribute__((always_inline)) static inline void esphome_main_task_notify_from_isr( + BaseType_t *px_higher_priority_task_woken) { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken); } } -#ifdef USE_ESP32 -/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). -static inline void esphome_main_task_notify_any_context() { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_main_task_notify(); - } -} -#endif - #ifdef __cplusplus } #endif diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index b6b59b59909..3709fa88ac7 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,12 +12,12 @@ namespace esphome { -// === ESP32 — IRAM_ATTR entry points === -#ifdef USE_ESP32 +// === ESP32 / LibreTiny — IRAM_ATTR entry points === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } -void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } +void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } #endif // === ESP8266 / RP2040 === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index a8c9b7ad08b..5733ee65f6c 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -28,17 +28,27 @@ extern volatile bool g_main_loop_woke; // === ESP32 / LibreTiny (FreeRTOS) === #if defined(USE_ESP32) || defined(USE_LIBRETINY) -#ifdef USE_ESP32 -/// IRAM_ATTR entry point — defined in wake.cpp. -void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); -/// IRAM_ATTR entry point — defined in wake.cpp. -void wake_loop_any_context(); +/// Wake the main loop from any context (ISR or task). +/// always_inline so callers placed in IRAM keep the whole wake path in IRAM. +__attribute__((always_inline)) inline void wake_main_task_any_context() { + if (in_isr_context()) { + BaseType_t px_higher_priority_task_woken = pdFALSE; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); +#ifdef portYIELD_FROM_ISR + portYIELD_FROM_ISR(px_higher_priority_task_woken); #else -/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not -/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake -/// is not possible. xTaskNotifyGive is used as the best available option. -inline void wake_loop_any_context() { esphome_main_task_notify(); } + // ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ + // exit sequence performs the context switch if one was requested. + (void) px_higher_priority_task_woken; #endif + } else { + esphome_main_task_notify(); + } +} + +/// IRAM_ATTR entry points — defined in wake.cpp. +void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); +void wake_loop_any_context(); inline void wake_loop_threadsafe() { esphome_main_task_notify(); } From ff52bb30299c9edd0a36fbd69b3446ef309c7611 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:16:58 -0400 Subject: [PATCH 1482/2056] [lvgl] Guard lv_image_set_src wrapper with LV_USE_IMAGE (#15789) --- esphome/components/lvgl/lvgl_esphome.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ba258b1a2b..3ec1d247d8e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -76,16 +76,17 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { } #endif #if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) -// Shortcut / overload, so that the source of an image can easily be updated -// from within a lambda. -inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); } +#if LV_USE_IMAGE +// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda. +inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); } +#endif // LV_USE_IMAGE inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); + ::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); } inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); + ::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG From ac50f333887bb1d54cf87d5c43a8fbad02f38a07 Mon Sep 17 00:00:00 2001 From: Yves Fischer <yvesf+git@xapek.org> Date: Fri, 17 Apr 2026 00:27:50 +0200 Subject: [PATCH 1483/2056] Fix typo in devcontainer.json (#15791) --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5a7a02a266c..29f63b54b52 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,7 @@ "--privileged", "-e", "GIT_EDITOR=code --wait" - // uncomment and edit the path in order to pass though local USB serial to the conatiner + // uncomment and edit the path in order to pass through local USB serial to the container // , "--device=/dev/ttyACM0" ], "appPort": 6052, From b232fc91aba620736e8678f825f7150369c1c4f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 14:07:26 -1000 Subject: [PATCH 1484/2056] [runtime_stats] Track main loop active time and report overhead (#15743) --- .../runtime_stats/runtime_stats.cpp | 99 +++++++++++++++---- .../components/runtime_stats/runtime_stats.h | 41 ++++++++ esphome/core/application.h | 27 +++++ esphome/core/component.cpp | 4 + esphome/core/component.h | 8 ++ tests/integration/test_runtime_stats.py | 33 +++++++ 6 files changed, 193 insertions(+), 19 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 06714b5a446..9ed141155a5 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -32,40 +32,101 @@ void RuntimeStatsCollector::log_stats_() { " Period stats (last %" PRIu32 "ms): %zu active components", this->log_interval_, count); - if (count == 0) { - return; + // Sum component time so we can derive main-loop overhead + // (active loop time minus time attributable to component loop()s). + // Period sum iterates the active-in-period subset; total sum must iterate + // all components since total_active_time_us_ includes iterations where + // currently-idle components previously ran. + uint64_t period_component_sum_us = 0; + for (size_t i = 0; i < count; i++) { + period_component_sum_us += sorted[i]->runtime_stats_.period_time_us; + } + uint64_t total_component_sum_us = 0; + for (auto *component : components) { + total_component_sum_us += component->runtime_stats_.total_time_us; } - // Sort by period runtime (descending) - std::sort(sorted, sorted + count, compare_period_time); + if (count > 0) { + // Sort by period runtime (descending) + std::sort(sorted, sorted + count, compare_period_time); - // Log top components by period runtime - for (size_t i = 0; i < count; i++) { - const auto &stats = sorted[i]->runtime_stats_; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, - stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, - stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); + // Log top components by period runtime + for (size_t i = 0; i < count; i++) { + const auto &stats = sorted[i]->runtime_stats_; + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, + stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, + stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); + } + } + + // Main-loop overhead for the period: active wall time minus component time. + // active = sum of per-iteration loop time excluding yield/sleep. + if (this->period_active_count_ > 0) { + uint64_t active = this->period_active_time_us_; + uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0; + // Use double for µs→ms conversion so multi-day uptimes (where total + // microsecond counters exceed float's ~7-digit mantissa) keep resolution. + ESP_LOGI(TAG, + " main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, " + "overhead_total=%.1fms", + this->period_active_count_, + static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0, + static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0, + static_cast<double>(overhead) / 1000.0); + uint64_t before = this->period_before_time_us_; + uint64_t tail = this->period_tail_time_us_; + uint64_t accounted = before + tail; + uint64_t inter = overhead > accounted ? overhead - accounted : 0; + ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms", + static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0, + static_cast<double>(inter) / 1000.0); } // Log total stats since boot (only for active components - idle ones haven't changed) ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); - // Re-sort by total runtime for all-time stats - std::sort(sorted, sorted + count, compare_total_time); + if (count > 0) { + // Re-sort by total runtime for all-time stats + std::sort(sorted, sorted + count, compare_total_time); - for (size_t i = 0; i < count; i++) { - const auto &stats = sorted[i]->runtime_stats_; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, - stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, - stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); + for (size_t i = 0; i < count; i++) { + const auto &stats = sorted[i]->runtime_stats_; + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, + stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); + } + } + + if (this->total_active_count_ > 0) { + uint64_t active = this->total_active_time_us_; + uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0; + ESP_LOGI(TAG, + " main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, " + "overhead_total=%.1fms", + this->total_active_count_, + static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0, + static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0, + static_cast<double>(overhead) / 1000.0); + uint64_t before = this->total_before_time_us_; + uint64_t tail = this->total_tail_time_us_; + uint64_t accounted = before + tail; + uint64_t inter = overhead > accounted ? overhead - accounted : 0; + ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms", + static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0, + static_cast<double>(inter) / 1000.0); } // Reset period stats for (auto *component : components) { component->runtime_stats_.reset_period(); } + this->period_active_count_ = 0; + this->period_active_time_us_ = 0; + this->period_active_max_us_ = 0; + this->period_before_time_us_ = 0; + this->period_tail_time_us_ = 0; } bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) { diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 3c2c9f78add..82e0fb7c618 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -29,6 +29,31 @@ class RuntimeStatsCollector { // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); + // Record the wall time of one main loop iteration excluding the yield/sleep. + // Called once per loop from Application::loop(). + // active_us = total time between loop start and just before yield. + // before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop). + // tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix. + // Residual overhead at log time = active − Σ(component) − before − tail, + // which captures per-iteration inter-component bookkeeping (set_current_component, + // WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls, + // the for-loop itself). + void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) { + this->period_active_count_++; + this->period_active_time_us_ += active_us; + if (active_us > this->period_active_max_us_) + this->period_active_max_us_ = active_us; + this->total_active_count_++; + this->total_active_time_us_ += active_us; + if (active_us > this->total_active_max_us_) + this->total_active_max_us_ = active_us; + + this->period_before_time_us_ += before_us; + this->total_before_time_us_ += before_us; + this->period_tail_time_us_ += tail_us; + this->total_tail_time_us_ += tail_us; + } + protected: void log_stats_(); // Static comparators — member functions have friend access, lambdas do not @@ -37,6 +62,22 @@ class RuntimeStatsCollector { uint32_t log_interval_; uint32_t next_log_time_{0}; + + // Main loop active-time stats (wall time per iteration, excluding yield/sleep). + // Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in + // a few weeks of uptime, which is well within ESPHome device lifetimes. + uint64_t period_active_count_{0}; + uint64_t period_active_time_us_{0}; + uint32_t period_active_max_us_{0}; + uint64_t total_active_count_{0}; + uint64_t total_active_time_us_{0}; + uint32_t total_active_max_us_{0}; + + // Split of overhead sections — accumulated per iteration. + uint64_t period_before_time_us_{0}; + uint64_t total_before_time_us_{0}; + uint64_t period_tail_time_us_{0}; + uint64_t total_tail_time_us_{0}; }; } // namespace runtime_stats diff --git a/esphome/core/application.h b/esphome/core/application.h index 7356263c557..bc40fe0c7ee 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -575,10 +575,25 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ } inline void ESPHOME_ALWAYS_INLINE Application::loop() { +#ifdef USE_RUNTIME_STATS + // Capture the start of the active (non-sleeping) portion of this iteration. + // Used to derive main-loop overhead = active time − Σ(component time) − + // before/tail splits recorded below. + uint32_t loop_active_start_us = micros(); + // Snapshot the cumulative component-recorded time so we can subtract the + // slice that the scheduler spends inside its own WarnIfComponentBlockingGuard + // (scheduler.cpp) — that time is already counted in per-component stats, + // so charging it again to "before" would double-count. + uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us; +#endif // Get the initial loop time at the start uint32_t last_op_end_time = millis(); this->before_loop_tasks_(last_op_end_time); +#ifdef USE_RUNTIME_STATS + uint32_t loop_before_end_us = micros(); + uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap; +#endif for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; this->current_loop_index_++) { @@ -597,12 +612,24 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { this->feed_wdt_with_time(last_op_end_time); } +#ifdef USE_RUNTIME_STATS + uint32_t loop_tail_start_us = micros(); +#endif this->after_loop_tasks_(); #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) { + uint32_t loop_now_us = micros(); + // Subtract scheduled-component time from the "before" bucket so it is + // not double-counted (it is already attributed to per-component stats). + uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us; + uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us + ? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us) + : 0; + global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, + loop_now_us - loop_tail_start_us); global_runtime_stats->process_pending_stats(last_op_end_time); } #endif diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8949b4b76dc..e33652482ea 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -506,6 +506,10 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } +#ifdef USE_RUNTIME_STATS +uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif + void __attribute__((noinline, cold)) WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { bool should_warn; diff --git a/esphome/core/component.h b/esphome/core/component.h index 3307c5ae76e..6fbb0d5c06e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -116,6 +116,13 @@ struct ComponentRuntimeStats { uint64_t total_time_us{0}; uint32_t total_max_time_us{0}; + // Cumulative sum of every record_time() duration since boot, across all + // components. Used by Application::loop() to snapshot time spent inside + // WarnIfComponentBlockingGuard (including guards constructed by the + // scheduler at scheduler.cpp) so main-loop overhead accounting can + // subtract scheduled-callback time from the before_loop_tasks_ wall time. + static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void record_time(uint32_t duration_us) { this->period_count++; this->period_time_us += duration_us; @@ -125,6 +132,7 @@ struct ComponentRuntimeStats { this->total_time_us += duration_us; if (duration_us > this->total_max_time_us) this->total_max_time_us = duration_us; + global_recorded_us += duration_us; } void reset_period() { this->period_count = 0; diff --git a/tests/integration/test_runtime_stats.py b/tests/integration/test_runtime_stats.py index 9e93035d835..bd7f36341d2 100644 --- a/tests/integration/test_runtime_stats.py +++ b/tests/integration/test_runtime_stats.py @@ -26,6 +26,7 @@ async def test_runtime_stats( # Track component stats component_stats_found = set() + main_loop_lines: list[dict[str, str]] = [] # Patterns to match - need to handle ANSI color codes and timestamps # The log format is: [HH:MM:SS][color codes][I][tag]: message @@ -34,6 +35,14 @@ async def test_runtime_stats( component_pattern = re.compile( r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms" ) + # Main loop overhead line emitted by runtime_stats + main_loop_pattern = re.compile( + r"main_loop:\s+iters=(?P<iters>\d+),\s+" + r"active_avg=(?P<active_avg>[\d.]+)ms,\s+" + r"active_max=(?P<active_max>[\d.]+)ms,\s+" + r"active_total=(?P<active_total>[\d.]+)ms,\s+" + r"overhead_total=(?P<overhead_total>[\d.]+)ms" + ) def check_output(line: str) -> None: """Check log output for runtime stats messages.""" @@ -54,6 +63,11 @@ async def test_runtime_stats( component_name = match.group(1) component_stats_found.add(component_name) + # Check for main_loop overhead line + ml_match = main_loop_pattern.search(line) + if ml_match: + main_loop_lines.append(ml_match.groupdict()) + async with ( run_compiled(yaml_config, line_callback=check_output), api_client_connected() as client, @@ -86,3 +100,22 @@ async def test_runtime_stats( assert "template.switch" in component_stats_found, ( f"Expected template.switch stats, found: {component_stats_found}" ) + + # Verify the main_loop overhead line is emitted (at least once for + # the period section and once for the total section, per log cycle). + assert len(main_loop_lines) >= 2, ( + f"Expected at least 2 main_loop lines, got {len(main_loop_lines)}" + ) + for fields in main_loop_lines: + assert int(fields["iters"]) > 0, f"iters should be > 0: {fields}" + assert float(fields["active_total"]) > 0.0, ( + f"active_total should be > 0: {fields}" + ) + assert float(fields["active_avg"]) >= 0.0, ( + f"active_avg should be >= 0: {fields}" + ) + # overhead_total is derived and may be 0 if components dominate, + # but the field must still be present and parseable as a float. + assert float(fields["overhead_total"]) >= 0.0, ( + f"overhead_total should be >= 0: {fields}" + ) From cfe8c0eeee0fa61fa40c1d47a89203b4bdee0f1a Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:20:55 +0200 Subject: [PATCH 1485/2056] [wireguard] Bump esp_wireguard to 0.4.5 for ESP-IDF v6 (#15804) --- .clang-tidy.hash | 2 +- esphome/components/wireguard/__init__.py | 2 +- platformio.ini | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index ab526134f80..72a99675909 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815 +075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592 diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 4fdb256d0cc..e128b8476dd 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -137,7 +137,7 @@ async def to_code(config): # the '+1' modifier is relative to the device's own address that will # be automatically added to the provided list. cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}") - cg.add_library("droscy/esp_wireguard", "0.4.4") + cg.add_library("droscy/esp_wireguard", "0.4.5") await cg.register_component(var, config) diff --git a/platformio.ini b/platformio.ini index 3897db83e13..e2c7e2b097d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - droscy/esp_wireguard@0.4.4 ; wireguard + droscy/esp_wireguard@0.4.5 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = @@ -154,7 +154,7 @@ lib_deps = DNSServer ; captive_portal (Arduino built-in) makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio - droscy/esp_wireguard@0.4.4 ; wireguard + droscy/esp_wireguard@0.4.5 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = @@ -176,7 +176,7 @@ platform_packages = framework = espidf lib_deps = ${common:idf.lib_deps} - droscy/esp_wireguard@0.4.4 ; wireguard + droscy/esp_wireguard@0.4.5 ; wireguard kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word tonia/HeatpumpIR@1.0.41 ; heatpumpir build_flags = @@ -221,7 +221,7 @@ lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base - droscy/esp_wireguard@0.4.4 ; wireguard + droscy/esp_wireguard@0.4.5 ; wireguard lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} From 6a46437a5f4f4a74c61943c02759d0209e43f9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= <efilistovic@gmail.com> Date: Fri, 17 Apr 2026 13:58:39 +0300 Subject: [PATCH 1486/2056] [wifi] Guard retry_phase_to_log_string with log level check to fix warning (#15801) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/wifi/wifi_component.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7b31a22ed5b..598aee8f66e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -308,6 +308,7 @@ bool CompactString::operator==(const StringRef &other) const { /// │ - Roaming fail (RECONNECTING on other AP): counter preserved │ /// └──────────────────────────────────────────────────────────────────────┘ +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { if (phase == WiFiRetryPhase::INITIAL_CONNECT) @@ -326,6 +327,7 @@ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { return LOG_STR("RESTARTING"); return LOG_STR("UNKNOWN"); } +#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO bool WiFiComponent::went_through_explicit_hidden_phase_() const { // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase From 1a529a62aad1bafd3e658cddf980376b3123dfa1 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:17:16 +1000 Subject: [PATCH 1487/2056] [mipi_spi] Drawing fixes for native display (#15802) --- esphome/components/mipi_spi/display.py | 6 +- esphome/components/mipi_spi/mipi_spi.h | 14 +- .../mipi_spi/test_final_validate.py | 185 ++++++++++++++++++ 3 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 tests/component_tests/mipi_spi/test_final_validate.py diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 42c7ec22249..364ada90463 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -195,7 +195,7 @@ def model_schema(config): "big_endian", "little_endian", lower=True ), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), - model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_DRAW_ROUNDING, 1): power_of_two, model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( *pixel_modes, lower=True ), @@ -297,9 +297,9 @@ def _final_validate(config): buffer_size = color_depth // 8 * width * height // frac # Target a buffer size of 20kB, except for large displays, which shouldn't end up here - fraction = min(20000.0, buffer_size // 16) / buffer_size + fraction = min(20000.0, buffer_size // 4) / buffer_size config[CONF_BUFFER_SIZE] = 1.0 / next( - x for x in range(2, 17) if fraction >= 1 / x + (x for x in range(2, 8) if fraction >= 1 / x), 8 ) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 2242be6c17b..f2923458937 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -546,13 +546,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS } // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of // the display height, - for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); - this->start_line_ += this->get_height_internal() / FRACTION) { + auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING; + for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) { #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE auto lap = millis(); #endif - this->end_line_ = - clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal()); + this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal()); if (this->auto_clear_enabled_) { this->clear(); } @@ -574,12 +573,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS // Some chips require that the drawing window be aligned on certain boundaries this->x_low_ = this->x_low_ / ROUNDING * ROUNDING; this->y_low_ = this->y_low_ / ROUNDING * ROUNDING; - this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; - this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; + this->x_high_ = round_buffer(this->x_high_ + 1) - 1; + this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1); int w = this->x_high_ - this->x_low_ + 1; int h = this->y_high_ - this->y_low_ + 1; this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, - this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w); + this->y_low_ - this->start_line_, + round_buffer(this->get_width_internal()) - w - this->x_low_); // invalidate watermarks this->x_low_ = this->get_width_internal(); this->y_low_ = this->get_height_internal(); diff --git a/tests/component_tests/mipi_spi/test_final_validate.py b/tests/component_tests/mipi_spi/test_final_validate.py new file mode 100644 index 00000000000..8c45b47752c --- /dev/null +++ b/tests/component_tests/mipi_spi/test_final_validate.py @@ -0,0 +1,185 @@ +"""Tests for the _final_validate buffer size calculation in mipi_spi.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32 +from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA +from esphome.const import CONF_BUFFER_SIZE, PlatformFramework +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def _validated(config: ConfigType) -> ConfigType: + """Run the component config schema followed by the final validation.""" + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config + + +def _custom_config( + width: int, + height: int, + color_depth: str | int | None = None, + **extra: Any, +) -> ConfigType: + """Build a minimal valid custom-model config with the given dimensions.""" + config: ConfigType = { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": width, "height": height}, + "init_sequence": [[0xA0, 0x01]], + } + if color_depth is not None: + config["color_depth"] = color_depth + config.update(extra) + return config + + +# The auto buffer-size selection inside _final_validate targets ~20 kB of +# pixel buffer. For a buffer of ``depth_bytes * width * height``, it picks the +# smallest integer ``x`` in range(2, 8) such that +# ``min(20000, buffer // 4) / buffer >= 1 / x`` (falling back to ``x = 8``). +# The test cases below cover the full range of possible outcomes (1/4 .. 1/8). +@pytest.mark.parametrize( + ("width", "height", "color_depth", "expected"), + [ + # 16-bit color depth -- buffer = 2 * width * height + # 128*160*2 = 40960 B -> fraction = 10240/40960 = 0.25 -> x = 4 + pytest.param(128, 160, "16bit", 1.0 / 4, id="16bit_tiny"), + # 200*224*2 = 89600 B -> fraction = 20000/89600 ≈ 0.2232 -> x = 5 + pytest.param(200, 224, "16bit", 1.0 / 5, id="16bit_small"), + # 240*224*2 = 107520 B -> fraction ≈ 0.1860 -> x = 6 + pytest.param(240, 224, "16bit", 1.0 / 6, id="16bit_medium"), + # 200*320*2 = 128000 B -> fraction = 0.15625 -> x = 7 + pytest.param(200, 320, "16bit", 1.0 / 7, id="16bit_large"), + # 240*320*2 = 153600 B -> fraction ≈ 0.1302 -> default x = 8 + pytest.param(240, 320, "16bit", 1.0 / 8, id="16bit_xlarge"), + # 320*480*2 = 307200 B -> fraction ≈ 0.0651 -> default x = 8 + pytest.param(320, 480, "16bit", 1.0 / 8, id="16bit_huge"), + # 8-bit color depth -- buffer = width * height + # 320*240 = 76800 B -> fraction = 19200/76800 = 0.25 -> x = 4 + pytest.param(320, 240, "8bit", 1.0 / 4, id="8bit_tiny"), + # 400*224 = 89600 B -> fraction ≈ 0.2232 -> x = 5 + pytest.param(400, 224, "8bit", 1.0 / 5, id="8bit_small"), + # 480*224 = 107520 B -> fraction ≈ 0.1860 -> x = 6 + pytest.param(480, 224, "8bit", 1.0 / 6, id="8bit_medium"), + # 400*320 = 128000 B -> fraction = 0.15625 -> x = 7 + pytest.param(400, 320, "8bit", 1.0 / 7, id="8bit_large"), + # 480*320 = 153600 B -> fraction ≈ 0.1302 -> default x = 8 + pytest.param(480, 320, "8bit", 1.0 / 8, id="8bit_xlarge"), + ], +) +def test_buffer_size_auto_selected( + width: int, + height: int, + color_depth: str, + expected: float, + set_core_config: SetCoreConfigCallable, +) -> None: + """Without PSRAM or an explicit buffer_size, a fraction is chosen from the display size. + + Without any drawing method and without LVGL, final validation also auto-enables + ``show_test_card``, which in turn makes the component require a buffer and therefore + triggers the buffer-size selection path. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + config = _validated(_custom_config(width, height, color_depth)) + + # Sanity check: final validation should have enabled the test card for us, + # which is what causes the buffer-size calculation to actually run. + assert config.get(CONF_SHOW_TEST_CARD) is True + assert config[CONF_BUFFER_SIZE] == pytest.approx(expected) + + +@pytest.mark.parametrize( + "buffer_size", + [0.125, 0.25, 0.5, 1.0], + ids=["one_eighth", "one_quarter", "half", "full"], +) +def test_explicit_buffer_size_is_preserved( + buffer_size: float, + set_core_config: SetCoreConfigCallable, +) -> None: + """An explicitly configured buffer_size is never overridden by final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + config = _validated( + _custom_config(240, 320, "16bit", buffer_size=buffer_size), + ) + + assert config[CONF_BUFFER_SIZE] == pytest.approx(buffer_size) + + +def test_buffer_size_not_set_when_psram_enabled( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """When PSRAM is enabled the auto buffer-size selection is skipped.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + # Presence of the psram domain in the full config is what _final_validate checks. + set_component_config("psram", True) + + config = _validated(_custom_config(240, 320, "16bit")) + + assert CONF_BUFFER_SIZE not in config + + +def test_buffer_size_not_set_when_buffer_not_required( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """With LVGL present and no drawing methods, no buffer fraction is chosen. + + LVGL suppresses the automatic show_test_card injection, which means + ``requires_buffer`` is False and the early-return branch fires. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + set_component_config("lvgl", []) + + config = _validated(_custom_config(240, 320, "16bit")) + + assert CONF_BUFFER_SIZE not in config + # And no test card should have been auto-enabled either. + assert not config.get(CONF_SHOW_TEST_CARD) + + +def test_buffer_size_selected_when_lvgl_with_test_card( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """LVGL present + an explicit drawing method still triggers buffer sizing. + + When LVGL is enabled, ``show_test_card`` is not injected automatically, + but users can still request it explicitly -- in that case ``requires_buffer`` + is True and the buffer-size heuristic still runs. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + set_component_config("lvgl", []) + + # 128x160 @ 16bit -> expected 1/4 (see test_buffer_size_auto_selected). + config = _validated( + _custom_config(128, 160, "16bit", show_test_card=True), + ) + + assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4) From b018ac67bcfdf8e8f826af385c8dd65aed9d41b0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:11:05 +1000 Subject: [PATCH 1488/2056] [image] Fix byte order handling (#15800) --- esphome/components/image/__init__.py | 17 ++++++++++------- esphome/components/image/image.cpp | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4a5fcc385e8..7db50597e6f 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -28,7 +28,6 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -676,12 +675,16 @@ def _final_validate(config): :param config: :return: """ - fv = full_config.get() - if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config): - config = config.copy() - for c in config: - if not c.get(CONF_BYTE_ORDER): - c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" + config = config.copy() + for c in config: + if byte_order := c.get(CONF_BYTE_ORDER): + if byte_order == "BIG_ENDIAN": + _LOGGER.warning( + "The image '%s' is configured with big-endian byte order, little-endian is expected", + c.get(CONF_FILE), + ) + else: + c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" return config diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index a6f9e35e2e5..5b4ed6968c6 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const { } Color Image::get_rgb565_pixel_(int x, int y) const { const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8; - uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); + uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos)); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; From 523c6f2376e491fe5f4391041ff9e0039ab3eb7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 02:45:50 -1000 Subject: [PATCH 1489/2056] [core] coerce set_interval(0) / update_interval: 0ms to 1ms (#15799) --- esphome/config_validation.py | 21 +++++- esphome/core/scheduler.cpp | 13 ++++ .../scheduler_interval_zero_coerced.yaml | 27 ++++++++ .../test_scheduler_interval_zero_coerced.py | 67 +++++++++++++++++++ tests/unit_tests/test_config_validation.py | 28 ++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/scheduler_interval_zero_coerced.yaml create mode 100644 tests/integration/test_scheduler_interval_zero_coerced.py diff --git a/esphome/config_validation.py b/esphome/config_validation.py index e6b0cb7ee23..fbafc5cb07b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -943,7 +943,26 @@ def time_period_in_minutes_(value): def update_interval(value): if value == "never": return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN) - return positive_time_period_milliseconds(value) + result = positive_time_period_milliseconds(value) + # 0ms was historically (mis)used as a pseudo-loop() mechanism for + # PollingComponents. Under the hood it calls set_interval(0), which + # causes Scheduler::call() to spin (WDT reset in the field). Coerce + # to 1ms so existing configs keep working at ~1kHz instead of + # spinning. Don't hard-fail so configs don't break on upgrade; + # authors should migrate to HighFrequencyLoopRequester (C++) for + # true run-every-loop behaviour. + if result.total_milliseconds == 0: + _LOGGER.warning( + "update_interval of 0ms is not supported - coercing to 1ms. " + "A literal 0ms schedule would spin the main loop (the scheduled " + "item would always be due, so the scheduler would never yield " + "back) and trigger a watchdog reset. Set update_interval to a " + "non-zero value such as 1ms or higher. (Custom C++ components " + "that need true run-every-loop behaviour should override loop() " + "with HighFrequencyLoopRequester instead.)" + ) + return TimePeriodMilliseconds(milliseconds=1) + return result time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3e75a680643..7e6ad19ac70 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -144,6 +144,19 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } + // An interval of 0 means "fire every tick forever," which is misuse: the + // item would always be due, causing Scheduler::call() to spin and starve + // the main loop (WDT reset in the field). Coerce to 1ms so existing code + // using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz, + // and warn so authors can migrate to HighFrequencyLoopRequester which is + // the intended mechanism for running fast in the main loop. Zero-delay + // timeouts (defer) remain legitimate one-shots and are not affected. + if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] { + ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)", + component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?")); + delay = 1; + } + // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check LockGuard guard{this->lock_}; diff --git a/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml b/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml new file mode 100644 index 00000000000..13be55d6176 --- /dev/null +++ b/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml @@ -0,0 +1,27 @@ +esphome: + name: sched-interval-zero + +host: +api: +logger: + level: DEBUG + +globals: + - id: fire_count + type: int + initial_value: "0" + +interval: + # Deliberately configure 0ms — this path goes through the C++ + # Scheduler::set_timer_common_ coercion (not the Python cv.update_interval + # path, since interval: doesn't call cv.update_interval — it's an intervals + # component schema, not a PollingComponent's update_interval). + # Expected: scheduler coerces to 1ms at registration, emits ESP_LOGE, + # fires at ~1kHz instead of spinning. + - interval: 0ms + then: + - lambda: |- + id(fire_count) += 1; + if (id(fire_count) == 50) { + ESP_LOGI("test", "ZERO_INTERVAL_50_FIRES_REACHED"); + } diff --git a/tests/integration/test_scheduler_interval_zero_coerced.py b/tests/integration/test_scheduler_interval_zero_coerced.py new file mode 100644 index 00000000000..f71c0f7281f --- /dev/null +++ b/tests/integration/test_scheduler_interval_zero_coerced.py @@ -0,0 +1,67 @@ +"""Test that Scheduler::set_timer_common_ coerces interval=0 to 1ms. + +Regression test for the scheduler busy-loop when interval=0 was passed +literally. Without the coercion, Scheduler::call() would spin forever +because the item's next_execution == now_64 after re-scheduling, failing +the loop's `> now_64` break condition. The device would fail to yield +back to the main loop and trigger a WDT reset. + +With the coercion, interval=0 becomes interval=1 and the scheduler +fires at ~1kHz (bounded by the loop), the main loop continues to run, +and the device stays responsive to API calls. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_interval_zero_coerced( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """interval=0ms must be coerced to 1ms and not starve the main loop.""" + loop = asyncio.get_running_loop() + reached_50: asyncio.Future[None] = loop.create_future() + coerce_warning: asyncio.Future[None] = loop.create_future() + + def on_log_line(line: str) -> None: + if "ZERO_INTERVAL_50_FIRES_REACHED" in line and not reached_50.done(): + reached_50.set_result(None) + if "would spin main loop" in line and not coerce_warning.done(): + coerce_warning.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # The API-client connection itself is evidence that the main loop + # is not starved — if set_interval(0) were spinning we could not + # get here at all. + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-interval-zero" + + # Coerce warning must fire at registration + try: + await asyncio.wait_for(coerce_warning, timeout=5.0) + except TimeoutError: + pytest.fail("Expected coerce warning 'would spin main loop' not seen") + + # The coerced 1ms interval should fire 50 times quickly — this + # confirms the callback actually runs (not just registered) and the + # scheduler yields back to the main loop each time. + try: + await asyncio.wait_for(reached_50, timeout=5.0) + except TimeoutError: + pytest.fail( + "Coerced interval=0→1ms did not reach 50 fires within 5s, " + "which would indicate either the coercion failed or the " + "main loop is still being starved." + ) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index ac84ce7cc89..f038272d8b0 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -24,6 +24,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, + SCHEDULER_DONT_RUN, ) from esphome.core import CORE, HexInt, Lambda @@ -765,3 +766,30 @@ def test_percentage_validators__raw_number_above_one_without_percent_sign( config_validation.unbounded_percentage(value) with pytest.raises(Invalid, match="percent sign"): config_validation.unbounded_possibly_negative_percentage(value) + + +def test_update_interval__coerces_zero_to_one_ms( + caplog: pytest.LogCaptureFixture, +) -> None: + """update_interval: 0ms must be coerced to 1ms (not rejected) because a + literal 0ms schedule causes Scheduler::call() to spin. Coercion keeps + existing configs compiling on upgrade while emitting a user-facing + warning that directs them to set a non-zero value.""" + with caplog.at_level("WARNING"): + result = config_validation.update_interval("0ms") + assert result.total_milliseconds == 1 + assert "update_interval of 0ms is not supported" in caplog.text + assert "1ms" in caplog.text + + +def test_update_interval__preserves_nonzero_values() -> None: + """Non-zero update_interval values must pass through unchanged.""" + assert config_validation.update_interval("1ms").total_milliseconds == 1 + assert config_validation.update_interval("50ms").total_milliseconds == 50 + assert config_validation.update_interval("60s").total_milliseconds == 60000 + + +def test_update_interval__never_passes_through() -> None: + """update_interval: never must still map to SCHEDULER_DONT_RUN.""" + result = config_validation.update_interval("never") + assert result.total_milliseconds == SCHEDULER_DONT_RUN From d4fe46bb24be368e882aa3b78655edc95d82ed6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 02:46:12 -1000 Subject: [PATCH 1490/2056] [core] Expose App.wake_loop_isrsafe() on ESP8266 (#15797) --- esphome/core/application.h | 3 +++ esphome/core/wake.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/core/application.h b/esphome/core/application.h index bc40fe0c7ee..82f399b2d67 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -340,6 +340,9 @@ class Application { #if defined(USE_ESP32) || defined(USE_LIBRETINY) /// Wake from ISR (ESP32 and LibreTiny). static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); } +#elif defined(USE_ESP8266) + /// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR. + static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); } #endif /// Wake from any context (ISR, thread, callback). diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 5733ee65f6c..77a38d429e5 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -77,6 +77,9 @@ void wake_loop_any_context(); /// Non-ISR: always inline. inline void wake_loop_threadsafe() { wake_loop_impl(); } +/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR. +inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); } + namespace internal { inline void wakeable_delay(uint32_t ms) { if (ms == 0) { From bcbfc843ae77ec72af2bc2d80b070c07923d8096 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:05:30 -0400 Subject: [PATCH 1491/2056] [ethernet] Fix SPI3_HOST default breaking compile on variants without SPI3 (#15809) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/ethernet/ethernet_component.h | 2 +- .../ethernet/test.esp32-c3-idf.yaml | 19 +++++++++++++++++++ ...720.esp32-idf.yaml => test.esp32-idf.yaml} | 0 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/components/ethernet/test.esp32-c3-idf.yaml rename tests/components/ethernet/{test-lan8720.esp32-idf.yaml => test.esp32-idf.yaml} (100%) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 3a878423156..17c84ee9543 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -221,7 +221,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; - spi_host_device_t interface_{SPI3_HOST}; + spi_host_device_t interface_{SPI2_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif diff --git a/tests/components/ethernet/test.esp32-c3-idf.yaml b/tests/components/ethernet/test.esp32-c3-idf.yaml new file mode 100644 index 00000000000..b7b95875c6d --- /dev/null +++ b/tests/components/ethernet/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +ethernet: + type: W5500 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 2 + cs_pin: 10 + interrupt_pin: 3 + reset_pin: 4 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-lan8720.esp32-idf.yaml b/tests/components/ethernet/test.esp32-idf.yaml similarity index 100% rename from tests/components/ethernet/test-lan8720.esp32-idf.yaml rename to tests/components/ethernet/test.esp32-idf.yaml From 34c35c84d5c0109cb45583a10c0ed9664462013e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 09:31:31 -0500 Subject: [PATCH 1492/2056] [core] Fix DelayAction compile error with non-const reference args (#15814) --- esphome/core/base_automation.h | 4 +++- tests/components/http_request/http_request.yaml | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 11133d39739..17f937d10d9 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -205,7 +205,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon } else { // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay - auto f = [this, x...]() { this->play_next_(x...); }; + // `mutable` is required so captured copies of non-const reference args (e.g. std::string&) + // are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&` + auto f = [this, x...]() mutable { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index 13ca5ceba03..ef67671c91d 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -45,6 +45,11 @@ esphome: args: - response->status_code - body.c_str() + - delay: 1s + - logger.log: + format: "After delay, body still: %s" + args: + - body.c_str() http_request: useragent: esphome/tagreader From 70ea52716172444dda1d4102d0857a20370c445d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:17:51 -0500 Subject: [PATCH 1493/2056] Bump ruff from 0.15.10 to 0.15.11 (#15790) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ac4f0049f83..e492d355954 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.10 + rev: v0.15.11 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 18d0461e830..bb98375cb65 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.10 # also change in .pre-commit-config.yaml when updating +ruff==0.15.11 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From b1b0005574b3a54f8aeead1d9eee96f586d72aa6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:14:54 -0400 Subject: [PATCH 1494/2056] [esp32] Downgrade unneeded `ignore_pin_validation_error` to a warning (#15811) --- esphome/components/esp32/gpio.py | 12 +++- tests/component_tests/esp32/test_esp32.py | 79 ++++++++++++++++++++++- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index a7180cbcd7e..36dd44155ae 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -172,10 +172,16 @@ def validate_gpio_pin(pin): exc, ) else: - # Throw an exception if used for a pin that would not have resulted - # in a validation error anyway! + # `ignore_pin_validation_error` only suppresses an error raised by the + # variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin + # numbers). If that didn't raise, the option is a no-op -- warn so the + # user can clean it up, but don't block the build. if ignore_pin_validation_warning: - raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin") + _LOGGER.warning( + "GPIO%d has no validation errors to ignore; " + "remove `ignore_pin_validation_error: true` from this pin.", + pin[CONF_NUMBER], + ) return pin diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index bd4f9828ce5..ac492e27529 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -8,10 +8,16 @@ from typing import Any import pytest -from esphome.components.esp32 import VARIANTS -from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS +from esphome.components.esp32 import VARIANT_ESP32, VARIANTS +from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT +from esphome.components.esp32.gpio import validate_gpio_pin import esphome.config_validation as cv -from esphome.const import CONF_ESPHOME, PlatformFramework +from esphome.const import ( + CONF_ESPHOME, + CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_NUMBER, + PlatformFramework, +) from esphome.core import CORE from tests.component_tests.types import SetCoreConfigCallable @@ -149,6 +155,73 @@ def test_execute_from_psram_p4_sdkconfig( assert "CONFIG_SPIRAM_RODATA" not in sdkconfig +def test_ignore_pin_validation_error_on_clean_pin_warns( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A pin that passes validation but sets `ignore_pin_validation_error: true` + should log a warning nudging the user to remove the flag, and not raise.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 4 + assert "GPIO4 has no validation errors to ignore" in caplog.text + + +def test_ignore_pin_validation_error_on_dirty_pin_suppresses( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A pin that fails validation with `ignore_pin_validation_error: true` should + log the suppression warning and not raise (existing behavior).""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + # GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid + pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 6 + assert "Ignoring validation error on pin 6" in caplog.text + + +def test_dirty_pin_without_ignore_flag_raises( + set_core_config: SetCoreConfigCallable, +) -> None: + """A pin that fails validation without the ignore flag should still raise.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + with pytest.raises(cv.Invalid, match="flash interface"): + validate_gpio_pin(pin) + + +def test_clean_pin_without_ignore_flag_does_not_warn( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A clean pin without the ignore flag should pass silently.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 4 + assert "has no validation errors to ignore" not in caplog.text + + def test_execute_from_psram_disabled_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From 290e213cd088c34f6156b6d99dc41eaa858e093f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 18 Apr 2026 06:41:33 +1000 Subject: [PATCH 1495/2056] [mipi_spi] Add Sunton ESP32-2424S012 (#15812) --- esphome/components/mipi_spi/models/cyd.py | 11 ++++++++++- esphome/components/mipi_spi/models/ili.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi_spi/models/cyd.py b/esphome/components/mipi_spi/models/cyd.py index 7229412f18c..0a35cb4feea 100644 --- a/esphome/components/mipi_spi/models/cyd.py +++ b/esphome/components/mipi_spi/models/cyd.py @@ -1,4 +1,6 @@ -from .ili import ILI9341, ILI9342, ST7789V +from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER + +from .ili import GC9A01A, ILI9341, ILI9342, ST7789V ILI9341.extend( # ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller @@ -43,3 +45,10 @@ ILI9342.extend( (0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction ) ) + +GC9A01A.extend( + "ESP32-2424S012", + invert_colors=True, + cs_pin=10, + dc_pin={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True}, +) diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index 6b672b08594..ae6accb9073 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -555,7 +555,7 @@ ST7789V = DriverChip( ), ), ) -DriverChip( +GC9A01A = DriverChip( "GC9A01A", mirror_x=True, width=240, From 1bf455cfbb0308e3d86ae3d7f9e662f27f32cda1 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 18 Apr 2026 06:42:45 +1000 Subject: [PATCH 1496/2056] [runtime_image] Fix RGB order (#15813) --- esphome/components/runtime_image/runtime_image.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index fa42b53496b..4c7f1bfb6f3 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { uint32_t pos = this->get_position_(x, y); Color mapped_color = color; this->map_chroma_key(mapped_color); - this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 0] = mapped_color.b; this->buffer_[pos + 1] = mapped_color.g; - this->buffer_[pos + 2] = mapped_color.b; + this->buffer_[pos + 2] = mapped_color.r; if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 3] = color.w; } From 6ebe1e92eb64cf099eb4f1ae97dba5fc1471905a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 17:54:12 -0500 Subject: [PATCH 1497/2056] [ci] Scope local pylint pre-commit hook to esphome/ (#15818) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e492d355954..d9b7df6ec53 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,7 @@ repos: entry: python3 script/run-in-env.py pylint language: system types: [python] + files: ^esphome/.+\.py$ - id: clang-tidy-hash name: Update clang-tidy hash entry: python script/clang_tidy_hash.py --update-if-changed From 562ce541a097c3ab79723553a091db07e1b847ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 17:54:24 -0500 Subject: [PATCH 1498/2056] [bme680_bsec] [bme68x_bsec2] Mark the two BSEC variants as mutually exclusive (#15826) --- esphome/components/bme680_bsec/__init__.py | 1 + esphome/components/bme68x_bsec2/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index a86e061cd4e..2365f8d1073 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensor", "text_sensor"] +CONFLICTS_WITH = ["bme68x_bsec2"] MULTI_CONF = True CONF_BME680_BSEC_ID = "bme680_bsec_id" diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index b56217fac18..5083d283ef3 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( ) CODEOWNERS = ["@neffs", "@kbx81"] +CONFLICTS_WITH = ["bme680_bsec"] DOMAIN = "bme68x_bsec2" From d3691c7ca5d06beb1ba63be18dee66a6e3c7a47d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:17:28 +1000 Subject: [PATCH 1499/2056] [lvgl] Fix crash with snow on rotated display (#15822) --- esphome/components/lvgl/lvgl_esphome.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index ce9b013dcf1..d8248e4aa4e 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -642,26 +642,28 @@ void LvglComponent::write_random_() { int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) iterations = 1; + int16_t width = lv_display_get_horizontal_resolution(this->disp_); + int16_t height = lv_display_get_vertical_resolution(this->disp_); while (iterations-- != 0) { - int32_t col = random_uint32() % this->width_; + int32_t col = random_uint32() % width; col = col / this->draw_rounding * this->draw_rounding; - int32_t row = random_uint32() % this->height_; + int32_t row = random_uint32() % height; row = row / this->draw_rounding * this->draw_rounding; // size will be between 8 and 32, and a multiple of draw_rounding int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding; - lv_area_t area{col, row, col + size - 1, row + size - 1}; + lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1}; // clip to display bounds just in case - if (area.x2 >= this->width_) - area.x2 = this->width_ - 1; - if (area.y2 >= this->height_) - area.y2 = this->height_ - 1; + if (area.x2 >= width) + area.x2 = width - 1; + if (area.y2 >= height) + area.y2 = height - 1; // line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2; for (size_t i = 0; i != line_len; i++) { - ((uint32_t *) (this->draw_buf_))[i] = random_uint32(); + reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32(); } - this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_); + this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_)); } } From df72aa26c0a11a00b581ef65f5c528c840a20f70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 06:58:54 -0500 Subject: [PATCH 1500/2056] [core] Feed WDT unconditionally in main loop to fix empty-config panic (#15830) --- esphome/core/application.h | 22 +++++++++++++--------- esphome/core/scheduler.cpp | 5 ++++- esphome/core/scheduler.h | 3 ++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 82f399b2d67..9252a474464 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -402,7 +402,7 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. @@ -546,18 +546,15 @@ inline void Application::drain_wake_notifications_() { } #endif // USE_HOST -inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { #ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif - // Process scheduled tasks. Scheduler::call now feeds the watchdog itself - // after each scheduled item that actually runs, so we no longer need an - // unconditional feed here — when Scheduler::call has no work to do, the - // only elapsed time is a sleep wake + a few instructions, and when it does - // have work, it fed the wdt as it went. - this->scheduler.call(loop_start_time); + // Scheduler::call feeds the WDT per item and returns the timestamp of the + // last fired item, or the input unchanged when nothing ran. + uint32_t last_op_end_time = this->scheduler.call(loop_start_time); // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions @@ -575,6 +572,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ // Mark that we're in the loop for safe reentrant modifications this->in_loop_ = true; + return last_op_end_time; } inline void ESPHOME_ALWAYS_INLINE Application::loop() { @@ -592,7 +590,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Get the initial loop time at the start uint32_t last_op_end_time = millis(); - this->before_loop_tasks_(last_op_end_time); + // Returned timestamp keeps us monotonic with last_wdt_feed_ (advanced by + // the scheduler's per-item feeds) without an extra millis() call. + last_op_end_time = this->before_loop_tasks_(last_op_end_time); + // Guarantee a WDT touch every tick — covers configs with no looping + // components and no scheduler work, where the per-item / per-component + // feeds never fire. Rate-limited inline fast path, ~free when unneeded. + this->feed_wdt_with_time(last_op_end_time); #ifdef USE_RUNTIME_STATS uint32_t loop_before_end_us = micros(); uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap; diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7e6ad19ac70..b0eaa670ac2 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -533,7 +533,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) { } #endif /* not ESPHOME_THREAD_SINGLE */ -void HOT Scheduler::call(uint32_t now) { +uint32_t HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ @@ -703,6 +703,9 @@ void HOT Scheduler::call(uint32_t now) { this->debug_verify_no_leak_(); } #endif + // execute_item_() advances `now` as items fire; return it so the caller + // stays monotonic with last_wdt_feed_. + return now; } void HOT Scheduler::process_to_add_slow_path_() { LockGuard guard{this->lock_}; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 7634b3bd087..b0ce365a6f8 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -129,7 +129,8 @@ class Scheduler { // Execute all scheduled items that are ready // @param now Fresh timestamp from millis() - must not be stale/cached - void call(uint32_t now); + // @return Timestamp of the last item that ran, or `now` unchanged if none ran. + uint32_t call(uint32_t now); // Move items from to_add_ into the main heap. // IMPORTANT: This method should only be called from the main thread (loop task). From ec9d59f3dc61535c069b8bd769b205d2b9b308d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:32:36 +0000 Subject: [PATCH 1501/2056] Bump aioesphomeapi from 44.16.0 to 44.16.1 (#15836) 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 36c81e25bf9..1623876cb56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.16.0 +aioesphomeapi==44.16.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From ccb53e34ca677659e78abdfe0d72ac95be179372 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 08:04:51 -0500 Subject: [PATCH 1502/2056] [core] Default PollingComponent() to 1ms when codegen is bypassed (#15831) --- esphome/core/component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 6fbb0d5c06e..717ca362576 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -601,7 +601,7 @@ class Component { */ class PollingComponent : public Component { public: - PollingComponent() : PollingComponent(0) {} + PollingComponent() : PollingComponent(1) {} /** Initialize this polling component with the given update interval in ms. * From b293be23b050a1da4efc5a2652b70c6168477b5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 08:11:38 -0500 Subject: [PATCH 1503/2056] [ci] Honor CONFLICTS_WITH when grouping component tests (#15834) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- script/helpers.py | 105 ++++++++++++++++++++ script/split_components_for_ci.py | 7 +- script/test_build_components.py | 9 +- tests/script/test_helpers.py | 156 ++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 9f5ea7894cd..7a6d7ecef68 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,6 +1,8 @@ from __future__ import annotations +import ast from collections.abc import Callable +from dataclasses import dataclass, field from functools import cache import hashlib import json @@ -139,6 +141,109 @@ def get_component_test_files( return list(tests_dir.glob("test.*.yaml")) +@dataclass(frozen=True) +class ComponentMetadata: + """Statically-parsed AUTO_LOAD and CONFLICTS_WITH declarations.""" + + auto_load: frozenset[str] = field(default_factory=frozenset) + conflicts_with: frozenset[str] = field(default_factory=frozenset) + + +@cache +def parse_component_metadata(name: str) -> ComponentMetadata: + """Return the AUTO_LOAD / CONFLICTS_WITH declarations for a component. + + Parses the component's ``esphome/components/<name>/__init__.py`` statically. + Callable forms (``def AUTO_LOAD():``) require runtime imports and are + reported as empty -- safe for conflict detection since they cannot be + evaluated without executing the module. + """ + init_file = Path(root_path) / ESPHOME_COMPONENTS_PATH / name / "__init__.py" + if not init_file.exists(): + return ComponentMetadata() + try: + tree = ast.parse(init_file.read_text(encoding="utf-8")) + except (OSError, SyntaxError, UnicodeError): + return ComponentMetadata() + fields: dict[str, frozenset[str]] = { + "AUTO_LOAD": frozenset(), + "CONFLICTS_WITH": frozenset(), + } + for node in tree.body: + if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.List): + continue + for target in node.targets: + if not isinstance(target, ast.Name) or target.id not in fields: + continue + fields[target.id] = frozenset( + e.value + for e in node.value.elts + if isinstance(e, ast.Constant) and isinstance(e.value, str) + ) + return ComponentMetadata( + auto_load=fields["AUTO_LOAD"], + conflicts_with=fields["CONFLICTS_WITH"], + ) + + +@dataclass +class _ConflictWalk: + loaded: set[str] + rejects: set[str] + + +def split_conflicting_groups( + grouped_components: dict[tuple[str, str], list[str]], +) -> dict[tuple[str, str], list[str]]: + """Split groups so components declaring mutual CONFLICTS_WITH end up in separate builds. + + A conflict propagates through AUTO_LOAD: if X declares CONFLICTS_WITH=[Y] + and Z auto-loads Y, then X and Z conflict (e.g. bme680_bsec vs. + bme68x_bsec2_i2c which auto-loads bme68x_bsec2). Only components that + appear in the batch (and their AUTO_LOAD closures) are parsed. The + conflict relation is treated as symmetric even when only one side + declares it (e.g. ethernet rejects wifi but wifi does not declare the + reverse). + """ + batch = {c for comps in grouped_components.values() for c in comps} + + walks: dict[str, _ConflictWalk] = {} + for comp in batch: + walk = _ConflictWalk(loaded={comp}, rejects=set()) + stack = [comp] + while stack: + metadata = parse_component_metadata(stack.pop()) + walk.rejects |= metadata.conflicts_with + new = metadata.auto_load - walk.loaded + walk.loaded |= new + stack.extend(new) + walks[comp] = walk + + def conflicts(a: str, b: str) -> bool: + wa, wb = walks[a], walks[b] + return not wa.rejects.isdisjoint(wb.loaded) or not wb.rejects.isdisjoint( + wa.loaded + ) + + result: dict[tuple[str, str], list[str]] = {} + for (platform, signature), components in grouped_components.items(): + buckets: list[list[str]] = [] + for comp in components: + for bucket in buckets: + if not any(conflicts(comp, other) for other in bucket): + bucket.append(comp) + break + else: + buckets.append([comp]) + if len(buckets) == 1: + result[(platform, signature)] = buckets[0] + continue + for index, bucket in enumerate(buckets): + key = signature if index == 0 else f"{signature}__conflict{index}" + result[(platform, key)] = bucket + return result + + def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str: prefix = "".join(color) if isinstance(color, tuple) else color suffix = colorama.Style.RESET_ALL if reset else "" diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index 65d09efb9bb..d95cdcbe815 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -28,7 +28,7 @@ from script.analyze_component_buses import ( create_grouping_signature, merge_compatible_bus_groups, ) -from script.helpers import get_component_test_files +from script.helpers import get_component_test_files, split_conflicting_groups # Weighting for batch creation # Isolated components can't be grouped/merged, so they count as 10x @@ -145,6 +145,11 @@ def create_intelligent_batches( # improving the efficiency of test_build_components.py grouping signature_groups = merge_compatible_bus_groups(signature_groups) + # Split groups containing mutually-incompatible components (CONFLICTS_WITH). + # Without this, batch weighting assumes the group is one build when it will + # actually be split into two at build time -- throwing off CI distribution. + signature_groups = split_conflicting_groups(signature_groups) + # Create batches by keeping signature groups together # Components with the same signature stay in the same batches batches = [] diff --git a/script/test_build_components.py b/script/test_build_components.py index e369b0364e6..82d05f78b23 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -39,7 +39,7 @@ from script.analyze_component_buses import ( merge_compatible_bus_groups, uses_local_file_references, ) -from script.helpers import get_component_test_files +from script.helpers import get_component_test_files, split_conflicting_groups from script.merge_component_configs import merge_component_configs @@ -675,6 +675,13 @@ def run_grouped_component_tests( # as long as they don't have conflicting configurations for the same bus type grouped_components = merge_compatible_bus_groups(grouped_components) + # Split groups that contain components declaring CONFLICTS_WITH each other. + # The bus-level merge above only considers shared bus configs; components + # with the same bus signature (e.g. both I2C) can still be mutually + # incompatible (e.g. bme680_bsec vs. bme68x_bsec2_i2c which auto-loads + # bme68x_bsec2). Those must end up in separate builds. + grouped_components = split_conflicting_groups(grouped_components) + # Print detailed grouping plan print("\nGrouping Plan:") print("-" * 80) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 948aabaa669..db0d2908f47 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1468,3 +1468,159 @@ def test_cache_miss_corrupted_json( result = helpers.create_components_graph() # Should handle corruption gracefully and rebuild assert result == {} + + +# --------------------------------------------------------------------------- +# parse_component_metadata / split_conflicting_groups +# --------------------------------------------------------------------------- + + +@pytest.fixture +def fake_components(tmp_path: Path) -> Path: + """Create a fake esphome/components/ tree and return the repo root. + + Component layout (tested against split_conflicting_groups): + + alpha -- CONFLICTS_WITH=["beta"] + beta -- CONFLICTS_WITH=["alpha"] + beta_variant -- AUTO_LOAD=["beta"] + gamma -- (no metadata) + one_sided -- CONFLICTS_WITH=["plain"] (plain does not reject back) + plain -- no CONFLICTS_WITH + callable_auto -- AUTO_LOAD is a function (not a list literal) -> ignored + broken -- __init__.py has a SyntaxError + """ + components = tmp_path / "esphome" / "components" + components.mkdir(parents=True) + + def write(name: str, body: str) -> None: + (components / name).mkdir() + (components / name / "__init__.py").write_text(body) + + write("alpha", 'CONFLICTS_WITH = ["beta"]\n') + write("beta", 'CONFLICTS_WITH = ["alpha"]\n') + write("beta_variant", 'AUTO_LOAD = ["beta"]\n') + write("gamma", "") + write("one_sided", 'CONFLICTS_WITH = ["plain"]\n') + write("plain", "") + write("callable_auto", "def AUTO_LOAD():\n return ['beta']\n") + write("broken", "this is not valid python !!!") + helpers.parse_component_metadata.cache_clear() + return tmp_path + + +def test_parse_component_metadata_list_literals( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + meta = helpers.parse_component_metadata("alpha") + assert meta.conflicts_with == frozenset({"beta"}) + assert meta.auto_load == frozenset() + + variant = helpers.parse_component_metadata("beta_variant") + assert variant.auto_load == frozenset({"beta"}) + assert variant.conflicts_with == frozenset() + + +def test_parse_component_metadata_missing_empty_and_callable( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + # Unknown component -> empty metadata, not an error. + unknown = helpers.parse_component_metadata("does_not_exist") + assert unknown == helpers.ComponentMetadata() + + # Empty __init__.py -> empty metadata. + assert helpers.parse_component_metadata("gamma") == helpers.ComponentMetadata() + + # Callable AUTO_LOAD cannot be statically evaluated -> empty. + callable_meta = helpers.parse_component_metadata("callable_auto") + assert callable_meta.auto_load == frozenset() + + # SyntaxError in __init__.py must not raise. + assert helpers.parse_component_metadata("broken") == helpers.ComponentMetadata() + + +def test_split_conflicting_groups_splits_direct_conflict( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + result = helpers.split_conflicting_groups( + {("esp32", "i2c"): ["alpha", "beta", "gamma"]} + ) + # alpha and beta must end up in different buckets; gamma has no conflicts. + buckets = list(result.values()) + assert any("alpha" in b for b in buckets) + assert any("beta" in b for b in buckets) + for bucket in buckets: + assert not ({"alpha", "beta"} <= set(bucket)) + # Gamma sticks with whichever bucket it landed in first (alpha's). + all_members = {c for b in buckets for c in b} + assert all_members == {"alpha", "beta", "gamma"} + + +def test_split_conflicting_groups_propagates_through_auto_load( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + """A component that AUTO_LOADs a conflicting one must also be split out.""" + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + result = helpers.split_conflicting_groups( + {("esp32", "i2c"): ["alpha", "beta_variant"]} + ) + buckets = list(result.values()) + for bucket in buckets: + assert not ({"alpha", "beta_variant"} <= set(bucket)) + assert sum(len(b) for b in buckets) == 2 + + +def test_split_conflicting_groups_symmetric_one_sided_declaration( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + """If only one side declares CONFLICTS_WITH, the pair must still be split.""" + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + result = helpers.split_conflicting_groups( + {("esp32", "i2c"): ["one_sided", "plain"]} + ) + buckets = list(result.values()) + for bucket in buckets: + assert not ({"one_sided", "plain"} <= set(bucket)) + + +def test_split_conflicting_groups_preserves_non_conflicting_group( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + original = {("esp32", "i2c"): ["alpha", "gamma", "plain"]} + result = helpers.split_conflicting_groups(original) + # All three are mutually compatible -- the group must not be split. + assert result == original + + +def test_split_conflicting_groups_preserves_original_signature_for_first_bucket( + fake_components: Path, monkeypatch: MonkeyPatch +) -> None: + """When a group is split, the first bucket keeps the original signature key.""" + monkeypatch.setattr(helpers, "root_path", str(fake_components)) + helpers.parse_component_metadata.cache_clear() + + result = helpers.split_conflicting_groups({("esp32", "i2c"): ["alpha", "beta"]}) + keys = set(result.keys()) + assert ("esp32", "i2c") in keys + # One additional bucket with a disambiguated signature. + extra = keys - {("esp32", "i2c")} + assert len(extra) == 1 + platform, signature = next(iter(extra)) + assert platform == "esp32" + assert signature.startswith("i2c__conflict") From 38d894dfe7f012fca0545a8fe1356b4b1f4b0325 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 08:17:22 -0500 Subject: [PATCH 1504/2056] [ld2412] Fix flaky integration test race condition (#15833) --- tests/integration/test_uart_mock_ld2412.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index 12aa3f8397c..ea2ec38b2a3 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -325,9 +325,13 @@ async def test_uart_mock_ld2412_engineering_truncated( ], ) - # Signal when we see Phase 3 recovery values (gate_0_move=50) + # Signal when we see ALL Phase 3 recovery values to avoid race where some + # arrive after the waiter fires but before we index into the lists recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["gate_0_move_energy"] + and pytest.approx(42.0) in collector.sensor_states["light"] + ) ) async with ( From 7a23a339e9a6f8234f2e8ad3339f047e3b18e27f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 19 Apr 2026 16:00:31 -0500 Subject: [PATCH 1505/2056] [substitutions] Fix `substitutions: !include file.yaml` regression (#15850) --- esphome/components/packages/__init__.py | 8 ++- esphome/components/substitutions/__init__.py | 35 +++++++++++-- .../15-substitutions_as_include.approved.yaml | 5 ++ .../15-substitutions_as_include.input.yaml | 5 ++ .../substitutions/15-substitutions_inc.yaml | 1 + ...ons_as_include_with_packages.approved.yaml | 5 ++ ...utions_as_include_with_packages.input.yaml | 9 ++++ ...ubstitutions_include_cli_var.approved.yaml | 6 +++ ...7-substitutions_include_cli_var.input.yaml | 8 +++ tests/unit_tests/test_substitutions.py | 51 +++++++++++++++++++ 10 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 3f3df753512..252a24061a0 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -10,6 +10,7 @@ from esphome.components.substitutions import ( ContextVars, push_context, resolve_include, + resolve_substitutions_block, substitute, ) from esphome.components.substitutions.jinja import has_jinja @@ -516,7 +517,12 @@ def do_packages_pass( if CONF_PACKAGES not in config: return config - substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + with cv.prepend_path(CONF_SUBSTITUTIONS): + substitutions = UserDict( + resolve_substitutions_block( + config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions + ) + ) processor = _PackageProcessor( substitutions, command_line_substitutions, skip_update ) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 94aebbbfe35..e451ad5db8d 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -414,6 +414,34 @@ def _warn_unresolved_variables(errors: ErrList) -> None: ) +def resolve_substitutions_block( + substitutions: Any, + command_line_substitutions: dict[str, Any] | None, +) -> dict[str, Any]: + """Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape. + + The caller is responsible for wrapping the call in + ``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting. + ``command_line_substitutions`` seeds the filename context so + ``substitutions: !include ${var}.yaml`` can reference CLI-provided vars. + """ + if isinstance(substitutions, IncludeFile): + # Single-shot resolution — matches ``_walk_packages`` for the + # ``packages: !include`` entry point. Chained includes (an include that + # itself loads another ``!include`` at the top level) are not supported. + substitutions, _ = resolve_include( + substitutions, + [], + ContextVars(command_line_substitutions or {}), + strict_undefined=False, + ) + if not isinstance(substitutions, dict): + raise cv.Invalid( + f"Substitutions must be a key to value mapping, got {type(substitutions)}" + ) + return substitutions + + def do_substitution_pass( config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None ) -> OrderedDict: @@ -429,10 +457,9 @@ def do_substitution_pass( # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() substitutions = config.pop(CONF_SUBSTITUTIONS, {}) with cv.prepend_path(CONF_SUBSTITUTIONS): - if not isinstance(substitutions, dict): - raise cv.Invalid( - f"Substitutions must be a key to value mapping, got {type(substitutions)}" - ) + substitutions = resolve_substitutions_block( + substitutions, command_line_substitutions + ) substitutions = merge_dicts_ordered( substitutions, command_line_substitutions or {} ) diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml new file mode 100644 index 00000000000..14aa707def5 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml @@ -0,0 +1,5 @@ +substitutions: + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml new file mode 100644 index 00000000000..5909e7bf4f7 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml @@ -0,0 +1,5 @@ +substitutions: !include 15-substitutions_inc.yaml + +wifi: + ssid: main_ssid + password: $wifi_password diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml new file mode 100644 index 00000000000..44d9a4b9ef2 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml @@ -0,0 +1 @@ +wifi_password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml new file mode 100644 index 00000000000..14aa707def5 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml @@ -0,0 +1,5 @@ +substitutions: + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml new file mode 100644 index 00000000000..a2e72f33a23 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml @@ -0,0 +1,9 @@ +substitutions: !include 15-substitutions_inc.yaml + +packages: + wifi_pkg: + wifi: + password: $wifi_password + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml new file mode 100644 index 00000000000..f1fd5fb0786 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml @@ -0,0 +1,6 @@ +substitutions: + subs_file: 15-substitutions_inc + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml new file mode 100644 index 00000000000..3248504b468 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml @@ -0,0 +1,8 @@ +command_line_substitutions: + subs_file: 15-substitutions_inc + +substitutions: !include ${subs_file}.yaml + +wifi: + ssid: main_ssid + password: $wifi_password diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 01c669e5420..71bbd9db86d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -675,6 +675,57 @@ def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: substitutions.do_substitution_pass(config) +def test_do_substitution_pass_included_substitutions_must_be_mapping( + tmp_path: Path, +) -> None: + """`substitutions: !include list.yaml` where the file holds a list raises cv.Invalid. + + Locks in the shape check that runs after the deferred IncludeFile has been + resolved. + """ + parent = tmp_path / "main.yaml" + parent.write_text("") + + def loader(path: Path): + return ["not", "a", "mapping"] + + include = yaml_util.IncludeFile(parent, "subs.yaml", None, loader) + config = OrderedDict({CONF_SUBSTITUTIONS: include}) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + substitutions.do_substitution_pass(config) + + +def test_do_packages_pass_included_substitutions_must_be_mapping( + tmp_path: Path, +) -> None: + """`substitutions: !include list.yaml` alongside `packages:` raises cv.Invalid. + + Without the shape check, ``UserDict(...)`` would surface a low-level + ``TypeError``; the explicit ``cv.Invalid`` points at the substitutions path. + """ + parent = tmp_path / "main.yaml" + parent.write_text("") + + def loader(path: Path): + return ["not", "a", "mapping"] + + include = yaml_util.IncludeFile(parent, "subs.yaml", None, loader) + config = OrderedDict( + { + CONF_SUBSTITUTIONS: include, + "packages": {"noop": {"wifi": {"ssid": "main"}}}, + } + ) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + do_packages_pass(config) + + def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> None: """An undefined substitution in a package include filename raises cv.Invalid. From aad1318b4a8eceec8c8e16dd3718d26f55790697 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:09:14 +0200 Subject: [PATCH 1506/2056] [packages] Improve error messages with include stack and fix missing path propagation (#15844) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/packages/__init__.py | 9 ++ esphome/components/substitutions/__init__.py | 50 +++++++++++ .../component_tests/packages/test_packages.py | 88 ++++++++++++++++++- tests/unit_tests/test_substitutions.py | 34 +++++++ 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 252a24061a0..97a53094800 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -8,7 +8,9 @@ from typing import Any from esphome import git, yaml_util from esphome.components.substitutions import ( ContextVars, + ErrList, push_context, + raise_first_undefined, resolve_include, resolve_substitutions_block, substitute, @@ -360,12 +362,19 @@ def _substitute_package_definition( if isinstance(package_config, str) or ( isinstance(package_config, dict) and is_remote_package(package_config) ): + # Collect undefined-variable errors (rather than raising strict) so the + # path walked through a remote-package dict is preserved and the user + # sees which field (url / path / ref / ...) referenced the undefined + # variable. + errors: ErrList = [] package_config = substitute( item=package_config, path=[], parent_context=context_vars or ContextVars(), strict_undefined=False, + errors=errors, ) + raise_first_undefined(errors, package_config, "package definition") return package_config diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index e451ad5db8d..8bbccffca16 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -30,6 +30,56 @@ ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] jinja = Jinja() +def raise_first_undefined( + errors: ErrList, + source: Any, + context_label: str, +) -> None: + """If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable. + + The raised error names the missing variable, the path walked into *source* + (for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location + when *source* carries one. Only the first error is surfaced; the user will + re-run after fixing it and any remaining undefined variables will be + reported then. + + ``context_label`` is the noun describing where the undefined variable + appeared (e.g. ``"package definition"``). + """ + if not errors: + return + err, err_path, err_value = errors[0] + if len(errors) > 1: + # Log any further undefined variables so debug-level output covers + # the full set, even though only the first is surfaced to the user. + extras = ", ".join( + f"{e.message} at '{'->'.join(str(p) for p in p_path)}'" + for e, p_path, _ in errors[1:] + ) + _LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras) + # Prefer the location of the offending scalar (e.g. the `url:` value) over + # the enclosing package-definition dict so the message points at the exact + # line/column that carries the undefined variable. + location_node = ( + err_value + if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None + else source + ) + location = "" + if ( + isinstance(location_node, ESPHomeDataBase) + and location_node.esp_range is not None + ): + mark = location_node.esp_range.start_mark + # DocumentLocation.line/column are 0-based (from the YAML Mark). Render + # as 1-based to match config.line_info() and editor line numbering. + location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})" + field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else "" + raise cv.Invalid( + f"Undefined variable in {context_label}{field}: {err.message}{location}" + ) + + def validate_substitution_key(value: Any) -> str: """Validate and normalize a substitution key, stripping a leading ``$`` if present.""" value = cv.string(value) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index cd91c4d8cb1..0bd339efa9c 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -2,18 +2,20 @@ import logging from pathlib import Path +import re from unittest.mock import MagicMock, patch import pytest from esphome.components.packages import ( CONFIG_SCHEMA, + _substitute_package_definition, _walk_packages, do_packages_pass, is_package_definition, merge_packages, ) -from esphome.components.substitutions import do_substitution_pass +from esphome.components.substitutions import ContextVars, do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove @@ -44,7 +46,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict -from esphome.yaml_util import IncludeFile, add_context +from esphome.yaml_util import IncludeFile, add_context, load_yaml # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -1399,3 +1401,85 @@ def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: "CORE.raw_config should contain esphome section after package merge" ) assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME + + +# --------------------------------------------------------------------------- +# _substitute_package_definition +# --------------------------------------------------------------------------- + + +def test_substitute_package_definition_local_dict_returned_unchanged() -> None: + """A plain local config dict is not substituted and is returned as-is.""" + pkg = {CONF_WIFI: {CONF_SSID: "test"}} + result = _substitute_package_definition(pkg, ContextVars()) + assert result is pkg + + +def test_substitute_package_definition_string_resolved_with_context() -> None: + """A string package definition has its variables substituted.""" + ctx = ContextVars({"variant": "esp32"}) + result = _substitute_package_definition("device-${variant}.yaml", ctx) + assert result == "device-esp32.yaml" + + +def test_substitute_package_definition_undefined_in_string() -> None: + """An undefined variable in a package URL string raises cv.Invalid.""" + with pytest.raises(cv.Invalid, match="Undefined variable in package definition"): + _substitute_package_definition( + "github://org/repo/${undefined_var}/pkg.yaml", ContextVars() + ) + + +def test_substitute_package_definition_undefined_in_remote_dict_field() -> None: + """An undefined variable inside a remote-dict field names the offending field.""" + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition( + {CONF_URL: "github://${typo}/repo"}, ContextVars() + ) + err = str(exc_info.value) + assert "'typo' is undefined" in err + assert CONF_URL in err + + +def test_substitute_package_definition_undefined_in_remote_dict_non_first_field() -> ( + None +): + """The field path joins correctly for non-first dict fields (e.g. ``ref``).""" + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition( + { + CONF_URL: "github://org/repo", + CONF_REF: "branch-${branch_typo}", + }, + ContextVars(), + ) + err = str(exc_info.value) + assert "'branch_typo' is undefined" in err + assert CONF_REF in err + + +def test_substitute_package_definition_includes_source_location(tmp_path: Path) -> None: + """A package loaded from YAML surfaces file/line/col in the cv.Invalid message. + + Line/column are rendered 1-based (matching config.line_info() and editor + line numbering) and point at the offending scalar, not the enclosing dict. + """ + yaml_file = tmp_path / "main.yaml" + yaml_file.write_text( + "packages:\n broken: github://org/repo/${undefined_var}/pkg.yaml\n" + ) + config = load_yaml(yaml_file) + package_config = config[CONF_PACKAGES]["broken"] + + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition(package_config, ContextVars()) + + err = str(exc_info.value) + assert "main.yaml" in err + # The offending value lives on line 2 (1-based). Column depends on the YAML + # loader, so we only pin line and check that a 1-based column is present. + match = re.search(r"main\.yaml (\d+):(\d+)", err) + assert match, err + line, col = int(match.group(1)), int(match.group(2)) + assert line == 2, f"expected 1-based line 2, got {line} (err={err!r})" + assert col >= 1, f"expected 1-based column ≥ 1, got {col} (err={err!r})" diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 71bbd9db86d..3599e703d9d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -14,6 +14,7 @@ from esphome.components.packages import ( do_packages_pass, merge_packages, ) +from esphome.components.substitutions.jinja import UndefinedError from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, merge_config import esphome.config_validation as cv @@ -675,6 +676,39 @@ def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: substitutions.do_substitution_pass(config) +def test_raise_first_undefined_logs_extras_at_debug( + caplog: pytest.LogCaptureFixture, +) -> None: + """Only the first undefined error is raised; extras are logged at debug.""" + errors: substitutions.ErrList = [ + (UndefinedError("'a' is undefined"), ["url"], None), + (UndefinedError("'b' is undefined"), ["ref"], None), + (UndefinedError("'c' is undefined"), ["path"], None), + ] + + with ( + caplog.at_level(logging.DEBUG, logger="esphome.components.substitutions"), + pytest.raises(cv.Invalid) as exc_info, + ): + substitutions.raise_first_undefined(errors, None, "package definition") + + # First error is surfaced as the cv.Invalid message. + raised = str(exc_info.value) + assert "'a' is undefined" in raised + assert "'b' is undefined" not in raised + assert "'c' is undefined" not in raised + + # Remaining errors are captured via debug logging for troubleshooting. + assert "Additional undefined variables in package definition" in caplog.text + assert "'b' is undefined at 'ref'" in caplog.text + assert "'c' is undefined at 'path'" in caplog.text + + +def test_raise_first_undefined_noop_on_empty() -> None: + """An empty errors list is a no-op — no exception, no log.""" + substitutions.raise_first_undefined([], None, "package definition") + + def test_do_substitution_pass_included_substitutions_must_be_mapping( tmp_path: Path, ) -> None: From 1847666e75950c50eb4be05564e0ca262ff852af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 19 Apr 2026 17:05:27 -0500 Subject: [PATCH 1507/2056] [core] Default PollingComponent() to not run when codegen is bypassed (#15832) --- esphome/core/component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 717ca362576..67db5423af9 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -601,7 +601,7 @@ class Component { */ class PollingComponent : public Component { public: - PollingComponent() : PollingComponent(1) {} + PollingComponent() : PollingComponent(SCHEDULER_DONT_RUN) {} /** Initialize this polling component with the given update interval in ms. * From cb90ac45c35975a882795013a69a6ac613185a58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 07:50:11 -1000 Subject: [PATCH 1508/2056] [core] Fix app_state_ status bits clobbered for non-looping components (#15658) --- esphome/core/application.cpp | 34 ++- esphome/core/application.h | 24 ++- esphome/core/component.cpp | 13 ++ esphome/core/component.h | 5 + tests/integration/fixtures/status_flags.yaml | 141 +++++++++++++ tests/integration/test_status_flags.py | 209 +++++++++++++++++++ 6 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 tests/integration/fixtures/status_flags.yaml create mode 100644 tests/integration/test_status_flags.py diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index cd758598801..0c17c701615 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -85,8 +85,12 @@ void Application::setup() { if (component->can_proceed()) continue; + // Force the status LED to blink WARNING while we wait for a slow + // component to come up. Cleared after setup() finishes if no real + // component has warning set. + this->app_state_ |= STATUS_LED_WARNING; + do { - uint8_t new_app_state = STATUS_LED_WARNING; uint32_t now = millis(); // Process pending loop enables to handle GPIO interrupts during setup @@ -96,17 +100,26 @@ void Application::setup() { // Update loop_component_start_time_ right before calling each component this->loop_component_start_time_ = millis(); this->components_[j]->call(); - new_app_state |= this->components_[j]->get_component_state(); - this->app_state_ |= new_app_state; this->feed_wdt(); } this->after_loop_tasks_(); - this->app_state_ = new_app_state; yield(); } while (!component->can_proceed() && !component->is_failed()); } + // Setup is complete. Reconcile STATUS_LED_WARNING: the slow-setup path + // above may have forced it on, and any status_clear_warning() calls + // from components during setup were intentional no-ops (gated by + // APP_STATE_SETUP_COMPLETE). Walk components once here to pick up the + // real state. STATUS_LED_ERROR is never artificially forced, so its + // clear path always works and needs no reconciliation. Finally, set + // APP_STATE_SETUP_COMPLETE so subsequent warning clears go through + // the normal walk-and-clear path. + if (!this->any_component_has_status_flag_(STATUS_LED_WARNING)) + this->app_state_ &= ~STATUS_LED_WARNING; + this->app_state_ |= APP_STATE_SETUP_COMPLETE; + ESP_LOGI(TAG, "setup() finished successfully!"); #ifdef USE_SETUP_PRIORITY_OVERRIDE @@ -211,6 +224,19 @@ void HOT Application::feed_wdt(uint32_t time) { #endif } } +bool Application::any_component_has_status_flag_(uint8_t flag) const { + // Walk all components (not just looping ones) so non-looping components' + // status bits are respected. Only called from the slow-path clear helpers + // (status_clear_warning_slow_path_ / status_clear_error_slow_path_) on an + // actual set→clear transition, so walking O(N) here is paid once per + // transition — not once per loop iteration. + for (auto *component : this->components_) { + if ((component->get_component_state() & flag) != 0) + return true; + } + return false; +} + void Application::reboot() { ESP_LOGI(TAG, "Forcing a reboot"); for (auto &component : std::ranges::reverse_view(this->components_)) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 6b2969b4907..0150bb6646a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -401,7 +401,18 @@ class Application { */ void teardown_components(uint32_t timeout_ms); - uint8_t get_app_state() const { return this->app_state_; } + /// Return the public app state status bits (STATUS_LED_* only). + /// Internal bookkeeping bits like APP_STATE_SETUP_COMPLETE are masked + /// out so external readers (status_led components, etc.) never see them. + uint8_t get_app_state() const { return this->app_state_ & ~APP_STATE_SETUP_COMPLETE; } + + /// True once Application::setup() has finished walking all components + /// and finalized the initial status flags. Before this point, the + /// slow-setup busy-wait may be forcing STATUS_LED_WARNING on, and + /// status_clear_* intentionally skips its walk-and-clear step so the + /// forced bit doesn't get wiped. Stored as a free bit on app_state_ + /// (bit 6) to avoid costing additional RAM. + bool is_setup_complete() const { return (this->app_state_ & APP_STATE_SETUP_COMPLETE) != 0; } // Helper macro for entity getter method declarations #ifdef USE_DEVICES @@ -577,6 +588,12 @@ class Application { bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif + /// Walk all registered components looking for any whose component_state_ + /// has the given flag set. Used by Component::status_clear_*_slow_path_() + /// (which is a friend) to decide whether to clear the corresponding bit on + /// this->app_state_ (the app-wide "any component has this status" indicator). + bool any_component_has_status_flag_(uint8_t flag) const; + /// Register a component, detecting loop() override at compile time. /// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance. template<typename T> void register_component_(T *comp) { @@ -838,8 +855,6 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ } inline void ESPHOME_ALWAYS_INLINE Application::loop() { - uint8_t new_app_state = 0; - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); @@ -859,13 +874,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; this->feed_wdt(last_op_end_time); } this->after_loop_tasks_(); - this->app_state_ = new_app_state; #ifdef USE_RUNTIME_STATS // Process any pending runtime stats printing after all components have run diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index deda42b0a7d..8949b4b76dc 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -411,10 +411,23 @@ void Component::status_set_error(const LogString *message) { } void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; + // Clear the app-wide STATUS_LED_WARNING bit only if setup has finished + // AND no other component still has it set. During setup the forced + // STATUS_LED_WARNING (from the slow-setup busy-wait) must not be wiped + // by a transient component clear — Application::setup() reconciles + // the warning bit once at the end before setting APP_STATE_SETUP_COMPLETE. + // The set path is unchanged (set_status_flag_ still writes directly). + if (App.is_setup_complete() && !App.any_component_has_status_flag_(STATUS_LED_WARNING)) + App.app_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; + // STATUS_LED_ERROR is never artificially forced — it only ever lands + // in app_state_ via a real set_status_flag_ call. So the walk-and-clear + // path is always safe, including during setup. + if (!App.any_component_has_status_flag_(STATUS_LED_ERROR)) + App.app_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_momentary_warning(const char *name, uint32_t length) { diff --git a/esphome/core/component.h b/esphome/core/component.h index e2b7aa85d3a..3307c5ae76e 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -89,6 +89,11 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; +// Bit 6 on Application::app_state_ (ONLY) — set at the end of +// Application::setup(). Component::status_clear_*_slow_path_() uses this to +// decide whether to propagate clears to App.app_state_. Never set on a +// Component's component_state_. +inline constexpr uint8_t APP_STATE_SETUP_COMPLETE = 0x40; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; diff --git a/tests/integration/fixtures/status_flags.yaml b/tests/integration/fixtures/status_flags.yaml new file mode 100644 index 00000000000..cb118dcc84c --- /dev/null +++ b/tests/integration/fixtures/status_flags.yaml @@ -0,0 +1,141 @@ +esphome: + name: status-flags-test + +host: +api: + actions: + # Warning flag services for sensor_a + - action: set_warning_a + then: + - lambda: "id(sensor_a)->status_set_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_warning_a + then: + - lambda: "id(sensor_a)->status_clear_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Warning flag services for sensor_b + - action: set_warning_b + then: + - lambda: "id(sensor_b)->status_set_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_warning_b + then: + - lambda: "id(sensor_b)->status_clear_warning();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Error flag services for sensor_a + - action: set_error_a + then: + - lambda: "id(sensor_a)->status_set_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_error_a + then: + - lambda: "id(sensor_a)->status_clear_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Error flag services for sensor_b + - action: set_error_b + then: + - lambda: "id(sensor_b)->status_set_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + - action: clear_error_b + then: + - lambda: "id(sensor_b)->status_clear_error();" + - component.update: app_warning_bit + - component.update: app_error_bit + + # Snapshot of the status_led_light's output state for observation. + - action: snapshot_led + then: + - component.update: status_led_writes + - component.update: status_led_last_state + +logger: + +# Tracks each write to the fake status_led output. +globals: + - id: status_led_write_count + type: uint32_t + restore_value: no + initial_value: "0" + - id: status_led_last_write + type: bool + restore_value: no + initial_value: "false" + +# Fake binary output — status_led_light writes to this instead of a pin. +# Every write bumps a counter and records the last value, both of which +# are exposed below so the test can verify status_led_light's loop is +# actually reading App.get_app_state() and responding. +output: + - platform: template + id: fake_status_led + type: binary + write_action: + - globals.set: + id: status_led_write_count + value: !lambda "return id(status_led_write_count) + 1;" + - globals.set: + id: status_led_last_write + value: !lambda "return state;" + +# Actual status_led_light component under test. +light: + - platform: status_led + name: Status LED + id: status_led_light_id + output: fake_status_led + +sensor: + # Two components that the test will toggle warning/error flags on. + - platform: template + name: Sensor A + id: sensor_a + update_interval: 24h + lambda: return 1.0; + - platform: template + name: Sensor B + id: sensor_b + update_interval: 24h + lambda: return 2.0; + + # Expose App.app_state_'s STATUS_LED_WARNING / STATUS_LED_ERROR bits + # as 0.0 / 1.0. force_update ensures every manual component.update + # publishes even if the value is unchanged. + - platform: template + name: App Warning Bit + id: app_warning_bit + update_interval: 24h + force_update: true + lambda: |- + return (App.get_app_state() & STATUS_LED_WARNING) != 0 ? 1.0 : 0.0; + - platform: template + name: App Error Bit + id: app_error_bit + update_interval: 24h + force_update: true + lambda: |- + return (App.get_app_state() & STATUS_LED_ERROR) != 0 ? 1.0 : 0.0; + + # Observables for the fake status_led output. + - platform: template + name: Status LED Writes + id: status_led_writes + update_interval: 24h + force_update: true + lambda: return id(status_led_write_count); + - platform: template + name: Status LED Last State + id: status_led_last_state + update_interval: 24h + force_update: true + lambda: |- + return id(status_led_last_write) ? 1.0 : 0.0; diff --git a/tests/integration/test_status_flags.py b/tests/integration/test_status_flags.py new file mode 100644 index 00000000000..ffbc7c7f634 --- /dev/null +++ b/tests/integration/test_status_flags.py @@ -0,0 +1,209 @@ +"""Integration tests for Component::status_set/clear_warning/error propagation. + +Verifies that toggling STATUS_LED_WARNING / STATUS_LED_ERROR on individual +components correctly updates the app-wide bits on Application::app_state_, +AND that the status_led_light component actually responds to those bits +by writing to its output (the full chain from component.status_set_warning +→ App.app_state_ → status_led_light.loop() reading get_app_state()). + +Exercises the multi-component OR semantics (the app bit stays set while +any component still has the flag, and only clears when the last component +clears its bit), the independence of warning and error, and the actual +status_led_light read of the bits via a fake template output that counts +writes. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .state_utils import InitialStateHelper, SensorTracker, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Time to let the host-mode main loop run so status_led_light.loop() can +# execute enough iterations to produce measurable write-count changes on +# the fake template output. 300 ms is well above the minimum needed. +STATUS_LED_SETTLE_S = 0.3 + + +@pytest.mark.asyncio +async def test_status_flags( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + async with run_compiled(yaml_config), api_client_connected() as client: + entities, services = await client.list_entities_services() + + # Map every custom API service by name for the test to execute. + svc = {s.name: s for s in services} + for name in ( + "set_warning_a", + "clear_warning_a", + "set_warning_b", + "clear_warning_b", + "set_error_a", + "clear_error_a", + "set_error_b", + "clear_error_b", + "snapshot_led", + ): + assert name in svc, f"service {name} not registered" + + # Track every sensor we care about. SensorTracker gives us + # expect(value) / expect_any() futures that resolve when a + # matching state arrives; much simpler than manual bookkeeping. + tracker = SensorTracker( + [ + "app_warning_bit", + "app_error_bit", + "status_led_writes", + "status_led_last_state", + ] + ) + tracker.key_to_sensor.update( + build_key_to_entity_mapping(entities, list(tracker.sensor_states.keys())) + ) + + # Swallow initial state broadcasts so the test only reacts to + # state changes triggered by our service calls. + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(tracker.on_state)) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def call(name: str) -> None: + await client.execute_service(svc[name], {}) + + async def call_and_expect_bits( + service_name: str, *, warning: float, error: float + ) -> None: + """Execute a service and wait for both app bit sensors to match. + + Each bit-toggling service calls component.update on both + app_warning_bit and app_error_bit, so both sensors publish. + """ + futures = tracker.expect_all( + {"app_warning_bit": warning, "app_error_bit": error} + ) + await call(service_name) + await tracker.await_all(futures) + + async def snapshot_led_writes() -> int: + """Trigger a publish of the fake status_led output counter and return it.""" + future = tracker.expect_any("status_led_writes") + await call("snapshot_led") + await tracker.await_change(future, "status_led_writes") + return int(tracker.sensor_states["status_led_writes"][-1]) + + # ---- Baseline: everything clean ---- + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # ================================================================ + # Part 1 — STATUS_LED_WARNING propagation to App.app_state_ + # ================================================================ + + # Single component set/clear + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # Multi-component OR: both set, clear A, bit stays (B still has it), clear B, gone + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_b", warning=0.0, error=0.0) + + # Opposite clear order + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_b", warning=1.0, error=0.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # ================================================================ + # Part 2 — STATUS_LED_ERROR propagation (same scenarios) + # ================================================================ + + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0) + + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("set_error_b", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0) + + # ================================================================ + # Part 3 — warning and error are independent + # ================================================================ + + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await call_and_expect_bits("set_error_b", warning=1.0, error=1.0) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=1.0) + await call_and_expect_bits("clear_error_b", warning=0.0, error=0.0) + + # ================================================================ + # Part 4 — status_led_light actually reads App.app_state_ + # ================================================================ + # The fake status_led_light output increments status_led_write_count + # on every write. status_led_light::loop() writes its output on every + # iteration while an error/warning bit is set, so after holding a + # warning for ~300 ms we should see the counter move significantly. + # This is the end-to-end proof that the bits we set above actually + # reach status_led_light and drive its behavior. + + count_before_warning = await snapshot_led_writes() + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + # Let status_led_light's loop run long enough to toggle the pin + # several times (it reads get_app_state() every main loop iteration). + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_warning = await snapshot_led_writes() + assert count_after_warning > count_before_warning, ( + "status_led_light did not respond to STATUS_LED_WARNING being set: " + f"write count stayed at {count_before_warning} → {count_after_warning}. " + "The full chain Component::status_set_warning → App.app_state_ → " + "status_led_light::loop reading get_app_state() is broken." + ) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) + + # Same check for ERROR + count_before_error = await snapshot_led_writes() + await call_and_expect_bits("set_error_a", warning=0.0, error=1.0) + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_error = await snapshot_led_writes() + assert count_after_error > count_before_error, ( + "status_led_light did not respond to STATUS_LED_ERROR being set: " + f"write count stayed at {count_before_error} → {count_after_error}. " + ) + await call_and_expect_bits("clear_error_a", warning=0.0, error=0.0) + + # ---- Set → clear → re-set round-trip ---- + # After clearing, status_led_light stops writing (steady state). + # Re-setting the flag must make it resume. This guards against a + # future idle optimization (e.g. #15642) where status_led disables + # its own loop when idle: if the re-enable path were broken, the + # second set would not produce writes. + # + # Snapshot AFTER the clear to avoid counting writes that were still + # in-flight from the error-set phase. + count_after_clear = await snapshot_led_writes() + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_idle = await snapshot_led_writes() + assert count_after_idle - count_after_clear <= 5, ( + "status_led_light kept writing after warning/error was cleared: " + f"count grew from {count_after_clear} to {count_after_idle}. " + "Expected it to stop writing once all status bits were clear." + ) + # Re-set warning — writes must resume. + await call_and_expect_bits("set_warning_a", warning=1.0, error=0.0) + await asyncio.sleep(STATUS_LED_SETTLE_S) + count_after_reset = await snapshot_led_writes() + assert count_after_reset > count_after_idle + 5, ( + "status_led_light did not resume writing after re-setting " + f"STATUS_LED_WARNING: count went from {count_after_idle} to " + f"{count_after_reset}. If an idle optimization disabled the " + "loop, the re-enable path may be broken." + ) + await call_and_expect_bits("clear_warning_a", warning=0.0, error=0.0) From 4479212008f551f38885c3f4a278befb9a400b67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 14 Apr 2026 09:08:30 -1000 Subject: [PATCH 1509/2056] [core] Inline feed_wdt hot path with out-of-line slow path (#15656) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/application.cpp | 34 ++++++++++++++++++++-------------- esphome/core/application.h | 36 +++++++++++++++++++++++++++++------- esphome/core/scheduler.cpp | 8 +++++++- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 0c17c701615..1c732307054 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -209,21 +209,27 @@ void Application::process_dump_config_() { this->dump_config_at_++; } -void HOT Application::feed_wdt(uint32_t time) { - static uint32_t last_feed = 0; - // Use provided time if available, otherwise get current time - uint32_t now = time ? time : millis(); - // Compare in milliseconds (3ms threshold) - if (now - last_feed > 3) { - arch_feed_wdt(); - last_feed = now; -#ifdef USE_STATUS_LED - if (status_led::global_status_led != nullptr) { - status_led::global_status_led->call(); - } -#endif +void Application::feed_wdt() { + // Cold entry: callers without a millis() timestamp in hand. Fetches the + // time and takes the same rate-limit path as feed_wdt_with_time(). + uint32_t now = millis(); + if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { + this->feed_wdt_slow_(now); } } + +void HOT Application::feed_wdt_slow_(uint32_t time) { + // Callers (both feed_wdt() and feed_wdt_with_time()) have already + // confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded. + arch_feed_wdt(); + this->last_wdt_feed_ = time; +#ifdef USE_STATUS_LED + if (status_led::global_status_led != nullptr) { + status_led::global_status_led->call(); + } +#endif +} + bool Application::any_component_has_status_flag_(uint8_t flag) const { // Walk all components (not just looping ones) so non-looping components' // status bits are respected. Only called from the slow-path clear helpers @@ -325,7 +331,7 @@ void Application::teardown_components(uint32_t timeout_ms) { while (pending_count > 0 && (now - start_time) < timeout_ms) { // Feed watchdog during teardown to prevent triggering - this->feed_wdt(now); + this->feed_wdt_with_time(now); // Process components and compact the array, keeping only those still pending size_t still_pending = 0; diff --git a/esphome/core/application.h b/esphome/core/application.h index 0150bb6646a..60087d527d2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -385,7 +385,24 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - void feed_wdt(uint32_t time = 0); + /// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the + /// rate of HAL pokes low while still being small enough that any plausible + /// watchdog timeout (seconds) has orders of magnitude of safety margin. + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3; + + /// Feed the task watchdog. Cold entry — callers without a millis() + /// timestamp in hand. Out of line to keep call sites tiny. + void feed_wdt(); + + /// Feed the task watchdog, hot entry. Callers that already have a + /// millis() timestamp pay only a load + sub + branch on the common + /// (no-op) path. The actual arch feed + status LED update live in + /// feed_wdt_slow_. + void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) { + if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] { + this->feed_wdt_slow_(time); + } + } void reboot(); @@ -632,7 +649,10 @@ class Application { /// Caller must ensure dump_config_at_ < components_.size(). void __attribute__((noinline)) process_dump_config_(); - void feed_wdt_arch_(); + /// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates + /// last_wdt_feed_, and re-dispatches the status LED. Out of line so the + /// inline wrapper stays tiny. + void feed_wdt_slow_(uint32_t time); /// Perform a delay while also monitoring socket file descriptors for readiness #ifdef USE_HOST @@ -686,6 +706,7 @@ class Application { // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; + uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path #ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() @@ -830,12 +851,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ this->drain_wake_notifications_(); #endif - // Process scheduled tasks + // Process scheduled tasks. Scheduler::call now feeds the watchdog itself + // after each scheduled item that actually runs, so we no longer need an + // unconditional feed here — when Scheduler::call has no work to do, the + // only elapsed time is a sleep wake + a few instructions, and when it does + // have work, it fed the wdt as it went. this->scheduler.call(loop_start_time); - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions if (this->has_pending_enable_loop_requests_) { @@ -874,7 +896,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Use the finish method to get the current time as the end time last_op_end_time = guard.finish(); } - this->feed_wdt(last_op_end_time); + this->feed_wdt_with_time(last_op_end_time); } this->after_loop_tasks_(); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index dff50b03ef0..3e75a680643 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -739,7 +739,13 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); - return guard.finish(); + uint32_t end = guard.finish(); + // Feed the watchdog after each scheduled item (both main heap and defer + // queue paths go through here). A run of back-to-back callbacks cannot + // starve the wdt. The inline fast path is a load + sub + branch — nearly + // free when the 3 ms rate limit hasn't elapsed. + App.feed_wdt_with_time(end); + return end; } // Common implementation for cancel operations - handles locking From 6857e1ceb4533f20a2e07d7cd08aa6e768c030ee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:32:11 -0400 Subject: [PATCH 1510/2056] [st7789v] Fix swapped offset_width/offset_height in model presets (#15755) --- esphome/components/st7789v/display.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 85414237cfe..745c37f47dd 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -45,8 +45,8 @@ MODELS = { presets={ CONF_HEIGHT: 240, CONF_WIDTH: 135, - CONF_OFFSET_HEIGHT: 52, - CONF_OFFSET_WIDTH: 40, + CONF_OFFSET_HEIGHT: 40, + CONF_OFFSET_WIDTH: 52, CONF_CS_PIN: "GPIO5", CONF_DC_PIN: "GPIO16", CONF_RESET_PIN: "GPIO23", @@ -68,8 +68,8 @@ MODELS = { presets={ CONF_HEIGHT: 280, CONF_WIDTH: 240, - CONF_OFFSET_HEIGHT: 0, - CONF_OFFSET_WIDTH: 20, + CONF_OFFSET_HEIGHT: 20, + CONF_OFFSET_WIDTH: 0, } ), "ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec( @@ -77,8 +77,8 @@ MODELS = { presets={ CONF_HEIGHT: 240, CONF_WIDTH: 135, - CONF_OFFSET_HEIGHT: 52, - CONF_OFFSET_WIDTH: 40, + CONF_OFFSET_HEIGHT: 40, + CONF_OFFSET_WIDTH: 52, CONF_CS_PIN: "GPIO7", CONF_DC_PIN: "GPIO39", CONF_RESET_PIN: "GPIO40", @@ -89,8 +89,8 @@ MODELS = { presets={ CONF_HEIGHT: 320, CONF_WIDTH: 170, - CONF_OFFSET_HEIGHT: 35, - CONF_OFFSET_WIDTH: 0, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 35, CONF_ROTATION: 270, CONF_CS_PIN: "GPIO10", CONF_DC_PIN: "GPIO13", @@ -102,8 +102,8 @@ MODELS = { presets={ CONF_HEIGHT: 320, CONF_WIDTH: 172, - CONF_OFFSET_HEIGHT: 34, - CONF_OFFSET_WIDTH: 0, + CONF_OFFSET_HEIGHT: 0, + CONF_OFFSET_WIDTH: 34, CONF_ROTATION: 90, CONF_CS_PIN: "GPIO21", CONF_DC_PIN: "GPIO22", From 4047d5af5fab0956c9ef782947b99c8c30accd6d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:32:33 -0400 Subject: [PATCH 1511/2056] [sx126x][sx127x] Fix frequency precision loss from float32 codegen (#15753) --- esphome/components/sx126x/__init__.py | 4 ++-- esphome/components/sx127x/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 08f4c0fb882..b8696158fe5 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -200,11 +200,11 @@ CONFIG_SCHEMA = ( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( - cv.frequency, cv.float_range(min=0, max=100000) + cv.frequency, cv.int_range(min=0, max=100000) ), cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.All( - cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6)) ), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 7f554fbf841..8fa72471927 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -197,11 +197,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( - cv.frequency, cv.float_range(min=0, max=100000) + cv.frequency, cv.int_range(min=0, max=100000) ), cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.All( - cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6)) ), cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), From 5a2b7546f666abc106d22860c3f2bea5ad1b1a64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:07:49 -1000 Subject: [PATCH 1512/2056] Bump aioesphomeapi from 44.15.0 to 44.16.0 (#15757) 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 726a0a221ae..b14d5f5f5a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.15.0 +aioesphomeapi==44.16.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From af1aaba547d28639521b07d2bbe763f8c8b9e963 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:19:42 +1000 Subject: [PATCH 1513/2056] [lvgl] Clean the build if lv_conf.h changes (#15777) --- esphome/components/lvgl/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b6421dc43d7..ac0363ca696 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -44,6 +44,7 @@ from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed +from esphome.writer import clean_build from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets @@ -451,7 +452,8 @@ async def to_code(configs): df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) - write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) + if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()): + clean_build(clear_pio_cache=False) cg.add_build_flag("-DLV_CONF_H=1") # handle windows paths in a way that doesn't break the generated C++ lv_conf_h_path = Path(lv_conf_h_file).as_posix() From 92c99a7d41bfd69389519f8628feaf63d95ec957 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Thu, 16 Apr 2026 15:35:24 +0200 Subject: [PATCH 1514/2056] [mitsubishi_cn105] use HEAT_COOL mode to enable temperature slider (#15748) --- .../mitsubishi_cn105_climate.cpp | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 40ddb88a795..284339e57f9 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 { static const char *const TAG = "mitsubishi_cn105.climate"; static constexpr std::array MODE_MAP{ - std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO}, + std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL}, std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT}, std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY}, std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL}, @@ -76,23 +76,13 @@ void MitsubishiCN105Climate::loop() { climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; - traits.set_supported_modes({ - climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_FAN_ONLY, - climate::CLIMATE_MODE_AUTO, - }); + for (const auto &p : MODE_MAP) { + traits.add_supported_mode(p.second); + } - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_QUIET, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_MIDDLE, - climate::CLIMATE_FAN_HIGH, - }); + for (const auto &p : FAN_MODE_MAP) { + traits.add_supported_fan_mode(p.second); + } traits.set_visual_min_temperature(16.0f); traits.set_visual_max_temperature(31.0f); From 914ed10bccee4e21b19916c7b10736124337a457 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 03:36:55 -1000 Subject: [PATCH 1515/2056] [core] Diagnose missing cg.templatable in codegen for TEMPLATABLE_VALUE fields (#15758) --- esphome/core/automation.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index eb270bfee26..468ea3b382c 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -62,6 +62,18 @@ template<typename T, typename... X> class TemplatableFn { !std::convertible_to<std::invoke_result_t<F, X...>, T> || !std::default_initializable<F>) = delete; + // Reject raw (non-callable) values with a helpful diagnostic pointing at the Python-side fix. + // TemplatableFn stores only a function pointer (4 bytes), so constants must be wrapped in a + // stateless lambda by codegen. External components hitting this error should use + // `cg.templatable(value, args, type)` in their Python __init__.py before passing to the setter. + template<typename V> TemplatableFn(V) requires(!std::invocable<V, X...>) && (!std::convertible_to<V, T (*)(X...)>) { + static_assert(sizeof(V) == 0, "Missing cg.templatable(...) in Python codegen for this TEMPLATABLE_VALUE " + "field. The wrapper was always required; it worked by accident because the old " + "TemplatableValue implicitly converted raw constants. TemplatableFn cannot. See " + "https://developers.esphome.io/blog/2026/04/09/" + "templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/"); + } + bool has_value() const { return this->f_ != nullptr; } T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; } From aa80bdbbc68ab65c19c05002b57ac630f393892a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 03:40:22 -1000 Subject: [PATCH 1516/2056] [time] Fix RTC is_valid() rejecting valid times after day_of_year cleanup (#15763) --- esphome/components/bm8563/bm8563.cpp | 2 +- esphome/components/ds1307/ds1307.cpp | 2 +- esphome/components/pcf85063/pcf85063.cpp | 2 +- esphome/components/pcf8563/pcf8563.cpp | 2 +- esphome/components/rx8130/rx8130.cpp | 2 +- esphome/core/time.h | 8 ++- tests/components/time/is_valid.cpp | 72 ++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/components/time/is_valid.cpp diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 062094c036f..d911301c9d3 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -63,7 +63,7 @@ void BM8563::read_time() { rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 8fff4213b4c..ba2ad6032f4 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -44,7 +44,7 @@ void DS1307Component::read_time() { .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 1cf28a49557..000de1433cb 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -44,7 +44,7 @@ void PCF85063Component::read_time() { .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index b748f0156aa..50003ca378b 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -44,7 +44,7 @@ void PCF8563Component::read_time() { .year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 3b704d2551f..0aa6e86d31e 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -81,7 +81,7 @@ void RX8130Component::read_time() { .year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000), }; rtc_time.recalc_timestamp_utc(false); - if (!rtc_time.is_valid()) { + if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); return; } diff --git a/esphome/core/time.h b/esphome/core/time.h index ed474320381..0b67b7b3fcd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -76,8 +76,12 @@ struct ESPTime { /// @copydoc strftime(const std::string &format) std::string strftime(const char *format); - /// Check if this ESPTime is valid (all fields in range and year is greater than or equal to 2019) - bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } + /// Check if this ESPTime is valid (year >= 2019 and the requested fields are in range). + /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) + /// @param check_day_of_year validate day_of_year (not always available when constructing from date/time fields) + bool is_valid(bool check_day_of_week = true, bool check_day_of_year = true) const { + return this->year >= 2019 && this->fields_in_range(check_day_of_week, check_day_of_year); + } /// Check if time fields are in range. /// @param check_day_of_week validate day_of_week (not always available when constructing from date/time fields) diff --git a/tests/components/time/is_valid.cpp b/tests/components/time/is_valid.cpp new file mode 100644 index 00000000000..9148c0e8d6b --- /dev/null +++ b/tests/components/time/is_valid.cpp @@ -0,0 +1,72 @@ +// Regression tests for ESPTime::is_valid() optional checks. +// +// The RTC components (ds1307, bm8563, pcf85063, pcf8563, rx8130) read date/time +// fields from hardware but do not populate day_of_year. They call +// recalc_timestamp_utc(false) -- which skips day_of_year -- and then is_valid(). +// These tests ensure the is_valid() overload can skip day_of_year validation so +// RTCs don't log "Invalid RTC time, not syncing to system clock." for valid times. + +#include <gtest/gtest.h> +#include "esphome/core/time.h" + +namespace esphome::testing { + +// Build an ESPTime that mirrors what the RTC components construct: all fields +// populated from hardware except day_of_year (left zero-initialized). +static ESPTime make_rtc_like_time() { + ESPTime t{}; + t.second = 30; + t.minute = 15; + t.hour = 12; + t.day_of_week = 4; // thursday + t.day_of_month = 15; + t.month = 4; + t.year = 2026; + // day_of_year intentionally left at 0 -- RTCs don't compute it. + return t; +} + +TEST(ESPTimeIsValid, DefaultRejectsZeroDayOfYear) { + // Default is_valid() checks day_of_year; zero-init is out of range. + ESPTime t = make_rtc_like_time(); + EXPECT_FALSE(t.is_valid()); +} + +TEST(ESPTimeIsValid, SkipDayOfYearAcceptsRTCLikeTime) { + // RTC code path: skip day_of_year validation. + ESPTime t = make_rtc_like_time(); + EXPECT_TRUE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipDayOfYearStillRejectsOutOfRangeFields) { + ESPTime t = make_rtc_like_time(); + t.hour = 25; + EXPECT_FALSE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipDayOfYearStillRejectsYearBefore2019) { + ESPTime t = make_rtc_like_time(); + t.year = 2000; + EXPECT_FALSE(t.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)); +} + +TEST(ESPTimeIsValid, SkipBothDayChecksAcceptsGPSLikeTime) { + // GPS path (gps_time.cpp) populates neither day_of_week nor day_of_year. + ESPTime t{}; + t.second = 30; + t.minute = 15; + t.hour = 12; + t.day_of_month = 15; + t.month = 4; + t.year = 2026; + EXPECT_TRUE(t.is_valid(/*check_day_of_week=*/false, /*check_day_of_year=*/false)); + EXPECT_FALSE(t.is_valid()); // default still rejects +} + +TEST(ESPTimeIsValid, FullyPopulatedAcceptsWithDefaults) { + ESPTime t = make_rtc_like_time(); + t.day_of_year = 105; + EXPECT_TRUE(t.is_valid()); +} + +} // namespace esphome::testing From 227dfa3730692c2ecf1931cdee1131bb08b4893f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:36:06 -0400 Subject: [PATCH 1517/2056] [qmc5883l] Move per-update log line from DEBUG to VERBOSE (#15781) --- esphome/components/qmc5883l/qmc5883l.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index bc2adb5cfe5..d0488d0c9f7 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -100,7 +100,7 @@ void QMC5883LComponent::update() { // ROL_PNT in setup and reading 7 bytes starting at the status register. // If status and all three axes are desired, using ROL_PNT saves you 3 bytes. // But simply not reading status saves you 4 bytes always and is much simpler. - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) { + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1); if (err != i2c::ERROR_OK) { char buf[32]; @@ -165,7 +165,7 @@ void QMC5883LComponent::update() { temp = int16_t(raw_temp) * 0.01f; } - ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading, + ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading, temp, status); if (this->x_sensor_ != nullptr) From 81fb6712fef424a0273237786b2a4e4898f57742 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 08:45:33 -1000 Subject: [PATCH 1518/2056] [bundle] Force-resolve nested IncludeFile during file discovery (#15762) --- esphome/bundle.py | 78 ++++++++- .../fixtures/bundle/bundle_test.yaml | 6 +- .../fixtures/bundle/common/wifi.yaml | 2 + tests/unit_tests/test_bundle.py | 150 +++++++++++++++++- 4 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 tests/unit_tests/fixtures/bundle/common/wifi.yaml diff --git a/esphome/bundle.py b/esphome/bundle.py index b6816c7c95e..efa80acc8cc 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -151,8 +151,8 @@ class ConfigBundleCreator: def __init__(self, config: dict[str, Any]) -> None: self._config = config - self._config_dir = CORE.config_dir - self._config_path = CORE.config_path + self._config_dir = Path(CORE.config_dir).resolve() + self._config_path = Path(CORE.config_path).resolve() self._files: list[BundleFile] = [] self._seen_paths: set[Path] = set() self._secrets_paths: set[Path] = set() @@ -258,21 +258,36 @@ class ConfigBundleCreator: def _discover_yaml_includes(self) -> None: """Discover YAML files loaded during config parsing. - We track files by wrapping _load_yaml_internal. The config has already - been loaded at this point (bundle is a POST_CONFIG_ACTION), so we - re-load just to discover the file list. + Deliberately uses a fresh re-parse and force-loads every deferred + ``IncludeFile`` to include *all* potentially-reachable includes, + even branches not selected by the local substitutions. Bundles are + meant to be compiled on another system where command-line + substitution overrides may choose a different branch — e.g. + ``!include network/${eth_model}/config.yaml`` must ship every + candidate so the remote build can pick any one. + + Entries with unresolved substitution variables in the filename + path are skipped with a warning (they cannot be resolved without + the substitution pass). Secrets files are tracked separately so we can filter them to only include the keys this config actually references. """ + # Must be a fresh parse: IncludeFile.load() caches its result in + # _content, and we discover files by listening for loader calls. On + # an already-parsed tree the cache is populated, .load() returns + # without calling the loader, the listener never fires, and the + # referenced files would be silently dropped from the bundle. with yaml_util.track_yaml_loads() as loaded_files: try: - yaml_util.load_yaml(self._config_path) + data = yaml_util.load_yaml(self._config_path) except EsphomeError: _LOGGER.debug( "Bundle: re-loading YAML for include discovery failed, " "proceeding with partial file list" ) + else: + _force_load_include_files(data) for fpath in loaded_files: if fpath == self._config_path.resolve(): @@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None: tar.addfile(info, io.BytesIO(data)) +def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None: + """Recursively resolve any ``IncludeFile`` instances in a YAML tree. + + Nested ``!include`` returns a deferred ``IncludeFile`` that is only + resolved during the substitution pass. During bundle discovery we need + the referenced files to actually load so the ``track_yaml_loads`` + listener fires for them. + + ``IncludeFile`` instances with unresolved substitution variables in the + filename cannot be loaded — we skip and warn about those. + """ + if _seen is None: + _seen = set() + + if isinstance(obj, yaml_util.IncludeFile): + if id(obj) in _seen: + return + _seen.add(id(obj)) + if obj.has_unresolved_expressions(): + _LOGGER.warning( + "Bundle: cannot resolve !include %s (referenced from %s) " + "with substitutions in path", + obj.file, + obj.parent_file, + ) + return + try: + loaded = obj.load() + except EsphomeError as err: + _LOGGER.warning( + "Bundle: failed to load !include %s (referenced from %s): %s", + obj.file, + obj.parent_file, + err, + ) + return + _force_load_include_files(loaded, _seen) + elif isinstance(obj, dict): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for value in obj.values(): + _force_load_include_files(value, _seen) + elif isinstance(obj, (list, tuple)): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for item in obj: + _force_load_include_files(item, _seen) + + def _resolve_include_path(include_path: Any) -> Path | None: """Resolve an include path to absolute, skipping system includes.""" if isinstance(include_path, str) and include_path.startswith("<"): diff --git a/tests/unit_tests/fixtures/bundle/bundle_test.yaml b/tests/unit_tests/fixtures/bundle/bundle_test.yaml index f834a8d867f..247f5cc8bb1 100644 --- a/tests/unit_tests/fixtures/bundle/bundle_test.yaml +++ b/tests/unit_tests/fixtures/bundle/bundle_test.yaml @@ -11,9 +11,9 @@ esp32: logger: <<: !include common/base.yaml -wifi: - ssid: !secret wifi_ssid - password: !secret wifi_password +# Plain nested !include — deferred as an IncludeFile until the substitution +# pass. The bundle must force-resolve it to pick up common/wifi.yaml. +wifi: !include common/wifi.yaml api: diff --git a/tests/unit_tests/fixtures/bundle/common/wifi.yaml b/tests/unit_tests/fixtures/bundle/common/wifi.yaml new file mode 100644 index 00000000000..d7e7b3cd450 --- /dev/null +++ b/tests/unit_tests/fixtures/bundle/common/wifi.yaml @@ -0,0 +1,2 @@ +ssid: !secret wifi_ssid +password: !secret wifi_password diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index b8b2d0ffd1a..89bf1a33b3e 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -5,8 +5,10 @@ from __future__ import annotations import io import json from pathlib import Path +import shutil import tarfile from typing import Any +from unittest.mock import patch import pytest @@ -20,6 +22,7 @@ from esphome.bundle import ( _add_bytes_to_tar, _default_target_dir, _find_used_secret_keys, + _force_load_include_files, extract_bundle, is_bundle_path, prepare_bundle_for_compile, @@ -485,7 +488,7 @@ def test_read_bundle_manifest_minimal(tmp_path: Path) -> None: result = read_bundle_manifest(bundle_path) assert result.esphome_version == "unknown" - assert result.files == [] + assert not result.files assert result.has_secrets is False @@ -862,6 +865,117 @@ def test_discover_files_skips_missing_directory(tmp_path: Path) -> None: assert len(files) == 1 +def test_discover_files_nested_include(tmp_path: Path) -> None: + """Nested !include files (e.g. wifi: !include wifi.yaml) are bundled.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include wifi.yaml\n" + ) + (config_dir / "wifi.yaml").write_text('ssid: "a"\npassword: "b"\n') + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + assert "wifi.yaml" in paths + + +def test_discover_files_deeply_nested_include(tmp_path: Path) -> None: + """Chains of !include (a includes b includes c) are fully resolved.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include level1.yaml\n" + ) + (config_dir / "level1.yaml").write_text("nested: !include level2.yaml\n") + (config_dir / "level2.yaml").write_text('value: "leaf"\n') + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "level1.yaml" in paths + assert "level2.yaml" in paths + + +def test_discover_files_nested_include_unresolved_substitution( + tmp_path: Path, +) -> None: + """!include with substitution vars in path cannot be resolved; skipped gracefully.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include ${platform}.yaml\n" + ) + + creator = ConfigBundleCreator({}) + # Should not raise + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + + +def test_discover_files_nested_include_load_failure( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A nested !include pointing at a missing file is logged and skipped.""" + config_dir = _setup_config_dir(tmp_path) + (config_dir / "test.yaml").write_text( + "esphome:\n name: test\nwifi: !include missing.yaml\n" + ) + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + + paths = [f.path for f in files] + assert "test.yaml" in paths + assert any( + "failed to load !include" in r.message and "missing.yaml" in r.message + for r in caplog.records + ) + + +def test_force_load_skips_duplicate_include_file() -> None: + """The same IncludeFile referenced twice is only loaded once.""" + + class _StubInclude: + """Mimics yaml_util.IncludeFile minimally for _force_load testing.""" + + def __init__(self) -> None: + self.file = Path("dup.yaml") + self.parent_file = Path("root.yaml") + self.load_calls = 0 + + def has_unresolved_expressions(self) -> bool: + return False + + def load(self) -> dict[str, Any]: + self.load_calls += 1 + return {} + + stub = _StubInclude() + # Same instance appears twice — second visit must hit the _seen guard. + tree = {"a": stub, "b": [stub]} + + with patch("esphome.bundle.yaml_util.IncludeFile", _StubInclude): + _force_load_include_files(tree) + + assert stub.load_calls == 1 + + +def test_force_load_handles_cyclic_containers() -> None: + """Cyclic dict/list references don't cause infinite recursion.""" + cyclic_dict: dict[str, Any] = {} + cyclic_dict["self"] = cyclic_dict + + cyclic_list: list[Any] = [] + cyclic_list.append(cyclic_list) + + # Should return without recursing forever + _force_load_include_files(cyclic_dict) + _force_load_include_files(cyclic_list) + + def test_discover_files_yaml_reload_failure( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1008,6 +1122,40 @@ def test_discover_files_walk_tuple_values(tmp_path: Path) -> None: assert "a.pem" in paths +# --------------------------------------------------------------------------- +# ConfigBundleCreator - fixture-based end-to-end +# --------------------------------------------------------------------------- + + +def test_discover_files_fixture_config(fixture_path: Path, tmp_path: Path) -> None: + """Use the real ``fixtures/bundle/`` tree as an end-to-end reproducer. + + The fixture config uses ``wifi: !include common/wifi.yaml`` — a plain + nested !include that is returned as a deferred ``IncludeFile`` and only + resolved during the substitution pass. Before this fix, bundle discovery + never ran substitutions, so ``common/wifi.yaml`` was silently missing + from the bundle. + """ + # Copy the fixture tree into a tmp dir so the test doesn't rely on the + # source repo being writable and so we can set CORE.config_path freely. + src = fixture_path / "bundle" + dst = tmp_path / "bundle" + shutil.copytree(src, dst) + + CORE.config_path = dst / "bundle_test.yaml" + + creator = ConfigBundleCreator({}) + files = creator.discover_files() + paths = {f.path for f in files} + + # Root and top-level !secret-referenced files + assert "bundle_test.yaml" in paths + assert "secrets.yaml" in paths + # The nested !include — this is what regressed when IncludeFile became + # deferred (PR #12213). + assert "common/wifi.yaml" in paths + + # --------------------------------------------------------------------------- # ConfigBundleCreator - create_bundle # --------------------------------------------------------------------------- From 9cb2b562b9e6706dc00117a7ac6299730eebc100 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 09:01:55 -1000 Subject: [PATCH 1519/2056] [ili9xxx] Guard against null buffer in display_() when allocation fails (#15786) --- esphome/components/ili9xxx/ili9xxx_defines.h | 2 ++ esphome/components/ili9xxx/ili9xxx_display.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/ili9xxx/ili9xxx_defines.h b/esphome/components/ili9xxx/ili9xxx_defines.h index f4c5aad957c..70e0937f790 100644 --- a/esphome/components/ili9xxx/ili9xxx_defines.h +++ b/esphome/components/ili9xxx/ili9xxx_defines.h @@ -1,5 +1,7 @@ #pragma once +#include <cstdint> + namespace esphome { namespace ili9xxx { diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index a3eff901d30..11acb8a73a4 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -229,6 +229,10 @@ void ILI9XXXDisplay::update() { } void ILI9XXXDisplay::display_() { + // buffer may be null if allocation failed + if (this->buffer_ == nullptr) { + return; + } // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { return; From 722cfae04c4ce7745701f783f2f9c75fed2243f7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:07:04 -0400 Subject: [PATCH 1520/2056] [esp32] Accept unquoted minimum_chip_revision values (#15785) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7b3f9da3da6..a68614cb436 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1222,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( - *ESP32_CHIP_REVISIONS + *ESP32_CHIP_REVISIONS, string=True ), cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, From b167b64f06ea724824ea5601ab024bac42c235a8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:16:58 -0400 Subject: [PATCH 1521/2056] [lvgl] Guard lv_image_set_src wrapper with LV_USE_IMAGE (#15789) --- esphome/components/lvgl/lvgl_esphome.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ba258b1a2b..3ec1d247d8e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -76,16 +76,17 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { } #endif #if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) -// Shortcut / overload, so that the source of an image can easily be updated -// from within a lambda. -inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); } +#if LV_USE_IMAGE +// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda. +inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); } +#endif // LV_USE_IMAGE inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); + ::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); } inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); + ::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG From c3e739eba9c73c34ce843c7b6cc1bdcee11355c4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:17:16 +1000 Subject: [PATCH 1522/2056] [mipi_spi] Drawing fixes for native display (#15802) --- esphome/components/mipi_spi/display.py | 6 +- esphome/components/mipi_spi/mipi_spi.h | 14 +- .../mipi_spi/test_final_validate.py | 185 ++++++++++++++++++ 3 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 tests/component_tests/mipi_spi/test_final_validate.py diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 42c7ec22249..364ada90463 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -195,7 +195,7 @@ def model_schema(config): "big_endian", "little_endian", lower=True ), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), - model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_DRAW_ROUNDING, 1): power_of_two, model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( *pixel_modes, lower=True ), @@ -297,9 +297,9 @@ def _final_validate(config): buffer_size = color_depth // 8 * width * height // frac # Target a buffer size of 20kB, except for large displays, which shouldn't end up here - fraction = min(20000.0, buffer_size // 16) / buffer_size + fraction = min(20000.0, buffer_size // 4) / buffer_size config[CONF_BUFFER_SIZE] = 1.0 / next( - x for x in range(2, 17) if fraction >= 1 / x + (x for x in range(2, 8) if fraction >= 1 / x), 8 ) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 2242be6c17b..f2923458937 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -546,13 +546,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS } // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of // the display height, - for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); - this->start_line_ += this->get_height_internal() / FRACTION) { + auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING; + for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) { #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE auto lap = millis(); #endif - this->end_line_ = - clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal()); + this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal()); if (this->auto_clear_enabled_) { this->clear(); } @@ -574,12 +573,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS // Some chips require that the drawing window be aligned on certain boundaries this->x_low_ = this->x_low_ / ROUNDING * ROUNDING; this->y_low_ = this->y_low_ / ROUNDING * ROUNDING; - this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; - this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1; + this->x_high_ = round_buffer(this->x_high_ + 1) - 1; + this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1); int w = this->x_high_ - this->x_low_ + 1; int h = this->y_high_ - this->y_low_ + 1; this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, - this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w); + this->y_low_ - this->start_line_, + round_buffer(this->get_width_internal()) - w - this->x_low_); // invalidate watermarks this->x_low_ = this->get_width_internal(); this->y_low_ = this->get_height_internal(); diff --git a/tests/component_tests/mipi_spi/test_final_validate.py b/tests/component_tests/mipi_spi/test_final_validate.py new file mode 100644 index 00000000000..8c45b47752c --- /dev/null +++ b/tests/component_tests/mipi_spi/test_final_validate.py @@ -0,0 +1,185 @@ +"""Tests for the _final_validate buffer size calculation in mipi_spi.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32 +from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA +from esphome.const import CONF_BUFFER_SIZE, PlatformFramework +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def _validated(config: ConfigType) -> ConfigType: + """Run the component config schema followed by the final validation.""" + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config + + +def _custom_config( + width: int, + height: int, + color_depth: str | int | None = None, + **extra: Any, +) -> ConfigType: + """Build a minimal valid custom-model config with the given dimensions.""" + config: ConfigType = { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": width, "height": height}, + "init_sequence": [[0xA0, 0x01]], + } + if color_depth is not None: + config["color_depth"] = color_depth + config.update(extra) + return config + + +# The auto buffer-size selection inside _final_validate targets ~20 kB of +# pixel buffer. For a buffer of ``depth_bytes * width * height``, it picks the +# smallest integer ``x`` in range(2, 8) such that +# ``min(20000, buffer // 4) / buffer >= 1 / x`` (falling back to ``x = 8``). +# The test cases below cover the full range of possible outcomes (1/4 .. 1/8). +@pytest.mark.parametrize( + ("width", "height", "color_depth", "expected"), + [ + # 16-bit color depth -- buffer = 2 * width * height + # 128*160*2 = 40960 B -> fraction = 10240/40960 = 0.25 -> x = 4 + pytest.param(128, 160, "16bit", 1.0 / 4, id="16bit_tiny"), + # 200*224*2 = 89600 B -> fraction = 20000/89600 ≈ 0.2232 -> x = 5 + pytest.param(200, 224, "16bit", 1.0 / 5, id="16bit_small"), + # 240*224*2 = 107520 B -> fraction ≈ 0.1860 -> x = 6 + pytest.param(240, 224, "16bit", 1.0 / 6, id="16bit_medium"), + # 200*320*2 = 128000 B -> fraction = 0.15625 -> x = 7 + pytest.param(200, 320, "16bit", 1.0 / 7, id="16bit_large"), + # 240*320*2 = 153600 B -> fraction ≈ 0.1302 -> default x = 8 + pytest.param(240, 320, "16bit", 1.0 / 8, id="16bit_xlarge"), + # 320*480*2 = 307200 B -> fraction ≈ 0.0651 -> default x = 8 + pytest.param(320, 480, "16bit", 1.0 / 8, id="16bit_huge"), + # 8-bit color depth -- buffer = width * height + # 320*240 = 76800 B -> fraction = 19200/76800 = 0.25 -> x = 4 + pytest.param(320, 240, "8bit", 1.0 / 4, id="8bit_tiny"), + # 400*224 = 89600 B -> fraction ≈ 0.2232 -> x = 5 + pytest.param(400, 224, "8bit", 1.0 / 5, id="8bit_small"), + # 480*224 = 107520 B -> fraction ≈ 0.1860 -> x = 6 + pytest.param(480, 224, "8bit", 1.0 / 6, id="8bit_medium"), + # 400*320 = 128000 B -> fraction = 0.15625 -> x = 7 + pytest.param(400, 320, "8bit", 1.0 / 7, id="8bit_large"), + # 480*320 = 153600 B -> fraction ≈ 0.1302 -> default x = 8 + pytest.param(480, 320, "8bit", 1.0 / 8, id="8bit_xlarge"), + ], +) +def test_buffer_size_auto_selected( + width: int, + height: int, + color_depth: str, + expected: float, + set_core_config: SetCoreConfigCallable, +) -> None: + """Without PSRAM or an explicit buffer_size, a fraction is chosen from the display size. + + Without any drawing method and without LVGL, final validation also auto-enables + ``show_test_card``, which in turn makes the component require a buffer and therefore + triggers the buffer-size selection path. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + config = _validated(_custom_config(width, height, color_depth)) + + # Sanity check: final validation should have enabled the test card for us, + # which is what causes the buffer-size calculation to actually run. + assert config.get(CONF_SHOW_TEST_CARD) is True + assert config[CONF_BUFFER_SIZE] == pytest.approx(expected) + + +@pytest.mark.parametrize( + "buffer_size", + [0.125, 0.25, 0.5, 1.0], + ids=["one_eighth", "one_quarter", "half", "full"], +) +def test_explicit_buffer_size_is_preserved( + buffer_size: float, + set_core_config: SetCoreConfigCallable, +) -> None: + """An explicitly configured buffer_size is never overridden by final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + config = _validated( + _custom_config(240, 320, "16bit", buffer_size=buffer_size), + ) + + assert config[CONF_BUFFER_SIZE] == pytest.approx(buffer_size) + + +def test_buffer_size_not_set_when_psram_enabled( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """When PSRAM is enabled the auto buffer-size selection is skipped.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + # Presence of the psram domain in the full config is what _final_validate checks. + set_component_config("psram", True) + + config = _validated(_custom_config(240, 320, "16bit")) + + assert CONF_BUFFER_SIZE not in config + + +def test_buffer_size_not_set_when_buffer_not_required( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """With LVGL present and no drawing methods, no buffer fraction is chosen. + + LVGL suppresses the automatic show_test_card injection, which means + ``requires_buffer`` is False and the early-return branch fires. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + set_component_config("lvgl", []) + + config = _validated(_custom_config(240, 320, "16bit")) + + assert CONF_BUFFER_SIZE not in config + # And no test card should have been auto-enabled either. + assert not config.get(CONF_SHOW_TEST_CARD) + + +def test_buffer_size_selected_when_lvgl_with_test_card( + set_core_config: SetCoreConfigCallable, + set_component_config, +) -> None: + """LVGL present + an explicit drawing method still triggers buffer sizing. + + When LVGL is enabled, ``show_test_card`` is not injected automatically, + but users can still request it explicitly -- in that case ``requires_buffer`` + is True and the buffer-size heuristic still runs. + """ + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + set_component_config("lvgl", []) + + # 128x160 @ 16bit -> expected 1/4 (see test_buffer_size_auto_selected). + config = _validated( + _custom_config(128, 160, "16bit", show_test_card=True), + ) + + assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4) From f5806818cd4b087fa282d1c3b98f618f8c8e3c10 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:11:05 +1000 Subject: [PATCH 1523/2056] [image] Fix byte order handling (#15800) --- esphome/components/image/__init__.py | 17 ++++++++++------- esphome/components/image/image.cpp | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4a5fcc385e8..7db50597e6f 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -28,7 +28,6 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt -from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -676,12 +675,16 @@ def _final_validate(config): :param config: :return: """ - fv = full_config.get() - if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config): - config = config.copy() - for c in config: - if not c.get(CONF_BYTE_ORDER): - c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" + config = config.copy() + for c in config: + if byte_order := c.get(CONF_BYTE_ORDER): + if byte_order == "BIG_ENDIAN": + _LOGGER.warning( + "The image '%s' is configured with big-endian byte order, little-endian is expected", + c.get(CONF_FILE), + ) + else: + c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" return config diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index a6f9e35e2e5..5b4ed6968c6 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const { } Color Image::get_rgb565_pixel_(int x, int y) const { const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8; - uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); + uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos)); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; From b26601a3dcfc284153b6c5405dcfe91f6356d8c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 02:45:50 -1000 Subject: [PATCH 1524/2056] [core] coerce set_interval(0) / update_interval: 0ms to 1ms (#15799) --- esphome/config_validation.py | 21 +++++- esphome/core/scheduler.cpp | 13 ++++ .../scheduler_interval_zero_coerced.yaml | 27 ++++++++ .../test_scheduler_interval_zero_coerced.py | 67 +++++++++++++++++++ tests/unit_tests/test_config_validation.py | 28 ++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/scheduler_interval_zero_coerced.yaml create mode 100644 tests/integration/test_scheduler_interval_zero_coerced.py diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 31cfb41a6d9..bf53013d9ba 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -943,7 +943,26 @@ def time_period_in_minutes_(value): def update_interval(value): if value == "never": return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN) - return positive_time_period_milliseconds(value) + result = positive_time_period_milliseconds(value) + # 0ms was historically (mis)used as a pseudo-loop() mechanism for + # PollingComponents. Under the hood it calls set_interval(0), which + # causes Scheduler::call() to spin (WDT reset in the field). Coerce + # to 1ms so existing configs keep working at ~1kHz instead of + # spinning. Don't hard-fail so configs don't break on upgrade; + # authors should migrate to HighFrequencyLoopRequester (C++) for + # true run-every-loop behaviour. + if result.total_milliseconds == 0: + _LOGGER.warning( + "update_interval of 0ms is not supported - coercing to 1ms. " + "A literal 0ms schedule would spin the main loop (the scheduled " + "item would always be due, so the scheduler would never yield " + "back) and trigger a watchdog reset. Set update_interval to a " + "non-zero value such as 1ms or higher. (Custom C++ components " + "that need true run-every-loop behaviour should override loop() " + "with HighFrequencyLoopRequester instead.)" + ) + return TimePeriodMilliseconds(milliseconds=1) + return result time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 3e75a680643..7e6ad19ac70 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -144,6 +144,19 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } + // An interval of 0 means "fire every tick forever," which is misuse: the + // item would always be due, causing Scheduler::call() to spin and starve + // the main loop (WDT reset in the field). Coerce to 1ms so existing code + // using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz, + // and warn so authors can migrate to HighFrequencyLoopRequester which is + // the intended mechanism for running fast in the main loop. Zero-delay + // timeouts (defer) remain legitimate one-shots and are not affected. + if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] { + ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)", + component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?")); + delay = 1; + } + // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check LockGuard guard{this->lock_}; diff --git a/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml b/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml new file mode 100644 index 00000000000..13be55d6176 --- /dev/null +++ b/tests/integration/fixtures/scheduler_interval_zero_coerced.yaml @@ -0,0 +1,27 @@ +esphome: + name: sched-interval-zero + +host: +api: +logger: + level: DEBUG + +globals: + - id: fire_count + type: int + initial_value: "0" + +interval: + # Deliberately configure 0ms — this path goes through the C++ + # Scheduler::set_timer_common_ coercion (not the Python cv.update_interval + # path, since interval: doesn't call cv.update_interval — it's an intervals + # component schema, not a PollingComponent's update_interval). + # Expected: scheduler coerces to 1ms at registration, emits ESP_LOGE, + # fires at ~1kHz instead of spinning. + - interval: 0ms + then: + - lambda: |- + id(fire_count) += 1; + if (id(fire_count) == 50) { + ESP_LOGI("test", "ZERO_INTERVAL_50_FIRES_REACHED"); + } diff --git a/tests/integration/test_scheduler_interval_zero_coerced.py b/tests/integration/test_scheduler_interval_zero_coerced.py new file mode 100644 index 00000000000..f71c0f7281f --- /dev/null +++ b/tests/integration/test_scheduler_interval_zero_coerced.py @@ -0,0 +1,67 @@ +"""Test that Scheduler::set_timer_common_ coerces interval=0 to 1ms. + +Regression test for the scheduler busy-loop when interval=0 was passed +literally. Without the coercion, Scheduler::call() would spin forever +because the item's next_execution == now_64 after re-scheduling, failing +the loop's `> now_64` break condition. The device would fail to yield +back to the main loop and trigger a WDT reset. + +With the coercion, interval=0 becomes interval=1 and the scheduler +fires at ~1kHz (bounded by the loop), the main loop continues to run, +and the device stays responsive to API calls. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_interval_zero_coerced( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """interval=0ms must be coerced to 1ms and not starve the main loop.""" + loop = asyncio.get_running_loop() + reached_50: asyncio.Future[None] = loop.create_future() + coerce_warning: asyncio.Future[None] = loop.create_future() + + def on_log_line(line: str) -> None: + if "ZERO_INTERVAL_50_FIRES_REACHED" in line and not reached_50.done(): + reached_50.set_result(None) + if "would spin main loop" in line and not coerce_warning.done(): + coerce_warning.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # The API-client connection itself is evidence that the main loop + # is not starved — if set_interval(0) were spinning we could not + # get here at all. + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-interval-zero" + + # Coerce warning must fire at registration + try: + await asyncio.wait_for(coerce_warning, timeout=5.0) + except TimeoutError: + pytest.fail("Expected coerce warning 'would spin main loop' not seen") + + # The coerced 1ms interval should fire 50 times quickly — this + # confirms the callback actually runs (not just registered) and the + # scheduler yields back to the main loop each time. + try: + await asyncio.wait_for(reached_50, timeout=5.0) + except TimeoutError: + pytest.fail( + "Coerced interval=0→1ms did not reach 50 fires within 5s, " + "which would indicate either the coercion failed or the " + "main loop is still being starved." + ) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index ac84ce7cc89..f038272d8b0 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -24,6 +24,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, + SCHEDULER_DONT_RUN, ) from esphome.core import CORE, HexInt, Lambda @@ -765,3 +766,30 @@ def test_percentage_validators__raw_number_above_one_without_percent_sign( config_validation.unbounded_percentage(value) with pytest.raises(Invalid, match="percent sign"): config_validation.unbounded_possibly_negative_percentage(value) + + +def test_update_interval__coerces_zero_to_one_ms( + caplog: pytest.LogCaptureFixture, +) -> None: + """update_interval: 0ms must be coerced to 1ms (not rejected) because a + literal 0ms schedule causes Scheduler::call() to spin. Coercion keeps + existing configs compiling on upgrade while emitting a user-facing + warning that directs them to set a non-zero value.""" + with caplog.at_level("WARNING"): + result = config_validation.update_interval("0ms") + assert result.total_milliseconds == 1 + assert "update_interval of 0ms is not supported" in caplog.text + assert "1ms" in caplog.text + + +def test_update_interval__preserves_nonzero_values() -> None: + """Non-zero update_interval values must pass through unchanged.""" + assert config_validation.update_interval("1ms").total_milliseconds == 1 + assert config_validation.update_interval("50ms").total_milliseconds == 50 + assert config_validation.update_interval("60s").total_milliseconds == 60000 + + +def test_update_interval__never_passes_through() -> None: + """update_interval: never must still map to SCHEDULER_DONT_RUN.""" + result = config_validation.update_interval("never") + assert result.total_milliseconds == SCHEDULER_DONT_RUN From ed5852c2d6a64b7618bdabb35c67f650afd59fce Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:05:30 -0400 Subject: [PATCH 1525/2056] [ethernet] Fix SPI3_HOST default breaking compile on variants without SPI3 (#15809) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/ethernet/ethernet_component.h | 2 +- .../ethernet/test.esp32-c3-idf.yaml | 19 +++++++++++++++++++ ...720.esp32-idf.yaml => test.esp32-idf.yaml} | 0 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/components/ethernet/test.esp32-c3-idf.yaml rename tests/components/ethernet/{test-lan8720.esp32-idf.yaml => test.esp32-idf.yaml} (100%) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 3a878423156..17c84ee9543 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -221,7 +221,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; - spi_host_device_t interface_{SPI3_HOST}; + spi_host_device_t interface_{SPI2_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif diff --git a/tests/components/ethernet/test.esp32-c3-idf.yaml b/tests/components/ethernet/test.esp32-c3-idf.yaml new file mode 100644 index 00000000000..b7b95875c6d --- /dev/null +++ b/tests/components/ethernet/test.esp32-c3-idf.yaml @@ -0,0 +1,19 @@ +ethernet: + type: W5500 + clk_pin: 6 + mosi_pin: 7 + miso_pin: 2 + cs_pin: 10 + interrupt_pin: 3 + reset_pin: 4 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-lan8720.esp32-idf.yaml b/tests/components/ethernet/test.esp32-idf.yaml similarity index 100% rename from tests/components/ethernet/test-lan8720.esp32-idf.yaml rename to tests/components/ethernet/test.esp32-idf.yaml From 9841deec31cb1d70a19228fb6691ba5b6acfaa16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 17 Apr 2026 09:31:31 -0500 Subject: [PATCH 1526/2056] [core] Fix DelayAction compile error with non-const reference args (#15814) --- esphome/core/base_automation.h | 4 +++- tests/components/http_request/http_request.yaml | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 11133d39739..17f937d10d9 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -205,7 +205,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon } else { // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay - auto f = [this, x...]() { this->play_next_(x...); }; + // `mutable` is required so captured copies of non-const reference args (e.g. std::string&) + // are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&` + auto f = [this, x...]() mutable { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index 13ca5ceba03..ef67671c91d 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -45,6 +45,11 @@ esphome: args: - response->status_code - body.c_str() + - delay: 1s + - logger.log: + format: "After delay, body still: %s" + args: + - body.c_str() http_request: useragent: esphome/tagreader From 1d88027618d06662b8b4df1e1d047922ff1330dc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:14:54 -0400 Subject: [PATCH 1527/2056] [esp32] Downgrade unneeded `ignore_pin_validation_error` to a warning (#15811) --- esphome/components/esp32/gpio.py | 12 +++- tests/component_tests/esp32/test_esp32.py | 79 ++++++++++++++++++++++- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index a7180cbcd7e..36dd44155ae 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -172,10 +172,16 @@ def validate_gpio_pin(pin): exc, ) else: - # Throw an exception if used for a pin that would not have resulted - # in a validation error anyway! + # `ignore_pin_validation_error` only suppresses an error raised by the + # variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin + # numbers). If that didn't raise, the option is a no-op -- warn so the + # user can clean it up, but don't block the build. if ignore_pin_validation_warning: - raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin") + _LOGGER.warning( + "GPIO%d has no validation errors to ignore; " + "remove `ignore_pin_validation_error: true` from this pin.", + pin[CONF_NUMBER], + ) return pin diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index bd4f9828ce5..ac492e27529 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -8,10 +8,16 @@ from typing import Any import pytest -from esphome.components.esp32 import VARIANTS -from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS +from esphome.components.esp32 import VARIANT_ESP32, VARIANTS +from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT +from esphome.components.esp32.gpio import validate_gpio_pin import esphome.config_validation as cv -from esphome.const import CONF_ESPHOME, PlatformFramework +from esphome.const import ( + CONF_ESPHOME, + CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_NUMBER, + PlatformFramework, +) from esphome.core import CORE from tests.component_tests.types import SetCoreConfigCallable @@ -149,6 +155,73 @@ def test_execute_from_psram_p4_sdkconfig( assert "CONFIG_SPIRAM_RODATA" not in sdkconfig +def test_ignore_pin_validation_error_on_clean_pin_warns( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A pin that passes validation but sets `ignore_pin_validation_error: true` + should log a warning nudging the user to remove the flag, and not raise.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 4 + assert "GPIO4 has no validation errors to ignore" in caplog.text + + +def test_ignore_pin_validation_error_on_dirty_pin_suppresses( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A pin that fails validation with `ignore_pin_validation_error: true` should + log the suppression warning and not raise (existing behavior).""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + # GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid + pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 6 + assert "Ignoring validation error on pin 6" in caplog.text + + +def test_dirty_pin_without_ignore_flag_raises( + set_core_config: SetCoreConfigCallable, +) -> None: + """A pin that fails validation without the ignore flag should still raise.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + with pytest.raises(cv.Invalid, match="flash interface"): + validate_gpio_pin(pin) + + +def test_clean_pin_without_ignore_flag_does_not_warn( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """A clean pin without the ignore flag should pass silently.""" + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32} + ) + + pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + with caplog.at_level("WARNING"): + result = validate_gpio_pin(pin) + + assert result[CONF_NUMBER] == 4 + assert "has no validation errors to ignore" not in caplog.text + + def test_execute_from_psram_disabled_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From e2dfef5ddce725e7f3323e43749452c4ed93c324 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 18 Apr 2026 06:42:45 +1000 Subject: [PATCH 1528/2056] [runtime_image] Fix RGB order (#15813) --- esphome/components/runtime_image/runtime_image.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index fa42b53496b..4c7f1bfb6f3 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { uint32_t pos = this->get_position_(x, y); Color mapped_color = color; this->map_chroma_key(mapped_color); - this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 0] = mapped_color.b; this->buffer_[pos + 1] = mapped_color.g; - this->buffer_[pos + 2] = mapped_color.b; + this->buffer_[pos + 2] = mapped_color.r; if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 3] = color.w; } From 6d5340f253009a1421329bcff3c6251ca0e6aa7d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:17:28 +1000 Subject: [PATCH 1529/2056] [lvgl] Fix crash with snow on rotated display (#15822) --- esphome/components/lvgl/lvgl_esphome.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index ce9b013dcf1..d8248e4aa4e 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -642,26 +642,28 @@ void LvglComponent::write_random_() { int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) iterations = 1; + int16_t width = lv_display_get_horizontal_resolution(this->disp_); + int16_t height = lv_display_get_vertical_resolution(this->disp_); while (iterations-- != 0) { - int32_t col = random_uint32() % this->width_; + int32_t col = random_uint32() % width; col = col / this->draw_rounding * this->draw_rounding; - int32_t row = random_uint32() % this->height_; + int32_t row = random_uint32() % height; row = row / this->draw_rounding * this->draw_rounding; // size will be between 8 and 32, and a multiple of draw_rounding int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding; - lv_area_t area{col, row, col + size - 1, row + size - 1}; + lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1}; // clip to display bounds just in case - if (area.x2 >= this->width_) - area.x2 = this->width_ - 1; - if (area.y2 >= this->height_) - area.y2 = this->height_ - 1; + if (area.x2 >= width) + area.x2 = width - 1; + if (area.y2 >= height) + area.y2 = height - 1; // line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2; for (size_t i = 0; i != line_len; i++) { - ((uint32_t *) (this->draw_buf_))[i] = random_uint32(); + reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32(); } - this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_); + this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_)); } } From 08ac61ae9424821ac868d817971313d1e8508394 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 06:58:54 -0500 Subject: [PATCH 1530/2056] [core] Feed WDT unconditionally in main loop to fix empty-config panic (#15830) --- esphome/core/application.h | 26 +++++++++++++++++--------- esphome/core/scheduler.cpp | 5 ++++- esphome/core/scheduler.h | 3 ++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 60087d527d2..db8af735bd4 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -641,7 +641,7 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. @@ -845,18 +845,15 @@ inline void Application::drain_wake_notifications_() { } #endif // USE_HOST -inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { #ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif - // Process scheduled tasks. Scheduler::call now feeds the watchdog itself - // after each scheduled item that actually runs, so we no longer need an - // unconditional feed here — when Scheduler::call has no work to do, the - // only elapsed time is a sleep wake + a few instructions, and when it does - // have work, it fed the wdt as it went. - this->scheduler.call(loop_start_time); + // Scheduler::call feeds the WDT per item and returns the timestamp of the + // last fired item, or the input unchanged when nothing ran. + uint32_t last_op_end_time = this->scheduler.call(loop_start_time); // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions @@ -874,13 +871,24 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ // Mark that we're in the loop for safe reentrant modifications this->in_loop_ = true; + return last_op_end_time; } inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Get the initial loop time at the start uint32_t last_op_end_time = millis(); - this->before_loop_tasks_(last_op_end_time); + // Returned timestamp keeps us monotonic with last_wdt_feed_ (advanced by + // the scheduler's per-item feeds) without an extra millis() call. + last_op_end_time = this->before_loop_tasks_(last_op_end_time); + // Guarantee a WDT touch every tick — covers configs with no looping + // components and no scheduler work, where the per-item / per-component + // feeds never fire. Rate-limited inline fast path, ~free when unneeded. + this->feed_wdt_with_time(last_op_end_time); +#ifdef USE_RUNTIME_STATS + uint32_t loop_before_end_us = micros(); + uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap; +#endif for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; this->current_loop_index_++) { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7e6ad19ac70..b0eaa670ac2 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -533,7 +533,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) { } #endif /* not ESPHOME_THREAD_SINGLE */ -void HOT Scheduler::call(uint32_t now) { +uint32_t HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ @@ -703,6 +703,9 @@ void HOT Scheduler::call(uint32_t now) { this->debug_verify_no_leak_(); } #endif + // execute_item_() advances `now` as items fire; return it so the caller + // stays monotonic with last_wdt_feed_. + return now; } void HOT Scheduler::process_to_add_slow_path_() { LockGuard guard{this->lock_}; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 43a3ec7049b..00a7f269538 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -129,7 +129,8 @@ class Scheduler { // Execute all scheduled items that are ready // @param now Fresh timestamp from millis() - must not be stale/cached - void call(uint32_t now); + // @return Timestamp of the last item that ran, or `now` unchanged if none ran. + uint32_t call(uint32_t now); // Move items from to_add_ into the main heap. // IMPORTANT: This method should only be called from the main thread (loop task). From 78701debec764ae9e0ba6339eb27b8ff96a8ba46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:32:36 +0000 Subject: [PATCH 1531/2056] Bump aioesphomeapi from 44.16.0 to 44.16.1 (#15836) 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 b14d5f5f5a3..25638c0e7bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.16.0 +aioesphomeapi==44.16.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 602305b20de2ab6da44baa4e113eb3016115365e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 18 Apr 2026 08:04:51 -0500 Subject: [PATCH 1532/2056] [core] Default PollingComponent() to 1ms when codegen is bypassed (#15831) --- esphome/core/component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 3307c5ae76e..9e339dce649 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -593,7 +593,7 @@ class Component { */ class PollingComponent : public Component { public: - PollingComponent() : PollingComponent(0) {} + PollingComponent() : PollingComponent(1) {} /** Initialize this polling component with the given update interval in ms. * From ef780886c35c019fdbdb9c0e4a1fe343958897e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 19 Apr 2026 16:00:31 -0500 Subject: [PATCH 1533/2056] [substitutions] Fix `substitutions: !include file.yaml` regression (#15850) --- esphome/components/packages/__init__.py | 8 ++- esphome/components/substitutions/__init__.py | 35 +++++++++++-- .../15-substitutions_as_include.approved.yaml | 5 ++ .../15-substitutions_as_include.input.yaml | 5 ++ .../substitutions/15-substitutions_inc.yaml | 1 + ...ons_as_include_with_packages.approved.yaml | 5 ++ ...utions_as_include_with_packages.input.yaml | 9 ++++ ...ubstitutions_include_cli_var.approved.yaml | 6 +++ ...7-substitutions_include_cli_var.input.yaml | 8 +++ tests/unit_tests/test_substitutions.py | 51 +++++++++++++++++++ 10 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 3f3df753512..252a24061a0 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -10,6 +10,7 @@ from esphome.components.substitutions import ( ContextVars, push_context, resolve_include, + resolve_substitutions_block, substitute, ) from esphome.components.substitutions.jinja import has_jinja @@ -516,7 +517,12 @@ def do_packages_pass( if CONF_PACKAGES not in config: return config - substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + with cv.prepend_path(CONF_SUBSTITUTIONS): + substitutions = UserDict( + resolve_substitutions_block( + config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions + ) + ) processor = _PackageProcessor( substitutions, command_line_substitutions, skip_update ) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index c0bd9d7be97..7aace9bb648 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -414,6 +414,34 @@ def _warn_unresolved_variables(errors: ErrList) -> None: ) +def resolve_substitutions_block( + substitutions: Any, + command_line_substitutions: dict[str, Any] | None, +) -> dict[str, Any]: + """Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape. + + The caller is responsible for wrapping the call in + ``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting. + ``command_line_substitutions`` seeds the filename context so + ``substitutions: !include ${var}.yaml`` can reference CLI-provided vars. + """ + if isinstance(substitutions, IncludeFile): + # Single-shot resolution — matches ``_walk_packages`` for the + # ``packages: !include`` entry point. Chained includes (an include that + # itself loads another ``!include`` at the top level) are not supported. + substitutions, _ = resolve_include( + substitutions, + [], + ContextVars(command_line_substitutions or {}), + strict_undefined=False, + ) + if not isinstance(substitutions, dict): + raise cv.Invalid( + f"Substitutions must be a key to value mapping, got {type(substitutions)}" + ) + return substitutions + + def do_substitution_pass( config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None ) -> OrderedDict: @@ -429,10 +457,9 @@ def do_substitution_pass( # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() substitutions = config.pop(CONF_SUBSTITUTIONS, {}) with cv.prepend_path(CONF_SUBSTITUTIONS): - if not isinstance(substitutions, dict): - raise cv.Invalid( - f"Substitutions must be a key to value mapping, got {type(substitutions)}" - ) + substitutions = resolve_substitutions_block( + substitutions, command_line_substitutions + ) substitutions = merge_dicts_ordered( substitutions, command_line_substitutions or {} ) diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml new file mode 100644 index 00000000000..14aa707def5 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.approved.yaml @@ -0,0 +1,5 @@ +substitutions: + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml new file mode 100644 index 00000000000..5909e7bf4f7 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_as_include.input.yaml @@ -0,0 +1,5 @@ +substitutions: !include 15-substitutions_inc.yaml + +wifi: + ssid: main_ssid + password: $wifi_password diff --git a/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml b/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml new file mode 100644 index 00000000000..44d9a4b9ef2 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/15-substitutions_inc.yaml @@ -0,0 +1 @@ +wifi_password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml new file mode 100644 index 00000000000..14aa707def5 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.approved.yaml @@ -0,0 +1,5 @@ +substitutions: + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml new file mode 100644 index 00000000000..a2e72f33a23 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/16-substitutions_as_include_with_packages.input.yaml @@ -0,0 +1,9 @@ +substitutions: !include 15-substitutions_inc.yaml + +packages: + wifi_pkg: + wifi: + password: $wifi_password + +wifi: + ssid: main_ssid diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml new file mode 100644 index 00000000000..f1fd5fb0786 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml @@ -0,0 +1,6 @@ +substitutions: + subs_file: 15-substitutions_inc + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml new file mode 100644 index 00000000000..3248504b468 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml @@ -0,0 +1,8 @@ +command_line_substitutions: + subs_file: 15-substitutions_inc + +substitutions: !include ${subs_file}.yaml + +wifi: + ssid: main_ssid + password: $wifi_password diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 01c669e5420..71bbd9db86d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -675,6 +675,57 @@ def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: substitutions.do_substitution_pass(config) +def test_do_substitution_pass_included_substitutions_must_be_mapping( + tmp_path: Path, +) -> None: + """`substitutions: !include list.yaml` where the file holds a list raises cv.Invalid. + + Locks in the shape check that runs after the deferred IncludeFile has been + resolved. + """ + parent = tmp_path / "main.yaml" + parent.write_text("") + + def loader(path: Path): + return ["not", "a", "mapping"] + + include = yaml_util.IncludeFile(parent, "subs.yaml", None, loader) + config = OrderedDict({CONF_SUBSTITUTIONS: include}) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + substitutions.do_substitution_pass(config) + + +def test_do_packages_pass_included_substitutions_must_be_mapping( + tmp_path: Path, +) -> None: + """`substitutions: !include list.yaml` alongside `packages:` raises cv.Invalid. + + Without the shape check, ``UserDict(...)`` would surface a low-level + ``TypeError``; the explicit ``cv.Invalid`` points at the substitutions path. + """ + parent = tmp_path / "main.yaml" + parent.write_text("") + + def loader(path: Path): + return ["not", "a", "mapping"] + + include = yaml_util.IncludeFile(parent, "subs.yaml", None, loader) + config = OrderedDict( + { + CONF_SUBSTITUTIONS: include, + "packages": {"noop": {"wifi": {"ssid": "main"}}}, + } + ) + + with pytest.raises( + cv.Invalid, match="Substitutions must be a key to value mapping" + ): + do_packages_pass(config) + + def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> None: """An undefined substitution in a package include filename raises cv.Invalid. From 1862c6115f51e89587dd8a1dd9462ae7fac18464 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:09:14 +0200 Subject: [PATCH 1534/2056] [packages] Improve error messages with include stack and fix missing path propagation (#15844) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/packages/__init__.py | 9 ++ esphome/components/substitutions/__init__.py | 50 +++++++++++ .../component_tests/packages/test_packages.py | 88 ++++++++++++++++++- tests/unit_tests/test_substitutions.py | 34 +++++++ 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 252a24061a0..97a53094800 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -8,7 +8,9 @@ from typing import Any from esphome import git, yaml_util from esphome.components.substitutions import ( ContextVars, + ErrList, push_context, + raise_first_undefined, resolve_include, resolve_substitutions_block, substitute, @@ -360,12 +362,19 @@ def _substitute_package_definition( if isinstance(package_config, str) or ( isinstance(package_config, dict) and is_remote_package(package_config) ): + # Collect undefined-variable errors (rather than raising strict) so the + # path walked through a remote-package dict is preserved and the user + # sees which field (url / path / ref / ...) referenced the undefined + # variable. + errors: ErrList = [] package_config = substitute( item=package_config, path=[], parent_context=context_vars or ContextVars(), strict_undefined=False, + errors=errors, ) + raise_first_undefined(errors, package_config, "package definition") return package_config diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 7aace9bb648..0144c13c017 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -30,6 +30,56 @@ ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] jinja = Jinja() +def raise_first_undefined( + errors: ErrList, + source: Any, + context_label: str, +) -> None: + """If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable. + + The raised error names the missing variable, the path walked into *source* + (for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location + when *source* carries one. Only the first error is surfaced; the user will + re-run after fixing it and any remaining undefined variables will be + reported then. + + ``context_label`` is the noun describing where the undefined variable + appeared (e.g. ``"package definition"``). + """ + if not errors: + return + err, err_path, err_value = errors[0] + if len(errors) > 1: + # Log any further undefined variables so debug-level output covers + # the full set, even though only the first is surfaced to the user. + extras = ", ".join( + f"{e.message} at '{'->'.join(str(p) for p in p_path)}'" + for e, p_path, _ in errors[1:] + ) + _LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras) + # Prefer the location of the offending scalar (e.g. the `url:` value) over + # the enclosing package-definition dict so the message points at the exact + # line/column that carries the undefined variable. + location_node = ( + err_value + if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None + else source + ) + location = "" + if ( + isinstance(location_node, ESPHomeDataBase) + and location_node.esp_range is not None + ): + mark = location_node.esp_range.start_mark + # DocumentLocation.line/column are 0-based (from the YAML Mark). Render + # as 1-based to match config.line_info() and editor line numbering. + location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})" + field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else "" + raise cv.Invalid( + f"Undefined variable in {context_label}{field}: {err.message}{location}" + ) + + def validate_substitution_key(value: Any) -> str: """Validate and normalize a substitution key, stripping a leading ``$`` if present.""" value = cv.string(value) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index cd91c4d8cb1..0bd339efa9c 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -2,18 +2,20 @@ import logging from pathlib import Path +import re from unittest.mock import MagicMock, patch import pytest from esphome.components.packages import ( CONFIG_SCHEMA, + _substitute_package_definition, _walk_packages, do_packages_pass, is_package_definition, merge_packages, ) -from esphome.components.substitutions import do_substitution_pass +from esphome.components.substitutions import ContextVars, do_substitution_pass import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove @@ -44,7 +46,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict -from esphome.yaml_util import IncludeFile, add_context +from esphome.yaml_util import IncludeFile, add_context, load_yaml # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -1399,3 +1401,85 @@ def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: "CORE.raw_config should contain esphome section after package merge" ) assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME + + +# --------------------------------------------------------------------------- +# _substitute_package_definition +# --------------------------------------------------------------------------- + + +def test_substitute_package_definition_local_dict_returned_unchanged() -> None: + """A plain local config dict is not substituted and is returned as-is.""" + pkg = {CONF_WIFI: {CONF_SSID: "test"}} + result = _substitute_package_definition(pkg, ContextVars()) + assert result is pkg + + +def test_substitute_package_definition_string_resolved_with_context() -> None: + """A string package definition has its variables substituted.""" + ctx = ContextVars({"variant": "esp32"}) + result = _substitute_package_definition("device-${variant}.yaml", ctx) + assert result == "device-esp32.yaml" + + +def test_substitute_package_definition_undefined_in_string() -> None: + """An undefined variable in a package URL string raises cv.Invalid.""" + with pytest.raises(cv.Invalid, match="Undefined variable in package definition"): + _substitute_package_definition( + "github://org/repo/${undefined_var}/pkg.yaml", ContextVars() + ) + + +def test_substitute_package_definition_undefined_in_remote_dict_field() -> None: + """An undefined variable inside a remote-dict field names the offending field.""" + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition( + {CONF_URL: "github://${typo}/repo"}, ContextVars() + ) + err = str(exc_info.value) + assert "'typo' is undefined" in err + assert CONF_URL in err + + +def test_substitute_package_definition_undefined_in_remote_dict_non_first_field() -> ( + None +): + """The field path joins correctly for non-first dict fields (e.g. ``ref``).""" + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition( + { + CONF_URL: "github://org/repo", + CONF_REF: "branch-${branch_typo}", + }, + ContextVars(), + ) + err = str(exc_info.value) + assert "'branch_typo' is undefined" in err + assert CONF_REF in err + + +def test_substitute_package_definition_includes_source_location(tmp_path: Path) -> None: + """A package loaded from YAML surfaces file/line/col in the cv.Invalid message. + + Line/column are rendered 1-based (matching config.line_info() and editor + line numbering) and point at the offending scalar, not the enclosing dict. + """ + yaml_file = tmp_path / "main.yaml" + yaml_file.write_text( + "packages:\n broken: github://org/repo/${undefined_var}/pkg.yaml\n" + ) + config = load_yaml(yaml_file) + package_config = config[CONF_PACKAGES]["broken"] + + with pytest.raises(cv.Invalid) as exc_info: + _substitute_package_definition(package_config, ContextVars()) + + err = str(exc_info.value) + assert "main.yaml" in err + # The offending value lives on line 2 (1-based). Column depends on the YAML + # loader, so we only pin line and check that a 1-based column is present. + match = re.search(r"main\.yaml (\d+):(\d+)", err) + assert match, err + line, col = int(match.group(1)), int(match.group(2)) + assert line == 2, f"expected 1-based line 2, got {line} (err={err!r})" + assert col >= 1, f"expected 1-based column ≥ 1, got {col} (err={err!r})" diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 71bbd9db86d..3599e703d9d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -14,6 +14,7 @@ from esphome.components.packages import ( do_packages_pass, merge_packages, ) +from esphome.components.substitutions.jinja import UndefinedError from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, merge_config import esphome.config_validation as cv @@ -675,6 +676,39 @@ def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: substitutions.do_substitution_pass(config) +def test_raise_first_undefined_logs_extras_at_debug( + caplog: pytest.LogCaptureFixture, +) -> None: + """Only the first undefined error is raised; extras are logged at debug.""" + errors: substitutions.ErrList = [ + (UndefinedError("'a' is undefined"), ["url"], None), + (UndefinedError("'b' is undefined"), ["ref"], None), + (UndefinedError("'c' is undefined"), ["path"], None), + ] + + with ( + caplog.at_level(logging.DEBUG, logger="esphome.components.substitutions"), + pytest.raises(cv.Invalid) as exc_info, + ): + substitutions.raise_first_undefined(errors, None, "package definition") + + # First error is surfaced as the cv.Invalid message. + raised = str(exc_info.value) + assert "'a' is undefined" in raised + assert "'b' is undefined" not in raised + assert "'c' is undefined" not in raised + + # Remaining errors are captured via debug logging for troubleshooting. + assert "Additional undefined variables in package definition" in caplog.text + assert "'b' is undefined at 'ref'" in caplog.text + assert "'c' is undefined at 'path'" in caplog.text + + +def test_raise_first_undefined_noop_on_empty() -> None: + """An empty errors list is a no-op — no exception, no log.""" + substitutions.raise_first_undefined([], None, "package definition") + + def test_do_substitution_pass_included_substitutions_must_be_mapping( tmp_path: Path, ) -> None: From 36812591ebf2bc00930618ae5eca8a78ab51bb1d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:20:56 +1200 Subject: [PATCH 1535/2056] Bump version to 2026.4.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 6f1f3d0e7e1..deb57df1d3e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.0 +PROJECT_NUMBER = 2026.4.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 95fd488dca6..1f5b3b6c577 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.0" +__version__ = "2026.4.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From bab9cd3e7a9b6ebfd2a0ac1ab0a9dd10371d6529 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 16 Apr 2026 14:07:26 -1000 Subject: [PATCH 1536/2056] [runtime_stats] Track main loop active time and report overhead (#15743) --- .../runtime_stats/runtime_stats.cpp | 99 +++++++++++++++---- .../components/runtime_stats/runtime_stats.h | 41 ++++++++ esphome/core/application.h | 23 +++++ esphome/core/component.cpp | 4 + esphome/core/component.h | 8 ++ tests/integration/test_runtime_stats.py | 33 +++++++ 6 files changed, 189 insertions(+), 19 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 06714b5a446..9ed141155a5 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -32,40 +32,101 @@ void RuntimeStatsCollector::log_stats_() { " Period stats (last %" PRIu32 "ms): %zu active components", this->log_interval_, count); - if (count == 0) { - return; + // Sum component time so we can derive main-loop overhead + // (active loop time minus time attributable to component loop()s). + // Period sum iterates the active-in-period subset; total sum must iterate + // all components since total_active_time_us_ includes iterations where + // currently-idle components previously ran. + uint64_t period_component_sum_us = 0; + for (size_t i = 0; i < count; i++) { + period_component_sum_us += sorted[i]->runtime_stats_.period_time_us; + } + uint64_t total_component_sum_us = 0; + for (auto *component : components) { + total_component_sum_us += component->runtime_stats_.total_time_us; } - // Sort by period runtime (descending) - std::sort(sorted, sorted + count, compare_period_time); + if (count > 0) { + // Sort by period runtime (descending) + std::sort(sorted, sorted + count, compare_period_time); - // Log top components by period runtime - for (size_t i = 0; i < count; i++) { - const auto &stats = sorted[i]->runtime_stats_; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, - stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, - stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); + // Log top components by period runtime + for (size_t i = 0; i < count; i++) { + const auto &stats = sorted[i]->runtime_stats_; + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, + stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, + stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); + } + } + + // Main-loop overhead for the period: active wall time minus component time. + // active = sum of per-iteration loop time excluding yield/sleep. + if (this->period_active_count_ > 0) { + uint64_t active = this->period_active_time_us_; + uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0; + // Use double for µs→ms conversion so multi-day uptimes (where total + // microsecond counters exceed float's ~7-digit mantissa) keep resolution. + ESP_LOGI(TAG, + " main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, " + "overhead_total=%.1fms", + this->period_active_count_, + static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0, + static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0, + static_cast<double>(overhead) / 1000.0); + uint64_t before = this->period_before_time_us_; + uint64_t tail = this->period_tail_time_us_; + uint64_t accounted = before + tail; + uint64_t inter = overhead > accounted ? overhead - accounted : 0; + ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms", + static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0, + static_cast<double>(inter) / 1000.0); } // Log total stats since boot (only for active components - idle ones haven't changed) ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); - // Re-sort by total runtime for all-time stats - std::sort(sorted, sorted + count, compare_total_time); + if (count > 0) { + // Re-sort by total runtime for all-time stats + std::sort(sorted, sorted + count, compare_total_time); - for (size_t i = 0; i < count; i++) { - const auto &stats = sorted[i]->runtime_stats_; - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, - stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, - stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); + for (size_t i = 0; i < count; i++) { + const auto &stats = sorted[i]->runtime_stats_; + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, + stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); + } + } + + if (this->total_active_count_ > 0) { + uint64_t active = this->total_active_time_us_; + uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0; + ESP_LOGI(TAG, + " main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, " + "overhead_total=%.1fms", + this->total_active_count_, + static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0, + static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0, + static_cast<double>(overhead) / 1000.0); + uint64_t before = this->total_before_time_us_; + uint64_t tail = this->total_tail_time_us_; + uint64_t accounted = before + tail; + uint64_t inter = overhead > accounted ? overhead - accounted : 0; + ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms", + static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0, + static_cast<double>(inter) / 1000.0); } // Reset period stats for (auto *component : components) { component->runtime_stats_.reset_period(); } + this->period_active_count_ = 0; + this->period_active_time_us_ = 0; + this->period_active_max_us_ = 0; + this->period_before_time_us_ = 0; + this->period_tail_time_us_ = 0; } bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) { diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 3c2c9f78add..82e0fb7c618 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -29,6 +29,31 @@ class RuntimeStatsCollector { // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); + // Record the wall time of one main loop iteration excluding the yield/sleep. + // Called once per loop from Application::loop(). + // active_us = total time between loop start and just before yield. + // before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop). + // tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix. + // Residual overhead at log time = active − Σ(component) − before − tail, + // which captures per-iteration inter-component bookkeeping (set_current_component, + // WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls, + // the for-loop itself). + void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) { + this->period_active_count_++; + this->period_active_time_us_ += active_us; + if (active_us > this->period_active_max_us_) + this->period_active_max_us_ = active_us; + this->total_active_count_++; + this->total_active_time_us_ += active_us; + if (active_us > this->total_active_max_us_) + this->total_active_max_us_ = active_us; + + this->period_before_time_us_ += before_us; + this->total_before_time_us_ += before_us; + this->period_tail_time_us_ += tail_us; + this->total_tail_time_us_ += tail_us; + } + protected: void log_stats_(); // Static comparators — member functions have friend access, lambdas do not @@ -37,6 +62,22 @@ class RuntimeStatsCollector { uint32_t log_interval_; uint32_t next_log_time_{0}; + + // Main loop active-time stats (wall time per iteration, excluding yield/sleep). + // Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in + // a few weeks of uptime, which is well within ESPHome device lifetimes. + uint64_t period_active_count_{0}; + uint64_t period_active_time_us_{0}; + uint32_t period_active_max_us_{0}; + uint64_t total_active_count_{0}; + uint64_t total_active_time_us_{0}; + uint32_t total_active_max_us_{0}; + + // Split of overhead sections — accumulated per iteration. + uint64_t period_before_time_us_{0}; + uint64_t total_before_time_us_{0}; + uint64_t period_tail_time_us_{0}; + uint64_t total_tail_time_us_{0}; }; } // namespace runtime_stats diff --git a/esphome/core/application.h b/esphome/core/application.h index db8af735bd4..19245ab203b 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -875,6 +875,17 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t l } inline void ESPHOME_ALWAYS_INLINE Application::loop() { +#ifdef USE_RUNTIME_STATS + // Capture the start of the active (non-sleeping) portion of this iteration. + // Used to derive main-loop overhead = active time − Σ(component time) − + // before/tail splits recorded below. + uint32_t loop_active_start_us = micros(); + // Snapshot the cumulative component-recorded time so we can subtract the + // slice that the scheduler spends inside its own WarnIfComponentBlockingGuard + // (scheduler.cpp) — that time is already counted in per-component stats, + // so charging it again to "before" would double-count. + uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us; +#endif // Get the initial loop time at the start uint32_t last_op_end_time = millis(); @@ -907,12 +918,24 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { this->feed_wdt_with_time(last_op_end_time); } +#ifdef USE_RUNTIME_STATS + uint32_t loop_tail_start_us = micros(); +#endif this->after_loop_tasks_(); #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) { + uint32_t loop_now_us = micros(); + // Subtract scheduled-component time from the "before" bucket so it is + // not double-counted (it is already attributed to per-component stats). + uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us; + uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us + ? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us) + : 0; + global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, + loop_now_us - loop_tail_start_us); global_runtime_stats->process_pending_stats(last_op_end_time); } #endif diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 8949b4b76dc..e33652482ea 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -506,6 +506,10 @@ void PollingComponent::stop_poller() { uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } +#ifdef USE_RUNTIME_STATS +uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif + void __attribute__((noinline, cold)) WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) { bool should_warn; diff --git a/esphome/core/component.h b/esphome/core/component.h index 9e339dce649..717ca362576 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -116,6 +116,13 @@ struct ComponentRuntimeStats { uint64_t total_time_us{0}; uint32_t total_max_time_us{0}; + // Cumulative sum of every record_time() duration since boot, across all + // components. Used by Application::loop() to snapshot time spent inside + // WarnIfComponentBlockingGuard (including guards constructed by the + // scheduler at scheduler.cpp) so main-loop overhead accounting can + // subtract scheduled-callback time from the before_loop_tasks_ wall time. + static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void record_time(uint32_t duration_us) { this->period_count++; this->period_time_us += duration_us; @@ -125,6 +132,7 @@ struct ComponentRuntimeStats { this->total_time_us += duration_us; if (duration_us > this->total_max_time_us) this->total_max_time_us = duration_us; + global_recorded_us += duration_us; } void reset_period() { this->period_count = 0; diff --git a/tests/integration/test_runtime_stats.py b/tests/integration/test_runtime_stats.py index 9e93035d835..bd7f36341d2 100644 --- a/tests/integration/test_runtime_stats.py +++ b/tests/integration/test_runtime_stats.py @@ -26,6 +26,7 @@ async def test_runtime_stats( # Track component stats component_stats_found = set() + main_loop_lines: list[dict[str, str]] = [] # Patterns to match - need to handle ANSI color codes and timestamps # The log format is: [HH:MM:SS][color codes][I][tag]: message @@ -34,6 +35,14 @@ async def test_runtime_stats( component_pattern = re.compile( r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms" ) + # Main loop overhead line emitted by runtime_stats + main_loop_pattern = re.compile( + r"main_loop:\s+iters=(?P<iters>\d+),\s+" + r"active_avg=(?P<active_avg>[\d.]+)ms,\s+" + r"active_max=(?P<active_max>[\d.]+)ms,\s+" + r"active_total=(?P<active_total>[\d.]+)ms,\s+" + r"overhead_total=(?P<overhead_total>[\d.]+)ms" + ) def check_output(line: str) -> None: """Check log output for runtime stats messages.""" @@ -54,6 +63,11 @@ async def test_runtime_stats( component_name = match.group(1) component_stats_found.add(component_name) + # Check for main_loop overhead line + ml_match = main_loop_pattern.search(line) + if ml_match: + main_loop_lines.append(ml_match.groupdict()) + async with ( run_compiled(yaml_config, line_callback=check_output), api_client_connected() as client, @@ -86,3 +100,22 @@ async def test_runtime_stats( assert "template.switch" in component_stats_found, ( f"Expected template.switch stats, found: {component_stats_found}" ) + + # Verify the main_loop overhead line is emitted (at least once for + # the period section and once for the total section, per log cycle). + assert len(main_loop_lines) >= 2, ( + f"Expected at least 2 main_loop lines, got {len(main_loop_lines)}" + ) + for fields in main_loop_lines: + assert int(fields["iters"]) > 0, f"iters should be > 0: {fields}" + assert float(fields["active_total"]) > 0.0, ( + f"active_total should be > 0: {fields}" + ) + assert float(fields["active_avg"]) >= 0.0, ( + f"active_avg should be >= 0: {fields}" + ) + # overhead_total is derived and may be 0 if components dominate, + # but the field must still be present and parseable as a float. + assert float(fields["overhead_total"]) >= 0.0, ( + f"overhead_total should be >= 0: {fields}" + ) From e5f6a734ba46884cafeed83e6e735cba0c901a2d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:08:07 +1000 Subject: [PATCH 1537/2056] [lvgl] Fix angles for arc (#15860) --- esphome/components/lvgl/widgets/arc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 9eaf3dadce4..ac993cc3829 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -77,8 +77,11 @@ class ArcType(NumberType): # start_angle and end_angle are mapped to bg_start_angle and bg_end_angle prop = str(prop) if prop.endswith("_angle"): - prop = "bg_" + prop - await w.set_property(prop, config, processor=validator) + await w.set_property( + "bg_" + prop, await validator.process(config.get(prop)) + ) + else: + await w.set_property(prop, config, processor=validator) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB) From 0d3db2b6701fb7e87cd41f9f8c60445b06575618 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:08:07 +1000 Subject: [PATCH 1538/2056] [lvgl] Fix angles for arc (#15860) --- esphome/components/lvgl/widgets/arc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 9eaf3dadce4..ac993cc3829 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -77,8 +77,11 @@ class ArcType(NumberType): # start_angle and end_angle are mapped to bg_start_angle and bg_end_angle prop = str(prop) if prop.endswith("_angle"): - prop = "bg_" + prop - await w.set_property(prop, config, processor=validator) + await w.set_property( + "bg_" + prop, await validator.process(config.get(prop)) + ) + else: + await w.set_property(prop, config, processor=validator) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB) From f0c21520aa5644370d1ed56e05702c35a95be721 Mon Sep 17 00:00:00 2001 From: guillempages <guillempages@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:56:56 +0200 Subject: [PATCH 1539/2056] [mipi_rgb] Add definitions for sunton displays (#15858) --- esphome/components/mipi_rgb/models/sunton.py | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 esphome/components/mipi_rgb/models/sunton.py diff --git a/esphome/components/mipi_rgb/models/sunton.py b/esphome/components/mipi_rgb/models/sunton.py new file mode 100644 index 00000000000..a33625dfe4b --- /dev/null +++ b/esphome/components/mipi_rgb/models/sunton.py @@ -0,0 +1,51 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +# fmt: off +sunton = DriverChip( + "ESP32-8048S070", + swap_xy=UNDEFINED, + initsequence=(), + width=800, + height=480, + pclk_frequency="12.5MHz", + de_pin=41, + hsync_pin=39, + vsync_pin=40, + pclk_pin=42, + hsync_pulse_width=30, + hsync_back_porch=16, + hsync_front_porch=210, + vsync_pulse_width=13, + vsync_back_porch=10, + vsync_front_porch=22, + data_pins={ + "red": [14, 21, 47, 48, 45], + "green": [9, 46, 3, 8, 16, 1], + "blue": [15, 7, 6, 5, 4], + }, +) + +sunton.extend( + "ESP32-8048S050", + swap_xy=UNDEFINED, + initsequence=(), + width=800, + height=480, + pclk_frequency="16MHz", + de_pin=40, + hsync_pin=39, + vsync_pin=41, + pclk_pin=42, + hsync_back_porch=8, + hsync_front_porch=8, + hsync_pulse_width=4, + vsync_back_porch=8, + vsync_front_porch=8, + vsync_pulse_width=4, + data_pins={ + "red": [45, 48, 47, 21, 14], + "green": [5, 6, 7, 15, 16, 4], + "blue": [8, 3, 46, 9, 1], + }, +) From 7321e6e52fc9a19e4c02de406088f04e7ec077ca Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht <rupprecht.thomas@gmail.com> Date: Mon, 20 Apr 2026 15:10:05 +0200 Subject: [PATCH 1540/2056] [rtttl] allow any control parameters order and default value fallback (#14438) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 98 +++++++++++++++--------------- tests/components/rtttl/common.yaml | 16 +++++ 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 01f5aad8109..08d902b4be8 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -294,57 +294,59 @@ void Rtttl::play(std::string rtttl) { } ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); - // Get default duration - this->position_ = this->rtttl_.find("d=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'd='"); - return; - } - this->position_ += 2; - num = this->get_integer_(); - if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) { - this->default_note_denominator_ = num; - } else { - ESP_LOGE(TAG, "Invalid default duration: %d", num); - return; - } - - // Get default octave - this->position_ = this->rtttl_.find("o=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing 'o="); - return; - } - this->position_ += 2; - num = this->get_integer_(); - if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) { - this->default_octave_ = num; - } else { - ESP_LOGE(TAG, "Invalid default octave: %d", num); - return; - } - - // Get BPM - this->position_ = this->rtttl_.find("b=", this->position_); - if (this->position_ == std::string::npos) { - ESP_LOGE(TAG, "Missing b="); - return; - } - this->position_ += 2; - num = this->get_integer_(); - if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow - bpm = num; - } else { - ESP_LOGE(TAG, "Invalid BPM: %d", num); - return; - } - - this->position_ = this->rtttl_.find(':', this->position_); - if (this->position_ == std::string::npos) { + size_t name_end_position = this->position_; + size_t control_end = this->rtttl_.find(':', name_end_position + 1); + if (control_end == std::string::npos) { ESP_LOGE(TAG, "Missing second ':'"); return; } - this->position_++; + + // Get default duration + size_t pos = this->rtttl_.find("d=", name_end_position); + if (pos == std::string::npos || pos >= control_end) { + ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_); + } else { + this->position_ = pos + 2; + num = this->get_integer_(); + if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) { + this->default_note_denominator_ = num; + } else { + ESP_LOGE(TAG, "Invalid default duration: %d", num); + return; + } + } + + // Get default octave + pos = this->rtttl_.find("o=", name_end_position); + if (pos == std::string::npos || pos >= control_end) { + ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_); + } else { + this->position_ = pos + 2; + num = this->get_integer_(); + if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) { + this->default_octave_ = num; + } else { + ESP_LOGE(TAG, "Invalid default octave: %d", num); + return; + } + } + + // Get BPM + pos = this->rtttl_.find("b=", name_end_position); + if (pos == std::string::npos || pos >= control_end) { + ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm); + } else { + this->position_ = pos + 2; + num = this->get_integer_(); + if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow + bpm = num; + } else { + ESP_LOGE(TAG, "Invalid BPM: %d", num); + return; + } + } + + this->position_ = control_end + 1; // BPM usually expresses the number of quarter notes per minute this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds) diff --git a/tests/components/rtttl/common.yaml b/tests/components/rtttl/common.yaml index 86b52ca3de9..529713583be 100644 --- a/tests/components/rtttl/common.yaml +++ b/tests/components/rtttl/common.yaml @@ -3,6 +3,22 @@ esphome: then: - rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e' - rtttl.stop + # Test all note features: all notes, denominators (1,2,4,8,16,32), sharp (#), octaves (4-7), dotted (.), note gap (c5,c5), pause (p) + - rtttl.play: 'special:d=4,o=5,b=120:1c4,2d#5,4e6.,8f#7,16g4,32a5,8a#5,4b6,8h5,c5,c5,8p,2c4' + # Different orders of control parameters + - rtttl.play: 'test_odb:o=5,d=8,b=100:c' + - rtttl.play: 'test_bod:b=100,o=5,d=8:c' + - rtttl.play: 'test_bdo:b=100,d=8,o=5:c' + - rtttl.play: 'test_obd:o=5,b=100,d=8:c' + - rtttl.play: 'test_dbo:d=8,b=100,o=5:c' + # Missing parameters (use defaults) + - rtttl.play: 'test_no_d:o=5,b=100:c' + - rtttl.play: 'test_no_o:d=8,b=100:c' + - rtttl.play: 'test_no_b:d=8,o=5:c' + - rtttl.play: 'test_only_d:d=8:c' + - rtttl.play: 'test_only_o:o=5:c' + - rtttl.play: 'test_only_b:b=100:c' + - rtttl.play: 'test_empty::c' output: - platform: ${output_platform} From 0dae41aa2209031a78c77387d85e27842ff3b9b9 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:13:42 +1000 Subject: [PATCH 1541/2056] [lvgl] Fix format of hello world page (#15868) --- esphome/components/lvgl/hello_world.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index bbbd34e30a9..7bf068cc5d0 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -89,10 +89,12 @@ id: hello_world_label_ text: "Hello World!" align: center - - obj: + - container: id: hello_world_qrcode_ outline_width: 0 border_width: 0 + height: 100 + width: 100 hidden: !lambda |- return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400; widgets: From 9459f0426dcec99e412287faab1b612c433e01c4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:14:15 +1000 Subject: [PATCH 1542/2056] [lvgl] Fix overloads for setting images on styles (#15864) --- esphome/components/lvgl/lvgl_esphome.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ec1d247d8e..146866f5bd7 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -88,6 +88,12 @@ inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { ::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } +inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) { + ::lv_style_set_bg_image_src(style, image->get_lv_image_dsc()); +} +inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) { + ::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc()); +} #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { From 73b8e8ac098955c8cdd3bb1f72e4ce7bf7372f4a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:15:51 +1000 Subject: [PATCH 1543/2056] [lvgl] Fix update of textarea attached to keyboard (#15866) --- esphome/components/lvgl/widgets/keyboard.py | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index 029ca5f6846..c5628cee3cd 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -52,19 +52,23 @@ class KeyboardType(WidgetType): if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) if textarea := config.get(CONF_TEXTAREA): - # If a textarea is configured, it must be generated before the keyboard can attach it. - # If not yet configured, defer the attachment code. + if not is_widget_completed(textarea): + # Can only happen for an initial config, where the keyboard is configured before the + # textarea, so it's ok to always emit into the global context + async def add_textarea(): + async with LvContext(): + await w.set_property( + CONF_TEXTAREA, + (await get_widgets(config, CONF_TEXTAREA))[0].obj, + ) - async def add_textarea(): - async with LvContext(): - await w.set_property( - CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj - ) - - if is_widget_completed(textarea): - await add_textarea() - else: CORE.add_job(add_textarea) + else: + # Handles updates in automations, and properly ordered initial config. Code is generated + # into the enclosing context (main or lambda) + await w.set_property( + CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj + ) keyboard_spec = KeyboardType() From b72f5447c3fc4a0130ef56a74677b8fea0a25ab8 Mon Sep 17 00:00:00 2001 From: Rui Marinho <ruipmarinho@gmail.com> Date: Mon, 20 Apr 2026 14:24:07 +0100 Subject: [PATCH 1544/2056] [modbus] Simplify payload size validation in modbus_helpers (#15838) --- esphome/components/modbus/modbus_helpers.cpp | 114 +++++++++--------- .../components/modbus/modbus_helpers_test.cpp | 22 ++++ 2 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 tests/components/modbus/modbus_helpers_test.cpp diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index 77190b28463..89dc3c08bc1 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -5,6 +5,29 @@ namespace esphome::modbus::helpers { static const char *const TAG = "modbus_helpers"; +static size_t required_payload_size(SensorValueType sensor_value_type) { + switch (sensor_value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + return 2; + case SensorValueType::U_DWORD: + case SensorValueType::FP32: + case SensorValueType::U_DWORD_R: + case SensorValueType::FP32_R: + case SensorValueType::S_DWORD: + case SensorValueType::S_DWORD_R: + return 4; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + return 8; + case SensorValueType::RAW: + default: + return 0; + } +} + void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) { switch (value_type) { case SensorValueType::U_WORD: @@ -47,93 +70,70 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens uint32_t bitmask) { int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits - if (offset > data.size()) { - ESP_LOGE(TAG, "not enough data for value"); + // Validate offset against the buffer for all types, including RAW/unsupported, so + // a malformed or misconfigured frame still produces an error log. + if (static_cast<size_t>(offset) > data.size()) { + ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type), + static_cast<unsigned int>(offset), data.size()); + return value; + } + + const size_t required_size = required_payload_size(sensor_value_type); + if (required_size == 0) { + return value; + } + + if (data.size() - offset < required_size) { + ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu", + static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(), + required_size); return value; } - size_t size = data.size() - offset; - bool error = false; switch (sensor_value_type) { case SensorValueType::U_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), - bitmask); // default is 0xFFFF ; - } else { - error = true; - } + value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ; break; case SensorValueType::U_DWORD: case SensorValueType::FP32: - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } + value = get_data<uint32_t>(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); break; case SensorValueType::U_DWORD_R: case SensorValueType::FP32_R: - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } + value = get_data<uint32_t>(data, offset); + value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); break; case SensorValueType::S_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), - bitmask); // default is 0xFFFF ; - } else { - error = true; - } + value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ; break; case SensorValueType::S_DWORD: - if (size >= 4) { - value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask); - } else { - error = true; - } + value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask); break; case SensorValueType::S_DWORD_R: { - if (size >= 4) { - value = get_data<uint32_t>(data, offset); - // Currently the high word is at the low position - // the sign bit is therefore at low before the switch - uint32_t sign_bit = (value & 0x8000) << 16; - value = mask_and_shift_by_rightbit( - static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); - } else { - error = true; - } + value = get_data<uint32_t>(data, offset); + // Currently the high word is at the low position + // the sign bit is therefore at low before the switch + uint32_t sign_bit = (value & 0x8000) << 16; + value = mask_and_shift_by_rightbit( + static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); } break; case SensorValueType::U_QWORD: case SensorValueType::S_QWORD: // Ignore bitmask for QWORD - if (size >= 8) { - value = get_data<uint64_t>(data, offset); - } else { - error = true; - } + value = get_data<uint64_t>(data, offset); break; case SensorValueType::U_QWORD_R: case SensorValueType::S_QWORD_R: { // Ignore bitmask for QWORD - if (size >= 8) { - uint64_t tmp = get_data<uint64_t>(data, offset); - value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); - } else { - error = true; - } + uint64_t tmp = get_data<uint64_t>(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); } break; case SensorValueType::RAW: default: break; } - if (error) - ESP_LOGE(TAG, "not enough data for value"); return value; } } // namespace esphome::modbus::helpers diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp new file mode 100644 index 00000000000..e1b4fb2aa6e --- /dev/null +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -0,0 +1,22 @@ +#include <gtest/gtest.h> + +#include "esphome/components/modbus/modbus_helpers.h" + +namespace esphome::modbus::helpers { + +TEST(ModbusHelpersTest, PayloadToNumberRejectsOffsetAtEndOfBuffer) { + const std::vector<uint8_t> data{0x12, 0x34}; + EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 2, 0xFFFFFFFF), 0); +} + +TEST(ModbusHelpersTest, PayloadToNumberRejectsTruncatedMultiRegisterValue) { + const std::vector<uint8_t> data{0x12, 0x34, 0x56}; + EXPECT_EQ(payload_to_number(data, SensorValueType::U_DWORD, 0, 0xFFFFFFFF), 0); +} + +TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) { + const std::vector<uint8_t> data{0x12, 0x34}; + EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234); +} + +} // namespace esphome::modbus::helpers From 82656cb0cf1c051d428438e5f562f8de3541d1ca Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:28:52 +1000 Subject: [PATCH 1545/2056] [mipi_dsi] Add Seeed reTerminal d1001 display (#15867) --- esphome/components/mipi_dsi/models/seeed.py | 29 +++++++++++++++++++ .../mipi_dsi/test_mipi_dsi_config.py | 5 ++++ 2 files changed, 34 insertions(+) create mode 100644 esphome/components/mipi_dsi/models/seeed.py diff --git a/esphome/components/mipi_dsi/models/seeed.py b/esphome/components/mipi_dsi/models/seeed.py new file mode 100644 index 00000000000..290b0e07ee4 --- /dev/null +++ b/esphome/components/mipi_dsi/models/seeed.py @@ -0,0 +1,29 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# Standalone display +# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html +DriverChip( + "SEEED-RETERMINAL-D1001", + height=1280, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}], + reset_pin={"xl9535": None, "number": 2}, + initsequence=( + (0xE0, 0x00), + (0xE1, 0x93), + (0xE2, 0x65), + (0xE3, 0xF8), + (0x80, 0x01), + ), +) diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 119bbf7feab..955e945526a 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -7,6 +7,11 @@ import pytest from esphome import config_validation as cv from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4 + +# Importing xl9535 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that +# models (e.g. SEEED-RETERMINAL-D1001) that reference xl9535-backed pins in their +# defaults can be validated by the mipi_dsi CONFIG_SCHEMA in this test. +import esphome.components.xl9535 # noqa: F401 from esphome.const import ( CONF_DIMENSIONS, CONF_HEIGHT, From 6af341bb5be6a3850bf16a9c01f49e24b60f5413 Mon Sep 17 00:00:00 2001 From: Elvin Luff <elvin@eelviny.me> Date: Mon, 20 Apr 2026 14:34:31 +0100 Subject: [PATCH 1546/2056] [epaper_spi] Support SSD1683 and GDEY042T81 4.2 inch display (#13910) --- .../epaper_spi/epaper_spi_ssd1683.cpp | 97 +++++++++++++++++++ .../epaper_spi/epaper_spi_ssd1683.h | 22 +++++ .../components/epaper_spi/models/ssd1683.py | 27 ++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 16 +++ 4 files changed, 162 insertions(+) create mode 100644 esphome/components/epaper_spi/epaper_spi_ssd1683.cpp create mode 100644 esphome/components/epaper_spi/epaper_spi_ssd1683.h create mode 100644 esphome/components/epaper_spi/models/ssd1683.py diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1683.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1683.cpp new file mode 100644 index 00000000000..6fb7e1ac1a6 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1683.cpp @@ -0,0 +1,97 @@ +#include "epaper_spi_ssd1683.h" + +#include <algorithm> + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.mono"; + +void EPaperSSD1683::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01}); + // On partial update, set red RAM to inverse to remove BW ghosting + this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00}); + // Set full update to 0xD7 for fast update, 0xF7 for normal + // Fast update flashes less and draws sooner but is in busy state for the same amount of time + // Manufacturer recommends not using fast update all the time, TODO expose this to the user + this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7}); + this->command(0x20); +} + +// Puts the display into deep sleep mode 1, only way to get out is to reset the display +// Mode 1 retains RAM while sleeping, necessary for future partial and window updates +void EPaperSSD1683::deep_sleep() { + if (this->is_using_partial_update_()) { + ESP_LOGV(TAG, "Deep sleep mode 1"); + this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM + } else { + ESP_LOGV(TAG, "Deep sleep mode 2"); + this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM + } +} + +void EPaperSSD1683::set_window() { + // if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire + // buffer since the display RAM will not retain contents + if (!this->is_using_partial_update_()) { + this->x_low_ = 0; + this->x_high_ = this->width_; + this->y_low_ = 0; + this->y_high_ = this->height_; + } + + // round x-coordinates to byte boundaries + this->x_low_ /= 8; + this->x_high_ += 7; + this->x_high_ /= 8; + + this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)}); + this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1), + (uint8_t) ((this->y_high_ - 1) / 256)}); + this->cmd_data(0x4E, {(uint8_t) this->x_low_}); + this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)}); +} + +bool HOT EPaperSSD1683::transfer_data() { + auto start_time = millis(); + if (this->current_data_index_ == 0) { + if (this->send_red_) { + // round to byte boundaries + this->set_window(); + } + // for monochrome, we need to send red on every refresh to prevent dirty pixels + // when doing a partial refresh + this->command(this->send_red_ ? 0x26 : 0x24); + this->current_data_index_ = this->y_low_; // actually current line + } + size_t row_length = this->x_high_ - this->x_low_; + FixedVector<uint8_t> bytes_to_send{}; + bytes_to_send.init(row_length); + ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis()); + this->start_data_(); + while (this->current_data_index_ != this->y_high_) { + size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_; + for (size_t i = 0; i != row_length; i++) { + bytes_to_send[i] = this->buffer_[data_idx++]; + } + ++this->current_data_index_; + this->write_array(&bytes_to_send.front(), row_length); // NOLINT + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + + this->disable(); + this->current_data_index_ = 0; + if (this->send_red_) { + this->send_red_ = false; + return false; + } + this->send_red_ = true; + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1683.h b/esphome/components/epaper_spi/epaper_spi_ssd1683.h new file mode 100644 index 00000000000..4532900dd1a --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1683.h @@ -0,0 +1,22 @@ +#pragma once + +#include "epaper_spi_mono.h" + +namespace esphome::epaper_spi { +/** + * A class for Solomon SSD1683 epaper displays. + */ +class EPaperSSD1683 : public EPaperMono { + public: + EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperMono(name, width, height, init_sequence, init_sequence_length) {} + + protected: + void refresh_screen(bool partial) override; + void deep_sleep() override; + void set_window() override; + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/ssd1683.py b/esphome/components/epaper_spi/models/ssd1683.py new file mode 100644 index 00000000000..983f5bb382e --- /dev/null +++ b/esphome/components/epaper_spi/models/ssd1683.py @@ -0,0 +1,27 @@ +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class SSD1683(EpaperModel): + def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults): + defaults[CONF_DATA_RATE] = data_rate + super().__init__(name, class_name, **defaults) + + # fmt: off + def get_init_sequence(self, config: dict): + _width, height = self.get_dimensions(config) + return ( + (0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit + (0x18, 0x80), # Select internal Temp sensor + (0x11, 0x03), # Set transform + ) + + +ssd1683 = SSD1683("ssd1683") + +goodisplay_gdey042t81 = ssd1683.extend( + "goodisplay-gdey042t81-4.2", + width=400, + height=300, +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index bf6053c78bc..8a420f299a9 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -145,3 +145,19 @@ display: it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK); it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0)); + + - platform: epaper_spi + spi_id: spi_bus + model: goodisplay-gdey042t81-4.2 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO17 + reset_pin: + allow_other_uses: true + number: GPIO16 + busy_pin: + allow_other_uses: true + number: GPIO4 From 94f30d5950e3d5aa3e49d31879ec394c30e63d45 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 20 Apr 2026 16:26:47 -0400 Subject: [PATCH 1547/2056] [micro_wake_word] Use ESPMicroSpeechFeatures from Espressif registry (v1.2.3) (#15879) --- .clang-tidy.hash | 2 +- esphome/components/micro_wake_word/__init__.py | 4 ++-- esphome/idf_component.yml | 2 ++ platformio.ini | 2 -- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 72a99675909..02aa990809e 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592 +c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9 diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 5ab1e4bb805..22d2098de0d 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -454,12 +454,12 @@ async def to_code(config): # Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn) esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2") + esp32.add_idf_component(name="esphome/esp-micro-speech-features", ref="1.2.3") + cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") cg.add_build_flag("-DESP_NN") - cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0") - if vad_model := config.get(CONF_VAD): cg.add_define("USE_MICRO_WAKE_WORD_VAD") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f4e3e751ec0..3637481c924 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.4 + esphome/esp-micro-speech-features: + version: 1.2.3 esphome/micro-decoder: version: 0.1.1 esphome/micro-flac: diff --git a/platformio.ini b/platformio.ini index e2c7e2b097d..d7b14944e47 100644 --- a/platformio.ini +++ b/platformio.ini @@ -155,7 +155,6 @@ lib_deps = makuna/NeoPixelBus@2.8.0 ; neopixelbus esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.5 ; wireguard - kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:arduino.build_flags} @@ -177,7 +176,6 @@ framework = espidf lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.5 ; wireguard - kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word tonia/HeatpumpIR@1.0.41 ; heatpumpir build_flags = ${common:idf.build_flags} From 213ab312d2d4f703056393eadc689fcfe0e42165 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:27:34 -0400 Subject: [PATCH 1548/2056] [image] Fix rodata bloat for multi-frame RGB565+alpha animations (#15873) --- esphome/components/image/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 7db50597e6f..8375ab91d3e 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -756,7 +756,7 @@ async def write_image(config, all_frames=False): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() - encoder.end_image() + encoder.end_image() rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) From a43ee15b567896cc932bb45da2d2de3fcdbd48ec Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:33:48 -0400 Subject: [PATCH 1549/2056] [core] Fix Pvariable placement new losing subclass identity (#15881) --- esphome/cpp_generator.py | 50 ++++++++++++------- tests/component_tests/ili9xxx/__init__.py | 0 .../ili9xxx/config/ili9xxx_test.yaml | 20 ++++++++ tests/component_tests/ili9xxx/test_ili9xxx.py | 31 ++++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 tests/component_tests/ili9xxx/__init__.py create mode 100644 tests/component_tests/ili9xxx/config/ili9xxx_test.yaml create mode 100644 tests/component_tests/ili9xxx/test_ili9xxx.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index cf90b878e1f..c622207daca 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -606,33 +606,43 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if isinstance(rhs, MockObj) and rhs.is_new_expr: # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. - the_type = id_.type + # + # Storage must be sized and aligned for the actual instantiated class, + # which may be a subclass of id_.type (e.g. `cv.declare_id(BaseClass)` + # combined with `SubClass.new()` — used by ili9xxx, waveshare_epaper, + # etc. to select a model-specific constructor). Using id_.type would + # run the base-class default constructor instead, silently losing any + # subclass initialization. Template args live on the CallExpression + # and are re-emitted below. + call_expr = rhs.base + assert isinstance(call_expr, CallExpression), ( + f"Expected CallExpression for placement new, got {type(call_expr)}" + ) + actual_type = rhs.new_type if rhs.new_type is not None else id_.type + if call_expr.template_args is not None: + actual_type = f"{actual_type}{call_expr.template_args}" + pointer_type = id_.type # Extract component namespace from type for memory analysis attribution - component_ns = _extract_component_ns(str(the_type)) + component_ns = _extract_component_ns(str(actual_type)) storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage CORE.add_global( RawStatement( - f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];" + f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];" ) ) + # Pointer declaration uses id_.type to preserve the declared base-class + # pointer type for downstream callers (polymorphism through base ptr). CORE.add_global( AssignmentExpression( - f"static {the_type}", + f"static {pointer_type}", "*const ", id_, - MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"), + MockObj(f"reinterpret_cast<{pointer_type} *>({storage_name})"), ) ) - # Extract args from the CallExpression and rebuild as placement new. - # Template args are already encoded in the_type (e.g. GlobalsComponent<int>), - # so we only pass the constructor args, not template_args. - call_expr = rhs.base - assert isinstance(call_expr, CallExpression), ( - f"Expected CallExpression for placement new, got {type(call_expr)}" - ) - placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args) CORE.add(ExpressionStatement(placement_new)) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) @@ -869,12 +879,16 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op", "is_new_expr") + __slots__ = ("base", "op", "is_new_expr", "new_type") - def __init__(self, base, op=".", is_new_expr=False) -> None: + def __init__(self, base, op=".", is_new_expr=False, new_type=None) -> None: self.base = base self.op = op self.is_new_expr = is_new_expr + # For `is_new_expr=True` objects, `new_type` holds the class name being + # constructed (e.g. "ili9xxx::ILI9XXXST7789V"). Needed by Pvariable so + # placement new uses the actual subclass rather than id_.type. + self.new_type = new_type def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -889,7 +903,9 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op, is_new_expr=self.is_new_expr) + return MockObj( + call, self.op, is_new_expr=self.is_new_expr, new_type=self.new_type + ) def __str__(self): return str(self.base) @@ -903,7 +919,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->", is_new_expr=True) + return MockObj(f"new {self.base}", "->", is_new_expr=True, new_type=self.base) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" diff --git a/tests/component_tests/ili9xxx/__init__.py b/tests/component_tests/ili9xxx/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml b/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml new file mode 100644 index 00000000000..bc6148b8d85 --- /dev/null +++ b/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: arduino + +spi: + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: ili9xxx + id: tft_display + model: ST7789V + cs_pin: GPIO5 + dc_pin: GPIO17 + reset_pin: GPIO16 + invert_colors: false diff --git a/tests/component_tests/ili9xxx/test_ili9xxx.py b/tests/component_tests/ili9xxx/test_ili9xxx.py new file mode 100644 index 00000000000..3919eb3823e --- /dev/null +++ b/tests/component_tests/ili9xxx/test_ili9xxx.py @@ -0,0 +1,31 @@ +"""Tests for the ili9xxx component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_ili9xxx_placement_new_uses_model_subclass( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Regression test for ili9xxx picking the right constructor under placement new. + + ili9xxx declares the ID as the base ``ILI9XXXDisplay`` but constructs a + model-specific subclass (e.g. ``ILI9XXXST7789V``) via ``MODELS[...].new()``. + Pvariable must emit placement new for the subclass — otherwise the base + default constructor runs and the panel is left with a null init sequence + and 0x0 dimensions, producing a silent blank screen. + """ + main_cpp = generate_main(component_config_path("ili9xxx_test.yaml")) + + # Storage is sized for the subclass so the full object fits. + assert "sizeof(ili9xxx::ILI9XXXST7789V)" in main_cpp + assert "alignas(ili9xxx::ILI9XXXST7789V)" in main_cpp + # Pointer is declared as the base type for polymorphism. + assert "static ili9xxx::ILI9XXXDisplay *const tft_display" in main_cpp + # Placement new runs the subclass constructor — this is the actual regression fix. + assert "new(tft_display) ili9xxx::ILI9XXXST7789V()" in main_cpp + # Base-class default constructor must NOT be used. + assert "new(tft_display) ili9xxx::ILI9XXXDisplay()" not in main_cpp From 4cb7ea2584225f5ea919d4a5f3088f075484dc9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:37:56 +0200 Subject: [PATCH 1550/2056] [light] Force-inline LightCall::set_flag_/clear_flag_ (#15729) --- esphome/components/light/light_call.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 88d29bd3490..39953d0d204 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -222,7 +222,7 @@ class LightCall { inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } // Helper to set flag - defaults to true for common case - void set_flag_(FieldFlags flag, bool value = true) { + void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE { if (value) { this->flags_ |= flag; } else { @@ -231,7 +231,7 @@ class LightCall { } // Helper to clear flag - reduces code size for common case - void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; } + void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; } // Helper to log unsupported feature and clear flag - reduces code duplication void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log); From 0a0176d600e1b33159ad737df15640ee43cac525 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:38:12 +0200 Subject: [PATCH 1551/2056] [core] raise WDT_FEED_INTERVAL_MS from 3 ms to 300 ms (#15846) --- esphome/core/application.cpp | 54 +++++++++++++++++++++------------ esphome/core/application.h | 59 ++++++++++++++++++++++++++++++------ 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 866edebbf60..e5e1b36a65b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -211,11 +211,16 @@ void Application::process_dump_config_() { void Application::feed_wdt() { // Cold entry: callers without a millis() timestamp in hand. Fetches the - // time and takes the same rate-limit path as feed_wdt_with_time(). + // time and takes the same rate-limit paths as feed_wdt_with_time(). uint32_t now = millis(); if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { this->feed_wdt_slow_(now); } +#ifdef USE_STATUS_LED + if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) { + this->service_status_led_slow_(now); + } +#endif } void HOT Application::feed_wdt_slow_(uint32_t time) { @@ -223,27 +228,36 @@ void HOT Application::feed_wdt_slow_(uint32_t time) { // confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded. arch_feed_wdt(); this->last_wdt_feed_ = time; -#ifdef USE_STATUS_LED - if (status_led::global_status_led != nullptr) { - auto *sl = status_led::global_status_led; - uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK; - if (sl_state == COMPONENT_STATE_LOOP_DONE) { - // status_led only transitions to LOOP_DONE from inside its own loop() (after the - // first idle-path dispatch), so its pin is already initialized by pre_setup() and - // its setup() has already run. Re-dispatch only if an error or warning bit has been - // set since; otherwise skip entirely. - if ((this->app_state_ & STATUS_LED_MASK) == 0) - return; - sl->enable_loop(); - } else if (sl_state != COMPONENT_STATE_LOOP) { - // CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle. - return; - } - sl->loop(); - } -#endif } +#ifdef USE_STATUS_LED +void HOT Application::service_status_led_slow_(uint32_t time) { + // Callers (feed_wdt(), feed_wdt_with_time()) have already confirmed the + // STATUS_LED_DISPATCH_INTERVAL_MS rate limit was exceeded. Rate-limited + // separately from arch_feed_wdt() so the LED blink pattern stays readable + // (status_led error blink period is 250 ms) while HAL watchdog pokes can + // still run at the much coarser WDT_FEED_INTERVAL_MS cadence. + this->last_status_led_service_ = time; + if (status_led::global_status_led == nullptr) + return; + auto *sl = status_led::global_status_led; + uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK; + if (sl_state == COMPONENT_STATE_LOOP_DONE) { + // status_led only transitions to LOOP_DONE from inside its own loop() (after the + // first idle-path dispatch), so its pin is already initialized by pre_setup() and + // its setup() has already run. Re-dispatch only if an error or warning bit has been + // set since; otherwise skip entirely. + if ((this->app_state_ & STATUS_LED_MASK) == 0) + return; + sl->enable_loop(); + } else if (sl_state != COMPONENT_STATE_LOOP) { + // CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle. + return; + } + sl->loop(); +} +#endif + bool Application::any_component_has_status_flag_(uint8_t flag) const { // Walk all components (not just looping ones) so non-looping components' // status bits are respected. Only called from the slow-path clear helpers diff --git a/esphome/core/application.h b/esphome/core/application.h index 9252a474464..645caa24045 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -229,23 +229,50 @@ class Application { void schedule_dump_config() { this->dump_config_at_ = 0; } - /// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the - /// rate of HAL pokes low while still being small enough that any plausible - /// watchdog timeout (seconds) has orders of magnitude of safety margin. - static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3; + /// Minimum interval between real arch_feed_wdt() calls. Sized so the outer + /// feed in Application::loop() is effectively rate-limited across both the + /// normal ~62 Hz cadence and worst-case wake-storm scenarios (e.g. external + /// stacks like OpenThread posting frequent wake notifications). Component + /// loops and scheduler items still feed after every op, so any op exceeding + /// this threshold triggers a real feed naturally. + /// Safety margins vs. platform watchdog timeouts: + /// - ESP32 task WDT default (5 s): ~16x + /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change + /// must keep comfortable margin here + /// - ESP8266 HW WDT (~6 s): ~20x + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300; /// Feed the task watchdog. Cold entry — callers without a millis() /// timestamp in hand. Out of line to keep call sites tiny. void feed_wdt(); +#ifdef USE_STATUS_LED + /// Dispatch interval for the status LED update. Deliberately shorter than + /// WDT_FEED_INTERVAL_MS because the status LED error blink has a 250 ms + /// period (status_led.cpp:ERROR_PERIOD_MS) and a 150 ms on-window; the + /// dispatch cadence must be short enough to render that blink without + /// aliasing. Sampling every 100 ms yields an on/off observation inside + /// every error period with headroom for the 250 ms warning on-window. + static constexpr uint32_t STATUS_LED_DISPATCH_INTERVAL_MS = 100; +#endif + /// Feed the task watchdog, hot entry. Callers that already have a /// millis() timestamp pay only a load + sub + branch on the common - /// (no-op) path. The actual arch feed + status LED update live in - /// feed_wdt_slow_. + /// (no-op) path. The actual arch feed lives in feed_wdt_slow_. + /// When USE_STATUS_LED is compiled in, also gates a separate (shorter) + /// interval for dispatching status_led so the LED blink pattern stays + /// readable even though arch_feed_wdt pokes are now rate-limited at + /// WDT_FEED_INTERVAL_MS. The two rate limits are independent so raising + /// WDT_FEED_INTERVAL_MS does not distort the LED cadence. void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) { if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] { this->feed_wdt_slow_(time); } +#ifdef USE_STATUS_LED + if (static_cast<uint32_t>(time - this->last_status_led_service_) > STATUS_LED_DISPATCH_INTERVAL_MS) [[unlikely]] { + this->service_status_led_slow_(time); + } +#endif } void reboot(); @@ -410,11 +437,21 @@ class Application { /// Caller must ensure dump_config_at_ < components_.size(). void __attribute__((noinline)) process_dump_config_(); - /// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates - /// last_wdt_feed_, and re-dispatches the status LED. Out of line so the - /// inline wrapper stays tiny. + /// Slow path for feed_wdt(): actually calls arch_feed_wdt() and updates + /// last_wdt_feed_. Out of line so the inline wrapper stays tiny. Does NOT + /// touch status_led — that's gated separately via service_status_led_slow_ + /// because the two rate limits have very different safe ranges (~ seconds + /// for WDT, < 250 ms for LED blink rendering). void feed_wdt_slow_(uint32_t time); +#ifdef USE_STATUS_LED + /// Slow path for the status_led dispatch rate limit. Runs the status_led + /// component's loop() based on its state (LOOP / LOOP_DONE with status + /// bits set), and updates last_status_led_service_. Out of line to keep + /// the feed_wdt_with_time hot path a couple of load+branch sequences. + void service_status_led_slow_(uint32_t time); +#endif + /// Perform a delay while also monitoring socket file descriptors for readiness #ifdef USE_HOST // select() fallback path is too complex to inline (host platform) @@ -468,6 +505,10 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path +#ifdef USE_STATUS_LED + // millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_ + uint32_t last_status_led_service_{0}; +#endif #ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() From 0d3a3552dadc74d8770fe9dd2f3e41402eac9c31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:39:49 +0200 Subject: [PATCH 1552/2056] [core] Move heap-allocating helpers to alloc_helpers.h/cpp (#15623) --- esphome/components/anova/anova_base.cpp | 6 +- .../components/http_request/http_request.cpp | 2 +- .../components/http_request/http_request.h | 5 +- .../http_request/http_request_arduino.cpp | 2 +- .../http_request/http_request_host.cpp | 2 +- .../http_request/http_request_idf.cpp | 2 +- esphome/core/alloc_helpers.cpp | 229 +++++++++++++++++ esphome/core/alloc_helpers.h | 128 +++++++++ esphome/core/helpers.cpp | 189 +------------- esphome/core/helpers.h | 242 ++---------------- script/ci-custom.py | 16 +- 11 files changed, 417 insertions(+), 406 deletions(-) create mode 100644 esphome/core/alloc_helpers.cpp create mode 100644 esphome/core/alloc_helpers.h diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index fef4f1d8526..a14dd728a80 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -2,6 +2,8 @@ #include <cstdio> #include <cstring> +#include "esphome/core/alloc_helpers.h" + namespace esphome { namespace anova { @@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } case READ_TARGET_TEMPERATURE: case SET_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); + this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { - this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); + this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT if (this->fahrenheit_) this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 2c74638f12c..d45208ed5df 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto lower = str_lower_case(header_name); + auto lower = str_lower_case(header_name); // NOLINT for (const auto &entry : this->response_headers_) { if (entry.name == lower) { ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index ae73983bab2..f37bf776333 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -11,6 +11,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/alloc_helpers.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -400,7 +401,7 @@ class HttpRequestComponent : public Component { std::vector<std::string> lower; lower.reserve(collect_headers.size()); for (const auto &h : collect_headers) { - lower.push_back(str_lower_case(h)); + lower.push_back(str_lower_case(h)); // NOLINT } return this->perform(url, method, body, request_headers, lower); } @@ -415,7 +416,7 @@ class HttpRequestComponent : public Component { std::vector<std::string> lower; lower.reserve(collect_headers.size()); for (const auto &h : collect_headers) { - lower.push_back(str_lower_case(h)); + lower.push_back(str_lower_case(h)); // NOLINT } return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower); } diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index f0dd6492852..05f9db1c069 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { - const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); + const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 60ab4d68a04..85c6e8b3c7b 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, container->content_length = container->response_body_.size(); for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); - auto lower_name = str_lower_case(header.first); + auto lower_name = str_lower_case(header.first); // NOLINT if (should_collect_header(lower_case_collect_headers, lower_name)) { container->response_headers_.push_back({lower_name, header.second}); } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 30f53eecdc7..3e341395a46 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { - const std::string header_name = str_lower_case(evt->header_key); + const std::string header_name = str_lower_case(evt->header_key); // NOLINT if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); diff --git a/esphome/core/alloc_helpers.cpp b/esphome/core/alloc_helpers.cpp new file mode 100644 index 00000000000..11c7abe3f7b --- /dev/null +++ b/esphome/core/alloc_helpers.cpp @@ -0,0 +1,229 @@ +#include "esphome/core/alloc_helpers.h" + +#include "esphome/core/helpers.h" + +#include <algorithm> +#include <cctype> +#include <cstdarg> +#include <cstdio> +#include <cstring> +#include <string> + +namespace esphome { + +// --- String helpers --- + +std::string str_truncate(const std::string &str, size_t length) { + return str.length() > length ? str.substr(0, length) : str; +} + +std::string str_until(const char *str, char ch) { + const char *pos = strchr(str, ch); + return pos == nullptr ? std::string(str) : std::string(str, pos - str); +} +std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } + +// wrapper around std::transform to run safely on functions from the ctype.h header +// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes +template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) { + std::string result; + result.resize(str.length()); + std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); + return result; +} +std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); } + +std::string str_upper_case(const std::string &str) { + std::string result; + result.resize(str.length()); + std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return std::toupper(ch); }); + return result; +} + +std::string str_snake_case(const std::string &str) { + std::string result = str; + for (char &c : result) { + c = to_snake_case_char(c); + } + return result; +} + +std::string str_sanitize(const std::string &str) { + std::string result; + result.resize(str.size()); + str_sanitize_to(&result[0], str.size() + 1, str.c_str()); + return result; +} + +std::string str_snprintf(const char *fmt, size_t len, ...) { + std::string str; + va_list args; + + str.resize(len); + va_start(args, len); + size_t out_length = vsnprintf(&str[0], len + 1, fmt, args); + va_end(args); + + if (out_length < len) + str.resize(out_length); + + return str; +} + +std::string str_sprintf(const char *fmt, ...) { + std::string str; + va_list args; + + va_start(args, fmt); + size_t length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + str.resize(length); + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + return str; +} + +// --- Value formatting helpers --- + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); +} + +// --- Base64 helpers --- + +static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])]; +} + +std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); } + +std::string base64_encode(const uint8_t *buf, size_t buf_len) { + std::string ret; + int i = 0; + char char_array_3[3]; + + while (buf_len--) { + char_array_3[i++] = *(buf++); + if (i == 3) { + base64_encode_triple(char_array_3, 4, ret); + i = 0; + } + } + + if (i) { + for (int j = i; j < 3; j++) + char_array_3[j] = '\0'; + + base64_encode_triple(char_array_3, i + 1, ret); + + while ((i++ < 3)) + ret += '='; + } + + return ret; +} + +std::vector<uint8_t> base64_decode(const std::string &encoded_string) { + // Calculate maximum decoded size: every 4 base64 chars = 3 bytes + size_t max_len = ((encoded_string.size() + 3) / 4) * 3; + std::vector<uint8_t> ret(max_len); + size_t actual_len = base64_decode(encoded_string, ret.data(), max_len); + ret.resize(actual_len); + return ret; +} + +// --- Hex/binary formatting helpers --- + +std::string format_mac_address_pretty(const uint8_t *mac) { + char buf[18]; + format_mac_addr_upper(mac, buf); + return std::string(buf); +} + +std::string format_hex(const uint8_t *data, size_t length) { + std::string ret; + ret.resize(length * 2); + format_hex_to(&ret[0], length * 2 + 1, data, length); + return ret; +} + +std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); } + +// Shared implementation for uint8_t and string hex pretty formatting +static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) + return ""; + std::string ret; + size_t hex_len = separator ? (length * 3 - 1) : (length * 2); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; + return ret; +} + +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { + return format_hex_pretty_uint8(data, length, separator, show_length); +} +std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} + +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) + return ""; + std::string ret; + size_t hex_len = separator ? (length * 5 - 1) : (length * 4); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; + return ret; +} +std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} +std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { + return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length); +} + +std::string format_bin(const uint8_t *data, size_t length) { + std::string result; + result.resize(length * 8); + format_bin_to(&result[0], length * 8 + 1, data, length); + return result; +} + +// --- MAC address helpers --- + +std::string get_mac_address() { + uint8_t mac[6]; + get_mac_address_raw(mac); + char buf[13]; + format_mac_addr_lower_no_sep(mac, buf); + return std::string(buf); +} + +std::string get_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(get_mac_address_pretty_into_buffer(buf)); +} + +} // namespace esphome diff --git a/esphome/core/alloc_helpers.h b/esphome/core/alloc_helpers.h new file mode 100644 index 00000000000..fe350886b76 --- /dev/null +++ b/esphome/core/alloc_helpers.h @@ -0,0 +1,128 @@ +#pragma once + +/// @file alloc_helpers.h +/// @brief Heap-allocating helper functions. +/// +/// These functions return std::string and allocate heap memory on every call. +/// On long-running embedded devices, repeated heap allocations fragment memory +/// over time, eventually causing crashes even with free memory available. +/// +/// Prefer the stack-based alternatives documented on each function instead. +/// New code should avoid using these functions. + +#include <cstdarg> +#include <cstdint> +#include <cstdio> +#include <string> +#include <vector> + +namespace esphome { + +// --- String helpers (allocating) --- + +/// Truncate a string to a specific length. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_truncate(const std::string &str, size_t length); + +/// Extract the part of the string until either the first occurrence of the specified character, or the end +/// (requires str to be null-terminated). +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_until(const char *str, char ch); +/// Extract the part of the string until either the first occurrence of the specified character, or the end. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_until(const std::string &str, char ch); + +/// Convert the string to lower case. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_lower_case(const std::string &str); + +/// Convert the string to upper case. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_upper_case(const std::string &str); + +/// Convert the string to snake case (lowercase with underscores). +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_snake_case(const std::string &str); + +/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. +/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead. +std::string str_sanitize(const std::string &str); + +/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. +std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); + +/// sprintf-like function returning std::string. +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. +std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); + +// --- Hex/binary formatting helpers (allocating) --- + +/// Format the six-byte array \p mac into a MAC address string. +/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. +std::string format_mac_address_pretty(const uint8_t mac[6]); + +/// Format the byte array \p data of length \p len in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +std::string format_hex(const uint8_t *data, size_t length); + +/// Format the vector \p data in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +std::string format_hex(const std::vector<uint8_t> &data); + +/// Format a byte array in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); + +/// Format a 16-bit word array in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true); + +/// Format a byte vector in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true); + +/// Format a 16-bit word vector in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true); + +/// Format a string's bytes in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); + +/// Format the byte array \p data of length \p len in binary. +/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. +std::string format_bin(const uint8_t *data, size_t length); + +// --- Value formatting helpers (allocating) --- + +/// Format a float value with accuracy decimals to a string. +/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0. +__attribute__((deprecated("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0."))) +std::string +value_accuracy_to_string(float value, int8_t accuracy_decimals); + +// --- Base64 helpers (allocating) --- + +/// Encode a byte buffer to base64 string. +/// @warning Allocates heap memory. +std::string base64_encode(const uint8_t *buf, size_t buf_len); +/// Encode a byte vector to base64 string. +/// @warning Allocates heap memory. +std::string base64_encode(const std::vector<uint8_t> &buf); + +/// Decode a base64 string to a byte vector. +/// @warning Allocates heap memory. Use base64_decode(data, len, buf, buf_len) with a pre-allocated buffer instead. +std::vector<uint8_t> base64_decode(const std::string &encoded_string); + +// --- MAC address helpers (allocating) --- + +/// Get the device MAC address as a string, in lowercase hex notation. +/// @warning Allocates heap memory. Use get_mac_address_into_buffer() instead. +std::string get_mac_address(); + +/// Get the device MAC address as a string, in colon-separated uppercase hex notation. +/// @warning Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead. +std::string get_mac_address_pretty(); + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 34ecaf137f0..1d0efd01ce5 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -221,31 +221,7 @@ bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffi return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0; } -std::string str_truncate(const std::string &str, size_t length) { - return str.length() > length ? str.substr(0, length) : str; -} -std::string str_until(const char *str, char ch) { - const char *pos = strchr(str, ch); - return pos == nullptr ? std::string(str) : std::string(str, pos - str); -} -std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } -// wrapper around std::transform to run safely on functions from the ctype.h header -// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes -template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) { - std::string result; - result.resize(str.length()); - std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); - return result; -} -std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); } -std::string str_upper_case(const std::string &str) { return str_ctype_transform<std::toupper>(str); } -std::string str_snake_case(const std::string &str) { - std::string result = str; - for (char &c : result) { - c = to_snake_case_char(c); - } - return result; -} +// str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { if (buffer_size == 0) { return buffer; @@ -258,41 +234,7 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { return buffer; } -std::string str_sanitize(const std::string &str) { - std::string result; - result.resize(str.size()); - str_sanitize_to(&result[0], str.size() + 1, str.c_str()); - return result; -} -std::string str_snprintf(const char *fmt, size_t len, ...) { - std::string str; - va_list args; - - str.resize(len); - va_start(args, len); - size_t out_length = vsnprintf(&str[0], len + 1, fmt, args); - va_end(args); - - if (out_length < len) - str.resize(out_length); - - return str; -} -std::string str_sprintf(const char *fmt, ...) { - std::string str; - va_list args; - - va_start(args, fmt); - size_t length = vsnprintf(nullptr, 0, fmt, args); - va_end(args); - - str.resize(length); - va_start(args, fmt); - vsnprintf(&str[0], length + 1, fmt, args); - va_end(args); - - return str; -} +// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; @@ -341,11 +283,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { return chars; } -std::string format_mac_address_pretty(const uint8_t *mac) { - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); -} +// format_mac_address_pretty moved to alloc_helpers.cpp // Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. // When separator is set, it is written unconditionally after each byte and the last @@ -398,13 +336,7 @@ char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); } -std::string format_hex(const uint8_t *data, size_t length) { - std::string ret; - ret.resize(length * 2); - format_hex_to(&ret[0], length * 2 + 1, data, length); - return ret; -} -std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); } +// format_hex (std::string returning overloads) moved to alloc_helpers.cpp char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); @@ -441,43 +373,7 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *dat return buffer; } -// Shared implementation for uint8_t and string hex formatting -static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { - if (data == nullptr || length == 0) - return ""; - std::string ret; - size_t hex_len = separator ? (length * 3 - 1) : (length * 2); - ret.resize(hex_len); - format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); - if (show_length && length > 4) - return ret + " (" + std::to_string(length) + ")"; - return ret; -} - -std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { - return format_hex_pretty_uint8(data, length, separator, show_length); -} -std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) { - return format_hex_pretty(data.data(), data.size(), separator, show_length); -} - -std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) { - if (data == nullptr || length == 0) - return ""; - std::string ret; - size_t hex_len = separator ? (length * 5 - 1) : (length * 4); - ret.resize(hex_len); - format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); - if (show_length && length > 4) - return ret + " (" + std::to_string(length) + ")"; - return ret; -} -std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) { - return format_hex_pretty(data.data(), data.size(), separator, show_length); -} -std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { - return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length); -} +// format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { if (buffer_size == 0) { @@ -500,12 +396,7 @@ char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return buffer; } -std::string format_bin(const uint8_t *data, size_t length) { - std::string result; - result.resize(length * 8); - format_bin_to(&result[0], length * 8 + 1, data, length); - return result; -} +// format_bin moved to alloc_helpers.cpp ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0) @@ -537,11 +428,7 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - char buf[VALUE_ACCURACY_MAX_LEN]; - value_accuracy_to_buf(buf, value, accuracy_decimals); - return std::string(buf); -} +// value_accuracy_to_string moved to alloc_helpers.cpp size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); @@ -606,45 +493,7 @@ static inline uint8_t base64_find_char(char c) { // Check if character is valid base64 or base64url static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); } -std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); } - -// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. -static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { - char char_array_4[4]; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (int j = 0; j < count; j++) - ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])]; -} - -std::string base64_encode(const uint8_t *buf, size_t buf_len) { - std::string ret; - int i = 0; - char char_array_3[3]; - - while (buf_len--) { - char_array_3[i++] = *(buf++); - if (i == 3) { - base64_encode_triple(char_array_3, 4, ret); - i = 0; - } - } - - if (i) { - for (int j = i; j < 3; j++) - char_array_3[j] = '\0'; - - base64_encode_triple(char_array_3, i + 1, ret); - - while ((i++ < 3)) - ret += '='; - } - - return ret; -} +// base64_encode (both overloads) moved to alloc_helpers.cpp size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { return base64_decode(reinterpret_cast<const uint8_t *>(encoded_string.data()), encoded_string.size(), buf, buf_len); @@ -705,14 +554,7 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b return out; } -std::vector<uint8_t> base64_decode(const std::string &encoded_string) { - // Calculate maximum decoded size: every 4 base64 chars = 3 bytes - size_t max_len = ((encoded_string.size() + 3) / 4) * 3; - std::vector<uint8_t> ret(max_len); - size_t actual_len = base64_decode(encoded_string, ret.data(), max_len); - ret.resize(actual_len); - return ret; -} +// base64_decode (vector-returning overload) moved to alloc_helpers.cpp /// Decode base64/base64url string directly into vector of little-endian int32 values /// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) @@ -851,18 +693,7 @@ void HighFrequencyLoopRequester::stop() { this->started_ = false; } -std::string get_mac_address() { - uint8_t mac[6]; - get_mac_address_raw(mac); - char buf[13]; - format_mac_addr_lower_no_sep(mac, buf); - return std::string(buf); -} - -std::string get_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(get_mac_address_pretty_into_buffer(buf)); -} +// get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp void get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_BUFFER_SIZE> buf) { uint8_t mac[6]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 54bc32a5a58..bb164f5034f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -21,6 +21,12 @@ #include "esphome/core/optional.h" +// Backward compatibility re-export of heap-allocating helpers. +// These functions have moved to alloc_helpers.h. External components should +// update their includes to use #include "esphome/core/alloc_helpers.h" directly. +// This re-export will be removed in 2026.11.0. +#include "esphome/core/alloc_helpers.h" + #ifdef USE_ESP8266 #include <Esp.h> #include <pgmspace.h> @@ -979,27 +985,13 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix) return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix)); } -/// Truncate a string to a specific length. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_truncate(const std::string &str, size_t length); +// str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0 -/// Extract the part of the string until either the first occurrence of the specified character, or the end -/// (requires str to be null-terminated). -std::string str_until(const char *str, char ch); -/// Extract the part of the string until either the first occurrence of the specified character, or the end. -std::string str_until(const std::string &str, char ch); - -/// Convert the string to lower case. -std::string str_lower_case(const std::string &str); -/// Convert the string to upper case. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_upper_case(const std::string &str); +// str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Convert a single char to snake_case: lowercase and space to underscore. constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } -/// Convert the string to snake case (lowercase with underscores). -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_snake_case(const std::string &str); +// str_snake_case moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. constexpr char to_sanitized_char(char c) { @@ -1022,9 +1014,7 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s return str_sanitize_to(buffer, N, str); } -/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. -/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead. -std::string str_sanitize(const std::string &str); +// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. /// This computes object_id hashes directly from names without creating an intermediate buffer. @@ -1040,13 +1030,7 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { return hash; } -/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). -/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. -std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); - -/// sprintf-like function returning std::string. -/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. -std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); +// str_snprintf, str_sprintf moved to alloc_helpers.h - remove this comment before 2026.11.0 #ifdef USE_ESP8266 // ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) @@ -1441,189 +1425,26 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); } -/// Format the six-byte array \p mac into a MAC address. -/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_mac_address_pretty(const uint8_t mac[6]); -/// Format the byte array \p data of length \p len in lowercased hex. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_hex(const uint8_t *data, size_t length); -/// Format the vector \p data in lowercased hex. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_hex(const std::vector<uint8_t> &data); +// format_mac_address_pretty, format_hex (all overloads) moved to alloc_helpers.h +// Remove this comment and the template overloads below before 2026.11.0 + /// Format an unsigned integer in lowercased hex, starting with the most significant byte. /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) { val = convert_big_endian(val); return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T)); } /// Format the std::array \p data in lowercased hex. /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) { return format_hex(data.data(), data.size()); } -/** Format a byte array in pretty-printed, human-readable hex format. - * - * Converts binary data to a hexadecimal string representation with customizable formatting. - * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. - * Optionally includes the total byte count in parentheses at the end. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Pointer to the byte array to format. - * @param length Number of bytes in the array. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters. - * - * @note Returns empty string if data is nullptr or length is 0. - * @note The length will only be appended if show_length is true AND the length is greater than 4. - * - * Example: - * @code - * uint8_t data[] = {0xA1, 0xB2, 0xC3}; - * format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts) - * uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5}; - * format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)" - * format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)" - * format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5" - * @endcode - */ -std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); +// format_hex_pretty (all overloads) moved to alloc_helpers.h +// Remove this comment and the template overload below before 2026.11.0 -/** Format a 16-bit word array in pretty-printed, human-readable hex format. - * - * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Pointer to the 16-bit word array to format. - * @param length Number of 16-bit words in the array. - * @param separator Character to use between hex words (default: '.'). - * @param show_length Whether to append the word count in parentheses (default: true). - * @return Formatted hex string with 4-digit hex values per word. - * - * @note The length will only be appended if show_length is true AND the length is greater than 4. - * - * Example: - * @code - * uint16_t data[] = {0xA1B2, 0xC3D4}; - * format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts) - * uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6}; - * format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)" - * @endcode - */ -std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true); - -/** Format a byte vector in pretty-printed, human-readable hex format. - * - * Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit - * uppercase hex value with customizable separator. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Vector of bytes to format. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string representation of the vector contents. - * - * @note The length will only be appended if show_length is true AND the vector size is greater than 4. - * - * Example: - * @code - * std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF}; - * format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts) - * std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA}; - * format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)" - * format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)" - * @endcode - */ -std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true); - -/** Format a 16-bit word vector in pretty-printed, human-readable hex format. - * - * Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted - * as a 4-digit uppercase hex value in big-endian order. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Vector of 16-bit words to format. - * @param separator Character to use between hex words (default: '.'). - * @param show_length Whether to append the word count in parentheses (default: true). - * @return Formatted hex string representation of the vector contents. - * - * @note The length will only be appended if show_length is true AND the vector size is greater than 4. - * - * Example: - * @code - * std::vector<uint16_t> data = {0x1234, 0x5678}; - * format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts) - * std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC}; - * format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)" - * @endcode - */ -std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true); - -/** Format a string's bytes in pretty-printed, human-readable hex format. - * - * Treats each character in the string as a byte and formats it in hex. - * Useful for debugging binary data stored in std::string containers. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data String whose bytes should be formatted as hex. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string representation of the string's byte contents. - * - * @note The length will only be appended if show_length is true AND the string length is greater than 4. - * - * Example: - * @code - * std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43 - * format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts) - * std::string data2 = "ABCDE"; - * format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)" - * @endcode - */ -std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); - -/** Format an unsigned integer in pretty-printed, human-readable hex format. - * - * Converts the integer to big-endian byte order and formats each byte as hex. - * The most significant byte appears first in the output string. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). - * @param val The unsigned integer value to format. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string with most significant byte first. - * - * @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4. - * - * Example: - * @code - * uint32_t value = 0x12345678; - * format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts) - * uint64_t value2 = 0x123456789ABCDEF0; - * format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)" - * format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)" - * format_hex_pretty<uint16_t>(0x1234); // Returns "12.34" - * @endcode - */ +/// Format an unsigned integer in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) { val = convert_big_endian(val); @@ -1683,13 +1504,10 @@ inline char *format_bin_to(char (&buffer)[N], T val) { return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T)); } -/// Format the byte array \p data of length \p len in binary. -/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_bin(const uint8_t *data, size_t length); +// format_bin moved to alloc_helpers.h - remove this comment and template overload before 2026.11.0 + /// Format an unsigned integer in binary, starting with the most significant byte. /// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) { val = convert_big_endian(val); return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T)); @@ -1705,9 +1523,7 @@ enum ParseOnOffState : uint8_t { /// Parse a string that contains either on, off or toggle. ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); -/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0. -ESPDEPRECATED("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.", "2026.1.0") -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +// value_accuracy_to_string moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; @@ -1721,10 +1537,8 @@ size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> bu /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); -std::string base64_encode(const uint8_t *buf, size_t buf_len); -std::string base64_encode(const std::vector<uint8_t> &buf); - -std::vector<uint8_t> base64_decode(const std::string &encoded_string); +// base64_encode (both overloads), base64_decode (vector overload) moved to alloc_helpers.h +// Remove this comment before 2026.11.0 size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); @@ -2160,15 +1974,7 @@ class HighFrequencyLoopRequester { /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) -/// Get the device MAC address as a string, in lowercase hex notation. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -/// Use get_mac_address_into_buffer() instead. -std::string get_mac_address(); - -/// Get the device MAC address as a string, in colon-separated uppercase hex notation. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -/// Use get_mac_address_pretty_into_buffer() instead. -std::string get_mac_address_pretty(); +// get_mac_address, get_mac_address_pretty moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Get the device MAC address into the given buffer, in lowercase hex notation. /// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null diff --git a/script/ci-custom.py b/script/ci-custom.py index 6dce86924e0..02ec08bc318 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -722,18 +722,22 @@ def lint_trailing_whitespace(fname, match): # Heap-allocating helpers that cause fragmentation on long-running embedded devices. # These return std::string and should be replaced with stack-based alternatives. HEAP_ALLOCATING_HELPERS = { + "base64_encode": "base64_encode_to() with a pre-allocated buffer", "format_bin": "format_bin_to() with a stack buffer", "format_hex": "format_hex_to() with a stack buffer", "format_hex_pretty": "format_hex_pretty_to() with a stack buffer", "format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer", "get_mac_address": "get_mac_address_into_buffer() with a stack buffer", "get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer", + "str_lower_case": "manual tolower() with a stack buffer", "str_sanitize": "str_sanitize_to() with a stack buffer", "str_truncate": "removal (function is unused)", + "str_until": "manual strchr()/find() with a StringRef or stack buffer", "str_upper_case": "removal (function is unused)", "str_snake_case": "removal (function is unused)", "str_sprintf": "snprintf() with a stack buffer", "str_snprintf": "snprintf() with a stack buffer", + "value_accuracy_to_string": "value_accuracy_to_buf() with a stack buffer", } @@ -743,24 +747,33 @@ HEAP_ALLOCATING_HELPERS = { # get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc. # CPP_RE_EOL captures rest of line so NOLINT comments are detected r"[^\w](" + r"base64_encode(?!_)|" r"format_bin(?!_)|" r"format_hex(?!_)|" r"format_hex_pretty(?!_)|" r"format_mac_address_pretty|" r"get_mac_address_pretty(?!_)|" r"get_mac_address(?!_)|" + r"str_lower_case|" r"str_sanitize(?!_)|" r"str_truncate|" + r"str_until|" r"str_upper_case|" r"str_snake_case|" r"str_sprintf|" - r"str_snprintf" + r"str_snprintf|" + r"value_accuracy_to_string" r")\s*\(" + CPP_RE_EOL, include=cpp_include, exclude=[ # The definitions themselves + "esphome/core/alloc_helpers.h", + "esphome/core/alloc_helpers.cpp", + # Backward compatibility re-exports (remove before 2026.11.0) "esphome/core/helpers.h", "esphome/core/helpers.cpp", + # Vendored third-party library + "esphome/components/http_request/httplib.h", ], ) def lint_no_heap_allocating_helpers(fname, match): @@ -812,6 +825,7 @@ def lint_no_sprintf(fname, match): "esphome/components/http_request/httplib.h", # Deprecated helpers that return std::string "esphome/core/helpers.cpp", + "esphome/core/alloc_helpers.cpp", # The using declaration itself "esphome/core/helpers.h", # Test fixtures - not production embedded code From a5b1f3eece4d1d64e62daaa902259fd87f61f967 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:40:03 +0200 Subject: [PATCH 1553/2056] [core] Remove pre-sleep socket scan from fast select path (#15639) --- .../components/socket/bsd_sockets_impl.cpp | 44 +++++++++---------- esphome/components/socket/bsd_sockets_impl.h | 15 +++++-- .../components/socket/lwip_sockets_impl.cpp | 44 +++++++++---------- esphome/components/socket/lwip_sockets_impl.h | 15 +++++-- esphome/components/socket/socket.h | 21 +++++++-- esphome/core/application.cpp | 27 +----------- esphome/core/application.h | 41 ++++++----------- 7 files changed, 96 insertions(+), 111 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index aea7c776c62..92691b17ab8 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -14,38 +14,34 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { if (!monitor_loop || this->fd_ < 0) return; #ifdef USE_LWIP_FAST_SELECT - // Cache lwip_sock pointer and register for monitoring (hooks callback internally) - this->cached_sock_ = esphome_lwip_get_sock(this->fd_); - this->loop_monitored_ = App.register_socket(this->cached_sock_); + this->cached_sock_ = hook_fd_for_fast_select(this->fd_); #else this->loop_monitored_ = App.register_socket_fd(this->fd_); #endif } -BSDSocketImpl::~BSDSocketImpl() { - if (!this->closed_) { - this->close(); - } -} +BSDSocketImpl::~BSDSocketImpl() { this->close(); } int BSDSocketImpl::close() { - if (!this->closed_) { - // Unregister before closing to avoid dangling pointer in monitored set -#ifdef USE_LWIP_FAST_SELECT - if (this->loop_monitored_) { - App.unregister_socket(this->cached_sock_); - this->cached_sock_ = nullptr; - } -#else - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } -#endif - int ret = ::close(this->fd_); - this->closed_ = true; - return ret; + if (this->fd_ < 0) { + // Already closed, or never opened. + return 0; } - return 0; +#ifdef USE_LWIP_FAST_SELECT + // Null the cached lwip_sock pointer before closing. The underlying lwip slot can be + // recycled for a new connection as soon as ::close() returns, so anything that might + // dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would otherwise + // touch an unrelated socket's pcb. No per-socket callback unhook is needed — + // all LwIP sockets share the same static event_callback. + this->cached_sock_ = nullptr; +#else + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); + } +#endif + int ret = ::close(this->fd_); + this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible. + return ret; } int BSDSocketImpl::setblocking(bool blocking) { diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index e520784702b..57c1a430a2b 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -119,12 +119,21 @@ class BSDSocketImpl { int get_fd() const { return this->fd_; } protected: + // fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This + // replaces a separate closed_ flag: close() sets fd_ = -1 after ::close(), and the + // destructor / double-close path just check fd_ < 0. int fd_{-1}; #ifdef USE_LWIP_FAST_SELECT - struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() -#endif - bool closed_{false}; + // Cached lwip_sock pointer used for direct rcvevent reads in ready() on the + // fast-select path. Replaces loop_monitored_: null means this socket is not being + // monitored for read events — either monitoring was not requested, the fd was + // invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event + // callback was hooked and notifications are flowing. close() nulls this to prevent + // use-after-free via a recycled lwip slot. + struct lwip_sock *cached_sock_{nullptr}; +#else bool loop_monitored_{false}; +#endif }; } // namespace esphome::socket diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index 2fad429e0f7..b4eba3febf0 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -14,38 +14,34 @@ LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { if (!monitor_loop || this->fd_ < 0) return; #ifdef USE_LWIP_FAST_SELECT - // Cache lwip_sock pointer and register for monitoring (hooks callback internally) - this->cached_sock_ = esphome_lwip_get_sock(this->fd_); - this->loop_monitored_ = App.register_socket(this->cached_sock_); + this->cached_sock_ = hook_fd_for_fast_select(this->fd_); #else this->loop_monitored_ = App.register_socket_fd(this->fd_); #endif } -LwIPSocketImpl::~LwIPSocketImpl() { - if (!this->closed_) { - this->close(); - } -} +LwIPSocketImpl::~LwIPSocketImpl() { this->close(); } int LwIPSocketImpl::close() { - if (!this->closed_) { - // Unregister before closing to avoid dangling pointer in monitored set -#ifdef USE_LWIP_FAST_SELECT - if (this->loop_monitored_) { - App.unregister_socket(this->cached_sock_); - this->cached_sock_ = nullptr; - } -#else - if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); - } -#endif - int ret = lwip_close(this->fd_); - this->closed_ = true; - return ret; + if (this->fd_ < 0) { + // Already closed, or never opened. + return 0; } - return 0; +#ifdef USE_LWIP_FAST_SELECT + // Null the cached lwip_sock pointer before closing. The underlying lwip slot can be + // recycled for a new connection as soon as lwip_close() returns, so anything that + // might dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would + // otherwise touch an unrelated socket's pcb. No per-socket callback unhook is needed — + // all LwIP sockets share the same static event_callback. + this->cached_sock_ = nullptr; +#else + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); + } +#endif + int ret = lwip_close(this->fd_); + this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible. + return ret; } int LwIPSocketImpl::setblocking(bool blocking) { diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index 942d0ccf857..7f3b706cd8d 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -85,12 +85,21 @@ class LwIPSocketImpl { int get_fd() const { return this->fd_; } protected: + // fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This + // replaces a separate closed_ flag: close() sets fd_ = -1 after lwip_close(), and the + // destructor / double-close path just check fd_ < 0. int fd_{-1}; #ifdef USE_LWIP_FAST_SELECT - struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready() -#endif - bool closed_{false}; + // Cached lwip_sock pointer used for direct rcvevent reads in ready() on the + // fast-select path. Replaces loop_monitored_: null means this socket is not being + // monitored for read events — either monitoring was not requested, the fd was + // invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event + // callback was hooked and notifications are flowing. close() nulls this to prevent + // use-after-free via a recycled lwip slot. + struct lwip_sock *cached_sock_{nullptr}; +#else bool loop_monitored_{false}; +#endif }; } // namespace esphome::socket diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index ad55e889e80..204113e4b25 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -42,8 +42,23 @@ using ListenSocket = LWIPRawListenImpl; #ifdef USE_LWIP_FAST_SELECT /// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read. -inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { - return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); +/// cached_sock == nullptr means the socket is not monitored (monitor_loop was false, fd +/// was invalid, or esphome_lwip_get_sock() failed) — in that case return true so the +/// caller attempts the read and handles blocking itself. +inline bool socket_ready(struct lwip_sock *cached_sock) { + return cached_sock == nullptr || esphome_lwip_socket_has_data(cached_sock); +} + +/// Resolve an fd to its lwip_sock and install the netconn event-callback hook so the +/// main loop is woken by FreeRTOS task notifications when data arrives. Shared between +/// BSD and LwIP socket impls on the fast-select path. Returns the cached lwip_sock +/// pointer (or nullptr if the fd does not map to a valid lwip_sock). +inline struct lwip_sock *hook_fd_for_fast_select(int fd) { + struct lwip_sock *sock = esphome_lwip_get_sock(fd); + if (sock != nullptr) { + esphome_lwip_hook_socket(sock); + } + return sock; } #elif defined(USE_HOST) /// Shared ready() helper for fd-based socket implementations. @@ -69,7 +84,7 @@ bool socket_ready_fd(int fd, bool loop_monitored); #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) inline bool Socket::ready() const { #ifdef USE_LWIP_FAST_SELECT - return socket_ready(this->cached_sock_, this->loop_monitored_); + return socket_ready(this->cached_sock_); #else return socket_ready_fd(this->fd_, this->loop_monitored_); #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e5e1b36a65b..affee200669 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -509,32 +509,7 @@ void Application::enable_pending_loops_() { } } -#ifdef USE_LWIP_FAST_SELECT -bool Application::register_socket(struct lwip_sock *sock) { - // It modifies monitored_sockets_ without locking — must only be called from the main loop. - if (sock == nullptr) - return false; - esphome_lwip_hook_socket(sock); - this->monitored_sockets_.push_back(sock); - return true; -} - -void Application::unregister_socket(struct lwip_sock *sock) { - // It modifies monitored_sockets_ without locking — must only be called from the main loop. - for (size_t i = 0; i < this->monitored_sockets_.size(); i++) { - if (this->monitored_sockets_[i] != sock) - continue; - - // Swap with last element and pop - O(1) removal since order doesn't matter. - // No need to unhook the netconn callback — all LwIP sockets share the same - // static event_callback, and the socket will be closed by the caller. - if (i < this->monitored_sockets_.size() - 1) - this->monitored_sockets_[i] = this->monitored_sockets_.back(); - this->monitored_sockets_.pop_back(); - return; - } -} -#elif defined(USE_HOST) +#ifdef USE_HOST bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking diff --git a/esphome/core/application.h b/esphome/core/application.h index 645caa24045..a512af9c611 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -345,16 +345,13 @@ class Application { Scheduler scheduler; - /// Register/unregister a socket to be monitored for read events. - /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop. -#ifdef USE_LWIP_FAST_SELECT - /// Fast select path: hooks netconn callback and registers for monitoring. - /// @return true if registration was successful, false if sock is null - bool register_socket(struct lwip_sock *sock); - void unregister_socket(struct lwip_sock *sock); -#elif defined(USE_HOST) - /// Fallback select() path: monitors file descriptors. +#ifdef USE_HOST + /// Register/unregister a socket file descriptor with the host select() fallback loop. + /// USE_LWIP_FAST_SELECT builds do not use this API — sockets hook the lwIP netconn + /// event_callback directly (see socket.h hook_fd_for_fast_select) and rely on FreeRTOS + /// task notifications for wake-up. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. + /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop. /// @return true if registration was successful, false if fd exceeds limits bool register_socket_fd(int fd); void unregister_socket_fd(int fd); @@ -488,9 +485,7 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop FixedVector<Component *> looping_components_{}; -#ifdef USE_LWIP_FAST_SELECT - std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read -#elif defined(USE_HOST) +#ifdef USE_HOST std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors #endif #ifdef USE_HOST @@ -705,26 +700,16 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { #ifndef USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { #ifdef USE_LWIP_FAST_SELECT - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + // Fast path (ESP32/LibreTiny): FreeRTOS task notifications posted by the lwip + // event_callback wrapper (see lwip_fast_select.c) are the single source of truth for + // socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification + // that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns + // immediately) or wakes a blocked Take directly. Additional wake sources: + // wake_loop_threadsafe() from background tasks, and the delay_ms timeout. if (delay_ms == 0) [[unlikely]] { yield(); return; } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. #endif esphome::internal::wakeable_delay(delay_ms); } From 37608c26560fe0ba7eefad7a0129074e6b7ccc2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:40:24 +0200 Subject: [PATCH 1554/2056] [ltr390] Reduce data polling delay and timeout (#15507) --- esphome/components/ltr390/ltr390.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index ba4a7ea5cb4..033f31a3d17 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -45,6 +45,7 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) { uint8_t buffer[num_bytes]; // Wait until data available + constexpr uint32_t max_wait_ms = 25; const uint32_t now = millis(); while (true) { std::bitset<8> status = this->reg(LTR390_MAIN_STATUS).get(); @@ -52,12 +53,12 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) { if (available) break; - if (millis() - now > 100) { + if (millis() - now > max_wait_ms) { ESP_LOGW(TAG, "Sensor didn't return any data, aborting"); return {}; } - ESP_LOGD(TAG, "Waiting for data"); - delay(2); + ESP_LOGV(TAG, "Waiting for data"); + delay(1); } if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) { From 78875abee4a396b73b36557cbfa58ff5b51b3c1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:40:40 +0200 Subject: [PATCH 1555/2056] [core] Make buf_append_str PROGMEM-aware on ESP8266 (#15738) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/helpers.h | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index bb164f5034f..78476bf596f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1079,7 +1079,33 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, } #endif -/// Safely append a string to buffer without format parsing, returning new position (capped at size). +#ifdef USE_ESP8266 +/// Safely append a PROGMEM string to buffer, returning new position (capped at size). +/// ESP8266 internal implementation — prefer the `buf_append_str` macro which wraps +/// literals with `PSTR()` automatically so they stay in flash instead of eating RAM. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param str PROGMEM-resident string to append (must not be null) +/// @return New position after appending; returns `size` if `pos >= size`, otherwise +/// returns at most `size - 1` because one byte is reserved for the null terminator +inline size_t buf_append_str_p(char *buf, size_t size, size_t pos, PGM_P str) { + if (pos >= size) { + return size; + } + size_t remaining = size - pos - 1; // reserve space for null terminator + size_t len = strnlen_P(str, remaining); + memcpy_P(buf + pos, str, len); + pos += len; + buf[pos] = '\0'; + return pos; +} +/// Safely append a string to buffer, returning new position (capped at size). +/// More efficient than buf_append_printf for plain string literals. +/// On ESP8266 the literal is wrapped with PSTR() so it stays in flash. +#define buf_append_str(buf, size, pos, str) buf_append_str_p(buf, size, pos, PSTR(str)) +#else +/// Safely append a string to buffer, returning new position (capped at size). /// More efficient than buf_append_printf for plain string literals. /// @param buf Output buffer /// @param size Total buffer size @@ -1091,15 +1117,13 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str return size; } size_t remaining = size - pos - 1; // reserve space for null terminator - size_t len = strlen(str); - if (len > remaining) { - len = remaining; - } + size_t len = strnlen(str, remaining); memcpy(buf + pos, str, len); pos += len; buf[pos] = '\0'; return pos; } +#endif /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. From f05fa45747f2ae8f71928f99b161a8b7f0c5fd0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 04:41:13 +0200 Subject: [PATCH 1556/2056] [sensor] Specialize `throttle_with_priority` NaN-only case (#15823) --- esphome/components/sensor/__init__.py | 18 +++++++++++++++--- esphome/components/sensor/filter.cpp | 12 ++++++++++++ esphome/components/sensor/filter.h | 13 +++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index b658ff7056d..8dcb7165e30 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -275,6 +275,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) +ThrottleWithPriorityNanFilter = sensor_ns.class_( + "ThrottleWithPriorityNanFilter", Filter +) TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) @@ -656,9 +659,18 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( THROTTLE_WITH_PRIORITY_SCHEMA, ) async def throttle_with_priority_filter_to_code(config, filter_id): - if not isinstance(config[CONF_VALUE], list): - config[CONF_VALUE] = [config[CONF_VALUE]] - template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]] + values = config[CONF_VALUE] + if not isinstance(values, list): + values = [values] + # Specialize the common "NaN-only" case (the schema default when the user + # omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the + # generic ValueListFilter path requires. Behavior is identical: NaN sensor + # readings always bypass the throttle. + if values and all(isinstance(v, float) and math.isnan(v) for v in values): + filter_id = filter_id.copy() + filter_id.type = ThrottleWithPriorityNanFilter + return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) + template_ = [await cg.templatable(x, [], cg.float_) for x in values] return cg.new_Pvariable( filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_ ) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index fbac7d35358..4896757d3f3 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -269,6 +269,18 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co return {}; } +// ThrottleWithPriorityNanFilter +ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs) + : min_time_between_inputs_(min_time_between_inputs) {} +optional<float> ThrottleWithPriorityNanFilter::new_value(float value) { + const uint32_t now = App.get_loop_component_start_time(); + if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) { + this->last_input_ = now; + return value; + } + return {}; +} + // DeltaFilter DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1) : min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {} diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 0dbbc33ab34..a91d66a8fbe 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -399,6 +399,19 @@ template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> uint32_t min_time_between_inputs_; }; +/// Specialization of ThrottleWithPriorityFilter for the common "prioritize NaN" +/// case: skips the TemplatableFn<float> array + lambda and inlines the check. +class ThrottleWithPriorityNanFilter : public Filter { + public: + explicit ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs); + + optional<float> new_value(float value) override; + + protected: + uint32_t last_input_{0}; + uint32_t min_time_between_inputs_; +}; + // Base class for timeout filters - contains common loop logic class TimeoutFilterBase : public Filter, public Component { public: From a8bd035b62b7ab9712e394fafdbdef9cd772bcde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 04:44:25 +0200 Subject: [PATCH 1557/2056] Bump CodSpeedHQ/action from 4.13.1 to 4.14.0 (#15880) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aa5b2a5472..20c349ac003 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4 + uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0 with: run: ${{ steps.build.outputs.binary }} mode: simulation From 26a656af296172ef140d7632e19bfe25f6d1873b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:47:07 -0400 Subject: [PATCH 1558/2056] [ld2412] Fix null deref in set_basic_config when entities unconfigured (#15893) --- esphome/components/ld2412/ld2412.cpp | 42 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index a502ae3c109..093e8c72dcb 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -766,32 +766,38 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } void LD2412Component::set_basic_config() { + uint8_t min_gate = 1; + uint8_t max_gate = TOTAL_GATES; + uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT; + uint8_t out_pin_level = 0x01; + #ifdef USE_NUMBER - if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || - !this->timeout_number_->has_state()) { - return; + if (this->min_distance_gate_number_ != nullptr) { + if (!this->min_distance_gate_number_->has_state()) + return; + min_gate = static_cast<int>(this->min_distance_gate_number_->state); + } + if (this->max_distance_gate_number_ != nullptr) { + if (!this->max_distance_gate_number_->has_state()) + return; + max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1; + } + if (this->timeout_number_ != nullptr) { + if (!this->timeout_number_->has_state()) + return; + timeout = static_cast<int>(this->timeout_number_->state); } #endif #ifdef USE_SELECT - if (!this->out_pin_level_select_->has_state()) { - return; + if (this->out_pin_level_select_ != nullptr) { + if (!this->out_pin_level_select_->has_state()) + return; + out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()); } #endif uint8_t value[5] = { -#ifdef USE_NUMBER - lowbyte(static_cast<int>(this->min_distance_gate_number_->state)), - lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1), - lowbyte(static_cast<int>(this->timeout_number_->state)), - highbyte(static_cast<int>(this->timeout_number_->state)), -#else - 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, -#endif -#ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()), -#else - 0x01, // Default value if not using select -#endif + lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level, }; this->set_config_mode_(true); this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); From cb56f9a9bfd3705ab350e2c19d9e5f387b35ca09 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:47:16 -0400 Subject: [PATCH 1559/2056] [qmc5883l] Use GPIO interrupt when DRDY pin is configured (#15876) --- esphome/components/qmc5883l/qmc5883l.cpp | 36 ++++++++++++++++++++---- esphome/components/qmc5883l/qmc5883l.h | 5 ++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index d0488d0c9f7..44bd006c1ae 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -24,6 +24,8 @@ static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09; static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A; static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B; +void IRAM_ATTR QMC5883LComponent::gpio_intr(QMC5883LComponent *arg) { arg->enable_loop_soon_any_context(); } + void QMC5883LComponent::setup() { // Soft Reset if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) { @@ -35,6 +37,12 @@ void QMC5883LComponent::setup() { if (this->drdy_pin_) { this->drdy_pin_->setup(); + if (this->drdy_pin_->is_internal()) { + static_cast<InternalGPIOPin *>(this->drdy_pin_) + ->attach_interrupt(&QMC5883LComponent::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE); + this->drdy_use_isr_ = true; + this->stop_poller(); + } } uint8_t control_1 = 0; @@ -65,8 +73,8 @@ void QMC5883LComponent::setup() { return; } - if (this->get_update_interval() < App.get_loop_interval()) { - high_freq_.start(); + if (!this->drdy_use_isr_ && this->get_update_interval() < App.get_loop_interval()) { + this->high_freq_.start(); } } @@ -84,16 +92,32 @@ void QMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Heading", this->heading_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_PIN(" DRDY Pin: ", this->drdy_pin_); + if (this->drdy_pin_ != nullptr) { + ESP_LOGCONFIG(TAG, " DRDY mode: %s", + this->drdy_use_isr_ ? LOG_STR_LITERAL("interrupt") : LOG_STR_LITERAL("polling")); + } } void QMC5883LComponent::update() { - i2c::ErrorCode err; - uint8_t status = false; - - // If DRDY pin is configured and the data is not ready return. + // If DRDY is on an external expander we keep the polling path and early-return + // if data is not ready yet. Internal DRDY pins take the ISR path via loop(). if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) { return; } + this->read_sensor_(); +} + +void QMC5883LComponent::loop() { + this->disable_loop(); + if (!this->drdy_use_isr_ || !this->drdy_pin_->digital_read()) { + return; + } + this->read_sensor_(); +} + +void QMC5883LComponent::read_sensor_() { + i2c::ErrorCode err; + uint8_t status = false; // Status byte gets cleared when data is read, so we have to read this first. // If status and two axes are desired, it's possible to save one byte of traffic by enabling diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 21ef9c2a175..2ab6aa3e9f0 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -32,6 +32,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; + void loop() override; void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } @@ -44,6 +45,9 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } protected: + static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg); + void read_sensor_(); + QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ}; QMC5883LRange range_{QMC5883L_RANGE_200_UT}; QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512}; @@ -53,6 +57,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *heading_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; GPIOPin *drdy_pin_{nullptr}; + bool drdy_use_isr_{false}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, From f50409948551875d9626e1aa14a092d3a2738bf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 13:47:37 +0200 Subject: [PATCH 1560/2056] [api] Replace clients_ std::vector with compile-time std::array + uint8_t count (#15889) --- esphome/components/api/__init__.py | 11 +++-- esphome/components/api/api_server.cpp | 61 ++++++++++++++------------- esphome/components/api/api_server.h | 33 ++++++++++++--- esphome/core/defines.h | 1 + 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 84589d540d3..ad778f20ad2 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault( CONF_MAX_CONNECTIONS, esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes - esp32=8, # 520KB RAM available + esp32=5, # 520KB RAM available rp2040=4, # 264KB RAM but LWIP constraints - bk72xx=8, # Moderate RAM - rtl87xx=8, # Moderate RAM + bk72xx=5, # Moderate RAM + rtl87xx=5, # Moderate RAM host=8, # Abundant resources - ln882x=8, # Moderate RAM + ln882x=5, # Moderate RAM ): cv.int_range(min=1, max=20), # Maximum queued send buffers per connection before dropping connection # Each buffer uses ~8-12 bytes overhead plus actual message size @@ -336,8 +336,7 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) if CONF_LISTEN_BACKLOG in config: cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) - if CONF_MAX_CONNECTIONS in config: - cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) + cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS]) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d9c3cc68467..4559168ece2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -118,7 +118,7 @@ void APIServer::loop() { this->accept_new_connections_(); } - if (this->clients_.empty()) { + if (this->api_connection_count_ == 0) { // Check reboot timeout - done in loop to avoid scheduler heap churn // (cancelled scheduler items sit in heap memory until their scheduled time) if (this->reboot_timeout_ != 0) { @@ -135,15 +135,15 @@ void APIServer::loop() { // Check network connectivity once for all clients if (!network::is_connected()) { // Network is down - disconnect all clients - for (auto &client : this->clients_) { + for (auto &client : this->active_clients()) { client->on_fatal_error(); client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); } // Continue to process and clean up the clients below } - size_t client_index = 0; - while (client_index < this->clients_.size()) { + uint8_t client_index = 0; + while (client_index < this->api_connection_count_) { auto &client = this->clients_[client_index]; // Common case: process active client @@ -161,7 +161,7 @@ void APIServer::loop() { } } -void APIServer::remove_client_(size_t client_index) { +void APIServer::remove_client_(uint8_t client_index) { auto &client = this->clients_[client_index]; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES @@ -179,14 +179,17 @@ void APIServer::remove_client_(size_t client_index) { // Close socket now (was deferred from on_fatal_error to allow getpeername) client->helper_->close(); - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); + // Swap-and-reset: move the removed client to the trailing slot and null it out so slots + // [api_connection_count_, N) remain nullptr. + const uint8_t last_index = this->api_connection_count_ - 1; + if (client_index < last_index) { + std::swap(this->clients_[client_index], this->clients_[last_index]); } - this->clients_.pop_back(); + this->clients_[last_index].reset(); + this->api_connection_count_--; // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { + if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) { this->status_set_warning(LOG_STR("waiting for client connection")); this->last_connected_ = App.get_loop_component_start_time(); } @@ -210,8 +213,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { sock->getpeername_to(peername); // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + if (this->api_connection_count_ >= MAX_API_CONNECTIONS) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername); // Immediately close - socket destructor will handle cleanup sock.reset(); continue; @@ -220,11 +223,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { ESP_LOGD(TAG, "Accept %s", peername); auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); + this->clients_[this->api_connection_count_++].reset(conn); conn->start(); // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) { this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } @@ -237,7 +240,7 @@ void APIServer::dump_config() { " Address: %s:%u\n" " Listen backlog: %u\n" " Max connections: %u", - network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); + network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS); #ifdef USE_API_NOISE ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); if (!this->noise_ctx_.has_psk()) { @@ -255,7 +258,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ return; \ - for (auto &c : this->clients_) { \ + for (auto &c : this->active_clients()) { \ if (c->flags_.state_subscription) \ c->send_##entity_name##_state(obj); \ } \ @@ -337,7 +340,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater) void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { if (c->flags_.state_subscription) c->send_event(obj); } @@ -349,7 +352,7 @@ void APIServer::on_event(event::Event *obj) { void APIServer::on_update(update::UpdateEntity *obj) { if (obj->is_internal()) return; - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { if (c->flags_.state_subscription) c->send_update_state(obj); } @@ -360,7 +363,7 @@ void APIServer::on_update(update::UpdateEntity *obj) { void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { // We could add code to manage a second subscription type, but, since this message type is // very infrequent and small, we simply send it to all clients - for (auto &c : this->clients_) + for (auto &c : this->active_clients()) c->send_message(msg); } #endif @@ -375,7 +378,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_ resp.key = key; resp.timings = timings; - for (auto &c : this->clients_) + for (auto &c : this->active_clients()) c->send_infrared_rf_receive_event(resp); } #endif @@ -392,7 +395,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat #ifdef USE_API_HOMEASSISTANT_SERVICES void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) { - for (auto &client : this->clients_) { + for (auto &client : this->active_clients()) { client->send_homeassistant_action(call); } } @@ -532,7 +535,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString return; } ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { DisconnectRequest req; c->send_message(req); } @@ -583,7 +586,7 @@ bool APIServer::clear_noise_psk(bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { - for (auto &client : this->clients_) { + for (auto &client : this->active_clients()) { if (!client->flags_.remove && client->is_authenticated()) { client->send_time_request(); return; // Only request from one client to avoid clock conflicts @@ -593,8 +596,8 @@ void APIServer::request_time() { #endif bool APIServer::is_connected_with_state_subscription() const { - for (const auto &client : this->clients_) { - if (client->flags_.state_subscription) { + for (uint8_t i = 0; i < this->api_connection_count_; i++) { + if (this->clients_[i]->flags_.state_subscription) { return true; } } @@ -609,7 +612,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size // we would be filling a buffer we are trying to clear return; } - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { if (!c->flags_.remove && c->get_log_subscription_level() >= level) c->try_send_log_message(level, tag, message, message_len); } @@ -618,7 +621,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size #ifdef USE_CAMERA void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) { - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { if (!c->flags_.remove) c->set_camera_state(image); } @@ -635,7 +638,7 @@ void APIServer::on_shutdown() { this->batch_delay_ = 5; // Send disconnect requests to all connected clients - for (auto &c : this->clients_) { + for (auto &c : this->active_clients()) { DisconnectRequest req; if (!c->send_message(req)) { // If we can't send the disconnect request directly (tx_buffer full), @@ -653,7 +656,7 @@ bool APIServer::teardown() { this->loop(); // Return true only when all clients have been torn down - return this->clients_.empty(); + return this->api_connection_count_ == 0; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 65076879a24..d6ac1a6d5d8 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -21,6 +21,8 @@ #include "esphome/components/camera/camera.h" #endif +#include <array> +#include <memory> #include <vector> namespace esphome::api { @@ -63,7 +65,6 @@ class APIServer final : public Component, void set_batch_delay(uint16_t batch_delay); uint16_t get_batch_delay() const { return batch_delay_; } void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; } - void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } @@ -186,9 +187,26 @@ class APIServer final : public Component, void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); #endif - bool is_connected() const { return !this->clients_.empty(); } + bool is_connected() const { return this->api_connection_count_ != 0; } bool is_connected_with_state_subscription() const; + // Range-for view over the populated slice [0, api_connection_count_). Read-only with respect + // to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the + // APIConnection but cannot reset/move the slot and break the count invariant. + using APIConnectionPtr = std::unique_ptr<APIConnection>; + class ActiveClientsView { + const APIConnectionPtr *begin_; + const APIConnectionPtr *end_; + + public: + ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {} + const APIConnectionPtr *begin() const { return this->begin_; } + const APIConnectionPtr *end() const { return this->end_; } + }; + ActiveClientsView active_clients() const { + return {this->clients_.data(), this->clients_.data() + this->api_connection_count_}; + } + #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { const char *entity_id; // Pointer to flash (internal) or heap (external) @@ -234,8 +252,8 @@ class APIServer final : public Component, protected: // Accept incoming socket connections. Only called when socket has pending connections. void __attribute__((noinline)) accept_new_connections_(); - // Remove a disconnected client by index. Swaps with last element and pops. - void __attribute__((noinline)) remove_client_(size_t client_index); + // Remove a disconnected client by index. Swaps with the last populated slot and resets it. + void __attribute__((noinline)) remove_client_(uint8_t client_index); #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, @@ -273,8 +291,9 @@ class APIServer final : public Component, uint32_t reboot_timeout_{300000}; uint32_t last_connected_{0}; + // Slots [0, api_connection_count_) are populated; trailing slots are always nullptr. + std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{}; // Vectors and strings (12 bytes each on 32-bit) - std::vector<std::unique_ptr<APIConnection>> clients_; // Shared proto write buffer for all connections. // Not pre-allocated: all send paths call prepare_first_message_buffer() which // reserves the exact needed size. Pre-allocating here would cause heap fragmentation @@ -309,10 +328,10 @@ class APIServer final : public Component, uint16_t port_{6053}; uint16_t batch_delay_{100}; // Connection limits - these defaults will be overridden by config values - // from cv.SplitDefault in __init__.py which sets platform-specific defaults + // from cv.SplitDefault in __init__.py which sets platform-specific defaults. uint8_t listen_backlog_{4}; - uint8_t max_connections_{8}; bool shutting_down_ = false; + uint8_t api_connection_count_{0}; // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d8b4faced9e..0fb72215713 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -177,6 +177,7 @@ #define USE_API_USER_DEFINED_ACTION_RESPONSES #define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #define API_MAX_SEND_QUEUE 8 +#define MAX_API_CONNECTIONS 6 #define USE_MD5 #define USE_SHA256 #define USE_MQTT From e4d5886383eff13b0c86139102f8e04d49e2f993 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 13:48:16 +0200 Subject: [PATCH 1561/2056] [zwave_proxy] Inline loop() hot-path fast-paths for response_handler_ and process_uart_ (#15887) --- .../components/zwave_proxy/zwave_proxy.cpp | 10 +++--- esphome/components/zwave_proxy/zwave_proxy.h | 34 +++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index ecb38b25e7f..8a24bd57d68 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -101,8 +101,10 @@ void ZWaveProxy::loop() { this->status_clear_warning(); } -void ZWaveProxy::process_uart_() { - while (this->available()) { +void ZWaveProxy::process_uart_slow_() { + // Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to + // drain bytes — available() is still checked at the tail, but not redundantly on entry. + do { uint8_t byte; if (!this->read_byte(&byte)) { this->status_set_warning(LOG_STR("UART read failed")); @@ -137,7 +139,7 @@ void ZWaveProxy::process_uart_() { this->api_connection_->send_message(this->outgoing_proto_msg_); } } - } + } while (this->available()); } void ZWaveProxy::dump_config() { @@ -414,7 +416,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { } } -bool ZWaveProxy::response_handler_() { +bool ZWaveProxy::response_handler_slow_() { switch (this->parsing_state_) { case ZWAVE_PARSING_STATE_SEND_ACK: this->last_response_ = ZWAVE_FRAME_TYPE_ACK; diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 0b810de29fe..dc5dc46abc0 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -38,6 +38,13 @@ enum ZWaveParsingState : uint8_t { ZWAVE_PARSING_STATE_READ_BL_MENU, }; +// response_handler_()'s inline fast-path relies on SEND_ACK/CAN/NAK being contiguous in this +// enum so a single range check (state - SEND_ACK < 3) is equivalent to three equality checks. +static_assert(ZWAVE_PARSING_STATE_SEND_CAN == ZWAVE_PARSING_STATE_SEND_ACK + 1, + "SEND_CAN must immediately follow SEND_ACK for response_handler_ fast-path"); +static_assert(ZWAVE_PARSING_STATE_SEND_NAK == ZWAVE_PARSING_STATE_SEND_ACK + 2, + "SEND_NAK must immediately follow SEND_CAN for response_handler_ fast-path"); + enum ZWaveProxyFeature : uint32_t { FEATURE_ZWAVE_PROXY_ENABLED = 1 << 0, }; @@ -72,8 +79,31 @@ class ZWaveProxy : public uart::UARTDevice, public Component { void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) void parse_start_(uint8_t byte); - bool response_handler_(); - void process_uart_(); // Process all available UART data + // Inline fast-path: most calls happen with parsing_state_ outside the SEND_* range, so skip the + // out-of-line call entirely in the hot path (e.g. every loop() tick) and only pay for the real + // work when a response is actually pending. ESPHOME_ALWAYS_INLINE is required because with -Os + // gcc otherwise clones the wrapper into a shared $isra$ outline and keeps the call8. + ESPHOME_ALWAYS_INLINE bool response_handler_() { + if (this->parsing_state_ < ZWAVE_PARSING_STATE_SEND_ACK || this->parsing_state_ > ZWAVE_PARSING_STATE_SEND_NAK) { + return false; + } + return this->response_handler_slow_(); + } + bool response_handler_slow_(); + // Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most backends). + // On an idle loop tick we want to skip the call to process_uart_ entirely. When bytes are + // pending we fall into the slow path, which drains the UART with a do/while so available() is + // only checked once per byte — no redundant re-check on entry. + ESPHOME_ALWAYS_INLINE void process_uart_() { + if (!this->available()) { + return; + } + this->process_uart_slow_(); + } + // Precondition: caller must guarantee available() > 0 before invoking (see inline + // process_uart_ above). The slow path uses do/while and would otherwise set a spurious UART + // warning on entry if called with no bytes pending. + void process_uart_slow_(); // Pre-allocated message - always ready to send api::ZWaveProxyFrame outgoing_proto_msg_; From 947c714f8936ffad5355a3bf9c72a2fbff3a5be2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 13:48:33 +0200 Subject: [PATCH 1562/2056] [core] Inline api_is_connected() for hot-path callers (#15888) --- esphome/core/util.cpp | 14 -------------- esphome/core/util.h | 22 ++++++++++++++++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index 996cf8e310a..54a7956163a 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -1,28 +1,14 @@ #include "esphome/core/util.h" -#include "esphome/core/defines.h" #include "esphome/core/application.h" #include "esphome/core/version.h" #include "esphome/core/log.h" -#ifdef USE_API -#include "esphome/components/api/api_server.h" -#endif - #ifdef USE_MQTT #include "esphome/components/mqtt/mqtt_client.h" #endif namespace esphome { -bool api_is_connected() { -#ifdef USE_API - if (api::global_api_server != nullptr) { - return api::global_api_server->is_connected(); - } -#endif - return false; -} - bool mqtt_is_connected() { #ifdef USE_MQTT if (mqtt::global_mqtt_client != nullptr) { diff --git a/esphome/core/util.h b/esphome/core/util.h index 1ca0173eab0..8f90aa34119 100644 --- a/esphome/core/util.h +++ b/esphome/core/util.h @@ -1,10 +1,28 @@ #pragma once #include <string> + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + namespace esphome { -/// Return whether the node has at least one client connected to the native API -bool api_is_connected(); +/// Return whether the node has at least one client connected to the native API. +/// +/// Inline so that hot-path callers (e.g. component loop() ticks that check connectivity every +/// iteration) can skip the call8/return pair. With USE_API disabled this trivially returns false +/// and collapses at compile time. +#ifdef USE_API +ESPHOME_ALWAYS_INLINE inline bool api_is_connected() { + return api::global_api_server != nullptr && api::global_api_server->is_connected(); +} +#else +ESPHOME_ALWAYS_INLINE inline bool api_is_connected() { return false; } +#endif /// Return whether the node has an active connection to an MQTT broker bool mqtt_is_connected(); From 1504ac3d19e2225f7713eabc4c20c8d543a91bfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 14:32:29 +0200 Subject: [PATCH 1563/2056] [core] Replace strnlen in buf_append_str for Zephyr compatibility (#15892) --- esphome/core/helpers.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 78476bf596f..6b71916cd23 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1117,7 +1117,10 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str return size; } size_t remaining = size - pos - 1; // reserve space for null terminator - size_t len = strnlen(str, remaining); + size_t len = 0; + while (len < remaining && str[len] != '\0') { + len++; + } memcpy(buf + pos, str, len); pos += len; buf[pos] = '\0'; From e4f413adad74735d7dc61332c06376dd6904e9ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 14:48:21 +0200 Subject: [PATCH 1564/2056] [core] decouple main loop cadence from scheduler wake timing (#15792) --- .../runtime_stats/runtime_stats.cpp | 11 +- .../components/runtime_stats/runtime_stats.h | 20 ++- esphome/core/application.cpp | 9 +- esphome/core/application.h | 153 ++++++++++++------ esphome/core/wake.cpp | 16 ++ esphome/core/wake.h | 51 +++++- .../wake_test_component/__init__.py | 19 +++ .../wake_test_component.cpp | 19 +++ .../wake_test_component/wake_test_component.h | 27 ++++ .../fixtures/loop_interval_decoupling.yaml | 60 +++++++ ...p_interval_default_not_pulled_forward.yaml | 51 ++++++ .../fixtures/wake_loop_forces_phase_b.yaml | 52 ++++++ .../test_loop_interval_decoupling.py | 75 +++++++++ ...oop_interval_default_not_pulled_forward.py | 67 ++++++++ .../test_wake_loop_forces_phase_b.py | 76 +++++++++ 15 files changed, 644 insertions(+), 62 deletions(-) create mode 100644 tests/integration/fixtures/external_components/wake_test_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp create mode 100644 tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h create mode 100644 tests/integration/fixtures/loop_interval_decoupling.yaml create mode 100644 tests/integration/fixtures/loop_interval_default_not_pulled_forward.yaml create mode 100644 tests/integration/fixtures/wake_loop_forces_phase_b.yaml create mode 100644 tests/integration/test_loop_interval_decoupling.py create mode 100644 tests/integration/test_loop_interval_default_not_pulled_forward.py create mode 100644 tests/integration/test_wake_loop_forces_phase_b.py diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 9ed141155a5..d733394b78d 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -137,11 +137,12 @@ bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) { return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us; } -void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { - if ((int32_t) (current_time - this->next_log_time_) >= 0) { - this->log_stats_(); - this->next_log_time_ = current_time + this->log_interval_; - } +// Slow path for process_pending_stats — gate already checked by the inline +// wrapper in runtime_stats.h. Out-of-line keeps the log_stats_ machinery out +// of Application::loop(). +void RuntimeStatsCollector::process_pending_stats_slow_(uint32_t current_time) { + this->log_stats_(); + this->next_log_time_ = current_time + this->log_interval_; } } // namespace runtime_stats diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 82e0fb7c618..888d48e6728 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -6,6 +6,7 @@ #include <cstdint> #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -26,14 +27,24 @@ class RuntimeStatsCollector { } uint32_t get_log_interval() const { return this->log_interval_; } - // Process any pending stats printing (should be called after component loop) - void process_pending_stats(uint32_t current_time); + // Process any pending stats printing. Called on every Application::loop() + // tick, so the common "not yet time to log" path must be cheap — inline + // the gate check and keep the actual logging work out-of-line. + void ESPHOME_ALWAYS_INLINE process_pending_stats(uint32_t current_time) { + if ((int32_t) (current_time - this->next_log_time_) >= 0) [[unlikely]] { + this->process_pending_stats_slow_(current_time); + } + } // Record the wall time of one main loop iteration excluding the yield/sleep. // Called once per loop from Application::loop(). // active_us = total time between loop start and just before yield. - // before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop). - // tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix. + // before_us = time spent in Phase A (scheduler tick) excluding time + // already attributed to per-component stats. + // tail_us = time spent in after_component_phase_ + the trailing record/stats + // prefix. Only meaningful on component-phase ticks; reported + // as 0 on Phase A-only ticks (no component phase ran, so any + // overhead between Phase A and stats belongs to "residual"). // Residual overhead at log time = active − Σ(component) − before − tail, // which captures per-iteration inter-component bookkeeping (set_current_component, // WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls, @@ -55,6 +66,7 @@ class RuntimeStatsCollector { } protected: + void process_pending_stats_slow_(uint32_t current_time); void log_stats_(); // Static comparators — member functions have friend access, lambdas do not static bool compare_period_time(Component *a, Component *b); diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index affee200669..b626eb1de67 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -93,8 +93,11 @@ void Application::setup() { do { uint32_t now = millis(); - // Process pending loop enables to handle GPIO interrupts during setup - this->before_loop_tasks_(now); + // Service scheduler and process pending loop enables to handle GPIO + // interrupts during setup. During setup we always run the component + // phase (no loop_interval_ gate), so call both helpers unconditionally. + this->scheduler_tick_(now); + this->before_component_phase_(); for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component @@ -103,7 +106,7 @@ void Application::setup() { this->feed_wdt(); } - this->after_loop_tasks_(); + this->after_component_phase_(); yield(); } while (!component->can_proceed() && !component->is_failed()); } diff --git a/esphome/core/application.h b/esphome/core/application.h index a512af9c611..d3851a32da9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -426,8 +426,9 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); - inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } + inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now); + inline void ESPHOME_ALWAYS_INLINE before_component_phase_(); + inline void ESPHOME_ALWAYS_INLINE after_component_phase_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -582,16 +583,25 @@ inline void Application::drain_wake_notifications_() { } #endif // USE_HOST -inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +// Phase A: drain wake notifications and run the scheduler. Invoked on every +// Application::loop() tick regardless of whether a component phase runs, so +// scheduler items fire at their requested cadence even when the caller has +// raised loop_interval_ for power savings (see Application::loop()). +// Returns the timestamp of the last scheduler item that ran (or `now` +// unchanged if none ran), so the caller's WDT feed stays monotonic with the +// per-item feeds inside scheduler.call() without an extra millis(). +inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) { #ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif + return this->scheduler.call(now); +} - // Scheduler::call feeds the WDT per item and returns the timestamp of the - // last fired item, or the input unchanged when nothing ran. - uint32_t last_op_end_time = this->scheduler.call(loop_start_time); - +// Phase B entry: only invoked when a component loop phase is about to run. +// Processes pending enable_loop requests from ISRs and marks in_loop_ so +// reentrant modifications during component.loop() are safe. +inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() { // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions if (this->has_pending_enable_loop_requests_) { @@ -608,7 +618,6 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t l // Mark that we're in the loop for safe reentrant modifications this->in_loop_ = true; - return last_op_end_time; } inline void ESPHOME_ALWAYS_INLINE Application::loop() { @@ -623,46 +632,77 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // so charging it again to "before" would double-count. uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us; #endif - // Get the initial loop time at the start - uint32_t last_op_end_time = millis(); + // Phase A: always service the scheduler. Decouples scheduler cadence from + // loop_interval_ so raised intervals (for power savings) don't drag scheduled + // items forward. A tick that only runs the scheduler is cheap. + // scheduler_tick_ returns the timestamp of the last scheduler item that ran + // (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now` + // so the gate check and WDT feed both reflect actual elapsed time after + // scheduler dispatch, without an extra millis() call. + uint32_t now = this->scheduler_tick_(millis()); + // Guarantee one WDT feed per tick even when the scheduler had nothing to + // dispatch and the component phase is gated out — covers configs with no + // looping components and no scheduler work (setup() has its own + // per-component feed_wdt calls, so only do this here, not in scheduler_tick_). + this->feed_wdt_with_time(now); - // Returned timestamp keeps us monotonic with last_wdt_feed_ (advanced by - // the scheduler's per-item feeds) without an extra millis() call. - last_op_end_time = this->before_loop_tasks_(last_op_end_time); - // Guarantee a WDT touch every tick — covers configs with no looping - // components and no scheduler work, where the per-item / per-component - // feeds never fire. Rate-limited inline fast path, ~free when unneeded. - this->feed_wdt_with_time(last_op_end_time); #ifdef USE_RUNTIME_STATS uint32_t loop_before_end_us = micros(); uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap; + // Only meaningful when do_component_phase is true; initialized to 0 so the + // tail bucket receives 0 on Phase A-only ticks (no component tail happened, + // the gate-check / stats-prefix overhead belongs to "residual", not "tail"). + uint32_t loop_tail_start_us = 0; #endif - for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; - this->current_loop_index_++) { - Component *component = this->looping_components_[this->current_loop_index_]; + // Gate the component phase on loop_interval_, an active high-frequency + // request, or an explicit wake from a background producer. A scheduler-only + // wake (e.g. set_interval firing under a raised loop_interval_) leaves the + // component phase gated; an external producer that called wake_loop_* + // (MQTT RX, USB RX, BLE event, etc.) needs the component phase to actually + // run so its component's loop() can drain the queued work — that is the + // long-standing semantic of wake_loop_threadsafe(), and the wake_request + // flag preserves it. wake_request_take() exchange-clears the flag; wakes + // that arrive during Phase B re-set it and run Phase B again on the next + // iteration. + const bool high_frequency = HighFrequencyLoopRequester::is_high_frequency(); + const uint32_t elapsed = now - this->last_loop_; + const bool woke = esphome::wake_request_take(); + const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_); - // Update the cached time before each component runs - this->loop_component_start_time_ = last_op_end_time; + if (do_component_phase) { + this->before_component_phase_(); - { - this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->loop(); - // Use the finish method to get the current time as the end time - last_op_end_time = guard.finish(); + uint32_t last_op_end_time = now; + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + + { + this->set_current_component(component); + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; + component->loop(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); + } + this->feed_wdt_with_time(last_op_end_time); } - this->feed_wdt_with_time(last_op_end_time); + +#ifdef USE_RUNTIME_STATS + loop_tail_start_us = micros(); +#endif + this->last_loop_ = last_op_end_time; + now = last_op_end_time; + this->after_component_phase_(); } #ifdef USE_RUNTIME_STATS - uint32_t loop_tail_start_us = micros(); -#endif - this->after_loop_tasks_(); - -#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 + // Record per-tick timing on every loop, not just component-phase ticks. + // record_loop_active is a small accumulator; process_pending_stats is an + // inline gate check that early-outs unless now >= next_log_time_. if (global_runtime_stats != nullptr) { uint32_t loop_now_us = micros(); // Subtract scheduled-component time from the "before" bucket so it is @@ -671,25 +711,40 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us ? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us) : 0; - global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, - loop_now_us - loop_tail_start_us); - global_runtime_stats->process_pending_stats(last_op_end_time); + // tail_us is only defined when Phase B ran; 0 on Phase A-only ticks so the + // stats bucket keeps its "component-phase trailing overhead" meaning. + uint32_t loop_tail_us = do_component_phase ? (loop_now_us - loop_tail_start_us) : 0; + global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, loop_tail_us); + global_runtime_stats->process_pending_stats(now); } #endif - // Use the last component's end time instead of calling millis() again + // Compute sleep: bounded by time-until-next-component-phase and the + // scheduler's next deadline. When a scheduler timer fires it re-enters + // loop(), Phase A services it, and the component phase stays gated by + // loop_interval_. When a background producer calls wake_loop_threadsafe() + // it sets the wake_request flag and wakes select() / the task notification; + // the gate above sees the flag and runs Phase B too so the producer's + // component can drain its queued work without waiting up to loop_interval_. + // + // Re-read HighFrequencyLoopRequester::is_high_frequency() here instead of + // reusing the cached `high_frequency` captured above: a component calling + // HighFrequencyLoopRequester::start() from within its loop() would + // otherwise sit under the stale value and sleep for up to loop_interval_ + // before the request took effect. That was fine pre-decoupling (the old + // main loop also called the function fresh at the sleep point) but now + // matters much more — loop_interval_ is a power-saving knob documented + // to accept multi-second values, so the stale path could add seconds of + // latency on an HF request. The call is a trivial atomic read. uint32_t delay_time = 0; - auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) { - delay_time = this->loop_interval_ - elapsed; - 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); - delay_time = std::min(next_schedule, delay_time); + if (!HighFrequencyLoopRequester::is_high_frequency()) { + const uint32_t elapsed_since_phase = now - this->last_loop_; + const uint32_t until_phase = + (elapsed_since_phase >= this->loop_interval_) ? 0 : (this->loop_interval_ - elapsed_since_phase); + const uint32_t until_sched = this->scheduler.next_schedule_in(now).value_or(until_phase); + delay_time = std::min(until_phase, until_sched); } this->yield_with_select_(delay_time); - this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { this->process_dump_config_(); diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 3709fa88ac7..cebc4d04b7f 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,9 +12,22 @@ namespace esphome { +// === Wake-requested flag storage === +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::atomic<uint8_t> g_wake_requested{0}; +#else +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; +#endif + // === ESP32 / LibreTiny — IRAM_ATTR entry points === #if defined(USE_ESP32) || defined(USE_LIBRETINY) void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { + // ISR-safe: set flag before notify so the wake is visible on the next gate + // check. wake_request_set() is just an aligned 8-bit store / atomic store + // and is safe from IRAM. + wake_request_set(); esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } @@ -72,6 +85,9 @@ void wakeable_delay(uint32_t ms) { // === Host (UDP loopback socket) === #ifdef USE_HOST void wake_loop_threadsafe() { + // Set flag before sending so the consumer's gate check on the next loop() + // entry observes the wake regardless of select() scheduling. + wake_request_set(); if (App.wake_socket_fd_ >= 0) { const char dummy = 1; ::send(App.wake_socket_fd_, &dummy, 1, 0); diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 77a38d429e5..41b7ab33b51 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -7,6 +7,10 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#include <atomic> +#endif + #if defined(USE_ESP32) || defined(USE_LIBRETINY) #include "esphome/core/main_task.h" #endif @@ -25,12 +29,48 @@ namespace esphome { extern volatile bool g_main_loop_woke; #endif +// === wake_request flag — signals Application::loop() that a producer queued +// work for some component's loop() to drain (MQTT RX, USB RX, BLE event, etc.) +// and the component phase should run this tick instead of being held off by +// the loop_interval_ gate. Set by every wake_loop_* entry point; consumed +// (via exchange-and-clear) at the gate in Application::loop(). === +// +// std::atomic<uint8_t> rather than std::atomic<bool> because GCC on Xtensa +// generates an indirect function call for atomic<bool> ops instead of inlining +// them — same workaround applied in scheduler.h for the SchedulerItem::remove +// flag. On non-atomic platforms a volatile uint8_t suffices: 8-bit aligned +// loads/stores are atomic on every supported MCU, and the platform signal +// that follows wake_request_set() (FreeRTOS task-notify, esp_schedule, socket +// send) provides the cross-thread/cross-core memory barrier. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern std::atomic<uint8_t> g_wake_requested; + +__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested.store(1, std::memory_order_release); } +__attribute__((always_inline)) inline bool wake_request_take() { + return g_wake_requested.exchange(0, std::memory_order_acquire) != 0; +} +#else +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern volatile uint8_t g_wake_requested; + +__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested = 1; } +__attribute__((always_inline)) inline bool wake_request_take() { + uint8_t v = g_wake_requested; + g_wake_requested = 0; + return v != 0; +} +#endif + // === ESP32 / LibreTiny (FreeRTOS) === #if defined(USE_ESP32) || defined(USE_LIBRETINY) /// Wake the main loop from any context (ISR or task). /// always_inline so callers placed in IRAM keep the whole wake path in IRAM. __attribute__((always_inline)) inline void wake_main_task_any_context() { + // Set the wake-requested flag BEFORE the task notification so the consumer + // (Application::loop() gate) is guaranteed to see it on its next gate check. + wake_request_set(); if (in_isr_context()) { BaseType_t px_higher_priority_task_woken = pdFALSE; esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); @@ -50,7 +90,10 @@ __attribute__((always_inline)) inline void wake_main_task_any_context() { void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); void wake_loop_any_context(); -inline void wake_loop_threadsafe() { esphome_main_task_notify(); } +inline void wake_loop_threadsafe() { + wake_request_set(); + esphome_main_task_notify(); +} namespace internal { inline void wakeable_delay(uint32_t ms) { @@ -67,6 +110,9 @@ inline void wakeable_delay(uint32_t ms) { /// Inline implementation — IRAM callers inline this directly. inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { + // Set the wake-requested flag BEFORE esp_schedule so the consumer is + // guaranteed to see it on its next gate check. + wake_request_set(); g_main_loop_woke = true; esp_schedule(); } @@ -98,6 +144,9 @@ inline void wakeable_delay(uint32_t ms) { #elif defined(USE_RP2040) inline void wake_loop_any_context() { + // Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed + // to see it on its next gate check. + wake_request_set(); g_main_loop_woke = true; __sev(); } diff --git a/tests/integration/fixtures/external_components/wake_test_component/__init__.py b/tests/integration/fixtures/external_components/wake_test_component/__init__.py new file mode 100644 index 00000000000..ce241678897 --- /dev/null +++ b/tests/integration/fixtures/external_components/wake_test_component/__init__.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@esphome/tests"] + +wake_test_component_ns = cg.esphome_ns.namespace("wake_test_component") +WakeTestComponent = wake_test_component_ns.class_("WakeTestComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(WakeTestComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp new file mode 100644 index 00000000000..b58f1c9adc8 --- /dev/null +++ b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.cpp @@ -0,0 +1,19 @@ +#include "wake_test_component.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include <chrono> +#include <thread> + +namespace esphome::wake_test_component { + +static const char *const TAG = "wake_test_component"; + +void WakeTestComponent::start_async_wake() { + ESP_LOGI(TAG, "Spawning async wake thread (50ms delay)"); + std::thread([] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + App.wake_loop_threadsafe(); + }).detach(); +} + +} // namespace esphome::wake_test_component diff --git a/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h new file mode 100644 index 00000000000..c8e4e0a89fe --- /dev/null +++ b/tests/integration/fixtures/external_components/wake_test_component/wake_test_component.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include <atomic> + +namespace esphome::wake_test_component { + +class WakeTestComponent : public Component { + public: + void setup() override {} + void loop() override { this->loop_count_.fetch_add(1, std::memory_order_relaxed); } + + int get_loop_count() const { return this->loop_count_.load(std::memory_order_relaxed); } + + // Spawn a detached thread that sleeps briefly then calls + // App.wake_loop_threadsafe(). Used by the integration test to verify a + // cross-thread wake forces a component-phase iteration even when + // loop_interval_ has been raised high enough to gate it off otherwise. + void start_async_wake(); + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + std::atomic<int> loop_count_{0}; +}; + +} // namespace esphome::wake_test_component diff --git a/tests/integration/fixtures/loop_interval_decoupling.yaml b/tests/integration/fixtures/loop_interval_decoupling.yaml new file mode 100644 index 00000000000..5aedd9aba5d --- /dev/null +++ b/tests/integration/fixtures/loop_interval_decoupling.yaml @@ -0,0 +1,60 @@ +esphome: + name: loop-interval-decouple + on_boot: + priority: -100 + then: + - lambda: |- + // Raise loop_interval_ to 500ms. With the decoupling fix the + // component phase should run ~twice per second while the 50ms + // scheduler interval below still fires at its requested cadence. + App.set_loop_interval(500); + # Start measurement after 1s so boot transients settle. + - delay: 1000ms + - lambda: |- + id(loop_at_start) = id(loop_counter)->get_loop_count(); + id(sched_at_start) = id(sched_count); + ESP_LOGI("test", "MEASUREMENT_STARTED loop=%d sched=%d", + id(loop_at_start), id(sched_at_start)); + # Observe for 2s. + - delay: 2000ms + - lambda: |- + int loop_delta = id(loop_counter)->get_loop_count() - id(loop_at_start); + int sched_delta = id(sched_count) - id(sched_at_start); + ESP_LOGI("test", "MEASUREMENT_DONE loop_delta=%d sched_delta=%d", + loop_delta, sched_delta); + +host: +api: +logger: + level: INFO + logs: + loop_test_component: WARN # Silence per-loop log spam + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +globals: + - id: sched_count + type: int + initial_value: "0" + - id: loop_at_start + type: int + initial_value: "0" + - id: sched_at_start + type: int + initial_value: "0" + +loop_test_component: + components: + - id: loop_counter + name: loop_counter + +interval: + # Fast scheduler interval — with the decoupling fix this should fire at + # its requested 50ms cadence regardless of loop_interval_. + - interval: 50ms + then: + - lambda: |- + id(sched_count) += 1; diff --git a/tests/integration/fixtures/loop_interval_default_not_pulled_forward.yaml b/tests/integration/fixtures/loop_interval_default_not_pulled_forward.yaml new file mode 100644 index 00000000000..fec83865b95 --- /dev/null +++ b/tests/integration/fixtures/loop_interval_default_not_pulled_forward.yaml @@ -0,0 +1,51 @@ +esphome: + name: loop-default-not-pulled + on_boot: + priority: -100 + then: + # Leave loop_interval_ at its default (16 ms → ~62 Hz). Do NOT call + # set_loop_interval here. The fast scheduler interval below used to + # pull the component phase forward to ~128 Hz via the old + # std::max(next_schedule, delay_time / 2) floor. + # Start measurement after 1s so boot transients settle. + - delay: 1000ms + - lambda: |- + id(loop_at_start) = id(loop_counter)->get_loop_count(); + ESP_LOGI("test", "MEASUREMENT_STARTED loop=%d", id(loop_at_start)); + # Observe for 2s. + - delay: 2000ms + - lambda: |- + int loop_delta = id(loop_counter)->get_loop_count() - id(loop_at_start); + ESP_LOGI("test", "MEASUREMENT_DONE loop_delta=%d", loop_delta); + +host: +api: +logger: + level: INFO + logs: + loop_test_component: WARN # Silence per-loop log spam + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +globals: + - id: loop_at_start + type: int + initial_value: "0" + +loop_test_component: + components: + - id: loop_counter + name: loop_counter + +interval: + # Fast scheduler interval (well under loop_interval_/2 = 8ms). In the + # pre-decoupling code this would have pulled the component phase forward + # to ~128 Hz. After the decoupling fix the component phase stays at + # ~62 Hz regardless. + - interval: 5ms + then: + - lambda: |- + // No-op; the presence of a due scheduler item is what matters. diff --git a/tests/integration/fixtures/wake_loop_forces_phase_b.yaml b/tests/integration/fixtures/wake_loop_forces_phase_b.yaml new file mode 100644 index 00000000000..d97ab8514f6 --- /dev/null +++ b/tests/integration/fixtures/wake_loop_forces_phase_b.yaml @@ -0,0 +1,52 @@ +esphome: + name: wake-loop-phase-b + on_boot: + priority: -100 + then: + - lambda: |- + // Raise loop_interval_ to 2000ms. Without the wake-request flag, + // a wake_loop_threadsafe() call would only run Phase A (scheduler) + // and leave the component phase gated for ~2s. + App.set_loop_interval(2000); + # Let boot transients settle. + - delay: 1000ms + - lambda: |- + // Snapshot the loop counter, then ask the component to spawn a + // background thread that calls App.wake_loop_threadsafe() after + // ~50ms. With the fix, that wake forces Phase B on the next tick + // and the counter increments well within the 500ms observation + // window below. + id(count_at_start) = id(wake_counter)->get_loop_count(); + id(start_time) = millis(); + id(wake_counter)->start_async_wake(); + ESP_LOGI("test", "WAKE_STARTED count=%d", id(count_at_start)); + # Observation window must be much shorter than loop_interval_ (2000ms) + # so a "false pass" isn't possible by simply waiting out the gate. + - delay: 500ms + - lambda: |- + int count_now = id(wake_counter)->get_loop_count(); + int delta = count_now - id(count_at_start); + uint32_t elapsed = millis() - id(start_time); + ESP_LOGI("test", "WAKE_RESULT delta=%d elapsed=%u", delta, elapsed); + +host: +api: +logger: + level: INFO + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [wake_test_component] + +globals: + - id: count_at_start + type: int + initial_value: "0" + - id: start_time + type: uint32_t + initial_value: "0" + +wake_test_component: + id: wake_counter diff --git a/tests/integration/test_loop_interval_decoupling.py b/tests/integration/test_loop_interval_decoupling.py new file mode 100644 index 00000000000..6c34aed4588 --- /dev/null +++ b/tests/integration/test_loop_interval_decoupling.py @@ -0,0 +1,75 @@ +"""Test that loop_interval_ no longer clamps scheduler cadence. + +Regression test for the decoupling of Application::loop() component-phase +cadence from scheduler wake timing. + +Setup: +- App.set_loop_interval(500) — raised for power-savings style cadence +- Scheduler interval at 50ms — should fire at 50ms regardless of loop_interval_ +- Component loop (LoopTestComponent) — should run at 500ms cadence + +Before the decoupling fix the old `std::max(next_schedule, delay_time / 2)` +floor clamped the sleep to ~250ms, so the 50ms scheduler only fired ~8 times +per 2s (vs the ~40 expected). After the fix the scheduler fires close to its +requested cadence while the component phase stays gated at loop_interval_. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_loop_interval_decoupling( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Raised loop_interval_ must not clamp scheduler item cadence.""" + loop = asyncio.get_running_loop() + measurement_done: asyncio.Future[tuple[int, int]] = loop.create_future() + + def on_log_line(line: str) -> None: + match = re.search(r"MEASUREMENT_DONE loop_delta=(\d+) sched_delta=(\d+)", line) + if match and not measurement_done.done(): + measurement_done.set_result((int(match.group(1)), int(match.group(2)))) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "loop-interval-decouple" + + try: + loop_delta, sched_delta = await asyncio.wait_for( + measurement_done, timeout=10.0 + ) + except TimeoutError: + pytest.fail("MEASUREMENT_DONE marker never appeared") + + # Observation window = 2s, loop_interval_ = 500ms. + # Component phase should fire ~4 times in 2s. The upper bound must be + # less than 8: the pre-decoupling behavior clamped to ~250ms cadence + # giving ~8 loops/2s, so allowing 8 would let the old behavior pass. + # Lower bound 3 (not 2) keeps the test honest: a >30% slowdown from + # the ~4 nominal is not normal CI jitter and should fail. + assert 3 <= loop_delta <= 6, ( + f"Component loop should fire ~4 times in 2s at loop_interval=500ms, " + f"got {loop_delta}" + ) + + # Scheduler interval = 50ms → ~40 fires in 2s. Before the decoupling + # fix this clamped to ~8 fires. Assert >= 20 to catch the old clamped + # behavior with comfortable jitter headroom for slow CI hosts. + assert sched_delta >= 20, ( + f"50ms scheduler interval should fire ~40 times in 2s but only " + f"fired {sched_delta}. This indicates loop_interval_ is still " + f"clamping scheduler cadence." + ) diff --git a/tests/integration/test_loop_interval_default_not_pulled_forward.py b/tests/integration/test_loop_interval_default_not_pulled_forward.py new file mode 100644 index 00000000000..17a7070436e --- /dev/null +++ b/tests/integration/test_loop_interval_default_not_pulled_forward.py @@ -0,0 +1,67 @@ +"""Test that a fast scheduler item does not pull the component phase forward. + +Regression test for the original ~128 Hz → ~62 Hz bug fixed by decoupling +Application::loop() component-phase cadence from scheduler wake timing. + +Setup: +- loop_interval_ left at its default (16 ms → ~62 Hz component phase). +- Scheduler interval at 5 ms (well under the old loop_interval_/2 = 8 ms floor). + +Before the decoupling fix the ``std::max(next_schedule, delay_time / 2)`` floor +clamped the sleep to ~8 ms whenever any scheduler item was due sooner than +loop_interval_/2. That pulled the component phase forward to ~128 Hz — twice +what the documented ~62 Hz default promised. After the fix the component +phase stays at ~62 Hz regardless of scheduler activity. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_loop_interval_default_not_pulled_forward( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Fast scheduler item must not pull component phase past default ~62 Hz.""" + loop = asyncio.get_running_loop() + measurement_done: asyncio.Future[int] = loop.create_future() + + def on_log_line(line: str) -> None: + match = re.search(r"MEASUREMENT_DONE loop_delta=(\d+)", line) + if match and not measurement_done.done(): + measurement_done.set_result(int(match.group(1))) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "loop-default-not-pulled" + + try: + loop_delta = await asyncio.wait_for(measurement_done, timeout=10.0) + except TimeoutError: + pytest.fail("MEASUREMENT_DONE marker never appeared") + + # Observation window = 2s, loop_interval_ default = 16ms → ~62 Hz → + # ~125 component-phase iterations expected. + # Pre-fix behavior: the 5 ms scheduler interval tripped the old + # delay_time/2 = 8 ms floor, pulling the phase to ~128 Hz → ~256. + # Upper bound 180 is comfortably below the ~256 pre-fix rate but + # above the ~125 nominal with CI jitter. + # Lower bound 80 covers very slow CI hosts without permitting a + # complete regression. + assert 80 <= loop_delta <= 180, ( + f"Component loop at default loop_interval_ should fire ~125 times " + f"in 2s (≈62 Hz × 2s); got {loop_delta}. Values >200 indicate the " + f"scheduler is again pulling the component phase forward." + ) diff --git a/tests/integration/test_wake_loop_forces_phase_b.py b/tests/integration/test_wake_loop_forces_phase_b.py new file mode 100644 index 00000000000..5f05f07dd82 --- /dev/null +++ b/tests/integration/test_wake_loop_forces_phase_b.py @@ -0,0 +1,76 @@ +"""Test that wake_loop_threadsafe() forces a component-phase iteration. + +Regression test for the wake-request flag added to Application::loop()'s +Phase A / Phase B gate. Background producers (MQTT RX, USB RX, BLE event, +etc.) call App.wake_loop_threadsafe() expecting their component's loop() +to drain queued work; if the component phase stays gated by loop_interval_, +the work waits up to loop_interval_ ms instead of running on the next tick. + +Setup: +- App.set_loop_interval(2000) — a wide gate that would clearly mask the bug. +- A test component spawns a detached std::thread that sleeps 50 ms and then + calls App.wake_loop_threadsafe() from a non-main thread. +- The on_boot block snapshots the component's loop counter before/after a + 500 ms observation window. + +Without the fix, delta=0 (the gate holds Phase B for ~2 s). +With the fix, delta>=1 (the wake forces Phase B within one tick of the wake). +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wake_loop_forces_phase_b( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """A wake_loop_threadsafe() call from a background thread must trigger the + component phase within the next tick, even when loop_interval_ is raised + well above the observation window.""" + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + result: asyncio.Future[tuple[int, int]] = loop.create_future() + + def on_log_line(line: str) -> None: + match = re.search(r"WAKE_RESULT delta=(\d+) elapsed=(\d+)", line) + if match and not result.done(): + result.set_result((int(match.group(1)), int(match.group(2)))) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "wake-loop-phase-b" + + try: + delta, elapsed = await asyncio.wait_for(result, timeout=15.0) + except TimeoutError: + pytest.fail("WAKE_RESULT marker never appeared") + + # Without the fix, delta would be 0 — loop_interval_=2000ms held + # Phase B off for the full 500ms observation window. With the fix + # the wake from the background thread (~50ms after start) forces + # Phase B on the next tick, so the counter increments at least once. + assert delta >= 1, ( + f"wake_loop_threadsafe() from a background thread should force " + f"Phase B within the next tick; observed delta={delta} after " + f"{elapsed}ms with loop_interval_=2000ms" + ) From 7bd36e0c8dc6c2b968e858087e77bacb376b84bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 14:51:45 +0200 Subject: [PATCH 1565/2056] [debug] Migrate trivial buf_append_printf sites to buf_append_str (#15885) --- esphome/components/debug/debug_component.cpp | 2 +- esphome/components/debug/debug_esp32.cpp | 23 +++++++++++------ esphome/components/debug/debug_libretiny.cpp | 9 ++++--- esphome/components/debug/debug_zephyr.cpp | 26 ++++++++++++++------ 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 15f68c3a3b1..d97a4aa135a 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -30,7 +30,7 @@ void DebugComponent::dump_config() { char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); - size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION); + size_t pos = buf_append_str(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, ESPHOME_VERSION); this->free_heap_ = get_free_heap_(); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 2e04090749e..ea0c635207f 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -224,17 +224,21 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> const char *model = ESPHOME_VARIANT; // Build features string - pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model); + pos = buf_append_str(buf, size, pos, "|Chip: "); + pos = buf_append_str(buf, size, pos, model); + pos = buf_append_str(buf, size, pos, " Features:"); bool first_feature = true; for (const auto &feature : CHIP_FEATURES) { if (info.features & feature.bit) { - pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name); + pos = buf_append_str(buf, size, pos, first_feature ? "" : ", "); + pos = buf_append_str(buf, size, pos, feature.name); first_feature = false; info.features &= ~feature.bit; } } if (info.features != 0) { - pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); + pos = buf_append_str(buf, size, pos, first_feature ? "" : ", "); + pos = buf_append_printf(buf, size, pos, "Other:0x%" PRIx32, info.features); } pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); @@ -267,17 +271,20 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> // Framework detection #ifdef USE_ARDUINO ESP_LOGD(TAG, " Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); + pos = buf_append_str(buf, size, pos, "|Framework: Arduino"); #else ESP_LOGD(TAG, " Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); + pos = buf_append_str(buf, size, pos, "|Framework: ESP-IDF"); #endif - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_str(buf, size, pos, "|ESP-IDF: "); + pos = buf_append_str(buf, size, pos, esp_get_idf_version()); pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); + pos = buf_append_str(buf, size, pos, "|Reset: "); + pos = buf_append_str(buf, size, pos, reset_reason); + pos = buf_append_str(buf, size, pos, "|Wakeup: "); + pos = buf_append_str(buf, size, pos, wakeup_cause); return pos; } diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 1d458c602a6..6f36debb95a 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -38,9 +38,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); - pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10); - pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason); - pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name()); + pos = buf_append_str(buf, size, pos, "|Version: "); + pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10); + pos = buf_append_str(buf, size, pos, "|Reset Reason: "); + pos = buf_append_str(buf, size, pos, reset_reason); + pos = buf_append_str(buf, size, pos, "|Chip Name: "); + pos = buf_append_str(buf, size, pos, lt_cpu_get_model_name()); pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib); pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index d1580dae80d..49790b5b9a0 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -162,14 +162,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> const char *supply_status = (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; ESP_LOGD(TAG, "Main supply status: %s", supply_status); - pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status); + pos = buf_append_str(buf, size, pos, "|Main supply status: "); + pos = buf_append_str(buf, size, pos, supply_status); // Regulator stage 0 if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO"; const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos); ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage); - pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage); + pos = buf_append_str(buf, size, pos, "|Regulator stage 0: "); + pos = buf_append_str(buf, size, pos, reg0_type); + pos = buf_append_str(buf, size, pos, ", "); + pos = buf_append_str(buf, size, pos, reg0_voltage); #ifdef USE_NRF52_REG0_VOUT if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) { ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT)); @@ -177,13 +181,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> #endif } else { ESP_LOGD(TAG, "Regulator stage 0: disabled"); - pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled"); + pos = buf_append_str(buf, size, pos, "|Regulator stage 0: disabled"); } // Regulator stage 1 const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO"; ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type); - pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type); + pos = buf_append_str(buf, size, pos, "|Regulator stage 1: "); + pos = buf_append_str(buf, size, pos, reg1_type); // USB power state const char *usb_state; @@ -197,7 +202,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> usb_state = "disconnected"; } ESP_LOGD(TAG, "USB power state: %s", usb_state); - pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state); + pos = buf_append_str(buf, size, pos, "|USB power state: "); + pos = buf_append_str(buf, size, pos, usb_state); // Power-fail comparator bool enabled; @@ -302,14 +308,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> break; } ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); - pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); + pos = buf_append_str(buf, size, pos, "|Power-fail comparator: "); + pos = buf_append_str(buf, size, pos, pof_voltage); + pos = buf_append_str(buf, size, pos, ", VDDH: "); + pos = buf_append_str(buf, size, pos, vddh_voltage); } else { ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage); - pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage); + pos = buf_append_str(buf, size, pos, "|Power-fail comparator: "); + pos = buf_append_str(buf, size, pos, pof_voltage); } } else { ESP_LOGD(TAG, "Power-fail comparator: disabled"); - pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled"); + pos = buf_append_str(buf, size, pos, "|Power-fail comparator: disabled"); } auto package = [](uint32_t value) { From 3a6f3dfb9468d1de3be68616a5bef95feba215dc Mon Sep 17 00:00:00 2001 From: Egor Vorontsov <sdoregor@sdore.me> Date: Tue, 21 Apr 2026 16:03:07 +0300 Subject: [PATCH 1566/2056] [lock] Implemented open states support (#15120) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/lock/__init__.py | 2 ++ esphome/components/lock/lock.cpp | 11 ++++++++--- esphome/components/lock/lock.h | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 1a45896ac1f..a36d52a5d82 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -35,9 +35,11 @@ LockStateForwarder = lock_ns.class_("LockStateForwarder") LockState = lock_ns.enum("LockState") LOCK_STATES = { + "OPEN": LockState.LOCK_STATE_OPEN, "LOCKED": LockState.LOCK_STATE_LOCKED, "UNLOCKED": LockState.LOCK_STATE_UNLOCKED, "JAMMED": LockState.LOCK_STATE_JAMMED, + "OPENING": LockState.LOCK_STATE_OPENING, "LOCKING": LockState.LOCK_STATE_LOCKING, "UNLOCKING": LockState.LOCK_STATE_UNLOCKING, } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 3ff131af3d5..66eb692bd5c 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -8,9 +8,10 @@ namespace esphome::lock { static const char *const TAG = "lock"; -// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING +// Lock state strings indexed by LockState enum. // Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range -PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING"); +PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING", "OPENING", + "OPEN"); const LogString *lock_state_to_string(LockState state) { return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0); @@ -74,12 +75,16 @@ LockCall &LockCall::set_state(optional<LockState> state) { return *this; } LockCall &LockCall::set_state(const char *state) { - if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPEN")) == 0) { + this->set_state(LOCK_STATE_OPEN); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) { this->set_state(LOCK_STATE_LOCKED); } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) { this->set_state(LOCK_STATE_UNLOCKED); } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) { this->set_state(LOCK_STATE_JAMMED); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPENING")) == 0) { + this->set_state(LOCK_STATE_OPENING); } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) { this->set_state(LOCK_STATE_LOCKING); } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 543a4b51a89..86a9cdd3fbb 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -26,7 +26,9 @@ enum LockState : uint8_t { LOCK_STATE_UNLOCKED = 2, LOCK_STATE_JAMMED = 3, LOCK_STATE_LOCKING = 4, - LOCK_STATE_UNLOCKING = 5 + LOCK_STATE_UNLOCKING = 5, + LOCK_STATE_OPENING = 6, + LOCK_STATE_OPEN = 7, }; const LogString *lock_state_to_string(LockState state); From 14defb69b64f3fb365a4b9a68ef5e791963c5add Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 21 Apr 2026 15:04:13 +0200 Subject: [PATCH 1567/2056] [template] Use placement new for template text restore saver (#15883) --- esphome/components/template/text/__init__.py | 13 +++++- tests/component_tests/template/__init__.py | 0 .../config/template_text_restore.yaml | 14 ++++++ .../template/test_template_text.py | 44 +++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/template/__init__.py create mode 100644 tests/component_tests/template/config/template_text_restore.yaml create mode 100644 tests/component_tests/template/test_template_text.py diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py index 572b5ba0f4a..1266370cb21 100644 --- a/esphome/components/template/text/__init__.py +++ b/esphome/components/template/text/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import text import esphome.config_validation as cv from esphome.const import ( + CONF_ID, CONF_INITIAL_VALUE, CONF_LAMBDA, CONF_MAX_LENGTH, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESTORE_VALUE, CONF_SET_ACTION, ) +from esphome.core import ID from .. import template_ns @@ -84,8 +86,15 @@ async def to_code(config): if initial_value_config := config.get(CONF_INITIAL_VALUE): cg.add(var.set_initial_value(initial_value_config)) if config[CONF_RESTORE_VALUE]: - args = cg.TemplateArguments(config[CONF_MAX_LENGTH]) - saver = TextSaverTemplate.template(args).new() + saver_id = ID( + f"{config[CONF_ID].id}_value_saver", + is_declaration=True, + type=TextSaverBase, + ) + saver_type = TextSaverTemplate.template( + cg.TemplateArguments(config[CONF_MAX_LENGTH]) + ) + saver = cg.Pvariable(saver_id, saver_type.new()) cg.add(var.set_value_saver(saver)) if CONF_SET_ACTION in config: diff --git a/tests/component_tests/template/__init__.py b/tests/component_tests/template/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/template/config/template_text_restore.yaml b/tests/component_tests/template/config/template_text_restore.yaml new file mode 100644 index 00000000000..4574470eab0 --- /dev/null +++ b/tests/component_tests/template/config/template_text_restore.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +host: + +text: + - platform: template + name: "Test Text Restore" + id: test_text_restore + optimistic: true + max_length: 10 + mode: text + initial_value: "hello" + restore_value: true diff --git a/tests/component_tests/template/test_template_text.py b/tests/component_tests/template/test_template_text.py new file mode 100644 index 00000000000..2ce9a88d671 --- /dev/null +++ b/tests/component_tests/template/test_template_text.py @@ -0,0 +1,44 @@ +"""Tests for the template text component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_template_text_saver_uses_placement_new_with_templated_subclass( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Regression test for template text restore saver using placement new. + + When ``restore_value: true``, the saver is its own Pvariable with + placement new: storage is sized for ``TextSaver<MAX_LENGTH>``, the + declared pointer stays at ``TemplateTextSaverBase *`` for polymorphism, + and the templated subclass constructor runs. A regression would either + reintroduce the heap ``new TextSaver<...>()`` expression or size the + storage for the base class and silently skip the subclass ctor. + """ + main_cpp = generate_main(component_config_path("template_text_restore.yaml")) + + # Storage is sized and aligned for the templated subclass. + assert "sizeof(template_::TextSaver<10>)" in main_cpp + assert "alignas(template_::TextSaver<10>)" in main_cpp + # Pointer declared as base type for polymorphism. + assert ( + "static template_::TemplateTextSaverBase *const test_text_restore_value_saver" + in main_cpp + ) + # Placement new runs the templated subclass constructor. + assert "new(test_text_restore_value_saver) template_::TextSaver<10>()" in main_cpp + # Base-class default ctor must NOT be used. + assert ( + "new(test_text_restore_value_saver) template_::TemplateTextSaverBase()" + not in main_cpp + ) + # No heap `new TextSaver<...>()` left over — the pre-fix pattern. + assert "new template_::TextSaver<" not in main_cpp + # Saver is wired into the text component. + assert ( + "test_text_restore->set_value_saver(test_text_restore_value_saver)" in main_cpp + ) From 0c9d443a5c041910f4df5cbba3a136d151c92c8e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:18:46 +0200 Subject: [PATCH 1568/2056] [esp32_ble] Add `use_psram` option to offload BT memory allocation to SPIRAM (#15644) --- esphome/components/esp32_ble/__init__.py | 20 +++++++++++++++++++ esphome/components/esp32_ble/ble.cpp | 3 +++ esphome/core/defines.h | 1 + .../esp32_ble/common_use_psram.yaml | 4 ++++ .../components/esp32_ble/test.esp32-ard.yaml | 1 + .../components/esp32_ble/test.esp32-idf.yaml | 1 + .../esp32_ble/test.esp32-p4-idf.yaml | 1 + 7 files changed, 31 insertions(+) create mode 100644 tests/components/esp32_ble/common_use_psram.yaml diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 79d05049bfb..c7b6b40394c 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,6 +7,7 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv @@ -342,6 +343,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All( cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS) ), + cv.Optional(CONF_USE_PSRAM): cv.All( + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -598,6 +602,22 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) + # When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for + # heap allocations and use dynamic (heap-based) environment memory tables + # instead of large static DRAM arrays. This frees ~40 kB of internal RAM. + # Reference: Espressif ADF Design Considerations + # https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/ + # design-guide/design-considerations.html + if config.get(CONF_USE_PSRAM, False): + cg.add_define("USE_ESP32_BLE_PSRAM") + # CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is only available on ESP32 + # (BTDM dual-mode controller). BLE-only SoCs (C3, S3, C2, H2) do not + # expose this Kconfig symbol; applying it there would cause a build error. + if get_esp32_variant() == const.VARIANT_ESP32: + add_idf_sdkconfig_option("CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST", True) + # CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY applies to all Bluedroid-enabled variants. + add_idf_sdkconfig_option("CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY", True) + # Register the core BLE loggers that are always needed register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 0280439731c..ebb44c7d91f 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -667,6 +667,9 @@ void ESP32BLE::dump_config() { " MAC address: %s\n" " IO Capability: %s", mac_s, io_capability_s); +#ifdef USE_ESP32_BLE_PSRAM + ESP_LOGCONFIG(TAG, " PSRAM BLE allocation: enabled"); +#endif #ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS const char *auth_req_mode_s = "<default>"; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0fb72215713..07cac97e171 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -48,6 +48,7 @@ #define USE_ENTITY_DEVICE_CLASS #define USE_ENTITY_ICON #define USE_ENTITY_UNIT_OF_MEASUREMENT +#define USE_ESP32_BLE_PSRAM #define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_IMPROV_STATE_CALLBACK diff --git a/tests/components/esp32_ble/common_use_psram.yaml b/tests/components/esp32_ble/common_use_psram.yaml new file mode 100644 index 00000000000..cce6cf547fd --- /dev/null +++ b/tests/components/esp32_ble/common_use_psram.yaml @@ -0,0 +1,4 @@ +esp32_ble: + use_psram: true + +psram: diff --git a/tests/components/esp32_ble/test.esp32-ard.yaml b/tests/components/esp32_ble/test.esp32-ard.yaml index dade44d145b..fa7b9befc73 100644 --- a/tests/components/esp32_ble/test.esp32-ard.yaml +++ b/tests/components/esp32_ble/test.esp32-ard.yaml @@ -1 +1,2 @@ <<: !include common.yaml +<<: !include common_use_psram.yaml diff --git a/tests/components/esp32_ble/test.esp32-idf.yaml b/tests/components/esp32_ble/test.esp32-idf.yaml index f8defaf28ff..0b2a920c60b 100644 --- a/tests/components/esp32_ble/test.esp32-idf.yaml +++ b/tests/components/esp32_ble/test.esp32-idf.yaml @@ -1,4 +1,5 @@ <<: !include common.yaml +<<: !include common_use_psram.yaml esp32_ble: io_capability: keyboard_only diff --git a/tests/components/esp32_ble/test.esp32-p4-idf.yaml b/tests/components/esp32_ble/test.esp32-p4-idf.yaml index 4eeb7c2f18b..170220bf488 100644 --- a/tests/components/esp32_ble/test.esp32-p4-idf.yaml +++ b/tests/components/esp32_ble/test.esp32-p4-idf.yaml @@ -2,6 +2,7 @@ packages: ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml <<: !include common.yaml +<<: !include common_use_psram.yaml esp32_ble: io_capability: keyboard_only From 43c6b839cd70e3c2430964ec441c3815ac203d91 Mon Sep 17 00:00:00 2001 From: Geoff <geoff@gapple.ca> Date: Tue, 21 Apr 2026 07:00:03 -0700 Subject: [PATCH 1569/2056] [sensor] Filter to round to significant digits (#11157) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/sensor/__init__.py | 14 ++++++ esphome/components/sensor/filter.h | 13 +++++ esphome/core/helpers.cpp | 17 +++++++ esphome/core/helpers.h | 5 ++ tests/components/core/helpers_test.cpp | 58 ++++++++++++++++++++++ tests/components/template/common-base.yaml | 1 + 6 files changed, 108 insertions(+) create mode 100644 tests/components/core/helpers_test.cpp diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 8dcb7165e30..43fbc989531 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -118,6 +118,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry CODEOWNERS = ["@esphome/core"] + DEVICE_CLASSES = [ DEVICE_CLASS_ABSOLUTE_HUMIDITY, DEVICE_CLASS_APPARENT_POWER, @@ -293,6 +294,7 @@ SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) +RoundSignificantDigitsFilter = sensor_ns.class_("RoundSignificantDigitsFilter", Filter) validate_unit_of_measurement = cv.All( cv.string_strict, @@ -900,6 +902,18 @@ async def round_multiple_filter_to_code(config, filter_id): ) +@FILTER_REGISTRY.register( + "round_to_significant_digits", + RoundSignificantDigitsFilter, + cv.int_range(min=1, max=6), +) +async def round_significant_digits_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + cg.TemplateArguments(config), + ) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index a91d66a8fbe..917a1ce7d5a 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -604,6 +604,19 @@ class RoundMultipleFilter : public Filter { float multiple_; }; +template<uint8_t Digits> class RoundSignificantDigitsFilter : public Filter { + public: + optional<float> new_value(float value) override { + if (std::isfinite(value)) { + if (value == 0.0f) + return 0.0f; + float factor = pow10_int(Digits - 1 - ilog10(value)); + return roundf(value * factor) / factor; + } + return value; + } +}; + class ToNTCResistanceFilter : public Filter { public: ToNTCResistanceFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {} diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1d0efd01ce5..113b6f61872 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -413,6 +413,23 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } +int8_t ilog10(float value) { + float abs_val = fabsf(value); + int8_t exp = 0; + if (abs_val >= 10.0f) { + while (abs_val >= 10.0f) { + abs_val /= 10.0f; + exp++; + } + } else if (abs_val < 1.0f) { + while (abs_val < 1.0f) { + abs_val *= 10.0f; + exp--; + } + } + return exp; +} + static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { float divisor; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6b71916cd23..939852bfcb5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -740,6 +740,11 @@ template<size_t STACK_SIZE, typename T = uint8_t> class SmallBufferWithHeapFallb /// @name Mathematics ///@{ +/// Compute floor(log10(fabs(value))) using iterative comparison. +/// Avoids pulling in __ieee754_logf/log10f (~1KB flash). +/// Only valid for finite, non-zero values. +int8_t ilog10(float value); + /// Compute 10^exp using iterative multiplication/division. /// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT /// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT diff --git a/tests/components/core/helpers_test.cpp b/tests/components/core/helpers_test.cpp new file mode 100644 index 00000000000..468185787f5 --- /dev/null +++ b/tests/components/core/helpers_test.cpp @@ -0,0 +1,58 @@ +#include <gtest/gtest.h> +#include <cmath> +#include "esphome/core/helpers.h" + +namespace esphome { + +TEST(HelpersTest, Ilog10PowersOfTen) { + EXPECT_EQ(ilog10(1.0f), 0); + EXPECT_EQ(ilog10(10.0f), 1); + EXPECT_EQ(ilog10(100.0f), 2); + EXPECT_EQ(ilog10(1000.0f), 3); + EXPECT_EQ(ilog10(10000.0f), 4); + EXPECT_EQ(ilog10(100000.0f), 5); + EXPECT_EQ(ilog10(0.1f), -1); + EXPECT_EQ(ilog10(0.001f), -3); +} + +TEST(HelpersTest, Ilog10General) { + EXPECT_EQ(ilog10(5.0f), 0); + EXPECT_EQ(ilog10(9.99f), 0); + EXPECT_EQ(ilog10(50.0f), 1); + EXPECT_EQ(ilog10(99.0f), 1); + EXPECT_EQ(ilog10(999.0f), 2); + EXPECT_EQ(ilog10(0.5f), -1); + EXPECT_EQ(ilog10(0.0072f), -3); + EXPECT_EQ(ilog10(120000.0f), 5); + EXPECT_EQ(ilog10(123456.789f), 5); +} + +TEST(HelpersTest, Ilog10Negative) { + EXPECT_EQ(ilog10(-1.0f), 0); + EXPECT_EQ(ilog10(-10.0f), 1); + EXPECT_EQ(ilog10(-0.1f), -1); + EXPECT_EQ(ilog10(-123.456f), 2); +} + +// Verify that ilog10 + pow10_int produces the same rounding result as log10/pow. +// ilog10 may differ from floor(log10f()) for values not exactly representable in float +// (e.g. 0.01f is 0.00999...), but the full round-trip must match. +TEST(HelpersTest, Ilog10RoundTripMatchesLog10) { + float values[] = {0.0072f, 0.05f, 0.1f, 0.5f, 1.0f, 3.14f, 9.99f, 10.0f, 42.0f, 100.0f, + 1234.5f, 9999.0f, 10000.0f, 99999.0f, 120000.0f, 999999.0f, -1.0f, -0.1f, -123.456f, -10000.0f}; + for (uint8_t digits = 1; digits <= 6; digits++) { + for (float v : values) { + // New implementation using ilog10 + pow10_int + float factor_new = pow10_int(digits - 1 - ilog10(v)); + float result_new = roundf(v * factor_new) / factor_new; + + // Reference using log10/pow + double factor_ref = pow(10.0, digits - std::ceil(std::log10(std::fabs(v)))); + float result_ref = static_cast<float>(round(v * factor_ref) / factor_ref); + + EXPECT_FLOAT_EQ(result_new, result_ref) << "mismatch for value=" << v << " digits=" << (int) digits; + } + } +} + +} // namespace esphome diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index ed398b0abd9..ecc65de66cc 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -171,6 +171,7 @@ sensor: quantile: .9 - round: 1 - round_to_multiple_of: 0.25 + - round_to_significant_digits: 3 - skip_initial: 3 - sliding_window_moving_average: window_size: 15 From 7560112144bf9e586d6ab54854294442ab7b6352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:08:41 +0200 Subject: [PATCH 1570/2056] Bump aioesphomeapi from 44.16.1 to 44.17.0 (#15906) 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 1623876cb56..95d7c8c0328 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.16.1 +aioesphomeapi==44.17.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From ee91ad8f068391a213cd1aa04b13aeed7e9a5dee Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 21 Apr 2026 18:25:05 -0500 Subject: [PATCH 1571/2056] [esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 (#15882) --- esphome/components/esp32/__init__.py | 95 +++++++++++--- esphome/components/esp32/post_build.py.script | 123 +++++++++++++++++- .../esp32/dummy_signing_key_v1_ecdsa.pem | 7 + .../esp32/test-signed_ota_v1.esp32-idf.yaml | 10 ++ 4 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 tests/components/esp32/dummy_signing_key_v1_ecdsa.pem create mode 100644 tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a68614cb436..77b405a4497 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -128,23 +128,30 @@ ASSERTION_LEVELS = { SIGNING_SCHEMES = { "rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME", "ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME", + "ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME", } -# Chip variants that only support one signing scheme for Secure Boot V2. +# Chip variants that only support one V2 signing scheme. # Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h. -# Variants not listed in either set support both RSA and ECDSA +# Variants not listed in either set support both RSA and ECDSA V2 # (e.g. C5, C6, H2, P4). New variants should be added to the # appropriate set if they only support one scheme. -SIGNED_OTA_RSA_ONLY_VARIANTS = { - VARIANT_ESP32, +# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only +# when minimum_chip_revision >= 3.0, which requires special handling. +SIGNED_OTA_V2_RSA_ONLY_VARIANTS = { VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32C3, } -SIGNED_OTA_ECC_ONLY_VARIANTS = { +SIGNED_OTA_V2_ECC_ONLY_VARIANTS = { VARIANT_ESP32C2, VARIANT_ESP32C61, } +# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32. +# Based on SOC_SECURE_BOOT_V1 in soc_caps.h. +SIGNED_OTA_V1_ECDSA_VARIANTS = { + VARIANT_ESP32, +} COMPILER_OPTIMIZATIONS = { "DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG", @@ -991,25 +998,73 @@ def final_validate(config): if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): scheme = signed_ota[CONF_SIGNING_SCHEME] variant = config[CONF_VARIANT] - scheme_variant_conflicts = { - "ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"), - "rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"), - } - if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[ - 0 - ]: + min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION) + scheme_path = [ + CONF_FRAMEWORK, + CONF_ADVANCED, + CONF_SIGNED_OTA_VERIFICATION, + CONF_SIGNING_SCHEME, + ] + + # V1 ECDSA is only available on the original ESP32 + if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS: errs.append( cv.Invalid( - f"Signing scheme '{scheme}' is not supported on " - f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", - path=[ - CONF_FRAMEWORK, - CONF_ADVANCED, - CONF_SIGNED_OTA_VERIFICATION, - CONF_SIGNING_SCHEME, - ], + f"Signing scheme 'ecdsa_v1' is only supported on " + f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. " + f"Use 'rsa3072' or 'ecdsa256' instead.", + path=scheme_path, ) ) + elif variant == VARIANT_ESP32: + # On ESP32, V2 RSA requires minimum_chip_revision >= 3.0 + # Note: string comparison works here because cv.one_of constrains + # min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1"). + if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"): + errs.append( + cv.Invalid( + f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} " + f"requires minimum_chip_revision: '3.0' or higher " + f"(Secure Boot V2 RSA needs chip revision 3.0+). " + f"For older chip revisions, use 'ecdsa_v1' instead.", + path=scheme_path, + ) + ) + # ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC) + elif scheme == "ecdsa256": + errs.append( + cv.Invalid( + f"Signing scheme 'ecdsa256' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with " + f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.", + path=scheme_path, + ) + ) + # V1 on rev 3.0+ -- suggest V2 RSA for stronger security + elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0": + _LOGGER.info( + "Using Secure Boot V1 ECDSA on %s rev %s. " + "Consider using 'rsa3072' (Secure Boot V2 RSA) for " + "stronger security on chip revision 3.0+.", + VARIANT_FRIENDLY[variant], + min_rev, + ) + else: + # Non-ESP32 variants: check V2 scheme-variant compatibility + scheme_variant_conflicts = { + "ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"), + "rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"), + } + if ( + conflict := scheme_variant_conflicts.get(scheme) + ) and variant in conflict[0]: + errs.append( + cv.Invalid( + f"Signing scheme '{scheme}' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", + path=scheme_path, + ) + ) if CONF_OTA not in full_config: _LOGGER.warning( "Signed OTA verification is enabled but no OTA component is configured. " diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 8d13214259a..b329f6b82b9 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -5,6 +5,7 @@ import json # noqa: E402 import os # noqa: E402 import pathlib # noqa: E402 import shutil # noqa: E402 +import subprocess # noqa: E402 from glob import glob # noqa: E402 @@ -25,6 +26,114 @@ def _parse_sdkconfig(sdkconfig_path): return options +def _generate_v1_verification_key(env): + """Generate the V1 ECDSA verification key binary and assembly source file. + + Secure Boot V1 embeds the public verification key directly in the app binary + as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates + these files via custom commands, but PlatformIO's SCons bridge does not execute + them. This function replicates that logic: + 1. Extracts the raw public key from the PEM signing key using espsecure. + 2. Generates the .S assembly source that embeds the key bytes. + """ + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + project_dir = pathlib.Path(env.subst("$PROJECT_DIR")) + pioenv = env.subst("$PIOENV") + sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}") + + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y": + return + + bin_path = build_dir / "signature_verification_key.bin" + asm_path = build_dir / "signature_verification_key.bin.S" + + # Determine the source of the verification key + if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y": + # Extract public key from the signing key + signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY") + if not signing_key: + return + signing_key_path = pathlib.Path(signing_key) + if not signing_key_path.exists(): + print(f"Error: V1 ECDSA signing key not found: {signing_key_path}") + env.Exit(1) + return + + if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime: + python_exe = env.subst("$PYTHONEXE") + result = subprocess.run( + [python_exe, "-m", "espsecure", "extract_public_key", + "--keyfile", str(signing_key_path), str(bin_path)], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f"Error extracting V1 verification key: {result.stderr}") + env.Exit(1) + return + print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}") + else: + # User-provided verification key -- should already be a raw binary file + verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY") + if not verification_key: + return + verification_key_path = pathlib.Path(verification_key) + if not verification_key_path.exists(): + print(f"Error: Verification key not found: {verification_key_path}") + env.Exit(1) + return + shutil.copyfile(str(verification_key_path), str(bin_path)) + + if not bin_path.exists(): + return + + # Generate the .S assembly file from the binary key data. + # Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin. + # The file is needed in both the app build dir and the bootloader build dir, since + # the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT + # is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that + # normally generate these files. + data = bin_path.read_bytes() + varname = "signature_verification_key_bin" + + lines = [] + lines.append(f"/* Data converted from {bin_path.name} */") + lines.append(".data") + lines.append("#if !defined (__APPLE__) && !defined (__linux__)") + lines.append(".section .rodata.embedded") + lines.append("#endif") + lines.append(f"\n.global {varname}") + lines.append(f"{varname}:") + lines.append(f"\n.global _binary_{varname}_start") + lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */") + + # Format binary data as .byte lines (16 bytes per line) + for i in range(0, len(data), 16): + chunk = data[i:i + 16] + hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk) + lines.append(f".byte {hex_bytes}") + + lines.append(f"\n.global _binary_{varname}_end") + lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */") + lines.append(f"\n.global {varname}_length") + lines.append(f"{varname}_length:") + lines.append(f".long {len(data)}") + lines.append("") + lines.append('#if defined (__linux__)') + lines.append('.section .note.GNU-stack,"",@progbits') + lines.append("#endif") + + asm_content = "\n".join(lines) + "\n" + + # Write to app build dir and bootloader build dir + asm_path.write_text(asm_content) + bootloader_dir = build_dir / "bootloader" + if bootloader_dir.is_dir(): + bootloader_bin = bootloader_dir / "signature_verification_key.bin" + bootloader_asm = bootloader_dir / "signature_verification_key.bin.S" + shutil.copyfile(str(bin_path), str(bootloader_bin)) + bootloader_asm.write_text(asm_content) + + def sign_firmware(source, target, env): """ Sign the firmware binary using espsecure.py if signed OTA verification is enabled. @@ -55,9 +164,12 @@ def sign_firmware(source, target, env): env.Exit(1) return - # ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes), - # so the espsecure signature version is always 2. - sign_version = "2" + # Determine espsecure signature version from the signing scheme: + # V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2. + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y": + sign_version = "1" + else: + sign_version = "2" firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" firmware_path = build_dir / firmware_name @@ -217,6 +329,11 @@ def esp32_copy_ota_bin(source, target, env): print(f"Copied firmware to {new_file_name}") +# Generate V1 ECDSA verification key files before build starts. +# Workaround for PlatformIO not executing CMake custom commands that extract +# the public key and generate the .S assembly file for Secure Boot V1. +_generate_v1_verification_key(env) # noqa: F821 + # Run signing first, then merge, then ota copy env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 diff --git a/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem b/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem new file mode 100644 index 00000000000..bd092056069 --- /dev/null +++ b/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem @@ -0,0 +1,7 @@ +*** DO NOT USE THIS KEY...EVER *** +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZIp96p7Z7QN6vxOFE5FdRNm535vW81Ax07KnGxVjiMoAoGCCqGSM49 +AwEHoUQDQgAEK+fBQDn1Q+r5lGwcDoMUgeg2Aq16LLrLUz7xWI6mS0PUClzolDIo +eaV/Pfjl7zAvkbQQsZq3rTNnr1eGAk5P+A== +-----END EC PRIVATE KEY----- +*** DO NOT USE THIS KEY...EVER *** diff --git a/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml b/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml new file mode 100644 index 00000000000..b32e157daf6 --- /dev/null +++ b/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml @@ -0,0 +1,10 @@ +esp32: + variant: esp32 + framework: + type: esp-idf + advanced: + signed_ota_verification: + signing_key: ../../components/esp32/dummy_signing_key_v1_ecdsa.pem + signing_scheme: ecdsa_v1 + +<<: !include common.yaml From b20fedd806d44ad3b3241347e39595cdea4b9089 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:18:21 +1200 Subject: [PATCH 1572/2056] [bl0906] Disable loop when idle and introduce BL0906Stage enum (#15884) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/bl0906/bl0906.cpp | 67 +++++++++++++++++++--------- esphome/components/bl0906/bl0906.h | 19 +++++++- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index 70db235a375..d554057f7b0 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -20,58 +20,77 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) } void BL0906::loop() { - if (this->current_channel_ == UINT8_MAX) { - return; - } - while (this->available()) this->flush(); - if (this->current_channel_ == 0) { + if (this->current_stage_ == STAGE_IDLE) { + // Woken up between cycles to drain the action queue. Go back to sleep. + this->handle_actions_(); + this->disable_loop(); + return; + } + + if (this->current_stage_ == STAGE_TEMP) { // Temperature this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_); - } else if (this->current_channel_ == 1) { + } else if (this->current_stage_ == STAGE_CHANNEL_1) { this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_); this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_); this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_); - } else if (this->current_channel_ == 2) { + } else if (this->current_stage_ == STAGE_CHANNEL_2) { this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_); this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_); this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_); - } else if (this->current_channel_ == 3) { + } else if (this->current_stage_ == STAGE_CHANNEL_3) { this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_); this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_); this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_); - } else if (this->current_channel_ == 4) { + } else if (this->current_stage_ == STAGE_CHANNEL_4) { this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_); this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_); this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_); - } else if (this->current_channel_ == 5) { + } else if (this->current_stage_ == STAGE_CHANNEL_5) { this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_); this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_); this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_); - } else if (this->current_channel_ == 6) { + } else if (this->current_stage_ == STAGE_CHANNEL_6) { this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_); this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_); this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_); - } else if (this->current_channel_ == UINT8_MAX - 2) { + } else if (this->current_stage_ == STAGE_FREQ) { // Frequency - this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_); + this->read_data_(BL0906_FREQUENCY, BL0906_FREF, this->frequency_sensor_); // Voltage - this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_); - } else if (this->current_channel_ == UINT8_MAX - 1) { + this->read_data_(BL0906_V_RMS, BL0906_UREF, this->voltage_sensor_); + } else if (this->current_stage_ == STAGE_POWER) { // Total power this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_); // Total Energy this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_); - } else { - this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage - return; } - this->current_channel_++; + this->advance_stage_(); this->handle_actions_(); } +void BL0906::advance_stage_() { + switch (this->current_stage_) { + case STAGE_CHANNEL_6: + this->current_stage_ = STAGE_FREQ; + break; + case STAGE_FREQ: + this->current_stage_ = STAGE_POWER; + break; + case STAGE_POWER: + // Cycle complete; sleep until the next update(). + this->current_stage_ = STAGE_IDLE; + this->disable_loop(); + break; + default: + this->current_stage_ = static_cast<BL0906Stage>(this->current_stage_ + 1); + break; + } +} + void BL0906::setup() { while (this->available()) this->flush(); @@ -85,12 +104,20 @@ void BL0906::setup() { this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6 this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD)); + + // Loop stays idle until the first update() or enqueued action. + this->disable_loop(); } -void BL0906::update() { this->current_channel_ = 0; } +void BL0906::update() { + this->current_stage_ = STAGE_TEMP; + this->enable_loop(); +} size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) { this->action_queue_.push_back(function); + // Ensure the queue is serviced even if the read cycle has already completed. + this->enable_loop(); return this->action_queue_.size(); } diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h index 493b645c89d..f7ba5423d27 100644 --- a/esphome/components/bl0906/bl0906.h +++ b/esphome/components/bl0906/bl0906.h @@ -12,6 +12,22 @@ namespace esphome { namespace bl0906 { +// Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine +// jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle +// as complete and disables the loop. +enum BL0906Stage : uint8_t { + STAGE_TEMP = 0, // chip temperature + STAGE_CHANNEL_1 = 1, // per-phase current + power + energy + STAGE_CHANNEL_2 = 2, + STAGE_CHANNEL_3 = 3, + STAGE_CHANNEL_4 = 4, + STAGE_CHANNEL_5 = 5, + STAGE_CHANNEL_6 = 6, + STAGE_FREQ = UINT8_MAX - 2, // frequency + voltage + STAGE_POWER = UINT8_MAX - 1, // total power + total energy + STAGE_IDLE = UINT8_MAX, // cycle complete +}; + struct DataPacket { // NOLINT(altera-struct-pack-align) uint8_t l{0}; uint8_t m{0}; @@ -79,7 +95,8 @@ class BL0906 : public PollingComponent, public uart::UARTDevice { void bias_correction_(uint8_t address, float measurements, float correction); - uint8_t current_channel_{0}; + BL0906Stage current_stage_{STAGE_IDLE}; + void advance_stage_(); size_t enqueue_action_(ActionCallbackFuncPtr function); void handle_actions_(); From 9cebce1b6ecefcdb6d2c8fa3b6b854f08c24484e Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:19:01 +0200 Subject: [PATCH 1573/2056] [substitutions] Improve error messages with include stack trace (#15874) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/packages/__init__.py | 64 +++++-- esphome/components/substitutions/__init__.py | 83 +++----- esphome/yaml_util.py | 119 ++++++++++++ .../component_tests/packages/test_packages.py | 24 ++- tests/unit_tests/test_substitutions.py | 47 ++++- tests/unit_tests/test_yaml_util.py | 181 +++++++++++++++++- 6 files changed, 432 insertions(+), 86 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 97a53094800..b6ec0067c93 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -42,6 +42,11 @@ DOMAIN = CONF_PACKAGES # Guard against infinite include chains (e.g. A includes B includes A). MAX_INCLUDE_DEPTH = 20 +PackageCallback = Callable[ + [dict | str | yaml_util.IncludeFile, ContextVars | None, yaml_util.DocumentPath], + dict, +] + def is_remote_package(package_config: dict) -> bool: """Returns True if the package_config is a remote package definition.""" @@ -281,8 +286,9 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: def _walk_package_dict( packages: dict, - callback: Callable[[dict, ContextVars | None], dict], + callback: PackageCallback, context: ContextVars | None, + path: yaml_util.DocumentPath, ) -> cv.Invalid | None: """Iterate a packages dict in reverse priority order, invoking callback on each entry. @@ -291,7 +297,9 @@ def _walk_package_dict( for package_name, package_config in reversed(packages.items()): with cv.prepend_path(package_name): try: - packages[package_name] = callback(package_config, context) + packages[package_name] = callback( + package_config, context, path + [package_name] + ) except cv.Invalid as err: return err return None @@ -299,20 +307,22 @@ def _walk_package_dict( def _walk_package_list( packages: list, - callback: Callable[[dict, ContextVars | None], dict], + callback: PackageCallback, context: ContextVars | None, + path: yaml_util.DocumentPath, ) -> None: """Iterate a packages list in reverse priority order, invoking callback on each entry.""" for idx in reversed(range(len(packages))): with cv.prepend_path(idx): - packages[idx] = callback(packages[idx], context) + packages[idx] = callback(packages[idx], context, path + [idx]) def _walk_packages( config: dict, - callback: Callable[[dict, ContextVars | None], dict], + callback: PackageCallback, context: ContextVars | None = None, validate_deprecated: bool = True, + path: yaml_util.DocumentPath | None = None, ) -> dict: """Walks the packages structure in priority order, invoking ``callback`` on each package definition found. @@ -323,19 +333,24 @@ def _walk_packages( if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] + packages_path = (path or []) + [CONF_PACKAGES] with cv.prepend_path(CONF_PACKAGES): if isinstance(packages, yaml_util.IncludeFile): # If the packages key is an IncludeFile, resolve it first before processing. - packages, _ = resolve_include(packages, [], context, strict_undefined=False) + packages = resolve_include( + packages, packages_path, context, strict_undefined=False + ) if not isinstance(packages, (dict, list)): raise cv.Invalid( f"Packages must be a key to value mapping or list, got {type(packages)} instead" ) if not isinstance(packages, dict): - _walk_package_list(packages, callback, context) - elif (result := _walk_package_dict(packages, callback, context)) is not None: + _walk_package_list(packages, callback, context, packages_path) + elif ( + result := _walk_package_dict(packages, callback, context, packages_path) + ) is not None: if not validate_deprecated or any( is_package_definition(v) for v in packages.values() ): @@ -344,14 +359,18 @@ def _walk_packages( # This block can be removed once the single-package # deprecation period (2026.7.0) is over. config[CONF_PACKAGES] = [packages] - return _walk_packages(deprecate_single_package(config), callback, context) + return _walk_packages( + deprecate_single_package(config), callback, context, path=path + ) config[CONF_PACKAGES] = packages return config def _substitute_package_definition( - package_config: dict | str, context_vars: ContextVars | None + package_config: dict | str, + context_vars: ContextVars | None, + path: yaml_util.DocumentPath | None = None, ) -> dict | str: """Substitute variables in a package definition string or remote package dict. @@ -369,12 +388,12 @@ def _substitute_package_definition( errors: ErrList = [] package_config = substitute( item=package_config, - path=[], + path=path or [], parent_context=context_vars or ContextVars(), strict_undefined=False, errors=errors, ) - raise_first_undefined(errors, package_config, "package definition") + raise_first_undefined(errors, "package definition") return package_config @@ -432,6 +451,7 @@ class _PackageProcessor: self, package_config: dict | str | yaml_util.IncludeFile, context_vars: ContextVars | None, + path: yaml_util.DocumentPath, ) -> dict: """Resolve a package definition to a concrete ``dict`` and fetch remote packages. @@ -454,15 +474,15 @@ class _PackageProcessor: """ for _ in range(MAX_INCLUDE_DEPTH): if isinstance(package_config, yaml_util.IncludeFile): - package_config, _ = resolve_include( + package_config = resolve_include( package_config, - [], + path, context_vars or ContextVars(), strict_undefined=False, ) package_config = _substitute_package_definition( - package_config, context_vars + package_config, context_vars, path ) package_config = PACKAGE_SCHEMA(package_config) if isinstance(package_config, dict): @@ -483,13 +503,16 @@ class _PackageProcessor: _update_substitutions_context(self.parent_context, subs) def process_package( - self, package_config: dict | str, context_vars: ContextVars | None + self, + package_config: dict | str, + context_vars: ContextVars | None, + path: yaml_util.DocumentPath, ) -> dict: """Resolve a single package and recurse into any nested packages.""" from_remote = isinstance(package_config, dict) and is_remote_package( package_config ) - package_config = self.resolve_package(package_config, context_vars) + package_config = self.resolve_package(package_config, context_vars, path) self.collect_substitutions(package_config) if CONF_PACKAGES not in package_config: @@ -509,6 +532,7 @@ class _PackageProcessor: self.process_package, context_vars, validate_deprecated=not from_remote, + path=path, ) @@ -565,11 +589,13 @@ def merge_packages(config: dict) -> dict: merge_list: list[dict] = [] def process_package_callback( - package_config: dict, context: ContextVars | None + package_config: dict, + context: ContextVars | None, + path: yaml_util.DocumentPath | None = None, ) -> dict: """This will be called for each package found in the config.""" merge_list.append(package_config) - return _walk_packages(package_config, process_package_callback) + return _walk_packages(package_config, process_package_callback, path=path) _walk_packages(config, process_package_callback, validate_deprecated=False) # Merge all packages into the main config: diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 8bbccffca16..fb7cd7c51b3 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -11,9 +11,11 @@ from esphome.types import ConfigType from esphome.util import OrderedDict from esphome.yaml_util import ( ConfigContext, + DocumentPath, ESPHomeDataBase, ESPLiteralValue, IncludeFile, + format_path, make_data_base, ) @@ -23,8 +25,8 @@ CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) ContextVars = ChainMap[str, Any] -SubstitutionPath = list[int | str] -ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]] +ErrList = list[tuple[UndefinedError, DocumentPath, Any]] + # Module-level instance is safe: context_vars is passed per-call, and context_trace # is stack-saved/restored within expand(). Not thread-safe — only use from one thread. jinja = Jinja() @@ -32,16 +34,13 @@ jinja = Jinja() def raise_first_undefined( errors: ErrList, - source: Any, context_label: str, ) -> None: """If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable. - The raised error names the missing variable, the path walked into *source* - (for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location - when *source* carries one. Only the first error is surfaced; the user will - re-run after fixing it and any remaining undefined variables will be - reported then. + The raised error names the missing variable and its location in the include + stack. Only the first error is surfaced; the user will re-run after fixing it + and any remaining undefined variables will be reported then. ``context_label`` is the noun describing where the undefined variable appeared (e.g. ``"package definition"``). @@ -57,26 +56,8 @@ def raise_first_undefined( for e, p_path, _ in errors[1:] ) _LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras) - # Prefer the location of the offending scalar (e.g. the `url:` value) over - # the enclosing package-definition dict so the message points at the exact - # line/column that carries the undefined variable. - location_node = ( - err_value - if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None - else source - ) - location = "" - if ( - isinstance(location_node, ESPHomeDataBase) - and location_node.esp_range is not None - ): - mark = location_node.esp_range.start_mark - # DocumentLocation.line/column are 0-based (from the YAML Mark). Render - # as 1-based to match config.line_info() and editor line numbering. - location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})" - field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else "" raise cv.Invalid( - f"Undefined variable in {context_label}{field}: {err.message}{location}" + f"Undefined variable in {context_label}: {err.message}\n{format_path(err_path, err_value)}" ) @@ -145,7 +126,7 @@ def _resolve_var(name: str, context_vars: ContextVars) -> Any: def _handle_undefined( err: UndefinedError, - path: SubstitutionPath, + path: DocumentPath, value: Any, strict_undefined: bool, errors: ErrList | None, @@ -163,7 +144,7 @@ def _handle_undefined( def _expand_substitutions( value: str, - path: SubstitutionPath, + path: DocumentPath, context_vars: ContextVars, strict_undefined: bool, errors: ErrList | None, @@ -236,7 +217,7 @@ def _expand_substitutions( f"\nEvaluation stack: (most recent evaluation last)" f"\n{err.stack_trace_str()}" f"\nRelevant context:\n{err.context_trace_str()}" - f"\nSee {'->'.join(str(x) for x in path)}", + f"\n{format_path(path, orig_value)}", path, ) from err else: @@ -345,15 +326,13 @@ def push_context( def resolve_include( include: IncludeFile, - path: list[int | str], + path: DocumentPath, context_vars: ContextVars, strict_undefined: bool = True, errors: ErrList | None = None, -) -> tuple[Any, str]: +) -> Any: """Resolve an include, substituting the filename if needed. - Returns the loaded content and the resolved filename. - Note: no path-traversal validation is performed on the resolved filename. A substitution that resolves to an absolute path will bypass the parent directory (Path.__truediv__ ignores the left operand for absolute paths). @@ -361,44 +340,44 @@ def resolve_include( values (including command-line substitutions), so path restrictions are an explicit non-goal here. """ - original = str(include.file) + original = include.file + original_str = str(original) filename = str( _expand_substitutions( - original, path + ["file"], context_vars, strict_undefined, errors + original_str, path + ["file"], context_vars, strict_undefined, errors ) ) - if filename != original: + substituted = filename != original_str + if substituted: include = IncludeFile( include.parent_file, filename, include.vars, include.yaml_loader ) try: - return include.load(), filename + return include.load() except esphome.core.EsphomeError as err: + resolved = f" (expanded from '{original}')" if substituted else "" raise cv.Invalid( - f"Error including file '{filename}': {err}", + f"Error including file '{filename}'{resolved}: {err}" + f"\n{format_path(path, original)}", path + [f"<{filename}>"], ) from err def _substitute_include( include: IncludeFile, - path: list[int | str], + path: DocumentPath, context_vars: ContextVars, strict_undefined: bool, errors: ErrList | None, ) -> Any: """Resolve an include and substitute its content.""" - content, filename = resolve_include( - include, path, context_vars, strict_undefined, errors - ) - return substitute( - content, path + [f"<{filename}>"], context_vars, strict_undefined, errors - ) + content = resolve_include(include, path, context_vars, strict_undefined, errors) + return substitute(content, path, context_vars, strict_undefined, errors) def substitute( item: Any, - path: SubstitutionPath, + path: DocumentPath, parent_context: ContextVars, strict_undefined: bool, errors: ErrList | None = None, @@ -451,16 +430,12 @@ def _warn_unresolved_variables(errors: ErrList) -> None: for err, path, expression in errors: if "password" in path: continue - location: str = "->".join(str(x) for x in path) - if isinstance(expression, ESPHomeDataBase) and expression.esp_range is not None: - location += f" in {str(expression.esp_range.start_mark)}" - _LOGGER.warning( "The string '%s' looks like an expression," - " but could not resolve all the variables: %s (see %s)", + " but could not resolve all the variables: %s\n%s", expression, err.message, - location, + format_path(path, expression), ) @@ -479,7 +454,7 @@ def resolve_substitutions_block( # Single-shot resolution — matches ``_walk_packages`` for the # ``packages: !include`` entry point. Chained includes (an include that # itself loads another ``!include`` at the top level) are not supported. - substitutions, _ = resolve_include( + substitutions = resolve_include( substitutions, [], ContextVars(command_line_substitutions or {}), diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index e15adff935c..42da27ec142 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -48,6 +48,8 @@ _SECRET_VALUES = {} # Not thread-safe — config processing is single-threaded today. _load_listeners: list[Callable[[Path], None]] = [] +DocumentPath = list[str | int] + @contextmanager def track_yaml_loads() -> Generator[list[Path]]: @@ -679,6 +681,123 @@ def is_secret(value): return None +def _path_doc(item: Any) -> str | None: + """Return the source document name if *item* carries location info.""" + if isinstance(item, ESPHomeDataBase) and (r := item.esp_range) is not None: + return r.start_mark.document + return None + + +def _fmt_mark(loc: Any) -> str: + """Render a DocumentLocation as a 1-based 'file line:col' string.""" + return f"{loc.document} {loc.line + 1}:{loc.column + 1}" + + +def _obj_loc(obj: Any) -> str: + """Return formatted source location for *obj*, or '' if it has none.""" + if isinstance(obj, ESPHomeDataBase) and (r := obj.esp_range) is not None: + return _fmt_mark(r.start_mark) + return "" + + +def _fmt_segment(seg: list) -> str: + """Format a path segment, rendering integers as [n] subscripts.""" + parts: list[str] = [] + for item in seg: + if isinstance(item, int): + if parts: + parts[-1] = f"{parts[-1]}[{item}]" + else: + parts.append(f"[{item}]") + else: + parts.append(str(item)) + return "->".join(parts) + + +def _split_into_frames( + path: DocumentPath, +) -> list[tuple[list, str]]: + """Group *path* into per-file frames at include boundaries. + + A "frame" is the slice of the path that belongs to one source document. + Each path item is either: + + * a **located key** — has an ``ESPHomeDataBase`` source mark; this is + what tells us which document owns the surrounding keys. + * an **integer** — a list subscript; always attaches to the open frame + (renders as ``foo[3]`` on the previous name). + * an **unlocated string** — a key with no source mark (e.g. constants + like ``CONF_PACKAGES``); it describes the parent of the *next* file, + so it migrates to the next frame when the document changes. + + Returns a list of ``(items, "file line:col")`` tuples in walk order + (outermost frame first). + """ + frames: list[tuple[list, str]] = [] + open_frame: list = [] + next_frame_keys: list = [] # unlocated strings buffered for the next frame + open_doc: str | None = None + open_loc = "" + + for item in path: + doc = _path_doc(item) + if doc is None: + # Ints subscript the open frame's last name; everything else + # (strings, or leading ints with no open frame) is buffered for + # the next frame. + if isinstance(item, int) and open_doc is not None: + open_frame.append(item) + else: + next_frame_keys.append(item) + continue + if open_doc is not None and doc != open_doc: + # Crossed an include boundary: close the open frame. + frames.append((open_frame, open_loc)) + open_frame = [] + open_frame.extend(next_frame_keys) + next_frame_keys.clear() + open_frame.append(item) + open_doc = doc + open_loc = _fmt_mark(item.esp_range.start_mark) + + if open_doc is not None: + # Trailing buffered keys belong to the innermost (last) frame. + open_frame.extend(next_frame_keys) + frames.append((open_frame, open_loc)) + return frames + + +def format_path(path: DocumentPath, current_obj: Any) -> str: + """Build a human-readable include stack from a config path. + + Each YAML key in *path* that carries an ``ESPHomeDataBase`` ``esp_range`` + reveals which file it came from. When the source document changes between + consecutive such keys, that is an include boundary. The path is split + into per-file frames and formatted innermost-first, e.g.:: + + In: packages->roam in common/package/wifi.yaml 26:10 + Included from packages->net in common/hardware.yaml 44:2 + Included from packages->device in my_project.yaml 11:2 + + The innermost ``In:`` line uses the location from *current_obj* when + available (the value that triggered the error) for extra precision. + """ + frames = _split_into_frames(path) + obj_loc = _obj_loc(current_obj) + + if not frames: + # No source info anywhere in the path: render as a flat path, + # using current_obj's location if it happens to have one. + suffix = f" in {obj_loc}" if obj_loc else "" + return f"In: {_fmt_segment(path)}{suffix}" + + inner_seg, inner_loc = frames[-1] + lines = [f"In: {_fmt_segment(inner_seg)} in {obj_loc or inner_loc}"] + for seg, loc in reversed(frames[:-1]): + lines.append(f" Included from {_fmt_segment(seg)} in {loc}") + return "\n".join(lines) + + class ESPHomeDumper(yaml.SafeDumper): def represent_mapping(self, tag, mapping, flow_style=None): value = [] diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0bd339efa9c..af4b6db7961 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -46,7 +46,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.util import OrderedDict -from esphome.yaml_util import IncludeFile, add_context, load_yaml +from esphome.yaml_util import DocumentPath, IncludeFile, add_context, load_yaml # Test strings TEST_DEVICE_NAME = "test_device_name" @@ -1113,7 +1113,7 @@ def test_packages_include_file_resolves_to_list(mock_resolve_include) -> None: """When packages: is an IncludeFile that resolves to a list, it is processed correctly.""" include_file = MagicMock(spec=IncludeFile) package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} - mock_resolve_include.return_value = ([package_content], None) + mock_resolve_include.return_value = [package_content] config = {CONF_PACKAGES: include_file} result = do_packages_pass(config) @@ -1127,7 +1127,7 @@ def test_packages_include_file_resolves_to_dict(mock_resolve_include) -> None: """When packages: is an IncludeFile that resolves to a dict, it is processed correctly.""" include_file = MagicMock(spec=IncludeFile) package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}} - mock_resolve_include.return_value = ({"network": package_content}, None) + mock_resolve_include.return_value = {"network": package_content} config = {CONF_PACKAGES: include_file} result = do_packages_pass(config) @@ -1142,7 +1142,7 @@ def test_packages_include_file_resolves_to_invalid_type_raises( ) -> None: """When packages: is an IncludeFile that resolves to an invalid type, cv.Invalid is raised.""" include_file = MagicMock(spec=IncludeFile) - mock_resolve_include.return_value = ("not_a_dict_or_list", None) + mock_resolve_include.return_value = "not_a_dict_or_list" config = {CONF_PACKAGES: include_file} with pytest.raises( @@ -1215,7 +1215,9 @@ def test_named_dict_with_include_files_no_false_deprecation_warning( call_count = 0 - def failing_callback(package_config: dict, context: object) -> dict: + def failing_callback( + package_config: dict, context: object, path: DocumentPath | None = None + ) -> dict: nonlocal call_count call_count += 1 if call_count == 1: @@ -1251,7 +1253,9 @@ def test_validate_deprecated_false_raises_directly( call_count = 0 - def failing_callback(package_config: dict, context: object) -> dict: + def failing_callback( + package_config: dict, context: object, path: DocumentPath | None = None + ) -> dict: nonlocal call_count call_count += 1 if call_count == 1: @@ -1283,7 +1287,9 @@ def test_error_on_first_declared_package_still_detected() -> None: call_count = 0 - def fail_on_last(package_config: dict, context: object) -> dict: + def fail_on_last( + package_config: dict, context: object, path: DocumentPath | None = None + ) -> dict: nonlocal call_count call_count += 1 # Reverse iteration: third_pkg (1), second_pkg (2), first_pkg (3) @@ -1312,7 +1318,9 @@ def test_deprecated_single_package_fallback_still_works( attempt = 0 - def fail_then_succeed(package_config: dict, context: object) -> dict: + def fail_then_succeed( + package_config: dict, context: object, path: DocumentPath | None = None + ) -> dict: nonlocal attempt attempt += 1 if attempt == 1: diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 3599e703d9d..215ec291f9e 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -659,7 +659,7 @@ def test_resolve_package_max_depth_exceeded(tmp_path: Path) -> None: cv.Invalid, match=f"Maximum include nesting depth \\({MAX_INCLUDE_DEPTH}\\) exceeded", ): - processor.resolve_package(package_config, substitutions.ContextVars()) + processor.resolve_package(package_config, substitutions.ContextVars(), []) def test_include_filename_substitution_undefined_var(tmp_path: Path) -> None: @@ -690,7 +690,7 @@ def test_raise_first_undefined_logs_extras_at_debug( caplog.at_level(logging.DEBUG, logger="esphome.components.substitutions"), pytest.raises(cv.Invalid) as exc_info, ): - substitutions.raise_first_undefined(errors, None, "package definition") + substitutions.raise_first_undefined(errors, "package definition") # First error is surfaced as the cv.Invalid message. raised = str(exc_info.value) @@ -706,7 +706,7 @@ def test_raise_first_undefined_logs_extras_at_debug( def test_raise_first_undefined_noop_on_empty() -> None: """An empty errors list is a no-op — no exception, no log.""" - substitutions.raise_first_undefined([], None, "package definition") + substitutions.raise_first_undefined([], "package definition") def test_do_substitution_pass_included_substitutions_must_be_mapping( @@ -778,4 +778,43 @@ def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> No ) processor = _PackageProcessor({}, None, False) with pytest.raises(cv.Invalid, match="unresolved substitutions"): - processor.resolve_package(package_config, substitutions.ContextVars()) + processor.resolve_package(package_config, substitutions.ContextVars(), []) + + +def test_resolve_include_error_shows_expanded_from_when_substituted( + tmp_path: Path, +) -> None: + """When a substituted filename fails to load, the error includes '(expanded from ...)'.""" + parent = tmp_path / "main.yaml" + parent.write_text("") + + def failing_loader(_path: Path) -> None: + raise EsphomeError("File not found") + + include = yaml_util.IncludeFile(parent, "${device}.yaml", None, failing_loader) + context = substitutions.ContextVars({"device": "my_device"}) + + with pytest.raises(cv.Invalid) as exc_info: + substitutions.resolve_include(include, [], context) + + msg = str(exc_info.value) + assert "my_device.yaml" in msg + assert "expanded from '${device}.yaml'" in msg + + +def test_resolve_include_error_no_expanded_from_for_literal_filename( + tmp_path: Path, +) -> None: + """When a literal filename fails to load, the error has no 'expanded from' clause.""" + parent = tmp_path / "main.yaml" + parent.write_text("") + + def failing_loader(_path: Path) -> None: + raise EsphomeError("File not found") + + include = yaml_util.IncludeFile(parent, "literal.yaml", None, failing_loader) + + with pytest.raises(cv.Invalid) as exc_info: + substitutions.resolve_include(include, [], substitutions.ContextVars()) + + assert "expanded from" not in str(exc_info.value) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index bfd60de44d9..e3aa2a16f56 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -9,8 +9,9 @@ from esphome import core, yaml_util from esphome.components import substitutions from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv -from esphome.core import EsphomeError +from esphome.core import DocumentLocation, DocumentRange, EsphomeError from esphome.util import OrderedDict +from esphome.yaml_util import ESPHomeDataBase, format_path, make_data_base @pytest.fixture(autouse=True) @@ -712,3 +713,181 @@ def test_yaml_merge_chain_include_depth_exceeded() -> None: yaml_text = "base:\n <<: !include loop.yaml\n" with pytest.raises(EsphomeError, match="Maximum include chain depth"): yaml_util.parse_yaml(parent, io.StringIO(yaml_text), self_referencing_loader) + + +def _located(value, doc: str, line: int, col: int): + """Return *value* wrapped with a fake ESPHomeDataBase source location.""" + loc = DocumentLocation(doc, line, col) + obj = make_data_base(value) + if isinstance(obj, ESPHomeDataBase): + obj._esp_range = DocumentRange(loc, loc) + return obj + + +def test_format_path_no_location_info_returns_flat_path(): + """Plain path items with no esp_range produce a simple flat 'In:' line.""" + result = format_path(["wifi", "ssid"], None) + assert result == "In: wifi->ssid" + + +def test_format_path_no_location_info_current_obj_adds_file(): + """When path has no location but current_obj does, its location is shown.""" + obj = _located("${var}", "main.yaml", 5, 10) + result = format_path(["wifi", "ssid"], obj) + assert result == "In: wifi->ssid in main.yaml 6:11" + + +def test_format_path_single_frame_no_include_boundary(): + """All located keys from the same document → single 'In:' line, no 'Included from'.""" + path = ["packages", _located("pkg1", "root.yaml", 5, 2)] + result = format_path(path, None) + assert result.startswith("In: packages->pkg1 in root.yaml 6:3") + assert "Included from" not in result + + +def test_format_path_two_frames_shows_included_from(): + """Keys from two different documents produce 'In:' + one 'Included from' line.""" + path = [ + "packages", + _located("device", "root.yaml", 10, 2), + "packages", + _located("inner", "hardware.yaml", 3, 2), + ] + result = format_path(path, None) + assert "In: packages->inner in hardware.yaml 4:3" in result + assert "Included from packages->device in root.yaml 11:3" in result + + +def test_format_path_three_frames_full_include_stack(): + """Three document levels produce two 'Included from' lines in correct order.""" + path = [ + "packages", + _located("device", "root.yaml", 10, 2), + "packages", + _located("_wifi_", "hardware.yaml", 43, 2), + "packages", + _located("_roam_", "wifi.yaml", 25, 2), + ] + result = format_path(path, None) + lines = result.splitlines() + assert lines[0].startswith("In: packages->_roam_ in wifi.yaml") + assert lines[1].startswith(" Included from packages->_wifi_ in hardware.yaml") + assert lines[2].startswith(" Included from packages->device in root.yaml") + + +def test_format_path_current_obj_overrides_innermost_location(): + """current_obj's esp_range replaces the key's column for the 'In:' line.""" + path = ["packages", _located("pkg1", "root.yaml", 5, 2)] + # Value (the expression) sits at column 10, not column 2 like the key + value = _located("${undefined}", "root.yaml", 5, 10) + result = format_path(path, value) + assert "6:11" in result + assert "6:3" not in result + + +def test_format_path_empty_path_with_no_location(): + """Empty path with no location info returns 'In: '.""" + result = format_path([], None) + assert result == "In: " + + +def test_format_path_integer_path_items_formatted_as_subscript(): + """Integer indices are rendered as [n] subscripts in the flat fallback.""" + result = format_path(["packages", 0], None) + assert result == "In: packages[0]" + + +def test_format_path_integer_list_index_attached_to_previous_frame(): + """A list index between two include boundaries attaches to the outer frame.""" + path = [ + "packages", + _located("packages", "main.yaml", 5, 0), + 0, + _located("packages", "level1.yaml", 2, 0), + 0, + _located("esphome", "level2.yaml", 0, 0), + _located("name", "level2.yaml", 1, 8), + ] + result = format_path(path, None) + lines = result.splitlines() + assert lines[0].startswith("In: esphome->name in level2.yaml") + assert "packages[0]" in lines[1] and "level1.yaml" in lines[1] + assert "packages[0]" in lines[2] and "main.yaml" in lines[2] + + +def test_format_path_trailing_unlocated_string_after_located_key(): + """Plain string keys after the last located key must still appear in output.""" + path = [_located("packages", "main.yaml", 5, 0), "sub", "key"] + result = format_path(path, None) + assert result == "In: packages->sub->key in main.yaml 6:1" + + +def test_format_path_trailing_unlocated_int_attaches_to_current_frame(): + """Trailing ints attach to the open frame's last key (subscript), strings + buffer until end-of-path and then flush behind.""" + path = [_located("packages", "main.yaml", 5, 0), 0, "sub"] + result = format_path(path, None) + # Int attaches to 'packages' as [0] subscript; trailing 'sub' is flushed + # at end and appears after. + assert result == "In: packages[0]->sub in main.yaml 6:1" + + +def test_format_path_only_trailing_unlocated_strings_are_preserved(): + """Trailing pending items must not be silently dropped after the last frame.""" + path = [ + _located("packages", "main.yaml", 5, 0), + _located("inner", "hardware.yaml", 3, 0), + "tail1", + "tail2", + ] + result = format_path(path, None) + lines = result.splitlines() + assert lines[0] == "In: inner->tail1->tail2 in hardware.yaml 4:1" + assert lines[1] == " Included from packages in main.yaml 6:1" + + +def test_format_path_leading_int_with_no_current_doc_goes_to_pending(): + """An int before any located key is buffered and shown in the first frame.""" + path = [0, _located("name", "main.yaml", 1, 0)] + result = format_path(path, None) + # Leading ints have no preceding name to subscript onto, so they render + # as bare [n] in the formatted segment. + assert result == "In: [0]->name in main.yaml 2:1" + + +def test_format_path_only_unlocated_int_returns_flat_fallback(): + """Path with only an int and no location info renders via the flat fallback.""" + result = format_path([0], None) + assert result == "In: [0]" + + +def test_format_path_current_obj_in_different_doc_than_innermost_frame(): + """current_obj's location is preferred even when its document differs from the frame's.""" + path = [_located("packages", "root.yaml", 1, 0)] + value = _located("${var}", "other.yaml", 9, 4) + result = format_path(path, value) + # Innermost line uses current_obj's mark (other.yaml 10:5), not the key's. + assert result == "In: packages in other.yaml 10:5" + + +def test_format_path_current_obj_without_location_falls_back_to_key(): + """An ESPHomeDataBase current_obj with no esp_range falls back to the key's location.""" + + class _NoRange(ESPHomeDataBase, str): + pass + + obj = _NoRange.__new__(_NoRange, "value") + str.__init__(obj) + # No _esp_range set on this instance. + assert obj.esp_range is None + + path = [_located("packages", "main.yaml", 5, 2)] + result = format_path(path, obj) + assert result == "In: packages in main.yaml 6:3" + + +def test_format_path_empty_path_with_located_current_obj(): + """An empty path with a located current_obj still surfaces the location.""" + obj = _located("${var}", "main.yaml", 0, 0) + result = format_path([], obj) + assert result == "In: in main.yaml 1:1" From da44d43981cff7b79765652dbcfdc39fd481935a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 05:07:48 +0200 Subject: [PATCH 1574/2056] Update pyparsing requirement from >=3.0 to >=3.3.2 (#15910) 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 95d7c8c0328..482ea92da79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ smpclient==6.0.0 requests==2.33.1 # esp-idf >= 5.0 requires this -pyparsing >= 3.0 +pyparsing >= 3.3.2 # For autocompletion argcomplete>=2.0.0 From 78f1467be46956a0a5f621a04f0ad0967cf92f44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:08:42 +0000 Subject: [PATCH 1575/2056] Bump aioesphomeapi from 44.17.0 to 44.18.0 (#15912) 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 482ea92da79..b49777beaaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.17.0 +aioesphomeapi==44.18.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From bb81c91d0c9ed21b367fba7cabbb05ef5b3bab26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:08:58 +0000 Subject: [PATCH 1576/2056] Update tzdata requirement from >=2021.1 to >=2026.1 (#15911) 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 b49777beaaf..90f06eff98a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ colorama==0.4.6 icmplib==3.0.4 tornado==6.5.5 tzlocal==5.3.1 # from time -tzdata>=2021.1 # from time +tzdata>=2026.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 From edcf96d0575232c153e8d4d7bb6de2aab0410c9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:24:09 +0200 Subject: [PATCH 1577/2056] [wifi] Use queue abstraction for LibreTiny WiFi events (#15343) --- esphome/components/libretiny/__init__.py | 7 ++ .../libretiny/freertos_static_alloc.c | 52 ++++++++++ esphome/components/wifi/wifi_component.cpp | 11 ++- esphome/components/wifi/wifi_component.h | 20 +++- .../wifi/wifi_component_esp8266.cpp | 5 +- .../wifi/wifi_component_esp_idf.cpp | 22 +++-- .../wifi/wifi_component_libretiny.cpp | 61 ++++-------- .../components/wifi/wifi_component_pico_w.cpp | 3 +- esphome/core/freertos_queue.h | 99 +++++++++++++++++++ 9 files changed, 227 insertions(+), 53 deletions(-) create mode 100644 esphome/components/libretiny/freertos_static_alloc.c create mode 100644 esphome/core/freertos_queue.h diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 4f42f40478c..40b8c8dc6c1 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -443,6 +443,13 @@ async def component_to_code(config): # 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS # mutexes are simpler and equivalent. cg.add_define(ThreadModel.MULTI_NO_ATOMICS) + # Enable FreeRTOS static allocation so FreeRTOSQueue can use + # xQueueCreateStatic (queue storage in BSS, no heap allocation). + # Also moves FreeRTOS internal structures (timer command queue) to BSS. + # BK72xx's FreeRTOSConfig.h doesn't define this, defaulting to 0. + # The -D wins over the #ifndef default in FreeRTOS.h. + # Not enabled on RTL87xx/LN882x — costs more heap than it saves there. + cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") # RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake # required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c new file mode 100644 index 00000000000..62b0524230a --- /dev/null +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -0,0 +1,52 @@ +/* + * FreeRTOS static allocation callbacks for LibreTiny platforms. + * + * Required when configSUPPORT_STATIC_ALLOCATION is enabled. These callbacks + * provide memory for the idle and timer tasks. Following ESP-IDF's approach, + * we allocate from the FreeRTOS heap (pvPortMalloc) rather than using truly + * static buffers, to avoid assumptions about memory layout. + * + * This enables xQueueCreateStatic, xTaskCreateStatic, etc. throughout ESPHome, + * allowing queue storage to live in BSS with zero runtime heap allocation. + */ + +#ifdef USE_BK72XX + +#include <FreeRTOS.h> +#include <task.h> + +#if (configSUPPORT_STATIC_ALLOCATION == 1) + +void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, + uint32_t *pulIdleTaskStackSize) { + /* Stack grows down on ARM — allocate stack first, then TCB, + * so the stack does not grow into the TCB. */ + StackType_t *stack = (StackType_t *) pvPortMalloc(configMINIMAL_STACK_SIZE * sizeof(StackType_t)); + StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t)); + configASSERT(stack != NULL); + configASSERT(tcb != NULL); + + *ppxIdleTaskTCBBuffer = tcb; + *ppxIdleTaskStackBuffer = stack; + *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; +} + +#if (configUSE_TIMERS == 1) + +void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, + uint32_t *pulTimerTaskStackSize) { + StackType_t *stack = (StackType_t *) pvPortMalloc(configTIMER_TASK_STACK_DEPTH * sizeof(StackType_t)); + StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t)); + configASSERT(stack != NULL); + configASSERT(tcb != NULL); + + *ppxTimerTaskTCBBuffer = tcb; + *ppxTimerTaskStackBuffer = stack; + *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; +} + +#endif /* configUSE_TIMERS */ + +#endif /* configSUPPORT_STATIC_ALLOCATION */ + +#endif /* USE_BK72XX */ diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 598aee8f66e..481846085c2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -732,9 +732,16 @@ void WiFiComponent::restart_adapter() { } void WiFiComponent::loop() { - this->wifi_loop_(); + bool events_processed = this->wifi_loop_(); const uint32_t now = App.get_loop_component_start_time(); - this->update_connected_state_(); + // Connection state can only change when events are processed (ESP-IDF/LibreTiny) + // or polled (ESP8266/Pico W). Skip the expensive wifi_sta_connect_status_() call + // when no events arrived and we're already in steady state. + // Must also run when connected_ is false — after state transitions to STA_CONNECTED, + // connected_ won't be set until update_connected_state_() runs. + if (events_processed || !this->connected_) { + this->update_connected_state_(); + } if (this->has_sta()) { #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 665dec37d5a..53fb0728fb7 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -9,6 +9,11 @@ #ifdef USE_ESP32 #include "esphome/core/lock_free_queue.h" #endif +#if defined(USE_LIBRETINY) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#include "esphome/core/lock_free_queue.h" +#elif defined(USE_LIBRETINY) && defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) +#include "esphome/core/freertos_queue.h" +#endif #include "esphome/core/string_ref.h" #include <span> @@ -657,7 +662,7 @@ class WiFiComponent final : public Component { void connect_soon_(); - void wifi_loop_(); + bool wifi_loop_(); #ifdef USE_ESP8266 void process_pending_callbacks_(); #endif @@ -882,6 +887,19 @@ class WiFiComponent final : public Component { LockFreeQueue<IDFWiFiEvent, 17> event_queue_; #endif +#ifdef USE_LIBRETINY + // Thread-safe queue for WiFi events from LibreTiny callback thread. + // LockFreeQueue on platforms with hardware atomics (RTL87xx, LN882x), + // FreeRTOSQueue on platforms without (BK72xx). + static constexpr uint8_t LT_EVENT_QUEUE_SIZE = 16; +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Ring buffer reserves one slot, so +1 for 16 usable slots + LockFreeQueue<LTWiFiEvent, LT_EVENT_QUEUE_SIZE + 1> event_queue_; +#else + FreeRTOSQueue<LTWiFiEvent, LT_EVENT_QUEUE_SIZE> event_queue_; +#endif +#endif + private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cb53d3ac1bd..e56a8df350b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -938,7 +938,10 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } +bool WiFiComponent::wifi_loop_() { + this->process_pending_callbacks_(); + return true; +} void WiFiComponent::process_pending_callbacks_() { // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4097df80afd..c790742c797 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -715,17 +715,25 @@ const char *get_disconnect_reason_str(uint8_t reason) { } } -void WiFiComponent::wifi_loop_() { +bool WiFiComponent::wifi_loop_() { + // Use pop() directly instead of empty() — pop() costs 1 memw (acquire on tail_), + // while empty() costs 2 memw (acquire on both head_ and tail_) on Xtensa. + IDFWiFiEvent *data = this->event_queue_.pop(); + if (data == nullptr) + return false; + + do { + wifi_process_event_(data); + delete data; // NOLINT(cppcoreguidelines-owning-memory) + } while ((data = this->event_queue_.pop()) != nullptr); + + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check. uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); } - - IDFWiFiEvent *data; - while ((data = this->event_queue_.pop()) != nullptr) { - wifi_process_event_(data); - delete data; // NOLINT(cppcoreguidelines-owning-memory) - } + return true; } // Events are processed from queue in main loop context, but listener notifications // must be deferred until after the state machine transitions (in check_connecting_finished) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 9565ffa7473..cdd11ceaefa 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -10,9 +10,6 @@ #include "lwip/err.h" #include "lwip/dns.h" -#include <FreeRTOS.h> -#include <queue.h> - #ifdef USE_BK72XX extern "C" { #include <wlan_ui_pub.h> @@ -43,16 +40,13 @@ static const char *const TAG = "wifi_lt"; // (like connection status flags) from the callback causes race conditions: // - The main loop may never see state changes (values cached in registers) // - State changes may be visible in inconsistent order -// - LibreTiny targets (BK7231, RTL8720) lack atomic instructions (no LDREX/STREX) // // Solution: Queue events in the callback and process them in the main loop. // This is the same approach used by ESP32 IDF's wifi_process_event_(). // All state modifications happen in the main loop context, eliminating races. - -static constexpr size_t EVENT_QUEUE_SIZE = 16; // Max pending WiFi events before overflow -static QueueHandle_t s_event_queue = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static volatile uint32_t s_event_queue_overflow_count = - 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// +// On platforms with hardware atomics (RTL87xx, LN882x): LockFreeQueue (SPSC ring buffer) +// On platforms without (BK72xx): FreeRTOSQueue (xQueue wrapper with critical sections) // Event structure for queued WiFi events - contains a copy of event data // to avoid lifetime issues with the original event data from the callback @@ -352,10 +346,6 @@ using esphome_wifi_event_info_t = arduino_event_info_t; // Event callback - runs in WiFi driver thread context // Only queues events for processing in main loop, no logging or state changes here void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { - if (s_event_queue == nullptr) { - return; - } - // Allocate on heap and fill directly to avoid extra memcpy auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory) to_send->event_id = event; @@ -428,9 +418,8 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } // Queue event (don't block if queue is full) - if (xQueueSend(s_event_queue, &to_send, 0) != pdPASS) { + if (!this->event_queue_.push(to_send)) { delete to_send; // NOLINT(cppcoreguidelines-owning-memory) - s_event_queue_overflow_count++; } } @@ -620,14 +609,6 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } } void WiFiComponent::wifi_pre_setup_() { - // Create event queue for thread-safe event handling - // Events are pushed from WiFi callback thread and processed in main loop - s_event_queue = xQueueCreate(EVENT_QUEUE_SIZE, sizeof(LTWiFiEvent *)); - if (s_event_queue == nullptr) { - ESP_LOGE(TAG, "Failed to create event queue"); - return; - } - WiFi.onEvent( [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); }); // Make sure WiFi is in clean state before anything starts @@ -796,28 +777,26 @@ int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } -void WiFiComponent::wifi_loop_() { - // Process all pending events from the queue - if (s_event_queue == nullptr) { - return; - } - - // Check for dropped events due to queue overflow - if (s_event_queue_overflow_count > 0) { - ESP_LOGW(TAG, "Event queue overflow, %" PRIu32 " events dropped", s_event_queue_overflow_count); - s_event_queue_overflow_count = 0; - } - - while (true) { - LTWiFiEvent *event; - if (xQueueReceive(s_event_queue, &event, 0) != pdTRUE) { - // No more events - break; - } +bool WiFiComponent::wifi_loop_() { + // Use pop() directly instead of empty() — avoids redundant synchronization. + // LockFreeQueue: pop() costs 1 memw vs empty()'s 2 memw on Xtensa. + // FreeRTOSQueue: pop() is 1 critical section vs empty() + pop() = 2. + LTWiFiEvent *event = this->event_queue_.pop(); + if (event == nullptr) + return false; + do { wifi_process_event_(event); delete event; // NOLINT(cppcoreguidelines-owning-memory) + } while ((event = this->event_queue_.pop()) != nullptr); + + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check. + uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %" PRIu16 " WiFi events due to buffer overflow", dropped); } + return true; } } // namespace esphome::wifi diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1cfeee3c1bf..4e1e0395c03 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -303,7 +303,7 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { // Connect state listener notifications are deferred until after the state machine // transitions (in check_connecting_finished) so that conditions like wifi.connected // return correct values in automations. -void WiFiComponent::wifi_loop_() { +bool WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; @@ -365,6 +365,7 @@ void WiFiComponent::wifi_loop_() { #endif } } + return true; } void WiFiComponent::wifi_pre_setup_() {} diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h new file mode 100644 index 00000000000..2f3faf818a3 --- /dev/null +++ b/esphome/core/freertos_queue.h @@ -0,0 +1,99 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS + +#include <cstddef> +#include <cstdint> + +#include <FreeRTOS.h> +#include <queue.h> + +/* + * FreeRTOS queue wrapper for single-producer single-consumer scenarios on + * platforms without hardware atomic support (e.g. BK72xx ARM968E-S). + * + * Provides the same API as LockFreeQueue (push, pop, get_and_reset_dropped_count, + * empty, full, size) but uses xQueue internally, which synchronizes via + * FreeRTOS critical sections. Uses xQueueCreateStatic so the queue storage + * lives in BSS with zero runtime heap allocation. + * + * @tparam T The type of elements stored in the queue (stored as pointers) + * @tparam SIZE The maximum number of elements + */ + +namespace esphome { + +template<class T, uint8_t SIZE> class FreeRTOSQueue { + public: + FreeRTOSQueue() : dropped_count_(0) { + this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_); + } + + // No destructor — ESPHome components are never destroyed. Intentionally + // omitted to avoid pulling in vQueueDelete code on resource-constrained targets. + + // Non-copyable, non-movable — queue handle is not transferable + FreeRTOSQueue(const FreeRTOSQueue &) = delete; + FreeRTOSQueue &operator=(const FreeRTOSQueue &) = delete; + FreeRTOSQueue(FreeRTOSQueue &&) = delete; + FreeRTOSQueue &operator=(FreeRTOSQueue &&) = delete; + + bool push(T *element) { + if (element == nullptr) + return false; + + if (xQueueSend(this->handle_, &element, 0) != pdPASS) { + this->increment_dropped_count(); + return false; + } + return true; + } + + T *pop() { + T *element; + if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) { + return nullptr; + } + return element; + } + + uint16_t get_and_reset_dropped_count() { + // Fast path: plain read of aligned uint16_t is a single ARM load instruction. + // Worst case is reading a stale zero and reporting drops one iteration later. + // Avoids critical section overhead on every loop() call since drops are rare. + if (this->dropped_count_ == 0) + return 0; + // Declare outside critical section — BK72xx portENTER_CRITICAL may introduce a scope + uint16_t count; + portENTER_CRITICAL(); + count = this->dropped_count_; + this->dropped_count_ = 0; + portEXIT_CRITICAL(); + return count; + } + + void increment_dropped_count() { + portENTER_CRITICAL(); + this->dropped_count_++; + portEXIT_CRITICAL(); + } + + bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } + + bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } + + size_t size() const { return uxQueueMessagesWaiting(this->handle_); } + + protected: + // Static storage for the queue — lives in BSS, no heap allocation + uint8_t storage_[SIZE * sizeof(T *)]; + StaticQueue_t queue_buf_; + QueueHandle_t handle_; + uint16_t dropped_count_; +}; + +} // namespace esphome + +#endif // ESPHOME_THREAD_MULTI_NO_ATOMICS From 67576d4879e252d4b765cef4ca92c580377d68b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:29:13 +0200 Subject: [PATCH 1578/2056] [rp2040] Tune oversized lwIP defaults for ESPHome (#14843) --- MANIFEST.in | 1 + esphome/components/rp2040/__init__.py | 162 +++++++++++++++++- esphome/components/rp2040/const.py | 1 + .../rp2040/inject_lwip_include.py.script | 18 ++ esphome/components/rp2040/lwipopts.h.jinja | 46 +++++ esphome/components/wifi/__init__.py | 10 +- script/stress_test_connect.py | 84 +++++++++ 7 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 esphome/components/rp2040/inject_lwip_include.py.script create mode 100644 esphome/components/rp2040/lwipopts.h.jinja create mode 100644 script/stress_test_connect.py diff --git a/MANIFEST.in b/MANIFEST.in index ed65edc656b..e426627e8d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,5 @@ include requirements.txt recursive-include esphome *.yaml recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.py.script +recursive-include esphome *.jinja recursive-include esphome LICENSE.txt diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index e452780d412..ed246416c91 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -26,7 +26,7 @@ from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed from . import boards -from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns +from .const import KEY_BOARD, KEY_LWIP_OPTS, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa @@ -240,6 +240,160 @@ async def to_code(config): cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT]) cg.add_define("USE_RP2040_CRASH_HANDLER") + _configure_lwip() + + +def _configure_lwip() -> None: + """Configure lwIP options for RP2040 by generating a custom lwipopts.h. + + Arduino-pico's lwipopts.h has no #ifndef guards, so -D flags cannot override + its settings. Instead, we generate a replacement lwipopts.h and place it in an + include directory that shadows the framework's version. + + lwIP is compiled from source on RP2040 (not pre-built), so our replacement + header fully controls the compiled lwIP behavior. + + RP2040 uses NO_SYS=1 (polling, no RTOS thread), LWIP_SOCKET=0, LWIP_NETCONN=0. + DHCP/DNS use raw udp_new() which allocates from MEMP_NUM_UDP_PCB. + + Comparison of arduino-pico defaults vs ESPHome targets (TCP_MSS=1460): + + Setting ESP8266 ESP32 arduino-pico New + ──────────────────────────────────────────────────────────────── + TCP_SND_BUF 2×MSS 4×MSS 8×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 8×MSS 4×MSS + MEM_LIBC_MALLOC 1 1 0 0* + MEMP_MEM_MALLOC 1 1 0 0** + MEM_SIZE N/A*** N/A*** 16KB 16KB + PBUF_POOL_SIZE 10 16 24 16 + MEMP_NUM_TCP_SEG 10 16 32 17 + MEMP_NUM_TCP_PCB 5 16 5 dynamic + MEMP_NUM_TCP_PCB_LISTEN 4 16 8**** dynamic + MEMP_NUM_UDP_PCB 4 16 7 dynamic + TCP_SND_QUEUELEN ~8 17 32 17 + + * MEM_LIBC_MALLOC must stay 0: arduino-pico uses + PICO_CYW43_ARCH_THREADSAFE_BACKGROUND which runs lwIP callbacks from + a low-priority pendsv IRQ. The pico-sdk explicitly blocks + MEM_LIBC_MALLOC=1 because libc malloc uses mutexes (unsafe in IRQ). + ** MEMP_MEM_MALLOC must stay 0: the dedicated lwIP heap (MEM_SIZE=16KB) + is too small to hold all pools dynamically. The PBUF_POOL alone needs + ~24KB (16 × 1524 bytes). Increasing MEM_SIZE would negate BSS savings. + *** ESP8266/ESP32 use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool). + **** opt.h default; arduino-pico doesn't override MEMP_NUM_TCP_PCB_LISTEN. + "dynamic" = auto-calculated from component socket registrations via + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP / 2 TCP_LISTEN. + """ + from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, + MIN_TCP_SOCKETS, + MIN_UDP_SOCKETS, + get_socket_counts, + ) + + sc = get_socket_counts() + # Apply platform minimums — ensure headroom for ESPHome's needs + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) + # RP2040 has more RAM (264KB) than most LibreTiny boards, so DHCP/DNS + # UDP PCBs (2) are absorbed by the generous minimum of 6. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) + + # TCP_SND_BUF: 4×MSS=5,840 matches ESP32. Down from arduino-pico's 8×MSS. + # ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per response chunk. + tcp_snd_buf = "(4*TCP_MSS)" + + # TCP_WND: receive window. 4×MSS matches ESP32. Down from arduino-pico's 8×MSS. + tcp_wnd = "(4*TCP_MSS)" + + # TCP_SND_QUEUELEN: max pbufs queued for send buffer + # ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS + # With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32 + tcp_snd_queuelen = 17 + # MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check) + memp_num_tcp_seg = tcp_snd_queuelen + + # PBUF_POOL_SIZE: RP2040 has 264KB RAM, more generous than LibreTiny. + # 16 matches ESP32 (vs arduino-pico's 24). With MEMP_MEM_MALLOC=1, + # this is a max count (allocated on demand from heap). + pbuf_pool_size = 16 + + # Build the lwIP override defines for the Jinja2 template. + # The template uses #include_next to chain to the framework's original + # lwipopts.h, then #undef/#define only the values we need to change. + # + # Note: MEMP_MEM_MALLOC stays 0 (framework default). While the memp + # allocations use the dedicated lwIP heap (IRQ-safe), the 16KB MEM_SIZE + # is too small to hold all pools dynamically under stress. The PBUF_POOL + # alone needs ~24KB (16 × 1524 bytes). Increasing MEM_SIZE would negate + # the BSS savings. + # + # MEM_LIBC_MALLOC stays 0 (framework default): arduino-pico uses + # PICO_CYW43_ARCH_THREADSAFE_BACKGROUND which runs lwIP callbacks from + # a low-priority pendsv IRQ where libc malloc (mutex-based) is unsafe. + lwip_defines: dict[str, str] = { + "TCP_SND_BUF": tcp_snd_buf, + "TCP_WND": tcp_wnd, + "TCP_SND_QUEUELEN": str(tcp_snd_queuelen), + "MEMP_NUM_TCP_SEG": str(memp_num_tcp_seg), + "PBUF_POOL_SIZE": str(pbuf_pool_size), + "MEMP_NUM_TCP_PCB": str(tcp_sockets), + "MEMP_NUM_TCP_PCB_LISTEN": str(listening_tcp), + "MEMP_NUM_UDP_PCB": str(udp_sockets), + } + + # Store for copy_files() to generate the header + CORE.data[KEY_RP2040][KEY_LWIP_OPTS] = lwip_defines + + # Add a pre-build extra script that injects our lwip_override directory + # into CCFLAGS so our lwipopts.h shadows the framework's version. + # Regular build_flags (-I/-isystem) come after -iwithprefixbefore in GCC's + # search order, so we must prepend via an extra_scripts hook. + cg.add_platformio_option("extra_scripts", ["pre:inject_lwip_include.py"]) + + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" + _LOGGER.info( + "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", + tcp_sockets, + tcp_min, + sc.tcp_details, + udp_sockets, + udp_min, + sc.udp_details, + listening_tcp, + listen_min, + sc.tcp_listen_details, + ) + + +def _generate_lwipopts_h() -> None: + """Generate a custom lwipopts.h that shadows the framework's version. + + Uses Jinja2 to render the template with the lwIP defines calculated + during code generation. The generated header is placed in lwip_override/ + in the build directory, and a pre-build script injects this directory + into the compiler include path before the framework's own include dir. + """ + from jinja2 import Environment, FileSystemLoader + + lwip_defines = CORE.data[KEY_RP2040].get(KEY_LWIP_OPTS) + if not lwip_defines: + return + + template_dir = Path(__file__).parent + jinja_env = Environment( + loader=FileSystemLoader(str(template_dir)), + keep_trailing_newline=True, + ) + template = jinja_env.get_template("lwipopts.h.jinja") + content = template.render(**lwip_defines) + + lwip_dir = CORE.relative_build_path("lwip_override") + lwip_dir.mkdir(parents=True, exist_ok=True) + write_file_if_changed(lwip_dir / "lwipopts.h", content) + def add_pio_file(component: str, key: str, data: str): try: @@ -289,6 +443,12 @@ def copy_files(): post_build_file, CORE.relative_build_path("post_build.py"), ) + inject_lwip_file = dir / "inject_lwip_include.py.script" + copy_file_if_changed( + inject_lwip_file, + CORE.relative_build_path("inject_lwip_include.py"), + ) + _generate_lwipopts_h() if generate_pio_files(): path = CORE.relative_src_path("esphome.h") content = read_file(path).rstrip("\n") diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index ab5f42d7573..e381d0482de 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -1,6 +1,7 @@ import esphome.codegen as cg KEY_BOARD = "board" +KEY_LWIP_OPTS = "lwip_opts" KEY_RP2040 = "rp2040" KEY_PIO_FILES = "pio_files" diff --git a/esphome/components/rp2040/inject_lwip_include.py.script b/esphome/components/rp2040/inject_lwip_include.py.script new file mode 100644 index 00000000000..4ae9863e371 --- /dev/null +++ b/esphome/components/rp2040/inject_lwip_include.py.script @@ -0,0 +1,18 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os + +# PlatformIO pre-build script: inject lwip_override include path so our +# lwipopts.h shadows the framework's version during lwIP compilation. +# +# The arduino-pico builder uses -iprefix + -iwithprefixbefore for includes, +# which takes priority over CPPPATH (-I). We must inject our path into the +# CCFLAGS BEFORE the -iprefix flag to ensure our lwipopts.h is found first. + +lwip_dir = os.path.join(env["PROJECT_DIR"], "lwip_override") + +if os.path.isdir(lwip_dir): + # Insert -I<lwip_dir> at the beginning of CCFLAGS, before the framework's + # -iprefix/-iwithprefixbefore flags which would otherwise take priority. + env.Prepend(CCFLAGS=["-I", lwip_dir]) diff --git a/esphome/components/rp2040/lwipopts.h.jinja b/esphome/components/rp2040/lwipopts.h.jinja new file mode 100644 index 00000000000..36d7d4da140 --- /dev/null +++ b/esphome/components/rp2040/lwipopts.h.jinja @@ -0,0 +1,46 @@ +// ESPHome lwIP configuration override for RP2040. +// Includes the framework's original lwipopts.h, then overrides specific +// settings to tune lwIP for ESPHome's IoT use case. +// +// This file is found first via -I injection (see inject_lwip_include.py.script). +// #include_next chains to the framework's original in include/lwipopts.h. +// Since the original uses #pragma once, it won't be included again later +// (e.g. via tusb_config.h), avoiding duplicate definition warnings. + +// Include the framework's original lwipopts.h first +#include_next "lwipopts.h" + +// --- ESPHome overrides below --- +// Only #undef and redefine values that differ from the framework defaults. + +// TCP send/receive buffers: 4xMSS matches ESP32 (down from 8xMSS) +#undef TCP_SND_BUF +#define TCP_SND_BUF {{ TCP_SND_BUF }} + +#undef TCP_WND +#define TCP_WND {{ TCP_WND }} + +// Queued segment limits: derived from 4xMSS buffer size, matching ESP32 +#undef TCP_SND_QUEUELEN +#define TCP_SND_QUEUELEN {{ TCP_SND_QUEUELEN }} + +#undef MEMP_NUM_TCP_SEG +#define MEMP_NUM_TCP_SEG {{ MEMP_NUM_TCP_SEG }} + +// Packet buffer pool: 16 matches ESP32 (down from 24) +#undef PBUF_POOL_SIZE +#define PBUF_POOL_SIZE {{ PBUF_POOL_SIZE }} + +// PCB pools: sized to actual component needs via socket.get_socket_counts() +#undef MEMP_NUM_TCP_PCB +#define MEMP_NUM_TCP_PCB {{ MEMP_NUM_TCP_PCB }} + +#undef MEMP_NUM_TCP_PCB_LISTEN +#define MEMP_NUM_TCP_PCB_LISTEN {{ MEMP_NUM_TCP_PCB_LISTEN }} + +#undef MEMP_NUM_UDP_PCB +#define MEMP_NUM_UDP_PCB {{ MEMP_NUM_UDP_PCB }} + +// Listen backlog: match component needs +#undef TCP_DEFAULT_LISTEN_BACKLOG +#define TCP_DEFAULT_LISTEN_BACKLOG {{ MEMP_NUM_TCP_PCB_LISTEN }} diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 33557f03c75..bc4e177219a 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -289,12 +289,12 @@ def final_validate(config): def _consume_wifi_sockets(config: ConfigType) -> ConfigType: """Register UDP PCBs used internally by lwIP for DHCP and DNS. - Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw - PCB pool shared by both application sockets and lwIP internals like DHCP/DNS). - On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer — - DHCP/DNS use raw udp_new() which bypasses it entirely. + Needed on LibreTiny and RP2040 where we directly set MEMP_NUM_UDP_PCB (the + raw PCB pool shared by both application sockets and lwIP internals like + DHCP/DNS). On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket + layer — DHCP/DNS use raw udp_new() which bypasses it entirely. """ - if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x): + if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x or CORE.is_rp2040): return config from esphome.components import socket diff --git a/script/stress_test_connect.py b/script/stress_test_connect.py new file mode 100644 index 00000000000..f91a7e8f996 --- /dev/null +++ b/script/stress_test_connect.py @@ -0,0 +1,84 @@ +"""Rapid connect/disconnect stress test for ESPHome native API.""" + +import asyncio +import sys +import time + +from aioesphomeapi import APIClient + +HOST = "192.168.1.100" +PORT = 6053 +PASSWORD = "" +NOISE_PSK = None +ITERATIONS = 500 +CONCURRENCY = 4 # simultaneous connection attempts + + +async def connect_disconnect(client_id: int, iteration: int) -> tuple[int, bool, str]: + """Connect and immediately disconnect.""" + cli = APIClient(HOST, PORT, PASSWORD, noise_psk=NOISE_PSK) + try: + await asyncio.wait_for(cli.connect(login=True), timeout=10) + await cli.disconnect() + return iteration, True, "" + except Exception as e: + return ( + iteration, + False, + f"client{client_id} iter{iteration}: {type(e).__name__}: {e}", + ) + finally: + await cli.disconnect(force=True) + + +async def main() -> None: + iterations = int(sys.argv[1]) if len(sys.argv) > 1 else ITERATIONS + concurrency = int(sys.argv[2]) if len(sys.argv) > 2 else CONCURRENCY + + print(f"Stress testing {HOST}:{PORT}") + print(f"Iterations: {iterations}, Concurrency: {concurrency}") + print() + + success = 0 + fail = 0 + errors: list[str] = [] + start = time.monotonic() + + sem = asyncio.Semaphore(concurrency) + + async def run(client_id: int, iteration: int) -> tuple[int, bool, str]: + async with sem: + return await connect_disconnect(client_id, iteration) + + tasks = [asyncio.create_task(run(i % concurrency, i)) for i in range(iterations)] + + for coro in asyncio.as_completed(tasks): + iteration, ok, err = await coro + if ok: + success += 1 + else: + fail += 1 + errors.append(err) + total = success + fail + if total % 10 == 0 or not ok: + elapsed = time.monotonic() - start + rate = total / elapsed if elapsed > 0 else 0 + print(f"[{total}/{iterations}] ok={success} fail={fail} ({rate:.1f}/s)") + if err: + print(f" ERROR: {err}") + + elapsed = time.monotonic() - start + print() + print(f"Done in {elapsed:.1f}s") + print(f"Success: {success}, Failed: {fail}, Rate: {iterations / elapsed:.1f}/s") + + if errors: + print("\nLast 10 errors:") + for e in errors[-10:]: + print(f" {e}") + + sys.exit(1 if fail > 0 else 0) + + +if __name__ == "__main__": + asyncio.run(main()) From 699cf9690ab32d374e63d8828440893bb62bc96a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:31:34 +0200 Subject: [PATCH 1579/2056] [core] Optimize value_accuracy_to_buf to avoid snprintf (#15596) --- esphome/core/helpers.cpp | 54 +++- esphome/core/helpers.h | 35 +++ tests/components/core/test_helpers.cpp | 96 +++++++ tests/components/core/test_value_accuracy.cpp | 237 ++++++++++++++++++ 4 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 tests/components/core/test_value_accuracy.cpp diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 113b6f61872..e71da95e6b3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -447,28 +447,58 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de // value_accuracy_to_string moved to alloc_helpers.cpp +// Fast float-to-string for accuracy_decimals 0-3 (covers virtually all sensor usage). +// Avoids snprintf("%.*f") which pulls in heavy float formatting machinery. +// Caller must guarantee value is finite and |value| * mult fits in uint32_t. +static size_t value_accuracy_to_buf_fast(char *buf, float value, int8_t accuracy_decimals, uint32_t mult) { + char *p = buf; + if (std::signbit(value)) { + *p++ = '-'; + value = -value; + } + // Cast to double for the multiply to match snprintf's rounding precision. + // float*int loses bits at exact-half boundaries (e.g. 23.45f*10 = 234.5 in float, + // but snprintf sees 234.500007... via double promotion and rounds differently). + // llrint returns long long so the result fits even on 32-bit targets where + // long is 32-bit; caller has already bounded |value * mult| to UINT32_MAX. + uint32_t scaled = static_cast<uint32_t>(llrint(static_cast<double>(value) * mult)); + p = uint32_to_str_unchecked(p, scaled / mult); + if (accuracy_decimals > 0) { + *p++ = '.'; + p = frac_to_str_unchecked(p, scaled % mult, mult / 10); + } + *p = '\0'; + return static_cast<size_t>(p - buf); +} + size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); - // snprintf returns chars that would be written (excluding null), or negative on error + + // Fast path for accuracy 0-3, finite values whose scaled magnitude fits in uint32_t. + // For 3 decimals that's |value| < ~4.29e6; larger totals fall through to snprintf. + if (accuracy_decimals <= 3 && std::isfinite(value)) { + const uint32_t mult = small_pow10(accuracy_decimals); + if (std::fabs(value) < static_cast<float>(UINT32_MAX) / mult) { + return value_accuracy_to_buf_fast(buf.data(), value, accuracy_decimals, mult); + } + } + + // Fallback for NaN/Inf/high accuracy/out-of-range int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); if (len < 0) - return 0; // encoding error - // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return 0; return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len); } size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { - if (unit_of_measurement.empty()) { - return value_accuracy_to_buf(buf, value, accuracy_decimals); + size_t len = value_accuracy_to_buf(buf, value, accuracy_decimals); + if (len == 0 || unit_of_measurement.empty()) { + return len; } - normalize_accuracy_decimals(value, accuracy_decimals); - // snprintf returns chars that would be written (excluding null), or negative on error - int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); - if (len < 0) - return 0; // encoding error - // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 - return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len); + char *end = buf_append_sep_str(buf.data() + len, buf.size() - len, ' ', unit_of_measurement.c_str(), + unit_of_measurement.size()); + return static_cast<size_t>(end - buf.data()); } int8_t step_to_accuracy_decimals(float step) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 939852bfcb5..4a91c460743 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1311,6 +1311,29 @@ inline char *int8_to_str(char *buf, int8_t val) { return buf; } +/// Append a separator char and a string to a buffer, respecting remaining space. +/// Returns pointer past last char written. The buffer is always null-terminated +/// when remaining >= 1 (even on the no-room early-return), so callers always get +/// a valid C string. +inline char *buf_append_sep_str(char *buf, size_t remaining, char separator, const char *str, size_t str_len) { + if (remaining < 2) { + if (remaining >= 1) { + *buf = '\0'; + } + return buf; + } + *buf++ = separator; + remaining--; + size_t copy_len = std::min(str_len, remaining - 1); + memcpy(buf, str, copy_len); + buf += copy_len; + *buf = '\0'; + return buf; +} + +/// Return 10^n for small non-negative n (0-3) as uint32_t, avoiding float. +inline uint32_t small_pow10(int8_t n) { return n == 3 ? 1000 : n == 2 ? 100 : n == 1 ? 10 : 1; } + /// Minimum buffer size for uint32_to_str: 10 digits + null terminator. static constexpr size_t UINT32_MAX_STR_SIZE = 11; @@ -1326,6 +1349,18 @@ inline size_t uint32_to_str(std::span<char, UINT32_MAX_STR_SIZE> buf, uint32_t v return static_cast<size_t>(end - buf.data()); } +/// Write fractional digits with leading zeros to buffer (internal, no size check). +/// frac is the fractional value, divisor is the highest place value (e.g. 100 for 3 digits). +/// Returns pointer past last char written. +inline char *frac_to_str_unchecked(char *buf, uint32_t frac, uint32_t divisor) { + while (divisor > 0) { + *buf++ = '0' + static_cast<char>(frac / divisor); + frac %= divisor; + divisor /= 10; + } + return buf; +} + /// Format byte array as lowercase hex to buffer (base implementation). char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index 00169621c34..5fb77ef7536 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -117,4 +117,100 @@ TEST(FormatHexChar, UppercaseDigits) { EXPECT_EQ(format_hex_pretty_char(15), 'F'); } +// --- small_pow10() --- + +TEST(SmallPow10, Zero) { EXPECT_EQ(small_pow10(0), 1u); } +TEST(SmallPow10, One) { EXPECT_EQ(small_pow10(1), 10u); } +TEST(SmallPow10, Two) { EXPECT_EQ(small_pow10(2), 100u); } +TEST(SmallPow10, Three) { EXPECT_EQ(small_pow10(3), 1000u); } + +// --- frac_to_str_unchecked() --- + +TEST(FracToStr, OneDigit) { + char buf[8]; + char *end = frac_to_str_unchecked(buf, 5, 1); + *end = '\0'; + EXPECT_STREQ(buf, "5"); + EXPECT_EQ(end - buf, 1); +} + +TEST(FracToStr, TwoDigits) { + char buf[8]; + char *end = frac_to_str_unchecked(buf, 46, 10); + *end = '\0'; + EXPECT_STREQ(buf, "46"); +} + +TEST(FracToStr, ThreeDigits) { + char buf[8]; + char *end = frac_to_str_unchecked(buf, 456, 100); + *end = '\0'; + EXPECT_STREQ(buf, "456"); + EXPECT_EQ(end - buf, 3); +} + +TEST(FracToStr, LeadingZeros) { + char buf[8]; + char *end = frac_to_str_unchecked(buf, 1, 100); + *end = '\0'; + EXPECT_STREQ(buf, "001"); + + end = frac_to_str_unchecked(buf, 5, 10); + *end = '\0'; + EXPECT_STREQ(buf, "05"); +} + +TEST(FracToStr, AllZeros) { + char buf[8]; + char *end = frac_to_str_unchecked(buf, 0, 100); + *end = '\0'; + EXPECT_STREQ(buf, "000"); + + end = frac_to_str_unchecked(buf, 0, 1); + *end = '\0'; + EXPECT_STREQ(buf, "0"); +} + +TEST(FracToStr, ZeroDivisor) { + char buf[8]; + buf[0] = 'X'; + char *end = frac_to_str_unchecked(buf, 0, 0); + EXPECT_EQ(end, buf); // writes nothing +} + +// --- buf_append_sep_str() --- + +TEST(BufAppendSepStr, Basic) { + char buf[32] = "23.46"; + char *start = buf + 5; + char *end = buf_append_sep_str(start, sizeof(buf) - 5, ' ', "°C", 3); + EXPECT_STREQ(buf, "23.46 °C"); + EXPECT_EQ(end - buf, 9); // "°C" is 3 bytes (UTF-8) +} + +TEST(BufAppendSepStr, EmptyString) { + char buf[32] = "100"; + char *start = buf + 3; + char *end = buf_append_sep_str(start, sizeof(buf) - 3, ' ', "", 0); + EXPECT_STREQ(buf, "100 "); + EXPECT_EQ(end - start, 1); // just the separator +} + +TEST(BufAppendSepStr, NoRoom) { + char buf[8] = "1234567"; + char *start = buf + 7; + char *end = buf_append_sep_str(start, 1, ' ', "unit", 4); + EXPECT_EQ(end, start); // nothing written +} + +TEST(BufAppendSepStr, Truncation) { + char buf[8] = "val"; + char *start = buf + 3; + // remaining = 5, separator takes 1, so 3 chars of string fit + null + char *end = buf_append_sep_str(start, 5, ' ', "longunit", 8); + *end = '\0'; + EXPECT_STREQ(buf, "val lon"); + EXPECT_EQ(end - buf, 7); +} + } // namespace esphome::core::testing diff --git a/tests/components/core/test_value_accuracy.cpp b/tests/components/core/test_value_accuracy.cpp new file mode 100644 index 00000000000..381a742a9cd --- /dev/null +++ b/tests/components/core/test_value_accuracy.cpp @@ -0,0 +1,237 @@ +#include <gtest/gtest.h> +#include <cmath> +#include <cstdio> +#include <cstring> +#include <span> +#include <string> + +#include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" + +namespace esphome::core::testing { + +// Helper to call value_accuracy_to_buf and return as string +static std::string va_to_string(float value, int8_t accuracy_decimals) { + char buf[VALUE_ACCURACY_MAX_LEN]; + std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf); + size_t len = value_accuracy_to_buf(sp, value, accuracy_decimals); + return std::string(buf, len); +} + +// Helper: reference implementation using snprintf for comparison +static std::string va_reference(float value, int8_t accuracy_decimals) { + // Replicate normalize_accuracy_decimals logic + if (accuracy_decimals < 0) { + float divisor; + if (accuracy_decimals == -1) { + divisor = 10.0f; + } else if (accuracy_decimals == -2) { + divisor = 100.0f; + } else { + divisor = pow10_int(-accuracy_decimals); + } + value = roundf(value / divisor) * divisor; + accuracy_decimals = 0; + } + char buf[VALUE_ACCURACY_MAX_LEN]; + snprintf(buf, sizeof(buf), "%.*f", accuracy_decimals, value); + return std::string(buf); +} + +// --- Basic formatting --- + +TEST(ValueAccuracyToBuf, ZeroDecimals) { + EXPECT_EQ(va_to_string(23.456f, 0), "23"); + EXPECT_EQ(va_to_string(0.0f, 0), "0"); + EXPECT_EQ(va_to_string(100.0f, 0), "100"); + EXPECT_EQ(va_to_string(1.0f, 0), "1"); +} + +TEST(ValueAccuracyToBuf, OneDecimal) { + EXPECT_EQ(va_to_string(23.456f, 1), "23.5"); + EXPECT_EQ(va_to_string(0.0f, 1), "0.0"); + EXPECT_EQ(va_to_string(1.05f, 1), va_reference(1.05f, 1)); +} + +TEST(ValueAccuracyToBuf, TwoDecimals) { + EXPECT_EQ(va_to_string(23.456f, 2), "23.46"); + EXPECT_EQ(va_to_string(0.0f, 2), "0.00"); + EXPECT_EQ(va_to_string(1.005f, 2), va_reference(1.005f, 2)); +} + +TEST(ValueAccuracyToBuf, ThreeDecimals) { + EXPECT_EQ(va_to_string(23.456f, 3), "23.456"); + EXPECT_EQ(va_to_string(0.0f, 3), "0.000"); +} + +// --- Negative values --- + +TEST(ValueAccuracyToBuf, NegativeValues) { + EXPECT_EQ(va_to_string(-23.456f, 2), "-23.46"); + EXPECT_EQ(va_to_string(-0.5f, 1), "-0.5"); + EXPECT_EQ(va_to_string(-100.0f, 0), "-100"); +} + +// --- Negative accuracy_decimals (rounding to tens/hundreds) --- + +TEST(ValueAccuracyToBuf, NegativeAccuracy) { + EXPECT_EQ(va_to_string(1234.0f, -1), va_reference(1234.0f, -1)); + EXPECT_EQ(va_to_string(1234.0f, -2), va_reference(1234.0f, -2)); + EXPECT_EQ(va_to_string(56.0f, -1), va_reference(56.0f, -1)); +} + +// --- Special float values --- + +TEST(ValueAccuracyToBuf, NaN) { + std::string result = va_to_string(NAN, 2); + EXPECT_EQ(result, va_reference(NAN, 2)); +} + +TEST(ValueAccuracyToBuf, Infinity) { + std::string result = va_to_string(INFINITY, 2); + EXPECT_EQ(result, va_reference(INFINITY, 2)); +} + +TEST(ValueAccuracyToBuf, NegativeInfinity) { + std::string result = va_to_string(-INFINITY, 2); + EXPECT_EQ(result, va_reference(-INFINITY, 2)); +} + +// --- Edge cases --- + +TEST(ValueAccuracyToBuf, VerySmallValues) { + EXPECT_EQ(va_to_string(0.001f, 3), "0.001"); + EXPECT_EQ(va_to_string(0.001f, 2), "0.00"); + EXPECT_EQ(va_to_string(0.009f, 2), "0.01"); +} + +TEST(ValueAccuracyToBuf, LargeValues) { + EXPECT_EQ(va_to_string(999999.0f, 0), va_reference(999999.0f, 0)); + EXPECT_EQ(va_to_string(1013.25f, 2), "1013.25"); +} + +TEST(ValueAccuracyToBuf, Rounding) { + // 0.5 rounds up + EXPECT_EQ(va_to_string(23.5f, 0), "24"); + EXPECT_EQ(va_to_string(23.45f, 1), "23.5"); // float: 23.45 -> 23.4 or 23.5 + EXPECT_EQ(va_to_string(23.45f, 1), va_reference(23.45f, 1)); +} + +// --- Match snprintf for a range of typical sensor values --- + +TEST(ValueAccuracyToBuf, MatchesSnprintf) { + float test_values[] = {0.0f, 1.0f, -1.0f, 23.456f, -23.456f, 100.0f, 0.1f, 0.01f, 99.99f, 1013.25f, -40.0f}; + int8_t test_accuracies[] = {0, 1, 2, 3}; + + for (float value : test_values) { + for (int8_t acc : test_accuracies) { + EXPECT_EQ(va_to_string(value, acc), va_reference(value, acc)) + << "Mismatch for value=" << value << " accuracy=" << static_cast<int>(acc); + } + } +} + +// --- Return value (length) --- + +TEST(ValueAccuracyToBuf, ReturnsCorrectLength) { + char buf[VALUE_ACCURACY_MAX_LEN]; + std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf); + + size_t len = value_accuracy_to_buf(sp, 23.456f, 2); + EXPECT_EQ(len, 5u); // "23.46" + EXPECT_EQ(strlen(buf), len); + + len = value_accuracy_to_buf(sp, 0.0f, 0); + EXPECT_EQ(len, 1u); // "0" + EXPECT_EQ(strlen(buf), len); + + len = value_accuracy_to_buf(sp, -100.0f, 1); + EXPECT_EQ(len, 6u); // "-100.0" + EXPECT_EQ(strlen(buf), len); +} + +TEST(ValueAccuracyToBuf, NegativeZero) { + // Hand-rolled formatter must preserve snprintf's sign-of-zero behavior. + EXPECT_EQ(va_to_string(-0.0f, 2), va_reference(-0.0f, 2)); + EXPECT_EQ(va_to_string(-0.0f, 0), va_reference(-0.0f, 0)); + // Tiny negative that rounds to zero at this precision must still render as "-0.00". + EXPECT_EQ(va_to_string(-0.001f, 2), va_reference(-0.001f, 2)); +} + +TEST(ValueAccuracyToBuf, OverflowFallsBackToSnprintf) { + // |value| * 10^acc must exceed UINT32_MAX to exercise the snprintf fallback path. + EXPECT_EQ(va_to_string(1.0e7f, 3), va_reference(1.0e7f, 3)); + EXPECT_EQ(va_to_string(-1.0e7f, 3), va_reference(-1.0e7f, 3)); + EXPECT_EQ(va_to_string(5.0e9f, 0), va_reference(5.0e9f, 0)); +} + +// --- value_accuracy_with_uom_to_buf --- + +static std::string va_uom_to_string(float value, int8_t accuracy_decimals, const char *uom) { + char buf[VALUE_ACCURACY_MAX_LEN]; + std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf); + StringRef ref(uom); + size_t len = value_accuracy_with_uom_to_buf(sp, value, accuracy_decimals, ref); + return std::string(buf, len); +} + +static std::string va_uom_reference(float value, int8_t accuracy_decimals, const char *uom) { + char buf[VALUE_ACCURACY_MAX_LEN]; + if (!uom || *uom == '\0') { + snprintf(buf, sizeof(buf), "%.*f", accuracy_decimals, value); + } else { + snprintf(buf, sizeof(buf), "%.*f %s", accuracy_decimals, value, uom); + } + return std::string(buf); +} + +TEST(ValueAccuracyWithUomToBuf, BasicWithUnit) { + EXPECT_EQ(va_uom_to_string(23.456f, 2, "°C"), va_uom_reference(23.456f, 2, "°C")); + EXPECT_EQ(va_uom_to_string(1013.25f, 2, "hPa"), va_uom_reference(1013.25f, 2, "hPa")); + EXPECT_EQ(va_uom_to_string(-40.0f, 1, "°F"), va_uom_reference(-40.0f, 1, "°F")); + EXPECT_EQ(va_uom_to_string(100.0f, 0, "%"), va_uom_reference(100.0f, 0, "%")); +} + +TEST(ValueAccuracyWithUomToBuf, EmptyUnit) { + EXPECT_EQ(va_uom_to_string(23.456f, 2, ""), "23.46"); + EXPECT_EQ(va_uom_to_string(0.0f, 1, ""), "0.0"); +} + +TEST(ValueAccuracyWithUomToBuf, ReturnsCorrectLength) { + char buf[VALUE_ACCURACY_MAX_LEN]; + std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf); + StringRef ref("°C"); + size_t len = value_accuracy_with_uom_to_buf(sp, 23.46f, 2, ref); + EXPECT_EQ(strlen(buf), len); + EXPECT_EQ(len, strlen("23.46 °C")); +} + +TEST(ValueAccuracyWithUomToBuf, NearBufferLimitTruncates) { + // Build a unit long enough that value + " " + unit exceeds VALUE_ACCURACY_MAX_LEN. + // "23.46" (5) + " " (1) + unit -> must cap at buf.size()-1 and stay null-terminated. + std::string long_unit(VALUE_ACCURACY_MAX_LEN, 'U'); + char buf[VALUE_ACCURACY_MAX_LEN]; + std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf); + StringRef ref(long_unit.c_str()); + size_t len = value_accuracy_with_uom_to_buf(sp, 23.46f, 2, ref); + EXPECT_LT(len, VALUE_ACCURACY_MAX_LEN); + EXPECT_EQ(strlen(buf), len); + // Should begin with the formatted value and a separator. + EXPECT_EQ(std::string(buf, 6), "23.46 "); +} + +TEST(ValueAccuracyWithUomToBuf, MatchesSnprintf) { + const char *units[] = {"°C", "hPa", "%", "W", "kWh", "m/s"}; + float values[] = {0.0f, 23.456f, -40.0f, 1013.25f, 100.0f}; + int8_t accs[] = {0, 1, 2, 3}; + for (const char *u : units) { + for (float v : values) { + for (int8_t a : accs) { + EXPECT_EQ(va_uom_to_string(v, a, u), va_uom_reference(v, a, u)) + << "value=" << v << " acc=" << static_cast<int>(a) << " uom=" << u; + } + } + } +} + +} // namespace esphome::core::testing From 9c80cbf19c6d604d1906d8e83bced3d67dd5f6dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:34:26 +0200 Subject: [PATCH 1580/2056] [light] Reduce validate_ clamp code size and speed up unit-range clamps (#15728) --- esphome/components/light/light_call.cpp | 63 +++++++++------ esphome/components/light/light_call.h | 39 ++++----- esphome/components/light/light_color_values.h | 80 ++++++++++++++----- 3 files changed, 117 insertions(+), 65 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index a749cd7305a..7b28065e4ea 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -10,13 +10,10 @@ namespace esphome::light { static const char *const TAG = "light"; -// Helper functions to reduce code size for logging -static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, - float max = 1.0f) { - if (value < min || value > max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); - value = clamp(value, min, max); - } +// Cold-path logger; caller handles the clamp so the in-range hot path avoids +// the spill/reload around the call. +static void log_value_out_of_range(const char *name, float value, const LogString *param_name, float min, float max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN @@ -57,6 +54,12 @@ static void log_invalid_parameter(const char *name, const LogString *message) { PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature", "Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white"); +// Indices 0-7 match FieldFlags bits 0-7; index 8 is color_temperature. +// PROGMEM_STRING_TABLE is constexpr-init (no RAM guard variable). +PROGMEM_STRING_TABLE(ValidateFieldNames, "Brightness", "Color brightness", "Red", "Green", "Blue", "White", + "Cold white", "Warm white", "Color temperature"); +static constexpr uint8_t VALIDATE_CT_INDEX = 8; + static const LogString *color_mode_to_human(ColorMode color_mode) { return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0); } @@ -277,25 +280,37 @@ LightColorValues LightCall::validate_() { if (this->has_state()) v.set_state(this->state_); - // clamp_and_log_if_invalid already clamps in-place, so assign directly - // to avoid redundant clamp code from the setter being inlined. -#define VALIDATE_AND_APPLY(field, name_str, ...) \ - if (this->has_##field()) { \ - clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ - v.field##_ = this->field##_; \ + // FieldFlags bits 0-7 must match unit_fields_ array indices. + static_assert(FLAG_HAS_BRIGHTNESS == 1u << 0 && FLAG_HAS_COLOR_BRIGHTNESS == 1u << 1 && FLAG_HAS_RED == 1u << 2 && + FLAG_HAS_GREEN == 1u << 3 && FLAG_HAS_BLUE == 1u << 4 && FLAG_HAS_WHITE == 1u << 5 && + FLAG_HAS_COLD_WHITE == 1u << 6 && FLAG_HAS_WARM_WHITE == 1u << 7, + "FieldFlags bits 0-7 must match unit_fields_ indices"); + + // Iterate set bits only (ctz + clear-lowest) — HA can drive perform() + // at high frequency so the hot path is O(popcount). + unsigned active = this->flags_ & CLAMP_FLAGS_MASK; + while (active != 0) { + unsigned bit = __builtin_ctz(active); + active &= active - 1; // clear lowest set bit + float &value = this->unit_fields_[bit]; + if (float_out_of_unit_range(value)) { + log_value_out_of_range(name, value, ValidateFieldNames::get_log_str(bit, 0), 0.0f, 1.0f); + value = clamp_unit_float(value); + } + v.unit_fields_[bit] = value; } - VALIDATE_AND_APPLY(brightness, "Brightness") - VALIDATE_AND_APPLY(color_brightness, "Color brightness") - VALIDATE_AND_APPLY(red, "Red") - VALIDATE_AND_APPLY(green, "Green") - VALIDATE_AND_APPLY(blue, "Blue") - VALIDATE_AND_APPLY(white, "White") - VALIDATE_AND_APPLY(cold_white, "Cold white") - VALIDATE_AND_APPLY(warm_white, "Warm white") - VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) - -#undef VALIDATE_AND_APPLY + // color_temperature: runtime range from traits. + if (this->has_color_temperature()) { + const float ct_min = traits.get_min_mireds(); + const float ct_max = traits.get_max_mireds(); + if (this->color_temperature_ < ct_min || this->color_temperature_ > ct_max) { + log_value_out_of_range(name, this->color_temperature_, ValidateFieldNames::get_log_str(VALIDATE_CT_INDEX, 0), + ct_min, ct_max); + this->color_temperature_ = clamp(this->color_temperature_, ct_min, ct_max); + } + v.color_temperature_ = this->color_temperature_; + } v.normalize_color(); diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 39953d0d204..e3352de7272 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -195,25 +195,26 @@ class LightCall { /// Some color modes also can be set using non-native parameters, transform those calls. void transform_parameters_(const LightTraits &traits); - // Bitfield flags - each flag indicates whether a corresponding value has been set. + // Bits 0-7 index unit_fields_[] in validate_(); don't reorder (asserts in light_call.cpp). enum FieldFlags : uint16_t { - FLAG_HAS_STATE = 1 << 0, - FLAG_HAS_TRANSITION = 1 << 1, - FLAG_HAS_FLASH = 1 << 2, - FLAG_HAS_EFFECT = 1 << 3, - FLAG_HAS_BRIGHTNESS = 1 << 4, - FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5, - FLAG_HAS_RED = 1 << 6, - FLAG_HAS_GREEN = 1 << 7, - FLAG_HAS_BLUE = 1 << 8, - FLAG_HAS_WHITE = 1 << 9, - FLAG_HAS_COLOR_TEMPERATURE = 1 << 10, - FLAG_HAS_COLD_WHITE = 1 << 11, - FLAG_HAS_WARM_WHITE = 1 << 12, + FLAG_HAS_BRIGHTNESS = 1 << 0, + FLAG_HAS_COLOR_BRIGHTNESS = 1 << 1, + FLAG_HAS_RED = 1 << 2, + FLAG_HAS_GREEN = 1 << 3, + FLAG_HAS_BLUE = 1 << 4, + FLAG_HAS_WHITE = 1 << 5, + FLAG_HAS_COLD_WHITE = 1 << 6, + FLAG_HAS_WARM_WHITE = 1 << 7, + FLAG_HAS_COLOR_TEMPERATURE = 1 << 8, + FLAG_HAS_STATE = 1 << 9, + FLAG_HAS_TRANSITION = 1 << 10, + FLAG_HAS_FLASH = 1 << 11, + FLAG_HAS_EFFECT = 1 << 12, FLAG_HAS_COLOR_MODE = 1 << 13, FLAG_PUBLISH = 1 << 14, FLAG_SAVE = 1 << 15, }; + static constexpr uint16_t CLAMP_FLAGS_MASK = 0x00FFu; // bits 0-7 inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } @@ -239,19 +240,11 @@ class LightCall { LightState *parent_; // Light state values - use flags_ to check if a value has been set. - // Group 4-byte aligned members first uint32_t transition_length_; uint32_t flash_length_; uint32_t effect_; - float brightness_; - float color_brightness_; - float red_; - float green_; - float blue_; - float white_; + ESPHOME_LIGHT_UNIT_FIELDS_UNION(); float color_temperature_; - float cold_white_; - float warm_white_; // Smaller members at the end for better packing uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index fa286a3941b..5cafa9fe827 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -3,11 +3,62 @@ #include "esphome/core/helpers.h" #include "color_mode.h" #include <cmath> +#include <cstdint> +#include <limits> namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roundf(x * 255.0f)); } +// IEEE 754 bit patterns. Values in [0.0f, 1.0f] have bits <= ONE_F_BITS; +// negatives have the sign bit set (→ huge unsigned). A single unsigned compare +// replaces two soft-float __ltsf2/__gtsf2 calls on ESP8266. +static constexpr uint32_t ONE_F_BITS = 0x3F800000u; // 1.0f +static constexpr uint32_t NEG_ZERO_F_BITS = 0x80000000u; // -0.0f / sign-bit mask +static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit"); +static_assert(std::numeric_limits<float>::is_iec559, "IEEE 754 float required"); + +// Union pun — memcpy/bit_cast don't fold on xtensa-gcc (see api/proto.h). +// -0.0f is numerically zero so it's reported in range (no warning, no clamp). +inline bool float_out_of_unit_range(float x) { + union { + float f; + uint32_t u; + } pun; + pun.f = x; + return pun.u > ONE_F_BITS && pun.u != NEG_ZERO_F_BITS; +} + +// Clamps to [0.0f, 1.0f] without float compares. Out of range: sign bit set +// (negatives, -NaN, -Inf) → 0.0f; sign bit clear (>1, +NaN, +Inf) → 1.0f. +inline float clamp_unit_float(float x) { + union { + float f; + uint32_t u; + } pun; + pun.f = x; + if (pun.u <= ONE_F_BITS) + return x; + return (pun.u & NEG_ZERO_F_BITS) ? 0.0f : 1.0f; // sign bit → negative → clamp to 0 +} + +// Shared anonymous union: eight unit-range floats alias unit_fields_[8] so +// LightCall::validate_() can iterate them as a real array. GCC/Clang ext. +#define ESPHOME_LIGHT_UNIT_FIELDS_UNION() \ + union { \ + struct { \ + float brightness_; \ + float color_brightness_; \ + float red_; \ + float green_; \ + float blue_; \ + float white_; \ + float cold_white_; \ + float warm_white_; \ + }; \ + float unit_fields_[8]; \ + } + /** This class represents the color state for a light object. * * The representation of the color state is dependent on the active color mode. A color mode consists of multiple @@ -52,9 +103,9 @@ class LightColorValues { green_(1.0f), blue_(1.0f), white_(1.0f), - color_temperature_{0.0f}, cold_white_{1.0f}, warm_white_{1.0f}, + color_temperature_{0.0f}, color_mode_(ColorMode::UNKNOWN) {} LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, @@ -220,39 +271,39 @@ class LightColorValues { /// Get the binary true/false state of these light color values. bool is_on() const { return this->get_state() != 0.0f; } /// Set the state of these light color values. In range from 0.0 (off) to 1.0 (on) - void set_state(float state) { this->state_ = clamp(state, 0.0f, 1.0f); } + void set_state(float state) { this->state_ = clamp_unit_float(state); } /// Set the state of these light color values as a binary true/false. void set_state(bool state) { this->state_ = state ? 1.0f : 0.0f; } /// Get the brightness property of these light color values. In range 0.0 to 1.0 float get_brightness() const { return this->brightness_; } /// Set the brightness property of these light color values. In range 0.0 to 1.0 - void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); } + void set_brightness(float brightness) { this->brightness_ = clamp_unit_float(brightness); } /// Get the color brightness property of these light color values. In range 0.0 to 1.0 float get_color_brightness() const { return this->color_brightness_; } /// Set the color brightness property of these light color values. In range 0.0 to 1.0 - void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); } + void set_color_brightness(float brightness) { this->color_brightness_ = clamp_unit_float(brightness); } /// Get the red property of these light color values. In range 0.0 to 1.0 float get_red() const { return this->red_; } /// Set the red property of these light color values. In range 0.0 to 1.0 - void set_red(float red) { this->red_ = clamp(red, 0.0f, 1.0f); } + void set_red(float red) { this->red_ = clamp_unit_float(red); } /// Get the green property of these light color values. In range 0.0 to 1.0 float get_green() const { return this->green_; } /// Set the green property of these light color values. In range 0.0 to 1.0 - void set_green(float green) { this->green_ = clamp(green, 0.0f, 1.0f); } + void set_green(float green) { this->green_ = clamp_unit_float(green); } /// Get the blue property of these light color values. In range 0.0 to 1.0 float get_blue() const { return this->blue_; } /// Set the blue property of these light color values. In range 0.0 to 1.0 - void set_blue(float blue) { this->blue_ = clamp(blue, 0.0f, 1.0f); } + void set_blue(float blue) { this->blue_ = clamp_unit_float(blue); } /// Get the white property of these light color values. In range 0.0 to 1.0 float get_white() const { return white_; } /// Set the white property of these light color values. In range 0.0 to 1.0 - void set_white(float white) { this->white_ = clamp(white, 0.0f, 1.0f); } + void set_white(float white) { this->white_ = clamp_unit_float(white); } /// Get the color temperature property of these light color values in mired. float get_color_temperature() const { return this->color_temperature_; } @@ -277,26 +328,19 @@ class LightColorValues { /// Get the cold white property of these light color values. In range 0.0 to 1.0. float get_cold_white() const { return this->cold_white_; } /// Set the cold white property of these light color values. In range 0.0 to 1.0. - void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); } + void set_cold_white(float cold_white) { this->cold_white_ = clamp_unit_float(cold_white); } /// Get the warm white property of these light color values. In range 0.0 to 1.0. float get_warm_white() const { return this->warm_white_; } /// Set the warm white property of these light color values. In range 0.0 to 1.0. - void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } + void set_warm_white(float warm_white) { this->warm_white_ = clamp_unit_float(warm_white); } friend class LightCall; protected: float state_; ///< ON / OFF, float for transition - float brightness_; - float color_brightness_; - float red_; - float green_; - float blue_; - float white_; + ESPHOME_LIGHT_UNIT_FIELDS_UNION(); float color_temperature_; ///< Color Temperature in Mired - float cold_white_; - float warm_white_; ColorMode color_mode_; }; From a3b49d1ed9f2ebdffce9ac1af73b5f6a67660a43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:43:33 +0200 Subject: [PATCH 1581/2056] [core] Use MAC_ADDRESS_BUFFER_SIZE constant instead of duplicated literal (#15913) --- esphome/components/esp32_ble/ble.cpp | 4 +--- esphome/core/application.h | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ebb44c7d91f..6bbf0d6a26b 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -257,11 +257,9 @@ bool ESP32BLE::ble_setup_() { if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; + char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac_addr); const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr, diff --git a/esphome/core/application.h b/esphome/core/application.h index d3851a32da9..e579080c977 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -82,11 +82,9 @@ class Application { void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); this->name_add_mac_suffix_ = true; - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; + char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac_addr); // Overwrite the placeholder suffix in the mutable static buffers with actual MAC // name is always non-empty (validated by validate_hostname in Python config) From 23ad30cb4cbb81aca313cf49b32473a375e5cc56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 06:44:53 +0200 Subject: [PATCH 1582/2056] [esp32] Use xTaskGetTickCount() for millis() when tick rate is 1kHz (#15661) --- esphome/components/esp32/core.cpp | 21 +++++++++++++++- esphome/core/application.cpp | 16 ++++++------ esphome/core/application.h | 2 +- esphome/core/component.h | 3 ++- esphome/core/millis_internal.h | 42 +++++++++++++++++++++++++++++++ esphome/core/scheduler.h | 10 ++++++-- 6 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 esphome/core/millis_internal.h diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index add50dcf4da..1c631371836 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -23,7 +23,26 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { void HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); } +// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), +// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because +// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. +// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract. +uint32_t IRAM_ATTR HOT millis() { +#if CONFIG_FREERTOS_HZ == 1000 + if (xPortInIsrContext()) [[unlikely]] { + return xTaskGetTickCountFromISR(); + } + return xTaskGetTickCount(); +#else + return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); +#endif +} +// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is +// safe because the two are never cross-compared: millis() values are only used for +// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while +// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME), +// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly, +// so the Scheduler is internally consistent on the esp_timer clock. uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b626eb1de67..ea1912d645c 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -78,7 +78,7 @@ void Application::setup() { Component *component = this->components_[i]; // Update loop_component_start_time_ before calling each component during setup - this->loop_component_start_time_ = millis(); + this->loop_component_start_time_ = MillisInternal::get(); component->call(); this->scheduler.process_to_add(); this->feed_wdt(); @@ -91,17 +91,15 @@ void Application::setup() { this->app_state_ |= STATUS_LED_WARNING; do { - uint32_t now = millis(); - // Service scheduler and process pending loop enables to handle GPIO // interrupts during setup. During setup we always run the component // phase (no loop_interval_ gate), so call both helpers unconditionally. - this->scheduler_tick_(now); + this->scheduler_tick_(MillisInternal::get()); this->before_component_phase_(); for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component - this->loop_component_start_time_ = millis(); + this->loop_component_start_time_ = MillisInternal::get(); this->components_[j]->call(); this->feed_wdt(); } @@ -215,7 +213,7 @@ void Application::process_dump_config_() { void Application::feed_wdt() { // Cold entry: callers without a millis() timestamp in hand. Fetches the // time and takes the same rate-limit paths as feed_wdt_with_time(). - uint32_t now = millis(); + uint32_t now = MillisInternal::get(); if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { this->feed_wdt_slow_(now); } @@ -305,7 +303,7 @@ void Application::run_powerdown_hooks() { } void Application::teardown_components(uint32_t timeout_ms) { - uint32_t start_time = millis(); + uint32_t start_time = MillisInternal::get(); // Use a StaticVector instead of std::vector to avoid heap allocation // since we know the actual size at compile time @@ -384,7 +382,7 @@ void Application::teardown_components(uint32_t timeout_ms) { } // Update time for next iteration - now = millis(); + now = MillisInternal::get(); } if (pending_count > 0) { @@ -427,7 +425,7 @@ void Application::disable_component_loop_(Component *component) { // This prevents integer underflow in timing calculations by ensuring // the swapped component starts with a fresh timing reference, avoiding // errors caused by stale or wrapped timing values. - this->loop_component_start_time_ = millis(); + this->loop_component_start_time_ = MillisInternal::get(); } } return; diff --git a/esphome/core/application.h b/esphome/core/application.h index e579080c977..b480e52b2da 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -637,7 +637,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now` // so the gate check and WDT feed both reflect actual elapsed time after // scheduler dispatch, without an extra millis() call. - uint32_t now = this->scheduler_tick_(millis()); + uint32_t now = this->scheduler_tick_(MillisInternal::get()); // Guarantee one WDT feed per tick even when the scheduler had nothing to // dispatch and the component phase is gated out — covers configs with no // looping components and no scheduler work (setup() has its own diff --git a/esphome/core/component.h b/esphome/core/component.h index 67db5423af9..6afcfda41db 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -9,6 +9,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/millis_internal.h" #include "esphome/core/optional.h" // Forward declarations for friend access from codegen-generated setup() @@ -656,7 +657,7 @@ class WarnIfComponentBlockingGuard { #ifdef USE_RUNTIME_STATS this->component_->runtime_stats_.record_time(micros() - this->started_us_); #endif - uint32_t curr_time = millis(); + uint32_t curr_time = MillisInternal::get(); #ifndef USE_BENCHMARK // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U; diff --git a/esphome/core/millis_internal.h b/esphome/core/millis_internal.h new file mode 100644 index 00000000000..6b73476680d --- /dev/null +++ b/esphome/core/millis_internal.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#if defined(USE_ESP32) +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#include <sdkconfig.h> +#endif + +namespace esphome { + +// Friend-gated accessor for a fast millis() variant intended only for +// known task-context callers on the main loop hot path (Application::loop() +// and WarnIfComponentBlockingGuard::finish()). It skips the ISR-context +// dispatch that the public esphome::millis() pays on ESP32. +// +// MUST NOT be called from ISR context: on ESP32 it calls the non-FromISR +// FreeRTOS API directly, which is undefined behavior in ISR context. +// +// Adding new callers requires adding a friend declaration here — that +// is the review point. Do not relax the access (e.g. by making get() +// public) without considering the ISR-safety contract. +// +// Other platforms currently delegate to the public millis(); the friend +// gate still enforces the intent so platform-specific fast paths can be +// added later without changing call sites. +class MillisInternal { + private: + static ESPHOME_ALWAYS_INLINE uint32_t get() { +#if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000 + return xTaskGetTickCount(); +#else + return millis(); +#endif + } + friend class Application; + friend class WarnIfComponentBlockingGuard; +}; + +} // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index b0ce365a6f8..b7e99d46030 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -285,8 +285,14 @@ class Scheduler { bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. - // On platforms with native 64-bit time, ignores now and uses millis_64() directly. - // On other platforms, extends now to 64-bit using rollover tracking. + // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040 — see + // USE_NATIVE_64BIT_TIME in defines.h), ignores now and uses millis_64() directly, so the + // Scheduler always works in 64-bit time regardless of what the caller's 32-bit now came + // from. On ESP32 specifically, millis() comes from xTaskGetTickCount while millis_64() + // comes from esp_timer — two different clocks — but that is safe because scheduling + // compares millis_64 values against millis_64 only, never against millis(). + // On platforms without native 64-bit time (e.g. ESP8266), extends now to 64-bit using + // rollover tracking, so both millis() and scheduling use the same underlying clock. uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) { #ifdef USE_NATIVE_64BIT_TIME (void) now; From 5218bbd7919225a946cdfa1b5409d999f5f163a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:19:47 +0200 Subject: [PATCH 1583/2056] Update argcomplete requirement from >=2.0.0 to >=3.6.3 (#15921) 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 90f06eff98a..68557614d9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,4 @@ requests==2.33.1 pyparsing >= 3.3.2 # For autocompletion -argcomplete>=2.0.0 +argcomplete>=3.6.3 From 73714dc489a04ae5e17ea546cacc0c64c07face1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:26:25 +0200 Subject: [PATCH 1584/2056] Bump aioesphomeapi from 44.18.0 to 44.19.0 (#15920) 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 68557614d9c..9e59bb59d00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.2 esphome-dashboard==20260408.1 -aioesphomeapi==44.18.0 +aioesphomeapi==44.19.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 886cd7ab725538dadd46a973b7c97594b45b8a6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 13:47:01 +0200 Subject: [PATCH 1585/2056] [core] Collapse adjacent USE_HOST ifdef blocks in Application (#15914) --- esphome/core/application.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index b480e52b2da..813f1ca8ed4 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -487,9 +487,6 @@ class Application { #ifdef USE_HOST std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors #endif -#ifdef USE_HOST - int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif // StringRef members (8 bytes each: pointer + size) StringRef name_; @@ -505,7 +502,8 @@ class Application { #endif #ifdef USE_HOST - int max_fd_{-1}; // Highest file descriptor number for select() + int max_fd_{-1}; // Highest file descriptor number for select() + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif // 2-byte members (grouped together for alignment) @@ -522,9 +520,7 @@ class Application { #ifdef USE_HOST bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes -#endif -#ifdef USE_HOST // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes From e35b435f027784f0a848066f2946b08c871db746 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 13:52:27 +0200 Subject: [PATCH 1586/2056] [libretiny] Inline xTaskGetTickCount() for millis() fast path (#15918) --- esphome/components/libretiny/core.cpp | 23 ++++++++++++++++++++++- esphome/core/millis_internal.h | 20 +++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 1cfe68e9241..1b74e3addb4 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -16,8 +16,29 @@ void loop(); namespace esphome { void HOT yield() { ::yield(); } +// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast +// path instead of going through the Arduino core's out-of-line ::millis() wrapper. +// +// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR +// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis(). +// +// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch +// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h), +// so no ISR runs while flash is stalled. +#if defined(USE_RTL87XX) || defined(USE_LN882X) +uint32_t IRAM_ATTR HOT millis() { + static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); + return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); +} +#elif defined(USE_BK72XX) +uint32_t HOT millis() { + static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); + return xTaskGetTickCount() * portTICK_PERIOD_MS; +} +#else uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return Millis64Impl::compute(::millis()); } +#endif +uint64_t millis_64() { return Millis64Impl::compute(millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/core/millis_internal.h b/esphome/core/millis_internal.h index 6b73476680d..bc1d55a1c4b 100644 --- a/esphome/core/millis_internal.h +++ b/esphome/core/millis_internal.h @@ -7,6 +7,9 @@ #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <sdkconfig.h> +#elif defined(USE_LIBRETINY) +#include <FreeRTOS.h> +#include <task.h> #endif namespace esphome { @@ -14,10 +17,11 @@ namespace esphome { // Friend-gated accessor for a fast millis() variant intended only for // known task-context callers on the main loop hot path (Application::loop() // and WarnIfComponentBlockingGuard::finish()). It skips the ISR-context -// dispatch that the public esphome::millis() pays on ESP32. +// dispatch that the public esphome::millis() pays on ESP32 and libretiny. // -// MUST NOT be called from ISR context: on ESP32 it calls the non-FromISR -// FreeRTOS API directly, which is undefined behavior in ISR context. +// MUST NOT be called from ISR context: on ESP32 and libretiny it calls the +// non-FromISR FreeRTOS API directly, which is undefined behavior in ISR +// context. // // Adding new callers requires adding a friend declaration here — that // is the review point. Do not relax the access (e.g. by making get() @@ -31,6 +35,16 @@ class MillisInternal { static ESPHOME_ALWAYS_INLINE uint32_t get() { #if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000 return xTaskGetTickCount(); +#elif defined(USE_LIBRETINY) && (defined(USE_RTL87XX) || defined(USE_LN882X)) + // 1 kHz: xTaskGetTickCount() is already ms. + static_assert(configTICK_RATE_HZ == 1000, "MillisInternal fast path requires 1 kHz FreeRTOS tick"); + return xTaskGetTickCount(); +#elif defined(USE_BK72XX) + // 500 Hz: scale by portTICK_PERIOD_MS (== 2). Inlined to avoid the + // out-of-line call to esphome::millis() (IRAM_ATTR is a no-op on BK72xx — + // SDK masks FIQ + IRQ during flash writes, see hal.h). + static_assert(configTICK_RATE_HZ == 500, "BK72xx MillisInternal assumes 500 Hz FreeRTOS tick"); + return xTaskGetTickCount() * portTICK_PERIOD_MS; #else return millis(); #endif From f6bf6dc8e5ceb33a1acc40a320d9370894244985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 13:52:40 +0200 Subject: [PATCH 1587/2056] [core] Dedupe yield() fast path in wakeable_delay and always-inline (#15915) --- esphome/core/application.h | 12 ------------ esphome/core/wake.cpp | 2 +- esphome/core/wake.h | 18 ++++++++++++------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 813f1ca8ed4..aad25c75307 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -748,18 +748,6 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Inline yield_with_select_ for all paths except the select() fallback #ifndef USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { -#ifdef USE_LWIP_FAST_SELECT - // Fast path (ESP32/LibreTiny): FreeRTOS task notifications posted by the lwip - // event_callback wrapper (see lwip_fast_select.c) are the single source of truth for - // socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification - // that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns - // immediately) or wakes a blocked Take directly. Additional wake sources: - // wake_loop_threadsafe() from background tasks, and the delay_ms timeout. - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } -#endif esphome::internal::wakeable_delay(delay_ms); } #endif // !USE_HOST diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index cebc4d04b7f..00b08b7b915 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -58,7 +58,7 @@ static int64_t alarm_callback_(alarm_id_t id, void *user_data) { namespace internal { void wakeable_delay(uint32_t ms) { - if (ms == 0) { + if (ms == 0) [[unlikely]] { yield(); return; } diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 41b7ab33b51..15b882b3062 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -96,8 +96,14 @@ inline void wake_loop_threadsafe() { } namespace internal { -inline void wakeable_delay(uint32_t ms) { - if (ms == 0) { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + // Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip + // event_callback wrapper (see lwip_fast_select.c) are the single source of truth for + // socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification + // that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns + // immediately) or wakes a blocked Take directly. Additional wake sources: + // wake_loop_threadsafe() from background tasks, and the ms timeout. + if (ms == 0) [[unlikely]] { yield(); return; } @@ -127,8 +133,8 @@ inline void wake_loop_threadsafe() { wake_loop_impl(); } inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); } namespace internal { -inline void wakeable_delay(uint32_t ms) { - if (ms == 0) { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { delay(0); return; } @@ -174,8 +180,8 @@ inline void wake_loop_threadsafe() {} inline void wake_loop_any_context() { wake_loop_threadsafe(); } namespace internal { -inline void wakeable_delay(uint32_t ms) { - if (ms == 0) { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { yield(); return; } From c399cd2fa29c2ab7a14f4fd19e0d93e9243c7948 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 22 Apr 2026 14:04:29 +0200 Subject: [PATCH 1588/2056] [core] RAII guard for component loop phase (#15897) --- esphome/core/application.cpp | 16 ++++++++-------- esphome/core/application.h | 30 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index ea1912d645c..8612782d952 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -95,16 +95,16 @@ void Application::setup() { // interrupts during setup. During setup we always run the component // phase (no loop_interval_ gate), so call both helpers unconditionally. this->scheduler_tick_(MillisInternal::get()); - this->before_component_phase_(); + { + ComponentPhaseGuard phase_guard{*this}; - for (uint32_t j = 0; j <= i; j++) { - // Update loop_component_start_time_ right before calling each component - this->loop_component_start_time_ = MillisInternal::get(); - this->components_[j]->call(); - this->feed_wdt(); + for (uint32_t j = 0; j <= i; j++) { + // Update loop_component_start_time_ right before calling each component + this->loop_component_start_time_ = MillisInternal::get(); + this->components_[j]->call(); + this->feed_wdt(); + } } - - this->after_component_phase_(); yield(); } while (!component->can_proceed() && !component->is_failed()); } diff --git a/esphome/core/application.h b/esphome/core/application.h index aad25c75307..3d8df88d2a2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -425,8 +425,20 @@ class Application { void enable_pending_loops_(); void activate_looping_component_(uint16_t index); inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now); - inline void ESPHOME_ALWAYS_INLINE before_component_phase_(); - inline void ESPHOME_ALWAYS_INLINE after_component_phase_() { this->in_loop_ = false; } + + // RAII guard for a component loop phase. Constructor processes any pending + // enable_loop requests from ISRs and marks in_loop_ so reentrant + // modifications during component.loop() are safe; destructor clears in_loop_. + class ComponentPhaseGuard { + public: + inline ESPHOME_ALWAYS_INLINE explicit ComponentPhaseGuard(Application &app); + inline ESPHOME_ALWAYS_INLINE ~ComponentPhaseGuard() { this->app_.in_loop_ = false; } + ComponentPhaseGuard(const ComponentPhaseGuard &) = delete; + ComponentPhaseGuard &operator=(const ComponentPhaseGuard &) = delete; + + private: + Application &app_; + }; /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -595,10 +607,10 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) // Phase B entry: only invoked when a component loop phase is about to run. // Processes pending enable_loop requests from ISRs and marks in_loop_ so // reentrant modifications during component.loop() are safe. -inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() { +inline ESPHOME_ALWAYS_INLINE Application::ComponentPhaseGuard::ComponentPhaseGuard(Application &app) : app_(app) { // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { + if (this->app_.has_pending_enable_loop_requests_) { // Clear flag BEFORE processing to avoid race condition // If ISR sets it during processing, we'll catch it next loop iteration // This is safe because: @@ -606,12 +618,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() { // 2. If we can't process a component (wrong state), enable_pending_loops_() // will set this flag back to true // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); + this->app_.has_pending_enable_loop_requests_ = false; + this->app_.enable_pending_loops_(); } // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; + this->app_.in_loop_ = true; } inline void ESPHOME_ALWAYS_INLINE Application::loop() { @@ -665,7 +677,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_); if (do_component_phase) { - this->before_component_phase_(); + ComponentPhaseGuard phase_guard{*this}; uint32_t last_op_end_time = now; for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; @@ -690,7 +702,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { #endif this->last_loop_ = last_op_end_time; now = last_op_end_time; - this->after_component_phase_(); + // phase_guard destructor clears in_loop_ at scope exit } #ifdef USE_RUNTIME_STATS From d5263cd46e9ef2d1e33bf926ba74dea55b5edcca Mon Sep 17 00:00:00 2001 From: rwrozelle <rwrozelle@gmail.com> Date: Wed, 22 Apr 2026 09:01:23 -0400 Subject: [PATCH 1589/2056] [esp32] add watchdog_timeout configuration variable (#15908) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/esp32/__init__.py | 9 +++++++++ tests/components/esp32/test.esp32-idf.yaml | 1 + 2 files changed, 10 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 77b405a4497..1a7ae700c78 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -33,6 +33,7 @@ from esphome.const import ( CONF_TYPE, CONF_VARIANT, CONF_VERSION, + CONF_WATCHDOG_TIMEOUT, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, @@ -1507,6 +1508,10 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, + cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)), + ), } ), _detect_variant, @@ -1874,6 +1879,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + add_idf_sdkconfig_option( + "CONFIG_ESP_TASK_WDT_TIMEOUT_S", + config[CONF_WATCHDOG_TIMEOUT].total_seconds, + ) # Disable dynamic log level control to save memory add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index b999f23e1c2..6b77a4e171d 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -20,6 +20,7 @@ esp32: disable_regi2c_in_iram: true disable_fatfs: true sram1_as_iram: true + watchdog_timeout: 7s wifi: ssid: MySSID From 5e715692d600a3d3a67f104901aea691026b2fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <lboue@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:01:20 +0200 Subject: [PATCH 1590/2056] [network] Reorder IPv6 configuration for network components (#11694) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/network/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1f75b12178a..811e7c875a0 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -109,21 +109,21 @@ CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( CONF_ENABLE_IPV6, - esp8266=False, - esp32=False, - rp2040=False, bk72xx=False, + esp32=False, + esp8266=False, host=False, + rp2040=False, ): cv.All( cv.boolean, cv.Any( cv.require_framework_version( + bk72xx_arduino=cv.Version(1, 7, 0), esp_idf=cv.Version(0, 0, 0), esp32_arduino=cv.Version(0, 0, 0), esp8266_arduino=cv.Version(0, 0, 0), - rp2040_arduino=cv.Version(0, 0, 0), - bk72xx_arduino=cv.Version(1, 7, 0), host=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), ), cv.boolean_false, ), @@ -218,9 +218,9 @@ async def to_code(config): elif enable_ipv6: cg.add_build_flag("-DCONFIG_LWIP_IPV6") cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") - if CORE.is_rp2040: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") - if CORE.is_esp8266: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") if CORE.is_bk72xx: cg.add_build_flag("-DCONFIG_IPV6") + if CORE.is_esp8266: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + if CORE.is_rp2040: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") From dcd103cec0e3dd95bda7beb794e1439553291d27 Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:11:18 +0200 Subject: [PATCH 1591/2056] [cse7761] bidirectional active power (#15162) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/cse7761/cse7761.cpp | 13 ++++++++----- esphome/components/cse7761/cse7761.h | 4 +--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 7525b901f8b..0ecaaced7f6 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -204,24 +204,27 @@ void CSE7761Component::get_data_() { value = this->read_(CSE7761_REG_RMSIA, 3); this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA value = this->read_(CSE7761_REG_POWERPA, 4); - this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value)); + // PowerPA is two's complement signed 32-bit per datasheet + this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : static_cast<int32_t>(value); value = this->read_(CSE7761_REG_RMSIB, 3); this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA value = this->read_(CSE7761_REG_POWERPB, 4); - this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value)); + // PowerPB is two's complement signed 32-bit per datasheet + this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : static_cast<int32_t>(value); // convert values and publish to sensors - float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC); + float voltage = static_cast<float>(this->data_.voltage_rms) / this->coefficient_by_unit_(RMS_UC); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); } for (uint8_t channel = 0; channel < 2; channel++) { // Active power = PowerPA * PowerPAC * 1000 / 0x80000000 - float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W - float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A + float active_power = + static_cast<float>(this->data_.active_power[channel]) / this->coefficient_by_unit_(POWER_PAC); // W + float amps = static_cast<float>(this->data_.current_rms[channel]) / this->coefficient_by_unit_(RMS_IAC); // A ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps); if (channel == 0) { if (this->power_sensor_1_ != nullptr) { diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 289c5e7e197..0e03171956f 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -11,10 +11,8 @@ struct CSE7761DataStruct { uint32_t frequency = 0; uint32_t voltage_rms = 0; uint32_t current_rms[2] = {0}; - uint32_t energy[2] = {0}; - uint32_t active_power[2] = {0}; + int32_t active_power[2] = {0}; uint16_t coefficient[8] = {0}; - uint8_t energy_update = 0; bool ready = false; }; From fcbc4d64fe059b8ffc7872c7cbea2285aac20c89 Mon Sep 17 00:00:00 2001 From: Michael Turner <mikeyt@pobox.com> Date: Wed, 22 Apr 2026 10:20:02 -0700 Subject: [PATCH 1592/2056] [one_wire] Reset bus before SKIP ROM command (#14669) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/one_wire/one_wire_bus.cpp | 5 ++++- esphome/components/one_wire/one_wire_bus.h | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index 27b7d58a0f8..99e1f352fb5 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -57,8 +57,11 @@ void OneWireBus::search() { } } -void OneWireBus::skip() { +bool OneWireBus::skip() { + if (!this->reset_()) + return false; this->write8(0xCC); // skip ROM + return true; } const LogString *OneWireBus::get_model_str(uint8_t model) { diff --git a/esphome/components/one_wire/one_wire_bus.h b/esphome/components/one_wire/one_wire_bus.h index c88532046fb..6302fcee7b9 100644 --- a/esphome/components/one_wire/one_wire_bus.h +++ b/esphome/components/one_wire/one_wire_bus.h @@ -16,7 +16,8 @@ class OneWireBus { virtual void write64(uint64_t val) = 0; /// Write a command to the bus that addresses all devices by skipping the ROM. - void skip(); + /// Returns true if a device presence pulse is detected. + bool skip(); /// Read an 8 bit word from the bus. virtual uint8_t read8() = 0; From ea2e36e55a732253355d02b782e423ab60c39cf7 Mon Sep 17 00:00:00 2001 From: PolarGoose <35307286+PolarGoose@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:49:14 +0200 Subject: [PATCH 1593/2056] [dsmr] Improve performance. Add missing sensors. Remove Crypto-no-arduino. (#15875) --- .clang-tidy.hash | 2 +- esphome/components/dsmr/__init__.py | 65 +++- esphome/components/dsmr/dsmr.cpp | 407 +++++++------------- esphome/components/dsmr/dsmr.h | 129 ++++--- esphome/components/dsmr/sensor.py | 81 ++++ esphome/components/dsmr/text_sensor.py | 3 + platformio.ini | 3 +- tests/components/dsmr/test.esp32-ard.yaml | 7 + tests/components/dsmr/test.esp32-idf.yaml | 14 + tests/components/dsmr/test.esp8266-ard.yaml | 7 + 10 files changed, 369 insertions(+), 349 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 02aa990809e..9b6b8176334 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9 +256216e144a626c8c9d1a458920a9db3de7dfc8c6a1b44b87946b9752e81026c diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 9c493bfcff1..31ec1ce5b52 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -1,8 +1,19 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID +from esphome.const import ( + CONF_ID, + CONF_RECEIVE_TIMEOUT, + CONF_RX_BUFFER_SIZE, + CONF_UART_ID, +) +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@glmnet", "@PolarGoose"] @@ -21,8 +32,7 @@ CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" -# Hack to prevent compile error due to ambiguity with lib namespace -dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") +dsmr_ns = cg.esphome_ns.namespace("dsmr") Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) @@ -54,24 +64,47 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): uart_component = await cg.get_variable(config[CONF_UART_ID]) - var = cg.new_Pvariable(config[CONF_ID], uart_component, config[CONF_CRC_CHECK]) - cg.add(var.set_max_telegram_length(config[CONF_MAX_TELEGRAM_LENGTH])) - if CONF_DECRYPTION_KEY in config: - cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) - await cg.register_component(var, config) - if CONF_REQUEST_PIN in config: request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN]) - cg.add(var.set_request_pin(request_pin)) - cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) - cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) + else: + request_pin = cg.nullptr + decryption_key = config.get(CONF_DECRYPTION_KEY) + if decryption_key is None: + decryption_key = cg.nullptr + var = cg.new_Pvariable( + config[CONF_ID], + uart_component, + config[CONF_CRC_CHECK], + config[CONF_MAX_TELEGRAM_LENGTH], + config[CONF_REQUEST_INTERVAL].total_milliseconds, + config[CONF_RECEIVE_TIMEOUT].total_milliseconds, + request_pin, + decryption_key, + ) + await cg.register_component(var, config) cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID])) - # DSMR Parser - cg.add_library("esphome/dsmr_parser", "1.1.0") + cg.add_library("esphome/dsmr_parser", "1.4.0") - # Crypto - cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") + +def final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + + for uart_conf in full_config["uart"]: + if uart_conf[CONF_ID] == config[CONF_UART_ID]: + rx_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE] + if rx_buffer_size < 1500: + _LOGGER.warning( + "UART '%s' rx_buffer_size should be bigger than 1500 bytes to avoid packet losses (currently %d bytes).", + config[CONF_UART_ID], + rx_buffer_size, + ) + break + + return config + + +FINAL_VALIDATE_SCHEMA = final_validate diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index baf7f593147..2fa51f73af5 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -1,315 +1,183 @@ -#include "dsmr.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" +// Ignore Zephyr. It doesn't have any encryption library. +#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST) -#include <AES.h> -#include <Crypto.h> -#include <GCM.h> +#include "dsmr.h" +#include "esphome/core/log.h" +#include <dsmr_parser/util.h> namespace esphome::dsmr { -static const char *const TAG = "dsmr"; +static constexpr auto &TAG = "dsmr"; + +static void log_callback(dsmr_parser::LogLevel level, const char *fmt, va_list args) { + std::array<char, 256> buf; + vsnprintf(buf.data(), buf.size(), fmt, args); + switch (level) { + case dsmr_parser::LogLevel::ERROR: + ESP_LOGE(TAG, "%s", buf.data()); + break; + case dsmr_parser::LogLevel::WARNING: + ESP_LOGW(TAG, "%s", buf.data()); + break; + case dsmr_parser::LogLevel::INFO: + ESP_LOGI(TAG, "%s", buf.data()); + break; + case dsmr_parser::LogLevel::VERBOSE: + ESP_LOGV(TAG, "%s", buf.data()); + break; + case dsmr_parser::LogLevel::VERY_VERBOSE: + ESP_LOGVV(TAG, "%s", buf.data()); + break; + case dsmr_parser::LogLevel::DEBUG: + ESP_LOGD(TAG, "%s", buf.data()); + break; + } +} void Dsmr::setup() { - this->telegram_ = new char[this->max_telegram_len_]; // NOLINT + dsmr_parser::Logger::set_log_function(log_callback); if (this->request_pin_ != nullptr) { this->request_pin_->setup(); } } void Dsmr::loop() { - if (this->ready_to_request_data_()) { - if (this->decryption_key_.empty()) { - this->receive_telegram_(); - } else { - this->receive_encrypted_telegram_(); - } + if (!this->ready_to_request_data_()) { + return; + } + + if (this->encryption_enabled_) { + this->receive_encrypted_telegram_(); + } else { + this->receive_telegram_(); } } bool Dsmr::ready_to_request_data_() { - // When using a request pin, then wait for the next request interval. - if (this->request_pin_ != nullptr) { - if (!this->requesting_data_ && this->request_interval_reached_()) { - this->start_requesting_data_(); - } - } - // Otherwise, sink serial data until next request interval. - else { - if (this->request_interval_reached_()) { - this->start_requesting_data_(); - } - if (!this->requesting_data_) { - this->drain_rx_buffer_(); - } + if (!this->requesting_data_ && this->request_interval_reached_()) { + this->start_requesting_data_(); } return this->requesting_data_; } -bool Dsmr::request_interval_reached_() { +bool Dsmr::request_interval_reached_() const { if (this->last_request_time_ == 0) { return true; } return millis() - this->last_request_time_ > this->request_interval_; } -bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; } - -bool Dsmr::available_within_timeout_() { - // Data are available for reading on the UART bus? - // Then we can start reading right away. - if (this->available()) { - this->last_read_time_ = millis(); - return true; - } - // When we're not in the process of reading a telegram, then there is - // no need to actively wait for new data to come in. - if (!header_found_) { - return false; - } - // A telegram is being read. The smart meter might not deliver a telegram - // in one go, but instead send it in chunks with small pauses in between. - // When the UART RX buffer cannot hold a full telegram, then make sure - // that the UART read buffer does not overflow while other components - // perform their work in their loop. Do this by not returning control to - // the main loop, until the read timeout is reached. - if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) { - while (!this->receive_timeout_reached_()) { - delay(5); - if (this->available()) { - this->last_read_time_ = millis(); - return true; - } - } - } - // No new data has come in during the read timeout? Then stop reading the - // telegram and start waiting for the next one to arrive. - if (this->receive_timeout_reached_()) { - ESP_LOGW(TAG, "Timeout while reading data for telegram"); - this->reset_telegram_(); - } - - return false; -} - void Dsmr::start_requesting_data_() { - if (!this->requesting_data_) { - if (this->request_pin_ != nullptr) { - ESP_LOGV(TAG, "Start requesting data from P1 port"); - this->request_pin_->digital_write(true); - } else { - ESP_LOGV(TAG, "Start reading data from P1 port"); - } - this->requesting_data_ = true; - this->last_request_time_ = millis(); + if (this->requesting_data_) { + return; } + + ESP_LOGV(TAG, "Start reading data from P1 port"); + this->flush_rx_buffer_(); + + if (this->request_pin_ != nullptr) { + ESP_LOGV(TAG, "Set request pin to 1"); + this->request_pin_->digital_write(true); + } + + this->requesting_data_ = true; + this->last_request_time_ = millis(); } void Dsmr::stop_requesting_data_() { - if (this->requesting_data_) { - if (this->request_pin_ != nullptr) { - ESP_LOGV(TAG, "Stop requesting data from P1 port"); - this->request_pin_->digital_write(false); - } else { - ESP_LOGV(TAG, "Stop reading data from P1 port"); - } - this->drain_rx_buffer_(); - this->requesting_data_ = false; + if (!this->requesting_data_) { + return; } + + ESP_LOGV(TAG, "Stop reading data from P1 port"); + if (this->request_pin_ != nullptr) { + ESP_LOGV(TAG, "Set request pin to 0"); + this->request_pin_->digital_write(false); + } + this->requesting_data_ = false; } -void Dsmr::drain_rx_buffer_() { - uint8_t buf[64]; - size_t avail; - while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { - break; - } +void Dsmr::flush_rx_buffer_() { + ESP_LOGV(TAG, "Flush UART RX buffer"); + while (!this->uart_read_chunk_().empty()) { } } -void Dsmr::reset_telegram_() { - this->header_found_ = false; - this->footer_found_ = false; - this->bytes_read_ = 0; - this->crypt_bytes_read_ = 0; - this->crypt_telegram_len_ = 0; -} - void Dsmr::receive_telegram_() { - while (this->available_within_timeout_()) { - // Read all available bytes in batches to reduce UART call overhead. - uint8_t buf[64]; - size_t avail = this->available(); - while (avail > 0) { - size_t to_read = std::min(avail, sizeof(buf)); - if (!this->read_array(buf, to_read)) + for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) { + for (uint8_t byte : data) { + const auto telegram = this->packet_accumulator_.process_byte(byte); + if (!telegram) { // No full packet received yet + continue; + } + if (this->parse_telegram_(telegram.value())) { return; - avail -= to_read; - - for (size_t i = 0; i < to_read; i++) { - const char c = static_cast<char>(buf[i]); - - // Find a new telegram header, i.e. forward slash. - if (c == '/') { - ESP_LOGV(TAG, "Header of telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - if (!this->header_found_) - continue; - - // Check for buffer overflow. - if (this->bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Some v2.2 or v3 meters will send a new value which starts with '(' - // in a new line, while the value belongs to the previous ObisId. For - // proper parsing, remove these new line characters. - if (c == '(') { - while (true) { - auto previous_char = this->telegram_[this->bytes_read_ - 1]; - if (previous_char == '\n' || previous_char == '\r') { - this->bytes_read_--; - } else { - break; - } - } - } - - // Store the byte in the buffer. - this->telegram_[this->bytes_read_] = c; - this->bytes_read_++; - - // Check for a footer, i.e. exclamation mark, followed by a hex checksum. - if (c == '!') { - ESP_LOGV(TAG, "Footer of telegram found"); - this->footer_found_ = true; - continue; - } - // Check for the end of the hex checksum, i.e. a newline. - if (this->footer_found_ && c == '\n') { - // Parse the telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } } } } } void Dsmr::receive_encrypted_telegram_() { - while (this->available_within_timeout_()) { - // Read all available bytes in batches to reduce UART call overhead. - uint8_t buf[64]; - size_t avail = this->available(); - while (avail > 0) { - size_t to_read = std::min(avail, sizeof(buf)); - if (!this->read_array(buf, to_read)) - return; - avail -= to_read; - - for (size_t i = 0; i < to_read; i++) { - const char c = static_cast<char>(buf[i]); - - // Find a new telegram start byte. - if (!this->header_found_) { - if ((uint8_t) c != 0xDB) { - continue; - } - ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - - // Check for buffer overflow. - if (this->crypt_bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Store the byte in the buffer. - this->crypt_telegram_[this->crypt_bytes_read_] = c; - this->crypt_bytes_read_++; - - // Read the length of the incoming encrypted telegram. - if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { - // Complete header + data bytes - this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); - } - - // Check for the end of the encrypted telegram. - if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { - continue; - } - ESP_LOGV(TAG, "End of encrypted telegram found"); - - // Decrypt the encrypted telegram. - GCM<AES128> *gcmaes128{new GCM<AES128>()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_), - // the ciphertext start at byte 18 - &this->crypt_telegram_[18], - // cipher size - this->crypt_bytes_read_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - // Parse the decrypted telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; + for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) { + for (uint8_t byte : data) { + if (this->buffer_pos_ >= this->buffer_.size()) { // Reset buffer if overflow + ESP_LOGW(TAG, "Encrypted buffer overflow, resetting"); + this->buffer_pos_ = 0; } + + this->buffer_[this->buffer_pos_] = byte; + this->buffer_pos_++; } + this->last_read_time_ = millis(); + } + + // Detect inter-frame delay. If no byte is received for more than receive_timeout, then the packet is complete. + if (millis() - this->last_read_time_ > this->receive_timeout_ && this->buffer_pos_ > 0) { + ESP_LOGV(TAG, "Encrypted telegram received (%zu bytes)", this->buffer_pos_); + + const auto telegram = this->dlms_decryptor_.decrypt_inplace({this->buffer_.data(), this->buffer_pos_}); + + // Reset buffer position for the next packet + this->buffer_pos_ = 0; + this->last_read_time_ = 0; + + if (!telegram) { // decryption failed + return; + } + + // Parse and publish the telegram + this->parse_telegram_(telegram.value()); } } -bool Dsmr::parse_telegram() { - MyData data; - ESP_LOGV(TAG, "Trying to parse telegram"); +bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) { this->stop_requesting_data_(); - const auto &res = dsmr_parser::P1Parser::parse( - data, this->telegram_, this->bytes_read_, false, - this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. - if (res.err) { - // Parsing error, show it - auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_); - ESP_LOGE(TAG, "%s", err_str.c_str()); - return false; - } else { - this->status_clear_warning(); - this->publish_sensors(data); + ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size()); + ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data()); - // publish the telegram, after publishing the sensors so it can also trigger action based on latest values - if (this->s_telegram_ != nullptr) { - this->s_telegram_->publish_state(this->telegram_, this->bytes_read_); - } - return true; + MyData data; + if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) { + ESP_LOGE(TAG, "Failed to parse telegram"); + return false; } + + this->status_clear_warning(); + this->publish_sensors(data); + + // Publish the telegram, after publishing the sensors so it can also trigger action based on latest values + if (this->s_telegram_ != nullptr) { + this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size()); + } + return true; } void Dsmr::dump_config() { ESP_LOGCONFIG(TAG, "DSMR:\n" - " Max telegram length: %d\n" + " Max telegram length: %zu\n" " Receive timeout: %.1fs", - this->max_telegram_len_, this->receive_timeout_ / 1e3f); + this->buffer_.size(), this->receive_timeout_ / 1e3f); if (this->request_pin_ != nullptr) { LOG_PIN(" Request Pin: ", this->request_pin_); } @@ -324,30 +192,37 @@ void Dsmr::dump_config() { DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) } -void Dsmr::set_decryption_key(const char *decryption_key) { +void Dsmr::set_decryption_key_(const char *decryption_key) { if (decryption_key == nullptr || decryption_key[0] == '\0') { - ESP_LOGI(TAG, "Disabling decryption"); - this->decryption_key_.clear(); - if (this->crypt_telegram_ != nullptr) { - delete[] this->crypt_telegram_; - this->crypt_telegram_ = nullptr; - } + this->encryption_enabled_ = false; return; } - if (!parse_hex(decryption_key, this->decryption_key_, 16)) { - ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters"); - this->decryption_key_.clear(); + auto key = dsmr_parser::Aes128GcmDecryptionKey::from_hex(decryption_key); + if (!key) { + ESP_LOGE(TAG, "Error, decryption key has incorrect format"); + this->encryption_enabled_ = false; return; } ESP_LOGI(TAG, "Decryption key is set"); - // Verbose level prints decryption key - ESP_LOGV(TAG, "Using decryption key: %s", decryption_key); - if (this->crypt_telegram_ == nullptr) { - this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT + this->gcm_decryptor_.set_encryption_key(key.value()); + this->encryption_enabled_ = true; +} + +std::span<uint8_t> Dsmr::uart_read_chunk_() { + const auto avail = this->available(); + if (avail == 0) { + return {}; } + size_t to_read = std::min(avail, uart_chunk_reading_buf_.size()); + if (!this->read_array(uart_chunk_reading_buf_.data(), to_read)) { + return {}; + } + return {uart_chunk_reading_buf_.data(), to_read}; } } // namespace esphome::dsmr + +#endif diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index dc81ba9b2af..c76a23fde41 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -1,31 +1,41 @@ #pragma once +// Ignore Zephyr. It doesn't have any encryption library. +#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST) + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/log.h" +#include <dsmr_parser/dlms_packet_decryptor.h> #include <dsmr_parser/fields.h> +#include <dsmr_parser/packet_accumulator.h> #include <dsmr_parser/parser.h> +#include <array> +#include <span> #include <vector> +#if __has_include(<psa/crypto.h>) +#include <dsmr_parser/decryption/aes128gcm_tfpsa.h> +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa; +#elif __has_include(<mbedtls/gcm.h>) +#if __has_include(<mbedtls/esp_config.h>) +#include <mbedtls/esp_config.h> +#endif +#include <dsmr_parser/decryption/aes128gcm_mbedtls.h> +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls; +#elif __has_include(<bearssl/bearssl.h>) +#include <dsmr_parser/decryption/aes128gcm_bearssl.h> +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl; +#else +#error "The platform doesn't provide a compatible encryption library for dsmr_parser" +#endif + namespace esphome::dsmr { using namespace dsmr_parser::fields; -// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines - -#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST) -// Neither set, set it to a dummy value to not break build -#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification) -#endif - -#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST) -#define DSMR_BOTH , -#else -#define DSMR_BOTH -#endif - #ifndef DSMR_SENSOR_LIST #define DSMR_SENSOR_LIST(F, SEP) #endif @@ -34,21 +44,33 @@ using namespace dsmr_parser::fields; #define DSMR_TEXT_SENSOR_LIST(F, SEP) #endif -#define DSMR_DATA_SENSOR(s) s +#define DSMR_IDENTITY(s) s #define DSMR_COMMA , +#define DSMR_PREPEND_COMMA(...) __VA_OPT__(, ) __VA_ARGS__ -using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA) - DSMR_BOTH DSMR_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)>; +#ifdef DSMR_TEXT_SENSOR_LIST_DEFINED +using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA) + DSMR_PREPEND_COMMA(DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA))>; +#else +using MyData = dsmr_parser::ParsedData<DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)>; +#endif class Dsmr : public Component, public uart::UARTDevice { public: - Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {} + Dsmr(uart::UARTComponent *uart, bool crc_check, size_t max_telegram_length, uint32_t request_interval, + uint32_t receive_timeout, GPIOPin *request_pin, const char *decryption_key) + : uart::UARTDevice(uart), + request_interval_(request_interval), + receive_timeout_(receive_timeout), + request_pin_(request_pin), + buffer_(max_telegram_length), + packet_accumulator_(buffer_, crc_check) { + this->set_decryption_key_(decryption_key); + } void setup() override; void loop() override; - bool parse_telegram(); - void publish_sensors(MyData &data) { #define DSMR_PUBLISH_SENSOR(s) \ if (data.s##_present && this->s_##s##_ != nullptr) \ @@ -57,20 +79,15 @@ class Dsmr : public Component, public uart::UARTDevice { #define DSMR_PUBLISH_TEXT_SENSOR(s) \ if (data.s##_present && this->s_##s##_ != nullptr) \ - s_##s##_->publish_state(data.s.c_str()); + s_##s##_->publish_state(data.s.data(), data.s.size()); DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) }; void dump_config() override; - void set_decryption_key(const char *decryption_key); // Remove before 2026.8.0 - ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") - void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } - void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } - void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } - void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } - void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; } + ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); } // Sensor setters #define DSMR_SET_SENSOR(s) \ @@ -85,56 +102,40 @@ class Dsmr : public Component, public uart::UARTDevice { void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; } protected: + void set_decryption_key_(const char *decryption_key); void receive_telegram_(); void receive_encrypted_telegram_(); - void reset_telegram_(); - void drain_rx_buffer_(); + void flush_rx_buffer_(); - /// Wait for UART data to become available within the read timeout. - /// - /// The smart meter might provide data in chunks, causing available() to - /// return 0. When we're already reading a telegram, then we don't return - /// right away (to handle further data in an upcoming loop) but wait a - /// little while using this method to see if more data are incoming. - /// By not returning, we prevent other components from taking so much - /// time that the UART RX buffer overflows and bytes of the telegram get - /// lost in the process. - bool available_within_timeout_(); - - // Request telegram - uint32_t request_interval_; - bool request_interval_reached_(); - GPIOPin *request_pin_{nullptr}; - uint32_t last_request_time_{0}; - bool requesting_data_{false}; + bool parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram); + bool request_interval_reached_() const; bool ready_to_request_data_(); void start_requesting_data_(); void stop_requesting_data_(); + std::span<uint8_t> uart_read_chunk_(); - // Read telegram + // Config + uint32_t request_interval_; uint32_t receive_timeout_; - bool receive_timeout_reached_(); - size_t max_telegram_len_; - char *telegram_{nullptr}; - size_t bytes_read_{0}; - uint8_t *crypt_telegram_{nullptr}; - size_t crypt_telegram_len_{0}; - size_t crypt_bytes_read_{0}; - uint32_t last_read_time_{0}; - bool header_found_{false}; - bool footer_found_{false}; - - // handled outside dsmr + GPIOPin *request_pin_{nullptr}; text_sensor::TextSensor *s_telegram_{nullptr}; - -// Sensor member pointers #define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) - #define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr}; DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) - std::vector<uint8_t> decryption_key_{}; - bool crc_check_; + // State + uint32_t last_request_time_{0}; + uint32_t last_read_time_{0}; + bool requesting_data_{false}; + bool encryption_enabled_{false}; + size_t buffer_pos_{0}; + std::vector<uint8_t> buffer_; + dsmr_parser::PacketAccumulator packet_accumulator_; + Aes128GcmDecryptorImpl gcm_decryptor_; + dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_}; + std::array<uint8_t, 256> uart_chunk_reading_buf_; }; } // namespace esphome::dsmr + +#endif diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index c49614eaa98..292e5a1156e 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_WATER, @@ -119,6 +120,42 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("energy_delivered_tariff1_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff2_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff3_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff1_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff2_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff3_il"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), cv.Optional("total_imported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, @@ -511,6 +548,12 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("gas_delivered_gj"): sensor.sensor_schema( + unit_of_measurement=UNIT_GIGA_JOULE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), cv.Optional("water_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_CUBIC_METER, accuracy_decimals=3, @@ -614,6 +657,12 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional("active_demand_net"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional("active_demand_abs"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, @@ -728,6 +777,37 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional("power_factor"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("power_factor_l1"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("power_factor_l2"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("power_factor_l3"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("min_power_factor"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("period_3_for_instantaneous_values"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -746,6 +826,7 @@ async def to_code(config): sensors.append(f"F({key})") if sensors: + cg.add_define("DSMR_SENSOR_LIST_DEFINED") cg.add_define( "DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors)) ) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 203c9c997e2..a8f29c7ca86 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -15,7 +15,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(), cv.Optional("timestamp"): text_sensor.text_sensor_schema(), cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(), cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_failure_log_il"): text_sensor.text_sensor_schema(), cv.Optional("message_short"): text_sensor.text_sensor_schema(), cv.Optional("message_long"): text_sensor.text_sensor_schema(), cv.Optional("equipment_id"): text_sensor.text_sensor_schema(), @@ -52,6 +54,7 @@ async def to_code(config): text_sensors.append(f"F({key})") if text_sensors: + cg.add_define("DSMR_TEXT_SENSOR_LIST_DEFINED") cg.add_define( "DSMR_TEXT_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(text_sensors)), diff --git a/platformio.ini b/platformio.ini index d7b14944e47..3023a157323 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,8 +37,7 @@ lib_deps_base = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier - esphome/dsmr_parser@1.1.0 ; dsmr - polargoose/Crypto-no-arduino@0.4.0 ; dsmr + esphome/dsmr_parser@1.4.0 ; dsmr https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library diff --git a/tests/components/dsmr/test.esp32-ard.yaml b/tests/components/dsmr/test.esp32-ard.yaml index f218b297aa8..41ea1e8d899 100644 --- a/tests/components/dsmr/test.esp32-ard.yaml +++ b/tests/components/dsmr/test.esp32-ard.yaml @@ -5,3 +5,10 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-ard.yaml <<: !include common.yaml + +sensor: + - platform: dsmr + energy_delivered_lux: + name: "Energy Consumed Luxembourg. OBIS: 1-0:1.8.0" + energy_delivered_tariff1: + name: "Energy Consumed Tariff 1. OBIS: 1-0:1.8.1" diff --git a/tests/components/dsmr/test.esp32-idf.yaml b/tests/components/dsmr/test.esp32-idf.yaml index 522f60db49b..9eb7d3e1784 100644 --- a/tests/components/dsmr/test.esp32-idf.yaml +++ b/tests/components/dsmr/test.esp32-idf.yaml @@ -5,3 +5,17 @@ packages: uart: !include ../../test_build_components/common/uart/esp32-idf.yaml <<: !include common.yaml + +sensor: + - platform: dsmr + energy_delivered_lux: + name: "Energy Consumed Luxembourg. OBIS: 1-0:1.8.0" + energy_delivered_tariff1: + name: "Energy Consumed Tariff 1. OBIS: 1-0:1.8.1" + +text_sensor: + - platform: dsmr + identification: + name: "DSMR Identification" + p1_version: + name: "DSMR Version. OBIS: 1-3:0.2.8" diff --git a/tests/components/dsmr/test.esp8266-ard.yaml b/tests/components/dsmr/test.esp8266-ard.yaml index 08bcf16fc98..d318076edb3 100644 --- a/tests/components/dsmr/test.esp8266-ard.yaml +++ b/tests/components/dsmr/test.esp8266-ard.yaml @@ -5,3 +5,10 @@ packages: uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml + +text_sensor: + - platform: dsmr + identification: + name: "DSMR Identification" + p1_version: + name: "DSMR Version. OBIS: 1-3:0.2.8" From 4e84611ae7b1fd58b46d566f9caa14e7936b9668 Mon Sep 17 00:00:00 2001 From: Rishab Mehta <45841886+rishabmehta7@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:20:59 +0530 Subject: [PATCH 1594/2056] [internal_temperature] Fix internal Temperature discrepancy on BK7231T (#15771) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../internal_temperature/internal_temperature_bk72xx.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp index 31a92f90a51..b7332ee81fa 100644 --- a/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp +++ b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp @@ -20,8 +20,6 @@ void InternalTemperatureSensor::update() { success = (result == 0); #if defined(USE_LIBRETINY_VARIANT_BK7231N) temperature = raw * -0.38f + 156.0f; -#elif defined(USE_LIBRETINY_VARIANT_BK7231T) - temperature = raw * 0.04f; #else // USE_LIBRETINY_VARIANT temperature = raw * 0.128f; #endif // USE_LIBRETINY_VARIANT From a73bac0b5f251d62f17f6f0d1a8c51595b4d6da2 Mon Sep 17 00:00:00 2001 From: Asela Fernando <25498128+aselafernando@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:57:53 +1000 Subject: [PATCH 1595/2056] [ac_dimmer] Zero-crossing interrupt type (#15862) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 24 ++++++++++++++-------- esphome/components/ac_dimmer/ac_dimmer.h | 2 ++ esphome/components/ac_dimmer/output.py | 14 +++++++++++++ tests/components/ac_dimmer/common.yaml | 1 + 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index f731a8c7537..3e21d6981d7 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -190,7 +190,7 @@ void AcDimmer::setup() { this->zero_cross_pin_->setup(); this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr(); this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + this->zero_cross_interrupt_type_); } #ifdef USE_ESP8266 @@ -226,19 +226,25 @@ void AcDimmer::write_state(float state) { void AcDimmer::dump_config() { ESP_LOGCONFIG(TAG, "AcDimmer:\n" - " Min Power: %.1f%%\n" - " Init with half cycle: %s", + " Min Power: %.1f%%\n" + " Init with half cycle: %s", this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_)); LOG_PIN(" Output Pin: ", this->gate_pin_); LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); - if (method_ == DIM_METHOD_LEADING_PULSE) { - ESP_LOGCONFIG(TAG, " Method: leading pulse"); - } else if (method_ == DIM_METHOD_LEADING) { - ESP_LOGCONFIG(TAG, " Method: leading"); + if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE) { + ESP_LOGCONFIG(TAG, " Interrupt Type: rising"); + } else if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE) { + ESP_LOGCONFIG(TAG, " Interrupt Type: falling"); } else { - ESP_LOGCONFIG(TAG, " Method: trailing"); + ESP_LOGCONFIG(TAG, " Interrupt Type: any"); + } + if (method_ == DIM_METHOD_LEADING_PULSE) { + ESP_LOGCONFIG(TAG, " Method: leading pulse"); + } else if (method_ == DIM_METHOD_LEADING) { + ESP_LOGCONFIG(TAG, " Method: leading"); + } else { + ESP_LOGCONFIG(TAG, " Method: trailing"); } - LOG_FLOAT_OUTPUT(this); ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); } diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h index ca2a19210ac..6bfcf0bdb5b 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.h +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -48,6 +48,7 @@ class AcDimmer : public output::FloatOutput, public Component { void dump_config() override; void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; } void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } + void set_zero_cross_interrupt_type(gpio::InterruptType type) { zero_cross_interrupt_type_ = type; } void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; } void set_method(DimMethod method) { method_ = method; } @@ -56,6 +57,7 @@ class AcDimmer : public output::FloatOutput, public Component { InternalGPIOPin *gate_pin_; InternalGPIOPin *zero_cross_pin_; + gpio::InterruptType zero_cross_interrupt_type_; AcDimmerDataStore store_; bool init_with_half_cycle_; DimMethod method_; diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index efc24b65e7c..1f35095e0e1 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -7,6 +7,8 @@ from esphome.core import CORE CODEOWNERS = ["@glmnet"] +gpio_ns = cg.esphome_ns.namespace("gpio") + ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer") AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component) @@ -17,15 +19,26 @@ DIM_METHODS = { "TRAILING": DimMethod.DIM_METHOD_TRAILING, } +ZC_INTERRUPT_TYPES = { + "RISING": gpio_ns.INTERRUPT_RISING_EDGE, + "FALLING": gpio_ns.INTERRUPT_FALLING_EDGE, + "ANY": gpio_ns.INTERRUPT_ANY_EDGE, +} + CONF_GATE_PIN = "gate_pin" CONF_ZERO_CROSS_PIN = "zero_cross_pin" CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle" +CONF_ZERO_CROSS_INTERRUPT_TYPE = "zero_cross_interrupt_type" + CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(AcDimmer), cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_ZERO_CROSS_INTERRUPT_TYPE, default="FALLING"): cv.enum( + ZC_INTERRUPT_TYPES, upper=True, space="_" + ), cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum( DIM_METHODS, upper=True, space="_" @@ -54,5 +67,6 @@ async def to_code(config): cg.add(var.set_gate_pin(pin)) pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN]) cg.add(var.set_zero_cross_pin(pin)) + cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE])) cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE])) cg.add(var.set_method(config[CONF_METHOD])) diff --git a/tests/components/ac_dimmer/common.yaml b/tests/components/ac_dimmer/common.yaml index 8f93066838f..c16e2e834a9 100644 --- a/tests/components/ac_dimmer/common.yaml +++ b/tests/components/ac_dimmer/common.yaml @@ -3,3 +3,4 @@ output: id: ac_dimmer_1 gate_pin: ${gate_pin} zero_cross_pin: ${zero_cross_pin} + zero_cross_interrupt_type: ANY From 162ee2ecaf8a5b1e7cfe18937100f9b2933e2f7a Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 22 Apr 2026 14:40:18 -0500 Subject: [PATCH 1596/2056] [i2s_audio] Split speaker into base class and standard subclass (#15404) --- .../components/i2s_audio/speaker/__init__.py | 13 +- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 456 ++++-------------- .../i2s_audio/speaker/i2s_audio_speaker.h | 89 +++- .../speaker/i2s_audio_speaker_standard.cpp | 307 ++++++++++++ .../speaker/i2s_audio_speaker_standard.h | 32 ++ 5 files changed, 515 insertions(+), 382 deletions(-) create mode 100644 esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp create mode 100644 esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index d1d1bc3ee3a..99aa712c682 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -33,13 +33,16 @@ AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] DEPENDENCIES = ["i2s_audio"] -I2SAudioSpeaker = i2s_audio_ns.class_( - "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut +I2SAudioSpeakerBase = i2s_audio_ns.class_( + "I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut ) +I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) CONF_DAC_TYPE = "dac_type" CONF_I2S_COMM_FMT = "i2s_comm_fmt" +I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True) + i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") INTERNAL_DAC_OPTIONS = { CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, @@ -183,11 +186,11 @@ async def to_code(config): await speaker.register_speaker(var, config) cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long + fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: - fmt = "msb" + fmt = I2SCommFmt.MSB elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: - fmt = "pcm" + fmt = I2SCommFmt.PCM cg.add(var.set_i2s_comm_fmt(fmt)) if config[CONF_TIMEOUT] != CONF_NEVER: cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index dde1f70bc58..836221e38a7 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -13,36 +13,10 @@ #include "esp_timer.h" -namespace esphome { -namespace i2s_audio { - -static const uint32_t DMA_BUFFER_DURATION_MS = 15; -static const size_t DMA_BUFFERS_COUNT = 4; - -static const size_t TASK_STACK_SIZE = 4096; -static const ssize_t TASK_PRIORITY = 19; - -static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; +namespace esphome::i2s_audio { static const char *const TAG = "i2s_audio.speaker"; -enum SpeakerEventGroupBits : uint32_t { - COMMAND_START = (1 << 0), // indicates loop should start speaker task - COMMAND_STOP = (1 << 1), // stops the speaker task - COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written - - TASK_STARTING = (1 << 10), - TASK_RUNNING = (1 << 11), - TASK_STOPPING = (1 << 12), - TASK_STOPPED = (1 << 13), - - ERR_ESP_NO_MEM = (1 << 19), - - WARN_DROPPED_EVENT = (1 << 20), - - ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits -}; - // Lists the Q15 fixed point scaling factor for volume reduction. // Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) @@ -56,17 +30,21 @@ static const std::vector<int16_t> Q15_VOLUME_SCALING_FACTORS = { 8218, 8706, 9222, 9770, 10349, 10963, 11613, 12302, 13032, 13805, 14624, 15491, 16410, 17384, 18415, 19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767}; -void I2SAudioSpeaker::setup() { +void I2SAudioSpeakerBase::setup() { this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Failed to create event group"); + ESP_LOGE(TAG, "Event group creation failed"); this->mark_failed(); return; } + + // Initialize volume control. When audio_dac is configured, this sets the DAC volume. + // When no audio_dac is configured, this initializes software volume control. + this->set_volume(this->volume_); } -void I2SAudioSpeaker::dump_config() { +void I2SAudioSpeakerBase::dump_config() { ESP_LOGCONFIG(TAG, "Speaker:\n" " Pin: %d\n" @@ -75,10 +53,9 @@ void I2SAudioSpeaker::dump_config() { if (this->timeout_.has_value()) { ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value()); } - ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str()); } -void I2SAudioSpeaker::loop() { +void I2SAudioSpeakerBase::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { @@ -92,12 +69,12 @@ void I2SAudioSpeaker::loop() { xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); } if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) { - ESP_LOGD(TAG, "Started"); + ESP_LOGV(TAG, "Started"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); this->state_ = speaker::STATE_RUNNING; } if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { - ESP_LOGD(TAG, "Stopping"); + ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); this->state_ = speaker::STATE_STOPPING; } @@ -111,10 +88,12 @@ void I2SAudioSpeaker::loop() { xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); this->status_clear_error(); + this->on_task_stopped(); + this->state_ = speaker::STATE_STOPPED; } - // Log any errors encounted by the task + // Log any errors encountered by the task if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { ESP_LOGE(TAG, "Not enough memory"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); @@ -133,14 +112,14 @@ void I2SAudioSpeaker::loop() { break; } - if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) { + if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); - this->status_momentary_error("driver-faiure", 1000); + this->status_momentary_error("driver-failure", 1000); break; } if (this->speaker_task_handle_ == nullptr) { - xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, &this->speaker_task_handle_); if (this->speaker_task_handle_ == nullptr) { @@ -157,7 +136,7 @@ void I2SAudioSpeaker::loop() { } } -void I2SAudioSpeaker::set_volume(float volume) { +void I2SAudioSpeakerBase::set_volume(float volume) { this->volume_ = volume; #ifdef USE_AUDIO_DAC if (this->audio_dac_ != nullptr) { @@ -166,15 +145,21 @@ void I2SAudioSpeaker::set_volume(float volume) { } this->audio_dac_->set_volume(volume); } else -#endif +#endif // USE_AUDIO_DAC { - // Fallback to software volume control by using a Q15 fixed point scaling factor - ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); - this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + // Fallback to software volume control by using a Q15 fixed point scaling factor. + // At maximum volume (1.0), set to INT16_MAX to completely bypass volume processing + // and avoid any floating-point precision issues that could cause slight volume reduction. + if (volume >= 1.0f) { + this->q15_volume_factor_ = INT16_MAX; + } else { + ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); + this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + } } } -void I2SAudioSpeaker::set_mute_state(bool mute_state) { +void I2SAudioSpeakerBase::set_mute_state(bool mute_state) { this->mute_state_ = mute_state; #ifdef USE_AUDIO_DAC if (this->audio_dac_) { @@ -184,7 +169,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) { this->audio_dac_->set_mute_off(); } } else -#endif +#endif // USE_AUDIO_DAC { if (mute_state) { // Fallback to software volume control and scale by 0 @@ -196,11 +181,12 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) { } } -size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { +size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { if (this->is_failed()) { ESP_LOGE(TAG, "Setup failed; cannot play audio"); return 0; } + if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { this->start(); } @@ -214,8 +200,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick size_t bytes_written = 0; if (this->state_ == speaker::STATE_RUNNING) { std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); - if (temp_ring_buffer.use_count() == 2) { - // Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to + if (temp_ring_buffer != nullptr) { + // The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); } } @@ -223,7 +209,7 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick return bytes_written; } -bool I2SAudioSpeaker::has_buffered_data() const { +bool I2SAudioSpeakerBase::has_buffered_data() const { if (this->audio_ring_buffer_.use_count() > 0) { std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); return temp_ring_buffer->available() > 0; @@ -231,216 +217,27 @@ bool I2SAudioSpeaker::has_buffered_data() const { return false; } -void I2SAudioSpeaker::speaker_task(void *params) { - I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING); - - const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; - // Ensure ring buffer duration is at least the duration of all DMA buffers - const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_); - - // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info - const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration); - - const uint32_t frames_to_fill_single_dma_buffer = - this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); - const size_t bytes_to_fill_single_dma_buffer = - this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); - - bool successful_setup = false; - std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = - audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); - - if (transfer_buffer != nullptr) { - std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size); - if (temp_ring_buffer.use_count() == 1) { - transfer_buffer->set_source(temp_ring_buffer); - this_speaker->audio_ring_buffer_ = temp_ring_buffer; - successful_setup = true; - } - } - - if (!successful_setup) { - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - } else { - bool stop_gracefully = false; - bool tx_dma_underflow = true; - - uint32_t frames_written = 0; - uint32_t last_data_received_time = millis(); - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - - while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() || - (millis() - last_data_received_time) <= this_speaker->timeout_.value()) { - uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_); - - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { - xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - break; - } - if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { - xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); - stop_gracefully = true; - } - - if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) { - // Audio stream info changed, stop the speaker task so it will restart with the proper settings. - break; - } - int64_t write_timestamp; - while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) { - // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes - // on the timing info via the audio_output_callback. - uint32_t frames_sent = frames_to_fill_single_dma_buffer; - if (frames_to_fill_single_dma_buffer > frames_written) { - tx_dma_underflow = true; - frames_sent = frames_written; - const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; - write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed); - } else { - tx_dma_underflow = false; - } - frames_written -= frames_sent; - if (frames_sent > 0) { - this_speaker->audio_output_callback_(frames_sent, write_timestamp); - } - } - - if (this_speaker->pause_state_) { - // Pause state is accessed atomically, so thread safe - // Delay so the task yields, then skip transferring audio data - vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); - continue; - } - - // Wait half the duration of the data already written to the DMA buffers for new audio data - // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 - const uint32_t read_delay = - (this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; - - size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); - uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; - - if (bytes_read > 0) { - if (this_speaker->q15_volume_factor_ < INT16_MAX) { - // Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it, - // multiplying by the volume factor, and packing the sample back into the original bytes per sample. - - const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1); - const uint32_t len = bytes_read / bytes_per_sample; - - // Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31 - int32_t shift = 15; // Q31 -> Q16 - int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15 - - if (bytes_per_sample >= 3) { - // Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31 - - shift = 8; // Q31 -> Q23 - gain_factor >>= 7; // Q15 -> Q8 - } - - for (uint32_t i = 0; i < len; ++i) { - int32_t sample = - audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31 - sample >>= shift; - sample *= gain_factor; // Q31 - audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample); - } - } - -#ifdef USE_ESP32_VARIANT_ESP32 - // For ESP32 16-bit mono mode, adjacent samples need to be swapped. - if (this_speaker->current_stream_info_.get_channels() == 1 && - this_speaker->current_stream_info_.get_bits_per_sample() == 16) { - int16_t *samples = reinterpret_cast<int16_t *>(new_data); - size_t sample_count = bytes_read / sizeof(int16_t); - for (size_t i = 0; i + 1 < sample_count; i += 2) { - int16_t tmp = samples[i]; - samples[i] = samples[i + 1]; - samples[i + 1] = tmp; - } - } -#endif - } - - if (transfer_buffer->available() == 0) { - if (stop_gracefully && tx_dma_underflow) { - break; - } - vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); - } else { - size_t bytes_written = 0; - if (tx_dma_underflow) { - // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing - // callbacks are accurate. Preload the data. - i2s_channel_disable(this_speaker->tx_handle_); - const i2s_event_callbacks_t callbacks = { - .on_sent = nullptr, - }; - - i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); - i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), - transfer_buffer->available(), &bytes_written); - } else { - // Audio is already playing, use regular I2S write to add to the DMA buffers - i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), - &bytes_written, DMA_BUFFER_DURATION_MS); - } - if (bytes_written > 0) { - last_data_received_time = millis(); - frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written); - transfer_buffer->decrease_buffer_length(bytes_written); - if (tx_dma_underflow) { - tx_dma_underflow = false; - // Reset the event queue timestamps - // Enable the on_sent callback to accurately track the timestamps of played audio - // Enable the I2S channel to start sending the preloaded audio - - xQueueReset(this_speaker->i2s_event_queue_); - - const i2s_event_callbacks_t callbacks = { - .on_sent = i2s_on_sent_cb, - }; - i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); - - i2s_channel_enable(this_speaker->tx_handle_); - } - } - } - } - } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING); - - if (transfer_buffer != nullptr) { - transfer_buffer.reset(); - } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED); - - while (true) { - // Continuously delay until the loop method deletes the task - vTaskDelay(pdMS_TO_TICKS(10)); - } +void I2SAudioSpeakerBase::speaker_task(void *params) { + I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) params; + this_speaker->run_speaker_task(); } -void I2SAudioSpeaker::start() { +void I2SAudioSpeakerBase::start() { if (!this->is_ready() || this->is_failed() || this->status_has_error()) return; if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; + // Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START. + this->state_ = speaker::STATE_STARTING; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } -void I2SAudioSpeaker::stop() { this->stop_(false); } +void I2SAudioSpeakerBase::stop() { this->stop_(false); } -void I2SAudioSpeaker::finish() { this->stop_(true); } +void I2SAudioSpeakerBase::finish() { this->stop_(true); } -void I2SAudioSpeaker::stop_(bool wait_on_empty) { +void I2SAudioSpeakerBase::stop_(bool wait_on_empty) { if (this->is_failed()) return; if (this->state_ == speaker::STATE_STOPPED) @@ -453,105 +250,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) { } } -esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { - this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use - - if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT - // Can't reconfigure I2S bus, so the sample rate must match the configured value - ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration"); - return ESP_ERR_NOT_SUPPORTED; - } - - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO && - (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { - // Currently can't handle the case when the incoming audio has more bits per sample than the configured value - ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported"); - return ESP_ERR_NOT_SUPPORTED; - } - - if (!this->parent_->try_lock()) { - ESP_LOGE(TAG, "Parent I2S bus not free"); - return ESP_ERR_INVALID_STATE; - } - - uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); - - i2s_chan_config_t chan_cfg = { - .id = this->parent_->get_port(), - .role = this->i2s_role_, - .dma_desc_num = DMA_BUFFERS_COUNT, - .dma_frame_num = dma_buffer_length, - .auto_clear = true, - .intr_priority = 3, - }; - /* Allocate a new TX channel and get the handle of this channel */ +esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, + size_t event_queue_size) { esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to allocate new I2S channel"); + ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err)); this->parent_->unlock(); return err; } - i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; -#ifdef I2S_CLK_SRC_APLL - if (this->use_apll_) { - clk_src = I2S_CLK_SRC_APLL; - } -#endif - i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config(); - - i2s_std_clk_config_t clk_cfg = { - .sample_rate_hz = audio_stream_info.get_sample_rate(), - .clk_src = clk_src, - .mclk_multiple = this->mclk_multiple_, - }; - - i2s_slot_mode_t slot_mode = this->slot_mode_; - i2s_std_slot_mask_t slot_mask = this->std_slot_mask_; - if (audio_stream_info.get_channels() == 1) { - slot_mode = I2S_SLOT_MODE_MONO; - } else if (audio_stream_info.get_channels() == 2) { - slot_mode = I2S_SLOT_MODE_STEREO; - slot_mask = I2S_STD_SLOT_BOTH; - } - - i2s_std_slot_config_t std_slot_cfg; - if (this->i2s_comm_fmt_ == "std") { - std_slot_cfg = - I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); - } else if (this->i2s_comm_fmt_ == "pcm") { - std_slot_cfg = - I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); - } else { - std_slot_cfg = - I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); - } -#ifdef USE_ESP32_VARIANT_ESP32 - // There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher then the bits - // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to - // make it play at the correct speed while sending more bits per slot. - if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_); - std_slot_cfg.ws_width = configured_bit_width; - if (configured_bit_width > 16) { - std_slot_cfg.msb_right = false; - } - } -#else - std_slot_cfg.slot_bit_width = this->slot_bit_width_; -#endif - std_slot_cfg.slot_mask = slot_mask; - - pin_config.dout = this->dout_pin_; - - i2s_std_config_t std_cfg = { - .clk_cfg = clk_cfg, - .slot_cfg = std_slot_cfg, - .gpio_cfg = pin_config, - }; - /* Initialize the channel */ err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); - if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize channel"); i2s_del_channel(this->tx_handle_); @@ -559,23 +267,34 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea this->parent_->unlock(); return err; } + if (this->i2s_event_queue_ == nullptr) { - this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t)); + this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t)); + } else { + // Reset queue to clear any stale events from previous task + xQueueReset(this->i2s_event_queue_); } - i2s_channel_enable(this->tx_handle_); - - return err; + return ESP_OK; } -bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { +void I2SAudioSpeakerBase::stop_i2s_driver_() { + if (this->tx_handle_ != nullptr) { + i2s_channel_disable(this->tx_handle_); + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; + } + this->parent_->unlock(); +} + +bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { int64_t now = esp_timer_get_time(); BaseType_t need_yield1 = pdFALSE; BaseType_t need_yield2 = pdFALSE; BaseType_t need_yield3 = pdFALSE; - I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; + I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx; if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { // Queue is full, so discard the oldest event and set the warning flag to inform the user @@ -589,14 +308,47 @@ bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_eve return need_yield1 | need_yield2 | need_yield3; } -void I2SAudioSpeaker::stop_i2s_driver_() { - i2s_channel_disable(this->tx_handle_); - i2s_del_channel(this->tx_handle_); - this->tx_handle_ = nullptr; - this->parent_->unlock(); +void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) { + if (this->q15_volume_factor_ >= INT16_MAX) { + return; // Max volume, no processing needed + } + + const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1); + const uint32_t len = bytes_read / bytes_per_sample; + + // Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31 + int32_t shift = 15; // Q31 -> Q16 + int32_t gain_factor = this->q15_volume_factor_; // Q15 + + if (bytes_per_sample >= 3) { + // Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31 + shift = 8; // Q31 -> Q23 + gain_factor >>= 7; // Q15 -> Q8 + } + + for (uint32_t i = 0; i < len; ++i) { + int32_t sample = audio::unpack_audio_sample_to_q31(&data[i * bytes_per_sample], bytes_per_sample); // Q31 + sample >>= shift; + sample *= gain_factor; // Q31 + audio::pack_q31_as_audio_sample(sample, &data[i * bytes_per_sample], bytes_per_sample); + } } -} // namespace i2s_audio -} // namespace esphome +void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) { +#ifdef USE_ESP32_VARIANT_ESP32 + // For ESP32 16-bit mono mode, adjacent samples need to be swapped. + if (this->current_stream_info_.get_channels() == 1 && this->current_stream_info_.get_bits_per_sample() == 16) { + int16_t *samples = reinterpret_cast<int16_t *>(data); + size_t sample_count = bytes_read / sizeof(int16_t); + for (size_t i = 0; i + 1 < sample_count; i += 2) { + int16_t tmp = samples[i]; + samples[i] = samples[i + 1]; + samples[i + 1] = tmp; + } + } +#endif // USE_ESP32_VARIANT_ESP32 +} + +} // namespace esphome::i2s_audio #endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 76b66922094..b2644efd054 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -16,10 +16,34 @@ #include "esphome/core/helpers.h" #include "esphome/core/ring_buffer.h" -namespace esphome { -namespace i2s_audio { +namespace esphome::i2s_audio { -class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component { +// Shared constants for I2S audio speaker implementations +static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15; +static constexpr size_t TASK_STACK_SIZE = 4096; +static constexpr ssize_t TASK_PRIORITY = 19; + +enum SpeakerEventGroupBits : uint32_t { + COMMAND_START = (1 << 0), // indicates loop should start speaker task + COMMAND_STOP = (1 << 1), // stops the speaker task + COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written + + TASK_STARTING = (1 << 10), + TASK_RUNNING = (1 << 11), + TASK_STOPPING = (1 << 12), + TASK_STOPPED = (1 << 13), + + ERR_ESP_NO_MEM = (1 << 19), + + WARN_DROPPED_EVENT = (1 << 20), + + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +/// @brief Abstract base class for I2S audio speaker implementations. +/// Provides shared infrastructure (event groups, ring buffer, volume control, task lifecycle) +/// for derived I2S speaker classes. +class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public Component { public: float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } @@ -30,7 +54,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } void set_timeout(uint32_t ms) { this->timeout_ = ms; } void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; } - void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); } + + /// @brief Get the I2S TX channel handle + i2s_chan_handle_t get_tx_handle() const { return this->tx_handle_; } void start() override; void stop() override; @@ -63,40 +89,55 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp void set_mute_state(bool mute_state) override; protected: - /// @brief Function for the FreeRTOS task handling audio output. - /// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops - /// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving - /// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds. - /// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``. - /// @param params I2SAudioSpeaker component + /// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_(). + /// @param params I2SAudioSpeakerBase component pointer static void speaker_task(void *params); + /// @brief The main speaker task loop. Implemented by derived classes for mode-specific behavior. + virtual void run_speaker_task() = 0; + /// @brief Sends a stop command to the speaker task via ``event_group_``. /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. void stop_(bool wait_on_empty); - /// @brief Callback function used to send playback timestamps the to the speaker task. + /// @brief Callback function used to send playback timestamps to the speaker task. /// @param handle (i2s_chan_handle_t) /// @param event (i2s_event_data_t) /// @param user_ctx (void*) User context pointer that the callback accesses /// @return True if a higher priority task was interrupted static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); - /// @brief Starts the ESP32 I2S driver. - /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out - /// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary. + /// @brief Starts the ESP32 I2S driver. Implemented by derived classes for mode-specific configuration. /// @param audio_stream_info Stream information for the I2S driver. - /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream. - /// ESP_ERR_INVALID_STATE if the I2S port is already locked. - /// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error. - /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. - /// ESP_FAIL if setting the data out pin fails due to an IO error - /// ESP_OK if successful - esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info); + /// @return ESP_OK if successful, or an error code + virtual esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) = 0; + + /// @brief Shared I2S channel allocation, initialization, and event queue setup. + /// Called by derived start_i2s_driver_() implementations after building mode-specific configs. + /// @param chan_cfg I2S channel configuration + /// @param std_cfg I2S standard mode configuration (clock, slot, GPIO) + /// @param event_queue_size Size of the event queue + /// @return ESP_OK if successful, or an error code. On failure, cleans up channel and unlocks parent. + esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg, + size_t event_queue_size); /// @brief Stops the I2S driver and unlocks the I2S port void stop_i2s_driver_(); + /// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup. + virtual void on_task_stopped() {} + + /// @brief Apply software volume control using Q15 fixed-point scaling. + /// @param data Pointer to audio sample data (modified in place) + /// @param bytes_read Number of bytes of audio data + void apply_software_volume_(uint8_t *data, size_t bytes_read); + + /// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk. + /// Only applies when running on original ESP32 with 16-bit mono audio. + /// @param data Pointer to audio sample data (modified in place) + /// @param bytes_read Number of bytes of audio data + void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read); + TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; @@ -115,11 +156,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info gpio_num_t dout_pin_; - std::string i2s_comm_fmt_; - i2s_chan_handle_t tx_handle_; + i2s_chan_handle_t tx_handle_{nullptr}; }; -} // namespace i2s_audio -} // namespace esphome +} // namespace esphome::i2s_audio #endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp new file mode 100644 index 00000000000..02034640346 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -0,0 +1,307 @@ +#include "i2s_audio_speaker_standard.h" + +#ifdef USE_ESP32 + +#include <driver/i2s_std.h> + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio/audio_transfer_buffer.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include "esp_timer.h" + +namespace esphome::i2s_audio { + +static const char *const TAG = "i2s_audio.speaker.std"; + +static constexpr size_t DMA_BUFFERS_COUNT = 4; +static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; + +void I2SAudioSpeaker::dump_config() { + I2SAudioSpeakerBase::dump_config(); + const char *fmt_str; + switch (this->i2s_comm_fmt_) { + case I2SCommFmt::PCM: + fmt_str = "pcm"; + break; + case I2SCommFmt::MSB: + fmt_str = "msb"; + break; + default: + fmt_str = "std"; + break; + } + ESP_LOGCONFIG(TAG, " Communication format: %s", fmt_str); +} + +void I2SAudioSpeaker::run_speaker_task() { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); + + const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; + // Ensure ring buffer duration is at least the duration of all DMA buffers + const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); + + // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info + const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration); + const uint32_t frames_to_fill_single_dma_buffer = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + const size_t bytes_to_fill_single_dma_buffer = + this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); + + bool successful_setup = false; + std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = + audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + + if (transfer_buffer != nullptr) { + std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer.use_count() == 1) { + transfer_buffer->set_source(temp_ring_buffer); + this->audio_ring_buffer_ = temp_ring_buffer; + successful_setup = true; + } + } + + if (!successful_setup) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + } else { + bool stop_gracefully = false; + bool tx_dma_underflow = true; + + uint32_t frames_written = 0; + uint32_t last_data_received_time = millis(); + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); + + // Main speaker task loop. Continues while: + // - Paused, OR + // - No timeout configured, OR + // - Timeout hasn't elapsed since last data + while (this->pause_state_ || !this->timeout_.has_value() || + (millis() - last_data_received_time) <= this->timeout_.value()) { + uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); + ESP_LOGV(TAG, "Exiting: COMMAND_STOP received"); + break; + } + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); + stop_gracefully = true; + } + + if (this->audio_stream_info_ != this->current_stream_info_) { + // Audio stream info changed, stop the speaker task so it will restart with the proper settings. + ESP_LOGV(TAG, "Exiting: stream info changed"); + break; + } + + int64_t write_timestamp; + while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { + // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes + // on the timing info via the audio_output_callback. + uint32_t frames_sent = frames_to_fill_single_dma_buffer; + if (frames_to_fill_single_dma_buffer > frames_written) { + tx_dma_underflow = true; + frames_sent = frames_written; + const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; + write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed); + } else { + tx_dma_underflow = false; + } + frames_written -= frames_sent; + + // Standard I2S mode: fire callback immediately for each event + if (frames_sent > 0) { + this->audio_output_callback_(frames_sent, write_timestamp); + } + } + + if (this->pause_state_) { + // Pause state is accessed atomically, so thread safe + // Delay so the task yields, then skip transferring audio data + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + continue; + } + + // Wait half the duration of the data already written to the DMA buffers for new audio data + // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 + uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; + + size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); + uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; + + if (bytes_read > 0) { + this->apply_software_volume_(new_data, bytes_read); + this->swap_esp32_mono_samples_(new_data, bytes_read); + } + + if (transfer_buffer->available() == 0) { + if (stop_gracefully && tx_dma_underflow) { + break; + } + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); + } else { + size_t bytes_written = 0; + + if (tx_dma_underflow) { + // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue + i2s_channel_disable(this->tx_handle_); + const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr}; + i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this); + i2s_channel_preload_data(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), + &bytes_written); + } else { + // Audio is already playing, use regular write to add to the DMA buffers + i2s_channel_write(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), + &bytes_written, DMA_BUFFER_DURATION_MS); + } + + if (bytes_written > 0) { + last_data_received_time = millis(); + frames_written += this->current_stream_info_.bytes_to_frames(bytes_written); + transfer_buffer->decrease_buffer_length(bytes_written); + + if (tx_dma_underflow) { + tx_dma_underflow = false; + // Enable the on_sent callback and channel after preload + xQueueReset(this->i2s_event_queue_); + const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; + i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); + i2s_channel_enable(this->tx_handle_); + } + } + } + } + } + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); + + if (transfer_buffer != nullptr) { + transfer_buffer.reset(); + } + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); + + while (true) { + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) { + this->current_stream_info_ = audio_stream_info; + + if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT + // Can't reconfigure I2S bus, so the sample rate must match the configured value + ESP_LOGE(TAG, "Incompatible stream settings"); + return ESP_ERR_NOT_SUPPORTED; + } + + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO && + (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { + // Currently can't handle the case when the incoming audio has more bits per sample than the configured value + ESP_LOGE(TAG, "Stream bits per sample must be less than or equal to the speaker's configuration"); + return ESP_ERR_NOT_SUPPORTED; + } + + if (!this->parent_->try_lock()) { + ESP_LOGE(TAG, "Parent bus is busy"); + return ESP_ERR_INVALID_STATE; + } + + uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS); + + i2s_role_t i2s_role = this->i2s_role_; + i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; + +#if SOC_CLK_APLL_SUPPORTED + if (this->use_apll_) { + clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL; + } +#endif // SOC_CLK_APLL_SUPPORTED + + // Log DMA configuration for debugging + ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames", (size_t) DMA_BUFFERS_COUNT, + (unsigned long) dma_buffer_length); + + i2s_chan_config_t chan_cfg = { + .id = this->parent_->get_port(), + .role = i2s_role, + .dma_desc_num = DMA_BUFFERS_COUNT, + .dma_frame_num = dma_buffer_length, + .auto_clear = true, + .intr_priority = 3, + }; + + // Build standard I2S clock/slot/gpio configuration + i2s_std_clk_config_t clk_cfg = { + .sample_rate_hz = audio_stream_info.get_sample_rate(), + .clk_src = clk_src, + .mclk_multiple = this->mclk_multiple_, + }; + + i2s_slot_mode_t slot_mode = this->slot_mode_; + i2s_std_slot_mask_t slot_mask = this->std_slot_mask_; + if (audio_stream_info.get_channels() == 1) { + slot_mode = I2S_SLOT_MODE_MONO; + } else if (audio_stream_info.get_channels() == 2) { + slot_mode = I2S_SLOT_MODE_STEREO; + slot_mask = I2S_STD_SLOT_BOTH; + } + + i2s_std_slot_config_t slot_cfg; + switch (this->i2s_comm_fmt_) { + case I2SCommFmt::PCM: + slot_cfg = + I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); + break; + case I2SCommFmt::MSB: + slot_cfg = + I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode); + break; + default: + slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), + slot_mode); + break; + } + +#ifdef USE_ESP32_VARIANT_ESP32 + // There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher than the + // bits per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems + // to make it play at the correct speed while sending more bits per slot. + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { + uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_); + slot_cfg.ws_width = configured_bit_width; + if (configured_bit_width > 16) { + slot_cfg.msb_right = false; + } + } +#else + slot_cfg.slot_bit_width = this->slot_bit_width_; +#endif // USE_ESP32_VARIANT_ESP32 + slot_cfg.slot_mask = slot_mask; + + i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config(); + gpio_cfg.dout = this->dout_pin_; + + i2s_std_config_t std_cfg = { + .clk_cfg = clk_cfg, + .slot_cfg = slot_cfg, + .gpio_cfg = gpio_cfg, + }; + + esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT); + if (err != ESP_OK) { + return err; + } + + i2s_channel_enable(this->tx_handle_); + + return ESP_OK; +} + +} // namespace esphome::i2s_audio + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h new file mode 100644 index 00000000000..7b7f8b647d7 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "i2s_audio_speaker.h" + +namespace esphome::i2s_audio { + +enum class I2SCommFmt : uint8_t { + STANDARD, // Philips / I2S standard + PCM, // PCM short + MSB, // MSB / left-justified +}; + +/// @brief Standard I2S speaker implementation. +/// Outputs PCM audio data directly to an I2S DAC using the standard I2S protocol. +class I2SAudioSpeaker : public I2SAudioSpeakerBase { + public: + void dump_config() override; + + void set_i2s_comm_fmt(I2SCommFmt fmt) { this->i2s_comm_fmt_ = fmt; } + + protected: + void run_speaker_task() override; + esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override; + + I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD}; +}; + +} // namespace esphome::i2s_audio + +#endif // USE_ESP32 From c48ab2ef923ce0e7679ee3a76621ce39670cf034 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:05:15 -0400 Subject: [PATCH 1597/2056] [io_expanders] Self-heal interrupt-driven expanders when INT stays asserted across the read (#15923) --- esphome/components/mcp23016/mcp23016.cpp | 5 ++++- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 5 ++++- esphome/components/pca6416a/pca6416a.cpp | 5 ++++- esphome/components/pca9554/pca9554.cpp | 6 ++++-- esphome/components/pcf8574/pcf8574.cpp | 6 ++++-- esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 5 ++++- esphome/components/tca9555/tca9555.cpp | 5 ++++- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 118a77ce378..b7a9cfd0ce7 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -37,7 +37,10 @@ void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_co void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 6efd04e2463..8a87dac1433 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -21,7 +21,10 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander: void loop() override { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index dc7463b01ba..d617336e7e2 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -62,7 +62,10 @@ void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enabl void PCA6416AComponent::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index ac4f119dfee..393bbfd61ef 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -50,8 +50,10 @@ void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_ void PCA9554Component::loop() { // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { - // Interrupt-driven: disable loop until next interrupt fires + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index bf4a9442a26..8fe8526797b 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -31,8 +31,10 @@ void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_ void PCF8574Component::loop() { // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { - // Interrupt-driven: disable loop until next interrupt fires + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 6e8631022a7..00f29983be8 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -82,7 +82,10 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 3eb794df441..2fefe08c0d7 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -57,7 +57,10 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { } void TCA9555Component::loop() { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } From 36720c8495e3428cce7caa922d2f94aad2a8c704 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 22 Apr 2026 16:16:14 -0500 Subject: [PATCH 1598/2056] [usb_uart] Derive TX output chunk count from `buffer_size` config (#15909) --- esphome/components/usb_uart/__init__.py | 13 ++++++++++++- esphome/components/usb_uart/usb_uart.h | 5 +++-- esphome/core/defines.h | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 0e8994a3ed2..d542788fb97 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -116,12 +116,23 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # The output chunk pool/queue are compile-time-sized templates shared by all + # USBUartChannel instances, so use the largest buffer_size across every channel + # of every device. Each chunk is 64 bytes (USB FS MPS); add one extra slot + # because LockFreeQueue<T,N> is a ring buffer that wastes one entry. + max_buffer_size = max( + channel[CONF_BUFFER_SIZE] + for device in config + for channel in device[CONF_CHANNELS] + ) + output_chunk_count = max_buffer_size // 64 + 1 + cg.add_define("USB_UART_OUTPUT_CHUNK_COUNT", output_chunk_count) + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE]) await cg.register_parented(chvar, var) - cg.add(chvar.set_rx_buffer_size(channel[CONF_BUFFER_SIZE])) cg.add(chvar.set_stop_bits(channel[CONF_STOP_BITS])) cg.add(chvar.set_data_bits(channel[CONF_DATA_BITS])) cg.add(chvar.set_parity(channel[CONF_PARITY])) diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 8e8e65032d6..f9648b795b6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -132,8 +132,9 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon friend class USBUartTypeCH34X; public: - // Number of output chunk slots per channel (8 × 64 bytes = 512 bytes peak, lazily allocated) - static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = 8; + // Number of output chunk slots per channel, derived from buffer_size config. + // Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1). + static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT; USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {} void write_array(const uint8_t *data, size_t len) override; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 07cac97e171..0978437039a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -286,6 +286,7 @@ #define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2 #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 +#define USB_UART_OUTPUT_CHUNK_COUNT 5 #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) From 70b1d9a087055dc1f0fc256b8a951056640bd579 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 22 Apr 2026 17:57:15 -0500 Subject: [PATCH 1599/2056] [api_protobuf] Support compound `ifdef` conditions in proto generator (#15930) --- script/api_protobuf/api_protobuf.py | 42 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 73e0859d5eb..c10479a726e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -65,11 +65,31 @@ _enum_max_values: dict[str, int] = {} _message_desc_map: dict[str, Any] = {} +def _make_ifdef_line(condition: str) -> str: + """Return the correct preprocessor open-guard line for a condition string. + + Simple identifiers use ``#ifdef IDENTIFIER``. + Compound expressions (containing ``||`` or ``&&``) use + ``#if defined(A) || defined(B)`` so that the preprocessor + evaluates them correctly. + """ + if any(op in condition for op in ("||", "&&", "!")): + # Replace each bare identifier token with defined(token) + expr = re.sub(r"\b([A-Za-z_]\w*)\b", r"defined(\1)", condition) + return f"#if {expr}" + return f"#ifdef {condition}" + + def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" lines = [] for line in text.splitlines(): - if line == "" or line.startswith("#ifdef") or line.startswith("#endif"): + if ( + line == "" + or line.startswith("#ifdef") + or line.startswith("#if ") + or line.startswith("#endif") + ): p = "" else: p = padding @@ -82,7 +102,7 @@ def indent(text: str, padding: str = " ") -> str: def wrap_with_ifdef(content: str | list[str], ifdef: str | None) -> list[str]: - """Wrap content with #ifdef directives if ifdef is provided. + """Wrap content with #ifdef / #if directives if ifdef is provided. Args: content: Single string or list of strings to wrap @@ -96,7 +116,7 @@ def wrap_with_ifdef(content: str | list[str], ifdef: str | None) -> list[str]: return [content] return content - result = [f"#ifdef {ifdef}"] + result = [_make_ifdef_line(ifdef)] if isinstance(content, str): result.append(content) else: @@ -3021,7 +3041,7 @@ def build_service_message_type( if source in (SOURCE_BOTH, SOURCE_CLIENT): # Only add ifdef when we're actually generating content if ifdef is not None: - hout += f"#ifdef {ifdef}\n" + hout += _make_ifdef_line(ifdef) + "\n" # Generate receive handler and switch case func = f"on_{snake}" has_fields = any(not field.options.deprecated for field in mt.field) @@ -3302,8 +3322,8 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint content += "#endif\n" dump_cpp += "#endif\n" if enum_ifdef is not None: - content += f"#ifdef {enum_ifdef}\n" - dump_cpp += f"#ifdef {enum_ifdef}\n" + content += _make_ifdef_line(enum_ifdef) + "\n" + dump_cpp += _make_ifdef_line(enum_ifdef) + "\n" current_ifdef = enum_ifdef content += s @@ -3378,9 +3398,9 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint if dump_cpp: dump_cpp += "#endif\n" if msg_ifdef is not None: - content += f"#ifdef {msg_ifdef}\n" - cpp += f"#ifdef {msg_ifdef}\n" - dump_cpp += f"#ifdef {msg_ifdef}\n" + content += _make_ifdef_line(msg_ifdef) + "\n" + cpp += _make_ifdef_line(msg_ifdef) + "\n" + dump_cpp += _make_ifdef_line(msg_ifdef) + "\n" current_ifdef = msg_ifdef content += s @@ -3529,7 +3549,7 @@ static const char *const TAG = "api.service"; for id_ in sorted(ids): _, ifdef, case_label = RECEIVE_CASES[id_] if ifdef: - result += f"#ifdef {ifdef}\n" + result += _make_ifdef_line(ifdef) + "\n" result += f" case {case_label}: {comment}\n" if ifdef: result += "#endif\n" @@ -3572,7 +3592,7 @@ static const char *const TAG = "api.service"; out += " switch (msg_type) {\n" for i, (case, ifdef, case_label) in cases: if ifdef is not None: - out += f"#ifdef {ifdef}\n" + out += _make_ifdef_line(ifdef) + "\n" c = f" case {case_label}: {{\n" c += indent(case, " ") + "\n" From 22f6791dea1f0d8602ead29cabe1360d09fd3ba8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:13:42 +1000 Subject: [PATCH 1600/2056] [lvgl] Fix format of hello world page (#15868) --- esphome/components/lvgl/hello_world.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index bbbd34e30a9..7bf068cc5d0 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -89,10 +89,12 @@ id: hello_world_label_ text: "Hello World!" align: center - - obj: + - container: id: hello_world_qrcode_ outline_width: 0 border_width: 0 + height: 100 + width: 100 hidden: !lambda |- return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400; widgets: From 3d0a2421a65a55f71b565feacc0ff6d5147ebc2a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:14:15 +1000 Subject: [PATCH 1601/2056] [lvgl] Fix overloads for setting images on styles (#15864) --- esphome/components/lvgl/lvgl_esphome.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3ec1d247d8e..146866f5bd7 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -88,6 +88,12 @@ inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { ::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } +inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) { + ::lv_style_set_bg_image_src(style, image->get_lv_image_dsc()); +} +inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) { + ::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc()); +} #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { From dc5b06285d84a311b1b103db97756ebe9f662912 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:15:51 +1000 Subject: [PATCH 1602/2056] [lvgl] Fix update of textarea attached to keyboard (#15866) --- esphome/components/lvgl/widgets/keyboard.py | 26 ++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index 029ca5f6846..c5628cee3cd 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -52,19 +52,23 @@ class KeyboardType(WidgetType): if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) if textarea := config.get(CONF_TEXTAREA): - # If a textarea is configured, it must be generated before the keyboard can attach it. - # If not yet configured, defer the attachment code. + if not is_widget_completed(textarea): + # Can only happen for an initial config, where the keyboard is configured before the + # textarea, so it's ok to always emit into the global context + async def add_textarea(): + async with LvContext(): + await w.set_property( + CONF_TEXTAREA, + (await get_widgets(config, CONF_TEXTAREA))[0].obj, + ) - async def add_textarea(): - async with LvContext(): - await w.set_property( - CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj - ) - - if is_widget_completed(textarea): - await add_textarea() - else: CORE.add_job(add_textarea) + else: + # Handles updates in automations, and properly ordered initial config. Code is generated + # into the enclosing context (main or lambda) + await w.set_property( + CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj + ) keyboard_spec = KeyboardType() From 06e5931ad736abf32f71140f4943f7d90eff3f11 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:27:34 -0400 Subject: [PATCH 1603/2056] [image] Fix rodata bloat for multi-frame RGB565+alpha animations (#15873) --- esphome/components/image/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 7db50597e6f..8375ab91d3e 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -756,7 +756,7 @@ async def write_image(config, all_frames=False): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() - encoder.end_image() + encoder.end_image() rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) From 92cb6dd7fd3c15452643ef5b58fd327ecba0dc77 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:33:48 -0400 Subject: [PATCH 1604/2056] [core] Fix Pvariable placement new losing subclass identity (#15881) --- esphome/cpp_generator.py | 50 ++++++++++++------- tests/component_tests/ili9xxx/__init__.py | 0 .../ili9xxx/config/ili9xxx_test.yaml | 20 ++++++++ tests/component_tests/ili9xxx/test_ili9xxx.py | 31 ++++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 tests/component_tests/ili9xxx/__init__.py create mode 100644 tests/component_tests/ili9xxx/config/ili9xxx_test.yaml create mode 100644 tests/component_tests/ili9xxx/test_ili9xxx.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index cf90b878e1f..c622207daca 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -606,33 +606,43 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": if isinstance(rhs, MockObj) and rhs.is_new_expr: # For 'new' allocations, use placement new into static storage # to avoid heap fragmentation on embedded devices. - the_type = id_.type + # + # Storage must be sized and aligned for the actual instantiated class, + # which may be a subclass of id_.type (e.g. `cv.declare_id(BaseClass)` + # combined with `SubClass.new()` — used by ili9xxx, waveshare_epaper, + # etc. to select a model-specific constructor). Using id_.type would + # run the base-class default constructor instead, silently losing any + # subclass initialization. Template args live on the CallExpression + # and are re-emitted below. + call_expr = rhs.base + assert isinstance(call_expr, CallExpression), ( + f"Expected CallExpression for placement new, got {type(call_expr)}" + ) + actual_type = rhs.new_type if rhs.new_type is not None else id_.type + if call_expr.template_args is not None: + actual_type = f"{actual_type}{call_expr.template_args}" + pointer_type = id_.type # Extract component namespace from type for memory analysis attribution - component_ns = _extract_component_ns(str(the_type)) + component_ns = _extract_component_ns(str(actual_type)) storage_name = f"{component_ns}__{id_.id}__pstorage" # Declare aligned byte array for the object storage CORE.add_global( RawStatement( - f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];" + f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];" ) ) + # Pointer declaration uses id_.type to preserve the declared base-class + # pointer type for downstream callers (polymorphism through base ptr). CORE.add_global( AssignmentExpression( - f"static {the_type}", + f"static {pointer_type}", "*const ", id_, - MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"), + MockObj(f"reinterpret_cast<{pointer_type} *>({storage_name})"), ) ) - # Extract args from the CallExpression and rebuild as placement new. - # Template args are already encoded in the_type (e.g. GlobalsComponent<int>), - # so we only pass the constructor args, not template_args. - call_expr = rhs.base - assert isinstance(call_expr, CallExpression), ( - f"Expected CallExpression for placement new, got {type(call_expr)}" - ) - placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args) CORE.add(ExpressionStatement(placement_new)) else: decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) @@ -869,12 +879,16 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op", "is_new_expr") + __slots__ = ("base", "op", "is_new_expr", "new_type") - def __init__(self, base, op=".", is_new_expr=False) -> None: + def __init__(self, base, op=".", is_new_expr=False, new_type=None) -> None: self.base = base self.op = op self.is_new_expr = is_new_expr + # For `is_new_expr=True` objects, `new_type` holds the class name being + # constructed (e.g. "ili9xxx::ILI9XXXST7789V"). Needed by Pvariable so + # placement new uses the actual subclass rather than id_.type. + self.new_type = new_type def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -889,7 +903,9 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op, is_new_expr=self.is_new_expr) + return MockObj( + call, self.op, is_new_expr=self.is_new_expr, new_type=self.new_type + ) def __str__(self): return str(self.base) @@ -903,7 +919,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->", is_new_expr=True) + return MockObj(f"new {self.base}", "->", is_new_expr=True, new_type=self.base) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" diff --git a/tests/component_tests/ili9xxx/__init__.py b/tests/component_tests/ili9xxx/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml b/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml new file mode 100644 index 00000000000..bc6148b8d85 --- /dev/null +++ b/tests/component_tests/ili9xxx/config/ili9xxx_test.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: arduino + +spi: + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: ili9xxx + id: tft_display + model: ST7789V + cs_pin: GPIO5 + dc_pin: GPIO17 + reset_pin: GPIO16 + invert_colors: false diff --git a/tests/component_tests/ili9xxx/test_ili9xxx.py b/tests/component_tests/ili9xxx/test_ili9xxx.py new file mode 100644 index 00000000000..3919eb3823e --- /dev/null +++ b/tests/component_tests/ili9xxx/test_ili9xxx.py @@ -0,0 +1,31 @@ +"""Tests for the ili9xxx component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_ili9xxx_placement_new_uses_model_subclass( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Regression test for ili9xxx picking the right constructor under placement new. + + ili9xxx declares the ID as the base ``ILI9XXXDisplay`` but constructs a + model-specific subclass (e.g. ``ILI9XXXST7789V``) via ``MODELS[...].new()``. + Pvariable must emit placement new for the subclass — otherwise the base + default constructor runs and the panel is left with a null init sequence + and 0x0 dimensions, producing a silent blank screen. + """ + main_cpp = generate_main(component_config_path("ili9xxx_test.yaml")) + + # Storage is sized for the subclass so the full object fits. + assert "sizeof(ili9xxx::ILI9XXXST7789V)" in main_cpp + assert "alignas(ili9xxx::ILI9XXXST7789V)" in main_cpp + # Pointer is declared as the base type for polymorphism. + assert "static ili9xxx::ILI9XXXDisplay *const tft_display" in main_cpp + # Placement new runs the subclass constructor — this is the actual regression fix. + assert "new(tft_display) ili9xxx::ILI9XXXST7789V()" in main_cpp + # Base-class default constructor must NOT be used. + assert "new(tft_display) ili9xxx::ILI9XXXDisplay()" not in main_cpp From 5c2ceb63e030c567933a80cda8102da844355206 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:47:07 -0400 Subject: [PATCH 1605/2056] [ld2412] Fix null deref in set_basic_config when entities unconfigured (#15893) --- esphome/components/ld2412/ld2412.cpp | 42 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 38e1a59abab..2955852200e 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -766,32 +766,38 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } void LD2412Component::set_basic_config() { + uint8_t min_gate = 1; + uint8_t max_gate = TOTAL_GATES; + uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT; + uint8_t out_pin_level = 0x01; + #ifdef USE_NUMBER - if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || - !this->timeout_number_->has_state()) { - return; + if (this->min_distance_gate_number_ != nullptr) { + if (!this->min_distance_gate_number_->has_state()) + return; + min_gate = static_cast<int>(this->min_distance_gate_number_->state); + } + if (this->max_distance_gate_number_ != nullptr) { + if (!this->max_distance_gate_number_->has_state()) + return; + max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1; + } + if (this->timeout_number_ != nullptr) { + if (!this->timeout_number_->has_state()) + return; + timeout = static_cast<int>(this->timeout_number_->state); } #endif #ifdef USE_SELECT - if (!this->out_pin_level_select_->has_state()) { - return; + if (this->out_pin_level_select_ != nullptr) { + if (!this->out_pin_level_select_->has_state()) + return; + out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()); } #endif uint8_t value[5] = { -#ifdef USE_NUMBER - lowbyte(static_cast<int>(this->min_distance_gate_number_->state)), - lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1), - lowbyte(static_cast<int>(this->timeout_number_->state)), - highbyte(static_cast<int>(this->timeout_number_->state)), -#else - 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, -#endif -#ifdef USE_SELECT - find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()), -#else - 0x01, // Default value if not using select -#endif + lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level, }; this->set_config_mode_(true); this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); From 629da4d878789f0f0da91ea5a633635738731c7d Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 21 Apr 2026 18:25:05 -0500 Subject: [PATCH 1606/2056] [esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 (#15882) --- esphome/components/esp32/__init__.py | 95 +++++++++++--- esphome/components/esp32/post_build.py.script | 123 +++++++++++++++++- .../esp32/dummy_signing_key_v1_ecdsa.pem | 7 + .../esp32/test-signed_ota_v1.esp32-idf.yaml | 10 ++ 4 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 tests/components/esp32/dummy_signing_key_v1_ecdsa.pem create mode 100644 tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a68614cb436..77b405a4497 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -128,23 +128,30 @@ ASSERTION_LEVELS = { SIGNING_SCHEMES = { "rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME", "ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME", + "ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME", } -# Chip variants that only support one signing scheme for Secure Boot V2. +# Chip variants that only support one V2 signing scheme. # Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h. -# Variants not listed in either set support both RSA and ECDSA +# Variants not listed in either set support both RSA and ECDSA V2 # (e.g. C5, C6, H2, P4). New variants should be added to the # appropriate set if they only support one scheme. -SIGNED_OTA_RSA_ONLY_VARIANTS = { - VARIANT_ESP32, +# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only +# when minimum_chip_revision >= 3.0, which requires special handling. +SIGNED_OTA_V2_RSA_ONLY_VARIANTS = { VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32C3, } -SIGNED_OTA_ECC_ONLY_VARIANTS = { +SIGNED_OTA_V2_ECC_ONLY_VARIANTS = { VARIANT_ESP32C2, VARIANT_ESP32C61, } +# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32. +# Based on SOC_SECURE_BOOT_V1 in soc_caps.h. +SIGNED_OTA_V1_ECDSA_VARIANTS = { + VARIANT_ESP32, +} COMPILER_OPTIMIZATIONS = { "DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG", @@ -991,25 +998,73 @@ def final_validate(config): if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): scheme = signed_ota[CONF_SIGNING_SCHEME] variant = config[CONF_VARIANT] - scheme_variant_conflicts = { - "ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"), - "rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"), - } - if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[ - 0 - ]: + min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION) + scheme_path = [ + CONF_FRAMEWORK, + CONF_ADVANCED, + CONF_SIGNED_OTA_VERIFICATION, + CONF_SIGNING_SCHEME, + ] + + # V1 ECDSA is only available on the original ESP32 + if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS: errs.append( cv.Invalid( - f"Signing scheme '{scheme}' is not supported on " - f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", - path=[ - CONF_FRAMEWORK, - CONF_ADVANCED, - CONF_SIGNED_OTA_VERIFICATION, - CONF_SIGNING_SCHEME, - ], + f"Signing scheme 'ecdsa_v1' is only supported on " + f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. " + f"Use 'rsa3072' or 'ecdsa256' instead.", + path=scheme_path, ) ) + elif variant == VARIANT_ESP32: + # On ESP32, V2 RSA requires minimum_chip_revision >= 3.0 + # Note: string comparison works here because cv.one_of constrains + # min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1"). + if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"): + errs.append( + cv.Invalid( + f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} " + f"requires minimum_chip_revision: '3.0' or higher " + f"(Secure Boot V2 RSA needs chip revision 3.0+). " + f"For older chip revisions, use 'ecdsa_v1' instead.", + path=scheme_path, + ) + ) + # ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC) + elif scheme == "ecdsa256": + errs.append( + cv.Invalid( + f"Signing scheme 'ecdsa256' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with " + f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.", + path=scheme_path, + ) + ) + # V1 on rev 3.0+ -- suggest V2 RSA for stronger security + elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0": + _LOGGER.info( + "Using Secure Boot V1 ECDSA on %s rev %s. " + "Consider using 'rsa3072' (Secure Boot V2 RSA) for " + "stronger security on chip revision 3.0+.", + VARIANT_FRIENDLY[variant], + min_rev, + ) + else: + # Non-ESP32 variants: check V2 scheme-variant compatibility + scheme_variant_conflicts = { + "ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"), + "rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"), + } + if ( + conflict := scheme_variant_conflicts.get(scheme) + ) and variant in conflict[0]: + errs.append( + cv.Invalid( + f"Signing scheme '{scheme}' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", + path=scheme_path, + ) + ) if CONF_OTA not in full_config: _LOGGER.warning( "Signed OTA verification is enabled but no OTA component is configured. " diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 8d13214259a..b329f6b82b9 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -5,6 +5,7 @@ import json # noqa: E402 import os # noqa: E402 import pathlib # noqa: E402 import shutil # noqa: E402 +import subprocess # noqa: E402 from glob import glob # noqa: E402 @@ -25,6 +26,114 @@ def _parse_sdkconfig(sdkconfig_path): return options +def _generate_v1_verification_key(env): + """Generate the V1 ECDSA verification key binary and assembly source file. + + Secure Boot V1 embeds the public verification key directly in the app binary + as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates + these files via custom commands, but PlatformIO's SCons bridge does not execute + them. This function replicates that logic: + 1. Extracts the raw public key from the PEM signing key using espsecure. + 2. Generates the .S assembly source that embeds the key bytes. + """ + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + project_dir = pathlib.Path(env.subst("$PROJECT_DIR")) + pioenv = env.subst("$PIOENV") + sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}") + + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y": + return + + bin_path = build_dir / "signature_verification_key.bin" + asm_path = build_dir / "signature_verification_key.bin.S" + + # Determine the source of the verification key + if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y": + # Extract public key from the signing key + signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY") + if not signing_key: + return + signing_key_path = pathlib.Path(signing_key) + if not signing_key_path.exists(): + print(f"Error: V1 ECDSA signing key not found: {signing_key_path}") + env.Exit(1) + return + + if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime: + python_exe = env.subst("$PYTHONEXE") + result = subprocess.run( + [python_exe, "-m", "espsecure", "extract_public_key", + "--keyfile", str(signing_key_path), str(bin_path)], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f"Error extracting V1 verification key: {result.stderr}") + env.Exit(1) + return + print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}") + else: + # User-provided verification key -- should already be a raw binary file + verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY") + if not verification_key: + return + verification_key_path = pathlib.Path(verification_key) + if not verification_key_path.exists(): + print(f"Error: Verification key not found: {verification_key_path}") + env.Exit(1) + return + shutil.copyfile(str(verification_key_path), str(bin_path)) + + if not bin_path.exists(): + return + + # Generate the .S assembly file from the binary key data. + # Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin. + # The file is needed in both the app build dir and the bootloader build dir, since + # the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT + # is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that + # normally generate these files. + data = bin_path.read_bytes() + varname = "signature_verification_key_bin" + + lines = [] + lines.append(f"/* Data converted from {bin_path.name} */") + lines.append(".data") + lines.append("#if !defined (__APPLE__) && !defined (__linux__)") + lines.append(".section .rodata.embedded") + lines.append("#endif") + lines.append(f"\n.global {varname}") + lines.append(f"{varname}:") + lines.append(f"\n.global _binary_{varname}_start") + lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */") + + # Format binary data as .byte lines (16 bytes per line) + for i in range(0, len(data), 16): + chunk = data[i:i + 16] + hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk) + lines.append(f".byte {hex_bytes}") + + lines.append(f"\n.global _binary_{varname}_end") + lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */") + lines.append(f"\n.global {varname}_length") + lines.append(f"{varname}_length:") + lines.append(f".long {len(data)}") + lines.append("") + lines.append('#if defined (__linux__)') + lines.append('.section .note.GNU-stack,"",@progbits') + lines.append("#endif") + + asm_content = "\n".join(lines) + "\n" + + # Write to app build dir and bootloader build dir + asm_path.write_text(asm_content) + bootloader_dir = build_dir / "bootloader" + if bootloader_dir.is_dir(): + bootloader_bin = bootloader_dir / "signature_verification_key.bin" + bootloader_asm = bootloader_dir / "signature_verification_key.bin.S" + shutil.copyfile(str(bin_path), str(bootloader_bin)) + bootloader_asm.write_text(asm_content) + + def sign_firmware(source, target, env): """ Sign the firmware binary using espsecure.py if signed OTA verification is enabled. @@ -55,9 +164,12 @@ def sign_firmware(source, target, env): env.Exit(1) return - # ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes), - # so the espsecure signature version is always 2. - sign_version = "2" + # Determine espsecure signature version from the signing scheme: + # V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2. + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y": + sign_version = "1" + else: + sign_version = "2" firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" firmware_path = build_dir / firmware_name @@ -217,6 +329,11 @@ def esp32_copy_ota_bin(source, target, env): print(f"Copied firmware to {new_file_name}") +# Generate V1 ECDSA verification key files before build starts. +# Workaround for PlatformIO not executing CMake custom commands that extract +# the public key and generate the .S assembly file for Secure Boot V1. +_generate_v1_verification_key(env) # noqa: F821 + # Run signing first, then merge, then ota copy env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 diff --git a/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem b/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem new file mode 100644 index 00000000000..bd092056069 --- /dev/null +++ b/tests/components/esp32/dummy_signing_key_v1_ecdsa.pem @@ -0,0 +1,7 @@ +*** DO NOT USE THIS KEY...EVER *** +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZIp96p7Z7QN6vxOFE5FdRNm535vW81Ax07KnGxVjiMoAoGCCqGSM49 +AwEHoUQDQgAEK+fBQDn1Q+r5lGwcDoMUgeg2Aq16LLrLUz7xWI6mS0PUClzolDIo +eaV/Pfjl7zAvkbQQsZq3rTNnr1eGAk5P+A== +-----END EC PRIVATE KEY----- +*** DO NOT USE THIS KEY...EVER *** diff --git a/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml b/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml new file mode 100644 index 00000000000..b32e157daf6 --- /dev/null +++ b/tests/components/esp32/test-signed_ota_v1.esp32-idf.yaml @@ -0,0 +1,10 @@ +esp32: + variant: esp32 + framework: + type: esp-idf + advanced: + signed_ota_verification: + signing_key: ../../components/esp32/dummy_signing_key_v1_ecdsa.pem + signing_scheme: ecdsa_v1 + +<<: !include common.yaml From 2a3bd8bc85d8fc63ec4f6e7b52c07af93873b1d1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:05:15 -0400 Subject: [PATCH 1607/2056] [io_expanders] Self-heal interrupt-driven expanders when INT stays asserted across the read (#15923) --- esphome/components/mcp23016/mcp23016.cpp | 5 ++++- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 5 ++++- esphome/components/pca6416a/pca6416a.cpp | 5 ++++- esphome/components/pca9554/pca9554.cpp | 6 ++++-- esphome/components/pcf8574/pcf8574.cpp | 6 ++++-- esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 5 ++++- esphome/components/tca9555/tca9555.cpp | 5 ++++- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 118a77ce378..b7a9cfd0ce7 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -37,7 +37,10 @@ void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_co void MCP23016::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 6efd04e2463..8a87dac1433 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -21,7 +21,10 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander: void loop() override { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index dc7463b01ba..d617336e7e2 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -62,7 +62,10 @@ void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enabl void PCA6416AComponent::loop() { // Invalidate cache at the start of each loop this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index ac4f119dfee..393bbfd61ef 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -50,8 +50,10 @@ void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_ void PCA9554Component::loop() { // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { - // Interrupt-driven: disable loop until next interrupt fires + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index bf4a9442a26..8fe8526797b 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -31,8 +31,10 @@ void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_ void PCF8574Component::loop() { // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { - // Interrupt-driven: disable loop until next interrupt fires + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 6e8631022a7..00f29983be8 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -82,7 +82,10 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 3eb794df441..2fefe08c0d7 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -57,7 +57,10 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { } void TCA9555Component::loop() { this->reset_pin_cache_(); - if (this->interrupt_pin_ != nullptr) { + // Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the + // I2C read leave INT asserted without re-firing a falling edge, which would strand us with + // stale state forever; keep looping until the line is released so we self-heal. + if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) { this->disable_loop(); } } From 76eb8f697f43b4777dffa66a327024b027a46d0e Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Wed, 22 Apr 2026 16:16:14 -0500 Subject: [PATCH 1608/2056] [usb_uart] Derive TX output chunk count from `buffer_size` config (#15909) --- esphome/components/usb_uart/__init__.py | 13 ++++++++++++- esphome/components/usb_uart/usb_uart.h | 5 +++-- esphome/core/defines.h | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 0e8994a3ed2..d542788fb97 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -116,12 +116,23 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # The output chunk pool/queue are compile-time-sized templates shared by all + # USBUartChannel instances, so use the largest buffer_size across every channel + # of every device. Each chunk is 64 bytes (USB FS MPS); add one extra slot + # because LockFreeQueue<T,N> is a ring buffer that wastes one entry. + max_buffer_size = max( + channel[CONF_BUFFER_SIZE] + for device in config + for channel in device[CONF_CHANNELS] + ) + output_chunk_count = max_buffer_size // 64 + 1 + cg.add_define("USB_UART_OUTPUT_CHUNK_COUNT", output_chunk_count) + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE]) await cg.register_parented(chvar, var) - cg.add(chvar.set_rx_buffer_size(channel[CONF_BUFFER_SIZE])) cg.add(chvar.set_stop_bits(channel[CONF_STOP_BITS])) cg.add(chvar.set_data_bits(channel[CONF_DATA_BITS])) cg.add(chvar.set_parity(channel[CONF_PARITY])) diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 8e8e65032d6..f9648b795b6 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -132,8 +132,9 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon friend class USBUartTypeCH34X; public: - // Number of output chunk slots per channel (8 × 64 bytes = 512 bytes peak, lazily allocated) - static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = 8; + // Number of output chunk slots per channel, derived from buffer_size config. + // Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1). + static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT; USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {} void write_array(const uint8_t *data, size_t len) override; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d8b4faced9e..1a8f3ef0bff 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -284,6 +284,7 @@ #define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2 #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 +#define USB_UART_OUTPUT_CHUNK_COUNT 5 #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7) From 00b71208a604bd84f8cee874e4794a2600c59f6d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:18:39 +1200 Subject: [PATCH 1609/2056] Bump version to 2026.4.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index deb57df1d3e..1cd12551dd1 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.1 +PROJECT_NUMBER = 2026.4.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 1f5b3b6c577..ef37cb2df6c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.1" +__version__ = "2026.4.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 6253947311c112ef24de67ca785aab622b974139 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:12:02 -0500 Subject: [PATCH 1610/2056] Bump click from 8.3.2 to 8.3.3 (#15927) 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 9e59bb59d00..821ca1927a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzdata>=2026.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 -click==8.3.2 +click==8.3.3 esphome-dashboard==20260408.1 aioesphomeapi==44.19.0 zeroconf==0.148.0 From 17f92698410301ee21a7546b45f945f7270406dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:12:15 -0500 Subject: [PATCH 1611/2056] Update wheel requirement from <0.47,>=0.43 to >=0.43,<0.48 (#15926) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a744286e888..dc6785001d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==82.0.1", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.1", "wheel>=0.43,<0.48"] build-backend = "setuptools.build_meta" [project] From 224cc7b4199a01d5c365996c602379b7fc078135 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:35:00 +1000 Subject: [PATCH 1612/2056] [lvgl] Triggers on tabview tabs fix (#15935) --- esphome/components/lvgl/widgets/tabview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 108bb38df58..5e9e0494dd5 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -22,7 +22,7 @@ from ..defines import ( literal, ) from ..lv_validation import animated, lv_int, size -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties @@ -83,8 +83,8 @@ class TabviewType(WidgetType): await w.set_property("tab_bar_size", await size.process(config[CONF_SIZE])) for tab_conf in config[CONF_TABS]: w_id = tab_conf[CONF_ID] - tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) - tab_widget = Widget.create(w_id, tab_obj, obj_spec) + tab_obj = lv_Pvariable(lv_tab_t, w_id) + tab_widget = Widget.create(w_id, tab_obj, obj_spec, tab_conf) lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) await set_obj_properties(tab_widget, tab_conf) await add_widgets(tab_widget, tab_conf) From e1d629f0d2f739de4b04f4c7d1b10ef90c2aa513 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:35:13 +1200 Subject: [PATCH 1613/2056] [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) --- esphome/components/time/__init__.py | 7 +++ tests/unit_tests/components/test_time.py | 67 +++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 37c08b3a126..3295366fea3 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,3 +1,4 @@ +import errno from importlib import resources import logging @@ -74,6 +75,12 @@ def _load_tzdata(iana_key: str) -> bytes | None: return (resources.files(package) / resource).read_bytes() except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None + except OSError as e: + # Windows raises EINVAL for paths with NTFS-illegal chars (e.g. '<'/'>' + # in POSIX TZ strings like "<+08>-8" that validate_tz feeds back here). + if e.errno == errno.EINVAL: + return None + raise def _extract_tz_string(tzfile: bytes) -> str: diff --git a/tests/unit_tests/components/test_time.py b/tests/unit_tests/components/test_time.py index 48988fb03f9..6325bfbe75c 100644 --- a/tests/unit_tests/components/test_time.py +++ b/tests/unit_tests/components/test_time.py @@ -1,6 +1,11 @@ """Tests for time component cron expression parsing.""" -from esphome.components.time import _parse_cron_part +import errno +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.components.time import _load_tzdata, _parse_cron_part, validate_tz def test_star_slash_seconds() -> None: @@ -78,3 +83,63 @@ def test_range() -> None: def test_single_value() -> None: assert _parse_cron_part("30", 0, 59, {}) == {30} + + +def _mock_resources_with_error(error: Exception) -> MagicMock: + """Return a mock of importlib.resources.files where read_bytes raises error.""" + leaf = MagicMock() + leaf.read_bytes.side_effect = error + package = MagicMock() + package.__truediv__.return_value = leaf + return MagicMock(return_value=package) + + +def test_load_tzdata_returns_none_on_windows_einval() -> None: + """On Windows, opening a tzdata path with NTFS-illegal chars raises OSError(EINVAL). + + Regression test for crash when the system TZ resolves to a POSIX string like + "<+08>-8" (Asia/Shanghai, IST, etc.) and is fed back into _load_tzdata by + validate_tz to check whether it is also a valid IANA key. + """ + err = OSError(errno.EINVAL, "Invalid argument") + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(err), + ): + assert _load_tzdata("<+08>-8") is None + + +def test_load_tzdata_propagates_unexpected_oserror() -> None: + """Unrelated OSErrors (e.g. PermissionError) must not be swallowed.""" + with ( + patch( + "esphome.components.time.resources.files", + _mock_resources_with_error( + PermissionError(errno.EACCES, "Permission denied") + ), + ), + pytest.raises(PermissionError), + ): + _load_tzdata("Some/Zone") + + +def test_load_tzdata_returns_none_on_file_not_found() -> None: + """Existing behavior: missing tz file returns None rather than raising.""" + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(FileNotFoundError()), + ): + assert _load_tzdata("Not/A/Zone") is None + + +def test_validate_tz_accepts_posix_string_when_read_bytes_raises_einval() -> None: + """validate_tz must not crash when _load_tzdata hits the Windows EINVAL path. + + Simulates the Windows case where the auto-detected POSIX TZ string is fed + back through _load_tzdata and the underlying read_bytes raises errno 22. + """ + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(OSError(errno.EINVAL, "Invalid argument")), + ): + assert validate_tz("<+08>-8") == "<+08>-8" From f8167c9a70d129e2a1c1ca9cf9cea5878fe1ec30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:40:19 +0000 Subject: [PATCH 1614/2056] Bump aioesphomeapi from 44.19.0 to 44.20.0 (#15936) 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 821ca1927a8..e7ab9bc2ad3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260408.1 -aioesphomeapi==44.19.0 +aioesphomeapi==44.20.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From a881121110111ba829ab830b338dfb7675b9a979 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:06:31 -0400 Subject: [PATCH 1615/2056] [ota] Make set_auth_password() lambda-callable via empty-password opt-in (#15928) --- esphome/components/esphome/ota/__init__.py | 10 +++++++--- esphome/components/esphome/ota/ota_esphome.h | 8 ++++++++ .../ota/test-empty_password.esp8266-ard.yaml | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/components/ota/test-empty_password.esp8266-ard.yaml diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 5d35910fbdf..bfa5ffb55ce 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -150,10 +150,14 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_port(config[CONF_PORT])) - # Password could be set to an empty string and we can assume that means no password - if config.get(CONF_PASSWORD): - cg.add(var.set_auth_password(config[CONF_PASSWORD])) + # Compile the auth path whenever `password:` is present in YAML, even if empty. + # An empty password opts in to the auth code path so set_auth_password() can be + # called at runtime (e.g. to rotate the password from a lambda). When `password:` + # is omitted entirely, the auth path is excluded to save flash on small devices. + if CONF_PASSWORD in config: cg.add_define("USE_OTA_PASSWORD") + if config[CONF_PASSWORD]: + cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index f3a5952398f..53288fc0005 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -28,6 +28,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { }; #ifdef USE_OTA_PASSWORD void set_auth_password(const std::string &password) { password_ = password; } +#else + // Stub so lambdas referencing set_auth_password() produce a clear error instead of + // a cryptic "no member" diagnostic. Only fires if the stub is actually instantiated. + template<bool B = false> void set_auth_password(const std::string &) { + static_assert(B, "set_auth_password() requires the OTA auth path to be compiled. " + "Add 'password: \"\"' (empty string) to your 'ota: - platform: esphome' " + "config to enable runtime password rotation."); + } #endif // USE_OTA_PASSWORD /// Manually set the port OTA should listen on diff --git a/tests/components/ota/test-empty_password.esp8266-ard.yaml b/tests/components/ota/test-empty_password.esp8266-ard.yaml new file mode 100644 index 00000000000..e48f67e47ea --- /dev/null +++ b/tests/components/ota/test-empty_password.esp8266-ard.yaml @@ -0,0 +1,14 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + id: my_ota + port: 3287 + password: "" + +esphome: + on_boot: + then: + - lambda: id(my_ota).set_auth_password("runtime_password"); From 6f00ea1457f5fc9216c2cd3f3b251073250486ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 05:53:10 +0200 Subject: [PATCH 1616/2056] [core] Move host socket-select wake mechanism into wake.h/wake.cpp (#15931) --- .../components/socket/bsd_sockets_impl.cpp | 7 +- .../components/socket/lwip_sockets_impl.cpp | 7 +- esphome/components/socket/socket.cpp | 7 +- esphome/core/application.cpp | 167 +--------------- esphome/core/application.h | 100 +--------- esphome/core/wake.cpp | 188 +++++++++++++++++- esphome/core/wake.h | 56 ++++++ 7 files changed, 263 insertions(+), 269 deletions(-) diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 92691b17ab8..8e9968e05c0 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -6,6 +6,9 @@ #include <cstring> #include "esphome/core/application.h" +#ifdef USE_HOST +#include "esphome/core/wake.h" +#endif namespace esphome::socket { @@ -16,7 +19,7 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { #ifdef USE_LWIP_FAST_SELECT this->cached_sock_ = hook_fd_for_fast_select(this->fd_); #else - this->loop_monitored_ = App.register_socket_fd(this->fd_); + this->loop_monitored_ = wake_register_fd(this->fd_); #endif } @@ -36,7 +39,7 @@ int BSDSocketImpl::close() { this->cached_sock_ = nullptr; #else if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); + wake_unregister_fd(this->fd_); } #endif int ret = ::close(this->fd_); diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index b4eba3febf0..a6bd639c10f 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -6,6 +6,9 @@ #include <cstring> #include "esphome/core/application.h" +#ifdef USE_HOST +#include "esphome/core/wake.h" +#endif namespace esphome::socket { @@ -16,7 +19,7 @@ LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) { #ifdef USE_LWIP_FAST_SELECT this->cached_sock_ = hook_fd_for_fast_select(this->fd_); #else - this->loop_monitored_ = App.register_socket_fd(this->fd_); + this->loop_monitored_ = wake_register_fd(this->fd_); #endif } @@ -36,7 +39,7 @@ int LwIPSocketImpl::close() { this->cached_sock_ = nullptr; #else if (this->loop_monitored_) { - App.unregister_socket_fd(this->fd_); + wake_unregister_fd(this->fd_); } #endif int ret = lwip_close(this->fd_); diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index bc43b2746ee..f14ac1e2d58 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -5,13 +5,16 @@ #include <string> #include "esphome/core/log.h" #include "esphome/core/application.h" +#ifdef USE_HOST +#include "esphome/core/wake.h" +#endif namespace esphome::socket { #ifdef USE_HOST // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). -// Checks if the Application's select() loop has marked this fd as ready. -bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } +// Checks if the host wake select() loop has marked this fd as ready. +bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || wake_fd_ready(fd); } #endif // Platform-specific inet_ntop wrappers diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8612782d952..3105ff2e8b4 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -28,10 +28,6 @@ #include "esphome/components/socket/socket.h" #endif -#ifdef USE_HOST -#include <cerrno> -#endif - namespace esphome { static const char *const TAG = "app"; @@ -133,8 +129,8 @@ void Application::setup() { esphome_main_task_handle = xTaskGetCurrentTaskHandle(); #endif #ifdef USE_HOST - // Set up wake socket for waking main loop from tasks (platforms without fast select only) - this->setup_wake_loop_threadsafe_(); + // Set up wake socket for waking main loop from tasks (host platform select() loop). + wake_setup(); #endif // Ensure all active looping components are in LOOP state. @@ -510,105 +506,6 @@ void Application::enable_pending_loops_() { } } -#ifdef USE_HOST -bool Application::register_socket_fd(int fd) { - // WARNING: This function is NOT thread-safe and must only be called from the main loop - // It modifies socket_fds_ and related variables without locking - if (fd < 0) - return false; - - if (fd >= FD_SETSIZE) { - ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); - return false; - } - - this->socket_fds_.push_back(fd); - this->socket_fds_changed_ = true; - if (fd > this->max_fd_) { - this->max_fd_ = fd; - } - - return true; -} - -void Application::unregister_socket_fd(int fd) { - // WARNING: This function is NOT thread-safe and must only be called from the main loop - // It modifies socket_fds_ and related variables without locking - if (fd < 0) - return; - - for (size_t i = 0; i < this->socket_fds_.size(); i++) { - if (this->socket_fds_[i] != fd) - continue; - - // Swap with last element and pop - O(1) removal since order doesn't matter. - if (i < this->socket_fds_.size() - 1) - this->socket_fds_[i] = this->socket_fds_.back(); - this->socket_fds_.pop_back(); - this->socket_fds_changed_ = true; - // Only recalculate max_fd if we removed the current max - if (fd == this->max_fd_) { - this->max_fd_ = -1; - for (int sock_fd : this->socket_fds_) { - if (sock_fd > this->max_fd_) - this->max_fd_ = sock_fd; - } - } - return; - } -} - -#endif - -// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h -#ifdef USE_HOST -void Application::yield_with_select_(uint32_t delay_ms) { - // Fallback select() path (host platform and any future platforms without fast select). - if (!this->socket_fds_.empty()) [[likely]] { - // Update fd_set if socket list has changed - if (this->socket_fds_changed_) [[unlikely]] { - FD_ZERO(&this->base_read_fds_); - // fd bounds are validated in register_socket_fd() - for (int fd : this->socket_fds_) { - FD_SET(fd, &this->base_read_fds_); - } - this->socket_fds_changed_ = false; - } - - // Copy base fd_set before each select - this->read_fds_ = this->base_read_fds_; - - // Convert delay_ms to timeval - struct timeval tv; - tv.tv_sec = delay_ms / 1000; - tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; - - // Call select with timeout - int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); - - // Process select() result: - // ret > 0: socket(s) have data ready - normal and expected - // ret == 0: timeout occurred - normal and expected - if (ret >= 0) [[likely]] { - // Yield if zero timeout since select(0) only polls without yielding - if (delay_ms == 0) [[unlikely]] { - yield(); - } - return; - } - // ret < 0: error (EINTR is normal, anything else is unexpected) - const int err = errno; - if (err == EINTR) { - return; - } - // select() error - log and fall through to delay() - ESP_LOGW(TAG, "select() failed with errno %d", err); - } - // No sockets registered or select() failed - use regular delay - delay(delay_ms); -} -#endif // USE_HOST - // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. // Constructed via placement new in the generated setup(). @@ -628,66 +525,6 @@ alignas(Application) char app_storage[sizeof(Application)] asm( #undef ESPHOME_STRINGIFY_ #undef ESPHOME_STRINGIFY_IMPL_ -// Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ -// ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp -#ifdef USE_HOST - -void Application::setup_wake_loop_threadsafe_() { - // Create UDP socket for wake notifications - this->wake_socket_fd_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (this->wake_socket_fd_ < 0) { - ESP_LOGW(TAG, "Wake socket create failed: %d", errno); - return; - } - - // Bind to loopback with auto-assigned port - struct sockaddr_in addr = {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; // Auto-assign port - - if (::bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); - ::close(this->wake_socket_fd_); - this->wake_socket_fd_ = -1; - return; - } - - // Get the assigned address and connect to it - // Connecting a UDP socket allows using send() instead of sendto() for better performance - struct sockaddr_in wake_addr; - socklen_t len = sizeof(wake_addr); - if (::getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { - ESP_LOGW(TAG, "Wake socket address failed: %d", errno); - ::close(this->wake_socket_fd_); - this->wake_socket_fd_ = -1; - return; - } - - // Connect to self (loopback) - allows using send() instead of sendto() - // After connect(), no need to store wake_addr - the socket remembers it - if (::connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { - ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); - ::close(this->wake_socket_fd_); - this->wake_socket_fd_ = -1; - return; - } - - // Set non-blocking mode - int flags = ::fcntl(this->wake_socket_fd_, F_GETFL, 0); - ::fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); - - // Register with application's select() loop - if (!this->register_socket_fd(this->wake_socket_fd_)) { - ESP_LOGW(TAG, "Wake socket register failed"); - ::close(this->wake_socket_fd_); - this->wake_socket_fd_ = -1; - return; - } -} - -#endif // USE_HOST - void Application::get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); buffer[buffer.size() - 1] = '\0'; diff --git a/esphome/core/application.h b/esphome/core/application.h index 3d8df88d2a2..8280b3bd4bc 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,27 +27,12 @@ #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" #endif -#ifdef USE_HOST -#include <sys/select.h> -#include <sys/socket.h> -#include <unistd.h> -#include <fcntl.h> -#include <netinet/in.h> -#include <arpa/inet.h> -#endif #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif #include "esphome/core/wake.h" #include "esphome/core/entity_includes.h" -namespace esphome::socket { -#ifdef USE_HOST -/// Shared ready() helper for fd-based socket implementations. -bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) -#endif -} // namespace esphome::socket - #ifdef USE_RUNTIME_STATS namespace esphome::runtime_stats { class RuntimeStatsCollector; @@ -343,18 +328,6 @@ class Application { Scheduler scheduler; -#ifdef USE_HOST - /// Register/unregister a socket file descriptor with the host select() fallback loop. - /// USE_LWIP_FAST_SELECT builds do not use this API — sockets hook the lwIP netconn - /// event_callback directly (see socket.h hook_fd_for_fast_select) and rely on FreeRTOS - /// task notifications for wake-up. - /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. - /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop. - /// @return true if registration was successful, false if fd exceeds limits - bool register_socket_fd(int fd); - void unregister_socket_fd(int fd); -#endif - /// Wake the main event loop from another thread or callback. /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } @@ -372,21 +345,11 @@ class Application { protected: friend Component; -#ifdef USE_HOST - friend bool socket::socket_ready_fd(int fd, bool loop_monitored); -#endif #ifdef USE_RUNTIME_STATS friend class runtime_stats::RuntimeStatsCollector; #endif friend void ::setup(); friend void ::original_setup(); -#ifdef USE_HOST - friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ -#endif - -#ifdef USE_HOST - bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } -#endif /// Walk all registered components looking for any whose component_state_ /// has the given flag set. Used by Component::status_clear_*_slow_path_() @@ -460,18 +423,9 @@ class Application { void service_status_led_slow_(uint32_t time); #endif - /// Perform a delay while also monitoring socket file descriptors for readiness -#ifdef USE_HOST - // select() fallback path is too complex to inline (host platform) - void yield_with_select_(uint32_t delay_ms); -#else + /// Sleep for up to delay_ms, returning early if a wake event arrives. + /// Thin wrapper over the platform wake primitive in wake.h. inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); -#endif - -#ifdef USE_HOST - void setup_wake_loop_threadsafe_(); // Create wake notification socket - inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) -#endif // === Member variables ordered by size to minimize padding === @@ -496,9 +450,6 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop FixedVector<Component *> looping_components_{}; -#ifdef USE_HOST - std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors -#endif // StringRef members (8 bytes each: pointer + size) StringRef name_; @@ -513,11 +464,6 @@ class Application { uint32_t last_status_led_service_{0}; #endif -#ifdef USE_HOST - int max_fd_{-1}; // Highest file descriptor number for select() - int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif - // 2-byte members (grouped together for alignment) uint16_t dump_config_at_{std::numeric_limits<uint16_t>::max()}; // Index into components_ for dump_config progress uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) @@ -530,14 +476,6 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#ifdef USE_HOST - bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes - - // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) - fd_set read_fds_{}; // Working fd_set: populated by select() - fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes -#endif - // StaticVectors (largest members - contain actual array data inline) StaticVector<Component *, ESPHOME_COMPONENT_COUNT> components_{}; @@ -565,30 +503,6 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#ifdef USE_HOST -// Inline implementations for hot-path functions -// drain_wake_notifications_() is called on every loop iteration - -// Small buffer for draining wake notification bytes (1 byte sent per wake) -// Size allows draining multiple notifications per recvfrom() without wasting stack -static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void Application::drain_wake_notifications_() { - // Called from main loop to drain any pending wake notifications - // Must check is_socket_ready_() to avoid blocking on empty socket - if (this->wake_socket_fd_ >= 0 && this->is_socket_ready_(this->wake_socket_fd_)) { - char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK - // We control both ends of this loopback socket (always write 1 byte per wake), - // so no error checking needed - any errors indicate catastrophic system failure - while (::recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - wake has already occurred - } - } -} -#endif // USE_HOST - // Phase A: drain wake notifications and run the scheduler. Invoked on every // Application::loop() tick regardless of whether a component phase runs, so // scheduler items fire at their requested cadence even when the caller has @@ -598,8 +512,8 @@ inline void Application::drain_wake_notifications_() { // per-item feeds inside scheduler.call() without an extra millis(). inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) { #ifdef USE_HOST - // Drain wake notifications first to clear socket for next wake - this->drain_wake_notifications_(); + // Drain wake notifications first to clear socket for next wake. + wake_drain_notifications(); #endif return this->scheduler.call(now); } @@ -757,11 +671,11 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } } -// Inline yield_with_select_ for all paths except the select() fallback -#ifndef USE_HOST +// All platforms route loop yields through the platform wake primitive. +// On host this drains the loopback wake socket via select(); on FreeRTOS +// targets it uses task notifications; on ESP8266/RP2040 it uses esp_delay/WFE. inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { esphome::internal::wakeable_delay(delay_ms); } -#endif // !USE_HOST } // namespace esphome diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 00b08b7b915..cac88ae91ef 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -1,13 +1,20 @@ #include "esphome/core/wake.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" #ifdef USE_ESP8266 #include <coredecls.h> #endif #ifdef USE_HOST -#include "esphome/core/application.h" +#include <arpa/inet.h> +#include <cerrno> +#include <fcntl.h> +#include <netinet/in.h> +#include <sys/select.h> #include <sys/socket.h> +#include <unistd.h> +#include <vector> #endif namespace esphome { @@ -82,17 +89,188 @@ void wakeable_delay(uint32_t ms) { } // namespace internal #endif // USE_RP2040 -// === Host (UDP loopback socket) === +// === Host (UDP loopback socket + select() based fd watcher) === #ifdef USE_HOST + +static const char *const TAG = "wake"; + +namespace internal { +// File-scope state — referenced inline by wake_drain_notifications() and +// wake_fd_ready() in wake.h, and by the bodies in this file. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +int g_wake_socket_fd = -1; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +fd_set g_read_fds{}; +} // namespace internal + +namespace { +// File-local state owned entirely by the select() loop. +// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) +std::vector<int> s_socket_fds; +int s_max_fd = -1; +bool s_socket_fds_changed = false; +fd_set s_base_read_fds{}; +// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) +} // namespace + +bool wake_register_fd(int fd) { + // WARNING: not thread-safe — must be called only from the main loop. + if (fd < 0) + return false; + + if (fd >= FD_SETSIZE) { + ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); + return false; + } + + s_socket_fds.push_back(fd); + s_socket_fds_changed = true; + if (fd > s_max_fd) { + s_max_fd = fd; + } + + return true; +} + +void wake_unregister_fd(int fd) { + // WARNING: not thread-safe — must be called only from the main loop. + if (fd < 0) + return; + + for (size_t i = 0; i < s_socket_fds.size(); i++) { + if (s_socket_fds[i] != fd) + continue; + + // Swap with last element and pop — O(1) removal since order doesn't matter. + if (i < s_socket_fds.size() - 1) + s_socket_fds[i] = s_socket_fds.back(); + s_socket_fds.pop_back(); + s_socket_fds_changed = true; + // Only recalculate max_fd if we removed the current max. + if (fd == s_max_fd) { + s_max_fd = -1; + for (int sock_fd : s_socket_fds) { + if (sock_fd > s_max_fd) + s_max_fd = sock_fd; + } + } + return; + } +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + // Fallback select() path for the host platform (and any future platform + // without fast select). select() is the host equivalent of FreeRTOS task + // notify / esp_delay / WFE used on the embedded targets. + if (!s_socket_fds.empty()) [[likely]] { + // Update fd_set if socket list has changed. + if (s_socket_fds_changed) [[unlikely]] { + FD_ZERO(&s_base_read_fds); + // fd bounds are validated in wake_register_fd(). + for (int fd : s_socket_fds) { + FD_SET(fd, &s_base_read_fds); + } + s_socket_fds_changed = false; + } + + // Copy base fd_set before each select. + g_read_fds = s_base_read_fds; + + // Convert ms to timeval. + struct timeval tv; + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms - tv.tv_sec * 1000) * 1000; + + // Call select with timeout. + int ret = ::select(s_max_fd + 1, &g_read_fds, nullptr, nullptr, &tv); + + // Process select() result: + // ret > 0: socket(s) have data ready - normal and expected + // ret == 0: timeout occurred - normal and expected + if (ret >= 0) [[likely]] { + // Yield if zero timeout since select(0) only polls without yielding. + if (ms == 0) [[unlikely]] { + yield(); + } + return; + } + // ret < 0: error (EINTR is normal, anything else is unexpected). + const int err = errno; + if (err == EINTR) { + return; + } + // select() error - log and fall through to delay(). + ESP_LOGW(TAG, "select() failed with errno %d", err); + } + // No sockets registered or select() failed - use regular delay. + delay(ms); +} +} // namespace internal + void wake_loop_threadsafe() { // Set flag before sending so the consumer's gate check on the next loop() // entry observes the wake regardless of select() scheduling. wake_request_set(); - if (App.wake_socket_fd_ >= 0) { + if (internal::g_wake_socket_fd >= 0) { const char dummy = 1; - ::send(App.wake_socket_fd_, &dummy, 1, 0); + ::send(internal::g_wake_socket_fd, &dummy, 1, 0); } } -#endif + +void wake_setup() { + // Create UDP socket for wake notifications. + internal::g_wake_socket_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (internal::g_wake_socket_fd < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // Bind to loopback with auto-assigned port. + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // Auto-assign port + + if (::bind(internal::g_wake_socket_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); + ::close(internal::g_wake_socket_fd); + internal::g_wake_socket_fd = -1; + return; + } + + // Get the assigned address and connect to it. + // Connecting a UDP socket allows using send() instead of sendto() for better performance. + struct sockaddr_in wake_addr; + socklen_t len = sizeof(wake_addr); + if (::getsockname(internal::g_wake_socket_fd, (struct sockaddr *) &wake_addr, &len) < 0) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + ::close(internal::g_wake_socket_fd); + internal::g_wake_socket_fd = -1; + return; + } + + // Connect to self (loopback) — allows using send() instead of sendto(). + // After connect(), no need to store wake_addr — the socket remembers it. + if (::connect(internal::g_wake_socket_fd, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + ::close(internal::g_wake_socket_fd); + internal::g_wake_socket_fd = -1; + return; + } + + // Set non-blocking mode. + int flags = ::fcntl(internal::g_wake_socket_fd, F_GETFL, 0); + ::fcntl(internal::g_wake_socket_fd, F_SETFL, flags | O_NONBLOCK); + + // Register with the select() loop. + if (!wake_register_fd(internal::g_wake_socket_fd)) { + ESP_LOGW(TAG, "Wake socket register failed"); + ::close(internal::g_wake_socket_fd); + internal::g_wake_socket_fd = -1; + return; + } +} +#endif // USE_HOST } // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 15b882b3062..0cfca94a78e 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -21,6 +21,11 @@ #include <pico/time.h> #endif +#ifdef USE_HOST +#include <sys/select.h> +#include <sys/socket.h> +#endif + namespace esphome { // === Wake flag for ESP8266/RP2040 === @@ -170,6 +175,21 @@ void wakeable_delay(uint32_t ms); #ifdef USE_HOST /// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. void wake_loop_threadsafe(); + +/// Register a socket file descriptor with the host select() loop. Not +/// thread-safe — main loop only. Returns false if fd is invalid or +/// >= FD_SETSIZE. +bool wake_register_fd(int fd); + +/// Unregister a socket file descriptor. Not thread-safe — main loop only. +void wake_unregister_fd(int fd); + +/// One-time setup of the loopback wake socket. Called from Application::setup(). +void wake_setup(); + +// wake_fd_ready() and wake_drain_notifications() are defined inline at the +// bottom of this file — they need internal::g_read_fds / g_wake_socket_fd in +// scope, which depend on USE_HOST-only includes pulled in above. #else /// Zephyr is currently the only platform without a wake mechanism. /// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). @@ -180,6 +200,10 @@ inline void wake_loop_threadsafe() {} inline void wake_loop_any_context() { wake_loop_threadsafe(); } namespace internal { +#ifdef USE_HOST +/// Host wakeable_delay uses select() over the registered fds — defined in wake.cpp. +void wakeable_delay(uint32_t ms); +#else inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { if (ms == 0) [[unlikely]] { yield(); @@ -187,8 +211,40 @@ inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { } delay(ms); } +#endif } // namespace internal #endif +#ifdef USE_HOST +namespace internal { +// File-scope state owned by wake.cpp. Accessed inline by wake_drain_notifications() +// and wake_fd_ready() so the hot path stays in the header. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern int g_wake_socket_fd; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern fd_set g_read_fds; +} // namespace internal + +inline bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd) { return FD_ISSET(fd, &internal::g_read_fds); } + +// Small buffer for draining wake notification bytes (1 byte sent per wake). +// Sized to drain multiple notifications per recvfrom() without wasting stack. +inline constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void ESPHOME_ALWAYS_INLINE wake_drain_notifications() { + // Called from main loop to drain any pending wake notifications. + // Must check wake_fd_ready() to avoid blocking on empty socket. + if (internal::g_wake_socket_fd >= 0 && wake_fd_ready(internal::g_wake_socket_fd)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads. Multiple wake events + // may have triggered multiple writes, so drain until EWOULDBLOCK. We control + // both ends of this loopback socket (always 1 byte per wake), so no error + // checking — any error indicates catastrophic system failure. + while (::recvfrom(internal::g_wake_socket_fd, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + } + } +} +#endif // USE_HOST + } // namespace esphome From 4c2efd41651cf2bd78a961eed42a6f9f35829f21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 23 Apr 2026 01:15:25 -0500 Subject: [PATCH 1617/2056] [radio_frequency] Add experimental `radio_frequency` entity type (base component + API) (#15556) --- CODEOWNERS | 1 + esphome/components/api/api.proto | 35 +++- esphome/components/api/api_connection.cpp | 64 +++++- esphome/components/api/api_connection.h | 5 +- esphome/components/api/api_pb2.cpp | 45 ++++- esphome/components/api/api_pb2.h | 26 ++- esphome/components/api/api_pb2_dump.cpp | 24 ++- esphome/components/api/api_pb2_service.cpp | 2 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/api_server.cpp | 2 +- esphome/components/api/api_server.h | 2 +- esphome/components/api/list_entities.cpp | 3 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.h | 3 + .../components/radio_frequency/__init__.py | 77 ++++++++ .../radio_frequency/radio_frequency.cpp | 109 ++++++++++ .../radio_frequency/radio_frequency.h | 187 ++++++++++++++++++ .../components/web_server/list_entities.cpp | 6 + esphome/components/web_server/list_entities.h | 3 + esphome/components/web_server/web_server.cpp | 110 +++++++++++ esphome/components/web_server/web_server.h | 9 + esphome/core/component_iterator.h | 5 + esphome/core/defines.h | 2 + esphome/core/entity_includes.h | 3 + esphome/core/entity_types.h | 4 + tests/components/web_server/common.yaml | 1 + 26 files changed, 710 insertions(+), 23 deletions(-) create mode 100644 esphome/components/radio_frequency/__init__.py create mode 100644 esphome/components/radio_frequency/radio_frequency.cpp create mode 100644 esphome/components/radio_frequency/radio_frequency.h diff --git a/CODEOWNERS b/CODEOWNERS index 5b1ae65f1bf..92efe4da4e5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -403,6 +403,7 @@ esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje esphome/components/qspi_dbi/* @clydebarrow esphome/components/qwiic_pir/* @kahrendt +esphome/components/radio_frequency/* @kbx81 esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f906cfb8d78..c3e4c386334 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2544,27 +2544,50 @@ message ListEntitiesInfraredResponse { message InfraredRFTransmitRawTimingsRequest { option (id) = 136; option (source) = SOURCE_CLIENT; - option (ifdef) = "USE_IR_RF"; + option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY"; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance - uint32 carrier_frequency = 3; // Carrier frequency in Hz - uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) + fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance + uint32 carrier_frequency = 3; // Carrier frequency in Hz + uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) + uint32 modulation = 6; // RadioFrequencyModulation enum value (0 = OOK; ignored for IR entities) } // Event message for received infrared/RF data message InfraredRFReceiveEvent { option (id) = 137; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_IR_RF"; + option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY"; option (no_delay) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance + fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } +// ==================== RADIO FREQUENCY ==================== + +// Lists available radio frequency entity instances +message ListEntitiesRadioFrequencyResponse { + option (id) = 148; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_RADIO_FREQUENCY"; + + string object_id = 1 [(max_data_length) = 120, (force) = true]; + fixed32 key = 2 [(force) = true]; + string name = 3 [(max_data_length) = 120, (force) = true]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; + bool disabled_by_default = 5; + EntityCategory entity_category = 6; + uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; + uint32 capabilities = 8; // Bitmask of RadioFrequencyCapabilityFlags: bit 0 = transmitter, bit 1 = receiver + uint32 frequency_min = 9; // Minimum tunable frequency in Hz; if min == max (non-zero): fixed frequency; 0 = unspecified + uint32 frequency_max = 10; // Maximum tunable frequency in Hz; 0 = unspecified + uint32 supported_modulations = 11; // Bitmask of supported RadioFrequencyModulation values (bit N = modulation N supported) +} + // ==================== SERIAL PROXY ==================== enum SerialProxyParity { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4663456da62..b6f4aa2141c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -49,6 +49,9 @@ #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_RADIO_FREQUENCY +#include "esphome/components/radio_frequency/radio_frequency.h" +#endif namespace esphome::api { @@ -100,6 +103,12 @@ static const int CAMERA_STOP_STREAM = 5000; entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \ if ((entity_var) == nullptr) \ return; + +// Helper macro for multi-entity dispatch: looks up an entity by key and device_id without early return or make_call(). +// Use when multiple entity types must be checked in sequence (at most one will match). +#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id) + #else // No device support, use simpler macros // Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call // object @@ -115,6 +124,12 @@ static const int CAMERA_STOP_STREAM = 5000; entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ if ((entity_var) == nullptr) \ return; + +// Helper macro for multi-entity dispatch: looks up an entity by key without early return or make_call(). +// Use when multiple entity types must be checked in sequence (at most one will match). +#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key) + #endif // USE_DEVICES APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) { @@ -1471,19 +1486,36 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c } #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { - // TODO: When RF is implemented, add a field to the message to distinguish IR vs RF - // and dispatch to the appropriate entity type based on that field. + // Dispatch by key: infrared entities are checked first, then radio frequency entities. + // The key is unique across all entity instances on a device, so at most one lookup will succeed. #ifdef USE_INFRARED - ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared) - call.set_carrier_frequency(msg.carrier_frequency); - call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_); - call.set_repeat_count(msg.repeat_count); - call.perform(); + ENTITY_COMMAND_LOOKUP(infrared::Infrared, infrared, infrared); + if (infrared != nullptr) { + auto call = infrared->make_call(); + call.set_carrier_frequency(msg.carrier_frequency); + call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_); + call.set_repeat_count(msg.repeat_count); + call.perform(); + return; + } +#endif +#ifdef USE_RADIO_FREQUENCY + ENTITY_COMMAND_LOOKUP(radio_frequency::RadioFrequency, radio_frequency, radio_frequency); + if (radio_frequency != nullptr) { + auto call = radio_frequency->make_call(); + call.set_frequency(msg.carrier_frequency); + call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation)); + call.set_repeat_count(msg.repeat_count); + call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_); + call.perform(); + } #endif } +#endif +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } #endif @@ -1580,6 +1612,19 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection } #endif +#ifdef USE_RADIO_FREQUENCY +uint16_t APIConnection::try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn, + uint32_t remaining_size) { + auto *rf = static_cast<radio_frequency::RadioFrequency *>(entity); + ListEntitiesRadioFrequencyResponse msg; + msg.capabilities = rf->get_capability_flags(); + msg.frequency_min = rf->get_traits().get_frequency_min_hz(); + msg.frequency_max = rf->get_traits().get_frequency_max_hz(); + msg.supported_modulations = rf->get_traits().get_supported_modulations(); + return fill_and_encode_entity_info(rf, msg, conn, remaining_size); +} +#endif + #ifdef USE_UPDATE bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE); @@ -2341,6 +2386,9 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, #ifdef USE_INFRARED CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse) #endif +#ifdef USE_RADIO_FREQUENCY + CASE_INFO_ONLY(radio_frequency, ListEntitiesRadioFrequencyResponse) +#endif #ifdef USE_EVENT CASE_INFO_ONLY(event, ListEntitiesEventResponse) #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 7d087970903..4165b7f3a2d 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -223,7 +223,7 @@ class APIConnection final : public APIServerConnectionBase { void on_water_heater_command_request(const WaterHeaterCommandRequest &msg); #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif @@ -612,6 +612,9 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_INFRARED static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif +#ifdef USE_RADIO_FREQUENCY + static uint16_t try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); +#endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, uint32_t remaining_size); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f304c852822..3d124539395 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3861,7 +3861,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const { return size; } #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES @@ -3875,6 +3875,9 @@ bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto case 4: this->repeat_count = value; break; + case 6: + this->modulation = value; + break; default: return false; } @@ -3928,6 +3931,46 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { return size; } #endif +#ifdef USE_RADIO_FREQUENCY +uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); +#ifdef USE_ENTITY_ICON + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); +#endif + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category)); +#ifdef USE_DEVICES + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id); +#endif + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->frequency_min); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->frequency_max); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supported_modulations); + return pos; +} +uint32_t ListEntitiesRadioFrequencyResponse::calculate_size() const { + uint32_t size = 0; + size += 2 + this->object_id.size(); + size += 5; + size += 2 + this->name.size(); +#ifdef USE_ENTITY_ICON + size += !this->icon.empty() ? 2 + this->icon.size() : 0; +#endif + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += this->entity_category ? 2 : 0; +#ifdef USE_DEVICES + size += ProtoSize::calc_uint32(1, this->device_id); +#endif + size += ProtoSize::calc_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->frequency_min); + size += ProtoSize::calc_uint32(1, this->frequency_max); + size += ProtoSize::calc_uint32(1, this->supported_modulations); + return size; +} +#endif #ifdef USE_SERIAL_PROXY bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5827a8728e6..5aa592e4fa8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3054,11 +3054,11 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { protected: }; #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 136; - static constexpr uint8_t ESTIMATED_SIZE = 220; + static constexpr uint8_t ESTIMATED_SIZE = 224; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); } #endif @@ -3071,6 +3071,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { const uint8_t *timings_data_{nullptr}; uint16_t timings_length_{0}; uint16_t timings_count_{0}; + uint32_t modulation{0}; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3101,6 +3102,27 @@ class InfraredRFReceiveEvent final : public ProtoMessage { protected: }; #endif +#ifdef USE_RADIO_FREQUENCY +class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 148; + static constexpr uint8_t ESTIMATED_SIZE = 56; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("list_entities_radio_frequency_response"); } +#endif + uint32_t capabilities{0}; + uint32_t frequency_min{0}; + uint32_t frequency_max{0}; + uint32_t supported_modulations{0}; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif #ifdef USE_SERIAL_PROXY class SerialProxyConfigureRequest final : public ProtoDecodableMessage { public: diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 640c3473719..bdcb6d4146d 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2576,7 +2576,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest")); #ifdef USE_DEVICES @@ -2591,6 +2591,7 @@ const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const out.append_p(ESPHOME_PSTR(" values, ")); append_uint(out, this->timings_length_); out.append_p(ESPHOME_PSTR(" bytes]\n")); + dump_field(out, ESPHOME_PSTR("modulation"), this->modulation); return out.c_str(); } const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { @@ -2605,6 +2606,27 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_RADIO_FREQUENCY +const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesRadioFrequencyResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); +#ifdef USE_ENTITY_ICON + dump_field(out, ESPHOME_PSTR("icon"), this->icon); +#endif + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); +#endif + dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); + dump_field(out, ESPHOME_PSTR("frequency_min"), this->frequency_min); + dump_field(out, ESPHOME_PSTR("frequency_max"), this->frequency_max); + dump_field(out, ESPHOME_PSTR("supported_modulations"), this->supported_modulations); + return out.c_str(); +} +#endif #ifdef USE_SERIAL_PROXY const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest")); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index b41233eddd1..6ae2a3e3694 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -625,7 +625,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui break; } #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: { InfraredRFTransmitRawTimingsRequest msg; msg.decode(msg_data, msg_size); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 6ff988902f3..aca42ca303d 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -211,7 +211,7 @@ class APIServerConnectionBase { void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4559168ece2..c30bd2e6124 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -368,7 +368,7 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { } #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings) { InfraredRFReceiveEvent resp{}; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index d6ac1a6d5d8..e662d78eba9 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -183,7 +183,7 @@ class APIServer final : public Component, #ifdef USE_ZWAVE_PROXY void on_zwave_proxy_request(const ZWaveProxyRequest &msg); #endif -#ifdef USE_IR_RF +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 0a94c1699b1..f9e645b5067 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -79,6 +79,9 @@ LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWater #ifdef USE_INFRARED LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse) #endif +#ifdef USE_RADIO_FREQUENCY +LIST_ENTITIES_HANDLER(radio_frequency, radio_frequency::RadioFrequency, ListEntitiesRadioFrequencyResponse) +#endif #ifdef USE_EVENT LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 7d0eb5bb136..95c626feb17 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -87,6 +87,9 @@ class ListEntitiesIterator final : public ComponentIterator { #ifdef USE_INFRARED bool on_infrared(infrared::Infrared *entity) override; #endif +#ifdef USE_RADIO_FREQUENCY + bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *entity) override; #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 9edf0f0f0cf..f20611e06ac 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -82,6 +82,9 @@ class InitialStateIterator final : public ComponentIterator { #ifdef USE_INFRARED bool on_infrared(infrared::Infrared *infrared) override { return true; }; #endif +#ifdef USE_RADIO_FREQUENCY + bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; }; +#endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif diff --git a/esphome/components/radio_frequency/__init__.py b/esphome/components/radio_frequency/__init__.py new file mode 100644 index 00000000000..b00590ceb58 --- /dev/null +++ b/esphome/components/radio_frequency/__init__.py @@ -0,0 +1,77 @@ +""" +Radio Frequency component for ESPHome. + +WARNING: This component is EXPERIMENTAL. The API (both Python configuration +and C++ interfaces) may change at any time without following the normal +breaking changes policy. Use at your own risk. + +Once the API is considered stable, this warning will be removed. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import setup_entity +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +AUTO_LOAD = ["remote_base"] + +IS_PLATFORM_COMPONENT = True + +radio_frequency_ns = cg.esphome_ns.namespace("radio_frequency") +RadioFrequency = radio_frequency_ns.class_( + "RadioFrequency", cg.EntityBase, cg.Component +) +RadioFrequencyCall = radio_frequency_ns.class_("RadioFrequencyCall") +RadioFrequencyTraits = radio_frequency_ns.class_("RadioFrequencyTraits") +RadioFrequencyModulation = radio_frequency_ns.enum("RadioFrequencyModulation") + +CONF_RADIO_FREQUENCY_ID = "radio_frequency_id" + + +def radio_frequency_schema(class_: type[cg.MockObjClass]) -> cv.Schema: + """Create a schema for a radio frequency platform. + + :param class_: The radio frequency class to use for this schema. + :return: An extended schema for radio frequency configuration. + """ + entity_schema = cv.ENTITY_BASE_SCHEMA.extend(cv.COMPONENT_SCHEMA) + return entity_schema.extend( + { + cv.GenerateID(): cv.declare_id(class_), + } + ) + + +@setup_entity("radio_frequency") +async def setup_radio_frequency_core_(var: cg.Pvariable, config: ConfigType) -> None: + """Set up core radio frequency configuration.""" + + +async def register_radio_frequency(var: cg.Pvariable, config: ConfigType) -> None: + """Register a radio frequency device with the core.""" + cg.add_define("USE_RADIO_FREQUENCY") + await cg.register_component(var, config) + await setup_radio_frequency_core_(var, config) + cg.add(cg.App.register_radio_frequency(var)) + CORE.register_platform_component("radio_frequency", var) + + +async def new_radio_frequency(config: ConfigType, *args) -> cg.Pvariable: + """Create a new RadioFrequency instance. + + :param config: Configuration dictionary. + :param args: Additional arguments to pass to new_Pvariable. + :return: The created RadioFrequency instance. + """ + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_radio_frequency(var, config) + return var + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config: ConfigType) -> None: + cg.add_global(radio_frequency_ns.using) diff --git a/esphome/components/radio_frequency/radio_frequency.cpp b/esphome/components/radio_frequency/radio_frequency.cpp new file mode 100644 index 00000000000..3c000ae1cad --- /dev/null +++ b/esphome/components/radio_frequency/radio_frequency.cpp @@ -0,0 +1,109 @@ +#include "radio_frequency.h" + +#include <cinttypes> + +#include "esphome/core/log.h" + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome::radio_frequency { + +static const char *const TAG = "radio_frequency"; + +// ========== RadioFrequencyCall ========== + +RadioFrequencyCall &RadioFrequencyCall::set_frequency(uint32_t frequency_hz) { + this->frequency_hz_ = frequency_hz; + return *this; +} + +RadioFrequencyCall &RadioFrequencyCall::set_modulation(RadioFrequencyModulation modulation) { + this->modulation_ = modulation; + return *this; +} + +RadioFrequencyCall &RadioFrequencyCall::set_raw_timings(const std::vector<int32_t> &timings) { + this->raw_timings_ = &timings; + this->packed_data_ = nullptr; + this->base64url_ptr_ = nullptr; + return *this; +} + +RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_base64url(const std::string &base64url) { + this->base64url_ptr_ = &base64url; + this->raw_timings_ = nullptr; + this->packed_data_ = nullptr; + return *this; +} + +RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) { + this->packed_data_ = data; + this->packed_length_ = length; + this->packed_count_ = count; + this->raw_timings_ = nullptr; + this->base64url_ptr_ = nullptr; + return *this; +} + +RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) { + this->repeat_count_ = count; + return *this; +} + +void RadioFrequencyCall::perform() { + if (this->parent_ != nullptr) { + this->parent_->control(*this); + } +} + +// ========== RadioFrequency ========== + +void RadioFrequency::dump_config() { + ESP_LOGCONFIG(TAG, + "Radio Frequency '%s'\n" + " Supports Transmitter: %s\n" + " Supports Receiver: %s", + this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), + YESNO(this->traits_.get_supports_receiver())); + if (this->traits_.get_frequency_min_hz() > 0) { + if (this->traits_.get_frequency_min_hz() == this->traits_.get_frequency_max_hz()) { + ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz (fixed)", this->traits_.get_frequency_min_hz()); + } else { + ESP_LOGCONFIG(TAG, " Frequency Range: %" PRIu32 " - %" PRIu32 " Hz", this->traits_.get_frequency_min_hz(), + this->traits_.get_frequency_max_hz()); + } + } +} + +RadioFrequencyCall RadioFrequency::make_call() { return RadioFrequencyCall(this); } + +uint32_t RadioFrequency::get_capability_flags() const { + uint32_t flags = 0; + if (this->traits_.get_supports_transmitter()) + flags |= RadioFrequencyCapability::CAPABILITY_TRANSMITTER; + if (this->traits_.get_supports_receiver()) + flags |= RadioFrequencyCapability::CAPABILITY_RECEIVER; + return flags; +} + +bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) { + // Invoke local callbacks + this->receive_callback_.call(data); + + // Forward received RF data to API server +#if defined(USE_API) && defined(USE_RADIO_FREQUENCY) + if (api::global_api_server != nullptr) { +#ifdef USE_DEVICES + uint32_t device_id = this->get_device_id(); +#else + uint32_t device_id = 0; +#endif + api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data()); + } +#endif + return false; // Don't consume the event, allow other listeners to process it +} + +} // namespace esphome::radio_frequency diff --git a/esphome/components/radio_frequency/radio_frequency.h b/esphome/components/radio_frequency/radio_frequency.h new file mode 100644 index 00000000000..db73a844ede --- /dev/null +++ b/esphome/components/radio_frequency/radio_frequency.h @@ -0,0 +1,187 @@ +#pragma once + +// WARNING: This component is EXPERIMENTAL. The API may change at any time +// without following the normal breaking changes policy. Use at your own risk. +// Once the API is considered stable, this warning will be removed. + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" +#include "esphome/components/remote_base/remote_base.h" + +#include <vector> + +namespace esphome::radio_frequency { + +/// Capability flags for individual radio frequency instances +enum RadioFrequencyCapability : uint32_t { + CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals + CAPABILITY_RECEIVER = 1 << 1, // Can receive signals +}; + +/// Modulation types supported by radio frequency implementations +enum RadioFrequencyModulation : uint8_t { + RADIO_FREQUENCY_MODULATION_OOK = 0, // On-Off Keying / Amplitude Shift Keying + // Future: RADIO_FREQUENCY_MODULATION_FSK, RADIO_FREQUENCY_MODULATION_GFSK, etc. +}; + +/// Forward declarations +class RadioFrequency; + +/// RadioFrequencyCall - Builder pattern for transmitting radio frequency signals +class RadioFrequencyCall { + public: + explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {} + + /// Set the carrier frequency in Hz (e.g. 433920000 for 433.92 MHz) + RadioFrequencyCall &set_frequency(uint32_t frequency_hz); + + /// Set the modulation type (defaults to OOK) + RadioFrequencyCall &set_modulation(RadioFrequencyModulation modulation); + + // ===== Raw Timings Methods ===== + // All set_raw_timings_* methods store pointers/references to external data. + // The referenced data must remain valid until perform() completes. + // Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous + // Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone! + + /// Set the raw timings from a vector (positive = mark, negative = space) + /// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform(). + /// @note Usage: Primarily for lambdas/automations where the vector is in scope. + RadioFrequencyCall &set_raw_timings(const std::vector<int32_t> &timings); + + /// Set the raw timings from base64url-encoded little-endian int32 data + /// @note Lifetime: Stores a pointer to the string. The string must outlive perform(). + /// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_'). + /// @note Decoding happens at perform() time, directly into the transmit buffer. + RadioFrequencyCall &set_raw_timings_base64url(const std::string &base64url); + + /// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded) + /// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform(). + /// @note Usage: For API component where data comes directly from the protobuf message. + RadioFrequencyCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); + + /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) + RadioFrequencyCall &set_repeat_count(uint32_t count); + + /// Perform the transmission + void perform(); + + /// Get the frequency in Hz + const optional<uint32_t> &get_frequency() const { return this->frequency_hz_; } + /// Get the modulation type + RadioFrequencyModulation get_modulation() const { return this->modulation_; } + /// Get the raw timings (only valid if set via set_raw_timings) + const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; } + /// Check if raw timings have been set (any format) + bool has_raw_timings() const { + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr; + } + /// Check if using packed data format + bool is_packed() const { return this->packed_data_ != nullptr; } + /// Check if using base64url data format + bool is_base64url() const { return this->base64url_ptr_ != nullptr; } + /// Get the base64url data string + const std::string &get_base64url_data() const { return *this->base64url_ptr_; } + /// Get packed data (only valid if set via set_raw_timings_packed) + const uint8_t *get_packed_data() const { return this->packed_data_; } + uint16_t get_packed_length() const { return this->packed_length_; } + uint16_t get_packed_count() const { return this->packed_count_; } + /// Get the repeat count + uint32_t get_repeat_count() const { return this->repeat_count_; } + + protected: + optional<uint32_t> frequency_hz_{}; + uint32_t repeat_count_{1}; + RadioFrequency *parent_; + // Pointer to vector-based timings (caller-owned, must outlive perform()) + const std::vector<int32_t> *raw_timings_{nullptr}; + // Pointer to base64url-encoded string (caller-owned, must outlive perform()) + const std::string *base64url_ptr_{nullptr}; + // Pointer to packed protobuf buffer (caller-owned, must outlive perform()) + const uint8_t *packed_data_{nullptr}; + uint16_t packed_length_{0}; + uint16_t packed_count_{0}; + RadioFrequencyModulation modulation_{RADIO_FREQUENCY_MODULATION_OOK}; +}; + +/// RadioFrequencyTraits - Describes the capabilities of a radio frequency implementation +class RadioFrequencyTraits { + public: + bool get_supports_transmitter() const { return this->supports_transmitter_; } + void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; } + + bool get_supports_receiver() const { return this->supports_receiver_; } + void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; } + + /// Hardware-supported tunable frequency range in Hz. + /// If min == max (and both non-zero): fixed-frequency hardware. + /// If both 0: range unspecified. + uint32_t get_frequency_min_hz() const { return this->frequency_min_hz_; } + void set_frequency_min_hz(uint32_t freq) { this->frequency_min_hz_ = freq; } + + uint32_t get_frequency_max_hz() const { return this->frequency_max_hz_; } + void set_frequency_max_hz(uint32_t freq) { this->frequency_max_hz_ = freq; } + + /// Convenience setter for fixed-frequency hardware (sets min == max). + void set_fixed_frequency_hz(uint32_t freq) { + this->frequency_min_hz_ = freq; + this->frequency_max_hz_ = freq; + } + + /// Bitmask of supported RadioFrequencyModulation values (bit N = modulation value N supported). + uint32_t get_supported_modulations() const { return this->supported_modulations_; } + void set_supported_modulations(uint32_t mask) { this->supported_modulations_ = mask; } + void add_supported_modulation(RadioFrequencyModulation mod) { + this->supported_modulations_ |= (1u << static_cast<uint8_t>(mod)); + } + + protected: + uint32_t frequency_min_hz_{0}; // Minimum tunable frequency in Hz (0 = unspecified) + uint32_t frequency_max_hz_{0}; // Maximum tunable frequency in Hz (0 = unspecified) + uint32_t supported_modulations_{0}; // Bitmask of supported RadioFrequencyModulation values + bool supports_transmitter_{false}; + bool supports_receiver_{false}; +}; + +/// RadioFrequency - Base class for radio frequency implementations +class RadioFrequency : public Component, public EntityBase, public remote_base::RemoteReceiverListener { + public: + RadioFrequency() = default; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + /// Get the traits for this radio frequency implementation + RadioFrequencyTraits &get_traits() { return this->traits_; } + const RadioFrequencyTraits &get_traits() const { return this->traits_; } + + /// Create a call object for transmitting + RadioFrequencyCall make_call(); + + /// Get capability flags for this radio frequency instance + uint32_t get_capability_flags() const; + + /// Called when RF data is received (from RemoteReceiverListener) + bool on_receive(remote_base::RemoteReceiveData data) override; + + /// Add a callback to invoke when RF data is received + template<typename F> void add_on_receive_callback(F &&callback) { + this->receive_callback_.add(std::forward<F>(callback)); + } + + protected: + friend class RadioFrequencyCall; + + /// Perform the actual transmission (called by RadioFrequencyCall::perform()) + /// Platforms must override this to implement hardware-specific transmission. + virtual void control(const RadioFrequencyCall &call) = 0; + + // Traits describing capabilities + RadioFrequencyTraits traits_; + + // Callback manager for receive events (lazy: saves memory when no callbacks registered) + LazyCallbackManager<void(remote_base::RemoteReceiveData)> receive_callback_; +}; + +} // namespace esphome::radio_frequency diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index ebe7bf44504..c1e7599c7e4 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -145,6 +145,12 @@ bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) { return true; } #endif +#ifdef USE_RADIO_FREQUENCY +bool ListEntitiesIterator::on_radio_frequency(radio_frequency::RadioFrequency *obj) { + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::radio_frequency_all_json_generator); + return true; +} +#endif #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *obj) { diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 8c22d757b67..9cfc6c7e330 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -87,6 +87,9 @@ class ListEntitiesIterator final : public ComponentIterator { #ifdef USE_INFRARED bool on_infrared(infrared::Infrared *obj) override; #endif +#ifdef USE_RADIO_FREQUENCY + bool on_radio_frequency(radio_frequency::RadioFrequency *obj) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *obj) override; #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1daec1786d9..198267204d9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -40,6 +40,9 @@ #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_RADIO_FREQUENCY +#include "esphome/components/radio_frequency/radio_frequency.h" +#endif #ifdef USE_WEBSERVER_LOCAL #if USE_WEBSERVER_VERSION == 2 @@ -2102,6 +2105,104 @@ json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, J } #endif +#ifdef USE_RADIO_FREQUENCY +void WebServer::handle_radio_frequency_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (radio_frequency::RadioFrequency *obj : App.get_radio_frequencies()) { + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) + continue; + + if (request->method() == HTTP_GET && entity_match.action_is_empty) { + auto detail = get_request_detail(request); + auto data = this->radio_frequency_json_(obj, detail); + request->send(200, ESPHOME_F("application/json"), data.c_str()); + return; + } + if (!match.method_equals(ESPHOME_F("transmit"))) { + request->send(404); + return; + } + + // Only allow transmit if the device supports it + if (!(obj->get_capability_flags() & radio_frequency::CAPABILITY_TRANSMITTER)) { + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission")); + return; + } + + auto call = obj->make_call(); + + // Parse carrier frequency (optional — overrides IC default) + { + auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("frequency")).c_str()); + if (value.has_value()) { + call.set_frequency(*value); + } + } + + // Parse repeat count (optional, defaults to 1) + { + auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str()); + if (value.has_value()) { + call.set_repeat_count(*value); + } + } + + // Parse base64url-encoded raw timings (required) + // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) + const auto &data_arg = request->arg(ESPHOME_F("data")); + + // Validate base64url is not empty (also catches missing parameter since arg() returns empty string) + // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility + if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty) + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter")); + return; + } + + // Defer to main loop for thread safety. Move encoded string into lambda to ensure + // it outlives the call - set_raw_timings_base64url stores a pointer, so the string + // must remain valid until perform() completes. + // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. + this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable { + call.set_raw_timings_base64url(encoded); + call.perform(); + }); + + request->send(200); + return; + } + request->send(404); +} + +json::SerializationBuffer<> WebServer::radio_frequency_all_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + return web_server->radio_frequency_json_(static_cast<radio_frequency::RadioFrequency *>(source), DETAIL_ALL); +} + +json::SerializationBuffer<> WebServer::radio_frequency_json_(radio_frequency::RadioFrequency *obj, + JsonDetail start_config) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + + set_json_icon_state_value(root, obj, "radio_frequency", "", 0, start_config); + + const auto &traits = obj->get_traits(); + auto caps = obj->get_capability_flags(); + + root[ESPHOME_F("supports_transmitter")] = bool(caps & radio_frequency::CAPABILITY_TRANSMITTER); + root[ESPHOME_F("supports_receiver")] = bool(caps & radio_frequency::CAPABILITY_RECEIVER); + if (traits.get_frequency_min_hz() != 0) { + root[ESPHOME_F("frequency_min")] = traits.get_frequency_min_hz(); + root[ESPHOME_F("frequency_max")] = traits.get_frequency_max_hz(); + } + + if (start_config == DETAIL_ALL) { + this->add_sorting_info_(root, obj); + } + + return builder.serialize(); +} +#endif + #ifdef USE_EVENT void WebServer::on_event(event::Event *obj) { if (!this->include_internal_ && obj->is_internal()) @@ -2357,6 +2458,10 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { #ifdef USE_INFRARED if (match.domain_equals(ESPHOME_F("infrared"))) return true; +#endif +#ifdef USE_RADIO_FREQUENCY + if (match.domain_equals(ESPHOME_F("radio_frequency"))) + return true; #endif } @@ -2516,6 +2621,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else if (match.domain_equals(ESPHOME_F("infrared"))) { this->handle_infrared_request(request, match); } +#endif +#ifdef USE_RADIO_FREQUENCY + else if (match.domain_equals(ESPHOME_F("radio_frequency"))) { + this->handle_radio_frequency_request(request, match); + } #endif else { // No matching handler found - send 404 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 8e8b1de8c4b..25f8f8212dd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -462,6 +462,12 @@ class WebServer final : public Controller, public Component, public AsyncWebHand static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source); #endif +#ifdef USE_RADIO_FREQUENCY + /// Handle a radio frequency request under '/radio_frequency/<id>/transmit'. + void handle_radio_frequency_request(AsyncWebServerRequest *request, const UrlMatch &match); + + static json::SerializationBuffer<> radio_frequency_all_json_generator(WebServer *web_server, void *source); +#endif #ifdef USE_EVENT void on_event(event::Event *obj) override; @@ -654,6 +660,9 @@ class WebServer final : public Controller, public Component, public AsyncWebHand #ifdef USE_INFRARED json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config); #endif +#ifdef USE_RADIO_FREQUENCY + json::SerializationBuffer<> radio_frequency_json_(radio_frequency::RadioFrequency *obj, JsonDetail start_config); +#endif #ifdef USE_UPDATE json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 9a1e5da3518..d271fcfed03 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -21,6 +21,11 @@ namespace infrared { class Infrared; } // namespace infrared #endif +#ifdef USE_RADIO_FREQUENCY +namespace radio_frequency { +class RadioFrequency; +} // namespace radio_frequency +#endif class ComponentIterator { public: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0978437039a..63fe4e677e1 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -65,6 +65,7 @@ #define USE_INFRARED #define USE_IR_RF #define USE_JSON +#define USE_RADIO_FREQUENCY #define USE_LIGHT #define USE_LIGHT_GAMMA_LUT #define USE_LOCK @@ -448,6 +449,7 @@ #define ESPHOME_ENTITY_LOCK_COUNT 1 #define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 #define ESPHOME_ENTITY_NUMBER_COUNT 1 +#define ESPHOME_ENTITY_RADIO_FREQUENCY_COUNT 1 #define ESPHOME_ENTITY_SELECT_COUNT 1 #define ESPHOME_ENTITY_SENSOR_COUNT 1 #define ESPHOME_ENTITY_SWITCH_COUNT 1 diff --git a/esphome/core/entity_includes.h b/esphome/core/entity_includes.h index f67887b30be..b1310e11426 100644 --- a/esphome/core/entity_includes.h +++ b/esphome/core/entity_includes.h @@ -68,6 +68,9 @@ #ifdef USE_INFRARED #include "esphome/components/infrared/infrared.h" #endif +#ifdef USE_RADIO_FREQUENCY +#include "esphome/components/radio_frequency/radio_frequency.h" +#endif #ifdef USE_SERIAL_PROXY #include "esphome/components/serial_proxy/serial_proxy.h" #endif diff --git a/esphome/core/entity_types.h b/esphome/core/entity_types.h index 04b490e10ea..f830911c071 100644 --- a/esphome/core/entity_types.h +++ b/esphome/core/entity_types.h @@ -90,6 +90,10 @@ ENTITY_CONTROLLER_TYPE_(water_heater::WaterHeater, water_heater, water_heaters, #ifdef USE_INFRARED ENTITY_TYPE_(infrared::Infrared, infrared, infrareds, ESPHOME_ENTITY_INFRARED_COUNT, INFRARED) #endif +#ifdef USE_RADIO_FREQUENCY +ENTITY_TYPE_(radio_frequency::RadioFrequency, radio_frequency, radio_frequencies, ESPHOME_ENTITY_RADIO_FREQUENCY_COUNT, + RADIO_FREQUENCY) +#endif #ifdef USE_EVENT ENTITY_CONTROLLER_TYPE_(event::Event, event, events, ESPHOME_ENTITY_EVENT_COUNT, EVENT, event) #endif diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index 35a605484c1..5a05a58c2d9 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -38,3 +38,4 @@ event: update: water_heater: infrared: +radio_frequency: From 9685d4eb0b7e6f28ebff3d47d77f0ede58c1ffbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:15:44 +0200 Subject: [PATCH 1618/2056] [core] feed_wdt wraps feed_wdt_with_time (#15932) --- esphome/core/application.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3105ff2e8b4..11381030a31 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -208,16 +208,8 @@ void Application::process_dump_config_() { void Application::feed_wdt() { // Cold entry: callers without a millis() timestamp in hand. Fetches the - // time and takes the same rate-limit paths as feed_wdt_with_time(). - uint32_t now = MillisInternal::get(); - if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) { - this->feed_wdt_slow_(now); - } -#ifdef USE_STATUS_LED - if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) { - this->service_status_led_slow_(now); - } -#endif + // time and defers to the hot path. + this->feed_wdt_with_time(MillisInternal::get()); } void HOT Application::feed_wdt_slow_(uint32_t time) { From 64290d32a1dd8289b04174905ec8e3c81f75fb94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:32:12 -0500 Subject: [PATCH 1619/2056] Bump aioesphomeapi from 44.20.0 to 44.21.0 (#15941) 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 e7ab9bc2ad3..90b06938408 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260408.1 -aioesphomeapi==44.20.0 +aioesphomeapi==44.21.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 43a371caab80f7a3ce64edd3be951acc84270d18 Mon Sep 17 00:00:00 2001 From: PolarGoose <35307286+PolarGoose@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:08:49 +0200 Subject: [PATCH 1620/2056] [dsmr] Small refactoring: Move `Aes128GcmDecryptorImpl` type inside `esphome::dsmr` namespace. (#15940) --- esphome/components/dsmr/dsmr.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index c76a23fde41..626a389c1f7 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -18,22 +18,27 @@ #if __has_include(<psa/crypto.h>) #include <dsmr_parser/decryption/aes128gcm_tfpsa.h> -using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa; #elif __has_include(<mbedtls/gcm.h>) #if __has_include(<mbedtls/esp_config.h>) #include <mbedtls/esp_config.h> #endif #include <dsmr_parser/decryption/aes128gcm_mbedtls.h> -using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls; #elif __has_include(<bearssl/bearssl.h>) #include <dsmr_parser/decryption/aes128gcm_bearssl.h> -using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl; #else #error "The platform doesn't provide a compatible encryption library for dsmr_parser" #endif namespace esphome::dsmr { +#if __has_include(<psa/crypto.h>) +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa; +#elif __has_include(<mbedtls/gcm.h>) +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls; +#else +using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl; +#endif + using namespace dsmr_parser::fields; #ifndef DSMR_SENSOR_LIST From 50c181671cc886457fd8c62dc376d97a087874aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 06:47:16 -0500 Subject: [PATCH 1621/2056] [ci] Better explain too-big bot review message (#15939) --- .github/scripts/auto-label-pr/reviews.js | 30 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/scripts/auto-label-pr/reviews.js b/.github/scripts/auto-label-pr/reviews.js index 7ac136515de..e9e848da6f2 100644 --- a/.github/scripts/auto-label-pr/reviews.js +++ b/.github/scripts/auto-label-pr/reviews.js @@ -41,16 +41,36 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; + message += + `Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` + + `this PR is on the large side `; + if (tooManyLabels && tooManyChanges) { - message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; + message += + `(${nonTestChanges} line changes excluding tests, across ` + + `${originalLabelCount} different components/areas)`; } else if (tooManyLabels) { - message += `This PR affects ${originalLabelCount} different components/areas.`; + message += + `(it touches ${originalLabelCount} different components/areas)`; } else { - message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; + message += `(${nonTestChanges} line changes excluding tests)`; } - message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`; - message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`; + message += `, which makes it harder for maintainers to review.\n\n`; + message += + `Smaller, focused PRs tend to be reviewed much faster since they ` + + `fit into the short gaps between other maintainer work; large ones ` + + `often have to wait for a rare long uninterrupted block of time. ` + + `If you can break this up into smaller pieces that can be reviewed ` + + `independently, it will almost certainly land faster overall.\n\n`; + message += + `Before putting more time in, it's also worth popping into ` + + `\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` + + `you scope things and flag anything already in flight.\n\n`; + message += + `For more details (including how to split the work up), see: ` + + `https://developers.esphome.io/contributing/submitting-your-work/` + + `#how-to-approach-large-submissions`; messages.push(message); } From 13fe881f70a142d1f2888c6b1141590a607445ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:20:31 -0500 Subject: [PATCH 1622/2056] [scheduler][core] Lock-free fast-path on ESPHOME_THREAD_MULTI_NO_ATOMICS via __atomic builtins (#15947) --- esphome/core/scheduler.cpp | 20 ++++---- esphome/core/scheduler.h | 100 +++++++++++++++++++++---------------- esphome/core/time_64.cpp | 23 ++++++--- 3 files changed, 82 insertions(+), 61 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b0eaa670ac2..a6f1558e4a4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -235,11 +235,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } target->push_back(item); if (target == &this->to_add_) { - this->to_add_count_increment_(); + this->to_add_count_increment_locked_(); } #ifndef ESPHOME_THREAD_SINGLE else { - this->defer_count_increment_(); + this->defer_count_increment_locked_(); } #endif } @@ -452,7 +452,7 @@ void Scheduler::full_cleanup_removed_items_() { this->items_.erase(this->items_.begin() + write, this->items_.end()); // Rebuild the heap structure since items are no longer in heap order std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - this->to_remove_clear_(); + this->to_remove_clear_locked_(); } #ifndef ESPHOME_THREAD_SINGLE @@ -501,7 +501,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) { this->lock_.lock(); // Reset counter and snapshot queue end under lock - this->defer_count_clear_(); + this->defer_count_clear_locked_(); size_t defer_queue_end = this->defer_queue_.size(); if (this->defer_queue_front_ >= defer_queue_end) { this->lock_.unlock(); @@ -621,7 +621,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; if (is_item_removed_locked_(item)) { this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_decrement_(); + this->to_remove_decrement_locked_(); continue; } } @@ -630,7 +630,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { if (is_item_removed_(item)) { LockGuard guard{this->lock_}; this->recycle_item_main_loop_(this->pop_raw_locked_()); - this->to_remove_decrement_(); + this->to_remove_decrement_locked_(); continue; } #endif @@ -658,7 +658,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { if (this->is_item_removed_locked_(executed_item)) { // We were removed/cancelled in the function call, recycle and continue - this->to_remove_decrement_(); + this->to_remove_decrement_locked_(); this->recycle_item_main_loop_(executed_item); continue; } @@ -721,7 +721,7 @@ void HOT Scheduler::process_to_add_slow_path_() { std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } this->to_add_.clear(); - this->to_add_count_clear_(); + this->to_add_count_clear_locked_(); } bool HOT Scheduler::cleanup_slow_path_() { // We must hold the lock for the entire cleanup operation because: @@ -737,7 +737,7 @@ bool HOT Scheduler::cleanup_slow_path_() { SchedulerItem *item = this->items_[0]; if (!this->is_item_removed_locked_(item)) break; - this->to_remove_decrement_(); + this->to_remove_decrement_locked_(); this->recycle_item_main_loop_(this->pop_raw_locked_()); } return !this->items_.empty(); @@ -825,7 +825,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry, find_first); total_cancelled += heap_cancelled; - this->to_remove_add_(heap_cancelled); + this->to_remove_add_locked_(heap_cancelled); if (find_first && total_cancelled > 0) return true; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index b7e99d46030..46b19855c35 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -524,11 +524,13 @@ class Scheduler { std::vector<SchedulerItem *> to_add_; #ifndef ESPHOME_THREAD_SINGLE - // Fast-path counter for process_to_add() to skip taking the lock when there is - // nothing to add. Uses std::atomic on platforms that support it, plain uint32_t - // otherwise. On non-atomic platforms, callers must hold the scheduler lock when - // mutating this counter. Not needed on single-threaded platforms where we can - // check to_add_.empty() directly. + // Fast-path counter for process_to_add() to skip taking the lock when there + // is nothing to add. std::atomic on ATOMICS; plain uint32_t on NO_ATOMICS + // (BK72xx — ARMv5TE single-core, lacks LDREX/STREX so std::atomic RMW would + // require libatomic). Reads use __atomic_load_n(__ATOMIC_RELAXED) on + // NO_ATOMICS — compiles to a plain LDR (aligned 32-bit load is naturally + // atomic on ARMv5TE) but expresses the concurrent-access intent in the C++ + // memory model. Writes live behind *_locked_ helpers and must hold lock_. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic<uint32_t> to_add_count_{0}; #else @@ -536,40 +538,41 @@ class Scheduler { #endif #endif /* ESPHOME_THREAD_SINGLE */ - // Fast-path helper for process_to_add() to decide if it can try the lock-free path. - // - On ESPHOME_THREAD_SINGLE: direct container check is safe (no concurrent writers). - // - On ESPHOME_THREAD_MULTI_ATOMICS: performs a lock-free check via to_add_count_. - // - On ESPHOME_THREAD_MULTI_NO_ATOMICS: always returns false to force the caller - // down the locked path; this is NOT a lock-free emptiness check on that platform. + // Fast-path helper for process_to_add() to decide if it can skip the lock. bool to_add_empty_() const { #ifdef ESPHOME_THREAD_SINGLE return this->to_add_.empty(); #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_add_count_.load(std::memory_order_relaxed) == 0; #else - return false; + return __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) == 0; #endif } - // Increment to_add_count_ (no-op on single-threaded platforms) - void to_add_count_increment_() { -#ifdef ESPHOME_THREAD_SINGLE + // Increment to_add_count_ (no-op on single-threaded platforms). + // On NO_ATOMICS the caller must hold lock_; both load and store go through + // __atomic_*_n with __ATOMIC_RELAXED to keep every access to the counter + // explicitly atomic in the C++ memory model (same ARMv5TE codegen as + // plain LDR+STR). + void to_add_count_increment_locked_() { +#if defined(ESPHOME_THREAD_SINGLE) // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.fetch_add(1, std::memory_order_relaxed); #else - this->to_add_count_++; + uint32_t v = __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED); + __atomic_store_n(&this->to_add_count_, v + 1, __ATOMIC_RELAXED); #endif } // Reset to_add_count_ (no-op on single-threaded platforms) - void to_add_count_clear_() { -#ifdef ESPHOME_THREAD_SINGLE + void to_add_count_clear_locked_() { +#if defined(ESPHOME_THREAD_SINGLE) // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.store(0, std::memory_order_relaxed); #else - this->to_add_count_ = 0; + __atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED); #endif } @@ -580,7 +583,8 @@ class Scheduler { std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) - // Fast-path counter for process_defer_queue_() to skip lock when nothing to process. + // Fast-path counter for process_defer_queue_() to skip lock when nothing to + // process. See to_add_count_ above for the NO_ATOMICS rationale. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic<uint32_t> defer_count_{0}; #else @@ -589,35 +593,35 @@ class Scheduler { bool defer_empty_() const { // defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path - // ESPHOME_THREAD_MULTI_NO_ATOMICS: always take the lock #ifdef ESPHOME_THREAD_MULTI_ATOMICS return this->defer_count_.load(std::memory_order_relaxed) == 0; #else - return false; + return __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) == 0; #endif } - void defer_count_increment_() { + void defer_count_increment_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.fetch_add(1, std::memory_order_relaxed); #else - this->defer_count_++; + uint32_t v = __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED); + __atomic_store_n(&this->defer_count_, v + 1, __ATOMIC_RELAXED); #endif } - void defer_count_clear_() { + void defer_count_clear_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.store(0, std::memory_order_relaxed); #else - this->defer_count_ = 0; + __atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED); #endif } #endif /* ESPHOME_THREAD_SINGLE */ - // Counter for items marked for removal. Incremented cross-thread in cancel_item_locked_(). - // On ESPHOME_THREAD_MULTI_ATOMICS this is read without a lock in the cleanup_() fast path; - // on ESPHOME_THREAD_MULTI_NO_ATOMICS the fast path is disabled so cleanup_() always takes the lock. + // Counter for items marked for removal. Incremented cross-thread in + // cancel_item_locked_(). See to_add_count_ above for the NO_ATOMICS + // rationale. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic<uint32_t> to_remove_{0}; #else @@ -626,44 +630,54 @@ class Scheduler { // Lock-free check if there are items to remove (for fast-path in cleanup_) bool to_remove_empty_() const { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_remove_.load(std::memory_order_relaxed) == 0; -#elif defined(ESPHOME_THREAD_SINGLE) - return this->to_remove_ == 0; +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) == 0; #else - return false; // Always take the lock path + return this->to_remove_ == 0; #endif } - void to_remove_add_(uint32_t count) { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS + void to_remove_add_locked_(uint32_t count) { +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + uint32_t v = __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED); + __atomic_store_n(&this->to_remove_, v + count, __ATOMIC_RELAXED); #else - this->to_remove_ += count; + this->to_remove_ += count; #endif } - void to_remove_decrement_() { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS + void to_remove_decrement_locked_() { +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + uint32_t v = __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED); + __atomic_store_n(&this->to_remove_, v - 1, __ATOMIC_RELAXED); #else - this->to_remove_--; + this->to_remove_--; #endif } - void to_remove_clear_() { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS + void to_remove_clear_locked_() { +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.store(0, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED); #else - this->to_remove_ = 0; + this->to_remove_ = 0; #endif } uint32_t to_remove_count_() const { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_remove_.load(std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED); #else - return this->to_remove_; + return this->to_remove_; #endif } diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index b8a299ff7eb..cf651c3e91a 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -74,8 +74,8 @@ uint64_t Millis64Impl::compute(uint32_t now) { // 2. Always locks when detecting a large backwards jump // 3. Updates without lock in normal forward progression (accepting minor races) // This is less efficient but necessary without atomic operations. - uint16_t major = millis_major; - uint32_t last = last_millis; + uint16_t major = __atomic_load_n(&millis_major, __ATOMIC_RELAXED); + uint32_t last = __atomic_load_n(&last_millis, __ATOMIC_RELAXED); // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption @@ -87,19 +87,26 @@ uint64_t Millis64Impl::compute(uint32_t now) { if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { // Near rollover or detected a rollover - need lock for safety LockGuard guard{lock}; - // Re-read with lock held - last = last_millis; + // Re-read both values with lock held. last_millis can be updated + // unlocked from the forward-progression branch below, so use an atomic + // load. millis_major can only be updated under this lock, but another + // thread may have completed a rollover between our unlocked loads above + // and the lock acquisition — reload or we'd return a stale high word. + last = __atomic_load_n(&last_millis, __ATOMIC_RELAXED); + major = __atomic_load_n(&millis_major, __ATOMIC_RELAXED); if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - millis_major++; + // True rollover detected (happens every ~49.7 days). + // Use the already-loaded `major` local; avoids a second read of the + // global (equivalent under the held lock). major++; + __atomic_store_n(&millis_major, major, __ATOMIC_RELAXED); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ } // Update last_millis while holding lock - last_millis = now; + __atomic_store_n(&last_millis, now, __ATOMIC_RELAXED); } else if (now > last) { // Normal case: Not near rollover and time moved forward // Update without lock. While this may cause minor races (microseconds of @@ -107,7 +114,7 @@ uint64_t Millis64Impl::compute(uint32_t now) { // 1. The scheduler operates at millisecond resolution, not microsecond // 2. We've already prevented the critical rollover race condition // 3. Any backwards movement is orders of magnitude smaller than scheduler delays - last_millis = now; + __atomic_store_n(&last_millis, now, __ATOMIC_RELAXED); } // If now <= last and we're not near rollover, don't update // This minimizes backwards time movement From b38db617a2f5da489f8160ad02d80c9561be1622 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:21:05 -0500 Subject: [PATCH 1623/2056] [core] Clean up stale includes and inline yield_with_select_ in application (#15945) --- esphome/components/libretiny/core.cpp | 2 +- esphome/core/application.cpp | 9 +-------- esphome/core/application.h | 19 ++++--------------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 1b74e3addb4..ca46bcb899e 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -56,7 +56,7 @@ void arch_init() { // // Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the // main loop, but below the TCP/IP thread (7) so packet processing keeps priority. - // This is safe because ESPHome yields voluntarily via yield_with_select_() and + // This is safe because ESPHome yields voluntarily via wakeable_delay() and // the Arduino mainTask yield() after each loop() iteration. static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6; static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES"); diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 11381030a31..d03696fbb65 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,9 +12,6 @@ #include <esp_ota_ops.h> #include <esp_bootloader_desc.h> #endif -#ifdef USE_LWIP_FAST_SELECT -#include "esphome/core/lwip_fast_select.h" -#endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include <algorithm> @@ -24,10 +21,6 @@ #include "esphome/components/status_led/status_led.h" #endif -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -#include "esphome/components/socket/socket.h" -#endif - namespace esphome { static const char *const TAG = "app"; @@ -366,7 +359,7 @@ void Application::teardown_components(uint32_t timeout_ms) { // Give some time for I/O operations if components are still pending if (pending_count > 0) { - this->yield_with_select_(1); + esphome::internal::wakeable_delay(1); } // Update time for next iteration diff --git a/esphome/core/application.h b/esphome/core/application.h index 8280b3bd4bc..b7004156814 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,9 +24,6 @@ #include "esphome/core/area.h" #endif -#ifdef USE_LWIP_FAST_SELECT -#include "esphome/core/lwip_fast_select.h" -#endif #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif @@ -423,10 +420,6 @@ class Application { void service_status_led_slow_(uint32_t time); #endif - /// Sleep for up to delay_ms, returning early if a wake event arrives. - /// Thin wrapper over the platform wake primitive in wake.h. - inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); - // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -664,18 +657,14 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { const uint32_t until_sched = this->scheduler.next_schedule_in(now).value_or(until_phase); delay_time = std::min(until_phase, until_sched); } - this->yield_with_select_(delay_time); + // All platforms route loop yields through the platform wake primitive. + // On host this drains the loopback wake socket via select(); on FreeRTOS + // targets it uses task notifications; on ESP8266/RP2040 it uses esp_delay/WFE. + esphome::internal::wakeable_delay(delay_time); if (this->dump_config_at_ < this->components_.size()) { this->process_dump_config_(); } } -// All platforms route loop yields through the platform wake primitive. -// On host this drains the loopback wake socket via select(); on FreeRTOS -// targets it uses task notifications; on ESP8266/RP2040 it uses esp_delay/WFE. -inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { - esphome::internal::wakeable_delay(delay_ms); -} - } // namespace esphome From 3ca86fc3fc6c41c2c51f15eadb2a1536e4955b3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:21:46 -0500 Subject: [PATCH 1624/2056] [core] Raise WDT_FEED_INTERVAL_MS to 2000ms on BK72xx (#15943) --- esphome/core/application.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index b7004156814..e9b386038ea 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -216,11 +216,19 @@ class Application { /// loops and scheduler items still feed after every op, so any op exceeding /// this threshold triggers a real feed naturally. /// Safety margins vs. platform watchdog timeouts: - /// - ESP32 task WDT default (5 s): ~16x - /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change - /// must keep comfortable margin here - /// - ESP8266 HW WDT (~6 s): ~20x + /// - ESP32 task WDT default (5 s): ~16x + /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change + /// must keep comfortable margin here + /// - ESP8266 HW WDT (~6 s): ~20x + /// - BK72xx HW WDT (10 s): ~5x <-- platform override below +#ifdef USE_BK72XX + // BDK busy-waits 200us per WDT reload (sctrl_dpll_delay200us). LibreTiny + // sets HW WDT to 10s; 2000ms keeps ~5x margin. See wdt_ctrl WCMD_RELOAD_PERIOD: + // https://github.com/libretiny-eu/framework-beken-bdk/blob/44800e7451ea30fbcbd3bb6e905315de59349fee/beken378/driver/wdt/wdt.c#L75-L87 + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 2000; +#else static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300; +#endif /// Feed the task watchdog. Cold entry — callers without a millis() /// timestamp in hand. Out of line to keep call sites tiny. From 8f9b91eecea69ab12f8afd7d408010daad2ced5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:22:17 -0500 Subject: [PATCH 1625/2056] [wifi] Avoid BDK 3.0.78 wifi_event_sta_disconnected_t collision on BK72xx (#15942) --- esphome/components/wifi/wifi_component_libretiny.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index cdd11ceaefa..6588e93e167 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -12,7 +12,12 @@ #ifdef USE_BK72XX extern "C" { +// BDK 3.0.78 (required for BK7238) redeclares wifi_event_sta_disconnected_t, +// which LibreTiny's Arduino WiFi API already defines. ESPHome doesn't use the +// BDK version, so rename it across this include to avoid the collision. +#define wifi_event_sta_disconnected_t bdk_wifi_event_sta_disconnected_t #include <wlan_ui_pub.h> +#undef wifi_event_sta_disconnected_t } #endif From 70ae614abd9c34cdf0be53feceb9c6f0624b39c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 08:23:38 -0500 Subject: [PATCH 1626/2056] [api] Fall back to plaintext for logger connections (#15938) --- esphome/components/api/client.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0c6c569c7d6..312d937f017 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -93,7 +93,24 @@ async def async_run_logs( config, raw_line, backtrace_state=backtrace_state ) - stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states) + # Safe to fall back to plaintext here only for this diagnostics use + # case: the stream is one-way from device to client, and this code + # never accepts commands or acts on any message the device sends. + # An on-path attacker could still both inject fabricated log lines + # and passively read the device's log output (and any state data + # delivered when subscribe_states is enabled), so this does lose + # confidentiality as well as authentication/integrity. That tradeoff + # is acceptable for operator-visible logs, which aioesphomeapi also + # warns may come from an unverified device. Never mirror this opt-in + # for any connection that sends data to the device or uses Home + # Assistant actions. + stop = await async_run( + cli, + on_log, + name=name, + subscribe_states=subscribe_states, + allow_plaintext_fallback=True, + ) try: await asyncio.Event().wait() finally: From 9b45b046a8992e65ff19b7610f2cc72e238ac760 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Thu, 23 Apr 2026 09:43:32 -0400 Subject: [PATCH 1627/2056] [core] Allow finding all devices as target that match mac suffix (#13135) --- esphome/__main__.py | 120 +++++- esphome/address_cache.py | 11 + esphome/async_thread.py | 56 +++ esphome/resolver.py | 48 +-- esphome/zeroconf.py | 181 ++++++++- tests/unit_tests/test_address_cache.py | 20 + tests/unit_tests/test_main.py | 513 ++++++++++++++++++++++++- tests/unit_tests/test_resolver.py | 33 +- 8 files changed, 912 insertions(+), 70 deletions(-) create mode 100644 esphome/async_thread.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 7879cdad0ca..8c80dab90af 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -39,6 +39,7 @@ from esphome.const import ( CONF_MDNS, CONF_MQTT, CONF_NAME, + CONF_NAME_ADD_MAC_SUFFIX, CONF_OTA, CONF_PASSWORD, CONF_PLATFORM, @@ -71,6 +72,7 @@ from esphome.util import ( run_external_process, safe_print, ) +from esphome.zeroconf import discover_mdns_devices _LOGGER = logging.getLogger(__name__) @@ -204,6 +206,64 @@ def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]: return [address] +def _populate_mdns_cache(hosts_to_addresses: dict[str, list[str]]) -> None: + """Store discovered ``host -> [ips]`` entries in ``CORE.address_cache``. + + Ensures ``CORE.address_cache`` exists, then records each mDNS hostname so + the downstream resolution path (``resolve_ip_address``) can skip opening a + second Zeroconf client. + """ + from esphome.address_cache import AddressCache + + if CORE.address_cache is None: + CORE.address_cache = AddressCache() + for host, addresses in hosts_to_addresses.items(): + if addresses: + _LOGGER.debug("Caching mDNS result %s -> %s", host, addresses) + CORE.address_cache.add_mdns_addresses(host, addresses) + + +def _discover_mac_suffix_devices() -> list[str] | None: + """Discover ``<name>-<mac>.local`` devices and cache their IPs. + + Returns: + - ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off, + mDNS disabled, or ``CORE.address`` is already an IP). Callers should + then fall back to whatever default OTA address they normally use. + - ``[]`` when discovery ran but found nothing. Callers should NOT fall + back to the base name: with ``name_add_mac_suffix`` enabled, the base + name by definition doesn't exist on the network. + - A non-empty sorted list of ``.local`` hostnames on success. + + Populates ``CORE.address_cache`` so downstream resolution (``espota2`` or + ``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we + already have without opening a second Zeroconf client. + """ + if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()): + return None + _LOGGER.info("Discovering devices...") + if not (discovered := discover_mdns_devices(CORE.name)): + _LOGGER.warning( + "No devices matching '%s-<mac>.local' were discovered.", CORE.name + ) + return [] + _populate_mdns_cache(discovered) + return list(discovered) + + +def _ota_hostnames_for_default(purpose: Purpose) -> list[str]: + """Return OTA hostname(s) for the ``--device OTA`` / default-resolve path. + + When ``name_add_mac_suffix`` is enabled, returns discovered + ``<name>-<mac>.local`` hostnames (possibly empty — in which case the + caller should not fall back to the base name). Otherwise falls back to + the cache-resolved ``CORE.address``. + """ + if (discovered := _discover_mac_suffix_devices()) is not None: + return discovered + return _resolve_with_cache(CORE.address, purpose) + + def choose_upload_log_host( default: list[str] | str | None, check_default: str | None, @@ -242,14 +302,14 @@ def choose_upload_log_host( resolved.append("MQTT") if has_api() and has_non_ip_address() and has_resolvable_address(): - resolved.extend(_resolve_with_cache(CORE.address, purpose)) + resolved.extend(_ota_hostnames_for_default(purpose)) elif purpose == Purpose.UPLOADING: if has_ota() and has_mqtt_ip_lookup(): resolved.append("MQTTIP") if has_ota() and has_non_ip_address() and has_resolvable_address(): - resolved.extend(_resolve_with_cache(CORE.address, purpose)) + resolved.extend(_ota_hostnames_for_default(purpose)) else: resolved.append(device) if not resolved: @@ -281,22 +341,29 @@ def choose_upload_log_host( elif bootsel.permission_error: bootsel_permission_error = True + def add_ota_options() -> None: + """Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled.""" + if (discovered := _discover_mac_suffix_devices()) is not None: + # Discovery was applicable. Use whatever we found — on empty, + # intentionally skip the base-name fallback since with + # name_add_mac_suffix on, the base name doesn't exist on the net. + for host in discovered: + options.append((f"Over The Air ({host})", host)) + elif has_resolvable_address(): + options.append((f"Over The Air ({CORE.address})", CORE.address)) + if has_mqtt_ip_lookup(): + options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + if purpose == Purpose.LOGGING: if has_mqtt_logging(): mqtt_config = CORE.config[CONF_MQTT] options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) if has_api(): - if has_resolvable_address(): - options.append((f"Over The Air ({CORE.address})", CORE.address)) - if has_mqtt_ip_lookup(): - options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + add_ota_options() elif purpose == Purpose.UPLOADING and has_ota(): - if has_resolvable_address(): - options.append((f"Over The Air ({CORE.address})", CORE.address)) - if has_mqtt_ip_lookup(): - options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + add_ota_options() # Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found if ( @@ -407,7 +474,17 @@ def has_resolvable_address() -> bool: return not CORE.address.endswith(".local") -def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): +def has_name_add_mac_suffix() -> bool: + """Check if name_add_mac_suffix is enabled in the config.""" + if CORE.config is None: + return False + esphome_config = CORE.config.get(CONF_ESPHOME, {}) + return esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False) + + +def mqtt_get_ip( + config: ConfigType, username: str, password: str, client_id: str +) -> list[str]: from esphome import mqtt return mqtt.get_esphome_device_ip(config, username, password, client_id) @@ -420,6 +497,9 @@ def _resolve_network_devices( This function filters the devices list to: - Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup + - Expand hostnames that are already in ``CORE.address_cache`` to their + cached IPs so downstream code (e.g. aioesphomeapi) doesn't open a second + Zeroconf client to resolve them - Deduplicate addresses while preserving order - Only resolve MQTT once even if multiple MQTT strings are present - If MQTT resolution fails, log a warning and continue with other devices @@ -444,13 +524,29 @@ def _resolve_network_devices( mqtt_ips = mqtt_get_ip( config, args.username, args.password, args.client_id ) - network_devices.extend(mqtt_ips) + # pylint can't infer mqtt_get_ip's return through its + # lazy ``from esphome import mqtt`` import, so it flags + # the genexpr below. + network_devices.extend( + addr + for addr in mqtt_ips # pylint: disable=not-an-iterable + if addr not in network_devices + ) except EsphomeError as err: _LOGGER.warning( "MQTT IP discovery failed (%s), will try other devices if available", err, ) mqtt_resolved = True + continue + + # If the hostname is already in the address cache (e.g. populated by + # mDNS discovery), substitute the cached IPs so aioesphomeapi doesn't + # open its own Zeroconf to re-resolve it. + if CORE.address_cache and (cached := CORE.address_cache.get_addresses(device)): + network_devices.extend( + addr for addr in cached if addr not in network_devices + ) elif device not in network_devices: # Regular network address or IP - add if not already present network_devices.append(device) diff --git a/esphome/address_cache.py b/esphome/address_cache.py index 7c20be90f00..4fb3689818b 100644 --- a/esphome/address_cache.py +++ b/esphome/address_cache.py @@ -101,6 +101,17 @@ class AddressCache: """Check if any cache entries exist.""" return bool(self.mdns_cache or self.dns_cache) + def add_mdns_addresses(self, hostname: str, addresses: list[str]) -> None: + """Store resolved mDNS addresses for ``hostname`` in the cache. + + Callers that discover ``.local`` hosts (e.g. via mDNS browse) can use + this to avoid a second resolution round-trip during the upload path. + No-op when ``addresses`` is empty. + """ + if not addresses: + return + self.mdns_cache[normalize_hostname(hostname)] = addresses + @classmethod def from_cli_args( cls, mdns_args: Iterable[str], dns_args: Iterable[str] diff --git a/esphome/async_thread.py b/esphome/async_thread.py new file mode 100644 index 00000000000..7be3c83a9a5 --- /dev/null +++ b/esphome/async_thread.py @@ -0,0 +1,56 @@ +"""Helpers for running an async coroutine from sync code via a daemon thread. + +``asyncio.run(coro())`` in the main thread blocks until the loop's cleanup +cycle finishes, which can add hundreds of milliseconds before the caller +receives the result. Running the loop in a daemon thread lets the caller +observe the result as soon as the coroutine completes while cleanup finishes +in the background. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +import threading +from typing import Generic, TypeVar + +_T = TypeVar("_T") + + +class AsyncThreadRunner(threading.Thread, Generic[_T]): + """Run an async coroutine in a daemon thread and expose its result. + + The runner catches all exceptions from the coroutine and stores them in + ``exception`` so ``event`` is always set — this prevents callers waiting + on ``event`` from hanging forever when the coroutine crashes. + + Typical usage:: + + runner = AsyncThreadRunner(lambda: my_coro(arg)) + runner.start() + if not runner.event.wait(timeout=5.0): + ... # timed out + if runner.exception is not None: + raise runner.exception + result = runner.result + """ + + def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None: + super().__init__(daemon=True) + self._coro_factory = coro_factory + self.result: _T | None = None + self.exception: BaseException | None = None + self.event = threading.Event() + + async def _runner(self) -> None: + try: + self.result = await self._coro_factory() + except Exception as exc: # pylint: disable=broad-except + # Capture all exceptions so ``event`` is always set — otherwise a + # crash would hang the waiter forever. + self.exception = exc + finally: + self.event.set() + + def run(self) -> None: + asyncio.run(self._runner()) diff --git a/esphome/resolver.py b/esphome/resolver.py index 99482aa20e9..9fb596ce7b5 100644 --- a/esphome/resolver.py +++ b/esphome/resolver.py @@ -2,66 +2,52 @@ from __future__ import annotations -import asyncio -import threading - from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError import aioesphomeapi.host_resolver as hr +from esphome.async_thread import AsyncThreadRunner from esphome.core import EsphomeError RESOLVE_TIMEOUT = 10.0 # seconds -class AsyncResolver(threading.Thread): +class AsyncResolver: """Resolver using aioesphomeapi that runs in a thread for faster results. - This resolver uses aioesphomeapi's async_resolve_host to handle DNS resolution, - including proper .local domain fallback. Running in a thread allows us to get - the result immediately without waiting for asyncio.run() to complete its - cleanup cycle, which can take significant time. + This resolver uses aioesphomeapi's async_resolve_host to handle DNS + resolution, including proper .local domain fallback. Running in a thread + (via :class:`AsyncThreadRunner`) allows us to get the result immediately + without waiting for ``asyncio.run()`` to complete its cleanup cycle, which + can take significant time. """ def __init__(self, hosts: list[str], port: int) -> None: """Initialize the resolver.""" - super().__init__(daemon=True) self.hosts = hosts self.port = port - self.result: list[hr.AddrInfo] | None = None - self.exception: Exception | None = None - self.event = threading.Event() - async def _resolve(self) -> None: + async def _resolve(self) -> list[hr.AddrInfo]: """Resolve hostnames to IP addresses.""" - try: - self.result = await hr.async_resolve_host( - self.hosts, self.port, timeout=RESOLVE_TIMEOUT - ) - except Exception as e: # pylint: disable=broad-except - # We need to catch all exceptions to ensure the event is set - # Otherwise the thread could hang forever - self.exception = e - finally: - self.event.set() - - def run(self) -> None: - """Run the DNS resolution.""" - asyncio.run(self._resolve()) + return await hr.async_resolve_host( + self.hosts, self.port, timeout=RESOLVE_TIMEOUT + ) def resolve(self) -> list[hr.AddrInfo]: """Start the thread and wait for the result.""" - self.start() + runner: AsyncThreadRunner[list[hr.AddrInfo]] = AsyncThreadRunner(self._resolve) + runner.start() - if not self.event.wait( + if not runner.event.wait( timeout=RESOLVE_TIMEOUT + 1.0 ): # Give it 1 second more than the resolver timeout raise EsphomeError("Timeout resolving IP address") - if exc := self.exception: + if exc := runner.exception: if isinstance(exc, ResolveTimeoutAPIError): raise EsphomeError(f"Timeout resolving IP address: {exc}") from exc if isinstance(exc, ResolveAPIError): raise EsphomeError(f"Error resolving IP address: {exc}") from exc raise exc - return self.result + assert runner.result is not None # guaranteed when event set and no exception + return runner.result diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index dd45b58a6cb..6f5d33c8085 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -14,8 +14,13 @@ from zeroconf import ( ) from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf +from esphome.async_thread import AsyncThreadRunner from esphome.storage_json import StorageJSON, ext_storage_path +# Length of the MAC suffix appended when name_add_mac_suffix is enabled. +MAC_SUFFIX_LEN = 6 +_HEX_CHARS = frozenset("0123456789abcdef") + _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10.0 @@ -188,15 +193,177 @@ class EsphomeZeroconf(Zeroconf): return None +async def async_resolve_hosts( + zeroconf: Zeroconf, hosts: list[str], timeout: float = DEFAULT_TIMEOUT +) -> dict[str, list[str]]: + """Resolve ``hosts`` to IPs using a shared ``Zeroconf`` instance. + + Tries the cache synchronously first (so hosts already primed by a recent + browse return immediately with no network round-trip), then issues + ``async_request`` for the remaining misses in parallel via + ``asyncio.gather``. Returns a dict mapping each host to its list of + addresses (empty list when unresolved). Only ``<short>.local`` form is + queried, matching the name scheme the resolvers below expect. + """ + resolvers: dict[str, AddressResolver] = {} + pending: list[str] = [] + for host in hosts: + resolver = AddressResolver(f"{host.partition('.')[0]}.local.") + resolvers[host] = resolver + if not resolver.load_from_cache(zeroconf): + pending.append(host) + + if pending and timeout: + results = await asyncio.gather( + *( + resolvers[host].async_request(zeroconf, timeout * 1000) + for host in pending + ), + return_exceptions=True, + ) + for host, result in zip(pending, results): + if isinstance(result, BaseException): + _LOGGER.debug("Failed to resolve %s: %s", host, result) + + return { + host: resolver.parsed_scoped_addresses(IPVersion.All) + for host, resolver in resolvers.items() + } + + class AsyncEsphomeZeroconf(AsyncZeroconf): async def async_resolve_host( self, host: str, timeout: float = DEFAULT_TIMEOUT ) -> list[str] | None: """Resolve a host name to an IP address.""" - info = AddressResolver(f"{host.partition('.')[0]}.local.") - if ( - info.load_from_cache(self.zeroconf) - or (timeout and await info.async_request(self.zeroconf, timeout * 1000)) - ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)): - return addresses - return None + addresses = (await async_resolve_hosts(self.zeroconf, [host], timeout))[host] + return addresses or None + + +def _is_mac_suffix_match(device_name: str, prefix: str) -> bool: + """Return True if ``device_name`` is ``prefix`` followed by a 6-char hex MAC.""" + if not device_name.startswith(prefix): + return False + suffix = device_name[len(prefix) :] + return len(suffix) == MAC_SUFFIX_LEN and all(c in _HEX_CHARS for c in suffix) + + +async def async_discover_mdns_devices( + base_name: str, timeout: float = 5.0 +) -> dict[str, list[str]]: + """Discover ESPHome devices via mDNS that match the base name + MAC suffix. + + When ``name_add_mac_suffix`` is enabled, devices advertise as + ``<base_name>-<6-hex-mac>.local``. This function uses a single + ``AsyncEsphomeZeroconf`` lifecycle to both browse for matching services and + resolve their IP addresses, so callers get resolved addresses without + opening a second Zeroconf client. + + Args: + base_name: The base device name (without MAC suffix). + timeout: How long to wait for mDNS responses (default 5 seconds). + + Returns: + Mapping of ``<device>.local`` hostnames to their resolved IP addresses + (may be empty for a device if resolution failed within the timeout). + """ + prefix = f"{base_name}-" + # Preserves insertion order for stable output and deduplicates + discovered: dict[str, list[str]] = {} + + def on_service_state_change( + zeroconf: Zeroconf, + service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: + if state_change not in (ServiceStateChange.Added, ServiceStateChange.Updated): + return + device_name = name.partition(".")[0] + if not _is_mac_suffix_match(device_name, prefix): + _LOGGER.debug( + "Ignoring %s (%s): does not match '%s<6-hex>'", + device_name, + state_change.name, + prefix, + ) + return + host = f"{device_name}.local" + if host in discovered: + return + discovered[host] = [] + _LOGGER.debug("Discovered %s (%s)", host, state_change.name) + + _LOGGER.debug( + "Starting mDNS discovery for '%s<mac>.local' (timeout=%.1fs)", + prefix, + timeout, + ) + try: + aiozc = AsyncEsphomeZeroconf() + except Exception as err: # pylint: disable=broad-except + # Zeroconf init can raise OSError, NonUniqueNameException, etc. + # Any failure here just means we can't discover — log and move on. + _LOGGER.warning("mDNS discovery failed to initialize: %s", err) + return {} + + try: + browser = AsyncServiceBrowser( + aiozc.zeroconf, + ESPHOME_SERVICE_TYPE, + handlers=[on_service_state_change], + ) + try: + await asyncio.sleep(timeout) + finally: + await browser.async_cancel() + _LOGGER.debug( + "Browse finished: %d device(s) matched '%s<mac>'", + len(discovered), + prefix, + ) + + # Resolve each discovered hostname on the SAME Zeroconf instance so + # we don't spin up a second client. ``async_resolve_hosts`` tries the + # cache synchronously (the browse usually primes it) before issuing + # any ``async_request`` in parallel for misses. + resolved = await async_resolve_hosts(aiozc.zeroconf, list(discovered)) + for host, addresses in resolved.items(): + if addresses: + discovered[host] = addresses + _LOGGER.debug("Resolved %s -> %s", host, addresses) + else: + _LOGGER.debug("No addresses returned for %s", host) + finally: + await aiozc.async_close() + + return dict(sorted(discovered.items())) + + +def _await_discovery( + runner: AsyncThreadRunner[dict[str, list[str]]], timeout: float +) -> dict[str, list[str]]: + """Wait for ``runner`` to finish and return its discovery result. + + Split out of :func:`discover_mdns_devices` so the timeout branch is + testable without patching ``asyncio`` or ``threading`` internals — a test + passes a stub whose ``event.wait`` returns ``False``. + """ + # Give the discovery an extra second over the browse timeout for the + # resolution + cleanup pass. + if not runner.event.wait(timeout=timeout + 2.0): + _LOGGER.warning("mDNS discovery timed out after %.1fs", timeout) + return {} + if runner.exception is not None: + _LOGGER.warning("mDNS discovery failed: %s", runner.exception) + return {} + return runner.result or {} + + +def discover_mdns_devices(base_name: str, timeout: float = 5.0) -> dict[str, list[str]]: + """Synchronous wrapper around :func:`async_discover_mdns_devices`.""" + runner = AsyncThreadRunner( + lambda: async_discover_mdns_devices(base_name, timeout=timeout) + ) + runner.start() + return _await_discovery(runner, timeout) diff --git a/tests/unit_tests/test_address_cache.py b/tests/unit_tests/test_address_cache.py index de43830d532..1ca28c4f029 100644 --- a/tests/unit_tests/test_address_cache.py +++ b/tests/unit_tests/test_address_cache.py @@ -121,6 +121,26 @@ def test_get_addresses_auto_detection() -> None: assert cache.get_addresses("unknown.com") is None +def test_add_mdns_addresses_stores_and_normalizes() -> None: + """add_mdns_addresses inserts entries under the normalized hostname.""" + cache = AddressCache() + cache.add_mdns_addresses("Device.Local.", ["192.168.1.10", "192.168.1.11"]) + + assert cache.mdns_cache == { + normalize_hostname("Device.Local."): ["192.168.1.10", "192.168.1.11"] + } + # Overwrites on subsequent calls for the same host + cache.add_mdns_addresses("device.local", ["10.0.0.1"]) + assert cache.mdns_cache[normalize_hostname("device.local")] == ["10.0.0.1"] + + +def test_add_mdns_addresses_empty_is_noop() -> None: + """Passing an empty address list must not create an entry.""" + cache = AddressCache() + cache.add_mdns_addresses("device.local", []) + assert cache.mdns_cache == {} + + def test_has_cache() -> None: """Test checking if cache has entries.""" # Empty cache diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index e07b4accf23..8ec9e70cf85 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Generator +from collections.abc import Callable, Generator from dataclasses import dataclass import json import logging @@ -12,16 +12,18 @@ import re import sys import time from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from pytest import CaptureFixture +from zeroconf import ServiceStateChange from esphome import platformio_api from esphome.__main__ import ( Purpose, _get_configured_xtal_freq, _make_crystal_freq_callback, + _resolve_network_devices, choose_upload_log_host, command_analyze_memory, command_bundle, @@ -36,6 +38,7 @@ from esphome.__main__ import ( has_mqtt, has_mqtt_ip_lookup, has_mqtt_logging, + has_name_add_mac_suffix, has_non_ip_address, has_ota, has_resolvable_address, @@ -48,6 +51,7 @@ from esphome.__main__ import ( upload_using_picotool, upload_using_platformio, ) +from esphome.address_cache import AddressCache from esphome.bundle import BUNDLE_EXTENSION, BundleFile, BundleResult from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( @@ -62,6 +66,7 @@ from esphome.const import ( CONF_MDNS, CONF_MQTT, CONF_NAME, + CONF_NAME_ADD_MAC_SUFFIX, CONF_OTA, CONF_PASSWORD, CONF_PLATFORM, @@ -79,6 +84,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError from esphome.util import BootselResult +from esphome.zeroconf import _await_discovery, discover_mdns_devices def strip_ansi_codes(text: str) -> str: @@ -2218,6 +2224,509 @@ def test_has_resolvable_address() -> None: assert has_resolvable_address() is False +def test_has_name_add_mac_suffix() -> None: + """Test has_name_add_mac_suffix function.""" + + # Test with name_add_mac_suffix enabled + setup_core(config={CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: True}}) + assert has_name_add_mac_suffix() is True + + # Test with name_add_mac_suffix disabled + setup_core(config={CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: False}}) + assert has_name_add_mac_suffix() is False + + # Test with name_add_mac_suffix not set (defaults to False) + setup_core(config={CONF_ESPHOME: {}}) + assert has_name_add_mac_suffix() is False + + # Test with no esphome config + setup_core(config={}) + assert has_name_add_mac_suffix() is False + + # Test with no config at all + CORE.config = None + assert has_name_add_mac_suffix() is False + + +@pytest.fixture +def mock_mdns_discovery() -> Generator[MagicMock]: + """Fixture to mock the async mDNS discovery infrastructure. + + Patches ``AsyncEsphomeZeroconf``, ``AsyncServiceBrowser`` and + ``AddressResolver`` in ``esphome.zeroconf`` and exposes hooks for tests to + stage browser events and control resolution results. The default + ``AddressResolver`` stub simulates a cache hit returning no addresses, so + matched hosts appear in the discovery output with empty address lists + unless the test overrides ``_resolver_setup``. + """ + with ( + patch("esphome.zeroconf.AsyncEsphomeZeroconf") as mock_aiozc_class, + patch("esphome.zeroconf.AsyncServiceBrowser") as mock_browser_class, + patch("esphome.zeroconf.AddressResolver") as mock_resolver_class, + ): + mock_aiozc = MagicMock() + mock_aiozc.zeroconf = MagicMock() + mock_aiozc.async_close = AsyncMock(return_value=None) + mock_aiozc_class.return_value = mock_aiozc + + mock_browser = MagicMock() + mock_browser.async_cancel = AsyncMock(return_value=None) + + # Default: each host gets a fresh resolver that hits the cache and + # returns no addresses. Tests can override via ``_resolver_setup``. + def default_resolver_factory(name: str) -> MagicMock: + resolver = MagicMock() + resolver._name = name + resolver.load_from_cache.return_value = True + resolver.async_request = AsyncMock(return_value=True) + resolver.parsed_scoped_addresses.return_value = [] + return resolver + + mock_resolver_class.side_effect = default_resolver_factory + + # Store references for test access + mock_aiozc._mock_browser_class = mock_browser_class + mock_aiozc._mock_browser = mock_browser + mock_aiozc._mock_class = mock_aiozc_class + mock_aiozc._mock_resolver_class = mock_resolver_class + yield mock_aiozc + + +@pytest.mark.parametrize( + ("discovered_services", "base_name", "expected_hosts"), + [ + # Matching devices; different-prefix device is filtered out + ( + [ + ("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Added), + ("mydevice-def456._esphomelib._tcp.local.", ServiceStateChange.Added), + ( + "otherdevice-abcdef._esphomelib._tcp.local.", + ServiceStateChange.Added, + ), + ], + "mydevice", + ["mydevice-abc123.local", "mydevice-def456.local"], + ), + # No matches at all + ( + [ + ( + "otherdevice-abcdef._esphomelib._tcp.local.", + ServiceStateChange.Added, + ), + ], + "mydevice", + [], + ), + # Deduplication (same device Added then Updated) + ( + [ + ("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Added), + ("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Updated), + ], + "mydevice", + ["mydevice-abc123.local"], + ), + # Suffix must be exactly 6 hex chars: wrong length and non-hex are rejected + ( + [ + # too short + ("mydevice-abcd._esphomelib._tcp.local.", ServiceStateChange.Added), + # too long + ( + "mydevice-abcdef1._esphomelib._tcp.local.", + ServiceStateChange.Added, + ), + # non-hex + ("mydevice-xyz123._esphomelib._tcp.local.", ServiceStateChange.Added), + # valid + ("mydevice-012345._esphomelib._tcp.local.", ServiceStateChange.Added), + ], + "mydevice", + ["mydevice-012345.local"], + ), + # Prefix-collision: base "foo" must not match "foo-bar-abc123" + ( + [ + ("foo-abcdef._esphomelib._tcp.local.", ServiceStateChange.Added), + ("foo-bar-abcdef._esphomelib._tcp.local.", ServiceStateChange.Added), + ], + "foo", + ["foo-abcdef.local"], + ), + ], + ids=[ + "matching_with_filter", + "no_matches", + "deduplication", + "hex_suffix_filter", + "prefix_collision", + ], +) +def test_discover_mdns_devices( + mock_mdns_discovery: MagicMock, + discovered_services: list[tuple[str, ServiceStateChange]], + base_name: str, + expected_hosts: list[str], +) -> None: + """Test discover_mdns_devices filtering and deduplication.""" + mock_browser = mock_mdns_discovery._mock_browser + + def capture_callback( + zc: MagicMock, + service_type: str, + handlers: list[Callable[..., None]], + ) -> MagicMock: + callback = handlers[0] + for service_name, state_change in discovered_services: + callback( + mock_mdns_discovery.zeroconf, service_type, service_name, state_change + ) + return mock_browser + + mock_mdns_discovery._mock_browser_class.side_effect = capture_callback + + # Each discovered host gets a resolver that returns a unique IP string + # derived from its server name so we can assert per-host. + def resolver_factory(name: str) -> MagicMock: + resolver = MagicMock() + resolver._name = name + resolver.load_from_cache.return_value = True + resolver.async_request = AsyncMock(return_value=True) + resolver.parsed_scoped_addresses.return_value = [f"10.0.0.1#{name}"] + return resolver + + mock_mdns_discovery._mock_resolver_class.side_effect = resolver_factory + + result = discover_mdns_devices(base_name, timeout=0) + + assert sorted(result) == expected_hosts + # Resolved addresses should be stored for matched hosts. AddressResolver + # receives the fully-qualified name (``<device>.local.``). + for host in expected_hosts: + short = host.partition(".")[0] + assert result[host] == [f"10.0.0.1#{short}.local."] + mock_browser.async_cancel.assert_awaited_once() + mock_mdns_discovery.async_close.assert_awaited_once() + + +def test_discover_mdns_devices_init_failure(caplog: pytest.LogCaptureFixture) -> None: + """If AsyncEsphomeZeroconf fails to init, return empty dict and log warning.""" + with ( + patch( + "esphome.zeroconf.AsyncEsphomeZeroconf", + side_effect=OSError("no network"), + ), + caplog.at_level(logging.WARNING, logger="esphome.zeroconf"), + ): + result = discover_mdns_devices("mydevice", timeout=0) + + assert result == {} + assert "mDNS discovery failed to initialize" in caplog.text + + +def test_discover_mdns_devices_resolution_failure( + mock_mdns_discovery: MagicMock, +) -> None: + """If resolution raises, the host is still listed with an empty address list.""" + mock_browser = mock_mdns_discovery._mock_browser + + def capture_callback( + zc: MagicMock, + service_type: str, + handlers: list[Callable[..., None]], + ) -> MagicMock: + handlers[0]( + mock_mdns_discovery.zeroconf, + service_type, + "mydevice-abc123._esphomelib._tcp.local.", + ServiceStateChange.Added, + ) + return mock_browser + + mock_mdns_discovery._mock_browser_class.side_effect = capture_callback + + # Resolver misses the cache, then async_request raises. + def failing_resolver_factory(name: str) -> MagicMock: + resolver = MagicMock() + resolver.load_from_cache.return_value = False + resolver.async_request = AsyncMock(side_effect=OSError("boom")) + resolver.parsed_scoped_addresses.return_value = [] + return resolver + + mock_mdns_discovery._mock_resolver_class.side_effect = failing_resolver_factory + + result = discover_mdns_devices("mydevice", timeout=0) + + assert result == {"mydevice-abc123.local": []} + + +def test_discover_mdns_devices_ignores_removed_state( + mock_mdns_discovery: MagicMock, +) -> None: + """``Removed`` state changes are ignored and do not appear in the result.""" + mock_browser = mock_mdns_discovery._mock_browser + + def capture_callback( + zc: MagicMock, + service_type: str, + handlers: list[Callable[..., None]], + ) -> MagicMock: + handlers[0]( + mock_mdns_discovery.zeroconf, + service_type, + "mydevice-abc123._esphomelib._tcp.local.", + ServiceStateChange.Removed, + ) + return mock_browser + + mock_mdns_discovery._mock_browser_class.side_effect = capture_callback + + result = discover_mdns_devices("mydevice", timeout=0) + + assert result == {} + # No AddressResolver should have been constructed since no host matched. + mock_mdns_discovery._mock_resolver_class.assert_not_called() + + +def test_discover_mdns_devices_empty_resolution( + mock_mdns_discovery: MagicMock, +) -> None: + """Host is listed with empty addresses when resolver returns no addresses.""" + mock_browser = mock_mdns_discovery._mock_browser + + def capture_callback( + zc: MagicMock, + service_type: str, + handlers: list[Callable[..., None]], + ) -> MagicMock: + handlers[0]( + mock_mdns_discovery.zeroconf, + service_type, + "mydevice-abc123._esphomelib._tcp.local.", + ServiceStateChange.Added, + ) + return mock_browser + + mock_mdns_discovery._mock_browser_class.side_effect = capture_callback + # Default fixture resolver is a cache-hit with no addresses — simulates + # the "browse found it but no A/AAAA records are available" case. + + result = discover_mdns_devices("mydevice", timeout=0) + + assert result == {"mydevice-abc123.local": []} + + +def test_resolve_network_devices_expands_cached_mdns_hosts(tmp_path: Path) -> None: + """Hostnames in ``CORE.address_cache`` are expanded to their cached IPs.""" + setup_core(tmp_path=tmp_path) + CORE.address_cache = AddressCache( + mdns_cache={ + "device-abc123.local": ["10.0.0.1", "10.0.0.2"], + } + ) + + result = _resolve_network_devices( + ["device-abc123.local", "192.168.1.50", "device-abc123.local"], + CORE.config, + MockArgs(), + ) + + # Cached hostname is replaced with its IPs (deduplicated across repeats) + # and the literal IP is preserved after. + assert result == ["10.0.0.1", "10.0.0.2", "192.168.1.50"] + + +def test_resolve_network_devices_keeps_uncached_hosts(tmp_path: Path) -> None: + """Hostnames not in the cache pass through unchanged.""" + setup_core(tmp_path=tmp_path) + CORE.address_cache = AddressCache() + + result = _resolve_network_devices( + ["unknown.local", "192.168.1.50"], + CORE.config, + MockArgs(), + ) + + assert result == ["unknown.local", "192.168.1.50"] + + +def test_await_discovery_timeout_returns_empty( + caplog: pytest.LogCaptureFixture, +) -> None: + """If the discovery runner never sets its event, return {} and warn.""" + stub = MagicMock() + stub.event.wait.return_value = False + stub.exception = None + stub.result = {"should_not_be_read": ["1.2.3.4"]} + + with caplog.at_level(logging.WARNING, logger="esphome.zeroconf"): + result = _await_discovery(stub, timeout=0.01) + + assert result == {} + assert "mDNS discovery timed out after 0.0s" in caplog.text + stub.event.wait.assert_called_once_with(timeout=pytest.approx(2.01)) + + +def test_await_discovery_propagates_exception_as_empty( + caplog: pytest.LogCaptureFixture, +) -> None: + """If the coroutine raised, log and return {} rather than re-raise.""" + stub = MagicMock() + stub.event.wait.return_value = True + stub.exception = RuntimeError("boom") + stub.result = None + + with caplog.at_level(logging.WARNING, logger="esphome.zeroconf"): + result = _await_discovery(stub, timeout=5.0) + + assert result == {} + assert "mDNS discovery failed: boom" in caplog.text + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_discovers_mac_suffix_devices(tmp_path: Path) -> None: + """Interactive mode discovers MAC-suffixed devices and populates the cache.""" + setup_core( + config={ + CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: True}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + }, + address="mydevice.local", + tmp_path=tmp_path, + name="mydevice", + ) + CORE.address_cache = None + + discovered = { + "mydevice-abc123.local": ["10.0.0.1"], + "mydevice-def456.local": ["10.0.0.2"], + } + with ( + patch( + "esphome.__main__.discover_mdns_devices", return_value=discovered + ) as mock_discover, + patch( + "esphome.__main__.choose_prompt", return_value="mydevice-abc123.local" + ) as mock_prompt, + ): + result = choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert result == ["mydevice-abc123.local"] + mock_discover.assert_called_once_with("mydevice") + mock_prompt.assert_called_once_with( + [ + ("Over The Air (mydevice-abc123.local)", "mydevice-abc123.local"), + ("Over The Air (mydevice-def456.local)", "mydevice-def456.local"), + ], + purpose=Purpose.UPLOADING, + ) + # Resolved IPs should be cached so downstream resolution skips a second + # Zeroconf lookup. + assert CORE.address_cache is not None + assert CORE.address_cache.get_mdns_addresses("mydevice-abc123.local") == [ + "10.0.0.1" + ] + assert CORE.address_cache.get_mdns_addresses("mydevice-def456.local") == [ + "10.0.0.2" + ] + + +@pytest.mark.usefixtures("mock_no_serial_ports") +def test_choose_upload_log_host_mac_suffix_no_devices_found( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """When discovery finds nothing, no OTA option is offered and a warning logs.""" + setup_core( + config={ + CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: True}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + }, + address="mydevice.local", + tmp_path=tmp_path, + name="mydevice", + ) + + with ( + patch("esphome.__main__.discover_mdns_devices", return_value={}), + caplog.at_level(logging.WARNING, logger="esphome.__main__"), + pytest.raises(EsphomeError), + ): + choose_upload_log_host( + default=None, + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert "No devices matching 'mydevice-<mac>.local'" in caplog.text + + +def test_choose_upload_log_host_default_ota_discovers_mac_suffix( + tmp_path: Path, +) -> None: + """``--device OTA`` also runs mDNS discovery when name_add_mac_suffix is on.""" + setup_core( + config={ + CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: True}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + }, + address="mydevice.local", + tmp_path=tmp_path, + name="mydevice", + ) + CORE.address_cache = None + + discovered = { + "mydevice-abc123.local": ["10.0.0.1"], + "mydevice-def456.local": ["10.0.0.2"], + } + with patch( + "esphome.__main__.discover_mdns_devices", return_value=discovered + ) as mock_discover: + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + + # Both discovered hostnames are returned so aioesphomeapi / espota2 can + # try each in turn with the cached IPs. + assert result == ["mydevice-abc123.local", "mydevice-def456.local"] + mock_discover.assert_called_once_with("mydevice") + assert CORE.address_cache is not None + assert CORE.address_cache.get_mdns_addresses("mydevice-abc123.local") == [ + "10.0.0.1" + ] + + +def test_choose_upload_log_host_default_ota_no_suffix_discovery( + tmp_path: Path, +) -> None: + """``--device OTA`` without name_add_mac_suffix uses CORE.address as-is.""" + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, + address="192.168.1.100", + tmp_path=tmp_path, + name="mydevice", + ) + + with patch("esphome.__main__.discover_mdns_devices") as mock_discover: + result = choose_upload_log_host( + default="OTA", + check_default=None, + purpose=Purpose.UPLOADING, + ) + + assert result == ["192.168.1.100"] + # Discovery must NOT run when name_add_mac_suffix is disabled. + mock_discover.assert_not_called() + + def test_command_wizard(tmp_path: Path) -> None: """Test command_wizard function.""" config_file = tmp_path / "test.yaml" diff --git a/tests/unit_tests/test_resolver.py b/tests/unit_tests/test_resolver.py index b4cca05d9fd..7862c268ca1 100644 --- a/tests/unit_tests/test_resolver.py +++ b/tests/unit_tests/test_resolver.py @@ -4,7 +4,7 @@ from __future__ import annotations import re import socket -from unittest.mock import patch +from unittest.mock import MagicMock, patch from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr @@ -115,24 +115,21 @@ def test_async_resolver_generic_exception() -> None: def test_async_resolver_thread_timeout() -> None: - """Test timeout when thread doesn't complete in time.""" - # Mock the start method to prevent actual thread execution - with ( - patch.object(AsyncResolver, "start"), - patch("esphome.resolver.hr.async_resolve_host"), - ): - resolver = AsyncResolver(["test.local"], 6053) - # Override event.wait to simulate timeout (return False = timeout occurred) - with ( - patch.object(resolver.event, "wait", return_value=False), - pytest.raises( - EsphomeError, match=re.escape("Timeout resolving IP address") - ), - ): - resolver.resolve() + """Test timeout when the runner thread doesn't complete in time.""" + # Patch AsyncThreadRunner inside esphome.resolver so we never actually + # start a thread and can control the wait return value directly. + fake_runner = MagicMock() + fake_runner.start = MagicMock() + fake_runner.event.wait.return_value = False # simulate timeout - # Verify thread start was called - resolver.start.assert_called_once() + with ( + patch("esphome.resolver.AsyncThreadRunner", return_value=fake_runner), + patch("esphome.resolver.hr.async_resolve_host"), + pytest.raises(EsphomeError, match=re.escape("Timeout resolving IP address")), + ): + AsyncResolver(["test.local"], 6053).resolve() + + fake_runner.start.assert_called_once() def test_async_resolver_ip_addresses(mock_addr_info_ipv4: AddrInfo) -> None: From f757cd1210447b6145bdd87cf50bdb4bcab164fd Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:46:56 +0200 Subject: [PATCH 1628/2056] [zigbee][core] Add support for Zigbee binary sensors on ESP32 H2 and C6 (#11553) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .clang-tidy.hash | 2 +- CODEOWNERS | 2 +- esphome/components/zigbee/__init__.py | 109 +++++- esphome/components/zigbee/automation.h | 3 + esphome/components/zigbee/const.py | 32 ++ esphome/components/zigbee/const_esp32.py | 35 ++ esphome/components/zigbee/const_zephyr.py | 21 -- esphome/components/zigbee/time/__init__.py | 3 +- .../zigbee/zigbee_attribute_esp32.cpp | 89 +++++ .../zigbee/zigbee_attribute_esp32.h | 90 +++++ esphome/components/zigbee/zigbee_ep_esp32.py | 70 ++++ esphome/components/zigbee/zigbee_esp32.cpp | 313 ++++++++++++++++++ esphome/components/zigbee/zigbee_esp32.h | 134 ++++++++ esphome/components/zigbee/zigbee_esp32.py | 274 +++++++++++++++ .../components/zigbee/zigbee_helpers_esp32.c | 74 +++++ .../components/zigbee/zigbee_helpers_esp32.h | 27 ++ esphome/components/zigbee/zigbee_zephyr.py | 27 +- esphome/core/defines.h | 1 + esphome/idf_component.yml | 8 + sdkconfig.defaults | 5 + tests/components/zigbee/common.yaml | 10 - tests/components/zigbee/common_esp32.yaml | 14 + tests/components/zigbee/common_nrf52.yaml | 12 + .../components/zigbee/test.esp32-c6-idf.yaml | 1 + .../zigbee/test.nrf52-adafruit.yaml | 2 +- .../components/zigbee/test.nrf52-mcumgr.yaml | 2 +- .../zigbee/test.nrf52-xiao-ble.yaml | 2 +- 27 files changed, 1295 insertions(+), 67 deletions(-) create mode 100644 esphome/components/zigbee/const.py create mode 100644 esphome/components/zigbee/const_esp32.py create mode 100644 esphome/components/zigbee/zigbee_attribute_esp32.cpp create mode 100644 esphome/components/zigbee/zigbee_attribute_esp32.h create mode 100644 esphome/components/zigbee/zigbee_ep_esp32.py create mode 100644 esphome/components/zigbee/zigbee_esp32.cpp create mode 100644 esphome/components/zigbee/zigbee_esp32.h create mode 100644 esphome/components/zigbee/zigbee_esp32.py create mode 100644 esphome/components/zigbee/zigbee_helpers_esp32.c create mode 100644 esphome/components/zigbee/zigbee_helpers_esp32.h create mode 100644 tests/components/zigbee/common_esp32.yaml create mode 100644 tests/components/zigbee/common_nrf52.yaml create mode 100644 tests/components/zigbee/test.esp32-c6-idf.yaml diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 9b6b8176334..41e1b7bd2fe 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -256216e144a626c8c9d1a458920a9db3de7dfc8c6a1b44b87946b9752e81026c +1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324 diff --git a/CODEOWNERS b/CODEOWNERS index 92efe4da4e5..69f2cb1d178 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -600,6 +600,6 @@ esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 -esphome/components/zigbee/* @tomaszduda23 +esphome/components/zigbee/* @luar123 @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zwave_proxy/* @kbx81 diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 280ff6b50cc..126e3aa2cd1 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -3,26 +3,42 @@ from typing import Any from esphome import automation, core import esphome.codegen as cg +from esphome.components.esp32 import only_on_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, +) from esphome.components.nrf52.boards import BOOTLOADER_CONFIG, Section from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME +from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType +from .const import ( + CONF_ON_JOIN, + CONF_POWER_SOURCE, + CONF_REPORT, + CONF_ROUTER, + CONF_WIPE_ON_BOOT, + KEY_ZIGBEE, + POWER_SOURCE, + REPORT, + ZigbeeComponent, + zigbee_ns, +) from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, CONF_MAX_EP_NUMBER, - CONF_ON_JOIN, - CONF_POWER_SOURCE, - CONF_WIPE_ON_BOOT, CONF_ZIGBEE_ID, KEY_EP_NUMBER, - KEY_ZIGBEE, - POWER_SOURCE, - ZigbeeComponent, - zigbee_ns, +) +from .zigbee_esp32 import ( + final_validate_esp32, + validate_binary_sensor_esp32, + zigbee_require_vfs_select, ) from .zigbee_zephyr import ( zephyr_binary_sensor, @@ -33,11 +49,11 @@ from .zigbee_zephyr import ( _LOGGER = logging.getLogger(__name__) -CODEOWNERS = ["@tomaszduda23"] +CODEOWNERS = ["@luar123", "@tomaszduda23"] def zigbee_set_core_data(config: ConfigType) -> ConfigType: - if zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: + if CORE.is_nrf52 and zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: zephyr_add_pm_static( [Section("empty_after_zboss_offset", 0xF4000, 0xC000, "flash_primary")] ) @@ -45,7 +61,15 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType: return config -BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor) +BINARY_SENSOR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REPORT): cv.All( + cv.requires_component("zigbee"), + cv.requires_component("esp32"), + cv.enum(REPORT, lower=True), + ) + } +).extend(zephyr_binary_sensor) SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number) @@ -54,16 +78,27 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(ZigbeeComponent), - cv.Optional(CONF_ON_JOIN): automation.validate_automation(single=True), - cv.Optional(CONF_WIPE_ON_BOOT, default=False): cv.All( + cv.Optional(CONF_MODEL, default=CORE.name): cv.All( + cv.string, cv.Length(max=31) + ), + cv.OnlyWith(CONF_ROUTER, "esp32", default=False): cv.All( + cv.requires_component("esp32"), + cv.boolean, + ), + cv.Optional(CONF_ON_JOIN): cv.All( + cv.requires_component("nrf52"), + automation.validate_automation(single=True), + ), + cv.OnlyWith(CONF_WIPE_ON_BOOT, "nrf52", default=False): cv.All( cv.Any( cv.boolean, cv.one_of(*["once"], lower=True), ), cv.requires_component("nrf52"), ), - cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum( - POWER_SOURCE, upper=True + cv.OnlyWith(CONF_POWER_SOURCE, "nrf52", default="DC_SOURCE"): cv.All( + cv.enum(POWER_SOURCE, upper=True), + cv.requires_component("nrf52"), ), cv.Optional(CONF_IEEE802154_VENDOR_OUI): cv.All( cv.Any( @@ -74,12 +109,27 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + zigbee_require_vfs_select, zigbee_set_core_data, - cv.only_with_framework("zephyr"), + cv.Any( + cv.All( + cv.only_on_esp32, + only_on_variant( + supported=[ + VARIANT_ESP32H2, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + ] + ), + ), + cv.only_with_framework("zephyr"), + ), ) -def validate_number_of_ep(config: ConfigType) -> None: +def validate_number_of_ep(config: ConfigType) -> ConfigType: + if not CORE.is_nrf52: + return config if KEY_ZIGBEE not in CORE.data: raise cv.Invalid("At least one zigbee device need to be included") count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) @@ -90,9 +140,12 @@ def validate_number_of_ep(config: ConfigType) -> None: if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") + return config + FINAL_VALIDATE_SCHEMA = cv.All( validate_number_of_ep, + final_validate_esp32, ) @@ -103,6 +156,10 @@ async def to_code(config: ConfigType) -> None: from .zigbee_zephyr import zephyr_to_code await zephyr_to_code(config) + if CORE.is_esp32: + from .zigbee_esp32 import esp32_to_code + + await esp32_to_code(config) async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: @@ -148,7 +205,7 @@ async def setup_number( def consume_endpoint(config: ConfigType) -> ConfigType: - if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + if not config.get(CONF_ZIGBEE_ID): return config if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( @@ -163,18 +220,34 @@ def consume_endpoint(config: ConfigType) -> ConfigType: def validate_binary_sensor(config: ConfigType) -> ConfigType: + if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + return config + if CORE.is_esp32: + return validate_binary_sensor_esp32(config) return consume_endpoint(config) def validate_sensor(config: ConfigType) -> ConfigType: + if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + return config + if CORE.is_esp32: + return config return consume_endpoint(config) def validate_switch(config: ConfigType) -> ConfigType: + if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + return config + if CORE.is_esp32: + return config return consume_endpoint(config) def validate_number(config: ConfigType) -> ConfigType: + if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): + return config + if CORE.is_esp32: + return config return consume_endpoint(config) diff --git a/esphome/components/zigbee/automation.h b/esphome/components/zigbee/automation.h index 1822e6a0296..55ee9746ea3 100644 --- a/esphome/components/zigbee/automation.h +++ b/esphome/components/zigbee/automation.h @@ -1,6 +1,9 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_ZIGBEE +#ifdef USE_ESP32 +#include "zigbee_esp32.h" +#endif #ifdef USE_NRF52 #include "zigbee_zephyr.h" #endif diff --git a/esphome/components/zigbee/const.py b/esphome/components/zigbee/const.py new file mode 100644 index 00000000000..26ae2cc0ec4 --- /dev/null +++ b/esphome/components/zigbee/const.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg + +zigbee_ns = cg.esphome_ns.namespace("zigbee") +ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) +ZigbeeAttribute = zigbee_ns.class_("ZigbeeAttribute", cg.Component) +BinaryAttrs = zigbee_ns.struct("BinaryAttrs") +AnalogAttrs = zigbee_ns.struct("AnalogAttrs") +AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput") + +report = zigbee_ns.enum("ZigbeeReportT") +REPORT = { + "coordinator": report.ZIGBEE_REPORT_COORDINATOR, + "enable": report.ZIGBEE_REPORT_ENABLE, + "force": report.ZIGBEE_REPORT_FORCE, +} + +CONF_ON_JOIN = "on_join" +CONF_WIPE_ON_BOOT = "wipe_on_boot" +CONF_REPORT = "report" +CONF_ROUTER = "router" +CONF_POWER_SOURCE = "power_source" +POWER_SOURCE = { + "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", + "MAINS_SINGLE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE", + "MAINS_THREE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_THREE_PHASE", + "BATTERY": "ZB_ZCL_BASIC_POWER_SOURCE_BATTERY", + "DC_SOURCE": "ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE", + "EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST", + "EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF", +} + +KEY_ZIGBEE = "zigbee" diff --git a/esphome/components/zigbee/const_esp32.py b/esphome/components/zigbee/const_esp32.py new file mode 100644 index 00000000000..682638439e9 --- /dev/null +++ b/esphome/components/zigbee/const_esp32.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg + +DEVICE_TYPE = "device_type" +ROLE = "role" +CONF_MAX_EP_NUMBER = 239 +CONF_NUM = "num" +CONF_CLUSTERS = "clusters" +CONF_ATTRIBUTES = "attributes" +CONF_ENDPOINT = "endpoint" +CONF_CLUSTER = "cluster" +SCALE = "scale" +CONF_ATTRIBUTE_ID = "attribute_id" +KEY_BS_EP = "binary_sensor_ep" + +ha_standard_devices = cg.esphome_ns.enum("zb_ha_standard_devs_e") +DEVICE_ID = { + "RANGE_EXTENDER": ha_standard_devices.ZB_HA_RANGE_EXTENDER_DEVICE_ID, + "SIMPLE_SENSOR": ha_standard_devices.ZB_HA_SIMPLE_SENSOR_DEVICE_ID, + "CUSTOM_ATTR": ha_standard_devices.ZB_HA_CUSTOM_ATTR_DEVICE_ID, +} +cluster_id = cg.esphome_ns.enum("esp_zb_zcl_cluster_id_t") +CLUSTER_ID = { + "BASIC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC, + "BINARY_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT, +} +cluster_role = cg.esphome_ns.enum("esp_zb_zcl_cluster_role_t") +CLUSTER_ROLE = { + "SERVER": cluster_role.ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, +} +attr_type = cg.esphome_ns.enum("esp_zb_zcl_attr_type_t") +ATTR_TYPE = { + "BOOL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BOOL, + "8BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BITMAP, + "CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING, +} diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 2d233755acc..103ef01a3d8 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -1,33 +1,12 @@ -import esphome.codegen as cg - -zigbee_ns = cg.esphome_ns.namespace("zigbee") -ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) -BinaryAttrs = zigbee_ns.struct("BinaryAttrs") -AnalogAttrs = zigbee_ns.struct("AnalogAttrs") -AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput") - CONF_MAX_EP_NUMBER = 8 CONF_ZIGBEE_ID = "zigbee_id" -CONF_ON_JOIN = "on_join" -CONF_WIPE_ON_BOOT = "wipe_on_boot" CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SWITCH = "zigbee_switch" CONF_ZIGBEE_NUMBER = "zigbee_number" -CONF_POWER_SOURCE = "power_source" -POWER_SOURCE = { - "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", - "MAINS_SINGLE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE", - "MAINS_THREE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_THREE_PHASE", - "BATTERY": "ZB_ZCL_BASIC_POWER_SOURCE_BATTERY", - "DC_SOURCE": "ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE", - "EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST", - "EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF", -} CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui" # Keys for CORE.data storage -KEY_ZIGBEE = "zigbee" KEY_EP_NUMBER = "ep_number" # External ZBOSS SDK types (just strings for codegen) diff --git a/esphome/components/zigbee/time/__init__.py b/esphome/components/zigbee/time/__init__.py index 82f94c83721..3acab0076f9 100644 --- a/esphome/components/zigbee/time/__init__.py +++ b/esphome/components/zigbee/time/__init__.py @@ -6,7 +6,8 @@ from esphome.core import CORE from esphome.types import ConfigType from .. import consume_endpoint -from ..const_zephyr import CONF_ZIGBEE_ID, zigbee_ns +from ..const import zigbee_ns +from ..const_zephyr import CONF_ZIGBEE_ID from ..zigbee_zephyr import ( ZigbeeClusterDesc, ZigbeeComponent, diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.cpp b/esphome/components/zigbee/zigbee_attribute_esp32.cpp new file mode 100644 index 00000000000..4d736001715 --- /dev/null +++ b/esphome/components/zigbee/zigbee_attribute_esp32.cpp @@ -0,0 +1,89 @@ +#include "zigbee_attribute_esp32.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.attribute"; + +void ZigbeeAttribute::set_attr_() { + if (!this->zb_->is_connected()) { + return; + } + if (esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) { + esp_zb_zcl_status_t state = esp_zb_zcl_set_attribute_val(this->endpoint_id_, this->cluster_id_, this->role_, + this->attr_id_, this->value_p_, false); + if (this->force_report_) { + this->report_(true); + } + this->set_attr_requested_ = false; + // Check for error + if (state != ESP_ZB_ZCL_STATUS_SUCCESS) { + ESP_LOGE(TAG, "Setting attribute failed, ZCL status: %u", static_cast<unsigned>(state)); + } + esp_zb_lock_release(); + } +} + +void ZigbeeAttribute::report_(bool has_lock) { + if (!this->zb_->is_connected()) { + return; + } + if (has_lock or esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) { + esp_zb_zcl_report_attr_cmd_t cmd = { + .address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT, + .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI, + }; + cmd.zcl_basic_cmd.dst_addr_u.addr_short = 0x0000; + cmd.zcl_basic_cmd.dst_endpoint = 1; + cmd.zcl_basic_cmd.src_endpoint = this->endpoint_id_; + cmd.clusterID = this->cluster_id_; + cmd.attributeID = this->attr_id_; + + esp_zb_zcl_report_attr_cmd_req(&cmd); + if (!has_lock) { + esp_zb_lock_release(); + } + } +} + +esp_zb_zcl_reporting_info_t ZigbeeAttribute::get_reporting_info() { + esp_zb_zcl_reporting_info_t reporting_info = { + .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV, + .ep = this->endpoint_id_, + .cluster_id = this->cluster_id_, + .cluster_role = this->role_, + .attr_id = this->attr_id_, + .manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC, + }; + reporting_info.dst.profile_id = ESP_ZB_AF_HA_PROFILE_ID; + reporting_info.u.send_info.min_interval = 10; /*!< Actual minimum reporting interval */ + reporting_info.u.send_info.max_interval = 0; /*!< Actual maximum reporting interval */ + reporting_info.u.send_info.def_min_interval = 10; /*!< Default minimum reporting interval */ + reporting_info.u.send_info.def_max_interval = 0; /*!< Default maximum reporting interval */ + reporting_info.u.send_info.delta.s16 = 0; /*!< Actual reportable change */ + + return reporting_info; +} + +void ZigbeeAttribute::set_report(bool force) { + this->report_enabled = true; + this->force_report_ = force; +} + +void ZigbeeAttribute::loop() { + if (this->set_attr_requested_) { + this->set_attr_(); + } + + if (!this->set_attr_requested_) { + this->disable_loop(); + } +} + +} // namespace esphome::zigbee + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.h b/esphome/components/zigbee/zigbee_attribute_esp32.h new file mode 100644 index 00000000000..5a0cfc4fbd7 --- /dev/null +++ b/esphome/components/zigbee/zigbee_attribute_esp32.h @@ -0,0 +1,90 @@ +#pragma once + +#include <type_traits> + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +#include "esp_zigbee_core.h" +#include "zigbee_esp32.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome::zigbee { + +enum ZigbeeReportT { + ZIGBEE_REPORT_COORDINATOR, + ZIGBEE_REPORT_ENABLE, + ZIGBEE_REPORT_FORCE, +}; + +class ZigbeeAttribute : public Component { + public: + ZigbeeAttribute(ZigbeeComponent *parent, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, + uint8_t attr_type, float scale, uint8_t max_size) + : zb_(parent), + endpoint_id_(endpoint_id), + cluster_id_(cluster_id), + role_(role), + attr_id_(attr_id), + attr_type_(attr_type), + scale_(scale), + max_size_(max_size) {} + void loop() override; + template<typename T> void add_attr(T value); + esp_zb_zcl_reporting_info_t get_reporting_info(); + template<typename T> void set_attr(const T &value); + uint8_t attr_type() { return attr_type_; } + void set_report(bool force); +#ifdef USE_BINARY_SENSOR + template<typename T> void connect(binary_sensor::BinarySensor *sensor); +#endif + bool report_enabled = false; + + protected: + void set_attr_(); + void report_(bool has_lock); + ZigbeeComponent *zb_; + uint8_t endpoint_id_; + uint16_t cluster_id_; + uint8_t role_; + uint16_t attr_id_; + uint8_t attr_type_; + uint8_t max_size_; + float scale_; + void *value_p_{nullptr}; + bool set_attr_requested_{false}; + bool force_report_{false}; +}; + +template<typename T> void ZigbeeAttribute::add_attr(T value) { + // Attribute type does never change and add_attr is only called once during startup, so this is safe. + // For now we need to support only simple numeric/bool types for (binary) sensors. + // For strings and arrays we would need to allocate a buffer of the maximum size. + this->value_p_ = (void *) (new T); + this->zb_->add_attr(this, this->endpoint_id_, this->cluster_id_, this->role_, this->attr_id_, this->max_size_, + std::move(value)); +} + +template<typename T> void ZigbeeAttribute::set_attr(const T &value) { + *static_cast<T *>(this->value_p_) = value; + this->set_attr_requested_ = true; + this->enable_loop(); +} + +#ifdef USE_BINARY_SENSOR +template<typename T> void ZigbeeAttribute::connect(binary_sensor::BinarySensor *sensor) { + sensor->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); }); +} +#endif + +} // namespace esphome::zigbee + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_ep_esp32.py b/esphome/components/zigbee/zigbee_ep_esp32.py new file mode 100644 index 00000000000..791232d4633 --- /dev/null +++ b/esphome/components/zigbee/zigbee_ep_esp32.py @@ -0,0 +1,70 @@ +from typing import Any + +import esphome.config_validation as cv +from esphome.const import CONF_DEVICE, CONF_ID, CONF_TYPE + +from .const import CONF_REPORT, REPORT +from .const_esp32 import ( + CLUSTER_ROLE, + CONF_ATTRIBUTE_ID, + CONF_ATTRIBUTES, + CONF_CLUSTERS, + CONF_MAX_EP_NUMBER, + CONF_NUM, + DEVICE_TYPE, + ROLE, +) + +# endpoint configs: +ep_configs: dict[str, dict[str, Any]] = { + "binary_input": { + DEVICE_TYPE: "SIMPLE_SENSOR", + CONF_CLUSTERS: [ + { + CONF_ID: "BINARY_INPUT", + ROLE: CLUSTER_ROLE["SERVER"], + CONF_ATTRIBUTES: [ + { + CONF_ATTRIBUTE_ID: 0x55, + CONF_TYPE: "BOOL", + CONF_REPORT: REPORT["enable"], + CONF_DEVICE: None, + }, + { + CONF_ATTRIBUTE_ID: 0x51, + CONF_TYPE: "BOOL", + }, + { + CONF_ATTRIBUTE_ID: 0x6F, + CONF_TYPE: "8BITMAP", + }, + { + CONF_ATTRIBUTE_ID: 0x1C, + CONF_TYPE: "CHAR_STRING", + }, + ], + }, + ], + }, +} + + +def create_ep(ep_list: list[dict[str, Any]], router: bool) -> list[dict[str, Any]]: + # create dummy endpoint if list is empty + if not ep_list: + ep_type = "CUSTOM_ATTR" + if router: + ep_type = "RANGE_EXTENDER" + ep_list = [ + { + DEVICE_TYPE: ep_type, + } + ] + # enumerate endpoints + for i, ep in enumerate(ep_list, 1): + ep[CONF_NUM] = i + if len(ep_list) > CONF_MAX_EP_NUMBER: + raise cv.Invalid( + f"Too many devices. Zigbee can define only {CONF_MAX_EP_NUMBER} endpoints." + ) + return ep_list diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp new file mode 100644 index 00000000000..c16736236a4 --- /dev/null +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -0,0 +1,313 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_check.h" +#include "nvs_flash.h" +#include "zigbee_attribute_esp32.h" +#include "zigbee_esp32.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "zigbee_helpers_esp32.h" +#ifdef USE_WIFI +#include "esp_coexist.h" +#endif + +namespace esphome::zigbee { + +static const char *const TAG = "zigbee"; + +static ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size) { + uint8_t str_len = static_cast<uint8_t>(strlen(str)); + uint8_t zcl_str_size = use_max_size ? max_size : std::min(max_size, str_len); + uint8_t *zcl_str = new uint8_t[zcl_str_size + 1]; // string + length octet + zcl_str[0] = zcl_str_size; + + // Initialize payload to avoid leaking uninitialized heap contents and clamp copy length + memset(zcl_str + 1, 0, zcl_str_size); + uint8_t copy_len = std::min(zcl_str_size, str_len); + if (copy_len > 0) { + memcpy(zcl_str + 1, str, copy_len); + } + return zcl_str; +} + +static void bdb_start_top_level_commissioning_cb(uint8_t mode_mask) { + if (esp_zb_bdb_start_top_level_commissioning(mode_mask) != ESP_OK) { + ESP_LOGE(TAG, "Start network steering failed!"); + } +} + +void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) { + static uint8_t steering_retry_count = 0; + uint32_t *p_sg_p = signal_struct->p_app_signal; + esp_err_t err_status = signal_struct->esp_err_status; + esp_zb_app_signal_type_t sig_type = (esp_zb_app_signal_type_t) *p_sg_p; + esp_zb_zdo_signal_leave_params_t *leave_params = NULL; + switch (sig_type) { + case ESP_ZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGD(TAG, "Zigbee stack initialized"); + esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_INITIALIZATION); + break; + case ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START: + case ESP_ZB_BDB_SIGNAL_DEVICE_REBOOT: + if (err_status == ESP_OK) { + ESP_LOGD(TAG, "Device started up in %sfactory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non "); + global_zigbee->started = true; + if (esp_zb_bdb_is_factory_new()) { + ESP_LOGD(TAG, "Start network steering"); + esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); + } else { + ESP_LOGD(TAG, "Device rebooted"); + global_zigbee->connected = true; + } + } else { + ESP_LOGE(TAG, "FIRST_START. Device started up in %sfactory-reset mode with an error %d (%s)", + esp_zb_bdb_is_factory_new() ? "" : "non ", err_status, esp_err_to_name(err_status)); + ESP_LOGW(TAG, "Failed to initialize Zigbee stack (status: %s)", esp_err_to_name(err_status)); + esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, ESP_ZB_BDB_MODE_INITIALIZATION, + 1000); + } + break; + case ESP_ZB_BDB_SIGNAL_STEERING: + if (err_status == ESP_OK) { + steering_retry_count = 0; + ESP_LOGI(TAG, "Joined network successfully (PAN ID: 0x%04hx, Channel:%d)", esp_zb_get_pan_id(), + esp_zb_get_current_channel()); + global_zigbee->connected = true; + } else { + ESP_LOGI(TAG, "Network steering was not successful (status: %s)", esp_err_to_name(err_status)); + if (steering_retry_count < 10) { + steering_retry_count++; + esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, + ESP_ZB_BDB_MODE_NETWORK_STEERING, 1000); + } else { + esp_zb_scheduler_alarm((esp_zb_callback_t) bdb_start_top_level_commissioning_cb, + ESP_ZB_BDB_MODE_NETWORK_STEERING, 600 * 1000); + } + } + break; + case ESP_ZB_ZDO_SIGNAL_LEAVE: + leave_params = (esp_zb_zdo_signal_leave_params_t *) esp_zb_app_signal_get_params(p_sg_p); + if (leave_params->leave_type == ESP_ZB_NWK_LEAVE_TYPE_RESET) { + esp_zb_factory_reset(); + } + break; + default: + ESP_LOGD(TAG, "ZDO signal: %s (0x%x), status: %s", esp_zb_zdo_signal_to_string(sig_type), sig_type, + esp_err_to_name(err_status)); + break; + } +} + +static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message) { + esp_err_t ret = ESP_OK; + ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); + ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, + "Received message: error status(%d)", message->info.status); + ESP_LOGD(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)", + message->info.dst_endpoint, message->info.cluster, message->attribute.id, message->attribute.data.size); + return ret; +} + +static esp_err_t zb_action_handler(esp_zb_core_action_callback_id_t callback_id, const void *message) { + esp_err_t ret = ESP_OK; + switch (callback_id) { + case ESP_ZB_CORE_SET_ATTR_VALUE_CB_ID: + ret = zb_attribute_handler((esp_zb_zcl_set_attr_value_message_t *) message); + break; + default: + ESP_LOGD(TAG, "Receive Zigbee action(0x%x) callback", callback_id); + break; + } + return ret; +} + +void ZigbeeComponent::create_default_cluster(uint8_t endpoint_id, zb_ha_standard_devs_e device_id) { + esp_zb_cluster_list_t *cluster_list = esp_zb_zcl_cluster_list_create(); + this->endpoint_list_[endpoint_id] = + std::tuple<zb_ha_standard_devs_e, esp_zb_cluster_list_t *>(device_id, cluster_list); + // Add basic cluster + this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_BASIC, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + // Add identify cluster if not already present + if (esp_zb_cluster_list_get_cluster(cluster_list, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE) == + nullptr) { + this->add_cluster(endpoint_id, ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); + } +} + +void ZigbeeComponent::add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role) { + esp_zb_attribute_list_t *attr_list; + if (cluster_id == 0) { + attr_list = create_basic_cluster_(); + } else { + attr_list = esphome_zb_default_attr_list_create(cluster_id); + } + this->attribute_list_[{endpoint_id, cluster_id, role}] = attr_list; +} + +void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer) { + char date_buf[16]; + time_t time_val = App.get_build_time(); + struct tm *timeinfo = localtime(&time_val); + strftime(date_buf, sizeof(date_buf), "%Y%m%d %H%M%S", timeinfo); + this->basic_cluster_data_ = { + .model = get_zcl_string(model, 31), + .manufacturer = get_zcl_string(manufacturer, 31), + .date = get_zcl_string(date_buf, 15), + }; +} + +esp_zb_attribute_list_t *ZigbeeComponent::create_basic_cluster_() { + esp_zb_basic_cluster_cfg_t basic_cluster_cfg = { + .zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE, + .power_source = 0, + }; + esp_zb_attribute_list_t *attr_list = esp_zb_basic_cluster_create(&basic_cluster_cfg); + esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, + this->basic_cluster_data_.manufacturer); + esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, this->basic_cluster_data_.model); + esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_DATE_CODE_ID, this->basic_cluster_data_.date); + return attr_list; +} + +esp_err_t ZigbeeComponent::create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id, + esp_zb_cluster_list_t *esp_zb_cluster_list) { + esp_zb_endpoint_config_t endpoint_config = {.endpoint = endpoint_id, + .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID, + .app_device_id = device_id, + .app_device_version = 0}; + return esp_zb_ep_list_add_ep(this->esp_zb_ep_list_, esp_zb_cluster_list, endpoint_config); +} + +static void esp_zb_task_(void *pvParameters) { + if (esp_zb_start(false) != ESP_OK) { + ESP_LOGE(TAG, "Could not setup Zigbee"); + vTaskDelete(NULL); + } + esp_zb_set_node_descriptor_power_source(1); + esp_zb_stack_main_loop(); +} + +void ZigbeeComponent::setup() { + global_zigbee = this; + esp_zb_platform_config_t config = { + .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), + .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), + }; +#ifdef USE_WIFI + if (esp_coex_wifi_i154_enable() != ESP_OK) { + this->mark_failed(); + return; + } +#endif + if (esp_zb_platform_config(&config) != ESP_OK) { + this->mark_failed(); + return; + } + + esp_zb_zed_cfg_t zb_zed_cfg = { + .ed_timeout = ESP_ZB_ED_AGING_TIMEOUT_64MIN, + .keep_alive = ED_KEEP_ALIVE, + }; + esp_zb_zczr_cfg_t zb_zczr_cfg = { + .max_children = MAX_CHILDREN, + }; + esp_zb_cfg_t zb_nwk_cfg = { + .esp_zb_role = this->device_role_, + .install_code_policy = false, + }; +#ifdef ZB_ROUTER_ROLE + zb_nwk_cfg.nwk_cfg.zczr_cfg = zb_zczr_cfg; +#else + zb_nwk_cfg.nwk_cfg.zed_cfg = zb_zed_cfg; +#endif + esp_zb_init(&zb_nwk_cfg); + + esp_err_t ret; + for (auto const &[key, val] : this->attribute_list_) { + esp_zb_cluster_list_t *esp_zb_cluster_list = std::get<1>(this->endpoint_list_[std::get<0>(key)]); + ret = esphome_zb_cluster_list_add_or_update_cluster(std::get<1>(key), esp_zb_cluster_list, val, std::get<2>(key)); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Could not create cluster 0x%04X with role %u: %s", std::get<1>(key), std::get<2>(key), + esp_err_to_name(ret)); + } else { + ESP_LOGD(TAG, "Endpoint %u: Added cluster 0x%04X with role %u", std::get<0>(key), std::get<1>(key), + std::get<2>(key)); +#ifdef ESPHOME_LOG_HAS_VERBOSE + // Dump cluster attributes in verbose log + ESP_LOGV(TAG, "Cluster 0x%04X attributes:", std::get<1>(key)); + esp_zb_attribute_list_t *attr_list = val; + while (attr_list) { + esp_zb_zcl_attr_t *attr = &attr_list->attribute; + ESP_LOGV(TAG, " Attr ID: 0x%04X, Type: 0x%02X, Access: 0x%02X", attr->id, attr->type, attr->access); + attr_list = attr_list->next; + } +#endif + } + } + this->attribute_list_.clear(); + + for (auto const &[ep_id, dev_id] : this->endpoint_list_) { + if (create_endpoint(ep_id, std::get<0>(dev_id), std::get<1>(dev_id)) != ESP_OK) { + ESP_LOGE(TAG, "Could not create endpoint %u", ep_id); + } + } + this->endpoint_list_.clear(); + + if (esp_zb_device_register(this->esp_zb_ep_list_) != ESP_OK) { + ESP_LOGE(TAG, "Could not register the endpoint list"); + this->mark_failed(); + return; + } + + esp_zb_core_action_handler_register(zb_action_handler); + + if (esp_zb_set_primary_network_channel_set(ESP_ZB_TRANSCEIVER_ALL_CHANNELS_MASK) != ESP_OK) { + ESP_LOGE(TAG, "Could not setup Zigbee"); + this->mark_failed(); + return; + } + for (auto &[_, attribute] : this->attributes_) { + if (attribute->report_enabled) { + esp_zb_zcl_reporting_info_t reporting_info = attribute->get_reporting_info(); + ESP_LOGD(TAG, "set reporting for cluster: %u", reporting_info.cluster_id); + if (esp_zb_zcl_update_reporting_info(&reporting_info) != ESP_OK) { + ESP_LOGE(TAG, "Could not configure reporting for attribute 0x%04X in cluster 0x%04X in endpoint %u", + reporting_info.attr_id, reporting_info.cluster_id, reporting_info.ep); + } + } + } + xTaskCreate(esp_zb_task_, "Zigbee_main", 4096, NULL, 24, NULL); +} + +void ZigbeeComponent::dump_config() { + if (esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) { + ESP_LOGCONFIG(TAG, + "Zigbee\n" + " Model: %s\n" + " Router: %s\n" + " Device is joined to the network: %s\n" + " Current channel: %d\n" + " Short addr: 0x%04X\n" + " Short pan id: 0x%04X", + this->basic_cluster_data_.model, YESNO(this->device_role_ == ESP_ZB_DEVICE_TYPE_ROUTER), + YESNO(esp_zb_bdb_dev_joined()), esp_zb_get_current_channel(), esp_zb_get_short_address(), + esp_zb_get_pan_id()); + esp_zb_lock_release(); + } else { + ESP_LOGCONFIG(TAG, + "Zigbee\n" + " Model: %s\n" + " Router: %s\n", + this->basic_cluster_data_.model, YESNO(this->device_role_ == ESP_ZB_DEVICE_TYPE_ROUTER)); + } +} +} // namespace esphome::zigbee + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h new file mode 100644 index 00000000000..80ecbfd639e --- /dev/null +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -0,0 +1,134 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +#include <map> +#include <tuple> +#include <atomic> + +#include "esp_zigbee_core.h" +#include "zboss_api.h" +#include "ha/esp_zigbee_ha_standard.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "zigbee_helpers_esp32.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome::zigbee { + +/* Zigbee configuration */ +static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */ +static const uint8_t MAX_CHILDREN = 10; + +#define ESP_ZB_DEFAULT_RADIO_CONFIG() \ + { .radio_mode = ZB_RADIO_MODE_NATIVE, } + +#define ESP_ZB_DEFAULT_HOST_CONFIG() \ + { .host_connection_mode = ZB_HOST_CONNECTION_MODE_NONE, } + +uint8_t *get_zcl_string(const char *str, uint8_t max_size, bool use_max_size = false); + +class ZigbeeAttribute; + +class ZigbeeComponent : public Component { + public: + void setup() override; + void dump_config() override; + esp_err_t create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id, + esp_zb_cluster_list_t *esp_zb_cluster_list); + void set_basic_cluster(const char *model, const char *manufacturer); + void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role); + void create_default_cluster(uint8_t endpoint_id, zb_ha_standard_devs_e device_id); + + template<typename T> + void add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, + uint8_t max_size, T value); + + template<typename T> + void add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, uint8_t max_size, T value); + + void factory_reset() { + esp_zb_lock_acquire(portMAX_DELAY); + esp_zb_factory_reset(); // triggers a reboot + esp_zb_lock_release(); + } + + bool is_started() { return this->started; } + bool is_connected() { return this->connected; } + std::atomic<bool> connected = false; + std::atomic<bool> started = false; + + protected: + struct { + uint8_t *model; + uint8_t *manufacturer; + uint8_t *date; + } basic_cluster_data_; +#ifdef ZB_ED_ROLE + esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ED; +#else + esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ROUTER; +#endif + esp_zb_attribute_list_t *create_basic_cluster_(); + template<typename T> + void add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, + T *value_p); + // endpoint_list_ and attribute_list_ are only used during setup and are cleared afterwards + // value tuple could be replaced by struct + std::map<uint8_t, std::tuple<zb_ha_standard_devs_e, esp_zb_cluster_list_t *>> endpoint_list_; + // key tuple could be replaced by single 32 bit int with bit fields for endpoint, cluster and role + std::map<std::tuple<uint8_t, uint16_t, uint8_t>, esp_zb_attribute_list_t *> attribute_list_; + // attributes_ will be used during operation in zigbee callbacks to update the attribute values and trigger + // automations + // key tuple could be replaced by single 64 (48) bit int with bit fields for endpoint, cluster, role and attr_id + std::map<std::tuple<uint8_t, uint16_t, uint8_t, uint16_t>, ZigbeeAttribute *> attributes_; + esp_zb_ep_list_t *esp_zb_ep_list_ = esp_zb_ep_list_create(); +}; + +extern "C" void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct); + +template<typename T> +void ZigbeeComponent::add_attr(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id, + uint8_t max_size, T value) { + this->add_attr<T>(nullptr, endpoint_id, cluster_id, role, attr_id, max_size, value); +} + +template<typename T> +void ZigbeeComponent::add_attr(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, + uint16_t attr_id, uint8_t max_size, T value) { + // The size byte of the zcl_str must be set to the maximum value, + // even though the initial string may be shorter. + if constexpr (std::is_same<T, std::string>::value) { + auto zcl_str = get_zcl_string(value.c_str(), max_size, true); + add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str); + delete[] zcl_str; + } else if constexpr (std::is_convertible<T, const char *>::value) { + auto zcl_str = get_zcl_string(value, max_size, true); + add_attr_(attr, endpoint_id, cluster_id, role, attr_id, zcl_str); + delete[] zcl_str; + } else { + add_attr_(attr, endpoint_id, cluster_id, role, attr_id, &value); + } +} + +template<typename T> +void ZigbeeComponent::add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, + uint16_t attr_id, T *value_p) { + esp_zb_attribute_list_t *attr_list = this->attribute_list_[{endpoint_id, cluster_id, role}]; + esp_err_t ret = esphome_zb_cluster_add_or_update_attr(cluster_id, attr_list, attr_id, value_p); + + if (attr != nullptr) { + this->attributes_[{endpoint_id, cluster_id, role, attr_id}] = attr; + } +} + +} // namespace esphome::zigbee + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py new file mode 100644 index 00000000000..1b98df6c0a5 --- /dev/null +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -0,0 +1,274 @@ +import copy +import logging +import re +from typing import Any + +import esphome.codegen as cg +from esphome.components.esp32 import ( + CONF_PARTITIONS, + add_idf_component, + add_idf_sdkconfig_option, + add_partition, + require_vfs_select, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_AP, + CONF_DEVICE, + CONF_ID, + CONF_MAX_LENGTH, + CONF_MODEL, + CONF_NAME, + CONF_TYPE, + CONF_VALUE, + CONF_WIFI, +) +from esphome.core import CORE +from esphome.coroutine import CoroPriority, coroutine_with_priority +import esphome.final_validate as fv +from esphome.types import ConfigType + +from .const import CONF_REPORT, CONF_ROUTER, KEY_ZIGBEE, REPORT, ZigbeeAttribute +from .const_esp32 import ( + ATTR_TYPE, + CLUSTER_ID, + CONF_ATTRIBUTE_ID, + CONF_ATTRIBUTES, + CONF_CLUSTERS, + CONF_NUM, + DEVICE_ID, + DEVICE_TYPE, + KEY_BS_EP, + ROLE, + SCALE, +) +from .zigbee_ep_esp32 import create_ep, ep_configs + +_LOGGER = logging.getLogger(__name__) + + +def get_c_size(bits: str, options: list[int]) -> str: + return str([n for n in options if n >= int(bits)][0]) + + +def get_c_type(attr_type: str) -> Any | None: + if attr_type == "BOOL": + return cg.bool_ + if "STRING" in attr_type: + return cg.std_string + test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) + if test and test.group(2): + return getattr(cg, "uint" + get_c_size(test.group(2), [8, 16, 32, 64])) + return None + + +def get_cv_by_type(attr_type: str) -> Any | None: + if attr_type == "BOOL": + return cv.boolean + if "STRING" in attr_type: + return cv.string + test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) + if test and test.group(2): + return cv.positive_int + return None + + +def get_default_by_type(attr_type: str) -> str | bool | int: + if attr_type == "CHAR_STRING": + return "" + if attr_type == "BOOL": + return False + return 0 + + +def validate_attributes(config: ConfigType) -> ConfigType: + if CONF_VALUE not in config: + config[CONF_VALUE] = get_default_by_type(config[CONF_TYPE]) + config[CONF_VALUE] = get_cv_by_type(config[CONF_TYPE])(config[CONF_VALUE]) + + return config + + +def final_validate_esp32(config: ConfigType) -> ConfigType: + if not CORE.is_esp32: + return config + if CONF_WIFI in fv.full_config.get(): + if config[CONF_ROUTER] and CONF_AP in fv.full_config.get()[CONF_WIFI]: + raise cv.Invalid( + "Only Zigbee End Device can be used together with a Wifi Access Point." + ) + if CONF_AP in fv.full_config.get()[CONF_WIFI]: + _LOGGER.warning( + "Wifi Access Point might be unstable while Zigbee is active, use only as fallback." + ) + elif config[CONF_ROUTER]: + _LOGGER.warning( + "The Zigbee Router might miss packets while Wifi is active and could destabilize " + "your network. Use only if Wifi is off most of the time." + ) + if CONF_PARTITIONS in fv.full_config.get() and not isinstance( + fv.full_config.get()[CONF_PARTITIONS], list + ): + with open( + CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]), + encoding="utf8", + ) as f: + partitions_tab = f.read() + for partition, types in [ + ("zb_storage", {"type": "data", "subtype": "fat", "size": 0x4000}), + ("zb_fct", {"type": "data", "subtype": "fat", "size": 0x1000}), + ]: + if partition not in partitions_tab: + raise cv.Invalid( + f"Add '{partition}, {types['type']}, {types['subtype']}, , {types['size']},' to your custom partition table." + ) + if not re.search( + rf"^{partition},\s*{types['type']},\s*{types['subtype']}", + partitions_tab, + re.MULTILINE, + ): + raise cv.Invalid( + f"Partition '{partition}' in your custom partition table has wrong format. It should be: '{partition}, {types['type']}, {types['subtype']}, , {types['size']},'" + ) + return config + + +def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType: + ep = copy.deepcopy(ep_configs["binary_input"]) + for cl in ep.get(CONF_CLUSTERS, []): + for attr in cl[CONF_ATTRIBUTES]: + if ( + attr[CONF_ATTRIBUTE_ID] == 0x1C + and CONF_VALUE not in attr + and CONF_NAME in config + ): # set name + name = ( + config[CONF_NAME].encode("ascii", "ignore").decode() + ) # or use unidecode + attr[CONF_VALUE] = str(name) + attr[CONF_MAX_LENGTH] = len(str(name)) + if CONF_DEVICE in attr: # connect device + attr[CONF_DEVICE] = config[CONF_ID] + if CONF_REPORT in config: + attr[CONF_REPORT] = config[CONF_REPORT] + attr[CONF_ID] = cv.declare_id(ZigbeeAttribute)(None) + if "zb_attr_ids" not in config: + config["zb_attr_ids"] = [] + config["zb_attr_ids"].append(attr[CONF_ID]) + else: + attr[CONF_ID] = None + validate_attributes(attr) + zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) + binary_sensor_ep: list[dict] = zb_data.setdefault(KEY_BS_EP, []) + binary_sensor_ep.append(ep) + return config + + +def zigbee_require_vfs_select(config: ConfigType) -> ConfigType: + """Register VFS select requirement during config validation.""" + # Zigbee uses esp_vfs_eventfd which requires VFS select support + if CORE.is_esp32: + require_vfs_select() + return config + + +@coroutine_with_priority(CoroPriority.WORKAROUNDS) +async def _zigbee_add_sdkconfigs(config: ConfigType) -> None: + """Add sdkconfigs late so they can overwrite esp32 defaults""" + add_idf_sdkconfig_option("CONFIG_ZB_ENABLED", True) + if config.get(CONF_ROUTER): + add_idf_sdkconfig_option("CONFIG_ZB_ZCZR", True) + else: + add_idf_sdkconfig_option("CONFIG_ZB_ZED", True) + add_idf_sdkconfig_option("CONFIG_ZB_RADIO_NATIVE", True) + if CONF_WIFI in CORE.config: + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE", 4096) + # The pre-built Zigbee library uses esp_log_default_level which requires + # dynamic log level control to be enabled + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", True) + + +async def attributes_to_code( + var: cg.Pvariable, ep_num: int, cl: dict[str, Any] +) -> None: + for attr in cl.get(CONF_ATTRIBUTES, []): + if attr.get(CONF_ID) is None: + cg.add( + var.add_attr( + ep_num, + CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), + cl[ROLE], + attr[CONF_ATTRIBUTE_ID], + attr.get(CONF_MAX_LENGTH, 0), + attr[CONF_VALUE], + ) + ) + continue + attr_var = cg.new_Pvariable( + attr[CONF_ID], + var, + ep_num, + CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), + cl[ROLE], + attr[CONF_ATTRIBUTE_ID], + ATTR_TYPE[attr[CONF_TYPE]], + attr.get(SCALE, 1), + attr.get(CONF_MAX_LENGTH, 0), + ) + await cg.register_component(attr_var, attr) + + cg.add(attr_var.add_attr(attr[CONF_VALUE])) + if CONF_REPORT in attr and attr[CONF_REPORT] in [ + REPORT["enable"], + REPORT["force"], + ]: + cg.add(attr_var.set_report(attr[CONF_REPORT] == REPORT["force"])) + + if CONF_DEVICE in attr: + device = await cg.get_variable(attr[CONF_DEVICE]) + template_arg = cg.TemplateArguments(get_c_type(attr[CONF_TYPE])) + cg.add(attr_var.connect(template_arg, device)) + + +async def esp32_to_code(config: ConfigType) -> None: + add_idf_component( + name="espressif/esp-zboss-lib", + ref="1.6.4", + ) + add_idf_component( + name="espressif/esp-zigbee-lib", + ref="1.6.8", + ) + + # add sdkconfigs later so they can overwrite esp32 defaults + CORE.add_job(_zigbee_add_sdkconfigs, config) + + # add partitions for zigbee + add_partition("zb_storage", "data", "fat", 0x4000) # 16KB + add_partition("zb_fct", "data", "fat", 0x1000) # 4KB, minimum size + + # create endpoints + zb_data = CORE.data.get(KEY_ZIGBEE, {}) + binary_sensor_ep: list[dict] = zb_data.get(KEY_BS_EP, []) + ep_list = create_ep(binary_sensor_ep, config.get(CONF_ROUTER)) + + # setup zigbee components + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add( + var.set_basic_cluster( + config[CONF_MODEL], + "esphome", + ) + ) + for ep in ep_list: + cg.add(var.create_default_cluster(ep[CONF_NUM], DEVICE_ID[ep[DEVICE_TYPE]])) + for cl in ep.get(CONF_CLUSTERS, []): + cg.add( + var.add_cluster( + ep[CONF_NUM], + CLUSTER_ID.get(cl[CONF_ID], cl[CONF_ID]), + cl[ROLE], + ) + ) + await attributes_to_code(var, ep[CONF_NUM], cl) diff --git a/esphome/components/zigbee/zigbee_helpers_esp32.c b/esphome/components/zigbee/zigbee_helpers_esp32.c new file mode 100644 index 00000000000..4ba71ec6098 --- /dev/null +++ b/esphome/components/zigbee/zigbee_helpers_esp32.c @@ -0,0 +1,74 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +#include "ha/esp_zigbee_ha_standard.h" +#include "zigbee_helpers_esp32.h" + +esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, + uint16_t attr_id, void *value_p) { + esp_err_t ret; + ret = esp_zb_cluster_update_attr(attr_list, attr_id, value_p); + if (ret != ESP_OK) { + ESP_LOGE("zigbee_helper", "Ignore previous attribute not found error"); + ret = esphome_zb_cluster_add_attr(cluster_id, attr_list, attr_id, value_p); + } + if (ret != ESP_OK) { + ESP_LOGE("zigbee_helper", "Could not add attribute 0x%04X to cluster 0x%04X: %s", attr_id, cluster_id, + esp_err_to_name(ret)); + } + return ret; +} + +esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list, + esp_zb_attribute_list_t *attr_list, uint8_t role_mask) { + esp_err_t ret; + ret = esp_zb_cluster_list_update_cluster(cluster_list, attr_list, cluster_id, role_mask); + if (ret != ESP_OK) { + ESP_LOGE("zigbee_helper", "Ignore previous cluster not found error"); + switch (cluster_id) { + case ESP_ZB_ZCL_CLUSTER_ID_BASIC: + ret = esp_zb_cluster_list_add_basic_cluster(cluster_list, attr_list, role_mask); + break; + case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: + ret = esp_zb_cluster_list_add_identify_cluster(cluster_list, attr_list, role_mask); + break; + case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: + ret = esp_zb_cluster_list_add_binary_input_cluster(cluster_list, attr_list, role_mask); + break; + default: + ret = esp_zb_cluster_list_add_custom_cluster(cluster_list, attr_list, role_mask); + } + } + return ret; +} + +esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id) { + switch (cluster_id) { + case ESP_ZB_ZCL_CLUSTER_ID_BASIC: + return esp_zb_basic_cluster_create(NULL); + case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: + return esp_zb_identify_cluster_create(NULL); + case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: + return esp_zb_binary_input_cluster_create(NULL); + default: + return esp_zb_zcl_attr_list_create(cluster_id); + } +} + +esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, + void *value_p) { + switch (cluster_id) { + case ESP_ZB_ZCL_CLUSTER_ID_BASIC: + return esp_zb_basic_cluster_add_attr(attr_list, attr_id, value_p); + case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: + return esp_zb_identify_cluster_add_attr(attr_list, attr_id, value_p); + case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: + return esp_zb_binary_input_cluster_add_attr(attr_list, attr_id, value_p); + default: + return ESP_FAIL; + } +} + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_helpers_esp32.h b/esphome/components/zigbee/zigbee_helpers_esp32.h new file mode 100644 index 00000000000..0650c1689f7 --- /dev/null +++ b/esphome/components/zigbee/zigbee_helpers_esp32.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#ifdef USE_ZIGBEE + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_zigbee_core.h" + +esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp_zb_cluster_list_t *cluster_list, + esp_zb_attribute_list_t *attr_list, uint8_t role_mask); +esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id); +esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, uint16_t attr_id, + void *value_p); +esp_err_t esphome_zb_cluster_add_or_update_attr(uint16_t cluster_id, esp_zb_attribute_list_t *attr_list, + uint16_t attr_id, void *value_p); + +#ifdef __cplusplus +} +namespace esphome::zigbee {} // namespace esphome::zigbee +#endif + +#endif +#endif diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 3288d924833..f6e3e88c63a 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime import random from esphome import automation @@ -7,6 +7,7 @@ from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MODEL, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, UNIT_AMPERE, @@ -48,19 +49,26 @@ from esphome.cpp_generator import ( ) from esphome.types import ConfigType -from .const_zephyr import ( - CONF_IEEE802154_VENDOR_OUI, +from .const import ( CONF_ON_JOIN, CONF_POWER_SOURCE, CONF_WIPE_ON_BOOT, + KEY_ZIGBEE, + POWER_SOURCE, + AnalogAttrs, + AnalogAttrsOutput, + BinaryAttrs, + ZigbeeComponent, + zigbee_ns, +) +from .const_zephyr import ( + CONF_IEEE802154_VENDOR_OUI, CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_ID, CONF_ZIGBEE_NUMBER, CONF_ZIGBEE_SENSOR, CONF_ZIGBEE_SWITCH, KEY_EP_NUMBER, - KEY_ZIGBEE, - POWER_SOURCE, ZB_ZCL_BASIC_ATTRS_EXT_T, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, @@ -69,11 +77,6 @@ from .const_zephyr import ( ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_IDENTIFY_ATTRS_T, - AnalogAttrs, - AnalogAttrsOutput, - BinaryAttrs, - ZigbeeComponent, - zigbee_ns, ) ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component) @@ -209,9 +212,9 @@ async def _attr_to_code(config: ConfigType) -> None: zigbee_assign(basic_attrs.stack_version, 0), zigbee_assign(basic_attrs.hw_version, 0), zigbee_set_string(basic_attrs.mf_name, "esphome"), - zigbee_set_string(basic_attrs.model_id, CORE.name), + zigbee_set_string(basic_attrs.model_id, config[CONF_MODEL]), zigbee_set_string( - basic_attrs.date_code, datetime.now().strftime("%d/%m/%y %H:%M") + basic_attrs.date_code, datetime.datetime.now().strftime("%Y%m%d %H%M%S") ), zigbee_assign( basic_attrs.power_source, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 63fe4e677e1..9b751dd8c0e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -322,6 +322,7 @@ #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) #define USE_OPENTHREAD +#define USE_ZIGBEE #endif #endif diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 3637481c924..c590f73642a 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -37,6 +37,14 @@ dependencies: version: "2.0.0" rules: - if: "target in [esp32, esp32p4]" + espressif/esp-zboss-lib: + version: 1.6.4 + rules: + - if: "target in [esp32h2, esp32c5, esp32c6]" + espressif/esp-zigbee-lib: + version: 1.6.8 + rules: + - if: "target in [esp32h2, esp32c5, esp32c6]" espressif/lan87xx: version: "1.0.0" rules: diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 72ca3f6e9ce..29964902956 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -20,3 +20,8 @@ CONFIG_BT_ENABLED=y # esp32_camera CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC=y CONFIG_ESP32_SPIRAM_SUPPORT=y + +# zigbee +CONFIG_ZB_ENABLED=y +CONFIG_ZB_ZED=y +CONFIG_ZB_RADIO_NATIVE=y diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index 2af35ff1483..c689d07f6ba 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -1,4 +1,3 @@ ---- binary_sensor: - platform: template name: "Garage Door Open 1" @@ -22,12 +21,6 @@ sensor: lambda: return 12.0; internal: True -zigbee: - wipe_on_boot: true - on_join: - then: - - logger.log: "Joined network" - output: - platform: template id: output_factory @@ -35,9 +28,6 @@ output: write_action: - zigbee.factory_reset -time: - - platform: zigbee - switch: - platform: template name: "Template Switch" diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml new file mode 100644 index 00000000000..4494b4081d4 --- /dev/null +++ b/tests/components/zigbee/common_esp32.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: template + name: "Garage Door Open 10" + report: "enable" + - platform: template + name: "Garage Door Open 11" + report: "coordinator" + - platform: template + name: "Garage Door Open 12" + report: "force" + +zigbee: + model: zigbee_test + router: true diff --git a/tests/components/zigbee/common_nrf52.yaml b/tests/components/zigbee/common_nrf52.yaml new file mode 100644 index 00000000000..bc39b371f51 --- /dev/null +++ b/tests/components/zigbee/common_nrf52.yaml @@ -0,0 +1,12 @@ +packages: + - !include common.yaml + +zigbee: + model: zigbee_test + wipe_on_boot: true + on_join: + then: + - logger.log: "Joined network" + +time: + - platform: zigbee diff --git a/tests/components/zigbee/test.esp32-c6-idf.yaml b/tests/components/zigbee/test.esp32-c6-idf.yaml new file mode 100644 index 00000000000..8e4796a0731 --- /dev/null +++ b/tests/components/zigbee/test.esp32-c6-idf.yaml @@ -0,0 +1 @@ +<<: !include common_esp32.yaml diff --git a/tests/components/zigbee/test.nrf52-adafruit.yaml b/tests/components/zigbee/test.nrf52-adafruit.yaml index dade44d145b..bf3cb9cdd92 100644 --- a/tests/components/zigbee/test.nrf52-adafruit.yaml +++ b/tests/components/zigbee/test.nrf52-adafruit.yaml @@ -1 +1 @@ -<<: !include common.yaml +<<: !include common_nrf52.yaml diff --git a/tests/components/zigbee/test.nrf52-mcumgr.yaml b/tests/components/zigbee/test.nrf52-mcumgr.yaml index dade44d145b..bf3cb9cdd92 100644 --- a/tests/components/zigbee/test.nrf52-mcumgr.yaml +++ b/tests/components/zigbee/test.nrf52-mcumgr.yaml @@ -1 +1 @@ -<<: !include common.yaml +<<: !include common_nrf52.yaml diff --git a/tests/components/zigbee/test.nrf52-xiao-ble.yaml b/tests/components/zigbee/test.nrf52-xiao-ble.yaml index 254f370ca7f..83d949b4ddc 100644 --- a/tests/components/zigbee/test.nrf52-xiao-ble.yaml +++ b/tests/components/zigbee/test.nrf52-xiao-ble.yaml @@ -1,4 +1,4 @@ -<<: !include common.yaml +<<: !include common_nrf52.yaml zigbee: wipe_on_boot: once From d759f1a56751207689f5db024181739c96d94646 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 23 Apr 2026 16:53:52 -0400 Subject: [PATCH 1629/2056] [audio_http] Add a media source for playing audio from HTTP URLs (#15741) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/audio_http/__init__.py | 0 .../audio_http/audio_http_media_source.cpp | 163 ++++++++++++++++++ .../audio_http/audio_http_media_source.h | 59 +++++++ esphome/components/audio_http/media_source.py | 59 +++++++ tests/components/audio_http/common.yaml | 7 + .../components/audio_http/test.esp32-idf.yaml | 1 + 7 files changed, 290 insertions(+) create mode 100644 esphome/components/audio_http/__init__.py create mode 100644 esphome/components/audio_http/audio_http_media_source.cpp create mode 100644 esphome/components/audio_http/audio_http_media_source.h create mode 100644 esphome/components/audio_http/media_source.py create mode 100644 tests/components/audio_http/common.yaml create mode 100644 tests/components/audio_http/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 69f2cb1d178..be835aae3d9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,6 +56,7 @@ esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 esphome/components/audio_file/* @kahrendt esphome/components/audio_file/media_source/* @kahrendt +esphome/components/audio_http/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_http/__init__.py b/esphome/components/audio_http/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/audio_http/audio_http_media_source.cpp b/esphome/components/audio_http/audio_http_media_source.cpp new file mode 100644 index 00000000000..04b7d046e6b --- /dev/null +++ b/esphome/components/audio_http/audio_http_media_source.cpp @@ -0,0 +1,163 @@ +#include "audio_http_media_source.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> + +#include <algorithm> + +namespace esphome::audio_http { + +static const char *const TAG = "audio_http_media_source"; + +// Decoder task / buffer tuning. Kept here as constants so the header stays free of magic numbers. +static constexpr size_t DEFAULT_TRANSFER_BUFFER_SIZE = 8 * 1024; // Staging buffer between HTTP reader and decoder +static constexpr uint32_t HTTP_TIMEOUT_MS = 5000; // HTTP connect/read timeout +static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; // Max blocking time per on_audio_write() call +static constexpr uint32_t READER_WRITE_TIMEOUT_MS = 50; // Max blocking time when writing into the ring buffer +static constexpr uint8_t READER_TASK_PRIORITY = 2; +static constexpr uint8_t DECODER_TASK_PRIORITY = 2; +static constexpr size_t READER_TASK_STACK_SIZE = 4096; +static constexpr size_t DECODER_TASK_STACK_SIZE = 5120; +static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20; +static constexpr const char *const HTTP_URI_PREFIX = "http://"; +static constexpr const char *const HTTPS_URI_PREFIX = "https://"; + +void AudioHTTPMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, + "Audio HTTP Media Source:\n" + " Buffer Size: %zu bytes\n" + " Decoder Task Stack in PSRAM: %s", + this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_)); +} + +void AudioHTTPMediaSource::setup() { + this->disable_loop(); + + micro_decoder::DecoderConfig config; + config.ring_buffer_size = this->buffer_size_; + // Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring + // while the decoder is still draining it, instead of oscillating between empty and full. + config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2); + config.http_timeout_ms = HTTP_TIMEOUT_MS; + config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS; + config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS; + config.reader_priority = READER_TASK_PRIORITY; + config.decoder_priority = DECODER_TASK_PRIORITY; + config.reader_stack_size = READER_TASK_STACK_SIZE; + config.decoder_stack_size = DECODER_TASK_STACK_SIZE; + config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_; + + this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config); + if (this->decoder_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate decoder"); + this->mark_failed(); + return; + } + this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener +} + +void AudioHTTPMediaSource::loop() { this->decoder_->loop(); } + +bool AudioHTTPMediaSource::can_handle(const std::string &uri) const { + return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX); +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +bool AudioHTTPMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) { + return false; + } + + // Check if source is already playing + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + // Validate URI starts with "http://" or "https://" + if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + if (this->decoder_->play_url(uri)) { + this->pause_.store(false, std::memory_order_relaxed); + this->enable_loop(); + return true; + } + + ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str()); + return false; +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +void AudioHTTPMediaSource::handle_command(media_source::MediaSourceCommand command) { + switch (command) { + case media_source::MediaSourceCommand::STOP: + this->decoder_->stop(); + break; + case media_source::MediaSourceCommand::PAUSE: + // Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state + // machine from getting stuck in PAUSED when no playback is active (which would block the + // next play_uri() call via its IDLE-state precondition). + if (this->get_state() != media_source::MediaSourceState::PLAYING) + break; + // PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily + // yields, which fills the ring buffer and applies back pressure that effectively pauses both + // the decoder and HTTP reader tasks. + this->set_state_(media_source::MediaSourceState::PAUSED); + this->pause_.store(true, std::memory_order_relaxed); + break; + case media_source::MediaSourceCommand::PLAY: + // Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR. + if (this->get_state() != media_source::MediaSourceState::PAUSED) + break; + this->set_state_(media_source::MediaSourceState::PLAYING); + this->pause_.store(false, std::memory_order_relaxed); + break; + default: + break; + } +} + +// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for +// being thread-safe with respect to its own audio writer. +size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) { + if (this->pause_.load(std::memory_order_relaxed)) { + vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS)); + return 0; + } + return this->write_output(data, length, timeout_ms, this->stream_info_); +} + +// Called from the decoder task before the first on_audio_write(). +void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) { + this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate()); +} + +// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main +// loop thread and it's safe to call set_state_() directly. +void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) { + switch (state) { + case micro_decoder::DecoderState::IDLE: + this->set_state_(media_source::MediaSourceState::IDLE); + this->disable_loop(); + break; + case micro_decoder::DecoderState::PLAYING: + this->set_state_(media_source::MediaSourceState::PLAYING); + break; + case micro_decoder::DecoderState::FAILED: + this->set_state_(media_source::MediaSourceState::ERROR); + break; + default: + break; + } +} + +} // namespace esphome::audio_http + +#endif // USE_ESP32 diff --git a/esphome/components/audio_http/audio_http_media_source.h b/esphome/components/audio_http/audio_http_media_source.h new file mode 100644 index 00000000000..e4bd69e9e6f --- /dev/null +++ b/esphome/components/audio_http/audio_http_media_source.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/core/component.h" + +#include <micro_decoder/decoder_source.h> +#include <micro_decoder/types.h> + +#include <atomic> +#include <memory> +#include <string> + +namespace esphome::audio_http { + +// Inherits from two unrelated listener-style interfaces: +// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator +// (the orchestrator calls set_listener() on us with a MediaSourceListener*). +// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded +// audio and state changes (we call decoder_->set_listener(this) in setup()). +// The two set_listener() methods live on different base classes and serve opposite directions. +class AudioHTTPMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; } + void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; } + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override; + + // DecoderListener interface implementation + size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override; + void on_stream_info(const micro_decoder::AudioStreamInfo &info) override; + void on_state_change(micro_decoder::DecoderState state) override; + + protected: + std::unique_ptr<micro_decoder::DecoderSource> decoder_; + audio::AudioStreamInfo stream_info_; + + size_t buffer_size_{50000}; + + // Written from the main loop in handle_command(), read from the decoder task in + // on_audio_write(). Must be atomic to avoid a data race. + std::atomic<bool> pause_{false}; + bool decoder_task_stack_in_psram_{false}; +}; + +} // namespace esphome::audio_http + +#endif // USE_ESP32 diff --git a/esphome/components/audio_http/media_source.py b/esphome/components/audio_http/media_source.py new file mode 100644 index 00000000000..519d8df698e --- /dev/null +++ b/esphome/components/audio_http/media_source.py @@ -0,0 +1,59 @@ +from typing import Any + +import esphome.codegen as cg +from esphome.components import audio, esp32, media_source, psram +import esphome.config_validation as cv +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.types import ConfigType + +CODEOWNERS = ["@kahrendt"] +AUTO_LOAD = ["audio"] + +audio_http_ns = cg.esphome_ns.namespace("audio_http") +AudioHTTPMediaSource = audio_http_ns.class_( + "AudioHTTPMediaSource", cg.Component, media_source.MediaSource +) + + +def _request_micro_decoder(config: ConfigType) -> ConfigType: + audio.request_micro_decoder_support() + return config + + +def _validate_task_stack_in_psram(value: Any) -> bool: + # Only require the psram component when actually enabling PSRAM stacks; validating + # the boolean first means `false` doesn't trigger the requires_component check. + if value := cv.boolean(value): + return cv.requires_component(psram.DOMAIN)(value) + return value + + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + AudioHTTPMediaSource, + ) + .extend( + { + cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range( + min=5000, max=1000000 + ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram, + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, + _request_micro_decoder, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) + cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) diff --git a/tests/components/audio_http/common.yaml b/tests/components/audio_http/common.yaml new file mode 100644 index 00000000000..b7457165a59 --- /dev/null +++ b/tests/components/audio_http/common.yaml @@ -0,0 +1,7 @@ +psram: + +media_source: + - platform: audio_http + id: audio_http_source + buffer_size: 100000 + task_stack_in_psram: true diff --git a/tests/components/audio_http/test.esp32-idf.yaml b/tests/components/audio_http/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/audio_http/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 90d7bfe02ea4f5fa61fdcec4a0645aa95da88b53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 23 Apr 2026 16:36:32 -0500 Subject: [PATCH 1630/2056] [ci] Auto-close PRs opened from a fork's default branch (#15957) --- .../close-pr-from-fork-default-branch.yml | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/close-pr-from-fork-default-branch.yml diff --git a/.github/workflows/close-pr-from-fork-default-branch.yml b/.github/workflows/close-pr-from-fork-default-branch.yml new file mode 100644 index 00000000000..1cd70f5efcd --- /dev/null +++ b/.github/workflows/close-pr-from-fork-default-branch.yml @@ -0,0 +1,72 @@ +name: Close PR From Fork Default Branch + +on: + # pull_request_target is required so we have permission to comment and close PRs from forks. + pull_request_target: + types: [opened, reopened] + +permissions: + pull-requests: write + issues: write + +jobs: + close: + name: Close PR opened from fork's default branch + runs-on: ubuntu-latest + if: >- + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + && github.event.pull_request.head.ref == github.event.repository.default_branch + steps: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { owner, repo } = context.repo; + const prNumber = context.payload.pull_request.number; + const author = context.payload.pull_request.user.login; + const defaultBranch = context.payload.repository.default_branch; + const headRepo = context.payload.pull_request.head.repo.full_name; + + const body = [ + `Hi @${author}, thanks for opening a pull request! :tada:`, + ``, + `It looks like this PR was opened from the \`${defaultBranch}\` branch of your fork (\`${headRepo}\`), which is the same name as this repository's default branch. Working directly on \`${defaultBranch}\` in your fork causes a few problems:`, + ``, + `- Your fork's \`${defaultBranch}\` branch will permanently diverge from \`esphome/esphome:${defaultBranch}\`, making it hard to keep your fork up to date.`, + `- Any additional commits you push to \`${defaultBranch}\` will be added to this PR, so you can't easily work on multiple changes at once.`, + `- Pushing maintainer fixes to your branch is awkward, since it means committing directly to your fork's default branch.`, + `- It makes local collaboration painful — \`${defaultBranch}\` in a checkout becomes ambiguous between upstream and your fork, and maintainers end up with naming collisions when fetching your branch.`, + ``, + `Please re-open this as a new PR from a dedicated feature branch. The usual flow looks like:`, + ``, + `\`\`\`bash`, + `# Make sure your fork's ${defaultBranch} is up to date with upstream`, + `git remote add upstream https://github.com/${owner}/${repo}.git # if you haven't already`, + `git fetch upstream`, + `git checkout ${defaultBranch}`, + `git reset --hard upstream/${defaultBranch}`, + `git push --force-with-lease origin ${defaultBranch}`, + ``, + `# Create a new branch for your change and cherry-pick / re-apply your commits there`, + `git checkout -b my-feature-branch upstream/${defaultBranch}`, + `# ...re-apply your changes, then:`, + `git push origin my-feature-branch`, + `\`\`\``, + ``, + `Then open a new pull request from \`my-feature-branch\` into \`${owner}/${repo}:${defaultBranch}\`.`, + ``, + `Closing this PR for now — sorry for the friction, and thanks again for contributing! :heart:`, + ].join('\n'); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body, + }); + + await github.rest.pulls.update({ + owner, + repo, + pull_number: prNumber, + state: 'closed', + }); From ddf1426f8622daf68e4be187d243fcdf410bda28 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 23 Apr 2026 18:09:36 -0400 Subject: [PATCH 1631/2056] [sendspin] Add initial Sendspin hub component (PR1) (#15924) Co-authored-by: Copilot <copilot@github.com> --- CODEOWNERS | 1 + esphome/components/sendspin/__init__.py | 146 ++++++++++++++++++ esphome/components/sendspin/sendspin_hub.cpp | 143 +++++++++++++++++ esphome/components/sendspin/sendspin_hub.h | 138 +++++++++++++++++ esphome/core/defines.h | 5 + esphome/idf_component.yml | 2 + tests/components/sendspin/common.yaml | 9 ++ tests/components/sendspin/test.esp32-idf.yaml | 1 + 8 files changed, 445 insertions(+) create mode 100644 esphome/components/sendspin/__init__.py create mode 100644 esphome/components/sendspin/sendspin_hub.cpp create mode 100644 esphome/components/sendspin/sendspin_hub.h create mode 100644 tests/components/sendspin/common.yaml create mode 100644 tests/components/sendspin/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index be835aae3d9..facfdb1705e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -440,6 +440,7 @@ esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct +esphome/components/sendspin/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/serial_proxy/* @kbx81 diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py new file mode 100644 index 00000000000..d86c5d6dab2 --- /dev/null +++ b/esphome/components/sendspin/__init__.py @@ -0,0 +1,146 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +from esphome.components import esp32, network, psram, socket, wifi +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.core import CORE +from esphome.types import ConfigType + +# mdns for autodiscovery +AUTO_LOAD = ["mdns"] +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["network"] +DOMAIN = "sendspin" + +# Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace. +# Analysis tools strip the trailing underscore (same pattern as `template_`). +sendspin_ns = cg.esphome_ns.namespace("sendspin_") +SendspinHub = sendspin_ns.class_( + "SendspinHub", + cg.Component, +) + + +@dataclass +class SendspinConfiguration: + artwork_support: bool = False + controller_support: bool = False + metadata_support: bool = False + player_support: bool = False + visualizer_support: bool = False + + +def _get_data() -> SendspinConfiguration: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = SendspinConfiguration() + return CORE.data[DOMAIN] + + +def request_artwork_support() -> None: + """Request artwork role support for Sendspin.""" + _get_data().artwork_support = True + + +def request_controller_support() -> None: + """Request controller role support for Sendspin.""" + _get_data().controller_support = True + + +def request_metadata_support() -> None: + """Request metadata role support for Sendspin.""" + _get_data().metadata_support = True + + +def request_player_support() -> None: + """Request player role support for Sendspin.""" + _get_data().player_support = True + + +def request_visualizer_support() -> None: + """Request visualizer role support for Sendspin.""" + _get_data().visualizer_support = True + + +def _validate_task_stack_in_psram(value): + value = cv.boolean(value) + if value: + return cv.requires_component(psram.DOMAIN)(value) + return value + + +def _request_high_performance_networking(config: ConfigType) -> ConfigType: + """Request high performance networking for Sendspin streaming. + + Also enables wake_loop_threadsafe support for fast defer() callbacks + from background threads (WebSocket handler, image decoder). + """ + network.require_high_performance_networking() + # Socket consumption varies by mode: + # - Server mode: 1 listening socket + 2 client connections (for handoff) + # - Client mode: 1 outbound connection + socket.consume_sockets( + 1, "sendspin_websocket_server", socket.SocketType.TCP_LISTEN + )(config) + socket.consume_sockets(2, "sendspin_websocket_server")(config) + socket.consume_sockets(1, "sendspin_websocket_client")(config) + + wifi.enable_runtime_power_save_control() + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SendspinHub), + cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram, + } + ), + cv.only_on_esp32, + _request_high_performance_networking, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) + + # sendspin-cpp library + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.0") + + cg.add_define("USE_SENDSPIN", True) # for MDNS + + data = _get_data() + + # Configure Sendspin roles based on requested features (ESPHome internally via USE_SENDSPIN_*) + # and disable building unused code paths in the sendspin-cpp library (IDF SDKConfig via CONFIG_SENDSPIN_ENABLE_*). + if data.artwork_support: + cg.add_define("USE_SENDSPIN_ARTWORK", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_ARTWORK", False) + + if data.controller_support: + cg.add_define("USE_SENDSPIN_CONTROLLER", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_CONTROLLER", False) + + if data.metadata_support: + cg.add_define("USE_SENDSPIN_METADATA", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_METADATA", False) + + if data.player_support: + cg.add_define("USE_SENDSPIN_PLAYER", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_PLAYER", False) + + if data.visualizer_support: + cg.add_define("USE_SENDSPIN_VISUALIZER", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_VISUALIZER", False) diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp new file mode 100644 index 00000000000..94338887946 --- /dev/null +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -0,0 +1,143 @@ +#include "sendspin_hub.h" + +#ifdef USE_ESP32 + +#include "esphome/components/network/util.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/version.h" + +#include <esp_log.h> + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.hub"; + +void SendspinHub::setup() { + auto config = this->build_client_config_(); + this->client_ = std::make_unique<sendspin::SendspinClient>(std::move(config)); + + // Set up persistence (preferences must be initialized before providers are added to the client) + this->last_played_server_pref_ = + global_preferences->make_preference<LastPlayedServerPref>(fnv1a_hash("sendspin_last_played")); + + // Wire providers and client listener + this->client_->set_listener(this); + this->client_->set_network_provider(this); + this->client_->set_persistence_provider(this); + + if (!this->client_->start_server()) { + ESP_LOGE(TAG, "Failed to start Sendspin server"); + this->mark_failed(); + return; + } +} + +void SendspinHub::loop() { this->client_->loop(); } + +void SendspinHub::dump_config() { + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, + "Sendspin Hub:\n" + " Client ID: %s\n" + " Task stack in PSRAM: %s", + get_mac_address_pretty_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_)); +} + +// --- Delegating methods --- + +// THREAD CONTEXT: Main loop (invoked from Sendspin components) +void SendspinHub::connect_to_server(const std::string &url) { + if (this->is_ready()) { + this->client_->connect_to(url); + } +} + +// THREAD CONTEXT: Main loop (invoked from Sendspin components) +void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) { + if (this->is_ready()) { + this->client_->disconnect(reason); + } +} + +// THREAD CONTEXT: Main loop (invoked from Sendspin components) +void SendspinHub::update_state(sendspin::SendspinClientState state) { + if (this->is_ready()) { + this->client_->update_state(state); + } +} + +sendspin::SendspinClientConfig SendspinHub::build_client_config_() { + sendspin::SendspinClientConfig config; + + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + config.client_id = get_mac_address_pretty_into_buffer(mac_buf); + config.name = App.get_friendly_name(); + config.product_name = App.get_name(); + config.manufacturer = "ESPHome"; + config.software_version = ESPHOME_VERSION; + config.httpd_psram_stack = this->task_stack_in_psram_; + + return config; +} + +// --- SendspinClientListener overrides --- +// THREAD CONTEXT: Main loop (fired from client_->loop()) + +void SendspinHub::on_group_update(const sendspin::GroupUpdateObject &group) { + this->group_update_callbacks_.call(group); +} + +void SendspinHub::on_request_high_performance() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->request_high_performance(); + } +#endif +} + +void SendspinHub::on_release_high_performance() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->release_high_performance(); + } +#endif +} + +// --- SendspinNetworkProvider override --- + +// THREAD CONTEXT: Main loop (polled by client_->loop()) +bool SendspinHub::is_network_ready() { return network::is_connected(); } + +// --- SendspinPersistenceProvider overrides --- + +// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events) +bool SendspinHub::save_last_server_hash(uint32_t hash) { + LastPlayedServerPref pref{.server_id_hash = hash}; + bool ok = this->last_played_server_pref_.save(&pref); + if (ok) { + ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash); + } else { + ESP_LOGW(TAG, "Failed to persist last played server hash"); + } + return ok; +} + +// THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events) +std::optional<uint32_t> SendspinHub::load_last_server_hash() { + LastPlayedServerPref pref{}; + if (this->last_played_server_pref_.load(&pref)) { + ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash); + return pref.server_id_hash; + } + return std::nullopt; +} + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h new file mode 100644 index 00000000000..4402d25fbd1 --- /dev/null +++ b/esphome/components/sendspin/sendspin_hub.h @@ -0,0 +1,138 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +#include <sendspin/client.h> +#include <sendspin/config.h> +#include <sendspin/types.h> + +#include <functional> +#include <memory> +#include <optional> + +namespace esphome::sendspin_ { + +/// @brief Setup priorities for the sendspin hub and its child components. +/// +/// Centralized here so every sendspin component orders itself relative to the hub +/// without each subcomponent having to pick a priority independently. Children run +/// one step later than hub so they can assume hub's setup() has already completed. +namespace sendspin_priority { +inline constexpr float HUB = esphome::setup_priority::PROCESSOR; +inline constexpr float CHILD = HUB - 1.0f; +} // namespace sendspin_priority + +/// @brief Persistent storage structure for last played server hash. +struct LastPlayedServerPref { + uint32_t server_id_hash; +}; + +/// @brief Thin adapter over sendspin::SendspinClient. +/// +/// The hub owns a SendspinClient instance and bridges its listener/provider interfaces to ESPHome's CallbackManager for +/// fan-out to child components. +/// - Provides persistence via ESPPreferenceObject and WiFi power management integration. +/// - Handles Sendspin roles that apply to multiple child components (artwork, controller, metadata) so their events +/// can be fanned out. Roles specific to a single component (player) are configured by the hub but owned by the +/// child thereafter, since no fan-out is needed. +/// +/// The sendspin-cpp library follows this design: +/// - Core and role configuration are passed at client/role construction time as structs. Built in our `setup()`. +/// - Library -> user code communication happens via two interface types the user implements and registers in our +/// `setup()`: listener interfaces (for events the library pushes; e.g., group updates) and provider interfaces +/// (for services the library pulls; e.g., persistence, network readiness). +/// - User -> library communication uses exposed functions on the client and role objects that the user calls. +class SendspinHub final : public Component, + public sendspin::SendspinClientListener, + public sendspin::SendspinNetworkProvider, + public sendspin::SendspinPersistenceProvider { + public: + float get_setup_priority() const override { return sendspin_priority::HUB; } + void setup() override; + void loop() override; + void dump_config() override; + + /// @brief Connects the underlying client to the given Sendspin server. + /// + /// No-op if the hub's client is not ready (e.g. setup() has not completed). + /// Must be called from the main loop thread. + /// @param url WebSocket URL of the Sendspin server, starting with `ws://` (e.g. `ws://host:port/path`). + void connect_to_server(const std::string &url); + + /// @brief Disconnects the underlying client from the current server. + /// + /// Sends a `client/goodbye` message with the given reason before closing the connection. + /// No-op if the hub's client is not ready. Must be called from the main loop thread. + /// @param reason Reason reported to the server: + /// - `ANOTHER_SERVER`: client is switching to another server. + /// - `SHUTDOWN`: client is shutting down. + /// - `RESTART`: client is restarting. + /// - `USER_REQUEST`: user explicitly requested disconnect. + void disconnect_from_server(sendspin::SendspinGoodbyeReason reason); + + /// @brief Updates the client's reported playback state on the server. + /// + /// No-op if the hub's client is not ready. Must be called from the main loop thread. + /// @param state New client state: + /// - `SYNCHRONIZED`: client is synchronized and playing from the server. + /// - `ERROR`: client encountered a playback error. + /// - `EXTERNAL_SOURCE`: client is playing from a non-Sendspin source. + void update_state(sendspin::SendspinClientState state); + + // --- Configuration setters (called from codegen) --- + + template<typename F> void add_group_update_callback(F &&callback) { + this->group_update_callbacks_.add(std::forward<F>(callback)); + } + + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + protected: + /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. + sendspin::SendspinClientConfig build_client_config_(); + + // --- SendspinClientListener overrides --- + void on_group_update(const sendspin::GroupUpdateObject &group) override; + + void on_request_high_performance() override; + + void on_release_high_performance() override; + + // --- SendspinNetworkProvider override --- + bool is_network_ready() override; + + // --- SendspinPersistenceProvider overrides --- + bool save_last_server_hash(uint32_t hash) override; + std::optional<uint32_t> load_last_server_hash() override; + + ESPPreferenceObject last_played_server_pref_; + + std::unique_ptr<sendspin::SendspinClient> client_; + + // Callback fan-out to child components + CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{}; + + bool task_stack_in_psram_{false}; +}; + +/// @brief Base class for all sendspin subcomponents. +/// +/// Consolidates the Component + Parented<SendspinHub> inheritance and pins the setup +/// priority so the hub's setup() always runs before any child. Subcomponents should +/// inherit from this instead of listing Component/Parented individually and must not +/// override get_setup_priority(). +class SendspinChild : public Component, public Parented<SendspinHub> { + public: + float get_setup_priority() const override { return sendspin_priority::CHILD; } +}; + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9b751dd8c0e..80247f69da1 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -257,6 +257,11 @@ #define USE_MICROPHONE #define USE_PSRAM #define USE_SENDSPIN +#define USE_SENDSPIN_ARTWORK +#define USE_SENDSPIN_CONTROLLER +#define USE_SENDSPIN_METADATA +#define USE_SENDSPIN_PLAYER +#define USE_SENDSPIN_VISUALIZER #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_LWIP_FAST_SELECT diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c590f73642a..f422d94097d 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -91,5 +91,7 @@ dependencies: - if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]" esp32async/asynctcp: version: 3.4.91 + sendspin/sendspin-cpp: + version: 0.3.0 lvgl/lvgl: version: 9.5.0 diff --git a/tests/components/sendspin/common.yaml b/tests/components/sendspin/common.yaml new file mode 100644 index 00000000000..9d7da76758a --- /dev/null +++ b/tests/components/sendspin/common.yaml @@ -0,0 +1,9 @@ +wifi: + ap: + +psram: + mode: quad + +sendspin: + id: sendspin_hub_id + task_stack_in_psram: true diff --git a/tests/components/sendspin/test.esp32-idf.yaml b/tests/components/sendspin/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/sendspin/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From b4a86e46b256020be129a67c59b48ccf3e5c3311 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 23 Apr 2026 21:22:47 -0400 Subject: [PATCH 1632/2056] [sendspin] Add controller role and sendspin.switch action (PR2) (#15929) Co-authored-by: Copilot <copilot@github.com> --- esphome/components/sendspin/__init__.py | 46 ++++++++++++++++++- esphome/components/sendspin/automation.h | 25 ++++++++++ esphome/components/sendspin/sendspin_hub.cpp | 22 +++++++++ esphome/components/sendspin/sendspin_hub.h | 31 +++++++++++++ tests/components/sendspin/common-action.yaml | 8 ++++ .../sendspin/test-action.esp32-idf.yaml | 1 + 6 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sendspin/automation.h create mode 100644 tests/components/sendspin/common-action.yaml create mode 100644 tests/components/sendspin/test-action.esp32-idf.yaml diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index d86c5d6dab2..166d3fd70da 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -1,10 +1,12 @@ from dataclasses import dataclass +from esphome import automation import esphome.codegen as cg from esphome.components import esp32, network, psram, socket, wifi import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType # mdns for autodiscovery @@ -22,6 +24,13 @@ SendspinHub = sendspin_ns.class_( ) +SendspinSwitchCommandAction = sendspin_ns.class_( + "SendspinSwitchCommandAction", + automation.Action, + cg.Parented.template(SendspinHub), +) + + @dataclass class SendspinConfiguration: artwork_support: bool = False @@ -101,6 +110,41 @@ CONFIG_SCHEMA = cv.All( ) +def _request_controller_role(config: ConfigType) -> ConfigType: + """Request the controller role for the sendspin.switch action.""" + request_controller_support() + return config + + +SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All( + automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(SendspinHub), + } + ) + ), + _request_controller_role, +) + + +@automation.register_action( + "sendspin.switch", + SendspinSwitchCommandAction, + SENDSPIN_SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +async def sendspin_switch_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/sendspin/automation.h b/esphome/components/sendspin/automation.h new file mode 100644 index 00000000000..be3b1eb39d3 --- /dev/null +++ b/esphome/components/sendspin/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "sendspin_hub.h" + +namespace esphome::sendspin_ { + +#ifdef USE_SENDSPIN_CONTROLLER +template<typename... Ts> class SendspinSwitchCommandAction : public Action<Ts...>, public Parented<SendspinHub> { + public: + void play(const Ts &...x) override { + // Clear any EXTERNAL_SOURCE state so the switch command is followed + this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED); + this->parent_->send_client_command(sendspin::SendspinControllerCommand::SWITCH); + } +}; +#endif // USE_SENDSPIN_CONTROLLER + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 94338887946..ec419f77412 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -31,6 +31,11 @@ void SendspinHub::setup() { this->client_->set_network_provider(this); this->client_->set_persistence_provider(this); +#ifdef USE_SENDSPIN_CONTROLLER + this->controller_role_ = &this->client_->add_controller(); + this->controller_role_->set_listener(this); +#endif + if (!this->client_->start_server()) { ESP_LOGE(TAG, "Failed to start Sendspin server"); this->mark_failed(); @@ -138,6 +143,23 @@ std::optional<uint32_t> SendspinHub::load_last_server_hash() { return std::nullopt; } +// --- Sendspin role specific methods/overrides --- + +#ifdef USE_SENDSPIN_CONTROLLER +// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components) +void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume, + std::optional<bool> mute) { + if (this->is_ready()) { + this->controller_role_->send_command(command, volume, mute); + } +} + +// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop()) +void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) { + this->controller_state_callbacks_.call(state); +} +#endif + } // namespace esphome::sendspin_ #endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 4402d25fbd1..1e217e0ea2e 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -13,6 +13,10 @@ #include <sendspin/config.h> #include <sendspin/types.h> +#ifdef USE_SENDSPIN_CONTROLLER +#include <sendspin/controller_role.h> +#endif + #include <functional> #include <memory> #include <optional> @@ -50,6 +54,9 @@ struct LastPlayedServerPref { /// (for services the library pulls; e.g., persistence, network readiness). /// - User -> library communication uses exposed functions on the client and role objects that the user calls. class SendspinHub final : public Component, +#ifdef USE_SENDSPIN_CONTROLLER + public sendspin::ControllerRoleListener, +#endif public sendspin::SendspinClientListener, public sendspin::SendspinNetworkProvider, public sendspin::SendspinPersistenceProvider { @@ -94,6 +101,17 @@ class SendspinHub final : public Component, void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + // --- Sendspin role specific methods --- + +#ifdef USE_SENDSPIN_CONTROLLER + void send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume = std::nullopt, + std::optional<bool> mute = std::nullopt); + + template<typename F> void add_controller_state_callback(F &&callback) { + this->controller_state_callbacks_.add(std::forward<F>(callback)); + } +#endif + protected: /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. sendspin::SendspinClientConfig build_client_config_(); @@ -112,6 +130,19 @@ class SendspinHub final : public Component, bool save_last_server_hash(uint32_t hash) override; std::optional<uint32_t> load_last_server_hash() override; + // --- Sendspin role specific methods/overrides/member variables --- + +#ifdef USE_SENDSPIN_CONTROLLER + sendspin::ControllerRole *controller_role_{nullptr}; + + void on_controller_state(const sendspin::ServerStateControllerObject &state) override; + + // Callback fan-out to child components; they filter as needed + CallbackManager<void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_{}; +#endif + + // --- Core member variables --- + ESPPreferenceObject last_played_server_pref_; std::unique_ptr<sendspin::SendspinClient> client_; diff --git a/tests/components/sendspin/common-action.yaml b/tests/components/sendspin/common-action.yaml new file mode 100644 index 00000000000..16f19ad7d17 --- /dev/null +++ b/tests/components/sendspin/common-action.yaml @@ -0,0 +1,8 @@ +# `sendspin.switch` action enables the controller role, so we use a standalone test +packages: + base: !include common.yaml + +wifi: + on_connect: + then: + - sendspin.switch: diff --git a/tests/components/sendspin/test-action.esp32-idf.yaml b/tests/components/sendspin/test-action.esp32-idf.yaml new file mode 100644 index 00000000000..70a7ee1bade --- /dev/null +++ b/tests/components/sendspin/test-action.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-action.yaml From 3ccaa771a7423f95cc1bd4cb1b5a77d5b7f04324 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 23 Apr 2026 21:46:25 -0400 Subject: [PATCH 1633/2056] [sendspin] Add a group media player controller (PR3) (#15948) Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- CODEOWNERS | 1 + esphome/components/sendspin/__init__.py | 2 + .../sendspin/media_player/__init__.py | 45 +++++ .../media_player/sendspin_media_player.cpp | 165 ++++++++++++++++++ .../media_player/sendspin_media_player.h | 33 ++++ .../sendspin/common-media_player.yaml | 5 + .../sendspin/test-media_player.esp32-idf.yaml | 1 + 7 files changed, 252 insertions(+) create mode 100644 esphome/components/sendspin/media_player/__init__.py create mode 100644 esphome/components/sendspin/media_player/sendspin_media_player.cpp create mode 100644 esphome/components/sendspin/media_player/sendspin_media_player.h create mode 100644 tests/components/sendspin/common-media_player.yaml create mode 100644 tests/components/sendspin/test-media_player.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index facfdb1705e..65db6ca25ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -441,6 +441,7 @@ esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sendspin/* @kahrendt +esphome/components/sendspin/media_player/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/serial_proxy/* @kbx81 diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 166d3fd70da..2d053903789 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -15,6 +15,8 @@ CODEOWNERS = ["@kahrendt"] DEPENDENCIES = ["network"] DOMAIN = "sendspin" +CONF_SENDSPIN_ID = "sendspin_id" + # Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace. # Analysis tools strip the trailing underscore (same pattern as `template_`). sendspin_ns = cg.esphome_ns.namespace("sendspin_") diff --git a/esphome/components/sendspin/media_player/__init__.py b/esphome/components/sendspin/media_player/__init__.py new file mode 100644 index 00000000000..4aaee8cd897 --- /dev/null +++ b/esphome/components/sendspin/media_player/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import media_player +from esphome.components.const import CONF_VOLUME_INCREMENT +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.types import ConfigType + +from .. import CONF_SENDSPIN_ID, SendspinHub, request_controller_support, sendspin_ns + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["sendspin"] + +SendspinMediaPlayer = sendspin_ns.class_( + "SendspinMediaPlayer", + media_player.MediaPlayer, + cg.Component, +) + + +def _request_roles(config: ConfigType) -> ConfigType: + """Request the necessary Sendspin roles for the media player.""" + request_controller_support() + + return config + + +CONFIG_SCHEMA = cv.All( + media_player.media_player_schema(SendspinMediaPlayer).extend( + { + cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub), + cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, + } + ), + cv.only_on_esp32, + _request_roles, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SENDSPIN_ID]) + await media_player.register_media_player(var, config) + + cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT])) diff --git a/esphome/components/sendspin/media_player/sendspin_media_player.cpp b/esphome/components/sendspin/media_player/sendspin_media_player.cpp new file mode 100644 index 00000000000..beb20286896 --- /dev/null +++ b/esphome/components/sendspin/media_player/sendspin_media_player.cpp @@ -0,0 +1,165 @@ +#include "sendspin_media_player.h" + +#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER) + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#include <sendspin/types.h> + +#include <algorithm> +#include <cmath> +#include <memory> +#include <optional> + +#include <esp_timer.h> + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.media_player"; + +// THREAD CONTEXT: Main loop. The callbacks registered here also fire on the main loop, +// since SendspinHub dispatches group updates and controller state from client_->loop(). +void SendspinMediaPlayer::setup() { + // Register for group updates to sync playback state + this->parent_->add_group_update_callback([this](const sendspin::GroupUpdateObject &group_obj) { + if (group_obj.playback_state.has_value()) { + media_player::MediaPlayerState new_state; + switch (group_obj.playback_state.value()) { + case sendspin::SendspinPlaybackState::PLAYING: + new_state = media_player::MEDIA_PLAYER_STATE_PLAYING; + break; + case sendspin::SendspinPlaybackState::STOPPED: + default: + new_state = media_player::MEDIA_PLAYER_STATE_IDLE; + break; + } + if (this->state != new_state) { + this->state = new_state; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } + } + }); + + this->parent_->add_controller_state_callback([this](const sendspin::ServerStateControllerObject &state) { + float new_volume = static_cast<float>(state.volume) / 100.0f; + bool new_muted = state.muted; + if ((new_volume != this->volume) || (new_muted != this->muted_)) { + this->volume = new_volume; + this->muted_ = new_muted; + this->publish_state(); + } + }); + + // Publish an initial state + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + this->publish_state(); +} + +// THREAD CONTEXT: Main loop (invoked by the media_player framework) +media_player::MediaPlayerTraits SendspinMediaPlayer::get_traits() { + auto traits = media_player::MediaPlayerTraits(); + + // By default, the base media player always enables these traits, but they are not actually supported by this media + // player + traits.clear_feature_flags(media_player::MediaPlayerEntityFeature::PLAY_MEDIA | + media_player::MediaPlayerEntityFeature::BROWSE_MEDIA | + media_player::MediaPlayerEntityFeature::MEDIA_ANNOUNCE); + + traits.add_feature_flags( + media_player::MediaPlayerEntityFeature::PLAY | media_player::MediaPlayerEntityFeature::PAUSE | + media_player::MediaPlayerEntityFeature::STOP | media_player::MediaPlayerEntityFeature::VOLUME_STEP | + media_player::MediaPlayerEntityFeature::VOLUME_SET | media_player::MediaPlayerEntityFeature::VOLUME_MUTE); + + // NEXT_TRACK, PREVIOUS_TRACK, SHUFFLE_SET, and REPEAT_SET are intentionally not advertised: the ESPHome native API + // does not implement the corresponding media player commands, so Home Assistant cannot actually send them even if + // we expose the capability. They remain accessible via ESPHome YAML automations. + + return traits; +} + +// THREAD CONTEXT: Main loop (invoked by the media_player framework) +void SendspinMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (!this->is_ready()) { + // Ignore any commands sent before the media player is setup + return; + } + + auto volume = call.get_volume(); + if (volume.has_value()) { + uint8_t new_volume = static_cast<uint8_t>(std::roundf(volume.value() * 100.0f)); + this->parent_->send_client_command(sendspin::SendspinControllerCommand::VOLUME, new_volume, std::nullopt); + } + + auto command = call.get_command(); + if (!command.has_value()) { + return; + } + switch (command.value()) { + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: + if (this->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING) { + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE); + } else { + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY); + } + break; + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY); + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE); + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::STOP); + break; + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_OFF); + break; + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ONE); + break; + case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ALL: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ALL); + break; + case media_player::MEDIA_PLAYER_COMMAND_SHUFFLE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::SHUFFLE); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNSHUFFLE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::UNSHUFFLE); + break; + case media_player::MEDIA_PLAYER_COMMAND_NEXT: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::NEXT); + break; + case media_player::MEDIA_PLAYER_COMMAND_PREVIOUS: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PREVIOUS); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: + this->parent_->send_client_command( + sendspin::SendspinControllerCommand::VOLUME, + static_cast<uint8_t>(std::roundf(std::min(1.0f, this->volume + this->volume_increment_) * 100.0f)), + std::nullopt); + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + this->parent_->send_client_command( + sendspin::SendspinControllerCommand::VOLUME, + static_cast<uint8_t>(std::roundf(std::max(0.0f, this->volume - this->volume_increment_) * 100.0f)), + std::nullopt); + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::MUTE, std::nullopt, true); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::MUTE, std::nullopt, false); + break; + default: + break; + } +} + +void SendspinMediaPlayer::dump_config() { + ESP_LOGCONFIG(TAG, "Sendspin Media Player: volume_increment=%.2f", this->volume_increment_); +} + +} // namespace esphome::sendspin_ +#endif diff --git a/esphome/components/sendspin/media_player/sendspin_media_player.h b/esphome/components/sendspin/media_player/sendspin_media_player.h new file mode 100644 index 00000000000..52786d6d7b3 --- /dev/null +++ b/esphome/components/sendspin/media_player/sendspin_media_player.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_MEDIA_PLAYER) && defined(USE_SENDSPIN_CONTROLLER) + +#include "esphome/components/media_player/media_player.h" +#include "esphome/components/sendspin/sendspin_hub.h" + +namespace esphome::sendspin_ { + +class SendspinMediaPlayer : public SendspinChild, public media_player::MediaPlayer { + public: + void setup() override; + void dump_config() override; + + // MediaPlayer implementations + media_player::MediaPlayerTraits get_traits() override; + + void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; } + + bool is_muted() const override { return this->muted_; } + + protected: + // Receives commands from HA + void control(const media_player::MediaPlayerCall &call) override; + + float volume_increment_{0.05f}; + bool muted_{false}; +}; + +} // namespace esphome::sendspin_ +#endif diff --git a/tests/components/sendspin/common-media_player.yaml b/tests/components/sendspin/common-media_player.yaml new file mode 100644 index 00000000000..d3792cf4708 --- /dev/null +++ b/tests/components/sendspin/common-media_player.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +media_player: + - platform: sendspin + id: media_player_id diff --git a/tests/components/sendspin/test-media_player.esp32-idf.yaml b/tests/components/sendspin/test-media_player.esp32-idf.yaml new file mode 100644 index 00000000000..cbbdb07c77c --- /dev/null +++ b/tests/components/sendspin/test-media_player.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-media_player.yaml From 404620b99cc805225c328ce49d81a0fe4e07dff1 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Fri, 24 Apr 2026 04:31:46 +0200 Subject: [PATCH 1634/2056] [deep_sleep][logger][zephyr][zigbee] add deep sleep support with zigbee wakeup (#13950) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/deep_sleep/__init__.py | 6 +- .../deep_sleep/deep_sleep_bk72xx.cpp | 2 + .../deep_sleep/deep_sleep_component.cpp | 27 +++++++-- .../deep_sleep/deep_sleep_component.h | 16 +++++ .../deep_sleep/deep_sleep_esp32.cpp | 2 + .../deep_sleep/deep_sleep_esp8266.cpp | 2 + .../deep_sleep/deep_sleep_zephyr.cpp | 60 +++++++++++++++++++ esphome/components/logger/__init__.py | 17 +++--- esphome/components/logger/logger_zephyr.cpp | 2 + esphome/components/zephyr/__init__.py | 26 +++++++- esphome/components/zephyr/const.py | 1 + esphome/components/zigbee/__init__.py | 4 ++ esphome/components/zigbee/const_zephyr.py | 1 + esphome/components/zigbee/zigbee_zephyr.cpp | 25 +++++++- esphome/components/zigbee/zigbee_zephyr.h | 4 ++ esphome/components/zigbee/zigbee_zephyr.py | 8 +++ .../deep_sleep/test.nrf52-adafruit.yaml | 12 ++++ .../zigbee/test.nrf52-xiao-ble.yaml | 1 + 18 files changed, 196 insertions(+), 20 deletions(-) create mode 100644 esphome/components/deep_sleep/deep_sleep_zephyr.cpp create mode 100644 tests/components/deep_sleep/test.nrf52-adafruit.yaml diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 16329bb0fa0..8184f954c74 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, get_esp32_variant, ) +from esphome.components.zephyr import zephyr_add_prj_conf from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -33,6 +34,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PlatformFramework, ) from esphome.core import CORE @@ -304,7 +306,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]), validate_config, ) @@ -369,6 +371,8 @@ async def to_code(config): if CONF_TOUCH_WAKEUP in config: cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) + if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations: + zephyr_add_prj_conf("POWEROFF", True) cg.add_define("USE_DEEP_SLEEP") diff --git a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp index b5fadd7230e..8dca32689bd 100644 --- a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp +++ b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp @@ -59,6 +59,8 @@ void DeepSleepComponent::deep_sleep_() { lt_deep_sleep_enter(); } +bool DeepSleepComponent::should_teardown_() { return true; } + } // namespace esphome::deep_sleep #endif // USE_BK72XX diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 3dd1b709308..d2c5db54b39 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -9,11 +9,22 @@ static const char *const TAG = "deep_sleep"; // 5 seconds for deep sleep to ensure clean disconnect from Home Assistant static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000; -bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void DeepSleepComponent::setup() { +#ifdef USE_ZEPHYR + k_sem_init(&this->wakeup_sem_, 0, 1); +#endif global_has_deep_sleep = true; + this->schedule_sleep_(); + // It can be used from another thread for waking up the device. + // It should be called as last item in setup. + global_deep_sleep.store(this); +} +void DeepSleepComponent::schedule_sleep_() { + this->next_enter_deep_sleep_ = false; const optional<uint32_t> run_duration = get_run_duration_(); if (run_duration.has_value()) { ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration); @@ -58,13 +69,17 @@ void DeepSleepComponent::begin_sleep(bool manual) { if (this->sleep_duration_.has_value()) { ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_); } - App.run_safe_shutdown_hooks(); - // It's critical to teardown components cleanly for deep sleep to ensure - // Home Assistant sees a clean disconnect instead of marking the device unavailable - App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS); - App.run_powerdown_hooks(); + + if (this->should_teardown_()) { + App.run_safe_shutdown_hooks(); + // It's critical to teardown components cleanly for deep sleep to ensure + // Home Assistant sees a clean disconnect instead of marking the device unavailable + App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS); + App.run_powerdown_hooks(); + } this->deep_sleep_(); + this->schedule_sleep_(); } float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 9090f91876a..854ab152a16 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -4,6 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include <atomic> #ifdef USE_ESP32 #include <esp_sleep.h> @@ -14,6 +15,10 @@ #include "esphome/core/time.h" #endif +#ifdef USE_ZEPHYR +#include <zephyr/kernel.h> +#endif + #include <cinttypes> namespace esphome { @@ -120,6 +125,9 @@ class DeepSleepComponent : public Component { void prevent_deep_sleep(); void allow_deep_sleep(); +#ifdef USE_ZEPHYR + void wakeup(); +#endif protected: // Returns nullopt if no run duration is set. Otherwise, returns the run @@ -129,6 +137,8 @@ class DeepSleepComponent : public Component { void dump_config_platform_(); bool prepare_to_sleep_(); void deep_sleep_(); + void schedule_sleep_(); + bool should_teardown_(); #ifdef USE_BK72XX bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const; @@ -157,6 +167,9 @@ class DeepSleepComponent : public Component { optional<uint32_t> run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; +#ifdef USE_ZEPHYR + k_sem wakeup_sem_; +#endif }; extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -243,5 +256,8 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; +extern std::atomic<DeepSleepComponent *> + global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + } // namespace deep_sleep } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 4f4d262d30d..80a218e9133 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -165,6 +165,8 @@ void DeepSleepComponent::deep_sleep_() { esp_deep_sleep_start(); } +bool DeepSleepComponent::should_teardown_() { return true; } + } // namespace deep_sleep } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index efbd45c34e7..42c153c2f38 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -18,6 +18,8 @@ void DeepSleepComponent::deep_sleep_() { ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance) } +bool DeepSleepComponent::should_teardown_() { return true; } + } // namespace deep_sleep } // namespace esphome #endif diff --git a/esphome/components/deep_sleep/deep_sleep_zephyr.cpp b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp new file mode 100644 index 00000000000..82d6d8c7ded --- /dev/null +++ b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp @@ -0,0 +1,60 @@ +#include "deep_sleep_component.h" +#ifdef USE_ZEPHYR +#include "esphome/core/log.h" +#include <zephyr/sys/poweroff.h> +#include <zephyr/kernel.h> +#include <zephyr/stats/stats.h> +#include <zephyr/pm/pm.h> + +namespace esphome::deep_sleep { + +static const char *const TAG = "deep_sleep"; + +void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); } + +optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } + +void DeepSleepComponent::dump_config_platform_() {} + +bool DeepSleepComponent::prepare_to_sleep_() { return true; } + +void DeepSleepComponent::deep_sleep_() { + k_timeout_t sleep_duration = K_FOREVER; + if (this->sleep_duration_.has_value()) { + sleep_duration = K_USEC(*this->sleep_duration_); + } else { +#ifndef USE_ZIGBEE + // the device can be woken up through one of the following signals: + // - The DETECT signal, optionally generated by the GPIO peripheral. + // - The ANADETECT signal, optionally generated by the LPCOMP module. + // - The SENSE signal, optionally generated by the NFC module to wake-on-field. + // - Detecting a valid USB voltage on the VBUS pin (VBUS,DETECT). + // - A reset. + // + // The system is reset when it wakes up from System OFF mode. + sys_poweroff(); +#endif + } + // It might wake up immediately if k_sem_give was called again after wake up + int ret = k_sem_take(&this->wakeup_sem_, sleep_duration); + if (ret == 0) { + ESP_LOGD(TAG, "Woken up by another thread"); + } else { + ESP_LOGD(TAG, "Timeout expired (normal sleep)"); + } +} + +bool DeepSleepComponent::should_teardown_() { + if (this->sleep_duration_.has_value()) { + return false; + } +#ifdef USE_ZIGBEE + return false; +#else + return true; +#endif +} + +} // namespace esphome::deep_sleep + +#endif diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 4144543b89a..9d7dc8d92c1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -472,14 +472,15 @@ async def _late_logger_init(config: ConfigType) -> None: # esphome implement own fatal error handler which save PC/LR before reset zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False) zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True) - if config[CONF_HARDWARE_UART] == UART0: - zephyr_add_overlay("""&uart0 { status = "okay";};""") - if config[CONF_HARDWARE_UART] == UART1: - zephyr_add_overlay("""&uart1 { status = "okay";};""") - if config[CONF_HARDWARE_UART] == USB_CDC: - cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") - zephyr_add_prj_conf("UART_LINE_CTRL", True) - zephyr_add_cdc_acm(config, 0) + if has_serial_logging: + if config[CONF_HARDWARE_UART] == UART0: + zephyr_add_overlay("""&uart0 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == UART1: + zephyr_add_overlay("""&uart1 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") + zephyr_add_prj_conf("UART_LINE_CTRL", True) + zephyr_add_cdc_acm(config, 0) # Register at end for safe mode await cg.register_component(log, config) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 6b46b93c61e..7fa9e42c6a0 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -65,10 +65,12 @@ void Logger::pre_setup() { break; #ifdef USE_LOGGER_USB_CDC case UART_SELECTION_USB_CDC: +#ifdef CONFIG_USB_DEVICE_STACK uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); if (device_is_ready(uart_dev)) { usb_enable(nullptr); } +#endif break; #endif } diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index d3cc6b2cf45..5dccecc0974 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -15,6 +15,7 @@ from .const import ( KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, + KEY_KCONFIG, KEY_OVERLAY, KEY_PM_STATIC, KEY_PRJ_CONF, @@ -54,6 +55,7 @@ class ZephyrData(TypedDict): extra_build_files: dict[str, Path] pm_static: list[Section] user: dict[str, list[str]] + kconfig: str def zephyr_set_core_data(config: ConfigType) -> None: @@ -65,6 +67,7 @@ def zephyr_set_core_data(config: ConfigType) -> None: extra_build_files={}, pm_static=[], user={}, + kconfig="", ) @@ -185,8 +188,12 @@ def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None: ) -def zephyr_add_pm_static(section: Section): - CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section) +def zephyr_add_kconfig(kconfig: str) -> None: + zephyr_data()[KEY_KCONFIG] += textwrap.dedent(kconfig) + "\n" + + +def zephyr_add_pm_static(sections: list[Section]) -> None: + zephyr_data()[KEY_PM_STATIC].extend(sections) def zephyr_add_user(key, value): @@ -273,3 +280,18 @@ def copy_files(): write_file_if_changed( CORE.relative_build_path("zephyr/pm_static.yml"), pm_static ) + + kconfig = zephyr_data()[KEY_KCONFIG] + if kconfig: + kconfig = ( + textwrap.dedent( + """ + menu "Zephyr" + source "Kconfig.zephyr" + endmenu + """ + ) + + "\n" + + kconfig + ) + write_file_if_changed(CORE.relative_build_path("zephyr/Kconfig"), kconfig) diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py index f67b058ed78..f2de861e314 100644 --- a/esphome/components/zephyr/const.py +++ b/esphome/components/zephyr/const.py @@ -8,6 +8,7 @@ KEY_BOOTLOADER: Final = "bootloader" KEY_EXTRA_BUILD_FILES: Final = "extra_build_files" KEY_OVERLAY: Final = "overlay" KEY_PM_STATIC: Final = "pm_static" +KEY_KCONFIG: Final = "kconfig" KEY_PRJ_CONF: Final = "prj_conf" KEY_ZEPHYR = "zephyr" KEY_BOARD: Final = "board" diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 126e3aa2cd1..0bb5f95bb68 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -32,6 +32,7 @@ from .const import ( from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, CONF_MAX_EP_NUMBER, + CONF_SLEEPY, CONF_ZIGBEE_ID, KEY_EP_NUMBER, ) @@ -107,6 +108,9 @@ CONFIG_SCHEMA = cv.All( ), cv.requires_component("nrf52"), ), + cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All( + cv.boolean, + ), } ).extend(cv.COMPONENT_SCHEMA), zigbee_require_vfs_select, diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 103ef01a3d8..63d03c7952b 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -4,6 +4,7 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SWITCH = "zigbee_switch" CONF_ZIGBEE_NUMBER = "zigbee_number" +CONF_SLEEPY = "sleepy" CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui" # Keys for CORE.data storage diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 047c30300e2..90bb66c91d5 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -4,6 +4,9 @@ #include <zephyr/settings/settings.h> #include <zephyr/storage/flash_map.h> #include "esphome/core/hal.h" +#ifdef USE_DEEP_SLEEP +#include "esphome/components/deep_sleep/deep_sleep_component.h" +#endif extern "C" { #include <zboss_api.h> @@ -116,6 +119,12 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { /* Set default response value. */ p_device_cb_param->status = RET_OK; +#ifdef USE_DEEP_SLEEP + if (auto *ds = deep_sleep::global_deep_sleep.load()) { + ds->wakeup(); + } +#endif + // endpoints are enumerated from 1 if (global_zigbee->callbacks_.size() >= endpoint) { const auto &cb = global_zigbee->callbacks_[endpoint - 1]; @@ -181,9 +190,11 @@ void ZigbeeComponent::setup() { ESP_LOGE(TAG, "Cannot load settings, err: %d", err); return; } + zigbee_configure_sleepy_behavior(this->sleepy_); zigbee_enable(); } +#ifdef ESPHOME_LOG_HAS_CONFIG static const char *role() { switch (zb_get_network_role()) { case ZB_NWK_DEVICE_TYPE_COORDINATOR: @@ -207,6 +218,7 @@ static const char *get_wipe_on_boot() { return "NO"; #endif } +#endif void ZigbeeComponent::dump_config() { char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0}; @@ -222,6 +234,7 @@ void ZigbeeComponent::dump_config() { " Wipe on boot: %s\n" " Device is joined to the network: %s\n" " Sleep time: %us\n" + " RX ON when idle: %s\n" " Current channel: %d\n" " Current page: %d\n" " Sleep threshold: %ums\n" @@ -230,9 +243,9 @@ void ZigbeeComponent::dump_config() { " Short addr: 0x%04X\n" " Long pan id: 0x%s\n" " Short pan id: 0x%04X", - get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(), - zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), - extended_pan_id_buf, zb_get_pan_id()); + get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, YESNO(zb_get_rx_on_when_idle()), + zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, + zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id()); dump_reporting_(); } @@ -302,6 +315,12 @@ void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *con extern "C" { void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); } +void zb_osif_serial_put_bytes(const zb_uint8_t *buf, zb_short_t len) { + (void) buf; + (void) len; +} +void zb_osif_serial_flush() {} +void zb_osif_serial_init() {} // NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req, diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index eeb142eff17..0a189ac1e04 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -81,6 +81,7 @@ class ZigbeeComponent : public Component { Trigger<> *get_join_trigger() { return &this->join_trigger_; }; void force_report(); void loop() override; + void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; } protected: static void zcl_device_cb(zb_bufid_t bufid); @@ -95,6 +96,7 @@ class ZigbeeComponent : public Component { bool force_report_{false}; uint32_t sleep_time_{}; uint32_t sleep_remainder_{}; + bool sleepy_{}; }; class ZigbeeEntity { @@ -107,5 +109,7 @@ class ZigbeeEntity { ZigbeeComponent *parent_{nullptr}; }; +extern ZigbeeComponent *global_zigbee; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + } // namespace esphome::zigbee #endif diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index f6e3e88c63a..7d904b6081d 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -63,6 +63,7 @@ from .const import ( ) from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, + CONF_SLEEPY, CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_ID, CONF_ZIGBEE_NUMBER, @@ -169,6 +170,11 @@ async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) zephyr_add_prj_conf("NET_UDP", False) + # disable all extra to reduce power and save flash + zephyr_add_prj_conf("ZIGBEE_HAVE_SERIAL", False) + zephyr_add_prj_conf("ZBOSS_ERROR_PRINT_TO_LOG", False) + zephyr_add_prj_conf("DK_LIBRARY", False) + cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req") if CONF_IEEE802154_VENDOR_OUI in config: @@ -200,6 +206,8 @@ async def zephyr_to_code(config: ConfigType) -> None: CORE.add_job(_ctx_to_code, config) + cg.add(var.set_sleepy(config[CONF_SLEEPY])) + async def _attr_to_code(config: ConfigType) -> None: # Create the basic attributes structure and attribute list diff --git a/tests/components/deep_sleep/test.nrf52-adafruit.yaml b/tests/components/deep_sleep/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..6362142be2e --- /dev/null +++ b/tests/components/deep_sleep/test.nrf52-adafruit.yaml @@ -0,0 +1,12 @@ +deep_sleep: + run_duration: 10s + sleep_duration: 50s + +<<: !include common.yaml + +zigbee: + +sensor: + - platform: template + name: "Temperature" + id: temperature_sensor diff --git a/tests/components/zigbee/test.nrf52-xiao-ble.yaml b/tests/components/zigbee/test.nrf52-xiao-ble.yaml index 83d949b4ddc..acfbc9e9961 100644 --- a/tests/components/zigbee/test.nrf52-xiao-ble.yaml +++ b/tests/components/zigbee/test.nrf52-xiao-ble.yaml @@ -4,3 +4,4 @@ zigbee: wipe_on_boot: once power_source: battery ieee802154_vendor_oui: 0x231 + sleepy: true From eceb534895dcaa1c9c77c906500799fefeb4f6de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 02:19:59 -0500 Subject: [PATCH 1635/2056] [deep_sleep] Fix sleep_duration codegen type to uint32_t (#15965) --- esphome/components/deep_sleep/__init__.py | 2 +- tests/components/deep_sleep/common.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 8184f954c74..0ca557bd6d8 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -417,7 +417,7 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_SLEEP_DURATION in config: - template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) + template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32) cg.add(var.set_sleep_duration(template_)) if CONF_UNTIL in config: diff --git a/tests/components/deep_sleep/common.yaml b/tests/components/deep_sleep/common.yaml index c090cb83e2d..7a1a709965b 100644 --- a/tests/components/deep_sleep/common.yaml +++ b/tests/components/deep_sleep/common.yaml @@ -4,3 +4,9 @@ esphome: - deep_sleep.prevent - delay: 1s - deep_sleep.allow + - if: + condition: + lambda: 'return false;' + then: + - deep_sleep.enter: + sleep_duration: 60min From ae02ab38656f484e55468302015016a9a59440a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 03:42:36 -0500 Subject: [PATCH 1636/2056] [wifi] Fix stale wifi.connected after state transition (#15966) --- esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ esphome/components/wifi/wifi_component_libretiny.cpp | 2 ++ esphome/components/wifi/wifi_component_pico_w.cpp | 2 ++ 5 files changed, 10 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 481846085c2..f7c70b1147a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1579,6 +1579,8 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { #endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; + // Refresh is_connected() cache; loop()'s refresh ran before this transition. + this->update_connected_state_(); this->num_retried_ = 0; this->print_connect_params_(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index e56a8df350b..bf3a0d29497 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -951,6 +951,8 @@ void WiFiComponent::process_pending_callbacks_() { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS if (this->pending_.disconnect) { this->pending_.disconnect = false; + // Refresh is_connected() cache here, not in the SDK callback (sys context). + this->update_connected_state_(); this->notify_disconnect_state_listeners_(); } #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index c790742c797..29d135ce900 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -804,6 +804,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; + // Refresh is_connected() cache; error_from_callback_ makes it false. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 6588e93e167..59efa4f8425 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -530,6 +530,8 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { this->error_from_callback_ = true; } + // Refresh is_connected() cache; sta_state_/error_from_callback_ make it false. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 4e1e0395c03..596fd2729b3 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -342,6 +342,8 @@ bool WiFiComponent::wifi_loop_() { s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); + // Refresh is_connected() cache; driver link status reports disconnected. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif From bc7f35b569c0dcec0364f7bf5f53fa19857ce572 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 06:00:22 -0400 Subject: [PATCH 1637/2056] [sendspin] Add a Sendspin media source component for playing audio (PR4) (#15950) Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- CODEOWNERS | 1 + esphome/components/sendspin/__init__.py | 78 ++++++- .../sendspin/media_source/__init__.py | 134 ++++++++++++ .../sendspin/media_source/automations.h | 26 +++ .../media_source/sendspin_media_source.cpp | 207 ++++++++++++++++++ .../media_source/sendspin_media_source.h | 72 ++++++ esphome/components/sendspin/sendspin_hub.cpp | 40 ++++ esphome/components/sendspin/sendspin_hub.h | 28 +++ .../sendspin/common-media_source.yaml | 9 + .../sendspin/test-media_source.esp32-idf.yaml | 1 + 10 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sendspin/media_source/__init__.py create mode 100644 esphome/components/sendspin/media_source/automations.h create mode 100644 esphome/components/sendspin/media_source/sendspin_media_source.cpp create mode 100644 esphome/components/sendspin/media_source/sendspin_media_source.h create mode 100644 tests/components/sendspin/common-media_source.yaml create mode 100644 tests/components/sendspin/test-media_source.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 65db6ca25ed..822b0e973c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -442,6 +442,7 @@ esphome/components/sen5x/* @martgras esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sendspin/* @kahrendt esphome/components/sendspin/media_player/* @kahrendt +esphome/components/sendspin/media_source/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/serial_proxy/* @kbx81 diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 2d053903789..6f5ccddb86d 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -4,7 +4,12 @@ from esphome import automation import esphome.codegen as cg from esphome.components import esp32, network, psram, socket, wifi import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_ID, + CONF_SAMPLE_RATE, + CONF_TASK_STACK_IN_PSRAM, +) from esphome.core import CORE, ID from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType @@ -17,6 +22,23 @@ DOMAIN = "sendspin" CONF_SENDSPIN_ID = "sendspin_id" +CONF_INITIAL_STATIC_DELAY = "initial_static_delay" +CONF_FIXED_DELAY = "fixed_delay" + +# sendspin-cpp library lives in the global `sendspin` namespace. +sendspin_library_ns = cg.global_ns.namespace("sendspin") + +# Library Enums +SendspinCodecFormat = sendspin_library_ns.enum("SendspinCodecFormat", is_class=True) +CODEC_FORMAT_FLAC = SendspinCodecFormat.enum("FLAC") +CODEC_FORMAT_OPUS = SendspinCodecFormat.enum("OPUS") +CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM") +CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED") + +# Library Structs +AudioSupportedFormatObject = sendspin_library_ns.struct("AudioSupportedFormatObject") +PlayerRoleConfig = sendspin_library_ns.struct("PlayerRoleConfig") + # Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace. # Analysis tools strip the trailing underscore (same pattern as `template_`). sendspin_ns = cg.esphome_ns.namespace("sendspin_") @@ -41,6 +63,8 @@ class SendspinConfiguration: player_support: bool = False visualizer_support: bool = False + player_config: ConfigType | None = None + def _get_data() -> SendspinConfiguration: if DOMAIN not in CORE.data: @@ -73,6 +97,17 @@ def request_visualizer_support() -> None: _get_data().visualizer_support = True +def register_player_config(config: ConfigType) -> None: + """Register the player role config from the media source subcomponent.""" + data = _get_data() + request_player_support() + if data.player_config is not None: + raise cv.Invalid( + "Only one sendspin media_source player configuration is supported" + ) + data.player_config = config + + def _validate_task_stack_in_psram(value): value = cv.boolean(value) if value: @@ -183,6 +218,47 @@ async def to_code(config: ConfigType) -> None: if data.player_support: cg.add_define("USE_SENDSPIN_PLAYER", True) + + # Configures the player role. We always assume support for 16 bits per sample mono and stereo FLAC, Opus, and PCM at the configured sample rate + # (with Opus only supported at 48 kHz since that's the only sample rate it supports). Users can configure the specific formats via the Sendspin server + player_cfg = data.player_config + sample_rate = player_cfg[CONF_SAMPLE_RATE] + + # OPUS only supports 48 kHz audio + codecs = [CODEC_FORMAT_FLAC] + if sample_rate == 48000: + codecs.append(CODEC_FORMAT_OPUS) + codecs.append(CODEC_FORMAT_PCM) + + def _audio_format(codec, channels): + return cg.StructInitializer( + AudioSupportedFormatObject, + ("codec", codec), + ("channels", channels), + ("sample_rate", sample_rate), + ("bit_depth", 16), + ) + + audio_format_structs = [ + _audio_format(codec, channels) for codec in codecs for channels in (2, 1) + ] + + psram_stack = player_cfg.get(CONF_TASK_STACK_IN_PSRAM, False) + if psram_stack: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) + + player_config_struct = cg.StructInitializer( + PlayerRoleConfig, + ("audio_formats", audio_format_structs), + ("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]), + ("fixed_delay_us", player_cfg[CONF_FIXED_DELAY]), + ("initial_static_delay_ms", player_cfg[CONF_INITIAL_STATIC_DELAY]), + ("psram_stack", psram_stack), + ("priority", 2), + ) + cg.add(var.set_player_config(player_config_struct)) else: esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_PLAYER", False) diff --git a/esphome/components/sendspin/media_source/__init__.py b/esphome/components/sendspin/media_source/__init__.py new file mode 100644 index 00000000000..6d61a8a6361 --- /dev/null +++ b/esphome/components/sendspin/media_source/__init__.py @@ -0,0 +1,134 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import media_source +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_ID, + CONF_SAMPLE_RATE, + CONF_TASK_STACK_IN_PSRAM, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import ( + CONF_FIXED_DELAY, + CONF_INITIAL_STATIC_DELAY, + CONF_SENDSPIN_ID, + SendspinHub, + _validate_task_stack_in_psram, + register_player_config, + request_controller_support, + sendspin_ns, +) + +AUTO_LOAD = ["audio"] +CODEOWNERS = ["@kahrendt"] + +CONF_STATIC_DELAY_ADJUSTABLE = "static_delay_adjustable" + + +SendspinMediaSource = sendspin_ns.class_( + "SendspinMediaSource", + cg.Component, + media_source.MediaSource, +) + +EnableStaticDelayAdjustmentAction = sendspin_ns.class_( + "EnableStaticDelayAdjustmentAction", + automation.Action, + cg.Parented.template(SendspinMediaSource), +) + +DisableStaticDelayAdjustmentAction = sendspin_ns.class_( + "DisableStaticDelayAdjustmentAction", + automation.Action, + cg.Parented.template(SendspinMediaSource), +) + + +def _register(config: ConfigType) -> ConfigType: + request_controller_support() + register_player_config( + { + CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE], + CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], + CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY], + CONF_FIXED_DELAY: config[CONF_FIXED_DELAY], + CONF_TASK_STACK_IN_PSRAM: config.get(CONF_TASK_STACK_IN_PSRAM, False), + } + ) + return config + + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + SendspinMediaSource, + ).extend( + { + cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub), + cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram, + cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(min=25000), + cv.Optional(CONF_INITIAL_STATIC_DELAY, default="0ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=5000)), + ), + cv.Optional(CONF_STATIC_DELAY_ADJUSTABLE, default=False): cv.boolean, + cv.Optional(CONF_FIXED_DELAY, default="0us"): cv.All( + cv.positive_time_period_microseconds, + cv.Range(max=cv.TimePeriod(microseconds=10000)), + ), + cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.int_range( + min=16000, max=96000 + ), + } + ), + cv.only_on_esp32, + _register, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + sendspin_hub = await cg.get_variable(config[CONF_SENDSPIN_ID]) + await cg.register_parented(var, sendspin_hub) + + cg.add(sendspin_hub.set_listener(var)) + + cg.add(var.set_static_delay_adjustable(config[CONF_STATIC_DELAY_ADJUSTABLE])) + + +SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA = automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(SendspinMediaSource), + } + ) +) + + +@automation.register_action( + "sendspin.media_source.enable_static_delay_adjustment", + EnableStaticDelayAdjustmentAction, + SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, + synchronous=True, +) +@automation.register_action( + "sendspin.media_source.disable_static_delay_adjustment", + DisableStaticDelayAdjustmentAction, + SENDSPIN_MEDIA_SOURCE_ACTION_SCHEMA, + synchronous=True, +) +async def sendspin_static_delay_adjustment_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/sendspin/media_source/automations.h b/esphome/components/sendspin/media_source/automations.h new file mode 100644 index 00000000000..08d2b2004b1 --- /dev/null +++ b/esphome/components/sendspin/media_source/automations.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_PLAYER) && defined(USE_SENDSPIN_CONTROLLER) + +#include "esphome/core/automation.h" +#include "sendspin_media_source.h" + +namespace esphome::sendspin_ { + +template<typename... Ts> +class EnableStaticDelayAdjustmentAction : public Action<Ts...>, public Parented<SendspinMediaSource> { + public: + void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(true); } +}; + +template<typename... Ts> +class DisableStaticDelayAdjustmentAction : public Action<Ts...>, public Parented<SendspinMediaSource> { + public: + void play(const Ts &...x) override { this->parent_->set_static_delay_adjustable(false); } +}; + +} // namespace esphome::sendspin_ + +#endif diff --git a/esphome/components/sendspin/media_source/sendspin_media_source.cpp b/esphome/components/sendspin/media_source/sendspin_media_source.cpp new file mode 100644 index 00000000000..0fdfb01c55f --- /dev/null +++ b/esphome/components/sendspin/media_source/sendspin_media_source.cpp @@ -0,0 +1,207 @@ +#include "sendspin_media_source.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER) + +#include "esphome/components/audio/audio.h" +#include "esphome/core/log.h" + +#include <cmath> + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.media_source"; + +static constexpr char URI_PREFIX[] = "sendspin://"; + +void SendspinMediaSource::setup() { + this->player_role_ = this->parent_->get_player_role(); + if (!this->player_role_) { + ESP_LOGE(TAG, "Failed to get player role from hub"); + this->mark_failed(); + return; + } + + // Push cached states to player role. They may have been set before setup() ran. + this->player_role_->update_volume(std::roundf(this->cached_volume_ * 100.0f)); + this->player_role_->update_muted(this->cached_muted_); + this->player_role_->set_static_delay_adjustable(this->static_delay_adjustable_); +} + +void SendspinMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, "Sendspin Media Source: static_delay_adjustable=%s", YESNO(this->static_delay_adjustable_)); +} + +// THREAD CONTEXT: Main loop (invoked from ESPHome actions / config) +void SendspinMediaSource::set_static_delay_adjustable(bool adjustable) { + this->static_delay_adjustable_ = adjustable; + if (this->player_role_) { + this->player_role_->set_static_delay_adjustable(adjustable); + } +} + +// --- MediaSource interface --- + +bool SendspinMediaSource::can_handle(const std::string &uri) const { return uri.starts_with(URI_PREFIX); } + +// THREAD CONTEXT: Main loop (media_source.h documents play_uri as main-loop only) +bool SendspinMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || !this->has_listener()) { + return false; + } + + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + if (!uri.starts_with(URI_PREFIX)) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + std::string sendspin_id = uri.substr(sizeof(URI_PREFIX) - 1); + + if (sendspin_id.empty()) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + ESP_LOGD(TAG, "sendspin_id: %s", sendspin_id.c_str()); + + if (sendspin_id != "current") { + // Connect to a new server as a websocket client + this->parent_->connect_to_server("ws://" + sendspin_id); + } + + // Tell the orchestrator we're now playing so it routes audio output from us + this->pending_start_ = false; + this->set_state_(media_source::MediaSourceState::PLAYING); + + return true; +} + +// THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only) +void SendspinMediaSource::handle_command(media_source::MediaSourceCommand command) { + switch (command) { + case media_source::MediaSourceCommand::STOP: { + if (!this->pending_start_) { + // Ignore stop commands if we have a pending start, since the orchestrator may send a stop command before + // play_uri + ESP_LOGD(TAG, "Received STOP command, updating Sendspin state to EXTERNAL_SOURCE"); + this->parent_->update_state(sendspin::SendspinClientState::EXTERNAL_SOURCE); + } + break; + } + case media_source::MediaSourceCommand::PLAY: // NOLINT(bugprone-branch-clone) + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PLAY, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::PAUSE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PAUSE, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::NEXT: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::NEXT, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::PREVIOUS: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::PREVIOUS, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::REPEAT_ALL: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ALL, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::REPEAT_ONE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_ONE, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::REPEAT_OFF: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::REPEAT_OFF, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::SHUFFLE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::SHUFFLE, std::nullopt, std::nullopt); + break; + case media_source::MediaSourceCommand::UNSHUFFLE: + this->parent_->send_client_command(sendspin::SendspinControllerCommand::UNSHUFFLE, std::nullopt, std::nullopt); + break; + default: + break; + } +} + +// THREAD CONTEXT: Main loop (orchestrator -> source notification) +void SendspinMediaSource::notify_volume_changed(float volume) { + this->cached_volume_ = volume; + if (this->player_role_) { + this->player_role_->update_volume(std::roundf(volume * 100.0f)); + } +} + +// THREAD CONTEXT: Main loop (orchestrator -> source notification) +void SendspinMediaSource::notify_mute_changed(bool is_muted) { + this->cached_muted_ = is_muted; + if (this->player_role_) { + this->player_role_->update_muted(is_muted); + } +} + +// THREAD CONTEXT: Speaker playback callback thread (forwarded from the speaker). +// PlayerRole::notify_audio_played() is documented as thread-safe for this use. +void SendspinMediaSource::notify_audio_played(uint32_t frames, int64_t timestamp) { + if (this->player_role_) { + this->player_role_->notify_audio_played(frames, timestamp); + } +} + +// --- Sendspin PlayerRoleListener overrides --- + +// THREAD CONTEXT: Sendspin sync task background thread. May block up to timeout_ms. +size_t SendspinMediaSource::on_audio_write(uint8_t *data, size_t length, uint32_t timeout_ms) { + if (!this->has_listener() || (this->get_state() != media_source::MediaSourceState::PLAYING)) { + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; + } + + // PlayerRole::get_current_stream_params() is safe to call from the sync task. + auto ¶ms = this->player_role_->get_current_stream_params(); + if (!params.bit_depth.has_value() || !params.channels.has_value() || !params.sample_rate.has_value()) { + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + return 0; + } + audio::AudioStreamInfo stream_info(*params.bit_depth, *params.channels, *params.sample_rate); + + return this->write_output(data, length, timeout_ms, stream_info); +} + +// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback) +void SendspinMediaSource::on_stream_start() { + this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED); + + if (!this->pending_start_) { + // Dedup rapid on_stream_start() calls + this->pending_start_ = true; + // Request the orchestrator to start this source + this->request_play_uri_("sendspin://current"); + } +} + +// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback) +void SendspinMediaSource::on_stream_end() { + if (this->get_state() != media_source::MediaSourceState::IDLE) { + // Only set to IDLE if we were previously in a non-IDLE state, to avoid duplicate state changes + this->set_state_(media_source::MediaSourceState::IDLE); + } +} + +// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback) +void SendspinMediaSource::on_stream_clear() { + if (this->get_state() != media_source::MediaSourceState::IDLE) { + // Only set to IDLE if we were previously in a non-IDLE state, to avoid duplicate state changes + this->set_state_(media_source::MediaSourceState::IDLE); + } +} + +// THREAD CONTEXT: Main loop (PlayerRoleListener callback) +void SendspinMediaSource::on_volume_changed(uint8_t volume) { this->request_volume_(volume / 100.0f); } + +// THREAD CONTEXT: Main loop (PlayerRoleListener callback) +void SendspinMediaSource::on_mute_changed(bool muted) { this->request_mute_(muted); } + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 && USE_SENDSPIN_PLAYER && USE_SENDSPIN_CONTROLLER diff --git a/esphome/components/sendspin/media_source/sendspin_media_source.h b/esphome/components/sendspin/media_source/sendspin_media_source.h new file mode 100644 index 00000000000..3b31716127c --- /dev/null +++ b/esphome/components/sendspin/media_source/sendspin_media_source.h @@ -0,0 +1,72 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_CONTROLLER) && defined(USE_SENDSPIN_PLAYER) + +#include "esphome/components/sendspin/sendspin_hub.h" + +#include "esphome/components/media_source/media_source.h" + +#include <sendspin/player_role.h> + +namespace esphome::sendspin_ { + +/// @brief Thin adapter media source for Sendspin. +/// +/// Implements PlayerRoleListener to receive audio data from the sendspin-cpp library's +/// SyncTask and bridges it to ESPHome's MediaSource output pipeline. Also forwards +/// transport commands to the hub's controller role. +class SendspinMediaSource : public SendspinChild, + public media_source::MediaSource, + public sendspin::PlayerRoleListener { + public: + void setup() override; + void dump_config() override; + + void set_static_delay_adjustable(bool adjustable); + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override; + bool has_internal_playlist() const override { return true; } + + void notify_volume_changed(float volume) override; + void notify_mute_changed(bool is_muted) override; + void notify_audio_played(uint32_t frames, int64_t timestamp) override; + + protected: + // --- Sendspin PlayerRoleListener overrides --- + + /// @brief Writes decoded PCM audio to ESPHome's media source output pipeline. + /// Called from the sync task's background thread. + size_t on_audio_write(uint8_t *data, size_t length, uint32_t timeout_ms) override; + + /// @brief Called when a new audio stream starts (main loop thread). + void on_stream_start() override; + + /// @brief Called when the audio stream ends (main loop thread). + void on_stream_end() override; + + /// @brief Called when the audio stream is cleared (main loop thread). + void on_stream_clear() override; + + /// @brief Called when volume changes (main loop thread). + void on_volume_changed(uint8_t volume) override; + + /// @brief Called when mute state changes (main loop thread). + void on_mute_changed(bool muted) override; + + sendspin::PlayerRole *player_role_{nullptr}; + + float cached_volume_{0.0f}; + + bool cached_muted_{false}; + bool pending_start_{false}; + bool static_delay_adjustable_{false}; +}; + +} // namespace esphome::sendspin_ + +#endif diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index ec419f77412..25e541a4938 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -25,6 +25,9 @@ void SendspinHub::setup() { // Set up persistence (preferences must be initialized before providers are added to the client) this->last_played_server_pref_ = global_preferences->make_preference<LastPlayedServerPref>(fnv1a_hash("sendspin_last_played")); +#ifdef USE_SENDSPIN_PLAYER + this->static_delay_pref_ = global_preferences->make_preference<StaticDelayPref>(fnv1a_hash("sendspin_static_delay")); +#endif // Wire providers and client listener this->client_->set_listener(this); @@ -36,6 +39,10 @@ void SendspinHub::setup() { this->controller_role_->set_listener(this); #endif +#ifdef USE_SENDSPIN_PLAYER + this->client_->add_player(this->player_config_).set_listener(this->player_listener_); +#endif + if (!this->client_->start_server()) { ESP_LOGE(TAG, "Failed to start Sendspin server"); this->mark_failed(); @@ -160,6 +167,39 @@ void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObjec } #endif +#ifdef USE_SENDSPIN_PLAYER +// THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured +sendspin::PlayerRole *SendspinHub::get_player_role() { + if (this->is_ready()) { + return this->client_->player(); + } + return nullptr; +} + +// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override) +bool SendspinHub::save_static_delay(uint16_t delay_ms) { + StaticDelayPref pref{.delay_ms = delay_ms}; + bool ok = this->static_delay_pref_.save(&pref); + if (ok) { + ESP_LOGD(TAG, "Persisted static delay: %u ms", delay_ms); + } else { + ESP_LOGW(TAG, "Failed to persist static delay"); + } + return ok; +} + +// THREAD CONTEXT: Main loop (SendspinPersistenceProvider override) +std::optional<uint16_t> SendspinHub::load_static_delay() { + StaticDelayPref pref{}; + if (this->static_delay_pref_.load(&pref)) { + ESP_LOGI(TAG, "Loaded static delay: %u ms", pref.delay_ms); + return pref.delay_ms; + } + return std::nullopt; +} + +#endif + } // namespace esphome::sendspin_ #endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 1e217e0ea2e..c9266bd4d1e 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -16,6 +16,9 @@ #ifdef USE_SENDSPIN_CONTROLLER #include <sendspin/controller_role.h> #endif +#ifdef USE_SENDSPIN_PLAYER +#include <sendspin/player_role.h> +#endif #include <functional> #include <memory> @@ -38,6 +41,13 @@ struct LastPlayedServerPref { uint32_t server_id_hash; }; +#ifdef USE_SENDSPIN_PLAYER +/// @brief Persistent storage structure for player static delay. +struct StaticDelayPref { + uint16_t delay_ms; +}; +#endif + /// @brief Thin adapter over sendspin::SendspinClient. /// /// The hub owns a SendspinClient instance and bridges its listener/provider interfaces to ESPHome's CallbackManager for @@ -112,6 +122,14 @@ class SendspinHub final : public Component, } #endif +#ifdef USE_SENDSPIN_PLAYER + void set_listener(sendspin::PlayerRoleListener *listener) { this->player_listener_ = listener; } + void set_player_config(const sendspin::PlayerRoleConfig &config) { this->player_config_ = config; } + + /// @brief Child components call this to get the PlayerRole instance after setup, so they can push updates to it. + sendspin::PlayerRole *get_player_role(); +#endif + protected: /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. sendspin::SendspinClientConfig build_client_config_(); @@ -141,6 +159,16 @@ class SendspinHub final : public Component, CallbackManager<void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_{}; #endif +#ifdef USE_SENDSPIN_PLAYER + sendspin::PlayerRoleListener *player_listener_{nullptr}; + sendspin::PlayerRoleConfig player_config_{}; + + // Part of SendspinPersistenceProvider overrides + ESPPreferenceObject static_delay_pref_; + std::optional<uint16_t> load_static_delay() override; + bool save_static_delay(uint16_t delay_ms) override; +#endif + // --- Core member variables --- ESPPreferenceObject last_played_server_pref_; diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml new file mode 100644 index 00000000000..4a7cd79c67d --- /dev/null +++ b/tests/components/sendspin/common-media_source.yaml @@ -0,0 +1,9 @@ +<<: !include common.yaml + +media_source: + - platform: sendspin + id: media_source_id + buffer_size: 500000 + initial_static_delay: 5ms + static_delay_adjustable: true + fixed_delay: 480us diff --git a/tests/components/sendspin/test-media_source.esp32-idf.yaml b/tests/components/sendspin/test-media_source.esp32-idf.yaml new file mode 100644 index 00000000000..47aeb2257c4 --- /dev/null +++ b/tests/components/sendspin/test-media_source.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-media_source.yaml From ac7f0f0b74549d4add4f97cf53186f289b01cbe9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 07:07:00 -0400 Subject: [PATCH 1638/2056] [sendspin] Add a metadata text sensor component (#15969) --- CODEOWNERS | 1 + esphome/components/sendspin/sendspin_hub.cpp | 11 +++ esphome/components/sendspin/sendspin_hub.h | 19 +++++ .../sendspin/text_sensor/__init__.py | 55 ++++++++++++ .../text_sensor/sendspin_text_sensor.cpp | 85 +++++++++++++++++++ .../text_sensor/sendspin_text_sensor.h | 35 ++++++++ .../sendspin/common-text_sensor.yaml | 21 +++++ .../sendspin/test-text_sensor.esp32-idf.yaml | 1 + 8 files changed, 228 insertions(+) create mode 100644 esphome/components/sendspin/text_sensor/__init__.py create mode 100644 esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp create mode 100644 esphome/components/sendspin/text_sensor/sendspin_text_sensor.h create mode 100644 tests/components/sendspin/common-text_sensor.yaml create mode 100644 tests/components/sendspin/test-text_sensor.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 822b0e973c1..f4b288b23d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -443,6 +443,7 @@ esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sendspin/* @kahrendt esphome/components/sendspin/media_player/* @kahrendt esphome/components/sendspin/media_source/* @kahrendt +esphome/components/sendspin/text_sensor/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/serial_proxy/* @kbx81 diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 25e541a4938..da298feb86d 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -39,6 +39,10 @@ void SendspinHub::setup() { this->controller_role_->set_listener(this); #endif +#ifdef USE_SENDSPIN_METADATA + this->client_->add_metadata().set_listener(this); +#endif + #ifdef USE_SENDSPIN_PLAYER this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif @@ -167,6 +171,13 @@ void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObjec } #endif +#ifdef USE_SENDSPIN_METADATA +// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop()) +void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) { + this->metadata_update_callbacks_.call(metadata); +} +#endif + #ifdef USE_SENDSPIN_PLAYER // THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured sendspin::PlayerRole *SendspinHub::get_player_role() { diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index c9266bd4d1e..8d9c58a3abb 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -16,6 +16,9 @@ #ifdef USE_SENDSPIN_CONTROLLER #include <sendspin/controller_role.h> #endif +#ifdef USE_SENDSPIN_METADATA +#include <sendspin/metadata_role.h> +#endif #ifdef USE_SENDSPIN_PLAYER #include <sendspin/player_role.h> #endif @@ -66,6 +69,9 @@ struct StaticDelayPref { class SendspinHub final : public Component, #ifdef USE_SENDSPIN_CONTROLLER public sendspin::ControllerRoleListener, +#endif +#ifdef USE_SENDSPIN_METADATA + public sendspin::MetadataRoleListener, #endif public sendspin::SendspinClientListener, public sendspin::SendspinNetworkProvider, @@ -122,6 +128,12 @@ class SendspinHub final : public Component, } #endif +#ifdef USE_SENDSPIN_METADATA + template<typename F> void add_metadata_update_callback(F &&callback) { + this->metadata_update_callbacks_.add(std::forward<F>(callback)); + } +#endif + #ifdef USE_SENDSPIN_PLAYER void set_listener(sendspin::PlayerRoleListener *listener) { this->player_listener_ = listener; } void set_player_config(const sendspin::PlayerRoleConfig &config) { this->player_config_ = config; } @@ -159,6 +171,13 @@ class SendspinHub final : public Component, CallbackManager<void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_{}; #endif +#ifdef USE_SENDSPIN_METADATA + void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override; + + // Callback fan-out to child components; they filter as needed + CallbackManager<void(const sendspin::ServerMetadataStateObject &)> metadata_update_callbacks_{}; +#endif + #ifdef USE_SENDSPIN_PLAYER sendspin::PlayerRoleListener *player_listener_{nullptr}; sendspin::PlayerRoleConfig player_config_{}; diff --git a/esphome/components/sendspin/text_sensor/__init__.py b/esphome/components/sendspin/text_sensor/__init__.py new file mode 100644 index 00000000000..b7f216ca0ce --- /dev/null +++ b/esphome/components/sendspin/text_sensor/__init__.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TYPE +from esphome.types import ConfigType + +from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["sendspin"] + +SendspinTextSensor = sendspin_ns.class_( + "SendspinTextSensor", + text_sensor.TextSensor, + cg.Component, +) + +SendspinTextMetadataTypes = sendspin_ns.enum("SendspinTextMetadataTypes", is_class=True) +SENDSPIN_TEXT_METADATA_TYPES = { + "title": SendspinTextMetadataTypes.TITLE, + "artist": SendspinTextMetadataTypes.ARTIST, + "album": SendspinTextMetadataTypes.ALBUM, + "album_artist": SendspinTextMetadataTypes.ALBUM_ARTIST, + "year": SendspinTextMetadataTypes.YEAR, + "track": SendspinTextMetadataTypes.TRACK, +} + + +def _request_roles(config: ConfigType) -> ConfigType: + """Request the necessary Sendspin roles for the text sensor.""" + request_metadata_support() + + return config + + +CONFIG_SCHEMA = cv.All( + text_sensor.text_sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(SendspinTextSensor), + cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub), + cv.Required(CONF_TYPE): cv.enum(SENDSPIN_TEXT_METADATA_TYPES), + } + ), + cv.only_on_esp32, + _request_roles, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SENDSPIN_ID]) + await text_sensor.register_text_sensor(var, config) + + cg.add(var.set_metadata_type(config[CONF_TYPE])) diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp new file mode 100644 index 00000000000..d16d51f63c6 --- /dev/null +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp @@ -0,0 +1,85 @@ +#include "sendspin_text_sensor.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR) + +#include "esphome/core/helpers.h" + +#include <sendspin/metadata_role.h> + +#include <string> + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.text_sensor"; + +void SendspinTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sendspin", this); } + +// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop +// (SendspinHub dispatches metadata from client_->loop()). +void SendspinTextSensor::setup() { + switch (this->metadata_type_) { + case SendspinTextMetadataTypes::TITLE: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.title.has_value()) { + this->publish_if_changed_(metadata.title.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ARTIST: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.artist.has_value()) { + this->publish_if_changed_(metadata.artist.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ALBUM: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.album.has_value()) { + this->publish_if_changed_(metadata.album.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ALBUM_ARTIST: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.album_artist.has_value()) { + this->publish_if_changed_(metadata.album_artist.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::YEAR: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.year.has_value() && metadata.year.value() <= 9999) { + char buf[UINT32_MAX_STR_SIZE]; + uint32_to_str(buf, metadata.year.value()); + this->publish_if_changed_(buf); + } + }); + break; + } + case SendspinTextMetadataTypes::TRACK: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.track.has_value() && metadata.track.value() <= 9999) { + char buf[UINT32_MAX_STR_SIZE]; + uint32_to_str(buf, metadata.track.value()); + this->publish_if_changed_(buf); + } + }); + break; + } + } +} + +// Dedup to avoid frontend churn; TextSensor::publish_state already dedups the string assign but still notifies. +void SendspinTextSensor::publish_if_changed_(const char *value) { + if (this->get_raw_state() != value) { + this->publish_state(value); + } +} + +} // namespace esphome::sendspin_ + +#endif diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h new file mode 100644 index 00000000000..d9ef49c938c --- /dev/null +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR) + +#include "esphome/components/sendspin/sendspin_hub.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::sendspin_ { + +enum class SendspinTextMetadataTypes { + TITLE, + ARTIST, + ALBUM, + ALBUM_ARTIST, + YEAR, + TRACK, +}; + +class SendspinTextSensor : public SendspinChild, public text_sensor::TextSensor { + public: + void dump_config() override; + void setup() override; + + void set_metadata_type(SendspinTextMetadataTypes metadata_type) { this->metadata_type_ = metadata_type; } + + protected: + void publish_if_changed_(const char *value); + + SendspinTextMetadataTypes metadata_type_; +}; + +} // namespace esphome::sendspin_ +#endif diff --git a/tests/components/sendspin/common-text_sensor.yaml b/tests/components/sendspin/common-text_sensor.yaml new file mode 100644 index 00000000000..0bfbf457574 --- /dev/null +++ b/tests/components/sendspin/common-text_sensor.yaml @@ -0,0 +1,21 @@ +<<: !include common.yaml + +text_sensor: + - platform: sendspin + name: "Title" + type: title + - platform: sendspin + name: "Artist" + type: artist + - platform: sendspin + name: "Album" + type: album + - platform: sendspin + name: "Album Artist" + type: album_artist + - platform: sendspin + name: "Year" + type: year + - platform: sendspin + name: "Track Number" + type: track diff --git a/tests/components/sendspin/test-text_sensor.esp32-idf.yaml b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml new file mode 100644 index 00000000000..8998b8896ef --- /dev/null +++ b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-text_sensor.yaml From 773b4d887bf25d8b564aab10f1c3ddd27ee65676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 08:11:29 -0500 Subject: [PATCH 1639/2056] [core] Scheduler: don't sleep while defer queue is non-empty (#15968) --- esphome/core/scheduler.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a6f1558e4a4..d83d67d6e42 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -414,8 +414,27 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) { optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). - // It performs cleanup and accesses items_[0] without holding a lock, which is only - // safe when called from the main thread. Other threads must not call this method. + // Accesses items_[0] and the fast-path empty checks without holding a lock, which + // is only safe from the main thread. Other threads must not call this method. + // + // Note: cleanup_() is only invoked on the items_[0] path below. The early returns + // skip it because they don't read items_[0], and Scheduler::call() at the top of + // every loop iteration already performs its own cleanup before the next sleep- + // duration computation happens. + +#ifndef ESPHOME_THREAD_SINGLE + // defer() items live in a separate queue that is drained at the top of every + // loop tick via process_defer_queue_(). If any are pending, the next loop + // iteration has work to do right now -- don't let the caller sleep. + if (!this->defer_empty_()) + return 0; +#else + // On single-threaded builds, defer() routes through set_timeout(..., 0) which + // stages in to_add_. process_to_add() runs at the top of every scheduler.call(), + // so anything in to_add_ becomes runnable on the next iteration; don't sleep. + if (!this->to_add_empty_()) + return 0; +#endif // If no items, return empty optional if (!this->cleanup_()) From baa6d5f96b85ff28f34af1a718e5bbe71bef3e2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 08:11:47 -0500 Subject: [PATCH 1640/2056] [web_server_idf] Fix cross-thread race on SSE session state (#15967) --- .../web_server_idf/web_server_idf.cpp | 70 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 16 ++++- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 8f464ae9121..e1d3e4bf34f 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -472,24 +472,36 @@ void AsyncResponseStream::printf(const char *fmt, ...) { #ifdef USE_WEBSERVER AsyncEventSource::~AsyncEventSource() { - for (auto *ses : this->sessions_) { - delete ses; // NOLINT(cppcoreguidelines-owning-memory) + LockGuard guard{this->pending_mutex_}; + for (auto *vec : {&this->sessions_, &this->pending_sessions_}) { + for (auto *ses : *vec) { + delete ses; // NOLINT(cppcoreguidelines-owning-memory) + } } } void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { + // Httpd task: set up the live httpd_req_t and park the session; main loop does the rest. // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks) auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_); - if (this->on_connect_) { - this->on_connect_(rsp); + { + LockGuard guard{this->pending_mutex_}; + this->pending_sessions_.push_back(rsp); + this->has_pending_sessions_.store(true, std::memory_order_release); } - this->sessions_.push_back(rsp); - // Wake up WebServer::loop() to drain deferred event queues for this client. - // Safe from httpd task context via the pending_enable_loop_ flag. this->web_server_->enable_loop_soon_any_context(); } +// clang-analyzer traces a false-positive leak path from loop() through +// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and +// finally ArduinoJson. Suppress along the entire in-our-code call chain. +// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) bool AsyncEventSource::loop() { + // Fast path: one atomic load per tick. Slow path is out-of-line on connect. + if (this->has_pending_sessions_.load(std::memory_order_acquire)) { + this->adopt_pending_sessions_main_loop_(); + } + // Clean up dead sessions safely // This follows the ESP-IDF pattern where free_ctx marks resources as dead // and the main loop handles the actual cleanup to avoid race conditions @@ -497,7 +509,7 @@ bool AsyncEventSource::loop() { auto *ses = this->sessions_[i]; // If the session has a dead socket (marked by destroy callback) if (ses->fd_.load() == 0) { - ESP_LOGD(TAG, "Removing dead event source session"); + // destroy() already logged the close with the fd; don't double-log here. delete ses; // NOLINT(cppcoreguidelines-owning-memory) // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions) this->sessions_[i] = this->sessions_.back(); @@ -510,6 +522,30 @@ bool AsyncEventSource::loop() { return !this->sessions_.empty(); } +void AsyncEventSource::adopt_pending_sessions_main_loop_() { + std::vector<AsyncEventSourceResponse *> incoming; + { + LockGuard guard{this->pending_mutex_}; + incoming.swap(this->pending_sessions_); + this->has_pending_sessions_.store(false, std::memory_order_relaxed); + } + for (auto *rsp : incoming) { + // Already disconnected? Drop it; skip on_connect_/session start on a dead session. + if (rsp->fd_.load() == 0) { + delete rsp; // NOLINT(cppcoreguidelines-owning-memory) + continue; + } + this->sessions_.push_back(rsp); + // Prime first so on_connect_ observes a session that has already sent its + // initial ping/config/sorting_groups, matching the pre-refactor ordering. + rsp->start_session_main_loop_(); + if (this->on_connect_) { + this->on_connect_(rsp); + } + } +} +// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { for (auto *ses : this->sessions_) { if (ses->fd_.load() != 0) { // Skip dead sessions @@ -534,6 +570,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * esphome::web_server_idf::AsyncEventSource *server, esphome::web_server::WebServer *ws) : server_(server), web_server_(ws), entities_iterator_(ws, server) { + // Httpd task only. start_session_main_loop_() handles event_buffer_ / iterator setup. httpd_req_t *req = *request; httpd_resp_set_status(req, HTTPD_200); @@ -555,21 +592,23 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send); +} - // Configure reconnect timeout and send config - // this should always go through since the tcp send buffer is empty on connect +// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson +void AsyncEventSourceResponse::start_session_main_loop_() { + auto *ws = this->web_server_; + + // tcp send buffer is empty on connect, so these should always go through auto message = ws->get_config_json(); this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); root["name"] = group.second.name; root["sorting_weight"] = group.second.weight; message = builder.serialize(); - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // a (very) large number of these should be able to be queued initially without defer // since the only thing in the send buffer at this point is the initial ping/config @@ -578,13 +617,8 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * #endif this->entities_iterator_.begin(ws->include_internal_); - - // just dump them all up-front and take advantage of the deferred queue - // on second thought that takes too long, but leaving the commented code here for debug purposes - // while(!this->entities_iterator_.completed()) { - // this->entities_iterator_.advance(); - //} } +// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index f2931fb5079..cdb58c2f046 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -299,6 +299,9 @@ class AsyncEventSourceResponse { AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server, esphome::web_server::WebServer *ws); + // Main-loop only: sends initial ping/config/sorting_groups, starts entity iterator. + void start_session_main_loop_(); + void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator); void process_deferred_queue_(); void process_buffer_(); @@ -335,6 +338,8 @@ class AsyncEventSource : public AsyncWebHandler { } // NOLINTNEXTLINE(readability-identifier-naming) void handleRequest(AsyncWebServerRequest *request) override; + // Callback runs on the main loop (not the httpd task) after the session's + // initial ping/config/sorting_groups have been sent. // NOLINTNEXTLINE(readability-identifier-naming) void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); } @@ -347,13 +352,18 @@ class AsyncEventSource : public AsyncWebHandler { size_t count() const { return this->sessions_.size(); } protected: + // Cold path: move sessions from pending_sessions_ into sessions_ and greet each one. + void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_(); + std::string url_; - // Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards). - // Linear search is faster than red-black tree overhead for this small dataset. - // Only operations needed: add session, remove session, iterate sessions - no need for sorted order. + // Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set. std::vector<AsyncEventSourceResponse *> sessions_; + // Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_. + std::vector<AsyncEventSourceResponse *> pending_sessions_; + Mutex pending_mutex_; connect_handler_t on_connect_{}; esphome::web_server::WebServer *web_server_; + std::atomic<bool> has_pending_sessions_{false}; }; #endif // USE_WEBSERVER From f132b7dc07f2f402eab87a8ee445a64c5b403e22 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 10:09:03 -0400 Subject: [PATCH 1641/2056] [media_player][speaker][speaker_source] Centralize preferred format codegen (#14771) --- esphome/components/media_player/__init__.py | 111 +++++++++++- .../speaker/media_player/__init__.py | 160 ++++-------------- .../components/speaker_source/media_player.py | 83 +-------- .../speaker/common-media_player.yaml | 2 +- 4 files changed, 156 insertions(+), 200 deletions(-) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 1c2c4746451..d1db868ace4 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -1,20 +1,31 @@ +from collections.abc import Callable + from esphome import automation import esphome.codegen as cg +from esphome.components import audio import esphome.config_validation as cv from esphome.const import ( CONF_ENTITY_CATEGORY, + CONF_FORMAT, CONF_ICON, CONF_ID, + CONF_NUM_CHANNELS, CONF_ON_IDLE, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_SAMPLE_RATE, CONF_VOLUME, ) from esphome.core import CORE -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + inherit_property_from, + setup_entity, +) from esphome.coroutine import CoroPriority, coroutine_with_priority -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -34,6 +45,102 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } +# Public API for external components. Do not remove. +FORMAT_MAPPING = { + "FLAC": "flac", + "MP3": "mp3", + "OPUS": "opus", + "WAV": "wav", +} + + +def build_supported_format_struct( + format_config: ConfigType, purpose: MockObj +) -> cg.StructInitializer: + """Build a MediaPlayerSupportedFormat struct from a format config and purpose. + + Public API for external components. Do not remove. + """ + args = [ + MediaPlayerSupportedFormat, + ("format", FORMAT_MAPPING[format_config[CONF_FORMAT]]), + ("sample_rate", format_config[CONF_SAMPLE_RATE]), + ("num_channels", format_config[CONF_NUM_CHANNELS]), + ("purpose", purpose), + ] + + # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails + # if the number of bytes per sample is specified for MP3. + if format_config[CONF_FORMAT] != "MP3": + args.append(("sample_bytes", 2)) + + return cg.StructInitializer(*args) + + +def validate_preferred_format( + component_name: str, audio_device_key: str +) -> Callable[[ConfigType], ConfigType]: + """Return a validator that inherits audio device settings and validates format constraints. + + Public API for external components. Do not remove. + """ + + def validator(config: ConfigType) -> ConfigType: + # Inherit settings from audio device if not manually set + inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config) + inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config) + + # Opus only supports 48 kHz + if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: + raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") + + # Validate the settings are compatible with the audio device + audio.final_validate_audio_schema( + component_name, + audio_device=audio_device_key, + bits_per_sample=16, + channels=config.get(CONF_NUM_CHANNELS), + sample_rate=config.get(CONF_SAMPLE_RATE), + )(config) + + return config + + return validator + + +def request_codecs_for_format_configs( + config: ConfigType, format_config_keys: list[str] +) -> None: + """Scan format configs for configured formats and request the needed codec support. + + If any config uses "NONE" (accepts any format), all codecs are requested. + + Public API for external components. Do not remove. + """ + needed_formats: set[str] = set() + need_all = False + + for key in format_config_keys: + if format_config := config.get(key): + fmt = format_config[CONF_FORMAT] + if fmt == "NONE": + need_all = True + else: + needed_formats.add(fmt) + + if need_all: + audio.request_flac_support() + audio.request_mp3_support() + audio.request_opus_support() + else: + if "FLAC" in needed_formats: + audio.request_flac_support() + if "MP3" in needed_formats: + audio.request_mp3_support() + if "OPUS" in needed_formats: + audio.request_opus_support() + + # Local config key constants CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 9b496637da1..abfd599808f 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -32,7 +32,6 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt -from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content _LOGGER = logging.getLogger(__name__) @@ -44,16 +43,12 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" -CODEC_SUPPORT_ALL = "all" -CODEC_SUPPORT_NEEDED = "needed" -CODEC_SUPPORT_NONE = "none" - TYPE_LOCAL = "local" TYPE_WEB = "web" CONF_ANNOUNCEMENT = "announcement" CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" -CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" +CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0 CONF_ENQUEUE = "enqueue" CONF_MEDIA_FILE = "media_file" CONF_MEDIA_PIPELINE = "media_pipeline" @@ -106,43 +101,10 @@ def _download_web_file(value): return value -# Returns a media_player.MediaPlayerSupportedFormat struct with the configured -# format, sample rate, number of channels, purpose, and bytes per sample -def _get_supported_format_struct(pipeline, type): - args = [ - media_player.MediaPlayerSupportedFormat, - ] - - if pipeline[CONF_FORMAT] == "FLAC": - args.append(("format", "flac")) - elif pipeline[CONF_FORMAT] == "MP3": - args.append(("format", "mp3")) - elif pipeline[CONF_FORMAT] == "OPUS": - args.append(("format", "opus")) - elif pipeline[CONF_FORMAT] == "WAV": - args.append(("format", "wav")) - - args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) - args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) - - if type == "MEDIA": - args.append( - ( - "purpose", - media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], - ) - ) - elif type == "ANNOUNCEMENT": - args.append( - ( - "purpose", - media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], - ) - ) - if pipeline[CONF_FORMAT] != "MP3": - args.append(("sample_bytes", 2)) - - return cg.StructInitializer(*args) +_PURPOSE_MAP = { + "MEDIA": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], + "ANNOUNCEMENT": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], +} def _file_schema(value): @@ -210,25 +172,9 @@ def _validate_file_shorthand(value): ) -def _validate_pipeline(config): - # Inherit transcoder settings from speaker if not manually set - inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) - inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) - - # Opus only supports 48 kHz - if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: - raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") - - # Validate the transcoder settings is compatible with the speaker - audio.final_validate_audio_schema( - "speaker media_player", - audio_device=CONF_SPEAKER, - bits_per_sample=16, - channels=config.get(CONF_NUM_CHANNELS), - sample_rate=config.get(CONF_SAMPLE_RATE), - )(config) - - return config +_validate_pipeline = media_player.validate_preferred_format( + "speaker media_player", CONF_SPEAKER +) def _validate_repeated_speaker(config): @@ -245,59 +191,34 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Normalize boolean values to string equivalents - codec_mode = config[CONF_CODEC_SUPPORT_ENABLED] - if codec_mode is True: - codec_mode = CODEC_SUPPORT_ALL - elif codec_mode is False: - codec_mode = CODEC_SUPPORT_NONE + # Remove before 2026.10.0 + if CONF_CODEC_SUPPORT_ENABLED in config: + _LOGGER.warning( + "'%s' is deprecated and will be removed in 2026.10.0. " + "Codec support is now automatically determined from the pipeline " + "'format' setting. Set format to 'NONE' to enable all codecs.", + CONF_CODEC_SUPPORT_ENABLED, + ) - use_codec = codec_mode != CODEC_SUPPORT_NONE - - # In "needed" mode, collect formats from pipelines and files - needed_formats = set() - need_all = False - if codec_mode == CODEC_SUPPORT_NEEDED: - for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): - if pipeline := config.get(pipeline_key): - fmt = pipeline[CONF_FORMAT] - if fmt == "NONE": - # No preferred format means any format could arrive - need_all = True - else: - needed_formats.add(fmt) + # Request codecs based on pipeline formats + media_player.request_codecs_for_format_configs( + config, [CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE] + ) + # Validate local files and request any additional codecs they need for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): raise cv.Invalid("Unsupported local media file") - if not use_codec and str(media_file_type) != str( - audio.AUDIO_FILE_TYPE_ENUM["WAV"] - ): - # Only wav files are supported - raise cv.Invalid( - f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav" - ) - # In "needed" mode, add file format to needed codecs - if codec_mode == CODEC_SUPPORT_NEEDED: - for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): - if str(media_file_type) == str(fmt_enum): - if fmt_name not in ("WAV", "NONE"): - needed_formats.add(fmt_name) - break - - # Request codec support - if codec_mode == CODEC_SUPPORT_ALL or need_all: - audio.request_flac_support() - audio.request_mp3_support() - audio.request_opus_support() - elif codec_mode == CODEC_SUPPORT_NEEDED: - if "FLAC" in needed_formats: - audio.request_flac_support() - if "MP3" in needed_formats: - audio.request_mp3_support() - if "OPUS" in needed_formats: - audio.request_opus_support() + for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): + if str(media_file_type) == str(fmt_enum): + if fmt_name == "FLAC": + audio.request_flac_support() + elif fmt_name == "MP3": + audio.request_mp3_support() + elif fmt_name == "OPUS": + audio.request_opus_support() + break return config @@ -362,17 +283,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional( - CONF_CODEC_SUPPORT_ENABLED, default=CODEC_SUPPORT_NEEDED - ): cv.Any( - cv.boolean, - cv.one_of( - CODEC_SUPPORT_ALL, - CODEC_SUPPORT_NEEDED, - CODEC_SUPPORT_NONE, - lower=True, - ), - ), + # Remove before 2026.10.0 + cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string), cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) @@ -432,8 +344,8 @@ async def to_code(config): if announcement_pipeline_config[CONF_FORMAT] != "NONE": cg.add( var.set_announcement_format( - _get_supported_format_struct( - announcement_pipeline_config, "ANNOUNCEMENT" + media_player.build_supported_format_struct( + announcement_pipeline_config, _PURPOSE_MAP["ANNOUNCEMENT"] ) ) ) @@ -444,7 +356,9 @@ async def to_code(config): if media_pipeline_config[CONF_FORMAT] != "NONE": cg.add( var.set_media_format( - _get_supported_format_struct(media_pipeline_config, "MEDIA") + media_player.build_supported_format_struct( + media_pipeline_config, _PURPOSE_MAP["MEDIA"] + ) ) ) diff --git a/esphome/components/speaker_source/media_player.py b/esphome/components/speaker_source/media_player.py index 70feeac3180..b6653fe5433 100644 --- a/esphome/components/speaker_source/media_player.py +++ b/esphome/components/speaker_source/media_player.py @@ -17,7 +17,6 @@ from esphome.const import ( CONF_SPEAKER, ) from esphome.core import ID -from esphome.core.entity_helpers import inherit_property_from from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType @@ -65,53 +64,9 @@ SetPlaylistDelayAction = speaker_source_ns.class_( ) -FORMAT_MAPPING = { - "FLAC": "flac", - "MP3": "mp3", - "OPUS": "opus", - "WAV": "wav", -} - - -# Returns a media_player.MediaPlayerSupportedFormat struct with the configured -# format, sample rate, number of channels, purpose, and bytes per sample -def _get_supported_format_struct(pipeline: ConfigType, purpose: MockObj): - args = [ - media_player.MediaPlayerSupportedFormat, - ] - - args.append(("format", FORMAT_MAPPING[pipeline[CONF_FORMAT]])) - - args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE])) - args.append(("num_channels", pipeline[CONF_NUM_CHANNELS])) - args.append(("purpose", purpose)) - - # Omit sample_bytes for MP3: ffmpeg transcoding in Home Assistant fails - # if the number of bytes per sample is specified for MP3. - if pipeline[CONF_FORMAT] != "MP3": - args.append(("sample_bytes", 2)) - - return cg.StructInitializer(*args) - - -def _validate_pipeline(config: ConfigType) -> ConfigType: - # Inherit settings from speaker if not manually set - inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config) - inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config) - - # Opus only supports 48 kHz - if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000: - raise cv.Invalid("Opus only supports a sample rate of 48000 Hz") - - audio.final_validate_audio_schema( - "speaker_source media_player", - audio_device=CONF_SPEAKER, - bits_per_sample=16, - channels=config.get(CONF_NUM_CHANNELS), - sample_rate=config.get(CONF_SAMPLE_RATE), - )(config) - - return config +_validate_pipeline = media_player.validate_preferred_format( + "speaker_source media_player", CONF_SPEAKER +) PIPELINE_SCHEMA = cv.Schema( @@ -198,31 +153,9 @@ CONFIG_SCHEMA = cv.All( def _final_validate_codecs(config: ConfigType) -> ConfigType: - # "NONE" means the pipeline accepts any format at runtime, so all optional codecs must be available. - # When a specific format is set, only that codec is requested. - needed_formats: set[str] = set() - need_all = False - - for pipeline_key in (CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE): - if pipeline := config.get(pipeline_key): - fmt = pipeline[CONF_FORMAT] - if fmt == "NONE": - need_all = True - else: - needed_formats.add(fmt) - - if need_all: - audio.request_flac_support() - audio.request_mp3_support() - audio.request_opus_support() - else: - if "FLAC" in needed_formats: - audio.request_flac_support() - if "MP3" in needed_formats: - audio.request_mp3_support() - if "OPUS" in needed_formats: - audio.request_opus_support() - + media_player.request_codecs_for_format_configs( + config, [CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE] + ) return config @@ -264,7 +197,9 @@ async def to_code(config: ConfigType) -> None: cg.add( var.set_format( pipeline_enum, - _get_supported_format_struct(pipeline_config, purpose), + media_player.build_supported_format_struct( + pipeline_config, purpose + ), ) ) diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index c958c0d9120..a849e04b33e 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -11,9 +11,9 @@ media_player: id: speaker_media_player_id announcement_pipeline: speaker: speaker_id + format: NONE buffer_size: 1000000 volume_increment: 0.02 volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true - codec_support_enabled: all From 55bcf33446dfc8c001482c57fcdbc2754d332058 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 10:32:47 -0400 Subject: [PATCH 1642/2056] [sendspin] Add metadata sensor component (#15971) --- CODEOWNERS | 1 + esphome/components/sendspin/sendspin_hub.cpp | 11 ++- esphome/components/sendspin/sendspin_hub.h | 15 +++ .../components/sendspin/sensor/__init__.py | 98 +++++++++++++++++++ .../sendspin/sensor/sendspin_sensor.cpp | 98 +++++++++++++++++++ .../sendspin/sensor/sendspin_sensor.h | 42 ++++++++ tests/components/sendspin/common-sensor.yaml | 15 +++ .../sendspin/test-sensor.esp32-idf.yaml | 1 + 8 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 esphome/components/sendspin/sensor/__init__.py create mode 100644 esphome/components/sendspin/sensor/sendspin_sensor.cpp create mode 100644 esphome/components/sendspin/sensor/sendspin_sensor.h create mode 100644 tests/components/sendspin/common-sensor.yaml create mode 100644 tests/components/sendspin/test-sensor.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index f4b288b23d6..20c19a7dfa0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -443,6 +443,7 @@ esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sendspin/* @kahrendt esphome/components/sendspin/media_player/* @kahrendt esphome/components/sendspin/media_source/* @kahrendt +esphome/components/sendspin/sensor/* @kahrendt esphome/components/sendspin/text_sensor/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index da298feb86d..d27c5672eb6 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -40,7 +40,8 @@ void SendspinHub::setup() { #endif #ifdef USE_SENDSPIN_METADATA - this->client_->add_metadata().set_listener(this); + this->metadata_role_ = &this->client_->add_metadata(); + this->metadata_role_->set_listener(this); #endif #ifdef USE_SENDSPIN_PLAYER @@ -176,6 +177,14 @@ void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObjec void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) { this->metadata_update_callbacks_.call(metadata); } + +// THREAD CONTEXT: Main loop (invoked from Sendspin components) +uint32_t SendspinHub::get_track_progress_ms() const { + if (this->is_ready()) { + return this->metadata_role_->get_track_progress_ms(); + } + return 0; +} #endif #ifdef USE_SENDSPIN_PLAYER diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 8d9c58a3abb..12fbf156ea6 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -132,6 +132,9 @@ class SendspinHub final : public Component, template<typename F> void add_metadata_update_callback(F &&callback) { this->metadata_update_callbacks_.add(std::forward<F>(callback)); } + + /// @brief Returns the interpolated track progress in milliseconds, or 0 if the hub is not yet ready. + uint32_t get_track_progress_ms() const; #endif #ifdef USE_SENDSPIN_PLAYER @@ -172,6 +175,8 @@ class SendspinHub final : public Component, #endif #ifdef USE_SENDSPIN_METADATA + sendspin::MetadataRole *metadata_role_{nullptr}; + void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override; // Callback fan-out to child components; they filter as needed @@ -211,6 +216,16 @@ class SendspinChild : public Component, public Parented<SendspinHub> { float get_setup_priority() const override { return sendspin_priority::CHILD; } }; +/// @brief Base class for sendspin subcomponents that need polling behavior. +/// +/// Same purpose as SendspinChild but inherits from PollingComponent for subcomponents +/// that poll on a fixed interval. Subcomponents should inherit from this instead of +/// listing PollingComponent/Parented individually and must not override get_setup_priority(). +class SendspinPollingChild : public PollingComponent, public Parented<SendspinHub> { + public: + float get_setup_priority() const override { return sendspin_priority::CHILD; } +}; + } // namespace esphome::sendspin_ #endif // USE_ESP32 diff --git a/esphome/components/sendspin/sensor/__init__.py b/esphome/components/sendspin/sensor/__init__.py new file mode 100644 index 00000000000..dc9b86c2a36 --- /dev/null +++ b/esphome/components/sendspin/sensor/__init__.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TYPE, + CONF_YEAR, + STATE_CLASS_MEASUREMENT, + UNIT_MILLISECOND, +) +from esphome.types import ConfigType + +from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["sendspin"] + +CONF_TRACK = "track" +CONF_TRACK_PROGRESS = "track_progress" +CONF_TRACK_DURATION = "track_duration" + +SendspinTrackProgressSensor = sendspin_ns.class_( + "SendspinTrackProgressSensor", + sensor.Sensor, + cg.PollingComponent, +) +SendspinMetadataSensor = sendspin_ns.class_( + "SendspinMetadataSensor", + sensor.Sensor, + cg.Component, +) + +SendspinNumericMetadataTypes = sendspin_ns.enum( + "SendspinNumericMetadataTypes", is_class=True +) +_METADATA_TYPE_ENUM = { + CONF_TRACK_DURATION: SendspinNumericMetadataTypes.TRACK_DURATION, + CONF_YEAR: SendspinNumericMetadataTypes.YEAR, + CONF_TRACK: SendspinNumericMetadataTypes.TRACK, +} + + +def _request_roles(config: ConfigType) -> ConfigType: + """Request the necessary Sendspin roles for the sensor.""" + request_metadata_support() + + return config + + +_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)}) + + +def _metadata_schema(**sensor_kwargs): + """Schema for event-driven numeric metadata sensors (duration/year/track).""" + return ( + sensor.sensor_schema( + SendspinMetadataSensor, + accuracy_decimals=0, + **sensor_kwargs, + ) + .extend(_HUB_ID_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + ) + + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + CONF_TRACK_PROGRESS: sensor.sensor_schema( + SendspinTrackProgressSensor, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLISECOND, + ) + .extend(_HUB_ID_SCHEMA) + .extend(cv.polling_component_schema("1s")), + CONF_TRACK_DURATION: _metadata_schema( + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLISECOND, + ), + CONF_YEAR: _metadata_schema(), + CONF_TRACK: _metadata_schema(), + }, + key=CONF_TYPE, + ), + cv.only_on_esp32, + _request_roles, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SENDSPIN_ID]) + await sensor.register_sensor(var, config) + + if (metadata_type := _METADATA_TYPE_ENUM.get(config[CONF_TYPE])) is not None: + cg.add(var.set_metadata_type(metadata_type)) diff --git a/esphome/components/sendspin/sensor/sendspin_sensor.cpp b/esphome/components/sendspin/sensor/sendspin_sensor.cpp new file mode 100644 index 00000000000..68848a6f3e1 --- /dev/null +++ b/esphome/components/sendspin/sensor/sendspin_sensor.cpp @@ -0,0 +1,98 @@ +#include "sendspin_sensor.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR) + +#include <sendspin/metadata_role.h> + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.sensor"; + +// --- SendspinTrackProgressSensor --- + +void SendspinTrackProgressSensor::dump_config() { + LOG_SENSOR("", "Track Progress", this); + LOG_UPDATE_INTERVAL(this); +} + +// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop +// (SendspinHub dispatches metadata from client_->loop()). +void SendspinTrackProgressSensor::setup() { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (!metadata.progress.has_value()) { + return; + } + const auto &progress = metadata.progress.value(); + if (progress.playback_speed == 0) { + // Paused: freeze progress at the reported position and stop polling to save cycles. + this->stop_poller(); + this->publish_state(progress.track_progress); + } else { + // Resumed: publish the fresh interpolated position immediately so the frontend doesn't show a stale + // paused value until the next poll tick. + this->publish_state(this->parent_->get_track_progress_ms()); + this->start_poller(); + } + }); +} + +// THREAD CONTEXT: Main loop. +// Sendspin only pushes progress on state changes (play/pause/seek/speed change), not continuously during +// playback. The hub helper interpolates the current position from the last server update and the playback +// speed, giving us a fresh value on every poll. +void SendspinTrackProgressSensor::update() { this->publish_state(this->parent_->get_track_progress_ms()); } + +// --- SendspinMetadataSensor --- + +void SendspinMetadataSensor::dump_config() { + switch (this->metadata_type_) { + case SendspinNumericMetadataTypes::TRACK_DURATION: + LOG_SENSOR("", "Track Duration", this); + break; + case SendspinNumericMetadataTypes::YEAR: + LOG_SENSOR("", "Year", this); + break; + case SendspinNumericMetadataTypes::TRACK: + LOG_SENSOR("", "Track", this); + break; + } +} + +std::optional<float> SendspinMetadataSensor::extract_value_(const sendspin::ServerMetadataStateObject &metadata) const { + switch (this->metadata_type_) { + case SendspinNumericMetadataTypes::TRACK_DURATION: + if (metadata.progress.has_value()) + return metadata.progress.value().track_duration; + return std::nullopt; + case SendspinNumericMetadataTypes::YEAR: + if (metadata.year.has_value()) + return metadata.year.value(); + return std::nullopt; + case SendspinNumericMetadataTypes::TRACK: + if (metadata.track.has_value()) + return metadata.track.value(); + return std::nullopt; + } + return std::nullopt; +} + +// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop +// (SendspinHub dispatches metadata from client_->loop()). +void SendspinMetadataSensor::setup() { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (auto value = this->extract_value_(metadata)) { + this->publish_if_changed_(*value); + } + }); +} + +// Dedup to avoid frontend churn; Sensor::publish_state always notifies without checking for changes. +void SendspinMetadataSensor::publish_if_changed_(float value) { + if (this->get_raw_state() != value) { + this->publish_state(value); + } +} + +} // namespace esphome::sendspin_ + +#endif diff --git a/esphome/components/sendspin/sensor/sendspin_sensor.h b/esphome/components/sendspin/sensor/sendspin_sensor.h new file mode 100644 index 00000000000..cbfe1742c95 --- /dev/null +++ b/esphome/components/sendspin/sensor/sendspin_sensor.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR) + +#include "esphome/components/sendspin/sendspin_hub.h" +#include "esphome/components/sensor/sensor.h" + +#include <optional> + +namespace esphome::sendspin_ { + +class SendspinTrackProgressSensor : public sensor::Sensor, public SendspinPollingChild { + public: + void dump_config() override; + void setup() override; + void update() override; +}; + +enum class SendspinNumericMetadataTypes { + TRACK_DURATION, + YEAR, + TRACK, +}; + +class SendspinMetadataSensor : public sensor::Sensor, public SendspinChild { + public: + void dump_config() override; + void setup() override; + + void set_metadata_type(SendspinNumericMetadataTypes metadata_type) { this->metadata_type_ = metadata_type; } + + protected: + std::optional<float> extract_value_(const sendspin::ServerMetadataStateObject &metadata) const; + void publish_if_changed_(float value); + + SendspinNumericMetadataTypes metadata_type_; +}; + +} // namespace esphome::sendspin_ +#endif diff --git a/tests/components/sendspin/common-sensor.yaml b/tests/components/sendspin/common-sensor.yaml new file mode 100644 index 00000000000..6d9745cff94 --- /dev/null +++ b/tests/components/sendspin/common-sensor.yaml @@ -0,0 +1,15 @@ +<<: !include common.yaml + +sensor: + - platform: sendspin + name: "Sendspin Track Progress" + type: track_progress + - platform: sendspin + name: "Sendspin Track Duration" + type: track_duration + - platform: sendspin + name: "Sendspin Year" + type: year + - platform: sendspin + name: "Sendspin Track" + type: track diff --git a/tests/components/sendspin/test-sensor.esp32-idf.yaml b/tests/components/sendspin/test-sensor.esp32-idf.yaml new file mode 100644 index 00000000000..f9127d47bc0 --- /dev/null +++ b/tests/components/sendspin/test-sensor.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-sensor.yaml From 94e300389c8accb7387d342e6d9ce75cad694fa7 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 11:35:32 -0400 Subject: [PATCH 1643/2056] [sendspin] remove year and track number text sensors and refactor (#15975) --- .../sendspin/text_sensor/__init__.py | 2 - .../text_sensor/sendspin_text_sensor.cpp | 81 ++++++------------- .../text_sensor/sendspin_text_sensor.h | 5 +- .../sendspin/common-text_sensor.yaml | 6 -- 4 files changed, 29 insertions(+), 65 deletions(-) diff --git a/esphome/components/sendspin/text_sensor/__init__.py b/esphome/components/sendspin/text_sensor/__init__.py index b7f216ca0ce..87f6c9b9362 100644 --- a/esphome/components/sendspin/text_sensor/__init__.py +++ b/esphome/components/sendspin/text_sensor/__init__.py @@ -21,8 +21,6 @@ SENDSPIN_TEXT_METADATA_TYPES = { "artist": SendspinTextMetadataTypes.ARTIST, "album": SendspinTextMetadataTypes.ALBUM, "album_artist": SendspinTextMetadataTypes.ALBUM_ARTIST, - "year": SendspinTextMetadataTypes.YEAR, - "track": SendspinTextMetadataTypes.TRACK, } diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp index d16d51f63c6..9843fb966ec 100644 --- a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp @@ -2,8 +2,6 @@ #if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR) -#include "esphome/core/helpers.h" - #include <sendspin/metadata_role.h> #include <string> @@ -14,63 +12,36 @@ static const char *const TAG = "sendspin.text_sensor"; void SendspinTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sendspin", this); } +const char *SendspinTextSensor::extract_value_(const sendspin::ServerMetadataStateObject &metadata) const { + switch (this->metadata_type_) { + case SendspinTextMetadataTypes::TITLE: + if (metadata.title.has_value()) + return metadata.title.value().c_str(); + return nullptr; + case SendspinTextMetadataTypes::ARTIST: + if (metadata.artist.has_value()) + return metadata.artist.value().c_str(); + return nullptr; + case SendspinTextMetadataTypes::ALBUM: + if (metadata.album.has_value()) + return metadata.album.value().c_str(); + return nullptr; + case SendspinTextMetadataTypes::ALBUM_ARTIST: + if (metadata.album_artist.has_value()) + return metadata.album_artist.value().c_str(); + return nullptr; + } + return nullptr; +} + // THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop // (SendspinHub dispatches metadata from client_->loop()). void SendspinTextSensor::setup() { - switch (this->metadata_type_) { - case SendspinTextMetadataTypes::TITLE: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.title.has_value()) { - this->publish_if_changed_(metadata.title.value().c_str()); - } - }); - break; + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (const char *value = this->extract_value_(metadata)) { + this->publish_if_changed_(value); } - case SendspinTextMetadataTypes::ARTIST: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.artist.has_value()) { - this->publish_if_changed_(metadata.artist.value().c_str()); - } - }); - break; - } - case SendspinTextMetadataTypes::ALBUM: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.album.has_value()) { - this->publish_if_changed_(metadata.album.value().c_str()); - } - }); - break; - } - case SendspinTextMetadataTypes::ALBUM_ARTIST: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.album_artist.has_value()) { - this->publish_if_changed_(metadata.album_artist.value().c_str()); - } - }); - break; - } - case SendspinTextMetadataTypes::YEAR: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.year.has_value() && metadata.year.value() <= 9999) { - char buf[UINT32_MAX_STR_SIZE]; - uint32_to_str(buf, metadata.year.value()); - this->publish_if_changed_(buf); - } - }); - break; - } - case SendspinTextMetadataTypes::TRACK: { - this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { - if (metadata.track.has_value() && metadata.track.value() <= 9999) { - char buf[UINT32_MAX_STR_SIZE]; - uint32_to_str(buf, metadata.track.value()); - this->publish_if_changed_(buf); - } - }); - break; - } - } + }); } // Dedup to avoid frontend churn; TextSensor::publish_state already dedups the string assign but still notifies. diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h index d9ef49c938c..203b01d0248 100644 --- a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h @@ -7,6 +7,8 @@ #include "esphome/components/sendspin/sendspin_hub.h" #include "esphome/components/text_sensor/text_sensor.h" +#include <sendspin/metadata_role.h> + namespace esphome::sendspin_ { enum class SendspinTextMetadataTypes { @@ -14,8 +16,6 @@ enum class SendspinTextMetadataTypes { ARTIST, ALBUM, ALBUM_ARTIST, - YEAR, - TRACK, }; class SendspinTextSensor : public SendspinChild, public text_sensor::TextSensor { @@ -26,6 +26,7 @@ class SendspinTextSensor : public SendspinChild, public text_sensor::TextSensor void set_metadata_type(SendspinTextMetadataTypes metadata_type) { this->metadata_type_ = metadata_type; } protected: + const char *extract_value_(const sendspin::ServerMetadataStateObject &metadata) const; void publish_if_changed_(const char *value); SendspinTextMetadataTypes metadata_type_; diff --git a/tests/components/sendspin/common-text_sensor.yaml b/tests/components/sendspin/common-text_sensor.yaml index 0bfbf457574..fc6a56a21ad 100644 --- a/tests/components/sendspin/common-text_sensor.yaml +++ b/tests/components/sendspin/common-text_sensor.yaml @@ -13,9 +13,3 @@ text_sensor: - platform: sendspin name: "Album Artist" type: album_artist - - platform: sendspin - name: "Year" - type: year - - platform: sendspin - name: "Track Number" - type: track From 9caf9ee02336fb7754ead08a11fd2da10c77d74a Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 24 Apr 2026 12:53:03 -0400 Subject: [PATCH 1644/2056] [sendspin] Bumps sendspin-cpp library for a bugfix (#15976) --- esphome/components/sendspin/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 6f5ccddb86d..58687ae8389 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -193,7 +193,7 @@ async def to_code(config: ConfigType) -> None: ) # sendspin-cpp library - esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.0") + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.1") cg.add_define("USE_SENDSPIN", True) # for MDNS diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f422d94097d..11531e6d7b4 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -92,6 +92,6 @@ dependencies: esp32async/asynctcp: version: 3.4.91 sendspin/sendspin-cpp: - version: 0.3.0 + version: 0.3.1 lvgl/lvgl: version: 9.5.0 From f36efbc762b08b51cf4766a2ac441c9ceee3abec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:27:12 +0000 Subject: [PATCH 1645/2056] Update tzdata requirement from >=2026.1 to >=2026.2 (#15980) 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 90b06938408..71db6d34447 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ colorama==0.4.6 icmplib==3.0.4 tornado==6.5.5 tzlocal==5.3.1 # from time -tzdata>=2026.1 # from time +tzdata>=2026.2 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 From f62972c2c6aaa78c2f9a798b30bc52609f959d6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:34:00 +0000 Subject: [PATCH 1646/2056] Bump ruff from 0.15.11 to 0.15.12 (#15981) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9b7df6ec53..ad82bd8e5d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.11 + rev: v0.15.12 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index bb98375cb65..b35025fa04a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.11 # also change in .pre-commit-config.yaml when updating +ruff==0.15.12 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From c27f9e512b63ced298e3634eb2cc17be92d521b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 02:28:04 +0000 Subject: [PATCH 1647/2056] Bump aioesphomeapi from 44.21.0 to 44.22.0 (#15989) 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 71db6d34447..c1838d3b512 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260408.1 -aioesphomeapi==44.21.0 +aioesphomeapi==44.22.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From a437b3086bed5e7dae2c448c09ff4d0df6320bf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 02:30:10 +0000 Subject: [PATCH 1648/2056] Bump cryptography from 46.0.7 to 47.0.0 (#15990) 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 c1838d3b512..ba7adaa747c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==46.0.7 +cryptography==47.0.0 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From b5ccd55f4ed8cf9c756505a0b2c1767f5d957be5 Mon Sep 17 00:00:00 2001 From: Javier Peletier <jpeletier@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:06:58 +0200 Subject: [PATCH 1649/2056] [packages] Fix premature substitution of vars in remote package files (#15997) Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/packages/__init__.py | 21 ++- esphome/yaml_util.py | 11 +- .../component_tests/packages/test_packages.py | 130 ++++++++++++++++++ tests/unit_tests/test_yaml_util.py | 62 ++++++++- 4 files changed, 219 insertions(+), 5 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index b6ec0067c93..1b9e03d88fc 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -378,9 +378,8 @@ def _substitute_package_definition( Local package contents are left untouched — they will be substituted later during the main substitution pass. """ - if isinstance(package_config, str) or ( - isinstance(package_config, dict) and is_remote_package(package_config) - ): + + def do_substitute(package_config: dict | str) -> dict | str: # Collect undefined-variable errors (rather than raising strict) so the # path walked through a remote-package dict is preserved and the user # sees which field (url / path / ref / ...) referenced the undefined @@ -394,6 +393,22 @@ def _substitute_package_definition( errors=errors, ) raise_first_undefined(errors, "package definition") + return package_config + + if isinstance(package_config, str): + return do_substitute(package_config) + + if isinstance(package_config, dict) and is_remote_package(package_config): + # Mark vars as literal to avoid substituting variables in the vars block itself, since they are meant to be + # passed as-is to the package YAML and may contain their own substitution expressions that should not + # be prematurely evaluated here. + if CONF_FILES in package_config: + for file_def in package_config[CONF_FILES]: + if isinstance(file_def, dict) and CONF_VARS in file_def: + file_def[CONF_VARS] = yaml_util.make_literal(file_def[CONF_VARS]) + + package_config = do_substitute(package_config) + return package_config diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 42da27ec142..3cfc9c4b15d 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -113,6 +113,15 @@ def make_data_base( return value +def make_literal(value: Any) -> ESPLiteralValue | Any: + """Wrap a value in an ESPLiteralValue object.""" + try: + return add_class_to_obj(value, ESPLiteralValue) + except TypeError: + # Adding class failed, ignore error + return value + + def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: """Tags a list/string/dict value with context vars that must be applied to it and its children during the substitution pass. If no vars are given, no tagging is done. @@ -525,7 +534,7 @@ class ESPHomeLoaderMixin: obj = self.construct_sequence(node) elif isinstance(node, yaml.MappingNode): obj = self.construct_mapping(node) - return add_class_to_obj(obj, ESPLiteralValue) + return make_literal(obj) @_add_data_ref def construct_extend(self, node: yaml.Node) -> Extend: diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index af4b6db7961..13a6da9f2c6 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -1491,3 +1491,133 @@ def test_substitute_package_definition_includes_source_location(tmp_path: Path) line, col = int(match.group(1)), int(match.group(2)) assert line == 2, f"expected 1-based line 2, got {line} (err={err!r})" assert col >= 1, f"expected 1-based column ≥ 1, got {col} (err={err!r})" + + +def test_substitute_package_definition_vars_preserved_literally() -> None: + """``vars:`` blocks in remote-package files are not substituted prematurely. + + Variable references inside ``vars:`` may resolve to substitutions + contributed by sibling packages that have not yet been loaded, so they + must be passed through untouched and resolved later by the package YAML. + """ + pkg = { + CONF_URL: "https://github.com/esphome/non-existant-repo", + CONF_REF: "main", + CONF_FILES: [ + { + CONF_PATH: "common/somefile.yaml", + CONF_VARS: {"pin": "${PIN}"}, + }, + ], + } + # Note: PIN is intentionally NOT in the context — it is meant to + # be resolved later, when the package YAML is processed. + result = _substitute_package_definition(pkg, ContextVars()) + + assert result[CONF_FILES][0][CONF_VARS] == {"pin": "${PIN}"} + + +def test_substitute_package_definition_other_fields_still_substituted() -> None: + """Marking ``vars:`` literal does not stop substitution of url/ref/path.""" + ctx = ContextVars({"branch": "release", "org": "esphome"}) + pkg = { + CONF_URL: "https://github.com/${org}/firmware", + CONF_REF: "${branch}", + CONF_FILES: [ + { + CONF_PATH: "common/sensor.yaml", + CONF_VARS: {"pin": "${PIN}"}, + }, + ], + } + result = _substitute_package_definition(pkg, ctx) + + assert result[CONF_URL] == "https://github.com/esphome/firmware" + assert result[CONF_REF] == "release" + # vars passed through unchanged + assert result[CONF_FILES][0][CONF_VARS] == {"pin": "${PIN}"} + + +def test_substitute_package_definition_without_vars_unaffected() -> None: + """Files entries without a ``vars:`` block continue to work.""" + ctx = ContextVars({"branch": "main"}) + pkg = { + CONF_URL: "https://github.com/esphome/firmware", + CONF_REF: "${branch}", + CONF_FILES: [ + {CONF_PATH: "file1.yaml"}, + "file2.yaml", + ], + } + result = _substitute_package_definition(pkg, ctx) + + assert result[CONF_REF] == "main" + assert result[CONF_FILES][0] == {CONF_PATH: "file1.yaml"} + assert result[CONF_FILES][1] == "file2.yaml" + + +@patch("esphome.yaml_util.load_yaml") +@patch("pathlib.Path.is_file") +@patch("esphome.git.clone_or_update") +def test_remote_package_vars_resolved_against_sibling_package_substitutions( + mock_clone_or_update, mock_is_file, mock_load_yaml +) -> None: + """A ``vars:`` reference in one remote package can resolve to a + substitution defined in a sibling remote package. + + A higher-priority package declares ``substitutions:`` (e.g. ``SENSOR_PIN: 5``) and a + lower-priority package's ``files: -> vars:`` references that substitution. + Because packages are processed highest-priority first and ``vars:`` is now + preserved literally during package-definition processing, the substitution + is resolved correctly when the package YAML itself is loaded. + """ + mock_clone_or_update.return_value = (Path("/tmp/noexists"), MagicMock()) + mock_is_file.return_value = True + + # Two YAML files mocked from the "remote" repo: + # - platform.yaml exports a substitution ``SENSOR_PIN`` + # - sensor.yaml uses ``${pin}`` (which is bound from ``vars:`` to + # ``${SENSOR_PIN}`` and resolved against the merged substitutions). + mock_load_yaml.side_effect = [ + # Order matches reverse-priority traversal (highest priority first). + OrderedDict( + { + CONF_SUBSTITUTIONS: {"SENSOR_PIN": "GPIO5"}, + } + ), + OrderedDict( + { + CONF_SENSOR: [ + { + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + "pin": "${pin}", + } + ], + } + ), + ] + + config = { + CONF_PACKAGES: { + "special_sensor": { + CONF_URL: "https://github.com/esphome/non-existant-repo", + CONF_FILES: [ + { + CONF_PATH: "sensor.yaml", + CONF_VARS: {"pin": "${SENSOR_PIN}"}, + }, + ], + CONF_REFRESH: "1d", + }, + "platform": { + CONF_URL: "https://github.com/esphome/non-existant-repo", + CONF_FILES: ["platform.yaml"], + CONF_REFRESH: "1d", + }, + } + } + + actual = packages_pass(config) + + assert actual[CONF_SENSOR][0]["pin"] == "GPIO5" diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index e3aa2a16f56..3815ac1d752 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -11,7 +11,13 @@ from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv from esphome.core import DocumentLocation, DocumentRange, EsphomeError from esphome.util import OrderedDict -from esphome.yaml_util import ESPHomeDataBase, format_path, make_data_base +from esphome.yaml_util import ( + ESPHomeDataBase, + ESPLiteralValue, + format_path, + make_data_base, + make_literal, +) @pytest.fixture(autouse=True) @@ -891,3 +897,57 @@ def test_format_path_empty_path_with_located_current_obj(): obj = _located("${var}", "main.yaml", 0, 0) result = format_path([], obj) assert result == "In: in main.yaml 1:1" + + +def test_make_literal_wraps_dict() -> None: + """A dict is wrapped so it becomes an ESPLiteralValue instance.""" + value = {"key": "${var}"} + result = make_literal(value) + assert isinstance(result, ESPLiteralValue) + assert isinstance(result, dict) + assert result == {"key": "${var}"} + + +def test_make_literal_wraps_list() -> None: + """A list is wrapped so it becomes an ESPLiteralValue instance.""" + value = ["${var}", "plain"] + result = make_literal(value) + assert isinstance(result, ESPLiteralValue) + assert isinstance(result, list) + assert result == ["${var}", "plain"] + + +def test_make_literal_wraps_string() -> None: + """A string is wrapped so it becomes an ESPLiteralValue instance.""" + result = make_literal("${var}") + assert isinstance(result, ESPLiteralValue) + assert result == "${var}" + + +def test_make_literal_returns_already_wrapped_value_unchanged() -> None: + """Wrapping a value that is already an ESPLiteralValue returns it as-is.""" + value = make_literal({"key": "value"}) + assert isinstance(value, ESPLiteralValue) + result = make_literal(value) + assert result is value + + +def test_make_literal_returns_none_unchanged() -> None: + """Values whose class cannot be augmented (e.g. ``None``) are returned as-is.""" + result = make_literal(None) + assert result is None + + +def test_make_literal_blocks_substitution() -> None: + """A value wrapped with make_literal is skipped by the substitution pass.""" + value = make_literal({"pin": "${PIN}"}) + result = substitutions.substitute( + value, + path=[], + parent_context=substitutions.ContextVars(), + strict_undefined=False, + ) + # The literal block must remain untouched, even though the variable is + # undefined in the context. + assert result == {"pin": "${PIN}"} + assert isinstance(result, ESPLiteralValue) From 4f8feb86f0fdea15ff09dbfee20b10ee899ae2e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 25 Apr 2026 15:43:05 -0500 Subject: [PATCH 1650/2056] [dashboard] Add --no-states support to logs WebSocket handler (#15993) --- esphome/dashboard/web_server.py | 6 +++- tests/dashboard/test_web_server.py | 58 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index b8e17244e53..d67245967c5 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -437,7 +437,11 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): class EsphomeLogsHandler(EsphomePortCommandWebSocket): async def build_command(self, json_message: dict[str, Any]) -> list[str]: """Build the command to run.""" - return await self.build_device_command(["logs"], json_message) + cmd = await self.build_device_command(["logs"], json_message) + if json_message.get("no_states"): + cmd.append("--no-states") + _LOGGER.debug("Built command: %s", cmd) + return cmd class EsphomeRenameHandler(EsphomeCommandWebSocket): diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index daff3845158..1a62cfda904 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -1744,6 +1744,64 @@ def test_proc_on_exit_skips_when_already_closed() -> None: handler.close.assert_not_called() +@pytest.mark.asyncio +async def test_esphome_logs_handler_appends_no_states_when_set() -> None: + """Test --no-states is appended when no_states is truthy in the message.""" + handler = Mock(spec=web_server.EsphomeLogsHandler) + handler.build_device_command = AsyncMock( + return_value=["esphome", "logs", "device.yaml", "--device", "OTA"] + ) + + json_message = { + "configuration": "device.yaml", + "port": "OTA", + "no_states": True, + } + cmd = await web_server.EsphomeLogsHandler.build_command(handler, json_message) + + assert cmd == [ + "esphome", + "logs", + "device.yaml", + "--device", + "OTA", + "--no-states", + ] + handler.build_device_command.assert_awaited_once_with(["logs"], json_message) + + +@pytest.mark.asyncio +async def test_esphome_logs_handler_omits_no_states_when_missing() -> None: + """Test --no-states is not added when no_states is absent from the message.""" + handler = Mock(spec=web_server.EsphomeLogsHandler) + handler.build_device_command = AsyncMock( + return_value=["esphome", "logs", "device.yaml", "--device", "OTA"] + ) + + cmd = await web_server.EsphomeLogsHandler.build_command( + handler, {"configuration": "device.yaml", "port": "OTA"} + ) + + assert "--no-states" not in cmd + assert cmd == ["esphome", "logs", "device.yaml", "--device", "OTA"] + + +@pytest.mark.asyncio +async def test_esphome_logs_handler_omits_no_states_when_false() -> None: + """Test --no-states is not added when no_states is explicitly False.""" + handler = Mock(spec=web_server.EsphomeLogsHandler) + handler.build_device_command = AsyncMock( + return_value=["esphome", "logs", "device.yaml", "--device", "OTA"] + ) + + cmd = await web_server.EsphomeLogsHandler.build_command( + handler, + {"configuration": "device.yaml", "port": "OTA", "no_states": False}, + ) + + assert "--no-states" not in cmd + + def _make_auth_handler(auth_header: str | None = None) -> Mock: """Create a mock handler with the given Authorization header.""" handler = Mock() From 9ad820c9214a3b7d6e2d93b82e6d88d270d4ffd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:59:01 +0000 Subject: [PATCH 1651/2056] Bump esphome-dashboard from 20260408.1 to 20260425.0 (#16006) 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 ba7adaa747c..abc8ac5dbb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.2.0 click==8.3.3 -esphome-dashboard==20260408.1 +esphome-dashboard==20260425.0 aioesphomeapi==44.22.0 zeroconf==0.148.0 puremagic==1.30 From 4cab262ef8bec892ea274e58a17e4d15a8784e4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sat, 25 Apr 2026 16:18:21 -0500 Subject: [PATCH 1652/2056] [ci] Trigger CodSpeed benchmarks on host platform changes (#15995) --- script/determine-jobs.py | 11 +++++++++-- tests/script/test_determine_jobs.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index d94d472c9ed..f036447542d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -402,8 +402,11 @@ def should_run_benchmarks(branch: str | None = None) -> bool: Benchmarks run when any of the following conditions are met: 1. Core C++ files changed (esphome/core/*) - 2. A directly changed component has benchmark files (no dependency expansion) - 3. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py, + 2. The host platform changed (esphome/components/host/*) — benchmarks + are built and run on the host platform, so its implementations of + ``millis()``/``micros()``/etc. affect every benchmark + 3. A directly changed component has benchmark files (no dependency expansion) + 4. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py, script/build_helpers.py, script/setup_codspeed_lib.py) Unlike unit tests, benchmarks do NOT expand to dependent components. @@ -420,6 +423,10 @@ def should_run_benchmarks(branch: str | None = None) -> bool: if core_changed(files): return True + # Host platform supplies the runtime that benchmarks execute on + if any(f.startswith("esphome/components/host/") for f in files): + return True + # Check if benchmark infrastructure changed if any( f.startswith("tests/benchmarks/") or f in BENCHMARK_INFRASTRUCTURE_FILES diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index de239ee0b55..2c726734fe7 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1842,6 +1842,22 @@ def test_should_run_benchmarks_core_header_change() -> None: assert determine_jobs.should_run_benchmarks() is True +def test_should_run_benchmarks_host_platform_change() -> None: + """Test benchmarks trigger on host platform changes. + + Benchmarks build and run on the host platform, so changes to its + millis()/micros()/etc. implementations affect every benchmark. + """ + for host_file in [ + "esphome/components/host/core.cpp", + "esphome/components/host/__init__.py", + ]: + with patch.object(determine_jobs, "changed_files", return_value=[host_file]): + assert determine_jobs.should_run_benchmarks() is True, ( + f"Expected benchmarks to run for {host_file}" + ) + + def test_should_run_benchmarks_benchmark_infra_change() -> None: """Test benchmarks trigger on benchmark infrastructure changes.""" for infra_file in [ From bc33260c61f2f635f7240ea7e5e1380fee504e95 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Sat, 25 Apr 2026 22:33:02 -0500 Subject: [PATCH 1653/2056] [ir_rf_proxy] Extend for RF (#15744) Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/ir_rf_proxy/ir_rf_proxy.cpp | 112 +++++++++++++++++- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 37 ++++++ .../components/ir_rf_proxy/radio_frequency.py | 68 +++++++++++ .../components/radio_frequency/common-rx.yaml | 18 +++ .../components/radio_frequency/common-tx.yaml | 19 +++ tests/components/radio_frequency/common.yaml | 7 ++ .../radio_frequency/test.bk72xx-ard.yaml | 8 ++ .../radio_frequency/test.esp32-idf.yaml | 8 ++ .../radio_frequency/test.esp8266-ard.yaml | 8 ++ .../radio_frequency/test.rp2040-ard.yaml | 8 ++ 10 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 esphome/components/ir_rf_proxy/radio_frequency.py create mode 100644 tests/components/radio_frequency/common-rx.yaml create mode 100644 tests/components/radio_frequency/common-tx.yaml create mode 100644 tests/components/radio_frequency/common.yaml create mode 100644 tests/components/radio_frequency/test.bk72xx-ard.yaml create mode 100644 tests/components/radio_frequency/test.esp32-idf.yaml create mode 100644 tests/components/radio_frequency/test.esp8266-ard.yaml create mode 100644 tests/components/radio_frequency/test.rp2040-ard.yaml diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp index 5239a4667c0..60b0cd513bb 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -1,13 +1,73 @@ #include "ir_rf_proxy.h" + +#include <cinttypes> + #include "esphome/core/log.h" namespace esphome::ir_rf_proxy { static const char *const TAG = "ir_rf_proxy"; +// ========== Shared transmit helper ========== +// Static template: all instantiations occur in this translation unit. + +template<typename CallT> +static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency, + const CallT &call) { + if (transmitter == nullptr) { + ESP_LOGW(TAG, "No transmitter configured"); + return; + } + + if (!call.has_raw_timings()) { + ESP_LOGE(TAG, "No raw timings provided"); + return; + } + + auto transmit_call = transmitter->transmit(); + auto *transmit_data = transmit_call.get_data(); + transmit_data->set_carrier_frequency(carrier_frequency); + + if (call.is_packed()) { + transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(), + call.get_packed_count()); + ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(), + call.get_repeat_count()); + } else if (call.is_base64url()) { + if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) { + ESP_LOGE(TAG, "Invalid base64url data"); + return; + } + constexpr int32_t max_timing_us = 500000; + for (int32_t timing : transmit_data->get_data()) { + int32_t abs_timing = timing < 0 ? -timing : timing; + if (abs_timing > max_timing_us) { + ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us); + return; + } + } + ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(), + call.get_repeat_count()); + } else { + transmit_data->set_data(call.get_raw_timings()); + ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(), + call.get_repeat_count()); + } + + if (call.get_repeat_count() > 0) { + transmit_call.set_send_times(call.get_repeat_count()); + } + + transmit_call.perform(); +} + +// ========== IrRfProxy (Infrared platform) ========== + +#ifdef USE_IR_RF + void IrRfProxy::dump_config() { ESP_LOGCONFIG(TAG, - "IR/RF Proxy '%s'\n" + "IR Proxy '%s'\n" " Supports Transmitter: %s\n" " Supports Receiver: %s", this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), @@ -20,4 +80,54 @@ void IrRfProxy::dump_config() { } } +void IrRfProxy::control(const infrared::InfraredCall &call) { + uint32_t carrier = call.get_carrier_frequency().value_or(0); + transmit_raw_timings(this->transmitter_, carrier, call); +} + +#endif // USE_IR_RF + +// ========== RfProxy (Radio Frequency platform) ========== + +#ifdef USE_RADIO_FREQUENCY + +void RfProxy::setup() { + this->traits_.set_supports_transmitter(this->transmitter_ != nullptr); + this->traits_.set_supports_receiver(this->receiver_ != nullptr); + + // remote_transmitter/receiver always uses OOK (on-off keying) + this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK); + + if (this->receiver_ != nullptr) { + this->receiver_->register_listener(this); + } +} + +void RfProxy::dump_config() { + ESP_LOGCONFIG(TAG, + "RF Proxy '%s'\n" + " Backend: remote_transmitter/receiver\n" + " Supports Transmitter: %s\n" + " Supports Receiver: %s", + this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), + YESNO(this->traits_.get_supports_receiver())); + + const auto &traits = this->traits_; + if (traits.get_frequency_min_hz() > 0) { + if (traits.get_frequency_min_hz() == traits.get_frequency_max_hz()) { + ESP_LOGCONFIG(TAG, " Frequency: %.3f MHz (fixed)", traits.get_frequency_min_hz() / 1e6f); + } else { + ESP_LOGCONFIG(TAG, " Frequency Range: %.3f - %.3f MHz", traits.get_frequency_min_hz() / 1e6f, + traits.get_frequency_max_hz() / 1e6f); + } + } +} + +void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) { + // RF: no IR carrier modulation + transmit_raw_timings(this->transmitter_, 0, call); +} + +#endif // USE_RADIO_FREQUENCY + } // namespace esphome::ir_rf_proxy diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 05b988f2877..973e9e20514 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -4,10 +4,19 @@ // without following the normal breaking changes policy. Use at your own risk. // Once the API is considered stable, this warning will be removed. +#include "esphome/components/remote_base/remote_base.h" + +#ifdef USE_IR_RF #include "esphome/components/infrared/infrared.h" +#endif + +#ifdef USE_RADIO_FREQUENCY +#include "esphome/components/radio_frequency/radio_frequency.h" +#endif namespace esphome::ir_rf_proxy { +#ifdef USE_IR_RF /// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend class IrRfProxy : public infrared::Infrared { public: @@ -26,8 +35,36 @@ class IrRfProxy : public infrared::Infrared { void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); } protected: + void control(const infrared::InfraredCall &call) override; + // RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF uint32_t frequency_khz_{0}; }; +#endif // USE_IR_RF + +#ifdef USE_RADIO_FREQUENCY +/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend +class RfProxy : public radio_frequency::RadioFrequency { + public: + RfProxy() = default; + + void setup() override; + void dump_config() override; + + /// Set the remote transmitter component + void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } + /// Set the remote receiver component + void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } + + /// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware) + void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); } + + protected: + void control(const radio_frequency::RadioFrequencyCall &call) override; + + remote_base::RemoteTransmitterBase *transmitter_{nullptr}; + remote_base::RemoteReceiverBase *receiver_{nullptr}; +}; +#endif // USE_RADIO_FREQUENCY } // namespace esphome::ir_rf_proxy diff --git a/esphome/components/ir_rf_proxy/radio_frequency.py b/esphome/components/ir_rf_proxy/radio_frequency.py new file mode 100644 index 00000000000..9982f5e4d10 --- /dev/null +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -0,0 +1,68 @@ +"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver).""" + +import esphome.codegen as cg +from esphome.components import radio_frequency, remote_receiver, remote_transmitter +import esphome.config_validation as cv +from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY +import esphome.final_validate as fv +from esphome.types import ConfigType + +from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["radio_frequency"] + +RfProxy = ir_rf_proxy_ns.class_("RfProxy", radio_frequency.RadioFrequency) + +CONFIG_SCHEMA = cv.All( + radio_frequency.radio_frequency_schema(RfProxy).extend( + { + cv.Optional(CONF_FREQUENCY): cv.frequency, + cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id( + remote_receiver.RemoteReceiverComponent + ), + cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + } + ), + cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID), +) + + +def _final_validate(config: ConfigType) -> None: + """Validate that RF transmitters have carrier duty set to 100%.""" + if CONF_REMOTE_TRANSMITTER_ID not in config: + return + + transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID] + full_config = fv.full_config.get() + transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1] + transmitter_config = full_config.get_config_for_path(transmitter_path) + + duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT) + if duty_percent is not None and duty_percent != 100: + raise cv.Invalid( + f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' " + "set to 100% for RF transmission. Dedicated RF hardware handles modulation; " + "applying a carrier duty cycle would corrupt the signal" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: ConfigType) -> None: + """Code generation for remote_base radio frequency platform.""" + var = await radio_frequency.new_radio_frequency(config) + + if CONF_FREQUENCY in config: + cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY]))) + + if CONF_REMOTE_TRANSMITTER_ID in config: + transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) + + if CONF_REMOTE_RECEIVER_ID in config: + receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) + cg.add(var.set_receiver(receiver)) diff --git a/tests/components/radio_frequency/common-rx.yaml b/tests/components/radio_frequency/common-rx.yaml new file mode 100644 index 00000000000..bcfa1f10c71 --- /dev/null +++ b/tests/components/radio_frequency/common-rx.yaml @@ -0,0 +1,18 @@ +remote_receiver: + id: rf_receiver + pin: ${rx_pin} + +# Test radio_frequency platform with receiver +radio_frequency: + # RF 900MHz receiver + - platform: ir_rf_proxy + id: rf_900_rx + name: "RF 900 Receiver" + frequency: 900 MHz + remote_receiver_id: rf_receiver + + # RF receiver (no frequency specified) + - platform: ir_rf_proxy + id: rf_rx + name: "RF Receiver" + remote_receiver_id: rf_receiver diff --git a/tests/components/radio_frequency/common-tx.yaml b/tests/components/radio_frequency/common-tx.yaml new file mode 100644 index 00000000000..778dd68d1ef --- /dev/null +++ b/tests/components/radio_frequency/common-tx.yaml @@ -0,0 +1,19 @@ +remote_transmitter: + id: rf_transmitter + pin: ${tx_pin} + carrier_duty_percent: 100% + +# Test radio_frequency platform with transmitter +radio_frequency: + # RF 433MHz transmitter + - platform: ir_rf_proxy + id: rf_433_tx + name: "RF 433 Transmitter" + frequency: 433 MHz + remote_transmitter_id: rf_transmitter + + # RF transmitter (no frequency specified) + - platform: ir_rf_proxy + id: rf_tx + name: "RF Transmitter" + remote_transmitter_id: rf_transmitter diff --git a/tests/components/radio_frequency/common.yaml b/tests/components/radio_frequency/common.yaml new file mode 100644 index 00000000000..53a0cd379a9 --- /dev/null +++ b/tests/components/radio_frequency/common.yaml @@ -0,0 +1,7 @@ +network: + +wifi: + ssid: MySSID + password: password1 + +api: diff --git a/tests/components/radio_frequency/test.bk72xx-ard.yaml b/tests/components/radio_frequency/test.bk72xx-ard.yaml new file mode 100644 index 00000000000..a0e145f4762 --- /dev/null +++ b/tests/components/radio_frequency/test.bk72xx-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/radio_frequency/test.esp32-idf.yaml b/tests/components/radio_frequency/test.esp32-idf.yaml new file mode 100644 index 00000000000..a0e145f4762 --- /dev/null +++ b/tests/components/radio_frequency/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/radio_frequency/test.esp8266-ard.yaml b/tests/components/radio_frequency/test.esp8266-ard.yaml new file mode 100644 index 00000000000..a0e145f4762 --- /dev/null +++ b/tests/components/radio_frequency/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml diff --git a/tests/components/radio_frequency/test.rp2040-ard.yaml b/tests/components/radio_frequency/test.rp2040-ard.yaml new file mode 100644 index 00000000000..a0e145f4762 --- /dev/null +++ b/tests/components/radio_frequency/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + common: !include common.yaml + rx: !include common-rx.yaml + tx: !include common-tx.yaml From 58f6ad2d0ce727ae6df08d6a57eadedbfdc496a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 00:01:21 -0500 Subject: [PATCH 1654/2056] [safe_mode] Use StaticCallbackManager for on_safe_mode (#16002) --- esphome/components/safe_mode/__init__.py | 3 ++- esphome/components/safe_mode/safe_mode.h | 2 +- esphome/core/defines.h | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 6df0ba78b1f..578376258a1 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -76,8 +76,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if config.get(CONF_ON_SAFE_MODE): + if on_safe_mode := config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") + cg.add_define("ESPHOME_SAFE_MODE_CALLBACK_COUNT", len(on_safe_mode)) await automation.build_callback_automations( var, config, _CALLBACK_AUTOMATIONS ) diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 2733054962e..b458a9a3021 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -57,7 +57,7 @@ class SafeModeComponent final : public Component { // Larger objects at the end ESPPreferenceObject rtc_; #ifdef USE_SAFE_MODE_CALLBACK - CallbackManager<void()> safe_mode_callback_{}; + StaticCallbackManager<ESPHOME_SAFE_MODE_CALLBACK_COUNT, void()> safe_mode_callback_{}; #endif static const uint32_t ENTER_SAFE_MODE_MAGIC = diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 80247f69da1..297bf081c5a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -136,6 +136,7 @@ #define USE_PREFERENCES_SYNC_EVERY_LOOP #define USE_QR_CODE #define USE_SAFE_MODE_CALLBACK +#define ESPHOME_SAFE_MODE_CALLBACK_COUNT 1 #define USE_SELECT #define USE_SENSOR #define USE_SENSOR_FILTER From f092e619d8b37e71054636ba1b63848159c31438 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 00:03:59 -0500 Subject: [PATCH 1655/2056] [rtttl] Gate on_finished_playback callback storage behind define (#16003) --- esphome/components/rtttl/__init__.py | 4 +++- esphome/components/rtttl/rtttl.cpp | 2 ++ esphome/components/rtttl/rtttl.h | 6 ++++++ esphome/core/defines.h | 1 + tests/components/rtttl/common.yaml | 5 +++++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index c661aad972a..4880f9ac41a 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -93,7 +93,9 @@ async def to_code(config): cg.add(var.set_gain(config[CONF_GAIN])) - await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) + if config.get(CONF_ON_FINISHED_PLAYBACK): + cg.add_define("USE_RTTTL_FINISHED_PLAYBACK_CALLBACK") + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) @automation.register_action( diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 08d902b4be8..a5f8567c9da 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -424,7 +424,9 @@ void Rtttl::set_state_(State state) { // Clear loop_done when transitioning from `State::STOPPED` to any other state if (state == State::STOPPED) { this->disable_loop(); +#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK this->on_finished_playback_callback_.call(); +#endif ESP_LOGD(TAG, "Playback finished"); } else if (old_state == State::STOPPED) { this->enable_loop(); diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 98ed9ba1bf4..9dac92be2ad 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -2,6 +2,8 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #ifdef USE_OUTPUT #include "esphome/components/output/float_output.h" @@ -45,9 +47,11 @@ class Rtttl : public Component { bool is_playing() { return this->state_ != State::STOPPED; } +#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK template<typename F> void add_on_finished_playback_callback(F &&callback) { this->on_finished_playback_callback_.add(std::forward<F>(callback)); } +#endif protected: inline uint16_t get_integer_() { @@ -106,8 +110,10 @@ class Rtttl : public Component { uint32_t samples_gap_{0}; #endif // USE_SPEAKER +#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK /// The callback to call when playback is finished. CallbackManager<void()> on_finished_playback_callback_; +#endif }; template<typename... Ts> class PlayAction : public Action<Ts...> { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 297bf081c5a..f929b224ca4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -186,6 +186,7 @@ #define USE_MQTT #define USE_MQTT_COVER_JSON #define USE_NETWORK +#define USE_RTTTL_FINISHED_PLAYBACK_CALLBACK #define USE_RUNTIME_IMAGE_BMP #define USE_RUNTIME_IMAGE_PNG #define USE_RUNTIME_IMAGE_JPEG diff --git a/tests/components/rtttl/common.yaml b/tests/components/rtttl/common.yaml index 529713583be..a4d8f951f42 100644 --- a/tests/components/rtttl/common.yaml +++ b/tests/components/rtttl/common.yaml @@ -29,3 +29,8 @@ output: rtttl: output: rtttl_output + on_finished_playback: + - then: + - logger.log: "Playback finished 1" + - then: + - logger.log: "Playback finished 2" From dc57969afdc96dd546ceaca27474200f090e6c6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 03:39:24 -0500 Subject: [PATCH 1656/2056] [host] Use integer math in millis()/micros() (#15994) --- esphome/components/host/core.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 0ade4274feb..b067ebbf6ef 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -8,7 +8,6 @@ #include <csignal> #include <sched.h> #include <time.h> -#include <cmath> #include <cstdlib> namespace { @@ -22,9 +21,7 @@ void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); - time_t seconds = spec.tv_sec; - uint32_t ms = round(spec.tv_nsec / 1e6); - return ((uint32_t) seconds) * 1000U + ms; + return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000); } uint64_t millis_64() { struct timespec spec; @@ -43,9 +40,7 @@ void HOT delay(uint32_t ms) { uint32_t IRAM_ATTR HOT micros() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); - time_t seconds = spec.tv_sec; - uint32_t us = round(spec.tv_nsec / 1e3); - return ((uint32_t) seconds) * 1000000U + us; + return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { struct timespec ts; From 68625a1b76aafaa5a859cea3c090333dfa81ce40 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski <me@komzpa.net> Date: Sun, 26 Apr 2026 13:11:09 +0400 Subject: [PATCH 1657/2056] [core] Isolate generated build metadata (#16007) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/core/config.py | 8 ++- esphome/writer.py | 101 ++++++++++++++++++++------- tests/unit_tests/test_writer.py | 119 ++++++++++++++++++++++---------- 3 files changed, 167 insertions(+), 61 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index bf210876dfb..018e05f17b4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -242,6 +242,10 @@ PROJECT_MAX_LENGTH = 127 # Max board/model string length (must fit in single-byte varint for proto encoding) BOARD_MAX_LENGTH = 127 +# Keep in sync with ESPHOME_COMMENT_SIZE_MAX in esphome/core/application.h +# (C++ side includes the null terminator). +COMMENT_MAX_LEN = 255 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), @@ -275,7 +279,9 @@ CONFIG_SCHEMA = cv.All( cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN) ), cv.Optional(CONF_AREA): validate_area_config, - cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), + cv.Optional(CONF_COMMENT): cv.All( + cv.string, cv.ByteLength(max=COMMENT_MAX_LEN) + ), cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { diff --git a/esphome/writer.py b/esphome/writer.py index 787ecac6f6e..816c57a0bc1 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -22,7 +22,6 @@ from esphome.helpers import ( read_file, rmtree, walk_files, - write_file, write_file_if_changed, ) from esphome.storage_json import StorageJSON, storage_path @@ -171,6 +170,7 @@ VERSION_H_FORMAT = """\ DEFINES_H_TARGET = "esphome/core/defines.h" VERSION_H_TARGET = "esphome/core/version.h" BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" +BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp" ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -209,13 +209,22 @@ def copy_src_tree(): source_files_copy = source_files_map.copy() ignore_targets = [ - Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET, BUILD_INFO_DATA_H_TARGET) + Path(x) + for x in ( + DEFINES_H_TARGET, + VERSION_H_TARGET, + BUILD_INFO_DATA_H_TARGET, + BUILD_INFO_DATA_CPP_TARGET, + ) ] for t in ignore_targets: source_files_copy.pop(t, None) # Files to exclude from sources_changed tracking (generated files) - generated_files = {Path("esphome/core/build_info_data.h")} + generated_files = { + Path("esphome/core/build_info_data.h"), + Path("esphome/core/build_info_data.cpp"), + } sources_changed = False for fname in walk_files(CORE.relative_src_path("esphome")): @@ -268,12 +277,15 @@ def copy_src_tree(): build_info_data_h_path = CORE.relative_src_path( "esphome", "core", "build_info_data.h" ) + build_info_data_cpp_path = CORE.relative_src_path( + "esphome", "core", "build_info_data.cpp" + ) build_info_json_path = CORE.relative_build_path("build_info.json") config_hash, build_time, build_time_str, comment = get_build_info() # Defensively force a rebuild if the build_info files don't exist, or if # there was a config change which didn't actually cause a source change - if not build_info_data_h_path.exists(): + if not build_info_data_h_path.exists() or not build_info_data_cpp_path.exists(): sources_changed = True else: try: @@ -288,13 +300,19 @@ def copy_src_tree(): # Write build_info header and JSON metadata if sources_changed: - write_file( + # write_file_if_changed avoids bumping mtime on identical content, + # which is what makes the stable header actually isolate metadata churn. + write_file_if_changed( build_info_data_h_path, - generate_build_info_data_h( + generate_build_info_data_h(), + ) + write_file_if_changed( + build_info_data_cpp_path, + generate_build_info_data_cpp( config_hash, build_time, build_time_str, comment ), ) - write_file( + write_file_if_changed( build_info_json_path, json.dumps( { @@ -345,27 +363,60 @@ def get_build_info() -> tuple[int, int, str, str]: return config_hash, build_time, build_time_str, comment -def generate_build_info_data_h( - config_hash: int, build_time: int, build_time_str: str, comment: str -) -> str: - """Generate build_info_data.h header with config hash, build time, and comment.""" - # cpp_string_escape returns '"escaped"', slice off the quotes since template has them - escaped_comment = cpp_string_escape(comment)[1:-1] - # +1 for null terminator - comment_size = len(comment) + 1 - return f"""#pragma once -// Auto-generated build_info data -#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT -#define ESPHOME_BUILD_TIME {build_time} // NOLINT -#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT +def generate_build_info_data_h() -> str: + """Generate stable declarations for build info provided by generated C++.""" + return """#pragma once +// Auto-generated build_info declarations +#include <cstddef> +#include <cstdint> +#include <ctime> #ifdef USE_ESP8266 #include <pgmspace.h> -static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; -static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; -#else -static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; -static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; #endif + +namespace esphome { +extern const uint32_t ESPHOME_CONFIG_HASH; +extern const time_t ESPHOME_BUILD_TIME; +extern const size_t ESPHOME_COMMENT_SIZE; +#ifdef USE_ESP8266 +extern const char ESPHOME_BUILD_TIME_STR[] PROGMEM; +extern const char ESPHOME_COMMENT_STR[] PROGMEM; +#else +extern const char ESPHOME_BUILD_TIME_STR[]; +extern const char ESPHOME_COMMENT_STR[]; +#endif +} // namespace esphome +""" + + +def generate_build_info_data_cpp( + config_hash: int, build_time: int, build_time_str: str, comment: str +) -> str: + """Generate build_info_data.cpp with config hash, build time, and comment.""" + from esphome.core.config import COMMENT_MAX_LEN + + # Defense-in-depth clamp; errors="ignore" drops a partial trailing UTF-8 + # sequence so the literal never decodes to a truncated codepoint. + encoded = comment.encode("utf-8")[:COMMENT_MAX_LEN] + comment = encoded.decode("utf-8", errors="ignore") + # cpp_string_escape wraps in quotes; strip them since the template has them. + escaped_comment = cpp_string_escape(comment)[1:-1] + comment_size = len(comment.encode("utf-8")) + 1 # +1 for NUL + return f"""// Auto-generated build_info data +#include "esphome/core/build_info_data.h" + +namespace esphome {{ +const uint32_t ESPHOME_CONFIG_HASH = 0x{config_hash:08x}U; // NOLINT +const time_t ESPHOME_BUILD_TIME = {build_time}; // NOLINT +const size_t ESPHOME_COMMENT_SIZE = {comment_size}; // NOLINT +#ifdef USE_ESP8266 +const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; +const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; +#else +const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; +const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; +#endif +}} // namespace esphome """ diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 940a394c080..e76769e6a83 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -7,6 +7,7 @@ from datetime import datetime import json import os from pathlib import Path +import re import stat from typing import Any from unittest.mock import MagicMock, patch @@ -32,6 +33,7 @@ from esphome.writer import ( clean_build, clean_cmake_cache, copy_src_tree, + generate_build_info_data_cpp, generate_build_info_data_h, get_build_info, storage_should_clean, @@ -1615,49 +1617,62 @@ def test_get_build_info_build_time_str_format( def test_generate_build_info_data_h_format() -> None: """Test generate_build_info_data_h produces correct header content.""" - config_hash = 0x12345678 - build_time = 1700000000 - build_time_str = "2023-11-14 22:13:20 +0000" - comment = "Test comment" - - result = generate_build_info_data_h( - config_hash, build_time, build_time_str, comment - ) + result = generate_build_info_data_h() assert "#pragma once" in result - assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result - assert "#define ESPHOME_BUILD_TIME 1700000000" in result - assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1 - assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result - assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result + assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in result + assert "extern const time_t ESPHOME_BUILD_TIME;" in result + assert "extern const size_t ESPHOME_COMMENT_SIZE;" in result + assert "extern const char ESPHOME_BUILD_TIME_STR[]" in result + assert "extern const char ESPHOME_COMMENT_STR[]" in result def test_generate_build_info_data_h_esp8266_progmem() -> None: """Test generate_build_info_data_h includes PROGMEM for ESP8266.""" - result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment") + result = generate_build_info_data_h() # Should have ESP8266 PROGMEM conditional assert "#ifdef USE_ESP8266" in result assert "#include <pgmspace.h>" in result assert "PROGMEM" in result - # Both build time and comment should have PROGMEM versions + + +def test_generate_build_info_data_cpp_format() -> None: + """Test generate_build_info_data_cpp produces correct data definitions.""" + result = generate_build_info_data_cpp( + 0x12345678, 1700000000, "2023-11-14 22:13:20 +0000", "Test comment" + ) + + assert '#include "esphome/core/build_info_data.h"' in result + assert "const uint32_t ESPHOME_CONFIG_HASH = 0x12345678U;" in result + assert "const time_t ESPHOME_BUILD_TIME = 1700000000;" in result + assert "const size_t ESPHOME_COMMENT_SIZE = 13;" in result + assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result + assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result + + +def test_generate_build_info_data_cpp_esp8266_progmem() -> None: + """Test generate_build_info_data_cpp includes PROGMEM definitions.""" + result = generate_build_info_data_cpp(0xABCDEF01, 1700000000, "test", "comment") + + assert "#ifdef USE_ESP8266" in result assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result -def test_generate_build_info_data_h_hash_formatting() -> None: - """Test generate_build_info_data_h formats hash with leading zeros.""" +def test_generate_build_info_data_cpp_hash_formatting() -> None: + """Test generate_build_info_data_cpp formats hash with leading zeros.""" # Test with small hash value that needs leading zeros - result = generate_build_info_data_h(0x00000001, 0, "test", "") - assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result + result = generate_build_info_data_cpp(0x00000001, 0, "test", "") + assert "const uint32_t ESPHOME_CONFIG_HASH = 0x00000001U;" in result # Test with larger hash value - result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "") - assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result + result = generate_build_info_data_cpp(0xFFFFFFFF, 0, "test", "") + assert "const uint32_t ESPHOME_CONFIG_HASH = 0xffffffffU;" in result -def test_generate_build_info_data_h_comment_escaping() -> None: - r"""Test generate_build_info_data_h properly escapes special characters in comment. +def test_generate_build_info_data_cpp_comment_escaping() -> None: + r"""Test generate_build_info_data_cpp properly escapes special characters in comment. Uses cpp_string_escape which outputs octal escapes for special characters: - backslash (ASCII 92) -> \134 @@ -1665,26 +1680,52 @@ def test_generate_build_info_data_h_comment_escaping() -> None: - newline (ASCII 10) -> \012 """ # Test backslash escaping (ASCII 92 = octal 134) - result = generate_build_info_data_h(0, 0, "test", "backslash\\here") + result = generate_build_info_data_cpp(0, 0, "test", "backslash\\here") assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result # Test quote escaping (ASCII 34 = octal 042) - result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') + result = generate_build_info_data_cpp(0, 0, "test", 'has "quotes"') assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result # Test newline escaping (ASCII 10 = octal 012) - result = generate_build_info_data_h(0, 0, "test", "line1\nline2") + result = generate_build_info_data_cpp(0, 0, "test", "line1\nline2") assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result -def test_generate_build_info_data_h_empty_comment() -> None: - """Test generate_build_info_data_h handles empty comment.""" - result = generate_build_info_data_h(0, 0, "test", "") +def test_generate_build_info_data_cpp_empty_comment() -> None: + """Test generate_build_info_data_cpp handles empty comment.""" + result = generate_build_info_data_cpp(0, 0, "test", "") - assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator + assert "const size_t ESPHOME_COMMENT_SIZE = 1;" in result # Just null terminator assert 'ESPHOME_COMMENT_STR[] = ""' in result +def test_generate_build_info_data_cpp_comment_size_counts_utf8_bytes() -> None: + """Comment size is in encoded UTF-8 bytes, not characters.""" + # "héllo" = 6 UTF-8 bytes + NUL. + result = generate_build_info_data_cpp(0, 0, "test", "héllo") + assert "const size_t ESPHOME_COMMENT_SIZE = 7;" in result + + +def test_generate_build_info_data_cpp_comment_clamped_to_buffer() -> None: + """Generator clamps at byte level and never truncates mid-codepoint.""" + # 100 thermometer-with-VS-16 sequences = 700 bytes, past the 256 buffer. + result = generate_build_info_data_cpp(0, 0, "test", "🌡️" * 100) + + match = re.search(r"ESPHOME_COMMENT_SIZE = (\d+);", result) + assert match is not None + size = int(match.group(1)) + assert 1 < size <= 256 + + lit_match = re.search(r'ESPHOME_COMMENT_STR\[\] = "([^"]*)"', result) + assert lit_match is not None + raw = re.sub( + r"\\([0-7]{3})", lambda m: chr(int(m.group(1), 8)), lit_match.group(1) + ).encode("latin-1") + raw.decode("utf-8") # raises if truncation left a partial UTF-8 sequence + assert len(raw) == size - 1 + + @patch("esphome.writer.CORE") @patch("esphome.writer.iter_components") @patch("esphome.writer.walk_files") @@ -1758,15 +1799,21 @@ def test_copy_src_tree_writes_build_info_files( ): copy_src_tree() - # Verify build_info_data.h was written + # Verify build_info_data.h declarations and build_info_data.cpp values were written build_info_h_path = esphome_core_path / "build_info_data.h" assert build_info_h_path.exists() build_info_h_content = build_info_h_path.read_text() - assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content - assert "#define ESPHOME_BUILD_TIME" in build_info_h_content + assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in build_info_h_content assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content - assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content + assert "extern const size_t ESPHOME_COMMENT_SIZE;" in build_info_h_content assert "ESPHOME_COMMENT_STR" in build_info_h_content + build_info_cpp_path = esphome_core_path / "build_info_data.cpp" + assert build_info_cpp_path.exists() + build_info_cpp_content = build_info_cpp_path.read_text() + assert "const uint32_t ESPHOME_CONFIG_HASH = 0xdeadbeefU;" in build_info_cpp_content + assert "const time_t ESPHOME_BUILD_TIME" in build_info_cpp_content + assert "const size_t ESPHOME_COMMENT_SIZE" in build_info_cpp_content + assert "ESPHOME_COMMENT_STR" in build_info_cpp_content # Verify build_info.json was written build_info_json_path = build_path / "build_info.json" @@ -1833,7 +1880,9 @@ def test_copy_src_tree_detects_config_hash_change( # Verify build_info files were updated due to config_hash change assert build_info_h_path.exists() - new_content = build_info_h_path.read_text() + build_info_cpp_path = esphome_core_path / "build_info_data.cpp" + assert build_info_cpp_path.exists() + new_content = build_info_cpp_path.read_text() assert "0xdeadbeef" in new_content.lower() new_json = json.loads(build_info_json_path.read_text()) From b084fa449008b4dbf4162d2e702d280cb25a9433 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski <me@komzpa.net> Date: Sun, 26 Apr 2026 15:31:32 +0400 Subject: [PATCH 1658/2056] [esp32] Make ESP-IDF builds reproducible (#16008) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/esp32/__init__.py | 4 ++++ .../esp32/config/reproducible_build.yaml | 8 ++++++++ tests/component_tests/esp32/test_esp32.py | 11 +++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/component_tests/esp32/config/reproducible_build.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1a7ae700c78..78a1715ccfd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1729,6 +1729,10 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") if use_platformio: cg.add_platformio_option("framework", "espidf") + # Strip volatile build path/time metadata from PlatformIO-managed + # ESP-IDF builds so equivalent projects can produce reproducible + # outputs and downstream tooling can safely reuse artifacts. + add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True) # Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of # exception class overhead. See throw_stubs.cpp for implementation. diff --git a/tests/component_tests/esp32/config/reproducible_build.yaml b/tests/component_tests/esp32/config/reproducible_build.yaml new file mode 100644 index 00000000000..eb9721b4320 --- /dev/null +++ b/tests/component_tests/esp32/config/reproducible_build.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + variant: esp32 + framework: + type: esp-idf diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index ac492e27529..c39a4aafc88 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -232,3 +232,14 @@ def test_execute_from_psram_disabled_sdkconfig( assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig assert "CONFIG_SPIRAM_RODATA" not in sdkconfig assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig + + +def test_platformio_idf_enables_reproducible_build( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test PlatformIO ESP-IDF builds enable reproducible app metadata.""" + generate_main(component_config_path("reproducible_build.yaml")) + + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True From c8d4420408c33680ab47c4576129fc4ad3dad1d7 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Sun, 26 Apr 2026 14:19:49 +0200 Subject: [PATCH 1659/2056] [mitsubishi_cn105] add support for half-degree temperature setpoint (#15919) --- .../components/mitsubishi_cn105/mitsubishi_cn105.cpp | 6 +++--- .../climate/mitsubishi_cn105_tests.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 1a354956183..f04a5906c16 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -352,7 +352,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) { ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature); return; } - this->status_.target_temperature = std::round(target_temperature); + this->status_.target_temperature = target_temperature; this->pending_updates_.set(UpdateFlag::TEMPERATURE); } @@ -387,9 +387,9 @@ void MitsubishiCN105::apply_settings_() { if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { payload[1] |= 0x04; if (this->use_temperature_encoding_b_) { - payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f); + payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128); } else { - payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature); + payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature)); } } diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index 7846a31193f..86faaeac784 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -341,6 +341,17 @@ TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0xC5)); } +TEST(MitsubishiCN105Tests, ApplySettingsHalfDegreeTemperatureEncodedB) { + auto ctx = TestContext{}; + + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_target_temperature(26.5f); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB5, 0x00, 0xC4)); +} + TEST(MitsubishiCN105Tests, ApplyModeCool) { auto ctx = TestContext{}; From df987a7ffb4580dd90f07a99e1544b447f9104b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:22:34 -0500 Subject: [PATCH 1660/2056] [ci-custom] Suggest uint32_to_str/int8_to_str for integer formatting (#15970) --- script/ci-custom.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index 02ec08bc318..4d71df74cfa 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -837,7 +837,16 @@ def lint_no_std_to_string(fname, match): f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) " f"allocates heap memory. On long-running embedded devices, repeated heap allocations " f"fragment memory over time.\n" - f"Please use {highlight('snprintf()')} with a stack buffer instead.\n" + f"\n" + f"For plain integer formatting, prefer the dedicated helpers in helpers.h over " + f"{highlight('snprintf()')} — they avoid pulling in printf formatting code and are " + f"smaller and faster:\n" + f" int8_t: {highlight('int8_to_str(buf, val)')} (buf >= 5 bytes)\n" + f" uint8_t/uint16_t/uint32_t: {highlight('uint32_to_str(buf, val)')} (buf = UINT32_MAX_STR_SIZE; smaller types auto-widen)\n" + f"Example: {highlight('char buf[UINT32_MAX_STR_SIZE]; uint32_to_str(buf, value);')}\n" + f"For sensor values, use {highlight('value_accuracy_to_buf()')} from helpers.h.\n" + f"\n" + f"Otherwise use {highlight('snprintf()')} with a stack buffer.\n" f"\n" f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n" f" uint8_t: 4 chars - %u (or PRIu8)\n" @@ -851,7 +860,6 @@ def lint_no_std_to_string(fname, match): f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n" f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n" f"\n" - f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n" f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n' f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" ) From 4c0dfb0e0d302373786ccbffbd07bb43b673fe3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:22:50 -0500 Subject: [PATCH 1661/2056] [core] Raise ESP32 WDT feed interval to 1/5 of configured timeout (#15984) --- esphome/core/application.h | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e9b386038ea..bc09f7d38c8 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -17,6 +17,10 @@ #include "esphome/core/string_ref.h" #include "esphome/core/version.h" +#ifdef USE_ESP32 +#include <sdkconfig.h> // for CONFIG_ESP_TASK_WDT_TIMEOUT_S (drives WDT_FEED_INTERVAL_MS) +#endif + #ifdef USE_DEVICES #include "esphome/core/device.h" #endif @@ -216,16 +220,30 @@ class Application { /// loops and scheduler items still feed after every op, so any op exceeding /// this threshold triggers a real feed naturally. /// Safety margins vs. platform watchdog timeouts: - /// - ESP32 task WDT default (5 s): ~16x - /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change - /// must keep comfortable margin here - /// - ESP8266 HW WDT (~6 s): ~20x - /// - BK72xx HW WDT (10 s): ~5x <-- platform override below + /// - ESP32 task WDT (user-configurable): ~5x <-- auto-scaled below + /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change + /// must keep comfortable margin here + /// - ESP8266 HW WDT (~6 s): ~20x + /// - BK72xx HW WDT (10 s): ~5x <-- platform override below #ifdef USE_BK72XX // BDK busy-waits 200us per WDT reload (sctrl_dpll_delay200us). LibreTiny // sets HW WDT to 10s; 2000ms keeps ~5x margin. See wdt_ctrl WCMD_RELOAD_PERIOD: // https://github.com/libretiny-eu/framework-beken-bdk/blob/44800e7451ea30fbcbd3bb6e905315de59349fee/beken378/driver/wdt/wdt.c#L75-L87 static constexpr uint32_t WDT_FEED_INTERVAL_MS = 2000; +#elif defined(USE_ESP32) + // Auto-scale to 1/5 of the configured ESP32 task WDT timeout so the safety + // margin stays constant when the user raises esp32.watchdog_timeout (default + // 5 s → 1000 ms feed; 10 s → 2000 ms; 60 s → 12000 ms). The esp32 component + // writes CONFIG_ESP_TASK_WDT_TIMEOUT_S into sdkconfig (range is validated + // to ≥ 5 s in esp32/__init__.py), giving us the value at compile time. + // esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so + // each call costs tens of microseconds; longer intervals materially reduce + // the main-loop's wdt bucket. Component loops and scheduler items still + // feed after every op, so any op exceeding this threshold triggers a real + // feed naturally regardless of the rate-limit. + static_assert(CONFIG_ESP_TASK_WDT_TIMEOUT_S >= 5, + "CONFIG_ESP_TASK_WDT_TIMEOUT_S must be at least 5s for a safe WDT feed interval"); + static constexpr uint32_t WDT_FEED_INTERVAL_MS = (CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000U) / 5U; #else static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300; #endif From 180105bb4b794fd9767714d4426a8a6c4402ce6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:23:08 -0500 Subject: [PATCH 1662/2056] =?UTF-8?q?[bluetooth=5Fproxy]=20Partial=20rever?= =?UTF-8?q?t=20of=20loop()=20=E2=86=92=20set=5Finterval=20migration=20(#15?= =?UTF-8?q?992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bluetooth_proxy/bluetooth_proxy.cpp | 32 +++++++++++-------- .../bluetooth_proxy/bluetooth_proxy.h | 4 +++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index c69163b1f74..45f848a2866 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -30,19 +30,6 @@ void BluetoothProxy::setup() { this->configured_scan_active_ = this->parent_->get_scan_active(); this->parent_->add_scanner_state_listener(this); - - this->set_interval(100, [this]() { - if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) { - this->flush_pending_advertisements_(); - return; - } - for (uint8_t i = 0; i < this->connection_count_; i++) { - auto *connection = this->connections_[i]; - if (connection->get_address() != 0 && !connection->disconnect_pending()) { - connection->disconnect(); - } - } - }); } void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { @@ -133,6 +120,25 @@ void BluetoothProxy::dump_config() { YESNO(this->active_), this->connection_count_); } +void BluetoothProxy::loop() { + // Run advertisement flush / connection cleanup every 100ms + uint32_t now = App.get_loop_component_start_time(); + if (now - this->last_advertisement_flush_time_ < 100) + return; + this->last_advertisement_flush_time_ = now; + + if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) { + this->flush_pending_advertisements_(); + return; + } + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; + if (connection->get_address() != 0 && !connection->disconnect_pending()) { + connection->disconnect(); + } + } +} + esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 6680ab0e840..10449f21f1e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -65,6 +65,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; + void loop() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { @@ -176,6 +177,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, // BLE advertisement batching api::BluetoothLERawAdvertisementsResponse response_; + // Group 3: 4-byte types + uint32_t last_advertisement_flush_time_{0}; + // Pre-allocated response message - always ready to send api::BluetoothConnectionsFreeResponse connections_free_response_; From 502c0104650e2f2d165896528069a635d22f95df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:23:24 -0500 Subject: [PATCH 1663/2056] [bh1750] Downgrade per-reading Illuminance log to verbose (#16005) --- esphome/components/bh1750/bh1750.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 045fb7cf454..ab952895a88 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -154,7 +154,7 @@ void BH1750Sensor::loop() { break; } - ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx); + ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx); this->status_clear_warning(); this->publish_state(lx); this->state_ = IDLE; From 04d067196d05737ea66bf4b121fbfc02f2c12a18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:23:41 -0500 Subject: [PATCH 1664/2056] [rotary_encoder][at581x] Fix templatable int field types (#16015) --- esphome/components/at581x/__init__.py | 8 ++++---- esphome/components/rotary_encoder/sensor.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 94b68db4b33..5031b72cceb 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -183,19 +183,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_sensing_distance(template_)) if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): - template_ = await cg.templatable(selfcheck, args, cg.int32) + template_ = await cg.templatable(selfcheck, args, cg.int_) cg.add(var.set_poweron_selfcheck_time(template_)) if protect := config.get(CONF_PROTECT_TIME): - template_ = await cg.templatable(protect, args, cg.int32) + template_ = await cg.templatable(protect, args, cg.int_) cg.add(var.set_protect_time(template_)) if trig_base := config.get(CONF_TRIGGER_BASE): - template_ = await cg.templatable(trig_base, args, cg.int32) + template_ = await cg.templatable(trig_base, args, cg.int_) cg.add(var.set_trigger_base(template_)) if trig_keep := config.get(CONF_TRIGGER_KEEP): - template_ = await cg.templatable(trig_keep, args, cg.int32) + template_ = await cg.templatable(trig_keep, args, cg.int_) cg.add(var.set_trigger_keep(template_)) if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 21239863e45..0e5a03523df 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -129,6 +129,6 @@ async def to_code(config): async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, cg.int32) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.int_) cg.add(var.set_value(template_)) return var From 8950afc3c4c654eb77e45015f46883def1b37178 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:23:53 -0500 Subject: [PATCH 1665/2056] [bluetooth_proxy] Drop redundant remote_bda_ write in connect handler (#16000) --- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 45f848a2866..c3461f9c519 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -207,7 +207,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); this->log_connection_info_(connection, "v3 without cache"); } - uint64_to_bd_addr(msg.address, connection->remote_bda_); connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type)); connection->set_state(espbt::ClientState::DISCOVERED); this->send_connections_free(); From 8dbdcfc1284864cbc2321530794938fda908a93a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:24:07 -0500 Subject: [PATCH 1666/2056] [bk72xx] Prepare for BK7238 support (#16018) --- esphome/components/bk72xx/boards.py | 500 +++++++++++++++++++++++++- esphome/components/libretiny/const.py | 4 + 2 files changed, 502 insertions(+), 2 deletions(-) diff --git a/esphome/components/bk72xx/boards.py b/esphome/components/bk72xx/boards.py index 4bee69fe6da..f8bedce329b 100644 --- a/esphome/components/bk72xx/boards.py +++ b/esphome/components/bk72xx/boards.py @@ -16,6 +16,7 @@ from esphome.components.libretiny.const import ( FAMILY_BK7231N, FAMILY_BK7231Q, FAMILY_BK7231T, + FAMILY_BK7238, FAMILY_BK7251, ) @@ -24,16 +25,32 @@ BK72XX_BOARDS = { "name": "WB2L_M1 Wi-Fi Module", "family": FAMILY_BK7231N, }, + "xh-wb3s": { + "name": "NiceMCU XH-WB3S", + "family": FAMILY_BK7238, + }, "cbu": { "name": "CBU Wi-Fi Module", "family": FAMILY_BK7231N, }, + "t1-u": { + "name": "T1-U Wi-Fi Module", + "family": FAMILY_BK7238, + }, + "generic-bk7238-tuya": { + "name": "Generic - BK7238 (Tuya T1)", + "family": FAMILY_BK7238, + }, + "t1-m": { + "name": "T1-M Wi-Fi Module", + "family": FAMILY_BK7238, + }, "generic-bk7231t-qfn32-tuya": { - "name": "Generic - BK7231T (Tuya QFN32)", + "name": "Generic - BK7231T (Tuya)", "family": FAMILY_BK7231T, }, "generic-bk7231n-qfn32-tuya": { - "name": "Generic - BK7231N (Tuya QFN32)", + "name": "Generic - BK7231N (Tuya)", "family": FAMILY_BK7231N, }, "cb1s": { @@ -64,6 +81,10 @@ BK72XX_BOARDS = { "name": "Generic - BK7252", "family": FAMILY_BK7251, }, + "t1-3s": { + "name": "T1-3S Wi-Fi Module", + "family": FAMILY_BK7238, + }, "wb2l": { "name": "WB2L Wi-Fi Module", "family": FAMILY_BK7231T, @@ -80,6 +101,10 @@ BK72XX_BOARDS = { "name": "CB2S Wi-Fi Module", "family": FAMILY_BK7231N, }, + "generic-bk7238": { + "name": "Generic - BK7238", + "family": FAMILY_BK7238, + }, "wa2": { "name": "WA2 Wi-Fi Module", "family": FAMILY_BK7231Q, @@ -100,6 +125,10 @@ BK72XX_BOARDS = { "name": "WB3L Wi-Fi Module", "family": FAMILY_BK7231T, }, + "t1-2s": { + "name": "T1-2S Wi-Fi Module", + "family": FAMILY_BK7238, + }, "wb2s": { "name": "WB2S Wi-Fi Module", "family": FAMILY_BK7231T, @@ -158,6 +187,83 @@ BK72XX_BOARD_PINS = { "D12": 22, "A0": 23, }, + "xh-wb3s": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, + "WIRE2_SCL_0": 15, + "WIRE2_SCL_1": 24, + "WIRE2_SDA_0": 17, + "WIRE2_SDA_1": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC3": 20, + "ADC4": 28, + "ADC5": 1, + "ADC6": 10, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "TX1": 11, + "TX2": 0, + "D0": 7, + "D1": 23, + "D2": 14, + "D3": 26, + "D4": 24, + "D5": 6, + "D6": 9, + "D7": 0, + "D8": 1, + "D9": 8, + "D10": 10, + "D11": 11, + "D12": 16, + "D13": 20, + "D14": 21, + "D15": 22, + "D16": 15, + "D17": 17, + "A0": 28, + "A1": 26, + "A2": 24, + "A3": 1, + "A4": 10, + "A5": 20, + }, "cbu": { "SPI0_CS": 15, "SPI0_MISO": 17, @@ -230,6 +336,204 @@ BK72XX_BOARD_PINS = { "D18": 21, "A0": 23, }, + "t1-u": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, + "WIRE2_SCL_0": 15, + "WIRE2_SCL_1": 24, + "WIRE2_SDA_0": 17, + "WIRE2_SDA_1": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC3": 20, + "ADC4": 28, + "ADC5": 1, + "ADC6": 10, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "TX1": 11, + "TX2": 0, + "D0": 14, + "D1": 16, + "D2": 23, + "D3": 22, + "D4": 20, + "D5": 1, + "D6": 0, + "D7": 24, + "D8": 9, + "D9": 26, + "D10": 6, + "D11": 8, + "D12": 11, + "D13": 10, + "D14": 28, + "D15": 21, + "D16": 17, + "D17": 15, + "A0": 20, + "A1": 1, + "A2": 24, + "A3": 26, + "A4": 10, + "A5": 28, + }, + "generic-bk7238-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, + "WIRE2_SCL_0": 15, + "WIRE2_SCL_1": 24, + "WIRE2_SDA_0": 17, + "WIRE2_SDA_1": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC3": 20, + "ADC4": 28, + "ADC5": 1, + "ADC6": 10, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "TX1": 11, + "TX2": 0, + "D0": 0, + "D1": 1, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 14, + "D9": 15, + "D10": 16, + "D11": 17, + "D12": 20, + "D13": 21, + "D14": 22, + "D15": 23, + "D16": 24, + "D17": 26, + "D18": 28, + "A0": 1, + "A1": 10, + "A2": 20, + "A3": 24, + "A4": 26, + "A5": 28, + }, + "t1-m": { + "WIRE2_SCL": 24, + "WIRE2_SDA": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC5": 1, + "ADC6": 10, + "P0": 0, + "P1": 1, + "P6": 6, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL2": 24, + "SDA2": 26, + "TX1": 11, + "TX2": 0, + "D0": 26, + "D1": 6, + "D2": 8, + "D3": 1, + "D4": 10, + "D5": 11, + "D6": 9, + "D7": 24, + "D11": 0, + "A0": 26, + "A1": 10, + "A2": 1, + "A3": 24, + }, "generic-bk7231t-qfn32-tuya": { "SPI0_CS": 15, "SPI0_MISO": 17, @@ -781,6 +1085,75 @@ BK72XX_BOARD_PINS = { "A6": 12, "A7": 13, }, + "t1-3s": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, + "WIRE2_SCL_0": 15, + "WIRE2_SCL_1": 24, + "WIRE2_SDA_0": 17, + "WIRE2_SDA_1": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC3": 20, + "ADC5": 1, + "ADC6": 10, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "TX1": 11, + "TX2": 0, + "D0": 20, + "D1": 22, + "D2": 6, + "D3": 8, + "D4": 9, + "D5": 23, + "D6": 0, + "D7": 1, + "D8": 24, + "D9": 26, + "D10": 10, + "D11": 11, + "D12": 17, + "D13": 16, + "D14": 15, + "D15": 14, + "A0": 20, + "A1": 1, + "A2": 24, + "A3": 26, + "A4": 10, + }, "wb2l": { "WIRE1_SCL": 20, "WIRE1_SDA": 21, @@ -965,6 +1338,84 @@ BK72XX_BOARD_PINS = { "D10": 21, "A0": 23, }, + "generic-bk7238": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, + "WIRE2_SCL_0": 15, + "WIRE2_SCL_1": 24, + "WIRE2_SDA_0": 17, + "WIRE2_SDA_1": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC3": 20, + "ADC4": 28, + "ADC5": 1, + "ADC6": 10, + "CS": 15, + "MISO": 17, + "MOSI": 16, + "P0": 0, + "P1": 1, + "P6": 6, + "P7": 7, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P14": 14, + "P15": 15, + "P16": 16, + "P17": 17, + "P20": 20, + "P21": 21, + "P22": 22, + "P23": 23, + "P24": 24, + "P26": 26, + "P28": 28, + "PWM0": 6, + "PWM1": 7, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCK": 14, + "TX1": 11, + "TX2": 0, + "D0": 0, + "D1": 1, + "D2": 6, + "D3": 7, + "D4": 8, + "D5": 9, + "D6": 10, + "D7": 11, + "D8": 14, + "D9": 15, + "D10": 16, + "D11": 17, + "D12": 20, + "D13": 21, + "D14": 22, + "D15": 23, + "D16": 24, + "D17": 26, + "D18": 28, + "A0": 1, + "A1": 10, + "A2": 20, + "A3": 24, + "A4": 26, + "A5": 28, + }, "wa2": { "WIRE1_SCL": 20, "WIRE1_SDA": 21, @@ -1235,6 +1686,51 @@ BK72XX_BOARD_PINS = { "D15": 1, "A0": 23, }, + "t1-2s": { + "WIRE2_SCL": 24, + "WIRE2_SDA": 26, + "SERIAL1_RX": 10, + "SERIAL1_TX": 11, + "SERIAL2_RX": 1, + "SERIAL2_TX": 0, + "ADC1": 26, + "ADC2": 24, + "ADC5": 1, + "ADC6": 10, + "P0": 0, + "P1": 1, + "P6": 6, + "P8": 8, + "P9": 9, + "P10": 10, + "P11": 11, + "P24": 24, + "P26": 26, + "PWM0": 6, + "PWM2": 8, + "PWM3": 9, + "PWM4": 24, + "PWM5": 26, + "RX1": 10, + "RX2": 1, + "SCL2": 24, + "SDA2": 26, + "TX1": 11, + "TX2": 0, + "D0": 26, + "D1": 6, + "D2": 8, + "D3": 1, + "D4": 10, + "D5": 11, + "D6": 9, + "D7": 24, + "D11": 0, + "A0": 26, + "A1": 10, + "A2": 1, + "A3": 24, + }, "wb2s": { "WIRE1_SCL": 20, "WIRE1_SDA": 21, diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 332be0de1dd..5de4a164b57 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -58,6 +58,7 @@ COMPONENT_RTL87XX = "rtl87xx" FAMILY_BK7231N = "BK7231N" FAMILY_BK7231Q = "BK7231Q" FAMILY_BK7231T = "BK7231T" +FAMILY_BK7238 = "BK7238" FAMILY_BK7251 = "BK7251" FAMILY_LN882H = "LN882H" FAMILY_RTL8710B = "RTL8710B" @@ -66,6 +67,7 @@ FAMILIES = [ FAMILY_BK7231N, FAMILY_BK7231Q, FAMILY_BK7231T, + FAMILY_BK7238, FAMILY_BK7251, FAMILY_LN882H, FAMILY_RTL8710B, @@ -75,6 +77,7 @@ FAMILY_FRIENDLY = { FAMILY_BK7231N: "BK7231N", FAMILY_BK7231Q: "BK7231Q", FAMILY_BK7231T: "BK7231T", + FAMILY_BK7238: "BK7238", FAMILY_BK7251: "BK7251", FAMILY_LN882H: "LN882H", FAMILY_RTL8710B: "RTL8710B", @@ -84,6 +87,7 @@ FAMILY_COMPONENT = { FAMILY_BK7231N: COMPONENT_BK72XX, FAMILY_BK7231Q: COMPONENT_BK72XX, FAMILY_BK7231T: COMPONENT_BK72XX, + FAMILY_BK7238: COMPONENT_BK72XX, FAMILY_BK7251: COMPONENT_BK72XX, FAMILY_LN882H: COMPONENT_LN882X, FAMILY_RTL8710B: COMPONENT_RTL87XX, From 0f25d91e68578878283c914c8cd9b6b9aa2b9978 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:24:33 -0500 Subject: [PATCH 1667/2056] [core] Unify `skip_external_update` and honor it in external_files for faster `esphome logs` (#16016) --- .../external_components/__init__.py | 19 ++- esphome/components/packages/__init__.py | 18 +-- esphome/config.py | 9 +- esphome/core/__init__.py | 4 + esphome/external_files.py | 5 +- esphome/git.py | 4 +- .../external_components/test_init.py | 121 +++++------------- tests/component_tests/packages/test_init.py | 115 +++++------------ tests/unit_tests/test_external_files.py | 46 +++++++ tests/unit_tests/test_git.py | 29 +++++ tests/unit_tests/test_substitutions.py | 4 +- 11 files changed, 170 insertions(+), 204 deletions(-) diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index ceb402c5b73..6eb577e5ade 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import Any from esphome import git, loader import esphome.config_validation as cv @@ -17,7 +18,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, ) -from esphome.core import CORE +from esphome.core import CORE, TimePeriodSeconds _LOGGER = logging.getLogger(__name__) @@ -35,17 +36,15 @@ CONFIG_SCHEMA = cv.ensure_list( ) -async def to_code(config): +async def to_code(config: dict[str, Any]) -> None: pass -def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str: - # When skip_update is True, use NEVER_REFRESH to prevent updates - actual_refresh = git.NEVER_REFRESH if skip_update else refresh +def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> Path: repo_dir, _ = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), - refresh=actual_refresh, + refresh=refresh, domain=DOMAIN, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), @@ -72,12 +71,12 @@ def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str return components_dir -def _process_single_config(config: dict, skip_update: bool = False): +def _process_single_config(config: dict[str, Any]) -> None: conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: with cv.prepend_path([CONF_SOURCE]): components_dir = _process_git_config( - config[CONF_SOURCE], config[CONF_REFRESH], skip_update + config[CONF_SOURCE], config[CONF_REFRESH] ) elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) @@ -107,7 +106,7 @@ def _process_single_config(config: dict, skip_update: bool = False): loader.install_meta_finder(components_dir, allowed_components=allowed_components) -def do_external_components_pass(config: dict, skip_update: bool = False) -> None: +def do_external_components_pass(config: dict[str, Any]) -> None: conf = config.get(DOMAIN) if conf is None: return @@ -115,4 +114,4 @@ def do_external_components_pass(config: dict, skip_update: bool = False) -> None conf = CONFIG_SCHEMA(conf) for i, c in enumerate(conf): with cv.prepend_path(i): - _process_single_config(c, skip_update) + _process_single_config(c) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 1b9e03d88fc..47a1fd20a75 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -205,7 +205,7 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: ) -def _process_remote_package(config: dict, skip_update: bool = False) -> dict: +def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]: """Clone/update a git repo and load the YAML files listed in the package definition. Returns ``{"packages": {<filename>: <loaded_yaml>, ...}}`` so the caller @@ -215,11 +215,10 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict: If loading fails after cloning, attempts a revert and retry in case a prior cached checkout is stale. """ - actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), - refresh=actual_refresh, + refresh=config[CONF_REFRESH], domain=DOMAIN, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), @@ -456,11 +455,9 @@ class _PackageProcessor: self, substitutions: UserDict, command_line_substitutions: dict[str, Any] | None, - skip_update: bool, ) -> None: self.substitutions = substitutions self.parent_context = UserDict(command_line_substitutions or {}) - self.skip_update = skip_update def resolve_package( self, @@ -508,7 +505,7 @@ class _PackageProcessor: ) if is_remote_package(package_config): - package_config = _process_remote_package(package_config, self.skip_update) + package_config = _process_remote_package(package_config) return package_config def collect_substitutions(self, package_config: dict) -> None: @@ -552,11 +549,10 @@ class _PackageProcessor: def do_packages_pass( - config: dict, + config: dict[str, Any], *, command_line_substitutions: dict[str, Any] | None = None, - skip_update: bool = False, -) -> dict: +) -> dict[str, Any]: """Load, validate, and flatten all packages in the config. Returns the config with all packages loaded in-place (but not yet merged) @@ -571,9 +567,7 @@ def do_packages_pass( config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions ) ) - processor = _PackageProcessor( - substitutions, command_line_substitutions, skip_update - ) + processor = _PackageProcessor(substitutions, command_line_substitutions) _update_substitutions_context(processor.parent_context, substitutions) context_vars = push_context( diff --git a/esphome/config.py b/esphome/config.py index 641b6ec1b48..6eb67af58b3 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -997,6 +997,8 @@ def validate_config( ) -> Config: result = Config() + CORE.skip_external_update = skip_external_update + loader.clear_component_meta_finders() loader.install_custom_components_meta_finder() @@ -1009,7 +1011,6 @@ def validate_config( config = do_packages_pass( config, command_line_substitutions=command_line_substitutions, - skip_update=skip_external_update, ) except vol.Invalid as err: result.update(config) @@ -1050,7 +1051,7 @@ def validate_config( result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS) try: - do_external_components_pass(config, skip_update=skip_external_update) + do_external_components_pass(config) except vol.Invalid as err: result.update(config) result.add_error(err) @@ -1341,7 +1342,9 @@ def strip_default_ids(config): return config -def read_config(command_line_substitutions, skip_external_update=False): +def read_config( + command_line_substitutions: dict[str, Any], skip_external_update: bool = False +) -> Config | None: _LOGGER.info("Reading configuration %s...", CORE.config_path) try: res = load_config(command_line_substitutions, skip_external_update) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 009fef2f863..4fecebcd8d2 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -615,6 +615,9 @@ class EsphomeCore: self.address_cache: AddressCache | None = None # Cached config hash (computed lazily) self._config_hash: int | None = None + # When True, skip network freshness checks for cached external files + # (e.g. for `esphome logs`, where remote downloads aren't needed) + self.skip_external_update: bool = False def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -644,6 +647,7 @@ class EsphomeCore: self.current_component = None self.address_cache = None self._config_hash = None + self.skip_external_update = False PIN_SCHEMA_REGISTRY.reset() @contextmanager diff --git a/esphome/external_files.py b/esphome/external_files.py index 55711e1b790..b6f6149ebbb 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -81,7 +81,10 @@ def compute_local_file_dir(domain: str) -> Path: return base_directory -def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: +def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> bytes: + if CORE.skip_external_update and path.exists(): + _LOGGER.debug("Skipping update for %s (refresh disabled)", url) + return path.read_bytes() if not has_remote_file_changed(url, path): _LOGGER.debug("Remote file has not changed %s", url) return path.read_bytes() diff --git a/esphome/git.py b/esphome/git.py index 096ff483a71..4d6e14001a7 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -150,9 +150,7 @@ def clone_or_update( raise else: - # Check refresh needed - # Skip refresh if NEVER_REFRESH is specified - if refresh == NEVER_REFRESH: + if refresh == NEVER_REFRESH or CORE.skip_external_update: _LOGGER.debug("Skipping update for %s (refresh disabled)", key) return repo_dir, None diff --git a/tests/component_tests/external_components/test_init.py b/tests/component_tests/external_components/test_init.py index 905c0afa8b3..d3813ecc759 100644 --- a/tests/component_tests/external_components/test_init.py +++ b/tests/component_tests/external_components/test_init.py @@ -1,4 +1,4 @@ -"""Tests for the external_components skip_update functionality.""" +"""Tests for the external_components skip-update behavior driven by CORE.skip_external_update.""" from pathlib import Path from typing import Any @@ -12,25 +12,17 @@ from esphome.const import ( CONF_URL, TYPE_GIT, ) +from esphome.core import CORE, TimePeriodSeconds -def test_external_components_skip_update_true( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock -) -> None: - """Test that external components don't update when skip_update=True.""" - # Create a components directory structure +def _make_config(tmp_path: Path) -> dict[str, Any]: components_dir = tmp_path / "components" components_dir.mkdir() - - # Create a test component test_component_dir = components_dir / "test_component" test_component_dir.mkdir() (test_component_dir / "__init__.py").write_text("# Test component") - # Set up mock to return our tmp_path - mock_clone_or_update.return_value = (tmp_path, None) - - config: dict[str, Any] = { + return { CONF_EXTERNAL_COMPONENTS: [ { CONF_SOURCE: { @@ -43,92 +35,37 @@ def test_external_components_skip_update_true( ] } - # Call with skip_update=True - do_external_components_pass(config, skip_update=True) - # Verify clone_or_update was called with NEVER_REFRESH - mock_clone_or_update.assert_called_once() - call_args = mock_clone_or_update.call_args - from esphome import git - - assert call_args.kwargs["refresh"] == git.NEVER_REFRESH - - -def test_external_components_skip_update_false( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock +def test_external_components_skip_update_via_core_flag( + tmp_path: Path, + mock_clone_or_update: MagicMock, + mock_install_meta_finder: MagicMock, ) -> None: - """Test that external components update when skip_update=False.""" - # Create a components directory structure - components_dir = tmp_path / "components" - components_dir.mkdir() - - # Create a test component - test_component_dir = components_dir / "test_component" - test_component_dir.mkdir() - (test_component_dir / "__init__.py").write_text("# Test component") - - # Set up mock to return our tmp_path + """When CORE.skip_external_update is True, refresh is still passed through; + git.clone_or_update itself short-circuits the actual fetch.""" mock_clone_or_update.return_value = (tmp_path, None) + config = _make_config(tmp_path) + + CORE.skip_external_update = True + do_external_components_pass(config) + + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + # Refresh is passed through verbatim — the global flag is enforced inside git.clone_or_update. + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) + + +def test_external_components_normal_refresh( + tmp_path: Path, + mock_clone_or_update: MagicMock, + mock_install_meta_finder: MagicMock, +) -> None: + """When CORE.skip_external_update is False, the configured refresh value is used.""" + mock_clone_or_update.return_value = (tmp_path, None) + config = _make_config(tmp_path) - config: dict[str, Any] = { - CONF_EXTERNAL_COMPONENTS: [ - { - CONF_SOURCE: { - "type": TYPE_GIT, - CONF_URL: "https://github.com/test/components", - }, - CONF_REFRESH: "1d", - "components": "all", - } - ] - } - - # Call with skip_update=False - do_external_components_pass(config, skip_update=False) - - # Verify clone_or_update was called with actual refresh value - mock_clone_or_update.assert_called_once() - call_args = mock_clone_or_update.call_args - from esphome.core import TimePeriodSeconds - - assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) - - -def test_external_components_default_no_skip( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock -) -> None: - """Test that external components update by default when skip_update not specified.""" - # Create a components directory structure - components_dir = tmp_path / "components" - components_dir.mkdir() - - # Create a test component - test_component_dir = components_dir / "test_component" - test_component_dir.mkdir() - (test_component_dir / "__init__.py").write_text("# Test component") - - # Set up mock to return our tmp_path - mock_clone_or_update.return_value = (tmp_path, None) - - config: dict[str, Any] = { - CONF_EXTERNAL_COMPONENTS: [ - { - CONF_SOURCE: { - "type": TYPE_GIT, - CONF_URL: "https://github.com/test/components", - }, - CONF_REFRESH: "1d", - "components": "all", - } - ] - } - - # Call without skip_update parameter do_external_components_pass(config) - # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() call_args = mock_clone_or_update.call_args - from esphome.core import TimePeriodSeconds - assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) diff --git a/tests/component_tests/packages/test_init.py b/tests/component_tests/packages/test_init.py index fd30c2433f2..19c7bd36692 100644 --- a/tests/component_tests/packages/test_init.py +++ b/tests/component_tests/packages/test_init.py @@ -1,4 +1,4 @@ -"""Tests for the packages component skip_update functionality.""" +"""Tests for the packages skip-update behavior driven by CORE.skip_external_update.""" from pathlib import Path from typing import Any @@ -6,24 +6,12 @@ from unittest.mock import MagicMock from esphome.components.packages import do_packages_pass from esphome.const import CONF_FILES, CONF_PACKAGES, CONF_REFRESH, CONF_URL +from esphome.core import CORE, TimePeriodSeconds from esphome.util import OrderedDict -def test_packages_skip_update_true( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock -) -> None: - """Test that packages don't update when skip_update=True.""" - # Set up mock to return our tmp_path - mock_clone_or_update.return_value = (tmp_path, None) - - # Create the test yaml file - test_file = tmp_path / "test.yaml" - test_file.write_text("sensor: []") - - # Set mock_load_yaml to return some valid config - mock_load_yaml.return_value = OrderedDict({"sensor": []}) - - config: dict[str, Any] = { +def _make_config() -> dict[str, Any]: + return { CONF_PACKAGES: { "test_package": { CONF_URL: "https://github.com/test/repo", @@ -33,82 +21,47 @@ def test_packages_skip_update_true( } } - # Call with skip_update=True - do_packages_pass(config, skip_update=True) - # Verify clone_or_update was called with NEVER_REFRESH - mock_clone_or_update.assert_called_once() - call_args = mock_clone_or_update.call_args - from esphome import git - - assert call_args.kwargs["refresh"] == git.NEVER_REFRESH - - -def test_packages_skip_update_false( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock +def test_packages_skip_update_via_core_flag( + tmp_path: Path, + mock_clone_or_update: MagicMock, + mock_load_yaml: MagicMock, ) -> None: - """Test that packages update when skip_update=False.""" - # Set up mock to return our tmp_path + """When CORE.skip_external_update is True, refresh is still passed through; + git.clone_or_update itself short-circuits the actual fetch.""" mock_clone_or_update.return_value = (tmp_path, None) - # Create the test yaml file test_file = tmp_path / "test.yaml" test_file.write_text("sensor: []") - - # Set mock_load_yaml to return some valid config mock_load_yaml.return_value = OrderedDict({"sensor": []}) - config: dict[str, Any] = { - CONF_PACKAGES: { - "test_package": { - CONF_URL: "https://github.com/test/repo", - CONF_FILES: ["test.yaml"], - CONF_REFRESH: "1d", - } - } - } + config = _make_config() + + CORE.skip_external_update = True + do_packages_pass(config, command_line_substitutions={}) + + mock_clone_or_update.assert_called_once() + call_args = mock_clone_or_update.call_args + # Refresh is passed through verbatim — the global flag is enforced inside git.clone_or_update. + assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) + + +def test_packages_normal_refresh( + tmp_path: Path, + mock_clone_or_update: MagicMock, + mock_load_yaml: MagicMock, +) -> None: + """When CORE.skip_external_update is False, the configured refresh value is used.""" + mock_clone_or_update.return_value = (tmp_path, None) + + test_file = tmp_path / "test.yaml" + test_file.write_text("sensor: []") + mock_load_yaml.return_value = OrderedDict({"sensor": []}) + + config = _make_config() - # Call with skip_update=False (default) - do_packages_pass(config, command_line_substitutions={}, skip_update=False) - - # Verify clone_or_update was called with actual refresh value - mock_clone_or_update.assert_called_once() - call_args = mock_clone_or_update.call_args - from esphome.core import TimePeriodSeconds - - assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) - - -def test_packages_default_no_skip( - tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock -) -> None: - """Test that packages update by default when skip_update not specified.""" - # Set up mock to return our tmp_path - mock_clone_or_update.return_value = (tmp_path, None) - - # Create the test yaml file - test_file = tmp_path / "test.yaml" - test_file.write_text("sensor: []") - - # Set mock_load_yaml to return some valid config - mock_load_yaml.return_value = OrderedDict({"sensor": []}) - - config: dict[str, Any] = { - CONF_PACKAGES: { - "test_package": { - CONF_URL: "https://github.com/test/repo", - CONF_FILES: ["test.yaml"], - CONF_REFRESH: "1d", - } - } - } - - # Call without skip_update parameter do_packages_pass(config, command_line_substitutions={}) - # Verify clone_or_update was called with actual refresh value mock_clone_or_update.assert_called_once() call_args = mock_clone_or_update.call_args - from esphome.core import TimePeriodSeconds - assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1) diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index a319fae83d4..4b0826db044 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -236,3 +236,49 @@ def test_download_content_with_network_error_no_cache_fails( with pytest.raises(Invalid, match="Could not download from.*Network error"): external_files.download_content(url, test_file) + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_skip_external_update_uses_cache( + mock_has_changed: MagicMock, + mock_get: MagicMock, + setup_core: Path, +) -> None: + """Test download_content skips network checks when CORE.skip_external_update is set.""" + test_file = setup_core / "cached.txt" + cached_content = b"cached content" + test_file.write_bytes(cached_content) + + CORE.skip_external_update = True + url = "https://example.com/file.txt" + result = external_files.download_content(url, test_file) + + assert result == cached_content + mock_has_changed.assert_not_called() + mock_get.assert_not_called() + + +@patch("esphome.external_files.requests.get") +@patch("esphome.external_files.has_remote_file_changed") +def test_download_content_skip_external_update_downloads_when_missing( + mock_has_changed: MagicMock, + mock_get: MagicMock, + setup_core: Path, +) -> None: + """Test download_content still downloads when file is missing, even with skip_external_update.""" + test_file = setup_core / "missing.txt" + new_content = b"fresh content" + + mock_has_changed.return_value = True + mock_response = MagicMock() + mock_response.content = new_content + mock_response.raise_for_status = MagicMock() + mock_get.return_value = mock_response + + CORE.skip_external_update = True + url = "https://example.com/file.txt" + result = external_files.download_content(url, test_file) + + assert result == new_content + assert test_file.read_bytes() == new_content diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index 745dfad487e..dd7d26cb714 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -236,6 +236,35 @@ def test_clone_or_update_with_never_refresh( assert revert is None +def test_clone_or_update_skips_when_core_skip_external_update( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """CORE.skip_external_update short-circuits the refresh for existing repos.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = None + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + repo_dir.mkdir(parents=True) + git_dir = repo_dir / ".git" + git_dir.mkdir() + (git_dir / "FETCH_HEAD").write_text("test") + + CORE.skip_external_update = True + result_dir, revert = git.clone_or_update( + url=url, + ref=ref, + refresh=TimePeriodSeconds(days=1), + domain=domain, + ) + + mock_run_git_command.assert_not_called() + assert result_dir == repo_dir + assert revert is None + + def test_clone_or_update_with_refresh_updates_old_repo( tmp_path: Path, mock_run_git_command: Mock ) -> None: diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 215ec291f9e..cf6d4adbf51 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -654,7 +654,7 @@ def test_resolve_package_max_depth_exceeded(tmp_path: Path) -> None: package_config = yaml_util.IncludeFile( parent, "test.yaml", None, always_returns_include ) - processor = _PackageProcessor({}, None, False) + processor = _PackageProcessor({}, None) with pytest.raises( cv.Invalid, match=f"Maximum include nesting depth \\({MAX_INCLUDE_DEPTH}\\) exceeded", @@ -776,7 +776,7 @@ def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> No package_config = yaml_util.IncludeFile( parent, "${undefined_var}.yaml", None, loader ) - processor = _PackageProcessor({}, None, False) + processor = _PackageProcessor({}, None) with pytest.raises(cv.Invalid, match="unresolved substitutions"): processor.resolve_package(package_config, substitutions.ContextVars(), []) From e87e78c5446f3bf256e2d9b5b53c0b3d31d6eebf Mon Sep 17 00:00:00 2001 From: Johan Henkens <jhenkens@users.noreply.github.com> Date: Sun, 26 Apr 2026 05:58:14 -0700 Subject: [PATCH 1668/2056] [api] Expose TemperatureUnit in water heater and climate api (#15815) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/components/api/api.proto | 9 +++++++++ esphome/components/api/api_pb2.cpp | 4 ++++ esphome/components/api/api_pb2.h | 11 +++++++++-- esphome/components/api/api_pb2_dump.cpp | 14 ++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c3e4c386334..1c33d92bea4 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1025,6 +1025,13 @@ message CameraImageRequest { bool stream = 2; } +// ==================== TEMPERATURE UNIT ==================== +enum TemperatureUnit { + TEMPERATURE_UNIT_CELSIUS = 0; + TEMPERATURE_UNIT_FAHRENHEIT = 1; + TEMPERATURE_UNIT_KELVIN = 2; +} + // ==================== CLIMATE ==================== enum ClimateMode { CLIMATE_MODE_OFF = 0; @@ -1110,6 +1117,7 @@ message ListEntitiesClimateResponse { float visual_max_humidity = 25; uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"]; uint32 feature_flags = 27; + TemperatureUnit temperature_unit = 28; } message ClimateStateResponse { option (id) = 47; @@ -1203,6 +1211,7 @@ message ListEntitiesWaterHeaterResponse { repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"]; // Bitmask of WaterHeaterFeature flags uint32 supported_features = 12; + TemperatureUnit temperature_unit = 13; } message WaterHeaterStateResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 3d124539395..f6ceee2296e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1439,6 +1439,7 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id); #endif ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit)); return pos; } uint32_t ListEntitiesClimateResponse::calculate_size() const { @@ -1488,6 +1489,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { size += ProtoSize::calc_uint32(2, this->device_id); #endif size += ProtoSize::calc_uint32(2, this->feature_flags); + size += this->temperature_unit ? 3 : 0; return size; } uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { @@ -1645,6 +1647,7 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_ ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true); } ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit)); return pos; } uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { @@ -1667,6 +1670,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { size += this->supported_modes->size() * 2; } size += ProtoSize::calc_uint32(1, this->supported_features); + size += this->temperature_unit ? 2 : 0; return size; } uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5aa592e4fa8..a8e01c017fe 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -92,6 +92,11 @@ enum SupportsResponseType : uint32_t { SUPPORTS_RESPONSE_STATUS = 100, }; #endif +enum TemperatureUnit : uint32_t { + TEMPERATURE_UNIT_CELSIUS = 0, + TEMPERATURE_UNIT_FAHRENHEIT = 1, + TEMPERATURE_UNIT_KELVIN = 2, +}; #ifdef USE_CLIMATE enum ClimateMode : uint32_t { CLIMATE_MODE_OFF = 0, @@ -1372,7 +1377,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; - static constexpr uint8_t ESTIMATED_SIZE = 150; + static constexpr uint8_t ESTIMATED_SIZE = 153; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); } #endif @@ -1394,6 +1399,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; + enums::TemperatureUnit temperature_unit{}; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1471,7 +1477,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 132; - static constexpr uint8_t ESTIMATED_SIZE = 63; + static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); } #endif @@ -1480,6 +1486,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; + enums::TemperatureUnit temperature_unit{}; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index bdcb6d4146d..541f5d4d11c 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -297,6 +297,18 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums:: } } #endif +template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) { + switch (value) { + case enums::TEMPERATURE_UNIT_CELSIUS: + return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS"); + case enums::TEMPERATURE_UNIT_FAHRENHEIT: + return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT"); + case enums::TEMPERATURE_UNIT_KELVIN: + return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} #ifdef USE_CLIMATE template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { switch (value) { @@ -1539,6 +1551,7 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); + dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit)); return out.c_str(); } const char *ClimateStateResponse::dump_to(DumpBuffer &out) const { @@ -1612,6 +1625,7 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4); } dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); + dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit)); return out.c_str(); } const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const { From 2e096bb036abfefa0e86d3830a55b9e5aeecd270 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 21:54:15 -0500 Subject: [PATCH 1669/2056] [core] Combine set_component_source_ + register_component_ into one call (#16029) --- esphome/core/application.h | 6 +++++- esphome/cpp_helpers.py | 6 +++--- tests/component_tests/deep_sleep/test_deep_sleep.py | 2 +- tests/component_tests/ota/test_web_server_ota.py | 2 +- tests/unit_tests/test_cpp_helpers.py | 10 ++++++---- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index bc09f7d38c8..c0b2639bd18 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -382,7 +382,11 @@ class Application { /// Register a component, detecting loop() override at compile time. /// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance. - template<typename T> void register_component_(T *comp) { + /// Optionally sets the component source index in the same call to avoid emitting + /// a separate set_component_source_() line in generated code. + template<typename T> void register_component_(T *comp, uint8_t source_index = 0) { + if (source_index != 0) + comp->set_component_source_(source_index); this->register_component_impl_(comp, HasLoopOverride<T>::value); } diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index f2bd3b92a31..b035e28a7ad 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -197,9 +197,9 @@ async def register_component(var, config): ) if name is not None: idx = register_component_source(name) - add(var.set_component_source_(idx)) - - add(App.register_component_(var)) + add(App.register_component_(var, idx)) + else: + add(App.register_component_(var)) # Collect C++ type for compile-time looping component count comp_entries = CORE.data.setdefault("looping_component_entries", []) diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 8c1278a3323..84128d75d7c 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -12,7 +12,7 @@ def test_deep_sleep_setup(generate_main): in main_cpp ) assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp - assert "App.register_component_(deepsleep);" in main_cpp + assert "App.register_component_(deepsleep, " in main_cpp def test_deep_sleep_sleep_duration(generate_main): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 4b3a4c705c1..4b8b7540e84 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -27,7 +27,7 @@ def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: assert "global_web_server_base" in main_cpp # Check component is registered - assert "App.register_component_(web_server_webserverotacomponent_id)" in main_cpp + assert "App.register_component_(web_server_webserverotacomponent_id" in main_cpp def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None: diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index a76ea21c23b..e389b56adac 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -34,8 +34,9 @@ async def test_register_component(monkeypatch): actual = await ch.register_component(var, {}) assert actual is var - assert add_mock.call_count == 2 - app_mock.register_component_.assert_called_with(var) + assert add_mock.call_count == 1 + app_mock.register_component_.assert_called_once() + assert app_mock.register_component_.call_args.args[0] is var assert core_mock.component_ids == [] @@ -77,8 +78,9 @@ async def test_register_component__with_setup_priority(monkeypatch): assert actual is var add_mock.assert_called() - assert add_mock.call_count == 4 - app_mock.register_component_.assert_called_with(var) + assert add_mock.call_count == 3 + app_mock.register_component_.assert_called_once() + assert app_mock.register_component_.call_args.args[0] is var assert core_mock.component_ids == [] From 112646a9c4ba92f49c3589e664d064f56193dee3 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 27 Apr 2026 05:02:09 +0200 Subject: [PATCH 1670/2056] [zigbee] add router for nrf52 (#16034) --- esphome/components/zigbee/__init__.py | 13 +++++++++---- esphome/components/zigbee/zigbee_zephyr.cpp | 2 ++ esphome/components/zigbee/zigbee_zephyr.py | 6 +++++- tests/components/zigbee/test.nrf52-mcumgr.yaml | 3 +++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 0bb5f95bb68..018dab73488 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -75,6 +75,13 @@ SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number) + +def _validate_router_sleepy(config: ConfigType) -> ConfigType: + if config.get(CONF_ROUTER) and config.get(CONF_SLEEPY): + raise cv.Invalid("router and sleepy are mutually exclusive") + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -82,10 +89,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MODEL, default=CORE.name): cv.All( cv.string, cv.Length(max=31) ), - cv.OnlyWith(CONF_ROUTER, "esp32", default=False): cv.All( - cv.requires_component("esp32"), - cv.boolean, - ), + cv.Optional(CONF_ROUTER, default=False): cv.boolean, cv.Optional(CONF_ON_JOIN): cv.All( cv.requires_component("nrf52"), automation.validate_automation(single=True), @@ -113,6 +117,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + _validate_router_sleepy, zigbee_require_vfs_select, zigbee_set_core_data, cv.Any( diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 90bb66c91d5..dfffd1c91f4 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -190,7 +190,9 @@ void ZigbeeComponent::setup() { ESP_LOGE(TAG, "Cannot load settings, err: %d", err); return; } +#ifdef CONFIG_ZIGBEE_ROLE_END_DEVICE zigbee_configure_sleepy_behavior(this->sleepy_); +#endif zigbee_enable(); } diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 7d904b6081d..b74074e50f8 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -52,6 +52,7 @@ from esphome.types import ConfigType from .const import ( CONF_ON_JOIN, CONF_POWER_SOURCE, + CONF_ROUTER, CONF_WIPE_ON_BOOT, KEY_ZIGBEE, POWER_SOURCE, @@ -160,7 +161,10 @@ zephyr_number = cv.Schema( async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("ZIGBEE", True) zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True) - zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True) + if config[CONF_ROUTER]: + zephyr_add_prj_conf("ZIGBEE_ROLE_ROUTER", True) + else: + zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True) zephyr_add_prj_conf("ZIGBEE_CHANNEL_SELECTION_MODE_MULTI", True) diff --git a/tests/components/zigbee/test.nrf52-mcumgr.yaml b/tests/components/zigbee/test.nrf52-mcumgr.yaml index bf3cb9cdd92..a81feea0691 100644 --- a/tests/components/zigbee/test.nrf52-mcumgr.yaml +++ b/tests/components/zigbee/test.nrf52-mcumgr.yaml @@ -1 +1,4 @@ <<: !include common_nrf52.yaml + +zigbee: + router: true From 79b741b8dc452de8430de732ae7ba316747456fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 22:03:39 -0500 Subject: [PATCH 1671/2056] [core] Combine entity register + configure_entity_ into one call (#16030) --- .../alarm_control_panel/__init__.py | 8 +++-- esphome/components/binary_sensor/__init__.py | 3 +- esphome/components/button/__init__.py | 3 +- esphome/components/climate/__init__.py | 8 +++-- esphome/components/cover/__init__.py | 3 +- esphome/components/datetime/__init__.py | 8 +++-- esphome/components/event/__init__.py | 3 +- esphome/components/fan/__init__.py | 8 +++-- esphome/components/infrared/__init__.py | 4 +-- esphome/components/light/__init__.py | 8 +++-- esphome/components/lock/__init__.py | 8 +++-- esphome/components/media_player/__init__.py | 3 +- esphome/components/number/__init__.py | 3 +- .../components/radio_frequency/__init__.py | 4 +-- esphome/components/select/__init__.py | 8 +++-- esphome/components/sensor/__init__.py | 3 +- esphome/components/switch/__init__.py | 3 +- esphome/components/text/__init__.py | 8 +++-- esphome/components/text_sensor/__init__.py | 3 +- esphome/components/update/__init__.py | 3 +- esphome/components/valve/__init__.py | 3 +- esphome/components/water_heater/__init__.py | 8 +++-- esphome/core/application.h | 13 ++++++-- esphome/core/entity_base.h | 3 ++ esphome/core/entity_helpers.py | 32 +++++++++++++++++-- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/helpers.py | 20 +++++++++--- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 6 ++-- 30 files changed, 145 insertions(+), 48 deletions(-) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 9fcdf42ecb4..2f5d4c7c2bf 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -13,7 +13,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@grahambrown11", "@hwstar"] @@ -181,7 +185,7 @@ async def setup_alarm_control_panel_core_(var, config): async def register_alarm_control_panel(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_alarm_control_panel(var)) + queue_entity_register("alarm_control_panel", config) CORE.register_platform_component("alarm_control_panel", var) await setup_alarm_control_panel_core_(var, config) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 29ddbab02cd..1456e5bc663 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -62,6 +62,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -624,7 +625,7 @@ async def setup_binary_sensor_core_(var, config): async def register_binary_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_binary_sensor(var)) + queue_entity_register("binary_sensor", config) CORE.register_platform_component("binary_sensor", var) await setup_binary_sensor_core_(var, config) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 2c19ea69b1a..dd4fde5705e 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -101,7 +102,7 @@ async def setup_button_core_(var, config): async def register_button(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_button(var)) + queue_entity_register("button", config) CORE.register_platform_component("button", var) await setup_button_core_(var, config) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index df77fa5c1c9..0fdb18a92c8 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -49,7 +49,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass IS_PLATFORM_COMPONENT = True @@ -442,7 +446,7 @@ async def setup_climate_core_(var, config): async def register_climate(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_climate(var)) + queue_entity_register("climate", config) CORE.register_platform_component("climate", var) await setup_climate_core_(var, config) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index fdfca55f0f8..41efd2ba7a5 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -39,6 +39,7 @@ from esphome.const import ( from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -232,7 +233,7 @@ async def setup_cover_core_(var, config): async def register_cover(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_cover(var)) + queue_entity_register("cover", config) CORE.register_platform_component("cover", var) await setup_cover_core_(var, config) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 895ac4e243e..87997daa3d9 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -22,7 +22,11 @@ from esphome.const import ( CONF_YEAR, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -160,7 +164,7 @@ async def register_datetime(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) entity_type = config[CONF_TYPE].lower() - cg.add(getattr(cg.App, f"register_{entity_type}")(var)) + queue_entity_register(entity_type, config) CORE.register_platform_component(entity_type, var) await setup_datetime_core_(var, config) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 9c9dd025b18..4cab1bff9bb 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -108,7 +109,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]): async def register_event(var, config, *, event_types: list[str]): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_event(var)) + queue_entity_register("event", config) CORE.register_platform_component("event", var) await setup_event_core_(var, config, event_types=event_types) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index ce1e55d36b1..713f20fb95e 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -32,7 +32,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) IS_PLATFORM_COMPONENT = True @@ -292,7 +296,7 @@ async def setup_fan_core_(var, config): async def register_fan(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_fan(var)) + queue_entity_register("fan", config) CORE.register_platform_component("fan", var) await setup_fan_core_(var, config) diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py index 6a2a72fa5d7..f8e77209b24 100644 --- a/esphome/components/infrared/__init__.py +++ b/esphome/components/infrared/__init__.py @@ -12,7 +12,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority -from esphome.core.entity_helpers import setup_entity +from esphome.core.entity_helpers import queue_entity_register, setup_entity from esphome.coroutine import CoroPriority from esphome.types import ConfigType @@ -54,8 +54,8 @@ async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None: """Register an infrared device with the core.""" cg.add_define("USE_IR_RF") await cg.register_component(var, config) + queue_entity_register("infrared", config) await setup_infrared_core_(var, config) - cg.add(cg.App.register_infrared(var)) CORE.register_platform_component("infrared", var) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 5925afb472d..9540c644860 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -40,7 +40,11 @@ from esphome.const import ( CONF_WHITE, ) from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv from esphome.types import ConfigType @@ -405,7 +409,7 @@ async def setup_light_core_(light_var, config, output_var): async def register_light(output_var, config): light_var = cg.new_Pvariable(config[CONF_ID], output_var) - cg.add(cg.App.register_light(light_var)) + queue_entity_register("light", config) CORE.register_platform_component("light", light_var) await cg.register_component(light_var, config) await setup_light_core_(light_var, config, output_var) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index a36d52a5d82..0a8ad58bc2d 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -13,7 +13,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -112,7 +116,7 @@ async def _setup_lock_core(var, config): async def register_lock(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_lock(var)) + queue_entity_register("lock", config) CORE.register_platform_component("lock", var) await _setup_lock_core(var, config) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index d1db868ace4..0024e3b9658 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -21,6 +21,7 @@ from esphome.core import CORE from esphome.core.entity_helpers import ( entity_duplicate_validator, inherit_property_from, + queue_entity_register, setup_entity, ) from esphome.coroutine import CoroPriority, coroutine_with_priority @@ -262,7 +263,7 @@ async def setup_media_player_core_(var, config): async def register_media_player(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_media_player(var)) + queue_entity_register("media_player", config) CORE.register_platform_component("media_player", var) await setup_media_player_core_(var, config) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index f13ccc4c36d..ee2d53c65a4 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -82,6 +82,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, setup_unit_of_measurement, @@ -301,7 +302,7 @@ async def register_number( ): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_number(var)) + queue_entity_register("number", config) CORE.register_platform_component("number", var) await setup_number_core_( var, config, min_value=min_value, max_value=max_value, step=step diff --git a/esphome/components/radio_frequency/__init__.py b/esphome/components/radio_frequency/__init__.py index b00590ceb58..a54ab6e2492 100644 --- a/esphome/components/radio_frequency/__init__.py +++ b/esphome/components/radio_frequency/__init__.py @@ -12,7 +12,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority -from esphome.core.entity_helpers import setup_entity +from esphome.core.entity_helpers import queue_entity_register, setup_entity from esphome.coroutine import CoroPriority from esphome.types import ConfigType @@ -55,8 +55,8 @@ async def register_radio_frequency(var: cg.Pvariable, config: ConfigType) -> Non """Register a radio frequency device with the core.""" cg.add_define("USE_RADIO_FREQUENCY") await cg.register_component(var, config) + queue_entity_register("radio_frequency", config) await setup_radio_frequency_core_(var, config) - cg.add(cg.App.register_radio_frequency(var)) CORE.register_platform_component("radio_frequency", var) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index ba5214e550a..f561c030a49 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -19,7 +19,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass, TemplateArguments from esphome.cpp_types import global_ns @@ -113,7 +117,7 @@ async def setup_select_core_(var, config, *, options: list[str]): async def register_select(var, config, *, options: list[str]): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_select(var)) + queue_entity_register("select", config) CORE.register_platform_component("select", var) await setup_select_core_(var, config, options=options) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 43fbc989531..48b7d25d4df 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -109,6 +109,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, setup_unit_of_measurement, @@ -982,7 +983,7 @@ async def setup_sensor_core_(var, config): async def register_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_sensor(var)) + queue_entity_register("sensor", config) CORE.register_platform_component("sensor", var) await setup_sensor_core_(var, config) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 9fa4a013ff8..1108652e993 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -23,6 +23,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -166,7 +167,7 @@ async def setup_switch_core_(var, config): async def register_switch(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_switch(var)) + queue_entity_register("switch", config) CORE.register_platform_component("switch", var) await setup_switch_core_(var, config) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 224f4580d4a..06b5a108926 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -14,7 +14,11 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@mauritskorse"] @@ -122,7 +126,7 @@ async def register_text( ): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_text(var)) + queue_entity_register("text", config) CORE.register_platform_component("text", var) await setup_text_core_( var, config, min_length=min_length, max_length=max_length, pattern=pattern diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 94014e8d206..01a57cbaa1b 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -221,7 +222,7 @@ async def setup_text_sensor_core_(var, config): async def register_text_sensor(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_text_sensor(var)) + queue_entity_register("text_sensor", config) CORE.register_platform_component("text_sensor", var) await setup_text_sensor_core_(var, config) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index db6c1445e34..ddb471be18f 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -113,7 +114,7 @@ async def setup_update_core_(var, config): async def register_update(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_update(var)) + queue_entity_register("update", config) CORE.register_platform_component("update", var) await setup_update_core_(var, config) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 1930a7ad0c9..a6808c9da7b 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, + queue_entity_register, setup_device_class, setup_entity, ) @@ -162,7 +163,7 @@ async def _setup_valve_core(var, config): async def register_valve(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) - cg.add(cg.App.register_valve(var)) + queue_entity_register("valve", config) CORE.register_platform_component("valve", var) await _setup_valve_core(var, config) diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py index 58cf5a4054e..f3eec16a406 100644 --- a/esphome/components/water_heater/__init__.py +++ b/esphome/components/water_heater/__init__.py @@ -9,7 +9,11 @@ from esphome.const import ( CONF_VISUAL, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + queue_entity_register, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.types import ConfigType @@ -90,7 +94,7 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva cg.add_define("USE_WATER_HEATER") - cg.add(cg.App.register_water_heater(var)) + queue_entity_register("water_heater", config) CORE.register_platform_component("water_heater", var) await setup_water_heater_core_(var, config) diff --git a/esphome/core/application.h b/esphome/core/application.h index c0b2639bd18..185ee4163b1 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -103,10 +103,19 @@ class Application { void set_current_component(Component *component) { this->current_component_ = component; } Component *get_current_component() { return this->current_component_; } -// Entity register methods (generated from entity_types.h) +// Entity register methods (generated from entity_types.h). +// Each entity type gets two overloads: +// - register_<entity>(obj) — bare push_back +// - register_<entity>(obj, name, hash, fields) — configure_entity_ + push_back +// The 4-arg form lets codegen collapse `App.register_<entity>(obj); obj->configure_entity_(...);` +// into a single call site, saving flash and a `main.cpp` line per entity. // NOLINTBEGIN(bugprone-macro-parentheses) #define ENTITY_TYPE_(type, singular, plural, count, upper) \ - void register_##singular(type *obj) { this->plural##_.push_back(obj); } + void register_##singular(type *obj) { this->plural##_.push_back(obj); } \ + void register_##singular(type *obj, const char *name, uint32_t object_id_hash, uint32_t entity_fields) { \ + obj->configure_entity_(name, object_id_hash, entity_fields); \ + this->plural##_.push_back(obj); \ + } #define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ ENTITY_TYPE_(type, singular, plural, count, upper) #include "esphome/core/entity_types.h" diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 5a69c9dd09b..2726a92c97a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -238,6 +238,9 @@ class EntityBase { protected: friend void ::setup(); friend void ::original_setup(); + // Application's register_<entity>(obj, name, hash, fields) overloads call configure_entity_ + // before push_back, so codegen can emit a single combined call per entity. + friend class Application; /// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags. /// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above. diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f09dd013fe2..ff60260280a 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.config import ( UNIT_OF_MEASUREMENT_MAX_LENGTH, ) from esphome.cpp_generator import MockObj, RawStatement, add, get_variable +from esphome.cpp_types import App import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata @@ -52,6 +53,12 @@ _KEY_INTERNAL = "_entity_internal" _KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default" _KEY_ENTITY_CATEGORY = "_entity_category" +# Private config key for the App.register_<method> entry point. +# When set, finalize_entity_strings() emits a single combined call +# `App.register_<method>(var, name, hash, packed)` instead of separate +# `App.register_<method>(var)` and `var->configure_entity_(...)` calls. +_KEY_REGISTER_METHOD = "_entity_register_method" + # Maximum unique strings per category (8-bit index, 0 = not set) _MAX_DEVICE_CLASSES = 0xFF # 255 _MAX_UNITS = 0xFF # 255 @@ -271,11 +278,26 @@ def _describe_packed_flags(config: ConfigType, entity_category: int) -> str: return ", ".join(parts) +def queue_entity_register(method_name: str, config: ConfigType) -> None: + """Defer ``App.register_<method_name>(var)`` emission to ``finalize_entity_strings``. + + When the deferred call is emitted, it is folded with ``configure_entity_`` into + a single ``App.register_<method_name>(var, name, hash, packed)`` call site, + which removes one statement and one method dispatch per entity from the + generated ``main.cpp``. + """ + config[_KEY_REGISTER_METHOD] = method_name + + def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single configure_entity_() call with name, hash, packed string indices, and flags. + """Emit the entity-registration / configure_entity_ tail. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. + + If queue_entity_register() was called for this entity, emits one combined call + ``App.register_<method>(var, name, hash, packed)``. Otherwise falls back to a + standalone ``var->configure_entity_(name, hash, packed)``. """ entity_name = config[_KEY_ENTITY_NAME] object_id_hash = config[_KEY_OBJECT_ID_HASH] @@ -295,7 +317,13 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: ) # Build inline comment describing the packed flags for readability comment = _describe_packed_flags(config, entity_category) - expr = var.configure_entity_(entity_name, object_id_hash, packed) + register_method = config.get(_KEY_REGISTER_METHOD) + if register_method is not None: + expr = getattr(App, f"register_{register_method}")( + var, entity_name, object_id_hash, packed + ) + else: + expr = var.configure_entity_(entity_name, object_id_hash, packed) if comment: add(RawStatement(f"{expr}; // {comment}")) else: diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 4f41f2cc704..e1d999abc7c 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -31,7 +31,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->configure_entity_("test bs1",' in main_cpp + assert 'App.register_binary_sensor(bs_1, "test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 544e748f913..f8881a832ce 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -29,7 +29,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp + assert 'App.register_button(wol_1, "wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/helpers.py b/tests/component_tests/helpers.py index 568d1639d0c..2eb588c0ca3 100644 --- a/tests/component_tests/helpers.py +++ b/tests/component_tests/helpers.py @@ -8,12 +8,22 @@ INTERNAL_BIT = 1 << 24 def extract_packed_value(main_cpp: str, var_name: str) -> int: - """Extract the third (packed) argument from a configure_entity_ call.""" - pattern = ( - rf"{re.escape(var_name)}->configure_entity_\(" + """Extract the packed-fields argument from the entity's configure call. + + Matches both legacy form ``var->configure_entity_(name, hash, packed)`` and the + combined form ``App.register_<entity>(var, name, hash, packed)``. + """ + escaped_var = re.escape(var_name) + legacy_pattern = ( + rf"{escaped_var}->configure_entity_\(" r'"(?:\\.|[^"\\])*"' r",\s*\w+,\s*(\d+)\)" ) - match = re.search(pattern, main_cpp) - assert match, f"configure_entity_ call not found for {var_name}" + combined_pattern = ( + rf"App\.register_\w+\(\s*{escaped_var}\s*,\s*" + r'"(?:\\.|[^"\\])*"' + r",\s*\w+,\s*(\d+)\)" + ) + match = re.search(combined_pattern, main_cpp) or re.search(legacy_pattern, main_cpp) + assert match, f"configure call not found for {var_name}" return int(match.group(1)) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 63eb4f19515..f5ac07c1cd1 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -28,7 +28,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->configure_entity_("test 1 text",' in main_cpp + assert 'App.register_text(it_1, "test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index ae094fadf87..eb25af3095f 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -28,9 +28,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp - assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp - assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp + assert 'App.register_text_sensor(ts_1, "Template Text Sensor 1",' in main_cpp + assert 'App.register_text_sensor(ts_2, "Template Text Sensor 2",' in main_cpp + assert 'App.register_text_sensor(ts_3, "Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): From dec5d0449bd1c844ba89f9c5900d48db8b17a466 Mon Sep 17 00:00:00 2001 From: plazarre <paul@lazarre.com> Date: Mon, 27 Apr 2026 06:51:54 -0500 Subject: [PATCH 1672/2056] [esp32_ble_tracker] Hold COEX_PREFER_BT for the lifetime of any active connection (#16036) Co-authored-by: Paul Lazarre <plazarre@gmail.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 22 ++++++++++++++----- .../esp32_ble_tracker/esp32_ble_tracker.h | 10 ++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index c7f2319d69d..f57cb7f5dc5 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -166,8 +166,9 @@ void ESP32BLETracker::loop() { ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, - this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); + ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d", + this->client_state_counts_.connecting, this->client_state_counts_.discovered, + this->client_state_counts_.disconnecting, this->client_state_counts_.active); } // Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set @@ -190,10 +191,18 @@ void ESP32BLETracker::loop() { */ // Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and - // all clients are idle (their state changes increment version when they finish) + // no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states + // (their state changes increment version when they finish). CONNECTED / ESTABLISHED + // clients do NOT block this branch — the coex revert below has its own active-count gate. if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - this->update_coex_preference_(false); + // Only revert to BALANCE when no connections are active. Established connections + // continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic + // (advertisement upload, log streaming) competes for the shared radio. Reverting too + // early causes Bluedroid to time out at ~20s and synthesize status=133. + if (!counts.active) { + this->update_coex_preference_(false); + } #endif if (this->scan_continuous_) { this->start_scan_(false); // first = false @@ -701,9 +710,10 @@ void ESP32BLETracker::dump_config() { this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); ESP_LOGCONFIG(TAG, " Scanner State: %s\n" - " Connecting: %d, discovered: %d, disconnecting: %d", + " Connecting: %d, discovered: %d, disconnecting: %d, active: %d", this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting, - this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting, + this->client_state_counts_.active); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 43405b02b7f..78ff60f3741 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -160,9 +160,13 @@ struct ClientStateCounts { uint8_t connecting = 0; uint8_t discovered = 0; uint8_t disconnecting = 0; + // CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT + // while active connections may still need to send/receive GATT traffic. + uint8_t active = 0; bool operator==(const ClientStateCounts &other) const { - return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting; + return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting && + active == other.active; } bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } @@ -381,6 +385,10 @@ class ESP32BLETracker : public Component, case ClientState::CONNECTING: counts.connecting++; break; + case ClientState::CONNECTED: + case ClientState::ESTABLISHED: + counts.active++; + break; default: break; } From 24c6a0d711ea99aee1bb57c9f8336a8597e89a1c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 27 Apr 2026 08:17:02 -0400 Subject: [PATCH 1673/2056] [audio] Bump microDecoder library to v0.2.0 (#16054) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index fee582ca25c..fe111be31e7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -220,7 +220,7 @@ async def to_code(config): data = _get_data() if data.micro_decoder_support: - add_idf_component(name="esphome/micro-decoder", ref="0.1.1") + add_idf_component(name="esphome/micro-decoder", ref="0.2.0") # All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash if not data.flac_support: diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 11531e6d7b4..cb7f5903cfd 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -6,7 +6,7 @@ dependencies: esphome/esp-micro-speech-features: version: 1.2.3 esphome/micro-decoder: - version: 0.1.1 + version: 0.2.0 esphome/micro-flac: version: 0.1.1 esphome/micro-opus: From 7198c912c7095c0e33a3c237dc25aca02ec334c4 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:41:28 +0000 Subject: [PATCH 1674/2056] [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/esp32/preferences.cpp | 18 +++++++++++++++++- .../components/wifi/wifi_component_esp_idf.cpp | 5 ++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 925c4e76624..09835385ac2 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -18,6 +18,12 @@ struct NVSData { static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// open() runs from app_main() before the logger is initialized, so any failure +// must be deferred until after global_logger is set. This is emitted from the +// first make_preference() call, which runs from the generated setup() after +// log->pre_setup() has run at EARLY_INIT priority. +static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { // try find in pending saves and update that for (auto &obj : s_pending_save) { @@ -70,12 +76,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { } void ESP32Preferences::open() { + // Runs from app_main() before the logger is initialized; any logging here + // must be deferred. See s_open_err and make_preference() below. nvs_flash_init(); esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); if (err == 0) return; - ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); + s_open_err = err; nvs_flash_deinit(); nvs_flash_erase(); nvs_flash_init(); @@ -87,6 +95,14 @@ void ESP32Preferences::open() { } ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { + if (s_open_err != ESP_OK) { + if (this->nvs_handle == 0) { + ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err)); + } else { + ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err)); + } + s_open_err = ESP_OK; + } auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = this->nvs_handle; pref->key = type; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 29d135ce900..82ecc80811d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -179,7 +179,10 @@ void WiFiComponent::wifi_pre_setup_() { #endif // USE_WIFI_AP wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - // cfg.nvs_enable = false; + if (global_preferences->nvs_handle == 0) { + ESP_LOGW(TAG, "starting wifi without nvs"); + cfg.nvs_enable = false; + } err = esp_wifi_init(&cfg); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); From 01ac22391348410d4a806b5d7b45a23fe8236bbc Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:30:40 +0200 Subject: [PATCH 1675/2056] [nextion] Unify TFT upload ack timeout to 5000ms (#15960) --- esphome/components/nextion/nextion_upload_arduino.cpp | 11 +++++++++-- esphome/components/nextion/nextion_upload_esp32.cpp | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index c79c68552ea..e0d18352ff4 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -16,6 +16,13 @@ namespace esphome::nextion { static const char *const TAG = "nextion.upload.arduino"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; +// Timeout for display acknowledgment during TFT upload (ms). +// A single value is used for all chunks; the happy path returns as soon as +// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field +// reports showed the previous 500ms steady-state value was too tight for +// some firmware variants. +static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000; + // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -80,14 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); this->upload_first_chunk_sent_ = true; if (recv_string.empty()) { - ESP_LOGW(TAG, "No response from display during upload"); + ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS); allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 40a284dc46b..db4558e2fe2 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -19,6 +19,13 @@ namespace esphome::nextion { static const char *const TAG = "nextion.upload.esp32"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; +// Timeout for display acknowledgment during TFT upload (ms). +// A single value is used for all chunks; the happy path returns as soon as +// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field +// reports showed the previous 500ms steady-state value was too tight for +// some firmware variants. +static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000; + // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -96,7 +103,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; #ifdef USE_PSRAM @@ -109,7 +116,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r #endif upload_first_chunk_sent_ = true; if (recv_string.empty()) { - ESP_LOGW(TAG, "No response from display during upload"); + ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS); allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; From a34836c2906bd5a1454bca01a135ed9cb1d9e2ea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:27:08 +1200 Subject: [PATCH 1676/2056] [esp32_touch] Feed wdt (#16066) --- esphome/components/esp32_touch/esp32_touch.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e44bc807e9a..54bbbe52ed8 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() { // Do initial oneshot scans to populate baseline values for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); + App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe if (err != ESP_OK) { ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err)); } From 39a69385fba1ea825906e5ef8759f9a8c3a2351c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 27 Apr 2026 19:57:42 -0500 Subject: [PATCH 1677/2056] [image] Fix RGB565+alpha rendering for multi-frame animations (#16017) Co-authored-by: Claude <noreply@anthropic.com> --- esphome/components/animation/animation.cpp | 7 ++- esphome/components/image/__init__.py | 21 ++++--- tests/component_tests/image/test_init.py | 69 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index c2ae3b2f768..2f59a7fa5a7 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -62,7 +62,12 @@ void Animation::set_frame(int frame) { } void Animation::update_data_start_() { - const uint32_t image_size = this->get_width_stride() * this->height_; + uint32_t image_size = this->get_width_stride() * this->height_; + // RGB565 with an alpha channel stores the alpha plane immediately after the RGB + // plane within each frame, so the per-frame stride includes the alpha bytes. + if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + image_size += static_cast<uint32_t>(this->width_) * this->height_; + } this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 8375ab91d3e..365554f7d2d 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -744,21 +744,28 @@ async def write_image(config, all_frames=False): if frame_count <= 1: _LOGGER.warning("Image file %s has no animation frames", path) - total_rows = height * frame_count - encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) - if byte_order := config.get(CONF_BYTE_ORDER): - # Check for valid type has already been done in validate_settings - encoder.set_big_endian(byte_order == "BIG_ENDIAN") + # Encode each frame with its own encoder and concatenate. This keeps every + # frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane] + # per frame) so animation frame stepping in image.cpp / animation.cpp stays + # correct without needing to know the total frame count. + byte_order = config.get(CONF_BYTE_ORDER) + combined_data: list[int] = [] + encoder: ImageEncoder | None = None for frame_index in range(frame_count): image.seek(frame_index) + encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha) + if byte_order is not None: + # Check for valid type has already been done in validate_settings + encoder.set_big_endian(byte_order == "BIG_ENDIAN") pixels = encoder.convert(image.resize((width, height)), path).getdata() for row in range(height): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() - encoder.end_image() + encoder.end_image() + combined_data.extend(encoder.data) - rhs = [HexInt(x) for x in encoder.data] + rhs = [HexInt(x) for x in combined_data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) image_type = get_image_type_enum(type) trans_value = get_transparency_enum(encoder.transparency) diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 6f73888c7d1..f7f60a1f4d5 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -7,10 +7,12 @@ from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch +from PIL import Image as PILImage import pytest from esphome import config_validation as cv from esphome.components.image import ( + CONF_ALPHA_CHANNEL, CONF_INVERT_ALPHA, CONF_OPAQUE, CONF_TRANSPARENCY, @@ -411,3 +413,70 @@ async def test_svg_with_mm_dimensions_succeeds( assert 30 < height < 50, ( f"Height should be around 39 pixels for 10mm at 100dpi, got {height}" ) + + +@pytest.mark.asyncio +async def test_rgb565_alpha_animation_layout_per_frame( + tmp_path: Path, + mock_progmem_array: MagicMock, +) -> None: + """RGB565+alpha animations must store each frame as a self-contained + [RGB plane | alpha plane] block. Animation::update_data_start_ steps frames + with a single per-frame stride, so any cross-frame layout (all RGB then all + alpha) makes the C++ alpha read land in the next frame's RGB bytes — that + was the regression behind issue #15999. + """ + # Build a 2-frame APNG where each frame is a solid color with a known + # alpha. APNG preserves full RGBA per pixel (GIF only has 1-bit alpha so + # round-tripping mid-range alpha values does not work). Frame 0 is fully + # opaque red, frame 1 is fully transparent blue. + width = 4 + height = 3 + frame0 = PILImage.new("RGBA", (width, height), (255, 0, 0, 0xFF)) + frame1 = PILImage.new("RGBA", (width, height), (0, 0, 255, 0x00)) + apng_path = tmp_path / "anim.png" + frame0.save( + apng_path, + format="PNG", + save_all=True, + append_images=[frame1], + duration=100, + loop=0, + ) + + config = { + CONF_FILE: str(apng_path), + CONF_TYPE: "RGB565", + CONF_TRANSPARENCY: CONF_ALPHA_CHANNEL, + CONF_DITHER: "NONE", + CONF_INVERT_ALPHA: False, + CONF_RAW_DATA_ID: "test_raw_data_id", + } + + _, _, _, _, _, frame_count = await write_image(config, all_frames=True) + assert frame_count == 2 + + # Recover the bytes handed to progmem_array. Signature is (id_, rhs). + _, raw_data = mock_progmem_array.call_args.args + data = [int(x) for x in raw_data] + + rgb_size = width * height * 2 + alpha_size = width * height + frame_size = rgb_size + alpha_size + assert len(data) == frame_size * frame_count, ( + "RGB565+alpha animation buffer must be (RGB + alpha) per frame, not " + "all RGB followed by all alpha" + ) + + # Frame 0: RGB plane is red, alpha plane is 0xFF. Frame 1: alpha plane is + # 0x00. If the layout regresses to [all RGB | all alpha], the alpha bytes + # would all land at the tail of the buffer and the per-frame slices below + # would point at RGB565 noise instead. + frame0_alpha = data[rgb_size : rgb_size + alpha_size] + frame1_alpha = data[frame_size + rgb_size : frame_size + rgb_size + alpha_size] + assert all(a == 0xFF for a in frame0_alpha), ( + f"Frame 0 alpha plane should be opaque, got {frame0_alpha}" + ) + assert all(a == 0x00 for a in frame1_alpha), ( + f"Frame 1 alpha plane should be transparent, got {frame1_alpha}" + ) From c26ea52620a5f3a88a2eb7a2e4b5446dedf77195 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:35:00 +1000 Subject: [PATCH 1678/2056] [lvgl] Triggers on tabview tabs fix (#15935) --- esphome/components/lvgl/widgets/tabview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 108bb38df58..5e9e0494dd5 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -22,7 +22,7 @@ from ..defines import ( literal, ) from ..lv_validation import animated, lv_int, size -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties @@ -83,8 +83,8 @@ class TabviewType(WidgetType): await w.set_property("tab_bar_size", await size.process(config[CONF_SIZE])) for tab_conf in config[CONF_TABS]: w_id = tab_conf[CONF_ID] - tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) - tab_widget = Widget.create(w_id, tab_obj, obj_spec) + tab_obj = lv_Pvariable(lv_tab_t, w_id) + tab_widget = Widget.create(w_id, tab_obj, obj_spec, tab_conf) lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) await set_obj_properties(tab_widget, tab_conf) await add_widgets(tab_widget, tab_conf) From b753ee4e94d060a55464c55e1a94a96b135aeed1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:35:13 +1200 Subject: [PATCH 1679/2056] [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) --- esphome/components/time/__init__.py | 7 +++ tests/unit_tests/components/test_time.py | 67 +++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ac0abeee0f..9e79c8e6c2e 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,3 +1,4 @@ +import errno from importlib import resources import logging @@ -74,6 +75,12 @@ def _load_tzdata(iana_key: str) -> bytes | None: return (resources.files(package) / resource).read_bytes() except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None + except OSError as e: + # Windows raises EINVAL for paths with NTFS-illegal chars (e.g. '<'/'>' + # in POSIX TZ strings like "<+08>-8" that validate_tz feeds back here). + if e.errno == errno.EINVAL: + return None + raise def _extract_tz_string(tzfile: bytes) -> str: diff --git a/tests/unit_tests/components/test_time.py b/tests/unit_tests/components/test_time.py index 48988fb03f9..6325bfbe75c 100644 --- a/tests/unit_tests/components/test_time.py +++ b/tests/unit_tests/components/test_time.py @@ -1,6 +1,11 @@ """Tests for time component cron expression parsing.""" -from esphome.components.time import _parse_cron_part +import errno +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.components.time import _load_tzdata, _parse_cron_part, validate_tz def test_star_slash_seconds() -> None: @@ -78,3 +83,63 @@ def test_range() -> None: def test_single_value() -> None: assert _parse_cron_part("30", 0, 59, {}) == {30} + + +def _mock_resources_with_error(error: Exception) -> MagicMock: + """Return a mock of importlib.resources.files where read_bytes raises error.""" + leaf = MagicMock() + leaf.read_bytes.side_effect = error + package = MagicMock() + package.__truediv__.return_value = leaf + return MagicMock(return_value=package) + + +def test_load_tzdata_returns_none_on_windows_einval() -> None: + """On Windows, opening a tzdata path with NTFS-illegal chars raises OSError(EINVAL). + + Regression test for crash when the system TZ resolves to a POSIX string like + "<+08>-8" (Asia/Shanghai, IST, etc.) and is fed back into _load_tzdata by + validate_tz to check whether it is also a valid IANA key. + """ + err = OSError(errno.EINVAL, "Invalid argument") + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(err), + ): + assert _load_tzdata("<+08>-8") is None + + +def test_load_tzdata_propagates_unexpected_oserror() -> None: + """Unrelated OSErrors (e.g. PermissionError) must not be swallowed.""" + with ( + patch( + "esphome.components.time.resources.files", + _mock_resources_with_error( + PermissionError(errno.EACCES, "Permission denied") + ), + ), + pytest.raises(PermissionError), + ): + _load_tzdata("Some/Zone") + + +def test_load_tzdata_returns_none_on_file_not_found() -> None: + """Existing behavior: missing tz file returns None rather than raising.""" + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(FileNotFoundError()), + ): + assert _load_tzdata("Not/A/Zone") is None + + +def test_validate_tz_accepts_posix_string_when_read_bytes_raises_einval() -> None: + """validate_tz must not crash when _load_tzdata hits the Windows EINVAL path. + + Simulates the Windows case where the auto-detected POSIX TZ string is fed + back through _load_tzdata and the underlying read_bytes raises errno 22. + """ + with patch( + "esphome.components.time.resources.files", + _mock_resources_with_error(OSError(errno.EINVAL, "Invalid argument")), + ): + assert validate_tz("<+08>-8") == "<+08>-8" From 6a5919ee8764571ce4dff8c3d939d1230531e112 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 02:19:59 -0500 Subject: [PATCH 1680/2056] [deep_sleep] Fix sleep_duration codegen type to uint32_t (#15965) --- esphome/components/deep_sleep/__init__.py | 2 +- tests/components/deep_sleep/common.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 16329bb0fa0..a98b7e60ef2 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -413,7 +413,7 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) if CONF_SLEEP_DURATION in config: - template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) + template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32) cg.add(var.set_sleep_duration(template_)) if CONF_UNTIL in config: diff --git a/tests/components/deep_sleep/common.yaml b/tests/components/deep_sleep/common.yaml index c090cb83e2d..7a1a709965b 100644 --- a/tests/components/deep_sleep/common.yaml +++ b/tests/components/deep_sleep/common.yaml @@ -4,3 +4,9 @@ esphome: - deep_sleep.prevent - delay: 1s - deep_sleep.allow + - if: + condition: + lambda: 'return false;' + then: + - deep_sleep.enter: + sleep_duration: 60min From 4137d93cbfc34778fed7b8b4697a8eb04c7e6777 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 24 Apr 2026 03:42:36 -0500 Subject: [PATCH 1681/2056] [wifi] Fix stale wifi.connected after state transition (#15966) --- esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ esphome/components/wifi/wifi_component_libretiny.cpp | 2 ++ esphome/components/wifi/wifi_component_pico_w.cpp | 2 ++ 5 files changed, 10 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7b31a22ed5b..6b49368933f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1570,6 +1570,8 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { #endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; + // Refresh is_connected() cache; loop()'s refresh ran before this transition. + this->update_connected_state_(); this->num_retried_ = 0; this->print_connect_params_(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index cb53d3ac1bd..d1a31cdfc94 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -948,6 +948,8 @@ void WiFiComponent::process_pending_callbacks_() { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS if (this->pending_.disconnect) { this->pending_.disconnect = false; + // Refresh is_connected() cache here, not in the SDK callback (sys context). + this->update_connected_state_(); this->notify_disconnect_state_listeners_(); } #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4097df80afd..e166fadb275 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -796,6 +796,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; + // Refresh is_connected() cache; error_from_callback_ makes it false. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 9565ffa7473..b721364631d 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -536,6 +536,8 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { this->error_from_callback_ = true; } + // Refresh is_connected() cache; sta_state_/error_from_callback_ make it false. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1cfeee3c1bf..a50dfd8c807 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -342,6 +342,8 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); + // Refresh is_connected() cache; driver link status reports disconnected. + this->update_connected_state_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); #endif From 433bbdb0163dabcbf31afb3d93d500a6a406020c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 26 Apr 2026 07:23:41 -0500 Subject: [PATCH 1682/2056] [rotary_encoder][at581x] Fix templatable int field types (#16015) --- esphome/components/at581x/__init__.py | 8 ++++---- esphome/components/rotary_encoder/sensor.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 94b68db4b33..5031b72cceb 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -183,19 +183,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_sensing_distance(template_)) if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): - template_ = await cg.templatable(selfcheck, args, cg.int32) + template_ = await cg.templatable(selfcheck, args, cg.int_) cg.add(var.set_poweron_selfcheck_time(template_)) if protect := config.get(CONF_PROTECT_TIME): - template_ = await cg.templatable(protect, args, cg.int32) + template_ = await cg.templatable(protect, args, cg.int_) cg.add(var.set_protect_time(template_)) if trig_base := config.get(CONF_TRIGGER_BASE): - template_ = await cg.templatable(trig_base, args, cg.int32) + template_ = await cg.templatable(trig_base, args, cg.int_) cg.add(var.set_trigger_base(template_)) if trig_keep := config.get(CONF_TRIGGER_KEEP): - template_ = await cg.templatable(trig_keep, args, cg.int32) + template_ = await cg.templatable(trig_keep, args, cg.int_) cg.add(var.set_trigger_keep(template_)) if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 21239863e45..0e5a03523df 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -129,6 +129,6 @@ async def to_code(config): async def sensor_template_publish_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_VALUE], args, cg.int32) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.int_) cg.add(var.set_value(template_)) return var From aea88aef5e9fdd053ed61007e2a17ccbd4b6982e Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:41:28 +0000 Subject: [PATCH 1683/2056] [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/esp32/preferences.cpp | 18 +++++++++++++++++- .../components/wifi/wifi_component_esp_idf.cpp | 5 ++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index bc0a34ebe86..72a0d979d9c 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -22,6 +22,12 @@ struct NVSData { static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// open() runs from app_main() before the logger is initialized, so any failure +// must be deferred until after global_logger is set. This is emitted from the +// first make_preference() call, which runs from the generated setup() after +// log->pre_setup() has run at EARLY_INIT priority. +static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { // try find in pending saves and update that for (auto &obj : s_pending_save) { @@ -74,12 +80,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { } void ESP32Preferences::open() { + // Runs from app_main() before the logger is initialized; any logging here + // must be deferred. See s_open_err and make_preference() below. nvs_flash_init(); esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); if (err == 0) return; - ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); + s_open_err = err; nvs_flash_deinit(); nvs_flash_erase(); nvs_flash_init(); @@ -91,6 +99,14 @@ void ESP32Preferences::open() { } ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { + if (s_open_err != ESP_OK) { + if (this->nvs_handle == 0) { + ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err)); + } else { + ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err)); + } + s_open_err = ESP_OK; + } auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = this->nvs_handle; pref->key = type; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e166fadb275..a6a48409bca 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -179,7 +179,10 @@ void WiFiComponent::wifi_pre_setup_() { #endif // USE_WIFI_AP wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - // cfg.nvs_enable = false; + if (global_preferences->nvs_handle == 0) { + ESP_LOGW(TAG, "starting wifi without nvs"); + cfg.nvs_enable = false; + } err = esp_wifi_init(&cfg); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); From a186f6fea9663d7daaf937021ea801073313f5d4 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:30:40 +0200 Subject: [PATCH 1684/2056] [nextion] Unify TFT upload ack timeout to 5000ms (#15960) --- esphome/components/nextion/nextion_upload_arduino.cpp | 11 +++++++++-- esphome/components/nextion/nextion_upload_esp32.cpp | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index c79c68552ea..e0d18352ff4 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -16,6 +16,13 @@ namespace esphome::nextion { static const char *const TAG = "nextion.upload.arduino"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; +// Timeout for display acknowledgment during TFT upload (ms). +// A single value is used for all chunks; the happy path returns as soon as +// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field +// reports showed the previous 500ms steady-state value was too tight for +// some firmware variants. +static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000; + // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -80,14 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, EspClass::getFreeHeap()); this->upload_first_chunk_sent_ = true; if (recv_string.empty()) { - ESP_LOGW(TAG, "No response from display during upload"); + ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS); allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 40a284dc46b..db4558e2fe2 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -19,6 +19,13 @@ namespace esphome::nextion { static const char *const TAG = "nextion.upload.esp32"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; +// Timeout for display acknowledgment during TFT upload (ms). +// A single value is used for all chunks; the happy path returns as soon as +// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field +// reports showed the previous 500ms steady-state value was too tight for +// some firmware variants. +static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000; + // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -96,7 +103,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); - this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); + this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; #ifdef USE_PSRAM @@ -109,7 +116,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r #endif upload_first_chunk_sent_ = true; if (recv_string.empty()) { - ESP_LOGW(TAG, "No response from display during upload"); + ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS); allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; From 191d3bc7e400ddfe228587c1a512b60f7c03706a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:27:08 +1200 Subject: [PATCH 1685/2056] [esp32_touch] Feed wdt (#16066) --- esphome/components/esp32_touch/esp32_touch.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e44bc807e9a..54bbbe52ed8 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() { // Do initial oneshot scans to populate baseline values for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); + App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe if (err != ESP_OK) { ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err)); } From 3ac0939f55a79655f52648696473806d84109b22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 27 Apr 2026 19:57:42 -0500 Subject: [PATCH 1686/2056] [image] Fix RGB565+alpha rendering for multi-frame animations (#16017) Co-authored-by: Claude <noreply@anthropic.com> --- esphome/components/animation/animation.cpp | 7 ++- esphome/components/image/__init__.py | 21 ++++--- tests/component_tests/image/test_init.py | 69 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index c2ae3b2f768..2f59a7fa5a7 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -62,7 +62,12 @@ void Animation::set_frame(int frame) { } void Animation::update_data_start_() { - const uint32_t image_size = this->get_width_stride() * this->height_; + uint32_t image_size = this->get_width_stride() * this->height_; + // RGB565 with an alpha channel stores the alpha plane immediately after the RGB + // plane within each frame, so the per-frame stride includes the alpha bytes. + if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + image_size += static_cast<uint32_t>(this->width_) * this->height_; + } this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 8375ab91d3e..365554f7d2d 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -744,21 +744,28 @@ async def write_image(config, all_frames=False): if frame_count <= 1: _LOGGER.warning("Image file %s has no animation frames", path) - total_rows = height * frame_count - encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) - if byte_order := config.get(CONF_BYTE_ORDER): - # Check for valid type has already been done in validate_settings - encoder.set_big_endian(byte_order == "BIG_ENDIAN") + # Encode each frame with its own encoder and concatenate. This keeps every + # frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane] + # per frame) so animation frame stepping in image.cpp / animation.cpp stays + # correct without needing to know the total frame count. + byte_order = config.get(CONF_BYTE_ORDER) + combined_data: list[int] = [] + encoder: ImageEncoder | None = None for frame_index in range(frame_count): image.seek(frame_index) + encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha) + if byte_order is not None: + # Check for valid type has already been done in validate_settings + encoder.set_big_endian(byte_order == "BIG_ENDIAN") pixels = encoder.convert(image.resize((width, height)), path).getdata() for row in range(height): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() - encoder.end_image() + encoder.end_image() + combined_data.extend(encoder.data) - rhs = [HexInt(x) for x in encoder.data] + rhs = [HexInt(x) for x in combined_data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) image_type = get_image_type_enum(type) trans_value = get_transparency_enum(encoder.transparency) diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 6f73888c7d1..f7f60a1f4d5 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -7,10 +7,12 @@ from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch +from PIL import Image as PILImage import pytest from esphome import config_validation as cv from esphome.components.image import ( + CONF_ALPHA_CHANNEL, CONF_INVERT_ALPHA, CONF_OPAQUE, CONF_TRANSPARENCY, @@ -411,3 +413,70 @@ async def test_svg_with_mm_dimensions_succeeds( assert 30 < height < 50, ( f"Height should be around 39 pixels for 10mm at 100dpi, got {height}" ) + + +@pytest.mark.asyncio +async def test_rgb565_alpha_animation_layout_per_frame( + tmp_path: Path, + mock_progmem_array: MagicMock, +) -> None: + """RGB565+alpha animations must store each frame as a self-contained + [RGB plane | alpha plane] block. Animation::update_data_start_ steps frames + with a single per-frame stride, so any cross-frame layout (all RGB then all + alpha) makes the C++ alpha read land in the next frame's RGB bytes — that + was the regression behind issue #15999. + """ + # Build a 2-frame APNG where each frame is a solid color with a known + # alpha. APNG preserves full RGBA per pixel (GIF only has 1-bit alpha so + # round-tripping mid-range alpha values does not work). Frame 0 is fully + # opaque red, frame 1 is fully transparent blue. + width = 4 + height = 3 + frame0 = PILImage.new("RGBA", (width, height), (255, 0, 0, 0xFF)) + frame1 = PILImage.new("RGBA", (width, height), (0, 0, 255, 0x00)) + apng_path = tmp_path / "anim.png" + frame0.save( + apng_path, + format="PNG", + save_all=True, + append_images=[frame1], + duration=100, + loop=0, + ) + + config = { + CONF_FILE: str(apng_path), + CONF_TYPE: "RGB565", + CONF_TRANSPARENCY: CONF_ALPHA_CHANNEL, + CONF_DITHER: "NONE", + CONF_INVERT_ALPHA: False, + CONF_RAW_DATA_ID: "test_raw_data_id", + } + + _, _, _, _, _, frame_count = await write_image(config, all_frames=True) + assert frame_count == 2 + + # Recover the bytes handed to progmem_array. Signature is (id_, rhs). + _, raw_data = mock_progmem_array.call_args.args + data = [int(x) for x in raw_data] + + rgb_size = width * height * 2 + alpha_size = width * height + frame_size = rgb_size + alpha_size + assert len(data) == frame_size * frame_count, ( + "RGB565+alpha animation buffer must be (RGB + alpha) per frame, not " + "all RGB followed by all alpha" + ) + + # Frame 0: RGB plane is red, alpha plane is 0xFF. Frame 1: alpha plane is + # 0x00. If the layout regresses to [all RGB | all alpha], the alpha bytes + # would all land at the tail of the buffer and the per-frame slices below + # would point at RGB565 noise instead. + frame0_alpha = data[rgb_size : rgb_size + alpha_size] + frame1_alpha = data[frame_size + rgb_size : frame_size + rgb_size + alpha_size] + assert all(a == 0xFF for a in frame0_alpha), ( + f"Frame 0 alpha plane should be opaque, got {frame0_alpha}" + ) + assert all(a == 0x00 for a in frame1_alpha), ( + f"Frame 1 alpha plane should be transparent, got {frame1_alpha}" + ) From 95b5ab7e78fabb794c69eb04669279fdfe9d767b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:58:29 +1200 Subject: [PATCH 1687/2056] Bump version to 2026.4.3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1cd12551dd1..0e6a845ed88 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.2 +PROJECT_NUMBER = 2026.4.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index ef37cb2df6c..89b6ff15ee9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.2" +__version__ = "2026.4.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a03de7cea2fa0747a59fe7502f4d072fa68cf25e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 27 Apr 2026 20:23:08 -0500 Subject: [PATCH 1688/2056] [core] Freshen loop_component_start_time_ before scheduler dispatch (#16064) --- esphome/core/application.h | 4 ++++ esphome/core/scheduler.cpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/esphome/core/application.h b/esphome/core/application.h index 185ee4163b1..221081a0e40 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -377,12 +377,16 @@ class Application { protected: friend Component; + friend class Scheduler; #ifdef USE_RUNTIME_STATS friend class runtime_stats::RuntimeStatsCollector; #endif friend void ::setup(); friend void ::original_setup(); + /// Freshen the cached loop component start time. Called by Scheduler before each dispatch. + void set_loop_component_start_time_(uint32_t now) { this->loop_component_start_time_ = now; } + /// Walk all registered components looking for any whose component_state_ /// has the given flag set. Used by Component::status_clear_*_slow_path_() /// (which is a friend) to decide whether to clear the corresponding bit on diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d83d67d6e42..11884ce4ba9 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -772,6 +772,8 @@ Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() { // Helper to execute a scheduler item uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); + // Freshen so callbacks reading App.get_loop_component_start_time() see this item's dispatch time. + App.set_loop_component_start_time_(now); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); uint32_t end = guard.finish(); From 42c9fdc87ef42eb7e5f894abe587d2f321915d8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 27 Apr 2026 23:39:08 -0500 Subject: [PATCH 1689/2056] [feedback] Use App.get_loop_component_start_time() and constexpr timeout id (#16063) --- .../components/feedback/feedback_cover.cpp | 20 +++++++++---------- esphome/components/feedback/feedback_cover.h | 6 ++---- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 1dff210cd6b..672e99949b7 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -3,11 +3,12 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace feedback { +namespace esphome::feedback { static const char *const TAG = "feedback.cover"; +static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1; + using namespace esphome::cover; void FeedbackCover::setup() { @@ -37,7 +38,7 @@ void FeedbackCover::setup() { } #endif - this->last_recompute_time_ = this->start_dir_time_ = millis(); + this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time(); } CoverTraits FeedbackCover::get_traits() { @@ -135,7 +136,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop #endif void FeedbackCover::endstop_reached_(bool open_endstop) { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); this->position = open_endstop ? COVER_OPEN : COVER_CLOSED; @@ -174,7 +175,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr)) #endif { - auto now = millis(); + const uint32_t now = App.get_loop_component_start_time(); this->current_operation = operation; this->start_dir_time_ = this->last_recompute_time_ = now; this->publish_state(); @@ -306,7 +307,7 @@ void FeedbackCover::control(const CoverCall &call) { void FeedbackCover::stop_prev_trigger_() { if (this->direction_change_waittime_.has_value()) { - this->cancel_timeout("direction_change"); + this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID); } if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); @@ -377,7 +378,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) { ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); this->start_direction_(COVER_OPERATION_IDLE); - this->set_timeout("direction_change", *this->direction_change_waittime_, + this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_, [this, dir]() { this->start_direction_(dir); }); } else { @@ -395,7 +396,7 @@ void FeedbackCover::recompute_position_() { if (this->current_operation == COVER_OPERATION_IDLE) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); float dir; float action_dur; float min_pos; @@ -451,5 +452,4 @@ void FeedbackCover::recompute_position_() { this->last_recompute_time_ = now; } -} // namespace feedback -} // namespace esphome +} // namespace esphome::feedback diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 6be8939413d..ed6f7490f8b 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -8,8 +8,7 @@ #endif #include "esphome/components/cover/cover.h" -namespace esphome { -namespace feedback { +namespace esphome::feedback { class FeedbackCover : public cover::Cover, public Component { public: @@ -85,5 +84,4 @@ class FeedbackCover : public cover::Cover, public Component { uint32_t update_interval_{1000}; }; -} // namespace feedback -} // namespace esphome +} // namespace esphome::feedback From 792f2e83630088536f33bf6d79d07fb75a15078b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 00:29:42 -0500 Subject: [PATCH 1690/2056] [ota] Add wall-clock timeout to OTA data transfer loop (#16047) --- esphome/components/esphome/ota/ota_esphome.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 47f661a8eaa..be771eb6899 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -292,6 +292,7 @@ void ESPHomeOTAComponent::handle_data_() { bool update_started = false; size_t total = 0; uint32_t last_progress = 0; + uint32_t last_data_ms = 0; uint8_t buf[OTA_BUFFER_SIZE]; char *sbuf = reinterpret_cast<char *>(buf); size_t ota_size; @@ -350,8 +351,18 @@ void ESPHomeOTAComponent::handle_data_() { // Acknowledge MD5 OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); + // Track when we last received data so a silently-vanished peer (no FIN/RST + // delivered, e.g. uploader killed mid-transfer or NAT/router dropped state) + // can't wedge the device indefinitely. Without this, the loop only exits + // on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP + // TCP keepalive isn't enabled here. + last_data_ms = millis(); while (total < ota_size) { - // TODO: timeout check + if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) { + ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA); + error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } size_t remaining = ota_size - total; size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; ssize_t read = this->client_->read(buf, requested); @@ -369,6 +380,7 @@ void ESPHomeOTAComponent::handle_data_() { goto error; // NOLINT(cppcoreguidelines-avoid-goto) } + last_data_ms = millis(); error_code = this->backend_->write(buf, read); if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGW(TAG, "Flash write err %d", error_code); From 49d3df2698b91f42cdeb8d723a5cc01c18467000 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 28 Apr 2026 05:27:20 -0500 Subject: [PATCH 1691/2056] [automation] Fix codegen type for component.resume update_interval (#16069) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/automation.py b/esphome/automation.py index 97d9a0a47a8..20eb9358cad 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -597,7 +597,7 @@ async def component_resume_action_to_code( comp = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, comp) if CONF_UPDATE_INTERVAL in config: - template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int) + template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32) cg.add(var.set_update_interval(template_)) return var From 41458d72e00f56f3a9850a2191b47bc4384c5d3b Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski <me@komzpa.net> Date: Tue, 28 Apr 2026 14:58:34 +0400 Subject: [PATCH 1692/2056] [esp32] Make Arduino app metadata reproducible (#16053) --- esphome/components/esp32/__init__.py | 9 +++--- .../config/reproducible_build_arduino.yaml | 8 ++++++ tests/component_tests/esp32/test_esp32.py | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/esp32/config/reproducible_build_arduino.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 78a1715ccfd..eb023ce32c3 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1724,15 +1724,16 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + # Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep + # volatile build path/time data out of the binary so equivalent projects can + # produce reproducible outputs and downstream tooling can reuse artifacts. + add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True) + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") if use_platformio: cg.add_platformio_option("framework", "espidf") - # Strip volatile build path/time metadata from PlatformIO-managed - # ESP-IDF builds so equivalent projects can produce reproducible - # outputs and downstream tooling can safely reuse artifacts. - add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True) # Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of # exception class overhead. See throw_stubs.cpp for implementation. diff --git a/tests/component_tests/esp32/config/reproducible_build_arduino.yaml b/tests/component_tests/esp32/config/reproducible_build_arduino.yaml new file mode 100644 index 00000000000..a5433a441d2 --- /dev/null +++ b/tests/component_tests/esp32/config/reproducible_build_arduino.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + variant: esp32 + framework: + type: arduino diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index c39a4aafc88..203f4841072 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_ESPHOME, CONF_IGNORE_PIN_VALIDATION_ERROR, CONF_NUMBER, + KEY_NATIVE_IDF, PlatformFramework, ) from esphome.core import CORE @@ -243,3 +244,30 @@ def test_platformio_idf_enables_reproducible_build( sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True + + +def test_platformio_arduino_enables_reproducible_build( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test PlatformIO Arduino builds enable reproducible app metadata.""" + generate_main(component_config_path("reproducible_build_arduino.yaml")) + + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True + + +def test_native_idf_enables_reproducible_build( + component_config_path: Callable[[str], Path], +) -> None: + """Test native ESP-IDF builds enable reproducible app metadata.""" + from esphome.__main__ import generate_cpp_contents + from esphome.config import read_config + + CORE.config_path = component_config_path("reproducible_build.yaml") + CORE.config = read_config({}) + CORE.data[KEY_NATIVE_IDF] = True + generate_cpp_contents(CORE.config) + + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True From 876c8c4c2a160f0aa7cd558c1ea452cad0fb59a3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:59:02 +1200 Subject: [PATCH 1693/2056] [ci-custom] Lint imports of esphome.components.const outside components (#16068) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- script/ci-custom.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index 4d71df74cfa..b257a3818bd 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -511,6 +511,40 @@ def lint_no_std_string_view(fname, match): ) +@lint_re_check( + r"(?:" + # `from esphome.components.const import ...` + r"from\s+esphome\.components\.const\s+import" + r"|" + # `import esphome.components.const` (with optional `as` alias) + r"import\s+esphome\.components\.const\b" + r"|" + # `from esphome.components import [(] ... const ... [)]` + # Handles parenthesized + multiline import lists by allowing newlines inside + # the parens via [^)]*. Single-line form falls back to the [^#\n]* branch. + r"from\s+esphome\.components\s+import\s*" + r"(?:\([^)]*\bconst\b[^)]*\)|(?:[^#\n]*[\s,])?\bconst\b)" + r")", + include=["*.py"], + exclude=[ + "esphome/components/*", + "tests/*", + "script/ci-custom.py", + ], +) +def lint_no_components_const_outside_components(fname, match): + return ( + f"Constants in {highlight('esphome/components/const/__init__.py')} are intended " + f"to be shared only between components in {highlight('esphome/components/')}. " + f"Code outside this folder must not import from " + f"{highlight('esphome.components.const')}.\n" + f"For core code (used outside {highlight('esphome/components/')}), define the " + f"constant in {highlight('esphome/const.py')} instead. When adding a new " + f"{highlight('CONF_')} constant there, bump {highlight('CONST_PY_MAX_CONF')} " + f"in this file accordingly (see {highlight('lint_const_py_frozen')})." + ) + + @lint_post_check def lint_constants_usage(): errs = [] From 52f80618d4b8e1b8f206bd360ca4f76116a69b7d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:00:29 +1000 Subject: [PATCH 1694/2056] [lvgl] Allow a binary sensor to report checked or pressed state (#16073) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- .../components/lvgl/binary_sensor/__init__.py | 35 ++++++++++++++----- tests/components/lvgl/lvgl-package.yaml | 15 ++++++++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index f9df7d23fa3..aa68e764211 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -4,15 +4,25 @@ from esphome.components.binary_sensor import ( new_binary_sensor, ) import esphome.config_validation as cv +from esphome.const import CONF_STATE -from ..defines import CONF_WIDGET -from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static -from ..types import LV_EVENT, lv_pseudo_button_t +from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant +from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static +from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t from ..widgets import Widget, get_widgets, wait_for_widgets +STATE_PRESSED = "PRESSED" +STATE_CHECKED = "CHECKED" + +BS_STATE = LvConstant( + "LV_STATE_", + STATE_PRESSED, + STATE_CHECKED, +) CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend( { cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of, } ) @@ -22,16 +32,23 @@ async def to_code(config): widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] assert isinstance(widget, Widget) + state = await BS_STATE.process(config[CONF_STATE]) await wait_for_widgets() - async with LambdaContext(EVENT_ARG) as pressed_ctx: - pressed_ctx.add(sensor.publish_state(widget.is_pressed())) + is_pressed = str(state) == str(LV_STATE.PRESSED) + test_expr = widget.is_pressed() if is_pressed else widget.is_checked() + async with LambdaContext(EVENT_ARG) as test_ctx: + test_ctx.add(sensor.publish_state(test_expr)) async with LvContext() as ctx: - ctx.add(sensor.publish_initial_state(widget.is_pressed())) + ctx.add(sensor.publish_initial_state(test_expr)) + if is_pressed: + events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED] + widget.add_flag(LV_OBJ_FLAG.CLICKABLE) + else: + events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT] ctx.add( lvgl_static.add_event_cb( widget.obj, - await pressed_ctx.get_lambda(), - LV_EVENT.PRESSED, - LV_EVENT.RELEASED, + await test_ctx.get_lambda(), + *events, ) ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d3565c6c59a..d6e237199ae 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,10 +16,19 @@ binary_sensor: platform: template - id: left_sensor platform: template + - platform: lvgl + name: Button A pressed + widget: button_a + state: pressed + - platform: lvgl + name: Button A checked + widget: button_a + state: checked - platform: lvgl id: button_checker name: LVGL button widget: button_button + state: checked on_state: then: - lvgl.checkbox.update: @@ -29,6 +38,12 @@ binary_sensor: auto y = x; // block inlining of one line return return y; + - platform: lvgl + id: button_presser + name: Button pressed + widget: button_button + state: pressed + lvgl: id: lvgl_id rotation: 90 From 8921e3bb3f821fff077300f04d7d52bc86f41997 Mon Sep 17 00:00:00 2001 From: Egor Vorontsov <sdoregor@sdore.me> Date: Tue, 28 Apr 2026 15:49:16 +0300 Subject: [PATCH 1695/2056] [api] add open states for `lock` to `api.proto` (#15901) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/api/api.proto | 2 ++ esphome/components/api/api_pb2.h | 2 ++ esphome/components/api/api_pb2_dump.cpp | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1c33d92bea4..c0fd990eca9 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1419,6 +1419,8 @@ enum LockState { LOCK_STATE_JAMMED = 3; LOCK_STATE_LOCKING = 4; LOCK_STATE_UNLOCKING = 5; + LOCK_STATE_OPENING = 6; + LOCK_STATE_OPEN = 7; } enum LockCommand { LOCK_UNLOCK = 0; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a8e01c017fe..7b82f1884d1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -181,6 +181,8 @@ enum LockState : uint32_t { LOCK_STATE_JAMMED = 3, LOCK_STATE_LOCKING = 4, LOCK_STATE_UNLOCKING = 5, + LOCK_STATE_OPENING = 6, + LOCK_STATE_OPEN = 7, }; enum LockCommand : uint32_t { LOCK_UNLOCK = 0, diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 541f5d4d11c..5258b355ceb 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -487,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v return ESPHOME_PSTR("LOCK_STATE_LOCKING"); case enums::LOCK_STATE_UNLOCKING: return ESPHOME_PSTR("LOCK_STATE_UNLOCKING"); + case enums::LOCK_STATE_OPENING: + return ESPHOME_PSTR("LOCK_STATE_OPENING"); + case enums::LOCK_STATE_OPEN: + return ESPHOME_PSTR("LOCK_STATE_OPEN"); default: return ESPHOME_PSTR("UNKNOWN"); } From 0759a3c6815e88fd333fef2ed843027ce24d445a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 08:48:13 -0500 Subject: [PATCH 1696/2056] [core] Split wake.{h,cpp} into per-platform files (#15978) --- esphome/core/config.py | 23 ++ esphome/core/defines.h | 15 +- esphome/core/time_64.cpp | 4 +- esphome/core/time_64.h | 14 +- esphome/core/wake.h | 214 ++---------------- esphome/core/wake/wake_esp8266.cpp | 21 ++ esphome/core/wake/wake_esp8266.h | 47 ++++ esphome/core/wake/wake_freertos.cpp | 33 +++ esphome/core/wake/wake_freertos.h | 60 +++++ esphome/core/wake/wake_generic.cpp | 17 ++ esphome/core/wake/wake_generic.h | 31 +++ esphome/core/{wake.cpp => wake/wake_host.cpp} | 89 +------- esphome/core/wake/wake_host.h | 64 ++++++ esphome/core/wake/wake_rp2040.cpp | 58 +++++ esphome/core/wake/wake_rp2040.h | 31 +++ esphome/loader.py | 50 ++-- tests/unit_tests/test_loader.py | 164 ++++++++++++++ 17 files changed, 632 insertions(+), 303 deletions(-) create mode 100644 esphome/core/wake/wake_esp8266.cpp create mode 100644 esphome/core/wake/wake_esp8266.h create mode 100644 esphome/core/wake/wake_freertos.cpp create mode 100644 esphome/core/wake/wake_freertos.h create mode 100644 esphome/core/wake/wake_generic.cpp create mode 100644 esphome/core/wake/wake_generic.h rename esphome/core/{wake.cpp => wake/wake_host.cpp} (74%) create mode 100644 esphome/core/wake/wake_host.h create mode 100644 esphome/core/wake/wake_rp2040.cpp create mode 100644 esphome/core/wake/wake_rp2040.h diff --git a/esphome/core/config.py b/esphome/core/config.py index 018e05f17b4..14161a7c8b6 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -792,6 +792,29 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + # Per-platform wake implementations — wake.h dispatches to exactly one of + # these based on USE_*, so the others can be skipped at the source level + # too. Header files next to each .cpp are always copied (the dispatcher + # #include's them) but compile to empty TUs on the wrong platform anyway. + "wake/wake_freertos.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "wake/wake_esp8266.cpp": { + PlatformFramework.ESP8266_ARDUINO, + }, + "wake/wake_rp2040.cpp": { + PlatformFramework.RP2040_ARDUINO, + }, + "wake/wake_host.cpp": { + PlatformFramework.HOST_NATIVE, + }, + "wake/wake_generic.cpp": { + PlatformFramework.NRF52_ZEPHYR, + }, # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered # as they are only included when needed by the preprocessor } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f929b224ca4..daca55d68a0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -17,8 +17,21 @@ #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API -// Default threading model for static analysis (ESP32 is multi-threaded with atomics) +// Threading model for static analysis. Match what the real codegen picks per +// platform (see esphome/components/<platform>/__init__.py ThreadModel.*): +// USE_ESP8266 / USE_RP2040 / USE_NRF52 → SINGLE +// USE_BK72XX (ARMv5TE, no LDREX/STREX) → MULTI_NO_ATOMICS +// everything else (ESP32, host, RTL87XX, LN882X) → MULTI_ATOMICS +// Without this the clang-tidy envs end up with USE_<single-threaded platform> +// + MULTI_ATOMICS simultaneously, a combination that can never occur in a +// real build. +#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_NRF52) +#define ESPHOME_THREAD_SINGLE +#elif defined(USE_BK72XX) +#define ESPHOME_THREAD_MULTI_NO_ATOMICS +#else #define ESPHOME_THREAD_MULTI_ATOMICS +#endif // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index cf651c3e91a..25076228d5e 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -22,8 +22,8 @@ static const char *const TAG = "time_64"; #ifdef ESPHOME_THREAD_SINGLE // Storage for Millis64Impl inline compute() — defined here so all TUs share one copy. -uint32_t Millis64Impl::last_millis_{0}; -uint16_t Millis64Impl::millis_major_{0}; +uint32_t Millis64Impl::last_millis{0}; +uint16_t Millis64Impl::millis_major{0}; #else uint64_t Millis64Impl::compute(uint32_t now) { diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h index 592e645d41a..d82373dbfe9 100644 --- a/esphome/core/time_64.h +++ b/esphome/core/time_64.h @@ -21,8 +21,8 @@ class Millis64Impl { #ifdef ESPHOME_THREAD_SINGLE // Storage defined in time_64.cpp — declared here so the inline body can access them. - static uint32_t last_millis_; - static uint16_t millis_major_; + static uint32_t last_millis; + static uint16_t millis_major; static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) { // Half the 32-bit range - used to detect rollovers vs normal time progression @@ -30,17 +30,17 @@ class Millis64Impl { // Single-core platforms have no concurrency, so this is a simple implementation // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. - uint16_t major = millis_major_; - uint32_t last = last_millis_; + uint16_t major = millis_major; + uint32_t last = last_millis; // Check for rollover if (now < last && (last - now) > HALF_MAX_UINT32) { - millis_major_++; + millis_major++; major++; - last_millis_ = now; + last_millis = now; } else if (now > last) { // Only update if time moved forward - last_millis_ = now; + last_millis = now; } // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 0cfca94a78e..a2f732fcdbf 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -3,6 +3,10 @@ /// @file wake.h /// Platform-specific main loop wake primitives. /// Always available on all platforms — no opt-in needed. +/// +/// The public API for callers lives here; the per-platform implementations +/// live under esphome/core/wake/ and are included at the bottom of this file +/// based on the active USE_* platform define. #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -11,21 +15,6 @@ #include <atomic> #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) -#include "esphome/core/main_task.h" -#endif -#ifdef USE_ESP8266 -#include <coredecls.h> -#elif defined(USE_RP2040) -#include <hardware/sync.h> -#include <pico/time.h> -#endif - -#ifdef USE_HOST -#include <sys/select.h> -#include <sys/socket.h> -#endif - namespace esphome { // === Wake flag for ESP8266/RP2040 === @@ -67,184 +56,19 @@ __attribute__((always_inline)) inline bool wake_request_take() { } #endif -// === ESP32 / LibreTiny (FreeRTOS) === -#if defined(USE_ESP32) || defined(USE_LIBRETINY) - -/// Wake the main loop from any context (ISR or task). -/// always_inline so callers placed in IRAM keep the whole wake path in IRAM. -__attribute__((always_inline)) inline void wake_main_task_any_context() { - // Set the wake-requested flag BEFORE the task notification so the consumer - // (Application::loop() gate) is guaranteed to see it on its next gate check. - wake_request_set(); - if (in_isr_context()) { - BaseType_t px_higher_priority_task_woken = pdFALSE; - esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); -#ifdef portYIELD_FROM_ISR - portYIELD_FROM_ISR(px_higher_priority_task_woken); -#else - // ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ - // exit sequence performs the context switch if one was requested. - (void) px_higher_priority_task_woken; -#endif - } else { - esphome_main_task_notify(); - } -} - -/// IRAM_ATTR entry points — defined in wake.cpp. -void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); -void wake_loop_any_context(); - -inline void wake_loop_threadsafe() { - wake_request_set(); - esphome_main_task_notify(); -} - -namespace internal { -inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { - // Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip - // event_callback wrapper (see lwip_fast_select.c) are the single source of truth for - // socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification - // that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns - // immediately) or wakes a blocked Take directly. Additional wake sources: - // wake_loop_threadsafe() from background tasks, and the ms timeout. - if (ms == 0) [[unlikely]] { - yield(); - return; - } - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); -} -} // namespace internal - -// === ESP8266 === -#elif defined(USE_ESP8266) - -/// Inline implementation — IRAM callers inline this directly. -inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { - // Set the wake-requested flag BEFORE esp_schedule so the consumer is - // guaranteed to see it on its next gate check. - wake_request_set(); - g_main_loop_woke = true; - esp_schedule(); -} - -/// IRAM_ATTR entry point for ISR callers — defined in wake.cpp. -void wake_loop_any_context(); - -/// Non-ISR: always inline. -inline void wake_loop_threadsafe() { wake_loop_impl(); } - -/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR. -inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); } - -namespace internal { -inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { - if (ms == 0) [[unlikely]] { - delay(0); - return; - } - if (g_main_loop_woke) { - g_main_loop_woke = false; - return; - } - esp_delay(ms, []() { return !g_main_loop_woke; }); -} -} // namespace internal - -// === RP2040 === -#elif defined(USE_RP2040) - -inline void wake_loop_any_context() { - // Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed - // to see it on its next gate check. - wake_request_set(); - g_main_loop_woke = true; - __sev(); -} - -inline void wake_loop_threadsafe() { wake_loop_any_context(); } - -/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp. -namespace internal { -void wakeable_delay(uint32_t ms); -} // namespace internal - -// === Host / Zephyr / other === -#else - -#ifdef USE_HOST -/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. -void wake_loop_threadsafe(); - -/// Register a socket file descriptor with the host select() loop. Not -/// thread-safe — main loop only. Returns false if fd is invalid or -/// >= FD_SETSIZE. -bool wake_register_fd(int fd); - -/// Unregister a socket file descriptor. Not thread-safe — main loop only. -void wake_unregister_fd(int fd); - -/// One-time setup of the loopback wake socket. Called from Application::setup(). -void wake_setup(); - -// wake_fd_ready() and wake_drain_notifications() are defined inline at the -// bottom of this file — they need internal::g_read_fds / g_wake_socket_fd in -// scope, which depend on USE_HOST-only includes pulled in above. -#else -/// Zephyr is currently the only platform without a wake mechanism. -/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). -/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. -inline void wake_loop_threadsafe() {} -#endif - -inline void wake_loop_any_context() { wake_loop_threadsafe(); } - -namespace internal { -#ifdef USE_HOST -/// Host wakeable_delay uses select() over the registered fds — defined in wake.cpp. -void wakeable_delay(uint32_t ms); -#else -inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { - if (ms == 0) [[unlikely]] { - yield(); - return; - } - delay(ms); -} -#endif -} // namespace internal - -#endif - -#ifdef USE_HOST -namespace internal { -// File-scope state owned by wake.cpp. Accessed inline by wake_drain_notifications() -// and wake_fd_ready() so the hot path stays in the header. -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern int g_wake_socket_fd; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern fd_set g_read_fds; -} // namespace internal - -inline bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd) { return FD_ISSET(fd, &internal::g_read_fds); } - -// Small buffer for draining wake notification bytes (1 byte sent per wake). -// Sized to drain multiple notifications per recvfrom() without wasting stack. -inline constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void ESPHOME_ALWAYS_INLINE wake_drain_notifications() { - // Called from main loop to drain any pending wake notifications. - // Must check wake_fd_ready() to avoid blocking on empty socket. - if (internal::g_wake_socket_fd >= 0 && wake_fd_ready(internal::g_wake_socket_fd)) { - char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads. Multiple wake events - // may have triggered multiple writes, so drain until EWOULDBLOCK. We control - // both ends of this loopback socket (always 1 byte per wake), so no error - // checking — any error indicates catastrophic system failure. - while (::recvfrom(internal::g_wake_socket_fd, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - } - } -} -#endif // USE_HOST - } // namespace esphome + +// Per-platform implementations. Each header re-enters namespace esphome {} and +// guards its body with the matching USE_* check, so only one contributes code +// for the active target. +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "esphome/core/wake/wake_freertos.h" +#elif defined(USE_ESP8266) +#include "esphome/core/wake/wake_esp8266.h" +#elif defined(USE_RP2040) +#include "esphome/core/wake/wake_rp2040.h" +#elif defined(USE_HOST) +#include "esphome/core/wake/wake_host.h" +#else +#include "esphome/core/wake/wake_generic.h" +#endif diff --git a/esphome/core/wake/wake_esp8266.cpp b/esphome/core/wake/wake_esp8266.cpp new file mode 100644 index 00000000000..9ced43c6dff --- /dev/null +++ b/esphome/core/wake/wake_esp8266.cpp @@ -0,0 +1,21 @@ +#include "esphome/core/defines.h" + +#ifdef USE_ESP8266 + +#include "esphome/core/hal.h" +#include "esphome/core/wake.h" + +namespace esphome { + +// === Wake-requested flag + main-loop woke flag storage === +// ESP8266 is always ESPHOME_THREAD_SINGLE. +// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; +volatile bool g_main_loop_woke = false; +// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) + +void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); } + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/core/wake/wake_esp8266.h b/esphome/core/wake/wake_esp8266.h new file mode 100644 index 00000000000..80cd61035be --- /dev/null +++ b/esphome/core/wake/wake_esp8266.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP8266 + +#include "esphome/core/hal.h" + +#include <coredecls.h> + +namespace esphome { + +/// Inline implementation — IRAM callers inline this directly. +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { + // Set the wake-requested flag BEFORE esp_schedule so the consumer is + // guaranteed to see it on its next gate check. + wake_request_set(); + g_main_loop_woke = true; + esp_schedule(); +} + +/// IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp. +void wake_loop_any_context(); + +/// Non-ISR: always inline. +inline void wake_loop_threadsafe() { wake_loop_impl(); } + +/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR. +inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); } + +namespace internal { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { + delay(0); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + esp_delay(ms, []() { return !g_main_loop_woke; }); +} +} // namespace internal + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/core/wake/wake_freertos.cpp b/esphome/core/wake/wake_freertos.cpp new file mode 100644 index 00000000000..0bf700daa89 --- /dev/null +++ b/esphome/core/wake/wake_freertos.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include "esphome/core/hal.h" +#include "esphome/core/wake.h" + +namespace esphome { + +// === Wake-requested flag storage === +// ESP32 is always MULTI_ATOMICS; LibreTiny is MULTI_ATOMICS on chips with +// proper atomics (e.g. RTL8720) and MULTI_NO_ATOMICS on others (e.g. BK72XX). +#ifdef ESPHOME_THREAD_MULTI_ATOMICS +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::atomic<uint8_t> g_wake_requested{0}; +#else +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; +#endif + +void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { + // ISR-safe: set flag before notify so the wake is visible on the next gate + // check. wake_request_set() is just an aligned 8-bit store / atomic store + // and is safe from IRAM. + wake_request_set(); + esphome_main_task_notify_from_isr(px_higher_priority_task_woken); +} + +void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } + +} // namespace esphome + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake/wake_freertos.h b/esphome/core/wake/wake_freertos.h new file mode 100644 index 00000000000..167a422c614 --- /dev/null +++ b/esphome/core/wake/wake_freertos.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#include "esphome/core/hal.h" +#include "esphome/core/main_task.h" + +namespace esphome { + +/// Wake the main loop from any context (ISR or task). +/// always_inline so callers placed in IRAM keep the whole wake path in IRAM. +__attribute__((always_inline)) inline void wake_main_task_any_context() { + // Set the wake-requested flag BEFORE the task notification so the consumer + // (Application::loop() gate) is guaranteed to see it on its next gate check. + wake_request_set(); + if (in_isr_context()) { + BaseType_t px_higher_priority_task_woken = pdFALSE; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); +#ifdef portYIELD_FROM_ISR + portYIELD_FROM_ISR(px_higher_priority_task_woken); +#else + // ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ + // exit sequence performs the context switch if one was requested. + (void) px_higher_priority_task_woken; +#endif + } else { + esphome_main_task_notify(); + } +} + +/// IRAM_ATTR entry points — defined in wake_freertos.cpp. +void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); +void wake_loop_any_context(); + +inline void wake_loop_threadsafe() { + wake_request_set(); + esphome_main_task_notify(); +} + +namespace internal { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + // Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip + // event_callback wrapper (see lwip_fast_select.c) are the single source of truth for + // socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification + // that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns + // immediately) or wakes a blocked Take directly. Additional wake sources: + // wake_loop_threadsafe() from background tasks, and the ms timeout. + if (ms == 0) [[unlikely]] { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} +} // namespace internal + +} // namespace esphome + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake/wake_generic.cpp b/esphome/core/wake/wake_generic.cpp new file mode 100644 index 00000000000..40044e43115 --- /dev/null +++ b/esphome/core/wake/wake_generic.cpp @@ -0,0 +1,17 @@ +#include "esphome/core/defines.h" + +#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \ + !defined(USE_HOST) + +#include "esphome/core/wake.h" + +namespace esphome { + +// === Wake-requested flag storage === +// Fallback platforms (currently only Zephyr/NRF52) are ESPHOME_THREAD_SINGLE. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; + +} // namespace esphome + +#endif // fallback guard diff --git a/esphome/core/wake/wake_generic.h b/esphome/core/wake/wake_generic.h new file mode 100644 index 00000000000..85424b61387 --- /dev/null +++ b/esphome/core/wake/wake_generic.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \ + !defined(USE_HOST) + +#include "esphome/core/hal.h" + +namespace esphome { + +/// Zephyr is currently the only platform without a wake mechanism. +/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). +/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. +inline void wake_loop_threadsafe() {} + +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + +namespace internal { +inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { + yield(); + return; + } + delay(ms); +} +} // namespace internal + +} // namespace esphome + +#endif // fallback guard diff --git a/esphome/core/wake.cpp b/esphome/core/wake/wake_host.cpp similarity index 74% rename from esphome/core/wake.cpp rename to esphome/core/wake/wake_host.cpp index cac88ae91ef..9d2a650ca24 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake/wake_host.cpp @@ -1,12 +1,11 @@ -#include "esphome/core/wake.h" -#include "esphome/core/hal.h" -#include "esphome/core/log.h" - -#ifdef USE_ESP8266 -#include <coredecls.h> -#endif +#include "esphome/core/defines.h" #ifdef USE_HOST + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/wake.h" + #include <arpa/inet.h> #include <cerrno> #include <fcntl.h> @@ -15,88 +14,19 @@ #include <sys/socket.h> #include <unistd.h> #include <vector> -#endif namespace esphome { // === Wake-requested flag storage === -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +// Host is always ESPHOME_THREAD_MULTI_ATOMICS. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::atomic<uint8_t> g_wake_requested{0}; -#else -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -volatile uint8_t g_wake_requested = 0; -#endif - -// === ESP32 / LibreTiny — IRAM_ATTR entry points === -#if defined(USE_ESP32) || defined(USE_LIBRETINY) -void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { - // ISR-safe: set flag before notify so the wake is visible on the next gate - // check. wake_request_set() is just an aligned 8-bit store / atomic store - // and is safe from IRAM. - wake_request_set(); - esphome_main_task_notify_from_isr(px_higher_priority_task_woken); -} -void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } -#endif - -// === ESP8266 / RP2040 === -#if defined(USE_ESP8266) || defined(USE_RP2040) -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -volatile bool g_main_loop_woke = false; -#endif - -#ifdef USE_ESP8266 -void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); } -#endif - -// === RP2040 — wakeable_delay (needs file-scope state for alarm callback) === -#ifdef USE_RP2040 -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_delay_expired = false; - -static int64_t alarm_callback_(alarm_id_t id, void *user_data) { - (void) id; - (void) user_data; - s_delay_expired = true; - __sev(); - return 0; -} - -namespace internal { -void wakeable_delay(uint32_t ms) { - if (ms == 0) [[unlikely]] { - yield(); - return; - } - if (g_main_loop_woke) { - g_main_loop_woke = false; - return; - } - s_delay_expired = false; - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - while (!g_main_loop_woke && !s_delay_expired) { - __wfe(); - } - if (!s_delay_expired) - cancel_alarm(alarm); - g_main_loop_woke = false; -} -} // namespace internal -#endif // USE_RP2040 - -// === Host (UDP loopback socket + select() based fd watcher) === -#ifdef USE_HOST static const char *const TAG = "wake"; namespace internal { // File-scope state — referenced inline by wake_drain_notifications() and -// wake_fd_ready() in wake.h, and by the bodies in this file. +// wake_fd_ready() in wake_host.h, and by the bodies in this file. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) int g_wake_socket_fd = -1; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) @@ -271,6 +201,7 @@ void wake_setup() { return; } } -#endif // USE_HOST } // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/wake/wake_host.h b/esphome/core/wake/wake_host.h new file mode 100644 index 00000000000..9756ed4c39b --- /dev/null +++ b/esphome/core/wake/wake_host.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_HOST + +#include "esphome/core/hal.h" + +#include <sys/select.h> +#include <sys/socket.h> + +namespace esphome { + +/// Host: wakes select() via UDP loopback socket. Defined in wake_host.cpp. +void wake_loop_threadsafe(); + +/// Register a socket file descriptor with the host select() loop. Not +/// thread-safe — main loop only. Returns false if fd is invalid or +/// >= FD_SETSIZE. +bool wake_register_fd(int fd); + +/// Unregister a socket file descriptor. Not thread-safe — main loop only. +void wake_unregister_fd(int fd); + +/// One-time setup of the loopback wake socket. Called from Application::setup(). +void wake_setup(); + +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + +namespace internal { +/// Host wakeable_delay uses select() over the registered fds — defined in wake_host.cpp. +void wakeable_delay(uint32_t ms); + +// File-scope state owned by wake_host.cpp. Accessed inline by +// wake_drain_notifications() and wake_fd_ready() so the hot path stays in the header. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern int g_wake_socket_fd; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern fd_set g_read_fds; +} // namespace internal + +inline bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd) { return FD_ISSET(fd, &internal::g_read_fds); } + +// Small buffer for draining wake notification bytes (1 byte sent per wake). +// Sized to drain multiple notifications per recvfrom() without wasting stack. +inline constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void ESPHOME_ALWAYS_INLINE wake_drain_notifications() { + // Called from main loop to drain any pending wake notifications. + // Must check wake_fd_ready() to avoid blocking on empty socket. + if (internal::g_wake_socket_fd >= 0 && wake_fd_ready(internal::g_wake_socket_fd)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads. Multiple wake events + // may have triggered multiple writes, so drain until EWOULDBLOCK. We control + // both ends of this loopback socket (always 1 byte per wake), so no error + // checking — any error indicates catastrophic system failure. + while (::recvfrom(internal::g_wake_socket_fd, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + } + } +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/wake/wake_rp2040.cpp b/esphome/core/wake/wake_rp2040.cpp new file mode 100644 index 00000000000..b18248dbd27 --- /dev/null +++ b/esphome/core/wake/wake_rp2040.cpp @@ -0,0 +1,58 @@ +#include "esphome/core/defines.h" + +#ifdef USE_RP2040 + +#include "esphome/core/hal.h" +#include "esphome/core/wake.h" + +#include <hardware/sync.h> +#include <pico/time.h> + +namespace esphome { + +// === Wake-requested flag + main-loop woke flag storage === +// RP2040 is always ESPHOME_THREAD_SINGLE. +// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; +volatile bool g_main_loop_woke = false; +// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback_(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + __sev(); + return 0; +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { + yield(); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} +} // namespace internal + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/core/wake/wake_rp2040.h b/esphome/core/wake/wake_rp2040.h new file mode 100644 index 00000000000..ea1242f535c --- /dev/null +++ b/esphome/core/wake/wake_rp2040.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_RP2040 + +#include "esphome/core/hal.h" + +#include <hardware/sync.h> +#include <pico/time.h> + +namespace esphome { + +inline void wake_loop_any_context() { + // Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed + // to see it on its next gate check. + wake_request_set(); + g_main_loop_woke = true; + __sev(); +} + +inline void wake_loop_threadsafe() { wake_loop_any_context(); } + +/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake_rp2040.cpp. +namespace internal { +void wakeable_delay(uint32_t ms); +} // namespace internal + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/loader.py b/esphome/loader.py index 68664aaa265..9390b8094bb 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -31,8 +31,9 @@ class FileResource: class ComponentManifest: - def __init__(self, module: ModuleType): + def __init__(self, module: ModuleType, recursive_sources: bool = False): self.module = module + self.recursive_sources = recursive_sources @property def package(self) -> str: @@ -108,8 +109,10 @@ class ComponentManifest: def resources(self) -> list[FileResource]: """Return a list of all file resources defined in the package of this component. - This will return all cpp source files that are located in the same folder as the - loaded .py file (does not look through subdirectories) + By default only files directly in the package directory are returned. Manifests + constructed with ``recursive_sources=True`` also descend into non-subpackage + subdirectories (subdirectories without an ``__init__.py``), so core code can + live under ``esphome/core/<group>/`` without every component paying the cost. """ ret: list[FileResource] = [] @@ -121,23 +124,30 @@ class ComponentManifest: set(filter_source_files_func()) if filter_source_files_func else set() ) - # Process all resources - for resource in ( - r.name - for r in importlib.resources.files(self.package).iterdir() - if r.is_file() - ): - if Path(resource).suffix not in SOURCE_FILE_EXTENSIONS: - continue - if not importlib.resources.files(self.package).joinpath(resource).is_file(): - # Not a resource = this is a directory (yeah this is confusing) - continue + root = importlib.resources.files(self.package) - # Skip excluded files - if resource in excluded_files: - continue + for child in root.iterdir(): + name = child.name + if child.is_file(): + if Path(name).suffix not in SOURCE_FILE_EXTENSIONS: + continue + if name in excluded_files: + continue + ret.append(FileResource(self.package, name)) + elif self.recursive_sources and child.is_dir() and name != "__pycache__": + # Skip Python subpackages — they load as their own components. + if child.joinpath("__init__.py").is_file(): + continue + for sub in child.iterdir(): + if not sub.is_file(): + continue + if Path(sub.name).suffix not in SOURCE_FILE_EXTENSIONS: + continue + resource = f"{name}/{sub.name}" + if resource in excluded_files: + continue + ret.append(FileResource(self.package, resource)) - ret.append(FileResource(self.package, resource)) return ret @@ -237,7 +247,9 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None: _COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() -_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) +_COMPONENT_CACHE["esphome"] = ComponentManifest( + esphome.core.config, recursive_sources=True +) def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None: diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py index a42cc5cca73..3fb0eca4a06 100644 --- a/tests/unit_tests/test_loader.py +++ b/tests/unit_tests/test_loader.py @@ -158,3 +158,167 @@ def test_component_manifest_resources_with_filter_source_files() -> None: # Verify the correct number of resources assert len(resources) == 3 # test.cpp, test.h, common.cpp + + +# --------------------------------------------------------------------------- +# recursive_sources — used only by the core "esphome" manifest so that files +# in esphome/core/<subdir>/*.cpp (e.g. esphome/core/wake/wake_host.cpp) are +# discovered without promoting <subdir>/ to a Python subpackage. +# --------------------------------------------------------------------------- + + +def _mock_file(filename: str) -> MagicMock: + m = MagicMock() + m.name = filename + m.is_file.return_value = True + m.is_dir.return_value = False + return m + + +def _mock_dir(dirname: str, children: list, has_init: bool = False) -> MagicMock: + """Mock a directory entry with an iterdir() and joinpath('__init__.py').""" + d = MagicMock() + d.name = dirname + d.is_file.return_value = False + d.is_dir.return_value = True + d.iterdir.return_value = children + init_marker = MagicMock() + init_marker.is_file.return_value = has_init + d.joinpath.return_value = init_marker + return d + + +def test_component_manifest_resources_non_recursive_skips_subdirs() -> None: + """Default (recursive_sources=False) does not descend into subdirectories.""" + mock_module = MagicMock() + mock_module.__package__ = "esphome.components.test_component" + # No FILTER_SOURCE_FILES. + del mock_module.FILTER_SOURCE_FILES + + manifest = ComponentManifest(mock_module) # recursive_sources defaults to False + + top_level = [ + _mock_file("top.cpp"), + _mock_dir("subdir", [_mock_file("nested.cpp")]), + ] + with patch("importlib.resources.files") as mock_files_func: + pkg = MagicMock() + pkg.iterdir.return_value = top_level + mock_files_func.return_value = pkg + + names = [r.resource for r in manifest.resources] + + assert names == ["top.cpp"] + + +def test_component_manifest_resources_recursive_walks_non_subpackage_subdirs() -> None: + """With recursive_sources=True, a subdir without __init__.py is walked.""" + mock_module = MagicMock() + mock_module.__package__ = "esphome.core" + del mock_module.FILTER_SOURCE_FILES + + manifest = ComponentManifest(mock_module, recursive_sources=True) + + wake_dir = _mock_dir( + "wake", + [ + _mock_file("wake_host.cpp"), + _mock_file("wake_host.h"), + _mock_file("README.md"), # wrong suffix, excluded + ], + has_init=False, + ) + top_level = [ + _mock_file("wake.h"), + wake_dir, + ] + with patch("importlib.resources.files") as mock_files_func: + pkg = MagicMock() + pkg.iterdir.return_value = top_level + mock_files_func.return_value = pkg + + names = sorted(r.resource for r in manifest.resources) + + assert names == ["wake.h", "wake/wake_host.cpp", "wake/wake_host.h"] + + +def test_component_manifest_resources_recursive_skips_subpackages() -> None: + """Subdirectories that ARE Python subpackages (contain __init__.py) are + skipped even with recursive_sources=True — those load as their own + ComponentManifest and would otherwise be double-counted.""" + mock_module = MagicMock() + mock_module.__package__ = "esphome.components.haier" + del mock_module.FILTER_SOURCE_FILES + + manifest = ComponentManifest(mock_module, recursive_sources=True) + + button_pkg = _mock_dir( + "button", + [_mock_file("self_cleaning.cpp")], + has_init=True, # Python subpackage — must be skipped. + ) + top_level = [ + _mock_file("haier.cpp"), + button_pkg, + ] + with patch("importlib.resources.files") as mock_files_func: + pkg = MagicMock() + pkg.iterdir.return_value = top_level + mock_files_func.return_value = pkg + + names = [r.resource for r in manifest.resources] + + assert names == ["haier.cpp"] + + +def test_component_manifest_resources_recursive_skips_pycache() -> None: + """__pycache__ inside a recursive walk must never be descended into.""" + mock_module = MagicMock() + mock_module.__package__ = "esphome.core" + del mock_module.FILTER_SOURCE_FILES + + manifest = ComponentManifest(mock_module, recursive_sources=True) + + # __pycache__ is_dir=True but must be skipped without checking __init__.py + # or calling iterdir (would yield compiled artifacts). + pycache = _mock_dir("__pycache__", [_mock_file("wake.cpython-314.pyc")]) + top_level = [ + _mock_file("wake.h"), + pycache, + ] + with patch("importlib.resources.files") as mock_files_func: + pkg = MagicMock() + pkg.iterdir.return_value = top_level + mock_files_func.return_value = pkg + + names = [r.resource for r in manifest.resources] + + assert names == ["wake.h"] + + +def test_component_manifest_resources_recursive_filter_source_files_supports_subpaths() -> ( + None +): + """FILTER_SOURCE_FILES entries using '/'-joined subpaths exclude files + inside a recursively-walked subdir.""" + mock_module = MagicMock() + mock_module.__package__ = "esphome.core" + mock_module.FILTER_SOURCE_FILES = lambda: ["wake/wake_host.cpp"] + + manifest = ComponentManifest(mock_module, recursive_sources=True) + + wake_dir = _mock_dir( + "wake", + [ + _mock_file("wake_host.cpp"), # excluded + _mock_file("wake_freertos.cpp"), # kept + ], + ) + with patch("importlib.resources.files") as mock_files_func: + pkg = MagicMock() + pkg.iterdir.return_value = [wake_dir] + mock_files_func.return_value = pkg + + names = [r.resource for r in manifest.resources] + + assert names == ["wake/wake_freertos.cpp"] From 0a4d9b430f1b6c98522fce0d1eb23edd3eeaf857 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 09:05:12 -0500 Subject: [PATCH 1697/2056] [ci] Add import-time regression check for esphome.__main__ (#15954) --- .github/workflows/ci.yml | 30 +++ requirements_test.txt | 3 + script/check_import_time.py | 241 +++++++++++++++++++++++++ script/determine-jobs.py | 38 ++++ script/import_time_budget.json | 5 + tests/script/test_check_import_time.py | 191 ++++++++++++++++++++ tests/script/test_determine_jobs.py | 61 +++++++ 7 files changed, 569 insertions(+) create mode 100755 script/check_import_time.py create mode 100644 script/import_time_budget.json create mode 100644 tests/script/test_check_import_time.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20c349ac003..d60bd6edc31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,34 @@ jobs: script/generate-esp32-boards.py --check script/generate-rp2040-boards.py --check + import-time: + name: Check import esphome.__main__ time + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.import-time == 'true' + steps: + - name: Check out code from GitHub + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Check import time against budget and write waterfall HAR + run: | + . venv/bin/activate + script/check_import_time.py --check --har importtime.har + - name: Upload waterfall HAR + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: import-time-waterfall + path: importtime.har + if-no-files-found: ignore + retention-days: 14 + pytest: name: Run pytest strategy: @@ -176,6 +204,7 @@ jobs: clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} + import-time: ${{ steps.determine.outputs.import-time }} changed-components: ${{ steps.determine.outputs.changed-components }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} @@ -219,6 +248,7 @@ jobs: echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT + echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT diff --git a/requirements_test.txt b/requirements_test.txt index b35025fa04a..568d79d6764 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,3 +12,6 @@ pytest-asyncio==1.3.0 pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 + +# Used by the import-time regression check (.github/workflows/ci.yml → import-time job) +importtime-waterfall==1.0.0 diff --git a/script/check_import_time.py b/script/check_import_time.py new file mode 100755 index 00000000000..0d5362c9681 --- /dev/null +++ b/script/check_import_time.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Regression check for `import esphome.__main__` cost. + +Runs `python -m importtime_waterfall --har esphome.__main__` (which invokes +`-X importtime` in fresh subprocesses, best-of-N) and compares the root +cumulative import time against a checked-in budget +(`script/import_time_budget.json`). + +The CLI pays this cost on every invocation before the requested command even +runs, so a regression here hurts every user. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import subprocess +import sys +from typing import Any, TextIO + +SCRIPT_DIR = Path(__file__).parent +BUDGET_PATH = SCRIPT_DIR / "import_time_budget.json" + +TARGET_MODULE = "esphome.__main__" +DEFAULT_MARGIN_PCT = 15 +OFFENDERS_TOP_N = 15 + + +def run_waterfall(module: str) -> str: + """Run `importtime_waterfall --har <module>` and return the HAR JSON text. + + `importtime_waterfall` itself runs the target in 6 fresh subprocesses + under `-X importtime` and emits the HAR of the fastest run. + """ + result = subprocess.run( + [sys.executable, "-m", "importtime_waterfall", "--har", module], + check=True, + stdout=subprocess.PIPE, + text=True, + ) + return result.stdout + + +def measure(module: str, har_path: Path | None = None) -> dict[str, Any]: + """Return the parsed HAR for importing `module`. + + When `har_path` is given, also write the raw HAR JSON to that path so + callers can combine `--check` with `--har` without measuring twice. + """ + har_text = run_waterfall(module) + if har_path is not None: + har_path.write_text(har_text) + return json.loads(har_text) + + +def _entries(har: dict[str, Any]) -> list[dict[str, Any]]: + return har["log"]["entries"] + + +def root_cumulative_us(har: dict[str, Any], module: str) -> int: + """Return the cumulative import time (µs) of `module` from a HAR. + + The HAR `time` field is authored by importtime_waterfall using µs values + fed through `timedelta(milliseconds=...)`, so the number read back is the + original self/cumulative time in microseconds (labelled "ms" in HAR). + """ + for entry in _entries(har): + if entry["request"]["url"] == module: + return entry["time"] + raise RuntimeError( + f"No HAR entry for {module!r}. Is it importable with " + f"`python -c 'import {module}'`?" + ) + + +def top_offenders(har: dict[str, Any], n: int) -> list[tuple[str, int, int]]: + """Return up to `n` (name, self_us, cumulative_us), ranked by self_us desc. + + A module imported from multiple places is counted once (first entry wins, + matching importtime's own de-duplication). + """ + seen: dict[str, tuple[int, int]] = {} + for entry in _entries(har): + name = entry["request"]["url"] + if name in seen: + continue + self_us = entry["timings"]["receive"] + cumulative_us = entry["time"] + seen[name] = (self_us, cumulative_us) + ranked = sorted( + ((name, s, c) for name, (s, c) in seen.items()), + key=lambda row: row[1], + reverse=True, + ) + return ranked[:n] + + +def read_budget() -> dict[str, Any]: + if not BUDGET_PATH.exists(): + return {} + with BUDGET_PATH.open() as f: + return json.load(f) + + +def write_budget(cumulative_us: int, margin_pct: int) -> None: + payload = { + "target_module": TARGET_MODULE, + "margin_pct": margin_pct, + "cumulative_us": cumulative_us, + } + with BUDGET_PATH.open("w") as f: + json.dump(payload, f, indent=2) + f.write("\n") + + +def _format_us(us: int) -> str: + if us >= 1000: + return f"{us / 1000:.1f}ms" + return f"{us}us" + + +def _print_offenders_table( + offenders: list[tuple[str, int, int]], stream: TextIO +) -> None: + name_w = max(len(name) for name, _, _ in offenders) + print(f"\n{'module':<{name_w}} {'self':>10} {'cumulative':>12}", file=stream) + print(f"{'-' * name_w} {'-' * 10} {'-' * 12}", file=stream) + for name, self_us, cum_us in offenders: + print( + f"{name:<{name_w}} {_format_us(self_us):>10} {_format_us(cum_us):>12}", + file=stream, + ) + + +def cmd_check(args: argparse.Namespace) -> int: + budget = read_budget() + if not budget: + print( + f"ERROR: {BUDGET_PATH.name} missing. Run with --update first.", + file=sys.stderr, + ) + return 2 + + har = measure(TARGET_MODULE, har_path=Path(args.har) if args.har else None) + measured = root_cumulative_us(har, TARGET_MODULE) + + baseline = budget["cumulative_us"] + margin_pct = budget.get("margin_pct", DEFAULT_MARGIN_PCT) + ceiling = int(baseline * (1 + margin_pct / 100)) + + summary = ( + f"measured {TARGET_MODULE}: {_format_us(measured)} " + f"(budget {_format_us(baseline)} + {margin_pct}% = {_format_us(ceiling)})" + ) + passed = measured <= ceiling + stream = sys.stdout if passed else sys.stderr + + if passed: + print(summary) + else: + print( + f"REGRESSION: `import {TARGET_MODULE}` took {_format_us(measured)}, " + f"exceeding the budget of {_format_us(baseline)} + {margin_pct}% " + f"({_format_us(ceiling)}).", + file=stream, + ) + + print("\nTop import-time offenders (by self time):", file=stream) + _print_offenders_table(top_offenders(har, OFFENDERS_TOP_N), stream) + + if not passed: + print( + "\nIf this regression is intentional, regenerate the budget with:\n" + " script/check_import_time.py --update\n" + "Otherwise, consider making the new import lazy " + "(import inside the function that uses it).", + file=stream, + ) + return 1 + return 0 + + +def cmd_update(args: argparse.Namespace) -> int: + har = measure(TARGET_MODULE, har_path=Path(args.har) if args.har else None) + measured = root_cumulative_us(har, TARGET_MODULE) + write_budget(measured, args.margin_pct) + print( + f"Wrote {BUDGET_PATH.name}: " + f"{TARGET_MODULE}={_format_us(measured)} " + f"(margin {args.margin_pct}%)" + ) + return 0 + + +def cmd_har_only(args: argparse.Namespace) -> int: + Path(args.har).write_text(run_waterfall(TARGET_MODULE)) + print(f"Wrote waterfall HAR to {args.har}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--margin-pct", + type=int, + default=DEFAULT_MARGIN_PCT, + help=(f"Margin over baseline for --update (default: {DEFAULT_MARGIN_PCT}%%)."), + ) + parser.add_argument( + "--har", + metavar="PATH", + help=( + "Write a waterfall HAR file at PATH. Can be combined with " + "--check or --update to reuse that run's measurement (avoids " + "measuring twice)." + ), + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--check", action="store_true", help="Fail if measured time exceeds budget." + ) + mode.add_argument( + "--update", + action="store_true", + help="Rewrite the budget from a fresh measurement.", + ) + args = parser.parse_args() + + if args.check: + return cmd_check(args) + if args.update: + return cmd_update(args) + if args.har: + return cmd_har_only(args) + parser.error("Specify at least one of --check, --update, or --har PATH.") + return 2 # unreachable; parser.error exits. Here to satisfy ruff RET503. + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index f036447542d..6fd7ab297c8 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -349,6 +349,42 @@ def should_run_python_linters(branch: str | None = None) -> bool: return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS) +# Files outside esphome/**/*.py whose changes can affect `import esphome.__main__` +# cost. requirements.txt / pyproject.toml change the dependency graph pulled in +# by top-level imports; check_import_time.py itself changes the check's behavior. +IMPORT_TIME_TRIGGER_FILES = frozenset( + { + "requirements.txt", + "requirements_dev.txt", + "requirements_test.txt", + "pyproject.toml", + "script/check_import_time.py", + "script/import_time_budget.json", + } +) + + +def should_run_import_time(branch: str | None = None) -> bool: + """Determine if the `import esphome.__main__` time regression check should run. + + Runs when any Python file under `esphome/` changes (those modules are + loaded transitively from `esphome.__main__`), when dependency + declarations change, or when the check script/budget itself changes. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if the import-time check should run, False otherwise. + """ + for file in changed_files(branch): + if file.startswith("esphome/") and file.endswith(PYTHON_FILE_EXTENSIONS): + return True + if file in IMPORT_TIME_TRIGGER_FILES: + return True + return False + + def determine_cpp_unit_tests( branch: str | None = None, ) -> tuple[bool, list[str]]: @@ -780,6 +816,7 @@ def main() -> None: run_clang_tidy = should_run_clang_tidy(args.branch) run_clang_format = should_run_clang_format(args.branch) run_python_linters = should_run_python_linters(args.branch) + run_import_time = should_run_import_time(args.branch) changed_cpp_file_count = count_changed_cpp_files(args.branch) # Get changed components @@ -913,6 +950,7 @@ def main() -> None: "clang_tidy_mode": clang_tidy_mode, "clang_format": run_clang_format, "python_linters": run_python_linters, + "import_time": run_import_time, "changed_components": changed_components, "changed_components_with_tests": changed_components_with_tests, "directly_changed_components_with_tests": list(directly_changed_with_tests), diff --git a/script/import_time_budget.json b/script/import_time_budget.json new file mode 100644 index 00000000000..1e656dc9776 --- /dev/null +++ b/script/import_time_budget.json @@ -0,0 +1,5 @@ +{ + "target_module": "esphome.__main__", + "margin_pct": 15, + "cumulative_us": 123000 +} diff --git a/tests/script/test_check_import_time.py b/tests/script/test_check_import_time.py new file mode 100644 index 00000000000..223c58002c9 --- /dev/null +++ b/tests/script/test_check_import_time.py @@ -0,0 +1,191 @@ +"""Unit tests for script/check_import_time.py.""" + +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import sys +from unittest.mock import patch + +import pytest + +# Load the script-under-test as `check_import_time` (it's a hyphenated path +# inside `script/` that mirrors the existing `determine_jobs` pattern). +script_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "script") +) +sys.path.insert(0, script_dir) +spec = importlib.util.spec_from_file_location( + "check_import_time", os.path.join(script_dir, "check_import_time.py") +) +check_import_time = importlib.util.module_from_spec(spec) +spec.loader.exec_module(check_import_time) + + +def _entry(name: str, self_us: int, cumulative_us: int) -> dict: + """Build a minimal HAR entry matching `importtime_waterfall --har`.""" + return { + "request": {"url": name}, + "time": cumulative_us, + "timings": {"receive": self_us, "wait": cumulative_us - self_us}, + } + + +def _har(*entries: dict) -> dict: + return {"log": {"entries": list(entries)}} + + +def test_root_cumulative_us_returns_time_for_root_module() -> None: + har = _har( + _entry("dep_a", 500, 500), + _entry("dep_b", 300, 300), + _entry("esphome.__main__", 100, 1000), + ) + assert check_import_time.root_cumulative_us(har, "esphome.__main__") == 1000 + + +def test_root_cumulative_us_missing_module_raises() -> None: + har = _har(_entry("something.else", 100, 100)) + with pytest.raises(RuntimeError, match="No HAR entry for 'esphome.__main__'"): + check_import_time.root_cumulative_us(har, "esphome.__main__") + + +def test_top_offenders_ranks_by_self_time_descending() -> None: + har = _har( + _entry("small", 100, 100), + _entry("big", 5000, 5000), + _entry("medium", 2000, 2500), + ) + result = check_import_time.top_offenders(har, n=10) + assert [name for name, _, _ in result] == ["big", "medium", "small"] + assert result[0] == ("big", 5000, 5000) + + +def test_top_offenders_respects_n_limit() -> None: + har = _har(*[_entry(f"m{i}", i * 100, i * 100) for i in range(1, 20)]) + assert len(check_import_time.top_offenders(har, n=5)) == 5 + + +def test_top_offenders_dedupes_repeat_names_keeping_first() -> None: + har = _har( + _entry("pkg", 5000, 5000), + _entry("pkg", 100, 100), # reimport later in trace + _entry("other", 1000, 1000), + ) + result = check_import_time.top_offenders(har, n=10) + assert [name for name, _, _ in result] == ["pkg", "other"] + # First occurrence wins + assert ("pkg", 5000, 5000) in result + + +def test_format_us_switches_to_ms_at_threshold() -> None: + assert check_import_time._format_us(500) == "500us" + assert check_import_time._format_us(999) == "999us" + assert check_import_time._format_us(1000) == "1.0ms" + assert check_import_time._format_us(12345) == "12.3ms" + + +def test_read_write_budget_roundtrip(tmp_path: Path) -> None: + budget_path = tmp_path / "budget.json" + with patch.object(check_import_time, "BUDGET_PATH", budget_path): + assert check_import_time.read_budget() == {} + check_import_time.write_budget(cumulative_us=12345, margin_pct=20) + loaded = check_import_time.read_budget() + assert loaded["cumulative_us"] == 12345 + assert loaded["margin_pct"] == 20 + assert loaded["target_module"] == check_import_time.TARGET_MODULE + + +def test_cmd_check_passes_when_measured_within_ceiling( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + budget_path = tmp_path / "budget.json" + budget_path.write_text( + json.dumps( + { + "target_module": check_import_time.TARGET_MODULE, + "margin_pct": 15, + "cumulative_us": 100000, # 100ms + } + ) + ) + # Measured 90ms: inside 100ms + 15% = 115ms ceiling + har = _har(_entry(check_import_time.TARGET_MODULE, 1000, 90000)) + args = type("A", (), {"har": None})() + with ( + patch.object(check_import_time, "BUDGET_PATH", budget_path), + patch.object(check_import_time, "measure", return_value=har), + ): + rc = check_import_time.cmd_check(args) + assert rc == 0 + out = capsys.readouterr().out + assert "measured esphome.__main__:" in out + assert "budget 100.0ms" in out + + +def test_cmd_check_fails_when_measured_exceeds_ceiling( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + budget_path = tmp_path / "budget.json" + budget_path.write_text( + json.dumps( + { + "target_module": check_import_time.TARGET_MODULE, + "margin_pct": 15, + "cumulative_us": 100000, + } + ) + ) + # Measured 120ms: over 100ms + 15% = 115ms ceiling + har = _har( + _entry("offender_a", 10000, 10000), + _entry(check_import_time.TARGET_MODULE, 1000, 120000), + ) + args = type("A", (), {"har": None})() + with ( + patch.object(check_import_time, "BUDGET_PATH", budget_path), + patch.object(check_import_time, "measure", return_value=har), + ): + rc = check_import_time.cmd_check(args) + assert rc == 1 + err = capsys.readouterr().err + assert "REGRESSION" in err + assert "120.0ms" in err + assert "offender_a" in err # top offender table + + +def test_cmd_check_returns_2_when_budget_missing( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + budget_path = tmp_path / "nonexistent.json" + args = type("A", (), {"har": None})() + with patch.object(check_import_time, "BUDGET_PATH", budget_path): + rc = check_import_time.cmd_check(args) + assert rc == 2 + assert "missing" in capsys.readouterr().err + + +def test_cmd_check_writes_har_when_path_given(tmp_path: Path) -> None: + budget_path = tmp_path / "budget.json" + budget_path.write_text( + json.dumps( + { + "target_module": check_import_time.TARGET_MODULE, + "margin_pct": 15, + "cumulative_us": 100000, + } + ) + ) + har_path = tmp_path / "out.har" + har_text = json.dumps(_har(_entry(check_import_time.TARGET_MODULE, 1000, 80000))) + args = type("A", (), {"har": str(har_path)})() + with ( + patch.object(check_import_time, "BUDGET_PATH", budget_path), + patch.object(check_import_time, "run_waterfall", return_value=har_text), + ): + rc = check_import_time.cmd_check(args) + assert rc == 0 + assert har_path.exists() + assert json.loads(har_path.read_text()) == json.loads(har_text) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 2c726734fe7..44c110b6892 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -56,6 +56,13 @@ def mock_should_run_python_linters() -> Generator[Mock, None, None]: yield mock +@pytest.fixture +def mock_should_run_import_time() -> Generator[Mock, None, None]: + """Mock should_run_import_time from determine_jobs.""" + with patch.object(determine_jobs, "should_run_import_time") as mock: + yield mock + + @pytest.fixture def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]: """Mock determine_cpp_unit_tests from helpers.""" @@ -91,6 +98,7 @@ def test_main_all_tests_should_run( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -104,6 +112,7 @@ def test_main_all_tests_should_run( mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = True mock_should_run_python_linters.return_value = True + mock_should_run_import_time.return_value = True mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -158,6 +167,7 @@ def test_main_all_tests_should_run( assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is True assert output["python_linters"] is True + assert output["import_time"] is True assert output["changed_components"] == ["wifi", "api", "sensor"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -189,6 +199,7 @@ def test_main_no_tests_should_run( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -202,6 +213,7 @@ def test_main_no_tests_should_run( mock_should_run_clang_tidy.return_value = False mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False + mock_should_run_import_time.return_value = False mock_determine_cpp_unit_tests.return_value = (False, []) # Mock changed_files to return no component files @@ -241,6 +253,7 @@ def test_main_no_tests_should_run( assert output["clang_tidy_mode"] == "disabled" assert output["clang_format"] is False assert output["python_linters"] is False + assert output["import_time"] is False assert output["changed_components"] == [] assert output["changed_components_with_tests"] == [] assert output["component_test_count"] == 0 @@ -261,6 +274,7 @@ def test_main_with_branch_argument( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -274,6 +288,7 @@ def test_main_with_branch_argument( mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = True + mock_should_run_import_time.return_value = True mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -310,6 +325,7 @@ def test_main_with_branch_argument( mock_should_run_clang_tidy.assert_called_once_with("main") mock_should_run_clang_format.assert_called_once_with("main") mock_should_run_python_linters.assert_called_once_with("main") + mock_should_run_import_time.assert_called_once_with("main") # Check output captured = capsys.readouterr() @@ -322,6 +338,7 @@ def test_main_with_branch_argument( assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is False assert output["python_linters"] is True + assert output["import_time"] is True assert output["changed_components"] == ["mqtt"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -597,6 +614,50 @@ def test_should_run_python_linters_with_branch() -> None: mock_changed.assert_called_once_with("release") +@pytest.mark.parametrize( + ("changed_files", "expected_result"), + [ + # esphome Python files trigger the check + (["esphome/__main__.py"], True), + (["esphome/components/wifi/__init__.py"], True), + (["esphome/core/config.py"], True), + (["esphome/types.pyi"], True), + # Dependency declarations and the check's own files trigger + (["requirements.txt"], True), + (["requirements_dev.txt"], True), + (["requirements_test.txt"], True), + (["pyproject.toml"], True), + (["script/check_import_time.py"], True), + (["script/import_time_budget.json"], True), + # Mixed: any triggering file is enough + (["docs/README.md", "esphome/config.py"], True), + # Python files outside esphome/ don't trigger + (["script/some_other_script.py"], False), + (["tests/script/test_determine_jobs.py"], False), + # Non-Python changes don't trigger + (["esphome/core/component.cpp"], False), + (["tests/components/wifi/test.esp32-idf.yaml"], False), + (["README.md"], False), + ([], False), + ], +) +def test_should_run_import_time( + changed_files: list[str], expected_result: bool +) -> None: + """Test should_run_import_time function.""" + with patch.object(determine_jobs, "changed_files", return_value=changed_files): + result = determine_jobs.should_run_import_time() + assert result == expected_result + + +def test_should_run_import_time_with_branch() -> None: + """Test should_run_import_time with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.should_run_import_time("release") + mock_changed.assert_called_once_with("release") + + @pytest.mark.parametrize( ("changed_files", "expected_result"), [ From 52e8c50f45a035362d62a1d47e967240ec6b65f6 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston <bonne@exciton.com.au> Date: Tue, 28 Apr 2026 08:21:25 -0700 Subject: [PATCH 1698/2056] [modbus] Split modbus_server from modbus_controller (#15509) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- CODEOWNERS | 1 + .../components/modbus_controller/__init__.py | 91 +-------- esphome/components/modbus_controller/const.py | 1 + .../modbus_controller/modbus_controller.cpp | 176 +--------------- .../modbus_controller/modbus_controller.h | 93 --------- esphome/components/modbus_server/__init__.py | 124 +++++++++++ esphome/components/modbus_server/const.py | 7 + .../modbus_server/modbus_server.cpp | 192 ++++++++++++++++++ .../components/modbus_server/modbus_server.h | 119 +++++++++++ .../components/modbus_controller/common.yaml | 44 +--- tests/components/modbus_server/common.yaml | 41 ++++ .../modbus_server/test.esp32-idf.yaml | 4 + .../modbus_server/test.esp8266-ard.yaml | 4 + .../modbus_server/test.rp2040-ard.yaml | 4 + .../fixtures/uart_mock_modbus_server.yaml | 4 +- .../uart_mock_modbus_server_controller.yaml | 5 +- ...ock_modbus_server_controller_multiple.yaml | 9 +- ...t_mock_modbus_server_controller_write.yaml | 5 +- 18 files changed, 523 insertions(+), 401 deletions(-) create mode 100644 esphome/components/modbus_server/__init__.py create mode 100644 esphome/components/modbus_server/const.py create mode 100644 esphome/components/modbus_server/modbus_server.cpp create mode 100644 esphome/components/modbus_server/modbus_server.h create mode 100644 tests/components/modbus_server/common.yaml create mode 100644 tests/components/modbus_server/test.esp32-idf.yaml create mode 100644 tests/components/modbus_server/test.esp8266-ard.yaml create mode 100644 tests/components/modbus_server/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 20c19a7dfa0..471def542bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -347,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras +esphome/components/modbus_server/* @exciton esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan esphome/components/mopeka_pro_check/* @spbrogan esphome/components/mopeka_std_check/* @Fabian-Schmidt diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 2af58a96beb..67e5757397c 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -3,11 +3,8 @@ import binascii from esphome import automation import esphome.codegen as cg from esphome.components import modbus -from esphome.components.const import CONF_ENABLED from esphome.components.modbus.helpers import ( - CPP_TYPE_REGISTER_MAP, MODBUS_REGISTER_TYPE, - SENSOR_VALUE_TYPE, TYPE_REGISTER_MAP, ModbusRegisterType, ) @@ -29,11 +26,10 @@ from .const import ( CONF_ON_OFFLINE, CONF_ON_ONLINE, CONF_REGISTER_COUNT, - CONF_REGISTER_LAST_ADDRESS, CONF_REGISTER_TYPE, - CONF_REGISTER_VALUE, CONF_RESPONSE_SIZE, CONF_SERVER_COURTESY_RESPONSE, + CONF_SERVER_REGISTERS, CONF_SKIP_UPDATES, CONF_VALUE_TYPE, ) @@ -42,9 +38,6 @@ CODEOWNERS = ["@martgras"] AUTO_LOAD = ["modbus"] -CONF_READ_LAMBDA = "read_lambda" -CONF_WRITE_LAMBDA = "write_lambda" -CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") @@ -53,30 +46,9 @@ ModbusController = modbus_controller_ns.class_( ) SensorItem = modbus_controller_ns.struct("SensorItem") -ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") -ServerRegister = modbus_controller_ns.struct("ServerRegister") _LOGGER = logging.getLogger(__name__) -SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( - { - cv.Optional(CONF_ENABLED, default=False): cv.boolean, - cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t, - cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t, - } -) - -ModbusServerRegisterSchema = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ServerRegister), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - } -) - - CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -85,12 +57,16 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA, + cv.Optional(CONF_SERVER_COURTESY_RESPONSE): cv.invalid( + "This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/" + ), cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, cv.Optional( CONF_SERVER_REGISTERS, - ): cv.ensure_list(ModbusServerRegisterSchema), + ): cv.invalid( + "This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/" + ), cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}), cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}), cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}), @@ -142,11 +118,9 @@ def validate_modbus_register(config): def _final_validate(config): - if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config: - return modbus.final_validate_modbus_device("modbus_controller", role="server")( - config - ) - return config + return modbus.final_validate_modbus_device("modbus_controller", role="client")( + config + ) FINAL_VALIDATE_SCHEMA = _final_validate @@ -228,53 +202,8 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) - if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE): - cg.add( - var.set_server_courtesy_response( - cg.StructInitializer( - ServerCourtesyResponse, - ("enabled", server_courtesy_response[CONF_ENABLED]), - ( - "register_last_address", - server_courtesy_response[CONF_REGISTER_LAST_ADDRESS], - ), - ("register_value", server_courtesy_response[CONF_REGISTER_VALUE]), - ) - ) - ) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) - if CONF_SERVER_REGISTERS in config: - for server_register in config[CONF_SERVER_REGISTERS]: - server_register_var = cg.new_Pvariable( - server_register[CONF_ID], - server_register[CONF_ADDRESS], - server_register[CONF_VALUE_TYPE], - TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], - ) - cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] - cg.add( - server_register_var.set_read_lambda( - cg.TemplateArguments(cpp_type), - await cg.process_lambda( - server_register[CONF_READ_LAMBDA], - [(cg.uint16, "address")], - return_type=cpp_type, - ), - ) - ) - if CONF_WRITE_LAMBDA in server_register: - cg.add( - server_register_var.set_write_lambda( - cg.TemplateArguments(cpp_type), - await cg.process_lambda( - server_register[CONF_WRITE_LAMBDA], - parameters=[(cg.uint16, "address"), (cpp_type, "x")], - return_type=cg.bool_, - ), - ) - ) - cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index c689d845765..0149a3cc499 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -18,6 +18,7 @@ CONF_REGISTER_TYPE = "register_type" CONF_REGISTER_VALUE = "register_value" CONF_RESPONSE_SIZE = "response_size" CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response" +CONF_SERVER_REGISTERS = "server_registers" CONF_SKIP_UPDATES = "skip_updates" CONF_USE_WRITE_MULTIPLE = "use_write_multiple" CONF_VALUE_TYPE = "value_type" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 5c3b39c9548..dabed7136b9 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -112,167 +112,6 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } -void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, - uint16_t number_of_registers) { - ESP_LOGD(TAG, - "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " - "0x%X.", - this->address_, function_code, start_address, number_of_registers); - - if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) { - ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); - return; - } - - std::vector<uint16_t> sixteen_bit_response; - for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { - bool found = false; - for (auto *server_register : this->server_registers_) { - if (server_register->address == current_address) { - if (!server_register->read_lambda) { - break; - } - int64_t value = server_register->read_lambda(); - ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", - server_register->address, static_cast<size_t>(server_register->value_type), - server_register->register_count, server_register->format_value(value).c_str()); - - std::vector<uint16_t> payload; - payload.reserve(server_register->register_count * 2); - modbus::helpers::number_to_payload(payload, value, server_register->value_type); - sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); - current_address += server_register->register_count; - found = true; - break; - } - } - - if (!found) { - if (this->server_courtesy_response_.enabled && - (current_address <= this->server_courtesy_response_.register_last_address)) { - ESP_LOGD(TAG, - "Could not match any register to address 0x%02X, but default allowed. " - "Returning default value: %d.", - current_address, this->server_courtesy_response_.register_value); - sixteen_bit_response.push_back(this->server_courtesy_response_.register_value); - current_address += 1; // Just increment by 1, as the default response is a single register - } else { - ESP_LOGW(TAG, - "Could not match any register to address 0x%02X and default not allowed. Sending exception response.", - current_address); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); - return; - } - } - } - - std::vector<uint8_t> response; - for (auto v : sixteen_bit_response) { - auto decoded_value = decode_value(v); - response.push_back(decoded_value[0]); - response.push_back(decoded_value[1]); - } - - this->send(function_code, start_address, number_of_registers, response.size(), response.data()); -} - -void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) { - uint16_t number_of_registers; - uint16_t payload_offset; - - if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { - if (data.size() < 5) { - ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size()); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); - return; - } - number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); - if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { - ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); - return; - } - uint16_t payload_size = data[4]; - if (payload_size != number_of_registers * 2) { - ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", - payload_size, number_of_registers); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); - return; - } - if (data.size() < 5 + payload_size) { - ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(), - 5 + payload_size); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); - return; - } - payload_offset = 5; - } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { - if (data.size() < 4) { - ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size()); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); - return; - } - number_of_registers = 1; - payload_offset = 2; - } else { - ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); - return; - } - - uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8); - ESP_LOGD(TAG, - "Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " - "0x%X.", - this->address_, function_code, start_address, number_of_registers); - - auto for_each_register = [this, start_address, number_of_registers, payload_offset]( - const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool { - uint16_t offset = payload_offset; - for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { - bool ok = false; - for (auto *server_register : this->server_registers_) { - if (server_register->address == current_address) { - ok = callback(server_register, offset); - current_address += server_register->register_count; - offset += server_register->register_count * sizeof(uint16_t); - break; - } - } - - if (!ok) { - return false; - } - } - return true; - }; - - // check all registers are writable before writing to any of them: - if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { - return server_register->write_lambda != nullptr; - })) { - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); - return; - } - - // Actually write to the registers: - if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { - int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); - return server_register->write_lambda(number); - })) { - this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); - return; - } - - std::vector<uint8_t> response; - response.reserve(6); - response.push_back(this->address_); - response.push_back(function_code); - response.insert(response.end(), data.begin(), data.begin() + 4); - this->send_raw(response); -} - SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { auto reg_it = std::find_if( std::begin(this->register_ranges_), std::end(this->register_ranges_), @@ -472,14 +311,8 @@ void ModbusController::dump_config() { "ModbusController:\n" " Address: 0x%02X\n" " Max Command Retries: %d\n" - " Offline Skip Updates: %d\n" - " Server Courtesy Response:\n" - " Enabled: %s\n" - " Register Last Address: 0x%02X\n" - " Register Value: %d", - this->address_, this->max_cmd_retries_, this->offline_skip_updates_, - this->server_courtesy_response_.enabled ? "true" : "false", - this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value); + " Offline Skip Updates: %d\n", + this->address_, this->max_cmd_retries_, this->offline_skip_updates_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); @@ -493,11 +326,6 @@ void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type), it.start_address, it.register_count, it.skip_updates); } - ESP_LOGCONFIG(TAG, "server registers"); - for (auto &r : this->server_registers_) { - ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address, - static_cast<uint8_t>(r->value_type), r->register_count); - } #endif } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 6c6c748b73f..40139f055b4 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -120,82 +120,6 @@ class SensorItem { bool force_new_range{false}; }; -struct ServerCourtesyResponse { - bool enabled{false}; - uint16_t register_last_address{0xFFFF}; - uint16_t register_value{0}; -}; - -class ServerRegister { - using ReadLambda = std::function<int64_t()>; - using WriteLambda = std::function<bool(int64_t value)>; - - public: - ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { - this->address = address; - this->value_type = value_type; - this->register_count = register_count; - } - - template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) { - this->read_lambda = [this, user_read_lambda]() -> int64_t { - T user_value = user_read_lambda(this->address); - if constexpr (std::is_same_v<T, float>) { - return bit_cast<uint32_t>(user_value); - } else { - return static_cast<int64_t>(user_value); - } - }; - } - - template<typename T> - void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) { - this->write_lambda = [this, user_write_lambda](int64_t number) { - if constexpr (std::is_same_v<T, float>) { - float float_value = bit_cast<float>(static_cast<uint32_t>(number)); - return user_write_lambda(this->address, float_value); - } - return user_write_lambda(this->address, static_cast<T>(number)); - }; - } - - // Formats a raw value into a string representation based on the value type for debugging - std::string format_value(int64_t value) const { - // max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit) - // plus null terminator = 43, rounded to 44 for 4-byte alignment - char buf[44]; - switch (this->value_type) { - case SensorValueType::U_WORD: - case SensorValueType::U_DWORD: - case SensorValueType::U_DWORD_R: - case SensorValueType::U_QWORD: - case SensorValueType::U_QWORD_R: - buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value)); - return buf; - case SensorValueType::S_WORD: - case SensorValueType::S_DWORD: - case SensorValueType::S_DWORD_R: - case SensorValueType::S_QWORD: - case SensorValueType::S_QWORD_R: - buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value); - return buf; - case SensorValueType::FP32_R: - case SensorValueType::FP32: - buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value))); - return buf; - default: - buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value); - return buf; - } - } - - uint16_t address{0}; - SensorValueType value_type{SensorValueType::RAW}; - uint8_t register_count{0}; - ReadLambda read_lambda; - WriteLambda write_lambda; -}; - // ModbusController::create_register_ranges_ tries to optimize register range // for this the sensors must be ordered by register_type, start_address and bitmask class SensorItemsComparator { @@ -367,16 +291,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void queue_command(const ModbusCommandItem &command); /// Registers a sensor with the controller. Called by esphomes code generator void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } - /// Registers a server register with the controller. Called by esphomes code generator - void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } /// called when a modbus response was parsed without errors void on_modbus_data(const std::vector<uint8_t> &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; - /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors - void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; - /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors - void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the @@ -413,12 +331,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } /// get how many times a command will be (re)sent if no response is received uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } - /// Called by esphome generated code to set the server courtesy response object - void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) { - this->server_courtesy_response_ = server_courtesy_response; - } - /// Get the server courtesy response object - ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; } protected: /// parse sensormap_ and create range of sequential addresses @@ -435,8 +347,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void dump_sensors_(); /// Collection of all sensors for this component SensorSet sensorset_; - /// Collection of all server registers for this component - std::vector<ServerRegister *> server_registers_{}; /// Continuous range of modbus registers std::vector<RegisterRange> register_ranges_{}; /// Hold the pending requests to be sent @@ -461,9 +371,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { CallbackManager<void(int, int)> online_callback_{}; /// Server offline callback CallbackManager<void(int, int)> offline_callback_{}; - /// Server courtesy response - ServerCourtesyResponse server_courtesy_response_{ - .enabled = false, .register_last_address = 0xFFFF, .register_value = 0}; }; /** Convert vector<uint8_t> response payload to float. diff --git a/esphome/components/modbus_server/__init__.py b/esphome/components/modbus_server/__init__.py new file mode 100644 index 00000000000..5182bc05d12 --- /dev/null +++ b/esphome/components/modbus_server/__init__.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import modbus +from esphome.components.const import CONF_ENABLED +from esphome.components.modbus.helpers import ( + CPP_TYPE_REGISTER_MAP, + SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, +) +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID + +from .const import ( + CONF_COURTESY_RESPONSE, + CONF_READ_LAMBDA, + CONF_REGISTER_LAST_ADDRESS, + CONF_REGISTER_VALUE, + CONF_REGISTERS, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +CODEOWNERS = ["@exciton"] + +AUTO_LOAD = ["modbus"] + +MULTI_CONF = True + +modbus_server_ns = cg.esphome_ns.namespace("modbus_server") +ModbusServer = modbus_server_ns.class_( + "ModbusServer", cg.Component, modbus.ModbusDevice +) + +ServerCourtesyResponse = modbus_server_ns.struct("ServerCourtesyResponse") +ServerRegister = modbus_server_ns.struct("ServerRegister") + +SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ENABLED, default=False): cv.boolean, + cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t, + cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t, + } +) + +ModbusServerRegisterSchema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ServerRegister), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + } +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ModbusServer), + cv.Optional(CONF_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA, + cv.Optional( + CONF_REGISTERS, + ): cv.ensure_list(ModbusServerRegisterSchema), + } + ).extend(modbus.modbus_device_schema(0x01)), +) + + +def _final_validate(config): + return modbus.final_validate_modbus_device("modbus_server", role="server")(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if server_courtesy_response := config.get(CONF_COURTESY_RESPONSE): + cg.add( + var.set_server_courtesy_response( + cg.StructInitializer( + ServerCourtesyResponse, + ("enabled", server_courtesy_response[CONF_ENABLED]), + ( + "register_last_address", + server_courtesy_response[CONF_REGISTER_LAST_ADDRESS], + ), + ("register_value", server_courtesy_response[CONF_REGISTER_VALUE]), + ) + ) + ) + if CONF_REGISTERS in config: + for server_register in config[CONF_REGISTERS]: + server_register_var = cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + ) + cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] + cg.add( + server_register_var.set_read_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [(cg.uint16, "address")], + return_type=cpp_type, + ), + ) + ) + if CONF_WRITE_LAMBDA in server_register: + cg.add( + server_register_var.set_write_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_WRITE_LAMBDA], + parameters=[(cg.uint16, "address"), (cpp_type, "x")], + return_type=cg.bool_, + ), + ) + ) + cg.add(var.add_server_register(server_register_var)) + cg.add(var.set_address(config[CONF_ADDRESS])) + await cg.register_component(var, config) + return await modbus.register_modbus_device(var, config) diff --git a/esphome/components/modbus_server/const.py b/esphome/components/modbus_server/const.py new file mode 100644 index 00000000000..f83211c207b --- /dev/null +++ b/esphome/components/modbus_server/const.py @@ -0,0 +1,7 @@ +CONF_REGISTER_LAST_ADDRESS = "register_last_address" +CONF_REGISTER_VALUE = "register_value" +CONF_VALUE_TYPE = "value_type" +CONF_COURTESY_RESPONSE = "courtesy_response" +CONF_READ_LAMBDA = "read_lambda" +CONF_WRITE_LAMBDA = "write_lambda" +CONF_REGISTERS = "registers" diff --git a/esphome/components/modbus_server/modbus_server.cpp b/esphome/components/modbus_server/modbus_server.cpp new file mode 100644 index 00000000000..0063da3a1da --- /dev/null +++ b/esphome/components/modbus_server/modbus_server.cpp @@ -0,0 +1,192 @@ +#include "modbus_server.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome::modbus_server { +using modbus::ModbusFunctionCode; +using modbus::ModbusExceptionCode; + +static const char *const TAG = "modbus_server"; + +void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers) { + ESP_LOGD(TAG, + "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } + + std::vector<uint16_t> sixteen_bit_response; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool found = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + if (!server_register->read_lambda) { + break; + } + int64_t value = server_register->read_lambda(); + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", + server_register->address, static_cast<size_t>(server_register->value_type), + server_register->register_count, server_register->format_value(value).c_str()); + + std::vector<uint16_t> payload; + payload.reserve(server_register->register_count * 2); + modbus::helpers::number_to_payload(payload, value, server_register->value_type); + sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); + current_address += server_register->register_count; + found = true; + break; + } + } + + if (!found) { + if (this->server_courtesy_response_.enabled && + (current_address <= this->server_courtesy_response_.register_last_address)) { + ESP_LOGD(TAG, + "Could not match any register to address 0x%02X, but default allowed. " + "Returning default value: %d.", + current_address, this->server_courtesy_response_.register_value); + sixteen_bit_response.push_back(this->server_courtesy_response_.register_value); + current_address += 1; // Just increment by 1, as the default response is a single register + } else { + ESP_LOGW(TAG, + "Could not match any register to address 0x%02X and default not allowed. Sending exception response.", + current_address); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return; + } + } + } + + std::vector<uint8_t> response; + for (auto v : sixteen_bit_response) { + auto decoded_value = decode_value(v); + response.push_back(decoded_value[0]); + response.push_back(decoded_value[1]); + } + + this->send(function_code, start_address, number_of_registers, response.size(), response.data()); +} + +void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) { + uint16_t number_of_registers; + uint16_t payload_offset; + + if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + if (data.size() < 5) { + ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); + if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + uint16_t payload_size = data[4]; + if (payload_size != number_of_registers * 2) { + ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", + payload_size, number_of_registers); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + if (data.size() < 5 + payload_size) { + ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(), + 5 + payload_size); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + payload_offset = 5; + } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) { + if (data.size() < 4) { + ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size()); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + number_of_registers = 1; + payload_offset = 2; + } else { + ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); + return; + } + + uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8); + ESP_LOGD(TAG, + "Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + auto for_each_register = [this, start_address, number_of_registers, payload_offset]( + const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool { + uint16_t offset = payload_offset; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool ok = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + ok = callback(server_register, offset); + current_address += server_register->register_count; + offset += server_register->register_count * sizeof(uint16_t); + break; + } + } + + if (!ok) { + return false; + } + } + return true; + }; + + // check all registers are writable before writing to any of them: + if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { + return server_register->write_lambda != nullptr; + })) { + this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); + return; + } + + // Actually write to the registers: + if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { + int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); + return server_register->write_lambda(number); + })) { + this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); + return; + } + + std::vector<uint8_t> response; + response.reserve(6); + response.push_back(this->address_); + response.push_back(function_code); + response.insert(response.end(), data.begin(), data.begin() + 4); + this->send_raw(response); +} + +void ModbusServer::dump_config() { + ESP_LOGCONFIG(TAG, + "ModbusServer:\n" + " Address: 0x%02X\n" + " Server Courtesy Response:\n" + " Enabled: %s\n" + " Register Last Address: 0x%02X\n" + " Register Value: %" PRIu16, + this->address_, this->server_courtesy_response_.enabled ? "true" : "false", + this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGCONFIG(TAG, "server registers"); + for (auto &r : this->server_registers_) { + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address, + static_cast<uint8_t>(r->value_type), r->register_count); + } +#endif +} + +} // namespace esphome::modbus_server diff --git a/esphome/components/modbus_server/modbus_server.h b/esphome/components/modbus_server/modbus_server.h new file mode 100644 index 00000000000..0fc2e0bef5d --- /dev/null +++ b/esphome/components/modbus_server/modbus_server.h @@ -0,0 +1,119 @@ +#pragma once + +#include "esphome/core/component.h" + +#include "esphome/components/modbus/modbus.h" +#include "esphome/components/modbus/modbus_helpers.h" +#include "esphome/core/automation.h" + +#include <utility> +#include <vector> + +namespace esphome::modbus_server { + +using modbus::helpers::SensorValueType; + +struct ServerCourtesyResponse { + bool enabled{false}; + uint16_t register_last_address{0xFFFF}; + uint16_t register_value{0}; +}; + +class ServerRegister { + using ReadLambda = std::function<int64_t()>; + using WriteLambda = std::function<bool(int64_t value)>; + + public: + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { + this->address = address; + this->value_type = value_type; + this->register_count = register_count; + } + + template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) { + this->read_lambda = [this, user_read_lambda]() -> int64_t { + T user_value = user_read_lambda(this->address); + if constexpr (std::is_same_v<T, float>) { + return bit_cast<uint32_t>(user_value); + } else { + return static_cast<int64_t>(user_value); + } + }; + } + + template<typename T> + void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) { + this->write_lambda = [this, user_write_lambda](int64_t number) { + if constexpr (std::is_same_v<T, float>) { + float float_value = bit_cast<float>(static_cast<uint32_t>(number)); + return user_write_lambda(this->address, float_value); + } + return user_write_lambda(this->address, static_cast<T>(number)); + }; + } + + // Formats a raw value into a string representation based on the value type for debugging + std::string format_value(int64_t value) const { + // max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit) + // plus null terminator = 43, rounded to 44 for 4-byte alignment + char buf[44]; + switch (this->value_type) { + case SensorValueType::U_WORD: + case SensorValueType::U_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value)); + return buf; + case SensorValueType::S_WORD: + case SensorValueType::S_DWORD: + case SensorValueType::S_DWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value); + return buf; + case SensorValueType::FP32_R: + case SensorValueType::FP32: + buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value))); + return buf; + default: + buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value); + return buf; + } + } + + uint16_t address{0}; + SensorValueType value_type{SensorValueType::RAW}; + uint8_t register_count{0}; + ReadLambda read_lambda; + WriteLambda write_lambda; +}; + +class ModbusServer : public Component, public modbus::ModbusDevice { + public: + void dump_config() override; + + /// Not used for ModbusServer. + void on_modbus_data(const std::vector<uint8_t> &data) override{}; + /// Registers a server register with the controller. Called by esphomes code generator + void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } + /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors + void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; + /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors + void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final; + /// Called by esphome generated code to set the server courtesy response object + void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) { + this->server_courtesy_response_ = server_courtesy_response; + } + /// Get the server courtesy response object + ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; } + + protected: + /// Collection of all server registers for this component + std::vector<ServerRegister *> server_registers_{}; + /// Server courtesy response + ServerCourtesyResponse server_courtesy_response_{ + .enabled = false, .register_last_address = 0xFFFF, .register_value = 0}; +}; + +} // namespace esphome::modbus_server diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index ffaa1491c54..51951a45280 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -1,53 +1,11 @@ -modbus: - - id: mod_bus2 - uart_id: uart_bus - role: server - modbus_controller: - id: modbus_controller1 address: 0x2 modbus_id: modbus_bus - allow_duplicate_commands: false on_online: then: logger.log: "Module Online" - - id: modbus_controller2 - address: 0x2 - modbus_id: mod_bus2 - server_registers: - - address: 0x0000 - value_type: S_DWORD_R - read_lambda: |- - return 42.3; - max_cmd_retries: 0 - - id: modbus_controller3 - address: 0x3 - modbus_id: mod_bus2 - server_registers: - - address: 0x0009 - value_type: S_DWORD - read_lambda: |- - return 31; - write_lambda: |- - printf("address=%d, value=%d", x); - return true; - max_cmd_retries: 0 - - id: modbus_controller4 - modbus_id: mod_bus2 - address: 0x4 - server_courtesy_response: - enabled: true - register_last_address: 100 - register_value: 0 - server_registers: - - address: 0x0001 - value_type: U_WORD - read_lambda: |- - return 0x8; - - address: 0x0005 - value_type: U_WORD - read_lambda: |- - return (random_uint32() % 100); + binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 diff --git a/tests/components/modbus_server/common.yaml b/tests/components/modbus_server/common.yaml new file mode 100644 index 00000000000..3522c9248c9 --- /dev/null +++ b/tests/components/modbus_server/common.yaml @@ -0,0 +1,41 @@ +modbus: + - id: mod_bus2 + uart_id: uart_bus + role: server + +modbus_server: + - id: modbus_server2 + address: 0x2 + modbus_id: mod_bus2 + registers: + - address: 0x0 + value_type: S_DWORD_R + read_lambda: |- + return 42.3; + - id: modbus_server3 + address: 0x3 + modbus_id: mod_bus2 + registers: + - address: 0x9 + value_type: S_DWORD + read_lambda: |- + return 31; + write_lambda: |- + printf("address=%d, value=%d", x); + return true; + - id: modbus_server4 + modbus_id: mod_bus2 + address: 0x4 + courtesy_response: + enabled: true + register_last_address: 100 + register_value: 0 + registers: + - address: 0x1 + value_type: U_WORD + read_lambda: |- + return 0x8; + - address: 0x5 + value_type: U_WORD + read_lambda: |- + return (random_uint32() % 100); diff --git a/tests/components/modbus_server/test.esp32-idf.yaml b/tests/components/modbus_server/test.esp32-idf.yaml new file mode 100644 index 00000000000..ace2d95a0b5 --- /dev/null +++ b/tests/components/modbus_server/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/modbus_server/test.esp8266-ard.yaml b/tests/components/modbus_server/test.esp8266-ard.yaml new file mode 100644 index 00000000000..560629b0cd1 --- /dev/null +++ b/tests/components/modbus_server/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/modbus_server/test.rp2040-ard.yaml b/tests/components/modbus_server/test.rp2040-ard.yaml new file mode 100644 index 00000000000..eeebbd2a8aa --- /dev/null +++ b/tests/components/modbus_server/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/integration/fixtures/uart_mock_modbus_server.yaml b/tests/integration/fixtures/uart_mock_modbus_server.yaml index b657a6fd21b..cc5a59e242a 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_server.yaml @@ -86,9 +86,9 @@ modbus: uart_id: virtual_uart_dev role: server -modbus_controller: +modbus_server: - address: 1 - server_registers: + registers: - address: 0x03 value_type: U_WORD read_lambda: |- diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml index f0f2c56a36d..1e5f5a3389e 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml @@ -33,7 +33,7 @@ uart_mock: data: !lambda return data; - id: virtual_uart_controller baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above + auto_start: true # See comment on virtual_uart_server above debug: on_tx: - then: @@ -56,10 +56,11 @@ modbus_controller: update_interval: 1s id: modbus_controller_1 +modbus_server: - address: 1 modbus_id: virtual_modbus_server id: modbus_server_1 - server_registers: + registers: - address: 0x01 value_type: U_WORD read_lambda: return 99; diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml index 7ec67b03db4..e68edd22715 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml @@ -36,7 +36,7 @@ uart_mock: data: !lambda return data; - id: virtual_uart_server_2 baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above + auto_start: true # See comment on virtual_uart_server above debug: on_tx: - then: @@ -48,7 +48,7 @@ uart_mock: data: !lambda return data; - id: virtual_uart_controller baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above + auto_start: true # See comment on virtual_uart_server above debug: on_tx: - then: @@ -81,15 +81,16 @@ modbus_controller: update_interval: 1s id: modbus_controller_2 +modbus_server: - address: 1 modbus_id: virtual_modbus_server - server_registers: + registers: - address: 0x01 value_type: U_WORD read_lambda: return 919; - address: 2 modbus_id: virtual_modbus_server_2 - server_registers: + registers: - address: 0x01 value_type: U_WORD read_lambda: return 929; diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml index 3edcc73f07f..94890e90de3 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml @@ -33,7 +33,7 @@ uart_mock: data: !lambda return data; - id: virtual_uart_controller baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above + auto_start: true # See comment on virtual_uart_server above debug: on_tx: - then: @@ -94,10 +94,11 @@ modbus_controller: update_interval: 2s id: modbus_controller_1 +modbus_server: - address: 1 modbus_id: virtual_modbus_server id: modbus_server_1 - server_registers: + registers: - address: 0x01 value_type: U_WORD read_lambda: return id(stored_u_word); From daf3f4d2f1b840c1ac9032e7b995b77dbb107b89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 10:41:17 -0500 Subject: [PATCH 1699/2056] [core] wakeable_delay: yield on already-woken fast path (ESP8266, RP2040) (#16045) --- esphome/core/wake/wake_esp8266.h | 4 ++++ esphome/core/wake/wake_rp2040.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/esphome/core/wake/wake_esp8266.h b/esphome/core/wake/wake_esp8266.h index 80cd61035be..7eaaae52930 100644 --- a/esphome/core/wake/wake_esp8266.h +++ b/esphome/core/wake/wake_esp8266.h @@ -36,6 +36,10 @@ inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { } if (g_main_loop_woke) { g_main_loop_woke = false; + // Yield even on the already-woken fast path so callers in tight loops + // (e.g. lwIP raw TCP wait_for_data_) make forward progress when ISRs + // keep re-setting g_main_loop_woke between iterations. + delay(0); return; } esp_delay(ms, []() { return !g_main_loop_woke; }); diff --git a/esphome/core/wake/wake_rp2040.cpp b/esphome/core/wake/wake_rp2040.cpp index b18248dbd27..bdcbb1ad00c 100644 --- a/esphome/core/wake/wake_rp2040.cpp +++ b/esphome/core/wake/wake_rp2040.cpp @@ -36,6 +36,10 @@ void wakeable_delay(uint32_t ms) { } if (g_main_loop_woke) { g_main_loop_woke = false; + // Yield even on the already-woken fast path so callers in tight loops + // (e.g. lwIP raw TCP wait_for_data_) make forward progress when async + // wakes keep re-setting g_main_loop_woke between iterations. + yield(); return; } s_delay_expired = false; From 968878a62d758d97cda953148c42f84cb039fe36 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Tue, 28 Apr 2026 18:35:12 +0200 Subject: [PATCH 1700/2056] [nrf52] implement wake_loop_threadsafe/wakeable_delay (#16032) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/deep_sleep/__init__.py | 13 +++--- .../deep_sleep/deep_sleep_component.cpp | 9 +--- .../deep_sleep/deep_sleep_component.h | 15 ------- .../deep_sleep/deep_sleep_zephyr.cpp | 16 +++----- esphome/components/zigbee/zigbee_zephyr.cpp | 10 +---- esphome/core/application.h | 3 ++ esphome/core/config.py | 2 +- esphome/core/wake.h | 4 +- esphome/core/wake/wake_generic.cpp | 17 -------- esphome/core/wake/wake_generic.h | 31 -------------- esphome/core/wake/wake_zephyr.cpp | 41 +++++++++++++++++++ esphome/core/wake/wake_zephyr.h | 28 +++++++++++++ 12 files changed, 93 insertions(+), 96 deletions(-) delete mode 100644 esphome/core/wake/wake_generic.cpp delete mode 100644 esphome/core/wake/wake_generic.h create mode 100644 esphome/core/wake/wake_zephyr.cpp create mode 100644 esphome/core/wake/wake_zephyr.h diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 0ca557bd6d8..9666c8e5071 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -193,11 +193,14 @@ def _validate_ex1_wakeup_mode(value): def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod: - if not CORE.is_bk72xx: - return value - max_duration = core.TimePeriod(hours=36) - if value > max_duration: - raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX") + if CORE.is_bk72xx: + max_duration = core.TimePeriod(hours=36) + if value > max_duration: + raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX") + elif CORE.using_zephyr: + max_duration = core.TimePeriod(days=49) + if value > max_duration: + raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr") return value diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index d2c5db54b39..d5e34b1f1c8 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -9,18 +9,11 @@ static const char *const TAG = "deep_sleep"; // 5 seconds for deep sleep to ensure clean disconnect from Home Assistant static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000; -bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void DeepSleepComponent::setup() { -#ifdef USE_ZEPHYR - k_sem_init(&this->wakeup_sem_, 0, 1); -#endif global_has_deep_sleep = true; this->schedule_sleep_(); - // It can be used from another thread for waking up the device. - // It should be called as last item in setup. - global_deep_sleep.store(this); } void DeepSleepComponent::schedule_sleep_() { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 854ab152a16..59381eeabeb 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -4,8 +4,6 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" -#include <atomic> - #ifdef USE_ESP32 #include <esp_sleep.h> #endif @@ -15,10 +13,6 @@ #include "esphome/core/time.h" #endif -#ifdef USE_ZEPHYR -#include <zephyr/kernel.h> -#endif - #include <cinttypes> namespace esphome { @@ -125,9 +119,6 @@ class DeepSleepComponent : public Component { void prevent_deep_sleep(); void allow_deep_sleep(); -#ifdef USE_ZEPHYR - void wakeup(); -#endif protected: // Returns nullopt if no run duration is set. Otherwise, returns the run @@ -167,9 +158,6 @@ class DeepSleepComponent : public Component { optional<uint32_t> run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; -#ifdef USE_ZEPHYR - k_sem wakeup_sem_; -#endif }; extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -256,8 +244,5 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; -extern std::atomic<DeepSleepComponent *> - global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace deep_sleep } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_zephyr.cpp b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp index 82d6d8c7ded..f77b73cd586 100644 --- a/esphome/components/deep_sleep/deep_sleep_zephyr.cpp +++ b/esphome/components/deep_sleep/deep_sleep_zephyr.cpp @@ -1,17 +1,13 @@ #include "deep_sleep_component.h" #ifdef USE_ZEPHYR #include "esphome/core/log.h" +#include "esphome/core/wake.h" #include <zephyr/sys/poweroff.h> -#include <zephyr/kernel.h> -#include <zephyr/stats/stats.h> -#include <zephyr/pm/pm.h> namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; -void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); } - optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } void DeepSleepComponent::dump_config_platform_() {} @@ -19,9 +15,8 @@ void DeepSleepComponent::dump_config_platform_() {} bool DeepSleepComponent::prepare_to_sleep_() { return true; } void DeepSleepComponent::deep_sleep_() { - k_timeout_t sleep_duration = K_FOREVER; if (this->sleep_duration_.has_value()) { - sleep_duration = K_USEC(*this->sleep_duration_); + esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000)); } else { #ifndef USE_ZIGBEE // the device can be woken up through one of the following signals: @@ -33,11 +28,12 @@ void DeepSleepComponent::deep_sleep_() { // // The system is reset when it wakes up from System OFF mode. sys_poweroff(); +#else + esphome::internal::wakeable_delay(UINT32_MAX); #endif } - // It might wake up immediately if k_sem_give was called again after wake up - int ret = k_sem_take(&this->wakeup_sem_, sleep_duration); - if (ret == 0) { + const bool woke = esphome::wake_request_take(); + if (woke) { ESP_LOGD(TAG, "Woken up by another thread"); } else { ESP_LOGD(TAG, "Timeout expired (normal sleep)"); diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index dfffd1c91f4..26bef8fb174 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -4,9 +4,7 @@ #include <zephyr/settings/settings.h> #include <zephyr/storage/flash_map.h> #include "esphome/core/hal.h" -#ifdef USE_DEEP_SLEEP -#include "esphome/components/deep_sleep/deep_sleep_component.h" -#endif +#include "esphome/core/wake.h" extern "C" { #include <zboss_api.h> @@ -119,11 +117,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { /* Set default response value. */ p_device_cb_param->status = RET_OK; -#ifdef USE_DEEP_SLEEP - if (auto *ds = deep_sleep::global_deep_sleep.load()) { - ds->wakeup(); - } -#endif + esphome::wake_loop_threadsafe(); // endpoints are enumerated from 1 if (global_zigbee->callbacks_.size() >= endpoint) { diff --git a/esphome/core/application.h b/esphome/core/application.h index 221081a0e40..04e0f1138e3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -370,6 +370,9 @@ class Application { #elif defined(USE_ESP8266) /// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR. static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); } +#elif defined(USE_ZEPHYR) + /// Wake from ISR (Zephyr). No task_woken arg — k_sem_give() handles ISR scheduling internally. + static void wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); } #endif /// Wake from any context (ISR, thread, callback). diff --git a/esphome/core/config.py b/esphome/core/config.py index 14161a7c8b6..b4e81ce49fa 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -812,7 +812,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "wake/wake_host.cpp": { PlatformFramework.HOST_NATIVE, }, - "wake/wake_generic.cpp": { + "wake/wake_zephyr.cpp": { PlatformFramework.NRF52_ZEPHYR, }, # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered diff --git a/esphome/core/wake.h b/esphome/core/wake.h index a2f732fcdbf..5a5d27ceff9 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -69,6 +69,8 @@ __attribute__((always_inline)) inline bool wake_request_take() { #include "esphome/core/wake/wake_rp2040.h" #elif defined(USE_HOST) #include "esphome/core/wake/wake_host.h" +#elif defined(USE_ZEPHYR) +#include "esphome/core/wake/wake_zephyr.h" #else -#include "esphome/core/wake/wake_generic.h" +#error "wake.h: wake_loop_threadsafe() is not implemented for this platform" #endif diff --git a/esphome/core/wake/wake_generic.cpp b/esphome/core/wake/wake_generic.cpp deleted file mode 100644 index 40044e43115..00000000000 --- a/esphome/core/wake/wake_generic.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "esphome/core/defines.h" - -#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \ - !defined(USE_HOST) - -#include "esphome/core/wake.h" - -namespace esphome { - -// === Wake-requested flag storage === -// Fallback platforms (currently only Zephyr/NRF52) are ESPHOME_THREAD_SINGLE. -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -volatile uint8_t g_wake_requested = 0; - -} // namespace esphome - -#endif // fallback guard diff --git a/esphome/core/wake/wake_generic.h b/esphome/core/wake/wake_generic.h deleted file mode 100644 index 85424b61387..00000000000 --- a/esphome/core/wake/wake_generic.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \ - !defined(USE_HOST) - -#include "esphome/core/hal.h" - -namespace esphome { - -/// Zephyr is currently the only platform without a wake mechanism. -/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). -/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. -inline void wake_loop_threadsafe() {} - -inline void wake_loop_any_context() { wake_loop_threadsafe(); } - -namespace internal { -inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) { - if (ms == 0) [[unlikely]] { - yield(); - return; - } - delay(ms); -} -} // namespace internal - -} // namespace esphome - -#endif // fallback guard diff --git a/esphome/core/wake/wake_zephyr.cpp b/esphome/core/wake/wake_zephyr.cpp new file mode 100644 index 00000000000..577d53f5d9d --- /dev/null +++ b/esphome/core/wake/wake_zephyr.cpp @@ -0,0 +1,41 @@ +#include "esphome/core/defines.h" + +#ifdef USE_ZEPHYR + +#include "esphome/core/hal.h" +#include "esphome/core/wake.h" + +#include <zephyr/kernel.h> + +namespace esphome { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +K_SEM_DEFINE(esphome_wake_sem, 0, 1); + +// === Wake-requested flag storage === +// Zephyr has preemptive threads and ISRs, so wake_loop_threadsafe() is genuinely +// called cross-context. volatile uint8_t is sufficient because: (1) Cortex-M +// 8-bit aligned store/load is a single non-tearing instruction, and (2) every +// producer pairs the store with k_sem_give() (release barrier) and the consumer +// pairs the load with k_sem_take() (acquire barrier). +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile uint8_t g_wake_requested = 0; + +void wake_loop_threadsafe() { + wake_request_set(); + k_sem_give(&esphome_wake_sem); +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + if (ms == 0) [[unlikely]] { + yield(); + return; + } + k_sem_take(&esphome_wake_sem, ms == UINT32_MAX ? K_FOREVER : K_MSEC(ms)); +} +} // namespace internal + +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/core/wake/wake_zephyr.h b/esphome/core/wake/wake_zephyr.h new file mode 100644 index 00000000000..c89cfc68e94 --- /dev/null +++ b/esphome/core/wake/wake_zephyr.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ZEPHYR + +#include "esphome/core/hal.h" + +namespace esphome { + +/// Zephyr: wakes the main loop via k_sem_give(). Thread- and ISR-safe. +/// Defined in wake_zephyr.cpp. +void wake_loop_threadsafe(); + +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + +/// ISR-safe: no task_woken arg because Zephyr's k_sem_give() does its own ISR +/// scheduling. Forwards to wake_loop_threadsafe(). +inline void wake_loop_isrsafe() { wake_loop_threadsafe(); } + +namespace internal { +/// Zephyr wakeable_delay uses k_sem_take() with a timeout — defined in wake_zephyr.cpp. +void wakeable_delay(uint32_t ms); +} // namespace internal + +} // namespace esphome + +#endif // USE_ZEPHYR From 6b3df66bdc14e6cfb6cb45bafdf5d118bed1717d Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Tue, 28 Apr 2026 19:20:38 +0200 Subject: [PATCH 1701/2056] [nrf52] make reset pin optional (#11684) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/nrf52/__init__.py | 28 +++++++++++++------ esphome/components/nrf52/dfu.cpp | 27 +++++++++++++----- esphome/components/nrf52/dfu.h | 8 ++---- .../nrf52/test-dfu-pin.nrf52-xiao-ble.yaml | 9 ++++++ .../components/nrf52/test.nrf52-xiao-ble.yaml | 7 +---- 5 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 tests/components/nrf52/test-dfu-pin.nrf52-xiao-ble.yaml diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 5d92a4fa801..d2ed3b15e9c 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -141,6 +141,22 @@ CONF_UICR_ERASE = "uicr_erase" VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] +_DFU_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } +) + + +def _dfu_schema(value: bool | ConfigType) -> ConfigType: + if isinstance(value, bool): + if not value: + raise cv.Invalid("Use 'dfu: true' or specify a configuration dict") + return _DFU_SCHEMA({}) + return _DFU_SCHEMA(value) + + CONFIG_SCHEMA = cv.All( _detect_bootloader, set_core_data, @@ -150,12 +166,7 @@ CONFIG_SCHEMA = cv.All( cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH) ), cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), - cv.Optional(CONF_DFU): cv.Schema( - { - cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate), - cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, - } - ), + cv.Optional(CONF_DFU): _dfu_schema, cv.Optional(CONF_DCDC, default=True): cv.boolean, cv.Optional(CONF_REG0): cv.Schema( { @@ -321,8 +332,9 @@ async def to_code(config: ConfigType) -> None: async def _dfu_to_code(dfu_config): cg.add_define("USE_NRF52_DFU") var = cg.new_Pvariable(dfu_config[CONF_ID]) - pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN]) - cg.add(var.set_reset_pin(pin)) + if CONF_RESET_PIN in dfu_config: + pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True) await cg.register_component(var, dfu_config) diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index c2017248d20..24dee997269 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -2,24 +2,34 @@ #ifdef USE_NRF52_DFU +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/components/zephyr/cdc_acm.h" -namespace esphome { -namespace nrf52 { +#include <hal/nrf_power.h> + +namespace esphome::nrf52 { static const char *const TAG = "dfu"; static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS +static const uint8_t DFU_MAGIC_UF2_RESET = 0x57; // Adafruit nRF52 bootloader UF2 magic void DeviceFirmwareUpdate::setup() { - this->reset_pin_->setup(); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } #if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) { if (rate == 1200) { volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; - this->reset_pin_->digital_write(true); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + } else { + NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET; + App.reboot(); + } } }); #endif @@ -27,10 +37,13 @@ void DeviceFirmwareUpdate::setup() { void DeviceFirmwareUpdate::dump_config() { ESP_LOGCONFIG(TAG, "DFU:"); - LOG_PIN(" RESET Pin: ", this->reset_pin_); + if (this->reset_pin_ != nullptr) { + LOG_PIN(" RESET Pin: ", this->reset_pin_); + } else { + ESP_LOGCONFIG(TAG, " Method: GPREGRET"); + } } -} // namespace nrf52 -} // namespace esphome +} // namespace esphome::nrf52 #endif diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h index 71060e43c18..82c7d9f54eb 100644 --- a/esphome/components/nrf52/dfu.h +++ b/esphome/components/nrf52/dfu.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/gpio.h" -namespace esphome { -namespace nrf52 { +namespace esphome::nrf52 { class DeviceFirmwareUpdate : public Component { public: void setup() override; @@ -14,10 +13,9 @@ class DeviceFirmwareUpdate : public Component { void dump_config() override; protected: - GPIOPin *reset_pin_; + GPIOPin *reset_pin_{nullptr}; }; -} // namespace nrf52 -} // namespace esphome +} // namespace esphome::nrf52 #endif diff --git a/tests/components/nrf52/test-dfu-pin.nrf52-xiao-ble.yaml b/tests/components/nrf52/test-dfu-pin.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..d53c6920017 --- /dev/null +++ b/tests/components/nrf52/test-dfu-pin.nrf52-xiao-ble.yaml @@ -0,0 +1,9 @@ +nrf52: + dfu: + reset_pin: + number: 14 + inverted: true + mode: + output: true + reg0: + voltage: 1.8V diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml index d53c6920017..de4c0c6e00f 100644 --- a/tests/components/nrf52/test.nrf52-xiao-ble.yaml +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -1,9 +1,4 @@ nrf52: - dfu: - reset_pin: - number: 14 - inverted: true - mode: - output: true + dfu: true reg0: voltage: 1.8V From 42ff10afe59e95028f6ac25dd0f3e04569251e3a Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:32:44 +0000 Subject: [PATCH 1702/2056] [watchdog] Fix WatchdogManager on single core apps (#16074) --- esphome/components/watchdog/watchdog.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index 2ce46756e44..545d83a6791 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -6,7 +6,6 @@ #include <cinttypes> #include <cstdint> #ifdef USE_ESP32 -#include <soc/soc_caps.h> #include "esp_idf_version.h" #include "esp_task_wdt.h" #endif @@ -40,7 +39,7 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) { #ifdef USE_ESP32 esp_task_wdt_config_t wdt_config = { .timeout_ms = timeout_ms, - .idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1, + .idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U, .trigger_panic = true, }; esp_task_wdt_reconfigure(&wdt_config); From 4ee9cc432b8ae755c0ef6801b014f6858305fb0b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:37:46 -0400 Subject: [PATCH 1703/2056] [ci] Install requirements_dev.txt in the cached venv (#16082) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d60bd6edc31..6ff3736a8c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Generate cache-key id: cache-key - run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT + run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -58,7 +58,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install -r requirements.txt -r requirements_test.txt pre-commit + pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit pip install -e . pylint: From 7891fd5cf16509ce3e2944c925349d994f0c4322 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:38:31 -0400 Subject: [PATCH 1704/2056] Add dependencies.lock to .gitignore (#16081) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index da568d9b832..4a4a88fd48f 100644 --- a/.gitignore +++ b/.gitignore @@ -146,5 +146,6 @@ sdkconfig.* /components /managed_components +/dependencies.lock api-docs/ From eb01d43feb80d9052cb4b01d845429c75bc8836c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:09:35 -0400 Subject: [PATCH 1705/2056] [spi][http_request][demo] Fix latent clang-tidy issues in headers (#16080) --- esphome/components/demo/demo_alarm_control_panel.h | 2 +- esphome/components/http_request/http_request.h | 2 +- esphome/components/spi/spi.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 9976e5c7f06..5f0725dd4bb 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { protected: void control(const AlarmControlPanelCall &call) override { auto state = call.get_state().value_or(ACP_STATE_DISARMED); - auto code = call.get_code(); + const auto &code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: if (this->get_requires_code_to_arm()) { diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index f37bf776333..2477e26bc12 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { this->request_headers_.push_back({key, value}); } - void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); } void init_json(size_t count) { this->json_.init(count); } void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index dc538f4c41f..e6f592c6e44 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -451,7 +451,7 @@ class SPIDevice : public SPIClient { uint8_t read_byte() { return this->delegate_->transfer(0); } - void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + void read_array(uint8_t *data, size_t length) { this->delegate_->read_array(data, length); } /** * Write a single data item, up to 32 bits. From 44fbb7f5a9cb6a131dec72bd2328ee86e5c2f8e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:10:21 -0500 Subject: [PATCH 1706/2056] Bump CodSpeedHQ/action from 4.14.0 to 4.15.0 (#16084) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ff3736a8c7..57053c36457 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -369,7 +369,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0 + uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0 with: run: ${{ steps.build.outputs.binary }} mode: simulation From c8dffcc9b875c95c7b7d162326c5de10d755880a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:28:33 -0400 Subject: [PATCH 1707/2056] [tlc5971] Remove dead bit-banging delay code (#16086) --- esphome/components/tlc5971/tlc5971.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index be17780f8c9..8128dd90462 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -68,13 +68,8 @@ void TLC5971::transfer_(uint8_t send) { uint8_t startbit = 0x80; bool towrite, lastmosi = !(send & startbit); - uint8_t bitdelay_us = (1000000 / 1000000) / 2; for (uint8_t b = startbit; b != 0; b = b >> 1) { - if (bitdelay_us) { - delayMicroseconds(bitdelay_us); - } - towrite = send & b; if ((lastmosi != towrite)) { this->data_pin_->digital_write(towrite); @@ -82,11 +77,6 @@ void TLC5971::transfer_(uint8_t send) { } this->clock_pin_->digital_write(true); - - if (bitdelay_us) { - delayMicroseconds(bitdelay_us); - } - this->clock_pin_->digital_write(false); } } From 1f4136e76f2d414d0ec0b20f83d9e7333070db06 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:29:09 -0400 Subject: [PATCH 1708/2056] [pipsolar] Guard handle_qmod_ against empty message (#16085) --- esphome/components/pipsolar/pipsolar.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index c304d206c00..5123d8d9d33 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -433,13 +433,17 @@ void Pipsolar::handle_qpigs_(const char *message) { } void Pipsolar::handle_qmod_(const char *message) { - std::string mode; - char device_mode = char(message[1]); if (this->last_qmod_) { this->last_qmod_->publish_state(message); } + // QMOD response is "(M" where M is the device-mode character. Bail out if the + // message is shorter than 2 chars (e.g. empty error response from + // handle_poll_error_) — reading message[1] would otherwise be out of bounds. + if (message[0] == '\0' || message[1] == '\0') + return; if (this->device_mode_) { - mode = device_mode; + std::string mode; + mode = char(message[1]); this->device_mode_->publish_state(mode); } } From 9af557de6dd5a0f88ccc556e7bfa3687ff496db3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Apr 2026 06:29:38 +1000 Subject: [PATCH 1709/2056] [lvgl] Add utility gradient function (#16048) --- esphome/components/lvgl/gradient.py | 6 +++++- esphome/components/lvgl/lvgl_esphome.cpp | 26 ++++++++++++++++++++++++ esphome/components/lvgl/lvgl_esphome.h | 10 +++++++++ esphome/core/defines.h | 1 + 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index c4a3c8f2cb6..e075433d03e 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -1,3 +1,5 @@ +from operator import itemgetter + from esphome import config_validation as cv import esphome.codegen as cg from esphome.const import ( @@ -11,6 +13,7 @@ from esphome.core import ID from esphome.cpp_generator import MockObj from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning +from .helpers import add_lv_use from .lv_validation import lv_color, lv_percentage, opacity from .lvcode import lv from .types import lv_color_t, lv_gradient_t, lv_opa_t @@ -50,6 +53,7 @@ GRADIENT_SCHEMA = cv.ensure_list( async def gradients_to_code(config): + add_lv_use("gradient") max_stops = 2 if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())): add_warning( @@ -58,7 +62,7 @@ async def gradients_to_code(config): for gradient in config.get(CONF_GRADIENTS, ()): var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") idbase = gradient[CONF_ID].id - stops = gradient[CONF_STOPS] + stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION)) max_stops = max(max_stops, len(stops)) if gradient[CONF_DIRECTION].startswith("VER"): lv.grad_vertical_init(var) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index d8248e4aa4e..0308e6b783f 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -864,6 +864,32 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en } #endif // USE_LVGL_SCALE +#ifdef USE_LVGL_GRADIENT +/** + * + * @param dsc The gradient descriptor containing the color stops + * @param pos The current position to calculate the color for + * @return The color for the given position + */ + +lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) { + if (dsc->stops_count == 0) + return lv_color_black(); + if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac) + return dsc->stops[0].color; + if (pos >= dsc->stops[dsc->stops_count - 1].frac) + return dsc->stops[dsc->stops_count - 1].color; + int i = 1; + while (i < dsc->stops_count && dsc->stops[i].frac < pos) + i++; + auto *stop1 = &dsc->stops[i - 1]; + auto *stop2 = &dsc->stops[i]; + int32_t range = stop2->frac - stop1->frac; + int32_t offset = pos - stop1->frac; + return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range); +} +#endif + static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) { LV_TRACE_OBJ_CREATE("begin"); LV_UNUSED(class_p); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 146866f5bd7..83cf9cc0995 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -115,6 +115,16 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value); #endif +#ifdef USE_LVGL_GRADIENT +/** + * + * @param dsc The gradient descriptor containing the color stops + * @param pos The current position to calculate the color for + * @return The color for the given position + */ + +lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos); +#endif // Parent class for things that wrap an LVGL object class LvCompound { public: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index daca55d68a0..592c8c46a24 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -96,6 +96,7 @@ #define USE_LVGL_CHECKBOX #define USE_LVGL_DROPDOWN #define USE_LVGL_FONT +#define USE_LVGL_GRADIENT #define USE_LVGL_IMAGE #define USE_LVGL_IMAGEBUTTON #define USE_LVGL_KEY_LISTENER From 8157c721a59c9ca90c7b076f15873b2ef5d799ec Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Apr 2026 06:31:37 +1000 Subject: [PATCH 1710/2056] [mapping] Implement default value (#15861) --- esphome/components/mapping/__init__.py | 113 +++++++++++++----- esphome/components/mapping/mapping.h | 9 ++ esphome/loader.py | 3 +- tests/components/mapping/common.yaml | 2 + tests/components/mapping/test.esp32-idf.yaml | 2 +- .../components/mapping/test.esp8266-ard.yaml | 2 +- tests/components/mapping/test.rp2040-ard.yaml | 2 +- 7 files changed, 97 insertions(+), 36 deletions(-) diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index a36b414fd51..3c7d78a27bc 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -1,18 +1,27 @@ +from collections.abc import Callable import difflib import esphome.codegen as cg +from esphome.components.const import KEY_METADATA import esphome.config_validation as cv from esphome.const import CONF_FROM, CONF_ID, CONF_TO -from esphome.core import CORE -from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global +from esphome.core import CORE, ID +from esphome.cpp_generator import ( + MockObj, + MockObjClass, + VariableDeclarationExpression, + add_global, +) from esphome.loader import get_component CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True +DOMAIN = "mapping" mapping_ns = cg.esphome_ns.namespace("mapping") mapping_class = mapping_ns.class_("Mapping") +CONF_DEFAULT_VALUE = "default_value" CONF_ENTRIES = "entries" CONF_CLASS = "class" @@ -22,11 +31,18 @@ class IndexType: Represents a type of index in a map. """ - def __init__(self, validator, data_type, conversion): + def __init__( + self, validator: Callable, data_type: MockObj, conversion: Callable = None + ) -> None: self.validator = validator self.data_type = data_type self.conversion = conversion + async def convert_value(self, value): + if self.conversion: + return self.conversion(value) + return await cg.get_variable(value) + INDEX_TYPES = { "int": IndexType(cv.int_, cg.int_, int), @@ -38,6 +54,12 @@ INDEX_TYPES = { } +class MappingMetaData: + def __init__(self, from_: IndexType, to_: IndexType) -> None: + self.from_ = from_ + self.to_ = to_ + + def to_schema(value): """ Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name. @@ -60,7 +82,7 @@ BASE_SCHEMA = cv.Schema( ) -def get_object_type(to_): +def get_object_type(to_) -> MockObjClass | None: """ Get the object type from a string. Possible formats: xxx The name of a component which defines INSTANCE_TYPE @@ -81,25 +103,60 @@ def get_object_type(to_): return None +def get_all_mapping_metadata() -> dict[str, MappingMetaData]: + """Get all mapping metadata.""" + return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + + +def get_mapping_metadata(mapping_id: str) -> MappingMetaData: + """Get mapping metadata by ID for use by other components.""" + return get_all_mapping_metadata()[mapping_id] + + +def add_metadata( + mapping_id: ID, + from_: IndexType, + to_: IndexType, +) -> None: + get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_) + + def map_schema(config): config = BASE_SCHEMA(config) if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict): - raise cv.Invalid("an entries list is required for a map") + raise cv.Invalid("an entries dictionary is required for a mapping") entries = config[CONF_ENTRIES] if len(entries) == 0: - raise cv.Invalid("Map must have at least one entry") + raise cv.Invalid("A mapping must have at least one entry") to_ = config[CONF_TO] if to_ in INDEX_TYPES: - value_type = INDEX_TYPES[to_].validator + value_type = INDEX_TYPES[to_] else: - value_type = get_object_type(to_) - if value_type is None: + object_type = get_object_type(to_) + if object_type is None: matches = difflib.get_close_matches(to_, CORE.id_classes) raise cv.Invalid( f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?" ) - value_type = cv.use_id(value_type) - config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()} + validator = cv.use_id(object_type) + value_type = IndexType(validator, object_type) + config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()} + if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None: + config[CONF_DEFAULT_VALUE] = value_type.validator(default_value) + unexpected_keys = config.keys() - { + CONF_ENTRIES, + CONF_TO, + CONF_FROM, + CONF_ID, + CONF_DEFAULT_VALUE, + } + if unexpected_keys: + errors = [ + cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys + ] + raise cv.MultipleInvalid(errors) + + add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type) return config @@ -107,29 +164,19 @@ CONFIG_SCHEMA = map_schema async def to_code(config): - entries = config[CONF_ENTRIES] - from_ = config[CONF_FROM] - to_ = config[CONF_TO] - index_conversion = INDEX_TYPES[from_].conversion - index_type = INDEX_TYPES[from_].data_type - if to_ in INDEX_TYPES: - value_conversion = INDEX_TYPES[to_].conversion - value_type = INDEX_TYPES[to_].data_type - entries = { - index_conversion(key): value_conversion(value) - for key, value in entries.items() - } - else: - entries = { - index_conversion(key): await cg.get_variable(value) - for key, value in entries.items() - } - value_type = get_object_type(to_) - if list(entries.values())[0].op != ".": - value_type = value_type.operator("ptr") varid = config[CONF_ID] + metadata = get_mapping_metadata(varid.id) + entries = { + metadata.from_.conversion(key): await metadata.to_.convert_value(value) + for key, value in config[CONF_ENTRIES].items() + } + value_type = metadata.to_.data_type + # entries guaranteed to be non-empty here. + value_0 = list(entries.values())[0] + if isinstance(value_0, MockObj) and value_0.op != ".": + value_type = value_type.operator("ptr") varid.type = mapping_class.template( - index_type, + metadata.from_.data_type, value_type, ) var = MockObj(varid, ".") @@ -139,4 +186,6 @@ async def to_code(config): for key, value in entries.items(): cg.add(var.set(key, value)) + if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None: + cg.add(var.set_default_value(await metadata.to_.convert_value(default_value))) return var diff --git a/esphome/components/mapping/mapping.h b/esphome/components/mapping/mapping.h index 2b8f0d39b2a..d6790caa35d 100644 --- a/esphome/components/mapping/mapping.h +++ b/esphome/components/mapping/mapping.h @@ -40,6 +40,9 @@ template<typename K, typename V> class Mapping { if (it != this->map_.end()) { return V{it->second}; } + if (this->default_value_.has_value()) { + return this->default_value_.value(); + } if constexpr (std::is_pointer_v<K>) { esph_log_e(TAG, "Key '%p' not found in mapping", key); } else if constexpr (std::is_same_v<K, std::string>) { @@ -69,11 +72,17 @@ template<typename K, typename V> class Mapping { if (it != this->map_.end()) { return it->second.c_str(); // safe since value remains in map } + if (this->default_value_.has_value()) { + return this->default_value_.value(); + } return ""; } + void set_default_value(const V &default_value) { this->default_value_ = default_value; } + protected: std::map<key_t, value_t, std::less<key_t>, RAMAllocator<std::pair<key_t, value_t>>> map_; + std::optional<V> default_value_{}; }; } // namespace esphome::mapping diff --git a/esphome/loader.py b/esphome/loader.py index 9390b8094bb..2405fa6f884 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -14,6 +14,7 @@ from typing import Any from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE import esphome.core.config +from esphome.cpp_generator import MockObjClass from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -93,7 +94,7 @@ class ComponentManifest: return getattr(self.module, "CODEOWNERS", []) @property - def instance_type(self) -> list[str]: + def instance_type(self) -> MockObjClass | None: return getattr(self.module, "INSTANCE_TYPE", None) @property diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml index 7ffcfa4f67a..b3db9d54eb7 100644 --- a/tests/components/mapping/common.yaml +++ b/tests/components/mapping/common.yaml @@ -21,6 +21,7 @@ mapping: entries: clear-night: image_1 sunny: image_2 + default_value: image_1 - id: weather_map_2 from: string to: image @@ -35,6 +36,7 @@ mapping: 2: "two" 3: "three" 77: "seventy-seven" + default_value: unknown - id: string_map from: string to: int diff --git a/tests/components/mapping/test.esp32-idf.yaml b/tests/components/mapping/test.esp32-idf.yaml index a35b6940c74..93adcf9988b 100644 --- a/tests/components/mapping/test.esp32-idf.yaml +++ b/tests/components/mapping/test.esp32-idf.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 cs_pin: 12 diff --git a/tests/components/mapping/test.esp8266-ard.yaml b/tests/components/mapping/test.esp8266-ard.yaml index c59821a2119..6a308b67ddf 100644 --- a/tests/components/mapping/test.esp8266-ard.yaml +++ b/tests/components/mapping/test.esp8266-ard.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 cs_pin: 5 diff --git a/tests/components/mapping/test.rp2040-ard.yaml b/tests/components/mapping/test.rp2040-ard.yaml index fdfed5f6ab9..01b83c4ab82 100644 --- a/tests/components/mapping/test.rp2040-ard.yaml +++ b/tests/components/mapping/test.rp2040-ard.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 data_rate: 31.25MHz From 594b269dba2e383e9f6cc7808af4b1bbb14c7a5d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:33:57 -0400 Subject: [PATCH 1711/2056] [bme680] Rename cal1/cal2 to coeff1/coeff2 (#16087) --- esphome/components/bme680/bme680.cpp | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index e3cd80de004..b599d64c0da 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -78,43 +78,43 @@ void BME680Component::setup() { } // Read calibration - uint8_t cal1[25]; - if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) { + uint8_t coeff1[25]; + if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) { this->mark_failed(); return; } - uint8_t cal2[16]; - if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) { + uint8_t coeff2[16]; + if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) { this->mark_failed(); return; } - this->calibration_.t1 = cal2[9] << 8 | cal2[8]; - this->calibration_.t2 = cal1[2] << 8 | cal1[1]; - this->calibration_.t3 = cal1[3]; + this->calibration_.t1 = coeff2[9] << 8 | coeff2[8]; + this->calibration_.t2 = coeff1[2] << 8 | coeff1[1]; + this->calibration_.t3 = coeff1[3]; - this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F); - this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4; - this->calibration_.h3 = cal2[3]; - this->calibration_.h4 = cal2[4]; - this->calibration_.h5 = cal2[5]; - this->calibration_.h6 = cal2[6]; - this->calibration_.h7 = cal2[7]; + this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F); + this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4; + this->calibration_.h3 = coeff2[3]; + this->calibration_.h4 = coeff2[4]; + this->calibration_.h5 = coeff2[5]; + this->calibration_.h6 = coeff2[6]; + this->calibration_.h7 = coeff2[7]; - this->calibration_.p1 = cal1[6] << 8 | cal1[5]; - this->calibration_.p2 = cal1[8] << 8 | cal1[7]; - this->calibration_.p3 = cal1[9]; - this->calibration_.p4 = cal1[12] << 8 | cal1[11]; - this->calibration_.p5 = cal1[14] << 8 | cal1[13]; - this->calibration_.p6 = cal1[16]; - this->calibration_.p7 = cal1[15]; - this->calibration_.p8 = cal1[20] << 8 | cal1[19]; - this->calibration_.p9 = cal1[22] << 8 | cal1[21]; - this->calibration_.p10 = cal1[23]; + this->calibration_.p1 = coeff1[6] << 8 | coeff1[5]; + this->calibration_.p2 = coeff1[8] << 8 | coeff1[7]; + this->calibration_.p3 = coeff1[9]; + this->calibration_.p4 = coeff1[12] << 8 | coeff1[11]; + this->calibration_.p5 = coeff1[14] << 8 | coeff1[13]; + this->calibration_.p6 = coeff1[16]; + this->calibration_.p7 = coeff1[15]; + this->calibration_.p8 = coeff1[20] << 8 | coeff1[19]; + this->calibration_.p9 = coeff1[22] << 8 | coeff1[21]; + this->calibration_.p10 = coeff1[23]; - this->calibration_.gh1 = cal2[14]; - this->calibration_.gh2 = cal2[12] << 8 | cal2[13]; - this->calibration_.gh3 = cal2[15]; + this->calibration_.gh1 = coeff2[14]; + this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13]; + this->calibration_.gh3 = coeff2[15]; uint8_t temp_var = 0; if (!this->read_byte(0x02, &temp_var)) { From 70503442f4e743793222c529ef56a03c08a8c4d2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:37:29 -0400 Subject: [PATCH 1712/2056] [dfrobot_sen0395] Brace single-statement else-if in enqueue() (#16089) --- esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp index f47025698b3..98901bd353c 100644 --- a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -104,8 +104,9 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) { if (this->is_full()) { ESP_LOGE(TAG, "Command queue is full"); return -1; - } else if (this->is_empty()) + } else if (this->is_empty()) { front_++; + } rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE; commands_[rear_] = std::move(cmd); // Transfer ownership using std::move return 1; From 16cf4fb5e8818933de0f833170ad4acde7a8f105 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:47:20 -0400 Subject: [PATCH 1713/2056] [nextion] Use std::string::starts_with for HTTPS URL check (#16090) --- esphome/components/nextion/nextion_upload_arduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index e0d18352ff4..399f217a19f 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -331,7 +331,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { #ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { - if (this->tft_url_.compare(0, 6, "https:") == 0) { + if (this->tft_url_.starts_with("https:")) { if (this->wifi_client_secure_ == nullptr) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); From 3d195d748c258236532dcc843dd24487956d06b6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:50:15 -0400 Subject: [PATCH 1714/2056] [ezo] Use make_unique to construct EzoCommand (#16092) --- esphome/components/ezo/ezo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 2dc65b7d14c..bb8fb92f21a 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -35,7 +35,7 @@ void EZOSensor::update() { } if (!found) { - std::unique_ptr<EzoCommand> ezo_command(new EzoCommand); + auto ezo_command = make_unique<EzoCommand>(); ezo_command->command = "R"; ezo_command->command_type = EzoCommandType::EZO_READ; ezo_command->delay_ms = 900; @@ -162,7 +162,7 @@ void EZOSensor::loop() { } void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) { - std::unique_ptr<EzoCommand> ezo_command(new EzoCommand); + auto ezo_command = make_unique<EzoCommand>(); ezo_command->command = command; ezo_command->command_type = command_type; ezo_command->delay_ms = delay_ms; From ab6bda50e46eab26bc229618bdc9375fe8cd52dc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:58:40 -0400 Subject: [PATCH 1715/2056] [esp32_ble] Widen loop variable in as_128bit() to match uuid_.len type (#16088) --- esphome/components/esp32_ble/ble_uuid.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 334780e3b82..886f8237adb 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const { } else { uuid32 = this->uuid_.uuid.uuid16; } - for (uint8_t i = 0; i < this->uuid_.len; i++) { + for (uint16_t i = 0; i < this->uuid_.len; i++) { data[12 + i] = ((uuid32 >> i * 8) & 0xFF); } return ESPBTUUID::from_raw(data); From 7d6b9bee19ffe49e0fab2c9c30252368c61a3cc0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:22:29 -0400 Subject: [PATCH 1716/2056] [wifi] Avoid copying EAP config in three connect handlers (#16094) --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp8266.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp_idf.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f7c70b1147a..1da2d630c10 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1099,9 +1099,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } #ifdef USE_WIFI_WPA2_EAP - auto eap_opt = ap.get_eap(); + const auto &eap_opt = ap.get_eap(); if (eap_opt.has_value()) { - EAPAuth eap_config = *eap_opt; + const EAPAuth &eap_config = *eap_opt; // clang-format off ESP_LOGV( TAG, diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bf3a0d29497..402ca051cde 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -313,10 +313,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - auto eap_opt = ap.get_eap(); + const auto &eap_opt = ap.get_eap(); if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = *eap_opt; + const EAPAuth &eap = *eap_opt; ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); if (ret) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 82ecc80811d..4f39a3a4b12 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -407,10 +407,10 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP - auto eap_opt = ap.get_eap(); + const auto &eap_opt = ap.get_eap(); if (eap_opt.has_value()) { // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = *eap_opt; + const EAPAuth &eap = *eap_opt; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); #else From a62e3fe4fcd13d135cd5710d4dd0189fd7d56cff Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:35:40 -0400 Subject: [PATCH 1717/2056] [json] NOLINT StackAddressEscape false positive in parse_json (#16091) --- esphome/components/json/json_util.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index edcd23f922e..ec1490be1ff 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -39,7 +39,8 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) { } JsonDocument parse_json(const uint8_t *data, size_t len) { - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with + // ArduinoJson if (data == nullptr || len == 0) { ESP_LOGE(TAG, "No data to parse"); return JsonObject(); // return unbound object @@ -63,7 +64,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) { } ESP_LOGE(TAG, "Parse error: %s", err.c_str()); return JsonObject(); // return unbound object - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) } SerializationBuffer<> JsonBuilder::serialize() { From e39c474577e73c5467b0e6828af4460f844e25b7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:13:35 -0400 Subject: [PATCH 1718/2056] [binary_sensor] Bind at_index_ once in MultiClick on_state_ (#16095) --- esphome/components/binary_sensor/automation.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index eb68abce3b7..b13e4a88dd1 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -50,29 +50,31 @@ void MultiClickTriggerBase::on_state_(bool state) { return; } - if (*this->at_index_ == this->timing_count_) { + // at_index_ has a value here (the !has_value() branch above returns). + size_t at_index = *this->at_index_; + if (at_index == this->timing_count_) { this->trigger_(); return; } - MultiClickTriggerEvent evt = this->timing_[*this->at_index_]; + MultiClickTriggerEvent evt = this->timing_[at_index]; if (evt.max_length != 4294967294UL) { - ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT + ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); - } else if (*this->at_index_ + 1 != this->timing_count_) { - ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT + } else if (at_index + 1 != this->timing_count_) { + ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); } else { - ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT + ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT this->is_valid_ = false; this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } - *this->at_index_ = *this->at_index_ + 1; + this->at_index_ = at_index + 1; } void MultiClickTriggerBase::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); From 2f433c78bd1294748b5c3d0c57dbb10a27ed0ef2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:56:36 -0400 Subject: [PATCH 1719/2056] [haier] Brace single-statement else-if in smartair2_climate (#16098) --- esphome/components/haier/smartair2_climate.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 2be5d130504..200cac25575 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -210,8 +210,9 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) #ifdef USE_WIFI else if (this->send_wifi_signal_ && (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() > - SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) { this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); + } #endif } break; default: From a241c9e622e623d5d0c20cfcc02ce44877de7428 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:02:39 -0400 Subject: [PATCH 1720/2056] [online_image][sim800l] Use std::string::starts_with for prefix checks (#16097) --- .../components/online_image/online_image.cpp | 2 +- esphome/components/sim800l/sim800l.cpp | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 24926aa4dcd..a5a3ea51041 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -28,7 +28,7 @@ bool OnlineImage::validate_url_(const std::string &url) { ESP_LOGE(TAG, "URL is too long"); return false; } - if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) { + if (!url.starts_with("http://") && !url.starts_with("https://")) { ESP_LOGE(TAG, "URL must start with http:// or https://"); return false; } diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 913d920c94e..001ec774546 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -110,7 +110,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_INIT: { // While we were waiting for update to check for messages, this notifies a message // is available. - bool message_available = message.compare(0, 6, "+CMTI:") == 0; + bool message_available = message.starts_with("+CMTI:"); if (!message_available) { if (message == "RING") { // Incoming call... @@ -120,7 +120,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->call_state_ = 6; this->call_disconnected_callback_.call(); } - } else if (message.compare(0, 6, "+CUSD:") == 0) { + } else if (message.starts_with("+CUSD:")) { // Incoming USSD MESSAGE this->state_ = STATE_CHECK_USSD; } @@ -175,7 +175,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { break; case STATE_CHECK_USSD: ESP_LOGD(TAG, "Check ussd code: '%s'", message.c_str()); - if (message.compare(0, 6, "+CUSD:") == 0) { + if (message.starts_with("+CUSD:")) { this->state_ = STATE_RECEIVED_USSD; this->ussd_ = ""; size_t start = 10; @@ -196,8 +196,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_CREG_WAIT: { // Response: "+CREG: 0,1" -- the one there means registered ok // "+CREG: -,-" means not registered ok - bool registered = - message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); + bool registered = message.size() > 9 && message.starts_with("+CREG:") && (message[9] == '1' || message[9] == '5'); if (registered) { if (!this->registered_) { ESP_LOGD(TAG, "Registered OK"); @@ -223,7 +222,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_CSQ_RESPONSE; break; case STATE_CSQ_RESPONSE: - if (message.compare(0, 5, "+CSQ:") == 0) { + if (message.starts_with("+CSQ:")) { size_t comma = message.find(',', 6); if (comma != 6) { int rssi = parse_number<int>(message.substr(6, comma - 6)).value_or(0); @@ -243,7 +242,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_CHECK_SMS; break; case STATE_PARSE_SMS_RESPONSE: - if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) { + if (message.starts_with("+CMGL:") && this->parse_index_ == 0) { size_t start = 7; size_t end = message.find(',', start); uint8_t item = 0; @@ -278,7 +277,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { } break; case STATE_CHECK_CALL: - if (message.compare(0, 6, "+CLCC:") == 0 && this->parse_index_ == 0) { + if (message.starts_with("+CLCC:") && this->parse_index_ == 0) { this->expect_ack_ = true; size_t start = 7; size_t end = message.find(',', start); @@ -324,7 +323,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { /* Our recipient is set and the message body is in message kick ESPHome callback now */ - if (ok || message.compare(0, 6, "+CMGL:") == 0) { + if (ok || message.starts_with("+CMGL:")) { ESP_LOGD(TAG, "Received SMS from: %s\n" " %s", @@ -360,7 +359,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { } break; case STATE_SENDING_SMS_3: - if (message.compare(0, 6, "+CMGS:") == 0) { + if (message.starts_with("+CMGS:")) { ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str()); this->send_pending_ = false; this->state_ = STATE_CHECK_SMS; @@ -383,7 +382,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_INIT; break; case STATE_PARSE_CLIP: - if (message.compare(0, 6, "+CLIP:") == 0) { + if (message.starts_with("+CLIP:")) { std::string caller_id; size_t start = 7; size_t end = message.find(',', start); From be0ee738474d6535bcb48e7c5e5c274f07577ae2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:22:42 -0400 Subject: [PATCH 1721/2056] [i2c] NOLINT readability-identifier-naming on Zephyr struct forward-decl (#16099) --- esphome/components/i2c/i2c_bus_zephyr.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h index 49cac5b9921..3c4aa9ed1d8 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.h +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -5,7 +5,7 @@ #include "i2c_bus.h" #include "esphome/core/component.h" -struct device; +struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type namespace esphome::i2c { From 15df47747267cb6e287738a7acb12edd8ad04a5a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:41:28 -0400 Subject: [PATCH 1722/2056] [core] Reduce copies in Callback/CallbackManager call paths (#16093) Co-authored-by: J. Nick Koston <nick+github@koston.org> --- esphome/core/helpers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 4a91c460743..b2b07c57a05 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1666,7 +1666,7 @@ template<typename... Ts> struct Callback<void(Ts...)> { void *ctx_{nullptr}; /// Invoke the callback. Only valid on Callbacks created via create(), never on default-constructed instances. - void call(Ts... args) const { this->fn_(this->ctx_, args...); } + void call(Ts... args) const { this->fn_(this->ctx_, std::forward<Ts>(args)...); } /// Create from any callable. Small trivially-copyable callables (like [this] lambdas) /// are stored inline in the ctx pointer without heap allocation. @@ -1742,7 +1742,7 @@ template<typename... Ts> class CallbackManager<void(Ts...)> { template<typename F> void add(F &&callback) { this->add_(CbType::create(std::forward<F>(callback))); } /// Call all callbacks in this manager. - inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { + inline void ESPHOME_ALWAYS_INLINE call(const Ts &...args) { if (this->size_ != 0) { for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { it->call(args...); @@ -1752,7 +1752,7 @@ template<typename... Ts> class CallbackManager<void(Ts...)> { uint16_t size() const { return this->size_; } /// Call all callbacks in this manager. - void operator()(Ts... args) { this->call(args...); } + void operator()(const Ts &...args) { this->call(args...); } protected: template<typename...> friend class LazyCallbackManager; From 0b5835284aaab1c77810ec8c0dea4723f7858f48 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:35:24 +1000 Subject: [PATCH 1723/2056] [lvgl] Additional layout features (#16041) --- esphome/components/lvgl/layout.py | 119 +++++++-- tests/component_tests/lvgl/__init__.py | 0 .../component_tests/lvgl/test_grid_layout.py | 239 ++++++++++++++++++ tests/components/lvgl/lvgl-package.yaml | 83 ++++++ 4 files changed, 419 insertions(+), 22 deletions(-) create mode 100644 tests/component_tests/lvgl/__init__.py create mode 100644 tests/component_tests/lvgl/test_grid_layout.py diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index 46026852af0..32304276d3e 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -1,3 +1,4 @@ +import math import re import textwrap @@ -85,6 +86,22 @@ def grid_free_space(value): grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space) + +def grid_dimension(value): + """ + Validator for a grid `rows` or `columns` value. + Accepts either a positive integer (interpreted as that many cells of equal + `LV_GRID_FR(1)` size) or a non-empty list of grid specs. + """ + if isinstance(value, int): + value = cv.int_range(min=1)(value) + return ["LV_GRID_FR(1)"] * value + result = cv.Schema([grid_spec])(value) + if not result: + raise cv.Invalid("Grid dimension list must contain at least one entry") + return result + + GRID_CELL_SCHEMA = { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, @@ -184,7 +201,16 @@ class DirectionalLayout(FlexLayout): class GridLayout(Layout): - _GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$") + # Match shorthand grid layout strings: "NxM", "Nx" or "xM". + # At least one of the two numbers must be present; this is enforced after matching. + _GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)?\s*x\s*(\d+)?\s*$") + + @staticmethod + def _match_shorthand(layout): + match = GridLayout._GRID_LAYOUT_REGEX.match(layout) + if match is None or (match.group(1) is None and match.group(2) is None): + return None + return match def get_type(self): return TYPE_GRID @@ -192,7 +218,7 @@ class GridLayout(Layout): def get_layout_schemas(self, config: dict) -> tuple: layout = config.get(CONF_LAYOUT) if isinstance(layout, str): - if GridLayout._GRID_LAYOUT_REGEX.match(layout): + if GridLayout._match_shorthand(layout): return ( cv.string, { @@ -213,59 +239,107 @@ class GridLayout(Layout): if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID: return None, {} + x_default = ( + "center" if isinstance(layout.get(CONF_GRID_ROWS), int) else cv.UNDEFINED + ) + y_default = ( + "center" if isinstance(layout.get(CONF_GRID_COLUMNS), int) else cv.UNDEFINED + ) + x_align = layout.get(CONF_GRID_CELL_X_ALIGN, x_default) + y_align = layout.get(CONF_GRID_CELL_Y_ALIGN, y_default) return ( { cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True), - cv.Required(CONF_GRID_ROWS): [grid_spec], - cv.Required(CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(CONF_GRID_ROWS): grid_dimension, + cv.Optional(CONF_GRID_COLUMNS): grid_dimension, cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(CONF_PAD_ROW): padding, cv.Optional(CONF_PAD_COLUMN): padding, cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, }, { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), - cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, - cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_X_ALIGN, default=x_align): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN, default=y_align): grid_alignments, }, ) def validate(self, config: dict): """ Validate the grid layout. - The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns". + The `layout:` key may be a dictionary with `rows` and/or `columns` keys, or a + shorthand string in the format "<rows>x<columns>", "<rows>x" or "x<columns>". + Either dimension may be omitted, in which case it will be calculated from the + other dimension and the number of configured widgets. Either all cells must have a row and column, or none, in which case the grid layout is auto-generated. :param config: :return: The config updated with auto-generated values """ layout = config.get(CONF_LAYOUT) + widgets = config.get(CONF_WIDGETS, []) + num_widgets = len(widgets) if isinstance(layout, str): - # If the layout is a string, assume it is in the format "rows x columns", implying - # a grid layout with the specified number of rows and columns each with CONTENT sizing. + # Shorthand string: "<rows>x<columns>", "<rows>x" or "x<columns>". + # Each dimension defaults to LV_GRID_FR(1). A missing dimension is + # calculated from the other dimension and the number of widgets. layout = layout.strip() - match = GridLayout._GRID_LAYOUT_REGEX.match(layout) - if match: - rows = int(match.group(1)) - cols = int(match.group(2)) - layout = { - CONF_TYPE: TYPE_GRID, - CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows, - CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols, - } - config[CONF_LAYOUT] = layout - else: + match = GridLayout._match_shorthand(layout) + if not match: raise cv.Invalid( - f"Invalid grid layout format: {config}, expected 'rows x columns'", + f"Invalid grid layout format: {layout!r}, expected " + "'<rows>x<columns>', '<rows>x' or 'x<columns>'", [CONF_LAYOUT], ) + rows_int = int(match.group(1)) if match.group(1) is not None else None + cols_int = int(match.group(2)) if match.group(2) is not None else None + for label, val in (("row", rows_int), ("column", cols_int)): + if val is not None and val < 1: + raise cv.Invalid( + f"Invalid grid layout {layout!r}: {label} count must be " + "at least 1", + [CONF_LAYOUT], + ) + if rows_int is not None and cols_int is not None: + rows = rows_int + cols = cols_int + elif rows_int is not None: + rows = rows_int + cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1 + else: + cols = cols_int + rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1 + layout = { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows, + CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols, + } + config[CONF_LAYOUT] = layout # should be guaranteed to be a dict at this point assert isinstance(layout, dict) assert layout.get(CONF_TYPE).lower() == TYPE_GRID + rows_list = layout.get(CONF_GRID_ROWS) + cols_list = layout.get(CONF_GRID_COLUMNS) + if rows_list is None and cols_list is None: + raise cv.Invalid( + "Grid layout requires at least one of 'rows' or 'columns' to be " + "specified", + [CONF_LAYOUT], + ) + if rows_list is None: + cols = len(cols_list) + rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1 + layout[CONF_GRID_ROWS] = ["LV_GRID_FR(1)"] * rows + elif cols_list is None: + rows = len(rows_list) + cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1 + layout[CONF_GRID_COLUMNS] = ["LV_GRID_FR(1)"] * cols allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) @@ -379,7 +453,8 @@ def append_layout_schema(schema, config: dict): textwrap.dedent( """ Invalid 'layout' value - layout choices are 'horizontal', 'vertical', '<rows>x<cols>', + layout choices are 'horizontal', 'vertical', + '<rows>x<cols>', '<rows>x', 'x<cols>', or a dictionary with a 'type' key """ ), diff --git a/tests/component_tests/lvgl/__init__.py b/tests/component_tests/lvgl/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/lvgl/test_grid_layout.py b/tests/component_tests/lvgl/test_grid_layout.py new file mode 100644 index 00000000000..dfd4b2460cb --- /dev/null +++ b/tests/component_tests/lvgl/test_grid_layout.py @@ -0,0 +1,239 @@ +"""Unit tests for the LVGL grid layout shorthand and rows/columns auto-sizing.""" + +from __future__ import annotations + +import pytest +from voluptuous import Invalid + +from esphome.components.lvgl.defines import ( + CONF_GRID_COLUMNS, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_WIDGETS, + TYPE_GRID, +) +from esphome.components.lvgl.layout import GridLayout, grid_dimension +from esphome.const import CONF_TYPE + +FR1 = "LV_GRID_FR(1)" + + +def _widgets(n: int) -> list[dict]: + """Build a list of `n` placeholder widgets for the validate() input.""" + return [{"label": {}} for _ in range(n)] + + +# --------------------------------------------------------------------------- +# grid_dimension validator +# --------------------------------------------------------------------------- + + +def test_grid_dimension_int_expands_to_fr1_list() -> None: + """A positive integer should expand to a list of LV_GRID_FR(1) entries.""" + assert grid_dimension(1) == [FR1] + assert grid_dimension(3) == [FR1, FR1, FR1] + + +def test_grid_dimension_zero_or_negative_rejected() -> None: + """Non-positive integers must be rejected.""" + with pytest.raises(Invalid): + grid_dimension(0) + with pytest.raises(Invalid): + grid_dimension(-2) + + +def test_grid_dimension_list_passes_through() -> None: + """A list should be validated through the existing grid_spec list schema.""" + result = grid_dimension(["100px", "content", "fr(2)"]) + # `grid_spec` normalises each entry: pixel sizes become ints, the + # CONTENT keyword is uppercased and prefixed, and FR(n) is normalised. + assert result == [100, "LV_GRID_CONTENT", "LV_GRID_FR(2)"] + + +def test_grid_dimension_invalid_string_rejected() -> None: + """A string is not a valid grid dimension and should be rejected.""" + with pytest.raises(Invalid): + grid_dimension("not a list") + + +def test_grid_dimension_empty_list_rejected() -> None: + """An empty list of grid specs must be rejected.""" + with pytest.raises(Invalid, match="at least one entry"): + grid_dimension([]) + + +# --------------------------------------------------------------------------- +# Shorthand string layouts +# --------------------------------------------------------------------------- + + +def test_shorthand_full_form_unchanged() -> None: + """`<rows>x<cols>` continues to work and yields the exact dimensions.""" + config = {CONF_LAYOUT: "2x3", CONF_WIDGETS: _widgets(0)} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + assert layout[CONF_TYPE] == TYPE_GRID + assert layout[CONF_GRID_ROWS] == [FR1, FR1] + assert layout[CONF_GRID_COLUMNS] == [FR1, FR1, FR1] + + +def test_shorthand_rows_only_calculates_columns_from_widgets() -> None: + """`<rows>x` derives the column count from the number of widgets.""" + config = {CONF_LAYOUT: "3x", CONF_WIDGETS: _widgets(7)} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + # 7 widgets / 3 rows -> ceil = 3 columns. + assert len(layout[CONF_GRID_ROWS]) == 3 + assert len(layout[CONF_GRID_COLUMNS]) == 3 + + +def test_shorthand_columns_only_calculates_rows_from_widgets() -> None: + """`x<cols>` derives the row count from the number of widgets.""" + config = {CONF_LAYOUT: "x4", CONF_WIDGETS: _widgets(5)} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + # 5 widgets / 4 cols -> ceil = 2 rows. + assert len(layout[CONF_GRID_ROWS]) == 2 + assert len(layout[CONF_GRID_COLUMNS]) == 4 + + +def test_shorthand_rows_only_no_widgets_defaults_columns_to_one() -> None: + """With no widgets and only rows specified, the column count defaults to 1.""" + config = {CONF_LAYOUT: "3x", CONF_WIDGETS: []} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + assert len(layout[CONF_GRID_ROWS]) == 3 + assert len(layout[CONF_GRID_COLUMNS]) == 1 + + +def test_shorthand_columns_only_no_widgets_defaults_rows_to_one() -> None: + """With no widgets and only columns specified, the row count defaults to 1.""" + config = {CONF_LAYOUT: "x4", CONF_WIDGETS: []} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + assert len(layout[CONF_GRID_ROWS]) == 1 + assert len(layout[CONF_GRID_COLUMNS]) == 4 + + +def test_shorthand_with_whitespace_accepted() -> None: + """The shorthand parser should tolerate whitespace around the components.""" + config = {CONF_LAYOUT: " 3 x ", CONF_WIDGETS: _widgets(6)} + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + # 6 widgets / 3 rows -> 2 columns. + assert len(layout[CONF_GRID_ROWS]) == 3 + assert len(layout[CONF_GRID_COLUMNS]) == 2 + + +def test_shorthand_bare_x_rejected() -> None: + """Pure `x` (no digits at all) is not a valid shorthand.""" + config = {CONF_LAYOUT: "x", CONF_WIDGETS: _widgets(2)} + with pytest.raises(Invalid): + GridLayout().validate(config) + + +@pytest.mark.parametrize( + "layout,bad_label", + [ + ("0x3", "row"), + ("3x0", "column"), + ("0x", "row"), + ("x0", "column"), + ("0x0", "row"), + ], +) +def test_shorthand_zero_dimension_rejected(layout: str, bad_label: str) -> None: + """Shorthand row/column counts must be >= 1.""" + config = {CONF_LAYOUT: layout, CONF_WIDGETS: _widgets(2)} + with pytest.raises(Invalid, match=f"{bad_label} count must be at least 1"): + GridLayout().validate(config) + + +def test_shorthand_get_layout_schemas_recognizes_partial_forms() -> None: + """`<rows>x` and `x<cols>` should be picked up by GridLayout.get_layout_schemas.""" + grid = GridLayout() + for layout in ("3x", "x4", "2x3"): + layout_schema, _ = grid.get_layout_schemas({CONF_LAYOUT: layout}) + assert layout_schema is not None, f"{layout!r} should be recognised" + # Pure `x` and unrelated strings should not be picked up as a grid layout. + for layout in ("x", "horizontal"): + layout_schema, _ = grid.get_layout_schemas({CONF_LAYOUT: layout}) + assert layout_schema is None, f"{layout!r} should not be recognised" + + +# --------------------------------------------------------------------------- +# Dict-form layouts with rows/columns auto-sizing +# --------------------------------------------------------------------------- + + +def test_dict_rows_only_calculates_columns_from_widgets() -> None: + """A dict layout with only rows fills in the column count from widget count.""" + config = { + CONF_LAYOUT: { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: [FR1, FR1], + }, + CONF_WIDGETS: _widgets(5), + } + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + # 5 widgets / 2 rows -> ceil = 3 columns. + assert len(layout[CONF_GRID_ROWS]) == 2 + assert layout[CONF_GRID_COLUMNS] == [FR1, FR1, FR1] + + +def test_dict_columns_only_calculates_rows_from_widgets() -> None: + """A dict layout with only columns fills in the row count from widget count.""" + config = { + CONF_LAYOUT: { + CONF_TYPE: TYPE_GRID, + CONF_GRID_COLUMNS: [FR1, FR1, FR1], + }, + CONF_WIDGETS: _widgets(7), + } + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + # 7 widgets / 3 cols -> ceil = 3 rows. + assert layout[CONF_GRID_ROWS] == [FR1, FR1, FR1] + assert len(layout[CONF_GRID_COLUMNS]) == 3 + + +def test_dict_rows_only_no_widgets_defaults_columns_to_one() -> None: + """A dict layout with rows but no widgets defaults columns to 1.""" + config = { + CONF_LAYOUT: { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: [FR1, FR1, FR1], + }, + CONF_WIDGETS: [], + } + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + assert len(layout[CONF_GRID_ROWS]) == 3 + assert layout[CONF_GRID_COLUMNS] == [FR1] + + +def test_dict_neither_rows_nor_columns_rejected() -> None: + """A grid layout dict without rows AND without columns must be rejected.""" + config = { + CONF_LAYOUT: {CONF_TYPE: TYPE_GRID}, + CONF_WIDGETS: _widgets(3), + } + with pytest.raises(Invalid): + GridLayout().validate(config) + + +def test_dict_both_rows_and_columns_unchanged() -> None: + """When both dimensions are present they are preserved as-is.""" + config = { + CONF_LAYOUT: { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: [FR1, FR1], + CONF_GRID_COLUMNS: [FR1, FR1, FR1], + }, + CONF_WIDGETS: _widgets(0), + } + result = GridLayout().validate(config) + layout = result[CONF_LAYOUT] + assert layout[CONF_GRID_ROWS] == [FR1, FR1] + assert layout[CONF_GRID_COLUMNS] == [FR1, FR1, FR1] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d6e237199ae..9c4ad4bbf85 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -1113,6 +1113,8 @@ lvgl: pad_row: 6px pad_column: 0 multiple_widgets_per_cell: true + grid_cell_x_align: center + grid_cell_y_align: center widgets: - image: grid_cell_row_pos: 0 @@ -1305,6 +1307,87 @@ lvgl: hidden: true mode: text_lower + # Grid shorthand "<rows>x": 3 rows specified, columns derived + # from widget count (4 widgets / 3 rows -> 2 columns) + - obj: + id: grid_rows_only_shorthand + layout: 3x + widgets: + - label: + text: "r1" + - label: + text: "r2" + - label: + text: "r3" + - label: + text: "r4" + + # Grid shorthand "x<cols>": 4 columns specified, rows derived + # from widget count (5 widgets / 4 cols -> 2 rows) + - obj: + id: grid_cols_only_shorthand + layout: x4 + widgets: + - label: + text: "a" + - label: + text: "b" + - label: + text: "c" + - label: + text: "d" + - label: + text: "e" + + # Grid dict form with grid_rows as a plain integer; columns derived + - obj: + id: grid_rows_int + layout: + type: grid + grid_rows: 2 + widgets: + - label: + text: "1" + - label: + text: "2" + - label: + text: "3" + + # Grid dict form with grid_columns as a plain integer; rows derived + - obj: + id: grid_cols_int + layout: + type: grid + grid_columns: 3 + widgets: + - label: + text: "x" + - label: + text: "y" + - label: + text: "z" + - label: + text: "w" + - label: + text: "v" + + # Grid dict form with both grid_rows and grid_columns as plain integers + - obj: + id: grid_both_int + layout: + type: grid + grid_rows: 2 + grid_columns: 2 + widgets: + - label: + text: "1,1" + - label: + text: "1,2" + - label: + text: "2,1" + - label: + text: "2,2" + font: - file: "gfonts://Roboto" id: space16 From 77b76ac48a41dcfda992199a0343ad06b8c5956a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:56:03 -0400 Subject: [PATCH 1724/2056] [inkbird_ibsth1_mini][speaker][speaker_source] Fix performance-unnecessary-copy-initialization (#16101) --- .../components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp | 4 ++-- .../components/speaker/media_player/speaker_media_player.cpp | 2 +- .../components/speaker_source/speaker_source_media_player.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 94c22ae84de..c53d8e50298 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty"); return false; } - auto mnf_datas = device.get_manufacturer_datas(); + const auto &mnf_datas = device.get_manufacturer_datas(); if (mnf_datas.size() != 1) { ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element"); return false; } - auto mnf_data = mnf_datas[0]; + const auto &mnf_data = mnf_datas[0]; if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) { ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16"); return false; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 930373c6fcd..ab11a89c3f4 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -502,7 +502,7 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) { media_command.announce = false; } - auto media_url = call.get_media_url(); + const auto &media_url = call.get_media_url(); if (media_url.has_value()) { media_command.url = new std::string(*media_url); // Must be manually deleted after receiving media_command from a queue diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 2caab828fb4..87fd4fe9ed5 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -698,7 +698,7 @@ void SpeakerSourceMediaPlayer::control(const media_player::MediaPlayerCall &call } } - auto media_url = call.get_media_url(); + const auto &media_url = call.get_media_url(); if (media_url.has_value()) { auto command = call.get_command(); bool enqueue = command.has_value() && command.value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE; From 29d3a3a4984b09d709e72b74252b1c2001ec68dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 19:58:00 -0500 Subject: [PATCH 1725/2056] [esp8266] Replace millis() with fast accumulator, wrap Arduino callers (#15662) --- esphome/components/esp8266/__init__.py | 5 ++ esphome/components/esp8266/core.cpp | 80 +++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index bef7e36470a..34540bd48da 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -314,6 +314,11 @@ async def to_code(config): for symbol in ("vprintf", "printf", "fprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") + # Wrap Arduino's millis() so all callers (including Arduino libraries and ISR + # handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply + # implementation in the Arduino ESP8266 core. + cg.add_build_flag("-Wl,--wrap=millis") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 159ec20e77b..c9bedb61be4 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -16,9 +16,75 @@ extern "C" { namespace esphome { void HOT yield() { ::yield(); } -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return Millis64Impl::compute(::millis()); } -void HOT delay(uint32_t ms) { ::delay(ms); } +// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit +// multiplies on the LX106). Tracks a running ms counter from 32-bit +// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis +// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g. +// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static +// state against ISR re-entry; the critical section is bounded (≤10 while-loop +// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on +// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level +// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis(). +// +// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles +// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a +// >71 min block would trip the watchdog long before it could matter here. +static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000; +static constexpr uint32_t US_PER_MS = 1000; + +uint32_t IRAM_ATTR HOT millis() { + // Struct packs the three statics so the compiler loads one base address + // instead of three separate literal pool entries (saves ~8 bytes IRAM). + static struct { + uint32_t cache; + uint32_t remainder; + uint32_t last_us; + } state = {0, 0, 0}; + uint32_t ps = xt_rsil(15); + uint32_t now_us = system_get_time(); + uint32_t delta = now_us - state.last_us; + state.last_us = now_us; + state.remainder += delta; + if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) { + // Rare path: large gap (WiFi scan, boot, long block). Constant-time + // conversion keeps the critical section bounded. + uint32_t ms = state.remainder / US_PER_MS; + state.cache += ms; + // Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a + // second __umodsi3 call on the LX106 (no hardware divide). + state.remainder -= ms * US_PER_MS; + } else { + // Common path: small gap. At most ~10 iterations since remainder was + // < threshold (10 ms) on entry and delta adds at most one more threshold + // before exiting this branch. + while (state.remainder >= US_PER_MS) { + state.cache++; + state.remainder -= US_PER_MS; + } + } + uint32_t result = state.cache; + xt_wsr_ps(ps); + return result; +} +uint64_t millis_64() { return Millis64Impl::compute(millis()); } +// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object +// call to the original millis() that --wrap can't intercept, so calling ::delay() +// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still +// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and +// WiFi run correctly. Theoretically less power-efficient than Arduino's +// os_timer-based delay() for long waits, but nearly all ESPHome delays are short +// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is +// negligible. +void HOT delay(uint32_t ms) { + if (ms == 0) { + optimistic_yield(1000); + return; + } + uint32_t start = millis(); + while (millis() - start < ms) { + optimistic_yield(1000); + } +} uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -78,4 +144,12 @@ extern "C" void resetPins() { // NOLINT } // namespace esphome +// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator. +// Requires -Wl,--wrap=millis in build flags (added by __init__.py). +// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); } +// Note: Arduino's init() registers a 60-second overflow timer for micros64(). +// We leave it running — wrapping init() as a no-op would break micros64()'s +// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s). + #endif // USE_ESP8266 From 676f26919ec088cf0f9f6fafc498cc05d5d8cc26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:02:21 -0500 Subject: [PATCH 1726/2056] [mdns] Drive MDNS.update() polling from IP state events on ESP8266/RP2040 (#15961) --- esphome/components/mdns/__init__.py | 39 +++++++++ esphome/components/mdns/mdns_component.h | 81 +++++++++++------ esphome/components/mdns/mdns_esp8266.cpp | 33 +++++-- esphome/components/mdns/mdns_rp2040.cpp | 86 ++++++++++++------- .../mdns/common-enabled-ethernet.yaml | 23 +++++ .../test-enabled-ethernet.rp2040-ard.yaml | 1 + 6 files changed, 202 insertions(+), 61 deletions(-) create mode 100644 tests/components/mdns/common-enabled-ethernet.yaml create mode 100644 tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 7c36295e8d9..2b25cf243d7 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( from esphome.core import CORE, Lambda, coroutine_with_priority from esphome.coroutine import CoroPriority from esphome.cpp_generator import LambdaExpression +import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -61,6 +62,28 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType: return config +def _require_network_interface(config: ConfigType) -> ConfigType: + """Require a network interface for mDNS on Arduino/LEAmDNS platforms. + + On ESP8266 and RP2040 the C++ implementation needs at least one IP state + listener (WiFi on ESP8266; WiFi or Ethernet on RP2040) to arm its polling + window. Reject at config time rather than silently producing a component + that never initializes. + """ + if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2040): + return config + full_config = fv.full_config.get() + has_wifi = "wifi" in full_config + has_ethernet = CORE.is_rp2040 and "ethernet" in full_config + if not (has_wifi or has_ethernet): + options = "'wifi'" if CORE.is_esp8266 else "'wifi' or 'ethernet'" + raise cv.Invalid( + "mdns on this platform requires a network interface — " + f"add a {options} component to your configuration." + ) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -74,6 +97,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = _require_network_interface + + def mdns_txt_record(key: str, value: str) -> cg.RawExpression: """Create a mDNS TXT record. @@ -169,6 +195,19 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) + # Subscribe to the network IP state listener(s) so MDNS.update() is only + # scheduled during the probe+announce phase. Same on_ip_state() override + # serves both WiFi and Ethernet (signatures match). + if CORE.is_esp8266 or CORE.is_rp2040: + if "wifi" in CORE.config: + from esphome.components import wifi + + wifi.request_wifi_ip_state_listener() + if CORE.is_rp2040 and "ethernet" in CORE.config: + from esphome.components import ethernet + + ethernet.request_ethernet_ip_state_listener() + if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.11.0") diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index adf88a9cf16..798af0e0bf5 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -5,6 +5,22 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by +// IP state listener events on whichever network interface is configured. +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \ + ((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \ + (defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS))) +#include "esphome/components/network/ip_address.h" +#define USE_MDNS_EVENT_DRIVEN_POLLING +#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS) +#include "esphome/components/wifi/wifi_component.h" +#define USE_MDNS_WIFI_LISTENER +#endif +#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS) +#include "esphome/components/ethernet/ethernet_component.h" +#define USE_MDNS_ETHERNET_LISTENER +#endif +#endif namespace esphome::mdns { @@ -40,33 +56,40 @@ struct MDNSService { FixedVector<MDNSTXTRecord> txt_records; }; -class MDNSComponent final : public Component { +class MDNSComponent final : public Component +#ifdef USE_MDNS_WIFI_LISTENER + , + public wifi::WiFiIPStateListener +#endif +#ifdef USE_MDNS_ETHERNET_LISTENER + , + public ethernet::EthernetIPStateListener +#endif +{ public: void setup() override; void dump_config() override; - // Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040). - // - // On these platforms, MDNS.update() calls _process(true) which only manages timer-driven - // state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS - // packets are handled independently via the lwIP onRx UDP callback and are NOT affected - // by how often update() is called. - // - // The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1). - // Announcement intervals are 1000ms and cache TTL checks are on the order of seconds - // to minutes. A 50ms polling interval provides sufficient resolution for all timers - // while completely removing mDNS from the per-iteration loop list. - // - // In steady state (after the ~8 second boot probe/announce phase completes), update() - // checks timers that are set to never expire, making every call pure overhead. - // - // Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this - // interval is safe in production. - // - // By using set_interval() instead of overriding loop(), the component is excluded from - // the main loop list via has_overridden_loop(), eliminating all per-iteration overhead - // including virtual dispatch. +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING + // LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes + + // 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires() + // and update() becomes pure overhead. We arm a bounded polling window from IP state + // listener events so update() runs only during that phase. static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50; + // Must exceed LEAmDNS's longest restart-to-announce-complete path: + // MDNS_PROBE_DELAY (250ms) × MDNS_PROBE_COUNT (3) = 750ms probing + // + MDNS_ANNOUNCE_DELAY (1000ms) × MDNS_ANNOUNCE_COUNT (8) = 8000ms announcing + // + rand() % MDNS_PROBE_DELAY jitter on first probe (0–250ms) + // + debounced schedule_function() hop when statusChangeCB fires on ESP8266 + // ≈ 9s nominal. 15s gives ~6s margin to absorb main-loop blocking (long + // component setup, WiFi scan, flash writes) that could stretch the deadlines + // between our polls. If LEAmDNS ever extends its phase (upstream library + // update) this constant needs to grow. Constants defined in LEAmDNS_Priv.h + // (ESP8266 core 3.1.2 / arduino-pico 5.5.1). + static constexpr uint32_t MDNS_POLL_WINDOW_MS = 15000; + static constexpr uint32_t MDNS_POLL_ID = 0; + static constexpr uint32_t MDNS_POLL_STOP_ID = 1; +#endif float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } #ifdef USE_MDNS_EXTRA_SERVICES @@ -87,7 +110,17 @@ class MDNSComponent final : public Component { } #endif +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; +#endif + protected: +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING + /// Arm a fresh MDNS_POLL_WINDOW_MS polling window. Idempotent — re-arming replaces + /// the previous window via the scheduler's atomic cancel-and-add on matching IDs. + void start_polling_window_(); +#endif /// Helper to set up services and MAC buffers, then call platform-specific registration using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &); @@ -130,8 +163,8 @@ class MDNSComponent final : public Component { #ifdef USE_MDNS_STORE_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; #endif -#ifdef USE_RP2040 - bool was_connected_{false}; +#if defined(USE_RP2040) && defined(USE_MDNS_EVENT_DRIVEN_POLLING) + // RP2040 defers MDNS.begin() until the first IP-up event; this tracks that. bool initialized_{false}; #endif void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 70c614f8d34..f6d57866756 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -8,6 +8,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" +// wifi_component.h is pulled in transitively by mdns_component.h when +// USE_MDNS_WIFI_LISTENER is defined. namespace esphome::mdns { @@ -36,15 +38,36 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER } } +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +void MDNSComponent::start_polling_window_() { + // uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add. + this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); +} +#endif + void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); - // Schedule MDNS.update() via set_interval() instead of overriding loop(). - // This removes the component from the per-iteration loop list entirely, - // eliminating virtual dispatch overhead on every main loop cycle. - // See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis. - this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +#ifdef USE_MDNS_WIFI_LISTENER + // LEAmDNS's own LwipIntf::statusChangeCB drives _restart() on netif changes; we just + // arm the window around the initial probe/announce and each reconnect. Unconditional + // here is safe: setup_priority::AFTER_CONNECTION guarantees the network is up. + wifi::global_wifi_component->add_ip_state_listener(this); + this->start_polling_window_(); +#endif } +#ifdef USE_MDNS_WIFI_LISTENER +void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &, + const network::IPAddress &) { + // IP listener only fires on acquisition (not loss), so any notification is a fresh + // IP worth re-arming for. start_polling_window_() is idempotent. + if (ips[0].is_set()) { + this->start_polling_window_(); + } +} +#endif + void MDNSComponent::on_shutdown() { MDNS.close(); delay(10); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 64b603030c8..f5848893a34 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -6,9 +6,10 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" +// wifi_component.h / ethernet_component.h are pulled in transitively by +// mdns_component.h when their respective listener defines are active. // Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty. -// Save and restore our definition around the include to avoid a redefinition warning. #pragma push_macro("IRAM_ATTR") #undef IRAM_ATTR #include <ESP8266mDNS.h> @@ -20,10 +21,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { - // Strip the leading underscore from the proto and service_type. While it is - // part of the wire protocol to have an underscore, and for example ESP-IDF - // expects the underscore to be there, the ESP8266 implementation always adds - // the underscore itself. + // ESP8266mDNS always adds the leading underscore itself, so strip it here. auto *proto = MDNS_STR_ARG(service.proto); while (*proto == '_') { proto++; @@ -40,34 +38,58 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV } } -void MDNSComponent::setup() { - // RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart - // mDNS when the network interface reconnects. However, stateUpCB() is stubbed out - // in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used - // schedule_function() which doesn't exist in arduino-pico, and the callback can't - // safely run directly since netif status callbacks fire from IRQ context - // (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets. - // - // Workaround: defer MDNS.begin() and service registration until the network is - // connected (has an IP), then call notifyAPChange() on subsequent reconnects to - // restart mDNS probing and announcing — all from main loop context so it's - // thread-safe. - this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() { - bool connected = network::is_connected(); - if (connected && !this->was_connected_) { - if (!this->initialized_) { - this->setup_buffers_and_register_(register_rp2040); - this->initialized_ = true; - } else { - MDNS.notifyAPChange(); - } - } - this->was_connected_ = connected; - if (this->initialized_) { - MDNS.update(); - } - }); +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +void MDNSComponent::start_polling_window_() { + // uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add. + this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); } +#endif + +void MDNSComponent::setup() { + // arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses + // on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our + // own IP state listener. Both WiFi and Ethernet have the same listener signature — + // one on_ip_state() override serves both. +#ifdef USE_MDNS_WIFI_LISTENER + wifi::global_wifi_component->add_ip_state_listener(this); + // AFTER_CONNECTION priority means the network may already be up; the listener only + // fires on subsequent changes, so seed the current state. + { + const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); + if (ips[0].is_set()) { + this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0), + wifi::global_wifi_component->get_dns_address(1)); + } + } +#endif +#ifdef USE_MDNS_ETHERNET_LISTENER + ethernet::global_eth_component->add_ip_state_listener(this); + if (ethernet::global_eth_component->is_connected()) { + const auto ips = ethernet::global_eth_component->get_ip_addresses(); + if (ips[0].is_set()) { + this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{}); + } + } +#endif +} + +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &, + const network::IPAddress &) { + // Listener only fires on IP acquisition (not loss); every event is a fresh IP. + if (!ips[0].is_set()) { + return; + } + if (!this->initialized_) { + this->setup_buffers_and_register_(register_rp2040); + this->initialized_ = true; + } else { + MDNS.notifyAPChange(); + } + this->start_polling_window_(); +} +#endif void MDNSComponent::on_shutdown() { MDNS.close(); diff --git a/tests/components/mdns/common-enabled-ethernet.yaml b/tests/components/mdns/common-enabled-ethernet.yaml new file mode 100644 index 00000000000..bfa9321d436 --- /dev/null +++ b/tests/components/mdns/common-enabled-ethernet.yaml @@ -0,0 +1,23 @@ +ethernet: + type: W5500 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + +mdns: + disabled: false + services: + - service: _test_service + protocol: _tcp + port: 8888 + txt: + static_string: Anything diff --git a/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml new file mode 100644 index 00000000000..f84a0bc276b --- /dev/null +++ b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled-ethernet.yaml From 9768380856ed1700155e44269d7394d9461c736c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:04:10 -0500 Subject: [PATCH 1727/2056] [api] Hoist memw out of socket ready check to once per main-loop iter (#15996) --- esphome/core/application.h | 13 +++++++++++++ esphome/core/lwip_fast_select.h | 30 ++++++++++++++---------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 04e0f1138e3..4a18714d0de 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -9,6 +9,10 @@ #include <vector> #include "esphome/core/component.h" #include "esphome/core/defines.h" + +#if defined(USE_LWIP_FAST_SELECT) && defined(ESPHOME_THREAD_MULTI_ATOMICS) +#include <atomic> // for std::atomic_thread_fence in Application::loop() +#endif #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -580,6 +584,15 @@ inline ESPHOME_ALWAYS_INLINE Application::ComponentPhaseGuard::ComponentPhaseGua } inline void ESPHOME_ALWAYS_INLINE Application::loop() { +#if defined(USE_LWIP_FAST_SELECT) && defined(ESPHOME_THREAD_MULTI_ATOMICS) + // Pairs with the TCP/IP thread's SYS_ARCH_UNPROTECT release on rcvevent so + // subsequent Socket::ready() checks in this iter observe the published state + // without a per-call memw. Wake is independent (xTaskNotifyGive/ + // ulTaskNotifyTake), so non-losing. Skipped on MULTI_NO_ATOMICS (e.g. + // BK72xx) — that path keeps `volatile` in esphome_lwip_socket_has_data() + // instead. + std::atomic_thread_fence(std::memory_order_acquire); +#endif #ifdef USE_RUNTIME_STATS // Capture the start of the active (non-sleeping) portion of this iteration. // Used to derive main-loop overhead = active time − Σ(component time) − diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 3b5e449148d..4ba2606d76c 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -26,25 +26,23 @@ extern "C" { struct lwip_sock *esphome_lwip_get_sock(int fd); /// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent. -/// This avoids lwIP core lock contention between the main loop (CPU0) and -/// streaming/networking work (CPU1). Correctness is preserved because callers -/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes -/// a harmless retry on the next loop iteration. In practice, stale reads have not -/// been observed across multi-day testing, but the design does not depend on that. -/// -/// The sock pointer must have been obtained from esphome_lwip_get_sock() and must -/// remain valid (caller owns socket lifetime — no concurrent close). -/// Hot path: inlined volatile 16-bit load — no function call overhead. -/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++. +/// On ESPHOME_THREAD_MULTI_ATOMICS builds, the caller must run on the main +/// loop task after Application::loop's per-iter std::atomic_thread_fence +/// (memory_order_acquire); that fence pairs with the TCP/IP thread's +/// SYS_ARCH_UNPROTECT release, so a plain load suffices and avoids the +/// per-call `memw` that volatile would emit on Xtensa under default +/// -mserialize-volatile. Without atomics (e.g. BK72xx), the fence is skipped +/// and the volatile load provides ordering on its own. +/// Stale reads are harmless either way: the hooked event_callback +/// xTaskNotifyGives on RCVPLUS, so the next iteration re-snapshots and +/// ulTaskNotifyTake never loses a wake. /// The offset and size are verified at compile time in lwip_fast_select.c. static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { - // Unlocked hint read — no lwIP core lock needed. - // volatile prevents the compiler from caching/reordering this cross-thread read. - // The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a - // FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value - // is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on - // Xtensa/RISC-V/ARM and cannot produce torn values. +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + return *(int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0; +#else return *(volatile int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0; +#endif } /// Hook a socket's netconn callback to notify the main loop task on receive events. From 1a57d9bc2fe16ad67342fefd5d7aa20e113858a0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:04:19 -0400 Subject: [PATCH 1728/2056] [sprinkler][pn532] Fix bugprone-unchecked-optional-access (#16102) --- esphome/components/pn532/pn532.cpp | 3 ++- esphome/components/sprinkler/sprinkler.cpp | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 199a44dacc1..3017b78414d 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -317,6 +317,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { if (!this->rd_start_time_.has_value()) { this->rd_start_time_ = millis(); } + const uint32_t rd_start_time = *this->rd_start_time_; while (true) { if (this->is_read_ready()) { @@ -324,7 +325,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { break; } - if (millis() - *this->rd_start_time_ > 100) { + if (millis() - rd_start_time > 100) { ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); this->rd_ready_ = TIMEOUT; break; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 0802cdec8e4..e977c05c48f 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -897,11 +897,12 @@ void Sprinkler::resume() { } if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { + const size_t paused_valve = *this->paused_valve_; + const uint32_t resume_duration = *this->resume_duration_; // Resume only if valve has not been completed yet - if (!this->valve_cycle_complete_(this->paused_valve_.value())) { - ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", this->paused_valve_.value_or(0), - this->resume_duration_.value_or(0)); - this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + if (!this->valve_cycle_complete_(paused_valve)) { + ESP_LOGD(TAG, "Resuming valve %zu with %" PRIu32 " seconds remaining", paused_valve, resume_duration); + this->fsm_request_(paused_valve, resume_duration); } this->reset_resume(); } else { From 8af499b591fa3ed3a7693940ff6b59db0dbc3872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:26:21 -0500 Subject: [PATCH 1729/2056] [api] Use custom deleter to fix incomplete-type error on macOS libc++ (#16050) --- esphome/components/api/api_server.cpp | 5 +++++ esphome/components/api/api_server.h | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index c30bd2e6124..6c26c4e1876 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -30,6 +30,11 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c APIServer::APIServer() { global_api_server = this; } +// Custom deleter defined here so `delete` sees the complete APIConnection type. +// This prevents libc++ from emitting an "incomplete type" error when other +// translation units only have the forward declaration of APIConnection. +void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; } + void APIServer::socket_failed_(const LogString *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); this->destroy_socket_(); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e662d78eba9..6b575e536d3 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -193,7 +193,13 @@ class APIServer final : public Component, // Range-for view over the populated slice [0, api_connection_count_). Read-only with respect // to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the // APIConnection but cannot reset/move the slot and break the count invariant. - using APIConnectionPtr = std::unique_ptr<APIConnection>; + // Custom deleter is defined out-of-line in api_server.cpp so libc++ does not + // eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where + // only the forward declaration of APIConnection is visible (incomplete type). + struct APIConnectionDeleter { + void operator()(APIConnection *p) const; + }; + using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>; class ActiveClientsView { const APIConnectionPtr *begin_; const APIConnectionPtr *end_; @@ -292,7 +298,7 @@ class APIServer final : public Component, uint32_t last_connected_{0}; // Slots [0, api_connection_count_) are populated; trailing slots are always nullptr. - std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{}; + std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{}; // Vectors and strings (12 bytes each on 32-bit) // Shared proto write buffer for all connections. // Not pre-allocated: all send paths call prepare_first_message_buffer() which From 1363f661e6b3b1a8325e096b0e60172a863e104e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:26:25 -0500 Subject: [PATCH 1730/2056] [core] Inline ContinuationAction in If/While/RepeatAction (#16040) --- esphome/core/base_automation.h | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 17f937d10d9..afd11c6867d 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -273,18 +273,32 @@ template<typename... Ts> class WhileLoopContinuation : public Action<Ts...> { WhileAction<Ts...> *parent_; }; +// Wraps a ContinuationAction when Enabled, empty otherwise. +// Lets IfAction elide the else continuation when HasElse is false. +template<bool Enabled, typename... Ts> struct OptionalContinuation { + ContinuationAction<Ts...> action; + explicit OptionalContinuation(Action<Ts...> *parent) : action(parent) {} +}; +template<typename... Ts> struct OptionalContinuation<false, Ts...> { + explicit OptionalContinuation(Action<Ts...> * /*parent*/) {} +}; + template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> { public: explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {} + // Precondition: add_then/add_else must be called at most once per instance. + // Codegen always batches the full action list into a single call. Calling + // twice would re-append the same inline continuation pointer and form a + // self-loop in the next_ chain. void add_then(const std::initializer_list<Action<Ts...> *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new ContinuationAction<Ts...>(this)); + this->then_.add_action(&this->then_continuation_); } void add_else(const std::initializer_list<Action<Ts...> *> &actions) requires(HasElse) { this->else_.add_actions(actions); - this->else_.add_action(new ContinuationAction<Ts...>(this)); + this->else_.add_action(&this->else_continuation_.action); } void play_complex(const Ts &...x) override { @@ -316,17 +330,20 @@ template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> { protected: Condition<Ts...> *condition_; ActionList<Ts...> then_; + ContinuationAction<Ts...> then_continuation_{this}; struct NoElse {}; [[no_unique_address]] std::conditional_t<HasElse, ActionList<Ts...>, NoElse> else_; + [[no_unique_address]] OptionalContinuation<HasElse, Ts...> else_continuation_{this}; }; template<typename... Ts> class WhileAction : public Action<Ts...> { public: WhileAction(Condition<Ts...> *condition) : condition_(condition) {} + // Precondition: must be called at most once per instance (see IfAction::add_then). void add_then(const std::initializer_list<Action<Ts...> *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new WhileLoopContinuation<Ts...>(this)); + this->then_.add_action(&this->loop_continuation_); } friend class WhileLoopContinuation<Ts...>; @@ -354,6 +371,7 @@ template<typename... Ts> class WhileAction : public Action<Ts...> { protected: Condition<Ts...> *condition_; ActionList<Ts...> then_; + WhileLoopContinuation<Ts...> loop_continuation_{this}; }; // Implementation of WhileLoopContinuation::play @@ -386,9 +404,10 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> { public: TEMPLATABLE_VALUE(uint32_t, count) + // Precondition: must be called at most once per instance (see IfAction::add_then). void add_then(const std::initializer_list<Action<uint32_t, Ts...> *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new RepeatLoopContinuation<Ts...>(this)); + this->then_.add_action(&this->loop_continuation_); } friend class RepeatLoopContinuation<Ts...>; @@ -409,6 +428,7 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> { protected: ActionList<uint32_t, Ts...> then_; + RepeatLoopContinuation<Ts...> loop_continuation_{this}; }; // Implementation of RepeatLoopContinuation::play From 35cb28edfe86a407c877ea133745862a1130062d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:27:22 -0500 Subject: [PATCH 1731/2056] [output] Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING (#15998) --- esphome/components/output/__init__.py | 10 ++++- esphome/components/output/automation.h | 3 ++ esphome/components/output/float_output.cpp | 8 +++- esphome/components/output/float_output.h | 51 ++++++++++++++++++++-- esphome/core/defines.h | 1 + 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 36798f2d7f0..4f6c8943f5e 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -54,10 +54,16 @@ async def setup_output_platform_(obj, config): power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) cg.add(obj.set_power_supply(power_supply_)) if CONF_MAX_POWER in config: + cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") cg.add(obj.set_max_power(config[CONF_MAX_POWER])) if CONF_MIN_POWER in config: + cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") cg.add(obj.set_min_power(config[CONF_MIN_POWER])) - if CONF_ZERO_MEANS_ZERO in config: + # Only emit when zero_means_zero is actually enabled. The schema defaults to False + # so this key is always present; emitting unconditionally would force + # USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate. + if config.get(CONF_ZERO_MEANS_ZERO): + cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO])) @@ -121,6 +127,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args): synchronous=True, ) async def output_set_min_power_to_code(config, action_id, template_arg, args): + cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_) @@ -140,6 +147,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args): synchronous=True, ) async def output_set_max_power_to_code(config, action_id, template_arg, args): + cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_) diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 32793781292..537226a143d 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/defines.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -40,6 +41,7 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> { FloatOutput *output_; }; +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING template<typename... Ts> class SetMinPowerAction : public Action<Ts...> { public: SetMinPowerAction(FloatOutput *output) : output_(output) {} @@ -63,6 +65,7 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> { protected: FloatOutput *output_; }; +#endif // USE_OUTPUT_FLOAT_POWER_SCALING } // namespace output } // namespace esphome diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 46014e0903a..35629c828af 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -7,13 +7,15 @@ namespace output { static const char *const TAG = "output.float"; +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING void FloatOutput::set_max_power(float max_power) { - this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0 + this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to min_power <= max <= 1.0 } void FloatOutput::set_min_power(float min_power) { - this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX + this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0 <= min <= max_power } +#endif void FloatOutput::set_level(float state) { state = clamp(state, 0.0f, 1.0f); @@ -26,8 +28,10 @@ void FloatOutput::set_level(float state) { } #endif +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING if (state != 0.0f || !this->zero_means_zero_) // regardless of min_power_, 0.0 means off state = (state * (this->max_power_ - this->min_power_)) + this->min_power_; +#endif if (this->is_inverted()) state = 1.0f - state; diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 5225f88c669..3e1bd839686 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -1,11 +1,13 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "binary_output.h" namespace esphome { namespace output { +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING #define LOG_FLOAT_OUTPUT(this) \ LOG_BINARY_OUTPUT(this) \ if (this->max_power_ != 1.0f) { \ @@ -14,6 +16,9 @@ namespace output { if (this->min_power_ != 0.0f) { \ ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \ } +#else +#define LOG_FLOAT_OUTPUT(this) LOG_BINARY_OUTPUT(this) +#endif /** Base class for all output components that can output a variable level, like PWM. * @@ -22,14 +27,18 @@ namespace output { * makes using maths much easier and (in theory) supports all possible bit depths. * * If you want to create a FloatOutput yourself, you essentially just have to override write_state(float). - * That method will be called for you with inversion and max-min power and offset to min power already applied. + * That method will be called for you with inversion already applied. When USE_OUTPUT_FLOAT_POWER_SCALING is + * enabled (set automatically by Python codegen if any output uses min_power/max_power/zero_means_zero or the + * matching runtime actions), the value will additionally have max-min power scaling and offset to min_power + * applied; otherwise only inversion is applied. * * This interface is compatible with BinaryOutput (and will automatically convert the binary states to floating * point states for you). Additionally, this class provides a way for users to set a minimum and/or maximum power - * output + * output (gated on USE_OUTPUT_FLOAT_POWER_SCALING). */ class FloatOutput : public BinaryOutput { public: +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING /** Set the maximum power output of this component. * * All values are multiplied by max_power - min_power and offset to min_power to get the adjusted value. @@ -51,6 +60,32 @@ class FloatOutput : public BinaryOutput { * @param zero_means_zero True if a 0 state should mean 0 and not min_power. */ void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } +#else + // Compile-time guards for users calling these methods from lambdas (documented usage at + // https://esphome.io/components/output/#output-set_min_power_action). When power scaling + // is compiled out, these template stubs fail to compile with an actionable error pointing + // at the user's lambda. Templating on a default-false bool means static_assert only fires + // on instantiation (i.e. when the user actually calls the method), not on every parse. + template<bool _use_output_float_power_scaling = false> void set_max_power(float max_power) { + static_assert(_use_output_float_power_scaling, + "set_max_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. " + "To enable it, add 'max_power: 100%' (or any value) to one output entry in your YAML — " + "the codegen will then keep the scaling fields. " + "See https://esphome.io/components/output/ for details."); + } + template<bool _use_output_float_power_scaling = false> void set_min_power(float min_power) { + static_assert(_use_output_float_power_scaling, + "set_min_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. " + "To enable it, add 'min_power: 0%' (or any value) to one output entry in your YAML — " + "the codegen will then keep the scaling fields. " + "See https://esphome.io/components/output/ for details."); + } + template<bool _use_output_float_power_scaling = false> void set_zero_means_zero(bool zero_means_zero) { + static_assert(_use_output_float_power_scaling, + "set_zero_means_zero() requires USE_OUTPUT_FLOAT_POWER_SCALING. " + "To enable it, add 'zero_means_zero: true' to one output entry in your YAML."); + } +#endif /** Set the level of this float output, this is called from the front-end. * @@ -69,20 +104,30 @@ class FloatOutput : public BinaryOutput { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING /// Get the maximum power output. float get_max_power() const { return this->max_power_; } /// Get the minimum power output. float get_min_power() const { return this->min_power_; } +#else + /// Get the maximum power output. + float get_max_power() const { return 1.0f; } + + /// Get the minimum power output. + float get_min_power() const { return 0.0f; } +#endif protected: /// Implement BinarySensor's write_enabled; this should never be called. void write_state(bool state) override; virtual void write_state(float state) = 0; +#ifdef USE_OUTPUT_FLOAT_POWER_SCALING float max_power_{1.0f}; float min_power_{0.0f}; - bool zero_means_zero_; + bool zero_means_zero_{false}; +#endif }; } // namespace output diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 592c8c46a24..99ec936c12c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -146,6 +146,7 @@ #define USE_NEXTION_WAVEFORM #define USE_NUMBER #define USE_OUTPUT +#define USE_OUTPUT_FLOAT_POWER_SCALING #define USE_POWER_SUPPLY #define USE_PREFERENCES_SYNC_EVERY_LOOP #define USE_QR_CODE From f05243bd9defaf5d1e665c8829af60f2512b4aa2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:48:35 -0500 Subject: [PATCH 1732/2056] [api] Add 48-bit MAC address varint fast path for BLE advertisements (#15988) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 6 + esphome/components/api/api_pb2.cpp | 4 +- esphome/components/api/api_pb2_service.cpp | 2 + esphome/components/api/proto.h | 34 +++++ script/api_protobuf/api_protobuf.py | 27 +++- script/build_helpers.py | 19 ++- .../components/api/test_proto_mac_varint.cpp | 123 ++++++++++++++++++ tests/components/json/__init__.py | 9 ++ 9 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 tests/components/api/test_proto_mac_varint.cpp create mode 100644 tests/components/json/__init__.py diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c0fd990eca9..391efbd6eb6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1639,7 +1639,7 @@ message BluetoothLEAdvertisementResponse { message BluetoothLERawAdvertisement { option (inline_encode) = true; - uint64 address = 1 [(force) = true]; + uint64 address = 1 [(force) = true, (mac_address) = true]; sint32 rssi = 2 [(force) = true]; uint32 address_type = 3 [(max_value) = 4]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index d5d0b37e8df..ac9c4e59cca 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -110,4 +110,10 @@ extend google.protobuf.FieldOptions { // length varint calculations and direct byte writes, since the length // varint is guaranteed to be 1 byte. optional uint32 max_data_length = 50018; + + // mac_address: Field is a 48-bit MAC address stored in a uint64. + // Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids + // the per-byte loop when the upper bits are non-zero (the common case + // for real MAC addresses, since OUIs occupy the top 24 bits). + optional bool mac_address = 50019 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f6ceee2296e..eb25bf7461a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2352,7 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO uint8_t *len_pos = pos; ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG); ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); - ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address); + ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address); ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi)); if (sub_msg.address_type) { @@ -2373,7 +2373,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const { for (uint16_t i = 0; i < this->advertisements_len; i++) { auto &sub_msg = this->advertisements[i]; size += 2; - size += ProtoSize::calc_uint64_force(1, sub_msg.address); + size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address); size += ProtoSize::calc_sint32_force(1, sub_msg.rssi); size += sub_msg.address_type ? 2 : 0; size += 2 + sub_msg.data_len; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6ae2a3e3694..0ba2961a138 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { } #endif +#ifdef USE_API void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements switch (msg_type) { @@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui break; } } +#endif // USE_API } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 8cac7fff3bf..3ff65029e1b 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -342,6 +342,32 @@ class ProtoEncode { } encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); } + /// Encode a 48-bit MAC address (stored in a uint64) as varint. + /// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the + /// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes + /// with no per-byte branch. Falls back to the general loop otherwise. + /// Caller must guarantee value fits in 48 bits (checked in debug builds). + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint64_t value) { +#ifdef ESPHOME_DEBUG_API + assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits"); +#endif + // 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes + // whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)). + if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 7); + pos[0] = static_cast<uint8_t>(value | 0x80); + pos[1] = static_cast<uint8_t>((value >> 7) | 0x80); + pos[2] = static_cast<uint8_t>((value >> 14) | 0x80); + pos[3] = static_cast<uint8_t>((value >> 21) | 0x80); + pos[4] = static_cast<uint8_t>((value >> 28) | 0x80); + pos[5] = static_cast<uint8_t>((value >> 35) | 0x80); + pos[6] = static_cast<uint8_t>(value >> 42); + pos += 7; + return; + } + encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value); + } static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) { encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type); @@ -817,6 +843,14 @@ class ProtoSize { static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) { return field_id_size + varint(value); } + /// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path. + /// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes; + /// otherwise fall back to the general size calculation. + /// Caller must guarantee value fits in 48 bits (encoder asserts in debug). + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size, + uint64_t value) { + return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value)); + } static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index c10479a726e..bf672d05674 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -184,6 +184,11 @@ class TypeInfo(ABC): """Check if this field should always be encoded (skip zero/empty check).""" return get_field_opt(self._field, pb.force, False) + @property + def mac_address(self) -> bool: + """Check if this uint64 field is a 48-bit MAC address (use 7-byte fast path).""" + return get_field_opt(self._field, pb.mac_address, False) + @property def max_value(self) -> int | None: """Get the max_value option for this field, or None if not set.""" @@ -665,8 +670,22 @@ class UInt64Type(VarintTypeMixin, TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: + if self.mac_address and force: + field_id_size = self.calculate_field_id_size() + return ( + f"size += ProtoSize::calc_uint64_48bit_force({field_id_size}, {name});" + ) return self._get_simple_size_calculation(name, force, "uint64") + @property + def RAW_ENCODE_MAP(self) -> dict[str, str]: # noqa: N802 + if self.mac_address: + return { + **TypeInfo.RAW_ENCODE_MAP, + "encode_uint64": "ProtoEncode::encode_varint_raw_48bit(pos, {value});", + } + return TypeInfo.RAW_ENCODE_MAP + def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -3558,8 +3577,13 @@ static const char *const TAG = "api.service"; # Generate read_message_ as APIConnection method (not base class) so the compiler # can devirtualize and inline the on_* handler calls within the same class. # APIConnection declares this method in api_connection.h. + # Guard with #ifdef USE_API since APIConnection itself is only defined when + # USE_API is set; without this, builds that compile this .cpp without + # USE_API (e.g. C++ unit tests for api dependencies) fail to find the + # class declaration. - out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" + out = "#ifdef USE_API\n" + out += "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" # Auth check block before dispatch switch out += " // Check authentication/connection requirements\n" @@ -3604,6 +3628,7 @@ static const char *const TAG = "api.service"; out += " break;\n" out += " }\n" out += "}\n" + out += "#endif // USE_API\n" cpp += out hpp += "};\n" diff --git a/script/build_helpers.py b/script/build_helpers.py index 1cfae51fca9..4cf2f93fbb6 100644 --- a/script/build_helpers.py +++ b/script/build_helpers.py @@ -324,8 +324,23 @@ def compile_and_get_binary( domain_list.append({CONF_PLATFORM: component}) # Skip "core" — it's a pseudo-component handled by the build # system, not a real loadable component (get_component returns None) - elif get_component(component_name) is not None: - config.setdefault(component_name, []) + elif (component := get_component(component_name)) is not None: + # MULTI_CONF components store their config as a list of dicts, + # everything else stores a single dict. Run the component's + # schema with {} so defaults get populated -- code paths like + # socket.FILTER_SOURCE_FILES expect a fully-populated mapping. + if component.multi_conf: + config.setdefault(component_name, []) + elif component_name not in config: + schema = component.config_schema + try: + config[component_name] = schema({}) if schema is not None else {} + except Exception: # noqa: BLE001 + # Schema requires explicit input we can't synthesize; fall + # back to an empty mapping so subscripting at least returns + # KeyError on missing keys rather than crashing on the + # wrong type. + config[component_name] = {} # Register platforms from the extra config (benchmark.yaml) so # USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing diff --git a/tests/components/api/test_proto_mac_varint.cpp b/tests/components/api/test_proto_mac_varint.cpp new file mode 100644 index 00000000000..317a6fb9d6e --- /dev/null +++ b/tests/components/api/test_proto_mac_varint.cpp @@ -0,0 +1,123 @@ +#include <gtest/gtest.h> + +#include <cstdint> +#include <ios> +#include <random> + +#include "esphome/components/api/api_buffer.h" +#include "esphome/components/api/proto.h" + +namespace esphome::api::testing { + +// Generic varint decoder, used to verify the encoded bytes round-trip back to +// the original 48-bit MAC value, independent of the specialized encoder under +// test. +static uint64_t decode_varint(const uint8_t *buf, size_t len, size_t *consumed) { + uint64_t value = 0; + int shift = 0; + for (size_t i = 0; i < len; i++) { + value |= static_cast<uint64_t>(buf[i] & 0x7F) << shift; + if ((buf[i] & 0x80) == 0) { + *consumed = i + 1; + return value; + } + shift += 7; + } + *consumed = 0; + return 0; +} + +// Reference encoder mirroring ProtoEncode::encode_varint_raw_64. +static size_t reference_encode(uint64_t value, uint8_t *out) { + uint8_t *p = out; + if (value < 128) { + *p++ = static_cast<uint8_t>(value); + return p - out; + } + do { + *p++ = static_cast<uint8_t>(value | 0x80); + value >>= 7; + } while (value > 0x7F); + *p++ = static_cast<uint8_t>(value); + return p - out; +} + +// Encode `mac` via the 48-bit fast path and verify: +// - byte-identical output to the reference loop +// - encoded byte length matches `expected_bytes` +// - calc_uint64_48bit_force agrees on the size +// - the bytes round-trip through a generic varint decoder +static void verify_mac(uint64_t mac, size_t expected_bytes) { + ASSERT_LT(mac, 1ULL << 48) << "test fixture mac exceeds 48 bits"; + + uint8_t ref_buf[16] = {0}; + size_t ref_len = reference_encode(mac, ref_buf); + + APIBuffer api_buf; + api_buf.resize(16); + uint8_t *pos = api_buf.data(); +#ifdef ESPHOME_DEBUG_API + uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size(); +#endif + ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac); + size_t new_len = pos - api_buf.data(); + + EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec; + EXPECT_EQ(ref_len, expected_bytes) << "reference disagrees on length for mac=0x" << std::hex << mac << std::dec; + + for (size_t i = 0; i < new_len; i++) { + EXPECT_EQ(api_buf.data()[i], ref_buf[i]) + << "byte " << i << " differs for mac=0x" << std::hex << mac << " (got 0x" << static_cast<int>(api_buf.data()[i]) + << ", expected 0x" << static_cast<int>(ref_buf[i]) << ")" << std::dec; + } + + size_t consumed = 0; + uint64_t decoded = decode_varint(api_buf.data(), new_len, &consumed); + EXPECT_EQ(consumed, new_len) << "decoder did not consume all bytes for mac=0x" << std::hex << mac << std::dec; + EXPECT_EQ(decoded, mac) << "round-trip mismatch for mac=0x" << std::hex << mac << std::dec; + + // Verify the size helper agrees. field_id_size = 1 (typical 1-byte tag). + uint32_t calc_size = ProtoSize::calc_uint64_48bit_force(1, mac); + EXPECT_EQ(calc_size, 1 + expected_bytes) + << "calc_uint64_48bit_force size mismatch for mac=0x" << std::hex << mac << std::dec; +} + +// Compute the canonical varint byte length for a value < 1<<48. +static size_t expected_varint_len(uint64_t v) { + if (v < (1ULL << 7)) + return 1; + if (v < (1ULL << 14)) + return 2; + if (v < (1ULL << 21)) + return 3; + if (v < (1ULL << 28)) + return 4; + if (v < (1ULL << 35)) + return 5; + if (v < (1ULL << 42)) + return 6; + return 7; +} + +// --- Specific MACs requested for verification --- + +TEST(ProtoMacVarint, AllZeros) { verify_mac(0x000000000000ULL, 1); } // 00:00:00:00:00:00 +TEST(ProtoMacVarint, FirstByteOnly) { verify_mac(0x110000000000ULL, 7); } // 11:00:00:00:00:00 +TEST(ProtoMacVarint, SecondByteOnly) { verify_mac(0x00AA00000000ULL, 6); } // 00:AA:00:00:00:00 +TEST(ProtoMacVarint, ThirdByteOnly) { verify_mac(0x0000BB000000ULL, 5); } // 00:00:BB:00:00:00 +TEST(ProtoMacVarint, FourthByteOnly) { verify_mac(0x000000CC0000ULL, 4); } // 00:00:00:CC:00:00 +TEST(ProtoMacVarint, FifthByteOnly) { verify_mac(0x00000000DD00ULL, 3); } // 00:00:00:00:DD:00 +TEST(ProtoMacVarint, SixthByteOnly) { verify_mac(0x0000000000EEULL, 2); } // 00:00:00:00:00:EE +TEST(ProtoMacVarint, AllOnes) { verify_mac(0xFFFFFFFFFFFFULL, 7); } // FF:FF:FF:FF:FF:FF + +// 100 deterministic-random 48-bit MACs to catch regressions across the space. +TEST(ProtoMacVarint, RandomSample) { + // NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp) -- intentional fixed seed for reproducibility. + std::mt19937_64 rng(0xC0FFEE); + for (int i = 0; i < 100; i++) { + uint64_t mac = rng() & 0xFFFFFFFFFFFFULL; + verify_mac(mac, expected_varint_len(mac)); + } +} + +} // namespace esphome::api::testing diff --git a/tests/components/json/__init__.py b/tests/components/json/__init__.py new file mode 100644 index 00000000000..40ec1f996ea --- /dev/null +++ b/tests/components/json/__init__.py @@ -0,0 +1,9 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # json's to_code calls cg.add_library("bblanchon/ArduinoJson", ...). C++ + # unit test builds that pull json in transitively (e.g. api) need that + # library registration to happen, otherwise json_util.cpp fails to find + # ArduinoJson.h. + manifest.enable_codegen() From d7b21a84a33a4cdae6db6f38dc4b0a06915c1373 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:49:51 -0500 Subject: [PATCH 1733/2056] [git] Make ref fetches and submodule updates shallow (#16014) --- esphome/git.py | 20 +++- tests/unit_tests/test_git.py | 190 +++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 5 deletions(-) diff --git a/esphome/git.py b/esphome/git.py index 4d6e14001a7..0106f248451 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -128,7 +128,10 @@ def clone_or_update( # We need to fetch the PR branch first, otherwise git will complain # about missing objects _LOGGER.info("Fetching %s", ref) - run_git_command(["git", "fetch", "--", "origin", ref], git_dir=repo_dir) + run_git_command( + ["git", "fetch", "--depth=1", "--", "origin", ref], + git_dir=repo_dir, + ) run_git_command( ["git", "reset", "--hard", "FETCH_HEAD"], git_dir=repo_dir ) @@ -138,7 +141,8 @@ def clone_or_update( "Initializing submodules (%s) for %s", ", ".join(submodules), key ) run_git_command( - ["git", "submodule", "update", "--init"] + submodules, + ["git", "submodule", "update", "--init", "--depth=1", "--"] + + submodules, git_dir=repo_dir, ) except GitException: @@ -179,8 +183,13 @@ def clone_or_update( git_dir=repo_dir, ) - # Fetch remote ref - cmd = ["git", "fetch", "--", "origin"] + # Fetch from the remote. --depth=1 keeps the clone shallow + # while still picking up new commits when the remote tip + # moves: a shallow fetch retrieves the current tip being + # fetched, whether that's an explicit ref or the remote's + # default branch, then reset --hard FETCH_HEAD updates the + # working tree to it. + cmd = ["git", "fetch", "--depth=1", "--", "origin"] if ref is not None: cmd.append(ref) run_git_command(cmd, git_dir=repo_dir) @@ -229,7 +238,8 @@ def clone_or_update( "Updating submodules (%s) for %s", ", ".join(submodules), key ) run_git_command( - ["git", "submodule", "update", "--init"] + submodules, + ["git", "submodule", "update", "--init", "--depth=1", "--"] + + submodules, git_dir=repo_dir, ) diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index dd7d26cb714..eab6bfc2cb4 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -811,3 +811,193 @@ def test_clone_or_update_stale_clone_is_retried_after_cleanup( assert repo_dir.exists() assert call_count["clone"] == 2 assert call_count["fetch"] == 2 + + +def test_clone_with_ref_uses_shallow_fetch( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Clone with a ref should use --depth=1 on both clone and fetch.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "pull/123/head" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "clone": + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + git.clone_or_update(url=url, ref=ref, refresh=None, domain=domain) + + call_list = mock_run_git_command.call_args_list + + clone_calls = [c for c in call_list if "clone" in c[0][0]] + assert len(clone_calls) == 1 + assert "--depth=1" in clone_calls[0][0][0] + + fetch_calls = [c for c in call_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + assert "--depth=1" in fetch_calls[0][0][0] + # Ref must still be passed so the requested commit/branch is fetched. + assert ref in fetch_calls[0][0][0] + + +def test_clone_with_submodules_uses_shallow_submodule_update( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Submodule init on a fresh clone should use --depth=1.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + if _get_git_command_type(cmd) == "clone": + repo_dir.mkdir(parents=True, exist_ok=True) + (repo_dir / ".git").mkdir(exist_ok=True) + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + git.clone_or_update( + url=url, + ref=None, + refresh=None, + domain=domain, + submodules=["components/foo"], + ) + + submodule_calls = [ + c for c in mock_run_git_command.call_args_list if "submodule" in c[0][0] + ] + assert len(submodule_calls) == 1 + cmd = submodule_calls[0][0][0] + assert "--depth=1" in cmd + assert "components/foo" in cmd + # The `--` terminator must precede the submodule paths so a path + # beginning with `-` cannot be parsed as an option. + assert cmd.index("--") < cmd.index("components/foo") + + +def test_refresh_fetch_is_shallow(tmp_path: Path, mock_run_git_command: Mock) -> None: + """The refresh-path fetch should use --depth=1.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + _setup_old_repo(repo_dir) + mock_run_git_command.return_value = "abc123" + + git.clone_or_update( + url=url, ref=ref, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + fetch_calls = [c for c in mock_run_git_command.call_args_list if "fetch" in c[0][0]] + assert len(fetch_calls) == 1 + cmd = fetch_calls[0][0][0] + assert "--depth=1" in cmd + # Ref must still be in the refresh fetch so the right tip is updated. + assert cmd[-1] == ref + + +def test_refresh_submodule_update_is_shallow( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """The refresh-path submodule update should use --depth=1.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + domain = "test" + repo_dir = _compute_repo_dir(url, None, domain) + + _setup_old_repo(repo_dir) + mock_run_git_command.return_value = "abc123" + + git.clone_or_update( + url=url, + ref=None, + refresh=TimePeriodSeconds(days=1), + domain=domain, + submodules=["components/foo"], + ) + + submodule_calls = [ + c for c in mock_run_git_command.call_args_list if "submodule" in c[0][0] + ] + assert len(submodule_calls) == 1 + cmd = submodule_calls[0][0][0] + assert "--depth=1" in cmd + assert "components/foo" in cmd + assert cmd.index("--") < cmd.index("components/foo") + + +def test_refresh_picks_up_new_remote_commits( + tmp_path: Path, mock_run_git_command: Mock +) -> None: + """Shallow fetch must still pull new commits when the remote tip moves. + + Simulates a stale local repo at SHA "old" while the remote has advanced + to SHA "new". The refresh path must run fetch (with --depth=1) followed + by reset --hard FETCH_HEAD so the working tree advances to the new tip. + """ + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + ref = "main" + domain = "test" + repo_dir = _compute_repo_dir(url, ref, domain) + + _setup_old_repo(repo_dir) + + # rev-parse is called once before fetch to record the pre-update SHA. + rev_parse_calls = {"count": 0} + + def git_command_side_effect( + cmd: list[str], cwd: str | None = None, **kwargs: Any + ) -> str: + cmd_type = _get_git_command_type(cmd) + if cmd_type == "rev-parse": + rev_parse_calls["count"] += 1 + return "old_sha" + return "" + + mock_run_git_command.side_effect = git_command_side_effect + + _, revert = git.clone_or_update( + url=url, ref=ref, refresh=TimePeriodSeconds(days=1), domain=domain + ) + + # Verify the refresh sequence: rev-parse -> stash -> fetch (depth=1) -> reset + call_list = mock_run_git_command.call_args_list + cmd_sequence = [_get_git_command_type(c[0][0]) for c in call_list] + assert cmd_sequence == ["rev-parse", "stash", "fetch", "reset"] + + fetch_cmd = call_list[2][0][0] + assert "--depth=1" in fetch_cmd + assert fetch_cmd[-1] == ref + + reset_cmd = call_list[3][0][0] + assert reset_cmd[-1] == "FETCH_HEAD" + + # revert callback should reset back to the recorded pre-update SHA. + assert revert is not None + revert() + assert mock_run_git_command.call_args_list[-1][0][0] == [ + "git", + "reset", + "--hard", + "old_sha", + ] From eec770d622571ed9f625f3a0e6b0457be0542d55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 20:52:09 -0500 Subject: [PATCH 1734/2056] [core] Use ETag in external_files cache to fix re-downloads from raw.githubusercontent.com (#16020) --- esphome/external_files.py | 76 +++++- tests/unit_tests/test_external_files.py | 345 +++++++++++++++++++++--- 2 files changed, 379 insertions(+), 42 deletions(-) diff --git a/esphome/external_files.py b/esphome/external_files.py index b6f6149ebbb..bd29dc93b1d 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -1,14 +1,17 @@ from __future__ import annotations +import contextlib from datetime import UTC, datetime import logging +import os from pathlib import Path import requests import esphome.config_validation as cv from esphome.const import __version__ -from esphome.core import CORE, TimePeriodSeconds +from esphome.core import CORE, EsphomeError, TimePeriodSeconds +from esphome.helpers import write_file _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@landonr"] @@ -16,12 +19,72 @@ CODEOWNERS = ["@landonr"] NETWORK_TIMEOUT = 30 IF_MODIFIED_SINCE = "If-Modified-Since" +IF_NONE_MATCH = "If-None-Match" +ETAG = "ETag" CACHE_CONTROL = "Cache-Control" CACHE_CONTROL_MAX_AGE = "max-age=" CONTENT_DISPOSITION = "content-disposition" TEMP_DIR = "temp" +def _etag_sidecar_path(local_file_path: Path) -> Path: + return local_file_path.parent / f".{local_file_path.name}.etag" + + +def _mtime_seconds(path: Path) -> int: + """Return `path`'s mtime as integer seconds. + + Whole seconds is the common-denominator resolution across all + filesystems we run on (FAT/exFAT 2s, NTFS 100ns, APFS/ext4 ns), so + comparisons survive setting+reading round-trips that would lose + sub-second precision on lower-resolution filesystems. + """ + return int(path.stat().st_mtime) + + +def _read_etag(local_file_path: Path) -> str | None: + """Return the cached ETag if its sidecar's mtime still matches the cache + file's. A mismatch means the cache file was modified out-of-band, so the + ETag no longer describes its contents -- delete the stale sidecar and + return None. + """ + etag_path = _etag_sidecar_path(local_file_path) + try: + if _mtime_seconds(etag_path) != _mtime_seconds(local_file_path): + _LOGGER.debug( + "ETag sidecar mtime mismatch at %s; treating as stale", + local_file_path, + ) + etag_path.unlink() + return None + return etag_path.read_text().strip() or None + except OSError: + return None + + +def _write_etag(local_file_path: Path, etag: str | None) -> None: + etag_path = _etag_sidecar_path(local_file_path) + if not etag: + # ETag persistence is best-effort; matches `_read_etag`'s tolerance. + with contextlib.suppress(OSError): + etag_path.unlink() + return + try: + write_file(etag_path, etag) + except EsphomeError as e: + _LOGGER.debug("Could not save ETag for %s: %s", local_file_path, e) + return + # Pin the sidecar's mtime to the cache file's mtime. _read_etag relies on + # this match to detect out-of-band edits to the cache file. + try: + file_mtime = _mtime_seconds(local_file_path) + os.utime(etag_path, (file_mtime, file_mtime)) + except OSError as e: + _LOGGER.debug( + "Could not sync ETag sidecar mtime for %s: %s", local_file_path, e + ) + + def has_remote_file_changed(url: str, local_file_path: Path) -> bool: if local_file_path.exists(): _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) @@ -35,14 +98,17 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: IF_MODIFIED_SINCE: local_modification_time_str, CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", } + if etag := _read_etag(local_file_path): + headers[IF_NONE_MATCH] = etag response = requests.head( url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True ) _LOGGER.debug( - "has_remote_file_changed: File %s, Local modified %s, response code %d", + "has_remote_file_changed: File %s, Local modified %s, ETag %s, response code %d", local_file_path, local_modification_time_str, + etag or "<none>", response.status_code, ) @@ -51,6 +117,8 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: "has_remote_file_changed: File not modified since %s", local_modification_time_str, ) + if (new_etag := response.headers.get(ETAG)) and new_etag != etag: + _write_etag(local_file_path, new_etag) return False _LOGGER.debug("has_remote_file_changed: File modified") return True @@ -112,7 +180,7 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by return path.read_bytes() raise cv.Invalid(f"Could not download from {url}: {e}") from e - path.parent.mkdir(parents=True, exist_ok=True) data = req.content - path.write_bytes(data) + write_file(path, data) + _write_etag(path, req.headers.get(ETAG)) return data diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index 4b0826db044..f4d268abe03 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -1,5 +1,6 @@ """Tests for external_files.py functions.""" +import os from pathlib import Path import time from unittest.mock import MagicMock, patch @@ -9,7 +10,54 @@ import requests from esphome import external_files from esphome.config_validation import Invalid -from esphome.core import CORE, TimePeriod +from esphome.core import CORE, EsphomeError, TimePeriod + + +def _seed_etag(cache_file: Path, etag: str) -> Path: + """Write an ETag sidecar with its mtime synced to the cache file's mtime, + matching the invariant that `_write_etag` enforces in production. + """ + sidecar = external_files._etag_sidecar_path(cache_file) + sidecar.write_text(etag) + file_mtime = int(cache_file.stat().st_mtime) + os.utime(sidecar, (file_mtime, file_mtime)) + return sidecar + + +@pytest.fixture +def mock_requests_head() -> MagicMock: + """Patch `external_files.requests.head` so the conditional HEAD-request + validator can be tested without doing real HTTP. + """ + with patch("esphome.external_files.requests.head") as m: + yield m + + +@pytest.fixture +def mock_requests_get() -> MagicMock: + """Patch `external_files.requests.get` so the download path can be + tested without doing real HTTP. + """ + with patch("esphome.external_files.requests.get") as m: + yield m + + +@pytest.fixture +def mock_has_remote_file_changed() -> MagicMock: + """Patch `external_files.has_remote_file_changed` so download tests can + control the conditional check independently from the GET path. + """ + with patch("esphome.external_files.has_remote_file_changed") as m: + yield m + + +@pytest.fixture +def mock_write_file() -> MagicMock: + """Patch `external_files.write_file` so atomic-write failures can be + injected without involving the real filesystem helper. + """ + with patch("esphome.external_files.write_file") as m: + yield m def test_compute_local_file_dir(setup_core: Path) -> None: @@ -88,9 +136,8 @@ def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None: assert result is False -@patch("esphome.external_files.requests.head") def test_has_remote_file_changed_not_modified( - mock_head: MagicMock, setup_core: Path + mock_requests_head: MagicMock, setup_core: Path ) -> None: """Test has_remote_file_changed returns False when file not modified.""" test_file = setup_core / "cached.txt" @@ -98,23 +145,23 @@ def test_has_remote_file_changed_not_modified( mock_response = MagicMock() mock_response.status_code = 304 - mock_head.return_value = mock_response + mock_response.headers = {} + mock_requests_head.return_value = mock_response url = "https://example.com/file.txt" result = external_files.has_remote_file_changed(url, test_file) assert result is False - mock_head.assert_called_once() + mock_requests_head.assert_called_once() - call_args = mock_head.call_args + call_args = mock_requests_head.call_args headers = call_args[1]["headers"] assert external_files.IF_MODIFIED_SINCE in headers assert external_files.CACHE_CONTROL in headers -@patch("esphome.external_files.requests.head") def test_has_remote_file_changed_modified( - mock_head: MagicMock, setup_core: Path + mock_requests_head: MagicMock, setup_core: Path ) -> None: """Test has_remote_file_changed returns True when file modified.""" test_file = setup_core / "cached.txt" @@ -122,7 +169,8 @@ def test_has_remote_file_changed_modified( mock_response = MagicMock() mock_response.status_code = 200 - mock_head.return_value = mock_response + mock_response.headers = {} + mock_requests_head.return_value = mock_response url = "https://example.com/file.txt" result = external_files.has_remote_file_changed(url, test_file) @@ -140,15 +188,16 @@ def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: assert result is True -@patch("esphome.external_files.requests.head") def test_has_remote_file_changed_network_error( - mock_head: MagicMock, setup_core: Path + mock_requests_head: MagicMock, setup_core: Path ) -> None: """Test has_remote_file_changed returns False on network error when file is cached.""" test_file = setup_core / "cached.txt" test_file.write_text("cached content") - mock_head.side_effect = requests.exceptions.RequestException("Network error") + mock_requests_head.side_effect = requests.exceptions.RequestException( + "Network error" + ) url = "https://example.com/file.txt" result = external_files.has_remote_file_changed(url, test_file) @@ -156,9 +205,8 @@ def test_has_remote_file_changed_network_error( assert result is False -@patch("esphome.external_files.requests.head") def test_has_remote_file_changed_timeout( - mock_head: MagicMock, setup_core: Path + mock_requests_head: MagicMock, setup_core: Path ) -> None: """Test has_remote_file_changed respects timeout.""" test_file = setup_core / "cached.txt" @@ -166,15 +214,176 @@ def test_has_remote_file_changed_timeout( mock_response = MagicMock() mock_response.status_code = 304 - mock_head.return_value = mock_response + mock_response.headers = {} + mock_requests_head.return_value = mock_response url = "https://example.com/file.txt" external_files.has_remote_file_changed(url, test_file) - call_args = mock_head.call_args + call_args = mock_requests_head.call_args assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT +def test_has_remote_file_changed_uses_etag( + mock_requests_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed sends If-None-Match when ETag is cached.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + _seed_etag(test_file, '"abc123"') + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_response.headers = {} + mock_requests_head.return_value = mock_response + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, test_file) + + assert result is False + headers = mock_requests_head.call_args[1]["headers"] + assert headers[external_files.IF_NONE_MATCH] == '"abc123"' + + +def test_has_remote_file_changed_no_etag_no_if_none_match( + mock_requests_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed omits If-None-Match when no ETag is cached.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_response.headers = {} + mock_requests_head.return_value = mock_response + + url = "https://example.com/file.txt" + external_files.has_remote_file_changed(url, test_file) + + headers = mock_requests_head.call_args[1]["headers"] + assert external_files.IF_NONE_MATCH not in headers + + +def test_has_remote_file_changed_refreshes_etag_on_304( + mock_requests_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed updates the cached ETag when the 304 sends a new one.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + _seed_etag(test_file, '"old"') + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_response.headers = {external_files.ETAG: '"new"'} + mock_requests_head.return_value = mock_response + + url = "https://example.com/file.txt" + external_files.has_remote_file_changed(url, test_file) + + assert external_files._etag_sidecar_path(test_file).read_text() == '"new"' + + +def test_has_remote_file_changed_ignores_etag_when_mtime_diverges( + mock_requests_head: MagicMock, setup_core: Path +) -> None: + """If the cache file was edited out-of-band (mtime no longer matches the + sidecar's), the cached ETag must not be used -- it no longer describes the + bytes on disk. + """ + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + sidecar = _seed_etag(test_file, '"abc123"') + + # Simulate an out-of-band edit to the cache file -- mtime advances by a + # full second (so it diverges at whole-second resolution) but the sidecar + # is left untouched, so the recorded ETag is now stale. + file_stat = test_file.stat() + os.utime(test_file, (file_stat.st_atime, file_stat.st_mtime + 1)) + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_response.headers = {} + mock_requests_head.return_value = mock_response + + external_files.has_remote_file_changed("https://example.com/file.txt", test_file) + + headers = mock_requests_head.call_args[1]["headers"] + assert external_files.IF_NONE_MATCH not in headers + # Stale sidecar should be removed so future calls don't keep paying the + # mtime-comparison cost on a known-bad sidecar. + assert not sidecar.exists() + + +def test_download_content_pins_etag_mtime_to_file_mtime( + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, +) -> None: + """After a successful download, the sidecar's mtime must equal the cache + file's mtime so `_read_etag` accepts it on the next call. + """ + test_file = setup_core / "fresh.txt" + mock_has_remote_file_changed.return_value = True + mock_response = MagicMock() + mock_response.content = b"fresh content" + mock_response.headers = {external_files.ETAG: '"deadbeef"'} + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + external_files.download_content("https://example.com/file.txt", test_file) + + sidecar = external_files._etag_sidecar_path(test_file) + assert int(sidecar.stat().st_mtime) == int(test_file.stat().st_mtime) + + +def test_write_etag_swallows_write_file_failure( + mock_write_file: MagicMock, setup_core: Path, caplog: pytest.LogCaptureFixture +) -> None: + """If `write_file` raises, _write_etag must not propagate -- ETag + persistence is best-effort and a failure here must not abort the + surrounding download. + """ + cache_file = setup_core / "cached.txt" + cache_file.write_text("cached content") + mock_write_file.side_effect = EsphomeError("disk full") + + with caplog.at_level("DEBUG", logger="esphome.external_files"): + external_files._write_etag(cache_file, '"abc123"') + + assert "Could not save ETag" in caplog.text + # Sidecar wasn't created, since write_file was mocked to fail before + # reaching the os.utime step. + assert not external_files._etag_sidecar_path(cache_file).exists() + + +def test_write_etag_swallows_utime_failure( + setup_core: Path, caplog: pytest.LogCaptureFixture +) -> None: + """If `os.utime` raises while pinning the sidecar's mtime, _write_etag + must not propagate. The sidecar is still written; if its mtime later + fails to match the cache file, `_read_etag` will discard it on next + read. + """ + cache_file = setup_core / "cached.txt" + cache_file.write_text("cached content") + + with ( + patch( + "esphome.external_files.os.utime", + side_effect=PermissionError("nope"), + ), + caplog.at_level("DEBUG", logger="esphome.external_files"), + ): + external_files._write_etag(cache_file, '"abc123"') + + assert "Could not sync ETag sidecar mtime" in caplog.text + # write_file succeeded, so the sidecar exists with the new value even + # though we couldn't pin its mtime. + sidecar = external_files._etag_sidecar_path(cache_file) + assert sidecar.exists() + assert sidecar.read_text() == '"abc123"' + + def test_compute_local_file_dir_creates_parent_dirs(setup_core: Path) -> None: """Test compute_local_file_dir creates parent directories.""" domain = "level1/level2/level3/level4" @@ -200,10 +409,10 @@ def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: assert result is True -@patch("esphome.external_files.requests.get") -@patch("esphome.external_files.has_remote_file_changed") def test_download_content_with_network_error_uses_cache( - mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, ) -> None: """Test download_content uses cached file when network fails.""" test_file = setup_core / "cached.txt" @@ -211,8 +420,10 @@ def test_download_content_with_network_error_uses_cache( test_file.write_bytes(cached_content) # Simulate file has changed, so it tries to download - mock_has_changed.return_value = True - mock_get.side_effect = requests.exceptions.RequestException("Network error") + mock_has_remote_file_changed.return_value = True + mock_requests_get.side_effect = requests.exceptions.RequestException( + "Network error" + ) url = "https://example.com/file.txt" result = external_files.download_content(url, test_file) @@ -220,17 +431,19 @@ def test_download_content_with_network_error_uses_cache( assert result == cached_content -@patch("esphome.external_files.requests.get") -@patch("esphome.external_files.has_remote_file_changed") def test_download_content_with_network_error_no_cache_fails( - mock_has_changed: MagicMock, mock_get: MagicMock, setup_core: Path + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, ) -> None: """Test download_content raises error when network fails and no cache exists.""" test_file = setup_core / "nonexistent.txt" # Simulate file has changed (doesn't exist), so it tries to download - mock_has_changed.return_value = True - mock_get.side_effect = requests.exceptions.RequestException("Network error") + mock_has_remote_file_changed.return_value = True + mock_requests_get.side_effect = requests.exceptions.RequestException( + "Network error" + ) url = "https://example.com/file.txt" @@ -238,11 +451,9 @@ def test_download_content_with_network_error_no_cache_fails( external_files.download_content(url, test_file) -@patch("esphome.external_files.requests.get") -@patch("esphome.external_files.has_remote_file_changed") def test_download_content_skip_external_update_uses_cache( - mock_has_changed: MagicMock, - mock_get: MagicMock, + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, setup_core: Path, ) -> None: """Test download_content skips network checks when CORE.skip_external_update is set.""" @@ -255,26 +466,25 @@ def test_download_content_skip_external_update_uses_cache( result = external_files.download_content(url, test_file) assert result == cached_content - mock_has_changed.assert_not_called() - mock_get.assert_not_called() + mock_has_remote_file_changed.assert_not_called() + mock_requests_get.assert_not_called() -@patch("esphome.external_files.requests.get") -@patch("esphome.external_files.has_remote_file_changed") def test_download_content_skip_external_update_downloads_when_missing( - mock_has_changed: MagicMock, - mock_get: MagicMock, + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, setup_core: Path, ) -> None: """Test download_content still downloads when file is missing, even with skip_external_update.""" test_file = setup_core / "missing.txt" new_content = b"fresh content" - mock_has_changed.return_value = True + mock_has_remote_file_changed.return_value = True mock_response = MagicMock() mock_response.content = new_content + mock_response.headers = {} mock_response.raise_for_status = MagicMock() - mock_get.return_value = mock_response + mock_requests_get.return_value = mock_response CORE.skip_external_update = True url = "https://example.com/file.txt" @@ -282,3 +492,62 @@ def test_download_content_skip_external_update_downloads_when_missing( assert result == new_content assert test_file.read_bytes() == new_content + + +def test_download_content_saves_etag( + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, +) -> None: + """Test download_content writes the ETag sidecar after a successful download.""" + test_file = setup_core / "fresh.txt" + new_content = b"fresh content" + + mock_has_remote_file_changed.return_value = True + mock_response = MagicMock() + mock_response.content = new_content + mock_response.headers = {external_files.ETAG: '"deadbeef"'} + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + url = "https://example.com/file.txt" + external_files.download_content(url, test_file) + + assert external_files._etag_sidecar_path(test_file).read_text() == '"deadbeef"' + + +def test_download_content_atomic_write_no_partial_on_failure( + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + mock_write_file: MagicMock, + setup_core: Path, +) -> None: + """If `write_file` (the atomic-write helper) fails, the existing cache + file must remain untouched and no temp files may be left behind. Patching + `write_file` directly exercises the atomic-rename path -- a failure inside + `write_file` is the only reason the rename wouldn't have happened. + """ + from esphome.core import EsphomeError + + test_file = setup_core / "cached.txt" + original_content = b"original content" + test_file.write_bytes(original_content) + + mock_has_remote_file_changed.return_value = True + mock_response = MagicMock() + mock_response.content = b"new content" + mock_response.headers = {} + mock_response.raise_for_status = MagicMock() + mock_requests_get.return_value = mock_response + + mock_write_file.side_effect = EsphomeError("disk full") + + with pytest.raises(EsphomeError, match="disk full"): + external_files.download_content("https://example.com/file.txt", test_file) + + # Original file is untouched -- write_file aborted before its rename step. + assert test_file.read_bytes() == original_content + # write_file is responsible for cleaning its own temp files; nothing leaks + # into the cache directory either way. + leftover_tmps = list(setup_core.glob("tmp*")) + assert leftover_tmps == [] From c3bd38af77ee103d7883a124e200c4fc14192ae6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:54:15 -0400 Subject: [PATCH 1735/2056] [feedback] Fix bugprone-unchecked-optional-access in start_direction_ (#16103) --- esphome/components/feedback/feedback_cover.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 672e99949b7..1139e6fa189 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -375,12 +375,10 @@ void FeedbackCover::start_direction_(CoverOperation dir) { // check if we have a wait time if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE && this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) { + const uint32_t waittime = *this->direction_change_waittime_; ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); this->start_direction_(COVER_OPERATION_IDLE); - - this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_, - [this, dir]() { this->start_direction_(dir); }); - + this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); }); } else { this->set_current_operation_(dir, true); this->prev_command_trigger_ = trig; From 592486ae9aef284062b552cc2995691fedc72f46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:06:54 -0500 Subject: [PATCH 1736/2056] [analyze_memory] Attribute main.cpp setup()/loop() to esphome core (#16033) --- esphome/analyze_memory/__init__.py | 19 +++++--- .../test_source_file_attribution.py | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests/unit_tests/analyze_memory/test_source_file_attribution.py diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index f56d720ec2a..33854ac2896 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -793,8 +793,11 @@ class MemoryAnalyzer: """Scan ESPHome source object files to map extern "C" symbols to components. When no linker map file is available, this uses ``nm`` to scan ``.o`` files - under ``src/esphome/`` and build a symbol-to-component mapping. This catches - ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + under ``src/`` (including ``src/main.cpp.o`` and everything beneath + ``src/esphome/``) and build a symbol-to-component mapping. This catches + ``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()`` + entry points in ``main.cpp``, and other symbols that lack C++ namespace + prefixes. Skips scanning if ``_source_symbol_map`` was already populated by ``_parse_map_file()``. @@ -806,12 +809,12 @@ class MemoryAnalyzer: if obj_dir is None: return - # Find ESPHome source object files - esphome_src_dir = obj_dir / "src" / "esphome" - if not esphome_src_dir.is_dir(): + # Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/... + src_dir = obj_dir / "src" + if not src_dir.is_dir(): return - obj_files = sorted(esphome_src_dir.rglob("*.o")) + obj_files = sorted(src_dir.rglob("*.o")) if not obj_files: return @@ -1064,6 +1067,10 @@ class MemoryAnalyzer: if component_name in self.external_components: return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + # ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop()) + if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"): + return _COMPONENT_CORE + # ESPHome core: src/esphome/core/... or src/esphome/... if "core" in parts and "esphome" in parts: return _COMPONENT_CORE diff --git a/tests/unit_tests/analyze_memory/test_source_file_attribution.py b/tests/unit_tests/analyze_memory/test_source_file_attribution.py new file mode 100644 index 00000000000..2793f41bd0e --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_source_file_attribution.py @@ -0,0 +1,43 @@ +"""Tests for source-file-to-component attribution in memory analyzer.""" + +from unittest.mock import patch + +from esphome.analyze_memory import MemoryAnalyzer + + +def _make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer: + """Create a MemoryAnalyzer with mocked dependencies.""" + with patch.object(MemoryAnalyzer, "__init__", lambda self, *a, **kw: None): + analyzer = MemoryAnalyzer.__new__(MemoryAnalyzer) + analyzer.external_components = external_components or set() + analyzer._lib_hash_to_name = {} + return analyzer + + +def test_source_file_to_component_main_cpp_relative() -> None: + """ESPHome-generated src/main.cpp.o (nm path form) attributes to core.""" + analyzer = _make_analyzer() + assert analyzer._source_file_to_component("src/main.cpp.o") == "[esphome]core" + + +def test_source_file_to_component_main_cpp_pioenvs_path() -> None: + """Linker map paths like .pioenvs/<env>/src/main.cpp.o attribute to core.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component(".pioenvs/drivewaygate/src/main.cpp.o") + assert result == "[esphome]core" + + +def test_source_file_to_component_esphome_core() -> None: + """Sources under src/esphome/core/ attribute to core.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component("src/esphome/core/application.cpp.o") + assert result == "[esphome]core" + + +def test_source_file_to_component_known_component() -> None: + """Known ESPHome components attribute to their component name.""" + analyzer = _make_analyzer() + result = analyzer._source_file_to_component( + "src/esphome/components/wifi/wifi_component.cpp.o" + ) + assert result == "[esphome]wifi" From d287876d8d94a56b1d53c22090e6358762a1b89a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:20:37 -0500 Subject: [PATCH 1737/2056] [light] Use bitmask template for LightControlAction unused fields (#16039) --- esphome/components/light/automation.h | 85 +++++++++++++------------- esphome/components/light/automation.py | 16 ++++- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index f6a2ca52d45..eda30bd786e 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -24,61 +24,60 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { LightState *state_; }; -template<typename... Ts> class LightControlAction : public Action<Ts...> { +// Unique Empty<Tag> per field so [[no_unique_address]] is guaranteed to coalesce. +namespace light_control_detail { +template<int Tag> struct Empty {}; +} // namespace light_control_detail + +// X-macro: (type, field_name, bit_index). Order and bit values must match +// the FIELDS table in automation.py. +#define LIGHT_CONTROL_FIELDS(X) \ + X(ColorMode, color_mode, 0) \ + X(bool, state, 1) \ + X(uint32_t, transition_length, 2) \ + X(uint32_t, flash_length, 3) \ + X(float, brightness, 4) \ + X(float, color_brightness, 5) \ + X(float, red, 6) \ + X(float, green, 7) \ + X(float, blue, 8) \ + X(float, white, 9) \ + X(float, color_temperature, 10) \ + X(float, cold_white, 11) \ + X(float, warm_white, 12) \ + X(uint32_t, effect, 13) + +template<uint16_t Fields, typename... Ts> class LightControlAction : public Action<Ts...> { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} - TEMPLATABLE_VALUE(ColorMode, color_mode) - TEMPLATABLE_VALUE(bool, state) - TEMPLATABLE_VALUE(uint32_t, transition_length) - TEMPLATABLE_VALUE(uint32_t, flash_length) - TEMPLATABLE_VALUE(float, brightness) - TEMPLATABLE_VALUE(float, color_brightness) - TEMPLATABLE_VALUE(float, red) - TEMPLATABLE_VALUE(float, green) - TEMPLATABLE_VALUE(float, blue) - TEMPLATABLE_VALUE(float, white) - TEMPLATABLE_VALUE(float, color_temperature) - TEMPLATABLE_VALUE(float, cold_white) - TEMPLATABLE_VALUE(float, warm_white) - TEMPLATABLE_VALUE(uint32_t, effect) +#define LIGHT_FIELD_SETTER_(type, name, idx) \ + template<typename V> void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; } +#define LIGHT_FIELD_APPLY_(type, name, idx) \ + if constexpr ((Fields & (1 << (idx))) != 0) \ + call.set_##name(this->name##_.value(x...)); +#define LIGHT_FIELD_DECL_(type, name, idx) \ + [[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableFn<type, Ts...>, \ + light_control_detail::Empty<(idx)>> \ + name##_{}; + + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) void play(const Ts &...x) override { auto call = this->parent_->make_call(); - if (this->color_mode_.has_value()) - call.set_color_mode(this->color_mode_.value(x...)); - if (this->state_.has_value()) - call.set_state(this->state_.value(x...)); - if (this->transition_length_.has_value()) - call.set_transition_length(this->transition_length_.value(x...)); - if (this->flash_length_.has_value()) - call.set_flash_length(this->flash_length_.value(x...)); - if (this->brightness_.has_value()) - call.set_brightness(this->brightness_.value(x...)); - if (this->color_brightness_.has_value()) - call.set_color_brightness(this->color_brightness_.value(x...)); - if (this->red_.has_value()) - call.set_red(this->red_.value(x...)); - if (this->green_.has_value()) - call.set_green(this->green_.value(x...)); - if (this->blue_.has_value()) - call.set_blue(this->blue_.value(x...)); - if (this->white_.has_value()) - call.set_white(this->white_.value(x...)); - if (this->color_temperature_.has_value()) - call.set_color_temperature(this->color_temperature_.value(x...)); - if (this->cold_white_.has_value()) - call.set_cold_white(this->cold_white_.value(x...)); - if (this->warm_white_.has_value()) - call.set_warm_white(this->warm_white_.value(x...)); - if (this->effect_.has_value()) - call.set_effect(this->effect_.value(x...)); + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) call.perform(); } protected: LightState *parent_; + LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) + +#undef LIGHT_FIELD_DECL_ +#undef LIGHT_FIELD_APPLY_ +#undef LIGHT_FIELD_SETTER_ }; +#undef LIGHT_CONTROL_FIELDS template<typename... Ts> class DimRelativeAction : public Action<Ts...> { public: diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 46d37239e5e..ea953e31999 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -178,9 +178,9 @@ def _resolve_effect_index(config: ConfigType) -> int: ) async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - # (config_key, setter_name, c++ type) + # Order/bits must match LIGHT_CONTROL_FIELDS in automation.h. + # EFFECT has special handling below; setter=None skips the generic loop. FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), (CONF_STATE, "set_state", cg.bool_), @@ -195,9 +195,19 @@ async def light_control_to_code(config, action_id, template_arg, args): (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), (CONF_COLD_WHITE, "set_cold_white", cg.float_), (CONF_WARM_WHITE, "set_warm_white", cg.float_), + (CONF_EFFECT, None, cg.uint32), ) + # Bitmask is passed as uint16_t in C++ — must stay within 16 bits. + assert len(FIELDS) <= 16, "LightControlAction Fields bitmask exceeds uint16_t" + + field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) + control_template_arg = cg.TemplateArguments( + cg.RawExpression(f"static_cast<uint16_t>({field_mask})"), *template_arg + ) + var = cg.new_Pvariable(action_id, control_template_arg, paren) + for conf_key, setter, type_ in FIELDS: - if conf_key in config: + if conf_key in config and setter is not None: template_ = await cg.templatable(config[conf_key], args, type_) cg.add(getattr(var, setter)(template_)) From 0d150dc57e53363bd21fe15da0da785f019ff584 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:25:18 -0500 Subject: [PATCH 1738/2056] [light] Use constexpr template for ToggleAction transition_length (#16037) --- esphome/components/light/automation.h | 13 +++- esphome/components/light/automation.py | 6 +- .../fixtures/light_toggle_action.yaml | 37 ++++++++++ tests/integration/test_light_toggle_action.py | 67 +++++++++++++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/light_toggle_action.yaml create mode 100644 tests/integration/test_light_toggle_action.py diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index eda30bd786e..8aee9b5dad1 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -8,20 +8,27 @@ namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; -template<typename... Ts> class ToggleAction : public Action<Ts...> { +template<bool HasTransitionLength, typename... Ts> class ToggleAction : public Action<Ts...> { public: explicit ToggleAction(LightState *state) : state_(state) {} - TEMPLATABLE_VALUE(uint32_t, transition_length) + template<typename V> void set_transition_length(V value) requires(HasTransitionLength) { + this->transition_length_ = value; + } void play(const Ts &...x) override { auto call = this->state_->toggle(); - call.set_transition_length(this->transition_length_.optional_value(x...)); + if constexpr (HasTransitionLength) { + call.set_transition_length(this->transition_length_.optional_value(x...)); + } call.perform(); } protected: LightState *state_; + struct NoTransition {}; + [[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition> + transition_length_{}; }; // Unique Empty<Tag> per field so [[no_unique_address]] is guaranteed to coalesce. diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index ea953e31999..389a6c4f58d 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -60,8 +60,10 @@ from .types import ( ) async def light_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_TRANSITION_LENGTH in config: + has_transition_length = CONF_TRANSITION_LENGTH in config + toggle_template_arg = cg.TemplateArguments(has_transition_length, *template_arg) + var = cg.new_Pvariable(action_id, toggle_template_arg, paren) + if has_transition_length: template_ = await cg.templatable( config[CONF_TRANSITION_LENGTH], args, cg.uint32 ) diff --git a/tests/integration/fixtures/light_toggle_action.yaml b/tests/integration/fixtures/light_toggle_action.yaml new file mode 100644 index 00000000000..265d8ba1acb --- /dev/null +++ b/tests/integration/fixtures/light_toggle_action.yaml @@ -0,0 +1,37 @@ +esphome: + name: light-toggle-action-test +host: +api: +logger: + level: DEBUG + +output: + - platform: template + id: test_out + type: float + write_action: + - lambda: "" + +light: + - platform: monochromatic + name: "Test Light" + id: test_light + output: test_out + default_transition_length: 0s + +button: + # Test 1: light.toggle without transition_length (HasTransitionLength=false) + - platform: template + id: btn_toggle + name: "Toggle" + on_press: + - light.toggle: test_light + + # Test 2: light.toggle with transition_length (HasTransitionLength=true) + - platform: template + id: btn_toggle_with_trans + name: "Toggle With Trans" + on_press: + - light.toggle: + id: test_light + transition_length: 0s diff --git a/tests/integration/test_light_toggle_action.py b/tests/integration/test_light_toggle_action.py new file mode 100644 index 00000000000..ffbadabb5bd --- /dev/null +++ b/tests/integration/test_light_toggle_action.py @@ -0,0 +1,67 @@ +"""Integration test for light::ToggleAction. + +Tests both ToggleAction<HasTransitionLength=false> and +ToggleAction<HasTransitionLength=true> instantiations. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_toggle_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test light.toggle with and without transition_length.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + light_state_future: asyncio.Future[LightState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, LightState) + and light_state_future is not None + and not light_state_future.done() + ): + light_state_future.set_result(state) + + async def wait_for_light_state(timeout: float = 5.0) -> LightState: + nonlocal light_state_future + light_state_future = loop.create_future() + try: + return await asyncio.wait_for(light_state_future, timeout) + finally: + light_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_light", LightInfo) + + async def press_and_wait(name: str) -> LightState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_light_state() + + # Test 1: toggle without transition_length flips off->on + state = await press_and_wait("Toggle") + assert state.state is True + + # Test 2: toggle with transition_length flips on->off + state = await press_and_wait("Toggle With Trans") + assert state.state is False + + # Test 3: toggle without transition_length flips off->on again + state = await press_and_wait("Toggle") + assert state.state is True From 5a33c5001555875d77b7fa228d0dc5785ce9e53f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:26:38 -0500 Subject: [PATCH 1739/2056] [light] Use constexpr template for DimRelativeAction transition_length (#16038) --- esphome/components/light/automation.h | 14 +++- esphome/components/light/automation.py | 6 +- tests/components/light/common.yaml | 4 ++ .../fixtures/light_dim_relative_action.yaml | 60 ++++++++++++++++ .../test_light_dim_relative_action.py | 72 +++++++++++++++++++ 5 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/light_dim_relative_action.yaml create mode 100644 tests/integration/test_light_dim_relative_action.py diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 8aee9b5dad1..bc6fd847099 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -86,12 +86,15 @@ template<uint16_t Fields, typename... Ts> class LightControlAction : public Acti }; #undef LIGHT_CONTROL_FIELDS -template<typename... Ts> class DimRelativeAction : public Action<Ts...> { +template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> { public: explicit DimRelativeAction(LightState *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, relative_brightness) - TEMPLATABLE_VALUE(uint32_t, transition_length) + + template<typename V> void set_transition_length(V value) requires(HasTransitionLength) { + this->transition_length_ = value; + } void play(const Ts &...x) override { auto call = this->parent_->make_call(); @@ -105,7 +108,9 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> { call.set_state(new_brightness != 0.0f); call.set_brightness(new_brightness); - call.set_transition_length(this->transition_length_.optional_value(x...)); + if constexpr (HasTransitionLength) { + call.set_transition_length(this->transition_length_.optional_value(x...)); + } call.perform(); } @@ -121,6 +126,9 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> { float min_brightness_{0.0}; float max_brightness_{1.0}; LimitMode limit_mode_{LimitMode::CLAMP}; + struct NoTransition {}; + [[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition> + transition_length_{}; }; template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 389a6c4f58d..c666c98e42f 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -273,10 +273,12 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( ) async def light_dim_relative_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + has_transition_length = CONF_TRANSITION_LENGTH in config + dim_template_arg = cg.TemplateArguments(has_transition_length, *template_arg) + var = cg.new_Pvariable(action_id, dim_template_arg, paren) templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_) cg.add(var.set_relative_brightness(templ)) - if CONF_TRANSITION_LENGTH in config: + if has_transition_length: templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) cg.add(var.set_transition_length(templ)) if conf := config.get(CONF_BRIGHTNESS_LIMITS): diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index e1216e7b60b..e58f7baee4b 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -108,6 +108,10 @@ esphome: relative_brightness: 5% brightness_limits: max_brightness: 90% + - light.dim_relative: + id: test_monochromatic_light + relative_brightness: -5% + transition_length: 250ms - light.turn_on: id: test_addressable_transition brightness: 50% diff --git a/tests/integration/fixtures/light_dim_relative_action.yaml b/tests/integration/fixtures/light_dim_relative_action.yaml new file mode 100644 index 00000000000..b52cf65b89c --- /dev/null +++ b/tests/integration/fixtures/light_dim_relative_action.yaml @@ -0,0 +1,60 @@ +esphome: + name: light-dim-relative-action-test +host: +api: +logger: + level: DEBUG + +output: + - platform: template + id: test_out + type: float + write_action: + - lambda: "" + +light: + - platform: monochromatic + name: "Test Light" + id: test_light + output: test_out + default_transition_length: 0s + +button: + # Set up: turn on at 50% brightness + - platform: template + id: btn_setup + name: "Setup" + on_press: + - light.turn_on: + id: test_light + brightness: 50% + + # Test 1: dim_relative without transition_length (HasTransitionLength=false) + - platform: template + id: btn_dim_up + name: "Dim Up" + on_press: + - light.dim_relative: + id: test_light + relative_brightness: 25% + + # Test 2: dim_relative with transition_length (HasTransitionLength=true) + - platform: template + id: btn_dim_down + name: "Dim Down" + on_press: + - light.dim_relative: + id: test_light + relative_brightness: -10% + transition_length: 0s + + # Test 3: dim_relative with brightness limits + - platform: template + id: btn_dim_clamp + name: "Dim Clamp" + on_press: + - light.dim_relative: + id: test_light + relative_brightness: 50% + brightness_limits: + max_brightness: 80% diff --git a/tests/integration/test_light_dim_relative_action.py b/tests/integration/test_light_dim_relative_action.py new file mode 100644 index 00000000000..d5078f44092 --- /dev/null +++ b/tests/integration/test_light_dim_relative_action.py @@ -0,0 +1,72 @@ +"""Integration test for light::DimRelativeAction. + +Tests both DimRelativeAction<HasTransitionLength=false> and +DimRelativeAction<HasTransitionLength=true> instantiations. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_dim_relative_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test light.dim_relative with and without transition_length.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + light_state_future: asyncio.Future[LightState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, LightState) + and light_state_future is not None + and not light_state_future.done() + ): + light_state_future.set_result(state) + + async def wait_for_light_state(timeout: float = 5.0) -> LightState: + nonlocal light_state_future + light_state_future = loop.create_future() + try: + return await asyncio.wait_for(light_state_future, timeout) + finally: + light_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_light", LightInfo) + + async def press_and_wait(name: str) -> LightState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_light_state() + + # Setup: turn on at 50% + state = await press_and_wait("Setup") + assert state.state is True + assert state.brightness == pytest.approx(0.5, abs=0.05) + + # Test 1: dim_relative without transition_length: 50% + 25% = 75% + state = await press_and_wait("Dim Up") + assert state.brightness == pytest.approx(0.75, abs=0.05) + + # Test 2: dim_relative with transition_length: 75% - 10% = 65% + state = await press_and_wait("Dim Down") + assert state.brightness == pytest.approx(0.65, abs=0.05) + + # Test 3: dim_relative with max_brightness limit: 65% + 50% clamped to 80% + state = await press_and_wait("Dim Clamp") + assert state.brightness == pytest.approx(0.80, abs=0.05) From 0d51a122d05c77156fcc4f6c9bd025aaf3cf4205 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:27:40 -0500 Subject: [PATCH 1740/2056] [cover] Add cover.control / cover.template.publish coverage to template tests (#16051) --- tests/components/template/common-base.yaml | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index ecc65de66cc..daa6f53d42f 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -293,6 +293,60 @@ cover: cover.is_closed: template_cover_with_triggers then: logger.log: Cover is closed + # Exercise cover.control / cover.template.publish action variants so they + # get build coverage in CI (and so memory-impact analysis on PRs that + # touch ControlAction / CoverPublishAction sees real instances). + - platform: template + name: "Template Cover Actions" + id: template_cover_actions + has_position: true + optimistic: true + open_action: + # CONF_STATE alias for the position bit + - cover.template.publish: + id: template_cover_actions + state: OPEN + - cover.template.publish: + id: template_cover_actions + position: 1.0 + - cover.template.publish: + id: template_cover_actions + current_operation: IDLE + close_action: + - cover.template.publish: + id: template_cover_actions + position: 0.0 + tilt: 0.0 + stop_action: + - cover.template.publish: + id: template_cover_actions + current_operation: IDLE + tilt_action: + - lambda: |- + id(template_cover_actions).tilt = tilt; + id(template_cover_actions).publish_state(); + on_idle: + # position only + - cover.control: + id: template_cover_actions + position: 50% + # tilt only + - cover.control: + id: template_cover_actions + tilt: 75% + # position + tilt + - cover.control: + id: template_cover_actions + position: 25% + tilt: 30% + # stop + - cover.control: + id: template_cover_actions + stop: true + # CONF_STATE alias for position + - cover.control: + id: template_cover_actions + state: OPEN number: - platform: template From 80251c54bedec89c5966972582aa09cd45ac2402 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:27:56 -0500 Subject: [PATCH 1741/2056] [climate] Add climate.control coverage to component tests via thermostat (#16052) --- tests/components/climate/common.yaml | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/components/climate/common.yaml b/tests/components/climate/common.yaml index ff405b68e22..2d35438afd2 100644 --- a/tests/components/climate/common.yaml +++ b/tests/components/climate/common.yaml @@ -29,3 +29,59 @@ climate: heat_action: - switch.turn_on: climate_heater_switch - switch.turn_off: climate_cooler_switch + # Thermostat-based climate so climate.control: action variants get build + # coverage (bang_bang doesn't support fan modes, presets, etc.). Climate + # has no template platform, so thermostat is the right vehicle. + - platform: thermostat + id: climate_test_thermostat + name: Test Thermostat + sensor: climate_temperature_sensor + min_idle_time: 30s + min_heating_off_time: 300s + min_heating_run_time: 300s + min_cooling_off_time: 300s + min_cooling_run_time: 300s + heat_action: + - logger.log: heating + idle_action: + - logger.log: idle + cool_action: + - logger.log: cooling + auto_mode: + - logger.log: auto + heat_cool_mode: + - logger.log: heat_cool + preset: + - name: Default + default_target_temperature_low: 18°C + default_target_temperature_high: 22°C + +button: + # Exercise the climate.control: action so ControlAction templates get + # build coverage. Various field combinations are tested. + - platform: template + name: "Climate Control Mode" + on_press: + - climate.control: + id: climate_test_thermostat + mode: HEAT + - platform: template + name: "Climate Control Mode And Temps" + on_press: + - climate.control: + id: climate_test_thermostat + mode: HEAT_COOL + target_temperature_low: 19.0°C + target_temperature_high: 23.0°C + - platform: template + name: "Climate Control Lambda Temp" + on_press: + - climate.control: + id: climate_test_thermostat + target_temperature_high: !lambda "return 21.5;" + - platform: template + name: "Climate Control Off" + on_press: + - climate.control: + id: climate_test_thermostat + mode: "OFF" From 2fce71e0d4004bb01f0c92581c60da70acd22dc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:31:07 -0500 Subject: [PATCH 1742/2056] [wifi] Add phy_mode option for ESP8266 (#16055) --- esphome/components/wifi/__init__.py | 16 +++++++++++++++ esphome/components/wifi/wifi_component.cpp | 15 ++++++++++++++ esphome/components/wifi/wifi_component.h | 20 +++++++++++++++++++ .../wifi/wifi_component_esp8266.cpp | 15 ++++++++++++++ esphome/core/defines.h | 1 + tests/components/wifi/test.esp8266-ard.yaml | 1 + 6 files changed, 68 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index bc4e177219a..69544f36367 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -73,6 +73,7 @@ NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" +CONF_PHY_MODE = "phy_mode" CONF_POST_CONNECT_ROAMING = "post_connect_roaming" # Maximum number of WiFi networks that can be configured @@ -112,6 +113,14 @@ WIFI_MIN_AUTH_MODES = { "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, } VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) + +WiFi8266PhyMode = wifi_ns.enum("WiFi8266PhyMode") +WIFI_8266_PHY_MODES = { + "AUTO": WiFi8266PhyMode.WIFI_8266_PHY_MODE_AUTO, + "11B": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11B, + "11G": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11G, + "11N": WiFi8266PhyMode.WIFI_8266_PHY_MODE_11N, +} WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) @@ -406,6 +415,10 @@ CONFIG_SCHEMA = cv.All( cv.only_on_esp32, only_on_variant(supported=[const.VARIANT_ESP32C5]), ), + cv.Optional(CONF_PHY_MODE): cv.All( + cv.enum(WIFI_8266_PHY_MODES, upper=True), + cv.only_on_esp8266, + ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, @@ -569,6 +582,9 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + if CONF_PHY_MODE in config: + cg.add_define("USE_WIFI_PHY_MODE") + cg.add(var.set_phy_mode(config[CONF_PHY_MODE])) elif CORE.is_rp2040: cg.add_library("WiFi", None) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1da2d630c10..edfb93bba24 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -309,6 +309,18 @@ bool CompactString::operator==(const StringRef &other) const { /// └──────────────────────────────────────────────────────────────────────┘ #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO +#ifdef USE_WIFI_PHY_MODE +// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) +static const LogString *phy_mode_to_log_string(WiFi8266PhyMode mode) { + if (mode == WIFI_8266_PHY_MODE_11B) + return LOG_STR("11B"); + if (mode == WIFI_8266_PHY_MODE_11G) + return LOG_STR("11G"); + if (mode == WIFI_8266_PHY_MODE_11N) + return LOG_STR("11N"); + return LOG_STR("Auto"); +} +#endif // Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { if (phase == WiFiRetryPhase::INITIAL_CONNECT) @@ -1535,6 +1547,9 @@ void WiFiComponent::dump_config() { break; } ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s); +#endif +#ifdef USE_WIFI_PHY_MODE + ESP_LOGCONFIG(TAG, " PHY Mode: %s", LOG_STR_ARG(phy_mode_to_log_string(this->phy_mode_))); #endif if (this->is_connected()) { this->print_connect_params_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 53fb0728fb7..0437267a1f0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -345,6 +345,17 @@ enum WifiMinAuthMode : uint8_t { WIFI_MIN_AUTH_MODE_WPA3, }; +#ifdef USE_WIFI_PHY_MODE +// Values 1-3 match ESP8266 SDK phy_mode_t (PHY_MODE_11B=1, PHY_MODE_11G=2, PHY_MODE_11N=3). +// AUTO leaves the SDK at its default (no wifi_set_phy_mode() call). +enum WiFi8266PhyMode : uint8_t { + WIFI_8266_PHY_MODE_AUTO = 0, + WIFI_8266_PHY_MODE_11B = 1, + WIFI_8266_PHY_MODE_11G = 2, + WIFI_8266_PHY_MODE_11N = 3, +}; +#endif + #ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -455,6 +466,9 @@ class WiFiComponent final : public Component { #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } #endif +#ifdef USE_WIFI_PHY_MODE + void set_phy_mode(WiFi8266PhyMode phy_mode) { this->phy_mode_ = phy_mode; } +#endif void set_passive_scan(bool passive); @@ -672,6 +686,9 @@ class WiFiComponent final : public Component { bool wifi_apply_power_save_(); #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) bool wifi_apply_band_mode_(); +#endif +#ifdef USE_WIFI_PHY_MODE + bool wifi_apply_phy_mode_(); #endif bool wifi_sta_ip_config_(const optional<ManualIP> &manual_ip); bool wifi_apply_hostname_(); @@ -810,6 +827,9 @@ class WiFiComponent final : public Component { WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; +#endif +#ifdef USE_WIFI_PHY_MODE + WiFi8266PhyMode phy_mode_{WIFI_8266_PHY_MODE_AUTO}; #endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 402ca051cde..717d542fbef 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -621,10 +621,25 @@ bool WiFiComponent::wifi_sta_pre_setup_() { ESP_LOGV(TAG, "Disabling Auto-Connect failed"); } +#ifdef USE_WIFI_PHY_MODE + if (!this->wifi_apply_phy_mode_()) { + ESP_LOGV(TAG, "Setting PHY Mode failed"); + } +#endif + delay(10); return true; } +#ifdef USE_WIFI_PHY_MODE +bool WiFiComponent::wifi_apply_phy_mode_() { + if (this->phy_mode_ == WIFI_8266_PHY_MODE_AUTO) + return true; + // Values of WiFi8266PhyMode are aligned with the SDK's phy_mode_t enum. + return wifi_set_phy_mode(static_cast<phy_mode_t>(this->phy_mode_)); +} +#endif + void WiFiComponent::wifi_pre_setup_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 99ec936c12c..93f4307e129 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -297,6 +297,7 @@ #define USE_CAPTIVE_PORTAL_GZIP #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_PHY_MODE #define USE_WIFI_IP_STATE_LISTENERS #define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index 709a639ad6f..ffeec136d3a 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1,6 +1,7 @@ wifi: min_auth_mode: WPA2 post_connect_roaming: true + phy_mode: 11G packages: - !include common.yaml From 49c7a6928e8a80566d7dfac957b868578a7bca73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:32:13 -0500 Subject: [PATCH 1743/2056] [script] Fix cpp_unit_test crash for non-MULTI_CONF platform components (#16104) --- script/build_helpers.py | 79 ++++++++++----- tests/script/test_test_helpers.py | 158 ++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 25 deletions(-) diff --git a/script/build_helpers.py b/script/build_helpers.py index 4cf2f93fbb6..0e0e8170a0a 100644 --- a/script/build_helpers.py +++ b/script/build_helpers.py @@ -57,6 +57,59 @@ def hash_components(components: list[str]) -> str: return hashlib.sha256(key.encode()).hexdigest()[:16] +def populate_dependency_config( + config: dict, + component_names: list[str], + *, + get_component_fn: Callable[[str], object | None] = get_component, + register_platform_fn: Callable[[str], None] | None = None, +) -> None: + """Populate ``config`` with empty entries for transitive dependencies. + + For every name in ``component_names``: + + * ``domain.platform`` form (e.g. ``sensor.gpio``) appends + ``{platform: <name>}`` to ``config[domain]``, creating the list if needed. + * Bare components are looked up via ``get_component_fn``. Platform + components (``IS_PLATFORM_COMPONENT``) and ``MULTI_CONF`` components are + initialised as ``[]`` so the sibling ``domain.platform`` branch can + ``append`` into them. Everything else is populated by running the + component's schema with ``{}`` so defaults exist; if the schema requires + explicit input, an empty ``{}`` is used as a fallback. + + Platform components must always be a list here even when no + ``domain.platform`` entry follows, because the ``domain.platform`` branch + does ``config.setdefault(domain, []).append(...)`` and would crash on a + leftover dict. + """ + if register_platform_fn is None: + register_platform_fn = CORE.testing_ensure_platform_registered + for component_name in component_names: + if "." in component_name: + domain, component = component_name.split(".", maxsplit=1) + domain_list = config.setdefault(domain, []) + register_platform_fn(domain) + domain_list.append({CONF_PLATFORM: component}) + continue + # Skip "core" — it's a pseudo-component handled by the build + # system, not a real loadable component (get_component returns None) + component = get_component_fn(component_name) + if component is None: + continue + if component.multi_conf or component.is_platform_component: + config.setdefault(component_name, []) + elif component_name not in config: + schema = component.config_schema + try: + config[component_name] = schema({}) if schema is not None else {} + except Exception: # noqa: BLE001 + # Schema requires explicit input we can't synthesize; fall + # back to an empty mapping so subscripting at least returns + # KeyError on missing keys rather than crashing on the + # wrong type. + config[component_name] = {} + + def filter_components_with_files(components: list[str], tests_dir: Path) -> list[str]: """Filter out components that do not have .cpp or .h files in the tests dir. @@ -316,31 +369,7 @@ def compile_and_get_binary( # Add remaining components and dependencies to the configuration after # validation, so their source files are included in the build. - for component_name in components_with_dependencies: - if "." in component_name: - domain, component = component_name.split(".", maxsplit=1) - domain_list = config.setdefault(domain, []) - CORE.testing_ensure_platform_registered(domain) - domain_list.append({CONF_PLATFORM: component}) - # Skip "core" — it's a pseudo-component handled by the build - # system, not a real loadable component (get_component returns None) - elif (component := get_component(component_name)) is not None: - # MULTI_CONF components store their config as a list of dicts, - # everything else stores a single dict. Run the component's - # schema with {} so defaults get populated -- code paths like - # socket.FILTER_SOURCE_FILES expect a fully-populated mapping. - if component.multi_conf: - config.setdefault(component_name, []) - elif component_name not in config: - schema = component.config_schema - try: - config[component_name] = schema({}) if schema is not None else {} - except Exception: # noqa: BLE001 - # Schema requires explicit input we can't synthesize; fall - # back to an empty mapping so subscripting at least returns - # KeyError on missing keys rather than crashing on the - # wrong type. - config[component_name] = {} + populate_dependency_config(config, components_with_dependencies) # Register platforms from the extra config (benchmark.yaml) so # USE_SENSOR, USE_LIGHT, etc. defines are emitted without needing diff --git a/tests/script/test_test_helpers.py b/tests/script/test_test_helpers.py index 467940fc33d..31497125632 100644 --- a/tests/script/test_test_helpers.py +++ b/tests/script/test_test_helpers.py @@ -258,3 +258,161 @@ def test_load_wraps_platform_component(tmp_path: Path) -> None: assert key == "bthome.sensor" assert isinstance(installed, ComponentManifestOverride) assert installed.to_code is None + + +# --------------------------------------------------------------------------- +# populate_dependency_config +# --------------------------------------------------------------------------- + + +def _make_component_stub( + *, + multi_conf: bool = False, + is_platform_component: bool = False, + config_schema=None, +) -> MagicMock: + stub = MagicMock() + stub.multi_conf = multi_conf + stub.is_platform_component = is_platform_component + stub.config_schema = config_schema + return stub + + +def test_populate_platform_component_listed_alone_uses_list() -> None: + """Regression: a platform component (sensor) with no `sensor.x` siblings + must land as `[]` in config. Previously it was populated as a dict via + `schema({})`, which then crashed the sibling `domain.platform` branch + when later dependencies tried `config.setdefault('sensor', []).append(...)`. + """ + sensor = _make_component_stub(is_platform_component=True) + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["sensor"], + get_component_fn=lambda name: sensor if name == "sensor" else None, + register_platform_fn=lambda _: None, + ) + + assert config["sensor"] == [] + + +def test_populate_platform_component_then_platform_entry() -> None: + """When `sensor` is processed before `sensor.gpio` (sorted order), + the bare-component branch must leave `config['sensor']` as a list so + the platform-entry branch can append into it. + """ + sensor = _make_component_stub(is_platform_component=True) + gpio = _make_component_stub() # the bare `gpio` component + components: dict[str, object] = {"sensor": sensor, "gpio": gpio} + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["gpio", "sensor", "sensor.gpio"], + get_component_fn=components.get, + register_platform_fn=lambda _: None, + ) + + assert config["sensor"] == [{"platform": "gpio"}] + + +def test_populate_multi_conf_component_uses_list() -> None: + multi = _make_component_stub(multi_conf=True) + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["multi"], + get_component_fn=lambda name: multi if name == "multi" else None, + register_platform_fn=lambda _: None, + ) + + assert config["multi"] == [] + + +def test_populate_plain_component_uses_schema_defaults() -> None: + schema = MagicMock(return_value={"default_key": 42}) + plain = _make_component_stub(config_schema=schema) + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["plain"], + get_component_fn=lambda name: plain if name == "plain" else None, + register_platform_fn=lambda _: None, + ) + + schema.assert_called_once_with({}) + assert config["plain"] == {"default_key": 42} + + +def test_populate_plain_component_falls_back_when_schema_raises() -> None: + def picky_schema(_): + raise ValueError("required field missing") + + plain = _make_component_stub(config_schema=picky_schema) + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["plain"], + get_component_fn=lambda name: plain if name == "plain" else None, + register_platform_fn=lambda _: None, + ) + + assert config["plain"] == {} + + +def test_populate_skips_unresolvable_pseudo_components() -> None: + """`core` and other names that get_component returns None for are skipped + silently without inserting anything into the config. + """ + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["core"], + get_component_fn=lambda _: None, + register_platform_fn=lambda _: None, + ) + + assert config == {} + + +def test_populate_preserves_existing_plain_component_config() -> None: + """If a plain component already has a config entry (e.g. from the user's + YAML), the schema-defaults branch must not overwrite it. + """ + schema = MagicMock() + plain = _make_component_stub(config_schema=schema) + config: dict = {"plain": {"user_key": "set_by_user"}} + + build_helpers.populate_dependency_config( + config, + ["plain"], + get_component_fn=lambda name: plain if name == "plain" else None, + register_platform_fn=lambda _: None, + ) + + schema.assert_not_called() + assert config["plain"] == {"user_key": "set_by_user"} + + +def test_populate_registers_platform_for_platform_entry() -> None: + """Each `domain.platform` entry triggers register_platform_fn(domain) so + USE_<DOMAIN> defines get emitted later in the build pipeline. + """ + registered: list[str] = [] + config: dict = {} + + build_helpers.populate_dependency_config( + config, + ["sensor.gpio", "binary_sensor.gpio"], + get_component_fn=lambda _: None, + register_platform_fn=registered.append, + ) + + assert registered == ["sensor", "binary_sensor"] + assert config["sensor"] == [{"platform": "gpio"}] + assert config["binary_sensor"] == [{"platform": "gpio"}] From 8ceada8d04a3b2fa9a428ce3ad5c7e68e7311404 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 28 Apr 2026 21:32:30 -0500 Subject: [PATCH 1744/2056] [core] Download external_files in parallel (#16021) --- esphome/components/audio_file/__init__.py | 18 +- .../speaker/media_player/__init__.py | 24 +-- esphome/external_files.py | 98 ++++++++- tests/unit_tests/test_external_files.py | 187 +++++++++++++++++- 4 files changed, 295 insertions(+), 32 deletions(-) diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py index bb1ce257db4..88be6db168a 100644 --- a/esphome/components/audio_file/__init__.py +++ b/esphome/components/audio_file/__init__.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from functools import partial import hashlib import logging from pathlib import Path @@ -19,7 +20,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, HexInt from esphome.cpp_generator import MockObj -from esphome.external_files import download_content +from esphome.external_files import download_web_files_in_config from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -63,15 +64,6 @@ def _compute_local_file_path(value: ConfigType) -> Path: return base_dir / key -def _download_web_file(value: ConfigType) -> ConfigType: - url = value[CONF_URL] - path = _compute_local_file_path(value) - - download_content(url, path) - _LOGGER.debug("download_web_file: path=%s", path) - return value - - def _file_schema(value: ConfigType | str) -> ConfigType: if isinstance(value, str): return _validate_file_shorthand(value) @@ -142,11 +134,10 @@ LOCAL_SCHEMA = cv.Schema( } ) -WEB_SCHEMA = cv.All( +WEB_SCHEMA = cv.Schema( { cv.Required(CONF_URL): cv.url, - }, - _download_web_file, + } ) @@ -209,6 +200,7 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType] CONFIG_SCHEMA = cv.All( cv.only_on_esp32, cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + partial(download_web_files_in_config, path_for=_compute_local_file_path), _validate_supported_local_file, ) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index abfd599808f..fbc83ef12f5 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -1,5 +1,6 @@ """Speaker Media Player Setup.""" +from functools import partial import hashlib import logging from pathlib import Path @@ -32,7 +33,7 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt -from esphome.external_files import download_content +from esphome.external_files import download_web_files_in_config _LOGGER = logging.getLogger(__name__) @@ -92,15 +93,6 @@ def _compute_local_file_path(value: dict) -> Path: return base_dir / key -def _download_web_file(value): - url = value[CONF_URL] - path = _compute_local_file_path(value) - - download_content(url, path) - _LOGGER.debug("download_web_file: path=%s", path) - return value - - _PURPOSE_MAP = { "MEDIA": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], "ANNOUNCEMENT": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], @@ -229,11 +221,10 @@ LOCAL_SCHEMA = cv.Schema( } ) -WEB_SCHEMA = cv.All( +WEB_SCHEMA = cv.Schema( { cv.Required(CONF_URL): cv.url, - }, - _download_web_file, + } ) @@ -285,7 +276,12 @@ CONFIG_SCHEMA = cv.All( ), # Remove before 2026.10.0 cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string), - cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + cv.Optional(CONF_FILES): cv.All( + cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + partial( + download_web_files_in_config, path_for=_compute_local_file_path + ), + ), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) ), diff --git a/esphome/external_files.py b/esphome/external_files.py index bd29dc93b1d..fbc261f8e0e 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -1,5 +1,7 @@ from __future__ import annotations +from collections.abc import Callable, Iterable +from concurrent.futures import ThreadPoolExecutor import contextlib from datetime import UTC, datetime import logging @@ -9,9 +11,10 @@ from pathlib import Path import requests import esphome.config_validation as cv -from esphome.const import __version__ +from esphome.const import CONF_FILE, CONF_TYPE, CONF_URL, __version__ from esphome.core import CORE, EsphomeError, TimePeriodSeconds from esphome.helpers import write_file +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@landonr"] @@ -85,7 +88,9 @@ def _write_etag(local_file_path: Path, etag: str | None) -> None: ) -def has_remote_file_changed(url: str, local_file_path: Path) -> bool: +def has_remote_file_changed( + url: str, local_file_path: Path, timeout: int = NETWORK_TIMEOUT +) -> bool: if local_file_path.exists(): _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) try: @@ -101,7 +106,7 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: if etag := _read_etag(local_file_path): headers[IF_NONE_MATCH] = etag response = requests.head( - url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True + url, headers=headers, timeout=timeout, allow_redirects=True ) _LOGGER.debug( @@ -153,7 +158,7 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by if CORE.skip_external_update and path.exists(): _LOGGER.debug("Skipping update for %s (refresh disabled)", url) return path.read_bytes() - if not has_remote_file_changed(url, path): + if not has_remote_file_changed(url, path, timeout): _LOGGER.debug("Remote file has not changed %s", url) return path.read_bytes() @@ -184,3 +189,88 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by write_file(path, data) _write_etag(path, req.headers.get(ETAG)) return data + + +# Cap concurrent connections so a config with hundreds of remote files doesn't +# open hundreds of sockets at once. 8 matches the requests connection-pool +# default and the per-host connection limit browsers use, which keeps us +# polite to the upstream host while still cutting wall time roughly 8x for +# typical configs (a couple dozen files). +DEFAULT_DOWNLOAD_WORKERS = 8 + + +def download_content_many( + items: Iterable[tuple[str, Path]], + timeout: int = NETWORK_TIMEOUT, + max_workers: int = DEFAULT_DOWNLOAD_WORKERS, +) -> None: + """Run `download_content` for each (url, path) pair concurrently. + + Wall time drops from `sum(latency)` to roughly `max(latency)` for cached + files where the HEAD round-trip dominates. All workers run to + completion before this returns; every `cv.Invalid` raised by a worker + is collected and surfaced together as `cv.MultipleInvalid` so the user + sees every broken file in a single validation pass instead of fixing + them one round-trip at a time. + + Items are de-duplicated by `path` -- two callers asking for the same + cache file (e.g. the same URL referenced twice in a config) would + otherwise race on `download_content`'s non-atomic write. When the + same `path` appears more than once, the last URL wins (standard dict + comprehension semantics); in practice duplicate paths only arise when + the URL is duplicated, so the choice doesn't matter. + """ + seen: dict[Path, str] = {path: url for url, path in items} + if not seen: + return + if len(seen) == 1: + path, url = next(iter(seen.items())) + download_content(url, path, timeout) + return + + def _download_one(path_url: tuple[Path, str]) -> None: + # `seen` stores entries as (path, url) so the dict can dedupe by + # path; flip them back to download_content's (url, path) order. + path, url = path_url + download_content(url, path, timeout) + + workers = max(1, min(max_workers, len(seen))) + errors: list[cv.Invalid] = [] + with ThreadPoolExecutor(max_workers=workers) as ex: + futures = [ex.submit(_download_one, item) for item in seen.items()] + for future in futures: + try: + future.result() + except cv.Invalid as e: + errors.append(e) + if not errors: + return + if len(errors) == 1: + raise errors[0] + raise cv.MultipleInvalid(errors) + + +# Each component that uses external_files defines its own local +# `TYPE_WEB = "web"`; the string is repeated here rather than imported +# because there is no canonical `TYPE_WEB` in `esphome.const` to share. +WEB_TYPE = "web" + + +def download_web_files_in_config( + config: list[ConfigType], + path_for: Callable[[ConfigType], Path], +) -> list[ConfigType]: + """Voluptuous-friendly validator that downloads any web-sourced files in + `config` in parallel. + + Each entry is expected to contain a `file` key whose value is a dict + that may be `{type: "web", url: ...}`; `path_for(file_dict)` returns + the cache path for that file. Returns `config` unchanged so it can be + slotted directly into a `cv.All(...)` chain. + """ + download_content_many( + (conf_file[CONF_URL], path_for(conf_file)) + for entry in config + if (conf_file := entry.get(CONF_FILE, {})).get(CONF_TYPE) == WEB_TYPE + ) + return config diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index f4d268abe03..c894f906662 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -9,7 +9,7 @@ import pytest import requests from esphome import external_files -from esphome.config_validation import Invalid +from esphome.config_validation import Invalid, MultipleInvalid from esphome.core import CORE, EsphomeError, TimePeriod @@ -60,6 +60,24 @@ def mock_write_file() -> MagicMock: yield m +@pytest.fixture +def mock_download_content() -> MagicMock: + """Patch `external_files.download_content` for tests that exercise the + parallel batch helper without doing real I/O. + """ + with patch("esphome.external_files.download_content") as m: + yield m + + +@pytest.fixture +def mock_download_content_many() -> MagicMock: + """Patch `external_files.download_content_many` for tests that exercise + the URL-collection helper without dispatching to the thread pool. + """ + with patch("esphome.external_files.download_content_many") as m: + yield m + + def test_compute_local_file_dir(setup_core: Path) -> None: """Test compute_local_file_dir creates and returns correct path.""" domain = "font" @@ -494,6 +512,173 @@ def test_download_content_skip_external_update_downloads_when_missing( assert test_file.read_bytes() == new_content +def test_download_content_many_empty_is_noop( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """Empty input shouldn't spin up a thread pool or call download_content.""" + external_files.download_content_many([]) + mock_download_content.assert_not_called() + + +def test_download_content_many_single_item_avoids_pool( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """A single item should be downloaded inline (no thread pool overhead).""" + item = ("https://example.com/file.txt", setup_core / "f.txt") + external_files.download_content_many([item]) + mock_download_content.assert_called_once_with( + item[0], item[1], external_files.NETWORK_TIMEOUT + ) + + +def test_download_content_many_runs_in_parallel( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """Multiple items should run concurrently — total wall time ≈ max latency.""" + import threading + + barrier = threading.Barrier(3) + + def slow_download(url: str, path: Path, timeout: int) -> bytes: + # If calls were serial this would deadlock (third caller never arrives + # while the first is blocked at the barrier). + barrier.wait(timeout=2.0) + return b"" + + mock_download_content.side_effect = slow_download + items = [ + ("https://example.com/a", setup_core / "a"), + ("https://example.com/b", setup_core / "b"), + ("https://example.com/c", setup_core / "c"), + ] + external_files.download_content_many(items, max_workers=4) + assert mock_download_content.call_count == 3 + + +def test_download_content_many_propagates_single_error( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """A single failing worker should raise its `Invalid` directly, not wrap + it in a `MultipleInvalid` that the caller would have to unpack. + """ + + def fake_download(url: str, path: Path, timeout: int) -> bytes: + if url.endswith("bad"): + raise Invalid(f"could not download {url}") + return b"" + + mock_download_content.side_effect = fake_download + items = [ + ("https://example.com/ok", setup_core / "ok"), + ("https://example.com/bad", setup_core / "bad"), + ] + with pytest.raises(Invalid, match="could not download") as exc_info: + external_files.download_content_many(items) + assert not isinstance(exc_info.value, MultipleInvalid) + + +def test_download_content_many_aggregates_multiple_errors( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """Every failing worker should be reported in a single MultipleInvalid so + the user sees all broken URLs in one validation pass instead of fixing + them one network round-trip at a time. + """ + + def fake_download(url: str, path: Path, timeout: int) -> bytes: + if url.endswith("ok"): + return b"" + raise Invalid(f"could not download {url}") + + mock_download_content.side_effect = fake_download + items = [ + ("https://example.com/ok", setup_core / "ok"), + ("https://example.com/bad1", setup_core / "bad1"), + ("https://example.com/bad2", setup_core / "bad2"), + ] + with pytest.raises(MultipleInvalid) as exc_info: + external_files.download_content_many(items) + messages = {str(e) for e in exc_info.value.errors} + assert messages == { + "could not download https://example.com/bad1", + "could not download https://example.com/bad2", + } + + +def test_download_content_many_dedupes_by_path( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """Two items pointing at the same cache path must collapse to one + download -- otherwise concurrent writes race on the same file. Which + URL wins doesn't matter (in practice duplicate paths only arise when + the URL is duplicated), so we only assert the call count and path. + """ + path = setup_core / "shared" + items = [ + ("https://example.com/a", path), + ("https://example.com/b", path), + ("https://example.com/a", path), + ] + external_files.download_content_many(items) + assert mock_download_content.call_count == 1 + args, _ = mock_download_content.call_args + assert args[1] == path + + +def test_download_content_many_clamps_invalid_max_workers( + mock_download_content: MagicMock, setup_core: Path +) -> None: + """`max_workers <= 0` must not raise from ThreadPoolExecutor; it should + be clamped up to at least 1 worker. + """ + items = [ + ("https://example.com/a", setup_core / "a"), + ("https://example.com/b", setup_core / "b"), + ] + external_files.download_content_many(items, max_workers=0) + assert mock_download_content.call_count == 2 + + +def test_download_web_files_in_config_filters_and_dispatches( + mock_download_content_many: MagicMock, setup_core: Path +) -> None: + """Only `file.type == "web"` entries should be forwarded to + download_content_many, and the unmodified config should be returned so + the helper can sit in a `cv.All(...)` chain. + """ + + def path_for(file_dict: dict) -> Path: + return setup_core / file_dict["url"].rsplit("/", 1)[-1] + + config = [ + {"file": {"type": "web", "url": "https://example.com/a"}}, + {"file": {"type": "local", "path": "/tmp/b"}}, + {"file": {"type": "web", "url": "https://example.com/c"}}, + {}, # no `file` key at all + ] + result = external_files.download_web_files_in_config(config, path_for) + + assert result is config + mock_download_content_many.assert_called_once() + assert list(mock_download_content_many.call_args[0][0]) == [ + ("https://example.com/a", setup_core / "a"), + ("https://example.com/c", setup_core / "c"), + ] + + +def test_download_web_files_in_config_no_web_entries( + mock_download_content_many: MagicMock, setup_core: Path +) -> None: + """A config with no web entries should still call through to + download_content_many (which is itself a no-op for empty input) so the + behavior stays consistent. + """ + config = [{"file": {"type": "local", "path": "/tmp/a"}}] + external_files.download_web_files_in_config(config, lambda _: setup_core / "x") + mock_download_content_many.assert_called_once() + assert list(mock_download_content_many.call_args[0][0]) == [] + + def test_download_content_saves_etag( mock_has_remote_file_changed: MagicMock, mock_requests_get: MagicMock, From ae5b211c8938f294bf1cbd0811b8c5eb0eab8a4e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:30:35 -0400 Subject: [PATCH 1745/2056] [api] Avoid JsonDocument copy-and-swap operator= in ActionResponse ctor (#16106) --- esphome/components/api/homeassistant_service.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9d14061d072..aef046fbb04 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -78,7 +78,8 @@ class ActionResponse { : success_(success), error_message_(error_message) { if (data == nullptr || data_len == 0) return; - this->json_document_ = json::parse_json(data, data_len); + JsonDocument tmp = json::parse_json(data, data_len); + swap(this->json_document_, tmp); } #endif From 79da2b9704cb115624697bded6c0294f3b9db528 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:30:46 -0400 Subject: [PATCH 1746/2056] [time] Fix bugprone-unchecked-optional-access in CronTrigger::check_time_ (#16107) --- esphome/components/time/automation.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 7eb99cfe743..3242669343d 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -31,13 +31,14 @@ void CronTrigger::check_time_() { return; if (this->last_check_.has_value()) { - if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { + auto &last_check = *this->last_check_; + if (last_check > time && last_check.timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) { // We went back in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped back!"); - } else if (*this->last_check_ >= time) { + } else if (last_check >= time) { // already handled this one return; - } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) { + } else if (time > last_check && time.timestamp - last_check.timestamp > MAX_TIMESTAMP_DRIFT) { // We went ahead in time (a lot), probably caused by time synchronization ESP_LOGW(TAG, "Time has jumped ahead!"); this->last_check_ = time; @@ -45,11 +46,11 @@ void CronTrigger::check_time_() { } while (true) { - this->last_check_->increment_second(); - if (*this->last_check_ >= time) + last_check.increment_second(); + if (last_check >= time) break; - if (this->matches(*this->last_check_)) + if (this->matches(last_check)) this->trigger(); } } From 0a497d3c22be8c4c83466178181f4c6b00179994 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 08:35:17 -0500 Subject: [PATCH 1747/2056] [light] Fold LightControlAction fields into a single stateless lambda (#16118) --- esphome/components/light/automation.h | 54 ++++----------------- esphome/components/light/automation.py | 67 ++++++++++++++------------ esphome/components/light/types.py | 1 + 3 files changed, 46 insertions(+), 76 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index bc6fd847099..a5c73997b0b 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -31,60 +31,26 @@ template<bool HasTransitionLength, typename... Ts> class ToggleAction : public A transition_length_{}; }; -// Unique Empty<Tag> per field so [[no_unique_address]] is guaranteed to coalesce. -namespace light_control_detail { -template<int Tag> struct Empty {}; -} // namespace light_control_detail - -// X-macro: (type, field_name, bit_index). Order and bit values must match -// the FIELDS table in automation.py. -#define LIGHT_CONTROL_FIELDS(X) \ - X(ColorMode, color_mode, 0) \ - X(bool, state, 1) \ - X(uint32_t, transition_length, 2) \ - X(uint32_t, flash_length, 3) \ - X(float, brightness, 4) \ - X(float, color_brightness, 5) \ - X(float, red, 6) \ - X(float, green, 7) \ - X(float, blue, 8) \ - X(float, white, 9) \ - X(float, color_temperature, 10) \ - X(float, cold_white, 11) \ - X(float, warm_white, 12) \ - X(uint32_t, effect, 13) - -template<uint16_t Fields, typename... Ts> class LightControlAction : public Action<Ts...> { +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `brightness: !lambda "return x;"`) keep working. +template<typename... Ts> class LightControlAction : public Action<Ts...> { public: - explicit LightControlAction(LightState *parent) : parent_(parent) {} - -#define LIGHT_FIELD_SETTER_(type, name, idx) \ - template<typename V> void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; } -#define LIGHT_FIELD_APPLY_(type, name, idx) \ - if constexpr ((Fields & (1 << (idx))) != 0) \ - call.set_##name(this->name##_.value(x...)); -#define LIGHT_FIELD_DECL_(type, name, idx) \ - [[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableFn<type, Ts...>, \ - light_control_detail::Empty<(idx)>> \ - name##_{}; - - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_) + using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...); + LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {} void play(const Ts &...x) override { auto call = this->parent_->make_call(); - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_) + this->apply_(this->parent_, call, x...); call.perform(); } protected: LightState *parent_; - LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_) - -#undef LIGHT_FIELD_DECL_ -#undef LIGHT_FIELD_APPLY_ -#undef LIGHT_FIELD_SETTER_ + ApplyFn apply_; }; -#undef LIGHT_CONTROL_FIELDS template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> { public: diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index c666c98e42f..ca4018a9756 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -37,6 +37,7 @@ from .types import ( AddressableSet, ColorMode, DimRelativeAction, + LightCall, LightControlAction, LightIsOffCondition, LightIsOnCondition, @@ -181,8 +182,8 @@ def _resolve_effect_index(config: ConfigType) -> int: async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - # Order/bits must match LIGHT_CONTROL_FIELDS in automation.h. - # EFFECT has special handling below; setter=None skips the generic loop. + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. FIELDS = ( (CONF_COLOR_MODE, "set_color_mode", ColorMode), (CONF_STATE, "set_state", cg.bool_), @@ -197,49 +198,51 @@ async def light_control_to_code(config, action_id, template_arg, args): (CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_), (CONF_COLD_WHITE, "set_cold_white", cg.float_), (CONF_WARM_WHITE, "set_warm_white", cg.float_), - (CONF_EFFECT, None, cg.uint32), ) - # Bitmask is passed as uint16_t in C++ — must stay within 16 bits. - assert len(FIELDS) <= 16, "LightControlAction Fields bitmask exceeds uint16_t" - field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) - control_template_arg = cg.TemplateArguments( - cg.RawExpression(f"static_cast<uint16_t>({field_mask})"), *template_arg - ) - var = cg.new_Pvariable(action_id, control_template_arg, paren) + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] for conf_key, setter, type_ in FIELDS: - if conf_key in config and setter is not None: - template_ = await cg.templatable(config[conf_key], args, type_) - cg.add(getattr(var, setter)(template_)) + if conf_key not in config: + continue + value = config[conf_key] + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): - # Lambda returns a string — wrap in a C++ lambda that resolves - # the effect name to its uint32_t index at runtime inner_lambda = await cg.process_lambda( config[CONF_EFFECT], args, return_type=cg.std_string ) - fwd_args = ", ".join(n for _, n in args) - # capture="" is correct: paren is a global variable name - # string-interpolated into the body at codegen time, not a - # C++ runtime capture. - wrapper = LambdaExpression( - f"auto __effect_s = ({inner_lambda})({fwd_args});\n" - f"return {paren}->get_effect_index(" - f"__effect_s.c_str(), __effect_s.size());", - args, - capture="", - return_type=cg.uint32, + body_lines.append( + f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n" + f"call.set_effect(parent->get_effect_index(" + f"__effect_s.c_str(), __effect_s.size())); }}" ) - cg.add(var.set_effect(wrapper)) else: - # Static string — resolve effect name to index at codegen time - template_ = await cg.templatable( - _resolve_effect_index(config), args, cg.uint32 + # Cast disambiguates between set_effect(uint32_t) and + # set_effect(optional<uint32_t>) when the literal is an int. + body_lines.append( + f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));" ) - cg.add(var.set_effect(template_)) - return var + + # Match LightControlAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (LightState.operator("ptr"), "parent"), + (LightCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) CONF_RELATIVE_BRIGHTNESS = "relative_brightness" diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index a586bcbd131..534dcd21948 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -13,6 +13,7 @@ Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") LightStateRTCState = light_ns.struct("LightStateRTCState") +LightCall = light_ns.class_("LightCall") # Color modes ColorMode = light_ns.enum("ColorMode", is_class=True) From 2bd28eee9d25b30eec64bd71c09c354df9630e65 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:51:31 -0400 Subject: [PATCH 1748/2056] [tormatic] Use .value() for checked optional access in read_gate_status_ (#16121) --- esphome/components/tormatic/tormatic_cover.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index a58228a219f..cca7b2bba02 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -282,12 +282,13 @@ optional<GateStatus> Tormatic::read_gate_status_() { } } + auto hdr = this->pending_hdr_.value(); + // Wait for all payload bytes to arrive before processing. - if (this->available() < this->pending_hdr_->payload_size()) { + if (this->available() < hdr.payload_size()) { return {}; } - auto hdr = *this->pending_hdr_; this->pending_hdr_.reset(); switch (hdr.type) { From 42b8597719f186f8bb5b2469260b1c62f67285a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:19 -0400 Subject: [PATCH 1749/2056] [api] Extend NOLINT to cover bugprone-random-generator-seed in MAC varint test (#16120) --- tests/components/api/test_proto_mac_varint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/api/test_proto_mac_varint.cpp b/tests/components/api/test_proto_mac_varint.cpp index 317a6fb9d6e..f2a63e96f63 100644 --- a/tests/components/api/test_proto_mac_varint.cpp +++ b/tests/components/api/test_proto_mac_varint.cpp @@ -112,7 +112,7 @@ TEST(ProtoMacVarint, AllOnes) { verify_mac(0xFFFFFFFFFFFFULL, 7); } // F // 100 deterministic-random 48-bit MACs to catch regressions across the space. TEST(ProtoMacVarint, RandomSample) { - // NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp) -- intentional fixed seed for reproducibility. + // NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) -- fixed seed for reproducibility std::mt19937_64 rng(0xC0FFEE); for (int i = 0; i < 100; i++) { uint64_t mac = rng() & 0xFFFFFFFFFFFFULL; From 2157d1191372b74bed4df5b8a371d5fb1dfeebe7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:26:53 -0400 Subject: [PATCH 1750/2056] [haier] Fix bugprone-unchecked-optional-access; switch HardwareInfo to char[9] (#16124) --- esphome/components/haier/hon_climate.cpp | 48 ++++++++----------- esphome/components/haier/hon_climate.h | 13 ++--- .../components/haier/smartair2_climate.cpp | 2 +- 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 1e9cb42f382..87b8add2a37 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct this->force_send_control_ = true; } -std::string HonClimate::get_cleaning_status_text() const { +const char *HonClimate::get_cleaning_status_text() const { switch (this->cleaning_status_) { case CleaningState::SELF_CLEAN: return "Self clean"; @@ -134,29 +134,22 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie } // All OK hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; - char tmp[9]; - tmp[8] = 0; - strncpy(tmp, answr->protocol_version, 8); - this->hvac_hardware_info_ = HardwareInfo(); - this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); - strncpy(tmp, answr->software_version, 8); - this->hvac_hardware_info_.value().software_version_ = std::string(tmp); - strncpy(tmp, answr->hardware_version, 8); - this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); - strncpy(tmp, answr->device_name, 8); - this->hvac_hardware_info_.value().device_name_ = std::string(tmp); + HardwareInfo info{}; // zero-init guarantees null-termination + strncpy(info.protocol_version_, answr->protocol_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.software_version_, answr->software_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.hardware_version_, answr->hardware_version, HARDWARE_INFO_STR_SIZE - 1); + strncpy(info.device_name_, answr->device_name, HARDWARE_INFO_STR_SIZE - 1); + info.functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + info.functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support + info.functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + info.functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + info.functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->use_crc_ = info.functions_[2]; #ifdef USE_TEXT_SENSOR - this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_); - this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, - this->hvac_hardware_info_.value().protocol_version_); + this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_); + this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_); #endif - this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support - this->hvac_hardware_info_.value().functions_[1] = - (answr->functions[1] & 0x02) != 0; // controller-device mode support - this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support - this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support - this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support - this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; + this->hvac_hardware_info_ = info; this->set_phase(ProtocolPhases::SENDING_INIT_2); return result; } else { @@ -347,10 +340,9 @@ void HonClimate::dump_config() { " Device software version: %s\n" " Device hardware version: %s\n" " Device name: %s", - this->hvac_hardware_info_.value().protocol_version_.c_str(), - this->hvac_hardware_info_.value().software_version_.c_str(), - this->hvac_hardware_info_.value().hardware_version_.c_str(), - this->hvac_hardware_info_.value().device_name_.c_str()); + this->hvac_hardware_info_.value().protocol_version_, + this->hvac_hardware_info_.value().software_version_, + this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_); ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), @@ -460,7 +452,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { if (this->action_request_.has_value()) { if (this->action_request_.value().message.has_value()) { this->send_message_(this->action_request_.value().message.value(), this->use_crc_); - this->action_request_.value().message.reset(); + this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access) } else { // Message already sent, reseting request and return to idle this->action_request_.reset(); @@ -796,7 +788,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe } } -void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) { +void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) { size_t index = (size_t) type; if (this->sub_text_sensors_[index] != nullptr) this->sub_text_sensors_[index]->publish_state(value); diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 7a87f27b668..a0bcdfb548c 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -90,7 +90,7 @@ class HonClimate : public HaierClimateBase { void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens); protected: - void update_sub_text_sensor_(SubTextSensorType type, const std::string &value); + void update_sub_text_sensor_(SubTextSensorType type, const char *value); text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr}; #endif #ifdef USE_SWITCH @@ -116,7 +116,7 @@ class HonClimate : public HaierClimateBase { void set_vertical_airflow(hon_protocol::VerticalSwingMode direction); esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const; void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction); - std::string get_cleaning_status_text() const; + const char *get_cleaning_status_text() const; CleaningState get_cleaning_status() const; void start_self_cleaning(); void start_steri_cleaning(); @@ -166,11 +166,12 @@ class HonClimate : public HaierClimateBase { void fill_control_messages_queue_(); void clear_control_messages_queue_(); + static constexpr size_t HARDWARE_INFO_STR_SIZE = 9; struct HardwareInfo { - std::string protocol_version_; - std::string software_version_; - std::string hardware_version_; - std::string device_name_; + char protocol_version_[HARDWARE_INFO_STR_SIZE]; + char software_version_[HARDWARE_INFO_STR_SIZE]; + char hardware_version_[HARDWARE_INFO_STR_SIZE]; + char device_name_[HARDWARE_INFO_STR_SIZE]; bool functions_[5]; }; diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 200cac25575..752f4d4f1c0 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -191,7 +191,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) if (this->action_request_.has_value()) { if (this->action_request_.value().message.has_value()) { this->send_message_(this->action_request_.value().message.value(), this->use_crc_); - this->action_request_.value().message.reset(); + this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access) } else { // Message already sent, reseting request and return to idle this->action_request_.reset(); From bacee89bca9ebde55687d746d75985516cc43c0d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:56:13 -0400 Subject: [PATCH 1751/2056] [mixer_speaker] NOLINT bugprone-unchecked-optional-access in audio_mixer_task (#16130) --- esphome/components/mixer/speaker/mixer_speaker.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 741239a2dd9..0d16bce330b 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -588,6 +588,7 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio } } +// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created void MixerSpeaker::audio_mixer_task(void *params) { MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params); @@ -764,6 +765,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } +// NOLINTEND(bugprone-unchecked-optional-access) } // namespace esphome::mixer_speaker From 557c3d443611eb8443959ebf1b15206a2030070f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:33:29 -0400 Subject: [PATCH 1752/2056] [aqi] Use std::max initializer-list for non-negative AQI clamp (#16134) --- esphome/components/aqi/aqi_calculator.h | 6 +----- esphome/components/aqi/caqi_calculator.h | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index d624af0432f..bb8e4022802 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -14,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator { uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - - float aqi = std::max(pm2_5_index, pm10_0_index); - if (aqi < 0.0f) { - aqi = 0.0f; - } + float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); return static_cast<uint16_t>(std::lround(aqi)); } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index fe2efe7059d..3f6da45aa91 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -12,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator { uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - - float aqi = std::max(pm2_5_index, pm10_0_index); - if (aqi < 0.0f) { - aqi = 0.0f; - } + float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f}); return static_cast<uint16_t>(std::lround(aqi)); } From bae6b516523a4814ae49f1fbeb3a964b9b192a5b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:33:57 -0400 Subject: [PATCH 1753/2056] [kamstrup_kmp][toshiba] Fix signed/unsigned comparisons against sizeof (#16135) --- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 6 +++--- esphome/components/toshiba/toshiba.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 9f2557243c8..9bebd4cd56d 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() { void KamstrupKMPComponent::read_command_(uint16_t command) { uint8_t buffer[20] = {0}; - int buffer_len = 0; + size_t buffer_len = 0; int data; int timeout = 250; // ms // Read the data from the UART - while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) { + while (timeout > 0 && buffer_len < sizeof(buffer)) { if (this->available()) { data = this->read(); if (data > -1) { @@ -183,7 +183,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) { // Decode uint8_t msg[20] = {0}; int msg_len = 0; - for (int i = 1; i < buffer_len - 1; i++) { + for (size_t i = 1; i < buffer_len - 1; i++) { if (buffer[i] == 0x1B) { msg[msg_len++] = buffer[i + 1] ^ 0xFF; i++; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 53114cc50fc..a23b4c7cc3e 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -275,7 +275,7 @@ static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::Clim */ static uint8_t get_ras_2819t_temp_code(float temperature) { int temp_index = static_cast<int>(temperature) - 18; - if (temp_index < 0 || temp_index >= static_cast<int>(sizeof(RAS_2819T_TEMP_CODES))) { + if (temp_index < 0 || static_cast<size_t>(temp_index) >= sizeof(RAS_2819T_TEMP_CODES)) { ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature); return 0x40; // Default to 24°C } From ce61dcf3873b7fe7a7e8063cc44168222a3aee4a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:54:17 -0400 Subject: [PATCH 1754/2056] [remote_base][core] Drop redundant typename in dependent type contexts (#16137) --- esphome/components/remote_base/remote_base.h | 8 ++++---- esphome/core/finite_set_mask.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index d73fff2b0a5..e5e923d7806 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -164,7 +164,7 @@ class RemoteTransmitterBase : public RemoteComponentBase { return TransmitCall(this); } template<typename Protocol> - void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { auto call = this->transmit(); Protocol().encode(call.get_data(), data); call.set_send_times(send_times); @@ -250,10 +250,10 @@ template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBin } public: - void set_data(typename T::ProtocolData data) { data_ = data; } + void set_data(T::ProtocolData data) { data_ = data; } protected: - typename T::ProtocolData data_; + T::ProtocolData data_; }; template<typename T> @@ -278,7 +278,7 @@ class RemoteTransmittable { protected: template<typename Protocol> - void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { this->transmitter_->transmit<Protocol>(data, send_times, send_wait); } RemoteTransmitterBase *transmitter_; diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index 616c69353d7..272337ff76f 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -55,7 +55,7 @@ template<typename ValueType, int MaxBits> struct DefaultBitPolicy { /// template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16>> class FiniteSetMask { public: - using bitmask_t = typename BitPolicy::mask_t; + using bitmask_t = BitPolicy::mask_t; constexpr FiniteSetMask() = default; From 69a33d8ac0c879646923767b7cc2164b69c838c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 12:31:55 -0500 Subject: [PATCH 1755/2056] [core] Inline HAL clock wrappers and split hal.h into per-platform headers (#15977) --- esphome/components/esp32/core.cpp | 11 +-- esphome/components/esp8266/core.cpp | 17 +--- esphome/components/libretiny/core.cpp | 28 +----- esphome/components/rp2040/core.cpp | 6 +- esphome/core/hal.h | 131 ++++++-------------------- esphome/core/hal/hal_esp32.h | 35 +++++++ esphome/core/hal/hal_esp8266.h | 65 +++++++++++++ esphome/core/hal/hal_host.h | 24 +++++ esphome/core/hal/hal_libretiny.h | 93 ++++++++++++++++++ esphome/core/hal/hal_rp2040.h | 40 ++++++++ esphome/core/hal/hal_zephyr.h | 24 +++++ esphome/core/helpers.h | 41 +------- esphome/core/time_64.h | 6 +- esphome/core/time_conversion.h | 46 +++++++++ 14 files changed, 366 insertions(+), 201 deletions(-) create mode 100644 esphome/core/hal/hal_esp32.h create mode 100644 esphome/core/hal/hal_esp8266.h create mode 100644 esphome/core/hal/hal_host.h create mode 100644 esphome/core/hal/hal_libretiny.h create mode 100644 esphome/core/hal/hal_rp2040.h create mode 100644 esphome/core/hal/hal_zephyr.h create mode 100644 esphome/core/time_conversion.h diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c631371836..4886745c068 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void HOT yield() { vPortYield(); } +// yield(), delay(), micros(), millis_64() inlined in hal.h. // Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), // falling back to esp_timer for non-standard rates. IRAM_ATTR is required because // Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. @@ -37,15 +37,6 @@ uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); #endif } -// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is -// safe because the two are never cross-compared: millis() values are only used for -// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while -// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME), -// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly, -// so the Scheduler is internally consistent on the esp_timer clock. -uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); } -void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } -uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index c9bedb61be4..9161ca6aaf5 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -15,7 +15,7 @@ extern "C" { namespace esphome { -void HOT yield() { ::yield(); } +// yield(), micros(), millis_64() inlined in hal.h. // Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit // multiplies on the LX106). Tracks a running ms counter from 32-bit // system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis @@ -66,7 +66,6 @@ uint32_t IRAM_ATTR HOT millis() { xt_wsr_ps(ps); return result; } -uint64_t millis_64() { return Millis64Impl::compute(millis()); } // Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object // call to the original millis() that --wrap can't intercept, so calling ::delay() // would keep the slow Arduino millis body alive in IRAM. optimistic_yield still @@ -85,8 +84,7 @@ void HOT delay(uint32_t ms) { optimistic_yield(1000); } } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h. void arch_restart() { system_restart(); // restart() doesn't always end execution @@ -95,17 +93,6 @@ void arch_restart() { } } void arch_init() {} -void HOT arch_feed_wdt() { system_soft_wdt_feed(); } - -uint8_t progmem_read_byte(const uint8_t *addr) { - return pgm_read_byte(addr); // NOLINT -} -const char *progmem_read_ptr(const char *const *addr) { - return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT -} -uint16_t progmem_read_uint16(const uint16_t *addr) { - return pgm_read_word(addr); // NOLINT -} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index ca46bcb899e..f46abe3b81a 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,7 +3,6 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,32 +14,7 @@ void loop(); namespace esphome { -void HOT yield() { ::yield(); } -// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast -// path instead of going through the Arduino core's out-of-line ::millis() wrapper. -// -// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR -// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis(). -// -// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch -// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h), -// so no ISR runs while flash is stalled. -#if defined(USE_RTL87XX) || defined(USE_LN882X) -uint32_t IRAM_ATTR HOT millis() { - static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); - return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); -} -#elif defined(USE_BK72XX) -uint32_t HOT millis() { - static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); - return xTaskGetTickCount() * portTICK_PERIOD_MS; -} -#else -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -#endif -uint64_t millis_64() { return Millis64Impl::compute(millis()); } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void HOT delay(uint32_t ms) { ::delay(ms); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index b7a90006123..d3dc1cf2bb5 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -13,11 +13,7 @@ namespace esphome { -void HOT yield() { ::yield(); } -uint64_t millis_64() { return micros_to_millis<uint64_t>(time_us_64()); } -uint32_t HOT millis() { return micros_to_millis(time_us_64()); } -void HOT delay(uint32_t ms) { ::delay(ms); } -uint32_t HOT micros() { return ::micros(); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { watchdog_reboot(0, 0, 10); diff --git a/esphome/core/hal.h b/esphome/core/hal.h index e4083622b98..e20797cf95d 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -2,125 +2,48 @@ #include <string> #include <cstdint> #include "gpio.h" +#include "esphome/core/defines.h" +#include "esphome/core/time_64.h" +#include "esphome/core/time_conversion.h" +// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), +// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem +// helpers) live under esphome/core/hal/ and are dispatched here based on +// the active USE_* platform define. Each header guards its body with the +// matching #ifdef USE_<platform> and re-enters namespace esphome {} so it +// is safe to be re-included. #if defined(USE_ESP32) -#include <esp_attr.h> -#ifndef PROGMEM -#define PROGMEM -#endif - +#include "esphome/core/hal/hal_esp32.h" #elif defined(USE_ESP8266) - -#include <c_types.h> -#ifndef PROGMEM -#define PROGMEM ICACHE_RODATA_ATTR -#endif - -#elif defined(USE_RP2040) - -#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) -#define PROGMEM - +#include "esphome/core/hal/hal_esp8266.h" #elif defined(USE_LIBRETINY) - -// IRAM_ATTR places a function in executable RAM so it is callable from an -// ISR even while flash is busy (XIP stall, OTA, logger flash write). -// Each family uses a section its stock linker already routes to RAM: -// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the -// exception: its stock linker has no matching glob, so patch_linker.py -// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. -// -// BK72xx (all variants) are left as a no-op: their SDK wraps flash -// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for -// the duration of every write, so no ISR fires while flash is stalled and -// the race IRAM_ATTR guards against cannot occur. The trade-off is that -// interrupts are delayed (not dropped) by up to ~20 ms during a sector -// erase, but that is an SDK-level choice and cannot be changed from this -// layer. -#if defined(USE_BK72XX) -#define IRAM_ATTR -#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) -// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). -#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#include "esphome/core/hal/hal_libretiny.h" +#elif defined(USE_RP2040) +#include "esphome/core/hal/hal_rp2040.h" +#elif defined(USE_HOST) +#include "esphome/core/hal/hal_host.h" +#elif defined(USE_ZEPHYR) +#include "esphome/core/hal/hal_zephyr.h" #else -// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. -// LN882H: patch_linker.py.script injects *(.sram.text*) into -// .flash_copysection (> RAM0 AT> FLASH). -#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) -#endif -#define PROGMEM - -#else - -#define IRAM_ATTR -#define PROGMEM - -#endif - -#ifdef USE_ESP32 -#include <freertos/FreeRTOS.h> -#include <freertos/task.h> -#endif - -#ifdef USE_BK72XX -// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so -// it is callable from Thumb code via interworking. The MRS CPSR instruction -// is ARM-only and user code here may be built in Thumb, so in_isr_context() -// defers to this port helper on BK72xx instead of reading CPSR inline. -extern "C" uint32_t platform_is_in_interrupt_context(void); +#error "hal.h: not implemented for this platform" #endif namespace esphome { -/// Returns true when executing inside an interrupt handler. -/// always_inline so callers placed in IRAM keep the detection in IRAM. -__attribute__((always_inline)) inline bool in_isr_context() { -#if defined(USE_ESP32) - return xPortInIsrContext() != 0; -#elif defined(USE_ESP8266) - // ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is - // non-zero both in a real ISR and when user code masks interrupts. The - // ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule - // which is ISR-safe) so this helper is unused on this platform. - return false; -#elif defined(USE_RP2040) - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#elif defined(USE_BK72XX) - // BK72xx is ARM968E-S (ARM9); see extern declaration above. - return platform_is_in_interrupt_context() != 0; -#elif defined(USE_LIBRETINY) - // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; - // non-zero means we're in a handler. - uint32_t ipsr; - __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr != 0; -#else - // Host and any future platform without an ISR concept. - return false; -#endif -} - -void yield(); -uint32_t millis(); -uint64_t millis_64(); -uint32_t micros(); -void delay(uint32_t ms); +// ESP8266 inlines delayMicroseconds() and arch_feed_wdt() in hal/hal_esp8266.h; +// every other platform keeps them out-of-line in components/<platform>/core.cpp. +#ifndef USE_ESP8266 void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +#endif void __attribute__((noreturn)) arch_restart(); void arch_init(); -void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); -#ifdef USE_ESP8266 -// ESP8266: pgm_read_* does real flash reads on Harvard architecture -uint8_t progmem_read_byte(const uint8_t *addr); -const char *progmem_read_ptr(const char *const *addr); -uint16_t progmem_read_uint16(const uint16_t *addr); -#else -// All other platforms: PROGMEM is a no-op, so these are direct dereferences +#ifndef USE_ESP8266 +// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences. +// ESP8266's out-of-line declarations live in hal/hal_esp8266.h. inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } inline const char *progmem_read_ptr(const char *const *addr) { return *addr; } inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } diff --git a/esphome/core/hal/hal_esp32.h b/esphome/core/hal/hal_esp32.h new file mode 100644 index 00000000000..e755337540d --- /dev/null +++ b/esphome/core/hal/hal_esp32.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef USE_ESP32 + +#include <cstdint> +#include <esp_attr.h> +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> + +#include "esphome/core/time_conversion.h" + +#ifndef PROGMEM +#define PROGMEM +#endif + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; } + +// Forward decl from <esp_timer.h>. +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" int64_t esp_timer_get_time(void); + +__attribute__((always_inline)) inline void yield() { vPortYield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(esp_timer_get_time()); } +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { + return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h new file mode 100644 index 00000000000..04326a3579c --- /dev/null +++ b/esphome/core/hal/hal_esp8266.h @@ -0,0 +1,65 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include <c_types.h> +#include <cstdint> +#include <pgmspace.h> + +#include "esphome/core/time_64.h" + +#ifndef PROGMEM +#define PROGMEM ICACHE_RODATA_ATTR +#endif + +// Forward decls from Arduino's <Arduino.h> for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +// Forward decl from <user_interface.h> for arch_feed_wdt() inline below. +// NOLINTNEXTLINE(readability-redundant-declaration) +extern "C" void system_soft_wdt_feed(void); + +namespace esphome { + +// Forward decl from helpers.h so this header stays cheap. +// NOLINTNEXTLINE(readability-redundant-declaration) +void delay_microseconds_safe(uint32_t us); + +/// Returns true when executing inside an interrupt handler. +/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is +/// non-zero both in a real ISR and when user code masks interrupts. The +/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule +/// which is ISR-safe) so this helper is unused on this platform. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); } +void delay(uint32_t ms); +uint32_t millis(); +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +// ESP8266: pgm_read_* does aligned 32-bit flash reads on Harvard architecture. +// Inline-forward to the platform macros so the wrappers themselves don't +// occupy IRAM/flash on every call site. +__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) { + return pgm_read_byte(addr); // NOLINT +} +__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT +} +__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} + +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); } + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h new file mode 100644 index 00000000000..145fe4ea9c1 --- /dev/null +++ b/esphome/core/hal/hal_host.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_HOST + +#include <cstdint> + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Host has no ISR concept. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h new file mode 100644 index 00000000000..e0d92735bbb --- /dev/null +++ b/esphome/core/hal/hal_libretiny.h @@ -0,0 +1,93 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include <cstdint> + +// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS). +#include <FreeRTOS.h> +#include <task.h> + +#include "esphome/core/time_64.h" + +// IRAM_ATTR places a function in executable RAM so it is callable from an +// ISR even while flash is busy (XIP stall, OTA, logger flash write). +// Each family uses a section its stock linker already routes to RAM: +// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the +// exception: its stock linker has no matching glob, so patch_linker.py +// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link. +// +// BK72xx (all variants) are left as a no-op: their SDK wraps flash +// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for +// the duration of every write, so no ISR fires while flash is stalled and +// the race IRAM_ATTR guards against cannot occur. The trade-off is that +// interrupts are delayed (not dropped) by up to ~20 ms during a sector +// erase, but that is an SDK-level choice and cannot be changed from this +// layer. +#if defined(USE_BK72XX) +#define IRAM_ATTR +#elif defined(USE_LIBRETINY_VARIANT_RTL8710B) +// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM). +#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#else +// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text. +// LN882H: patch_linker.py.script injects *(.sram.text*) into +// .flash_copysection (> RAM0 AT> FLASH). +#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#endif +#define PROGMEM + +#ifdef USE_BK72XX +// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so +// it is callable from Thumb code via interworking. The MRS CPSR instruction +// is ARM-only and user code here may be built in Thumb, so in_isr_context() +// defers to this port helper on BK72xx instead of reading CPSR inline. +extern "C" uint32_t platform_is_in_interrupt_context(void); +#endif + +// Forward decls from Arduino's <Arduino.h> for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { +#if defined(USE_BK72XX) + // BK72xx is ARM968E-S (ARM9); see extern declaration above. + return platform_is_in_interrupt_context() != 0; +#else + // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; + // non-zero means we're in a handler. + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#endif +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); } + +// Per-variant millis() fast path — matches MillisInternal::get(). +#if defined(USE_RTL87XX) || defined(USE_LN882X) +static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { + // xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract. + return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); +} +#elif defined(USE_BK72XX) +static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; } +#else +__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); } +#endif +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h new file mode 100644 index 00000000000..156ff33b863 --- /dev/null +++ b/esphome/core/hal/hal_rp2040.h @@ -0,0 +1,40 @@ +#pragma once + +#ifdef USE_RP2040 + +#include <cstdint> + +#include "esphome/core/time_conversion.h" + +#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) +#define PROGMEM + +// Forward decls from Arduino's <Arduino.h> for the inline wrappers below. +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); +extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) + +// Forward decl from <pico/time.h>. +extern "C" uint64_t time_us_64(void); + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +__attribute__((always_inline)) inline bool in_isr_context() { + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +} + +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); } +__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h new file mode 100644 index 00000000000..e28be5c775d --- /dev/null +++ b/esphome/core/hal/hal_zephyr.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include <cstdint> + +#define IRAM_ATTR +#define PROGMEM + +namespace esphome { + +/// Returns true when executing inside an interrupt handler. +/// Zephyr/nRF52: not currently consulted — wake path is platform-specific. +__attribute__((always_inline)) inline bool in_isr_context() { return false; } + +void yield(); +void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); + +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b2b07c57a05..355db6c7f4f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -20,6 +20,7 @@ #include <strings.h> #include "esphome/core/optional.h" +#include "esphome/core/time_conversion.h" // Backward compatibility re-export of heap-allocating helpers. // These functions have moved to alloc_helpers.h. External components should @@ -833,43 +834,9 @@ template<std::integral T> constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } -/// Convert a 64-bit microsecond count to milliseconds without calling -/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). -/// -/// Returns uint32_t by default (for millis()), or uint64_t when requested -/// (for millis_64()). The only difference is whether hi * Q is truncated -/// to 32 bits or widened to 64. -/// -/// On 32-bit targets, GCC does not optimize 64-bit constant division into a -/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 -/// (free divide-by-8), then use the Euclidean division identity to decompose -/// the remaining 64-bit divide-by-125 into a single 32-bit division: -/// -/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] -/// 2^32 = Q * 125 + R (34359738 * 125 + 46) -/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 -/// -/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal -/// (mulhu + shift), so no division instruction is emitted. -/// -/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). -/// -/// See: https://en.wikipedia.org/wiki/Euclidean_division -/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html -template<typename ReturnT = uint32_t> inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) { - constexpr uint32_t d = 125U; - constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738 - constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46 - // 1000 = 8 * 125; divide-by-8 is a free shift - uint64_t x = us >> 3; - uint32_t lo = static_cast<uint32_t>(x); - uint32_t hi = static_cast<uint32_t>(x >> 32); - // Combine remainder term: hi * (2^32 % 125) + lo - uint32_t adj = hi * r + lo; - // If adj overflowed, the true value is 2^32 + adj; apply the identity again - // static_cast<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q - return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); -} +// micros_to_millis<>() lives in its own lightweight header so hal.h can pull it +// in for inline millis_64() without forcing every TU that includes hal.h to +// also include the rest of helpers.h. /// Return a random 32-bit unsigned integer. /// Not thread-safe. Must only be called from the main loop. diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h index d82373dbfe9..f66f9afddb8 100644 --- a/esphome/core/time_64.h +++ b/esphome/core/time_64.h @@ -6,8 +6,6 @@ #include <cstdint> #include <limits> -#include "esphome/core/helpers.h" - namespace esphome { class Scheduler; @@ -24,7 +22,9 @@ class Millis64Impl { static uint32_t last_millis; static uint16_t millis_major; - static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) { + // Raw __attribute__((always_inline)) (not ESPHOME_ALWAYS_INLINE) so this + // header does not need to pull helpers.h. + static inline uint64_t __attribute__((always_inline)) compute(uint32_t now) { // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2; diff --git a/esphome/core/time_conversion.h b/esphome/core/time_conversion.h new file mode 100644 index 00000000000..e9060c06267 --- /dev/null +++ b/esphome/core/time_conversion.h @@ -0,0 +1,46 @@ +#pragma once + +#include <cstdint> + +namespace esphome { + +/// Convert a 64-bit microsecond count to milliseconds without calling +/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). +/// +/// Returns uint32_t by default (for millis()), or uint64_t when requested +/// (for millis_64()). The only difference is whether hi * Q is truncated +/// to 32 bits or widened to 64. +/// +/// On 32-bit targets, GCC does not optimize 64-bit constant division into a +/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 +/// (free divide-by-8), then use the Euclidean division identity to decompose +/// the remaining 64-bit divide-by-125 into a single 32-bit division: +/// +/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] +/// 2^32 = Q * 125 + R (34359738 * 125 + 46) +/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 +/// +/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal +/// (mulhu + shift), so no division instruction is emitted. +/// +/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). +/// +/// See: https://en.wikipedia.org/wiki/Euclidean_division +/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html +template<typename ReturnT = uint32_t> +__attribute__((always_inline)) inline constexpr ReturnT micros_to_millis(uint64_t us) { + constexpr uint32_t d = 125U; + constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738 + constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46 + // 1000 = 8 * 125; divide-by-8 is a free shift + uint64_t x = us >> 3; + uint32_t lo = static_cast<uint32_t>(x); + uint32_t hi = static_cast<uint32_t>(x >> 32); + // Combine remainder term: hi * (2^32 % 125) + lo + uint32_t adj = hi * r + lo; + // If adj overflowed, the true value is 2^32 + adj; apply the identity again + // static_cast<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q + return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); +} + +} // namespace esphome From 7fba57ce51e5babfb77eff76603b5008236c4f55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 12:45:30 -0500 Subject: [PATCH 1756/2056] [valve] Add tests for valve.control action field combinations (#16126) --- tests/components/template/common-base.yaml | 14 ++++ .../fixtures/valve_control_action.yaml | 69 ++++++++++++++++++ .../integration/test_valve_control_action.py | 72 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 tests/integration/fixtures/valve_control_action.yaml create mode 100644 tests/integration/test_valve_control_action.py diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index daa6f53d42f..819eaa8bbfe 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -442,6 +442,20 @@ valve: state: CLOSED stop_action: - logger.log: stop_action + # Exercise valve.control with various field combinations so the + # ControlAction codegen paths get build coverage. + - valve.control: + id: template_valve + stop: true + - valve.control: + id: template_valve + position: 50% + - valve.control: + id: template_valve + state: OPEN + - valve.control: + id: template_valve + position: !lambda 'return 0.25f;' optimistic: true text: diff --git a/tests/integration/fixtures/valve_control_action.yaml b/tests/integration/fixtures/valve_control_action.yaml new file mode 100644 index 00000000000..4f43d16289c --- /dev/null +++ b/tests/integration/fixtures/valve_control_action.yaml @@ -0,0 +1,69 @@ +esphome: + name: valve-control-action-test +host: +api: +logger: + level: DEBUG + +globals: + - id: test_position + type: float + initial_value: "0.42" + +valve: + - platform: template + name: "Test Valve" + id: test_valve + has_position: true + optimistic: true + assumed_state: true + open_action: + - valve.template.publish: + id: test_valve + position: 1.0 + close_action: + - valve.template.publish: + id: test_valve + position: 0.0 + stop_action: + - valve.template.publish: + id: test_valve + current_operation: IDLE + +button: + # valve.control: position only + - platform: template + id: btn_position + name: "Set Position" + on_press: + - valve.control: + id: test_valve + position: 50% + + # valve.control: state alias for position 1.0 + - platform: template + id: btn_open_state + name: "Open State" + on_press: + - valve.control: + id: test_valve + state: OPEN + + # valve.control: lambda position (exercises lambda path) + - platform: template + id: btn_lambda_position + name: "Lambda Position" + on_press: + - valve.control: + id: test_valve + position: !lambda "return id(test_position);" + + # valve.control: stop only — template valve's stop_action publishes + # current_operation: IDLE. + - platform: template + id: btn_stop + name: "Stop Valve" + on_press: + - valve.control: + id: test_valve + stop: true diff --git a/tests/integration/test_valve_control_action.py b/tests/integration/test_valve_control_action.py new file mode 100644 index 00000000000..d6515b8960e --- /dev/null +++ b/tests/integration/test_valve_control_action.py @@ -0,0 +1,72 @@ +"""Integration test for valve ControlAction. + +Tests that valve.control automation actions work correctly across multiple +field combinations and the lambda path. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, EntityState, ValveInfo, ValveOperation, ValveState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_valve_control_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test valve ControlAction with constants and a lambda.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + valve_state_future: asyncio.Future[ValveState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, ValveState) + and valve_state_future is not None + and not valve_state_future.done() + ): + valve_state_future.set_result(state) + + async def wait_for_valve_state(timeout: float = 5.0) -> ValveState: + nonlocal valve_state_future + valve_state_future = loop.create_future() + try: + return await asyncio.wait_for(valve_state_future, timeout) + finally: + valve_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_valve", ValveInfo) + + async def press_and_wait(name: str) -> ValveState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_valve_state() + + # valve.control: position only + state = await press_and_wait("Set Position") + assert state.position == pytest.approx(0.5, abs=0.01) + + # valve.control: state alias for position 1.0 + state = await press_and_wait("Open State") + assert state.position == pytest.approx(1.0, abs=0.01) + + # valve.control: lambda position (test_position global = 0.42) + state = await press_and_wait("Lambda Position") + assert state.position == pytest.approx(0.42, abs=0.01) + + # valve.control: stop only — template valve's stop_action publishes + # current_operation: IDLE. + state = await press_and_wait("Stop Valve") + assert state.current_operation == ValveOperation.IDLE From e5b1991cf79943534f2eaa9cadc06470fa5be2f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 12:46:06 -0500 Subject: [PATCH 1757/2056] [fan] Add tests for fan.turn_on action field combinations (#16125) --- tests/components/fan/common.yaml | 31 ++++++++ .../fixtures/fan_turn_on_action.yaml | 59 +++++++++++++++ tests/integration/test_fan_turn_on_action.py | 75 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 tests/integration/fixtures/fan_turn_on_action.yaml create mode 100644 tests/integration/test_fan_turn_on_action.py diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml index 099bbfef08e..6cabbd24f8d 100644 --- a/tests/components/fan/common.yaml +++ b/tests/components/fan/common.yaml @@ -57,3 +57,34 @@ binary_sensor: return true; } return false; + +# Exercise fan.turn_on with various field combinations so the +# TurnOnAction codegen paths get build coverage. +button: + - platform: template + name: "Fan Speed Only" + on_press: + - fan.turn_on: + id: test_fan + speed: 2 + - platform: template + name: "Fan Oscillating + Direction" + on_press: + - fan.turn_on: + id: test_fan + oscillating: true + direction: REVERSE + - platform: template + name: "Fan All Fields" + on_press: + - fan.turn_on: + id: test_fan + oscillating: false + speed: 3 + direction: FORWARD + - platform: template + name: "Fan Lambda Speed" + on_press: + - fan.turn_on: + id: test_fan + speed: !lambda 'return 1;' diff --git a/tests/integration/fixtures/fan_turn_on_action.yaml b/tests/integration/fixtures/fan_turn_on_action.yaml new file mode 100644 index 00000000000..11bf033e48c --- /dev/null +++ b/tests/integration/fixtures/fan_turn_on_action.yaml @@ -0,0 +1,59 @@ +esphome: + name: fan-turn-on-action-test +host: +api: +logger: + level: DEBUG + +globals: + - id: test_speed + type: int + initial_value: "2" + +fan: + - platform: template + id: test_fan + name: "Test Fan" + has_oscillating: true + has_direction: true + speed_count: 5 + +button: + # fan.turn_on: speed only + - platform: template + id: btn_speed + name: "Set Speed" + on_press: + - fan.turn_on: + id: test_fan + speed: 3 + + # fan.turn_on: oscillating + direction (no speed) + - platform: template + id: btn_oscillate_direction + name: "Set Oscillate Direction" + on_press: + - fan.turn_on: + id: test_fan + oscillating: true + direction: REVERSE + + # fan.turn_on: all three fields + - platform: template + id: btn_all_fields + name: "Set All Fields" + on_press: + - fan.turn_on: + id: test_fan + oscillating: false + speed: 4 + direction: FORWARD + + # fan.turn_on: lambda for speed (exercises lambda path) + - platform: template + id: btn_lambda_speed + name: "Lambda Speed" + on_press: + - fan.turn_on: + id: test_fan + speed: !lambda "return id(test_speed);" diff --git a/tests/integration/test_fan_turn_on_action.py b/tests/integration/test_fan_turn_on_action.py new file mode 100644 index 00000000000..bce258cb5cd --- /dev/null +++ b/tests/integration/test_fan_turn_on_action.py @@ -0,0 +1,75 @@ +"""Integration test for fan TurnOnAction. + +Tests that fan.turn_on automation actions work correctly across multiple +field combinations and the lambda path. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, EntityState, FanDirection, FanInfo, FanState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_fan_turn_on_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test fan TurnOnAction with constants and a lambda.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + fan_state_future: asyncio.Future[FanState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, FanState) + and fan_state_future is not None + and not fan_state_future.done() + ): + fan_state_future.set_result(state) + + async def wait_for_fan_state(timeout: float = 5.0) -> FanState: + nonlocal fan_state_future + fan_state_future = loop.create_future() + try: + return await asyncio.wait_for(fan_state_future, timeout) + finally: + fan_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_fan", FanInfo) + + async def press_and_wait(name: str) -> FanState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_fan_state() + + # speed only + state = await press_and_wait("Set Speed") + assert state.state is True + assert state.speed_level == 3 + + # oscillating + direction + state = await press_and_wait("Set Oscillate Direction") + assert state.oscillating is True + assert state.direction == FanDirection.REVERSE + + # all three fields + state = await press_and_wait("Set All Fields") + assert state.oscillating is False + assert state.speed_level == 4 + assert state.direction == FanDirection.FORWARD + + # lambda path: speed computed at runtime (test_speed global = 2) + state = await press_and_wait("Lambda Speed") + assert state.speed_level == 2 From 44cabc191d65b7fe18a4dd95714e252a5a3a5597 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 13:06:41 -0500 Subject: [PATCH 1758/2056] [core] Catch body-read errors in download_content (#16023) --- esphome/external_files.py | 6 ++- tests/unit_tests/test_external_files.py | 63 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/esphome/external_files.py b/esphome/external_files.py index fbc261f8e0e..dfabc54f474 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -175,6 +175,11 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, ) req.raise_for_status() + # `.content` reads the body lazily; chunked-decode, gzip-decode, + # and mid-stream connection errors all surface here as + # RequestException subclasses, so this needs the same fall-back + # treatment as the request itself. + data = req.content except requests.exceptions.RequestException as e: if path.exists(): _LOGGER.warning( @@ -185,7 +190,6 @@ def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> by return path.read_bytes() raise cv.Invalid(f"Could not download from {url}: {e}") from e - data = req.content write_file(path, data) _write_etag(path, req.headers.get(ETAG)) return data diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py index c894f906662..64ef1495817 100644 --- a/tests/unit_tests/test_external_files.py +++ b/tests/unit_tests/test_external_files.py @@ -469,6 +469,69 @@ def test_download_content_with_network_error_no_cache_fails( external_files.download_content(url, test_file) +class _BodyReadErrorResponse: + """Stand-in for `requests.Response` whose `.content` raises on access. + + A small dedicated stub avoids mutating `MagicMock`'s class with a + `property` (which would leak across every other MagicMock-based test + in this file). + """ + + def __init__(self, exc: Exception) -> None: + self._exc = exc + self.headers: dict[str, str] = {} + + def raise_for_status(self) -> None: + return None + + @property + def content(self) -> bytes: + raise self._exc + + +def test_download_content_with_body_read_error_uses_cache( + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, +) -> None: + """Body-read errors (chunked-decode/gzip-decode/mid-stream connection + drop) raise RequestException subclasses on `.content` access, not from + `requests.get` itself. They must follow the same fall-back-to-cache + path as a connect-time failure. + """ + test_file = setup_core / "cached.txt" + cached_content = b"cached content" + test_file.write_bytes(cached_content) + + mock_has_remote_file_changed.return_value = True + mock_requests_get.return_value = _BodyReadErrorResponse( + requests.exceptions.ChunkedEncodingError("body truncated") + ) + + result = external_files.download_content("https://example.com/file.txt", test_file) + + assert result == cached_content + + +def test_download_content_with_body_read_error_no_cache_fails( + mock_has_remote_file_changed: MagicMock, + mock_requests_get: MagicMock, + setup_core: Path, +) -> None: + """A body-read failure with no cache available must surface as a + cv.Invalid, same as a connect-time failure with no cache. + """ + test_file = setup_core / "nonexistent.txt" + + mock_has_remote_file_changed.return_value = True + mock_requests_get.return_value = _BodyReadErrorResponse( + requests.exceptions.ChunkedEncodingError("body truncated") + ) + + with pytest.raises(Invalid, match="Could not download from.*body truncated"): + external_files.download_content("https://example.com/file.txt", test_file) + + def test_download_content_skip_external_update_uses_cache( mock_has_remote_file_changed: MagicMock, mock_requests_get: MagicMock, From ca3f7251d42b857ee55abcf456a20af89e7b5940 Mon Sep 17 00:00:00 2001 From: GelidusResearch <120155735+GelidusResearch@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:07:28 -0500 Subject: [PATCH 1759/2056] [ens160] Fix sensor initialization timing (#16024) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/ens160_base/ens160_base.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index e1cee5005cb..42baa68b35d 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -5,6 +5,15 @@ // Implementation based on: // https://github.com/sciosense/ENS160_driver +// For best performance, the sensor shall be operated in normal indoor air in the range -5 to 60°C +// (typical: 25°C); relative humidity: 20 to 80%RH (typical: 50%RH), non-condensing with no aggressive +// or poisonous gases present. Prolonged exposure to environments outside these conditions can affect +// performance and lifetime of the sensor. +// The sensor is designed for indoor use and is not waterproof or dustproof. It should be protected from +// water, condensation, dust, and aggressive gases. Note that the status will only be stored in non-volatile +// memory after an initial 24 h of continuous operation. If unpowered before the conclusion of that period, +// the ENS160 will resume "Initial Start-up" mode after re-powering. + #include "ens160_base.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -14,7 +23,9 @@ namespace ens160_base { static const char *const TAG = "ens160"; -static const uint8_t ENS160_BOOTING = 10; +// Datasheet specifies 10ms, but some users report that 10ms is not sufficient for the +// sensor to boot and be ready for commands. 11ms seems to be a safe value. +static const uint8_t ENS160_BOOTING = 11; static const uint16_t ENS160_PART_ID = 0x0160; @@ -91,6 +102,8 @@ void ENS160Component::setup() { this->mark_failed(); return; } + delay(ENS160_BOOTING); + // clear command if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { this->error_code_ = WRITE_FAILED; @@ -102,6 +115,7 @@ void ENS160Component::setup() { this->mark_failed(); return; } + delay(ENS160_BOOTING); // read firmware version if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { @@ -109,6 +123,8 @@ void ENS160Component::setup() { this->mark_failed(); return; } + delay(ENS160_BOOTING); + uint8_t version_data[3]; if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) { this->error_code_ = READ_FAILED; @@ -223,7 +239,6 @@ void ENS160Component::update() { if (this->aqi_ != nullptr) { // remove reserved bits, just in case they are used in future data_aqi = ENS160_DATA_AQI & data_aqi; - this->aqi_->publish_state(data_aqi); } From 985dba933241ed8224f004ce19f2878cce4dbb54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 13:17:59 -0500 Subject: [PATCH 1760/2056] [core] Defer heavy module-scope imports in __main__, loader, and config (#15955) --- esphome/__main__.py | 29 +++++++++++++++++++++++++---- esphome/config.py | 9 ++++++++- esphome/loader.py | 27 ++++++++++++++++++++------- script/import_time_budget.json | 2 +- tests/unit_tests/test_main.py | 8 ++++---- 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 8c80dab90af..781bcd62885 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -21,7 +21,7 @@ import argcomplete # Note: Do not import modules from esphome.components here, as this would # cause them to be loaded before external components are processed, resulting # in the built-in version being used instead of the external component one. -from esphome import const, writer, yaml_util +from esphome import const import esphome.codegen as cg from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( @@ -72,7 +72,12 @@ from esphome.util import ( run_external_process, safe_print, ) -from esphome.zeroconf import discover_mdns_devices + +# Keep expensive imports (zeroconf, writer, yaml_util, etc.) out of this +# module's top level. Every `esphome` invocation — including fast paths +# like `esphome version` — pays the cost of what's imported here before +# any command runs. Import inside the function that needs it instead. +# `script/check_import_time.py` enforces a budget in CI. _LOGGER = logging.getLogger(__name__) @@ -241,6 +246,8 @@ def _discover_mac_suffix_devices() -> list[str] | None: """ if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()): return None + from esphome.zeroconf import discover_mdns_devices + _LOGGER.info("Discovering devices...") if not (discovered := discover_mdns_devices(CORE.name)): _LOGGER.warning( @@ -660,7 +667,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 0 -def wrap_to_code(name, comp): +def _wrap_to_code(name, comp, yaml_util): coro = coroutine(comp.to_code) @functools.wraps(comp.to_code) @@ -680,6 +687,8 @@ def wrap_to_code(name, comp): def write_cpp(config: ConfigType, native_idf: bool = False) -> int: + from esphome import writer + if not get_bool_env(ENV_NOGITIGNORE): writer.write_gitignore() @@ -691,17 +700,21 @@ def write_cpp(config: ConfigType, native_idf: bool = False) -> int: def generate_cpp_contents(config: ConfigType) -> None: + from esphome import yaml_util + _LOGGER.info("Generating C++ source...") for name, component, conf in iter_component_configs(CORE.config): if component.to_code is not None: - coro = wrap_to_code(name, component) + coro = _wrap_to_code(name, component, yaml_util) CORE.add_job(coro, conf) CORE.flush_tasks() def write_cpp_file(native_idf: bool = False) -> int: + from esphome import writer + code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) @@ -1180,6 +1193,8 @@ def command_wizard(args: ArgsProtocol) -> int | None: def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: + from esphome import yaml_util + if not CORE.verbose: config = strip_default_ids(config) output = yaml_util.dump(config, args.show_secrets) @@ -1321,6 +1336,8 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None: def command_clean_all(args: ArgsProtocol) -> int | None: + from esphome import writer + try: writer.clean_all(args.configuration) except OSError as err: @@ -1336,6 +1353,8 @@ def command_version(args: ArgsProtocol) -> int | None: def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None: + from esphome import writer + try: writer.clean_build() except OSError as err: @@ -1538,6 +1557,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: + from esphome import yaml_util + new_name = args.name for c in new_name: if c not in ALLOWED_NAME_CHARS: diff --git a/esphome/config.py b/esphome/config.py index 6eb67af58b3..79d0d2b02ba 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -25,7 +25,10 @@ from esphome.const import ( CONF_SUBSTITUTIONS, ) from esphome.core import CORE, DocumentRange, EsphomeError -import esphome.core.config as core_config + +# `esphome.core.config` is imported lazily at its two use sites below. +# It pulls in `esphome.automation` and `esphome.config_validation`, which +# dominate `esphome.__main__` startup cost when loaded eagerly here. import esphome.final_validate as fv from esphome.helpers import indent from esphome.loader import ComponentManifest, get_component, get_platform @@ -968,6 +971,8 @@ class CoreFinalValidateStep(ConfigValidationStep): if result.errors: return + import esphome.core.config as core_config + token = fv.full_config.set(result) with result.catch_error([CONF_ESPHOME]): if CONF_ESPHOME in result: @@ -1073,6 +1078,8 @@ def validate_config( return result # 2. Load partial core config + import esphome.core.config as core_config + result[CONF_ESPHOME] = config[CONF_ESPHOME] result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) try: diff --git a/esphome/loader.py b/esphome/loader.py index 2405fa6f884..d50554f8c93 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -9,14 +9,23 @@ import logging from pathlib import Path import sys from types import ModuleType -from typing import Any +from typing import TYPE_CHECKING, Any from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE -import esphome.core.config -from esphome.cpp_generator import MockObjClass from esphome.types import ConfigType +if TYPE_CHECKING: + from esphome.cpp_generator import MockObjClass + +# `esphome.core.config` is imported lazily in `_lookup_module` when the +# "esphome" pseudo-component is first resolved. It pulls in +# `esphome.automation` and `esphome.config_validation`, which together +# dominate `esphome.__main__` startup cost when loaded eagerly. +# `esphome.cpp_generator` is similarly avoided at module scope; it pulls +# in `esphome.yaml_util` and is only needed for the `MockObjClass` type +# annotation, which is resolved lazily via `TYPE_CHECKING`. + _LOGGER = logging.getLogger(__name__) @@ -94,7 +103,7 @@ class ComponentManifest: return getattr(self.module, "CODEOWNERS", []) @property - def instance_type(self) -> MockObjClass | None: + def instance_type(self) -> "MockObjClass | None": return getattr(self.module, "INSTANCE_TYPE", None) @property @@ -213,6 +222,13 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None: if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] + if domain == "esphome": + import esphome.core.config + + manif = ComponentManifest(esphome.core.config, recursive_sources=True) + _COMPONENT_CACHE[domain] = manif + return manif + try: module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: @@ -248,9 +264,6 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None: _COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() -_COMPONENT_CACHE["esphome"] = ComponentManifest( - esphome.core.config, recursive_sources=True -) def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None: diff --git a/script/import_time_budget.json b/script/import_time_budget.json index 1e656dc9776..af3aa835113 100644 --- a/script/import_time_budget.json +++ b/script/import_time_budget.json @@ -1,5 +1,5 @@ { "target_module": "esphome.__main__", "margin_pct": 15, - "cumulative_us": 123000 + "cumulative_us": 91000 } diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 8ec9e70cf85..fb8f206a1d2 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2605,7 +2605,7 @@ def test_choose_upload_log_host_discovers_mac_suffix_devices(tmp_path: Path) -> } with ( patch( - "esphome.__main__.discover_mdns_devices", return_value=discovered + "esphome.zeroconf.discover_mdns_devices", return_value=discovered ) as mock_discover, patch( "esphome.__main__.choose_prompt", return_value="mydevice-abc123.local" @@ -2653,7 +2653,7 @@ def test_choose_upload_log_host_mac_suffix_no_devices_found( ) with ( - patch("esphome.__main__.discover_mdns_devices", return_value={}), + patch("esphome.zeroconf.discover_mdns_devices", return_value={}), caplog.at_level(logging.WARNING, logger="esphome.__main__"), pytest.raises(EsphomeError), ): @@ -2686,7 +2686,7 @@ def test_choose_upload_log_host_default_ota_discovers_mac_suffix( "mydevice-def456.local": ["10.0.0.2"], } with patch( - "esphome.__main__.discover_mdns_devices", return_value=discovered + "esphome.zeroconf.discover_mdns_devices", return_value=discovered ) as mock_discover: result = choose_upload_log_host( default="OTA", @@ -2715,7 +2715,7 @@ def test_choose_upload_log_host_default_ota_no_suffix_discovery( name="mydevice", ) - with patch("esphome.__main__.discover_mdns_devices") as mock_discover: + with patch("esphome.zeroconf.discover_mdns_devices") as mock_discover: result = choose_upload_log_host( default="OTA", check_default=None, From 0ad8a071a706d6b83d70b6ee15074c5b6c9ca736 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:18:21 +1000 Subject: [PATCH 1761/2056] [espnow] Cleanup method visibility and naming (#16109) --- esphome/components/espnow/__init__.py | 16 +++++------ esphome/components/espnow/automation.h | 27 ++++++++++--------- .../components/espnow/espnow_component.cpp | 8 +++--- esphome/components/espnow/espnow_component.h | 24 ++++++++--------- .../packet_transport/espnow_transport.cpp | 10 +++---- .../packet_transport/espnow_transport.h | 6 ++--- 6 files changed, 45 insertions(+), 46 deletions(-) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index a9624734d02..7861c0affa1 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -26,9 +26,9 @@ espnow_ns = cg.esphome_ns.namespace("espnow") ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component) # Handler interfaces that other components can use to register callbacks -ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler") +ESPNowReceivePacketHandler = espnow_ns.class_("ESPNowReceivePacketHandler") ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler") -ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler") +ESPNowBroadcastHandler = espnow_ns.class_("ESPNowBroadcastHandler") ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo") ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref") @@ -48,10 +48,10 @@ OnUnknownPeerTrigger = espnow_ns.class_( "OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler ) OnReceiveTrigger = espnow_ns.class_( - "OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler + "OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivePacketHandler ) -OnBroadcastedTrigger = espnow_ns.class_( - "OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler +OnBroadcastTrigger = espnow_ns.class_( + "OnBroadcastTrigger", ESPNowHandlerTrigger, ESPNowBroadcastHandler ) @@ -94,7 +94,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ON_BROADCAST): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger), cv.Optional(CONF_ADDRESS): cv.mac_address, } ), @@ -140,11 +140,11 @@ async def to_code(config): for on_receive in config.get(CONF_ON_RECEIVE, []): trigger = await _trigger_to_code(on_receive) - cg.add(var.register_received_handler(trigger)) + cg.add(var.register_receive_handler(trigger)) for on_receive in config.get(CONF_ON_BROADCAST, []): trigger = await _trigger_to_code(on_receive) - cg.add(var.register_broadcasted_handler(trigger)) + cg.add(var.register_broadcast_handler(trigger)) # ========================================== A C T I O N S ================================================ diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0fbb14e3888..9c3c55e4eff 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -67,6 +67,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente } } + protected: void play(const Ts &...x) override { /* ignore - see play_complex */ } @@ -75,7 +76,6 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente this->error_.stop(); } - protected: ActionList<Ts...> sent_; ActionList<Ts...> error_; @@ -89,7 +89,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> { TEMPLATABLE_VALUE(peer_address_t, address); - public: + protected: void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->add_peer(address.data()); @@ -99,7 +99,7 @@ template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Pare template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> { TEMPLATABLE_VALUE(peer_address_t, address); - public: + protected: void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->del_peer(address.data()); @@ -107,8 +107,9 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P }; template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> { - public: TEMPLATABLE_VALUE(uint8_t, channel) + + protected: void play(const Ts &...x) override { if (this->parent_->is_wifi_enabled()) { return; @@ -125,9 +126,9 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN); } - explicit OnReceiveTrigger() : has_address_(false) {} + explicit OnReceiveTrigger() {} - bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; @@ -138,7 +139,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, protected: bool has_address_{false}; - uint8_t address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]{}; }; class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, public ESPNowUnknownPeerHandler { @@ -148,15 +149,15 @@ class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_ return false; // Return false to continue processing other internal handlers } }; -class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, - public ESPNowBroadcastedHandler { +class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>, + public ESPNowBroadcastHandler { public: - explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) { + explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) { memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN); } - explicit OnBroadcastedTrigger() : has_address_(false) {} + explicit OnBroadcastTrigger() {} - bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; @@ -167,7 +168,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_ protected: bool has_address_{false}; - uint8_t address_[ESP_NOW_ETH_ALEN]; + uint8_t address_[ESP_NOW_ETH_ALEN]{}; }; } // namespace esphome::espnow diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 282287ca83d..91d44394e8d 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -299,13 +299,13 @@ void ESPNowComponent::loop() { format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); #endif if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { - for (auto *handler : this->broadcasted_handlers_) { - if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size)) + for (auto *handler : this->broadcast_handlers_) { + if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size)) break; // If a handler returns true, stop processing further handlers } } else { - for (auto *handler : this->received_handlers_) { - if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size)) + for (auto *handler : this->receive_handlers_) { + if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size)) break; // If a handler returns true, stop processing further handlers } } diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index ee4adc1b4d4..ff9581ec2ff 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -31,8 +31,8 @@ using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>; enum class ESPNowTriggers : uint8_t { TRIGGER_NONE = 0, ON_NEW_PEER = 1, - ON_RECEIVED = 2, - ON_BROADCASTED = 3, + ON_RECEIVE = 2, + ON_BROADCAST = 3, ON_SUCCEED = 10, ON_FAILED = 11, }; @@ -74,18 +74,18 @@ class ESPNowReceivedPacketHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; }; -/// Handler interface for receiving broadcasted ESPNow packets +/// Handler interface for receiving ESPNow broadcast packets /// Components should inherit from this class to handle incoming ESPNow data -class ESPNowBroadcastedHandler { +class ESPNowBroadcastHandler { public: - /// Called when a broadcasted ESPNow packet is received + /// Called when an ESPNow broadcast packet is received /// @param info Information about the received packet (sender MAC, etc.) /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; }; class ESPNowComponent : public Component { @@ -136,13 +136,11 @@ class ESPNowComponent : public Component { esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback = nullptr); - void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); } + void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); } void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) { this->unknown_peer_handlers_.push_back(handler); } - void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) { - this->broadcasted_handlers_.push_back(handler); - } + void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); } protected: friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size); @@ -156,8 +154,8 @@ class ESPNowComponent : public Component { void send_(); std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_; - std::vector<ESPNowReceivedPacketHandler *> received_handlers_; - std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_; + std::vector<ESPNowReceivedPacketHandler *> receive_handlers_; + std::vector<ESPNowBroadcastHandler *> broadcast_handlers_; std::vector<ESPNowPeer> peers_{}; diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 6e4f606466f..384e3fe2a9a 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -26,10 +26,10 @@ void ESPNowTransport::setup() { this->peer_address_[5]); // Register received handler - this->parent_->register_received_handler(this); + this->parent_->register_receive_handler(this); - // Register broadcasted handler - this->parent_->register_broadcasted_handler(this); + // Register broadcast handler + this->parent_->register_broadcast_handler(this); } void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const { @@ -56,7 +56,7 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const { }); } -bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { +bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); @@ -71,7 +71,7 @@ bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *dat return false; // Allow other handlers to run } -bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { +bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index d85119db7dd..98c33f01fdb 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -15,7 +15,7 @@ namespace espnow { class ESPNowTransport : public packet_transport::PacketTransport, public Parented<ESPNowComponent>, public ESPNowReceivedPacketHandler, - public ESPNowBroadcastedHandler { + public ESPNowBroadcastHandler { public: void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -25,8 +25,8 @@ class ESPNowTransport : public packet_transport::PacketTransport, } // ESPNow handler interface - bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; - bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; protected: void send_packet(const std::vector<uint8_t> &buf) const override; From c41f38e16d2a3f179f02cfb56a99fbbd7ca76217 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 13:24:37 -0500 Subject: [PATCH 1762/2056] [scheduler] Add self-keyed timer API for callers without a Component (#16127) --- esphome/core/scheduler.cpp | 33 +++++- esphome/core/scheduler.h | 80 +++++++++---- .../fixtures/scheduler_self_keyed.yaml | 112 ++++++++++++++++++ .../integration/test_scheduler_self_keyed.py | 96 +++++++++++++++ 4 files changed, 295 insertions(+), 26 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_self_keyed.yaml create mode 100644 tests/integration/test_scheduler_self_keyed.py diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 11884ce4ba9..57deeab0da3 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -35,7 +35,9 @@ static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; // Uses a stack buffer to avoid heap allocation // Uses ESPHOME_snprintf_P/ESPHOME_PSTR for ESP8266 to keep format strings in flash struct SchedulerNameLog { - char buffer[20]; // Enough for "id:4294967295" or "hash:0xFFFFFFFF" or "(null)" + // Sized for the widest formatted output: "self:0x" + 16 hex digits (64-bit pointer) + nul. + // Also covers "id:4294967295", "hash:0xFFFFFFFF", "iid:4294967295", "(null)". + char buffer[28]; // Format a scheduler item name for logging // Returns pointer to formatted string (either static_name or internal buffer) @@ -53,9 +55,15 @@ struct SchedulerNameLog { } else if (name_type == NameType::NUMERIC_ID) { ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id); return buffer; - } else { // NUMERIC_ID_INTERNAL + } else if (name_type == NameType::NUMERIC_ID_INTERNAL) { ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id); return buffer; + } else { // SELF_POINTER + // static_name carries the void* key for SELF_POINTER (pointer-width union slot). + // %p is specified as void* (not const void*), so strip const for the varargs call. + ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("self:%p"), + const_cast<void *>(static_cast<const void *>(static_name))); + return buffer; } } }; @@ -293,6 +301,27 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) { return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL); } +// Self-keyed scheduler API. The cancellation key is `self` (typically the caller's `this`), +// passed through the existing static_name pointer slot. Matching is by raw pointer equality +// (see matches_item_locked_'s SELF_POINTER branch). No Component pointer is stored, so +// is_failed() skip and component-based log attribution don't apply. +void HOT Scheduler::set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func) { + this->set_timer_common_(nullptr, SchedulerItem::TIMEOUT, NameType::SELF_POINTER, static_cast<const char *>(self), 0, + timeout, std::move(func)); +} +void HOT Scheduler::set_interval(const void *self, uint32_t interval, std::function<void()> &&func) { + this->set_timer_common_(nullptr, SchedulerItem::INTERVAL, NameType::SELF_POINTER, static_cast<const char *>(self), 0, + interval, std::move(func)); +} +bool HOT Scheduler::cancel_timeout(const void *self) { + return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0, + SchedulerItem::TIMEOUT); +} +bool HOT Scheduler::cancel_interval(const void *self) { + return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0, + SchedulerItem::INTERVAL); +} + // Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation. // Remove before 2026.8.0 along with all retry code. #pragma GCC diagnostic push diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 46b19855c35..7a6be6bea9a 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -146,22 +146,43 @@ class Scheduler { } // Name storage type discriminator for SchedulerItem - // Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs + // Used to distinguish between static strings, hashed strings, numeric IDs, internal numeric IDs, + // and self-keyed pointers (caller-supplied `void *`, typically `this`). enum class NameType : uint8_t { - STATIC_STRING = 0, // const char* pointer to static/flash storage - HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string - NUMERIC_ID = 2, // uint32_t numeric identifier (component-level) - NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace) + STATIC_STRING = 0, // const char* pointer to static/flash storage + HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string + NUMERIC_ID = 2, // uint32_t numeric identifier (component-level) + NUMERIC_ID_INTERNAL = 3, // uint32_t numeric identifier (core/internal, separate namespace) + SELF_POINTER = 4 // void* caller-supplied key (typically `this`); pointer equality }; + /** Self-keyed timeout. The cancellation key is `self` (typically the caller's `this`). + * + * Use this when the caller schedules at most one timer of a single purpose at a time and + * does not need a `Component` for `is_failed()` skip or log source attribution. Lets + * small classes drop `Component` inheritance entirely when their only Component dependency + * was the per-instance scheduler key. + * + * NOT applied for self-keyed items: + * - `is_failed()` skip — callbacks always fire (no Component to consult). + * - Log source attribution — logs use a generic "self:0x…" label. + * + * If you need either of those, use the existing `(Component *, id)` overloads. + */ + void set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func); + /// Self-keyed interval. See set_timeout(const void *, ...) for semantics. + void set_interval(const void *self, uint32_t interval, std::function<void()> &&func); + bool cancel_timeout(const void *self); + bool cancel_interval(const void *self); + protected: struct SchedulerItem { // Ordered by size to minimize padding Component *component; // Optimized name storage using tagged union - zero heap allocation union { - const char *static_name; // For STATIC_STRING (string literals, no allocation) - uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID + const char *static_name; // For STATIC_STRING (string literals) and SELF_POINTER (caller's `this`) + uint32_t hash_or_id; // For HASHED_STRING, NUMERIC_ID, and NUMERIC_ID_INTERNAL } name_; uint32_t interval; // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() @@ -182,19 +203,19 @@ class Scheduler { // std::atomic<uint8_t> inlines correctly on all platforms. std::atomic<uint8_t> remove{0}; - // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) - enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; - NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum) - bool is_retry : 1; // True if this is a retry timeout - // 4 bits padding -#else - // Single-threaded or multi-threaded without atomics: can pack all fields together // Bit-packed fields (5 bits used, 3 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; - bool remove : 1; - NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum) + NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum) bool is_retry : 1; // True if this is a retry timeout // 3 bits padding +#else + // Single-threaded or multi-threaded without atomics: can pack all fields together + // Bit-packed fields (6 bits used, 2 bits padding in 1 byte) + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool remove : 1; + NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum) + bool is_retry : 1; // True if this is a retry timeout + // 2 bits padding #endif // Constructor @@ -228,19 +249,26 @@ class Scheduler { SchedulerItem(SchedulerItem &&) = delete; SchedulerItem &operator=(SchedulerItem &&) = delete; - // Helper to get the static name (only valid for STATIC_STRING type) - const char *get_name() const { return (name_type_ == NameType::STATIC_STRING) ? name_.static_name : nullptr; } + // Helper to get the pointer-slot value (valid for STATIC_STRING and SELF_POINTER types). + // Both share the same union member, so callers (e.g. log formatters) can read either uniformly. + const char *get_name() const { + return (name_type_ == NameType::STATIC_STRING || name_type_ == NameType::SELF_POINTER) ? name_.static_name + : nullptr; + } - // Helper to get the hash or numeric ID (only valid for HASHED_STRING or NUMERIC_ID types) - uint32_t get_name_hash_or_id() const { return (name_type_ != NameType::STATIC_STRING) ? name_.hash_or_id : 0; } + // Helper to get the hash or numeric ID (only valid for HASHED_STRING / NUMERIC_ID / NUMERIC_ID_INTERNAL types) + uint32_t get_name_hash_or_id() const { + return (name_type_ != NameType::STATIC_STRING && name_type_ != NameType::SELF_POINTER) ? name_.hash_or_id : 0; + } // Helper to get the name type NameType get_name_type() const { return name_type_; } - // Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id. - // Both union members occupy the same offset, so only one store is needed. + // Set name storage. STATIC_STRING/SELF_POINTER use the static_name pointer slot + // (both are pointer-width); other types use hash_or_id. Both union members occupy + // the same offset, so only one store is needed. void set_name(NameType type, const char *static_name, uint32_t hash_or_id) { - if (type == NameType::STATIC_STRING) { + if (type == NameType::STATIC_STRING || type == NameType::SELF_POINTER) { name_.static_name = static_name; } else { name_.hash_or_id = hash_or_id; @@ -367,10 +395,14 @@ class Scheduler { // Name type must match if (item->get_name_type() != name_type) return false; - // For static strings, compare the string content; for hash/ID, compare the value + // STATIC_STRING: compare string content. SELF_POINTER: raw pointer equality (no strcmp). + // Other types: compare hash/ID value. if (name_type == NameType::STATIC_STRING) { return this->names_match_static_(item->get_name(), static_name); } + if (name_type == NameType::SELF_POINTER) { + return item->name_.static_name == static_name; + } return item->get_name_hash_or_id() == hash_or_id; } diff --git a/tests/integration/fixtures/scheduler_self_keyed.yaml b/tests/integration/fixtures/scheduler_self_keyed.yaml new file mode 100644 index 00000000000..9a691136f36 --- /dev/null +++ b/tests/integration/fixtures/scheduler_self_keyed.yaml @@ -0,0 +1,112 @@ +esphome: + debug_scheduler: true # Enable scheduler leak detection + name: scheduler-self-keyed-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler self-keyed tests" + +host: +api: +logger: + level: VERBOSE + +globals: + - id: tests_done + type: bool + initial_value: 'false' + +script: + - id: test_self_keyed + then: + - logger.log: "Testing self-keyed scheduler API" + - lambda: |- + // Two distinct keys backed by addresses of static markers — they + // must not collide even though both are self-keyed and share no + // Component pointer. Static storage gives them stable, unique + // addresses for the lifetime of the program. + static int key_a_marker = 0; + static int key_b_marker = 0; + void *key_a = &key_a_marker; + void *key_b = &key_b_marker; + + // ---- Test 1: Self-keyed timeout fires ---- + App.scheduler.set_timeout(key_a, 50, []() { + ESP_LOGI("test", "Self timeout A fired"); + }); + + // ---- Test 2: Self-keyed cancel cancels only that key ---- + App.scheduler.set_timeout(key_b, 100, []() { + ESP_LOGE("test", "ERROR: Self timeout B should have been cancelled"); + }); + App.scheduler.cancel_timeout(key_b); + + // ---- Test 3: Two independent self keys don't collide ---- + // Using fresh static markers so neither matches key_a / key_b. + static int key_c_marker = 0; + static int key_d_marker = 0; + void *key_c = &key_c_marker; + void *key_d = &key_d_marker; + App.scheduler.set_timeout(key_c, 150, []() { + ESP_LOGI("test", "Self timeout C fired"); + }); + App.scheduler.set_timeout(key_d, 150, []() { + ESP_LOGI("test", "Self timeout D fired"); + }); + + // ---- Test 4: Self-keyed and component-keyed don't collide ---- + // Use a self pointer that happens to look like a Component-attached id. + // The scheduler must treat them as separate namespaces. + static int shared_marker = 0; + void *self_shared = &shared_marker; + App.scheduler.set_timeout(self_shared, 200, []() { + ESP_LOGI("test", "Self timeout shared fired"); + }); + App.scheduler.set_timeout(id(test_sensor), 7777U, 200, []() { + ESP_LOGI("test", "Component timeout 7777 fired"); + }); + + // ---- Test 5: Self-keyed interval fires multiple times then cancels ---- + static int interval_count = 0; + static int key_e_marker = 0; + void *key_e = &key_e_marker; + App.scheduler.set_interval(key_e, 80, [key_e]() { + interval_count++; + if (interval_count == 2) { + ESP_LOGI("test", "Self interval E fired twice"); + App.scheduler.cancel_interval(key_e); + } + }); + + // ---- Test 6: Re-registering same self-key replaces the timer ---- + // The old timer must NOT fire; only the new one does. + static int key_f_marker = 0; + void *key_f = &key_f_marker; + App.scheduler.set_timeout(key_f, 250, []() { + ESP_LOGE("test", "ERROR: Self timeout F first registration should have been replaced"); + }); + App.scheduler.set_timeout(key_f, 300, []() { + ESP_LOGI("test", "Self timeout F replacement fired"); + }); + + // Log completion after all timers should have fired + App.scheduler.set_timeout(id(test_sensor), 9999U, 1500, []() { + ESP_LOGI("test", "All self-keyed tests complete"); + }); + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +interval: + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(tests_done) == false;' + then: + - lambda: 'id(tests_done) = true;' + - script.execute: test_self_keyed diff --git a/tests/integration/test_scheduler_self_keyed.py b/tests/integration/test_scheduler_self_keyed.py new file mode 100644 index 00000000000..e0825ea8254 --- /dev/null +++ b/tests/integration/test_scheduler_self_keyed.py @@ -0,0 +1,96 @@ +"""Test the self-keyed scheduler API. + +Verifies that `Scheduler::set_timeout(const void *, ...)` / +`set_interval(const void *, ...)` and the matching `cancel_*(const void *)` +overloads behave correctly: callbacks fire, distinct keys don't collide, +self-keyed and component-keyed namespaces are independent, and re-registering +the same key replaces the existing timer. +""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_self_keyed( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test self-keyed scheduler API.""" + self_a_fired = asyncio.Event() + self_b_error = asyncio.Event() + self_c_fired = asyncio.Event() + self_d_fired = asyncio.Event() + self_shared_fired = asyncio.Event() + component_7777_fired = asyncio.Event() + self_interval_done = asyncio.Event() + self_f_first_error = asyncio.Event() + self_f_replacement_fired = asyncio.Event() + all_tests_complete = asyncio.Event() + + def on_log_line(line: str) -> None: + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + if "Self timeout A fired" in clean_line: + self_a_fired.set() + elif "ERROR: Self timeout B" in clean_line: + self_b_error.set() + elif "Self timeout C fired" in clean_line: + self_c_fired.set() + elif "Self timeout D fired" in clean_line: + self_d_fired.set() + elif "Self timeout shared fired" in clean_line: + self_shared_fired.set() + elif "Component timeout 7777 fired" in clean_line: + component_7777_fired.set() + elif "Self interval E fired twice" in clean_line: + self_interval_done.set() + elif "ERROR: Self timeout F first registration" in clean_line: + self_f_first_error.set() + elif "Self timeout F replacement fired" in clean_line: + self_f_replacement_fired.set() + elif "All self-keyed tests complete" in clean_line: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-self-keyed-test" + + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("Not all self-keyed tests completed within 5 seconds") + + # Test 1: self-keyed timeout fires + assert self_a_fired.is_set(), "Self timeout A should have fired" + + # Test 2: cancel_timeout(self) actually cancels + assert not self_b_error.is_set(), "Self timeout B should have been cancelled" + + # Test 3: distinct self keys don't collide + assert self_c_fired.is_set(), "Self timeout C should have fired" + assert self_d_fired.is_set(), "Self timeout D should have fired" + + # Test 4: self-keyed and component-keyed namespaces are independent + assert self_shared_fired.is_set(), "Self timeout shared should have fired" + assert component_7777_fired.is_set(), "Component timeout 7777 should have fired" + + # Test 5: self-keyed interval fires repeatedly and cancels cleanly + assert self_interval_done.is_set(), "Self interval E should have fired twice" + + # Test 6: re-registering same self-key replaces the previous timer + assert not self_f_first_error.is_set(), ( + "Self timeout F first registration should have been replaced" + ) + assert self_f_replacement_fired.is_set(), ( + "Self timeout F replacement should have fired" + ) From 59b4cfd07ca158e4fc6c778f49e38fc7f863e658 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:41:12 +0000 Subject: [PATCH 1763/2056] [watchdog] Use default CHECK_IDLE_TASK and PANIC when configuring the watchdog (#16142) --- esphome/components/watchdog/watchdog.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index 545d83a6791..edf113b0b4a 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -39,9 +39,18 @@ void WatchdogManager::set_timeout_(uint32_t timeout_ms) { #ifdef USE_ESP32 esp_task_wdt_config_t wdt_config = { .timeout_ms = timeout_ms, - .idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U, - .trigger_panic = true, + .idle_core_mask = 0, + .trigger_panic = false, }; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdt_config.idle_core_mask |= (1U << 0U); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdt_config.idle_core_mask |= (1U << 1U); +#endif +#if CONFIG_ESP_TASK_WDT_PANIC + wdt_config.trigger_panic = true; +#endif esp_task_wdt_reconfigure(&wdt_config); #endif // USE_ESP32 From 61a41402df1b371c63146e732321461125e7d3f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 14:16:05 -0500 Subject: [PATCH 1764/2056] [fan] Fold TurnOnAction fields into a single stateless lambda (#16122) --- esphome/components/fan/__init__.py | 47 +++++++++++++++++++++-------- esphome/components/fan/automation.h | 23 ++++++-------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 713f20fb95e..f47fc06b3d6 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -31,17 +31,19 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) +from esphome.cpp_generator import LambdaExpression IS_PLATFORM_COMPONENT = True fan_ns = cg.esphome_ns.namespace("fan") Fan = fan_ns.class_("Fan", cg.EntityBase) +FanCall = fan_ns.class_("FanCall") FanDirection = fan_ns.enum("FanDirection", is_class=True) FAN_DIRECTION_ENUM = { @@ -347,17 +349,38 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): ) async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if (oscillating := config.get(CONF_OSCILLATING)) is not None: - template_ = await cg.templatable(oscillating, args, cg.bool_) - cg.add(var.set_oscillating(template_)) - if (speed := config.get(CONF_SPEED)) is not None: - template_ = await cg.templatable(speed, args, cg.int_) - cg.add(var.set_speed(template_)) - if (direction := config.get(CONF_DIRECTION)) is not None: - template_ = await cg.templatable(direction, args, FanDirection) - cg.add(var.set_direction(template_)) - return var + + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. + FIELDS = ( + (CONF_OSCILLATING, "set_oscillating", cg.bool_), + (CONF_SPEED, "set_speed", cg.int_), + (CONF_DIRECTION, "set_direction", FanDirection), + ) + + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] + for conf_key, setter, type_ in FIELDS: + if (value := config.get(conf_key)) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + + # Match TurnOnAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (FanCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) @automation.register_action( diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3ee6f89e557..d8eda41b279 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -7,29 +7,24 @@ namespace esphome { namespace fan { +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `speed: !lambda "return x;"`) keep working. template<typename... Ts> class TurnOnAction : public Action<Ts...> { public: - explicit TurnOnAction(Fan *state) : state_(state) {} - - TEMPLATABLE_VALUE(bool, oscillating) - TEMPLATABLE_VALUE(int, speed) - TEMPLATABLE_VALUE(FanDirection, direction) + using ApplyFn = void (*)(FanCall &, const Ts &...); + TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {} void play(const Ts &...x) override { auto call = this->state_->turn_on(); - if (this->oscillating_.has_value()) { - call.set_oscillating(this->oscillating_.value(x...)); - } - if (this->speed_.has_value()) { - call.set_speed(this->speed_.value(x...)); - } - if (this->direction_.has_value()) { - call.set_direction(this->direction_.value(x...)); - } + this->apply_(call, x...); call.perform(); } Fan *state_; + ApplyFn apply_; }; template<typename... Ts> class TurnOffAction : public Action<Ts...> { From 5a146ab6b75958fd855ccbbe965a95fe7e1f15ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 14:20:15 -0500 Subject: [PATCH 1765/2056] [valve] Fold ControlAction fields into a single stateless lambda (#16123) --- esphome/components/valve/__init__.py | 50 ++++++++++++++++++++------- esphome/components/valve/automation.h | 17 ++++----- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index a6808c9da7b..7377aea1ed2 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -21,14 +21,14 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_WATER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import LambdaExpression, MockObjClass IS_PLATFORM_COMPONENT = True @@ -43,6 +43,7 @@ DEVICE_CLASSES = [ valve_ns = cg.esphome_ns.namespace("valve") Valve = valve_ns.class_("Valve", cg.EntityBase) +ValveCall = valve_ns.class_("ValveCall") VALVE_OPEN = valve_ns.VALVE_OPEN VALVE_CLOSED = valve_ns.VALVE_CLOSED @@ -228,17 +229,40 @@ VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( ) async def valve_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if stop_config := config.get(CONF_STOP): - template_ = await cg.templatable(stop_config, args, cg.bool_) - cg.add(var.set_stop(template_)) - if state_config := config.get(CONF_STATE): - template_ = await cg.templatable(state_config, args, cg.float_) - cg.add(var.set_position(template_)) - if (position_config := config.get(CONF_POSITION)) is not None: - template_ = await cg.templatable(position_config, args, cg.float_) - cg.add(var.set_position(template_)) - return var + + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. + # CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most + # one is present and both dispatch to set_position. + FIELDS = ( + (CONF_STOP, "set_stop", cg.bool_), + (CONF_STATE, "set_position", cg.float_), + (CONF_POSITION, "set_position", cg.float_), + ) + + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] + for conf_key, setter, type_ in FIELDS: + if (value := config.get(conf_key)) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + + # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (ValveCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index a064f375f72..ae9ac0db762 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -47,24 +47,25 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { Valve *valve_; }; +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `position: !lambda "return x;"`) keep working. template<typename... Ts> class ControlAction : public Action<Ts...> { public: - explicit ControlAction(Valve *valve) : valve_(valve) {} - - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) + using ApplyFn = void (*)(ValveCall &, const Ts &...); + ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {} void play(const Ts &...x) override { auto call = this->valve_->make_call(); - if (this->stop_.has_value()) - call.set_stop(this->stop_.value(x...)); - if (this->position_.has_value()) - call.set_position(this->position_.value(x...)); + this->apply_(call, x...); call.perform(); } protected: Valve *valve_; + ApplyFn apply_; }; template<typename... Ts> class ValveIsOpenCondition : public Condition<Ts...> { From 813964714ce881b85b0730347e739591124cf89a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 15:09:08 -0500 Subject: [PATCH 1766/2056] [esp32] Move HAL bodies into components/esp32/hal.cpp + inline trivial dispatches (#16111) --- esphome/components/esp32/core.cpp | 60 +------------------------- esphome/components/esp32/hal.cpp | 71 +++++++++++++++++++++++++++++++ esphome/core/hal.h | 10 ++--- esphome/core/hal/hal_esp32.h | 12 ++++++ esphome/core/hal/hal_esp8266.h | 2 + esphome/core/hal/hal_host.h | 4 ++ esphome/core/hal/hal_libretiny.h | 4 ++ esphome/core/hal/hal_rp2040.h | 4 ++ esphome/core/hal/hal_zephyr.h | 4 ++ 9 files changed, 106 insertions(+), 65 deletions(-) create mode 100644 esphome/components/esp32/hal.cpp diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 4886745c068..5249f4a59e0 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,17 +1,8 @@ #ifdef USE_ESP32 -#include "esphome/core/defines.h" -#include "crash_handler.h" #include "esphome/core/application.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" #include "preferences.h" -#include <esp_clk_tree.h> -#include <esp_cpu.h> -#include <esp_idf_version.h> -#include <esp_ota_ops.h> -#include <esp_task_wdt.h> -#include <esp_timer.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> @@ -22,54 +13,7 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -// yield(), delay(), micros(), millis_64() inlined in hal.h. -// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), -// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because -// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. -// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract. -uint32_t IRAM_ATTR HOT millis() { -#if CONFIG_FREERTOS_HZ == 1000 - if (xPortInIsrContext()) [[unlikely]] { - return xTaskGetTickCountFromISR(); - } - return xTaskGetTickCount(); -#else - return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); -#endif -} -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } -void arch_restart() { - esp_restart(); - // restart() doesn't always end execution - while (true) { // NOLINT(clang-diagnostic-unreachable-code) - yield(); - } -} - -void arch_init() { -#ifdef USE_ESP32_CRASH_HANDLER - // Read crash data from previous boot before anything else - esp32::crash_handler_read_and_clear(); -#endif - - // Enable the task watchdog only on the loop task (from which we're currently running) - esp_task_wdt_add(nullptr); - - // Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, - // in which case safe_mode will mark it valid after confirming successful boot. -#ifndef USE_OTA_ROLLBACK - esp_ota_mark_app_valid_cancel_rollback(); -#endif -} -void HOT arch_feed_wdt() { esp_task_wdt_reset(); } - -uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } -uint32_t arch_get_cpu_freq_hz() { - uint32_t freq = 0; - esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); - return freq; -} - +// HAL functions live in hal.cpp. This file keeps only the loop task setup. TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static StackType_t diff --git a/esphome/components/esp32/hal.cpp b/esphome/components/esp32/hal.cpp new file mode 100644 index 00000000000..f6199d557f3 --- /dev/null +++ b/esphome/components/esp32/hal.cpp @@ -0,0 +1,71 @@ +#ifdef USE_ESP32 + +// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set +// before crash_handler.h's #ifdef-guarded namespace block is parsed. +#include "esphome/core/defines.h" +#include "crash_handler.h" +#include "esphome/core/hal.h" + +#include <esp_clk_tree.h> +#include <esp_ota_ops.h> +#include <esp_system.h> +#include <esp_task_wdt.h> +#include <esp_timer.h> +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> + +// Empty esp32 namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// esp32 component's API. +namespace esphome::esp32 {} // namespace esphome::esp32 + +namespace esphome { + +// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), +// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because +// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. +// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract. +uint32_t IRAM_ATTR HOT millis() { +#if CONFIG_FREERTOS_HZ == 1000 + if (xPortInIsrContext()) [[unlikely]] { + return xTaskGetTickCountFromISR(); + } + return xTaskGetTickCount(); +#else + return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); +#endif +} + +void arch_restart() { + esp_restart(); + // restart() doesn't always end execution + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} + +void arch_init() { +#ifdef USE_ESP32_CRASH_HANDLER + // Read crash data from previous boot before anything else + esp32::crash_handler_read_and_clear(); +#endif + + // Enable the task watchdog only on the loop task (from which we're currently running) + esp_task_wdt_add(nullptr); + + // Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, + // in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + esp_ota_mark_app_valid_cancel_rollback(); +#endif +} + +uint32_t arch_get_cpu_freq_hz() { + uint32_t freq = 0; + esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); + return freq; +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/core/hal.h b/esphome/core/hal.h index e20797cf95d..312effa7b0e 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -30,15 +30,11 @@ namespace esphome { -// ESP8266 inlines delayMicroseconds() and arch_feed_wdt() in hal/hal_esp8266.h; -// every other platform keeps them out-of-line in components/<platform>/core.cpp. -#ifndef USE_ESP8266 -void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) -void arch_feed_wdt(); -#endif +// Cross-platform declarations. delayMicroseconds(), arch_feed_wdt(), +// arch_get_cpu_cycle_count() vary per platform (some inline, some +// out-of-line) so they live in hal/hal_<platform>.h. void __attribute__((noreturn)) arch_restart(); void arch_init(); -uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); #ifndef USE_ESP8266 diff --git a/esphome/core/hal/hal_esp32.h b/esphome/core/hal/hal_esp32.h index e755337540d..2bff4244419 100644 --- a/esphome/core/hal/hal_esp32.h +++ b/esphome/core/hal/hal_esp32.h @@ -4,6 +4,8 @@ #include <cstdint> #include <esp_attr.h> +#include <esp_cpu.h> +#include <esp_task_wdt.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> @@ -15,6 +17,11 @@ namespace esphome { +// Forward decl from helpers.h (esphome/core/helpers.h) — kept here so this +// header does not need to pull the rest of helpers.h. +// NOLINTNEXTLINE(readability-redundant-declaration) +void delay_microseconds_safe(uint32_t us); + /// Returns true when executing inside an interrupt handler. __attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; } @@ -30,6 +37,11 @@ __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); } +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +__attribute__((always_inline)) inline void arch_feed_wdt() { esp_task_wdt_reset(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } + } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h index 04326a3579c..484118f1f50 100644 --- a/esphome/core/hal/hal_esp8266.h +++ b/esphome/core/hal/hal_esp8266.h @@ -60,6 +60,8 @@ __attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_ __attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } __attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); } +uint32_t arch_get_cpu_cycle_count(); + } // namespace esphome #endif // USE_ESP8266 diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h index 145fe4ea9c1..682a1a422b4 100644 --- a/esphome/core/hal/hal_host.h +++ b/esphome/core/hal/hal_host.h @@ -19,6 +19,10 @@ uint32_t micros(); uint32_t millis(); uint64_t millis_64(); +void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +uint32_t arch_get_cpu_cycle_count(); + } // namespace esphome #endif // USE_HOST diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h index e0d92735bbb..e9d33b7753b 100644 --- a/esphome/core/hal/hal_libretiny.h +++ b/esphome/core/hal/hal_libretiny.h @@ -88,6 +88,10 @@ __attribute__((always_inline)) inline uint32_t millis() { return static_cast<uin #endif __attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } +void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +uint32_t arch_get_cpu_cycle_count(); + } // namespace esphome #endif // USE_LIBRETINY diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h index 156ff33b863..2a1d67b4a32 100644 --- a/esphome/core/hal/hal_rp2040.h +++ b/esphome/core/hal/hal_rp2040.h @@ -35,6 +35,10 @@ __attribute__((always_inline)) inline uint32_t micros() { return static_cast<uin __attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); } +void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +uint32_t arch_get_cpu_cycle_count(); + } // namespace esphome #endif // USE_RP2040 diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h index e28be5c775d..6707c85b2c2 100644 --- a/esphome/core/hal/hal_zephyr.h +++ b/esphome/core/hal/hal_zephyr.h @@ -19,6 +19,10 @@ uint32_t micros(); uint32_t millis(); uint64_t millis_64(); +void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) +void arch_feed_wdt(); +uint32_t arch_get_cpu_cycle_count(); + } // namespace esphome #endif // USE_ZEPHYR From 14910e65d97ceff9d42f0b383f056b555a2aa6f4 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:15:21 +0000 Subject: [PATCH 1767/2056] [ota] Use WatchdogManager for OTA on ESP32 (#16138) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/ota/__init__.py | 2 ++ .../components/ota/ota_backend_esp_idf.cpp | 23 ++----------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 8f31eb5cdd3..579491fe1a3 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -24,6 +24,8 @@ def AUTO_LOAD() -> list[str]: components = ["safe_mode"] if not CORE.using_zephyr: components.extend(["md5"]) + if CORE.is_esp32: + components.extend(["watchdog"]) return components diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 598fce1562a..b4b38a192f0 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -2,6 +2,7 @@ #include "ota_backend_esp_idf.h" #include "esphome/components/md5/md5.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -28,29 +29,9 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } -#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 - // The following function takes longer than the 5 seconds timeout of WDT - esp_task_wdt_config_t wdtc; - wdtc.idle_core_mask = 0; -#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 - wdtc.idle_core_mask |= (1 << 0); -#endif -#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 - wdtc.idle_core_mask |= (1 << 1); -#endif - wdtc.timeout_ms = 15000; - wdtc.trigger_panic = false; - esp_task_wdt_reconfigure(&wdtc); -#endif - + watchdog::WatchdogManager watchdog(15000); esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); -#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 - // Set the WDT back to the configured timeout - wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; - esp_task_wdt_reconfigure(&wdtc); -#endif - if (err != ESP_OK) { esp_ota_abort(this->update_handle_); this->update_handle_ = 0; From 53b682e48fce681685d0e19d62d0a52db0441043 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:19:33 -0400 Subject: [PATCH 1768/2056] [ci] Bump clang-tidy from 18.1.8 to 22.1.0.1 (#16078) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .clang-tidy | 33 ++++++++++++++++--- .clang-tidy.hash | 2 +- .../adalight/adalight_light_effect.cpp | 2 +- esphome/components/api/proto.h | 1 + esphome/components/climate/climate.cpp | 3 +- esphome/components/debug/debug_zephyr.cpp | 13 +++++--- .../e131/e131_addressable_light_effect.cpp | 3 +- esphome/components/esp8266/gpio.cpp | 1 + esphome/components/esp8266/preferences.cpp | 4 +-- .../http_request/http_request_arduino.cpp | 2 +- esphome/components/nrf52/uicr.cpp | 2 ++ .../components/ota/ota_backend_esp8266.cpp | 1 + esphome/components/sprinkler/sprinkler.cpp | 2 +- esphome/core/color.h | 2 +- esphome/core/helpers.cpp | 4 +-- esphome/core/preference_backend.h | 4 +++ requirements_dev.txt | 2 +- script/clang-tidy | 6 ++-- 18 files changed, 63 insertions(+), 24 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 5878028f48e..4e128a19455 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,24 +5,30 @@ Checks: >- -altera-*, -android-*, -boost-*, + -bugprone-derived-method-shadowing-base-method, -bugprone-easily-swappable-parameters, -bugprone-implicit-widening-of-multiplication-result, + -bugprone-invalid-enum-default-initialization, -bugprone-multi-level-implicit-pointer-conversion, -bugprone-narrowing-conversions, + -bugprone-tagged-union-member-count, -bugprone-signed-char-misuse, -bugprone-switch-missing-default-case, -cert-dcl50-cpp, -cert-err33-c, -cert-err58-cpp, + -cert-int09-c, -cert-oop57-cpp, -cert-str34-c, -clang-analyzer-optin.core.EnumCastOutOfRange, -clang-analyzer-optin.cplusplus.UninitializedObject, -clang-analyzer-osx.*, + -clang-analyzer-security.ArrayBound, -clang-diagnostic-delete-abstract-non-virtual-dtor, -clang-diagnostic-delete-non-abstract-non-virtual-dtor, -clang-diagnostic-deprecated-declarations, -clang-diagnostic-ignored-optimization-argument, + -clang-diagnostic-missing-designated-field-initializers, -clang-diagnostic-missing-field-initializers, -clang-diagnostic-shadow-field, -clang-diagnostic-unused-const-variable, @@ -42,6 +48,7 @@ Checks: >- -cppcoreguidelines-owning-memory, -cppcoreguidelines-prefer-member-initializer, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-pro-type-const-cast, @@ -54,12 +61,13 @@ Checks: >- -cppcoreguidelines-rvalue-reference-param-not-moved, -cppcoreguidelines-special-member-functions, -cppcoreguidelines-use-default-member-init, + -cppcoreguidelines-use-enum-class, -cppcoreguidelines-virtual-class-destructor, + -fuchsia-default-arguments-calls, + -fuchsia-default-arguments-declarations, -fuchsia-multiple-inheritance, -fuchsia-overloaded-operator, -fuchsia-statically-constructed-objects, - -fuchsia-default-arguments-declarations, - -fuchsia-default-arguments-calls, -google-build-using-namespace, -google-explicit-constructor, -google-readability-braces-around-statements, @@ -71,16 +79,23 @@ Checks: >- -llvm-else-after-return, -llvm-header-guard, -llvm-include-order, + -llvm-prefer-static-over-anonymous-namespace, -llvm-qualified-auto, + -llvm-use-ranges, -llvmlibc-*, -misc-const-correctness, -misc-include-cleaner, + -misc-multiple-inheritance, -misc-no-recursion, -misc-non-private-member-variables-in-classes, + -misc-override-with-different-visibility, -misc-unused-parameters, -misc-use-anonymous-namespace, + -misc-use-internal-linkage, -modernize-avoid-bind, + -modernize-avoid-variadic-functions, -modernize-avoid-c-arrays, + -modernize-avoid-c-style-cast, -modernize-concat-nested-namespaces, -modernize-macro-to-enum, -modernize-return-braced-init-list, @@ -88,32 +103,42 @@ Checks: >- -modernize-use-auto, -modernize-use-constraints, -modernize-use-default-member-init, + -modernize-use-designated-initializers, -modernize-use-equals-default, + -modernize-use-integer-sign-comparison, -modernize-use-nodiscard, -modernize-use-nullptr, - -modernize-use-nodiscard, - -modernize-use-nullptr, + -modernize-use-ranges, -modernize-use-trailing-return-type, -mpi-*, -objc-*, -performance-enum-size, + -portability-avoid-pragma-once, + -portability-template-virtual-member-function, + -readability-ambiguous-smartptr-reset-call, -readability-avoid-nested-conditional-operator, -readability-container-contains, -readability-container-data-pointer, -readability-convert-member-functions-to-static, -readability-else-after-return, + -readability-enum-initial-value, -readability-function-cognitive-complexity, -readability-implicit-bool-conversion, -readability-isolate-declaration, -readability-magic-numbers, -readability-make-member-function-const, + -readability-math-missing-parentheses, -readability-named-parameter, -readability-redundant-casting, -readability-redundant-inline-specifier, -readability-redundant-member-init, + -readability-redundant-parentheses, -readability-redundant-string-init, + -readability-redundant-typename, -readability-uppercase-literal-suffix, -readability-use-anyofallof, + -readability-use-std-min-max, + -readability-use-concise-preprocessor-directives, WarningsAsErrors: '*' FormatStyle: google CheckOptions: diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 41e1b7bd2fe..582e0c1eaa2 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324 +0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8 diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 4cf639a01f0..06d7e0e8970 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -129,7 +129,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL uint8_t *led_data = &frame_[6]; for (int led = 0; led < accepted_led_count; led++, led_data += 3) { - auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + auto white = std::min({led_data[0], led_data[1], led_data[2]}); it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 3ff65029e1b..f058f6af222 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -424,6 +424,7 @@ class ProtoEncode { if (len == 0 && !force) return; encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string + // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute if (len < VARINT_MAX_1_BYTE) [[likely]] { PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len); *pos++ = static_cast<uint8_t>(len); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index e1324971407..b41ca4a5404 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -374,7 +374,8 @@ void Climate::save_state_(const ClimateTraits &traits) { #define TEMP_IGNORE_MEMACCESS #endif ClimateDeviceRestoreState state{}; - // initialize as zero to prevent random data on stack triggering erase + // initialize as zero (including padding) to prevent random data on stack triggering erase + // NOLINTNEXTLINE(bugprone-raw-memory-call-on-non-trivial-type) -- intentional bytewise zero for RTC save memset(&state, 0, sizeof(ClimateDeviceRestoreState)); #ifdef TEMP_IGNORE_MEMACCESS #pragma GCC diagnostic pop diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 49790b5b9a0..81c86127848 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -17,11 +17,13 @@ constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; static inline uint32_t read_mem_u32(uintptr_t addr) { - return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr) + // NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference) + return *reinterpret_cast<volatile uint32_t *>(addr); } static inline uint8_t read_mem_u8(uintptr_t addr) { - return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr) + // NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference) + return *reinterpret_cast<volatile uint8_t *>(addr); } // defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information @@ -98,6 +100,7 @@ void DebugComponent::log_partition_info_() { #define NRF_PERIPH_ENABLED(periph, reg) \ YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos)) +// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- nRF peripheral registers are MMIO at fixed addresses static void log_peripherals_info() { // most peripherals are enabled only when in use so ESP_LOGV is enough ESP_LOGV(TAG, "Peripherals status:"); @@ -131,6 +134,7 @@ static void log_peripherals_info() { YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) == (CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos))); } +// NOLINTEND(clang-analyzer-core.FixedAddressDereference) #undef NRF_PERIPH_ENABLED #endif @@ -159,8 +163,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> char *buf = buffer.data(); // Main supply status - const char *supply_status = - (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; + // NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- NRF_POWER is MMIO at a fixed address + auto regstatus = nrf_power_mainregstatus_get(NRF_POWER); + const char *supply_status = (regstatus == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; ESP_LOGD(TAG, "Main supply status: %s", supply_status); pos = buf_append_str(buf, size, pos, "|Main supply status: "); pos = buf_append_str(buf, size, pos, supply_status); diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index f6010a7cc9f..f6300874ac0 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -56,8 +56,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet // limit amount of lights per universe and received // packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0; - int output_end = - std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet)); + int output_end = std::min({it->size(), output_offset + get_lights_per_universe(), output_offset + lights_in_packet}); auto *input_data = packet.values + 1; auto effect_name = get_name(); diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 659233443e2..a85f054dfe9 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -140,6 +140,7 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { auto *arg = reinterpret_cast<ISRPinArg *>(arg_); + // NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPIO_REG_WRITE is MMIO at a fixed address GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index f444f035553..696f83bce1a 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -51,7 +51,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { return false; } - *dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr) + *dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference) return true; } @@ -64,7 +64,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { } auto *ptr = &ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr) - *ptr = value; + *ptr = value; // NOLINT(clang-analyzer-core.FixedAddressDereference) return true; } diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 05f9db1c069..217ad0064d9 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -243,7 +243,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { // Non-chunked path int available_data = stream_ptr->available(); size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; - int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); + int bufsize = std::min({max_len, remaining, (size_t) available_data}); if (bufsize == 0) { this->duration_ms += (millis() - start); diff --git a/esphome/components/nrf52/uicr.cpp b/esphome/components/nrf52/uicr.cpp index 4c0beeb503b..03b07f8fe38 100644 --- a/esphome/components/nrf52/uicr.cpp +++ b/esphome/components/nrf52/uicr.cpp @@ -11,6 +11,7 @@ void nvmc_wait(); nrfx_err_t nrfx_nvmc_uicr_erase(); } +// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- NRF_UICR / NRF_TIMER2 are MMIO at fixed addresses namespace esphome::nrf52 { enum class StatusFlags : uint8_t { @@ -113,6 +114,7 @@ static int board_esphome_init() { return 0; } } // namespace esphome::nrf52 +// NOLINTEND(clang-analyzer-core.FixedAddressDereference) static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); } diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 93e6249fb3d..7c9d3925321 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -60,6 +60,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { // Check boot mode - if boot mode is UART download mode, // we will not be able to reset into normal mode once update is done + // NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPI is MMIO at a fixed address int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK; if (boot_mode == BOOT_MODE_UART_DOWNLOAD) { return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index e977c05c48f..336123a472f 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -669,7 +669,7 @@ uint32_t Sprinkler::valve_run_duration_adjusted(const size_t valve_number) { // run_duration must not be less than any of these if ((run_duration < this->start_delay_) || (run_duration < this->stop_delay_) || (run_duration < this->switching_delay_.value_or(0) * 2)) { - return std::max(this->switching_delay_.value_or(0) * 2, std::max(this->start_delay_, this->stop_delay_)); + return std::max({this->switching_delay_.value_or(0) * 2, this->start_delay_, this->stop_delay_}); } return run_duration; } diff --git a/esphome/core/color.h b/esphome/core/color.h index 32d63b18569..442470623df 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -169,7 +169,7 @@ struct Color { uint8_t r = rand >> 16; uint8_t g = rand >> 8; uint8_t b = rand >> 0; - const uint16_t max_rgb = std::max(r, std::max(g, b)); + const uint16_t max_rgb = std::max({r, g, b}); return Color(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)), uint8_t((uint16_t(b) * 255U / max_rgb)), w); } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e71da95e6b3..1eb33454910 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -663,8 +663,8 @@ float gamma_uncorrect(float value, float gamma) { } void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) { - float max_color_value = std::max(std::max(red, green), blue); - float min_color_value = std::min(std::min(red, green), blue); + float max_color_value = std::max({red, green, blue}); + float min_color_value = std::min({red, green, blue}); float delta = max_color_value - min_color_value; if (delta == 0) { diff --git a/esphome/core/preference_backend.h b/esphome/core/preference_backend.h index 3766934da46..431de205af9 100644 --- a/esphome/core/preference_backend.h +++ b/esphome/core/preference_backend.h @@ -69,6 +69,10 @@ template<typename Derived> class PreferencesMixin { ESPPreferenceObject make_preference(uint32_t type) { return static_cast<Derived *>(this)->make_preference(sizeof(T), type); } + + private: + PreferencesMixin() = default; + friend Derived; }; // Macro for platform preferences.h headers to declare the standard aliases. diff --git a/requirements_dev.txt b/requirements_dev.txt index 0884e5b5e46..31463e07c37 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ # Useful stuff when working in a development environment clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating -clang-tidy==18.1.8 # When updating clang-tidy, also update Dockerfile +clang-tidy==22.1.0.1 yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating diff --git a/script/clang-tidy b/script/clang-tidy index f2834b44ac8..1c413ffa23c 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -295,7 +295,7 @@ def main(): failed_files = [] try: - executable = get_binary("clang-tidy", 18) + executable = get_binary("clang-tidy", 22) task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): @@ -341,13 +341,13 @@ def main(): try: try: subprocess.call( - ["clang-apply-replacements-18", tmpdir], close_fds=False + ["clang-apply-replacements-22", tmpdir], close_fds=False ) except FileNotFoundError: subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False) except FileNotFoundError: print( - "Error please install clang-apply-replacements-18 or clang-apply-replacements.\n", + "Error please install clang-apply-replacements-22 or clang-apply-replacements.\n", file=sys.stderr, ) except: From cecccebc64a3838f7bb2bec40d0c82c7150620d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 15:35:04 -0500 Subject: [PATCH 1769/2056] [core] DelayAction: drop Component inheritance, use self-keyed scheduler (#16129) --- esphome/automation.py | 3 +-- esphome/core/base_automation.h | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 20eb9358cad..1689d29c42f 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -127,7 +127,7 @@ def validate_potentially_or_condition(value): return validate_condition(value) -DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) +DelayAction = cg.esphome_ns.class_("DelayAction", Action) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action) @@ -396,7 +396,6 @@ async def delay_action_to_code( args: TemplateArgsType, ) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) - await cg.register_component(var, {}) template_ = await cg.templatable(config, args, cg.uint32) cg.add(var.set_delay(template_)) return var diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index afd11c6867d..dcad7c9d2e7 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -178,7 +178,7 @@ class ProjectUpdateTrigger : public Trigger<std::string>, public Component { }; #endif -template<typename... Ts> class DelayAction : public Action<Ts...>, public Component { +template<typename... Ts> class DelayAction : public Action<Ts...> { public: explicit DelayAction() = default; @@ -198,8 +198,8 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon // to avoid overhead from capturing arguments by value if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( - this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, - static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(), + /* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER, + /* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(), [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { @@ -208,18 +208,18 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon // `mutable` is required so captured copies of non-const reference args (e.g. std::string&) // are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&` auto f = [this, x...]() mutable { this->play_next_(x...); }; - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, - nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), - this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + App.scheduler.set_timer_common_( + /* component= */ nullptr, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::SELF_POINTER, + /* static_name= */ reinterpret_cast<const char *>(this), /* hash_or_id= */ 0, this->delay_.value(x...), + std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } } - float get_setup_priority() const override { return setup_priority::HARDWARE; } void play(const Ts &...x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); } + void stop() override { App.scheduler.cancel_timeout(this); } }; template<typename... Ts> class LambdaAction : public Action<Ts...> { From 77da64a367c62a123d01559d344255a2c338bebe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:05:51 -0400 Subject: [PATCH 1770/2056] [sx126x] Add cold sleep option and drop unused RTC wakeup bit (#16144) --- esphome/components/sx126x/__init__.py | 58 +++++++++++++++++++++----- esphome/components/sx126x/automation.h | 3 +- esphome/components/sx126x/sx126x.cpp | 5 ++- esphome/components/sx126x/sx126x.h | 2 +- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index b8696158fe5..a4ba5c34f31 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from esphome import automation, pins import esphome.codegen as cg from esphome.components import spi @@ -5,6 +7,8 @@ from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.core import ID, TimePeriod +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType, TemplateArgsType MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -15,6 +19,7 @@ CONF_SX126X_ID = "sx126x_id" CONF_BANDWIDTH = "bandwidth" CONF_BITRATE = "bitrate" CONF_CODING_RATE = "coding_rate" +CONF_COLD = "cold" CONF_CRC_INVERTED = "crc_inverted" CONF_CRC_SIZE = "crc_size" CONF_CRC_POLYNOMIAL = "crc_polynomial" @@ -144,7 +149,7 @@ SetModeStandbyAction = sx126x_ns.class_( ) -def validate_raw_data(value): +def validate_raw_data(value: Any) -> bytes | list[int]: if isinstance(value, str): return value.encode("utf-8") if isinstance(value, list): @@ -154,7 +159,7 @@ def validate_raw_data(value): ) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: lora_bws = [ "7_8kHz", "10_4kHz", @@ -235,7 +240,7 @@ CONFIG_SCHEMA = ( ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await spi.register_spi_device(var, config) @@ -307,24 +312,50 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -@automation.register_action( - "sx126x.set_mode_sleep", - SetModeSleepAction, - NO_ARGS_ACTION_SCHEMA, - synchronous=True, -) @automation.register_action( "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA, synchronous=True, ) -async def no_args_action_to_code(config, action_id, template_arg, args): +async def no_args_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var +SET_MODE_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SX126x), + cv.Optional(CONF_COLD, default=False): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "sx126x.set_mode_sleep", + SetModeSleepAction, + SET_MODE_SLEEP_ACTION_SCHEMA, + synchronous=True, +) +async def set_mode_sleep_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_COLD], args, bool) + cg.add(var.set_cold(template_)) + return var + + SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(SX126x), @@ -340,7 +371,12 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( SEND_PACKET_ACTION_SCHEMA, synchronous=True, ) -async def send_packet_action_to_code(config, action_id, template_arg, args): +async def send_packet_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) data = config[CONF_DATA] diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 2282c583cbf..ed5986e0974 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -56,7 +56,8 @@ template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Pa template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> { public: - void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } + TEMPLATABLE_VALUE(bool, cold) + void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); } }; template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> { diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 6ea09e3a9e8..02f7d972a99 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -459,9 +459,10 @@ void SX126x::set_mode_tx() { this->write_opcode_(RADIO_SET_TX, buf, 3); } -void SX126x::set_mode_sleep() { +void SX126x::set_mode_sleep(bool cold) { + // 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power) uint8_t buf[1]; - buf[0] = 0x05; + buf[0] = cold ? 0x00 : 0x04; this->write_opcode_(RADIO_SET_SLEEP, buf, 1); } diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index edc00e3727e..87bbf18c793 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -79,7 +79,7 @@ class SX126x : public Component, void set_mode_rx(); void set_mode_tx(); void set_mode_standby(SX126xStandbyMode mode); - void set_mode_sleep(); + void set_mode_sleep(bool cold = false); void set_modulation(uint8_t modulation) { this->modulation_ = modulation; } void set_pa_power(int8_t power) { this->pa_power_ = power; } void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; } From e4b33fddf5556aa3a7d806a7d6b1ee2caa522d8c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:43:15 -0400 Subject: [PATCH 1771/2056] [esp32] Add ESP-IDF 6.0.1 platform entry (#16146) --- esphome/components/esp32/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index eb023ce32c3..bd299d71f05 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -729,6 +729,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version( + 6, 0, 1 + ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", cv.Version( 6, 0, 0 ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", From 9b1f5c59bb6ea7417174a8751d458c4f9bdb2722 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 18:05:38 -0500 Subject: [PATCH 1772/2056] [core] Fix null deref in WarnIfComponentBlockingGuard for self-keyed scheduler timers (#16150) --- esphome/core/component.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 6afcfda41db..185d51ab37d 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -655,7 +655,15 @@ class WarnIfComponentBlockingGuard { // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { #ifdef USE_RUNTIME_STATS - this->component_->runtime_stats_.record_time(micros() - this->started_us_); + uint32_t elapsed_us = micros() - this->started_us_; + // component_ is nullptr for self-keyed scheduler items (set_timeout/set_interval(self, ...)) + if (this->component_ != nullptr) { + this->component_->runtime_stats_.record_time(elapsed_us); + } else { + // Still accumulate into the global counter so Application::loop() can subtract + // this time from before_loop_tasks_ wall time. + ComponentRuntimeStats::global_recorded_us += elapsed_us; + } #endif uint32_t curr_time = MillisInternal::get(); #ifndef USE_BENCHMARK From b8d24c9e494f4abfd24e700ad52b1430c25334fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 18:14:07 -0500 Subject: [PATCH 1773/2056] [mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#16149) --- esphome/components/mcp23xxx_base/__init__.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index cd952099c05..76a3aabe3fa 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -2,6 +2,7 @@ from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ALLOW_OTHER_USES, CONF_ID, CONF_INPUT, CONF_INTERRUPT, @@ -30,10 +31,29 @@ MCP23XXX_INTERRUPT_MODES = { "FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING, } + +def _validate_interrupt_pin(value): + # The MCP component owns INT polarity (active-low, hardcoded falling-edge ISR) + # and installs a single ISR per GPIO, so neither inversion nor sharing is supported. + value = pins.internal_gpio_input_pin_schema(value) + if value.get(CONF_INVERTED): + raise cv.Invalid( + f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; " + "the MCP23xxx INT line is fixed active-low" + ) + if value.get(CONF_ALLOW_OTHER_USES): + raise cv.Invalid( + f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; " + "sharing the interrupt pin between multiple MCP23xxx (or other components) " + "is not implemented. Remove the interrupt_pin to fall back to polling." + ) + return value + + MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): _validate_interrupt_pin, } ).extend(cv.COMPONENT_SCHEMA) From 8066325e0b89ac4b0c08d54a95a5828f5235aefb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:52:25 +1200 Subject: [PATCH 1774/2056] Bump esphome/workflows/.github/workflows/lock.yml from 2026.4.0 to 2026.4.1 (#16143) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 20f9a74ea97..8d1dfe857de 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -8,4 +8,4 @@ on: jobs: lock: - uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0 + uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 From 47765bd2d07901e233ad56b1d7dfb272a932d16a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:08:56 +1200 Subject: [PATCH 1775/2056] [ci] Correct version comment on create-github-app-token pin (#16156) --- .github/workflows/auto-label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 0e5ceb9346c..5e685ce0aaf 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -27,7 +27,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From 1a871e231dd658e99e44c27d34ffe3f6358541cd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:09:37 +1200 Subject: [PATCH 1776/2056] [ci] Use client-id for GitHub App token generation (#16155) --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 5e685ce0aaf..ea22f75ef04 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -29,7 +29,7 @@ jobs: id: generate-token uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: - app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - name: Auto Label PR diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35b9e065e1e..a16af92b6fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -223,7 +223,7 @@ jobs: id: generate-token uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: - app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: home-assistant-addon @@ -258,7 +258,7 @@ jobs: id: generate-token uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: - app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: esphome-schema @@ -289,7 +289,7 @@ jobs: id: generate-token uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: - app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: version-notifier From f0bffed3c04c75f65513a4405914e093340d5782 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 22:42:17 -0500 Subject: [PATCH 1777/2056] [esp8266] Move HAL bodies into components/esp8266/hal.cpp + inline arch_init (#16112) --- esphome/components/esp8266/core.cpp | 98 +----------------------- esphome/components/esp8266/hal.cpp | 111 ++++++++++++++++++++++++++++ esphome/core/hal.h | 7 +- esphome/core/hal/hal_esp32.h | 3 + esphome/core/hal/hal_esp8266.h | 8 +- esphome/core/hal/hal_host.h | 2 + esphome/core/hal/hal_libretiny.h | 2 + esphome/core/hal/hal_rp2040.h | 2 + esphome/core/hal/hal_zephyr.h | 2 + 9 files changed, 133 insertions(+), 102 deletions(-) create mode 100644 esphome/components/esp8266/hal.cpp diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 9161ca6aaf5..b9ad4082e98 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,98 +3,12 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/time_64.h" -#include "esphome/core/helpers.h" -#include "preferences.h" #include <Arduino.h> -#include <core_esp8266_features.h> - -extern "C" { -#include <user_interface.h> -} namespace esphome { -// yield(), micros(), millis_64() inlined in hal.h. -// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit -// multiplies on the LX106). Tracks a running ms counter from 32-bit -// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis -// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g. -// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static -// state against ISR re-entry; the critical section is bounded (≤10 while-loop -// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on -// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level -// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis(). -// -// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles -// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a -// >71 min block would trip the watchdog long before it could matter here. -static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000; -static constexpr uint32_t US_PER_MS = 1000; - -uint32_t IRAM_ATTR HOT millis() { - // Struct packs the three statics so the compiler loads one base address - // instead of three separate literal pool entries (saves ~8 bytes IRAM). - static struct { - uint32_t cache; - uint32_t remainder; - uint32_t last_us; - } state = {0, 0, 0}; - uint32_t ps = xt_rsil(15); - uint32_t now_us = system_get_time(); - uint32_t delta = now_us - state.last_us; - state.last_us = now_us; - state.remainder += delta; - if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) { - // Rare path: large gap (WiFi scan, boot, long block). Constant-time - // conversion keeps the critical section bounded. - uint32_t ms = state.remainder / US_PER_MS; - state.cache += ms; - // Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a - // second __umodsi3 call on the LX106 (no hardware divide). - state.remainder -= ms * US_PER_MS; - } else { - // Common path: small gap. At most ~10 iterations since remainder was - // < threshold (10 ms) on entry and delta adds at most one more threshold - // before exiting this branch. - while (state.remainder >= US_PER_MS) { - state.cache++; - state.remainder -= US_PER_MS; - } - } - uint32_t result = state.cache; - xt_wsr_ps(ps); - return result; -} -// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object -// call to the original millis() that --wrap can't intercept, so calling ::delay() -// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still -// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and -// WiFi run correctly. Theoretically less power-efficient than Arduino's -// os_timer-based delay() for long waits, but nearly all ESPHome delays are short -// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is -// negligible. -void HOT delay(uint32_t ms) { - if (ms == 0) { - optimistic_yield(1000); - return; - } - uint32_t start = millis(); - while (millis() - start < ms) { - optimistic_yield(1000); - } -} -// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h. -void arch_restart() { - system_restart(); - // restart() doesn't always end execution - while (true) { // NOLINT(clang-diagnostic-unreachable-code) - yield(); - } -} -void arch_init() {} -uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } -uint32_t arch_get_cpu_freq_hz() { return F_CPU; } +// HAL functions live in hal.cpp. This file keeps only the ESP8266-specific +// firmware bootstrap (Tasmota OTA magic bytes, optional GPIO pre-init). void force_link_symbols() { // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible @@ -131,12 +45,4 @@ extern "C" void resetPins() { // NOLINT } // namespace esphome -// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator. -// Requires -Wl,--wrap=millis in build flags (added by __init__.py). -// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) -extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); } -// Note: Arduino's init() registers a 60-second overflow timer for micros64(). -// We leave it running — wrapping init() as a no-op would break micros64()'s -// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s). - #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/hal.cpp b/esphome/components/esp8266/hal.cpp new file mode 100644 index 00000000000..56910e5b399 --- /dev/null +++ b/esphome/components/esp8266/hal.cpp @@ -0,0 +1,111 @@ +#ifdef USE_ESP8266 + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include <Arduino.h> +#include <core_esp8266_features.h> + +extern "C" { +#include <user_interface.h> +} + +// Empty esp8266 namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// esp8266 component's API. +namespace esphome::esp8266 {} // namespace esphome::esp8266 + +namespace esphome { + +// yield(), micros(), millis_64(), delayMicroseconds(), arch_feed_wdt(), +// progmem_read_*() are inlined in core/hal/hal_esp8266.h. +// +// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit +// multiplies on the LX106). Tracks a running ms counter from 32-bit +// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis +// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g. +// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static +// state against ISR re-entry; the critical section is bounded (≤10 while-loop +// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on +// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level +// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis(). +// +// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles +// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a +// >71 min block would trip the watchdog long before it could matter here. +static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000; +static constexpr uint32_t US_PER_MS = 1000; + +uint32_t IRAM_ATTR HOT millis() { + // Struct packs the three statics so the compiler loads one base address + // instead of three separate literal pool entries (saves ~8 bytes IRAM). + static struct { + uint32_t cache; + uint32_t remainder; + uint32_t last_us; + } state = {0, 0, 0}; + uint32_t ps = xt_rsil(15); + uint32_t now_us = system_get_time(); + uint32_t delta = now_us - state.last_us; + state.last_us = now_us; + state.remainder += delta; + if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) { + // Rare path: large gap (WiFi scan, boot, long block). Constant-time + // conversion keeps the critical section bounded. + uint32_t ms = state.remainder / US_PER_MS; + state.cache += ms; + // Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a + // second __umodsi3 call on the LX106 (no hardware divide). + state.remainder -= ms * US_PER_MS; + } else { + // Common path: small gap. At most ~10 iterations since remainder was + // < threshold (10 ms) on entry and delta adds at most one more threshold + // before exiting this branch. + while (state.remainder >= US_PER_MS) { + state.cache++; + state.remainder -= US_PER_MS; + } + } + uint32_t result = state.cache; + xt_wsr_ps(ps); + return result; +} + +// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object +// call to the original millis() that --wrap can't intercept, so calling ::delay() +// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still +// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and +// WiFi run correctly. Theoretically less power-efficient than Arduino's +// os_timer-based delay() for long waits, but nearly all ESPHome delays are short +// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is +// negligible. +void HOT delay(uint32_t ms) { + if (ms == 0) { + optimistic_yield(1000); + return; + } + uint32_t start = millis(); + while (millis() - start < ms) { + optimistic_yield(1000); + } +} + +void arch_restart() { + system_restart(); + // restart() doesn't always end execution + while (true) { // NOLINT(clang-diagnostic-unreachable-code) + yield(); + } +} + +} // namespace esphome + +// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator. +// Requires -Wl,--wrap=millis in build flags (added by __init__.py). +// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); } +// Note: Arduino's init() registers a 60-second overflow timer for micros64(). +// We leave it running — wrapping init() as a no-op would break micros64()'s +// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s). + +#endif // USE_ESP8266 diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 312effa7b0e..a53296979c6 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -31,11 +31,10 @@ namespace esphome { // Cross-platform declarations. delayMicroseconds(), arch_feed_wdt(), -// arch_get_cpu_cycle_count() vary per platform (some inline, some -// out-of-line) so they live in hal/hal_<platform>.h. +// arch_get_cpu_cycle_count(), arch_init(), arch_get_cpu_freq_hz() vary +// per platform (some inline, some out-of-line) so they live in +// hal/hal_<platform>.h. void __attribute__((noreturn)) arch_restart(); -void arch_init(); -uint32_t arch_get_cpu_freq_hz(); #ifndef USE_ESP8266 // All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences. diff --git a/esphome/core/hal/hal_esp32.h b/esphome/core/hal/hal_esp32.h index 2bff4244419..d5d7752bf6a 100644 --- a/esphome/core/hal/hal_esp32.h +++ b/esphome/core/hal/hal_esp32.h @@ -42,6 +42,9 @@ __attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { dela __attribute__((always_inline)) inline void arch_feed_wdt() { esp_task_wdt_reset(); } __attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } +void arch_init(); +uint32_t arch_get_cpu_freq_hz(); + } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/core/hal/hal_esp8266.h index 484118f1f50..b6e3b1ee3cb 100644 --- a/esphome/core/hal/hal_esp8266.h +++ b/esphome/core/hal/hal_esp8266.h @@ -3,6 +3,7 @@ #ifdef USE_ESP8266 #include <c_types.h> +#include <core_esp8266_features.h> #include <cstdint> #include <pgmspace.h> @@ -59,8 +60,11 @@ __attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_ // NOLINTNEXTLINE(readability-identifier-naming) __attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } __attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); } - -uint32_t arch_get_cpu_cycle_count(); +__attribute__((always_inline)) inline void arch_init() {} +// esp_get_cycle_count() declared in <core_esp8266_features.h>; F_CPU is a +// compiler-driven macro from the ESP8266 Arduino board defs (-DF_CPU=...). +__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return F_CPU; } } // namespace esphome diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h index 682a1a422b4..a8896fdf638 100644 --- a/esphome/core/hal/hal_host.h +++ b/esphome/core/hal/hal_host.h @@ -22,6 +22,8 @@ uint64_t millis_64(); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); +void arch_init(); +uint32_t arch_get_cpu_freq_hz(); } // namespace esphome diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h index e9d33b7753b..ecfe830fe35 100644 --- a/esphome/core/hal/hal_libretiny.h +++ b/esphome/core/hal/hal_libretiny.h @@ -91,6 +91,8 @@ __attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); +void arch_init(); +uint32_t arch_get_cpu_freq_hz(); } // namespace esphome diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h index 2a1d67b4a32..46f6e421cd4 100644 --- a/esphome/core/hal/hal_rp2040.h +++ b/esphome/core/hal/hal_rp2040.h @@ -38,6 +38,8 @@ __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_mi void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); +void arch_init(); +uint32_t arch_get_cpu_freq_hz(); } // namespace esphome diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h index 6707c85b2c2..d4b37b5eb6c 100644 --- a/esphome/core/hal/hal_zephyr.h +++ b/esphome/core/hal/hal_zephyr.h @@ -22,6 +22,8 @@ uint64_t millis_64(); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); +void arch_init(); +uint32_t arch_get_cpu_freq_hz(); } // namespace esphome From e127268dac9b2072b9abd8174f2ba804dfad0b90 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:04:52 +1200 Subject: [PATCH 1778/2056] [core] Strip \\?\ prefix from sys.executable for PlatformIO subprocess (#16158) --- esphome/platformio_api.py | 44 ++++++++++- tests/unit_tests/test_platformio_api.py | 99 +++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index dec541985f6..c0cd048890a 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -14,6 +14,37 @@ from esphome.util import run_external_process _LOGGER = logging.getLogger(__name__) +def _strip_win_long_path_prefix(path: str) -> str: + r"""Strip the Windows extended-length path prefix from ``path``. + + Handles both forms documented at + https://learn.microsoft.com/windows/win32/fileio/naming-a-file: + + * ``\\?\C:\path\to\file`` -> ``C:\path\to\file`` + * ``\\?\UNC\server\share\path`` -> ``\\server\share\path`` + + The NSIS-installed ``esphome.exe`` launcher on Windows starts Python with + ``sys.executable`` already prefixed with ``\\?\``. That prefix propagates + into PlatformIO's ``$PYTHONEXE`` (PlatformIO reads ``PYTHONEXEPATH`` from + the environment, falling back to ``os.path.normpath(sys.executable)``) + and ends up baked into SCons-emitted command lines for build steps such + as the esp8266 ``elf2bin`` invocation. ``cmd.exe`` does not understand + the ``\\?\`` prefix, so the build fails with + "The system cannot find the path specified." Stripping the prefix early + keeps the path shell-quotable. + + No-op on non-Windows platforms. + """ + if sys.platform != "win32": + return path + if path.startswith("\\\\?\\UNC\\"): + # \\?\UNC\server\share\... -> \\server\share\... + return "\\\\" + path[len("\\\\?\\UNC\\") :] + if path.startswith("\\\\?\\"): + return path[len("\\\\?\\") :] + return path + + def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -24,7 +55,18 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") # Increase uv retry count to handle transient network errors (default is 3) os.environ.setdefault("UV_HTTP_RETRIES", "10") - cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) + # Strip the Windows extended-length path prefix from sys.executable so it + # doesn't propagate into PlatformIO's $PYTHONEXE and break SCons-emitted + # command lines run through cmd.exe. + python_exe = _strip_win_long_path_prefix(sys.executable) + if python_exe != sys.executable: + # Only override PYTHONEXEPATH when we actually stripped a prefix. + # PlatformIO's get_pythonexe_path() reads this and falls back to + # sys.executable otherwise; setting it unconditionally would clobber + # a user-provided value (or the unmodified path on platforms that + # don't need the strip). + os.environ["PYTHONEXEPATH"] = python_exe + cmd = [python_exe, "-m", "esphome.platformio_runner"] + list(args) return run_external_process(*cmd, **kwargs) diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 67e64e5f612..b241622f89d 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -311,6 +311,105 @@ def test_run_platformio_cli_sets_environment_variables( assert "arg" in args +@pytest.mark.parametrize( + ("platform", "input_path", "expected"), + [ + # win32: drive-letter extended-length prefix is stripped + ( + "win32", + "\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + ), + # win32: UNC extended-length prefix is translated to a regular UNC path + ( + "win32", + "\\\\?\\UNC\\server\\share\\python.exe", + "\\\\server\\share\\python.exe", + ), + # win32: paths without the prefix are returned unchanged + ( + "win32", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + ), + # non-win32: prefix is left alone (no-op) + ("linux", "\\\\?\\C:\\python.exe", "\\\\?\\C:\\python.exe"), + ("darwin", "/usr/bin/python3", "/usr/bin/python3"), + ], +) +def test_strip_win_long_path_prefix( + platform: str, input_path: str, expected: str +) -> None: + r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32.""" + with patch("esphome.platformio_api.sys.platform", platform): + assert platformio_api._strip_win_long_path_prefix(input_path) == expected + + +def test_run_platformio_cli_strips_win_long_path_prefix( + setup_core: Path, mock_run_external_process: Mock +) -> None: + r"""Windows ``\\?\`` prefix on sys.executable does not leak into the subprocess. + + The NSIS-installed esphome.exe launcher starts Python with + ``sys.executable`` already prefixed by the extended-length path marker. + That prefix would otherwise propagate into PlatformIO's ``PYTHONEXE`` and + break SCons-emitted command lines run through ``cmd.exe``. + """ + CORE.build_path = str(setup_core / "build" / "test") + prefixed_exe = ( + "\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe" + ) + stripped_exe = ( + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe" + ) + + with ( + patch.dict(os.environ, {}, clear=False), + patch("esphome.platformio_api.sys.platform", "win32"), + patch("esphome.platformio_api.sys.executable", prefixed_exe), + ): + # Pop any pre-existing PYTHONEXEPATH so the assertion below reflects + # what run_platformio_cli set, not whatever the test runner's + # environment happened to contain. + os.environ.pop("PYTHONEXEPATH", None) + mock_run_external_process.return_value = 0 + platformio_api.run_platformio_cli("test", "arg") + + # The subprocess is invoked with the stripped executable path. + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert args[0] == stripped_exe + # PYTHONEXEPATH is exported with the stripped path so PlatformIO's + # get_pythonexe_path() picks it up in the subprocess. + assert os.environ["PYTHONEXEPATH"] == stripped_exe + + +def test_run_platformio_cli_does_not_set_pythonexepath_without_strip( + setup_core: Path, mock_run_external_process: Mock +) -> None: + r"""PYTHONEXEPATH is not touched when sys.executable has no ``\\?\`` prefix. + + Setting it unconditionally would clobber a user-provided value (or + interfere with non-Windows tooling that has no prefix to strip). + """ + CORE.build_path = str(setup_core / "build" / "test") + plain_exe = "/usr/bin/python3" + + with ( + patch.dict(os.environ, {}, clear=False), + patch("esphome.platformio_api.sys.platform", "linux"), + patch("esphome.platformio_api.sys.executable", plain_exe), + ): + os.environ.pop("PYTHONEXEPATH", None) + mock_run_external_process.return_value = 0 + platformio_api.run_platformio_cli("test", "arg") + + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert args[0] == plain_exe + assert "PYTHONEXEPATH" not in os.environ + + def test_run_platformio_cli_run_builds_command( setup_core: Path, mock_run_platformio_cli: Mock ) -> None: From 096d0c4279bea52776fdbb3d1ab004d2cd86f6a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:45:19 +0000 Subject: [PATCH 1779/2056] Bump aioesphomeapi from 44.22.0 to 44.23.0 (#16161) 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 abc8ac5dbb6..789a3f79957 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==44.22.0 +aioesphomeapi==44.23.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 1398dcebb459c0c974a3986028a112184ce7eb59 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:53:37 +1000 Subject: [PATCH 1780/2056] [st7789v] Add deprecation warnings (#16162) --- esphome/components/st7789v/__init__.py | 5 +++++ esphome/components/st7789v/display.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/esphome/components/st7789v/__init__.py b/esphome/components/st7789v/__init__.py index 3e64d09c57f..7915cf119c9 100644 --- a/esphome/components/st7789v/__init__.py +++ b/esphome/components/st7789v/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg st7789v_ns = cg.esphome_ns.namespace("st7789v") + +DEPRECATED_COMPONENT = """ +The 'st7789v' component is deprecated and no new functionality will be added to it. +PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 745c37f47dd..3b4d6d99ea5 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import display, power_supply, spi @@ -26,6 +28,8 @@ CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["spi"] +LOGGER = logging.getLogger(__name__) + ST7789V = st7789v_ns.class_( "ST7789V", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer ) @@ -175,6 +179,9 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): + LOGGER.warning( + "The 'st7789v' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) await spi.register_spi_device(var, config, write_only=True) From a8b0133ec115984ed05b8129a6e85dbc56e93166 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 30 Apr 2026 08:49:28 -0400 Subject: [PATCH 1781/2056] [audio] Enable specific codecs and configure advanced features (#16166) --- esphome/components/audio/__init__.py | 196 ++++++++++++++++++++- tests/components/audio/common.yaml | 14 ++ tests/components/audio/test.esp32-idf.yaml | 1 + 3 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 tests/components/audio/common.yaml create mode 100644 tests/components/audio/test.esp32-idf.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index fe111be31e7..db8f69e6a51 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import esphome.codegen as cg from esphome.components.esp32 import ( @@ -7,7 +7,12 @@ from esphome.components.esp32 import ( include_builtin_idf_component, ) import esphome.config_validation as cv -from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.const import ( + CONF_BITS_PER_SAMPLE, + CONF_NUM_CHANNELS, + CONF_SAMPLE_RATE, + CONF_SIZE, +) from esphome.core import CORE import esphome.final_validate as fv @@ -25,13 +30,46 @@ AUDIO_FILE_TYPE_ENUM = { "OPUS": AudioFileType.OPUS, } +MEMORY_PSRAM = "psram" +MEMORY_INTERNAL = "internal" +MEMORY_LOCATIONS = [MEMORY_PSRAM, MEMORY_INTERNAL] + + +@dataclass +class FlacOptions: + buffer_memory: str | None = None + + +@dataclass +class Mp3Options: + buffer_memory: str | None = None + + +@dataclass +class OpusPseudostackOptions: + threadsafe: bool | None = None + buffer_memory: str | None = None + size: int | None = None + + +@dataclass +class OpusOptions: + floating_point: bool | None = None + state_memory: str | None = None + pseudostack: OpusPseudostackOptions = field(default_factory=OpusPseudostackOptions) + @dataclass class AudioData: flac_support: bool = False mp3_support: bool = False opus_support: bool = False + # WAV defaults to True for backward compatibility; will become opt-in in a future release + wav_support: bool = True micro_decoder_support: bool = False + flac: FlacOptions = field(default_factory=FlacOptions) + mp3: Mp3Options = field(default_factory=Mp3Options) + opus: OpusOptions = field(default_factory=OpusOptions) def _get_data() -> AudioData: @@ -55,6 +93,11 @@ def request_opus_support() -> None: _get_data().opus_support = True +def request_wav_support() -> None: + """Request WAV codec support for audio decoding.""" + _get_data().wav_support = True + + def request_micro_decoder_support() -> None: """Request micro-decoder library support for audio decoding.""" _get_data().micro_decoder_support = True @@ -67,9 +110,78 @@ CONF_MAX_CHANNELS = "max_channels" CONF_MIN_SAMPLE_RATE = "min_sample_rate" CONF_MAX_SAMPLE_RATE = "max_sample_rate" +CONF_CODECS = "codecs" +CONF_WAV = "wav" +CONF_FLAC = "flac" +CONF_MP3 = "mp3" +CONF_OPUS = "opus" +CONF_BUFFER_MEMORY = "buffer_memory" +CONF_FLOATING_POINT = "floating_point" +CONF_STATE_MEMORY = "state_memory" +CONF_PSEUDOSTACK = "pseudostack" +CONF_THREADSAFE = "threadsafe" + + +_MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True) + + +def _maybe_empty_codec(schema): + """Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict.""" + + def validator(value): + if value is None: + value = {} + return schema(value) + + return validator + + +CODEC_FLAC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR, + } +) + +CODEC_MP3_SCHEMA = cv.Schema( + { + cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR, + } +) + +OPUS_PSEUDOSTACK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_THREADSAFE): cv.boolean, + cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR, + cv.Optional(CONF_SIZE): cv.int_range(60000, 240000), + } +) + +CODEC_OPUS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_FLOATING_POINT): cv.boolean, + cv.Optional(CONF_STATE_MEMORY): _MEMORY_LOCATION_VALIDATOR, + cv.Optional(CONF_PSEUDOSTACK): _maybe_empty_codec(OPUS_PSEUDOSTACK_SCHEMA), + } +) + +CODEC_WAV_SCHEMA = cv.Schema({}) + +CODECS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_FLAC): _maybe_empty_codec(CODEC_FLAC_SCHEMA), + cv.Optional(CONF_MP3): _maybe_empty_codec(CODEC_MP3_SCHEMA), + cv.Optional(CONF_OPUS): _maybe_empty_codec(CODEC_OPUS_SCHEMA), + cv.Optional(CONF_WAV): _maybe_empty_codec(CODEC_WAV_SCHEMA), + } +) CONFIG_SCHEMA = cv.All( - cv.Schema({}), + cv.Schema( + { + cv.Optional(CONF_CODECS): _maybe_empty_codec(CODECS_SCHEMA), + } + ), + cv.only_on_esp32, ) AUDIO_COMPONENT_SCHEMA = cv.Schema( @@ -208,6 +320,15 @@ def final_validate_audio_schema( ) +def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> None: + if value == MEMORY_PSRAM: + add_idf_sdkconfig_option(psram_key, True) + add_idf_sdkconfig_option(internal_key, False) + elif value == MEMORY_INTERNAL: + add_idf_sdkconfig_option(psram_key, False) + add_idf_sdkconfig_option(internal_key, True) + + async def to_code(config): # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) include_builtin_idf_component("esp_http_client") @@ -219,6 +340,36 @@ async def to_code(config): data = _get_data() + # Merge user-supplied codec configuration (additive: presence enables the codec) + if codecs_config := config.get(CONF_CODECS): + if (flac_config := codecs_config.get(CONF_FLAC)) is not None: + data.flac_support = True + if (buffer_memory := flac_config.get(CONF_BUFFER_MEMORY)) is not None: + data.flac.buffer_memory = buffer_memory + if (mp3_config := codecs_config.get(CONF_MP3)) is not None: + data.mp3_support = True + if (buffer_memory := mp3_config.get(CONF_BUFFER_MEMORY)) is not None: + data.mp3.buffer_memory = buffer_memory + if (opus_config := codecs_config.get(CONF_OPUS)) is not None: + data.opus_support = True + floating_point = opus_config.get(CONF_FLOATING_POINT) + if floating_point is not None: + data.opus.floating_point = floating_point + if (state_memory := opus_config.get(CONF_STATE_MEMORY)) is not None: + data.opus.state_memory = state_memory + if (pseudostack_config := opus_config.get(CONF_PSEUDOSTACK)) is not None: + threadsafe = pseudostack_config.get(CONF_THREADSAFE) + if threadsafe is not None: + data.opus.pseudostack.threadsafe = threadsafe + if ( + buffer_memory := pseudostack_config.get(CONF_BUFFER_MEMORY) + ) is not None: + data.opus.pseudostack.buffer_memory = buffer_memory + if (size := pseudostack_config.get(CONF_SIZE)) is not None: + data.opus.pseudostack.size = size + if CONF_WAV in codecs_config: + data.wav_support = True + if data.micro_decoder_support: add_idf_component(name="esphome/micro-decoder", ref="0.2.0") @@ -229,13 +380,50 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False) if not data.opus_support: add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False) + if not data.wav_support: + add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_WAV", False) - # Legacy audio_decoder.cpp support defines and components + # Configure each codec library. + # Adds a define and IDF component for legacy `audio_decoder.cpp`. if data.flac_support: cg.add_define("USE_AUDIO_FLAC_SUPPORT") add_idf_component(name="esphome/micro-flac", ref="0.1.1") + _emit_memory_pair( + data.flac.buffer_memory, + "CONFIG_MICRO_FLAC_PREFER_PSRAM", + "CONFIG_MICRO_FLAC_PREFER_INTERNAL", + ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") + _emit_memory_pair( + data.mp3.buffer_memory, + "CONFIG_MP3_DECODER_PREFER_PSRAM", + "CONFIG_MP3_DECODER_PREFER_INTERNAL", + ) if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") add_idf_component(name="esphome/micro-opus", ref="0.3.6") + if data.opus.floating_point is not None: + add_idf_sdkconfig_option( + "CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point + ) + _emit_memory_pair( + data.opus.state_memory, + "CONFIG_OPUS_STATE_PREFER_PSRAM", + "CONFIG_OPUS_STATE_PREFER_INTERNAL", + ) + if data.opus.pseudostack.threadsafe is True: + add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", True) + add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", False) + elif data.opus.pseudostack.threadsafe is False: + add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", False) + add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", True) + _emit_memory_pair( + data.opus.pseudostack.buffer_memory, + "CONFIG_OPUS_PSEUDOSTACK_PREFER_PSRAM", + "CONFIG_OPUS_PSEUDOSTACK_PREFER_INTERNAL", + ) + if data.opus.pseudostack.size is not None: + add_idf_sdkconfig_option( + "CONFIG_OPUS_PSEUDOSTACK_SIZE", data.opus.pseudostack.size + ) diff --git a/tests/components/audio/common.yaml b/tests/components/audio/common.yaml new file mode 100644 index 00000000000..3cde9b84495 --- /dev/null +++ b/tests/components/audio/common.yaml @@ -0,0 +1,14 @@ +audio: + codecs: + flac: + buffer_memory: internal + mp3: + buffer_memory: psram + opus: + floating_point: false + state_memory: psram + pseudostack: + threadsafe: false + buffer_memory: internal + size: 80000 + wav: diff --git a/tests/components/audio/test.esp32-idf.yaml b/tests/components/audio/test.esp32-idf.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/audio/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 2758aa551782464ed34d1a1f7affe91e7d9120e8 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 30 Apr 2026 09:12:39 -0400 Subject: [PATCH 1782/2056] [audio] bump microOpus to v0.4.0 to use fixed-point by default on ESP32 (#16168) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index db8f69e6a51..8528e77ae7d 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -402,7 +402,7 @@ async def to_code(config): ) if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.3.6") + add_idf_component(name="esphome/micro-opus", ref="0.4.0") if data.opus.floating_point is not None: add_idf_sdkconfig_option( "CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index cb7f5903cfd..757fcf9dd77 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: esphome/micro-flac: version: 0.1.1 esphome/micro-opus: - version: 0.3.6 + version: 0.4.0 espressif/esp-dsp: version: "1.7.1" espressif/esp-tflite-micro: From f1d3be4bdaaa1d6fcf7119d075b41cdc57ea31eb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 30 Apr 2026 12:03:40 -0400 Subject: [PATCH 1783/2056] [core] Simplify RAMAllocator and add internal fallback to external mode (#16171) --- esphome/core/helpers.h | 61 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 355db6c7f4f..07bcb7a74fa 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2045,7 +2045,8 @@ void delay_microseconds_safe(uint32_t us); * Returns `nullptr` in case no memory is available. * * By setting flags, it can be configured to: - * - perform external allocation falling back to main memory if SPI RAM is full or unavailable + * - perform external allocation falling back to internal memory if SPI RAM is full or unavailable (default) + * - perform internal allocation falling back to external memory (with PREFER_INTERNAL) * - perform external allocation only * - perform internal allocation only */ @@ -2054,16 +2055,26 @@ template<class T> class RAMAllocator { using value_type = T; enum Flags { - NONE = 0, // Perform external allocation and fall back to internal memory - ALLOC_EXTERNAL = 1 << 0, // Perform external allocation only. - ALLOC_INTERNAL = 1 << 1, // Perform internal allocation only. - ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. + NONE = 0, // Perform external allocation and fall back to internal memory + ALLOC_EXTERNAL = 1 << 0, // Perform external allocation only. + ALLOC_INTERNAL = 1 << 1, // Perform internal allocation only. + ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. + PREFER_INTERNAL = 1 << 3, // Perform internal allocation and fall back to external memory }; constexpr RAMAllocator() = default; - constexpr RAMAllocator(uint8_t flags) - : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) - : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {} + constexpr RAMAllocator(uint8_t flags) { + if (flags & PREFER_INTERNAL) { + this->flags_ = ALLOC_INTERNAL | ALLOC_EXTERNAL | PREFER_INTERNAL; + return; + } + const uint8_t alloc_bits = flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL); + if (alloc_bits != 0) { + this->flags_ = alloc_bits; + return; + } + this->flags_ = ALLOC_INTERNAL | ALLOC_EXTERNAL; + } template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {} T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } @@ -2072,12 +2083,8 @@ template<class T> class RAMAllocator { size_t size = n * manual_size; T *ptr = nullptr; #ifdef USE_ESP32 - if (this->flags_ & Flags::ALLOC_EXTERNAL) { - ptr = static_cast<T *>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); - } - if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) { - ptr = static_cast<T *>(heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); - } + const auto caps = this->get_caps_(); + ptr = static_cast<T *>(heap_caps_malloc_prefer(size, 2, caps[0], caps[1])); #else // Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported ptr = static_cast<T *>(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) @@ -2091,12 +2098,8 @@ template<class T> class RAMAllocator { size_t size = n * manual_size; T *ptr = nullptr; #ifdef USE_ESP32 - if (this->flags_ & Flags::ALLOC_EXTERNAL) { - ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); - } - if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) { - ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); - } + const auto caps = this->get_caps_(); + ptr = static_cast<T *>(heap_caps_realloc_prefer(p, size, 2, caps[0], caps[1])); #else // Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported ptr = static_cast<T *>(realloc(p, size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) @@ -2147,6 +2150,24 @@ template<class T> class RAMAllocator { } private: +#ifdef USE_ESP32 + /// Returns {primary_caps, fallback_caps} for heap_caps_*_prefer based on the configured flags. + /// PREFER_INTERNAL implies both regions are enabled (enforced by the constructor), so when it is set + /// the primary is internal and the fallback is external. Otherwise the primary is whichever region + /// is enabled (external preferred when both are enabled), and the fallback is the other region (or + /// the same region when only one is enabled, making the second attempt a no-op). + std::array<uint32_t, 2> get_caps_() const { + constexpr uint32_t external_caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + constexpr uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; + if (this->flags_ & PREFER_INTERNAL) { + return {internal_caps, external_caps}; + } + const uint32_t primary = (this->flags_ & ALLOC_EXTERNAL) ? external_caps : internal_caps; + const uint32_t fallback = (this->flags_ & ALLOC_INTERNAL) ? internal_caps : external_caps; + return {primary, fallback}; + } +#endif + uint8_t flags_{ALLOC_INTERNAL | ALLOC_EXTERNAL}; }; From d48aad8c4d42431beb4f7f89f35bf42628128a04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 12:27:54 -0500 Subject: [PATCH 1784/2056] [esp32] Replace 512B stack buffer in printf wraps with picolibc cookie FILE (#16170) --- esphome/components/esp32/printf_stubs.cpp | 77 +++++++++++++++++++---- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index 386fbbd79d1..908b4023ead 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -13,14 +13,21 @@ * and printf() calls in SDK components are only in debug/assert paths * (gpio_dump_io_configuration, ringbuf diagnostics) that are either * GC'd or never called. Crash backtraces and panic output are - * unaffected — they use esp_rom_printf() which is a ROM function + * unaffected; they use esp_rom_printf() which is a ROM function * and does not go through libc. * - * These stubs redirect through vsnprintf() (which uses _svfprintf_r - * already in the binary) and fwrite(), allowing the linker to - * dead-code eliminate _vfprintf_r. + * On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we + * route output through a stack-allocated cookie FILE that forwards each + * byte to the real target stream via fputc(). Picolibc's tinystdio + * vfprintf walks the FILE::put callback one character at a time, so this + * costs ~32 bytes of stack for the cookie struct vs. a 512-byte format + * buffer. The buffered path overflows the loopTask stack on IDF 6. * - * Saves ~11 KB of flash. + * On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite + * path because that loopTask stack budget has plenty of headroom for the + * 512-byte buffer; the picolibc-only crash above does not affect it. + * + * Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc. * * To disable these wraps, set enable_full_printf: true in the esp32 * advanced config section. @@ -30,10 +37,55 @@ #include <cstdarg> #include <cstdio> +#ifndef __PICOLIBC__ #include "esp_system.h" +#endif namespace esphome::esp32 {} +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +#ifdef __PICOLIBC__ + +#include <cstddef> +#include <type_traits> + +extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap); + +namespace { + +struct CookieFile { + FILE base; + FILE *target; +}; + +// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is +// only well-defined when FILE is the first member at offset 0 and CookieFile +// is standard-layout. +static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile"); +static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout"); + +int cookie_put(char c, FILE *stream) { + auto *cookie = reinterpret_cast<CookieFile *>(stream); + return fputc(static_cast<unsigned char>(c), cookie->target); +} + +const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE); + +} // namespace + +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + CookieFile cookie; + cookie.base = COOKIE_FILE_TEMPLATE; + cookie.target = stream; + return __real_vfprintf(&cookie.base, fmt, ap); +} + +int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); } + +#else // !__PICOLIBC__ + static constexpr size_t PRINTF_BUFFER_SIZE = 512; // These stubs are essentially dead code at runtime — ESPHome replaces the @@ -55,14 +107,18 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) { return len; } -// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) -extern "C" { - int __wrap_vprintf(const char *fmt, va_list ap) { char buf[PRINTF_BUFFER_SIZE]; return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +#endif // __PICOLIBC__ + int __wrap_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); @@ -71,11 +127,6 @@ int __wrap_printf(const char *fmt, ...) { return len; } -int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { - char buf[PRINTF_BUFFER_SIZE]; - return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); -} - int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); From 61261b4a592fa449a1e820a6392c4696fbc3d49a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 12:33:22 -0500 Subject: [PATCH 1785/2056] [libretiny] Move HAL bodies into components/libretiny/hal.cpp + inline trivial dispatches (#16113) --- esphome/components/libretiny/core.cpp | 53 +-------------------------- esphome/components/libretiny/hal.cpp | 53 +++++++++++++++++++++++++++ esphome/core/hal/hal_libretiny.h | 18 +++++++-- 3 files changed, 69 insertions(+), 55 deletions(-) create mode 100644 esphome/components/libretiny/hal.cpp diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index f46abe3b81a..8686a41e646 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -1,55 +1,6 @@ #ifdef USE_LIBRETINY -#include "core.h" -#include "esphome/core/defines.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "preferences.h" - -#include <FreeRTOS.h> -#include <task.h> - -void setup(); -void loop(); - -namespace esphome { - -// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } - -void arch_init() { - libretiny::setup_preferences(); - lt_wdt_enable(10000L); -#ifdef USE_BK72XX - // BK72xx SDK creates the main Arduino task at priority 3, which is lower than - // all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop - // stalls whenever WiFi background processing runs, because the main task - // cannot resume until every higher-priority task finishes. - // - // By contrast, RTL87xx creates the main task at osPriorityRealtime (highest). - // - // Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the - // main loop, but below the TCP/IP thread (7) so packet processing keeps priority. - // This is safe because ESPHome yields voluntarily via wakeable_delay() and - // the Arduino mainTask yield() after each loop() iteration. - static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6; - static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES"); - vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY); -#endif -#if LT_GPIO_RECOVER - lt_gpio_recover(); -#endif -} - -void arch_restart() { - lt_reboot(); - while (1) { - } -} -void HOT arch_feed_wdt() { lt_wdt_feed(); } -uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } -uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } - -} // namespace esphome +// HAL functions live in hal.cpp. core.cpp is intentionally empty for +// libretiny — there is no extra component bootstrap to keep here. #endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/hal.cpp b/esphome/components/libretiny/hal.cpp new file mode 100644 index 00000000000..e6dbb7296c5 --- /dev/null +++ b/esphome/components/libretiny/hal.cpp @@ -0,0 +1,53 @@ +#ifdef USE_LIBRETINY + +#include "core.h" +#include "esphome/core/hal.h" +#include "preferences.h" + +#include <FreeRTOS.h> +#include <task.h> + +// Empty libretiny namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// libretiny component's API. +namespace esphome::libretiny {} // namespace esphome::libretiny + +namespace esphome { + +// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), +// arch_feed_wdt(), arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz() +// inlined in core/hal/hal_libretiny.h. + +void arch_init() { + libretiny::setup_preferences(); + lt_wdt_enable(10000L); +#ifdef USE_BK72XX + // BK72xx SDK creates the main Arduino task at priority 3, which is lower than + // all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop + // stalls whenever WiFi background processing runs, because the main task + // cannot resume until every higher-priority task finishes. + // + // By contrast, RTL87xx creates the main task at osPriorityRealtime (highest). + // + // Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the + // main loop, but below the TCP/IP thread (7) so packet processing keeps priority. + // This is safe because ESPHome yields voluntarily via wakeable_delay() and + // the Arduino mainTask yield() after each loop() iteration. + static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6; + static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES"); + vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY); +#endif +#if LT_GPIO_RECOVER + lt_gpio_recover(); +#endif +} + +void arch_restart() { + lt_reboot(); + while (1) { + } +} + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/core/hal/hal_libretiny.h index ecfe830fe35..db0fc11bfbe 100644 --- a/esphome/core/hal/hal_libretiny.h +++ b/esphome/core/hal/hal_libretiny.h @@ -51,8 +51,16 @@ extern "C" void yield(void); extern "C" void delay(unsigned long ms); extern "C" unsigned long micros(void); extern "C" unsigned long millis(void); +extern "C" void delayMicroseconds(unsigned int us); // NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +// Forward decls from libretiny's <lt_api.h> family for the inline arch_* +// wrappers below. Pulling the full header would drag in the rest of the +// LibreTiny C API. +extern "C" void lt_wdt_feed(void); +extern "C" uint32_t lt_cpu_get_cycle_count(void); +extern "C" uint32_t lt_cpu_get_freq(void); + namespace esphome { /// Returns true when executing inside an interrupt handler. @@ -88,11 +96,13 @@ __attribute__((always_inline)) inline uint32_t millis() { return static_cast<uin #endif __attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } -void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) -void arch_feed_wdt(); -uint32_t arch_get_cpu_cycle_count(); +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } +__attribute__((hot, always_inline)) inline void arch_feed_wdt() { lt_wdt_feed(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } + void arch_init(); -uint32_t arch_get_cpu_freq_hz(); } // namespace esphome From 2fbfb4c385506070b2ab23d4114ad221d48da544 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:10:53 -0500 Subject: [PATCH 1786/2056] [ci] Split integration tests into 3 buckets when count is more than 10 (#16152) --- .github/workflows/ci.yml | 29 +++++----- script/determine-jobs.py | 66 ++++++++++++++++++++-- tests/script/test_determine_jobs.py | 88 +++++++++++++++++++++++++++-- 3 files changed, 156 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57053c36457..3af1709774d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,8 +199,7 @@ jobs: - common outputs: integration-tests: ${{ steps.determine.outputs.integration-tests }} - integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }} - integration-test-files: ${{ steps.determine.outputs.integration-test-files }} + integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} @@ -243,8 +242,7 @@ jobs: # Extract individual fields echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT - echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT - echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT + echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT @@ -267,12 +265,16 @@ jobs: key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} integration-tests: - name: Run integration tests + name: Run integration tests (${{ matrix.bucket.name }}) runs-on: ubuntu-latest needs: - common - determine-jobs if: needs.determine-jobs.outputs.integration-tests == 'true' + strategy: + fail-fast: false + matrix: + bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }} steps: - name: Check out code from GitHub uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -299,19 +301,14 @@ jobs: run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run integration tests env: - INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }} - INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }} + # JSON array of test paths; parsed into a bash array below to avoid + # shell word-splitting / glob hazards. + BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }} run: | . venv/bin/activate - if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then - echo "Running all integration tests" - pytest -vv --no-cov --tb=native -n auto tests/integration/ - else - # Parse JSON array into bash array to avoid shell expansion issues - mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]') - echo "Running ${#test_files[@]} specific integration tests" - pytest -vv --no-cov --tb=native -n auto "${test_files[@]}" - fi + mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]') + echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" + pytest -vv --no-cov --tb=native -n auto "${test_files[@]}" cpp-unit-tests: name: Run C++ unit tests diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 6fd7ab297c8..c0cf8ecbdc8 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -6,8 +6,7 @@ what files have changed. It outputs JSON with the following structure: { "integration_tests": true/false, - "integration_tests_run_all": true/false, - "integration_test_files": ["tests/integration/test_foo.py", ...], + "integration_test_buckets": [{"name": "1/3", "tests": ["tests/integration/test_foo.py", ...]}, ...], "clang_tidy": true/false, "clang_format": true/false, "python_linters": true/false, @@ -81,6 +80,62 @@ CLANG_TIDY_SPLIT_THRESHOLD = 65 # Isolated components count as 10x, groupable components count as 1x COMPONENT_TEST_BATCH_SIZE = 40 +# Integration test bucketing: when more than the threshold tests are scheduled, +# fan out across this many parallel jobs. Below the threshold, a single job runs. +INTEGRATION_TESTS_SPLIT_THRESHOLD = 10 +INTEGRATION_TESTS_SPLIT_BUCKETS = 3 + + +def _split_list(items: list[str], n: int) -> list[list[str]]: + """Split a list into n roughly-equal contiguous parts (matches script/clang-tidy).""" + k, m = divmod(len(items), n) + return [items[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)] + + +def _all_integration_test_files() -> list[str]: + """Return all integration test file paths, sorted, relative to repo root.""" + return sorted( + str(p.relative_to(root_path)) + for p in (Path(root_path) / "tests" / "integration").glob("test_*.py") + ) + + +def _compute_integration_test_buckets( + integration_run_all: bool, + integration_test_files: list[str], +) -> tuple[bool, list[dict[str, Any]]]: + """Compute (run_integration, buckets) from the determine_integration_tests result. + + Pure function for unit testing — no I/O beyond `_all_integration_test_files` + when `integration_run_all` is set. + + `buckets` is a list of `{name, tests}` dicts where `tests` is a JSON-friendly + list of file paths so the workflow can build a bash array via jq, avoiding + shell word-splitting / glob hazards. + """ + if integration_run_all: + files = _all_integration_test_files() + else: + files = sorted(integration_test_files) + + # Empty list (e.g. run_all expansion with no files on disk) would otherwise + # cause the workflow to invoke pytest with no path argument and collect + # tests outside tests/integration/. Suppress the run instead. + if not files: + return False, [] + + if len(files) > INTEGRATION_TESTS_SPLIT_THRESHOLD: + parts = [ + part for part in _split_list(files, INTEGRATION_TESTS_SPLIT_BUCKETS) if part + ] + buckets = [ + {"name": f"{i + 1}/{len(parts)}", "tests": part} + for i, part in enumerate(parts) + ] + else: + buckets = [{"name": "1/1", "tests": files}] + return True, buckets + class Platform(StrEnum): """Platform identifiers for memory impact analysis.""" @@ -812,7 +867,9 @@ def main() -> None: integration_run_all, integration_test_files = determine_integration_tests( args.branch ) - run_integration = integration_run_all or bool(integration_test_files) + run_integration, integration_test_buckets = _compute_integration_test_buckets( + integration_run_all, integration_test_files + ) run_clang_tidy = should_run_clang_tidy(args.branch) run_clang_format = should_run_clang_format(args.branch) run_python_linters = should_run_python_linters(args.branch) @@ -944,8 +1001,7 @@ def main() -> None: output: dict[str, Any] = { "integration_tests": run_integration, - "integration_tests_run_all": integration_run_all, - "integration_test_files": integration_test_files, + "integration_test_buckets": integration_test_buckets, "clang_tidy": run_clang_tidy, "clang_tidy_mode": clang_tidy_mode, "clang_format": run_clang_format, diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 44c110b6892..e85f1757b03 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -122,10 +122,19 @@ def test_main_all_tests_should_run( "esphome/helpers.py", ] + # Stable, deterministic stand-in for the tests/integration/ glob so the + # bucket assertions don't drift with the real test count. + fake_test_files = [f"tests/integration/test_{i:03d}.py" for i in range(15)] + # Run main function with mocked argv with ( patch("sys.argv", ["determine-jobs.py"]), patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "_all_integration_test_files", + return_value=fake_test_files, + ), patch.object( determine_jobs, "get_changed_components", @@ -161,8 +170,24 @@ def test_main_all_tests_should_run( output = json.loads(captured.out) assert output["integration_tests"] is True - assert output["integration_tests_run_all"] is True - assert output["integration_test_files"] == [] + # run_all=True expands to the full glob and pre-buckets into 3 parts. + # Each bucket's `tests` is a JSON list of file paths. + assert isinstance(output["integration_test_buckets"], list) + assert len(output["integration_test_buckets"]) == 3 + assert [b["name"] for b in output["integration_test_buckets"]] == [ + "1/3", + "2/3", + "3/3", + ] + for bucket in output["integration_test_buckets"]: + assert isinstance(bucket["tests"], list) + for path in bucket["tests"]: + assert isinstance(path, str) + bucket_files = [f for b in output["integration_test_buckets"] for f in b["tests"]] + assert bucket_files == fake_test_files + # Bucket sizes are balanced (max-min difference at most 1). + sizes = [len(b["tests"]) for b in output["integration_test_buckets"]] + assert max(sizes) - min(sizes) <= 1 assert output["clang_tidy"] is True assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is True @@ -247,8 +272,7 @@ def test_main_no_tests_should_run( output = json.loads(captured.out) assert output["integration_tests"] is False - assert output["integration_tests_run_all"] is False - assert output["integration_test_files"] == [] + assert output["integration_test_buckets"] == [] assert output["clang_tidy"] is False assert output["clang_tidy_mode"] == "disabled" assert output["clang_format"] is False @@ -332,8 +356,7 @@ def test_main_with_branch_argument( output = json.loads(captured.out) assert output["integration_tests"] is False - assert output["integration_tests_run_all"] is False - assert output["integration_test_files"] == [] + assert output["integration_test_buckets"] == [] assert output["clang_tidy"] is True assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is False @@ -357,6 +380,59 @@ def test_main_with_branch_argument( assert output["cpp_unit_tests_components"] == ["mqtt"] +def test_compute_integration_test_buckets_empty() -> None: + """No integration tests scheduled => (False, []).""" + run, buckets = determine_jobs._compute_integration_test_buckets(False, []) + assert run is False + assert buckets == [] + + +def test_compute_integration_test_buckets_below_threshold() -> None: + """A small explicit list (<= threshold) => single 1/1 bucket with that list.""" + files = [f"tests/integration/test_{name}.py" for name in ("c", "a", "b")] + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + assert buckets == [{"name": "1/1", "tests": sorted(files)}] + + +def test_compute_integration_test_buckets_at_threshold_stays_single() -> None: + """Exactly INTEGRATION_TESTS_SPLIT_THRESHOLD files => still one bucket + (the split kicks in only when count is strictly greater than threshold).""" + files = [ + f"tests/integration/test_{i:02d}.py" + for i in range(determine_jobs.INTEGRATION_TESTS_SPLIT_THRESHOLD) + ] + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + assert len(buckets) == 1 + assert buckets[0]["name"] == "1/1" + assert buckets[0]["tests"] == sorted(files) + + +def test_compute_integration_test_buckets_just_over_threshold_splits() -> None: + """One file over the threshold triggers the 3-bucket fan-out, balanced.""" + n = determine_jobs.INTEGRATION_TESTS_SPLIT_THRESHOLD + 1 + files = [f"tests/integration/test_{i:02d}.py" for i in range(n)] + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + assert [b["name"] for b in buckets] == ["1/3", "2/3", "3/3"] + union = [path for b in buckets for path in b["tests"]] + assert union == sorted(files) + sizes = [len(b["tests"]) for b in buckets] + assert max(sizes) - min(sizes) <= 1 + + +def test_compute_integration_test_buckets_run_all_with_empty_glob_disables_run() -> ( + None +): + """run_all=True but glob returns no files => run suppressed (otherwise + pytest would collect tests outside tests/integration/).""" + with patch.object(determine_jobs, "_all_integration_test_files", return_value=[]): + run, buckets = determine_jobs._compute_integration_test_buckets(True, []) + assert run is False + assert buckets == [] + + def test_determine_integration_tests( monkeypatch: pytest.MonkeyPatch, ) -> None: From e085cb50d98a7a784050d5e2948754247d07fb9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:11:30 -0500 Subject: [PATCH 1787/2056] [sensor] Drop Component from filter classes, use self-keyed scheduler (#16132) --- esphome/components/sensor/__init__.py | 19 ++++++------------- esphome/components/sensor/filter.cpp | 21 +++++++-------------- esphome/components/sensor/filter.h | 15 +++++---------- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 48b7d25d4df..c18aa32f377 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -266,7 +266,7 @@ StreamingMovingAverageFilter = sensor_ns.class_("StreamingMovingAverageFilter", ExponentialMovingAverageFilter = sensor_ns.class_( "ExponentialMovingAverageFilter", Filter ) -ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) +ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) @@ -283,8 +283,8 @@ ThrottleWithPriorityNanFilter = sensor_ns.class_( TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) -DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) -HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) +DebounceFilter = sensor_ns.class_("DebounceFilter", Filter) +HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) OrFilter = sensor_ns.class_("OrFilter", Filter) CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) @@ -567,9 +567,7 @@ async def exponential_moving_average_filter_to_code(config, filter_id): "throttle_average", ThrottleAverageFilter, cv.positive_time_period_milliseconds ) async def throttle_average_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) - await cg.register_component(var, {}) - return var + return cg.new_Pvariable(filter_id, config) @FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) @@ -698,13 +696,10 @@ HEARTBEAT_SCHEMA = cv.Schema( async def heartbeat_filter_to_code(config, filter_id): if isinstance(config, dict): var = cg.new_Pvariable(filter_id, config[CONF_PERIOD]) - await cg.register_component(var, {}) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) return var - var = cg.new_Pvariable(filter_id, config) - await cg.register_component(var, {}) - return var + return cg.new_Pvariable(filter_id, config) TIMEOUT_SCHEMA = cv.maybe_simple_value( @@ -738,9 +733,7 @@ async def timeout_filter_to_code(config, filter_id): "debounce", DebounceFilter, cv.positive_time_period_milliseconds ) async def debounce_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) - await cg.register_component(var, {}) - return var + return cg.new_Pvariable(filter_id, config) CONF_DATAPOINTS = "datapoints" diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 4896757d3f3..5f7f19769a9 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -13,11 +13,6 @@ namespace esphome::sensor { static const char *const TAG = "sensor.filter"; -// Filter scheduler IDs. -// Each filter is its own Component instance, so the scheduler scopes -// IDs by component pointer — no risk of collisions between instances. -constexpr uint32_t FILTER_ID = 0; - // Filter void Filter::input(float value) { ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); @@ -185,8 +180,9 @@ optional<float> ThrottleAverageFilter::new_value(float value) { } return {}; } -void ThrottleAverageFilter::setup() { - this->set_interval(FILTER_ID, this->time_period_, [this]() { +void ThrottleAverageFilter::initialize(Sensor *parent, Filter *next) { + Filter::initialize(parent, next); + App.scheduler.set_interval(this, this->time_period_, [this]() { ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); if (this->n_ == 0) { if (this->have_nan_) @@ -199,7 +195,6 @@ void ThrottleAverageFilter::setup() { this->have_nan_ = false; }); } -float ThrottleAverageFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // LambdaFilter LambdaFilter::LambdaFilter(lambda_filter_t lambda_filter) : lambda_filter_(std::move(lambda_filter)) {} @@ -362,13 +357,12 @@ optional<float> TimeoutFilterConfigured::new_value(float value) { // DebounceFilter optional<float> DebounceFilter::new_value(float value) { - this->set_timeout(FILTER_ID, this->time_period_, [this, value]() { this->output(value); }); + App.scheduler.set_timeout(this, this->time_period_, [this, value]() { this->output(value); }); return {}; } DebounceFilter::DebounceFilter(uint32_t time_period) : time_period_(time_period) {} -float DebounceFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // HeartbeatFilter HeartbeatFilter::HeartbeatFilter(uint32_t time_period) : time_period_(time_period), last_input_(NAN) {} @@ -384,8 +378,9 @@ optional<float> HeartbeatFilter::new_value(float value) { return {}; } -void HeartbeatFilter::setup() { - this->set_interval(FILTER_ID, this->time_period_, [this]() { +void HeartbeatFilter::initialize(Sensor *parent, Filter *next) { + Filter::initialize(parent, next); + App.scheduler.set_interval(this, this->time_period_, [this]() { ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), this->last_input_); if (!this->has_value_) @@ -395,8 +390,6 @@ void HeartbeatFilter::setup() { }); } -float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } - optional<float> calibrate_linear_compute(const std::array<float, 3> *functions, size_t count, float value) { for (size_t i = 0; i < count; i++) { if (!std::isfinite(functions[i][2]) || value < functions[i][2]) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 917a1ce7d5a..d61df11d9b9 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -254,16 +254,14 @@ class ExponentialMovingAverageFilter : public Filter { * * It takes the average of all the values received in a period of time. */ -class ThrottleAverageFilter : public Filter, public Component { +class ThrottleAverageFilter : public Filter { public: explicit ThrottleAverageFilter(uint32_t time_period); - void setup() override; + void initialize(Sensor *parent, Filter *next) override; optional<float> new_value(float value) override; - float get_setup_priority() const override; - protected: float sum_{0.0f}; unsigned int n_{0}; @@ -454,25 +452,22 @@ class TimeoutFilterConfigured : public TimeoutFilterBase { // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead }; -class DebounceFilter : public Filter, public Component { +class DebounceFilter : public Filter { public: explicit DebounceFilter(uint32_t time_period); optional<float> new_value(float value) override; - float get_setup_priority() const override; - protected: uint32_t time_period_; }; -class HeartbeatFilter : public Filter, public Component { +class HeartbeatFilter : public Filter { public: explicit HeartbeatFilter(uint32_t time_period); - void setup() override; + void initialize(Sensor *parent, Filter *next) override; optional<float> new_value(float value) override; - float get_setup_priority() const override; void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } From 2f3e16b4823870cbf701c9273529d185b37a34eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:12:06 -0500 Subject: [PATCH 1788/2056] [bk72xx] Apply CFG_SUPPORT_BLE=0 SDK option to BK7238 (#16181) --- .../components/beken_spi_led_strip/light.py | 1 + esphome/components/libretiny/__init__.py | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/light.py b/esphome/components/beken_spi_led_strip/light.py index 31572cd8001..9093b08b62d 100644 --- a/esphome/components/beken_spi_led_strip/light.py +++ b/esphome/components/beken_spi_led_strip/light.py @@ -62,6 +62,7 @@ CONF_IS_WRGB = "is_wrgb" SUPPORTED_PINS = { libretiny.const.FAMILY_BK7231N: [16], libretiny.const.FAMILY_BK7231T: [16], + libretiny.const.FAMILY_BK7238: [16], libretiny.const.FAMILY_BK7251: [16], } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 40b8c8dc6c1..74ac51d2001 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -37,6 +37,7 @@ from .const import ( CONF_UART_PORT, FAMILIES, FAMILY_BK7231N, + FAMILY_BK7238, FAMILY_COMPONENT, FAMILY_FRIENDLY, FAMILY_RTL8710B, @@ -56,19 +57,22 @@ CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True -# BK7231N SDK options to disable unused features. +# BLE 5.x BK 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. +# This only works on BLE 5.x BK chips (BK7231N, BK7238). 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. +# +# On BK7238 the SDK also hangs at WiFi STA enable when BLE init runs, so +# disabling it is required for reliable boot, not just an optimization. # # 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 = [ +_BLE5_BK_SYS_CONFIG_OPTIONS = [ "CFG_SUPPORT_BLE=0", ] @@ -549,9 +553,9 @@ async def component_to_code(config): cg.add_platformio_option("custom_fw_version", __version__) # Apply chip-specific SDK options to save RAM/Flash - if config[CONF_FAMILY] == FAMILY_BK7231N: + if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238): cg.add_platformio_option( - "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS + "custom_options.sys_config#h", _BLE5_BK_SYS_CONFIG_OPTIONS ) # Tune lwIP for ESPHome's actual needs. From 3b3e003aa3312635ce22454703d386d49563fdca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:13:10 -0500 Subject: [PATCH 1789/2056] [sensor] Pack ThrottleAverageFilter have_nan_ into n_ bitfield (-4 B/instance) (#16169) --- esphome/components/sensor/__init__.py | 7 ++++++- esphome/components/sensor/filter.h | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index c18aa32f377..ed02cc25438 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -564,7 +564,12 @@ async def exponential_moving_average_filter_to_code(config, filter_id): @FILTER_REGISTRY.register( - "throttle_average", ThrottleAverageFilter, cv.positive_time_period_milliseconds + "throttle_average", + ThrottleAverageFilter, + cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(hours=24)), + ), ) async def throttle_average_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index d61df11d9b9..57a2386a7fa 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -264,9 +264,12 @@ class ThrottleAverageFilter : public Filter { protected: float sum_{0.0f}; - unsigned int n_{0}; uint32_t time_period_; - bool have_nan_{false}; + // Sample count packed with NaN-seen flag in a single 32-bit word. + // n_ is bounded by YAML cap on time_period_ (24 h) × max plausible source + // rate (1 kHz) = 86.4M ≪ 2^31, so 31 bits has 25x headroom. + uint32_t n_ : 31 {0}; + uint32_t have_nan_ : 1 {0}; }; using lambda_filter_t = std::function<optional<float>(float)>; From 45e78e4114a9dba7628fca434163c8e4795670b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:13:54 -0500 Subject: [PATCH 1790/2056] [core] Inline loop gate expression to avoid stale local reuse (#16167) --- esphome/core/application.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 4a18714d0de..5baf570e622 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -637,10 +637,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // flag preserves it. wake_request_take() exchange-clears the flag; wakes // that arrive during Phase B re-set it and run Phase B again on the next // iteration. - const bool high_frequency = HighFrequencyLoopRequester::is_high_frequency(); - const uint32_t elapsed = now - this->last_loop_; - const bool woke = esphome::wake_request_take(); - const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_); + // + // wake_request_take() must always be called first since it does an + // atomic exchange to clear the flag, and we want to run the component phase + // if either the flag was set or the scheduler requested a high-frequency loop. + const bool do_component_phase = esphome::wake_request_take() || HighFrequencyLoopRequester::is_high_frequency() || + (now - this->last_loop_ >= this->loop_interval_); if (do_component_phase) { ComponentPhaseGuard phase_guard{*this}; From 148d478decdbc07c69442162799d4a28a5e41c8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:14:20 -0500 Subject: [PATCH 1791/2056] [api] Add encode/decode benchmarks for Z-Wave, IR/RF, and serial proxy messages (#16157) --- tests/benchmarks/components/api/__init__.py | 14 +- .../components/api/bench_proto_proxy.cpp | 280 ++++++++++++++++++ .../esphome/components/infrared/infrared.h | 45 +++ .../radio_frequency/radio_frequency.h | 51 ++++ .../components/serial_proxy/serial_proxy.h | 46 +++ .../components/zwave_proxy/zwave_proxy.h | 29 ++ 6 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 tests/benchmarks/components/api/bench_proto_proxy.cpp create mode 100644 tests/benchmarks/stubs/esphome/components/infrared/infrared.h create mode 100644 tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h create mode 100644 tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h create mode 100644 tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py index eb86492964d..0d02e0b0548 100644 --- a/tests/benchmarks/components/api/__init__.py +++ b/tests/benchmarks/components/api/__init__.py @@ -11,11 +11,19 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: async def to_code(config): await original_to_code(config) - # Enable BLE proto message types for benchmarks. The real - # bluetooth_proxy component is ESP32-only; a lightweight stub - # header in tests/benchmarks/stubs/ satisfies the include. + # Enable proxy proto message types for benchmarks. The real + # components have hardware dependencies (BLE/UART/RMT); lightweight + # stub headers in tests/benchmarks/stubs/ satisfy the includes. cg.add_define("USE_BLUETOOTH_PROXY") cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3) cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) + cg.add_define("USE_ZWAVE_PROXY") + cg.add_define("USE_INFRARED") + cg.add_define("USE_IR_RF") + cg.add_define("USE_RADIO_FREQUENCY") + cg.add_define("USE_SERIAL_PROXY") + cg.add_define("SERIAL_PROXY_COUNT", 0) + cg.add_define("ESPHOME_ENTITY_INFRARED_COUNT", 0) + cg.add_define("ESPHOME_ENTITY_RADIO_FREQUENCY_COUNT", 0) manifest.to_code = to_code diff --git a/tests/benchmarks/components/api/bench_proto_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp new file mode 100644 index 00000000000..fa3191a969c --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -0,0 +1,280 @@ +// Encode/decode microbenchmarks for proxy message families that carry +// high-volume traffic (Z-Wave, IR/RF, serial). Mirrors the existing +// BluetoothLERawAdvertisementsResponse benchmarks in bench_proto_encode.cpp. + +#include <benchmark/benchmark.h> + +#include <cstring> + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Encodes `src` into `out`. Caller owns `out` and must keep it alive across +// the decode loop (decoded messages may store pointers back into its bytes). +template<typename T> static void encode_into(APIBuffer &out, const T &src) { + out.resize(src.calculate_size()); + ProtoWriteBuffer writer(&out, 0); + src.encode(writer); +} + +// --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) --- + +#ifdef USE_ZWAVE_PROXY + +static const uint8_t kZWaveFrameData[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, + 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; + +static void Encode_ZWaveProxyFrame(benchmark::State &state) { + ZWaveProxyFrame msg; + msg.data = kZWaveFrameData; + msg.data_len = sizeof(kZWaveFrameData); + APIBuffer buffer; + buffer.resize(msg.calculate_size()); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ZWaveProxyFrame); + +static void Decode_ZWaveProxyFrame(benchmark::State &state) { + ZWaveProxyFrame source; + source.data = kZWaveFrameData; + source.data_len = sizeof(kZWaveFrameData); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyFrame msg; + msg.decode(data, size); + benchmark::DoNotOptimize(msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyFrame); + +static const uint8_t kZWaveRequestData[] = {0xDE, 0xAD, 0xBE, 0xEF}; + +static void Decode_ZWaveProxyRequest(benchmark::State &state) { + ZWaveProxyRequest source; + source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + source.data = kZWaveRequestData; + source.data_len = sizeof(kZWaveRequestData); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyRequest msg; + msg.decode(data, size); + benchmark::DoNotOptimize(msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyRequest); + +#endif // USE_ZWAVE_PROXY + +// --- SerialProxyDataReceived encode + SerialProxyWriteRequest decode --- +// +// SerialProxyWriteRequest is decode-only (SOURCE_CLIENT) but has the same +// wire layout as SerialProxyDataReceived, so we encode via the latter and +// decode as the former. + +#ifdef USE_SERIAL_PROXY + +static constexpr size_t kSerialPayloadSize = 64; +static const uint8_t kSerialPayload[kSerialPayloadSize] = { + 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, + 0xF0, 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF}; + +static void Encode_SerialProxyDataReceived(benchmark::State &state) { + SerialProxyDataReceived msg; + msg.instance = 0; + msg.set_data(kSerialPayload, kSerialPayloadSize); + APIBuffer buffer; + buffer.resize(msg.calculate_size()); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_SerialProxyDataReceived); + +static void Decode_SerialProxyWriteRequest(benchmark::State &state) { + SerialProxyDataReceived source; + source.instance = 0; + source.set_data(kSerialPayload, kSerialPayloadSize); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + SerialProxyWriteRequest msg; + msg.decode(data, size); + benchmark::DoNotOptimize(msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_SerialProxyWriteRequest); + +#endif // USE_SERIAL_PROXY + +// --- InfraredRFReceiveEvent encode (100 sint32 timings) + +// InfraredRFTransmitRawTimingsRequest decode (hand-built wire bytes) --- + +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) + +// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings). +static std::vector<int32_t> make_ir_timings_100() { + std::vector<int32_t> v; + v.reserve(100); + for (int i = 0; i < 100; i++) { + v.push_back((i % 2 == 0) ? 560 : -560); + } + return v; +} + +static const std::vector<int32_t> &get_ir_timings_100() { + static const std::vector<int32_t> timings = make_ir_timings_100(); + return timings; +} + +static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { + InfraredRFReceiveEvent msg; + msg.key = 0xDEADBEEF; + msg.timings = &get_ir_timings_100(); + APIBuffer buffer; + buffer.resize(msg.calculate_size()); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_InfraredRFReceiveEvent); + +static void CalculateSize_InfraredRFReceiveEvent(benchmark::State &state) { + InfraredRFReceiveEvent msg; + msg.key = 0xDEADBEEF; + msg.timings = &get_ir_timings_100(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_InfraredRFReceiveEvent); + +// Hand-built wire bytes for InfraredRFTransmitRawTimingsRequest (decode-only, +// no sister message with identical wire layout). +// field 2 (key, fixed32): tag=0x15, 4 LE bytes +// field 3 (carrier_frequency): tag=0x18, varint +// field 4 (repeat_count): tag=0x20, varint +// field 5 (timings, packed sint32): tag=0x2A, length varint, packed payload +// field 6 (modulation): tag=0x30, varint +static APIBuffer build_infrared_rf_transmit_wire() { + uint8_t bytes[256]; + size_t len = 0; + + auto put_byte = [&](uint8_t b) { bytes[len++] = b; }; + auto put_varint = [&](uint32_t v) { + while (v >= 0x80) { + bytes[len++] = static_cast<uint8_t>((v & 0x7F) | 0x80); + v >>= 7; + } + bytes[len++] = static_cast<uint8_t>(v); + }; + auto encode_zigzag = [](int32_t v) -> uint32_t { + return (static_cast<uint32_t>(v) << 1) ^ static_cast<uint32_t>(v >> 31); + }; + + put_byte(0x15); + put_byte(0xEF); + put_byte(0xBE); + put_byte(0xAD); + put_byte(0xDE); + put_byte(0x18); + put_varint(38000); + put_byte(0x20); + put_varint(2); + + uint8_t packed[200]; + size_t packed_len = 0; + for (int i = 0; i < 100; i++) { + int32_t value = (i % 2 == 0) ? 560 : -560; + uint32_t zz = encode_zigzag(value); + while (zz >= 0x80) { + packed[packed_len++] = static_cast<uint8_t>((zz & 0x7F) | 0x80); + zz >>= 7; + } + packed[packed_len++] = static_cast<uint8_t>(zz); + } + put_byte(0x2A); + put_varint(static_cast<uint32_t>(packed_len)); + std::memcpy(bytes + len, packed, packed_len); + len += packed_len; + // field 6: modulation = 1 (non-zero so it's actually emitted and exercises + // decode_varint for this field, matching the documented layout above). + put_byte(0x30); + put_varint(1); + + APIBuffer buf; + buf.resize(len); + std::memcpy(buf.data(), bytes, len); + return buf; +} + +static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { + auto encoded = build_infrared_rf_transmit_wire(); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + InfraredRFTransmitRawTimingsRequest msg; + msg.decode(data, size); + benchmark::DoNotOptimize(msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest); + +#endif // USE_IR_RF || USE_RADIO_FREQUENCY + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/stubs/esphome/components/infrared/infrared.h b/tests/benchmarks/stubs/esphome/components/infrared/infrared.h new file mode 100644 index 00000000000..874e7a270bd --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/infrared/infrared.h @@ -0,0 +1,45 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_INFRARED is defined, +// without pulling in the real remote_base/RMT dependencies. +#pragma once + +#include <cstdint> +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome::infrared { + +class Infrared; + +class InfraredCall { + public: + explicit InfraredCall(Infrared *parent) : parent_(parent) {} + InfraredCall &set_carrier_frequency(uint32_t /*frequency*/) { return *this; } + InfraredCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) { + return *this; + } + InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; } + void perform() {} + + protected: + Infrared *parent_; +}; + +class InfraredTraits { + public: + uint32_t get_receiver_frequency_hz() const { return 0; } +}; + +class Infrared : public Component, public EntityBase { + public: + Infrared() = default; + InfraredTraits &get_traits() { return this->traits_; } + const InfraredTraits &get_traits() const { return this->traits_; } + InfraredCall make_call() { return InfraredCall(this); } + uint32_t get_capability_flags() const { return 0; } + + protected: + InfraredTraits traits_; +}; + +} // namespace esphome::infrared diff --git a/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h b/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h new file mode 100644 index 00000000000..72fc08034bd --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h @@ -0,0 +1,51 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_RADIO_FREQUENCY is defined. +#pragma once + +#include <cstdint> +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome::radio_frequency { + +enum RadioFrequencyModulation : uint32_t { + RADIO_FREQUENCY_MODULATION_OOK = 0, +}; + +class RadioFrequency; + +class RadioFrequencyCall { + public: + explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {} + RadioFrequencyCall &set_frequency(uint32_t /*frequency*/) { return *this; } + RadioFrequencyCall &set_modulation(RadioFrequencyModulation /*mod*/) { return *this; } + RadioFrequencyCall &set_repeat_count(uint32_t /*count*/) { return *this; } + RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) { + return *this; + } + void perform() {} + + protected: + RadioFrequency *parent_; +}; + +class RadioFrequencyTraits { + public: + uint32_t get_frequency_min_hz() const { return 0; } + uint32_t get_frequency_max_hz() const { return 0; } + uint32_t get_supported_modulations() const { return 0; } +}; + +class RadioFrequency : public Component, public EntityBase { + public: + RadioFrequency() = default; + RadioFrequencyTraits &get_traits() { return this->traits_; } + const RadioFrequencyTraits &get_traits() const { return this->traits_; } + RadioFrequencyCall make_call() { return RadioFrequencyCall(this); } + uint32_t get_capability_flags() const { return 0; } + + protected: + RadioFrequencyTraits traits_; +}; + +} // namespace esphome::radio_frequency diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h new file mode 100644 index 00000000000..bab27549e7c --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -0,0 +1,46 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_SERIAL_PROXY is defined, +// without pulling in the real UART implementation. +#pragma once + +#include <cstdint> +#include <cstddef> +#include "esphome/components/api/api_pb2.h" + +namespace esphome { + +namespace api { +class APIConnection; +} // namespace api + +namespace uart { +enum class UARTFlushResult : uint8_t { + UART_FLUSH_RESULT_SUCCESS, + UART_FLUSH_RESULT_ASSUMED_SUCCESS, + UART_FLUSH_RESULT_TIMEOUT, + UART_FLUSH_RESULT_FAILED, +}; +} // namespace uart + +namespace serial_proxy { + +class SerialProxy { + public: + void set_instance_index(uint32_t index) { this->instance_index_ = index; } + uint32_t get_instance_index() const { return this->instance_index_; } + const char *get_name() const { return ""; } + api::enums::SerialProxyPortType get_port_type() const { return {}; } + api::APIConnection *get_api_connection() { return nullptr; } + void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {} + void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint32_t stop_bits, uint32_t data_size) {} + void write_from_client(const uint8_t *data, size_t len) {} + void set_modem_pins(uint32_t line_states) {} + uint32_t get_modem_pins() const { return 0; } + uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } + + protected: + uint32_t instance_index_{0}; +}; + +} // namespace serial_proxy +} // namespace esphome diff --git a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h new file mode 100644 index 00000000000..ba97e812365 --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h @@ -0,0 +1,29 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp needs when USE_ZWAVE_PROXY is defined, +// without pulling in the real UART-based ZWaveProxy implementation. +#pragma once + +#include "esphome/components/api/api_pb2.h" + +namespace esphome { +namespace api { +class APIConnection; +} // namespace api + +namespace zwave_proxy { + +class ZWaveProxy { + public: + api::APIConnection *get_api_connection() { return nullptr; } + void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {} + void send_frame(const uint8_t *data, size_t length) {} + void api_connection_authenticated(api::APIConnection *conn) {} + uint32_t get_feature_flags() const { return 0; } + uint32_t get_home_id() { return 0; } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern ZWaveProxy *global_zwave_proxy; + +} // namespace zwave_proxy +} // namespace esphome From b708d1a8260b55acd9c0c7c66af3fbadc6b4c564 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:14:34 -0500 Subject: [PATCH 1792/2056] [core] Drop unused DELAY_ACTION from InternalSchedulerID enum (#16151) --- esphome/core/component.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 185d51ab37d..5baf795ca62 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -65,7 +65,6 @@ inline constexpr uint32_t SCHEDULER_DONT_RUN = 4294967295UL; /// with component-level NUMERIC_ID values, even if the uint32_t values overlap. enum class InternalSchedulerID : uint32_t { POLLING_UPDATE = 0, // PollingComponent interval - DELAY_ACTION = 1, // DelayAction timeout }; // Forward declaration From ba7c06785a03f1f7c570b572e5aa677d0ed9d073 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:14:55 -0500 Subject: [PATCH 1793/2056] [mdns] Broadcast config_hash TXT record on _esphomelib._tcp (#16145) --- esphome/components/mdns/mdns_component.cpp | 40 ++++++++++++++++++++-- esphome/components/mdns/mdns_component.h | 33 +++++------------- esphome/components/mdns/mdns_host.cpp | 7 +++- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index e05373ac5d3..9bf27e71e4c 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -39,7 +39,39 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); // Wrap build-time defines into flash storage MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); -void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf) { +void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) { +#ifdef USE_MDNS_STORE_SERVICES + auto &services = this->services_; +#else + StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_storage; + auto &services = services_storage; +#endif + +#ifdef USE_API +#ifdef USE_MDNS_STORE_SERVICES + get_mac_address_into_buffer(this->mac_address_); + char *mac_ptr = this->mac_address_; + format_hex_to(this->config_hash_str_, App.get_config_hash()); + char *cfg_ptr = this->config_hash_str_; +#else + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + char config_hash_str[CONFIG_HASH_STR_SIZE]; + get_mac_address_into_buffer(mac_address); + format_hex_to(config_hash_str, App.get_config_hash()); + char *mac_ptr = mac_address; + char *cfg_ptr = config_hash_str; +#endif +#else + char *mac_ptr = nullptr; + char *cfg_ptr = nullptr; +#endif + + this->compile_records_(services, mac_ptr, cfg_ptr); + platform_register(this, services); +} + +void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf, + char *config_hash_buf) { // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -47,6 +79,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); + MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash"); MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); @@ -63,7 +96,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records - size_t txt_count = 3; // version, mac, board (always present) + size_t txt_count = 4; // version, config_hash, mac, board (always present) if (!friendly_name_empty) { txt_count++; // friendly_name } @@ -91,6 +124,9 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN } txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}); + // Config hash: passed from caller (either member buffer or stack buffer depending on USE_MDNS_STORE_SERVICES) + txt_records.push_back({MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}); + // MAC address: passed from caller (either member buffer or stack buffer depending on USE_MDNS_STORE_SERVICES) txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)}); diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 798af0e0bf5..e981a659419 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -70,6 +70,9 @@ class MDNSComponent final : public Component void setup() override; void dump_config() override; + /// Size of buffer required for config hash hex string (8 hex chars + null terminator) + static constexpr size_t CONFIG_HASH_STR_SIZE = format_hex_size(sizeof(uint32_t)); + #ifdef USE_MDNS_EVENT_DRIVEN_POLLING // LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes + // 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires() @@ -124,30 +127,7 @@ class MDNSComponent final : public Component /// Helper to set up services and MAC buffers, then call platform-specific registration using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &); - void setup_buffers_and_register_(PlatformRegisterFn platform_register) { -#ifdef USE_MDNS_STORE_SERVICES - auto &services = this->services_; -#else - StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_storage; - auto &services = services_storage; -#endif - -#ifdef USE_API -#ifdef USE_MDNS_STORE_SERVICES - get_mac_address_into_buffer(this->mac_address_); - char *mac_ptr = this->mac_address_; -#else - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(mac_address); - char *mac_ptr = mac_address; -#endif -#else - char *mac_ptr = nullptr; -#endif - - this->compile_records_(services, mac_ptr); - platform_register(this, services); - } + void setup_buffers_and_register_(PlatformRegisterFn platform_register); #ifdef USE_MDNS_DYNAMIC_TXT /// Storage for runtime-generated TXT values from user lambdas @@ -159,6 +139,8 @@ class MDNSComponent final : public Component #if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES) /// Fixed buffer for MAC address (only needed when services are stored) char mac_address_[MAC_ADDRESS_BUFFER_SIZE]; + /// Fixed buffer for config hash hex string (only needed when services are stored) + char config_hash_str_[CONFIG_HASH_STR_SIZE]; #endif #ifdef USE_MDNS_STORE_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; @@ -167,7 +149,8 @@ class MDNSComponent final : public Component // RP2040 defers MDNS.begin() until the first IP-up event; this tracks that. bool initialized_{false}; #endif - void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf); + void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf, + char *config_hash_buf); }; } // namespace esphome::mdns diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 4d902319b88..1e66a10df09 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -3,6 +3,8 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -13,10 +15,13 @@ void MDNSComponent::setup() { #ifdef USE_API get_mac_address_into_buffer(this->mac_address_); char *mac_ptr = this->mac_address_; + format_hex_to(this->config_hash_str_, App.get_config_hash()); + char *cfg_ptr = this->config_hash_str_; #else char *mac_ptr = nullptr; + char *cfg_ptr = nullptr; #endif - this->compile_records_(this->services_, mac_ptr); + this->compile_records_(this->services_, mac_ptr, cfg_ptr); #endif // Host platform doesn't have actual mDNS implementation } From 550444dc34da939cd3c1da77042d7d07c88fe766 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:15:18 -0500 Subject: [PATCH 1794/2056] [binary_sensor] Drop Component from filter classes, use self-keyed scheduler (#16131) --- esphome/components/binary_sensor/__init__.py | 15 +++------ esphome/components/binary_sensor/filter.cpp | 34 +++++++------------- esphome/components/binary_sensor/filter.h | 18 +++-------- 3 files changed, 22 insertions(+), 45 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1456e5bc663..db82290750b 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -143,15 +143,15 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi # Filters Filter = binary_sensor_ns.class_("Filter") -TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component) -DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) -DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) -DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) +TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter) +DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter) +DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter) +DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) -SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) +SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter) _LOGGER = getLogger(__name__) @@ -175,7 +175,6 @@ async def invert_filter_to_code(config, filter_id): ) async def timeout_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id) - await cg.register_component(var, {}) template_ = await cg.templatable(config, [], cg.uint32) cg.add(var.set_timeout_value(template_)) return var @@ -203,7 +202,6 @@ async def timeout_filter_to_code(config, filter_id): ) async def delayed_on_off_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id) - await cg.register_component(var, {}) if isinstance(config, dict): template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32) cg.add(var.set_on_delay(template_)) @@ -221,7 +219,6 @@ async def delayed_on_off_filter_to_code(config, filter_id): ) async def delayed_on_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id) - await cg.register_component(var, {}) template_ = await cg.templatable(config, [], cg.uint32) cg.add(var.set_delay(template_)) return var @@ -234,7 +231,6 @@ async def delayed_on_filter_to_code(config, filter_id): ) async def delayed_off_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id) - await cg.register_component(var, {}) template_ = await cg.templatable(config, [], cg.uint32) cg.add(var.set_delay(template_)) return var @@ -306,7 +302,6 @@ async def lambda_filter_to_code(config, filter_id): ) async def settle_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id) - await cg.register_component(var, {}) template_ = await cg.templatable(config, [], cg.uint32) cg.add(var.set_delay(template_)) return var diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 914060ce138..0a463ee9a9c 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -4,16 +4,14 @@ #include "filter.h" #include "binary_sensor.h" +#include "esphome/core/application.h" namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; -// Timeout IDs for filter classes. -// Each filter is its own Component instance, so the scheduler scopes -// IDs by component pointer — no risk of collisions between instances. -constexpr uint32_t FILTER_TIMEOUT_ID = 0; -// AutorepeatFilter needs two distinct IDs (both timeouts on the same component) +// AutorepeatFilter still inherits Component (it schedules two distinct timer +// purposes), so it keeps the (Component *, id) scheduler API. constexpr uint32_t AUTOREPEAT_TIMING_ID = 0; constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1; @@ -34,46 +32,40 @@ void Filter::input(bool value) { } void TimeoutFilter::input(bool value) { - this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + App.scheduler.set_timeout(this, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); // we do not de-dup here otherwise changes from invalid to valid state will not be output this->output(value); } optional<bool> DelayedOnOffFilter::new_value(bool value) { if (value) { - this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); }); + App.scheduler.set_timeout(this, this->on_delay_.value(), [this]() { this->output(true); }); } else { - this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); }); + App.scheduler.set_timeout(this, this->off_delay_.value(), [this]() { this->output(false); }); } return {}; } -float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } - optional<bool> DelayedOnFilter::new_value(bool value) { if (value) { - this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); }); + App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(true); }); return {}; } else { - this->cancel_timeout(FILTER_TIMEOUT_ID); + App.scheduler.cancel_timeout(this); return false; } } -float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } - optional<bool> DelayedOffFilter::new_value(bool value) { if (!value) { - this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); }); + App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(false); }); return {}; } else { - this->cancel_timeout(FILTER_TIMEOUT_ID); + App.scheduler.cancel_timeout(this); return true; } } -float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } - optional<bool> InvertFilter::new_value(bool value) { return !value; } // AutorepeatFilterBase @@ -118,20 +110,18 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); } optional<bool> SettleFilter::new_value(bool value) { if (!this->steady_) { - this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() { + App.scheduler.set_timeout(this, this->delay_.value(), [this, value]() { this->steady_ = true; this->output(value); }); return {}; } else { this->steady_ = false; - this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); + App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } } -float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } - } // namespace esphome::binary_sensor #endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2e45554f810..8ff57cab0c4 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -29,7 +29,7 @@ class Filter { Deduplicator<bool> dedup_; }; -class TimeoutFilter : public Filter, public Component { +class TimeoutFilter : public Filter { public: optional<bool> new_value(bool value) override { return value; } void input(bool value) override; @@ -39,12 +39,10 @@ class TimeoutFilter : public Filter, public Component { TemplatableFn<uint32_t> timeout_delay_{}; }; -class DelayedOnOffFilter final : public Filter, public Component { +class DelayedOnOffFilter final : public Filter { public: optional<bool> new_value(bool value) override; - float get_setup_priority() const override; - template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; } template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; } @@ -53,24 +51,20 @@ class DelayedOnOffFilter final : public Filter, public Component { TemplatableFn<uint32_t> off_delay_{}; }; -class DelayedOnFilter : public Filter, public Component { +class DelayedOnFilter : public Filter { public: optional<bool> new_value(bool value) override; - float get_setup_priority() const override; - template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: TemplatableFn<uint32_t> delay_{}; }; -class DelayedOffFilter : public Filter, public Component { +class DelayedOffFilter : public Filter { public: optional<bool> new_value(bool value) override; - float get_setup_priority() const override; - template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: @@ -146,12 +140,10 @@ class StatelessLambdaFilter : public Filter { optional<bool> (*f_)(bool); }; -class SettleFilter : public Filter, public Component { +class SettleFilter : public Filter { public: optional<bool> new_value(bool value) override; - float get_setup_priority() const override; - template<typename T> void set_delay(T delay) { this->delay_ = delay; } protected: From 24fdfcf1a18be08c720a674384cd15712168d7d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:15:41 -0500 Subject: [PATCH 1795/2056] [rp2040] Move HAL bodies into components/rp2040/hal.cpp + inline trivial dispatches (#16114) --- esphome/components/rp2040/core.cpp | 39 ++-------------------------- esphome/components/rp2040/hal.cpp | 41 ++++++++++++++++++++++++++++++ esphome/core/hal/hal_rp2040.h | 19 +++++++++++--- 3 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 esphome/components/rp2040/hal.cpp diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index d3dc1cf2bb5..11f23ccfef0 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -1,41 +1,6 @@ #ifdef USE_RP2040 -#include "core.h" -#include "esphome/core/defines.h" -#ifdef USE_RP2040_CRASH_HANDLER -#include "crash_handler.h" -#endif -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" - -#include "hardware/timer.h" -#include "hardware/watchdog.h" - -namespace esphome { - -// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. -void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } -void arch_restart() { - watchdog_reboot(0, 0, 10); - while (1) { - continue; - } -} - -void arch_init() { -#ifdef USE_RP2040_CRASH_HANDLER - rp2040::crash_handler_read_and_clear(); -#endif -#if USE_RP2040_WATCHDOG_TIMEOUT > 0 - watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); -#endif -} - -void HOT arch_feed_wdt() { watchdog_update(); } - -uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } -uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } - -} // namespace esphome +// HAL functions live in hal.cpp. core.cpp is intentionally empty for +// rp2040 — there is no extra component bootstrap to keep here. #endif // USE_RP2040 diff --git a/esphome/components/rp2040/hal.cpp b/esphome/components/rp2040/hal.cpp new file mode 100644 index 00000000000..7475205d60c --- /dev/null +++ b/esphome/components/rp2040/hal.cpp @@ -0,0 +1,41 @@ +#ifdef USE_RP2040 + +#include "core.h" +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#ifdef USE_RP2040_CRASH_HANDLER +#include "crash_handler.h" +#endif + +#include "hardware/watchdog.h" + +// Empty rp2040 namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// rp2040 component's API. +namespace esphome::rp2040 {} // namespace esphome::rp2040 + +namespace esphome { + +// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), +// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in core/hal/hal_rp2040.h. +void arch_restart() { + watchdog_reboot(0, 0, 10); + while (1) { + continue; + } +} + +void arch_init() { +#ifdef USE_RP2040_CRASH_HANDLER + rp2040::crash_handler_read_and_clear(); +#endif +#if USE_RP2040_WATCHDOG_TIMEOUT > 0 + watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false); +#endif +} + +uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/core/hal/hal_rp2040.h index 46f6e421cd4..27a9b23c0b6 100644 --- a/esphome/core/hal/hal_rp2040.h +++ b/esphome/core/hal/hal_rp2040.h @@ -20,8 +20,17 @@ extern "C" unsigned long millis(void); // Forward decl from <pico/time.h>. extern "C" uint64_t time_us_64(void); +// Forward decls from pico-sdk / FreeRTOS port for the inline arch_* +// wrappers below. +extern "C" void watchdog_update(void); +extern "C" unsigned long ulMainGetRunTimeCounterValue(void); + namespace esphome { +// Forward decl from helpers.h. +// NOLINTNEXTLINE(readability-redundant-declaration) +void delay_microseconds_safe(uint32_t us); + /// Returns true when executing inside an interrupt handler. __attribute__((always_inline)) inline bool in_isr_context() { uint32_t ipsr; @@ -35,9 +44,13 @@ __attribute__((always_inline)) inline uint32_t micros() { return static_cast<uin __attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); } -void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) -void arch_feed_wdt(); -uint32_t arch_get_cpu_cycle_count(); +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } +__attribute__((always_inline)) inline void arch_feed_wdt() { watchdog_update(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { + return static_cast<uint32_t>(ulMainGetRunTimeCounterValue()); +} + void arch_init(); uint32_t arch_get_cpu_freq_hz(); From 3d69169141984487a28d5b1e323a3740748a1408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:16:16 -0500 Subject: [PATCH 1796/2056] [climate] Fold ControlAction fields into a single stateless lambda (#16044) --- esphome/components/climate/__init__.py | 87 +++++++++++------- esphome/components/climate/automation.h | 35 ++----- .../fixtures/climate_control_action.yaml | 92 +++++++++++++++++++ .../test_climate_control_action.py | 84 +++++++++++++++++ 4 files changed, 238 insertions(+), 60 deletions(-) create mode 100644 tests/integration/fixtures/climate_control_action.yaml create mode 100644 tests/integration/test_climate_control_action.py diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 0fdb18a92c8..7c9002d6dc3 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -48,13 +48,13 @@ from esphome.const import ( CONF_VISUAL, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import LambdaExpression, MockObjClass IS_PLATFORM_COMPONENT = True @@ -487,38 +487,57 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( ) async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if (mode := config.get(CONF_MODE)) is not None: - template_ = await cg.templatable(mode, args, ClimateMode) - cg.add(var.set_mode(template_)) - if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None: - template_ = await cg.templatable(target_temp, args, cg.float_) - cg.add(var.set_target_temperature(template_)) - if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: - template_ = await cg.templatable(target_temp_low, args, cg.float_) - cg.add(var.set_target_temperature_low(template_)) - if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: - template_ = await cg.templatable(target_temp_high, args, cg.float_) - cg.add(var.set_target_temperature_high(template_)) - if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None: - template_ = await cg.templatable(target_humidity, args, cg.float_) - cg.add(var.set_target_humidity(template_)) - if (fan_mode := config.get(CONF_FAN_MODE)) is not None: - template_ = await cg.templatable(fan_mode, args, ClimateFanMode) - cg.add(var.set_fan_mode(template_)) - if (custom_fan_mode := config.get(CONF_CUSTOM_FAN_MODE)) is not None: - template_ = await cg.templatable(custom_fan_mode, args, cg.std_string) - cg.add(var.set_custom_fan_mode(template_)) - if (preset := config.get(CONF_PRESET)) is not None: - template_ = await cg.templatable(preset, args, ClimatePreset) - cg.add(var.set_preset(template_)) - if (custom_preset := config.get(CONF_CUSTOM_PRESET)) is not None: - template_ = await cg.templatable(custom_preset, args, cg.std_string) - cg.add(var.set_custom_preset(template_)) - if (swing_mode := config.get(CONF_SWING_MODE)) is not None: - template_ = await cg.templatable(swing_mode, args, ClimateSwingMode) - cg.add(var.set_swing_mode(template_)) - return var + + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. + # For custom_fan_mode/custom_preset the static-string path emits the + # (const char *, size_t) overload of set_fan_mode/set_preset to avoid + # constructing a std::string and calling runtime strlen. + FIELDS = ( + (CONF_MODE, "set_mode", ClimateMode), + (CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_), + (CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_), + (CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_), + (CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_), + (CONF_FAN_MODE, "set_fan_mode", ClimateFanMode), + (CONF_CUSTOM_FAN_MODE, "set_fan_mode", cg.std_string), + (CONF_PRESET, "set_preset", ClimatePreset), + (CONF_CUSTOM_PRESET, "set_preset", cg.std_string), + (CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode), + ) + + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] + + for conf_key, setter, type_ in FIELDS: + if (value := config.get(conf_key)) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + elif type_ is cg.std_string: + # Static custom strings: emit a flash literal and pass the + # UTF-8 byte length to skip the runtime strlen inside + # set_fan_mode/set_preset. + literal = cg.safe_exp(value) + body_lines.append( + f"call.{setter}({literal}, {len(value.encode('utf-8'))});" + ) + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + + # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (ClimateCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index fac56d9d9e9..71d23fd6b6d 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -5,42 +5,25 @@ namespace esphome::climate { +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `target_temperature: !lambda "return x;"`) keep working. template<typename... Ts> class ControlAction : public Action<Ts...> { public: - explicit ControlAction(Climate *climate) : climate_(climate) {} - - TEMPLATABLE_VALUE(ClimateMode, mode) - TEMPLATABLE_VALUE(float, target_temperature) - TEMPLATABLE_VALUE(float, target_temperature_low) - TEMPLATABLE_VALUE(float, target_temperature_high) - TEMPLATABLE_VALUE(float, target_humidity) - TEMPLATABLE_VALUE(bool, away) - TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) - TEMPLATABLE_VALUE(std::string, custom_fan_mode) - TEMPLATABLE_VALUE(ClimatePreset, preset) - TEMPLATABLE_VALUE(std::string, custom_preset) - TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) + using ApplyFn = void (*)(ClimateCall &, const Ts &...); + ControlAction(Climate *climate, ApplyFn apply) : climate_(climate), apply_(apply) {} void play(const Ts &...x) override { auto call = this->climate_->make_call(); - call.set_mode(this->mode_.optional_value(x...)); - call.set_target_temperature(this->target_temperature_.optional_value(x...)); - call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); - call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); - call.set_target_humidity(this->target_humidity_.optional_value(x...)); - if (away_.has_value()) { - call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); - } - call.set_fan_mode(this->fan_mode_.optional_value(x...)); - call.set_fan_mode(this->custom_fan_mode_.optional_value(x...)); - call.set_preset(this->preset_.optional_value(x...)); - call.set_preset(this->custom_preset_.optional_value(x...)); - call.set_swing_mode(this->swing_mode_.optional_value(x...)); + this->apply_(call, x...); call.perform(); } protected: Climate *climate_; + ApplyFn apply_; }; class ControlTrigger : public Trigger<ClimateCall &> { diff --git a/tests/integration/fixtures/climate_control_action.yaml b/tests/integration/fixtures/climate_control_action.yaml new file mode 100644 index 00000000000..1dd300fcc29 --- /dev/null +++ b/tests/integration/fixtures/climate_control_action.yaml @@ -0,0 +1,92 @@ +esphome: + name: climate-control-action-test +host: +api: +logger: + level: DEBUG + +globals: + - id: test_target_temp + type: float + initial_value: "21.5" + +sensor: + - platform: template + id: temp_sensor + name: "Temp" + lambda: 'return 20.0;' + update_interval: 60s + +climate: + - platform: thermostat + id: test_climate + name: "Test Climate" + sensor: temp_sensor + min_idle_time: 30s + min_heating_off_time: 300s + min_heating_run_time: 300s + min_cooling_off_time: 300s + min_cooling_run_time: 300s + heat_action: + - logger.log: heating + idle_action: + - logger.log: idle + cool_action: + - logger.log: cooling + heat_cool_mode: + - logger.log: heat_cool + preset: + - name: Default + default_target_temperature_low: 18 °C + default_target_temperature_high: 22 °C + visual: + min_temperature: 10 °C + max_temperature: 30 °C + +button: + # mode only + - platform: template + id: btn_mode + name: "Set Mode Heat" + on_press: + - climate.control: + id: test_climate + mode: HEAT + + # mode + target_temperature_low + target_temperature_high + - platform: template + id: btn_mode_temps + name: "Set Mode Temps" + on_press: + - climate.control: + id: test_climate + mode: HEAT_COOL + target_temperature_low: 19.0 °C + target_temperature_high: 23.0 °C + + # target_temperature_low only + - platform: template + id: btn_low_only + name: "Set Low Only" + on_press: + - climate.control: + id: test_climate + target_temperature_low: 17.5 °C + + # Lambda path: target_temperature_high computed at runtime + - platform: template + id: btn_lambda_high + name: "Lambda High" + on_press: + - climate.control: + id: test_climate + target_temperature_high: !lambda "return id(test_target_temp);" + + # mode only — turn off via mode + - platform: template + id: btn_off + name: "Set Off" + on_press: + - climate.control: + id: test_climate + mode: "OFF" diff --git a/tests/integration/test_climate_control_action.py b/tests/integration/test_climate_control_action.py new file mode 100644 index 00000000000..2b0293b2095 --- /dev/null +++ b/tests/integration/test_climate_control_action.py @@ -0,0 +1,84 @@ +"""Integration test for climate ControlAction. + +Tests that climate.control automation actions work correctly with the +single stateless apply lambda/function pointer implementation. Exercises +multiple field combinations and the lambda path. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ( + ButtonInfo, + ClimateInfo, + ClimateMode, + ClimateState, + EntityState, +) +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_climate_control_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test climate ControlAction with constants and lambdas.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + climate_state_future: asyncio.Future[ClimateState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, ClimateState) + and climate_state_future is not None + and not climate_state_future.done() + ): + climate_state_future.set_result(state) + + async def wait_for_climate_state(timeout: float = 5.0) -> ClimateState: + nonlocal climate_state_future + climate_state_future = loop.create_future() + try: + return await asyncio.wait_for(climate_state_future, timeout) + finally: + climate_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_climate", ClimateInfo) + + async def press_and_wait(name: str) -> ClimateState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_climate_state() + + # mode only — set HEAT + state = await press_and_wait("Set Mode Heat") + assert state.mode == ClimateMode.HEAT + + # mode + target_temperature_low + target_temperature_high + state = await press_and_wait("Set Mode Temps") + assert state.mode == ClimateMode.HEAT_COOL + assert state.target_temperature_low == pytest.approx(19.0, abs=0.5) + assert state.target_temperature_high == pytest.approx(23.0, abs=0.5) + + # target_temperature_low only + state = await press_and_wait("Set Low Only") + assert state.target_temperature_low == pytest.approx(17.5, abs=0.5) + + # lambda path: target_temperature_high computed at runtime + state = await press_and_wait("Lambda High") + assert state.target_temperature_high == pytest.approx(21.5, abs=0.5) + + # mode only — turn off via mode + state = await press_and_wait("Set Off") + assert state.mode == ClimateMode.OFF From 92aa98f680c8ea40ff424dfd6a529fd3f6313d7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 19:42:38 -0500 Subject: [PATCH 1797/2056] [host] Move HAL bodies into components/host/hal.cpp + inline trivial dispatches (#16115) --- esphome/components/host/core.cpp | 60 +---------------------------- esphome/components/host/hal.cpp | 65 ++++++++++++++++++++++++++++++++ esphome/core/hal/hal_host.h | 12 +++--- 3 files changed, 73 insertions(+), 64 deletions(-) create mode 100644 esphome/components/host/hal.cpp diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index b067ebbf6ef..91239758848 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,74 +1,16 @@ #ifdef USE_HOST #include "esphome/core/application.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" #include "preferences.h" #include <csignal> -#include <sched.h> -#include <time.h> -#include <cstdlib> namespace { volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void signal_handler(int signal) { s_signal_received = signal; } } // namespace -namespace esphome { - -void HOT yield() { ::sched_yield(); } -uint32_t IRAM_ATTR HOT millis() { - struct timespec spec; - clock_gettime(CLOCK_MONOTONIC, &spec); - return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000); -} -uint64_t millis_64() { - struct timespec spec; - clock_gettime(CLOCK_MONOTONIC, &spec); - return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL; -} -void HOT delay(uint32_t ms) { - struct timespec ts; - ts.tv_sec = ms / 1000; - ts.tv_nsec = (ms % 1000) * 1000000; - int res; - do { - res = nanosleep(&ts, &ts); - } while (res != 0 && errno == EINTR); -} -uint32_t IRAM_ATTR HOT micros() { - struct timespec spec; - clock_gettime(CLOCK_MONOTONIC, &spec); - return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000); -} -void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { - struct timespec ts; - ts.tv_sec = us / 1000000U; - ts.tv_nsec = (us % 1000000U) * 1000U; - int res; - do { - res = nanosleep(&ts, &ts); - } while (res != 0 && errno == EINTR); -} -void arch_restart() { exit(0); } -void arch_init() { - // pass -} -void HOT arch_feed_wdt() { - // pass -} - -uint32_t arch_get_cpu_cycle_count() { - struct timespec spec; - clock_gettime(CLOCK_MONOTONIC, &spec); - time_t seconds = spec.tv_sec; - uint32_t us = spec.tv_nsec; - return ((uint32_t) seconds) * 1000000000U + us; -} -uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } - -} // namespace esphome +// HAL functions live in hal.cpp. void setup(); void loop(); diff --git a/esphome/components/host/hal.cpp b/esphome/components/host/hal.cpp new file mode 100644 index 00000000000..256a12ac624 --- /dev/null +++ b/esphome/components/host/hal.cpp @@ -0,0 +1,65 @@ +#ifdef USE_HOST + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +#include <time.h> +#include <cerrno> +#include <cstdlib> + +// Empty host namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// host component's API. +namespace esphome::host {} // namespace esphome::host + +namespace esphome { + +// yield(), arch_init(), arch_feed_wdt(), arch_get_cpu_freq_hz() inlined in +// core/hal/hal_host.h. + +uint32_t IRAM_ATTR HOT millis() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000); +} +uint64_t millis_64() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL; +} +void HOT delay(uint32_t ms) { + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +uint32_t IRAM_ATTR HOT micros() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000); +} +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { + struct timespec ts; + ts.tv_sec = us / 1000000U; + ts.tv_nsec = (us % 1000000U) * 1000U; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +void arch_restart() { exit(0); } + +uint32_t arch_get_cpu_cycle_count() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t ns = static_cast<uint32_t>(spec.tv_nsec); + return static_cast<uint32_t>(seconds) * 1000000000U + ns; +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/core/hal/hal_host.h b/esphome/core/hal/hal_host.h index a8896fdf638..d7f317176ea 100644 --- a/esphome/core/hal/hal_host.h +++ b/esphome/core/hal/hal_host.h @@ -3,6 +3,7 @@ #ifdef USE_HOST #include <cstdint> +#include <sched.h> #define IRAM_ATTR #define PROGMEM @@ -13,17 +14,18 @@ namespace esphome { /// Host has no ISR concept. __attribute__((always_inline)) inline bool in_isr_context() { return false; } -void yield(); +__attribute__((always_inline)) inline void yield() { ::sched_yield(); } + void delay(uint32_t ms); uint32_t micros(); uint32_t millis(); uint64_t millis_64(); - void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) -void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); -void arch_init(); -uint32_t arch_get_cpu_freq_hz(); + +__attribute__((always_inline)) inline void arch_init() {} +__attribute__((always_inline)) inline void arch_feed_wdt() {} +__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } } // namespace esphome From 9999913d072b930bdf330ba421e403126d7b65a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 20:10:51 -0500 Subject: [PATCH 1798/2056] [zephyr] Move HAL bodies into components/zephyr/hal.cpp + inline trivial dispatches (#16116) --- esphome/components/zephyr/core.cpp | 52 +----------------------- esphome/components/zephyr/hal.cpp | 63 ++++++++++++++++++++++++++++++ esphome/core/hal/hal_zephyr.h | 20 ++++++---- 3 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 esphome/components/zephyr/hal.cpp diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 93a9a1ae8ee..d1bdaee02d3 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -1,8 +1,6 @@ #ifdef USE_ZEPHYR #include <zephyr/kernel.h> -#include <zephyr/drivers/watchdog.h> -#include <zephyr/sys/reboot.h> #include <zephyr/random/random.h> #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -10,55 +8,7 @@ namespace esphome { -#ifdef CONFIG_WATCHDOG -static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); -#endif - -void yield() { ::k_yield(); } -uint32_t millis() { return static_cast<uint32_t>(millis_64()); } -uint64_t millis_64() { return static_cast<uint64_t>(k_uptime_get()); } -uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } -void delayMicroseconds(uint32_t us) { ::k_usleep(us); } -void delay(uint32_t ms) { ::k_msleep(ms); } - -void arch_init() { -#ifdef CONFIG_WATCHDOG - if (device_is_ready(WDT)) { - static wdt_timeout_cfg wdt_config{}; - wdt_config.flags = WDT_FLAG_RESET_SOC; -#ifdef USE_ZIGBEE - // zboss thread use a lot of cpu cycles during start - wdt_config.window.max = 10000; -#else - wdt_config.window.max = 2000; -#endif - wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); - if (wdt_channel_id >= 0) { - uint8_t options = 0; -#ifdef USE_DEBUG - options |= WDT_OPT_PAUSE_HALTED_BY_DBG; -#endif -#ifdef USE_DEEP_SLEEP - options |= WDT_OPT_PAUSE_IN_SLEEP; -#endif - wdt_setup(WDT, options); - } - } -#endif -} - -void arch_feed_wdt() { -#ifdef CONFIG_WATCHDOG - if (wdt_channel_id >= 0) { - wdt_feed(WDT, wdt_channel_id); - } -#endif -} - -void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } -uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } -uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } +// HAL functions live in hal.cpp. Mutex::Mutex() { auto *mutex = new k_mutex(); diff --git a/esphome/components/zephyr/hal.cpp b/esphome/components/zephyr/hal.cpp new file mode 100644 index 00000000000..5c08ed25196 --- /dev/null +++ b/esphome/components/zephyr/hal.cpp @@ -0,0 +1,63 @@ +#ifdef USE_ZEPHYR + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#include <zephyr/drivers/watchdog.h> +#include <zephyr/sys/reboot.h> + +// Empty zephyr namespace block to satisfy ci-custom's lint_namespace check. +// HAL functions live in namespace esphome (root) — they are not part of the +// zephyr component's API. +namespace esphome::zephyr {} // namespace esphome::zephyr + +namespace esphome { + +#ifdef CONFIG_WATCHDOG +static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); +#endif + +// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), +// arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz() inlined in +// core/hal/hal_zephyr.h. + +void arch_init() { +#ifdef CONFIG_WATCHDOG + if (device_is_ready(WDT)) { + static wdt_timeout_cfg wdt_config{}; + wdt_config.flags = WDT_FLAG_RESET_SOC; +#ifdef USE_ZIGBEE + // zboss thread uses a lot of CPU cycles during startup + wdt_config.window.max = 10000; +#else + wdt_config.window.max = 2000; +#endif + wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); + if (wdt_channel_id >= 0) { + uint8_t options = 0; +#ifdef USE_DEBUG + options |= WDT_OPT_PAUSE_HALTED_BY_DBG; +#endif +#ifdef USE_DEEP_SLEEP + options |= WDT_OPT_PAUSE_IN_SLEEP; +#endif + wdt_setup(WDT, options); + } + } +#endif +} + +void arch_feed_wdt() { +#ifdef CONFIG_WATCHDOG + if (wdt_channel_id >= 0) { + wdt_feed(WDT, wdt_channel_id); + } +#endif +} + +void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } + +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/core/hal/hal_zephyr.h index d4b37b5eb6c..613b3911c11 100644 --- a/esphome/core/hal/hal_zephyr.h +++ b/esphome/core/hal/hal_zephyr.h @@ -4,6 +4,8 @@ #include <cstdint> +#include <zephyr/kernel.h> + #define IRAM_ATTR #define PROGMEM @@ -13,17 +15,19 @@ namespace esphome { /// Zephyr/nRF52: not currently consulted — wake path is platform-specific. __attribute__((always_inline)) inline bool in_isr_context() { return false; } -void yield(); -void delay(uint32_t ms); -uint32_t micros(); -uint32_t millis(); -uint64_t millis_64(); +__attribute__((always_inline)) inline void yield() { ::k_yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::k_msleep(ms); } +__attribute__((always_inline)) inline uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return static_cast<uint64_t>(k_uptime_get()); } +__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(millis_64()); } + +// NOLINTNEXTLINE(readability-identifier-naming) +__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { ::k_usleep(us); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } +__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } -void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void arch_feed_wdt(); -uint32_t arch_get_cpu_cycle_count(); void arch_init(); -uint32_t arch_get_cpu_freq_hz(); } // namespace esphome From faa61696e036cb26e44ef0e4a28400e0636c5ca3 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 30 Apr 2026 21:43:24 -0400 Subject: [PATCH 1799/2056] [sendspin] Use sendspin-cpp to v0.4.0 to reduce stuttering (#16178) --- esphome/components/sendspin/__init__.py | 30 ++++++++++++++++--- .../sendspin/media_source/__init__.py | 4 +++ esphome/idf_component.yml | 2 +- .../sendspin/common-media_source.yaml | 1 + 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 58687ae8389..1348bf2bfc3 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -24,6 +24,7 @@ CONF_SENDSPIN_ID = "sendspin_id" CONF_INITIAL_STATIC_DELAY = "initial_static_delay" CONF_FIXED_DELAY = "fixed_delay" +CONF_DECODE_MEMORY = "decode_memory" # sendspin-cpp library lives in the global `sendspin` namespace. sendspin_library_ns = cg.global_ns.namespace("sendspin") @@ -39,6 +40,18 @@ CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED") AudioSupportedFormatObject = sendspin_library_ns.struct("AudioSupportedFormatObject") PlayerRoleConfig = sendspin_library_ns.struct("PlayerRoleConfig") +# MemoryLocation enum (from sendspin/types.h) controls SPIRAM-vs-internal-RAM placement +# preference for the player role's transfer buffers. +SendspinMemoryLocation = sendspin_library_ns.enum("MemoryLocation", is_class=True) + +MEMORY_PSRAM = "psram" +MEMORY_INTERNAL = "internal" +MEMORY_LOCATIONS = [MEMORY_PSRAM, MEMORY_INTERNAL] +MEMORY_LOCATION_ENUM = { + MEMORY_PSRAM: SendspinMemoryLocation.PREFER_EXTERNAL, + MEMORY_INTERNAL: SendspinMemoryLocation.PREFER_INTERNAL, +} + # Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace. # Analysis tools strip the trailing underscore (same pattern as `template_`). sendspin_ns = cg.esphome_ns.namespace("sendspin_") @@ -193,7 +206,7 @@ async def to_code(config: ConfigType) -> None: ) # sendspin-cpp library - esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.1") + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.4.0") cg.add_define("USE_SENDSPIN", True) # for MDNS @@ -249,14 +262,23 @@ async def to_code(config: ConfigType) -> None: "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True ) - player_config_struct = cg.StructInitializer( - PlayerRoleConfig, + # Library defaults: priority 18 (one above httpd_priority 17 so the decoder is not + # starved by the HTTP server during the initial encoded-audio burst at stream start), + # interpolation/decode buffer locations PREFER_EXTERNAL. + player_struct_fields = [ ("audio_formats", audio_format_structs), ("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]), ("fixed_delay_us", player_cfg[CONF_FIXED_DELAY]), ("initial_static_delay_ms", player_cfg[CONF_INITIAL_STATIC_DELAY]), ("psram_stack", psram_stack), - ("priority", 2), + ] + if (decode_memory := player_cfg.get(CONF_DECODE_MEMORY)) is not None: + player_struct_fields.append( + ("decode_buffer_location", MEMORY_LOCATION_ENUM[decode_memory]) + ) + player_config_struct = cg.StructInitializer( + PlayerRoleConfig, + *player_struct_fields, ) cg.add(var.set_player_config(player_config_struct)) else: diff --git a/esphome/components/sendspin/media_source/__init__.py b/esphome/components/sendspin/media_source/__init__.py index 6d61a8a6361..f689ab01cb4 100644 --- a/esphome/components/sendspin/media_source/__init__.py +++ b/esphome/components/sendspin/media_source/__init__.py @@ -13,9 +13,11 @@ from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType from .. import ( + CONF_DECODE_MEMORY, CONF_FIXED_DELAY, CONF_INITIAL_STATIC_DELAY, CONF_SENDSPIN_ID, + MEMORY_LOCATIONS, SendspinHub, _validate_task_stack_in_psram, register_player_config, @@ -57,6 +59,7 @@ def _register(config: ConfigType) -> ConfigType: CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY], CONF_FIXED_DELAY: config[CONF_FIXED_DELAY], CONF_TASK_STACK_IN_PSRAM: config.get(CONF_TASK_STACK_IN_PSRAM, False), + CONF_DECODE_MEMORY: config.get(CONF_DECODE_MEMORY), } ) return config @@ -82,6 +85,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.int_range( min=16000, max=96000 ), + cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True), } ), cv.only_on_esp32, diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 757fcf9dd77..d02c3adb8f5 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -92,6 +92,6 @@ dependencies: esp32async/asynctcp: version: 3.4.91 sendspin/sendspin-cpp: - version: 0.3.1 + version: 0.4.0 lvgl/lvgl: version: 9.5.0 diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 4a7cd79c67d..5b33a546477 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -7,3 +7,4 @@ media_source: initial_static_delay: 5ms static_delay_adjustable: true fixed_delay: 480us + decode_memory: internal From 08e5cb55764033d11e4feb35c2c6a7fc3fa2da50 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:47:22 -0400 Subject: [PATCH 1800/2056] [esp32_hosted] Bump esp_hosted to 2.12.6 and esp_wifi_remote to 1.5.1 (#16176) --- esphome/components/esp32_hosted/__init__.py | 7 ++++--- esphome/idf_component.yml | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 1619a845d84..eca7c24b102 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -246,9 +246,10 @@ async def to_code(config): idf_ver = esp32.idf_version() os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}" if idf_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") - esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1") + esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2") + esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index d02c3adb8f5..f5a8dd8c601 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -20,15 +20,19 @@ dependencies: espressif/mdns: version: 1.11.0 espressif/esp_wifi_remote: - version: 1.4.0 + version: 1.5.1 + rules: + - if: "target in [esp32h2, esp32p4]" + espressif/wifi_remote_over_eppp: + version: 0.3.2 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: - version: 1.1.4 + version: 1.1.5 rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.1 + version: 2.12.6 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From f6e39d305d5410e7524ffb4d9d7fb0b388c861ae Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Fri, 1 May 2026 04:08:55 +0200 Subject: [PATCH 1801/2056] [zigbee] Add newlib compatibility for zigbee sdk in idf 6 (#16174) --- esphome/components/zigbee/zigbee_esp32.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index 1b98df6c0a5..9081582c7be 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -9,6 +9,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, add_partition, + idf_version, require_vfs_select, ) import esphome.config_validation as cv @@ -186,6 +187,10 @@ async def _zigbee_add_sdkconfigs(config: ConfigType) -> None: # The pre-built Zigbee library uses esp_log_default_level which requires # dynamic log level control to be enabled add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", True) + # The pre-built Zigbee library is compiled against newlib which requires newlib + # reentrancy to be enabled with picolibc compatibility. + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", True) async def attributes_to_code( From b8dfffdf062f47c1097c0a8afa28305218275915 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 21:20:07 -0500 Subject: [PATCH 1802/2056] [core] Enable ruff FLY (flynt) lint family (#16182) --- esphome/platformio_runner.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/platformio_runner.py b/esphome/platformio_runner.py index 599c9408a49..5b14a725577 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio_runner.py @@ -101,7 +101,7 @@ def patch_file_downloader() -> None: FileDownloader.__init__ = patched_init -_IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" +_IGNORE_LIB_WARNINGS = "(?:Hash|Update)" # Regex patterns matched against each line of PlatformIO output. Lines that # match are dropped by RedirectText before they reach the parent process. # Patterns are anchored at the start of the line (RedirectText uses diff --git a/pyproject.toml b/pyproject.toml index dc6785001d3..d16bf2b6255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ exclude = ['generated'] select = [ "E", # pycodestyle "F", # pyflakes/autoflake + "FLY", # flynt: convert string formatting to f-strings "FURB", # refurb "I", # isort "PERF", # performance From 0980630f6820300cfc5a1f97df3d8f333987e9ad Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 1 May 2026 12:23:14 +1000 Subject: [PATCH 1803/2056] [lvgl] Clamp values for meter line indicators (#16180) --- esphome/components/lvgl/lvgl_esphome.cpp | 6 ++++-- esphome/components/lvgl/lvgl_esphome.h | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 0308e6b783f..eb85faa16cd 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -454,10 +454,12 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { #ifdef USE_LVGL_METER -int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) { +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value) { auto *scale = lv_obj_get_parent(obj); auto min_value = lv_scale_get_range_min_value(scale); - return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) + + auto max_value = lv_scale_get_range_max_value(scale); + value = clamp(value, min_value, max_value); + return ((value - min_value) * lv_scale_get_angle_range(scale) / (max_value - min_value) + lv_scale_get_rotation((scale))) % 360; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 83cf9cc0995..be1f150affe 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -112,7 +112,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images #endif // USE_LVGL_ANIMIMG #ifdef USE_LVGL_METER -int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value); +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); #endif #ifdef USE_LVGL_GRADIENT From 5cc447e0da5eb24c53afe594acffcb24e8bc0bb0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 21:27:31 -0500 Subject: [PATCH 1804/2056] [core] Move per-platform hal_platform.h into components/platform/hal.h (#16183) --- .../hal_esp32.h => components/esp32/hal.h} | 2 ++ esphome/components/esp8266/hal.cpp | 2 +- .../esp8266/hal.h} | 2 ++ esphome/components/host/hal.cpp | 2 +- .../hal/hal_host.h => components/host/hal.h} | 2 ++ esphome/components/libretiny/hal.cpp | 2 +- .../libretiny/hal.h} | 2 ++ esphome/components/rp2040/hal.cpp | 2 +- .../hal_rp2040.h => components/rp2040/hal.h} | 2 ++ esphome/components/zephyr/hal.cpp | 2 +- .../hal_zephyr.h => components/zephyr/hal.h} | 2 ++ esphome/core/hal.h | 24 +++++++++---------- 12 files changed, 29 insertions(+), 17 deletions(-) rename esphome/{core/hal/hal_esp32.h => components/esp32/hal.h} (98%) rename esphome/{core/hal/hal_esp8266.h => components/esp8266/hal.h} (98%) rename esphome/{core/hal/hal_host.h => components/host/hal.h} (96%) rename esphome/{core/hal/hal_libretiny.h => components/libretiny/hal.h} (99%) rename esphome/{core/hal/hal_rp2040.h => components/rp2040/hal.h} (98%) rename esphome/{core/hal/hal_zephyr.h => components/zephyr/hal.h} (97%) diff --git a/esphome/core/hal/hal_esp32.h b/esphome/components/esp32/hal.h similarity index 98% rename from esphome/core/hal/hal_esp32.h rename to esphome/components/esp32/hal.h index d5d7752bf6a..2180f07f6c3 100644 --- a/esphome/core/hal/hal_esp32.h +++ b/esphome/components/esp32/hal.h @@ -15,6 +15,8 @@ #define PROGMEM #endif +namespace esphome::esp32 {} + namespace esphome { // Forward decl from helpers.h (esphome/core/helpers.h) — kept here so this diff --git a/esphome/components/esp8266/hal.cpp b/esphome/components/esp8266/hal.cpp index 56910e5b399..e8f472dc8a6 100644 --- a/esphome/components/esp8266/hal.cpp +++ b/esphome/components/esp8266/hal.cpp @@ -18,7 +18,7 @@ namespace esphome::esp8266 {} // namespace esphome::esp8266 namespace esphome { // yield(), micros(), millis_64(), delayMicroseconds(), arch_feed_wdt(), -// progmem_read_*() are inlined in core/hal/hal_esp8266.h. +// progmem_read_*() are inlined in components/esp8266/hal.h. // // Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit // multiplies on the LX106). Tracks a running ms counter from 32-bit diff --git a/esphome/core/hal/hal_esp8266.h b/esphome/components/esp8266/hal.h similarity index 98% rename from esphome/core/hal/hal_esp8266.h rename to esphome/components/esp8266/hal.h index b6e3b1ee3cb..effa9c93719 100644 --- a/esphome/core/hal/hal_esp8266.h +++ b/esphome/components/esp8266/hal.h @@ -25,6 +25,8 @@ extern "C" unsigned long millis(void); // NOLINTNEXTLINE(readability-redundant-declaration) extern "C" void system_soft_wdt_feed(void); +namespace esphome::esp8266 {} + namespace esphome { // Forward decl from helpers.h so this header stays cheap. diff --git a/esphome/components/host/hal.cpp b/esphome/components/host/hal.cpp index 256a12ac624..c7fef8d2e86 100644 --- a/esphome/components/host/hal.cpp +++ b/esphome/components/host/hal.cpp @@ -15,7 +15,7 @@ namespace esphome::host {} // namespace esphome::host namespace esphome { // yield(), arch_init(), arch_feed_wdt(), arch_get_cpu_freq_hz() inlined in -// core/hal/hal_host.h. +// components/host/hal.h. uint32_t IRAM_ATTR HOT millis() { struct timespec spec; diff --git a/esphome/core/hal/hal_host.h b/esphome/components/host/hal.h similarity index 96% rename from esphome/core/hal/hal_host.h rename to esphome/components/host/hal.h index d7f317176ea..12abf6684d3 100644 --- a/esphome/core/hal/hal_host.h +++ b/esphome/components/host/hal.h @@ -8,6 +8,8 @@ #define IRAM_ATTR #define PROGMEM +namespace esphome::host {} + namespace esphome { /// Returns true when executing inside an interrupt handler. diff --git a/esphome/components/libretiny/hal.cpp b/esphome/components/libretiny/hal.cpp index e6dbb7296c5..67e902024d3 100644 --- a/esphome/components/libretiny/hal.cpp +++ b/esphome/components/libretiny/hal.cpp @@ -16,7 +16,7 @@ namespace esphome { // yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), // arch_feed_wdt(), arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz() -// inlined in core/hal/hal_libretiny.h. +// inlined in components/libretiny/hal.h. void arch_init() { libretiny::setup_preferences(); diff --git a/esphome/core/hal/hal_libretiny.h b/esphome/components/libretiny/hal.h similarity index 99% rename from esphome/core/hal/hal_libretiny.h rename to esphome/components/libretiny/hal.h index db0fc11bfbe..9c512504b72 100644 --- a/esphome/core/hal/hal_libretiny.h +++ b/esphome/components/libretiny/hal.h @@ -61,6 +61,8 @@ extern "C" void lt_wdt_feed(void); extern "C" uint32_t lt_cpu_get_cycle_count(void); extern "C" uint32_t lt_cpu_get_freq(void); +namespace esphome::libretiny {} + namespace esphome { /// Returns true when executing inside an interrupt handler. diff --git a/esphome/components/rp2040/hal.cpp b/esphome/components/rp2040/hal.cpp index 7475205d60c..e71d3fd54d6 100644 --- a/esphome/components/rp2040/hal.cpp +++ b/esphome/components/rp2040/hal.cpp @@ -17,7 +17,7 @@ namespace esphome::rp2040 {} // namespace esphome::rp2040 namespace esphome { // yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), -// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in core/hal/hal_rp2040.h. +// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in components/rp2040/hal.h. void arch_restart() { watchdog_reboot(0, 0, 10); while (1) { diff --git a/esphome/core/hal/hal_rp2040.h b/esphome/components/rp2040/hal.h similarity index 98% rename from esphome/core/hal/hal_rp2040.h rename to esphome/components/rp2040/hal.h index 27a9b23c0b6..c9c61c921da 100644 --- a/esphome/core/hal/hal_rp2040.h +++ b/esphome/components/rp2040/hal.h @@ -25,6 +25,8 @@ extern "C" uint64_t time_us_64(void); extern "C" void watchdog_update(void); extern "C" unsigned long ulMainGetRunTimeCounterValue(void); +namespace esphome::rp2040 {} + namespace esphome { // Forward decl from helpers.h. diff --git a/esphome/components/zephyr/hal.cpp b/esphome/components/zephyr/hal.cpp index 5c08ed25196..6c405b650ec 100644 --- a/esphome/components/zephyr/hal.cpp +++ b/esphome/components/zephyr/hal.cpp @@ -20,7 +20,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); // yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(), // arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz() inlined in -// core/hal/hal_zephyr.h. +// components/zephyr/hal.h. void arch_init() { #ifdef CONFIG_WATCHDOG diff --git a/esphome/core/hal/hal_zephyr.h b/esphome/components/zephyr/hal.h similarity index 97% rename from esphome/core/hal/hal_zephyr.h rename to esphome/components/zephyr/hal.h index 613b3911c11..11994b68b7b 100644 --- a/esphome/core/hal/hal_zephyr.h +++ b/esphome/components/zephyr/hal.h @@ -9,6 +9,8 @@ #define IRAM_ATTR #define PROGMEM +namespace esphome::zephyr {} + namespace esphome { /// Returns true when executing inside an interrupt handler. diff --git a/esphome/core/hal.h b/esphome/core/hal.h index a53296979c6..4babda807d9 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -8,22 +8,22 @@ // Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(), // inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem -// helpers) live under esphome/core/hal/ and are dispatched here based on -// the active USE_* platform define. Each header guards its body with the -// matching #ifdef USE_<platform> and re-enters namespace esphome {} so it -// is safe to be re-included. +// helpers) live next to each platform component as components/<platform>/hal.h +// and are dispatched here based on the active USE_* platform define. Each +// header guards its body with the matching #ifdef USE_<platform> and re-enters +// namespace esphome {} so it is safe to be re-included. #if defined(USE_ESP32) -#include "esphome/core/hal/hal_esp32.h" +#include "esphome/components/esp32/hal.h" #elif defined(USE_ESP8266) -#include "esphome/core/hal/hal_esp8266.h" +#include "esphome/components/esp8266/hal.h" #elif defined(USE_LIBRETINY) -#include "esphome/core/hal/hal_libretiny.h" +#include "esphome/components/libretiny/hal.h" #elif defined(USE_RP2040) -#include "esphome/core/hal/hal_rp2040.h" +#include "esphome/components/rp2040/hal.h" #elif defined(USE_HOST) -#include "esphome/core/hal/hal_host.h" +#include "esphome/components/host/hal.h" #elif defined(USE_ZEPHYR) -#include "esphome/core/hal/hal_zephyr.h" +#include "esphome/components/zephyr/hal.h" #else #error "hal.h: not implemented for this platform" #endif @@ -33,12 +33,12 @@ namespace esphome { // Cross-platform declarations. delayMicroseconds(), arch_feed_wdt(), // arch_get_cpu_cycle_count(), arch_init(), arch_get_cpu_freq_hz() vary // per platform (some inline, some out-of-line) so they live in -// hal/hal_<platform>.h. +// components/<platform>/hal.h. void __attribute__((noreturn)) arch_restart(); #ifndef USE_ESP8266 // All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences. -// ESP8266's out-of-line declarations live in hal/hal_esp8266.h. +// ESP8266's out-of-line declarations live in components/esp8266/hal.h. inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } inline const char *progmem_read_ptr(const char *const *addr) { return *addr; } inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } From f073c1cabe180f10fc9af0b345a2b03cc4ec164e Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke <okleinecke@web.de> Date: Fri, 1 May 2026 12:43:13 +0200 Subject: [PATCH 1805/2056] [usb_host][usb_uart] Add configurable max packet size (#14584) --- esphome/components/usb_host/__init__.py | 25 ++++++++++++++++--- esphome/components/usb_host/usb_host.h | 2 ++ .../components/usb_host/usb_host_client.cpp | 2 +- esphome/components/usb_uart/__init__.py | 12 ++++++--- esphome/components/usb_uart/usb_uart.cpp | 14 +++++------ esphome/components/usb_uart/usb_uart.h | 11 ++++---- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 338bd8d5728..8e591bd80c7 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -10,6 +10,7 @@ from esphome.components.esp32 import ( ) import esphome.config_validation as cv from esphome.const import CONF_DEVICES, CONF_ID +from esphome.core import CORE from esphome.cpp_types import Component from esphome.types import ConfigType @@ -19,14 +20,15 @@ DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") USBHost = usb_host_ns.class_("USBHost", Component) USBClient = usb_host_ns.class_("USBClient", Component) - +DOMAIN = "usb_host" CONF_VID = "vid" CONF_PID = "pid" CONF_ENABLE_HUBS = "enable_hubs" CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests" +CONF_MAX_PACKET_SIZE = "max_packet_size" -def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: +def usb_device_schema(cls=USBClient, vid: int = None, pid: int = None) -> cv.Schema: schema = cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(cls), @@ -43,6 +45,17 @@ def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.S return schema +def _set_max_packet_size(config: dict) -> dict: + CORE.data.setdefault(DOMAIN, {})[CONF_MAX_PACKET_SIZE] = config[ + CONF_MAX_PACKET_SIZE + ] + return config + + +def get_max_packet_size() -> int: + return CORE.data.get(DOMAIN, {}).get(CONF_MAX_PACKET_SIZE, 64) + + CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { @@ -51,10 +64,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range( min=1, max=32 ), + cv.Optional(CONF_MAX_PACKET_SIZE, default=64): cv.one_of( + 64, 128, 256, 512, 1024, int=True + ), cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), only_on_variant(supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3]), + _set_max_packet_size, ) @@ -72,8 +89,8 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_ENABLE_HUBS): add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) - max_requests = config[CONF_MAX_TRANSFER_REQUESTS] - cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) + cg.add_define("USB_HOST_MAX_REQUESTS", config[CONF_MAX_TRANSFER_REQUESTS]) + cg.add_define("USB_HOST_MAX_PACKET_SIZE", config[CONF_MAX_PACKET_SIZE]) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index dcb76a3a3ba..480fd86750b 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -66,6 +66,8 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; +static constexpr size_t USB_MAX_PACKET_SIZE = + USB_HOST_MAX_PACKET_SIZE; // Max USB packet size (64 for FS, 512 for P4 HS) static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5) diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index c34c7ef67d9..4ee8e2ac5e2 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -217,7 +217,7 @@ void USBClient::setup() { // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime for (auto &request : this->requests_) { - usb_host_transfer_alloc(64, 0, &request.transfer); + usb_host_transfer_alloc(USB_MAX_PACKET_SIZE, 0, &request.transfer); request.client = this; // Set once, never changes } diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index d542788fb97..1cf78fdbd53 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,7 +1,11 @@ import esphome.codegen as cg from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent -from esphome.components.usb_host import register_usb_client, usb_device_schema +from esphome.components.usb_host import ( + get_max_packet_size, + register_usb_client, + usb_device_schema, +) import esphome.config_validation as cv from esphome.const import ( CONF_BAUD_RATE, @@ -118,14 +122,14 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): # The output chunk pool/queue are compile-time-sized templates shared by all # USBUartChannel instances, so use the largest buffer_size across every channel - # of every device. Each chunk is 64 bytes (USB FS MPS); add one extra slot - # because LockFreeQueue<T,N> is a ring buffer that wastes one entry. + # of every device. Add one extra slot because LockFreeQueue<T,N> is a ring + # buffer that wastes one entry. max_buffer_size = max( channel[CONF_BUFFER_SIZE] for device in config for channel in device[CONF_CHANNELS] ) - output_chunk_count = max_buffer_size // 64 + 1 + output_chunk_count = max(max_buffer_size // get_max_packet_size(), 2) + 1 cg.add_define("USB_UART_OUTPUT_CHUNK_COUNT", output_chunk_count) for device in config: diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 30ec61fdc4b..e3bf5e40bc1 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -157,7 +157,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { ESP_LOGE(TAG, "Output pool full - lost %zu bytes", len); break; } - size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); + uint16_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE); memcpy(chunk->data, data, chunk_len); chunk->length = static_cast<uint8_t>(chunk_len); // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if @@ -222,7 +222,7 @@ void USBUartComponent::loop() { #ifdef USE_UART_DEBUGGER if (channel->debug_) { - char buf[4 + format_hex_pretty_size(UsbDataChunk::MAX_CHUNK_SIZE)]; // "<<< " + hex + char buf[4 + format_hex_pretty_size(usb_host::USB_MAX_PACKET_SIZE)]; // "<<< " + hex memcpy(buf, "<<< ", 4); format_hex_pretty_to(buf + 4, sizeof(buf) - 4, chunk->data, chunk->length, ','); ESP_LOGD(TAG, "%s%s", channel->debug_prefix_.c_str(), buf); @@ -377,7 +377,7 @@ void USBUartComponent::start_output(USBUartChannel *channel) { this->start_output(channel); }; - const uint8_t len = chunk->length; + const auto len = chunk->length; if (!this->transfer_out(ep->bEndpointAddress, callback, chunk->data, len)) { // Transfer submission failed — return chunk and release flag so callers can retry. channel->output_pool_.release(chunk); @@ -394,10 +394,10 @@ void USBUartComponent::start_output(USBUartChannel *channel) { static void fix_mps(const usb_ep_desc_t *ep) { if (ep != nullptr) { auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep); - if (ep->wMaxPacketSize > 64) { - ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to 64", static_cast<uint8_t>(ep->bEndpointAddress & 0xFF), - ep->wMaxPacketSize); - ep_mutable->wMaxPacketSize = 64; + if (ep->wMaxPacketSize > usb_host::USB_MAX_PACKET_SIZE) { + ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to %u", static_cast<uint8_t>(ep->bEndpointAddress & 0xFF), + ep->wMaxPacketSize, usb_host::USB_MAX_PACKET_SIZE); + ep_mutable->wMaxPacketSize = usb_host::USB_MAX_PACKET_SIZE; } } } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index f9648b795b6..e88c41c0cb3 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -106,20 +106,19 @@ class RingBuffer { // Structure for queuing received USB data chunks struct UsbDataChunk { - static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size - uint8_t data[MAX_CHUNK_SIZE]; - uint8_t length; // Max 64 bytes, so uint8_t is sufficient + uint8_t data[usb_host::USB_MAX_PACKET_SIZE]; + uint16_t length; USBUartChannel *channel; // Required for EventPool - no cleanup needed for POD types void release() {} }; -// Structure for queuing outgoing USB data chunks (one per USB FS packet) +// Structure for queuing outgoing USB data chunks (one per USB packet) struct UsbOutputChunk { - static constexpr size_t MAX_CHUNK_SIZE = 64; // USB FS MPS + static constexpr size_t MAX_CHUNK_SIZE = usb_host::USB_MAX_PACKET_SIZE; uint8_t data[MAX_CHUNK_SIZE]; - uint8_t length; + uint16_t length; // Required for EventPool - no cleanup needed for POD types void release() {} From 3dd60c57134eea138ed0d78d84ff5d2465670e29 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 1 May 2026 08:55:08 -0400 Subject: [PATCH 1806/2056] [core] Support allocating ring buffer in internal memory (#16187) --- esphome/core/ring_buffer.cpp | 13 +++++++------ esphome/core/ring_buffer.h | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index 6a2232599f4..2e0802eceb7 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -1,11 +1,9 @@ #include "ring_buffer.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - #ifdef USE_ESP32 -#include "helpers.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { @@ -19,12 +17,15 @@ RingBuffer::~RingBuffer() { } } -std::unique_ptr<RingBuffer> RingBuffer::create(size_t len) { +std::unique_ptr<RingBuffer> RingBuffer::create(size_t len, MemoryPreference preference) { std::unique_ptr<RingBuffer> rb = make_unique<RingBuffer>(); rb->size_ = len; - RAMAllocator<uint8_t> allocator; + const uint8_t type = (preference == MemoryPreference::INTERNAL_FIRST) ? RAMAllocator<uint8_t>::PREFER_INTERNAL + : RAMAllocator<uint8_t>::NONE; + + RAMAllocator<uint8_t> allocator(type); rb->storage_ = allocator.allocate(rb->size_); if (rb->storage_ == nullptr) { return nullptr; diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 98a273781fb..4acd07d5b0c 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -80,7 +80,12 @@ class RingBuffer { */ BaseType_t reset(); - static std::unique_ptr<RingBuffer> create(size_t len); + enum class MemoryPreference { + EXTERNAL_FIRST, // External RAM preferred, fall back to internal (default) + INTERNAL_FIRST, // Internal RAM preferred, fall back to external + }; + + static std::unique_ptr<RingBuffer> create(size_t len, MemoryPreference preference = MemoryPreference::EXTERNAL_FIRST); protected: /// @brief Discards data from the ring buffer. From 58cb7effd45323c28c679036b9574d52d0d85e95 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Fri, 1 May 2026 15:40:14 +0000 Subject: [PATCH 1807/2056] [ota] Add extended OTA protocol (#16164) Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/__main__.py | 11 +- .../components/esphome/ota/ota_esphome.cpp | 50 +++- esphome/components/esphome/ota/ota_esphome.h | 4 +- esphome/components/ota/ota_backend.h | 8 + esphome/espota2.py | 220 ++++++++++------ tests/unit_tests/test_espota2.py | 244 +++++++++++++++++- tests/unit_tests/test_main.py | 23 +- 7 files changed, 456 insertions(+), 104 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 781bcd62885..e7ce36ae2d2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1125,15 +1125,16 @@ def upload_program( remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) - if getattr(args, "file", None) is not None: - binary = Path(args.file) - else: - binary = CORE.firmware_bin # Resolve MQTT magic strings to actual IP addresses network_devices = _resolve_network_devices(devices, config, args) - return espota2.run_ota(network_devices, remote_port, password, binary) + binary = CORE.firmware_bin + ota_type = espota2.OTA_TYPE_UPDATE_APP + if getattr(args, "file", None) is not None: + binary = Path(args.file) + + return espota2.run_ota(network_devices, remote_port, password, binary, ota_type) def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index be771eb6899..955b4dc96fb 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -114,8 +114,10 @@ void ESPHomeOTAComponent::loop() { this->handle_handshake_(); } -static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; -static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; +static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; +static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; +static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; +static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -201,16 +203,30 @@ void ESPHomeOTAComponent::handle_handshake_() { this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); this->transition_ota_state_(OTAState::FEATURE_ACK); - this->handshake_buf_[0] = - ((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression()) - ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION - : ota::OTA_RESPONSE_HEADER_OK; + + const bool supports_compression = + (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression(); + + // Compose the feature-ack response. When the client negotiates the extended protocol we emit + // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte + // legacy response. + this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; + if (this->extended_proto_) { + static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack"); + this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS; + this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); + } else { + this->handshake_buf_[0] = + supports_compression ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION : ota::OTA_RESPONSE_HEADER_OK; + } [[fallthrough]]; } case OTAState::FEATURE_ACK: { - // Acknowledge header - 1 byte - if (!this->try_write_(1, LOG_STR("ack feature"))) { + static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1; + static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2; + const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; + if (!this->try_write_(ack_size, LOG_STR("ack feature"))) { return; } #ifdef USE_OTA_PASSWORD @@ -296,6 +312,7 @@ void ESPHomeOTAComponent::handle_data_() { uint8_t buf[OTA_BUFFER_SIZE]; char *sbuf = reinterpret_cast<char *>(buf); size_t ota_size; + ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP; #if USE_OTA_VERSION == 2 size_t size_acknowledged = 0; #endif @@ -311,6 +328,16 @@ void ESPHomeOTAComponent::handle_data_() { // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); + if (this->extended_proto_) { + // Read ota type, 1 byte + if (!this->readall_(buf, 1)) { + this->log_read_error_(LOG_STR("OTA type")); + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + ota_type = static_cast<ota::OTAType>(buf[0]); + } + ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type); + // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { this->log_read_error_(LOG_STR("size")); @@ -320,6 +347,11 @@ void ESPHomeOTAComponent::handle_data_() { (static_cast<size_t>(buf[2]) << 8) | buf[3]; ESP_LOGV(TAG, "Size is %u bytes", ota_size); + if (ota_type != ota::OTA_TYPE_UPDATE_APP) { + error_code = ota::OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + // Now that we've passed authentication and are actually // starting the update, set the warning status and notify // listeners. This ensures that port scanners do not @@ -616,7 +648,7 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } bool ESPHomeOTAComponent::select_auth_type_() { - bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; + bool client_supports_sha256 = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_SHA256_AUTH) != 0; // Require SHA256 if (!client_supports_sha256) { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 53288fc0005..5043bc33ef8 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -97,8 +97,9 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { ota::OTABackendPtr backend_; uint32_t client_connect_time_{0}; + static constexpr size_t HANDSHAKE_BUF_SIZE = 5; uint16_t port_; - uint8_t handshake_buf_[5]; + uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE]; OTAState ota_state_{OTAState::IDLE}; uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; @@ -106,6 +107,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD + bool extended_proto_{false}; }; } // namespace esphome diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index bd9c4819010..7e7b0f6523c 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include <cstdint> + #ifdef USE_OTA_STATE_LISTENER #include <vector> #endif @@ -23,6 +25,7 @@ enum OTAResponseTypes { OTA_RESPONSE_UPDATE_END_OK = 0x45, OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46, OTA_RESPONSE_CHUNK_OK = 0x47, + OTA_RESPONSE_FEATURE_FLAGS = 0x48, OTA_RESPONSE_ERROR_MAGIC = 0x80, OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81, @@ -38,6 +41,7 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D, + OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; @@ -49,6 +53,10 @@ enum OTAState { OTA_ERROR, }; +enum OTAType : uint8_t { + OTA_TYPE_UPDATE_APP = 0x00, +}; + /** Listener interface for OTA state changes. * * Components can implement this interface to receive OTA state updates diff --git a/esphome/espota2.py b/esphome/espota2.py index 39f51e02e93..f4c0c73589a 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -15,6 +15,8 @@ from typing import Any from esphome.core import EsphomeError from esphome.helpers import ProgressBar, resolve_ip_address +OTA_TYPE_UPDATE_APP = 0x00 + RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 RESPONSE_REQUEST_SHA256_AUTH = 0x02 @@ -27,6 +29,7 @@ RESPONSE_RECEIVE_OK = 0x44 RESPONSE_UPDATE_END_OK = 0x45 RESPONSE_SUPPORTS_COMPRESSION = 0x46 RESPONSE_CHUNK_OK = 0x47 +RESPONSE_FEATURE_FLAGS = 0x48 RESPONSE_ERROR_MAGIC = 0x80 RESPONSE_ERROR_UPDATE_PREPARE = 0x81 @@ -42,6 +45,7 @@ RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A RESPONSE_ERROR_MD5_MISMATCH = 0x8B RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D +RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -49,9 +53,16 @@ OTA_VERSION_2_0 = 2 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] -FEATURE_SUPPORTS_COMPRESSION = 0x01 -FEATURE_SUPPORTS_SHA256_AUTH = 0x02 +CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01 +CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02 +CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04 +SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01 +SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02 +# OTA types this client knows how to send. Future PRs that add bootloader/partition +# updates extend this set. Anything outside the set is rejected up front so callers +# of perform_ota/run_ota get a clear error instead of a post-auth 0x8E from the device. +_SUPPORTED_OTA_TYPES: frozenset[int] = frozenset({OTA_TYPE_UPDATE_APP}) UPLOAD_BLOCK_SIZE = 8192 UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 @@ -64,6 +75,62 @@ _AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { RESPONSE_REQUEST_AUTH: (hashlib.md5, 32, "MD5"), } +# Error response code -> human-readable message (without the "Error: " prefix; check_error() +# prepends it uniformly). Looked up by check_error() to translate a single byte from the device +# into an OTAError. Add new error codes here rather than extending the if-chain in check_error(). +_ERROR_MESSAGES: dict[int, str] = { + RESPONSE_ERROR_MAGIC: "Invalid magic byte", + RESPONSE_ERROR_UPDATE_PREPARE: ( + "Couldn't prepare flash memory for update. Is the binary too big? " + "Please try restarting the ESP." + ), + RESPONSE_ERROR_AUTH_INVALID: "Authentication invalid. Is the password correct?", + RESPONSE_ERROR_WRITING_FLASH: ( + "Writing OTA data to flash memory failed. See USB logs for more information." + ), + RESPONSE_ERROR_UPDATE_END: ( + "Finishing update failed. See the MQTT/USB logs for more information." + ), + RESPONSE_ERROR_INVALID_BOOTSTRAPPING: ( + "Please press the reset button on the ESP. A manual reset is " + "required on the first OTA-Update after flashing via USB." + ), + RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG: ( + "ESP has been flashed with wrong flash size. Please choose the " + "correct 'board' option (esp01_1m always works) and then flash over USB." + ), + RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: ( + "ESP does not have the requested flash size (wrong board). Please " + "choose the correct 'board' option (esp01_1m always works) and try " + "uploading again." + ), + RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: ( + "ESP does not have enough space to store OTA file. Please try " + "flashing a minimal firmware (remove everything except ota)" + ), + RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: ( + "The OTA partition on the ESP is too small. ESPHome needs to resize " + "this partition, please flash over USB." + ), + RESPONSE_ERROR_NO_UPDATE_PARTITION: ( + "The OTA partition on the ESP couldn't be found. ESPHome needs to " + "create this partition, please flash over USB." + ), + RESPONSE_ERROR_MD5_MISMATCH: ( + "Application MD5 code mismatch. Please try again " + "or flash over USB with a good quality cable." + ), + RESPONSE_ERROR_SIGNATURE_INVALID: ( + "Firmware signature verification failed. The firmware was not signed " + "with the correct key. Ensure the signing key matches the one used to build " + "the firmware currently running on the device." + ), + RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE: ( + "The requested OTA type is not supported by the device." + ), + RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", +} + class OTAError(EsphomeError): pass @@ -130,8 +197,10 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None :param expect: Expected response code(s), None to skip validation. :raises OTAError: If an error code is detected or response doesn't match expected. """ - if expect is None: - return + # Detect device errors and connection-closed cases regardless of `expect`. If we + # only ran these checks when expect was set, error bytes returned during + # accept-any-response reads (e.g. feature negotiation, auth nonces) would be + # silently passed through and surface later as cryptic decode/timeout failures. if not data: raise OTAError( "Error: Device closed connection without responding. " @@ -139,69 +208,11 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None "a network issue, or the connection was interrupted." ) dat = data[0] - if dat == RESPONSE_ERROR_MAGIC: - raise OTAError("Error: Invalid magic byte") - if dat == RESPONSE_ERROR_UPDATE_PREPARE: - raise OTAError( - "Error: Couldn't prepare flash memory for update. Is the binary too big? " - "Please try restarting the ESP." - ) - if dat == RESPONSE_ERROR_AUTH_INVALID: - raise OTAError("Error: Authentication invalid. Is the password correct?") - if dat == RESPONSE_ERROR_WRITING_FLASH: - raise OTAError( - "Error: Writing OTA data to flash memory failed. See USB logs for more " - "information." - ) - if dat == RESPONSE_ERROR_UPDATE_END: - raise OTAError( - "Error: Finishing update failed. See the MQTT/USB logs for more " - "information." - ) - if dat == RESPONSE_ERROR_INVALID_BOOTSTRAPPING: - raise OTAError( - "Error: Please press the reset button on the ESP. A manual reset is " - "required on the first OTA-Update after flashing via USB." - ) - if dat == RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG: - raise OTAError( - "Error: ESP has been flashed with wrong flash size. Please choose the " - "correct 'board' option (esp01_1m always works) and then flash over USB." - ) - if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: - raise OTAError( - "Error: ESP does not have the requested flash size (wrong board). Please " - "choose the correct 'board' option (esp01_1m always works) and try " - "uploading again." - ) - if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: - raise OTAError( - "Error: ESP does not have enough space to store OTA file. Please try " - "flashing a minimal firmware (remove everything except ota)" - ) - if dat == RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: - raise OTAError( - "Error: The OTA partition on the ESP is too small. ESPHome needs to resize " - "this partition, please flash over USB." - ) - if dat == RESPONSE_ERROR_NO_UPDATE_PARTITION: - raise OTAError( - "Error: The OTA partition on the ESP couldn't be found. ESPHome needs to create " - "this partition, please flash over USB." - ) - if dat == RESPONSE_ERROR_MD5_MISMATCH: - raise OTAError( - "Error: Application MD5 code mismatch. Please try again " - "or flash over USB with a good quality cable." - ) - if dat == RESPONSE_ERROR_SIGNATURE_INVALID: - raise OTAError( - "Error: Firmware signature verification failed. The firmware was not signed " - "with the correct key. Ensure the signing key matches the one used to build " - "the firmware currently running on the device." - ) - if dat == RESPONSE_ERROR_UNKNOWN: - raise OTAError("Unknown error from ESP") + error_msg = _ERROR_MESSAGES.get(dat) + if error_msg is not None: + raise OTAError(f"Error: {error_msg}") + if expect is None: + return if not isinstance(expect, (list, tuple)): expect = [expect] if dat not in expect: @@ -232,8 +243,25 @@ def send_check( def perform_ota( - sock: socket.socket, password: str | None, file_handle: io.IOBase, filename: Path + sock: socket.socket, + password: str | None, + file_handle: io.IOBase, + filename: Path, + ota_type: int = OTA_TYPE_UPDATE_APP, ) -> None: + # Validate ota_type up front. It travels as a single byte on the wire, and + # passing an out-of-range value would only surface as a ValueError from + # bytes([ota_type]) deep inside send_check, bypassing OTAError handling. + if not isinstance(ota_type, int) or not 0 <= ota_type <= 0xFF: + raise OTAError( + f"Invalid ota_type {ota_type!r}; expected an integer in range 0-255" + ) + if ota_type not in _SUPPORTED_OTA_TYPES: + supported = ", ".join(f"0x{t:02X}" for t in sorted(_SUPPORTED_OTA_TYPES)) + raise OTAError( + f"Unsupported OTA type 0x{ota_type:02X}; this ESPHome supports: {supported}" + ) + file_contents = file_handle.read() file_size = len(file_contents) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) @@ -251,7 +279,11 @@ def perform_ota( ) # Features - send both compression and SHA256 auth support - features_to_send = FEATURE_SUPPORTS_COMPRESSION | FEATURE_SUPPORTS_SHA256_AUTH + features_to_send = ( + CLIENT_FEATURE_SUPPORTS_COMPRESSION + | CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + ) send_check(sock, features_to_send, "features") features = receive_exactly( sock, @@ -260,7 +292,36 @@ def perform_ota( None, # Accept any response )[0] - if features == RESPONSE_SUPPORTS_COMPRESSION: + extended_proto = False + if features == RESPONSE_FEATURE_FLAGS: + extended_proto = True + features = receive_exactly( + sock, + 1, + "feature flags", + None, # Accept any response + )[0] + elif features == RESPONSE_SUPPORTS_COMPRESSION: + features = SERVER_FEATURE_SUPPORTS_COMPRESSION + else: + features = 0 + + if ota_type != OTA_TYPE_UPDATE_APP: + # Any non-app OTA type requires the extended protocol and the + # partition-access server feature. Reject up front so the user gets + # a clear capability error instead of a post-auth 0x8E from the device. + if not extended_proto: + raise OTAError( + f"Device does not support extended OTA protocol; " + f"OTA type 0x{ota_type:02X} requires it" + ) + if not (features & SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS): + raise OTAError( + f"Device does not support partition access; " + f"OTA type 0x{ota_type:02X} cannot be used" + ) + + if features & SERVER_FEATURE_SUPPORTS_COMPRESSION: upload_contents = gzip.compress(file_contents, compresslevel=9) _LOGGER.info("Compressed to %s bytes", len(upload_contents)) else: @@ -315,6 +376,9 @@ def perform_ota( # Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures sock.settimeout(90.0) + if extended_proto: + send_check(sock, ota_type, "ota type") + upload_size = len(upload_contents) upload_size_encoded = [ (upload_size >> 24) & 0xFF, @@ -375,7 +439,11 @@ def perform_ota( def run_ota_impl_( - remote_host: str | list[str], remote_port: int, password: str | None, filename: Path + remote_host: str | list[str], + remote_port: int, + password: str | None, + filename: Path, + ota_type: int = OTA_TYPE_UPDATE_APP, ) -> tuple[int, str | None]: from esphome.core import CORE @@ -413,7 +481,7 @@ def run_ota_impl_( _LOGGER.info("Connected to %s", sa[0]) with open(filename, "rb") as file_handle: try: - perform_ota(sock, password, file_handle, filename) + perform_ota(sock, password, file_handle, filename, ota_type) except OTAError as err: _LOGGER.error(str(err)) return 1, None @@ -428,10 +496,14 @@ def run_ota_impl_( def run_ota( - remote_host: str | list[str], remote_port: int, password: str | None, filename: Path + remote_host: str | list[str], + remote_port: int, + password: str | None, + filename: Path, + ota_type: int = OTA_TYPE_UPDATE_APP, ) -> tuple[int, str | None]: try: - return run_ota_impl_(remote_host, remote_port, password, filename) + return run_ota_impl_(remote_host, remote_port, password, filename, ota_type) except OTAError as err: _LOGGER.error(err) return 1, None diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 20ba4b1f760..b114f17e6c4 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -185,6 +185,14 @@ def test_receive_exactly_socket_error(mock_socket: Mock) -> None: "Error: The OTA partition on the ESP couldn't be found", ), (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Error: Application MD5 code mismatch"), + ( + espota2.RESPONSE_ERROR_SIGNATURE_INVALID, + "Error: Firmware signature verification failed", + ), + ( + espota2.RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE, + "Error: The requested OTA type is not supported by the device", + ), (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), ], ) @@ -270,12 +278,13 @@ def test_perform_ota_successful_md5_auth( # Verify magic bytes were sent assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) - # Verify features were sent (compression + SHA256 support) + # Verify features were sent (compression + SHA256 support + extended protocol) assert mock_socket.sendall.call_args_list[1] == call( bytes( [ - espota2.FEATURE_SUPPORTS_COMPRESSION - | espota2.FEATURE_SUPPORTS_SHA256_AUTH + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL ] ) ) @@ -640,12 +649,13 @@ def test_perform_ota_successful_sha256_auth( # Verify magic bytes were sent assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) - # Verify features were sent (compression + SHA256 support) + # Verify features were sent (compression + SHA256 support + extended protocol) assert mock_socket.sendall.call_args_list[1] == call( bytes( [ - espota2.FEATURE_SUPPORTS_COMPRESSION - | espota2.FEATURE_SUPPORTS_SHA256_AUTH + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL ] ) ) @@ -699,8 +709,9 @@ def test_perform_ota_sha256_fallback_to_md5( assert mock_socket.sendall.call_args_list[1] == call( bytes( [ - espota2.FEATURE_SUPPORTS_COMPRESSION - | espota2.FEATURE_SUPPORTS_SHA256_AUTH + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL ] ) ) @@ -765,3 +776,220 @@ def test_perform_ota_version_differences( # For v2.0, verify more recv calls due to chunk acknowledgments assert mock_socket.recv.call_count == 9 # v2.0 has 9 recv calls (includes chunk OK) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_extended_protocol_app( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Test OTA extended protocol app update.""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_FEATURE_FLAGS]), # Device supports extended protocol + bytes( + [ + espota2.SERVER_FEATURE_SUPPORTS_COMPRESSION + | espota2.SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS + ] + ), # Device feature flags + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + espota2.OTA_TYPE_UPDATE_APP, + ) + + # Verify magic bytes were sent + assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) + + # Verify features were sent (compression + SHA256 support + extended protocol) + assert mock_socket.sendall.call_args_list[1] == call( + bytes( + [ + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + ] + ) + ) + + # Verify ota type was sent + assert mock_socket.sendall.call_args_list[2] == call( + bytes([espota2.OTA_TYPE_UPDATE_APP]) + ) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_device_rejects_with_unsupported_ota_type( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """End-to-end: device returns 0x8E after the size byte; perform_ota must + surface the human-readable 'unsupported OTA type' error from the lookup + table in check_error().""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_FEATURE_FLAGS]), # Extended protocol marker + bytes( + [ + espota2.SERVER_FEATURE_SUPPORTS_COMPRESSION + | espota2.SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS + ] + ), # Feature flags + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE]), # Reject at size step + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, + match="The requested OTA type is not supported by the device", + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + espota2.OTA_TYPE_UPDATE_APP, + ) + + # Verify the client did send the OTA type byte before the size step + assert mock_socket.sendall.call_args_list[2] == call( + bytes([espota2.OTA_TYPE_UPDATE_APP]) + ) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_unsupported_type_rejected_early( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """ota_type values not in _SUPPORTED_OTA_TYPES are rejected before any I/O.""" + with pytest.raises(espota2.OTAError, match="Unsupported OTA type 0xFF"): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + 0xFF, + ) + # No bytes should have been transmitted to the device. + mock_socket.sendall.assert_not_called() + + +@pytest.mark.parametrize("bad_type", [-1, 256, 0x10000, "app", None, 1.5]) +def test_perform_ota_rejects_out_of_range_type( + mock_socket: Mock, mock_file: io.BytesIO, bad_type: object +) -> None: + """Out-of-range or non-int ota_type must raise OTAError, not ValueError.""" + with pytest.raises(espota2.OTAError, match="Invalid ota_type"): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + bad_type, # type: ignore[arg-type] + ) + mock_socket.sendall.assert_not_called() + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_non_app_type_requires_extended_protocol( + mock_socket: Mock, mock_file: io.BytesIO, monkeypatch: pytest.MonkeyPatch +) -> None: + """Non-app OTA type must fail when device only supports the legacy protocol.""" + monkeypatch.setattr( + espota2, + "_SUPPORTED_OTA_TYPES", + frozenset({espota2.OTA_TYPE_UPDATE_APP, 0xFF}), + ) + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_HEADER_OK]), # Legacy single-byte feature ack + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, match="Device does not support extended OTA protocol" + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + 0xFF, + ) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_non_app_type_requires_partition_access( + mock_socket: Mock, mock_file: io.BytesIO, monkeypatch: pytest.MonkeyPatch +) -> None: + """Non-app OTA type must fail when device advertises extended protocol but + not the partition-access feature.""" + monkeypatch.setattr( + espota2, + "_SUPPORTED_OTA_TYPES", + frozenset({espota2.OTA_TYPE_UPDATE_APP, 0xFF}), + ) + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_FEATURE_FLAGS]), # Extended protocol marker + bytes( + [espota2.SERVER_FEATURE_SUPPORTS_COMPRESSION] + ), # Compression only, no partition access + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, match="Device does not support partition access" + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + 0xFF, + ) + + +def test_check_error_detects_errors_when_expect_is_none() -> None: + """check_error must surface device error bytes even when expect is None. + + Regression test: previously, receive_exactly(..., expect=None) calls (used + during feature negotiation and nonce reads) silently passed error bytes + through, turning clean device errors into confusing later failures. + """ + with pytest.raises(espota2.OTAError, match="Error: Authentication invalid"): + espota2.check_error([espota2.RESPONSE_ERROR_AUTH_INVALID], None) + + +def test_check_error_detects_empty_when_expect_is_none() -> None: + """Empty data with expect=None must still raise (connection closed).""" + with pytest.raises( + espota2.OTAError, match="Device closed connection without responding" + ): + espota2.check_error([], None) + + +def test_check_error_passes_non_error_when_expect_is_none() -> None: + """Non-error bytes with expect=None must pass through silently.""" + espota2.check_error([espota2.RESPONSE_OK], None) + espota2.check_error([espota2.RESPONSE_HEADER_OK], None) + espota2.check_error([espota2.RESPONSE_FEATURE_FLAGS], None) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index fb8f206a1d2..186d8a9573c 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -83,6 +83,7 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError +from esphome.espota2 import OTA_TYPE_UPDATE_APP from esphome.util import BootselResult from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -1593,7 +1594,7 @@ def test_upload_program_ota_success( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, "secret", expected_firmware + ["192.168.1.100"], 3232, "secret", expected_firmware, OTA_TYPE_UPDATE_APP ) @@ -1624,7 +1625,7 @@ def test_upload_program_ota_with_file_arg( assert exit_code == 0 assert host == "192.168.1.100" mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, Path("custom.bin") + ["192.168.1.100"], 3232, None, Path("custom.bin"), OTA_TYPE_UPDATE_APP ) @@ -1682,7 +1683,7 @@ def test_upload_program_ota_with_mqtt_resolution( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware + ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP ) @@ -1730,7 +1731,7 @@ def test_upload_program_ota_with_mqtt_empty_broker( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.50"], 3232, None, expected_firmware + ["192.168.1.50"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP ) # Verify warning was logged assert "MQTT IP discovery failed" in caplog.text @@ -3207,7 +3208,11 @@ def test_upload_program_ota_static_ip_with_mqttip( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100", "192.168.2.50"], 3232, None, expected_firmware + ["192.168.1.100", "192.168.2.50"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, ) @@ -3250,7 +3255,11 @@ def test_upload_program_ota_multiple_mqttip_resolves_once( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.2.50", "192.168.2.51", "192.168.1.100"], 3232, None, expected_firmware + ["192.168.2.50", "192.168.2.51", "192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, ) @@ -3415,7 +3424,7 @@ def test_upload_program_ota_mqtt_timeout_fallback( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware + ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP ) From 81d147ff9e55bb164821d516bb8ffbabca7fd7a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 1 May 2026 14:31:56 -0500 Subject: [PATCH 1808/2056] [esp32] Drop printf wrap on IDF 6.0+ (picolibc no longer needs it) (#16189) --- esphome/components/esp32/__init__.py | 12 +++- esphome/components/esp32/printf_stubs.cpp | 85 ++++++----------------- 2 files changed, 31 insertions(+), 66 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index bd299d71f05..9a9ee8fb083 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1753,7 +1753,17 @@ async def to_code(config): # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r # (~11 KB). See printf_stubs.cpp for implementation. - if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + # + # The wrap is only beneficial against newlib. Picolibc's tinystdio + # implements vsnprintf by building a string-output FILE and calling + # vfprintf, so vfprintf is unconditionally linked in by any caller + # of snprintf/vsnprintf — effectively every build — and the wrap + # saves nothing while costing ~170 B of shim. IDF 5.x defaults to + # newlib on every variant; IDF 6.0+ switches to picolibc on every + # variant. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or idf_version() >= cv.Version( + 6, 0, 0 + ): cg.add_define("USE_FULL_PRINTF") else: for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index 908b4023ead..489c5039429 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -1,91 +1,48 @@ /* - * Linker wrap stubs for FILE*-based printf functions. + * Linker wrap stubs for FILE*-based printf functions (newlib only). * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full - * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on - * picolibc's vfprintf). This is a separate implementation from the one - * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering - * and locking. + * fprintf(), printf(), vprintf(), and vfprintf(), which on newlib pull + * in _vfprintf_r (~11 KB) — a separate implementation from the one used + * by snprintf/vsnprintf that handles FILE* stream I/O with buffering. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() * and printf() calls in SDK components are only in debug/assert paths * (gpio_dump_io_configuration, ringbuf diagnostics) that are either * GC'd or never called. Crash backtraces and panic output are - * unaffected; they use esp_rom_printf() which is a ROM function - * and does not go through libc. + * unaffected; they use esp_rom_printf() which is a ROM function and + * does not go through libc. * - * On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we - * route output through a stack-allocated cookie FILE that forwards each - * byte to the real target stream via fputc(). Picolibc's tinystdio - * vfprintf walks the FILE::put callback one character at a time, so this - * costs ~32 bytes of stack for the cookie struct vs. a 512-byte format - * buffer. The buffered path overflows the loopTask stack on IDF 6. + * This wrap is newlib-only. On picolibc, vsnprintf is implemented as + * vfprintf into a string-output FILE, so vfprintf is unconditionally + * linked in by any caller of snprintf/vsnprintf and the wrap can never + * elide it — it just adds shim cost. Codegen forces USE_FULL_PRINTF + * on picolibc builds (IDF 6.0+ on all variants) so this file compiles + * to nothing there; the #error below catches a desynchronised gate. * - * On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite - * path because that loopTask stack budget has plenty of headroom for the - * 512-byte buffer; the picolibc-only crash above does not affect it. + * Saves ~11 KB of flash on newlib. * - * Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc. - * - * To disable these wraps, set enable_full_printf: true in the esp32 - * advanced config section. + * To disable this wrap on newlib, set enable_full_printf: true in the + * esp32 advanced config section. */ #if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) + +#ifdef __PICOLIBC__ +#error "printf wrap is net-negative on picolibc; codegen should set USE_FULL_PRINTF" +#endif + #include <cstdarg> #include <cstdio> -#ifndef __PICOLIBC__ #include "esp_system.h" -#endif namespace esphome::esp32 {} // NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) extern "C" { -#ifdef __PICOLIBC__ - -#include <cstddef> -#include <type_traits> - -extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap); - -namespace { - -struct CookieFile { - FILE base; - FILE *target; -}; - -// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is -// only well-defined when FILE is the first member at offset 0 and CookieFile -// is standard-layout. -static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile"); -static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout"); - -int cookie_put(char c, FILE *stream) { - auto *cookie = reinterpret_cast<CookieFile *>(stream); - return fputc(static_cast<unsigned char>(c), cookie->target); -} - -const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE); - -} // namespace - -int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { - CookieFile cookie; - cookie.base = COOKIE_FILE_TEMPLATE; - cookie.target = stream; - return __real_vfprintf(&cookie.base, fmt, ap); -} - -int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); } - -#else // !__PICOLIBC__ - static constexpr size_t PRINTF_BUFFER_SIZE = 512; // These stubs are essentially dead code at runtime — ESPHome replaces the @@ -117,8 +74,6 @@ int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); } -#endif // __PICOLIBC__ - int __wrap_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); From 5e9db1c8c686d86bb6165bed56efe3f76f028740 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 21:46:29 -0500 Subject: [PATCH 1809/2056] Bump github/codeql-action from 4.35.2 to 4.35.3 (#16201) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 246a8656934..5429434a7f3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: "/language:${{matrix.language}}" From 8046ff7e1e9a35094c2cb3e6fe96c1935bebb0ea Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sun, 3 May 2026 10:40:09 +0200 Subject: [PATCH 1810/2056] [nextion] TFT upload no longer fails when the display sends a split `0x08` ack (#16205) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../nextion/nextion_upload_arduino.cpp | 27 +++++++++++++++++++ .../nextion/nextion_upload_esp32.cpp | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 399f217a19f..41379c2345d 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -88,6 +88,33 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { this->write_array(buffer, buffer_size); App.feed_wdt(); this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); + + // Some Nextion firmware variants (notably bootloader/recovery mode on panels + // with no installed TFT) emit the 5-byte 0x08+position fast-mode ack with a + // multi-second gap between the leading 0x08 byte and the 4 trailing position + // bytes. recv_ret_string_ returns after the first byte; manually drain the + // trailing bytes from the UART before continuing. + if (!recv_string.empty() && recv_string[0] == 0x08 && recv_string.size() < 5) { + const uint32_t deadline = millis() + NEXTION_UPLOAD_ACK_TIMEOUT_MS; + while (recv_string.size() < 5 && millis() < deadline) { + if (this->available()) { + uint8_t b = 0; + if (this->read_byte(&b)) { + recv_string.push_back(static_cast<char>(b)); + } + } else { + delay(5); // NOLINT + App.feed_wdt(); + } + } + if (recv_string.size() < 5) { + ESP_LOGE(TAG, "Truncated 0x08 response: got %zu bytes within %" PRIu32 "ms", recv_string.size(), + NEXTION_UPLOAD_ACK_TIMEOUT_MS); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + } this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index db4558e2fe2..cd8feab84f6 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -104,6 +104,33 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r this->write_array(buffer, buffer_size); App.feed_wdt(); this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true); + + // Some Nextion firmware variants (notably bootloader/recovery mode on panels + // with no installed TFT) emit the 5-byte 0x08+position fast-mode ack with a + // multi-second gap between the leading 0x08 byte and the 4 trailing position + // bytes. recv_ret_string_ returns after the first byte; manually drain the + // trailing bytes from the UART before continuing. + if (!recv_string.empty() && recv_string[0] == 0x08 && recv_string.size() < 5) { + const uint32_t deadline = millis() + NEXTION_UPLOAD_ACK_TIMEOUT_MS; + while (recv_string.size() < 5 && millis() < deadline) { + if (this->available()) { + uint8_t b = 0; + if (this->read_byte(&b)) { + recv_string.push_back(static_cast<char>(b)); + } + } else { + vTaskDelay(pdMS_TO_TICKS(5)); // NOLINT + App.feed_wdt(); + } + } + if (recv_string.size() < 5) { + ESP_LOGE(TAG, "Truncated 0x08 response: got %zu bytes within %" PRIu32 "ms", recv_string.size(), + NEXTION_UPLOAD_ACK_TIMEOUT_MS); + allocator.deallocate(buffer, 4096); + buffer = nullptr; + return -1; + } + } this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; #ifdef USE_PSRAM From 0f174ee62620818c90cb73bab9e9de5f98e0f483 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 17:55:40 -0500 Subject: [PATCH 1811/2056] [api] Fall back to owning types for service array args used after a delay (#16140) --- esphome/components/api/__init__.py | 45 ++++++++++++++++++++------- tests/components/api/common-base.yaml | 18 +++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index ad778f20ad2..ca74483a2b3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -72,17 +72,35 @@ APIUnregisterServiceCallAction = api_ns.class_( UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") -SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { +# Owning element type for each YAML service variable type. Used to derive both +# the zero-copy native types and the owning fallback types below. +_SERVICE_ARG_SCALAR_TYPES: dict[str, MockObj] = { "bool": cg.bool_, "int": cg.int32, "float": cg.float_, + "string": cg.std_string, +} +SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { + # Scalars are passed by value; string uses a non-owning view into rx_buf_. + **_SERVICE_ARG_SCALAR_TYPES, "string": cg.StringRef, - "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), - "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), - "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), - "string[]": cg.FixedVector.template(cg.std_string) - .operator("const") - .operator("ref"), + # Arrays are passed as non-owning const references into rx_buf_. + **{ + f"{name}[]": cg.FixedVector.template(t).operator("const").operator("ref") + for name, t in _SERVICE_ARG_SCALAR_TYPES.items() + }, +} +# Owning fallback types used when the action chain contains non-synchronous actions +# (delay, wait_until, script.wait, etc.). The default non-owning types reference +# storage in the receive buffer, which is reused once the synchronous portion of +# the chain returns. FixedVector is also non-copyable, so the deferred lambda +# capture in DelayAction::play_complex would fail to compile. +SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = { + "string": cg.std_string, + **{ + f"{name}[]": cg.std_vector.template(t) + for name, t in _SERVICE_ARG_SCALAR_TYPES.items() + }, } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" @@ -381,17 +399,20 @@ async def to_code(config: ConfigType) -> None: func_args.append((cg.bool_, "return_response")) # Check if action chain has non-synchronous actions that would make - # non-owning StringRef dangle (rx_buf_ reused after delay) + # non-owning args (StringRef, const FixedVector&) dangle once the + # rx_buf_ is reused after a delay/wait_until/script.wait/etc. The + # FixedVector references would also fail to compile because they + # are non-copyable and DelayAction captures args by value. has_non_synchronous = automation.has_non_synchronous_actions( conf.get(CONF_THEN, []) ) service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - # Fall back to std::string for string args if non-synchronous actions exist - if has_non_synchronous and native is cg.StringRef: - native = cg.std_string + if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES: + native = SERVICE_ARG_FALLBACK_TYPES[var_] + else: + native = SERVICE_ARG_NATIVE_TYPES[var_] service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c766b61b13f..504c52a57b4 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -91,6 +91,24 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + # Test array + string args used after a non-synchronous action (delay). + # The default non-owning types (StringRef, const FixedVector&) would + # dangle once rx_buf_ is reused, and FixedVector is non-copyable so + # DelayAction's lambda capture would fail to compile. The api codegen + # must fall back to owning std::string / std::vector here. + - action: array_with_delay + variables: + name: string + int_arr: int[] + string_arr: string[] + then: + - delay: 20ms + - logger.log: + format: "Delayed: %s (%u ints, %u strings)" + args: + - name.c_str() + - int_arr.size() + - string_arr.size() # Test ContinuationAction (IfAction with then/else branches) - action: test_if_action variables: From 85e1e4b95ec6ec3d9c7ba3a5819d86d1ae1fb8a6 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 4 May 2026 02:37:32 +0200 Subject: [PATCH 1812/2056] [zephyr] feed watchdog early. Otherwise OTA may rollback. (#16218) --- esphome/components/zephyr/hal.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/zephyr/hal.cpp b/esphome/components/zephyr/hal.cpp index 6c405b650ec..ad8ed5c95c3 100644 --- a/esphome/components/zephyr/hal.cpp +++ b/esphome/components/zephyr/hal.cpp @@ -46,6 +46,8 @@ void arch_init() { } } #endif + // feed watchdog early. Otherwise OTA may rollback. + arch_feed_wdt(); } void arch_feed_wdt() { From 7cfab58a0558c8070aa9475f15758c4f443a612d Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Mon, 4 May 2026 00:42:10 +0000 Subject: [PATCH 1813/2056] [ota] Add partition table update functionality to ota component (#15780) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/__main__.py | 82 +++++ esphome/components/esphome/ota/__init__.py | 13 +- .../components/esphome/ota/ota_esphome.cpp | 44 ++- esphome/components/esphome/ota/ota_esphome.h | 4 + esphome/components/ota/ota_backend.h | 3 + .../ota/ota_backend_arduino_libretiny.cpp | 5 +- .../ota/ota_backend_arduino_libretiny.h | 2 +- .../ota/ota_backend_arduino_rp2040.cpp | 5 +- .../ota/ota_backend_arduino_rp2040.h | 2 +- .../components/ota/ota_backend_esp8266.cpp | 5 +- esphome/components/ota/ota_backend_esp8266.h | 2 +- .../components/ota/ota_backend_esp_idf.cpp | 56 ++- esphome/components/ota/ota_backend_esp_idf.h | 35 +- esphome/components/ota/ota_backend_host.cpp | 4 +- esphome/components/ota/ota_backend_host.h | 2 +- .../components/ota/ota_partitions_esp_idf.cpp | 327 ++++++++++++++++++ esphome/core/__init__.py | 11 + esphome/espota2.py | 17 +- .../ota/test-partition_access.esp32-idf.yaml | 5 + .../partition_tables/esp_idf_hello_world.bin | Bin 0 -> 3072 bytes .../esphome_dashboard_firmware.bin | Bin 0 -> 3072 bytes .../partition_tables/esphome_default.bin | Bin 0 -> 3072 bytes tests/unit_tests/test_espota2.py | 68 ++++ tests/unit_tests/test_main.py | 235 ++++++++++++- 24 files changed, 912 insertions(+), 15 deletions(-) create mode 100644 esphome/components/ota/ota_partitions_esp_idf.cpp create mode 100644 tests/components/ota/test-partition_access.esp32-idf.yaml create mode 100644 tests/unit_tests/fixtures/partition_tables/esp_idf_hello_world.bin create mode 100644 tests/unit_tests/fixtures/partition_tables/esphome_dashboard_firmware.bin create mode 100644 tests/unit_tests/fixtures/partition_tables/esphome_default.bin diff --git a/esphome/__main__.py b/esphome/__main__.py index e7ce36ae2d2..9ab2dee189a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1091,6 +1091,15 @@ def upload_program( port_type = get_port_type(host) + # MQTT and MQTTIP are also OTA paths; MQTTIP gets resolved to a real IP later by + # _resolve_network_devices(). Only SERIAL and BOOTSEL are non-OTA upload paths. + if port_type in (PortType.SERIAL, PortType.BOOTSEL) and getattr( + args, "partition_table", False + ): + raise EsphomeError( + "The option --partition-table can only be used for Over The Air updates." + ) + if port_type == PortType.BOOTSEL: exit_code = upload_using_picotool(config) # Return None for device - BOOTSEL can't be used for logging, @@ -1131,12 +1140,80 @@ def upload_program( binary = CORE.firmware_bin ota_type = espota2.OTA_TYPE_UPDATE_APP + if getattr(args, "partition_table", False): + # Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access. + # The device-side handshake also rejects this with "Device only supports app updates", + # but checking here surfaces the misconfiguration before opening a network connection. + if not ota_conf.get("allow_partition_access"): + raise EsphomeError( + "The option --partition-table requires 'allow_partition_access: true' on the " + "esphome OTA platform in the device's YAML configuration. Add it, recompile, " + "flash a build with the option enabled, and then retry --partition-table." + ) + binary = CORE.partition_table_bin + ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE if getattr(args, "file", None) is not None: binary = Path(args.file) + if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE: + _validate_partition_table_binary(binary) + return espota2.run_ota(network_devices, remote_port, password, binary, ota_type) +# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a +# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as +# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the +# trailing checksum entry. Padding past the last entry is 0xFF. The full table is +# exactly ESP_PARTITION_TABLE_MAX_LEN bytes. +_PARTITION_TABLE_MAX_LEN = 0xC00 +_ESP_PARTITION_MAGIC = 0x50AA +_ESP_PARTITION_MAGIC_MD5 = 0xEBEB + + +def _validate_partition_table_binary(binary: Path) -> None: + """Validate that ``binary`` looks like an ESP32 partition table image. + + Catches common mistakes (wrong file, truncated build output, swapped --file path) + before opening a network connection so the failure mode is a clear local error + instead of a post-handshake device rejection. + """ + try: + data = binary.read_bytes() + except OSError as err: + raise EsphomeError( + f"Cannot read partition table file '{binary}': {err}" + ) from err + + if len(data) != _PARTITION_TABLE_MAX_LEN: + raise EsphomeError( + f"Partition table file '{binary}' has wrong size: expected " + f"{_PARTITION_TABLE_MAX_LEN} bytes, got {len(data)}. " + "Pass the partition table image (e.g. partitions.bin / partition-table.bin), " + "not the firmware image." + ) + + first_magic = data[0] | (data[1] << 8) + if first_magic != _ESP_PARTITION_MAGIC: + raise EsphomeError( + f"Partition table file '{binary}' does not start with the expected " + f"partition magic 0x{_ESP_PARTITION_MAGIC:04X} (got 0x{first_magic:04X}). " + "This file does not look like an ESP32 partition table." + ) + + # The MD5 checksum entry is required: without it the device-side + # esp_partition_table_verify will accept the table but the bootloader will + # refuse to boot from it. Scan the 32-byte entries for the MD5 magic. + if not any( + (data[off] | (data[off + 1] << 8)) == _ESP_PARTITION_MAGIC_MD5 + for off in range(0, _PARTITION_TABLE_MAX_LEN, 32) + ): + raise EsphomeError( + f"Partition table file '{binary}' is missing the MD5 checksum entry. " + "Regenerate the partition table with gen_esp32part.py or rebuild the project." + ) + + def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: try: module = importlib.import_module("esphome.components." + CORE.target_platform) @@ -1804,6 +1881,11 @@ def parse_args(argv): "--file", help="Manually specify the binary file to upload.", ) + parser_upload.add_argument( + "--partition-table", + help="Upload as partition table (OTA).", + action="store_true", + ) parser_logs = subparsers.add_parser( "logs", diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index bfa5ffb55ce..ee3b7f0c20d 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -16,11 +16,13 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_VERSION, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority import esphome.final_validate as fv from esphome.types import ConfigType +CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" + _LOGGER = logging.getLogger(__name__) @@ -75,6 +77,10 @@ def ota_esphome_final_validate(config): merged_ota_esphome_configs_by_port[conf_port] = merge_config( merged_ota_esphome_configs_by_port[conf_port], ota_conf ) + if ota_conf.get(CONF_ALLOW_PARTITION_ACCESS) and not CORE.is_esp32: + raise cv.Invalid( + f"{CONF_ALLOW_PARTITION_ACCESS} is only supported on the esp32" + ) else: new_ota_conf.append(ota_conf) @@ -125,6 +131,7 @@ CONFIG_SCHEMA = cv.All( ln882x=8820, rtl87xx=8892, ): cv.port, + cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean, cv.Optional(CONF_PASSWORD): cv.string, cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" @@ -159,6 +166,10 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PASSWORD]: cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) + + if config.get(CONF_ALLOW_PARTITION_ACCESS): + cg.add_define("USE_OTA_PARTITIONS") + # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 955b4dc96fb..3ce3f2302db 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -87,6 +87,10 @@ void ESPHomeOTAComponent::setup() { // no wakes fire and loop() falls back to the self-disable safety net. esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd())); #endif + +#ifdef USE_OTA_PARTITIONS + ota::get_running_app_position(this->running_app_offset_, this->running_app_size_); +#endif } void ESPHomeOTAComponent::dump_config() { @@ -100,6 +104,29 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Password configured"); } #endif +#ifdef USE_OTA_PARTITIONS + ESP_LOGCONFIG(TAG, + " Partition access allowed\n" + " Running app:\n" + " Partition address: 0x%X\n" + " Used size: %zu bytes (0x%X)", + this->running_app_offset_, this->running_app_size_, this->running_app_size_); + +#ifdef USE_ESP32 + ESP_LOGCONFIG(TAG, + " Partition table:\n" + " %-12s %-4s %-8s %-10s %-10s", + "Name", "Type", "Subtype", "Address", "Size"); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it != NULL) { + const esp_partition_t *partition = esp_partition_get(it); + ESP_LOGCONFIG(TAG, " %-12s 0x%-2X 0x%-6X 0x%-8" PRIX32 " 0x%-8" PRIX32, partition->label, partition->type, + partition->subtype, partition->address, partition->size); + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); +#endif +#endif } void ESPHomeOTAComponent::loop() { @@ -118,6 +145,7 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; +static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02; void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -215,6 +243,9 @@ void ESPHomeOTAComponent::handle_handshake_() { static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack"); this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS; this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); +#ifdef USE_OTA_PARTITIONS + this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS; +#endif } else { this->handshake_buf_[0] = supports_compression ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION : ota::OTA_RESPONSE_HEADER_OK; @@ -347,10 +378,12 @@ void ESPHomeOTAComponent::handle_data_() { (static_cast<size_t>(buf[2]) << 8) | buf[3]; ESP_LOGV(TAG, "Size is %u bytes", ota_size); +#ifndef USE_OTA_PARTITIONS if (ota_type != ota::OTA_TYPE_UPDATE_APP) { error_code = ota::OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; goto error; // NOLINT(cppcoreguidelines-avoid-goto) } +#endif // Now that we've passed authentication and are actually // starting the update, set the warning status and notify @@ -362,8 +395,8 @@ void ESPHomeOTAComponent::handle_data_() { this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif - // This will block for a few seconds as it locks flash - error_code = this->backend_->begin(ota_size); + // begin() may block for a few seconds while it locks flash. + error_code = this->backend_->begin(ota_size, ota_type); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) update_started = true; @@ -465,6 +498,13 @@ void ESPHomeOTAComponent::handle_data_() { this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); #endif delay(100); // NOLINT +#ifdef USE_OTA_PARTITIONS + if (ota_type == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { + // Skip on_safe_shutdown: nvs_flash_deinit() has already invalidated open NVS handles, so + // preferences flush would emit ESP_ERR_NVS_INVALID_HANDLE for every entry. Reboot directly. + App.reboot(); + } +#endif App.safe_reboot(); error: diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 5043bc33ef8..0053ca69691 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -98,6 +98,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint32_t client_connect_time_{0}; static constexpr size_t HANDSHAKE_BUF_SIZE = 5; +#ifdef USE_OTA_PARTITIONS + uint32_t running_app_offset_{0}; + size_t running_app_size_{0}; +#endif uint16_t port_; uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE]; OTAState ota_state_{OTAState::IDLE}; diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 7e7b0f6523c..5888a8e12d4 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -42,6 +42,8 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D, OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E, + OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F, + OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; @@ -55,6 +57,7 @@ enum OTAState { enum OTAType : uint8_t { OTA_TYPE_UPDATE_APP = 0x00, + OTA_TYPE_UPDATE_PARTITION_TABLE = 0x01, }; /** Listener interface for OTA state changes. diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index dcd71e92dda..4cc99202a77 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -13,7 +13,10 @@ static const char *const TAG = "ota.arduino_libretiny"; std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); } -OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { +OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA // where the exact firmware size is unknown due to multipart encoding if (image_size == 0) { diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 3d426e67599..c2716a44d1c 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -8,7 +8,7 @@ namespace esphome::ota { class ArduinoLibreTinyOTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index bc8ef812e6e..0ca0602519d 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -15,7 +15,10 @@ static const char *const TAG = "ota.arduino_rp2040"; std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend() { return make_unique<ArduinoRP2040OTABackend>(); } -OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { +OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // OTA size of 0 is not currently handled, but // web_server is not supported for RP2040, so this is not an issue. bool ret = Update.begin(image_size, U_FLASH); diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 05bd2f5cc4e..d04d5c1a844 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -10,7 +10,7 @@ namespace esphome::ota { class ArduinoRP2040OTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp index 7c9d3925321..6a678fb4197 100644 --- a/esphome/components/ota/ota_backend_esp8266.cpp +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -50,7 +50,10 @@ static const char *const TAG = "ota.esp8266"; std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); } -OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { +OTAResponseTypes ESP8266OTABackend::begin(size_t image_size, OTAType ota_type) { + if (ota_type != OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space if (image_size == 0) { // Round down to sector boundary: subtract one sector, then mask to sector alignment diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h index b364e216a36..21b5c12c2d6 100644 --- a/esphome/components/ota/ota_backend_esp8266.h +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -14,7 +14,7 @@ namespace esphome::ota { /// by not having a global Update object in .bss. class ESP8266OTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index b4b38a192f0..42d106bf1fb 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -16,7 +16,30 @@ static const char *const TAG = "ota.idf"; std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); } -OTAResponseTypes IDFOTABackend::begin(size_t image_size) { +OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) { +#ifdef USE_OTA_PARTITIONS + this->ota_type_ = ota_type; + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { + // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN: under- leaves stale bytes from the + // previous table; over- can't fit the reserved region. + if (image_size != ESP_PARTITION_TABLE_MAX_LEN) { + ESP_LOGE(TAG, "Wrong partition table size: expected %u bytes, got %zu", ESP_PARTITION_TABLE_MAX_LEN, image_size); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + memset(this->buf_, 0xFF, sizeof this->buf_); + this->buf_written_ = 0; + this->image_size_ = image_size; + this->md5_.init(); + return OTA_RESPONSE_OK; + } + if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } +#else + if (ota_type != ota::OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } +#endif #ifdef USE_OTA_ROLLBACK // If we're starting an OTA, the current boot is good enough - mark it valid // to prevent rollback and allow the OTA to proceed even if the safe mode @@ -52,6 +75,21 @@ void IDFOTABackend::set_update_md5(const char *expected_md5) { } OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { +#ifdef USE_OTA_PARTITIONS + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { + if (len > PARTITION_TABLE_BUFFER_SIZE - this->buf_written_) { + ESP_LOGE(TAG, "Wrong partition table size"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + memcpy(this->buf_ + this->buf_written_, data, len); + this->buf_written_ += len; + this->md5_.add(data, len); + return OTA_RESPONSE_OK; + } + if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } +#endif esp_err_t err = esp_ota_write(this->update_handle_, data, len); this->md5_.add(data, len); if (err != ESP_OK) { @@ -73,6 +111,14 @@ OTAResponseTypes IDFOTABackend::end() { return OTA_RESPONSE_ERROR_MD5_MISMATCH; } } +#ifdef USE_OTA_PARTITIONS + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { + return this->update_partition_table(); + } + if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + } +#endif esp_err_t err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; if (err == ESP_OK) { @@ -96,6 +142,14 @@ OTAResponseTypes IDFOTABackend::end() { } void IDFOTABackend::abort() { +#ifdef USE_OTA_PARTITIONS + if (this->partition_table_part_ != nullptr) { + esp_partition_deregister_external(this->partition_table_part_); + this->partition_table_part_ = nullptr; + } +#endif + // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether + // or not an update is in flight. esp_ota_abort(this->update_handle_); this->update_handle_ = 0; } diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index d007bcd1281..54fdd24f934 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -9,21 +9,54 @@ namespace esphome::ota { +#ifdef USE_OTA_PARTITIONS +// Staging buffer holds the entire partition table for verification before any flash op. +static constexpr size_t PARTITION_TABLE_BUFFER_SIZE = ESP_PARTITION_TABLE_MAX_LEN; // 0xC00 + +void get_running_app_position(uint32_t &offset, size_t &size); +#endif + class IDFOTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); void abort(); bool supports_compression() { return false; } + protected: +#ifdef USE_OTA_PARTITIONS + // copy_dest_part non-null means the running app must be copied INTO this slot of the current + // table before the new partition table is committed. The destination is in the current table + // because that's where esp_partition_copy can write; once the new table replaces it, the same + // flash region becomes target_app_index in the new table. + struct PartitionTablePlan { + int target_app_index{-1}; + const esp_partition_t *copy_dest_part{nullptr}; + }; + + OTAResponseTypes validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size, + PartitionTablePlan &plan); + OTAResponseTypes update_partition_table(); +#endif + private: esp_ota_handle_t update_handle_{0}; const esp_partition_t *partition_; md5::MD5Digest md5_{}; char expected_bin_md5_[32]; bool md5_set_{false}; +#ifdef USE_OTA_PARTITIONS + // Buffer first so it packs tightly after the preceding `bool md5_set_` with no alignment + // padding. Only resident during an active OTA: the backend is constructed per connection and + // destroyed on cleanup_connection_(). + uint8_t buf_[PARTITION_TABLE_BUFFER_SIZE]; + size_t buf_written_{0}; + size_t image_size_{0}; + const esp_partition_t *partition_table_part_{nullptr}; + ota::OTAType ota_type_{ota::OTA_TYPE_UPDATE_APP}; +#endif }; std::unique_ptr<IDFOTABackend> make_ota_backend(); diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp index 2e2132418d8..a2c9f2cc33a 100644 --- a/esphome/components/ota/ota_backend_host.cpp +++ b/esphome/components/ota/ota_backend_host.cpp @@ -10,7 +10,9 @@ namespace esphome::ota { std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); } -OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; } +OTAResponseTypes HostOTABackend::begin(size_t image_size, OTAType ota_type) { + return OTA_RESPONSE_ERROR_UPDATE_PREPARE; +} void HostOTABackend::set_update_md5(const char *expected_md5) {} diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index 300facf72f9..4451fdfe182 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -9,7 +9,7 @@ namespace esphome::ota { /// OTA triggers to compile for host platform during development. class HostOTABackend final { public: - OTAResponseTypes begin(size_t image_size); + OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); void set_update_md5(const char *md5); OTAResponseTypes write(uint8_t *data, size_t len); OTAResponseTypes end(); diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp new file mode 100644 index 00000000000..2a2ed577f1e --- /dev/null +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -0,0 +1,327 @@ +#ifdef USE_ESP32 +#include "ota_backend_esp_idf.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OTA_PARTITIONS +#include "esphome/components/watchdog/watchdog.h" +#include "esphome/core/log.h" + +#include <esp_image_format.h> +#include <esp_ota_ops.h> +#include <nvs_flash.h> + +#include <cstring> + +namespace esphome::ota { + +static const char *const TAG = "ota.idf"; + +static inline bool check_overlap(uint32_t a_offset, size_t a_size, uint32_t b_offset, size_t b_size) { + return (a_offset + a_size > b_offset && b_offset + b_size > a_offset); +} + +// Wraps esp_partition_find/_get/_next/_release. Returns nullptr if no APP partition at `address` +// is at least `min_size` bytes. +static const esp_partition_t *find_app_partition_at(uint32_t address, size_t min_size) { + const esp_partition_t *found = nullptr; + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + while (it != nullptr) { + const esp_partition_t *p = esp_partition_get(it); + if (p->address == address && p->size >= min_size) { + found = p; + break; + } + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); + return found; +} + +// Validates the staged partition table and picks the post-update boot slot. All non-destructive +// checks live here; the destructive write is in update_partition_table(). +// Side effect: registers the live partition-table region as partition_table_part_ so the caller +// can write to it; abort() releases it on error. +OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size, + PartitionTablePlan &plan) { + esp_err_t err = esp_partition_register_external( + nullptr, ESP_PRIMARY_PARTITION_TABLE_OFFSET, ESP_PARTITION_TABLE_SIZE, "PrimaryPrtTable", + ESP_PARTITION_TYPE_PARTITION_TABLE, ESP_PARTITION_SUBTYPE_PARTITION_TABLE_PRIMARY, &this->partition_table_part_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_register_external failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + int num_partitions = 0; + const esp_partition_info_t *existing_partition_table = nullptr; + esp_partition_mmap_handle_t partition_table_map; + err = esp_partition_mmap(this->partition_table_part_, 0, ESP_PARTITION_TABLE_MAX_LEN, ESP_PARTITION_MMAP_DATA, + reinterpret_cast<const void **>(&existing_partition_table), &partition_table_map); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_mmap failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + err = esp_partition_table_verify(existing_partition_table, true, &num_partitions); + esp_partition_munmap(partition_table_map); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_table_verify failed (existing partition table) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + const esp_partition_info_t *new_partition_table = reinterpret_cast<const esp_partition_info_t *>(this->buf_); + err = esp_partition_table_verify(new_partition_table, true, &num_partitions); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_table_verify failed (new partition table) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + // esp_partition_table_verify does not catch a missing MD5 entry, but the bootloader refuses + // to boot from a table without one. + bool checksum_found = false; + for (size_t i = 0; i < ESP_PARTITION_TABLE_MAX_ENTRIES; i++) { + if (new_partition_table[i].magic == ESP_PARTITION_MAGIC_MD5) { + checksum_found = true; + break; + } + } + if (!checksum_found) { + ESP_LOGE(TAG, "New partition table has no checksum"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + // Slot-selection policy when multiple slots can host the running app: pick the FIRST eligible + // slot in table order, preferring the no-copy path (matching offset) over the copy path. + // Deterministic and table-ordering-stable. + int app_partitions_found = 0; + int new_app_part_index = -1; + int new_app_part_index_with_copy = -1; + const esp_partition_t *app_copy_dest_part = nullptr; + bool otadata_partition_found = false; + bool otadata_overlap = false; + bool nvs_partition_found = false; + for (int i = 0; i < num_partitions; i++) { + const esp_partition_info_t *new_part = &new_partition_table[i]; + if (new_part->type == ESP_PARTITION_TYPE_APP) { + app_partitions_found++; + if (new_part->pos.size >= running_app_size) { + if (new_part->pos.offset == running_app_offset) { + if (new_app_part_index == -1) { + new_app_part_index = i; + } + } else if (new_app_part_index_with_copy == -1 && + !check_overlap(running_app_offset, running_app_size, new_part->pos.offset, running_app_size)) { + // esp_partition_copy writes into a registered partition; need one at this offset in the + // current table. + const esp_partition_t *p = find_app_partition_at(new_part->pos.offset, running_app_size); + if (p != nullptr) { + new_app_part_index_with_copy = i; + app_copy_dest_part = p; + } + } + } + } else if (new_part->type == ESP_PARTITION_TYPE_DATA) { + if (new_part->subtype == ESP_PARTITION_SUBTYPE_DATA_OTA) { + otadata_partition_found = true; + otadata_overlap = check_overlap(running_app_offset, running_app_size, new_part->pos.offset, new_part->pos.size); + } else if (new_part->subtype == ESP_PARTITION_SUBTYPE_DATA_NVS && + strncmp(reinterpret_cast<const char *>(new_part->label), "nvs", sizeof(new_part->label)) == 0) { + nvs_partition_found = true; + } + } + } + + if (new_app_part_index == -1 && new_app_part_index_with_copy == -1) { + // Most likely cause: the user picked the wrong migration .bin for their running app's size. + // Rejecting here is non-destructive (no flash op has run yet); the user can safely retry with + // a different .bin. Log enough info that they can pick the right method without guessing. + ESP_LOGE(TAG, + "Running app at 0x%X (%u bytes used) does not fit any compatible slot in the new " + "partition table. Pick a migration method whose size limit is at least %u bytes and " + "retry; no flash content was modified.", + running_app_offset, running_app_size, running_app_size); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + if (app_partitions_found < 2) { + ESP_LOGE(TAG, "New partition table needs at least 2 app partitions, found %d", app_partitions_found); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + if (!otadata_partition_found) { + ESP_LOGE(TAG, "New partition table is missing the required otadata partition"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + if (!nvs_partition_found) { + ESP_LOGE(TAG, "New partition table is missing the required nvs partition"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + if (otadata_overlap) { + ESP_LOGE(TAG, + "New otadata partition overlaps with the running app at 0x%X (size %u). The chosen " + "partition table is not compatible with this device's current flash layout; pick a " + "different migration method.", + running_app_offset, running_app_size); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + if (new_app_part_index != -1) { + plan.target_app_index = new_app_part_index; + plan.copy_dest_part = nullptr; + } else { + plan.target_app_index = new_app_part_index_with_copy; + plan.copy_dest_part = app_copy_dest_part; + } + return OTA_RESPONSE_OK; +} + +OTAResponseTypes IDFOTABackend::update_partition_table() { + if (this->buf_written_ == 0 || this->image_size_ != this->buf_written_) { + ESP_LOGE(TAG, "Not enough data received"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + // Without a valid running-app size we cannot compute overlap or copy bounds. zero indicates + // esp_ota_get_running_partition() failed (e.g. cache unloaded by a previous aborted OTA). + uint32_t running_app_offset; + size_t running_app_size; + get_running_app_position(running_app_offset, running_app_size); + if (running_app_size == 0) { + ESP_LOGE(TAG, "Failed to determine running app position"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + PartitionTablePlan plan; + OTAResponseTypes validate_result = this->validate_new_partition_table_(running_app_offset, running_app_size, plan); + if (validate_result != OTA_RESPONSE_OK) { + return validate_result; + } + + // ERROR severity so the warning shows up in default log filters; any failure past this point + // can leave the device unbootable until it is recovered with a serial flash. + ESP_LOGE(TAG, "Starting partition table update.\n" + " DO NOT REMOVE POWER until the device reboots successfully.\n" + " Loss of power during this operation may render the device unable to boot until\n" + " it is recovered via a serial flash."); + + // One guard over the whole critical section in case an IDF call takes longer than expected on + // some chip variant. + watchdog::WatchdogManager watchdog(15000); + + esp_err_t err; + const esp_partition_info_t *new_partition_table = reinterpret_cast<const esp_partition_info_t *>(this->buf_); + + if (plan.copy_dest_part != nullptr) { + // Resolve the source via running_app_offset rather than esp_ota_get_running_partition() in + // case a prior aborted partition-table OTA called esp_partition_unload_all() in this boot, + // which leaves esp_ota_get_running_partition() returning nullptr. + const esp_partition_t *running_app_part = find_app_partition_at(running_app_offset, running_app_size); + if (running_app_part == nullptr) { + ESP_LOGE(TAG, "Cannot resolve running app partition at offset 0x%X", running_app_offset); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, + plan.copy_dest_part->address, running_app_size); + err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_copy failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + } + + // Deinit NVS only just before the first destructive write so verify/copy failure paths return + // with NVS still functional. From this point on, components that hold open NVS handles + // (e.g. preferences) will fail with ESP_ERR_NVS_INVALID_HANDLE on success or failure; + // nvs_flash_init() can re-init the subsystem but cannot revive existing handles. On the + // success path the device reboots immediately afterwards so this doesn't matter; on the + // failure path the user must reboot the device before retrying. + nvs_flash_deinit(); + + // Update the partition table + err = esp_ota_begin(this->partition_table_part_, ESP_PARTITION_TABLE_MAX_LEN, &this->update_handle_); + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + err = esp_ota_write(this->update_handle_, this->buf_, ESP_PARTITION_TABLE_MAX_LEN); + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + err = esp_ota_end(this->update_handle_); + this->update_handle_ = 0; // esp_ota_end releases the handle internally + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + // unload first, then null the member pointer; if abort() ran between the two steps it would + // see a freed pointer. esp_partition_unload_all() invalidates partition_table_part_ too, so + // an explicit deregister would be redundant. + esp_partition_unload_all(); + this->partition_table_part_ = nullptr; + + // Write otadata to set the new boot partition + const esp_partition_info_t *new_part = &new_partition_table[plan.target_app_index]; + const esp_partition_t *new_boot_partition = find_app_partition_at(new_part->pos.offset, 0); + if (new_boot_partition == nullptr) { + ESP_LOGE(TAG, "Selected app partition not found after partition table update"); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address); + err = esp_ota_set_boot_partition(new_boot_partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; + } + return OTA_RESPONSE_OK; +} + +// Process-scoped cache. Cannot be a backend member: backends are per-connection but the cache +// must outlive a connection that called esp_partition_unload_all(), after which +// esp_ota_get_running_partition() no longer returns valid data. +static bool s_running_app_initialized = false; +static uint32_t s_running_app_cached_offset = 0; +static size_t s_running_app_cached_size = 0; + +// Flag-gated rather than size==0 so a failed first call doesn't poison the cache. +void get_running_app_position(uint32_t &offset, size_t &size) { + if (!s_running_app_initialized) { + const esp_partition_t *running_app_part = esp_ota_get_running_partition(); + if (running_app_part == nullptr || running_app_part->erase_size == 0) { + // Surface zeros without committing to the cache so a later call has a chance to succeed. + offset = 0; + size = 0; + return; + } + + uint32_t pending_offset = running_app_part->address; + size_t pending_size = running_app_part->size; + + const esp_partition_pos_t running_app_pos = { + .offset = running_app_part->address, + .size = running_app_part->size, + }; + esp_image_metadata_t image_metadata = {}; + image_metadata.start_addr = running_app_part->address; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &running_app_pos, &image_metadata) == ESP_OK && + image_metadata.image_len < running_app_part->size) { + pending_size = image_metadata.image_len; + } + // Round up to a full flash sector so the copy spans complete erase blocks. + pending_size = ((pending_size + running_app_part->erase_size - 1) / running_app_part->erase_size) * + running_app_part->erase_size; + + s_running_app_cached_offset = pending_offset; + s_running_app_cached_size = pending_size; + s_running_app_initialized = true; + } + + offset = s_running_app_cached_offset; + size = s_running_app_cached_size; +} + +} // namespace esphome::ota + +#endif // USE_OTA_PARTITIONS +#endif // USE_ESP32 diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 4fecebcd8d2..94a48dd31b0 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -779,6 +779,17 @@ class EsphomeCore: return self.relative_pioenvs_path(self.name, "firmware.uf2") return self.relative_pioenvs_path(self.name, "firmware.bin") + @property + def partition_table_bin(self) -> Path: + # Native ESP-IDF (--native-idf): the partition table image is emitted under + # build/partition_table/partition-table.bin alongside firmware.bin. PlatformIO writes the + # equivalent file as partitions.bin in the env-specific .pioenvs directory. + if self.data.get(KEY_NATIVE_IDF): + return self.relative_build_path( + "build", "partition_table", "partition-table.bin" + ) + return self.relative_pioenvs_path(self.name, "partitions.bin") + @property def target_platform(self): return self.data[KEY_CORE][KEY_TARGET_PLATFORM] diff --git a/esphome/espota2.py b/esphome/espota2.py index f4c0c73589a..a45a6ef2343 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -16,6 +16,7 @@ from esphome.core import EsphomeError from esphome.helpers import ProgressBar, resolve_ip_address OTA_TYPE_UPDATE_APP = 0x00 +OTA_TYPE_UPDATE_PARTITION_TABLE = 0x01 RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -46,6 +47,8 @@ RESPONSE_ERROR_MD5_MISMATCH = 0x8B RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E +RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F +RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90 RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -62,7 +65,9 @@ SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02 # OTA types this client knows how to send. Future PRs that add bootloader/partition # updates extend this set. Anything outside the set is rejected up front so callers # of perform_ota/run_ota get a clear error instead of a post-auth 0x8E from the device. -_SUPPORTED_OTA_TYPES: frozenset[int] = frozenset({OTA_TYPE_UPDATE_APP}) +_SUPPORTED_OTA_TYPES: frozenset[int] = frozenset( + {OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE} +) UPLOAD_BLOCK_SIZE = 8192 UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 @@ -128,6 +133,16 @@ _ERROR_MESSAGES: dict[int, str] = { RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE: ( "The requested OTA type is not supported by the device." ), + RESPONSE_ERROR_PARTITION_TABLE_VERIFY: ( + "The partition table update could not be verified. No changes were " + "made to the flash content. Check the logs for more information and retry." + ), + RESPONSE_ERROR_PARTITION_TABLE_UPDATE: ( + "An error occurred while updating the partition table. The device is now " + "in a degraded state (NVS handles are invalid; many components will fail) " + "and may not be able to boot. Check the logs, reboot the device, and " + "retry the update. If the device fails to boot, recover it via a serial flash." + ), RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", } diff --git a/tests/components/ota/test-partition_access.esp32-idf.yaml b/tests/components/ota/test-partition_access.esp32-idf.yaml new file mode 100644 index 00000000000..0cbf8549520 --- /dev/null +++ b/tests/components/ota/test-partition_access.esp32-idf.yaml @@ -0,0 +1,5 @@ +ota: + - platform: esphome + allow_partition_access: true + +<<: !include common.yaml diff --git a/tests/unit_tests/fixtures/partition_tables/esp_idf_hello_world.bin b/tests/unit_tests/fixtures/partition_tables/esp_idf_hello_world.bin new file mode 100644 index 0000000000000000000000000000000000000000..b8fa03b4b3536b1f4d0def4c1fed550e8fc2acc7 GIT binary patch literal 3072 zcmZ1#z{tcffq{V`fq@~fte62EtO{UcWca|qz#zcDP>@j>pP83gf~;m$0Eov3R*;sM zT#{c@2@-(g*RTJhfG=zPT`j`AV@pi8>6C7ps)8ap${7uT(GVC7fzc2c4S~@R7!85Z O5Eu=C(GZ|%2mk;_=9q5) literal 0 HcmV?d00001 diff --git a/tests/unit_tests/fixtures/partition_tables/esphome_dashboard_firmware.bin b/tests/unit_tests/fixtures/partition_tables/esphome_dashboard_firmware.bin new file mode 100644 index 0000000000000000000000000000000000000000..e648fa32709414410fbd9be592c84659b7ebb3ad GIT binary patch literal 3072 zcmZ1#z{tcffq{V`fPo>ete62EtO{UcV0gg5z@WgukYAFRl30?6qGVM7g8%~qBLf42 zBtv3BfdPsn0|UdV00u#@W{A8Yraa?J1_nz8kSVFD1x5L}s47+kFg7s=STZntU|=XN z$V^K^bK>jQ|51SEf~h|;7*|G0zA!t#)$$w%isUF~Gz3ONU^E0qLtr!nMnhmU1V%$( KGz3O?2mk;I)<f3- literal 0 HcmV?d00001 diff --git a/tests/unit_tests/fixtures/partition_tables/esphome_default.bin b/tests/unit_tests/fixtures/partition_tables/esphome_default.bin new file mode 100644 index 0000000000000000000000000000000000000000..d39bf337c9a1bb267f1cb2e4ae9ead2de0b49ef7 GIT binary patch literal 3072 zcmZ1#z{tQbfq{WRfq@~vBrzqiB#{9btO{UcWZ1yKz#zcDP>@j>pP83gf+`P^VPs%n zkYPwHC@?^l1F=^HFbFa*$in0eL1M^wRRALs1A`?40|PrlURg1+6qx<`^?wv_Dy30A wx{o#F{@PPlP82`;j3PP884ZEa5Eu=C(GVC7fzc2c4S~@R7!85Z5WpJ(0C1>a2LJ#7 literal 0 HcmV?d00001 diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index b114f17e6c4..2cad1d2ec81 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -193,6 +193,14 @@ def test_receive_exactly_socket_error(mock_socket: Mock) -> None: espota2.RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE, "Error: The requested OTA type is not supported by the device", ), + ( + espota2.RESPONSE_ERROR_PARTITION_TABLE_VERIFY, + "Error: The partition table update could not be verified", + ), + ( + espota2.RESPONSE_ERROR_PARTITION_TABLE_UPDATE, + "Error: An error occurred while updating the partition table", + ), (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), ], ) @@ -831,6 +839,66 @@ def test_perform_ota_extended_protocol_app( ) +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_successful_partition_table( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Test OTA partition table update. + + The mocked server advertises both COMPRESSION and PARTITION_ACCESS to exercise + the full extended-protocol negotiation path. Real IDFOTABackend devices return + ``supports_compression() == false`` and never set the COMPRESSION flag for a + partition-table OTA; the flag here is intentional protocol-coverage, not a + description of on-device behaviour. + """ + recv_responses = [ + bytes([espota2.RESPONSE_OK]), # First byte of version response + bytes([espota2.OTA_VERSION_2_0]), # Version number + bytes([espota2.RESPONSE_FEATURE_FLAGS]), # Device supports extended protocol + bytes( + [ + espota2.SERVER_FEATURE_SUPPORTS_COMPRESSION + | espota2.SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS + ] + ), # Device feature flags (compression flag is unrealistic; see docstring) + bytes([espota2.RESPONSE_AUTH_OK]), # No auth required + bytes([espota2.RESPONSE_UPDATE_PREPARE_OK]), # Binary size OK + bytes([espota2.RESPONSE_BIN_MD5_OK]), # MD5 checksum OK + bytes([espota2.RESPONSE_CHUNK_OK]), # Chunk OK + bytes([espota2.RESPONSE_RECEIVE_OK]), # Receive OK + bytes([espota2.RESPONSE_UPDATE_END_OK]), # Update end OK + ] + + mock_socket.recv.side_effect = recv_responses + + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "partitions.bin", + espota2.OTA_TYPE_UPDATE_PARTITION_TABLE, + ) + + # Verify magic bytes were sent + assert mock_socket.sendall.call_args_list[0] == call(bytes(espota2.MAGIC_BYTES)) + + # Verify features were sent (compression + SHA256 support + extended protocol) + assert mock_socket.sendall.call_args_list[1] == call( + bytes( + [ + espota2.CLIENT_FEATURE_SUPPORTS_COMPRESSION + | espota2.CLIENT_FEATURE_SUPPORTS_SHA256_AUTH + | espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + ] + ) + ) + + # Verify ota type was sent + assert mock_socket.sendall.call_args_list[2] == call( + bytes([espota2.OTA_TYPE_UPDATE_PARTITION_TABLE]) + ) + + @pytest.mark.usefixtures("mock_time") def test_perform_ota_device_rejects_with_unsupported_ota_type( mock_socket: Mock, mock_file: io.BytesIO diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 186d8a9573c..798a43a4cea 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -24,6 +24,7 @@ from esphome.__main__ import ( _get_configured_xtal_freq, _make_crystal_freq_callback, _resolve_network_devices, + _validate_partition_table_binary, choose_upload_log_host, command_analyze_memory, command_bundle, @@ -83,7 +84,7 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError -from esphome.espota2 import OTA_TYPE_UPDATE_APP +from esphome.espota2 import OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE from esphome.util import BootselResult from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -1112,6 +1113,7 @@ class MockArgs: reset: bool = False list_only: bool = False output: str | None = None + partition_table: bool = False def test_upload_program_serial_esp32( @@ -1629,6 +1631,237 @@ def test_upload_program_ota_with_file_arg( ) +_PARTITION_TABLE_LEN = 0xC00 + + +def _make_partition_table_bytes() -> bytes: + """Build a minimal partition table image accepted by _validate_partition_table_binary.""" + table = bytearray(b"\xff" * _PARTITION_TABLE_LEN) + # First entry: ESP_PARTITION_MAGIC (0x50AA) little-endian -> bytes 0xAA, 0x50. + table[0] = 0xAA + table[1] = 0x50 + # MD5 checksum entry at offset 32: ESP_PARTITION_MAGIC_MD5 (0xEBEB) little-endian. + table[32] = 0xEB + table[33] = 0xEB + return bytes(table) + + +def test_upload_program_ota_partition_table_with_file_arg( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with OTA and partition table.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + partition_file = tmp_path / "partitions.bin" + partition_file.write_bytes(_make_partition_table_bytes()) + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file=str(partition_file), partition_table=True) + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], + 3232, + None, + partition_file, + OTA_TYPE_UPDATE_PARTITION_TABLE, + ) + + +def test_upload_program_serial_partition_table( + mock_upload_using_esptool: Mock, + mock_get_port_type: Mock, +) -> None: + """Test serial upload with partition table option (unsupported).""" + setup_core(platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "SERIAL" + mock_upload_using_esptool.return_value = 0 + + config = {} + args = MockArgs(partition_table=True) + devices = ["/dev/ttyUSB0"] + + with pytest.raises( + EsphomeError, + match="The option --partition-table can only be used for Over The Air updates", + ): + upload_program(config, args, devices) + + +def test_upload_program_ota_partition_table_mqttip( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--partition-table is allowed for MQTTIP devices; they resolve to a real IP at OTA time.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "MQTTIP" + mock_run_ota.return_value = (0, "192.168.1.100") + + partition_file = tmp_path / "partitions.bin" + partition_file.write_bytes(_make_partition_table_bytes()) + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file=str(partition_file), partition_table=True) + + with patch( + "esphome.__main__._resolve_network_devices", return_value=["192.168.1.100"] + ): + exit_code, host = upload_program(config, args, ["MQTTIP"]) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], + 3232, + None, + partition_file, + OTA_TYPE_UPDATE_PARTITION_TABLE, + ) + + +def test_validate_partition_table_binary_accepts_valid(tmp_path: Path) -> None: + f = tmp_path / "partitions.bin" + f.write_bytes(_make_partition_table_bytes()) + _validate_partition_table_binary(f) + + +_PARTITION_FIXTURE_DIR = Path(__file__).parent / "fixtures" / "partition_tables" + + +@pytest.mark.parametrize( + "fixture", + [ + # Stock ESP-IDF gen_esp32part.py output for an ESPHome build. + "esphome_default.bin", + # ESP-IDF Hello-world example partition table (vendored from espressif/esp-serial-flasher). + "esp_idf_hello_world.bin", + # Partition table shipped with esphome_dashboard's prebuilt firmware. + "esphome_dashboard_firmware.bin", + ], +) +def test_validate_partition_table_binary_accepts_real_binaries(fixture: str) -> None: + """Real-world partition-table binaries from ESP-IDF / ESPHome tooling pass validation.""" + _validate_partition_table_binary(_PARTITION_FIXTURE_DIR / fixture) + + +def test_validate_partition_table_binary_rejects_wrong_size(tmp_path: Path) -> None: + f = tmp_path / "partitions.bin" + f.write_bytes(b"\xaa\x50" + b"\xff" * 100) + with pytest.raises(EsphomeError, match="wrong size"): + _validate_partition_table_binary(f) + + +def test_validate_partition_table_binary_rejects_wrong_magic(tmp_path: Path) -> None: + data = bytearray(_make_partition_table_bytes()) + data[0] = 0x00 + data[1] = 0x00 + f = tmp_path / "partitions.bin" + f.write_bytes(bytes(data)) + with pytest.raises(EsphomeError, match="partition magic"): + _validate_partition_table_binary(f) + + +def test_validate_partition_table_binary_rejects_missing_md5(tmp_path: Path) -> None: + data = bytearray(_make_partition_table_bytes()) + data[32] = 0xFF + data[33] = 0xFF + f = tmp_path / "partitions.bin" + f.write_bytes(bytes(data)) + with pytest.raises(EsphomeError, match="missing the MD5 checksum entry"): + _validate_partition_table_binary(f) + + +def test_validate_partition_table_binary_missing_file(tmp_path: Path) -> None: + with pytest.raises(EsphomeError, match="Cannot read partition table file"): + _validate_partition_table_binary(tmp_path / "does-not-exist.bin") + + +def test_upload_program_ota_partition_table_invalid_file( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--partition-table must fail before calling run_ota when the file is not a partition table.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + + bad_file = tmp_path / "firmware.bin" + bad_file.write_bytes(b"\x00" * 4096) + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file=str(bad_file), partition_table=True) + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError, match="wrong size"): + upload_program(config, args, devices) + mock_run_ota.assert_not_called() + + +def test_upload_program_ota_partition_table_without_allow_flag( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--partition-table must fail fast when allow_partition_access is not enabled in YAML.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ] + } + args = MockArgs(file="partitions.bin", partition_table=True) + devices = ["192.168.1.100"] + + with pytest.raises( + EsphomeError, + match="requires 'allow_partition_access: true'", + ): + upload_program(config, args, devices) + mock_run_ota.assert_not_called() + + def test_upload_program_ota_no_config( mock_get_port_type: Mock, ) -> None: From 4108b271970d599b214765303d840cdbf392b1cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:43:09 -0500 Subject: [PATCH 1814/2056] [esp8266] Lower WDT_FEED_INTERVAL_MS to 100 ms (#16197) --- esphome/core/application.h | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 5baf570e622..369c970d46d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -233,11 +233,10 @@ class Application { /// loops and scheduler items still feed after every op, so any op exceeding /// this threshold triggers a real feed naturally. /// Safety margins vs. platform watchdog timeouts: - /// - ESP32 task WDT (user-configurable): ~5x <-- auto-scaled below - /// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change - /// must keep comfortable margin here - /// - ESP8266 HW WDT (~6 s): ~20x - /// - BK72xx HW WDT (10 s): ~5x <-- platform override below + /// - ESP32 task WDT (user-configurable): ~5x <-- auto-scaled below + /// - ESP8266 soft WDT (~1.6 s): ~16x <-- 100 ms feed (see USE_ESP8266 below) + /// - ESP8266 HW WDT (~6 s): ~60x + /// - BK72xx HW WDT (10 s): ~5x <-- platform override below #ifdef USE_BK72XX // BDK busy-waits 200us per WDT reload (sctrl_dpll_delay200us). LibreTiny // sets HW WDT to 10s; 2000ms keeps ~5x margin. See wdt_ctrl WCMD_RELOAD_PERIOD: @@ -257,6 +256,15 @@ class Application { static_assert(CONFIG_ESP_TASK_WDT_TIMEOUT_S >= 5, "CONFIG_ESP_TASK_WDT_TIMEOUT_S must be at least 5s for a safe WDT feed interval"); static constexpr uint32_t WDT_FEED_INTERVAL_MS = (CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000U) / 5U; +#elif defined(USE_ESP8266) + // ESP8266 needs a tighter feed cadence than the other targets: the soft WDT + // is ~1.6 s and the HW WDT ~6 s, but a single long iteration (mDNS reply, + // wifi scan, OTA verify, lwIP TCP retransmit storm) can push the loop past + // a few hundred ms without giving the SDK a chance to feed. 100 ms keeps a + // ~16x margin to the soft WDT and ~60x to the HW WDT while still avoiding + // the per-iteration arch_feed_wdt() cost (this is the rate limit; component + // loops and scheduler items still feed after every op). + static constexpr uint32_t WDT_FEED_INTERVAL_MS = 100; #else static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300; #endif From af74b639cfc210fc4b63835c8b6f9f319d355d42 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:43:35 -0500 Subject: [PATCH 1815/2056] [fan] Fix TurnOnAction trigger args with reference types (#16222) --- esphome/components/fan/__init__.py | 14 +++++++++++--- esphome/components/fan/automation.h | 9 ++++++++- tests/components/fan/common.yaml | 26 ++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index f47fc06b3d6..3949f16d2e1 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -358,21 +358,29 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): (CONF_DIRECTION, "set_direction", FanDirection), ) + # Normalize trigger args to `const std::remove_cvref_t<T> &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T (value, ref, or const-ref). Matches TurnOnAction::ApplyFn. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] for conf_key, setter, type_ in FIELDS: if (value := config.get(conf_key)) is None: continue if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=type_) + inner = await cg.process_lambda(value, normalized_args, return_type=type_) body_lines.append(f"call.{setter}(({inner})({fwd_args}));") else: body_lines.append(f"call.{setter}({cg.safe_exp(value)});") - # Match TurnOnAction::ApplyFn signature: const Ts &... for trigger args. apply_args = [ (FanCall.operator("ref"), "call"), - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index d8eda41b279..577c9ce600e 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -12,9 +12,16 @@ namespace fan { // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `speed: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template<typename... Ts> class TurnOnAction : public Action<Ts...> { public: - using ApplyFn = void (*)(FanCall &, const Ts &...); + using ApplyFn = void (*)(FanCall &, const std::remove_cvref_t<Ts> &...); TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {} void play(const Ts &...x) override { diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml index 6cabbd24f8d..76508f391e2 100644 --- a/tests/components/fan/common.yaml +++ b/tests/components/fan/common.yaml @@ -9,6 +9,14 @@ fan: has_oscillating: true has_direction: true speed_count: 3 + # Exercise fan.turn_on inside a trigger whose Ts pack is non-empty + # (StringRef from on_preset_set) so the apply-lambda + inner-lambda + # codegen runs through the cvref-normalized path. + on_preset_set: + then: + - fan.turn_on: + id: test_fan + speed: !lambda "return x.empty() ? 1 : 3;" # Test lambdas using get_preset_mode() which returns StringRef # These examples match the migration guide in the PR description @@ -88,3 +96,21 @@ button: - fan.turn_on: id: test_fan speed: !lambda 'return 1;' + +# Exercise fan.turn_on inside triggers with non-empty Ts: +# - number.on_value: Ts = float (Python value type; previously raised +# AttributeError on .operator("const")) +# - fan.on_preset_set: Ts = StringRef (already a value-type wrapper around +# a const char * + size; tests the cvref-normalized inner-lambda path) +number: + - platform: template + id: fan_speed_number + optimistic: true + min_value: 1 + max_value: 3 + step: 1 + on_value: + then: + - fan.turn_on: + id: test_fan + speed: !lambda "return (int) x;" From cf223674e51cff65ed9c71b46b434c9c48dd8b72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:43:49 -0500 Subject: [PATCH 1816/2056] [climate] Fix ControlAction trigger args with reference types (#16221) --- esphome/components/climate/__init__.py | 14 +++++++++++--- esphome/components/climate/automation.h | 9 ++++++++- tests/components/climate/common.yaml | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 7c9002d6dc3..fc1b0f368e8 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -506,6 +506,15 @@ async def climate_control_to_code(config, action_id, template_arg, args): (CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode), ) + # Normalize trigger args to `const std::remove_cvref_t<T> &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T (value, ref, or const-ref). Matches ControlAction::ApplyFn. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] @@ -513,7 +522,7 @@ async def climate_control_to_code(config, action_id, template_arg, args): if (value := config.get(conf_key)) is None: continue if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=type_) + inner = await cg.process_lambda(value, normalized_args, return_type=type_) body_lines.append(f"call.{setter}(({inner})({fwd_args}));") elif type_ is cg.std_string: # Static custom strings: emit a flash literal and pass the @@ -526,10 +535,9 @@ async def climate_control_to_code(config, action_id, template_arg, args): else: body_lines.append(f"call.{setter}({cg.safe_exp(value)});") - # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. apply_args = [ (ClimateCall.operator("ref"), "call"), - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 71d23fd6b6d..6ac9bd8bae4 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -10,9 +10,16 @@ namespace esphome::climate { // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `target_temperature: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template<typename... Ts> class ControlAction : public Action<Ts...> { public: - using ApplyFn = void (*)(ClimateCall &, const Ts &...); + using ApplyFn = void (*)(ClimateCall &, const std::remove_cvref_t<Ts> &...); ControlAction(Climate *climate, ApplyFn apply) : climate_(climate), apply_(apply) {} void play(const Ts &...x) override { diff --git a/tests/components/climate/common.yaml b/tests/components/climate/common.yaml index 2d35438afd2..c28fde8eeb4 100644 --- a/tests/components/climate/common.yaml +++ b/tests/components/climate/common.yaml @@ -85,3 +85,18 @@ button: - climate.control: id: climate_test_thermostat mode: "OFF" + +# Exercise climate.control inside a trigger with non-empty Ts (number on_value +# passes float). +number: + - platform: template + id: climate_target_temp_number + optimistic: true + min_value: 16 + max_value: 28 + step: 0.5 + on_value: + then: + - climate.control: + id: climate_test_thermostat + target_temperature_high: !lambda "return x;" From 41bd570d309ac9590f0f122740b134536c08ca02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:44:01 -0500 Subject: [PATCH 1817/2056] [light] Fix LightControlAction trigger args with reference types (#16220) --- esphome/components/light/automation.h | 9 ++++++++- esphome/components/light/automation.py | 16 ++++++++++++---- tests/components/light/common.yaml | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index a5c73997b0b..993d4a2ea66 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -36,9 +36,16 @@ template<bool HasTransitionLength, typename... Ts> class ToggleAction : public A // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `brightness: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template<typename... Ts> class LightControlAction : public Action<Ts...> { public: - using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...); + using ApplyFn = void (*)(LightState *, LightCall &, const std::remove_cvref_t<Ts> &...); LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {} void play(const Ts &...x) override { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index ca4018a9756..cef774af38d 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -200,6 +200,15 @@ async def light_control_to_code(config, action_id, template_arg, args): (CONF_WARM_WHITE, "set_warm_white", cg.float_), ) + # Normalize trigger args to `const std::remove_cvref_t<T> &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T (value, ref, or const-ref). Matches LightControlAction::ApplyFn. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] @@ -208,7 +217,7 @@ async def light_control_to_code(config, action_id, template_arg, args): continue value = config[conf_key] if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=type_) + inner = await cg.process_lambda(value, normalized_args, return_type=type_) body_lines.append(f"call.{setter}(({inner})({fwd_args}));") else: body_lines.append(f"call.{setter}({cg.safe_exp(value)});") @@ -216,7 +225,7 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_EFFECT in config: if isinstance(config[CONF_EFFECT], Lambda): inner_lambda = await cg.process_lambda( - config[CONF_EFFECT], args, return_type=cg.std_string + config[CONF_EFFECT], normalized_args, return_type=cg.std_string ) body_lines.append( f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n" @@ -230,11 +239,10 @@ async def light_control_to_code(config, action_id, template_arg, args): f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));" ) - # Match LightControlAction::ApplyFn signature: const Ts &... for trigger args. apply_args = [ (LightState.operator("ptr"), "parent"), (LightCall.operator("ref"), "call"), - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml index e58f7baee4b..044a8144fad 100644 --- a/tests/components/light/common.yaml +++ b/tests/components/light/common.yaml @@ -127,6 +127,21 @@ esphome: blue: 0% transition_length: 1s +# Exercise light actions inside a trigger with non-empty Ts (number on_value +# passes float). +number: + - platform: template + id: test_number_brightness + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + on_value: + then: + - light.turn_on: + id: test_monochromatic_light + brightness: !lambda "return x / 100.0;" + light: - platform: binary id: test_binary_light From df1200629fcf59d763675c7b7c59bf26544693b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:44:11 -0500 Subject: [PATCH 1818/2056] [tests] Fix flaky host_mode_climate_basic_state (#16225) --- tests/integration/fixtures/host_mode_climate_basic_state.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/fixtures/host_mode_climate_basic_state.yaml b/tests/integration/fixtures/host_mode_climate_basic_state.yaml index f79d684fc60..744b418d0fa 100644 --- a/tests/integration/fixtures/host_mode_climate_basic_state.yaml +++ b/tests/integration/fixtures/host_mode_climate_basic_state.yaml @@ -28,6 +28,10 @@ climate: min_temperature: 15.0 max_temperature: 32.0 temperature_step: 0.1 + # Don't restore previous state from flash — this fixture shares the + # `host-climate-test` build dir with host_mode_climate_control.yaml, so a + # prior run of that test could leave the thermostat in HEAT/COOL. + on_boot_restore_from: default_preset default_preset: home preset: - name: "away" From 33f88619daf76c64a5614e36aa54fb89cc0f734a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:44:21 -0500 Subject: [PATCH 1819/2056] [valve] Fix ControlAction trigger args with reference types (#16223) --- esphome/components/valve/__init__.py | 14 +++++++++++--- esphome/components/valve/automation.h | 9 ++++++++- tests/components/template/common-base.yaml | 13 +++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 7377aea1ed2..d82a9fdec28 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -240,21 +240,29 @@ async def valve_control_to_code(config, action_id, template_arg, args): (CONF_POSITION, "set_position", cg.float_), ) + # Normalize trigger args to `const std::remove_cvref_t<T> &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T (value, ref, or const-ref). Matches ControlAction::ApplyFn. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] for conf_key, setter, type_ in FIELDS: if (value := config.get(conf_key)) is None: continue if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=type_) + inner = await cg.process_lambda(value, normalized_args, return_type=type_) body_lines.append(f"call.{setter}(({inner})({fwd_args}));") else: body_lines.append(f"call.{setter}({cg.safe_exp(value)});") - # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. apply_args = [ (ValveCall.operator("ref"), "call"), - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index ae9ac0db762..27c0e329f08 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -52,9 +52,16 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `position: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template<typename... Ts> class ControlAction : public Action<Ts...> { public: - using ApplyFn = void (*)(ValveCall &, const Ts &...); + using ApplyFn = void (*)(ValveCall &, const std::remove_cvref_t<Ts> &...); ControlAction(Valve *valve, ApplyFn apply) : valve_(valve), apply_(apply) {} void play(const Ts &...x) override { diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 819eaa8bbfe..984ef129ad4 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -356,6 +356,19 @@ number: min_value: 0 max_value: 100 step: 1 + # Exercise valve.control inside a trigger with non-empty Ts (number on_value + # passes float). + - platform: template + id: template_valve_position_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + on_value: + then: + - valve.control: + id: template_valve + position: !lambda "return x / 100.0f;" select: - platform: template From 15ab5422c793e94df4ea69828468c6ae7131e6a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 19:45:08 -0500 Subject: [PATCH 1820/2056] [ci] Run downstream device-builder tests against PR Python code (#16214) --- .github/workflows/ci.yml | 50 ++++++++++++++++ script/determine-jobs.py | 54 +++++++++++++++++ tests/script/test_determine_jobs.py | 93 +++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af1709774d..87058e4fa5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,53 @@ jobs: if-no-files-found: ignore retention-days: 14 + device-builder: + name: Test downstream esphome/device-builder + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: needs.determine-jobs.outputs.device-builder == 'true' + steps: + - name: Check out esphome (this PR) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: esphome + - name: Check out esphome/device-builder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: esphome/device-builder + ref: main + path: device-builder + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.13" + - name: Set up uv + # Mirrors the install shape device-builder's own CI uses + # (esphome/device-builder#192): uv replaces pip for the + # install step (order-of-magnitude faster on cold boots, + # with its own wheel cache). actions/setup-python still + # provides the interpreter. + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + - name: Install device-builder + esphome from PR + # Install device-builder with its esphome + test extras + # first so its pinned versions of pytest/etc. land, then + # overlay the PR's esphome so the downstream tests run + # against this PR's Python code. ``--system`` installs into + # the runner's Python instead of a venv. + run: | + uv pip install --system -e './device-builder[esphome,test]' + uv pip install --system -e ./esphome + - name: Run device-builder pytest + # ``-n auto`` runs under pytest-xdist (matches device-builder's + # own CI). No ``--cov`` here -- this is purely a downstream + # smoke check against this PR's esphome code. + working-directory: device-builder + run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks + pytest: name: Run pytest strategy: @@ -204,6 +251,7 @@ jobs: clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} import-time: ${{ steps.determine.outputs.import-time }} + device-builder: ${{ steps.determine.outputs.device-builder }} changed-components: ${{ steps.determine.outputs.changed-components }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} @@ -247,6 +295,7 @@ jobs: echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT + echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT @@ -1063,6 +1112,7 @@ jobs: - clang-tidy-nosplit - clang-tidy-split - determine-jobs + - device-builder - test-build-components-split - pre-commit-ci-lite - memory-impact-target-branch diff --git a/script/determine-jobs.py b/script/determine-jobs.py index c0cf8ecbdc8..b8f324784d8 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -10,6 +10,7 @@ what files have changed. It outputs JSON with the following structure: "clang_tidy": true/false, "clang_format": true/false, "python_linters": true/false, + "device_builder": true/false, "changed_components": ["component1", "component2", ...], "component_test_count": 5, "memory_impact": { @@ -25,6 +26,7 @@ The CI workflow uses this information to: - Skip or run clang-tidy (and whether to do a full scan) - Skip or run clang-format - Skip or run Python linters (ruff, flake8, pylint, pyupgrade) +- Skip or run downstream esphome/device-builder tests against the PR's Python code - Determine which components to test individually - Decide how to split component tests (if there are many) - Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes @@ -440,6 +442,56 @@ def should_run_import_time(branch: str | None = None) -> bool: return False +# Files outside esphome/**/*.py whose changes can affect the downstream +# device-builder build. requirements.txt / pyproject.toml change the runtime +# dependency graph that device-builder picks up when it installs esphome. +DEVICE_BUILDER_TRIGGER_FILES = frozenset( + { + "requirements.txt", + "pyproject.toml", + } +) + + +def should_run_device_builder(branch: str | None = None) -> bool: + """Determine if downstream esphome/device-builder tests should run. + + device-builder imports esphome as a library, so whenever the importable + Python surface, the runtime dependencies, or any non-C++ file packaged + with esphome (pyproject.toml has ``include-package-data = true``, so + things like esphome/idf_component.yml ship and can affect installs) + changes we re-run its test suite against the PR's code to catch + breakage we'd otherwise only see after a release. + + Skipped on beta/release branches: those branches typically lag behind + device-builder@main, so a new device-builder API dependency would + falsely fail the run without reflecting any problem in the PR itself. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if the device-builder downstream tests should run, False otherwise. + """ + target_branch = get_target_branch() + if target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ): + return False + + for file in changed_files(branch): + if file in DEVICE_BUILDER_TRIGGER_FILES: + return True + # Anything under esphome/ that isn't C++ source can change the + # importable / packaged surface device-builder consumes + # (Python sources, packaged YAML/JSON like idf_component.yml, + # etc.). C++ files only affect compiled firmware, not the + # Python install device-builder pulls in. + if file.startswith("esphome/") and not file.endswith(CPP_FILE_EXTENSIONS): + return True + return False + + def determine_cpp_unit_tests( branch: str | None = None, ) -> tuple[bool, list[str]]: @@ -874,6 +926,7 @@ def main() -> None: run_clang_format = should_run_clang_format(args.branch) run_python_linters = should_run_python_linters(args.branch) run_import_time = should_run_import_time(args.branch) + run_device_builder = should_run_device_builder(args.branch) changed_cpp_file_count = count_changed_cpp_files(args.branch) # Get changed components @@ -1007,6 +1060,7 @@ def main() -> None: "clang_format": run_clang_format, "python_linters": run_python_linters, "import_time": run_import_time, + "device_builder": run_device_builder, "changed_components": changed_components, "changed_components_with_tests": changed_components_with_tests, "directly_changed_components_with_tests": list(directly_changed_with_tests), diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e85f1757b03..cc795bc5532 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -63,6 +63,13 @@ def mock_should_run_import_time() -> Generator[Mock, None, None]: yield mock +@pytest.fixture +def mock_should_run_device_builder() -> Generator[Mock, None, None]: + """Mock should_run_device_builder from determine_jobs.""" + with patch.object(determine_jobs, "should_run_device_builder") as mock: + yield mock + + @pytest.fixture def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]: """Mock determine_cpp_unit_tests from helpers.""" @@ -99,6 +106,7 @@ def test_main_all_tests_should_run( mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -113,6 +121,7 @@ def test_main_all_tests_should_run( mock_should_run_clang_format.return_value = True mock_should_run_python_linters.return_value = True mock_should_run_import_time.return_value = True + mock_should_run_device_builder.return_value = True mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -193,6 +202,7 @@ def test_main_all_tests_should_run( assert output["clang_format"] is True assert output["python_linters"] is True assert output["import_time"] is True + assert output["device_builder"] is True assert output["changed_components"] == ["wifi", "api", "sensor"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -225,6 +235,7 @@ def test_main_no_tests_should_run( mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -239,6 +250,7 @@ def test_main_no_tests_should_run( mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False mock_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False mock_determine_cpp_unit_tests.return_value = (False, []) # Mock changed_files to return no component files @@ -278,6 +290,7 @@ def test_main_no_tests_should_run( assert output["clang_format"] is False assert output["python_linters"] is False assert output["import_time"] is False + assert output["device_builder"] is False assert output["changed_components"] == [] assert output["changed_components_with_tests"] == [] assert output["component_test_count"] == 0 @@ -299,6 +312,7 @@ def test_main_with_branch_argument( mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -313,6 +327,7 @@ def test_main_with_branch_argument( mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = True mock_should_run_import_time.return_value = True + mock_should_run_device_builder.return_value = True mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -350,6 +365,7 @@ def test_main_with_branch_argument( mock_should_run_clang_format.assert_called_once_with("main") mock_should_run_python_linters.assert_called_once_with("main") mock_should_run_import_time.assert_called_once_with("main") + mock_should_run_device_builder.assert_called_once_with("main") # Check output captured = capsys.readouterr() @@ -362,6 +378,7 @@ def test_main_with_branch_argument( assert output["clang_format"] is False assert output["python_linters"] is True assert output["import_time"] is True + assert output["device_builder"] is True assert output["changed_components"] == ["mqtt"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -734,6 +751,82 @@ def test_should_run_import_time_with_branch() -> None: mock_changed.assert_called_once_with("release") +@pytest.mark.parametrize( + ("changed_files", "expected_result"), + [ + # esphome Python files trigger downstream device-builder tests + (["esphome/__main__.py"], True), + (["esphome/components/wifi/__init__.py"], True), + (["esphome/core/config.py"], True), + (["esphome/types.pyi"], True), + # Runtime dependency changes trigger + (["requirements.txt"], True), + (["pyproject.toml"], True), + # Non-C++ files packaged with esphome trigger -- device-builder + # picks them up because esphome's pyproject sets + # include-package-data = true. + (["esphome/idf_component.yml"], True), + (["esphome/dashboard/templates/index.html"], True), + (["esphome/components/api/api_pb2_service.json"], True), + # Mixed: any triggering file is enough + (["docs/README.md", "esphome/config.py"], True), + # Dev/test-only dependency changes don't trigger device-builder + # (they don't affect the importable surface device-builder uses) + (["requirements_dev.txt"], False), + (["requirements_test.txt"], False), + # Files outside esphome/ don't trigger + (["script/some_other_script.py"], False), + (["tests/script/test_determine_jobs.py"], False), + # C++ files under esphome/ don't trigger -- they only affect + # compiled firmware, not the Python install device-builder pulls in. + (["esphome/core/component.cpp"], False), + (["esphome/core/component.h"], False), + (["esphome/components/wifi/wifi_component.cpp"], False), + # Files outside esphome/ entirely + (["tests/components/wifi/test.esp32-idf.yaml"], False), + (["README.md"], False), + ([], False), + ], +) +def test_should_run_device_builder( + changed_files: list[str], expected_result: bool +) -> None: + """Test should_run_device_builder function (non-beta/release target).""" + with ( + patch.object(determine_jobs, "changed_files", return_value=changed_files), + # Mock target branch to "dev" so the beta/release skip is bypassed + # for these per-file behavior checks. + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + result = determine_jobs.should_run_device_builder() + assert result == expected_result + + +def test_should_run_device_builder_with_branch() -> None: + """Test should_run_device_builder with branch argument.""" + with ( + patch.object(determine_jobs, "changed_files") as mock_changed, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed.return_value = [] + determine_jobs.should_run_device_builder("release") + mock_changed.assert_called_once_with("release") + + +@pytest.mark.parametrize("target_branch", ["beta", "release", "release-2026.5"]) +def test_should_run_device_builder_skips_beta_release(target_branch: str) -> None: + """Beta/release target branches skip device-builder (lag behind device-builder@main).""" + with ( + patch.object(determine_jobs, "get_target_branch", return_value=target_branch), + patch.object(determine_jobs, "changed_files") as mock_changed, + ): + # Even with a triggering file present, the target-branch guard wins. + mock_changed.return_value = ["esphome/__main__.py"] + assert determine_jobs.should_run_device_builder() is False + # changed_files shouldn't even be consulted -- the guard short-circuits. + mock_changed.assert_not_called() + + @pytest.mark.parametrize( ("changed_files", "expected_result"), [ From b5eb4440155ce09e126361eb0046f200225df4d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:01:51 -0500 Subject: [PATCH 1821/2056] [dashboard] Stabilize device-builder dashboard backend's API surface (#16206) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/dashboard_import/__init__.py | 11 + esphome/components/esp32/__init__.py | 12 + esphome/components/esp8266/__init__.py | 12 + esphome/components/libretiny/__init__.py | 12 + esphome/components/libretiny/const.py | 8 + esphome/components/rp2040/__init__.py | 12 + esphome/dashboard/util/text.py | 18 +- esphome/helpers.py | 24 +- esphome/storage_json.py | 28 +++ esphome/zeroconf.py | 41 +++ tests/unit_tests/test_dashboard_import.py | 203 +++++++++++++++ tests/unit_tests/test_helpers.py | 45 ++++ tests/unit_tests/test_zeroconf.py | 237 ++++++++++++++++++ 13 files changed, 656 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/test_dashboard_import.py create mode 100644 tests/unit_tests/test_zeroconf.py diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index dbe5532902d..30b33941653 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -89,6 +89,17 @@ def import_config( network: str = CONF_WIFI, encryption: bool = False, ) -> None: + """Materialise a dashboard-imported device's YAML on disk. + + Used by: + - esphome.dashboard (legacy dashboard) + - device-builder (esphome/device-builder) — called from the + ``devices/import`` WS handler to seed the YAML for an adopted + factory firmware. Coordinate before changing the kwargs or the + generated YAML's top-level keys; both consumers depend on the + output shape (``esphome.name`` / ``packages:`` import url) to + route subsequent compile + flash operations. + """ p = Path(path) if p.exists(): diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9a9ee8fb083..b60dab36349 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -489,6 +489,18 @@ def get_board(core_obj=None): def get_download_types(storage_json): + """Binary-download entries for a built ESP32 firmware. + + Used by: + - esphome.dashboard (legacy "Download .bin" button) + - device-builder (esphome/device-builder) — same dispatch via + ``importlib.import_module(f"esphome.components.{platform}")`` + then ``module.get_download_types(storage)``. The contract is + "returns ``list[dict]`` with at least ``title`` / + ``description`` / ``file`` / ``download`` keys"; please keep + the shape stable so the new dashboard's download panel + doesn't have to special-case per-platform schemas. + """ return [ { "title": "Factory format (Previously Modern)", diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 34540bd48da..3c2806a307d 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -94,6 +94,18 @@ def set_core_data(config): def get_download_types(storage_json): + """Binary-download entries for a built ESP8266 firmware. + + Used by: + - esphome.dashboard (legacy "Download .bin" button) + - device-builder (esphome/device-builder) — same dispatch via + ``importlib.import_module(f"esphome.components.{platform}")`` + then ``module.get_download_types(storage)``. The contract is + "returns ``list[dict]`` with at least ``title`` / + ``description`` / ``file`` / ``download`` keys"; please keep + the shape stable so the new dashboard's download panel + doesn't have to special-case per-platform schemas. + """ return [ { "title": "Standard format", diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 74ac51d2001..40fb773784b 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -156,6 +156,18 @@ def only_on_family(*, supported=None, unsupported=None): def get_download_types(storage_json: StorageJSON = None): + """Binary-download entries for a built LibreTiny firmware. + + Used by: + - esphome.dashboard (legacy "Download .bin" button) + - device-builder (esphome/device-builder) — same dispatch via + ``importlib.import_module(f"esphome.components.{platform}")`` + then ``module.get_download_types(storage)``. The contract is + "returns ``list[dict]`` with at least ``title`` / + ``description`` / ``file`` / ``download`` keys"; please keep + the shape stable so the new dashboard's download panel + doesn't have to special-case per-platform schemas. + """ types = [ { "title": "UF2 package (recommended)", diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 5de4a164b57..0119a0db3ff 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -54,6 +54,14 @@ COMPONENT_LN882X = "ln882x" COMPONENT_RTL87XX = "rtl87xx" # COMPONENTS - end +# Note for ``generate_components.py`` maintainers: the +# ``FAMILY_COMPONENT`` map below is also consumed externally — +# device-builder (esphome/device-builder) derives the set of +# ``target_platform`` values that should route to the ``libretiny`` +# component for the dashboard's ``get_download_types`` lookup from +# ``FAMILY_COMPONENT.values()``. New chip families added by the +# generator are picked up automatically; please don't repurpose +# the public ``FAMILY_COMPONENT`` name without coordinating. # FAMILIES - auto-generated! Do not modify this block. FAMILY_BK7231N = "BK7231N" FAMILY_BK7231Q = "BK7231Q" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index ed246416c91..79ed00cb416 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -69,6 +69,18 @@ def set_core_data(config): def get_download_types(storage_json): + """Binary-download entries for a built RP2040 firmware. + + Used by: + - esphome.dashboard (legacy "Download .bin" button) + - device-builder (esphome/device-builder) — same dispatch via + ``importlib.import_module(f"esphome.components.{platform}")`` + then ``module.get_download_types(storage)``. The contract is + "returns ``list[dict]`` with at least ``title`` / + ``description`` / ``file`` / ``download`` keys"; please keep + the shape stable so the new dashboard's download panel + doesn't have to special-case per-platform schemas. + """ return [ { "title": "UF2 factory format", diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py index 2a3b9042e6e..bdf9abfdb9b 100644 --- a/esphome/dashboard/util/text.py +++ b/esphome/dashboard/util/text.py @@ -1,9 +1,15 @@ +"""Back-compat shim for ``friendly_name_slugify``. + +The function moved to :mod:`esphome.helpers` so it survives the legacy +dashboard's eventual removal — see the +``esphome.helpers.friendly_name_slugify`` docstring. This module +re-exports the name so existing +``from esphome.dashboard.util.text import friendly_name_slugify`` +imports keep working while downstream consumers migrate. +""" + from __future__ import annotations -from esphome.helpers import slugify +from esphome.helpers import friendly_name_slugify - -def friendly_name_slugify(value: str) -> str: - """Convert a friendly name to a slug with dashes instead of underscores.""" - # First use the standard slugify, then convert underscores to dashes - return slugify(value).replace("_", "-") +__all__ = ["friendly_name_slugify"] diff --git a/esphome/helpers.py b/esphome/helpers.py index f41bec357d6..bb1984e17cb 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -120,6 +120,24 @@ def slugify(value: str) -> str: return "".join(c for c in value if c in ALLOWED_NAME_CHARS) +def friendly_name_slugify(value: str) -> str: + """Convert a friendly name to a slug with dashes instead of underscores. + + Used by: + - esphome.dashboard.web_server (legacy dashboard) + - device-builder (esphome/device-builder) — slugifies friendly names + into the YAML filename / device name during adoption + wizard flows. + + Lives here rather than in ``esphome.dashboard.util.text`` so it + survives the legacy dashboard's eventual removal. + The dashboard module re-exports this name as a back-compat shim. + Coordinate with the device-builder team before changing the + slugification rules — the mapping must stay stable so existing + on-disk filenames keep matching across releases. + """ + return slugify(value).replace("_", "-") + + def indent_all_but_first_and_last(text, padding=" "): lines = text.splitlines(True) if len(lines) <= 2: @@ -370,7 +388,11 @@ def rmtree(path: Path | str) -> None: os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) func(path) - shutil.rmtree(path, onerror=_onerror) + # ``onerror`` is deprecated in 3.12 in favour of ``onexc`` (different + # callable signature); keep the existing handler shape for now and + # silence the lint locally so this PR doesn't bundle an unrelated + # migration. + shutil.rmtree(path, onerror=_onerror) # pylint: disable=deprecated-argument def walk_files(path: Path): diff --git a/esphome/storage_json.py b/esphome/storage_json.py index d5423ab1c75..c6df16ce78d 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -21,6 +21,14 @@ def storage_path() -> Path: def ext_storage_path(config_filename: str) -> Path: + """Path to the per-config StorageJSON sidecar. + + Used by: + - device-builder (esphome/device-builder) — locates the sidecar + to read board / framework / firmware-bin / loaded_integrations + info for the dashboard. Coordinate before changing the path + shape; device-builder reads the same file on disk. + """ return CORE.data_dir / "storage" / f"{config_filename}.json" @@ -29,6 +37,14 @@ def esphome_storage_path() -> Path: def ignored_devices_storage_path() -> Path: + """Path to the dashboard's ignored-devices list. + + Used by: + - device-builder (esphome/device-builder) — reads the same + ``ignored-devices.json`` so the new dashboard's "ignore" toggle + stays compatible with the legacy one. Don't change the file + shape without coordinating. + """ return CORE.data_dir / "ignored-devices.json" @@ -46,6 +62,18 @@ def _to_path_if_not_none(value: str | None) -> Path | None: class StorageJSON: + """Persisted device metadata sidecar. + + Used by: + - esphome.dashboard (legacy dashboard) + - device-builder (esphome/device-builder) — reads/writes the same + JSON file as the legacy dashboard so a single config_dir can be + shared between the two during the transition. The schema + (``storage_version``, field names, types) must stay backwards + compatible — coordinate with the device-builder team before + adding required fields or changing semantics of existing ones. + """ + def __init__( self, storage_version: int, diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 6f5d33c8085..5d922ea9111 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -60,6 +60,18 @@ TXT_RECORD_VERSION = b"version" @dataclass class DiscoveredImport: + """An importable device discovered via mDNS ``_esphomelib._tcp.local.``. + + Used by: + - esphome.dashboard (legacy dashboard) + - device-builder (esphome/device-builder) — surfaces these as + "discovered devices" on the new dashboard's adoption flow. + + Fields are populated from TXT records on the broadcast service + info (see :class:`DashboardImportDiscovery`). Coordinate before + adding/removing fields — both consumers persist them. + """ + friendly_name: str | None device_name: str package_import_url: str @@ -73,6 +85,22 @@ class DashboardBrowser(AsyncServiceBrowser): class DashboardImportDiscovery: + """Track importable devices announcing on ``_esphomelib._tcp.local.``. + + Used by: + - esphome.dashboard (legacy dashboard) + - device-builder (esphome/device-builder) — wired up alongside + the dashboard's own ``ServiceBrowser`` to populate the + "Discovered devices" panel and the adoption flow. + + The class maintains ``import_state: dict[str, DiscoveredImport]`` + keyed by the mDNS service name. ``on_update`` is invoked with + ``(name, info | None)`` for additions and removals; update events + refresh ``import_state`` without firing the callback. + Coordinate before changing the callback signature or the keys + of ``import_state`` — device-builder reads both directly. + """ + def __init__( self, on_update: Callable[[str, DiscoveredImport | None], None] | None = None ) -> None: @@ -232,6 +260,19 @@ async def async_resolve_hosts( class AsyncEsphomeZeroconf(AsyncZeroconf): + """ESPHome-tuned ``AsyncZeroconf`` with a hostname-resolve helper. + + Used by: + - esphome.dashboard (legacy dashboard) + - device-builder (esphome/device-builder) — drives both the live + mDNS browser and the per-sweep ``async_resolve_host`` fallback + for non-API devices that don't broadcast esphomelib. + + Coordinate before adding required constructor args or changing + the ``async_resolve_host`` signature — device-builder calls it + on every ping cycle. + """ + async def async_resolve_host( self, host: str, timeout: float = DEFAULT_TIMEOUT ) -> list[str] | None: diff --git a/tests/unit_tests/test_dashboard_import.py b/tests/unit_tests/test_dashboard_import.py new file mode 100644 index 00000000000..427bee0f86d --- /dev/null +++ b/tests/unit_tests/test_dashboard_import.py @@ -0,0 +1,203 @@ +"""Unit tests for ``esphome.components.dashboard_import.import_config``. + +Locks the YAML shape that ``import_config`` materialises on disk for +adopted factory firmware. Both the legacy dashboard and the new +device-builder backend (esphome/device-builder) call this function +during the adoption flow and depend on the output's ``esphome.name`` +/ ``packages:`` keys to route subsequent compile + flash operations. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml as pyyaml + +from esphome.components.dashboard_import import import_config + + +def _load_plain_yaml(path: Path) -> dict: + """Load YAML without invoking ESPHome's ``CORE``-aware loader. + + ``esphome.yaml_util.load_yaml`` resolves ``!include`` / + ``!secret`` against ``CORE.config_path`` which isn't set in + these tests. We're only asserting on plain key/value structure, + so ``pyyaml.load`` with a custom loader subclassing + ``pyyaml.SafeLoader`` (and empty fallbacks for the secret/include + tags) is enough. + """ + + class _Loader(pyyaml.SafeLoader): + pass + + _Loader.add_constructor("!secret", lambda loader, node: f"!secret {node.value}") + _Loader.add_constructor("!include", lambda loader, node: f"!include {node.value}") + + return pyyaml.load(path.read_text(encoding="utf-8"), Loader=_Loader) + + +def test_basic_import_writes_expected_yaml_shape(tmp_path: Path) -> None: + """A minimal Wi-Fi import emits the substitutions / packages / esphome triad. + + These three top-level blocks are the contract: substitutions + holds the device-specific name, packages pulls in the upstream + firmware via the import URL, and esphome.name interpolates from + substitutions. Anything that depends on this output (frontend + config viewer, follow-up edits, version checks) reads those + keys directly. + """ + yaml_path = tmp_path / "kitchen.yaml" + + import_config( + path=str(yaml_path), + name="kitchen", + friendly_name="Kitchen", + project_name="acme.kitchen-light", + import_url="github://acme/firmware/kitchen.yaml@main", + ) + + assert yaml_path.exists() + config = _load_plain_yaml(yaml_path) + + assert config["substitutions"] == { + "name": "kitchen", + "friendly_name": "Kitchen", + } + assert config["packages"] == { + "acme.kitchen-light": "github://acme/firmware/kitchen.yaml@main" + } + assert config["esphome"] == { + "name": "${name}", + "name_add_mac_suffix": False, + "friendly_name": "${friendly_name}", + } + + +def test_import_appends_wifi_config_when_network_is_wifi(tmp_path: Path) -> None: + """Wi-Fi devices get a ``wifi:`` block templated with secrets references. + + Adopted Wi-Fi devices need a ``wifi:`` section so they can + actually connect on the user's LAN — the boilerplate references + ``!secret wifi_ssid`` / ``!secret wifi_password`` so the + user's existing secrets file plugs in. Devices on other + networks (Ethernet) shouldn't get the Wi-Fi block. + """ + yaml_path = tmp_path / "kitchen.yaml" + import_config( + path=str(yaml_path), + name="kitchen", + friendly_name=None, + project_name="acme.kitchen-light", + import_url="github://acme/firmware/kitchen.yaml@main", + ) + contents = yaml_path.read_text() + assert "wifi:" in contents + assert "!secret wifi_ssid" in contents + assert "!secret wifi_password" in contents + + +def test_import_omits_wifi_block_for_ethernet_network(tmp_path: Path) -> None: + """Ethernet devices get no ``wifi:`` block — caller wires Ethernet separately. + + The ``network`` parameter exists specifically so non-Wi-Fi + devices (PoE / Ethernet, etc.) skip the Wi-Fi templating — + otherwise their generated YAML would carry an unused ``wifi:`` + section the user has to clean up by hand. + """ + yaml_path = tmp_path / "olimex-poe.yaml" + import_config( + path=str(yaml_path), + name="olimex-poe", + friendly_name=None, + project_name="acme.poe-monitor", + import_url="github://acme/firmware/poe.yaml@main", + network="ethernet", + ) + contents = yaml_path.read_text() + assert "wifi:" not in contents + + +def test_import_with_encryption_writes_api_key(tmp_path: Path) -> None: + """``encryption=True`` generates a fresh Noise PSK in the api block. + + Used during the adoption flow when the device-builder UI + explicitly opts the new device into encrypted API. Each + invocation must produce a fresh 32-byte PSK base64-encoded into + the YAML; subsequent compiles and the dashboard's encryption + indicator both read it from there. + """ + yaml_path_1 = tmp_path / "a.yaml" + yaml_path_2 = tmp_path / "b.yaml" + + import_config( + path=str(yaml_path_1), + name="a", + friendly_name=None, + project_name="acme.dev", + import_url="github://acme/firmware/dev.yaml@main", + encryption=True, + ) + import_config( + path=str(yaml_path_2), + name="b", + friendly_name=None, + project_name="acme.dev", + import_url="github://acme/firmware/dev.yaml@main", + encryption=True, + ) + + config_1 = _load_plain_yaml(yaml_path_1) + config_2 = _load_plain_yaml(yaml_path_2) + assert "api" in config_1 and "encryption" in config_1["api"] + key_1 = config_1["api"]["encryption"]["key"] + key_2 = config_2["api"]["encryption"]["key"] + # Fresh per-call PSK, not a hardcoded value. + assert key_1 != key_2 + # Base64-encoded 32 bytes → length 44 with one trailing `=`. + assert len(key_1) == 44 + + +def test_import_without_friendly_name_omits_friendly_substitution( + tmp_path: Path, +) -> None: + """``friendly_name=None`` skips the friendly_name substitution. + + Some imported configs don't carry a friendly name. The output + shouldn't pretend they do — the substitutions block must omit + ``friendly_name`` so the dashboard renders blank rather than + the literal substitution token. + """ + yaml_path = tmp_path / "noname.yaml" + import_config( + path=str(yaml_path), + name="noname", + friendly_name=None, + project_name="acme.dev", + import_url="github://acme/firmware/dev.yaml@main", + ) + config = _load_plain_yaml(yaml_path) + assert config["substitutions"] == {"name": "noname"} + assert "friendly_name" not in config["esphome"] + + +def test_import_refuses_to_overwrite_existing_yaml(tmp_path: Path) -> None: + """An already-present file raises rather than clobbering the user's edits. + + Both the legacy dashboard and device-builder rely on the + ``FileExistsError`` to surface a "config already exists" message + instead of silently destroying user data. + """ + yaml_path = tmp_path / "existing.yaml" + yaml_path.write_text("# user's hand-edited config\n", encoding="utf-8") + + with pytest.raises(FileExistsError): + import_config( + path=str(yaml_path), + name="existing", + friendly_name=None, + project_name="acme.dev", + import_url="github://acme/firmware/dev.yaml@main", + ) + # Original content survives unchanged. + assert yaml_path.read_text() == "# user's hand-edited config\n" diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 159d3230ab8..f2faf3ba8f9 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -90,6 +90,51 @@ def test_cpp_string_escape(string, expected): assert actual == expected +@pytest.mark.parametrize( + "value, expected", + ( + # Basic underscore→dash conversion. + ("Living Room Sensor", "living-room-sensor"), + # Already-slugified input passes through with dash output. + ("kitchen_light", "kitchen-light"), + # Accents are stripped (matches the underlying ``slugify``). + ("Café Caché", "cafe-cache"), + # Mixed casing + multiple separators collapse correctly. + ("Foo Bar__Baz", "foo-bar-baz"), + # Empty input yields empty output. + ("", ""), + # Numbers survive intact. + ("Sensor 42", "sensor-42"), + ), +) +def test_friendly_name_slugify(value, expected): + """Friendly-name → URL-safe dash-slug. + + Stable mapping is part of the cross-tool contract + (legacy dashboard + device-builder both depend on it for + filename → device-name routing). Lock the cases here so a + refactor can't accidentally change a slug shape and break + on-disk filenames in already-deployed installs. + """ + assert helpers.friendly_name_slugify(value) == expected + + +def test_friendly_name_slugify_back_compat_shim(): + """``esphome.dashboard.util.text`` keeps re-exporting for back-compat. + + The function moved to ``esphome.helpers`` so the new + device-builder dashboard backend can import it without depending + on the legacy dashboard package, but downstream code that still + imports from the old path keeps working until the dashboard + module is removed. + """ + from esphome.dashboard.util.text import ( + friendly_name_slugify as legacy_friendly_name_slugify, + ) + + assert legacy_friendly_name_slugify is helpers.friendly_name_slugify + + @pytest.mark.parametrize( "host", ( diff --git a/tests/unit_tests/test_zeroconf.py b/tests/unit_tests/test_zeroconf.py new file mode 100644 index 00000000000..e325eb1e267 --- /dev/null +++ b/tests/unit_tests/test_zeroconf.py @@ -0,0 +1,237 @@ +"""Unit tests for ``esphome.zeroconf`` device-discovery primitives. + +Covers ``DashboardImportDiscovery`` (state transitions for adoption / +import flows) and ``DiscoveredImport`` (TXT-record parse shape). Both +are part of the cross-tool contract between the legacy dashboard and +the new device-builder backend (esphome/device-builder); changes to +the callback signature, the ``import_state`` dict shape, or the +``DiscoveredImport`` field set will break downstream consumers. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from zeroconf import ServiceStateChange + +from esphome.zeroconf import ( + ESPHOME_SERVICE_TYPE, + DashboardImportDiscovery, + DiscoveredImport, +) + + +def _make_service_info( + package_import_url: str = "github://esphome/example/example.yaml", + project_name: str = "esphome.example", + project_version: str = "1.0.0", + network: str | None = "wifi", + friendly_name: str | None = "Living Room", + version: str | None = "2025.1.0", +) -> MagicMock: + """Build a fake ``AsyncServiceInfo`` with the TXT records we care about. + + The real callback path resolves a service via zeroconf and then + reads ``info.properties`` (a ``dict[bytes, bytes | None]``). Mock + that shape so we can drive ``_process_service_info`` directly + without spinning up a real zeroconf instance. + """ + info = MagicMock() + properties: dict[bytes, bytes | None] = { + b"package_import_url": package_import_url.encode(), + b"project_name": project_name.encode(), + b"project_version": project_version.encode(), + } + if network is not None: + properties[b"network"] = network.encode() + if friendly_name is not None: + properties[b"friendly_name"] = friendly_name.encode() + if version is not None: + properties[b"version"] = version.encode() + info.properties = properties + info.load_from_cache.return_value = True + return info + + +def test_added_service_populates_import_state_and_fires_callback() -> None: + """An ADD with the required TXT records lands a ``DiscoveredImport`` and notifies. + + Mirrors what both the legacy dashboard and device-builder rely + on — the callback is the only signal that an importable device + has appeared on the LAN, and ``import_state`` is the snapshot + they read on demand. + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + info = _make_service_info() + name = f"living-room.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + + assert name in discovery.import_state + entry = discovery.import_state[name] + assert isinstance(entry, DiscoveredImport) + assert entry.device_name == "living-room" + assert entry.package_import_url == "github://esphome/example/example.yaml" + assert entry.project_name == "esphome.example" + assert entry.project_version == "1.0.0" + assert entry.network == "wifi" + assert entry.friendly_name == "Living Room" + on_update.assert_called_once_with(name, entry) + + +def test_added_service_without_required_txt_is_ignored() -> None: + """A device that doesn't carry ``package_import_url`` etc. isn't importable. + + The dashboard browser also fires for plain ``_esphomelib._tcp`` + services that happen to match the type but aren't dashboard + imports. Those must not land in ``import_state`` or fire the + update callback — otherwise the dashboard would surface every + API-enabled device on the LAN as "ready to adopt". + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + info = MagicMock() + # Empty TXT records — no import URL, no version. ``version``-only + # services hit a separate ``update_device_mdns`` path that talks + # to ``StorageJSON``; that's covered elsewhere. + info.properties = {} + info.load_from_cache.return_value = True + + discovery._process_service_info(f"plain.{ESPHOME_SERVICE_TYPE}", info) + + assert discovery.import_state == {} + on_update.assert_not_called() + + +def test_repeated_add_does_not_re_fire_callback() -> None: + """Re-resolving the same service doesn't spam the on_update callback. + + The dashboard re-resolves periodically; without the ``is_new`` + guard, every refresh would fire ``IMPORTABLE_DEVICE_ADDED`` and + the dashboard's UI would re-render endlessly. + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + info = _make_service_info() + name = f"living-room.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + discovery._process_service_info(name, info) + + on_update.assert_called_once() + + +def test_removed_service_clears_state_and_fires_none_callback() -> None: + """A ServiceStateChange.Removed pops the entry and notifies with ``None``. + + Both consumers rely on the ``(name, None)`` callback shape to + distinguish "device gone" from "device updated". Coordinate + before changing the second-arg semantics. + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + info = _make_service_info() + name = f"living-room.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + on_update.reset_mock() + + discovery.browser_callback( + zeroconf=MagicMock(), + service_type=ESPHOME_SERVICE_TYPE, + name=name, + state_change=ServiceStateChange.Removed, + ) + + assert name not in discovery.import_state + on_update.assert_called_once_with(name, None) + + +def test_remove_for_unknown_service_does_not_fire_callback() -> None: + """A spurious Removed for a service we never tracked is a silent no-op. + + The browser can fire Removed for any matching service type, + not just the importable ones we're tracking. Don't let those + confuse the callback consumer. + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + discovery.browser_callback( + zeroconf=MagicMock(), + service_type=ESPHOME_SERVICE_TYPE, + name=f"never-seen.{ESPHOME_SERVICE_TYPE}", + state_change=ServiceStateChange.Removed, + ) + + on_update.assert_not_called() + + +def test_updated_service_for_unknown_name_is_ignored() -> None: + """Updates without a prior Add don't seed ``import_state``. + + The dashboard counts on Add to introduce the device and Update + to refresh it. Letting Update silently introduce new state would + let an unrelated TXT change bypass the Add-time validation. + """ + on_update = MagicMock() + discovery = DashboardImportDiscovery(on_update=on_update) + + discovery.browser_callback( + zeroconf=MagicMock(), + service_type=ESPHOME_SERVICE_TYPE, + name=f"living-room.{ESPHOME_SERVICE_TYPE}", + state_change=ServiceStateChange.Updated, + ) + + assert discovery.import_state == {} + on_update.assert_not_called() + + +def test_network_defaults_to_wifi_when_txt_absent() -> None: + """Older firmware that doesn't broadcast ``network`` defaults to ``wifi``. + + The TXT record was added in a later release; pre-existing + factory firmwares advertise without it. ``DiscoveredImport`` + has to default cleanly so adoption flows can still produce a + valid YAML for those devices. + """ + discovery = DashboardImportDiscovery() + info = _make_service_info(network=None) + name = f"older.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + + assert discovery.import_state[name].network == "wifi" + + +def test_friendly_name_optional() -> None: + """``friendly_name`` may be ``None`` if the device doesn't broadcast it. + + Both consumers handle the ``None`` case (rendering the device + name as fallback in the UI). Locking this in keeps the + optionality explicit so a future refactor doesn't accidentally + coerce it into an empty string. + """ + discovery = DashboardImportDiscovery() + info = _make_service_info(friendly_name=None) + name = f"no-friendly.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + + assert discovery.import_state[name].friendly_name is None + + +def test_callback_is_optional() -> None: + """``on_update=None`` lets ``import_state`` track silently. + + Used by callers that read the dict directly rather than + subscribing to events. + """ + discovery = DashboardImportDiscovery(on_update=None) + info = _make_service_info() + name = f"silent.{ESPHOME_SERVICE_TYPE}" + discovery._process_service_info(name, info) + + # No callback to assert against; just verify state landed. + assert name in discovery.import_state From 72a75f2d3f7c2073802e55aee6519f8f6955b66e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:02:07 -0500 Subject: [PATCH 1822/2056] [cover] Fold ControlAction/CoverPublishAction fields into stateless lambdas (#16046) --- esphome/components/cover/__init__.py | 108 ++++++++++++++--- esphome/components/cover/automation.h | 35 +++--- esphome/components/template/cover/__init__.py | 48 +++++--- .../fixtures/cover_control_action.yaml | 111 ++++++++++++++++++ .../integration/test_cover_control_action.py | 92 +++++++++++++++ 5 files changed, 337 insertions(+), 57 deletions(-) create mode 100644 tests/integration/fixtures/cover_control_action.yaml create mode 100644 tests/integration/test_cover_control_action.py diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 41efd2ba7a5..954ad7a3457 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,3 +1,5 @@ +from collections.abc import Callable +from dataclasses import dataclass import logging from esphome import automation @@ -36,14 +38,14 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_device_class, setup_entity, ) -from esphome.cpp_generator import MockObj, MockObjClass +from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass from esphome.types import ConfigType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -68,6 +70,7 @@ _LOGGER = logging.getLogger(__name__) cover_ns = cg.esphome_ns.namespace("cover") Cover = cover_ns.class_("Cover", cg.EntityBase) +CoverCall = cover_ns.class_("CoverCall") COVER_OPEN = cover_ns.COVER_OPEN COVER_CLOSED = cover_ns.COVER_CLOSED @@ -294,25 +297,94 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( ) +@dataclass(frozen=True) +class ApplyField: + """One field in a folded-lambda action. + + `conf_key` is the YAML key looked up in `config`. When present, the + helper emits `statement_fn(target, value_expr)` into the lambda body. + `target` is whatever the statement function needs to identify the + field (typically a setter name like `"set_position"` or a struct + member like `"position"`). `type_` is the C++ return type for + `cg.process_lambda` when the value is a user lambda. + """ + + conf_key: str + target: str + type_: object + + +async def build_apply_lambda_action( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, + fields: tuple[ApplyField, ...], + prefix_args: list[tuple[object, str]], + statement_fn: Callable[[str, str], str], +) -> MockObj: + """Fold configured fields into a single stateless apply lambda action. + + Used by both `cover.control` and `cover.template.publish` (and shared + with the template/cover platform). Constants are emitted as flash + immediates; user lambdas are invoked inline so trigger args still flow. + The trigger arg types are wrapped as `const T &` to match the + `void (*)(..., const Ts &...)` ApplyFn signature. + """ + paren = await cg.get_variable(config[CONF_ID]) + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] + for field in fields: + if (value := config.get(field.conf_key)) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=field.type_) + value_expr = f"({inner})({fwd_args})" + else: + value_expr = str(cg.safe_exp(value)) + body_lines.append(statement_fn(field.target, value_expr)) + + apply_args = [ + *prefix_args, + *((t.operator("const").operator("ref"), n) for t, n in args), + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) + + +# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most +# one is present and both dispatch to set_position. +_COVER_CONTROL_FIELDS: tuple[ApplyField, ...] = ( + ApplyField(CONF_STOP, "set_stop", cg.bool_), + ApplyField(CONF_STATE, "set_position", cg.float_), + ApplyField(CONF_POSITION, "set_position", cg.float_), + ApplyField(CONF_TILT, "set_tilt", cg.float_), +) + + @automation.register_action( "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True ) -async def cover_control_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if (stop := config.get(CONF_STOP)) is not None: - template_ = await cg.templatable(stop, args, cg.bool_) - cg.add(var.set_stop(template_)) - if (state := config.get(CONF_STATE)) is not None: - template_ = await cg.templatable(state, args, cg.float_) - cg.add(var.set_position(template_)) - if (position := config.get(CONF_POSITION)) is not None: - template_ = await cg.templatable(position, args, cg.float_) - cg.add(var.set_position(template_)) - if (tilt := config.get(CONF_TILT)) is not None: - template_ = await cg.templatable(tilt, args, cg.float_) - cg.add(var.set_tilt(template_)) - return var +async def cover_control_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + return await build_apply_lambda_action( + config=config, + action_id=action_id, + template_arg=template_arg, + args=args, + fields=_COVER_CONTROL_FIELDS, + prefix_args=[(CoverCall.operator("ref"), "call")], + statement_fn=lambda setter, expr: f"call.{setter}({expr});", + ) COVER_CONDITION_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index f121e5c2d67..e2384c23593 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -46,48 +46,41 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { Cover *cover_; }; +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. Each action stores only one function pointer +// plus one parent pointer, regardless of how many fields the user set. +// Trigger args are forwarded to the apply function so user lambdas +// (e.g. `position: !lambda "return x;"`) keep working. + template<typename... Ts> class ControlAction : public Action<Ts...> { public: - explicit ControlAction(Cover *cover) : cover_(cover) {} - - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) + using ApplyFn = void (*)(CoverCall &, const Ts &...); + ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { auto call = this->cover_->make_call(); - if (this->stop_.has_value()) - call.set_stop(this->stop_.value(x...)); - if (this->position_.has_value()) - call.set_position(this->position_.value(x...)); - if (this->tilt_.has_value()) - call.set_tilt(this->tilt_.value(x...)); + this->apply_(call, x...); call.perform(); } protected: Cover *cover_; + ApplyFn apply_; }; template<typename... Ts> class CoverPublishAction : public Action<Ts...> { public: - CoverPublishAction(Cover *cover) : cover_(cover) {} - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) + using ApplyFn = void (*)(Cover *, const Ts &...); + CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { - if (this->position_.has_value()) - this->cover_->position = this->position_.value(x...); - if (this->tilt_.has_value()) - this->cover_->tilt = this->tilt_.value(x...); - if (this->current_operation_.has_value()) - this->cover_->current_operation = this->current_operation_.value(x...); + this->apply_(this->cover_, x...); this->cover_->publish_state(); } protected: Cover *cover_; + ApplyFn apply_; }; template<bool OPEN, typename... Ts> class CoverPositionCondition : public Condition<Ts...> { diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index a30c0af3131..7cb50df84c5 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -19,6 +19,9 @@ from esphome.const import ( CONF_TILT_ACTION, CONF_TILT_LAMBDA, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType, TemplateArgsType from .. import template_ns @@ -110,6 +113,16 @@ async def to_code(config): cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) +# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most +# one is present and both map to the position field. +_COVER_PUBLISH_FIELDS: tuple[cover.ApplyField, ...] = ( + cover.ApplyField(CONF_STATE, "position", cg.float_), + cover.ApplyField(CONF_POSITION, "position", cg.float_), + cover.ApplyField(CONF_TILT, "tilt", cg.float_), + cover.ApplyField(CONF_CURRENT_OPERATION, "current_operation", cover.CoverOperation), +) + + @automation.register_action( "cover.template.publish", cover.CoverPublishAction, @@ -126,21 +139,20 @@ async def to_code(config): ), synchronous=True, ) -async def cover_template_publish_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_STATE in config: - template_ = await cg.templatable(config[CONF_STATE], args, cg.float_) - cg.add(var.set_position(template_)) - if CONF_POSITION in config: - template_ = await cg.templatable(config[CONF_POSITION], args, cg.float_) - cg.add(var.set_position(template_)) - if CONF_TILT in config: - template_ = await cg.templatable(config[CONF_TILT], args, cg.float_) - cg.add(var.set_tilt(template_)) - if CONF_CURRENT_OPERATION in config: - template_ = await cg.templatable( - config[CONF_CURRENT_OPERATION], args, cover.CoverOperation - ) - cg.add(var.set_current_operation(template_)) - return var +async def cover_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + # Mutates Cover fields directly (no CoverCall) since publish is a state + # push, not a control request. + return await cover.build_apply_lambda_action( + config=config, + action_id=action_id, + template_arg=template_arg, + args=args, + fields=_COVER_PUBLISH_FIELDS, + prefix_args=[(cover.Cover.operator("ptr"), "cover")], + statement_fn=lambda field, expr: f"cover->{field} = {expr};", + ) diff --git a/tests/integration/fixtures/cover_control_action.yaml b/tests/integration/fixtures/cover_control_action.yaml new file mode 100644 index 00000000000..085d6327963 --- /dev/null +++ b/tests/integration/fixtures/cover_control_action.yaml @@ -0,0 +1,111 @@ +esphome: + name: cover-control-action-test +host: +api: +logger: + level: DEBUG + +globals: + - id: test_position + type: float + initial_value: "0.42" + +cover: + - platform: template + name: "Test Cover" + id: test_cover + has_position: true + optimistic: true + assumed_state: true + open_action: + - cover.template.publish: + id: test_cover + position: 1.0 + close_action: + - cover.template.publish: + id: test_cover + position: 0.0 + stop_action: + - cover.template.publish: + id: test_cover + current_operation: IDLE + tilt_action: + - lambda: |- + // Manually set tilt and publish + id(test_cover).tilt = tilt; + id(test_cover).publish_state(); + +button: + # cover.control: position only + - platform: template + id: btn_position + name: "Set Position" + on_press: + - cover.control: + id: test_cover + position: 50% + + # cover.control: tilt only + - platform: template + id: btn_tilt + name: "Set Tilt" + on_press: + - cover.control: + id: test_cover + tilt: 75% + + # cover.control: position + tilt + - platform: template + id: btn_pos_tilt + name: "Set Pos Tilt" + on_press: + - cover.control: + id: test_cover + position: 25% + tilt: 30% + + # cover.control: state alias for position + - platform: template + id: btn_open_state + name: "Open State" + on_press: + - cover.control: + id: test_cover + state: OPEN + + # cover.control: lambda position (exercises lambda path) + - platform: template + id: btn_lambda_position + name: "Lambda Position" + on_press: + - cover.control: + id: test_cover + position: !lambda "return id(test_position);" + + # cover.template.publish: position only + - platform: template + id: btn_publish_pos + name: "Publish Pos" + on_press: + - cover.template.publish: + id: test_cover + position: 0.6 + + # cover.template.publish: current_operation only + - platform: template + id: btn_publish_op + name: "Publish Op" + on_press: + - cover.template.publish: + id: test_cover + current_operation: OPENING + + # cover.control: stop only — runs after Publish Op so the test can + # verify current_operation transitions OPENING -> IDLE. + - platform: template + id: btn_stop + name: "Stop Cover" + on_press: + - cover.control: + id: test_cover + stop: true diff --git a/tests/integration/test_cover_control_action.py b/tests/integration/test_cover_control_action.py new file mode 100644 index 00000000000..9c7395371bb --- /dev/null +++ b/tests/integration/test_cover_control_action.py @@ -0,0 +1,92 @@ +"""Integration test for cover ControlAction and CoverPublishAction. + +Tests that cover.control and cover.template.publish automation actions +work correctly with the single stateless apply lambda/function pointer +implementation. Exercises multiple field combinations and the lambda path. +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import ButtonInfo, CoverInfo, CoverState, EntityState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_cover_control_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test cover ControlAction/CoverPublishAction with constants and lambdas.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + cover_state_future: asyncio.Future[CoverState] | None = None + + def on_state(state: EntityState) -> None: + if ( + isinstance(state, CoverState) + and cover_state_future is not None + and not cover_state_future.done() + ): + cover_state_future.set_result(state) + + async def wait_for_cover_state(timeout: float = 5.0) -> CoverState: + nonlocal cover_state_future + cover_state_future = loop.create_future() + try: + return await asyncio.wait_for(cover_state_future, timeout) + finally: + cover_state_future = None + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + await initial_state_helper.wait_for_initial_states() + + require_entity(entities, "test_cover", CoverInfo) + + async def press_and_wait(name: str) -> CoverState: + btn = require_entity(entities, name.lower().replace(" ", "_"), ButtonInfo) + client.button_command(btn.key) + return await wait_for_cover_state() + + # cover.control: position only + state = await press_and_wait("Set Position") + assert state.position == pytest.approx(0.5, abs=0.01) + + # cover.control: tilt only + state = await press_and_wait("Set Tilt") + assert state.tilt == pytest.approx(0.75, abs=0.01) + + # cover.control: position + tilt + state = await press_and_wait("Set Pos Tilt") + assert state.position == pytest.approx(0.25, abs=0.01) + assert state.tilt == pytest.approx(0.30, abs=0.01) + + # cover.control: state alias for position 1.0 + state = await press_and_wait("Open State") + assert state.position == pytest.approx(1.0, abs=0.01) + + # cover.control: lambda position (test_position global = 0.42) + state = await press_and_wait("Lambda Position") + assert state.position == pytest.approx(0.42, abs=0.01) + + # cover.template.publish: position only + state = await press_and_wait("Publish Pos") + assert state.position == pytest.approx(0.6, abs=0.01) + + # cover.template.publish: current_operation only + state = await press_and_wait("Publish Op") + # CoverOperation.OPENING == 1 + assert state.current_operation == 1 + + # cover.control: stop only — template cover's stop_action publishes + # current_operation: IDLE. + state = await press_and_wait("Stop Cover") + # CoverOperation.IDLE == 0 + assert state.current_operation == 0 From 844a36f7a101fe5aaf1f32828392cbb89de55037 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:03:52 -0500 Subject: [PATCH 1823/2056] [api] Mark high-volume proxy messages as speed_optimized (Infrared/RF, Z-Wave, serial) (#16159) --- esphome/components/api/api.proto | 3 +++ esphome/components/api/api_pb2.cpp | 24 ++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 391efbd6eb6..4d72be5407b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2511,6 +2511,7 @@ message ZWaveProxyFrame { option (source) = SOURCE_BOTH; option (ifdef) = "USE_ZWAVE_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; bytes data = 1; } @@ -2571,6 +2572,7 @@ message InfraredRFReceiveEvent { option (source) = SOURCE_SERVER; option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY"; option (no_delay) = true; + option (speed_optimized) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance @@ -2627,6 +2629,7 @@ message SerialProxyDataReceived { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SERIAL_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; uint32 instance = 1; // Instance index (0-based) bytes data = 2; // Raw data received from the serial device diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index eb25bf7461a..68be7550eeb 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3784,12 +3784,16 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -uint8_t *ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); return pos; } -uint32_t ZWaveProxyFrame::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +ZWaveProxyFrame::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->data_len); return size; @@ -3910,7 +3914,9 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); #ifdef USE_DEVICES ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); @@ -3921,7 +3927,9 @@ uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DE } return pos; } -uint32_t InfraredRFReceiveEvent::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +InfraredRFReceiveEvent::calculate_size() const { uint32_t size = 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -4001,13 +4009,17 @@ bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_ } return true; } -uint8_t *SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); return pos; } -uint32_t SerialProxyDataReceived::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SerialProxyDataReceived::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->instance); size += ProtoSize::calc_length(1, this->data_len_); From 120d1e51fbe320a79108e1126f206bc90eb161c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:04:34 -0500 Subject: [PATCH 1824/2056] [tests] Fix flaky host_mode_climate_basic_state integration test (#16192) --- .../host_mode_climate_basic_state.yaml | 3 +- tests/integration/state_utils.py | 37 ++++++++++++ .../test_host_mode_climate_basic_state.py | 60 ++++++++++--------- 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/tests/integration/fixtures/host_mode_climate_basic_state.yaml b/tests/integration/fixtures/host_mode_climate_basic_state.yaml index 744b418d0fa..82bf321bbea 100644 --- a/tests/integration/fixtures/host_mode_climate_basic_state.yaml +++ b/tests/integration/fixtures/host_mode_climate_basic_state.yaml @@ -1,5 +1,5 @@ esphome: - name: host-climate-test + name: host-climate-basic-state host: api: logger: @@ -10,6 +10,7 @@ climate: name: Dual-mode Thermostat sensor: host_thermostat_temperature_sensor humidity_sensor: host_thermostat_humidity_sensor + on_boot_restore_from: default_preset humidity_hysteresis: 1.0 min_cooling_off_time: 20s min_cooling_run_time: 20s diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index d42b50ecdb4..c8517aff092 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -8,6 +8,7 @@ import logging from typing import TypeVar from aioesphomeapi import ( + APIClient, BinarySensorState, ButtonInfo, EntityInfo, @@ -19,6 +20,42 @@ from aioesphomeapi import ( _LOGGER = logging.getLogger(__name__) T = TypeVar("T", bound=EntityInfo) +S = TypeVar("S", bound=EntityState) + + +async def wait_for_state( + client: APIClient, + predicate: Callable[[EntityState], bool], + timeout: float = 5.0, +) -> EntityState: + """Subscribe to states and wait for one matching ``predicate``. + + Resolves with the first :class:`EntityState` for which ``predicate`` + returns ``True``. Useful when a component publishes multiple states + during setup (e.g. before sensor readings arrive) and the test needs + to wait for the state to converge to expected values rather than + capturing whichever state happens to arrive first. + + Args: + client: Connected API client. + predicate: Callable invoked for every received state; the first + state for which it returns ``True`` is returned. + timeout: Maximum time to wait in seconds. + + Returns: + The first state matching ``predicate``. + + Raises: + asyncio.TimeoutError: If no matching state arrives within ``timeout``. + """ + future: asyncio.Future[EntityState] = asyncio.get_running_loop().create_future() + + def on_state(state: EntityState) -> None: + if not future.done() and predicate(state): + future.set_result(state) + + client.subscribe_states(on_state) + return await asyncio.wait_for(future, timeout=timeout) def find_entity( diff --git a/tests/integration/test_host_mode_climate_basic_state.py b/tests/integration/test_host_mode_climate_basic_state.py index 7d871ed5a83..0c82d28c3c9 100644 --- a/tests/integration/test_host_mode_climate_basic_state.py +++ b/tests/integration/test_host_mode_climate_basic_state.py @@ -2,11 +2,17 @@ from __future__ import annotations -import aioesphomeapi -from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset +from aioesphomeapi import ( + ClimateAction, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateState, + EntityState, +) import pytest -from .state_utils import InitialStateHelper +from .state_utils import wait_for_state from .types import APIClientConnectedFactory, RunCompiledFunction @@ -18,32 +24,30 @@ async def test_host_mode_climate_basic_state( ) -> None: """Test basic climate state reporting.""" async with run_compiled(yaml_config), api_client_connected() as client: - # Get entities and set up state synchronization - entities, services = await client.list_entities_services() - initial_state_helper = InitialStateHelper(entities) + entities, _ = await client.list_entities_services() climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] assert len(climate_infos) >= 1, "Expected at least 1 climate entity" - - # Subscribe with the wrapper (no-op callback since we just want initial states) - client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) - - # Wait for all initial states to be broadcast - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") - - # Get the climate entity and its initial state test_climate = climate_infos[0] - climate_state = initial_state_helper.initial_states.get(test_climate.key) - assert climate_state is not None, "Climate initial state not found" - assert isinstance(climate_state, aioesphomeapi.ClimateState) - assert climate_state.mode == ClimateMode.OFF - assert climate_state.action == ClimateAction.OFF - assert climate_state.current_temperature == 22.0 - assert climate_state.target_temperature_low == 18.0 - assert climate_state.target_temperature_high == 24.0 - assert climate_state.preset == ClimatePreset.HOME - assert climate_state.current_humidity == 42.0 - assert climate_state.target_humidity == 20.0 + # The thermostat publishes multiple states during setup as the + # temperature/humidity sensors come online. Wait for the state to + # converge to the expected default values rather than relying on + # whichever state happens to arrive first. + def is_default_state(state: EntityState) -> bool: + return ( + isinstance(state, ClimateState) + and state.key == test_climate.key + and state.mode == ClimateMode.OFF + and state.action == ClimateAction.OFF + and state.current_temperature == 22.0 + and state.target_temperature_low == 18.0 + and state.target_temperature_high == 24.0 + and state.preset == ClimatePreset.HOME + and state.current_humidity == 42.0 + and state.target_humidity == 20.0 + ) + + try: + await wait_for_state(client, is_default_state) + except TimeoutError: + pytest.fail("Climate did not converge to expected default state") From 9ddb828da33f4f8bfe68f46b5e55b95e152b8cd5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:05:13 -0500 Subject: [PATCH 1825/2056] [api] Don't tear down log connection on stack-trace decode failure (#16196) --- esphome/components/api/client.py | 61 ++++++++-- tests/unit_tests/components/api/__init__.py | 0 .../unit_tests/components/api/test_client.py | 113 ++++++++++++++++++ 3 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/components/api/__init__.py create mode 100644 tests/unit_tests/components/api/test_client.py diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 312d937f017..d5214ccbf61 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -18,7 +18,7 @@ with warnings.catch_warnings(): import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ -from esphome.core import CORE +from esphome.core import CORE, EsphomeError from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -32,6 +32,52 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +class _LogLineProcessor: + """Feeds incoming log lines to the stack-trace decoder. + + Two responsibilities beyond just calling the decoder: + 1. Catch EsphomeError. on_log runs inside an asyncio protocol + callback; if an exception escapes, the loop tears the transport + down with "Fatal error: protocol.data_received() call failed." + and ReconnectLogic immediately reconnects, the device replays + the same crash trace, and we loop forever. + 2. Disable decoding after the first failure. _decode_pc shells out + to PlatformIO via _run_idedata, which is expensive; a single + crash dump can contain many PC/BT lines and we don't want to + retry the failing subprocess for each one. + """ + + def __init__(self, config: dict[str, Any], platform_handler: Any | None) -> None: + self._config = config + self._platform_handler = platform_handler + self._decode_enabled = True + self.backtrace_state = False + + def process_line(self, raw_line: str) -> None: + if not self._decode_enabled: + return + try: + if self._platform_handler is not None: + self.backtrace_state = self._platform_handler( + self._config, raw_line, self.backtrace_state + ) + else: + self.backtrace_state = process_stacktrace( + self._config, raw_line, backtrace_state=self.backtrace_state + ) + except EsphomeError as exc: + self._decode_enabled = False + self.backtrace_state = False + # _run_idedata raises EsphomeError with no message; fall back + # to a generic explanation when str(exc) is empty. + detail = str(exc) or "build artifacts not found locally" + _LOGGER.warning( + "Crash trace decoding unavailable: %s. " + "Run 'esphome compile' for this device to enable PC decoding.", + detail, + ) + + async def async_run_logs( config: dict[str, Any], addresses: list[str], @@ -61,7 +107,6 @@ async def async_run_logs( addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard - backtrace_state = False # Try platform-specific stacktrace handler first, fall back to generic platform_process_stacktrace = None @@ -71,9 +116,10 @@ async def async_run_logs( except (AttributeError, ImportError): pass + processor = _LogLineProcessor(config, platform_process_stacktrace) + def on_log(msg: SubscribeLogsResponse) -> None: """Handle a new log message.""" - nonlocal backtrace_state time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") @@ -84,14 +130,7 @@ async def async_run_logs( for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) for raw_line in text.splitlines(): - if platform_process_stacktrace: - backtrace_state = platform_process_stacktrace( - config, raw_line, backtrace_state - ) - else: - backtrace_state = process_stacktrace( - config, raw_line, backtrace_state=backtrace_state - ) + processor.process_line(raw_line) # Safe to fall back to plaintext here only for this diagnostics use # case: the stream is one-way from device to client, and this code diff --git a/tests/unit_tests/components/api/__init__.py b/tests/unit_tests/components/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit_tests/components/api/test_client.py b/tests/unit_tests/components/api/test_client.py new file mode 100644 index 00000000000..3970d1ce8b9 --- /dev/null +++ b/tests/unit_tests/components/api/test_client.py @@ -0,0 +1,113 @@ +"""Tests for esphome.components.api.client.""" + +from __future__ import annotations + +from unittest.mock import patch + +from esphome.components.api import client as api_client +from esphome.core import EsphomeError + + +def test_decoder_swallows_esphome_error() -> None: + """A failing stack-trace decode must not propagate. + + on_log runs inside an asyncio protocol callback; if EsphomeError + escapes, the loop reports "Fatal error: protocol.data_received() + call failed.", tears the connection down, and ReconnectLogic loops + forever as the device replays the same crash trace on every + reconnect. + """ + config = {"esphome": {"name": "test"}} + processor = api_client._LogLineProcessor(config, None) + + with patch.object( + api_client, "process_stacktrace", side_effect=EsphomeError("no idedata") + ) as mock_process: + processor.process_line("PC: 0x4010496e") + + assert mock_process.called + assert processor.backtrace_state is False + + +def test_decoder_swallows_platform_handler_error() -> None: + """The same protection must apply to the platform-specific handler.""" + config = {"esphome": {"name": "test"}} + + def platform_handler(_config, _line, _state): + raise EsphomeError("no idedata") + + processor = api_client._LogLineProcessor(config, platform_handler) + processor.process_line("PC: 0x4010496e") + + assert processor.backtrace_state is False + + +def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None: + """_run_idedata raises EsphomeError with no message; the warning + must show a useful explanation rather than empty parens. + """ + config = {"esphome": {"name": "test"}} + processor = api_client._LogLineProcessor(config, None) + + with patch.object(api_client, "process_stacktrace", side_effect=EsphomeError()): + processor.process_line("PC: 0x4010496e") + + warnings = [r.message for r in caplog.records if r.levelname == "WARNING"] + assert any("build artifacts not found locally" in m for m in warnings) + assert not any("()" in m for m in warnings) + + +def test_decoder_short_circuits_after_failure() -> None: + """After one failure, subsequent lines must not retry the decoder. + + _decode_pc shells out to PlatformIO; a crash dump can contain many + PC/BT lines and retrying the failing subprocess for each one would + stall log streaming. + """ + config = {"esphome": {"name": "test"}} + processor = api_client._LogLineProcessor(config, None) + + with patch.object( + api_client, "process_stacktrace", side_effect=EsphomeError("no idedata") + ) as mock_process: + processor.process_line("PC: 0x4010496e") + processor.process_line("BT0: 0x4010496e") + processor.process_line("BT1: 0x401049aa") + + assert mock_process.call_count == 1 + + +def test_decoder_threads_backtrace_state() -> None: + """When decoding succeeds, backtrace_state is threaded across calls.""" + config = {"esphome": {"name": "test"}} + processor = api_client._LogLineProcessor(config, None) + + with patch.object( + api_client, "process_stacktrace", side_effect=[True, False] + ) as mock_process: + processor.process_line(">>>stack>>>") + assert processor.backtrace_state is True + processor.process_line("<<<stack<<<") + assert processor.backtrace_state is False + + assert mock_process.call_args_list[0].kwargs == {"backtrace_state": False} + assert mock_process.call_args_list[1].kwargs == {"backtrace_state": True} + + +def test_decoder_uses_platform_handler_when_provided() -> None: + """The platform handler is preferred over the generic one.""" + config = {"esphome": {"name": "test"}} + calls: list[tuple[object, str, bool]] = [] + + def platform_handler(cfg, line, state): + calls.append((cfg, line, state)) + return True + + processor = api_client._LogLineProcessor(config, platform_handler) + + with patch.object(api_client, "process_stacktrace") as mock_generic: + processor.process_line("BT0: 0x4010496e") + + assert calls == [(config, "BT0: 0x4010496e", False)] + assert mock_generic.called is False + assert processor.backtrace_state is True From 013dee44ebd36cc0fda5d4c10aa15570ce1e2a73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:05:27 -0500 Subject: [PATCH 1826/2056] [binary_sensor] Drop Component from AutorepeatFilter, use self-keyed scheduler (#16191) --- esphome/components/binary_sensor/__init__.py | 6 +- esphome/components/binary_sensor/filter.cpp | 22 ++-- esphome/components/binary_sensor/filter.h | 5 +- .../binary_sensor_autorepeat_filter.yaml | 40 ++++++ .../test_binary_sensor_autorepeat_filter.py | 123 ++++++++++++++++++ 5 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 tests/integration/fixtures/binary_sensor_autorepeat_filter.yaml create mode 100644 tests/integration/test_binary_sensor_autorepeat_filter.py diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index db82290750b..a9a09363fc1 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -148,7 +148,7 @@ DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter) DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter) DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) -AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) +AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter) @@ -282,9 +282,7 @@ async def autorepeat_filter_to_code(config, filter_id): ), ) ] - var = cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings) - await cg.register_component(var, {}) - return var + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings) @register_filter("lambda", LambdaFilter, cv.returning_lambda) diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 0a463ee9a9c..8b882212c8a 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -10,11 +10,6 @@ namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; -// AutorepeatFilter still inherits Component (it schedules two distinct timer -// purposes), so it keeps the (Component *, id) scheduler API. -constexpr uint32_t AUTOREPEAT_TIMING_ID = 0; -constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1; - void Filter::output(bool value) { if (this->next_ == nullptr) { this->parent_->send_state_internal(value); @@ -69,6 +64,10 @@ optional<bool> DelayedOffFilter::new_value(bool value) { optional<bool> InvertFilter::new_value(bool value) { return !value; } // AutorepeatFilterBase +// Two independent timers per instance, keyed off two stable addresses inside +// the filter: `this` for the timing-step timer, `&active_timing_` for the +// on/off timer. Both are unique per instance and don't collide with anything +// else, so the self-keyed scheduler API is sufficient. optional<bool> AutorepeatFilterBase::new_value(bool value) { if (value) { if (this->active_timing_ != 0) @@ -76,8 +75,8 @@ optional<bool> AutorepeatFilterBase::new_value(bool value) { this->next_timing_(); return true; } else { - this->cancel_timeout(AUTOREPEAT_TIMING_ID); - this->cancel_timeout(AUTOREPEAT_ON_OFF_ID); + App.scheduler.cancel_timeout(this); + App.scheduler.cancel_timeout(&this->active_timing_); this->active_timing_ = 0; return false; } @@ -85,8 +84,7 @@ optional<bool> AutorepeatFilterBase::new_value(bool value) { void AutorepeatFilterBase::next_timing_() { if (this->active_timing_ < this->timings_count_) { - this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, - [this]() { this->next_timing_(); }); + App.scheduler.set_timeout(this, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); } if (this->active_timing_ <= this->timings_count_) { this->active_timing_++; @@ -98,12 +96,10 @@ void AutorepeatFilterBase::next_timing_() { void AutorepeatFilterBase::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; this->output(val); - this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, - [this, val]() { this->next_value_(!val); }); + App.scheduler.set_timeout(&this->active_timing_, val ? timing.time_on : timing.time_off, + [this, val]() { this->next_value_(!val); }); } -float AutorepeatFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } - LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 8ff57cab0c4..6887de35e1d 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -84,10 +84,11 @@ struct AutorepeatFilterTiming { /// Non-template base for AutorepeatFilter — all methods in filter.cpp. /// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once. -class AutorepeatFilterBase : public Filter, public Component { +/// The two scheduled timers are keyed off `this` and `&active_timing_`; since the address +/// of `active_timing_` is taken as a scheduler key, the class must not be copied or moved. +class AutorepeatFilterBase : public Filter { public: optional<bool> new_value(bool value) override; - float get_setup_priority() const override; AutorepeatFilterBase(const AutorepeatFilterBase &) = delete; AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete; diff --git a/tests/integration/fixtures/binary_sensor_autorepeat_filter.yaml b/tests/integration/fixtures/binary_sensor_autorepeat_filter.yaml new file mode 100644 index 00000000000..5799ece00cd --- /dev/null +++ b/tests/integration/fixtures/binary_sensor_autorepeat_filter.yaml @@ -0,0 +1,40 @@ +esphome: + name: test-autorepeat-filter + +host: +api: + batch_delay: 0ms # Disable batching to receive every state transition +logger: + level: DEBUG + +binary_sensor: + # The autorepeat filter is applied directly to the template sensor, so each + # write through `binary_sensor.template.publish` runs through the filter + # chain. With the source true the filter must oscillate after `delay`; once + # the source returns to false the filter must cancel both timers and emit a + # final false. + - platform: template + name: "Autorepeat Sensor" + id: autorepeat_sensor + filters: + - autorepeat: + - delay: 200ms + time_off: 100ms + time_on: 100ms + +button: + - platform: template + name: "Press" + id: press_button + on_press: + - binary_sensor.template.publish: + id: autorepeat_sensor + state: true + + - platform: template + name: "Release" + id: release_button + on_press: + - binary_sensor.template.publish: + id: autorepeat_sensor + state: false diff --git a/tests/integration/test_binary_sensor_autorepeat_filter.py b/tests/integration/test_binary_sensor_autorepeat_filter.py new file mode 100644 index 00000000000..443d5293f23 --- /dev/null +++ b/tests/integration/test_binary_sensor_autorepeat_filter.py @@ -0,0 +1,123 @@ +"""Integration test for the binary_sensor autorepeat filter. + +Verifies that the autorepeat filter: + +1. Passes the initial true through unchanged. +2. Begins oscillating after the configured ``delay`` while the source stays true. +3. Stops oscillating and emits a final false when the source goes false. + +This exercises both scheduled timers in ``AutorepeatFilter`` (the per-step +``delay`` timer keyed off the filter ``this`` pointer and the on/off toggle +timer keyed off ``&active_timing_``). +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .state_utils import InitialStateHelper, SensorStateCollector, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_binary_sensor_autorepeat_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Drive the source true and verify the downstream sensor oscillates.""" + collector = SensorStateCollector( + sensor_names=[], + binary_sensor_names=["autorepeat_sensor"], + ) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-autorepeat-filter" + + entities, _ = await client.list_entities_services() + collector.build_key_mapping(entities) + + press_button = require_entity(entities, "press", description="Press button") + release_button = require_entity( + entities, "release", description="Release button" + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states( + initial_state_helper.on_state_wrapper(collector.on_state) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + autorepeat_states = collector.binary_states["autorepeat_sensor"] + + # Press: source becomes true, autorepeat passes the initial true through + # and then oscillates after the configured delay. + # Configured timings: delay=200ms, time_on=100ms, time_off=100ms. + # Expected within ~700ms: + # true (0ms), false (200ms), true (300ms), false (400ms), + # true (500ms), false (600ms) + client.button_command(press_button.key) + + # Wait for at least 5 transitions to verify the oscillation pattern. + oscillation_seen = collector.add_waiter(lambda: len(autorepeat_states) >= 5) + try: + await asyncio.wait_for(oscillation_seen, timeout=2.0) + except TimeoutError: + pytest.fail( + f"Expected at least 5 autorepeat transitions, got {autorepeat_states}" + ) + + assert autorepeat_states[0] is True, ( + f"First transition should be the pass-through true, got {autorepeat_states}" + ) + # After the initial true and the configured delay, the filter must + # toggle false/true/false/... — verify the alternation pattern. + for index, value in enumerate(autorepeat_states): + expected = index % 2 == 0 + assert value is expected, ( + f"Expected alternating values starting with True, " + f"got {autorepeat_states} (mismatch at index {index})" + ) + + # Release: source becomes false, autorepeat must cancel both timers + # and settle on false. If the most recent oscillation was already + # false, the binary sensor will dedup and not emit a new state event; + # if it was true, exactly one final false transition arrives. Either + # way, the steady state must be false and no further toggles should + # arrive after a settle window longer than time_on + time_off. + was_true_before_release = autorepeat_states[-1] is True + before_count = len(autorepeat_states) + client.button_command(release_button.key) + + if was_true_before_release: + settle_seen = collector.add_waiter( + lambda: len(autorepeat_states) > before_count + ) + try: + await asyncio.wait_for(settle_seen, timeout=2.0) + except TimeoutError: + pytest.fail("Timeout waiting for autorepeat to settle to false") + assert autorepeat_states[-1] is False, ( + f"After release, final state should be False, got {autorepeat_states}" + ) + + steady_count = len(autorepeat_states) + await asyncio.sleep(0.5) + assert len(autorepeat_states) == steady_count, ( + f"Expected no further toggles after release, " + f"got {autorepeat_states[steady_count:]}" + ) + assert autorepeat_states[-1] is False, ( + f"Final autorepeat state should be False, got {autorepeat_states}" + ) From 1d631584801b84515f684adffcfe9e607fd0ed78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:05:56 -0500 Subject: [PATCH 1827/2056] [zephyr] Add nRF52 component tests so CI runs on zephyr-only changes (#16188) --- tests/components/zephyr/common.yaml | 8 ++++++++ tests/components/zephyr/test.nrf52-adafruit.yaml | 1 + 2 files changed, 9 insertions(+) create mode 100644 tests/components/zephyr/common.yaml create mode 100644 tests/components/zephyr/test.nrf52-adafruit.yaml diff --git a/tests/components/zephyr/common.yaml b/tests/components/zephyr/common.yaml new file mode 100644 index 00000000000..345042df3b9 --- /dev/null +++ b/tests/components/zephyr/common.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + - lambda: |- + ESP_LOGD("test", "millis=%u micros=%u cycles=%u", + (unsigned) millis(), (unsigned) micros(), + (unsigned) arch_get_cpu_cycle_count()); + delay(1); + delayMicroseconds(1); diff --git a/tests/components/zephyr/test.nrf52-adafruit.yaml b/tests/components/zephyr/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..dade44d145b --- /dev/null +++ b/tests/components/zephyr/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 53c4e6f386a6e25b25684707a136cffe025fc731 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:12:51 -0500 Subject: [PATCH 1828/2056] [tests] Drop duplicate on_boot_restore_from in host_mode_climate_basic_state (#16228) --- tests/integration/fixtures/host_mode_climate_basic_state.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/fixtures/host_mode_climate_basic_state.yaml b/tests/integration/fixtures/host_mode_climate_basic_state.yaml index 82bf321bbea..2bfd63ceff3 100644 --- a/tests/integration/fixtures/host_mode_climate_basic_state.yaml +++ b/tests/integration/fixtures/host_mode_climate_basic_state.yaml @@ -29,10 +29,6 @@ climate: min_temperature: 15.0 max_temperature: 32.0 temperature_step: 0.1 - # Don't restore previous state from flash — this fixture shares the - # `host-climate-test` build dir with host_mode_climate_control.yaml, so a - # prior run of that test could leave the thermostat in HEAT/COOL. - on_boot_restore_from: default_preset default_preset: home preset: - name: "away" From 24d4da1021244e33ddd385aa3194aeef8de4525b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 20:18:28 -0500 Subject: [PATCH 1829/2056] [sensor] Document why TimeoutFilterBase intentionally keeps Component (#16194) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/sensor/filter.h | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 57a2386a7fa..b79bfa17d61 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -413,7 +413,31 @@ class ThrottleWithPriorityNanFilter : public Filter { uint32_t min_time_between_inputs_; }; -// Base class for timeout filters - contains common loop logic +// Base class for timeout filters - contains common loop logic. +// +// Why this intentionally inherits Component (and does NOT use the self-keyed +// `App.scheduler.set_timeout(this, ...)` pattern that the other Filter classes +// migrated to): +// +// Timeout filters re-arm on every input, so on devices with many sensors +// using timeout filters (e.g. multi-LD2450 boards) every armed filter would +// require a live SchedulerItem in RAM at the same time. A SchedulerItem is +// substantially larger than the Component bookkeeping bytes carried by this +// class, so paying the Component cost per filter (one-time, BSS) is cheaper +// than paying for a SchedulerItem per filter (live, while armed). #11922 +// is the original symptom and switchover to the loop-based design; #16173 +// attempted to migrate this onto the scheduler and was closed for exactly +// this reason — even if the scheduler pool were unbounded, RAM per armed +// filter would still be dominated by the SchedulerItem itself, not by +// anything we can shrink in the scheduler. +// +// The loop-based design has additional advantages on top of the RAM win: +// `enable_loop()` / `disable_loop()` partitions the cost away when no +// timeout is armed; while armed, work is a single timestamp compare per +// active filter, with no per-input scheduler cancel/insert path. +// +// Don't try to migrate this class onto the self-keyed scheduler. The math +// doesn't work — at scale, this design is the smaller one. class TimeoutFilterBase : public Filter, public Component { public: void loop() override; From 690a1973460f5492e67bc06ff4606afc5f778c69 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Mon, 4 May 2026 21:07:31 +0200 Subject: [PATCH 1830/2056] [main] Move stacktrace handling out of platformio_api and FlashImage into platform components/util (#16186) --- esphome/__main__.py | 26 ++- esphome/components/api/client.py | 10 +- esphome/components/esp32/__init__.py | 76 +++++++++ esphome/components/esp8266/__init__.py | 115 ++++++++++++++ esphome/platformio_api.py | 150 +----------------- esphome/util.py | 6 + .../unit_tests/components/api/test_client.py | 23 +-- .../components/test_esp_stacktrace.py | 109 +++++++++++++ tests/unit_tests/conftest.py | 13 +- tests/unit_tests/test_main.py | 23 +-- tests/unit_tests/test_platformio_api.py | 100 +----------- 11 files changed, 359 insertions(+), 292 deletions(-) create mode 100644 tests/unit_tests/components/test_esp_stacktrace.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 9ab2dee189a..222e299f6dd 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -63,6 +63,7 @@ from esphome.log import AnsiFore, color, setup_log from esphome.types import ConfigType from esphome.util import ( PICOTOOL_PACKAGE, + FlashImage, detect_rp2040_bootsel, get_picotool_path, get_serial_ports, @@ -586,8 +587,6 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: from aioesphomeapi import LogParser import serial - from esphome import platformio_api - if CONF_LOGGER not in config: _LOGGER.info("Logger is not enabled. Not starting UART logs.") return 1 @@ -602,8 +601,11 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: try: module = importlib.import_module("esphome.components." + CORE.target_platform) process_stacktrace = getattr(module, "process_stacktrace") - except AttributeError: - pass + except (AttributeError, ImportError): + _LOGGER.info( + 'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".', + CORE.target_platform, + ) backtrace_state = False ser = serial.Serial() @@ -646,14 +648,10 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - if process_stacktrace: + if process_stacktrace is not None: backtrace_state = process_stacktrace( config, line, backtrace_state ) - else: - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 @@ -843,22 +841,20 @@ def _make_crystal_freq_callback( def upload_using_esptool( config: ConfigType, port: str, file: str, speed: int ) -> str | int: - from esphome import platformio_api - first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( "upload_speed", os.getenv("ESPHOME_UPLOAD_SPEED", "460800") ) if file is not None: - flash_images = [platformio_api.FlashImage(path=file, offset="0x0")] + flash_images = [FlashImage(path=file, offset="0x0")] else: + from esphome import platformio_api + idedata = platformio_api.get_idedata(config) firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" flash_images = [ - platformio_api.FlashImage( - path=idedata.firmware_bin_path, offset=firmware_offset - ), + FlashImage(path=idedata.firmware_bin_path, offset=firmware_offset), ] for image in idedata.extra_flash_images: if not image.path.is_file(): diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index d5214ccbf61..0a5370cb303 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -19,7 +19,6 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE, EsphomeError -from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -61,10 +60,6 @@ class _LogLineProcessor: self.backtrace_state = self._platform_handler( self._config, raw_line, self.backtrace_state ) - else: - self.backtrace_state = process_stacktrace( - self._config, raw_line, backtrace_state=self.backtrace_state - ) except EsphomeError as exc: self._decode_enabled = False self.backtrace_state = False @@ -114,7 +109,10 @@ async def async_run_logs( module = importlib.import_module("esphome.components." + CORE.target_platform) platform_process_stacktrace = getattr(module, "process_stacktrace") except (AttributeError, ImportError): - pass + _LOGGER.info( + 'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".', + CORE.target_platform, + ) processor = _LogLineProcessor(config, platform_process_stacktrace) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b60dab36349..582721ef736 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -5,6 +5,7 @@ import logging import os from pathlib import Path import re +import subprocess from esphome import yaml_util import esphome.codegen as cg @@ -2515,3 +2516,78 @@ def copy_files(): CORE.relative_build_path(name).write_bytes(content) else: copy_file_if_changed(path, CORE.relative_build_path(name)) + + +def _decode_pc(config, addr): + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + if not idedata.addr2line_path or not idedata.firmware_elf_path: + _LOGGER.debug("decode_pc no addr2line") + return + command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] + try: + translation = subprocess.check_output(command, close_fds=False).decode().strip() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Caught exception for command %s", command, exc_info=1) + return + + if "?? ??:0" in translation: + # Nothing useful + return + translation = translation.replace(" at ??:?", "").replace(":?", "") + _LOGGER.warning("Decoded %s", translation) + + +def _parse_register(config, regex, line): + match = regex.match(line) + if match is not None: + _decode_pc(config, match.group(1)) + + +STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*") +STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_BAD_ALLOC_RE = re.compile( + r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" +) +STACKTRACE_ESP32_BACKTRACE_RE = re.compile( + r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" +) +STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") +# ESP32 crash handler (stored backtrace from previous boot) +STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})") + + +def process_stacktrace(config, line, backtrace_state): + line = line.strip() + + # ESP32 PC/EXCVADDR + _parse_register(config, STACKTRACE_ESP32_PC_RE, line) + _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) + # ESP32-C3 PC/RA + _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) + _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) + + # bad alloc + match = re.match(STACKTRACE_BAD_ALLOC_RE, line) + if match is not None: + _LOGGER.warning( + "Memory allocation of %s bytes failed at %s", match.group(2), match.group(1) + ) + _decode_pc(config, match.group(1)) + + # ESP32 crash handler backtrace (from previous boot) + match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line) + if match is not None: + _decode_pc(config, match.group(1)) + + # ESP32 single-line backtrace + match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line) + if match is not None: + _LOGGER.warning("Found stack trace! Trying to decode it") + for addr in re.finditer(STACKTRACE_ESP32_BACKTRACE_PC_RE, line): + _decode_pc(config, addr.group()) + + return backtrace_state diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 3c2806a307d..b6383653f4e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,6 +1,7 @@ import logging from pathlib import Path import re +import subprocess import esphome.codegen as cg import esphome.config_validation as cv @@ -419,3 +420,117 @@ def copy_files() -> None: remove_float_scanf_file, CORE.relative_build_path("remove_float_scanf.py"), ) + + +# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder +ESP8266_EXCEPTION_CODES = { + 0: "Illegal instruction (Is the flash damaged?)", + 1: "SYSCALL instruction", + 2: "InstructionFetchError: Processor internal physical address or data error during " + "instruction fetch", + 3: "LoadStoreError: Processor internal physical address or data error during load or store", + 4: "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT " + "register", + 5: "Alloca: MOVSP instruction, if caller's registers are not in the register file", + 6: "Integer Divide By Zero", + 7: "reserved", + 8: "Privileged: Attempt to execute a privileged operation when CRING ? 0", + 9: "LoadStoreAlignmentCause: Load or store to an unaligned address", + 10: "reserved", + 11: "reserved", + 12: "InstrPIFDataError: PIF data error during instruction fetch", + 13: "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", + 14: "InstrPIFAddrError: PIF address error during instruction fetch", + 15: "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", + 16: "InstTLBMiss: Error during Instruction TLB refill", + 17: "InstTLBMultiHit: Multiple instruction TLB entries matched", + 18: "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level " + "less than CRING", + 19: "reserved", + 20: "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute " + "that does not permit instruction fetch", + 21: "reserved", + 22: "reserved", + 23: "reserved", + 24: "LoadStoreTLBMiss: Error during TLB refill for a load or store", + 25: "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", + 26: "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less " + "than ", + 27: "reserved", + 28: "Access to invalid address: LOAD (wild pointer?)", + 29: "Access to invalid address: STORE (wild pointer?)", +} + + +def _decode_pc(config, addr): + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + if not idedata.addr2line_path or not idedata.firmware_elf_path: + _LOGGER.debug("decode_pc no addr2line") + return + command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] + try: + translation = subprocess.check_output(command, close_fds=False).decode().strip() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Caught exception for command %s", command, exc_info=1) + return + + if "?? ??:0" in translation: + # Nothing useful + return + translation = translation.replace(" at ??:?", "").replace(":?", "") + _LOGGER.warning("Decoded %s", translation) + + +def _parse_register(config, regex, line): + match = regex.match(line) + if match is not None: + _decode_pc(config, match.group(1)) + + +STACKTRACE_ESP8266_EXCEPTION_TYPE_RE = re.compile(r"[eE]xception \((\d+)\):") +STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})") +STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})") +STACKTRACE_BAD_ALLOC_RE = re.compile( + r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" +) +STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") + + +def process_stacktrace(config, line, backtrace_state): + line = line.strip() + # ESP8266 Exception type + match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) + if match is not None: + code = int(match.group(1)) + _LOGGER.warning( + "Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, "unknown") + ) + + # ESP8266 PC/EXCVADDR + _parse_register(config, STACKTRACE_ESP8266_PC_RE, line) + _parse_register(config, STACKTRACE_ESP8266_EXCVADDR_RE, line) + + # bad alloc + match = re.match(STACKTRACE_BAD_ALLOC_RE, line) + if match is not None: + _LOGGER.warning( + "Memory allocation of %s bytes failed at %s", match.group(2), match.group(1) + ) + _decode_pc(config, match.group(1)) + + # ESP8266 multi-line backtrace + if ">>>stack>>>" in line: + # Start of backtrace + backtrace_state = True + _LOGGER.warning("Found stack trace! Trying to decode it") + elif "<<<stack<<<" in line: + # End of backtrace + backtrace_state = False + + if backtrace_state: + for addr in re.finditer(STACKTRACE_ESP8266_BACKTRACE_PC_RE, line): + _decode_pc(config, addr.group()) + + return backtrace_state diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c0cd048890a..81ff01306a6 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,15 +1,13 @@ -from dataclasses import dataclass import json import logging import os from pathlib import Path import re -import subprocess import sys from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError -from esphome.util import run_external_process +from esphome.util import FlashImage, run_external_process _LOGGER = logging.getLogger(__name__) @@ -140,152 +138,6 @@ def get_idedata(config) -> "IDEData": return idedata -# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder -ESP8266_EXCEPTION_CODES = { - 0: "Illegal instruction (Is the flash damaged?)", - 1: "SYSCALL instruction", - 2: "InstructionFetchError: Processor internal physical address or data error during " - "instruction fetch", - 3: "LoadStoreError: Processor internal physical address or data error during load or store", - 4: "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT " - "register", - 5: "Alloca: MOVSP instruction, if caller's registers are not in the register file", - 6: "Integer Divide By Zero", - 7: "reserved", - 8: "Privileged: Attempt to execute a privileged operation when CRING ? 0", - 9: "LoadStoreAlignmentCause: Load or store to an unaligned address", - 10: "reserved", - 11: "reserved", - 12: "InstrPIFDataError: PIF data error during instruction fetch", - 13: "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", - 14: "InstrPIFAddrError: PIF address error during instruction fetch", - 15: "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", - 16: "InstTLBMiss: Error during Instruction TLB refill", - 17: "InstTLBMultiHit: Multiple instruction TLB entries matched", - 18: "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level " - "less than CRING", - 19: "reserved", - 20: "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute " - "that does not permit instruction fetch", - 21: "reserved", - 22: "reserved", - 23: "reserved", - 24: "LoadStoreTLBMiss: Error during TLB refill for a load or store", - 25: "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", - 26: "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less " - "than ", - 27: "reserved", - 28: "Access to invalid address: LOAD (wild pointer?)", - 29: "Access to invalid address: STORE (wild pointer?)", -} - - -def _decode_pc(config, addr): - idedata = get_idedata(config) - if not idedata.addr2line_path or not idedata.firmware_elf_path: - _LOGGER.debug("decode_pc no addr2line") - return - command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] - try: - translation = subprocess.check_output(command, close_fds=False).decode().strip() - except Exception: # pylint: disable=broad-except - _LOGGER.debug("Caught exception for command %s", command, exc_info=1) - return - - if "?? ??:0" in translation: - # Nothing useful - return - translation = translation.replace(" at ??:?", "").replace(":?", "") - _LOGGER.warning("Decoded %s", translation) - - -def _parse_register(config, regex, line): - match = regex.match(line) - if match is not None: - _decode_pc(config, match.group(1)) - - -STACKTRACE_ESP8266_EXCEPTION_TYPE_RE = re.compile(r"[eE]xception \((\d+)\):") -STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})") -STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})") -STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*") -STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") -STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") -STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") -STACKTRACE_BAD_ALLOC_RE = re.compile( - r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" -) -STACKTRACE_ESP32_BACKTRACE_RE = re.compile( - r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+" -) -STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") -# ESP32 crash handler (stored backtrace from previous boot) -STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})") -STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}") - - -def process_stacktrace(config, line, backtrace_state): - line = line.strip() - # ESP8266 Exception type - match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) - if match is not None: - code = int(match.group(1)) - _LOGGER.warning( - "Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, "unknown") - ) - - # ESP8266 PC/EXCVADDR - _parse_register(config, STACKTRACE_ESP8266_PC_RE, line) - _parse_register(config, STACKTRACE_ESP8266_EXCVADDR_RE, line) - # ESP32 PC/EXCVADDR - _parse_register(config, STACKTRACE_ESP32_PC_RE, line) - _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) - # ESP32-C3 PC/RA - _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) - _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) - - # bad alloc - match = re.match(STACKTRACE_BAD_ALLOC_RE, line) - if match is not None: - _LOGGER.warning( - "Memory allocation of %s bytes failed at %s", match.group(2), match.group(1) - ) - _decode_pc(config, match.group(1)) - - # ESP32 crash handler backtrace (from previous boot) - match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line) - if match is not None: - _decode_pc(config, match.group(1)) - - # ESP32 single-line backtrace - match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line) - if match is not None: - _LOGGER.warning("Found stack trace! Trying to decode it") - for addr in re.finditer(STACKTRACE_ESP32_BACKTRACE_PC_RE, line): - _decode_pc(config, addr.group()) - - # ESP8266 multi-line backtrace - if ">>>stack>>>" in line: - # Start of backtrace - backtrace_state = True - _LOGGER.warning("Found stack trace! Trying to decode it") - elif "<<<stack<<<" in line: - # End of backtrace - backtrace_state = False - - if backtrace_state: - for addr in re.finditer(STACKTRACE_ESP8266_BACKTRACE_PC_RE, line): - _decode_pc(config, addr.group()) - - return backtrace_state - - -@dataclass -class FlashImage: - path: Path - offset: str - - class IDEData: def __init__(self, raw): self.raw = raw diff --git a/esphome/util.py b/esphome/util.py index 73cc3aa5ab6..9d6e995f1f2 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -487,3 +487,9 @@ def get_esp32_arduino_flash_error_help() -> str | None: "https://esphome.io/guides/esp32_arduino_to_idf/\n\n", ) ) + + +@dataclass +class FlashImage: + path: Path + offset: str diff --git a/tests/unit_tests/components/api/test_client.py b/tests/unit_tests/components/api/test_client.py index 3970d1ce8b9..333ef70b223 100644 --- a/tests/unit_tests/components/api/test_client.py +++ b/tests/unit_tests/components/api/test_client.py @@ -4,6 +4,7 @@ from __future__ import annotations from unittest.mock import patch +from esphome.components import esp32 from esphome.components.api import client as api_client from esphome.core import EsphomeError @@ -18,11 +19,11 @@ def test_decoder_swallows_esphome_error() -> None: reconnect. """ config = {"esphome": {"name": "test"}} - processor = api_client._LogLineProcessor(config, None) with patch.object( - api_client, "process_stacktrace", side_effect=EsphomeError("no idedata") + esp32, "process_stacktrace", side_effect=EsphomeError("no idedata") ) as mock_process: + processor = api_client._LogLineProcessor(config, esp32.process_stacktrace) processor.process_line("PC: 0x4010496e") assert mock_process.called @@ -47,9 +48,9 @@ def test_decoder_warning_uses_fallback_for_empty_error(caplog) -> None: must show a useful explanation rather than empty parens. """ config = {"esphome": {"name": "test"}} - processor = api_client._LogLineProcessor(config, None) - with patch.object(api_client, "process_stacktrace", side_effect=EsphomeError()): + with patch.object(esp32, "process_stacktrace", side_effect=EsphomeError()): + processor = api_client._LogLineProcessor(config, esp32.process_stacktrace) processor.process_line("PC: 0x4010496e") warnings = [r.message for r in caplog.records if r.levelname == "WARNING"] @@ -65,11 +66,11 @@ def test_decoder_short_circuits_after_failure() -> None: stall log streaming. """ config = {"esphome": {"name": "test"}} - processor = api_client._LogLineProcessor(config, None) with patch.object( - api_client, "process_stacktrace", side_effect=EsphomeError("no idedata") + esp32, "process_stacktrace", side_effect=EsphomeError("no idedata") ) as mock_process: + processor = api_client._LogLineProcessor(config, esp32.process_stacktrace) processor.process_line("PC: 0x4010496e") processor.process_line("BT0: 0x4010496e") processor.process_line("BT1: 0x401049aa") @@ -80,18 +81,18 @@ def test_decoder_short_circuits_after_failure() -> None: def test_decoder_threads_backtrace_state() -> None: """When decoding succeeds, backtrace_state is threaded across calls.""" config = {"esphome": {"name": "test"}} - processor = api_client._LogLineProcessor(config, None) with patch.object( - api_client, "process_stacktrace", side_effect=[True, False] + esp32, "process_stacktrace", side_effect=[True, False] ) as mock_process: + processor = api_client._LogLineProcessor(config, esp32.process_stacktrace) processor.process_line(">>>stack>>>") assert processor.backtrace_state is True processor.process_line("<<<stack<<<") assert processor.backtrace_state is False - assert mock_process.call_args_list[0].kwargs == {"backtrace_state": False} - assert mock_process.call_args_list[1].kwargs == {"backtrace_state": True} + assert not mock_process.call_args_list[0].args[-1] + assert mock_process.call_args_list[1].args[-1] def test_decoder_uses_platform_handler_when_provided() -> None: @@ -105,7 +106,7 @@ def test_decoder_uses_platform_handler_when_provided() -> None: processor = api_client._LogLineProcessor(config, platform_handler) - with patch.object(api_client, "process_stacktrace") as mock_generic: + with patch.object(esp32, "process_stacktrace") as mock_generic: processor.process_line("BT0: 0x4010496e") assert calls == [(config, "BT0: 0x4010496e", False)] diff --git a/tests/unit_tests/components/test_esp_stacktrace.py b/tests/unit_tests/components/test_esp_stacktrace.py new file mode 100644 index 00000000000..5235f313d62 --- /dev/null +++ b/tests/unit_tests/components/test_esp_stacktrace.py @@ -0,0 +1,109 @@ +"""Tests for ESP32 component.""" + +from pathlib import Path +from unittest.mock import Mock + + +def test_process_stacktrace_esp8266_exception(setup_core: Path, caplog) -> None: + """Test process_stacktrace handles ESP8266 exceptions.""" + from esphome.components.esp8266 import process_stacktrace + + config = {"name": "test"} + + # Test exception type parsing + line = "Exception (28):" + backtrace_state = False + + result = process_stacktrace(config, line, backtrace_state) + + assert "Access to invalid address: LOAD (wild pointer?)" in caplog.text + assert result is False + + +def test_process_stacktrace_esp8266_backtrace( + setup_core: Path, mock_esp8266_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP8266 multi-line backtrace.""" + from esphome.components.esp8266 import process_stacktrace + + config = {"name": "test"} + + # Start of backtrace + line1 = ">>>stack>>>" + state = process_stacktrace(config, line1, False) + assert state is True + + # Backtrace content with addresses + line2 = "40201234 40205678" + state = process_stacktrace(config, line2, state) + assert state is True + assert mock_esp8266_decode_pc.call_count == 2 + + # End of backtrace + line3 = "<<<stack<<<" + state = process_stacktrace(config, line3, state) + assert state is False + + +def test_process_stacktrace_esp32_backtrace( + setup_core: Path, mock_esp32_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP32 single-line backtrace.""" + from esphome.components.esp32 import process_stacktrace + + config = {"name": "test"} + + line = "Backtrace: 0x40081234:0x3ffb1234 0x40085678:0x3ffb5678" + state = process_stacktrace(config, line, False) + + # Should decode both addresses + assert mock_esp32_decode_pc.call_count == 2 + mock_esp32_decode_pc.assert_any_call(config, "40081234") + mock_esp32_decode_pc.assert_any_call(config, "40085678") + assert state is False + + +def test_process_stacktrace_bad_alloc( + setup_core: Path, mock_esp32_decode_pc: Mock, caplog +) -> None: + """Test process_stacktrace handles bad alloc messages.""" + from esphome.components.esp32 import process_stacktrace + + config = {"name": "test"} + + line = "last failed alloc call: 40201234(512)" + state = process_stacktrace(config, line, False) + + assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text + mock_esp32_decode_pc.assert_called_once_with(config, "40201234") + assert state is False + + +def test_process_stacktrace_esp32_crash_handler( + setup_core: Path, mock_esp32_decode_pc: Mock +) -> None: + """Test process_stacktrace handles ESP32 crash handler backtrace lines.""" + from esphome.components.esp32 import process_stacktrace + + config = {"name": "test"} + + # Simulate crash handler log lines as they appear from the API/serial + line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)" + state = process_stacktrace(config, line_pc, False) + # PC line is matched by existing STACKTRACE_ESP32_PC_RE + mock_esp32_decode_pc.assert_called_with(config, "400D1234") + assert state is False + + mock_esp32_decode_pc.reset_mock() + + line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)" + state = process_stacktrace(config, line_bt0, False) + mock_esp32_decode_pc.assert_called_once_with(config, "400D5678") + assert state is False + + mock_esp32_decode_pc.reset_mock() + + line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)" + state = process_stacktrace(config, line_bt1, False) + mock_esp32_decode_pc.assert_called_once_with(config, "42005ABC") + assert state is False diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index dfd4305c4d4..626f4168a60 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -77,9 +77,16 @@ def mock_run_platformio_cli_run() -> Generator[Mock, None, None]: @pytest.fixture -def mock_decode_pc() -> Generator[Mock, None, None]: - """Mock _decode_pc for platformio_api.""" - with patch("esphome.platformio_api._decode_pc") as mock: +def mock_esp32_decode_pc() -> Generator[Mock, None, None]: + """Mock _decode_pc for esp32.""" + with patch("esphome.components.esp32._decode_pc") as mock: + yield mock + + +@pytest.fixture +def mock_esp8266_decode_pc() -> Generator[Mock, None, None]: + """Mock _decode_pc for esp8266.""" + with patch("esphome.components.esp8266._decode_pc") as mock: yield mock diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 798a43a4cea..f8a3ea888e0 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -54,6 +54,7 @@ from esphome.__main__ import ( ) from esphome.address_cache import AddressCache from esphome.bundle import BUNDLE_EXTENSION, BundleFile, BundleResult +from esphome.components import esp32 from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( CONF_API, @@ -85,7 +86,7 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError from esphome.espota2 import OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE -from esphome.util import BootselResult +from esphome.util import BootselResult, FlashImage from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -1181,8 +1182,8 @@ def test_upload_using_esptool_path_conversion( mock_idedata = MagicMock(spec=platformio_api.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ - platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), - platformio_api.FlashImage(path=tmp_path / "partitions.bin", offset="0x8000"), + FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + FlashImage(path=tmp_path / "partitions.bin", offset="0x8000"), ] mock_get_idedata.return_value = mock_idedata @@ -1259,8 +1260,8 @@ def test_upload_using_esptool_skips_missing_extra_flash_images( mock_idedata = MagicMock(spec=platformio_api.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ - platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), - platformio_api.FlashImage(path=missing_path, offset="0x2d0000"), + FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), + FlashImage(path=missing_path, offset="0x2d0000"), ] mock_get_idedata.return_value = mock_idedata @@ -4225,7 +4226,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp( with ( patch("serial.Serial", return_value=mock_serial), - patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch.object(esp32, "process_stacktrace") as mock_bt, ): mock_bt.return_value = False result = run_miniterm(config, "/dev/ttyUSB0", args) @@ -4264,7 +4265,7 @@ def test_run_miniterm_different_chunks_different_timestamps( with ( patch("serial.Serial", return_value=mock_serial), - patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch.object(esp32, "process_stacktrace") as mock_bt, ): mock_bt.return_value = False result = run_miniterm(config, "/dev/ttyUSB0", args) @@ -4295,7 +4296,7 @@ def test_run_miniterm_handles_split_lines() -> None: with ( patch("serial.Serial", return_value=mock_serial), - patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch.object(esp32, "process_stacktrace") as mock_bt, patch("esphome.__main__.safe_print") as mock_print, ): mock_bt.return_value = False @@ -4349,7 +4350,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None: with ( patch("serial.Serial", return_value=mock_serial), patch.object( - platformio_api, + esp32, "process_stacktrace", side_effect=track_backtrace_state, ), @@ -4400,7 +4401,7 @@ def test_run_miniterm_handles_empty_reads( with ( patch("serial.Serial", return_value=mock_serial), - patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch.object(esp32, "process_stacktrace") as mock_bt, ): mock_bt.return_value = False result = run_miniterm(config, "/dev/ttyUSB0", args) @@ -4473,7 +4474,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: with ( patch("serial.Serial", return_value=mock_serial), - patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch.object(esp32, "process_stacktrace") as mock_bt, patch("esphome.__main__.safe_print") as mock_print, patch("esphome.__main__.SERIAL_BUFFER_MAX_SIZE", test_buffer_limit), ): diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index b241622f89d..7a88ec4d9e1 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -13,6 +13,7 @@ import pytest from esphome import platformio_api, platformio_runner from esphome.core import CORE, EsphomeError +from esphome.util import FlashImage def test_idedata_firmware_elf_path(setup_core: Path) -> None: @@ -70,7 +71,7 @@ def test_idedata_extra_flash_images(setup_core: Path) -> None: images = idedata.extra_flash_images assert len(images) == 2 - assert all(isinstance(img, platformio_api.FlashImage) for img in images) + assert all(isinstance(img, FlashImage) for img in images) assert images[0].path == Path("/path/to/bootloader.bin") assert images[0].offset == "0x1000" assert images[1].path == Path("/path/to/partition.bin") @@ -106,7 +107,7 @@ def test_idedata_cc_path(setup_core: Path) -> None: def test_flash_image_dataclass() -> None: """Test FlashImage dataclass stores path and offset correctly.""" - image = platformio_api.FlashImage(path=Path("/path/to/image.bin"), offset="0x10000") + image = FlashImage(path=Path("/path/to/image.bin"), offset="0x10000") assert image.path == Path("/path/to/image.bin") assert image.offset == "0x10000" @@ -708,101 +709,6 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: assert build_dir.exists() -def test_process_stacktrace_esp8266_exception(setup_core: Path, caplog) -> None: - """Test process_stacktrace handles ESP8266 exceptions.""" - config = {"name": "test"} - - # Test exception type parsing - line = "Exception (28):" - backtrace_state = False - - result = platformio_api.process_stacktrace(config, line, backtrace_state) - - assert "Access to invalid address: LOAD (wild pointer?)" in caplog.text - assert result is False - - -def test_process_stacktrace_esp8266_backtrace( - setup_core: Path, mock_decode_pc: Mock -) -> None: - """Test process_stacktrace handles ESP8266 multi-line backtrace.""" - config = {"name": "test"} - - # Start of backtrace - line1 = ">>>stack>>>" - state = platformio_api.process_stacktrace(config, line1, False) - assert state is True - - # Backtrace content with addresses - line2 = "40201234 40205678" - state = platformio_api.process_stacktrace(config, line2, state) - assert state is True - assert mock_decode_pc.call_count == 2 - - # End of backtrace - line3 = "<<<stack<<<" - state = platformio_api.process_stacktrace(config, line3, state) - assert state is False - - -def test_process_stacktrace_esp32_backtrace( - setup_core: Path, mock_decode_pc: Mock -) -> None: - """Test process_stacktrace handles ESP32 single-line backtrace.""" - config = {"name": "test"} - - line = "Backtrace: 0x40081234:0x3ffb1234 0x40085678:0x3ffb5678" - state = platformio_api.process_stacktrace(config, line, False) - - # Should decode both addresses - assert mock_decode_pc.call_count == 2 - mock_decode_pc.assert_any_call(config, "40081234") - mock_decode_pc.assert_any_call(config, "40085678") - assert state is False - - -def test_process_stacktrace_bad_alloc( - setup_core: Path, mock_decode_pc: Mock, caplog -) -> None: - """Test process_stacktrace handles bad alloc messages.""" - config = {"name": "test"} - - line = "last failed alloc call: 40201234(512)" - state = platformio_api.process_stacktrace(config, line, False) - - assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text - mock_decode_pc.assert_called_once_with(config, "40201234") - assert state is False - - -def test_process_stacktrace_esp32_crash_handler( - setup_core: Path, mock_decode_pc: Mock -) -> None: - """Test process_stacktrace handles ESP32 crash handler backtrace lines.""" - config = {"name": "test"} - - # Simulate crash handler log lines as they appear from the API/serial - line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)" - state = platformio_api.process_stacktrace(config, line_pc, False) - # PC line is matched by existing STACKTRACE_ESP32_PC_RE - mock_decode_pc.assert_called_with(config, "400D1234") - assert state is False - - mock_decode_pc.reset_mock() - - line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)" - state = platformio_api.process_stacktrace(config, line_bt0, False) - mock_decode_pc.assert_called_once_with(config, "400D5678") - assert state is False - - mock_decode_pc.reset_mock() - - line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)" - state = platformio_api.process_stacktrace(config, line_bt1, False) - mock_decode_pc.assert_called_once_with(config, "42005ABC") - assert state is False - - def test_patch_file_downloader_succeeds_first_try() -> None: """Test patch_file_downloader succeeds on first attempt.""" mock_exception_cls = type("PackageException", (Exception,), {}) From 7c2a63bf82d7d5c34c475ba4413cc74e2876bae3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 May 2026 08:12:20 +1200 Subject: [PATCH 1831/2056] [api] Use safe_print for log output and fix safe_print bytes-repr fallback (#16160) --- esphome/components/api/client.py | 8 ++- esphome/util.py | 26 +++++-- tests/unit_tests/test_util.py | 116 +++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0a5370cb303..d6150fbd29d 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -19,6 +19,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE, EsphomeError +from esphome.util import safe_print from . import CONF_ENCRYPTION @@ -101,7 +102,6 @@ async def async_run_logs( noise_psk=noise_psk, addresses=addresses, # Pass all addresses for automatic retry ) - dashboard = CORE.dashboard # Try platform-specific stacktrace handler first, fall back to generic platform_process_stacktrace = None @@ -126,7 +126,11 @@ async def async_run_logs( f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" ) for parsed_msg in parse_log_message(text, timestamp): - print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + # safe_print handles the dashboard \033 escaping and falls back + # to backslashreplace encoding on stdouts that can't represent + # the wifi signal-bar block characters (Windows redirected + # cp1252 pipe). + safe_print(parsed_msg) for raw_line in text.splitlines(): processor.process_line(raw_line) diff --git a/esphome/util.py b/esphome/util.py index 9d6e995f1f2..39ce7c0963a 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -94,13 +94,29 @@ def safe_print(message="", end="\n"): except UnicodeEncodeError: pass + # Fall back to the stream's actual encoding (e.g. cp1252 on Windows + # redirected pipes). Use "backslashreplace" so unencodable code points + # like the wifi signal-bar block characters (U+2582..U+2588) become + # readable ``\uXXXX`` escapes, and decode back to ``str`` so ``print`` + # never receives a ``bytes`` object (which would render as a ``b'...'`` + # repr). + encoding = sys.stdout.encoding or "ascii" try: - print(message.encode("utf-8", "backslashreplace"), end=end) + print( + message.encode(encoding, "backslashreplace").decode(encoding), + end=end, + ) + return except UnicodeEncodeError: - try: - print(message.encode("ascii", "backslashreplace"), end=end) - except UnicodeEncodeError: - print("Cannot print line because of invalid locale!") + pass + + try: + print( + message.encode("ascii", "backslashreplace").decode("ascii"), + end=end, + ) + except UnicodeEncodeError: + print("Cannot print line because of invalid locale!") def safe_input(prompt=""): diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index ff58fb13944..581b1aca99a 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -709,3 +709,119 @@ def test_detect_rp2040_bootsel_timeout() -> None: result = util.detect_rp2040_bootsel("/usr/bin/picotool") assert result.device_count == 0 assert result.permission_error is False + + +class TestSafePrint: + """Tests for ``safe_print`` and its UnicodeEncodeError fallback chain.""" + + @pytest.fixture(autouse=True) + def _no_dashboard(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Default ``CORE.dashboard`` to False so each test starts hermetic.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", False) + + def test_prints_plain_message(self, capsys: pytest.CaptureFixture[str]) -> None: + """ASCII-only messages take the fast path through native ``print``.""" + util.safe_print("hello world") + assert capsys.readouterr().out == "hello world\n" + + def test_prints_unicode_on_utf8_stdout( + self, capsys: pytest.CaptureFixture[str] + ) -> None: + """Non-ASCII goes straight through when stdout can encode it.""" + util.safe_print("bars: \u2582\u2584\u2586\u2588") + assert capsys.readouterr().out == "bars: \u2582\u2584\u2586\u2588\n" + + def test_dashboard_escapes_esc_byte( + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + ) -> None: + r"""Dashboard mode escapes raw ``\033`` ESC bytes to literal ``\\033``.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", True) + util.safe_print("\033[0;32mhi\033[0m") + assert capsys.readouterr().out == "\\033[0;32mhi\\033[0m\n" + + def test_fallback_writes_string_not_bytes_repr( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Regression: cp1252 fallback must produce a printable str, not ``b'...'``. + + On Windows, when stdout is a redirected pipe (e.g. the dashboard), + Python uses cp1252, which cannot encode the wifi signal-bar block + characters (U+2582..U+2588). The previous fallback path called + ``print(message.encode(...))`` with a ``bytes`` object, which + Python's ``print`` rendered as a literal ``b'...'`` repr — visible + in the user's dashboard output. The fix re-encodes through the + stream's encoding with ``backslashreplace`` and decodes back to + ``str``. + """ + buf = io.BytesIO() + cp1252_stream = io.TextIOWrapper(buf, encoding="cp1252", errors="strict") + monkeypatch.setattr(sys, "stdout", cp1252_stream) + + util.safe_print("bars: \u2582\u2584\u2586\u2588 done") + cp1252_stream.flush() + output = buf.getvalue().decode("cp1252") + + # Output is a clean line, not the bytes repr. + assert not output.startswith("b'") + assert "b'bars" not in output + # Unencodable codepoints become readable backslash escapes. + assert "\\u2582\\u2584\\u2586\\u2588" in output + # Encodable parts survive unchanged. + assert "bars: " in output + assert " done" in output + assert output.endswith("\n") + + def test_fallback_with_dashboard_escaped_message( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Dashboard ESC escaping + cp1252 fallback compose correctly.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", True) + buf = io.BytesIO() + cp1252_stream = io.TextIOWrapper(buf, encoding="cp1252", errors="strict") + monkeypatch.setattr(sys, "stdout", cp1252_stream) + + util.safe_print("\033[0;32m\u2582\u2584\u2586\u2588\033[0m") + cp1252_stream.flush() + output = buf.getvalue().decode("cp1252") + + # Dashboard escaping turned ESC into literal "\033" (5 chars), which + # cp1252 can encode, so it survives the round-trip verbatim. + assert "\\033[0;32m" in output + assert "\\033[0m" in output + # Block characters became backslash escapes via backslashreplace. + assert "\\u2582\\u2584\\u2586\\u2588" in output + + def test_final_message_when_locale_is_invalid( + self, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + ) -> None: + """If every encoding path fails, surface the locale-error sentinel.""" + original_print = print + call_count = 0 + + def fake_print(*args: Any, **kwargs: Any) -> None: + nonlocal call_count + call_count += 1 + # The first three calls are: native print, stream-encoding + # fallback, ASCII fallback. Make all three raise so we reach + # the final sentinel "Cannot print line..." which is expected + # to succeed (no encoding required). + if call_count <= 3: + raise UnicodeEncodeError("ascii", "x", 0, 1, "boom") + original_print(*args, **kwargs) + + monkeypatch.setattr("builtins.print", fake_print) + util.safe_print("x") + assert call_count == 4 + assert ( + capsys.readouterr().out == "Cannot print line because of invalid locale!\n" + ) From a460f5343cf8a350d34e25f159a1b54295a85a65 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 28 Apr 2026 05:27:20 -0500 Subject: [PATCH 1832/2056] [automation] Fix codegen type for component.resume update_interval (#16069) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/automation.py b/esphome/automation.py index b4dcc419950..bfbfd58d8ac 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -598,7 +598,7 @@ async def component_resume_action_to_code( comp = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, comp) if CONF_UPDATE_INTERVAL in config: - template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int) + template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32) cg.add(var.set_update_interval(template_)) return var From ce466c6b60fa086e323c8b7b65b2ffccb4a5070d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 29 Apr 2026 18:14:07 -0500 Subject: [PATCH 1833/2056] [mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#16149) --- esphome/components/mcp23xxx_base/__init__.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index cd952099c05..76a3aabe3fa 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -2,6 +2,7 @@ from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ALLOW_OTHER_USES, CONF_ID, CONF_INPUT, CONF_INTERRUPT, @@ -30,10 +31,29 @@ MCP23XXX_INTERRUPT_MODES = { "FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING, } + +def _validate_interrupt_pin(value): + # The MCP component owns INT polarity (active-low, hardcoded falling-edge ISR) + # and installs a single ISR per GPIO, so neither inversion nor sharing is supported. + value = pins.internal_gpio_input_pin_schema(value) + if value.get(CONF_INVERTED): + raise cv.Invalid( + f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; " + "the MCP23xxx INT line is fixed active-low" + ) + if value.get(CONF_ALLOW_OTHER_USES): + raise cv.Invalid( + f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; " + "sharing the interrupt pin between multiple MCP23xxx (or other components) " + "is not implemented. Remove the interrupt_pin to fall back to polling." + ) + return value + + MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): _validate_interrupt_pin, } ).extend(cv.COMPONENT_SCHEMA) From 9371ec319a507bd0dfb185fbe65181370711e7b4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:04:52 +1200 Subject: [PATCH 1834/2056] [core] Strip \\?\ prefix from sys.executable for PlatformIO subprocess (#16158) --- esphome/platformio_api.py | 44 ++++++++++- tests/unit_tests/test_platformio_api.py | 99 +++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index fc21977fdd1..544bca3b942 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -53,6 +53,37 @@ FILTER_PLATFORMIO_LINES = [ ] +def _strip_win_long_path_prefix(path: str) -> str: + r"""Strip the Windows extended-length path prefix from ``path``. + + Handles both forms documented at + https://learn.microsoft.com/windows/win32/fileio/naming-a-file: + + * ``\\?\C:\path\to\file`` -> ``C:\path\to\file`` + * ``\\?\UNC\server\share\path`` -> ``\\server\share\path`` + + The NSIS-installed ``esphome.exe`` launcher on Windows starts Python with + ``sys.executable`` already prefixed with ``\\?\``. That prefix propagates + into PlatformIO's ``$PYTHONEXE`` (PlatformIO reads ``PYTHONEXEPATH`` from + the environment, falling back to ``os.path.normpath(sys.executable)``) + and ends up baked into SCons-emitted command lines for build steps such + as the esp8266 ``elf2bin`` invocation. ``cmd.exe`` does not understand + the ``\\?\`` prefix, so the build fails with + "The system cannot find the path specified." Stripping the prefix early + keeps the path shell-quotable. + + No-op on non-Windows platforms. + """ + if sys.platform != "win32": + return path + if path.startswith("\\\\?\\UNC\\"): + # \\?\UNC\server\share\... -> \\server\share\... + return "\\\\" + path[len("\\\\?\\UNC\\") :] + if path.startswith("\\\\?\\"): + return path[len("\\\\?\\") :] + return path + + def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -63,7 +94,18 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") # Increase uv retry count to handle transient network errors (default is 3) os.environ.setdefault("UV_HTTP_RETRIES", "10") - cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args) + # Strip the Windows extended-length path prefix from sys.executable so it + # doesn't propagate into PlatformIO's $PYTHONEXE and break SCons-emitted + # command lines run through cmd.exe. + python_exe = _strip_win_long_path_prefix(sys.executable) + if python_exe != sys.executable: + # Only override PYTHONEXEPATH when we actually stripped a prefix. + # PlatformIO's get_pythonexe_path() reads this and falls back to + # sys.executable otherwise; setting it unconditionally would clobber + # a user-provided value (or the unmodified path on platforms that + # don't need the strip). + os.environ["PYTHONEXEPATH"] = python_exe + cmd = [python_exe, "-m", "esphome.platformio_runner"] + list(args) return run_external_process(*cmd, **kwargs) diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index ddc4e45c844..a92e00167d4 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -311,6 +311,105 @@ def test_run_platformio_cli_sets_environment_variables( assert "arg" in args +@pytest.mark.parametrize( + ("platform", "input_path", "expected"), + [ + # win32: drive-letter extended-length prefix is stripped + ( + "win32", + "\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + ), + # win32: UNC extended-length prefix is translated to a regular UNC path + ( + "win32", + "\\\\?\\UNC\\server\\share\\python.exe", + "\\\\server\\share\\python.exe", + ), + # win32: paths without the prefix are returned unchanged + ( + "win32", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe", + ), + # non-win32: prefix is left alone (no-op) + ("linux", "\\\\?\\C:\\python.exe", "\\\\?\\C:\\python.exe"), + ("darwin", "/usr/bin/python3", "/usr/bin/python3"), + ], +) +def test_strip_win_long_path_prefix( + platform: str, input_path: str, expected: str +) -> None: + r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32.""" + with patch("esphome.platformio_api.sys.platform", platform): + assert platformio_api._strip_win_long_path_prefix(input_path) == expected + + +def test_run_platformio_cli_strips_win_long_path_prefix( + setup_core: Path, mock_run_external_process: Mock +) -> None: + r"""Windows ``\\?\`` prefix on sys.executable does not leak into the subprocess. + + The NSIS-installed esphome.exe launcher starts Python with + ``sys.executable`` already prefixed by the extended-length path marker. + That prefix would otherwise propagate into PlatformIO's ``PYTHONEXE`` and + break SCons-emitted command lines run through ``cmd.exe``. + """ + CORE.build_path = str(setup_core / "build" / "test") + prefixed_exe = ( + "\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe" + ) + stripped_exe = ( + "C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe" + ) + + with ( + patch.dict(os.environ, {}, clear=False), + patch("esphome.platformio_api.sys.platform", "win32"), + patch("esphome.platformio_api.sys.executable", prefixed_exe), + ): + # Pop any pre-existing PYTHONEXEPATH so the assertion below reflects + # what run_platformio_cli set, not whatever the test runner's + # environment happened to contain. + os.environ.pop("PYTHONEXEPATH", None) + mock_run_external_process.return_value = 0 + platformio_api.run_platformio_cli("test", "arg") + + # The subprocess is invoked with the stripped executable path. + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert args[0] == stripped_exe + # PYTHONEXEPATH is exported with the stripped path so PlatformIO's + # get_pythonexe_path() picks it up in the subprocess. + assert os.environ["PYTHONEXEPATH"] == stripped_exe + + +def test_run_platformio_cli_does_not_set_pythonexepath_without_strip( + setup_core: Path, mock_run_external_process: Mock +) -> None: + r"""PYTHONEXEPATH is not touched when sys.executable has no ``\\?\`` prefix. + + Setting it unconditionally would clobber a user-provided value (or + interfere with non-Windows tooling that has no prefix to strip). + """ + CORE.build_path = str(setup_core / "build" / "test") + plain_exe = "/usr/bin/python3" + + with ( + patch.dict(os.environ, {}, clear=False), + patch("esphome.platformio_api.sys.platform", "linux"), + patch("esphome.platformio_api.sys.executable", plain_exe), + ): + os.environ.pop("PYTHONEXEPATH", None) + mock_run_external_process.return_value = 0 + platformio_api.run_platformio_cli("test", "arg") + + mock_run_external_process.assert_called_once() + args = mock_run_external_process.call_args[0] + assert args[0] == plain_exe + assert "PYTHONEXEPATH" not in os.environ + + def test_run_platformio_cli_run_builds_command( setup_core: Path, mock_run_platformio_cli: Mock ) -> None: From 60a94fd10972681f28772716103d0ca46177104d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 30 Apr 2026 12:27:54 -0500 Subject: [PATCH 1835/2056] [esp32] Replace 512B stack buffer in printf wraps with picolibc cookie FILE (#16170) --- esphome/components/esp32/printf_stubs.cpp | 77 +++++++++++++++++++---- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index 386fbbd79d1..908b4023ead 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -13,14 +13,21 @@ * and printf() calls in SDK components are only in debug/assert paths * (gpio_dump_io_configuration, ringbuf diagnostics) that are either * GC'd or never called. Crash backtraces and panic output are - * unaffected — they use esp_rom_printf() which is a ROM function + * unaffected; they use esp_rom_printf() which is a ROM function * and does not go through libc. * - * These stubs redirect through vsnprintf() (which uses _svfprintf_r - * already in the binary) and fwrite(), allowing the linker to - * dead-code eliminate _vfprintf_r. + * On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we + * route output through a stack-allocated cookie FILE that forwards each + * byte to the real target stream via fputc(). Picolibc's tinystdio + * vfprintf walks the FILE::put callback one character at a time, so this + * costs ~32 bytes of stack for the cookie struct vs. a 512-byte format + * buffer. The buffered path overflows the loopTask stack on IDF 6. * - * Saves ~11 KB of flash. + * On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite + * path because that loopTask stack budget has plenty of headroom for the + * 512-byte buffer; the picolibc-only crash above does not affect it. + * + * Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc. * * To disable these wraps, set enable_full_printf: true in the esp32 * advanced config section. @@ -30,10 +37,55 @@ #include <cstdarg> #include <cstdio> +#ifndef __PICOLIBC__ #include "esp_system.h" +#endif namespace esphome::esp32 {} +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +#ifdef __PICOLIBC__ + +#include <cstddef> +#include <type_traits> + +extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap); + +namespace { + +struct CookieFile { + FILE base; + FILE *target; +}; + +// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is +// only well-defined when FILE is the first member at offset 0 and CookieFile +// is standard-layout. +static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile"); +static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout"); + +int cookie_put(char c, FILE *stream) { + auto *cookie = reinterpret_cast<CookieFile *>(stream); + return fputc(static_cast<unsigned char>(c), cookie->target); +} + +const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE); + +} // namespace + +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + CookieFile cookie; + cookie.base = COOKIE_FILE_TEMPLATE; + cookie.target = stream; + return __real_vfprintf(&cookie.base, fmt, ap); +} + +int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); } + +#else // !__PICOLIBC__ + static constexpr size_t PRINTF_BUFFER_SIZE = 512; // These stubs are essentially dead code at runtime — ESPHome replaces the @@ -55,14 +107,18 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) { return len; } -// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) -extern "C" { - int __wrap_vprintf(const char *fmt, va_list ap) { char buf[PRINTF_BUFFER_SIZE]; return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); } +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +#endif // __PICOLIBC__ + int __wrap_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); @@ -71,11 +127,6 @@ int __wrap_printf(const char *fmt, ...) { return len; } -int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { - char buf[PRINTF_BUFFER_SIZE]; - return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); -} - int __wrap_fprintf(FILE *stream, const char *fmt, ...) { va_list ap; va_start(ap, fmt); From d9c22d6b56d80adbd9cf767244dc98b0dc4af4de Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 1 May 2026 12:23:14 +1000 Subject: [PATCH 1836/2056] [lvgl] Clamp values for meter line indicators (#16180) --- esphome/components/lvgl/lvgl_esphome.cpp | 6 ++++-- esphome/components/lvgl/lvgl_esphome.h | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index d8248e4aa4e..722a7a1b022 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -454,10 +454,12 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { #ifdef USE_LVGL_METER -int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) { +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value) { auto *scale = lv_obj_get_parent(obj); auto min_value = lv_scale_get_range_min_value(scale); - return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) + + auto max_value = lv_scale_get_range_max_value(scale); + value = clamp(value, min_value, max_value); + return ((value - min_value) * lv_scale_get_angle_range(scale) / (max_value - min_value) + lv_scale_get_rotation((scale))) % 360; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 146866f5bd7..8c0b10e1bcf 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -112,7 +112,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images #endif // USE_LVGL_ANIMIMG #ifdef USE_LVGL_METER -int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value); +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); #endif // Parent class for things that wrap an LVGL object From 0418f2138af5ba4057708e91d06dd015a0d9d4d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 1 May 2026 14:31:56 -0500 Subject: [PATCH 1837/2056] [esp32] Drop printf wrap on IDF 6.0+ (picolibc no longer needs it) (#16189) --- esphome/components/esp32/__init__.py | 12 +++- esphome/components/esp32/printf_stubs.cpp | 85 ++++++----------------- 2 files changed, 31 insertions(+), 66 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 77b405a4497..30acbc3e412 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1740,7 +1740,17 @@ async def to_code(config): # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r # (~11 KB). See printf_stubs.cpp for implementation. - if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + # + # The wrap is only beneficial against newlib. Picolibc's tinystdio + # implements vsnprintf by building a string-output FILE and calling + # vfprintf, so vfprintf is unconditionally linked in by any caller + # of snprintf/vsnprintf — effectively every build — and the wrap + # saves nothing while costing ~170 B of shim. IDF 5.x defaults to + # newlib on every variant; IDF 6.0+ switches to picolibc on every + # variant. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or idf_version() >= cv.Version( + 6, 0, 0 + ): cg.add_define("USE_FULL_PRINTF") else: for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp index 908b4023ead..489c5039429 100644 --- a/esphome/components/esp32/printf_stubs.cpp +++ b/esphome/components/esp32/printf_stubs.cpp @@ -1,91 +1,48 @@ /* - * Linker wrap stubs for FILE*-based printf functions. + * Linker wrap stubs for FILE*-based printf functions (newlib only). * * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference - * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full - * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on - * picolibc's vfprintf). This is a separate implementation from the one - * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering - * and locking. + * fprintf(), printf(), vprintf(), and vfprintf(), which on newlib pull + * in _vfprintf_r (~11 KB) — a separate implementation from the one used + * by snprintf/vsnprintf that handles FILE* stream I/O with buffering. * * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), * so the SDK's vprintf() path is dead code at runtime. The fprintf() * and printf() calls in SDK components are only in debug/assert paths * (gpio_dump_io_configuration, ringbuf diagnostics) that are either * GC'd or never called. Crash backtraces and panic output are - * unaffected; they use esp_rom_printf() which is a ROM function - * and does not go through libc. + * unaffected; they use esp_rom_printf() which is a ROM function and + * does not go through libc. * - * On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we - * route output through a stack-allocated cookie FILE that forwards each - * byte to the real target stream via fputc(). Picolibc's tinystdio - * vfprintf walks the FILE::put callback one character at a time, so this - * costs ~32 bytes of stack for the cookie struct vs. a 512-byte format - * buffer. The buffered path overflows the loopTask stack on IDF 6. + * This wrap is newlib-only. On picolibc, vsnprintf is implemented as + * vfprintf into a string-output FILE, so vfprintf is unconditionally + * linked in by any caller of snprintf/vsnprintf and the wrap can never + * elide it — it just adds shim cost. Codegen forces USE_FULL_PRINTF + * on picolibc builds (IDF 6.0+ on all variants) so this file compiles + * to nothing there; the #error below catches a desynchronised gate. * - * On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite - * path because that loopTask stack budget has plenty of headroom for the - * 512-byte buffer; the picolibc-only crash above does not affect it. + * Saves ~11 KB of flash on newlib. * - * Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc. - * - * To disable these wraps, set enable_full_printf: true in the esp32 - * advanced config section. + * To disable this wrap on newlib, set enable_full_printf: true in the + * esp32 advanced config section. */ #if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) + +#ifdef __PICOLIBC__ +#error "printf wrap is net-negative on picolibc; codegen should set USE_FULL_PRINTF" +#endif + #include <cstdarg> #include <cstdio> -#ifndef __PICOLIBC__ #include "esp_system.h" -#endif namespace esphome::esp32 {} // NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) extern "C" { -#ifdef __PICOLIBC__ - -#include <cstddef> -#include <type_traits> - -extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap); - -namespace { - -struct CookieFile { - FILE base; - FILE *target; -}; - -// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is -// only well-defined when FILE is the first member at offset 0 and CookieFile -// is standard-layout. -static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile"); -static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout"); - -int cookie_put(char c, FILE *stream) { - auto *cookie = reinterpret_cast<CookieFile *>(stream); - return fputc(static_cast<unsigned char>(c), cookie->target); -} - -const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE); - -} // namespace - -int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { - CookieFile cookie; - cookie.base = COOKIE_FILE_TEMPLATE; - cookie.target = stream; - return __real_vfprintf(&cookie.base, fmt, ap); -} - -int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); } - -#else // !__PICOLIBC__ - static constexpr size_t PRINTF_BUFFER_SIZE = 512; // These stubs are essentially dead code at runtime — ESPHome replaces the @@ -117,8 +74,6 @@ int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); } -#endif // __PICOLIBC__ - int __wrap_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); From be84e6c9f48294c5e231e01ea21e7fadf562c815 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 3 May 2026 17:55:40 -0500 Subject: [PATCH 1838/2056] [api] Fall back to owning types for service array args used after a delay (#16140) --- esphome/components/api/__init__.py | 45 ++++++++++++++++++++------- tests/components/api/common-base.yaml | 18 +++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 84589d540d3..623da2247ef 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -72,17 +72,35 @@ APIUnregisterServiceCallAction = api_ns.class_( UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") -SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { +# Owning element type for each YAML service variable type. Used to derive both +# the zero-copy native types and the owning fallback types below. +_SERVICE_ARG_SCALAR_TYPES: dict[str, MockObj] = { "bool": cg.bool_, "int": cg.int32, "float": cg.float_, + "string": cg.std_string, +} +SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { + # Scalars are passed by value; string uses a non-owning view into rx_buf_. + **_SERVICE_ARG_SCALAR_TYPES, "string": cg.StringRef, - "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), - "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), - "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), - "string[]": cg.FixedVector.template(cg.std_string) - .operator("const") - .operator("ref"), + # Arrays are passed as non-owning const references into rx_buf_. + **{ + f"{name}[]": cg.FixedVector.template(t).operator("const").operator("ref") + for name, t in _SERVICE_ARG_SCALAR_TYPES.items() + }, +} +# Owning fallback types used when the action chain contains non-synchronous actions +# (delay, wait_until, script.wait, etc.). The default non-owning types reference +# storage in the receive buffer, which is reused once the synchronous portion of +# the chain returns. FixedVector is also non-copyable, so the deferred lambda +# capture in DelayAction::play_complex would fail to compile. +SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = { + "string": cg.std_string, + **{ + f"{name}[]": cg.std_vector.template(t) + for name, t in _SERVICE_ARG_SCALAR_TYPES.items() + }, } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" @@ -382,17 +400,20 @@ async def to_code(config: ConfigType) -> None: func_args.append((cg.bool_, "return_response")) # Check if action chain has non-synchronous actions that would make - # non-owning StringRef dangle (rx_buf_ reused after delay) + # non-owning args (StringRef, const FixedVector&) dangle once the + # rx_buf_ is reused after a delay/wait_until/script.wait/etc. The + # FixedVector references would also fail to compile because they + # are non-copyable and DelayAction captures args by value. has_non_synchronous = automation.has_non_synchronous_actions( conf.get(CONF_THEN, []) ) service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - # Fall back to std::string for string args if non-synchronous actions exist - if has_non_synchronous and native is cg.StringRef: - native = cg.std_string + if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES: + native = SERVICE_ARG_FALLBACK_TYPES[var_] + else: + native = SERVICE_ARG_NATIVE_TYPES[var_] service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c766b61b13f..504c52a57b4 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -91,6 +91,24 @@ api: - float_arr.size() - string_arr[0].c_str() - string_arr.size() + # Test array + string args used after a non-synchronous action (delay). + # The default non-owning types (StringRef, const FixedVector&) would + # dangle once rx_buf_ is reused, and FixedVector is non-copyable so + # DelayAction's lambda capture would fail to compile. The api codegen + # must fall back to owning std::string / std::vector here. + - action: array_with_delay + variables: + name: string + int_arr: int[] + string_arr: string[] + then: + - delay: 20ms + - logger.log: + format: "Delayed: %s (%u ints, %u strings)" + args: + - name.c_str() + - int_arr.size() + - string_arr.size() # Test ContinuationAction (IfAction with then/else branches) - action: test_if_action variables: From 2d7f9dc48d9d072d0f3c291f31fa83d9d4e6604c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 May 2026 08:12:20 +1200 Subject: [PATCH 1839/2056] [api] Use safe_print for log output and fix safe_print bytes-repr fallback (#16160) --- esphome/components/api/client.py | 8 ++- esphome/util.py | 26 +++++-- tests/unit_tests/test_util.py | 116 +++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 0c6c569c7d6..0982ca905bd 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -20,6 +20,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE from esphome.platformio_api import process_stacktrace +from esphome.util import safe_print from . import CONF_ENCRYPTION @@ -60,7 +61,6 @@ async def async_run_logs( noise_psk=noise_psk, addresses=addresses, # Pass all addresses for automatic retry ) - dashboard = CORE.dashboard backtrace_state = False # Try platform-specific stacktrace handler first, fall back to generic @@ -82,7 +82,11 @@ async def async_run_logs( f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" ) for parsed_msg in parse_log_message(text, timestamp): - print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + # safe_print handles the dashboard \033 escaping and falls back + # to backslashreplace encoding on stdouts that can't represent + # the wifi signal-bar block characters (Windows redirected + # cp1252 pipe). + safe_print(parsed_msg) for raw_line in text.splitlines(): if platform_process_stacktrace: backtrace_state = platform_process_stacktrace( diff --git a/esphome/util.py b/esphome/util.py index 73cc3aa5ab6..e96a52de868 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -94,13 +94,29 @@ def safe_print(message="", end="\n"): except UnicodeEncodeError: pass + # Fall back to the stream's actual encoding (e.g. cp1252 on Windows + # redirected pipes). Use "backslashreplace" so unencodable code points + # like the wifi signal-bar block characters (U+2582..U+2588) become + # readable ``\uXXXX`` escapes, and decode back to ``str`` so ``print`` + # never receives a ``bytes`` object (which would render as a ``b'...'`` + # repr). + encoding = sys.stdout.encoding or "ascii" try: - print(message.encode("utf-8", "backslashreplace"), end=end) + print( + message.encode(encoding, "backslashreplace").decode(encoding), + end=end, + ) + return except UnicodeEncodeError: - try: - print(message.encode("ascii", "backslashreplace"), end=end) - except UnicodeEncodeError: - print("Cannot print line because of invalid locale!") + pass + + try: + print( + message.encode("ascii", "backslashreplace").decode("ascii"), + end=end, + ) + except UnicodeEncodeError: + print("Cannot print line because of invalid locale!") def safe_input(prompt=""): diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py index ff58fb13944..581b1aca99a 100644 --- a/tests/unit_tests/test_util.py +++ b/tests/unit_tests/test_util.py @@ -709,3 +709,119 @@ def test_detect_rp2040_bootsel_timeout() -> None: result = util.detect_rp2040_bootsel("/usr/bin/picotool") assert result.device_count == 0 assert result.permission_error is False + + +class TestSafePrint: + """Tests for ``safe_print`` and its UnicodeEncodeError fallback chain.""" + + @pytest.fixture(autouse=True) + def _no_dashboard(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Default ``CORE.dashboard`` to False so each test starts hermetic.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", False) + + def test_prints_plain_message(self, capsys: pytest.CaptureFixture[str]) -> None: + """ASCII-only messages take the fast path through native ``print``.""" + util.safe_print("hello world") + assert capsys.readouterr().out == "hello world\n" + + def test_prints_unicode_on_utf8_stdout( + self, capsys: pytest.CaptureFixture[str] + ) -> None: + """Non-ASCII goes straight through when stdout can encode it.""" + util.safe_print("bars: \u2582\u2584\u2586\u2588") + assert capsys.readouterr().out == "bars: \u2582\u2584\u2586\u2588\n" + + def test_dashboard_escapes_esc_byte( + self, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + ) -> None: + r"""Dashboard mode escapes raw ``\033`` ESC bytes to literal ``\\033``.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", True) + util.safe_print("\033[0;32mhi\033[0m") + assert capsys.readouterr().out == "\\033[0;32mhi\\033[0m\n" + + def test_fallback_writes_string_not_bytes_repr( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Regression: cp1252 fallback must produce a printable str, not ``b'...'``. + + On Windows, when stdout is a redirected pipe (e.g. the dashboard), + Python uses cp1252, which cannot encode the wifi signal-bar block + characters (U+2582..U+2588). The previous fallback path called + ``print(message.encode(...))`` with a ``bytes`` object, which + Python's ``print`` rendered as a literal ``b'...'`` repr — visible + in the user's dashboard output. The fix re-encodes through the + stream's encoding with ``backslashreplace`` and decodes back to + ``str``. + """ + buf = io.BytesIO() + cp1252_stream = io.TextIOWrapper(buf, encoding="cp1252", errors="strict") + monkeypatch.setattr(sys, "stdout", cp1252_stream) + + util.safe_print("bars: \u2582\u2584\u2586\u2588 done") + cp1252_stream.flush() + output = buf.getvalue().decode("cp1252") + + # Output is a clean line, not the bytes repr. + assert not output.startswith("b'") + assert "b'bars" not in output + # Unencodable codepoints become readable backslash escapes. + assert "\\u2582\\u2584\\u2586\\u2588" in output + # Encodable parts survive unchanged. + assert "bars: " in output + assert " done" in output + assert output.endswith("\n") + + def test_fallback_with_dashboard_escaped_message( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Dashboard ESC escaping + cp1252 fallback compose correctly.""" + from esphome.core import CORE + + monkeypatch.setattr(CORE, "dashboard", True) + buf = io.BytesIO() + cp1252_stream = io.TextIOWrapper(buf, encoding="cp1252", errors="strict") + monkeypatch.setattr(sys, "stdout", cp1252_stream) + + util.safe_print("\033[0;32m\u2582\u2584\u2586\u2588\033[0m") + cp1252_stream.flush() + output = buf.getvalue().decode("cp1252") + + # Dashboard escaping turned ESC into literal "\033" (5 chars), which + # cp1252 can encode, so it survives the round-trip verbatim. + assert "\\033[0;32m" in output + assert "\\033[0m" in output + # Block characters became backslash escapes via backslashreplace. + assert "\\u2582\\u2584\\u2586\\u2588" in output + + def test_final_message_when_locale_is_invalid( + self, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + ) -> None: + """If every encoding path fails, surface the locale-error sentinel.""" + original_print = print + call_count = 0 + + def fake_print(*args: Any, **kwargs: Any) -> None: + nonlocal call_count + call_count += 1 + # The first three calls are: native print, stream-encoding + # fallback, ASCII fallback. Make all three raise so we reach + # the final sentinel "Cannot print line..." which is expected + # to succeed (no encoding required). + if call_count <= 3: + raise UnicodeEncodeError("ascii", "x", 0, 1, "boom") + original_print(*args, **kwargs) + + monkeypatch.setattr("builtins.print", fake_print) + util.safe_print("x") + assert call_count == 4 + assert ( + capsys.readouterr().out == "Cannot print line because of invalid locale!\n" + ) From 197d4dac8e5e9da086f3596f90edbdce78886706 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 May 2026 08:27:10 +1200 Subject: [PATCH 1840/2056] Bump version to 2026.4.4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 0e6a845ed88..2e86f54e961 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.3 +PROJECT_NUMBER = 2026.4.4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 89b6ff15ee9..bc31ab36b5e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.3" +__version__ = "2026.4.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 857e529803d10971ffd36089068b6590fac3dc6f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 4 May 2026 18:41:50 -0400 Subject: [PATCH 1841/2056] [audio] Use the microMP3 library instead of esp-audio-libs (#16236) --- esphome/components/audio/__init__.py | 1 + esphome/components/audio/audio_decoder.cpp | 108 ++++++++++----------- esphome/components/audio/audio_decoder.h | 13 +-- esphome/idf_component.yml | 2 + 4 files changed, 61 insertions(+), 63 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 8528e77ae7d..60ff40ea4b8 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -395,6 +395,7 @@ async def to_code(config): ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") + add_idf_component(name="esphome/micro-mp3", ref="0.2.0") _emit_memory_pair( data.mp3.buffer_memory, "CONFIG_MP3_DECODER_PREFER_PSRAM", diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index baa4c41c064..65a4db4e10e 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -20,14 +20,6 @@ AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } -AudioDecoder::~AudioDecoder() { -#ifdef USE_AUDIO_MP3_SUPPORT - if (this->audio_file_type_ == AudioFileType::MP3) { - esp_audio_libs::helix_decoder::MP3FreeDecoder(this->mp3_decoder_); - } -#endif -} - esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) { auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); if (source == nullptr) { @@ -92,13 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { #endif #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: - this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder(); - - // MP3 always has 1152 samples per chunk - this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2; // samples * size per sample * channels - - // Always reallocate the output transfer buffer to the smallest necessary size - this->output_transfer_buffer_->reallocate(this->free_buffer_required_); + this->mp3_decoder_ = make_unique<micro_mp3::Mp3Decoder>(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_OPUS_SUPPORT @@ -312,51 +301,56 @@ FileDecoderState AudioDecoder::decode_flac_() { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState AudioDecoder::decode_mp3_() { - // Look for the next sync word - int buffer_length = (int) this->input_buffer_->available(); - int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length); + // microMP3's samples_decoded value is samples per channel; e.g., what ESPHome typically calls an audio frame. + // microMP3 uses the term frame to refer to an MP3 frame: an encoded packet that contains multiple audio frames. + size_t bytes_consumed = 0; + size_t samples_decoded = 0; - if (offset < 0) { - // New data may have the sync word - this->input_buffer_->consume(buffer_length); + // microMP3 buffers internally: it consumes from our input buffer at its own pace, emits MP3_STREAM_INFO_READY once + // the first frame header is parsed, and only then produces PCM. It handles sync-word search and ID3v2 tag skipping. + micro_mp3::Mp3Result result = this->mp3_decoder_->decode( + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); + + this->input_buffer_->consume(bytes_consumed); + + if (result == micro_mp3::MP3_OK) { + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + } else if (result == micro_mp3::MP3_STREAM_INFO_READY) { + // First successful header parse: capture stream info and resize the output buffer to fit one full frame. + // microMP3 always outputs 16-bit PCM. + this->audio_stream_info_ = + audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate()); + this->free_buffer_required_ = + this->mp3_decoder_->get_samples_per_frame() * this->mp3_decoder_->get_channels() * sizeof(int16_t); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + return FileDecoderState::FAILED; + } + } else if (result == micro_mp3::MP3_NEED_MORE_DATA) { + return FileDecoderState::MORE_TO_PROCESS; + } else if (result == micro_mp3::MP3_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the frame on the next call + if (this->mp3_decoder_->get_channels() > 0) { + this->free_buffer_required_ = + this->mp3_decoder_->get_samples_per_frame() * this->mp3_decoder_->get_channels() * sizeof(int16_t); + } else { + // Fallback to worst-case size if channel info isn't available + this->free_buffer_required_ = this->mp3_decoder_->get_min_output_buffer_bytes(); + } + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + return FileDecoderState::FAILED; + } + } else if (result == micro_mp3::MP3_DECODE_ERROR) { + // Corrupt frame skipped; recoverable, retry on next call + ESP_LOGW(TAG, "MP3 decoder skipped a corrupt frame"); return FileDecoderState::POTENTIALLY_FAILED; - } - - // Advance read pointer to match the offset for the syncword - this->input_buffer_->consume(offset); - const uint8_t *buffer_start = this->input_buffer_->data(); - - buffer_length = (int) this->input_buffer_->available(); - int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, - (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); - - size_t consumed = this->input_buffer_->available() - buffer_length; - this->input_buffer_->consume(consumed); - - if (err) { - switch (err) { - case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: - [[fallthrough]]; - case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: - return FileDecoderState::FAILED; - break; - default: - // Most errors are recoverable by moving on to the next frame, so mark as potentailly failed - return FileDecoderState::POTENTIALLY_FAILED; - break; - } } else { - esp_audio_libs::helix_decoder::MP3FrameInfo mp3_frame_info; - esp_audio_libs::helix_decoder::MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info); - if (mp3_frame_info.outputSamps > 0) { - int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8); - this->output_transfer_buffer_->increase_buffer_length(mp3_frame_info.outputSamps * bytes_per_sample); - - if (!this->audio_stream_info_.has_value()) { - this->audio_stream_info_ = - audio::AudioStreamInfo(mp3_frame_info.bitsPerSample, mp3_frame_info.nChans, mp3_frame_info.samprate); - } - } + // MP3_ALLOCATION_FAILED, MP3_INPUT_INVALID, or any future error -- not recoverable + ESP_LOGE(TAG, "MP3 decoder failed: %d", static_cast<int>(result)); + return FileDecoderState::FAILED; } return FileDecoderState::MORE_TO_PROCESS; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 6e3a228a686..4cbe8b67209 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -16,9 +16,6 @@ #include "esp_err.h" // esp-audio-libs -#ifdef USE_AUDIO_MP3_SUPPORT -#include <mp3_decoder.h> -#endif #include <wav_decoder.h> // micro-flac @@ -26,6 +23,11 @@ #include <micro_flac/flac_decoder.h> #endif +// micro-mp3 +#ifdef USE_AUDIO_MP3_SUPPORT +#include <micro_mp3/mp3_decoder.h> +#endif + // micro-opus #ifdef USE_AUDIO_OPUS_SUPPORT #include <micro_opus/ogg_opus_decoder.h> @@ -62,8 +64,7 @@ class AudioDecoder { /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) - ~AudioDecoder(); + ~AudioDecoder() = default; /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. /// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership @@ -125,7 +126,7 @@ class AudioDecoder { #endif #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); - esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; + std::unique_ptr<micro_mp3::Mp3Decoder> mp3_decoder_; #endif #ifdef USE_AUDIO_OPUS_SUPPORT FileDecoderState decode_opus_(); diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index f5a8dd8c601..5ad9090215d 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -9,6 +9,8 @@ dependencies: version: 0.2.0 esphome/micro-flac: version: 0.1.1 + esphome/micro-mp3: + version: 0.2.0 esphome/micro-opus: version: 0.4.0 espressif/esp-dsp: From 556783b95ba30c4a40eda780f058c0fee0b18bf9 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER <olivier.archer@gmail.com> Date: Tue, 5 May 2026 01:19:52 +0200 Subject: [PATCH 1842/2056] [http_request] remove slow http_request warning on 8266 (#16239) --- esphome/components/http_request/http_request_arduino.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 217ad0064d9..bb5e9427dd1 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -70,12 +70,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur stream_ptr = std::make_unique<WiFiClient>(); #endif // USE_HTTP_REQUEST_ESP8266_HTTPS -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?) - if (!secure) { - ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 " - "in your YAML, or use HTTPS"); - } -#endif // USE_ARDUINO_VERSION_CODE bool status = container->client_.begin(*stream_ptr, url.c_str()); #elif defined(USE_RP2040) From d28498ac2c6892a775988bc6712bbd18e5cf12b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 23:39:44 +0000 Subject: [PATCH 1843/2056] Bump cryptography from 47.0.0 to 48.0.0 (#16245) 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 789a3f79957..818453dc8f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cryptography==47.0.0 +cryptography==48.0.0 voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 From f33d137669672fe909c3e4c625805f8190b4fc05 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 4 May 2026 19:45:11 -0400 Subject: [PATCH 1844/2056] [audio][media_player][speaker] WAV decoding is no longer always built (#16244) --- esphome/components/audio/__init__.py | 5 +++-- esphome/components/audio/audio.cpp | 6 ++++++ esphome/components/audio/audio.h | 2 ++ esphome/components/audio/audio_decoder.cpp | 6 ++++++ esphome/components/audio/audio_decoder.h | 18 +++++++++++++----- esphome/components/audio_file/__init__.py | 2 ++ esphome/components/media_player/__init__.py | 3 +++ .../speaker/media_player/__init__.py | 2 ++ .../media_player/speaker_media_player.cpp | 5 ++++- esphome/core/defines.h | 1 + 10 files changed, 42 insertions(+), 8 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 60ff40ea4b8..7bd7ba9768d 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -64,8 +64,7 @@ class AudioData: flac_support: bool = False mp3_support: bool = False opus_support: bool = False - # WAV defaults to True for backward compatibility; will become opt-in in a future release - wav_support: bool = True + wav_support: bool = False micro_decoder_support: bool = False flac: FlacOptions = field(default_factory=FlacOptions) mp3: Mp3Options = field(default_factory=Mp3Options) @@ -428,3 +427,5 @@ async def to_code(config): add_idf_sdkconfig_option( "CONFIG_OPUS_PSEUDOSTACK_SIZE", data.opus.pseudostack.size ) + if data.wav_support: + cg.add_define("USE_AUDIO_WAV_SUPPORT") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 3d675109e49..b977c4e9181 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -55,8 +55,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { case AudioFileType::OPUS: return "OPUS"; #endif +#ifdef USE_AUDIO_WAV_SUPPORT case AudioFileType::WAV: return "WAV"; +#endif default: return "unknown"; } @@ -71,9 +73,11 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url) return AudioFileType::MP3; } #endif +#ifdef USE_AUDIO_WAV_SUPPORT if (strcasecmp(content_type, "audio/wav") == 0) { return AudioFileType::WAV; } +#endif #ifdef USE_AUDIO_FLAC_SUPPORT if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { return AudioFileType::FLAC; @@ -91,9 +95,11 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url) // Fallback to URL extension if (url != nullptr && url[0] != '\0') { +#ifdef USE_AUDIO_WAV_SUPPORT if (str_endswith_ignore_case(url, ".wav")) { return AudioFileType::WAV; } +#endif #ifdef USE_AUDIO_MP3_SUPPORT if (str_endswith_ignore_case(url, ".mp3")) { return AudioFileType::MP3; diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index d3b41a362f1..9259f0a3c65 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -116,7 +116,9 @@ enum class AudioFileType : uint8_t { #ifdef USE_AUDIO_OPUS_SUPPORT OPUS, #endif +#ifdef USE_AUDIO_WAV_SUPPORT WAV, +#endif }; struct AudioFile { diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 65a4db4e10e..156704fb86b 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -98,6 +98,7 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { this->decoder_buffers_internally_ = true; break; #endif +#ifdef USE_AUDIO_WAV_SUPPORT case AudioFileType::WAV: this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>(); this->wav_decoder_->reset(); @@ -109,6 +110,7 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { this->output_transfer_buffer_->reallocate(this->free_buffer_required_); } break; +#endif case AudioFileType::NONE: default: return ESP_ERR_NOT_SUPPORTED; @@ -226,9 +228,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { state = this->decode_opus_(); break; #endif +#ifdef USE_AUDIO_WAV_SUPPORT case AudioFileType::WAV: state = this->decode_wav_(); break; +#endif case AudioFileType::NONE: default: state = FileDecoderState::IDLE; @@ -395,6 +399,7 @@ FileDecoderState AudioDecoder::decode_opus_() { } #endif +#ifdef USE_AUDIO_WAV_SUPPORT FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed @@ -441,6 +446,7 @@ FileDecoderState AudioDecoder::decode_wav_() { return FileDecoderState::END_OF_FILE; } +#endif } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 4cbe8b67209..8769e0b38b7 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -15,9 +15,6 @@ #include "esp_err.h" -// esp-audio-libs -#include <wav_decoder.h> - // micro-flac #ifdef USE_AUDIO_FLAC_SUPPORT #include <micro_flac/flac_decoder.h> @@ -33,6 +30,11 @@ #include <micro_opus/ogg_opus_decoder.h> #endif +// esp-audio-libs +#ifdef USE_AUDIO_WAV_SUPPORT +#include <wav_decoder.h> +#endif + namespace esphome { namespace audio { @@ -56,7 +58,7 @@ class AudioDecoder { * @brief Class that facilitates decoding an audio file. * The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink * (ring buffer, speaker component, or callback). - * Supports wav, flac, mp3, and ogg opus formats. + * Supports flac, mp3, ogg opus, and wav formats (each enabled independently at compile time). */ public: /// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source() @@ -119,7 +121,6 @@ class AudioDecoder { void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; } protected: - std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_; #ifdef USE_AUDIO_FLAC_SUPPORT FileDecoderState decode_flac_(); std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_; @@ -132,7 +133,10 @@ class AudioDecoder { FileDecoderState decode_opus_(); std::unique_ptr<micro_opus::OggOpusDecoder> opus_decoder_; #endif +#ifdef USE_AUDIO_WAV_SUPPORT FileDecoderState decode_wav_(); + std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_; +#endif std::unique_ptr<AudioReadableBuffer> input_buffer_; std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_; @@ -142,14 +146,18 @@ class AudioDecoder { size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; +#ifdef USE_AUDIO_WAV_SUPPORT size_t wav_bytes_left_{0}; +#endif uint32_t potentially_failed_count_{0}; uint32_t accumulated_frames_written_{0}; uint32_t playback_ms_{0}; bool end_of_file_{false}; +#ifdef USE_AUDIO_WAV_SUPPORT bool wav_has_known_end_{false}; +#endif bool decoder_buffers_internally_{false}; diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py index 88be6db168a..b246633c31b 100644 --- a/esphome/components/audio_file/__init__.py +++ b/esphome/components/audio_file/__init__.py @@ -193,6 +193,8 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType] audio.request_mp3_support() elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]): audio.request_opus_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["WAV"]): + audio.request_wav_support() return config diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 0024e3b9658..aa1e88dca95 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -133,6 +133,7 @@ def request_codecs_for_format_configs( audio.request_flac_support() audio.request_mp3_support() audio.request_opus_support() + audio.request_wav_support() else: if "FLAC" in needed_formats: audio.request_flac_support() @@ -140,6 +141,8 @@ def request_codecs_for_format_configs( audio.request_mp3_support() if "OPUS" in needed_formats: audio.request_opus_support() + if "WAV" in needed_formats: + audio.request_wav_support() # Local config key constants diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index fbc83ef12f5..90d9309f460 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -210,6 +210,8 @@ def _final_validate(config): audio.request_mp3_support() elif fmt_name == "OPUS": audio.request_opus_support() + elif fmt_name == "WAV": + audio.request_wav_support() break return config diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index ab11a89c3f4..afd93b3f45c 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -17,9 +17,12 @@ namespace speaker { // - Each stream has an individual speaker component for output // - Each stream is handled by an ``AudioPipeline`` object with two parts/tasks // - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time -// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample +// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per +// sample. +// Each format is enabled independently at compile time: // - FLAC // - MP3 (based on the libhelix decoder) +// - Ogg Opus // - WAV // - Each task runs until it is done processing the file or it receives a stop command // - Inter-task communication uses a FreeRTOS Event Group diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 93f4307e129..85454d3cc00 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -180,6 +180,7 @@ #define USE_AUDIO_FLAC_SUPPORT #define USE_AUDIO_MP3_SUPPORT #define USE_AUDIO_OPUS_SUPPORT +#define USE_AUDIO_WAV_SUPPORT #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER From ea2b2b39201a80c13d2ae1620179b2edc7bb40e9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Mon, 4 May 2026 21:12:26 -0400 Subject: [PATCH 1845/2056] [audio_file] Use microDecoder library instead of manual task management/decoding (#16237) --- .../audio_file/media_source/__init__.py | 28 +- .../media_source/audio_file_media_source.cpp | 332 +++++++----------- .../media_source/audio_file_media_source.h | 43 ++- 3 files changed, 165 insertions(+), 238 deletions(-) diff --git a/esphome/components/audio_file/media_source/__init__.py b/esphome/components/audio_file/media_source/__init__.py index e9e292a2b2e..635a51b610a 100644 --- a/esphome/components/audio_file/media_source/__init__.py +++ b/esphome/components/audio_file/media_source/__init__.py @@ -1,5 +1,7 @@ +from typing import Any + import esphome.codegen as cg -from esphome.components import media_source, psram +from esphome.components import audio, esp32, media_source, psram import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM from esphome.types import ConfigType @@ -13,19 +15,30 @@ AudioFileMediaSource = audio_file_ns.class_( "AudioFileMediaSource", cg.Component, media_source.MediaSource ) + +def _request_micro_decoder(config: ConfigType) -> ConfigType: + audio.request_micro_decoder_support() + return config + + +def _validate_task_stack_in_psram(value: Any) -> bool: + if value := cv.boolean(value): + return cv.requires_component(psram.DOMAIN)(value) + return value + + CONFIG_SCHEMA = cv.All( media_source.media_source_schema( AudioFileMediaSource, ) .extend( { - cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( - cv.boolean, cv.requires_component(psram.DOMAIN) - ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram, } ) .extend(cv.COMPONENT_SCHEMA), cv.only_on_esp32, + _request_micro_decoder, ) @@ -34,5 +47,8 @@ async def to_code(config: ConfigType) -> None: await cg.register_component(var, config) await media_source.register_media_source(var, config) - if CONF_TASK_STACK_IN_PSRAM in config: - cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp index fbb5ecd88d4..0cda1eca9ed 100644 --- a/esphome/components/audio_file/media_source/audio_file_media_source.cpp +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -2,281 +2,185 @@ #ifdef USE_ESP32 -#include "esphome/components/audio/audio_decoder.h" +#include "esphome/core/log.h" + +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> -#include <cinttypes> #include <cstring> namespace esphome::audio_file { -namespace { // anonymous namespace for internal linkage -struct AudioSinkAdapter : public audio::AudioSinkCallback { - media_source::MediaSource *source; - audio::AudioStreamInfo stream_info; - - size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override { - return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info); - } -}; -} // namespace - -#if defined(USE_AUDIO_OPUS_SUPPORT) -static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; -#else -static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; -#endif - static const char *const TAG = "audio_file_media_source"; -enum EventGroupBits : uint32_t { - // Requests to start playback (set by play_uri, handled by loop) - REQUEST_START = (1 << 0), - // Commands from main loop to decode task - COMMAND_STOP = (1 << 1), - COMMAND_PAUSE = (1 << 2), - // Decode task lifecycle signals (one-shot, cleared by loop) - TASK_STARTING = (1 << 7), - TASK_RUNNING = (1 << 8), - TASK_STOPPING = (1 << 9), - TASK_STOPPED = (1 << 10), - TASK_ERROR = (1 << 11), - // Decode task state (level-triggered, set/cleared by decode task) - TASK_PAUSED = (1 << 12), - ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits -}; +static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; +static constexpr size_t DECODER_TASK_STACK_SIZE = 5120; +static constexpr uint8_t DECODER_TASK_PRIORITY = 2; +static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20; +static constexpr char URI_PREFIX[] = "audio-file://"; + +namespace { // anonymous namespace for internal linkage + +// audio::AudioFileType and micro_decoder::AudioFileType use different numeric layouts (audio's +// values shift with USE_AUDIO_*_SUPPORT defines; micro_decoder's are fixed and guarded by +// MICRO_DECODER_CODEC_*). The codec request flow in audio/__init__.py keeps the two sets of +// guards aligned, so a switch with matching #ifdefs covers all reachable cases. +micro_decoder::AudioFileType to_micro_decoder_type(audio::AudioFileType type) { + switch (type) { +#ifdef USE_AUDIO_FLAC_SUPPORT + case audio::AudioFileType::FLAC: + return micro_decoder::AudioFileType::FLAC; +#endif +#ifdef USE_AUDIO_MP3_SUPPORT + case audio::AudioFileType::MP3: + return micro_decoder::AudioFileType::MP3; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case audio::AudioFileType::OPUS: + return micro_decoder::AudioFileType::OPUS; +#endif +#ifdef USE_AUDIO_WAV_SUPPORT + case audio::AudioFileType::WAV: + return micro_decoder::AudioFileType::WAV; +#endif + default: + return micro_decoder::AudioFileType::NONE; + } +} + +} // namespace void AudioFileMediaSource::dump_config() { - ESP_LOGCONFIG(TAG, "Audio File Media Source:"); - ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No"); + ESP_LOGCONFIG(TAG, + "Audio File Media Source:\n" + " Decoder Task Stack in PSRAM: %s", + YESNO(this->decoder_task_stack_in_psram_)); } void AudioFileMediaSource::setup() { this->disable_loop(); - this->event_group_ = xEventGroupCreate(); - if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Failed to create event group"); + micro_decoder::DecoderConfig config; + config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS; + config.decoder_priority = DECODER_TASK_PRIORITY; + config.decoder_stack_size = DECODER_TASK_STACK_SIZE; + config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_; + + this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config); + if (this->decoder_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate decoder"); this->mark_failed(); return; } + this->decoder_->set_listener(this); } -void AudioFileMediaSource::loop() { - EventBits_t event_bits = xEventGroupGetBits(this->event_group_); +void AudioFileMediaSource::loop() { this->decoder_->loop(); } - if (event_bits & REQUEST_START) { - xEventGroupClearBits(this->event_group_, REQUEST_START); - this->decoding_state_ = AudioFileDecodingState::START_TASK; - } - - switch (this->decoding_state_) { - case AudioFileDecodingState::START_TASK: { - if (!this->decode_task_.is_created()) { - xEventGroupClearBits(this->event_group_, ALL_BITS); - if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1, - this->task_stack_in_psram_)) { - ESP_LOGE(TAG, "Failed to create task"); - this->status_momentary_error("task_create", 1000); - this->set_state_(media_source::MediaSourceState::ERROR); - this->decoding_state_ = AudioFileDecodingState::IDLE; - return; - } - } - this->decoding_state_ = AudioFileDecodingState::DECODING; - break; - } - case AudioFileDecodingState::DECODING: { - if (event_bits & TASK_STARTING) { - ESP_LOGD(TAG, "Starting"); - xEventGroupClearBits(this->event_group_, TASK_STARTING); - } - - if (event_bits & TASK_RUNNING) { - ESP_LOGV(TAG, "Started"); - xEventGroupClearBits(this->event_group_, TASK_RUNNING); - this->set_state_(media_source::MediaSourceState::PLAYING); - } - - if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) { - this->set_state_(media_source::MediaSourceState::PAUSED); - } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) { - this->set_state_(media_source::MediaSourceState::PLAYING); - } - - if (event_bits & TASK_STOPPING) { - ESP_LOGV(TAG, "Stopping"); - xEventGroupClearBits(this->event_group_, TASK_STOPPING); - } - - if (event_bits & TASK_ERROR) { - // Report error so the orchestrator knows playback failed; task will have already logged the specific error - this->set_state_(media_source::MediaSourceState::ERROR); - } - - if (event_bits & TASK_STOPPED) { - ESP_LOGD(TAG, "Stopped"); - xEventGroupClearBits(this->event_group_, ALL_BITS); - - this->decode_task_.deallocate(); - this->set_state_(media_source::MediaSourceState::IDLE); - this->decoding_state_ = AudioFileDecodingState::IDLE; - } - break; - } - case AudioFileDecodingState::IDLE: { - if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) { - this->set_state_(media_source::MediaSourceState::IDLE); - } - break; - } - } - - if ((this->decoding_state_ == AudioFileDecodingState::IDLE) && - (this->get_state() == media_source::MediaSourceState::IDLE)) { - this->disable_loop(); - } -} +bool AudioFileMediaSource::can_handle(const std::string &uri) const { return uri.starts_with(URI_PREFIX); } // Called from the orchestrator's main loop, so no synchronization needed with loop() bool AudioFileMediaSource::play_uri(const std::string &uri) { - if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() || - xEventGroupGetBits(this->event_group_) & REQUEST_START) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) { return false; } - // Check if source is already playing if (this->get_state() != media_source::MediaSourceState::IDLE) { ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); return false; } - // Validate URI starts with "audio-file://" - if (!uri.starts_with("audio-file://")) { + if (!uri.starts_with(URI_PREFIX)) { ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); return false; } - // Strip "audio-file://" prefix and find the file - const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters - + const char *file_id = uri.c_str() + sizeof(URI_PREFIX) - 1; + this->current_file_ = nullptr; for (const auto &named_file : get_named_audio_files()) { if (strcmp(named_file.file_id, file_id) == 0) { this->current_file_ = named_file.file; - xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START); - this->enable_loop(); - return true; + break; } } - ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + if (this->current_file_ == nullptr) { + ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + return false; + } + + micro_decoder::AudioFileType type = to_micro_decoder_type(this->current_file_->file_type); + if (this->decoder_->play_buffer(this->current_file_->data, this->current_file_->length, type)) { + this->pause_.store(false, std::memory_order_relaxed); + this->enable_loop(); + return true; + } + + ESP_LOGE(TAG, "Failed to start playback of '%s'", file_id); return false; } // Called from the orchestrator's main loop, so no synchronization needed with loop() void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) { - if (this->decoding_state_ != AudioFileDecodingState::DECODING) { - return; - } - switch (command) { case media_source::MediaSourceCommand::STOP: - xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP); + this->decoder_->stop(); break; case media_source::MediaSourceCommand::PAUSE: - xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + // Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state + // machine from getting stuck in PAUSED when no playback is active (which would block the + // next play_uri() call via its IDLE-state precondition). + if (this->get_state() != media_source::MediaSourceState::PLAYING) + break; + // PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily + // yields, which fills any internal buffering and applies back pressure that effectively + // pauses the decoder task. + this->set_state_(media_source::MediaSourceState::PAUSED); + this->pause_.store(true, std::memory_order_relaxed); break; case media_source::MediaSourceCommand::PLAY: - xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + if (this->get_state() != media_source::MediaSourceState::PAUSED) + break; + this->set_state_(media_source::MediaSourceState::PLAYING); + this->pause_.store(false, std::memory_order_relaxed); break; default: break; } } -void AudioFileMediaSource::decode_task(void *params) { - AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params); +// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for +// being thread-safe with respect to its own audio writer. +size_t AudioFileMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) { + if (this->pause_.load(std::memory_order_relaxed)) { + vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS)); + return 0; + } + return this->write_output(data, length, timeout_ms, this->stream_info_); +} - do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break +// Called from the decoder task before the first on_audio_write(). +void AudioFileMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) { + this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate()); +} - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING); - - // 0 bytes for input transfer buffer makes it an inplace buffer - std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096); - - esp_err_t err = decoder->start(this_source->current_file_->file_type); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err)); - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING); +// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main +// loop thread and it's safe to call set_state_() directly. +void AudioFileMediaSource::on_state_change(micro_decoder::DecoderState state) { + switch (state) { + case micro_decoder::DecoderState::IDLE: + this->set_state_(media_source::MediaSourceState::IDLE); + this->disable_loop(); break; - } - - // Add the file as a const data source - decoder->add_source(this_source->current_file_->data, this_source->current_file_->length); - - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING); - - AudioSinkAdapter audio_sink; - bool has_stream_info = false; - - while (true) { - EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_); - - if (event_bits & EventGroupBits::COMMAND_STOP) { - break; - } - - bool paused = event_bits & EventGroupBits::COMMAND_PAUSE; - decoder->set_pause_output_state(paused); - if (paused) { - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); - vTaskDelay(pdMS_TO_TICKS(20)); - } else { - xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); - } - - // Will stop gracefully once finished with the current file - audio::AudioDecoderState decoder_state = decoder->decode(true); - - if (decoder_state == audio::AudioDecoderState::FINISHED) { - break; - } else if (decoder_state == audio::AudioDecoderState::FAILED) { - ESP_LOGE(TAG, "Decoder failed"); - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); - break; - } - - if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { - has_stream_info = true; - - audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); - - ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %" PRIu32, stream_info.get_bits_per_sample(), - stream_info.get_channels(), stream_info.get_sample_rate()); - - if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { - ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported"); - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); - break; - } - - audio_sink.source = this_source; - audio_sink.stream_info = stream_info; - esp_err_t err = decoder->add_sink(&audio_sink); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err)); - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); - break; - } - } - } - - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING); - } while (false); - - // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed. - - xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED); - vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it + case micro_decoder::DecoderState::PLAYING: + this->set_state_(media_source::MediaSourceState::PLAYING); + break; + case micro_decoder::DecoderState::FAILED: + this->set_state_(media_source::MediaSourceState::ERROR); + break; + default: + break; + } } } // namespace esphome::audio_file diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h index 75e18c13b88..2c6189f2727 100644 --- a/esphome/components/audio_file/media_source/audio_file_media_source.h +++ b/esphome/components/audio_file/media_source/audio_file_media_source.h @@ -8,41 +8,48 @@ #include "esphome/components/audio_file/audio_file.h" #include "esphome/components/media_source/media_source.h" #include "esphome/core/component.h" -#include "esphome/core/static_task.h" -#include <freertos/FreeRTOS.h> -#include <freertos/event_groups.h> +#include <micro_decoder/decoder_source.h> +#include <micro_decoder/types.h> + +#include <atomic> +#include <memory> +#include <string> namespace esphome::audio_file { -enum class AudioFileDecodingState : uint8_t { - START_TASK, - DECODING, - IDLE, -}; - -class AudioFileMediaSource : public Component, public media_source::MediaSource { +// Inherits from two unrelated listener-style interfaces: +// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator +// (the orchestrator calls set_listener() on us with a MediaSourceListener*). +// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded +// audio and state changes (we call decoder_->set_listener(this) in setup()). +class AudioFileMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener { public: void setup() override; void loop() override; void dump_config() override; + void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; } + // MediaSource interface implementation bool play_uri(const std::string &uri) override; void handle_command(media_source::MediaSourceCommand command) override; - bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); } + bool can_handle(const std::string &uri) const override; - void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + // DecoderListener interface implementation + size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override; + void on_stream_info(const micro_decoder::AudioStreamInfo &info) override; + void on_state_change(micro_decoder::DecoderState state) override; protected: - static void decode_task(void *params); - + std::unique_ptr<micro_decoder::DecoderSource> decoder_; + audio::AudioStreamInfo stream_info_; audio::AudioFile *current_file_{nullptr}; - AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE}; - EventGroupHandle_t event_group_{nullptr}; - StaticTask decode_task_; - bool task_stack_in_psram_{false}; + // Written from the main loop in handle_command(), read from the decoder task in + // on_audio_write(). Must be atomic to avoid a data race. + std::atomic<bool> pause_{false}; + bool decoder_task_stack_in_psram_{false}; }; } // namespace esphome::audio_file From efff8fe8be8a8b14076e92ecef57a78132fcba56 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 May 2026 14:29:23 +1200 Subject: [PATCH 1846/2056] [platformio_api] Remove duplicated _strip_win_long_path_prefix (#16249) --- esphome/platformio_api.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index fa88776acfd..81ff01306a6 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -12,37 +12,6 @@ from esphome.util import FlashImage, run_external_process _LOGGER = logging.getLogger(__name__) -def _strip_win_long_path_prefix(path: str) -> str: - r"""Strip the Windows extended-length path prefix from ``path``. - - Handles both forms documented at - https://learn.microsoft.com/windows/win32/fileio/naming-a-file: - - * ``\\?\C:\path\to\file`` -> ``C:\path\to\file`` - * ``\\?\UNC\server\share\path`` -> ``\\server\share\path`` - - The NSIS-installed ``esphome.exe`` launcher on Windows starts Python with - ``sys.executable`` already prefixed with ``\\?\``. That prefix propagates - into PlatformIO's ``$PYTHONEXE`` (PlatformIO reads ``PYTHONEXEPATH`` from - the environment, falling back to ``os.path.normpath(sys.executable)``) - and ends up baked into SCons-emitted command lines for build steps such - as the esp8266 ``elf2bin`` invocation. ``cmd.exe`` does not understand - the ``\\?\`` prefix, so the build fails with - "The system cannot find the path specified." Stripping the prefix early - keeps the path shell-quotable. - - No-op on non-Windows platforms. - """ - if sys.platform != "win32": - return path - if path.startswith("\\\\?\\UNC\\"): - # \\?\UNC\server\share\... -> \\server\share\... - return "\\\\" + path[len("\\\\?\\UNC\\") :] - if path.startswith("\\\\?\\"): - return path[len("\\\\?\\") :] - return path - - def _strip_win_long_path_prefix(path: str) -> str: r"""Strip the Windows extended-length path prefix from ``path``. From edbb9f7b287da034eb529d325aa0f4245d2e3068 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 5 May 2026 07:15:32 -0500 Subject: [PATCH 1847/2056] [i2s_audio] Fix stereo playback when slot bit width exceeds data bit width (#16248) --- .../i2s_audio/speaker/i2s_audio_speaker_standard.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index 02034640346..edb316e3a25 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -280,6 +280,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream } #else slot_cfg.slot_bit_width = this->slot_bit_width_; + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { + slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_); + } #endif // USE_ESP32_VARIANT_ESP32 slot_cfg.slot_mask = slot_mask; From 87a705b1cc69ccfb07d2234c49356590db670621 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 5 May 2026 08:47:07 -0400 Subject: [PATCH 1848/2056] [audio] Bump microOpus to v0.4.1 (#16255) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 7bd7ba9768d..7ecc45db5ae 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -402,7 +402,7 @@ async def to_code(config): ) if data.opus_support: cg.add_define("USE_AUDIO_OPUS_SUPPORT") - add_idf_component(name="esphome/micro-opus", ref="0.4.0") + add_idf_component(name="esphome/micro-opus", ref="0.4.1") if data.opus.floating_point is not None: add_idf_sdkconfig_option( "CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 5ad9090215d..e8cad294390 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -12,7 +12,7 @@ dependencies: esphome/micro-mp3: version: 0.2.0 esphome/micro-opus: - version: 0.4.0 + version: 0.4.1 espressif/esp-dsp: version: "1.7.1" espressif/esp-tflite-micro: From 57397a318a2e08ea4c44bb7836358a7da20985f2 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 5 May 2026 12:21:02 -0400 Subject: [PATCH 1849/2056] [audio] Use the microWAV library for decoding (#16251) --- esphome/components/audio/__init__.py | 1 + esphome/components/audio/audio_decoder.cpp | 78 ++++++++-------------- esphome/components/audio/audio_decoder.h | 14 +--- esphome/idf_component.yml | 2 + 4 files changed, 35 insertions(+), 60 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 7ecc45db5ae..80fd328e48e 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -429,3 +429,4 @@ async def to_code(config): ) if data.wav_support: cg.add_define("USE_AUDIO_WAV_SUPPORT") + add_idf_component(name="esphome/micro-wav", ref="0.2.0") diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 156704fb86b..7abd03a36e5 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -79,7 +79,6 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>(); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header - this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_MP3_SUPPORT @@ -87,7 +86,6 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { this->mp3_decoder_ = make_unique<micro_mp3::Mp3Decoder>(); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header - this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_OPUS_SUPPORT @@ -95,16 +93,12 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { this->opus_decoder_ = make_unique<micro_opus::OggOpusDecoder>(); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header - this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_WAV_SUPPORT case AudioFileType::WAV: - this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>(); - this->wav_decoder_->reset(); - - // Processing WAVs doesn't actually require a specific amount of buffer size, as it is already in PCM format. - // Thus, we don't reallocate to a minimum size. + this->wav_decoder_ = make_unique<micro_wav::WAVDecoder>(); + // 1 KiB suffices to always make progress while avoiding excessive CPU spinning for decoding this->free_buffer_required_ = 1024; if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) { this->output_transfer_buffer_->reallocate(this->free_buffer_required_); @@ -181,10 +175,8 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio - // Only shift data on the first loop iteration to avoid unnecessary, slow moves - // If the decoder buffers internally, then never shift - size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration && !this->decoder_buffers_internally_); + // Never shift the input buffer; every decoder buffers internally and consumes only what it processed. + size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false); if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. @@ -401,50 +393,38 @@ FileDecoderState AudioDecoder::decode_opus_() { #ifdef USE_AUDIO_WAV_SUPPORT FileDecoderState AudioDecoder::decode_wav_() { - if (!this->audio_stream_info_.has_value()) { - // Header hasn't been processed + // microWAV's samples_decoded counts individual channel samples; e.g., for + // 16-bit stereo, 4 input bytes results in 2 samples_decoded. + size_t bytes_consumed = 0; + size_t samples_decoded = 0; - esp_audio_libs::wav_decoder::WAVDecoderResult result = - this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available()); + micro_wav::WAVDecoderResult result = this->wav_decoder_->decode( + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); - if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - this->input_buffer_->consume(this->wav_decoder_->bytes_processed()); + this->input_buffer_->consume(bytes_consumed); - this->audio_stream_info_ = audio::AudioStreamInfo( - this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); - - this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left(); - this->wav_has_known_end_ = (this->wav_bytes_left_ > 0); - return FileDecoderState::MORE_TO_PROCESS; - } else if (result == esp_audio_libs::wav_decoder::WAV_DECODER_WARNING_INCOMPLETE_DATA) { - // Available data didn't have the full header - return FileDecoderState::POTENTIALLY_FAILED; - } else { - return FileDecoderState::FAILED; + if (result == micro_wav::WAV_DECODER_SUCCESS) { + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().samples_to_bytes(samples_decoded)); } + } else if (result == micro_wav::WAV_DECODER_HEADER_READY) { + // After HEADER_READY, get_bits_per_sample() returns the output bit depth + // (16 for A-law/mu-law, 32 for IEEE float, original value for PCM). + this->audio_stream_info_ = + audio::AudioStreamInfo(this->wav_decoder_->get_bits_per_sample(), this->wav_decoder_->get_channels(), + this->wav_decoder_->get_sample_rate()); + } else if (result == micro_wav::WAV_DECODER_NEED_MORE_DATA) { + return FileDecoderState::MORE_TO_PROCESS; + } else if (result == micro_wav::WAV_DECODER_END_OF_STREAM) { + return FileDecoderState::END_OF_FILE; } else { - if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { - size_t bytes_to_copy = this->input_buffer_->available(); - - if (this->wav_has_known_end_) { - bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); - } - - bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); - - if (bytes_to_copy > 0) { - std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy); - this->input_buffer_->consume(bytes_to_copy); - this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); - if (this->wav_has_known_end_) { - this->wav_bytes_left_ -= bytes_to_copy; - } - } - return FileDecoderState::IDLE; - } + ESP_LOGE(TAG, "WAV decoder failed: %d", static_cast<int>(result)); + return FileDecoderState::FAILED; } - return FileDecoderState::END_OF_FILE; + return FileDecoderState::MORE_TO_PROCESS; } #endif diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 8769e0b38b7..58e982317c3 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -30,9 +30,9 @@ #include <micro_opus/ogg_opus_decoder.h> #endif -// esp-audio-libs +// micro-wav #ifdef USE_AUDIO_WAV_SUPPORT -#include <wav_decoder.h> +#include <micro_wav/wav_decoder.h> #endif namespace esphome { @@ -135,7 +135,7 @@ class AudioDecoder { #endif #ifdef USE_AUDIO_WAV_SUPPORT FileDecoderState decode_wav_(); - std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_; + std::unique_ptr<micro_wav::WAVDecoder> wav_decoder_; #endif std::unique_ptr<AudioReadableBuffer> input_buffer_; @@ -146,20 +146,12 @@ class AudioDecoder { size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; -#ifdef USE_AUDIO_WAV_SUPPORT - size_t wav_bytes_left_{0}; -#endif uint32_t potentially_failed_count_{0}; uint32_t accumulated_frames_written_{0}; uint32_t playback_ms_{0}; bool end_of_file_{false}; -#ifdef USE_AUDIO_WAV_SUPPORT - bool wav_has_known_end_{false}; -#endif - - bool decoder_buffers_internally_{false}; bool pause_output_{false}; }; diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index e8cad294390..14fb11ace54 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -13,6 +13,8 @@ dependencies: version: 0.2.0 esphome/micro-opus: version: 0.4.1 + esphome/micro-wav: + version: 0.2.0 espressif/esp-dsp: version: "1.7.1" espressif/esp-tflite-micro: From be82e8faeb35fde79b4d5e8ad51e57d8cc8de8e5 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 6 May 2026 01:02:26 +0200 Subject: [PATCH 1850/2056] [debug] Remove unused buffer in uicr lambda function (#16208) --- esphome/components/debug/debug_zephyr.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 81c86127848..e23a0f668a0 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -401,7 +401,6 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> #endif auto uicr = [](volatile uint32_t *data, uint8_t size) { std::string res; - char buf[sizeof(uint32_t) * 2 + 1]; for (size_t i = 0; i < size; i++) { if (i > 0) { res += ' '; From f30ad588ea421db25f0784b91365bb44f5991751 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:25:53 -0500 Subject: [PATCH 1851/2056] [cli] Add --ota-platform flag to pick web_server or native API OTA (#16207) --- esphome/__main__.py | 166 +++++- esphome/web_server_ota.py | 202 +++++++ tests/unit_tests/test_main.py | 283 ++++++++++ tests/unit_tests/test_web_server_ota.py | 670 ++++++++++++++++++++++++ 4 files changed, 1307 insertions(+), 14 deletions(-) create mode 100644 esphome/web_server_ota.py create mode 100644 tests/unit_tests/test_web_server_ota.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 222e299f6dd..f4a276b74cc 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -28,6 +28,7 @@ from esphome.const import ( ALLOWED_NAME_CHARS, ARGUMENT_HELP_DEVICE, CONF_API, + CONF_AUTH, CONF_BAUD_RATE, CONF_BROKER, CONF_DEASSERT_RTS_DTR, @@ -47,6 +48,8 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, + CONF_USERNAME, + CONF_WEB_SERVER, ENV_NOGITIGNORE, KEY_CORE, KEY_NATIVE_IDF, @@ -349,6 +352,17 @@ def choose_upload_log_host( elif bootsel.permission_error: bootsel_permission_error = True + # Annotate the OTA chooser entry only in the non-default case: when the + # config has web_server OTA but no native API OTA, the upload will fall + # through to the HTTP path and the user benefits from seeing that + # explicitly. The native-API path is the default and gets a plain label + # to avoid noise on the most common scenario. For LOGGING the OTA + # transport doesn't apply, so always leave the label plain. + if purpose == Purpose.UPLOADING and not has_native_ota() and has_web_server_ota(): + ota_suffix = " via web_server" + else: + ota_suffix = "" + def add_ota_options() -> None: """Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled.""" if (discovered := _discover_mac_suffix_devices()) is not None: @@ -356,11 +370,11 @@ def choose_upload_log_host( # intentionally skip the base-name fallback since with # name_add_mac_suffix on, the base name doesn't exist on the net. for host in discovered: - options.append((f"Over The Air ({host})", host)) + options.append((f"Over The Air{ota_suffix} ({host})", host)) elif has_resolvable_address(): - options.append((f"Over The Air ({CORE.address})", CORE.address)) + options.append((f"Over The Air{ota_suffix} ({CORE.address})", CORE.address)) if has_mqtt_ip_lookup(): - options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + options.append((f"Over The Air{ota_suffix} (MQTT IP lookup)", "MQTTIP")) if purpose == Purpose.LOGGING: if has_mqtt_logging(): @@ -429,7 +443,19 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA upload is available (requires platform: esphome).""" + """Check if any network OTA upload is available. + + True if the config exposes either ``platform: esphome`` (native API + OTA) or ``platform: web_server`` (HTTP OTA). Both reach the device + over the same network stack, so the OTA discovery path treats them + interchangeably; ``upload_program`` picks the actual transport based + on ``--ota-platform`` and what's configured. + """ + return has_native_ota() or has_web_server_ota() + + +def has_native_ota() -> bool: + """Check if native API OTA upload is available (``platform: esphome``).""" if CONF_OTA not in CORE.config: return False return any( @@ -438,6 +464,16 @@ def has_ota() -> bool: ) +def has_web_server_ota() -> bool: + """Check if web_server OTA upload is available (``platform: web_server``).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_WEB_SERVER + for ota_item in CORE.config[CONF_OTA] + ) + + def has_mqtt_ip_lookup() -> bool: """Check if MQTT is available and IP lookup is supported.""" from esphome.components.mqtt import CONF_DISCOVER_IP @@ -1115,25 +1151,83 @@ def upload_program( return exit_code, host if exit_code == 0 else None - ota_conf = {} + requested_platform = getattr(args, "ota_platform", None) + chosen_platform = _choose_ota_platform(config, requested_platform) + + # Resolve MQTT magic strings to actual IP addresses + network_devices = _resolve_network_devices(devices, config, args) + + if chosen_platform == CONF_WEB_SERVER: + if getattr(args, "partition_table", False): + raise EsphomeError( + "--partition-table is only supported with the esphome OTA platform; " + "the web_server OTA path can only update the firmware image." + ) + binary = CORE.firmware_bin + if getattr(args, "file", None) is not None: + binary = Path(args.file) + return _upload_via_web_server(config, network_devices, binary) + + return _upload_via_native_api(config, network_devices, args) + + +def _choose_ota_platform(config: ConfigType, requested: str | None) -> str: + """Pick the OTA platform to use, optionally honoring ``--ota-platform``. + + Default behavior prefers ``esphome`` (native API) when it is configured. + The native API uses challenge-response auth with MD5/SHA256 hashing of a + server-issued nonce, so the password is never sent over the wire; the + ``web_server`` path uses HTTP Basic auth which transmits credentials in + cleartext over the LAN. (The native path also supports gzip compression + on ESP8266, where flash space is tight; on ESP32/RP2040/LibreTiny the + backend reports ``supports_compression() == false`` and the firmware is + sent uncompressed regardless of which platform is used.) Falls back to + ``web_server`` only when that is the only available platform. + """ + # Use a dict (insertion-ordered) instead of a list so error messages and + # membership checks see one entry per platform even if the user has + # multiple ``ota:`` items of the same platform; the web_server OTA + # platform's final-validate hook merges duplicates anyway. + available: dict[str, None] = {} for ota_item in config.get(CONF_OTA, []): - if ota_item[CONF_PLATFORM] == CONF_ESPHOME: + platform = ota_item.get(CONF_PLATFORM) + if platform in (CONF_ESPHOME, CONF_WEB_SERVER): + available[platform] = None + + if not available: + raise EsphomeError( + f"Cannot upload Over the Air as the {CONF_OTA} configuration is not " + f"present or does not include {CONF_PLATFORM}: {CONF_ESPHOME} or " + f"{CONF_PLATFORM}: {CONF_WEB_SERVER}" + ) + + if requested is not None: + if requested not in available: + raise EsphomeError( + f"--ota-platform {requested} was requested but the configuration " + f"only provides: {', '.join(available)}" + ) + return requested + + if CONF_ESPHOME in available: + return CONF_ESPHOME + return CONF_WEB_SERVER + + +def _upload_via_native_api( + config: ConfigType, network_devices: list[str], args: ArgsProtocol +) -> tuple[int, str | None]: + ota_conf: ConfigType = {} + for ota_item in config.get(CONF_OTA, []): + if ota_item.get(CONF_PLATFORM) == CONF_ESPHOME: ota_conf = ota_item break - if not ota_conf: - raise EsphomeError( - f"Cannot upload Over the Air as the {CONF_OTA} configuration is not present or does not include {CONF_PLATFORM}: {CONF_ESPHOME}" - ) - from esphome import espota2 remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) - # Resolve MQTT magic strings to actual IP addresses - network_devices = _resolve_network_devices(devices, config, args) - binary = CORE.firmware_bin ota_type = espota2.OTA_TYPE_UPDATE_APP if getattr(args, "partition_table", False): @@ -1157,6 +1251,28 @@ def upload_program( return espota2.run_ota(network_devices, remote_port, password, binary, ota_type) +def _upload_via_web_server( + config: ConfigType, network_devices: list[str], binary: Path +) -> tuple[int, str | None]: + web_conf = config.get(CONF_WEB_SERVER) + if not web_conf: + raise EsphomeError( + f"Cannot upload via web_server OTA: the {CONF_WEB_SERVER} component " + f"is not configured." + ) + + remote_port = int(web_conf[CONF_PORT]) + auth = web_conf.get(CONF_AUTH) or {} + username = auth.get(CONF_USERNAME) + password = auth.get(CONF_PASSWORD) + + from esphome import web_server_ota + + return web_server_ota.run_ota( + network_devices, remote_port, username, password, binary + ) + + # Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a # 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as # bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the @@ -1877,6 +1993,17 @@ def parse_args(argv): "--file", help="Manually specify the binary file to upload.", ) + parser_upload.add_argument( + "--ota-platform", + choices=[CONF_ESPHOME, CONF_WEB_SERVER], + help=( + "OTA platform to use for network uploads. Defaults to " + f"'{CONF_ESPHOME}' (native API) when configured because it uses " + "challenge-response auth so the password is never sent in " + f"cleartext on the wire. Falls back to '{CONF_WEB_SERVER}' " + "(HTTP Basic auth) when that is the only configured platform." + ), + ) parser_upload.add_argument( "--partition-table", help="Upload as partition table (OTA).", @@ -1951,6 +2078,17 @@ def parse_args(argv): help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).", action="store_true", ) + parser_run.add_argument( + "--ota-platform", + choices=[CONF_ESPHOME, CONF_WEB_SERVER], + help=( + "OTA platform to use for network uploads. Defaults to " + f"'{CONF_ESPHOME}' (native API) when configured because it uses " + "challenge-response auth so the password is never sent in " + f"cleartext on the wire. Falls back to '{CONF_WEB_SERVER}' " + "(HTTP Basic auth) when that is the only configured platform." + ), + ) parser_clean = subparsers.add_parser( "clean-mqtt", diff --git a/esphome/web_server_ota.py b/esphome/web_server_ota.py new file mode 100644 index 00000000000..a49f46b270d --- /dev/null +++ b/esphome/web_server_ota.py @@ -0,0 +1,202 @@ +"""HTTP-based OTA upload via the ``web_server`` component's ``/update`` endpoint. + +This is the alternative to ``espota2`` (the native API OTA path). Useful when +a device only has ``platform: web_server`` configured under ``ota:``, or when +the user has lost the native OTA password but still has ``web_server`` basic +auth credentials. +""" + +from __future__ import annotations + +import io +import logging +from pathlib import Path +import secrets +import socket +from typing import BinaryIO + +import requests +from requests.auth import HTTPBasicAuth + +from esphome.core import EsphomeError +from esphome.helpers import ProgressBar, resolve_ip_address + +_LOGGER = logging.getLogger(__name__) + +OTA_PATH = "/update" +FORM_FIELD = "update" +# (connect_timeout, read_timeout). The device reboots after a successful +# upload so the read side must allow for a slow flash + response. +TIMEOUT = (20.0, 120.0) + + +class WebServerOTAError(EsphomeError): + pass + + +class _MultipartStreamer: + """Stream a single-file multipart/form-data body during transmission. + + ``requests.post(files=...)`` materializes the entire body in memory before + sending, so a progress callback wired into the file-like fires during + encoding instead of during the network send. Pass this via ``data=`` + (with ``__len__`` so urllib3 sets ``Content-Length`` instead of using + chunked transfer encoding); urllib3 then calls ``read(blocksize)`` + repeatedly during the POST and the progress bar tracks bytes leaving the + host. + """ + + def __init__(self, file: BinaryIO, file_size: int, filename: str) -> None: + self.boundary = f"esphomeOTA{secrets.token_hex(16)}" + prefix = ( + f"--{self.boundary}\r\n" + f'Content-Disposition: form-data; name="{FORM_FIELD}"; ' + f'filename="{filename}"\r\n' + f"Content-Type: application/octet-stream\r\n\r\n" + ).encode() + suffix = f"\r\n--{self.boundary}--\r\n".encode() + # Walked in order; ``read()`` advances to the next source on EOF. + self._sources: list[BinaryIO] = [io.BytesIO(prefix), file, io.BytesIO(suffix)] + self._idx = 0 + self._total = len(prefix) + file_size + len(suffix) + self._sent = 0 + self.progress = ProgressBar() + + def __len__(self) -> int: + return self._total + + @property + def content_type(self) -> str: + return f"multipart/form-data; boundary={self.boundary}" + + def read(self, size: int = -1) -> bytes: + remaining = self._total if size is None or size < 0 else size + out = bytearray() + while remaining > 0 and self._idx < len(self._sources): + chunk = self._sources[self._idx].read(remaining) + if not chunk: + self._idx += 1 + continue + out += chunk + remaining -= len(chunk) + if out: + self._sent += len(out) + self.progress.update(self._sent / self._total) + return bytes(out) + + +def _try_upload( + host: str, + port: int, + username: str | None, + password: str | None, + filename: Path, +) -> tuple[int, str | None]: + from esphome.core import CORE + + try: + addr_infos = resolve_ip_address(host, port, address_cache=CORE.address_cache) + except EsphomeError as err: + _LOGGER.error( + "Error resolving IP address of %s. Is it connected to WiFi?", host + ) + if not CORE.dashboard: + _LOGGER.error("(If you know the IP, try --device <IP>)") + raise WebServerOTAError(err) from err + + if not addr_infos: + _LOGGER.error("Could not resolve %s", host) + return 1, None + + file_size = filename.stat().st_size + _LOGGER.info("Uploading %s (%s bytes) via web_server OTA", filename, file_size) + auth = HTTPBasicAuth(username, password) if username and password else None + + # Iterate resolved IPs (IPv4 + IPv6 candidates) just like espota2 does. + for af, _socktype, _, _, sa in addr_infos: + ip = sa[0] + # IPv6 literals must be wrapped in brackets in URLs; link-local + # addresses need a percent-encoded zone index per RFC 6874. + if af == socket.AF_INET6: + scope = sa[3] if len(sa) >= 4 else 0 + host_part = f"[{ip}%25{scope}]" if scope else f"[{ip}]" + else: + host_part = ip + url = f"http://{host_part}:{port}{OTA_PATH}" + _LOGGER.info("Connecting to %s port %s...", ip, port) + + try: + with open(filename, "rb") as fh: + streamer = _MultipartStreamer(fh, file_size, filename.name) + try: + response = requests.post( + url, + data=streamer, + auth=auth, + timeout=TIMEOUT, + headers={ + "Content-Type": streamer.content_type, + "Connection": "close", + }, + ) + finally: + streamer.progress.done() + except requests.RequestException as err: + _LOGGER.error("OTA upload to %s port %s failed: %s", ip, port, err) + continue + + if response.status_code == 401: + raise WebServerOTAError( + "Authentication failed (HTTP 401). Check the 'web_server' " + "'auth' username and password." + ) + if response.status_code != 200: + detail = response.text.strip() or response.reason or "no response body" + raise WebServerOTAError( + f"Unexpected HTTP {response.status_code} response from device: {detail}" + ) + + # The endpoint returns HTTP 200 for both success and failure; the + # body is what tells us which (see ota_web_server.cpp handleRequest). + body = response.text.strip() + if "Successful" in body: + _LOGGER.info("Device response: %s", body) + _LOGGER.info("OTA successful") + return 0, ip + + raise WebServerOTAError( + f"Device reported OTA failure: {body or 'no response body'}" + ) + + return 1, None + + +def run_ota( + remote_hosts: str | list[str], + remote_port: int, + username: str | None, + password: str | None, + filename: Path, +) -> tuple[int, str | None]: + """Upload ``filename`` to the first reachable host via ``web_server`` OTA. + + Mirrors :func:`esphome.espota2.run_ota` so callers can swap between the + two paths with the same return contract: ``(0, host)`` on success or + ``(1, None)`` on failure. + """ + hosts = [remote_hosts] if isinstance(remote_hosts, str) else list(remote_hosts) + for host in hosts: + try: + exit_code, used_host = _try_upload( + host, remote_port, username, password, filename + ) + except WebServerOTAError as err: + _LOGGER.error("%s", err) + continue + if exit_code == 0: + return 0, used_host + # Reached only when every attempt failed; per-attempt errors were + # already logged. This summary line gives the user an unambiguous + # "stop reading, nothing worked" marker. + _LOGGER.error("OTA upload failed.") + return 1, None diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index f8a3ea888e0..0b96000a575 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -43,6 +43,7 @@ from esphome.__main__ import ( has_non_ip_address, has_ota, has_resolvable_address, + has_web_server_ota, mqtt_get_ip, run_esphome, run_miniterm, @@ -58,6 +59,7 @@ from esphome.components import esp32 from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( CONF_API, + CONF_AUTH, CONF_BAUD_RATE, CONF_BROKER, CONF_DISABLED, @@ -76,6 +78,8 @@ from esphome.const import ( CONF_SUBSTITUTIONS, CONF_TOPIC, CONF_USE_ADDRESS, + CONF_USERNAME, + CONF_WEB_SERVER, CONF_WIFI, KEY_CORE, KEY_TARGET_PLATFORM, @@ -213,6 +217,13 @@ def mock_run_ota() -> Generator[Mock]: yield mock +@pytest.fixture +def mock_run_web_server_ota() -> Generator[Mock]: + """Mock web_server_ota.run_ota for testing.""" + with patch("esphome.web_server_ota.run_ota") as mock: + yield mock + + @pytest.fixture def mock_is_ip_address() -> Generator[Mock]: """Mock is_ip_address for testing.""" @@ -1114,6 +1125,7 @@ class MockArgs: reset: bool = False list_only: bool = False output: str | None = None + ota_platform: str | None = None partition_table: bool = False @@ -1878,6 +1890,277 @@ def test_upload_program_ota_no_config( upload_program(config, args, devices) +def test_has_web_server_ota_detects_platform() -> None: + """has_web_server_ota returns True when web_server OTA platform is configured.""" + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_WEB_SERVER}], + } + ) + assert has_web_server_ota() is True + assert has_ota() is True + + +def test_has_web_server_ota_returns_false_without_config() -> None: + """has_web_server_ota returns False when only native OTA is configured.""" + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + } + ) + assert has_web_server_ota() is False + assert has_ota() is True + + +def test_upload_program_web_server_only_auto_dispatches( + mock_run_web_server_ota: Mock, + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """When only web_server OTA is configured, upload_program picks it automatically.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_web_server_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [{CONF_PLATFORM: CONF_WEB_SERVER}], + CONF_WEB_SERVER: { + CONF_PORT: 80, + CONF_AUTH: {CONF_USERNAME: "admin", CONF_PASSWORD: "pw"}, + }, + } + args = MockArgs() + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_web_server_ota.assert_called_once_with( + ["192.168.1.100"], 80, "admin", "pw", expected_firmware + ) + mock_run_ota.assert_not_called() + + +def test_upload_program_web_server_no_auth( + mock_run_web_server_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """web_server OTA works without an auth block (passes None for credentials).""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_web_server_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [{CONF_PLATFORM: CONF_WEB_SERVER}], + CONF_WEB_SERVER: {CONF_PORT: 8080}, + } + args = MockArgs() + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_web_server_ota.assert_called_once_with( + ["192.168.1.100"], 8080, None, None, expected_firmware + ) + + +def test_upload_program_both_platforms_default_prefers_native( + mock_run_ota: Mock, + mock_run_web_server_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """When both OTA platforms are configured, default selection is native API.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "secret", + }, + {CONF_PLATFORM: CONF_WEB_SERVER}, + ], + CONF_WEB_SERVER: {CONF_PORT: 80}, + } + args = MockArgs() + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once() + mock_run_web_server_ota.assert_not_called() + + +def test_upload_program_ota_platform_override_to_web_server( + mock_run_ota: Mock, + mock_run_web_server_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--ota-platform web_server forces web_server OTA even when native is configured.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_web_server_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "secret", + }, + {CONF_PLATFORM: CONF_WEB_SERVER}, + ], + CONF_WEB_SERVER: {CONF_PORT: 80}, + } + args = MockArgs(ota_platform=CONF_WEB_SERVER) + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_not_called() + mock_run_web_server_ota.assert_called_once() + + +def test_upload_program_ota_platform_unavailable( + mock_get_port_type: Mock, +) -> None: + """--ota-platform must reference a platform that is actually configured.""" + setup_core(platform=PLATFORM_ESP32) + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "secret", + } + ], + } + args = MockArgs(ota_platform=CONF_WEB_SERVER) + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError, match="--ota-platform web_server"): + upload_program(config, args, devices) + + +def test_upload_program_web_server_missing_component( + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """web_server OTA without a web_server component fails with a clear error.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [{CONF_PLATFORM: CONF_WEB_SERVER}], + # No CONF_WEB_SERVER + } + args = MockArgs() + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError, match="web_server.*not configured"): + upload_program(config, args, devices) + + +def test_upload_program_unrelated_ota_platform_ignored( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """OTA list entries that are neither esphome nor web_server are ignored. + + Covers the false branch in _choose_ota_platform's filter loop and the + no-match branch in _upload_via_native_api's lookup loop. + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + {CONF_PLATFORM: "http_request"}, # unrelated platform; ignored + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_PASSWORD: "secret", + }, + ], + } + args = MockArgs() + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once() + + +def test_upload_program_duplicate_platform_dedup_in_error( + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Duplicate same-platform OTA entries don't repeat in --ota-platform errors.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + {CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232}, + {CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3233}, + ], + } + args = MockArgs(ota_platform=CONF_WEB_SERVER) + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError) as excinfo: + upload_program(config, args, devices) + + # Error mentions esphome once in the platform list, not "esphome, esphome". + msg = str(excinfo.value) + assert "esphome, esphome" not in msg + assert msg.endswith(": esphome") + + +def test_upload_program_only_unrelated_ota_platforms( + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Only unrelated OTA platforms configured -> raises like missing OTA.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [{CONF_PLATFORM: "http_request"}], + } + args = MockArgs() + devices = ["192.168.1.100"] + + with pytest.raises(EsphomeError, match="Cannot upload Over the Air"): + upload_program(config, args, devices) + + def test_upload_program_ota_with_mqtt_resolution( mock_mqtt_get_ip: Mock, mock_is_ip_address: Mock, diff --git a/tests/unit_tests/test_web_server_ota.py b/tests/unit_tests/test_web_server_ota.py new file mode 100644 index 00000000000..606905e36e8 --- /dev/null +++ b/tests/unit_tests/test_web_server_ota.py @@ -0,0 +1,670 @@ +"""Unit tests for esphome.web_server_ota module.""" + +from __future__ import annotations + +import io +import logging +from pathlib import Path +import socket +from unittest.mock import MagicMock, patch + +import pytest +import requests +from requests.auth import HTTPBasicAuth + +from esphome.core import CORE, EsphomeError +from esphome.helpers import ProgressBar +from esphome.web_server_ota import ( + OTA_PATH, + WebServerOTAError, + _MultipartStreamer, + run_ota, +) + + +@pytest.fixture +def firmware(tmp_path: Path) -> Path: + binary = tmp_path / "firmware.bin" + binary.write_bytes(b"\x00\x01\x02FIRMWARE\xff" * 64) + return binary + + +def _make_response(status: int, body: str) -> MagicMock: + response = MagicMock(spec=requests.Response) + response.status_code = status + response.text = body + response.reason = "" + return response + + +def _patch_resolve( + monkeypatch: pytest.MonkeyPatch, hosts: list[tuple[str, int]] +) -> None: + """Replace resolve_ip_address so tests don't actually do DNS.""" + addr_infos = [ + (socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port)) + for host, port in hosts + ] + monkeypatch.setattr( + "esphome.web_server_ota.resolve_ip_address", lambda *a, **kw: addr_infos + ) + + +# --------------------------------------------------------------------------- +# _MultipartStreamer +# --------------------------------------------------------------------------- + + +def test_multipart_streamer_emits_full_body() -> None: + """Streaming the whole body in one call yields prefix + file + suffix.""" + data = b"abcdef" * 100 + streamer = _MultipartStreamer(io.BytesIO(data), len(data), "fw.bin") + + body = streamer.read() + while True: + chunk = streamer.read() + if not chunk: + break + body += chunk + + assert body.startswith(f"--{streamer.boundary}\r\n".encode()) + assert b'name="update"' in body + assert b'filename="fw.bin"' in body + assert data in body + assert body.endswith(f"\r\n--{streamer.boundary}--\r\n".encode()) + + +def test_multipart_streamer_chunked_read_matches_full_read() -> None: + """Chunked reads (urllib3 calls read(8192) repeatedly) yield the same body.""" + data = b"abcdef" * 1000 # 6000 bytes + full = _MultipartStreamer(io.BytesIO(data), len(data), "fw.bin").read() + + streamed = bytearray() + s = _MultipartStreamer(io.BytesIO(data), len(data), "fw.bin") + # Same boundary lengths -> identical total length. + while True: + chunk = s.read(64) + if not chunk: + break + streamed += chunk + # Boundaries are random per instance, so compare lengths and structure. + assert len(streamed) == len(full) + assert streamed.startswith(f"--{s.boundary}\r\n".encode()) + assert streamed.endswith(f"\r\n--{s.boundary}--\r\n".encode()) + + +def test_multipart_streamer_len_matches_emitted_bytes() -> None: + """``__len__`` is what urllib3 uses to set Content-Length, so it must + equal the total bytes emitted by ``read``.""" + data = b"x" * 12345 + s = _MultipartStreamer(io.BytesIO(data), len(data), "fw.bin") + declared = len(s) + + emitted = 0 + while True: + chunk = s.read(1024) + if not chunk: + break + emitted += len(chunk) + + assert emitted == declared + + +def test_multipart_streamer_progress_ticks_during_read() -> None: + """Each read advances the progress bar (this is the whole point of + streaming via ``data=``: progress reflects bytes leaving the host).""" + data = b"x" * 1000 + s = _MultipartStreamer(io.BytesIO(data), len(data), "fw.bin") + + updates: list[float] = [] + s.progress.update = updates.append # type: ignore[method-assign] + + while True: + chunk = s.read(128) + if not chunk: + break + + assert updates, "progress.update was never called" + # Strictly non-decreasing. + assert updates == sorted(updates) + # Final update reaches (within FP) 1.0 because all bytes were read. + assert updates[-1] == pytest.approx(1.0, abs=1e-9) + + +def test_multipart_streamer_content_type_includes_boundary() -> None: + s = _MultipartStreamer(io.BytesIO(b""), 0, "fw.bin") + assert s.content_type == f"multipart/form-data; boundary={s.boundary}" + + +def test_multipart_streamer_zero_size_file() -> None: + """A zero-byte file still produces a well-formed body and progress is + skipped (avoiding a divide-by-zero on the empty file segment).""" + s = _MultipartStreamer(io.BytesIO(b""), 0, "empty.bin") + body = b"" + while True: + chunk = s.read(64) + if not chunk: + break + body += chunk + assert body.startswith(f"--{s.boundary}".encode()) + assert body.endswith(f"--{s.boundary}--\r\n".encode()) + + +def test_multipart_streamer_unique_boundary_per_instance() -> None: + a = _MultipartStreamer(io.BytesIO(b""), 0, "a") + b = _MultipartStreamer(io.BytesIO(b""), 0, "a") + assert a.boundary != b.boundary + + +def test_multipart_streamer_zero_size_read_returns_empty() -> None: + """``read(0)`` short-circuits without touching state.""" + s = _MultipartStreamer(io.BytesIO(b"x" * 10), 10, "fw.bin") + assert s.read(0) == b"" + # No bytes consumed. + assert s._sent == 0 + + +# --------------------------------------------------------------------------- +# run_ota +# --------------------------------------------------------------------------- + + +def test_run_ota_success(monkeypatch: pytest.MonkeyPatch, firmware: Path) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + exit_code, host = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 0 + assert host == "192.168.1.50" + post.assert_called_once() + args, kwargs = post.call_args + assert args == (f"http://192.168.1.50:80{OTA_PATH}",) + assert kwargs["auth"] is None + # Streaming body, not files=, so progress fires during transmission. + assert "files" not in kwargs + assert isinstance(kwargs["data"], _MultipartStreamer) + assert kwargs["headers"]["Content-Type"] == kwargs["data"].content_type + assert kwargs["headers"]["Connection"] == "close" + + +def test_run_ota_logs_device_response_body( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The device's HTTP response body is surfaced on success.""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + caplog.set_level(logging.INFO, logger="esphome.web_server_ota") + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ): + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert "Device response: Update Successful!" in caplog.text + assert "OTA successful" in caplog.text + + +def test_run_ota_log_says_via_web_server( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The upload-start log line names the transport explicitly.""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + caplog.set_level(logging.INFO, logger="esphome.web_server_ota") + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ): + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert "via web_server OTA" in caplog.text + + +def test_run_ota_sends_basic_auth( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + exit_code, _ = run_ota(["192.168.1.50"], 80, "admin", "secret", firmware) + + assert exit_code == 0 + auth = post.call_args.kwargs["auth"] + assert isinstance(auth, HTTPBasicAuth) + assert auth.username == "admin" + assert auth.password == "secret" + + +def test_run_ota_skips_auth_when_no_credentials( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert post.call_args.kwargs["auth"] is None + + +def test_run_ota_skips_auth_when_only_username( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """Both username and password are required to send Basic auth.""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + run_ota(["192.168.1.50"], 80, "admin", None, firmware) + + assert post.call_args.kwargs["auth"] is None + + +def test_run_ota_uses_update_url( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 8080)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + run_ota(["192.168.1.50"], 8080, None, None, firmware) + + url = post.call_args.args[0] + assert url == f"http://192.168.1.50:8080{OTA_PATH}" + assert OTA_PATH == "/update" + + +def test_run_ota_failure_response( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Failed!"), + ): + exit_code, host = run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + assert "OTA failure" in caplog.text + + +def test_run_ota_failure_response_empty_body( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, ""), + ): + exit_code, host = run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + assert "no response body" in caplog.text + + +def test_run_ota_auth_failed( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(401, "Unauthorized"), + ): + exit_code, host = run_ota(["192.168.1.50"], 80, "user", "wrong", firmware) + + assert exit_code == 1 + assert host is None + assert "Authentication failed" in caplog.text + + +def test_run_ota_unexpected_status_code( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(500, "Internal Error"), + ): + exit_code, host = run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + assert "Unexpected HTTP 500" in caplog.text + + +def test_run_ota_unexpected_status_empty_body_falls_back( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Empty response body uses response.reason / a fallback in the error.""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + response = _make_response(503, "") + response.reason = "Service Unavailable" + + with patch( + "esphome.web_server_ota.requests.post", + return_value=response, + ): + exit_code, host = run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + assert "Service Unavailable" in caplog.text + + +def test_run_ota_unexpected_status_no_body_no_reason( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Empty body and empty reason still produce a usable error message.""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + response = _make_response(599, "") + response.reason = "" + + with patch( + "esphome.web_server_ota.requests.post", + return_value=response, + ): + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert "no response body" in caplog.text + + +def test_run_ota_connection_error_then_success( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """First resolved address fails to connect, second succeeds.""" + _patch_resolve( + monkeypatch, + [("192.168.1.10", 80), ("192.168.1.50", 80)], + ) + + with patch( + "esphome.web_server_ota.requests.post", + side_effect=[ + requests.ConnectionError("refused"), + _make_response(200, "Update Successful!"), + ], + ) as post: + exit_code, host = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 0 + assert host == "192.168.1.50" + assert post.call_count == 2 + + +def test_run_ota_request_exception_falls_through( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """A non-ConnectionError RequestException (e.g. timeout) falls through too.""" + _patch_resolve( + monkeypatch, + [("192.168.1.10", 80), ("192.168.1.50", 80)], + ) + + with patch( + "esphome.web_server_ota.requests.post", + side_effect=[ + requests.Timeout("read timeout"), + _make_response(200, "Update Successful!"), + ], + ): + exit_code, host = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 0 + assert host == "192.168.1.50" + + +def test_run_ota_all_addresses_unreachable( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """When every resolved address fails to connect, run_ota returns failure.""" + _patch_resolve( + monkeypatch, + [("192.168.1.10", 80), ("192.168.1.20", 80)], + ) + + with patch( + "esphome.web_server_ota.requests.post", + side_effect=requests.ConnectionError("refused"), + ): + exit_code, host = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + # Per-address failure is logged for each attempt; final summary follows. + assert caplog.text.count("OTA upload to ") >= 2 + assert "OTA upload failed." in caplog.text + + +def test_run_ota_no_resolved_addresses( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """If resolve_ip_address returns no candidates, log and return failure.""" + _patch_resolve(monkeypatch, []) + + exit_code, host = run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + assert "Could not resolve 192.168.1.50" in caplog.text + + +def test_run_ota_resolution_failure( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + def _raise(*_args, **_kwargs): + raise EsphomeError("dns failed") + + monkeypatch.setattr("esphome.web_server_ota.resolve_ip_address", _raise) + + exit_code, host = run_ota(["does.not.exist"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + + +def test_run_ota_resolution_failure_dashboard_mode( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Dashboard mode skips the '--device <IP>' tip on resolution failure.""" + + def _raise(*_args, **_kwargs): + raise EsphomeError("dns failed") + + monkeypatch.setattr("esphome.web_server_ota.resolve_ip_address", _raise) + monkeypatch.setattr(CORE, "dashboard", True) + try: + exit_code, host = run_ota(["does.not.exist"], 80, None, None, firmware) + finally: + monkeypatch.setattr(CORE, "dashboard", False) + + assert exit_code == 1 + assert host is None + assert "--device <IP>" not in caplog.text + + +def test_run_ota_empty_hosts(firmware: Path) -> None: + exit_code, host = run_ota([], 80, None, None, firmware) + assert exit_code == 1 + assert host is None + + +def test_run_ota_string_host_accepted( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """A bare string is accepted in addition to a list of hosts.""" + _patch_resolve(monkeypatch, [("10.0.0.5", 80)]) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ): + exit_code, host = run_ota("10.0.0.5", 80, None, None, firmware) + + assert exit_code == 0 + assert host == "10.0.0.5" + + +def test_run_ota_multiple_hosts_first_fails( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """Multi-host fallthrough: first host's addresses all fail, second host wins.""" + addr_lookup = { + "primary.local": [ + (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("192.168.1.10", 80)), + ], + "secondary.local": [ + (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("192.168.1.50", 80)), + ], + } + + def _resolve(host, port, address_cache=None): # noqa: ARG001 + return addr_lookup[host] + + monkeypatch.setattr("esphome.web_server_ota.resolve_ip_address", _resolve) + + with patch( + "esphome.web_server_ota.requests.post", + side_effect=[ + requests.ConnectionError("refused"), + _make_response(200, "Update Successful!"), + ], + ): + exit_code, host = run_ota( + ["primary.local", "secondary.local"], 80, None, None, firmware + ) + + assert exit_code == 0 + assert host == "192.168.1.50" + + +def test_run_ota_all_hosts_return_failure_no_exception( + monkeypatch: pytest.MonkeyPatch, firmware: Path, caplog: pytest.LogCaptureFixture +) -> None: + """All hosts resolve to no addresses; run_ota cleanly returns failure.""" + addr_lookup = { + "a.local": [], + "b.local": [], + } + + def _resolve(host, port, address_cache=None): # noqa: ARG001 + return addr_lookup[host] + + monkeypatch.setattr("esphome.web_server_ota.resolve_ip_address", _resolve) + + exit_code, host = run_ota(["a.local", "b.local"], 80, None, None, firmware) + + assert exit_code == 1 + assert host is None + # Each host gets its own "Could not resolve" log line + final summary. + assert caplog.text.count("Could not resolve") == 2 + assert "OTA upload failed." in caplog.text + + +def test_web_server_ota_error_is_esphome_error() -> None: + assert issubclass(WebServerOTAError, EsphomeError) + + +def test_run_ota_finalizes_progress_bar_on_success( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """progress.done() fires on the success path (finally block).""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + done_called: list[bool] = [] + + with ( + patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ), + patch.object(ProgressBar, "done", lambda self: done_called.append(True)), + ): + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert done_called + + +def test_run_ota_finalizes_progress_bar_on_failure( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """progress.done() fires when the request itself raises (finally block).""" + _patch_resolve(monkeypatch, [("192.168.1.50", 80)]) + + done_called: list[bool] = [] + + with ( + patch( + "esphome.web_server_ota.requests.post", + side_effect=requests.ConnectionError("boom"), + ), + patch.object(ProgressBar, "done", lambda self: done_called.append(True)), + ): + run_ota(["192.168.1.50"], 80, None, None, firmware) + + assert done_called + + +def test_run_ota_ipv6_url_brackets_host( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """IPv6 candidates are bracketed in the URL so the port parses correctly.""" + addr_infos = [ + (socket.AF_INET6, socket.SOCK_STREAM, 0, "", ("2001:db8::1", 80, 0, 0)), + ] + monkeypatch.setattr( + "esphome.web_server_ota.resolve_ip_address", lambda *a, **kw: addr_infos + ) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + exit_code, host = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 0 + assert host == "2001:db8::1" + url = post.call_args.args[0] + assert url == f"http://[2001:db8::1]:80{OTA_PATH}" + + +def test_run_ota_ipv6_link_local_includes_scope_id( + monkeypatch: pytest.MonkeyPatch, firmware: Path +) -> None: + """Link-local IPv6 candidates include the percent-encoded zone index.""" + addr_infos = [ + (socket.AF_INET6, socket.SOCK_STREAM, 0, "", ("fe80::1", 80, 0, 3)), + ] + monkeypatch.setattr( + "esphome.web_server_ota.resolve_ip_address", lambda *a, **kw: addr_infos + ) + + with patch( + "esphome.web_server_ota.requests.post", + return_value=_make_response(200, "Update Successful!"), + ) as post: + exit_code, _ = run_ota(["device.local"], 80, None, None, firmware) + + assert exit_code == 0 + url = post.call_args.args[0] + assert url == f"http://[fe80::1%253]:80{OTA_PATH}" From 39b2b901f7ae3cd6000a46bafaa56358a1f739bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:26:19 -0500 Subject: [PATCH 1852/2056] [core] Replace scheduler pool vector with unbounded intrusive freelist (#16172) --- esphome/core/application.cpp | 7 ++ esphome/core/scheduler.cpp | 97 ++++++++++++------- esphome/core/scheduler.h | 34 ++++--- tests/benchmarks/core/bench_scheduler.cpp | 10 +- .../integration/fixtures/scheduler_pool.yaml | 10 +- tests/integration/test_scheduler_pool.py | 24 +++-- 6 files changed, 115 insertions(+), 67 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index d03696fbb65..38d3503c2c3 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -25,6 +25,10 @@ namespace esphome { static const char *const TAG = "app"; +// Delay after setup() finishes before trimming the scheduler freelist of its post-boot peak. +// 10 s is well past the bulk of post-setup async work (Wi-Fi/MQTT connects, first-read latency). +static constexpr uint32_t SCHEDULER_FREELIST_TRIM_DELAY_MS = 10000; + // Helper function for insertion sort of components by priority // Using insertion sort instead of std::stable_sort saves ~1.3KB of flash // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) @@ -112,6 +116,9 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); + // Trim the scheduler freelist of its post-boot peak once startup churn settles. + this->scheduler.set_timeout(this, SCHEDULER_FREELIST_TRIM_DELAY_MS, [this]() { this->scheduler.trim_freelist(); }); + #ifdef USE_SETUP_PRIORITY_OVERRIDE // Clear setup priority overrides to free memory clear_setup_priority_overrides(); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 57deeab0da3..a7c624486db 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,18 +14,8 @@ namespace esphome { static const char *const TAG = "scheduler"; -// Memory pool configuration constants -// Pool size of 5 matches typical usage patterns (2-4 active timers) -// - Minimal memory overhead (~250 bytes on ESP32) -// - Sufficient for most configs with a couple sensors/components -// - Still prevents heap fragmentation and allocation stalls -// - Complex setups with many timers will just allocate beyond the pool -// See https://github.com/esphome/backlog/issues/52 -static constexpr size_t MAX_POOL_SIZE = 5; - // Maximum number of logically deleted (cancelled) items before forcing cleanup. -// Set to 5 to match the pool size - when we have as many cancelled items as our -// pool can hold, it's time to clean up and recycle them. +// Empirically chosen to balance cleanup overhead against tombstone accumulation in items_. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -165,7 +155,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type delay = 1; } - // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check + // Take lock early to protect scheduler_item_pool_head_ access and retry-cancelled check LockGuard guard{this->lock_}; // For retries, check if there's a cancelled timeout first - before allocating an item. @@ -599,7 +589,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector<SchedulerItem *> old_items; - ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_size_, now_64); // Cleanup before debug output this->cleanup_(); @@ -894,30 +884,68 @@ bool HOT Scheduler::SchedulerItem::cmp(SchedulerItem *a, SchedulerItem *b) { : (a->next_execution_high_ > b->next_execution_high_); } -// Recycle a SchedulerItem back to the pool for reuse. -// IMPORTANT: Caller must hold the scheduler lock before calling this function. -// This protects scheduler_item_pool_ from concurrent access by other threads -// that may be acquiring items from the pool in set_timer_common_(). +// Recycle a SchedulerItem back to the freelist for reuse. +// IMPORTANT: Caller must hold the scheduler lock. void Scheduler::recycle_item_main_loop_(SchedulerItem *item) { if (item == nullptr) return; - if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { - // Clear callback to release captured resources - item->callback = nullptr; - this->scheduler_item_pool_.push_back(item); + item->callback = nullptr; // release captured resources + item->next_free = this->scheduler_item_pool_head_; + this->scheduler_item_pool_head_ = item; + this->scheduler_item_pool_size_++; #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); -#endif - } else { -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); + ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_size_); #endif +} + +// Shrink a SchedulerItem* vector's capacity to its current size. +// std::vector::shrink_to_fit() is non-binding and our toolchain ignores it; the classic +// swap-with-copy idiom (std::vector<T>(other).swap(other)) instantiates the iterator-range +// constructor which pulls in std::__throw_bad_array_new_length and ~120 B of related +// stdlib RTTI/typeinfo. Build into a temp via reserve + push_back instead, then move-assign: +// reserve uses operator new (throws bad_alloc, already linked) and push_back without growth +// is the noexcept tail path. Move-assign just swaps pointers. +// Out-of-line + noinline so the callers in trim_freelist() share one body. +void __attribute__((noinline)) Scheduler::shrink_scheduler_vector_(std::vector<SchedulerItem *> *v) { + if (v->capacity() == v->size()) + return; // already exact, common after a quiet period + std::vector<SchedulerItem *> tmp; + tmp.reserve(v->size()); + for (SchedulerItem *p : *v) + tmp.push_back(p); + *v = std::move(tmp); +} + +void Scheduler::trim_freelist() { + LockGuard guard{this->lock_}; + SchedulerItem *item = this->scheduler_item_pool_head_; + size_t freed = 0; + while (item != nullptr) { + SchedulerItem *next = item->next_free; delete item; #ifdef ESPHOME_DEBUG_SCHEDULER this->debug_live_items_--; #endif + item = next; + freed++; } + this->scheduler_item_pool_head_ = nullptr; + this->scheduler_item_pool_size_ = 0; + + // items_/to_add_/defer_queue_ retain their boot-peak vector capacity (vector grows + // by doubling and otherwise keeps the peak). Reclaim that slack as well. + shrink_scheduler_vector_(&this->items_); + shrink_scheduler_vector_(&this->to_add_); +#ifndef ESPHOME_THREAD_SINGLE + shrink_scheduler_vector_(&this->defer_queue_); +#endif + +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Freelist trimmed (%zu items freed)", freed); +#else + (void) freed; +#endif } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -942,14 +970,15 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, } #endif /* ESPHOME_DEBUG_SCHEDULER */ -// Helper to get or create a scheduler item from the pool -// IMPORTANT: Caller must hold the scheduler lock before calling this function. +// Pop from freelist or allocate. IMPORTANT: caller must hold the lock and must overwrite +// `item->component` before releasing it -- the popped slot still holds the freelist link. Scheduler::SchedulerItem *Scheduler::get_item_from_pool_locked_() { - if (!this->scheduler_item_pool_.empty()) { - SchedulerItem *item = this->scheduler_item_pool_.back(); - this->scheduler_item_pool_.pop_back(); + if (this->scheduler_item_pool_head_ != nullptr) { + SchedulerItem *item = this->scheduler_item_pool_head_; + this->scheduler_item_pool_head_ = item->next_free; + this->scheduler_item_pool_size_--; #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); + ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_size_); #endif return item; } @@ -967,7 +996,7 @@ Scheduler::SchedulerItem *Scheduler::get_item_from_pool_locked_() { bool Scheduler::debug_verify_no_leak_() const { // Invariant: every live SchedulerItem must be in exactly one container. // debug_live_items_ tracks allocations minus deletions. - size_t accounted = this->items_.size() + this->to_add_.size() + this->scheduler_item_pool_.size(); + size_t accounted = this->items_.size() + this->to_add_.size() + this->scheduler_item_pool_size_; #ifndef ESPHOME_THREAD_SINGLE accounted += this->defer_queue_.size(); #endif @@ -981,7 +1010,7 @@ bool Scheduler::debug_verify_no_leak_() const { ")", static_cast<uint32_t>(this->debug_live_items_), static_cast<uint32_t>(accounted), static_cast<uint32_t>(this->items_.size()), static_cast<uint32_t>(this->to_add_.size()), - static_cast<uint32_t>(this->scheduler_item_pool_.size()) + static_cast<uint32_t>(this->scheduler_item_pool_size_) #ifndef ESPHOME_THREAD_SINGLE , static_cast<uint32_t>(this->defer_queue_.size()) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 7a6be6bea9a..b640aa86fea 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -132,6 +132,12 @@ class Scheduler { // @return Timestamp of the last item that ran, or `now` unchanged if none ran. uint32_t call(uint32_t now); + // Reclaim memory held by the post-boot peak. Frees every SchedulerItem in the + // recycle freelist and shrinks items_/to_add_/defer_queue_ vector capacity to + // their current sizes (std::vector grows by doubling and otherwise retains the + // peak). Live items in those vectors are preserved. + void trim_freelist(); + // Move items from to_add_ into the main heap. // IMPORTANT: This method should only be called from the main thread (loop task). // Inlined: the fast path (nothing to add) is just an atomic load / empty check. @@ -177,8 +183,12 @@ class Scheduler { protected: struct SchedulerItem { - // Ordered by size to minimize padding - Component *component; + // Ordered by size to minimize padding. + // `component` while live; `next_free` while in scheduler_item_pool_head_ (mutually exclusive). + union { + Component *component; + SchedulerItem *next_free; + }; // Optimized name storage using tagged union - zero heap allocation union { const char *static_name; // For STATIC_STRING (string literals) and SELF_POINTER (caller's `this`) @@ -355,6 +365,10 @@ class Scheduler { SchedulerItem *get_item_from_pool_locked_(); private: + // Out-of-line helper that shrinks a SchedulerItem* vector's capacity to its current + // size. Centralised so trim_freelist() doesn't pay flash cost per call site. + void shrink_scheduler_vector_(std::vector<SchedulerItem *> *v); + // Helper to cancel matching items - must be called with lock held. // When find_first=true, stops after the first match (used by set_timer_common_ where // the cancel-before-add invariant guarantees at most one match). @@ -713,19 +727,15 @@ class Scheduler { #endif } - // Memory pool for recycling SchedulerItem objects to reduce heap churn. - // Design decisions: - // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items - // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups - // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32) - // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation - // can stall the entire system, causing timing issues and dropped events for any components that need - // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) - std::vector<SchedulerItem *> scheduler_item_pool_; + // Intrusive freelist threaded through SchedulerItem::next_free. Unbounded so it quiesces at the + // app's concurrent-timer high-water mark; the previous fixed cap caused steady-state new/delete + // churn on devices with many timers (see https://github.com/esphome/backlog/issues/52). + SchedulerItem *scheduler_item_pool_head_{nullptr}; + size_t scheduler_item_pool_size_{0}; #ifdef ESPHOME_DEBUG_SCHEDULER // Leak detection: tracks total live SchedulerItem allocations. - // Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_.size() + // Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_size_ // Verified periodically in call() to catch leaks early. size_t debug_live_items_{0}; diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 214fe0e4b87..32bbc2de887 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -101,8 +101,8 @@ static void Scheduler_SetTimeout(benchmark::State &state) { Component dummy_component; // Register 3 timeouts then call() — realistic worst case where multiple - // components schedule in the same loop iteration. Keeps item count within - // the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free. + // components schedule in the same loop iteration. warm_pool fills the + // freelist so acquire/recycle never falls back to malloc. static constexpr int kBatchSize = 3; static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); warm_pool(scheduler, &dummy_component, kBatchSize, 1000); @@ -209,9 +209,9 @@ static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) { Scheduler scheduler; Component dummy_component; - // Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure - // the performance cliff when the recycling pool is exhausted and items - // must be malloc'd/freed. + // Register 10 timeouts then call() — larger working set than the 3-item + // batches above. With the unbounded freelist, warm_pool preallocates 10 + // items so this measures steady-state, not malloc cliff. static constexpr int kBatchSize = 10; static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); warm_pool(scheduler, &dummy_component, kBatchSize, 1000); diff --git a/tests/integration/fixtures/scheduler_pool.yaml b/tests/integration/fixtures/scheduler_pool.yaml index 5389125188c..989c1535b01 100644 --- a/tests/integration/fixtures/scheduler_pool.yaml +++ b/tests/integration/fixtures/scheduler_pool.yaml @@ -221,14 +221,10 @@ script: - id: test_full_pool_reuse then: - lambda: |- - ESP_LOGI("test", "Phase 6: Testing pool size limits after Phase 5 items complete"); + ESP_LOGI("test", "Phase 6: Testing pool reuse after Phase 5 items complete"); - // At this point, all Phase 5 timeouts should have completed and been recycled. - // The pool should be at its maximum size (5). - // Creating 10 new items tests that: - // - First 5 items reuse from the pool - // - Remaining 5 items allocate new (pool empty) - // - Pool doesn't grow beyond MAX_POOL_SIZE of 5 + // Phase 5 timeouts have completed and been recycled. The freelist is unbounded; + // creating 10 new items reuses from it and only allocates fresh when empty. auto *component = id(test_sensor); int full_reuse_count = 10; diff --git a/tests/integration/test_scheduler_pool.py b/tests/integration/test_scheduler_pool.py index 021917cc25a..cc25190e302 100644 --- a/tests/integration/test_scheduler_pool.py +++ b/tests/integration/test_scheduler_pool.py @@ -180,16 +180,22 @@ async def test_scheduler_pool( # Verify pool behavior assert pool_recycle_count > 0, "Should have recycled items to pool" - # Check pool metrics - if pool_recycle_count > 0: - max_pool_size = 0 - for line in log_lines: - if match := recycle_pattern.search(line): - size = int(match.group(1)) - max_pool_size = max(max_pool_size, size) + # Pool is unbounded; the cap was the source of the churn it was meant to prevent. + assert pool_full_count == 0, ( + f"Pool should never report full (got {pool_full_count})" + ) - # Pool can grow up to its maximum of 5 - assert max_pool_size <= 5, f"Pool grew beyond maximum ({max_pool_size})" + # Verify the pool actually grew past the old MAX_POOL_SIZE=5 cap. + # Phase 5 + Phase 6 schedule 8 + 10 same-component timeouts respectively, so the + # observed peak should comfortably exceed 5. Without this lower-bound check, a + # silent regression that re-introduced a small cap could pass the test above. + max_pool_size = 0 + for line in log_lines: + if match := recycle_pattern.search(line): + max_pool_size = max(max_pool_size, int(match.group(1))) + assert max_pool_size > 5, ( + f"Pool should grow past the old cap of 5; observed peak {max_pool_size}" + ) # Log summary for debugging print("\nScheduler Pool Test Summary (Python Orchestrated):") From 67491c3194067a38b9463d24f7bac9cd64ea9c76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:26:52 -0500 Subject: [PATCH 1853/2056] [packages] Add resolve_packages single-call seam (#16235) --- esphome/components/packages/__init__.py | 59 +++++++++ .../component_tests/packages/test_packages.py | 120 ++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 47a1fd20a75..d63f17aa7ed 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -611,3 +611,62 @@ def merge_packages(config: dict) -> dict: config = reduce(lambda new, old: merge_config(old, new), merge_list, config) del config[CONF_PACKAGES] return config + + +def resolve_packages( + config: dict[str, Any], + *, + command_line_substitutions: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Load and merge ``packages:`` in one call; return the flattened config. + + Convenience wrapper around :func:`do_packages_pass` followed by + :func:`merge_packages`. External tools that want the package- + merged dict (without going through full schema validation via + :func:`esphome.config.read_config`) get one stable seam to call + instead of having to chain the two functions and stay in sync + with the pipeline order. + + Note: the full :func:`esphome.config.validate_config` pipeline + runs two extra passes around the merge that this wrapper + deliberately skips: + + 1. :func:`esphome.components.substitutions.do_substitution_pass` + runs BETWEEN :func:`do_packages_pass` and + :func:`merge_packages`, so ``${var}`` placeholders inside + package content are NOT resolved here. Callers that need + substitution should invoke ``do_substitution_pass`` + themselves between calls, or go through the full + ``validate_config``. + 2. :func:`esphome.config.resolve_extend_remove` runs AFTER + :func:`merge_packages`, so top-level ``!remove`` / ``!extend`` + markers are NOT applied here. A package-contributed block + paired with a top-level ``key: !remove`` will still appear + in the returned dict (the marker just sits next to it). + + The wrapper exists for the "what blocks did packages + contribute?" question — metadata callers that just need to + see merged top-level keys. It is NOT a stand-in for + :func:`esphome.config.validate_config` and the two passes + above are the reasons why. + + Used by: + + - ``esphome/device-builder`` — the new WebSocket dashboard + backend reads device metadata (api / wifi / target-platform + flags) off the merged config so packages contribute the same + blocks the compiler sees, not just whatever sits at the top + of the user's YAML. See + https://github.com/esphome/device-builder/issues/288 for the + bug this fixes. + + Returns *config* unchanged when ``packages:`` isn't present, so + callers can apply this unconditionally without having to peek + at the config first. + """ + if CONF_PACKAGES not in config: + return config + config = do_packages_pass( + config, command_line_substitutions=command_line_substitutions + ) + return merge_packages(config) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 13a6da9f2c6..8c809c5e911 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -14,6 +14,7 @@ from esphome.components.packages import ( do_packages_pass, is_package_definition, merge_packages, + resolve_packages, ) from esphome.components.substitutions import ContextVars, do_substitution_pass import esphome.config as config_module @@ -1621,3 +1622,122 @@ def test_remote_package_vars_resolved_against_sibling_package_substitutions( actual = packages_pass(config) assert actual[CONF_SENSOR][0]["pin"] == "GPIO5" + + +# --------------------------------------------------------------------------- +# resolve_packages — single-call wrapper around do_packages_pass + merge_packages +# --------------------------------------------------------------------------- + + +def test_resolve_packages_returns_config_unchanged_without_packages() -> None: + """No ``packages:`` key → no-op, same dict back.""" + config = {CONF_ESPHOME: {CONF_NAME: "test"}, CONF_WIFI: {CONF_SSID: "x"}} + result = resolve_packages(config) + assert result is config + assert CONF_PACKAGES not in result + + +def test_resolve_packages_loads_and_merges_in_one_call() -> None: + """End-to-end: a config with one local-dict package gets its blocks flattened.""" + config = { + CONF_ESPHOME: {CONF_NAME: "main"}, + CONF_PACKAGES: { + "shared": { + CONF_WIFI: {CONF_SSID: "from_package"}, + CONF_SENSOR: [ + {CONF_PLATFORM: "template", CONF_NAME: "from_package_sensor"}, + ], + } + }, + } + result = resolve_packages(config) + # ``packages:`` is gone — it was consumed by the merge. + assert CONF_PACKAGES not in result + # Blocks contributed by the package are now top-level. + assert result[CONF_WIFI][CONF_SSID] == "from_package" + assert result[CONF_SENSOR][0][CONF_NAME] == "from_package_sensor" + # The main config's own keys survive untouched. + assert result[CONF_ESPHOME][CONF_NAME] == "main" + + +def test_resolve_packages_preserves_main_config_overrides() -> None: + """Main-config values win over package values for the same key. + + Pinning the precedence ESPHome's compiler uses so any future + refactor of the wrapper doesn't accidentally flip the order. + """ + config = { + CONF_ESPHOME: {CONF_NAME: "main"}, + CONF_WIFI: {CONF_SSID: "main_wins"}, + CONF_PACKAGES: { + "shared": {CONF_WIFI: {CONF_SSID: "package_loses"}}, + }, + } + result = resolve_packages(config) + assert result[CONF_WIFI][CONF_SSID] == "main_wins" + + +def test_resolve_packages_forwards_command_line_substitutions() -> None: + """``command_line_substitutions`` reaches the underlying ``do_packages_pass``. + + The wrapper exists so external tools have one stable seam; if + that seam silently dropped a kwarg the underlying call accepts, + callers would see surprising behaviour. This pins the + pass-through. + """ + config = { + CONF_ESPHOME: {CONF_NAME: "main"}, + CONF_PACKAGES: {"shared": {CONF_WIFI: {CONF_SSID: "from_package"}}}, + } + with patch( + "esphome.components.packages.do_packages_pass", + wraps=do_packages_pass, + ) as spy: + resolve_packages(config, command_line_substitutions={"foo": "bar"}) + spy.assert_called_once() + _, kwargs = spy.call_args + assert kwargs.get("command_line_substitutions") == {"foo": "bar"} + + +def test_resolve_packages_does_not_run_substitutions() -> None: + """``${var}`` placeholders inside package content stay literal. + + The full ``validate_config`` pipeline runs ``do_substitution_pass`` + BETWEEN ``do_packages_pass`` and ``merge_packages``; this wrapper + skips it on purpose. Pin that contract so a future refactor can't + silently start resolving substitutions and break callers that + deliberately compose the passes themselves. + """ + config = { + CONF_ESPHOME: {CONF_NAME: "main"}, + CONF_SUBSTITUTIONS: {"ssid_value": "resolved_ssid"}, + CONF_PACKAGES: { + "shared": {CONF_WIFI: {CONF_SSID: "${ssid_value}"}}, + }, + } + result = resolve_packages(config) + # Without ``do_substitution_pass`` the placeholder is preserved. + assert result[CONF_WIFI][CONF_SSID] == "${ssid_value}" + + +def test_resolve_packages_does_not_apply_extend_remove() -> None: + """Top-level ``!remove`` / ``!extend`` markers stay in the merged dict. + + The full ``validate_config`` pipeline runs ``resolve_extend_remove`` + AFTER ``merge_packages``; this wrapper skips it on purpose. Pin + that contract: a package-contributed block paired with a top-level + ``!remove`` is left as-is for callers to handle (or for them to + call ``resolve_extend_remove`` themselves). + """ + config = { + CONF_ESPHOME: {CONF_NAME: "main"}, + CONF_WIFI: Remove(), + CONF_PACKAGES: { + "shared": {CONF_WIFI: {CONF_SSID: "from_package"}}, + }, + } + result = resolve_packages(config) + # ``merge_packages`` keeps the top-level ``!remove`` (it wins + # over the package value during merge), and the marker is not + # resolved by this wrapper. + assert isinstance(result[CONF_WIFI], Remove) From 4404dd68ba56d3c041b32cd8494bb386e3545134 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:27:18 -0500 Subject: [PATCH 1854/2056] [cover] Fix ControlAction / CoverPublishAction trigger args with reference types (#16227) --- esphome/components/cover/__init__.py | 19 +++++++++++++++---- esphome/components/cover/automation.h | 11 +++++++++-- tests/components/template/common-base.yaml | 13 +++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 954ad7a3457..839ca532e66 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -328,17 +328,28 @@ async def build_apply_lambda_action( Used by both `cover.control` and `cover.template.publish` (and shared with the template/cover platform). Constants are emitted as flash immediates; user lambdas are invoked inline so trigger args still flow. - The trigger arg types are wrapped as `const T &` to match the - `void (*)(..., const Ts &...)` ApplyFn signature. + Trigger arg types are normalized to `const std::remove_cvref_t<T> &` + to match the ApplyFn signature for any T (value, ref, or const-ref). """ paren = await cg.get_variable(config[CONF_ID]) + # Normalize trigger args to `const std::remove_cvref_t<T> &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] for field in fields: if (value := config.get(field.conf_key)) is None: continue if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=field.type_) + inner = await cg.process_lambda( + value, normalized_args, return_type=field.type_ + ) value_expr = f"({inner})({fwd_args})" else: value_expr = str(cg.safe_exp(value)) @@ -346,7 +357,7 @@ async def build_apply_lambda_action( apply_args = [ *prefix_args, - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index e2384c23593..ee7a4f5f760 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -51,10 +51,17 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `position: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template<typename... Ts> class ControlAction : public Action<Ts...> { public: - using ApplyFn = void (*)(CoverCall &, const Ts &...); + using ApplyFn = void (*)(CoverCall &, const std::remove_cvref_t<Ts> &...); ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { @@ -70,7 +77,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> { template<typename... Ts> class CoverPublishAction : public Action<Ts...> { public: - using ApplyFn = void (*)(Cover *, const Ts &...); + using ApplyFn = void (*)(Cover *, const std::remove_cvref_t<Ts> &...); CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 984ef129ad4..b97cafd25c3 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -369,6 +369,19 @@ number: - valve.control: id: template_valve position: !lambda "return x / 100.0f;" + # Same regression test for cover.control: forces the apply-lambda + # codegen to handle a non-empty trigger Ts (float). + - platform: template + id: template_cover_position_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + on_value: + then: + - cover.control: + id: template_cover_with_triggers + position: !lambda "return x / 100.0f;" select: - platform: template From f5c1b8839da0fdea431d11542dfd6f6bebe5332a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:29:10 -0500 Subject: [PATCH 1855/2056] [web_server] Use entity_types.h X-macro for ListEntitiesIterator declarations (#16077) --- .../components/web_server/list_entities.cpp | 4 + esphome/components/web_server/list_entities.h | 83 +++---------------- 2 files changed, 15 insertions(+), 72 deletions(-) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index c1e7599c7e4..869ed3ea173 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -167,5 +167,9 @@ bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { } #endif +#ifdef USE_MEDIA_PLAYER +bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *obj) { return true; } +#endif + } // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 9cfc6c7e330..3edb84f5553 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -24,78 +24,17 @@ class ListEntitiesIterator final : public ComponentIterator { #elif defined(USE_ARDUINO) ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); #endif -#ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *obj) override; -#endif -#ifdef USE_COVER - bool on_cover(cover::Cover *obj) override; -#endif -#ifdef USE_FAN - bool on_fan(fan::Fan *obj) override; -#endif -#ifdef USE_LIGHT - bool on_light(light::LightState *obj) override; -#endif -#ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *obj) override; -#endif -#ifdef USE_SWITCH - bool on_switch(switch_::Switch *obj) override; -#endif -#ifdef USE_BUTTON - bool on_button(button::Button *obj) override; -#endif -#ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *obj) override; -#endif -#ifdef USE_CLIMATE - bool on_climate(climate::Climate *obj) override; -#endif -#ifdef USE_NUMBER - bool on_number(number::Number *obj) override; -#endif -#ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *obj) override; -#endif -#ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *obj) override; -#endif -#ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *obj) override; -#endif -#ifdef USE_TEXT - bool on_text(text::Text *obj) override; -#endif -#ifdef USE_SELECT - bool on_select(select::Select *obj) override; -#endif -#ifdef USE_LOCK - bool on_lock(lock::Lock *obj) override; -#endif -#ifdef USE_VALVE - bool on_valve(valve::Valve *obj) override; -#endif -#ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *obj) override { return true; } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override; -#endif -#ifdef USE_WATER_HEATER - bool on_water_heater(water_heater::WaterHeater *obj) override; -#endif -#ifdef USE_INFRARED - bool on_infrared(infrared::Infrared *obj) override; -#endif -#ifdef USE_RADIO_FREQUENCY - bool on_radio_frequency(radio_frequency::RadioFrequency *obj) override; -#endif -#ifdef USE_EVENT - bool on_event(event::Event *obj) override; -#endif -#ifdef USE_UPDATE - bool on_update(update::UpdateEntity *obj) override; -#endif + +// Entity overrides (generated from entity_types.h). +// Implementations live in list_entities.cpp. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) bool on_##singular(type *obj) override; +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) bool completed() { return this->state_ == IteratorState::NONE; } protected: From bf1c339dc1ff83b34ece07e45b40de3239eb149e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:29:32 -0500 Subject: [PATCH 1856/2056] [api] Use entity_types.h X-macro for ListEntitiesIterator declarations (#16076) --- esphome/components/api/list_entities.h | 83 ++++---------------------- 1 file changed, 11 insertions(+), 72 deletions(-) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 95c626feb17..88fbdb77c87 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -18,83 +18,22 @@ class APIConnection; class ListEntitiesIterator final : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); -#ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; -#endif -#ifdef USE_COVER - bool on_cover(cover::Cover *entity) override; -#endif -#ifdef USE_FAN - bool on_fan(fan::Fan *entity) override; -#endif -#ifdef USE_LIGHT - bool on_light(light::LightState *entity) override; -#endif -#ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *entity) override; -#endif -#ifdef USE_SWITCH - bool on_switch(switch_::Switch *entity) override; -#endif -#ifdef USE_BUTTON - bool on_button(button::Button *entity) override; -#endif -#ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *entity) override; -#endif + +// Entity overrides (generated from entity_types.h). +// All implementations live in list_entities.cpp via LIST_ENTITIES_HANDLER. +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) bool on_##singular(type *entity) override; +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + ENTITY_TYPE_(type, singular, plural, count, upper) +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) #ifdef USE_API_USER_DEFINED_ACTIONS bool on_service(UserServiceDescriptor *service) override; #endif #ifdef USE_CAMERA bool on_camera(camera::Camera *entity) override; -#endif -#ifdef USE_CLIMATE - bool on_climate(climate::Climate *entity) override; -#endif -#ifdef USE_NUMBER - bool on_number(number::Number *entity) override; -#endif -#ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *entity) override; -#endif -#ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *entity) override; -#endif -#ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *entity) override; -#endif -#ifdef USE_TEXT - bool on_text(text::Text *entity) override; -#endif -#ifdef USE_SELECT - bool on_select(select::Select *entity) override; -#endif -#ifdef USE_LOCK - bool on_lock(lock::Lock *entity) override; -#endif -#ifdef USE_VALVE - bool on_valve(valve::Valve *entity) override; -#endif -#ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *entity) override; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; -#endif -#ifdef USE_WATER_HEATER - bool on_water_heater(water_heater::WaterHeater *entity) override; -#endif -#ifdef USE_INFRARED - bool on_infrared(infrared::Infrared *entity) override; -#endif -#ifdef USE_RADIO_FREQUENCY - bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override; -#endif -#ifdef USE_EVENT - bool on_event(event::Event *entity) override; -#endif -#ifdef USE_UPDATE - bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; From 700676b34001d58483b7df5dbd4e339ac3d882ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 5 May 2026 18:29:48 -0500 Subject: [PATCH 1857/2056] [api] Use entity_types.h X-macro for InitialStateIterator declarations (#16075) --- esphome/components/api/subscribe_state.cpp | 5 +- esphome/components/api/subscribe_state.h | 86 ++++------------------ 2 files changed, 18 insertions(+), 73 deletions(-) diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 4bbc17018e9..09b5640d8ac 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -67,7 +67,10 @@ INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater) INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif -// Special cases (button and event) are already defined inline in subscribe_state.h +// event is an ENTITY_CONTROLLER_TYPE_ but has no state to send. +#ifdef USE_EVENT +bool InitialStateIterator::on_event(event::Event *entity) { return true; } +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index f20611e06ac..6b1ae9651de 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -19,78 +19,20 @@ class APIConnection; class InitialStateIterator final : public ComponentIterator { public: InitialStateIterator(APIConnection *client); -#ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; -#endif -#ifdef USE_COVER - bool on_cover(cover::Cover *entity) override; -#endif -#ifdef USE_FAN - bool on_fan(fan::Fan *entity) override; -#endif -#ifdef USE_LIGHT - bool on_light(light::LightState *entity) override; -#endif -#ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *entity) override; -#endif -#ifdef USE_SWITCH - bool on_switch(switch_::Switch *entity) override; -#endif -#ifdef USE_BUTTON - bool on_button(button::Button *button) override { return true; }; -#endif -#ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *entity) override; -#endif -#ifdef USE_CLIMATE - bool on_climate(climate::Climate *entity) override; -#endif -#ifdef USE_NUMBER - bool on_number(number::Number *entity) override; -#endif -#ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *entity) override; -#endif -#ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *entity) override; -#endif -#ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *entity) override; -#endif -#ifdef USE_TEXT - bool on_text(text::Text *entity) override; -#endif -#ifdef USE_SELECT - bool on_select(select::Select *entity) override; -#endif -#ifdef USE_LOCK - bool on_lock(lock::Lock *entity) override; -#endif -#ifdef USE_VALVE - bool on_valve(valve::Valve *entity) override; -#endif -#ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *entity) override; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; -#endif -#ifdef USE_WATER_HEATER - bool on_water_heater(water_heater::WaterHeater *entity) override; -#endif -#ifdef USE_INFRARED - bool on_infrared(infrared::Infrared *infrared) override { return true; }; -#endif -#ifdef USE_RADIO_FREQUENCY - bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; }; -#endif -#ifdef USE_EVENT - bool on_event(event::Event *event) override { return true; }; -#endif -#ifdef USE_UPDATE - bool on_update(update::UpdateEntity *entity) override; -#endif + +// Entity overrides (generated from entity_types.h). +// ENTITY_TYPE_ entities have no state to send and default to a no-op. +// ENTITY_CONTROLLER_TYPE_ entities are implemented in subscribe_state.cpp via INITIAL_STATE_HANDLER, +// except on_event which has no state (defined out-of-line in subscribe_state.cpp). +// NOLINTBEGIN(bugprone-macro-parentheses) +#define ENTITY_TYPE_(type, singular, plural, count, upper) \ + bool on_##singular(type *entity) override { return true; } +#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \ + bool on_##singular(type *entity) override; +#include "esphome/core/entity_types.h" +#undef ENTITY_TYPE_ +#undef ENTITY_CONTROLLER_TYPE_ + // NOLINTEND(bugprone-macro-parentheses) protected: APIConnection *client_; From 2d6af1f7e5967e04aa7a2c5e337e77c8005a9a08 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 5 May 2026 20:22:53 -0400 Subject: [PATCH 1858/2056] [audio] Bump esp-audio-libs to v3.0.0 (#16263) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 80fd328e48e..cfb2ad4e757 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -334,7 +334,7 @@ async def to_code(config): add_idf_component( name="esphome/esp-audio-libs", - ref="2.0.4", + ref="3.0.0", ) data = _get_data() diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 14fb11ace54..37c0da11f57 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: bblanchon/arduinojson: version: "7.4.2" esphome/esp-audio-libs: - version: 2.0.4 + version: 3.0.0 esphome/esp-micro-speech-features: version: 1.2.3 esphome/micro-decoder: From a99c1b3e08de399c1edc9b9364ef293fb0dc1b1a Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 6 May 2026 02:37:03 +0200 Subject: [PATCH 1859/2056] [nrf52] add reserve area for bootloader (#16204) --- esphome/components/nrf52/boards.py | 9 ++++++--- esphome/components/zigbee/__init__.py | 13 ------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py index 6064fe844a2..4c33cd99399 100644 --- a/esphome/components/nrf52/boards.py +++ b/esphome/components/nrf52/boards.py @@ -31,12 +31,15 @@ BOARDS_ZEPHYR = { # https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map BOOTLOADER_CONFIG = { BOOTLOADER_ADAFRUIT_NRF52_SD132: [ - Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + Section("SoftDevice", 0x0, 0x26000, "flash_primary"), + Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"), ], BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [ - Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + Section("SoftDevice", 0x0, 0x26000, "flash_primary"), + Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"), ], BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [ - Section("empty_app_offset", 0x0, 0x27000, "flash_primary"), + Section("SoftDevice", 0x0, 0x27000, "flash_primary"), + Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"), ], } diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 018dab73488..8605b4fa1a3 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -9,9 +9,6 @@ from esphome.components.esp32.const import ( VARIANT_ESP32C6, VARIANT_ESP32H2, ) -from esphome.components.nrf52.boards import BOOTLOADER_CONFIG, Section -from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data -from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -53,15 +50,6 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@luar123", "@tomaszduda23"] -def zigbee_set_core_data(config: ConfigType) -> ConfigType: - if CORE.is_nrf52 and zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: - zephyr_add_pm_static( - [Section("empty_after_zboss_offset", 0xF4000, 0xC000, "flash_primary")] - ) - - return config - - BINARY_SENSOR_SCHEMA = cv.Schema( { cv.Optional(CONF_REPORT): cv.All( @@ -119,7 +107,6 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), _validate_router_sleepy, zigbee_require_vfs_select, - zigbee_set_core_data, cv.Any( cv.All( cv.only_on_esp32, From e9f7579910386c07065a6184bb0245d4fd6170d9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Wed, 6 May 2026 03:37:40 +0200 Subject: [PATCH 1860/2056] [logger] give a chance to print crash (#16203) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/logger/logger_zephyr.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 7fa9e42c6a0..e9caa8d9d99 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -80,8 +80,8 @@ void Logger::pre_setup() { this->uart_dev_ = uart_dev; #if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) uint32_t dtr = 0; - uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs - while (dtr == 0 && count-- != 0) { + int32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs + while (dtr == 0 && count-- > 0) { uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); delay(10); arch_feed_wdt(); @@ -160,6 +160,11 @@ void Logger::dump_crash_() { #if defined(CONFIG_THREAD_NAME) ESP_LOGE(TAG, "Thread: %s", crash_buf.thread); #endif + int32_t count = (2 * 100); // wait 2 sec to give a chance to print crash + while (count-- > 0) { + delay(10); + arch_feed_wdt(); + } } } From 6f6d991dd21f003bc612010ba2cd785e8a9611dc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 May 2026 21:42:11 +1200 Subject: [PATCH 1861/2056] [ha-addon] Add opt-in toggle for the new ESPHome Device Builder (#16247) --- .../etc/cont-init.d/40-device-builder.sh | 22 +++++++++++++++++++ .../etc/s6-overlay/s6-rc.d/esphome/run | 7 ++++++ .../etc/s6-overlay/s6-rc.d/init-nginx/run | 8 +++++++ .../etc/s6-overlay/s6-rc.d/nginx/run | 8 +++++++ 4 files changed, 45 insertions(+) create mode 100755 docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh b/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh new file mode 100755 index 00000000000..b9904697626 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh @@ -0,0 +1,22 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Installs the latest prerelease of esphome-device-builder when the +# `use_new_device_builder` config option is enabled. +# This is a temporary install-on-boot step until esphome-device-builder +# becomes a direct dependency of esphome. +# ============================================================================== + +if ! bashio::config.true 'use_new_device_builder'; then + exit 0 +fi + +bashio::log.info "Installing latest prerelease of esphome-device-builder..." +if command -v uv > /dev/null; then + uv pip install --system --no-cache-dir --prerelease=allow --upgrade \ + esphome-device-builder || + bashio::exit.nok "Failed installing esphome-device-builder." +else + pip install --no-cache-dir --pre --upgrade esphome-device-builder || + bashio::exit.nok "Failed installing esphome-device-builder." +fi +bashio::log.info "Installed esphome-device-builder." diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index cdbaff6c04c..64ac0b18d2e 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -49,5 +49,12 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then rm -rf /config/esphome/.esphome fi +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "Starting ESPHome Device Builder..." + exec esphome-device-builder /config/esphome \ + --ha-addon \ + --ingress-port "$(bashio::addon.ingress_port)" +fi + bashio::log.info "Starting ESPHome dashboard..." exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run index 2725f56670b..18c75898ec0 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run @@ -4,6 +4,14 @@ # Community Hass.io Add-ons: ESPHome # Configures NGINX for use with ESPHome # ============================================================================== + +# When the new device builder is enabled it serves HA ingress directly, +# so nginx is not used at all -- skip configuration. +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly." + bashio::exit.ok +fi + mkdir -p /var/log/nginx # Generate Ingress configuration diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run index e96991cdd19..bb5f52e10c7 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -5,6 +5,14 @@ # Runs the NGINX proxy # ============================================================================== +# The new device builder handles HA ingress itself, so nginx is bypassed. +# Block the longrun forever so s6 keeps the dependency satisfied and does +# not respawn it. +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "NGINX bypassed: new device builder serves ingress directly." + exec sleep infinity +fi + bashio::log.info "Waiting for ESPHome dashboard to come up..." while [[ ! -S /var/run/esphome.sock ]]; do From febf8815c733778a25c8ab358b017a579217a94d Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 6 May 2026 05:59:51 -0400 Subject: [PATCH 1862/2056] [audio_file][speaker] Eliminate code duplication for files built into firmware (#16266) --- esphome/components/audio_file/__init__.py | 81 ++++---- .../speaker/media_player/__init__.py | 183 ++---------------- .../speaker/common-media_player.yaml | 13 ++ tests/components/speaker/test.wav | Bin 0 -> 46 bytes 4 files changed, 73 insertions(+), 204 deletions(-) create mode 100644 tests/components/speaker/test.wav diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py index b246633c31b..23c90e9b769 100644 --- a/esphome/components/audio_file/__init__.py +++ b/esphome/components/audio_file/__init__.py @@ -199,51 +199,60 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType] return config +def audio_files_schema() -> cv.All: + """Schema for a list of audio file entries. + + Validates each entry, downloads any web files, and detects the audio file + type while requesting codec support. Reusable by other components (e.g. + speaker media_player) that embed audio files in firmware without going + through the audio_file component's C++ registry. + """ + return cv.All( + cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + partial(download_web_files_in_config, path_for=_compute_local_file_path), + _validate_supported_local_file, + ) + + +def generate_audio_file_code(file_config: ConfigType) -> MockObj: + """Generate the progmem data, AudioFile struct, and Pvariable for one file. + + Returns the created Pvariable. Caller is responsible for any further + registration (the audio_file component additionally registers each file in + its named C++ registry; other consumers may skip that). + """ + cache = _get_data().file_cache + file_id = str(file_config[CONF_ID]) + if file_id in cache: + data, media_file_type = cache[file_id] + else: + data, media_file_type = read_audio_file_and_type(file_config) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + audio.AudioFile, + ("data", prog_arr), + ("length", len(rhs)), + ("file_type", media_file_type), + ) + + return cg.new_Pvariable(file_config[CONF_ID], media_files_struct) + + CONFIG_SCHEMA = cv.All( cv.only_on_esp32, - cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - partial(download_web_files_in_config, path_for=_compute_local_file_path), - _validate_supported_local_file, + audio_files_schema(), ) async def to_code(config: list[ConfigType]) -> None: - cache = _get_data().file_cache - for file_config in config: file_id = str(file_config[CONF_ID]) - data, media_file_type = cache[file_id] - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) - - media_files_struct = cg.StructInitializer( - audio.AudioFile, - ( - "data", - prog_arr, - ), - ( - "length", - len(rhs), - ), - ( - "file_type", - media_file_type, - ), - ) - - cg.new_Pvariable( - file_config[CONF_ID], - media_files_struct, - ) - - # Store file ID for cross-component access + file_var = generate_audio_file_code(file_config) _get_data().file_ids[file_id] = file_config[CONF_ID] + cg.add(audio_file_ns.add_named_audio_file(file_var, file_id)) # Register all files in the shared C++ registry cg.add_define("AUDIO_FILE_MAX_FILES", len(config)) - for file_config in config: - file_id = str(file_config[CONF_ID]) - file_var = await cg.get_variable(file_config[CONF_ID]) - cg.add(audio_file_ns.add_named_audio_file(file_var, file_id)) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 90d9309f460..094043c2927 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -1,13 +1,19 @@ """Speaker Media Player Setup.""" -from functools import partial -import hashlib import logging -from pathlib import Path -from esphome import automation, external_files +from esphome import automation import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, network, ota, psram, speaker +from esphome.components import ( + audio, + audio_file, + esp32, + media_player, + network, + ota, + psram, + speaker, +) from esphome.components.const import ( CONF_VOLUME_INCREMENT, CONF_VOLUME_INITIAL, @@ -17,23 +23,16 @@ from esphome.components.const import ( import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_FILE, CONF_FILES, CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_PATH, - CONF_RAW_DATA_ID, CONF_SAMPLE_RATE, CONF_SPEAKER, CONF_TASK_STACK_IN_PSRAM, - CONF_TYPE, - CONF_URL, ) -from esphome.core import CORE, HexInt -from esphome.external_files import download_web_files_in_config _LOGGER = logging.getLogger(__name__) @@ -44,9 +43,6 @@ DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" -TYPE_LOCAL = "local" -TYPE_WEB = "web" - CONF_ANNOUNCEMENT = "announcement" CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0 @@ -83,87 +79,12 @@ StopStreamAction = speaker_ns.class_( ) -def _compute_local_file_path(value: dict) -> Path: - url = value[CONF_URL] - h = hashlib.new("sha256") - h.update(url.encode()) - key = h.hexdigest()[:8] - base_dir = external_files.compute_local_file_dir(DOMAIN) - _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) - return base_dir / key - - _PURPOSE_MAP = { "MEDIA": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"], "ANNOUNCEMENT": media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"], } -def _file_schema(value): - if isinstance(value, str): - return _validate_file_shorthand(value) - return TYPED_FILE_SCHEMA(value) - - -def _read_audio_file_and_type(file_config): - conf_file = file_config[CONF_FILE] - file_source = conf_file[CONF_TYPE] - if file_source == TYPE_LOCAL: - path = CORE.relative_config_path(conf_file[CONF_PATH]) - elif file_source == TYPE_WEB: - path = _compute_local_file_path(conf_file) - else: - raise cv.Invalid("Unsupported file source") - - with open(path, "rb") as f: - data = f.read() - - import puremagic - - try: - file_type: str = puremagic.from_string(data) - file_type = file_type.removeprefix(".") - except puremagic.PureError as e: - raise cv.Invalid( - f"Unable to determine audio file type of '{path}'. " - f"Try re-encoding the file into a supported format. Details: {e}" - ) from e - - media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] - if file_type in ("wav"): - media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] - elif file_type in ("mp3", "mpeg", "mpga"): - media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] - elif file_type in ("flac"): - media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] - elif ( - file_type in ("ogg") - and len(data) >= 36 - and data.startswith(b"OggS") - and data[28:36] == b"OpusHead" - ): - media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] - - return data, media_file_type - - -def _validate_file_shorthand(value): - value = cv.string_strict(value) - if value.startswith("http://") or value.startswith("https://"): - return _file_schema( - { - CONF_TYPE: TYPE_WEB, - CONF_URL: value, - } - ) - return _file_schema( - { - CONF_TYPE: TYPE_LOCAL, - CONF_PATH: value, - } - ) - - _validate_pipeline = media_player.validate_preferred_format( "speaker media_player", CONF_SPEAKER ) @@ -192,60 +113,15 @@ def _final_validate(config): CONF_CODEC_SUPPORT_ENABLED, ) - # Request codecs based on pipeline formats + # Request codecs based on pipeline formats. Codecs needed by local files are + # already requested during CONFIG_SCHEMA validation (via audio_files_schema). media_player.request_codecs_for_format_configs( config, [CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE] ) - # Validate local files and request any additional codecs they need - for file_config in config.get(CONF_FILES, []): - _, media_file_type = _read_audio_file_and_type(file_config) - if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): - raise cv.Invalid("Unsupported local media file") - for fmt_name, fmt_enum in audio.AUDIO_FILE_TYPE_ENUM.items(): - if str(media_file_type) == str(fmt_enum): - if fmt_name == "FLAC": - audio.request_flac_support() - elif fmt_name == "MP3": - audio.request_mp3_support() - elif fmt_name == "OPUS": - audio.request_opus_support() - elif fmt_name == "WAV": - audio.request_wav_support() - break - return config -LOCAL_SCHEMA = cv.Schema( - { - cv.Required(CONF_PATH): cv.file_, - } -) - -WEB_SCHEMA = cv.Schema( - { - cv.Required(CONF_URL): cv.url, - } -) - - -TYPED_FILE_SCHEMA = cv.typed_schema( - { - TYPE_LOCAL: LOCAL_SCHEMA, - TYPE_WEB: WEB_SCHEMA, - }, -) - - -MEDIA_FILE_TYPE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(audio.AudioFile), - cv.Required(CONF_FILE): _file_schema, - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } -) - PIPELINE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(AudioPipeline), @@ -278,12 +154,7 @@ CONFIG_SCHEMA = cv.All( ), # Remove before 2026.10.0 cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string), - cv.Optional(CONF_FILES): cv.All( - cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - partial( - download_web_files_in_config, path_for=_compute_local_file_path - ), - ), + cv.Optional(CONF_FILES): audio_file.audio_files_schema(), cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( cv.boolean, cv.requires_component(psram.DOMAIN) ), @@ -380,31 +251,7 @@ async def to_code(config): ) for file_config in config.get(CONF_FILES, []): - data, media_file_type = _read_audio_file_and_type(file_config) - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) - - media_files_struct = cg.StructInitializer( - audio.AudioFile, - ( - "data", - prog_arr, - ), - ( - "length", - len(rhs), - ), - ( - "file_type", - media_file_type, - ), - ) - - cg.new_Pvariable( - file_config[CONF_ID], - media_files_struct, - ) + audio_file.generate_audio_file_code(file_config) @automation.register_action( diff --git a/tests/components/speaker/common-media_player.yaml b/tests/components/speaker/common-media_player.yaml index a849e04b33e..3b2212a0caa 100644 --- a/tests/components/speaker/common-media_player.yaml +++ b/tests/components/speaker/common-media_player.yaml @@ -17,3 +17,16 @@ media_player: volume_max: 0.95 volume_min: 0.0 task_stack_in_psram: true + files: + - id: speaker_test_audio + file: + type: local + path: $component_dir/test.wav + +script: + - id: play_built_in_file + then: + - media_player.speaker.play_on_device_media_file: + id: speaker_media_player_id + media_file: speaker_test_audio + announcement: true diff --git a/tests/components/speaker/test.wav b/tests/components/speaker/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..f9d07ef2238eb2fcb355055466d3789ee1a1fe0b GIT binary patch literal 46 ycmWIYbaPW<U|<M$40BD(Em06)U|?WmU}R{pV_;yYWnf@p5MW42EJ<Wy0098Ji3Z~U literal 0 HcmV?d00001 From 79786f1cc711f426a00926fcdfd42ee3b973239f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 6 May 2026 21:22:43 +1000 Subject: [PATCH 1863/2056] [lvgl] Allow line points as percentages (#16209) --- esphome/components/lvgl/lv_validation.py | 4 +- esphome/components/lvgl/schemas.py | 12 +- esphome/components/lvgl/widgets/canvas.py | 24 ++- esphome/components/lvgl/widgets/line.py | 17 +- .../lvgl/config/line_points.yaml | 84 ++++++++++ tests/component_tests/lvgl/test_line.py | 147 ++++++++++++++++++ tests/components/lvgl/lvgl-package.yaml | 5 +- 7 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 tests/component_tests/lvgl/config/line_points.yaml create mode 100644 tests/component_tests/lvgl/test_line.py diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 503730098e1..974eed9e817 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -41,7 +41,7 @@ from .helpers import ( lv_fonts_used, requires_component, ) -from .types import lv_gradient_t, lv_opa_t +from .types import lv_coord_t, lv_gradient_t, lv_opa_t LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -277,7 +277,7 @@ def pixels_or_percent_validator(value): pixels_or_percent = LValidator( pixels_or_percent_validator, - uint32, + lv_coord_t, retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"), ) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 2c57452a55c..62117fbd328 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -123,8 +123,8 @@ ENCODER_SCHEMA = cv.Schema( POINT_SCHEMA = cv.Schema( { - cv.Required(CONF_X): cv.templatable(cv.int_), - cv.Required(CONF_Y): cv.templatable(cv.int_), + cv.Required(CONF_X): lvalid.pixels_or_percent, + cv.Required(CONF_Y): lvalid.pixels_or_percent, } ) @@ -137,9 +137,13 @@ def point_schema(value): """ if isinstance(value, dict): return POINT_SCHEMA(value) + if isinstance(value, list): + if len(value) != 2: + raise cv.Invalid("Invalid point format, should be <x_int>, <y_int>") + return POINT_SCHEMA({CONF_X: value[0], CONF_Y: value[1]}) try: - x, y = map(int, value.split(",")) - return {CONF_X: x, CONF_Y: y} + x, y = str(value).split(",") + return POINT_SCHEMA({CONF_X: x, CONF_Y: y}) except ValueError: pass # not raising this in the catch block because pylint doesn't like it diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index f12766bae12..1308b82dcd8 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -52,6 +52,7 @@ from ..lv_validation import ( lv_text, opacity, pixels, + pixels_or_percent, size, ) from ..lvcode import LocalVariable, lv, lv_assign, lv_expr @@ -59,7 +60,7 @@ from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property from ..types import LvType, ObjUpdateAction from . import Widget, WidgetType, get_widgets from .img import CONF_IMAGE -from .line import lv_point_precise_t, process_coord +from .line import lv_point_precise_t CONF_CANVAS = "canvas" CONF_BUFFER_ID = "buffer_id" @@ -434,6 +435,13 @@ LINE_PROPS = { } +def _validate_points(config): + for index, point in enumerate(config[CONF_POINTS]): + if not all(isinstance(p, int) for p in point.values()): + raise cv.Invalid("Points must be integers", path=[CONF_POINTS, index]) + return config + + @automation.register_action( "lvgl.canvas.draw_line", ObjUpdateAction, @@ -444,12 +452,15 @@ LINE_PROPS = { cv.Required(CONF_POINTS): cv.ensure_list(point_schema), **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, } - ), + ).add_extra(_validate_points), synchronous=True, ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + [ + await pixels.process(p[CONF_X]), + await pixels.process(p[CONF_Y]), + ] for p in config[CONF_POINTS] ] @@ -470,12 +481,15 @@ async def canvas_draw_line(config, action_id, template_arg, args): cv.Required(CONF_POINTS): cv.ensure_list(point_schema), **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, - ), + ).add_extra(_validate_points), synchronous=True, ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + [ + await pixels_or_percent.process(p[CONF_X]), + await pixels_or_percent.process(p[CONF_Y]), + ] for p in config[CONF_POINTS] ] # Close the polygon diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index 3112cc28d0c..19f421cbbd8 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -1,12 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_X, CONF_Y -from esphome.core import Lambda -from ..defines import CONF_MAIN, call_lambda +from ..defines import CONF_MAIN +from ..lv_validation import pixels_or_percent from ..lvcode import lv_add from ..schemas import point_schema -from ..types import LvCompound, LvType, lv_coord_t +from ..types import LvCompound, LvType from . import Widget, WidgetType CONF_LINE = "line" @@ -17,12 +17,6 @@ lv_point_t = cg.global_ns.struct("lv_point_t") lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") -async def process_coord(coord): - if isinstance(coord, Lambda): - return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) - return cg.safe_exp(coord) - - class LineType(WidgetType): def __init__(self): super().__init__( @@ -36,7 +30,10 @@ class LineType(WidgetType): async def to_code(self, w: Widget, config): if CONF_POINTS in config: points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + [ + await pixels_or_percent.process(p[CONF_X]), + await pixels_or_percent.process(p[CONF_Y]), + ] for p in config[CONF_POINTS] ] lv_add(w.var.set_points(points)) diff --git a/tests/component_tests/lvgl/config/line_points.yaml b/tests/component_tests/lvgl/config/line_points.yaml new file mode 100644 index 00000000000..5d7be3bc204 --- /dev/null +++ b/tests/component_tests/lvgl/config/line_points.yaml @@ -0,0 +1,84 @@ +esphome: + name: test-line + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + widgets: + # Dict format + - line: + id: line_dict + points: + - x: 10 + y: 20 + - x: 100 + y: 200 + - x: 0 + y: 0 + + # List format + - line: + id: line_list + points: + - [10, 20] + - [100, 200] + - [0, 0] + + # String format + - line: + id: line_string + points: + - "10, 20" + - "100, 200" + - "0, 0" + + # Percentage - dict format + - line: + id: line_pct_dict + points: + - x: "50%" + y: "75%" + + # Percentage - list format + - line: + id: line_pct_list + points: + - ["50%", "75%"] + + # Percentage - string format + - line: + id: line_pct_string + points: + - "50%, 75%" + + # Mixed integer and percentage + - line: + id: line_mixed_dict + points: + - x: 10 + y: "50%" + - x: "25%" + y: 200 + + - line: + id: line_mixed_list + points: + - [10, "50%"] + - ["25%", 200] diff --git a/tests/component_tests/lvgl/test_line.py b/tests/component_tests/lvgl/test_line.py new file mode 100644 index 00000000000..fce0ef8fa8e --- /dev/null +++ b/tests/component_tests/lvgl/test_line.py @@ -0,0 +1,147 @@ +"""Tests for the LVGL line widget point schema and code generation.""" + +from __future__ import annotations + +import re + +import pytest + +from esphome.components.lvgl.schemas import point_schema +from esphome.config_validation import Invalid +from esphome.const import CONF_X, CONF_Y + +# --------------------------------------------------------------------------- +# Validation: point_schema normalises dict / list / string to same result +# --------------------------------------------------------------------------- + + +class TestPointSchemaValidation: + """Test that all point input formats normalise to the same dict.""" + + @pytest.mark.parametrize( + "dict_input,list_input,string_input", + [ + ({CONF_X: 10, CONF_Y: 20}, [10, 20], "10, 20"), + ({CONF_X: 0, CONF_Y: 0}, [0, 0], "0, 0"), + ({CONF_X: 100, CONF_Y: 200}, [100, 200], "100, 200"), + ({CONF_X: -5, CONF_Y: -10}, [-5, -10], "-5, -10"), + ], + ) + def test_integer_formats_produce_same_result( + self, dict_input, list_input, string_input + ): + result_dict = point_schema(dict_input) + result_list = point_schema(list_input) + result_string = point_schema(string_input) + + assert result_dict == result_list + assert result_dict == result_string + + def test_percentage_formats_produce_same_result(self): + result_dict = point_schema({CONF_X: "50%", CONF_Y: "75%"}) + result_list = point_schema(["50%", "75%"]) + result_string = point_schema("50%, 75%") + + assert result_dict == result_list + assert result_dict == result_string + + def test_pixel_suffix_matches_plain_integer(self): + result_px = point_schema({CONF_X: "10px", CONF_Y: "20px"}) + result_int = point_schema({CONF_X: 10, CONF_Y: 20}) + + assert result_px == result_int + + @pytest.mark.parametrize( + "value", + [ + {CONF_X: 50, CONF_Y: 75}, + [50, 75], + "50, 75", + ], + ) + def test_output_contains_x_and_y(self, value): + result = point_schema(value) + + assert CONF_X in result + assert CONF_Y in result + + def test_list_wrong_length_raises(self): + with pytest.raises(Invalid, match="Invalid point"): + point_schema([1]) + + with pytest.raises(Invalid, match="Invalid point"): + point_schema([1, 2, 3]) + + def test_string_without_comma_raises(self): + with pytest.raises(Invalid, match="Invalid point"): + point_schema("garbage") + + def test_string_extra_commas_raises(self): + with pytest.raises(Invalid, match="Invalid point"): + point_schema("1,2,3") + + +# --------------------------------------------------------------------------- +# Code generation: different point formats produce identical C++ output +# --------------------------------------------------------------------------- + +_SET_POINTS_RE = re.compile(r"(\w+)->set_points\((.+?)\);") + + +def _extract_set_points(main_cpp: str) -> dict[str, str]: + """Return {var_name: args_text} for every set_points() call found.""" + return {m.group(1): m.group(2) for m in _SET_POINTS_RE.finditer(main_cpp)} + + +class TestLineCodeGeneration: + """Verify that alternative point formats generate identical C++ code.""" + + @pytest.fixture() + def main_cpp(self, generate_main, component_config_path) -> str: + return generate_main(component_config_path("line_points.yaml")) + + @pytest.fixture() + def set_points_calls(self, main_cpp) -> dict[str, str]: + return _extract_set_points(main_cpp) + + def test_integer_points_all_formats_match(self, set_points_calls): + """Dict, list, and string formats with integer points produce same set_points call.""" + assert set_points_calls["line_dict"] == set_points_calls["line_list"] + assert set_points_calls["line_dict"] == set_points_calls["line_string"] + + def test_percentage_points_all_formats_match(self, set_points_calls): + """Dict, list, and string formats with percentage points produce same set_points call.""" + assert set_points_calls["line_pct_dict"] == set_points_calls["line_pct_list"] + assert set_points_calls["line_pct_dict"] == set_points_calls["line_pct_string"] + + def test_mixed_points_formats_match(self, set_points_calls): + """Dict and list formats with mixed int/percent points produce same set_points call.""" + assert ( + set_points_calls["line_mixed_dict"] == set_points_calls["line_mixed_list"] + ) + + def test_integer_points_contain_expected_values(self, set_points_calls): + """Integer points appear literally in the generated code.""" + args = set_points_calls["line_dict"] + for val in ("10", "20", "100", "200"): + assert val in args + + def test_percentage_points_use_lv_pct(self, set_points_calls): + """Percentage points are generated using the lv_pct() macro.""" + args = set_points_calls["line_pct_dict"] + assert "lv_pct(50)" in args + assert "lv_pct(75)" in args + + def test_all_lines_present(self, set_points_calls): + """All expected line IDs have a set_points call.""" + expected = { + "line_dict", + "line_list", + "line_string", + "line_pct_dict", + "line_pct_list", + "line_pct_string", + "line_mixed_dict", + "line_mixed_list", + } + assert expected.issubset(set_points_calls.keys()) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 9c4ad4bbf85..39d7472054a 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -1038,7 +1038,10 @@ lvgl: - 5, 5 - x: !lambda return random_uint32() % 100; y: !lambda return random_uint32() % 100; - - 70, 70 + - x: 10% + y: 50% + - 70%, 70% + - [75%, 75%] - 120, 10 - 180, 60 - 240, 10 From 85f33978e73a28df88955db52d8ccdb7557b5290 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 06:23:35 -0500 Subject: [PATCH 1864/2056] [core] Skip external component update on `esphome clean` (#16268) --- esphome/__main__.py | 5 +++-- tests/unit_tests/test_main.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index f4a276b74cc..825a502dbf3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2301,8 +2301,9 @@ def run_esphome(argv): CORE.config_path = conf_path CORE.dashboard = args.dashboard - # For logs command, skip updating external components - skip_external = args.command == "logs" + # Commands that don't need fresh external components: logs just connects + # to the device, and clean is about to delete the build directory. + skip_external = args.command in ("logs", "clean") config = read_config( dict(args.substitution) if args.substitution else {}, skip_external_update=skip_external, diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 0b96000a575..4ab7bb3344b 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -5038,6 +5038,32 @@ def test_run_esphome_non_bundle_skips_extraction(tmp_path: Path) -> None: assert result == 2 +@pytest.mark.parametrize( + ("command", "expected_skip"), + [ + ("logs", True), + ("clean", True), + ("compile", False), + ("config", False), + ("run", False), + ("clean-mqtt", False), + ], +) +def test_run_esphome_skip_external_update_per_command( + tmp_path: Path, command: str, expected_skip: bool +) -> None: + """read_config is invoked with skip_external_update=True only for commands + that don't need fresh external components (logs, clean).""" + yaml_file = tmp_path / "device.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + with patch("esphome.__main__.read_config", return_value=None) as mock_read: + run_esphome(["esphome", command, str(yaml_file)]) + + mock_read.assert_called_once() + assert mock_read.call_args.kwargs["skip_external_update"] is expected_skip + + def test_get_configured_xtal_freq_reads_sdkconfig(tmp_path: Path) -> None: """Test reading XTAL_FREQ from sdkconfig.""" CORE.name = "test-device" From 29db5fa4bb79333310837c9287405200bdba12b9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 6 May 2026 08:11:06 -0400 Subject: [PATCH 1865/2056] [script] Make pre-commit and helpers work on Windows (#16260) Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- script/ci-custom.py | 2 +- script/run-in-env.py | 9 ++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad82bd8e5d8..da5fb94d5e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: hooks: - id: pylint name: pylint - entry: python3 script/run-in-env.py pylint + entry: python script/run-in-env.py pylint language: system types: [python] files: ^esphome/.+\.py$ @@ -68,5 +68,5 @@ repos: additional_dependencies: [] - id: ci-custom name: ci-custom - entry: python3 script/run-in-env.py script/ci-custom.py + entry: python script/run-in-env.py script/ci-custom.py language: system diff --git a/script/ci-custom.py b/script/ci-custom.py index b257a3818bd..8cd8fd75444 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -250,7 +250,7 @@ def lint_ext_check(fname): ] ) def lint_executable_bit(fname: Path) -> str | None: - ex = EXECUTABLE_BIT[str(fname)] + ex = EXECUTABLE_BIT[fname.as_posix()] if ex != 100644: return ( f"File has invalid executable bit {ex}. If running from a windows machine please " diff --git a/script/run-in-env.py b/script/run-in-env.py index 9283ba9940f..996db60554f 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -44,7 +44,14 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - result = subprocess.run(sys.argv[1:], check=False, close_fds=False) + args = sys.argv[1:] + # Windows CreateProcess doesn't follow shebangs, so prepend the + # current interpreter when the entry is a .py script. Using + # sys.executable also pins the nested call to the same Python that + # ran us — no ambiguous PATH lookup for "python". + if args[0].endswith(".py"): + args = [sys.executable, *args] + result = subprocess.run(args, check=False, close_fds=False) sys.exit(result.returncode) else: print( From f06ad8c4366f4f51ae54370f9713ccf29a998573 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 07:32:19 -0500 Subject: [PATCH 1866/2056] [http_request] Add regression test for light action inside on_response (#16270) --- .../components/http_request/http_request.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/components/http_request/http_request.yaml b/tests/components/http_request/http_request.yaml index ef67671c91d..46d4b88ec5a 100644 --- a/tests/components/http_request/http_request.yaml +++ b/tests/components/http_request/http_request.yaml @@ -50,12 +50,33 @@ esphome: format: "After delay, body still: %s" args: - body.c_str() + # Regression test for esphome/esphome#16224: a LightControlAction + # nested inside on_response with capture_response: true puts + # `std::string &` into the trigger's Ts..., which exposed a codegen + # bug where the apply lambda's parameter list did not match the + # ApplyFn signature. + - light.turn_on: + id: test_regression_light + brightness: 100% + effect: "None" http_request: useragent: esphome/tagreader timeout: 10s verify_ssl: ${verify_ssl} +output: + - platform: template + id: test_regression_output + type: float + write_action: + - logger.log: "set" + +light: + - platform: monochromatic + id: test_regression_light + output: test_regression_output + script: - id: does_not_compile parameters: From ff0c5f575e84f8143fd42a40f0c8a360599aa543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 07:32:35 -0500 Subject: [PATCH 1867/2056] [bundle] Include secrets.yaml when `!secret` keys are quoted (#16271) --- esphome/bundle.py | 12 +++++---- tests/unit_tests/test_bundle.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/esphome/bundle.py b/esphome/bundle.py index efa80acc8cc..70c4fad0fda 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -98,11 +98,13 @@ _KNOWN_FILE_EXTENSIONS = frozenset( ) -# Matches !secret references in YAML text. This is intentionally a simple -# regex scan rather than a YAML parse — it may match inside comments or -# multi-line strings, which is the conservative direction (include more -# secrets rather than fewer). -_SECRET_RE = re.compile(r"!secret\s+(\S+)") +# Matches !secret references in YAML text. An optional surrounding +# quote pair around the key is allowed and ignored: YAML treats +# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is +# intentionally a simple regex scan rather than a YAML parse — it may +# match inside comments or multi-line strings, which is the conservative +# direction (include more secrets rather than fewer). +_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""") def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]: diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 89bf1a33b3e..5d046252dae 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -170,6 +170,23 @@ def test_find_used_secret_keys_deduplicates(tmp_path: Path) -> None: assert keys == {"key1"} +def test_find_used_secret_keys_quoted(tmp_path: Path) -> None: + """Quoted !secret keys should resolve to the same key as unquoted form. + + YAML strips surrounding quotes during parsing, so the secrets.yaml + lookup uses the unquoted key. The bundle scan must do the same. + """ + yaml1 = tmp_path / "a.yaml" + yaml1.write_text( + "single: !secret 'wifi_ssid'\n" + 'double: !secret "wifi_pw"\n' + "bare: !secret api_key\n" + ) + + keys = _find_used_secret_keys([yaml1]) + assert keys == {"wifi_ssid", "wifi_pw", "api_key"} + + # --------------------------------------------------------------------------- # _add_bytes_to_tar # --------------------------------------------------------------------------- @@ -1217,6 +1234,35 @@ def test_create_bundle_filters_secrets(tmp_path: Path) -> None: assert "should_not_appear" not in secrets_data +def test_create_bundle_filters_secrets_quoted(tmp_path: Path) -> None: + """Bundling must include secrets.yaml when !secret keys are quoted. + + Regression test for issue 16259: quoted !secret references previously + captured the quotes as part of the key, so no key matched secrets.yaml + entries and the secrets file was dropped from the bundle entirely. + """ + config_dir = _setup_config_dir(tmp_path) + + secrets = config_dir / "secrets.yaml" + secrets.write_text("ota_password: hunter2\nunused: should_not_appear\n") + + config_yaml = "ota:\n password: !secret 'ota_password'\n" + (config_dir / "test.yaml").write_text(config_yaml) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert result.manifest[ManifestKey.HAS_SECRETS] is True + + buf = io.BytesIO(result.data) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + secrets_data = tar.extractfile("secrets.yaml").read().decode() + + assert "ota_password" in secrets_data + assert "hunter2" in secrets_data + assert "unused" not in secrets_data + + def test_create_bundle_no_secrets(tmp_path: Path) -> None: _setup_config_dir(tmp_path) From caaa1aefc7d4c467fb07289bcf83f13ecb464bf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 08:41:17 -0500 Subject: [PATCH 1868/2056] [substitutions] Fix sibling references inside dict-valued substitutions (#16273) --- esphome/components/substitutions/__init__.py | 13 ++++++++++++- .../18-dict_self_reference.approved.yaml | 16 ++++++++++++++++ .../18-dict_self_reference.input.yaml | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index fb7cd7c51b3..ea79054c886 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -278,7 +278,18 @@ def _push_context( """Resolve a variable, recursively resolving any dependencies it references.""" value = unresolved_vars.pop(key, Missing) if value is Missing: - return Missing + # Either already resolved (in resolved_vars) or currently being + # resolved (self-reference from inside a dict-valued substitution). + # Returning what we have lets sibling references inside a dict + # value, e.g. ``${device.manufacturer}`` inside ``device.name``, + # see literal sibling values during their own resolution. + return resolved_vars.get(key, Missing) + if isinstance(value, dict): + # Dict-valued substitutions form a namespace; eagerly publish the + # original mapping so its members can reference each other while + # the dict's own substitution pass is still running. The entry is + # replaced with the fully-substituted dict once recursion returns. + resolved_vars[key] = value try: value = substitute(value, [], resolver_context, True) except UndefinedError as err: diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml new file mode 100644 index 00000000000..e5e6d4568e8 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml @@ -0,0 +1,16 @@ +substitutions: + device: + manufacturer: espressif + model: esp32 + mac_suffix: ffffff + name: espressif-esp32-ffffff + network: + host: example.com + port: 8080 + url: http://example.com:8080/api +esphome: + name: espressif-esp32-ffffff +test_list: + - espressif-esp32-ffffff + - http://example.com:8080/api + - espressif/esp32 diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml new file mode 100644 index 00000000000..b27c4b8c291 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml @@ -0,0 +1,18 @@ +substitutions: + device: + manufacturer: "espressif" + model: "esp32" + mac_suffix: "ffffff" + name: ${device.manufacturer}-${device.model}-${device.mac_suffix} + network: + host: "example.com" + port: 8080 + url: "http://${network.host}:${network.port}/api" + +esphome: + name: ${device.name} + +test_list: + - ${device.name} + - ${network.url} + - "${device.manufacturer}/${device.model}" From 545ee03f42e830b8ea0899d06886ab4b8f22ba85 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Wed, 6 May 2026 10:15:04 -0400 Subject: [PATCH 1869/2056] [atm90e32] Fix calibration instance not saving in flash properly (#14152) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/atm90e32/atm90e32.cpp | 139 ++++++++++++++++------- esphome/components/atm90e32/atm90e32.h | 3 + esphome/components/atm90e32/sensor.py | 1 + 3 files changed, 105 insertions(+), 38 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index db29702c54a..693b1b49612 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,6 +1,7 @@ #include "atm90e32.h" #include <cinttypes> #include <cmath> +#include <cstring> #include <numbers> #include "esphome/core/log.h" @@ -8,6 +9,25 @@ namespace esphome { namespace atm90e32 { static const char *const TAG = "atm90e32"; + +static uint32_t pref_hash(const char *prefix, const char *name_space) { + auto hash = fnv1_hash(prefix); + return fnv1_hash_extend(hash, name_space); +} + +template<typename T> +static int migrate_legacy_pref_if_needed(ESPPreferenceObject ¤t_pref, ESPPreferenceObject &legacy_pref, + T *scratch) { + T current{}; + if (current_pref.load(¤t)) { + return 0; + } + if (!legacy_pref.load(scratch)) { + return 0; + } + return current_pref.save(scratch) ? 1 : -1; +} + void ATM90E32Component::loop() { if (this->get_publish_interval_flag_()) { this->set_publish_interval_flag_(false); @@ -112,10 +132,14 @@ void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> bu this->cs_->dump_summary(buffer.data(), buffer.size()); } +const char *ATM90E32Component::get_calibration_id_() { return this->instance_id_; } + void ATM90E32Component::setup() { this->spi_setup(); - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); + char legacy_cs[GPIO_SUMMARY_MAX_LEN]; + this->get_cs_summary_(legacy_cs); + const bool has_distinct_legacy_namespace = strcmp(cs, legacy_cs) != 0; uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t high_thresh = 0; @@ -162,15 +186,46 @@ void ATM90E32Component::setup() { if (this->enable_offset_calibration_) { // Initialize flash storage for offset calibrations - uint32_t o_hash = fnv1_hash("_offset_calibration_"); - o_hash = fnv1_hash_extend(o_hash, cs); + uint32_t o_hash = pref_hash("_offset_calibration_", cs); this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true); - this->restore_offset_calibrations_(); + bool migrated_offset = false; + if (has_distinct_legacy_namespace) { + uint32_t legacy_o_hash = pref_hash("_offset_calibration_", legacy_cs); + auto legacy_offset_pref = global_preferences->make_preference<OffsetCalibration[3]>(legacy_o_hash, true); + OffsetCalibration offset_data[3]{}; + int migration_status = migrate_legacy_pref_if_needed(this->offset_pref_, legacy_offset_pref, &offset_data); + migrated_offset = migration_status > 0; + if (migration_status > 0) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated offset calibrations from legacy storage.", cs); + } else if (migration_status < 0) { + ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate offset calibrations from legacy storage.", cs); + } + } // Initialize flash storage for power offset calibrations - uint32_t po_hash = fnv1_hash("_power_offset_calibration_"); - po_hash = fnv1_hash_extend(po_hash, cs); + uint32_t po_hash = pref_hash("_power_offset_calibration_", cs); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true); + bool migrated_power_offset = false; + if (has_distinct_legacy_namespace) { + uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs); + auto legacy_power_offset_pref = + global_preferences->make_preference<PowerOffsetCalibration[3]>(legacy_po_hash, true); + PowerOffsetCalibration power_offset_data[3]{}; + int migration_status = + migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data); + migrated_power_offset = migration_status > 0; + if (migration_status > 0) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated power offset calibrations from legacy storage.", cs); + } else if (migration_status < 0) { + ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate power offset calibrations from legacy storage.", cs); + } + } + + if (migrated_offset || migrated_power_offset) { + global_preferences->sync(); + } + + this->restore_offset_calibrations_(); this->restore_power_offset_calibrations_(); } else { ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", @@ -189,9 +244,27 @@ void ATM90E32Component::setup() { if (this->enable_gain_calibration_) { // Initialize flash storage for gain calibration - uint32_t g_hash = fnv1_hash("_gain_calibration_"); - g_hash = fnv1_hash_extend(g_hash, cs); + uint32_t g_hash = pref_hash("_gain_calibration_", cs); this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true); + bool migrated_gain = false; + if (has_distinct_legacy_namespace) { + uint32_t legacy_g_hash = pref_hash("_gain_calibration_", legacy_cs); + auto legacy_gain_calibration_pref = global_preferences->make_preference<GainCalibration[3]>(legacy_g_hash, true); + GainCalibration gain_data[3]{}; + int migration_status = + migrate_legacy_pref_if_needed(this->gain_calibration_pref_, legacy_gain_calibration_pref, &gain_data); + migrated_gain = migration_status > 0; + if (migration_status > 0) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated gain calibrations from legacy storage.", cs); + } else if (migration_status < 0) { + ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate gain calibrations from legacy storage.", cs); + } + } + + if (migrated_gain) { + global_preferences->sync(); + } + this->restore_gain_calibrations_(); if (!this->using_saved_calibrations_) { @@ -221,8 +294,7 @@ void ATM90E32Component::setup() { } void ATM90E32Component::log_calibration_status_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); bool offset_mismatch = false; bool power_mismatch = false; @@ -573,8 +645,7 @@ float ATM90E32Component::get_chip_temperature_() { } void ATM90E32Component::run_gain_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->enable_gain_calibration_) { ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true", cs); @@ -674,8 +745,7 @@ void ATM90E32Component::run_gain_calibrations() { } void ATM90E32Component::save_gain_calibration_to_memory_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); bool success = this->gain_calibration_pref_.save(&this->gain_phase_); global_preferences->sync(); if (success) { @@ -688,8 +758,7 @@ void ATM90E32Component::save_gain_calibration_to_memory_() { } void ATM90E32Component::save_offset_calibration_to_memory_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); bool success = this->offset_pref_.save(&this->offset_phase_); global_preferences->sync(); if (success) { @@ -705,8 +774,7 @@ void ATM90E32Component::save_offset_calibration_to_memory_() { } void ATM90E32Component::save_power_offset_calibration_to_memory_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); bool success = this->power_offset_pref_.save(&this->power_offset_phase_); global_preferences->sync(); if (success) { @@ -722,8 +790,7 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() { } void ATM90E32Component::run_offset_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->enable_offset_calibration_) { ESP_LOGW(TAG, "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true", @@ -753,8 +820,7 @@ void ATM90E32Component::run_offset_calibrations() { } void ATM90E32Component::run_power_offset_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->enable_offset_calibration_) { ESP_LOGW( TAG, @@ -827,15 +893,16 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t } void ATM90E32Component::restore_gain_calibrations_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); for (uint8_t i = 0; i < 3; ++i) { this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_; this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_; this->gain_phase_[i] = this->config_gain_phase_[i]; } - if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + bool have_data = this->gain_calibration_pref_.load(&this->gain_phase_); + + if (have_data) { bool all_zero = true; bool same_as_config = true; for (uint8_t phase = 0; phase < 3; ++phase) { @@ -882,12 +949,12 @@ void ATM90E32Component::restore_gain_calibrations_() { } void ATM90E32Component::restore_offset_calibrations_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); for (uint8_t i = 0; i < 3; ++i) this->config_offset_phase_[i] = this->offset_phase_[i]; bool have_data = this->offset_pref_.load(&this->offset_phase_); + bool all_zero = true; if (have_data) { for (auto &phase : this->offset_phase_) { @@ -925,12 +992,12 @@ void ATM90E32Component::restore_offset_calibrations_() { } void ATM90E32Component::restore_power_offset_calibrations_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); for (uint8_t i = 0; i < 3; ++i) this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); + bool all_zero = true; if (have_data) { for (auto &phase : this->power_offset_phase_) { @@ -968,8 +1035,7 @@ void ATM90E32Component::restore_power_offset_calibrations_() { } void ATM90E32Component::clear_gain_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->using_saved_calibrations_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); @@ -1018,8 +1084,7 @@ void ATM90E32Component::clear_gain_calibrations() { } void ATM90E32Component::clear_offset_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->restored_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); @@ -1061,8 +1126,7 @@ void ATM90E32Component::clear_offset_calibrations() { } void ATM90E32Component::clear_power_offset_calibrations() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); if (!this->restored_power_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); @@ -1137,8 +1201,7 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) } bool ATM90E32Component::verify_gain_writes_() { - char cs[GPIO_SUMMARY_MAX_LEN]; - this->get_cs_summary_(cs); + const char *cs = this->get_calibration_id_(); bool success = true; for (uint8_t phase = 0; phase < 3; phase++) { uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 95154812cb4..62c7bada86c 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -102,6 +102,7 @@ class ATM90E32Component : public PollingComponent, void clear_gain_calibrations(); void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; } + void set_instance_id(const char *id) { instance_id_ = id; } int16_t calibrate_offset(uint8_t phase, bool voltage); int16_t calibrate_power_offset(uint8_t phase, bool reactive); void run_gain_calibrations(); @@ -183,6 +184,7 @@ class ATM90E32Component : public PollingComponent, bool verify_gain_writes_(); bool validate_spi_read_(uint16_t expected, const char *context = nullptr); void log_calibration_status_(); + const char *get_calibration_id_(); void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer); struct ATM90E32Phase { @@ -263,6 +265,7 @@ class ATM90E32Component : public PollingComponent, bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; + const char *instance_id_{nullptr}; bool restored_offset_calibration_{false}; bool restored_power_offset_calibration_{false}; bool restored_gain_calibration_{false}; diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 7e5d85c57a4..dc46138addc 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -193,6 +193,7 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_instance_id(str(config[CONF_ID]))) await cg.register_component(var, config) await spi.register_spi_device(var, config) From 6e1a59da3e583c5624a18da82b62d85256e7f0f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 09:53:14 -0500 Subject: [PATCH 1870/2056] [packages] Make package !include vars visible to its substitutions block (#16274) --- esphome/components/packages/__init__.py | 59 +++++++++++++++---- .../18-package_vars_in_subs.approved.yaml | 9 +++ .../18-package_vars_in_subs.input.yaml | 9 +++ .../18-package_vars_in_subs_inc.yaml | 8 +++ 4 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs_inc.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index d63f17aa7ed..06a64208b6e 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -414,25 +414,39 @@ def _substitute_package_definition( def _update_substitutions_context( parent_context: UserDict, package_substitutions: dict[str, Any], + eval_context: ContextVars | None = None, ) -> None: """Resolve and add new substitutions to the parent context. Skips keys already present (higher-priority sources win). - String values are substituted against the current context so that - cross-references between substitutions are expanded when possible. + String values are substituted against *eval_context* (or *parent_context* + if not provided) so that cross-references between substitutions are + expanded when possible. Resolved values are written into *parent_context* + and back into *package_substitutions* so that subsequent merges into the + consolidated ``substitutions:`` block carry the resolved value (the + package's ``!include vars`` are no longer in scope after this function + returns). + + *eval_context* may layer additional vars (e.g. a package's own ``!include + vars``) on top of *parent_context* so that a package's substitutions can + reference vars passed in by the parent file. """ + if eval_context is None: + eval_context = ContextVars(parent_context) for key, value in package_substitutions.items(): if key in parent_context: continue if not isinstance(value, str): parent_context[key] = value continue - parent_context[key] = substitute( + resolved = substitute( item=value, path=[CONF_SUBSTITUTIONS, key], - parent_context=ContextVars(parent_context), + parent_context=eval_context, strict_undefined=False, ) + parent_context[key] = resolved + package_substitutions[key] = resolved class _PackageProcessor: @@ -508,11 +522,36 @@ class _PackageProcessor: package_config = _process_remote_package(package_config) return package_config - def collect_substitutions(self, package_config: dict) -> None: - """Extract substitutions from a package and merge into the shared context.""" + def collect_substitutions( + self, + package_config: dict, + context_vars: ContextVars | None, + ) -> ContextVars: + """Extract substitutions from a package and merge into the shared context. + + Returns the context updated with the package's ``!include vars`` (or + an equivalent of *context_vars* if the package has none) so the caller + can reuse it when recursing into nested packages. ``None`` inputs are + normalized to an empty :class:`ContextVars`, so the result is always + non-``None``. + """ + # Push the package's own !include vars before evaluating its + # substitutions so they can reference vars passed in by the parent + # (e.g. ``vars: {my_variable: ...}`` on the include entry). + package_context = push_context( + package_config, context_vars if context_vars is not None else ContextVars() + ) if subs := package_config.pop(CONF_SUBSTITUTIONS, {}): + # Resolve before merging so that values referencing the package's + # ``!include vars`` are baked into the consolidated substitutions + # block; once we return, the package vars are no longer in scope. + # ``package_context`` is a ChainMap whose chain already terminates + # in ``self.parent_context`` (set up by ``do_packages_pass``), so + # ``parent_context`` mutations from ``_update_substitutions_context`` + # remain visible to evaluation reads. + _update_substitutions_context(self.parent_context, subs, package_context) self.substitutions.data = merge_config(subs, self.substitutions.data) - _update_substitutions_context(self.parent_context, subs) + return package_context def process_package( self, @@ -525,13 +564,13 @@ class _PackageProcessor: package_config ) package_config = self.resolve_package(package_config, context_vars, path) - self.collect_substitutions(package_config) + context_vars = self.collect_substitutions(package_config, context_vars) if CONF_PACKAGES not in package_config: return package_config - # Push context from !include vars on the package root and on the packages key - context_vars = push_context(package_config, context_vars) + # Push context from !include vars on the packages key (the package root + # was already pushed in collect_substitutions above). context_vars = push_context(package_config[CONF_PACKAGES], context_vars) # Disable the deprecated single-package fallback for remote # packages. _process_remote_package returns dicts with diff --git a/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.approved.yaml b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.approved.yaml new file mode 100644 index 00000000000..647a33a9838 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.approved.yaml @@ -0,0 +1,9 @@ +binary_sensor: + - platform: template + id: front_door_enrolling + name: Front Door Enrolling +substitutions: + enrolling_id: front_door_enrolling + enrolling_name: Front Door Enrolling +esphome: + name: test diff --git a/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.input.yaml b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.input.yaml new file mode 100644 index 00000000000..21a0f2d235e --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs.input.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +packages: + fingerprint: !include + file: 18-package_vars_in_subs_inc.yaml + vars: + sensor_name: "Front Door" + sensor_id_prefix: "front_door" diff --git a/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs_inc.yaml b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs_inc.yaml new file mode 100644 index 00000000000..8b420d73e7f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-package_vars_in_subs_inc.yaml @@ -0,0 +1,8 @@ +substitutions: + enrolling_id: ${sensor_id_prefix}_enrolling + enrolling_name: ${sensor_name} Enrolling + +binary_sensor: + - platform: template + id: ${enrolling_id} + name: ${enrolling_name} From 90693fb39aac35c2303e8b6165fa4638085dbd8d Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 6 May 2026 14:56:33 +0000 Subject: [PATCH 1871/2056] [core] Fix WiFi connection in safe mode (#16269) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/core/config.py | 27 ++--- tests/integration/conftest.py | 111 +++++++++--------- .../fixtures/safe_mode_loop_runs.yaml | 25 ++++ tests/integration/host_prefs.py | 39 ++++++ tests/integration/test_safe_mode_loop_runs.py | 94 +++++++++++++++ tests/integration/test_template_text_save.py | 15 +-- tests/unit_tests/core/test_config.py | 70 +++++++++++ 7 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 tests/integration/fixtures/safe_mode_loop_runs.yaml create mode 100644 tests/integration/host_prefs.py create mode 100644 tests/integration/test_safe_mode_loop_runs.py diff --git a/esphome/core/config.py b/esphome/core/config.py index b4e81ce49fa..fe55c0fe25d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -568,14 +568,9 @@ async def _add_controller_registry_define() -> None: @coroutine_with_priority(CoroPriority.FINAL) async def _add_looping_components() -> None: - # Emit a constexpr that computes the looping component count at C++ compile time - # and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to - # detect loop() overrides. The constexpr goes in main.cpp's global section where - # all component types are in scope. calculate_looping_components_() then skips - # the counting pass and only does the two population passes. + # Emit ESPHOME_LOOPING_COMPONENT_COUNT. Sizing of looping_components_ + # happens in core to_code() so it lands before safe_mode's early return. entries = CORE.data.get("looping_component_entries", []) - if not entries: - return # Build constexpr sum for the exact count, deduplicating by type # Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance @@ -583,7 +578,7 @@ async def _add_looping_components() -> None: terms = [ f"({count} * HasLoopOverride<{cpp_type}>::value)" for cpp_type, count in type_counts.items() - ] + ] or ["0"] constexpr_expr = " + \\\n ".join(terms) cg.add_global( cg.RawStatement( @@ -592,14 +587,6 @@ async def _add_looping_components() -> None: ) ) - # Pre-init FixedVector with exact capacity so calculate_looping_components_() - # can skip the counting pass - cg.add( - cg.RawExpression( - "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" - ) - ) - @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: @@ -642,6 +629,14 @@ async def to_code(config: ConfigType) -> None: # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) + # Pre-init FixedVector with exact capacity so calculate_looping_components_() + # can skip the counting pass + cg.add( + cg.RawExpression( + "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" + ) + ) + CORE.add_job(_add_platform_defines) CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_looping_components) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 7c85bf753ca..f36543b7cd9 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -501,14 +501,15 @@ async def _read_stream_lines( @asynccontextmanager -async def run_binary_and_wait_for_port( +async def run_binary( binary_path: Path, - host: str, - port: int, - timeout: float = PORT_WAIT_TIMEOUT, line_callback: Callable[[str], None] | None = None, -) -> AsyncGenerator[None]: - """Run a binary, wait for it to open a port, and clean up on exit.""" +) -> AsyncGenerator[tuple[asyncio.subprocess.Process, list[str]]]: + """Run a binary under a PTY, capture log output, and clean up on exit. + + Yields the running ``Process`` and a live list of captured log lines. + No port wait -- callers that need that should use + ``run_binary_and_wait_for_port``.""" # Create a pseudo-terminal to make the binary think it's running interactively # This is needed because the ESPHome host logger checks isatty() controller_fd, device_fd = pty.openpty() @@ -535,7 +536,6 @@ async def run_binary_and_wait_for_port( controller_transport, _ = await loop.connect_read_pipe( lambda: controller_protocol, os.fdopen(controller_fd, "rb", 0) ) - output_reader = controller_reader if process.returncode is not None: raise RuntimeError( @@ -543,27 +543,59 @@ async def run_binary_and_wait_for_port( "Ensure the binary is valid and can run successfully." ) - # Wait for the API server to start listening - loop = asyncio.get_running_loop() - start_time = loop.time() - - # Start collecting output stdout_lines: list[str] = [] - output_tasks: list[asyncio.Task] = [] + output_task = asyncio.create_task( + _read_stream_lines(controller_reader, stdout_lines, sys.stdout, line_callback) + ) try: - # Read from output stream - output_tasks = [ - asyncio.create_task( - _read_stream_lines( - output_reader, stdout_lines, sys.stdout, line_callback - ) - ) - ] - # Small yield to ensure the process has a chance to start await asyncio.sleep(0) + yield process, stdout_lines + finally: + output_task.cancel() + result = await asyncio.gather(output_task, return_exceptions=True) + if isinstance(result[0], Exception) and not isinstance( + result[0], asyncio.CancelledError + ): + print(f"Error reading from PTY: {result[0]}", file=sys.stderr) + # Close the PTY transport (Unix only) + if controller_transport is not None: + controller_transport.close() + + # Cleanup: terminate the process gracefully + if process.returncode is None: + # Send SIGINT (Ctrl+C) for graceful shutdown + process.send_signal(signal.SIGINT) + try: + await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) + except TimeoutError: + # If SIGINT didn't work, try SIGTERM + process.terminate() + try: + await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) + except TimeoutError: + # Last resort: SIGKILL + process.kill() + await process.wait() + + +@asynccontextmanager +async def run_binary_and_wait_for_port( + binary_path: Path, + host: str, + port: int, + timeout: float = PORT_WAIT_TIMEOUT, + line_callback: Callable[[str], None] | None = None, +) -> AsyncGenerator[None]: + """Run a binary, wait for it to open a port, and clean up on exit.""" + async with run_binary(binary_path, line_callback=line_callback) as ( + process, + stdout_lines, + ): + loop = asyncio.get_running_loop() + start_time = loop.time() while loop.time() - start_time < timeout: try: # Try to connect to the port @@ -593,41 +625,6 @@ async def run_binary_and_wait_for_port( raise TimeoutError(error_msg) - finally: - # Cancel output collection tasks - for task in output_tasks: - task.cancel() - # Wait for tasks to complete and check for exceptions - results = await asyncio.gather(*output_tasks, return_exceptions=True) - for i, result in enumerate(results): - if isinstance(result, Exception) and not isinstance( - result, asyncio.CancelledError - ): - print( - f"Error reading from PTY: {result}", - file=sys.stderr, - ) - - # Close the PTY transport (Unix only) - if controller_transport is not None: - controller_transport.close() - - # Cleanup: terminate the process gracefully - if process.returncode is None: - # Send SIGINT (Ctrl+C) for graceful shutdown - process.send_signal(signal.SIGINT) - try: - await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) - except TimeoutError: - # If SIGINT didn't work, try SIGTERM - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) - except TimeoutError: - # Last resort: SIGKILL - process.kill() - await process.wait() - @asynccontextmanager async def run_compiled_context( diff --git a/tests/integration/fixtures/safe_mode_loop_runs.yaml b/tests/integration/fixtures/safe_mode_loop_runs.yaml new file mode 100644 index 00000000000..342622428bc --- /dev/null +++ b/tests/integration/fixtures/safe_mode_loop_runs.yaml @@ -0,0 +1,25 @@ +esphome: + name: safe-mode-loop-runs + +host: + +logger: + +safe_mode: + num_attempts: 10 + on_safe_mode: + - lambda: |- + // Spawn a detached thread that logs a unique marker. The + // non-main-thread log goes through the task log buffer, which + // is only drained by Logger::loop(). If looping components + // weren't initialized (the bug fixed in #16269), the buffer is + // never read and the marker never reaches the console. + struct MarkerThread { + static void *thread_func(void *) { + ESP_LOGI("safe_mode_test", "looping component ran in safe mode"); + return nullptr; + } + }; + pthread_t t; + pthread_create(&t, nullptr, MarkerThread::thread_func, nullptr); + pthread_detach(t); diff --git a/tests/integration/host_prefs.py b/tests/integration/host_prefs.py new file mode 100644 index 00000000000..f835bee3bc6 --- /dev/null +++ b/tests/integration/host_prefs.py @@ -0,0 +1,39 @@ +"""Helpers for manipulating the host platform's preferences file. + +ESPHome's host platform stores preferences in +``~/.esphome/prefs/<app_name>.prefs`` using a simple binary layout that +mirrors ``HostPreferences::sync()``: +``[uint32_t key][uint8_t len][uint8_t data[len]]`` per entry. + +Tests use these helpers to pre-populate state the binary will see at +boot (e.g. forcing safe mode) or to clear stale state between runs. +""" + +from __future__ import annotations + +from pathlib import Path +import struct + + +def host_prefs_path(device_name: str) -> Path: + """Return the on-disk prefs file path for a host-platform device.""" + return Path.home() / ".esphome" / "prefs" / f"{device_name}.prefs" + + +def clear_host_prefs(device_name: str) -> None: + """Delete the prefs file for a host-platform device, if it exists.""" + host_prefs_path(device_name).unlink(missing_ok=True) + + +def write_host_pref(device_name: str, key: int, data: bytes) -> Path: + """Write a single preference entry, replacing the file's contents. + + Returns the path that was written. + """ + if len(data) > 255: + raise ValueError(f"Preference data too long: {len(data)} bytes (max 255)") + path = host_prefs_path(device_name) + path.parent.mkdir(parents=True, exist_ok=True) + payload = struct.pack("<IB", key, len(data)) + data + path.write_bytes(payload) + return path diff --git a/tests/integration/test_safe_mode_loop_runs.py b/tests/integration/test_safe_mode_loop_runs.py new file mode 100644 index 00000000000..652877fc092 --- /dev/null +++ b/tests/integration/test_safe_mode_loop_runs.py @@ -0,0 +1,94 @@ +"""Regression test for safe_mode + looping_components init ordering. + +Reproduces the bug fixed in https://github.com/esphome/esphome/pull/16269: +``App.looping_components_.init(...)`` was emitted at ``CoroPriority.FINAL``, +which placed it *after* the ``safe_mode`` early-return in ``setup_app()``. +When safe mode was entered, the ``FixedVector`` backing the looping-component +list was never sized, ``looping_components_active_end_`` stayed at 0, and +``loop()`` iterated zero components -- so any looping component above +``CoroPriority.APPLICATION`` (e.g. wifi, logger) never ran. + +The test forces safe mode by writing ``ENTER_SAFE_MODE_MAGIC`` to the host +preferences file before booting, then asserts that ``Logger::loop()`` runs +by logging from a non-main thread. Non-main-thread logs are buffered in +``TaskLogBuffer`` and only emitted to the console when ``Logger::loop()`` +drains the buffer. Without the fix, the marker stays in the buffer +forever; with the fix, it reaches the console. + +The API server (``CoroPriority.WEB``, 40) is registered below safe_mode +(``CoroPriority.APPLICATION``, 50), so it's never set up when safe mode +is active and ``run_compiled`` would hang waiting for the API port. +This test uses ``run_binary`` directly to skip the port wait. +""" + +from __future__ import annotations + +import asyncio +import re +import struct + +import pytest + +from .conftest import run_binary +from .host_prefs import clear_host_prefs, write_host_pref +from .types import CompileFunction, ConfigWriter + +# Must match esphome::safe_mode::RTC_KEY in safe_mode.h +SAFE_MODE_RTC_KEY = 233825507 +# Must match esphome::safe_mode::SafeModeComponent::ENTER_SAFE_MODE_MAGIC +ENTER_SAFE_MODE_MAGIC = 0x5AFE5AFE + +DEVICE_NAME = "safe-mode-loop-runs" +THREAD_LOG_MARKER = "looping component ran in safe mode" + + +@pytest.mark.asyncio +async def test_safe_mode_loop_runs( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, +) -> None: + """When safe mode is active, ``App.loop()`` must still iterate looping + components -- proven here by a thread-logged marker reaching the + console (which requires ``Logger::loop()`` to run).""" + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # Compile finished successfully; pre-populate prefs so the *next* run + # enters safe mode immediately. + write_host_pref( + DEVICE_NAME, SAFE_MODE_RTC_KEY, struct.pack("<I", ENTER_SAFE_MODE_MAGIC) + ) + + try: + loop = asyncio.get_running_loop() + safe_mode_active = loop.create_future() + thread_log_seen = loop.create_future() + safe_mode_pattern = re.compile(r"SAFE MODE IS ACTIVE") + thread_log_pattern = re.compile(re.escape(THREAD_LOG_MARKER)) + + def on_log(line: str) -> None: + if not safe_mode_active.done() and safe_mode_pattern.search(line): + safe_mode_active.set_result(True) + if not thread_log_seen.done() and thread_log_pattern.search(line): + thread_log_seen.set_result(True) + + async with run_binary(binary_path, line_callback=on_log): + try: + await asyncio.wait_for(safe_mode_active, timeout=15.0) + except TimeoutError: + pytest.fail( + "Did not observe 'SAFE MODE IS ACTIVE' -- safe mode " + "didn't trigger, so this test isn't exercising the bug." + ) + try: + await asyncio.wait_for(thread_log_seen, timeout=10.0) + except TimeoutError: + pytest.fail( + f"Did not observe thread-logged marker {THREAD_LOG_MARKER!r} " + "within timeout. Logger::loop() never drained the task " + "log buffer, meaning App.looping_components_ was never " + "sized -- this is the regression #16269 fixed." + ) + finally: + clear_host_prefs(DEVICE_NAME) diff --git a/tests/integration/test_template_text_save.py b/tests/integration/test_template_text_save.py index 47c8e3188ab..7e56209c500 100644 --- a/tests/integration/test_template_text_save.py +++ b/tests/integration/test_template_text_save.py @@ -9,7 +9,6 @@ Tests that: from __future__ import annotations import asyncio -from pathlib import Path import socket from typing import Any @@ -17,9 +16,12 @@ from aioesphomeapi import TextInfo, TextState import pytest from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client +from .host_prefs import clear_host_prefs from .state_utils import InitialStateHelper, require_entity from .types import CompileFunction, ConfigWriter +DEVICE_NAME = "host-template-text-save-test" + @pytest.mark.asyncio async def test_template_text_save( @@ -32,11 +34,7 @@ async def test_template_text_save( port, port_socket = reserved_tcp_port # Clean up any stale preference file from previous runs - prefs_file = ( - Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs" - ) - if prefs_file.exists(): - prefs_file.unlink() + clear_host_prefs(DEVICE_NAME) # Write and compile once config_path = await write_yaml_config(yaml_config) @@ -59,7 +57,7 @@ async def test_template_text_save( wait_and_connect_api_client(port=port) as client, ): device_info = await client.device_info() - assert device_info.name == "host-template-text-save-test" + assert device_info.name == DEVICE_NAME entities, _ = await client.list_entities_services() text_entity = require_entity( @@ -127,5 +125,4 @@ async def test_template_text_save( ) # Clean up preference file - if prefs_file.exists(): - prefs_file.unlink() + clear_host_prefs(DEVICE_NAME) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 6fa8f7ed43a..4ce862315d7 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config_validation as cv, core +from esphome.components.safe_mode import to_code as safe_mode_to_code from esphome.const import ( CONF_AREA, CONF_AREAS, @@ -312,6 +313,75 @@ def test_add_platform_defines_priority() -> None: ) +def test_to_code_priority_above_safe_mode() -> None: + """Test that core to_code emits the looping_components_ init before safe_mode. + + Regression test for https://github.com/esphome/esphome/issues/16262. + safe_mode emits an `if (should_enter_safe_mode(...)) return;` line in main() + at APPLICATION priority. The `App.looping_components_.init(...)` call must be + emitted at a higher priority than APPLICATION so it lands in main() before + the early return; otherwise the FixedVector is never sized when safe mode is + active and loop() never runs (Wi-Fi never connects). + """ + assert config.to_code.priority > safe_mode_to_code.priority, ( + f"core to_code priority ({config.to_code.priority}) must be greater than " + f"safe_mode to_code priority ({safe_mode_to_code.priority}) so that " + "App.looping_components_.init() is emitted before safe_mode's early return" + ) + + +@pytest.mark.asyncio +async def test_add_looping_components_handles_empty_entries() -> None: + """Test that _add_looping_components emits a valid constexpr when there are + no looping component entries. + + With zero entries the generated constexpr must still be syntactically valid + C++ (`= 0;`), not an empty expression (`= ;`). This guards the empty-list + case that would otherwise produce uncompilable main.cpp output. + """ + CORE.data["looping_component_entries"] = [] + + await config._add_looping_components() + + constexpr_lines = [ + str(s) + for s in CORE.global_statements + if "ESPHOME_LOOPING_COMPONENT_COUNT" in str(s) + ] + assert len(constexpr_lines) == 1 + text = constexpr_lines[0] + assert "static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in text + # The right-hand side must contain a literal `0`, not be empty. + rhs = text.split("=", 1)[1] + assert "0" in rhs + assert rhs.strip().rstrip(";").strip(), ( + f"constexpr right-hand side must not be empty, got: {text!r}" + ) + + +@pytest.mark.asyncio +async def test_add_looping_components_with_entries() -> None: + """Test that _add_looping_components builds a HasLoopOverride sum from entries.""" + CORE.data["looping_component_entries"] = [ + "esphome::wifi::WiFiComponent", + "esphome::logger::Logger", + "esphome::wifi::WiFiComponent", + ] + + await config._add_looping_components() + + constexpr_lines = [ + str(s) + for s in CORE.global_statements + if "ESPHOME_LOOPING_COMPONENT_COUNT" in str(s) + ] + assert len(constexpr_lines) == 1 + text = constexpr_lines[0] + # Deduplicated by type, with per-type counts as multiplier. + assert "(2 * HasLoopOverride<esphome::wifi::WiFiComponent>::value)" in text + assert "(1 * HasLoopOverride<esphome::logger::Logger>::value)" in text + + def test_valid_include_with_angle_brackets() -> None: """Test valid_include accepts angle bracket includes.""" assert valid_include("<ArduinoJson.h>") == "<ArduinoJson.h>" From 2864922ac05311159596877c51fc7464cec8fa1b Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 6 May 2026 14:59:10 +0000 Subject: [PATCH 1872/2056] [ota] Partition table update: Fix log messages (#16241) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/esphome/ota/ota_esphome.cpp | 4 +-- .../components/ota/ota_backend_esp_idf.cpp | 3 +- .../components/ota/ota_partitions_esp_idf.cpp | 33 ++++++++++++------- esphome/espota2.py | 6 ++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 3ce3f2302db..5d3deca4896 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -117,8 +117,8 @@ void ESPHomeOTAComponent::dump_config() { " Partition table:\n" " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); - esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); - while (it != NULL) { + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, nullptr); + while (it != nullptr) { const esp_partition_t *partition = esp_partition_get(it); ESP_LOGCONFIG(TAG, " %-12s 0x%-2X 0x%-6X 0x%-8" PRIX32 " 0x%-8" PRIX32, partition->label, partition->type, partition->subtype, partition->address, partition->size); diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 42d106bf1fb..50a0988ba21 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -20,8 +20,7 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) #ifdef USE_OTA_PARTITIONS this->ota_type_ = ota_type; if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { - // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN: under- leaves stale bytes from the - // previous table; over- can't fit the reserved region. + // Reject any size other than ESP_PARTITION_TABLE_MAX_LEN if (image_size != ESP_PARTITION_TABLE_MAX_LEN) { ESP_LOGE(TAG, "Wrong partition table size: expected %u bytes, got %zu", ESP_PARTITION_TABLE_MAX_LEN, image_size); return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index 2a2ed577f1e..f7fd529986d 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -11,6 +11,7 @@ #include <esp_ota_ops.h> #include <nvs_flash.h> +#include <cinttypes> #include <cstring> namespace esphome::ota { @@ -135,10 +136,20 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a // Rejecting here is non-destructive (no flash op has run yet); the user can safely retry with // a different .bin. Log enough info that they can pick the right method without guessing. ESP_LOGE(TAG, - "Running app at 0x%X (%u bytes used) does not fit any compatible slot in the new " - "partition table. Pick a migration method whose size limit is at least %u bytes and " - "retry; no flash content was modified.", - running_app_offset, running_app_size, running_app_size); + "The new partition table must contain a compatible app partition with:\n" + " size: at least %" PRIu32 " bytes (0x%" PRIX32 ")\n" + " address: one of", + (uint32_t) running_app_size, (uint32_t) running_app_size); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + while (it != nullptr) { + const esp_partition_t *partition = esp_partition_get(it); + if (partition->size >= running_app_size) { + ESP_LOGE(TAG, " 0x%" PRIX32, partition->address); + } + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); + ESP_LOGE(TAG, "Upload a different partition table. No flash content was modified."); return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } if (app_partitions_found < 2) { @@ -154,11 +165,11 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } if (otadata_overlap) { + // Unlikely, the otadata partition is before the start of the first app partition in most cases ESP_LOGE(TAG, - "New otadata partition overlaps with the running app at 0x%X (size %u). The chosen " - "partition table is not compatible with this device's current flash layout; pick a " - "different migration method.", - running_app_offset, running_app_size); + "New otadata partition overlaps with the running app at address: 0x%" PRIX32 ", running app size: %" PRIu32 + " bytes", + running_app_offset, (uint32_t) running_app_size); return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } @@ -198,8 +209,8 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { // can leave the device unbootable until it is recovered with a serial flash. ESP_LOGE(TAG, "Starting partition table update.\n" " DO NOT REMOVE POWER until the device reboots successfully.\n" - " Loss of power during this operation may render the device unable to boot until\n" - " it is recovered via a serial flash."); + " Loss of power during this operation may render the device\n" + " unable to boot until it is recovered via a serial flash."); // One guard over the whole critical section in case an IDF call takes longer than expected on // some chip variant. @@ -214,7 +225,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { // which leaves esp_ota_get_running_partition() returning nullptr. const esp_partition_t *running_app_part = find_app_partition_at(running_app_offset, running_app_size); if (running_app_part == nullptr) { - ESP_LOGE(TAG, "Cannot resolve running app partition at offset 0x%X", running_app_offset); + ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, diff --git a/esphome/espota2.py b/esphome/espota2.py index a45a6ef2343..b2a1fd2a40a 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -139,9 +139,9 @@ _ERROR_MESSAGES: dict[int, str] = { ), RESPONSE_ERROR_PARTITION_TABLE_UPDATE: ( "An error occurred while updating the partition table. The device is now " - "in a degraded state (NVS handles are invalid; many components will fail) " - "and may not be able to boot. Check the logs, reboot the device, and " - "retry the update. If the device fails to boot, recover it via a serial flash." + "in a degraded state and may not be able to boot. Open the logs and retry " + "the partition table update without rebooting the device. If the device " + "fails to boot, recover it via a serial flash." ), RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", } From cfd2c9182c9cf7355cd4c536f64bc81e7073073c Mon Sep 17 00:00:00 2001 From: Didier A <DidierA@users.noreply.github.com> Date: Wed, 6 May 2026 18:34:55 +0200 Subject: [PATCH 1873/2056] [bl0942] Remove broken 24-bit overflow tracking (#15650) Co-authored-by: DidierA <1620015+didiera@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/bl0942/bl0942.cpp | 8 ++------ esphome/components/bl0942/bl0942.h | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 7d385974231..074aff96431 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -161,13 +161,9 @@ void BL0942::received_package_(DataPacket *data) { return; } - // cf_cnt is only 24 bits, so track overflows + // cf_cnt wraps at 24 bits; total_increasing on the energy sensor handles the + // wrap (and any spurious chip resets) downstream. uint32_t cf_cnt = (uint24_t) data->cf_cnt; - cf_cnt |= this->prev_cf_cnt_ & 0xff000000; - if (cf_cnt < this->prev_cf_cnt_) { - cf_cnt += 0x1000000; - } - this->prev_cf_cnt_ = cf_cnt; float v_rms = (uint24_t) data->v_rms / voltage_reference_; float i_rms = (uint24_t) data->i_rms / current_reference_; diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 3c013f86e7d..7604399c25d 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -141,7 +141,6 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { bool reset_ = false; LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; optional<uint32_t> rx_start_{}; - uint32_t prev_cf_cnt_ = 0; bool validate_checksum_(DataPacket *data); int read_reg_(uint8_t reg); From a4a57a540d38b009dff46996203ea7249fda10a8 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 6 May 2026 12:56:54 -0400 Subject: [PATCH 1874/2056] [core] Adds acquire and release methods to the ring buffer class (#16277) --- esphome/core/ring_buffer.cpp | 8 ++++++++ esphome/core/ring_buffer.h | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index 2e0802eceb7..486cf67f25d 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -37,6 +37,14 @@ std::unique_ptr<RingBuffer> RingBuffer::create(size_t len, MemoryPreference pref return rb; } +void *RingBuffer::receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait) { + length = 0; + void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &length, ticks_to_wait, max_length); + return buffer_data; +} + +void RingBuffer::receive_release(void *item) { vRingbufferReturnItem(this->handle_, item); } + size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 4acd07d5b0c..8ac3ff38111 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -27,6 +27,28 @@ class RingBuffer { */ size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); + /** + * @brief Acquires a pointer into the ring buffer's internal storage without copying. + * + * The returned pointer is valid until receive_release() is called. Only one item + * may be checked out at a time. + * + * @param[out] length Set to the number of bytes actually acquired (may be less than max_length at wrap boundary) + * @param max_length Maximum number of bytes to acquire + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Pointer into the ring buffer's internal storage, or nullptr if no data is available + */ + void *receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait = 0); + + /** + * @brief Releases a previously acquired ring buffer item. + * + * Must be called exactly once for each successful receive_acquire(). + * + * @param item Pointer returned by receive_acquire() + */ + void receive_release(void *item); + /** * @brief Writes to the ring buffer, overwriting oldest data if necessary. * From fc25ab0246b1575d3e1bb2ea69dbc4546a72f239 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 6 May 2026 12:57:03 -0400 Subject: [PATCH 1875/2056] [i2s_audio] Optimize software volume control (#16278) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 51 ++++++------------- .../i2s_audio/speaker/i2s_audio_speaker.h | 2 +- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 836221e38a7..58d17ea6c43 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -13,22 +13,16 @@ #include "esp_timer.h" +// esp-audio-libs +#include <gain.h> + namespace esphome::i2s_audio { static const char *const TAG = "i2s_audio.speaker"; -// Lists the Q15 fixed point scaling factor for volume reduction. -// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. -// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) -// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15) -static const std::vector<int16_t> Q15_VOLUME_SCALING_FACTORS = { - 0, 116, 122, 130, 137, 146, 154, 163, 173, 183, 194, 206, 218, 231, 244, - 259, 274, 291, 308, 326, 345, 366, 388, 411, 435, 461, 488, 517, 548, 580, - 615, 651, 690, 731, 774, 820, 868, 920, 974, 1032, 1094, 1158, 1227, 1300, 1377, - 1459, 1545, 1637, 1734, 1837, 1946, 2061, 2184, 2313, 2450, 2596, 2750, 2913, 3085, 3269, - 3462, 3668, 3885, 4116, 4360, 4619, 4893, 5183, 5490, 5816, 6161, 6527, 6914, 7324, 7758, - 8218, 8706, 9222, 9770, 10349, 10963, 11613, 12302, 13032, 13805, 14624, 15491, 16410, 17384, 18415, - 19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767}; +// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor. +// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB. +static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f; void I2SAudioSpeakerBase::setup() { this->event_group_ = xEventGroupCreate(); @@ -147,14 +141,16 @@ void I2SAudioSpeakerBase::set_volume(float volume) { } else #endif // USE_AUDIO_DAC { - // Fallback to software volume control by using a Q15 fixed point scaling factor. - // At maximum volume (1.0), set to INT16_MAX to completely bypass volume processing + // Fallback to software volume control by using a Q31 fixed point scaling factor. + // At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely // and avoid any floating-point precision issues that could cause slight volume reduction. if (volume >= 1.0f) { - this->q15_volume_factor_ = INT16_MAX; + this->q31_volume_factor_ = INT32_MAX; + } else if (volume <= 0.0f) { + this->q31_volume_factor_ = 0; } else { - ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); - this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + this->q31_volume_factor_ = + esp_audio_libs::gain::db_to_q31(remap<float, float>(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f)); } } } @@ -173,7 +169,7 @@ void I2SAudioSpeakerBase::set_mute_state(bool mute_state) { { if (mute_state) { // Fallback to software volume control and scale by 0 - this->q15_volume_factor_ = 0; + this->q31_volume_factor_ = 0; } else { // Revert to previous volume when unmuting this->set_volume(this->volume_); @@ -309,29 +305,14 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s } void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) { - if (this->q15_volume_factor_ >= INT16_MAX) { + if (this->q31_volume_factor_ == INT32_MAX) { return; // Max volume, no processing needed } const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1); const uint32_t len = bytes_read / bytes_per_sample; - // Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31 - int32_t shift = 15; // Q31 -> Q16 - int32_t gain_factor = this->q15_volume_factor_; // Q15 - - if (bytes_per_sample >= 3) { - // Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31 - shift = 8; // Q31 -> Q23 - gain_factor >>= 7; // Q15 -> Q8 - } - - for (uint32_t i = 0; i < len; ++i) { - int32_t sample = audio::unpack_audio_sample_to_q31(&data[i * bytes_per_sample], bytes_per_sample); // Q31 - sample >>= shift; - sample *= gain_factor; // Q31 - audio::pack_q31_as_audio_sample(sample, &data[i * bytes_per_sample], bytes_per_sample); - } + esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample); } void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b2644efd054..d9a228ef2c6 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -151,7 +151,7 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public bool pause_state_{false}; - int16_t q15_volume_factor_{INT16_MAX}; + int32_t q31_volume_factor_{INT32_MAX}; audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info From 9f49e3f80e75d2309eaf6766222aa622a458516f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Wed, 6 May 2026 13:22:18 -0400 Subject: [PATCH 1876/2056] [audio] Bump microFLAC to v0.2.0 (#16279) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index cfb2ad4e757..67ef2e7d1ae 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -386,7 +386,7 @@ async def to_code(config): # Adds a define and IDF component for legacy `audio_decoder.cpp`. if data.flac_support: cg.add_define("USE_AUDIO_FLAC_SUPPORT") - add_idf_component(name="esphome/micro-flac", ref="0.1.1") + add_idf_component(name="esphome/micro-flac", ref="0.2.0") _emit_memory_pair( data.flac.buffer_memory, "CONFIG_MICRO_FLAC_PREFER_PSRAM", diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 37c0da11f57..8ffcffa7051 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -8,7 +8,7 @@ dependencies: esphome/micro-decoder: version: 0.2.0 esphome/micro-flac: - version: 0.1.1 + version: 0.2.0 esphome/micro-mp3: version: 0.2.0 esphome/micro-opus: From 4da62067cf5864ebcd8448c2c7ee8aed16ccdd11 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 6 May 2026 19:32:50 +0200 Subject: [PATCH 1877/2056] [nextion] Fix text sensor state not updated on string response (#16280) --- esphome/components/nextion/nextion.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e42f7ca2169..b644cad5077 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -641,6 +641,7 @@ void Nextion::process_nextion_commands_() { } else { ESP_LOGN(TAG, "String resp: '%s' id: %s type: %s", to_process.c_str(), component->get_variable_name().c_str(), component->get_queue_type_string()); + component->set_state_from_string(to_process, true, false); } delete nb; // NOLINT(cppcoreguidelines-owning-memory) From 0d94ffe15dbde9cd0bb8f852b7167e14417bdbda Mon Sep 17 00:00:00 2001 From: dbl-0 <keel.avail-5g@icloud.com> Date: Wed, 6 May 2026 11:48:38 -0600 Subject: [PATCH 1878/2056] [resolver] Make RESOLVE_TIMEOUT configurable via environment variable (#15951) Co-authored-by: Daniel Lowe <dan@auth.dxj.app> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/resolver.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/esphome/resolver.py b/esphome/resolver.py index 9fb596ce7b5..f80a910afe5 100644 --- a/esphome/resolver.py +++ b/esphome/resolver.py @@ -2,13 +2,28 @@ from __future__ import annotations +import logging +import os + from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError import aioesphomeapi.host_resolver as hr from esphome.async_thread import AsyncThreadRunner from esphome.core import EsphomeError -RESOLVE_TIMEOUT = 10.0 # seconds +_LOGGER = logging.getLogger(__name__) + +_DEFAULT_RESOLVE_TIMEOUT = 20.0 +_env_timeout = os.environ.get("ESPHOME_RESOLVE_TIMEOUT", _DEFAULT_RESOLVE_TIMEOUT) +try: + RESOLVE_TIMEOUT = float(_env_timeout) +except ValueError: + _LOGGER.warning( + "ESPHOME_RESOLVE_TIMEOUT=%r is not a valid number; using default %.1fs", + _env_timeout, + _DEFAULT_RESOLVE_TIMEOUT, + ) + RESOLVE_TIMEOUT = _DEFAULT_RESOLVE_TIMEOUT class AsyncResolver: From 6173656bf8db35a563071cddfb3180304bf0a9fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 12:49:00 -0500 Subject: [PATCH 1879/2056] [schema] Surface OnlyWith / OnlyWithout default + gate components in schema generator (#16276) --- script/build_language_schema.py | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 09ff9999013..05ac47bfcce 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1065,7 +1065,40 @@ def convert_keys(converted, schema, path): else: converted["key_type"] = str(k) - if hasattr(k, "default") and str(k.default) != "...": + # ``cv.OnlyWith`` / ``cv.OnlyWithout`` expose ``default`` as + # a property that returns ``vol.UNDEFINED`` when the gating + # component isn't loaded — and at schema-generation time + # ``CORE.loaded_integrations`` is always empty, so the + # property never resolves. The unconditional default lives + # on ``_default``; expose it under a *new* per-class field + # (``default_with`` for ``OnlyWith``, ``default_without`` for + # ``OnlyWithout``) that bundles the value with the gating + # component(s). Pure addition to the bundle — old consumers + # that read only ``default`` see these fields as + # default-less (same as today, no regression where they used + # to fall back to a hard-coded UI default); new consumers + # opt-in to the gated fields and apply the default + # *conditionally* on which integrations the user has + # loaded. Without the gate info, an ethernet-only config on + # ``cv.OnlyWith(K, "wifi", default=True)`` would otherwise + # render ``True`` even though ESPHome itself wouldn't apply + # the default for that config. + if isinstance(k, (cv.OnlyWith, cv.OnlyWithout)): + default_value = k._default() + if default_value is not None: + components = ( + list(k._component) + if isinstance(k._component, list) + else [k._component] + ) + gate_field = ( + "default_with" if isinstance(k, cv.OnlyWith) else "default_without" + ) + result[gate_field] = { + "value": str(default_value), + "components": components, + } + elif hasattr(k, "default") and str(k.default) != "...": default_value = k.default() if default_value is not None: result["default"] = str(default_value) From 1e58e8729a0a73117bbfa533f0a498ffb3e228f6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 6 May 2026 14:53:48 -0400 Subject: [PATCH 1880/2056] [uart] Use `tcdrain` for flushing instead of`tcflush` on host (#14877) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/uart/uart_component_host.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 085610a9835..5bb7a497260 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -276,9 +276,12 @@ UARTFlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } - tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); - return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; + if (tcdrain(this->file_descriptor_) == -1) { + this->update_error_(strerror(errno)); + return UARTFlushResult::UART_FLUSH_RESULT_FAILED; + } + return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { From d2bbaeccf33359f9b96010aae17736f5224ba659 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 May 2026 21:42:11 +1200 Subject: [PATCH 1881/2056] [ha-addon] Add opt-in toggle for the new ESPHome Device Builder (#16247) --- .../etc/cont-init.d/40-device-builder.sh | 22 +++++++++++++++++++ .../etc/s6-overlay/s6-rc.d/esphome/run | 7 ++++++ .../etc/s6-overlay/s6-rc.d/init-nginx/run | 8 +++++++ .../etc/s6-overlay/s6-rc.d/nginx/run | 8 +++++++ 4 files changed, 45 insertions(+) create mode 100755 docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh b/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh new file mode 100755 index 00000000000..b9904697626 --- /dev/null +++ b/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh @@ -0,0 +1,22 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Installs the latest prerelease of esphome-device-builder when the +# `use_new_device_builder` config option is enabled. +# This is a temporary install-on-boot step until esphome-device-builder +# becomes a direct dependency of esphome. +# ============================================================================== + +if ! bashio::config.true 'use_new_device_builder'; then + exit 0 +fi + +bashio::log.info "Installing latest prerelease of esphome-device-builder..." +if command -v uv > /dev/null; then + uv pip install --system --no-cache-dir --prerelease=allow --upgrade \ + esphome-device-builder || + bashio::exit.nok "Failed installing esphome-device-builder." +else + pip install --no-cache-dir --pre --upgrade esphome-device-builder || + bashio::exit.nok "Failed installing esphome-device-builder." +fi +bashio::log.info "Installed esphome-device-builder." diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index cdbaff6c04c..64ac0b18d2e 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -49,5 +49,12 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then rm -rf /config/esphome/.esphome fi +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "Starting ESPHome Device Builder..." + exec esphome-device-builder /config/esphome \ + --ha-addon \ + --ingress-port "$(bashio::addon.ingress_port)" +fi + bashio::log.info "Starting ESPHome dashboard..." exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run index 2725f56670b..18c75898ec0 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run @@ -4,6 +4,14 @@ # Community Hass.io Add-ons: ESPHome # Configures NGINX for use with ESPHome # ============================================================================== + +# When the new device builder is enabled it serves HA ingress directly, +# so nginx is not used at all -- skip configuration. +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly." + bashio::exit.ok +fi + mkdir -p /var/log/nginx # Generate Ingress configuration diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run index e96991cdd19..bb5f52e10c7 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -5,6 +5,14 @@ # Runs the NGINX proxy # ============================================================================== +# The new device builder handles HA ingress itself, so nginx is bypassed. +# Block the longrun forever so s6 keeps the dependency satisfied and does +# not respawn it. +if bashio::config.true 'use_new_device_builder'; then + bashio::log.info "NGINX bypassed: new device builder serves ingress directly." + exec sleep infinity +fi + bashio::log.info "Waiting for ESPHome dashboard to come up..." while [[ ! -S /var/run/esphome.sock ]]; do From 016b509b551610d8fefd3fa90f1baf1b387940c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 07:32:35 -0500 Subject: [PATCH 1882/2056] [bundle] Include secrets.yaml when `!secret` keys are quoted (#16271) --- esphome/bundle.py | 12 +++++---- tests/unit_tests/test_bundle.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/esphome/bundle.py b/esphome/bundle.py index efa80acc8cc..70c4fad0fda 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -98,11 +98,13 @@ _KNOWN_FILE_EXTENSIONS = frozenset( ) -# Matches !secret references in YAML text. This is intentionally a simple -# regex scan rather than a YAML parse — it may match inside comments or -# multi-line strings, which is the conservative direction (include more -# secrets rather than fewer). -_SECRET_RE = re.compile(r"!secret\s+(\S+)") +# Matches !secret references in YAML text. An optional surrounding +# quote pair around the key is allowed and ignored: YAML treats +# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is +# intentionally a simple regex scan rather than a YAML parse — it may +# match inside comments or multi-line strings, which is the conservative +# direction (include more secrets rather than fewer). +_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""") def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]: diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 89bf1a33b3e..5d046252dae 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -170,6 +170,23 @@ def test_find_used_secret_keys_deduplicates(tmp_path: Path) -> None: assert keys == {"key1"} +def test_find_used_secret_keys_quoted(tmp_path: Path) -> None: + """Quoted !secret keys should resolve to the same key as unquoted form. + + YAML strips surrounding quotes during parsing, so the secrets.yaml + lookup uses the unquoted key. The bundle scan must do the same. + """ + yaml1 = tmp_path / "a.yaml" + yaml1.write_text( + "single: !secret 'wifi_ssid'\n" + 'double: !secret "wifi_pw"\n' + "bare: !secret api_key\n" + ) + + keys = _find_used_secret_keys([yaml1]) + assert keys == {"wifi_ssid", "wifi_pw", "api_key"} + + # --------------------------------------------------------------------------- # _add_bytes_to_tar # --------------------------------------------------------------------------- @@ -1217,6 +1234,35 @@ def test_create_bundle_filters_secrets(tmp_path: Path) -> None: assert "should_not_appear" not in secrets_data +def test_create_bundle_filters_secrets_quoted(tmp_path: Path) -> None: + """Bundling must include secrets.yaml when !secret keys are quoted. + + Regression test for issue 16259: quoted !secret references previously + captured the quotes as part of the key, so no key matched secrets.yaml + entries and the secrets file was dropped from the bundle entirely. + """ + config_dir = _setup_config_dir(tmp_path) + + secrets = config_dir / "secrets.yaml" + secrets.write_text("ota_password: hunter2\nunused: should_not_appear\n") + + config_yaml = "ota:\n password: !secret 'ota_password'\n" + (config_dir / "test.yaml").write_text(config_yaml) + + creator = ConfigBundleCreator({}) + result = creator.create_bundle() + + assert result.manifest[ManifestKey.HAS_SECRETS] is True + + buf = io.BytesIO(result.data) + with tarfile.open(fileobj=buf, mode="r:gz") as tar: + secrets_data = tar.extractfile("secrets.yaml").read().decode() + + assert "ota_password" in secrets_data + assert "hunter2" in secrets_data + assert "unused" not in secrets_data + + def test_create_bundle_no_secrets(tmp_path: Path) -> None: _setup_config_dir(tmp_path) From 7f6aef4f33f882936ab2e68008ed6079c7a27508 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 6 May 2026 08:41:17 -0500 Subject: [PATCH 1883/2056] [substitutions] Fix sibling references inside dict-valued substitutions (#16273) --- esphome/components/substitutions/__init__.py | 13 ++++++++++++- .../18-dict_self_reference.approved.yaml | 16 ++++++++++++++++ .../18-dict_self_reference.input.yaml | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 0144c13c017..af261fe2a38 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -297,7 +297,18 @@ def _push_context( """Resolve a variable, recursively resolving any dependencies it references.""" value = unresolved_vars.pop(key, Missing) if value is Missing: - return Missing + # Either already resolved (in resolved_vars) or currently being + # resolved (self-reference from inside a dict-valued substitution). + # Returning what we have lets sibling references inside a dict + # value, e.g. ``${device.manufacturer}`` inside ``device.name``, + # see literal sibling values during their own resolution. + return resolved_vars.get(key, Missing) + if isinstance(value, dict): + # Dict-valued substitutions form a namespace; eagerly publish the + # original mapping so its members can reference each other while + # the dict's own substitution pass is still running. The entry is + # replaced with the fully-substituted dict once recursion returns. + resolved_vars[key] = value try: value = substitute(value, [], resolver_context, True) except UndefinedError as err: diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml new file mode 100644 index 00000000000..e5e6d4568e8 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.approved.yaml @@ -0,0 +1,16 @@ +substitutions: + device: + manufacturer: espressif + model: esp32 + mac_suffix: ffffff + name: espressif-esp32-ffffff + network: + host: example.com + port: 8080 + url: http://example.com:8080/api +esphome: + name: espressif-esp32-ffffff +test_list: + - espressif-esp32-ffffff + - http://example.com:8080/api + - espressif/esp32 diff --git a/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml new file mode 100644 index 00000000000..b27c4b8c291 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/18-dict_self_reference.input.yaml @@ -0,0 +1,18 @@ +substitutions: + device: + manufacturer: "espressif" + model: "esp32" + mac_suffix: "ffffff" + name: ${device.manufacturer}-${device.model}-${device.mac_suffix} + network: + host: "example.com" + port: 8080 + url: "http://${network.host}:${network.port}/api" + +esphome: + name: ${device.name} + +test_list: + - ${device.name} + - ${network.url} + - "${device.manufacturer}/${device.model}" From b89c71c1eac2c99412540da1a3e9c1117275c093 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 6 May 2026 14:56:33 +0000 Subject: [PATCH 1884/2056] [core] Fix WiFi connection in safe mode (#16269) Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/core/config.py | 27 ++--- tests/integration/conftest.py | 111 +++++++++--------- .../fixtures/safe_mode_loop_runs.yaml | 25 ++++ tests/integration/host_prefs.py | 39 ++++++ tests/integration/test_safe_mode_loop_runs.py | 94 +++++++++++++++ tests/integration/test_template_text_save.py | 15 +-- tests/unit_tests/core/test_config.py | 70 +++++++++++ 7 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 tests/integration/fixtures/safe_mode_loop_runs.yaml create mode 100644 tests/integration/host_prefs.py create mode 100644 tests/integration/test_safe_mode_loop_runs.py diff --git a/esphome/core/config.py b/esphome/core/config.py index bf210876dfb..70c28a0368c 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -562,14 +562,9 @@ async def _add_controller_registry_define() -> None: @coroutine_with_priority(CoroPriority.FINAL) async def _add_looping_components() -> None: - # Emit a constexpr that computes the looping component count at C++ compile time - # and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to - # detect loop() overrides. The constexpr goes in main.cpp's global section where - # all component types are in scope. calculate_looping_components_() then skips - # the counting pass and only does the two population passes. + # Emit ESPHOME_LOOPING_COMPONENT_COUNT. Sizing of looping_components_ + # happens in core to_code() so it lands before safe_mode's early return. entries = CORE.data.get("looping_component_entries", []) - if not entries: - return # Build constexpr sum for the exact count, deduplicating by type # Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance @@ -577,7 +572,7 @@ async def _add_looping_components() -> None: terms = [ f"({count} * HasLoopOverride<{cpp_type}>::value)" for cpp_type, count in type_counts.items() - ] + ] or ["0"] constexpr_expr = " + \\\n ".join(terms) cg.add_global( cg.RawStatement( @@ -586,14 +581,6 @@ async def _add_looping_components() -> None: ) ) - # Pre-init FixedVector with exact capacity so calculate_looping_components_() - # can skip the counting pass - cg.add( - cg.RawExpression( - "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" - ) - ) - @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: @@ -636,6 +623,14 @@ async def to_code(config: ConfigType) -> None: # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) + # Pre-init FixedVector with exact capacity so calculate_looping_components_() + # can skip the counting pass + cg.add( + cg.RawExpression( + "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" + ) + ) + CORE.add_job(_add_platform_defines) CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_looping_components) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 7c85bf753ca..f36543b7cd9 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -501,14 +501,15 @@ async def _read_stream_lines( @asynccontextmanager -async def run_binary_and_wait_for_port( +async def run_binary( binary_path: Path, - host: str, - port: int, - timeout: float = PORT_WAIT_TIMEOUT, line_callback: Callable[[str], None] | None = None, -) -> AsyncGenerator[None]: - """Run a binary, wait for it to open a port, and clean up on exit.""" +) -> AsyncGenerator[tuple[asyncio.subprocess.Process, list[str]]]: + """Run a binary under a PTY, capture log output, and clean up on exit. + + Yields the running ``Process`` and a live list of captured log lines. + No port wait -- callers that need that should use + ``run_binary_and_wait_for_port``.""" # Create a pseudo-terminal to make the binary think it's running interactively # This is needed because the ESPHome host logger checks isatty() controller_fd, device_fd = pty.openpty() @@ -535,7 +536,6 @@ async def run_binary_and_wait_for_port( controller_transport, _ = await loop.connect_read_pipe( lambda: controller_protocol, os.fdopen(controller_fd, "rb", 0) ) - output_reader = controller_reader if process.returncode is not None: raise RuntimeError( @@ -543,27 +543,59 @@ async def run_binary_and_wait_for_port( "Ensure the binary is valid and can run successfully." ) - # Wait for the API server to start listening - loop = asyncio.get_running_loop() - start_time = loop.time() - - # Start collecting output stdout_lines: list[str] = [] - output_tasks: list[asyncio.Task] = [] + output_task = asyncio.create_task( + _read_stream_lines(controller_reader, stdout_lines, sys.stdout, line_callback) + ) try: - # Read from output stream - output_tasks = [ - asyncio.create_task( - _read_stream_lines( - output_reader, stdout_lines, sys.stdout, line_callback - ) - ) - ] - # Small yield to ensure the process has a chance to start await asyncio.sleep(0) + yield process, stdout_lines + finally: + output_task.cancel() + result = await asyncio.gather(output_task, return_exceptions=True) + if isinstance(result[0], Exception) and not isinstance( + result[0], asyncio.CancelledError + ): + print(f"Error reading from PTY: {result[0]}", file=sys.stderr) + # Close the PTY transport (Unix only) + if controller_transport is not None: + controller_transport.close() + + # Cleanup: terminate the process gracefully + if process.returncode is None: + # Send SIGINT (Ctrl+C) for graceful shutdown + process.send_signal(signal.SIGINT) + try: + await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) + except TimeoutError: + # If SIGINT didn't work, try SIGTERM + process.terminate() + try: + await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) + except TimeoutError: + # Last resort: SIGKILL + process.kill() + await process.wait() + + +@asynccontextmanager +async def run_binary_and_wait_for_port( + binary_path: Path, + host: str, + port: int, + timeout: float = PORT_WAIT_TIMEOUT, + line_callback: Callable[[str], None] | None = None, +) -> AsyncGenerator[None]: + """Run a binary, wait for it to open a port, and clean up on exit.""" + async with run_binary(binary_path, line_callback=line_callback) as ( + process, + stdout_lines, + ): + loop = asyncio.get_running_loop() + start_time = loop.time() while loop.time() - start_time < timeout: try: # Try to connect to the port @@ -593,41 +625,6 @@ async def run_binary_and_wait_for_port( raise TimeoutError(error_msg) - finally: - # Cancel output collection tasks - for task in output_tasks: - task.cancel() - # Wait for tasks to complete and check for exceptions - results = await asyncio.gather(*output_tasks, return_exceptions=True) - for i, result in enumerate(results): - if isinstance(result, Exception) and not isinstance( - result, asyncio.CancelledError - ): - print( - f"Error reading from PTY: {result}", - file=sys.stderr, - ) - - # Close the PTY transport (Unix only) - if controller_transport is not None: - controller_transport.close() - - # Cleanup: terminate the process gracefully - if process.returncode is None: - # Send SIGINT (Ctrl+C) for graceful shutdown - process.send_signal(signal.SIGINT) - try: - await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) - except TimeoutError: - # If SIGINT didn't work, try SIGTERM - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) - except TimeoutError: - # Last resort: SIGKILL - process.kill() - await process.wait() - @asynccontextmanager async def run_compiled_context( diff --git a/tests/integration/fixtures/safe_mode_loop_runs.yaml b/tests/integration/fixtures/safe_mode_loop_runs.yaml new file mode 100644 index 00000000000..342622428bc --- /dev/null +++ b/tests/integration/fixtures/safe_mode_loop_runs.yaml @@ -0,0 +1,25 @@ +esphome: + name: safe-mode-loop-runs + +host: + +logger: + +safe_mode: + num_attempts: 10 + on_safe_mode: + - lambda: |- + // Spawn a detached thread that logs a unique marker. The + // non-main-thread log goes through the task log buffer, which + // is only drained by Logger::loop(). If looping components + // weren't initialized (the bug fixed in #16269), the buffer is + // never read and the marker never reaches the console. + struct MarkerThread { + static void *thread_func(void *) { + ESP_LOGI("safe_mode_test", "looping component ran in safe mode"); + return nullptr; + } + }; + pthread_t t; + pthread_create(&t, nullptr, MarkerThread::thread_func, nullptr); + pthread_detach(t); diff --git a/tests/integration/host_prefs.py b/tests/integration/host_prefs.py new file mode 100644 index 00000000000..f835bee3bc6 --- /dev/null +++ b/tests/integration/host_prefs.py @@ -0,0 +1,39 @@ +"""Helpers for manipulating the host platform's preferences file. + +ESPHome's host platform stores preferences in +``~/.esphome/prefs/<app_name>.prefs`` using a simple binary layout that +mirrors ``HostPreferences::sync()``: +``[uint32_t key][uint8_t len][uint8_t data[len]]`` per entry. + +Tests use these helpers to pre-populate state the binary will see at +boot (e.g. forcing safe mode) or to clear stale state between runs. +""" + +from __future__ import annotations + +from pathlib import Path +import struct + + +def host_prefs_path(device_name: str) -> Path: + """Return the on-disk prefs file path for a host-platform device.""" + return Path.home() / ".esphome" / "prefs" / f"{device_name}.prefs" + + +def clear_host_prefs(device_name: str) -> None: + """Delete the prefs file for a host-platform device, if it exists.""" + host_prefs_path(device_name).unlink(missing_ok=True) + + +def write_host_pref(device_name: str, key: int, data: bytes) -> Path: + """Write a single preference entry, replacing the file's contents. + + Returns the path that was written. + """ + if len(data) > 255: + raise ValueError(f"Preference data too long: {len(data)} bytes (max 255)") + path = host_prefs_path(device_name) + path.parent.mkdir(parents=True, exist_ok=True) + payload = struct.pack("<IB", key, len(data)) + data + path.write_bytes(payload) + return path diff --git a/tests/integration/test_safe_mode_loop_runs.py b/tests/integration/test_safe_mode_loop_runs.py new file mode 100644 index 00000000000..652877fc092 --- /dev/null +++ b/tests/integration/test_safe_mode_loop_runs.py @@ -0,0 +1,94 @@ +"""Regression test for safe_mode + looping_components init ordering. + +Reproduces the bug fixed in https://github.com/esphome/esphome/pull/16269: +``App.looping_components_.init(...)`` was emitted at ``CoroPriority.FINAL``, +which placed it *after* the ``safe_mode`` early-return in ``setup_app()``. +When safe mode was entered, the ``FixedVector`` backing the looping-component +list was never sized, ``looping_components_active_end_`` stayed at 0, and +``loop()`` iterated zero components -- so any looping component above +``CoroPriority.APPLICATION`` (e.g. wifi, logger) never ran. + +The test forces safe mode by writing ``ENTER_SAFE_MODE_MAGIC`` to the host +preferences file before booting, then asserts that ``Logger::loop()`` runs +by logging from a non-main thread. Non-main-thread logs are buffered in +``TaskLogBuffer`` and only emitted to the console when ``Logger::loop()`` +drains the buffer. Without the fix, the marker stays in the buffer +forever; with the fix, it reaches the console. + +The API server (``CoroPriority.WEB``, 40) is registered below safe_mode +(``CoroPriority.APPLICATION``, 50), so it's never set up when safe mode +is active and ``run_compiled`` would hang waiting for the API port. +This test uses ``run_binary`` directly to skip the port wait. +""" + +from __future__ import annotations + +import asyncio +import re +import struct + +import pytest + +from .conftest import run_binary +from .host_prefs import clear_host_prefs, write_host_pref +from .types import CompileFunction, ConfigWriter + +# Must match esphome::safe_mode::RTC_KEY in safe_mode.h +SAFE_MODE_RTC_KEY = 233825507 +# Must match esphome::safe_mode::SafeModeComponent::ENTER_SAFE_MODE_MAGIC +ENTER_SAFE_MODE_MAGIC = 0x5AFE5AFE + +DEVICE_NAME = "safe-mode-loop-runs" +THREAD_LOG_MARKER = "looping component ran in safe mode" + + +@pytest.mark.asyncio +async def test_safe_mode_loop_runs( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, +) -> None: + """When safe mode is active, ``App.loop()`` must still iterate looping + components -- proven here by a thread-logged marker reaching the + console (which requires ``Logger::loop()`` to run).""" + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # Compile finished successfully; pre-populate prefs so the *next* run + # enters safe mode immediately. + write_host_pref( + DEVICE_NAME, SAFE_MODE_RTC_KEY, struct.pack("<I", ENTER_SAFE_MODE_MAGIC) + ) + + try: + loop = asyncio.get_running_loop() + safe_mode_active = loop.create_future() + thread_log_seen = loop.create_future() + safe_mode_pattern = re.compile(r"SAFE MODE IS ACTIVE") + thread_log_pattern = re.compile(re.escape(THREAD_LOG_MARKER)) + + def on_log(line: str) -> None: + if not safe_mode_active.done() and safe_mode_pattern.search(line): + safe_mode_active.set_result(True) + if not thread_log_seen.done() and thread_log_pattern.search(line): + thread_log_seen.set_result(True) + + async with run_binary(binary_path, line_callback=on_log): + try: + await asyncio.wait_for(safe_mode_active, timeout=15.0) + except TimeoutError: + pytest.fail( + "Did not observe 'SAFE MODE IS ACTIVE' -- safe mode " + "didn't trigger, so this test isn't exercising the bug." + ) + try: + await asyncio.wait_for(thread_log_seen, timeout=10.0) + except TimeoutError: + pytest.fail( + f"Did not observe thread-logged marker {THREAD_LOG_MARKER!r} " + "within timeout. Logger::loop() never drained the task " + "log buffer, meaning App.looping_components_ was never " + "sized -- this is the regression #16269 fixed." + ) + finally: + clear_host_prefs(DEVICE_NAME) diff --git a/tests/integration/test_template_text_save.py b/tests/integration/test_template_text_save.py index 47c8e3188ab..7e56209c500 100644 --- a/tests/integration/test_template_text_save.py +++ b/tests/integration/test_template_text_save.py @@ -9,7 +9,6 @@ Tests that: from __future__ import annotations import asyncio -from pathlib import Path import socket from typing import Any @@ -17,9 +16,12 @@ from aioesphomeapi import TextInfo, TextState import pytest from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client +from .host_prefs import clear_host_prefs from .state_utils import InitialStateHelper, require_entity from .types import CompileFunction, ConfigWriter +DEVICE_NAME = "host-template-text-save-test" + @pytest.mark.asyncio async def test_template_text_save( @@ -32,11 +34,7 @@ async def test_template_text_save( port, port_socket = reserved_tcp_port # Clean up any stale preference file from previous runs - prefs_file = ( - Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs" - ) - if prefs_file.exists(): - prefs_file.unlink() + clear_host_prefs(DEVICE_NAME) # Write and compile once config_path = await write_yaml_config(yaml_config) @@ -59,7 +57,7 @@ async def test_template_text_save( wait_and_connect_api_client(port=port) as client, ): device_info = await client.device_info() - assert device_info.name == "host-template-text-save-test" + assert device_info.name == DEVICE_NAME entities, _ = await client.list_entities_services() text_entity = require_entity( @@ -127,5 +125,4 @@ async def test_template_text_save( ) # Clean up preference file - if prefs_file.exists(): - prefs_file.unlink() + clear_host_prefs(DEVICE_NAME) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 6fa8f7ed43a..4ce862315d7 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from esphome import config_validation as cv, core +from esphome.components.safe_mode import to_code as safe_mode_to_code from esphome.const import ( CONF_AREA, CONF_AREAS, @@ -312,6 +313,75 @@ def test_add_platform_defines_priority() -> None: ) +def test_to_code_priority_above_safe_mode() -> None: + """Test that core to_code emits the looping_components_ init before safe_mode. + + Regression test for https://github.com/esphome/esphome/issues/16262. + safe_mode emits an `if (should_enter_safe_mode(...)) return;` line in main() + at APPLICATION priority. The `App.looping_components_.init(...)` call must be + emitted at a higher priority than APPLICATION so it lands in main() before + the early return; otherwise the FixedVector is never sized when safe mode is + active and loop() never runs (Wi-Fi never connects). + """ + assert config.to_code.priority > safe_mode_to_code.priority, ( + f"core to_code priority ({config.to_code.priority}) must be greater than " + f"safe_mode to_code priority ({safe_mode_to_code.priority}) so that " + "App.looping_components_.init() is emitted before safe_mode's early return" + ) + + +@pytest.mark.asyncio +async def test_add_looping_components_handles_empty_entries() -> None: + """Test that _add_looping_components emits a valid constexpr when there are + no looping component entries. + + With zero entries the generated constexpr must still be syntactically valid + C++ (`= 0;`), not an empty expression (`= ;`). This guards the empty-list + case that would otherwise produce uncompilable main.cpp output. + """ + CORE.data["looping_component_entries"] = [] + + await config._add_looping_components() + + constexpr_lines = [ + str(s) + for s in CORE.global_statements + if "ESPHOME_LOOPING_COMPONENT_COUNT" in str(s) + ] + assert len(constexpr_lines) == 1 + text = constexpr_lines[0] + assert "static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT" in text + # The right-hand side must contain a literal `0`, not be empty. + rhs = text.split("=", 1)[1] + assert "0" in rhs + assert rhs.strip().rstrip(";").strip(), ( + f"constexpr right-hand side must not be empty, got: {text!r}" + ) + + +@pytest.mark.asyncio +async def test_add_looping_components_with_entries() -> None: + """Test that _add_looping_components builds a HasLoopOverride sum from entries.""" + CORE.data["looping_component_entries"] = [ + "esphome::wifi::WiFiComponent", + "esphome::logger::Logger", + "esphome::wifi::WiFiComponent", + ] + + await config._add_looping_components() + + constexpr_lines = [ + str(s) + for s in CORE.global_statements + if "ESPHOME_LOOPING_COMPONENT_COUNT" in str(s) + ] + assert len(constexpr_lines) == 1 + text = constexpr_lines[0] + # Deduplicated by type, with per-type counts as multiplier. + assert "(2 * HasLoopOverride<esphome::wifi::WiFiComponent>::value)" in text + assert "(1 * HasLoopOverride<esphome::logger::Logger>::value)" in text + + def test_valid_include_with_angle_brackets() -> None: """Test valid_include accepts angle bracket includes.""" assert valid_include("<ArduinoJson.h>") == "<ArduinoJson.h>" From d9835c8705492923dfaa7bbb0413066b119a101b Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 6 May 2026 19:32:50 +0200 Subject: [PATCH 1885/2056] [nextion] Fix text sensor state not updated on string response (#16280) --- esphome/components/nextion/nextion.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e42f7ca2169..b644cad5077 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -641,6 +641,7 @@ void Nextion::process_nextion_commands_() { } else { ESP_LOGN(TAG, "String resp: '%s' id: %s type: %s", to_process.c_str(), component->get_variable_name().c_str(), component->get_queue_type_string()); + component->set_state_from_string(to_process, true, false); } delete nb; // NOLINT(cppcoreguidelines-owning-memory) From 5283cdec124507957ac6d096d2102e4b86bb465a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 7 May 2026 07:25:35 +1200 Subject: [PATCH 1886/2056] Bump version to 2026.4.5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 2e86f54e961..98237cc228c 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.4.4 +PROJECT_NUMBER = 2026.4.5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index bc31ab36b5e..2513a566352 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.4.4" +__version__ = "2026.4.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 3b6250bceecfa0a35388350f3483d3c2eee698d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 15:23:58 -0500 Subject: [PATCH 1887/2056] Bump CodSpeedHQ/action from 4.15.0 to 4.15.1 (#16281) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87058e4fa5a..f43c46dd006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -415,7 +415,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0 + uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 with: run: ${{ steps.build.outputs.binary }} mode: simulation From 004aa4913189ddc9ce1211d4ca5337df22d5b1fd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 7 May 2026 06:57:53 +1000 Subject: [PATCH 1888/2056] [lvgl] Pass touch point to touch event lambdas (#16272) --- esphome/components/lvgl/defines.py | 8 ++++++++ esphome/components/lvgl/lvgl_esphome.cpp | 16 +++++++++++++++- esphome/components/lvgl/lvgl_esphome.h | 19 +++++++++++++------ esphome/components/lvgl/schemas.py | 13 +++++++++++-- esphome/components/lvgl/trigger.py | 17 +++++++++++------ esphome/components/lvgl/types.py | 2 ++ esphome/components/lvgl/widgets/__init__.py | 2 +- esphome/components/lvgl/widgets/canvas.py | 3 +-- esphome/components/lvgl/widgets/line.py | 4 ---- tests/components/lvgl/lvgl-package.yaml | 12 ++++++++++-- 10 files changed, 72 insertions(+), 24 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index ef29a99ddd4..03bbaf8ddb4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -309,6 +309,14 @@ LV_EVENT_MAP = { "STYLE_CHANGE": "STYLE_CHANGED", "TRIPLE_CLICK": "TRIPLE_CLICKED", } + +LV_PRESS_EVENTS = ("PRESS", "PRESSING", "RELEASE") + + +def is_press_event(event: str) -> bool: + return event.removeprefix("on_").upper() in LV_PRESS_EVENTS + + LV_SCREEN_EVENT_MAP = { "SCREEN_LOAD": "SCREEN_LOADED", "SCREEN_LOAD_START": "SCREEN_LOAD_START", diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index eb85faa16cd..3141c5f93c3 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -890,7 +890,21 @@ lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) { int32_t offset = pos - stop1->frac; return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range); } -#endif +#endif // USE_LVGL_GRADIENT + +lv_point_t LvglComponent::get_touch_relative_to_obj(lv_obj_t *obj) { + auto *indev = lv_indev_get_act(); + if (indev == nullptr) { + return {INT32_MAX, INT32_MAX}; + } + lv_point_t point; + lv_indev_get_point(indev, &point); + lv_area_t coords; + lv_obj_get_coords(obj, &coords); + point.x -= coords.x1; + point.y -= coords.y1; + return point; +} static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) { LV_TRACE_OBJ_CREATE("begin"); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index be1f150affe..32bf3ccac6b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -6,7 +6,7 @@ #endif // USE_BINARY_SENSOR #ifdef USE_IMAGE #include "esphome/components/image/image.h" -#endif // USE_LVGL_IMAGE +#endif // USE_IMAGE #ifdef USE_LVGL_ROTARY_ENCODER #include "esphome/components/rotary_encoder/rotary_encoder.h" #endif // USE_LVGL_ROTARY_ENCODER @@ -32,10 +32,10 @@ #ifdef USE_FONT #include "esphome/components/font/font.h" -#endif // USE_LVGL_FONT +#endif // USE_FONT #ifdef USE_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" -#endif // USE_LVGL_TOUCHSCREEN +#endif // USE_TOUCHSCREEN #if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD) #include "esphome/components/key_provider/key_provider.h" @@ -124,7 +124,8 @@ int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); */ lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos); -#endif +#endif // USE_LVGL_GRADIENT + // Parent class for things that wrap an LVGL object class LvCompound { public: @@ -169,9 +170,9 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> { public: explicit ObjUpdateAction(std::function<void(Ts...)> &&lamb) : lamb_(std::move(lamb)) {} + protected: void play(const Ts &...x) override { this->lamb_(x...); } - protected: std::function<void(Ts...)> lamb_; }; #ifdef USE_LVGL_ANIMIMG @@ -190,6 +191,12 @@ class LvglComponent : public PollingComponent { LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding, bool resume_on_input, bool update_when_display_idle, RotationType rotation_type); static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); + /** + * + * @param obj A widget + * @return The position of the last indev point relative to the widget's origin. + */ + static lv_point_t get_touch_relative_to_obj(lv_obj_t *obj); float get_setup_priority() const override { return setup_priority::PROCESSOR; } void setup() override; @@ -311,9 +318,9 @@ class IdleTrigger : public Trigger<> { template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> { public: explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {} - void play(const Ts &...x) override { this->action_(this->parent_); } protected: + void play(const Ts &...x) override { this->action_(this->parent_); } std::function<void(LvglComponent *)> action_{}; }; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 62117fbd328..a9427a98520 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -31,6 +31,7 @@ from .defines import ( CONF_TIME_FORMAT, LV_GRAD_DIR, get_remapped_uses, + is_press_event, ) from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( @@ -46,6 +47,7 @@ from .types import ( LvType, lv_group_t, lv_obj_t, + lv_point_t, lv_pseudo_button_t, lv_style_t, ) @@ -370,13 +372,20 @@ def automation_schema(typ: LvType): if typ.has_on_value: events = events + (CONF_ON_VALUE,) args = typ.get_arg_type() - args.append(lv_event_t_ptr) + + def get_trigger_args(event): + result = args.copy() + if is_press_event(event): + result.append(lv_point_t) + result.append(lv_event_t_ptr) + return result + return { **{ cv.Optional(event): validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Trigger.template(*args) + Trigger.template(*get_trigger_args(event)) ), } ) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index f825999e8a3..b3d12ed1837 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -24,6 +24,7 @@ from .defines import ( LV_SCREEN_EVENT_MAP, LV_SCREEN_EVENT_TRIGGERS, SWIPE_TRIGGERS, + is_press_event, literal, ) from .lvcode import ( @@ -34,11 +35,10 @@ from .lvcode import ( LvConditional, lv, lv_add, - lv_event_t_ptr, lv_expr, lvgl_static, ) -from .types import LV_EVENT +from .types import LV_EVENT, lv_point_t from .widgets import LvScrActType, get_screen_active, widget_map @@ -133,19 +133,24 @@ def _get_event_literal(trigger: str | MockObj) -> MockObj: return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()]) -async def add_trigger(conf, w, *events, is_selected=None): +async def add_trigger(conf, w, *events: str | MockObj, is_selected=None): is_selected = is_selected or w.is_selected() tid = conf[CONF_TRIGGER_ID] trigger = cg.new_Pvariable(tid) - args = w.get_args() + [(lv_event_t_ptr, "event")] - value = w.get_values() + args = w.get_args() + value: list = w.get_values() + if len(events) == 1 and is_press_event(str(events[0])): + # Make the touch point available for selected events + args.append((lv_point_t, "point")) + value.append(lvgl_static.get_touch_relative_to_obj(w.obj)) + args.extend(EVENT_ARG) await automation.build_automation(trigger, args, conf) async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(is_selected): lv_add(trigger.trigger(*value, literal("event"))) callback = await context.get_lambda() event_literals = [_get_event_literal(event) for event in events] - if isinstance(events[0], str) and events[0] in DISPLAY_TRIGGERS: + if str(events[0]) in DISPLAY_TRIGGERS: assert len(events) == 1 lv.display_add_event_cb( lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 0c8ddfbfbd0..1872ce2d32a 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -70,6 +70,8 @@ lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") RotationType = lvgl_ns.enum("RotationType") +lv_point_t = cg.global_ns.struct("lv_point_t") +lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 0ac4062106d..d35f84c4f26 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -366,7 +366,7 @@ class Widget: def get_args(self): if isinstance(self.type.w_type, LvType): - return self.type.w_type.args + return self.type.w_type.args.copy() return [(lv_obj_t_ptr, "obj")] def get_value(self): diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 1308b82dcd8..4427a3b00eb 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -57,10 +57,9 @@ from ..lv_validation import ( ) from ..lvcode import LocalVariable, lv, lv_assign, lv_expr from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property -from ..types import LvType, ObjUpdateAction +from ..types import LvType, ObjUpdateAction, lv_point_precise_t from . import Widget, WidgetType, get_widgets from .img import CONF_IMAGE -from .line import lv_point_precise_t CONF_CANVAS = "canvas" CONF_BUFFER_ID = "buffer_id" diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index 19f421cbbd8..9d6aa7b4ad6 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -1,4 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_X, CONF_Y @@ -13,9 +12,6 @@ CONF_LINE = "line" CONF_POINTS = "points" CONF_POINT_LIST_ID = "point_list_id" -lv_point_t = cg.global_ns.struct("lv_point_t") -lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") - class LineType(WidgetType): def __init__(self): diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 39d7472054a..4bf5b9d494a 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -649,11 +649,15 @@ lvgl: on_scroll_begin: logger.log: Button clicked on_release: - logger.log: Button clicked + logger.log: + format: Button released at %d/%d + args: [point.x, point.y] on_long_press_repeat: logger.log: Button clicked on_pressing: - logger.log: Button pressing + logger.log: + format: Button pressing at %d/%d + args: [point.x, point.y] on_press_lost: logger.log: Button press lost on_single_click: @@ -925,6 +929,10 @@ lvgl: value: !lambda |- static float yyy = 83.0; return yyy + .8; + on_release: + logger.log: + format: Slider released at %d/%d with value %.0f + args: [point.x, point.y, x] - button: styles: spin_button id: spin_up From 9301f76482de58997712cefbe4391d5b9bca74ac Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 7 May 2026 06:59:22 +1000 Subject: [PATCH 1889/2056] [sensor] Add alternate calibration format for ntc (#15937) --- esphome/components/const/__init__.py | 5 ++- esphome/components/lc709203f/sensor.py | 3 +- esphome/components/ntc/sensor.py | 2 +- esphome/components/sensor/__init__.py | 49 ++++++++++++++++++---- tests/components/template/common-base.yaml | 7 +++- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 846d3fd883b..6f418b48ea0 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -2,11 +2,12 @@ CODEOWNERS = ["@esphome/core"] -CONF_BYTE_ORDER = "byte_order" -CONF_CLIMATE_ID = "climate_id" BYTE_ORDER_LITTLE = "little_endian" BYTE_ORDER_BIG = "big_endian" +CONF_B_CONSTANT = "b_constant" +CONF_BYTE_ORDER = "byte_order" +CONF_CLIMATE_ID = "climate_id" CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" CONF_DATA_BITS = "data_bits" diff --git a/esphome/components/lc709203f/sensor.py b/esphome/components/lc709203f/sensor.py index 75ae7036380..d4e62134257 100644 --- a/esphome/components/lc709203f/sensor.py +++ b/esphome/components/lc709203f/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import i2c, sensor +from esphome.components.const import CONF_B_CONSTANT import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_LEVEL, @@ -22,8 +23,6 @@ DEPENDENCIES = ["i2c"] lc709203f_ns = cg.esphome_ns.namespace("lc709203f") -CONF_B_CONSTANT = "b_constant" - LC709203FBatteryVoltage = lc709203f_ns.enum("LC709203FBatteryVoltage") BATTERY_VOLTAGE_OPTIONS = { "3.7": LC709203FBatteryVoltage.LC709203F_BATTERY_VOLTAGE_3_7, diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index d47052cac68..dd7d1bd35d7 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -2,6 +2,7 @@ from math import log import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import CONF_B_CONSTANT import esphome.config_validation as cv from esphome.const import ( CONF_CALIBRATION, @@ -18,7 +19,6 @@ from esphome.const import ( ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) -CONF_B_CONSTANT = "b_constant" CONF_A = "a" CONF_B = "b" CONF_C = "c" diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ed02cc25438..f076c7f17b0 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -4,6 +4,7 @@ import math from esphome import automation import esphome.codegen as cg from esphome.components import mqtt, web_server, zigbee +from esphome.components.const import CONF_B_CONSTANT import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, @@ -32,6 +33,8 @@ from esphome.const import ( CONF_OPTIMISTIC, CONF_PERIOD, CONF_QUANTILE, + CONF_REFERENCE_RESISTANCE, + CONF_REFERENCE_TEMPERATURE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_STATE_CLASS, @@ -1078,16 +1081,44 @@ def ntc_get_abc(value): return a, b, c +def ntc_calc_b_constant(value): + beta = value[CONF_B_CONSTANT] + t0 = value[CONF_REFERENCE_TEMPERATURE] + ZERO_POINT + r0 = value[CONF_REFERENCE_RESISTANCE] + + a = (1 / t0) - (1 / beta) * math.log(r0) + b = 1 / beta + c = 0 + return a, b, c + + def ntc_process_calibration(value): if isinstance(value, dict): - value = cv.Schema( - { - cv.Required(CONF_A): cv.float_, - cv.Required(CONF_B): cv.float_, - cv.Required(CONF_C): cv.float_, - } - )(value) - a, b, c = ntc_get_abc(value) + if CONF_B_CONSTANT in value: + value = cv.Schema( + { + cv.Required(CONF_B_CONSTANT): cv.All( + cv.float_, cv.Range(min=0, min_included=False) + ), + cv.Required(CONF_REFERENCE_TEMPERATURE): cv.All( + cv.temperature, + cv.Range(min=-ZERO_POINT, min_included=False), + ), + cv.Required(CONF_REFERENCE_RESISTANCE): cv.All( + cv.resistance, cv.Range(min=0, min_included=False) + ), + } + )(value) + a, b, c = ntc_calc_b_constant(value) + else: + value = cv.Schema( + { + cv.Required(CONF_A): cv.float_, + cv.Required(CONF_B): cv.float_, + cv.Required(CONF_C): cv.float_, + } + )(value) + a, b, c = ntc_get_abc(value) elif isinstance(value, list): if len(value) != 3: raise cv.Invalid( @@ -1097,7 +1128,7 @@ def ntc_process_calibration(value): a, b, c = ntc_calc_steinhart_hart(value) else: raise cv.Invalid( - f"Calibration parameter accepts either a list for steinhart-hart calibration, or mapping for b-constant calibration, not {type(value)}" + f"Calibration parameter accepts either a list for steinhart-hart calibration, or mapping for b-constant or precomputed (a, b, c) calibration, not {type(value)}" ) _LOGGER.info("Coefficient: a:%s, b:%s, c:%s", a, b, c) return { diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b97cafd25c3..d3985a848bf 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -202,6 +202,11 @@ sensor: value: last - timeout: timeout: 1d + - to_ntc_temperature: + calibration: + b_constant: 3950 + reference_temperature: 25.0°C + reference_resistance: 10kOhm - to_ntc_resistance: calibration: - 10.0kOhm -> 25°C @@ -270,8 +275,6 @@ cover: stop_action: - logger.log: stop_action optimistic: true - on_open: - - logger.log: "Cover on_open (deprecated)" on_opened: - logger.log: "Cover fully opened" on_closed: From 71193e2b2cbfffc46f925263eb8b80c61ef875a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 7 May 2026 13:21:35 -0500 Subject: [PATCH 1890/2056] [helpers] Document write_file's external consumer contract (esphome-device-builder) (#16290) --- esphome/helpers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/helpers.py b/esphome/helpers.py index bb1984e17cb..9d341af1461 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -465,6 +465,12 @@ def _write_file( def write_file(path: Path, text: str | bytes, private: bool = False) -> None: + """Atomically write text or bytes to path. Wraps OSError as EsphomeError. + + Used by esphome-device-builder for in-place YAML rewrites; the + atomicity (sibling tempfile + shutil.move) and EsphomeError + wrapping are part of the public contract. + """ try: _write_file(path, text, private=private) except OSError as err: From 06bd92c3880c575a366f7b8e6fb77f74fd200937 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 14:21:39 -0400 Subject: [PATCH 1891/2056] [clang-tidy] Concatenate nested namespaces (1/7: components a-c) (#16294) --- esphome/components/a01nyub/a01nyub.cpp | 6 ++---- esphome/components/a01nyub/a01nyub.h | 6 ++---- esphome/components/a02yyuw/a02yyuw.cpp | 6 ++---- esphome/components/a02yyuw/a02yyuw.h | 6 ++---- esphome/components/a4988/a4988.cpp | 6 ++---- esphome/components/a4988/a4988.h | 6 ++---- esphome/components/adalight/adalight_light_effect.cpp | 6 ++---- esphome/components/adalight/adalight_light_effect.h | 6 ++---- esphome/components/adc/adc_sensor.h | 6 ++---- esphome/components/adc/adc_sensor_common.cpp | 6 ++---- esphome/components/adc/adc_sensor_esp32.cpp | 6 ++---- esphome/components/adc128s102/adc128s102.cpp | 6 ++---- esphome/components/adc128s102/adc128s102.h | 6 ++---- .../components/adc128s102/sensor/adc128s102_sensor.cpp | 6 ++---- esphome/components/adc128s102/sensor/adc128s102_sensor.h | 6 ++---- .../addressable_light/addressable_light_display.cpp | 6 ++---- .../addressable_light/addressable_light_display.h | 6 ++---- esphome/components/ade7880/ade7880.cpp | 6 ++---- esphome/components/ade7880/ade7880.h | 6 ++---- esphome/components/ade7880/ade7880_i2c.cpp | 6 ++---- esphome/components/ade7880/ade7880_registers.h | 6 ++---- esphome/components/ade7953_base/ade7953_base.cpp | 6 ++---- esphome/components/ade7953_base/ade7953_base.h | 6 ++---- esphome/components/ade7953_i2c/ade7953_i2c.cpp | 6 ++---- esphome/components/ade7953_i2c/ade7953_i2c.h | 6 ++---- esphome/components/ade7953_spi/ade7953_spi.cpp | 6 ++---- esphome/components/ade7953_spi/ade7953_spi.h | 6 ++---- esphome/components/ads1115/ads1115.cpp | 6 ++---- esphome/components/ads1115/ads1115.h | 6 ++---- esphome/components/ads1115/sensor/ads1115_sensor.cpp | 6 ++---- esphome/components/ads1115/sensor/ads1115_sensor.h | 6 ++---- esphome/components/ads1118/ads1118.cpp | 6 ++---- esphome/components/ads1118/ads1118.h | 6 ++---- esphome/components/ads1118/sensor/ads1118_sensor.cpp | 6 ++---- esphome/components/ads1118/sensor/ads1118_sensor.h | 6 ++---- esphome/components/ags10/ags10.cpp | 6 ++---- esphome/components/ags10/ags10.h | 6 ++---- esphome/components/aht10/aht10.cpp | 6 ++---- esphome/components/aht10/aht10.h | 6 ++---- esphome/components/aic3204/aic3204.cpp | 6 ++---- esphome/components/aic3204/aic3204.h | 6 ++---- esphome/components/aic3204/automation.h | 6 ++---- esphome/components/airthings_ble/airthings_listener.cpp | 6 ++---- esphome/components/airthings_ble/airthings_listener.h | 6 ++---- .../airthings_wave_base/airthings_wave_base.cpp | 6 ++---- .../components/airthings_wave_base/airthings_wave_base.h | 6 ++---- .../airthings_wave_mini/airthings_wave_mini.cpp | 6 ++---- .../components/airthings_wave_mini/airthings_wave_mini.h | 6 ++---- .../airthings_wave_plus/airthings_wave_plus.cpp | 6 ++---- .../components/airthings_wave_plus/airthings_wave_plus.h | 6 ++---- esphome/components/alpha3/alpha3.cpp | 6 ++---- esphome/components/alpha3/alpha3.h | 6 ++---- esphome/components/am2315c/am2315c.cpp | 6 ++---- esphome/components/am2315c/am2315c.h | 6 ++---- esphome/components/am2320/am2320.cpp | 6 ++---- esphome/components/am2320/am2320.h | 6 ++---- esphome/components/am43/am43_base.cpp | 6 ++---- esphome/components/am43/am43_base.h | 6 ++---- esphome/components/am43/cover/am43_cover.cpp | 6 ++---- esphome/components/am43/cover/am43_cover.h | 6 ++---- esphome/components/am43/sensor/am43_sensor.cpp | 6 ++---- esphome/components/am43/sensor/am43_sensor.h | 6 ++---- .../analog_threshold/analog_threshold_binary_sensor.cpp | 6 ++---- .../analog_threshold/analog_threshold_binary_sensor.h | 6 ++---- esphome/components/animation/animation.cpp | 6 ++---- esphome/components/animation/animation.h | 6 ++---- esphome/components/anova/anova.cpp | 6 ++---- esphome/components/anova/anova.h | 6 ++---- esphome/components/anova/anova_base.cpp | 6 ++---- esphome/components/anova/anova_base.h | 6 ++---- esphome/components/apds9306/apds9306.cpp | 6 ++---- esphome/components/apds9306/apds9306.h | 6 ++---- esphome/components/apds9960/apds9960.cpp | 6 ++---- esphome/components/apds9960/apds9960.h | 6 ++---- esphome/components/as3935/as3935.cpp | 6 ++---- esphome/components/as3935/as3935.h | 6 ++---- esphome/components/as3935_i2c/as3935_i2c.cpp | 6 ++---- esphome/components/as3935_i2c/as3935_i2c.h | 6 ++---- esphome/components/as3935_spi/as3935_spi.cpp | 6 ++---- esphome/components/as3935_spi/as3935_spi.h | 6 ++---- esphome/components/as5600/as5600.cpp | 6 ++---- esphome/components/as5600/as5600.h | 6 ++---- esphome/components/as5600/sensor/as5600_sensor.cpp | 6 ++---- esphome/components/as5600/sensor/as5600_sensor.h | 6 ++---- esphome/components/as7341/as7341.cpp | 6 ++---- esphome/components/as7341/as7341.h | 6 ++---- esphome/components/at581x/at581x.cpp | 6 ++---- esphome/components/at581x/at581x.h | 6 ++---- esphome/components/at581x/automation.h | 6 ++---- esphome/components/at581x/switch/rf_switch.cpp | 6 ++---- esphome/components/at581x/switch/rf_switch.h | 6 ++---- .../components/atc_mithermometer/atc_mithermometer.cpp | 6 ++---- esphome/components/atc_mithermometer/atc_mithermometer.h | 6 ++---- esphome/components/atm90e26/atm90e26.cpp | 6 ++---- esphome/components/atm90e26/atm90e26.h | 6 ++---- esphome/components/atm90e26/atm90e26_reg.h | 6 ++---- esphome/components/atm90e32/atm90e32.cpp | 6 ++---- esphome/components/atm90e32/atm90e32.h | 6 ++---- esphome/components/atm90e32/atm90e32_reg.h | 6 ++---- esphome/components/atm90e32/button/atm90e32_button.cpp | 6 ++---- esphome/components/atm90e32/button/atm90e32_button.h | 6 ++---- esphome/components/atm90e32/number/atm90e32_number.h | 6 ++---- esphome/components/audio/audio.cpp | 6 ++---- esphome/components/audio/audio.h | 6 ++---- esphome/components/audio/audio_decoder.cpp | 6 ++---- esphome/components/audio/audio_decoder.h | 6 ++---- esphome/components/audio/audio_reader.cpp | 6 ++---- esphome/components/audio/audio_reader.h | 6 ++---- esphome/components/audio/audio_resampler.cpp | 6 ++---- esphome/components/audio/audio_resampler.h | 6 ++---- esphome/components/audio/audio_transfer_buffer.cpp | 6 ++---- esphome/components/audio/audio_transfer_buffer.h | 6 ++---- esphome/components/audio_adc/audio_adc.h | 6 ++---- esphome/components/audio_adc/automation.h | 6 ++---- esphome/components/audio_dac/audio_dac.h | 6 ++---- esphome/components/audio_dac/automation.h | 6 ++---- .../axs15231/touchscreen/axs15231_touchscreen.cpp | 6 ++---- .../axs15231/touchscreen/axs15231_touchscreen.h | 6 ++---- esphome/components/b_parasite/b_parasite.cpp | 6 ++---- esphome/components/b_parasite/b_parasite.h | 6 ++---- esphome/components/ballu/ballu.cpp | 6 ++---- esphome/components/ballu/ballu.h | 6 ++---- esphome/components/bang_bang/bang_bang_climate.cpp | 6 ++---- esphome/components/bang_bang/bang_bang_climate.h | 6 ++---- esphome/components/bedjet/bedjet_child.h | 6 ++---- esphome/components/bedjet/bedjet_codec.cpp | 6 ++---- esphome/components/bedjet/bedjet_codec.h | 6 ++---- esphome/components/bedjet/bedjet_const.h | 6 ++---- esphome/components/bedjet/bedjet_hub.cpp | 6 ++---- esphome/components/bedjet/bedjet_hub.h | 6 ++---- esphome/components/bedjet/climate/bedjet_climate.cpp | 6 ++---- esphome/components/bedjet/climate/bedjet_climate.h | 6 ++---- esphome/components/bedjet/fan/bedjet_fan.cpp | 6 ++---- esphome/components/bedjet/fan/bedjet_fan.h | 6 ++---- esphome/components/bedjet/sensor/bedjet_sensor.cpp | 6 ++---- esphome/components/bedjet/sensor/bedjet_sensor.h | 6 ++---- esphome/components/bh1900nux/bh1900nux.cpp | 6 ++---- esphome/components/bh1900nux/bh1900nux.h | 6 ++---- esphome/components/binary/fan/binary_fan.cpp | 6 ++---- esphome/components/binary/fan/binary_fan.h | 6 ++---- esphome/components/binary/light/binary_light_output.h | 6 ++---- .../components/binary_sensor_map/binary_sensor_map.cpp | 6 ++---- esphome/components/binary_sensor_map/binary_sensor_map.h | 6 ++---- esphome/components/bl0906/bl0906.cpp | 6 ++---- esphome/components/bl0906/bl0906.h | 6 ++---- esphome/components/bl0906/constants.h | 6 ++---- esphome/components/bl0939/bl0939.cpp | 6 ++---- esphome/components/bl0939/bl0939.h | 6 ++---- esphome/components/bl0940/bl0940.cpp | 6 ++---- esphome/components/bl0940/bl0940.h | 6 ++---- .../components/bl0940/button/calibration_reset_button.cpp | 6 ++---- .../components/bl0940/button/calibration_reset_button.h | 6 ++---- esphome/components/bl0940/number/calibration_number.cpp | 6 ++---- esphome/components/bl0940/number/calibration_number.h | 6 ++---- esphome/components/bl0942/bl0942.cpp | 6 ++---- esphome/components/bl0942/bl0942.h | 6 ++---- esphome/components/ble_presence/ble_presence_device.cpp | 6 ++---- esphome/components/ble_presence/ble_presence_device.h | 6 ++---- esphome/components/ble_rssi/ble_rssi_sensor.cpp | 6 ++---- esphome/components/ble_rssi/ble_rssi_sensor.h | 6 ++---- esphome/components/ble_scanner/ble_scanner.cpp | 6 ++---- esphome/components/ble_scanner/ble_scanner.h | 6 ++---- esphome/components/bme280_base/bme280_base.cpp | 6 ++---- esphome/components/bme280_base/bme280_base.h | 6 ++---- esphome/components/bme280_i2c/bme280_i2c.cpp | 6 ++---- esphome/components/bme280_i2c/bme280_i2c.h | 6 ++---- esphome/components/bme280_spi/bme280_spi.cpp | 6 ++---- esphome/components/bme280_spi/bme280_spi.h | 6 ++---- esphome/components/bme680/bme680.cpp | 6 ++---- esphome/components/bme680/bme680.h | 6 ++---- esphome/components/bme680_bsec/bme680_bsec.cpp | 6 ++---- esphome/components/bme680_bsec/bme680_bsec.h | 6 ++---- esphome/components/bmi160/bmi160.cpp | 6 ++---- esphome/components/bmi160/bmi160.h | 6 ++---- esphome/components/bmp085/bmp085.cpp | 6 ++---- esphome/components/bmp085/bmp085.h | 6 ++---- esphome/components/bmp280_base/bmp280_base.cpp | 6 ++---- esphome/components/bmp280_base/bmp280_base.h | 6 ++---- esphome/components/bmp280_i2c/bmp280_i2c.cpp | 6 ++---- esphome/components/bmp280_i2c/bmp280_i2c.h | 6 ++---- esphome/components/bmp280_spi/bmp280_spi.cpp | 6 ++---- esphome/components/bmp280_spi/bmp280_spi.h | 6 ++---- esphome/components/bmp3xx_base/bmp3xx_base.cpp | 6 ++---- esphome/components/bmp3xx_base/bmp3xx_base.h | 6 ++---- esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp | 6 ++---- esphome/components/bmp3xx_i2c/bmp3xx_i2c.h | 6 ++---- esphome/components/bmp3xx_spi/bmp3xx_spi.cpp | 6 ++---- esphome/components/bmp3xx_spi/bmp3xx_spi.h | 6 ++---- esphome/components/bp1658cj/bp1658cj.cpp | 6 ++---- esphome/components/bp1658cj/bp1658cj.h | 6 ++---- esphome/components/bp5758d/bp5758d.cpp | 6 ++---- esphome/components/bp5758d/bp5758d.h | 6 ++---- esphome/components/bthome_mithermometer/bthome_ble.cpp | 6 ++---- esphome/components/bthome_mithermometer/bthome_ble.h | 6 ++---- esphome/components/bytebuffer/bytebuffer.h | 6 ++---- esphome/components/camera/camera.cpp | 6 ++---- esphome/components/camera/camera.h | 6 ++---- esphome/components/canbus/canbus.cpp | 6 ++---- esphome/components/canbus/canbus.h | 6 ++---- esphome/components/cap1188/cap1188.cpp | 6 ++---- esphome/components/cap1188/cap1188.h | 6 ++---- esphome/components/captive_portal/captive_portal.cpp | 7 +++---- esphome/components/captive_portal/captive_portal.h | 8 +++----- esphome/components/ccs811/ccs811.cpp | 6 ++---- esphome/components/ccs811/ccs811.h | 6 ++---- esphome/components/cd74hc4067/cd74hc4067.cpp | 6 ++---- esphome/components/cd74hc4067/cd74hc4067.h | 6 ++---- esphome/components/ch422g/ch422g.cpp | 6 ++---- esphome/components/ch422g/ch422g.h | 6 ++---- esphome/components/chsc6x/chsc6x_touchscreen.cpp | 6 ++---- esphome/components/chsc6x/chsc6x_touchscreen.h | 6 ++---- esphome/components/climate_ir/climate_ir.cpp | 6 ++---- esphome/components/climate_ir/climate_ir.h | 6 ++---- esphome/components/climate_ir_lg/climate_ir_lg.cpp | 6 ++---- esphome/components/climate_ir_lg/climate_ir_lg.h | 6 ++---- esphome/components/cm1106/cm1106.cpp | 6 ++---- esphome/components/cm1106/cm1106.h | 6 ++---- esphome/components/color_temperature/ct_light_output.h | 6 ++---- esphome/components/combination/combination.cpp | 6 ++---- esphome/components/combination/combination.h | 6 ++---- esphome/components/coolix/coolix.cpp | 6 ++---- esphome/components/coolix/coolix.h | 6 ++---- .../components/copy/binary_sensor/copy_binary_sensor.cpp | 6 ++---- .../components/copy/binary_sensor/copy_binary_sensor.h | 6 ++---- esphome/components/copy/button/copy_button.cpp | 6 ++---- esphome/components/copy/button/copy_button.h | 6 ++---- esphome/components/copy/cover/copy_cover.cpp | 6 ++---- esphome/components/copy/cover/copy_cover.h | 6 ++---- esphome/components/copy/fan/copy_fan.cpp | 6 ++---- esphome/components/copy/fan/copy_fan.h | 6 ++---- esphome/components/copy/lock/copy_lock.cpp | 6 ++---- esphome/components/copy/lock/copy_lock.h | 6 ++---- esphome/components/copy/number/copy_number.cpp | 6 ++---- esphome/components/copy/number/copy_number.h | 6 ++---- esphome/components/copy/select/copy_select.cpp | 6 ++---- esphome/components/copy/select/copy_select.h | 6 ++---- esphome/components/copy/sensor/copy_sensor.cpp | 6 ++---- esphome/components/copy/sensor/copy_sensor.h | 6 ++---- esphome/components/copy/switch/copy_switch.cpp | 6 ++---- esphome/components/copy/switch/copy_switch.h | 6 ++---- esphome/components/copy/text/copy_text.cpp | 6 ++---- esphome/components/copy/text/copy_text.h | 6 ++---- esphome/components/copy/text_sensor/copy_text_sensor.cpp | 6 ++---- esphome/components/copy/text_sensor/copy_text_sensor.h | 6 ++---- esphome/components/cs5460a/cs5460a.cpp | 6 ++---- esphome/components/cs5460a/cs5460a.h | 6 ++---- esphome/components/cse7761/cse7761.cpp | 6 ++---- esphome/components/cse7761/cse7761.h | 6 ++---- esphome/components/cst226/binary_sensor/cs226_button.h | 6 ++---- esphome/components/cst226/binary_sensor/cstt6_button.cpp | 6 ++---- .../components/cst226/touchscreen/cst226_touchscreen.cpp | 6 ++---- .../components/cst226/touchscreen/cst226_touchscreen.h | 6 ++---- .../components/cst816/touchscreen/cst816_touchscreen.cpp | 6 ++---- .../components/cst816/touchscreen/cst816_touchscreen.h | 6 ++---- esphome/components/ct_clamp/ct_clamp_sensor.cpp | 6 ++---- esphome/components/ct_clamp/ct_clamp_sensor.h | 6 ++---- esphome/components/current_based/current_based_cover.cpp | 6 ++---- esphome/components/current_based/current_based_cover.h | 6 ++---- esphome/components/cwww/cwww_light_output.h | 6 ++---- 259 files changed, 520 insertions(+), 1037 deletions(-) diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index 210c3557b3e..344456854bf 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace a01nyub { +namespace esphome::a01nyub { static const char *const TAG = "a01nyub.sensor"; @@ -42,5 +41,4 @@ void A01nyubComponent::check_buffer_() { void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); } -} // namespace a01nyub -} // namespace esphome +} // namespace esphome::a01nyub diff --git a/esphome/components/a01nyub/a01nyub.h b/esphome/components/a01nyub/a01nyub.h index 6b22e9bcadb..5c0d20bd378 100644 --- a/esphome/components/a01nyub/a01nyub.h +++ b/esphome/components/a01nyub/a01nyub.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace a01nyub { +namespace esphome::a01nyub { class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice { public: @@ -23,5 +22,4 @@ class A01nyubComponent : public sensor::Sensor, public Component, public uart::U std::vector<uint8_t> buffer_; }; -} // namespace a01nyub -} // namespace esphome +} // namespace esphome::a01nyub diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp index a2aad0cef13..2832334ef19 100644 --- a/esphome/components/a02yyuw/a02yyuw.cpp +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace a02yyuw { +namespace esphome::a02yyuw { static const char *const TAG = "a02yyuw.sensor"; @@ -41,5 +40,4 @@ void A02yyuwComponent::check_buffer_() { void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } -} // namespace a02yyuw -} // namespace esphome +} // namespace esphome::a02yyuw diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h index 6ff370fdc34..693bcfd03c6 100644 --- a/esphome/components/a02yyuw/a02yyuw.h +++ b/esphome/components/a02yyuw/a02yyuw.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace a02yyuw { +namespace esphome::a02yyuw { class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { public: @@ -23,5 +22,4 @@ class A02yyuwComponent : public sensor::Sensor, public Component, public uart::U std::vector<uint8_t> buffer_; }; -} // namespace a02yyuw -} // namespace esphome +} // namespace esphome::a02yyuw diff --git a/esphome/components/a4988/a4988.cpp b/esphome/components/a4988/a4988.cpp index b9efb4ea448..d8fc6752f37 100644 --- a/esphome/components/a4988/a4988.cpp +++ b/esphome/components/a4988/a4988.cpp @@ -1,8 +1,7 @@ #include "a4988.h" #include "esphome/core/log.h" -namespace esphome { -namespace a4988 { +namespace esphome::a4988 { static const char *const TAG = "a4988.stepper"; @@ -51,5 +50,4 @@ void A4988::loop() { this->step_pin_->digital_write(false); } -} // namespace a4988 -} // namespace esphome +} // namespace esphome::a4988 diff --git a/esphome/components/a4988/a4988.h b/esphome/components/a4988/a4988.h index 0fe7891110c..04040241c0f 100644 --- a/esphome/components/a4988/a4988.h +++ b/esphome/components/a4988/a4988.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/stepper/stepper.h" -namespace esphome { -namespace a4988 { +namespace esphome::a4988 { class A4988 : public stepper::Stepper, public Component { public: @@ -25,5 +24,4 @@ class A4988 : public stepper::Stepper, public Component { HighFrequencyLoopRequester high_freq_; }; -} // namespace a4988 -} // namespace esphome +} // namespace esphome::a4988 diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp index 06d7e0e8970..bf6849acaf7 100644 --- a/esphome/components/adalight/adalight_light_effect.cpp +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -1,8 +1,7 @@ #include "adalight_light_effect.h" #include "esphome/core/log.h" -namespace esphome { -namespace adalight { +namespace esphome::adalight { static const char *const TAG = "adalight_light_effect"; @@ -138,5 +137,4 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL return CONSUMED; } -} // namespace adalight -} // namespace esphome +} // namespace esphome::adalight diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h index bb7319c99c4..c30e8467786 100644 --- a/esphome/components/adalight/adalight_light_effect.h +++ b/esphome/components/adalight/adalight_light_effect.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace adalight { +namespace esphome::adalight { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { public: @@ -35,5 +34,4 @@ class AdalightLightEffect : public light::AddressableLightEffect, public uart::U std::vector<uint8_t> frame_; }; -} // namespace adalight -} // namespace esphome +} // namespace esphome::adalight diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index cf48ccd9c3d..676940eca12 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -17,8 +17,7 @@ #include <zephyr/drivers/adc.h> #endif -namespace esphome { -namespace adc { +namespace esphome::adc { #ifdef USE_ESP32 // clang-format off @@ -162,5 +161,4 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif }; -} // namespace adc -} // namespace esphome +} // namespace esphome::adc diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index c779fd5893b..16c86aee18a 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -1,8 +1,7 @@ #include "adc_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.common"; @@ -79,5 +78,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index fc707013a89..a761b377490 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.esp32"; @@ -364,7 +363,6 @@ float ADCSensor::sample_autorange_() { return final_result; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc #endif // USE_ESP32 diff --git a/esphome/components/adc128s102/adc128s102.cpp b/esphome/components/adc128s102/adc128s102.cpp index 935dbde8eac..ef0db4730a1 100644 --- a/esphome/components/adc128s102/adc128s102.cpp +++ b/esphome/components/adc128s102/adc128s102.cpp @@ -1,8 +1,7 @@ #include "adc128s102.h" #include "esphome/core/log.h" -namespace esphome { -namespace adc128s102 { +namespace esphome::adc128s102 { static const char *const TAG = "adc128s102"; @@ -28,5 +27,4 @@ uint16_t ADC128S102::read_data(uint8_t channel) { return digital_value; } -} // namespace adc128s102 -} // namespace esphome +} // namespace esphome::adc128s102 diff --git a/esphome/components/adc128s102/adc128s102.h b/esphome/components/adc128s102/adc128s102.h index bd6b7f7af17..f04ed87b2af 100644 --- a/esphome/components/adc128s102/adc128s102.h +++ b/esphome/components/adc128s102/adc128s102.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace adc128s102 { +namespace esphome::adc128s102 { class ADC128S102 : public Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -19,5 +18,4 @@ class ADC128S102 : public Component, uint16_t read_data(uint8_t channel); }; -} // namespace adc128s102 -} // namespace esphome +} // namespace esphome::adc128s102 diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp index 800b2d52610..01b6a3f2b83 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace adc128s102 { +namespace esphome::adc128s102 { static const char *const TAG = "adc128s102.sensor"; @@ -18,5 +17,4 @@ void ADC128S102Sensor::dump_config() { float ADC128S102Sensor::sample() { return this->parent_->read_data(this->channel_); } void ADC128S102Sensor::update() { this->publish_state(this->sample()); } -} // namespace adc128s102 -} // namespace esphome +} // namespace esphome::adc128s102 diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h index 5e6fc74e9c5..c840102380f 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -7,8 +7,7 @@ #include "../adc128s102.h" -namespace esphome { -namespace adc128s102 { +namespace esphome::adc128s102 { class ADC128S102Sensor : public PollingComponent, public Parented<ADC128S102>, @@ -24,5 +23,4 @@ class ADC128S102Sensor : public PollingComponent, protected: uint8_t channel_; }; -} // namespace adc128s102 -} // namespace esphome +} // namespace esphome::adc128s102 diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp index 329620bcf04..4cbcb3324be 100644 --- a/esphome/components/addressable_light/addressable_light_display.cpp +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -1,8 +1,7 @@ #include "addressable_light_display.h" #include "esphome/core/log.h" -namespace esphome { -namespace addressable_light { +namespace esphome::addressable_light { static const char *const TAG = "addressable_light.display"; @@ -66,5 +65,4 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; } } -} // namespace addressable_light -} // namespace esphome +} // namespace esphome::addressable_light diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index d9b8680547f..917d334f05f 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace addressable_light { +namespace esphome::addressable_light { class AddressableLightDisplay : public display::DisplayBuffer { public: @@ -61,5 +60,4 @@ class AddressableLightDisplay : public display::DisplayBuffer { optional<uint32_t> last_effect_index_; optional<std::function<int(int, int)>> pixel_mapper_f_; }; -} // namespace addressable_light -} // namespace esphome +} // namespace esphome::addressable_light diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index 8fb3e55b91a..9d19770c57c 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -13,8 +13,7 @@ #include <cinttypes> -namespace esphome { -namespace ade7880 { +namespace esphome::ade7880 { static const char *const TAG = "ade7880"; @@ -313,5 +312,4 @@ void ADE7880::reset_device_() { this->store_.reset_pending = true; } -} // namespace ade7880 -} // namespace esphome +} // namespace esphome::ade7880 diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h index 40bc22e54a1..69c8e5abba2 100644 --- a/esphome/components/ade7880/ade7880.h +++ b/esphome/components/ade7880/ade7880.h @@ -16,8 +16,7 @@ #include "ade7880_registers.h" -namespace esphome { -namespace ade7880 { +namespace esphome::ade7880 { struct NeutralChannel { void set_current(sensor::Sensor *sens) { this->current = sens; } @@ -125,5 +124,4 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent { void write_u32_register16_(uint16_t a_register, uint32_t value); }; -} // namespace ade7880 -} // namespace esphome +} // namespace esphome::ade7880 diff --git a/esphome/components/ade7880/ade7880_i2c.cpp b/esphome/components/ade7880/ade7880_i2c.cpp index fae20f175d6..294fd430d3e 100644 --- a/esphome/components/ade7880/ade7880_i2c.cpp +++ b/esphome/components/ade7880/ade7880_i2c.cpp @@ -9,8 +9,7 @@ #include "ade7880.h" -namespace esphome { -namespace ade7880 { +namespace esphome::ade7880 { // adapted from https://stackoverflow.com/a/55912127/1886371 template<size_t Bits, typename T> inline T sign_extend(const T &v) noexcept { @@ -97,5 +96,4 @@ void ADE7880::write_u32_register16_(uint16_t a_register, uint32_t value) { this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out)); } -} // namespace ade7880 -} // namespace esphome +} // namespace esphome::ade7880 diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 9fd8ca3bf5e..aee4e424455 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -4,8 +4,7 @@ // Source: https://www.analog.com/media/en/technical-documentation/application-notes/AN-1127.pdf -namespace esphome { -namespace ade7880 { +namespace esphome::ade7880 { // DSP Data Memory RAM registers constexpr uint16_t AIGAIN = 0x4380; @@ -242,5 +241,4 @@ constexpr uint8_t DSPWP_SET_RO = (1 << 7); // DSPWP_SEL Register Bits constexpr uint8_t DSPWP_SEL_SET = 0xAD; -} // namespace ade7880 -} // namespace esphome +} // namespace esphome::ade7880 diff --git a/esphome/components/ade7953_base/ade7953_base.cpp b/esphome/components/ade7953_base/ade7953_base.cpp index 2dfab8ff853..1adf44f8f72 100644 --- a/esphome/components/ade7953_base/ade7953_base.cpp +++ b/esphome/components/ade7953_base/ade7953_base.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace ade7953_base { +namespace esphome::ade7953_base { static const char *const TAG = "ade7953"; @@ -160,5 +159,4 @@ void ADE7953::update() { ADE_PUBLISH(frequency, 223750.0f, 1 + val_16); } -} // namespace ade7953_base -} // namespace esphome +} // namespace esphome::ade7953_base diff --git a/esphome/components/ade7953_base/ade7953_base.h b/esphome/components/ade7953_base/ade7953_base.h index b58f95b2304..a1dfea23b04 100644 --- a/esphome/components/ade7953_base/ade7953_base.h +++ b/esphome/components/ade7953_base/ade7953_base.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace ade7953_base { +namespace esphome::ade7953_base { static constexpr uint8_t PGA_V_8 = 0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0]) @@ -131,5 +130,4 @@ class ADE7953 : public PollingComponent, public sensor::Sensor { virtual bool ade_read_32(uint16_t reg, uint32_t *value) = 0; }; -} // namespace ade7953_base -} // namespace esphome +} // namespace esphome::ade7953_base diff --git a/esphome/components/ade7953_i2c/ade7953_i2c.cpp b/esphome/components/ade7953_i2c/ade7953_i2c.cpp index 59c2254d444..252e55ee5c1 100644 --- a/esphome/components/ade7953_i2c/ade7953_i2c.cpp +++ b/esphome/components/ade7953_i2c/ade7953_i2c.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ade7953_i2c { +namespace esphome::ade7953_i2c { static const char *const TAG = "ade7953"; @@ -76,5 +75,4 @@ bool AdE7953I2c::ade_read_32(uint16_t reg, uint32_t *value) { return false; } -} // namespace ade7953_i2c -} // namespace esphome +} // namespace esphome::ade7953_i2c diff --git a/esphome/components/ade7953_i2c/ade7953_i2c.h b/esphome/components/ade7953_i2c/ade7953_i2c.h index 65dc30dddb7..74d7e3e7cce 100644 --- a/esphome/components/ade7953_i2c/ade7953_i2c.h +++ b/esphome/components/ade7953_i2c/ade7953_i2c.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace ade7953_i2c { +namespace esphome::ade7953_i2c { class AdE7953I2c : public ade7953_base::ADE7953, public i2c::I2CDevice { public: @@ -24,5 +23,4 @@ class AdE7953I2c : public ade7953_base::ADE7953, public i2c::I2CDevice { bool ade_read_32(uint16_t reg, uint32_t *value) override; }; -} // namespace ade7953_i2c -} // namespace esphome +} // namespace esphome::ade7953_i2c diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp index a69b7d19fb5..c2d85231d6b 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.cpp +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ade7953_spi { +namespace esphome::ade7953_spi { static const char *const TAG = "ade7953"; @@ -83,5 +82,4 @@ bool AdE7953Spi::ade_read_32(uint16_t reg, uint32_t *value) { return false; } -} // namespace ade7953_spi -} // namespace esphome +} // namespace esphome::ade7953_spi diff --git a/esphome/components/ade7953_spi/ade7953_spi.h b/esphome/components/ade7953_spi/ade7953_spi.h index 27f6025d983..657397db4e6 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.h +++ b/esphome/components/ade7953_spi/ade7953_spi.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace ade7953_spi { +namespace esphome::ade7953_spi { class AdE7953Spi : public ade7953_base::ADE7953, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -28,5 +27,4 @@ class AdE7953Spi : public ade7953_base::ADE7953, bool ade_read_32(uint16_t reg, uint32_t *value) override; }; -} // namespace ade7953_spi -} // namespace esphome +} // namespace esphome::ade7953_spi diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index d493a6a6d3c..58704bf7c68 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace ads1115 { +namespace esphome::ads1115 { static const char *const TAG = "ads1115"; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; @@ -208,5 +207,4 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 return millivolts / 1e3f; } -} // namespace ads1115 -} // namespace esphome +} // namespace esphome::ads1115 diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index e827a739d2a..b1eed68aff2 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace ads1115 { +namespace esphome::ads1115 { enum ADS1115Multiplexer { ADS1115_MULTIPLEXER_P0_N1 = 0b000, @@ -60,5 +59,4 @@ class ADS1115Component : public Component, public i2c::I2CDevice { bool continuous_mode_; }; -} // namespace ads1115 -} // namespace esphome +} // namespace esphome::ads1115 diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.cpp b/esphome/components/ads1115/sensor/ads1115_sensor.cpp index fac6b60d0ae..8086d972310 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.cpp +++ b/esphome/components/ads1115/sensor/ads1115_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace ads1115 { +namespace esphome::ads1115 { static const char *const TAG = "ads1115.sensor"; @@ -29,5 +28,4 @@ void ADS1115Sensor::dump_config() { this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); } -} // namespace ads1115 -} // namespace esphome +} // namespace esphome::ads1115 diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.h b/esphome/components/ads1115/sensor/ads1115_sensor.h index 5ca25c13add..3b82c153dd5 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.h +++ b/esphome/components/ads1115/sensor/ads1115_sensor.h @@ -8,8 +8,7 @@ #include "../ads1115.h" -namespace esphome { -namespace ads1115 { +namespace esphome::ads1115 { /// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. class ADS1115Sensor : public sensor::Sensor, @@ -33,5 +32,4 @@ class ADS1115Sensor : public sensor::Sensor, ADS1115Samplerate samplerate_; }; -} // namespace ads1115 -} // namespace esphome +} // namespace esphome::ads1115 diff --git a/esphome/components/ads1118/ads1118.cpp b/esphome/components/ads1118/ads1118.cpp index f7db9f93dde..0a07193bfe0 100644 --- a/esphome/components/ads1118/ads1118.cpp +++ b/esphome/components/ads1118/ads1118.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ads1118 { +namespace esphome::ads1118 { static const char *const TAG = "ads1118"; static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; @@ -122,5 +121,4 @@ float ADS1118::request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain g } } -} // namespace ads1118 -} // namespace esphome +} // namespace esphome::ads1118 diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h index e96baab3869..ef125a0b44b 100644 --- a/esphome/components/ads1118/ads1118.h +++ b/esphome/components/ads1118/ads1118.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ads1118 { +namespace esphome::ads1118 { enum ADS1118Multiplexer { ADS1118_MULTIPLEXER_P0_N1 = 0b000, @@ -41,5 +40,4 @@ class ADS1118 : public Component, uint16_t config_{0}; }; -} // namespace ads1118 -} // namespace esphome +} // namespace esphome::ads1118 diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.cpp b/esphome/components/ads1118/sensor/ads1118_sensor.cpp index 7193c3c8801..383a3d25fcb 100644 --- a/esphome/components/ads1118/sensor/ads1118_sensor.cpp +++ b/esphome/components/ads1118/sensor/ads1118_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace ads1118 { +namespace esphome::ads1118 { static const char *const TAG = "ads1118.sensor"; @@ -27,5 +26,4 @@ void ADS1118Sensor::update() { } } -} // namespace ads1118 -} // namespace esphome +} // namespace esphome::ads1118 diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.h b/esphome/components/ads1118/sensor/ads1118_sensor.h index d2d7a03f59d..b929e75c62d 100644 --- a/esphome/components/ads1118/sensor/ads1118_sensor.h +++ b/esphome/components/ads1118/sensor/ads1118_sensor.h @@ -8,8 +8,7 @@ #include "../ads1118.h" -namespace esphome { -namespace ads1118 { +namespace esphome::ads1118 { class ADS1118Sensor : public PollingComponent, public sensor::Sensor, @@ -32,5 +31,4 @@ class ADS1118Sensor : public PollingComponent, bool temperature_mode_; }; -} // namespace ads1118 -} // namespace esphome +} // namespace esphome::ads1118 diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp index fa7170114cf..230548ae947 100644 --- a/esphome/components/ags10/ags10.cpp +++ b/esphome/components/ags10/ags10.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace ags10 { +namespace esphome::ags10 { static const char *const TAG = "ags10"; // Data acquisition. @@ -192,5 +191,4 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che return data; } -} // namespace ags10 -} // namespace esphome +} // namespace esphome::ags10 diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index 9e034b20cb2..703acd5228c 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -5,8 +5,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace ags10 { +namespace esphome::ags10 { class AGS10Component : public PollingComponent, public i2c::I2CDevice { public: @@ -136,5 +135,4 @@ template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, p } } }; -} // namespace ags10 -} // namespace esphome +} // namespace esphome::ags10 diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 1b1f8335cc5..cc90abfc3a6 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -17,8 +17,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace aht10 { +namespace esphome::aht10 { static const char *const TAG = "aht10"; static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00}; @@ -160,5 +159,4 @@ void AHT10Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -} // namespace aht10 -} // namespace esphome +} // namespace esphome::aht10 diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index ce9cd963ad3..7b9b1761c4d 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace aht10 { +namespace esphome::aht10 { enum AHT10Variant { AHT10, AHT20 }; @@ -31,5 +30,4 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { uint32_t start_time_{}; }; -} // namespace aht10 -} // namespace esphome +} // namespace esphome::aht10 diff --git a/esphome/components/aic3204/aic3204.cpp b/esphome/components/aic3204/aic3204.cpp index e1acf32f83c..0ba960fd702 100644 --- a/esphome/components/aic3204/aic3204.cpp +++ b/esphome/components/aic3204/aic3204.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace aic3204 { +namespace esphome::aic3204 { static const char *const TAG = "aic3204"; @@ -167,5 +166,4 @@ bool AIC3204::write_volume_() { return true; } -} // namespace aic3204 -} // namespace esphome +} // namespace esphome::aic3204 diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h index 28006e33fcc..9b8c7928246 100644 --- a/esphome/components/aic3204/aic3204.h +++ b/esphome/components/aic3204/aic3204.h @@ -6,8 +6,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" -namespace esphome { -namespace aic3204 { +namespace esphome::aic3204 { // TLV320AIC3204 Register Addresses // Page 0 @@ -83,5 +82,4 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev float volume_{0}; }; -} // namespace aic3204 -} // namespace esphome +} // namespace esphome::aic3204 diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h index 851ff930f8a..50ae03edbd9 100644 --- a/esphome/components/aic3204/automation.h +++ b/esphome/components/aic3204/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "aic3204.h" -namespace esphome { -namespace aic3204 { +namespace esphome::aic3204 { template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> { public: @@ -19,5 +18,4 @@ template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> { AIC3204 *aic3204_; }; -} // namespace aic3204 -} // namespace esphome +} // namespace esphome::aic3204 diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp index 58faf923f54..881b3e297b5 100644 --- a/esphome/components/airthings_ble/airthings_listener.cpp +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace airthings_ble { +namespace esphome::airthings_ble { static const char *const TAG = "airthings_ble"; @@ -29,7 +28,6 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic return false; } -} // namespace airthings_ble -} // namespace esphome +} // namespace esphome::airthings_ble #endif diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h index 52f69ea9700..707e9c3f210 100644 --- a/esphome/components/airthings_ble/airthings_listener.h +++ b/esphome/components/airthings_ble/airthings_listener.h @@ -5,15 +5,13 @@ #include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -namespace esphome { -namespace airthings_ble { +namespace esphome::airthings_ble { class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; -} // namespace airthings_ble -} // namespace esphome +} // namespace esphome::airthings_ble #endif diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index e4c7d2a81d8..5fa59f22fd5 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace airthings_wave_base { +namespace esphome::airthings_wave_base { static const char *const TAG = "airthings_wave_base"; @@ -211,7 +210,6 @@ void AirthingsWaveBase::set_response_timeout_() { }); } -} // namespace airthings_wave_base -} // namespace esphome +} // namespace esphome::airthings_wave_base #endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.h b/esphome/components/airthings_wave_base/airthings_wave_base.h index 1dc2e1f71f2..2f1e15491f9 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.h +++ b/esphome/components/airthings_wave_base/airthings_wave_base.h @@ -14,8 +14,7 @@ #include "esphome/core/component.h" #include "esphome/core/log.h" -namespace esphome { -namespace airthings_wave_base { +namespace esphome::airthings_wave_base { namespace espbt = esphome::esp32_ble_tracker; @@ -84,7 +83,6 @@ class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientN }; }; -} // namespace airthings_wave_base -} // namespace esphome +} // namespace esphome::airthings_wave_base #endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp index 873826d06c2..f487e9dbc08 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.cpp @@ -2,8 +2,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace airthings_wave_mini { +namespace esphome::airthings_wave_mini { static const char *const TAG = "airthings_wave_mini"; @@ -49,7 +48,6 @@ AirthingsWaveMini::AirthingsWaveMini() { espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); } -} // namespace airthings_wave_mini -} // namespace esphome +} // namespace esphome::airthings_wave_mini #endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h index 825ddbdc69b..910ac902390 100644 --- a/esphome/components/airthings_wave_mini/airthings_wave_mini.h +++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h @@ -4,8 +4,7 @@ #include "esphome/components/airthings_wave_base/airthings_wave_base.h" -namespace esphome { -namespace airthings_wave_mini { +namespace esphome::airthings_wave_mini { namespace espbt = esphome::esp32_ble_tracker; @@ -34,7 +33,6 @@ class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { }; }; -} // namespace airthings_wave_mini -} // namespace esphome +} // namespace esphome::airthings_wave_mini #endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index 5ed62fff623..80fe081b572 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -2,8 +2,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace airthings_wave_plus { +namespace esphome::airthings_wave_plus { static const char *const TAG = "airthings_wave_plus"; @@ -98,7 +97,6 @@ void AirthingsWavePlus::setup() { espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid); } -} // namespace airthings_wave_plus -} // namespace esphome +} // namespace esphome::airthings_wave_plus #endif // USE_ESP32 diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index c978a9af927..6f51f3c65ac 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -4,8 +4,7 @@ #include "esphome/components/airthings_wave_base/airthings_wave_base.h" -namespace esphome { -namespace airthings_wave_plus { +namespace esphome::airthings_wave_plus { namespace espbt = esphome::esp32_ble_tracker; @@ -58,7 +57,6 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { }; }; -} // namespace airthings_wave_plus -} // namespace esphome +} // namespace esphome::airthings_wave_plus #endif // USE_ESP32 diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 6e82ec047db..048c365616c 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace alpha3 { +namespace esphome::alpha3 { static const char *const TAG = "alpha3"; @@ -185,7 +184,6 @@ void Alpha3::update() { delay(25); // need to wait between requests } } -} // namespace alpha3 -} // namespace esphome +} // namespace esphome::alpha3 #endif diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 19d8e99331a..c63129031ad 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -9,8 +9,7 @@ #include <esp_gattc_api.h> -namespace esphome { -namespace alpha3 { +namespace esphome::alpha3 { namespace espbt = esphome::esp32_ble_tracker; @@ -64,7 +63,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen void send_request_(uint8_t *request, size_t len); bool is_current_response_type_(const uint8_t *response_type); }; -} // namespace alpha3 -} // namespace esphome +} // namespace esphome::alpha3 #endif diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index 1390b749754..8980a8dfc3a 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -24,8 +24,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace am2315c { +namespace esphome::am2315c { static const char *const TAG = "am2315c"; @@ -176,5 +175,4 @@ void AM2315C::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -} // namespace am2315c -} // namespace esphome +} // namespace esphome::am2315c diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index d7baf01cae1..5a959af4c37 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -25,8 +25,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace am2315c { +namespace esphome::am2315c { class AM2315C : public PollingComponent, public i2c::I2CDevice { public: @@ -45,5 +44,4 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace am2315c -} // namespace esphome +} // namespace esphome::am2315c diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 7fef3bb3a62..5445ab38981 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -8,8 +8,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace am2320 { +namespace esphome::am2320 { static const char *const TAG = "am2320"; @@ -86,5 +85,4 @@ bool AM2320Component::read_data_(uint8_t *data) { return true; } -} // namespace am2320 -} // namespace esphome +} // namespace esphome::am2320 diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h index 708dbb632e8..ddb5c6f1653 100644 --- a/esphome/components/am2320/am2320.h +++ b/esphome/components/am2320/am2320.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace am2320 { +namespace esphome::am2320 { class AM2320Component : public PollingComponent, public i2c::I2CDevice { public: @@ -24,5 +23,4 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace am2320 -} // namespace esphome +} // namespace esphome::am2320 diff --git a/esphome/components/am43/am43_base.cpp b/esphome/components/am43/am43_base.cpp index d70e6383829..977185e5e31 100644 --- a/esphome/components/am43/am43_base.cpp +++ b/esphome/components/am43/am43_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include <cstring> -namespace esphome { -namespace am43 { +namespace esphome::am43 { const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; @@ -134,5 +133,4 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) { } }; -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 diff --git a/esphome/components/am43/am43_base.h b/esphome/components/am43/am43_base.h index 35354af9edd..5df83747c05 100644 --- a/esphome/components/am43/am43_base.h +++ b/esphome/components/am43/am43_base.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace am43 { +namespace esphome::am43 { static const uint16_t AM43_SERVICE_UUID = 0xFE50; static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51; @@ -74,5 +73,4 @@ class Am43Decoder { bool has_pin_response_; }; -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 2fa26d266a2..35366dbaa69 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace am43 { +namespace esphome::am43 { static const char *const TAG = "am43_cover"; @@ -154,7 +153,6 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 #endif diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h index d6d020e98c9..aa48aced158 100644 --- a/esphome/components/am43/cover/am43_cover.h +++ b/esphome/components/am43/cover/am43_cover.h @@ -10,8 +10,7 @@ #include <esp_gattc_api.h> -namespace esphome { -namespace am43 { +namespace esphome::am43 { namespace espbt = esphome::esp32_ble_tracker; @@ -38,7 +37,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient float position_; }; -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 #endif diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index b2bc3254e28..ddc3eadae9a 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace am43 { +namespace esphome::am43 { static const char *const TAG = "am43"; @@ -111,7 +110,6 @@ void Am43::update() { } } -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 #endif diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 195b96a19eb..9198a5cbcbd 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -10,8 +10,7 @@ #include <esp_gattc_api.h> -namespace esphome { -namespace am43 { +namespace esphome::am43 { namespace espbt = esphome::esp32_ble_tracker; @@ -38,7 +37,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent uint32_t last_battery_update_; }; -} // namespace am43 -} // namespace esphome +} // namespace esphome::am43 #endif diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp index 0b3bd0e472f..d25c10021c0 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "analog_threshold_binary_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace analog_threshold { +namespace esphome::analog_threshold { static const char *const TAG = "analog_threshold.binary_sensor"; @@ -43,5 +42,4 @@ void AnalogThresholdBinarySensor::dump_config() { this->upper_threshold_.value(), this->lower_threshold_.value()); } -} // namespace analog_threshold -} // namespace esphome +} // namespace esphome::analog_threshold diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 55a822b9b09..c768f1f82d6 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace analog_threshold { +namespace esphome::analog_threshold { class AnalogThresholdBinarySensor : public Component, public binary_sensor::BinarySensor { public: @@ -24,5 +23,4 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina bool raw_state_{false}; // Pre-filter state for hysteresis logic }; -} // namespace analog_threshold -} // namespace esphome +} // namespace esphome::analog_threshold diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 2f59a7fa5a7..2a293adf1d8 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace animation { +namespace esphome::animation { Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, image::Transparency transparent) @@ -71,5 +70,4 @@ void Animation::update_data_start_() { this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } -} // namespace animation -} // namespace esphome +} // namespace esphome::animation diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index b33254df30f..ca800ad9311 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace animation { +namespace esphome::animation { class Animation : public image::Image { public: @@ -64,5 +63,4 @@ template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> { Animation *parent_; }; -} // namespace animation -} // namespace esphome +} // namespace esphome::animation diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index f21230b075e..6e382872e2b 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace anova { +namespace esphome::anova { static const char *const TAG = "anova"; @@ -160,7 +159,6 @@ void Anova::update() { } } -} // namespace anova -} // namespace esphome +} // namespace esphome::anova #endif diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 2e43ebfb983..a3e175be280 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -10,8 +10,7 @@ #include <esp_gattc_api.h> -namespace esphome { -namespace anova { +namespace esphome::anova { namespace espbt = esphome::esp32_ble_tracker; @@ -45,7 +44,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode bool fahrenheit_; }; -} // namespace anova -} // namespace esphome +} // namespace esphome::anova #endif diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index a14dd728a80..84dd4393eb2 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -4,8 +4,7 @@ #include "esphome/core/alloc_helpers.h" -namespace esphome { -namespace anova { +namespace esphome::anova { float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); } @@ -132,5 +131,4 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } } -} // namespace anova -} // namespace esphome +} // namespace esphome::anova diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h index b8311578497..b3ed0f01a00 100644 --- a/esphome/components/anova/anova_base.h +++ b/esphome/components/anova/anova_base.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace anova { +namespace esphome::anova { enum CurrentQuery { NONE, @@ -75,5 +74,4 @@ class AnovaCodec { CurrentQuery current_query_; }; -} // namespace anova -} // namespace esphome +} // namespace esphome::anova diff --git a/esphome/components/apds9306/apds9306.cpp b/esphome/components/apds9306/apds9306.cpp index fb3adde8688..57a502ca420 100644 --- a/esphome/components/apds9306/apds9306.cpp +++ b/esphome/components/apds9306/apds9306.cpp @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace apds9306 { +namespace esphome::apds9306 { static const char *const TAG = "apds9306"; @@ -147,5 +146,4 @@ void APDS9306::update() { this->publish_state(lux); } -} // namespace apds9306 -} // namespace esphome +} // namespace esphome::apds9306 diff --git a/esphome/components/apds9306/apds9306.h b/esphome/components/apds9306/apds9306.h index 44362908c81..093ec55bc63 100644 --- a/esphome/components/apds9306/apds9306.h +++ b/esphome/components/apds9306/apds9306.h @@ -7,8 +7,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace apds9306 { +namespace esphome::apds9306 { enum MeasurementBitWidth : uint8_t { MEASUREMENT_BIT_WIDTH_20 = 0, @@ -62,5 +61,4 @@ class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2C AmbientLightGain gain_; }; -} // namespace apds9306 -} // namespace esphome +} // namespace esphome::apds9306 diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index a07175f2c9c..da8029b4ee0 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace apds9960 { +namespace esphome::apds9960 { static const char *const TAG = "apds9960"; @@ -402,5 +401,4 @@ bool APDS9960::is_gesture_enabled_() const { #endif } -} // namespace apds9960 -} // namespace esphome +} // namespace esphome::apds9960 diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h index 4574b70a42e..2823294207b 100644 --- a/esphome/components/apds9960/apds9960.h +++ b/esphome/components/apds9960/apds9960.h @@ -10,8 +10,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -namespace esphome { -namespace apds9960 { +namespace esphome::apds9960 { class APDS9960 : public PollingComponent, public i2c::I2CDevice { #ifdef USE_SENSOR @@ -71,5 +70,4 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { uint32_t gesture_start_{0}; }; -} // namespace apds9960 -} // namespace esphome +} // namespace esphome::apds9960 diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index c4dc0466a04..a1d74c78deb 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -1,8 +1,7 @@ #include "as3935.h" #include "esphome/core/log.h" -namespace esphome { -namespace as3935 { +namespace esphome::as3935 { static const char *const TAG = "as3935"; @@ -320,5 +319,4 @@ uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) { return value; } -} // namespace as3935 -} // namespace esphome +} // namespace esphome::as3935 diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index 5f46dadfa8c..274ce7a138d 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -10,8 +10,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -namespace esphome { -namespace as3935 { +namespace esphome::as3935 { static const uint8_t DIRECT_COMMAND = 0x96; static const uint8_t ANTFREQ = 3; @@ -127,5 +126,4 @@ class AS3935Component : public Component { bool calibration_; }; -} // namespace as3935 -} // namespace esphome +} // namespace esphome::as3935 diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp index 3a7fa7bf846..4c1020daa77 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.cpp +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -1,8 +1,7 @@ #include "as3935_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace as3935_i2c { +namespace esphome::as3935_i2c { static const char *const TAG = "as3935_i2c"; @@ -40,5 +39,4 @@ void I2CAS3935Component::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace as3935_i2c -} // namespace esphome +} // namespace esphome::as3935_i2c diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h index a2a3d213efa..c43ec4afd5b 100644 --- a/esphome/components/as3935_i2c/as3935_i2c.h +++ b/esphome/components/as3935_i2c/as3935_i2c.h @@ -3,8 +3,7 @@ #include "esphome/components/as3935/as3935.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace as3935_i2c { +namespace esphome::as3935_i2c { class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice { public: @@ -15,5 +14,4 @@ class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice uint8_t read_register(uint8_t reg) override; }; -} // namespace as3935_i2c -} // namespace esphome +} // namespace esphome::as3935_i2c diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp index 1b2e9ccd3fa..026fde2f213 100644 --- a/esphome/components/as3935_spi/as3935_spi.cpp +++ b/esphome/components/as3935_spi/as3935_spi.cpp @@ -1,8 +1,7 @@ #include "as3935_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace as3935_spi { +namespace esphome::as3935_spi { static const char *const TAG = "as3935_spi"; @@ -42,5 +41,4 @@ uint8_t SPIAS3935Component::read_register(uint8_t reg) { return value; } -} // namespace as3935_spi -} // namespace esphome +} // namespace esphome::as3935_spi diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h index e5422f9b377..935707a18c0 100644 --- a/esphome/components/as3935_spi/as3935_spi.h +++ b/esphome/components/as3935_spi/as3935_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/as3935/as3935.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace as3935_spi { +namespace esphome::as3935_spi { enum AS3935RegisterMasks { SPI_READ_M = 0x40 }; @@ -21,5 +20,4 @@ class SPIAS3935Component : public as3935::AS3935Component, uint8_t read_register(uint8_t reg) override; }; -} // namespace as3935_spi -} // namespace esphome +} // namespace esphome::as3935_spi diff --git a/esphome/components/as5600/as5600.cpp b/esphome/components/as5600/as5600.cpp index ee3083d5611..da1add5458b 100644 --- a/esphome/components/as5600/as5600.cpp +++ b/esphome/components/as5600/as5600.cpp @@ -1,8 +1,7 @@ #include "as5600.h" #include "esphome/core/log.h" -namespace esphome { -namespace as5600 { +namespace esphome::as5600 { static const char *const TAG = "as5600"; @@ -134,5 +133,4 @@ optional<uint16_t> AS5600Component::read_raw_position() { return pos; } -} // namespace as5600 -} // namespace esphome +} // namespace esphome::as5600 diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h index 914a4431bd0..414633f978b 100644 --- a/esphome/components/as5600/as5600.h +++ b/esphome/components/as5600/as5600.h @@ -6,8 +6,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace as5600 { +namespace esphome::as5600 { static const uint16_t POSITION_COUNT = 4096; static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT; @@ -100,5 +99,4 @@ class AS5600Component : public Component, public i2c::I2CDevice { float range_scale_{1.0}; }; -} // namespace as5600 -} // namespace esphome +} // namespace esphome::as5600 diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index 4e549d24d52..ba295659a13 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -1,8 +1,7 @@ #include "as5600_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace as5600 { +namespace esphome::as5600 { static const char *const TAG = "as5600.sensor"; @@ -75,5 +74,4 @@ void AS5600Sensor::update() { this->status_clear_warning(); } -} // namespace as5600 -} // namespace esphome +} // namespace esphome::as5600 diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index 77593f4b12c..0086fe54ccd 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -7,8 +7,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/as5600/as5600.h" -namespace esphome { -namespace as5600 { +namespace esphome::as5600 { class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor { public: @@ -32,5 +31,4 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX}; }; -} // namespace as5600 -} // namespace esphome +} // namespace esphome::as5600 diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 1e78d814c8c..32094ddf101 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace as7341 { +namespace esphome::as7341 { static const char *const TAG = "as7341"; @@ -266,5 +265,4 @@ bool AS7341Component::clear_register_bit(uint8_t address, uint8_t bit_position) uint16_t AS7341Component::swap_bytes(uint16_t data) { return (data >> 8) | (data << 8); } -} // namespace as7341 -} // namespace esphome +} // namespace esphome::as7341 diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h index 3ede9d4aa4b..8bc157fe79a 100644 --- a/esphome/components/as7341/as7341.h +++ b/esphome/components/as7341/as7341.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace as7341 { +namespace esphome::as7341 { static const uint8_t AS7341_CHIP_ID = 0x09; @@ -139,5 +138,4 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice { uint16_t channel_readings_[12]; }; -} // namespace as7341 -} // namespace esphome +} // namespace esphome::as7341 diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6fc85b07902..575a9afd40b 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -49,8 +49,7 @@ const uint8_t TRIGGER_KEEP_TIME_ADDR = 0x42; // 4 bytes, so up to 0x45 const uint8_t TIME41_VALUE = 1; const uint8_t SELF_CHECK_TIME_ADDR = 0x38; // 2 bytes, up to 0x39 -namespace esphome { -namespace at581x { +namespace esphome::at581x { static const char *const TAG = "at581x"; @@ -199,5 +198,4 @@ void AT581XComponent::set_rf_mode(bool enable) { } } -} // namespace at581x -} // namespace esphome +} // namespace esphome::at581x diff --git a/esphome/components/at581x/at581x.h b/esphome/components/at581x/at581x.h index 558a5c8b19d..e7f8ee36923 100644 --- a/esphome/components/at581x/at581x.h +++ b/esphome/components/at581x/at581x.h @@ -10,8 +10,7 @@ #endif #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace at581x { +namespace esphome::at581x { class AT581XComponent : public Component, public i2c::I2CDevice { public: @@ -58,5 +57,4 @@ class AT581XComponent : public Component, public i2c::I2CDevice { int power_; /*!< In µA */ }; -} // namespace at581x -} // namespace esphome +} // namespace esphome::at581x diff --git a/esphome/components/at581x/automation.h b/esphome/components/at581x/automation.h index b1611a6758f..eb8b1b25628 100644 --- a/esphome/components/at581x/automation.h +++ b/esphome/components/at581x/automation.h @@ -5,8 +5,7 @@ #include "at581x.h" -namespace esphome { -namespace at581x { +namespace esphome::at581x { template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> { public: @@ -67,5 +66,4 @@ template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, publ } } }; -} // namespace at581x -} // namespace esphome +} // namespace esphome::at581x diff --git a/esphome/components/at581x/switch/rf_switch.cpp b/esphome/components/at581x/switch/rf_switch.cpp index f1d03dc8a54..7432233863e 100644 --- a/esphome/components/at581x/switch/rf_switch.cpp +++ b/esphome/components/at581x/switch/rf_switch.cpp @@ -1,12 +1,10 @@ #include "rf_switch.h" -namespace esphome { -namespace at581x { +namespace esphome::at581x { void RFSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_rf_mode(state); } -} // namespace at581x -} // namespace esphome +} // namespace esphome::at581x diff --git a/esphome/components/at581x/switch/rf_switch.h b/esphome/components/at581x/switch/rf_switch.h index 920ddbb66a1..47367fad45f 100644 --- a/esphome/components/at581x/switch/rf_switch.h +++ b/esphome/components/at581x/switch/rf_switch.h @@ -3,13 +3,11 @@ #include "esphome/components/switch/switch.h" #include "../at581x.h" -namespace esphome { -namespace at581x { +namespace esphome::at581x { class RFSwitch : public switch_::Switch, public Parented<AT581XComponent> { protected: void write_state(bool state) override; }; -} // namespace at581x -} // namespace esphome +} // namespace esphome::at581x diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index 9afd6334f5b..f8bbd9d55ee 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace atc_mithermometer { +namespace esphome::atc_mithermometer { static const char *const TAG = "atc_mithermometer"; @@ -133,7 +132,6 @@ bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, cons return true; } -} // namespace atc_mithermometer -} // namespace esphome +} // namespace esphome::atc_mithermometer #endif diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index e37b5f43507..8f62f05bc13 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace atc_mithermometer { +namespace esphome::atc_mithermometer { struct ParseResult { optional<float> temperature; @@ -44,7 +43,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice bool report_results_(const optional<ParseResult> &result, const char *address); }; -} // namespace atc_mithermometer -} // namespace esphome +} // namespace esphome::atc_mithermometer #endif diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index e6602411bb8..46948be47d8 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -2,8 +2,7 @@ #include "atm90e26_reg.h" #include "esphome/core/log.h" -namespace esphome { -namespace atm90e26 { +namespace esphome::atm90e26 { static const char *const TAG = "atm90e26"; @@ -229,5 +228,4 @@ float ATM90E26Component::get_frequency_() { return freq / 100.0f; } -} // namespace atm90e26 -} // namespace esphome +} // namespace esphome::atm90e26 diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h index d15a53ea433..657f8f3c433 100644 --- a/esphome/components/atm90e26/atm90e26.h +++ b/esphome/components/atm90e26/atm90e26.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace atm90e26 { +namespace esphome::atm90e26 { class ATM90E26Component : public PollingComponent, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, @@ -67,5 +66,4 @@ class ATM90E26Component : public PollingComponent, uint32_t pl_const_{1429876}; }; -} // namespace atm90e26 -} // namespace esphome +} // namespace esphome::atm90e26 diff --git a/esphome/components/atm90e26/atm90e26_reg.h b/esphome/components/atm90e26/atm90e26_reg.h index 0a925f424ea..592bb3bc56a 100644 --- a/esphome/components/atm90e26/atm90e26_reg.h +++ b/esphome/components/atm90e26/atm90e26_reg.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace atm90e26 { +namespace esphome::atm90e26 { /* Status and Special Register */ static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset @@ -66,5 +65,4 @@ static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power -} // namespace atm90e26 -} // namespace esphome +} // namespace esphome::atm90e26 diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 693b1b49612..d948b3741df 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -5,8 +5,7 @@ #include <numbers> #include "esphome/core/log.h" -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { static const char *const TAG = "atm90e32"; @@ -1313,5 +1312,4 @@ bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *contex return true; } -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 62c7bada86c..5fa224b3535 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -11,8 +11,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { class ATM90E32Component : public PollingComponent, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, @@ -275,5 +274,4 @@ class ATM90E32Component : public PollingComponent, bool gain_calibration_mismatch_[3]{false, false, false}; }; -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index 86c2d645695..480c2b267c6 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -2,8 +2,7 @@ #include <cinttypes> -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { /* STATUS REGISTERS */ static const uint16_t ATM90E32_REGISTER_METEREN = 0x00; // Metering Enable @@ -268,5 +267,4 @@ static const uint16_t ATM90E32_REGISTER_UANGLEA = 0xFD; // A Voltage Phase Angl static const uint16_t ATM90E32_REGISTER_UANGLEB = 0xFE; // B Voltage Phase Angle static const uint16_t ATM90E32_REGISTER_UANGLEC = 0xFF; // C Voltage Phase Angle -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp index a89f071997d..e120e453644 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.cpp +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -2,8 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/log.h" -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { static const char *const TAG = "atm90e32.button"; @@ -75,5 +74,4 @@ void ATM90E32ClearPowerOffsetCalibrationButton::press_action() { this->parent_->clear_power_offset_calibrations(); } -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h index 24495815315..0cfce622934 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.h +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -4,8 +4,7 @@ #include "esphome/components/atm90e32/atm90e32.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { class ATM90E32GainCalibrationButton : public button::Button, public Parented<ATM90E32Component> { public: @@ -55,5 +54,4 @@ class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public void press_action() override; }; -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/atm90e32/number/atm90e32_number.h b/esphome/components/atm90e32/number/atm90e32_number.h index 9b6129b26d6..a575a94ea49 100644 --- a/esphome/components/atm90e32/number/atm90e32_number.h +++ b/esphome/components/atm90e32/number/atm90e32_number.h @@ -4,13 +4,11 @@ #include "esphome/components/atm90e32/atm90e32.h" #include "esphome/components/number/number.h" -namespace esphome { -namespace atm90e32 { +namespace esphome::atm90e32 { class ATM90E32Number : public number::Number, public Parented<ATM90E32Component> { public: void control(float value) override { this->publish_state(value); } }; -} // namespace atm90e32 -} // namespace esphome +} // namespace esphome::atm90e32 diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index b977c4e9181..b0aa3c1abbe 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -4,8 +4,7 @@ #include <cstring> -namespace esphome { -namespace audio { +namespace esphome::audio { // Euclidean's algorithm for finding the greatest common divisor static uint32_t gcd(uint32_t a, uint32_t b) { @@ -129,5 +128,4 @@ void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, i } } -} // namespace audio -} // namespace esphome +} // namespace esphome::audio diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 9259f0a3c65..62c57b18cf7 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -5,8 +5,7 @@ #include <cstddef> #include <cstdint> -namespace esphome { -namespace audio { +namespace esphome::audio { class AudioStreamInfo { /* Class to respresent important parameters of the audio stream that also provides helper function to convert between @@ -195,5 +194,4 @@ inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes } } -} // namespace audio -} // namespace esphome +} // namespace esphome::audio diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 7abd03a36e5..3e6fad1101e 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace audio { +namespace esphome::audio { static const char *const TAG = "audio.decoder"; @@ -428,7 +427,6 @@ FileDecoderState AudioDecoder::decode_wav_() { } #endif -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 58e982317c3..7ea7a824f96 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -35,8 +35,7 @@ #include <micro_wav/wav_decoder.h> #endif -namespace esphome { -namespace audio { +namespace esphome::audio { enum class AudioDecoderState : uint8_t { DECODING = 0, // More data is available to decode @@ -155,7 +154,6 @@ class AudioDecoder { bool pause_output_{false}; }; -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 79ebf58889f..500f20533cd 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -11,8 +11,7 @@ #include "esp_crt_bundle.h" #endif -namespace esphome { -namespace audio { +namespace esphome::audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; @@ -289,7 +288,6 @@ void AudioReader::cleanup_connection_() { } } -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 753b310213e..61f187d151d 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -11,8 +11,7 @@ #include <esp_http_client.h> -namespace esphome { -namespace audio { +namespace esphome::audio { enum class AudioReaderState : uint8_t { READING = 0, // More data is available to read @@ -74,7 +73,6 @@ class AudioReader { AudioFileType audio_file_type_{AudioFileType::NONE}; const uint8_t *file_current_{nullptr}; }; -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_resampler.cpp b/esphome/components/audio/audio_resampler.cpp index 20d246f1e04..ac1039971e6 100644 --- a/esphome/components/audio/audio_resampler.cpp +++ b/esphome/components/audio/audio_resampler.cpp @@ -6,8 +6,7 @@ #include <cstring> -namespace esphome { -namespace audio { +namespace esphome::audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; @@ -157,7 +156,6 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d return AudioResamplerState::RESAMPLING; } -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_resampler.h b/esphome/components/audio/audio_resampler.h index 082ade33712..e7503d1de07 100644 --- a/esphome/components/audio/audio_resampler.h +++ b/esphome/components/audio/audio_resampler.h @@ -17,8 +17,7 @@ #include <resampler.h> // esp-audio-libs -namespace esphome { -namespace audio { +namespace esphome::audio { enum class AudioResamplerState : uint8_t { RESAMPLING, // More data is available to resample @@ -96,7 +95,6 @@ class AudioResampler { std::unique_ptr<esp_audio_libs::resampler::Resampler> resampler_; }; -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 5cd7cf9e631..6ee9e4d28c0 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace audio { +namespace esphome::audio { AudioTransferBuffer::~AudioTransferBuffer() { this->deallocate_buffer_(); }; @@ -208,7 +207,6 @@ void ConstAudioSourceBuffer::consume(size_t bytes) { this->data_start_ += bytes; } -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index c32d4d0e41d..7aa830fafa1 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -12,8 +12,7 @@ #include <freertos/FreeRTOS.h> -namespace esphome { -namespace audio { +namespace esphome::audio { /// @brief Abstract interface for writing decoded audio data to a sink. class AudioSinkCallback { @@ -213,7 +212,6 @@ class ConstAudioSourceBuffer : public AudioReadableBuffer { size_t length_{0}; }; -} // namespace audio -} // namespace esphome +} // namespace esphome::audio #endif diff --git a/esphome/components/audio_adc/audio_adc.h b/esphome/components/audio_adc/audio_adc.h index 94bfb57db55..a1da2360acd 100644 --- a/esphome/components/audio_adc/audio_adc.h +++ b/esphome/components/audio_adc/audio_adc.h @@ -3,8 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" -namespace esphome { -namespace audio_adc { +namespace esphome::audio_adc { class AudioAdc { public: @@ -13,5 +12,4 @@ class AudioAdc { virtual float mic_gain() = 0; }; -} // namespace audio_adc -} // namespace esphome +} // namespace esphome::audio_adc diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h index 0c424684793..e74e0232036 100644 --- a/esphome/components/audio_adc/automation.h +++ b/esphome/components/audio_adc/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "audio_adc.h" -namespace esphome { -namespace audio_adc { +namespace esphome::audio_adc { template<typename... Ts> class SetMicGainAction : public Action<Ts...> { public: @@ -19,5 +18,4 @@ template<typename... Ts> class SetMicGainAction : public Action<Ts...> { AudioAdc *audio_adc_; }; -} // namespace audio_adc -} // namespace esphome +} // namespace esphome::audio_adc diff --git a/esphome/components/audio_dac/audio_dac.h b/esphome/components/audio_dac/audio_dac.h index a62d17b8497..16a422f4ac7 100644 --- a/esphome/components/audio_dac/audio_dac.h +++ b/esphome/components/audio_dac/audio_dac.h @@ -3,8 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" -namespace esphome { -namespace audio_dac { +namespace esphome::audio_dac { class AudioDac { public: @@ -19,5 +18,4 @@ class AudioDac { bool is_muted_{false}; }; -} // namespace audio_dac -} // namespace esphome +} // namespace esphome::audio_dac diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h index 3eb3441f3d3..67bbc78ac21 100644 --- a/esphome/components/audio_dac/automation.h +++ b/esphome/components/audio_dac/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "audio_dac.h" -namespace esphome { -namespace audio_dac { +namespace esphome::audio_dac { template<typename... Ts> class MuteOffAction : public Action<Ts...> { public: @@ -39,5 +38,4 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...> { AudioDac *audio_dac_; }; -} // namespace audio_dac -} // namespace esphome +} // namespace esphome::audio_dac diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index ab3f1dad4f9..3869224b911 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace axs15231 { +namespace esphome::axs15231 { static const char *const TAG = "ax15231.touchscreen"; @@ -64,5 +63,4 @@ void AXS15231Touchscreen::dump_config() { this->x_raw_max_, this->y_raw_max_); } -} // namespace axs15231 -} // namespace esphome +} // namespace esphome::axs15231 diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h index a55c5c0d324..94d232777c6 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace axs15231 { +namespace esphome::axs15231 { class AXS15231Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: @@ -23,5 +22,4 @@ class AXS15231Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevi GPIOPin *reset_pin_{}; }; -} // namespace axs15231 -} // namespace esphome +} // namespace esphome::axs15231 diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 7be26efa7f2..160d22a5b66 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace b_parasite { +namespace esphome::b_parasite { static const char *const TAG = "b_parasite"; @@ -113,7 +112,6 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return true; } -} // namespace b_parasite -} // namespace esphome +} // namespace esphome::b_parasite #endif // USE_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 7dd08968ec0..c719599b998 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace b_parasite { +namespace esphome::b_parasite { class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -35,7 +34,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene sensor::Sensor *illuminance_{nullptr}; }; -} // namespace b_parasite -} // namespace esphome +} // namespace esphome::b_parasite #endif // USE_ESP32 diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp index deb742f8c67..eebc970795a 100644 --- a/esphome/components/ballu/ballu.cpp +++ b/esphome/components/ballu/ballu.cpp @@ -1,8 +1,7 @@ #include "ballu.h" #include "esphome/core/log.h" -namespace esphome { -namespace ballu { +namespace esphome::ballu { static const char *const TAG = "ballu.climate"; @@ -235,5 +234,4 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace ballu -} // namespace esphome +} // namespace esphome::ballu diff --git a/esphome/components/ballu/ballu.h b/esphome/components/ballu/ballu.h index 80a4699cfb0..8a45d39c703 100644 --- a/esphome/components/ballu/ballu.h +++ b/esphome/components/ballu/ballu.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace ballu { +namespace esphome::ballu { // Support for Ballu air conditioners with YKR-K/002E remote @@ -27,5 +26,4 @@ class BalluClimate : public climate_ir::ClimateIR { bool on_receive(remote_base::RemoteReceiveData data) override; }; -} // namespace ballu -} // namespace esphome +} // namespace esphome::ballu diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 1058bce6a45..5dfb1213429 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -1,8 +1,7 @@ #include "bang_bang_climate.h" #include "esphome/core/log.h" -namespace esphome { -namespace bang_bang { +namespace esphome::bang_bang { static const char *const TAG = "bang_bang.climate"; @@ -231,5 +230,4 @@ BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_t float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} -} // namespace bang_bang -} // namespace esphome +} // namespace esphome::bang_bang diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index d0ddef2848e..1e5ff84883f 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -5,8 +5,7 @@ #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bang_bang { +namespace esphome::bang_bang { struct BangBangClimateTargetTempConfig { public: @@ -84,5 +83,4 @@ class BangBangClimate : public climate::Climate, public Component { BangBangClimateTargetTempConfig away_config_{}; }; -} // namespace bang_bang -} // namespace esphome +} // namespace esphome::bang_bang diff --git a/esphome/components/bedjet/bedjet_child.h b/esphome/components/bedjet/bedjet_child.h index 4e07745c631..5b6c5f7f25a 100644 --- a/esphome/components/bedjet/bedjet_child.h +++ b/esphome/components/bedjet/bedjet_child.h @@ -3,8 +3,7 @@ #include "bedjet_codec.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { // Forward declare BedJetHub class BedJetHub; @@ -19,5 +18,4 @@ class BedJetClient : public Parented<BedJetHub> { virtual std::string describe() = 0; }; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bedjet/bedjet_codec.cpp b/esphome/components/bedjet/bedjet_codec.cpp index 7a959390f31..6f6242a4cd7 100644 --- a/esphome/components/bedjet/bedjet_codec.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -3,8 +3,7 @@ #include <cstdio> #include <cstring> -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { /// Converts a BedJet temp step into degrees Fahrenheit. float bedjet_temp_to_f(const uint8_t temp) { @@ -177,5 +176,4 @@ float bedjet_temp_to_c(uint8_t temp) { return temp / 2.0f; } -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 3936ba23151..7cf463b5665 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -5,8 +5,7 @@ #include "bedjet_const.h" -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { struct BedjetPacket { uint8_t data_length; @@ -190,5 +189,4 @@ class BedjetCodec { /// Converts a BedJet temp step into degrees Celsius. float bedjet_temp_to_c(uint8_t temp); -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 10f403dd1ac..eb777a6a834 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -2,8 +2,7 @@ #include <set> -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { static const char *const TAG = "bedjet"; @@ -101,5 +100,4 @@ static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fec34c5b2ab..b04603b8c6d 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -6,8 +6,7 @@ #include "esphome/core/application.h" #include <cinttypes> -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { static const LogString *bedjet_button_to_string(BedjetButton button) { switch (button) { @@ -551,7 +550,6 @@ void BedJetHub::register_child(BedJetClient *obj) { obj->set_parent(this); } -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 59b0af93ade..9f25f7a4660 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -18,8 +18,7 @@ #include <esp_gattc_api.h> -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { namespace espbt = esphome::esp32_ble_tracker; @@ -172,7 +171,6 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo uint8_t write_notify_config_descriptor_(bool enable); }; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 88ed902a112..196d4785e98 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { using namespace esphome::climate; @@ -359,7 +358,6 @@ void BedJetClimate::update() { ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false"); } -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h index d12c2a8255c..f59e67eeb7b 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.h +++ b/esphome/components/bedjet/climate/bedjet_climate.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { class BedJetClimate : public climate::Climate, public BedJetClient, public PollingComponent { public: @@ -72,7 +71,6 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli } }; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index 9539e169a45..4b1bd14ae34 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { using namespace esphome::fan; @@ -109,7 +108,6 @@ void BedJetFan::reset_state_() { this->state = false; this->publish_state(); } -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/fan/bedjet_fan.h b/esphome/components/bedjet/fan/bedjet_fan.h index 19db06e9d3c..03f42f1438a 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.h +++ b/esphome/components/bedjet/fan/bedjet_fan.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { class BedJetFan : public fan::Fan, public BedJetClient, public PollingComponent { public: @@ -34,7 +33,6 @@ class BedJetFan : public fan::Fan, public BedJetClient, public PollingComponent bool update_status_(); }; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet #endif diff --git a/esphome/components/bedjet/sensor/bedjet_sensor.cpp b/esphome/components/bedjet/sensor/bedjet_sensor.cpp index 2fda8c927f1..05417bd5193 100644 --- a/esphome/components/bedjet/sensor/bedjet_sensor.cpp +++ b/esphome/components/bedjet/sensor/bedjet_sensor.cpp @@ -1,8 +1,7 @@ #include "bedjet_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { std::string BedjetSensor::describe() { return "BedJet Sensor"; } @@ -30,5 +29,4 @@ void BedjetSensor::on_status(const BedjetStatusPacket *data) { } } -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bedjet/sensor/bedjet_sensor.h b/esphome/components/bedjet/sensor/bedjet_sensor.h index 8cbaa863eec..0c3f713579d 100644 --- a/esphome/components/bedjet/sensor/bedjet_sensor.h +++ b/esphome/components/bedjet/sensor/bedjet_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/bedjet/bedjet_child.h" #include "esphome/components/bedjet/bedjet_codec.h" -namespace esphome { -namespace bedjet { +namespace esphome::bedjet { class BedjetSensor : public BedJetClient, public Component { public: @@ -28,5 +27,4 @@ class BedjetSensor : public BedJetClient, public Component { sensor::Sensor *ambient_temperature_sensor_{nullptr}; }; -} // namespace bedjet -} // namespace esphome +} // namespace esphome::bedjet diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp index 0e71bd6532b..4f746a17d4b 100644 --- a/esphome/components/bh1900nux/bh1900nux.cpp +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "bh1900nux.h" -namespace esphome { -namespace bh1900nux { +namespace esphome::bh1900nux { static const char *const TAG = "bh1900nux.sensor"; @@ -50,5 +49,4 @@ void BH1900NUXSensor::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace bh1900nux -} // namespace esphome +} // namespace esphome::bh1900nux diff --git a/esphome/components/bh1900nux/bh1900nux.h b/esphome/components/bh1900nux/bh1900nux.h index fd7f8848d63..61d1bac268e 100644 --- a/esphome/components/bh1900nux/bh1900nux.h +++ b/esphome/components/bh1900nux/bh1900nux.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bh1900nux { +namespace esphome::bh1900nux { class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -14,5 +13,4 @@ class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i void dump_config() override; }; -} // namespace bh1900nux -} // namespace esphome +} // namespace esphome::bh1900nux diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 17d4df095a0..118d87c09d0 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -1,8 +1,7 @@ #include "binary_fan.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary { +namespace esphome::binary { static const char *const TAG = "binary.fan"; @@ -39,5 +38,4 @@ void BinaryFan::write_state_() { this->direction_->set_state(this->direction == fan::FanDirection::REVERSE); } -} // namespace binary -} // namespace esphome +} // namespace esphome::binary diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 16bce2e6af2..17157dd29ca 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -4,8 +4,7 @@ #include "esphome/components/output/binary_output.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace binary { +namespace esphome::binary { class BinaryFan : public Component, public fan::Fan { public: @@ -27,5 +26,4 @@ class BinaryFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; }; -} // namespace binary -} // namespace esphome +} // namespace esphome::binary diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 8346a82cf0e..f6be7e162e9 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/binary_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace binary { +namespace esphome::binary { class BinaryLightOutput : public light::LightOutput { public: @@ -29,5 +28,4 @@ class BinaryLightOutput : public light::LightOutput { output::BinaryOutput *output_; }; -} // namespace binary -} // namespace esphome +} // namespace esphome::binary diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.cpp b/esphome/components/binary_sensor_map/binary_sensor_map.cpp index 0bf62028939..316d44ba59e 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.cpp +++ b/esphome/components/binary_sensor_map/binary_sensor_map.cpp @@ -1,8 +1,7 @@ #include "binary_sensor_map.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary_sensor_map { +namespace esphome::binary_sensor_map { static const char *const TAG = "binary_sensor_map"; @@ -138,5 +137,4 @@ void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float pro }; this->channels_.push_back(sensor_channel); } -} // namespace binary_sensor_map -} // namespace esphome +} // namespace esphome::binary_sensor_map diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.h b/esphome/components/binary_sensor_map/binary_sensor_map.h index a07154c0e86..60224242db6 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.h +++ b/esphome/components/binary_sensor_map/binary_sensor_map.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace binary_sensor_map { +namespace esphome::binary_sensor_map { enum BinarySensorMapType { BINARY_SENSOR_MAP_TYPE_GROUP, @@ -96,5 +95,4 @@ class BinarySensorMap : public sensor::Sensor, public Component { float bayesian_predicate_(bool sensor_state, float prior, float prob_given_true, float prob_given_false); }; -} // namespace binary_sensor_map -} // namespace esphome +} // namespace esphome::binary_sensor_map diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index d554057f7b0..d3877570512 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace bl0906 { +namespace esphome::bl0906 { static const char *const TAG = "bl0906"; @@ -262,5 +261,4 @@ void BL0906::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -} // namespace bl0906 -} // namespace esphome +} // namespace esphome::bl0906 diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h index f7ba5423d27..821aac476c4 100644 --- a/esphome/components/bl0906/bl0906.h +++ b/esphome/components/bl0906/bl0906.h @@ -9,8 +9,7 @@ // https://www.belling.com.cn/media/file_object/bel_product/BL0906/datasheet/BL0906_V1.02_cn.pdf // https://www.belling.com.cn/media/file_object/bel_product/BL0906/guide/BL0906%20APP%20Note_V1.02.pdf -namespace esphome { -namespace bl0906 { +namespace esphome::bl0906 { // Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine // jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle @@ -109,5 +108,4 @@ template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } }; -} // namespace bl0906 -} // namespace esphome +} // namespace esphome::bl0906 diff --git a/esphome/components/bl0906/constants.h b/esphome/components/bl0906/constants.h index a174e54bb26..423d460210c 100644 --- a/esphome/components/bl0906/constants.h +++ b/esphome/components/bl0906/constants.h @@ -1,8 +1,7 @@ #pragma once #include <cstdint> -namespace esphome { -namespace bl0906 { +namespace esphome::bl0906 { // Total power conversion static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / @@ -118,5 +117,4 @@ const uint8_t BL0906_INIT[2][6] = { // Enable User Operation Write {BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}}; -} // namespace bl0906 -} // namespace esphome +} // namespace esphome::bl0906 diff --git a/esphome/components/bl0939/bl0939.cpp b/esphome/components/bl0939/bl0939.cpp index 7428e487402..9ae3360e3ac 100644 --- a/esphome/components/bl0939/bl0939.cpp +++ b/esphome/components/bl0939/bl0939.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace bl0939 { +namespace esphome::bl0939 { static const char *const TAG = "bl0939"; @@ -142,5 +141,4 @@ uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } -} // namespace bl0939 -} // namespace esphome +} // namespace esphome::bl0939 diff --git a/esphome/components/bl0939/bl0939.h b/esphome/components/bl0939/bl0939.h index 673d4ff351f..b4f6d42e71f 100644 --- a/esphome/components/bl0939/bl0939.h +++ b/esphome/components/bl0939/bl0939.h @@ -4,8 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bl0939 { +namespace esphome::bl0939 { // https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf // (unfortunately chinese, but the formulas can be easily understood) @@ -103,5 +102,4 @@ class BL0939 : public PollingComponent, public uart::UARTDevice { void received_package_(const DataPacket *data) const; }; -} // namespace bl0939 -} // namespace esphome +} // namespace esphome::bl0939 diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 31625ebf6d6..b7df603f2f4 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { static const char *const TAG = "bl0940"; @@ -274,5 +273,4 @@ void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexit LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); } -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index e0ca748a222..14cb69d0b09 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -12,8 +12,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { // Caveat: All these values are big endian (low - middle - high) struct DataPacket { @@ -148,5 +147,4 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { void recalibrate_(); }; -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0940/button/calibration_reset_button.cpp b/esphome/components/bl0940/button/calibration_reset_button.cpp index 79a6b872d82..a0b41b9a5bb 100644 --- a/esphome/components/bl0940/button/calibration_reset_button.cpp +++ b/esphome/components/bl0940/button/calibration_reset_button.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { static const char *const TAG = "bl0940.button.calibration_reset"; @@ -16,5 +15,4 @@ void CalibrationResetButton::press_action() { this->parent_->reset_calibration(); } -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0940/button/calibration_reset_button.h b/esphome/components/bl0940/button/calibration_reset_button.h index 6ea3b35cb49..d528992d586 100644 --- a/esphome/components/bl0940/button/calibration_reset_button.h +++ b/esphome/components/bl0940/button/calibration_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { class BL0940; // Forward declaration of BL0940 class @@ -15,5 +14,4 @@ class CalibrationResetButton : public button::Button, public Component, public P void press_action() override; }; -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index 5e775004bd9..6f054443a28 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -1,8 +1,7 @@ #include "calibration_number.h" #include "esphome/core/log.h" -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { static const char *const TAG = "bl0940.number"; @@ -25,5 +24,4 @@ void CalibrationNumber::control(float value) { void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0940/number/calibration_number.h b/esphome/components/bl0940/number/calibration_number.h index 3a19e36dc95..062890d918e 100644 --- a/esphome/components/bl0940/number/calibration_number.h +++ b/esphome/components/bl0940/number/calibration_number.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace bl0940 { +namespace esphome::bl0940 { class CalibrationNumber : public number::Number, public Component { public: @@ -22,5 +21,4 @@ class CalibrationNumber : public number::Number, public Component { ESPPreferenceObject pref_; }; -} // namespace bl0940 -} // namespace esphome +} // namespace esphome::bl0940 diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 074aff96431..1c57616c826 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -4,8 +4,7 @@ // Datasheet: https://www.belling.com.cn/media/file_object/bel_product/BL0942/datasheet/BL0942_V1.06_en.pdf -namespace esphome { -namespace bl0942 { +namespace esphome::bl0942 { static const char *const TAG = "bl0942"; @@ -210,5 +209,4 @@ void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexit LOG_SENSOR("", "Frequency", this->frequency_sensor_); } -} // namespace bl0942 -} // namespace esphome +} // namespace esphome::bl0942 diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 7604399c25d..c3668786377 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -5,8 +5,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bl0942 { +namespace esphome::bl0942 { // The BL0942 IC is "calibration-free", which means that it doesn't care // at all about calibration, and that's left to software. It measures a @@ -147,5 +146,4 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { void write_reg_(uint8_t reg, uint32_t val); void received_package_(DataPacket *data); }; -} // namespace bl0942 -} // namespace esphome +} // namespace esphome::bl0942 diff --git a/esphome/components/ble_presence/ble_presence_device.cpp b/esphome/components/ble_presence/ble_presence_device.cpp index e482bb9a78d..4a70648ac52 100644 --- a/esphome/components/ble_presence/ble_presence_device.cpp +++ b/esphome/components/ble_presence/ble_presence_device.cpp @@ -3,14 +3,12 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_presence { +namespace esphome::ble_presence { static const char *const TAG = "ble_presence"; void BLEPresenceDevice::dump_config() { LOG_BINARY_SENSOR("", "BLE Presence", this); } -} // namespace ble_presence -} // namespace esphome +} // namespace esphome::ble_presence #endif diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 8ae5edab3ad..76e80799485 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_presence { +namespace esphome::ble_presence { class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public esp32_ble_tracker::ESPBTDeviceListener, @@ -137,7 +136,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, uint32_t timeout_{}; }; -} // namespace ble_presence -} // namespace esphome +} // namespace esphome::ble_presence #endif diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.cpp b/esphome/components/ble_rssi/ble_rssi_sensor.cpp index 4b37fcc6ef5..f678865f47c 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.cpp +++ b/esphome/components/ble_rssi/ble_rssi_sensor.cpp @@ -3,14 +3,12 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_rssi { +namespace esphome::ble_rssi { static const char *const TAG = "ble_rssi"; void BLERSSISensor::dump_config() { LOG_SENSOR("", "BLE RSSI Sensor", this); } -} // namespace ble_rssi -} // namespace esphome +} // namespace esphome::ble_rssi #endif diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 81f21c94ddb..a876fa51d27 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_rssi { +namespace esphome::ble_rssi { class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: @@ -120,7 +119,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi bool check_ibeacon_minor_; }; -} // namespace ble_rssi -} // namespace esphome +} // namespace esphome::ble_rssi #endif diff --git a/esphome/components/ble_scanner/ble_scanner.cpp b/esphome/components/ble_scanner/ble_scanner.cpp index f2cda227bb3..d85894edc8a 100644 --- a/esphome/components/ble_scanner/ble_scanner.cpp +++ b/esphome/components/ble_scanner/ble_scanner.cpp @@ -3,14 +3,12 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_scanner { +namespace esphome::ble_scanner { static const char *const TAG = "ble_scanner"; void BLEScanner::dump_config() { LOG_TEXT_SENSOR("", "BLE Scanner", this); } -} // namespace ble_scanner -} // namespace esphome +} // namespace esphome::ble_scanner #endif diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index c6d7f24ccec..c2d48741b1e 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_scanner { +namespace esphome::ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: @@ -43,7 +42,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP void dump_config() override; }; -} // namespace ble_scanner -} // namespace esphome +} // namespace esphome::ble_scanner #endif diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index f31940df106..0f7e42cce3e 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -9,8 +9,7 @@ #define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" -namespace esphome { -namespace bme280_base { +namespace esphome::bme280_base { static const char *const TAG = "bme280.sensor"; @@ -355,5 +354,4 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) { } int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } -} // namespace bme280_base -} // namespace esphome +} // namespace esphome::bme280_base diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 00781d05b20..7fe5f7401da 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bme280_base { +namespace esphome::bme280_base { /// Internal struct storing the calibration values of an BME280. struct BME280CalibrationData { @@ -109,5 +108,4 @@ class BME280Component : public PollingComponent { } error_code_{NONE}; }; -} // namespace bme280_base -} // namespace esphome +} // namespace esphome::bme280_base diff --git a/esphome/components/bme280_i2c/bme280_i2c.cpp b/esphome/components/bme280_i2c/bme280_i2c.cpp index e29675b5b75..f4380b0d343 100644 --- a/esphome/components/bme280_i2c/bme280_i2c.cpp +++ b/esphome/components/bme280_i2c/bme280_i2c.cpp @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "../bme280_base/bme280_base.h" -namespace esphome { -namespace bme280_i2c { +namespace esphome::bme280_i2c { bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { return I2CDevice::read_byte(a_register, data); @@ -26,5 +25,4 @@ void BME280I2CComponent::dump_config() { BME280Component::dump_config(); } -} // namespace bme280_i2c -} // namespace esphome +} // namespace esphome::bme280_i2c diff --git a/esphome/components/bme280_i2c/bme280_i2c.h b/esphome/components/bme280_i2c/bme280_i2c.h index c5e2f7e342a..ad4a283fc78 100644 --- a/esphome/components/bme280_i2c/bme280_i2c.h +++ b/esphome/components/bme280_i2c/bme280_i2c.h @@ -3,8 +3,7 @@ #include "esphome/components/bme280_base/bme280_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bme280_i2c { +namespace esphome::bme280_i2c { static const char *const TAG = "bme280_i2c.sensor"; @@ -16,5 +15,4 @@ class BME280I2CComponent : public esphome::bme280_base::BME280Component, public void dump_config() override; }; -} // namespace bme280_i2c -} // namespace esphome +} // namespace esphome::bme280_i2c diff --git a/esphome/components/bme280_spi/bme280_spi.cpp b/esphome/components/bme280_spi/bme280_spi.cpp index c6ebfdfd0b6..a4e7b7d95c3 100644 --- a/esphome/components/bme280_spi/bme280_spi.cpp +++ b/esphome/components/bme280_spi/bme280_spi.cpp @@ -4,8 +4,7 @@ #include "bme280_spi.h" #include <esphome/components/bme280_base/bme280_base.h> -namespace esphome { -namespace bme280_spi { +namespace esphome::bme280_spi { uint8_t set_bit(uint8_t num, int position) { int mask = 1 << position; @@ -61,5 +60,4 @@ bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { return true; } -} // namespace bme280_spi -} // namespace esphome +} // namespace esphome::bme280_spi diff --git a/esphome/components/bme280_spi/bme280_spi.h b/esphome/components/bme280_spi/bme280_spi.h index b6b8997fa76..4e842e9596c 100644 --- a/esphome/components/bme280_spi/bme280_spi.h +++ b/esphome/components/bme280_spi/bme280_spi.h @@ -3,8 +3,7 @@ #include "esphome/components/bme280_base/bme280_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace bme280_spi { +namespace esphome::bme280_spi { class BME280SPIComponent : public esphome::bme280_base::BME280Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -16,5 +15,4 @@ class BME280SPIComponent : public esphome::bme280_base::BME280Component, bool read_byte_16(uint8_t a_register, uint16_t *data) override; }; -} // namespace bme280_spi -} // namespace esphome +} // namespace esphome::bme280_spi diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index b599d64c0da..ef98174e064 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace bme680 { +namespace esphome::bme680 { static const char *const TAG = "bme680.sensor"; @@ -508,5 +507,4 @@ void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_du this->heater_duration_ = heater_duration; } -} // namespace bme680 -} // namespace esphome +} // namespace esphome::bme680 diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index 239823fa8c9..e40daf87201 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bme680 { +namespace esphome::bme680 { /// Enum listing all IIR Filter options for the BME680. enum BME680IIRFilter { @@ -134,5 +133,4 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *gas_resistance_sensor_{nullptr}; }; -} // namespace bme680 -} // namespace esphome +} // namespace esphome::bme680 diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index bb0417b823d..b7f8c0da77b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <string> -namespace esphome { -namespace bme680_bsec { +namespace esphome::bme680_bsec { #ifdef USE_BSEC static const char *const TAG = "bme680_bsec.sensor"; @@ -565,5 +564,4 @@ void BME680BSECComponent::save_state_(uint8_t accuracy) { ESP_LOGI(TAG, "Saved state"); } #endif -} // namespace bme680_bsec -} // namespace esphome +} // namespace esphome::bme680_bsec diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 22aa2789e6b..742b07b59bf 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -13,8 +13,7 @@ #include <bsec.h> #endif -namespace esphome { -namespace bme680_bsec { +namespace esphome::bme680_bsec { #ifdef USE_BSEC enum IAQMode { @@ -133,5 +132,4 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { sensor::Sensor *breath_voc_equivalent_sensor_{nullptr}; }; #endif -} // namespace bme680_bsec -} // namespace esphome +} // namespace esphome::bme680_bsec diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index ed92979d24e..442ee183bf9 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace bmi160 { +namespace esphome::bmi160 { static const char *const TAG = "bmi160"; static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100; @@ -265,5 +264,4 @@ void BMI160Component::update() { this->status_clear_warning(); } -} // namespace bmi160 -} // namespace esphome +} // namespace esphome::bmi160 diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h index 16cab697330..e86c353eaa9 100644 --- a/esphome/components/bmi160/bmi160.h +++ b/esphome/components/bmi160/bmi160.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bmi160 { +namespace esphome::bmi160 { class BMI160Component : public PollingComponent, public i2c::I2CDevice { public: @@ -38,5 +37,4 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice { i2c::ErrorCode read_le_int16_(uint8_t reg, int16_t *value, uint8_t len); }; -} // namespace bmi160 -} // namespace esphome +} // namespace esphome::bmi160 diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 9a383b26546..f42875b2089 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -1,8 +1,7 @@ #include "bmp085.h" #include "esphome/core/log.h" -namespace esphome { -namespace bmp085 { +namespace esphome::bmp085 { static const char *const TAG = "bmp085.sensor"; @@ -132,5 +131,4 @@ bool BMP085Component::set_mode_(uint8_t mode) { return this->write_byte(BMP085_REGISTER_CONTROL, mode); } -} // namespace bmp085 -} // namespace esphome +} // namespace esphome::bmp085 diff --git a/esphome/components/bmp085/bmp085.h b/esphome/components/bmp085/bmp085.h index c7315827e0e..a64f3936f0a 100644 --- a/esphome/components/bmp085/bmp085.h +++ b/esphome/components/bmp085/bmp085.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bmp085 { +namespace esphome::bmp085 { class BMP085Component : public PollingComponent, public i2c::I2CDevice { public: @@ -39,5 +38,4 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice { CalibrationData calibration_; }; -} // namespace bmp085 -} // namespace esphome +} // namespace esphome::bmp085 diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 603966a2b52..1dae5a689e6 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -4,8 +4,7 @@ #define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" -namespace esphome { -namespace bmp280_base { +namespace esphome::bmp280_base { static const char *const TAG = "bmp280.sensor"; @@ -268,5 +267,4 @@ uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } -} // namespace bmp280_base -} // namespace esphome +} // namespace esphome::bmp280_base diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index 836eafaf8b6..3bf1edab043 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bmp280_base { +namespace esphome::bmp280_base { /// Internal struct storing the calibration values of an BMP280. struct BMP280CalibrationData { @@ -93,5 +92,4 @@ class BMP280Component : public PollingComponent { } error_code_{NONE}; }; -} // namespace bmp280_base -} // namespace esphome +} // namespace esphome::bmp280_base diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp index 75d899008d3..098d1aff8b8 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.cpp +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -2,13 +2,11 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace bmp280_i2c { +namespace esphome::bmp280_i2c { void BMP280I2CComponent::dump_config() { LOG_I2C_DEVICE(this); BMP280Component::dump_config(); } -} // namespace bmp280_i2c -} // namespace esphome +} // namespace esphome::bmp280_i2c diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h index 0ac956202b9..bf1c2fd6247 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.h +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -3,8 +3,7 @@ #include "esphome/components/bmp280_base/bmp280_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bmp280_i2c { +namespace esphome::bmp280_i2c { static const char *const TAG = "bmp280_i2c.sensor"; @@ -20,5 +19,4 @@ class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public void dump_config() override; }; -} // namespace bmp280_i2c -} // namespace esphome +} // namespace esphome::bmp280_i2c diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp index 88983e77c37..04f92f9b899 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.cpp +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -4,8 +4,7 @@ #include "bmp280_spi.h" #include <esphome/components/bmp280_base/bmp280_base.h> -namespace esphome { -namespace bmp280_spi { +namespace esphome::bmp280_spi { uint8_t set_bit(uint8_t num, uint8_t position) { uint8_t mask = 1 << position; @@ -61,5 +60,4 @@ bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) { return true; } -} // namespace bmp280_spi -} // namespace esphome +} // namespace esphome::bmp280_spi diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h index 1bb7678e55b..17d39998849 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.h +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -3,8 +3,7 @@ #include "esphome/components/bmp280_base/bmp280_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace bmp280_spi { +namespace esphome::bmp280_spi { class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -16,5 +15,4 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override; }; -} // namespace bmp280_spi -} // namespace esphome +} // namespace esphome::bmp280_spi diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index c781252de3a..aa5e799db16 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -11,8 +11,7 @@ #include "esphome/core/progmem.h" #include <cinttypes> -namespace esphome { -namespace bmp3xx_base { +namespace esphome::bmp3xx_base { static const char *const TAG = "bmp3xx.sensor"; @@ -356,5 +355,4 @@ float BMP3XXComponent::bmp388_compensate_pressure_(float uncomp_press, float t_l return partial_out1 + partial_out2 + partial_data4; } -} // namespace bmp3xx_base -} // namespace esphome +} // namespace esphome::bmp3xx_base diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h index 8d2312231b9..cd70b2fb163 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.h +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -10,8 +10,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace bmp3xx_base { +namespace esphome::bmp3xx_base { static const uint8_t BMP388_ID = 0x50; // The BMP388 device ID static const uint8_t BMP390_ID = 0x60; // The BMP390 device ID @@ -237,5 +236,4 @@ class BMP3XXComponent : public PollingComponent { virtual bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; }; -} // namespace bmp3xx_base -} // namespace esphome +} // namespace esphome::bmp3xx_base diff --git a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp index 75310901850..704d415993d 100644 --- a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp +++ b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.cpp @@ -2,8 +2,7 @@ #include "bmp3xx_i2c.h" #include <cinttypes> -namespace esphome { -namespace bmp3xx_i2c { +namespace esphome::bmp3xx_i2c { static const char *const TAG = "bmp3xx_i2c.sensor"; @@ -25,5 +24,4 @@ void BMP3XXI2CComponent::dump_config() { BMP3XXComponent::dump_config(); } -} // namespace bmp3xx_i2c -} // namespace esphome +} // namespace esphome::bmp3xx_i2c diff --git a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h index d8b95cf8432..bec99cf9f8b 100644 --- a/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h +++ b/esphome/components/bmp3xx_i2c/bmp3xx_i2c.h @@ -2,8 +2,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/bmp3xx_base/bmp3xx_base.h" -namespace esphome { -namespace bmp3xx_i2c { +namespace esphome::bmp3xx_i2c { class BMP3XXI2CComponent : public bmp3xx_base::BMP3XXComponent, public i2c::I2CDevice { bool read_byte(uint8_t a_register, uint8_t *data) override; @@ -13,5 +12,4 @@ class BMP3XXI2CComponent : public bmp3xx_base::BMP3XXComponent, public i2c::I2CD void dump_config() override; }; -} // namespace bmp3xx_i2c -} // namespace esphome +} // namespace esphome::bmp3xx_i2c diff --git a/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp b/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp index 20845301259..5657626c2d7 100644 --- a/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp +++ b/esphome/components/bmp3xx_spi/bmp3xx_spi.cpp @@ -1,8 +1,7 @@ #include "bmp3xx_spi.h" #include <cinttypes> -namespace esphome { -namespace bmp3xx_spi { +namespace esphome::bmp3xx_spi { static const char *const TAG = "bmp3xx_spi.sensor"; @@ -53,5 +52,4 @@ bool BMP3XXSPIComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t l return true; } -} // namespace bmp3xx_spi -} // namespace esphome +} // namespace esphome::bmp3xx_spi diff --git a/esphome/components/bmp3xx_spi/bmp3xx_spi.h b/esphome/components/bmp3xx_spi/bmp3xx_spi.h index 2183994abe5..fa0c0e1b477 100644 --- a/esphome/components/bmp3xx_spi/bmp3xx_spi.h +++ b/esphome/components/bmp3xx_spi/bmp3xx_spi.h @@ -2,8 +2,7 @@ #include "esphome/components/bmp3xx_base/bmp3xx_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace bmp3xx_spi { +namespace esphome::bmp3xx_spi { class BMP3XXSPIComponent : public bmp3xx_base::BMP3XXComponent, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -15,5 +14,4 @@ class BMP3XXSPIComponent : public bmp3xx_base::BMP3XXComponent, bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; }; -} // namespace bmp3xx_spi -} // namespace esphome +} // namespace esphome::bmp3xx_spi diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index d5516384ff3..95a6b3be457 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -1,8 +1,7 @@ #include "bp1658cj.h" #include "esphome/core/log.h" -namespace esphome { -namespace bp1658cj { +namespace esphome::bp1658cj { static const char *const TAG = "bp1658cj"; @@ -128,5 +127,4 @@ void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { delayMicroseconds(BP1658CJ_DELAY); } -} // namespace bp1658cj -} // namespace esphome +} // namespace esphome::bp1658cj diff --git a/esphome/components/bp1658cj/bp1658cj.h b/esphome/components/bp1658cj/bp1658cj.h index 778f49b3e91..8905642ec46 100644 --- a/esphome/components/bp1658cj/bp1658cj.h +++ b/esphome/components/bp1658cj/bp1658cj.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace bp1658cj { +namespace esphome::bp1658cj { class BP1658CJ : public Component { public: @@ -60,5 +59,4 @@ class BP1658CJ : public Component { bool update_{true}; }; -} // namespace bp1658cj -} // namespace esphome +} // namespace esphome::bp1658cj diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp index 4f330b9c773..3cd841b2cd6 100644 --- a/esphome/components/bp5758d/bp5758d.cpp +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -1,8 +1,7 @@ #include "bp5758d.h" #include "esphome/core/log.h" -namespace esphome { -namespace bp5758d { +namespace esphome::bp5758d { static const char *const TAG = "bp5758d"; @@ -164,5 +163,4 @@ void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) { delayMicroseconds(BP5758D_DELAY); } -} // namespace bp5758d -} // namespace esphome +} // namespace esphome::bp5758d diff --git a/esphome/components/bp5758d/bp5758d.h b/esphome/components/bp5758d/bp5758d.h index cc7cc3d5f8a..f07d51fe518 100644 --- a/esphome/components/bp5758d/bp5758d.h +++ b/esphome/components/bp5758d/bp5758d.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace bp5758d { +namespace esphome::bp5758d { class BP5758D : public Component { public: @@ -60,5 +59,4 @@ class BP5758D : public Component { bool update_{true}; }; -} // namespace bp5758d -} // namespace esphome +} // namespace esphome::bp5758d diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index 32278dbfbd4..ff12e6157dd 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -17,8 +17,7 @@ #include "mbedtls/ccm.h" #endif -namespace esphome { -namespace bthome_mithermometer { +namespace esphome::bthome_mithermometer { static const char *const TAG = "bthome_mithermometer"; static constexpr size_t BTHOME_BINDKEY_SIZE = 16; @@ -434,7 +433,6 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD return reported; } -} // namespace bthome_mithermometer -} // namespace esphome +} // namespace esphome::bthome_mithermometer #endif diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h index ef3038ec93e..9bec8ba7a10 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.h +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace bthome_mithermometer { +namespace esphome::bthome_mithermometer { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: @@ -45,7 +44,6 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi sensor::Sensor *signal_strength_{nullptr}; }; -} // namespace bthome_mithermometer -} // namespace esphome +} // namespace esphome::bthome_mithermometer #endif diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 3c68094dbcc..f17b9873941 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -6,8 +6,7 @@ #include <cstddef> #include "esphome/core/helpers.h" -namespace esphome { -namespace bytebuffer { +namespace esphome::bytebuffer { enum Endian { LITTLE, BIG }; @@ -417,5 +416,4 @@ class ByteBuffer { size_t limit_{0}; }; -} // namespace bytebuffer -} // namespace esphome +} // namespace esphome::bytebuffer diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 66b8138f38b..d35f7a47931 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -1,7 +1,6 @@ #include "camera.h" -namespace esphome { -namespace camera { +namespace esphome::camera { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) Camera *Camera::global_camera = nullptr; @@ -18,5 +17,4 @@ Camera::Camera() { Camera *Camera::instance() { return global_camera; } -} // namespace camera -} // namespace esphome +} // namespace esphome::camera diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index 6e1fc8cc06b..bf80b42e54d 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -5,8 +5,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace camera { +namespace esphome::camera { /** Different sources for filtering. * IDLE: Camera requests to send an image to the API. @@ -134,5 +133,4 @@ class Camera : public EntityBase, public Component { static Camera *global_camera; }; -} // namespace camera -} // namespace esphome +} // namespace esphome::camera diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index ce48bfbba51..c5ba59a645a 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -2,8 +2,7 @@ #include <algorithm> #include "esphome/core/log.h" -namespace esphome { -namespace canbus { +namespace esphome::canbus { static const char *const TAG = "canbus"; @@ -103,5 +102,4 @@ void Canbus::loop() { } } -} // namespace canbus -} // namespace esphome +} // namespace esphome::canbus diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 420125e1d34..691d7384f1d 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -7,8 +7,7 @@ #include <cinttypes> #include <vector> -namespace esphome { -namespace canbus { +namespace esphome::canbus { enum Error : uint8_t { ERROR_OK = 0, @@ -177,5 +176,4 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, publ optional<bool> remote_transmission_request_{}; }; -} // namespace canbus -} // namespace esphome +} // namespace esphome::canbus diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 64bdc620cd8..125a4aba51e 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace cap1188 { +namespace esphome::cap1188 { static const char *const TAG = "cap1188"; @@ -100,5 +99,4 @@ void CAP1188Component::loop() { } } -} // namespace cap1188 -} // namespace esphome +} // namespace esphome::cap1188 diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index 297c601b050..848e6fe4305 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace cap1188 { +namespace esphome::cap1188 { enum { CAP1188_I2CADDR = 0x29, @@ -67,5 +66,4 @@ class CAP1188Component : public Component, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace cap1188 -} // namespace esphome +} // namespace esphome::cap1188 diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 183f16c5f84..365e5f64db5 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -5,8 +5,7 @@ #include "esphome/components/wifi/wifi_component.h" #include "captive_index.h" -namespace esphome { -namespace captive_portal { +namespace esphome::captive_portal { static const char *const TAG = "captive_portal"; @@ -135,6 +134,6 @@ void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); } CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace captive_portal -} // namespace esphome +} // namespace esphome::captive_portal + #endif diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 0c63a3670ab..8c8b43e608c 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -12,9 +12,7 @@ #include "esphome/core/preferences.h" #include "esphome/components/web_server_base/web_server_base.h" -namespace esphome { - -namespace captive_portal { +namespace esphome::captive_portal { class CaptivePortal : public AsyncWebHandler, public Component { public: @@ -69,6 +67,6 @@ class CaptivePortal : public AsyncWebHandler, public Component { extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace captive_portal -} // namespace esphome +} // namespace esphome::captive_portal + #endif diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 9ff01b32b21..afaeac55aaa 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ccs811 { +namespace esphome::ccs811 { static const char *const TAG = "ccs811"; @@ -186,5 +185,4 @@ void CCS811Component::dump_config() { } } -} // namespace ccs811 -} // namespace esphome +} // namespace esphome::ccs811 diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index 675ba7da97c..fde24947532 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -6,8 +6,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ccs811 { +namespace esphome::ccs811 { class CCS811Component : public PollingComponent, public i2c::I2CDevice { public: @@ -51,5 +50,4 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *temperature_{nullptr}; }; -} // namespace ccs811 -} // namespace esphome +} // namespace esphome::ccs811 diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 302c83d7d3a..c275aca75f7 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace cd74hc4067 { +namespace esphome::cd74hc4067 { static const char *const TAG = "cd74hc4067"; @@ -81,5 +80,4 @@ void CD74HC4067Sensor::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace cd74hc4067 -} // namespace esphome +} // namespace esphome::cd74hc4067 diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 76ebc1ebbe1..f41b5e294ab 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" -namespace esphome { -namespace cd74hc4067 { +namespace esphome::cd74hc4067 { class CD74HC4067Component : public Component { public: @@ -60,5 +59,4 @@ class CD74HC4067Sensor : public sensor::Sensor, public PollingComponent, public uint8_t pin_; }; -} // namespace cd74hc4067 -} // namespace esphome +} // namespace esphome::cd74hc4067 diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index fc856cd5636..85603f46f4f 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -1,8 +1,7 @@ #include "ch422g.h" #include "esphome/core/log.h" -namespace esphome { -namespace ch422g { +namespace esphome::ch422g { static const uint8_t CH422G_REG_MODE = 0x24; static const uint8_t CH422G_MODE_OUTPUT = 0x01; // enables output mode on 0-7 @@ -136,5 +135,4 @@ void CH422GGPIOPin::set_flags(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } -} // namespace ch422g -} // namespace esphome +} // namespace esphome::ch422g diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 6e6bdad64a0..f74e0c46a47 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ch422g { +namespace esphome::ch422g { class CH422GComponent : public Component, public i2c::I2CDevice { public: @@ -65,5 +64,4 @@ class CH422GGPIOPin : public GPIOPin { gpio::Flags flags_{}; }; -} // namespace ch422g -} // namespace esphome +} // namespace esphome::ch422g diff --git a/esphome/components/chsc6x/chsc6x_touchscreen.cpp b/esphome/components/chsc6x/chsc6x_touchscreen.cpp index 941144e451d..297e7b65a2d 100644 --- a/esphome/components/chsc6x/chsc6x_touchscreen.cpp +++ b/esphome/components/chsc6x/chsc6x_touchscreen.cpp @@ -1,7 +1,6 @@ #include "chsc6x_touchscreen.h" -namespace esphome { -namespace chsc6x { +namespace esphome::chsc6x { void CHSC6XTouchscreen::setup() { if (this->interrupt_pin_ != nullptr) { @@ -42,5 +41,4 @@ void CHSC6XTouchscreen::dump_config() { LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } -} // namespace chsc6x -} // namespace esphome +} // namespace esphome::chsc6x diff --git a/esphome/components/chsc6x/chsc6x_touchscreen.h b/esphome/components/chsc6x/chsc6x_touchscreen.h index 25b79ad34af..32077b3d33c 100644 --- a/esphome/components/chsc6x/chsc6x_touchscreen.h +++ b/esphome/components/chsc6x/chsc6x_touchscreen.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace chsc6x { +namespace esphome::chsc6x { static const char *const TAG = "chsc6x.touchscreen"; @@ -30,5 +29,4 @@ class CHSC6XTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice InternalGPIOPin *interrupt_pin_{}; }; -} // namespace chsc6x -} // namespace esphome +} // namespace esphome::chsc6x diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index cc291ff17cf..a8edaae6ea8 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -1,8 +1,7 @@ #include "climate_ir.h" #include "esphome/core/log.h" -namespace esphome { -namespace climate_ir { +namespace esphome::climate_ir { static const char *const TAG = "climate_ir"; @@ -100,5 +99,4 @@ void ClimateIR::dump_config() { YESNO(this->supports_cool_)); } -} // namespace climate_ir -} // namespace esphome +} // namespace esphome::climate_ir diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index ac76d338538..6c49b310303 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -7,8 +7,7 @@ #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace climate_ir { +namespace esphome::climate_ir { /* A base for climate which works by sending (and receiving) IR codes @@ -71,5 +70,4 @@ class ClimateIR : public Component, sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace climate_ir -} // namespace esphome +} // namespace esphome::climate_ir diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 90e3d006a85..588566dd9d4 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -1,8 +1,7 @@ #include "climate_ir_lg.h" #include "esphome/core/log.h" -namespace esphome { -namespace climate_ir_lg { +namespace esphome::climate_ir_lg { static const char *const TAG = "climate.climate_ir_lg"; @@ -218,5 +217,4 @@ void LgIrClimate::calc_checksum_(uint32_t &value) { value |= (sum & mask); } -} // namespace climate_ir_lg -} // namespace esphome +} // namespace esphome::climate_ir_lg diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h index 958245279f2..a09da65ac6c 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.h +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace climate_ir_lg { +namespace esphome::climate_ir_lg { // Temperature const uint8_t TEMP_MIN = 18; // Celsius @@ -54,5 +53,4 @@ class LgIrClimate : public climate_ir::ClimateIR { climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; }; -} // namespace climate_ir_lg -} // namespace esphome +} // namespace esphome::climate_ir_lg diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index d88ea2e1da4..7e5d25b7aef 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace cm1106 { +namespace esphome::cm1106 { static const char *const TAG = "cm1106"; static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED}; @@ -107,5 +106,4 @@ void CM1106Component::dump_config() { } } -} // namespace cm1106 -} // namespace esphome +} // namespace esphome::cm1106 diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index 8c33e564578..047e91d6326 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace cm1106 { +namespace esphome::cm1106 { class CM1106Component : public PollingComponent, public uart::UARTDevice { public: @@ -34,5 +33,4 @@ template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> CM1106Component *cm1106_; }; -} // namespace cm1106 -} // namespace esphome +} // namespace esphome::cm1106 diff --git a/esphome/components/color_temperature/ct_light_output.h b/esphome/components/color_temperature/ct_light_output.h index 4ff86c8b802..a4da1011b09 100644 --- a/esphome/components/color_temperature/ct_light_output.h +++ b/esphome/components/color_temperature/ct_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/core/component.h" -namespace esphome { -namespace color_temperature { +namespace esphome::color_temperature { class CTLightOutput : public light::LightOutput { public: @@ -34,5 +33,4 @@ class CTLightOutput : public light::LightOutput { float warm_white_temperature_; }; -} // namespace color_temperature -} // namespace esphome +} // namespace esphome::color_temperature diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index b858eee4ee4..ddf1a105e0e 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -5,8 +5,7 @@ #include <cmath> -namespace esphome { -namespace combination { +namespace esphome::combination { static const char *const TAG = "combination"; @@ -267,5 +266,4 @@ void SumCombinationComponent::handle_new_value(float value) { this->publish_state(sum); } -} // namespace combination -} // namespace esphome +} // namespace esphome::combination diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 463eedc5644..34e9e4e2c65 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -6,8 +6,7 @@ #include <functional> -namespace esphome { -namespace combination { +namespace esphome::combination { class CombinationComponent : public Component, public sensor::Sensor { public: @@ -143,5 +142,4 @@ class SumCombinationComponent : public CombinationNoParameterComponent { void handle_new_value(float value) override; }; -} // namespace combination -} // namespace esphome +} // namespace esphome::combination diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index d8ea6764781..56eb43cd82b 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -2,8 +2,7 @@ #include "esphome/components/remote_base/coolix_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace coolix { +namespace esphome::coolix { static const char *const TAG = "coolix.climate"; @@ -159,5 +158,4 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei return true; } -} // namespace coolix -} // namespace esphome +} // namespace esphome::coolix diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 51ddcdf8f2f..2d8862e2b63 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace coolix { +namespace esphome::coolix { // Temperature const uint8_t COOLIX_TEMP_MIN = 17; // Celsius @@ -42,5 +41,4 @@ class CoolixClimate : public climate_ir::ClimateIR { bool send_swing_cmd_{false}; }; -} // namespace coolix -} // namespace esphome +} // namespace esphome::coolix diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp index 0d96f58750e..b084b0080a9 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "copy_binary_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.binary_sensor"; @@ -14,5 +13,4 @@ void CopyBinarySensor::setup() { void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h index fc1e368b387..a6ce705a2a4 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.h +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { public: @@ -16,5 +15,4 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { binary_sensor::BinarySensor *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/button/copy_button.cpp b/esphome/components/copy/button/copy_button.cpp index 595388775cc..297e1b0c948 100644 --- a/esphome/components/copy/button/copy_button.cpp +++ b/esphome/components/copy/button/copy_button.cpp @@ -1,8 +1,7 @@ #include "copy_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.button"; @@ -10,5 +9,4 @@ void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); } void CopyButton::press_action() { source_->press(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h index 79d5dbcf040..afd783375d9 100644 --- a/esphome/components/copy/button/copy_button.h +++ b/esphome/components/copy/button/copy_button.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyButton : public button::Button, public Component { public: @@ -17,5 +16,4 @@ class CopyButton : public button::Button, public Component { button::Button *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp index c139869d8f5..3412784cc77 100644 --- a/esphome/components/copy/cover/copy_cover.cpp +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -1,8 +1,7 @@ #include "copy_cover.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.cover"; @@ -50,5 +49,4 @@ void CopyCover::control(const cover::CoverCall &call) { call2.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h index ec27b6782aa..0b493e4c3bb 100644 --- a/esphome/components/copy/cover/copy_cover.h +++ b/esphome/components/copy/cover/copy_cover.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyCover : public cover::Cover, public Component { public: @@ -20,5 +19,4 @@ class CopyCover : public cover::Cover, public Component { cover::Cover *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index bdaa35c467b..6eed787b058 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -1,8 +1,7 @@ #include "copy_fan.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.fan"; @@ -69,5 +68,4 @@ void CopyFan::control(const fan::FanCall &call) { call2.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 988129f07b9..9090c910957 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyFan : public fan::Fan, public Component { public: @@ -21,5 +20,4 @@ class CopyFan : public fan::Fan, public Component { fan::Fan *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index c846954510f..5fdb9a757b8 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -1,8 +1,7 @@ #include "copy_lock.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.lock"; @@ -25,5 +24,4 @@ void CopyLock::control(const lock::LockCall &call) { call2.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h index 8799eebb4a2..c6c46467a9e 100644 --- a/esphome/components/copy/lock/copy_lock.h +++ b/esphome/components/copy/lock/copy_lock.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/lock/lock.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyLock : public lock::Lock, public Component { public: @@ -18,5 +17,4 @@ class CopyLock : public lock::Lock, public Component { lock::Lock *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/number/copy_number.cpp b/esphome/components/copy/number/copy_number.cpp index 46dc200b735..a10bd209f4e 100644 --- a/esphome/components/copy/number/copy_number.cpp +++ b/esphome/components/copy/number/copy_number.cpp @@ -1,8 +1,7 @@ #include "copy_number.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.number"; @@ -25,5 +24,4 @@ void CopyNumber::control(float value) { call2.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h index 09b65e2cbf2..b4d8bb83e68 100644 --- a/esphome/components/copy/number/copy_number.h +++ b/esphome/components/copy/number/copy_number.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/number/number.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyNumber : public number::Number, public Component { public: @@ -18,5 +17,4 @@ class CopyNumber : public number::Number, public Component { number::Number *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index 227fe33182b..e4f97f3c162 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -1,8 +1,7 @@ #include "copy_select.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.select"; @@ -24,5 +23,4 @@ void CopySelect::control(size_t index) { call.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index bd74a93e820..1a17c7a55aa 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopySelect : public select::Select, public Component { public: @@ -18,5 +17,4 @@ class CopySelect : public select::Select, public Component { select::Select *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/sensor/copy_sensor.cpp b/esphome/components/copy/sensor/copy_sensor.cpp index a47a0cf22b4..9df23ab92d0 100644 --- a/esphome/components/copy/sensor/copy_sensor.cpp +++ b/esphome/components/copy/sensor/copy_sensor.cpp @@ -1,8 +1,7 @@ #include "copy_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.sensor"; @@ -14,5 +13,4 @@ void CopySensor::setup() { void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h index 500e6872fe2..d6e5026ce13 100644 --- a/esphome/components/copy/sensor/copy_sensor.h +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopySensor : public sensor::Sensor, public Component { public: @@ -16,5 +15,4 @@ class CopySensor : public sensor::Sensor, public Component { sensor::Sensor *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/switch/copy_switch.cpp b/esphome/components/copy/switch/copy_switch.cpp index 8a9fbb03dd0..91b76f11c0a 100644 --- a/esphome/components/copy/switch/copy_switch.cpp +++ b/esphome/components/copy/switch/copy_switch.cpp @@ -1,8 +1,7 @@ #include "copy_switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.switch"; @@ -22,5 +21,4 @@ void CopySwitch::write_state(bool state) { } } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h index 80310af03f0..9ce6b48ed14 100644 --- a/esphome/components/copy/switch/copy_switch.h +++ b/esphome/components/copy/switch/copy_switch.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopySwitch : public switch_::Switch, public Component { public: @@ -18,5 +17,4 @@ class CopySwitch : public switch_::Switch, public Component { switch_::Switch *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/text/copy_text.cpp b/esphome/components/copy/text/copy_text.cpp index cb742013839..5ec3ba05b7c 100644 --- a/esphome/components/copy/text/copy_text.cpp +++ b/esphome/components/copy/text/copy_text.cpp @@ -1,8 +1,7 @@ #include "copy_text.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.text"; @@ -21,5 +20,4 @@ void CopyText::control(const std::string &value) { call2.perform(); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h index 9eaebae4bea..ad289365227 100644 --- a/esphome/components/copy/text/copy_text.h +++ b/esphome/components/copy/text/copy_text.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text/text.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyText : public text::Text, public Component { public: @@ -18,5 +17,4 @@ class CopyText : public text::Text, public Component { text::Text *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.cpp b/esphome/components/copy/text_sensor/copy_text_sensor.cpp index 95fa6d7a1bd..97182e8a618 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.cpp +++ b/esphome/components/copy/text_sensor/copy_text_sensor.cpp @@ -1,8 +1,7 @@ #include "copy_text_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace copy { +namespace esphome::copy { static const char *const TAG = "copy.text_sensor"; @@ -14,5 +13,4 @@ void CopyTextSensor::setup() { void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); } -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h index 489986c59d6..dc4ef7a29da 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.h +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace copy { +namespace esphome::copy { class CopyTextSensor : public text_sensor::TextSensor, public Component { public: @@ -16,5 +15,4 @@ class CopyTextSensor : public text_sensor::TextSensor, public Component { text_sensor::TextSensor *source_; }; -} // namespace copy -} // namespace esphome +} // namespace esphome::copy diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index e026eccf80e..c9e8f3cf472 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -1,8 +1,7 @@ #include "cs5460a.h" #include "esphome/core/log.h" -namespace esphome { -namespace cs5460a { +namespace esphome::cs5460a { static const char *const TAG = "cs5460a"; @@ -339,5 +338,4 @@ void CS5460AComponent::dump_config() { LOG_SENSOR(" ", "Power", power_sensor_); } -} // namespace cs5460a -} // namespace esphome +} // namespace esphome::cs5460a diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 99c30175101..c6b02f53ee7 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -7,8 +7,7 @@ #include <cinttypes> -namespace esphome { -namespace cs5460a { +namespace esphome::cs5460a { enum CS5460ACommand { CMD_SYNC0 = 0xfe, @@ -119,5 +118,4 @@ template<typename... Ts> class CS5460ARestartAction : public Action<Ts...> { CS5460AComponent *cs5460a_; }; -} // namespace cs5460a -} // namespace esphome +} // namespace esphome::cs5460a diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 0ecaaced7f6..42517515314 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace cse7761 { +namespace esphome::cse7761 { static const char *const TAG = "cse7761"; @@ -244,5 +243,4 @@ void CSE7761Component::get_data_() { } } -} // namespace cse7761 -} // namespace esphome +} // namespace esphome::cse7761 diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 0e03171956f..5f683f424bd 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -4,8 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" -namespace esphome { -namespace cse7761 { +namespace esphome::cse7761 { struct CSE7761DataStruct { uint32_t frequency = 0; @@ -45,5 +44,4 @@ class CSE7761Component : public PollingComponent, public uart::UARTDevice { void get_data_(); }; -} // namespace cse7761 -} // namespace esphome +} // namespace esphome::cse7761 diff --git a/esphome/components/cst226/binary_sensor/cs226_button.h b/esphome/components/cst226/binary_sensor/cs226_button.h index 6d409df04ff..e7e334b9bbf 100644 --- a/esphome/components/cst226/binary_sensor/cs226_button.h +++ b/esphome/components/cst226/binary_sensor/cs226_button.h @@ -4,8 +4,7 @@ #include "../touchscreen/cst226_touchscreen.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace cst226 { +namespace esphome::cst226 { class CST226Button : public binary_sensor::BinarySensor, public Component, @@ -18,5 +17,4 @@ class CST226Button : public binary_sensor::BinarySensor, void update_button(bool state) override; }; -} // namespace cst226 -} // namespace esphome +} // namespace esphome::cst226 diff --git a/esphome/components/cst226/binary_sensor/cstt6_button.cpp b/esphome/components/cst226/binary_sensor/cstt6_button.cpp index c481ce5d570..c14da2b176a 100644 --- a/esphome/components/cst226/binary_sensor/cstt6_button.cpp +++ b/esphome/components/cst226/binary_sensor/cstt6_button.cpp @@ -1,8 +1,7 @@ #include "cs226_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace cst226 { +namespace esphome::cst226 { static const char *const TAG = "CST226.binary_sensor"; @@ -15,5 +14,4 @@ void CST226Button::dump_config() { LOG_BINARY_SENSOR("", "CST226 Button", this); void CST226Button::update_button(bool state) { this->publish_state(state); } -} // namespace cst226 -} // namespace esphome +} // namespace esphome::cst226 diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp index e65997b7fce..49d61cbfbca 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -1,7 +1,6 @@ #include "cst226_touchscreen.h" -namespace esphome { -namespace cst226 { +namespace esphome::cst226 { static const char *const TAG = "cst226.touchscreen"; @@ -110,5 +109,4 @@ void CST226Touchscreen::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); } -} // namespace cst226 -} // namespace esphome +} // namespace esphome::cst226 diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.h b/esphome/components/cst226/touchscreen/cst226_touchscreen.h index c744e51fecb..362eee5fc2d 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.h +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace cst226 { +namespace esphome::cst226 { static const uint8_t CST226_REG_STATUS = 0x00; @@ -40,5 +39,4 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice bool button_touched_{}; }; -} // namespace cst226 -} // namespace esphome +} // namespace esphome::cst226 diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index d18d4e7c94c..50ebebbbb5b 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -1,8 +1,7 @@ #include "cst816_touchscreen.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace cst816 { +namespace esphome::cst816 { void CST816Touchscreen::continue_setup_() { if (this->interrupt_pin_ != nullptr) { @@ -121,5 +120,4 @@ void CST816Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, " Chip type: %s", name); } -} // namespace cst816 -} // namespace esphome +} // namespace esphome::cst816 diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.h b/esphome/components/cst816/touchscreen/cst816_touchscreen.h index 99b93d8342f..19c169c3ecb 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.h +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace cst816 { +namespace esphome::cst816 { static const char *const TAG = "cst816.touchscreen"; @@ -57,5 +56,4 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice bool skip_probe_{}; // if set, do not expect to be able to probe the controller on the i2c bus. }; -} // namespace cst816 -} // namespace esphome +} // namespace esphome::cst816 diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 0aa0258a9b6..613c3428be3 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -4,8 +4,7 @@ #include <cinttypes> #include <cmath> -namespace esphome { -namespace ct_clamp { +namespace esphome::ct_clamp { static const char *const TAG = "ct_clamp"; @@ -70,5 +69,4 @@ void CTClampSensor::loop() { this->sample_squared_sum_ += value * value; } -} // namespace ct_clamp -} // namespace esphome +} // namespace esphome::ct_clamp diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h index db4dc1ea57e..2055edbd3eb 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.h +++ b/esphome/components/ct_clamp/ct_clamp_sensor.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" -namespace esphome { -namespace ct_clamp { +namespace esphome::ct_clamp { class CTClampSensor : public sensor::Sensor, public PollingComponent { public: @@ -50,5 +49,4 @@ class CTClampSensor : public sensor::Sensor, public PollingComponent { bool is_sampling_ = false; }; -} // namespace ct_clamp -} // namespace esphome +} // namespace esphome::ct_clamp diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 13bf11b9912..5a499d54a41 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -4,8 +4,7 @@ #include "esphome/core/application.h" #include <cfloat> -namespace esphome { -namespace current_based { +namespace esphome::current_based { static const char *const TAG = "current_based.cover"; @@ -271,5 +270,4 @@ void CurrentBasedCover::recompute_position_() { this->last_recompute_time_ = now; } -} // namespace current_based -} // namespace esphome +} // namespace esphome::current_based diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 40b39517e4e..531f8d5a4f5 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include <cfloat> -namespace esphome { -namespace current_based { +namespace esphome::current_based { class CurrentBasedCover : public cover::Cover, public Component { public: @@ -92,5 +91,4 @@ class CurrentBasedCover : public cover::Cover, public Component { cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; -} // namespace current_based -} // namespace esphome +} // namespace esphome::current_based diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 2b7698ce5aa..6eed8de7ccc 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace cwww { +namespace esphome::cwww { class CWWWLightOutput : public light::LightOutput { public: @@ -36,5 +35,4 @@ class CWWWLightOutput : public light::LightOutput { bool constant_brightness_; }; -} // namespace cwww -} // namespace esphome +} // namespace esphome::cwww From 4e4e4b4411e09a0c5cfa68bc4309067ba81987ba Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 16:49:55 -0400 Subject: [PATCH 1892/2056] [clang-tidy] Concatenate nested namespaces (2/7: components d-h) (#16295) --- esphome/components/dac7678/dac7678_output.cpp | 6 ++---- esphome/components/dac7678/dac7678_output.h | 6 ++---- esphome/components/daikin/daikin.cpp | 6 ++---- esphome/components/daikin/daikin.h | 6 ++---- esphome/components/daikin_arc/daikin_arc.cpp | 6 ++---- esphome/components/daikin_arc/daikin_arc.h | 6 ++---- esphome/components/daikin_brc/daikin_brc.cpp | 6 ++---- esphome/components/daikin_brc/daikin_brc.h | 6 ++---- esphome/components/dallas_temp/dallas_temp.cpp | 6 ++---- esphome/components/dallas_temp/dallas_temp.h | 6 ++---- esphome/components/daly_bms/daly_bms.cpp | 6 ++---- esphome/components/daly_bms/daly_bms.h | 6 ++---- esphome/components/dashboard_import/dashboard_import.cpp | 6 ++---- esphome/components/dashboard_import/dashboard_import.h | 6 ++---- esphome/components/debug/debug_component.cpp | 6 ++---- esphome/components/debug/debug_component.h | 6 ++---- esphome/components/debug/debug_esp32.cpp | 7 +++---- esphome/components/deep_sleep/deep_sleep_component.cpp | 6 ++---- esphome/components/deep_sleep/deep_sleep_component.h | 6 ++---- esphome/components/deep_sleep/deep_sleep_esp32.cpp | 7 +++---- esphome/components/delonghi/delonghi.cpp | 6 ++---- esphome/components/delonghi/delonghi.h | 6 ++---- esphome/components/demo/demo_alarm_control_panel.h | 6 ++---- esphome/components/demo/demo_binary_sensor.h | 6 ++---- esphome/components/demo/demo_button.h | 6 ++---- esphome/components/demo/demo_climate.h | 6 ++---- esphome/components/demo/demo_cover.h | 6 ++---- esphome/components/demo/demo_date.h | 6 ++---- esphome/components/demo/demo_datetime.h | 6 ++---- esphome/components/demo/demo_fan.h | 6 ++---- esphome/components/demo/demo_light.h | 6 ++---- esphome/components/demo/demo_lock.h | 6 ++---- esphome/components/demo/demo_number.h | 6 ++---- esphome/components/demo/demo_select.h | 6 ++---- esphome/components/demo/demo_sensor.h | 6 ++---- esphome/components/demo/demo_switch.h | 6 ++---- esphome/components/demo/demo_text.h | 6 ++---- esphome/components/demo/demo_text_sensor.h | 6 ++---- esphome/components/demo/demo_time.h | 6 ++---- esphome/components/demo/demo_valve.h | 6 ++---- esphome/components/dfplayer/dfplayer.cpp | 6 ++---- esphome/components/dfplayer/dfplayer.h | 6 ++---- esphome/components/dfrobot_sen0395/automation.h | 6 ++---- esphome/components/dfrobot_sen0395/commands.cpp | 6 ++---- esphome/components/dfrobot_sen0395/commands.h | 6 ++---- esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp | 6 ++---- esphome/components/dfrobot_sen0395/dfrobot_sen0395.h | 6 ++---- .../dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp | 6 ++---- .../dfrobot_sen0395/switch/dfrobot_sen0395_switch.h | 6 ++---- esphome/components/dht12/dht12.cpp | 6 ++---- esphome/components/dht12/dht12.h | 6 ++---- esphome/components/display/display.cpp | 6 ++---- esphome/components/display/display.h | 6 ++---- esphome/components/display/display_buffer.cpp | 6 ++---- esphome/components/display/display_buffer.h | 6 ++---- esphome/components/display/display_color_utils.h | 6 ++---- esphome/components/display/rect.cpp | 6 ++---- esphome/components/display/rect.h | 6 ++---- esphome/components/display_menu_base/automation.h | 6 ++---- .../components/display_menu_base/display_menu_base.cpp | 6 ++---- esphome/components/display_menu_base/display_menu_base.h | 6 ++---- esphome/components/display_menu_base/menu_item.cpp | 6 ++---- esphome/components/display_menu_base/menu_item.h | 6 ++---- esphome/components/dps310/dps310.cpp | 6 ++---- esphome/components/dps310/dps310.h | 6 ++---- esphome/components/ds1307/ds1307.cpp | 6 ++---- esphome/components/ds1307/ds1307.h | 6 ++---- esphome/components/ds2484/ds2484.cpp | 6 ++---- esphome/components/ds2484/ds2484.h | 6 ++---- esphome/components/duty_cycle/duty_cycle_sensor.cpp | 6 ++---- esphome/components/duty_cycle/duty_cycle_sensor.h | 6 ++---- esphome/components/duty_time/duty_time_sensor.cpp | 6 ++---- esphome/components/duty_time/duty_time_sensor.h | 6 ++---- esphome/components/e131/e131.cpp | 7 +++---- esphome/components/e131/e131.h | 7 +++---- esphome/components/e131/e131_addressable_light_effect.cpp | 7 +++---- esphome/components/e131/e131_addressable_light_effect.h | 8 ++++---- esphome/components/e131/e131_packet.cpp | 7 +++---- esphome/components/ee895/ee895.cpp | 6 ++---- esphome/components/ee895/ee895.h | 6 ++---- esphome/components/ektf2232/touchscreen/ektf2232.cpp | 6 ++---- esphome/components/ektf2232/touchscreen/ektf2232.h | 6 ++---- esphome/components/emc2101/emc2101.cpp | 6 ++---- esphome/components/emc2101/emc2101.h | 6 ++---- esphome/components/emc2101/output/emc2101_output.cpp | 6 ++---- esphome/components/emc2101/output/emc2101_output.h | 6 ++---- esphome/components/emc2101/sensor/emc2101_sensor.cpp | 6 ++---- esphome/components/emc2101/sensor/emc2101_sensor.h | 6 ++---- esphome/components/emmeti/emmeti.cpp | 6 ++---- esphome/components/emmeti/emmeti.h | 6 ++---- esphome/components/endstop/endstop_cover.cpp | 6 ++---- esphome/components/endstop/endstop_cover.h | 6 ++---- esphome/components/ens160_base/ens160_base.cpp | 6 ++---- esphome/components/ens160_base/ens160_base.h | 6 ++---- esphome/components/ens160_i2c/ens160_i2c.cpp | 6 ++---- esphome/components/ens160_i2c/ens160_i2c.h | 6 ++---- esphome/components/ens160_spi/ens160_spi.cpp | 6 ++---- esphome/components/ens160_spi/ens160_spi.h | 6 ++---- esphome/components/ens210/ens210.cpp | 6 ++---- esphome/components/ens210/ens210.h | 6 ++---- esphome/components/es7210/es7210.cpp | 6 ++---- esphome/components/es7210/es7210.h | 6 ++---- esphome/components/es7210/es7210_const.h | 6 ++---- esphome/components/es7243e/es7243e.cpp | 6 ++---- esphome/components/es7243e/es7243e.h | 6 ++---- esphome/components/es7243e/es7243e_const.h | 6 ++---- esphome/components/es8156/es8156.cpp | 6 ++---- esphome/components/es8156/es8156.h | 6 ++---- esphome/components/es8156/es8156_const.h | 6 ++---- esphome/components/es8311/es8311.cpp | 6 ++---- esphome/components/es8311/es8311.h | 6 ++---- esphome/components/es8311/es8311_const.h | 6 ++---- esphome/components/es8388/es8388.cpp | 6 ++---- esphome/components/es8388/es8388.h | 6 ++---- esphome/components/es8388/es8388_const.h | 6 ++---- esphome/components/es8388/select/adc_input_mic_select.cpp | 6 ++---- esphome/components/es8388/select/adc_input_mic_select.h | 6 ++---- esphome/components/es8388/select/dac_output_select.cpp | 6 ++---- esphome/components/es8388/select/dac_output_select.h | 6 ++---- esphome/components/esp32/gpio.h | 6 ++---- esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp | 6 ++---- esphome/components/esp32_ble_beacon/esp32_ble_beacon.h | 6 ++---- esphome/components/esp32_ble_server/ble_2902.cpp | 6 ++---- esphome/components/esp32_ble_server/ble_2902.h | 6 ++---- .../components/esp32_ble_server/ble_characteristic.cpp | 6 ++---- esphome/components/esp32_ble_server/ble_characteristic.h | 6 ++---- esphome/components/esp32_ble_server/ble_descriptor.cpp | 6 ++---- esphome/components/esp32_ble_server/ble_descriptor.h | 6 ++---- esphome/components/esp32_ble_server/ble_server.cpp | 6 ++---- esphome/components/esp32_ble_server/ble_server.h | 6 ++---- .../esp32_ble_server/ble_server_automations.cpp | 8 ++------ .../components/esp32_ble_server/ble_server_automations.h | 8 ++------ esphome/components/esp32_ble_server/ble_service.cpp | 6 ++---- esphome/components/esp32_ble_server/ble_service.h | 6 ++---- esphome/components/esp32_camera/esp32_camera.cpp | 6 ++---- esphome/components/esp32_camera/esp32_camera.h | 6 ++---- .../esp32_camera_web_server/camera_web_server.cpp | 6 ++---- .../esp32_camera_web_server/camera_web_server.h | 6 ++---- esphome/components/esp32_can/esp32_can.cpp | 6 ++---- esphome/components/esp32_can/esp32_can.h | 6 ++---- esphome/components/esp32_dac/esp32_dac.cpp | 6 ++---- esphome/components/esp32_dac/esp32_dac.h | 6 ++---- esphome/components/esp32_improv/automation.h | 7 +++---- .../components/esp32_improv/esp32_improv_component.cpp | 6 ++---- esphome/components/esp32_improv/esp32_improv_component.h | 6 ++---- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 6 ++---- esphome/components/esp32_rmt_led_strip/led_strip.h | 6 ++---- .../espnow/packet_transport/espnow_transport.cpp | 6 ++---- .../components/espnow/packet_transport/espnow_transport.h | 6 ++---- esphome/components/event/automation.h | 6 ++---- esphome/components/event/event.cpp | 6 ++---- esphome/components/event/event.h | 6 ++---- .../exposure_notifications/exposure_notifications.cpp | 6 ++---- .../exposure_notifications/exposure_notifications.h | 6 ++---- esphome/components/ezo/ezo.cpp | 6 ++---- esphome/components/ezo/ezo.h | 6 ++---- esphome/components/ezo_pmp/ezo_pmp.cpp | 6 ++---- esphome/components/ezo_pmp/ezo_pmp.h | 6 ++---- .../factory_reset/button/factory_reset_button.cpp | 6 ++---- .../factory_reset/button/factory_reset_button.h | 6 ++---- .../factory_reset/switch/factory_reset_switch.cpp | 6 ++---- .../factory_reset/switch/factory_reset_switch.h | 6 ++---- esphome/components/fan/automation.h | 6 ++---- esphome/components/fan/fan.cpp | 6 ++---- esphome/components/fan/fan.h | 6 ++---- esphome/components/fan/fan_traits.h | 7 ++----- esphome/components/fastled_base/fastled_light.cpp | 6 ++---- esphome/components/fastled_base/fastled_light.h | 6 ++---- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 6 ++---- esphome/components/fingerprint_grow/fingerprint_grow.h | 6 ++---- esphome/components/font/font.cpp | 6 ++---- esphome/components/font/font.h | 6 ++---- esphome/components/fs3000/fs3000.cpp | 6 ++---- esphome/components/fs3000/fs3000.h | 6 ++---- .../components/ft5x06/touchscreen/ft5x06_touchscreen.cpp | 6 ++---- .../components/ft5x06/touchscreen/ft5x06_touchscreen.h | 6 ++---- esphome/components/ft63x6/ft63x6.cpp | 7 +++---- esphome/components/ft63x6/ft63x6.h | 6 ++---- esphome/components/fujitsu_general/fujitsu_general.cpp | 6 ++---- esphome/components/fujitsu_general/fujitsu_general.h | 6 ++---- esphome/components/gcja5/gcja5.cpp | 6 ++---- esphome/components/gcja5/gcja5.h | 6 ++---- esphome/components/gdk101/gdk101.cpp | 6 ++---- esphome/components/gdk101/gdk101.h | 6 ++---- esphome/components/gl_r01_i2c/gl_r01_i2c.cpp | 6 ++---- esphome/components/gl_r01_i2c/gl_r01_i2c.h | 6 ++---- esphome/components/gp2y1010au0f/gp2y1010au0f.cpp | 6 ++---- esphome/components/gp2y1010au0f/gp2y1010au0f.h | 6 ++---- esphome/components/gp8403/gp8403.cpp | 6 ++---- esphome/components/gp8403/gp8403.h | 6 ++---- esphome/components/gp8403/output/gp8403_output.cpp | 6 ++---- esphome/components/gp8403/output/gp8403_output.h | 6 ++---- .../components/gpio/binary_sensor/gpio_binary_sensor.cpp | 6 ++---- .../components/gpio/binary_sensor/gpio_binary_sensor.h | 6 ++---- esphome/components/gpio/one_wire/gpio_one_wire.cpp | 6 ++---- esphome/components/gpio/one_wire/gpio_one_wire.h | 6 ++---- esphome/components/gpio/output/gpio_binary_output.cpp | 6 ++---- esphome/components/gpio/output/gpio_binary_output.h | 6 ++---- esphome/components/gpio/switch/gpio_switch.cpp | 6 ++---- esphome/components/gpio/switch/gpio_switch.h | 6 ++---- esphome/components/gps/gps.cpp | 6 ++---- esphome/components/gps/gps.h | 6 ++---- esphome/components/gps/time/gps_time.cpp | 6 ++---- esphome/components/gps/time/gps_time.h | 6 ++---- esphome/components/graph/graph.cpp | 7 +++---- .../graphical_display_menu/graphical_display_menu.cpp | 6 ++---- esphome/components/gree/gree.cpp | 6 ++---- esphome/components/gree/switch/gree_switch.cpp | 6 ++---- esphome/components/gree/switch/gree_switch.h | 6 ++---- esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp | 6 ++---- esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h | 6 ++---- esphome/components/grove_tb6612fng/grove_tb6612fng.cpp | 6 ++---- esphome/components/grove_tb6612fng/grove_tb6612fng.h | 8 +++----- esphome/components/growatt_solar/growatt_solar.cpp | 6 ++---- esphome/components/growatt_solar/growatt_solar.h | 6 ++---- esphome/components/gt911/binary_sensor/gt911_button.cpp | 6 ++---- esphome/components/gt911/binary_sensor/gt911_button.h | 6 ++---- .../components/gt911/touchscreen/gt911_touchscreen.cpp | 6 ++---- esphome/components/gt911/touchscreen/gt911_touchscreen.h | 6 ++---- esphome/components/haier/automation.h | 6 ++---- esphome/components/haier/button/self_cleaning.cpp | 6 ++---- esphome/components/haier/button/self_cleaning.h | 6 ++---- esphome/components/haier/button/steri_cleaning.cpp | 6 ++---- esphome/components/haier/button/steri_cleaning.h | 6 ++---- esphome/components/haier/haier_base.cpp | 6 ++---- esphome/components/haier/haier_base.h | 6 ++---- esphome/components/haier/hon_climate.cpp | 6 ++---- esphome/components/haier/hon_climate.h | 6 ++---- esphome/components/haier/hon_packet.h | 8 ++------ esphome/components/haier/logger_handler.cpp | 6 ++---- esphome/components/haier/logger_handler.h | 6 ++---- esphome/components/haier/smartair2_climate.cpp | 6 ++---- esphome/components/haier/smartair2_climate.h | 6 ++---- esphome/components/haier/smartair2_packet.h | 8 ++------ esphome/components/haier/switch/beeper.cpp | 6 ++---- esphome/components/haier/switch/beeper.h | 6 ++---- esphome/components/haier/switch/display.cpp | 6 ++---- esphome/components/haier/switch/display.h | 6 ++---- esphome/components/haier/switch/health_mode.cpp | 6 ++---- esphome/components/haier/switch/health_mode.h | 6 ++---- esphome/components/haier/switch/quiet_mode.cpp | 6 ++---- esphome/components/haier/switch/quiet_mode.h | 6 ++---- esphome/components/havells_solar/havells_solar.cpp | 6 ++---- esphome/components/havells_solar/havells_solar.h | 6 ++---- .../components/havells_solar/havells_solar_registers.h | 7 +++---- esphome/components/hbridge/fan/hbridge_fan.cpp | 6 ++---- esphome/components/hbridge/fan/hbridge_fan.h | 6 ++---- esphome/components/hbridge/light/hbridge_light_output.h | 6 ++---- esphome/components/hbridge/switch/hbridge_switch.cpp | 6 ++---- esphome/components/hbridge/switch/hbridge_switch.h | 6 ++---- esphome/components/hdc1080/hdc1080.cpp | 6 ++---- esphome/components/hdc1080/hdc1080.h | 6 ++---- esphome/components/hdc2010/hdc2010.cpp | 7 +++---- esphome/components/hdc2010/hdc2010.h | 6 ++---- esphome/components/he60r/he60r.cpp | 6 ++---- esphome/components/he60r/he60r.h | 6 ++---- esphome/components/heatpumpir/heatpumpir.cpp | 6 ++---- esphome/components/heatpumpir/heatpumpir.h | 6 ++---- esphome/components/hitachi_ac344/hitachi_ac344.cpp | 6 ++---- esphome/components/hitachi_ac344/hitachi_ac344.h | 6 ++---- esphome/components/hitachi_ac424/hitachi_ac424.cpp | 6 ++---- esphome/components/hitachi_ac424/hitachi_ac424.h | 6 ++---- esphome/components/hlw8012/hlw8012.cpp | 6 ++---- esphome/components/hlw8012/hlw8012.h | 6 ++---- esphome/components/hm3301/hm3301.cpp | 6 ++---- esphome/components/hm3301/hm3301.h | 6 ++---- esphome/components/hmac_md5/hmac_md5.cpp | 7 +++---- esphome/components/hmac_md5/hmac_md5.h | 7 +++---- esphome/components/hmc5883l/hmc5883l.cpp | 6 ++---- esphome/components/hmc5883l/hmc5883l.h | 6 ++---- .../binary_sensor/homeassistant_binary_sensor.cpp | 6 ++---- .../binary_sensor/homeassistant_binary_sensor.h | 6 ++---- .../homeassistant/number/homeassistant_number.cpp | 6 ++---- .../homeassistant/number/homeassistant_number.h | 6 ++---- .../homeassistant/sensor/homeassistant_sensor.cpp | 6 ++---- .../homeassistant/sensor/homeassistant_sensor.h | 6 ++---- .../homeassistant/switch/homeassistant_switch.cpp | 6 ++---- .../homeassistant/switch/homeassistant_switch.h | 6 ++---- .../text_sensor/homeassistant_text_sensor.cpp | 6 ++---- .../homeassistant/text_sensor/homeassistant_text_sensor.h | 6 ++---- .../components/homeassistant/time/homeassistant_time.cpp | 6 ++---- .../components/homeassistant/time/homeassistant_time.h | 6 ++---- esphome/components/honeywell_hih_i2c/honeywell_hih.cpp | 6 ++---- esphome/components/honeywell_hih_i2c/honeywell_hih.h | 6 ++---- esphome/components/honeywellabp/honeywellabp.cpp | 6 ++---- esphome/components/honeywellabp/honeywellabp.h | 6 ++---- esphome/components/honeywellabp2_i2c/honeywellabp2.cpp | 6 ++---- esphome/components/honeywellabp2_i2c/honeywellabp2.h | 6 ++---- esphome/components/host/time/host_time.h | 6 ++---- esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp | 6 ++---- esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h | 6 ++---- esphome/components/hte501/hte501.cpp | 6 ++---- esphome/components/hte501/hte501.h | 6 ++---- esphome/components/http_request/ota/automation.h | 6 ++---- esphome/components/http_request/ota/ota_http_request.cpp | 6 ++---- esphome/components/http_request/ota/ota_http_request.h | 6 ++---- .../http_request/update/http_request_update.cpp | 6 ++---- .../components/http_request/update/http_request_update.h | 6 ++---- esphome/components/htu21d/htu21d.cpp | 6 ++---- esphome/components/htu21d/htu21d.h | 6 ++---- esphome/components/htu31d/htu31d.cpp | 6 ++---- esphome/components/htu31d/htu31d.h | 6 ++---- esphome/components/hx711/hx711.cpp | 6 ++---- esphome/components/hx711/hx711.h | 6 ++---- esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 6 ++---- esphome/components/hydreon_rgxx/hydreon_rgxx.h | 6 ++---- esphome/components/hyt271/hyt271.cpp | 6 ++---- esphome/components/hyt271/hyt271.h | 6 ++---- 308 files changed, 632 insertions(+), 1242 deletions(-) diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 27ab54f0be9..15575583d97 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace dac7678 { +namespace esphome::dac7678 { static const char *const TAG = "dac7678"; @@ -82,5 +81,4 @@ void DAC7678Channel::write_state(float state) { this->parent_->set_channel_value_(this->channel_, input); } -} // namespace dac7678 -} // namespace esphome +} // namespace esphome::dac7678 diff --git a/esphome/components/dac7678/dac7678_output.h b/esphome/components/dac7678/dac7678_output.h index abd9875e4ce..a0173259398 100644 --- a/esphome/components/dac7678/dac7678_output.h +++ b/esphome/components/dac7678/dac7678_output.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace dac7678 { +namespace esphome::dac7678 { class DAC7678Output; @@ -51,5 +50,4 @@ class DAC7678Output : public Component, public i2c::I2CDevice { }; }; -} // namespace dac7678 -} // namespace esphome +} // namespace esphome::dac7678 diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index a285f3613db..d45586ba2dc 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -1,8 +1,7 @@ #include "daikin.h" #include "esphome/components/remote_base/remote_base.h" -namespace esphome { -namespace daikin { +namespace esphome::daikin { static const char *const TAG = "daikin.climate"; @@ -251,5 +250,4 @@ bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) { return this->parse_state_frame_(state_frame); } -} // namespace daikin -} // namespace esphome +} // namespace esphome::daikin diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index 159292cb554..9c05993f378 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace daikin { +namespace esphome::daikin { // Values for Daikin ARC43XXX IR Controllers // Temperature @@ -60,5 +59,4 @@ class DaikinClimate : public climate_ir::ClimateIR { bool parse_state_frame_(const uint8_t frame[]); }; -} // namespace daikin -} // namespace esphome +} // namespace esphome::daikin diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index 18f12dbfc6c..a455e2fd7f8 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -5,8 +5,7 @@ #include "esphome/components/remote_base/remote_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace daikin_arc { +namespace esphome::daikin_arc { static const char *const TAG = "daikin.climate"; @@ -492,5 +491,4 @@ void DaikinArcClimate::control(const climate::ClimateCall &call) { climate_ir::ClimateIR::control(call); } -} // namespace daikin_arc -} // namespace esphome +} // namespace esphome::daikin_arc diff --git a/esphome/components/daikin_arc/daikin_arc.h b/esphome/components/daikin_arc/daikin_arc.h index 2b4d4375aae..2337351e280 100644 --- a/esphome/components/daikin_arc/daikin_arc.h +++ b/esphome/components/daikin_arc/daikin_arc.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace daikin_arc { +namespace esphome::daikin_arc { // Values for Daikin ARC43XXX IR Controllers // Temperature @@ -73,5 +72,4 @@ class DaikinArcClimate : public climate_ir::ClimateIR { uint8_t last_humidity_{0x66}; }; -} // namespace daikin_arc -} // namespace esphome +} // namespace esphome::daikin_arc diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp index 1179cb07d70..5fe3d30a850 100644 --- a/esphome/components/daikin_brc/daikin_brc.cpp +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -1,8 +1,7 @@ #include "daikin_brc.h" #include "esphome/components/remote_base/remote_base.h" -namespace esphome { -namespace daikin_brc { +namespace esphome::daikin_brc { static const char *const TAG = "daikin_brc.climate"; @@ -269,5 +268,4 @@ bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) { return this->parse_state_frame_(state_frame); } -} // namespace daikin_brc -} // namespace esphome +} // namespace esphome::daikin_brc diff --git a/esphome/components/daikin_brc/daikin_brc.h b/esphome/components/daikin_brc/daikin_brc.h index bdc63848092..4b5c679f7fc 100644 --- a/esphome/components/daikin_brc/daikin_brc.h +++ b/esphome/components/daikin_brc/daikin_brc.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace daikin_brc { +namespace esphome::daikin_brc { // Values for Daikin BRC4CXXX IR Controllers // Temperature @@ -78,5 +77,4 @@ class DaikinBrcClimate : public climate_ir::ClimateIR { bool fahrenheit_{false}; }; -} // namespace daikin_brc -} // namespace esphome +} // namespace esphome::daikin_brc diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index f119e28e78e..35488eab03f 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -1,8 +1,7 @@ #include "dallas_temp.h" #include "esphome/core/log.h" -namespace esphome { -namespace dallas_temp { +namespace esphome::dallas_temp { static const char *const TAG = "dallas.temp.sensor"; @@ -159,5 +158,4 @@ float DallasTemperatureSensor::get_temp_c_() { return temp / 16.0f; } -} // namespace dallas_temp -} // namespace esphome +} // namespace esphome::dallas_temp diff --git a/esphome/components/dallas_temp/dallas_temp.h b/esphome/components/dallas_temp/dallas_temp.h index 1bd28650952..3f7b447fc7c 100644 --- a/esphome/components/dallas_temp/dallas_temp.h +++ b/esphome/components/dallas_temp/dallas_temp.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/one_wire/one_wire.h" -namespace esphome { -namespace dallas_temp { +namespace esphome::dallas_temp { class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor, public one_wire::OneWireDevice { public: @@ -27,5 +26,4 @@ class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor, float get_temp_c_(); }; -} // namespace dallas_temp -} // namespace esphome +} // namespace esphome::dallas_temp diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 90ccee78f8a..530d8ad5415 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace daly_bms { +namespace esphome::daly_bms { static const char *const TAG = "daly_bms"; @@ -321,5 +320,4 @@ void DalyBmsComponent::decode_data_(std::vector<uint8_t> data) { } } -} // namespace daly_bms -} // namespace esphome +} // namespace esphome::daly_bms diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index 1983ba0ef19..6dcc66b5eeb 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -15,8 +15,7 @@ #include <vector> -namespace esphome { -namespace daly_bms { +namespace esphome::daly_bms { class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { public: @@ -88,5 +87,4 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { uint8_t next_request_; }; -} // namespace daly_bms -} // namespace esphome +} // namespace esphome::daly_bms diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index d4a95b81f6c..f553adf273a 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -1,12 +1,10 @@ #include "dashboard_import.h" -namespace esphome { -namespace dashboard_import { +namespace esphome::dashboard_import { static const char *g_package_import_url = ""; // NOLINT const char *get_package_import_url() { return g_package_import_url; } void set_package_import_url(const char *url) { g_package_import_url = url; } -} // namespace dashboard_import -} // namespace esphome +} // namespace esphome::dashboard_import diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index 488bf80a2ed..19f69b85461 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,10 +1,8 @@ #pragma once -namespace esphome { -namespace dashboard_import { +namespace esphome::dashboard_import { const char *get_package_import_url(); void set_package_import_url(const char *url); -} // namespace dashboard_import -} // namespace esphome +} // namespace esphome::dashboard_import diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index d97a4aa135a..9020c261c26 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -9,8 +9,7 @@ #include <cinttypes> #include <climits> -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; @@ -93,5 +92,4 @@ void DebugComponent::update() { float DebugComponent::get_setup_priority() const { return setup_priority::LATE; } -} // namespace debug -} // namespace esphome +} // namespace esphome::debug diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 3da6b800c6f..871b7cfd258 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -13,8 +13,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif -namespace esphome { -namespace debug { +namespace esphome::debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; @@ -101,5 +100,4 @@ class DebugComponent : public PollingComponent { void update_platform_(); }; -} // namespace debug -} // namespace esphome +} // namespace esphome::debug diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index ea0c635207f..7c01f9b54fe 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -16,8 +16,7 @@ #include <Esp.h> #endif -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; @@ -311,6 +310,6 @@ void DebugComponent::update_platform_() { #endif } -} // namespace debug -} // namespace esphome +} // namespace esphome::debug + #endif // USE_ESP32 diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index d5e34b1f1c8..e7ce70b60c3 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace deep_sleep { +namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; // 5 seconds for deep sleep to ensure clean disconnect from Home Assistant @@ -81,5 +80,4 @@ void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } -} // namespace deep_sleep -} // namespace esphome +} // namespace esphome::deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 59381eeabeb..2df53f15407 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -15,8 +15,7 @@ #include <cinttypes> -namespace esphome { -namespace deep_sleep { +namespace esphome::deep_sleep { #if defined(USE_ESP32) || defined(USE_BK72XX) @@ -244,5 +243,4 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; -} // namespace deep_sleep -} // namespace esphome +} // namespace esphome::deep_sleep diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 80a218e9133..c905b8fcbca 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include <esp_idf_version.h> -namespace esphome { -namespace deep_sleep { +namespace esphome::deep_sleep { // Deep Sleep feature support matrix for ESP32 variants: // @@ -167,6 +166,6 @@ void DeepSleepComponent::deep_sleep_() { bool DeepSleepComponent::should_teardown_() { return true; } -} // namespace deep_sleep -} // namespace esphome +} // namespace esphome::deep_sleep + #endif // USE_ESP32 diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp index 19af703ab26..fab2d68347b 100644 --- a/esphome/components/delonghi/delonghi.cpp +++ b/esphome/components/delonghi/delonghi.cpp @@ -1,8 +1,7 @@ #include "delonghi.h" #include "esphome/components/remote_base/remote_base.h" -namespace esphome { -namespace delonghi { +namespace esphome::delonghi { static const char *const TAG = "delonghi.climate"; @@ -182,5 +181,4 @@ bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { return this->parse_state_frame_(state_frame); } -} // namespace delonghi -} // namespace esphome +} // namespace esphome::delonghi diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h index d310a58aee3..c2fbc36b4f7 100644 --- a/esphome/components/delonghi/delonghi.h +++ b/esphome/components/delonghi/delonghi.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace delonghi { +namespace esphome::delonghi { // Values for DELONGHI ARC43XXX IR Controllers const uint8_t DELONGHI_ADDRESS = 83; @@ -60,5 +59,4 @@ class DelonghiClimate : public climate_ir::ClimateIR { bool parse_state_frame_(const uint8_t frame[]); }; -} // namespace delonghi -} // namespace esphome +} // namespace esphome::delonghi diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 5f0725dd4bb..7aaf3219cf1 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -3,8 +3,7 @@ #include "esphome/components/alarm_control_panel/alarm_control_panel.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { using namespace alarm_control_panel; @@ -62,5 +61,4 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { DemoAlarmControlPanelType type_{}; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_binary_sensor.h b/esphome/components/demo/demo_binary_sensor.h index 4dfd038761c..4bc3737d5a2 100644 --- a/esphome/components/demo/demo_binary_sensor.h +++ b/esphome/components/demo/demo_binary_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { public: @@ -18,5 +17,4 @@ class DemoBinarySensor : public binary_sensor::BinarySensor, public PollingCompo bool last_state_ = false; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_button.h b/esphome/components/demo/demo_button.h index be80d26a8af..a0ed92d3d8e 100644 --- a/esphome/components/demo/demo_button.h +++ b/esphome/components/demo/demo_button.h @@ -3,13 +3,11 @@ #include "esphome/components/button/button.h" #include "esphome/core/log.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoButton : public button::Button { protected: void press_action() override {} }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index c6d328b1bc5..d0cd2d553d3 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/climate/climate.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoClimateType { TYPE_1, @@ -158,5 +157,4 @@ class DemoClimate : public climate::Climate, public Component { DemoClimateType type_; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index 69dd5a4d2d1..c1597a75653 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoCoverType { TYPE_1, @@ -85,5 +84,4 @@ class DemoCover : public cover::Cover, public Component { DemoCoverType type_; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_date.h b/esphome/components/demo/demo_date.h index e09ab5f8873..5a868342cd4 100644 --- a/esphome/components/demo/demo_date.h +++ b/esphome/components/demo/demo_date.h @@ -7,8 +7,7 @@ #include "esphome/components/datetime/date_entity.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoDate : public datetime::DateEntity, public Component { public: @@ -28,7 +27,6 @@ class DemoDate : public datetime::DateEntity, public Component { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo #endif diff --git a/esphome/components/demo/demo_datetime.h b/esphome/components/demo/demo_datetime.h index 5ebcc3e64ed..84869d1a9f8 100644 --- a/esphome/components/demo/demo_datetime.h +++ b/esphome/components/demo/demo_datetime.h @@ -7,8 +7,7 @@ #include "esphome/components/datetime/datetime_entity.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoDateTime : public datetime::DateTimeEntity, public Component { public: @@ -34,7 +33,6 @@ class DemoDateTime : public datetime::DateTimeEntity, public Component { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo #endif diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index a8b397f19ac..2e2fbce7d66 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoFanType { TYPE_1, @@ -66,5 +65,4 @@ class DemoFan : public fan::Fan, public Component { DemoFanType type_; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h index 2007e9ff506..071adb0831c 100644 --- a/esphome/components/demo/demo_light.h +++ b/esphome/components/demo/demo_light.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoLightType { // binary @@ -64,5 +63,4 @@ class DemoLight : public light::LightOutput, public Component { DemoLightType type_; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_lock.h b/esphome/components/demo/demo_lock.h index 1e3fd51db4c..85c1c238ef7 100644 --- a/esphome/components/demo/demo_lock.h +++ b/esphome/components/demo/demo_lock.h @@ -2,8 +2,7 @@ #include "esphome/components/lock/lock.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoLock : public lock::Lock { protected: @@ -14,5 +13,4 @@ class DemoLock : public lock::Lock { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_number.h b/esphome/components/demo/demo_number.h index 2ce3a269bc7..0059cdc2eec 100644 --- a/esphome/components/demo/demo_number.h +++ b/esphome/components/demo/demo_number.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/number/number.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoNumberType { TYPE_1, @@ -35,5 +34,4 @@ class DemoNumber : public number::Number, public Component { DemoNumberType type_; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_select.h b/esphome/components/demo/demo_select.h index 1a5df13eda7..2ecb37db99f 100644 --- a/esphome/components/demo/demo_select.h +++ b/esphome/components/demo/demo_select.h @@ -3,13 +3,11 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoSelect : public select::Select, public Component { protected: void control(size_t index) override { this->publish_state(index); } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index d965d987dea..867115f21bc 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoSensor : public sensor::Sensor, public PollingComponent { public: @@ -25,5 +24,4 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_switch.h b/esphome/components/demo/demo_switch.h index 9c291318cac..b2d6e52c673 100644 --- a/esphome/components/demo/demo_switch.h +++ b/esphome/components/demo/demo_switch.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoSwitch : public switch_::Switch, public Component { public: @@ -18,5 +17,4 @@ class DemoSwitch : public switch_::Switch, public Component { void write_state(bool state) override { this->publish_state(state); } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_text.h b/esphome/components/demo/demo_text.h index a753175062d..56376c8c42a 100644 --- a/esphome/components/demo/demo_text.h +++ b/esphome/components/demo/demo_text.h @@ -3,8 +3,7 @@ #include "esphome/components/text/text.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoText : public text::Text, public Component { public: @@ -14,5 +13,4 @@ class DemoText : public text::Text, public Component { void control(const std::string &value) override { this->publish_state(value); } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_text_sensor.h b/esphome/components/demo/demo_text_sensor.h index b4152fc2483..03852a1e7f8 100644 --- a/esphome/components/demo/demo_text_sensor.h +++ b/esphome/components/demo/demo_text_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -21,5 +20,4 @@ class DemoTextSensor : public text_sensor::TextSensor, public PollingComponent { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/demo/demo_time.h b/esphome/components/demo/demo_time.h index 42788504bbc..f94678fae40 100644 --- a/esphome/components/demo/demo_time.h +++ b/esphome/components/demo/demo_time.h @@ -7,8 +7,7 @@ #include "esphome/components/datetime/time_entity.h" #include "esphome/core/component.h" -namespace esphome { -namespace demo { +namespace esphome::demo { class DemoTime : public datetime::TimeEntity, public Component { public: @@ -28,7 +27,6 @@ class DemoTime : public datetime::TimeEntity, public Component { } }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo #endif diff --git a/esphome/components/demo/demo_valve.h b/esphome/components/demo/demo_valve.h index 9a3122aca5c..3f1342959ad 100644 --- a/esphome/components/demo/demo_valve.h +++ b/esphome/components/demo/demo_valve.h @@ -2,8 +2,7 @@ #include "esphome/components/valve/valve.h" -namespace esphome { -namespace demo { +namespace esphome::demo { enum class DemoValveType { TYPE_1, @@ -53,5 +52,4 @@ class DemoValve : public valve::Valve { DemoValveType type_{}; }; -} // namespace demo -} // namespace esphome +} // namespace esphome::demo diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 1e1c33adaf3..5c9d497c877 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace dfplayer { +namespace esphome::dfplayer { static const char *const TAG = "dfplayer"; @@ -283,5 +282,4 @@ void DFPlayer::dump_config() { this->check_uart_settings(9600); } -} // namespace dfplayer -} // namespace esphome +} // namespace esphome::dfplayer diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 0d240566c3d..5936a06b60c 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -6,8 +6,7 @@ const size_t DFPLAYER_READ_BUFFER_LENGTH = 25; // two messages + some extra -namespace esphome { -namespace dfplayer { +namespace esphome::dfplayer { enum EqPreset { NORMAL = 0, @@ -171,5 +170,4 @@ template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts. bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; -} // namespace dfplayer -} // namespace esphome +} // namespace esphome::dfplayer diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index 422555d6eba..bd91381d474 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -5,8 +5,7 @@ #include "dfrobot_sen0395.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { template<typename... Ts> class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> { @@ -85,5 +84,4 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob } }; -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 0f69c82f39d..29ee166f51f 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -6,8 +6,7 @@ #include "dfrobot_sen0395.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { static const char *const TAG = "dfrobot_sen0395.commands"; @@ -330,5 +329,4 @@ uint8_t SensitivityCommand::on_message(std::string &message) { return 0; // Command not done yet } -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index 95167efb4db..765131bcfe8 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { class DfrobotSen0395Component; @@ -150,5 +149,4 @@ class SensitivityCommand : public Command { uint8_t sensitivity_; }; -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp index 98901bd353c..42cfd5dff68 100644 --- a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { static const char *const TAG = "dfrobot_sen0395"; const char ASCII_CR = 0x0D; @@ -140,5 +139,4 @@ uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) { } } -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h index d3b2ecedc3c..03e3b6b6ec1 100644 --- a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h @@ -13,8 +13,7 @@ #include "commands.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255; @@ -121,5 +120,4 @@ class DfrobotSen0395Component : public uart::UARTDevice, public Component { friend class ReadStateCommand; }; -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp index ca72d945318..477a6f98af0 100644 --- a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp @@ -1,7 +1,6 @@ #include "dfrobot_sen0395_switch.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique<PowerCommand>(state)); } @@ -44,5 +43,4 @@ void Sen0395StartAfterBootSwitch::write_state(bool state) { } } -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h index ab32d81dd8f..d83734b0344 100644 --- a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h @@ -5,8 +5,7 @@ #include "../dfrobot_sen0395.h" -namespace esphome { -namespace dfrobot_sen0395 { +namespace esphome::dfrobot_sen0395 { class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented<DfrobotSen0395Component> {}; @@ -30,5 +29,4 @@ class Sen0395StartAfterBootSwitch : public DfrobotSen0395Switch { void write_state(bool state) override; }; -} // namespace dfrobot_sen0395 -} // namespace esphome +} // namespace esphome::dfrobot_sen0395 diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 1d884daad68..78f9140929b 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -5,8 +5,7 @@ #include "dht12.h" #include "esphome/core/log.h" -namespace esphome { -namespace dht12 { +namespace esphome::dht12 { static const char *const TAG = "dht12"; @@ -65,5 +64,4 @@ bool DHT12Component::read_data_(uint8_t *data) { return true; } -} // namespace dht12 -} // namespace esphome +} // namespace esphome::dht12 diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index ab19d7c7230..5f4f822e70f 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace dht12 { +namespace esphome::dht12 { class DHT12Component : public PollingComponent, public i2c::I2CDevice { public: @@ -23,5 +22,4 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace dht12 -} // namespace esphome +} // namespace esphome::dht12 diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index f8569b6e7c9..cd2d2143f58 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace display { +namespace esphome::display { static const char *const TAG = "display"; // COLOR_OFF and COLOR_ON are now inline constexpr in display.h @@ -927,5 +926,4 @@ const LogString *text_align_to_string(TextAlign textalign) { return LOG_STR("UNKNOWN"); } } -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 6e38300d0e5..6d0b7acfe88 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -23,8 +23,7 @@ #include "esphome/components/graphical_display_menu/graphical_display_menu.h" #endif -namespace esphome { -namespace display { +namespace esphome::display { /** TextAlign is used to tell the display class how to position a piece of text. By default * the coordinates you enter for the print*() functions take the upper left corner of the text @@ -871,5 +870,4 @@ class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> const LogString *text_align_to_string(TextAlign textalign); -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 0ecdccc38a3..4c919140494 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -5,8 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace display { +namespace esphome::display { static const char *const TAG = "display"; @@ -68,5 +67,4 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { App.feed_wdt(); } -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b7c4db56bea..d3032a33f7e 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -9,8 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" -namespace esphome { -namespace display { +namespace esphome::display { class DisplayBuffer : public Display { public: @@ -30,5 +29,4 @@ class DisplayBuffer : public Display { uint8_t *buffer_{nullptr}; }; -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 3114dee3592..608642caa44 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -1,8 +1,7 @@ #pragma once #include "esphome/core/color.h" -namespace esphome { -namespace display { +namespace esphome::display { enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 }; enum ColorBitness : uint8_t { COLOR_BITNESS_888 = 0, COLOR_BITNESS_565 = 1, COLOR_BITNESS_332 = 2 }; inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 255) { return (max_value * i / scale); } @@ -155,5 +154,4 @@ class ColorUtil { return color; } }; -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp index 2c41127860c..a47f7269175 100644 --- a/esphome/components/display/rect.cpp +++ b/esphome/components/display/rect.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace display { +namespace esphome::display { static const char *const TAG = "display"; @@ -90,5 +89,4 @@ void Rect::info(const std::string &prefix) { } } -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h index 5f11d94681c..f4958fab88c 100644 --- a/esphome/components/display/rect.h +++ b/esphome/components/display/rect.h @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace display { +namespace esphome::display { static const int16_t VALUE_NO_SET = 32766; @@ -32,5 +31,4 @@ class Rect { void info(const std::string &prefix = "rect info:"); }; -} // namespace display -} // namespace esphome +} // namespace esphome::display diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 50c26c344cd..d4f83055d19 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "display_menu_base.h" -namespace esphome { -namespace display_menu_base { +namespace esphome::display_menu_base { template<typename... Ts> class UpAction : public Action<Ts...> { public: @@ -144,5 +143,4 @@ class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> { MenuItemCustom *parent_; }; -} // namespace display_menu_base -} // namespace esphome +} // namespace esphome::display_menu_base diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 2d8e6ae5fc2..634a82a8927 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -1,8 +1,7 @@ #include "display_menu_base.h" #include <algorithm> -namespace esphome { -namespace display_menu_base { +namespace esphome::display_menu_base { void DisplayMenuComponent::up() { if (this->check_healthy_and_active_()) { @@ -325,5 +324,4 @@ void DisplayMenuComponent::draw_menu() { } } -} // namespace display_menu_base -} // namespace esphome +} // namespace esphome::display_menu_base diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 6208fcd3b46..07cdb7a10f5 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -6,8 +6,7 @@ #include <forward_list> -namespace esphome { -namespace display_menu_base { +namespace esphome::display_menu_base { enum MenuMode { MENU_MODE_ROTARY, @@ -78,5 +77,4 @@ class DisplayMenuComponent : public Component { bool root_on_enter_called_{false}; }; -} // namespace display_menu_base -} // namespace esphome +} // namespace esphome::display_menu_base diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index ad8b03de606..8d1d315e322 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -2,8 +2,7 @@ #include <cstdio> -namespace esphome { -namespace display_menu_base { +namespace esphome::display_menu_base { const LogString *menu_item_type_to_string(MenuItemType type) { switch (type) { @@ -201,5 +200,4 @@ void MenuItemCustom::on_next_() { this->on_next_callbacks_.call(); } void MenuItemCustom::on_prev_() { this->on_prev_callbacks_.call(); } -} // namespace display_menu_base -} // namespace esphome +} // namespace esphome::display_menu_base diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index 57d7350b9e7..f3c41583f73 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -16,8 +16,7 @@ #include <vector> #include "esphome/core/log.h" -namespace esphome { -namespace display_menu_base { +namespace esphome::display_menu_base { enum MenuItemType { MENU_ITEM_LABEL, @@ -187,5 +186,4 @@ class MenuItemCustom : public MenuItemEditable { CallbackManager<void()> on_prev_callbacks_{}; }; -} // namespace display_menu_base -} // namespace esphome +} // namespace esphome::display_menu_base diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index b1366cd0697..3b4693139d3 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace dps310 { +namespace esphome::dps310 { static const char *const TAG = "dps310"; @@ -182,5 +181,4 @@ int32_t DPS310Component::twos_complement(int32_t val, uint8_t bits) { return val; } -} // namespace dps310 -} // namespace esphome +} // namespace esphome::dps310 diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h index dce220d44b5..09143bf6b88 100644 --- a/esphome/components/dps310/dps310.h +++ b/esphome/components/dps310/dps310.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace dps310 { +namespace esphome::dps310 { static const uint8_t DPS310_REG_PRS_B2 = 0x00; // Highest byte of pressure data static const uint8_t DPS310_REG_TMP_B2 = 0x03; // Highest byte of temperature data @@ -60,5 +59,4 @@ class DPS310Component : public PollingComponent, public i2c::I2CDevice { bool got_pres_, got_temp_, update_in_progress_; }; -} // namespace dps310 -} // namespace esphome +} // namespace esphome::dps310 diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index ba2ad6032f4..1d04b52168e 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -4,8 +4,7 @@ // Datasheet: // - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf -namespace esphome { -namespace ds1307 { +namespace esphome::ds1307 { static const char *const TAG = "ds1307"; @@ -99,5 +98,4 @@ bool DS1307Component::write_rtc_() { ds1307_.reg.day, ONOFF(ds1307_.reg.ch), ds1307_.reg.rs, ONOFF(ds1307_.reg.sqwe), ONOFF(ds1307_.reg.out)); return true; } -} // namespace ds1307 -} // namespace esphome +} // namespace esphome::ds1307 diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index 1712056006d..2004978cc66 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace ds1307 { +namespace esphome::ds1307 { class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { public: @@ -65,5 +64,4 @@ template<typename... Ts> class ReadAction : public Action<Ts...>, public Parente public: void play(const Ts &...x) override { this->parent_->read_time(); } }; -} // namespace ds1307 -} // namespace esphome +} // namespace esphome::ds1307 diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp index 0b36f868749..69103ccbd22 100644 --- a/esphome/components/ds2484/ds2484.cpp +++ b/esphome/components/ds2484/ds2484.cpp @@ -1,7 +1,6 @@ #include "ds2484.h" -namespace esphome { -namespace ds2484 { +namespace esphome::ds2484 { static const char *const TAG = "ds2484.onewire"; void DS2484OneWireBus::setup() { @@ -204,5 +203,4 @@ uint64_t IRAM_ATTR DS2484OneWireBus::search_int() { return address; } -} // namespace ds2484 -} // namespace esphome +} // namespace esphome::ds2484 diff --git a/esphome/components/ds2484/ds2484.h b/esphome/components/ds2484/ds2484.h index 223227c0a26..9e6bb088584 100644 --- a/esphome/components/ds2484/ds2484.h +++ b/esphome/components/ds2484/ds2484.h @@ -6,8 +6,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/one_wire/one_wire.h" -namespace esphome { -namespace ds2484 { +namespace esphome::ds2484 { class DS2484OneWireBus : public one_wire::OneWireBus, public i2c::I2CDevice, public Component { public: @@ -39,5 +38,4 @@ class DS2484OneWireBus : public one_wire::OneWireBus, public i2c::I2CDevice, pub bool active_pullup_{false}; bool strong_pullup_{false}; }; -} // namespace ds2484 -} // namespace esphome +} // namespace esphome::ds2484 diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index f801769d272..fd0a48b9351 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace duty_cycle { +namespace esphome::duty_cycle { static const char *const TAG = "duty_cycle"; @@ -56,5 +55,4 @@ void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { arg->last_interrupt = now; } -} // namespace duty_cycle -} // namespace esphome +} // namespace esphome::duty_cycle diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index ffb8e3b6221..58beee946a2 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace duty_cycle { +namespace esphome::duty_cycle { /// Store data in a class that doesn't use multiple-inheritance (vtables in flash) struct DutyCycleSensorStore { @@ -32,5 +31,4 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { uint32_t last_update_{0}; }; -} // namespace duty_cycle -} // namespace esphome +} // namespace esphome::duty_cycle diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index 561040623d3..697a4e96f3c 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -1,8 +1,7 @@ #include "duty_time_sensor.h" #include "esphome/core/hal.h" -namespace esphome { -namespace duty_time_sensor { +namespace esphome::duty_time_sensor { static const char *const TAG = "duty_time_sensor"; @@ -103,5 +102,4 @@ void DutyTimeSensor::dump_config() { LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); } -} // namespace duty_time_sensor -} // namespace esphome +} // namespace esphome::duty_time_sensor diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index d21802ebb6c..9b1e10ea8c4 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -10,8 +10,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -namespace esphome { -namespace duty_time_sensor { +namespace esphome::duty_time_sensor { class DutyTimeSensor : public sensor::Sensor, public PollingComponent { public: @@ -71,5 +70,4 @@ template<typename... Ts> class RunningCondition : public Condition<Ts...>, publi bool state_; }; -} // namespace duty_time_sensor -} // namespace esphome +} // namespace esphome::duty_time_sensor diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index a7a695c1679..061af4c4c0e 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -5,8 +5,7 @@ #include <algorithm> -namespace esphome { -namespace e131 { +namespace esphome::e131 { static const char *const TAG = "e131"; static const int PORT = 5568; @@ -134,6 +133,6 @@ bool E131Component::process_(int universe, const E131Packet &packet) { return handled; } -} // namespace e131 -} // namespace esphome +} // namespace esphome::e131 + #endif diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 8f0b808946c..bfcb0ca7f8e 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -12,8 +12,7 @@ #include <memory> #include <vector> -namespace esphome { -namespace e131 { +namespace esphome::e131 { class E131AddressableLightEffect; @@ -72,6 +71,6 @@ class E131Component : public esphome::Component { std::vector<UniverseConsumer> universe_consumers_; }; -} // namespace e131 -} // namespace esphome +} // namespace esphome::e131 + #endif diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index f6300874ac0..6157eba4e93 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -3,8 +3,7 @@ #ifdef USE_NETWORK #include "esphome/core/log.h" -namespace esphome { -namespace e131 { +namespace esphome::e131 { static const char *const TAG = "e131_addressable_light_effect"; static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); @@ -91,6 +90,6 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet return true; } -} // namespace e131 -} // namespace esphome +} // namespace esphome::e131 + #endif diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 381e08163b1..b28dc22dbe2 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -3,8 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" #ifdef USE_NETWORK -namespace esphome { -namespace e131 { + +namespace esphome::e131 { class E131Component; struct E131Packet; @@ -40,6 +40,6 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { friend class E131Component; }; -} // namespace e131 -} // namespace esphome +} // namespace esphome::e131 + #endif diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index 600793f5d35..afed2abe311 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -12,8 +12,7 @@ #include <lwip/ip4_addr.h> #include <lwip/ip_addr.h> -namespace esphome { -namespace e131 { +namespace esphome::e131 { static const char *const TAG = "e131"; @@ -158,6 +157,6 @@ bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131 return true; } -} // namespace e131 -} // namespace esphome +} // namespace esphome::e131 + #endif diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 93e5d4203b6..2dd7405ff5f 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ee895 { +namespace esphome::ee895 { static const char *const TAG = "ee895"; @@ -111,5 +110,4 @@ uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { uint16_t crc = crc16(&addr, 1); return crc16(buf, len, crc); } -} // namespace ee895 -} // namespace esphome +} // namespace esphome::ee895 diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index ff1085e05d8..ba8e594feac 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ee895 { +namespace esphome::ee895 { /// This class implements support for the ee895 of temperature i2c sensors. class EE895Component : public PollingComponent, public i2c::I2CDevice { @@ -29,5 +28,4 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; -} // namespace ee895 -} // namespace esphome +} // namespace esphome::ee895 diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 63ebb2166b2..51532548c1f 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -4,8 +4,7 @@ #include <vector> -namespace esphome { -namespace ektf2232 { +namespace esphome::ektf2232 { static const char *const TAG = "ektf2232"; @@ -130,5 +129,4 @@ void EKTF2232Touchscreen::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); } -} // namespace ektf2232 -} // namespace esphome +} // namespace esphome::ektf2232 diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h index 2ddc60851f2..45da74a2a5d 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.h +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ektf2232 { +namespace esphome::ektf2232 { using namespace touchscreen; @@ -31,5 +30,4 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { GPIOPin *reset_pin_; }; -} // namespace ektf2232 -} // namespace esphome +} // namespace esphome::ektf2232 diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 068e25568f1..464f49fe519 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "emc2101.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { static const char *const TAG = "EMC2101"; @@ -165,5 +164,4 @@ float Emc2101Component::get_speed() { return tach == 0xFFFF ? 0.0f : 5400000.0f / tach; } -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emc2101/emc2101.h b/esphome/components/emc2101/emc2101.h index 0f4bc560ddd..1fe03a26307 100644 --- a/esphome/components/emc2101/emc2101.h +++ b/esphome/components/emc2101/emc2101.h @@ -3,8 +3,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { /** Enum listing all DAC conversion rates for the EMC2101. * @@ -111,5 +110,4 @@ class Emc2101Component : public Component, public i2c::I2CDevice { Emc2101DACConversionRate dac_conversion_rate_; }; -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emc2101/output/emc2101_output.cpp b/esphome/components/emc2101/output/emc2101_output.cpp index 2ed506cd99c..6b046296f3a 100644 --- a/esphome/components/emc2101/output/emc2101_output.cpp +++ b/esphome/components/emc2101/output/emc2101_output.cpp @@ -1,9 +1,7 @@ #include "emc2101_output.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { void EMC2101Output::write_state(float state) { this->parent_->set_duty_cycle(state); } -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emc2101/output/emc2101_output.h b/esphome/components/emc2101/output/emc2101_output.h index 232df6ff5f2..95077f55242 100644 --- a/esphome/components/emc2101/output/emc2101_output.h +++ b/esphome/components/emc2101/output/emc2101_output.h @@ -3,8 +3,7 @@ #include "../emc2101.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { /// This class allows to control the EMC2101 output. class EMC2101Output : public output::FloatOutput { @@ -18,5 +17,4 @@ class EMC2101Output : public output::FloatOutput { Emc2101Component *parent_; }; -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp index 3014c7da07c..bb5eea21f3b 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.cpp +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { static const char *const TAG = "EMC2101.sensor"; @@ -37,5 +36,4 @@ void EMC2101Sensor::update() { } } -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h index 3e033f58a73..2336ac2f15c 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.h +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace emc2101 { +namespace esphome::emc2101 { /// This class exposes the EMC2101 sensors. class EMC2101Sensor : public PollingComponent { @@ -33,5 +32,4 @@ class EMC2101Sensor : public PollingComponent { sensor::Sensor *duty_cycle_sensor_{nullptr}; }; -} // namespace emc2101 -} // namespace esphome +} // namespace esphome::emc2101 diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 04976d95d70..82991aeac75 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -1,8 +1,7 @@ #include "emmeti.h" #include "esphome/components/remote_base/remote_base.h" -namespace esphome { -namespace emmeti { +namespace esphome::emmeti { static const char *const TAG = "emmeti.climate"; @@ -308,5 +307,4 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { return this->parse_state_frame_(curr_state); } -} // namespace emmeti -} // namespace esphome +} // namespace esphome::emmeti diff --git a/esphome/components/emmeti/emmeti.h b/esphome/components/emmeti/emmeti.h index 9bfb7a7a984..9dc78ce07c2 100644 --- a/esphome/components/emmeti/emmeti.h +++ b/esphome/components/emmeti/emmeti.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace emmeti { +namespace esphome::emmeti { const uint8_t EMMETI_TEMP_MIN = 16; // Celsius const uint8_t EMMETI_TEMP_MAX = 30; // Celsius @@ -105,5 +104,4 @@ class EmmetiClimate : public climate_ir::ClimateIR { uint8_t blades_ = EMMETI_BLADES_STOP; }; -} // namespace emmeti -} // namespace esphome +} // namespace esphome::emmeti diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 5e0b9c72d3c..b3c210a8d8a 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/application.h" -namespace esphome { -namespace endstop { +namespace esphome::endstop { static const char *const TAG = "endstop.cover"; @@ -192,5 +191,4 @@ void EndstopCover::recompute_position_() { this->last_recompute_time_ = now; } -} // namespace endstop -} // namespace esphome +} // namespace esphome::endstop diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 32ede123356..b910139bcd6 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -5,8 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace endstop { +namespace esphome::endstop { class EndstopCover : public cover::Cover, public Component { public: @@ -53,5 +52,4 @@ class EndstopCover : public cover::Cover, public Component { cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; -} // namespace endstop -} // namespace esphome +} // namespace esphome::endstop diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 42baa68b35d..20c67c34505 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -18,8 +18,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ens160_base { +namespace esphome::ens160_base { static const char *const TAG = "ens160"; @@ -332,5 +331,4 @@ void ENS160Component::dump_config() { } } -} // namespace ens160_base -} // namespace esphome +} // namespace esphome::ens160_base diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h index ae850c81807..f42272684ed 100644 --- a/esphome/components/ens160_base/ens160_base.h +++ b/esphome/components/ens160_base/ens160_base.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ens160_base { +namespace esphome::ens160_base { class ENS160Component : public PollingComponent, public sensor::Sensor { public: @@ -59,5 +58,4 @@ class ENS160Component : public PollingComponent, public sensor::Sensor { sensor::Sensor *temperature_{nullptr}; }; -} // namespace ens160_base -} // namespace esphome +} // namespace esphome::ens160_base diff --git a/esphome/components/ens160_i2c/ens160_i2c.cpp b/esphome/components/ens160_i2c/ens160_i2c.cpp index 7163a5ad6e1..1f02ddb7182 100644 --- a/esphome/components/ens160_i2c/ens160_i2c.cpp +++ b/esphome/components/ens160_i2c/ens160_i2c.cpp @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "../ens160_base/ens160_base.h" -namespace esphome { -namespace ens160_i2c { +namespace esphome::ens160_i2c { static const char *const TAG = "ens160_i2c.sensor"; @@ -28,5 +27,4 @@ void ENS160I2CComponent::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace ens160_i2c -} // namespace esphome +} // namespace esphome::ens160_i2c diff --git a/esphome/components/ens160_i2c/ens160_i2c.h b/esphome/components/ens160_i2c/ens160_i2c.h index 2df32f27bf1..98318a7eca7 100644 --- a/esphome/components/ens160_i2c/ens160_i2c.h +++ b/esphome/components/ens160_i2c/ens160_i2c.h @@ -3,8 +3,7 @@ #include "esphome/components/ens160_base/ens160_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ens160_i2c { +namespace esphome::ens160_i2c { class ENS160I2CComponent : public esphome::ens160_base::ENS160Component, public i2c::I2CDevice { void dump_config() override; @@ -15,5 +14,4 @@ class ENS160I2CComponent : public esphome::ens160_base::ENS160Component, public bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; }; -} // namespace ens160_i2c -} // namespace esphome +} // namespace esphome::ens160_i2c diff --git a/esphome/components/ens160_spi/ens160_spi.cpp b/esphome/components/ens160_spi/ens160_spi.cpp index fba2fdf0e43..41ab8298dbb 100644 --- a/esphome/components/ens160_spi/ens160_spi.cpp +++ b/esphome/components/ens160_spi/ens160_spi.cpp @@ -4,8 +4,7 @@ #include "ens160_spi.h" #include <esphome/components/ens160_base/ens160_base.h> -namespace esphome { -namespace ens160_spi { +namespace esphome::ens160_spi { static const char *const TAG = "ens160_spi.sensor"; @@ -55,5 +54,4 @@ bool ENS160SPIComponent::write_bytes(uint8_t a_register, uint8_t *data, size_t l return true; } -} // namespace ens160_spi -} // namespace esphome +} // namespace esphome::ens160_spi diff --git a/esphome/components/ens160_spi/ens160_spi.h b/esphome/components/ens160_spi/ens160_spi.h index 3371f37ffd8..d4d3cf3ae99 100644 --- a/esphome/components/ens160_spi/ens160_spi.h +++ b/esphome/components/ens160_spi/ens160_spi.h @@ -3,8 +3,7 @@ #include "esphome/components/ens160_base/ens160_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ens160_spi { +namespace esphome::ens160_spi { class ENS160SPIComponent : public esphome::ens160_base::ENS160Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -18,5 +17,4 @@ class ENS160SPIComponent : public esphome::ens160_base::ENS160Component, bool write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; }; -} // namespace ens160_spi -} // namespace esphome +} // namespace esphome::ens160_spi diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 8bee9bfb18c..468c627d4b5 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -10,8 +10,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ens210 { +namespace esphome::ens210 { static const char *const TAG = "ens210"; @@ -223,5 +222,4 @@ bool ENS210Component::set_low_power_(bool enable) { return result; } -} // namespace ens210 -} // namespace esphome +} // namespace esphome::ens210 diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h index ae2bf81b5fb..f1520fc4834 100644 --- a/esphome/components/ens210/ens210.h +++ b/esphome/components/ens210/ens210.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ens210 { +namespace esphome::ens210 { /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. class ENS210Component : public PollingComponent, public i2c::I2CDevice { @@ -34,5 +33,4 @@ class ENS210Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace ens210 -} // namespace esphome +} // namespace esphome::ens210 diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 4371075fa90..bbd966fbe0e 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace es7210 { +namespace esphome::es7210 { static const char *const TAG = "es7210"; @@ -224,5 +223,4 @@ bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8 return this->write_byte(reg_addr, regv); } -} // namespace es7210 -} // namespace esphome +} // namespace esphome::es7210 diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index 7071a547ec8..914fbd633b8 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -6,8 +6,7 @@ #include "es7210_const.h" -namespace esphome { -namespace es7210 { +namespace esphome::es7210 { enum ES7210BitsPerSample : uint8_t { ES7210_BITS_PER_SAMPLE_16 = 16, @@ -57,5 +56,4 @@ class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevi uint32_t sample_rate_{0}; }; -} // namespace es7210 -} // namespace esphome +} // namespace esphome::es7210 diff --git a/esphome/components/es7210/es7210_const.h b/esphome/components/es7210/es7210_const.h index e5ffea57431..70705b24748 100644 --- a/esphome/components/es7210/es7210_const.h +++ b/esphome/components/es7210/es7210_const.h @@ -2,8 +2,7 @@ #include <cinttypes> -namespace esphome { -namespace es7210 { +namespace esphome::es7210 { // ES7210 register addresses static const uint8_t ES7210_RESET_REG00 = 0x00; /* Reset control */ @@ -125,5 +124,4 @@ static const ES7210Coefficient ES7210_COEFFICIENTS[] = { static const float ES7210_MIC_GAIN_MIN = 0.0; static const float ES7210_MIC_GAIN_MAX = 37.5; -} // namespace es7210 -} // namespace esphome +} // namespace esphome::es7210 diff --git a/esphome/components/es7243e/es7243e.cpp b/esphome/components/es7243e/es7243e.cpp index d45c1d5a8c7..b4d9fba4c52 100644 --- a/esphome/components/es7243e/es7243e.cpp +++ b/esphome/components/es7243e/es7243e.cpp @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace es7243e { +namespace esphome::es7243e { static const char *const TAG = "es7243e"; @@ -119,5 +118,4 @@ uint8_t ES7243E::es7243e_gain_reg_value_(float mic_gain) { return 14; } -} // namespace es7243e -} // namespace esphome +} // namespace esphome::es7243e diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h index f7c9d67371b..6386ea529a7 100644 --- a/esphome/components/es7243e/es7243e.h +++ b/esphome/components/es7243e/es7243e.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace es7243e { +namespace esphome::es7243e { class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDevice { /* Class for configuring an ES7243E ADC for microphone input. @@ -32,5 +31,4 @@ class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDev float mic_gain_{0}; }; -} // namespace es7243e -} // namespace esphome +} // namespace esphome::es7243e diff --git a/esphome/components/es7243e/es7243e_const.h b/esphome/components/es7243e/es7243e_const.h index daae53a1083..9f926db3b17 100644 --- a/esphome/components/es7243e/es7243e_const.h +++ b/esphome/components/es7243e/es7243e_const.h @@ -2,8 +2,7 @@ #include <cinttypes> -namespace esphome { -namespace es7243e { +namespace esphome::es7243e { // ES7243E register addresses static const uint8_t ES7243E_RESET_REG00 = 0x00; // Reset control @@ -50,5 +49,4 @@ static const uint8_t ES7243E_CHIP_ID1_REGFD = 0xFD; // chip ID 1, reads 0x7 static const uint8_t ES7243E_CHIP_ID2_REGFE = 0xFE; // chip ID 2, reads 0x43 (RO) static const uint8_t ES7243E_CHIP_VERSION_REGFF = 0xFF; // chip version, reads 0x00 (RO) -} // namespace es7243e -} // namespace esphome +} // namespace esphome::es7243e diff --git a/esphome/components/es8156/es8156.cpp b/esphome/components/es8156/es8156.cpp index 961dc24b295..03d9713df95 100644 --- a/esphome/components/es8156/es8156.cpp +++ b/esphome/components/es8156/es8156.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace es8156 { +namespace esphome::es8156 { static const char *const TAG = "es8156"; @@ -118,5 +117,4 @@ bool ES8156::set_mute_state_(bool mute_state) { return this->write_byte(ES8156_REG13_DAC_MUTE, reg13); } -} // namespace es8156 -} // namespace esphome +} // namespace esphome::es8156 diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h index 082514485c2..c3cec3dc14d 100644 --- a/esphome/components/es8156/es8156.h +++ b/esphome/components/es8156/es8156.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace es8156 { +namespace esphome::es8156 { class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { public: @@ -46,5 +45,4 @@ class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi bool set_mute_state_(bool mute_state); }; -} // namespace es8156 -} // namespace esphome +} // namespace esphome::es8156 diff --git a/esphome/components/es8156/es8156_const.h b/esphome/components/es8156/es8156_const.h index 0bc8f89dd44..0836e4766dd 100644 --- a/esphome/components/es8156/es8156_const.h +++ b/esphome/components/es8156/es8156_const.h @@ -2,8 +2,7 @@ #include "es8156.h" -namespace esphome { -namespace es8156 { +namespace esphome::es8156 { /* ES8156 register addresses */ /* @@ -64,5 +63,4 @@ static const uint8_t ES8156_REGFD_CHIPID1 = 0xFD; static const uint8_t ES8156_REGFE_CHIPID0 = 0xFE; static const uint8_t ES8156_REGFF_CHIP_VERSION = 0xFF; -} // namespace es8156 -} // namespace esphome +} // namespace esphome::es8156 diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp index cf864187f99..0386d84200f 100644 --- a/esphome/components/es8311/es8311.cpp +++ b/esphome/components/es8311/es8311.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace es8311 { +namespace esphome::es8311 { static const char *const TAG = "es8311"; @@ -223,5 +222,4 @@ bool ES8311::set_mute_state_(bool mute_state) { return this->write_byte(ES8311_REG31_DAC, reg31); } -} // namespace es8311 -} // namespace esphome +} // namespace esphome::es8311 diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h index 5eccc480047..1190bcb0aa0 100644 --- a/esphome/components/es8311/es8311.h +++ b/esphome/components/es8311/es8311.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace es8311 { +namespace esphome::es8311 { enum ES8311MicGain { ES8311_MIC_GAIN_MIN = -1, @@ -130,5 +129,4 @@ class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ES8311Resolution resolution_out_; }; -} // namespace es8311 -} // namespace esphome +} // namespace esphome::es8311 diff --git a/esphome/components/es8311/es8311_const.h b/esphome/components/es8311/es8311_const.h index 7463a92ef1c..27b9e02c138 100644 --- a/esphome/components/es8311/es8311_const.h +++ b/esphome/components/es8311/es8311_const.h @@ -2,8 +2,7 @@ #include "es8311.h" -namespace esphome { -namespace es8311 { +namespace esphome::es8311 { // ES8311 register addresses static const uint8_t ES8311_REG00_RESET = 0x00; // Reset @@ -191,5 +190,4 @@ static const ES8311Coefficient ES8311_COEFFICIENTS[] = { // clang-format on }; -} // namespace es8311 -} // namespace esphome +} // namespace esphome::es8311 diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index c252cdb707d..c015393e146 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { static const char *const TAG = "es8388"; @@ -284,5 +283,4 @@ optional<AdcInputMicLine> ES8388::get_mic_input() { }; } -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/es8388.h b/esphome/components/es8388/es8388.h index 373f71b437f..1f744e25b37 100644 --- a/esphome/components/es8388/es8388.h +++ b/esphome/components/es8388/es8388.h @@ -11,8 +11,7 @@ #include "es8388_const.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { enum DacOutputLine : uint8_t { DAC_OUTPUT_LINE1, @@ -76,5 +75,4 @@ class ES8388 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi bool set_mute_state_(bool mute_state); }; -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/es8388_const.h b/esphome/components/es8388/es8388_const.h index 2a51f078bc4..451c9cc0266 100644 --- a/esphome/components/es8388/es8388_const.h +++ b/esphome/components/es8388/es8388_const.h @@ -1,8 +1,7 @@ #pragma once #include <cstdint> -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { /* ES8388 register */ static const uint8_t ES8388_CONTROL1 = 0x00; @@ -79,5 +78,4 @@ static const uint8_t ES8388_ADC_INPUT_MIC2 = 0x06; static const uint8_t ES8388_ADC_INPUT_LINPUT2_RINPUT2 = 0x50; static const uint8_t ES8388_ADC_INPUT_DIFFERENCE = 0xf0; -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/select/adc_input_mic_select.cpp b/esphome/components/es8388/select/adc_input_mic_select.cpp index 2e475342961..a91ccf4d5b4 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.cpp +++ b/esphome/components/es8388/select/adc_input_mic_select.cpp @@ -1,12 +1,10 @@ #include "adc_input_mic_select.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { void ADCInputMicSelect::control(size_t index) { this->publish_state(index); this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index)); } -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/select/adc_input_mic_select.h b/esphome/components/es8388/select/adc_input_mic_select.h index f0fa840d006..29978f1623b 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.h +++ b/esphome/components/es8388/select/adc_input_mic_select.h @@ -3,13 +3,11 @@ #include "esphome/components/es8388/es8388.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { class ADCInputMicSelect : public select::Select, public Parented<ES8388> { protected: void control(size_t index) override; }; -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/select/dac_output_select.cpp b/esphome/components/es8388/select/dac_output_select.cpp index 9af288a721d..cfe0fe1472f 100644 --- a/esphome/components/es8388/select/dac_output_select.cpp +++ b/esphome/components/es8388/select/dac_output_select.cpp @@ -1,12 +1,10 @@ #include "dac_output_select.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { void DacOutputSelect::control(size_t index) { this->publish_state(index); this->parent_->set_dac_output(static_cast<DacOutputLine>(index)); } -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/es8388/select/dac_output_select.h b/esphome/components/es8388/select/dac_output_select.h index 40d8a665535..030f12406e1 100644 --- a/esphome/components/es8388/select/dac_output_select.h +++ b/esphome/components/es8388/select/dac_output_select.h @@ -3,13 +3,11 @@ #include "esphome/components/es8388/es8388.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace es8388 { +namespace esphome::es8388 { class DacOutputSelect : public select::Select, public Parented<ES8388> { protected: void control(size_t index) override; }; -} // namespace es8388 -} // namespace esphome +} // namespace esphome::es8388 diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 3c13bd9b4ff..a140eeef775 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include <driver/gpio.h> -namespace esphome { -namespace esp32 { +namespace esphome::esp32 { // Static assertions to ensure our bit-packed fields can hold the enum values static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t"); @@ -51,7 +50,6 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { static bool isr_service_installed; }; -} // namespace esp32 -} // namespace esphome +} // namespace esphome::esp32 #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 093273b399b..9f1723430b4 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -16,8 +16,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace esp32_ble_beacon { +namespace esphome::esp32_ble_beacon { static const char *const TAG = "esp32_ble_beacon"; @@ -129,7 +128,6 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap } } -} // namespace esp32_ble_beacon -} // namespace esphome +} // namespace esphome::esp32_ble_beacon #endif diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 44a71334543..8b3899a681d 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -10,8 +10,7 @@ #endif #include <esp_gap_ble_api.h> -namespace esphome { -namespace esp32_ble_beacon { +namespace esphome::esp32_ble_beacon { using esp_ble_ibeacon_head_t = struct { uint8_t flags[3]; @@ -69,7 +68,6 @@ class ESP32BLEBeacon : public Component { bool advertising_{false}; }; -} // namespace esp32_ble_beacon -} // namespace esphome +} // namespace esphome::esp32_ble_beacon #endif diff --git a/esphome/components/esp32_ble_server/ble_2902.cpp b/esphome/components/esp32_ble_server/ble_2902.cpp index 2f34573c372..90d0871a967 100644 --- a/esphome/components/esp32_ble_server/ble_2902.cpp +++ b/esphome/components/esp32_ble_server/ble_2902.cpp @@ -5,8 +5,7 @@ #include <cstring> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2902)) { this->value_.attr_len = 2; @@ -14,7 +13,6 @@ BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2902)) { memcpy(this->value_.attr_value, data, 2); } -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_2902.h b/esphome/components/esp32_ble_server/ble_2902.h index 64605924ade..46a5f73e9ed 100644 --- a/esphome/components/esp32_ble_server/ble_2902.h +++ b/esphome/components/esp32_ble_server/ble_2902.h @@ -4,15 +4,13 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { class BLE2902 : public BLEDescriptor { public: BLE2902(); }; -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index aa82b773ba6..cc519846be4 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { static const char *const TAG = "esp32_ble_server.characteristic"; @@ -340,7 +339,6 @@ BLECharacteristic::ClientNotificationEntry *BLECharacteristic::find_client_in_no return nullptr; } -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 062052cdf8c..94c7495cbd3 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -17,8 +17,7 @@ #include <esp_gatts_api.h> #include <esp_bt_defs.h> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; @@ -109,7 +108,6 @@ class BLECharacteristic { } state_{INIT}; }; -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index 4ffca7312b6..5ca80d6a7a3 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { static const char *const TAG = "esp32_ble_server.descriptor"; @@ -91,7 +90,6 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ } } -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 5f4f146d6f4..5096d39f284 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -11,8 +11,7 @@ #include <functional> #include <memory> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; @@ -63,7 +62,6 @@ class BLEDescriptor { } state_{INIT}; }; -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index be0691dc065..2dea1666bbe 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -16,8 +16,7 @@ #include <freertos/task.h> #include <esp_gap_ble_api.h> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { static const char *const TAG = "esp32_ble_server"; @@ -248,7 +247,6 @@ void BLEServer::dump_config() { BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 9708ed40c8b..9ba108499e0 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -18,8 +18,7 @@ #include <esp_gatts_api.h> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; @@ -113,7 +112,6 @@ class BLEServer : public Component, public Parented<ESP32BLE> { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern BLEServer *global_ble_server; -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp index 0761de994a2..1b15c90fe8c 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.cpp +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -2,10 +2,8 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { // Interface to interact with ESPHome automations and triggers -namespace esp32_ble_server_automations { +namespace esphome::esp32_ble_server::esp32_ble_server_automations { using namespace esp32_ble; @@ -86,8 +84,6 @@ void BLECharacteristicSetValueActionManager::remove_listener_(BLECharacteristic } #endif -} // namespace esp32_ble_server_automations -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server::esp32_ble_server_automations #endif diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 0bbfdffd5bd..b4e9ed004ec 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -11,10 +11,8 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { // Interface to interact with ESPHome actions and triggers -namespace esp32_ble_server_automations { +namespace esphome::esp32_ble_server::esp32_ble_server_automations { using namespace esp32_ble; @@ -125,8 +123,6 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts... }; #endif // USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION -} // namespace esp32_ble_server_automations -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server::esp32_ble_server_automations #endif diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 8956c87b3e8..ac8d7fa003c 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { static const char *const TAG = "esp32_ble_server.service"; @@ -173,7 +172,6 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g } } -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index dcfad5f501a..03fa8093acb 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -13,8 +13,7 @@ #include <esp_gattc_api.h> #include <esp_gatts_api.h> -namespace esphome { -namespace esp32_ble_server { +namespace esphome::esp32_ble_server { class BLEServer; @@ -80,7 +79,6 @@ class BLEService { } state_{INIT}; }; -} // namespace esp32_ble_server -} // namespace esphome +} // namespace esphome::esp32_ble_server #endif diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index a7546476d89..598fe61d464 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -7,8 +7,7 @@ #include <freertos/task.h> -namespace esphome { -namespace esp32_camera { +namespace esphome::esp32_camera { static const char *const TAG = "esp32_camera"; static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; @@ -556,7 +555,6 @@ bool ESP32CameraImage::was_requested_by(camera::CameraRequester requester) const return (this->requesters_ & (1 << requester)) != 0; } -} // namespace esp32_camera -} // namespace esphome +} // namespace esphome::esp32_camera #endif diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 9fbd3848f28..7d020b5caf7 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -15,8 +15,7 @@ #include "esphome/components/i2c/i2c_bus.h" #endif // USE_I2C -namespace esphome { -namespace esp32_camera { +namespace esphome::esp32_camera { class ESP32Camera; @@ -259,7 +258,6 @@ class ESP32CameraStreamStopTrigger : public Trigger<>, public camera::CameraList void on_stream_stop() override { this->trigger(); } }; -} // namespace esp32_camera -} // namespace esphome +} // namespace esphome::esp32_camera #endif diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index f49578c425d..7527bbf7e4e 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -11,8 +11,7 @@ #include <esp_http_server.h> #include <utility> -namespace esphome { -namespace esp32_camera_web_server { +namespace esphome::esp32_camera_web_server { static const int IMAGE_REQUEST_TIMEOUT = 5000; static const char *const TAG = "esp32_camera_web_server"; @@ -242,7 +241,6 @@ esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { return res; } -} // namespace esp32_camera_web_server -} // namespace esphome +} // namespace esphome::esp32_camera_web_server #endif // USE_ESP32 diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index ad7b29fb11c..568dc68c460 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -13,8 +13,7 @@ struct httpd_req; // NOLINT(readability-identifier-naming) -namespace esphome { -namespace esp32_camera_web_server { +namespace esphome::esp32_camera_web_server { enum Mode { STREAM, SNAPSHOT }; @@ -48,7 +47,6 @@ class CameraWebServer : public Component, public camera::CameraListener { Mode mode_{STREAM}; }; -} // namespace esp32_camera_web_server -} // namespace esphome +} // namespace esphome::esp32_camera_web_server #endif // USE_ESP32 diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index f521b63430f..e04405c63c0 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -9,8 +9,7 @@ #undef CAN_IO_UNUSED #define CAN_IO_UNUSED ((gpio_num_t) -1) -namespace esphome { -namespace esp32_can { +namespace esphome::esp32_can { static const char *const TAG = "esp32_can"; @@ -184,7 +183,6 @@ canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { return canbus::ERROR_OK; } -} // namespace esp32_can -} // namespace esphome +} // namespace esphome::esp32_can #endif diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index c3f200271bb..2e10d254e6e 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -7,8 +7,7 @@ #include <driver/twai.h> -namespace esphome { -namespace esp32_can { +namespace esphome::esp32_can { enum CanMode : uint8_t { CAN_MODE_NORMAL = 0, @@ -41,7 +40,6 @@ class ESP32Can : public canbus::Canbus { twai_handle_t twai_handle_{nullptr}; }; -} // namespace esp32_can -} // namespace esphome +} // namespace esphome::esp32_can #endif diff --git a/esphome/components/esp32_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp index 8f226a5cc22..54b89c46ad0 100644 --- a/esphome/components/esp32_dac/esp32_dac.cpp +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -4,8 +4,7 @@ #if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2) -namespace esphome { -namespace esp32_dac { +namespace esphome::esp32_dac { #ifdef USE_ESP32_VARIANT_ESP32S2 static constexpr uint8_t DAC0_PIN = 17; @@ -41,7 +40,6 @@ void ESP32DAC::write_state(float state) { dac_oneshot_output_voltage(this->dac_handle_, state); } -} // namespace esp32_dac -} // namespace esphome +} // namespace esphome::esp32_dac #endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h index 95c687d3072..108b96cd399 100644 --- a/esphome/components/esp32_dac/esp32_dac.h +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -9,8 +9,7 @@ #include <driver/dac_oneshot.h> -namespace esphome { -namespace esp32_dac { +namespace esphome::esp32_dac { class ESP32DAC : public output::FloatOutput, public Component { public: @@ -30,7 +29,6 @@ class ESP32DAC : public output::FloatOutput, public Component { dac_oneshot_handle_t dac_handle_; }; -} // namespace esp32_dac -} // namespace esphome +} // namespace esphome::esp32_dac #endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index cd2bd84c30c..19e1b6e7e39 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -7,8 +7,7 @@ #include <improv.h> -namespace esphome { -namespace esp32_improv { +namespace esphome::esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: @@ -81,7 +80,7 @@ class ESP32ImprovStoppedTrigger : public Trigger<> { ESP32ImprovComponent *parent_; }; -} // namespace esp32_improv -} // namespace esphome +} // namespace esphome::esp32_improv + #endif #endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index c24b08b06fa..183820256f4 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -9,8 +9,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_improv { +namespace esphome::esp32_improv { using namespace bytebuffer; @@ -490,7 +489,6 @@ improv::State ESP32ImprovComponent::get_initial_state_() const { ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace esp32_improv -} // namespace esphome +} // namespace esphome::esp32_improv #endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 41799f23251..400006cfb32 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -28,8 +28,7 @@ #include <improv.h> -namespace esphome { -namespace esp32_improv { +namespace esphome::esp32_improv { using namespace esp32_ble_server; @@ -124,7 +123,6 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32ImprovComponent *global_improv_component; -} // namespace esp32_improv -} // namespace esphome +} // namespace esphome::esp32_improv #endif diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index ca97a181fd4..ed2a8c5a686 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -9,8 +9,7 @@ #include <esp_attr.h> #include <esp_clk_tree.h> -namespace esphome { -namespace esp32_rmt_led_strip { +namespace esphome::esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; @@ -305,7 +304,6 @@ void ESP32RMTLEDStripLightOutput::dump_config() { float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace esp32_rmt_led_strip -} // namespace esphome +} // namespace esphome::esp32_rmt_led_strip #endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 6f3aea98784..8fb6b63afed 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -13,8 +13,7 @@ #include <esp_idf_version.h> #include <driver/rmt_tx.h> -namespace esphome { -namespace esp32_rmt_led_strip { +namespace esphome::esp32_rmt_led_strip { enum RGBOrder : uint8_t { ORDER_RGB, @@ -102,7 +101,6 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { optional<uint32_t> max_refresh_rate_{}; }; -} // namespace esp32_rmt_led_strip -} // namespace esphome +} // namespace esphome::esp32_rmt_led_strip #endif // USE_ESP32 diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 384e3fe2a9a..1e37073321e 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -5,8 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace espnow { +namespace esphome::espnow { static const char *const TAG = "espnow.transport"; @@ -86,7 +85,6 @@ bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *da return false; // Allow other handlers to run } -} // namespace espnow -} // namespace esphome +} // namespace esphome::espnow #endif // USE_ESP32 diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index 98c33f01fdb..5916a7fa5f0 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -9,8 +9,7 @@ #include <vector> -namespace esphome { -namespace espnow { +namespace esphome::espnow { class ESPNowTransport : public packet_transport::PacketTransport, public Parented<ESPNowComponent>, @@ -37,7 +36,6 @@ class ESPNowTransport : public packet_transport::PacketTransport, std::vector<uint8_t> packet_buffer_; }; -} // namespace espnow -} // namespace esphome +} // namespace esphome::espnow #endif // USE_ESP32 diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h index 7730506c108..3444a7b1bb7 100644 --- a/esphome/components/event/automation.h +++ b/esphome/components/event/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace event { +namespace esphome::event { template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public Parented<Event> { public: @@ -21,5 +20,4 @@ class EventTrigger : public Trigger<StringRef> { } }; -} // namespace event -} // namespace esphome +} // namespace esphome::event diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index a5d64a2748d..673ccc98028 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace event { +namespace esphome::event { static const char *const TAG = "event"; @@ -45,5 +44,4 @@ void Event::set_event_types(const std::vector<const char *> &event_types) { this->last_event_type_ = nullptr; // Reset when types change } -} // namespace event -} // namespace esphome +} // namespace esphome::event diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index ebbee0bfe23..e6fc7111c8b 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -10,8 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace event { +namespace esphome::event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -80,5 +79,4 @@ class Event : public EntityBase { const char *last_event_type_{nullptr}; }; -} // namespace event -} // namespace esphome +} // namespace esphome::event diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp index 307bee26f86..e7038d2ca9e 100644 --- a/esphome/components/exposure_notifications/exposure_notifications.cpp +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace exposure_notifications { +namespace esphome::exposure_notifications { using namespace esp32_ble_tracker; @@ -43,7 +42,6 @@ bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) { return true; } -} // namespace exposure_notifications -} // namespace esphome +} // namespace esphome::exposure_notifications #endif diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h index f7383c28d91..80184f9cfd4 100644 --- a/esphome/components/exposure_notifications/exposure_notifications.h +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace exposure_notifications { +namespace esphome::exposure_notifications { struct ExposureNotification { std::array<uint8_t, 6> address; @@ -23,7 +22,6 @@ class ExposureNotificationTrigger : public Trigger<ExposureNotification>, bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; -} // namespace exposure_notifications -} // namespace esphome +} // namespace esphome::exposure_notifications #endif diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index bb8fb92f21a..d18e6e67cb1 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ezo { +namespace esphome::ezo { static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", "EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP", @@ -238,5 +237,4 @@ void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM); } -} // namespace ezo -} // namespace esphome +} // namespace esphome::ezo diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index d80869fbd9a..aea276e0011 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include <deque> -namespace esphome { -namespace ezo { +namespace esphome::ezo { static const char *const TAG = "ezo.sensor"; @@ -102,5 +101,4 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 uint32_t start_time_ = 0; }; -} // namespace ezo -} // namespace esphome +} // namespace esphome::ezo diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 4ce4da57ffc..21307fcd7af 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ezo_pmp { +namespace esphome::ezo_pmp { static const char *const TAG = "ezo-pmp"; @@ -547,5 +546,4 @@ void EzoPMP::exec_arbitrary_command(const std::basic_string<char> &command) { this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); } -} // namespace ezo_pmp -} // namespace esphome +} // namespace esphome::ezo_pmp diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index bbfd8991707..8a6da5fe744 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -17,8 +17,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif -namespace esphome { -namespace ezo_pmp { +namespace esphome::ezo_pmp { class EzoPMP : public PollingComponent, public i2c::I2CDevice { public: @@ -247,5 +246,4 @@ template<typename... Ts> class EzoPMPArbitraryCommandAction : public Action<Ts.. EzoPMP *ezopmp_; }; -} // namespace ezo_pmp -} // namespace esphome +} // namespace esphome::ezo_pmp diff --git a/esphome/components/factory_reset/button/factory_reset_button.cpp b/esphome/components/factory_reset/button/factory_reset_button.cpp index d5823177674..efce998d9d1 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.cpp +++ b/esphome/components/factory_reset/button/factory_reset_button.cpp @@ -9,8 +9,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { static const char *const TAG = "factory_reset.button"; @@ -34,5 +33,4 @@ void FactoryResetButton::factory_reset_callback() { } #endif -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset diff --git a/esphome/components/factory_reset/button/factory_reset_button.h b/esphome/components/factory_reset/button/factory_reset_button.h index c68da2ca74e..a8cb8976141 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.h +++ b/esphome/components/factory_reset/button/factory_reset_button.h @@ -5,8 +5,7 @@ #include "esphome/components/button/button.h" #include "esphome/core/component.h" -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { class FactoryResetButton : public button::Button, public Component { public: @@ -19,5 +18,4 @@ class FactoryResetButton : public button::Button, public Component { void press_action() override; }; -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.cpp b/esphome/components/factory_reset/switch/factory_reset_switch.cpp index 75449aa5267..65404a35d57 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.cpp +++ b/esphome/components/factory_reset/switch/factory_reset_switch.cpp @@ -9,8 +9,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { static const char *const TAG = "factory_reset.switch"; @@ -39,5 +38,4 @@ void FactoryResetSwitch::factory_reset_callback() { } #endif -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.h b/esphome/components/factory_reset/switch/factory_reset_switch.h index 8ea0c791084..be80356b311 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.h +++ b/esphome/components/factory_reset/switch/factory_reset_switch.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { class FactoryResetSwitch : public switch_::Switch, public Component { public: @@ -18,5 +17,4 @@ class FactoryResetSwitch : public switch_::Switch, public Component { void write_state(bool state) override; }; -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 577c9ce600e..964ebe77a07 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "fan.h" -namespace esphome { -namespace fan { +namespace esphome::fan { // All configured fields are baked into a single stateless lambda whose // constants live in flash. The action only stores one function pointer @@ -237,5 +236,4 @@ class FanPresetSetTrigger : public Trigger<StringRef> { StringRef last_preset_mode_{}; }; -} // namespace fan -} // namespace esphome +} // namespace esphome::fan diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 9301e0cea41..853bf94ffe4 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace fan { +namespace esphome::fan { static const char *const TAG = "fan"; @@ -345,5 +344,4 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { } } -} // namespace fan -} // namespace esphome +} // namespace esphome::fan diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index d5763edf2f7..3d731e6eb08 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -8,8 +8,7 @@ #include "esphome/core/string_ref.h" #include "fan_traits.h" -namespace esphome { -namespace fan { +namespace esphome::fan { #define LOG_FAN(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -199,5 +198,4 @@ class Fan : public EntityBase { const char *preset_mode_{nullptr}; }; -} // namespace fan -} // namespace esphome +} // namespace esphome::fan diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index a2b2633af17..1d42cce371a 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -5,9 +5,7 @@ #include <initializer_list> #include "esphome/core/helpers.h" -namespace esphome { - -namespace fan { +namespace esphome::fan { class Fan; // Forward declaration @@ -95,5 +93,4 @@ class FanTraits { std::vector<const char *> compat_preset_modes_; }; -} // namespace fan -} // namespace esphome +} // namespace esphome::fan diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 504b8d473e4..8d1dd49dad3 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -3,8 +3,7 @@ #include "fastled_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace fastled_base { +namespace esphome::fastled_base { static const char *const TAG = "fastled"; @@ -39,7 +38,6 @@ void FastLEDLightOutput::write_state(light::LightState *state) { this->controller_->showLeds(this->state_parent_->current_values.get_brightness() * 255); } -} // namespace fastled_base -} // namespace esphome +} // namespace esphome::fastled_base #endif // USE_ARDUINO diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 26f0f33d2a8..8e87f67e6d2 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -15,8 +15,7 @@ #include "FastLED.h" -namespace esphome { -namespace fastled_base { +namespace esphome::fastled_base { class FastLEDLightOutput : public light::AddressableLight { public: @@ -237,7 +236,6 @@ class FastLEDLightOutput : public light::AddressableLight { optional<uint32_t> max_refresh_rate_{}; }; -} // namespace fastled_base -} // namespace esphome +} // namespace esphome::fastled_base #endif // USE_ARDUINO diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index a633fbca282..3f57789034e 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace fingerprint_grow { +namespace esphome::fingerprint_grow { static const char *const TAG = "fingerprint_grow"; @@ -581,5 +580,4 @@ void FingerprintGrowComponent::dump_config() { } } -} // namespace fingerprint_grow -} // namespace esphome +} // namespace esphome::fingerprint_grow diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 947c701c98a..7cecb7dc82f 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -9,8 +9,7 @@ #include <limits> #include <vector> -namespace esphome { -namespace fingerprint_grow { +namespace esphome::fingerprint_grow { static const uint16_t START_CODE = 0xEF01; @@ -274,5 +273,4 @@ template<typename... Ts> class AuraLEDControlAction : public Action<Ts...>, publ } }; -} // namespace fingerprint_grow -} // namespace esphome +} // namespace esphome::fingerprint_grow diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index ecf0ca6bdd2..fda9c269e51 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace font { +namespace esphome::font { static const char *const TAG = "font"; #ifdef USE_LVGL_FONT @@ -359,5 +358,4 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } #endif -} // namespace font -} // namespace esphome +} // namespace esphome::font diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 4a09d7314d2..9c9cfa0f6d0 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -10,8 +10,7 @@ #include <lvgl.h> #endif -namespace esphome { -namespace font { +namespace esphome::font { class Font; @@ -98,5 +97,4 @@ class Font #endif }; -} // namespace font -} // namespace esphome +} // namespace esphome::font diff --git a/esphome/components/fs3000/fs3000.cpp b/esphome/components/fs3000/fs3000.cpp index cea599211de..a89b1661828 100644 --- a/esphome/components/fs3000/fs3000.cpp +++ b/esphome/components/fs3000/fs3000.cpp @@ -1,8 +1,7 @@ #include "fs3000.h" #include "esphome/core/log.h" -namespace esphome { -namespace fs3000 { +namespace esphome::fs3000 { static const char *const TAG = "fs3000"; @@ -101,5 +100,4 @@ float FS3000Component::fit_raw_(uint16_t raw_value) { } } -} // namespace fs3000 -} // namespace esphome +} // namespace esphome::fs3000 diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h index e33c72215fd..c019b1366bc 100644 --- a/esphome/components/fs3000/fs3000.h +++ b/esphome/components/fs3000/fs3000.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace fs3000 { +namespace esphome::fs3000 { // FS3000 has two models, 1005 and 1015 // 1005 has a max speed detection of 7.23 m/s @@ -30,5 +29,4 @@ class FS3000Component : public PollingComponent, public i2c::I2CDevice, public s float fit_raw_(uint16_t raw_value); }; -} // namespace fs3000 -} // namespace esphome +} // namespace esphome::fs3000 diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp index 505c3cffc0f..835dc4aac03 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ft5x06 { +namespace esphome::ft5x06 { static const char *const TAG = "ft5x06.touchscreen"; @@ -99,5 +98,4 @@ bool FT5x06Touchscreen::set_mode_(FTMode mode) { return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); } -} // namespace ft5x06 -} // namespace esphome +} // namespace esphome::ft5x06 diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h index 23e5a0c49fd..7cf8769f7ac 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -6,8 +6,7 @@ #include "esphome/core/gpio.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ft5x06 { +namespace esphome::ft5x06 { enum VendorId { FT5X06_ID_UNKNOWN = 0, @@ -52,5 +51,4 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice InternalGPIOPin *interrupt_pin_{nullptr}; }; -} // namespace ft5x06 -} // namespace esphome +} // namespace esphome::ft5x06 diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index f7c4f255a07..dd8f5ba629a 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -10,8 +10,8 @@ // Registers // Reference: https://focuslcds.com/content/FT6236.pdf -namespace esphome { -namespace ft63x6 { + +namespace esphome::ft63x6 { static const uint8_t FT6X36_ADDR_DEVICE_MODE = 0x00; static const uint8_t FT63X6_ADDR_TD_STATUS = 0x02; @@ -133,5 +133,4 @@ uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { return byte; } -} // namespace ft63x6 -} // namespace esphome +} // namespace esphome::ft63x6 diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h index 8000894294e..efa03168d97 100644 --- a/esphome/components/ft63x6/ft63x6.h +++ b/esphome/components/ft63x6/ft63x6.h @@ -11,8 +11,7 @@ #include "esphome/components/touchscreen/touchscreen.h" #include "esphome/core/component.h" -namespace esphome { -namespace ft63x6 { +namespace esphome::ft63x6 { using namespace touchscreen; @@ -47,5 +46,4 @@ class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { uint8_t read_byte_(uint8_t addr); }; -} // namespace ft63x6 -} // namespace esphome +} // namespace esphome::ft63x6 diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 8aa0f517287..f801239153c 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -1,7 +1,6 @@ #include "fujitsu_general.h" -namespace esphome { -namespace fujitsu_general { +namespace esphome::fujitsu_general { // bytes' bits are reversed for fujitsu, so nibbles are ordered 1, 0, 3, 2, 5, 4, etc... @@ -400,5 +399,4 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace fujitsu_general -} // namespace esphome +} // namespace esphome::fujitsu_general diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index d7d01bf6f3e..ca93e4b3002 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -5,8 +5,7 @@ #include "esphome/core/automation.h" #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace fujitsu_general { +namespace esphome::fujitsu_general { const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius @@ -78,5 +77,4 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { bool power_{false}; }; -} // namespace fujitsu_general -} // namespace esphome +} // namespace esphome::fujitsu_general diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index 43b2fa20d3f..e84c51bde8a 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -9,8 +9,7 @@ #include "esphome/core/application.h" #include <cstring> -namespace esphome { -namespace gcja5 { +namespace esphome::gcja5 { static const char *const TAG = "gcja5"; @@ -107,5 +106,4 @@ void GCJA5Component::parse_data_() { void GCJA5Component::dump_config() { ; } -} // namespace gcja5 -} // namespace esphome +} // namespace esphome::gcja5 diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h index 30bc8771695..30c9464b4a2 100644 --- a/esphome/components/gcja5/gcja5.h +++ b/esphome/components/gcja5/gcja5.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace gcja5 { +namespace esphome::gcja5 { class GCJA5Component : public Component, public uart::UARTDevice { public: @@ -52,5 +51,4 @@ class GCJA5Component : public Component, public uart::UARTDevice { sensor::Sensor *pmc_10_0_sensor_{nullptr}; }; -} // namespace gcja5 -} // namespace esphome +} // namespace esphome::gcja5 diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 0ee718cd20e..bc0ef70578f 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace gdk101 { +namespace esphome::gdk101 { static const char *const TAG = "gdk101"; static constexpr uint8_t NUMBER_OF_READ_RETRIES = 5; @@ -206,5 +205,4 @@ bool GDK101Component::read_measurement_duration_(uint8_t *data) { return true; } -} // namespace gdk101 -} // namespace esphome +} // namespace esphome::gdk101 diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index abe3fd60d83..2ef75262947 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -13,8 +13,7 @@ #endif // USE_TEXT_SENSOR #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace gdk101 { +namespace esphome::gdk101 { static const uint8_t GDK101_REG_READ_FIRMWARE = 0xB4; // Firmware version static const uint8_t GDK101_REG_RESET = 0xA0; // Reset register - reading its value triggers reset @@ -55,5 +54,4 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { uint8_t reset_retries_remaining_{0}; }; -} // namespace gdk101 -} // namespace esphome +} // namespace esphome::gdk101 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp index 38328c4b034..75f0c69a7b1 100644 --- a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "gl_r01_i2c.h" -namespace esphome { -namespace gl_r01_i2c { +namespace esphome::gl_r01_i2c { static const char *const TAG = "gl_r01_i2c"; @@ -65,5 +64,4 @@ void GLR01I2CComponent::read_distance_() { } } -} // namespace gl_r01_i2c -} // namespace esphome +} // namespace esphome::gl_r01_i2c diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.h b/esphome/components/gl_r01_i2c/gl_r01_i2c.h index 9a7aa023fd2..1d023c245ae 100644 --- a/esphome/components/gl_r01_i2c/gl_r01_i2c.h +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace gl_r01_i2c { +namespace esphome::gl_r01_i2c { class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent { public: @@ -18,5 +17,4 @@ class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public P uint16_t version_{0}; }; -} // namespace gl_r01_i2c -} // namespace esphome +} // namespace esphome::gl_r01_i2c diff --git a/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp index c8b0f13d3a8..0dd4a13a212 100644 --- a/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp +++ b/esphome/components/gp2y1010au0f/gp2y1010au0f.cpp @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace gp2y1010au0f { +namespace esphome::gp2y1010au0f { static const char *const TAG = "gp2y1010au0f"; static const float MIN_VOLTAGE = 0.0f; @@ -65,5 +64,4 @@ void GP2Y1010AU0FSensor::loop() { this->sample_sum_ += read_voltage; } -} // namespace gp2y1010au0f -} // namespace esphome +} // namespace esphome::gp2y1010au0f diff --git a/esphome/components/gp2y1010au0f/gp2y1010au0f.h b/esphome/components/gp2y1010au0f/gp2y1010au0f.h index 5ee58e68d29..f3398ac4a3a 100644 --- a/esphome/components/gp2y1010au0f/gp2y1010au0f.h +++ b/esphome/components/gp2y1010au0f/gp2y1010au0f.h @@ -5,8 +5,7 @@ #include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace gp2y1010au0f { +namespace esphome::gp2y1010au0f { class GP2Y1010AU0FSensor : public sensor::Sensor, public PollingComponent { public: @@ -48,5 +47,4 @@ class GP2Y1010AU0FSensor : public sensor::Sensor, public PollingComponent { bool is_sampling_ = false; }; -} // namespace gp2y1010au0f -} // namespace esphome +} // namespace esphome::gp2y1010au0f diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp index 11c2f9a7c06..7e93bb6295d 100644 --- a/esphome/components/gp8403/gp8403.cpp +++ b/esphome/components/gp8403/gp8403.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace gp8403 { +namespace esphome::gp8403 { static const char *const TAG = "gp8403"; @@ -51,5 +50,4 @@ void GP8403Component::write_state(float state, uint8_t channel) { } } -} // namespace gp8403 -} // namespace esphome +} // namespace esphome::gp8403 diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index a19df15515d..d30d9674795 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -3,8 +3,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace gp8403 { +namespace esphome::gp8403 { enum GP8403Voltage : uint8_t { GP8403_VOLTAGE_5V = 0x00, @@ -30,5 +29,4 @@ class GP8403Component : public Component, public i2c::I2CDevice { GP8403Model model_{GP8403Model::GP8403}; }; -} // namespace gp8403 -} // namespace esphome +} // namespace esphome::gp8403 diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp index dfdc2d6ccb1..7a22a280ac7 100644 --- a/esphome/components/gp8403/output/gp8403_output.cpp +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace gp8403 { +namespace esphome::gp8403 { static const char *const TAG = "gp8403.output"; @@ -16,5 +15,4 @@ void GP8403Output::dump_config() { void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); } -} // namespace gp8403 -} // namespace esphome +} // namespace esphome::gp8403 diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h index c0d6650500e..8b1f920680d 100644 --- a/esphome/components/gp8403/output/gp8403_output.h +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -5,8 +5,7 @@ #include "esphome/components/gp8403/gp8403.h" -namespace esphome { -namespace gp8403 { +namespace esphome::gp8403 { class GP8403Output : public Component, public output::FloatOutput, public Parented<GP8403Component> { public: @@ -19,5 +18,4 @@ class GP8403Output : public Component, public output::FloatOutput, public Parent uint8_t channel_; }; -} // namespace gp8403 -} // namespace esphome +} // namespace esphome::gp8403 diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 1f0154c70b7..ff07d76901f 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { static const char *const TAG = "gpio.binary_sensor"; @@ -86,5 +85,4 @@ void GPIOBinarySensor::loop() { float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 24efc2a0e60..100edb4cca4 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { // Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { @@ -64,5 +63,4 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon GPIOBinarySensorStore store_; }; -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 4e2a306fc94..1fecfbf0dd1 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { static const char *const TAG = "gpio.one_wire"; @@ -202,5 +201,4 @@ uint64_t IRAM_ATTR GPIOOneWireBus::search_int() { return address; } -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.h b/esphome/components/gpio/one_wire/gpio_one_wire.h index 88747039715..02797b57371 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.h +++ b/esphome/components/gpio/one_wire/gpio_one_wire.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/one_wire/one_wire.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { class GPIOOneWireBus : public one_wire::OneWireBus, public Component { public: @@ -38,5 +37,4 @@ class GPIOOneWireBus : public one_wire::OneWireBus, public Component { bool read_bit_(uint32_t *t); }; -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/output/gpio_binary_output.cpp b/esphome/components/gpio/output/gpio_binary_output.cpp index 13538b6f2bd..402ad03c49a 100644 --- a/esphome/components/gpio/output/gpio_binary_output.cpp +++ b/esphome/components/gpio/output/gpio_binary_output.cpp @@ -1,8 +1,7 @@ #include "gpio_binary_output.h" #include "esphome/core/log.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { static const char *const TAG = "gpio.output"; @@ -12,5 +11,4 @@ void GPIOBinaryOutput::dump_config() { LOG_BINARY_OUTPUT(this); } -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/output/gpio_binary_output.h b/esphome/components/gpio/output/gpio_binary_output.h index 6b72c61c0f6..4100cb94c2b 100644 --- a/esphome/components/gpio/output/gpio_binary_output.h +++ b/esphome/components/gpio/output/gpio_binary_output.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { class GPIOBinaryOutput : public output::BinaryOutput, public Component { public: @@ -25,5 +24,4 @@ class GPIOBinaryOutput : public output::BinaryOutput, public Component { GPIOPin *pin_; }; -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9c6464815a0..d432655a2a4 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -1,8 +1,7 @@ #include "gpio_switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { static const char *const TAG = "switch.gpio"; #ifdef USE_GPIO_SWITCH_INTERLOCK @@ -79,5 +78,4 @@ void GPIOSwitch::write_state(bool state) { void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } #endif -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index f7415d1dba1..7ed0de7c6f0 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace gpio { +namespace esphome::gpio { class GPIOSwitch final : public switch_::Switch, public Component { public: @@ -33,5 +32,4 @@ class GPIOSwitch final : public switch_::Switch, public Component { #endif }; -} // namespace gpio -} // namespace esphome +} // namespace esphome::gpio diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 65cddcd9842..4b8abd219b0 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -1,8 +1,7 @@ #include "gps.h" #include "esphome/core/log.h" -namespace esphome { -namespace gps { +namespace esphome::gps { static const char *const TAG = "gps"; @@ -91,5 +90,4 @@ void GPS::loop() { } } -} // namespace gps -} // namespace esphome +} // namespace esphome::gps diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 36923c68be4..9cd79e25b42 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace gps { +namespace esphome::gps { class GPS; @@ -67,5 +66,4 @@ class GPS : public PollingComponent, public uart::UARTDevice { std::vector<GPSListener *> listeners_{}; }; -} // namespace gps -} // namespace esphome +} // namespace esphome::gps diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index fb662a3d60e..3859983ceb7 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -1,8 +1,7 @@ #include "gps_time.h" #include "esphome/core/log.h" -namespace esphome { -namespace gps { +namespace esphome::gps { static const char *const TAG = "gps.time"; @@ -24,5 +23,4 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { this->has_time_ = true; } -} // namespace gps -} // namespace esphome +} // namespace esphome::gps diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index a8414f00154..3d6d870efc2 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -4,8 +4,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/core/component.h" -namespace esphome { -namespace gps { +namespace esphome::gps { class GPSTime : public time::RealTimeClock, public GPSListener { public: @@ -21,5 +20,4 @@ class GPSTime : public time::RealTimeClock, public GPSListener { bool has_time_{false}; }; -} // namespace gps -} // namespace esphome +} // namespace esphome::gps diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 801c97e3f56..5d59f605095 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -4,8 +4,8 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include <algorithm> -namespace esphome { -namespace graph { + +namespace esphome::graph { using namespace display; @@ -397,5 +397,4 @@ void Graph::dump_config() { } } -} // namespace graph -} // namespace esphome +} // namespace esphome::graph diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index cf1672f217a..81971e457cd 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -5,8 +5,7 @@ #include <cstdlib> #include "esphome/components/display/display.h" -namespace esphome { -namespace graphical_display_menu { +namespace esphome::graphical_display_menu { static const char *const TAG = "graphical_display_menu"; @@ -246,5 +245,4 @@ void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, co void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } -} // namespace graphical_display_menu -} // namespace esphome +} // namespace esphome::graphical_display_menu diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index 732ebd9632e..705c741dd0f 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -1,8 +1,7 @@ #include "gree.h" #include "esphome/components/remote_base/remote_base.h" -namespace esphome { -namespace gree { +namespace esphome::gree { static const char *const TAG = "gree.climate"; @@ -241,5 +240,4 @@ uint8_t GreeClimate::preset_() { return GREE_PRESET_NONE; } -} // namespace gree -} // namespace esphome +} // namespace esphome::gree diff --git a/esphome/components/gree/switch/gree_switch.cpp b/esphome/components/gree/switch/gree_switch.cpp index 13f14e54530..2f649733afc 100644 --- a/esphome/components/gree/switch/gree_switch.cpp +++ b/esphome/components/gree/switch/gree_switch.cpp @@ -1,8 +1,7 @@ #include "gree_switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace gree { +namespace esphome::gree { static const char *const TAG = "gree.switch"; @@ -20,5 +19,4 @@ void GreeModeBitSwitch::write_state(bool state) { this->publish_state(state); } -} // namespace gree -} // namespace esphome +} // namespace esphome::gree diff --git a/esphome/components/gree/switch/gree_switch.h b/esphome/components/gree/switch/gree_switch.h index 239ac4bf171..9d9f187f9d5 100644 --- a/esphome/components/gree/switch/gree_switch.h +++ b/esphome/components/gree/switch/gree_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/components/gree/gree.h" -namespace esphome { -namespace gree { +namespace esphome::gree { class GreeModeBitSwitch : public switch_::Switch, public Component, public Parented<GreeClimate> { public: @@ -20,5 +19,4 @@ class GreeModeBitSwitch : public switch_::Switch, public Component, public Paren uint8_t bit_mask_; }; -} // namespace gree -} // namespace esphome +} // namespace esphome::gree diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index b0f3429314c..0de0b02182f 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace grove_gas_mc_v2 { +namespace esphome::grove_gas_mc_v2 { static const char *const TAG = "grove_gas_mc_v2"; @@ -82,5 +81,4 @@ void GroveGasMultichannelV2Component::dump_config() { } } -} // namespace grove_gas_mc_v2 -} // namespace esphome +} // namespace esphome::grove_gas_mc_v2 diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h index aab881bd059..38165ab68cd 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace grove_gas_mc_v2 { +namespace esphome::grove_gas_mc_v2 { class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(tvoc) @@ -33,5 +32,4 @@ class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2C bool read_sensor_(uint8_t address, sensor::Sensor *sensor); }; -} // namespace grove_gas_mc_v2 -} // namespace esphome +} // namespace esphome::grove_gas_mc_v2 diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index c10fa4cf257..eaa1440c4da 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace grove_tb6612fng { +namespace esphome::grove_tb6612fng { static const char *const TAG = "GroveMotorDriveTB6612FNG"; @@ -167,5 +166,4 @@ void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t return; } } -} // namespace grove_tb6612fng -} // namespace esphome +} // namespace esphome::grove_tb6612fng diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index bf471632267..c0216805198 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -4,7 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -//#include "esphome/core/helpers.h" +// #include "esphome/core/helpers.h" /* Grove_Motor_Driver_TB6612FNG.h @@ -33,8 +33,7 @@ THE SOFTWARE. */ -namespace esphome { -namespace grove_tb6612fng { +namespace esphome::grove_tb6612fng { enum MotorChannelTypeT { MOTOR_CHA = 0, @@ -219,5 +218,4 @@ class GROVETB6612FNGMotorChangeAddressAction : public Action<Ts...>, public Pare void play(const Ts &...x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } }; -} // namespace grove_tb6612fng -} // namespace esphome +} // namespace esphome::grove_tb6612fng diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index 29974258723..41beb6e4e94 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace growatt_solar { +namespace esphome::growatt_solar { static const char *const TAG = "growatt_solar"; @@ -141,5 +140,4 @@ void GrowattSolar::dump_config() { this->address_); } -} // namespace growatt_solar -} // namespace esphome +} // namespace esphome::growatt_solar diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 833d6a36ddb..7eba7956015 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace growatt_solar { +namespace esphome::growatt_solar { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; @@ -83,5 +82,4 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { GrowattProtocolVersion protocol_version_; }; -} // namespace growatt_solar -} // namespace esphome +} // namespace esphome::growatt_solar diff --git a/esphome/components/gt911/binary_sensor/gt911_button.cpp b/esphome/components/gt911/binary_sensor/gt911_button.cpp index 35ffaecefcc..7b356f3946d 100644 --- a/esphome/components/gt911/binary_sensor/gt911_button.cpp +++ b/esphome/components/gt911/binary_sensor/gt911_button.cpp @@ -1,8 +1,7 @@ #include "gt911_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace gt911 { +namespace esphome::gt911 { static const char *const TAG = "GT911.binary_sensor"; @@ -23,5 +22,4 @@ void GT911Button::update_button(uint8_t index, bool state) { this->publish_state(state); } -} // namespace gt911 -} // namespace esphome +} // namespace esphome::gt911 diff --git a/esphome/components/gt911/binary_sensor/gt911_button.h b/esphome/components/gt911/binary_sensor/gt911_button.h index 556ed65f919..5aab4570950 100644 --- a/esphome/components/gt911/binary_sensor/gt911_button.h +++ b/esphome/components/gt911/binary_sensor/gt911_button.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace gt911 { +namespace esphome::gt911 { class GT911Button : public binary_sensor::BinarySensor, public Component, @@ -24,5 +23,4 @@ class GT911Button : public binary_sensor::BinarySensor, uint8_t index_; }; -} // namespace gt911 -} // namespace esphome +} // namespace esphome::gt911 diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 17bfa82cb45..2152ae7b842 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" -namespace esphome { -namespace gt911 { +namespace esphome::gt911 { static const char *const TAG = "gt911.touchscreen"; @@ -157,5 +156,4 @@ void GT911Touchscreen::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); } -} // namespace gt911 -} // namespace esphome +} // namespace esphome::gt911 diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index a6577b5879d..0f1eeae7207 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace gt911 { +namespace esphome::gt911 { class GT911ButtonListener { public: @@ -57,5 +56,4 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. }; -} // namespace gt911 -} // namespace esphome +} // namespace esphome::gt911 diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h index c1ce7c01ea4..e345867d6f8 100644 --- a/esphome/components/haier/automation.h +++ b/esphome/components/haier/automation.h @@ -4,8 +4,7 @@ #include "haier_base.h" #include "hon_climate.h" -namespace esphome { -namespace haier { +namespace esphome::haier { template<typename... Ts> class DisplayOnAction : public Action<Ts...> { public: @@ -126,5 +125,4 @@ template<typename... Ts> class PowerToggleAction : public Action<Ts...> { HaierClimateBase *parent_; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/button/self_cleaning.cpp b/esphome/components/haier/button/self_cleaning.cpp index 128726036e3..bf4baa716e6 100644 --- a/esphome/components/haier/button/self_cleaning.cpp +++ b/esphome/components/haier/button/self_cleaning.cpp @@ -1,9 +1,7 @@ #include "self_cleaning.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/button/self_cleaning.h b/esphome/components/haier/button/self_cleaning.h index 308fb70f064..9d330e4dfe4 100644 --- a/esphome/components/haier/button/self_cleaning.h +++ b/esphome/components/haier/button/self_cleaning.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../hon_climate.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class SelfCleaningButton : public button::Button, public Parented<HonClimate> { public: @@ -14,5 +13,4 @@ class SelfCleaningButton : public button::Button, public Parented<HonClimate> { void press_action() override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/button/steri_cleaning.cpp b/esphome/components/haier/button/steri_cleaning.cpp index 02b723f1a4c..8c4f5808b21 100644 --- a/esphome/components/haier/button/steri_cleaning.cpp +++ b/esphome/components/haier/button/steri_cleaning.cpp @@ -1,9 +1,7 @@ #include "steri_cleaning.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/button/steri_cleaning.h b/esphome/components/haier/button/steri_cleaning.h index 6cad313fb38..cac02dd2678 100644 --- a/esphome/components/haier/button/steri_cleaning.h +++ b/esphome/components/haier/button/steri_cleaning.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../hon_climate.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class SteriCleaningButton : public button::Button, public Parented<HonClimate> { public: @@ -14,5 +13,4 @@ class SteriCleaningButton : public button::Button, public Parented<HonClimate> { void press_action() override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 4a06066d3c9..74a218263d9 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -10,8 +10,7 @@ using namespace esphome::climate; using namespace esphome::uart; -namespace esphome { -namespace haier { +namespace esphome::haier { static const char *const TAG = "haier.climate"; constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000; @@ -418,5 +417,4 @@ void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command this->last_request_timestamp_ = std::chrono::steady_clock::now(); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 0c416623c08..13e8d7548d5 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -11,8 +11,7 @@ #include "esphome/components/switch/switch.h" #endif -namespace esphome { -namespace haier { +namespace esphome::haier { enum class ActionRequest : uint8_t { SEND_CUSTOM_COMMAND = 0, @@ -177,5 +176,4 @@ class HaierClimateBase : public esphome::Component, ESPPreferenceObject base_rtc_; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 87b8add2a37..0ad9b00ce4f 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -9,8 +9,7 @@ using namespace esphome::climate; using namespace esphome::uart; -namespace esphome { -namespace haier { +namespace esphome::haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; @@ -1370,5 +1369,4 @@ bool HonClimate::should_get_big_data_() { return false; } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a0bcdfb548c..5b477a5cea1 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -18,8 +18,7 @@ #include "haier_base.h" #include "hon_packet.h" -namespace esphome { -namespace haier { +namespace esphome::haier { enum class CleaningState : uint8_t { NO_CLEANING = 0, @@ -201,5 +200,4 @@ class HonClimate : public HaierClimateBase { SwitchState quiet_mode_state_{SwitchState::OFF}; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index 615f93528ec..63799f04ba1 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -2,9 +2,7 @@ #include <cstdint> -namespace esphome { -namespace haier { -namespace hon_protocol { +namespace esphome::haier::hon_protocol { enum class VerticalSwingMode : uint8_t { HEALTH_UP = 0x01, @@ -255,6 +253,4 @@ const std::string HON_ALARM_MESSAGES[] = { constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]); -} // namespace hon_protocol -} // namespace haier -} // namespace esphome +} // namespace esphome::haier::hon_protocol diff --git a/esphome/components/haier/logger_handler.cpp b/esphome/components/haier/logger_handler.cpp index f886318097a..1c4004cf6ce 100644 --- a/esphome/components/haier/logger_handler.cpp +++ b/esphome/components/haier/logger_handler.cpp @@ -1,8 +1,7 @@ #include "logger_handler.h" #include "esphome/core/log.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) { switch (level) { @@ -29,5 +28,4 @@ void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/logger_handler.h b/esphome/components/haier/logger_handler.h index 2955468f37e..192259409f2 100644 --- a/esphome/components/haier/logger_handler.h +++ b/esphome/components/haier/logger_handler.h @@ -3,12 +3,10 @@ // HaierProtocol #include <utils/haier_log.h> -namespace esphome { -namespace haier { +namespace esphome::haier { // This file is called in the code generated by python script // Do not use it directly! void init_haier_protocol_logging(); -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 752f4d4f1c0..bd5678a4258 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -7,8 +7,7 @@ using namespace esphome::climate; using namespace esphome::uart; -namespace esphome { -namespace haier { +namespace esphome::haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; @@ -555,5 +554,4 @@ void Smartair2Climate::set_alternative_swing_control(bool swing_control) { this->use_alternative_swing_control_ = swing_control; } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h index 6914d8a1fbb..68b0e4a0db2 100644 --- a/esphome/components/haier/smartair2_climate.h +++ b/esphome/components/haier/smartair2_climate.h @@ -3,8 +3,7 @@ #include <chrono> #include "haier_base.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class Smartair2Climate : public HaierClimateBase { public: @@ -36,5 +35,4 @@ class Smartair2Climate : public HaierClimateBase { bool use_alternative_swing_control_; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h index 22570ff048a..144b7db8795 100644 --- a/esphome/components/haier/smartair2_packet.h +++ b/esphome/components/haier/smartair2_packet.h @@ -1,8 +1,6 @@ #pragma once -namespace esphome { -namespace haier { -namespace smartair2_protocol { +namespace esphome::haier::smartair2_protocol { enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 }; @@ -83,6 +81,4 @@ struct HaierStatus { HaierPacketControl control; }; -} // namespace smartair2_protocol -} // namespace haier -} // namespace esphome +} // namespace esphome::haier::smartair2_protocol diff --git a/esphome/components/haier/switch/beeper.cpp b/esphome/components/haier/switch/beeper.cpp index 1ce64d0848c..40b048be5c7 100644 --- a/esphome/components/haier/switch/beeper.cpp +++ b/esphome/components/haier/switch/beeper.cpp @@ -1,7 +1,6 @@ #include "beeper.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void BeeperSwitch::write_state(bool state) { if (this->parent_->get_beeper_state() != state) { @@ -10,5 +9,4 @@ void BeeperSwitch::write_state(bool state) { this->publish_state(state); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/beeper.h b/esphome/components/haier/switch/beeper.h index 7396a7a0dda..2d20f1cd835 100644 --- a/esphome/components/haier/switch/beeper.h +++ b/esphome/components/haier/switch/beeper.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../hon_climate.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class BeeperSwitch : public switch_::Switch, public Parented<HonClimate> { public: @@ -14,5 +13,4 @@ class BeeperSwitch : public switch_::Switch, public Parented<HonClimate> { void write_state(bool state) override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/display.cpp b/esphome/components/haier/switch/display.cpp index 5e24843dcf1..e34b45985f4 100644 --- a/esphome/components/haier/switch/display.cpp +++ b/esphome/components/haier/switch/display.cpp @@ -1,7 +1,6 @@ #include "display.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void DisplaySwitch::write_state(bool state) { if (this->parent_->get_display_state() != state) { @@ -10,5 +9,4 @@ void DisplaySwitch::write_state(bool state) { this->publish_state(state); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/display.h b/esphome/components/haier/switch/display.h index f93ccfcdb7e..9baf3b9fb8c 100644 --- a/esphome/components/haier/switch/display.h +++ b/esphome/components/haier/switch/display.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../haier_base.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class DisplaySwitch : public switch_::Switch, public Parented<HaierClimateBase> { public: @@ -14,5 +13,4 @@ class DisplaySwitch : public switch_::Switch, public Parented<HaierClimateBase> void write_state(bool state) override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/health_mode.cpp b/esphome/components/haier/switch/health_mode.cpp index 3715759bdd0..c8656fe0d4e 100644 --- a/esphome/components/haier/switch/health_mode.cpp +++ b/esphome/components/haier/switch/health_mode.cpp @@ -1,7 +1,6 @@ #include "health_mode.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void HealthModeSwitch::write_state(bool state) { if (this->parent_->get_health_mode() != state) { @@ -10,5 +9,4 @@ void HealthModeSwitch::write_state(bool state) { this->publish_state(state); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/health_mode.h b/esphome/components/haier/switch/health_mode.h index cfd2aa2f226..ec77b1638aa 100644 --- a/esphome/components/haier/switch/health_mode.h +++ b/esphome/components/haier/switch/health_mode.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../haier_base.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class HealthModeSwitch : public switch_::Switch, public Parented<HaierClimateBase> { public: @@ -14,5 +13,4 @@ class HealthModeSwitch : public switch_::Switch, public Parented<HaierClimateBas void write_state(bool state) override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/quiet_mode.cpp b/esphome/components/haier/switch/quiet_mode.cpp index 056312b5f0c..bee9b0a6728 100644 --- a/esphome/components/haier/switch/quiet_mode.cpp +++ b/esphome/components/haier/switch/quiet_mode.cpp @@ -1,7 +1,6 @@ #include "quiet_mode.h" -namespace esphome { -namespace haier { +namespace esphome::haier { void QuietModeSwitch::write_state(bool state) { if (this->parent_->get_quiet_mode_state() != state) { @@ -10,5 +9,4 @@ void QuietModeSwitch::write_state(bool state) { this->publish_state(state); } -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/haier/switch/quiet_mode.h b/esphome/components/haier/switch/quiet_mode.h index bad52895008..8ef7b5bb894 100644 --- a/esphome/components/haier/switch/quiet_mode.h +++ b/esphome/components/haier/switch/quiet_mode.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../hon_climate.h" -namespace esphome { -namespace haier { +namespace esphome::haier { class QuietModeSwitch : public switch_::Switch, public Parented<HonClimate> { public: @@ -14,5 +13,4 @@ class QuietModeSwitch : public switch_::Switch, public Parented<HonClimate> { void write_state(bool state) override; }; -} // namespace haier -} // namespace esphome +} // namespace esphome::haier diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp index 20dddf39ed0..9257a37fd92 100644 --- a/esphome/components/havells_solar/havells_solar.cpp +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace havells_solar { +namespace esphome::havells_solar { static const char *const TAG = "havells_solar"; @@ -164,5 +163,4 @@ void HavellsSolar::dump_config() { LOG_SENSOR(" ", "DCI Of T", this->dci_of_t_sensor_); } -} // namespace havells_solar -} // namespace esphome +} // namespace esphome::havells_solar diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h index f3ac8fafcfc..c54b0dcf148 100644 --- a/esphome/components/havells_solar/havells_solar.h +++ b/esphome/components/havells_solar/havells_solar.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace havells_solar { +namespace esphome::havells_solar { class HavellsSolar : public PollingComponent, public modbus::ModbusDevice { public: @@ -113,5 +112,4 @@ class HavellsSolar : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *dci_of_t_sensor_{nullptr}; }; -} // namespace havells_solar -} // namespace esphome +} // namespace esphome::havells_solar diff --git a/esphome/components/havells_solar/havells_solar_registers.h b/esphome/components/havells_solar/havells_solar_registers.h index 8e1cb3ec7af..4ed797b3e7a 100644 --- a/esphome/components/havells_solar/havells_solar_registers.h +++ b/esphome/components/havells_solar/havells_solar_registers.h @@ -1,6 +1,6 @@ #pragma once -namespace esphome { -namespace havells_solar { + +namespace esphome::havells_solar { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; @@ -45,5 +45,4 @@ static const uint16_t HAVELLS_GFCI_VALUE = 0x002A; static const uint16_t HAVELLS_DCI_OF_R = 0x002B; static const uint16_t HAVELLS_DCI_OF_S = 0x002C; static const uint16_t HAVELLS_DCI_OF_T = 0x002D; -} // namespace havells_solar -} // namespace esphome +} // namespace esphome::havells_solar diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index d548128b99b..fc2a738e8d5 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -1,8 +1,7 @@ #include "hbridge_fan.h" #include "esphome/core/log.h" -namespace esphome { -namespace hbridge { +namespace esphome::hbridge { static const char *const TAG = "fan.hbridge"; @@ -93,5 +92,4 @@ void HBridgeFan::write_state_() { this->oscillating_->set_state(this->oscillating); } -} // namespace hbridge -} // namespace esphome +} // namespace esphome::hbridge diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 997f66ae486..62149d99cd0 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace hbridge { +namespace esphome::hbridge { enum DecayMode { DECAY_MODE_SLOW = 0, @@ -56,5 +55,4 @@ template<typename... Ts> class BrakeAction : public Action<Ts...> { HBridgeFan *parent_; }; -} // namespace hbridge -} // namespace esphome +} // namespace esphome::hbridge diff --git a/esphome/components/hbridge/light/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h index 4e064d53522..16408f24f1b 100644 --- a/esphome/components/hbridge/light/hbridge_light_output.h +++ b/esphome/components/hbridge/light/hbridge_light_output.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace hbridge { +namespace esphome::hbridge { class HBridgeLightOutput : public Component, public light::LightOutput { public: @@ -68,5 +67,4 @@ class HBridgeLightOutput : public Component, public light::LightOutput { HighFrequencyLoopRequester high_freq_; }; -} // namespace hbridge -} // namespace esphome +} // namespace esphome::hbridge diff --git a/esphome/components/hbridge/switch/hbridge_switch.cpp b/esphome/components/hbridge/switch/hbridge_switch.cpp index 55012fed211..1012a264f24 100644 --- a/esphome/components/hbridge/switch/hbridge_switch.cpp +++ b/esphome/components/hbridge/switch/hbridge_switch.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace hbridge { +namespace esphome::hbridge { static const char *const TAG = "switch.hbridge"; @@ -89,5 +88,4 @@ void HBridgeSwitch::timer_fn_() { this->timer_running_ = false; } -} // namespace hbridge -} // namespace esphome +} // namespace esphome::hbridge diff --git a/esphome/components/hbridge/switch/hbridge_switch.h b/esphome/components/hbridge/switch/hbridge_switch.h index ce00c6baa2a..de867271fe2 100644 --- a/esphome/components/hbridge/switch/hbridge_switch.h +++ b/esphome/components/hbridge/switch/hbridge_switch.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace hbridge { +namespace esphome::hbridge { enum RelayState : uint8_t { RELAY_STATE_OFF = 0, @@ -46,5 +45,4 @@ class HBridgeSwitch : public switch_::Switch, public Component { bool optimistic_{false}; }; -} // namespace hbridge -} // namespace esphome +} // namespace esphome::hbridge diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index fa293f6fc5e..6692345e62e 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace hdc1080 { +namespace esphome::hdc1080 { static const char *const TAG = "hdc1080"; @@ -78,5 +77,4 @@ void HDC1080Component::update() { }); } -} // namespace hdc1080 -} // namespace esphome +} // namespace esphome::hdc1080 diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index a5bece82c49..1e3bf777888 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace hdc1080 { +namespace esphome::hdc1080 { class HDC1080Component : public PollingComponent, public i2c::I2CDevice { public: @@ -21,5 +20,4 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_{nullptr}; }; -} // namespace hdc1080 -} // namespace esphome +} // namespace esphome::hdc1080 diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp index 0334b30eeca..946cf4898f1 100644 --- a/esphome/components/hdc2010/hdc2010.cpp +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -2,8 +2,8 @@ #include "hdc2010.h" // https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js // https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp -namespace esphome { -namespace hdc2010 { + +namespace esphome::hdc2010 { static const char *const TAG = "hdc2010"; @@ -93,5 +93,4 @@ float HDC2010Component::read_humidity() { return (float) humidity * 0.001525879f; } -} // namespace hdc2010 -} // namespace esphome +} // namespace esphome::hdc2010 diff --git a/esphome/components/hdc2010/hdc2010.h b/esphome/components/hdc2010/hdc2010.h index 52c00686e65..ad6df3ff48d 100644 --- a/esphome/components/hdc2010/hdc2010.h +++ b/esphome/components/hdc2010/hdc2010.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace hdc2010 { +namespace esphome::hdc2010 { class HDC2010Component : public PollingComponent, public i2c::I2CDevice { public: @@ -28,5 +27,4 @@ class HDC2010Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace hdc2010 -} // namespace esphome +} // namespace esphome::hdc2010 diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index 47440cc1f73..84edbb2866a 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace he60r { +namespace esphome::he60r { static const char *const TAG = "he60r.cover"; static const uint8_t QUERY_BYTE = 0x38; @@ -264,5 +263,4 @@ void HE60rCover::recompute_position_() { } } -} // namespace he60r -} // namespace esphome +} // namespace esphome::he60r diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h index 02a2b44e66f..e7b5c97969d 100644 --- a/esphome/components/he60r/he60r.h +++ b/esphome/components/he60r/he60r.h @@ -5,8 +5,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace he60r { +namespace esphome::he60r { class HE60rCover : public cover::Cover, public Component, public uart::UARTDevice { public: @@ -41,5 +40,4 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic uint8_t counter_{}; }; -} // namespace he60r -} // namespace esphome +} // namespace esphome::he60r diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 11e7672dc13..8e9a2c52985 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -8,8 +8,7 @@ #include "esphome/components/remote_base/remote_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace heatpumpir { +namespace esphome::heatpumpir { // IRSenderESPHome - bridge between ESPHome's remote_transmitter and HeatpumpIR library // Defined here (not in a header) to isolate HeatpumpIR's headers from the rest of ESPHome, @@ -243,7 +242,6 @@ void HeatpumpIRClimate::transmit_state() { swing_h_cmd); } -} // namespace heatpumpir -} // namespace esphome +} // namespace esphome::heatpumpir #endif diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 6270dd1e5af..a277424df6b 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -8,8 +8,7 @@ // that conflict with ESPHome. class HeatpumpIR; -namespace esphome { -namespace heatpumpir { +namespace esphome::heatpumpir { // Simple enum to represent protocols. enum Protocol { @@ -126,7 +125,6 @@ class HeatpumpIRClimate : public climate_ir::ClimateIR { float min_temperature_; }; -} // namespace heatpumpir -} // namespace esphome +} // namespace esphome::heatpumpir #endif diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 69469cab2ea..ea211a7a59b 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -1,7 +1,6 @@ #include "hitachi_ac344.h" -namespace esphome { -namespace hitachi_ac344 { +namespace esphome::hitachi_ac344 { static const char *const TAG = "climate.hitachi_ac344"; @@ -366,5 +365,4 @@ void HitachiClimate::dump_state_(const char action[], uint8_t state[]) { ESP_LOGV(TAG, "%s: %02X %02X %02X", action, state[40], state[41], state[42]); } -} // namespace hitachi_ac344 -} // namespace esphome +} // namespace esphome::hitachi_ac344 diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index 0877b832617..b9d776cc59d 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace hitachi_ac344 { +namespace esphome::hitachi_ac344 { const uint16_t HITACHI_AC344_HDR_MARK = 3300; // ac const uint16_t HITACHI_AC344_HDR_SPACE = 1700; // ac @@ -117,5 +116,4 @@ class HitachiClimate : public climate_ir::ClimateIR { void dump_state_(const char action[], uint8_t remote_state[]); }; -} // namespace hitachi_ac344 -} // namespace esphome +} // namespace esphome::hitachi_ac344 diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 0b3cc99a82b..cdf8dda22ce 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -1,7 +1,6 @@ #include "hitachi_ac424.h" -namespace esphome { -namespace hitachi_ac424 { +namespace esphome::hitachi_ac424 { static const char *const TAG = "climate.hitachi_ac424"; @@ -367,5 +366,4 @@ void HitachiClimate::dump_state_(const char action[], uint8_t state[]) { ESP_LOGV(TAG, "%s: %02X %02X %02X", action, state[40], state[41], state[42]); } -} // namespace hitachi_ac424 -} // namespace esphome +} // namespace esphome::hitachi_ac424 diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.h b/esphome/components/hitachi_ac424/hitachi_ac424.h index 1005aa6df79..ef7f128a5a2 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.h +++ b/esphome/components/hitachi_ac424/hitachi_ac424.h @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace hitachi_ac424 { +namespace esphome::hitachi_ac424 { const uint16_t HITACHI_AC424_HDR_MARK = 3416; // ac const uint16_t HITACHI_AC424_HDR_SPACE = 1604; // ac @@ -119,5 +118,4 @@ class HitachiClimate : public climate_ir::ClimateIR { void dump_state_(const char action[], uint8_t remote_state[]); }; -} // namespace hitachi_ac424 -} // namespace esphome +} // namespace esphome::hitachi_ac424 diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 22f292e47e1..c92c76a20ad 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -1,8 +1,7 @@ #include "hlw8012.h" #include "esphome/core/log.h" -namespace esphome { -namespace hlw8012 { +namespace esphome::hlw8012 { static const char *const TAG = "hlw8012"; @@ -104,5 +103,4 @@ void HLW8012Component::update() { } } -} // namespace hlw8012 -} // namespace esphome +} // namespace esphome::hlw8012 diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 8a13ec07d8e..d1d340bf45d 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -7,8 +7,7 @@ #include <cinttypes> -namespace esphome { -namespace hlw8012 { +namespace esphome::hlw8012 { enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE }; @@ -72,5 +71,4 @@ class HLW8012Component : public PollingComponent { float power_multiplier_{0.0f}; }; -} // namespace hlw8012 -} // namespace esphome +} // namespace esphome::hlw8012 diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 9343b47823f..f46a6b85806 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "hm3301.h" -namespace esphome { -namespace hm3301 { +namespace esphome::hm3301 { static const char *const TAG = "hm3301.sensor"; @@ -94,5 +93,4 @@ uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) { return (uint16_t) data[i * 2] << 8 | data[i * 2 + 1]; } -} // namespace hm3301 -} // namespace esphome +} // namespace esphome::hm3301 diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 6b10a5e2373..55e708e34a1 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/aqi/aqi_calculator_factory.h" -namespace esphome { -namespace hm3301 { +namespace esphome::hm3301 { static const uint8_t SELECT_COMM_CMD = 0x88; @@ -47,5 +46,4 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { uint16_t get_sensor_value_(const uint8_t *data, uint8_t i); }; -} // namespace hm3301 -} // namespace esphome +} // namespace esphome::hm3301 diff --git a/esphome/components/hmac_md5/hmac_md5.cpp b/esphome/components/hmac_md5/hmac_md5.cpp index d766a55fabb..49a55592d33 100644 --- a/esphome/components/hmac_md5/hmac_md5.cpp +++ b/esphome/components/hmac_md5/hmac_md5.cpp @@ -4,8 +4,7 @@ #ifdef USE_MD5 #include "esphome/core/helpers.h" -namespace esphome { -namespace hmac_md5 { +namespace esphome::hmac_md5 { void HmacMD5::init(const uint8_t *key, size_t len) { uint8_t ipad[64], opad[64]; @@ -53,6 +52,6 @@ bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } -} // namespace hmac_md5 -} // namespace esphome +} // namespace esphome::hmac_md5 + #endif diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h index fb9479e3afa..c2fd7f28009 100644 --- a/esphome/components/hmac_md5/hmac_md5.h +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -5,8 +5,7 @@ #include "esphome/components/md5/md5.h" #include <string> -namespace esphome { -namespace hmac_md5 { +namespace esphome::hmac_md5 { class HmacMD5 { public: @@ -44,6 +43,6 @@ class HmacMD5 { md5::MD5Digest ohash_; }; -} // namespace hmac_md5 -} // namespace esphome +} // namespace esphome::hmac_md5 + #endif diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index bee52821255..7930df7a38c 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace hmc5883l { +namespace esphome::hmc5883l { static const char *const TAG = "hmc5883l"; static const uint8_t HMC5883L_ADDRESS = 0x1E; @@ -140,5 +139,4 @@ void HMC5883LComponent::update() { this->heading_sensor_->publish_state(heading); } -} // namespace hmc5883l -} // namespace esphome +} // namespace esphome::hmc5883l diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index b5cf93e62b8..4f170d74015 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace hmc5883l { +namespace esphome::hmc5883l { enum HMC5883LOversampling { HMC5883L_OVERSAMPLING_1 = 0b000, @@ -65,5 +64,4 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { HighFrequencyLoopRequester high_freq_; }; -} // namespace hmc5883l -} // namespace esphome +} // namespace esphome::hmc5883l diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index b0d91358220..735fb9f5da1 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.binary_sensor"; @@ -43,5 +42,4 @@ void HomeassistantBinarySensor::dump_config() { } float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; } -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index 9aec61a3701..6d95ea2c609 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component { public: @@ -20,5 +19,4 @@ class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Com bool initial_{true}; }; -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index da802b7fe9f..965f91d202b 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.number"; @@ -103,5 +102,4 @@ void HomeassistantNumber::control(float value) { api::global_api_server->send_homeassistant_action(resp); } -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h index 275d2d5f03f..a1e351fdf45 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.h +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantNumber : public number::Number, public Component { public: @@ -25,5 +24,4 @@ class HomeassistantNumber : public number::Number, public Component { const char *entity_id_{nullptr}; }; -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index 66300ebba54..112795a4ff8 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.sensor"; @@ -34,5 +33,4 @@ void HomeassistantSensor::dump_config() { } float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.h b/esphome/components/homeassistant/sensor/homeassistant_sensor.h index d89fc069ff2..afc49355378 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.h +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantSensor : public sensor::Sensor, public Component { public: @@ -19,5 +18,4 @@ class HomeassistantSensor : public sensor::Sensor, public Component { const char *attribute_{nullptr}; }; -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index cc3d582bf30..8a4ea19f2e5 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.switch"; @@ -60,5 +59,4 @@ void HomeassistantSwitch::write_state(bool state) { api::global_api_server->send_homeassistant_action(resp); } -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h index c180b7f98a1..c6c178c205b 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.h +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/core/component.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantSwitch : public switch_::Switch, public Component { public: @@ -18,5 +17,4 @@ class HomeassistantSwitch : public switch_::Switch, public Component { const char *entity_id_{nullptr}; }; -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp index 109574e0c8e..40d7455ece1 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/string_ref.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.text_sensor"; @@ -26,5 +25,4 @@ void HomeassistantTextSensor::dump_config() { } } float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h index 4d66c65a17a..8af81cefcb7 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantTextSensor : public text_sensor::TextSensor, public Component { public: @@ -19,5 +18,4 @@ class HomeassistantTextSensor : public text_sensor::TextSensor, public Component const char *attribute_{nullptr}; }; -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index d0398920736..c08bd73d625 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -1,8 +1,7 @@ #include "homeassistant_time.h" #include "esphome/core/log.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { static const char *const TAG = "homeassistant.time"; @@ -16,5 +15,4 @@ void HomeassistantTime::setup() { global_homeassistant_time = this; } void HomeassistantTime::update() { api::global_api_server->request_time(); } HomeassistantTime *global_homeassistant_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 455ded20226..77edb50cd09 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -4,8 +4,7 @@ #include "esphome/components/time/real_time_clock.h" #include "esphome/components/api/api_server.h" -namespace esphome { -namespace homeassistant { +namespace esphome::homeassistant { class HomeassistantTime final : public time::RealTimeClock { public: @@ -17,5 +16,4 @@ class HomeassistantTime final : public time::RealTimeClock { extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace homeassistant -} // namespace esphome +} // namespace esphome::homeassistant diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index 904672d1369..7f075847fdc 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -5,8 +5,7 @@ #include "honeywell_hih.h" #include "esphome/core/log.h" -namespace esphome { -namespace honeywell_hih_i2c { +namespace esphome::honeywell_hih_i2c { static const char *const TAG = "honeywell_hih.i2c"; @@ -91,5 +90,4 @@ void HoneywellHIComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace honeywell_hih_i2c -} // namespace esphome +} // namespace esphome::honeywell_hih_i2c diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h index 79140f7399e..d9ea6401ce0 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.h +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace honeywell_hih_i2c { +namespace esphome::honeywell_hih_i2c { class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { public: @@ -29,5 +28,4 @@ class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { void measurement_timeout_(); }; -} // namespace honeywell_hih_i2c -} // namespace esphome +} // namespace esphome::honeywell_hih_i2c diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index 58c5df230f4..8bfc5e4f4f8 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -1,8 +1,7 @@ #include "honeywellabp.h" #include "esphome/core/log.h" -namespace esphome { -namespace honeywellabp { +namespace esphome::honeywellabp { static const char *const TAG = "honeywellabp"; @@ -96,5 +95,4 @@ void HONEYWELLABPSensor::set_honeywellabp_max_pressure(float max_pressure) { this->honeywellabp_max_pressure_ = max_pressure; } -} // namespace honeywellabp -} // namespace esphome +} // namespace esphome::honeywellabp diff --git a/esphome/components/honeywellabp/honeywellabp.h b/esphome/components/honeywellabp/honeywellabp.h index 98f6f08c4a3..3c31968c490 100644 --- a/esphome/components/honeywellabp/honeywellabp.h +++ b/esphome/components/honeywellabp/honeywellabp.h @@ -6,8 +6,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/core/component.h" -namespace esphome { -namespace honeywellabp { +namespace esphome::honeywellabp { class HONEYWELLABPSensor : public PollingComponent, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -41,5 +40,4 @@ class HONEYWELLABPSensor : public PollingComponent, float read_temperature_(); }; -} // namespace honeywellabp -} // namespace esphome +} // namespace esphome::honeywellabp diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp index f173a1afbdc..bfab045838d 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace honeywellabp2_i2c { +namespace esphome::honeywellabp2_i2c { static const uint8_t STATUS_BIT_POWER = 6; static const uint8_t STATUS_BIT_BUSY = 5; @@ -107,5 +106,4 @@ void HONEYWELLABP2Sensor::set_transfer_function(ABP2TRANFERFUNCTION transfer_fun } } -} // namespace honeywellabp2_i2c -} // namespace esphome +} // namespace esphome::honeywellabp2_i2c diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index d29ebb855dd..41ea21344bd 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -7,8 +7,7 @@ #include "esphome/core/hal.h" #include "esphome/core/component.h" -namespace esphome { -namespace honeywellabp2_i2c { +namespace esphome::honeywellabp2_i2c { enum ABP2TRANFERFUNCTION { ABP2_TRANS_FUNC_A = 0, ABP2_TRANS_FUNC_B = 1 }; @@ -55,5 +54,4 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { float last_temperature_; }; -} // namespace honeywellabp2_i2c -} // namespace esphome +} // namespace esphome::honeywellabp2_i2c diff --git a/esphome/components/host/time/host_time.h b/esphome/components/host/time/host_time.h index 4f1473b8096..19e1af99d1f 100644 --- a/esphome/components/host/time/host_time.h +++ b/esphome/components/host/time/host_time.h @@ -3,13 +3,11 @@ #include "esphome/core/component.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace host { +namespace esphome::host { class HostTime : public time::RealTimeClock { public: void update() override {} }; -} // namespace host -} // namespace esphome +} // namespace esphome::host diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index b56e96badcf..0b3a746c34f 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -9,8 +9,7 @@ #include "hrxl_maxsonar_wr.h" #include "esphome/core/log.h" -namespace esphome { -namespace hrxl_maxsonar_wr { +namespace esphome::hrxl_maxsonar_wr { static const char *const TAG = "hrxl.maxsonar.wr.sensor"; static const uint8_t ASCII_CR = 0x0D; @@ -73,5 +72,4 @@ void HrxlMaxsonarWrComponent::dump_config() { this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); } -} // namespace hrxl_maxsonar_wr -} // namespace esphome +} // namespace esphome::hrxl_maxsonar_wr diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h index efb8bc5f4b2..e98eeea723b 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace hrxl_maxsonar_wr { +namespace esphome::hrxl_maxsonar_wr { class HrxlMaxsonarWrComponent : public sensor::Sensor, public Component, public uart::UARTDevice { public: @@ -21,5 +20,4 @@ class HrxlMaxsonarWrComponent : public sensor::Sensor, public Component, public std::string buffer_; }; -} // namespace hrxl_maxsonar_wr -} // namespace esphome +} // namespace esphome::hrxl_maxsonar_wr diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index ef9ef1fabf7..67bfb1917eb 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace hte501 { +namespace esphome::hte501 { static const char *const TAG = "hte501"; @@ -71,5 +70,4 @@ void HTE501Component::update() { this->status_clear_warning(); }); } -} // namespace hte501 -} // namespace esphome +} // namespace esphome::hte501 diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index 7f29885f49d..310073f88b9 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace hte501 { +namespace esphome::hte501 { /// This class implements support for the hte501 of temperature i2c sensors. class HTE501Component : public PollingComponent, public i2c::I2CDevice { @@ -24,5 +23,4 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; -} // namespace hte501 -} // namespace esphome +} // namespace esphome::hte501 diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h index 6c50bb9b0d9..f6f49b14b1a 100644 --- a/esphome/components/http_request/ota/automation.h +++ b/esphome/components/http_request/ota/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { template<typename... Ts> class OtaHttpRequestComponentFlashAction : public Action<Ts...> { public: @@ -38,5 +37,4 @@ template<typename... Ts> class OtaHttpRequestComponentFlashAction : public Actio OtaHttpRequestComponent *parent_; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 5dd21c314c6..8893b96c65b 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -9,8 +9,7 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.ota"; @@ -297,5 +296,4 @@ bool OtaHttpRequestComponent::validate_url_(const std::string &url) { return true; } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 70e4559fa7c..a706331d9ac 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -11,8 +11,7 @@ #include "../http_request.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const uint8_t MD5_SIZE = 32; @@ -56,5 +55,4 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented< static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 1c52a281059..57dc86d55cf 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -6,8 +6,7 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { // The update function runs in a task only on ESP32s. #ifdef USE_ESP32 @@ -258,5 +257,4 @@ void HttpRequestUpdate::perform(bool force) { this->defer([this]() { this->ota_parent_->flash(); }); } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index b8350346f9e..be9fbf72bfd 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -11,8 +11,7 @@ #include <freertos/FreeRTOS.h> #endif -namespace esphome { -namespace http_request { +namespace esphome::http_request { class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { public: @@ -43,5 +42,4 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo uint8_t initial_check_remaining_{0}; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index 58a28b213f7..8a3b2cb5d5c 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace htu21d { +namespace esphome::htu21d { static const char *const TAG = "htu21d"; @@ -143,5 +142,4 @@ uint8_t HTU21DComponent::get_heater_level() { return raw_heater & 0xF; } -} // namespace htu21d -} // namespace esphome +} // namespace esphome::htu21d diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 594be783261..a111722dc79 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/automation.h" -namespace esphome { -namespace htu21d { +namespace esphome::htu21d { enum HTU21DSensorModels { HTU21D_SENSOR_MODEL_HTU21D = 0, HTU21D_SENSOR_MODEL_SI7021, HTU21D_SENSOR_MODEL_SHT21 }; @@ -57,5 +56,4 @@ template<typename... Ts> class SetHeaterAction : public Action<Ts...>, public Pa } }; -} // namespace htu21d -} // namespace esphome +} // namespace esphome::htu21d diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 4bb38a11a20..0b679bf2b77 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -14,8 +14,7 @@ #include <cinttypes> -namespace esphome { -namespace htu31d { +namespace esphome::htu31d { /** Logging prefix */ static const char *const TAG = "htu31d"; @@ -259,5 +258,4 @@ void HTU31DComponent::set_heater_state(bool desired) { } } -} // namespace htu31d -} // namespace esphome +} // namespace esphome::htu31d diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h index 24d85243cc1..451918cb3bc 100644 --- a/esphome/components/htu31d/htu31d.h +++ b/esphome/components/htu31d/htu31d.h @@ -5,8 +5,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace htu31d { +namespace esphome::htu31d { class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { public: @@ -27,5 +26,4 @@ class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; }; -} // namespace htu31d -} // namespace esphome +} // namespace esphome::htu31d diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index f2e3234127d..fe2fb4c350b 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace hx711 { +namespace esphome::hx711 { static const char *const TAG = "hx711"; @@ -77,5 +76,4 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { return true; } -} // namespace hx711 -} // namespace esphome +} // namespace esphome::hx711 diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index 37723ee81fc..43ab4c0f562 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace hx711 { +namespace esphome::hx711 { enum HX711Gain : uint8_t { HX711_GAIN_128 = 1, @@ -33,5 +32,4 @@ class HX711Sensor : public sensor::Sensor, public PollingComponent { HX711Gain gain_{HX711_GAIN_128}; }; -} // namespace hx711 -} // namespace esphome +} // namespace esphome::hx711 diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 983a0a66493..695a823cb7a 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -1,8 +1,7 @@ #include "hydreon_rgxx.h" #include "esphome/core/log.h" -namespace esphome { -namespace hydreon_rgxx { +namespace esphome::hydreon_rgxx { static const char *const TAG = "hydreon_rgxx.sensor"; static const int MAX_DATA_LENGTH_BYTES = 80; @@ -284,5 +283,4 @@ void HydreonRGxxComponent::process_line_() { } } -} // namespace hydreon_rgxx -} // namespace esphome +} // namespace esphome::hydreon_rgxx diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index e3f9798a937..2ae46907c15 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -8,8 +8,7 @@ #endif #include "esphome/components/uart/uart.h" -namespace esphome { -namespace hydreon_rgxx { +namespace esphome::hydreon_rgxx { enum RGModel { RG9 = 1, @@ -92,5 +91,4 @@ class HydreonRGxxBinaryComponent : public Component { HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {} }; -} // namespace hydreon_rgxx -} // namespace esphome +} // namespace esphome::hydreon_rgxx diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index 4c0e3cd96e0..7b2c960a5f9 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace hyt271 { +namespace esphome::hyt271 { static const char *const TAG = "hyt271"; @@ -46,5 +45,4 @@ void HYT271Component::update() { this->status_clear_warning(); }); } -} // namespace hyt271 -} // namespace esphome +} // namespace esphome::hyt271 diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h index 19409d830cb..d08b3779ad3 100644 --- a/esphome/components/hyt271/hyt271.h +++ b/esphome/components/hyt271/hyt271.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace hyt271 { +namespace esphome::hyt271 { class HYT271Component : public PollingComponent, public i2c::I2CDevice { public: @@ -21,5 +20,4 @@ class HYT271Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *humidity_{nullptr}; }; -} // namespace hyt271 -} // namespace esphome +} // namespace esphome::hyt271 From 56ef35716235e6e6c503e3f74a9399543ac647af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 17:51:30 -0500 Subject: [PATCH 1893/2056] Bump github/codeql-action from 4.35.3 to 4.35.4 (#16299) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5429434a7f3..949e45e45c9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: category: "/language:${{matrix.language}}" From cbe192df49f2a2aa3f10ae1b5e9a14cb267b662e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 19:00:17 -0400 Subject: [PATCH 1894/2056] [clang-tidy] Concatenate nested namespaces (3/7: components i-m) (#16297) --- esphome/components/i2c_device/i2c_device.cpp | 6 ++---- esphome/components/i2c_device/i2c_device.h | 6 ++---- esphome/components/i2s_audio/i2s_audio.h | 6 ++---- .../i2s_audio/microphone/i2s_audio_microphone.cpp | 6 ++---- .../i2s_audio/microphone/i2s_audio_microphone.h | 6 ++---- esphome/components/iaqcore/iaqcore.cpp | 6 ++---- esphome/components/iaqcore/iaqcore.h | 6 ++---- esphome/components/ili9xxx/ili9xxx_defines.h | 6 ++---- esphome/components/ili9xxx/ili9xxx_display.cpp | 6 ++---- esphome/components/ili9xxx/ili9xxx_display.h | 6 ++---- esphome/components/ili9xxx/ili9xxx_init.h | 6 ++---- esphome/components/image/image.cpp | 6 ++---- esphome/components/image/image.h | 6 ++---- esphome/components/improv_base/improv_base.cpp | 6 ++---- esphome/components/improv_base/improv_base.h | 6 ++---- .../components/improv_serial/improv_serial_component.cpp | 7 +++---- .../components/improv_serial/improv_serial_component.h | 7 +++---- esphome/components/ina219/ina219.cpp | 6 ++---- esphome/components/ina219/ina219.h | 6 ++---- esphome/components/ina226/ina226.cpp | 6 ++---- esphome/components/ina226/ina226.h | 6 ++---- esphome/components/ina260/ina260.cpp | 6 ++---- esphome/components/ina260/ina260.h | 6 ++---- esphome/components/ina2xx_base/ina2xx_base.cpp | 6 ++---- esphome/components/ina2xx_base/ina2xx_base.h | 6 ++---- esphome/components/ina2xx_i2c/ina2xx_i2c.cpp | 6 ++---- esphome/components/ina2xx_i2c/ina2xx_i2c.h | 6 ++---- esphome/components/ina2xx_spi/ina2xx_spi.cpp | 6 ++---- esphome/components/ina2xx_spi/ina2xx_spi.h | 6 ++---- esphome/components/ina3221/ina3221.cpp | 6 ++---- esphome/components/ina3221/ina3221.h | 6 ++---- .../inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp | 6 ++---- .../components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h | 6 ++---- esphome/components/inkplate/inkplate.cpp | 6 ++---- esphome/components/inkplate/inkplate.h | 6 ++---- esphome/components/integration/integration_sensor.cpp | 6 ++---- esphome/components/integration/integration_sensor.h | 6 ++---- esphome/components/interval/interval.h | 6 ++---- esphome/components/jsn_sr04t/jsn_sr04t.cpp | 6 ++---- esphome/components/jsn_sr04t/jsn_sr04t.h | 6 ++---- esphome/components/json/json_util.cpp | 6 ++---- esphome/components/json/json_util.h | 6 ++---- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 6 ++---- esphome/components/kamstrup_kmp/kamstrup_kmp.h | 6 ++---- esphome/components/key_collector/key_collector.cpp | 6 ++---- esphome/components/key_collector/key_collector.h | 6 ++---- esphome/components/key_provider/key_provider.cpp | 6 ++---- esphome/components/key_provider/key_provider.h | 6 ++---- esphome/components/kmeteriso/kmeteriso.cpp | 6 ++---- esphome/components/kmeteriso/kmeteriso.h | 6 ++---- esphome/components/kuntze/kuntze.cpp | 6 ++---- esphome/components/kuntze/kuntze.h | 6 ++---- esphome/components/lc709203f/lc709203f.cpp | 6 ++---- esphome/components/lc709203f/lc709203f.h | 6 ++---- esphome/components/lcd_base/lcd_display.cpp | 6 ++---- esphome/components/lcd_base/lcd_display.h | 6 ++---- esphome/components/lcd_gpio/gpio_lcd_display.cpp | 6 ++---- esphome/components/lcd_gpio/gpio_lcd_display.h | 6 ++---- esphome/components/lcd_menu/lcd_menu.cpp | 6 ++---- esphome/components/lcd_menu/lcd_menu.h | 6 ++---- esphome/components/lcd_pcf8574/pcf8574_display.cpp | 6 ++---- esphome/components/lcd_pcf8574/pcf8574_display.h | 6 ++---- esphome/components/lightwaverf/LwRx.h | 6 ++---- esphome/components/lightwaverf/LwTx.h | 6 ++---- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp | 6 ++---- .../lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h | 6 ++---- esphome/components/lm75b/lm75b.cpp | 6 ++---- esphome/components/lm75b/lm75b.h | 6 ++---- esphome/components/lps22/lps22.cpp | 6 ++---- esphome/components/lps22/lps22.h | 6 ++---- esphome/components/ltr390/ltr390.cpp | 6 ++---- esphome/components/ltr390/ltr390.h | 6 ++---- esphome/components/ltr501/ltr501.cpp | 6 ++---- esphome/components/ltr501/ltr501.h | 6 ++---- esphome/components/ltr501/ltr_definitions_501.h | 6 ++---- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 6 ++---- esphome/components/ltr_als_ps/ltr_als_ps.h | 6 ++---- esphome/components/ltr_als_ps/ltr_definitions.h | 6 ++---- esphome/components/lvgl/light/lvgl_light.h | 6 ++---- esphome/components/lvgl/lvgl_proxy.h | 6 ++---- esphome/components/lvgl/number/lvgl_number.h | 6 ++---- esphome/components/lvgl/select/lvgl_select.h | 6 ++---- esphome/components/lvgl/switch/lvgl_switch.h | 6 ++---- esphome/components/lvgl/text/lvgl_text.h | 6 ++---- .../binary_sensor/m5stack_8angle_binary_sensor.cpp | 6 ++---- .../binary_sensor/m5stack_8angle_binary_sensor.h | 6 ++---- .../m5stack_8angle/light/m5stack_8angle_light.cpp | 6 ++---- .../m5stack_8angle/light/m5stack_8angle_light.h | 6 ++---- esphome/components/m5stack_8angle/m5stack_8angle.cpp | 6 ++---- esphome/components/m5stack_8angle/m5stack_8angle.h | 6 ++---- .../m5stack_8angle/sensor/m5stack_8angle_sensor.cpp | 6 ++---- .../m5stack_8angle/sensor/m5stack_8angle_sensor.h | 6 ++---- .../binary_sensor/matrix_keypad_binary_sensor.h | 6 ++---- esphome/components/matrix_keypad/matrix_keypad.cpp | 6 ++---- esphome/components/matrix_keypad/matrix_keypad.h | 6 ++---- esphome/components/max17043/automation.h | 6 ++---- esphome/components/max17043/max17043.cpp | 6 ++---- esphome/components/max17043/max17043.h | 6 ++---- esphome/components/max31855/max31855.cpp | 6 ++---- esphome/components/max31855/max31855.h | 6 ++---- esphome/components/max31856/max31856.cpp | 6 ++---- esphome/components/max31856/max31856.h | 6 ++---- esphome/components/max31865/max31865.cpp | 6 ++---- esphome/components/max31865/max31865.h | 6 ++---- esphome/components/max44009/max44009.cpp | 6 ++---- esphome/components/max44009/max44009.h | 6 ++---- esphome/components/max6675/max6675.cpp | 6 ++---- esphome/components/max6675/max6675.h | 6 ++---- esphome/components/max6956/automation.h | 6 ++---- esphome/components/max6956/max6956.cpp | 6 ++---- esphome/components/max6956/max6956.h | 6 ++---- esphome/components/max6956/output/max6956_led_output.cpp | 6 ++---- esphome/components/max6956/output/max6956_led_output.h | 6 ++---- esphome/components/max7219digit/automation.h | 6 ++---- esphome/components/max7219digit/max7219digit.cpp | 6 ++---- esphome/components/max7219digit/max7219digit.h | 6 ++---- esphome/components/max7219digit/max7219font.h | 6 ++---- esphome/components/max9611/max9611.cpp | 7 +++---- esphome/components/max9611/max9611.h | 6 ++---- esphome/components/mcp23008/mcp23008.cpp | 6 ++---- esphome/components/mcp23008/mcp23008.h | 6 ++---- esphome/components/mcp23016/mcp23016.cpp | 6 ++---- esphome/components/mcp23016/mcp23016.h | 6 ++---- esphome/components/mcp23017/mcp23017.cpp | 6 ++---- esphome/components/mcp23017/mcp23017.h | 6 ++---- esphome/components/mcp23s08/mcp23s08.cpp | 6 ++---- esphome/components/mcp23s08/mcp23s08.h | 6 ++---- esphome/components/mcp23s17/mcp23s17.cpp | 6 ++---- esphome/components/mcp23s17/mcp23s17.h | 6 ++---- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 6 ++---- esphome/components/mcp23x08_base/mcp23x08_base.h | 6 ++---- esphome/components/mcp23x17_base/mcp23x17_base.cpp | 6 ++---- esphome/components/mcp23x17_base/mcp23x17_base.h | 6 ++---- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 6 ++---- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 6 ++---- esphome/components/mcp2515/mcp2515.cpp | 6 ++---- esphome/components/mcp2515/mcp2515.h | 6 ++---- esphome/components/mcp2515/mcp2515_defs.h | 6 ++---- esphome/components/mcp3008/mcp3008.cpp | 6 ++---- esphome/components/mcp3008/mcp3008.h | 6 ++---- esphome/components/mcp3008/sensor/mcp3008_sensor.cpp | 6 ++---- esphome/components/mcp3008/sensor/mcp3008_sensor.h | 6 ++---- esphome/components/mcp3204/mcp3204.cpp | 6 ++---- esphome/components/mcp3204/mcp3204.h | 6 ++---- esphome/components/mcp3204/sensor/mcp3204_sensor.cpp | 6 ++---- esphome/components/mcp3204/sensor/mcp3204_sensor.h | 6 ++---- esphome/components/mcp3221/mcp3221_sensor.cpp | 6 ++---- esphome/components/mcp3221/mcp3221_sensor.h | 6 ++---- esphome/components/mcp4461/mcp4461.cpp | 6 ++---- esphome/components/mcp4461/mcp4461.h | 6 ++---- esphome/components/mcp4461/output/mcp4461_output.cpp | 6 ++---- esphome/components/mcp4461/output/mcp4461_output.h | 6 ++---- esphome/components/mcp4725/mcp4725.cpp | 6 ++---- esphome/components/mcp4725/mcp4725.h | 6 ++---- esphome/components/mcp4728/mcp4728.cpp | 6 ++---- esphome/components/mcp4728/mcp4728.h | 6 ++---- esphome/components/mcp4728/output/mcp4728_output.cpp | 6 ++---- esphome/components/mcp4728/output/mcp4728_output.h | 6 ++---- esphome/components/mcp47a1/mcp47a1.cpp | 6 ++---- esphome/components/mcp47a1/mcp47a1.h | 6 ++---- esphome/components/mcp9600/mcp9600.cpp | 6 ++---- esphome/components/mcp9600/mcp9600.h | 6 ++---- esphome/components/mcp9808/mcp9808.cpp | 6 ++---- esphome/components/mcp9808/mcp9808.h | 6 ++---- esphome/components/md5/md5.cpp | 7 +++---- esphome/components/md5/md5.h | 7 +++---- esphome/components/media_player/automation.h | 7 ++----- esphome/components/media_player/media_player.cpp | 6 ++---- esphome/components/media_player/media_player.h | 6 ++---- esphome/components/micro_wake_word/automation.h | 8 ++++---- esphome/components/micro_wake_word/micro_wake_word.cpp | 6 ++---- esphome/components/micro_wake_word/micro_wake_word.h | 6 ++---- .../components/micro_wake_word/preprocessor_settings.h | 6 ++---- esphome/components/micro_wake_word/streaming_model.cpp | 6 ++---- esphome/components/micro_wake_word/streaming_model.h | 6 ++---- esphome/components/microphone/automation.h | 6 ++---- esphome/components/microphone/microphone.h | 6 ++---- esphome/components/microphone/microphone_source.cpp | 6 ++---- esphome/components/microphone/microphone_source.h | 6 ++---- esphome/components/mics_4514/mics_4514.cpp | 6 ++---- esphome/components/mics_4514/mics_4514.h | 6 ++---- esphome/components/midea/ac_adapter.cpp | 8 ++------ esphome/components/midea/ac_adapter.h | 8 ++------ esphome/components/midea/ac_automations.h | 8 ++------ esphome/components/midea/air_conditioner.cpp | 8 ++------ esphome/components/midea/air_conditioner.h | 8 ++------ esphome/components/midea/appliance_base.h | 6 ++---- esphome/components/midea_ir/midea_data.h | 6 ++---- esphome/components/midea_ir/midea_ir.cpp | 6 ++---- esphome/components/midea_ir/midea_ir.h | 6 ++---- esphome/components/mipi_spi/mipi_spi.h | 6 ++---- esphome/components/mitsubishi/mitsubishi.cpp | 6 ++---- esphome/components/mitsubishi/mitsubishi.h | 6 ++---- esphome/components/mixer/speaker/automation.h | 6 ++---- esphome/components/mlx90393/sensor_mlx90393.cpp | 6 ++---- esphome/components/mlx90393/sensor_mlx90393.h | 6 ++---- esphome/components/mlx90614/mlx90614.cpp | 6 ++---- esphome/components/mlx90614/mlx90614.h | 6 ++---- esphome/components/mmc5603/mmc5603.cpp | 6 ++---- esphome/components/mmc5603/mmc5603.h | 6 ++---- esphome/components/mmc5983/mmc5983.cpp | 6 ++---- esphome/components/mmc5983/mmc5983.h | 6 ++---- esphome/components/modbus/modbus.cpp | 6 ++---- esphome/components/modbus/modbus.h | 6 ++---- esphome/components/modbus/modbus_definitions.h | 6 ++---- .../binary_sensor/modbus_binarysensor.cpp | 6 ++---- .../modbus_controller/binary_sensor/modbus_binarysensor.h | 6 ++---- .../components/modbus_controller/modbus_controller.cpp | 6 ++---- esphome/components/modbus_controller/modbus_controller.h | 6 ++---- .../components/modbus_controller/number/modbus_number.cpp | 6 ++---- .../components/modbus_controller/number/modbus_number.h | 6 ++---- .../components/modbus_controller/output/modbus_output.cpp | 6 ++---- .../components/modbus_controller/output/modbus_output.h | 6 ++---- .../components/modbus_controller/select/modbus_select.cpp | 6 ++---- .../components/modbus_controller/select/modbus_select.h | 6 ++---- .../components/modbus_controller/sensor/modbus_sensor.cpp | 6 ++---- .../components/modbus_controller/sensor/modbus_sensor.h | 6 ++---- .../components/modbus_controller/switch/modbus_switch.cpp | 7 +++---- .../components/modbus_controller/switch/modbus_switch.h | 6 ++---- .../modbus_controller/text_sensor/modbus_textsensor.cpp | 6 ++---- .../modbus_controller/text_sensor/modbus_textsensor.h | 6 ++---- .../components/monochromatic/monochromatic_light_output.h | 6 ++---- esphome/components/mopeka_ble/mopeka_ble.cpp | 6 ++---- esphome/components/mopeka_ble/mopeka_ble.h | 6 ++---- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 6 ++---- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 6 ++---- esphome/components/mopeka_std_check/mopeka_std_check.cpp | 6 ++---- esphome/components/mopeka_std_check/mopeka_std_check.h | 6 ++---- esphome/components/mpl3115a2/mpl3115a2.cpp | 6 ++---- esphome/components/mpl3115a2/mpl3115a2.h | 6 ++---- .../mpr121/binary_sensor/mpr121_binary_sensor.cpp | 6 ++---- .../mpr121/binary_sensor/mpr121_binary_sensor.h | 6 ++---- esphome/components/mpr121/mpr121.cpp | 6 ++---- esphome/components/mpr121/mpr121.h | 6 ++---- esphome/components/mpu6050/mpu6050.cpp | 6 ++---- esphome/components/mpu6050/mpu6050.h | 7 ++----- esphome/components/mpu6886/mpu6886.cpp | 6 ++---- esphome/components/mpu6886/mpu6886.h | 7 ++----- .../mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp | 6 ++---- .../mqtt_subscribe/sensor/mqtt_subscribe_sensor.h | 6 ++---- .../text_sensor/mqtt_subscribe_text_sensor.cpp | 6 ++---- .../text_sensor/mqtt_subscribe_text_sensor.h | 6 ++---- esphome/components/ms5611/ms5611.cpp | 6 ++---- esphome/components/ms5611/ms5611.h | 6 ++---- esphome/components/ms8607/ms8607.cpp | 6 ++---- esphome/components/ms8607/ms8607.h | 6 ++---- esphome/components/msa3xx/msa3xx.cpp | 6 ++---- esphome/components/msa3xx/msa3xx.h | 6 ++---- esphome/components/my9231/my9231.cpp | 6 ++---- esphome/components/my9231/my9231.h | 6 ++---- 250 files changed, 508 insertions(+), 1013 deletions(-) diff --git a/esphome/components/i2c_device/i2c_device.cpp b/esphome/components/i2c_device/i2c_device.cpp index 455c68fbed3..2f68a9e96a5 100644 --- a/esphome/components/i2c_device/i2c_device.cpp +++ b/esphome/components/i2c_device/i2c_device.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include <cinttypes> -namespace esphome { -namespace i2c_device { +namespace esphome::i2c_device { static const char *const TAG = "i2c_device"; @@ -13,5 +12,4 @@ void I2CDeviceComponent::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace i2c_device -} // namespace esphome +} // namespace esphome::i2c_device diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h index 9944ca92045..aeae622c2e8 100644 --- a/esphome/components/i2c_device/i2c_device.h +++ b/esphome/components/i2c_device/i2c_device.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace i2c_device { +namespace esphome::i2c_device { class I2CDeviceComponent : public Component, public i2c::I2CDevice { public: @@ -13,5 +12,4 @@ class I2CDeviceComponent : public Component, public i2c::I2CDevice { protected: }; -} // namespace i2c_device -} // namespace esphome +} // namespace esphome::i2c_device diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index 5b260fa7ede..6b32b556d9b 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -8,8 +8,7 @@ #include <esp_idf_version.h> #include <driver/i2s_std.h> -namespace esphome { -namespace i2s_audio { +namespace esphome::i2s_audio { class I2SAudioComponent; @@ -77,7 +76,6 @@ class I2SAudioComponent : public Component { int port_{}; }; -} // namespace i2s_audio -} // namespace esphome +} // namespace esphome::i2s_audio #endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index d697808c998..66ca32b8303 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -10,8 +10,7 @@ #include "esphome/components/audio/audio.h" -namespace esphome { -namespace i2s_audio { +namespace esphome::i2s_audio { static const UBaseType_t MAX_LISTENERS = 16; @@ -426,7 +425,6 @@ void I2SAudioMicrophone::loop() { } } -} // namespace i2s_audio -} // namespace esphome +} // namespace esphome::i2s_audio #endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index e277409262a..06f2de76107 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -12,8 +12,7 @@ #include <freertos/semphr.h> #include <freertos/task.h> -namespace esphome { -namespace i2s_audio { +namespace esphome::i2s_audio { class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component { public: @@ -65,7 +64,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub int32_t dc_offset_prev_output_{0}; }; -} // namespace i2s_audio -} // namespace esphome +} // namespace esphome::i2s_audio #endif // USE_ESP32 diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index c414eb8f608..7397d0975cf 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace iaqcore { +namespace esphome::iaqcore { static const char *const TAG = "iaqcore"; @@ -97,5 +96,4 @@ void IAQCore::dump_config() { LOG_SENSOR(" ", "TVOC", this->tvoc_); } -} // namespace iaqcore -} // namespace esphome +} // namespace esphome::iaqcore diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h index bb0bfcc7544..39f290e1205 100644 --- a/esphome/components/iaqcore/iaqcore.h +++ b/esphome/components/iaqcore/iaqcore.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace iaqcore { +namespace esphome::iaqcore { class IAQCore : public PollingComponent, public i2c::I2CDevice { public: @@ -23,5 +22,4 @@ class IAQCore : public PollingComponent, public i2c::I2CDevice { void publish_nans_(); }; -} // namespace iaqcore -} // namespace esphome +} // namespace esphome::iaqcore diff --git a/esphome/components/ili9xxx/ili9xxx_defines.h b/esphome/components/ili9xxx/ili9xxx_defines.h index 70e0937f790..c9b54c0f0ed 100644 --- a/esphome/components/ili9xxx/ili9xxx_defines.h +++ b/esphome/components/ili9xxx/ili9xxx_defines.h @@ -2,8 +2,7 @@ #include <cstdint> -namespace esphome { -namespace ili9xxx { +namespace esphome::ili9xxx { // Color definitions // clang-format off @@ -98,5 +97,4 @@ static const uint8_t ILI9XXX_DELAY_FLAG = 0xFF; // special marker for delay - command byte reprents ms, length byte is an impossible value #define ILI9XXX_DELAY(ms) ((uint8_t) ((ms) | 0x80)), ILI9XXX_DELAY_FLAG -} // namespace ili9xxx -} // namespace esphome +} // namespace esphome::ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 11acb8a73a4..e8840c0cf19 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ili9xxx { +namespace esphome::ili9xxx { static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer @@ -470,5 +469,4 @@ void ILI9XXXDisplay::invert_colors(bool invert) { int ILI9XXXDisplay::get_width_internal() { return this->width_; } int ILI9XXXDisplay::get_height_internal() { return this->height_; } -} // namespace ili9xxx -} // namespace esphome +} // namespace esphome::ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 629bbb41cb6..2529e54021b 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -5,8 +5,7 @@ #include "ili9xxx_defines.h" #include "ili9xxx_init.h" -namespace esphome { -namespace ili9xxx { +namespace esphome::ili9xxx { static const char *const TAG = "ili9xxx"; const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6 @@ -283,5 +282,4 @@ class ILI9XXXST7735 : public ILI9XXXDisplay { ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160) {} }; -} // namespace ili9xxx -} // namespace esphome +} // namespace esphome::ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index f0c6a94a656..529571022ae 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace ili9xxx { +namespace esphome::ili9xxx { // clang-format off static constexpr uint8_t PROGMEM INITCMD_M5STACK[] = { @@ -478,5 +477,4 @@ static constexpr uint8_t PROGMEM INITCMD_ST7735[] = { }; // clang-format on -} // namespace ili9xxx -} // namespace esphome +} // namespace esphome::ili9xxx diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 5b4ed6968c6..c95b693cf0b 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace image { +namespace esphome::image { void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { int img_x0 = 0; @@ -243,5 +242,4 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T } } -} // namespace image -} // namespace esphome +} // namespace esphome::image diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index d4865570e40..ccc2f23f200 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -6,8 +6,7 @@ #include "esphome/components/lvgl/lvgl_proxy.h" #endif // USE_LVGL -namespace esphome { -namespace image { +namespace esphome::image { enum ImageType { IMAGE_TYPE_BINARY = 0, @@ -61,5 +60,4 @@ class Image : public display::BaseImage { #endif }; -} // namespace image -} // namespace esphome +} // namespace esphome::image diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index d0340344a6c..fa1b855d6c4 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -5,8 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/defines.h" -namespace esphome { -namespace improv_base { +namespace esphome::improv_base { #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}"; @@ -65,5 +64,4 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) { } #endif -} // namespace improv_base -} // namespace esphome +} // namespace esphome::improv_base diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h index ebc8f38d60b..9dded85a468 100644 --- a/esphome/components/improv_base/improv_base.h +++ b/esphome/components/improv_base/improv_base.h @@ -3,8 +3,7 @@ #include <cstddef> #include "esphome/core/defines.h" -namespace esphome { -namespace improv_base { +namespace esphome::improv_base { class ImprovBase { public: @@ -20,5 +19,4 @@ class ImprovBase { #endif }; -} // namespace improv_base -} // namespace esphome +} // namespace esphome::improv_base diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 003328d535e..18d0b44701d 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -8,8 +8,7 @@ #include "esphome/components/logger/logger.h" -namespace esphome { -namespace improv_serial { +namespace esphome::improv_serial { static const char *const TAG = "improv_serial"; @@ -329,6 +328,6 @@ void ImprovSerialComponent::on_wifi_connect_timeout_() { ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace improv_serial -} // namespace esphome +} // namespace esphome::improv_serial + #endif diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index dd8f5e47191..2f1d0136a44 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -23,8 +23,7 @@ #include <HardwareSerial.h> #endif -namespace esphome { -namespace improv_serial { +namespace esphome::improv_serial { // TX buffer layout constants static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV" @@ -99,6 +98,6 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { extern ImprovSerialComponent *global_improv_serial_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace improv_serial -} // namespace esphome +} // namespace esphome::improv_serial + #endif diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 278017651bb..85da1965848 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ina219 { +namespace esphome::ina219 { static const char *const TAG = "ina219"; @@ -196,5 +195,4 @@ void INA219Component::update() { this->status_clear_warning(); } -} // namespace ina219 -} // namespace esphome +} // namespace esphome::ina219 diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index bcadb65e36b..7462c072724 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace ina219 { +namespace esphome::ina219 { class INA219Component : public PollingComponent, public i2c::I2CDevice { public: @@ -35,5 +34,4 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *power_sensor_{nullptr}; }; -} // namespace ina219 -} // namespace esphome +} // namespace esphome::ina219 diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index cbc44c9a1aa..695de57c617 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include <cinttypes> -namespace esphome { -namespace ina226 { +namespace esphome::ina226 { static const char *const TAG = "ina226"; @@ -161,5 +160,4 @@ int32_t INA226Component::twos_complement_(int32_t val, uint8_t bits) { return val; } -} // namespace ina226 -} // namespace esphome +} // namespace esphome::ina226 diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index 0aa66ff765f..7d6b526f40f 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ina226 { +namespace esphome::ina226 { enum AdcTime : uint16_t { ADC_TIME_140US = 0, @@ -73,5 +72,4 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { int32_t twos_complement_(int32_t val, uint8_t bits); }; -} // namespace ina226 -} // namespace esphome +} // namespace esphome::ina226 diff --git a/esphome/components/ina260/ina260.cpp b/esphome/components/ina260/ina260.cpp index 4d6acf400cc..05039f0e331 100644 --- a/esphome/components/ina260/ina260.cpp +++ b/esphome/components/ina260/ina260.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ina260 { +namespace esphome::ina260 { static const char *const TAG = "ina260"; @@ -122,5 +121,4 @@ void INA260Component::update() { this->status_clear_warning(); } -} // namespace ina260 -} // namespace esphome +} // namespace esphome::ina260 diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h index 6cbc157cf34..856e715774c 100644 --- a/esphome/components/ina260/ina260.h +++ b/esphome/components/ina260/ina260.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ina260 { +namespace esphome::ina260 { class INA260Component : public PollingComponent, public i2c::I2CDevice { public: @@ -33,5 +32,4 @@ class INA260Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace ina260 -} // namespace esphome +} // namespace esphome::ina260 diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 2d08562e542..d3acf00eef6 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -5,8 +5,7 @@ #include <cinttypes> #include <cmath> -namespace esphome { -namespace ina2xx_base { +namespace esphome::ina2xx_base { static const char *const TAG = "ina2xx"; @@ -600,5 +599,4 @@ bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { return (int64_t) (value << (64 - bits)) >> (64 - bits); } -} // namespace ina2xx_base -} // namespace esphome +} // namespace esphome::ina2xx_base diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h index 104c384a0d2..beb158944bb 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.h +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ina2xx_base { +namespace esphome::ina2xx_base { enum RegisterMap : uint8_t { REG_CONFIG = 0x00, @@ -250,5 +249,4 @@ class INA2XX : public PollingComponent { virtual bool read_ina_register(uint8_t a_register, uint8_t *data, size_t len) = 0; virtual bool write_ina_register(uint8_t a_register, const uint8_t *data, size_t len) = 0; }; -} // namespace ina2xx_base -} // namespace esphome +} // namespace esphome::ina2xx_base diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp index a363a9c12f4..4fc3b00a215 100644 --- a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp @@ -1,8 +1,7 @@ #include "ina2xx_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace ina2xx_i2c { +namespace esphome::ina2xx_i2c { static const char *const TAG = "ina2xx_i2c"; @@ -35,5 +34,4 @@ bool INA2XXI2C::write_ina_register(uint8_t reg, const uint8_t *data, size_t len) } return ret == i2c::ERROR_OK; } -} // namespace ina2xx_i2c -} // namespace esphome +} // namespace esphome::ina2xx_i2c diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.h b/esphome/components/ina2xx_i2c/ina2xx_i2c.h index c90b9bf1903..783723b3961 100644 --- a/esphome/components/ina2xx_i2c/ina2xx_i2c.h +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/ina2xx_base/ina2xx_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ina2xx_i2c { +namespace esphome::ina2xx_i2c { class INA2XXI2C : public ina2xx_base::INA2XX, public i2c::I2CDevice { public: @@ -17,5 +16,4 @@ class INA2XXI2C : public ina2xx_base::INA2XX, public i2c::I2CDevice { bool write_ina_register(uint8_t reg, const uint8_t *data, size_t len) override; }; -} // namespace ina2xx_i2c -} // namespace esphome +} // namespace esphome::ina2xx_i2c diff --git a/esphome/components/ina2xx_spi/ina2xx_spi.cpp b/esphome/components/ina2xx_spi/ina2xx_spi.cpp index 3e04a876656..43eb6762367 100644 --- a/esphome/components/ina2xx_spi/ina2xx_spi.cpp +++ b/esphome/components/ina2xx_spi/ina2xx_spi.cpp @@ -1,8 +1,7 @@ #include "ina2xx_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace ina2xx_spi { +namespace esphome::ina2xx_spi { static const char *const TAG = "ina2xx_spi"; @@ -34,5 +33,4 @@ bool INA2XXSPI::write_ina_register(uint8_t reg, const uint8_t *data, size_t len) this->disable(); return true; } -} // namespace ina2xx_spi -} // namespace esphome +} // namespace esphome::ina2xx_spi diff --git a/esphome/components/ina2xx_spi/ina2xx_spi.h b/esphome/components/ina2xx_spi/ina2xx_spi.h index 3b21518d34d..8e065de8169 100644 --- a/esphome/components/ina2xx_spi/ina2xx_spi.h +++ b/esphome/components/ina2xx_spi/ina2xx_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ina2xx_base/ina2xx_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ina2xx_spi { +namespace esphome::ina2xx_spi { class INA2XXSPI : public ina2xx_base::INA2XX, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_TRAILING, @@ -18,5 +17,4 @@ class INA2XXSPI : public ina2xx_base::INA2XX, bool read_ina_register(uint8_t reg, uint8_t *data, size_t len) override; bool write_ina_register(uint8_t reg, const uint8_t *data, size_t len) override; }; -} // namespace ina2xx_spi -} // namespace esphome +} // namespace esphome::ina2xx_spi diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index d03183e002e..b88480fc001 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ina3221 { +namespace esphome::ina3221 { static const char *const TAG = "ina3221"; @@ -128,5 +127,4 @@ bool INA3221Component::INA3221Channel::should_measure_bus_voltage() { return this->bus_voltage_sensor_ != nullptr || this->power_sensor_ != nullptr; } -} // namespace ina3221 -} // namespace esphome +} // namespace esphome::ina3221 diff --git a/esphome/components/ina3221/ina3221.h b/esphome/components/ina3221/ina3221.h index 3769df77aa8..9d9762caf37 100644 --- a/esphome/components/ina3221/ina3221.h +++ b/esphome/components/ina3221/ina3221.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ina3221 { +namespace esphome::ina3221 { class INA3221Component : public PollingComponent, public i2c::I2CDevice { public: @@ -35,5 +34,4 @@ class INA3221Component : public PollingComponent, public i2c::I2CDevice { } channels_[3]; }; -} // namespace ina3221 -} // namespace esphome +} // namespace esphome::ina3221 diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index c53d8e50298..4df22aa9de9 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace inkbird_ibsth1_mini { +namespace esphome::inkbird_ibsth1_mini { static const char *const TAG = "inkbird_ibsth1_mini"; @@ -104,7 +103,6 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic return true; } -} // namespace inkbird_ibsth1_mini -} // namespace esphome +} // namespace esphome::inkbird_ibsth1_mini #endif diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index cd2ea99717d..37e50943f33 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace inkbird_ibsth1_mini { +namespace esphome::inkbird_ibsth1_mini { class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -29,7 +28,6 @@ class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_level_{nullptr}; }; -} // namespace inkbird_ibsth1_mini -} // namespace esphome +} // namespace esphome::inkbird_ibsth1_mini #endif diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 0511b451a83..39110ca83b9 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -7,8 +7,7 @@ #include <hal/gpio_hal.h> -namespace esphome { -namespace inkplate { +namespace esphome::inkplate { static const char *const TAG = "inkplate"; @@ -820,5 +819,4 @@ void Inkplate::pins_as_outputs_() { this->display_data_7_pin_->pin_mode(gpio::FLAG_OUTPUT); } -} // namespace inkplate -} // namespace esphome +} // namespace esphome::inkplate diff --git a/esphome/components/inkplate/inkplate.h b/esphome/components/inkplate/inkplate.h index bcd56b829a0..40e32c4cc44 100644 --- a/esphome/components/inkplate/inkplate.h +++ b/esphome/components/inkplate/inkplate.h @@ -7,8 +7,7 @@ #include <array> -namespace esphome { -namespace inkplate { +namespace esphome::inkplate { enum InkplateModel : uint8_t { INKPLATE_6 = 0, @@ -210,5 +209,4 @@ class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice { GPIOPin *wakeup_pin_; }; -} // namespace inkplate -} // namespace esphome +} // namespace esphome::inkplate diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index b084801d3b0..bc26fb0f193 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace integration { +namespace esphome::integration { static const char *const TAG = "integration"; @@ -47,5 +46,4 @@ void IntegrationSensor::process_sensor_value_(float value) { this->publish_and_save_(this->result_ + area); } -} // namespace integration -} // namespace esphome +} // namespace esphome::integration diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 6c4ef7049bb..1c5edfcba54 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -6,8 +6,7 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace integration { +namespace esphome::integration { enum IntegrationSensorTime { INTEGRATION_SENSOR_TIME_MILLISECOND = 0, @@ -84,5 +83,4 @@ template<typename... Ts> class SetValueAction : public Action<Ts...>, public Par void play(const Ts &...x) override { this->parent_->set_value(this->value_.value(x...)); } }; -} // namespace integration -} // namespace esphome +} // namespace esphome::integration diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index e419841e6c6..c9d4e8ea3e8 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/automation.h" -namespace esphome { -namespace interval { +namespace esphome::interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: @@ -24,5 +23,4 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { uint32_t startup_delay_{0}; }; -} // namespace interval -} // namespace esphome +} // namespace esphome::interval diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 6fd8b1bd655..c67771a0b60 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -4,8 +4,7 @@ // Very basic support for JSN_SR04T V3.0 distance sensor in mode 2 -namespace esphome { -namespace jsn_sr04t { +namespace esphome::jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; @@ -62,5 +61,4 @@ void Jsnsr04tComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace jsn_sr04t -} // namespace esphome +} // namespace esphome::jsn_sr04t diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.h b/esphome/components/jsn_sr04t/jsn_sr04t.h index 2a22ff92ecb..f9d07ea5393 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.h +++ b/esphome/components/jsn_sr04t/jsn_sr04t.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace jsn_sr04t { +namespace esphome::jsn_sr04t { enum Model { JSN_SR04T, @@ -30,5 +29,4 @@ class Jsnsr04tComponent : public sensor::Sensor, public PollingComponent, public std::vector<uint8_t> buffer_; }; -} // namespace jsn_sr04t -} // namespace esphome +} // namespace esphome::jsn_sr04t diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index ec1490be1ff..984134b95f9 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -3,8 +3,7 @@ // ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h -namespace esphome { -namespace json { +namespace esphome::json { static const char *const TAG = "json"; @@ -149,5 +148,4 @@ SerializationBuffer<> JsonBuilder::serialize() { return result; } -} // namespace json -} // namespace esphome +} // namespace esphome::json diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 0dc9ff883cc..9f51d9927b8 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -13,8 +13,7 @@ #include <ArduinoJson.h> -namespace esphome { -namespace json { +namespace esphome::json { /// Buffer for JSON serialization that uses stack allocation for small payloads. /// Template parameter STACK_SIZE specifies the stack buffer size (default 512 bytes). @@ -192,5 +191,4 @@ class JsonBuilder { bool root_created_{false}; }; -} // namespace json -} // namespace esphome +} // namespace esphome::json diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 9bebd4cd56d..ed03c4d6df4 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace kamstrup_kmp { +namespace esphome::kamstrup_kmp { static const char *const TAG = "kamstrup_kmp"; @@ -303,5 +302,4 @@ uint16_t crc16_ccitt(const uint8_t *buffer, int len) { return (uint16_t) reg; } -} // namespace kamstrup_kmp -} // namespace esphome +} // namespace esphome::kamstrup_kmp diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index 725cf20abfa..a05a0ee17af 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -5,8 +5,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" -namespace esphome { -namespace kamstrup_kmp { +namespace esphome::kamstrup_kmp { /* =========================================================================== @@ -127,5 +126,4 @@ class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { // "true" CCITT CRC-16 uint16_t crc16_ccitt(const uint8_t *buffer, int len); -} // namespace kamstrup_kmp -} // namespace esphome +} // namespace esphome::kamstrup_kmp diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index 68d1c60bf94..cb7d47b7f0d 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace key_collector { +namespace esphome::key_collector { static const char *const TAG = "key_collector"; @@ -102,5 +101,4 @@ void KeyCollector::send_key(uint8_t key) { this->progress_callbacks_.call(this->result_, this->start_key_); } -} // namespace key_collector -} // namespace esphome +} // namespace esphome::key_collector diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 014e2034bd3..27209c50df5 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -5,8 +5,7 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace key_collector { +namespace esphome::key_collector { class KeyCollector : public Component { public: @@ -63,5 +62,4 @@ template<typename... Ts> class DisableAction : public Action<Ts...>, public Pare void play(const Ts &...x) override { this->parent_->set_enabled(false); } }; -} // namespace key_collector -} // namespace esphome +} // namespace esphome::key_collector diff --git a/esphome/components/key_provider/key_provider.cpp b/esphome/components/key_provider/key_provider.cpp index 64b0729d4d1..0efeeff0068 100644 --- a/esphome/components/key_provider/key_provider.cpp +++ b/esphome/components/key_provider/key_provider.cpp @@ -1,9 +1,7 @@ #include "key_provider.h" -namespace esphome { -namespace key_provider { +namespace esphome::key_provider { void KeyProvider::send_key_(uint8_t key) { this->key_callback_.call(key); } -} // namespace key_provider -} // namespace esphome +} // namespace esphome::key_provider diff --git a/esphome/components/key_provider/key_provider.h b/esphome/components/key_provider/key_provider.h index 97403427512..85c27523846 100644 --- a/esphome/components/key_provider/key_provider.h +++ b/esphome/components/key_provider/key_provider.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace key_provider { +namespace esphome::key_provider { /// interface for components that provide keypresses class KeyProvider { @@ -17,5 +16,4 @@ class KeyProvider { CallbackManager<void(uint8_t)> key_callback_{}; }; -} // namespace key_provider -} // namespace esphome +} // namespace esphome::key_provider diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 186686e472a..d6934a97ac0 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace kmeteriso { +namespace esphome::kmeteriso { static const char *const TAG = "kmeteriso.sensor"; @@ -74,5 +73,4 @@ void KMeterISOComponent::update() { } } -} // namespace kmeteriso -} // namespace esphome +} // namespace esphome::kmeteriso diff --git a/esphome/components/kmeteriso/kmeteriso.h b/esphome/components/kmeteriso/kmeteriso.h index 6f1978105fb..d5a2f9a01b1 100644 --- a/esphome/components/kmeteriso/kmeteriso.h +++ b/esphome/components/kmeteriso/kmeteriso.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c_bus.h" -namespace esphome { -namespace kmeteriso { +namespace esphome::kmeteriso { /// This class implements support for the KMeterISO thermocouple sensor. class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { @@ -29,5 +28,4 @@ class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace kmeteriso -} // namespace esphome +} // namespace esphome::kmeteriso diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 1b772d062ca..6df114e93c0 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace kuntze { +namespace esphome::kuntze { static const char *const TAG = "kuntze"; @@ -97,5 +96,4 @@ void Kuntze::dump_config() { LOG_SENSOR("", "OCI", this->oci_sensor_); } -} // namespace kuntze -} // namespace esphome +} // namespace esphome::kuntze diff --git a/esphome/components/kuntze/kuntze.h b/esphome/components/kuntze/kuntze.h index aad7c1cbbf0..bbd93a22cec 100644 --- a/esphome/components/kuntze/kuntze.h +++ b/esphome/components/kuntze/kuntze.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" -namespace esphome { -namespace kuntze { +namespace esphome::kuntze { class Kuntze : public PollingComponent, public modbus::ModbusDevice { public: @@ -38,5 +37,4 @@ class Kuntze : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *oci_sensor_{nullptr}; }; -} // namespace kuntze -} // namespace esphome +} // namespace esphome::kuntze diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index 8c7018124a7..cbd733b6113 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace lc709203f { +namespace esphome::lc709203f { static const char *const TAG = "lc709203f.sensor"; @@ -279,5 +278,4 @@ void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constan void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; } -} // namespace lc709203f -} // namespace esphome +} // namespace esphome::lc709203f diff --git a/esphome/components/lc709203f/lc709203f.h b/esphome/components/lc709203f/lc709203f.h index 59988a00793..42aa9a15a10 100644 --- a/esphome/components/lc709203f/lc709203f.h +++ b/esphome/components/lc709203f/lc709203f.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace lc709203f { +namespace esphome::lc709203f { enum LC709203FState { STATE_INIT, @@ -50,5 +49,4 @@ class Lc709203f : public sensor::Sensor, public PollingComponent, public i2c::I2 uint16_t pack_voltage_; }; -} // namespace lc709203f -} // namespace esphome +} // namespace esphome::lc709203f diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 1f0ba482d74..0890da85c13 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace lcd_base { +namespace esphome::lcd_base { static const char *const TAG = "lcd"; @@ -173,5 +172,4 @@ void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { } } -} // namespace lcd_base -} // namespace esphome +} // namespace esphome::lcd_base diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 473acb0bd35..4b3413e3289 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -6,8 +6,7 @@ #include <map> #include <vector> -namespace esphome { -namespace lcd_base { +namespace esphome::lcd_base { class LCDDisplay; @@ -62,5 +61,4 @@ class LCDDisplay : public PollingComponent { std::map<uint8_t, std::vector<uint8_t> > user_defined_chars_; }; -} // namespace lcd_base -} // namespace esphome +} // namespace esphome::lcd_base diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.cpp b/esphome/components/lcd_gpio/gpio_lcd_display.cpp index ae6e1194b8f..213fc8637e7 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.cpp +++ b/esphome/components/lcd_gpio/gpio_lcd_display.cpp @@ -1,8 +1,7 @@ #include "gpio_lcd_display.h" #include "esphome/core/log.h" -namespace esphome { -namespace lcd_gpio { +namespace esphome::lcd_gpio { static const char *const TAG = "lcd_gpio"; @@ -63,5 +62,4 @@ void GPIOLCDDisplay::send(uint8_t value, bool rs) { } } -} // namespace lcd_gpio -} // namespace esphome +} // namespace esphome::lcd_gpio diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index 81e4dc51a0c..dd9ea5929cd 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -4,8 +4,7 @@ #include "esphome/components/lcd_base/lcd_display.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace lcd_gpio { +namespace esphome::lcd_gpio { class GPIOLCDDisplay; @@ -51,5 +50,4 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { gpio_lcd_writer_t writer_; }; -} // namespace lcd_gpio -} // namespace esphome +} // namespace esphome::lcd_gpio diff --git a/esphome/components/lcd_menu/lcd_menu.cpp b/esphome/components/lcd_menu/lcd_menu.cpp index c664b394bf4..f731817bdb4 100644 --- a/esphome/components/lcd_menu/lcd_menu.cpp +++ b/esphome/components/lcd_menu/lcd_menu.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <algorithm> -namespace esphome { -namespace lcd_menu { +namespace esphome::lcd_menu { static const char *const TAG = "lcd_menu"; @@ -72,5 +71,4 @@ void LCDCharacterMenuComponent::draw_item(const display_menu_base::MenuItem *ite this->display_->print(0, row, data); } -} // namespace lcd_menu -} // namespace esphome +} // namespace esphome::lcd_menu diff --git a/esphome/components/lcd_menu/lcd_menu.h b/esphome/components/lcd_menu/lcd_menu.h index d0dbca7b2fc..ae1c2502fea 100644 --- a/esphome/components/lcd_menu/lcd_menu.h +++ b/esphome/components/lcd_menu/lcd_menu.h @@ -6,8 +6,7 @@ #include <forward_list> #include <vector> -namespace esphome { -namespace lcd_menu { +namespace esphome::lcd_menu { /** Class to display a hierarchical menu. * @@ -41,5 +40,4 @@ class LCDCharacterMenuComponent : public display_menu_base::DisplayMenuComponent char mark_back_; }; -} // namespace lcd_menu -} // namespace esphome +} // namespace esphome::lcd_menu diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index d582eead913..d5fc6835985 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace lcd_pcf8574 { +namespace esphome::lcd_pcf8574 { static const char *const TAG = "lcd_pcf8574"; @@ -56,5 +55,4 @@ void PCF8574LCDDisplay::no_backlight() { this->write_bytes(this->backlight_value_, nullptr, 0); } -} // namespace lcd_pcf8574 -} // namespace esphome +} // namespace esphome::lcd_pcf8574 diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 672b6090368..9ec5ad71af1 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace lcd_pcf8574 { +namespace esphome::lcd_pcf8574 { class PCF8574LCDDisplay; @@ -32,5 +31,4 @@ class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { pcf8574_lcd_writer_t writer_; }; -} // namespace lcd_pcf8574 -} // namespace esphome +} // namespace esphome::lcd_pcf8574 diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h index 8b34de9fbbd..1e005ab44cf 100644 --- a/esphome/components/lightwaverf/LwRx.h +++ b/esphome/components/lightwaverf/LwRx.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { // LwRx.h // @@ -138,5 +137,4 @@ class LwRx { InternalGPIOPin *rx_pin_; }; -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index 9192426440f..2d0019c0957 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { // LxTx.h // @@ -90,5 +89,4 @@ class LwTx { uint32_t duty_off_; }; -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index ee6c2ee4718..87319235e9b 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace lilygo_t5_47 { +namespace esphome::lilygo_t5_47 { static const char *const TAG = "lilygo_t5_47.touchscreen"; @@ -104,5 +103,4 @@ void LilygoT547Touchscreen::dump_config() { LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } -} // namespace lilygo_t5_47 -} // namespace esphome +} // namespace esphome::lilygo_t5_47 diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h index 6767bf0a71c..8b345515ab0 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace lilygo_t5_47 { +namespace esphome::lilygo_t5_47 { using namespace touchscreen; @@ -27,5 +26,4 @@ class LilygoT547Touchscreen : public Touchscreen, public i2c::I2CDevice { InternalGPIOPin *interrupt_pin_; }; -} // namespace lilygo_t5_47 -} // namespace esphome +} // namespace esphome::lilygo_t5_47 diff --git a/esphome/components/lm75b/lm75b.cpp b/esphome/components/lm75b/lm75b.cpp index 19398eda853..2e8b9d3cfe1 100644 --- a/esphome/components/lm75b/lm75b.cpp +++ b/esphome/components/lm75b/lm75b.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace lm75b { +namespace esphome::lm75b { static const char *const TAG = "lm75b"; @@ -35,5 +34,4 @@ void LM75BComponent::update() { } } -} // namespace lm75b -} // namespace esphome +} // namespace esphome::lm75b diff --git a/esphome/components/lm75b/lm75b.h b/esphome/components/lm75b/lm75b.h index 79d9fa3f32d..eaf1b46550e 100644 --- a/esphome/components/lm75b/lm75b.h +++ b/esphome/components/lm75b/lm75b.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace lm75b { +namespace esphome::lm75b { static const uint8_t LM75B_REG_TEMPERATURE = 0x00; @@ -15,5 +14,4 @@ class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public se void update() override; }; -} // namespace lm75b -} // namespace esphome +} // namespace esphome::lm75b diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp index 592b7faaf0f..020add695c1 100644 --- a/esphome/components/lps22/lps22.cpp +++ b/esphome/components/lps22/lps22.cpp @@ -1,7 +1,6 @@ #include "lps22.h" -namespace esphome { -namespace lps22 { +namespace esphome::lps22 { static constexpr const char *const TAG = "lps22"; @@ -78,5 +77,4 @@ void LPS22Component::try_read_() { } } -} // namespace lps22 -} // namespace esphome +} // namespace esphome::lps22 diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h index 95ee4ad4427..c6746f23433 100644 --- a/esphome/components/lps22/lps22.h +++ b/esphome/components/lps22/lps22.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace lps22 { +namespace esphome::lps22 { class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -24,5 +23,4 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2 uint8_t read_attempts_remaining_{0}; }; -} // namespace lps22 -} // namespace esphome +} // namespace esphome::lps22 diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index 033f31a3d17..62a0d2290ae 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace ltr390 { +namespace esphome::ltr390 { static const char *const TAG = "ltr390"; @@ -203,5 +202,4 @@ void LTR390Component::update() { this->read_mode_((this->enabled_modes_ & ENABLED_MODE_ALS) ? LTR390_MODE_ALS : LTR390_MODE_UVS); } -} // namespace ltr390 -} // namespace esphome +} // namespace esphome::ltr390 diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 47884b91667..1ead84b4a86 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" -namespace esphome { -namespace ltr390 { +namespace esphome::ltr390 { enum LTR390CTRL { LTR390_CTRL_EN = 1, @@ -85,5 +84,4 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *uv_sensor_{nullptr}; }; -} // namespace ltr390 -} // namespace esphome +} // namespace esphome::ltr390 diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 4c9006be1d9..9cba06e483b 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -6,8 +6,7 @@ using esphome::i2c::ErrorCode; -namespace esphome { -namespace ltr501 { +namespace esphome::ltr501 { static const char *const TAG = "ltr501"; @@ -542,5 +541,4 @@ void LTRAlsPs501Component::publish_data_part_2_(AlsReadings &data) { this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); } } -} // namespace ltr501 -} // namespace esphome +} // namespace esphome::ltr501 diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 2b914631087..c7eccbeea96 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -8,8 +8,7 @@ #include "ltr_definitions_501.h" -namespace esphome { -namespace ltr501 { +namespace esphome::ltr501 { enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; @@ -162,5 +161,4 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; }; -} // namespace ltr501 -} // namespace esphome +} // namespace esphome::ltr501 diff --git a/esphome/components/ltr501/ltr_definitions_501.h b/esphome/components/ltr501/ltr_definitions_501.h index 604bd92b680..c92fad2d66e 100644 --- a/esphome/components/ltr501/ltr_definitions_501.h +++ b/esphome/components/ltr501/ltr_definitions_501.h @@ -2,8 +2,7 @@ #include <cstdint> -namespace esphome { -namespace ltr501 { +namespace esphome::ltr501 { enum class CommandRegisters : uint8_t { ALS_CONTR = 0x80, // ALS operation mode control and SW reset @@ -256,5 +255,4 @@ union InterruptPersistRegister { } __attribute__((packed)); }; -} // namespace ltr501 -} // namespace esphome +} // namespace esphome::ltr501 diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index ff335fe34c6..b7fad2e8767 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -6,8 +6,7 @@ using esphome::i2c::ErrorCode; -namespace esphome { -namespace ltr_als_ps { +namespace esphome::ltr_als_ps { static const char *const TAG = "ltr_als_ps"; @@ -521,5 +520,4 @@ void LTRAlsPsComponent::publish_data_part_2_(AlsReadings &data) { this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); } } -} // namespace ltr_als_ps -} // namespace esphome +} // namespace esphome::ltr_als_ps diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 8aa5c9f24b1..67d8fddad22 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -8,8 +8,7 @@ #include "ltr_definitions.h" -namespace esphome { -namespace ltr_als_ps { +namespace esphome::ltr_als_ps { enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; @@ -162,5 +161,4 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { CallbackManager<void()> on_ps_high_trigger_callback_; CallbackManager<void()> on_ps_low_trigger_callback_; }; -} // namespace ltr_als_ps -} // namespace esphome +} // namespace esphome::ltr_als_ps diff --git a/esphome/components/ltr_als_ps/ltr_definitions.h b/esphome/components/ltr_als_ps/ltr_definitions.h index 739445e9a0e..c70c2f1804e 100644 --- a/esphome/components/ltr_als_ps/ltr_definitions.h +++ b/esphome/components/ltr_als_ps/ltr_definitions.h @@ -2,8 +2,7 @@ #include <cstdint> -namespace esphome { -namespace ltr_als_ps { +namespace esphome::ltr_als_ps { enum class CommandRegisters : uint8_t { ALS_CONTR = 0x80, // ALS operation mode control and SW reset @@ -271,5 +270,4 @@ union InterruptPersistRegister { } __attribute__((packed)); }; -} // namespace ltr_als_ps -} // namespace esphome +} // namespace esphome::ltr_als_ps diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 7309df97638..50da7af6021 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -4,8 +4,7 @@ #include "esphome/components/light/light_output.h" #include "../lvgl_esphome.h" -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { class LVLight : public light::LightOutput { public: @@ -44,5 +43,4 @@ class LVLight : public light::LightOutput { optional<lv_color_t> initial_value_{}; }; -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/lvgl_proxy.h b/esphome/components/lvgl/lvgl_proxy.h index 0ccd80e541b..499735ad88e 100644 --- a/esphome/components/lvgl/lvgl_proxy.h +++ b/esphome/components/lvgl/lvgl_proxy.h @@ -11,7 +11,5 @@ file is included in the build, LVGL is always included. #endif // LV_CONF_H #include <lvgl.h> -namespace esphome { -namespace lvgl {} // namespace lvgl -} // namespace esphome -#endif // USE_LVGL +namespace esphome::lvgl {} // namespace esphome::lvgl +#endif // USE_LVGL diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 44409a0ad53..ba16b1f0b39 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { class LVGLNumber : public number::Number, public Component { public: @@ -48,5 +47,4 @@ class LVGLNumber : public number::Number, public Component { ESPPreferenceObject pref_{}; }; -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 3b00310b67b..ffbe29d701e 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -8,8 +8,7 @@ #include "esphome/core/preferences.h" #include "esphome/components/lvgl/lvgl_esphome.h" -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { class LVGLSelect : public select::Select, public Component { public: @@ -71,5 +70,4 @@ class LVGLSelect : public select::Select, public Component { ESPPreferenceObject pref_{}; }; -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h index 485459691c9..8f5502a7d5e 100644 --- a/esphome/components/lvgl/switch/lvgl_switch.h +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -7,8 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { class LVGLSwitch : public switch_::Switch, public Component { public: @@ -21,5 +20,4 @@ class LVGLSwitch : public switch_::Switch, public Component { std::function<void(bool)> state_lambda_{}; }; -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index eacf69b6ec8..fead48d6fe1 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { class LVGLText : public text::Text { public: @@ -29,5 +28,4 @@ class LVGLText : public text::Text { optional<std::string> initial_state_{}; }; -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp index 3eeba4a644f..680b3bcd9e7 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp @@ -1,7 +1,6 @@ #include "m5stack_8angle_binary_sensor.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { void M5Stack8AngleSwitchBinarySensor::update() { int8_t out = this->parent_->read_switch(); @@ -13,5 +12,4 @@ void M5Stack8AngleSwitchBinarySensor::update() { this->status_clear_warning(); } -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h index b8bb601525e..14400bcea17 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.h @@ -5,8 +5,7 @@ #include "../m5stack_8angle.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor, public PollingComponent, @@ -15,5 +14,4 @@ class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor, void update() override; }; -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp index 0e7b9029198..e132c54daad 100644 --- a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { static const char *const TAG = "m5stack_8angle.light"; @@ -41,5 +40,4 @@ light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) c nullptr, this->effect_data_ + index, &this->correction_}; } -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h index 204f2c04c7b..0a5a50f2a86 100644 --- a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.h @@ -5,8 +5,7 @@ #include "../m5stack_8angle.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9; static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4; @@ -33,5 +32,4 @@ class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented uint8_t *effect_data_{nullptr}; }; -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index 2de900c21dc..f466fba77e7 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { static const char *const TAG = "m5stack_8angle"; @@ -69,5 +68,4 @@ int8_t M5Stack8AngleComponent::read_switch() { } } -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.h b/esphome/components/m5stack_8angle/m5stack_8angle.h index 49425180540..ab2e232204e 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.h +++ b/esphome/components/m5stack_8angle/m5stack_8angle.h @@ -3,8 +3,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00; static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10; @@ -29,5 +28,4 @@ class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { uint8_t fw_version_; }; -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp index d22b3451415..b05e1e68163 100644 --- a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp @@ -1,7 +1,6 @@ #include "m5stack_8angle_sensor.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { void M5Stack8AngleKnobSensor::update() { if (this->parent_ != nullptr) { @@ -20,5 +19,4 @@ void M5Stack8AngleKnobSensor::update() { }; } -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h index 4848f8f80f5..418503d7c87 100644 --- a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.h @@ -5,8 +5,7 @@ #include "../m5stack_8angle.h" -namespace esphome { -namespace m5stack_8angle { +namespace esphome::m5stack_8angle { class M5Stack8AngleKnobSensor : public sensor::Sensor, public PollingComponent, @@ -23,5 +22,4 @@ class M5Stack8AngleKnobSensor : public sensor::Sensor, bool raw_; }; -} // namespace m5stack_8angle -} // namespace esphome +} // namespace esphome::m5stack_8angle diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h index 2c1ce96f0a6..53ae0b5c03b 100644 --- a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h +++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/matrix_keypad/matrix_keypad.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace matrix_keypad { +namespace esphome::matrix_keypad { class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensorInitiallyOff { public: @@ -47,5 +46,4 @@ class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sens int col_; }; -} // namespace matrix_keypad -} // namespace esphome +} // namespace esphome::matrix_keypad diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index cc46ba98d63..3b71b50fd87 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace matrix_keypad { +namespace esphome::matrix_keypad { static const char *const TAG = "matrix_keypad"; @@ -110,5 +109,4 @@ void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->lis void MatrixKeypad::register_key_trigger(MatrixKeyTrigger *trig) { this->key_triggers_.push_back(trig); } -} // namespace matrix_keypad -} // namespace esphome +} // namespace esphome::matrix_keypad diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 8963612d0c7..1e263842ea0 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -8,8 +8,7 @@ #include <cstdlib> #include <utility> -namespace esphome { -namespace matrix_keypad { +namespace esphome::matrix_keypad { class MatrixKeypadListener { public: @@ -51,5 +50,4 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::vector<MatrixKeyTrigger *> key_triggers_; }; -} // namespace matrix_keypad -} // namespace esphome +} // namespace esphome::matrix_keypad diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h index ac201a73096..c98516d2593 100644 --- a/esphome/components/max17043/automation.h +++ b/esphome/components/max17043/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "max17043.h" -namespace esphome { -namespace max17043 { +namespace esphome::max17043 { template<typename... Ts> class SleepAction : public Action<Ts...> { public: @@ -16,5 +15,4 @@ template<typename... Ts> class SleepAction : public Action<Ts...> { MAX17043Component *max17043_; }; -} // namespace max17043 -} // namespace esphome +} // namespace esphome::max17043 diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index dfd59f1e7df..b59bac7ebf5 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -1,8 +1,7 @@ #include "max17043.h" #include "esphome/core/log.h" -namespace esphome { -namespace max17043 { +namespace esphome::max17043 { // MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert // Consult the datasheet at https://www.analog.com/en/products/max17043.html @@ -90,5 +89,4 @@ void MAX17043Component::sleep_mode() { } } -} // namespace max17043 -} // namespace esphome +} // namespace esphome::max17043 diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h index f477ce59487..dd2e35df555 100644 --- a/esphome/components/max17043/max17043.h +++ b/esphome/components/max17043/max17043.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace max17043 { +namespace esphome::max17043 { class MAX17043Component : public PollingComponent, public i2c::I2CDevice { public: @@ -24,5 +23,4 @@ class MAX17043Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *battery_remaining_sensor_{nullptr}; }; -} // namespace max17043 -} // namespace esphome +} // namespace esphome::max17043 diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 8370977ce2e..ac6b3cecb42 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max31855 { +namespace esphome::max31855 { static const char *const TAG = "max31855"; @@ -100,5 +99,4 @@ void MAX31855Sensor::read_data_() { this->status_clear_warning(); } -} // namespace max31855 -} // namespace esphome +} // namespace esphome::max31855 diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index b755d240f26..dd7a2052686 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace max31855 { +namespace esphome::max31855 { class MAX31855Sensor : public sensor::Sensor, public PollingComponent, @@ -26,5 +25,4 @@ class MAX31855Sensor : public sensor::Sensor, sensor::Sensor *temperature_reference_{nullptr}; }; -} // namespace max31855 -} // namespace esphome +} // namespace esphome::max31855 diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 35e12309ba3..4062d21beee 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <cmath> -namespace esphome { -namespace max31856 { +namespace esphome::max31856 { static const char *const TAG = "max31856"; @@ -197,5 +196,4 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { return value; } -} // namespace max31856 -} // namespace esphome +} // namespace esphome::max31856 diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index a27ababa2ea..0a983b72d98 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace max31856 { +namespace esphome::max31856 { enum MAX31856RegisterMasks { SPI_WRITE_M = 0x80 }; @@ -98,5 +97,4 @@ class MAX31856Sensor : public sensor::Sensor, void set_noise_filter_(); }; -} // namespace max31856 -} // namespace esphome +} // namespace esphome::max31856 diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 8b06a01166e..220fb4e7041 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -4,8 +4,7 @@ #include <cmath> #include <cinttypes> -namespace esphome { -namespace max31865 { +namespace esphome::max31865 { static const char *const TAG = "max31865"; @@ -226,5 +225,4 @@ float MAX31865Sensor::calc_temperature_(float rtd_ratio) { return neg_temp; } -} // namespace max31865 -} // namespace esphome +} // namespace esphome::max31865 diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index 440c6523a6c..3362cd30dee 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace max31865 { +namespace esphome::max31865 { enum MAX31865RegisterMasks { SPI_WRITE_M = 0x80 }; enum MAX31865Registers { @@ -53,5 +52,4 @@ class MAX31865Sensor : public sensor::Sensor, float calc_temperature_(float rtd_ratio); }; -} // namespace max31865 -} // namespace esphome +} // namespace esphome::max31865 diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index cbce0535196..6b8bdc8de5e 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace max44009 { +namespace esphome::max44009 { static const char *const TAG = "max44009.sensor"; @@ -137,5 +136,4 @@ void MAX44009Sensor::write_(uint8_t reg, uint8_t value) { void MAX44009Sensor::set_mode(MAX44009Mode mode) { this->mode_ = mode; } -} // namespace max44009 -} // namespace esphome +} // namespace esphome::max44009 diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index d0ffd7bc706..12fd0b1ce0f 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace max44009 { +namespace esphome::max44009 { enum MAX44009Mode { MAX44009_MODE_AUTO, MAX44009_MODE_LOW_POWER, MAX44009_MODE_CONTINUOUS }; @@ -32,5 +31,4 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 MAX44009Mode mode_{MAX44009_MODE_AUTO}; }; -} // namespace max44009 -} // namespace esphome +} // namespace esphome::max44009 diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index b8527c6b1dd..8734405508f 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -1,8 +1,7 @@ #include "max6675.h" #include "esphome/core/log.h" -namespace esphome { -namespace max6675 { +namespace esphome::max6675 { static const char *const TAG = "max6675"; @@ -43,5 +42,4 @@ void MAX6675Sensor::read_data_() { this->status_clear_warning(); } -} // namespace max6675 -} // namespace esphome +} // namespace esphome::max6675 diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index f0db4a6c268..e7b5c4dbde5 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace max6675 { +namespace esphome::max6675 { class MAX6675Sensor : public sensor::Sensor, public PollingComponent, @@ -21,5 +20,4 @@ class MAX6675Sensor : public sensor::Sensor, void read_data_(); }; -} // namespace max6675 -} // namespace esphome +} // namespace esphome::max6675 diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h index ca2c3e3ce4d..547ed5a8656 100644 --- a/esphome/components/max6956/automation.h +++ b/esphome/components/max6956/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/max6956/max6956.h" -namespace esphome { -namespace max6956 { +namespace esphome::max6956 { template<typename... Ts> class SetCurrentGlobalAction : public Action<Ts...> { public: @@ -36,5 +35,4 @@ template<typename... Ts> class SetCurrentModeAction : public Action<Ts...> { protected: MAX6956 *max6956_; }; -} // namespace max6956 -} // namespace esphome +} // namespace esphome::max6956 diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index ce45541b635..ccb14496aa8 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -1,8 +1,7 @@ #include "max6956.h" #include "esphome/core/log.h" -namespace esphome { -namespace max6956 { +namespace esphome::max6956 { static const char *const TAG = "max6956"; @@ -167,5 +166,4 @@ size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via Max6956", this->pin_); } -} // namespace max6956 -} // namespace esphome +} // namespace esphome::max6956 diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 31f97c11f8a..83ccfab559f 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace max6956 { +namespace esphome::max6956 { /// Modes for MAX6956 pins enum MAX6956GPIOMode : uint8_t { @@ -92,5 +91,4 @@ class MAX6956GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace max6956 -} // namespace esphome +} // namespace esphome::max6956 diff --git a/esphome/components/max6956/output/max6956_led_output.cpp b/esphome/components/max6956/output/max6956_led_output.cpp index 5fa2dd9b344..c53a429d203 100644 --- a/esphome/components/max6956/output/max6956_led_output.cpp +++ b/esphome/components/max6956/output/max6956_led_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max6956 { +namespace esphome::max6956 { static const char *const TAG = "max6956_led_channel"; @@ -22,5 +21,4 @@ void MAX6956LedChannel::dump_config() { LOG_FLOAT_OUTPUT(this); } -} // namespace max6956 -} // namespace esphome +} // namespace esphome::max6956 diff --git a/esphome/components/max6956/output/max6956_led_output.h b/esphome/components/max6956/output/max6956_led_output.h index b844a7ceee6..49e5b9ef842 100644 --- a/esphome/components/max6956/output/max6956_led_output.h +++ b/esphome/components/max6956/output/max6956_led_output.h @@ -3,8 +3,7 @@ #include "esphome/components/max6956/max6956.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace max6956 { +namespace esphome::max6956 { class MAX6956; @@ -24,5 +23,4 @@ class MAX6956LedChannel : public output::FloatOutput, public Component { uint8_t pin_; }; -} // namespace max6956 -} // namespace esphome +} // namespace esphome::max6956 diff --git a/esphome/components/max7219digit/automation.h b/esphome/components/max7219digit/automation.h index be8245d14d3..485a34075ee 100644 --- a/esphome/components/max7219digit/automation.h +++ b/esphome/components/max7219digit/automation.h @@ -5,8 +5,7 @@ #include "max7219digit.h" -namespace esphome { -namespace max7219digit { +namespace esphome::max7219digit { template<typename... Ts> class DisplayInvertAction : public Action<Ts...>, public Parented<MAX7219Component> { public: @@ -48,5 +47,4 @@ template<typename... Ts> class DisplayIntensityAction : public Action<Ts...>, pu } }; -} // namespace max7219digit -} // namespace esphome +} // namespace esphome::max7219digit diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index f9b46cf797d..26c65aa5d34 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -8,8 +8,7 @@ #include <algorithm> #include <cinttypes> -namespace esphome { -namespace max7219digit { +namespace esphome::max7219digit { static const char *const TAG = "max7219DIGIT"; @@ -352,5 +351,4 @@ uint8_t MAX7219Component::strftimedigit(const char *format, ESPTime time) { return this->strftimedigit(0, format, time); } -} // namespace max7219digit -} // namespace esphome +} // namespace esphome::max7219digit diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index af419b9b382..bbf43059ddc 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace max7219digit { +namespace esphome::max7219digit { enum ChipLinesStyle { ZIGZAG = 0, @@ -120,5 +119,4 @@ class MAX7219Component : public display::DisplayBuffer, max7219_writer_t writer_local_{}; }; -} // namespace max7219digit -} // namespace esphome +} // namespace esphome::max7219digit diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h index 53674dc60f1..a5eea7e20fd 100644 --- a/esphome/components/max7219digit/max7219font.h +++ b/esphome/components/max7219digit/max7219font.h @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace max7219digit { +namespace esphome::max7219digit { // bit patterns for the CP437 font @@ -266,5 +265,4 @@ constexpr uint8_t MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0xFF }; // end of MAX7219_Dot_Matrix_font -} // namespace max7219digit -} // namespace esphome +} // namespace esphome::max7219digit diff --git a/esphome/components/max9611/max9611.cpp b/esphome/components/max9611/max9611.cpp index f00f9d76be4..68ef7c31358 100644 --- a/esphome/components/max9611/max9611.cpp +++ b/esphome/components/max9611/max9611.cpp @@ -1,8 +1,8 @@ #include "max9611.h" #include "esphome/core/log.h" #include "esphome/components/i2c/i2c_bus.h" -namespace esphome { -namespace max9611 { + +namespace esphome::max9611 { using namespace esphome::i2c; // Sign extend // http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend @@ -91,5 +91,4 @@ void MAX9611Component::update() { ESP_LOGD(TAG, "V: %f, A: %f, W: %f, Deg C: %f", voltage, amps, watts, temp); } -} // namespace max9611 -} // namespace esphome +} // namespace esphome::max9611 diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h index 1eb7542aeee..b6fb5d81273 100644 --- a/esphome/components/max9611/max9611.h +++ b/esphome/components/max9611/max9611.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/hal.h" -namespace esphome { -namespace max9611 { +namespace esphome::max9611 { enum MAX9611Multiplexer { MAX9611_MULTIPLEXER_CSA_GAIN1 = 0b000, @@ -57,5 +56,4 @@ class MAX9611Component : public PollingComponent, public i2c::I2CDevice { MAX9611Multiplexer gain_; }; -} // namespace max9611 -} // namespace esphome +} // namespace esphome::max9611 diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 5f73e03f6f6..6be5f4c9518 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -1,8 +1,7 @@ #include "mcp23008.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23008 { +namespace esphome::mcp23008 { static const char *const TAG = "mcp23008"; @@ -45,5 +44,4 @@ bool MCP23008::write_reg(uint8_t reg, uint8_t value) { return this->write_byte(reg, value); } -} // namespace mcp23008 -} // namespace esphome +} // namespace esphome::mcp23008 diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h index 406ce0b419f..ae2f9e1f3c0 100644 --- a/esphome/components/mcp23008/mcp23008.h +++ b/esphome/components/mcp23008/mcp23008.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp23008 { +namespace esphome::mcp23008 { class MCP23008 : public mcp23x08_base::MCP23X08Base, public i2c::I2CDevice { public: @@ -20,5 +19,4 @@ class MCP23008 : public mcp23x08_base::MCP23X08Base, public i2c::I2CDevice { bool write_reg(uint8_t reg, uint8_t value) override; }; -} // namespace mcp23008 -} // namespace esphome +} // namespace esphome::mcp23008 diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index b7a9cfd0ce7..126ece3e7b9 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cstdio> -namespace esphome { -namespace mcp23016 { +namespace esphome::mcp23016 { static const char *const TAG = "mcp23016"; @@ -101,5 +100,4 @@ size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via MCP23016", this->pin_); } -} // namespace mcp23016 -} // namespace esphome +} // namespace esphome::mcp23016 diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 32149ba3e24..4a936a5b02e 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/gpio_expander/cached_gpio.h" -namespace esphome { -namespace mcp23016 { +namespace esphome::mcp23016 { enum MCP23016GPIORegisters { // 0 side @@ -79,5 +78,4 @@ class MCP23016GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace mcp23016 -} // namespace esphome +} // namespace esphome::mcp23016 diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 212c15ccf27..9e3d75575ae 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -1,8 +1,7 @@ #include "mcp23017.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23017 { +namespace esphome::mcp23017 { static const char *const TAG = "mcp23017"; @@ -54,5 +53,4 @@ bool MCP23017::write_reg(uint8_t reg, uint8_t value) { return this->write_byte(reg, value); } -} // namespace mcp23017 -} // namespace esphome +} // namespace esphome::mcp23017 diff --git a/esphome/components/mcp23017/mcp23017.h b/esphome/components/mcp23017/mcp23017.h index 8959e06a413..86b84f9ad80 100644 --- a/esphome/components/mcp23017/mcp23017.h +++ b/esphome/components/mcp23017/mcp23017.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp23017 { +namespace esphome::mcp23017 { class MCP23017 : public mcp23x17_base::MCP23X17Base, public i2c::I2CDevice { public: @@ -20,5 +19,4 @@ class MCP23017 : public mcp23x17_base::MCP23X17Base, public i2c::I2CDevice { bool write_reg(uint8_t reg, uint8_t value) override; }; -} // namespace mcp23017 -} // namespace esphome +} // namespace esphome::mcp23017 diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 983c1aa6008..e7f582f787c 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -1,8 +1,7 @@ #include "mcp23s08.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23s08 { +namespace esphome::mcp23s08 { static const char *const TAG = "mcp23s08"; @@ -62,5 +61,4 @@ bool MCP23S08::write_reg(uint8_t reg, uint8_t value) { return true; } -} // namespace mcp23s08 -} // namespace esphome +} // namespace esphome::mcp23s08 diff --git a/esphome/components/mcp23s08/mcp23s08.h b/esphome/components/mcp23s08/mcp23s08.h index a2a6be880aa..441525469f7 100644 --- a/esphome/components/mcp23s08/mcp23s08.h +++ b/esphome/components/mcp23s08/mcp23s08.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace mcp23s08 { +namespace esphome::mcp23s08 { class MCP23S08 : public mcp23x08_base::MCP23X08Base, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -26,5 +25,4 @@ class MCP23S08 : public mcp23x08_base::MCP23X08Base, uint8_t device_opcode_ = 0x40; }; -} // namespace mcp23s08 -} // namespace esphome +} // namespace esphome::mcp23s08 diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index db9a34e2302..652931e9a93 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -1,8 +1,7 @@ #include "mcp23s17.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23s17 { +namespace esphome::mcp23s17 { static const char *const TAG = "mcp23s17"; @@ -79,5 +78,4 @@ bool MCP23S17::write_reg(uint8_t reg, uint8_t value) { return true; } -} // namespace mcp23s17 -} // namespace esphome +} // namespace esphome::mcp23s17 diff --git a/esphome/components/mcp23s17/mcp23s17.h b/esphome/components/mcp23s17/mcp23s17.h index cb5d6cfcd8d..0cc9321c887 100644 --- a/esphome/components/mcp23s17/mcp23s17.h +++ b/esphome/components/mcp23s17/mcp23s17.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace mcp23s17 { +namespace esphome::mcp23s17 { class MCP23S17 : public mcp23x17_base::MCP23X17Base, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -25,5 +24,4 @@ class MCP23S17 : public mcp23x17_base::MCP23X17Base, uint8_t device_opcode_ = 0x40; }; -} // namespace mcp23s17 -} // namespace esphome +} // namespace esphome::mcp23s17 diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 197b7392042..98fd2f62493 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -1,8 +1,7 @@ #include "mcp23x08_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23x08_base { +namespace esphome::mcp23x08_base { static const char *const TAG = "mcp23x08_base"; @@ -92,5 +91,4 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { } } -} // namespace mcp23x08_base -} // namespace esphome +} // namespace esphome::mcp23x08_base diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.h b/esphome/components/mcp23x08_base/mcp23x08_base.h index 6eee8274b15..0d9fefaf948 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.h +++ b/esphome/components/mcp23x08_base/mcp23x08_base.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mcp23x08_base { +namespace esphome::mcp23x08_base { enum MCP23S08GPIORegisters { // A side @@ -40,5 +39,4 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> { uint8_t input_mask_{0x00}; }; -} // namespace mcp23x08_base -} // namespace esphome +} // namespace esphome::mcp23x08_base diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index efed7f5f17e..870c6c165f5 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23x17_base { +namespace esphome::mcp23x17_base { static const char *const TAG = "mcp23x17_base"; @@ -108,5 +107,4 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { } } -} // namespace mcp23x17_base -} // namespace esphome +} // namespace esphome::mcp23x17_base diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.h b/esphome/components/mcp23x17_base/mcp23x17_base.h index bdd66503e2c..bddfff132de 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.h +++ b/esphome/components/mcp23x17_base/mcp23x17_base.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mcp23x17_base { +namespace esphome::mcp23x17_base { enum MCP23X17GPIORegisters { // A side @@ -53,5 +52,4 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> { uint16_t input_mask_{0x00}; }; -} // namespace mcp23x17_base -} // namespace esphome +} // namespace esphome::mcp23x17_base diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 4c1daac5623..b8032fcec8c 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp23xxx_base { +namespace esphome::mcp23xxx_base { template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() { this->pin_mode(flags_); @@ -28,5 +27,4 @@ template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t template class MCP23XXXGPIOPin<8>; template class MCP23XXXGPIOPin<16>; -} // namespace mcp23xxx_base -} // namespace esphome +} // namespace esphome::mcp23xxx_base diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 8a87dac1433..5904a1eef62 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mcp23xxx_base { +namespace esphome::mcp23xxx_base { enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; @@ -81,5 +80,4 @@ template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin { MCP23XXXInterruptMode interrupt_mode_; }; -} // namespace mcp23xxx_base -} // namespace esphome +} // namespace esphome::mcp23xxx_base diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp index c2db9228c8b..f8c5e9f0685 100644 --- a/esphome/components/mcp2515/mcp2515.cpp +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -1,8 +1,7 @@ #include "mcp2515.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp2515 { +namespace esphome::mcp2515 { static const char *const TAG = "mcp2515"; @@ -707,5 +706,4 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo return canbus::ERROR_FAIL; } } -} // namespace mcp2515 -} // namespace esphome +} // namespace esphome::mcp2515 diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h index c77480ce7de..b77d9a25828 100644 --- a/esphome/components/mcp2515/mcp2515.h +++ b/esphome/components/mcp2515/mcp2515.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "mcp2515_defs.h" -namespace esphome { -namespace mcp2515 { +namespace esphome::mcp2515 { static const uint32_t SPI_CLOCK = 10000000; // 10MHz static const int N_TXBUFFERS = 3; @@ -108,5 +107,4 @@ class MCP2515 : public canbus::Canbus, void clear_merr_(); void clear_errif_(); }; -} // namespace mcp2515 -} // namespace esphome +} // namespace esphome::mcp2515 diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h index b33adcbba67..e2a7b97bd67 100644 --- a/esphome/components/mcp2515/mcp2515_defs.h +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace mcp2515 { +namespace esphome::mcp2515 { static const uint8_t CANCTRL_REQOP = 0xE0; static const uint8_t CANCTRL_ABAT = 0x10; @@ -371,5 +370,4 @@ static const uint8_t MCP_20MHZ_33K3BPS_CFG1 = 0x0B; static const uint8_t MCP_20MHZ_33K3BPS_CFG2 = 0xFF; static const uint8_t MCP_20MHZ_33K3BPS_CFG3 = 0x87; -} // namespace mcp2515 -} // namespace esphome +} // namespace esphome::mcp2515 diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 812a3b0c83d..e65e249f529 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp3008 { +namespace esphome::mcp3008 { static const char *const TAG = "mcp3008"; @@ -36,5 +35,4 @@ float MCP3008::read_data(uint8_t pin) { return data / 1023.0f; } -} // namespace mcp3008 -} // namespace esphome +} // namespace esphome::mcp3008 diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index baf8d7c152d..1b1b50c7935 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mcp3008 { +namespace esphome::mcp3008 { class MCP3008 : public Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -18,5 +17,4 @@ class MCP3008 : public Component, float read_data(uint8_t pin); }; -} // namespace mcp3008 -} // namespace esphome +} // namespace esphome::mcp3008 diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp index ee052e9fb79..2647ed199e1 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace mcp3008 { +namespace esphome::mcp3008 { static const char *const TAG = "mcp3008.sensor"; @@ -23,5 +22,4 @@ float MCP3008Sensor::sample() { void MCP3008Sensor::update() { this->publish_state(this->sample()); } -} // namespace mcp3008 -} // namespace esphome +} // namespace esphome::mcp3008 diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h index 9478d38e747..9267f80ea83 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.h +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -6,8 +6,7 @@ #include "../mcp3008.h" -namespace esphome { -namespace mcp3008 { +namespace esphome::mcp3008 { class MCP3008Sensor : public PollingComponent, public sensor::Sensor, @@ -26,5 +25,4 @@ class MCP3008Sensor : public PollingComponent, float reference_voltage_; }; -} // namespace mcp3008 -} // namespace esphome +} // namespace esphome::mcp3008 diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index abefcad0eb1..5351d6a2cb9 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -1,8 +1,7 @@ #include "mcp3204.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp3204 { +namespace esphome::mcp3204 { static const char *const TAG = "mcp3204"; @@ -35,5 +34,4 @@ float MCP3204::read_data(uint8_t pin, bool differential) { return float(digital_value) / 4096.000 * this->reference_voltage_; // in V } -} // namespace mcp3204 -} // namespace esphome +} // namespace esphome::mcp3204 diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h index 6287263a2a7..8ce592f3866 100644 --- a/esphome/components/mcp3204/mcp3204.h +++ b/esphome/components/mcp3204/mcp3204.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace mcp3204 { +namespace esphome::mcp3204 { class MCP3204 : public Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -24,5 +23,4 @@ class MCP3204 : public Component, float reference_voltage_; }; -} // namespace mcp3204 -} // namespace esphome +} // namespace esphome::mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index 711448cf448..153c37d160f 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace mcp3204 { +namespace esphome::mcp3204 { static const char *const TAG = "mcp3204.sensor"; @@ -18,5 +17,4 @@ void MCP3204Sensor::dump_config() { float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_, this->differential_mode_); } void MCP3204Sensor::update() { this->publish_state(this->sample()); } -} // namespace mcp3204 -} // namespace esphome +} // namespace esphome::mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 2bf75a9c1e5..5fe5f54d1b8 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -7,8 +7,7 @@ #include "../mcp3204.h" -namespace esphome { -namespace mcp3204 { +namespace esphome::mcp3204 { class MCP3204Sensor : public PollingComponent, public Parented<MCP3204>, @@ -26,5 +25,4 @@ class MCP3204Sensor : public PollingComponent, bool differential_mode_; }; -} // namespace mcp3204 -} // namespace esphome +} // namespace esphome::mcp3204 diff --git a/esphome/components/mcp3221/mcp3221_sensor.cpp b/esphome/components/mcp3221/mcp3221_sensor.cpp index c04b1c0b935..1b794ba9663 100644 --- a/esphome/components/mcp3221/mcp3221_sensor.cpp +++ b/esphome/components/mcp3221/mcp3221_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp3221 { +namespace esphome::mcp3221 { static const char *const TAG = "mcp3221"; @@ -27,5 +26,4 @@ void MCP3221Sensor::update() { this->publish_state(v); } -} // namespace mcp3221 -} // namespace esphome +} // namespace esphome::mcp3221 diff --git a/esphome/components/mcp3221/mcp3221_sensor.h b/esphome/components/mcp3221/mcp3221_sensor.h index c83caccabfe..deef14e14d8 100644 --- a/esphome/components/mcp3221/mcp3221_sensor.h +++ b/esphome/components/mcp3221/mcp3221_sensor.h @@ -8,8 +8,7 @@ #include <cinttypes> -namespace esphome { -namespace mcp3221 { +namespace esphome::mcp3221 { class MCP3221Sensor : public sensor::Sensor, public PollingComponent, @@ -24,5 +23,4 @@ class MCP3221Sensor : public sensor::Sensor, float reference_voltage_; }; -} // namespace mcp3221 -} // namespace esphome +} // namespace esphome::mcp3221 diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 48d90377df8..45735536642 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mcp4461 { +namespace esphome::mcp4461 { static const char *const TAG = "mcp4461"; constexpr uint8_t EEPROM_WRITE_TIMEOUT_MS = 10; @@ -628,5 +627,4 @@ bool Mcp4461Component::mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolat } return this->write_byte(reg, value_byte); } -} // namespace mcp4461 -} // namespace esphome +} // namespace esphome::mcp4461 diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h index 59f6358a567..3a76f855b86 100644 --- a/esphome/components/mcp4461/mcp4461.h +++ b/esphome/components/mcp4461/mcp4461.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp4461 { +namespace esphome::mcp4461 { struct WiperState { bool enabled = true; @@ -168,5 +167,4 @@ class Mcp4461Component : public Component, public i2c::I2CDevice { bool wiper_2_disabled_{false}; bool wiper_3_disabled_{false}; }; -} // namespace mcp4461 -} // namespace esphome +} // namespace esphome::mcp4461 diff --git a/esphome/components/mcp4461/output/mcp4461_output.cpp b/esphome/components/mcp4461/output/mcp4461_output.cpp index 2d85a5df61e..6912ad5f36f 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.cpp +++ b/esphome/components/mcp4461/output/mcp4461_output.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp4461 { +namespace esphome::mcp4461 { static const char *const TAG = "mcp4461.output"; @@ -69,5 +68,4 @@ void Mcp4461Wiper::enable_terminal(char terminal) { this->parent_->enable_termin void Mcp4461Wiper::disable_terminal(char terminal) { this->parent_->disable_terminal_(this->wiper_, terminal); } -} // namespace mcp4461 -} // namespace esphome +} // namespace esphome::mcp4461 diff --git a/esphome/components/mcp4461/output/mcp4461_output.h b/esphome/components/mcp4461/output/mcp4461_output.h index 4055cef30a5..73eadceb502 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.h +++ b/esphome/components/mcp4461/output/mcp4461_output.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp4461 { +namespace esphome::mcp4461 { class Mcp4461Wiper : public output::FloatOutput, public Parented<Mcp4461Component> { public: @@ -45,5 +44,4 @@ class Mcp4461Wiper : public output::FloatOutput, public Parented<Mcp4461Componen float state_; }; -} // namespace mcp4461 -} // namespace esphome +} // namespace esphome::mcp4461 diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp index 137ac9cb61d..a32527c7256 100644 --- a/esphome/components/mcp4725/mcp4725.cpp +++ b/esphome/components/mcp4725/mcp4725.cpp @@ -1,8 +1,7 @@ #include "mcp4725.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp4725 { +namespace esphome::mcp4725 { static const char *const TAG = "mcp4725"; @@ -30,5 +29,4 @@ void MCP4725::write_state(float state) { this->write_byte_16(64, value << 4); } -} // namespace mcp4725 -} // namespace esphome +} // namespace esphome::mcp4725 diff --git a/esphome/components/mcp4725/mcp4725.h b/esphome/components/mcp4725/mcp4725.h index d6fa52e323e..1acefc3ee4f 100644 --- a/esphome/components/mcp4725/mcp4725.h +++ b/esphome/components/mcp4725/mcp4725.h @@ -7,8 +7,7 @@ static const uint8_t MCP4725_ADDR = 0x60; static const uint8_t MCP4725_RES = 12; -namespace esphome { -namespace mcp4725 { +namespace esphome::mcp4725 { class MCP4725 : public Component, public output::FloatOutput, public i2c::I2CDevice { public: void setup() override; @@ -19,5 +18,4 @@ class MCP4725 : public Component, public output::FloatOutput, public i2c::I2CDev enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; }; -} // namespace mcp4725 -} // namespace esphome +} // namespace esphome::mcp4725 diff --git a/esphome/components/mcp4728/mcp4728.cpp b/esphome/components/mcp4728/mcp4728.cpp index bab94cb2338..1b15ca05100 100644 --- a/esphome/components/mcp4728/mcp4728.cpp +++ b/esphome/components/mcp4728/mcp4728.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp4728 { +namespace esphome::mcp4728 { static const char *const TAG = "mcp4728"; @@ -109,5 +108,4 @@ void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) this->update_ = true; } -} // namespace mcp4728 -} // namespace esphome +} // namespace esphome::mcp4728 diff --git a/esphome/components/mcp4728/mcp4728.h b/esphome/components/mcp4728/mcp4728.h index d6574080810..13076b3c4cf 100644 --- a/esphome/components/mcp4728/mcp4728.h +++ b/esphome/components/mcp4728/mcp4728.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp4728 { +namespace esphome::mcp4728 { enum class CMD { FAST_WRITE = 0x00, @@ -63,5 +62,4 @@ class MCP4728Component : public Component, public i2c::I2CDevice { bool update_ = false; }; -} // namespace mcp4728 -} // namespace esphome +} // namespace esphome::mcp4728 diff --git a/esphome/components/mcp4728/output/mcp4728_output.cpp b/esphome/components/mcp4728/output/mcp4728_output.cpp index b587e8801b1..7cd5d9d2524 100644 --- a/esphome/components/mcp4728/output/mcp4728_output.cpp +++ b/esphome/components/mcp4728/output/mcp4728_output.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp4728 { +namespace esphome::mcp4728 { void MCP4728Channel::write_state(float state) { const uint16_t max_duty = 4095; @@ -13,5 +12,4 @@ void MCP4728Channel::write_state(float state) { this->parent_->set_channel_value_(this->channel_, duty); } -} // namespace mcp4728 -} // namespace esphome +} // namespace esphome::mcp4728 diff --git a/esphome/components/mcp4728/output/mcp4728_output.h b/esphome/components/mcp4728/output/mcp4728_output.h index 453d632f4c6..3ea65ecc7b0 100644 --- a/esphome/components/mcp4728/output/mcp4728_output.h +++ b/esphome/components/mcp4728/output/mcp4728_output.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp4728 { +namespace esphome::mcp4728 { class MCP4728Channel : public output::FloatOutput { public: @@ -28,5 +27,4 @@ class MCP4728Channel : public output::FloatOutput { MCP4728ChannelIdx channel_; }; -} // namespace mcp4728 -} // namespace esphome +} // namespace esphome::mcp4728 diff --git a/esphome/components/mcp47a1/mcp47a1.cpp b/esphome/components/mcp47a1/mcp47a1.cpp index 58f3b2ac729..e2b0fa5575d 100644 --- a/esphome/components/mcp47a1/mcp47a1.cpp +++ b/esphome/components/mcp47a1/mcp47a1.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp47a1 { +namespace esphome::mcp47a1 { static const char *const TAG = "mcp47a1"; @@ -17,5 +16,4 @@ void MCP47A1::write_state(float state) { this->write_byte(0, value); } -} // namespace mcp47a1 -} // namespace esphome +} // namespace esphome::mcp47a1 diff --git a/esphome/components/mcp47a1/mcp47a1.h b/esphome/components/mcp47a1/mcp47a1.h index 5c02e062ad4..da9794e5aa1 100644 --- a/esphome/components/mcp47a1/mcp47a1.h +++ b/esphome/components/mcp47a1/mcp47a1.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/core/component.h" -namespace esphome { -namespace mcp47a1 { +namespace esphome::mcp47a1 { class MCP47A1 : public Component, public output::FloatOutput, public i2c::I2CDevice { public: @@ -13,5 +12,4 @@ class MCP47A1 : public Component, public output::FloatOutput, public i2c::I2CDev void write_state(float state) override; }; -} // namespace mcp47a1 -} // namespace esphome +} // namespace esphome::mcp47a1 diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index 0c5362b4bad..0e9b472be27 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -1,8 +1,7 @@ #include "mcp9600.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp9600 { +namespace esphome::mcp9600 { static const char *const TAG = "mcp9600"; @@ -109,5 +108,4 @@ void MCP9600Component::update() { this->status_clear_warning(); } -} // namespace mcp9600 -} // namespace esphome +} // namespace esphome::mcp9600 diff --git a/esphome/components/mcp9600/mcp9600.h b/esphome/components/mcp9600/mcp9600.h index c414653ea65..b7c0c834abe 100644 --- a/esphome/components/mcp9600/mcp9600.h +++ b/esphome/components/mcp9600/mcp9600.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp9600 { +namespace esphome::mcp9600 { enum MCP9600ThermocoupleType : uint8_t { MCP9600_THERMOCOUPLE_TYPE_K = 0b000, @@ -45,5 +44,4 @@ class MCP9600Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace mcp9600 -} // namespace esphome +} // namespace esphome::mcp9600 diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index ed12e522395..10e26ed7097 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -1,8 +1,7 @@ #include "mcp9808.h" #include "esphome/core/log.h" -namespace esphome { -namespace mcp9808 { +namespace esphome::mcp9808 { static const uint8_t MCP9808_REG_AMBIENT_TEMP = 0x05; static const uint8_t MCP9808_REG_MANUF_ID = 0x06; @@ -74,5 +73,4 @@ void MCP9808Sensor::update() { this->status_clear_warning(); } -} // namespace mcp9808 -} // namespace esphome +} // namespace esphome::mcp9808 diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h index 894e4599d0a..89530d9ed0f 100644 --- a/esphome/components/mcp9808/mcp9808.h +++ b/esphome/components/mcp9808/mcp9808.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mcp9808 { +namespace esphome::mcp9808 { class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -15,5 +14,4 @@ class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c void update() override; }; -} // namespace mcp9808 -} // namespace esphome +} // namespace esphome::mcp9808 diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 26554e4d3c1..10c81aac399 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -3,8 +3,7 @@ #ifdef USE_MD5 #include "esphome/core/helpers.h" -namespace esphome { -namespace md5 { +namespace esphome::md5 { #if defined(USE_ARDUINO) && !defined(USE_RP2040) && !defined(USE_ESP32) void MD5Digest::init() { @@ -77,6 +76,6 @@ void MD5Digest::calculate() { MD5Digest::~MD5Digest() = default; #endif // USE_HOST -} // namespace md5 -} // namespace esphome +} // namespace esphome::md5 + #endif diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index 80e74d188ef..5e841edd838 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -29,8 +29,7 @@ #define MD5_CTX_TYPE LT_MD5_CTX_T #endif -namespace esphome { -namespace md5 { +namespace esphome::md5 { class MD5Digest final : public HashBase { public: @@ -59,6 +58,6 @@ class MD5Digest final : public HashBase { #endif }; -} // namespace md5 -} // namespace esphome +} // namespace esphome::md5 + #endif diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 14ce3c6aedb..9319335872a 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -3,9 +3,7 @@ #include "esphome/core/automation.h" #include "media_player.h" -namespace esphome { - -namespace media_player { +namespace esphome::media_player { template<MediaPlayerCommand Command, typename... Ts> class MediaPlayerCommandAction : public Action<Ts...>, public Parented<MediaPlayer> { @@ -136,5 +134,4 @@ template<typename... Ts> class IsMutedCondition : public Condition<Ts...>, publi bool check(const Ts &...x) override { return this->parent_->is_muted(); } }; -} // namespace media_player -} // namespace esphome +} // namespace esphome::media_player diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 48d23fa0b12..7dce74117a9 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace media_player { +namespace esphome::media_player { static const char *const TAG = "media_player"; @@ -205,5 +204,4 @@ void MediaPlayer::publish_state() { #endif } -} // namespace media_player -} // namespace esphome +} // namespace esphome::media_player diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index d5d00207970..73de6036923 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace media_player { +namespace esphome::media_player { enum MediaPlayerEntityFeature : uint32_t { PAUSE = 1 << 0, @@ -171,5 +170,4 @@ class MediaPlayer : public EntityBase { LazyCallbackManager<void(MediaPlayerState)> state_callback_{}; }; -} // namespace media_player -} // namespace esphome +} // namespace esphome::media_player diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index 218ce9e4bcc..e3b35583fb1 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -4,8 +4,8 @@ #include "streaming_model.h" #ifdef USE_ESP32 -namespace esphome { -namespace micro_wake_word { + +namespace esphome::micro_wake_word { template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<MicroWakeWord> { public: @@ -49,6 +49,6 @@ template<typename... Ts> class ModelIsEnabledCondition : public Condition<Ts...> WakeWordModel *wake_word_model_; }; -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word + #endif diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index f1aac875f13..1568fc6373b 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -13,8 +13,7 @@ #include "esphome/components/ota/ota_backend.h" #endif -namespace esphome { -namespace micro_wake_word { +namespace esphome::micro_wake_word { static const char *const TAG = "micro_wake_word"; @@ -468,7 +467,6 @@ bool MicroWakeWord::update_model_probabilities_(const int8_t audio_features[PREP return success; } -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word #endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index 44d5d893728..79a1226fba6 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -21,8 +21,7 @@ #include <frontend.h> #include <frontend_util.h> -namespace esphome { -namespace micro_wake_word { +namespace esphome::micro_wake_word { enum State { STARTING, @@ -137,7 +136,6 @@ class MicroWakeWord : public Component bool update_model_probabilities_(const int8_t audio_features[PREPROCESSOR_FEATURE_SIZE]); }; -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word #endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/preprocessor_settings.h b/esphome/components/micro_wake_word/preprocessor_settings.h index c9d195b49b2..fa0bfffbebd 100644 --- a/esphome/components/micro_wake_word/preprocessor_settings.h +++ b/esphome/components/micro_wake_word/preprocessor_settings.h @@ -4,8 +4,7 @@ #include <cstdint> -namespace esphome { -namespace micro_wake_word { +namespace esphome::micro_wake_word { // Settings for controlling the spectrogram feature generation by the preprocessor. // These must match the settings used when training a particular model. @@ -31,7 +30,6 @@ static const uint8_t PCAN_GAIN_CONTROL_GAIN_BITS = 21; static const bool LOG_SCALE_ENABLE_LOG = true; static const uint8_t LOG_SCALE_SCALE_SHIFT = 6; -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word #endif diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index e761e4866fd..1cdc06b3520 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -7,8 +7,7 @@ static const char *const TAG = "micro_wake_word"; -namespace esphome { -namespace micro_wake_word { +namespace esphome::micro_wake_word { void WakeWordModel::log_model_config() { ESP_LOGCONFIG(TAG, @@ -387,7 +386,6 @@ bool StreamingModel::register_streaming_ops_(tflite::MicroMutableOpResolver<20> return true; } -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word #endif diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index fc9eeb5e2d8..07ba78d1f44 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -10,8 +10,7 @@ #include <tensorflow/lite/micro/micro_interpreter.h> #include <tensorflow/lite/micro/micro_mutable_op_resolver.h> -namespace esphome { -namespace micro_wake_word { +namespace esphome::micro_wake_word { static const uint8_t MIN_SLICES_BEFORE_DETECTION = 100; static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024; @@ -155,7 +154,6 @@ class VADModel final : public StreamingModel { DetectionEvent determine_detected() override; }; -} // namespace micro_wake_word -} // namespace esphome +} // namespace esphome::micro_wake_word #endif diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index a6c4bdae663..1dfd91f903a 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace microphone { +namespace esphome::microphone { template<typename... Ts> class CaptureAction : public Action<Ts...>, public Parented<Microphone> { void play(const Ts &...x) override { this->parent_->start(); } @@ -40,5 +39,4 @@ template<typename... Ts> class IsMutedCondition : public Condition<Ts...>, publi bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } }; -} // namespace microphone -} // namespace esphome +} // namespace esphome::microphone diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index 50ce1a72818..d89b9a83629 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -7,8 +7,7 @@ #include <vector> #include "esphome/core/helpers.h" -namespace esphome { -namespace microphone { +namespace esphome::microphone { enum State : uint8_t { STATE_STOPPED = 0, @@ -48,5 +47,4 @@ class Microphone { CallbackManager<void(const std::vector<uint8_t> &)> data_callbacks_{}; }; -} // namespace microphone -} // namespace esphome +} // namespace esphome::microphone diff --git a/esphome/components/microphone/microphone_source.cpp b/esphome/components/microphone/microphone_source.cpp index fb4ebc4a042..288e9f8c8e5 100644 --- a/esphome/components/microphone/microphone_source.cpp +++ b/esphome/components/microphone/microphone_source.cpp @@ -1,7 +1,6 @@ #include "microphone_source.h" -namespace esphome { -namespace microphone { +namespace esphome::microphone { static const int32_t Q25_MAX_VALUE = (1 << 25) - 1; static const int32_t Q25_MIN_VALUE = ~Q25_MAX_VALUE; @@ -73,5 +72,4 @@ void MicrophoneSource::process_audio_(const std::vector<uint8_t> &data, std::vec } } -} // namespace microphone -} // namespace esphome +} // namespace esphome::microphone diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 5c8053e502e..c3c675e854b 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -9,8 +9,7 @@ #include <cstdint> #include <vector> -namespace esphome { -namespace microphone { +namespace esphome::microphone { static const int32_t MAX_GAIN_FACTOR = 64; @@ -89,5 +88,4 @@ class MicrophoneSource { bool passive_; // Only pass audio if ``mic_`` is already running }; -} // namespace microphone -} // namespace esphome +} // namespace esphome::microphone diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index ce63a7d062b..d99d4fd7723 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mics_4514 { +namespace esphome::mics_4514 { static const char *const TAG = "mics_4514"; @@ -131,5 +130,4 @@ void MICS4514Component::update() { } } -} // namespace mics_4514 -} // namespace esphome +} // namespace esphome::mics_4514 diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h index e7271314c82..4f8b970f067 100644 --- a/esphome/components/mics_4514/mics_4514.h +++ b/esphome/components/mics_4514/mics_4514.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mics_4514 { +namespace esphome::mics_4514 { class MICS4514Component : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(carbon_monoxide) @@ -29,5 +28,4 @@ class MICS4514Component : public PollingComponent, public i2c::I2CDevice { float red_calibration_{0}; }; -} // namespace mics_4514 -} // namespace esphome +} // namespace esphome::mics_4514 diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index 8b20a562c8b..ec9dc102970 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -3,9 +3,7 @@ #include "esphome/core/log.h" #include "ac_adapter.h" -namespace esphome { -namespace midea { -namespace ac { +namespace esphome::midea::ac { const char *const Constants::TAG = "midea"; const char *const Constants::FREEZE_PROTECTION = "freeze protection"; @@ -172,8 +170,6 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: // since custom presets are stored on the Climate base class } -} // namespace ac -} // namespace midea -} // namespace esphome +} // namespace esphome::midea::ac #endif // USE_ARDUINO diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index b0589a37f9b..a7924ae51e4 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -8,9 +8,7 @@ #include "esphome/components/climate/climate_traits.h" #include "air_conditioner.h" -namespace esphome { -namespace midea { -namespace ac { +namespace esphome::midea::ac { using MideaMode = dudanov::midea::ac::Mode; using MideaSwingMode = dudanov::midea::ac::SwingMode; @@ -44,8 +42,6 @@ class Converters { static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; -} // namespace ac -} // namespace midea -} // namespace esphome +} // namespace esphome::midea::ac #endif // USE_ARDUINO diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index 760737be87e..acd91919162 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -5,9 +5,7 @@ #include "esphome/core/automation.h" #include "air_conditioner.h" -namespace esphome { -namespace midea { -namespace ac { +namespace esphome::midea::ac { template<typename... Ts> class MideaActionBase : public Action<Ts...> { public: @@ -63,8 +61,6 @@ template<typename... Ts> class PowerToggleAction : public MideaActionBase<Ts...> void play(const Ts &...x) override { this->parent_->do_power_toggle(); } }; -} // namespace ac -} // namespace midea -} // namespace esphome +} // namespace esphome::midea::ac #endif // USE_ARDUINO diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 69e0d46d2d8..594f7fa661b 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -7,9 +7,7 @@ #include <cmath> #include <cstdint> -namespace esphome { -namespace midea { -namespace ac { +namespace esphome::midea::ac { static void set_sensor(Sensor *sensor, float value) { if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) @@ -197,8 +195,6 @@ void AirConditioner::do_display_toggle() { } } -} // namespace ac -} // namespace midea -} // namespace esphome +} // namespace esphome::midea::ac #endif // USE_ARDUINO diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 8dbc71b4226..6ed5a82ff57 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -8,9 +8,7 @@ #include "appliance_base.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace midea { -namespace ac { +namespace esphome::midea::ac { using sensor::Sensor; using climate::ClimateCall; @@ -61,8 +59,6 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, Sensor *power_sensor_{nullptr}; }; -} // namespace ac -} // namespace midea -} // namespace esphome +} // namespace esphome::midea::ac #endif // USE_ARDUINO diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index c7737ba7d69..d36f5a322cd 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -15,8 +15,7 @@ #include "esphome/components/climate/climate.h" #include "ir_transmitter.h" -namespace esphome { -namespace midea { +namespace esphome::midea { /* Stream from UART component */ class UARTStream : public Stream { @@ -98,7 +97,6 @@ template<typename T> class ApplianceBase : public Component { #endif }; -} // namespace midea -} // namespace esphome +} // namespace esphome::midea #endif // USE_ARDUINO diff --git a/esphome/components/midea_ir/midea_data.h b/esphome/components/midea_ir/midea_data.h index 0f7e24907db..1fe65b958f1 100644 --- a/esphome/components/midea_ir/midea_data.h +++ b/esphome/components/midea_ir/midea_data.h @@ -3,8 +3,7 @@ #include "esphome/components/remote_base/midea_protocol.h" #include "esphome/components/climate/climate_mode.h" -namespace esphome { -namespace midea_ir { +namespace esphome::midea_ir { using climate::ClimateMode; using climate::ClimateFanMode; @@ -88,5 +87,4 @@ class SpecialData : public MideaData { static const uint8_t TURBO_TOGGLE = 9; }; -} // namespace midea_ir -} // namespace esphome +} // namespace esphome::midea_ir diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp index 220bb3f414d..de25c4652cb 100644 --- a/esphome/components/midea_ir/midea_ir.cpp +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/components/coolix/coolix.h" -namespace esphome { -namespace midea_ir { +namespace esphome::midea_ir { static const char *const TAG = "midea_ir.climate"; @@ -204,5 +203,4 @@ bool MideaIR::on_midea_(const MideaData &data) { return false; } -} // namespace midea_ir -} // namespace esphome +} // namespace esphome::midea_ir diff --git a/esphome/components/midea_ir/midea_ir.h b/esphome/components/midea_ir/midea_ir.h index b89b2a7efcc..dd883172d4d 100644 --- a/esphome/components/midea_ir/midea_ir.h +++ b/esphome/components/midea_ir/midea_ir.h @@ -3,8 +3,7 @@ #include "esphome/components/climate_ir/climate_ir.h" #include "midea_data.h" -namespace esphome { -namespace midea_ir { +namespace esphome::midea_ir { // Temperature const uint8_t MIDEA_TEMPC_MIN = 17; // Celsius @@ -43,5 +42,4 @@ class MideaIR : public climate_ir::ClimateIR { bool boost_{false}; }; -} // namespace midea_ir -} // namespace esphome +} // namespace esphome::midea_ir diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index f2923458937..5023cf80891 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -7,8 +7,7 @@ #include "esphome/components/display/display_color_utils.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mipi_spi { +namespace esphome::mipi_spi { constexpr static const char *const TAG = "display.mipi_spi"; @@ -672,5 +671,4 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS uint16_t end_line_{1}; }; -} // namespace mipi_spi -} // namespace esphome +} // namespace esphome::mipi_spi diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 882163ff5db..d108ea46215 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mitsubishi { +namespace esphome::mitsubishi { static const char *const TAG = "mitsubishi.climate"; @@ -402,5 +401,4 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace mitsubishi -} // namespace esphome +} // namespace esphome::mitsubishi diff --git a/esphome/components/mitsubishi/mitsubishi.h b/esphome/components/mitsubishi/mitsubishi.h index cfe12428da4..769390ce3a8 100644 --- a/esphome/components/mitsubishi/mitsubishi.h +++ b/esphome/components/mitsubishi/mitsubishi.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace mitsubishi { +namespace esphome::mitsubishi { // Temperature const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius @@ -79,5 +78,4 @@ class MitsubishiClimate : public climate_ir::ClimateIR { climate::ClimateTraits traits() override; }; -} // namespace mitsubishi -} // namespace esphome +} // namespace esphome::mitsubishi diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index 4fa38535833..cdfda0c700f 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mixer_speaker { +namespace esphome::mixer_speaker { template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> { TEMPLATABLE_VALUE(uint8_t, decibel_reduction); TEMPLATABLE_VALUE(uint32_t, duration); @@ -14,7 +13,6 @@ template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...)); } }; -} // namespace mixer_speaker -} // namespace esphome +} // namespace esphome::mixer_speaker #endif diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 01084e50df7..70483021243 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -1,8 +1,7 @@ #include "sensor_mlx90393.h" #include "esphome/core/log.h" -namespace esphome { -namespace mlx90393 { +namespace esphome::mlx90393 { static const char *const TAG = "mlx90393"; @@ -270,5 +269,4 @@ void MLX90393Cls::verify_settings_timeout_(MLX90393Setting stage) { this->set_timeout("verify settings", 3000, [this, next_stage]() { this->verify_settings_timeout_(next_stage); }); } -} // namespace mlx90393 -} // namespace esphome +} // namespace esphome::mlx90393 diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h index 845ae87e09f..28053216e22 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.h +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -7,8 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace mlx90393 { +namespace esphome::mlx90393 { enum MLX90393Setting { MLX90393_GAIN_SEL = 0, @@ -76,5 +75,4 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 void verify_settings_timeout_(MLX90393Setting stage); }; -} // namespace mlx90393 -} // namespace esphome +} // namespace esphome::mlx90393 diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 8a514cbc260..2d3b6631bc6 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mlx90614 { +namespace esphome::mlx90614 { static const uint8_t MLX90614_RAW_IR_1 = 0x04; static const uint8_t MLX90614_RAW_IR_2 = 0x05; @@ -101,5 +100,4 @@ void MLX90614Component::update() { this->status_clear_warning(); } -} // namespace mlx90614 -} // namespace esphome +} // namespace esphome::mlx90614 diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index bf081c3e907..12081f20acc 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace mlx90614 { +namespace esphome::mlx90614 { class MLX90614Component : public PollingComponent, public i2c::I2CDevice { public: @@ -28,5 +27,4 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { float emissivity_{NAN}; }; -} // namespace mlx90614 -} // namespace esphome +} // namespace esphome::mlx90614 diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 51b94eb7671..79c580c6b78 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -1,8 +1,7 @@ #include "mmc5603.h" #include "esphome/core/log.h" -namespace esphome { -namespace mmc5603 { +namespace esphome::mmc5603 { static const char *const TAG = "mmc5603"; static const uint8_t MMC5603_ADDRESS = 0x30; @@ -157,5 +156,4 @@ void MMC5603Component::update() { this->heading_sensor_->publish_state(heading); } -} // namespace mmc5603 -} // namespace esphome +} // namespace esphome::mmc5603 diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index 9a77b78bc19..0d8eb152a72 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mmc5603 { +namespace esphome::mmc5603 { enum MMC5603Datarate { MMC5603_DATARATE_75_0_HZ, @@ -40,5 +39,4 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace mmc5603 -} // namespace esphome +} // namespace esphome::mmc5603 diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp index b038084a72d..a99df36a705 100644 --- a/esphome/components/mmc5983/mmc5983.cpp +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -4,8 +4,7 @@ #include "mmc5983.h" #include "esphome/core/log.h" -namespace esphome { -namespace mmc5983 { +namespace esphome::mmc5983 { static const char *const TAG = "mmc5983"; @@ -133,5 +132,4 @@ void MMC5983Component::dump_config() { LOG_SENSOR(" ", "Z", this->z_sensor_); } -} // namespace mmc5983 -} // namespace esphome +} // namespace esphome::mmc5983 diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h index 3e87e54daa6..020d3b2e4c1 100644 --- a/esphome/components/mmc5983/mmc5983.h +++ b/esphome/components/mmc5983/mmc5983.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mmc5983 { +namespace esphome::mmc5983 { class MMC5983Component : public PollingComponent, public i2c::I2CDevice { public: @@ -23,5 +22,4 @@ class MMC5983Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *z_sensor_{nullptr}; }; -} // namespace mmc5983 -} // namespace esphome +} // namespace esphome::mmc5983 diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 3b1a038be34..679ec34c0f5 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus { +namespace esphome::modbus { static const char *const TAG = "modbus"; @@ -425,5 +424,4 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { } } -} // namespace modbus -} // namespace esphome +} // namespace esphome::modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index c90d4c78ae2..26f64401be0 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -10,8 +10,7 @@ #include <vector> #include <queue> -namespace esphome { -namespace modbus { +namespace esphome::modbus { static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15; @@ -122,5 +121,4 @@ class ModbusDevice { uint8_t address_; }; -} // namespace modbus -} // namespace esphome +} // namespace esphome::modbus diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index c86d548578a..fb8c011259f 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace modbus { +namespace esphome::modbus { /// Modbus definitions from specs: /// https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf @@ -84,5 +83,4 @@ const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D static constexpr uint16_t MAX_FRAME_SIZE = 256; /// End of Modbus definitions -} // namespace modbus -} // namespace esphome +} // namespace esphome::modbus diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp index 1ea3041b4d5..60c19bb66a3 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -1,8 +1,7 @@ #include "modbus_binarysensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.binary_sensor"; @@ -34,5 +33,4 @@ void ModbusBinarySensor::parse_and_publish(const std::vector<uint8_t> &data) { this->publish_state(value); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index 119f4fdd5a2..98c6840e15f 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem { public: @@ -40,5 +39,4 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, optional<transform_func_t> transform_func_{nullopt}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index dabed7136b9..6604276cc20 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller"; @@ -539,5 +538,4 @@ bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { other.register_type == this->register_type && other.function_code == this->function_code; } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 40139f055b4..ba86c2cd166 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -12,8 +12,7 @@ #include <utility> #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusController; @@ -391,5 +390,4 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem return float_value; } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index ed5d91ec5b6..2c81dd6830c 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus.number"; @@ -90,5 +89,4 @@ void ModbusNumber::control(float value) { } void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 169f85ff36c..dd8f418bfc1 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { using value_to_data_t = std::function<float>(float); @@ -46,5 +45,4 @@ class ModbusNumber : public number::Number, public Component, public SensorItem bool use_write_multiple_{false}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index e7f1a39716e..504e09a0931 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.output"; @@ -118,5 +117,4 @@ void ModbusBinaryOutput::dump_config() { this->start_address, this->register_count, static_cast<int>(this->sensor_value_type)); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 3f3cadfe2fb..c5323e3bf30 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { public: @@ -72,5 +71,4 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public bool use_write_multiple_{false}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 2cff7e89ee8..859828f5f68 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -1,8 +1,7 @@ #include "modbus_select.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.select"; @@ -86,5 +85,4 @@ void ModbusSelect::control(size_t index) { this->publish_state(index); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index fde441f2bcf..a736abd0dbc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -7,8 +7,7 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusSelect : public Component, public select::Select, public SensorItem { public: @@ -49,5 +48,4 @@ class ModbusSelect : public Component, public select::Select, public SensorItem optional<write_transform_func_t> write_transform_func_{nullopt}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp index a21fd910326..559724057a6 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -2,8 +2,7 @@ #include "modbus_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.sensor"; @@ -27,5 +26,4 @@ void ModbusSensor::parse_and_publish(const std::vector<uint8_t> &data) { this->publish_state(result); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index ba943c873c6..2e6967b07cd 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusSensor : public Component, public sensor::Sensor, public SensorItem { public: @@ -33,5 +32,4 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem optional<transform_func_t> transform_func_{nullopt}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index dbaff04cc69..044ca2f8cc2 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -2,8 +2,8 @@ #include "modbus_switch.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { + +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.switch"; @@ -112,5 +112,4 @@ void ModbusSwitch::write_state(bool state) { this->publish_state(state); } // ModbusSwitch end -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 301c2bf5489..541a23706d1 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { public: @@ -49,5 +48,4 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem bool assumed_state_{false}; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp index b26411b72e7..56265156385 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -2,8 +2,7 @@ #include "modbus_textsensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { static const char *const TAG = "modbus_controller.text_sensor"; @@ -56,5 +55,4 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) { this->publish_state(output_str); } -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 6666aea9767..a99fea58609 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace modbus_controller { +namespace esphome::modbus_controller { enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 }; @@ -39,5 +38,4 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi RawEncoding encode_; }; -} // namespace modbus_controller -} // namespace esphome +} // namespace esphome::modbus_controller diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index f1708ae70bc..458140ef09c 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace monochromatic { +namespace esphome::monochromatic { class MonochromaticLightOutput : public light::LightOutput { public: @@ -25,5 +24,4 @@ class MonochromaticLightOutput : public light::LightOutput { output::FloatOutput *output_; }; -} // namespace monochromatic -} // namespace esphome +} // namespace esphome::monochromatic diff --git a/esphome/components/mopeka_ble/mopeka_ble.cpp b/esphome/components/mopeka_ble/mopeka_ble.cpp index b926beaff20..ff5dd8d61b9 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.cpp +++ b/esphome/components/mopeka_ble/mopeka_ble.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_ble { +namespace esphome::mopeka_ble { static const char *const TAG = "mopeka_ble"; @@ -86,7 +85,6 @@ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return false; } -} // namespace mopeka_ble -} // namespace esphome +} // namespace esphome::mopeka_ble #endif diff --git a/esphome/components/mopeka_ble/mopeka_ble.h b/esphome/components/mopeka_ble/mopeka_ble.h index b7d0c5a9c54..cc91ef17d67 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.h +++ b/esphome/components/mopeka_ble/mopeka_ble.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_ble { +namespace esphome::mopeka_ble { class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -21,7 +20,6 @@ class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { bool show_sensors_without_sync_; }; -} // namespace mopeka_ble -} // namespace esphome +} // namespace esphome::mopeka_ble #endif diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index 9bc9900a5ac..ab0ff9a113e 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_pro_check { +namespace esphome::mopeka_pro_check { static const char *const TAG = "mopeka_pro_check"; static const uint8_t MANUFACTURER_DATA_LENGTH = 10; @@ -154,7 +153,6 @@ SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector<uint8_t> return static_cast<SensorReadQuality>(message[4] >> 6); } -} // namespace mopeka_pro_check -} // namespace esphome +} // namespace esphome::mopeka_pro_check #endif diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 41fb3121529..bfdfe80c486 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -9,8 +9,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_pro_check { +namespace esphome::mopeka_pro_check { enum SensorType { STANDARD_BOTTOM_UP = 0x03, @@ -65,7 +64,6 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi SensorReadQuality parse_read_quality_(const std::vector<uint8_t> &message); }; -} // namespace mopeka_pro_check -} // namespace esphome +} // namespace esphome::mopeka_pro_check #endif diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index a4a31b82608..519a45fcb59 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_std_check { +namespace esphome::mopeka_std_check { static const char *const TAG = "mopeka_std_check"; static const uint16_t SERVICE_UUID = 0xADA0; @@ -232,7 +231,6 @@ int8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) { } } -} // namespace mopeka_std_check -} // namespace esphome +} // namespace esphome::mopeka_std_check #endif diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index c0a02f27f20..a38abeabf03 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -9,8 +9,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace mopeka_std_check { +namespace esphome::mopeka_std_check { enum SensorType { STANDARD = 0x02, @@ -74,7 +73,6 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi int8_t parse_temperature_(const mopeka_std_package *message); }; -} // namespace mopeka_std_check -} // namespace esphome +} // namespace esphome::mopeka_std_check #endif diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index a689149c892..d7994327b1c 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace mpl3115a2 { +namespace esphome::mpl3115a2 { static const char *const TAG = "mpl3115a2"; @@ -94,5 +93,4 @@ void MPL3115A2Component::update() { this->status_clear_warning(); } -} // namespace mpl3115a2 -} // namespace esphome +} // namespace esphome::mpl3115a2 diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index 05da71f8300..d78c9d571c8 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mpl3115a2 { +namespace esphome::mpl3115a2 { // enums from https://github.com/adafruit/Adafruit_MPL3115A2_Library/ /** MPL3115A2 registers **/ @@ -102,5 +101,4 @@ class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace mpl3115a2 -} // namespace esphome +} // namespace esphome::mpl3115a2 diff --git a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp index dce0e73b9a4..4f500f4e050 100644 --- a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp +++ b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.cpp @@ -1,7 +1,6 @@ #include "mpr121_binary_sensor.h" -namespace esphome { -namespace mpr121 { +namespace esphome::mpr121 { void MPR121BinarySensor::setup() { uint8_t touch_threshold = this->touch_threshold_.value_or(this->parent_->get_touch_threshold()); @@ -16,5 +15,4 @@ void MPR121BinarySensor::process(uint16_t data) { this->publish_state(new_state); } -} // namespace mpr121 -} // namespace esphome +} // namespace esphome::mpr121 diff --git a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h index 577ba828939..5fa10bf5980 100644 --- a/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h +++ b/esphome/components/mpr121/binary_sensor/mpr121_binary_sensor.h @@ -4,8 +4,7 @@ #include "../mpr121.h" -namespace esphome { -namespace mpr121 { +namespace esphome::mpr121 { class MPR121BinarySensor : public binary_sensor::BinarySensor, public MPR121Channel, public Parented<MPR121Component> { public: @@ -22,5 +21,4 @@ class MPR121BinarySensor : public binary_sensor::BinarySensor, public MPR121Chan optional<uint8_t> release_threshold_{}; }; -} // namespace mpr121 -} // namespace esphome +} // namespace esphome::mpr121 diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index cd9c81fe034..6b183233d1e 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace mpr121 { +namespace esphome::mpr121 { static const char *const TAG = "mpr121"; @@ -157,5 +156,4 @@ size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "ELE%u on MPR121", this->pin_); } -} // namespace mpr121 -} // namespace esphome +} // namespace esphome::mpr121 diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 085018fff06..54b5c8abf46 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace mpr121 { +namespace esphome::mpr121 { enum { MPR121_TOUCHSTATUS_L = 0x00, @@ -125,5 +124,4 @@ class MPR121GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace mpr121 -} // namespace esphome +} // namespace esphome::mpr121 diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 91a84d061ad..8784e8caf81 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -1,8 +1,7 @@ #include "mpu6050.h" #include "esphome/core/log.h" -namespace esphome { -namespace mpu6050 { +namespace esphome::mpu6050 { static const char *const TAG = "mpu6050"; @@ -141,5 +140,4 @@ void MPU6050Component::update() { this->status_clear_warning(); } -} // namespace mpu6050 -} // namespace esphome +} // namespace esphome::mpu6050 diff --git a/esphome/components/mpu6050/mpu6050.h b/esphome/components/mpu6050/mpu6050.h index cc7c3620df7..bac07cb4a5e 100644 --- a/esphome/components/mpu6050/mpu6050.h +++ b/esphome/components/mpu6050/mpu6050.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mpu6050 { +namespace esphome::mpu6050 { class MPU6050Component : public PollingComponent, public i2c::I2CDevice { public: @@ -31,7 +30,5 @@ class MPU6050Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *gyro_y_sensor_{nullptr}; sensor::Sensor *gyro_z_sensor_{nullptr}; }; -; -} // namespace mpu6050 -} // namespace esphome +} // namespace esphome::mpu6050 diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 02747da3064..b8cbd4635ac 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -1,8 +1,7 @@ #include "mpu6886.h" #include "esphome/core/log.h" -namespace esphome { -namespace mpu6886 { +namespace esphome::mpu6886 { static const char *const TAG = "mpu6886"; @@ -146,5 +145,4 @@ void MPU6886Component::update() { this->status_clear_warning(); } -} // namespace mpu6886 -} // namespace esphome +} // namespace esphome::mpu6886 diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h index 96e2bf61a16..a23858a7b7c 100644 --- a/esphome/components/mpu6886/mpu6886.h +++ b/esphome/components/mpu6886/mpu6886.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace mpu6886 { +namespace esphome::mpu6886 { class MPU6886Component : public PollingComponent, public i2c::I2CDevice { public: @@ -31,7 +30,5 @@ class MPU6886Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *gyro_y_sensor_{nullptr}; sensor::Sensor *gyro_z_sensor_{nullptr}; }; -; -} // namespace mpu6886 -} // namespace esphome +} // namespace esphome::mpu6886 diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index 273de103765..40b5b46e1d2 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace mqtt_subscribe { +namespace esphome::mqtt_subscribe { static const char *const TAG = "mqtt_subscribe.sensor"; @@ -32,7 +31,6 @@ void MQTTSubscribeSensor::dump_config() { ESP_LOGCONFIG(TAG, " Topic: %s", this->topic_.c_str()); } -} // namespace mqtt_subscribe -} // namespace esphome +} // namespace esphome::mqtt_subscribe #endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h index 0619326ac93..229c0586ab7 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/mqtt/mqtt_client.h" -namespace esphome { -namespace mqtt_subscribe { +namespace esphome::mqtt_subscribe { class MQTTSubscribeSensor : public sensor::Sensor, public Component { public: @@ -27,7 +26,6 @@ class MQTTSubscribeSensor : public sensor::Sensor, public Component { uint8_t qos_{0}; }; -} // namespace mqtt_subscribe -} // namespace esphome +} // namespace esphome::mqtt_subscribe #endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp index 8aa094a2d4c..edc197671e6 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include <utility> -namespace esphome { -namespace mqtt_subscribe { +namespace esphome::mqtt_subscribe { static const char *const TAG = "mqtt_subscribe.text_sensor"; @@ -22,7 +21,6 @@ void MQTTSubscribeTextSensor::dump_config() { ESP_LOGCONFIG(TAG, " Topic: %s", this->topic_.c_str()); } -} // namespace mqtt_subscribe -} // namespace esphome +} // namespace esphome::mqtt_subscribe #endif // USE_MQTT diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h index 9f8e5c63cce..f218bf2a8a3 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/mqtt/mqtt_client.h" -namespace esphome { -namespace mqtt_subscribe { +namespace esphome::mqtt_subscribe { class MQTTSubscribeTextSensor : public text_sensor::TextSensor, public Component { public: @@ -26,7 +25,6 @@ class MQTTSubscribeTextSensor : public text_sensor::TextSensor, public Component uint8_t qos_{}; }; -} // namespace mqtt_subscribe -} // namespace esphome +} // namespace esphome::mqtt_subscribe #endif // USE_MQTT diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index d47ca245b84..9b7dcbe653e 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace ms5611 { +namespace esphome::ms5611 { static const char *const TAG = "ms5611"; @@ -123,5 +122,4 @@ void MS5611Component::calculate_values_(uint32_t raw_temperature, uint32_t raw_p this->status_clear_warning(); } -} // namespace ms5611 -} // namespace esphome +} // namespace esphome::ms5611 diff --git a/esphome/components/ms5611/ms5611.h b/esphome/components/ms5611/ms5611.h index 7e4806f3194..c6ad5b231ae 100644 --- a/esphome/components/ms5611/ms5611.h +++ b/esphome/components/ms5611/ms5611.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ms5611 { +namespace esphome::ms5611 { class MS5611Component : public PollingComponent, public i2c::I2CDevice { public: @@ -26,5 +25,4 @@ class MS5611Component : public PollingComponent, public i2c::I2CDevice { uint16_t prom_[6]; }; -} // namespace ms5611 -} // namespace esphome +} // namespace esphome::ms5611 diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index d141dcb1916..f733a8349d4 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ms8607 { +namespace esphome::ms8607 { /// TAG used for logging calls static const char *const TAG = "ms8607"; @@ -438,5 +437,4 @@ void MS8607Component::calculate_values_(uint32_t d2_raw_temperature, uint32_t d1 } } -} // namespace ms8607 -} // namespace esphome +} // namespace esphome::ms8607 diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 2888b6cdd24..8f9cc9cb883 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ms8607 { +namespace esphome::ms8607 { /** Class for I2CDevice used to communicate with the Humidity sensor @@ -108,5 +107,4 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { uint8_t reset_attempts_remaining_{0}; }; -} // namespace ms8607 -} // namespace esphome +} // namespace esphome::ms8607 diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index 6d6b21e6af2..f23fcfc8eac 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace msa3xx { +namespace esphome::msa3xx { static const char *const TAG = "msa3xx"; @@ -410,5 +409,4 @@ void MSA3xxComponent::process_motions_(RegMotionInterrupt old) { } } -} // namespace msa3xx -} // namespace esphome +} // namespace esphome::msa3xx diff --git a/esphome/components/msa3xx/msa3xx.h b/esphome/components/msa3xx/msa3xx.h index 439d3b5f4dc..345afc50abb 100644 --- a/esphome/components/msa3xx/msa3xx.h +++ b/esphome/components/msa3xx/msa3xx.h @@ -14,8 +14,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #endif -namespace esphome { -namespace msa3xx { +namespace esphome::msa3xx { // Combined register map of MSA301 and MSA311 // Differences @@ -305,5 +304,4 @@ class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice { void process_motions_(RegMotionInterrupt old); }; -} // namespace msa3xx -} // namespace esphome +} // namespace esphome::msa3xx diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index 25f7e6925d3..0072f7196e7 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace my9231 { +namespace esphome::my9231 { static const char *const TAG = "my9231.output"; @@ -123,5 +122,4 @@ void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { } } -} // namespace my9231 -} // namespace esphome +} // namespace esphome::my9231 diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index dff68d247c3..60b113079ef 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace my9231 { +namespace esphome::my9231 { /// MY9231 float output component. class MY9231OutputComponent : public Component { @@ -60,5 +59,4 @@ class MY9231OutputComponent : public Component { bool update_{true}; }; -} // namespace my9231 -} // namespace esphome +} // namespace esphome::my9231 From ab1d2de78e407fad54077606d48d4be86c8b1d6c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 19:28:30 -0400 Subject: [PATCH 1895/2056] [clang-tidy] Concatenate nested namespaces (4/7: components n-r) (#16301) --- esphome/components/nau7802/nau7802.cpp | 6 ++---- esphome/components/nau7802/nau7802.h | 6 ++---- esphome/components/nfc/automation.cpp | 6 ++---- esphome/components/nfc/automation.h | 6 ++---- esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp | 6 ++---- esphome/components/nfc/binary_sensor/nfc_binary_sensor.h | 6 ++---- esphome/components/nfc/nci_core.h | 6 ++---- esphome/components/nfc/nci_message.cpp | 6 ++---- esphome/components/nfc/nci_message.h | 6 ++---- esphome/components/nfc/ndef_message.cpp | 6 ++---- esphome/components/nfc/ndef_message.h | 6 ++---- esphome/components/nfc/ndef_record.cpp | 6 ++---- esphome/components/nfc/ndef_record.h | 6 ++---- esphome/components/nfc/ndef_record_text.cpp | 6 ++---- esphome/components/nfc/ndef_record_text.h | 6 ++---- esphome/components/nfc/ndef_record_uri.cpp | 6 ++---- esphome/components/nfc/ndef_record_uri.h | 6 ++---- esphome/components/nfc/nfc.cpp | 6 ++---- esphome/components/nfc/nfc.h | 6 ++---- esphome/components/nfc/nfc_helpers.cpp | 6 ++---- esphome/components/nfc/nfc_helpers.h | 6 ++---- esphome/components/nfc/nfc_tag.cpp | 6 ++---- esphome/components/nfc/nfc_tag.h | 6 ++---- esphome/components/noblex/noblex.cpp | 6 ++---- esphome/components/noblex/noblex.h | 6 ++---- esphome/components/npi19/npi19.cpp | 6 ++---- esphome/components/npi19/npi19.h | 6 ++---- esphome/components/ntc/ntc.cpp | 6 ++---- esphome/components/ntc/ntc.h | 6 ++---- esphome/components/one_wire/one_wire.cpp | 6 ++---- esphome/components/one_wire/one_wire.h | 6 ++---- esphome/components/one_wire/one_wire_bus.cpp | 6 ++---- esphome/components/one_wire/one_wire_bus.h | 6 ++---- esphome/components/opentherm/automation.h | 6 ++---- esphome/components/opentherm/hub.cpp | 6 ++---- esphome/components/opentherm/hub.h | 6 ++---- esphome/components/opentherm/input.h | 6 ++---- esphome/components/opentherm/number/opentherm_number.cpp | 6 ++---- esphome/components/opentherm/number/opentherm_number.h | 6 ++---- esphome/components/opentherm/opentherm.cpp | 6 ++---- esphome/components/opentherm/opentherm.h | 6 ++---- esphome/components/opentherm/opentherm_macros.h | 7 +++---- esphome/components/opentherm/output/opentherm_output.cpp | 6 ++---- esphome/components/opentherm/output/opentherm_output.h | 6 ++---- esphome/components/opentherm/switch/opentherm_switch.cpp | 6 ++---- esphome/components/opentherm/switch/opentherm_switch.h | 6 ++---- esphome/components/opt3001/opt3001.cpp | 6 ++---- esphome/components/opt3001/opt3001.h | 6 ++---- esphome/components/output/automation.cpp | 6 ++---- esphome/components/output/automation.h | 6 ++---- esphome/components/output/binary_output.h | 6 ++---- esphome/components/output/button/output_button.cpp | 6 ++---- esphome/components/output/button/output_button.h | 6 ++---- esphome/components/output/float_output.cpp | 6 ++---- esphome/components/output/float_output.h | 6 ++---- esphome/components/output/lock/output_lock.cpp | 6 ++---- esphome/components/output/lock/output_lock.h | 6 ++---- esphome/components/output/switch/output_switch.cpp | 6 ++---- esphome/components/output/switch/output_switch.h | 6 ++---- esphome/components/packet_transport/packet_transport.cpp | 6 ++---- esphome/components/packet_transport/packet_transport.h | 6 ++---- esphome/components/partition/light_partition.cpp | 6 ++---- esphome/components/partition/light_partition.h | 6 ++---- esphome/components/pca6416a/pca6416a.cpp | 6 ++---- esphome/components/pca6416a/pca6416a.h | 6 ++---- esphome/components/pca9554/pca9554.cpp | 6 ++---- esphome/components/pca9554/pca9554.h | 6 ++---- esphome/components/pca9685/pca9685_output.cpp | 6 ++---- esphome/components/pca9685/pca9685_output.h | 6 ++---- esphome/components/pcd8544/pcd_8544.cpp | 6 ++---- esphome/components/pcd8544/pcd_8544.h | 6 ++---- esphome/components/pcf85063/pcf85063.cpp | 6 ++---- esphome/components/pcf85063/pcf85063.h | 6 ++---- esphome/components/pcf8563/pcf8563.cpp | 6 ++---- esphome/components/pcf8563/pcf8563.h | 6 ++---- esphome/components/pcf8574/pcf8574.cpp | 6 ++---- esphome/components/pcf8574/pcf8574.h | 6 ++---- esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 6 ++---- esphome/components/pi4ioe5v6408/pi4ioe5v6408.h | 6 ++---- esphome/components/pid/pid_autotuner.cpp | 6 ++---- esphome/components/pid/pid_autotuner.h | 6 ++---- esphome/components/pid/pid_climate.cpp | 6 ++---- esphome/components/pid/pid_climate.h | 6 ++---- esphome/components/pid/pid_controller.cpp | 6 ++---- esphome/components/pid/pid_controller.h | 6 ++---- esphome/components/pid/sensor/pid_climate_sensor.cpp | 6 ++---- esphome/components/pid/sensor/pid_climate_sensor.h | 6 ++---- esphome/components/pipsolar/output/pipsolar_output.cpp | 6 ++---- esphome/components/pipsolar/output/pipsolar_output.h | 6 ++---- esphome/components/pipsolar/pipsolar.cpp | 6 ++---- esphome/components/pipsolar/pipsolar.h | 6 ++---- esphome/components/pipsolar/switch/pipsolar_switch.cpp | 6 ++---- esphome/components/pipsolar/switch/pipsolar_switch.h | 6 ++---- esphome/components/pm1006/pm1006.cpp | 6 ++---- esphome/components/pm1006/pm1006.h | 6 ++---- esphome/components/pm2005/pm2005.cpp | 6 ++---- esphome/components/pm2005/pm2005.h | 6 ++---- esphome/components/pmsa003i/pmsa003i.cpp | 6 ++---- esphome/components/pmsa003i/pmsa003i.h | 6 ++---- esphome/components/pmwcs3/pmwcs3.cpp | 6 ++---- esphome/components/pmwcs3/pmwcs3.h | 6 ++---- esphome/components/pn532/pn532.cpp | 6 ++---- esphome/components/pn532/pn532.h | 6 ++---- esphome/components/pn532/pn532_mifare_classic.cpp | 6 ++---- esphome/components/pn532/pn532_mifare_ultralight.cpp | 6 ++---- esphome/components/pn532_i2c/pn532_i2c.cpp | 6 ++---- esphome/components/pn532_i2c/pn532_i2c.h | 6 ++---- esphome/components/pn532_spi/pn532_spi.cpp | 6 ++---- esphome/components/pn532_spi/pn532_spi.h | 6 ++---- esphome/components/pn7150/automation.h | 6 ++---- esphome/components/pn7150/pn7150.cpp | 6 ++---- esphome/components/pn7150/pn7150.h | 6 ++---- esphome/components/pn7150/pn7150_mifare_classic.cpp | 6 ++---- esphome/components/pn7150/pn7150_mifare_ultralight.cpp | 6 ++---- esphome/components/pn7150_i2c/pn7150_i2c.cpp | 6 ++---- esphome/components/pn7150_i2c/pn7150_i2c.h | 6 ++---- esphome/components/pn7160/automation.h | 6 ++---- esphome/components/pn7160/pn7160.cpp | 6 ++---- esphome/components/pn7160/pn7160.h | 6 ++---- esphome/components/pn7160/pn7160_mifare_classic.cpp | 6 ++---- esphome/components/pn7160/pn7160_mifare_ultralight.cpp | 6 ++---- esphome/components/pn7160_i2c/pn7160_i2c.cpp | 6 ++---- esphome/components/pn7160_i2c/pn7160_i2c.h | 6 ++---- esphome/components/pn7160_spi/pn7160_spi.cpp | 6 ++---- esphome/components/pn7160_spi/pn7160_spi.h | 6 ++---- esphome/components/power_supply/power_supply.cpp | 6 ++---- esphome/components/power_supply/power_supply.h | 6 ++---- esphome/components/preferences/syncer.h | 6 ++---- esphome/components/prometheus/prometheus_handler.cpp | 7 +++---- esphome/components/prometheus/prometheus_handler.h | 7 +++---- esphome/components/psram/psram.cpp | 6 ++---- esphome/components/psram/psram.h | 6 ++---- esphome/components/pulse_counter/automation.h | 7 ++----- esphome/components/pulse_counter/pulse_counter_sensor.cpp | 6 ++---- esphome/components/pulse_counter/pulse_counter_sensor.h | 6 ++---- esphome/components/pulse_meter/automation.h | 7 ++----- esphome/components/pulse_meter/pulse_meter_sensor.cpp | 6 ++---- esphome/components/pulse_meter/pulse_meter_sensor.h | 6 ++---- esphome/components/pulse_width/pulse_width.cpp | 6 ++---- esphome/components/pulse_width/pulse_width.h | 6 ++---- .../components/pvvx_mithermometer/display/pvvx_display.cpp | 7 +++---- .../components/pvvx_mithermometer/display/pvvx_display.h | 6 ++---- .../components/pvvx_mithermometer/pvvx_mithermometer.cpp | 6 ++---- esphome/components/pvvx_mithermometer/pvvx_mithermometer.h | 6 ++---- esphome/components/pylontech/pylontech.cpp | 6 ++---- esphome/components/pylontech/pylontech.h | 6 ++---- esphome/components/pylontech/sensor/pylontech_sensor.cpp | 6 ++---- esphome/components/pylontech/sensor/pylontech_sensor.h | 6 ++---- .../pylontech/text_sensor/pylontech_text_sensor.cpp | 6 ++---- .../pylontech/text_sensor/pylontech_text_sensor.h | 6 ++---- esphome/components/pzem004t/pzem004t.cpp | 6 ++---- esphome/components/pzem004t/pzem004t.h | 6 ++---- esphome/components/pzemac/pzemac.cpp | 6 ++---- esphome/components/pzemac/pzemac.h | 6 ++---- esphome/components/pzemdc/pzemdc.cpp | 6 ++---- esphome/components/pzemdc/pzemdc.h | 6 ++---- esphome/components/qmc5883l/qmc5883l.cpp | 6 ++---- esphome/components/qmc5883l/qmc5883l.h | 6 ++---- esphome/components/qmp6988/qmp6988.cpp | 6 ++---- esphome/components/qmp6988/qmp6988.h | 6 ++---- esphome/components/qr_code/qr_code.cpp | 6 ++---- esphome/components/qwiic_pir/qwiic_pir.cpp | 6 ++---- esphome/components/qwiic_pir/qwiic_pir.h | 6 ++---- esphome/components/radon_eye_ble/radon_eye_listener.cpp | 6 ++---- esphome/components/radon_eye_ble/radon_eye_listener.h | 6 ++---- esphome/components/radon_eye_rd200/radon_eye_rd200.cpp | 6 ++---- esphome/components/radon_eye_rd200/radon_eye_rd200.h | 6 ++---- esphome/components/rc522/rc522.cpp | 6 ++---- esphome/components/rc522/rc522.h | 6 ++---- esphome/components/rc522_i2c/rc522_i2c.cpp | 6 ++---- esphome/components/rc522_i2c/rc522_i2c.h | 6 ++---- esphome/components/rc522_spi/rc522_spi.cpp | 6 ++---- esphome/components/rc522_spi/rc522_spi.h | 6 ++---- esphome/components/rdm6300/rdm6300.cpp | 6 ++---- esphome/components/rdm6300/rdm6300.h | 6 ++---- esphome/components/remote_base/abbwelcome_protocol.cpp | 6 ++---- esphome/components/remote_base/abbwelcome_protocol.h | 6 ++---- esphome/components/remote_base/aeha_protocol.cpp | 6 ++---- esphome/components/remote_base/aeha_protocol.h | 6 ++---- esphome/components/remote_base/beo4_protocol.cpp | 6 ++---- esphome/components/remote_base/beo4_protocol.h | 6 ++---- esphome/components/remote_base/byronsx_protocol.cpp | 6 ++---- esphome/components/remote_base/byronsx_protocol.h | 6 ++---- esphome/components/remote_base/canalsat_protocol.cpp | 6 ++---- esphome/components/remote_base/canalsat_protocol.h | 6 ++---- esphome/components/remote_base/coolix_protocol.cpp | 6 ++---- esphome/components/remote_base/coolix_protocol.h | 6 ++---- esphome/components/remote_base/dish_protocol.cpp | 6 ++---- esphome/components/remote_base/dish_protocol.h | 6 ++---- esphome/components/remote_base/dooya_protocol.cpp | 6 ++---- esphome/components/remote_base/dooya_protocol.h | 6 ++---- esphome/components/remote_base/drayton_protocol.cpp | 6 ++---- esphome/components/remote_base/drayton_protocol.h | 6 ++---- esphome/components/remote_base/dyson_protocol.cpp | 6 ++---- esphome/components/remote_base/dyson_protocol.h | 6 ++---- esphome/components/remote_base/gobox_protocol.cpp | 6 ++---- esphome/components/remote_base/gobox_protocol.h | 6 ++---- esphome/components/remote_base/haier_protocol.cpp | 6 ++---- esphome/components/remote_base/haier_protocol.h | 6 ++---- esphome/components/remote_base/jvc_protocol.cpp | 6 ++---- esphome/components/remote_base/jvc_protocol.h | 6 ++---- esphome/components/remote_base/keeloq_protocol.cpp | 6 ++---- esphome/components/remote_base/keeloq_protocol.h | 6 ++---- esphome/components/remote_base/lg_protocol.cpp | 6 ++---- esphome/components/remote_base/lg_protocol.h | 6 ++---- esphome/components/remote_base/magiquest_protocol.cpp | 6 ++---- esphome/components/remote_base/magiquest_protocol.h | 6 ++---- esphome/components/remote_base/midea_protocol.cpp | 6 ++---- esphome/components/remote_base/midea_protocol.h | 6 ++---- esphome/components/remote_base/mirage_protocol.cpp | 6 ++---- esphome/components/remote_base/mirage_protocol.h | 6 ++---- esphome/components/remote_base/nec_protocol.cpp | 6 ++---- esphome/components/remote_base/nec_protocol.h | 6 ++---- esphome/components/remote_base/nexa_protocol.cpp | 6 ++---- esphome/components/remote_base/nexa_protocol.h | 6 ++---- esphome/components/remote_base/panasonic_protocol.cpp | 6 ++---- esphome/components/remote_base/panasonic_protocol.h | 6 ++---- esphome/components/remote_base/pioneer_protocol.cpp | 6 ++---- esphome/components/remote_base/pioneer_protocol.h | 6 ++---- esphome/components/remote_base/pronto_protocol.cpp | 6 ++---- esphome/components/remote_base/pronto_protocol.h | 6 ++---- esphome/components/remote_base/raw_protocol.cpp | 6 ++---- esphome/components/remote_base/raw_protocol.h | 6 ++---- esphome/components/remote_base/rc5_protocol.cpp | 6 ++---- esphome/components/remote_base/rc5_protocol.h | 6 ++---- esphome/components/remote_base/rc6_protocol.cpp | 6 ++---- esphome/components/remote_base/rc6_protocol.h | 6 ++---- esphome/components/remote_base/rc_switch_protocol.cpp | 6 ++---- esphome/components/remote_base/rc_switch_protocol.h | 6 ++---- esphome/components/remote_base/remote_base.cpp | 6 ++---- esphome/components/remote_base/remote_base.h | 6 ++---- esphome/components/remote_base/roomba_protocol.cpp | 6 ++---- esphome/components/remote_base/roomba_protocol.h | 6 ++---- esphome/components/remote_base/samsung36_protocol.cpp | 6 ++---- esphome/components/remote_base/samsung36_protocol.h | 6 ++---- esphome/components/remote_base/samsung_protocol.cpp | 6 ++---- esphome/components/remote_base/samsung_protocol.h | 6 ++---- esphome/components/remote_base/sony_protocol.cpp | 6 ++---- esphome/components/remote_base/sony_protocol.h | 6 ++---- esphome/components/remote_base/symphony_protocol.cpp | 6 ++---- esphome/components/remote_base/symphony_protocol.h | 6 ++---- esphome/components/remote_base/toshiba_ac_protocol.cpp | 6 ++---- esphome/components/remote_base/toshiba_ac_protocol.h | 6 ++---- esphome/components/remote_base/toto_protocol.cpp | 6 ++---- esphome/components/remote_base/toto_protocol.h | 6 ++---- esphome/components/resampler/speaker/resampler_speaker.cpp | 6 ++---- esphome/components/resampler/speaker/resampler_speaker.h | 6 ++---- esphome/components/resistance/resistance_sensor.cpp | 6 ++---- esphome/components/resistance/resistance_sensor.h | 6 ++---- esphome/components/restart/button/restart_button.cpp | 6 ++---- esphome/components/restart/button/restart_button.h | 6 ++---- esphome/components/restart/switch/restart_switch.cpp | 6 ++---- esphome/components/restart/switch/restart_switch.h | 6 ++---- esphome/components/rf_bridge/rf_bridge.cpp | 6 ++---- esphome/components/rf_bridge/rf_bridge.h | 6 ++---- esphome/components/rgb/rgb_light_output.h | 6 ++---- esphome/components/rgbct/rgbct_light_output.h | 6 ++---- esphome/components/rgbw/rgbw_light_output.h | 6 ++---- esphome/components/rgbww/rgbww_light_output.h | 6 ++---- esphome/components/rotary_encoder/rotary_encoder.cpp | 6 ++---- esphome/components/rotary_encoder/rotary_encoder.h | 6 ++---- esphome/components/ruuvi_ble/ruuvi_ble.cpp | 6 ++---- esphome/components/ruuvi_ble/ruuvi_ble.h | 6 ++---- esphome/components/ruuvitag/ruuvitag.cpp | 6 ++---- esphome/components/ruuvitag/ruuvitag.h | 6 ++---- esphome/components/rx8130/rx8130.cpp | 6 ++---- esphome/components/rx8130/rx8130.h | 6 ++---- 267 files changed, 538 insertions(+), 1070 deletions(-) diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 66d36dd7416..4d73ed6dd0f 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace nau7802 { +namespace esphome::nau7802 { static const char *const TAG = "nau7802"; @@ -313,5 +312,4 @@ void NAU7802Sensor::update() { bool NAU7802Sensor::is_data_ready_() { return this->reg(PU_CTRL_REG).get() & PU_CTRL_CYCLE_READY; } -} // namespace nau7802 -} // namespace esphome +} // namespace esphome::nau7802 diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index ae39e167a4d..67f36ca6772 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -7,8 +7,7 @@ #include <cinttypes> -namespace esphome { -namespace nau7802 { +namespace esphome::nau7802 { enum NAU7802Gain { NAU7802_GAIN_128 = 0b111, @@ -114,5 +113,4 @@ template<typename... Ts> class NAU7802CalbrateGainAction : public Action<Ts...>, void play(const Ts &...x) override { this->parent_->calibrate_gain(); } }; -} // namespace nau7802 -} // namespace esphome +} // namespace esphome::nau7802 diff --git a/esphome/components/nfc/automation.cpp b/esphome/components/nfc/automation.cpp index e2956e4c123..7129aaf2af9 100644 --- a/esphome/components/nfc/automation.cpp +++ b/esphome/components/nfc/automation.cpp @@ -1,13 +1,11 @@ #include "automation.h" #include "nfc.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { void NfcOnTagTrigger::process(const std::unique_ptr<NfcTag> &tag) { char uid_buf[FORMAT_UID_BUFFER_SIZE]; this->trigger(std::string(format_uid_to(uid_buf, tag->get_uid())), *tag); } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/automation.h b/esphome/components/nfc/automation.h index 565b71bdd97..0ac3e3b8b67 100644 --- a/esphome/components/nfc/automation.h +++ b/esphome/components/nfc/automation.h @@ -5,13 +5,11 @@ #include "nfc.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { class NfcOnTagTrigger : public Trigger<std::string, NfcTag> { public: void process(const std::unique_ptr<NfcTag> &tag); }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp index 524ad5a413e..6e8162fc912 100644 --- a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp @@ -3,8 +3,7 @@ #include "../nfc_helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.binary_sensor"; @@ -112,5 +111,4 @@ void NfcTagBinarySensor::tag_on(NfcTag &tag) { } } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h index 0a7ca0ca76a..b3448a57ccd 100644 --- a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { class NfcTagBinarySensor : public binary_sensor::BinarySensor, public Component, @@ -34,5 +33,4 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor, NfcTagUid uid_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h index 6b42070ed02..dd4bc547f61 100644 --- a/esphome/components/nfc/nci_core.h +++ b/esphome/components/nfc/nci_core.h @@ -4,8 +4,7 @@ #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { // Header info static constexpr uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes @@ -140,5 +139,4 @@ static constexpr uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SI static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; static constexpr uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nci_message.cpp b/esphome/components/nfc/nci_message.cpp index c6b21f6ae07..0b60fd0aded 100644 --- a/esphome/components/nfc/nci_message.cpp +++ b/esphome/components/nfc/nci_message.cpp @@ -4,8 +4,7 @@ #include <cstdio> -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "NciMessage"; @@ -162,5 +161,4 @@ void NciMessage::set_payload(const std::vector<uint8_t> &payload) { this->nci_message_ = message; } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nci_message.h b/esphome/components/nfc/nci_message.h index 0c5c871f740..8e8b1103363 100644 --- a/esphome/components/nfc/nci_message.h +++ b/esphome/components/nfc/nci_message.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { class NciMessage { public: @@ -46,5 +45,4 @@ class NciMessage { std::vector<uint8_t> nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index 35028555c55..ba3aa77e340 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -1,8 +1,7 @@ #include "ndef_message.h" #include <cinttypes> -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.ndef_message"; @@ -120,5 +119,4 @@ std::vector<uint8_t> NdefMessage::encode() { return data; } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h index 48f79b88548..7d431b22963 100644 --- a/esphome/components/nfc/ndef_message.h +++ b/esphome/components/nfc/ndef_message.h @@ -9,8 +9,7 @@ #include "ndef_record_text.h" #include "ndef_record_uri.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static constexpr uint8_t MAX_NDEF_RECORDS = 4; @@ -38,5 +37,4 @@ class NdefMessage { std::vector<std::shared_ptr<NdefRecord>> records_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp index 540ba629408..8b9e7023ed6 100644 --- a/esphome/components/nfc/ndef_record.cpp +++ b/esphome/components/nfc/ndef_record.cpp @@ -1,7 +1,6 @@ #include "ndef_record.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.ndef_record"; @@ -61,5 +60,4 @@ uint8_t NdefRecord::create_flag_byte(bool first, bool last, size_t payload_size) return value; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h index 1a7c24aee9c..fc9fe254024 100644 --- a/esphome/components/nfc/ndef_record.h +++ b/esphome/components/nfc/ndef_record.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { static constexpr uint8_t TNF_EMPTY = 0x00; static constexpr uint8_t TNF_WELL_KNOWN = 0x01; @@ -53,5 +52,4 @@ class NdefRecord { std::string payload_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp index 8a9a2cb014f..8ad687daf8c 100644 --- a/esphome/components/nfc/ndef_record_text.cpp +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -1,8 +1,7 @@ #include "ndef_record_text.h" #include "ndef_record.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.ndef_record_text"; @@ -41,5 +40,4 @@ std::vector<uint8_t> NdefRecordText::get_encoded_payload() { return data; } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record_text.h b/esphome/components/nfc/ndef_record_text.h index e6c15704f0f..ceee585e892 100644 --- a/esphome/components/nfc/ndef_record_text.h +++ b/esphome/components/nfc/ndef_record_text.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { class NdefRecordText : public NdefRecord { public: @@ -39,5 +38,4 @@ class NdefRecordText : public NdefRecord { std::string language_code_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record_uri.cpp b/esphome/components/nfc/ndef_record_uri.cpp index 9064f04f296..e2c6d21a34e 100644 --- a/esphome/components/nfc/ndef_record_uri.cpp +++ b/esphome/components/nfc/ndef_record_uri.cpp @@ -1,7 +1,6 @@ #include "ndef_record_uri.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.ndef_record_uri"; @@ -44,5 +43,4 @@ std::vector<uint8_t> NdefRecordUri::get_encoded_payload() { return data; } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/ndef_record_uri.h b/esphome/components/nfc/ndef_record_uri.h index 2f7790a9a9a..619cdf7cf38 100644 --- a/esphome/components/nfc/ndef_record_uri.h +++ b/esphome/components/nfc/ndef_record_uri.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { static constexpr uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; static const char *const PAYLOAD_IDENTIFIERS[] = {"", @@ -74,5 +73,4 @@ class NdefRecordUri : public NdefRecord { std::string uri_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 55543cd292a..99e476dbdfd 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc"; @@ -108,5 +107,4 @@ bool mifare_classic_is_trailer_block(uint8_t block_num) { } } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 8ca5cb7ea44..42ef9939136 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -9,8 +9,7 @@ #include <span> #include <vector> -namespace esphome { -namespace nfc { +namespace esphome::nfc { static constexpr uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; static constexpr uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; @@ -95,5 +94,4 @@ class Nfcc { std::vector<NfcTagListener *> tag_listeners_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp index fb0954a8333..6c8a5b626d5 100644 --- a/esphome/components/nfc/nfc_helpers.cpp +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -1,7 +1,6 @@ #include "nfc_helpers.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.helpers"; @@ -43,5 +42,4 @@ std::string get_random_ha_tag_ndef() { return uri; } -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc_helpers.h b/esphome/components/nfc/nfc_helpers.h index 74f5beba134..dedc602bf19 100644 --- a/esphome/components/nfc/nfc_helpers.h +++ b/esphome/components/nfc/nfc_helpers.h @@ -2,8 +2,7 @@ #include "nfc_tag.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char HA_TAG_ID_EXT_RECORD_TYPE[] = "android.com:pkg"; static const char HA_TAG_ID_EXT_RECORD_PAYLOAD[] = "io.homeassistant.companion.android"; @@ -13,5 +12,4 @@ std::string get_ha_tag_ndef(NfcTag &tag); std::string get_random_ha_tag_ndef(); bool has_ha_tag_ndef(NfcTag &tag); -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc_tag.cpp b/esphome/components/nfc/nfc_tag.cpp index c5c15b00ec8..c43210517dc 100644 --- a/esphome/components/nfc/nfc_tag.cpp +++ b/esphome/components/nfc/nfc_tag.cpp @@ -1,9 +1,7 @@ #include "nfc_tag.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { static const char *const TAG = "nfc.tag"; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/nfc/nfc_tag.h b/esphome/components/nfc/nfc_tag.h index 0ded4cd6ee2..6cc1a00c627 100644 --- a/esphome/components/nfc/nfc_tag.h +++ b/esphome/components/nfc/nfc_tag.h @@ -7,8 +7,7 @@ #include "esphome/core/log.h" #include "ndef_message.h" -namespace esphome { -namespace nfc { +namespace esphome::nfc { // NFC UIDs are 4, 7, or 10 bytes depending on tag type static constexpr size_t NFC_UID_MAX_LENGTH = 10; @@ -54,5 +53,4 @@ class NfcTag { std::shared_ptr<NdefMessage> ndef_message_; }; -} // namespace nfc -} // namespace esphome +} // namespace esphome::nfc diff --git a/esphome/components/noblex/noblex.cpp b/esphome/components/noblex/noblex.cpp index e7e421d1776..ac8d2641fea 100644 --- a/esphome/components/noblex/noblex.cpp +++ b/esphome/components/noblex/noblex.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace noblex { +namespace esphome::noblex { static const char *const TAG = "noblex.climate"; @@ -299,5 +298,4 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) { return true; } // end on_receive() -} // namespace noblex -} // namespace esphome +} // namespace esphome::noblex diff --git a/esphome/components/noblex/noblex.h b/esphome/components/noblex/noblex.h index 3d52a1a5389..62070e5deed 100644 --- a/esphome/components/noblex/noblex.h +++ b/esphome/components/noblex/noblex.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace noblex { +namespace esphome::noblex { // Temperature const uint8_t NOBLEX_TEMP_MIN = 16; // Celsius @@ -45,5 +44,4 @@ class NoblexClimate : public climate_ir::ClimateIR { uint8_t remote_state_[8]{}; }; -} // namespace noblex -} // namespace esphome +} // namespace esphome::noblex diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index 995abdff376..207acc9d991 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace npi19 { +namespace esphome::npi19 { static const char *const TAG = "npi19"; @@ -101,5 +100,4 @@ void NPI19Component::update() { this->status_clear_warning(); } -} // namespace npi19 -} // namespace esphome +} // namespace esphome::npi19 diff --git a/esphome/components/npi19/npi19.h b/esphome/components/npi19/npi19.h index 8e6a8e3bfa5..d1f74141ac7 100644 --- a/esphome/components/npi19/npi19.h +++ b/esphome/components/npi19/npi19.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace npi19 { +namespace esphome::npi19 { /// This class implements support for the npi19 pressure and temperature i2c sensors. class NPI19Component : public PollingComponent, public i2c::I2CDevice { @@ -25,5 +24,4 @@ class NPI19Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *raw_pressure_sensor_{nullptr}; }; -} // namespace npi19 -} // namespace esphome +} // namespace esphome::npi19 diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index e2097bdd770..49a42e858e5 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -1,8 +1,7 @@ #include "ntc.h" #include "esphome/core/log.h" -namespace esphome { -namespace ntc { +namespace esphome::ntc { static const char *const TAG = "ntc"; @@ -26,5 +25,4 @@ void NTC::process_(float value) { this->publish_state(temp); } -} // namespace ntc -} // namespace esphome +} // namespace esphome::ntc diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index a0c72340dee..466d03f789f 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ntc { +namespace esphome::ntc { class NTC : public Component, public sensor::Sensor { public: @@ -24,5 +23,4 @@ class NTC : public Component, public sensor::Sensor { double c_; }; -} // namespace ntc -} // namespace esphome +} // namespace esphome::ntc diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index d14c1c92bdd..aeab821f6ad 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -1,7 +1,6 @@ #include "one_wire.h" -namespace esphome { -namespace one_wire { +namespace esphome::one_wire { static const char *const TAG = "one_wire"; @@ -51,5 +50,4 @@ bool OneWireDevice::check_address_or_index_() { return true; } -} // namespace one_wire -} // namespace esphome +} // namespace esphome::one_wire diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index 324e46cd55d..4dbbe117927 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace one_wire { +namespace esphome::one_wire { #define LOG_ONE_WIRE_DEVICE(this) \ ESP_LOGCONFIG(TAG, " Address: %s (%s)", this->get_address_name().c_str(), \ @@ -43,5 +42,4 @@ class OneWireDevice { bool send_command_(uint8_t cmd); }; -} // namespace one_wire -} // namespace esphome +} // namespace esphome::one_wire diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index 99e1f352fb5..c7ea59050c4 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -1,8 +1,7 @@ #include "one_wire_bus.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace one_wire { +namespace esphome::one_wire { static const char *const TAG = "one_wire"; @@ -93,5 +92,4 @@ void OneWireBus::dump_devices_(const char *tag) { } } -} // namespace one_wire -} // namespace esphome +} // namespace esphome::one_wire diff --git a/esphome/components/one_wire/one_wire_bus.h b/esphome/components/one_wire/one_wire_bus.h index 6302fcee7b9..00f5a6462ee 100644 --- a/esphome/components/one_wire/one_wire_bus.h +++ b/esphome/components/one_wire/one_wire_bus.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <vector> -namespace esphome { -namespace one_wire { +namespace esphome::one_wire { class OneWireBus { public: @@ -64,5 +63,4 @@ class OneWireBus { virtual uint64_t search_int() = 0; }; -} // namespace one_wire -} // namespace esphome +} // namespace esphome::one_wire diff --git a/esphome/components/opentherm/automation.h b/esphome/components/opentherm/automation.h index acbe33ac8f9..aa20a4ec5a4 100644 --- a/esphome/components/opentherm/automation.h +++ b/esphome/components/opentherm/automation.h @@ -4,8 +4,7 @@ #include "hub.h" #include "opentherm.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { class BeforeSendTrigger : public Trigger<OpenthermData &> { public: @@ -21,5 +20,4 @@ class BeforeProcessResponseTrigger : public Trigger<OpenthermData &> { } }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index 7a0cdc7f801..e2828a9e305 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -3,8 +3,7 @@ #include <string> -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { static const char *const TAG = "opentherm"; namespace message_data { @@ -419,5 +418,4 @@ void OpenthermHub::dump_config() { } } -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 960e23d6dd3..26381376686 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -35,8 +35,7 @@ #include "opentherm_macros.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { static const uint8_t REPEATING_MESSAGE_ORDER = 255; static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254; @@ -175,5 +174,4 @@ class OpenthermHub : public Component { void dump_config() override; }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/input.h b/esphome/components/opentherm/input.h index 35671387926..f196d490375 100644 --- a/esphome/components/opentherm/input.h +++ b/esphome/components/opentherm/input.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { class OpenthermInput { public: @@ -14,5 +13,4 @@ class OpenthermInput { virtual void set_auto_max_value(bool auto_max_value) { this->auto_max_value = auto_max_value; } }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/number/opentherm_number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp index bdb02a605c1..d78c6eb38a1 100644 --- a/esphome/components/opentherm/number/opentherm_number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -1,7 +1,6 @@ #include "opentherm_number.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { static const char *const TAG = "opentherm.number"; @@ -38,5 +37,4 @@ void OpenthermNumber::dump_config() { this->restore_value_, this->initial_value_, this->state); } -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/number/opentherm_number.h b/esphome/components/opentherm/number/opentherm_number.h index 6f860727540..c110bed2eb2 100644 --- a/esphome/components/opentherm/number/opentherm_number.h +++ b/esphome/components/opentherm/number/opentherm_number.h @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "esphome/components/opentherm/input.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { // Just a simple number, which stores the number class OpenthermNumber : public number::Number, public Component, public OpenthermInput { @@ -27,5 +26,4 @@ class OpenthermNumber : public number::Number, public Component, public Openther void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 97cf83a5aa2..1ee4c9191bc 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -16,8 +16,7 @@ #endif #include <string> -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { using std::string; @@ -566,5 +565,4 @@ void OpenthermData::s16(int16_t value) { this->valueHB = (value >> 8) & 0xFF; } -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index eb8c5b3ad6c..3078e92c9da 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -16,8 +16,7 @@ #include "driver/gptimer.h" #endif -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; } @@ -403,5 +402,4 @@ class OpenTherm { #endif }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h index 398c64aa8f2..c5ed02a50bc 100644 --- a/esphome/components/opentherm/opentherm_macros.h +++ b/esphome/components/opentherm/opentherm_macros.h @@ -1,6 +1,6 @@ #pragma once -namespace esphome { -namespace opentherm { + +namespace esphome::opentherm { // ===== hub.h macros ===== @@ -158,5 +158,4 @@ namespace opentherm { #define SHOW_INNER(x) #x #define SHOW(x) SHOW_INNER(x) -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/output/opentherm_output.cpp b/esphome/components/opentherm/output/opentherm_output.cpp index ff82ddd72c2..2735c85d069 100644 --- a/esphome/components/opentherm/output/opentherm_output.cpp +++ b/esphome/components/opentherm/output/opentherm_output.cpp @@ -1,8 +1,7 @@ #include "esphome/core/helpers.h" // for clamp() and lerp() #include "opentherm_output.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { static const char *const TAG = "opentherm.output"; @@ -14,5 +13,4 @@ void opentherm::OpenthermOutput::write_state(float state) { this->has_state_ = true; ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); } -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/output/opentherm_output.h b/esphome/components/opentherm/output/opentherm_output.h index 8d6a0ee4bae..e789d727022 100644 --- a/esphome/components/opentherm/output/opentherm_output.h +++ b/esphome/components/opentherm/output/opentherm_output.h @@ -4,8 +4,7 @@ #include "esphome/components/opentherm/input.h" #include "esphome/core/log.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { class OpenthermOutput : public output::FloatOutput, public Component, public OpenthermInput { protected: @@ -29,5 +28,4 @@ class OpenthermOutput : public output::FloatOutput, public Component, public Ope float get_max_value() { return this->max_value_; } }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/switch/opentherm_switch.cpp b/esphome/components/opentherm/switch/opentherm_switch.cpp index 5c5d62e68e5..2fc99e793e8 100644 --- a/esphome/components/opentherm/switch/opentherm_switch.cpp +++ b/esphome/components/opentherm/switch/opentherm_switch.cpp @@ -1,7 +1,6 @@ #include "opentherm_switch.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { static const char *const TAG = "opentherm.switch"; @@ -24,5 +23,4 @@ void OpenthermSwitch::dump_config() { ESP_LOGCONFIG(TAG, " Current state: %d", this->state); } -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opentherm/switch/opentherm_switch.h b/esphome/components/opentherm/switch/opentherm_switch.h index 0c20a0d9edd..ca930d4f7c1 100644 --- a/esphome/components/opentherm/switch/opentherm_switch.h +++ b/esphome/components/opentherm/switch/opentherm_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace opentherm { +namespace esphome::opentherm { class OpenthermSwitch : public switch_::Switch, public Component { protected: @@ -16,5 +15,4 @@ class OpenthermSwitch : public switch_::Switch, public Component { void dump_config() override; }; -} // namespace opentherm -} // namespace esphome +} // namespace esphome::opentherm diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index a942e450355..ce5fbdfd826 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -1,8 +1,7 @@ #include "opt3001.h" #include "esphome/core/log.h" -namespace esphome { -namespace opt3001 { +namespace esphome::opt3001 { static const char *const TAG = "opt3001.sensor"; @@ -116,5 +115,4 @@ void OPT3001Sensor::update() { }); } -} // namespace opt3001 -} // namespace esphome +} // namespace esphome::opt3001 diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h index 3bce9f0aeb0..e5de5363537 100644 --- a/esphome/components/opt3001/opt3001.h +++ b/esphome/components/opt3001/opt3001.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace opt3001 { +namespace esphome::opt3001 { /// This class implements support for the i2c-based OPT3001 ambient light sensor. class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { @@ -22,5 +21,4 @@ class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c bool updating_{false}; }; -} // namespace opt3001 -} // namespace esphome +} // namespace esphome::opt3001 diff --git a/esphome/components/output/automation.cpp b/esphome/components/output/automation.cpp index 5533a6bee4c..610da897d93 100644 --- a/esphome/components/output/automation.cpp +++ b/esphome/components/output/automation.cpp @@ -1,10 +1,8 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace output { +namespace esphome::output { static const char *const TAG = "output.automation"; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 537226a143d..301f568388a 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -6,8 +6,7 @@ #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace output { +namespace esphome::output { template<typename... Ts> class TurnOffAction : public Action<Ts...> { public: @@ -67,5 +66,4 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> { }; #endif // USE_OUTPUT_FLOAT_POWER_SCALING -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/binary_output.h b/esphome/components/output/binary_output.h index 7a15bc7b51c..74ca3ad594d 100644 --- a/esphome/components/output/binary_output.h +++ b/esphome/components/output/binary_output.h @@ -7,8 +7,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace output { +namespace esphome::output { #define LOG_BINARY_OUTPUT(this) \ if (this->inverted_) { \ @@ -69,5 +68,4 @@ class BinaryOutput { #endif }; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/button/output_button.cpp b/esphome/components/output/button/output_button.cpp index 4dd7ec249b1..5833743586f 100644 --- a/esphome/components/output/button/output_button.cpp +++ b/esphome/components/output/button/output_button.cpp @@ -1,8 +1,7 @@ #include "output_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace output { +namespace esphome::output { static const char *const TAG = "output.button"; @@ -17,5 +16,4 @@ void OutputButton::press_action() { this->set_timeout("reset", this->duration_, [this]() { this->output_->turn_off(); }); } -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/button/output_button.h b/esphome/components/output/button/output_button.h index 8802c9754dc..1a2997bdcf9 100644 --- a/esphome/components/output/button/output_button.h +++ b/esphome/components/output/button/output_button.h @@ -4,8 +4,7 @@ #include "esphome/components/button/button.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace output { +namespace esphome::output { class OutputButton : public button::Button, public Component { public: @@ -21,5 +20,4 @@ class OutputButton : public button::Button, public Component { uint32_t duration_; }; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 35629c828af..e2b029f3687 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace output { +namespace esphome::output { static const char *const TAG = "output.float"; @@ -40,5 +39,4 @@ void FloatOutput::set_level(float state) { void FloatOutput::write_state(bool state) { this->set_level(state != this->inverted_ ? 1.0f : 0.0f); } -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 3e1bd839686..673f4235728 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -4,8 +4,7 @@ #include "esphome/core/defines.h" #include "binary_output.h" -namespace esphome { -namespace output { +namespace esphome::output { #ifdef USE_OUTPUT_FLOAT_POWER_SCALING #define LOG_FLOAT_OUTPUT(this) \ @@ -130,5 +129,4 @@ class FloatOutput : public BinaryOutput { #endif }; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/lock/output_lock.cpp b/esphome/components/output/lock/output_lock.cpp index c373cd7b7c9..ee9d9185425 100644 --- a/esphome/components/output/lock/output_lock.cpp +++ b/esphome/components/output/lock/output_lock.cpp @@ -1,8 +1,7 @@ #include "output_lock.h" #include "esphome/core/log.h" -namespace esphome { -namespace output { +namespace esphome::output { static const char *const TAG = "output.lock"; @@ -21,5 +20,4 @@ void OutputLock::control(const lock::LockCall &call) { this->publish_state(state); } -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/lock/output_lock.h b/esphome/components/output/lock/output_lock.h index c183c3a3ea9..7be96e1e824 100644 --- a/esphome/components/output/lock/output_lock.h +++ b/esphome/components/output/lock/output_lock.h @@ -4,8 +4,7 @@ #include "esphome/components/lock/lock.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace output { +namespace esphome::output { class OutputLock : public lock::Lock, public Component { public: @@ -20,5 +19,4 @@ class OutputLock : public lock::Lock, public Component { output::BinaryOutput *output_; }; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index 54260ba37af..7cee2a86398 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -1,8 +1,7 @@ #include "output_switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace output { +namespace esphome::output { static const char *const TAG = "output.switch"; @@ -25,5 +24,4 @@ void OutputSwitch::write_state(bool state) { this->publish_state(state); } -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/output/switch/output_switch.h b/esphome/components/output/switch/output_switch.h index a184a342fe6..b0d85678be0 100644 --- a/esphome/components/output/switch/output_switch.h +++ b/esphome/components/output/switch/output_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace output { +namespace esphome::output { class OutputSwitch : public switch_::Switch, public Component { public: @@ -21,5 +20,4 @@ class OutputSwitch : public switch_::Switch, public Component { output::BinaryOutput *output_; }; -} // namespace output -} // namespace esphome +} // namespace esphome::output diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index a2199977aa0..a21f0e2f63c 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -7,8 +7,7 @@ #include "esphome/components/xxtea/xxtea.h" -namespace esphome { -namespace packet_transport { +namespace esphome::packet_transport { // Maximum bytes to log in hex output (168 * 3 = 504, under TX buffer size of 512) static constexpr size_t PACKET_MAX_LOG_BYTES = 168; @@ -609,5 +608,4 @@ void PacketTransport::send_ping_pong_request_() { this->resend_ping_key_ = false; ESP_LOGV(TAG, "Sent new ping request %08X", (unsigned) this->ping_key_); } -} // namespace packet_transport -} // namespace esphome +} // namespace esphome::packet_transport diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 836775bc855..3938054c159 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -22,8 +22,7 @@ * On receipt of a data packet, it should call `this->process_()` with the data. */ -namespace esphome { -namespace packet_transport { +namespace esphome::packet_transport { // std::less provides allocation-free comparison with const char * template<typename T> using string_map_t = std::map<std::string, T, std::less<>>; @@ -168,5 +167,4 @@ class PacketTransport : public PollingComponent { bool is_encrypted_() const { return !this->encryption_key_.empty(); } }; -} // namespace packet_transport -} // namespace esphome +} // namespace esphome::packet_transport diff --git a/esphome/components/partition/light_partition.cpp b/esphome/components/partition/light_partition.cpp index 63c0d0186e7..2755f82294a 100644 --- a/esphome/components/partition/light_partition.cpp +++ b/esphome/components/partition/light_partition.cpp @@ -1,10 +1,8 @@ #include "light_partition.h" #include "esphome/core/log.h" -namespace esphome { -namespace partition { +namespace esphome::partition { static const char *const TAG = "partition.light"; -} // namespace partition -} // namespace esphome +} // namespace esphome::partition diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index bd90b4c4f1b..7a2f3678c18 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace partition { +namespace esphome::partition { class AddressableSegment { public: @@ -93,5 +92,4 @@ class PartitionLightOutput : public light::AddressableLight { std::vector<AddressableSegment> segments_; }; -} // namespace partition -} // namespace esphome +} // namespace esphome::partition diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index d617336e7e2..4c19feab6eb 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -1,8 +1,7 @@ #include "pca6416a.h" #include "esphome/core/log.h" -namespace esphome { -namespace pca6416a { +namespace esphome::pca6416a { enum PCA6416AGPIORegisters { // 0 side @@ -205,5 +204,4 @@ size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via PCA6416A", this->pin_); } -} // namespace pca6416a -} // namespace esphome +} // namespace esphome::pca6416a diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 4d2e6b219e4..3170033b286 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/gpio_expander/cached_gpio.h" -namespace esphome { -namespace pca6416a { +namespace esphome::pca6416a { class PCA6416AComponent : public Component, public i2c::I2CDevice, @@ -72,5 +71,4 @@ class PCA6416AGPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace pca6416a -} // namespace esphome +} // namespace esphome::pca6416a diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 393bbfd61ef..c3ea6a3c0ce 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -1,8 +1,7 @@ #include "pca9554.h" #include "esphome/core/log.h" -namespace esphome { -namespace pca9554 { +namespace esphome::pca9554 { // for 16 bit expanders, these addresses will be doubled. const uint8_t INPUT_REG = 0; @@ -152,5 +151,4 @@ size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via PCA9554", this->pin_); } -} // namespace pca9554 -} // namespace esphome +} // namespace esphome::pca9554 diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index f33f9d4592c..9fa398cf293 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/gpio_expander/cached_gpio.h" -namespace esphome { -namespace pca9554 { +namespace esphome::pca9554 { class PCA9554Component : public Component, public i2c::I2CDevice, @@ -76,5 +75,4 @@ class PCA9554GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace pca9554 -} // namespace esphome +} // namespace esphome::pca9554 diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 89a6bcdcc0b..533b3391b14 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pca9685 { +namespace esphome::pca9685 { static const char *const TAG = "pca9685"; @@ -160,5 +159,4 @@ void PCA9685Channel::write_state(float state) { this->parent_->set_channel_value_(this->channel_, duty); } -} // namespace pca9685 -} // namespace esphome +} // namespace esphome::pca9685 diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 785cc974dae..33819f23ee0 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace pca9685 { +namespace esphome::pca9685 { enum class PhaseBalancer { NONE = 0x00, @@ -76,5 +75,4 @@ class PCA9685Output : public Component, public i2c::I2CDevice { bool update_{true}; }; -} // namespace pca9685 -} // namespace esphome +} // namespace esphome::pca9685 diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index 95d91ff18a8..c80283ffc98 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pcd8544 { +namespace esphome::pcd8544 { static const char *const TAG = "pcd_8544"; @@ -128,5 +127,4 @@ void PCD8544::fill(Color color) { this->buffer_[i] = fill; } -} // namespace pcd8544 -} // namespace esphome +} // namespace esphome::pcd8544 diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index cfdb96de619..9e4ee930356 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -4,8 +4,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace pcd8544 { +namespace esphome::pcd8544 { class PCD8544 : public display::DisplayBuffer, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -74,5 +73,4 @@ class PCD8544 : public display::DisplayBuffer, GPIOPin *dc_pin_; }; -} // namespace pcd8544 -} // namespace esphome +} // namespace esphome::pcd8544 diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 000de1433cb..6ec61770f24 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -4,8 +4,7 @@ // Datasheet: // - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf -namespace esphome { -namespace pcf85063 { +namespace esphome::pcf85063 { static const char *const TAG = "pcf85063"; @@ -99,5 +98,4 @@ bool PCF85063Component::write_rtc_() { pcf85063_.reg.day_10, pcf85063_.reg.day, ONOFF(!pcf85063_.reg.osc_stop), pcf85063_.reg.clkout_control); return true; } -} // namespace pcf85063 -} // namespace esphome +} // namespace esphome::pcf85063 diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index 697837f223b..1c6b6bf36d5 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace pcf85063 { +namespace esphome::pcf85063 { class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { public: @@ -91,5 +90,4 @@ template<typename... Ts> class ReadAction : public Action<Ts...>, public Parente public: void play(const Ts &...x) override { this->parent_->read_time(); } }; -} // namespace pcf85063 -} // namespace esphome +} // namespace esphome::pcf85063 diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 50003ca378b..93c0f2bdf27 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -4,8 +4,7 @@ // Datasheet: // - https://nl.mouser.com/datasheet/2/302/PCF8563-1127619.pdf -namespace esphome { -namespace pcf8563 { +namespace esphome::pcf8563 { static const char *const TAG = "PCF8563"; @@ -99,5 +98,4 @@ bool PCF8563Component::write_rtc_() { pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled); return true; } -} // namespace pcf8563 -} // namespace esphome +} // namespace esphome::pcf8563 diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index cd37d05816d..72b600d9ba7 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace pcf8563 { +namespace esphome::pcf8563 { class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { public: @@ -119,5 +118,4 @@ template<typename... Ts> class ReadAction : public Action<Ts...>, public Parente public: void play(const Ts &...x) override { this->parent_->read_time(); } }; -} // namespace pcf8563 -} // namespace esphome +} // namespace esphome::pcf8563 diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 8fe8526797b..2e054b0683a 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -1,8 +1,7 @@ #include "pcf8574.h" #include "esphome/core/log.h" -namespace esphome { -namespace pcf8574 { +namespace esphome::pcf8574 { static const char *const TAG = "pcf8574"; @@ -131,5 +130,4 @@ size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via PCF8574", this->pin_); } -} // namespace pcf8574 -} // namespace esphome +} // namespace esphome::pcf8574 diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index cae2e930b74..ece472c4bbb 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/gpio_expander/cached_gpio.h" -namespace esphome { -namespace pcf8574 { +namespace esphome::pcf8574 { // PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction // so we use uint16_t as bank type to ensure all pins are in one bank and cached together @@ -72,5 +71,4 @@ class PCF8574GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace pcf8574 -} // namespace esphome +} // namespace esphome::pcf8574 diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 00f29983be8..e8e9530dba7 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -1,8 +1,7 @@ #include "pi4ioe5v6408.h" #include "esphome/core/log.h" -namespace esphome { -namespace pi4ioe5v6408 { +namespace esphome::pi4ioe5v6408 { static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; @@ -204,5 +203,4 @@ size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via PI4IOE5V6408", this->pin_); } -} // namespace pi4ioe5v6408 -} // namespace esphome +} // namespace esphome::pi4ioe5v6408 diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index ff2474fe994..6225956430a 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace pi4ioe5v6408 { +namespace esphome::pi4ioe5v6408 { class PI4IOE5V6408Component : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint8_t, 8> { @@ -70,5 +69,4 @@ class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented<PI4IOE5V6408Componen gpio::Flags flags_; }; -} // namespace pi4ioe5v6408 -} // namespace esphome +} // namespace esphome::pi4ioe5v6408 diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 3b971e6559d..1988f574db8 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -6,8 +6,7 @@ #define M_PI 3.1415926535897932384626433 #endif -namespace esphome { -namespace pid { +namespace esphome::pid { static const char *const TAG = "pid.autotune"; @@ -368,5 +367,4 @@ bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; } -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 1db9ca71385..5115c73c95b 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pid { +namespace esphome::pid { class PIDAutotuner { public: @@ -110,5 +109,4 @@ class PIDAutotuner { std::string id_; }; -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 54b7a688b41..8c9231fda61 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -1,8 +1,7 @@ #include "pid_climate.h" #include "esphome/core/log.h" -namespace esphome { -namespace pid { +namespace esphome::pid { static const char *const TAG = "pid.climate"; @@ -186,5 +185,4 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) { void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 479a0e48ee4..9e3c89ca4d9 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -9,8 +9,7 @@ #include "pid_controller.h" #include "pid_autotuner.h" -namespace esphome { -namespace pid { +namespace esphome::pid { class PIDClimate : public climate::Climate, public Component { public: @@ -164,5 +163,4 @@ template<typename... Ts> class PIDSetControlParametersAction : public Action<Ts. PIDClimate *parent_; }; -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index cab15331cde..1c624866838 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -1,7 +1,6 @@ #include "pid_controller.h" -namespace esphome { -namespace pid { +namespace esphome::pid { float PIDController::update(float setpoint, float process_value) { // e(t) ... error at timestamp t @@ -123,5 +122,4 @@ float PIDController::calculate_relative_time_() { return dt / 1000.0f; } -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 6848a23965a..e7fa501cf62 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include <cmath> -namespace esphome { -namespace pid { +namespace esphome::pid { struct PIDController { float update(float setpoint, float process_value); @@ -71,5 +70,4 @@ struct PIDController { FixedRingBuffer<float> output_window_; }; // Struct PIDController -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp index 41ca027d8d7..4e963168e68 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.cpp +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pid { +namespace esphome::pid { static const char *const TAG = "pid.sensor"; @@ -55,5 +54,4 @@ void PIDClimateSensor::update_from_parent_() { } void PIDClimateSensor::dump_config() { LOG_SENSOR("", "PID Climate Sensor", this); } -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h index f3774610f80..d6bdc66a465 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.h +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/pid/pid_climate.h" -namespace esphome { -namespace pid { +namespace esphome::pid { enum PIDClimateSensorType { PID_SENSOR_TYPE_RESULT, @@ -33,5 +32,4 @@ class PIDClimateSensor : public sensor::Sensor, public Component { PIDClimateSensorType type_; }; -} // namespace pid -} // namespace esphome +} // namespace esphome::pid diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp index 60f63427591..1af753fce33 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.cpp +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { static const char *const TAG = "pipsolar.output"; @@ -18,5 +17,4 @@ void PipsolarOutput::write_state(float state) { ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); } } -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h index 66eda8e3916..4a6e4c29d77 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.h +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { class Pipsolar; @@ -40,5 +39,4 @@ template<typename... Ts> class SetOutputAction : public Action<Ts...> { PipsolarOutput *output_; }; -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 5123d8d9d33..bd5733fe747 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { static const char *const TAG = "pipsolar"; @@ -811,5 +810,4 @@ uint16_t Pipsolar::pipsolar_crc_(uint8_t *msg, uint8_t len) { return crc; } -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index beae67a4e06..59332080cf4 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -9,8 +9,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { enum ENUMPollingCommand { POLLING_QPIRI = 0, @@ -246,5 +245,4 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX]; }; -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp index 512587511b1..1eedfed0fd3 100644 --- a/esphome/components/pipsolar/switch/pipsolar_switch.cpp +++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { static const char *const TAG = "pipsolar.switch"; @@ -15,5 +14,4 @@ void PipsolarSwitch::write_state(bool state) { } } -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.h b/esphome/components/pipsolar/switch/pipsolar_switch.h index bb62d4794a5..20d2640d90e 100644 --- a/esphome/components/pipsolar/switch/pipsolar_switch.h +++ b/esphome/components/pipsolar/switch/pipsolar_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/core/component.h" -namespace esphome { -namespace pipsolar { +namespace esphome::pipsolar { class Pipsolar; class PipsolarSwitch : public switch_::Switch, public Component { public: @@ -24,5 +23,4 @@ class PipsolarSwitch : public switch_::Switch, public Component { Pipsolar *parent_; }; -} // namespace pipsolar -} // namespace esphome +} // namespace esphome::pipsolar diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index fe8890e7774..6a325c57dc8 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -1,8 +1,7 @@ #include "pm1006.h" #include "esphome/core/log.h" -namespace esphome { -namespace pm1006 { +namespace esphome::pm1006 { static const char *const TAG = "pm1006"; @@ -93,5 +92,4 @@ void PM1006Component::parse_data_() { } } -} // namespace pm1006 -} // namespace esphome +} // namespace esphome::pm1006 diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 6b6332e1e33..38ab284f476 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace pm1006 { +namespace esphome::pm1006 { class PM1006Component : public PollingComponent, public uart::UARTDevice { public: @@ -33,5 +32,4 @@ class PM1006Component : public PollingComponent, public uart::UARTDevice { uint32_t last_transmission_{0}; }; -} // namespace pm1006 -} // namespace esphome +} // namespace esphome::pm1006 diff --git a/esphome/components/pm2005/pm2005.cpp b/esphome/components/pm2005/pm2005.cpp index d8e253a7717..54a98bf3adb 100644 --- a/esphome/components/pm2005/pm2005.cpp +++ b/esphome/components/pm2005/pm2005.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "pm2005.h" -namespace esphome { -namespace pm2005 { +namespace esphome::pm2005 { static const char *const TAG = "pm2005"; @@ -117,5 +116,4 @@ void PM2005Component::dump_config() { LOG_SENSOR(" ", "PM10 ", this->pm_10_0_sensor_); } -} // namespace pm2005 -} // namespace esphome +} // namespace esphome::pm2005 diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h index e788569b7e0..9661d082d1b 100644 --- a/esphome/components/pm2005/pm2005.h +++ b/esphome/components/pm2005/pm2005.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace pm2005 { +namespace esphome::pm2005 { enum SensorType { PM2005, @@ -40,5 +39,4 @@ class PM2005Component : public PollingComponent, public i2c::I2CDevice { uint8_t measuring_value_index_{10}; }; -} // namespace pm2005 -} // namespace esphome +} // namespace esphome::pm2005 diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp index 4a618586f8d..15f5d3e8793 100644 --- a/esphome/components/pmsa003i/pmsa003i.cpp +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <cstring> -namespace esphome { -namespace pmsa003i { +namespace esphome::pmsa003i { static const char *const TAG = "pmsa003i"; @@ -131,5 +130,4 @@ bool PMSA003IComponent::read_data_(PM25AQIData *data) { return true; } -} // namespace pmsa003i -} // namespace esphome +} // namespace esphome::pmsa003i diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index cd106704a66..aebe80b711d 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace pmsa003i { +namespace esphome::pmsa003i { /**! Structure holding Plantower's standard packet **/ // From https://github.com/adafruit/Adafruit_PM25AQI @@ -63,5 +62,4 @@ class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *pmc_10_0_sensor_{nullptr}; }; -} // namespace pmsa003i -} // namespace esphome +} // namespace esphome::pmsa003i diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 2ed7789c53d..94c0c307665 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace pmwcs3 { +namespace esphome::pmwcs3 { static const uint8_t PMWCS3_I2C_ADDRESS = 0x63; static const uint8_t PMWCS3_REG_READ_START = 0x01; @@ -106,5 +105,4 @@ void PMWCS3Component::read_data_() { }); } -} // namespace pmwcs3 -} // namespace esphome +} // namespace esphome::pmwcs3 diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index b1e26eec4fd..d6691478197 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -7,8 +7,7 @@ // ref: // https://github.com/tinovi/i2cArduino/blob/master/i2cArduino.h -namespace esphome { -namespace pmwcs3 { +namespace esphome::pmwcs3 { class PMWCS3Component : public PollingComponent, public i2c::I2CDevice { public: @@ -64,5 +63,4 @@ template<typename... Ts> class PMWCS3NewI2cAddressAction : public Action<Ts...> PMWCS3Component *parent_; }; -} // namespace pmwcs3 -} // namespace esphome +} // namespace esphome::pmwcs3 diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 3017b78414d..8ef77217262 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -9,8 +9,7 @@ // - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf // - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf -namespace esphome { -namespace pn532 { +namespace esphome::pn532 { static const char *const TAG = "pn532"; @@ -458,5 +457,4 @@ bool PN532BinarySensor::process(const nfc::NfcTagUid &data) { return true; } -} // namespace pn532 -} // namespace esphome +} // namespace esphome::pn532 diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index b76cbb19462..a26f27ed54c 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -10,8 +10,7 @@ #include <cinttypes> #include <vector> -namespace esphome { -namespace pn532 { +namespace esphome::pn532 { static const uint8_t PN532_COMMAND_VERSION_DATA = 0x02; static const uint8_t PN532_COMMAND_SAMCONFIGURATION = 0x14; @@ -138,5 +137,4 @@ template<typename... Ts> class PN532IsWritingCondition : public Condition<Ts...> bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; -} // namespace pn532 -} // namespace esphome +} // namespace esphome::pn532 diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index cca6acd96d1..37674080d83 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -4,8 +4,7 @@ #include "pn532.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn532 { +namespace esphome::pn532 { static const char *const TAG = "pn532.mifare_classic"; @@ -258,5 +257,4 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes return true; } -} // namespace pn532 -} // namespace esphome +} // namespace esphome::pn532 diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 0e0dc1542f4..eb3d13a7e06 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -4,8 +4,7 @@ #include "pn532.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn532 { +namespace esphome::pn532 { static const char *const TAG = "pn532.mifare_ultralight"; @@ -189,5 +188,4 @@ bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write return true; } -} // namespace pn532 -} // namespace esphome +} // namespace esphome::pn532 diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index 41f0f079aae..7f4d78461be 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -7,8 +7,7 @@ // - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf // - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf -namespace esphome { -namespace pn532_i2c { +namespace esphome::pn532_i2c { static const char *const TAG = "pn532_i2c"; @@ -125,5 +124,4 @@ void PN532I2C::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace pn532_i2c -} // namespace esphome +} // namespace esphome::pn532_i2c diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h index 00c0df206d6..b2a2ac2e189 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.h +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pn532_i2c { +namespace esphome::pn532_i2c { class PN532I2C : public pn532::PN532, public i2c::I2CDevice { public: @@ -21,5 +20,4 @@ class PN532I2C : public pn532::PN532, public i2c::I2CDevice { uint8_t read_response_length_(); }; -} // namespace pn532_i2c -} // namespace esphome +} // namespace esphome::pn532_i2c diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 553c6d26a6a..13d9aebc20c 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -7,8 +7,7 @@ // - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf // - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf -namespace esphome { -namespace pn532_spi { +namespace esphome::pn532_spi { static const char *const TAG = "pn532_spi"; @@ -151,5 +150,4 @@ void PN532Spi::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); } -} // namespace pn532_spi -} // namespace esphome +} // namespace esphome::pn532_spi diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h index b7adca22e97..2bfd4accf7d 100644 --- a/esphome/components/pn532_spi/pn532_spi.h +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pn532_spi { +namespace esphome::pn532_spi { class PN532Spi : public pn532::PN532, public spi::SPIDevice<spi::BIT_ORDER_LSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -24,5 +23,4 @@ class PN532Spi : public pn532::PN532, bool read_response(uint8_t command, std::vector<uint8_t> &data) override; }; -} // namespace pn532_spi -} // namespace esphome +} // namespace esphome::pn532_spi diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index a8c65ae633a..0b2e5f5d247 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/pn7150/pn7150.h" -namespace esphome { -namespace pn7150 { +namespace esphome::pn7150 { template<typename... Ts> class PN7150IsWritingCondition : public Condition<Ts...>, public Parented<PN7150> { public: @@ -64,5 +63,4 @@ template<typename... Ts> class SetWriteModeAction : public Action<Ts...>, public void play(const Ts &...x) override { this->parent_->write_mode(); } }; -} // namespace pn7150 -} // namespace esphome +} // namespace esphome::pn7150 diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index d68bea41b3d..2a2724f56b5 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7150 { +namespace esphome::pn7150 { static const char *const TAG = "pn7150"; @@ -1160,5 +1159,4 @@ uint8_t PN7150::wait_for_irq_(uint16_t timeout, bool pin_state) { return nfc::STATUS_FAILED; } -} // namespace pn7150 -} // namespace esphome +} // namespace esphome::pn7150 diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index a468d80943e..fa38c5c3134 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -11,8 +11,7 @@ #include <functional> -namespace esphome { -namespace pn7150 { +namespace esphome::pn7150 { static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; @@ -292,5 +291,4 @@ class PN7150 : public nfc::Nfcc, public Component { std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_; }; -} // namespace pn7150 -} // namespace esphome +} // namespace esphome::pn7150 diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp index 61434cdb285..f1832d95f18 100644 --- a/esphome/components/pn7150/pn7150_mifare_classic.cpp +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -4,8 +4,7 @@ #include "pn7150.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7150 { +namespace esphome::pn7150 { static const char *const TAG = "pn7150.mifare_classic"; @@ -324,5 +323,4 @@ uint8_t PN7150::halt_mifare_classic_tag_() { return nfc::STATUS_OK; } -} // namespace pn7150 -} // namespace esphome +} // namespace esphome::pn7150 diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index 854ddd1be18..ef594144d92 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -5,8 +5,7 @@ #include "pn7150.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7150 { +namespace esphome::pn7150 { static const char *const TAG = "pn7150.mifare_ultralight"; @@ -183,5 +182,4 @@ uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *w return nfc::STATUS_OK; } -} // namespace pn7150 -} // namespace esphome +} // namespace esphome::pn7150 diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp index 4ae884595bb..a61bd27c64e 100644 --- a/esphome/components/pn7150_i2c/pn7150_i2c.cpp +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace pn7150_i2c { +namespace esphome::pn7150_i2c { static const char *const TAG = "pn7150_i2c"; @@ -46,5 +45,4 @@ void PN7150I2C::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace pn7150_i2c -} // namespace esphome +} // namespace esphome::pn7150_i2c diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.h b/esphome/components/pn7150_i2c/pn7150_i2c.h index 9308dddd26b..2ea8c8f75ce 100644 --- a/esphome/components/pn7150_i2c/pn7150_i2c.h +++ b/esphome/components/pn7150_i2c/pn7150_i2c.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pn7150_i2c { +namespace esphome::pn7150_i2c { class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { public: @@ -18,5 +17,4 @@ class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { uint8_t write_nfcc(nfc::NciMessage &tx) override; }; -} // namespace pn7150_i2c -} // namespace esphome +} // namespace esphome::pn7150_i2c diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 7759da8f532..7300c4a8d6e 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/pn7160/pn7160.h" -namespace esphome { -namespace pn7160 { +namespace esphome::pn7160 { template<typename... Ts> class PN7160IsWritingCondition : public Condition<Ts...>, public Parented<PN7160> { public: @@ -64,5 +63,4 @@ template<typename... Ts> class SetWriteModeAction : public Action<Ts...>, public void play(const Ts &...x) override { this->parent_->write_mode(); } }; -} // namespace pn7160 -} // namespace esphome +} // namespace esphome::pn7160 diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 5f0f8d0629c..7abd89b3714 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7160 { +namespace esphome::pn7160 { static const char *const TAG = "pn7160"; @@ -1186,5 +1185,4 @@ uint8_t PN7160::wait_for_irq_(uint16_t timeout, bool pin_state) { return nfc::STATUS_FAILED; } -} // namespace pn7160 -} // namespace esphome +} // namespace esphome::pn7160 diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index 44f7eb07965..da4577874ca 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -11,8 +11,7 @@ #include <functional> -namespace esphome { -namespace pn7160 { +namespace esphome::pn7160 { static constexpr uint16_t NFCC_DEFAULT_TIMEOUT = 10; static constexpr uint16_t NFCC_INIT_TIMEOUT = 50; @@ -311,5 +310,4 @@ class PN7160 : public nfc::Nfcc, public Component { std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_; }; -} // namespace pn7160 -} // namespace esphome +} // namespace esphome::pn7160 diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp index 710a7198c6b..0dc8bbdfe46 100644 --- a/esphome/components/pn7160/pn7160_mifare_classic.cpp +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -4,8 +4,7 @@ #include "pn7160.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7160 { +namespace esphome::pn7160 { static const char *const TAG = "pn7160.mifare_classic"; @@ -324,5 +323,4 @@ uint8_t PN7160::halt_mifare_classic_tag_() { return nfc::STATUS_OK; } -} // namespace pn7160 -} // namespace esphome +} // namespace esphome::pn7160 diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index 8ca0fa2c11b..e319a4cb2eb 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -5,8 +5,7 @@ #include "pn7160.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7160 { +namespace esphome::pn7160 { static const char *const TAG = "pn7160.mifare_ultralight"; @@ -183,5 +182,4 @@ uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *w return nfc::STATUS_OK; } -} // namespace pn7160 -} // namespace esphome +} // namespace esphome::pn7160 diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp index e33c6c793d0..c34cf90e68c 100644 --- a/esphome/components/pn7160_i2c/pn7160_i2c.cpp +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace pn7160_i2c { +namespace esphome::pn7160_i2c { static const char *const TAG = "pn7160_i2c"; @@ -46,5 +45,4 @@ void PN7160I2C::dump_config() { LOG_I2C_DEVICE(this); } -} // namespace pn7160_i2c -} // namespace esphome +} // namespace esphome::pn7160_i2c diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.h b/esphome/components/pn7160_i2c/pn7160_i2c.h index eb253085eb7..d29fd04fac4 100644 --- a/esphome/components/pn7160_i2c/pn7160_i2c.h +++ b/esphome/components/pn7160_i2c/pn7160_i2c.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace pn7160_i2c { +namespace esphome::pn7160_i2c { class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { public: @@ -18,5 +17,4 @@ class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { uint8_t write_nfcc(nfc::NciMessage &tx) override; }; -} // namespace pn7160_i2c -} // namespace esphome +} // namespace esphome::pn7160_i2c diff --git a/esphome/components/pn7160_spi/pn7160_spi.cpp b/esphome/components/pn7160_spi/pn7160_spi.cpp index 09f673f700c..f3c413e9520 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.cpp +++ b/esphome/components/pn7160_spi/pn7160_spi.cpp @@ -1,8 +1,7 @@ #include "pn7160_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace pn7160_spi { +namespace esphome::pn7160_spi { static const char *const TAG = "pn7160_spi"; @@ -50,5 +49,4 @@ void PN7160Spi::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); } -} // namespace pn7160_spi -} // namespace esphome +} // namespace esphome::pn7160_spi diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h index 9b6e21fa2a8..2d9c1fda117 100644 --- a/esphome/components/pn7160_spi/pn7160_spi.h +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace pn7160_spi { +namespace esphome::pn7160_spi { static constexpr uint8_t TDD_SPI_READ = 0xFF; static constexpr uint8_t TDD_SPI_WRITE = 0x0A; @@ -26,5 +25,4 @@ class PN7160Spi : public pn7160::PN7160, uint8_t write_nfcc(nfc::NciMessage &tx) override; }; -} // namespace pn7160_spi -} // namespace esphome +} // namespace esphome::pn7160_spi diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index 5db2122412d..4da73e76ae0 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -1,8 +1,7 @@ #include "power_supply.h" #include "esphome/core/log.h" -namespace esphome { -namespace power_supply { +namespace esphome::power_supply { static const char *const TAG = "power_supply"; @@ -54,5 +53,4 @@ void PowerSupply::on_powerdown() { this->pin_->digital_write(false); } -} // namespace power_supply -} // namespace esphome +} // namespace esphome::power_supply diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 0387074eb87..e096f69e3bc 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace power_supply { +namespace esphome::power_supply { class PowerSupply : public Component { public: @@ -63,5 +62,4 @@ class PowerSupplyRequester { bool requested_{false}; }; -} // namespace power_supply -} // namespace esphome +} // namespace esphome::power_supply diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index e28cc8c8d54..cee02394b48 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -3,8 +3,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/component.h" -namespace esphome { -namespace preferences { +namespace esphome::preferences { class IntervalSyncer final : public Component { public: @@ -25,5 +24,4 @@ class IntervalSyncer final : public Component { #endif }; -} // namespace preferences -} // namespace esphome +} // namespace esphome::preferences diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e2639a22983..0412d8a8422 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -2,8 +2,7 @@ #ifdef USE_NETWORK #include "esphome/core/application.h" -namespace esphome { -namespace prometheus { +namespace esphome::prometheus { void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8"); @@ -1098,6 +1097,6 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima } #endif -} // namespace prometheus -} // namespace esphome +} // namespace esphome::prometheus + #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 7aecab99d1b..53326e94722 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -12,8 +12,7 @@ #include "esphome/core/log.h" #endif -namespace esphome { -namespace prometheus { +namespace esphome::prometheus { class PrometheusHandler : public AsyncWebHandler, public Component { public: @@ -218,6 +217,6 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::map<EntityBase *, std::string> relabel_map_name_; }; -} // namespace prometheus -} // namespace esphome +} // namespace esphome::prometheus + #endif diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index 6c110a577d4..ab680c96955 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -8,8 +8,7 @@ #include <esp_heap_caps.h> -namespace esphome { -namespace psram { +namespace esphome::psram { static const char *const TAG = "psram"; void PsramComponent::dump_config() { @@ -25,7 +24,6 @@ void PsramComponent::dump_config() { } } -} // namespace psram -} // namespace esphome +} // namespace esphome::psram #endif diff --git a/esphome/components/psram/psram.h b/esphome/components/psram/psram.h index 8c891feee95..22a49588b42 100644 --- a/esphome/components/psram/psram.h +++ b/esphome/components/psram/psram.h @@ -4,14 +4,12 @@ #include "esphome/core/component.h" -namespace esphome { -namespace psram { +namespace esphome::psram { class PsramComponent : public Component { void dump_config() override; }; -} // namespace psram -} // namespace esphome +} // namespace esphome::psram #endif diff --git a/esphome/components/pulse_counter/automation.h b/esphome/components/pulse_counter/automation.h index 0c0dc2552d1..14264e87b36 100644 --- a/esphome/components/pulse_counter/automation.h +++ b/esphome/components/pulse_counter/automation.h @@ -4,9 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/pulse_counter/pulse_counter_sensor.h" -namespace esphome { - -namespace pulse_counter { +namespace esphome::pulse_counter { template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> { public: @@ -20,5 +18,4 @@ template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> { PulseCounterSensor *pulse_counter_; }; -} // namespace pulse_counter -} // namespace esphome +} // namespace esphome::pulse_counter diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 5d73bef7da0..13bf3baf832 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -7,8 +7,7 @@ #include <hal/pcnt_ll.h> #endif -namespace esphome { -namespace pulse_counter { +namespace esphome::pulse_counter { static const char *const TAG = "pulse_counter"; @@ -210,5 +209,4 @@ void PulseCounterSensor::update() { this->last_time_ = now; } -} // namespace pulse_counter -} // namespace esphome +} // namespace esphome::pulse_counter diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 7a68858099c..4f23ef15483 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -14,8 +14,7 @@ #endif // defined(SOC_PCNT_SUPPORTED) && __has_include(<driver/pulse_cnt.h>) #endif // USE_ESP32 -namespace esphome { -namespace pulse_counter { +namespace esphome::pulse_counter { enum PulseCounterCountMode { PULSE_COUNTER_DISABLE = 0, @@ -85,5 +84,4 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { sensor::Sensor *total_sensor_{nullptr}; }; -} // namespace pulse_counter -} // namespace esphome +} // namespace esphome::pulse_counter diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h index bf0768b7af5..1def89c3d30 100644 --- a/esphome/components/pulse_meter/automation.h +++ b/esphome/components/pulse_meter/automation.h @@ -4,9 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/pulse_meter/pulse_meter_sensor.h" -namespace esphome { - -namespace pulse_meter { +namespace esphome::pulse_meter { template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> { public: @@ -20,5 +18,4 @@ template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> { PulseMeterSensor *pulse_meter_; }; -} // namespace pulse_meter -} // namespace esphome +} // namespace esphome::pulse_meter diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 433e1f0b7e3..3fe1c722ebb 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -2,8 +2,7 @@ #include <utility> #include "esphome/core/log.h" -namespace esphome { -namespace pulse_meter { +namespace esphome::pulse_meter { static const char *const TAG = "pulse_meter"; @@ -186,5 +185,4 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { sensor->last_pin_val_ = pin_val; } -} // namespace pulse_meter -} // namespace esphome +} // namespace esphome::pulse_meter diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index e46f1e615ff..243a64bf053 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -7,8 +7,7 @@ #include <cinttypes> -namespace esphome { -namespace pulse_meter { +namespace esphome::pulse_meter { class PulseMeterSensor : public sensor::Sensor, public Component { public: @@ -77,5 +76,4 @@ class PulseMeterSensor : public sensor::Sensor, public Component { PulseState pulse_state_{}; }; -} // namespace pulse_meter -} // namespace esphome +} // namespace esphome::pulse_meter diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index d083d48b328..5209ed5352a 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -1,8 +1,7 @@ #include "pulse_width.h" #include "esphome/core/log.h" -namespace esphome { -namespace pulse_width { +namespace esphome::pulse_width { static const char *const TAG = "pulse_width"; @@ -27,5 +26,4 @@ void PulseWidthSensor::update() { this->publish_state(width); } -} // namespace pulse_width -} // namespace esphome +} // namespace esphome::pulse_width diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index c6b896988db..f77766a9615 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace pulse_width { +namespace esphome::pulse_width { /// Store data in a class that doesn't use multiple-inheritance (vtables in flash) class PulseWidthSensorStore { @@ -39,5 +38,4 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { InternalGPIOPin *pin_; }; -} // namespace pulse_width -} // namespace esphome +} // namespace esphome::pulse_width diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 4d4a5466bb2..7a6be40d6c3 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -3,8 +3,8 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 -namespace esphome { -namespace pvvx_mithermometer { + +namespace esphome::pvvx_mithermometer { static const char *const TAG = "display.pvvx_mithermometer"; @@ -186,7 +186,6 @@ void PVVXDisplay::sync_time_() { } #endif -} // namespace pvvx_mithermometer -} // namespace esphome +} // namespace esphome::pvvx_mithermometer #endif diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index 06837b94abf..e1aebae7a5d 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -13,8 +13,7 @@ #include "esphome/components/time/real_time_clock.h" #endif -namespace esphome { -namespace pvvx_mithermometer { +namespace esphome::pvvx_mithermometer { class PVVXDisplay; @@ -130,7 +129,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { pvvx_writer_t writer_{}; }; -} // namespace pvvx_mithermometer -} // namespace esphome +} // namespace esphome::pvvx_mithermometer #endif diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 35badf48bb6..f674fc36949 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace pvvx_mithermometer { +namespace esphome::pvvx_mithermometer { static const char *const TAG = "pvvx_mithermometer"; @@ -140,7 +139,6 @@ bool PVVXMiThermometer::report_results_(const optional<ParseResult> &result, con return true; } -} // namespace pvvx_mithermometer -} // namespace esphome +} // namespace esphome::pvvx_mithermometer #endif diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 09b5e91a16b..b5d6da21eff 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace pvvx_mithermometer { +namespace esphome::pvvx_mithermometer { struct ParseResult { optional<float> temperature; @@ -46,7 +45,6 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic bool report_results_(const optional<ParseResult> &result, const char *address); }; -} // namespace pvvx_mithermometer -} // namespace esphome +} // namespace esphome::pvvx_mithermometer #endif diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 7eb89d5b321..0973699da8c 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -24,8 +24,7 @@ } \ } -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { static const char *const TAG = "pylontech"; static const int MAX_DATA_LENGTH_BYTES = 256; @@ -198,8 +197,7 @@ void PylontechComponent::process_line_(std::string &buffer) { } } -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech #undef PARSE_INT #undef PARSE_STR diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 5727928a60e..1d86803cc23 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -4,8 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { static const uint8_t NUM_BUFFERS = 20; static const uint8_t TEXT_SENSOR_MAX_LEN = 14; @@ -48,5 +47,4 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { std::vector<PylontechListener *> listeners_{}; }; -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.cpp b/esphome/components/pylontech/sensor/pylontech_sensor.cpp index 11437369edf..e2def28be54 100644 --- a/esphome/components/pylontech/sensor/pylontech_sensor.cpp +++ b/esphome/components/pylontech/sensor/pylontech_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { static const char *const TAG = "pylontech.sensor"; @@ -58,5 +57,4 @@ void PylontechSensor::on_line_read(PylontechListener::LineContents *line) { } } -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h index 25e71606a4d..36576e83327 100644 --- a/esphome/components/pylontech/sensor/pylontech_sensor.h +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -3,8 +3,7 @@ #include "../pylontech.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { class PylontechSensor : public PylontechListener { public: @@ -28,5 +27,4 @@ class PylontechSensor : public PylontechListener { int8_t bat_num_; }; -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp index 8175477cb2d..a7c9db15996 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { static const char *const TAG = "pylontech.textsensor"; @@ -38,5 +37,4 @@ void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { } } -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h index 27a3993b3e9..30921b13f4f 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -3,8 +3,7 @@ #include "../pylontech.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace pylontech { +namespace esphome::pylontech { class PylontechTextSensor : public PylontechListener { public: @@ -22,5 +21,4 @@ class PylontechTextSensor : public PylontechListener { int8_t bat_num_; }; -} // namespace pylontech -} // namespace esphome +} // namespace esphome::pylontech diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index d0f96d6d1e5..a28b448340a 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -3,8 +3,7 @@ #include "esphome/core/application.h" #include <cinttypes> -namespace esphome { -namespace pzem004t { +namespace esphome::pzem004t { static const char *const TAG = "pzem004t"; @@ -126,5 +125,4 @@ void PZEM004T::dump_config() { LOG_SENSOR("", "Power", this->power_sensor_); } -} // namespace pzem004t -} // namespace esphome +} // namespace esphome::pzem004t diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index e18413f35c1..71fc1e70ad1 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -4,8 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace pzem004t { +namespace esphome::pzem004t { class PZEM004T : public PollingComponent, public uart::UARTDevice { public: @@ -42,5 +41,4 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { uint32_t last_read_{0}; }; -} // namespace pzem004t -} // namespace esphome +} // namespace esphome::pzem004t diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index 0dbe0e761d6..d36e5d02504 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -1,8 +1,7 @@ #include "pzemac.h" #include "esphome/core/log.h" -namespace esphome { -namespace pzemac { +namespace esphome::pzemac { static const char *const TAG = "pzemac"; @@ -83,5 +82,4 @@ void PZEMAC::reset_energy_() { this->send_raw(cmd); } -} // namespace pzemac -} // namespace esphome +} // namespace esphome::pzemac diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index e5b96115f93..264604fedce 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace pzemac { +namespace esphome::pzemac { template<typename... Ts> class ResetEnergyAction; @@ -49,5 +48,4 @@ template<typename... Ts> class ResetEnergyAction : public Action<Ts...> { PZEMAC *pzemac_; }; -} // namespace pzemac -} // namespace esphome +} // namespace esphome::pzemac diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 428bcc1fcf9..6ded9b3a34c 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -1,8 +1,7 @@ #include "pzemdc.h" #include "esphome/core/log.h" -namespace esphome { -namespace pzemdc { +namespace esphome::pzemdc { static const char *const TAG = "pzemdc"; @@ -71,5 +70,4 @@ void PZEMDC::reset_energy() { this->send_raw(cmd); } -} // namespace pzemdc -} // namespace esphome +} // namespace esphome::pzemdc diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index 2e6c26a10cf..6a7e8404480 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace pzemdc { +namespace esphome::pzemdc { class PZEMDC : public PollingComponent, public modbus::ModbusDevice { public: @@ -42,5 +41,4 @@ template<typename... Ts> class ResetEnergyAction : public Action<Ts...> { PZEMDC *pzemdc_; }; -} // namespace pzemdc -} // namespace esphome +} // namespace esphome::pzemdc diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 44bd006c1ae..5b04a904b54 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include <cmath> -namespace esphome { -namespace qmc5883l { +namespace esphome::qmc5883l { static const char *const TAG = "qmc5883l"; @@ -213,5 +212,4 @@ i2c::ErrorCode QMC5883LComponent::read_bytes_16_le_(uint8_t a_register, uint16_t return err; } -} // namespace qmc5883l -} // namespace esphome +} // namespace esphome::qmc5883l diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 2ab6aa3e9f0..6b8ffa0f40c 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -5,8 +5,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/hal.h" -namespace esphome { -namespace qmc5883l { +namespace esphome::qmc5883l { enum QMC5883LDatarate { QMC5883L_DATARATE_10_HZ = 0b00, @@ -66,5 +65,4 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { HighFrequencyLoopRequester high_freq_; }; -} // namespace qmc5883l -} // namespace esphome +} // namespace esphome::qmc5883l diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 976efe79103..8c8a04c5b71 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -3,8 +3,7 @@ #include <cinttypes> #include <cmath> -namespace esphome { -namespace qmp6988 { +namespace esphome::qmp6988 { static const uint8_t QMP6988_CHIP_ID = 0x5C; @@ -351,5 +350,4 @@ void QMP6988Component::update() { this->pressure_sensor_->publish_state(pressurehectopascals); } -} // namespace qmp6988 -} // namespace esphome +} // namespace esphome::qmp6988 diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h index 5b0f80c77e1..26f858b5d21 100644 --- a/esphome/components/qmp6988/qmp6988.h +++ b/esphome/components/qmp6988/qmp6988.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace qmp6988 { +namespace esphome::qmp6988 { /* oversampling */ enum QMP6988Oversampling : uint8_t { @@ -106,5 +105,4 @@ class QMP6988Component : public PollingComponent, public i2c::I2CDevice { int16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt); }; -} // namespace qmp6988 -} // namespace esphome +} // namespace esphome::qmp6988 diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp index 0322c8a1412..edb78b98e1e 100644 --- a/esphome/components/qr_code/qr_code.cpp +++ b/esphome/components/qr_code/qr_code.cpp @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/log.h" -namespace esphome { -namespace qr_code { +namespace esphome::qr_code { static const char *const TAG = "qr_code"; @@ -74,5 +73,4 @@ uint8_t QrCode::get_size() { return size; } -} // namespace qr_code -} // namespace esphome +} // namespace esphome::qr_code diff --git a/esphome/components/qwiic_pir/qwiic_pir.cpp b/esphome/components/qwiic_pir/qwiic_pir.cpp index c04c0fcc183..baf8dc122df 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.cpp +++ b/esphome/components/qwiic_pir/qwiic_pir.cpp @@ -1,8 +1,7 @@ #include "qwiic_pir.h" #include "esphome/core/log.h" -namespace esphome { -namespace qwiic_pir { +namespace esphome::qwiic_pir { static const char *const TAG = "qwiic_pir"; @@ -129,5 +128,4 @@ void QwiicPIRComponent::clear_events_() { ESP_LOGW(TAG, "Failed to clear events"); } -} // namespace qwiic_pir -} // namespace esphome +} // namespace esphome::qwiic_pir diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h index 797ded2cc62..339632a508a 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.h +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -12,8 +12,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace qwiic_pir { +namespace esphome::qwiic_pir { // Qwiic PIR I2C Register Addresses enum { @@ -65,5 +64,4 @@ class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary void clear_events_(); }; -} // namespace qwiic_pir -} // namespace esphome +} // namespace esphome::qwiic_pir diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index 2c3ef77adda..7e7263d73fc 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace radon_eye_ble { +namespace esphome::radon_eye_ble { static const char *const TAG = "radon_eye_ble"; @@ -19,7 +18,6 @@ bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device return false; } -} // namespace radon_eye_ble -} // namespace esphome +} // namespace esphome::radon_eye_ble #endif diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.h b/esphome/components/radon_eye_ble/radon_eye_listener.h index 26d0233c56e..ceca736e78c 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.h +++ b/esphome/components/radon_eye_ble/radon_eye_listener.h @@ -5,15 +5,13 @@ #include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -namespace esphome { -namespace radon_eye_ble { +namespace esphome::radon_eye_ble { class RadonEyeListener : public esp32_ble_tracker::ESPBTDeviceListener { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; -} // namespace radon_eye_ble -} // namespace esphome +} // namespace esphome::radon_eye_ble #endif diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp index f2d32d51dea..de5bd3d8d58 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace radon_eye_rd200 { +namespace esphome::radon_eye_rd200 { static const char *const TAG = "radon_eye_rd200"; @@ -211,7 +210,6 @@ void RadonEyeRD200::dump_config() { RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {} -} // namespace radon_eye_rd200 -} // namespace esphome +} // namespace esphome::radon_eye_rd200 #endif // USE_ESP32 diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.h b/esphome/components/radon_eye_rd200/radon_eye_rd200.h index f874c815f85..48e075c2d66 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.h +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.h @@ -11,8 +11,7 @@ #include "esphome/core/component.h" #include "esphome/core/log.h" -namespace esphome { -namespace radon_eye_rd200 { +namespace esphome::radon_eye_rd200 { class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode { public: @@ -41,7 +40,6 @@ class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode esp32_ble_tracker::ESPBTUUID sensors_read_characteristic_uuid_; }; -} // namespace radon_eye_rd200 -} // namespace esphome +} // namespace esphome::radon_eye_rd200 #endif // USE_ESP32 diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index c5f7ec2cd41..7c1b6ae3148 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -5,8 +5,7 @@ // Based on: // - https://github.com/miguelbalboa/rfid -namespace esphome { -namespace rc522 { +namespace esphome::rc522 { static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq @@ -498,5 +497,4 @@ void RC522Trigger::process(std::vector<uint8_t> &data) { this->trigger(format_hex_pretty_to(uid_buf, data.data(), data.size(), '-')); } -} // namespace rc522 -} // namespace esphome +} // namespace esphome::rc522 diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index 437cea808bc..45473e04b06 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace rc522 { +namespace esphome::rc522 { class RC522BinarySensor; class RC522Trigger; @@ -275,5 +274,4 @@ class RC522Trigger : public Trigger<std::string> { void process(std::vector<uint8_t> &data); }; -} // namespace rc522 -} // namespace esphome +} // namespace esphome::rc522 diff --git a/esphome/components/rc522_i2c/rc522_i2c.cpp b/esphome/components/rc522_i2c/rc522_i2c.cpp index 6a3d8d24861..dbc86ff8e78 100644 --- a/esphome/components/rc522_i2c/rc522_i2c.cpp +++ b/esphome/components/rc522_i2c/rc522_i2c.cpp @@ -1,8 +1,7 @@ #include "rc522_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace rc522_i2c { +namespace esphome::rc522_i2c { static const char *const TAG = "rc522_i2c"; @@ -66,5 +65,4 @@ void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write t write_bytes(reg >> 1, values, count); } -} // namespace rc522_i2c -} // namespace esphome +} // namespace esphome::rc522_i2c diff --git a/esphome/components/rc522_i2c/rc522_i2c.h b/esphome/components/rc522_i2c/rc522_i2c.h index 8d8b0a07164..bd6f2269d8c 100644 --- a/esphome/components/rc522_i2c/rc522_i2c.h +++ b/esphome/components/rc522_i2c/rc522_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/rc522/rc522.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace rc522_i2c { +namespace esphome::rc522_i2c { class RC522I2C : public rc522::RC522, public i2c::I2CDevice { public: @@ -38,5 +37,4 @@ class RC522I2C : public rc522::RC522, public i2c::I2CDevice { ) override; }; -} // namespace rc522_i2c -} // namespace esphome +} // namespace esphome::rc522_i2c diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp index 40da4498148..b63ad1cfdc1 100644 --- a/esphome/components/rc522_spi/rc522_spi.cpp +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -5,8 +5,7 @@ // Based on: // - https://github.com/miguelbalboa/rfid -namespace esphome { -namespace rc522_spi { +namespace esphome::rc522_spi { static const char *const TAG = "rc522_spi"; @@ -136,5 +135,4 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t ESP_LOGVV(TAG, "write_register_(%d, %d) -> %s", reg, count, buf.c_str()); } -} // namespace rc522_spi -} // namespace esphome +} // namespace esphome::rc522_spi diff --git a/esphome/components/rc522_spi/rc522_spi.h b/esphome/components/rc522_spi/rc522_spi.h index 0ccbcd7588f..54caf5c1171 100644 --- a/esphome/components/rc522_spi/rc522_spi.h +++ b/esphome/components/rc522_spi/rc522_spi.h @@ -4,7 +4,6 @@ #include "esphome/components/rc522/rc522.h" #include "esphome/components/spi/spi.h" -namespace esphome { /** * Library based on https://github.com/miguelbalboa/rfid * and adapted to ESPHome by @glmnet @@ -13,7 +12,7 @@ namespace esphome { * * */ -namespace rc522_spi { +namespace esphome::rc522_spi { class RC522Spi : public rc522::RC522, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, @@ -50,5 +49,4 @@ class RC522Spi : public rc522::RC522, ) override; }; -} // namespace rc522_spi -} // namespace esphome +} // namespace esphome::rc522_spi diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index 04065982f62..00c91cbc473 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace rdm6300 { +namespace esphome::rdm6300 { static const char *const TAG = "rdm6300"; @@ -67,5 +66,4 @@ void rdm6300::RDM6300Component::loop() { } } -} // namespace rdm6300 -} // namespace esphome +} // namespace esphome::rdm6300 diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 24a808b62c7..f088f7de4c6 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -8,8 +8,7 @@ #include <cinttypes> #include <vector> -namespace esphome { -namespace rdm6300 { +namespace esphome::rdm6300 { class RDM6300BinarySensor; class RDM6300Trigger; @@ -52,5 +51,4 @@ class RDM6300Trigger : public Trigger<uint32_t> { void process(uint32_t uid) { this->trigger(uid); } }; -} // namespace rdm6300 -} // namespace esphome +} // namespace esphome::rdm6300 diff --git a/esphome/components/remote_base/abbwelcome_protocol.cpp b/esphome/components/remote_base/abbwelcome_protocol.cpp index a67ca48dbeb..2000148ca8f 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.cpp +++ b/esphome/components/remote_base/abbwelcome_protocol.cpp @@ -1,8 +1,7 @@ #include "abbwelcome_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.abbwelcome"; @@ -123,5 +122,4 @@ void ABBWelcomeProtocol::dump(const ABBWelcomeData &data) { ESP_LOGD(TAG, "Received ABBWelcome: %s", data.format_to(buf)); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 66664a89f3b..7ff32923bef 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -8,8 +8,7 @@ #include <utility> #include <vector> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static constexpr uint8_t MAX_DATA_LENGTH = 15; static constexpr uint8_t DATA_LENGTH_MASK = 0x3f; @@ -272,5 +271,4 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction } data_; }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index f40cff7623d..69f91ba90e4 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.aeha"; @@ -98,5 +97,4 @@ void AEHAProtocol::dump(const AEHAData &data) { ESP_LOGI(TAG, "Received AEHA: address=0x%04X, data=[%s]", data.address, data_str.c_str()); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index 51718eefcb0..3f4e98bd438 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -4,8 +4,7 @@ #include <vector> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct AEHAData { uint16_t address; @@ -42,5 +41,4 @@ template<typename... Ts> class AEHAAction : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/beo4_protocol.cpp b/esphome/components/remote_base/beo4_protocol.cpp index 8f5a642401c..9291501ea3d 100644 --- a/esphome/components/remote_base/beo4_protocol.cpp +++ b/esphome/components/remote_base/beo4_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.beo4"; @@ -149,5 +148,4 @@ void Beo4Protocol::dump(const Beo4Data &data) { ESP_LOGI(TAG, "Beo4: source=0x%02x command=0x%02x repeats=%d ", data.source, data.command, data.repeats); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/beo4_protocol.h b/esphome/components/remote_base/beo4_protocol.h index 445e792cbc0..30b99dbeb77 100644 --- a/esphome/components/remote_base/beo4_protocol.h +++ b/esphome/components/remote_base/beo4_protocol.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct Beo4Data { uint8_t source; // beoSource, e.g. video, audio, light... @@ -39,5 +38,4 @@ template<typename... Ts> class Beo4Action : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/byronsx_protocol.cpp b/esphome/components/remote_base/byronsx_protocol.cpp index f243b5bdfd3..11d6d975d14 100644 --- a/esphome/components/remote_base/byronsx_protocol.cpp +++ b/esphome/components/remote_base/byronsx_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.byronsx"; @@ -135,5 +134,4 @@ void ByronSXProtocol::dump(const ByronSXData &data) { ESP_LOGD(TAG, "Received ByronSX: address=0x%08X, command=0x%02x", data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/byronsx_protocol.h b/esphome/components/remote_base/byronsx_protocol.h index 5d23237ab11..674fa99ea10 100644 --- a/esphome/components/remote_base/byronsx_protocol.h +++ b/esphome/components/remote_base/byronsx_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct ByronSXData { uint8_t address; @@ -42,5 +41,4 @@ template<typename... Ts> class ByronSXAction : public RemoteTransmitterActionBas } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp index 1468b669395..eafa98ebcc4 100644 --- a/esphome/components/remote_base/canalsat_protocol.cpp +++ b/esphome/components/remote_base/canalsat_protocol.cpp @@ -1,8 +1,7 @@ #include "canalsat_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const CANALSAT_TAG = "remote.canalsat"; static const char *const CANALSATLD_TAG = "remote.canalsatld"; @@ -104,5 +103,4 @@ void CanalSatBaseProtocol::dump(const CanalSatData &data) { } } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h index 180989ef996..5ba9115ea86 100644 --- a/esphome/components/remote_base/canalsat_protocol.h +++ b/esphome/components/remote_base/canalsat_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct CanalSatData { uint8_t device : 7; @@ -74,5 +73,4 @@ template<typename... Ts> class CanalSatLDAction : public RemoteTransmitterAction } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp index 21a9f598b7a..53e1d59f135 100644 --- a/esphome/components/remote_base/coolix_protocol.cpp +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -1,8 +1,7 @@ #include "coolix_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.coolix"; @@ -109,5 +108,4 @@ void CoolixProtocol::dump(const CoolixData &data) { } } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h index b66415ff70e..d9441e84178 100644 --- a/esphome/components/remote_base/coolix_protocol.h +++ b/esphome/components/remote_base/coolix_protocol.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct CoolixData { CoolixData() {} @@ -37,5 +36,4 @@ template<typename... Ts> class CoolixAction : public RemoteTransmitterActionBase } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dish_protocol.cpp b/esphome/components/remote_base/dish_protocol.cpp index 69226101bf1..9a6420afd56 100644 --- a/esphome/components/remote_base/dish_protocol.cpp +++ b/esphome/components/remote_base/dish_protocol.cpp @@ -1,8 +1,7 @@ #include "dish_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.dish"; @@ -90,5 +89,4 @@ void DishProtocol::dump(const DishData &data) { ESP_LOGI(TAG, "Received Dish: address=0x%02X, command=0x%02X", data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dish_protocol.h b/esphome/components/remote_base/dish_protocol.h index ca4d04ed342..c89f4e78e11 100644 --- a/esphome/components/remote_base/dish_protocol.h +++ b/esphome/components/remote_base/dish_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct DishData { uint8_t address; @@ -34,5 +33,4 @@ template<typename... Ts> class DishAction : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dooya_protocol.cpp b/esphome/components/remote_base/dooya_protocol.cpp index 84bdbf3e082..6dc7926b4a5 100644 --- a/esphome/components/remote_base/dooya_protocol.cpp +++ b/esphome/components/remote_base/dooya_protocol.cpp @@ -1,8 +1,7 @@ #include "dooya_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.dooya"; @@ -116,5 +115,4 @@ void DooyaProtocol::dump(const DooyaData &data) { data.button, data.check); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dooya_protocol.h b/esphome/components/remote_base/dooya_protocol.h index 9d17ad5d5ee..148c7c17bc8 100644 --- a/esphome/components/remote_base/dooya_protocol.h +++ b/esphome/components/remote_base/dooya_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct DooyaData { uint32_t id; @@ -45,5 +44,4 @@ template<typename... Ts> class DooyaAction : public RemoteTransmitterActionBase< } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index 946bd9cacb1..2261bd04e99 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.drayton"; @@ -236,5 +235,4 @@ void DraytonProtocol::dump(const DraytonData &data) { ((data.address << 1) & 0xffff), data.channel, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h index 75213b91866..693a1bbe85b 100644 --- a/esphome/components/remote_base/drayton_protocol.h +++ b/esphome/components/remote_base/drayton_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct DraytonData { uint16_t address; @@ -42,5 +41,4 @@ template<typename... Ts> class DraytonAction : public RemoteTransmitterActionBas } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dyson_protocol.cpp b/esphome/components/remote_base/dyson_protocol.cpp index db4e1135f43..942b40d26fc 100644 --- a/esphome/components/remote_base/dyson_protocol.cpp +++ b/esphome/components/remote_base/dyson_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.dyson"; @@ -67,5 +66,4 @@ void DysonProtocol::dump(const DysonData &data) { ESP_LOGI(TAG, "Dyson: code=0x%x rolling index=%d", data.code, data.index); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/dyson_protocol.h b/esphome/components/remote_base/dyson_protocol.h index d1c08fefba5..3473a489b2c 100644 --- a/esphome/components/remote_base/dyson_protocol.h +++ b/esphome/components/remote_base/dyson_protocol.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static constexpr uint8_t IGNORE_INDEX = 0xFF; @@ -42,5 +41,4 @@ template<typename... Ts> class DysonAction : public RemoteTransmitterActionBase< } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 0e1617659d8..1d67be86b86 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.gobox"; @@ -128,5 +127,4 @@ void GoboxProtocol::dump(const GoboxData &data) { } } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/gobox_protocol.h b/esphome/components/remote_base/gobox_protocol.h index 7e18b61458d..f6b278771e0 100644 --- a/esphome/components/remote_base/gobox_protocol.h +++ b/esphome/components/remote_base/gobox_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct GoboxData { int code; @@ -50,5 +49,4 @@ template<typename... Ts> class GoboxAction : public RemoteTransmitterActionBase< } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/haier_protocol.cpp b/esphome/components/remote_base/haier_protocol.cpp index 734f3c77893..fa4cec773ff 100644 --- a/esphome/components/remote_base/haier_protocol.cpp +++ b/esphome/components/remote_base/haier_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.haier"; @@ -84,5 +83,4 @@ void HaierProtocol::dump(const HaierData &data) { ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h index 7a4ee640e8f..9c45ba1a635 100644 --- a/esphome/components/remote_base/haier_protocol.h +++ b/esphome/components/remote_base/haier_protocol.h @@ -3,8 +3,7 @@ #include "remote_base.h" #include <vector> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct HaierData { std::vector<uint8_t> data; @@ -35,5 +34,4 @@ template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase< } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/jvc_protocol.cpp b/esphome/components/remote_base/jvc_protocol.cpp index c33cae7a486..86a47e757de 100644 --- a/esphome/components/remote_base/jvc_protocol.cpp +++ b/esphome/components/remote_base/jvc_protocol.cpp @@ -1,8 +1,7 @@ #include "jvc_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.jvc"; @@ -48,5 +47,4 @@ optional<JVCData> JVCProtocol::decode(RemoteReceiveData src) { } void JVCProtocol::dump(const JVCData &data) { ESP_LOGI(TAG, "Received JVC: data=0x%04" PRIX32, data.data); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index a17e593ad25..f6e2548dead 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct JVCData { uint32_t data; @@ -33,5 +32,4 @@ template<typename... Ts> class JVCAction : public RemoteTransmitterActionBase<Ts } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/keeloq_protocol.cpp b/esphome/components/remote_base/keeloq_protocol.cpp index e95c79ef25f..8cc9de94b8d 100644 --- a/esphome/components/remote_base/keeloq_protocol.cpp +++ b/esphome/components/remote_base/keeloq_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.keeloq"; @@ -190,5 +189,4 @@ void KeeloqProtocol::dump(const KeeloqData &data) { ESP_LOGD(TAG, "Received Keeloq: address=0x%08" PRIx32 ", command=0x%02x", data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/keeloq_protocol.h b/esphome/components/remote_base/keeloq_protocol.h index 47125c151be..432313b87b2 100644 --- a/esphome/components/remote_base/keeloq_protocol.h +++ b/esphome/components/remote_base/keeloq_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct KeeloqData { uint32_t encrypted; // 32 bit encrypted field @@ -49,5 +48,4 @@ template<typename... Ts> class KeeloqAction : public RemoteTransmitterActionBase } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/lg_protocol.cpp b/esphome/components/remote_base/lg_protocol.cpp index 4c54ff00bd7..e450659b42f 100644 --- a/esphome/components/remote_base/lg_protocol.cpp +++ b/esphome/components/remote_base/lg_protocol.cpp @@ -1,8 +1,7 @@ #include "lg_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.lg"; @@ -54,5 +53,4 @@ void LGProtocol::dump(const LGData &data) { ESP_LOGI(TAG, "Received LG: data=0x%08" PRIX32 ", nbits=%d", data.data, data.nbits); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index e0039d033da..97159749956 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct LGData { uint32_t data; @@ -37,5 +36,4 @@ template<typename... Ts> class LGAction : public RemoteTransmitterActionBase<Ts. } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp index f25a982fdc3..f417827b389 100644 --- a/esphome/components/remote_base/magiquest_protocol.cpp +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -5,8 +5,7 @@ * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html */ -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.magiquest"; @@ -79,5 +78,4 @@ void MagiQuestProtocol::dump(const MagiQuestData &data) { ESP_LOGI(TAG, "Received MagiQuest: wand_id=0x%08" PRIX32 ", magnitude=0x%04X", data.wand_id, data.magnitude); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h index a531a9aee18..18662ec7598 100644 --- a/esphome/components/remote_base/magiquest_protocol.h +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -8,8 +8,7 @@ * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html */ -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct MagiQuestData { uint16_t magnitude; @@ -48,5 +47,4 @@ template<typename... Ts> class MagiQuestAction : public RemoteTransmitterActionB } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index 4fa717cf088..6889c5d9b4f 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -1,8 +1,7 @@ #include "midea_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.midea"; @@ -75,5 +74,4 @@ void MideaProtocol::dump(const MideaData &data) { ESP_LOGI(TAG, "Received Midea: %s", data.to_str(buf)); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 334e8a7cb31..f21dd40828f 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { class MideaData { public: @@ -88,5 +87,4 @@ template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase< } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/mirage_protocol.cpp b/esphome/components/remote_base/mirage_protocol.cpp index 2ae877f1931..380cfaecb26 100644 --- a/esphome/components/remote_base/mirage_protocol.cpp +++ b/esphome/components/remote_base/mirage_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.mirage"; @@ -85,5 +84,4 @@ void MirageProtocol::dump(const MirageData &data) { ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/mirage_protocol.h b/esphome/components/remote_base/mirage_protocol.h index 4257f7fa005..c967e72f134 100644 --- a/esphome/components/remote_base/mirage_protocol.h +++ b/esphome/components/remote_base/mirage_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct MirageData { std::vector<uint8_t> data; @@ -35,5 +34,4 @@ template<typename... Ts> class MirageAction : public RemoteTransmitterActionBase } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 062f81b4d60..e639248b4e4 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -1,8 +1,7 @@ #include "nec_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.nec"; @@ -98,5 +97,4 @@ void NECProtocol::dump(const NECData &data) { data.command_repeats); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 71e1bccba8f..7b310e8ba5b 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct NECData { uint16_t address; @@ -37,5 +36,4 @@ template<typename... Ts> class NECAction : public RemoteTransmitterActionBase<Ts } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp index 28b415d4d64..e691a695987 100644 --- a/esphome/components/remote_base/nexa_protocol.cpp +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -1,8 +1,7 @@ #include "nexa_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.nexa"; @@ -236,5 +235,4 @@ void NexaProtocol::dump(const NexaData &data) { data.state, data.channel, data.level); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h index 4d9443ce0e8..ebcd2a2c113 100644 --- a/esphome/components/remote_base/nexa_protocol.h +++ b/esphome/components/remote_base/nexa_protocol.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct NexaData { uint32_t device; @@ -50,5 +49,4 @@ template<typename... Ts> class NexaAction : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/panasonic_protocol.cpp b/esphome/components/remote_base/panasonic_protocol.cpp index e0acc42692e..b0262130804 100644 --- a/esphome/components/remote_base/panasonic_protocol.cpp +++ b/esphome/components/remote_base/panasonic_protocol.cpp @@ -1,8 +1,7 @@ #include "panasonic_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.panasonic"; @@ -70,5 +69,4 @@ void PanasonicProtocol::dump(const PanasonicData &data) { ESP_LOGI(TAG, "Received Panasonic: address=0x%04X, command=0x%08" PRIX32, data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index c81366138a6..d13c0f27985 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct PanasonicData { uint16_t address; @@ -37,5 +36,4 @@ template<typename... Ts> class PanasonicAction : public RemoteTransmitterActionB } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp index f350ef66aee..f4d6aa4026c 100644 --- a/esphome/components/remote_base/pioneer_protocol.cpp +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -1,8 +1,7 @@ #include "pioneer_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.pioneer"; @@ -152,5 +151,4 @@ void PioneerProtocol::dump(const PioneerData &data) { } } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index 4cac4f9f32c..514ab675016 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct PioneerData { uint16_t rc_code_1; @@ -34,5 +33,4 @@ template<typename... Ts> class PioneerAction : public RemoteTransmitterActionBas } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 6903cd46058..dc128d4622d 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -35,8 +35,7 @@ #include "pronto_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.pronto"; @@ -243,5 +242,4 @@ void ProntoProtocol::dump(const ProntoData &data) { } while (remaining > 0); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index e600834d1ab..f4f6b2144d9 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { std::vector<uint16_t> encode_pronto(const std::string &str); @@ -51,5 +50,4 @@ template<typename... Ts> class ProntoAction : public RemoteTransmitterActionBase } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/raw_protocol.cpp b/esphome/components/remote_base/raw_protocol.cpp index 7e6be3b77ed..02c29168496 100644 --- a/esphome/components/remote_base/raw_protocol.cpp +++ b/esphome/components/remote_base/raw_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.raw"; @@ -38,5 +37,4 @@ bool RawDumper::dump(RemoteReceiveData src) { return true; } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 941b6aab426..1bcf390b628 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -6,8 +6,7 @@ #include <cinttypes> #include <vector> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { class RawBinarySensor : public RemoteReceiverBinarySensorBase { public: @@ -82,5 +81,4 @@ class RawDumper : public RemoteReceiverDumperBase { bool is_secondary() override { return true; } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index bb6d382d804..c7f79ad84a3 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -1,8 +1,7 @@ #include "rc5_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.rc5"; @@ -86,5 +85,4 @@ void RC5Protocol::dump(const RC5Data &data) { ESP_LOGI(TAG, "Received RC5: address=0x%02X, command=0x%02X", data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 589c8d42dee..dbb89e41c60 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct RC5Data { uint8_t address; @@ -35,5 +34,4 @@ template<typename... Ts> class RC5Action : public RemoteTransmitterActionBase<Ts } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc6_protocol.cpp b/esphome/components/remote_base/rc6_protocol.cpp index b442bb4c270..ddd9e532580 100644 --- a/esphome/components/remote_base/rc6_protocol.cpp +++ b/esphome/components/remote_base/rc6_protocol.cpp @@ -1,8 +1,7 @@ #include "rc6_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const RC6_TAG = "remote.rc6"; @@ -177,5 +176,4 @@ void RC6Protocol::dump(const RC6Data &data) { data.command, data.toggle); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc6_protocol.h b/esphome/components/remote_base/rc6_protocol.h index 980509a2fc5..fda9d98ecbb 100644 --- a/esphome/components/remote_base/rc6_protocol.h +++ b/esphome/components/remote_base/rc6_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct RC6Data { uint8_t mode : 3; @@ -42,5 +41,4 @@ template<typename... Ts> class RC6Action : public RemoteTransmitterActionBase<Ts uint8_t toggle_{0}; }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index cb8a0779755..612558ca1c9 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -1,8 +1,7 @@ #include "rc_switch_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.rc_switch"; @@ -267,5 +266,4 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { return false; } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 96cbbd1467c..3224c04fb29 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct RCSwitchData { uint64_t code; @@ -217,5 +216,4 @@ class RCSwitchDumper : public RemoteReceiverDumperBase { using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase>; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index b4a549f0bed..4d9bc55f216 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote_base"; @@ -198,5 +197,4 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { #endif this->send_internal(send_times, send_wait); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index e5e923d7806..0b1109267fa 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -8,8 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { enum ToleranceMode : uint8_t { TOLERANCE_MODE_PERCENTAGE = 0, @@ -317,5 +316,4 @@ template<typename T> class RemoteReceiverDumper : public RemoteReceiverDumperBas using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol>; #define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix) -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/roomba_protocol.cpp b/esphome/components/remote_base/roomba_protocol.cpp index 6b7d2163748..8053792a60c 100644 --- a/esphome/components/remote_base/roomba_protocol.cpp +++ b/esphome/components/remote_base/roomba_protocol.cpp @@ -1,8 +1,7 @@ #include "roomba_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.roomba"; @@ -52,5 +51,4 @@ optional<RoombaData> RoombaProtocol::decode(RemoteReceiveData src) { } void RoombaProtocol::dump(const RoombaData &data) { ESP_LOGD(TAG, "Received Roomba: data=0x%02X", data.data); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/roomba_protocol.h b/esphome/components/remote_base/roomba_protocol.h index f94cb7df1b1..3582dac398b 100644 --- a/esphome/components/remote_base/roomba_protocol.h +++ b/esphome/components/remote_base/roomba_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct RoombaData { uint8_t data; @@ -31,5 +30,4 @@ template<typename... Ts> class RoombaAction : public RemoteTransmitterActionBase } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/samsung36_protocol.cpp b/esphome/components/remote_base/samsung36_protocol.cpp index 10e8bd2d013..ded8c71aa38 100644 --- a/esphome/components/remote_base/samsung36_protocol.cpp +++ b/esphome/components/remote_base/samsung36_protocol.cpp @@ -1,8 +1,7 @@ #include "samsung36_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.samsung36"; @@ -99,5 +98,4 @@ void Samsung36Protocol::dump(const Samsung36Data &data) { ESP_LOGI(TAG, "Received Samsung36: address=0x%04X, command=0x%08" PRIX32, data.address, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/samsung36_protocol.h b/esphome/components/remote_base/samsung36_protocol.h index aa7fd21609f..4f15d906e76 100644 --- a/esphome/components/remote_base/samsung36_protocol.h +++ b/esphome/components/remote_base/samsung36_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct Samsung36Data { uint16_t address; @@ -37,5 +36,4 @@ template<typename... Ts> class Samsung36Action : public RemoteTransmitterActionB } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 2a48cbb918e..7190e974038 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.samsung"; @@ -61,5 +60,4 @@ void SamsungProtocol::dump(const SamsungData &data) { ESP_LOGI(TAG, "Received Samsung: data=0x%" PRIX64 ", nbits=%d", data.data, data.nbits); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index 41434f28892..bb234d681de 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct SamsungData { uint64_t data; @@ -35,5 +34,4 @@ template<typename... Ts> class SamsungAction : public RemoteTransmitterActionBas } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 504b3469251..0abb7fc0e02 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -1,8 +1,7 @@ #include "sony_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.sony"; @@ -65,5 +64,4 @@ void SonyProtocol::dump(const SonyData &data) { ESP_LOGI(TAG, "Received Sony: data=0x%08" PRIX32 ", nbits=%d", data.data, data.nbits); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index d9e4f37d53f..eb873e8b7dc 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct SonyData { uint32_t data; @@ -37,5 +36,4 @@ template<typename... Ts> class SonyAction : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index 6844e449ed0..8b39abf9ad9 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.symphony"; @@ -118,5 +117,4 @@ void SymphonyProtocol::dump(const SymphonyData &data) { ESP_LOGI(TAG, "Received Symphony: data=0x%0*" PRIX32 ", nbits=%" PRIu8, hex_width, (uint32_t) data.data, data.nbits); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/symphony_protocol.h b/esphome/components/remote_base/symphony_protocol.h index 7e77a268ba2..7caf5eab867 100644 --- a/esphome/components/remote_base/symphony_protocol.h +++ b/esphome/components/remote_base/symphony_protocol.h @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct SymphonyData { uint32_t data; @@ -40,5 +39,4 @@ template<typename... Ts> class SymphonyAction : public RemoteTransmitterActionBa } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/toshiba_ac_protocol.cpp b/esphome/components/remote_base/toshiba_ac_protocol.cpp index a20a29b84a9..077b4340fa6 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.cpp +++ b/esphome/components/remote_base/toshiba_ac_protocol.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.toshibaac"; @@ -111,5 +110,4 @@ void ToshibaAcProtocol::dump(const ToshibaAcData &data) { } } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/toshiba_ac_protocol.h b/esphome/components/remote_base/toshiba_ac_protocol.h index c69401c3785..8a853005acb 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.h +++ b/esphome/components/remote_base/toshiba_ac_protocol.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct ToshibaAcData { uint64_t rc_code_1; @@ -35,5 +34,4 @@ template<typename... Ts> class ToshibaAcAction : public RemoteTransmitterActionB } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/toto_protocol.cpp b/esphome/components/remote_base/toto_protocol.cpp index f08258c4a3e..042efcbc363 100644 --- a/esphome/components/remote_base/toto_protocol.cpp +++ b/esphome/components/remote_base/toto_protocol.cpp @@ -1,8 +1,7 @@ #include "toto_protocol.h" #include "esphome/core/log.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { static const char *const TAG = "remote.toto"; @@ -96,5 +95,4 @@ void TotoProtocol::dump(const TotoData &data) { data.rc_code_2, data.command); } -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index 53d453f7e3d..285c9f21257 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -2,8 +2,7 @@ #include "remote_base.h" -namespace esphome { -namespace remote_base { +namespace esphome::remote_base { struct TotoData { uint8_t rc_code_1 : 4; @@ -39,5 +38,4 @@ template<typename... Ts> class TotoAction : public RemoteTransmitterActionBase<T } }; -} // namespace remote_base -} // namespace esphome +} // namespace esphome::remote_base diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 3b50353ddc8..9f69a7b50b0 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -12,8 +12,7 @@ #include <algorithm> #include <cstring> -namespace esphome { -namespace resampler { +namespace esphome::resampler { static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1; @@ -373,7 +372,6 @@ void ResamplerSpeaker::resample_task(void *params) { vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } -} // namespace resampler -} // namespace esphome +} // namespace esphome::resampler #endif diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index cdbc1c22db4..36f39fda97f 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -11,8 +11,7 @@ #include <freertos/event_groups.h> -namespace esphome { -namespace resampler { +namespace esphome::resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: @@ -99,7 +98,6 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { uint64_t callback_remainder_{0}; }; -} // namespace resampler -} // namespace esphome +} // namespace esphome::resampler #endif diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp index 706a059de35..60565090938 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -1,8 +1,7 @@ #include "resistance_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace resistance { +namespace esphome::resistance { static const char *const TAG = "resistance"; @@ -43,5 +42,4 @@ void ResistanceSensor::process_(float value) { this->publish_state(res); } -} // namespace resistance -} // namespace esphome +} // namespace esphome::resistance diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index a3b6e92c595..b646fb509a1 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace resistance { +namespace esphome::resistance { enum ResistanceConfiguration { UPSTREAM, @@ -33,5 +32,4 @@ class ResistanceSensor : public Component, public sensor::Sensor { float reference_voltage_; }; -} // namespace resistance -} // namespace esphome +} // namespace esphome::resistance diff --git a/esphome/components/restart/button/restart_button.cpp b/esphome/components/restart/button/restart_button.cpp index accb1a8356c..d6404315ea9 100644 --- a/esphome/components/restart/button/restart_button.cpp +++ b/esphome/components/restart/button/restart_button.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace restart { +namespace esphome::restart { static const char *const TAG = "restart.button"; @@ -16,5 +15,4 @@ void RestartButton::press_action() { } void RestartButton::dump_config() { LOG_BUTTON("", "Restart Button", this); } -} // namespace restart -} // namespace esphome +} // namespace esphome::restart diff --git a/esphome/components/restart/button/restart_button.h b/esphome/components/restart/button/restart_button.h index fd51282d36d..974db0cec48 100644 --- a/esphome/components/restart/button/restart_button.h +++ b/esphome/components/restart/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "esphome/core/component.h" -namespace esphome { -namespace restart { +namespace esphome::restart { class RestartButton final : public button::Button, public Component { public: @@ -14,5 +13,4 @@ class RestartButton final : public button::Button, public Component { void press_action() override; }; -} // namespace restart -} // namespace esphome +} // namespace esphome::restart diff --git a/esphome/components/restart/switch/restart_switch.cpp b/esphome/components/restart/switch/restart_switch.cpp index 422e85f4cd7..96a4fc40f5c 100644 --- a/esphome/components/restart/switch/restart_switch.cpp +++ b/esphome/components/restart/switch/restart_switch.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace restart { +namespace esphome::restart { static const char *const TAG = "restart"; @@ -21,5 +20,4 @@ void RestartSwitch::write_state(bool state) { } void RestartSwitch::dump_config() { LOG_SWITCH("", "Restart Switch", this); } -} // namespace restart -} // namespace esphome +} // namespace esphome::restart diff --git a/esphome/components/restart/switch/restart_switch.h b/esphome/components/restart/switch/restart_switch.h index 7f1902ab53c..67b4a2bfd10 100644 --- a/esphome/components/restart/switch/restart_switch.h +++ b/esphome/components/restart/switch/restart_switch.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace restart { +namespace esphome::restart { class RestartSwitch : public switch_::Switch, public Component { public: @@ -14,5 +13,4 @@ class RestartSwitch : public switch_::Switch, public Component { void write_state(bool state) override; }; -} // namespace restart -} // namespace esphome +} // namespace esphome::restart diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 5ca629c12b0..cec32e04064 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -5,8 +5,7 @@ #include <cinttypes> #include <cstring> -namespace esphome { -namespace rf_bridge { +namespace esphome::rf_bridge { static const char *const TAG = "rf_bridge"; @@ -243,5 +242,4 @@ void RFBridgeComponent::beep(uint16_t ms) { this->flush(); } -} // namespace rf_bridge -} // namespace esphome +} // namespace esphome::rf_bridge diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 571ac6c385e..2f91459076f 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -7,8 +7,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" -namespace esphome { -namespace rf_bridge { +namespace esphome::rf_bridge { static const uint8_t RF_MESSAGE_SIZE = 9; static const uint8_t RF_CODE_START = 0xAA; @@ -179,5 +178,4 @@ template<typename... Ts> class RFBridgeBeepAction : public Action<Ts...> { RFBridgeComponent *parent_; }; -} // namespace rf_bridge -} // namespace esphome +} // namespace esphome::rf_bridge diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index 783187667a0..f0d599cf575 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace rgb { +namespace esphome::rgb { class RGBLightOutput : public light::LightOutput { public: @@ -32,5 +31,4 @@ class RGBLightOutput : public light::LightOutput { output::FloatOutput *blue_; }; -} // namespace rgb -} // namespace esphome +} // namespace esphome::rgb diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h index 9e23f783ae5..84ecb232cc6 100644 --- a/esphome/components/rgbct/rgbct_light_output.h +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/core/component.h" -namespace esphome { -namespace rgbct { +namespace esphome::rgbct { class RGBCTLightOutput : public light::LightOutput { public: @@ -55,5 +54,4 @@ class RGBCTLightOutput : public light::LightOutput { bool color_interlock_{true}; }; -} // namespace rgbct -} // namespace esphome +} // namespace esphome::rgbct diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 140726a43c8..ae96eb20246 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace rgbw { +namespace esphome::rgbw { class RGBWLightOutput : public light::LightOutput { public: @@ -40,5 +39,4 @@ class RGBWLightOutput : public light::LightOutput { bool color_interlock_{false}; }; -} // namespace rgbw -} // namespace esphome +} // namespace esphome::rgbw diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index 96873600595..de5ee993f8c 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace rgbww { +namespace esphome::rgbww { class RGBWWLightOutput : public light::LightOutput { public: @@ -51,5 +50,4 @@ class RGBWWLightOutput : public light::LightOutput { bool color_interlock_{false}; }; -} // namespace rgbww -} // namespace esphome +} // namespace esphome::rgbww diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 38fd14375d6..0831822d868 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace rotary_encoder { +namespace esphome::rotary_encoder { static const char *const TAG = "rotary_encoder"; @@ -242,5 +241,4 @@ void RotaryEncoderSensor::set_resolution(RotaryEncoderResolution mode) { this->s void RotaryEncoderSensor::set_min_value(int32_t min_value) { this->store_.min_value = min_value; } void RotaryEncoderSensor::set_max_value(int32_t max_value) { this->store_.max_value = max_value; } -} // namespace rotary_encoder -} // namespace esphome +} // namespace esphome::rotary_encoder diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 6f4a4fd83ce..8a56da4fe27 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -7,8 +7,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace rotary_encoder { +namespace esphome::rotary_encoder { /// All possible restore modes for the rotary encoder enum RotaryEncoderRestoreMode { @@ -118,5 +117,4 @@ template<typename... Ts> class RotaryEncoderSetValueAction : public Action<Ts... RotaryEncoderSensor *encoder_; }; -} // namespace rotary_encoder -} // namespace esphome +} // namespace esphome::rotary_encoder diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 07f870b60cc..b73b73d56e5 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ruuvi_ble { +namespace esphome::ruuvi_ble { static const char *const TAG = "ruuvi_ble"; @@ -142,7 +141,6 @@ bool RuuviListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return true; } -} // namespace ruuvi_ble -} // namespace esphome +} // namespace esphome::ruuvi_ble #endif diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.h b/esphome/components/ruuvi_ble/ruuvi_ble.h index add431ce423..80b07d410b0 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.h +++ b/esphome/components/ruuvi_ble/ruuvi_ble.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ruuvi_ble { +namespace esphome::ruuvi_ble { struct RuuviParseResult { optional<float> humidity; @@ -31,7 +30,6 @@ class RuuviListener : public esp32_ble_tracker::ESPBTDeviceListener { bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; -} // namespace ruuvi_ble -} // namespace esphome +} // namespace esphome::ruuvi_ble #endif diff --git a/esphome/components/ruuvitag/ruuvitag.cpp b/esphome/components/ruuvitag/ruuvitag.cpp index 9b462b47945..99c6b8ae26e 100644 --- a/esphome/components/ruuvitag/ruuvitag.cpp +++ b/esphome/components/ruuvitag/ruuvitag.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ruuvitag { +namespace esphome::ruuvitag { static const char *const TAG = "ruuvitag"; @@ -23,7 +22,6 @@ void RuuviTag::dump_config() { LOG_SENSOR(" ", "Measurement Sequence Number", this->measurement_sequence_number_); } -} // namespace ruuvitag -} // namespace esphome +} // namespace esphome::ruuvitag #endif diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index dfe393724cc..259675835d5 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ruuvitag { +namespace esphome::ruuvitag { class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -77,7 +76,6 @@ class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener sensor::Sensor *measurement_sequence_number_{nullptr}; }; -} // namespace ruuvitag -} // namespace esphome +} // namespace esphome::ruuvitag #endif diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp index 0aa6e86d31e..1b5b71f0e0e 100644 --- a/esphome/components/rx8130/rx8130.cpp +++ b/esphome/components/rx8130/rx8130.cpp @@ -3,8 +3,7 @@ // https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf -namespace esphome { -namespace rx8130 { +namespace esphome::rx8130 { static const uint8_t RX8130_REG_SEC = 0x10; static const uint8_t RX8130_REG_MIN = 0x11; @@ -121,5 +120,4 @@ void RX8130Component::stop_(bool stop) { } } -} // namespace rx8130 -} // namespace esphome +} // namespace esphome::rx8130 diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h index 979da3e19cb..152bd10f273 100644 --- a/esphome/components/rx8130/rx8130.h +++ b/esphome/components/rx8130/rx8130.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace rx8130 { +namespace esphome::rx8130 { class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { public: @@ -29,5 +28,4 @@ template<typename... Ts> class ReadAction : public Action<Ts...>, public Parente void play(const Ts... x) override { this->parent_->read_time(); } }; -} // namespace rx8130 -} // namespace esphome +} // namespace esphome::rx8130 From ded83812f4b88c04b8a1c6d1985b4c6d73b94fc9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 20:16:51 -0400 Subject: [PATCH 1896/2056] [clang-tidy] Concatenate nested namespaces (5/7: components s) (#16302) --- esphome/components/safe_mode/button/safe_mode_button.cpp | 6 ++---- esphome/components/safe_mode/button/safe_mode_button.h | 6 ++---- esphome/components/safe_mode/switch/safe_mode_switch.cpp | 6 ++---- esphome/components/safe_mode/switch/safe_mode_switch.h | 6 ++---- esphome/components/scd30/automation.h | 6 ++---- esphome/components/scd30/scd30.cpp | 6 ++---- esphome/components/scd30/scd30.h | 6 ++---- esphome/components/scd4x/automation.h | 6 ++---- esphome/components/scd4x/scd4x.cpp | 6 ++---- esphome/components/scd4x/scd4x.h | 6 ++---- esphome/components/script/script.cpp | 6 ++---- esphome/components/script/script.h | 7 +++---- esphome/components/sdm_meter/sdm_meter.cpp | 6 ++---- esphome/components/sdm_meter/sdm_meter.h | 6 ++---- esphome/components/sdm_meter/sdm_meter_registers.h | 6 ++---- esphome/components/sdp3x/sdp3x.cpp | 6 ++---- esphome/components/sdp3x/sdp3x.h | 6 ++---- esphome/components/sds011/sds011.cpp | 6 ++---- esphome/components/sds011/sds011.h | 6 ++---- .../seeed_mr24hpc1/button/custom_mode_end_button.cpp | 6 ++---- .../seeed_mr24hpc1/button/custom_mode_end_button.h | 6 ++---- .../components/seeed_mr24hpc1/button/restart_button.cpp | 6 ++---- esphome/components/seeed_mr24hpc1/button/restart_button.h | 6 ++---- .../seeed_mr24hpc1/number/custom_mode_number.cpp | 6 ++---- .../components/seeed_mr24hpc1/number/custom_mode_number.h | 6 ++---- .../seeed_mr24hpc1/number/custom_unman_time_number.cpp | 6 ++---- .../seeed_mr24hpc1/number/custom_unman_time_number.h | 6 ++---- .../seeed_mr24hpc1/number/existence_threshold_number.cpp | 6 ++---- .../seeed_mr24hpc1/number/existence_threshold_number.h | 6 ++---- .../seeed_mr24hpc1/number/motion_threshold_number.cpp | 6 ++---- .../seeed_mr24hpc1/number/motion_threshold_number.h | 6 ++---- .../seeed_mr24hpc1/number/motion_trigger_time_number.cpp | 6 ++---- .../seeed_mr24hpc1/number/motion_trigger_time_number.h | 6 ++---- .../seeed_mr24hpc1/number/motiontorest_time_number.cpp | 6 ++---- .../seeed_mr24hpc1/number/motiontorest_time_number.h | 6 ++---- .../seeed_mr24hpc1/number/sensitivity_number.cpp | 6 ++---- .../components/seeed_mr24hpc1/number/sensitivity_number.h | 6 ++---- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 6 ++---- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h | 6 ++---- .../components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h | 6 ++---- .../seeed_mr24hpc1/select/existence_boundary_select.cpp | 6 ++---- .../seeed_mr24hpc1/select/existence_boundary_select.h | 6 ++---- .../seeed_mr24hpc1/select/motion_boundary_select.cpp | 6 ++---- .../seeed_mr24hpc1/select/motion_boundary_select.h | 6 ++---- .../seeed_mr24hpc1/select/scene_mode_select.cpp | 6 ++---- .../components/seeed_mr24hpc1/select/scene_mode_select.h | 6 ++---- .../seeed_mr24hpc1/select/unman_time_select.cpp | 6 ++---- .../components/seeed_mr24hpc1/select/unman_time_select.h | 6 ++---- .../seeed_mr24hpc1/switch/underlyFuc_switch.cpp | 6 ++---- .../components/seeed_mr24hpc1/switch/underlyFuc_switch.h | 6 ++---- esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp | 6 ++---- esphome/components/seeed_mr60bha2/seeed_mr60bha2.h | 6 ++---- .../seeed_mr60fda2/button/get_radar_parameters_button.cpp | 6 ++---- .../seeed_mr60fda2/button/get_radar_parameters_button.h | 6 ++---- .../seeed_mr60fda2/button/reset_radar_button.cpp | 6 ++---- .../components/seeed_mr60fda2/button/reset_radar_button.h | 6 ++---- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 6 ++---- esphome/components/seeed_mr60fda2/seeed_mr60fda2.h | 6 ++---- .../seeed_mr60fda2/select/height_threshold_select.cpp | 6 ++---- .../seeed_mr60fda2/select/height_threshold_select.h | 6 ++---- .../seeed_mr60fda2/select/install_height_select.cpp | 6 ++---- .../seeed_mr60fda2/select/install_height_select.h | 6 ++---- .../seeed_mr60fda2/select/sensitivity_select.cpp | 6 ++---- .../components/seeed_mr60fda2/select/sensitivity_select.h | 6 ++---- esphome/components/selec_meter/selec_meter.cpp | 6 ++---- esphome/components/selec_meter/selec_meter.h | 6 ++---- esphome/components/selec_meter/selec_meter_registers.h | 6 ++---- esphome/components/sen0321/sen0321.cpp | 6 ++---- esphome/components/sen0321/sen0321.h | 6 ++---- esphome/components/sen21231/sen21231.cpp | 6 ++---- esphome/components/sen21231/sen21231.h | 6 ++---- esphome/components/sen5x/automation.h | 6 ++---- esphome/components/sen5x/sen5x.cpp | 6 ++---- esphome/components/sen5x/sen5x.h | 6 ++---- esphome/components/senseair/senseair.cpp | 6 ++---- esphome/components/senseair/senseair.h | 6 ++---- esphome/components/sensirion_common/i2c_sensirion.cpp | 6 ++---- esphome/components/sensirion_common/i2c_sensirion.h | 6 ++---- esphome/components/servo/servo.cpp | 6 ++---- esphome/components/servo/servo.h | 6 ++---- esphome/components/sfa30/sfa30.cpp | 6 ++---- esphome/components/sfa30/sfa30.h | 6 ++---- esphome/components/sgp30/sgp30.cpp | 6 ++---- esphome/components/sgp30/sgp30.h | 6 ++---- esphome/components/sgp4x/sgp4x.cpp | 6 ++---- esphome/components/sgp4x/sgp4x.h | 6 ++---- esphome/components/sht3xd/sht3xd.cpp | 6 ++---- esphome/components/sht3xd/sht3xd.h | 6 ++---- esphome/components/sht4x/sht4x.cpp | 6 ++---- esphome/components/sht4x/sht4x.h | 6 ++---- esphome/components/shutdown/button/shutdown_button.cpp | 6 ++---- esphome/components/shutdown/button/shutdown_button.h | 6 ++---- esphome/components/shutdown/switch/shutdown_switch.cpp | 6 ++---- esphome/components/shutdown/switch/shutdown_switch.h | 6 ++---- .../components/sigma_delta_output/sigma_delta_output.cpp | 6 ++---- .../components/sigma_delta_output/sigma_delta_output.h | 6 ++---- esphome/components/sim800l/sim800l.cpp | 6 ++---- esphome/components/sim800l/sim800l.h | 6 ++---- esphome/components/slow_pwm/slow_pwm_output.cpp | 6 ++---- esphome/components/slow_pwm/slow_pwm_output.h | 6 ++---- esphome/components/sm10bit_base/sm10bit_base.cpp | 6 ++---- esphome/components/sm10bit_base/sm10bit_base.h | 6 ++---- esphome/components/sm16716/sm16716.cpp | 6 ++---- esphome/components/sm16716/sm16716.h | 6 ++---- esphome/components/sm2135/sm2135.cpp | 6 ++---- esphome/components/sm2135/sm2135.h | 6 ++---- esphome/components/sm2235/sm2235.cpp | 6 ++---- esphome/components/sm2235/sm2235.h | 6 ++---- esphome/components/sm2335/sm2335.cpp | 6 ++---- esphome/components/sm2335/sm2335.h | 6 ++---- esphome/components/sm300d2/sm300d2.cpp | 6 ++---- esphome/components/sm300d2/sm300d2.h | 6 ++---- esphome/components/sml/constants.h | 6 ++---- esphome/components/sml/sensor/sml_sensor.cpp | 6 ++---- esphome/components/sml/sensor/sml_sensor.h | 6 ++---- esphome/components/sml/sml.cpp | 6 ++---- esphome/components/sml/sml.h | 6 ++---- esphome/components/sml/sml_parser.cpp | 6 ++---- esphome/components/sml/sml_parser.h | 6 ++---- esphome/components/sml/text_sensor/sml_text_sensor.cpp | 6 ++---- esphome/components/sml/text_sensor/sml_text_sensor.h | 6 ++---- esphome/components/smt100/smt100.cpp | 6 ++---- esphome/components/smt100/smt100.h | 6 ++---- esphome/components/sn74hc165/sn74hc165.cpp | 6 ++---- esphome/components/sn74hc165/sn74hc165.h | 6 ++---- esphome/components/sn74hc595/sn74hc595.cpp | 6 ++---- esphome/components/sn74hc595/sn74hc595.h | 6 ++---- esphome/components/sntp/sntp_component.cpp | 6 ++---- esphome/components/sntp/sntp_component.h | 6 ++---- esphome/components/sonoff_d1/sonoff_d1.cpp | 6 ++---- esphome/components/sonoff_d1/sonoff_d1.h | 6 ++---- esphome/components/sound_level/sound_level.cpp | 6 ++---- esphome/components/sound_level/sound_level.h | 7 +++---- esphome/components/speaker/automation.h | 6 ++---- .../components/speaker/media_player/audio_pipeline.cpp | 6 ++---- esphome/components/speaker/media_player/audio_pipeline.h | 6 ++---- esphome/components/speaker/media_player/automation.h | 6 ++---- .../speaker/media_player/speaker_media_player.cpp | 6 ++---- .../speaker/media_player/speaker_media_player.h | 6 ++---- esphome/components/speaker/speaker.h | 6 ++---- esphome/components/speed/fan/speed_fan.cpp | 6 ++---- esphome/components/speed/fan/speed_fan.h | 6 ++---- esphome/components/spi_device/spi_device.cpp | 6 ++---- esphome/components/spi_device/spi_device.h | 6 ++---- esphome/components/spi_led_strip/spi_led_strip.cpp | 6 ++---- esphome/components/spi_led_strip/spi_led_strip.h | 6 ++---- esphome/components/sps30/automation.h | 6 ++---- esphome/components/sps30/sps30.cpp | 6 ++---- esphome/components/sps30/sps30.h | 6 ++---- esphome/components/ssd1306_base/ssd1306_base.cpp | 6 ++---- esphome/components/ssd1306_base/ssd1306_base.h | 6 ++---- esphome/components/ssd1306_i2c/ssd1306_i2c.cpp | 6 ++---- esphome/components/ssd1306_i2c/ssd1306_i2c.h | 6 ++---- esphome/components/ssd1306_spi/ssd1306_spi.cpp | 6 ++---- esphome/components/ssd1306_spi/ssd1306_spi.h | 6 ++---- esphome/components/ssd1322_base/ssd1322_base.cpp | 6 ++---- esphome/components/ssd1322_base/ssd1322_base.h | 6 ++---- esphome/components/ssd1322_spi/ssd1322_spi.cpp | 6 ++---- esphome/components/ssd1322_spi/ssd1322_spi.h | 6 ++---- esphome/components/ssd1325_base/ssd1325_base.cpp | 6 ++---- esphome/components/ssd1325_base/ssd1325_base.h | 6 ++---- esphome/components/ssd1325_spi/ssd1325_spi.cpp | 6 ++---- esphome/components/ssd1325_spi/ssd1325_spi.h | 6 ++---- esphome/components/ssd1327_base/ssd1327_base.cpp | 6 ++---- esphome/components/ssd1327_base/ssd1327_base.h | 6 ++---- esphome/components/ssd1327_i2c/ssd1327_i2c.cpp | 6 ++---- esphome/components/ssd1327_i2c/ssd1327_i2c.h | 6 ++---- esphome/components/ssd1327_spi/ssd1327_spi.cpp | 6 ++---- esphome/components/ssd1327_spi/ssd1327_spi.h | 6 ++---- esphome/components/ssd1331_base/ssd1331_base.cpp | 6 ++---- esphome/components/ssd1331_base/ssd1331_base.h | 6 ++---- esphome/components/ssd1331_spi/ssd1331_spi.cpp | 6 ++---- esphome/components/ssd1331_spi/ssd1331_spi.h | 6 ++---- esphome/components/ssd1351_base/ssd1351_base.cpp | 6 ++---- esphome/components/ssd1351_base/ssd1351_base.h | 6 ++---- esphome/components/ssd1351_spi/ssd1351_spi.cpp | 6 ++---- esphome/components/ssd1351_spi/ssd1351_spi.h | 6 ++---- esphome/components/st7567_base/st7567_base.cpp | 6 ++---- esphome/components/st7567_base/st7567_base.h | 6 ++---- esphome/components/st7567_i2c/st7567_i2c.cpp | 6 ++---- esphome/components/st7567_i2c/st7567_i2c.h | 6 ++---- esphome/components/st7567_spi/st7567_spi.cpp | 6 ++---- esphome/components/st7567_spi/st7567_spi.h | 6 ++---- esphome/components/st7735/st7735.cpp | 6 ++---- esphome/components/st7735/st7735.h | 6 ++---- esphome/components/st7789v/st7789v.cpp | 6 ++---- esphome/components/st7789v/st7789v.h | 6 ++---- esphome/components/st7920/st7920.cpp | 6 ++---- esphome/components/st7920/st7920.h | 6 ++---- esphome/components/statsd/statsd.cpp | 8 ++++---- esphome/components/statsd/statsd.h | 7 +++---- esphome/components/status_led/light/status_led_light.cpp | 6 ++---- esphome/components/status_led/light/status_led_light.h | 6 ++---- esphome/components/status_led/status_led.cpp | 6 ++---- esphome/components/status_led/status_led.h | 6 ++---- esphome/components/stepper/stepper.cpp | 6 ++---- esphome/components/stepper/stepper.h | 6 ++---- esphome/components/sts3x/sts3x.cpp | 6 ++---- esphome/components/sts3x/sts3x.h | 6 ++---- esphome/components/sun/sensor/sun_sensor.cpp | 6 ++---- esphome/components/sun/sensor/sun_sensor.h | 6 ++---- esphome/components/sun/sun.cpp | 6 ++---- esphome/components/sun/sun.h | 6 ++---- esphome/components/sun/text_sensor/sun_text_sensor.cpp | 6 ++---- esphome/components/sun/text_sensor/sun_text_sensor.h | 6 ++---- esphome/components/sun_gtil2/sun_gtil2.cpp | 6 ++---- esphome/components/sun_gtil2/sun_gtil2.h | 6 ++---- esphome/components/sx126x/automation.h | 6 ++---- .../sx126x/packet_transport/sx126x_transport.cpp | 6 ++---- .../components/sx126x/packet_transport/sx126x_transport.h | 6 ++---- esphome/components/sx126x/sx126x.cpp | 6 ++---- esphome/components/sx126x/sx126x.h | 6 ++---- esphome/components/sx126x/sx126x_reg.h | 6 ++---- esphome/components/sx127x/automation.h | 6 ++---- .../sx127x/packet_transport/sx127x_transport.cpp | 6 ++---- .../components/sx127x/packet_transport/sx127x_transport.h | 6 ++---- esphome/components/sx127x/sx127x.cpp | 6 ++---- esphome/components/sx127x/sx127x.h | 6 ++---- esphome/components/sx127x/sx127x_reg.h | 6 ++---- .../sx1509/binary_sensor/sx1509_binary_keypad_sensor.h | 6 ++---- esphome/components/sx1509/output/sx1509_float_output.cpp | 6 ++---- esphome/components/sx1509/output/sx1509_float_output.h | 6 ++---- esphome/components/sx1509/sx1509.cpp | 6 ++---- esphome/components/sx1509/sx1509.h | 6 ++---- esphome/components/sx1509/sx1509_gpio_pin.cpp | 6 ++---- esphome/components/sx1509/sx1509_gpio_pin.h | 6 ++---- esphome/components/sx1509/sx1509_registers.h | 6 ++---- 227 files changed, 459 insertions(+), 908 deletions(-) diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp index bb5b64daf76..04203854fb5 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.cpp +++ b/esphome/components/safe_mode/button/safe_mode_button.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace safe_mode { +namespace esphome::safe_mode { static const char *const TAG = "safe_mode.button"; @@ -23,5 +22,4 @@ void SafeModeButton::press_action() { void SafeModeButton::dump_config() { LOG_BUTTON("", "Safe Mode Button", this); } -} // namespace safe_mode -} // namespace esphome +} // namespace esphome::safe_mode diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h index 0307a81febd..6012bb2aebd 100644 --- a/esphome/components/safe_mode/button/safe_mode_button.h +++ b/esphome/components/safe_mode/button/safe_mode_button.h @@ -4,8 +4,7 @@ #include "esphome/components/safe_mode/safe_mode.h" #include "esphome/core/component.h" -namespace esphome { -namespace safe_mode { +namespace esphome::safe_mode { class SafeModeButton final : public button::Button, public Component { public: @@ -17,5 +16,4 @@ class SafeModeButton final : public button::Button, public Component { void press_action() override; }; -} // namespace safe_mode -} // namespace esphome +} // namespace esphome::safe_mode diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp index 1637da3059d..f513465db0d 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp +++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace safe_mode { +namespace esphome::safe_mode { static const char *const TAG = "safe_mode.switch"; @@ -28,5 +27,4 @@ void SafeModeSwitch::write_state(bool state) { void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); } -} // namespace safe_mode -} // namespace esphome +} // namespace esphome::safe_mode diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h index 24e660c8032..c73a2087d79 100644 --- a/esphome/components/safe_mode/switch/safe_mode_switch.h +++ b/esphome/components/safe_mode/switch/safe_mode_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "esphome/core/component.h" -namespace esphome { -namespace safe_mode { +namespace esphome::safe_mode { class SafeModeSwitch : public switch_::Switch, public Component { public: @@ -17,5 +16,4 @@ class SafeModeSwitch : public switch_::Switch, public Component { void write_state(bool state) override; }; -} // namespace safe_mode -} // namespace esphome +} // namespace esphome::safe_mode diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h index 1f89e7c8159..1f047398933 100644 --- a/esphome/components/scd30/automation.h +++ b/esphome/components/scd30/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "scd30.h" -namespace esphome { -namespace scd30 { +namespace esphome::scd30 { template<typename... Ts> class ForceRecalibrationWithReference : public Action<Ts...>, public Parented<SCD30Component> { public: @@ -19,5 +18,4 @@ template<typename... Ts> class ForceRecalibrationWithReference : public Action<T TEMPLATABLE_VALUE(uint16_t, value) }; -} // namespace scd30 -} // namespace esphome +} // namespace esphome::scd30 diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index e61d0142be6..a7802977562 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -6,8 +6,7 @@ #include <Wire.h> #endif -namespace esphome { -namespace scd30 { +namespace esphome::scd30 { static const char *const TAG = "scd30"; @@ -230,5 +229,4 @@ uint16_t SCD30Component::get_forced_calibration_reference() { return forced_calibration_reference; } -} // namespace scd30 -} // namespace esphome +} // namespace esphome::scd30 diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index ed3f5e7e9aa..a5a5df1903a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace scd30 { +namespace esphome::scd30 { /// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. class SCD30Component : public Component, public sensirion_common::SensirionI2CDevice { @@ -48,5 +47,4 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe sensor::Sensor *temperature_sensor_{nullptr}; }; -} // namespace scd30 -} // namespace esphome +} // namespace esphome::scd30 diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index 6ce14685770..e485289c95b 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "scd4x.h" -namespace esphome { -namespace scd4x { +namespace esphome::scd4x { template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> { public: @@ -24,5 +23,4 @@ template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public void play(const Ts &...x) override { this->parent_->factory_reset(); } }; -} // namespace scd4x -} // namespace esphome +} // namespace esphome::scd4x diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 0c108fba9d6..d9a2439bb90 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace scd4x { +namespace esphome::scd4x { static const char *const TAG = "scd4x"; @@ -324,5 +323,4 @@ bool SCD4XComponent::start_measurement_() { return false; } -} // namespace scd4x -} // namespace esphome +} // namespace esphome::scd4x diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index ab5d72aeec2..3e4827ef141 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace scd4x { +namespace esphome::scd4x { enum ErrorCode : uint8_t { COMMUNICATION_FAILED, @@ -59,5 +58,4 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri MeasurementMode measurement_mode_{PERIODIC}; }; -} // namespace scd4x -} // namespace esphome +} // namespace esphome::scd4x diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp index 81f652d26a0..61bca5bc281 100644 --- a/esphome/components/script/script.cpp +++ b/esphome/components/script/script.cpp @@ -1,8 +1,7 @@ #include "script.h" #include "esphome/core/log.h" -namespace esphome { -namespace script { +namespace esphome::script { static const char *const TAG = "script"; @@ -16,5 +15,4 @@ void ScriptLogger::esp_log_(int level, int line, const char *format, const char } #endif -} // namespace script -} // namespace esphome +} // namespace esphome::script diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index a0dffe26bf2..847fab02bd2 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -7,8 +7,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace script { + +namespace esphome::script { class ScriptLogger { protected: @@ -338,5 +338,4 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>, std::list<std::tuple<Ts...>> param_queue_; }; -} // namespace script -} // namespace esphome +} // namespace esphome::script diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index 12a3277269e..a4fe6e7d356 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace sdm_meter { +namespace esphome::sdm_meter { static const char *const TAG = "sdm_meter"; @@ -110,5 +109,4 @@ void SDMMeter::dump_config() { LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); } -} // namespace sdm_meter -} // namespace esphome +} // namespace esphome::sdm_meter diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index f8a3014a892..e729e29d6c0 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace sdm_meter { +namespace esphome::sdm_meter { class SDMMeter : public PollingComponent, public modbus::ModbusDevice { public: @@ -79,5 +78,4 @@ class SDMMeter : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *export_reactive_energy_sensor_{nullptr}; }; -} // namespace sdm_meter -} // namespace esphome +} // namespace esphome::sdm_meter diff --git a/esphome/components/sdm_meter/sdm_meter_registers.h b/esphome/components/sdm_meter/sdm_meter_registers.h index dd981d6f003..b4b2855576c 100644 --- a/esphome/components/sdm_meter/sdm_meter_registers.h +++ b/esphome/components/sdm_meter/sdm_meter_registers.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace sdm_meter { +namespace esphome::sdm_meter { /* PHASE STATUS REGISTERS */ static const uint16_t SDM_PHASE_1_VOLTAGE = 0x0000; @@ -110,5 +109,4 @@ static const uint16_t SDM_CURRENT_RESETTABLE_EXPORT_ENERGY = 0x0186; static const uint16_t SDM_IMPORT_POWER = 0x0500; static const uint16_t SDM_EXPORT_POWER = 0x0502; -} // namespace sdm_meter -} // namespace esphome +} // namespace esphome::sdm_meter diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index 6f6cc1ebd8f..7fcd47a265e 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace sdp3x { +namespace esphome::sdp3x { static const char *const TAG = "sdp3x.sensor"; static const uint16_t SDP3X_SOFT_RESET = 0x0006; @@ -114,5 +113,4 @@ void SDP3XComponent::read_pressure_() { this->status_clear_warning(); } -} // namespace sdp3x -} // namespace esphome +} // namespace esphome::sdp3x diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index afb58d47c8f..c4ef6a4a1e8 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace sdp3x { +namespace esphome::sdp3x { enum MeasurementMode { MASS_FLOW_AVG, DP_AVG }; @@ -25,5 +24,4 @@ class SDP3XComponent : public PollingComponent, public sensirion_common::Sensiri MeasurementMode measurement_mode_; }; -} // namespace sdp3x -} // namespace esphome +} // namespace esphome::sdp3x diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index cdfd7544ad8..b1f89f18bfa 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace sds011 { +namespace esphome::sds011 { static const char *const TAG = "sds011"; @@ -184,5 +183,4 @@ void SDS011Component::set_update_interval_min(uint8_t update_interval_min) { this->update_interval_min_ = update_interval_min; } -} // namespace sds011 -} // namespace esphome +} // namespace esphome::sds011 diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index d65299c6350..56d46d118f8 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace sds011 { +namespace esphome::sds011 { class SDS011Component : public Component, public uart::UARTDevice { public: @@ -44,5 +43,4 @@ class SDS011Component : public Component, public uart::UARTDevice { bool rx_mode_only_; }; -} // namespace sds011 -} // namespace esphome +} // namespace esphome::sds011 diff --git a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp index 0ae88892479..aec940dd22e 100644 --- a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp +++ b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.cpp @@ -1,9 +1,7 @@ #include "custom_mode_end_button.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void CustomSetEndButton::press_action() { this->parent_->set_custom_end_mode(); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h index a1701d8581b..bc98bb93b68 100644 --- a/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h +++ b/esphome/components/seeed_mr24hpc1/button/custom_mode_end_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class CustomSetEndButton : public button::Button, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class CustomSetEndButton : public button::Button, public Parented<MR24HPC1Compon void press_action() override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/button/restart_button.cpp b/esphome/components/seeed_mr24hpc1/button/restart_button.cpp index 6c4a070d1cf..f32056ee1ba 100644 --- a/esphome/components/seeed_mr24hpc1/button/restart_button.cpp +++ b/esphome/components/seeed_mr24hpc1/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void RestartButton::press_action() { this->parent_->set_restart(); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/button/restart_button.h b/esphome/components/seeed_mr24hpc1/button/restart_button.h index 8a2ec2087ca..49a4f46138e 100644 --- a/esphome/components/seeed_mr24hpc1/button/restart_button.h +++ b/esphome/components/seeed_mr24hpc1/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class RestartButton : public button::Button, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented<MR24HPC1Component> void press_action() override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp index 0aebd8fb9fe..08a8076e227 100644 --- a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.cpp @@ -1,12 +1,10 @@ #include "custom_mode_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void CustomModeNumber::control(float value) { this->publish_state(value); this->parent_->set_custom_mode(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h index 40ff3f201ac..f51e592fc00 100644 --- a/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h +++ b/esphome/components/seeed_mr24hpc1/number/custom_mode_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class CustomModeNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class CustomModeNumber : public number::Number, public Parented<MR24HPC1Componen void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp index 12a8ff69fa5..ea8ec2e2e07 100644 --- a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.cpp @@ -1,9 +1,7 @@ #include "custom_unman_time_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void CustomUnmanTimeNumber::control(float value) { this->parent_->set_custom_unman_time(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h index 6b871c4c134..281e727a365 100644 --- a/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h +++ b/esphome/components/seeed_mr24hpc1/number/custom_unman_time_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class CustomUnmanTimeNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class CustomUnmanTimeNumber : public number::Number, public Parented<MR24HPC1Com void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp index 58ef56509ef..8168530c194 100644 --- a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.cpp @@ -1,9 +1,7 @@ #include "existence_threshold_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void ExistenceThresholdNumber::control(float value) { this->parent_->set_existence_threshold(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h index 656bad17deb..c811b2d6b69 100644 --- a/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h +++ b/esphome/components/seeed_mr24hpc1/number/existence_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class ExistenceThresholdNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class ExistenceThresholdNumber : public number::Number, public Parented<MR24HPC1 void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp index d252b4f01dd..00abcd0cc7a 100644 --- a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.cpp @@ -1,9 +1,7 @@ #include "motion_threshold_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void MotionThresholdNumber::control(float value) { this->parent_->set_motion_threshold(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h index e8ae37b96f0..748119f1985 100644 --- a/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h +++ b/esphome/components/seeed_mr24hpc1/number/motion_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class MotionThresholdNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class MotionThresholdNumber : public number::Number, public Parented<MR24HPC1Com void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp index fc7659dc54b..637fc3c78e1 100644 --- a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.cpp @@ -1,9 +1,7 @@ #include "motion_trigger_time_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void MotionTriggerTimeNumber::control(float value) { this->parent_->set_motion_trigger_time(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h index 996356e2371..dd7947b2a5a 100644 --- a/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h +++ b/esphome/components/seeed_mr24hpc1/number/motion_trigger_time_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class MotionTriggerTimeNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class MotionTriggerTimeNumber : public number::Number, public Parented<MR24HPC1C void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp index f598e6686c6..069b0618c11 100644 --- a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.cpp @@ -1,9 +1,7 @@ #include "motiontorest_time_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void MotionToRestTimeNumber::control(float value) { this->parent_->set_motion_to_rest_time(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h index 559d23fdeb7..47493e79542 100644 --- a/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h +++ b/esphome/components/seeed_mr24hpc1/number/motiontorest_time_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class MotionToRestTimeNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class MotionToRestTimeNumber : public number::Number, public Parented<MR24HPC1Co void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp index d903dfd8187..3dcd61a1486 100644 --- a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp +++ b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.cpp @@ -1,9 +1,7 @@ #include "sensitivity_number.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void SensitivityNumber::control(float value) { this->parent_->set_sensitivity(value); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h index fee33521d04..c1d54351517 100644 --- a/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h +++ b/esphome/components/seeed_mr24hpc1/number/sensitivity_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class SensitivityNumber : public number::Number, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class SensitivityNumber : public number::Number, public Parented<MR24HPC1Compone void control(float value) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index b44c5ce83d3..a0dc7ae4cb1 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -5,8 +5,7 @@ #include <utility> -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { static const char *const TAG = "seeed_mr24hpc1"; @@ -1002,5 +1001,4 @@ void MR24HPC1Component::set_custom_unman_time(uint16_t value) { this->get_custom_unman_time(); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h index 8fc61ad37c7..b62504ba0eb 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.h @@ -30,8 +30,7 @@ #include <map> -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { enum FrameState { FRAME_IDLE, @@ -213,5 +212,4 @@ class MR24HPC1Component : public Component, void set_custom_unman_time(uint16_t value); }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h index dafc6c03680..7ed7e1db940 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1_constants.h @@ -2,8 +2,7 @@ #include <cstdint> -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { static const uint8_t FRAME_BUF_MAX_SIZE = 128; static const uint8_t PRODUCT_BUF_MAX_SIZE = 32; @@ -169,5 +168,4 @@ static const uint8_t GET_KEEP_AWAY[] = { FRAME_TAIL1_VALUE, FRAME_TAIL2_VALUE, }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp index 81543055a48..af01152e487 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp @@ -1,12 +1,10 @@ #include "existence_boundary_select.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void ExistenceBoundarySelect::control(size_t index) { this->publish_state(index); this->parent_->set_existence_boundary(index); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h index 933279dd13e..878d0525c90 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class ExistenceBoundarySelect : public select::Select, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class ExistenceBoundarySelect : public select::Select, public Parented<MR24HPC1C void control(size_t index) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp index 3ba87c44de9..b13fb930569 100644 --- a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.cpp @@ -1,12 +1,10 @@ #include "motion_boundary_select.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void MotionBoundarySelect::control(size_t index) { this->publish_state(index); this->parent_->set_motion_boundary(index); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h index b0051ae6b15..eecdef2019f 100644 --- a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class MotionBoundarySelect : public select::Select, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class MotionBoundarySelect : public select::Select, public Parented<MR24HPC1Comp void control(size_t index) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp index 3849e1ecf9d..8b6eb813c72 100644 --- a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.cpp @@ -1,12 +1,10 @@ #include "scene_mode_select.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void SceneModeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_scene_mode(index); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h index f478ea5b669..377c61b32f4 100644 --- a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class SceneModeSelect : public select::Select, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class SceneModeSelect : public select::Select, public Parented<MR24HPC1Component void control(size_t index) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp b/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp index 541ed74085f..5d4b0204240 100644 --- a/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.cpp @@ -1,12 +1,10 @@ #include "unman_time_select.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void UnmanTimeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_unman_time(index); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h index a64ff4b8406..e68ae5e54fd 100644 --- a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class UnmanTimeSelect : public select::Select, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class UnmanTimeSelect : public select::Select, public Parented<MR24HPC1Component void control(size_t index) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp index 0fcc49bc4c3..d4691102915 100644 --- a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp +++ b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.cpp @@ -1,12 +1,10 @@ #include "underlyFuc_switch.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { void UnderlyOpenFunctionSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_underlying_open_function(state); } -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h index 1baabb25ce7..3224640ce7b 100644 --- a/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h +++ b/esphome/components/seeed_mr24hpc1/switch/underlyFuc_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../seeed_mr24hpc1.h" -namespace esphome { -namespace seeed_mr24hpc1 { +namespace esphome::seeed_mr24hpc1 { class UnderlyOpenFunctionSwitch : public switch_::Switch, public Parented<MR24HPC1Component> { public: @@ -14,5 +13,4 @@ class UnderlyOpenFunctionSwitch : public switch_::Switch, public Parented<MR24HP void write_state(bool state) override; }; -} // namespace seeed_mr24hpc1 -} // namespace esphome +} // namespace esphome::seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 1b5eaf63679..cbf0415f6c8 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -5,8 +5,7 @@ #include <cinttypes> #include <utility> -namespace esphome { -namespace seeed_mr60bha2 { +namespace esphome::seeed_mr60bha2 { static const char *const TAG = "seeed_mr60bha2"; @@ -219,5 +218,4 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } } -} // namespace seeed_mr60bha2 -} // namespace esphome +} // namespace esphome::seeed_mr60bha2 diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h index d20c8e50cc0..008acc6a57d 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h @@ -13,8 +13,7 @@ #include <map> -namespace esphome { -namespace seeed_mr60bha2 { +namespace esphome::seeed_mr60bha2 { static const uint8_t FRAME_HEADER_BUFFER = 0x01; static const uint16_t BREATH_RATE_TYPE_BUFFER = 0x0A14; static const uint16_t PEOPLE_EXIST_TYPE_BUFFER = 0x0F09; @@ -46,5 +45,4 @@ class MR60BHA2Component : public Component, std::vector<uint8_t> rx_message_; }; -} // namespace seeed_mr60bha2 -} // namespace esphome +} // namespace esphome::seeed_mr60bha2 diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp index 88be6dfe7c6..c377128fe6f 100644 --- a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp +++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp @@ -1,9 +1,7 @@ #include "get_radar_parameters_button.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { void GetRadarParametersButton::press_action() { this->parent_->get_radar_parameters(); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h index 9d6d507383a..c1b96d5f083 100644 --- a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h +++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../seeed_mr60fda2.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { class GetRadarParametersButton : public button::Button, public Parented<MR60FDA2Component> { public: @@ -14,5 +13,4 @@ class GetRadarParametersButton : public button::Button, public Parented<MR60FDA2 void press_action() override; }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp index 0a5833a18cb..6c928be4016 100644 --- a/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp +++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp @@ -1,9 +1,7 @@ #include "reset_radar_button.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { void ResetRadarButton::press_action() { this->parent_->factory_reset(); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.h b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h index 66780fb8af8..174ef5425e5 100644 --- a/esphome/components/seeed_mr60fda2/button/reset_radar_button.h +++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../seeed_mr60fda2.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { class ResetRadarButton : public button::Button, public Parented<MR60FDA2Component> { public: @@ -14,5 +13,4 @@ class ResetRadarButton : public button::Button, public Parented<MR60FDA2Componen void press_action() override; }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index e24e9b338e6..4875aa5cff9 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -5,8 +5,7 @@ #include <cinttypes> #include <utility> -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { static const char *const TAG = "seeed_mr60fda2"; @@ -393,5 +392,4 @@ void MR60FDA2Component::factory_reset() { this->get_radar_parameters(); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h index e1ffa4f0715..0e97447074f 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h @@ -19,8 +19,7 @@ #include <map> -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { static const uint8_t DATA_BUF_MAX_SIZE = 28; static const uint8_t FRAME_BUF_MAX_SIZE = 37; @@ -97,5 +96,4 @@ class MR60FDA2Component : public Component, void factory_reset(); }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp index c963ccdadd4..09cb9c4c1c4 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp @@ -1,12 +1,10 @@ #include "height_threshold_select.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { void HeightThresholdSelect::control(size_t index) { this->publish_state(index); this->parent_->set_height_threshold(index); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h index f5707c7a880..0e495766588 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr60fda2.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { class HeightThresholdSelect : public select::Select, public Parented<MR60FDA2Component> { public: @@ -14,5 +13,4 @@ class HeightThresholdSelect : public select::Select, public Parented<MR60FDA2Com void control(size_t index) override; }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.cpp b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp index 88b86adf435..f2fa71c66b9 100644 --- a/esphome/components/seeed_mr60fda2/select/install_height_select.cpp +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp @@ -1,12 +1,10 @@ #include "install_height_select.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { void InstallHeightSelect::control(size_t index) { this->publish_state(index); this->parent_->set_install_height(index); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h index 470d96c50cd..c1e2a3eeb1e 100644 --- a/esphome/components/seeed_mr60fda2/select/install_height_select.h +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr60fda2.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { class InstallHeightSelect : public select::Select, public Parented<MR60FDA2Component> { public: @@ -14,5 +13,4 @@ class InstallHeightSelect : public select::Select, public Parented<MR60FDA2Compo void control(size_t index) override; }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp index 3e84725bb0d..35df7b9cbbe 100644 --- a/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp @@ -1,12 +1,10 @@ #include "sensitivity_select.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { void SensitivitySelect::control(size_t index) { this->publish_state(index); this->parent_->set_sensitivity(index); } -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h index 82ed4c5d79b..f2e0307dc10 100644 --- a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../seeed_mr60fda2.h" -namespace esphome { -namespace seeed_mr60fda2 { +namespace esphome::seeed_mr60fda2 { class SensitivitySelect : public select::Select, public Parented<MR60FDA2Component> { public: @@ -14,5 +13,4 @@ class SensitivitySelect : public select::Select, public Parented<MR60FDA2Compone void control(size_t index) override; }; -} // namespace seeed_mr60fda2 -} // namespace esphome +} // namespace esphome::seeed_mr60fda2 diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp index ae41327896e..f612b89934c 100644 --- a/esphome/components/selec_meter/selec_meter.cpp +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace selec_meter { +namespace esphome::selec_meter { static const char *const TAG = "selec_meter"; @@ -107,5 +106,4 @@ void SelecMeter::dump_config() { LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_); } -} // namespace selec_meter -} // namespace esphome +} // namespace esphome::selec_meter diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h index 730791c91b2..159acab124f 100644 --- a/esphome/components/selec_meter/selec_meter.h +++ b/esphome/components/selec_meter/selec_meter.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace selec_meter { +namespace esphome::selec_meter { #define SELEC_METER_SENSOR(name) \ protected: \ @@ -43,5 +42,4 @@ class SelecMeter : public PollingComponent, public modbus::ModbusDevice { void dump_config() override; }; -} // namespace selec_meter -} // namespace esphome +} // namespace esphome::selec_meter diff --git a/esphome/components/selec_meter/selec_meter_registers.h b/esphome/components/selec_meter/selec_meter_registers.h index dfaf65ff083..d299560aaba 100644 --- a/esphome/components/selec_meter/selec_meter_registers.h +++ b/esphome/components/selec_meter/selec_meter_registers.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace selec_meter { +namespace esphome::selec_meter { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; @@ -28,5 +27,4 @@ static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C; static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E; static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020; -} // namespace selec_meter -} // namespace esphome +} // namespace esphome::selec_meter diff --git a/esphome/components/sen0321/sen0321.cpp b/esphome/components/sen0321/sen0321.cpp index 6a5931272dc..e074934d0f0 100644 --- a/esphome/components/sen0321/sen0321.cpp +++ b/esphome/components/sen0321/sen0321.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace sen0321_sensor { +namespace esphome::sen0321_sensor { static const char *const TAG = "sen0321_sensor.sensor"; @@ -31,5 +30,4 @@ void Sen0321Sensor::read_data_() { this->publish_state(((uint16_t) (result[0] << 8) + result[1])); } -} // namespace sen0321_sensor -} // namespace esphome +} // namespace esphome::sen0321_sensor diff --git a/esphome/components/sen0321/sen0321.h b/esphome/components/sen0321/sen0321.h index 3bb3d5b0154..6d5aa20a610 100644 --- a/esphome/components/sen0321/sen0321.h +++ b/esphome/components/sen0321/sen0321.h @@ -7,8 +7,7 @@ // ref: // https://github.com/DFRobot/DFRobot_OzoneSensor -namespace esphome { -namespace sen0321_sensor { +namespace esphome::sen0321_sensor { // Sensor Mode // While passive is supposedly supported, it does not appear to work reliably. static const uint8_t SENSOR_MODE_REGISTER = 0x03; @@ -31,5 +30,4 @@ class Sen0321Sensor : public sensor::Sensor, public PollingComponent, public i2c void read_data_(); }; -} // namespace sen0321_sensor -} // namespace esphome +} // namespace esphome::sen0321_sensor diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index 8c9f3d71346..b42ba2fa1dc 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -1,8 +1,7 @@ #include "sen21231.h" #include "esphome/core/log.h" -namespace esphome { -namespace sen21231_sensor { +namespace esphome::sen21231_sensor { static const char *const TAG = "sen21231_sensor.sensor"; @@ -33,5 +32,4 @@ void Sen21231Sensor::read_data_() { } } -} // namespace sen21231_sensor -} // namespace esphome +} // namespace esphome::sen21231_sensor diff --git a/esphome/components/sen21231/sen21231.h b/esphome/components/sen21231/sen21231.h index b4d540df556..486a9473d2d 100644 --- a/esphome/components/sen21231/sen21231.h +++ b/esphome/components/sen21231/sen21231.h @@ -7,8 +7,7 @@ // ref: // https://github.com/usefulsensors/person_sensor_pico_c/blob/main/person_sensor.h -namespace esphome { -namespace sen21231_sensor { +namespace esphome::sen21231_sensor { // The I2C address of the person sensor board. static const uint8_t PERSON_SENSOR_I2C_ADDRESS = 0x62; static const uint8_t PERSON_SENSOR_REG_MODE = 0x01; @@ -73,5 +72,4 @@ class Sen21231Sensor : public sensor::Sensor, public PollingComponent, public i2 void read_data_(); }; -} // namespace sen21231_sensor -} // namespace esphome +} // namespace esphome::sen21231_sensor diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h index 558ea46e47f..e6111f4a8fa 100644 --- a/esphome/components/sen5x/automation.h +++ b/esphome/components/sen5x/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "sen5x.h" -namespace esphome { -namespace sen5x { +namespace esphome::sen5x { template<typename... Ts> class StartFanAction : public Action<Ts...> { public: @@ -17,5 +16,4 @@ template<typename... Ts> class StartFanAction : public Action<Ts...> { SEN5XComponent *sen5x_; }; -} // namespace sen5x -} // namespace esphome +} // namespace esphome::sen5x diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 09dda8bca46..588650e6307 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace sen5x { +namespace esphome::sen5x { static const char *const TAG = "sen5x"; @@ -423,5 +422,4 @@ bool SEN5XComponent::start_fan_cleaning() { return true; } -} // namespace sen5x -} // namespace esphome +} // namespace esphome::sen5x diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index a9d4da86b89..ec8f9cc5447 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -6,8 +6,7 @@ #include "esphome/core/application.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace sen5x { +namespace esphome::sen5x { enum ERRORCODE : uint8_t { COMMUNICATION_FAILED, @@ -130,5 +129,4 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri ESPPreferenceObject pref_; }; -} // namespace sen5x -} // namespace esphome +} // namespace esphome::sen5x diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 84520d407dd..8ed9fbb53b0 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace senseair { +namespace esphome::senseair { static const char *const TAG = "senseair"; static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; @@ -150,5 +149,4 @@ void SenseAirComponent::dump_config() { this->check_uart_settings(9600); } -} // namespace senseair -} // namespace esphome +} // namespace esphome::senseair diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 9db849075d9..333c003f48d 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace senseair { +namespace esphome::senseair { enum SenseAirStatus : uint8_t { FATAL_ERROR = 1 << 0, @@ -88,5 +87,4 @@ template<typename... Ts> class SenseAirABCGetPeriodAction : public Action<Ts...> SenseAirComponent *senseair_; }; -} // namespace senseair -} // namespace esphome +} // namespace esphome::senseair diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index 6e244faf596..f6ff4711d40 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <cinttypes> -namespace esphome { -namespace sensirion_common { +namespace esphome::sensirion_common { static const char *const TAG = "sensirion_i2c"; // To avoid memory allocations for small writes a stack buffer is used @@ -79,5 +78,4 @@ bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uin return result; } -} // namespace sensirion_common -} // namespace esphome +} // namespace esphome::sensirion_common diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index 3c2c14ccb81..558fbdbb129 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -4,8 +4,7 @@ #include <vector> -namespace esphome { -namespace sensirion_common { +namespace esphome::sensirion_common { /** * Implementation of I2C functions for Sensirion sensors @@ -149,5 +148,4 @@ class SensirionI2CDevice : public i2c::I2CDevice { i2c::ErrorCode last_error_; }; -} // namespace sensirion_common -} // namespace esphome +} // namespace esphome::sensirion_common diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index b4511de2d0f..d2028ce9bdf 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include <cinttypes> -namespace esphome { -namespace servo { +namespace esphome::servo { static const char *const TAG = "servo"; @@ -106,5 +105,4 @@ void Servo::save_level_(float v) { this->rtc_.save(&v); } -} // namespace servo -} // namespace esphome +} // namespace esphome::servo diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 3d15aefefec..31e93579471 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -6,8 +6,7 @@ #include "esphome/core/preferences.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace servo { +namespace esphome::servo { extern uint32_t global_servo_id; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -73,5 +72,4 @@ template<typename... Ts> class ServoDetachAction : public Action<Ts...> { Servo *servo_; }; -} // namespace servo -} // namespace esphome +} // namespace esphome::servo diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index bbe3bcd7d28..960806e98b0 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -1,8 +1,7 @@ #include "sfa30.h" #include "esphome/core/log.h" -namespace esphome { -namespace sfa30 { +namespace esphome::sfa30 { static const char *const TAG = "sfa30"; @@ -91,5 +90,4 @@ void SFA30Component::update() { }); } -} // namespace sfa30 -} // namespace esphome +} // namespace esphome::sfa30 diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h index 2b744b8da43..d2f2520a576 100644 --- a/esphome/components/sfa30/sfa30.h +++ b/esphome/components/sfa30/sfa30.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace sfa30 { +namespace esphome::sfa30 { class SFA30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; @@ -29,5 +28,4 @@ class SFA30Component : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *temperature_sensor_{nullptr}; }; -} // namespace sfa30 -} // namespace esphome +} // namespace esphome::sfa30 diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 35e5b3dd42b..fd007c92cc8 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace sgp30 { +namespace esphome::sgp30 { static const char *const TAG = "sgp30"; @@ -302,5 +301,4 @@ void SGP30Component::update() { }); } -} // namespace sgp30 -} // namespace esphome +} // namespace esphome::sgp30 diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 4648a33e157..cb4aa1c1bb1 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -8,8 +8,7 @@ #include <cinttypes> #include <cmath> -namespace esphome { -namespace sgp30 { +namespace esphome::sgp30 { struct SGP30Baselines { uint16_t eco2; @@ -67,5 +66,4 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *temperature_sensor_{nullptr}; }; -} // namespace sgp30 -} // namespace esphome +} // namespace esphome::sgp30 diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index bbf1ffd4c09..94e6d69dcbd 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include <cinttypes> -namespace esphome { -namespace sgp4x { +namespace esphome::sgp4x { static const char *const TAG = "sgp4x"; @@ -290,5 +289,4 @@ void SGP4xComponent::dump_config() { LOG_SENSOR(" ", "NOx", this->nox_sensor_); } -} // namespace sgp4x -} // namespace esphome +} // namespace esphome::sgp4x diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 6b8b598aff3..23bf6319a90 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -11,8 +11,7 @@ #include <VOCGasIndexAlgorithm.h> #include <NOxGasIndexAlgorithm.h> -namespace esphome { -namespace sgp4x { +namespace esphome::sgp4x { struct SGP4xBaselines { int32_t state0; @@ -134,5 +133,4 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se uint32_t seconds_since_last_store_; SGP4xBaselines voc_baselines_storage_; }; -} // namespace sgp4x -} // namespace esphome +} // namespace esphome::sgp4x diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 8050a2d5f97..eba6be65d58 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -1,8 +1,7 @@ #include "sht3xd.h" #include "esphome/core/log.h" -namespace esphome { -namespace sht3xd { +namespace esphome::sht3xd { static const char *const TAG = "sht3xd"; @@ -82,5 +81,4 @@ void SHT3XDComponent::update() { }); } -} // namespace sht3xd -} // namespace esphome +} // namespace esphome::sht3xd diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 54514d6de73..6df5587507c 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace sht3xd { +namespace esphome::sht3xd { /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. class SHT3XDComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { @@ -25,5 +24,4 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir uint32_t serial_number_{0}; }; -} // namespace sht3xd -} // namespace esphome +} // namespace esphome::sht3xd diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index b1dbde22a4d..4a3df5a91f2 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace sht4x { +namespace esphome::sht4x { static const char *const TAG = "sht4x"; @@ -127,5 +126,4 @@ void SHT4XComponent::update() { }); } -} // namespace sht4x -} // namespace esphome +} // namespace esphome::sht4x diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 51f473fe3f2..d1fa9033df4 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace sht4x { +namespace esphome::sht4x { enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECISION_LOW }; @@ -45,5 +44,4 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace sht4x -} // namespace esphome +} // namespace esphome::sht4x diff --git a/esphome/components/shutdown/button/shutdown_button.cpp b/esphome/components/shutdown/button/shutdown_button.cpp index b40af7517b4..6394c6c14e6 100644 --- a/esphome/components/shutdown/button/shutdown_button.cpp +++ b/esphome/components/shutdown/button/shutdown_button.cpp @@ -10,8 +10,7 @@ #include <Esp.h> #endif -namespace esphome { -namespace shutdown { +namespace esphome::shutdown { static const char *const TAG = "shutdown.button"; @@ -29,5 +28,4 @@ void ShutdownButton::press_action() { #endif } -} // namespace shutdown -} // namespace esphome +} // namespace esphome::shutdown diff --git a/esphome/components/shutdown/button/shutdown_button.h b/esphome/components/shutdown/button/shutdown_button.h index d0094c899d8..d4247ec0f98 100644 --- a/esphome/components/shutdown/button/shutdown_button.h +++ b/esphome/components/shutdown/button/shutdown_button.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace shutdown { +namespace esphome::shutdown { class ShutdownButton : public button::Button, public Component { public: @@ -14,5 +13,4 @@ class ShutdownButton : public button::Button, public Component { void press_action() override; }; -} // namespace shutdown -} // namespace esphome +} // namespace esphome::shutdown diff --git a/esphome/components/shutdown/switch/shutdown_switch.cpp b/esphome/components/shutdown/switch/shutdown_switch.cpp index b685ab14ab4..a44a572aa1c 100644 --- a/esphome/components/shutdown/switch/shutdown_switch.cpp +++ b/esphome/components/shutdown/switch/shutdown_switch.cpp @@ -10,8 +10,7 @@ #include <Esp.h> #endif -namespace esphome { -namespace shutdown { +namespace esphome::shutdown { static const char *const TAG = "shutdown.switch"; @@ -34,5 +33,4 @@ void ShutdownSwitch::write_state(bool state) { } } -} // namespace shutdown -} // namespace esphome +} // namespace esphome::shutdown diff --git a/esphome/components/shutdown/switch/shutdown_switch.h b/esphome/components/shutdown/switch/shutdown_switch.h index 6aa64ff06b2..933345915ff 100644 --- a/esphome/components/shutdown/switch/shutdown_switch.h +++ b/esphome/components/shutdown/switch/shutdown_switch.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace shutdown { +namespace esphome::shutdown { class ShutdownSwitch : public switch_::Switch, public Component { public: @@ -14,5 +13,4 @@ class ShutdownSwitch : public switch_::Switch, public Component { void write_state(bool state) override; }; -} // namespace shutdown -} // namespace esphome +} // namespace esphome::shutdown diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.cpp b/esphome/components/sigma_delta_output/sigma_delta_output.cpp index d386f8db1a5..6cc131c68ff 100644 --- a/esphome/components/sigma_delta_output/sigma_delta_output.cpp +++ b/esphome/components/sigma_delta_output/sigma_delta_output.cpp @@ -1,8 +1,7 @@ #include "sigma_delta_output.h" #include "esphome/core/log.h" -namespace esphome { -namespace sigma_delta_output { +namespace esphome::sigma_delta_output { static const char *const TAG = "output.sigma_delta"; @@ -53,5 +52,4 @@ void SigmaDeltaOutput::update() { } } -} // namespace sigma_delta_output -} // namespace esphome +} // namespace esphome::sigma_delta_output diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h index 8fd1e1f761a..a5df3c6c7c2 100644 --- a/esphome/components/sigma_delta_output/sigma_delta_output.h +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace sigma_delta_output { +namespace esphome::sigma_delta_output { class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { public: @@ -43,5 +42,4 @@ class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { float state_{0.}; bool value_{false}; }; -} // namespace sigma_delta_output -} // namespace esphome +} // namespace esphome::sigma_delta_output diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 001ec774546..b8e97b11211 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <cstring> -namespace esphome { -namespace sim800l { +namespace esphome::sim800l { static const char *const TAG = "sim800l"; @@ -492,5 +491,4 @@ void Sim800LComponent::set_registered_(bool registered) { #endif } -} // namespace sim800l -} // namespace esphome +} // namespace esphome::sim800l diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index d0da1230398..0b3259ede00 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -13,8 +13,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" -namespace esphome { -namespace sim800l { +namespace esphome::sim800l { const uint16_t SIM800L_READ_BUFFER_LENGTH = 1024; @@ -184,5 +183,4 @@ template<typename... Ts> class Sim800LDisconnectAction : public Action<Ts...> { Sim800LComponent *parent_; }; -} // namespace sim800l -} // namespace esphome +} // namespace esphome::sim800l diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp index 033729c407d..e695ab95403 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.cpp +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -3,8 +3,7 @@ #include "esphome/core/gpio.h" #include "esphome/core/log.h" -namespace esphome { -namespace slow_pwm { +namespace esphome::slow_pwm { static const char *const TAG = "output.slow_pwm"; @@ -79,5 +78,4 @@ void SlowPWMOutput::write_state(float state) { this->restart_cycle(); } -} // namespace slow_pwm -} // namespace esphome +} // namespace esphome::slow_pwm diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h index 3e5a3e2a400..d866435af1e 100644 --- a/esphome/components/slow_pwm/slow_pwm_output.h +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace slow_pwm { +namespace esphome::slow_pwm { class SlowPWMOutput : public output::FloatOutput, public Component { public: @@ -57,5 +56,4 @@ class SlowPWMOutput : public output::FloatOutput, public Component { bool restart_cycle_on_state_change_; }; -} // namespace slow_pwm -} // namespace esphome +} // namespace esphome::slow_pwm diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp index d380f31c6fa..45de3c457d2 100644 --- a/esphome/components/sm10bit_base/sm10bit_base.cpp +++ b/esphome/components/sm10bit_base/sm10bit_base.cpp @@ -1,8 +1,7 @@ #include "sm10bit_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace sm10bit_base { +namespace esphome::sm10bit_base { static const char *const TAG = "sm10bit_base"; @@ -127,5 +126,4 @@ void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) { delayMicroseconds(SM10BIT_DELAY); } -} // namespace sm10bit_base -} // namespace esphome +} // namespace esphome::sm10bit_base diff --git a/esphome/components/sm10bit_base/sm10bit_base.h b/esphome/components/sm10bit_base/sm10bit_base.h index c8e92e352fe..b419b86dbfa 100644 --- a/esphome/components/sm10bit_base/sm10bit_base.h +++ b/esphome/components/sm10bit_base/sm10bit_base.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace sm10bit_base { +namespace esphome::sm10bit_base { class Sm10BitBase : public Component { public: @@ -59,5 +58,4 @@ class Sm10BitBase : public Component { bool update_{true}; }; -} // namespace sm10bit_base -} // namespace esphome +} // namespace esphome::sm10bit_base diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp index b8e293929bd..59e9f1b7120 100644 --- a/esphome/components/sm16716/sm16716.cpp +++ b/esphome/components/sm16716/sm16716.cpp @@ -1,8 +1,7 @@ #include "sm16716.h" #include "esphome/core/log.h" -namespace esphome { -namespace sm16716 { +namespace esphome::sm16716 { static const char *const TAG = "sm16716"; @@ -49,5 +48,4 @@ void SM16716::loop() { this->update_ = false; } -} // namespace sm16716 -} // namespace esphome +} // namespace esphome::sm16716 diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h index 73414c0003b..09deb2e8bf8 100644 --- a/esphome/components/sm16716/sm16716.h +++ b/esphome/components/sm16716/sm16716.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace sm16716 { +namespace esphome::sm16716 { class SM16716 : public Component { public: @@ -68,5 +67,4 @@ class SM16716 : public Component { bool update_{true}; }; -} // namespace sm16716 -} // namespace esphome +} // namespace esphome::sm16716 diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index c3d10e70c28..0086a63878b 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -3,8 +3,7 @@ // Tnx to the work of https://github.com/arendst (Tasmota) for making the initial version of the driver -namespace esphome { -namespace sm2135 { +namespace esphome::sm2135 { static const char *const TAG = "sm2135"; @@ -149,5 +148,4 @@ void SM2135::sm2135_set_high_(GPIOPin *pin) { pin->pin_mode(gpio::FLAG_PULLUP); } -} // namespace sm2135 -} // namespace esphome +} // namespace esphome::sm2135 diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h index 6f207d093aa..040ec14b7fd 100644 --- a/esphome/components/sm2135/sm2135.h +++ b/esphome/components/sm2135/sm2135.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace sm2135 { +namespace esphome::sm2135 { enum SM2135Current : uint8_t { SM2135_CURRENT_10MA = 0x00, @@ -86,5 +85,4 @@ class SM2135 : public Component { bool update_{true}; }; -} // namespace sm2135 -} // namespace esphome +} // namespace esphome::sm2135 diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp index 44768623182..e981bd0f71f 100644 --- a/esphome/components/sm2235/sm2235.cpp +++ b/esphome/components/sm2235/sm2235.cpp @@ -1,8 +1,7 @@ #include "sm2235.h" #include "esphome/core/log.h" -namespace esphome { -namespace sm2235 { +namespace esphome::sm2235 { static const char *const TAG = "sm2235"; @@ -24,5 +23,4 @@ void SM2235::dump_config() { LOG_PIN(" Clock Pin: ", this->clock_pin_); } -} // namespace sm2235 -} // namespace esphome +} // namespace esphome::sm2235 diff --git a/esphome/components/sm2235/sm2235.h b/esphome/components/sm2235/sm2235.h index 56d1782055a..cdb754e2980 100644 --- a/esphome/components/sm2235/sm2235.h +++ b/esphome/components/sm2235/sm2235.h @@ -4,8 +4,7 @@ #include "esphome/components/sm10bit_base/sm10bit_base.h" #include "esphome/core/hal.h" -namespace esphome { -namespace sm2235 { +namespace esphome::sm2235 { class SM2235 : public sm10bit_base::Sm10BitBase { public: @@ -15,5 +14,4 @@ class SM2235 : public sm10bit_base::Sm10BitBase { void dump_config() override; }; -} // namespace sm2235 -} // namespace esphome +} // namespace esphome::sm2235 diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp index f860517021b..93f1096800f 100644 --- a/esphome/components/sm2335/sm2335.cpp +++ b/esphome/components/sm2335/sm2335.cpp @@ -1,8 +1,7 @@ #include "sm2335.h" #include "esphome/core/log.h" -namespace esphome { -namespace sm2335 { +namespace esphome::sm2335 { static const char *const TAG = "sm2335"; @@ -24,5 +23,4 @@ void SM2335::dump_config() { LOG_PIN(" Clock Pin: ", this->clock_pin_); } -} // namespace sm2335 -} // namespace esphome +} // namespace esphome::sm2335 diff --git a/esphome/components/sm2335/sm2335.h b/esphome/components/sm2335/sm2335.h index c8cf8251895..44e0e5b03ff 100644 --- a/esphome/components/sm2335/sm2335.h +++ b/esphome/components/sm2335/sm2335.h @@ -4,8 +4,7 @@ #include "esphome/components/sm10bit_base/sm10bit_base.h" #include "esphome/core/hal.h" -namespace esphome { -namespace sm2335 { +namespace esphome::sm2335 { class SM2335 : public sm10bit_base::Sm10BitBase { public: @@ -15,5 +14,4 @@ class SM2335 : public sm10bit_base::Sm10BitBase { void dump_config() override; }; -} // namespace sm2335 -} // namespace esphome +} // namespace esphome::sm2335 diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp index 365271cec94..391cc0ac117 100644 --- a/esphome/components/sm300d2/sm300d2.cpp +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -1,8 +1,7 @@ #include "sm300d2.h" #include "esphome/core/log.h" -namespace esphome { -namespace sm300d2 { +namespace esphome::sm300d2 { static const char *const TAG = "sm300d2"; static const uint8_t SM300D2_RESPONSE_LENGTH = 17; @@ -104,5 +103,4 @@ void SM300D2Sensor::dump_config() { this->check_uart_settings(9600); } -} // namespace sm300d2 -} // namespace esphome +} // namespace esphome::sm300d2 diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 4e97b54988b..629e758e30d 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace sm300d2 { +namespace esphome::sm300d2 { class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { public: @@ -32,5 +31,4 @@ class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace sm300d2 -} // namespace esphome +} // namespace esphome::sm300d2 diff --git a/esphome/components/sml/constants.h b/esphome/components/sml/constants.h index 0142fe98f75..b44fff7bdeb 100644 --- a/esphome/components/sml/constants.h +++ b/esphome/components/sml/constants.h @@ -3,8 +3,7 @@ #include <array> #include <cstdint> -namespace esphome { -namespace sml { +namespace esphome::sml { enum SmlType : uint8_t { SML_OCTET = 0, @@ -24,5 +23,4 @@ const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a constexpr std::array<uint8_t, 8> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01}; -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sensor/sml_sensor.cpp b/esphome/components/sml/sensor/sml_sensor.cpp index e9a384d2755..047a13e88d8 100644 --- a/esphome/components/sml/sensor/sml_sensor.cpp +++ b/esphome/components/sml/sensor/sml_sensor.cpp @@ -2,8 +2,7 @@ #include "sml_sensor.h" #include "../sml_parser.h" -namespace esphome { -namespace sml { +namespace esphome::sml { static const char *const TAG = "sml_sensor"; @@ -37,5 +36,4 @@ void SmlSensor::dump_config() { ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); } -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sensor/sml_sensor.h b/esphome/components/sml/sensor/sml_sensor.h index eb7b108f94e..d2f8a7743f0 100644 --- a/esphome/components/sml/sensor/sml_sensor.h +++ b/esphome/components/sml/sensor/sml_sensor.h @@ -2,8 +2,7 @@ #include "esphome/components/sml/sml.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace sml { +namespace esphome::sml { class SmlSensor : public SmlListener, public sensor::Sensor, public Component { public: @@ -12,5 +11,4 @@ class SmlSensor : public SmlListener, public sensor::Sensor, public Component { void dump_config() override; }; -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp index c8d5fcc2694..bacfbcccef0 100644 --- a/esphome/components/sml/sml.cpp +++ b/esphome/components/sml/sml.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "sml_parser.h" -namespace esphome { -namespace sml { +namespace esphome::sml { static const char *const TAG = "sml"; @@ -140,5 +139,4 @@ uint8_t get_code(uint8_t byte) { } } -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h index 29a2f48bbec..60a80e3ad84 100644 --- a/esphome/components/sml/sml.h +++ b/esphome/components/sml/sml.h @@ -7,8 +7,7 @@ #include "esphome/components/uart/uart.h" #include "sml_parser.h" -namespace esphome { -namespace sml { +namespace esphome::sml { class SmlListener { public: @@ -44,5 +43,4 @@ class Sml : public Component, public uart::UARTDevice { bool check_sml_data(const bytes &buffer); uint8_t get_code(uint8_t byte); -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index ed086e385d2..66e12ea64b5 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -2,8 +2,7 @@ #include "constants.h" #include "sml_parser.h" -namespace esphome { -namespace sml { +namespace esphome::sml { SmlFile::SmlFile(const BytesView &buffer) : buffer_(buffer) { // extract messages @@ -158,5 +157,4 @@ std::string ObisInfo::code_repr() const { return buf; } -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/sml_parser.h b/esphome/components/sml/sml_parser.h index bee0c8965bc..2fe404c96af 100644 --- a/esphome/components/sml/sml_parser.h +++ b/esphome/components/sml/sml_parser.h @@ -7,8 +7,7 @@ #include <vector> #include "constants.h" -namespace esphome { -namespace sml { +namespace esphome::sml { using bytes = std::vector<uint8_t>; @@ -80,5 +79,4 @@ uint64_t bytes_to_uint(const BytesView &buffer); int64_t bytes_to_int(const BytesView &buffer); std::string bytes_to_string(const BytesView &buffer); -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp index 17b93ecccf6..d0965e3dcaa 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.cpp +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -4,8 +4,7 @@ #include "../sml_parser.h" #include <cinttypes> -namespace esphome { -namespace sml { +namespace esphome::sml { static const char *const TAG = "sml_text_sensor"; @@ -60,5 +59,4 @@ void SmlTextSensor::dump_config() { ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); } -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.h b/esphome/components/sml/text_sensor/sml_text_sensor.h index 20d27c9f715..6194f223493 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.h +++ b/esphome/components/sml/text_sensor/sml_text_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "../constants.h" -namespace esphome { -namespace sml { +namespace esphome::sml { class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component { public: @@ -17,5 +16,4 @@ class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public SmlType format_; }; -} // namespace sml -} // namespace esphome +} // namespace esphome::sml diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 6eb6416447f..ed33fc54c53 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -1,8 +1,7 @@ #include "smt100.h" #include "esphome/core/log.h" -namespace esphome { -namespace smt100 { +namespace esphome::smt100 { static const char *const TAG = "smt100"; @@ -91,5 +90,4 @@ int SMT100Component::readline_(int readch, char *buffer, int len) { return -1; } -} // namespace smt100 -} // namespace esphome +} // namespace esphome::smt100 diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index cb01b1ed554..b68151eeb40 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace smt100 { +namespace esphome::smt100 { class SMT100Component : public PollingComponent, public uart::UARTDevice { static const uint16_t MAX_LINE_LENGTH = 31; @@ -40,5 +39,4 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { uint32_t last_transmission_{0}; }; -} // namespace smt100 -} // namespace esphome +} // namespace esphome::smt100 diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 63b3f98521a..fde789e90c7 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -1,8 +1,7 @@ #include "sn74hc165.h" #include "esphome/core/log.h" -namespace esphome { -namespace sn74hc165 { +namespace esphome::sn74hc165 { static const char *const TAG = "sn74hc165"; @@ -68,5 +67,4 @@ size_t SN74HC165GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via SN74HC165", this->pin_); } -} // namespace sn74hc165 -} // namespace esphome +} // namespace esphome::sn74hc165 diff --git a/esphome/components/sn74hc165/sn74hc165.h b/esphome/components/sn74hc165/sn74hc165.h index 5a3f3fe8ef0..596f2eb4f59 100644 --- a/esphome/components/sn74hc165/sn74hc165.h +++ b/esphome/components/sn74hc165/sn74hc165.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace sn74hc165 { +namespace esphome::sn74hc165 { class SN74HC165Component : public Component { public: @@ -60,5 +59,4 @@ class SN74HC165GPIOPin : public GPIOPin, public Parented<SN74HC165Component> { bool inverted_; }; -} // namespace sn74hc165 -} // namespace esphome +} // namespace esphome::sn74hc165 diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index 1bb8c7936db..710e51ad128 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <ranges> -namespace esphome { -namespace sn74hc595 { +namespace esphome::sn74hc595 { static const char *const TAG = "sn74hc595"; @@ -97,5 +96,4 @@ size_t SN74HC595GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via SN74HC595", this->pin_); } -} // namespace sn74hc595 -} // namespace esphome +} // namespace esphome::sn74hc595 diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 1cf70c86b50..23977e3d04c 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -11,8 +11,7 @@ #include <vector> -namespace esphome { -namespace sn74hc595 { +namespace esphome::sn74hc595 { class SN74HC595Component : public Component { public: @@ -93,5 +92,4 @@ class SN74HC595SPIComponent : public SN74HC595Component, #endif -} // namespace sn74hc595 -} // namespace esphome +} // namespace esphome::sn74hc595 diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index c4d78b6e0b1..f8d48b40981 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -9,8 +9,7 @@ #include "lwip/apps/sntp.h" #endif -namespace esphome { -namespace sntp { +namespace esphome::sntp { static const char *const TAG = "sntp"; @@ -102,5 +101,4 @@ void SNTPComponent::time_synced() { this->time_sync_callback_.call(); } -} // namespace sntp -} // namespace esphome +} // namespace esphome::sntp diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 8f2e411c18b..ef737c1978c 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -4,8 +4,7 @@ #include "esphome/components/time/real_time_clock.h" #include <array> -namespace esphome { -namespace sntp { +namespace esphome::sntp { // Server count is calculated at compile time by Python codegen // SNTP_SERVER_COUNT will always be defined @@ -42,5 +41,4 @@ class SNTPComponent : public time::RealTimeClock { #endif }; -} // namespace sntp -} // namespace esphome +} // namespace esphome::sntp diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index 03586b63984..3e5af2b51ff 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -44,8 +44,7 @@ #include "sonoff_d1.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace sonoff_d1 { +namespace esphome::sonoff_d1 { static const char *const TAG = "sonoff_d1"; @@ -321,5 +320,4 @@ void SonoffD1Output::loop() { } } -} // namespace sonoff_d1 -} // namespace esphome +} // namespace esphome::sonoff_d1 diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h index 20bea23287b..a92877e6c8f 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.h +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -39,8 +39,7 @@ #include "esphome/components/light/light_state.h" #include "esphome/components/light/light_traits.h" -namespace esphome { -namespace sonoff_d1 { +namespace esphome::sonoff_d1 { class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, public Component { public: @@ -80,5 +79,4 @@ class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, publi void publish_state_(bool is_on, uint8_t brightness); }; -} // namespace sonoff_d1 -} // namespace esphome +} // namespace esphome::sonoff_d1 diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index 2719172409d..99533dbdd55 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -7,8 +7,7 @@ #include <cmath> #include <cstdint> -namespace esphome { -namespace sound_level { +namespace esphome::sound_level { static const char *const TAG = "sound_level"; @@ -190,7 +189,6 @@ bool SoundLevelComponent::start_() { void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); } -} // namespace sound_level -} // namespace esphome +} // namespace esphome::sound_level #endif diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index a1021eb1e82..0e46a203e86 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -10,8 +10,7 @@ #include "esphome/core/component.h" #include "esphome/core/ring_buffer.h" -namespace esphome { -namespace sound_level { +namespace esphome::sound_level { class SoundLevelComponent : public Component { public: @@ -69,6 +68,6 @@ template<typename... Ts> class StopAction : public Action<Ts...>, public Parente void play(const Ts &...x) override { this->parent_->stop(); } }; -} // namespace sound_level -} // namespace esphome +} // namespace esphome::sound_level + #endif diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 391c9e4c627..9997b064d50 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace speaker { +namespace esphome::speaker { template<typename... Ts> class PlayAction : public Action<Ts...>, public Parented<Speaker> { public: @@ -84,5 +83,4 @@ template<typename... Ts> class IsStoppedCondition : public Condition<Ts...>, pub bool check(const Ts &...x) override { return this->parent_->is_stopped(); } }; -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 0822d80254e..892d4f4112f 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace speaker { +namespace esphome::speaker { static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file @@ -513,7 +512,6 @@ void AudioPipeline::decode_task(void *params) { } } -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker #endif diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 2c785728359..ef7f75dd38c 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -15,8 +15,7 @@ #include <freertos/event_groups.h> #include <freertos/queue.h> -namespace esphome { -namespace speaker { +namespace esphome::speaker { // Internal sink/source buffers for reader and decoder static const size_t DEFAULT_TRANSFER_BUFFER_SIZE = 24 * 1024; @@ -147,7 +146,6 @@ class AudioPipeline { StaticTask decode_task_; }; -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker #endif diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index 6270da7bd4a..7843399866f 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -7,8 +7,7 @@ #include "esphome/components/audio/audio.h" #include "esphome/core/automation.h" -namespace esphome { -namespace speaker { +namespace esphome::speaker { template<typename... Ts> class PlayOnDeviceMediaAction : public Action<Ts...>, public Parented<SpeakerMediaPlayer> { TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) @@ -20,7 +19,6 @@ template<typename... Ts> class PlayOnDeviceMediaAction : public Action<Ts...>, p } }; -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker #endif diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index afd93b3f45c..7d9cfecfdfa 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -9,8 +9,7 @@ #include "esphome/components/ota/ota_backend.h" #endif -namespace esphome { -namespace speaker { +namespace esphome::speaker { // Framework: // - Media player that can handle two streams: one for media and one for announcements @@ -622,7 +621,6 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker #endif diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 3fa6f47b848..2d80377312e 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -21,8 +21,7 @@ #include <freertos/FreeRTOS.h> #include <freertos/queue.h> -namespace esphome { -namespace speaker { +namespace esphome::speaker { struct MediaCallCommand { optional<media_player::MediaPlayerCommand> command; @@ -167,7 +166,6 @@ class SpeakerMediaPlayer : public Component, Trigger<float> volume_trigger_; }; -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker #endif diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 5b89d00c69f..c89b6c588c7 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -16,8 +16,7 @@ #include "esphome/components/audio_dac/audio_dac.h" #endif -namespace esphome { -namespace speaker { +namespace esphome::speaker { enum State : uint8_t { STATE_STOPPED = 0, @@ -123,5 +122,4 @@ class Speaker { CallbackManager<void(uint32_t, int64_t)> audio_output_callback_{}; }; -} // namespace speaker -} // namespace esphome +} // namespace esphome::speaker diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index eaa8a55858d..af46ef52e1c 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -1,8 +1,7 @@ #include "speed_fan.h" #include "esphome/core/log.h" -namespace esphome { -namespace speed { +namespace esphome::speed { static const char *const TAG = "speed.fan"; @@ -47,5 +46,4 @@ void SpeedFan::write_state_() { this->direction_->set_state(this->direction == fan::FanDirection::REVERSE); } -} // namespace speed -} // namespace esphome +} // namespace esphome::speed diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index db96039a135..c618d6bc5f6 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace speed { +namespace esphome::speed { class SpeedFan : public Component, public fan::Fan { public: @@ -33,5 +32,4 @@ class SpeedFan : public Component, public fan::Fan { fan::FanTraits traits_; }; -} // namespace speed -} // namespace esphome +} // namespace esphome::speed diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index 34f83027db4..bdf5978bc7d 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include <cinttypes> -namespace esphome { -namespace spi_device { +namespace esphome::spi_device { static const char *const TAG = "spi_device"; @@ -23,5 +22,4 @@ void SPIDeviceComponent::dump_config() { } } -} // namespace spi_device -} // namespace esphome +} // namespace esphome::spi_device diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h index e3aa74aaf05..3a2523fbab0 100644 --- a/esphome/components/spi_device/spi_device.h +++ b/esphome/components/spi_device/spi_device.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace spi_device { +namespace esphome::spi_device { class SPIDeviceComponent : public Component, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, @@ -16,5 +15,4 @@ class SPIDeviceComponent : public Component, protected: }; -} // namespace spi_device -} // namespace esphome +} // namespace esphome::spi_device diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index ff8d2e6ee0d..f41952cac99 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -1,8 +1,7 @@ #include "spi_led_strip.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace spi_led_strip { +namespace esphome::spi_led_strip { SpiLedStrip::SpiLedStrip(uint16_t num_leds) { this->num_leds_ = num_leds; @@ -65,5 +64,4 @@ light::ESPColorView SpiLedStrip::get_view_internal(int32_t index) const { return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, this->effect_data_ + index, &this->correction_}; } -} // namespace spi_led_strip -} // namespace esphome +} // namespace esphome::spi_led_strip diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 14c5627ac32..e2bcd5af639 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -5,8 +5,7 @@ #include "esphome/components/light/addressable_light.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace spi_led_strip { +namespace esphome::spi_led_strip { static const char *const TAG = "spi_led_strip"; class SpiLedStrip : public light::AddressableLight, @@ -36,5 +35,4 @@ class SpiLedStrip : public light::AddressableLight, uint16_t num_leds_; }; -} // namespace spi_led_strip -} // namespace esphome +} // namespace esphome::spi_led_strip diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index 5eafc1b6c2d..e58f857eb3a 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "sps30.h" -namespace esphome { -namespace sps30 { +namespace esphome::sps30 { template<typename... Ts> class StartFanAction : public Action<Ts...>, public Parented<SPS30Component> { public: @@ -22,5 +21,4 @@ template<typename... Ts> class StopMeasurementAction : public Action<Ts...>, pub void play(const Ts &...x) override { this->parent_->stop_measurement(); } }; -} // namespace sps30 -} // namespace esphome +} // namespace esphome::sps30 diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index e4fc4ffd31d..73fa4ef4633 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace sps30 { +namespace esphome::sps30 { static const char *const TAG = "sps30"; @@ -291,5 +290,4 @@ bool SPS30Component::start_fan_cleaning() { return true; } -} // namespace sps30 -} // namespace esphome +} // namespace esphome::sps30 diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 4e9b90ba7ec..ccb3e8ff413 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace sps30 { +namespace esphome::sps30 { /// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter /// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. @@ -67,5 +66,4 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri optional<uint32_t> idle_interval_; }; -} // namespace sps30 -} // namespace esphome +} // namespace esphome::sps30 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 5bd83ec8a8e..5f1c6fca8f4 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace ssd1306_base { +namespace esphome::ssd1306_base { static const char *const TAG = "ssd1306"; @@ -376,5 +375,4 @@ const LogString *SSD1306::model_str_() { return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX); } -} // namespace ssd1306_base -} // namespace esphome +} // namespace esphome::ssd1306_base diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 3cc795a3236..7b4c9fe0bfc 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1306_base { +namespace esphome::ssd1306_base { enum SSD1306Model { SSD1306_MODEL_128_32 = 0, @@ -88,5 +87,4 @@ class SSD1306 : public display::DisplayBuffer { bool invert_{false}; }; -} // namespace ssd1306_base -} // namespace esphome +} // namespace esphome::ssd1306_base diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index e1f6e912430..8ff908fe7a6 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -1,8 +1,7 @@ #include "ssd1306_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1306_i2c { +namespace esphome::ssd1306_i2c { static const char *const TAG = "ssd1306_i2c"; @@ -76,5 +75,4 @@ void HOT I2CSSD1306::write_display_data() { } } -} // namespace ssd1306_i2c -} // namespace esphome +} // namespace esphome::ssd1306_i2c diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.h b/esphome/components/ssd1306_i2c/ssd1306_i2c.h index e3f21fe74c9..0316da0e778 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.h +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1306_base/ssd1306_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ssd1306_i2c { +namespace esphome::ssd1306_i2c { class I2CSSD1306 : public ssd1306_base::SSD1306, public i2c::I2CDevice { public: @@ -19,5 +18,4 @@ class I2CSSD1306 : public ssd1306_base::SSD1306, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; }; -} // namespace ssd1306_i2c -} // namespace esphome +} // namespace esphome::ssd1306_i2c diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index af9a17c8ab2..5c9369f1a20 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1306_spi { +namespace esphome::ssd1306_spi { static const char *const TAG = "ssd1306_spi"; @@ -63,5 +62,4 @@ void HOT SPISSD1306::write_display_data() { } } -} // namespace ssd1306_spi -} // namespace esphome +} // namespace esphome::ssd1306_spi diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.h b/esphome/components/ssd1306_spi/ssd1306_spi.h index c58ebc800a6..f8346033b32 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.h +++ b/esphome/components/ssd1306_spi/ssd1306_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1306_base/ssd1306_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1306_spi { +namespace esphome::ssd1306_spi { class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -25,5 +24,4 @@ class SPISSD1306 : public ssd1306_base::SSD1306, GPIOPin *dc_pin_; }; -} // namespace ssd1306_spi -} // namespace esphome +} // namespace esphome::ssd1306_spi diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 1fce826ad9d..a8a39b9ab01 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1322_base { +namespace esphome::ssd1322_base { static const char *const TAG = "ssd1322"; @@ -206,5 +205,4 @@ const char *SSD1322::model_str_() { } } -} // namespace ssd1322_base -} // namespace esphome +} // namespace esphome::ssd1322_base diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h index 9f4d39976c9..19dd6600ccf 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.h +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1322_base { +namespace esphome::ssd1322_base { enum SSD1322Model { SSD1322_MODEL_256_64 = 0, @@ -51,5 +50,4 @@ class SSD1322 : public display::DisplayBuffer { float brightness_{1.0}; }; -} // namespace ssd1322_base -} // namespace esphome +} // namespace esphome::ssd1322_base diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp index bc7d298922a..79f15508c34 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.cpp +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1322_spi { +namespace esphome::ssd1322_spi { static const char *const TAG = "ssd1322_spi"; @@ -68,5 +67,4 @@ void HOT SPISSD1322::write_display_data() { this->disable(); } -} // namespace ssd1322_spi -} // namespace esphome +} // namespace esphome::ssd1322_spi diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.h b/esphome/components/ssd1322_spi/ssd1322_spi.h index 316742706ef..31d17d0ef1a 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.h +++ b/esphome/components/ssd1322_spi/ssd1322_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1322_base/ssd1322_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1322_spi { +namespace esphome::ssd1322_spi { class SPISSD1322 : public ssd1322_base::SSD1322, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -26,5 +25,4 @@ class SPISSD1322 : public ssd1322_base::SSD1322, GPIOPin *dc_pin_; }; -} // namespace ssd1322_spi -} // namespace esphome +} // namespace esphome::ssd1322_spi diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index fe7df9674b6..b8d8f999995 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1325_base { +namespace esphome::ssd1325_base { static const char *const TAG = "ssd1325"; @@ -241,5 +240,4 @@ const char *SSD1325::model_str_() { } } -} // namespace ssd1325_base -} // namespace esphome +} // namespace esphome::ssd1325_base diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index ae033e582bb..8404f8209d6 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1325_base { +namespace esphome::ssd1325_base { enum SSD1325Model { SSD1325_MODEL_128_32 = 0, @@ -56,5 +55,4 @@ class SSD1325 : public display::DisplayBuffer { float brightness_{1.0}; }; -} // namespace ssd1325_base -} // namespace esphome +} // namespace esphome::ssd1325_base diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 07a5119d8f7..af6a01c7f39 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1325_spi { +namespace esphome::ssd1325_spi { static const char *const TAG = "ssd1325_spi"; @@ -56,5 +55,4 @@ void HOT SPISSD1325::write_display_data() { this->disable(); } -} // namespace ssd1325_spi -} // namespace esphome +} // namespace esphome::ssd1325_spi diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h index e4e7d55769f..32cbb28fd85 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.h +++ b/esphome/components/ssd1325_spi/ssd1325_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1325_base/ssd1325_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1325_spi { +namespace esphome::ssd1325_spi { class SPISSD1325 : public ssd1325_base::SSD1325, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -25,5 +24,4 @@ class SPISSD1325 : public ssd1325_base::SSD1325, GPIOPin *dc_pin_; }; -} // namespace ssd1325_spi -} // namespace esphome +} // namespace esphome::ssd1325_spi diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 87e52206f2d..ae02b836bf4 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1327_base { +namespace esphome::ssd1327_base { static const char *const TAG = "ssd1327"; @@ -182,5 +181,4 @@ const char *SSD1327::model_str_() { } } -} // namespace ssd1327_base -} // namespace esphome +} // namespace esphome::ssd1327_base diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h index 207023a3d3e..ab521b402d1 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.h +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1327_base { +namespace esphome::ssd1327_base { enum SSD1327Model { SSD1327_MODEL_128_128 = 0, @@ -50,5 +49,4 @@ class SSD1327 : public display::DisplayBuffer { float brightness_{1.0}; }; -} // namespace ssd1327_base -} // namespace esphome +} // namespace esphome::ssd1327_base diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp index 3597a38c446..90fbcbd05cf 100644 --- a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp @@ -1,8 +1,7 @@ #include "ssd1327_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1327_i2c { +namespace esphome::ssd1327_i2c { static const char *const TAG = "ssd1327_i2c"; @@ -39,5 +38,4 @@ void HOT I2CSSD1327::write_display_data() { } } -} // namespace ssd1327_i2c -} // namespace esphome +} // namespace esphome::ssd1327_i2c diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.h b/esphome/components/ssd1327_i2c/ssd1327_i2c.h index dd292f99364..f08ef94fefa 100644 --- a/esphome/components/ssd1327_i2c/ssd1327_i2c.h +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1327_base/ssd1327_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ssd1327_i2c { +namespace esphome::ssd1327_i2c { class I2CSSD1327 : public ssd1327_base::SSD1327, public i2c::I2CDevice { public: @@ -19,5 +18,4 @@ class I2CSSD1327 : public ssd1327_base::SSD1327, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; }; -} // namespace ssd1327_i2c -} // namespace esphome +} // namespace esphome::ssd1327_i2c diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp index 54d1a51100f..c6a64227fc4 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.cpp +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1327_spi { +namespace esphome::ssd1327_spi { static const char *const TAG = "ssd1327_spi"; @@ -55,5 +54,4 @@ void HOT SPISSD1327::write_display_data() { this->disable(); } -} // namespace ssd1327_spi -} // namespace esphome +} // namespace esphome::ssd1327_spi diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.h b/esphome/components/ssd1327_spi/ssd1327_spi.h index 6f7abea96f5..fd1ed0357f3 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.h +++ b/esphome/components/ssd1327_spi/ssd1327_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1327_base/ssd1327_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1327_spi { +namespace esphome::ssd1327_spi { class SPISSD1327 : public ssd1327_base::SSD1327, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -25,5 +24,4 @@ class SPISSD1327 : public ssd1327_base::SSD1327, GPIOPin *dc_pin_; }; -} // namespace ssd1327_spi -} // namespace esphome +} // namespace esphome::ssd1327_spi diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index a2993edef3d..7755e5a2a03 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1331_base { +namespace esphome::ssd1331_base { static const char *const TAG = "ssd1331"; @@ -156,5 +155,4 @@ void SSD1331::init_reset_() { } } -} // namespace ssd1331_base -} // namespace esphome +} // namespace esphome::ssd1331_base diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h index 719bfc1f8ba..fe16cabdf0e 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.h +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1331_base { +namespace esphome::ssd1331_base { class SSD1331 : public display::DisplayBuffer { public: @@ -43,5 +42,4 @@ class SSD1331 : public display::DisplayBuffer { float brightness_{1.0}; }; -} // namespace ssd1331_base -} // namespace esphome +} // namespace esphome::ssd1331_base diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.cpp b/esphome/components/ssd1331_spi/ssd1331_spi.cpp index 232822d1924..a5118cdfbef 100644 --- a/esphome/components/ssd1331_spi/ssd1331_spi.cpp +++ b/esphome/components/ssd1331_spi/ssd1331_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1331_spi { +namespace esphome::ssd1331_spi { static const char *const TAG = "ssd1331_spi"; @@ -52,5 +51,4 @@ void HOT SPISSD1331::write_display_data() { this->disable(); } -} // namespace ssd1331_spi -} // namespace esphome +} // namespace esphome::ssd1331_spi diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.h b/esphome/components/ssd1331_spi/ssd1331_spi.h index 93b2e228b11..acdc004b26a 100644 --- a/esphome/components/ssd1331_spi/ssd1331_spi.h +++ b/esphome/components/ssd1331_spi/ssd1331_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1331_base/ssd1331_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1331_spi { +namespace esphome::ssd1331_spi { class SPISSD1331 : public ssd1331_base::SSD1331, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -25,5 +24,4 @@ class SPISSD1331 : public ssd1331_base::SSD1331, GPIOPin *dc_pin_; }; -} // namespace ssd1331_spi -} // namespace esphome +} // namespace esphome::ssd1331_spi diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 69bf67f4766..3df575499d9 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ssd1351_base { +namespace esphome::ssd1351_base { static const char *const TAG = "ssd1351"; @@ -198,5 +197,4 @@ const char *SSD1351::model_str_() { } } -} // namespace ssd1351_base -} // namespace esphome +} // namespace esphome::ssd1351_base diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h index 62777a60a05..325241b77eb 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.h +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace ssd1351_base { +namespace esphome::ssd1351_base { enum SSD1351Model { SSD1351_MODEL_128_96 = 0, @@ -52,5 +51,4 @@ class SSD1351 : public display::DisplayBuffer { float brightness_{1.0}; }; -} // namespace ssd1351_base -} // namespace esphome +} // namespace esphome::ssd1351_base diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp index b046f0adcb7..f6d2323e9cc 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.cpp +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace ssd1351_spi { +namespace esphome::ssd1351_spi { static const char *const TAG = "ssd1351_spi"; @@ -68,5 +67,4 @@ void HOT SPISSD1351::write_display_data() { this->disable(); } -} // namespace ssd1351_spi -} // namespace esphome +} // namespace esphome::ssd1351_spi diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h index b8f3310f5c9..5ce41c1f9ed 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.h +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/ssd1351_base/ssd1351_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace ssd1351_spi { +namespace esphome::ssd1351_spi { class SPISSD1351 : public ssd1351_base::SSD1351, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -26,5 +25,4 @@ class SPISSD1351 : public ssd1351_base::SSD1351, GPIOPin *dc_pin_; }; -} // namespace ssd1351_spi -} // namespace esphome +} // namespace esphome::ssd1351_spi diff --git a/esphome/components/st7567_base/st7567_base.cpp b/esphome/components/st7567_base/st7567_base.cpp index 8c47094b26a..fff3aa7e039 100644 --- a/esphome/components/st7567_base/st7567_base.cpp +++ b/esphome/components/st7567_base/st7567_base.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace st7567_base { +namespace esphome::st7567_base { static const char *const TAG = "st7567"; @@ -157,5 +156,4 @@ void ST7567::init_reset_() { const char *ST7567::model_str_() { return "ST7567 128x64"; } -} // namespace st7567_base -} // namespace esphome +} // namespace esphome::st7567_base diff --git a/esphome/components/st7567_base/st7567_base.h b/esphome/components/st7567_base/st7567_base.h index e3053673d1a..ea897632445 100644 --- a/esphome/components/st7567_base/st7567_base.h +++ b/esphome/components/st7567_base/st7567_base.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace st7567_base { +namespace esphome::st7567_base { static const uint8_t ST7567_BOOSTER_ON = 0x2C; // internal power supply on static const uint8_t ST7567_REGULATOR_ON = 0x2E; // internal power supply on @@ -96,5 +95,4 @@ class ST7567 : public display::DisplayBuffer { bool refresh_requested_{false}; }; -} // namespace st7567_base -} // namespace esphome +} // namespace esphome::st7567_base diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 3214339571e..c99815ecd69 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -1,8 +1,7 @@ #include "st7567_i2c.h" #include "esphome/core/log.h" -namespace esphome { -namespace st7567_i2c { +namespace esphome::st7567_i2c { static const char *const TAG = "st7567_i2c"; @@ -58,5 +57,4 @@ void HOT I2CST7567::write_display_data() { } } -} // namespace st7567_i2c -} // namespace esphome +} // namespace esphome::st7567_i2c diff --git a/esphome/components/st7567_i2c/st7567_i2c.h b/esphome/components/st7567_i2c/st7567_i2c.h index f760a54945c..49489d79e67 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.h +++ b/esphome/components/st7567_i2c/st7567_i2c.h @@ -4,8 +4,7 @@ #include "esphome/components/st7567_base/st7567_base.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace st7567_i2c { +namespace esphome::st7567_i2c { class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice { public: @@ -19,5 +18,4 @@ class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice { enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; }; -} // namespace st7567_i2c -} // namespace esphome +} // namespace esphome::st7567_i2c diff --git a/esphome/components/st7567_spi/st7567_spi.cpp b/esphome/components/st7567_spi/st7567_spi.cpp index 7476fd7c8d5..fae8c313814 100644 --- a/esphome/components/st7567_spi/st7567_spi.cpp +++ b/esphome/components/st7567_spi/st7567_spi.cpp @@ -1,8 +1,7 @@ #include "st7567_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace st7567_spi { +namespace esphome::st7567_spi { static const char *const TAG = "st7567_spi"; @@ -63,5 +62,4 @@ void HOT SPIST7567::write_display_data() { } } -} // namespace st7567_spi -} // namespace esphome +} // namespace esphome::st7567_spi diff --git a/esphome/components/st7567_spi/st7567_spi.h b/esphome/components/st7567_spi/st7567_spi.h index e8d1ebe0cce..fb6f9501a92 100644 --- a/esphome/components/st7567_spi/st7567_spi.h +++ b/esphome/components/st7567_spi/st7567_spi.h @@ -4,8 +4,7 @@ #include "esphome/components/st7567_base/st7567_base.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace st7567_spi { +namespace esphome::st7567_spi { class SPIST7567 : public st7567_base::ST7567, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, @@ -25,5 +24,4 @@ class SPIST7567 : public st7567_base::ST7567, GPIOPin *dc_pin_; }; -} // namespace st7567_spi -} // namespace esphome +} // namespace esphome::st7567_spi diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 0fcfdd6c717..7eb70c5c235 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace st7735 { +namespace esphome::st7735 { static const uint8_t ST_CMD_DELAY = 0x80; // special signifier for command lists @@ -476,5 +475,4 @@ void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { this->write_array(byte, 4); } -} // namespace st7735 -} // namespace esphome +} // namespace esphome::st7735 diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index e81be520ed9..7fa0ad73357 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -4,8 +4,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace st7735 { +namespace esphome::st7735 { static const uint8_t ST7735_TFTWIDTH_128 = 128; // for 1.44 and mini^M static const uint8_t ST7735_TFTWIDTH_80 = 80; // for mini^M @@ -85,5 +84,4 @@ class ST7735 : public display::DisplayBuffer, GPIOPin *dc_pin_{nullptr}; }; -} // namespace st7735 -} // namespace esphome +} // namespace esphome::st7735 diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index dc03fb04cae..b3a60af8c33 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <algorithm> -namespace esphome { -namespace st7789v { +namespace esphome::st7789v { static const char *const TAG = "st7789v"; #ifdef USE_ESP32 @@ -317,5 +316,4 @@ void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { } } -} // namespace st7789v -} // namespace esphome +} // namespace esphome::st7789v diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 29ea3159791..3f9942b1173 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -7,8 +7,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace st7789v { +namespace esphome::st7789v { static const uint8_t ST7789_NOP = 0x00; // No Operation static const uint8_t ST7789_SWRESET = 0x01; // Software Reset @@ -168,5 +167,4 @@ class ST7789V : public display::DisplayBuffer, const char *model_str_; }; -} // namespace st7789v -} // namespace esphome +} // namespace esphome::st7789v diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index a840f981523..429ed9e57be 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -3,8 +3,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace st7920 { +namespace esphome::st7920 { static const char *const TAG = "st7920"; @@ -154,5 +153,4 @@ void ST7920::display_init_() { this->write_display_data(); } -} // namespace st7920 -} // namespace esphome +} // namespace esphome::st7920 diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c48fe8cc1c3..71fe7aa89c0 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -4,8 +4,7 @@ #include "esphome/components/display/display_buffer.h" #include "esphome/components/spi/spi.h" -namespace esphome { -namespace st7920 { +namespace esphome::st7920 { class ST7920; @@ -47,5 +46,4 @@ class ST7920 : public display::DisplayBuffer, st7920_writer_t writer_local_{}; }; -} // namespace st7920 -} // namespace esphome +} // namespace esphome::st7920 diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp index 7d773bc56eb..7086e462a75 100644 --- a/esphome/components/statsd/statsd.cpp +++ b/esphome/components/statsd/statsd.cpp @@ -3,8 +3,8 @@ #include "statsd.h" #ifdef USE_NETWORK -namespace esphome { -namespace statsd { + +namespace esphome::statsd { // send UDP packet if we reach 1Kb packed size // this is needed since statsD does not support fragmented UDP packets @@ -165,6 +165,6 @@ void StatsdComponent::send_(std::string *out) { #endif } -} // namespace statsd -} // namespace esphome +} // namespace esphome::statsd + #endif diff --git a/esphome/components/statsd/statsd.h b/esphome/components/statsd/statsd.h index eab77a7a6e0..349bffe6fbd 100644 --- a/esphome/components/statsd/statsd.h +++ b/esphome/components/statsd/statsd.h @@ -25,8 +25,7 @@ #include "IPAddress.h" #endif -namespace esphome { -namespace statsd { +namespace esphome::statsd { class StatsdComponent : public PollingComponent { public: @@ -82,6 +81,6 @@ class StatsdComponent : public PollingComponent { void send_(std::string *out); }; -} // namespace statsd -} // namespace esphome +} // namespace esphome::statsd + #endif diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index ec7bf2dae16..341d3bfce53 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -3,8 +3,7 @@ #include "esphome/core/application.h" #include <cinttypes> -namespace esphome { -namespace status_led { +namespace esphome::status_led { static const char *const TAG = "status_led"; @@ -71,5 +70,4 @@ void StatusLEDLightOutput::output_state_(bool state) { this->output_->set_state(state); } -} // namespace status_led -} // namespace esphome +} // namespace esphome::status_led diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index 3a745e0017a..0483669d0ab 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -5,8 +5,7 @@ #include "esphome/components/light/light_output.h" #include "esphome/components/output/binary_output.h" -namespace esphome { -namespace status_led { +namespace esphome::status_led { class StatusLEDLightOutput : public light::LightOutput, public Component { public: @@ -39,5 +38,4 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void output_state_(bool state); }; -} // namespace status_led -} // namespace esphome +} // namespace esphome::status_led diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 48762a73334..ae37b3fae78 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace status_led { +namespace esphome::status_led { static const char *const TAG = "status_led"; @@ -40,5 +39,4 @@ void StatusLED::loop() { } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace status_led -} // namespace esphome +} // namespace esphome::status_led diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index a4b5db93d7e..bda144d2cdd 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace status_led { +namespace esphome::status_led { class StatusLED : public Component { public: @@ -21,5 +20,4 @@ class StatusLED : public Component { extern StatusLED *global_status_led; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace status_led -} // namespace esphome +} // namespace esphome::status_led diff --git a/esphome/components/stepper/stepper.cpp b/esphome/components/stepper/stepper.cpp index 79260242047..54df83782ec 100644 --- a/esphome/components/stepper/stepper.cpp +++ b/esphome/components/stepper/stepper.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace stepper { +namespace esphome::stepper { static const char *const TAG = "stepper"; @@ -47,5 +46,4 @@ int32_t Stepper::should_step_() { return 0; } -} // namespace stepper -} // namespace esphome +} // namespace esphome::stepper diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 2bad672494f..9fbd0d92e6e 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -namespace esphome { -namespace stepper { +namespace esphome::stepper { #define LOG_STEPPER(this) \ ESP_LOGCONFIG(TAG, \ @@ -108,5 +107,4 @@ template<typename... Ts> class SetDecelerationAction : public Action<Ts...> { Stepper *parent_; }; -} // namespace stepper -} // namespace esphome +} // namespace esphome::stepper diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index 8713b0b6b89..ff2a7748bf0 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -1,8 +1,7 @@ #include "sts3x.h" #include "esphome/core/log.h" -namespace esphome { -namespace sts3x { +namespace esphome::sts3x { static const char *const TAG = "sts3x"; @@ -66,5 +65,4 @@ void STS3XComponent::update() { }); } -} // namespace sts3x -} // namespace esphome +} // namespace esphome::sts3x diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 6c1dd2b2442..038fa0dd802 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace sts3x { +namespace esphome::sts3x { /// This class implements support for the ST3x-DIS family of temperature i2c sensors. class STS3XComponent : public sensor::Sensor, public PollingComponent, public sensirion_common::SensirionI2CDevice { @@ -17,5 +16,4 @@ class STS3XComponent : public sensor::Sensor, public PollingComponent, public se void update() override; }; -} // namespace sts3x -} // namespace esphome +} // namespace esphome::sts3x diff --git a/esphome/components/sun/sensor/sun_sensor.cpp b/esphome/components/sun/sensor/sun_sensor.cpp index 6c90722c29a..d788e19ea41 100644 --- a/esphome/components/sun/sensor/sun_sensor.cpp +++ b/esphome/components/sun/sensor/sun_sensor.cpp @@ -1,12 +1,10 @@ #include "sun_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace sun { +namespace esphome::sun { static const char *const TAG = "sun.sensor"; void SunSensor::dump_config() { LOG_SENSOR("", "Sun Sensor", this); } -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun/sensor/sun_sensor.h b/esphome/components/sun/sensor/sun_sensor.h index 2bd33375eff..148e5297d94 100644 --- a/esphome/components/sun/sensor/sun_sensor.h +++ b/esphome/components/sun/sensor/sun_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/sun/sun.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace sun { +namespace esphome::sun { enum SensorType { SUN_SENSOR_ELEVATION, @@ -37,5 +36,4 @@ class SunSensor : public sensor::Sensor, public PollingComponent { SensorType type_; }; -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index d55a14f192e..d03ff07981e 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -12,8 +12,7 @@ like exact nutation are not included. But in some testing the accuracy appears t for random spots around the globe. */ -namespace esphome { -namespace sun { +namespace esphome::sun { using namespace esphome::sun::internal; @@ -322,5 +321,4 @@ optional<ESPTime> Sun::sunset(ESPTime date, double elevation) { return this->cal double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 67a0306a37b..2999c93c715 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -7,8 +7,7 @@ #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace sun { +namespace esphome::sun { namespace internal { @@ -129,5 +128,4 @@ template<typename... Ts> class SunCondition : public Condition<Ts...>, public Pa bool above_; }; -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.cpp b/esphome/components/sun/text_sensor/sun_text_sensor.cpp index c047b87fdd1..21aa4b86e0f 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.cpp +++ b/esphome/components/sun/text_sensor/sun_text_sensor.cpp @@ -1,12 +1,10 @@ #include "sun_text_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace sun { +namespace esphome::sun { static const char *const TAG = "sun.text_sensor"; void SunTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sun Text Sensor", this); } -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index c3b60ffd65e..65b0e358d0b 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -6,8 +6,7 @@ #include "esphome/components/sun/sun.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace sun { +namespace esphome::sun { class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { public: @@ -44,5 +43,4 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { bool sunrise_; }; -} // namespace sun -} // namespace esphome +} // namespace esphome::sun diff --git a/esphome/components/sun_gtil2/sun_gtil2.cpp b/esphome/components/sun_gtil2/sun_gtil2.cpp index d416d9a636b..78e398d0868 100644 --- a/esphome/components/sun_gtil2/sun_gtil2.cpp +++ b/esphome/components/sun_gtil2/sun_gtil2.cpp @@ -1,8 +1,7 @@ #include "sun_gtil2.h" #include "esphome/core/log.h" -namespace esphome { -namespace sun_gtil2 { +namespace esphome::sun_gtil2 { static const char *const TAG = "sun_gtil2"; @@ -131,5 +130,4 @@ void SunGTIL2::dump_config() { #endif } -} // namespace sun_gtil2 -} // namespace esphome +} // namespace esphome::sun_gtil2 diff --git a/esphome/components/sun_gtil2/sun_gtil2.h b/esphome/components/sun_gtil2/sun_gtil2.h index 3e28527cf7a..e774fefcf8c 100644 --- a/esphome/components/sun_gtil2/sun_gtil2.h +++ b/esphome/components/sun_gtil2/sun_gtil2.h @@ -13,8 +13,7 @@ #endif #include "esphome/components/uart/uart.h" -namespace esphome { -namespace sun_gtil2 { +namespace esphome::sun_gtil2 { class SunGTIL2 : public Component, public uart::UARTDevice { public: @@ -58,5 +57,4 @@ class SunGTIL2 : public Component, public uart::UARTDevice { std::vector<uint8_t> rx_message_; }; -} // namespace sun_gtil2 -} // namespace esphome +} // namespace esphome::sun_gtil2 diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index ed5986e0974..2721cbfbbfa 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sx126x/sx126x.h" -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX126x> { public: @@ -65,5 +64,4 @@ template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, publ void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); } }; -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp index 59d80bd2974..5e992cc731b 100644 --- a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "sx126x_transport.h" -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { static const char *const TAG = "sx126x_transport"; @@ -16,5 +15,4 @@ void SX126xTransport::send_packet(const std::vector<uint8_t> &buf) const { this- void SX126xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); } -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h index 640c6a76f91..7590e35c28d 100644 --- a/esphome/components/sx126x/packet_transport/sx126x_transport.h +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h @@ -5,8 +5,7 @@ #include "esphome/components/packet_transport/packet_transport.h" #include <vector> -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener { public: @@ -20,5 +19,4 @@ class SX126xTransport : public packet_transport::PacketTransport, public Parente size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } }; -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 02f7d972a99..6e6857fadbb 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { static const char *const TAG = "sx126x"; static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400}; @@ -547,5 +546,4 @@ void SX126x::dump_config() { } } -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 87bbf18c793..8298beb36e0 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -8,8 +8,7 @@ #include <utility> #include <vector> -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { enum SX126xBw : uint8_t { // FSK @@ -146,5 +145,4 @@ class SX126x : public Component, bool rf_switch_{false}; }; -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx126x/sx126x_reg.h b/esphome/components/sx126x/sx126x_reg.h index 143f4a05da2..c70817364fc 100644 --- a/esphome/components/sx126x/sx126x_reg.h +++ b/esphome/components/sx126x/sx126x_reg.h @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace sx126x { +namespace esphome::sx126x { static const uint32_t XTAL_FREQ = 32000000; @@ -161,5 +160,4 @@ enum SX126xRampTime : uint8_t { PA_RAMP_3400 = 0x07, }; -} // namespace sx126x -} // namespace esphome +} // namespace esphome::sx126x diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index fb0367fcca0..7a2eb7ee8d9 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/sx127x/sx127x.h" -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX127x> { public: @@ -64,5 +63,4 @@ template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, publ void play(const Ts &...x) override { this->parent_->set_mode_standby(); } }; -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp index 893726e816e..52d5631791c 100644 --- a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "sx127x_transport.h" -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { static const char *const TAG = "sx127x_transport"; @@ -16,5 +15,4 @@ void SX127xTransport::send_packet(const std::vector<uint8_t> &buf) const { this- void SX127xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); } -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h index 62083729714..5dcfe02c339 100644 --- a/esphome/components/sx127x/packet_transport/sx127x_transport.h +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h @@ -5,8 +5,7 @@ #include "esphome/components/packet_transport/packet_transport.h" #include <vector> -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener { public: @@ -20,5 +19,4 @@ class SX127xTransport : public packet_transport::PacketTransport, public Parente size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } }; -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 2b13efb38da..0596e91ccc0 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { static const char *const TAG = "sx127x"; static const uint32_t FXOSC = 32000000u; @@ -507,5 +506,4 @@ void SX127x::dump_config() { } } -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index 76f942fddac..376c987ed15 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -7,8 +7,7 @@ #include "esphome/core/hal.h" #include <vector> -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { enum SX127xBw : uint8_t { SX127X_BW_2_6, @@ -126,5 +125,4 @@ class SX127x : public Component, bool rx_start_{false}; }; -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx127x/sx127x_reg.h b/esphome/components/sx127x/sx127x_reg.h index d5e9c50957e..295af738cc1 100644 --- a/esphome/components/sx127x/sx127x_reg.h +++ b/esphome/components/sx127x/sx127x_reg.h @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace sx127x { +namespace esphome::sx127x { enum SX127xReg : uint8_t { // Common registers @@ -291,5 +290,4 @@ enum SX127xModemCfg3 : uint8_t { MODEM_AGC_AUTO_ON = 0x04, }; -} // namespace sx127x -} // namespace esphome +} // namespace esphome::sx127x diff --git a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h index 2eef19782c0..bcd89015307 100644 --- a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h +++ b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h @@ -3,8 +3,7 @@ #include "esphome/components/sx1509/sx1509.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor { public: @@ -15,5 +14,4 @@ class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor: uint16_t key_{0}; }; -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp index 4a24d78478d..528de1fde5a 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.cpp +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { static const char *const TAG = "sx1509_float_channel"; @@ -29,5 +28,4 @@ void SX1509FloatOutputChannel::dump_config() { LOG_FLOAT_OUTPUT(this); } -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/output/sx1509_float_output.h b/esphome/components/sx1509/output/sx1509_float_output.h index 39e51839ea4..ee53cef637a 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.h +++ b/esphome/components/sx1509/output/sx1509_float_output.h @@ -3,8 +3,7 @@ #include "esphome/components/sx1509/sx1509.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { class SX1509Component; @@ -23,5 +22,4 @@ class SX1509FloatOutputChannel : public output::FloatOutput, public Component { uint8_t pin_; }; -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 1cdae76eaf4..2397049000d 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { static const char *const TAG = "sx1509"; @@ -313,5 +312,4 @@ void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8 set_debounce_pin_(i + 8); } -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index f98fc0a44f3..f645ede7549 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -10,8 +10,7 @@ #include <vector> -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { // These are used for clock config: const uint8_t INTERNAL_CLOCK_2MHZ = 2; @@ -97,5 +96,4 @@ class SX1509Component : public Component, void clock_(uint8_t osc_source = 2, uint8_t osc_pin_function = 1, uint8_t osc_freq_out = 0, uint8_t osc_divider = 0); }; -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index a7e5d0514d9..28ef1c5830f 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -3,8 +3,7 @@ #include "sx1509.h" #include "sx1509_gpio_pin.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { static const char *const TAG = "sx1509_gpio_pin"; @@ -16,5 +15,4 @@ size_t SX1509GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via sx1509", this->pin_); } -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h index 5903af9d12a..9dcad37b272 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.h +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -2,8 +2,7 @@ #include "esphome/core/gpio.h" -namespace esphome { -namespace sx1509 { +namespace esphome::sx1509 { class SX1509Component; @@ -29,5 +28,4 @@ class SX1509GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index 9712cacf9b2..8349a8b8292 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -7,7 +7,6 @@ https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library */ #pragma once -namespace esphome { /** Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the @@ -25,7 +24,7 @@ local, and you've found our code helpful, please buy us a round! Distributed as-is; no warranty is given. */ -namespace sx1509 { +namespace esphome::sx1509 { const uint8_t REG_INPUT_DISABLE_B = 0x00; // RegInputDisableB Input buffer disable register _ I/O[15_8] (Bank B) 0000 0000 @@ -106,5 +105,4 @@ const uint8_t REG_RESET = 0x7D; // RegReset Software reset register 0000 00 const uint8_t REG_TEST_1 = 0x7E; // RegTest1 Test register 0000 0000 const uint8_t REG_TEST_2 = 0x7F; // RegTest2 Test register 0000 0000 -} // namespace sx1509 -} // namespace esphome +} // namespace esphome::sx1509 From 6ffcb821cae98cc4b419d6fe5efd84ad52f8eb86 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 21:13:38 -0400 Subject: [PATCH 1897/2056] [clang-tidy] Concatenate nested namespaces (6/7: components t-z) (#16305) --- esphome/components/t6615/t6615.cpp | 6 ++---- esphome/components/t6615/t6615.h | 6 ++---- esphome/components/tc74/tc74.cpp | 6 ++---- esphome/components/tc74/tc74.h | 6 ++---- esphome/components/tca9548a/tca9548a.cpp | 6 ++---- esphome/components/tca9548a/tca9548a.h | 6 ++---- esphome/components/tca9555/tca9555.cpp | 6 ++---- esphome/components/tca9555/tca9555.h | 6 ++---- esphome/components/tcl112/tcl112.cpp | 6 ++---- esphome/components/tcl112/tcl112.h | 6 ++---- esphome/components/tcs34725/tcs34725.cpp | 6 ++---- esphome/components/tcs34725/tcs34725.h | 6 ++---- esphome/components/tee501/tee501.cpp | 6 ++---- esphome/components/tee501/tee501.h | 6 ++---- esphome/components/teleinfo/sensor/teleinfo_sensor.cpp | 7 +++---- esphome/components/teleinfo/sensor/teleinfo_sensor.h | 6 ++---- esphome/components/teleinfo/teleinfo.cpp | 6 ++---- esphome/components/teleinfo/teleinfo.h | 6 ++---- .../teleinfo/text_sensor/teleinfo_text_sensor.cpp | 7 +++---- .../components/teleinfo/text_sensor/teleinfo_text_sensor.h | 7 +++---- esphome/components/tem3200/tem3200.cpp | 6 ++---- esphome/components/tem3200/tem3200.h | 6 ++---- esphome/components/time_based/cover/time_based_cover.cpp | 6 ++---- esphome/components/time_based/cover/time_based_cover.h | 6 ++---- esphome/components/tlc59208f/tlc59208f_output.cpp | 6 ++---- esphome/components/tlc59208f/tlc59208f_output.h | 6 ++---- esphome/components/tlc5947/output/tlc5947_output.cpp | 6 ++---- esphome/components/tlc5947/output/tlc5947_output.h | 6 ++---- esphome/components/tlc5947/tlc5947.cpp | 6 ++---- esphome/components/tlc5947/tlc5947.h | 6 ++---- esphome/components/tlc5971/output/tlc5971_output.cpp | 6 ++---- esphome/components/tlc5971/output/tlc5971_output.h | 6 ++---- esphome/components/tlc5971/tlc5971.cpp | 6 ++---- esphome/components/tlc5971/tlc5971.h | 6 ++---- esphome/components/tm1621/tm1621.cpp | 6 ++---- esphome/components/tm1621/tm1621.h | 6 ++---- esphome/components/tm1637/tm1637.cpp | 6 ++---- esphome/components/tm1637/tm1637.h | 6 ++---- esphome/components/tm1638/binary_sensor/tm1638_key.cpp | 6 ++---- esphome/components/tm1638/binary_sensor/tm1638_key.h | 6 ++---- esphome/components/tm1638/output/tm1638_output_led.cpp | 6 ++---- esphome/components/tm1638/output/tm1638_output_led.h | 6 ++---- esphome/components/tm1638/sevenseg.h | 6 ++---- esphome/components/tm1638/switch/tm1638_switch_led.cpp | 6 ++---- esphome/components/tm1638/switch/tm1638_switch_led.h | 6 ++---- esphome/components/tm1638/tm1638.cpp | 6 ++---- esphome/components/tm1638/tm1638.h | 6 ++---- esphome/components/tm1651/tm1651.cpp | 6 ++---- esphome/components/tm1651/tm1651.h | 6 ++---- esphome/components/tmp102/tmp102.cpp | 6 ++---- esphome/components/tmp102/tmp102.h | 6 ++---- esphome/components/tmp1075/tmp1075.cpp | 6 ++---- esphome/components/tmp1075/tmp1075.h | 6 ++---- esphome/components/tof10120/tof10120_sensor.cpp | 6 ++---- esphome/components/tof10120/tof10120_sensor.h | 6 ++---- esphome/components/tormatic/tormatic_cover.cpp | 6 ++---- esphome/components/tormatic/tormatic_cover.h | 6 ++---- esphome/components/tormatic/tormatic_protocol.h | 6 ++---- esphome/components/toshiba/toshiba.cpp | 6 ++---- esphome/components/toshiba/toshiba.h | 6 ++---- .../binary_sensor/touchscreen_binary_sensor.cpp | 6 ++---- .../touchscreen/binary_sensor/touchscreen_binary_sensor.h | 6 ++---- esphome/components/touchscreen/touchscreen.cpp | 6 ++---- esphome/components/touchscreen/touchscreen.h | 6 ++---- esphome/components/tsl2561/tsl2561.cpp | 6 ++---- esphome/components/tsl2561/tsl2561.h | 6 ++---- esphome/components/tsl2591/tsl2591.cpp | 6 ++---- esphome/components/tsl2591/tsl2591.h | 6 ++---- .../components/tt21100/binary_sensor/tt21100_button.cpp | 6 ++---- esphome/components/tt21100/binary_sensor/tt21100_button.h | 6 ++---- esphome/components/tt21100/touchscreen/tt21100.cpp | 6 ++---- esphome/components/tt21100/touchscreen/tt21100.h | 6 ++---- esphome/components/ttp229_bsf/ttp229_bsf.cpp | 6 ++---- esphome/components/ttp229_bsf/ttp229_bsf.h | 6 ++---- esphome/components/ttp229_lsf/ttp229_lsf.cpp | 6 ++---- esphome/components/ttp229_lsf/ttp229_lsf.h | 6 ++---- esphome/components/tuya/automation.cpp | 6 ++---- esphome/components/tuya/automation.h | 6 ++---- .../components/tuya/binary_sensor/tuya_binary_sensor.cpp | 6 ++---- esphome/components/tuya/binary_sensor/tuya_binary_sensor.h | 6 ++---- esphome/components/tuya/climate/tuya_climate.cpp | 6 ++---- esphome/components/tuya/climate/tuya_climate.h | 6 ++---- esphome/components/tuya/cover/tuya_cover.cpp | 6 ++---- esphome/components/tuya/cover/tuya_cover.h | 6 ++---- esphome/components/tuya/fan/tuya_fan.cpp | 6 ++---- esphome/components/tuya/fan/tuya_fan.h | 6 ++---- esphome/components/tuya/light/tuya_light.cpp | 6 ++---- esphome/components/tuya/light/tuya_light.h | 6 ++---- esphome/components/tuya/number/tuya_number.cpp | 6 ++---- esphome/components/tuya/number/tuya_number.h | 6 ++---- esphome/components/tuya/select/tuya_select.cpp | 6 ++---- esphome/components/tuya/select/tuya_select.h | 6 ++---- esphome/components/tuya/sensor/tuya_sensor.cpp | 6 ++---- esphome/components/tuya/sensor/tuya_sensor.h | 6 ++---- esphome/components/tuya/switch/tuya_switch.cpp | 6 ++---- esphome/components/tuya/switch/tuya_switch.h | 6 ++---- esphome/components/tuya/text_sensor/tuya_text_sensor.cpp | 6 ++---- esphome/components/tuya/text_sensor/tuya_text_sensor.h | 6 ++---- esphome/components/tuya/tuya.cpp | 6 ++---- esphome/components/tuya/tuya.h | 6 ++---- esphome/components/tx20/tx20.cpp | 6 ++---- esphome/components/tx20/tx20.h | 6 ++---- esphome/components/udp/automation.h | 7 +++---- esphome/components/udp/packet_transport/udp_transport.cpp | 6 ++---- esphome/components/udp/packet_transport/udp_transport.h | 7 +++---- esphome/components/ufire_ec/ufire_ec.cpp | 6 ++---- esphome/components/ufire_ec/ufire_ec.h | 6 ++---- esphome/components/ufire_ise/ufire_ise.cpp | 6 ++---- esphome/components/ufire_ise/ufire_ise.h | 6 ++---- esphome/components/update/automation.h | 6 ++---- esphome/components/update/update_entity.cpp | 6 ++---- esphome/components/update/update_entity.h | 6 ++---- .../uponor_smatrix/climate/uponor_smatrix_climate.cpp | 6 ++---- .../uponor_smatrix/climate/uponor_smatrix_climate.h | 6 ++---- .../uponor_smatrix/sensor/uponor_smatrix_sensor.cpp | 6 ++---- .../uponor_smatrix/sensor/uponor_smatrix_sensor.h | 6 ++---- esphome/components/uponor_smatrix/uponor_smatrix.cpp | 6 ++---- esphome/components/uponor_smatrix/uponor_smatrix.h | 6 ++---- esphome/components/valve/automation.h | 6 ++---- esphome/components/valve/valve.cpp | 6 ++---- esphome/components/valve/valve.h | 6 ++---- esphome/components/valve/valve_traits.h | 6 ++---- .../components/vbus/binary_sensor/vbus_binary_sensor.cpp | 6 ++---- esphome/components/vbus/binary_sensor/vbus_binary_sensor.h | 6 ++---- esphome/components/vbus/sensor/vbus_sensor.cpp | 6 ++---- esphome/components/vbus/sensor/vbus_sensor.h | 6 ++---- esphome/components/vbus/vbus.cpp | 6 ++---- esphome/components/vbus/vbus.h | 6 ++---- esphome/components/veml3235/veml3235.cpp | 6 ++---- esphome/components/veml3235/veml3235.h | 6 ++---- esphome/components/veml7700/veml7700.cpp | 6 ++---- esphome/components/veml7700/veml7700.h | 6 ++---- esphome/components/vl53l0x/vl53l0x_sensor.cpp | 6 ++---- esphome/components/vl53l0x/vl53l0x_sensor.h | 6 ++---- esphome/components/voice_assistant/voice_assistant.cpp | 6 ++---- esphome/components/voice_assistant/voice_assistant.h | 6 ++---- esphome/components/voltage_sampler/voltage_sampler.h | 6 ++---- esphome/components/wake_on_lan/wake_on_lan.cpp | 7 +++---- esphome/components/wake_on_lan/wake_on_lan.h | 7 +++---- esphome/components/watchdog/watchdog.cpp | 6 ++---- esphome/components/watchdog/watchdog.h | 6 ++---- esphome/components/waveshare_epaper/waveshare_213v3.cpp | 6 ++---- esphome/components/waveshare_epaper/waveshare_epaper.cpp | 6 ++---- esphome/components/waveshare_epaper/waveshare_epaper.h | 6 ++---- esphome/components/weikai/weikai.cpp | 6 ++---- esphome/components/weikai/weikai.h | 6 ++---- esphome/components/weikai/wk_reg_def.h | 6 ++---- esphome/components/weikai_i2c/weikai_i2c.cpp | 6 ++---- esphome/components/weikai_i2c/weikai_i2c.h | 6 ++---- esphome/components/weikai_spi/weikai_spi.cpp | 6 ++---- esphome/components/weikai_spi/weikai_spi.h | 6 ++---- esphome/components/whirlpool/whirlpool.cpp | 6 ++---- esphome/components/whirlpool/whirlpool.h | 6 ++---- esphome/components/whynter/whynter.cpp | 6 ++---- esphome/components/whynter/whynter.h | 6 ++---- esphome/components/wiegand/wiegand.cpp | 6 ++---- esphome/components/wiegand/wiegand.h | 6 ++---- esphome/components/wk2132_i2c/wk2132_i2c.cpp | 4 +--- esphome/components/wl_134/wl_134.cpp | 6 ++---- esphome/components/wl_134/wl_134.h | 6 ++---- esphome/components/wled/wled_light_effect.cpp | 6 ++---- esphome/components/wled/wled_light_effect.h | 6 ++---- esphome/components/wts01/wts01.cpp | 6 ++---- esphome/components/wts01/wts01.h | 6 ++---- esphome/components/x9c/x9c.cpp | 6 ++---- esphome/components/x9c/x9c.h | 6 ++---- esphome/components/xgzp68xx/xgzp68xx.cpp | 6 ++---- esphome/components/xgzp68xx/xgzp68xx.h | 6 ++---- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 6 ++---- esphome/components/xiaomi_ble/xiaomi_ble.h | 6 ++---- esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp | 6 ++---- esphome/components/xiaomi_cgd1/xiaomi_cgd1.h | 6 ++---- esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp | 6 ++---- esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h | 6 ++---- esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp | 6 ++---- esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 6 ++---- esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp | 6 ++---- esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h | 6 ++---- esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp | 6 ++---- esphome/components/xiaomi_gcls002/xiaomi_gcls002.h | 6 ++---- esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp | 6 ++---- esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h | 6 ++---- esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp | 6 ++---- esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h | 6 ++---- esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp | 6 ++---- esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h | 6 ++---- esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp | 6 ++---- esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h | 6 ++---- esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp | 6 ++---- esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h | 6 ++---- esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp | 6 ++---- esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h | 6 ++---- esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp | 6 ++---- esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h | 6 ++---- esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp | 6 ++---- esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h | 6 ++---- esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp | 6 ++---- esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h | 6 ++---- esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp | 6 ++---- esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h | 6 ++---- esphome/components/xiaomi_miscale/xiaomi_miscale.cpp | 6 ++---- esphome/components/xiaomi_miscale/xiaomi_miscale.h | 6 ++---- esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp | 6 ++---- esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h | 6 ++---- esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp | 6 ++---- esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h | 6 ++---- esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp | 6 ++---- esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 6 ++---- esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp | 6 ++---- esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h | 6 ++---- .../components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp | 6 ++---- esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h | 6 ++---- esphome/components/xl9535/xl9535.cpp | 6 ++---- esphome/components/xl9535/xl9535.h | 6 ++---- esphome/components/xpt2046/touchscreen/xpt2046.cpp | 6 ++---- esphome/components/xpt2046/touchscreen/xpt2046.h | 6 ++---- esphome/components/xxtea/xxtea.cpp | 6 ++---- esphome/components/xxtea/xxtea.h | 6 ++---- esphome/components/yashima/yashima.cpp | 6 ++---- esphome/components/yashima/yashima.h | 6 ++---- esphome/components/zhlt01/zhlt01.cpp | 6 ++---- esphome/components/zhlt01/zhlt01.h | 6 ++---- esphome/components/zio_ultrasonic/zio_ultrasonic.cpp | 6 ++---- esphome/components/zio_ultrasonic/zio_ultrasonic.h | 6 ++---- esphome/components/zyaura/zyaura.cpp | 6 ++---- esphome/components/zyaura/zyaura.h | 6 ++---- 226 files changed, 458 insertions(+), 903 deletions(-) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index 75f9ed108e4..1a98e48c144 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace t6615 { +namespace esphome::t6615 { static const char *const TAG = "t6615"; @@ -92,5 +91,4 @@ void T6615Component::dump_config() { this->check_uart_settings(19200); } -} // namespace t6615 -} // namespace esphome +} // namespace esphome::t6615 diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index 69c406a5bad..0c2088f7b04 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace t6615 { +namespace esphome::t6615 { enum class T6615Command : uint8_t { NONE = 0, @@ -38,5 +37,4 @@ class T6615Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *co2_sensor_{nullptr}; }; -} // namespace t6615 -} // namespace esphome +} // namespace esphome::t6615 diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index cb58e583dca..bc522d1b745 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -3,8 +3,7 @@ #include "tc74.h" #include "esphome/core/log.h" -namespace esphome { -namespace tc74 { +namespace esphome::tc74 { static const char *const TAG = "tc74"; @@ -62,5 +61,4 @@ void TC74Component::read_temperature_() { this->status_clear_warning(); } -} // namespace tc74 -} // namespace esphome +} // namespace esphome::tc74 diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h index f3ce225ff41..4a53f39bc18 100644 --- a/esphome/components/tc74/tc74.h +++ b/esphome/components/tc74/tc74.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tc74 { +namespace esphome::tc74 { class TC74Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: @@ -22,5 +21,4 @@ class TC74Component : public PollingComponent, public i2c::I2CDevice, public sen bool data_ready_ = false; }; -} // namespace tc74 -} // namespace esphome +} // namespace esphome::tc74 diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index 1de3c491087..3fc91a3bbb9 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -1,8 +1,7 @@ #include "tca9548a.h" #include "esphome/core/log.h" -namespace esphome { -namespace tca9548a { +namespace esphome::tca9548a { static const char *const TAG = "tca9548a"; @@ -44,5 +43,4 @@ void TCA9548AComponent::disable_all_channels() { } } -} // namespace tca9548a -} // namespace esphome +} // namespace esphome::tca9548a diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 0fb9ada99ad..f0417ac7f70 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tca9548a { +namespace esphome::tca9548a { static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00; @@ -35,5 +34,4 @@ class TCA9548AComponent : public Component, public i2c::I2CDevice { protected: friend class TCA9548AChannel; }; -} // namespace tca9548a -} // namespace esphome +} // namespace esphome::tca9548a diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 2fefe08c0d7..b210c082dda 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -10,8 +10,7 @@ static const uint8_t TCA9555_POLARITY_REGISTER_1 = 0x05; static const uint8_t TCA9555_CONFIGURATION_PORT_0 = 0x06; static const uint8_t TCA9555_CONFIGURATION_PORT_1 = 0x07; -namespace esphome { -namespace tca9555 { +namespace esphome::tca9555 { static const char *const TAG = "tca9555"; @@ -162,5 +161,4 @@ size_t TCA9555GPIOPin::dump_summary(char *buffer, size_t len) const { return buf_append_printf(buffer, len, 0, "%u via TCA9555", this->pin_); } -} // namespace tca9555 -} // namespace esphome +} // namespace esphome::tca9555 diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index d4d070013c7..7d37edad73b 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace tca9555 { +namespace esphome::tca9555 { class TCA9555Component : public Component, public i2c::I2CDevice, @@ -67,5 +66,4 @@ class TCA9555GPIOPin : public GPIOPin, public Parented<TCA9555Component> { gpio::Flags flags_; }; -} // namespace tca9555 -} // namespace esphome +} // namespace esphome::tca9555 diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index afeee3d7396..cd819e5b16b 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -1,8 +1,7 @@ #include "tcl112.h" #include "esphome/core/log.h" -namespace esphome { -namespace tcl112 { +namespace esphome::tcl112 { static const char *const TAG = "tcl112.climate"; @@ -240,5 +239,4 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace tcl112 -} // namespace esphome +} // namespace esphome::tcl112 diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index e982755d402..0aef2decc82 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace tcl112 { +namespace esphome::tcl112 { // Temperature const float TCL112_TEMP_MAX = 31.0; @@ -24,5 +23,4 @@ class Tcl112Climate : public climate_ir::ClimateIR { bool on_receive(remote_base::RemoteReceiveData data) override; }; -} // namespace tcl112 -} // namespace esphome +} // namespace esphome::tcl112 diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 1098d8de5fe..40c65e9f84b 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <algorithm> -namespace esphome { -namespace tcs34725 { +namespace esphome::tcs34725 { static const char *const TAG = "tcs34725"; @@ -348,5 +347,4 @@ void TCS34725Component::set_glass_attenuation_factor(float ga) { this->glass_attenuation_ = ga; } -} // namespace tcs34725 -} // namespace esphome +} // namespace esphome::tcs34725 diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 85bb383e4b7..15e4fae52ff 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tcs34725 { +namespace esphome::tcs34725 { enum TCS34725IntegrationTime { TCS34725_INTEGRATION_TIME_2_4MS = 0xFF, @@ -85,5 +84,4 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { uint8_t gain_reg_{TCS34725_GAIN_1X}; }; -} // namespace tcs34725 -} // namespace esphome +} // namespace esphome::tcs34725 diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 00a62247f90..c198ff10813 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tee501 { +namespace esphome::tee501 { static const char *const TAG = "tee501"; @@ -66,5 +65,4 @@ void TEE501Component::update() { }); } -} // namespace tee501 -} // namespace esphome +} // namespace esphome::tee501 diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index 62a6f1c9440..4a082913188 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace tee501 { +namespace esphome::tee501 { /// This class implements support for the tee501 of temperature i2c sensors. class TEE501Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { @@ -18,5 +17,4 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; -} // namespace tee501 -} // namespace esphome +} // namespace esphome::tee501 diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp index ad9c6dae004..3878a3967b2 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp @@ -1,7 +1,7 @@ #include "esphome/core/log.h" #include "teleinfo_sensor.h" -namespace esphome { -namespace teleinfo { + +namespace esphome::teleinfo { static const char *const TAG = "teleinfo_sensor"; TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); } @@ -10,5 +10,4 @@ void TeleInfoSensor::publish_val(const std::string &val) { publish_state(newval); } void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", "Teleinfo Sensor", this); } -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.h b/esphome/components/teleinfo/sensor/teleinfo_sensor.h index 56781166abb..37736c4e737 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.h +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.h @@ -2,8 +2,7 @@ #include "esphome/components/teleinfo/teleinfo.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace teleinfo { +namespace esphome::teleinfo { class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component { public: @@ -12,5 +11,4 @@ class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Co void dump_config() override; }; -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index 4d617ae4e61..cd2ddbbb381 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -1,8 +1,7 @@ #include "teleinfo.h" #include "esphome/core/log.h" -namespace esphome { -namespace teleinfo { +namespace esphome::teleinfo { static const char *const TAG = "teleinfo"; @@ -205,5 +204,4 @@ TeleInfo::TeleInfo(bool historical_mode) { } void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); } -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h index 0c6217853e7..eeab3b5103a 100644 --- a/esphome/components/teleinfo/teleinfo.h +++ b/esphome/components/teleinfo/teleinfo.h @@ -5,8 +5,7 @@ #include <vector> -namespace esphome { -namespace teleinfo { +namespace esphome::teleinfo { /* * 198 bytes should be enough to contain a full session in historical mode with * three phases. But go with 1024 just to be sure. @@ -50,5 +49,4 @@ class TeleInfo : public PollingComponent, public uart::UARTDevice { bool check_crc_(const char *grp, const char *grp_end); void publish_value_(const std::string &tag, const std::string &val); }; -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp index 87cf0dea178..7c638d8545b 100644 --- a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp @@ -1,11 +1,10 @@ #include "esphome/core/log.h" #include "teleinfo_text_sensor.h" -namespace esphome { -namespace teleinfo { + +namespace esphome::teleinfo { static const char *const TAG = "teleinfo_text_sensor"; TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); } void TeleInfoTextSensor::publish_val(const std::string &val) { publish_state(val); } void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); } -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h index 5a7dc9d1a73..f4c04a03a03 100644 --- a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h @@ -1,13 +1,12 @@ #pragma once #include "esphome/components/teleinfo/teleinfo.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace teleinfo { + +namespace esphome::teleinfo { class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component { public: TeleInfoTextSensor(const char *tag); void publish_val(const std::string &val) override; void dump_config() override; }; -} // namespace teleinfo -} // namespace esphome +} // namespace esphome::teleinfo diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp index 9c305f8f6f0..72cf31e0a67 100644 --- a/esphome/components/tem3200/tem3200.cpp +++ b/esphome/components/tem3200/tem3200.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tem3200 { +namespace esphome::tem3200 { static const char *const TAG = "tem3200"; @@ -142,5 +141,4 @@ void TEM3200Component::update() { this->status_clear_warning(); } -} // namespace tem3200 -} // namespace esphome +} // namespace esphome::tem3200 diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h index 37589b2a066..5c73a25fbb4 100644 --- a/esphome/components/tem3200/tem3200.h +++ b/esphome/components/tem3200/tem3200.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tem3200 { +namespace esphome::tem3200 { /// This class implements support for the tem3200 pressure and temperature i2c sensors. class TEM3200Component : public PollingComponent, public i2c::I2CDevice { @@ -25,5 +24,4 @@ class TEM3200Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *raw_pressure_sensor_{nullptr}; }; -} // namespace tem3200 -} // namespace esphome +} // namespace esphome::tem3200 diff --git a/esphome/components/time_based/cover/time_based_cover.cpp b/esphome/components/time_based/cover/time_based_cover.cpp index c83829ff592..613b190cf3c 100644 --- a/esphome/components/time_based/cover/time_based_cover.cpp +++ b/esphome/components/time_based/cover/time_based_cover.cpp @@ -3,8 +3,7 @@ #include "esphome/core/hal.h" #include "esphome/core/application.h" -namespace esphome { -namespace time_based { +namespace esphome::time_based { static const char *const TAG = "time_based.cover"; @@ -183,5 +182,4 @@ void TimeBasedCover::recompute_position_() { this->last_recompute_time_ = now; } -} // namespace time_based -} // namespace esphome +} // namespace esphome::time_based diff --git a/esphome/components/time_based/cover/time_based_cover.h b/esphome/components/time_based/cover/time_based_cover.h index 0adc5cb370d..ce0b105cebe 100644 --- a/esphome/components/time_based/cover/time_based_cover.h +++ b/esphome/components/time_based/cover/time_based_cover.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace time_based { +namespace esphome::time_based { class TimeBasedCover : public cover::Cover, public Component { public: @@ -50,5 +49,4 @@ class TimeBasedCover : public cover::Cover, public Component { cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; -} // namespace time_based -} // namespace esphome +} // namespace esphome::time_based diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index d35585fe5f3..def337befc0 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tlc59208f { +namespace esphome::tlc59208f { static const char *const TAG = "tlc59208f"; @@ -143,5 +142,4 @@ void TLC59208FChannel::write_state(float state) { this->parent_->set_channel_value_(this->channel_, duty); } -} // namespace tlc59208f -} // namespace esphome +} // namespace esphome::tlc59208f diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h index 34663cd3644..46f88de01f4 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.h +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -5,8 +5,7 @@ #include "esphome/components/output/float_output.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tlc59208f { +namespace esphome::tlc59208f { // 0*: Group dimming, 1: Group blinking inline constexpr uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); @@ -65,5 +64,4 @@ class TLC59208FOutput : public Component, public i2c::I2CDevice { bool update_{true}; }; -} // namespace tlc59208f -} // namespace esphome +} // namespace esphome::tlc59208f diff --git a/esphome/components/tlc5947/output/tlc5947_output.cpp b/esphome/components/tlc5947/output/tlc5947_output.cpp index 9630fb8c1e8..b1badbac993 100644 --- a/esphome/components/tlc5947/output/tlc5947_output.cpp +++ b/esphome/components/tlc5947/output/tlc5947_output.cpp @@ -1,12 +1,10 @@ #include "tlc5947_output.h" -namespace esphome { -namespace tlc5947 { +namespace esphome::tlc5947 { void TLC5947Channel::write_state(float state) { auto amount = static_cast<uint16_t>(state * 0xfff); this->parent_->set_channel_value(this->channel_, amount); } -} // namespace tlc5947 -} // namespace esphome +} // namespace esphome::tlc5947 diff --git a/esphome/components/tlc5947/output/tlc5947_output.h b/esphome/components/tlc5947/output/tlc5947_output.h index 0faec96acbf..16a96b51401 100644 --- a/esphome/components/tlc5947/output/tlc5947_output.h +++ b/esphome/components/tlc5947/output/tlc5947_output.h @@ -6,8 +6,7 @@ #include "../tlc5947.h" -namespace esphome { -namespace tlc5947 { +namespace esphome::tlc5947 { class TLC5947Channel : public output::FloatOutput, public Parented<TLC5947> { public: @@ -18,5 +17,4 @@ class TLC5947Channel : public output::FloatOutput, public Parented<TLC5947> { uint16_t channel_; }; -} // namespace tlc5947 -} // namespace esphome +} // namespace esphome::tlc5947 diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp index 0a278bbaf61..f886118a088 100644 --- a/esphome/components/tlc5947/tlc5947.cpp +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -1,8 +1,7 @@ #include "tlc5947.h" #include "esphome/core/log.h" -namespace esphome { -namespace tlc5947 { +namespace esphome::tlc5947 { static const char *const TAG = "tlc5947"; @@ -69,5 +68,4 @@ void TLC5947::set_channel_value(uint16_t channel, uint16_t value) { this->pwm_amounts_[channel] = value; } -} // namespace tlc5947 -} // namespace esphome +} // namespace esphome::tlc5947 diff --git a/esphome/components/tlc5947/tlc5947.h b/esphome/components/tlc5947/tlc5947.h index 95d76408c9c..18acffa25f4 100644 --- a/esphome/components/tlc5947/tlc5947.h +++ b/esphome/components/tlc5947/tlc5947.h @@ -7,8 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace tlc5947 { +namespace esphome::tlc5947 { class TLC5947 : public Component { public: @@ -42,5 +41,4 @@ class TLC5947 : public Component { bool update_{true}; }; -} // namespace tlc5947 -} // namespace esphome +} // namespace esphome::tlc5947 diff --git a/esphome/components/tlc5971/output/tlc5971_output.cpp b/esphome/components/tlc5971/output/tlc5971_output.cpp index b4378890721..5c9183def65 100644 --- a/esphome/components/tlc5971/output/tlc5971_output.cpp +++ b/esphome/components/tlc5971/output/tlc5971_output.cpp @@ -1,12 +1,10 @@ #include "tlc5971_output.h" -namespace esphome { -namespace tlc5971 { +namespace esphome::tlc5971 { void TLC5971Channel::write_state(float state) { auto amount = static_cast<uint16_t>(state * 0xffff); this->parent_->set_channel_value(this->channel_, amount); } -} // namespace tlc5971 -} // namespace esphome +} // namespace esphome::tlc5971 diff --git a/esphome/components/tlc5971/output/tlc5971_output.h b/esphome/components/tlc5971/output/tlc5971_output.h index ca3099e7b28..2a24a19b6c4 100644 --- a/esphome/components/tlc5971/output/tlc5971_output.h +++ b/esphome/components/tlc5971/output/tlc5971_output.h @@ -6,8 +6,7 @@ #include "../tlc5971.h" -namespace esphome { -namespace tlc5971 { +namespace esphome::tlc5971 { class TLC5971Channel : public output::FloatOutput, public Parented<TLC5971> { public: @@ -18,5 +17,4 @@ class TLC5971Channel : public output::FloatOutput, public Parented<TLC5971> { uint16_t channel_; }; -} // namespace tlc5971 -} // namespace esphome +} // namespace esphome::tlc5971 diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index 8128dd90462..5818eace679 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -1,8 +1,7 @@ #include "tlc5971.h" #include "esphome/core/log.h" -namespace esphome { -namespace tlc5971 { +namespace esphome::tlc5971 { static const char *const TAG = "tlc5971"; @@ -90,5 +89,4 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { this->pwm_amounts_[channel] = value; } -} // namespace tlc5971 -} // namespace esphome +} // namespace esphome::tlc5971 diff --git a/esphome/components/tlc5971/tlc5971.h b/esphome/components/tlc5971/tlc5971.h index 6b0daf10d16..080249c89c2 100644 --- a/esphome/components/tlc5971/tlc5971.h +++ b/esphome/components/tlc5971/tlc5971.h @@ -7,8 +7,7 @@ #include "esphome/components/output/float_output.h" #include <vector> -namespace esphome { -namespace tlc5971 { +namespace esphome::tlc5971 { class TLC5971 : public Component { public: @@ -39,5 +38,4 @@ class TLC5971 : public Component { std::vector<uint16_t> pwm_amounts_; bool update_{true}; }; -} // namespace tlc5971 -} // namespace esphome +} // namespace esphome::tlc5971 diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index c82d3064601..68d16e38116 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1621 { +namespace esphome::tm1621 { static const char *const TAG = "tm1621"; @@ -277,5 +276,4 @@ int TM1621Display::get_command_code_(char *destination, size_t destination_size, } return result; } -} // namespace tm1621 -} // namespace esphome +} // namespace esphome::tm1621 diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h index fe923417a60..7708ee6c988 100644 --- a/esphome/components/tm1621/tm1621.h +++ b/esphome/components/tm1621/tm1621.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace tm1621 { +namespace esphome::tm1621 { class TM1621Display; @@ -71,5 +70,4 @@ class TM1621Display : public PollingComponent { bool kwh_; }; -} // namespace tm1621 -} // namespace esphome +} // namespace esphome::tm1621 diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 4814d5b1c49..a1604fa60e8 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1637 { +namespace esphome::tm1637 { static const char *const TAG = "display.tm1637"; const uint8_t TM1637_CMD_DATA = 0x40; //!< Display data command @@ -391,5 +390,4 @@ uint8_t TM1637Display::strftime(uint8_t pos, const char *format, ESPTime time) { } uint8_t TM1637Display::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } -} // namespace tm1637 -} // namespace esphome +} // namespace esphome::tm1637 diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 1738d37107a..1ad56ae75a3 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -12,8 +12,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -namespace esphome { -namespace tm1637 { +namespace esphome::tm1637 { class TM1637Display; #ifdef USE_BINARY_SENSOR @@ -105,5 +104,4 @@ class TM1637Key : public binary_sensor::BinarySensor { }; #endif -} // namespace tm1637 -} // namespace esphome +} // namespace esphome::tm1637 diff --git a/esphome/components/tm1638/binary_sensor/tm1638_key.cpp b/esphome/components/tm1638/binary_sensor/tm1638_key.cpp index c143bafaea0..9eecf97a9bd 100644 --- a/esphome/components/tm1638/binary_sensor/tm1638_key.cpp +++ b/esphome/components/tm1638/binary_sensor/tm1638_key.cpp @@ -1,7 +1,6 @@ #include "tm1638_key.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { void TM1638Key::keys_update(uint8_t keys) { bool pressed = keys & (1 << key_code_); @@ -9,5 +8,4 @@ void TM1638Key::keys_update(uint8_t keys) { this->publish_state(pressed); } -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/binary_sensor/tm1638_key.h b/esphome/components/tm1638/binary_sensor/tm1638_key.h index 0ea385f434d..fba1e43bde0 100644 --- a/esphome/components/tm1638/binary_sensor/tm1638_key.h +++ b/esphome/components/tm1638/binary_sensor/tm1638_key.h @@ -3,8 +3,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "../tm1638.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { class TM1638Key : public binary_sensor::BinarySensor, public KeyListener { public: @@ -15,5 +14,4 @@ class TM1638Key : public binary_sensor::BinarySensor, public KeyListener { uint8_t key_code_{0}; }; -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/output/tm1638_output_led.cpp b/esphome/components/tm1638/output/tm1638_output_led.cpp index ea1c84e64bf..e32826fa930 100644 --- a/esphome/components/tm1638/output/tm1638_output_led.cpp +++ b/esphome/components/tm1638/output/tm1638_output_led.cpp @@ -1,8 +1,7 @@ #include "tm1638_output_led.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { static const char *const TAG = "tm1638.led"; @@ -13,5 +12,4 @@ void TM1638OutputLed::dump_config() { ESP_LOGCONFIG(TAG, " LED: %d", led_); } -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/output/tm1638_output_led.h b/esphome/components/tm1638/output/tm1638_output_led.h index 6aa1015aae7..b1c1090447b 100644 --- a/esphome/components/tm1638/output/tm1638_output_led.h +++ b/esphome/components/tm1638/output/tm1638_output_led.h @@ -4,8 +4,7 @@ #include "esphome/components/output/binary_output.h" #include "../tm1638.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { class TM1638OutputLed : public output::BinaryOutput, public Component { public: @@ -21,5 +20,4 @@ class TM1638OutputLed : public output::BinaryOutput, public Component { int led_; }; -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/sevenseg.h b/esphome/components/tm1638/sevenseg.h index a4c16c7422c..61098b5a5b1 100644 --- a/esphome/components/tm1638/sevenseg.h +++ b/esphome/components/tm1638/sevenseg.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { namespace TM1638Translation { constexpr unsigned char SEVEN_SEG[] PROGMEM = { @@ -103,5 +102,4 @@ constexpr unsigned char SEVEN_SEG[] PROGMEM = { }; }; // namespace TM1638Translation -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/switch/tm1638_switch_led.cpp b/esphome/components/tm1638/switch/tm1638_switch_led.cpp index 60c9e8b4a96..743d0af5075 100644 --- a/esphome/components/tm1638/switch/tm1638_switch_led.cpp +++ b/esphome/components/tm1638/switch/tm1638_switch_led.cpp @@ -1,8 +1,7 @@ #include "tm1638_switch_led.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { static const char *const TAG = "tm1638.led"; @@ -16,5 +15,4 @@ void TM1638SwitchLed::dump_config() { ESP_LOGCONFIG(TAG, " LED: %d", led_); } -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/switch/tm1638_switch_led.h b/esphome/components/tm1638/switch/tm1638_switch_led.h index 10516e0079a..c7154eefb38 100644 --- a/esphome/components/tm1638/switch/tm1638_switch_led.h +++ b/esphome/components/tm1638/switch/tm1638_switch_led.h @@ -4,8 +4,7 @@ #include "esphome/components/switch/switch.h" #include "../tm1638.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { class TM1638SwitchLed : public switch_::Switch, public Component { public: @@ -19,5 +18,4 @@ class TM1638SwitchLed : public switch_::Switch, public Component { TM1638Component *tm1638_; int led_; }; -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index c67ff1adbc2..1f0692479c2 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { static const char *const TAG = "display.tm1638"; static const uint8_t TM1638_REGISTER_FIXEDADDRESS = 0x44; @@ -282,5 +281,4 @@ void TM1638Component::shift_out_(uint8_t val) { } } -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index 27898aa3dcc..24d49f4a9f5 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -9,8 +9,7 @@ #include <vector> -namespace esphome { -namespace tm1638 { +namespace esphome::tm1638 { class KeyListener { public: @@ -75,5 +74,4 @@ class TM1638Component : public PollingComponent { std::vector<KeyListener *> listeners_{}; }; -} // namespace tm1638 -} // namespace esphome +} // namespace esphome::tm1638 diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index 15ada0f8ff3..282b0dcf76b 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -51,8 +51,7 @@ #include "tm1651.h" #include "esphome/core/log.h" -namespace esphome { -namespace tm1651 { +namespace esphome::tm1651 { static const char *const TAG = "tm1651.display"; @@ -256,5 +255,4 @@ void TM1651Display::delineate_transmission_(bool dio_state) { delayMicroseconds(QUARTER_CLOCK_CYCLE); } -} // namespace tm1651 -} // namespace esphome +} // namespace esphome::tm1651 diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 83e74c5f330..f1abbcc7922 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace tm1651 { +namespace esphome::tm1651 { enum TM1651Brightness : uint8_t { TM1651_DARKEST = 1, @@ -97,5 +96,4 @@ template<typename... Ts> class TurnOffAction : public Action<Ts...>, public Pare void play(const Ts &...x) override { this->parent_->turn_off(); } }; -} // namespace tm1651 -} // namespace esphome +} // namespace esphome::tm1651 diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 99f6753ddcc..cb2462858b6 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace tmp102 { +namespace esphome::tmp102 { static const char *const TAG = "tmp102"; @@ -46,5 +45,4 @@ void TMP102Component::update() { }); } -} // namespace tmp102 -} // namespace esphome +} // namespace esphome::tmp102 diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h index fe860a38191..aedfefd0528 100644 --- a/esphome/components/tmp102/tmp102.h +++ b/esphome/components/tmp102/tmp102.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tmp102 { +namespace esphome::tmp102 { class TMP102Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: @@ -13,5 +12,4 @@ class TMP102Component : public PollingComponent, public i2c::I2CDevice, public s void update() override; }; -} // namespace tmp102 -} // namespace esphome +} // namespace esphome::tmp102 diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 3c7ed019706..681603d1136 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tmp1075.h" -namespace esphome { -namespace tmp1075 { +namespace esphome::tmp1075 { static const char *const TAG = "tmp1075"; @@ -127,5 +126,4 @@ static float regvalue2temp(const uint16_t regvalue) { return (signed_value >> 4) * 0.0625f; } -} // namespace tmp1075 -} // namespace esphome +} // namespace esphome::tmp1075 diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h index b5fd60c08e2..4dc9449597a 100644 --- a/esphome/components/tmp1075/tmp1075.h +++ b/esphome/components/tmp1075/tmp1075.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace tmp1075 { +namespace esphome::tmp1075 { struct TMP1075Config { union { @@ -85,5 +84,4 @@ class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c void log_config_(); }; -} // namespace tmp1075 -} // namespace esphome +} // namespace esphome::tmp1075 diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp index e27c7bbd649..290bc76a521 100644 --- a/esphome/components/tof10120/tof10120_sensor.cpp +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -5,8 +5,7 @@ // Very basic support for TOF10120 distance sensor -namespace esphome { -namespace tof10120 { +namespace esphome::tof10120 { static const char *const TAG = "tof10120"; static const uint8_t TOF10120_READ_DISTANCE_CMD[] = {0x00}; @@ -56,5 +55,4 @@ void TOF10120Sensor::update() { this->status_clear_warning(); } -} // namespace tof10120 -} // namespace esphome +} // namespace esphome::tof10120 diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h index d0cca19d4c6..8bf92b50a06 100644 --- a/esphome/components/tof10120/tof10120_sensor.h +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tof10120 { +namespace esphome::tof10120 { class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -14,5 +13,4 @@ class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2 void dump_config() override; void update() override; }; -} // namespace tof10120 -} // namespace esphome +} // namespace esphome::tof10120 diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index cca7b2bba02..7004c4f8365 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -5,8 +5,7 @@ using namespace std; -namespace esphome { -namespace tormatic { +namespace esphome::tormatic { static const char *const TAG = "tormatic.cover"; @@ -390,5 +389,4 @@ void Tormatic::drain_rx_(uint16_t n) { } } -} // namespace tormatic -} // namespace esphome +} // namespace esphome::tormatic diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 34483ed6a35..2a83213ffec 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -5,8 +5,7 @@ #include "tormatic_protocol.h" -namespace esphome { -namespace tormatic { +namespace esphome::tormatic { using namespace esphome::cover; @@ -56,5 +55,4 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom optional<float> target_position_{}; }; -} // namespace tormatic -} // namespace esphome +} // namespace esphome::tormatic diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 269b63ff783..476aa668d71 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -46,8 +46,7 @@ * for this purpose. */ -namespace esphome { -namespace tormatic { +namespace esphome::tormatic { using namespace esphome::cover; @@ -225,5 +224,4 @@ struct CommandRequestReply { void byteswap() { this->type = convert_big_endian(this->type); } } __attribute__((packed)); -} // namespace tormatic -} // namespace esphome +} // namespace esphome::tormatic diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index a23b4c7cc3e..1b37c6897da 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -4,8 +4,7 @@ #include <vector> -namespace esphome { -namespace toshiba { +namespace esphome::toshiba { struct RacPt1411hwruFanSpeed { uint8_t code1; @@ -1372,5 +1371,4 @@ bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *mess return true; } -} // namespace toshiba -} // namespace esphome +} // namespace esphome::toshiba diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h index ee1dec5cc9f..4525d6bffef 100644 --- a/esphome/components/toshiba/toshiba.h +++ b/esphome/components/toshiba/toshiba.h @@ -3,8 +3,7 @@ #include "esphome/components/climate_ir/climate_ir.h" #include "esphome/components/remote_base/toshiba_ac_protocol.h" -namespace esphome { -namespace toshiba { +namespace esphome::toshiba { // Simple enum to represent models. enum Model { @@ -82,5 +81,4 @@ class ToshibaClimate : public climate_ir::ClimateIR { Model model_; }; -} // namespace toshiba -} // namespace esphome +} // namespace esphome::toshiba diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 0662cebf872..25a7ffacf21 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -1,7 +1,6 @@ #include "touchscreen_binary_sensor.h" -namespace esphome { -namespace touchscreen { +namespace esphome::touchscreen { void TouchscreenBinarySensor::setup() { this->parent_->register_listener(this); @@ -30,5 +29,4 @@ void TouchscreenBinarySensor::touch(TouchPoint tp) { void TouchscreenBinarySensor::release() { this->publish_state(false); } -} // namespace touchscreen -} // namespace esphome +} // namespace esphome::touchscreen diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 79055e6c953..2f86bc97498 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -8,8 +8,7 @@ #include <vector> -namespace esphome { -namespace touchscreen { +namespace esphome::touchscreen { class TouchscreenBinarySensor : public binary_sensor::BinarySensor, public Component, @@ -44,5 +43,4 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, std::vector<display::DisplayPage *> pages_{}; }; -} // namespace touchscreen -} // namespace esphome +} // namespace esphome::touchscreen diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index dcf32097524..5687213eb53 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace touchscreen { +namespace esphome::touchscreen { static const char *const TAG = "touchscreen"; @@ -162,5 +161,4 @@ int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, b return ret; } -} // namespace touchscreen -} // namespace esphome +} // namespace esphome::touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 7451c207ec3..f1f53986038 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -9,8 +9,7 @@ #include <vector> #include <map> -namespace esphome { -namespace touchscreen { +namespace esphome::touchscreen { static const uint8_t STATE_RELEASED = 0x00; static const uint8_t STATE_PRESSED = 0x01; @@ -120,5 +119,4 @@ class Touchscreen : public PollingComponent { bool skip_update_{false}; }; -} // namespace touchscreen -} // namespace esphome +} // namespace esphome::touchscreen diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index bccff1fb26a..963114b230c 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -1,8 +1,7 @@ #include "tsl2561.h" #include "esphome/core/log.h" -namespace esphome { -namespace tsl2561 { +namespace esphome::tsl2561 { static const char *const TAG = "tsl2561"; @@ -165,5 +164,4 @@ bool TSL2561Sensor::tsl2561_read_byte(uint8_t a_register, uint8_t *value) { return this->read_byte(a_register | TSL2561_COMMAND_BIT, value); } -} // namespace tsl2561 -} // namespace esphome +} // namespace esphome::tsl2561 diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index a8f0aef90f9..0fbb59c648d 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tsl2561 { +namespace esphome::tsl2561 { /** Enum listing all conversion/integration time settings for the TSL2561 * @@ -82,5 +81,4 @@ class TSL2561Sensor : public sensor::Sensor, public PollingComponent, public i2c bool package_cs_{false}; }; -} // namespace tsl2561 -} // namespace esphome +} // namespace esphome::tsl2561 diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 4ce673a91a1..fb34dd833d9 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace tsl2591 { +namespace esphome::tsl2591 { static const char *const TAG = "tsl2591.sensor"; @@ -475,5 +474,4 @@ float TSL2591Component::get_actual_gain() { } } -} // namespace tsl2591 -} // namespace esphome +} // namespace esphome::tsl2591 diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index 84c92b6ba98..4b63c8ec40a 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace tsl2591 { +namespace esphome::tsl2591 { /** Enum listing all conversion/integration time settings for the TSL2591. * @@ -270,5 +269,4 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { void interval_function_for_update_(); }; -} // namespace tsl2591 -} // namespace esphome +} // namespace esphome::tsl2591 diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.cpp b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp index 2d5ac22a830..ccf6e53d665 100644 --- a/esphome/components/tt21100/binary_sensor/tt21100_button.cpp +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.cpp @@ -1,8 +1,7 @@ #include "tt21100_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace tt21100 { +namespace esphome::tt21100 { static const char *const TAG = "tt21100.binary_sensor"; @@ -23,5 +22,4 @@ void TT21100Button::update_button(uint8_t index, uint16_t state) { this->publish_state(state > 0); } -} // namespace tt21100 -} // namespace esphome +} // namespace esphome::tt21100 diff --git a/esphome/components/tt21100/binary_sensor/tt21100_button.h b/esphome/components/tt21100/binary_sensor/tt21100_button.h index 90b55bb75a0..a1f59464479 100644 --- a/esphome/components/tt21100/binary_sensor/tt21100_button.h +++ b/esphome/components/tt21100/binary_sensor/tt21100_button.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace tt21100 { +namespace esphome::tt21100 { class TT21100Button : public binary_sensor::BinarySensor, public Component, @@ -24,5 +23,4 @@ class TT21100Button : public binary_sensor::BinarySensor, uint8_t index_; }; -} // namespace tt21100 -} // namespace esphome +} // namespace esphome::tt21100 diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index b4735fe6d70..018094df737 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -1,8 +1,7 @@ #include "tt21100.h" #include "esphome/core/log.h" -namespace esphome { -namespace tt21100 { +namespace esphome::tt21100 { static const char *const TAG = "tt21100"; @@ -139,5 +138,4 @@ void TT21100Touchscreen::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); } -} // namespace tt21100 -} // namespace esphome +} // namespace esphome::tt21100 diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h index 5d1b2efe3cf..3c6030c9c1b 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.h +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -7,8 +7,7 @@ #include <vector> -namespace esphome { -namespace tt21100 { +namespace esphome::tt21100 { using namespace touchscreen; @@ -39,5 +38,4 @@ class TT21100Touchscreen : public Touchscreen, public i2c::I2CDevice { std::vector<TT21100ButtonListener *> button_listeners_; }; -} // namespace tt21100 -} // namespace esphome +} // namespace esphome::tt21100 diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.cpp b/esphome/components/ttp229_bsf/ttp229_bsf.cpp index 8d1ed45bb01..1c7fa3531f0 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.cpp +++ b/esphome/components/ttp229_bsf/ttp229_bsf.cpp @@ -1,8 +1,7 @@ #include "ttp229_bsf.h" #include "esphome/core/log.h" -namespace esphome { -namespace ttp229_bsf { +namespace esphome::ttp229_bsf { static const char *const TAG = "ttp229_bsf"; @@ -18,5 +17,4 @@ void TTP229BSFComponent::dump_config() { LOG_PIN(" SDO pin: ", this->sdo_pin_); } -} // namespace ttp229_bsf -} // namespace esphome +} // namespace esphome::ttp229_bsf diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index fea4356b55b..07f0c638c2d 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace ttp229_bsf { +namespace esphome::ttp229_bsf { class TTP229BSFChannel : public binary_sensor::BinarySensor { public: @@ -51,5 +50,4 @@ class TTP229BSFComponent : public Component { std::vector<TTP229BSFChannel *> channels_{}; }; -} // namespace ttp229_bsf -} // namespace esphome +} // namespace esphome::ttp229_bsf diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 7bdb57ebec9..eaef33d7935 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -1,8 +1,7 @@ #include "ttp229_lsf.h" #include "esphome/core/log.h" -namespace esphome { -namespace ttp229_lsf { +namespace esphome::ttp229_lsf { static const char *const TAG = "ttp229_lsf"; @@ -40,5 +39,4 @@ void TTP229LSFComponent::loop() { } } -} // namespace ttp229_lsf -} // namespace esphome +} // namespace esphome::ttp229_lsf diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index 7cc4bfca89a..09e7745d256 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace ttp229_lsf { +namespace esphome::ttp229_lsf { class TTP229Channel : public binary_sensor::BinarySensor { public: @@ -33,5 +32,4 @@ class TTP229LSFComponent : public Component, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace ttp229_lsf -} // namespace esphome +} // namespace esphome::ttp229_lsf diff --git a/esphome/components/tuya/automation.cpp b/esphome/components/tuya/automation.cpp index a8cfd098f10..5c84f36b832 100644 --- a/esphome/components/tuya/automation.cpp +++ b/esphome/components/tuya/automation.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "tuya.automation"; -namespace esphome { -namespace tuya { +namespace esphome::tuya { void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) { if (dp.type != expected) { @@ -63,5 +62,4 @@ TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *paren }); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/automation.h b/esphome/components/tuya/automation.h index 8d91cfdfbf5..f5c806b0135 100644 --- a/esphome/components/tuya/automation.h +++ b/esphome/components/tuya/automation.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaDatapointUpdateTrigger : public Trigger<TuyaDatapoint> { public: @@ -51,5 +50,4 @@ class TuyaBitmaskDatapointUpdateTrigger : public Trigger<uint32_t> { explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp index a63e9c8318e..f93bd31b9d9 100644 --- a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_binary_sensor.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.binary_sensor"; @@ -20,5 +19,4 @@ void TuyaBinarySensor::dump_config() { this->sensor_id_); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h index 1eeeb404776..f92652d087b 100644 --- a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { public: @@ -20,5 +19,4 @@ class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { uint8_t sensor_id_{0}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 6602ccd8c9a..7dbf33878a4 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -1,8 +1,7 @@ #include "tuya_climate.h" #include "esphome/core/log.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.climate"; @@ -533,5 +532,4 @@ void TuyaClimate::switch_to_action_(climate::ClimateAction action) { this->action = action; } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index 09f3fd30c39..b9fb45257aa 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/climate/climate.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaClimate : public climate::Climate, public Component { public: @@ -125,5 +124,4 @@ class TuyaClimate : public climate::Climate, public Component { bool reports_fahrenheit_{false}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index 125afec0483..dd268388d00 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_cover.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { const uint8_t COMMAND_OPEN = 0x00; const uint8_t COMMAND_CLOSE = 0x02; @@ -140,5 +139,4 @@ cover::CoverTraits TuyaCover::get_traits() { return traits; } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/cover/tuya_cover.h b/esphome/components/tuya/cover/tuya_cover.h index bb5a00bc598..ab639756832 100644 --- a/esphome/components/tuya/cover/tuya_cover.h +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { enum TuyaCoverRestoreMode { COVER_NO_RESTORE, @@ -46,5 +45,4 @@ class TuyaCover : public cover::Cover, public Component { bool invert_position_report_; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index a387606b776..0b5fc19038c 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_fan.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.fan"; @@ -127,5 +126,4 @@ void TuyaFan::control(const fan::FanCall &call) { } } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 527efa8246d..bfb6bdeca03 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaFan : public Component, public fan::Fan { public: @@ -32,5 +31,4 @@ class TuyaFan : public Component, public fan::Fan { TuyaDatapointType oscillation_type_{}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 620bb88d0b7..9f3f3c13cce 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -2,8 +2,7 @@ #include "tuya_light.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.light"; @@ -228,5 +227,4 @@ void TuyaLight::write_state(light::LightState *state) { } } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index ded94f390af..d990eea72ad 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/light/light_output.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { enum TuyaColorType { RGB, HSV, RGBHSV }; @@ -65,5 +64,4 @@ class TuyaLight : public Component, public light::LightOutput { light::LightState *state_{nullptr}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index fd22e642c64..bfedbb93194 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_number.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.number"; @@ -91,5 +90,4 @@ void TuyaNumber::dump_config() { ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h index 53137d6f66c..51c53a4442e 100644 --- a/esphome/components/tuya/number/tuya_number.h +++ b/esphome/components/tuya/number/tuya_number.h @@ -6,8 +6,7 @@ #include "esphome/core/optional.h" #include "esphome/core/preferences.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaNumber : public number::Number, public Component { public: @@ -34,5 +33,4 @@ class TuyaNumber : public number::Number, public Component { ESPPreferenceObject pref_; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 9d46e4c8ca0..f0fc47f5040 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_select.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.select"; @@ -47,5 +46,4 @@ void TuyaSelect::dump_config() { } } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index 24505c99104..f8d2d89ea89 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaSelect : public select::Select, public Component { public: @@ -32,5 +31,4 @@ class TuyaSelect : public select::Select, public Component { bool is_int_ = false; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp index 673471a6cec..c9489847864 100644 --- a/esphome/components/tuya/sensor/tuya_sensor.cpp +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -2,8 +2,7 @@ #include "tuya_sensor.h" #include <cinttypes> -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.sensor"; @@ -30,5 +29,4 @@ void TuyaSensor::dump_config() { ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/sensor/tuya_sensor.h b/esphome/components/tuya/sensor/tuya_sensor.h index 8fd7cd17702..b700fc8bd74 100644 --- a/esphome/components/tuya/sensor/tuya_sensor.h +++ b/esphome/components/tuya/sensor/tuya_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaSensor : public sensor::Sensor, public Component { public: @@ -20,5 +19,4 @@ class TuyaSensor : public sensor::Sensor, public Component { uint8_t sensor_id_{0}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp index cbd794b0013..8d4d183d5b6 100644 --- a/esphome/components/tuya/switch/tuya_switch.cpp +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -1,8 +1,7 @@ #include "esphome/core/log.h" #include "tuya_switch.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.switch"; @@ -24,5 +23,4 @@ void TuyaSwitch::dump_config() { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", this->switch_id_); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/switch/tuya_switch.h b/esphome/components/tuya/switch/tuya_switch.h index 89e6264e5c3..7e0109c34c9 100644 --- a/esphome/components/tuya/switch/tuya_switch.h +++ b/esphome/components/tuya/switch/tuya_switch.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaSwitch : public switch_::Switch, public Component { public: @@ -22,5 +21,4 @@ class TuyaSwitch : public switch_::Switch, public Component { uint8_t switch_id_{0}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index b15fb6f85a7..ebb35cead7f 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya.text_sensor"; @@ -43,5 +42,4 @@ void TuyaTextSensor::dump_config() { this->sensor_id_); } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.h b/esphome/components/tuya/text_sensor/tuya_text_sensor.h index 502ae5e8c73..c9ac64deb8e 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.h +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/tuya/tuya.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace tuya { +namespace esphome::tuya { class TuyaTextSensor : public text_sensor::TextSensor, public Component { public: @@ -20,5 +19,4 @@ class TuyaTextSensor : public text_sensor::TextSensor, public Component { uint8_t sensor_id_{0}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index a1acbf2f56c..d682adffe38 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -13,8 +13,7 @@ #include "esphome/components/captive_portal/captive_portal.h" #endif -namespace esphome { -namespace tuya { +namespace esphome::tuya { static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; @@ -760,5 +759,4 @@ void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(Tuya TuyaInitState Tuya::get_init_state() { return this->init_state_; } -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 7e6b50f0847..8ba8ac85a05 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -13,8 +13,7 @@ #include "esphome/core/time.h" #endif -namespace esphome { -namespace tuya { +namespace esphome::tuya { enum class TuyaDatapointType : uint8_t { RAW = 0x00, // variable length @@ -162,5 +161,4 @@ class Tuya : public Component, public uart::UARTDevice { CallbackManager<void()> initialized_callback_{}; }; -} // namespace tuya -} // namespace esphome +} // namespace esphome::tuya diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 3e0234fac04..353cb31513a 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -4,8 +4,7 @@ #include <array> -namespace esphome { -namespace tx20 { +namespace esphome::tx20 { static const char *const TAG = "tx20"; static const uint8_t MAX_BUFFER_SIZE = 41; @@ -206,5 +205,4 @@ void IRAM_ATTR Tx20ComponentStore::reset() { start_time = 0; } -} // namespace tx20 -} // namespace esphome +} // namespace esphome::tx20 diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index d1673f99f2b..7ca29eaf3bc 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace tx20 { +namespace esphome::tx20 { /// Store data in a class that doesn't use multiple-inheritance (vtables in flash) struct Tx20ComponentStore { @@ -47,5 +46,4 @@ class Tx20Component : public Component { Tx20ComponentStore store_; }; -} // namespace tx20 -} // namespace esphome +} // namespace esphome::tx20 diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index b66c2a9892a..c37b82921e4 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -6,8 +6,7 @@ #include <vector> -namespace esphome { -namespace udp { +namespace esphome::udp { template<typename... Ts> class UDPWriteAction : public Action<Ts...>, public Parented<UDPComponent> { public: @@ -40,6 +39,6 @@ template<typename... Ts> class UDPWriteAction : public Action<Ts...>, public Par } data_; }; -} // namespace udp -} // namespace esphome +} // namespace esphome::udp + #endif diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index b5e73af7779..40ce46d74e9 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -3,8 +3,7 @@ #include "esphome/components/network/util.h" #include "udp_transport.h" -namespace esphome { -namespace udp { +namespace esphome::udp { static const char *const TAG = "udp_transport"; @@ -17,5 +16,4 @@ void UDPTransport::setup() { } void UDPTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->send_packet(buf); } -} // namespace udp -} // namespace esphome +} // namespace esphome::udp diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h index 8d01ae0909f..8621ddca482 100644 --- a/esphome/components/udp/packet_transport/udp_transport.h +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -6,8 +6,7 @@ #include "esphome/components/packet_transport/packet_transport.h" #include <vector> -namespace esphome { -namespace udp { +namespace esphome::udp { class UDPTransport : public packet_transport::PacketTransport, public Parented<UDPComponent> { public: @@ -21,6 +20,6 @@ class UDPTransport : public packet_transport::PacketTransport, public Parented<U size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } }; -} // namespace udp -} // namespace esphome +} // namespace esphome::udp + #endif diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 40e3be2757e..72bf837409f 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -2,8 +2,7 @@ #include "ufire_ec.h" #include <cmath> -namespace esphome { -namespace ufire_ec { +namespace esphome::ufire_ec { static const char *const TAG = "ufire_ec"; @@ -122,5 +121,4 @@ void UFireECComponent::dump_config() { LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); } -} // namespace ufire_ec -} // namespace esphome +} // namespace esphome::ufire_ec diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index 8a648b5038b..fce62586322 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ufire_ec { +namespace esphome::ufire_ec { static const uint8_t CONFIG_TEMP_COMPENSATION = 0x02; @@ -83,5 +82,4 @@ template<typename... Ts> class UFireECResetAction : public Action<Ts...> { UFireECComponent *parent_; }; -} // namespace ufire_ec -} // namespace esphome +} // namespace esphome::ufire_ec diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index e967fc53c37..bd2dc2836ed 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -3,8 +3,7 @@ #include <cmath> -namespace esphome { -namespace ufire_ise { +namespace esphome::ufire_ise { static const char *const TAG = "ufire_ise"; @@ -147,5 +146,4 @@ void UFireISEComponent::dump_config() { LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); } -} // namespace ufire_ise -} // namespace esphome +} // namespace esphome::ufire_ise diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h index fe9a6dfb9c8..bff8eeff9de 100644 --- a/esphome/components/ufire_ise/ufire_ise.h +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace ufire_ise { +namespace esphome::ufire_ise { static const float PROBE_MV_TO_PH = 59.2; static const float PROBE_TMP_CORRECTION = 0.03; @@ -91,5 +90,4 @@ template<typename... Ts> class UFireISEResetAction : public Action<Ts...> { UFireISEComponent *parent_; }; -} // namespace ufire_ise -} // namespace esphome +} // namespace esphome::ufire_ise diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index af24c838b14..821151f67ce 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace update { +namespace esphome::update { template<typename... Ts> class PerformAction : public Action<Ts...>, public Parented<UpdateEntity> { TEMPLATABLE_VALUE(bool, force) @@ -24,5 +23,4 @@ template<typename... Ts> class IsAvailableCondition : public Condition<Ts...>, p bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } }; -} // namespace update -} // namespace esphome +} // namespace esphome::update diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 1a5a55577f9..b0d4c01cc96 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/progmem.h" -namespace esphome { -namespace update { +namespace esphome::update { static const char *const TAG = "update"; @@ -49,5 +48,4 @@ void UpdateEntity::publish_state() { #endif } -} // namespace update -} // namespace esphome +} // namespace esphome::update diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index f7d0032f217..f925d338ff7 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" -namespace esphome { -namespace update { +namespace esphome::update { struct UpdateInfo { std::string latest_version; @@ -58,5 +57,4 @@ class UpdateEntity : public EntityBase { std::unique_ptr<Trigger<const UpdateInfo &>> update_available_trigger_{nullptr}; }; -} // namespace update -} // namespace esphome +} // namespace esphome::update diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 512a2581229..0400888511d 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { static const char *const TAG = "uponor_smatrix.climate"; @@ -100,5 +99,4 @@ void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t this->last_data_ = millis(); } -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h index b8458045c61..4cc5a4a3bcf 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h @@ -4,8 +4,7 @@ #include "esphome/components/uponor_smatrix/uponor_smatrix.h" #include "esphome/core/component.h" -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { class UponorSmatrixClimate : public climate::Climate, public Component, public UponorSmatrixDevice { public: @@ -24,5 +23,4 @@ class UponorSmatrixClimate : public climate::Climate, public Component, public U uint16_t target_temperature_raw_; }; -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 5f690a6879a..97e9c275700 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -3,8 +3,7 @@ #include <cinttypes> -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { static const char *const TAG = "uponor_smatrix.sensor"; @@ -42,5 +41,4 @@ void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t d } } -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h index 97d0d21838f..346fe1e3d66 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.h @@ -4,8 +4,7 @@ #include "esphome/components/uponor_smatrix/uponor_smatrix.h" #include "esphome/core/component.h" -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { class UponorSmatrixSensor : public sensor::Sensor, public Component, public UponorSmatrixDevice { SUB_SENSOR(temperature) @@ -20,5 +19,4 @@ class UponorSmatrixSensor : public sensor::Sensor, public Component, public Upon void on_device_data(const UponorSmatrixData *data, size_t data_len) override; }; -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 1fd53955a0e..3f1feaa9273 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -5,8 +5,7 @@ #include <cinttypes> -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { static const char *const TAG = "uponor_smatrix"; @@ -221,5 +220,4 @@ bool UponorSmatrixComponent::do_send_time_() { } #endif -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index bd20e9b6a0a..e9e772feaba 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -15,8 +15,7 @@ #include <set> #include <vector> -namespace esphome { -namespace uponor_smatrix { +namespace esphome::uponor_smatrix { /// Date/Time Part 1 (year, month, day of week) static const uint8_t UPONOR_ID_DATETIME1 = 0x08; @@ -123,5 +122,4 @@ inline uint16_t celsius_to_raw(float celsius) { : static_cast<uint16_t>(lroundf(celsius_to_fahrenheit(celsius) * 10.0f)); } -} // namespace uponor_smatrix -} // namespace esphome +} // namespace esphome::uponor_smatrix diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 27c0e329f08..08c9f4e0110 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "valve.h" -namespace esphome { -namespace valve { +namespace esphome::valve { template<typename... Ts> class OpenAction : public Action<Ts...> { public: @@ -121,5 +120,4 @@ class ValveClosedTrigger : public Trigger<> { Valve *valve_; }; -} // namespace valve -} // namespace esphome +} // namespace esphome::valve diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 9e1ef9da508..8fccd1e6d69 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -6,8 +6,7 @@ #include <strings.h> -namespace esphome { -namespace valve { +namespace esphome::valve { static const char *const TAG = "valve"; @@ -176,5 +175,4 @@ void ValveRestoreState::apply(Valve *valve) { valve->publish_state(); } -} // namespace valve -} // namespace esphome +} // namespace esphome::valve diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index b4141f5ff59..c6cdf07096a 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -7,8 +7,7 @@ #include "esphome/core/preferences.h" #include "valve_traits.h" -namespace esphome { -namespace valve { +namespace esphome::valve { const extern float VALVE_OPEN; const extern float VALVE_CLOSED; @@ -147,5 +146,4 @@ class Valve : public EntityBase { ESPPreferenceObject rtc_; }; -} // namespace valve -} // namespace esphome +} // namespace esphome::valve diff --git a/esphome/components/valve/valve_traits.h b/esphome/components/valve/valve_traits.h index 7e9aab2f26f..81b845a5f20 100644 --- a/esphome/components/valve/valve_traits.h +++ b/esphome/components/valve/valve_traits.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace valve { +namespace esphome::valve { class ValveTraits { public: @@ -23,5 +22,4 @@ class ValveTraits { bool supports_stop_{false}; }; -} // namespace valve -} // namespace esphome +} // namespace esphome::valve diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index e598b1de6b7..ddb6b530686 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace vbus { +namespace esphome::vbus { static const char *const TAG = "vbus.binary_sensor"; @@ -199,5 +198,4 @@ void VBusCustomSubBSensor::parse_message(std::vector<uint8_t> &message) { this->publish_state(this->message_parser_(message)); } -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 04c9a7b8264..8d372f45d63 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -3,8 +3,7 @@ #include "../vbus.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace vbus { +namespace esphome::vbus { class DeltaSolBSPlusBSensor : public VBusListener, public Component { public: @@ -166,5 +165,4 @@ class VBusCustomSubBSensor : public binary_sensor::BinarySensor, public Componen message_parser_t message_parser_; }; -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index 407a81c83b1..773e1435e29 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace vbus { +namespace esphome::vbus { static const char *const TAG = "vbus.sensor"; @@ -337,5 +336,4 @@ void VBusCustomSubSensor::parse_message(std::vector<uint8_t> &message) { this->publish_state(this->message_parser_(message)); } -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index ea248b1db24..34f2c442242 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -3,8 +3,7 @@ #include "../vbus.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace vbus { +namespace esphome::vbus { class DeltaSolBSPlusSensor : public VBusListener, public Component { public: @@ -243,5 +242,4 @@ class VBusCustomSubSensor : public sensor::Sensor, public Component { message_parser_t message_parser_; }; -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index 195d6ed5680..81714a2049c 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -4,8 +4,7 @@ #include <algorithm> #include <cinttypes> -namespace esphome { -namespace vbus { +namespace esphome::vbus { static const char *const TAG = "vbus"; @@ -131,5 +130,4 @@ void VBusListener::on_message(uint16_t command, uint16_t source, uint16_t dest, this->handle_message(message); } -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index 0a253f1bdbd..ff523178ef6 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace vbus { +namespace esphome::vbus { using message_parser_t = std::function<float(std::vector<uint8_t> &)>; @@ -47,5 +46,4 @@ class VBus : public uart::UARTDevice, public Component { std::vector<VBusListener *> listeners_{}; }; -} // namespace vbus -} // namespace esphome +} // namespace esphome::vbus diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index 1e02e3e802e..fd6cf1e2ede 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace veml3235 { +namespace esphome::veml3235 { static const char *const TAG = "veml3235.sensor"; @@ -225,5 +224,4 @@ void VEML3235Sensor::dump_config() { digital_gain, gain, integration_time); } -} // namespace veml3235 -} // namespace esphome +} // namespace esphome::veml3235 diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index b57e1571f1a..df88bc6ff57 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -5,8 +5,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace veml3235 { +namespace esphome::veml3235 { // Register IDs/locations // @@ -104,5 +103,4 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 VEML3235ComponentIntegrationTime integration_time_{VEML3235_INTEGRATION_TIME_50MS}; }; -} // namespace veml3235 -} // namespace esphome +} // namespace esphome::veml3235 diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 1ed484119bd..80e6f872abb 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include <limits> -namespace esphome { -namespace veml7700 { +namespace esphome::veml7700 { static const char *const TAG = "veml7700"; static const size_t VEML_REG_SIZE = 2; @@ -434,5 +433,4 @@ void VEML7700Component::publish_data_part_3_(Readings &data) { this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.actual_time)); } } -} // namespace veml7700 -} // namespace esphome +} // namespace esphome::veml7700 diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index 4b5edf733dc..a036bdf0029 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -namespace esphome { -namespace veml7700 { +namespace esphome::veml7700 { using esphome::i2c::ErrorCode; @@ -196,5 +195,4 @@ class VEML7700Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *actual_integration_time_sensor_{nullptr}; // Actual integration time for the measurement }; -} // namespace veml7700 -} // namespace esphome +} // namespace esphome::veml7700 diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 58b5a426754..df7929f676b 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -12,8 +12,7 @@ * in the vl53l0x integration directory. */ -namespace esphome { -namespace vl53l0x { +namespace esphome::vl53l0x { static const char *const TAG = "vl53l0x"; @@ -535,5 +534,4 @@ bool VL53L0XSensor::perform_single_ref_calibration_(uint8_t vhv_init_byte) { return true; } -} // namespace vl53l0x -} // namespace esphome +} // namespace esphome::vl53l0x diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index f533005b5b3..7c916f4fdeb 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -7,8 +7,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace vl53l0x { +namespace esphome::vl53l0x { struct SequenceStepEnables { bool tcc, msrc, dss, pre_range, final_range; @@ -70,5 +69,4 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c static bool enable_pin_setup_complete; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) }; -} // namespace vl53l0x -} // namespace esphome +} // namespace esphome::vl53l0x diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index ddce606b2c4..f2244a8ff43 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -9,8 +9,7 @@ #include <cinttypes> #include <cstdio> -namespace esphome { -namespace voice_assistant { +namespace esphome::voice_assistant { static const char *const TAG = "voice_assistant"; @@ -1007,7 +1006,6 @@ const Configuration &VoiceAssistant::get_configuration() { VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace voice_assistant -} // namespace esphome +} // namespace esphome::voice_assistant #endif // USE_VOICE_ASSISTANT diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index b1b5f20bff2..eb23dcb5e0d 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -26,8 +26,7 @@ #include <span> #include <vector> -namespace esphome { -namespace voice_assistant { +namespace esphome::voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support @@ -367,7 +366,6 @@ template<typename... Ts> class ConnectedCondition : public Condition<Ts...>, pub extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace voice_assistant -} // namespace esphome +} // namespace esphome::voice_assistant #endif // USE_VOICE_ASSISTANT diff --git a/esphome/components/voltage_sampler/voltage_sampler.h b/esphome/components/voltage_sampler/voltage_sampler.h index d2e74d33bc5..c1b6ffb11e9 100644 --- a/esphome/components/voltage_sampler/voltage_sampler.h +++ b/esphome/components/voltage_sampler/voltage_sampler.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace voltage_sampler { +namespace esphome::voltage_sampler { /// Abstract interface for components to request voltage (usually ADC readings) class VoltageSampler { @@ -12,5 +11,4 @@ class VoltageSampler { virtual float sample() = 0; }; -} // namespace voltage_sampler -} // namespace esphome +} // namespace esphome::voltage_sampler diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 8c5bdac54b2..fee6377965e 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -4,8 +4,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" -namespace esphome { -namespace wake_on_lan { +namespace esphome::wake_on_lan { static const char *const TAG = "wake_on_lan.button"; static const uint8_t PREFIX[6] = {255, 255, 255, 255, 255, 255}; @@ -84,6 +83,6 @@ void WakeOnLanButton::setup() { #endif } -} // namespace wake_on_lan -} // namespace esphome +} // namespace esphome::wake_on_lan + #endif diff --git a/esphome/components/wake_on_lan/wake_on_lan.h b/esphome/components/wake_on_lan/wake_on_lan.h index f516c4d669c..48f8d00a662 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.h +++ b/esphome/components/wake_on_lan/wake_on_lan.h @@ -9,8 +9,7 @@ #include "WiFiUdp.h" #endif -namespace esphome { -namespace wake_on_lan { +namespace esphome::wake_on_lan { class WakeOnLanButton : public button::Button, public Component { public: @@ -31,6 +30,6 @@ class WakeOnLanButton : public button::Button, public Component { uint8_t macaddr_[6]; }; -} // namespace wake_on_lan -} // namespace esphome +} // namespace esphome::wake_on_lan + #endif diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index edf113b0b4a..b05d7d4f6d9 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -14,8 +14,7 @@ #include "pico/stdlib.h" #endif -namespace esphome { -namespace watchdog { +namespace esphome::watchdog { static const char *const TAG = "http_request.watchdog"; @@ -75,5 +74,4 @@ uint32_t WatchdogManager::get_timeout_() { return timeout_ms; } -} // namespace watchdog -} // namespace esphome +} // namespace esphome::watchdog diff --git a/esphome/components/watchdog/watchdog.h b/esphome/components/watchdog/watchdog.h index 899ec3fde01..795c057672f 100644 --- a/esphome/components/watchdog/watchdog.h +++ b/esphome/components/watchdog/watchdog.h @@ -4,8 +4,7 @@ #include <cstdint> -namespace esphome { -namespace watchdog { +namespace esphome::watchdog { class WatchdogManager { public: @@ -20,5 +19,4 @@ class WatchdogManager { uint32_t timeout_ms_{0}; }; -} // namespace watchdog -} // namespace esphome +} // namespace esphome::watchdog diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index b55f3c8d26d..cc9c34cb42f 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace waveshare_epaper { +namespace esphome::waveshare_epaper { static const char *const TAG = "waveshare_2.13v3"; @@ -188,5 +187,4 @@ void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) this->full_update_every_ = full_update_every; } -} // namespace waveshare_epaper -} // namespace esphome +} // namespace esphome::waveshare_epaper diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 4db9438206a..14ff5ed53cc 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -5,8 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace waveshare_epaper { +namespace esphome::waveshare_epaper { static const char *const TAG = "waveshare_epaper"; @@ -4770,5 +4769,4 @@ void WaveshareEPaper13P3InK::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace waveshare_epaper -} // namespace esphome +} // namespace esphome::waveshare_epaper diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 74bb1535196..fa3737238e6 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -4,8 +4,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" -namespace esphome { -namespace waveshare_epaper { +namespace esphome::waveshare_epaper { class WaveshareEPaperBase : public display::DisplayBuffer, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, @@ -1099,5 +1098,4 @@ class WaveshareEPaper13P3InK : public WaveshareEPaper { uint32_t idle_timeout_() override; }; -} // namespace waveshare_epaper -} // namespace esphome +} // namespace esphome::waveshare_epaper diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 2ec56326912..a19dce4db34 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -6,8 +6,7 @@ #include "weikai.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace weikai { +namespace esphome::weikai { static const char *const TAG = "weikai"; @@ -567,5 +566,4 @@ bool WeikaiChannel::uart_receive_test_(char *message) { /// @} #endif -} // namespace weikai -} // namespace esphome +} // namespace esphome::weikai diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 36d8f66265e..6f38f583182 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -25,8 +25,7 @@ /// @endcode // #define TEST_COMPONENT -namespace esphome { -namespace weikai { +namespace esphome::weikai { /// @brief XFER_MAX_SIZE defines the maximum number of bytes allowed during one transfer. #if defined(I2C_BUFFER_LENGTH) @@ -440,5 +439,4 @@ class WeikaiChannel : public uart::UARTComponent { std::string name_; ///< name of the entity }; -} // namespace weikai -} // namespace esphome +} // namespace esphome::weikai diff --git a/esphome/components/weikai/wk_reg_def.h b/esphome/components/weikai/wk_reg_def.h index f3c90b196a6..eefbfbc0bf3 100644 --- a/esphome/components/weikai/wk_reg_def.h +++ b/esphome/components/weikai/wk_reg_def.h @@ -4,8 +4,7 @@ /// @date Last Modified: 2024/02/18 15:49:18 #pragma once -namespace esphome { -namespace weikai { +namespace esphome::weikai { //////////////////////////////////////////////////////////////////////////////////////// /// Definition of the WeiKai registers @@ -300,5 +299,4 @@ constexpr uint8_t WKREG_TFI = 0x08; /// @} /// @} -} // namespace weikai -} // namespace esphome +} // namespace esphome::weikai diff --git a/esphome/components/weikai_i2c/weikai_i2c.cpp b/esphome/components/weikai_i2c/weikai_i2c.cpp index 03ac74e0707..af5bd7f313b 100644 --- a/esphome/components/weikai_i2c/weikai_i2c.cpp +++ b/esphome/components/weikai_i2c/weikai_i2c.cpp @@ -5,8 +5,7 @@ #include "weikai_i2c.h" -namespace esphome { -namespace weikai_i2c { +namespace esphome::weikai_i2c { static const char *const TAG = "weikai_i2c"; /// @brief Display a buffer in hexadecimal format (32 hex values / line). @@ -177,5 +176,4 @@ void WeikaiComponentI2C::dump_config() { } } -} // namespace weikai_i2c -} // namespace esphome +} // namespace esphome::weikai_i2c diff --git a/esphome/components/weikai_i2c/weikai_i2c.h b/esphome/components/weikai_i2c/weikai_i2c.h index 0da9ed9cdec..940dbad9f26 100644 --- a/esphome/components/weikai_i2c/weikai_i2c.h +++ b/esphome/components/weikai_i2c/weikai_i2c.h @@ -13,8 +13,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/weikai/weikai.h" -namespace esphome { -namespace weikai_i2c { +namespace esphome::weikai_i2c { class WeikaiComponentI2C; @@ -57,5 +56,4 @@ class WeikaiComponentI2C : public weikai::WeikaiComponent, public i2c::I2CDevice WeikaiRegisterI2C reg_i2c_{this, 0, 0}; ///< init to this component }; -} // namespace weikai_i2c -} // namespace esphome +} // namespace esphome::weikai_i2c diff --git a/esphome/components/weikai_spi/weikai_spi.cpp b/esphome/components/weikai_spi/weikai_spi.cpp index 20671a58155..03bf66e07be 100644 --- a/esphome/components/weikai_spi/weikai_spi.cpp +++ b/esphome/components/weikai_spi/weikai_spi.cpp @@ -5,8 +5,7 @@ #include "weikai_spi.h" -namespace esphome { -namespace weikai_spi { +namespace esphome::weikai_spi { using namespace weikai; static const char *const TAG = "weikai_spi"; @@ -187,5 +186,4 @@ void WeikaiComponentSPI::dump_config() { } } -} // namespace weikai_spi -} // namespace esphome +} // namespace esphome::weikai_spi diff --git a/esphome/components/weikai_spi/weikai_spi.h b/esphome/components/weikai_spi/weikai_spi.h index a75b85dc8e2..3b581ef44ce 100644 --- a/esphome/components/weikai_spi/weikai_spi.h +++ b/esphome/components/weikai_spi/weikai_spi.h @@ -12,8 +12,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/weikai/weikai.h" -namespace esphome { -namespace weikai_spi { +namespace esphome::weikai_spi { //////////////////////////////////////////////////////////////////////////////////// /// @brief WeikaiRegisterSPI objects acts as proxies to access remote register through an SPI Bus //////////////////////////////////////////////////////////////////////////////////// @@ -49,5 +48,4 @@ class WeikaiComponentSPI : public weikai::WeikaiComponent, WeikaiRegisterSPI reg_spi_{this, 0, 0}; ///< init to this component }; -} // namespace weikai_spi -} // namespace esphome +} // namespace esphome::weikai_spi diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 86209cb7a6b..ace96d78fc2 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -1,8 +1,7 @@ #include "whirlpool.h" #include "esphome/core/log.h" -namespace esphome { -namespace whirlpool { +namespace esphome::whirlpool { static const char *const TAG = "whirlpool.climate"; @@ -307,5 +306,4 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace whirlpool -} // namespace esphome +} // namespace esphome::whirlpool diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index ada5a36de92..03b4cf21a82 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -2,8 +2,7 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace whirlpool { +namespace esphome::whirlpool { /// Simple enum to represent models. enum Model { @@ -61,5 +60,4 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { } }; -} // namespace whirlpool -} // namespace esphome +} // namespace esphome::whirlpool diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp index 003d2e0ba65..b8a8db4d7c3 100644 --- a/esphome/components/whynter/whynter.cpp +++ b/esphome/components/whynter/whynter.cpp @@ -1,8 +1,7 @@ #include "whynter.h" #include "esphome/core/log.h" -namespace esphome { -namespace whynter { +namespace esphome::whynter { static const char *const TAG = "climate.whynter"; @@ -177,5 +176,4 @@ void Whynter::transmit_(uint32_t value) { transmit.perform(); } -} // namespace whynter -} // namespace esphome +} // namespace esphome::whynter diff --git a/esphome/components/whynter/whynter.h b/esphome/components/whynter/whynter.h index 8273c21e4b8..d67bfa8fa03 100644 --- a/esphome/components/whynter/whynter.h +++ b/esphome/components/whynter/whynter.h @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace whynter { +namespace esphome::whynter { // Temperature const uint8_t TEMP_MIN_C = 16; // Celsius @@ -49,5 +48,4 @@ class Whynter : public climate_ir::ClimateIR { climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; }; -} // namespace whynter -} // namespace esphome +} // namespace esphome::whynter diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index f3f578794a6..e5c29f8b115 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace wiegand { +namespace esphome::wiegand { static const char *const TAG = "wiegand"; static const char *const KEYS = "0123456789*#"; @@ -127,5 +126,4 @@ void Wiegand::dump_config() { LOG_PIN(" D1 pin: ", this->d1_pin_); } -} // namespace wiegand -} // namespace esphome +} // namespace esphome::wiegand diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h index 994631a3a35..33d81ba086b 100644 --- a/esphome/components/wiegand/wiegand.h +++ b/esphome/components/wiegand/wiegand.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace wiegand { +namespace esphome::wiegand { class Wiegand; @@ -50,5 +49,4 @@ class Wiegand : public key_provider::KeyProvider, public Component { std::vector<WiegandKeyTrigger *> key_triggers_; }; -} // namespace wiegand -} // namespace esphome +} // namespace esphome::wiegand diff --git a/esphome/components/wk2132_i2c/wk2132_i2c.cpp b/esphome/components/wk2132_i2c/wk2132_i2c.cpp index aaefae6f970..d60e8a834f9 100644 --- a/esphome/components/wk2132_i2c/wk2132_i2c.cpp +++ b/esphome/components/wk2132_i2c/wk2132_i2c.cpp @@ -1,4 +1,2 @@ /* compiling with esp-idf framework requires a .cpp file for some reason ? */ -namespace esphome { -namespace wk2132_i2c {} -} // namespace esphome +namespace esphome::wk2132_i2c {} // namespace esphome::wk2132_i2c diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index a902adfddd0..f3eb17965d0 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -4,8 +4,7 @@ #include <cinttypes> -namespace esphome { -namespace wl_134 { +namespace esphome::wl_134 { static const char *const TAG = "wl_134.sensor"; static const uint8_t ASCII_CR = 0x0D; @@ -114,5 +113,4 @@ void Wl134Component::dump_config() { // As specified in the sensor's data sheet this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); } -} // namespace wl_134 -} // namespace esphome +} // namespace esphome::wl_134 diff --git a/esphome/components/wl_134/wl_134.h b/esphome/components/wl_134/wl_134.h index c0a90de17dd..973e5a1e7c3 100644 --- a/esphome/components/wl_134/wl_134.h +++ b/esphome/components/wl_134/wl_134.h @@ -6,8 +6,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace wl_134 { +namespace esphome::wl_134 { class Wl134Component : public text_sensor::TextSensor, public Component, public uart::UARTDevice { public: @@ -59,5 +58,4 @@ class Wl134Component : public text_sensor::TextSensor, public Component, public uint64_t hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t text_size); }; -} // namespace wl_134 -} // namespace esphome +} // namespace esphome::wl_134 diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index db2708d6d02..e0724aa94a3 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -17,8 +17,7 @@ #include <WiFiUdp.h> #endif -namespace esphome { -namespace wled { +namespace esphome::wled { // Description of protocols: // https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control @@ -284,7 +283,6 @@ bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint return true; } -} // namespace wled -} // namespace esphome +} // namespace esphome::wled #endif // USE_ARDUINO diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h index 3f3b7106119..bed897f5a61 100644 --- a/esphome/components/wled/wled_light_effect.h +++ b/esphome/components/wled/wled_light_effect.h @@ -10,8 +10,7 @@ class UDP; -namespace esphome { -namespace wled { +namespace esphome::wled { class WLEDLightEffect : public light::AddressableLightEffect { public: @@ -42,7 +41,6 @@ class WLEDLightEffect : public light::AddressableLightEffect { bool blank_on_start_{true}; }; -} // namespace wled -} // namespace esphome +} // namespace esphome::wled #endif // USE_ARDUINO diff --git a/esphome/components/wts01/wts01.cpp b/esphome/components/wts01/wts01.cpp index a7948c805a1..cc7ee980790 100644 --- a/esphome/components/wts01/wts01.cpp +++ b/esphome/components/wts01/wts01.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include <cmath> -namespace esphome { -namespace wts01 { +namespace esphome::wts01 { constexpr uint8_t HEADER_1 = 0x55; constexpr uint8_t HEADER_2 = 0x01; @@ -90,5 +89,4 @@ void WTS01Sensor::process_packet_() { this->publish_state(temperature); } -} // namespace wts01 -} // namespace esphome +} // namespace esphome::wts01 diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h index aae90c2c771..17d4dc57a2f 100644 --- a/esphome/components/wts01/wts01.h +++ b/esphome/components/wts01/wts01.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace wts01 { +namespace esphome::wts01 { constexpr uint8_t PACKET_SIZE = 9; @@ -22,5 +21,4 @@ class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Compo void process_packet_(); }; -} // namespace wts01 -} // namespace esphome +} // namespace esphome::wts01 diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index 773e52d6e13..52ce328b3c6 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -1,8 +1,7 @@ #include "x9c.h" #include "esphome/core/log.h" -namespace esphome { -namespace x9c { +namespace esphome::x9c { static const char *const TAG = "x9c.output"; @@ -73,5 +72,4 @@ void X9cOutput::dump_config() { LOG_FLOAT_OUTPUT(this); } -} // namespace x9c -} // namespace esphome +} // namespace esphome::x9c diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h index 7dcd79bb7ce..112f0405d7d 100644 --- a/esphome/components/x9c/x9c.h +++ b/esphome/components/x9c/x9c.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace x9c { +namespace esphome::x9c { class X9cOutput : public output::FloatOutput, public Component { public: @@ -30,5 +29,4 @@ class X9cOutput : public output::FloatOutput, public Component { int step_delay_{0}; }; -} // namespace x9c -} // namespace esphome +} // namespace esphome::x9c diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index 5e816469acf..0eefd594d09 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -6,8 +6,7 @@ #include <cinttypes> -namespace esphome { -namespace xgzp68xx { +namespace esphome::xgzp68xx { static const char *const TAG = "xgzp68xx.sensor"; @@ -118,5 +117,4 @@ void XGZP68XXComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace xgzp68xx -} // namespace esphome +} // namespace esphome::xgzp68xx diff --git a/esphome/components/xgzp68xx/xgzp68xx.h b/esphome/components/xgzp68xx/xgzp68xx.h index ce9cfd6b781..1bab9b091aa 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.h +++ b/esphome/components/xgzp68xx/xgzp68xx.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace xgzp68xx { +namespace esphome::xgzp68xx { /// Enum listing all oversampling options for the XGZP68XX. enum XGZP68XXOversampling : uint8_t { @@ -43,5 +42,4 @@ class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public XGZP68XXOversampling last_pressure_oversampling_{XGZP68XX_OVERSAMPLING_UNKNOWN}; }; -} // namespace xgzp68xx -} // namespace esphome +} // namespace esphome::xgzp68xx diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 2c1611d0c7d..0961df2bd63 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -12,8 +12,7 @@ #include "mbedtls/ccm.h" #endif -namespace esphome { -namespace xiaomi_ble { +namespace esphome::xiaomi_ble { static const char *const TAG = "xiaomi_ble"; @@ -460,7 +459,6 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return false; // with true it's not showing device scans } -} // namespace xiaomi_ble -} // namespace esphome +} // namespace esphome::xiaomi_ble #endif diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 42609a998b2..a4ecca0c66e 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_ble { +namespace esphome::xiaomi_ble { struct XiaomiParseResult { enum { @@ -78,7 +77,6 @@ class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; }; -} // namespace xiaomi_ble -} // namespace esphome +} // namespace esphome::xiaomi_ble #endif diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp index 82a04f0d6e6..948e02be463 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgd1 { +namespace esphome::xiaomi_cgd1 { static const char *const TAG = "xiaomi_cgd1"; @@ -65,7 +64,6 @@ bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { void XiaomiCGD1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_cgd1 -} // namespace esphome +} // namespace esphome::xiaomi_cgd1 #endif diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index 4a34eea32ad..1c510c7eb4b 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgd1 { +namespace esphome::xiaomi_cgd1 { class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -29,7 +28,6 @@ class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_cgd1 -} // namespace esphome +} // namespace esphome::xiaomi_cgd1 #endif diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp index 39ece3e0916..ff9036db14e 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgdk2 { +namespace esphome::xiaomi_cgdk2 { static const char *const TAG = "xiaomi_cgdk2"; @@ -65,7 +64,6 @@ bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { void XiaomiCGDK2::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_cgdk2 -} // namespace esphome +} // namespace esphome::xiaomi_cgdk2 #endif diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index ed917e2bbd8..02d098c31b0 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgdk2 { +namespace esphome::xiaomi_cgdk2 { class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -29,7 +28,6 @@ class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListe sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_cgdk2 -} // namespace esphome +} // namespace esphome::xiaomi_cgdk2 #endif diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 448592db16f..ef4ef46424d 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgg1 { +namespace esphome::xiaomi_cgg1 { static const char *const TAG = "xiaomi_cgg1"; @@ -65,7 +64,6 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { void XiaomiCGG1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_cgg1 -} // namespace esphome +} // namespace esphome::xiaomi_cgg1 #endif diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index c560bddd695..d49e3a08d1e 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgg1 { +namespace esphome::xiaomi_cgg1 { class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -30,7 +29,6 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_cgg1 -} // namespace esphome +} // namespace esphome::xiaomi_cgg1 #endif diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp index 8813f6479b7..3203f358b99 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgpr1 { +namespace esphome::xiaomi_cgpr1 { static const char *const TAG = "xiaomi_cgpr1"; @@ -62,7 +61,6 @@ bool XiaomiCGPR1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { void XiaomiCGPR1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_cgpr1 -} // namespace esphome +} // namespace esphome::xiaomi_cgpr1 #endif diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index 82bbbfa58d9..28a7a3ae2d6 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_cgpr1 { +namespace esphome::xiaomi_cgpr1 { class XiaomiCGPR1 : public Component, public binary_sensor::BinarySensorInitiallyOff, @@ -33,7 +32,6 @@ class XiaomiCGPR1 : public Component, sensor::Sensor *illuminance_{nullptr}; }; -} // namespace xiaomi_cgpr1 -} // namespace esphome +} // namespace esphome::xiaomi_cgpr1 #endif diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp index 159b6df80bc..11ea98045b5 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_gcls002 { +namespace esphome::xiaomi_gcls002 { static const char *const TAG = "xiaomi_gcls002"; @@ -58,7 +57,6 @@ bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -} // namespace xiaomi_gcls002 -} // namespace esphome +} // namespace esphome::xiaomi_gcls002 #endif diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h index 83c8f15ace7..e14077adb0f 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_gcls002 { +namespace esphome::xiaomi_gcls002 { class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -30,7 +29,6 @@ class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *illuminance_{nullptr}; }; -} // namespace xiaomi_gcls002 -} // namespace esphome +} // namespace esphome::xiaomi_gcls002 #endif diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index e10754d8329..1d872c68c11 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccjcy01 { +namespace esphome::xiaomi_hhccjcy01 { static const char *const TAG = "xiaomi_hhccjcy01"; @@ -61,7 +60,6 @@ bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -} // namespace xiaomi_hhccjcy01 -} // namespace esphome +} // namespace esphome::xiaomi_hhccjcy01 #endif diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index 96ea9217fbb..8bc63990655 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccjcy01 { +namespace esphome::xiaomi_hhccjcy01 { class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -32,7 +31,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_hhccjcy01 -} // namespace esphome +} // namespace esphome::xiaomi_hhccjcy01 #endif diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp index 028d797ac1e..c6ebd5ff744 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccjcy10 { +namespace esphome::xiaomi_hhccjcy10 { static const char *const TAG = "xiaomi_hhccjcy10"; @@ -63,7 +62,6 @@ bool XiaomiHHCCJCY10::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -} // namespace xiaomi_hhccjcy10 -} // namespace esphome +} // namespace esphome::xiaomi_hhccjcy10 #endif diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h index bd4ad75c1d5..812e3a7d8f4 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccjcy10 { +namespace esphome::xiaomi_hhccjcy10 { class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -31,7 +30,6 @@ class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_hhccjcy10 -} // namespace esphome +} // namespace esphome::xiaomi_hhccjcy10 #endif diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp index 2d2447db271..bbca9faaa6a 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccpot002 { +namespace esphome::xiaomi_hhccpot002 { static const char *const TAG = "xiaomi_hhccpot002"; @@ -52,7 +51,6 @@ bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device return success; } -} // namespace xiaomi_hhccpot002 -} // namespace esphome +} // namespace esphome::xiaomi_hhccpot002 #endif diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h index 0ec34b1871d..2bdd6102be0 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_hhccpot002 { +namespace esphome::xiaomi_hhccpot002 { class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -26,7 +25,6 @@ class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *conductivity_{nullptr}; }; -} // namespace xiaomi_hhccpot002 -} // namespace esphome +} // namespace esphome::xiaomi_hhccpot002 #endif diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp index 8216a92e54d..c0f4de3d06b 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_jqjcy01ym { +namespace esphome::xiaomi_jqjcy01ym { static const char *const TAG = "xiaomi_jqjcy01ym"; @@ -58,7 +57,6 @@ bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -} // namespace xiaomi_jqjcy01ym -} // namespace esphome +} // namespace esphome::xiaomi_jqjcy01ym #endif diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h index e9c44800f29..aaf34f899fe 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_jqjcy01ym { +namespace esphome::xiaomi_jqjcy01ym { class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -30,7 +29,6 @@ class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_jqjcy01ym -} // namespace esphome +} // namespace esphome::xiaomi_jqjcy01ym #endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index e140835d039..75909738c87 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd02 { +namespace esphome::xiaomi_lywsd02 { static const char *const TAG = "xiaomi_lywsd02"; @@ -55,7 +54,6 @@ bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -} // namespace xiaomi_lywsd02 -} // namespace esphome +} // namespace esphome::xiaomi_lywsd02 #endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 772b389a927..e45596f966d 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd02 { +namespace esphome::xiaomi_lywsd02 { class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -28,7 +27,6 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_lywsd02 -} // namespace esphome +} // namespace esphome::xiaomi_lywsd02 #endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp index 2dd60d4ecbc..79610ee2662 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd02mmc { +namespace esphome::xiaomi_lywsd02mmc { static const char *const TAG = "xiaomi_lywsd02mmc"; @@ -65,7 +64,6 @@ bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device void XiaomiLYWSD02MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_lywsd02mmc -} // namespace esphome +} // namespace esphome::xiaomi_lywsd02mmc #endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index 968604fee69..23efcbf8fc4 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd02mmc { +namespace esphome::xiaomi_lywsd02mmc { class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -30,7 +29,6 @@ class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_lywsd02mmc -} // namespace esphome +} // namespace esphome::xiaomi_lywsd02mmc #endif diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index b11bbdc40cd..a0a92601563 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd03mmc { +namespace esphome::xiaomi_lywsd03mmc { static const char *const TAG = "xiaomi_lywsd03mmc"; @@ -69,7 +68,6 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device void XiaomiLYWSD03MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_lywsd03mmc -} // namespace esphome +} // namespace esphome::xiaomi_lywsd03mmc #endif diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index d890e5ed120..03462b850fd 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsd03mmc { +namespace esphome::xiaomi_lywsd03mmc { class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -29,7 +28,6 @@ class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_lywsd03mmc -} // namespace esphome +} // namespace esphome::xiaomi_lywsd03mmc #endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 65991ffa0e9..56efaaef513 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsdcgq { +namespace esphome::xiaomi_lywsdcgq { static const char *const TAG = "xiaomi_lywsdcgq"; @@ -55,7 +54,6 @@ bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -} // namespace xiaomi_lywsdcgq -} // namespace esphome +} // namespace esphome::xiaomi_lywsdcgq #endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index cf90db937f5..e169afc6519 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_lywsdcgq { +namespace esphome::xiaomi_lywsdcgq { class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -28,7 +27,6 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_lywsdcgq -} // namespace esphome +} // namespace esphome::xiaomi_lywsdcgq #endif diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp index 1097b9c1e81..74626ed0a51 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mhoc303 { +namespace esphome::xiaomi_mhoc303 { static const char *const TAG = "xiaomi_mhoc303"; @@ -55,7 +54,6 @@ bool XiaomiMHOC303::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -} // namespace xiaomi_mhoc303 -} // namespace esphome +} // namespace esphome::xiaomi_mhoc303 #endif diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h index c3b8e7d68f5..daacd6be867 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mhoc303 { +namespace esphome::xiaomi_mhoc303 { class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -28,7 +27,6 @@ class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_mhoc303 -} // namespace esphome +} // namespace esphome::xiaomi_mhoc303 #endif diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index 10cd15ddbdd..1cf0de14d36 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mhoc401 { +namespace esphome::xiaomi_mhoc401 { static const char *const TAG = "xiaomi_mhoc401"; @@ -69,7 +68,6 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { void XiaomiMHOC401::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_mhoc401 -} // namespace esphome +} // namespace esphome::xiaomi_mhoc401 #endif diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 13547e45d9f..225c9ff1898 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mhoc401 { +namespace esphome::xiaomi_mhoc401 { class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -29,7 +28,6 @@ class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_mhoc401 -} // namespace esphome +} // namespace esphome::xiaomi_mhoc401 #endif diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index e4f77fb9153..2b1492129c2 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_miscale { +namespace esphome::xiaomi_miscale { static const char *const TAG = "xiaomi_miscale"; @@ -167,7 +166,6 @@ bool XiaomiMiscale::report_results_(const optional<ParseResult> &result, const c return true; } -} // namespace xiaomi_miscale -} // namespace esphome +} // namespace esphome::xiaomi_miscale #endif diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 3d793e07ac5..c75a22c9fb8 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_miscale { +namespace esphome::xiaomi_miscale { struct ParseResult { int version; @@ -40,7 +39,6 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool report_results_(const optional<ParseResult> &result, const char *address); }; -} // namespace xiaomi_miscale -} // namespace esphome +} // namespace esphome::xiaomi_miscale #endif diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp index ec03c851cd4..a7b2554aad9 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mjyd02yla { +namespace esphome::xiaomi_mjyd02yla { static const char *const TAG = "xiaomi_mjyd02yla"; @@ -65,7 +64,6 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) void XiaomiMJYD02YLA::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_mjyd02yla -} // namespace esphome +} // namespace esphome::xiaomi_mjyd02yla #endif diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index bf9dcaf8446..ee4ed525209 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mjyd02yla { +namespace esphome::xiaomi_mjyd02yla { class XiaomiMJYD02YLA : public Component, public binary_sensor::BinarySensorInitiallyOff, @@ -35,7 +34,6 @@ class XiaomiMJYD02YLA : public Component, binary_sensor::BinarySensor *is_light_{nullptr}; }; -} // namespace xiaomi_mjyd02yla -} // namespace esphome +} // namespace esphome::xiaomi_mjyd02yla #endif diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp index a3f9325946c..259e0159c50 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mue4094rt { +namespace esphome::xiaomi_mue4094rt { static const char *const TAG = "xiaomi_mue4094rt"; @@ -51,7 +50,6 @@ bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -} // namespace xiaomi_mue4094rt -} // namespace esphome +} // namespace esphome::xiaomi_mue4094rt #endif diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h index f1da0705d02..a6d8abc5bf6 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_mue4094rt { +namespace esphome::xiaomi_mue4094rt { class XiaomiMUE4094RT : public Component, public binary_sensor::BinarySensorInitiallyOff, @@ -26,7 +25,6 @@ class XiaomiMUE4094RT : public Component, uint16_t timeout_; }; -} // namespace xiaomi_mue4094rt -} // namespace esphome +} // namespace esphome::xiaomi_mue4094rt #endif diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index 0f27f09c871..b42a5a37006 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_rtcgq02lm { +namespace esphome::xiaomi_rtcgq02lm { static const char *const TAG = "xiaomi_rtcgq02lm"; @@ -79,7 +78,6 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) void XiaomiRTCGQ02LM::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_rtcgq02lm -} // namespace esphome +} // namespace esphome::xiaomi_rtcgq02lm #endif diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index 87dfc0b62bc..cc6a334a20d 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -13,8 +13,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_rtcgq02lm { +namespace esphome::xiaomi_rtcgq02lm { class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -54,7 +53,6 @@ class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceL #endif }; -} // namespace xiaomi_rtcgq02lm -} // namespace esphome +} // namespace esphome::xiaomi_rtcgq02lm #endif diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp index b0e02e2372e..1bf861a6afd 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_wx08zm { +namespace esphome::xiaomi_wx08zm { static const char *const TAG = "xiaomi_wx08zm"; @@ -56,7 +55,6 @@ bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -} // namespace xiaomi_wx08zm -} // namespace esphome +} // namespace esphome::xiaomi_wx08zm #endif diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h index 081705fd505..0b0cb8db0bc 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_wx08zm { +namespace esphome::xiaomi_wx08zm { class XiaomiWX08ZM : public Component, public binary_sensor::BinarySensorInitiallyOff, @@ -29,7 +28,6 @@ class XiaomiWX08ZM : public Component, sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_wx08zm -} // namespace esphome +} // namespace esphome::xiaomi_wx08zm #endif diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp index 50cf5f2d767..a4303b055ab 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_xmwsdj04mmc { +namespace esphome::xiaomi_xmwsdj04mmc { static const char *const TAG = "xiaomi_xmwsdj04mmc"; @@ -69,7 +68,6 @@ bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &devic void XiaomiXMWSDJ04MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } -} // namespace xiaomi_xmwsdj04mmc -} // namespace esphome +} // namespace esphome::xiaomi_xmwsdj04mmc #endif diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index 22cac630595..9bab943ab94 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace xiaomi_xmwsdj04mmc { +namespace esphome::xiaomi_xmwsdj04mmc { class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: @@ -30,7 +29,6 @@ class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_xmwsdj04mmc -} // namespace esphome +} // namespace esphome::xiaomi_xmwsdj04mmc #endif diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index cfcbeeeb8d7..d189d9b5c71 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -1,8 +1,7 @@ #include "xl9535.h" #include "esphome/core/log.h" -namespace esphome { -namespace xl9535 { +namespace esphome::xl9535 { static const char *const TAG = "xl9535"; @@ -118,5 +117,4 @@ void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-> bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void XL9535GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -} // namespace xl9535 -} // namespace esphome +} // namespace esphome::xl9535 diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h index be0e2fbd820..253ce762734 100644 --- a/esphome/components/xl9535/xl9535.h +++ b/esphome/components/xl9535/xl9535.h @@ -4,8 +4,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/core/hal.h" -namespace esphome { -namespace xl9535 { +namespace esphome::xl9535 { enum { XL9535_INPUT_PORT_0_REGISTER = 0x00, @@ -52,5 +51,4 @@ class XL9535GPIOPin : public GPIOPin { gpio::Flags flags_; }; -} // namespace xl9535 -} // namespace esphome +} // namespace esphome::xl9535 diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp index 84d3daf8239..d08a54529d8 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.cpp +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -4,8 +4,7 @@ #include <algorithm> -namespace esphome { -namespace xpt2046 { +namespace esphome::xpt2046 { static const char *const TAG = "xpt2046"; @@ -107,5 +106,4 @@ int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT return ((data[0] << 8) | data[1]) >> 3; } -} // namespace xpt2046 -} // namespace esphome +} // namespace esphome::xpt2046 diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h index f691ae2c7b3..f619e06fb77 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.h +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace xpt2046 { +namespace esphome::xpt2046 { using namespace touchscreen; @@ -37,5 +36,4 @@ class XPT2046Component : public Touchscreen, InternalGPIOPin *irq_pin_{nullptr}; }; -} // namespace xpt2046 -} // namespace esphome +} // namespace esphome::xpt2046 diff --git a/esphome/components/xxtea/xxtea.cpp b/esphome/components/xxtea/xxtea.cpp index ba17530b243..ded7abc2216 100644 --- a/esphome/components/xxtea/xxtea.cpp +++ b/esphome/components/xxtea/xxtea.cpp @@ -1,7 +1,6 @@ #include "xxtea.h" -namespace esphome { -namespace xxtea { +namespace esphome::xxtea { static const uint32_t DELTA = 0x9e3779b9; #define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) @@ -46,5 +45,4 @@ void decrypt(uint32_t *v, size_t n, const uint32_t *k) { } } -} // namespace xxtea -} // namespace esphome +} // namespace esphome::xxtea diff --git a/esphome/components/xxtea/xxtea.h b/esphome/components/xxtea/xxtea.h index 86afbd1d46b..8019fb07fc2 100644 --- a/esphome/components/xxtea/xxtea.h +++ b/esphome/components/xxtea/xxtea.h @@ -3,8 +3,7 @@ #include <cstdint> #include <cstddef> -namespace esphome { -namespace xxtea { +namespace esphome::xxtea { /** * Encrypt a block of data in-place using XXTEA algorithm with 256-bit key @@ -22,5 +21,4 @@ void encrypt(uint32_t *v, size_t n, const uint32_t *k); */ void decrypt(uint32_t *v, size_t n, const uint32_t *k); -} // namespace xxtea -} // namespace esphome +} // namespace esphome::xxtea diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index 4a64e6c41c6..ba0a3a64040 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -1,8 +1,7 @@ #include "yashima.h" #include "esphome/core/log.h" -namespace esphome { -namespace yashima { +namespace esphome::yashima { static const char *const TAG = "yashima.climate"; @@ -197,5 +196,4 @@ void YashimaClimate::transmit_state_() { transmit.perform(); } -} // namespace yashima -} // namespace esphome +} // namespace esphome::yashima diff --git a/esphome/components/yashima/yashima.h b/esphome/components/yashima/yashima.h index 466816bd5f7..336b28f5c52 100644 --- a/esphome/components/yashima/yashima.h +++ b/esphome/components/yashima/yashima.h @@ -7,8 +7,7 @@ #include "esphome/components/remote_transmitter/remote_transmitter.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace yashima { +namespace esphome::yashima { class YashimaClimate : public climate::Climate, public Component { public: @@ -36,5 +35,4 @@ class YashimaClimate : public climate::Climate, public Component { sensor::Sensor *sensor_{nullptr}; }; -} // namespace yashima -} // namespace esphome +} // namespace esphome::yashima diff --git a/esphome/components/zhlt01/zhlt01.cpp b/esphome/components/zhlt01/zhlt01.cpp index e5ab5915e4b..2585ac1b572 100644 --- a/esphome/components/zhlt01/zhlt01.cpp +++ b/esphome/components/zhlt01/zhlt01.cpp @@ -1,8 +1,7 @@ #include "zhlt01.h" #include "esphome/core/log.h" -namespace esphome { -namespace zhlt01 { +namespace esphome::zhlt01 { static const char *const TAG = "zhlt01.climate"; @@ -234,5 +233,4 @@ bool ZHLT01Climate::on_receive(remote_base::RemoteReceiveData data) { return true; } -} // namespace zhlt01 -} // namespace esphome +} // namespace esphome::zhlt01 diff --git a/esphome/components/zhlt01/zhlt01.h b/esphome/components/zhlt01/zhlt01.h index 4413be2835c..61fc2cc16a1 100644 --- a/esphome/components/zhlt01/zhlt01.h +++ b/esphome/components/zhlt01/zhlt01.h @@ -82,8 +82,7 @@ * ***********************************************************************************/ -namespace esphome { -namespace zhlt01 { +namespace esphome::zhlt01 { /******************************************************************************** * TIMINGS @@ -163,5 +162,4 @@ class ZHLT01Climate : public climate_ir::ClimateIR { bool on_receive(remote_base::RemoteReceiveData data) override; }; -} // namespace zhlt01 -} // namespace esphome +} // namespace esphome::zhlt01 diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp index 565bbe9b4f6..1fa995b3dcd 100644 --- a/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace zio_ultrasonic { +namespace esphome::zio_ultrasonic { static const char *const TAG = "zio_ultrasonic"; @@ -27,5 +26,4 @@ void ZioUltrasonicComponent::update() { } } -} // namespace zio_ultrasonic -} // namespace esphome +} // namespace esphome::zio_ultrasonic diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h index 23057b2ab01..d4d2ac974fe 100644 --- a/esphome/components/zio_ultrasonic/zio_ultrasonic.h +++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h @@ -6,8 +6,7 @@ static const char *const TAG = "Zio Ultrasonic"; -namespace esphome { -namespace zio_ultrasonic { +namespace esphome::zio_ultrasonic { class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor { public: @@ -16,5 +15,4 @@ class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, p void update() override; }; -} // namespace zio_ultrasonic -} // namespace esphome +} // namespace esphome::zio_ultrasonic diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp index 621439aa0c2..0b834de90a4 100644 --- a/esphome/components/zyaura/zyaura.cpp +++ b/esphome/components/zyaura/zyaura.cpp @@ -1,8 +1,7 @@ #include "zyaura.h" #include "esphome/core/log.h" -namespace esphome { -namespace zyaura { +namespace esphome::zyaura { static const char *const TAG = "zyaura"; @@ -121,5 +120,4 @@ void ZyAuraSensor::update() { } } -} // namespace zyaura -} // namespace esphome +} // namespace esphome::zyaura diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index 3070aa90c5a..7c7954dec25 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace zyaura { +namespace esphome::zyaura { static const uint8_t ZA_MAX_MS = 2; static const uint8_t ZA_MSG_LEN = 5; @@ -81,5 +80,4 @@ class ZyAuraSensor : public PollingComponent { bool publish_state_(ZaDataType data_type, sensor::Sensor *sensor, uint16_t *data_value); }; -} // namespace zyaura -} // namespace esphome +} // namespace esphome::zyaura From e152c6155b1d76fe7b5e84dd42b7f2144834c139 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 8 May 2026 14:00:50 +1200 Subject: [PATCH 1898/2056] [ci] Skip needs-docs for new components without CONFIG_SCHEMA (#16303) --- .github/scripts/auto-label-pr/detectors.js | 105 +++++++++++++++------ .github/scripts/auto-label-pr/index.js | 17 +++- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 25c0ba49afc..410c1a53c07 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -1,4 +1,3 @@ -const fs = require('fs'); const { DOCS_PR_PATTERNS } = require('./constants'); const { COMPONENT_REGEX, @@ -9,6 +8,31 @@ const { } = require('../detect-tags'); const { loadCodeowners, getEffectiveOwners } = require('../codeowners'); +// Top-level `CONFIG_SCHEMA = ...` (assignment) or `CONFIG_SCHEMA: ConfigType = ...` (annotation). +// Ruff/Black enforce exactly one space around `=` and no space before `:`, +// so we can match strictly: `CONFIG_SCHEMA ` or `CONFIG_SCHEMA:`. +const CONFIG_SCHEMA_REGEX = /^CONFIG_SCHEMA[ :]/m; + +// Fetch a file's contents from the PR head SHA via the GitHub API. +// The auto-label workflow runs on `pull_request_target`, which checks out the +// base branch — files added by the PR don't exist in the workspace, so we have +// to fetch them from the head SHA. Returns null if the file can't be fetched. +async function fetchPrFileContent(github, context, path) { + try { + const { owner, repo } = context.repo; + const { data } = await github.rest.repos.getContent({ + owner, + repo, + path, + ref: context.payload.pull_request.head.sha, + }); + return Buffer.from(data.content, 'base64').toString('utf8'); + } catch (error) { + console.log(`Failed to fetch ${path} from PR head:`, error.message); + return null; + } +} + // Strategy: Merge branch detection async function detectMergeBranch(context) { const labels = new Set(); @@ -45,52 +69,64 @@ async function detectComponentPlatforms(changedFiles, apiData) { } // Strategy: New component detection -async function detectNewComponents(prFiles) { +async function detectNewComponents(github, context, prFiles) { const labels = new Set(); + let hasYamlLoadable = false; const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); for (const file of addedFiles) { const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); - if (componentMatch) { - try { - const content = fs.readFileSync(file, 'utf8'); - if (content.includes('IS_TARGET_PLATFORM = True')) { - labels.add('new-target-platform'); - } - } catch (error) { - console.log(`Failed to read content of ${file}:`, error.message); - } - labels.add('new-component'); + if (!componentMatch) continue; + + labels.add('new-component'); + const content = await fetchPrFileContent(github, context, file); + if (content === null) { + // Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure + hasYamlLoadable = true; + continue; + } + if (content.includes('IS_TARGET_PLATFORM = True')) { + labels.add('new-target-platform'); + } + if (CONFIG_SCHEMA_REGEX.test(content)) { + hasYamlLoadable = true; } } - return labels; + return { labels, hasYamlLoadable }; } // Strategy: New platform detection -async function detectNewPlatforms(prFiles, apiData) { +async function detectNewPlatforms(github, context, prFiles, apiData) { const labels = new Set(); + let hasYamlLoadable = false; const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); - for (const file of addedFiles) { - const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); - if (platformFileMatch) { - const [, component, platform] = platformFileMatch; - if (apiData.platformComponents.includes(platform)) { - labels.add('new-platform'); - } - } + const platformPathPatterns = [ + /^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/, + /^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/, + ]; - const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); - if (platformDirMatch) { - const [, component, platform] = platformDirMatch; - if (apiData.platformComponents.includes(platform)) { - labels.add('new-platform'); + for (const file of addedFiles) { + for (const re of platformPathPatterns) { + const match = file.match(re); + if (!match) continue; + const platform = match[2]; + if (!apiData.platformComponents.includes(platform)) break; + + labels.add('new-platform'); + const content = await fetchPrFileContent(github, context, file); + if (content === null) { + // Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure + hasYamlLoadable = true; + } else if (CONFIG_SCHEMA_REGEX.test(content)) { + hasYamlLoadable = true; } + break; } } - return labels; + return { labels, hasYamlLoadable }; } // Strategy: Core files detection @@ -300,7 +336,7 @@ function detectMaintainerAccess(context) { } // Strategy: Requirements detection -async function detectRequirements(allLabels, prFiles, context) { +async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) { const labels = new Set(); // Check for missing tests @@ -308,8 +344,15 @@ async function detectRequirements(allLabels, prFiles, context) { labels.add('needs-tests'); } - // Check for missing docs - if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) { + // Check for missing docs. + // `new-feature` (PR-body checkbox) always counts. `new-component` / `new-platform` + // only count when at least one newly added file defines a top-level CONFIG_SCHEMA, + // i.e. the new component/platform is actually loadable from YAML. + const docsEligible = + allLabels.has('new-feature') || + ((allLabels.has('new-component') || allLabels.has('new-platform')) && hasYamlLoadable); + + if (docsEligible) { const prBody = context.payload.pull_request.body || ''; const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 021e91a9ee1..9769cd80601 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -106,8 +106,8 @@ module.exports = async ({ github, context }) => { const [ branchLabels, componentLabels, - newComponentLabels, - newPlatformLabels, + newComponentResult, + newPlatformResult, coreLabels, sizeLabels, dashboardLabels, @@ -120,8 +120,8 @@ module.exports = async ({ github, context }) => { ] = await Promise.all([ detectMergeBranch(context), detectComponentPlatforms(changedFiles, apiData), - detectNewComponents(prFiles), - detectNewPlatforms(prFiles, apiData), + detectNewComponents(github, context, prFiles), + detectNewPlatforms(github, context, prFiles, apiData), detectCoreChanges(changedFiles), detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectDashboardChanges(changedFiles), @@ -133,6 +133,13 @@ module.exports = async ({ github, context }) => { detectMaintainerAccess(context) ]); + // Extract new-component / new-platform results + const newComponentLabels = newComponentResult.labels; + const newPlatformLabels = newPlatformResult.labels; + // Eligible for needs-docs only if any newly added component or platform file + // defines a top-level CONFIG_SCHEMA (i.e. is actually loadable from YAML). + const hasYamlLoadable = newComponentResult.hasYamlLoadable || newPlatformResult.hasYamlLoadable; + // Extract deprecated component info const deprecatedLabels = deprecatedResult.labels; const deprecatedInfo = deprecatedResult.deprecatedInfo; @@ -154,7 +161,7 @@ module.exports = async ({ github, context }) => { ]); // Detect requirements based on all other labels - const requirementLabels = await detectRequirements(allLabels, prFiles, context); + const requirementLabels = await detectRequirements(allLabels, prFiles, context, hasYamlLoadable); for (const label of requirementLabels) { allLabels.add(label); } From 08b17c9da1fcdb5d58d3133b144481969ca7bf54 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 7 May 2026 22:01:37 -0400 Subject: [PATCH 1899/2056] [core] Move core ring buffer to helper component (#16298) --- CODEOWNERS | 1 + esphome/components/audio/__init__.py | 1 + esphome/components/audio/audio_decoder.cpp | 4 +- esphome/components/audio/audio_decoder.h | 6 +- esphome/components/audio/audio_reader.cpp | 2 +- esphome/components/audio/audio_reader.h | 6 +- esphome/components/audio/audio_resampler.cpp | 4 +- esphome/components/audio/audio_resampler.h | 6 +- .../components/audio/audio_transfer_buffer.h | 10 +- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 4 +- .../i2s_audio/speaker/i2s_audio_speaker.h | 4 +- .../speaker/i2s_audio_speaker_standard.cpp | 2 +- .../components/micro_wake_word/__init__.py | 1 + .../micro_wake_word/micro_wake_word.cpp | 4 +- .../micro_wake_word/micro_wake_word.h | 4 +- .../mixer/speaker/mixer_speaker.cpp | 6 +- .../components/mixer/speaker/mixer_speaker.h | 3 +- .../resampler/speaker/resampler_speaker.cpp | 8 +- .../resampler/speaker/resampler_speaker.h | 3 +- esphome/components/ring_buffer/__init__.py | 7 + .../ring_buffer}/ring_buffer.cpp | 6 +- esphome/components/ring_buffer/ring_buffer.h | 126 ++++++++++++++++++ .../components/sound_level/sound_level.cpp | 6 +- esphome/components/sound_level/sound_level.h | 4 +- .../speaker/media_player/audio_pipeline.cpp | 6 +- .../speaker/media_player/audio_pipeline.h | 4 +- .../components/voice_assistant/__init__.py | 2 +- .../voice_assistant/voice_assistant.cpp | 4 +- .../voice_assistant/voice_assistant.h | 4 +- esphome/core/config.py | 4 - esphome/core/ring_buffer.h | 126 ++---------------- esphome/writer.py | 10 +- 32 files changed, 214 insertions(+), 174 deletions(-) create mode 100644 esphome/components/ring_buffer/__init__.py rename esphome/{core => components/ring_buffer}/ring_buffer.cpp (97%) create mode 100644 esphome/components/ring_buffer/ring_buffer.h diff --git a/CODEOWNERS b/CODEOWNERS index 471def542bb..f8cdfdc6c68 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -416,6 +416,7 @@ esphome/components/resampler/speaker/* @kahrendt esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz +esphome/components/ring_buffer/* @kahrendt esphome/components/rp2040/* @jesserockz esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 67ef2e7d1ae..44371e87ab2 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( from esphome.core import CORE import esphome.final_validate as fv +AUTO_LOAD = ["ring_buffer"] CODEOWNERS = ["@kahrendt"] DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 3e6fad1101e..d4ff59fc363 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -19,7 +19,7 @@ AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } -esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) { +esp_err_t AudioDecoder::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) { auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); if (source == nullptr) { return ESP_ERR_NO_MEM; @@ -36,7 +36,7 @@ esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) { return ESP_OK; } -esp_err_t AudioDecoder::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) { +esp_err_t AudioDecoder::add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) { if (this->output_transfer_buffer_ != nullptr) { this->output_transfer_buffer_->set_sink(output_ring_buffer); return ESP_OK; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 7ea7a824f96..c34ebbc613b 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -5,9 +5,9 @@ #include "audio.h" #include "audio_transfer_buffer.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" @@ -70,12 +70,12 @@ class AudioDecoder { /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. /// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated - esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer); + esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer); /// @brief Adds a sink ring buffer for decoded audio. Takes ownership of the ring buffer in a shared_ptr. /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated - esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer); + esp_err_t add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer); #ifdef USE_SPEAKER /// @brief Adds a sink speaker for decoded audio. diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 500f20533cd..4678ed548c7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -54,7 +54,7 @@ enum HttpStatus { AudioReader::~AudioReader() { this->cleanup_connection_(); } -esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) { +esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) { if (current_audio_file_ != nullptr) { // A transfer buffer isn't ncessary for a local file this->file_ring_buffer_ = output_ring_buffer.lock(); diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 61f187d151d..b1f76172b01 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -5,7 +5,7 @@ #include "audio.h" #include "audio_transfer_buffer.h" -#include "esphome/core/ring_buffer.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esp_err.h" @@ -35,7 +35,7 @@ class AudioReader { /// @brief Adds a sink ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership /// @return ESP_OK if successful, ESP_ERR_INVALID_STATE otherwise - esp_err_t add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer); + esp_err_t add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer); /// @brief Starts reading an audio file from an http source. The transfer buffer is allocated here. /// @param uri Web url to the http file. @@ -60,7 +60,7 @@ class AudioReader { AudioReaderState file_read_(); AudioReaderState http_read_(); - std::shared_ptr<RingBuffer> file_ring_buffer_; + std::shared_ptr<ring_buffer::RingBuffer> file_ring_buffer_; std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_; void cleanup_connection_(); diff --git a/esphome/components/audio/audio_resampler.cpp b/esphome/components/audio/audio_resampler.cpp index ac1039971e6..c04cc881f5b 100644 --- a/esphome/components/audio/audio_resampler.cpp +++ b/esphome/components/audio/audio_resampler.cpp @@ -16,7 +16,7 @@ AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_si this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } -esp_err_t AudioResampler::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) { +esp_err_t AudioResampler::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) { if (this->input_transfer_buffer_ != nullptr) { this->input_transfer_buffer_->set_source(input_ring_buffer); return ESP_OK; @@ -24,7 +24,7 @@ esp_err_t AudioResampler::add_source(std::weak_ptr<RingBuffer> &input_ring_buffe return ESP_ERR_NO_MEM; } -esp_err_t AudioResampler::add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer) { +esp_err_t AudioResampler::add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) { if (this->output_transfer_buffer_ != nullptr) { this->output_transfer_buffer_->set_sink(output_ring_buffer); return ESP_OK; diff --git a/esphome/components/audio/audio_resampler.h b/esphome/components/audio/audio_resampler.h index e7503d1de07..575ad13692c 100644 --- a/esphome/components/audio/audio_resampler.h +++ b/esphome/components/audio/audio_resampler.h @@ -5,9 +5,9 @@ #include "audio.h" #include "audio_transfer_buffer.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" @@ -40,12 +40,12 @@ class AudioResampler { /// @brief Adds a source ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr. /// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated - esp_err_t add_source(std::weak_ptr<RingBuffer> &input_ring_buffer); + esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer); /// @brief Adds a sink ring buffer for resampled audio. Takes ownership of the ring buffer in a shared_ptr. /// @param output_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership /// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated - esp_err_t add_sink(std::weak_ptr<RingBuffer> &output_ring_buffer); + esp_err_t add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer); #ifdef USE_SPEAKER /// @brief Adds a sink speaker for decoded audio. diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 7aa830fafa1..68151bf4e21 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -1,8 +1,8 @@ #pragma once #ifdef USE_ESP32 +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/core/defines.h" -#include "esphome/core/ring_buffer.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" @@ -76,7 +76,7 @@ class AudioTransferBuffer { void deallocate_buffer_(); // A possible source or sink for the transfer buffer - std::shared_ptr<RingBuffer> ring_buffer_; + std::shared_ptr<ring_buffer::RingBuffer> ring_buffer_; uint8_t *buffer_{nullptr}; uint8_t *data_start_{nullptr}; @@ -105,7 +105,7 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { /// @brief Adds a ring buffer as the transfer buffer's sink. /// @param ring_buffer weak_ptr to the allocated ring buffer - void set_sink(const std::weak_ptr<RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); } + void set_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); } #ifdef USE_SPEAKER /// @brief Adds a speaker as the transfer buffer's sink. @@ -179,7 +179,9 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadab /// @brief Adds a ring buffer as the transfer buffer's source. /// @param ring_buffer weak_ptr to the allocated ring buffer - void set_source(const std::weak_ptr<RingBuffer> &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }; + void set_source(const std::weak_ptr<ring_buffer::RingBuffer> &ring_buffer) { + this->ring_buffer_ = ring_buffer.lock(); + }; // AudioReadableBuffer interface const uint8_t *data() const override { return this->data_start_; } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 58d17ea6c43..a71b7db3baf 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -195,7 +195,7 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t size_t bytes_written = 0; if (this->state_ == speaker::STATE_RUNNING) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); if (temp_ring_buffer != nullptr) { // The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); @@ -207,7 +207,7 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t bool I2SAudioSpeakerBase::has_buffered_data() const { if (this->audio_ring_buffer_.use_count() > 0) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); return temp_ring_buffer->available() > 0; } return false; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index d9a228ef2c6..c598ca1bf87 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -9,12 +9,12 @@ #include <freertos/FreeRTOS.h> #include "esphome/components/audio/audio.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" #include "esphome/core/gpio.h" #include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" namespace esphome::i2s_audio { @@ -143,7 +143,7 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public QueueHandle_t i2s_event_queue_{nullptr}; - std::weak_ptr<RingBuffer> audio_ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> audio_ring_buffer_; uint32_t buffer_duration_ms_; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index edb316e3a25..51f2b225e20 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -54,7 +54,7 @@ void I2SAudioSpeaker::run_speaker_task() { audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); if (transfer_buffer != nullptr) { - std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); if (temp_ring_buffer.use_count() == 1) { transfer_buffer->set_source(temp_ring_buffer); this->audio_ring_buffer_ = temp_ring_buffer; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 22d2098de0d..38926fce997 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -30,6 +30,7 @@ from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["ring_buffer"] CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 1568fc6373b..c031a9f269d 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -106,7 +106,7 @@ void MicroWakeWord::setup() { if (this->state_ == State::STOPPED) { return; } - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (this->ring_buffer_.use_count() > 1) { size_t bytes_free = temp_ring_buffer->free(); @@ -156,7 +156,7 @@ void MicroWakeWord::inference_task(void *params) { if (!(xEventGroupGetBits(this_mww->event_group_) & ERROR_BITS)) { // Allocate ring buffer - std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create( + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( this_mww->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); if (temp_ring_buffer.use_count() == 0) { xEventGroupSetBits(this_mww->event_group_, EventGroupBits::ERROR_MEMORY); diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index 79a1226fba6..5c0c056ac04 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -6,11 +6,11 @@ #include "streaming_model.h" #include "esphome/components/microphone/microphone_source.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/ring_buffer.h" #ifdef USE_OTA_STATE_LISTENER #include "esphome/components/ota/ota_backend.h" @@ -80,7 +80,7 @@ class MicroWakeWord : public Component Trigger<std::string> wake_word_detected_trigger_; State state_{State::STOPPED}; - std::weak_ptr<RingBuffer> ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; std::vector<WakeWordModel *> wake_word_models_; #ifdef USE_MICRO_WAKE_WORD_VAD diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 0d16bce330b..8dea4560c24 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -226,7 +226,7 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_ this->start(); } size_t bytes_written = 0; - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (temp_ring_buffer.use_count() > 0) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); @@ -263,9 +263,9 @@ esp_err_t SourceSpeaker::start_() { return ESP_ERR_NO_MEM; } - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (!temp_ring_buffer) { - temp_ring_buffer = RingBuffer::create(ring_buffer_size); + temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); this->ring_buffer_ = temp_ring_buffer; } diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 29876ea262f..f392e83081a 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -4,6 +4,7 @@ #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_transfer_buffer.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" @@ -106,7 +107,7 @@ class SourceSpeaker : public speaker::Speaker, public Component { MixerSpeaker *parent_; std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer_; - std::weak_ptr<RingBuffer> ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; uint32_t buffer_duration_ms_; uint32_t last_seen_data_ms_{0}; diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 9f69a7b50b0..ecbd445a806 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -226,7 +226,7 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) { bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); } else { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (temp_ring_buffer) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); @@ -286,7 +286,7 @@ void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits:: bool ResamplerSpeaker::has_buffered_data() const { bool has_ring_buffer_data = false; if (this->requires_resampling_()) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (temp_ring_buffer) { has_ring_buffer_data = (temp_ring_buffer->available() > 0); } @@ -323,8 +323,8 @@ void ResamplerSpeaker::resample_task(void *params) { this_resampler->taps_, this_resampler->filters_); if (err == ESP_OK) { - std::shared_ptr<RingBuffer> temp_ring_buffer = - RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( + this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); if (!temp_ring_buffer) { err = ESP_ERR_NO_MEM; diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 36f39fda97f..4a091e298a6 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -4,6 +4,7 @@ #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_transfer_buffer.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/component.h" @@ -75,7 +76,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker { EventGroupHandle_t event_group_{nullptr}; - std::weak_ptr<RingBuffer> ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; speaker::Speaker *output_speaker_{nullptr}; diff --git a/esphome/components/ring_buffer/__init__.py b/esphome/components/ring_buffer/__init__.py new file mode 100644 index 00000000000..b53476dcacb --- /dev/null +++ b/esphome/components/ring_buffer/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["esp32"] + +ring_buffer_ns = cg.esphome_ns.namespace("ring_buffer") +RingBuffer = ring_buffer_ns.class_("RingBuffer") diff --git a/esphome/core/ring_buffer.cpp b/esphome/components/ring_buffer/ring_buffer.cpp similarity index 97% rename from esphome/core/ring_buffer.cpp rename to esphome/components/ring_buffer/ring_buffer.cpp index 486cf67f25d..9604290cf03 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/components/ring_buffer/ring_buffer.cpp @@ -5,7 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { +namespace esphome::ring_buffer { static const char *const TAG = "ring_buffer"; @@ -135,6 +135,6 @@ bool RingBuffer::discard_bytes_(size_t discard_bytes) { return (bytes_read == discard_bytes); } -} // namespace esphome +} // namespace esphome::ring_buffer -#endif +#endif // USE_ESP32 diff --git a/esphome/components/ring_buffer/ring_buffer.h b/esphome/components/ring_buffer/ring_buffer.h new file mode 100644 index 00000000000..62094899d78 --- /dev/null +++ b/esphome/components/ring_buffer/ring_buffer.h @@ -0,0 +1,126 @@ +#pragma once + +#ifdef USE_ESP32 + +#include <freertos/FreeRTOS.h> +#include <freertos/ringbuf.h> + +#include <cinttypes> +#include <memory> + +namespace esphome::ring_buffer { + +class RingBuffer { + public: + ~RingBuffer(); + + /** + * @brief Reads from the ring buffer, waiting up to a specified number of ticks if necessary. + * + * Available bytes are read into the provided data pointer. If not enough bytes are available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before reading what is available. + * + * @param data Pointer to copy read data into + * @param len Number of bytes to read + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes read + */ + size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); + + /** + * @brief Acquires a pointer into the ring buffer's internal storage without copying. + * + * The returned pointer is valid until receive_release() is called. Only one item + * may be checked out at a time. + * + * @param[out] length Set to the number of bytes actually acquired (may be less than max_length at wrap boundary) + * @param max_length Maximum number of bytes to acquire + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Pointer into the ring buffer's internal storage, or nullptr if no data is available + */ + void *receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait = 0); + + /** + * @brief Releases a previously acquired ring buffer item. + * + * Must be called exactly once for each successful receive_acquire(). + * + * @param item Pointer returned by receive_acquire() + */ + void receive_release(void *item); + + /** + * @brief Writes to the ring buffer, overwriting oldest data if necessary. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will overwrite the oldest data in the ring buffer. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @return Number of bytes written + */ + size_t write(const void *data, size_t len); + + /** + * @brief Writes to the ring buffer without overwriting oldest data. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before writing as much as possible. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes written + */ + size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0, + bool write_partial = true); + + /** + * @brief Returns the number of available bytes in the ring buffer. + * + * This function provides the number of bytes that can be read from the ring buffer + * without blocking the calling FreeRTOS task. + * + * @return Number of available bytes + */ + size_t available() const; + + /** + * @brief Returns the number of free bytes in the ring buffer. + * + * This function provides the number of bytes that can be written to the ring buffer + * without overwriting data or blocking the calling FreeRTOS task. + * + * @return Number of free bytes + */ + size_t free() const; + + /** + * @brief Resets the ring buffer, discarding all stored data. + * + * @return pdPASS if successful, pdFAIL otherwise + */ + BaseType_t reset(); + + enum class MemoryPreference { + EXTERNAL_FIRST, // External RAM preferred, fall back to internal (default) + INTERNAL_FIRST, // Internal RAM preferred, fall back to external + }; + + static std::unique_ptr<RingBuffer> create(size_t len, MemoryPreference preference = MemoryPreference::EXTERNAL_FIRST); + + protected: + /// @brief Discards data from the ring buffer. + /// @param discard_bytes amount of bytes to discard + /// @return True if all bytes were successfully discarded, false otherwise + bool discard_bytes_(size_t discard_bytes); + + RingbufHandle_t handle_{nullptr}; + StaticRingbuffer_t structure_; + uint8_t *storage_{nullptr}; + size_t size_{0}; +}; + +} // namespace esphome::ring_buffer + +#endif // USE_ESP32 diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index 99533dbdd55..fb8bfd30856 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -29,7 +29,7 @@ void SoundLevelComponent::dump_config() { void SoundLevelComponent::setup() { this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (this->ring_buffer_.use_count() == 2) { // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write temp_ring_buffer->write((void *) data.data(), data.size()); @@ -172,8 +172,8 @@ bool SoundLevelComponent::start_() { // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation - std::shared_ptr<RingBuffer> temp_ring_buffer = - RingBuffer::create(this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( + this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); if (temp_ring_buffer.use_count() == 0) { this->status_momentary_error("ring_buffer", 15000); this->stop_(); diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 0e46a203e86..4f0081a5104 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -4,11 +4,11 @@ #include "esphome/components/audio/audio_transfer_buffer.h" #include "esphome/components/microphone/microphone_source.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/ring_buffer.h" namespace esphome::sound_level { @@ -49,7 +49,7 @@ class SoundLevelComponent : public Component { sensor::Sensor *rms_sensor_{nullptr}; std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer_; - std::weak_ptr<RingBuffer> ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; int32_t squared_peak_{0}; uint64_t squared_samples_sum_{0}; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 892d4f4112f..010f0c50b33 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -315,10 +315,10 @@ void AudioPipeline::read_task(void *params) { if (err == ESP_OK) { size_t file_ring_buffer_size = this_pipeline->buffer_size_; - std::shared_ptr<RingBuffer> temp_ring_buffer; + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer; if (!this_pipeline->raw_file_ring_buffer_.use_count()) { - temp_ring_buffer = RingBuffer::create(file_ring_buffer_size); + temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size); this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer; } @@ -502,7 +502,7 @@ void AudioPipeline::decode_task(void *params) { if (!started_playback && has_stream_info) { // Verify enough data is available before starting playback - std::shared_ptr<RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); if (temp_ring_buffer != nullptr && temp_ring_buffer->available() >= initial_bytes_to_buffer) { started_playback = true; } diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index ef7f75dd38c..89f4707ab3c 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -5,9 +5,9 @@ #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_reader.h" #include "esphome/components/audio/audio_decoder.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/speaker/speaker.h" -#include "esphome/core/ring_buffer.h" #include "esphome/core/static_task.h" #include "esp_err.h" @@ -129,7 +129,7 @@ class AudioPipeline { size_t buffer_size_; // Ring buffer between reader and decoder size_t transfer_buffer_size_; // Internal source/sink buffers for the audio reader and decoder - std::weak_ptr<RingBuffer> raw_file_ring_buffer_; + std::weak_ptr<ring_buffer::RingBuffer> raw_file_ring_buffer_; // Handles basic control/state of the three tasks EventGroupHandle_t event_group_{nullptr}; diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d970df2a44b..9387797ba2e 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_SPEAKER, ) -AUTO_LOAD = ["socket"] +AUTO_LOAD = ["ring_buffer", "socket"] DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz", "@kahrendt"] diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index f2244a8ff43..bff0026b242 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -30,7 +30,7 @@ VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; } void VoiceAssistant::setup() { this->mic_source_->add_data_callback([this](const std::vector<uint8_t> &data) { - std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_; + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_; if (this->ring_buffer_.use_count() > 1) { temp_ring_buffer->write((void *) data.data(), data.size()); } @@ -116,7 +116,7 @@ bool VoiceAssistant::allocate_buffers_() { #endif if (this->ring_buffer_.use_count() == 0) { - this->ring_buffer_ = RingBuffer::create(RING_BUFFER_SIZE); + this->ring_buffer_ = ring_buffer::RingBuffer::create(RING_BUFFER_SIZE); if (this->ring_buffer_.use_count() == 0) { ESP_LOGE(TAG, "Could not allocate ring buffer"); return false; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index eb23dcb5e0d..faef09d8bd0 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -7,9 +7,9 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/ring_buffer.h" #include "esphome/components/api/api_connection.h" +#include "esphome/components/ring_buffer/ring_buffer.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/microphone/microphone_source.h" #ifdef USE_MEDIA_PLAYER @@ -300,7 +300,7 @@ class VoiceAssistant : public Component { std::string wake_word_{""}; - std::shared_ptr<RingBuffer> ring_buffer_; + std::shared_ptr<ring_buffer::RingBuffer> ring_buffer_; bool use_wake_word_; uint8_t noise_suppression_level_; diff --git a/esphome/core/config.py b/esphome/core/config.py index fe55c0fe25d..5a98b947819 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -759,10 +759,6 @@ async def to_code(config: ConfigType) -> None: # Platform-specific source files for core FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "ring_buffer.cpp": { - PlatformFramework.ESP32_ARDUINO, - PlatformFramework.ESP32_IDF, - }, "static_task.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 8ac3ff38111..fbb089e9066 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -2,124 +2,20 @@ #ifdef USE_ESP32 -#include <freertos/FreeRTOS.h> -#include <freertos/ringbuf.h> - -#include <cinttypes> -#include <memory> +// Deprecated: include "esphome/components/ring_buffer/ring_buffer.h" and use +// esphome::ring_buffer::RingBuffer. This shim will be removed in 2026.11.0. +#if __has_include("esphome/components/ring_buffer/ring_buffer.h") +#include "esphome/components/ring_buffer/ring_buffer.h" +#else +#error \ + "esphome/components/ring_buffer/ring_buffer.h not found. Add 'ring_buffer' to your component's AUTO_LOAD list to use esphome::ring_buffer::RingBuffer." +#endif +#include "esphome/core/helpers.h" // for ESPDEPRECATED namespace esphome { -class RingBuffer { - public: - ~RingBuffer(); - - /** - * @brief Reads from the ring buffer, waiting up to a specified number of ticks if necessary. - * - * Available bytes are read into the provided data pointer. If not enough bytes are available, - * the function will wait up to `ticks_to_wait` FreeRTOS ticks before reading what is available. - * - * @param data Pointer to copy read data into - * @param len Number of bytes to read - * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) - * @return Number of bytes read - */ - size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); - - /** - * @brief Acquires a pointer into the ring buffer's internal storage without copying. - * - * The returned pointer is valid until receive_release() is called. Only one item - * may be checked out at a time. - * - * @param[out] length Set to the number of bytes actually acquired (may be less than max_length at wrap boundary) - * @param max_length Maximum number of bytes to acquire - * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) - * @return Pointer into the ring buffer's internal storage, or nullptr if no data is available - */ - void *receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait = 0); - - /** - * @brief Releases a previously acquired ring buffer item. - * - * Must be called exactly once for each successful receive_acquire(). - * - * @param item Pointer returned by receive_acquire() - */ - void receive_release(void *item); - - /** - * @brief Writes to the ring buffer, overwriting oldest data if necessary. - * - * The provided data is written to the ring buffer. If not enough space is available, - * the function will overwrite the oldest data in the ring buffer. - * - * @param data Pointer to data for writing - * @param len Number of bytes to write - * @return Number of bytes written - */ - size_t write(const void *data, size_t len); - - /** - * @brief Writes to the ring buffer without overwriting oldest data. - * - * The provided data is written to the ring buffer. If not enough space is available, - * the function will wait up to `ticks_to_wait` FreeRTOS ticks before writing as much as possible. - * - * @param data Pointer to data for writing - * @param len Number of bytes to write - * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) - * @return Number of bytes written - */ - size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0, - bool write_partial = true); - - /** - * @brief Returns the number of available bytes in the ring buffer. - * - * This function provides the number of bytes that can be read from the ring buffer - * without blocking the calling FreeRTOS task. - * - * @return Number of available bytes - */ - size_t available() const; - - /** - * @brief Returns the number of free bytes in the ring buffer. - * - * This function provides the number of bytes that can be written to the ring buffer - * without overwriting data or blocking the calling FreeRTOS task. - * - * @return Number of free bytes - */ - size_t free() const; - - /** - * @brief Resets the ring buffer, discarding all stored data. - * - * @return pdPASS if successful, pdFAIL otherwise - */ - BaseType_t reset(); - - enum class MemoryPreference { - EXTERNAL_FIRST, // External RAM preferred, fall back to internal (default) - INTERNAL_FIRST, // Internal RAM preferred, fall back to external - }; - - static std::unique_ptr<RingBuffer> create(size_t len, MemoryPreference preference = MemoryPreference::EXTERNAL_FIRST); - - protected: - /// @brief Discards data from the ring buffer. - /// @param discard_bytes amount of bytes to discard - /// @return True if all bytes were successfully discarded, false otherwise - bool discard_bytes_(size_t discard_bytes); - - RingbufHandle_t handle_{nullptr}; - StaticRingbuffer_t structure_; - uint8_t *storage_{nullptr}; - size_t size_{0}; -}; +using RingBuffer ESPDEPRECATED("Use esphome::ring_buffer::RingBuffer instead. Removed in 2026.11.0.", + "2026.5.0") = ring_buffer::RingBuffer; } // namespace esphome diff --git a/esphome/writer.py b/esphome/writer.py index 816c57a0bc1..2fa43fa5eb1 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -199,7 +199,15 @@ def copy_src_tree(): # Build #include list for esphome.h # X-macro files are included multiple times with different macro definitions # and must not be included bare in esphome.h - esphome_h_exclude = {Path(ENTITY_TYPES_H_TARGET)} + # Deprecated headers that re-export from a relocated component must not be + # auto-included, since their #include of the new path only resolves when the + # new component is loaded by a consumer. + esphome_h_exclude = { + Path(ENTITY_TYPES_H_TARGET), + Path( + "esphome/core/ring_buffer.h" + ), # moved to components/ring_buffer/, removed in 2026.11.0 + } include_l = [] for target, _ in source_files_l: if target.suffix in HEADER_FILE_EXTENSIONS and target not in esphome_h_exclude: From 696a65473329a0f92df5c636e6af28bd2813b2a2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 7 May 2026 23:05:17 -0400 Subject: [PATCH 1900/2056] [clang-tidy] Concatenate nested namespaces (7/7: tests, platform-gated, enable check) (#16307) --- .clang-tidy | 1 - .clang-tidy.hash | 2 +- esphome/components/adc/adc_sensor_esp8266.cpp | 6 ++---- esphome/components/adc/adc_sensor_libretiny.cpp | 6 ++---- esphome/components/adc/adc_sensor_rp2040.cpp | 6 ++---- esphome/components/adc/adc_sensor_zephyr.cpp | 6 ++---- .../components/beken_spi_led_strip/led_strip.cpp | 6 ++---- esphome/components/beken_spi_led_strip/led_strip.h | 6 ++---- .../bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp | 6 ++---- .../components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h | 6 ++---- esphome/components/debug/debug_esp8266.cpp | 6 ++---- esphome/components/debug/debug_host.cpp | 6 ++---- esphome/components/debug/debug_libretiny.cpp | 6 ++---- esphome/components/debug/debug_rp2040.cpp | 6 ++---- .../components/deep_sleep/deep_sleep_esp8266.cpp | 6 ++---- esphome/components/esp8266_pwm/esp8266_pwm.cpp | 6 ++---- esphome/components/esp8266_pwm/esp8266_pwm.h | 6 ++---- esphome/components/esp_ldo/esp_ldo.cpp | 6 ++---- esphome/components/esp_ldo/esp_ldo.h | 6 ++---- esphome/components/host/gpio.h | 6 ++---- esphome/components/libretiny_pwm/libretiny_pwm.cpp | 6 ++---- esphome/components/libretiny_pwm/libretiny_pwm.h | 6 ++---- esphome/components/light/light_traits.h | 13 ++----------- esphome/components/lightwaverf/LwRx.cpp | 6 ++---- esphome/components/lightwaverf/LwTx.cpp | 6 ++---- esphome/components/lightwaverf/lightwaverf.cpp | 6 ++---- esphome/components/lightwaverf/lightwaverf.h | 6 ++---- esphome/components/midea/ir_transmitter.h | 6 ++---- esphome/components/mipi_dsi/mipi_dsi.cpp | 6 ++---- esphome/components/mipi_dsi/mipi_dsi.h | 6 ++---- esphome/components/mipi_rgb/mipi_rgb.cpp | 6 ++---- esphome/components/mipi_rgb/mipi_rgb.h | 6 ++---- esphome/components/neopixelbus/neopixelbus_light.h | 6 ++---- esphome/components/qspi_dbi/qspi_dbi.cpp | 6 ++---- esphome/components/qspi_dbi/qspi_dbi.h | 6 ++---- esphome/components/rp2040/core.h | 4 +--- esphome/components/rp2040/gpio.h | 6 ++---- .../components/rp2040_pio_led_strip/led_strip.cpp | 6 ++---- esphome/components/rp2040_pio_led_strip/led_strip.h | 6 ++---- esphome/components/rp2040_pwm/rp2040_pwm.cpp | 6 ++---- esphome/components/rp2040_pwm/rp2040_pwm.h | 6 ++---- esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 6 ++---- esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h | 6 ++---- esphome/components/sdl/sdl_esphome.cpp | 6 ++---- esphome/components/sdl/sdl_esphome.h | 6 ++---- .../components/sdl/touchscreen/sdl_touchscreen.h | 6 ++---- esphome/components/shelly_dimmer/dev_table.h | 6 ++---- esphome/components/shelly_dimmer/shelly_dimmer.cpp | 6 ++---- esphome/components/shelly_dimmer/shelly_dimmer.h | 6 ++---- esphome/components/shelly_dimmer/stm32flash.cpp | 12 ++---------- esphome/components/shelly_dimmer/stm32flash.h | 6 ++---- esphome/components/st7701s/st7701s.cpp | 6 ++---- esphome/components/st7701s/st7701s.h | 6 ++---- .../crc8_test_component/crc8_test_component.cpp | 6 ++---- .../crc8_test_component/crc8_test_component.h | 6 ++---- .../custom_api_device_component.cpp | 6 ++---- .../custom_api_device_component.h | 6 ++---- .../defer_stress_component.cpp | 6 ++---- .../defer_stress_component/defer_stress_component.h | 6 ++---- .../loop_test_component/loop_test_component.cpp | 6 ++---- .../loop_test_component/loop_test_component.h | 6 ++---- .../loop_test_component/loop_test_isr_component.cpp | 6 ++---- .../loop_test_component/loop_test_isr_component.h | 6 ++---- .../scheduler_bulk_cleanup_component.cpp | 6 ++---- .../scheduler_bulk_cleanup_component.h | 6 ++---- .../heap_scheduler_stress_component.cpp | 6 ++---- .../heap_scheduler_stress_component.h | 6 ++---- .../rapid_cancellation_component.cpp | 6 ++---- .../rapid_cancellation_component.h | 6 ++---- .../recursive_timeout_component.cpp | 6 ++---- .../recursive_timeout_component.h | 6 ++---- .../simultaneous_callbacks_component.cpp | 6 ++---- .../simultaneous_callbacks_component.h | 6 ++---- .../string_lifetime_component.cpp | 6 ++---- .../string_lifetime_component.h | 6 ++---- .../string_name_stress_component.cpp | 6 ++---- .../string_name_stress_component.h | 6 ++---- 77 files changed, 150 insertions(+), 314 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 4e128a19455..9aeeb1fc26c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -96,7 +96,6 @@ Checks: >- -modernize-avoid-variadic-functions, -modernize-avoid-c-arrays, -modernize-avoid-c-style-cast, - -modernize-concat-nested-namespaces, -modernize-macro-to-enum, -modernize-return-braced-init-list, -modernize-type-traits, diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 582e0c1eaa2..2f973e34d9f 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8 +c1e4375738304baabf7e915e5f7ca50fb22ec9b4a2d8312827a32a659f3fa40c diff --git a/esphome/components/adc/adc_sensor_esp8266.cpp b/esphome/components/adc/adc_sensor_esp8266.cpp index be14b252d43..e4f2f82f085 100644 --- a/esphome/components/adc/adc_sensor_esp8266.cpp +++ b/esphome/components/adc/adc_sensor_esp8266.cpp @@ -11,8 +11,7 @@ ADC_MODE(ADC_VCC) #include <Arduino.h> #endif // USE_ADC_SENSOR_VCC -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.esp8266"; @@ -55,7 +54,6 @@ float ADCSensor::sample() { return aggr.aggregate() / 1024.0f; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc #endif // USE_ESP8266 diff --git a/esphome/components/adc/adc_sensor_libretiny.cpp b/esphome/components/adc/adc_sensor_libretiny.cpp index 0b1393c2e70..d9b9f50be1f 100644 --- a/esphome/components/adc/adc_sensor_libretiny.cpp +++ b/esphome/components/adc/adc_sensor_libretiny.cpp @@ -3,8 +3,7 @@ #include "adc_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.libretiny"; @@ -48,7 +47,6 @@ float ADCSensor::sample() { return aggr.aggregate() / 1000.0f; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc #endif // USE_LIBRETINY diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index a79707e2347..8d41edb8145 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -15,8 +15,7 @@ #define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage) #endif -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.rp2040"; @@ -98,7 +97,6 @@ float ADCSensor::sample() { return aggr.aggregate() * 3.3f / 4096.0f * coeff; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc #endif // USE_RP2040 diff --git a/esphome/components/adc/adc_sensor_zephyr.cpp b/esphome/components/adc/adc_sensor_zephyr.cpp index 2fb9d4b0e50..c3632b00e20 100644 --- a/esphome/components/adc/adc_sensor_zephyr.cpp +++ b/esphome/components/adc/adc_sensor_zephyr.cpp @@ -5,8 +5,7 @@ #include "hal/nrf_saadc.h" -namespace esphome { -namespace adc { +namespace esphome::adc { static const char *const TAG = "adc.zephyr"; @@ -202,6 +201,5 @@ float ADCSensor::sample() { return val_mv / 1000.0f; } -} // namespace adc -} // namespace esphome +} // namespace esphome::adc #endif diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index f425f3ca5c4..4e22489844b 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -33,8 +33,7 @@ static const uint32_t CTRL_NSSMD_3 = 1 << 17; static const uint32_t SPI_TX_FINISH_EN = 1 << 2; static const uint32_t SPI_RX_FINISH_EN = 1 << 3; -namespace esphome { -namespace beken_spi_led_strip { +namespace esphome::beken_spi_led_strip { static const char *const TAG = "beken_spi_led_strip"; @@ -382,7 +381,6 @@ void BekenSPILEDStripLightOutput::dump_config() { float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace beken_spi_led_strip -} // namespace esphome +} // namespace esphome::beken_spi_led_strip #endif // USE_BK72XX diff --git a/esphome/components/beken_spi_led_strip/led_strip.h b/esphome/components/beken_spi_led_strip/led_strip.h index 705f9102a99..4ed640a3bc4 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.h +++ b/esphome/components/beken_spi_led_strip/led_strip.h @@ -8,8 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace beken_spi_led_strip { +namespace esphome::beken_spi_led_strip { enum RGBOrder : uint8_t { ORDER_RGB, @@ -79,7 +78,6 @@ class BekenSPILEDStripLightOutput : public light::AddressableLight { optional<uint32_t> max_refresh_rate_{}; }; -} // namespace beken_spi_led_strip -} // namespace esphome +} // namespace esphome::beken_spi_led_strip #endif // USE_BK72XX diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp index 2d74ba6b122..2a2c5e2bcc8 100644 --- a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp @@ -9,8 +9,7 @@ #include <cinttypes> -namespace esphome { -namespace bme68x_bsec2_i2c { +namespace esphome::bme68x_bsec2_i2c { static const char *const TAG = "bme68x_bsec2_i2c.sensor"; @@ -53,6 +52,5 @@ void BME68xBSEC2I2CComponent::delay_us(uint32_t period, void *intfPtr) { delayMicroseconds(period); } -} // namespace bme68x_bsec2_i2c -} // namespace esphome +} // namespace esphome::bme68x_bsec2_i2c #endif diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h index a21a123f7b9..6d20b613903 100644 --- a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h @@ -9,8 +9,7 @@ #include "esphome/components/bme68x_bsec2/bme68x_bsec2.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bme68x_bsec2_i2c { +namespace esphome::bme68x_bsec2_i2c { class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, public i2c::I2CDevice { void setup() override; @@ -23,6 +22,5 @@ class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, publi static void delay_us(uint32_t period, void *intfPtr); }; -} // namespace bme68x_bsec2_i2c -} // namespace esphome +} // namespace esphome::bme68x_bsec2_i2c #endif diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 0519ab72fe9..272123dfc0e 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -15,8 +15,7 @@ extern uint32_t core_version; extern const char *core_release; } -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; @@ -170,6 +169,5 @@ void DebugComponent::update_platform_() { #endif } -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp index 0dfab86e4c4..298a7e44e70 100644 --- a/esphome/components/debug/debug_host.cpp +++ b/esphome/components/debug/debug_host.cpp @@ -2,8 +2,7 @@ #ifdef USE_HOST #include <climits> -namespace esphome { -namespace debug { +namespace esphome::debug { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; } @@ -15,6 +14,5 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 6f36debb95a..1cc04dcbd8e 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -2,8 +2,7 @@ #ifdef USE_LIBRETINY #include "esphome/core/log.h" -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; @@ -62,6 +61,5 @@ void DebugComponent::update_platform_() { #endif } -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index 73f08492c86..adc23dbf51f 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -12,8 +12,7 @@ #ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" #endif -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; @@ -84,6 +83,5 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index 42c153c2f38..9239a7fb311 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -3,8 +3,7 @@ #include <Esp.h> -namespace esphome { -namespace deep_sleep { +namespace esphome::deep_sleep { static const char *const TAG = "deep_sleep"; @@ -20,6 +19,5 @@ void DeepSleepComponent::deep_sleep_() { bool DeepSleepComponent::should_teardown_() { return true; } -} // namespace deep_sleep -} // namespace esphome +} // namespace esphome::deep_sleep #endif diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index cc6bfbc8a88..b5b3d5073aa 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -8,8 +8,7 @@ #include <core_esp8266_waveform.h> -namespace esphome { -namespace esp8266_pwm { +namespace esphome::esp8266_pwm { static const char *const TAG = "esp8266_pwm"; @@ -53,7 +52,6 @@ void HOT ESP8266PWM::write_state(float state) { } } -} // namespace esp8266_pwm -} // namespace esphome +} // namespace esphome::esp8266_pwm #endif diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 4b021fc4622..51c4ea16028 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -7,8 +7,7 @@ #include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace esp8266_pwm { +namespace esphome::esp8266_pwm { class ESP8266PWM : public output::FloatOutput, public Component { public: @@ -48,7 +47,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { ESP8266PWM *parent_; }; -} // namespace esp8266_pwm -} // namespace esphome +} // namespace esphome::esp8266_pwm #endif diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index f8ebec19035..b3c9a59865d 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -3,8 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace esp_ldo { +namespace esphome::esp_ldo { static const char *const TAG = "esp_ldo"; void EspLdo::setup() { @@ -41,7 +40,6 @@ void EspLdo::adjust_voltage(float voltage) { } } -} // namespace esp_ldo -} // namespace esphome +} // namespace esphome::esp_ldo #endif // USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index 1a20f1d08ad..bb1579e83dd 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esp_ldo_regulator.h" -namespace esphome { -namespace esp_ldo { +namespace esphome::esp_ldo { class EspLdo : public Component { public: @@ -40,7 +39,6 @@ template<typename... Ts> class AdjustAction : public Action<Ts...> { EspLdo *ldo_; }; -} // namespace esp_ldo -} // namespace esphome +} // namespace esphome::esp_ldo #endif // USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index ea6b13f436e..6f2bccf1021 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -4,8 +4,7 @@ #include "esphome/core/hal.h" -namespace esphome { -namespace host { +namespace esphome::host { class HostGPIOPin : public InternalGPIOPin { public: @@ -32,7 +31,6 @@ class HostGPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace host -} // namespace esphome +} // namespace esphome::host #endif // USE_HOST diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.cpp b/esphome/components/libretiny_pwm/libretiny_pwm.cpp index 4e4a16d761f..eea593a39da 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.cpp +++ b/esphome/components/libretiny_pwm/libretiny_pwm.cpp @@ -3,8 +3,7 @@ #ifdef USE_LIBRETINY -namespace esphome { -namespace libretiny_pwm { +namespace esphome::libretiny_pwm { static const char *const TAG = "libretiny.pwm"; @@ -49,7 +48,6 @@ void LibreTinyPWM::update_frequency(float frequency) { this->write_state(this->duty_); } -} // namespace libretiny_pwm -} // namespace esphome +} // namespace esphome::libretiny_pwm #endif diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.h b/esphome/components/libretiny_pwm/libretiny_pwm.h index f9117090548..f7737be386b 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.h +++ b/esphome/components/libretiny_pwm/libretiny_pwm.h @@ -7,8 +7,7 @@ #ifdef USE_LIBRETINY -namespace esphome { -namespace libretiny_pwm { +namespace esphome::libretiny_pwm { class LibreTinyPWM : public output::FloatOutput, public Component { public: @@ -49,7 +48,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { LibreTinyPWM *parent_; }; -} // namespace libretiny_pwm -} // namespace esphome +} // namespace esphome::libretiny_pwm #endif diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index c3bb27a9647..a2a6b0a916b 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -3,15 +3,7 @@ #include "color_mode.h" #include "esphome/core/helpers.h" -namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - -namespace light { +namespace esphome::light { /// This class is used to represent the capabilities of a light. class LightTraits { @@ -43,5 +35,4 @@ class LightTraits { ColorModeMask supported_color_modes_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp index 97104578508..dfb8edfaf7a 100644 --- a/esphome/components/lightwaverf/LwRx.cpp +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -10,8 +10,7 @@ #include <cstring> #include "esphome/core/helpers.h" -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { /** Pin change interrupt routine that identifies 1 and 0 LightwaveRF bits @@ -430,6 +429,5 @@ void LwRx::rx_remove_pair_(uint8_t *buf) { } } -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf #endif diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index 8852935bfd8..339980d6ee6 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -11,8 +11,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { static const uint8_t TX_NIBBLE[] = {0xF6, 0xEE, 0xED, 0xEB, 0xDE, 0xDD, 0xDB, 0xBE, 0xBD, 0xBB, 0xB7, 0x7E, 0x7D, 0x7B, 0x77, 0x6F}; @@ -208,6 +207,5 @@ void LwTx::lw_timer_stop() { } } -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf #endif diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp index 2c6a1ecf5b5..b8b6a9697ea 100644 --- a/esphome/components/lightwaverf/lightwaverf.cpp +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -5,8 +5,7 @@ #include "lightwaverf.h" -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { static const char *const TAG = "lightwaverf.sensor"; @@ -62,7 +61,6 @@ void LightWaveRF::dump_config() { LOG_PIN(" Pin RX: ", this->pin_rx_); LOG_UPDATE_INTERVAL(this); } -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf #endif diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index 6210e6b5d47..224da6315f4 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -11,8 +11,7 @@ #include "LwRx.h" #include "LwTx.h" -namespace esphome { -namespace lightwaverf { +namespace esphome::lightwaverf { #ifdef USE_ESP8266 @@ -61,6 +60,5 @@ template<typename... Ts> class SendRawAction : public Action<Ts...> { }; #endif -} // namespace lightwaverf -} // namespace esphome +} // namespace esphome::lightwaverf #endif diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index a16aed2e722..f11682230d3 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -4,8 +4,7 @@ #ifdef USE_REMOTE_TRANSMITTER #include "esphome/components/remote_base/midea_protocol.h" -namespace esphome { -namespace midea { +namespace esphome::midea { using remote_base::RemoteTransmitterBase; using IrData = remote_base::MideaData; @@ -84,8 +83,7 @@ class IrTransmitter { RemoteTransmitterBase *transmitter_{nullptr}; }; -} // namespace midea -} // namespace esphome +} // namespace esphome::midea #endif #endif // USE_ARDUINO diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fc59aeffe89..9bd2dded2c8 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -3,8 +3,7 @@ #include "mipi_dsi.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mipi_dsi { +namespace esphome::mipi_dsi { // Maximum bytes to log for init commands (truncated if larger) static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; @@ -411,6 +410,5 @@ void MIPI_DSI::dump_config() { YESNO(this->invert_colors_), this->pclk_frequency_); LOG_PIN(" Reset Pin ", this->reset_pin_); } -} // namespace mipi_dsi -} // namespace esphome +} // namespace esphome::mipi_dsi #endif // USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index c27c9ccc6ef..82827d813e8 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -16,8 +16,7 @@ #include "esp_lcd_mipi_dsi.h" -namespace esphome { -namespace mipi_dsi { +namespace esphome::mipi_dsi { constexpr static const char *const TAG = "display.mipi_dsi"; const uint8_t SW_RESET_CMD = 0x01; @@ -113,6 +112,5 @@ class MIPI_DSI : public display::Display { uint16_t y_high_{0}; }; -} // namespace mipi_dsi -} // namespace esphome +} // namespace esphome::mipi_dsi #endif diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 6f5e2f2490c..b07460fdba9 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -8,8 +8,7 @@ #include <esp_lcd_panel_rgb.h> #include <span> -namespace esphome { -namespace mipi_rgb { +namespace esphome::mipi_rgb { static const uint8_t DELAY_FLAG = 0xFF; @@ -400,6 +399,5 @@ void MipiRgb::dump_config() { this->dump_pins_(3, 8, "Red", 0); } -} // namespace mipi_rgb -} // namespace esphome +} // namespace esphome::mipi_rgb #endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index accc251a184..4d1d8360998 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -8,8 +8,7 @@ #include "esphome/components/spi/spi.h" #endif -namespace esphome { -namespace mipi_rgb { +namespace esphome::mipi_rgb { constexpr static const char *const TAG = "display.mipi_rgb"; const uint8_t SW_RESET_CMD = 0x01; @@ -120,6 +119,5 @@ class MipiRgbSpi : public MipiRgb, }; #endif -} // namespace mipi_rgb -} // namespace esphome +} // namespace esphome::mipi_rgb #endif diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index c27244b94dd..1cbe889acf9 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -11,8 +11,7 @@ #include "NeoPixelBus.h" -namespace esphome { -namespace neopixelbus { +namespace esphome::neopixelbus { enum class ESPNeoPixelOrder { GBWR = 0b11000110, @@ -140,7 +139,6 @@ class NeoPixelRGBWLightOutput : public NeoPixelBusLightOutputBase<T_METHOD, T_CO } }; -} // namespace neopixelbus -} // namespace esphome +} // namespace esphome::neopixelbus #endif // USE_ARDUINO diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index d42f95dca37..5b4974b6317 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace qspi_dbi { +namespace esphome::qspi_dbi { // Maximum bytes to log in verbose hex output static constexpr size_t QSPI_DBI_MAX_LOG_BYTES = 64; @@ -226,6 +225,5 @@ void QspiDbi::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); } -} // namespace qspi_dbi -} // namespace esphome +} // namespace esphome::qspi_dbi #endif diff --git a/esphome/components/qspi_dbi/qspi_dbi.h b/esphome/components/qspi_dbi/qspi_dbi.h index 3eee9bec47b..fa77cc5f762 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.h +++ b/esphome/components/qspi_dbi/qspi_dbi.h @@ -11,8 +11,7 @@ #include "esp_lcd_panel_rgb.h" -namespace esphome { -namespace qspi_dbi { +namespace esphome::qspi_dbi { constexpr static const char *const TAG = "display.qspi_dbi"; static const uint8_t SW_RESET_CMD = 0x01; @@ -168,6 +167,5 @@ class QspiDbi : public display::DisplayBuffer, esp_lcd_panel_handle_t handle_{}; }; -} // namespace qspi_dbi -} // namespace esphome +} // namespace esphome::qspi_dbi #endif diff --git a/esphome/components/rp2040/core.h b/esphome/components/rp2040/core.h index 92fc4f824e6..db8937a8a36 100644 --- a/esphome/components/rp2040/core.h +++ b/esphome/components/rp2040/core.h @@ -7,8 +7,6 @@ extern "C" unsigned long ulMainGetRunTimeCounterValue(); -namespace esphome { -namespace rp2040 {} // namespace rp2040 -} // namespace esphome +namespace esphome::rp2040 {} // namespace esphome::rp2040 #endif // USE_RP2040 diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index a98e1dab140..da97cff9b1e 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -5,8 +5,7 @@ #include <Arduino.h> #include "esphome/core/hal.h" -namespace esphome { -namespace rp2040 { +namespace esphome::rp2040 { class RP2040GPIOPin : public InternalGPIOPin { public: @@ -33,7 +32,6 @@ class RP2040GPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace rp2040 -} // namespace esphome +} // namespace esphome::rp2040 #endif // USE_RP2040 diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index fdb49fb3efe..8afba6ba1d4 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -12,8 +12,7 @@ #include <pico/sem.h> #include <pico/stdlib.h> -namespace esphome { -namespace rp2040_pio_led_strip { +namespace esphome::rp2040_pio_led_strip { static const char *TAG = "rp2040_pio_led_strip"; @@ -210,7 +209,6 @@ void RP2040PIOLEDStripLightOutput::dump_config() { float RP2040PIOLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace rp2040_pio_led_strip -} // namespace esphome +} // namespace esphome::rp2040_pio_led_strip #endif diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 7b626489743..ebc3bbbaa54 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -16,8 +16,7 @@ #include <pico/sem.h> #include <map> -namespace esphome { -namespace rp2040_pio_led_strip { +namespace esphome::rp2040_pio_led_strip { enum RGBOrder : uint8_t { ORDER_RGB, @@ -127,7 +126,6 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { inline static struct semaphore dma_write_complete_sem_[12]; }; -} // namespace rp2040_pio_led_strip -} // namespace esphome +} // namespace esphome::rp2040_pio_led_strip #endif // USE_RP2040 diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index 90a507b14f7..c9b9e6739d1 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -11,8 +11,7 @@ #include <hardware/pwm.h> #include <cmath> -namespace esphome { -namespace rp2040_pwm { +namespace esphome::rp2040_pwm { static const char *const TAG = "rp2040_pwm"; @@ -60,7 +59,6 @@ void HOT RP2040PWM::write_state(float state) { pwm_set_gpio_level(this->pin_->get_pin(), state * this->wrap_); } -} // namespace rp2040_pwm -} // namespace esphome +} // namespace esphome::rp2040_pwm #endif diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index b82765b1c01..58d3955a31e 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -7,8 +7,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -namespace esphome { -namespace rp2040_pwm { +namespace esphome::rp2040_pwm { class RP2040PWM : public output::FloatOutput, public Component { public: @@ -53,7 +52,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { RP2040PWM *parent_; }; -} // namespace rp2040_pwm -} // namespace esphome +} // namespace esphome::rp2040_pwm #endif // USE_RP2040 diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index d29f6a0bcb9..00530c3f96e 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <driver/gpio.h> -namespace esphome { -namespace rpi_dpi_rgb { +namespace esphome::rpi_dpi_rgb { void RpiDpiRgb::setup() { this->reset_display_(); @@ -160,7 +159,6 @@ void RpiDpiRgb::reset_display_() const { } } -} // namespace rpi_dpi_rgb -} // namespace esphome +} // namespace esphome::rpi_dpi_rgb #endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h index 7525040cd1f..8b1457926c1 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h @@ -14,8 +14,7 @@ #include "esp_lcd_panel_rgb.h" -namespace esphome { -namespace rpi_dpi_rgb { +namespace esphome::rpi_dpi_rgb { constexpr static const char *const TAG = "rpi_dpi_rgb"; @@ -92,6 +91,5 @@ class RpiDpiRgb : public display::Display { esp_lcd_panel_handle_t handle_{}; }; -} // namespace rpi_dpi_rgb -} // namespace esphome +} // namespace esphome::rpi_dpi_rgb #endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index 74ca2ce39a5..c99b5081b3d 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -2,8 +2,7 @@ #include "sdl_esphome.h" #include "esphome/components/display/display_color_utils.h" -namespace esphome { -namespace sdl { +namespace esphome::sdl { int Sdl::get_width() { switch (this->rotation_) { @@ -162,6 +161,5 @@ void Sdl::loop() { } } -} // namespace sdl -} // namespace esphome +} // namespace esphome::sdl #endif diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index ce34cb817e0..3f54b705606 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -9,8 +9,7 @@ #include "SDL.h" #include <map> -namespace esphome { -namespace sdl { +namespace esphome::sdl { constexpr static const char *const TAG = "sdl"; @@ -66,7 +65,6 @@ class Sdl : public display::Display { uint16_t y_high_{0}; std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{}; }; -} // namespace sdl -} // namespace esphome +} // namespace esphome::sdl #endif diff --git a/esphome/components/sdl/touchscreen/sdl_touchscreen.h b/esphome/components/sdl/touchscreen/sdl_touchscreen.h index a1f0fb15e32..cf2fd650889 100644 --- a/esphome/components/sdl/touchscreen/sdl_touchscreen.h +++ b/esphome/components/sdl/touchscreen/sdl_touchscreen.h @@ -4,8 +4,7 @@ #include "../sdl_esphome.h" #include "esphome/components/touchscreen/touchscreen.h" -namespace esphome { -namespace sdl { +namespace esphome::sdl { class SdlTouchscreen : public touchscreen::Touchscreen, public Parented<Sdl> { public: @@ -21,6 +20,5 @@ class SdlTouchscreen : public touchscreen::Touchscreen, public Parented<Sdl> { } }; -} // namespace sdl -} // namespace esphome +} // namespace esphome::sdl #endif diff --git a/esphome/components/shelly_dimmer/dev_table.h b/esphome/components/shelly_dimmer/dev_table.h index e73cd1271cb..32b4810d7a2 100644 --- a/esphome/components/shelly_dimmer/dev_table.h +++ b/esphome/components/shelly_dimmer/dev_table.h @@ -23,8 +23,7 @@ #ifdef USE_SHD_FIRMWARE_DATA #include "stm32flash.h" -namespace esphome { -namespace shelly_dimmer { +namespace esphome::shelly_dimmer { constexpr uint32_t SZ_128 = 0x00000080; constexpr uint32_t SZ_256 = 0x00000100; @@ -153,7 +152,6 @@ constexpr stm32_dev_t DEVICES[] = { {0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0}, }; -} // namespace shelly_dimmer -} // namespace esphome +} // namespace esphome::shelly_dimmer #endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 230fb963b13..b0f43f0ffca 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -56,8 +56,7 @@ template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) n } // Anonymous namespace -namespace esphome { -namespace shelly_dimmer { +namespace esphome::shelly_dimmer { /// Computes a crappy checksum as defined by the Shelly Dimmer protocol. uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) { @@ -522,7 +521,6 @@ void ShellyDimmer::reset_dfu_boot_() { this->reset_(true); } -} // namespace shelly_dimmer -} // namespace esphome +} // namespace esphome::shelly_dimmer #endif // USE_ESP8266 diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h index fd75caa797d..c6d0e20afe8 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.h +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -10,8 +10,7 @@ #include <array> -namespace esphome { -namespace shelly_dimmer { +namespace esphome::shelly_dimmer { class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice { private: @@ -117,7 +116,6 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public void reset_dfu_boot_(); }; -} // namespace shelly_dimmer -} // namespace esphome +} // namespace esphome::shelly_dimmer #endif // USE_ESP8266 diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index a1a933bcab1..c758b0a3123 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -112,8 +112,7 @@ constexpr char TAG[] = "stm32flash"; } // Anonymous namespace -namespace esphome { -namespace shelly_dimmer { +namespace esphome::shelly_dimmer { namespace { @@ -487,12 +486,6 @@ template<typename T> stm32_unique_ptr make_stm32_with_deletor(T ptr) { } // Anonymous namespace -} // namespace shelly_dimmer -} // namespace esphome - -namespace esphome { -namespace shelly_dimmer { - /* find newer command by higher code */ #define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) @@ -1059,7 +1052,6 @@ stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uin return STM32_ERR_OK; } -} // namespace shelly_dimmer -} // namespace esphome +} // namespace esphome::shelly_dimmer #endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h index d973b352221..9a81f07373e 100644 --- a/esphome/components/shelly_dimmer/stm32flash.h +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -26,8 +26,7 @@ #include <memory> #include "esphome/components/uart/uart.h" -namespace esphome { -namespace shelly_dimmer { +namespace esphome::shelly_dimmer { /* flags */ constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */ @@ -125,7 +124,6 @@ stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); -} // namespace shelly_dimmer -} // namespace esphome +} // namespace esphome::shelly_dimmer #endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 701b6dd79ea..dac8ac9dbcd 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include <driver/gpio.h> -namespace esphome { -namespace st7701s { +namespace esphome::st7701s { void ST7701S::setup() { this->spi_setup(); @@ -195,6 +194,5 @@ void ST7701S::dump_config() { ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); } -} // namespace st7701s -} // namespace esphome +} // namespace esphome::st7701s #endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/st7701s/st7701s.h b/esphome/components/st7701s/st7701s.h index a1e3c2e54a0..de5e4c13d49 100644 --- a/esphome/components/st7701s/st7701s.h +++ b/esphome/components/st7701s/st7701s.h @@ -12,8 +12,7 @@ #include "esp_lcd_panel_rgb.h" -namespace esphome { -namespace st7701s { +namespace esphome::st7701s { constexpr static const char *const TAG = "display.st7701s"; const uint8_t SW_RESET_CMD = 0x01; @@ -113,6 +112,5 @@ class ST7701S : public display::Display, esp_lcd_panel_handle_t handle_{}; }; -} // namespace st7701s -} // namespace esphome +} // namespace esphome::st7701s #endif diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp index 6c46af19fdf..0c0201508af 100644 --- a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace crc8_test_component { +namespace esphome::crc8_test_component { static const char *const TAG = "crc8_test"; @@ -166,5 +165,4 @@ void CRC8TestComponent::log_test_result(const char *test_name, bool passed) { } } -} // namespace crc8_test_component -} // namespace esphome +} // namespace esphome::crc8_test_component diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h index 3b8847259c3..5d21ab134e0 100644 --- a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace crc8_test_component { +namespace esphome::crc8_test_component { class CRC8TestComponent : public Component { public: @@ -25,5 +24,4 @@ class CRC8TestComponent : public Component { uint8_t poly = 0x8C, bool msb_first = false); }; -} // namespace crc8_test_component -} // namespace esphome +} // namespace esphome::crc8_test_component diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp index c86ab992425..b66372d8ba6 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #ifdef USE_API -namespace esphome { -namespace custom_api_device_component { +namespace esphome::custom_api_device_component { static const char *const TAG = "custom_api"; @@ -58,6 +57,5 @@ void CustomAPIDeviceComponent::on_ha_state_changed(std::string entity_id, std::s ESP_LOGI(TAG, "This subscription uses std::string API for backward compatibility"); } -} // namespace custom_api_device_component -} // namespace esphome +} // namespace esphome::custom_api_device_component #endif // USE_API diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h index 4d519d3ed1f..e75b3401223 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h @@ -6,8 +6,7 @@ #include "esphome/components/api/custom_api_device.h" #ifdef USE_API -namespace esphome { -namespace custom_api_device_component { +namespace esphome::custom_api_device_component { using namespace api; @@ -28,6 +27,5 @@ class CustomAPIDeviceComponent : public Component, public CustomAPIDevice { void on_ha_state_changed(std::string entity_id, std::string state); }; -} // namespace custom_api_device_component -} // namespace esphome +} // namespace esphome::custom_api_device_component #endif // USE_API diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp index 21ca45947ec..5c3bbeea84d 100644 --- a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp +++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp @@ -5,8 +5,7 @@ #include <vector> #include <chrono> -namespace esphome { -namespace defer_stress_component { +namespace esphome::defer_stress_component { static const char *const TAG = "defer_stress"; @@ -71,5 +70,4 @@ void DeferStressComponent::run_multi_thread_test() { ESP_LOGI(TAG, "All threads finished in %lldms. Created %d defer requests", thread_time, this->total_defers_.load()); } -} // namespace defer_stress_component -} // namespace esphome +} // namespace esphome::defer_stress_component diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h index 59b75657260..2aac0aeddce 100644 --- a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h +++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include <atomic> -namespace esphome { -namespace defer_stress_component { +namespace esphome::defer_stress_component { class DeferStressComponent : public Component { public: @@ -16,5 +15,4 @@ class DeferStressComponent : public Component { std::atomic<int> executed_defers_{0}; }; -} // namespace defer_stress_component -} // namespace esphome +} // namespace esphome::defer_stress_component diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp index 28a05d3d45d..dd54381c350 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp @@ -1,7 +1,6 @@ #include "loop_test_component.h" -namespace esphome { -namespace loop_test_component { +namespace esphome::loop_test_component { void LoopTestComponent::setup() { ESP_LOGI(TAG, "[%s] Setup called", this->name_.c_str()); } @@ -63,5 +62,4 @@ void LoopTestUpdateComponent::update() { this->update_count_, loop_disabled ? "YES" : "NO"); } -} // namespace loop_test_component -} // namespace esphome +} // namespace esphome::loop_test_component diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index 3dca2da2e9f..1a814118619 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -6,8 +6,7 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace loop_test_component { +namespace esphome::loop_test_component { static const char *const TAG = "loop_test_component"; @@ -79,5 +78,4 @@ class LoopTestUpdateComponent : public PollingComponent { int disable_loop_after_{0}; }; -} // namespace loop_test_component -} // namespace esphome +} // namespace esphome::loop_test_component diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp index 30afec04223..b6f94aacf2d 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp @@ -2,8 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/application.h" -namespace esphome { -namespace loop_test_component { +namespace esphome::loop_test_component { static const char *const ISR_TAG = "loop_test_isr_component"; @@ -76,5 +75,4 @@ void IRAM_ATTR LoopTestISRComponent::simulate_isr_enable() { // For testing, we'll track the call count and log it from the main loop } -} // namespace loop_test_component -} // namespace esphome +} // namespace esphome::loop_test_component diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h index 20e11b5ecdb..5537bd02333 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace loop_test_component { +namespace esphome::loop_test_component { class LoopTestISRComponent : public Component { public: @@ -28,5 +27,4 @@ class LoopTestISRComponent : public Component { int isr_call_count_{0}; }; -} // namespace loop_test_component -} // namespace esphome +} // namespace esphome::loop_test_component diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp index be85228c3cf..f6fd1b1de72 100644 --- a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace scheduler_bulk_cleanup_component { +namespace esphome::scheduler_bulk_cleanup_component { static const char *const TAG = "bulk_cleanup"; @@ -68,5 +67,4 @@ void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { } } -} // namespace scheduler_bulk_cleanup_component -} // namespace esphome +} // namespace esphome::scheduler_bulk_cleanup_component diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h index f55472d426c..34b4a8e0d01 100644 --- a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/application.h" -namespace esphome { -namespace scheduler_bulk_cleanup_component { +namespace esphome::scheduler_bulk_cleanup_component { class SchedulerBulkCleanupComponent : public Component { public: @@ -14,5 +13,4 @@ class SchedulerBulkCleanupComponent : public Component { void trigger_bulk_cleanup(); }; -} // namespace scheduler_bulk_cleanup_component -} // namespace esphome +} // namespace esphome::scheduler_bulk_cleanup_component diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp index 305d3595919..f75a2fdd920 100644 --- a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp @@ -6,8 +6,7 @@ #include <chrono> #include <random> -namespace esphome { -namespace scheduler_heap_stress_component { +namespace esphome::scheduler_heap_stress_component { static const char *const TAG = "scheduler_heap_stress"; @@ -100,5 +99,4 @@ void SchedulerHeapStressComponent::run_multi_thread_test() { ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load()); } -} // namespace scheduler_heap_stress_component -} // namespace esphome +} // namespace esphome::scheduler_heap_stress_component diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h index 5da32ca9f8a..9f7810d0adf 100644 --- a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include <atomic> -namespace esphome { -namespace scheduler_heap_stress_component { +namespace esphome::scheduler_heap_stress_component { class SchedulerHeapStressComponent : public Component { public: @@ -18,5 +17,4 @@ class SchedulerHeapStressComponent : public Component { std::atomic<int> executed_callbacks_{0}; }; -} // namespace scheduler_heap_stress_component -} // namespace esphome +} // namespace esphome::scheduler_heap_stress_component diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp index b735c453f2d..0e5525d2656 100644 --- a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp @@ -6,8 +6,7 @@ #include <random> #include <sstream> -namespace esphome { -namespace scheduler_rapid_cancellation_component { +namespace esphome::scheduler_rapid_cancellation_component { static const char *const TAG = "scheduler_rapid_cancellation"; @@ -76,5 +75,4 @@ void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { }); } -} // namespace scheduler_rapid_cancellation_component -} // namespace esphome +} // namespace esphome::scheduler_rapid_cancellation_component diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h index 0a01b2a8de7..f1ef9a72b61 100644 --- a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include <atomic> -namespace esphome { -namespace scheduler_rapid_cancellation_component { +namespace esphome::scheduler_rapid_cancellation_component { class SchedulerRapidCancellationComponent : public Component { public: @@ -18,5 +17,4 @@ class SchedulerRapidCancellationComponent : public Component { std::atomic<int> total_executed_{0}; }; -} // namespace scheduler_rapid_cancellation_component -} // namespace esphome +} // namespace esphome::scheduler_rapid_cancellation_component diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp index 2a08bd72a9f..6bc03f34c0d 100644 --- a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp @@ -1,8 +1,7 @@ #include "recursive_timeout_component.h" #include "esphome/core/log.h" -namespace esphome { -namespace scheduler_recursive_timeout_component { +namespace esphome::scheduler_recursive_timeout_component { static const char *const TAG = "scheduler_recursive_timeout"; @@ -36,5 +35,4 @@ void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() { }); } -} // namespace scheduler_recursive_timeout_component -} // namespace esphome +} // namespace esphome::scheduler_recursive_timeout_component diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h index 8d2c085a111..237f9785b21 100644 --- a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace scheduler_recursive_timeout_component { +namespace esphome::scheduler_recursive_timeout_component { class SchedulerRecursiveTimeoutComponent : public Component { public: @@ -16,5 +15,4 @@ class SchedulerRecursiveTimeoutComponent : public Component { int nested_level_{0}; }; -} // namespace scheduler_recursive_timeout_component -} // namespace esphome +} // namespace esphome::scheduler_recursive_timeout_component diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp index b4c2b8c6c2d..a817b9f508f 100644 --- a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp @@ -5,8 +5,7 @@ #include <chrono> #include <sstream> -namespace esphome { -namespace scheduler_simultaneous_callbacks_component { +namespace esphome::scheduler_simultaneous_callbacks_component { static const char *const TAG = "scheduler_simultaneous_callbacks"; @@ -105,5 +104,4 @@ void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() }); } -} // namespace scheduler_simultaneous_callbacks_component -} // namespace esphome +} // namespace esphome::scheduler_simultaneous_callbacks_component diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h index 1a36af4b3de..9746331aec1 100644 --- a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include <atomic> -namespace esphome { -namespace scheduler_simultaneous_callbacks_component { +namespace esphome::scheduler_simultaneous_callbacks_component { class SchedulerSimultaneousCallbacksComponent : public Component { public: @@ -20,5 +19,4 @@ class SchedulerSimultaneousCallbacksComponent : public Component { std::atomic<int> max_concurrent_{0}; }; -} // namespace scheduler_simultaneous_callbacks_component -} // namespace esphome +} // namespace esphome::scheduler_simultaneous_callbacks_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp index 8c3f665f197..cc1b9f78147 100644 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp @@ -4,8 +4,7 @@ #include <thread> #include <chrono> -namespace esphome { -namespace scheduler_string_lifetime_component { +namespace esphome::scheduler_string_lifetime_component { static const char *const TAG = "scheduler_string_lifetime"; @@ -258,5 +257,4 @@ void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { }); } -} // namespace scheduler_string_lifetime_component -} // namespace esphome +} // namespace esphome::scheduler_string_lifetime_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h index 95532328bb8..20185f128d1 100644 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h @@ -4,8 +4,7 @@ #include <vector> #include <string> -namespace esphome { -namespace scheduler_string_lifetime_component { +namespace esphome::scheduler_string_lifetime_component { class SchedulerStringLifetimeComponent : public Component { public: @@ -33,5 +32,4 @@ class SchedulerStringLifetimeComponent : public Component { int tests_failed_{0}; }; -} // namespace scheduler_string_lifetime_component -} // namespace esphome +} // namespace esphome::scheduler_string_lifetime_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp index 9071e573bbf..677d371f255 100644 --- a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp @@ -7,8 +7,7 @@ #include <string> #include <sstream> -namespace esphome { -namespace scheduler_string_name_stress_component { +namespace esphome::scheduler_string_name_stress_component { static const char *const TAG = "scheduler_string_name_stress"; @@ -106,5 +105,4 @@ void SchedulerStringNameStressComponent::run_string_name_stress_test() { }); } -} // namespace scheduler_string_name_stress_component -} // namespace esphome +} // namespace esphome::scheduler_string_name_stress_component diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h index 002a0a7b511..121bda62041 100644 --- a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include <atomic> -namespace esphome { -namespace scheduler_string_name_stress_component { +namespace esphome::scheduler_string_name_stress_component { class SchedulerStringNameStressComponent : public Component { public: @@ -18,5 +17,4 @@ class SchedulerStringNameStressComponent : public Component { std::atomic<int> executed_callbacks_{0}; }; -} // namespace scheduler_string_name_stress_component -} // namespace esphome +} // namespace esphome::scheduler_string_name_stress_component From 7b6e2589f11dc8e01b346d9bd743f8d90d700928 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 8 May 2026 14:09:22 +1000 Subject: [PATCH 1901/2056] [modbus_server] Reduce log spam (#16283) --- esphome/components/modbus_server/modbus_server.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_server/modbus_server.cpp b/esphome/components/modbus_server/modbus_server.cpp index 0063da3a1da..e5ea2efa4d0 100644 --- a/esphome/components/modbus_server/modbus_server.cpp +++ b/esphome/components/modbus_server/modbus_server.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "modbus_server"; void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) { - ESP_LOGD(TAG, + ESP_LOGV(TAG, "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " "0x%X.", this->address_, function_code, start_address, number_of_registers); @@ -30,7 +30,7 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star break; } int64_t value = server_register->read_lambda(); - ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", + ESP_LOGV(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", server_register->address, static_cast<size_t>(server_register->value_type), server_register->register_count, server_register->format_value(value).c_str()); @@ -47,7 +47,7 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star if (!found) { if (this->server_courtesy_response_.enabled && (current_address <= this->server_courtesy_response_.register_last_address)) { - ESP_LOGD(TAG, + ESP_LOGV(TAG, "Could not match any register to address 0x%02X, but default allowed. " "Returning default value: %d.", current_address, this->server_courtesy_response_.register_value); From a970f05b69b5d04d198a5e1bcfa968bc4e47b982 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 8 May 2026 07:26:03 -0400 Subject: [PATCH 1902/2056] [clang-tidy] Enable readability-redundant-string-init (#16310) --- .clang-tidy | 1 - .clang-tidy.hash | 2 +- esphome/components/graph/graph.h | 2 +- esphome/components/nfc/ndef_message.cpp | 2 +- esphome/components/online_image/online_image.h | 6 +++--- esphome/components/remote_receiver/remote_receiver.h | 2 +- esphome/components/remote_transmitter/remote_transmitter.h | 2 +- esphome/components/rtttl/rtttl.h | 2 +- esphome/components/tuya/tuya.h | 2 +- esphome/components/uart/uart_component_host.h | 2 +- esphome/components/voice_assistant/voice_assistant.cpp | 6 +++--- esphome/components/voice_assistant/voice_assistant.h | 6 +++--- esphome/components/web_server_idf/web_server_idf.h | 2 +- 13 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 9aeeb1fc26c..ea7370a3b21 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -132,7 +132,6 @@ Checks: >- -readability-redundant-inline-specifier, -readability-redundant-member-init, -readability-redundant-parentheses, - -readability-redundant-string-init, -readability-redundant-typename, -readability-uppercase-literal-suffix, -readability-use-anyofallof, diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 2f973e34d9f..4c4b4e5c9c7 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -c1e4375738304baabf7e915e5f7ca50fb22ec9b4a2d8312827a32a659f3fa40c +edce6cd78b33b296cef3caa5869d237061345eb346e3f9cb21e3239d2711051f diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 468583ca216..a601e9eeb1b 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -123,7 +123,7 @@ class GraphTrace { protected: sensor::Sensor *sensor_{nullptr}; - std::string name_{""}; + std::string name_; uint8_t line_thickness_{3}; enum LineType line_type_ { LINE_TYPE_SOLID }; Color line_color_{COLOR_ON}; diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp index ba3aa77e340..d33f3f7b5cb 100644 --- a/esphome/components/nfc/ndef_message.cpp +++ b/esphome/components/nfc/ndef_message.cpp @@ -52,7 +52,7 @@ NdefMessage::NdefMessage(std::vector<uint8_t> &data) { index += type_length; - std::string id_str = ""; + std::string id_str; if (il) { id_str = std::string(data.begin() + index, data.begin() + index + id_length); index += id_length; diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 816d6525eaa..a967bb6c0e6 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -88,18 +88,18 @@ class OnlineImage : public PollingComponent, */ size_t download_buffer_initial_size_; - std::string url_{""}; + std::string url_; std::vector<std::pair<std::string, TemplatableValue<std::string>>> request_headers_; /** * The value of the ETag HTTP header provided in the last response. */ - std::string etag_ = ""; + std::string etag_; /** * The value of the Last-Modified HTTP header provided in the last response. */ - std::string last_modified_ = ""; + std::string last_modified_; uint32_t start_time_{0}; }; diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 5da9283a6e7..cc707346ebb 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -90,7 +90,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, uint32_t carrier_frequency_{0}; uint8_t carrier_duty_percent_{100}; esp_err_t error_code_{ESP_OK}; - std::string error_string_{""}; + std::string error_string_; #endif #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || defined(USE_ESP32) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 6b4ebfe24bd..d30966e3daa 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -92,7 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, rmt_channel_handle_t channel_{NULL}; rmt_encoder_handle_t encoder_{NULL}; esp_err_t error_code_{ESP_OK}; - std::string error_string_{""}; + std::string error_string_; bool inverted_{false}; bool non_blocking_{false}; #endif diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 9dac92be2ad..d060b6b024d 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -72,7 +72,7 @@ class Rtttl : public Component { void set_state_(State state); /// The RTTTL string to play. - std::string rtttl_{""}; + std::string rtttl_; /// The current position in the RTTTL string. size_t position_{0}; /// The default duration of a note (e.g. 4 for a quarter note). diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 8ba8ac85a05..470b97e7e70 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -150,7 +150,7 @@ class Tuya : public Component, public uart::UARTDevice { int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; uint32_t last_rx_char_timestamp_ = 0; - std::string product_ = ""; + std::string product_; std::vector<TuyaDatapointListener> listeners_; std::vector<TuyaDatapoint> datapoints_; std::vector<uint8_t> rx_message_; diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index 56ff525bc3d..a47e5649be8 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -25,7 +25,7 @@ class HostUartComponent : public UARTComponent, public Component { void update_error_(const std::string &error); void check_logger_conflict() override {} std::string port_name_; - std::string first_error_{""}; + std::string first_error_; int file_descriptor_ = -1; bool has_peek_{false}; uint8_t peek_byte_; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index bff0026b242..50a82652977 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -676,7 +676,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { ESP_LOGD(TAG, "Intent progress"); - std::string tts_url_for_trigger = ""; + std::string tts_url_for_trigger; #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { for (const auto &arg : msg.data) { @@ -782,8 +782,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_ERROR: { - std::string code = ""; - std::string message = ""; + std::string code; + std::string message; for (const auto &arg : msg.data) { if (arg.name == "code") { code = arg.value; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index faef09d8bd0..3de46730018 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -288,7 +288,7 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; - std::string tts_response_url_{""}; + std::string tts_response_url_; bool started_streaming_tts_{false}; MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE}; @@ -296,9 +296,9 @@ class VoiceAssistant : public Component { bool local_output_{false}; - std::string conversation_id_{""}; + std::string conversation_id_; - std::string wake_word_{""}; + std::string wake_word_; std::shared_ptr<ring_buffer::RingBuffer> ring_buffer_; diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index cdb58c2f046..c622d53e893 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -313,7 +313,7 @@ class AsyncEventSourceResponse { std::vector<DeferredEvent> deferred_queue_; esphome::web_server::WebServer *web_server_; esphome::web_server::ListEntitiesIterator entities_iterator_; - std::string event_buffer_{""}; + std::string event_buffer_; size_t event_bytes_sent_; uint16_t consecutive_send_failures_{0}; static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate From eb52ca61fe528df25110814ae3adcd24b816edd4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 8 May 2026 07:26:14 -0400 Subject: [PATCH 1903/2056] [climate][ektf2232] Remove deprecations scheduled for 2026.5.0 (#16289) --- esphome/components/climate/climate_traits.h | 57 ------------------- .../ektf2232/touchscreen/__init__.py | 4 -- 2 files changed, 61 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 082b2127a9f..599894c8a9f 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -81,63 +81,6 @@ class ClimateTraits { bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; } void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; } - ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") - bool get_supports_current_temperature() const { - return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); - } - ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") - void set_supports_current_temperature(bool supports_current_temperature) { - if (supports_current_temperature) { - this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); - } else { - this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); - } - } - ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") - bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); } - ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") - void set_supports_current_humidity(bool supports_current_humidity) { - if (supports_current_humidity) { - this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); - } else { - this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); - } - } - ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") - bool get_supports_two_point_target_temperature() const { - return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); - } - ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") - void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { - if (supports_two_point_target_temperature) - // Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior - { - this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); - } else { - this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE); - } - } - ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") - bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); } - ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") - void set_supports_target_humidity(bool supports_target_humidity) { - if (supports_target_humidity) { - this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); - } else { - this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); - } - } - ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0") - bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); } - ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0") - void set_supports_action(bool supports_action) { - if (supports_action) { - this->add_feature_flags(CLIMATE_SUPPORTS_ACTION); - } else { - this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION); - } - } - void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } diff --git a/esphome/components/ektf2232/touchscreen/__init__.py b/esphome/components/ektf2232/touchscreen/__init__.py index 123f03ca087..64bb17a7db6 100644 --- a/esphome/components/ektf2232/touchscreen/__init__.py +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -15,7 +15,6 @@ EKTF2232Touchscreen = ektf2232_ns.class_( ) CONF_EKTF2232_ID = "ektf2232_id" -CONF_RTS_PIN = "rts_pin" # To be removed before 2026.4.0 CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Schema( @@ -25,9 +24,6 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_RTS_PIN): cv.invalid( - f"{CONF_RTS_PIN} has been renamed to {CONF_RESET_PIN}" - ), } ).extend(i2c.i2c_device_schema(0x15)) ) From 3d8fffbea954580a082792ae919d611e8890e5c8 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Fri, 8 May 2026 13:17:14 +0000 Subject: [PATCH 1904/2056] [htu31d][kamstrup_kmp][ms8607] Use CRC functions from helpers.h (#16313) --- esphome/components/htu31d/htu31d.cpp | 32 ++----------------- .../components/kamstrup_kmp/kamstrup_kmp.cpp | 28 ++-------------- .../components/kamstrup_kmp/kamstrup_kmp.h | 3 -- esphome/components/ms8607/ms8607.cpp | 32 +------------------ tests/components/ms8607/common.yaml | 11 +++++++ tests/components/ms8607/test.esp32-idf.yaml | 8 +++++ tests/components/ms8607/test.esp8266-ard.yaml | 8 +++++ tests/components/ms8607/test.rp2040-ard.yaml | 8 +++++ 8 files changed, 42 insertions(+), 88 deletions(-) create mode 100644 tests/components/ms8607/common.yaml create mode 100644 tests/components/ms8607/test.esp32-idf.yaml create mode 100644 tests/components/ms8607/test.esp8266-ard.yaml create mode 100644 tests/components/ms8607/test.rp2040-ard.yaml diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 0b679bf2b77..6821b8b69ef 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -43,32 +43,6 @@ static const uint8_t HTU31D_RESET = 0x1E; /** Diagnostics command. */ static const uint8_t HTU31D_DIAGNOSTICS = 0x08; -/** - * Computes a CRC result for the provided input. - * - * @returns the computed CRC result for the provided input - */ -uint8_t compute_crc(uint32_t value) { - uint32_t polynom = 0x98800000; // x^8 + x^5 + x^4 + 1 - uint32_t msb = 0x80000000; - uint32_t mask = 0xFF800000; - uint32_t threshold = 0x00000080; - uint32_t result = value; - - while (msb != threshold) { - // Check if msb of current value is 1 and apply XOR mask - if (result & msb) - result = ((result ^ polynom) & mask) | (result & ~mask); - - // Shift by one - msb >>= 1; - mask >>= 1; - polynom >>= 1; - } - - return result; -} - /** * Resets the sensor and ensures that the devices serial number can be read over * I2C. @@ -112,7 +86,7 @@ void HTU31DComponent::update() { // Calculate temperature value. uint16_t raw_temp = encode_uint16(thdata[0], thdata[1]); - uint8_t crc = compute_crc((uint32_t) raw_temp << 8); + uint8_t crc = crc8(thdata, 2, 0, 0x31, true); if (crc != thdata[2]) { this->status_set_warning(); ESP_LOGE(TAG, "Error validating temperature CRC"); @@ -131,7 +105,7 @@ void HTU31DComponent::update() { // Calculate humidty value. uint16_t raw_hum = encode_uint16(thdata[3], thdata[4]); - crc = compute_crc((uint32_t) raw_hum << 8); + crc = crc8(thdata + 3, 2, 0, 0x31, true); if (crc != thdata[5]) { this->status_set_warning(); ESP_LOGE(TAG, "Error validating humidty CRC"); @@ -197,7 +171,7 @@ uint32_t HTU31DComponent::read_serial_num_() { serial = encode_uint32(reply[0], reply[1], reply[2], padding); - uint8_t crc = compute_crc(serial); + uint8_t crc = crc8(reply, 3, 0, 0x31, true); if (crc != reply[3]) { ESP_LOGE(TAG, "Error validating serial CRC"); return 0; diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index ed03c4d6df4..70f6d4eaa76 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -1,5 +1,6 @@ #include "kamstrup_kmp.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::kamstrup_kmp { @@ -95,10 +96,7 @@ void KamstrupKMPComponent::send_message_(const uint8_t *msg, int msg_len) { buffer[i] = msg[i]; } - buffer[buffer_len - 2] = 0; - buffer[buffer_len - 1] = 0; - - uint16_t crc = crc16_ccitt(buffer, buffer_len); + uint16_t crc = crc16be(buffer, buffer_len - 2); buffer[buffer_len - 2] = crc >> 8; buffer[buffer_len - 1] = crc & 0xFF; @@ -192,7 +190,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) { } // Validate CRC - if (crc16_ccitt(msg, msg_len)) { + if (crc16be(msg, msg_len - 2) != encode_uint16(msg[msg_len - 2], msg[msg_len - 1])) { ESP_LOGE(TAG, "Received invalid message (CRC mismatch)"); return; } @@ -282,24 +280,4 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint ESP_LOGD(TAG, "Received value for command 0x%04X: %.3f [%s]", command, value, unit); } -uint16_t crc16_ccitt(const uint8_t *buffer, int len) { - uint32_t poly = 0x1021; - uint32_t reg = 0x00; - for (int i = 0; i < len; i++) { - int mask = 0x80; - while (mask > 0) { - reg <<= 1; - if (buffer[i] & mask) { - reg |= 1; - } - mask >>= 1; - if (reg & 0x10000) { - reg &= 0xffff; - reg ^= poly; - } - } - } - return (uint16_t) reg; -} - } // namespace esphome::kamstrup_kmp diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index a05a0ee17af..a4eacec4532 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -123,7 +123,4 @@ class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { void set_sensor_value_(uint16_t command, float value, uint8_t unit_idx); }; -// "true" CCITT CRC-16 -uint16_t crc16_ccitt(const uint8_t *buffer, int len); - } // namespace esphome::kamstrup_kmp diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index f733a8349d4..b9cbdc749b4 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -63,7 +63,6 @@ enum class MS8607Component::SetupStatus { }; static uint8_t crc4(uint16_t *buffer, size_t length); -static uint8_t hsensor_crc_check(uint16_t value); void MS8607Component::setup() { this->error_code_ = ErrorCode::NONE; @@ -244,35 +243,6 @@ static uint8_t crc4(uint16_t *buffer, size_t length) { return (crc_remainder >> 12) & 0xF; // only the most significant 4 bits } -/** - * @brief Calculates CRC value for the provided humidity (+ status bits) value - * - * CRC-8 check comes from other MS8607 libraries on github. I did not find it in the datasheet, - * and it differs from the crc8 implementation that's already part of esphome. - * - * @param value two byte humidity sensor value read from i2c - * @return uint8_t computed crc value - */ -static uint8_t hsensor_crc_check(uint16_t value) { - uint32_t polynom = 0x988000; // x^8 + x^5 + x^4 + 1 - uint32_t msb = 0x800000; - uint32_t mask = 0xFF8000; - uint32_t result = (uint32_t) value << 8; // Pad with zeros as specified in spec - - while (msb != 0x80) { - // Check if msb of current value is 1 and apply XOR mask - if (result & msb) { - result = ((result ^ polynom) & mask) | (result & ~mask); - } - - // Shift by one - msb >>= 1; - mask >>= 1; - polynom >>= 1; - } - return result & 0xFF; -} - void MS8607Component::request_read_temperature_() { // Tell MS8607 to start ADC conversion of temperature sensor if (!this->write_bytes(MS8607_CMD_CONV_D2_OSR_8K, nullptr, 0)) { @@ -338,7 +308,7 @@ void MS8607Component::read_humidity_(float temperature_float) { // Bit1 of the two LSBS must be set to '1'. Bit0 is currently not assigned" uint16_t humidity = encode_uint16(bytes[0], bytes[1]); uint8_t const expected_crc = bytes[2]; - uint8_t const actual_crc = hsensor_crc_check(humidity); + uint8_t const actual_crc = crc8(bytes, 2, 0, 0x31, true); if (expected_crc != actual_crc) { ESP_LOGE(TAG, "Incorrect Humidity CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, actual_crc); diff --git a/tests/components/ms8607/common.yaml b/tests/components/ms8607/common.yaml new file mode 100644 index 00000000000..09a3f5a617a --- /dev/null +++ b/tests/components/ms8607/common.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: ms8607 + i2c_id: i2c_bus + temperature: + name: Temperature + humidity: + name: Humidity + pressure: + name: Pressure + address: 0x76 + update_interval: 15s diff --git a/tests/components/ms8607/test.esp32-idf.yaml b/tests/components/ms8607/test.esp32-idf.yaml new file mode 100644 index 00000000000..4598505c3a2 --- /dev/null +++ b/tests/components/ms8607/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + i2c_scl: GPIO16 + i2c_sda: GPIO17 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ms8607/test.esp8266-ard.yaml b/tests/components/ms8607/test.esp8266-ard.yaml new file mode 100644 index 00000000000..5565bb8c352 --- /dev/null +++ b/tests/components/ms8607/test.esp8266-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ms8607/test.rp2040-ard.yaml b/tests/components/ms8607/test.rp2040-ard.yaml new file mode 100644 index 00000000000..888762a7423 --- /dev/null +++ b/tests/components/ms8607/test.rp2040-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + i2c_scl: GPIO5 + i2c_sda: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 88c2a1c09639a28f140cc59d89e97b5d660f1994 Mon Sep 17 00:00:00 2001 From: John Boiles <johnaboiles@gmail.com> Date: Fri, 8 May 2026 11:43:55 -0700 Subject: [PATCH 1905/2056] [speaker] Add SPDIF output support (#8065) Co-authored-by: Keith Burzinski <kbx81x@gmail.com> Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> --- esphome/components/i2s_audio/__init__.py | 5 +- .../components/i2s_audio/speaker/__init__.py | 72 +- .../i2s_audio/speaker/i2s_audio_spdif.cpp | 629 ++++++++++++++++++ .../i2s_audio/speaker/i2s_audio_spdif.h | 34 + .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- .../i2s_audio/speaker/i2s_audio_speaker.h | 4 +- .../i2s_audio/speaker/spdif_encoder.cpp | 385 +++++++++++ .../i2s_audio/speaker/spdif_encoder.h | 146 ++++ esphome/core/defines.h | 1 + .../speaker/spdif_mode.esp32-idf.yaml | 25 + 10 files changed, 1291 insertions(+), 12 deletions(-) create mode 100644 esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp create mode 100644 esphome/components/i2s_audio/speaker/i2s_audio_spdif.h create mode 100644 esphome/components/i2s_audio/speaker/spdif_encoder.cpp create mode 100644 esphome/components/i2s_audio/speaker/spdif_encoder.h create mode 100644 tests/components/speaker/spdif_mode.esp32-idf.yaml diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index ffa63f5ee8e..951b8c04983 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -201,7 +201,7 @@ async def register_i2s_audio_component(var, config): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(I2SAudioComponent), - cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, }, @@ -290,7 +290,8 @@ async def to_code(config): # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) - cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) + if CONF_I2S_LRCLK_PIN in config: + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) if CONF_I2S_MCLK_PIN in config: diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 99aa712c682..759cc40ca9e 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -18,10 +18,12 @@ from .. import ( CONF_I2S_DOUT_PIN, CONF_I2S_MODE, CONF_LEFT, + CONF_MCLK_MULTIPLE, CONF_MONO, CONF_PRIMARY, CONF_RIGHT, CONF_STEREO, + CONF_USE_APLL, I2SAudioOut, i2s_audio_component_schema, i2s_audio_ns, @@ -40,6 +42,15 @@ I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) CONF_DAC_TYPE = "dac_type" CONF_I2S_COMM_FMT = "i2s_comm_fmt" +CONF_SPDIF_MODE = "spdif_mode" + +I2SAudioSpeakerBase = i2s_audio_ns.class_( + "I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut +) +I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase) +I2SAudioSpeakerSPDIF = i2s_audio_ns.class_("I2SAudioSpeakerSPDIF", I2SAudioSpeakerBase) + +I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True) I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True) @@ -77,7 +88,17 @@ def _set_num_channels_from_config(config): def _set_stream_limits(config): - if config[CONF_I2S_MODE] == CONF_PRIMARY: + if config.get(CONF_SPDIF_MODE, False): + # SPDIF mode: fixed to 16-bit stereo at configured sample rate + audio.set_stream_limits( + min_bits_per_sample=16, + max_bits_per_sample=16, + min_channels=2, + max_channels=2, + min_sample_rate=config.get(CONF_SAMPLE_RATE), + max_sample_rate=config.get(CONF_SAMPLE_RATE), + )(config) + elif config[CONF_I2S_MODE] == CONF_PRIMARY: # Primary mode has modifiable stream settings audio.set_stream_limits( min_bits_per_sample=8, @@ -101,6 +122,13 @@ def _set_stream_limits(config): return config +def _select_speaker_class(config): + """Override ID type when SPDIF mode is enabled.""" + if config.get(CONF_SPDIF_MODE, False): + config[CONF_ID].type = I2SAudioSpeakerSPDIF + return config + + def _validate_esp32_variant(config): variant = esp32.get_esp32_variant() if config[CONF_DAC_TYPE] == "internal": @@ -155,6 +183,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.one_of( *I2C_COMM_FMT_OPTIONS, lower=True ), + cv.Optional(CONF_SPDIF_MODE, default=False): cv.boolean, } ), }, @@ -163,6 +192,7 @@ CONFIG_SCHEMA = cv.All( _validate_esp32_variant, _set_num_channels_from_config, _set_stream_limits, + _select_speaker_class, validate_mclk_divisible_by_3, ) @@ -175,6 +205,28 @@ def _final_validate(config): if config[CONF_I2S_COMM_FMT] == "stand_max": raise cv.Invalid("I2S standard max format is no longer supported.") + if config.get(CONF_SPDIF_MODE, False): + # SPDIF mode specific validations + if config[CONF_SAMPLE_RATE] not in [44100, 48000]: + raise cv.Invalid( + "SPDIF mode only supports 44100 Hz or 48000 Hz sample rates" + ) + if config[CONF_CHANNEL] != CONF_STEREO: + raise cv.Invalid("SPDIF mode only supports stereo channel configuration") + # bits_per_sample is converted to float by the schema + if config[CONF_BITS_PER_SAMPLE] != 16: + raise cv.Invalid("SPDIF mode only supports 16 bits per sample") + if not config[CONF_USE_APLL]: + raise cv.Invalid( + "SPDIF mode requires 'use_apll: true' for accurate clock generation" + ) + if config[CONF_I2S_MODE] != CONF_PRIMARY: + raise cv.Invalid("SPDIF mode requires 'i2s_mode: primary'") + if config[CONF_I2S_COMM_FMT] != "stand_i2s": + raise cv.Invalid("SPDIF mode requires 'i2s_comm_fmt: stand_i2s'") + if config[CONF_MCLK_MULTIPLE] != 256: + raise cv.Invalid("SPDIF mode requires 'mclk_multiple: 256'") + FINAL_VALIDATE_SCHEMA = _final_validate @@ -186,12 +238,18 @@ async def to_code(config): await speaker.register_speaker(var, config) cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long - if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: - fmt = I2SCommFmt.MSB - elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: - fmt = I2SCommFmt.PCM - cg.add(var.set_i2s_comm_fmt(fmt)) + + is_spdif = config.get(CONF_SPDIF_MODE, False) + if is_spdif: + cg.add_define("USE_I2S_AUDIO_SPDIF_MODE") + else: + fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long + if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]: + fmt = I2SCommFmt.MSB + elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]: + fmt = I2SCommFmt.PCM + cg.add(var.set_i2s_comm_fmt(fmt)) + if config[CONF_TIMEOUT] != CONF_NEVER: cg.add(var.set_timeout(config[CONF_TIMEOUT])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp new file mode 100644 index 00000000000..e2146de63ce --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp @@ -0,0 +1,629 @@ +#include "i2s_audio_spdif.h" + +#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) + +#include <driver/i2s_std.h> + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio/audio_transfer_buffer.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include "esp_timer.h" + +namespace esphome::i2s_audio { + +static const char *const TAG = "i2s_audio.spdif"; + +// SPDIF mode adds overhead as each sample is encapsulated in a subframe; +// each DMA buffer can hold only 192 samples (~4ms each vs. ~15ms for standard I2S). +// To match the standard I2S buffering duration, we use more buffers to minimize +// the impact of the overhead, such as stuttering or audio/silence oscillation. +// 15 buffers x 4ms = 60ms of DMA buffering (same as 4 x 15ms for standard) +static constexpr size_t SPDIF_DMA_BUFFERS_COUNT = 15; + +// Timeout for flushing pending frames if no callback received. +static constexpr uint32_t SPDIF_FLUSH_TIMEOUT_MS = 20; + +// Number of DMA events between upstream callbacks (~16ms = 4 events x 4ms each). +// Matches non-SPDIF timing to prevent overwhelming upstream sync algorithms. +static constexpr uint32_t SPDIF_DMA_EVENTS_PER_CALLBACK = 4; + +// Consider TX stalled only if no DMA callbacks have arrived for this long. +// Zero-block non-blocking writes alone are not sufficient (they can happen when DMA is simply full). +static constexpr uint32_t SPDIF_STALL_NO_DMA_MS = 80; + +// Fallback stall detector: force recovery if silence writes make no forward progress for too long, +// even if occasional DMA callbacks are still observed. +static constexpr uint32_t SPDIF_STALL_ZERO_PROGRESS_MS = 1000; + +// Minimum spacing between re-prime attempts to avoid churn. +static constexpr uint32_t SPDIF_REPRIME_COOLDOWN_MS = 500; + +// Small waits used in SPDIF mode to keep DMA fed during rapid pipeline churn. +static constexpr uint32_t SPDIF_EMPTY_READ_DELAY_MS = 1; +static constexpr uint32_t SPDIF_SILENCE_LOOP_DELAY_MS = 1; +static constexpr uint32_t SPDIF_PLAY_RETRY_WAIT_MS = 5; + +static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = SPDIF_DMA_BUFFERS_COUNT + 1; + +// Static silence buffer for SPDIF continuous mode +// 192 samples * 2 channels * 2 bytes per sample = 768 bytes +// Stored in flash (.rodata section) to avoid stack/heap usage +static const int16_t SPDIF_SILENCE_BUFFER[SPDIF_BLOCK_SAMPLES * 2] = {0}; + +// Static callback functions for SPDIF encoder (avoids std::function overhead) +static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) { + auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx); + size_t bytes_written = 0; + esp_err_t err = i2s_channel_preload_data(speaker->get_tx_handle(), data, size, &bytes_written); + if (err != ESP_OK || bytes_written != size) { + ESP_LOGW(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); + return (err != ESP_OK) ? err : ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) { + auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx); + size_t bytes_written = 0; + esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait); + // ESP_ERR_TIMEOUT is expected under DMA backpressure in SPDIF mode. + if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { + ESP_LOGW(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); + } + return err; +} + +void I2SAudioSpeakerSPDIF::setup() { + I2SAudioSpeakerBase::setup(); + if (this->is_failed()) { + return; + } + + this->spdif_encoder_ = new SPDIFEncoder(); + if (!this->spdif_encoder_->setup()) { + ESP_LOGE(TAG, "Encoder setup failed"); + this->mark_failed(); + return; + } + + // Configure channel status block with the sample rate + this->spdif_encoder_->set_sample_rate(this->sample_rate_); + + // Separate callbacks for preload (during underflow recovery) and normal writes + this->spdif_encoder_->set_preload_callback(spdif_preload_cb, this); + this->spdif_encoder_->set_write_callback(spdif_write_cb, this); +} + +void I2SAudioSpeakerSPDIF::dump_config() { + I2SAudioSpeakerBase::dump_config(); + ESP_LOGCONFIG(TAG, + " SPDIF Mode: YES\n" + " Sample Rate: %" PRIu32 " Hz", + this->sample_rate_); +} + +void I2SAudioSpeakerSPDIF::on_task_stopped() { this->spdif_silence_start_ = 0; } + +size_t I2SAudioSpeakerSPDIF::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { + if (this->is_failed()) { + ESP_LOGE(TAG, "Setup failed; cannot play audio"); + return 0; + } + + // In SPDIF mode, keep accepting upstream audio while the speaker task is active. + // This avoids transient drops during stop/start transitions. + const bool task_active = (this->speaker_task_handle_ != nullptr); + + if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { + this->start(); + } + + if (!task_active && this->state_ != speaker::STATE_RUNNING) { + // Unable to write data to a running speaker, so delay the max amount of time so it can get ready + vTaskDelay(ticks_to_wait); + ticks_to_wait = 0; + } + + size_t bytes_written = 0; + if (this->state_ == speaker::STATE_RUNNING || task_active) { + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock(); + if (temp_ring_buffer != nullptr) { + // In SPDIF mode, a tiny wait helps avoid transient 0-byte writes during short backpressure windows. + TickType_t effective_ticks_to_wait = ticks_to_wait; + if (effective_ticks_to_wait == 0) { + effective_ticks_to_wait = pdMS_TO_TICKS(1); + } + bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, effective_ticks_to_wait); + if (bytes_written == 0 && length > 0) { + // Retry once to catch short free-space windows during rapid seek/track transitions. + bytes_written = + temp_ring_buffer->write_without_replacement((void *) data, length, pdMS_TO_TICKS(SPDIF_PLAY_RETRY_WAIT_MS)); + } + } + } + + return bytes_written; +} + +void I2SAudioSpeakerSPDIF::run_speaker_task() { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); + + // Reset SPDIF encoder at task start to ensure clean state + // (previous task may have left stale data in encoder buffer) + if (this->spdif_encoder_ != nullptr) { + this->spdif_encoder_->reset(); + } + + const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * SPDIF_DMA_BUFFERS_COUNT; + // Ensure ring buffer duration is at least the duration of all DMA buffers + const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); + + // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info + const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration); + + // For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames + const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES; + const size_t bytes_to_fill_single_dma_buffer = + this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); + + bool successful_setup = false; + std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = + audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + + if (transfer_buffer != nullptr) { + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer.use_count() == 1) { + transfer_buffer->set_source(temp_ring_buffer); + this->audio_ring_buffer_ = temp_ring_buffer; + successful_setup = true; + } + } + + if (!successful_setup) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + } else { + // Preload DMA buffers with SPDIF-encoded silence before enabling the channel. + // This ensures the first data transmitted is valid SPDIF (not raw zeros from + // auto_clear) and prevents phantom DMA events before real audio is available. + // Track how many buffers were preloaded so the DMA event loop can skip + // frame accounting until the preloaded silence has fully drained. + uint32_t preload_buffers_remaining = 0; + this->spdif_encoder_->set_preload_mode(true); + for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) { + uint32_t preload_blocks = 0; + esp_err_t preload_err = this->spdif_encoder_->write(reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), + sizeof(SPDIF_SILENCE_BUFFER), + pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks); + if (preload_err != ESP_OK || preload_blocks == 0) { + break; // DMA buffers full or error + } + preload_buffers_remaining += preload_blocks; + } + this->spdif_encoder_->set_preload_mode(false); + this->spdif_encoder_->reset(); // Clean encoder state for the main loop + + // Now register the callback and enable the channel + xQueueReset(this->i2s_event_queue_); + const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; + i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); + i2s_channel_enable(this->tx_handle_); + + bool stop_gracefully = false; + bool tx_dma_underflow = true; + + uint32_t frames_written = 0; + + // SPDIF Continuous Silence Mode + Callback Decimation + // + // Key principles: + // 1. NEVER stop the I2S channel - always output a valid SPDIF stream + // 2. When no audio data, output silence-encoded SPDIF blocks (not zeros!) + // 3. Fire callbacks every 4 DMA events (~16ms), matching non-SPDIF timing + // + // This eliminates gaps that cause SPDIF receivers to re-sync, and reduces + // callback rate to prevent overwhelming upstream sync algorithms. + const uint32_t spdif_callback_threshold = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + uint32_t spdif_pending_frames = 0; + int64_t spdif_pending_timestamp = 0; + uint32_t spdif_last_callback_time = millis(); + // Count DMA events for decimation + uint32_t spdif_dma_event_count = 0; + uint32_t spdif_last_dma_event_time = millis(); + // Detect a stalled DMA path (many silence write attempts with zero accepted blocks). + uint32_t spdif_zero_block_streak = 0; + uint32_t spdif_last_block_progress_time = millis(); + uint32_t spdif_last_reprime_time = 0; + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); + + // SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data + // to keep the receiver synced. Exits only via break (stream info change or silence timeout). + while (true) { + uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); + // In SPDIF continuous mode, don't tear down or expose STOPPED here. + // Keep the task alive and transition to silence output. + this->spdif_silence_start_ = millis(); + ESP_LOGV(TAG, "COMMAND_STOP received, continuing in silence mode"); + } + if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); + stop_gracefully = true; + } + + if (this->audio_stream_info_ != this->current_stream_info_) { + // Audio stream info changed, stop the speaker task so it will restart with the proper settings. + ESP_LOGV(TAG, "Exiting: stream info changed"); + break; + } + + int64_t write_timestamp; + while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { + spdif_last_dma_event_time = millis(); + + // Skip frame accounting for preloaded silence buffers still draining. + // These DMA events correspond to silence that was preloaded before the + // channel was enabled, not real audio written by the task. + if (preload_buffers_remaining > 0) { + preload_buffers_remaining--; + continue; + } + + // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes + // on the timing info via the audio_output_callback. + uint32_t frames_sent = frames_to_fill_single_dma_buffer; + if (frames_to_fill_single_dma_buffer > frames_written) { + tx_dma_underflow = true; + frames_sent = frames_written; + const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; + write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed); + } else { + tx_dma_underflow = false; + } + frames_written -= frames_sent; + + // SPDIF Callback Decimation: fire every 4th DMA event (~16ms) + // This matches non-SPDIF timing and prevents overwhelming upstream. + if (spdif_callback_threshold > 0) { + spdif_dma_event_count++; + + // Accumulate frames; always keep the latest timestamp so the + // callback reports when the last sample left the wire, not the first. + if (frames_sent > 0) { + spdif_pending_timestamp = write_timestamp; + spdif_pending_frames += frames_sent; + } + + // Fire callback every 4 DMA events, or on timeout if we have pending frames + bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK); + bool timeout_flush = + (spdif_pending_frames > 0) && ((millis() - spdif_last_callback_time) >= SPDIF_FLUSH_TIMEOUT_MS); + + if (decimation_reached || timeout_flush) { + if (spdif_pending_frames > 0) { + this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp); + spdif_pending_frames = 0; + spdif_last_callback_time = millis(); + } + spdif_dma_event_count = 0; // Reset decimation counter + } + } + } + + if (this->pause_state_) { + // Pause state is accessed atomically, so thread safe + // Delay so the task yields, then skip transferring audio data + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + continue; + } + + // Wait half the duration of the data already written to the DMA buffers for new audio data + // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 + uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; + + // In SPDIF mode, if transfer buffer is empty (we're pumping silence), use a very short timeout. + // This ensures we can pump silence fast enough to keep the DMA fed (~250 blocks/sec needed). + // Otherwise the long timeout based on frames_written causes DMA to run dry. + if (transfer_buffer->available() == 0) { + read_delay = SPDIF_EMPTY_READ_DELAY_MS; + } + + size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); + uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; + + if (bytes_read > 0) { + this->apply_software_volume_(new_data, bytes_read); + this->swap_esp32_mono_samples_(new_data, bytes_read); + } + + if (transfer_buffer->available() == 0) { + // SPDIF Continuous Silence Mode: always output valid SPDIF stream + // When no audio data, write silence-encoded blocks to keep receiver happy + if (this->spdif_encoder_ != nullptr) { + // "Graceful stop" means "drain buffered audio, then stop." In SPDIF + // continuous mode we never actually stop, so once audio is drained + // (we're here), reset the flag to re-enable silence writing and stall + // recovery. Without this, stop_gracefully stays true forever and + // blocks silence output, causing DMA to degrade on auto_clear zeros. + stop_gracefully = false; + + // Track when we entered silence mode + if (this->spdif_silence_start_ == 0) { + this->spdif_silence_start_ = millis(); + } + + // If silence persists past the configured timeout, stop the task + // so components expecting timeout semantics can recover. + if (this->timeout_.has_value()) { + const uint32_t silence_duration = millis() - this->spdif_silence_start_; + if (silence_duration >= this->timeout_.value()) { + ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration); + break; + } + } + + // First flush any partial block with silence padding (non-blocking to avoid getting stuck). + // IMPORTANT: Credit any partial block frames to frames_written so the audio_output_callback_ + // fires for them. Without this, pending_playback_frames_ in the mixer's SourceSpeaker never + // reaches 0 when a stream ends on a non-192-frame boundary, permanently blocking teardown. + if (this->spdif_encoder_->has_pending_data()) { + uint32_t partial_frames = this->spdif_encoder_->get_pending_frames(); + // Use a tiny timeout to allow DMA queue progress without stalling the task. + esp_err_t flush_err = this->spdif_encoder_->flush_with_silence(pdMS_TO_TICKS(1)); + if (flush_err == ESP_OK && partial_frames > 0) { + frames_written += partial_frames; + } + } + + // CRITICAL: In SPDIF continuous mode, ALWAYS write silence when no audio data. + // We don't check tx_dma_underflow because: + // 1. When DMA runs empty, callbacks stop, so tx_dma_underflow doesn't update + // 2. The non-blocking write handles "DMA full" gracefully (just doesn't write) + // 3. We need continuous output to prevent receiver from losing sync + if (!stop_gracefully) { + uint32_t silence_blocks = 0; + esp_err_t write_err = this->spdif_encoder_->write( + reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER), pdMS_TO_TICKS(1), + &silence_blocks); // Non-blocking + // Don't count silence as frames_written - it's not real audio + + // Recovery path for a stalled SPDIF TX channel: + // if silence writes repeatedly produce zero blocks AND DMA callbacks have stopped, + // re-prime DMA using preload mode. + const uint32_t ms_since_dma = millis() - spdif_last_dma_event_time; + const bool dma_events_stalled = ms_since_dma >= SPDIF_STALL_NO_DMA_MS; + if (silence_blocks > 0) { + spdif_last_block_progress_time = millis(); + } + const bool long_zero_progress = (millis() - spdif_last_block_progress_time) >= SPDIF_STALL_ZERO_PROGRESS_MS; + if (dma_events_stalled && silence_blocks == 0 && (write_err == ESP_OK || write_err == ESP_ERR_TIMEOUT)) { + spdif_zero_block_streak++; + } else { + spdif_zero_block_streak = 0; + } + + const uint32_t now_ms = millis(); + const bool reprime_cooldown_elapsed = + (spdif_last_reprime_time == 0) || ((now_ms - spdif_last_reprime_time) >= SPDIF_REPRIME_COOLDOWN_MS); + + if ((spdif_zero_block_streak >= 100 || long_zero_progress) && reprime_cooldown_elapsed) { + ESP_LOGV(TAG, "TX appears stalled, attempting DMA re-prime"); + + i2s_channel_disable(this->tx_handle_); + + const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr}; + i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this); + + this->spdif_encoder_->set_preload_mode(true); + uint32_t preload_blocks = 0; + esp_err_t preload_err = this->spdif_encoder_->write( + reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER), + pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks); + this->spdif_encoder_->set_preload_mode(false); + + xQueueReset(this->i2s_event_queue_); + const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; + i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); + i2s_channel_enable(this->tx_handle_); + + if (preload_err == ESP_OK && preload_blocks > 0) { + tx_dma_underflow = false; + preload_buffers_remaining = preload_blocks; + frames_written = 0; // Stale after channel disable/enable cycle + ESP_LOGV(TAG, "DMA re-prime successful (%" PRIu32 " preload blocks)", preload_blocks); + spdif_last_block_progress_time = now_ms; + } else { + ESP_LOGW(TAG, "DMA re-prime failed (%s, blocks=%" PRIu32 ")", esp_err_to_name(preload_err), + preload_blocks); + } + spdif_last_reprime_time = now_ms; + spdif_zero_block_streak = 0; + } + } + } + + if (stop_gracefully && tx_dma_underflow) { + // In SPDIF continuous mode, don't break on graceful stop during silence + // Keep outputting silence until new audio arrives or explicit COMMAND_STOP + // (handled above which transitions to silence mode rather than breaking) + } + + // In SPDIF mode, use a shorter delay to pump silence faster + // We need ~250 blocks/sec to keep DMA fed, so max 4ms per iteration + vTaskDelay(pdMS_TO_TICKS(SPDIF_SILENCE_LOOP_DELAY_MS)); + } else { + // Have audio data to write + size_t bytes_written = 0; + + // Clear silence timer since we have audio data now + if (this->spdif_silence_start_ != 0) { + uint32_t silence_duration = millis() - this->spdif_silence_start_; + if (silence_duration > 100) { + ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration); + } + this->spdif_silence_start_ = 0; + } + + { + uint32_t blocks_sent = 0; + size_t pcm_bytes_consumed = 0; + + // Write audio data to encoder (which writes to DMA) + esp_err_t err = + this->spdif_encoder_->write(transfer_buffer->get_buffer_start(), transfer_buffer->available(), + pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &blocks_sent, &pcm_bytes_consumed); + if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { + ESP_LOGW(TAG, "Write failed: %s", esp_err_to_name(err)); + } + + // Only consume source bytes that were actually accepted by the encoder. + bytes_written = pcm_bytes_consumed; + + // Update frame accounting based on complete blocks sent (192 frames per block) + if (bytes_written > 0) { + frames_written += blocks_sent * SPDIF_BLOCK_SAMPLES; + transfer_buffer->decrease_buffer_length(bytes_written); + // Audio blocks count as DMA progress for the stall detector. + // Without this, a long uninterrupted audio stream makes the + // progress timer stale, triggering a spurious re-prime the + // instant we transition to silence. + spdif_last_block_progress_time = millis(); + } + } + } + } + // If we reach here, the while loop exited - either via break or condition became false + // In SPDIF mode, loop exit is expected when: + // 1. Timeout reached (user configured timeout) + // 2. Stream info changed + // Only warn if timeout is "never" since that should never exit + if (!this->timeout_.has_value()) { + ESP_LOGW(TAG, "Unexpected loop exit; set 'timeout: never' to prevent this"); + } + } + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); + + // Reset SPDIF encoder state to prevent stale state on next start + if (this->spdif_encoder_ != nullptr) { + this->spdif_encoder_->set_preload_mode(false); + this->spdif_encoder_->reset(); + } + + if (transfer_buffer != nullptr) { + transfer_buffer.reset(); + } + + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); + + while (true) { + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); + } +} + +esp_err_t I2SAudioSpeakerSPDIF::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) { + this->current_stream_info_ = audio_stream_info; + + // SPDIF mode validation + if (this->sample_rate_ != audio_stream_info.get_sample_rate()) { + ESP_LOGE(TAG, "Only supports a single sample rate (configured: %" PRIu32 " Hz, stream: %" PRIu32 " Hz)", + this->sample_rate_, audio_stream_info.get_sample_rate()); + return ESP_ERR_NOT_SUPPORTED; + } + if (audio_stream_info.get_bits_per_sample() != 16) { + ESP_LOGE(TAG, "Only supports 16 bits per sample"); + return ESP_ERR_NOT_SUPPORTED; + } + if (audio_stream_info.get_channels() != 2) { + ESP_LOGE(TAG, "Only supports stereo (2 channels)"); + return ESP_ERR_NOT_SUPPORTED; + } + + if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO && + (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { + ESP_LOGE(TAG, "Stream bits per sample must be less than or equal to the speaker's configuration"); + return ESP_ERR_NOT_SUPPORTED; + } + + if (!this->parent_->try_lock()) { + ESP_LOGE(TAG, "Parent bus is busy"); + return ESP_ERR_INVALID_STATE; + } + + i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT; + +#if SOC_CLK_APLL_SUPPORTED + if (this->use_apll_) { + clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL; + } +#endif // SOC_CLK_APLL_SUPPORTED + + // SPDIF mode: fixed configuration for BMC encoding + // For new driver, dma_frame_num is in I2S frames (8 bytes each for 32-bit stereo) + uint32_t dma_buffer_length = SPDIF_BLOCK_I2S_FRAMES; // One SPDIF block = 384 I2S frames = 3072 bytes + + // Log DMA configuration for debugging + ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames = %lu bytes total", (size_t) SPDIF_DMA_BUFFERS_COUNT, + (unsigned long) dma_buffer_length, + (unsigned long) (SPDIF_DMA_BUFFERS_COUNT * dma_buffer_length * 8)); // 8 bytes per frame for 32-bit stereo + + i2s_chan_config_t chan_cfg = { + .id = this->parent_->get_port(), + .role = this->i2s_role_, + .dma_desc_num = SPDIF_DMA_BUFFERS_COUNT, + .dma_frame_num = dma_buffer_length, + .auto_clear = true, + .intr_priority = 3, + }; + + // SPDIF: double sample rate for BMC, 32-bit stereo, only data pin needed + i2s_std_clk_config_t clk_cfg = { + .sample_rate_hz = this->sample_rate_ * 2, + .clk_src = clk_src, + .mclk_multiple = this->mclk_multiple_, + }; + + i2s_std_slot_config_t slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO); + + i2s_std_gpio_config_t gpio_cfg = { + .mclk = GPIO_NUM_NC, + .bclk = GPIO_NUM_NC, + .ws = GPIO_NUM_NC, + .dout = this->dout_pin_, + .din = GPIO_NUM_NC, + .invert_flags = + { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }; + + i2s_std_config_t std_cfg = { + .clk_cfg = clk_cfg, + .slot_cfg = slot_cfg, + .gpio_cfg = gpio_cfg, + }; + + esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, SPDIF_I2S_EVENT_QUEUE_COUNT); + if (err != ESP_OK) { + return err; + } + + // Channel is NOT enabled here. The speaker task will preload DMA buffers + // with SPDIF-encoded silence before enabling, ensuring the first data on + // the wire is valid SPDIF (not raw zeros from auto_clear) and preventing + // phantom DMA events before real audio data is available. + + return ESP_OK; +} + +} // namespace esphome::i2s_audio + +#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.h b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.h new file mode 100644 index 00000000000..ca7774123b4 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) + +#include "i2s_audio_speaker.h" +#include "spdif_encoder.h" + +namespace esphome::i2s_audio { + +/// @brief SPDIF speaker implementation. +/// Encodes PCM audio into IEC 60958-3 S/PDIF bitstream using BMC encoding, +/// outputting through a single I2S data pin. Maintains continuous output +/// (silence when no audio) to keep SPDIF receivers synchronized. +class I2SAudioSpeakerSPDIF : public I2SAudioSpeakerBase { + public: + void setup() override; + void dump_config() override; + + size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override; + + protected: + void run_speaker_task() override; + esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override; + void on_task_stopped() override; + + SPDIFEncoder *spdif_encoder_{nullptr}; + uint32_t spdif_silence_start_{0}; // Timestamp when silence mode started (0 = not in silence) +}; + +} // namespace esphome::i2s_audio + +#endif // USE_ESP32 && USE_I2S_AUDIO_SPDIF_MODE diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index a71b7db3baf..f34839a3140 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -257,7 +257,7 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize channel"); + ESP_LOGE(TAG, "Failed to initialize I2S channel"); i2s_del_channel(this->tx_handle_); this->tx_handle_ = nullptr; this->parent_->unlock(); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index c598ca1bf87..bfde455c755 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -18,7 +18,7 @@ namespace esphome::i2s_audio { -// Shared constants for I2S audio speaker implementations +// Shared constants used by both standard and SPDIF speaker implementations static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15; static constexpr size_t TASK_STACK_SIZE = 4096; static constexpr ssize_t TASK_PRIORITY = 19; @@ -42,7 +42,7 @@ enum SpeakerEventGroupBits : uint32_t { /// @brief Abstract base class for I2S audio speaker implementations. /// Provides shared infrastructure (event groups, ring buffer, volume control, task lifecycle) -/// for derived I2S speaker classes. +/// for derived standard I2S and SPDIF speaker classes. class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public Component { public: float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } diff --git a/esphome/components/i2s_audio/speaker/spdif_encoder.cpp b/esphome/components/i2s_audio/speaker/spdif_encoder.cpp new file mode 100644 index 00000000000..a853f934bb2 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/spdif_encoder.cpp @@ -0,0 +1,385 @@ +#include "spdif_encoder.h" + +#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) + +#include "esphome/core/log.h" + +namespace esphome::i2s_audio { + +static const char *const TAG = "i2s_audio.spdif_encoder"; + +// S/PDIF preamble patterns (8 BMC bits each) +// These are the BMC-encoded sync patterns that violate normal BMC rules for easy detection. +// All preambles end at phase HIGH (last bit = 1), enabling consistent data encoding. +// Preamble is placed at bits 24-31 of word[0] for MSB-first transmission. +static constexpr uint8_t PREAMBLE_B = 0x17; // Block start (left channel, frame 0) +static constexpr uint8_t PREAMBLE_M = 0x1d; // Left channel (not block start) +static constexpr uint8_t PREAMBLE_W = 0x1b; // Right channel + +// BMC encoding of 4 zero bits starting at phase HIGH: 00_11_00_11 = 0x33 +// Since both aux nibbles (bits 4-7, 8-11) are zero for 16-bit audio and phase is preserved, both are 0x33. +static constexpr uint32_t BMC_ZERO_NIBBLE = 0x33; + +// Constexpr BMC encoder for compile-time LUT generation. +// Encodes with start phase=true (HIGH). The complement property allows phase=false +// via XOR: bmc_encode(v, N, false) == bmc_encode(v, N, true) ^ mask +static constexpr uint16_t bmc_lut_encode(uint32_t data, uint8_t num_bits) { + uint16_t bmc = 0; + bool phase = true; + for (uint8_t i = 0; i < num_bits; i++) { + bool bit = (data >> i) & 1; + uint8_t bmc_pair = phase ? (bit ? 0b01 : 0b00) : (bit ? 0b10 : 0b11); + bmc |= static_cast<uint16_t>(bmc_pair) << ((num_bits - 1 - i) * 2); + if (!bit) + phase = !phase; + } + return bmc; +} + +// 4-bit BMC lookup table: 16 entries (16 bytes in flash) +// Index: 4-bit data value (0-15), always phase=true start +static constexpr auto BMC_LUT_4 = [] { + std::array<uint8_t, 16> t{}; + for (uint32_t i = 0; i < 16; i++) + t[i] = static_cast<uint8_t>(bmc_lut_encode(i, 4)); + return t; +}(); + +// 8-bit BMC lookup table: 256 entries (512 bytes in flash) +// Index: 8-bit data value (0-255), always phase=true start +static constexpr auto BMC_LUT_8 = [] { + std::array<uint16_t, 256> t{}; + for (uint32_t i = 0; i < 256; i++) + t[i] = bmc_lut_encode(i, 8); + return t; +}(); + +// Initialize S/PDIF buffer +bool SPDIFEncoder::setup() { + this->spdif_block_buf_ = std::make_unique<uint32_t[]>(SPDIF_BLOCK_SIZE_U32); + if (!this->spdif_block_buf_) { + ESP_LOGE(TAG, "Buffer allocation failed (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES); + return false; + } + ESP_LOGV(TAG, "Buffer allocated (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES); + + // Build initial channel status block with default sample rate + this->build_channel_status_(); + + this->reset(); + return true; +} + +void SPDIFEncoder::reset() { + this->spdif_block_ptr_ = this->spdif_block_buf_.get(); + this->frame_in_block_ = 0; + this->is_left_channel_ = true; +} + +void SPDIFEncoder::set_sample_rate(uint32_t sample_rate) { + if (this->sample_rate_ != sample_rate) { + this->sample_rate_ = sample_rate; + this->build_channel_status_(); + ESP_LOGD(TAG, "Sample rate set to %lu Hz", (unsigned long) sample_rate); + } +} + +void SPDIFEncoder::build_channel_status_() { + // IEC 60958-3 Consumer Channel Status Block (192 bits = 24 bytes) + // Transmitted LSB-first within each byte, one bit per frame via C bit + // + // Byte 0: Control bits + // Bit 0: 0 = Consumer format (not professional AES3) + // Bit 1: 0 = PCM audio (not non-audio data like AC3) + // Bit 2: 0 = No copyright assertion + // Bits 3-5: 000 = No pre-emphasis + // Bits 6-7: 00 = Mode 0 (basic consumer format) + // + // Byte 1: Category code (0x00 = general, 0x01 = CD, etc.) + // + // Byte 2: Source/channel numbers + // Bits 0-3: Source number (0 = unspecified) + // Bits 4-7: Channel number (0 = unspecified) + // + // Byte 3: Sample frequency and clock accuracy + // Bits 0-3: Sample frequency code + // Bits 4-5: Clock accuracy (00 = Level II, ±1000 ppm, appropriate for ESP32) + // Bits 6-7: Reserved (0) + // + // Bytes 4-23: Reserved (zeros for basic compliance) + + // Clear all bytes first + this->channel_status_.fill(0); + + // Byte 0: Consumer, PCM audio, no copyright, no pre-emphasis, Mode 0 + // All bits are 0, which is already set + + // Byte 1: Category code = 0x00 (general) + // Already 0 + + // Byte 2: Source/channel unspecified + // Already 0 + + // Byte 3: Sample frequency code (bits 0-3) + clock accuracy (bits 4-5) + // Clock accuracy = 00 (Level II, ±1000 ppm) - appropriate for ESP32 + uint8_t freq_code; + switch (this->sample_rate_) { + case 44100: + freq_code = 0x0; // 0000 + break; + case 48000: + freq_code = 0x2; // 0010 + break; + default: + // Other values are possible but they're not supported by ESPHome + freq_code = 0x1; // 0001 = not indicated + ESP_LOGW(TAG, "Unsupported sample rate %lu Hz, channel status will indicate 'not specified'", + (unsigned long) this->sample_rate_); + break; + } + // Byte 3: freq_code in bits 0-3, clock accuracy (00) in bits 4-5 + this->channel_status_[3] = freq_code; // Clock accuracy bits 4-5 are already 0 + + // Bytes 4-23 remain zero (word length not specified, no original sample freq, etc.) +} + +HOT void SPDIFEncoder::encode_sample_(const uint8_t *pcm_sample) { + // ============================================================================ + // Build raw 32-bit subframe (IEC 60958 format) + // ============================================================================ + // Bit layout: + // Bits 0-3: Preamble (handled separately, not in raw_subframe) + // Bits 4-7: Auxiliary audio data (zeros for 16-bit audio) + // Bits 8-11: Audio LSB extension (zeros for 16-bit audio) + // Bits 12-27: 16-bit audio sample (MSB-aligned in 20-bit audio field) + // Bit 28: V (Validity) - 0 = valid audio + // Bit 29: U (User data) - 0 + // Bit 30: C (Channel status) - from channel status block + // Bit 31: P (Parity) - even parity over bits 4-31 + // ============================================================================ + + // Place 16-bit audio sample at bits 12-27 (little-endian input: [0]=LSB, [1]=MSB) + uint32_t raw_subframe = (static_cast<uint32_t>(pcm_sample[1]) << 20) | (static_cast<uint32_t>(pcm_sample[0]) << 12); + + // V = 0 (valid audio), U = 0 (no user data) + // C = channel status bit for current frame (same bit used for both L and R subframes) + bool c_bit = this->get_channel_status_bit_(this->frame_in_block_); + if (c_bit) { + raw_subframe |= (1U << 30); + } + + // Calculate even parity over bits 4-30 + // This ensures consistent BMC ending phase regardless of audio content + uint32_t bits_4_30 = (raw_subframe >> 4) & 0x07FFFFFF; // 27 bits (4-30) + uint32_t ones_count = __builtin_popcount(bits_4_30); + uint32_t parity = ones_count & 1; // 1 if odd count, 0 if even + raw_subframe |= parity << 31; // Set P bit to make total even + + // ============================================================================ + // Select preamble based on position in block and channel + // ============================================================================ + // B = block start (left channel, frame 0 of 192-frame block) + // M = left channel (frames 1-191) + // W = right channel (all frames) + uint8_t preamble; + if (this->is_left_channel_) { + preamble = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M; + } else { + preamble = PREAMBLE_W; + } + + // ============================================================================ + // BMC encode the data portion (bits 4-31) using lookup tables + // ============================================================================ + // The I2S uses 16-bit halfword swap: bits 16-31 transmit before bits 0-15. + // This applies to BOTH word[0] and word[1]. + // + // word[0] transmission order: [16-23] → [24-31] → [0-7] → [8-15] + // For correct S/PDIF subframe order (preamble → aux → audio): + // - bits 16-23: preamble (8 BMC bits) + // - bits 24-31: BMC(subframe bits 4-7) - first aux nibble + // - bits 0-7: BMC(subframe bits 8-11) - second aux nibble + // - bits 8-15: BMC(subframe bits 12-15) - audio low nibble + // + // word[1] transmission order: [16-31] → [0-15] + // For correct S/PDIF subframe order: + // - bits 16-31: BMC(subframe bits 16-23) - audio mid byte + // - bits 0-15: BMC(subframe bits 24-31) - audio high nibble + VUCP + // ============================================================================ + + // All preambles end at phase HIGH. Bits 4-11 are always zero for 16-bit audio; + // two zero nibbles flip phase 8 times total → back to HIGH. + // So bits 12-15 always start encoding at phase=true. + + // Bits 12-15: 4-bit LUT lookup (always phase=true start) + uint32_t nibble = (raw_subframe >> 12) & 0xF; + uint32_t bmc_12_15 = BMC_LUT_4[nibble]; + + // Phase tracking via branchless XOR mask: + // - 0x0000 means phase=true (use LUT value directly) + // - 0xFFFF means phase=false (complement LUT value) + // End phase = start XOR (popcount & 1) since zero-bits flip phase, + // and for even bit widths: #zeros parity == popcount parity. + uint32_t phase_mask = -(__builtin_popcount(nibble) & 1u) & 0xFFFF; + + // Bits 16-23: 8-bit LUT lookup with phase correction + uint32_t byte_mid = (raw_subframe >> 16) & 0xFF; + uint32_t bmc_16_23 = BMC_LUT_8[byte_mid] ^ phase_mask; + phase_mask ^= -(__builtin_popcount(byte_mid) & 1u) & 0xFFFF; + + // Bits 24-31: 8-bit LUT lookup with phase correction + uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; + uint32_t bmc_24_31 = BMC_LUT_8[byte_hi] ^ phase_mask; + + // ============================================================================ + // Combine with correct positioning for I2S transmission + // ============================================================================ + // I2S with halfword swap: transmits bits 16-31, then bits 0-15. + // Within each halfword, MSB (highest bit) is transmitted first. + // + // For upper halfword (bits 16-31): bit 31 → bit 16 + // For lower halfword (bits 0-15): bit 15 → bit 0 + // + // Desired S/PDIF order: preamble → bmc_4_7 → bmc_8_11 → bmc_12_15 + // + // word[0] layout for correct transmission: + // bits 24-31: preamble (transmitted 1st, as MSB of upper halfword) + // bits 16-23: BMC_ZERO_NIBBLE (transmitted 2nd, aux bits 4-7) + // bits 8-15: BMC_ZERO_NIBBLE (transmitted 3rd, aux bits 8-11) + // bits 0-7: bmc_12_15 (transmitted 4th, audio low nibble) + // + // word[1] layout: + // bits 16-31: bmc_16_23 (transmitted 5th) + // bits 0-15: bmc_24_31 (transmitted 6th) + this->spdif_block_ptr_[0] = + bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast<uint32_t>(preamble) << 24); + this->spdif_block_ptr_[1] = bmc_24_31 | (bmc_16_23 << 16); + this->spdif_block_ptr_ += 2; + + // ============================================================================ + // Update position tracking + // ============================================================================ + if (!this->is_left_channel_) { + // Completed a stereo frame, advance frame counter + if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) { + this->frame_in_block_ = 0; + } + } + this->is_left_channel_ = !this->is_left_channel_; +} + +esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) { + // Use the appropriate callback and context based on preload mode + SPDIFBlockCallback callback; + void *ctx; + + if (this->preload_mode_) { + callback = this->preload_callback_; + ctx = this->preload_callback_ctx_; + } else { + callback = this->write_callback_; + ctx = this->write_callback_ctx_; + } + + if (callback == nullptr) { + return ESP_ERR_INVALID_STATE; + } + + esp_err_t err = callback(ctx, this->spdif_block_buf_.get(), SPDIF_BLOCK_SIZE_BYTES, ticks_to_wait); + + if (err == ESP_OK) { + // Reset pointer for next block; position tracking continues from where it left off + this->spdif_block_ptr_ = this->spdif_block_buf_.get(); + } + + return err; +} + +size_t SPDIFEncoder::get_pending_pcm_bytes() const { + if (this->spdif_block_ptr_ == nullptr || this->spdif_block_buf_ == nullptr) { + return 0; + } + // Each PCM sample (2 bytes) produces 2 uint32_t values in the SPDIF buffer + // So pending uint32s / 2 = pending samples, and each sample is 2 bytes + size_t pending_uint32s = this->spdif_block_ptr_ - this->spdif_block_buf_.get(); + size_t pending_samples = pending_uint32s / 2; + return pending_samples * 2; // 2 bytes per sample +} + +HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent, + size_t *bytes_consumed) { + const uint8_t *pcm_data = src; + const uint8_t *pcm_end = src + size; + uint32_t block_count = 0; + + while (pcm_data < pcm_end) { + // Check if there's a pending complete block from a previous failed send + if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + esp_err_t err = this->send_block_(ticks_to_wait); + if (err != ESP_OK) { + if (blocks_sent != nullptr) { + *blocks_sent = block_count; + } + if (bytes_consumed != nullptr) { + *bytes_consumed = pcm_data - src; + } + return err; + } + ++block_count; + } + + // Encode one 16-bit sample + this->encode_sample_(pcm_data); + pcm_data += 2; + } + + // Send any complete block that was just finished + if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + esp_err_t err = this->send_block_(ticks_to_wait); + if (err != ESP_OK) { + if (blocks_sent != nullptr) { + *blocks_sent = block_count; + } + if (bytes_consumed != nullptr) { + *bytes_consumed = pcm_data - src; + } + return err; + } + ++block_count; + } + + if (blocks_sent != nullptr) { + *blocks_sent = block_count; + } + if (bytes_consumed != nullptr) { + *bytes_consumed = size; + } + return ESP_OK; +} + +esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) { + // First, send any pending complete block from a previous failed send + if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + esp_err_t err = this->send_block_(ticks_to_wait); + if (err != ESP_OK) { + return err; + } + } + + if (!this->has_pending_data()) { + return ESP_OK; // Nothing to flush + } + + // Encode silence (zeros) until the block is complete + static const uint8_t SILENCE[2] = {0, 0}; + + while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + this->encode_sample_(SILENCE); + } + + return this->send_block_(ticks_to_wait); +} + +} // namespace esphome::i2s_audio + +#endif // USE_I2S_AUDIO_SPDIF_MODE diff --git a/esphome/components/i2s_audio/speaker/spdif_encoder.h b/esphome/components/i2s_audio/speaker/spdif_encoder.h new file mode 100644 index 00000000000..85166434329 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/spdif_encoder.h @@ -0,0 +1,146 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE) + +#include <array> +#include <cstdint> +#include <memory> +#include <freertos/FreeRTOS.h> +#include "esp_err.h" +#include "esphome/core/helpers.h" + +namespace esphome::i2s_audio { + +// A SPDIF sample is 64-bits +static constexpr uint8_t SPDIF_BITS_PER_SAMPLE = 64; +// Number of samples in a SPDIF block +static constexpr uint16_t SPDIF_BLOCK_SAMPLES = 192; +// To emulate bi-phase mark code (BMC) (aka differential Manchester encoding) we send twice +// as many bits per sample so that we can generate the transitions this encoding requires. +static constexpr uint8_t EMULATED_BMC_BITS_PER_SAMPLE = SPDIF_BITS_PER_SAMPLE * 2; +static constexpr uint16_t SPDIF_BLOCK_SIZE_BYTES = SPDIF_BLOCK_SAMPLES * (EMULATED_BMC_BITS_PER_SAMPLE / 8); +static constexpr uint32_t SPDIF_BLOCK_SIZE_U32 = SPDIF_BLOCK_SIZE_BYTES / sizeof(uint32_t); // 3072 bytes / 4 = 768 +// I2S frame count for one SPDIF block (for new driver where frame = 8 bytes for 32-bit stereo) +static constexpr uint32_t SPDIF_BLOCK_I2S_FRAMES = SPDIF_BLOCK_SIZE_BYTES / 8; // 3072 / 8 = 384 frames +// PCM bytes needed for one complete SPDIF block (192 stereo frames * 2 bytes per sample * 2 channels) +static constexpr uint16_t SPDIF_PCM_BYTES_PER_BLOCK = SPDIF_BLOCK_SAMPLES * 2 * 2; // = 768 bytes + +/// Callback signature for block completion (raw function pointer for minimal overhead) +/// @param user_ctx User context pointer passed during callback registration +/// @param data Pointer to SPDIF encoded block data +/// @param size Size of the block in bytes (always SPDIF_BLOCK_SIZE_BYTES) +/// @param ticks_to_wait FreeRTOS ticks to wait for write completion +/// @return ESP_OK on success, or an error code +using SPDIFBlockCallback = esp_err_t (*)(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait); + +class SPDIFEncoder { + public: + /// @brief Initialize the SPDIF working buffer + /// @return true if setup was successful, false if allocation failed + bool setup(); + + /// @brief Set callback for normal writes (used when channel is running) + /// @param callback Function pointer to call when a block is ready + /// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker) + void set_write_callback(SPDIFBlockCallback callback, void *user_ctx) { + this->write_callback_ = callback; + this->write_callback_ctx_ = user_ctx; + } + + /// @brief Set callback for preload writes (used when preloading to DMA before enabling channel) + /// @param callback Function pointer to call when a block is ready for preload + /// @param user_ctx Context pointer passed to callback (typically 'this' pointer of speaker) + void set_preload_callback(SPDIFBlockCallback callback, void *user_ctx) { + this->preload_callback_ = callback; + this->preload_callback_ctx_ = user_ctx; + } + + /// @brief Enable or disable preload mode + /// When in preload mode, completed blocks use the preload callback instead of write callback + void set_preload_mode(bool preload) { this->preload_mode_ = preload; } + + /// @brief Check if currently in preload mode + bool is_preload_mode() const { return this->preload_mode_; } + + /// @brief Convert PCM audio data to SPDIF BMC encoded data + /// @param src Source PCM audio data (16-bit stereo) + /// @param size Size of source data in bytes + /// @param ticks_to_wait Timeout for blocking writes + /// @param blocks_sent Optional pointer to receive the number of complete SPDIF blocks sent + /// @param bytes_consumed Optional pointer to receive the number of PCM bytes consumed from src + /// @return esp_err_t as returned from the callback + esp_err_t write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent = nullptr, + size_t *bytes_consumed = nullptr); + + /// @brief Get the number of PCM bytes currently pending in the partial block buffer + /// @return Number of pending PCM bytes (0 to SPDIF_PCM_BYTES_PER_BLOCK - 1) + size_t get_pending_pcm_bytes() const; + + /// @brief Get the number of PCM frames currently pending in the partial block buffer + /// @return Number of pending PCM frames (0 to SPDIF_BLOCK_SAMPLES - 1) + uint32_t get_pending_frames() const { return this->get_pending_pcm_bytes() / 4; } + + /// @brief Check if there is a partial block pending + bool has_pending_data() const { return this->spdif_block_ptr_ != this->spdif_block_buf_.get(); } + + /// @brief Flush any pending partial block by padding with silence and sending + /// @param ticks_to_wait Timeout for blocking writes + /// @return esp_err_t as returned from the callback, or ESP_OK if nothing to flush + esp_err_t flush_with_silence(TickType_t ticks_to_wait); + + /// @brief Reset the SPDIF block buffer and position tracking, discarding any partial block + void reset(); + + /// @brief Set the sample rate for Channel Status Block encoding + /// @param sample_rate Sample rate in Hz (e.g., 44100, 48000, 96000) + /// Call this before writing audio data to ensure correct channel status. + void set_sample_rate(uint32_t sample_rate); + + /// @brief Get the currently configured sample rate + uint32_t get_sample_rate() const { return this->sample_rate_; } + + protected: + /// @brief Encode a single 16-bit PCM sample into the current block position + HOT void encode_sample_(const uint8_t *pcm_sample); + + /// @brief Send the completed block via the appropriate callback + esp_err_t send_block_(TickType_t ticks_to_wait); + + /// @brief Build the channel status block from current configuration + void build_channel_status_(); + + /// @brief Get the channel status bit for a specific frame + /// @param frame Frame number (0-191) + /// @return The C bit value for this frame + ESPHOME_ALWAYS_INLINE inline bool get_channel_status_bit_(uint8_t frame) const { + // Channel status is 192 bits transmitted over 192 frames + // Bit N is transmitted in frame N, LSB-first within each byte + return (this->channel_status_[frame >> 3] >> (frame & 7)) & 1; + } + + // Member ordering optimized to minimize padding (largest alignment first) + + // 4-byte aligned members (pointers and uint32_t) + SPDIFBlockCallback write_callback_{nullptr}; + SPDIFBlockCallback preload_callback_{nullptr}; + void *write_callback_ctx_{nullptr}; + void *preload_callback_ctx_{nullptr}; + std::unique_ptr<uint32_t[]> spdif_block_buf_; // Working buffer for SPDIF block (heap allocated) + uint32_t *spdif_block_ptr_{nullptr}; // Current position in block buffer + uint32_t sample_rate_{48000}; // Sample rate for Channel Status Block encoding + + // 1-byte aligned members (grouped together to avoid internal padding) + uint8_t frame_in_block_{0}; // 0-191, tracks stereo frame position within block + bool is_left_channel_{true}; // Alternates L/R for stereo samples + bool preload_mode_{false}; // Whether to use preload callback vs write callback + + // Channel Status Block (192 bits = 24 bytes, transmitted over 192 frames) + // Placed last since std::array<uint8_t> has 1-byte alignment + std::array<uint8_t, 24> channel_status_{}; +}; + +} // namespace esphome::i2s_audio + +#endif // USE_I2S_AUDIO_SPDIF_MODE diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 85454d3cc00..162a6034b88 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -72,6 +72,7 @@ #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT +#define USE_I2S_AUDIO_SPDIF_MODE #define USE_IMAGE #define USE_IMPROV_SERIAL #define USE_IMPROV_SERIAL_NEXT_URL diff --git a/tests/components/speaker/spdif_mode.esp32-idf.yaml b/tests/components/speaker/spdif_mode.esp32-idf.yaml new file mode 100644 index 00000000000..4d6859feaed --- /dev/null +++ b/tests/components/speaker/spdif_mode.esp32-idf.yaml @@ -0,0 +1,25 @@ +substitutions: + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO12 + spdif_data_pin: GPIO4 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +i2s_audio: + - id: i2s_output + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: ${spdif_data_pin} + spdif_mode: true + use_apll: true + timeout: 2s + sample_rate: 48000 + bits_per_sample: 16bit + channel: stereo + i2s_mode: primary From 70b9edfabebb9a433c6730eb4e22bd26a9406e64 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Fri, 8 May 2026 17:26:09 -0400 Subject: [PATCH 1906/2056] [i2s_audio] Refactor SPDIF output, fixing synchronization problems (#16319) --- .../i2s_audio/speaker/i2s_audio_spdif.cpp | 434 ++++++------------ .../i2s_audio/speaker/i2s_audio_speaker.cpp | 44 +- .../i2s_audio/speaker/i2s_audio_speaker.h | 8 +- .../i2s_audio/speaker/spdif_encoder.cpp | 24 +- .../i2s_audio/speaker/spdif_encoder.h | 5 +- 5 files changed, 205 insertions(+), 310 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp index e2146de63ce..d257dd1d8f4 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp @@ -23,35 +23,14 @@ static const char *const TAG = "i2s_audio.spdif"; // 15 buffers x 4ms = 60ms of DMA buffering (same as 4 x 15ms for standard) static constexpr size_t SPDIF_DMA_BUFFERS_COUNT = 15; -// Timeout for flushing pending frames if no callback received. -static constexpr uint32_t SPDIF_FLUSH_TIMEOUT_MS = 20; - // Number of DMA events between upstream callbacks (~16ms = 4 events x 4ms each). // Matches non-SPDIF timing to prevent overwhelming upstream sync algorithms. static constexpr uint32_t SPDIF_DMA_EVENTS_PER_CALLBACK = 4; -// Consider TX stalled only if no DMA callbacks have arrived for this long. -// Zero-block non-blocking writes alone are not sufficient (they can happen when DMA is simply full). -static constexpr uint32_t SPDIF_STALL_NO_DMA_MS = 80; - -// Fallback stall detector: force recovery if silence writes make no forward progress for too long, -// even if occasional DMA callbacks are still observed. -static constexpr uint32_t SPDIF_STALL_ZERO_PROGRESS_MS = 1000; - -// Minimum spacing between re-prime attempts to avoid churn. -static constexpr uint32_t SPDIF_REPRIME_COOLDOWN_MS = 500; - -// Small waits used in SPDIF mode to keep DMA fed during rapid pipeline churn. -static constexpr uint32_t SPDIF_EMPTY_READ_DELAY_MS = 1; -static constexpr uint32_t SPDIF_SILENCE_LOOP_DELAY_MS = 1; +// Brief retry wait used by play() to catch short free-space windows during rapid track transitions. static constexpr uint32_t SPDIF_PLAY_RETRY_WAIT_MS = 5; -static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = SPDIF_DMA_BUFFERS_COUNT + 1; - -// Static silence buffer for SPDIF continuous mode -// 192 samples * 2 channels * 2 bytes per sample = 768 bytes -// Stored in flash (.rodata section) to avoid stack/heap usage -static const int16_t SPDIF_SILENCE_BUFFER[SPDIF_BLOCK_SAMPLES * 2] = {0}; +static constexpr size_t SPDIF_I2S_EVENT_QUEUE_COUNT = 2 * SPDIF_DMA_BUFFERS_COUNT; // Static callback functions for SPDIF encoder (avoids std::function overhead) static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) { @@ -59,7 +38,7 @@ static esp_err_t spdif_preload_cb(void *user_ctx, uint32_t *data, size_t size, T size_t bytes_written = 0; esp_err_t err = i2s_channel_preload_data(speaker->get_tx_handle(), data, size, &bytes_written); if (err != ESP_OK || bytes_written != size) { - ESP_LOGW(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); + ESP_LOGV(TAG, "Preload failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); return (err != ESP_OK) ? err : ESP_ERR_NO_MEM; } return ESP_OK; @@ -69,9 +48,8 @@ static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, Tic auto *speaker = static_cast<I2SAudioSpeakerSPDIF *>(user_ctx); size_t bytes_written = 0; esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait); - // ESP_ERR_TIMEOUT is expected under DMA backpressure in SPDIF mode. - if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { - ESP_LOGW(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); + if (err != ESP_OK) { + ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); } return err; } @@ -157,6 +135,9 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { this->spdif_encoder_->reset(); } + // Reset lockstep records queue so it starts paired with the (also-reset) i2s_event_queue_. + xQueueReset(this->write_records_queue_); + const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * SPDIF_DMA_BUFFERS_COUNT; // Ensure ring buffer duration is at least the duration of all DMA buffers const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); @@ -188,19 +169,16 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { // Preload DMA buffers with SPDIF-encoded silence before enabling the channel. // This ensures the first data transmitted is valid SPDIF (not raw zeros from // auto_clear) and prevents phantom DMA events before real audio is available. - // Track how many buffers were preloaded so the DMA event loop can skip - // frame accounting until the preloaded silence has fully drained. - uint32_t preload_buffers_remaining = 0; + // Each preloaded block pushes a 0-real-frame record so that the corresponding + // on_sent events drain in lockstep without crediting any audio frames. this->spdif_encoder_->set_preload_mode(true); for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) { - uint32_t preload_blocks = 0; - esp_err_t preload_err = this->spdif_encoder_->write(reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), - sizeof(SPDIF_SILENCE_BUFFER), - pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks); - if (preload_err != ESP_OK || preload_blocks == 0) { - break; // DMA buffers full or error + esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + if (preload_err != ESP_OK) { + break; // DMA preload buffer full or error } - preload_buffers_remaining += preload_blocks; + const uint32_t silence_record = 0; + xQueueSendToBack(this->write_records_queue_, &silence_record, 0); } this->spdif_encoder_->set_preload_mode(false); this->spdif_encoder_->reset(); // Clean encoder state for the main loop @@ -211,299 +189,193 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); i2s_channel_enable(this->tx_handle_); - bool stop_gracefully = false; - bool tx_dma_underflow = true; + // Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer). + // We drain real PCM up to one block from the ring buffer and silence-pad any remainder. + // Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard + // I2S speaker pattern (PR #16317): fill what you can, then silence-pad whatever is still + // missing to complete the DMA buffer. + const uint32_t block_duration_us = this->current_stream_info_.frames_to_microseconds(SPDIF_BLOCK_SAMPLES); + // Sized to absorb the worst case where every DMA buffer is full when we issue the write. + const TickType_t write_timeout_ticks = + pdMS_TO_TICKS(((block_duration_us * (SPDIF_DMA_BUFFERS_COUNT + 1)) + 999) / 1000); + // Brief read budget when the ring buffer is empty (~half a block). + const TickType_t read_timeout_ticks = pdMS_TO_TICKS(((block_duration_us / 2) + 999) / 1000); - uint32_t frames_written = 0; - - // SPDIF Continuous Silence Mode + Callback Decimation - // - // Key principles: - // 1. NEVER stop the I2S channel - always output a valid SPDIF stream - // 2. When no audio data, output silence-encoded SPDIF blocks (not zeros!) - // 3. Fire callbacks every 4 DMA events (~16ms), matching non-SPDIF timing - // - // This eliminates gaps that cause SPDIF receivers to re-sync, and reduces - // callback rate to prevent overwhelming upstream sync algorithms. - const uint32_t spdif_callback_threshold = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + // SPDIF Callback Decimation: fire every 4th DMA event (~16ms), matching non-SPDIF timing. uint32_t spdif_pending_frames = 0; int64_t spdif_pending_timestamp = 0; - uint32_t spdif_last_callback_time = millis(); - // Count DMA events for decimation uint32_t spdif_dma_event_count = 0; - uint32_t spdif_last_dma_event_time = millis(); - // Detect a stalled DMA path (many silence write attempts with zero accepted blocks). - uint32_t spdif_zero_block_streak = 0; - uint32_t spdif_last_block_progress_time = millis(); - uint32_t spdif_last_reprime_time = 0; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); // SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data - // to keep the receiver synced. Exits only via break (stream info change or silence timeout). + // to keep the receiver synced. Exits only via break (stream info change, silence timeout, + // lockstep desync, dropped event, or partial-write failure). while (true) { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - // In SPDIF continuous mode, don't tear down or expose STOPPED here. - // Keep the task alive and transition to silence output. + // The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion + // event; that desyncs the lockstep queues permanently and the only safe recovery is a full + // task restart. + if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { + ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep"); + break; + } + // User-initiated stop. In SPDIF continuous mode, transition to silence output rather + // than tearing the task down. this->spdif_silence_start_ = millis(); ESP_LOGV(TAG, "COMMAND_STOP received, continuing in silence mode"); } if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) { + // SPDIF continuous mode never tears the channel down on graceful stop. Clear the flag and + // let the audio simply drain through the always-fill loop into the silence-timeout path. xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); - stop_gracefully = true; } if (this->audio_stream_info_ != this->current_stream_info_) { - // Audio stream info changed, stop the speaker task so it will restart with the proper settings. ESP_LOGV(TAG, "Exiting: stream info changed"); break; } + // Drain ISR completion events, popping a matching record for each. int64_t write_timestamp; + bool lockstep_broken = false; while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { - spdif_last_dma_event_time = millis(); - - // Skip frame accounting for preloaded silence buffers still draining. - // These DMA events correspond to silence that was preloaded before the - // channel was enabled, not real audio written by the task. - if (preload_buffers_remaining > 0) { - preload_buffers_remaining--; - continue; + // Lockstep: pop the matching record (real audio frames packed into this DMA block). + // Records are pushed by the task right after each successful block commit, so the FIFO + // order matches DMA completion order. Empty records queue here means lockstep broke. + uint32_t real_frames = 0; + if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { + ESP_LOGV(TAG, "Event without matching write record"); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + lockstep_broken = true; + break; } - // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes - // on the timing info via the audio_output_callback. - uint32_t frames_sent = frames_to_fill_single_dma_buffer; - if (frames_to_fill_single_dma_buffer > frames_written) { - tx_dma_underflow = true; - frames_sent = frames_written; - const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; + // Per-block timestamp adjustment: shift back by the silence-padding portion of the block + // so the reported timestamp reflects when the last real sample left the wire. + uint32_t frames_sent = real_frames; + if (real_frames < SPDIF_BLOCK_SAMPLES) { + const uint32_t frames_zeroed = SPDIF_BLOCK_SAMPLES - real_frames; write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed); - } else { - tx_dma_underflow = false; } - frames_written -= frames_sent; - // SPDIF Callback Decimation: fire every 4th DMA event (~16ms) - // This matches non-SPDIF timing and prevents overwhelming upstream. - if (spdif_callback_threshold > 0) { - spdif_dma_event_count++; + spdif_dma_event_count++; + // Accumulate frames; keep the latest timestamp so the callback reports when the last + // sample left the wire, not the first. + if (frames_sent > 0) { + spdif_pending_timestamp = write_timestamp; + spdif_pending_frames += frames_sent; + } - // Accumulate frames; always keep the latest timestamp so the - // callback reports when the last sample left the wire, not the first. - if (frames_sent > 0) { - spdif_pending_timestamp = write_timestamp; - spdif_pending_frames += frames_sent; - } + bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK); + // Partial blocks mark an end-of-stream boundary (silence-padded tail). Fire immediately + // so the back-shifted timestamp isn't overwritten by a later full audio block landing + // in the same decimation window. + bool partial_flush = (real_frames > 0 && real_frames < SPDIF_BLOCK_SAMPLES); - // Fire callback every 4 DMA events, or on timeout if we have pending frames - bool decimation_reached = (spdif_dma_event_count >= SPDIF_DMA_EVENTS_PER_CALLBACK); - bool timeout_flush = - (spdif_pending_frames > 0) && ((millis() - spdif_last_callback_time) >= SPDIF_FLUSH_TIMEOUT_MS); - - if (decimation_reached || timeout_flush) { - if (spdif_pending_frames > 0) { - this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp); - spdif_pending_frames = 0; - spdif_last_callback_time = millis(); - } - spdif_dma_event_count = 0; // Reset decimation counter + if (decimation_reached || partial_flush) { + if (spdif_pending_frames > 0) { + this->audio_output_callback_(spdif_pending_frames, spdif_pending_timestamp); + spdif_pending_frames = 0; } + spdif_dma_event_count = 0; } } - - if (this->pause_state_) { - // Pause state is accessed atomically, so thread safe - // Delay so the task yields, then skip transferring audio data - vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); - continue; + if (lockstep_broken) { + ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task"); + break; } - // Wait half the duration of the data already written to the DMA buffers for new audio data - // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 - uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; + // Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write + // paces the task at the DMA consumption rate. + uint32_t real_frames_in_block = 0; + bool block_committed = false; + bool partial_write_failure = false; - // In SPDIF mode, if transfer buffer is empty (we're pumping silence), use a very short timeout. - // This ensures we can pump silence fast enough to keep the DMA fed (~250 blocks/sec needed). - // Otherwise the long timeout based on frames_written causes DMA to run dry. - if (transfer_buffer->available() == 0) { - read_delay = SPDIF_EMPTY_READ_DELAY_MS; - } - - size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); - uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; - - if (bytes_read > 0) { - this->apply_software_volume_(new_data, bytes_read); - this->swap_esp32_mono_samples_(new_data, bytes_read); - } - - if (transfer_buffer->available() == 0) { - // SPDIF Continuous Silence Mode: always output valid SPDIF stream - // When no audio data, write silence-encoded blocks to keep receiver happy - if (this->spdif_encoder_ != nullptr) { - // "Graceful stop" means "drain buffered audio, then stop." In SPDIF - // continuous mode we never actually stop, so once audio is drained - // (we're here), reset the flag to re-enable silence writing and stall - // recovery. Without this, stop_gracefully stays true forever and - // blocks silence output, causing DMA to degrade on auto_clear zeros. - stop_gracefully = false; - - // Track when we entered silence mode - if (this->spdif_silence_start_ == 0) { - this->spdif_silence_start_ = millis(); + if (!this->pause_state_) { + while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) { + if (transfer_buffer->available() == 0) { + size_t bytes_read = transfer_buffer->transfer_data_from_source(read_timeout_ticks); + if (bytes_read == 0) { + break; // No upstream data within the read budget; silence-pad the remainder. + } + uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; + this->apply_software_volume_(new_data, bytes_read); + this->swap_esp32_mono_samples_(new_data, bytes_read); } - // If silence persists past the configured timeout, stop the task - // so components expecting timeout semantics can recover. - if (this->timeout_.has_value()) { - const uint32_t silence_duration = millis() - this->spdif_silence_start_; - if (silence_duration >= this->timeout_.value()) { - ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration); - break; - } - } + const uint32_t frames_still_needed = SPDIF_BLOCK_SAMPLES - real_frames_in_block; + const size_t bytes_still_needed = this->current_stream_info_.frames_to_bytes(frames_still_needed); + const size_t bytes_to_feed = std::min(transfer_buffer->available(), bytes_still_needed); - // First flush any partial block with silence padding (non-blocking to avoid getting stuck). - // IMPORTANT: Credit any partial block frames to frames_written so the audio_output_callback_ - // fires for them. Without this, pending_playback_frames_ in the mixer's SourceSpeaker never - // reaches 0 when a stream ends on a non-192-frame boundary, permanently blocking teardown. - if (this->spdif_encoder_->has_pending_data()) { - uint32_t partial_frames = this->spdif_encoder_->get_pending_frames(); - // Use a tiny timeout to allow DMA queue progress without stalling the task. - esp_err_t flush_err = this->spdif_encoder_->flush_with_silence(pdMS_TO_TICKS(1)); - if (flush_err == ESP_OK && partial_frames > 0) { - frames_written += partial_frames; - } - } - - // CRITICAL: In SPDIF continuous mode, ALWAYS write silence when no audio data. - // We don't check tx_dma_underflow because: - // 1. When DMA runs empty, callbacks stop, so tx_dma_underflow doesn't update - // 2. The non-blocking write handles "DMA full" gracefully (just doesn't write) - // 3. We need continuous output to prevent receiver from losing sync - if (!stop_gracefully) { - uint32_t silence_blocks = 0; - esp_err_t write_err = this->spdif_encoder_->write( - reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER), pdMS_TO_TICKS(1), - &silence_blocks); // Non-blocking - // Don't count silence as frames_written - it's not real audio - - // Recovery path for a stalled SPDIF TX channel: - // if silence writes repeatedly produce zero blocks AND DMA callbacks have stopped, - // re-prime DMA using preload mode. - const uint32_t ms_since_dma = millis() - spdif_last_dma_event_time; - const bool dma_events_stalled = ms_since_dma >= SPDIF_STALL_NO_DMA_MS; - if (silence_blocks > 0) { - spdif_last_block_progress_time = millis(); - } - const bool long_zero_progress = (millis() - spdif_last_block_progress_time) >= SPDIF_STALL_ZERO_PROGRESS_MS; - if (dma_events_stalled && silence_blocks == 0 && (write_err == ESP_OK || write_err == ESP_ERR_TIMEOUT)) { - spdif_zero_block_streak++; - } else { - spdif_zero_block_streak = 0; - } - - const uint32_t now_ms = millis(); - const bool reprime_cooldown_elapsed = - (spdif_last_reprime_time == 0) || ((now_ms - spdif_last_reprime_time) >= SPDIF_REPRIME_COOLDOWN_MS); - - if ((spdif_zero_block_streak >= 100 || long_zero_progress) && reprime_cooldown_elapsed) { - ESP_LOGV(TAG, "TX appears stalled, attempting DMA re-prime"); - - i2s_channel_disable(this->tx_handle_); - - const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr}; - i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this); - - this->spdif_encoder_->set_preload_mode(true); - uint32_t preload_blocks = 0; - esp_err_t preload_err = this->spdif_encoder_->write( - reinterpret_cast<const uint8_t *>(SPDIF_SILENCE_BUFFER), sizeof(SPDIF_SILENCE_BUFFER), - pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &preload_blocks); - this->spdif_encoder_->set_preload_mode(false); - - xQueueReset(this->i2s_event_queue_); - const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - i2s_channel_enable(this->tx_handle_); - - if (preload_err == ESP_OK && preload_blocks > 0) { - tx_dma_underflow = false; - preload_buffers_remaining = preload_blocks; - frames_written = 0; // Stale after channel disable/enable cycle - ESP_LOGV(TAG, "DMA re-prime successful (%" PRIu32 " preload blocks)", preload_blocks); - spdif_last_block_progress_time = now_ms; - } else { - ESP_LOGW(TAG, "DMA re-prime failed (%s, blocks=%" PRIu32 ")", esp_err_to_name(preload_err), - preload_blocks); - } - spdif_last_reprime_time = now_ms; - spdif_zero_block_streak = 0; - } - } - } - - if (stop_gracefully && tx_dma_underflow) { - // In SPDIF continuous mode, don't break on graceful stop during silence - // Keep outputting silence until new audio arrives or explicit COMMAND_STOP - // (handled above which transitions to silence mode rather than breaking) - } - - // In SPDIF mode, use a shorter delay to pump silence faster - // We need ~250 blocks/sec to keep DMA fed, so max 4ms per iteration - vTaskDelay(pdMS_TO_TICKS(SPDIF_SILENCE_LOOP_DELAY_MS)); - } else { - // Have audio data to write - size_t bytes_written = 0; - - // Clear silence timer since we have audio data now - if (this->spdif_silence_start_ != 0) { - uint32_t silence_duration = millis() - this->spdif_silence_start_; - if (silence_duration > 100) { - ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration); - } - this->spdif_silence_start_ = 0; - } - - { uint32_t blocks_sent = 0; - size_t pcm_bytes_consumed = 0; - - // Write audio data to encoder (which writes to DMA) - esp_err_t err = - this->spdif_encoder_->write(transfer_buffer->get_buffer_start(), transfer_buffer->available(), - pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS), &blocks_sent, &pcm_bytes_consumed); - if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { - ESP_LOGW(TAG, "Write failed: %s", esp_err_to_name(err)); + size_t pcm_consumed = 0; + esp_err_t err = this->spdif_encoder_->write(transfer_buffer->get_buffer_start(), bytes_to_feed, + write_timeout_ticks, &blocks_sent, &pcm_consumed); + if (err != ESP_OK) { + // A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer; + // resuming would credit the next iteration's bytes against an old block. Bail and + // let loop() restart the task with a clean encoder. + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + partial_write_failure = true; + break; } - // Only consume source bytes that were actually accepted by the encoder. - bytes_written = pcm_bytes_consumed; - - // Update frame accounting based on complete blocks sent (192 frames per block) - if (bytes_written > 0) { - frames_written += blocks_sent * SPDIF_BLOCK_SAMPLES; - transfer_buffer->decrease_buffer_length(bytes_written); - // Audio blocks count as DMA progress for the stall detector. - // Without this, a long uninterrupted audio stream makes the - // progress timer stale, triggering a spurious re-prime the - // instant we transition to silence. - spdif_last_block_progress_time = millis(); + if (pcm_consumed > 0) { + transfer_buffer->decrease_buffer_length(pcm_consumed); + real_frames_in_block += this->current_stream_info_.bytes_to_frames(pcm_consumed); + } + if (blocks_sent > 0) { + block_committed = true; + break; } } } - } - // If we reach here, the while loop exited - either via break or condition became false - // In SPDIF mode, loop exit is expected when: - // 1. Timeout reached (user configured timeout) - // 2. Stream info changed - // Only warn if timeout is "never" since that should never exit - if (!this->timeout_.has_value()) { - ESP_LOGW(TAG, "Unexpected loop exit; set 'timeout: never' to prevent this"); + + if (partial_write_failure) { + break; + } + + if (!block_committed) { + // Pad whatever real audio we managed to feed (if any) with silence to complete one block, + // or emit a full silence block if the encoder is empty. + esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks); + if (err != ESP_OK) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + break; + } + } + + // One block committed to DMA; push exactly one record carrying its real-audio frame count. + // Failure here means the records queue is full, which violates the lockstep invariant. + if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + break; + } + + // Silence-timeout tracking and graceful-stop reset. + if (real_frames_in_block == 0) { + if (this->spdif_silence_start_ == 0) { + this->spdif_silence_start_ = millis(); + } + + if (this->timeout_.has_value()) { + const uint32_t silence_duration = millis() - this->spdif_silence_start_; + if (silence_duration >= this->timeout_.value()) { + ESP_LOGV(TAG, "Silence timeout reached (%" PRIu32 "ms) - stopping speaker", silence_duration); + break; + } + } + } else if (this->spdif_silence_start_ != 0) { + uint32_t silence_duration = millis() - this->spdif_silence_start_; + if (silence_duration > 100) { + ESP_LOGV(TAG, "Exiting silence mode after %" PRIu32 "ms, have audio data", silence_duration); + } + this->spdif_silence_start_ = 0; + } } } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index f34839a3140..27961050e6c 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -69,6 +69,17 @@ void I2SAudioSpeakerBase::loop() { } if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { ESP_LOGV(TAG, "Stopping"); + // Lockstep-breaking error bits are latched by the task and cleared along with all other bits + // when TASK_STOPPED is processed; log them here, exactly once, as the task winds down. + if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { + ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync"); + } + if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) { + ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task"); + } + if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) { + ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task"); + } xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); this->state_ = speaker::STATE_STOPPING; } @@ -87,18 +98,11 @@ void I2SAudioSpeakerBase::loop() { this->state_ = speaker::STATE_STOPPED; } - // Log any errors encountered by the task if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { ESP_LOGE(TAG, "Not enough memory"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); } - // Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy - if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) { - ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT); - } - // Handle the speaker's state switch (this->state_) { case speaker::STATE_STARTING: @@ -271,6 +275,22 @@ esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_c xQueueReset(this->i2s_event_queue_); } + // Lockstep records queue. One record per in-flight DMA buffer; sized to match the I2S event queue + // so a fully-saturated DMA pipeline cannot overflow either side before drain. + if (this->write_records_queue_ == nullptr) { + this->write_records_queue_ = xQueueCreate(event_queue_size, sizeof(uint32_t)); + } else { + xQueueReset(this->write_records_queue_); + } + + if (this->i2s_event_queue_ == nullptr || this->write_records_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate I2S event queue(s)"); + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; + this->parent_->unlock(); + return ESP_ERR_NO_MEM; + } + return ESP_OK; } @@ -293,10 +313,16 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx; if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { - // Queue is full, so discard the oldest event and set the warning flag to inform the user + // Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_`` + // and any per-buffer record queue maintained by the task are permanently desynced, so the task + // must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP + // (so the task bails immediately, closing the race where loop() could clear the error bit + // before the task observes it). int64_t dummy; xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1); - xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2); + xEventGroupSetBitsFromISR(this_speaker->event_group_, + SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP, + &need_yield2); } xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index bfde455c755..c57af2775b3 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -35,7 +35,11 @@ enum SpeakerEventGroupBits : uint32_t { ERR_ESP_NO_MEM = (1 << 19), - WARN_DROPPED_EVENT = (1 << 20), + ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event + ERR_PARTIAL_WRITE = (1 << 21), // a DMA write returned fewer bytes than requested (or the encoder + // failed to commit a complete block), which breaks the lockstep + // invariant for every subsequent event + ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; @@ -141,7 +145,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; + // Lockstepped DMA buffer queues: i2s_event is outgoing, write_records is incoming QueueHandle_t i2s_event_queue_{nullptr}; + QueueHandle_t write_records_queue_{nullptr}; std::weak_ptr<ring_buffer::RingBuffer> audio_ring_buffer_; diff --git a/esphome/components/i2s_audio/speaker/spdif_encoder.cpp b/esphome/components/i2s_audio/speaker/spdif_encoder.cpp index a853f934bb2..42a72346ccb 100644 --- a/esphome/components/i2s_audio/speaker/spdif_encoder.cpp +++ b/esphome/components/i2s_audio/speaker/spdif_encoder.cpp @@ -358,25 +358,15 @@ HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ti } esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) { - // First, send any pending complete block from a previous failed send - if (this->spdif_block_ptr_ >= &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { - esp_err_t err = this->send_block_(ticks_to_wait); - if (err != ESP_OK) { - return err; + // If a complete block is already pending (from a previous failed send), emit just that block. + // Otherwise pad the partial block with silence (or generate a full silence block if empty) + // and send. Always emits exactly one block on success. + if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + static const uint8_t SILENCE[2] = {0, 0}; + while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { + this->encode_sample_(SILENCE); } } - - if (!this->has_pending_data()) { - return ESP_OK; // Nothing to flush - } - - // Encode silence (zeros) until the block is complete - static const uint8_t SILENCE[2] = {0, 0}; - - while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) { - this->encode_sample_(SILENCE); - } - return this->send_block_(ticks_to_wait); } diff --git a/esphome/components/i2s_audio/speaker/spdif_encoder.h b/esphome/components/i2s_audio/speaker/spdif_encoder.h index 85166434329..8c5e0688419 100644 --- a/esphome/components/i2s_audio/speaker/spdif_encoder.h +++ b/esphome/components/i2s_audio/speaker/spdif_encoder.h @@ -85,9 +85,10 @@ class SPDIFEncoder { /// @brief Check if there is a partial block pending bool has_pending_data() const { return this->spdif_block_ptr_ != this->spdif_block_buf_.get(); } - /// @brief Flush any pending partial block by padding with silence and sending + /// @brief Emit one complete SPDIF block: pad any pending partial block with silence and send, + /// or send a full silence block if nothing is pending. Always produces exactly one block on success. /// @param ticks_to_wait Timeout for blocking writes - /// @return esp_err_t as returned from the callback, or ESP_OK if nothing to flush + /// @return esp_err_t as returned from the callback esp_err_t flush_with_silence(TickType_t ticks_to_wait); /// @brief Reset the SPDIF block buffer and position tracking, discarding any partial block From 3abf2c99a2421827980fd5454458de295efab2ae Mon Sep 17 00:00:00 2001 From: rwrozelle <rwrozelle@gmail.com> Date: Fri, 8 May 2026 17:35:47 -0400 Subject: [PATCH 1907/2056] [openthread] add coroutine-with-priority COMMUNICATION (#16318) --- esphome/components/openthread/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 21373b77dfb..bc1e91d6dac 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -21,7 +21,12 @@ from esphome.const import ( CONF_USE_ADDRESS, PLATFORM_ESP32, ) -from esphome.core import CORE, TimePeriodMilliseconds +from esphome.core import ( + CORE, + CoroPriority, + TimePeriodMilliseconds, + coroutine_with_priority, +) import esphome.final_validate as fv from esphome.types import ConfigType @@ -223,6 +228,7 @@ def _final_validate(_): FINAL_VALIDATE_SCHEMA = _final_validate +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): # Re-enable openthread IDF component (excluded by default) include_builtin_idf_component("openthread") From 136525136565860489670ff9ca6c3ccbd0bc81e7 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Fri, 8 May 2026 21:36:06 +0000 Subject: [PATCH 1908/2056] [ota] Add bootloader update functionality to ota component (#16238) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/__main__.py | 66 +++++-- .../components/esphome/ota/ota_esphome.cpp | 17 +- esphome/components/ota/ota_backend.h | 3 + .../components/ota/ota_backend_esp_idf.cpp | 34 +++- esphome/components/ota/ota_backend_esp_idf.h | 13 ++ .../components/ota/ota_bootloader_esp_idf.cpp | 133 +++++++++++++ .../components/ota/ota_partitions_esp_idf.cpp | 53 +++--- esphome/core/__init__.py | 6 + esphome/espota2.py | 32 +++- tests/unit_tests/test_core.py | 17 ++ tests/unit_tests/test_espota2.py | 68 ++++++- tests/unit_tests/test_main.py | 179 +++++++++++++++++- 12 files changed, 570 insertions(+), 51 deletions(-) create mode 100644 esphome/components/ota/ota_bootloader_esp_idf.cpp diff --git a/esphome/__main__.py b/esphome/__main__.py index 825a502dbf3..c1451c5fafd 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1125,11 +1125,18 @@ def upload_program( # MQTT and MQTTIP are also OTA paths; MQTTIP gets resolved to a real IP later by # _resolve_network_devices(). Only SERIAL and BOOTSEL are non-OTA upload paths. - if port_type in (PortType.SERIAL, PortType.BOOTSEL) and getattr( - args, "partition_table", False + is_partition_table = getattr(args, "partition_table", False) + is_bootloader = getattr(args, "bootloader", False) + if is_partition_table and is_bootloader: + raise EsphomeError( + "The options --partition-table and --bootloader can't be used together." + ) + option_string = "--partition-table" if is_partition_table else "--bootloader" + if port_type in (PortType.SERIAL, PortType.BOOTSEL) and ( + is_partition_table or is_bootloader ): raise EsphomeError( - "The option --partition-table can only be used for Over The Air updates." + f"The option {option_string} can only be used for Over The Air updates." ) if port_type == PortType.BOOTSEL: @@ -1158,9 +1165,9 @@ def upload_program( network_devices = _resolve_network_devices(devices, config, args) if chosen_platform == CONF_WEB_SERVER: - if getattr(args, "partition_table", False): + if is_partition_table or is_bootloader: raise EsphomeError( - "--partition-table is only supported with the esphome OTA platform; " + f"{option_string} is only supported with the esphome OTA platform; " "the web_server OTA path can only update the firmware image." ) binary = CORE.firmware_bin @@ -1228,25 +1235,34 @@ def _upload_via_native_api( remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) + def check_partition_access(option_string: str) -> None: + if not ota_conf.get("allow_partition_access"): + raise EsphomeError( + f"The option {option_string} requires 'allow_partition_access: true' on the " + "esphome OTA platform in the device's YAML configuration. Add it, recompile, " + f"flash a build with the option enabled, and then retry {option_string}." + ) + binary = CORE.firmware_bin ota_type = espota2.OTA_TYPE_UPDATE_APP if getattr(args, "partition_table", False): # Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access. # The device-side handshake also rejects this with "Device only supports app updates", # but checking here surfaces the misconfiguration before opening a network connection. - if not ota_conf.get("allow_partition_access"): - raise EsphomeError( - "The option --partition-table requires 'allow_partition_access: true' on the " - "esphome OTA platform in the device's YAML configuration. Add it, recompile, " - "flash a build with the option enabled, and then retry --partition-table." - ) + check_partition_access("--partition-table") binary = CORE.partition_table_bin ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE + elif getattr(args, "bootloader", False): + check_partition_access("--bootloader") + binary = CORE.bootloader_bin + ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER if getattr(args, "file", None) is not None: binary = Path(args.file) if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE: _validate_partition_table_binary(binary) + if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER: + _validate_bootloader_binary(binary) return espota2.run_ota(network_devices, remote_port, password, binary, ota_type) @@ -1281,6 +1297,7 @@ def _upload_via_web_server( _PARTITION_TABLE_MAX_LEN = 0xC00 _ESP_PARTITION_MAGIC = 0x50AA _ESP_PARTITION_MAGIC_MD5 = 0xEBEB +_ESP_IMAGE_HEADER_MAGIC = 0xE9 def _validate_partition_table_binary(binary: Path) -> None: @@ -1326,6 +1343,28 @@ def _validate_partition_table_binary(binary: Path) -> None: ) +def _validate_bootloader_binary(binary: Path) -> None: + """Validate that ``binary`` looks like an ESP32 bootloader image.""" + try: + data = binary.read_bytes() + except OSError as err: + raise EsphomeError(f"Cannot read bootloader file '{binary}': {err}") from err + + if not data: + raise EsphomeError( + f"Bootloader file '{binary}' is empty. " + "This file does not look like an ESP32 bootloader." + ) + + first_magic = data[0] + if first_magic != _ESP_IMAGE_HEADER_MAGIC: + raise EsphomeError( + f"Bootloader file '{binary}' does not start with the expected " + f"image header magic 0x{_ESP_IMAGE_HEADER_MAGIC:02X} (got 0x{first_magic:02X}). " + "This file does not look like an ESP32 bootloader." + ) + + def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: try: module = importlib.import_module("esphome.components." + CORE.target_platform) @@ -2009,6 +2048,11 @@ def parse_args(argv): help="Upload as partition table (OTA).", action="store_true", ) + parser_upload.add_argument( + "--bootloader", + help="Upload as bootloader (OTA).", + action="store_true", + ) parser_logs = subparsers.add_parser( "logs", diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 5d3deca4896..843028fc974 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -125,8 +125,11 @@ void ESPHomeOTAComponent::dump_config() { it = esp_partition_next(it); } esp_partition_iterator_release(it); -#endif -#endif + esp_bootloader_desc_t bootloader_desc; + esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc); + ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown"); +#endif // USE_ESP32 +#endif // USE_OTA_PARTITIONS } void ESPHomeOTAComponent::loop() { @@ -336,7 +339,6 @@ void ESPHomeOTAComponent::handle_data_() { /// wakeable_delay() in read(); /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; - bool update_started = false; size_t total = 0; uint32_t last_progress = 0; uint32_t last_data_ms = 0; @@ -399,7 +401,6 @@ void ESPHomeOTAComponent::handle_data_() { error_code = this->backend_->begin(ota_size, ota_type); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) - update_started = true; // Acknowledge prepare OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); @@ -510,8 +511,12 @@ void ESPHomeOTAComponent::handle_data_() { error: this->write_byte_(static_cast<uint8_t>(error_code)); - // Abort backend before cleanup - cleanup_connection_() destroys the backend - if (this->backend_ != nullptr && update_started) { + // Abort backend before cleanup - cleanup_connection_() destroys the backend. + // Always call abort() unconditionally: backends register external partitions before + // esp_ota_begin (partition table / bootloader paths), and abort() is responsible for + // releasing those even if begin() failed before an OTA handle was opened. The IDF + // backend's esp_ota_abort(0) is documented as harmless. + if (this->backend_ != nullptr) { this->backend_->abort(); } diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 5888a8e12d4..de236c19513 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -44,6 +44,8 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E, OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F, OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90, + OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91, + OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; @@ -58,6 +60,7 @@ enum OTAState { enum OTAType : uint8_t { OTA_TYPE_UPDATE_APP = 0x00, OTA_TYPE_UPDATE_PARTITION_TABLE = 0x01, + OTA_TYPE_UPDATE_BOOTLOADER = 0x02, }; /** Listener interface for OTA state changes. diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 50a0988ba21..f391c1791aa 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -31,7 +31,13 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) this->md5_.init(); return OTA_RESPONSE_OK; } - if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) { + OTAResponseTypes result = this->prepare_bootloader_update_(image_size); + if (result != OTA_RESPONSE_OK) { + return result; + } + } + if (!this->is_app_or_bootloader_update_()) { return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } #else @@ -55,6 +61,7 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err); esp_ota_abort(this->update_handle_); this->update_handle_ = 0; if (err == ESP_ERR_INVALID_SIZE) { @@ -64,6 +71,14 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) } return OTA_RESPONSE_ERROR_UNKNOWN; } +#ifdef USE_OTA_PARTITIONS + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) { + OTAResponseTypes result = this->setup_bootloader_staging_(); + if (result != OTA_RESPONSE_OK) { + return result; + } + } +#endif this->md5_.init(); return OTA_RESPONSE_OK; } @@ -85,13 +100,14 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { this->md5_.add(data, len); return OTA_RESPONSE_OK; } - if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + if (!this->is_app_or_bootloader_update_()) { return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } #endif esp_err_t err = esp_ota_write(this->update_handle_, data, len); this->md5_.add(data, len); if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err); if (err == ESP_ERR_OTA_VALIDATE_FAILED) { return OTA_RESPONSE_ERROR_MAGIC; } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { @@ -114,12 +130,20 @@ OTAResponseTypes IDFOTABackend::end() { if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) { return this->update_partition_table(); } - if (this->ota_type_ != ota::OTA_TYPE_UPDATE_APP) { + if (!this->is_app_or_bootloader_update_()) { return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; } #endif esp_err_t err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end failed (err=0x%X)", err); + } +#ifdef USE_OTA_PARTITIONS + if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) { + return this->finalize_bootloader_update_(err); + } +#endif if (err == ESP_OK) { err = esp_ota_set_boot_partition(this->partition_); if (err == ESP_OK) { @@ -146,6 +170,10 @@ void IDFOTABackend::abort() { esp_partition_deregister_external(this->partition_table_part_); this->partition_table_part_ = nullptr; } + if (this->bootloader_part_ != nullptr) { + esp_partition_deregister_external(this->bootloader_part_); + this->bootloader_part_ = nullptr; + } #endif // esp_ota_abort with handle 0 returns ESP_ERR_INVALID_ARG harmlessly, so this is safe whether // or not an update is in flight. diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 54fdd24f934..73dd685df69 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -39,6 +39,18 @@ class IDFOTABackend final { OTAResponseTypes validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size, PartitionTablePlan &plan); OTAResponseTypes update_partition_table(); + OTAResponseTypes register_and_validate_partition_table_part_(); + // Defined in ota_bootloader_esp_idf.cpp: + OTAResponseTypes register_and_validate_bootloader_part_(); + OTAResponseTypes prepare_bootloader_update_(size_t image_size); + OTAResponseTypes setup_bootloader_staging_(); + OTAResponseTypes finalize_bootloader_update_(esp_err_t ota_end_err); + + // The OTA types that flow through esp_ota_begin/write/end. Partition-table updates take a + // separate code path that buffers the table in RAM and never touches the OTA handle. + bool is_app_or_bootloader_update_() const { + return this->ota_type_ == ota::OTA_TYPE_UPDATE_APP || this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER; + } #endif private: @@ -55,6 +67,7 @@ class IDFOTABackend final { size_t buf_written_{0}; size_t image_size_{0}; const esp_partition_t *partition_table_part_{nullptr}; + const esp_partition_t *bootloader_part_{nullptr}; ota::OTAType ota_type_{ota::OTA_TYPE_UPDATE_APP}; #endif }; diff --git a/esphome/components/ota/ota_bootloader_esp_idf.cpp b/esphome/components/ota/ota_bootloader_esp_idf.cpp new file mode 100644 index 00000000000..062e4d0811d --- /dev/null +++ b/esphome/components/ota/ota_bootloader_esp_idf.cpp @@ -0,0 +1,133 @@ +#ifdef USE_ESP32 +#include "ota_backend_esp_idf.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OTA_PARTITIONS +#include "esphome/core/log.h" + +#include <esp_image_format.h> +#include <esp_ota_ops.h> + +namespace esphome::ota { + +static const char *const TAG = "ota.idf"; + +OTAResponseTypes IDFOTABackend::register_and_validate_bootloader_part_() { + // Register the bootloader partition + esp_err_t err = esp_partition_register_external(nullptr, ESP_PRIMARY_BOOTLOADER_OFFSET, ESP_BOOTLOADER_SIZE, + "PrimaryBTLDR", ESP_PARTITION_TYPE_BOOTLOADER, + ESP_PARTITION_SUBTYPE_BOOTLOADER_PRIMARY, &this->bootloader_part_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_register_external failed (bootloader) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + + // Verify existing bootloader to make sure ESP_PRIMARY_BOOTLOADER_OFFSET is correct + esp_image_metadata_t data = {}; + const esp_partition_pos_t part_pos = { + .offset = this->bootloader_part_->address, + .size = this->bootloader_part_->size, + }; + err = esp_image_verify(ESP_IMAGE_VERIFY, &part_pos, &data); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_image_verify failed (existing bootloader) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + return OTA_RESPONSE_OK; +} + +// Pre-esp_ota_begin: enforce size limit, register/verify the existing bootloader, and validate the +// partition table to confirm the bootloader region is at the expected offset (and therefore the +// expected size). The partition table registration is released here; abort() cleans up the +// bootloader registration if any later step fails. +OTAResponseTypes IDFOTABackend::prepare_bootloader_update_(size_t image_size) { + if (image_size > ESP_BOOTLOADER_SIZE) { + ESP_LOGE(TAG, "Length of received data exceeds the available bootloader size: expected <=%zu bytes, got %zu", + ESP_BOOTLOADER_SIZE, image_size); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + OTAResponseTypes result = this->register_and_validate_bootloader_part_(); + if (result != OTA_RESPONSE_OK) { + return result; + } + result = this->register_and_validate_partition_table_part_(); + if (result != OTA_RESPONSE_OK) { + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + esp_partition_deregister_external(this->partition_table_part_); + this->partition_table_part_ = nullptr; + return OTA_RESPONSE_OK; +} + +// Post-esp_ota_begin: verify the staging app partition is large enough, erase it, and redirect the +// final write target to the bootloader partition. esp_ota_set_final_partition is called with +// `restore_old_data=false` because we erased the staging region in advance. +OTAResponseTypes IDFOTABackend::setup_bootloader_staging_() { + if (this->partition_->size < this->bootloader_part_->size) { + ESP_LOGE(TAG, "Staging partition too small"); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + // Erase full size of the bootloader partition in the staging partition + // to avoid copying old data to the bootloader partition later + esp_err_t err = esp_partition_erase_range(this->partition_, 0, this->bootloader_part_->size); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_partition_erase_range failed (err=0x%X)", err); + // No critical error, don't return + } + err = esp_ota_set_final_partition(this->update_handle_, this->bootloader_part_, false); + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + ESP_LOGE(TAG, "esp_ota_set_final_partition failed (err=0x%X)", err); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + return OTA_RESPONSE_OK; +} + +// After esp_ota_end: copy the staged image into the bootloader partition. esp_partition_copy is +// the only window in which a power loss can render the device unbootable; everything before this +// point either preserves the existing bootloader or fails harmlessly. After a successful copy the +// first sector of staging is wiped so the device can't accidentally boot from it, and the +// bootloader partition is deregistered. +OTAResponseTypes IDFOTABackend::finalize_bootloader_update_(esp_err_t ota_end_err) { + if (ota_end_err != ESP_OK) { + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } + esp_bootloader_desc_t bootloader_desc; + esp_err_t desc_err = esp_ota_get_bootloader_description(this->partition_, &bootloader_desc); +#ifdef USE_ESP32_SRAM1_AS_IRAM + if (desc_err != ESP_OK) { + ESP_LOGE(TAG, "New bootloader does not support SRAM1 as IRAM"); + return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY; + } +#endif + ESP_LOGE(TAG, "Starting bootloader update.\n" + " DO NOT REMOVE POWER until the update completes successfully.\n" + " Loss of power during this operation may render the device\n" + " unable to boot until it is recovered via a serial flash."); + esp_err_t err = esp_partition_copy(this->bootloader_part_, 0, this->partition_, 0, this->bootloader_part_->size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_copy failed (err=0x%X)", err); + // Only if esp_partition_copy failed there's a chance of the device being unbootable + return OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE; + } + ESP_LOGI(TAG, + "Successfully installed the new bootloader\n" + " ESP-IDF %s", + (desc_err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown"); + // Wipe first sector of staging partition to make sure the device can't boot from it + err = esp_partition_erase_range(this->partition_, 0, this->partition_->erase_size); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_partition_erase_range failed (err=0x%X)", err); + // No critical error, don't return + } + esp_partition_deregister_external(this->bootloader_part_); + this->bootloader_part_ = nullptr; + return OTA_RESPONSE_OK; +} + +} // namespace esphome::ota + +#endif // USE_OTA_PARTITIONS +#endif // USE_ESP32 diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index f7fd529986d..f91e88bde0a 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -45,32 +45,14 @@ static const esp_partition_t *find_app_partition_at(uint32_t address, size_t min // can write to it; abort() releases it on error. OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size, PartitionTablePlan &plan) { - esp_err_t err = esp_partition_register_external( - nullptr, ESP_PRIMARY_PARTITION_TABLE_OFFSET, ESP_PARTITION_TABLE_SIZE, "PrimaryPrtTable", - ESP_PARTITION_TYPE_PARTITION_TABLE, ESP_PARTITION_SUBTYPE_PARTITION_TABLE_PRIMARY, &this->partition_table_part_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_partition_register_external failed (err=0x%X)", err); - return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + OTAResponseTypes validate_result = this->register_and_validate_partition_table_part_(); + if (validate_result != OTA_RESPONSE_OK) { + return validate_result; } int num_partitions = 0; - const esp_partition_info_t *existing_partition_table = nullptr; - esp_partition_mmap_handle_t partition_table_map; - err = esp_partition_mmap(this->partition_table_part_, 0, ESP_PARTITION_TABLE_MAX_LEN, ESP_PARTITION_MMAP_DATA, - reinterpret_cast<const void **>(&existing_partition_table), &partition_table_map); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_partition_mmap failed (err=0x%X)", err); - return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; - } - err = esp_partition_table_verify(existing_partition_table, true, &num_partitions); - esp_partition_munmap(partition_table_map); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_partition_table_verify failed (existing partition table) (err=0x%X)", err); - return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; - } - const esp_partition_info_t *new_partition_table = reinterpret_cast<const esp_partition_info_t *>(this->buf_); - err = esp_partition_table_verify(new_partition_table, true, &num_partitions); + esp_err_t err = esp_partition_table_verify(new_partition_table, true, &num_partitions); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_partition_table_verify failed (new partition table) (err=0x%X)", err); return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; @@ -288,6 +270,33 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { return OTA_RESPONSE_OK; } +OTAResponseTypes IDFOTABackend::register_and_validate_partition_table_part_() { + esp_err_t err = esp_partition_register_external( + nullptr, ESP_PRIMARY_PARTITION_TABLE_OFFSET, ESP_PARTITION_TABLE_SIZE, "PrimaryPrtTable", + ESP_PARTITION_TYPE_PARTITION_TABLE, ESP_PARTITION_SUBTYPE_PARTITION_TABLE_PRIMARY, &this->partition_table_part_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_register_external failed (partition table) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + + int num_partitions = 0; + const esp_partition_info_t *existing_partition_table = nullptr; + esp_partition_mmap_handle_t partition_table_map; + err = esp_partition_mmap(this->partition_table_part_, 0, ESP_PARTITION_TABLE_MAX_LEN, ESP_PARTITION_MMAP_DATA, + reinterpret_cast<const void **>(&existing_partition_table), &partition_table_map); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_mmap failed (partition table) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + err = esp_partition_table_verify(existing_partition_table, true, &num_partitions); + esp_partition_munmap(partition_table_map); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_partition_table_verify failed (existing partition table) (err=0x%X)", err); + return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; + } + return OTA_RESPONSE_OK; +} + // Process-scoped cache. Cannot be a backend member: backends are per-connection but the cache // must outlive a connection that called esp_partition_unload_all(), after which // esp_ota_get_running_partition() no longer returns valid data. diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 94a48dd31b0..0cc207aa543 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -790,6 +790,12 @@ class EsphomeCore: ) return self.relative_pioenvs_path(self.name, "partitions.bin") + @property + def bootloader_bin(self) -> Path: + if self.data.get(KEY_NATIVE_IDF): + return self.relative_build_path("build", "bootloader", "bootloader.bin") + return self.relative_pioenvs_path(self.name, "bootloader.bin") + @property def target_platform(self): return self.data[KEY_CORE][KEY_TARGET_PLATFORM] diff --git a/esphome/espota2.py b/esphome/espota2.py index b2a1fd2a40a..576b1c6b2da 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -17,6 +17,7 @@ from esphome.helpers import ProgressBar, resolve_ip_address OTA_TYPE_UPDATE_APP = 0x00 OTA_TYPE_UPDATE_PARTITION_TABLE = 0x01 +OTA_TYPE_UPDATE_BOOTLOADER = 0x02 RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -49,6 +50,8 @@ RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE = 0x8E RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90 +RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91 +RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92 RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -66,7 +69,7 @@ SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02 # updates extend this set. Anything outside the set is rejected up front so callers # of perform_ota/run_ota get a clear error instead of a post-auth 0x8E from the device. _SUPPORTED_OTA_TYPES: frozenset[int] = frozenset( - {OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE} + {OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE, OTA_TYPE_UPDATE_BOOTLOADER} ) UPLOAD_BLOCK_SIZE = 8192 @@ -143,6 +146,16 @@ _ERROR_MESSAGES: dict[int, str] = { "the partition table update without rebooting the device. If the device " "fails to boot, recover it via a serial flash." ), + RESPONSE_ERROR_BOOTLOADER_VERIFY: ( + "The bootloader update could not be verified. No changes were " + "made to the bootloader. Check the logs for more information and retry." + ), + RESPONSE_ERROR_BOOTLOADER_UPDATE: ( + "An error occurred while updating the bootloader. The device is now " + "in a degraded state and may not be able to boot. Open the logs and retry " + "the bootloader update without rebooting the device. If the device " + "fails to boot, recover it via a serial flash." + ), RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", } @@ -325,15 +338,24 @@ def perform_ota( # Any non-app OTA type requires the extended protocol and the # partition-access server feature. Reject up front so the user gets # a clear capability error instead of a post-auth 0x8E from the device. + flag_name = { + OTA_TYPE_UPDATE_PARTITION_TABLE: "--partition-table", + OTA_TYPE_UPDATE_BOOTLOADER: "--bootloader", + }.get(ota_type, f"OTA type 0x{ota_type:02X}") if not extended_proto: raise OTAError( - f"Device does not support extended OTA protocol; " - f"OTA type 0x{ota_type:02X} requires it" + f"Device does not support the extended OTA protocol that " + f"{flag_name} requires. The running firmware is too old; " + f"recompile and upload a current ESPHome firmware via a " + f"regular OTA (without {flag_name}), then retry." ) if not (features & SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS): raise OTAError( - f"Device does not support partition access; " - f"OTA type 0x{ota_type:02X} cannot be used" + f"The running firmware was built without " + f"'allow_partition_access: true', so {flag_name} cannot be " + f"used. Add the option to the esphome OTA platform in your " + f"YAML, recompile and upload (without {flag_name}), then " + f"retry {flag_name}." ) if features & SERVER_FEATURE_SUPPORTS_COMPRESSION: diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 22be59653aa..9dc37918ae9 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -853,6 +853,23 @@ class TestEsphomeCore: target.testing_ensure_platform_registered("sensor") assert target.platform_counts["sensor"] == 3 + def test_bootloader_bin__native_idf(self, target): + """Native ESP-IDF builds emit the bootloader under build/bootloader/bootloader.bin.""" + target.data[const.KEY_NATIVE_IDF] = True + + assert target.bootloader_bin == Path( + "foo/build/build/bootloader/bootloader.bin" + ) + + def test_bootloader_bin__platformio(self, target): + """For PlatformIO builds bootloader.bin lives in the env-specific .pioenvs directory.""" + target.name = "test-device" + target.data[const.KEY_NATIVE_IDF] = False + + assert target.bootloader_bin == Path( + "foo/build/.pioenvs/test-device/bootloader.bin" + ) + def test_add_library__extracts_short_name_from_path(self, target): """Test add_library extracts short name from library paths like owner/lib.""" target.data[const.KEY_CORE] = { diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 2cad1d2ec81..b22ad461132 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -201,6 +201,14 @@ def test_receive_exactly_socket_error(mock_socket: Mock) -> None: espota2.RESPONSE_ERROR_PARTITION_TABLE_UPDATE, "Error: An error occurred while updating the partition table", ), + ( + espota2.RESPONSE_ERROR_BOOTLOADER_VERIFY, + "Error: The bootloader update could not be verified", + ), + ( + espota2.RESPONSE_ERROR_BOOTLOADER_UPDATE, + "Error: An error occurred while updating the bootloader", + ), (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), ], ) @@ -992,7 +1000,8 @@ def test_perform_ota_non_app_type_requires_extended_protocol( mock_socket.recv.side_effect = recv_responses with pytest.raises( - espota2.OTAError, match="Device does not support extended OTA protocol" + espota2.OTAError, + match="Device does not support the extended OTA protocol", ): espota2.perform_ota( mock_socket, @@ -1026,7 +1035,8 @@ def test_perform_ota_non_app_type_requires_partition_access( mock_socket.recv.side_effect = recv_responses with pytest.raises( - espota2.OTAError, match="Device does not support partition access" + espota2.OTAError, + match=(r"running firmware was built without 'allow_partition_access: true'"), ): espota2.perform_ota( mock_socket, @@ -1037,6 +1047,60 @@ def test_perform_ota_non_app_type_requires_partition_access( ) +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_partition_access_error_names_bootloader_flag( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Bootloader OTA against a stale device must point at the --bootloader flag.""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), + bytes([espota2.OTA_VERSION_2_0]), + bytes([espota2.RESPONSE_FEATURE_FLAGS]), + bytes([0]), # No partition access + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, + match=r"--bootloader.*recompile and upload.*--bootloader.*retry --bootloader", + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + espota2.OTA_TYPE_UPDATE_BOOTLOADER, + ) + + +@pytest.mark.usefixtures("mock_time") +def test_perform_ota_partition_access_error_names_partition_table_flag( + mock_socket: Mock, mock_file: io.BytesIO +) -> None: + """Partition-table OTA against a stale device must point at the --partition-table flag.""" + recv_responses = [ + bytes([espota2.RESPONSE_OK]), + bytes([espota2.OTA_VERSION_2_0]), + bytes([espota2.RESPONSE_FEATURE_FLAGS]), + bytes([0]), # No partition access + ] + + mock_socket.recv.side_effect = recv_responses + + with pytest.raises( + espota2.OTAError, + match=r"--partition-table.*retry --partition-table", + ): + espota2.perform_ota( + mock_socket, + "testpass", + mock_file, + "test.bin", + espota2.OTA_TYPE_UPDATE_PARTITION_TABLE, + ) + + def test_check_error_detects_errors_when_expect_is_none() -> None: """check_error must surface device error bytes even when expect is None. diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 4ab7bb3344b..4b0590cf76d 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -24,6 +24,7 @@ from esphome.__main__ import ( _get_configured_xtal_freq, _make_crystal_freq_callback, _resolve_network_devices, + _validate_bootloader_binary, _validate_partition_table_binary, choose_upload_log_host, command_analyze_memory, @@ -89,7 +90,11 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError -from esphome.espota2 import OTA_TYPE_UPDATE_APP, OTA_TYPE_UPDATE_PARTITION_TABLE +from esphome.espota2 import ( + OTA_TYPE_UPDATE_APP, + OTA_TYPE_UPDATE_BOOTLOADER, + OTA_TYPE_UPDATE_PARTITION_TABLE, +) from esphome.util import BootselResult, FlashImage from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -1127,6 +1132,7 @@ class MockArgs: output: str | None = None ota_platform: str | None = None partition_table: bool = False + bootloader: bool = False def test_upload_program_serial_esp32( @@ -1816,6 +1822,27 @@ def test_validate_partition_table_binary_missing_file(tmp_path: Path) -> None: _validate_partition_table_binary(tmp_path / "does-not-exist.bin") +def test_validate_bootloader_binary_rejects_wrong_magic(tmp_path: Path) -> None: + data = bytearray(_make_bootloader_bytes()) + data[0] = 0x00 + f = tmp_path / "bootloader.bin" + f.write_bytes(bytes(data)) + with pytest.raises(EsphomeError, match="magic"): + _validate_bootloader_binary(f) + + +def test_validate_bootloader_binary_missing_file(tmp_path: Path) -> None: + with pytest.raises(EsphomeError, match="Cannot read bootloader file"): + _validate_bootloader_binary(tmp_path / "does-not-exist.bin") + + +def test_validate_bootloader_binary_rejects_empty_file(tmp_path: Path) -> None: + f = tmp_path / "bootloader.bin" + f.write_bytes(b"") + with pytest.raises(EsphomeError, match="is empty"): + _validate_bootloader_binary(f) + + def test_upload_program_ota_partition_table_invalid_file( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -1869,7 +1896,155 @@ def test_upload_program_ota_partition_table_without_allow_flag( with pytest.raises( EsphomeError, - match="requires 'allow_partition_access: true'", + match=( + r"The option --partition-table requires 'allow_partition_access: true'.*" + r"retry --partition-table" + ), + ): + upload_program(config, args, devices) + mock_run_ota.assert_not_called() + + +def _make_bootloader_bytes() -> bytes: + """Build a minimal bootloader image accepted by _validate_bootloader_binary.""" + table = bytearray(b"\xff") + # Starts with: ESP_IMAGE_HEADER_MAGIC (0xE9) + table[0] = 0xE9 + return bytes(table) + + +def test_upload_program_ota_bootloader_with_file_arg( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Test upload_program with OTA and bootloader.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + bootloader_file = tmp_path / "bootloader.bin" + bootloader_file.write_bytes(_make_bootloader_bytes()) + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file=str(bootloader_file), bootloader=True) + devices = ["192.168.1.100"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.100" + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], + 3232, + None, + bootloader_file, + OTA_TYPE_UPDATE_BOOTLOADER, + ) + + +def test_upload_program_ota_partition_table_and_bootloader_options( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--partition-table and --bootloader can't be used together.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file="partitions.bin", partition_table=True, bootloader=True) + devices = ["192.168.1.100"] + + with pytest.raises( + EsphomeError, + match="--partition-table and --bootloader", + ): + upload_program(config, args, devices) + mock_run_ota.assert_not_called() + + +def test_upload_program_ota_bootloader_without_allow_flag( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """--bootloader must fail fast when allow_partition_access is not enabled in YAML.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ] + } + args = MockArgs(file="bootloader.bin", bootloader=True) + devices = ["192.168.1.100"] + + with pytest.raises( + EsphomeError, + match=( + r"The option --bootloader requires 'allow_partition_access: true'.*" + r"retry --bootloader" + ), + ): + upload_program(config, args, devices) + mock_run_ota.assert_not_called() + + +def test_upload_program_ota_bootloader_platform_web_server( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Test bootloader upload with web_server OTA.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_get_port_type.return_value = "NETWORK" + + bootloader_file = tmp_path / "bootloader.bin" + bootloader_file.write_bytes(_make_bootloader_bytes()) + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_WEB_SERVER: { + CONF_PORT: 80, + CONF_AUTH: {CONF_USERNAME: "admin", CONF_PASSWORD: "pw"}, + }, + "allow_partition_access": True, + } + ] + } + args = MockArgs(file=str(bootloader_file), bootloader=True) + devices = ["192.168.1.100"] + + with pytest.raises( + EsphomeError, + match="the web_server OTA path can only update the firmware image", ): upload_program(config, args, devices) mock_run_ota.assert_not_called() From ed10fbea3e920ed241ff691d350f7531b015525c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 May 2026 07:25:49 +1200 Subject: [PATCH 1909/2056] [docker] Silence CopyIgnoredFile warning for build context root (#16311) --- .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index ccd466d8cba..d6fb5e82ae0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -115,4 +115,4 @@ examples/ Dockerfile .git/ tests/ -.* +.?* From 3c042e2e442330d6d2e6f2cb51c92e5064019e46 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 May 2026 09:58:18 +1000 Subject: [PATCH 1910/2056] [lvgl] Ensure that `on_value` events fire on checked change (#16119) --- esphome/components/lvgl/__init__.py | 38 ++-- esphome/components/lvgl/automation.py | 17 +- esphome/components/lvgl/defines.py | 130 +++++++++++--- esphome/components/lvgl/encoders.py | 7 +- esphome/components/lvgl/gradient.py | 10 +- esphome/components/lvgl/helpers.py | 25 --- esphome/components/lvgl/keypads.py | 4 +- esphome/components/lvgl/light/lvgl_light.h | 2 +- esphome/components/lvgl/lv_validation.py | 27 ++- esphome/components/lvgl/lvcode.py | 3 +- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/lvgl/lvgl_esphome.h | 33 +++- esphome/components/lvgl/number/__init__.py | 3 +- esphome/components/lvgl/schemas.py | 8 +- esphome/components/lvgl/sensor/__init__.py | 2 - esphome/components/lvgl/styles.py | 14 +- esphome/components/lvgl/switch/__init__.py | 9 +- esphome/components/lvgl/text/__init__.py | 3 +- .../components/lvgl/text_sensor/__init__.py | 10 +- esphome/components/lvgl/touchscreens.py | 8 +- esphome/components/lvgl/trigger.py | 8 +- esphome/components/lvgl/widgets/__init__.py | 85 +++------ esphome/components/lvgl/widgets/animimg.py | 5 +- esphome/components/lvgl/widgets/button.py | 3 +- .../components/lvgl/widgets/buttonmatrix.py | 24 +-- esphome/components/lvgl/widgets/dropdown.py | 4 +- esphome/components/lvgl/widgets/keyboard.py | 14 +- esphome/components/lvgl/widgets/meter.py | 5 +- esphome/components/lvgl/widgets/msgbox.py | 2 +- esphome/components/lvgl/widgets/roller.py | 2 - .../lvgl/config/widget_state_test.yaml | 83 +++++++++ .../component_tests/lvgl/test_widget_state.py | 167 ++++++++++++++++++ 32 files changed, 532 insertions(+), 227 deletions(-) create mode 100644 tests/component_tests/lvgl/config/widget_state_test.yaml create mode 100644 tests/component_tests/lvgl/test_widget_state.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index ac0363ca696..31131d253f9 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -47,9 +47,15 @@ from esphome.helpers import write_file_if_changed from esphome.writer import clean_build from esphome.yaml_util import load_yaml -from . import defines as df, helpers, lv_validation as lvalid, widgets -from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets -from .defines import CONF_ALIGN_TO_LAMBDA_ID +from . import defines as df, lv_validation as lvalid, widgets +from .automation import layers_to_code, lvgl_update +from .defines import ( + CONF_ALIGN_TO_LAMBDA_ID, + get_focused_widgets, + get_lv_images_used, + get_refreshed_widgets, + set_widgets_completed, +) from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -58,7 +64,7 @@ from .encoders import ( ) from .gradient import GRADIENT_SCHEMA, gradients_to_code from .keypads import KEYPADS_CONFIG, keypads_to_code -from .lv_validation import lv_bool, lv_images_used +from .lv_validation import lv_bool from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( DISP_BG_SCHEMA, @@ -89,7 +95,6 @@ from .widgets import ( add_widgets, get_screen_active, set_obj_properties, - styles_used, ) # Import only what we actually use directly in this file @@ -144,7 +149,7 @@ def generate_lv_conf_h(): df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES) ) # Get the defines that are actually used based on the config - lv_defines = df.get_data(df.KEY_LV_DEFINES) + lv_defines = df.get_defines() unused_defines = all_defines - set(lv_defines) # Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [ @@ -211,7 +216,7 @@ def final_validation(config_list): buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config: df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM") - for w in focused_widgets: + for w in get_focused_widgets(): path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) if ( @@ -222,7 +227,7 @@ def final_validation(config_list): "A non adjustable arc may not be focused", path, ) - for w in refreshed_widgets: + for w in get_refreshed_widgets(): path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()): @@ -230,7 +235,7 @@ def final_validation(config_list): f"Widget '{w}' does not have any dynamic properties to refresh", ) # Do per-widget type final validation for update actions - for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items(): + for widget_type, update_configs in df.get_updated_widgets().items(): for conf in update_configs: for id_conf in conf.get(CONF_ID, ()): name = id_conf[CONF_ID] @@ -279,7 +284,7 @@ async def to_code(configs): cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) - for font in helpers.lv_fonts_used: + for font in df.get_lv_fonts_used(): df.add_define(f"LV_FONT_{font.upper()}") if config_0[CONF_COLOR_DEPTH] == 16: @@ -294,7 +299,7 @@ async def to_code(configs): cg.add_build_flag("-Isrc") cg.add_global(lvgl_ns.using) - for font in helpers.esphome_fonts_used: + for font in df.get_esphome_fonts_used(): await cg.get_variable(font) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): @@ -377,8 +382,8 @@ async def to_code(configs): await lvgl_update(lv_component, config) await msgboxes_to_code(lv_component, config) # await disp_update(lv_component.get_disp(), config) - # Set this directly since we are limited in how many methods can be added to the Widget class. - Widget.widgets_completed = True + # Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed. + set_widgets_completed(True) async with LvContext(): await generate_triggers() await generate_align_tos(configs[0]) @@ -404,9 +409,8 @@ async def to_code(configs): ) # This must be done after all widgets are created - for comp in helpers.lvgl_components_required: - cg.add_define(f"USE_LVGL_{comp.upper()}") - for use in helpers.lv_uses: + styles_used = df.get_styles_used() + for use in df.get_lv_uses(): df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") @@ -433,7 +437,7 @@ async def to_code(configs): } & styles_used: lv_image_formats.add("A8") - for image_id in lv_images_used: + for image_id in get_lv_images_used(): await cg.get_variable(image_id) metadata = get_image_metadata(image_id.id) image_type = IMAGE_TYPE[metadata.image_type] diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 977f1af9b49..bf9a3d74ad3 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -25,7 +25,9 @@ from .defines import ( PARTS, StaticCastExpression, add_warning, + get_focused_widgets, get_options, + get_refreshed_widgets, ) from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( @@ -70,9 +72,9 @@ from .widgets import ( wait_for_widgets, ) -# Record widgets that are used in a focused action here -focused_widgets = set() -refreshed_widgets = set() +# Widgets that are used in a focused/refreshed action are tracked in +# ``CORE.data`` (under the lvgl domain) so the state is cleared between +# successive compilations / unit tests via ``CORE.reset()``. async def layers_to_code(lv_component, config): @@ -316,7 +318,7 @@ async def resume_action_to_code(config, action_id, template_arg, args): ) async def obj_disable_to_code(config, action_id, template_arg, args): async def do_disable(widget: Widget): - widget.add_state(LV_STATE.DISABLED) + widget.set_state(LV_STATE.DISABLED, True) return await action_to_code( await get_widgets(config), do_disable, action_id, template_arg, args @@ -328,7 +330,7 @@ async def obj_disable_to_code(config, action_id, template_arg, args): ) async def obj_enable_to_code(config, action_id, template_arg, args): async def do_enable(widget: Widget): - widget.clear_state(LV_STATE.DISABLED) + widget.set_state(LV_STATE.DISABLED, False) return await action_to_code( await get_widgets(config), do_enable, action_id, template_arg, args @@ -361,7 +363,7 @@ async def obj_show_to_code(config, action_id, template_arg, args): def focused_id(value): value = cv.use_id(lv_pseudo_button_t)(value) - focused_widgets.add(value) + get_focused_widgets().add(value) return value @@ -446,8 +448,9 @@ async def obj_update_to_code(config, action_id, template_arg, args): def validate_refresh_config(config): + refreshed = get_refreshed_widgets() for w in config: - refreshed_widgets.add(w[CONF_ID]) + refreshed.add(w[CONF_ID]) return config diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 03bbaf8ddb4..7bfd26bb6ec 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -19,46 +19,134 @@ from esphome.cpp_generator import ( from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType -from .helpers import requires_component - LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") DOMAIN = "lvgl" KEY_COLOR_FORMATS = "color_formats" +KEY_ESPHOME_FONTS_USED = "esphome_fonts_used" +KEY_FOCUSED_WIDGETS = "focused_widgets" KEY_LV_DEFINES = "lv_defines" +KEY_LV_FONTS_USED = "lv_fonts_used" +KEY_LV_IMAGES_USED = "lv_images_used" +KEY_LV_USES = "lv_uses" +KEY_NAMED_STYLES = "named_styles" +KEY_REFRESHED_WIDGETS = "refreshed_widgets" KEY_REMAPPED_USES = "remapped_uses" +KEY_STYLES_USED = "styles_used" +KEY_THEME_WIDGET_MAP = "theme_widget_map" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_WIDGET_MAP = "widget_map" +KEY_WIDGETS_COMPLETED = "widgets_completed" KEY_OPTIONS = "options" KEY_WARNINGS = "warnings" +# Initial set of LVGL features that are always enabled. +_INITIAL_LV_USES = frozenset( + { + "USER_DATA", + "LOG", + "STYLE", + "FONT_PLACEHOLDER", + "THEME_DEFAULT", + } +) -def get_data(key, default=None): + +# These collections accumulate state across a single compilation run. They +# are stored under ``CORE.data`` (which ``CORE.reset()`` clears between runs) +# rather than as module-level globals, otherwise they would leak between +# successive compilations / unit tests. + + +def _get_data(key: str, default: Any) -> Any: """ Get a data structure from the global data store by key :param key: A key for the data - :param default: The default data - the default is an empty dict + :param default: The default data :return: """ - return CORE.data.setdefault(DOMAIN, {}).setdefault( - key, {} if default is None else default - ) + return CORE.data.setdefault(DOMAIN, {}).setdefault(key, default) -def get_warnings(): - return get_data(KEY_WARNINGS, set()) +def get_lv_images_used() -> set[ID]: + return _get_data(KEY_LV_IMAGES_USED, set()) -def get_remapped_uses(): - return get_data(KEY_REMAPPED_USES, set()) +def get_lv_uses() -> set[str]: + return _get_data(KEY_LV_USES, set(_INITIAL_LV_USES)) -def add_warning(msg: str): +def get_lv_fonts_used() -> set[str]: + return _get_data(KEY_LV_FONTS_USED, set()) + + +def get_esphome_fonts_used() -> set[ID]: + return _get_data(KEY_ESPHOME_FONTS_USED, set()) + + +def add_lv_use(*names: str) -> None: + uses = get_lv_uses() + for name in names: + uses.add(name) + + +def get_warnings() -> set[str]: + return _get_data(KEY_WARNINGS, set()) + + +def get_remapped_uses() -> set[str]: + return _get_data(KEY_REMAPPED_USES, set()) + + +def add_warning(msg: str) -> None: get_warnings().add(msg) -def get_options(): - return get_data(KEY_OPTIONS) +def get_options() -> dict[str, Any]: + return _get_data(KEY_OPTIONS, {}) + + +def get_defines() -> dict[str, str]: + return _get_data(KEY_LV_DEFINES, {}) + + +def get_updated_widgets() -> dict: + return _get_data(KEY_UPDATED_WIDGETS, {}) + + +def get_theme_widget_map() -> dict[str, Any]: + return _get_data(KEY_THEME_WIDGET_MAP, {}) + + +def get_styles_used() -> set[str]: + return _get_data(KEY_STYLES_USED, set()) + + +def get_widget_map() -> dict[str, Any]: + return _get_data(KEY_WIDGET_MAP, {}) + + +def get_widgets_completed() -> bool: + # ``[value]`` rather than the bare value so that we can mutate the + # entry in place; ``CORE.data`` is reset for us between runs. + return _get_data(KEY_WIDGETS_COMPLETED, [False])[0] + + +def set_widgets_completed(value: bool) -> None: + _get_data(KEY_WIDGETS_COMPLETED, [False])[0] = value + + +def is_widget_completed(name: ID) -> bool: + return name in get_widget_map() + + +def get_focused_widgets() -> set: + return _get_data(KEY_FOCUSED_WIDGETS, set()) + + +def get_refreshed_widgets() -> set: + return _get_data(KEY_REFRESHED_WIDGETS, set()) class StaticCastExpression(Expression): @@ -72,8 +160,8 @@ class StaticCastExpression(Expression): return f"static_cast<{self.type}>({self.exp})" -def add_define(macro, value="1"): - lv_defines = get_data(KEY_LV_DEFINES) +def add_define(macro: str, value="1"): + lv_defines = get_defines() value = str(value) if lv_defines.setdefault(macro, value) != value: LOGGER.error( @@ -82,8 +170,8 @@ def add_define(macro, value="1"): lv_defines[macro] = value -def is_defined(macro): - return macro in get_data(KEY_LV_DEFINES) +def is_defined(macro) -> bool: + return macro in get_defines() def literal(arg) -> MockObj: @@ -96,7 +184,7 @@ def addr(arg) -> MockObj: return MockObj(f"&{arg}") -def call_lambda(lamb: LambdaExpression): +def call_lambda(lamb: LambdaExpression) -> Expression: """ Given a lambda, either reduce to a simple expression or call it, possibly with parameters from the surrounding context @@ -135,7 +223,7 @@ class LValidator: def __call__(self, value): if self.requires: - value = requires_component(self.requires)(value) + value = cv.requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) return self.validator(value) @@ -196,7 +284,7 @@ class LvConstant(LValidator): cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper ) - def mapper(self, value): + def mapper(self, value) -> Any: if not isinstance(value, list): value = [value] value = [ diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index bafda8382ee..e6527bbc9b5 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -13,8 +13,8 @@ from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, CONF_RIGHT_BUTTON, + add_lv_use, ) -from .helpers import lvgl_components_required, requires_component from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable from .schemas import ENCODER_SCHEMA from .types import lv_group_t, lv_indev_type_t, lv_key_t @@ -26,7 +26,8 @@ ENCODERS_CONFIG = cv.ensure_list( cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), cv.Required(CONF_SENSOR): cv.Any( cv.All( - cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder") + cv.use_id(RotaryEncoderSensor), + cv.requires_component("rotary_encoder"), ), cv.Schema( { @@ -48,7 +49,7 @@ def get_default_group(config): async def encoders_to_code(var, config, default_group): for enc_conf in config[CONF_ENCODERS]: - lvgl_components_required.add("KEY_LISTENER") + add_lv_use("KEY_LISTENER", "ROTARY_ENCODER") lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds listener = cg.new_Pvariable( diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index e075433d03e..2f1be207722 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -12,8 +12,14 @@ from esphome.const import ( from esphome.core import ID from esphome.cpp_generator import MockObj -from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning -from .helpers import add_lv_use +from .defines import ( + CONF_GRADIENTS, + CONF_OPA, + LV_DITHER, + add_define, + add_lv_use, + add_warning, +) from .lv_validation import lv_color, lv_percentage, opacity from .lvcode import lv from .types import lv_color_t, lv_gradient_t, lv_opa_t diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index c2bd58f71c3..baa618d472b 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -5,23 +5,6 @@ from esphome.const import CONF_ARGS, CONF_FORMAT CONF_IF_NAN = "if_nan" -lv_uses = { - "USER_DATA", - "LOG", - "STYLE", - "FONT_PLACEHOLDER", - "THEME_DEFAULT", -} - - -def add_lv_use(*names): - for name in names: - lv_uses.add(name) - - -lv_fonts_used = set() -esphome_fonts_used = set() -lvgl_components_required = set() # noqa f_regex = re.compile( @@ -66,11 +49,3 @@ def validate_printf(value): "Use of 'if_nan' requires a single valid printf-pattern of type %f" ) return value - - -def requires_component(comp): - def validator(value): - lvgl_components_required.add(comp) - return cv.requires_component(comp)(value) - - return validator diff --git a/esphome/components/lvgl/keypads.py b/esphome/components/lvgl/keypads.py index 7d8b3dd1282..6d4abbc63b7 100644 --- a/esphome/components/lvgl/keypads.py +++ b/esphome/components/lvgl/keypads.py @@ -9,9 +9,9 @@ from .defines import ( CONF_KEYPADS, CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, + add_lv_use, literal, ) -from .helpers import lvgl_components_required from .lvcode import lv, lv_assign, lv_expr, lv_Pvariable from .schemas import ENCODER_SCHEMA from .types import lv_group_t, lv_indev_type_t @@ -52,7 +52,7 @@ KEYPADS_CONFIG = cv.ensure_list( async def keypads_to_code(var, config, default_group): for enc_conf in config[CONF_KEYPADS]: - lvgl_components_required.add("KEY_LISTENER") + add_lv_use("KEY_LISTENER") lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds listener = cg.new_Pvariable( diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 50da7af6021..bf019964c7b 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -37,7 +37,7 @@ class LVLight : public light::LightOutput { void set_value_(lv_color_t value) { lv_led_set_color(this->obj_, value); lv_led_on(this->obj_); - lv_obj_send_event(this->obj_, lv_api_event, nullptr); + lv_obj_send_event(this->obj_, lv_update_event, nullptr); } lv_obj_t *obj_{}; optional<lv_color_t> initial_value_{}; diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 974eed9e817..a1b75182ebb 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -31,16 +31,14 @@ from .defines import ( LValidator, LvConstant, StaticCastExpression, + add_lv_use, call_lambda, + get_esphome_fonts_used, + get_lv_fonts_used, + get_lv_images_used, literal, ) -from .helpers import ( - CONF_IF_NAN, - add_lv_use, - esphome_fonts_used, - lv_fonts_used, - requires_component, -) +from .helpers import CONF_IF_NAN from .types import lv_coord_t, lv_gradient_t, lv_opa_t LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -370,14 +368,11 @@ def stop_value(value): return cv.int_range(0, 255)(value) -lv_images_used = set() - - def image_validator(value): - value = requires_component("image")(value) + value = cv.requires_component("image")(value) value = cv.use_id(Image_)(value) - lv_images_used.add(value) - add_lv_use("img", "label") + get_lv_images_used().add(value) + add_lv_use("label") return value @@ -496,7 +491,7 @@ class LvFont(LValidator): def __init__(self): def lv_builtin_font(value): fontval = cv.one_of(*LV_FONTS, lower=True)(value) - lv_fonts_used.add(fontval) + get_lv_fonts_used().add(fontval) return fontval def validator(value): @@ -506,8 +501,8 @@ class LvFont(LValidator): return lv_builtin_font(value) add_lv_use("font") fontval = cv.use_id(Font)(value) - esphome_fonts_used.add(fontval) - return requires_component("font")(fontval) + get_esphome_fonts_used().add(fontval) + return cv.requires_component("font")(fontval) # Use font::Font* as return type for lambdas returning ESPHome fonts # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index eb8f7d4437c..32fb02e3d21 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -29,10 +29,9 @@ LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") EVENT_ARG = [(lv_event_t_ptr, "event")] -# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction; +# One custom event; # UPDATE_EVENT is fired when an entity is programmatically updated locally. # VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction. -API_EVENT = literal("lvgl::lv_api_event") UPDATE_EVENT = literal("lvgl::lv_update_event") diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 3141c5f93c3..678ed9dbbfb 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -147,7 +147,6 @@ void LvglComponent::render_start_cb(lv_event_t *event) { comp->draw_start_(); } -lv_event_code_t lv_api_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, @@ -194,7 +193,6 @@ void LvglComponent::esphome_lvgl_init() { LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core; lv_tick_set_cb([] { return millis(); }); lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id()); - lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id()); } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { @@ -547,7 +545,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a auto index = std::find(this->options_.begin(), this->options_.end(), text); if (index != this->options_.end()) { this->set_selected_index(index - this->options_.begin(), anim); - lv_obj_send_event(this->obj, lv_api_event, nullptr); + lv_obj_send_event(this->obj, lv_update_event, nullptr); } } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 32bf3ccac6b..218f9a60ab9 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -50,7 +50,6 @@ using lv_color_data = uint16_t; using lv_color_data = uint32_t; #endif -extern lv_event_code_t lv_api_event; // NOLINT extern lv_event_code_t lv_update_event; // NOLINT extern std::string lv_event_code_name_for(lv_event_t *event); @@ -227,11 +226,43 @@ class LvglComponent : public PollingComponent { * Initialize the LVGL library and register custom events. */ static void esphome_lvgl_init(); + + // Convenience overloads for adding a callback for one or more events static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3); + // change the state of a widget and fire an event if changed (only needed for CHECKED) + + static void lv_obj_set_state_value(lv_obj_t *obj, lv_state_t state, bool value) { + if (value != lv_obj_has_state(obj, state)) { + if (value) { + lv_obj_add_state(obj, state); + } else { + lv_obj_remove_state(obj, state); + } + if (state == LV_STATE_CHECKED) + lv_obj_send_event(obj, lv_update_event, nullptr); + } + } + + // change the state of a buttonmatrix button and fire an event if changed (only needed for CHECKED) +#ifdef USE_LVGL_BUTTONMATRIX + static void lv_buttonmatrix_set_button_ctrl_value(lv_obj_t *obj, uint32_t index, lv_buttonmatrix_ctrl_t ctrl, + bool value) { + if (value != lv_buttonmatrix_has_button_ctrl(obj, index, ctrl)) { + if (value) { + lv_buttonmatrix_set_button_ctrl(obj, index, ctrl); + } else { + lv_buttonmatrix_clear_button_ctrl(obj, index, ctrl); + } + if (ctrl == LV_BUTTONMATRIX_CTRL_CHECKED) + lv_obj_send_event(obj, lv_update_event, nullptr); + } + } +#endif + void add_page(LvPageType *page); void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time); void show_next_page(lv_screen_load_anim_t anim, uint32_t time); diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index d80e93708bf..0c7e4b9524a 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -7,7 +7,6 @@ from esphome.cpp_generator import MockObj from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET from ..lv_validation import animated from ..lvcode import ( - API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, @@ -40,7 +39,7 @@ async def to_code(config): await widget.set_property( "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) - lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr) event_code = ( LV_EVENT.VALUE_CHANGED if not config[CONF_UPDATE_ON_RELEASE] diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index a9427a98520..6b71c2875e7 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -33,7 +33,7 @@ from .defines import ( get_remapped_uses, is_press_event, ) -from .helpers import CONF_IF_NAN, requires_component, validate_printf +from .helpers import CONF_IF_NAN, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, GRID_CELL_SCHEMA, @@ -112,7 +112,7 @@ PRESS_TIME = cv.All( ENCODER_SCHEMA = cv.Schema( { cv.GenerateID(): cv.All( - cv.declare_id(LVEncoderListener), requires_component("binary_sensor") + cv.declare_id(LVEncoderListener), cv.requires_component("binary_sensor") ), cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), cv.Optional(df.CONF_INITIAL_FOCUS): cv.All( @@ -406,7 +406,7 @@ def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]: """ def validator(value: dict) -> dict: - df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value) + df.get_updated_widgets().setdefault(widget_type, []).append(value) return value return validator @@ -573,7 +573,7 @@ def any_widget_schema(extras=None): container_validator = container_schema(widget_type, extras=extras) if required := widget_type.required_component: container_validator = cv.All( - container_validator, requires_component(required) + container_validator, cv.requires_component(required) ) # Apply custom validation path = [key] if is_dict else [index, key] diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 167af9c6e19..682d84ede3e 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from ..defines import CONF_WIDGET from ..lvcode import ( - API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, @@ -35,7 +34,6 @@ async def to_code(config): widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED, - API_EVENT, UPDATE_EVENT, ) ) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index c17f30383bb..c1441526f95 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -4,12 +4,18 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal -from .helpers import add_lv_use +from .defines import ( + CONF_STYLE_DEFINITIONS, + CONF_THEME, + LValidator, + add_lv_use, + get_theme_widget_map, + literal, +) from .lvcode import LambdaContext, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property from .types import ObjUpdateAction, lv_style_t -from .widgets import collect_parts, theme_widget_map, wait_for_widgets +from .widgets import collect_parts, wait_for_widgets def has_style_props(config) -> bool: @@ -97,4 +103,4 @@ async def theme_to_code(config): ) for state, props in states.items() } - theme_widget_map[w_name] = styles + get_theme_widget_map()[w_name] = styles diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index a43851b4a39..509e4f42ad7 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -7,14 +7,11 @@ from esphome.cpp_types import Component from ..defines import CONF_WIDGET, literal from ..lvcode import ( - API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, - LvConditional, LvContext, lv_add, - lv_obj, lvgl_static, ) from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns @@ -35,11 +32,7 @@ async def to_code(config): switch_id = MockObj(config[CONF_ID], "->") v = literal("v") async with LambdaContext([(cg.bool_, "v")]) as control: - with LvConditional(v) as cond: - widget.add_state(LV_STATE.CHECKED) - cond.else_() - widget.clear_state(LV_STATE.CHECKED) - lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) + widget.set_state(LV_STATE.CHECKED, literal("v")) control.add(switch_id.publish_state(v)) switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda()) await cg.register_component(switch, config) diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index 190ecacda52..61db5444e86 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -5,7 +5,6 @@ import esphome.config_validation as cv from ..defines import CONF_WIDGET from ..lvcode import ( - API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, @@ -33,7 +32,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext([(cg.std_string, "text_value")]) as control: await widget.set_property("text", "text_value.c_str()") - lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr) control.add(textvar.publish_state(widget.get_value())) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py index 4728fd137a5..c3306ad57a0 100644 --- a/esphome/components/lvgl/text_sensor/__init__.py +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -6,14 +6,7 @@ from esphome.components.text_sensor import ( import esphome.config_validation as cv from ..defines import CONF_WIDGET -from ..lvcode import ( - API_EVENT, - EVENT_ARG, - UPDATE_EVENT, - LambdaContext, - LvContext, - lvgl_static, -) +from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static from ..types import LV_EVENT, LvText from ..widgets import get_widgets, wait_for_widgets @@ -37,7 +30,6 @@ async def to_code(config): widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, - API_EVENT, UPDATE_EVENT, ) ) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 0eb9f22f12e..0bb5715439d 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -8,15 +8,17 @@ from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, CONF_TOUCHSCREENS, + add_lv_use, ) -from .helpers import lvgl_components_required from .schemas import PRESS_TIME from .types import LVTouchListener CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { - cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen), + cv.Required(CONF_TOUCHSCREEN_ID): cv.All( + cv.use_id(Touchscreen), cv.requires_component(CONF_TOUCHSCREEN) + ), cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, cv.GenerateID(): cv.declare_id(LVTouchListener), @@ -34,7 +36,7 @@ def touchscreen_schema(config): async def touchscreens_to_code(lv_component, config): for tconf in config[CONF_TOUCHSCREENS]: - lvgl_components_required.add(CONF_TOUCHSCREEN) + add_lv_use(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index b3d12ed1837..64590d56f61 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -24,11 +24,11 @@ from .defines import ( LV_SCREEN_EVENT_MAP, LV_SCREEN_EVENT_TRIGGERS, SWIPE_TRIGGERS, + get_widget_map, is_press_event, literal, ) from .lvcode import ( - API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, @@ -39,7 +39,7 @@ from .lvcode import ( lvgl_static, ) from .types import LV_EVENT, lv_point_t -from .widgets import LvScrActType, get_screen_active, widget_map +from .widgets import LvScrActType, get_screen_active async def add_on_boot_triggers(triggers): @@ -58,7 +58,7 @@ async def generate_triggers(): all_triggers = ( LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS ) - for w in widget_map.values(): + for w in get_widget_map().values(): config = w.config if isinstance(w.type, LvScrActType): w = get_screen_active(w.var) @@ -89,7 +89,6 @@ async def generate_triggers(): conf, w, LV_EVENT.VALUE_CHANGED, - API_EVENT, UPDATE_EVENT, ) @@ -104,6 +103,7 @@ async def generate_align_tos(config: dict): :param config: :return: """ + widget_map = get_widget_map() align_tos = tuple( w for w in widget_map.values() if w.config and CONF_ALIGN_TO in w.config ) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index d35f84c4f26..534ebe0b93d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( from esphome.core import ID, EsphomeError, TimePeriod from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import MockObj +from esphome.types import Expression from ..defines import ( CONF_FLEX_ALIGN_CROSS, @@ -38,11 +39,15 @@ from ..defines import ( TYPE_FLEX, TYPE_GRID, LValidator, + add_lv_use, call_lambda, + get_styles_used, + get_theme_widget_map, + get_widget_map, + get_widgets_completed, join_enums, literal, ) -from ..helpers import add_lv_use from ..lvcode import ( LvConditional, add_line_marks, @@ -52,6 +57,7 @@ from ..lvcode import ( lv_expr, lv_obj, lv_Pvariable, + lvgl_static, ) from ..types import ( LV_STATE, @@ -65,9 +71,6 @@ from ..types import ( EVENT_LAMB = "event_lamb__" -theme_widget_map = {} -styles_used = set() - class WidgetType: """ @@ -157,7 +160,7 @@ class WidgetType: await self.on_create(var, config) w = Widget.create(wid, var, self, config) - if theme := theme_widget_map.get(self.name): + if theme := get_theme_widget_map().get(self.name): for part, states in theme.items(): part = "LV_PART_" + part.upper() for state, style in states.items(): @@ -240,8 +243,6 @@ class Widget: This class has a lot of methods. Adding any more runs foul of lint checks ("too many public methods"). """ - widgets_completed = False - def __init__(self, var, wtype: WidgetType, config: dict = None): self.var = var self.type = wtype @@ -262,21 +263,14 @@ class Widget: @staticmethod def create(name, var, wtype: WidgetType, config: dict = None): w = Widget(var, wtype, config) - widget_map[name] = w + get_widget_map()[name] = w return w - def add_state(self, state): - if "|" in state: - state = f"(lv_state_t)({state})" - return lv_obj.add_state(self.obj, literal(state)) + def set_state(self, state: MockObj, value: bool | Expression): + lv_add(lvgl_static.lv_obj_set_state_value(self.obj, state, value)) - def clear_state(self, state): - if "|" in state: - state = f"(lv_state_t)({state})" - return lv_obj.remove_state(self.obj, literal(state)) - - def has_state(self, state): - return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 + def has_state(self, state: MockObj): + return lv_expr.obj_has_state(self.obj, state) def is_pressed(self): return self.has_state(LV_STATE.PRESSED) @@ -346,10 +340,10 @@ class Widget: ltype = ltype or self.__type_base() return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") - def set_style(self, prop, value, state=LV_STATE.DEFAULT): + def set_style(self, prop: str, value, state=LV_STATE.DEFAULT): if value is None: return - styles_used.add(prop) + get_styles_used().add(prop) if isinstance(value, str): value = literal(value) lv.call(f"obj_set_style_{prop}", self.obj, value, state) @@ -403,14 +397,6 @@ class Widget: return self.type.get_scale(self.config) -# Map of widgets to their config, used for trigger generation -widget_map: dict[ID, Widget] = {} - - -def is_widget_completed(name: ID) -> bool: - return name in widget_map - - class LvScrActType(WidgetType): """ A "widget" representing the active screen. @@ -433,10 +419,11 @@ def get_widget_generator(wid): :param wid: :return: """ + widget_map = get_widget_map() while True: if obj := widget_map.get(wid): return obj - if Widget.widgets_completed: + if get_widgets_completed(): raise Invalid( f"Widget {wid} not found, yet all widgets should be defined by now" ) @@ -444,20 +431,20 @@ def get_widget_generator(wid): async def get_widget_(wid): - if obj := widget_map.get(wid): + if obj := get_widget_map().get(wid): return obj return await FakeAwaitable(get_widget_generator(wid)) def widgets_wait_generator(): while True: - if Widget.widgets_completed: + if get_widgets_completed(): return yield async def wait_for_widgets(): - if Widget.widgets_completed: + if get_widgets_completed(): return await FakeAwaitable(widgets_wait_generator()) @@ -608,30 +595,14 @@ async def set_obj_properties(w: Widget, config): cond.else_() w.clear_flag(flag) - if states := config.get(CONF_STATE): - adds = set() - clears = set() - lambs = {} - for key, value in states.items(): - if isinstance(value, cv.Lambda): - lambs[key] = value - elif value: - adds.add(key) - else: - clears.add(key) - if adds: - adds = join_enums(adds, "LV_STATE_") - w.add_state(adds) - if clears: - clears = join_enums(clears, "LV_STATE_") - w.clear_state(clears) - for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) - state = f"LV_STATE_{key.upper()}" - with LvConditional(call_lambda(lamb)) as cond: - w.add_state(state) - cond.else_() - w.clear_state(state) + for key, value in config.get(CONF_STATE, {}).items(): + if isinstance(value, cv.Lambda): + value = call_lambda( + await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) + ) + state = getattr(LV_STATE, key.upper()) + w.set_state(state, value) + for property in OBJ_PROPERTIES: await w.set_property(property, config, lv_name="obj") diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index 8e2db5ff350..b6d59df7f25 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -4,7 +4,6 @@ from esphome.const import CONF_DURATION, CONF_ID from ..automation import action_to_code from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC -from ..helpers import lvgl_components_required from ..lv_validation import lv_image_list, lv_milliseconds from ..lvcode import lv from ..types import LvType, ObjUpdateAction @@ -55,8 +54,6 @@ class AnimimgType(WidgetType): ) async def to_code(self, w: Widget, config): - lvgl_components_required.add(CONF_IMAGE) - lvgl_components_required.add(CONF_ANIMIMG) if srcs := config.get(CONF_SRC): srcs = await lv_image_list.process(srcs) lv.animimg_set_src(w.obj, srcs) @@ -68,7 +65,7 @@ class AnimimgType(WidgetType): lv.animimg_start(w.obj) def get_uses(self): - return "img", CONF_IMAGE, CONF_LABEL + return CONF_IMAGE, CONF_LABEL animimg_spec = AnimimgType() diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index b943a4d9aa6..0ad512cd8bb 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -2,8 +2,7 @@ from esphome import config_validation as cv from esphome.const import CONF_BUTTON, CONF_TEXT from esphome.cpp_generator import MockObj -from ..defines import CONF_MAIN, CONF_WIDGETS -from ..helpers import add_lv_use +from ..defines import CONF_MAIN, CONF_WIDGETS, add_lv_use from ..lv_validation import lv_text from ..lvcode import lv, lv_expr from ..schemas import TEXT_SCHEMA diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index f5ae0deba93..02dc9ed4ba8 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -5,6 +5,7 @@ from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj +from esphome.types import Expression from ..automation import action_to_code from ..defines import ( @@ -17,10 +18,10 @@ from ..defines import ( CONF_PAD_COLUMN, CONF_PAD_ROW, CONF_SELECTED, + get_widget_map, ) -from ..helpers import lvgl_components_required from ..lv_validation import key_code, lv_bool, padding -from ..lvcode import lv, lv_add, lv_expr +from ..lvcode import lv, lv_add, lv_expr, lvgl_static from ..schemas import automation_schema from ..types import ( LV_BTNMATRIX_CTRL, @@ -32,7 +33,7 @@ from ..types import ( char_ptr, lv_pseudo_button_t, ) -from . import Widget, WidgetType, get_widgets, widget_map +from . import Widget, WidgetType, get_widgets from .button import lv_button_t CONF_BUTTONMATRIX = "buttonmatrix" @@ -98,7 +99,7 @@ class MatrixButton(Widget): @staticmethod def create_button(id, parent, config: dict, index): w = MatrixButton(id, parent, config, index) - widget_map[id] = w + get_widget_map()[id] = w return w def __init__(self, id, parent: Widget, config, index): @@ -120,13 +121,13 @@ class MatrixButton(Widget): state = self.map_ctrls(state) return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state) - def add_state(self, state): - state = self.map_ctrls(state) - return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state) - - def clear_state(self, state): - state = self.map_ctrls(state) - return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state) + def set_state(self, state: MockObj, value: bool | Expression): + ctrl = self.map_ctrls(state) + lv_add( + lvgl_static.lv_buttonmatrix_set_button_ctrl_value( + self.obj, self.index, ctrl, value + ) + ) def is_pressed(self): return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) @@ -191,7 +192,6 @@ class ButtonMatrixType(WidgetType): ) async def to_code(self, w: Widget, config): - lvgl_components_required.add("BUTTONMATRIX") if CONF_ROWS not in config: return text_list, ctrl_list, width_list, key_list = await get_button_data( diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py index ca89bb625be..34e6ef4e357 100644 --- a/esphome/components/lvgl/widgets/dropdown.py +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -14,7 +14,6 @@ from ..defines import ( DIRECTIONS, literal, ) -from ..helpers import lvgl_components_required from ..lv_validation import lv_int, lv_text, option_string from ..lvcode import LocalVariable, lv, lv_add, lv_expr from ..schemas import part_schema @@ -95,7 +94,6 @@ class DropdownType(WidgetType): ) async def to_code(self, w: Widget, config): - lvgl_components_required.add(CONF_DROPDOWN) if options := config.get(CONF_OPTIONS): lv_add(w.var.set_options(options)) if symbol := config.get(CONF_SYMBOL): @@ -116,7 +114,7 @@ class DropdownType(WidgetType): await set_obj_properties(dwid, dlist) def get_uses(self): - return (CONF_LABEL,) + return CONF_LABEL, CONF_DROPDOWN dropdown_spec = DropdownType() diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index c5628cee3cd..bcd2d2ae597 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -5,10 +5,15 @@ from esphome.core import CORE from esphome.cpp_types import std_string from .. import LvContext -from ..defines import CONF_MAIN, KEYBOARD_MODES, literal -from ..helpers import lvgl_components_required +from ..defines import ( + CONF_MAIN, + KEYBOARD_MODES, + add_lv_use, + is_widget_completed, + literal, +) from ..types import LvCompound, LvType -from . import Widget, WidgetType, get_widgets, is_widget_completed +from . import Widget, WidgetType, get_widgets from .buttonmatrix import CONF_BUTTONMATRIX from .textarea import CONF_TEXTAREA, lv_textarea_t @@ -47,8 +52,7 @@ class KeyboardType(WidgetType): return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX async def to_code(self, w: Widget, config: dict): - lvgl_components_required.add("KEY_LISTENER") - lvgl_components_required.add(CONF_KEYBOARD) + add_lv_use("KEY_LISTENER") if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) if textarea := config.get(CONF_TEXTAREA): diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index ab65a7c47d5..62ea14bddaf 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -41,10 +41,10 @@ from ..defines import ( LV_OBJ_FLAG, LV_PART, LV_SCALE_MODE, + add_lv_use, get_remapped_uses, get_warnings, ) -from ..helpers import add_lv_use from ..lv_validation import ( LV_OPA, LV_RADIUS, @@ -61,7 +61,6 @@ from ..lv_validation import ( padding, pixels, pixels_or_percent, - requires_component, size, ) from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj @@ -214,7 +213,7 @@ INDICATOR_SCHEMA = cv.Schema( cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t), } ), - requires_component("image"), + cv.requires_component("image"), ), cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( { diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index d0e6bfa3a2b..29087009cb9 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -16,10 +16,10 @@ from ..defines import ( CONF_TITLE, LV_OBJ_FLAG, TYPE_FLEX, + add_lv_use, add_warning, literal, ) -from ..helpers import add_lv_use from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj from ..schemas import ( diff --git a/esphome/components/lvgl/widgets/roller.py b/esphome/components/lvgl/widgets/roller.py index 6f9fee47d4b..f3caaa43499 100644 --- a/esphome/components/lvgl/widgets/roller.py +++ b/esphome/components/lvgl/widgets/roller.py @@ -11,7 +11,6 @@ from ..defines import ( ROLLER_MODES, literal, ) -from ..helpers import lvgl_components_required from ..lv_validation import animated, lv_int, lv_text, option_string from ..lvcode import lv_add from ..types import LvSelect @@ -55,7 +54,6 @@ class RollerType(WidgetType): ) async def to_code(self, w, config): - lvgl_components_required.add(CONF_ROLLER) if mode := config.get(CONF_MODE): mode = await ROLLER_MODES.process(mode) lv_add(w.var.set_mode(mode)) diff --git a/tests/component_tests/lvgl/config/widget_state_test.yaml b/tests/component_tests/lvgl/config/widget_state_test.yaml new file mode 100644 index 00000000000..644bf7dac73 --- /dev/null +++ b/tests/component_tests/lvgl/config/widget_state_test.yaml @@ -0,0 +1,83 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + id: lvgl_id + displays: tft_display + pages: + - id: main_page + widgets: + # Widget with multiple static states; one true, one false. + - button: + id: btn_static + state: + checked: true + disabled: false + + # Widget with a templated (lambda) state. + - button: + id: btn_lambda + state: + pressed: !lambda return true; + + # Button referenced by enable/disable actions; the on_click handler + # exercises both branches of the obj_disable/obj_enable code path. + - button: + id: btn_actions + on_click: + - lvgl.widget.disable: btn_actions + - lvgl.widget.enable: btn_actions + + # Button matrix with two buttons; matrix_btn_a is targeted by + # lvgl.widget.disable/enable actions to exercise the + # MatrixButton.set_state code path. + - buttonmatrix: + id: matrix + rows: + - buttons: + - id: matrix_btn_a + text: A + control: + checkable: true + - id: matrix_btn_b + text: B + control: + checkable: true + on_click: + - lvgl.widget.disable: matrix_btn_a + - lvgl.widget.enable: matrix_btn_a + + # Switch derived from an LVGL switch widget – exercises + # set_state(LV_STATE.CHECKED, v) inside the control lambda. + - switch: + id: switch_widget + +switch: + - platform: lvgl + id: lvgl_switch + name: LVGL Switch + widget: switch_widget diff --git a/tests/component_tests/lvgl/test_widget_state.py b/tests/component_tests/lvgl/test_widget_state.py new file mode 100644 index 00000000000..2d87bb382e2 --- /dev/null +++ b/tests/component_tests/lvgl/test_widget_state.py @@ -0,0 +1,167 @@ +"""Tests for LVGL widget state code generation. + +These tests cover the change from the old ``add_state``/``clear_state`` helpers +on :class:`Widget` (and on :class:`MatrixButton`) to a single ``set_state`` +method that delegates to the new C++ helpers +``LvglComponent::lv_obj_set_state_value`` and +``LvglComponent::lv_buttonmatrix_set_button_ctrl_value``. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from esphome.__main__ import generate_cpp_contents +from esphome.config import read_config +from esphome.core import CORE + + +@pytest.fixture(scope="module") +def main_cpp(request: pytest.FixtureRequest) -> str: + """Generate the C++ output for the shared widget-state YAML config once + per module. + + Module-scoped so the (relatively expensive) codegen runs a single time; + the function-scoped fixtures from ``conftest.py`` (e.g. ``generate_main``) + can't be requested from a higher-scoped fixture, so the small amount of + setup is inlined here. The captured string is independent of + ``CORE.reset()`` calls that the per-test autouse fixtures perform after + this fixture has produced its value. + """ + config_path = Path(request.fspath).parent / "config" / "widget_state_test.yaml" + original_path = CORE.config_path + try: + CORE.config_path = config_path + CORE.config = read_config({}) + generate_cpp_contents(CORE.config) + return CORE.cpp_global_section + CORE.cpp_main_section + finally: + CORE.config_path = original_path + CORE.reset() + + +def test_static_state_emits_set_state_value(main_cpp: str) -> None: + """A widget with ``state: { checked: true, disabled: false }`` should + generate one ``lv_obj_set_state_value`` call per entry, with the + appropriate boolean argument. + """ + assert ( + "LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_CHECKED, true)" + in main_cpp + ) + assert ( + "LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_DISABLED, false)" + in main_cpp + ) + + +def test_lambda_state_emits_set_state_value_with_lambda(main_cpp: str) -> None: + """A widget with ``state: { pressed: !lambda return true; }`` should + generate ``lv_obj_set_state_value(..., LV_STATE_PRESSED, <expr>)`` where + ``<expr>`` is the lambda's return value (cast or inlined), not a static + bool. + """ + # The set_state call is emitted for the templated state. + assert ( + "LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED," + in main_cpp + ) + # And it must NOT have collapsed the lambda to a literal true/false. + assert ( + "LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED, true)" + not in main_cpp + ) + # The legacy if/else over add_state/remove_state is gone. + assert "lv_obj_add_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp + assert "lv_obj_remove_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp + + +def test_widget_disable_action_uses_set_state_value(main_cpp: str) -> None: + """``lvgl.widget.disable: btn_actions`` should emit a + ``set_state_value(..., LV_STATE_DISABLED, true)`` call rather than the + legacy ``lv_obj_add_state``. + """ + assert ( + "LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, true)" + in main_cpp + ) + # No leftover legacy add_state for the DISABLED state of this widget. + assert "lv_obj_add_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp + + +def test_widget_enable_action_uses_set_state_value(main_cpp: str) -> None: + """``lvgl.widget.enable: btn_actions`` should emit a + ``set_state_value(..., LV_STATE_DISABLED, false)`` call rather than the + legacy ``lv_obj_remove_state``. + """ + assert ( + "LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, false)" + in main_cpp + ) + assert "lv_obj_remove_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp + + +def test_buttonmatrix_disable_action_uses_helper(main_cpp: str) -> None: + """``lvgl.widget.disable: matrix_btn_a`` should route through the new + ``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0 + with the ``DISABLED`` control bit set to ``true``, instead of the + legacy ``lv_buttonmatrix_set_button_ctrl``. + + The button matrix obj is the compound's ``obj`` member and the index + is the position of the button in the row layout. + """ + assert ( + "LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, " + "LV_BUTTONMATRIX_CTRL_DISABLED, true)" + ) in main_cpp + + +def test_buttonmatrix_enable_action_uses_helper(main_cpp: str) -> None: + """``lvgl.widget.enable: matrix_btn_a`` should route through the new + ``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0 + with the ``DISABLED`` control bit set to ``false``, instead of the + legacy ``lv_buttonmatrix_clear_button_ctrl``. + """ + assert ( + "LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, " + "LV_BUTTONMATRIX_CTRL_DISABLED, false)" + ) in main_cpp + # The legacy clear_button_ctrl path is gone for the matrix button enable + # action. + assert ( + "lv_buttonmatrix_clear_button_ctrl(matrix->obj, 0, LV_BUTTONMATRIX_CTRL_DISABLED)" + not in main_cpp + ) + + +def test_lvgl_switch_control_calls_set_state_value(main_cpp: str) -> None: + """The LVGL switch platform installs a control lambda that mirrors the + switch's bool value into ``LV_STATE_CHECKED`` via + ``lv_obj_set_state_value`` (replacing the previous if/else over + ``add_state``/``clear_state`` plus an explicit ``send_event`` of + ``lv_api_event``). + """ + # The control lambda calls the new helper with the bool ``v`` parameter. + assert ( + "LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_CHECKED, v)" + in main_cpp + ) + # The deprecated lv_api_event symbol must no longer appear anywhere. + assert "lv_api_event" not in main_cpp + + +def test_default_state_does_not_emit_set_state_value(main_cpp: str) -> None: + """A widget without a ``state:`` block must not generate any + ``lv_obj_set_state_value`` calls for it. (Sanity-check that the + new code path is opt-in driven by the YAML.) + """ + assert ( + "LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_DISABLED" + not in main_cpp + ) + assert ( + "LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_PRESSED" + not in main_cpp + ) From 930d539969db8a38970ccc53b20d98af218b6e4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 10 May 2026 19:07:15 -0500 Subject: [PATCH 1911/2056] [config_validation] Add a visibility UI-hint kwarg (#16267) --- esphome/components/time/__init__.py | 9 +- esphome/config_validation.py | 115 ++++++++++++- script/build_language_schema.py | 12 ++ tests/unit_tests/test_config_validation.py | 184 +++++++++++++++++++++ 4 files changed, 313 insertions(+), 7 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 3295366fea3..29bb01b499d 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -345,7 +345,14 @@ TIME_SCHEMA = cv.Schema( } ), } -).extend(cv.polling_component_schema("15min")) +).extend( + # ``visibility=ADVANCED`` flags the inherited ``update_interval`` + # field for visual editors — the 15min default is correct for + # essentially every user, so editors should keep it tucked under + # "advanced" so it doesn't crowd the form. Validation is + # unaffected; YAML can override as before. + cv.polling_component_schema("15min", visibility=cv.Visibility.ADVANCED) +) def _emit_dst_rule_fields(prefix, rule): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fbafc5cb07b..c993c1dcc5e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -89,6 +89,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.enum import StrEnum from esphome.expression import SUBSTITUTION_VARIABLE_PROG as VARIABLE_PROG from esphome.helpers import add_class_to_obj, docs_url, list_starts_with from esphome.schema_extractors import ( @@ -281,6 +282,54 @@ RESERVED_IDS = [ ] +class Visibility(StrEnum): + """Schema-driven UI hint for visual editors. + + The values describe how a schema-aware editor (e.g. the + device-builder dashboard catalog via + ``script/build_language_schema.py``) should render the field. + They do NOT affect validation — the YAML still accepts the key + the same way. ESPHome itself ignores the value at runtime; + consumers downstream of the schema dump act on it. + + A field with no ``visibility`` set (the default) renders on the + editor's main form. The two values below are points along a + single axis of "how prominently to surface this": + + - ``ADVANCED`` — render under the editor's "advanced settings" + disclosure. Use for fields whose default is right for ~all + users (e.g. ``update_interval`` on time platforms — 15 min is + universally correct, but power users can still tune the YAML + directly). + - ``YAML_ONLY`` — never render in a visual editor. Use for + knobs that are dangerous to expose in a UI even as advanced + (``setup_priority`` is the canonical example — casual UI + tweaks can break boot). The YAML escape hatch stays + available for the rare power-user override. + + The single-axis shape encodes "yaml-only is strictly stronger + than advanced" at the type level — there's no way to ask for + both at once, and no way to set a contradictory state like + "advanced=False, yaml_only=True". + + Per-field; the dumper walks recursively into nested schemas + and emits each field's setting independently. Cascading + semantics — "a stricter parent makes its descendants at-least + as strict" — belong on the consumer side: the schema marker + is faithfully what the field author wrote, and a consumer that + cares about effective visibility walks the parent chain and + takes the strictest setting. ``YAML_ONLY`` is strictly stronger + than ``ADVANCED``, which is strictly stronger than no setting. + Inner fields can declare their own visibility; an inner + ``YAML_ONLY`` under an ``ADVANCED`` parent stays ``YAML_ONLY``, + and the consumer's cascade keeps siblings under the parent at + ``ADVANCED`` regardless of their own (less-strict) setting. + """ + + ADVANCED = "advanced" + YAML_ONLY = "yaml_only" + + class Optional(vol.Optional): """Mark a field as optional and optionally define a default for the field. @@ -295,22 +344,45 @@ class Optional(vol.Optional): In ESPHome, all configuration defaults should be defined with the Optional class during config validation - specifically *not* in the C++ code or the code generation phase. + + See :class:`Visibility` for the ``visibility`` kwarg — a UI + hint for schema-driven editors that doesn't affect validation. """ - def __init__(self, key, default=UNDEFINED): + def __init__( + self, + key, + default=UNDEFINED, + *, + visibility: Visibility | None = None, + ): super().__init__(key, default=default) + self.visibility: Visibility | None = visibility class Required(vol.Required): """Define a field to be required to be set. The validated configuration is guaranteed to contain this key. - All required values should be acceessed with the `config[CONF_<KEY>]` syntax in code + All required values should be accessed with the `config[CONF_<KEY>]` syntax in code - *not* the `config.get(CONF_<KEY>)` syntax. + + See :class:`Visibility` for the ``visibility`` kwarg — a UI + hint for schema-driven editors that doesn't affect validation. + Required fields rarely need it (a required field by definition + needs the user's attention) but the kwarg is exposed for + symmetry so consumers can apply uniform logic across key markers. """ - def __init__(self, key, msg=None): + def __init__( + self, + key, + msg=None, + *, + visibility: Visibility | None = None, + ): super().__init__(key, msg=msg) + self.visibility: Visibility | None = visibility class FinalExternalInvalid(Invalid): @@ -2162,16 +2234,45 @@ ENTITY_BASE_SCHEMA = Schema( ENTITY_BASE_SCHEMA.add_extra(_entity_base_validator) -COMPONENT_SCHEMA = Schema({Optional(CONF_SETUP_PRIORITY): float_}) +COMPONENT_SCHEMA = Schema( + { + # ``setup_priority`` controls the relative order in which + # components are brought up at boot. Wrong values can break + # the boot sequence in subtle ways (e.g. an i2c device set + # to higher priority than the bus). Mark it ``YAML_ONLY`` so + # visual editors never render it — the YAML escape hatch + # stays available for the rare component author who really + # needs to override the default. + Optional(CONF_SETUP_PRIORITY, visibility=Visibility.YAML_ONLY): float_, + } +) -def polling_component_schema(default_update_interval): +def polling_component_schema( + default_update_interval, *, visibility: Visibility | None = None +): """Validate that this component represents a PollingComponent with a configurable update_interval. :param default_update_interval: The default update interval to set for the integration. + :param visibility: When set, propagate to the inherited + ``update_interval`` field's :class:`Visibility` UI hint. Set + this for components whose default cadence is already correct + for ~all users (e.g. time platforms — pass + ``Visibility.ADVANCED``). + + Only honoured on the optional-default branch. When + ``default_update_interval`` is ``None`` the field becomes + ``Required`` (the component has no sensible default cadence and + needs the user to choose), and hiding a Required field behind + an advanced disclosure would be a UX hazard — collapsed-by-default + editors could let the user submit without realising the form has + an unfilled required field. The kwarg is silently ignored on that + path so callers can pass it unconditionally. """ if default_update_interval is None: + # Required → don't honour ``visibility``. + # See the docstring for the UX rationale. return COMPONENT_SCHEMA.extend( { Required(CONF_UPDATE_INTERVAL): update_interval, @@ -2181,7 +2282,9 @@ def polling_component_schema(default_update_interval): return COMPONENT_SCHEMA.extend( { Optional( - CONF_UPDATE_INTERVAL, default=default_update_interval + CONF_UPDATE_INTERVAL, + default=default_update_interval, + visibility=visibility, ): update_interval, } ) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 05ac47bfcce..a7142fa8b5e 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1103,6 +1103,18 @@ def convert_keys(converted, schema, path): if default_value is not None: result["default"] = str(default_value) + # UI hint from ``cv.Optional`` / ``cv.Required`` — surfaced + # for schema consumers (visual editors) that want to render + # advanced / yaml-only fields differently. ESPHome itself + # ignores it at runtime; emitting only when set keeps the + # dump compact and backwards-compatible with markers that + # don't carry the attribute. The value is the str form of + # ``cv.Visibility`` (e.g. ``"advanced"`` / ``"yaml_only"``) + # so consumers don't need an enum import to read it. + visibility = getattr(k, "visibility", None) + if visibility is not None: + result["visibility"] = str(visibility) + # Do value convert(v, result, path + f"/{str(k)}") if "schema" not in converted: diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index f038272d8b0..fd6c0e95f2a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -793,3 +793,187 @@ def test_update_interval__never_passes_through() -> None: """update_interval: never must still map to SCHEDULER_DONT_RUN.""" result = config_validation.update_interval("never") assert result.total_milliseconds == SCHEDULER_DONT_RUN + + +# --------------------------------------------------------------------------- +# Visibility UI-hint kwarg +# --------------------------------------------------------------------------- + + +def test_optional_default_visibility_is_none() -> None: + """An ``Optional`` with no ``visibility`` kwarg reports ``None``. + + Consumers can read the attribute directly with plain attribute + access; absence (``None``) means "render on the editor's main + form." + """ + o = config_validation.Optional("foo") + assert o.visibility is None + + +def test_optional_visibility_advanced() -> None: + """``visibility=Visibility.ADVANCED`` is recorded on the marker.""" + o = config_validation.Optional( + "foo", visibility=config_validation.Visibility.ADVANCED + ) + assert o.visibility is config_validation.Visibility.ADVANCED + + +def test_optional_visibility_yaml_only() -> None: + """``visibility=Visibility.YAML_ONLY`` is recorded on the marker.""" + o = config_validation.Optional( + "foo", visibility=config_validation.Visibility.YAML_ONLY + ) + assert o.visibility is config_validation.Visibility.YAML_ONLY + + +def test_visibility_str_values_match_dump_emission() -> None: + """``Visibility`` is a ``StrEnum`` whose values are the literal + strings the schema dumper emits. + + The schema bundle consumers (catalog generators, third-party + schema-aware tooling) shouldn't need an enum import to read the + field — pinning the on-the-wire spelling here keeps the dump + contract stable. + """ + assert str(config_validation.Visibility.ADVANCED) == "advanced" + assert str(config_validation.Visibility.YAML_ONLY) == "yaml_only" + + +def test_optional_visibility_does_not_affect_validation() -> None: + """The kwarg is an advisory UI hint — it must not change how the + validator behaves. A schema with ``visibility`` applied must + accept and reject the same values it would without it. + """ + plain = config_validation.Schema( + {config_validation.Optional("foo", default=42): config_validation.int_} + ) + flagged = config_validation.Schema( + { + config_validation.Optional( + "foo", + default=42, + visibility=config_validation.Visibility.YAML_ONLY, + ): config_validation.int_ + } + ) + # Same accept / default-fill behavior. + assert plain({"foo": 7}) == flagged({"foo": 7}) == {"foo": 7} + assert plain({}) == flagged({}) == {"foo": 42} + # Same rejection on bad input. + with pytest.raises(Invalid): + plain({"foo": "not-an-int"}) + with pytest.raises(Invalid): + flagged({"foo": "not-an-int"}) + + +def test_required_default_visibility_is_none() -> None: + """``Required`` mirrors ``Optional`` for the ``visibility`` kwarg.""" + r = config_validation.Required("foo") + assert r.visibility is None + + +def test_required_visibility_kwarg() -> None: + """``Required`` accepts ``visibility`` for symmetry with ``Optional``. + + Required fields rarely need the kwarg, but exposing it lets + consumers apply uniform logic across key markers. + """ + r = config_validation.Required( + "foo", visibility=config_validation.Visibility.ADVANCED + ) + assert r.visibility is config_validation.Visibility.ADVANCED + + +def test_polling_component_schema_visibility_opt_in() -> None: + """``visibility=`` propagates to the inherited ``update_interval``. + + Time platforms pass ``Visibility.ADVANCED``; sensors and other + polling components leave it ``None`` and keep the un-flagged shape. + """ + default = config_validation.polling_component_schema("15min") + advanced = config_validation.polling_component_schema( + "15min", visibility=config_validation.Visibility.ADVANCED + ) + default_keys = {str(k): k for k in default.schema} + advanced_keys = {str(k): k for k in advanced.schema} + assert default_keys["update_interval"].visibility is None + assert ( + advanced_keys["update_interval"].visibility + is config_validation.Visibility.ADVANCED + ) + # The opt-in only touches update_interval — setup_priority + # still inherits its YAML_ONLY visibility from COMPONENT_SCHEMA + # in both shapes. + assert ( + default_keys["setup_priority"].visibility + is config_validation.Visibility.YAML_ONLY + ) + assert ( + advanced_keys["setup_priority"].visibility + is config_validation.Visibility.YAML_ONLY + ) + + +def test_polling_component_schema_no_default_ignores_visibility() -> None: + """``visibility`` is silently ignored when the field is Required. + + When ``default_update_interval=None`` the field becomes + ``Required``. Hiding a Required field behind an advanced + disclosure is a UX hazard — a collapsed-by-default editor could + let the user submit without noticing the form has an unfilled + required field. The helper accepts the kwarg unconditionally + for caller ergonomics but doesn't honour it on this branch. + """ + schema = config_validation.polling_component_schema( + None, visibility=config_validation.Visibility.ADVANCED + ) + keys = {str(k): k for k in schema.schema} + assert isinstance(keys["update_interval"], config_validation.Required) + assert keys["update_interval"].visibility is None + + +def test_visibility_marker_is_per_field_no_mutation() -> None: + """Each field's ``visibility`` is recorded as the author wrote it. + + Cascading semantics — "a stricter parent forces its descendants + at-least as strict" — live on the consumer side, not in the + marker itself. The schema marker stays as-written so consumers + can walk the parent chain and compute the effective visibility + themselves; mutating the marker would lose the per-field author + intent. + + Pin both directions of the no-mutation contract: an inner + ``YAML_ONLY`` under an ``ADVANCED`` parent stays ``YAML_ONLY`` + on the marker (the consumer's effective-visibility cascade + would also report ``YAML_ONLY`` since it's stricter), and an + un-marked inner field stays ``None`` on the marker (the + cascade's job is to compute ``ADVANCED`` from the parent — a + detail this test deliberately doesn't pin, since it's a + consumer concern). + """ + inner_unset = config_validation.Optional("baz") + inner_yaml_only = config_validation.Optional( + "qux", visibility=config_validation.Visibility.YAML_ONLY + ) + parent = config_validation.Optional( + "foo", visibility=config_validation.Visibility.ADVANCED + ) + + # Wire them into a nested schema — none of the markers' own + # ``visibility`` should change as a result. + schema = config_validation.Schema( + { + parent: config_validation.Schema( + { + inner_unset: config_validation.int_, + inner_yaml_only: config_validation.string, + } + ) + } + ) + assert schema # touch the schema so any deferred mutation runs + + assert parent.visibility is config_validation.Visibility.ADVANCED + assert inner_unset.visibility is None + assert inner_yaml_only.visibility is config_validation.Visibility.YAML_ONLY From 17080ddce6c98a10ebdd50544fbd688c226e5c44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 10 May 2026 19:56:56 -0500 Subject: [PATCH 1912/2056] [wifi][rp2040] Add stable wifi-capability helpers for device-builder (#16300) --- esphome/components/rp2040/__init__.py | 16 +++- esphome/components/wifi/__init__.py | 61 +++++++++++++- tests/unit_tests/components/test_rp2040.py | 29 +++++++ tests/unit_tests/components/test_wifi.py | 96 ++++++++++++++++++++++ 4 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/components/test_rp2040.py create mode 100644 tests/unit_tests/components/test_wifi.py diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 79ed00cb416..7e450578cdd 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -48,7 +48,21 @@ def board_has_wifi() -> bool: Returns True for unknown/custom boards to avoid rejecting valid configurations for boards not in the generated list. """ - board_info = boards.BOARDS.get(get_board()) + return board_id_has_wifi(get_board()) + + +def board_id_has_wifi(board_id: str) -> bool: + """Return True if *board_id* has WiFi (CYW43 wireless chip). + + Returns True for unknown/custom boards to avoid rejecting valid + configurations for boards not in the generated list. + + Used by device-builder (esphome/device-builder) — separate + explicit-arg helper so callers outside the compile pipeline + don't need ``CORE`` set up to query the board map. Please keep + the signature stable. + """ + board_info = boards.BOARDS.get(board_id) if board_info is None: return True return board_info.get("wifi", False) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 69544f36367..316d4321403 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -67,9 +67,66 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] -_LOGGER = logging.getLogger(__name__) - NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] + + +def variant_has_wifi(variant: str) -> bool: + """Return True if *variant* has a native WiFi PHY. + + Variants without a native PHY (ESP32-H2, ESP32-P4) need the + ``esp32_hosted`` co-processor to use ``wifi:``. + + Used by device-builder (esphome/device-builder) to decide whether + its basic-setup wizard emits a ``wifi:`` block — please keep the + signature stable. + """ + return variant not in NO_WIFI_VARIANTS + + +_WIFI_FIRST_PLATFORMS: frozenset[str] = frozenset( + { + Platform.ESP8266, + Platform.BK72XX, + Platform.RTL87XX, + Platform.LN882X, + # Legacy umbrella key for the LibreTiny families (bk72xx / + # rtl87xx / ln882x); still produced by older configs that + # haven't migrated to the per-family keys. + Platform.LIBRETINY_OLDSTYLE, + } +) + + +def has_native_wifi( + *, platform: str, board: str | None = None, variant: str | None = None +) -> bool: + """Return True when the given platform/board/variant has native WiFi. + + Single dispatch entry point for tooling that needs to decide + whether emitting a ``wifi:`` block produces a compilable + config. Caller passes whichever platform-relevant fields they + have (``variant`` for ESP32, ``board`` for RP2040), and this + function routes to the right per-platform check internally. + + Allowlist-based: unknown / Wi-Fi-less platforms (``host``, + ``nrf52``) return False so a future platform added to ESPHome + fails closed in external tooling rather than silently emitting + a ``wifi:`` block the new platform's component would reject. + + Used by device-builder (esphome/device-builder)'s basic-setup + wizard. Centralised here so callers don't have to special-case + each platform — as ESPHome adds new platforms, this dispatcher + is the one place to teach them about Wi-Fi capability. + """ + if platform == Platform.ESP32: + return variant_has_wifi(variant) if variant else True + if platform == Platform.RP2040: + from esphome.components.rp2040 import board_id_has_wifi + + return board_id_has_wifi(board) if board else True + return platform in _WIFI_FIRST_PLATFORMS + + CONF_SAVE = "save" CONF_BAND_MODE = "band_mode" CONF_MIN_AUTH_MODE = "min_auth_mode" diff --git a/tests/unit_tests/components/test_rp2040.py b/tests/unit_tests/components/test_rp2040.py new file mode 100644 index 00000000000..25a9ade567b --- /dev/null +++ b/tests/unit_tests/components/test_rp2040.py @@ -0,0 +1,29 @@ +"""Tests for RP2040 component public helpers.""" + +from esphome.components.rp2040 import board_id_has_wifi + + +def test_board_id_has_wifi_for_known_wifi_board() -> None: + """``rpipicow`` is the canonical Pico W → True.""" + assert board_id_has_wifi("rpipicow") is True + + +def test_board_id_has_wifi_for_known_non_wifi_board() -> None: + """Plain ``rpipico`` has no CYW43 → False.""" + assert board_id_has_wifi("rpipico") is False + + +def test_board_id_has_wifi_for_rp2350_w_variant() -> None: + """``rpipico2w`` is the RP2350 Pico 2 W → True.""" + assert board_id_has_wifi("rpipico2w") is True + + +def test_board_id_has_wifi_for_unknown_board_returns_true() -> None: + """Unknown ids fail open so a custom board is not rejected. + + The validator falls back to ESPHome's compile-time check; the + helper returning True here means the wizard emits a ``wifi:`` + block and any genuinely-unsupported config trips the existing + "no CYW43" guard at compile time. + """ + assert board_id_has_wifi("not-a-real-board-id") is True diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py new file mode 100644 index 00000000000..e93ae4b5036 --- /dev/null +++ b/tests/unit_tests/components/test_wifi.py @@ -0,0 +1,96 @@ +"""Tests for WiFi component public helpers.""" + +import pytest + +from esphome.components.esp32 import const +from esphome.components.wifi import has_native_wifi, variant_has_wifi +from esphome.const import Platform + + +@pytest.mark.parametrize( + "variant", + [ + const.VARIANT_ESP32, + const.VARIANT_ESP32S2, + const.VARIANT_ESP32S3, + const.VARIANT_ESP32C3, + const.VARIANT_ESP32C6, + ], +) +def test_variant_has_wifi_for_native_phy_variants(variant: str) -> None: + """Variants with a native WiFi PHY → True.""" + assert variant_has_wifi(variant) is True + + +@pytest.mark.parametrize( + "variant", + [ + const.VARIANT_ESP32H2, + const.VARIANT_ESP32P4, + ], +) +def test_variant_has_wifi_for_no_phy_variants(variant: str) -> None: + """Variants that need ``esp32_hosted`` → False.""" + assert variant_has_wifi(variant) is False + + +def test_has_native_wifi_dispatches_esp32_to_variant_check() -> None: + """ESP32 platform routes through ``variant_has_wifi``.""" + assert ( + has_native_wifi(platform=Platform.ESP32, variant=const.VARIANT_ESP32C3) is True + ) + assert ( + has_native_wifi(platform=Platform.ESP32, variant=const.VARIANT_ESP32H2) is False + ) + + +def test_has_native_wifi_dispatches_rp2040_to_board_check() -> None: + """RP2040 platform routes through ``rp2040.board_id_has_wifi``.""" + assert has_native_wifi(platform=Platform.RP2040, board="rpipicow") is True + assert has_native_wifi(platform=Platform.RP2040, board="rpipico") is False + + +def test_has_native_wifi_returns_false_for_nrf52() -> None: + """nRF52 family is BLE-only — no Wi-Fi PHY in the platform.""" + assert has_native_wifi(platform=Platform.NRF52) is False + + +def test_has_native_wifi_returns_false_for_host() -> None: + """``host`` platform compiles ESPHome to a host binary — no radio at all.""" + assert has_native_wifi(platform=Platform.HOST) is False + + +def test_has_native_wifi_returns_false_for_unknown_platform() -> None: + """Unknown platform string fails closed. + + A future platform added to ESPHome that's missed here returns + False rather than silently emitting a ``wifi:`` block external + tooling would have to compile and reject — fail-closed surfaces + the gap as an obvious "needs wifi support added" signal. + """ + assert has_native_wifi(platform="not-a-real-platform") is False + + +@pytest.mark.parametrize( + "platform", + [ + Platform.ESP8266, + Platform.BK72XX, + Platform.RTL87XX, + Platform.LN882X, + Platform.LIBRETINY_OLDSTYLE, + ], +) +def test_has_native_wifi_returns_true_for_wifi_first_platforms(platform: str) -> None: + """Catch-all Wi-Fi-first platforms → True regardless of board / variant.""" + assert has_native_wifi(platform=platform) is True + + +def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: + """ESP32 without a variant id falls open to True (the chip family default).""" + assert has_native_wifi(platform=Platform.ESP32) is True + + +def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: + """RP2040 without a board id falls open to True (custom-board default).""" + assert has_native_wifi(platform=Platform.RP2040) is True From 66e2dcffc448ae60e8c1209bbc0aeca1b50f371d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Sun, 10 May 2026 20:00:09 -0500 Subject: [PATCH 1913/2056] [cli] Tighten command_rename: scoped name rewrite, target-collision check (#16296) --- esphome/__main__.py | 62 ++++- tests/unit_tests/test_main.py | 461 ++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+), 4 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c1451c5fafd..a0ee5a359f4 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1813,11 +1813,32 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: old_name = yaml[CONF_ESPHOME][CONF_NAME] match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) if match is None: - new_raw = re.sub( - rf"name:\s+[\"']?{old_name}[\"']?", - f'name: "{new_name}"', - raw_contents, + # Only swap the ``name:`` line that sits directly under the + # top-level ``esphome:`` block. A naked ``re.sub`` would + # also clobber any other ``name:`` line whose value happens + # to match (e.g. a sensor / output / wifi entry sharing the + # device's hostname), silently rewriting unrelated user + # configuration. The pattern anchors: + # - at the start of the line so ``friendly_name:``, + # ``device_name:`` etc. don't match the trailing ``name:`` + # substring; and + # - at the end of the value (lookahead for whitespace + + # comment + EOL) so ``old_name`` doesn't match as a + # prefix of a longer value (``kitchen`` vs ``kitchen2``). + name_pattern = re.compile( + rf"^(\s*)name:\s+[\"']?{re.escape(old_name)}[\"']?(?=\s*(?:#|$))" ) + out_lines: list[str] = [] + in_esphome_block = False + for line in raw_contents.splitlines(keepends=True): + if line and not line[0].isspace() and line.strip(): + in_esphome_block = line.lstrip().startswith("esphome:") + out_lines.append(line) + continue + if in_esphome_block: + line = name_pattern.sub(rf'\1name: "{new_name}"', line, count=1) + out_lines.append(line) + new_raw = "".join(out_lines) else: old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] if ( @@ -1840,7 +1861,40 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: flags=re.MULTILINE, ) + # ``new_name == old_name`` (after substitution resolution) is + # a no-op rewrite that would still queue a pointless re-flash. + # Catch it before the path-equality check below — covers the + # case where the config filename doesn't match the device name + # (e.g. ``weird-file.yaml`` whose ``esphome.name`` is + # ``kitchen``; running ``esphome rename weird-file.yaml kitchen`` + # would otherwise just re-flash the same hostname). + if new_name == old_name: + print( + color( + AnsiFore.BOLD_RED, + f"'{new_name}' is already the device's name.", + ) + ) + return 1 + new_path: Path = CORE.config_dir / (new_name + ".yaml") + if new_path.resolve() == CORE.config_path.resolve(): + print( + color( + AnsiFore.BOLD_RED, + f"'{new_name}' is already the device's name.", + ) + ) + return 1 + if new_path.exists(): + print( + color( + AnsiFore.BOLD_RED, + f"Cannot rename: {new_path} already exists. " + "Refusing to overwrite an existing configuration.", + ) + ) + return 1 print( f"Updating {color(AnsiFore.CYAN, str(CORE.config_path))} to {color(AnsiFore.CYAN, str(new_path))}" ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 4b0590cf76d..2823310f0e4 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -3595,6 +3595,467 @@ esp32: assert "Rename failed" in captured.out +def test_command_rename_install_failure_reverts( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename when the install (esphome run) step fails.""" + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +esphome: + name: oldname + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}} + + args = MockArgs(name="newname", dashboard=False) + + # First call (config validation) succeeds; second (esphome run) fails. + mock_run_external_process.side_effect = [0, 1] + + result = command_rename(args, {}) + + assert result == 1 + + # New file was unlinked when install failed. + new_file = tmp_path / "newname.yaml" + assert not new_file.exists() + + # Old file is preserved so the device stays reachable under the + # original hostname. + assert config_file.exists() + + +def test_command_rename_target_exists_refuses( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename refuses when the target filename already exists. + + Without this guard, the rename would overwrite the unrelated + device's YAML and OTA-install our firmware to the wrong device. + """ + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +esphome: + name: oldname + +esp32: + board: nodemcu-32s +""") + target_file = tmp_path / "newname.yaml" + target_file.write_text(""" +esphome: + name: someoneelse + +esp32: + board: nodemcu-32s +""") + target_original = target_file.read_text() + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}} + + args = MockArgs(name="newname", dashboard=False) + + result = command_rename(args, {}) + + assert result == 1 + # No subprocess work happened — refusal is up-front. + mock_run_external_process.assert_not_called() + # Target file untouched: same content, still on disk. + assert target_file.exists() + assert target_file.read_text() == target_original + # Source file untouched. + assert config_file.exists() + + captured = capfd.readouterr() + assert "already exists" in captured.out + + +def test_command_rename_same_name_refuses( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename refuses when the new name matches the current name. + + A same-name rename would otherwise re-write the YAML and queue + a redundant compile + install — wasted work the user almost + certainly didn't intend. + """ + config_file = tmp_path / "samename.yaml" + config_file.write_text(""" +esphome: + name: samename + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "samename"}} + + args = MockArgs(name="samename", dashboard=False) + + result = command_rename(args, {}) + + assert result == 1 + mock_run_external_process.assert_not_called() + # File preserved verbatim — no rewrite happened. + assert config_file.exists() + + captured = capfd.readouterr() + assert "already" in captured.out.lower() + + +def test_command_rename_does_not_touch_friendly_name_substring( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + r"""Test rename does not match the ``name:`` substring of ``friendly_name:``. + + Without anchoring the regex at line start, the pattern + ``\s*name:\s+<old>`` could match the trailing ``name:`` + substring inside ``friendly_name: <old>``. The rewrite would + flip both lines to the new name, leaving the user with a + silently corrupted ``friendly_name``. + """ + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +esphome: + name: oldname + friendly_name: oldname + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}} + + args = MockArgs(name="newname", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + new_file = tmp_path / "newname.yaml" + content = new_file.read_text() + # esphome.name swapped. + assert 'name: "newname"' in content + # friendly_name kept verbatim. + assert "friendly_name: oldname" in content + + +def test_command_rename_does_not_match_old_name_as_value_prefix( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + r"""Test rename does not match ``old_name`` as a prefix of a longer value. + + With ``old_name = kitchen`` the value ``kitchen2`` (a sensor + or wifi entry) would otherwise match the unanchored + ``["']?kitchen["']?`` pattern at the prefix and get + rewritten to the new name. The end-of-value lookahead keeps + the match restricted to whole tokens. + """ + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: kitchen + +esp32: + board: nodemcu-32s + +wifi: + ap: + ssid: kitchen2 +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="garage", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + new_file = tmp_path / "garage.yaml" + content = new_file.read_text() + assert 'name: "garage"' in content + # The wifi ssid value is unrelated and stays intact. + assert "ssid: kitchen2" in content + + +def test_command_rename_same_resolved_name_refuses( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename refuses when ``new_name`` matches the resolved device name. + + The path-equality check only catches the case where the + config filename matches the device name. For a config whose + filename and ``esphome.name`` differ (here ``weird-file.yaml`` + holds ``esphome.name: kitchen``), running + ``esphome rename weird-file.yaml kitchen`` would otherwise + fall through to the rewrite + install: the YAML's name stays + ``kitchen``, the file is renamed to ``kitchen.yaml``, and the + device gets a redundant flash. Refuse up-front so the + "already the device's name" message matches reality. + """ + config_file = tmp_path / "weird-file.yaml" + config_file.write_text(""" +esphome: + name: kitchen + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="kitchen", dashboard=False) + + result = command_rename(args, {}) + + assert result == 1 + mock_run_external_process.assert_not_called() + # Source file untouched, no derived target written. + assert config_file.exists() + assert not (tmp_path / "kitchen.yaml").exists() + + captured = capfd.readouterr() + assert "already" in captured.out.lower() + + +def test_command_rename_target_path_equals_source_refuses( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename refuses when the new path resolves to the source file. + + Reachable only when the YAML's filename and ``esphome.name`` + disagree — here ``kitchen.yaml`` holds ``esphome.name: garage`` + and the user runs ``esphome rename kitchen.yaml kitchen``. The + name-equality check above passes (``garage != kitchen``), but + ``<config_dir>/kitchen.yaml`` resolves to the source file + itself, so the rewrite would clobber the source mid-rename. + Refuse rather than silently overwriting. + """ + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: garage + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "garage"}} + + args = MockArgs(name="kitchen", dashboard=False) + + result = command_rename(args, {}) + + assert result == 1 + mock_run_external_process.assert_not_called() + # Source file still present and unmodified. + assert config_file.exists() + assert "name: garage" in config_file.read_text() + + captured = capfd.readouterr() + assert "already" in captured.out.lower() + + +def test_command_rename_does_not_touch_lookalike_name_in_other_blocks( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + """Test rename only swaps the esphome.name line. + + A device whose name happens to match a sensor's / output's + ``name:`` value must not have those other names rewritten — + they're independent. Without an anchor for the esphome block + a naive regex would clobber every line whose value matches. + """ + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: kitchen + +esp32: + board: nodemcu-32s + +sensor: + - platform: template + name: kitchen + lambda: 'return 0;' +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="garage", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + + new_file = tmp_path / "garage.yaml" + content = new_file.read_text() + # esphome.name renamed. + assert 'name: "garage"' in content + # Sensor's name is the user's entity name — must not be touched. + assert " name: kitchen\n" in content + + +def test_command_rename_preserves_trailing_comment( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + """Test rename preserves a trailing ``# comment`` on the name line.""" + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: kitchen # primary device + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="garage", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + + new_file = tmp_path / "garage.yaml" + content = new_file.read_text() + assert "# primary device" in content + + +def test_command_rename_handles_double_quoted_value( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + """Test rename matches when the existing value is double-quoted.""" + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: "kitchen" + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="garage", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + new_file = tmp_path / "garage.yaml" + assert 'name: "garage"' in new_file.read_text() + + +def test_command_rename_handles_single_quoted_value( + tmp_path: Path, + mock_run_external_process: Mock, +) -> None: + """Test rename matches when the existing value is single-quoted.""" + config_file = tmp_path / "kitchen.yaml" + config_file.write_text(""" +esphome: + name: 'kitchen' + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = {CONF_ESPHOME: {CONF_NAME: "kitchen"}} + + args = MockArgs(name="garage", dashboard=False) + mock_run_external_process.return_value = 0 + + result = command_rename(args, {}) + + assert result == 0 + new_file = tmp_path / "garage.yaml" + assert 'name: "garage"' in new_file.read_text() + + +def test_command_rename_too_many_substitution_matches_refuses( + tmp_path: Path, + capfd: CaptureFixture[str], + mock_run_external_process: Mock, +) -> None: + """Test rename refuses when ``${var}`` resolves to multiple matches. + + When ``esphome.name: ${device_name}`` and the substitution + definition ``device_name: foo`` appears more than once in the + YAML (e.g. inside multiple included blocks), the regex rewrite + can't tell which one to flip. Rather than silently picking one + or rewriting both, the command refuses. + """ + config_file = tmp_path / "oldname.yaml" + config_file.write_text(""" +substitutions: + device_name: oldname + +esphome: + name: ${device_name} + +# A copy-pasted block that re-declares the substitution at the +# same indent level - happens when users splice in a packaged +# fragment without renaming the variable. +example: + device_name: oldname + +esp32: + board: nodemcu-32s +""") + setup_core(tmp_path=tmp_path) + CORE.config_path = config_file + CORE.config = { + CONF_ESPHOME: {CONF_NAME: "oldname"}, + CONF_SUBSTITUTIONS: {"device_name": "oldname"}, + } + + args = MockArgs(name="newname", dashboard=False) + + result = command_rename(args, {}) + + assert result == 1 + mock_run_external_process.assert_not_called() + # File untouched. + assert config_file.exists() + assert "device_name: oldname" in config_file.read_text() + + captured = capfd.readouterr() + assert "Too many matches" in captured.out + + def test_command_update_all_path_string_conversion( tmp_path: Path, mock_run_external_process: Mock, From e9cc10fedca583767a5351b520cc2c64126babd8 Mon Sep 17 00:00:00 2001 From: Diorcet Yann <diorcety@users.noreply.github.com> Date: Mon, 11 May 2026 04:12:07 +0200 Subject: [PATCH 1914/2056] [core] Native idf full support (#14678) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/workflows/ci.yml | 86 ++ esphome/__main__.py | 124 ++- esphome/build_gen/espidf.py | 24 +- esphome/components/esp32/__init__.py | 91 +- esphome/components/ethernet/__init__.py | 3 +- esphome/components/neopixelbus/light.py | 1 + esphome/components/nrf52/ota.py | 2 +- esphome/const.py | 9 +- esphome/core/__init__.py | 25 +- esphome/espidf/__init__.py | 0 esphome/espidf/api.py | 499 ++++++++++ esphome/espidf/component.py | 937 ++++++++++++++++++ esphome/espidf/framework.py | 1098 +++++++++++++++++++++ esphome/espidf/get_idf_tool_paths.py | 51 + esphome/espidf/get_idf_version.py | 14 + esphome/espidf/runner.py | 223 +++++ esphome/espidf_api.py | 274 ----- esphome/espota2.py | 2 +- esphome/helpers.py | 13 +- esphome/web_server_ota.py | 2 +- script/ci-custom.py | 2 +- script/test_build_components.py | 51 +- tests/component_tests/esp32/test_esp32.py | 4 +- tests/unit_tests/test_core.py | 4 +- tests/unit_tests/test_espidf_component.py | 357 +++++++ tests/unit_tests/test_espota2.py | 3 +- tests/unit_tests/test_main.py | 2 + 27 files changed, 3537 insertions(+), 364 deletions(-) create mode 100644 esphome/espidf/__init__.py create mode 100644 esphome/espidf/api.py create mode 100644 esphome/espidf/component.py create mode 100644 esphome/espidf/framework.py create mode 100644 esphome/espidf/get_idf_tool_paths.py create mode 100644 esphome/espidf/get_idf_version.py create mode 100644 esphome/espidf/runner.py delete mode 100644 esphome/espidf_api.py create mode 100644 tests/unit_tests/test_espidf_component.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f43c46dd006..9909d7a5ddf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -783,6 +783,91 @@ jobs: # Run compilation with grouping and isolation python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv" + test-native-idf: + name: Test components with native ESP-IDF + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: github.event_name == 'pull_request' + env: + ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf + TEST_COMPONENTS: esp32,api,heatpumpir,bme280_i2c,bh1750,aht10,esp32_ble,esp32_ble_beacon,esp32_ble_client,esp32_ble_server,esp32_ble_tracker,ble_client,ble_presence,ble_rssi,ble_scanner + steps: + - name: Check out code from GitHub + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Cache ESPHome + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ~/.esphome-idf + key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }} + + - name: Run native ESP-IDF compile test + run: | + . venv/bin/activate + + # Check if /mnt has more free space than / before bind mounting + # Extract available space in KB for comparison + root_avail=$(df -k / | awk 'NR==2 {print $4}') + mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}') + + echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB" + + # Only use /mnt if it has more space than / + if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then + echo "Using /mnt for build files (more space available)" + # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there) + sudo mkdir -p /mnt/esphome-idf + sudo chown $USER:$USER /mnt/esphome-idf + mkdir -p ~/.esphome-idf + sudo mount --bind /mnt/esphome-idf ~/.esphome-idf + + # Bind mount test build directory to /mnt + sudo mkdir -p /mnt/test_build_components_build + sudo chown $USER:$USER /mnt/test_build_components_build + mkdir -p tests/test_build_components/build + sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build + else + echo "Using / for build files (more space available than /mnt or /mnt unavailable)" + fi + + echo "Testing components: $TEST_COMPONENTS" + echo "" + + # Show disk space before validation (after bind mounts setup) + echo "Disk space before config validation:" + df -h + echo "" + + # Run config validation (auto-grouped by test_build_components.py) + python3 script/test_build_components.py -e config -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf + + echo "" + echo "Config validation passed! Starting compilation..." + echo "" + + # Show disk space before compilation + echo "Disk space before compilation:" + df -h + echo "" + + # Run compilation (auto-grouped by test_build_components.py) + python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf + + - name: Save ESPHome cache + if: github.ref == 'refs/heads/dev' + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ~/.esphome-idf + key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }} + pre-commit-ci-lite: name: pre-commit.ci lite runs-on: ubuntu-latest @@ -1114,6 +1199,7 @@ jobs: - determine-jobs - device-builder - test-build-components-split + - test-native-idf - pre-commit-ci-lite - memory-impact-target-branch - memory-impact-pr-branch diff --git a/esphome/__main__.py b/esphome/__main__.py index a0ee5a359f4..01b33eb8acf 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -52,12 +52,12 @@ from esphome.const import ( CONF_WEB_SERVER, ENV_NOGITIGNORE, KEY_CORE, - KEY_NATIVE_IDF, KEY_TARGET_PLATFORM, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, SECRETS_FILES, + Toolchain, ) from esphome.core import CORE, EsphomeError, coroutine from esphome.enum import StrEnum @@ -155,7 +155,6 @@ class ArgsProtocol(Protocol): configuration: str name: str upload_speed: str | None - native_idf: bool def choose_prompt(options, purpose: str = None): @@ -720,17 +719,14 @@ def _wrap_to_code(name, comp, yaml_util): return wrapped -def write_cpp(config: ConfigType, native_idf: bool = False) -> int: +def write_cpp(config: ConfigType) -> int: from esphome import writer if not get_bool_env(ENV_NOGITIGNORE): writer.write_gitignore() - # Store native_idf flag so esp32 component can check it - CORE.data[KEY_NATIVE_IDF] = native_idf - generate_cpp_contents(config) - return write_cpp_file(native_idf=native_idf) + return write_cpp_file() def generate_cpp_contents(config: ConfigType) -> None: @@ -746,13 +742,13 @@ def generate_cpp_contents(config: ConfigType) -> None: CORE.flush_tasks() -def write_cpp_file(native_idf: bool = False) -> int: +def write_cpp_file() -> int: from esphome import writer code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) - if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf": + if CORE.using_toolchain_esp_idf: from esphome.build_gen import espidf espidf.write_project() @@ -765,22 +761,21 @@ def write_cpp_file(native_idf: bool = False) -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int: - native_idf = getattr(args, "native_idf", False) - # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) - if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf": - from esphome import espidf_api + if CORE.using_toolchain_esp_idf: + from esphome.espidf import api - rc = espidf_api.run_compile(config, CORE.verbose) + rc = api.run_compile(config, CORE.verbose) if rc != 0: return rc - # Create factory.bin and ota.bin - espidf_api.create_factory_bin() - espidf_api.create_ota_bin() + # Create factory.bin, ota.bin, and firmware.elf copy + api.create_factory_bin() + api.create_ota_bin() + api.create_elf_copy() else: from esphome import platformio_api @@ -883,6 +878,10 @@ def upload_using_esptool( if file is not None: flash_images = [FlashImage(path=file, offset="0x0")] + elif CORE.using_toolchain_esp_idf: + from esphome.espidf import api + + flash_images = [FlashImage(path=api.get_factory_firmware_path(), offset="0x0")] else: from esphome import platformio_api @@ -1447,8 +1446,7 @@ def command_vscode(args: ArgsProtocol) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: - native_idf = getattr(args, "native_idf", False) - exit_code = write_cpp(config, native_idf=native_idf) + exit_code = write_cpp(config) if exit_code != 0: return exit_code if args.only_generate: @@ -1458,9 +1456,14 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: if exit_code != 0: return exit_code if CORE.is_host: - from esphome.platformio_api import get_idedata + if CORE.using_toolchain_esp_idf: + from esphome.espidf import api - program_path = str(get_idedata(config).firmware_elf_path) + program_path = str(api.get_elf_path()) + else: + from esphome.platformio_api import get_idedata + + program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Successfully compiled program to path '%s'", program_path) else: _LOGGER.info("Successfully compiled program.") @@ -1503,8 +1506,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None: def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: - native_idf = getattr(args, "native_idf", False) - exit_code = write_cpp(config, native_idf=native_idf) + exit_code = write_cpp(config) if exit_code != 0: return exit_code exit_code = compile_program(args, config) @@ -1512,9 +1514,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: return exit_code _LOGGER.info("Successfully compiled program.") if CORE.is_host: - from esphome.platformio_api import get_idedata + if CORE.using_toolchain_esp_idf: + from esphome.espidf import api - program_path = str(get_idedata(config).firmware_elf_path) + program_path = str(api.get_elf_path()) + else: + from esphome.platformio_api import get_idedata + + program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Running program from path '%s'", program_path) return run_external_process(program_path) @@ -1705,6 +1712,13 @@ def command_update_all(args: ArgsProtocol) -> int | None: def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: import json + if not CORE.using_toolchain_platformio: + _LOGGER.error( + "The idedata command is not compatible with %s toolchain", + CORE.toolchain.value, + ) + return 1 + from esphome import platformio_api logging.disable(logging.INFO) @@ -1724,7 +1738,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: This command compiles the configuration and performs memory analysis. Compilation is fast if sources haven't changed (just relinking). """ - from esphome import platformio_api from esphome.analyze_memory.cli import MemoryAnalyzerCLI from esphome.analyze_memory.ram_strings import RamStringsAnalyzer @@ -1738,12 +1751,25 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: _LOGGER.info("Successfully compiled program.") # Get idedata for analysis - idedata = platformio_api.get_idedata(config) - if idedata is None: - _LOGGER.error("Failed to get IDE data for memory analysis") - return 1 + idedata = None + if CORE.using_toolchain_esp_idf: + from esphome.espidf import api - firmware_elf = Path(idedata.firmware_elf_path) + objdump_path = str(api.get_objdump_path()) + readelf_path = str(api.get_readelf_path()) + + firmware_elf = api.get_elf_path() + else: + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + if idedata is None: + _LOGGER.error("Failed to get IDE data for memory analysis") + return 1 + objdump_path = idedata.objdump_path + readelf_path = idedata.readelf_path + + firmware_elf = Path(idedata.firmware_elf_path) # Extract external components from config external_components = detect_external_components(config) @@ -1753,8 +1779,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: _LOGGER.info("Analyzing memory usage...") analyzer = MemoryAnalyzerCLI( str(firmware_elf), - idedata.objdump_path, - idedata.readelf_path, + objdump_path, + readelf_path, external_components, idedata=idedata, ) @@ -1770,7 +1796,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: try: ram_analyzer = RamStringsAnalyzer( str(firmware_elf), - objdump_path=idedata.objdump_path, + objdump_path=objdump_path, platform=CORE.target_platform, ) ram_analyzer.analyze() @@ -2015,6 +2041,17 @@ def parse_args(argv): action="store_true", default=False, ) + options_parser.add_argument( + "--toolchain", + type=Toolchain, + default=None, + choices=list(Toolchain), + metavar="{" + ",".join(t.value for t in Toolchain) + "}", + help=( + "Select toolchain for compiling. Overrides '<platform>.toolchain' in YAML. " + f"Default: {Toolchain.PLATFORMIO.value}." + ), + ) parser = argparse.ArgumentParser( description=f"ESPHome {const.__version__}", parents=[options_parser] @@ -2059,11 +2096,6 @@ def parse_args(argv): help="Only generate source code, do not compile.", action="store_true", ) - parser_compile.add_argument( - "--native-idf", - help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).", - action="store_true", - ) parser_upload = subparsers.add_parser( "upload", @@ -2171,11 +2203,6 @@ def parse_args(argv): help="Reset the device before starting serial logs.", default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), ) - parser_run.add_argument( - "--native-idf", - help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).", - action="store_true", - ) parser_run.add_argument( "--ota-platform", choices=[CONF_ESPHOME, CONF_WEB_SERVER], @@ -2398,6 +2425,9 @@ def run_esphome(argv): CORE.config_path = conf_path CORE.dashboard = args.dashboard + if args.toolchain is not None: + # CLI toolchain wins over esp32.toolchain in YAML. + CORE.toolchain = args.toolchain # Commands that don't need fresh external components: logs just connects # to the device, and clean is about to delete the build directory. @@ -2410,6 +2440,12 @@ def run_esphome(argv): return 2 CORE.config = config + # Fallback for platforms whose validators didn't set the toolchain + # (only the esp32 component reads esp32.framework.toolchain). All + # other platforms only support PlatformIO today. + if CORE.toolchain is None: + CORE.toolchain = Toolchain.PLATFORMIO + if args.command not in POST_CONFIG_ACTIONS: safe_print(f"Unknown command {args.command}") return 1 diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 01923baaac2..b1443edac31 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -6,6 +6,7 @@ from pathlib import Path from esphome.components.esp32 import get_esp32_variant from esphome.core import CORE from esphome.helpers import mkdir_p, write_file_if_changed +from esphome.writer import update_storage_json def get_available_components() -> list[str] | None: @@ -54,7 +55,7 @@ def get_project_cmakelists() -> str: idf_target = variant.lower().replace("-", "") # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] + compile_defs = [flag for flag in sorted(CORE.build_flags) if flag.startswith("-D")] extra_compile_options = "\n".join( f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' for compile_def in compile_defs @@ -64,6 +65,22 @@ def get_project_cmakelists() -> str: # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) +# On Windows, Ninja can fail with: +# "CreateProcess: The parameter is incorrect (is the command line too long?)" +# when compiler/linker command lines exceed the OS length limit. +# +# The following settings force CMake/Ninja to use *response files* (@file.rsp) +# to pass long lists of includes, objects, and other arguments indirectly, +# avoiding command-line length limits and fixing the build failure. +# +# This is especially useful for large ESP-IDF / ESPHome projects with many +# source files or include directories. +set(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1) +set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES 1) +set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1) +set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1) +set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1) + set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) @@ -124,6 +141,11 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC def write_project(minimal: bool = False) -> None: """Write ESP-IDF project files.""" + # Refresh <data_dir>/storage/<name>.yaml.json so the dashboard's + # /info and /downloads endpoints can locate the build (they 404 + # otherwise). This mirrors the PlatformIO build-gen path's call + # in build_gen/platformio.py:write_ini(). + update_storage_json() mkdir_p(CORE.build_path) mkdir_p(CORE.relative_src_path()) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 582721ef736..ba32d13ab34 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_SIZE, CONF_SOURCE, + CONF_TOOLCHAIN, CONF_TYPE, CONF_VARIANT, CONF_VERSION, @@ -38,16 +39,17 @@ from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, - KEY_NATIVE_IDF, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, ThreadModel, + Toolchain, __version__, ) -from esphome.core import CORE, HexInt +from esphome.core import CORE, HexInt, Library from esphome.core.config import BOARD_MAX_LENGTH from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.espidf.component import generate_idf_component import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType @@ -465,6 +467,9 @@ def set_core_data(config): if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: + if CORE.using_toolchain_esp_idf: + # Official ESP-IDF frameworks don't use extra + idf_ver = cv.Version(idf_ver.major, idf_ver.minor, idf_ver.patch) CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver else: raise cv.Invalid( @@ -652,7 +657,7 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" -def _format_framework_espidf_version( +def _format_framework_pio_espidf_version( ver: cv.Version, release: str | None = None ) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to @@ -741,6 +746,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "latest": cv.Version(5, 5, 4), "dev": cv.Version(5, 5, 4), } + ESP_IDF_PLATFORM_VERSION_LOOKUP = { cv.Version( 6, 0, 1 @@ -774,7 +780,7 @@ PLATFORM_VERSION_LOOKUP = { } -def _check_versions(config): +def _check_pio_versions(config): config = config.copy() value = config[CONF_FRAMEWORK] @@ -785,7 +791,7 @@ def _check_versions(config): ) platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]] - value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup)) if value[CONF_TYPE] == FRAMEWORK_ARDUINO: version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]] @@ -813,7 +819,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE)), + _format_framework_pio_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -823,7 +829,7 @@ def _check_versions(config): raise cv.Invalid( "Framework version not recognized; please specify platform_version" ) - value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup)) + value[CONF_PLATFORM_VERSION] = _parse_pio_platform_version(str(platform_lookup)) if version != recommended_version: _LOGGER.warning( @@ -831,7 +837,7 @@ def _check_versions(config): "If there are connectivity or build issues please remove the manual version." ) - if value[CONF_PLATFORM_VERSION] != _parse_platform_version( + if value[CONF_PLATFORM_VERSION] != _parse_pio_platform_version( str(PLATFORM_VERSION_LOOKUP["recommended"]) ): _LOGGER.warning( @@ -842,7 +848,38 @@ def _check_versions(config): return config -def _parse_platform_version(value): +def _check_esp_idf_versions(config): + config = _check_pio_versions(config) + value = config[CONF_FRAMEWORK] + + # Remove unwanted keys if present + for key in (CONF_SOURCE, CONF_PLATFORM_VERSION): + value.pop(key, None) + + # Official ESP-IDF frameworks don't use extra + version = cv.Version.parse(value[CONF_VERSION]) + version = cv.Version(version.major, version.minor, version.patch) + + value[CONF_VERSION] = str(version) + + return config + + +def _validate_toolchain(value) -> Toolchain: + return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value)) + + +def _check_versions(config): + # Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default. + if CORE.toolchain is None: + CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO) + + if CORE.using_toolchain_esp_idf: + return _check_esp_idf_versions(config) + return _check_pio_versions(config) + + +def _parse_pio_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" @@ -1272,7 +1309,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, cv.Optional(CONF_RELEASE): cv.string_strict, cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version, cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.string_strict: cv.string_strict }, @@ -1524,6 +1561,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, + cv.Optional(CONF_TOOLCHAIN): _validate_toolchain, cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All( cv.positive_time_period_seconds, cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)), @@ -1672,11 +1710,11 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] conf = config[CONF_FRAMEWORK] - # Check if using native ESP-IDF build (--native-idf) - use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False) + # Check if using ESP-IDF toolchain + use_platformio = not CORE.using_toolchain_esp_idf if use_platformio: # Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF - # but keep them when using --native-idf for native ESP-IDF builds + # but keep them when using ESP-IDF toolchain for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): os.environ.pop(clean_var, None) @@ -1716,6 +1754,8 @@ async def to_code(config): ) else: cg.add_build_flag("-Wno-error=format") + cg.add_build_flag("-Wno-error=missing-field-initializers") + cg.add_build_flag("-Wno-error=volatile") cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") @@ -1792,7 +1832,7 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver)], + [_format_framework_pio_espidf_version(idf_ver)], ) # Use stub package to skip downloading precompiled libs stubs_dir = CORE.relative_build_path("arduino_libs_stub") @@ -2424,6 +2464,14 @@ def _write_sdkconfig(): clean_build(clear_pio_cache=False) +def _platformio_library_to_dependency(library: Library) -> tuple[str, dict[str, str]]: + dependency: dict[str, str] = {} + name, version, path = generate_idf_component(library) + dependency["override_path"] = str(path) + dependency["version"] = version + return name, dependency + + def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") dependencies: dict[str, dict] = {} @@ -2465,6 +2513,21 @@ def _write_idf_component_yml(): if stub_path.exists(): rmtree(stub_path) + if CORE.using_toolchain_esp_idf: + add_idf_component( + name="espressif/arduino-esp32", + ref=str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]), + ) + + if CORE.using_toolchain_esp_idf: + # Try to convert PlatformIO library to ESP-IDF components + for name, library in CORE.platformio_libraries.items(): + # Don't process arduino libraries + if name in ARDUINO_DISABLED_LIBRARIES: + continue + dependency_name, dependency = _platformio_library_to_dependency(library) + dependencies[dependency_name] = dependency + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] for name, component in components.items(): diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 10f9a73863d..3f88f8ef9a4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -36,7 +36,6 @@ from esphome.const import ( CONF_VALUE, KEY_CORE, KEY_FRAMEWORK_VERSION, - KEY_NATIVE_IDF, Platform, PlatformFramework, ) @@ -705,7 +704,7 @@ def _filter_source_files() -> list[str]: # and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x) if eth_type != "JL1101": excluded.append("esp_eth_phy_jl1101.c") - elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False): + elif CORE.is_esp32 and not CORE.using_toolchain_esp_idf: from esphome.components.esp32 import idf_version # pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 104762c69e0..943fd141f69 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -244,6 +244,7 @@ async def to_code(config): # disable built in rgb support as it uses the new RMT drivers and will # conflict with NeoPixelBus which uses the legacy drivers cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN") + cg.add_library("SPI", None) cg.add_library("makuna/NeoPixelBus", "2.8.0") else: cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/esphome/components/nrf52/ota.py b/esphome/components/nrf52/ota.py index e4b26b45eb6..eb1caa55958 100644 --- a/esphome/components/nrf52/ota.py +++ b/esphome/components/nrf52/ota.py @@ -142,7 +142,7 @@ async def _smpmgr_upload_connected( with open(firmware, "rb") as file: image = file.read() upload_size = len(image) - progress = ProgressBar() + progress = ProgressBar("Uploading") progress.update(0) try: async for offset in smp_client.upload(image): diff --git a/esphome/const.py b/esphome/const.py index c2bf86d5321..c39225fdec0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -15,6 +15,13 @@ VALID_SUBSTITUTIONS_CHARACTERS = ( ARGUMENT_HELP_DEVICE = "Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses. Use 'OTA' for resolving from MQTT, DNS or mDNS and avoiding the interactive prompt." +class Toolchain(StrEnum): + """Toolchain identifiers for ESPHome.""" + + PLATFORMIO = "platformio" + ESP_IDF = "esp-idf" + + class Platform(StrEnum): """Platform identifiers for ESPHome.""" @@ -1036,6 +1043,7 @@ CONF_TO = "to" CONF_TO_NTC_RESISTANCE = "to_ntc_resistance" CONF_TO_NTC_TEMPERATURE = "to_ntc_temperature" CONF_TOLERANCE = "tolerance" +CONF_TOOLCHAIN = "toolchain" CONF_TOPIC = "topic" CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOTAL = "total" @@ -1393,7 +1401,6 @@ KEY_FRAMEWORK_VERSION = "framework_version" KEY_NAME = "name" KEY_VARIANT = "variant" KEY_PAST_SAFE_MODE = "past_safe_mode" -KEY_NATIVE_IDF = "native_idf" # Entity categories ENTITY_CATEGORY_NONE = "" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 0cc207aa543..ef0eddc603a 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -17,7 +17,6 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WIFI, KEY_CORE, - KEY_NATIVE_IDF, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_BK72XX, @@ -28,6 +27,7 @@ from esphome.const import ( PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, + Toolchain, ) # pylint: disable=unused-import @@ -618,6 +618,10 @@ class EsphomeCore: # When True, skip network freshness checks for cached external files # (e.g. for `esphome logs`, where remote downloads aren't needed) self.skip_external_update: bool = False + # Toolchain used for building the configuration. None until resolved + # by CLI (--toolchain) or by `esphome.toolchain:` in YAML during + # preload_core_config; defaults to PLATFORMIO if neither sets it. + self.toolchain: Toolchain | None = None def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -648,6 +652,7 @@ class EsphomeCore: self.address_cache = None self._config_hash = None self.skip_external_update = False + self.toolchain = None PIN_SCHEMA_REGISTRY.reset() @contextmanager @@ -772,8 +777,8 @@ class EsphomeCore: @property def firmware_bin(self) -> Path: - # Check if using native ESP-IDF build (--native-idf) - if self.data.get(KEY_NATIVE_IDF, False): + # Check if using ESP-IDF toolchain + if self.using_toolchain_esp_idf: return self.relative_build_path("build", f"{self.name}.bin") if self.is_libretiny: return self.relative_pioenvs_path(self.name, "firmware.uf2") @@ -781,10 +786,10 @@ class EsphomeCore: @property def partition_table_bin(self) -> Path: - # Native ESP-IDF (--native-idf): the partition table image is emitted under + # Native ESP-IDF (--toolchain esp-idf): the partition table image is emitted under # build/partition_table/partition-table.bin alongside firmware.bin. PlatformIO writes the # equivalent file as partitions.bin in the env-specific .pioenvs directory. - if self.data.get(KEY_NATIVE_IDF): + if self.using_toolchain_esp_idf: return self.relative_build_path( "build", "partition_table", "partition-table.bin" ) @@ -792,7 +797,7 @@ class EsphomeCore: @property def bootloader_bin(self) -> Path: - if self.data.get(KEY_NATIVE_IDF): + if self.using_toolchain_esp_idf: return self.relative_build_path("build", "bootloader", "bootloader.bin") return self.relative_pioenvs_path(self.name, "bootloader.bin") @@ -853,6 +858,14 @@ class EsphomeCore: ) return self.target_framework == "esp-idf" + @property + def using_toolchain_esp_idf(self): + return self.toolchain == Toolchain.ESP_IDF + + @property + def using_toolchain_platformio(self): + return self.toolchain == Toolchain.PLATFORMIO + @property def using_zephyr(self): return self.target_framework == "zephyr" diff --git a/esphome/espidf/__init__.py b/esphome/espidf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/espidf/api.py b/esphome/espidf/api.py new file mode 100644 index 00000000000..847de249a7f --- /dev/null +++ b/esphome/espidf/api.py @@ -0,0 +1,499 @@ +"""ESP-IDF direct build API for ESPHome.""" + +from dataclasses import dataclass, field +import json +import logging +import os +from pathlib import Path +import re +import shutil +import subprocess + +from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION +from esphome.core import CORE, EsphomeError +from esphome.espidf.framework import check_esp_idf_install, get_framework_env + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "espidf_api" + + +@dataclass +class _CacheData: + paths: dict[str, tuple] = field(default_factory=dict) + env: dict[str, dict[str, str]] = field(default_factory=dict) + cmake_output: dict[Path, str] = field(default_factory=dict) + cmake_tools: dict[Path, dict[str, Path]] = field(default_factory=dict) + + +def _cache() -> _CacheData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = _CacheData() + return CORE.data[DOMAIN] + + +def _get_core_framework_version(): + return str(CORE.data[KEY_ESP32][KEY_IDF_VERSION]) + + +def _get_esphome_esp_idf_paths( + version: str | None = None, +) -> tuple[os.PathLike, os.PathLike]: + version = version or _get_core_framework_version() + paths = _cache().paths + if version not in paths: + paths[version] = check_esp_idf_install(version) + return paths[version] + + +def _get_idf_path(version: str | None = None) -> Path | None: + """Get IDF_PATH from environment or common locations.""" + # Use provided IDF framework if available + if "IDF_PATH" in os.environ: + return Path(os.environ["IDF_PATH"]) + return Path(_get_esphome_esp_idf_paths(version)[0]) + + +def _get_idf_env(version: str | None = None) -> dict[str, str]: + """Get environment variables needed for ESP-IDF build.""" + version = version or _get_core_framework_version() + env_cache = _cache().env + if version not in env_cache: + env_cache[version] = os.environ.copy() + + # Use provided IDF framework if available + if "IDF_PATH" not in os.environ: + env_cache[version] |= get_framework_env( + *_get_esphome_esp_idf_paths(version) + ) + return env_cache[version] + + +def _get_cmake_output(build_dir) -> str: + cmake_output_cache = _cache().cmake_output + if build_dir not in cmake_output_cache: + cmd = ["cmake", "-LA", "-N", "."] + + env = _get_idf_env() + result = subprocess.run( + cmd, + cwd=build_dir, + env=env, + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + raise RuntimeError(f"CMake failed: {result.stderr}") + + cmake_output_cache[build_dir] = result.stdout + return cmake_output_cache[build_dir] + + +def _get_cmake_tool_path(var_name: str) -> Path: + build_dir = CORE.relative_build_path("build") + cmake_output = _get_cmake_output(build_dir) + + cmake_tools_cache = _cache().cmake_tools + if build_dir not in cmake_tools_cache: + cmake_tools_cache[build_dir] = {} + + if var_name not in cmake_tools_cache[build_dir]: + pattern = rf"^{var_name}:FILEPATH=(.+)$" + match = re.search(pattern, cmake_output, re.MULTILINE) + + if not match: + raise RuntimeError(f"{var_name} not found in CMake output") + + path = match.group(1).strip() + cmake_tools_cache[build_dir][var_name] = Path(path) + + return cmake_tools_cache[build_dir][var_name] + + +def _get_idf_tool(name: str) -> str: + """Return the path to an executable from the ESP-IDF environment PATH or raise if not found.""" + env = _get_idf_env() + executable = shutil.which(name, path=env.get("PATH", None)) + if executable is None: + raise EsphomeError( + f"{name} executable not found in ESP-IDF environment. " + "Check that the IDF environment is correctly set up." + ) + return executable + + +def run_idf_py( + *args, cwd: Path | None = None, capture_output: bool = False +) -> int | str: + """Run idf.py with the given arguments.""" + idf_path = _get_idf_path() + if idf_path is None: + raise EsphomeError("ESP-IDF not found") + + env = _get_idf_env() + python_executable = _get_idf_tool("python") + idf_py = idf_path / "tools" / "idf.py" + # Dispatch idf.py through esphome.espidf.runner, which wraps + # sys.stdout/sys.stderr so ``isatty()`` reports True. This keeps CMake, + # Ninja, and idf.py's own progress-bar code emitting TTY-format output + # (``\r`` cursor moves, ANSI colors, fancy progress bars) even when our + # real stdout is a pipe — e.g. when esphome is running under the Home + # Assistant dashboard add-on. The runner is a plain script (not a + # ``python -m`` module) because IDF's Python venv does not have the + # esphome package installed. + runner_py = Path(__file__).parent / "runner.py" + + cmd = [python_executable, str(runner_py), str(idf_py)] + list(args) + + if cwd is None: + cwd = CORE.build_path + + _LOGGER.debug("Running: %s", " ".join(cmd)) + _LOGGER.debug(" in directory: %s", cwd) + + if capture_output: + result = subprocess.run( + cmd, + cwd=cwd, + env=env, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + _LOGGER.error("idf.py failed:\n%s", result.stderr) + return result.stdout + result = subprocess.run( + cmd, + cwd=cwd, + env=env, + check=False, + ) + return result.returncode + + +def _get_sdkconfig_args() -> list[str]: + """Get cmake -D flags for the sdkconfig file, if it exists.""" + sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") + if sdkconfig_path.is_file(): + return ["-D", f"SDKCONFIG={sdkconfig_path}"] + return [] + + +def run_reconfigure() -> int: + """Run cmake reconfigure only (no build).""" + return run_idf_py(*_get_sdkconfig_args(), "reconfigure") + + +def has_outdated_files(): + """Check if the build configuration is stale. + + Returns True if required build files are missing or if configuration inputs + are newer than the generated CMake/Ninja build artifacts. + """ + cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") + + cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") + cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") + build_config_path = CORE.relative_build_path("build/config") + sdkconfig_internal_path = CORE.relative_build_path( + f"sdkconfig.{CORE.name}.esphomeinternal" + ) + dependency_lock_path = CORE.relative_build_path("dependencies.lock") + build_ninja_path = CORE.relative_build_path("build/build.ninja") + + if not os.path.isdir(build_config_path) or not os.listdir(build_config_path): + return True + if not os.path.isfile(cmakecache_txt_path): + return True + if not os.path.isfile(build_ninja_path): + return True + if os.path.isfile(dependency_lock_path) and os.path.getmtime( + dependency_lock_path + ) > os.path.getmtime(build_ninja_path): + return True + + cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) + return any( + os.path.getmtime(f) > cmakecache_txt_mtime + for f in [ + _get_idf_path(), + cmakelists_txt_build_path, + cmakelists_txt_src_path, + sdkconfig_internal_path, + build_config_path, + ] + if f and os.path.exists(f) + ) + + +def need_reconfigure() -> bool: + from esphome.build_gen.espidf import has_discovered_components + + # We need to reconfigure either if the files are outdated or if there is no component discovered + return has_outdated_files() or not has_discovered_components() + + +def _patch_memory_segments(): + """Patch memory.ld to expand IRAM/DRAM for testing mode. + + Mirrors the PlatformIO iram_fix.py.script logic for native IDF builds. + Must be called after cmake configure (which generates memory.ld) and + before the build/link step. + """ + # Same sizes as iram_fix.py.script + testing_iram_size = 0x200000 # 2MB + testing_dram_size = 0x200000 # 2MB + + memory_ld = CORE.relative_build_path( + "build", "esp-idf", "esp_system", "ld", "memory.ld" + ) + if not memory_ld.is_file(): + _LOGGER.warning("Could not find linker script at %s", memory_ld) + return + + content = memory_ld.read_text() + patches = [] + + def _patch_segment(text, segment_name, new_size): + pattern = rf"({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)" + if match := re.search(pattern, text, re.DOTALL): + replacement = f"{match.group(1)}{new_size:#x}" + new_text = text[: match.start()] + replacement + text[match.end() :] + if new_text != text: + return new_text, True + return text, False + + content, patched = _patch_segment(content, "iram0_0_seg", testing_iram_size) + if patched: + patches.append(f"IRAM={testing_iram_size:#x}") + + content, patched = _patch_segment(content, "dram0_0_seg", testing_dram_size) + if patched: + patches.append(f"DRAM={testing_dram_size:#x}") + + if patches: + memory_ld.write_text(content) + _LOGGER.info("Patched %s in %s for testing mode", ", ".join(patches), memory_ld) + else: + _LOGGER.warning("Could not patch memory segments in %s", memory_ld) + + +def run_compile(config, verbose: bool) -> int: + """Compile the ESP-IDF project. + + Uses two-phase configure to auto-discover available components: + 1. If no previous build, configure with minimal REQUIRES to discover components + 2. Regenerate CMakeLists.txt with discovered components + 3. Run full build + """ + from esphome.build_gen.espidf import write_project + + # Check if we need to do discovery phase + if need_reconfigure(): + _LOGGER.info("Discovering available ESP-IDF components...") + write_project(minimal=True) + rc = run_reconfigure() + if rc != 0: + _LOGGER.error("Component discovery failed") + return rc + _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") + write_project(minimal=False) + if CORE.testing_mode: + # Reconfigure again so cmake is up to date with the full component + # list. This ensures idf.py build won't re-run cmake, which would + # regenerate memory.ld and wipe the DRAM/IRAM patches applied below. + rc = run_reconfigure() + if rc != 0: + _LOGGER.error("Reconfigure with discovered components failed") + return rc + + # In testing mode, generate the linker script first, patch DRAM/IRAM sizes, + # then build. memory.ld is regenerated by ninja during the build phase, + # so we must patch after it's generated but before linking (same timing + # as iram_fix.py.script's AddPreAction hook in the PlatformIO path). + if CORE.testing_mode: + memory_ld = CORE.relative_build_path( + "build", "esp-idf", "esp_system", "ld", "memory.ld" + ) + build_dir = CORE.relative_build_path("build") + # Build just the memory.ld target - ninja needs the path relative to build dir + memory_ld_target = os.path.relpath(str(memory_ld), str(build_dir)) + env = _get_idf_env() + ninja_executable = _get_idf_tool("ninja") + result = subprocess.run( + [ninja_executable, "-C", str(build_dir), memory_ld_target], + env=env, + check=False, + ) + if result.returncode != 0: + _LOGGER.error("Failed to generate linker script") + return result.returncode + _patch_memory_segments() + + # Build + args = [] + + if verbose: + args.append("-v") + + args.extend(_get_sdkconfig_args()) + args.append("build") + + return run_idf_py(*args) + + +def get_firmware_path() -> Path: + """Get the path to the compiled firmware binary. + + This is the file idf.py writes directly (named after the project), + not the copy used for OTA/factory downloads below. + """ + build_dir = CORE.relative_build_path("build") + return build_dir / f"{CORE.name}.bin" + + +def get_factory_firmware_path() -> Path: + """Get the path to the factory firmware (with bootloader). + + Uses the PlatformIO ``firmware.factory.bin`` naming convention so + the dashboard's download handler — which requests files by name + relative to ``firmware_bin_path.parent`` — finds it. Without this, + the native IDF path produced ``<name>.factory.bin`` and the + dashboard returned 500 trying to locate ``firmware.factory.bin``. + """ + build_dir = CORE.relative_build_path("build") + return build_dir / "firmware.factory.bin" + + +def get_ota_firmware_path() -> Path: + """Get the path to the OTA firmware binary. + + Uses the PlatformIO ``firmware.ota.bin`` naming convention for the + same dashboard-compatibility reason as ``get_factory_firmware_path``. + """ + build_dir = CORE.relative_build_path("build") + return build_dir / "firmware.ota.bin" + + +def get_elf_path() -> Path: + """Get the path to the firmware ELF file. + + idf.py writes ``<build>/<name>.elf`` directly; this returns the + ``<build>/firmware.elf`` copy created by ``create_elf_copy`` so + the dashboard's "download ELF" link can find it under the + PlatformIO-convention name. + """ + build_dir = CORE.relative_build_path("build") + return build_dir / "firmware.elf" + + +def get_objdump_path() -> Path: + return _get_cmake_tool_path("CMAKE_OBJDUMP") + + +def get_readelf_path() -> Path: + return _get_cmake_tool_path("CMAKE_READELF") + + +def get_addr2line_path() -> Path: + return _get_cmake_tool_path("CMAKE_ADDR2LINE") + + +def create_factory_bin() -> bool: + """Create factory.bin by merging bootloader, partition table, and app.""" + build_dir = CORE.relative_build_path("build") + flasher_args_path = build_dir / "flasher_args.json" + + if not flasher_args_path.is_file(): + _LOGGER.warning("flasher_args.json not found, cannot create factory.bin") + return False + + try: + with open(flasher_args_path, encoding="utf-8") as f: + flash_data = json.load(f) + except (json.JSONDecodeError, OSError) as e: + _LOGGER.error("Failed to read flasher_args.json: %s", e) + return False + + # Get flash size from config + flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] + + # Build esptool merge command + sections = [] + for addr, fname in sorted( + flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16) + ): + file_path = build_dir / fname + if file_path.is_file(): + sections.extend([addr, str(file_path)]) + else: + _LOGGER.warning("Flash file not found: %s", file_path) + + if not sections: + _LOGGER.warning("No flash sections found") + return False + + output_path = get_factory_firmware_path() + chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32") + + env = _get_idf_env() + python_executable = _get_idf_tool("python") + cmd = [ + python_executable, + "-m", + "esptool", + "--chip", + chip, + "merge_bin", + "--flash_size", + flash_size, + "--output", + str(output_path), + ] + sections + + _LOGGER.info("Creating factory.bin...") + result = subprocess.run(cmd, env=env, capture_output=True, text=True, check=False) + + if result.returncode != 0: + _LOGGER.error("Failed to create factory.bin: %s", result.stderr) + return False + + _LOGGER.info("Created: %s", output_path) + return True + + +def create_ota_bin() -> bool: + """Copy the firmware to firmware.ota.bin for ESPHome OTA compatibility.""" + firmware_path = get_firmware_path() + ota_path = get_ota_firmware_path() + + if not firmware_path.is_file(): + _LOGGER.warning("Firmware not found: %s", firmware_path) + return False + + shutil.copy(firmware_path, ota_path) + _LOGGER.info("Created: %s", ota_path) + return True + + +def create_elf_copy() -> bool: + """Copy the ELF binary to firmware.elf for dashboard compatibility. + + idf.py writes the ELF at ``<build>/<name>.elf``; the dashboard's + "download ELF" link requests the literal filename ``firmware.elf`` + (PlatformIO convention), so copy it to that name. + """ + build_dir = CORE.relative_build_path("build") + src_elf = build_dir / f"{CORE.name}.elf" + dst_elf = get_elf_path() + + if not src_elf.is_file(): + _LOGGER.warning("ELF not found: %s", src_elf) + return False + + shutil.copy(src_elf, dst_elf) + _LOGGER.info("Created: %s", dst_elf) + return True diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py new file mode 100644 index 00000000000..bb675d2c77c --- /dev/null +++ b/esphome/espidf/component.py @@ -0,0 +1,937 @@ +from collections.abc import Callable +import glob +import hashlib +import itertools +import json +import logging +import os +from pathlib import Path +import re +import tempfile +from typing import TypeVar +from urllib.parse import urlparse, urlsplit, urlunsplit + +from esphome import git, yaml_util +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.core import CORE, Library +from esphome.espidf.framework import archive_extract_all, download_from_mirrors, rmdir +from esphome.helpers import write_file_if_changed + +_LOGGER = logging.getLogger(__name__) + +PathType = str | os.PathLike + +# +# Constants from platformio +# + +FILTER_REGEX = re.compile(r"([+-])<([^>]+)>") +DEFAULT_BUILD_SRC_FILTER = ( + "+<*> -<.git/> -<.svn/> -<example/> -<examples/> -<test/> -<tests/>" +) +DEFAULT_BUILD_SRC_DIRS = "src" +DEFAULT_BUILD_INCLUDE_DIR = "include" +DEFAULT_BUILD_FLAGS = [] +SRC_FILE_EXTENSIONS = [ + ".c", + ".cpp", + ".cc", + ".cxx", + ".c++", + ".S", + ".spp", + ".SPP", + ".sx", + ".s", + ".asm", + ".ASM", +] + +ESP32_PLATFORM = "espressif32" +DOMAIN = "pio_components" + +# +# Constants for workarounds +# + +REQUIRES_DETECT_PATTERNS = { + "mbedtls": [re.compile(r'^\s*#\s*include\s*[<"]mbedtls[^">]*[">]', re.MULTILINE)], + "esp_netif": [ + re.compile(r'^\s*#\s*include\s*[<"]esp_netif[^">]*[">]', re.MULTILINE) + ], + "esp_driver_gpio": [ + re.compile(r'^\s*#\s*include\s*[<"]driver/gpio\.h[^">]*[">]', re.MULTILINE) + ], + "esp_timer": [ + re.compile(r'^\s*#\s*include\s*[<"]esp_timer\.h[^">]*[">]', re.MULTILINE) + ], + "esp_wifi": [ + re.compile( + r'^\s*#\s*include\s*[<"]WiFi\.h[^">]*[">]', re.MULTILINE + ) # Arduino WiFi + ], +} + +ESPHOME_DATA_KEY = "ESPHOME" +ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE" + + +class Source: + def download(self, dir_suffix: str, force: bool = False) -> Path: + raise NotImplementedError() + + +class URLSource(Source): + def __init__(self, url: str): + self.url = url + + def download(self, dir_suffix: str, force: bool = False) -> Path: + base_dir = Path(CORE.data_dir) / DOMAIN + h = hashlib.new("sha256") + h.update(self.url.encode()) + path = base_dir / h.hexdigest()[:8] / dir_suffix + # Marker file written last to signal a complete extraction. Using a + # marker (instead of just `path.is_dir()`) means an interrupted + # extraction is correctly detected and re-run on the next invocation, + # and lets us extract directly into ``path`` — avoiding a + # post-extraction rename that races with antivirus on Windows. + extracted_marker = path / ".esphome_extracted" + if not extracted_marker.is_file() or force: + rmdir(path, msg=f"Clean up library directory {path}") + + # Download in temporary file + with tempfile.NamedTemporaryFile() as tmp: + _LOGGER.info("Downloading %s ...", self.url) + _LOGGER.debug("Location: %s", path) + + download_from_mirrors([self.url], {}, tmp.file) + + _LOGGER.debug("Extracting archive to %s ...", path) + archive_extract_all(tmp.file, path) + extracted_marker.touch() + return path + + def __str__(self): + return self.url + + +class GitSource(Source): + def __init__(self, url: str, ref: str): + self.url = url + self.ref = ref + + def download(self, dir_suffix: str, force: bool = False) -> Path: + path, _ = git.clone_or_update( + url=self.url, + ref=self.ref, + refresh=git.NEVER_REFRESH if not force else None, + domain=DOMAIN, + submodules=[], + subpath=Path(dir_suffix), + ) + return path + + def __str__(self): + return f"{self.url}#{self.ref}" + + +class InvalidIDFComponent(Exception): + pass + + +class IDFComponent: + def __init__(self, name: str, version: str, source: Source | None): + self.name = name + self.version = version + self.source = source + self.data = {} + self.dependencies: list[IDFComponent] = [] + self._path: Path | None = None + + def __str__(self): + return f"{self.name}@{self.version}={self.source}" + + @property + def path(self) -> Path: + if self._path is None: + raise RuntimeError(f"path not set for component {self}") + return self._path + + @path.setter + def path(self, value: Path) -> None: + self._path = value + + def get_sanitized_name(self): + return re.sub(r"[^a-zA-Z0-9_.\-/]", "_", self.name) + + def get_require_name(self): + return self.get_sanitized_name().replace("/", "__") + + def download(self, force: bool = False): + """ + The dependency name should match the directory name at the end of the override path. + The ESP-IDF build system uses the directory name as the component name, so the directory of the override_path should match the component name. + If you want to specify the full name of the component with the namespace, replace / in the component name with __. + @see https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html + """ + self.path = self.source.download(self.get_sanitized_name(), force=force) + + +def _sanitize_version(version: str) -> str: + """ + Sanitize a version string by removing common requirement prefixes or a leading v. + + Args: + version: Version string to clean. + + Returns: + Cleaned version string without common requirement symbols. + """ + version = version.strip() + + prefixes = ( + "^", + "~=", + "~", + ">=", + "<=", + "==", + "!=", + ">", + "<", + "=", + "v", + "V", + ) + + for p in prefixes: + if version.startswith(p): + version = version[len(p) :] + break + + return version.strip() + + +def _get_package_from_pio_registry( + username: str | None, pkgname: str, requirements: str +) -> tuple[str, str, str | None, str | None]: + """ + Fetch package information from PlatformIO registry. + + This function queries the PlatformIO registry to find a library package + that matches the given criteria and returns its metadata including version + and download URL. + + Args: + username: The owner/username of the package (can be None) + pkgname: The name of the package + requirements: Version requirements (e.g., "^1.0.0") + + Returns: + tuple[str, str, str | None, str | None]: + A tuple containing (owner, name, version, download_url) + where version and download_url can be None if not found + """ + + from platformio.package.manager._registry import PackageManagerRegistryMixin + from platformio.package.meta import PackageSpec + + # Create a minimal PackageManagerRegistry class + class PackageManagerRegistry(PackageManagerRegistryMixin): + def __init__(self): + self._registry_client = None + self.pkg_type = "library" + + @staticmethod + def is_system_compatible(value, custom_system=None): + return True + + pio_registry = PackageManagerRegistry() + + # Fetch package metadata from registry + package = pio_registry.fetch_registry_package( + PackageSpec( + owner=username, + name=pkgname, + ) + ) + owner = package["owner"]["username"] + name = package["name"] + + # Find the best matching version based on requirements + version = pio_registry.pick_best_registry_version( + package.get("versions"), + PackageSpec(owner=username, name=pkgname, requirements=requirements), + ) + + # If no version found, return with None for version and URL + if not version: + return owner, name, None, None + + # Find the compatible package file for this version + pkgfile = pio_registry.pick_compatible_pkg_file(version["files"]) + + # If no package file found, return with None for URL but valid version + if not pkgfile: + return owner, name, version["name"], None + + return owner, name, version["name"], pkgfile["download_url"] + + +def _patch_component(component: IDFComponent, first_pass: bool): + """ + Apply patches/workarounds to specific components that have known issues. + + This function modifies component data to fix compatibility issues or missing + dependencies for certain libraries. It applies different patches based on + whether it's the first or second pass of processing. + + Args: + component: The IDFComponent object to potentially patch + first_pass: Boolean indicating if this is the first pass of processing + """ + + # Patch only on the second step + if not first_pass and CORE.using_arduino: + # Add the missing dependency to Arduino framework. Source is None so + # the IDF component manager resolves it from the registry instead of + # cloning the 2 GB arduino-esp32 git history. + component.dependencies.append( + IDFComponent( + "espressif/arduino-esp32", + str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]), + None, + ) + ) + + # + # fastled/FastLED + # + + # Patch only on the first step + if ( + first_pass + and component.name == _owner_pkgname_to_name("fastled", "FastLED") + and not (component.path / "idf_component.yml").is_file() + ): + # Force fake idf_component: This project already support ESP-IDF + (component.path / "idf_component.yml").write_text("") + + +T = TypeVar("T") + + +def _ensure_list(obj: T | list[T]) -> list[T]: + """ + Convert an object to a list if it isn't already a list. + + Args: + obj: Object that may or may not already be a list. + + Returns: + list[T]: The original list if ``obj`` is a list, otherwise a single-item + list containing ``obj``. + """ + return [obj] if not isinstance(obj, list) else obj + + +def _owner_pkgname_to_name(owner: str | None, pkgname: str) -> str: + """ + Convert owner and package name to a standardized component name. + + This function combines owner and package name with a forward slash when + both are provided, otherwise returns just the package name. + + Args: + owner: The owner/username of the package (can be None) + pkgname: The name of the package + + Returns: + str: The standardized component name in "owner/pkgname" format or just "pkgname" + """ + return f"{owner}/{pkgname}" if owner else pkgname + + +def _collect_filtered_files(src_dir: PathType, src_filters: list[str]) -> list[str]: + """ + Recursively match files in a directory according to include/exclude patterns. + + This function processes a list of filter strings that indicate which files + to include or exclude. Each filter is parsed into patterns with a sign: + '+' for inclusion and '-' for exclusion. Directory patterns ending with '/' + are normalized to include all their contents recursively. + + Args: + src_dir (PathType): Root directory to search within. + src_filters (list[str]): List of filter strings, which may contain multiple + patterns. Each pattern can start with '+' or '-' to indicate inclusion + or exclusion. + + Returns: + list[str]: List of matched file paths as strings. Only files (not directories) + are returned, even if a directory matches a pattern. + """ + matches = list( + itertools.chain.from_iterable( + FILTER_REGEX.findall(src_filter) for src_filter in src_filters + ) + ) + + selected = set() + + for sign, pattern in matches: + pattern = pattern.strip() + + if pattern.endswith("/"): + pattern = pattern.rstrip("/") + "/**" + + full_pattern = os.path.join(glob.escape(str(src_dir)), pattern) + + matched = [] + for item in glob.glob(full_pattern, recursive=True): + if not os.path.isdir(item): + matched.append(item) + else: + # PlatformIO quirk: a directory matched with "*" should include all its + # nested files and subdirectories, not just the directory itself. + for root, _, files in os.walk(item): + matched.extend([os.path.join(root, f) for f in files]) + + if sign == "+": + selected.update(matched) + elif sign == "-": + selected.difference_update(matched) + + return [r for r in selected if os.path.isfile(r)] + + +def _convert_library_to_component(library: Library) -> IDFComponent: + """ + Convert a Library object to an IDFComponent object by resolving its metadata. + + This function handles the conversion of library specifications to component + objects, resolving versions through PlatformIO registry when needed or + parsing direct repository URLs. + + Args: + library: The Library object containing name, version, and/or repository information + + Returns: + IDFComponent: The resolved component with name, version, and URL + + Raises: + ValueError: If a repository URL is missing a reference (#) + RuntimeError: If no artifact can be found for the library + """ + name = None + version = None + source = None + + # Repository is provided directly + if library.repository: + # Parse repository URL to extract name and version + split_result = urlsplit(library.repository) + if not split_result.fragment.strip(): + raise ValueError(f"Missing ref in URL {library.repository}") + + # Sanitize name + name = str(split_result.path).strip("/") + name = name.removesuffix(".git") + + # Sanitize version + version = _sanitize_version(split_result.fragment) + repository = urlunsplit(split_result._replace(fragment="")) + + source = GitSource(str(repository), split_result.fragment) + + # Version is provided - resolve using PlatformIO registry + elif library.version: + name = library.name + if "/" not in name: + owner, pkgname = None, name + else: + owner, pkgname = name.split("/", 1) + + owner, pkgname, version, url = _get_package_from_pio_registry( + owner, pkgname, library.version + ) + if url is None: + raise RuntimeError( + f"Can't find an pkg file from PlatformIO registry for library {library}" + ) + + name = _owner_pkgname_to_name(owner, pkgname) + source = URLSource(url) + + if source is None: + raise RuntimeError(f"Can't find an artifact associated to library {library}") + + assert name, "Missing library name" + assert version, "Missing library version" + + return IDFComponent(name, version, source) + + +def _detect_requires(build_src_files: list[str]) -> set[str]: + """ + Detect required components from source files. + + Args: + build_src_files: List of source file paths to analyze + + Returns: + Set of detected required components + """ + detected = set() + + # 1. Process each source file + for file in build_src_files: + path = Path(file) + + if not path.is_file(): + continue + + try: + content = path.read_text(encoding="utf-8", errors="ignore") + except Exception: # pylint: disable=broad-exception-caught + continue + + # 2. Add required component if one of these patterns matches + for require_name, patterns in REQUIRES_DETECT_PATTERNS.items(): + if require_name in detected: + continue # already found + + for pattern in patterns: + if pattern.search(content): + detected.add(require_name) + break + + return detected + + +def _split_list_by_condition( + items: list[str], match_fn: Callable[[str], str | None] +) -> tuple[list[str], list[str]]: + """ + Splits a list into two lists based on a matching function. + + Args: + items: List of items to split. + match_fn: Function that returns a value for items that should go into the "matched" list. + + Returns: + A tuple (matched, non_matched) + """ + matched = [] + non_matched = [] + for item in items: + result = match_fn(item) + if result: + matched.append(result) + else: + non_matched.append(item) + return matched, non_matched + + +def generate_cmakelists_txt(component: IDFComponent) -> str: + """ + Generate a CMakeLists.txt file for an ESP-IDF component. + + This function creates the necessary CMake configuration to build a library + with ESP-IDF, including source files, include directories, dependencies, + and build flags. + + Args: + component: The IDFComponent object containing library metadata and path + + Returns: + str: The complete CMakeLists.txt content as a string + """ + + def escape_entry(p: PathType) -> str: + # In CMakeLists.txt, backslashes need to be escaped + return f'"{str(p)}"'.replace("\\", "\\\\") + + # Extract the values + build_src_dir = component.data.get("build", {}).get("srcDir", None) + if not build_src_dir: + for d in ["src", "Src", "."]: + if (component.path / Path(d)).is_dir(): + build_src_dir = d + break + + build_include_dir = component.data.get("build", {}).get( + "includeDir", DEFAULT_BUILD_INCLUDE_DIR + ) + build_src_filter = _ensure_list( + component.data.get("build", {}).get("srcFilter", DEFAULT_BUILD_SRC_FILTER) + ) + build_flags = _ensure_list( + component.data.get("build", {}).get("flags", DEFAULT_BUILD_FLAGS) + ) + + # List all sources files + build_src_files = _collect_filtered_files( + component.path / Path(build_src_dir), build_src_filter + ) + + # Detect in the files which requirements to add + # By default in platformio, all the components are added: we need to detect them when using ESP-IDF + requires = _detect_requires(build_src_files) + + # Dependencies are required + for dependency in component.dependencies: + requires.add(dependency.get_require_name()) + + # Only keep sources + build_src_files = [os.path.relpath(p, component.path) for p in build_src_files] + build_src_files = [ + f for f in build_src_files if os.path.splitext(f)[1] in SRC_FILE_EXTENSIONS + ] + + # Handle build flags + include_dir_flags, build_flags = _split_list_by_condition( + build_flags, lambda a: a[2:].strip() if a.startswith("-I") else None + ) + link_directories, build_flags = _split_list_by_condition( + build_flags, lambda a: a[2:].strip() if a.startswith("-L") else None + ) + link_libraries, build_flags = _split_list_by_condition( + build_flags, lambda a: a[2:].strip() if a.startswith("-l") else None + ) + + # Split include directories from build_flags + # Only keep an include directory if it exists + build_include_dirs = [build_include_dir, build_src_dir] + include_dir_flags + build_include_dirs = [ + d for d in build_include_dirs if (component.path / Path(d)).is_dir() + ] + + # Split build_flags list into private and public lists + private_build_flags, public_build_flags = _split_list_by_condition( + build_flags, lambda a: a if a.startswith("-W") else None + ) + + # Generate the component + content = "idf_component_register(\n" + if build_src_files: + str_srcs = " ".join([escape_entry(p) for p in sorted(build_src_files)]) + content += f" SRCS {str_srcs}\n" + if build_include_dirs: + str_include_dirs = " ".join([escape_entry(p) for p in build_include_dirs]) + content += f" INCLUDE_DIRS {str_include_dirs}\n" + if requires: + str_requires = " ".join(sorted(requires)) + content += f" REQUIRES {str_requires}\n" + content += ")\n" + + # Add public and private build flags + if public_build_flags: + content += "target_compile_options(${COMPONENT_LIB} PUBLIC\n" + for build_flag in public_build_flags: + str_build_flag = escape_entry(build_flag) + content += f" {str_build_flag}\n" + content += ")\n" + if private_build_flags: + content += "target_compile_options(${COMPONENT_LIB} PRIVATE\n" + for build_flag in private_build_flags: + str_build_flag = escape_entry(build_flag) + content += f" {str_build_flag}\n" + content += ")\n" + + # Add library paths and files + if link_directories: + content += "target_link_directories(${COMPONENT_LIB} INTERFACE\n" + for link_directory in link_directories: + str_build_flag = escape_entry(link_directory) + content += f" {str_build_flag}\n" + content += ")\n" + + if link_libraries: + content += "target_link_libraries(${COMPONENT_LIB} INTERFACE\n" + for link_library in link_libraries: + str_build_flag = escape_entry(link_library) + content += f" {str_build_flag}\n" + content += ")\n" + + # Add custom CMake scripts + content += "\n".join( + component.data.get(ESPHOME_DATA_KEY, {}).get(ESPHOME_DATA_EXTRA_CMAKE_KEY, []) + ) + + return content + + +def generate_idf_component_yml(component: IDFComponent) -> str: + """ + Generate ESP-IDF component YAML configuration for a library. + + Args: + component: IDFComponent object to generate YAML for + + Returns: + YAML string representation of ESP-IDF component configuration + """ + data = {} + + description = component.data.get("description") + if description: + data["description"] = description + + # Do not use the version from library.json/library.properties; it may be incorrect. + data["version"] = component.version + + repository = component.data.get("repository", {}).get("url", None) + if repository: + data["repository"] = repository + + for dependency in component.dependencies: + # Initialize dependencies section if needed + if "dependencies" not in data: + data["dependencies"] = {} + + # Add this dependency to dependencies + dep = {} + dep["version"] = dependency.version + + # Should use dependency.path as override path + try: + dep["override_path"] = str(dependency.path) + except RuntimeError as e: + # No local path; let the IDF component manager resolve. + # GitSource gives an explicit URL; arduino-esp32 is resolved by + # version from the registry. Anything else is a bug. + if isinstance(dependency.source, GitSource): + dep["git"] = dependency.source.url + elif dependency.name != "espressif/arduino-esp32": + raise e + + data["dependencies"][dependency.get_sanitized_name()] = dep + + return yaml_util.dump(data) + + +def _check_library_data(data: dict): + """ + Check if a library data is compatible with the ESP-IDF framework. + + Args: + component: IDFComponent object being processed + + Raises: + ValueError: If library has unsupported platforms or frameworks + """ + platforms = data.get("platforms", "*") + if isinstance(platforms, str): + platforms = [a.strip() for a in platforms.split(",")] + platforms = _ensure_list(platforms) + + # Check if library supports ESP-IDF platform + valid_platforms = "*" in platforms or ESP32_PLATFORM in platforms + + if not valid_platforms: + raise InvalidIDFComponent(f"Unsupported library platforms: {platforms}") + + frameworks = data.get("frameworks", "*") + if isinstance(frameworks, str): + frameworks = [a.strip() for a in frameworks.split(",")] + frameworks = _ensure_list(frameworks) + + # Check if library supports ESP-IDF framework + framework = "arduino" if CORE.using_arduino else "espidf" + valid_framework = "*" in frameworks or framework in frameworks + + if not valid_framework: + raise InvalidIDFComponent(f"Unsupported library frameworks: {frameworks}") + + extra_script = data.get("build", {}).get("extraScript", None) + if extra_script: + _LOGGER.warning( + 'Extra scripts are not supported. The script "%s" will not be executed.', + extra_script, + ) + + +def _process_dependencies(component: IDFComponent): + """ + Process library dependencies and generate ESP-IDF components. + + Args: + component: IDFComponent object being processed + + Returns: + None + """ + + name, version = component.name, component.version + dependencies = component.data.get("dependencies") + if not dependencies: + return + + _LOGGER.info("Processing %s@%s component dependencies...", name, version) + for dependency in dependencies: + # Validate dependency structure + if not all(k in dependency for k in ("name", "version")): + _LOGGER.debug("Ignore invalid library: %s", dependency) + continue + + try: + _check_library_data(dependency) + except InvalidIDFComponent as e: + _LOGGER.debug( + "Skip %s@%s: %s", dependency["name"], dependency["version"], str(e) + ) + continue + + # The version field may actually contain a URL + version = dependency["version"] + url = None + try: + result = urlparse(version) + if all([result.scheme, result.netloc]): + url, version = version, None + except (TypeError, ValueError): + pass + + # Generate ESP-IDF component from PlatformIO library + component.dependencies.append( + _generate_idf_component( + Library( + _owner_pkgname_to_name( + dependency.get("owner", None), dependency.get("name") + ), + version, + url, + ) + ) + ) + + +def _parse_library_json(library_json_path: PathType): + """ + Load and parse a JSON file describing a library. + + Args: + library_json_path (PathType): Path to the JSON file. + + Returns: + dict: Parsed JSON content as a Python dictionary. + """ + with open(library_json_path, encoding="utf8") as fp: + return json.load(fp) + + +def _parse_library_properties(library_properties_path: PathType): + """ + Parse a key-value platformio .properties style file into a dictionary. + + Args: + library_properties_path (PathType): Path to the properties file. + + Returns: + dict[str, str]: Mapping of parsed property keys to values. + """ + with open(library_properties_path, encoding="utf8") as fp: + data = {} + for line in fp.read().splitlines(): + line = line.strip() + if not line or "=" not in line: + continue + # skip comments + if line.startswith("#"): + continue + key, value = line.split("=", 1) + if not value.strip(): + continue + data[key.strip()] = value.strip() + return data + + +def _generate_idf_component(library: Library, force: bool = False) -> IDFComponent: + """ + Generate an ESP-IDF component from a library specification. + + This function resolves the library, downloads it, processes metadata files, + and generates necessary ESP-IDF build files (CMakeLists.txt, idf_component.yml). + + Args: + library: The library specification containing name, version, and repository URL + force: If True, forces re-download of the library even if it exists locally + + Returns: + IDFComponent: The generated component object with resolved metadata + """ + _LOGGER.info("Generate IDF component for %s library ...", library) + + # Resolve component name, version and url + component = _convert_library_to_component(library) + name, version = component.name, component.version + + # Download the library + component.download(force) + + # Paths to component metadata and build files + library_json_path = component.path / "library.json" + library_properties_path = component.path / "library.properties" + cmakelists_txt_path = component.path / "CMakeLists.txt" + idf_component_yml_path = component.path / "idf_component.yml" + + # Apply patches to the library metadata + _patch_component(component, True) + + if cmakelists_txt_path.is_file() and idf_component_yml_path.is_file(): + # Already an ESP-IDF component + return component + + if library_json_path.is_file(): + component.data = _parse_library_json(library_json_path) + elif library_properties_path.is_file(): + component.data = _parse_library_properties(library_properties_path) + else: + raise RuntimeError( + "Invalid PIO library: missing library.json and/or library.properties" + ) + + # Apply additional patches to the library metadata + _patch_component(component, False) + + # Check if the component is usable with ESP-IDF + _check_library_data(component.data) + + # Handle the dependencies (convert PlatformIO library to ESP-IDF component if needed) + _process_dependencies(component) + + # Generate files + _LOGGER.debug("Generating CMakeLists.txt for %s@%s ...", name, version) + write_file_if_changed( + cmakelists_txt_path, + generate_cmakelists_txt(component), + ) + + _LOGGER.debug("Generating idf_component.yml for %s@%s ...", name, version) + write_file_if_changed( + idf_component_yml_path, + generate_idf_component_yml(component), + ) + + return component + + +def generate_idf_component( + library: Library, force: bool = False +) -> tuple[str, str, Path]: + """ + Generate an ESP-IDF component and return its name, version, and path. + + This is a wrapper function that calls _generate_idf_component and returns + the standardized tuple format (name, version, path). + + Args: + library: The library specification containing name, version, and repository URL + force: If True, forces re-download of the library even if it exists locally + + Returns: + tuple[str, str, Path]: A tuple containing (component_name, component_version, component_path) + """ + component = _generate_idf_component(library, force) + return component.get_sanitized_name(), component.version, component.path diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py new file mode 100644 index 00000000000..7ff373aba83 --- /dev/null +++ b/esphome/espidf/framework.py @@ -0,0 +1,1098 @@ +"""ESP-IDF framework tools for ESPHome.""" + +from collections.abc import Iterable +from contextlib import ExitStack +import io +import json +import logging +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +from typing import IO + +import requests + +from esphome.config_validation import Version +from esphome.core import CORE +from esphome.helpers import ProgressBar, get_str_env, rmtree + +PathType = str | os.PathLike + +_LOGGER = logging.getLogger(__name__) + +_SCRIPTS_DIR = Path(__file__).parent + + +def _str_to_lst_of_str(a: str) -> list[str]: + """ + Convert a string to a list of string + + Args: + a: A string containing semicolon-separated values + + Returns: + list of strings + """ + return list(f.strip() for f in a.split(";") if f.strip()) + + +ESPHOME_STAMP_FILE = ".esphome.stamp.json" + +# Cache-buster baked into the stamp file. Bump this whenever a change would +# make pre-existing stamped installs invalid, e.g.: +# - the inlined Python helpers (_get_idf_version, _get_idf_tool_paths) are +# rewritten in a way that's incompatible with prior installs +# - the stamp_info schema changes (keys added/renamed/removed) +# - the tool selection or env-construction logic changes meaning +# Bumping triggers a full reinstall on every user's next run. +STAMP_SCHEMA_VERSION = "0" + +ESPHOME_IDF_DEFAULT_TARGETS = _str_to_lst_of_str( + os.environ.get("ESPHOME_IDF_DEFAULT_TARGETS", "all") +) + +ESPHOME_IDF_DEFAULT_TOOLS = _str_to_lst_of_str( + os.environ.get("ESPHOME_IDF_DEFAULT_TOOLS", "cmake;ninja") +) + +ESPHOME_IDF_DEFAULT_TOOLS_FORCE = _str_to_lst_of_str( + os.environ.get("ESPHOME_IDF_DEFAULT_TOOLS_FORCE", "required") +) + +ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str( + os.environ.get("ESPHOME_IDF_DEFAULT_FEATURES", "core") +) + +ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str( + os.environ.get( + "ESPHOME_IDF_FRAMEWORK_MIRRORS", + "https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip", + ) +) + +ESP_IDF_CONSTRAINTS_MIRRORS = _str_to_lst_of_str( + os.environ.get( + "ESP_IDF_CONSTRAINTS_MIRRORS", + "https://dl.espressif.com/dl/esp-idf/espidf.constraints.v{VERSION}.txt", + ) +) + + +def _get_idf_tools_path() -> Path: + """ + Get the path to the ESP-IDF tools directory. + + Returns: + Path object pointing to the ESP-IDF tools directory + """ + if "ESPHOME_ESP_IDF_PREFIX" in os.environ: + return Path(get_str_env("ESPHOME_ESP_IDF_PREFIX", None)).expanduser() + return CORE.data_dir / "idf" + + +def _get_framework_path(version: str) -> Path: + """ + Get the path to the ESPHome ESP-IDF framework directory for a specific version. + + Args: + version: ESP-IDF version string + + Returns: + Path object pointing to the framework directory + """ + return _get_idf_tools_path() / "frameworks" / f"{version}" + + +def _get_python_env_path(version: str) -> Path: + """ + Get the path to the ESPHome ESP-IDF Python environment directory for a specific version. + + Args: + version: ESP-IDF version string + + Returns: + Path object pointing to the Python environment directory + """ + return _get_idf_tools_path() / "penvs" / f"{version}" + + +def rmdir(directory: PathType, msg: str | None = None): + """ + Remove a directory and its contents recursively if it exists. + + Args: + directory: Path to the directory to be removed + msg: Optional debug message to log before removal or it an error occurs + + Returns: + None + + Raises: + RuntimeError: If directory removal fails + """ + if os.path.isdir(directory): + try: + if msg: + _LOGGER.debug(msg) + rmtree(directory) + except OSError as e: + raise RuntimeError( + f"Error during {msg}: can't remove `{directory}`. Please remove it manually!" + ) from e + + +def _get_pythonexe_path() -> str: + """ + Get the path to the Python executable. + + Returns: + Path to Python executable as string + """ + # Try to get PYTHONEXEPATH environment variable + # Fallback to sys.executable if not set + return os.environ.get("PYTHONEXEPATH", os.path.normpath(sys.executable)) + + +def _get_python_env_executable_path(root: PathType, binary: str) -> Path: + """ + Get the path to a Python environment executable file. + + Args: + root: Root directory of the Python environment + binary: Name of the executable binary + + Returns: + Path object pointing to the executable file + """ + if os.name == "nt": + return Path(root) / "Scripts" / f"{binary}.exe" + return Path(root) / "bin" / binary + + +def _check_stamp(file: PathType, data: dict[str, str]) -> bool: + """ + Check if a stamp file contains the expected data. + + Args: + file: Path to the stamp file + data: Dictionary containing expected data + + Returns: + True if file exists and contains expected data, False otherwise + """ + if not Path(file).is_file(): + return False + + try: + with open(file, encoding="utf-8") as f: + return json.load(f) == data + except (json.JSONDecodeError, OSError): + return False + + +def _write_stamp(file: PathType, data: dict[str, str]): + """ + Write data to a stamp file in JSON format. + + Args: + file: Path to the stamp file to write + data: Dictionary containing data to write + """ + with open(file, "w", encoding="utf8") as fp: + json.dump(data, fp) + + +def _exec( + cmd: list[str], + msg: str | None = None, + env: dict[str, str] | None = None, + stream_output: bool = False, +) -> tuple[bool, str | None, str | None]: + """ + Execute a command and return results. + + Args: + cmd: list of command arguments + msg: Optional custom message for logging + env: Optional dictionary of environment variables to set + stream_output: If True, inherit parent stdio so the subprocess prints + directly to the terminal (useful for commands that produce their + own progress output). stdout/stderr are not captured in this mode. + + Returns: + tuple of (success: bool, stdout: str or None, stderr: str or None). + When stream_output is True, stdout and stderr are always None. + """ + cmd_str = msg or " ".join(cmd) + try: + _LOGGER.debug("%s - running ...", cmd_str) + + run_env = os.environ.copy() + if env: + run_env.update(env) + + if stream_output: + result = subprocess.run(cmd, check=False, env=run_env) + stdout = stderr = None + else: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + env=run_env, + ) + stdout = result.stdout + stderr = result.stderr + + if result.returncode != 0: + if stream_output: + _LOGGER.error("%s - failed (returncode=%s)", cmd_str, result.returncode) + else: + tail = (stderr or stdout or "").strip()[-1000:] + _LOGGER.error( + "%s - failed (returncode=%s). Tail:\n%s", + cmd_str, + result.returncode, + tail, + ) + return False, stdout, stderr + + _LOGGER.debug("%s - executed successfully", cmd_str) + return True, stdout, stderr + + except (subprocess.SubprocessError, OSError) as e: + _LOGGER.error("%s - error: %s", cmd_str, str(e)) + return False, None, None + + +def _exec_ok(*args, **kwargs) -> bool: + """ + Execute a command and return only the success status. + + Args: + *args: Positional arguments to pass to _exec function + **kwargs: Keyword arguments to pass to _exec function + + Returns: + True if command executed successfully, False otherwise + """ + return _exec(*args, **kwargs)[0] + + +def _get_idf_version( + idf_framework_root: PathType, env: dict[str, str] | None = None +) -> str: + """ + Get the ESP-IDF version from the specified framework root. + + Args: + idf_framework_root: Path to the ESP-IDF framework root directory + env: Optional dictionary of environment variables to set + + Returns: + String containing ESP-IDF version + + Raises: + RuntimeError: If ESP-IDF version cannot be determined + """ + + cmd = [ + _get_pythonexe_path(), + str(_SCRIPTS_DIR / "get_idf_version.py"), + str(idf_framework_root), + ] + + success, stdout, stderr = _exec( + cmd, + msg="ESP-IDF version", + env=(env or os.environ) + | {"PYTHONPATH": str(Path(idf_framework_root) / "tools")}, + ) + if stdout: + stdout = stdout.strip() + if not success or not stdout: + detail = (stderr or "").strip() + raise RuntimeError( + f"Can't get ESP-IDF version of {idf_framework_root}" + + (f": {detail}" if detail else "") + ) + return stdout + + +def _get_idf_tool_paths( + idf_framework_root: PathType, env: dict[str, str] | None = None +) -> tuple[list[str], dict[str, str]]: + """ + Get ESP-IDF tool paths and environment variables needed for building. + + Args: + idf_framework_root: Path to the ESP-IDF framework root directory + env: Optional dictionary of environment variables to set + + Returns: + tuple containing (list of tool paths, dictionary of environment variables) + + Raises: + RuntimeError: If ESP-IDF tool paths cannot be determined + """ + + cmd = [ + _get_pythonexe_path(), + str(_SCRIPTS_DIR / "get_idf_tool_paths.py"), + str(idf_framework_root), + ] + + success, stdout, stderr = _exec( + cmd, + msg="ESP-IDF tool paths", + env=(env or os.environ) + | {"PYTHONPATH": str(Path(idf_framework_root) / "tools")}, + ) + if not success or not stdout: + detail = (stderr or "").strip() + raise RuntimeError( + f"Can't get ESP-IDF tool paths of {idf_framework_root}" + + (f": {detail}" if detail else "") + ) + + # Extract json values + try: + data = json.loads(stdout) + return data["paths_to_export"], data["export_vars"] + except Exception as e: + raise RuntimeError( + f"Can't extract ESP-IDF tool paths of {idf_framework_root}" + ) from e + + +def _get_python_version( + python_executable: PathType, + env: dict[str, str] | None = None, + throw_exception=False, +) -> str | None: + """ + Get the Python version from the specified executable. + + Args: + python_executable: Path to the Python executable to check + env: Optional dictionary of environment variables to set + throw_exception: If True, raise RuntimeError when version can't be determined + + Returns: + String containing Python version in "major.minor.patch" format, or None if failed + """ + + script = """ +import sys +print(".".join([str(x) for x in sys.version_info])) +""" + cmd = [python_executable, "-c", script] + + success, stdout, _ = _exec(cmd, msg="Python version", env=env) + + if stdout: + stdout = stdout.strip() + if throw_exception and (not success or not stdout): + raise RuntimeError(f"Can't get Python version of {python_executable}") + return stdout + + +def _create_venv(root: PathType, msg: str | None = None): + """ + Create a Python virtual environment. + + Args: + root: Path to the virtual environment directory + msg: Optional message for logging + + Returns: + None + + Raises: + Exception: If virtual environment creation fails + """ + cmd = [_get_pythonexe_path(), "-m", "venv", "--clear", root] + if not _exec_ok(cmd, msg=f"Create Python virtual environment for {msg}"): + raise RuntimeError(f"Can't create Python virtual environment for {msg}") + + +def _detect_archive_root(names: Iterable[str]) -> str | None: + """Detect a single top-level directory shared by all archive entries. + + Returns the directory name if every non-empty entry sits under the same + top-level directory, else ``None``. Extraction helpers use this to strip + the wrapper directory commonly found in source archives during extraction + rather than renaming it afterwards — post-extraction renames are + unreliable on Windows because antivirus and the search indexer briefly + hold handles on freshly written files. + """ + root: str | None = None + has_descendant = False + for raw in names: + name = raw.replace("\\", "/").strip("/") + if not name: + continue + first, sep, _ = name.partition("/") + if root is None: + root = first + elif root != first: + return None + if sep: + has_descendant = True + return root if has_descendant else None + + +def _tar_extract_all( + data: io.BufferedIOBase, + extract_dir: PathType = ".", + progress_header: str | None = None, +): + """ + Extract a TAR archive to the specified directory. + + Implementation is inspired by Python 3.12's tarfile data filtering logic. + This can be replaced with the standard library implementation once + support for Python 3.11 is no longer required. + + Args: + data: File-like object containing the TAR archive + extract_dir: Directory to extract contents to + progress_header: If set, show a progress bar with this header + """ + import stat + import tarfile + + extract_dir = os.fspath(extract_dir) + abs_dest = os.path.abspath(extract_dir) + + with tarfile.open(fileobj=data, mode="r") as tar_ref: + all_members = tar_ref.getmembers() + + # Detect a single common top-level directory and strip it during + # extraction so we don't have to flatten it via a rename afterwards. + strip_root = _detect_archive_root(m.name for m in all_members) + strip_prefix = f"{strip_root}/" if strip_root is not None else None + + safe_members = [] + + for member in all_members: + name = member.name + + # 1. Strip leading slashes + name = name.lstrip("/" + os.sep) + + # 2. Reject absolute paths (incl. Windows drive) + if os.path.isabs(name) or ( + os.name == "nt" and ":" in name.split(os.sep)[0] + ): + continue + + # 3. Strip wrapper directory if one was detected + if strip_prefix is not None: + norm = name.replace("\\", "/") + if norm in (strip_root, strip_prefix): + continue + if not norm.startswith(strip_prefix): + continue + name = norm[len(strip_prefix) :] + + # 4. Compute final path + target_path = os.path.realpath(os.path.join(abs_dest, name)) + if os.path.commonpath([abs_dest, target_path]) != abs_dest: + continue + + # 5. Validate links properly + if member.issym() or member.islnk(): + linkname = member.linkname + + # Reject absolute link targets + if os.path.isabs(linkname): + continue + + # Strip leading slashes + linkname = os.path.normpath(linkname) + + if member.issym(): + link_target = os.path.join( + abs_dest, os.path.dirname(name), linkname + ) + else: + link_target = os.path.join(abs_dest, linkname) + link_target = os.path.realpath(link_target) + + if os.path.commonpath([abs_dest, link_target]) != abs_dest: + continue + + # write back normalized linkname + member.linkname = linkname + + # 6. Sanitize permissions + mode = member.mode + if mode is not None: + # Strip high bits & group/other write bits + mode &= ( + stat.S_IRWXU + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH + ) + if member.isfile() or member.islnk(): + # remove exec bits unless explicitly user-executable + if not (mode & stat.S_IXUSR): + mode &= ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + mode |= stat.S_IRUSR | stat.S_IWUSR + elif member.isdir() or member.issym(): + # Ignore mode for directories & symlinks + mode = None + else: + # Block special files + continue + + member.mode = mode + + # 7. Strip ownership + member.uid = None + member.gid = None + member.uname = None + member.gname = None + + # 8. Assign sanitized name back + member.name = name + + safe_members.append(member) + + total = len(safe_members) + progress = ( + ProgressBar(progress_header) if progress_header and total > 0 else None + ) + for i, member in enumerate(safe_members, 1): + tar_ref.extract(member, abs_dest) + if progress is not None: + progress.update(i / total) + if progress is not None: + progress.update(1) + + +def _zip_extract_all( + data: io.BufferedIOBase, + extract_dir: PathType = ".", + progress_header: str | None = None, +): + """ + Extract a ZIP archive to the specified directory. + + Args: + data: File-like object containing the ZIP archive + extract_dir: Directory to extract contents to + progress_header: If set, show a progress bar with this header + """ + import zipfile + + extract_dir = os.path.abspath(extract_dir) + + with zipfile.ZipFile(data, "r") as zip_ref: + all_members = zip_ref.infolist() + + # Detect a single common top-level directory and strip it during + # extraction so we don't have to flatten it via a rename afterwards. + strip_root = _detect_archive_root(m.filename for m in all_members) + strip_prefix = f"{strip_root}/" if strip_root is not None else None + + total = len(all_members) + progress = ( + ProgressBar(progress_header) if progress_header and total > 0 else None + ) + + for i, member in enumerate(all_members, 1): + # 1. Normalize name + name = member.filename.lstrip("/\\") + + # 2. Reject absolute paths / Windows drives + if os.path.isabs(name) or ( + os.name == "nt" and ":" in name.split(os.sep)[0] + ): + continue + + # 3. Strip wrapper directory if one was detected + if strip_prefix is not None: + norm = name.replace("\\", "/") + if norm in (strip_root, strip_prefix): + continue + if not norm.startswith(strip_prefix): + continue + name = norm[len(strip_prefix) :] + + # 4. Compute safe target path + target_path = os.path.abspath(os.path.join(extract_dir, name)) + + if os.path.commonpath([extract_dir, target_path]) != extract_dir: + raise ValueError(f"Unsafe path detected: {member.filename}") + + # 5. Assign sanitized name back + member.filename = name + + # 6. Extract + zip_ref.extract(member, extract_dir) + + if progress is not None: + progress.update(i / total) + if progress is not None: + progress.update(1) + + +_ARCHIVE_MAGIC_MAP = { + b"\x1f\x8b\x08": _tar_extract_all, + b"\x42\x5a\x68": _tar_extract_all, + b"\xfd\x37\x7a\x58\x5a\x00": _tar_extract_all, + b"\x50\x4b\x03\x04": _zip_extract_all, +} + + +def archive_extract_all( + archive: PathType | io.RawIOBase | IO[bytes], + extract_dir: PathType = ".", + progress_header: str | None = None, +): + """ + Extract an archive file to the specified directory. + + Args: + archive: Path to archive file or file-like object + extract_dir: Directory to extract contents to + progress_header: If set, show a progress bar with this header + + Raises: + TypeError: If archive is not a valid type + ValueError: If archive format is unsupported + """ + + # 1. Handle different archive input types + with ExitStack() as stack: + archive_ref: io.BufferedIOBase + if isinstance(archive, (str, os.PathLike)): + archive_ref = stack.enter_context(open(archive, "rb")) + elif isinstance(archive, (io.BufferedReader, io.BufferedRandom)): + archive_ref = archive + elif isinstance(archive, io.RawIOBase): + archive_ref = io.BufferedReader(archive) + else: + raise TypeError( + f"archive must be str, Path, or file-like object: {type(archive)}" + ) + + # 2. Detect archive format and select appropriate extraction function + matched_fct = None + magic_len = max(len(k) for k in _ARCHIVE_MAGIC_MAP) + header = archive_ref.peek(magic_len) + for magic, fct in _ARCHIVE_MAGIC_MAP.items(): + if header.startswith(magic): + matched_fct = fct + break + if matched_fct is None: + raise ValueError("Unsupported archive format") + matched_fct(archive_ref, extract_dir, progress_header=progress_header) + + +def download_from_mirrors( + mirrors: list[str], + substitutions: dict[str, str], + target: io.RawIOBase | IO[bytes] | PathType, + timeout: int = 30, +) -> str | None: + """ + Download file from multiple mirrors with substitution support. + + Args: + mirrors: list of mirror URLs + substitutions: Dictionary of substitutions to apply to URLs + target: Target file path or file-like object + timeout: Download timeout in seconds + + Returns: + The source URL. + + Raises: + Exception: If all download attempts fail + """ + # 1. Open target file for writing if path given + with ExitStack() as stack: + if isinstance(target, (str, os.PathLike)): + f = stack.enter_context(open(target, "wb")) + elif isinstance(target, (io.RawIOBase, io.IOBase)): + f = target + else: + raise TypeError( + f"target must be str, Path, or file-like object: {type(target)}" + ) + + # 2. Try each mirror in order + last_exception = None + + for mirror in mirrors: + # 3. Apply substitutions to URL + url = mirror.format(**substitutions) + + _LOGGER.debug("Trying downloading from %s", url) + + try: + # 4. Reset file pointer and download + f.seek(0) + f.truncate(0) + + with requests.get(url, stream=True, timeout=timeout) as r: + r.raise_for_status() + + total_size = int(r.headers.get("content-length", 0)) + downloaded = 0 + + progress = ProgressBar("Downloading") if total_size > 0 else None + + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + downloaded += len(chunk) + + if progress is not None: + progress.update(downloaded / total_size) + + if progress is not None: + progress.update(1) + + _LOGGER.debug("Downloaded successfully from: %s", url) + + # 6. Reset file pointer and return + f.seek(0) + return url + + except Exception as e: # pylint: disable=broad-exception-caught + _LOGGER.debug("Failed to download %s: %s", url, str(e)) + last_exception = e + + # 7. Raise last exception if all mirrors failed + if last_exception: + raise last_exception + return None + + +def _check_esphome_idf_framework_install( + version: str, + targets: list[str], + tools: list[str], + force: bool = False, + env: dict[str, str] | None = None, +) -> tuple[Path, bool]: + """ + Check and install ESP-IDF framework. + + Args: + version: ESP-IDF version to check/install + targets: Target platforms to install + tools: list of tools to install + force: If True, force reinstallation + env: Optional dictionary of environment variables to set + + Returns: + tuple of (framework_path, install_flag) + """ + + # Sanitize inputs + targets = sorted(set(targets)) + tools = sorted(set(tools)) + + stamp_info = {} + stamp_info["schema_version"] = STAMP_SCHEMA_VERSION + stamp_info["targets"] = targets + stamp_info["tools"] = tools + # TODO: Add stamp with this module version + + # 1. Get framework path and stamp file path + framework_path = _get_framework_path(version) + extracted_marker = framework_path / ".esphome_extracted" + env_stamp_file = framework_path / ESPHOME_STAMP_FILE + idf_tools_path = framework_path / "tools" / "idf_tools.py" + _LOGGER.info("Checking ESP-IDF %s framework ...", version) + + # 2. Download and extract the framework if not already extracted. + # The marker is written last after extraction succeeds, so its presence + # is the authoritative "extraction complete" signal — no half-extracted + # tree can pass for installed. Extracting directly into framework_path + # avoids post-extraction renames that race with antivirus on Windows. + # Tool install state is tracked separately by the stamp file in step 3, + # so we only re-extract when extraction itself is missing or incomplete. + install = force or not extracted_marker.is_file() + if install: + rmdir(framework_path, msg=f"Clean up ESP-IDF {version} framework") + + # Download in temporary file + with tempfile.NamedTemporaryFile() as tmp: + _LOGGER.info("Downloading ESP-IDF %s framework ...", version) + + # Create substitutions for the URLs + substitutions = {"VERSION": version} + try: + ver = Version.parse(version) + substitutions["MAJOR"] = str(ver.major) + substitutions["MINOR"] = str(ver.minor) + substitutions["PATCH"] = str(ver.patch) + substitutions["EXTRA"] = ver.extra + except ValueError: + pass + + download_from_mirrors( + ESPHOME_IDF_FRAMEWORK_MIRRORS, substitutions, tmp.file + ) + + _LOGGER.info("Extracting ESP-IDF %s framework ...", version) + archive_extract_all(tmp.file, framework_path, progress_header="Extracting") + extracted_marker.touch() + + # 3. Check if the framework tools are the same and correctly installed + if not install: + install = True + if _check_stamp(env_stamp_file, stamp_info): + _LOGGER.info("Checking ESP-IDF %s framework installation ...", version) + cmd = [ + _get_pythonexe_path(), + str(idf_tools_path), + "--non-interactive", + "check", + ] + if _exec_ok(cmd, msg=f"ESP-IDF {version} check", env=env): + install = False + + # 4. Install framework tools if not installed or needs update + if install: + _LOGGER.info("Installing ESP-IDF %s framework ...", version) + targets_str = ",".join(targets) + cmd = [ + _get_pythonexe_path(), + str(idf_tools_path), + "--non-interactive", + "install", + f"--targets={targets_str}", + ] + tools + if not _exec_ok( + cmd, + msg=f"ESP-IDF {version} framework installation", + env=env, + stream_output=True, + ): + raise RuntimeError(f"ESP-IDF {version} framework installation failure") + + _write_stamp(env_stamp_file, stamp_info) + + return framework_path, install + + +def _check_esp_idf_python_env_install( + version: str, + features: list[str], + force: bool = False, + env: dict[str, str] | None = None, +) -> tuple[Path, bool]: + """ + Check and install ESP-IDF Python environment. + + Args: + version: ESP-IDF version to check/install + features: Features to install + force: If True, force reinstallation + env: Environment variables to use + + Returns: + tuple of (python_env_path, install_flag) + """ + + # Sanitize inputs + features = sorted(set(features)) + + stamp_info = {} + stamp_info["schema_version"] = STAMP_SCHEMA_VERSION + stamp_info["features"] = features + + framework_path = _get_framework_path(version) + python_env_path = _get_python_env_path(version) + env_stamp_file = python_env_path / ESPHOME_STAMP_FILE + env_python_path = _get_python_env_executable_path(python_env_path, "python") + + _LOGGER.info("Checking ESP-IDF %s Python environment ...", version) + install = force or not python_env_path.is_dir() or not env_python_path.is_file() + if not install: + # Check it against the stamp file + install = True + python_version = _get_python_version(env_python_path, env=env) + if python_version: + stamp_info["python_version"] = python_version + if _check_stamp(env_stamp_file, stamp_info): + install = False + + if install: + rmdir(python_env_path, msg=f"Clean up ESP-IDF {version} Python environment") + + _create_venv(python_env_path, msg=f"ESP-IDF {version}") + + esp_idf_version = _get_idf_version(framework_path, env=env) + constraint_file_path = ( + _get_idf_tools_path() / f"espidf.constraints.v{esp_idf_version}.txt" + ) + _LOGGER.debug("ESP-IDF version %s", esp_idf_version) + + _LOGGER.info("Downloading constraints file for ESP-IDF %s ...", esp_idf_version) + download_from_mirrors( + ESP_IDF_CONSTRAINTS_MIRRORS, + {"VERSION": esp_idf_version}, + constraint_file_path, + ) + + cmd_pip_install = [ + str(env_python_path), + "-m", + "pip", + "install", + "--upgrade", + "--constraint", + constraint_file_path, + ] + + _LOGGER.info("Installing ESP-IDF %s Python dependencies ...", version) + cmd = cmd_pip_install + [ + "pip", + "setuptools", + ] + if not _exec_ok( + cmd, + msg=f"Upgrade ESP-IDF {version} Python environment packages", + env=env, + ): + raise RuntimeError( + f"Upgrade ESP-IDF {version} Python environment packages failure" + ) + + for feature in features: + requirements_file = ( + framework_path + / "tools" + / "requirements" + / f"requirements.{feature}.txt" + ) + cmd = cmd_pip_install + [ + "-r", + str(requirements_file), + ] + if not _exec_ok( + cmd, + msg=f"Install ESP-IDF {version} Python dependencies for {feature}", + env=env, + ): + raise RuntimeError( + f"Install ESP-IDF {version} Python dependencies for {feature} failure" + ) + + stamp_info["python_version"] = _get_python_version( + env_python_path, env=env, throw_exception=True + ) + _write_stamp(env_stamp_file, stamp_info) + + return python_env_path, install + + +def check_esp_idf_install( + version: str, + targets: list[str] | None = None, + tools: list[str] | None = None, + features: list[str] | None = None, + force: bool = False, +) -> tuple[Path, Path]: + """ + Check and install ESP-IDF framework and Python environment. + + Args: + version: ESP-IDF version to check/install + targets: Target platforms to install + tools: list of tools to install + features: Features to install + force: If True, force reinstallation + + Returns: + tuple of (framework_path, python_env_path) + """ + env = {} + env["IDF_TOOLS_PATH"] = str(_get_idf_tools_path()) + env["IDF_PATH"] = "" + + targets = targets or ESPHOME_IDF_DEFAULT_TARGETS + + # Determine which tools need to be installed if not provided + if tools is None: + tools = [] + for tool in set(ESPHOME_IDF_DEFAULT_TOOLS) | set( + ESPHOME_IDF_DEFAULT_TOOLS_FORCE + ): + # Check if the tool exist + if tool in ESPHOME_IDF_DEFAULT_TOOLS_FORCE or not shutil.which(tool): + tools.append(tool) + + # 1) Framework + framework_path, installed = _check_esphome_idf_framework_install( + version, targets, tools, force=force, env=env + ) + + features = features or ESPHOME_IDF_DEFAULT_FEATURES + + # 2) Python env + python_env_path, installed = _check_esp_idf_python_env_install( + version, features, force=force or installed, env=env + ) + + return framework_path, python_env_path + + +def get_framework_env( + framework_path: PathType, + python_env_path: PathType | None = None, + env: dict[str, str] | None = None, +): + """ + Get environment variables for ESP-IDF framework. + + Args: + framework_path: Path to the ESP-IDF framework + python_env_path: Optional path to Python environment + env: Optional dictionary of environment variables to set + + Returns: + Dictionary containing updated environment variables + """ + # 1. Initialize base environment with extra ESP-IDF environment variables + env = env.copy() if env else {} + env["IDF_TOOLS_PATH"] = str(_get_idf_tools_path()) + env["IDF_PATH"] = "" + + # 2. Get existing PATH from env or os.environ + if "PATH" in env: + path_list = env["PATH"].split(os.pathsep) + else: + path_list = os.environ["PATH"].split(os.pathsep) + + # 3. If Python environment path is provided, add it to PATH and set IDF_PYTHON_ENV_PATH + if python_env_path: + python_path = _get_python_env_executable_path(python_env_path, "python") + path_list.insert(0, str(python_path.parent)) + env["IDF_PYTHON_ENV_PATH"] = str(python_env_path) + + # 4. Set framework-specific environment variables + env["IDF_PATH"] = str(framework_path) + env["ESP_IDF_VERSION"] = _get_idf_version(framework_path, env) + + # 5. Get and add tool paths and environment variables + paths_to_export, export_vars = _get_idf_tool_paths(framework_path, env) + env.update(export_vars) + env["PATH"] = os.pathsep.join(paths_to_export + path_list) + + return env diff --git a/esphome/espidf/get_idf_tool_paths.py b/esphome/espidf/get_idf_tool_paths.py new file mode 100644 index 00000000000..2e8859631d0 --- /dev/null +++ b/esphome/espidf/get_idf_tool_paths.py @@ -0,0 +1,51 @@ +"""Print JSON ``{paths_to_export, export_vars}`` for ESP-IDF tools. + +Run via ``python <this file> <idf_framework_root>``. PYTHONPATH must include +``<idf_framework_root>/tools`` so ``idf_tools`` is importable. Exits with +status 1 and prints ``Missing ESP-IDF tools: ...`` on stderr if any tool is +not installed. +""" + +# pylint: disable=import-error # idf_tools is on PYTHONPATH at runtime only + +import json +import os +import sys +from types import SimpleNamespace + +from idf_tools import ( + TOOLS_FILE, + IDFEnv, + IDFTool, + filter_tools_info, + g, + load_tools_info, + process_tool, +) + +g.idf_path = sys.argv[1] +g.idf_tools_path = os.environ.get("IDF_TOOLS_PATH") +g.tools_json = os.path.join(g.idf_path, TOOLS_FILE) + +tools_info = filter_tools_info(IDFEnv.get_idf_env(), load_tools_info()) +args = SimpleNamespace(prefer_system=False) +paths_to_export: list[str] = [] +export_vars: dict[str, str] = {} +missing_tools: list[str] = [] + +for name, tool in tools_info.items(): + if tool.get_install_type() == IDFTool.INSTALL_NEVER: + continue + tool_paths, tool_vars, found = process_tool( + tool, name, args, "install_cmd", "prefer_system_hint" + ) + if not found: + missing_tools.append(name) + paths_to_export += tool_paths + export_vars |= tool_vars + +if missing_tools: + print("Missing ESP-IDF tools: " + ", ".join(missing_tools), file=sys.stderr) + raise SystemExit(1) + +print(json.dumps({"paths_to_export": paths_to_export, "export_vars": export_vars})) diff --git a/esphome/espidf/get_idf_version.py b/esphome/espidf/get_idf_version.py new file mode 100644 index 00000000000..5be51275ec3 --- /dev/null +++ b/esphome/espidf/get_idf_version.py @@ -0,0 +1,14 @@ +"""Print the ESP-IDF version of a given framework root. + +Run via ``python <this file> <idf_framework_root>``. PYTHONPATH must include +``<idf_framework_root>/tools`` so ``idf_tools`` is importable. +""" + +# pylint: disable=import-error # idf_tools is on PYTHONPATH at runtime only + +import sys + +from idf_tools import g, get_idf_version + +g.idf_path = sys.argv[1] +print(get_idf_version()) diff --git a/esphome/espidf/runner.py b/esphome/espidf/runner.py new file mode 100644 index 00000000000..e740ab72854 --- /dev/null +++ b/esphome/espidf/runner.py @@ -0,0 +1,223 @@ +r"""Subprocess entry point for running ``idf.py`` with stdio wrapping. + +Invoked as ``python runner.py <script_path> [script args...]``. + +Wraps ``sys.stdout`` and ``sys.stderr`` with a ``_FilteringTTYStream`` +shim so that: + +1. ``isatty()`` unconditionally returns True. CMake, Ninja, and idf.py's + own progress-bar code all check ``stream.isatty()`` to decide between + TTY-format output (``\\r`` cursor moves, ANSI colors, fancy progress + bars) and a plain fallback. With the wrapper in place they always + emit TTY format, even when our real stdout is a pipe to the parent + process (e.g. running under the Home Assistant dashboard add-on). + Downstream consumers — local terminals and the HA dashboard log + viewer — render the TTY control sequences correctly. + +2. ``FILTER_IDF_LINES`` is applied inside the shim's ``write()`` so + noisy idf.py output is dropped before it leaves this subprocess. + Filtering is skipped when ``-v`` / ``--verbose`` appears in argv so + verbose mode still shows everything. + +ESP-IDF runs under its own Python virtual environment which does not +have the ``esphome`` package installed, so the runner is intentionally +self-contained: no imports from ``esphome`` at all. The line-filtering +wrapper is inlined below rather than imported from +``esphome.util.RedirectText`` for that reason. +""" + +import sys + +# Regex patterns matched against each line of idf.py / CMake / Ninja +# output. Lines that match are dropped before reaching the parent +# process. Patterns are anchored at the start of the line (the shim +# uses ``re.match``). Disabled when the user passes ``-v`` / +# ``--verbose`` to ``esphome compile``. +FILTER_IDF_LINES: list[str] = [ + # idf.py's "how to flash" block at the end of a successful build. + # ESPHome handles flashing itself, so these instructions just clutter + # the output. + r"Project build complete\.", + r" idf\.py ", + r" python -m esptool ", + r"or$", + r"or from the ", + # CMake dumps the full list of IDF component paths on one giant line. + # It's purely informational and bloats the log. + r"-- Component paths:", + # CMake lists every linker script it adds (dozens of lines) and the + # complete flat list of IDF components on one giant line. Neither + # has diagnostic value for end users. + r"-- Adding linker script ", + r"-- Components:", + # IDF component manager notices: emitted on first build (no lock), + # once per stubbed dependency, plus the final "Processing N + # dependencies" enumeration. Patterns allow a leading run of dots + # because the component manager prints progress dots on the same + # line, so a NOTICE often arrives prefixed with ".NOTICE:" or + # "...........NOTICE:". + r"\.*NOTICE: ", +] + + +def main() -> int: + # ---- sys.path fix-up --------------------------------------------------- + # + # When Python runs this file as ``python runner.py``, it prepends the + # script's directory — ``<site-packages>/esphome/espidf/`` — to + # ``sys.path[0]``. That directory is part of the esphome package whose + # sibling ``types.py`` (in ``esphome/``) collides with stdlib ``types``. + # Any subsequent import that transitively touches ``types`` (``runpy``, + # ``pathlib``, ``functools``, ``typing``, ...) could resolve the wrong + # module. Drop the entry pre-emptively. ``sys`` is a built-in so + # importing it at module level earlier did not trigger the shadow. + if sys.path and sys.path[0]: + sys.path.pop(0) + # ---- end sys.path fix-up ----------------------------------------------- + + import os + import re + import runpy + + # Patch ``os.get_terminal_size`` to return a fallback size instead + # of raising ``OSError`` when the underlying fd isn't a real + # terminal. + # + # idf.py's ``fit_text_in_terminal`` (in ``idf_py_actions/tools.py``) + # unconditionally calls ``os.get_terminal_size()`` to format ninja + # progress lines. When that raises ``[Errno 25] Inappropriate + # ioctl for device`` on our pipe-backed stdout, idf.py catches the + # exception as ``EnvironmentError`` and silently exits its stdout + # reader coroutine — dropping all ninja build output from that + # point on. Returning a valid value keeps the coroutine alive so + # progress and error lines continue to flow through to the parent + # process. + # + # Honour the ``COLUMNS`` / ``LINES`` env vars if the caller set + # them explicitly. Otherwise fall back to ``(0, 0)``, which + # ``fit_text_in_terminal`` treats as "unknown width, don't + # truncate" (see the ``if not terminal_width: return out`` guard). + # Downstream log viewers (local terminals, the HA dashboard) wrap + # or scroll long lines themselves, so we'd rather emit the full + # file path than have idf.py elide its middle. + _orig_get_terminal_size = os.get_terminal_size + + def _get_terminal_size_fallback(fd: int = 1) -> os.terminal_size: + try: + return _orig_get_terminal_size(fd) + except OSError: + try: + columns = int(os.environ.get("COLUMNS", "0")) + except ValueError: + columns = 0 + try: + lines = int(os.environ.get("LINES", "0")) + except ValueError: + lines = 0 + return os.terminal_size((columns, lines)) + + os.get_terminal_size = _get_terminal_size_fallback # type: ignore[assignment] + + # Strip ANSI escape sequences before comparing a line against the filter + # patterns, so colorized lines still match plain-text patterns. + ansi_escape = re.compile(r"\033[@-_][0-?]*[ -/]*[@-~]") + + class _FilteringTTYStream: + r"""Minimal stdout/stderr wrapper. + + * ``isatty()`` unconditionally returns True, tricking downstream + code into emitting TTY-format output. + * Input is split on ``\\n`` / ``\\r`` via + ``str.splitlines(keepends=True)`` and any complete line whose + ANSI-stripped, right-stripped form matches one of + ``filter_lines`` is dropped. + * Incomplete trailing chunks are held in a buffer until a + terminator arrives. + + Mirrors the matching semantics of ``esphome.util.RedirectText`` + so filter patterns behave identically in both the PlatformIO + and IDF runner paths. + """ + + def __init__(self, stream, filter_lines: list[str] | None) -> None: + self._stream = stream + if filter_lines: + combined = r"|".join(r"(?:" + p + r")" for p in filter_lines) + self._filter_pattern: re.Pattern[str] | None = re.compile(combined) + else: + self._filter_pattern = None + self._line_buffer = "" + + def __getattr__(self, name: str): + return getattr(self._stream, name) + + def isatty(self) -> bool: + return True + + def flush(self) -> None: + self._stream.flush() + + def write(self, data) -> int: + # Text streams normally hand us ``str``; decode in case + # somebody writes bytes directly. + if not isinstance(data, str): + data = data.decode(errors="replace") + + if self._filter_pattern is None: + self._stream.write(data) + return len(data) + + self._line_buffer += data + for line in self._line_buffer.splitlines(keepends=True): + if "\n" not in line and "\r" not in line: + # Incomplete — hold until we see a terminator. + self._line_buffer = line + break + self._line_buffer = "" + + stripped = ansi_escape.sub("", line).rstrip() + if self._filter_pattern.match(stripped) is not None: + continue + self._stream.write(line) + return len(data) + + if len(sys.argv) < 2: + print( + "usage: runner.py <script_path> [args...]", + file=sys.stderr, + ) + return 2 + + script_path = sys.argv[1] + + # Mirror the platformio_runner behaviour: verbose mode disables the + # line filter so all output reaches the user. + is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[2:]) + filter_lines = None if is_verbose else FILTER_IDF_LINES or None + + sys.stdout = _FilteringTTYStream(sys.stdout, filter_lines) # type: ignore[assignment] + sys.stderr = _FilteringTTYStream(sys.stderr, filter_lines) # type: ignore[assignment] + + # Shift argv so the target script sees its own path as argv[0] and + # its own arguments starting at argv[1]. runpy.run_path does not + # modify sys.argv itself. + sys.argv = [script_path] + sys.argv[2:] + + # Emulate Python's default behaviour of prepending the script's + # directory to sys.path[0] when running ``python script.py``. + # runpy.run_path does not do this automatically, but idf.py relies + # on it to import its sibling modules (python_version_checker, + # idf_py_actions, ...). + script_dir = os.path.dirname(os.path.abspath(script_path)) + if script_dir not in sys.path: + sys.path.insert(0, script_dir) + + # If idf.py calls sys.exit(), SystemExit propagates out of run_path + # and carries the exit code back to our caller. For normal returns, + # fall through and exit with 0. + runpy.run_path(script_path, run_name="__main__") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/esphome/espidf_api.py b/esphome/espidf_api.py deleted file mode 100644 index 9ebcc48513c..00000000000 --- a/esphome/espidf_api.py +++ /dev/null @@ -1,274 +0,0 @@ -"""ESP-IDF direct build API for ESPHome.""" - -import json -import logging -import os -from pathlib import Path -import shutil -import subprocess - -from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE -from esphome.core import CORE, EsphomeError - -_LOGGER = logging.getLogger(__name__) - - -def _get_idf_path() -> Path | None: - """Get IDF_PATH from environment or common locations.""" - # Check environment variable first - if "IDF_PATH" in os.environ: - path = Path(os.environ["IDF_PATH"]) - if path.is_dir(): - return path - - # Check common installation locations - common_paths = [ - Path.home() / "esp" / "esp-idf", - Path.home() / ".espressif" / "esp-idf", - Path("/opt/esp-idf"), - ] - - for path in common_paths: - if path.is_dir() and (path / "tools" / "idf.py").is_file(): - return path - - return None - - -def _get_idf_env() -> dict[str, str]: - """Get environment variables needed for ESP-IDF build. - - Requires the user to have sourced export.sh before running esphome. - """ - env = os.environ.copy() - - idf_path = _get_idf_path() - if idf_path is None: - raise EsphomeError( - "ESP-IDF not found. Please install ESP-IDF and source export.sh:\n" - " git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n" - " cd ~/esp-idf && ./install.sh\n" - " source ~/esp-idf/export.sh\n" - "See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/" - ) - - env["IDF_PATH"] = str(idf_path) - return env - - -def run_idf_py( - *args, cwd: Path | None = None, capture_output: bool = False -) -> int | str: - """Run idf.py with the given arguments.""" - idf_path = _get_idf_path() - if idf_path is None: - raise EsphomeError("ESP-IDF not found") - - env = _get_idf_env() - idf_py = idf_path / "tools" / "idf.py" - - cmd = ["python", str(idf_py)] + list(args) - - if cwd is None: - cwd = CORE.build_path - - _LOGGER.debug("Running: %s", " ".join(cmd)) - _LOGGER.debug(" in directory: %s", cwd) - - if capture_output: - result = subprocess.run( - cmd, - cwd=cwd, - env=env, - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - _LOGGER.error("idf.py failed:\n%s", result.stderr) - return result.stdout - result = subprocess.run( - cmd, - cwd=cwd, - env=env, - check=False, - ) - return result.returncode - - -def run_reconfigure() -> int: - """Run cmake reconfigure only (no build).""" - return run_idf_py("reconfigure") - - -def has_outdated_files(): - """Check if the build configuration is stale. - - Returns True if required build files are missing or if configuration inputs - are newer than the generated CMake/Ninja build artifacts. - """ - cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") - - cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") - cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") - build_config_path = CORE.relative_build_path("build/config") - sdkconfig_internal_path = CORE.relative_build_path( - f"sdkconfig.{CORE.name}.esphomeinternal" - ) - dependency_lock_path = CORE.relative_build_path("dependencies.lock") - build_ninja_path = CORE.relative_build_path("build/build.ninja") - - if not os.path.isdir(build_config_path) or not os.listdir(build_config_path): - return True - if not os.path.isfile(cmakecache_txt_path): - return True - if not os.path.isfile(build_ninja_path): - return True - if os.path.isfile(dependency_lock_path) and os.path.getmtime( - dependency_lock_path - ) > os.path.getmtime(build_ninja_path): - return True - - cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) - return any( - os.path.getmtime(f) > cmakecache_txt_mtime - for f in [ - _get_idf_path(), - cmakelists_txt_build_path, - cmakelists_txt_src_path, - sdkconfig_internal_path, - build_config_path, - ] - if f and os.path.exists(f) - ) - - -def need_reconfigure() -> bool: - from esphome.build_gen.espidf import has_discovered_components - - # We need to reconfigure either if the files are outdated or if there is no component discovered - return has_outdated_files() or not has_discovered_components() - - -def run_compile(config, verbose: bool) -> int: - """Compile the ESP-IDF project. - - Uses two-phase configure to auto-discover available components: - 1. If no previous build, configure with minimal REQUIRES to discover components - 2. Regenerate CMakeLists.txt with discovered components - 3. Run full build - """ - from esphome.build_gen.espidf import write_project - - # Check if we need to do discovery phase - if need_reconfigure(): - _LOGGER.info("Discovering available ESP-IDF components...") - write_project(minimal=True) - rc = run_reconfigure() - if rc != 0: - _LOGGER.error("Component discovery failed") - return rc - _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") - write_project(minimal=False) - - # Build - args = [] - - if verbose: - args.append("-v") - - args.append("build") - - # Set the sdkconfig file - sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") - if sdkconfig_path.is_file(): - args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"]) - - return run_idf_py(*args) - - -def get_firmware_path() -> Path: - """Get the path to the compiled firmware binary.""" - build_dir = CORE.relative_build_path("build") - return build_dir / f"{CORE.name}.bin" - - -def get_factory_firmware_path() -> Path: - """Get the path to the factory firmware (with bootloader).""" - build_dir = CORE.relative_build_path("build") - return build_dir / f"{CORE.name}.factory.bin" - - -def create_factory_bin() -> bool: - """Create factory.bin by merging bootloader, partition table, and app.""" - build_dir = CORE.relative_build_path("build") - flasher_args_path = build_dir / "flasher_args.json" - - if not flasher_args_path.is_file(): - _LOGGER.warning("flasher_args.json not found, cannot create factory.bin") - return False - - try: - with open(flasher_args_path, encoding="utf-8") as f: - flash_data = json.load(f) - except (json.JSONDecodeError, OSError) as e: - _LOGGER.error("Failed to read flasher_args.json: %s", e) - return False - - # Get flash size from config - flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] - - # Build esptool merge command - sections = [] - for addr, fname in sorted( - flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16) - ): - file_path = build_dir / fname - if file_path.is_file(): - sections.extend([addr, str(file_path)]) - else: - _LOGGER.warning("Flash file not found: %s", file_path) - - if not sections: - _LOGGER.warning("No flash sections found") - return False - - output_path = get_factory_firmware_path() - chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32") - - cmd = [ - "python", - "-m", - "esptool", - "--chip", - chip, - "merge_bin", - "--flash_size", - flash_size, - "--output", - str(output_path), - ] + sections - - _LOGGER.info("Creating factory.bin...") - result = subprocess.run(cmd, capture_output=True, text=True, check=False) - - if result.returncode != 0: - _LOGGER.error("Failed to create factory.bin: %s", result.stderr) - return False - - _LOGGER.info("Created: %s", output_path) - return True - - -def create_ota_bin() -> bool: - """Copy the firmware to .ota.bin for ESPHome OTA compatibility.""" - firmware_path = get_firmware_path() - ota_path = firmware_path.with_suffix(".ota.bin") - - if not firmware_path.is_file(): - _LOGGER.warning("Firmware not found: %s", firmware_path) - return False - - shutil.copy(firmware_path, ota_path) - _LOGGER.info("Created: %s", ota_path) - return True diff --git a/esphome/espota2.py b/esphome/espota2.py index 576b1c6b2da..c13c3ea207f 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -441,7 +441,7 @@ def perform_ota( start_time = time.perf_counter() offset = 0 - progress = ProgressBar() + progress = ProgressBar("Uploading") while True: chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE] if not chunk: diff --git a/esphome/helpers.py b/esphome/helpers.py index 9d341af1461..62ddc489ba2 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -11,7 +11,7 @@ import shutil import stat import sys import tempfile -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TextIO from urllib.parse import urlparse from esphome.const import __version__ as ESPHOME_VERSION @@ -617,10 +617,15 @@ def sanitize(value): class ProgressBar: """A simple terminal progress bar for upload operations.""" - def __init__(self) -> None: + def __init__(self, header: str, stream: TextIO | None = None) -> None: + self.header = header + self.stream = stream or sys.stderr self.last_progress: int | None = None + self.enabled = hasattr(self.stream, "isatty") and self.stream.isatty() def update(self, progress: float) -> None: + if not self.enabled: + return bar_length = 60 status = "" if progress >= 1: @@ -631,11 +636,13 @@ class ProgressBar: return self.last_progress = new_progress block = int(round(bar_length * progress)) - text = f"\rUploading: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" + text = f"\r{self.header}: [{'=' * block + ' ' * (bar_length - block)}] {new_progress}% {status}" sys.stderr.write(text) sys.stderr.flush() def done(self) -> None: + if not self.enabled: + return sys.stderr.write("\n") sys.stderr.flush() diff --git a/esphome/web_server_ota.py b/esphome/web_server_ota.py index a49f46b270d..7c31c1b1232 100644 --- a/esphome/web_server_ota.py +++ b/esphome/web_server_ota.py @@ -60,7 +60,7 @@ class _MultipartStreamer: self._idx = 0 self._total = len(prefix) + file_size + len(suffix) self._sent = 0 - self.progress = ProgressBar() + self.progress = ProgressBar("Uploading") def __len__(self) -> int: return self._total diff --git a/script/ci-custom.py b/script/ci-custom.py index 8cd8fd75444..25db32105c6 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -562,7 +562,7 @@ def lint_constants_usage(): # Maximum allowed CONF_ constants in esphome/const.py. # This file is frozen — new constants go in esphome/components/const/__init__.py. # Decrease this number when constants are moved out of const.py. -CONST_PY_MAX_CONF = 1011 +CONST_PY_MAX_CONF = 1012 @lint_content_check(include=["esphome/const.py"]) diff --git a/script/test_build_components.py b/script/test_build_components.py index 82d05f78b23..10c5e5463fe 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -175,7 +175,7 @@ def group_components_by_platform( } -def format_github_summary(test_results: list[TestResult]) -> str: +def format_github_summary(test_results: list[TestResult], toolchain=None) -> str: """Format test results as GitHub Actions job summary markdown. Args: @@ -225,11 +225,12 @@ def format_github_summary(test_results: list[TestResult]) -> str: lines.append("```bash\n") # Generate one command per platform and test type + extra_arguments = f" --toolchain {toolchain}" if toolchain else "" platform_components = group_components_by_platform(failed_results) for platform, test_type in sorted(platform_components.keys()): components_csv = ",".join(platform_components[(platform, test_type)]) lines.append( - f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}\n" + f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}{extra_arguments}\n" ) lines.append("```\n") @@ -274,13 +275,15 @@ def format_github_summary(test_results: list[TestResult]) -> str: return "".join(lines) -def write_github_summary(test_results: list[TestResult]) -> None: +def write_github_summary( + test_results: list[TestResult], toolchain: str | None = None +) -> None: """Write GitHub Actions job summary with test results and timing. Args: test_results: List of all test results """ - summary_content = format_github_summary(test_results) + summary_content = format_github_summary(test_results, toolchain) with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f: f.write(summary_content) @@ -308,6 +311,7 @@ def run_esphome_test( esphome_command: str, continue_on_fail: bool, use_testing_mode: bool = False, + toolchain: str | None = None, ) -> TestResult: """Run esphome test for a single component. @@ -367,8 +371,14 @@ def run_esphome_test( ] ) - # Add command and config file - cmd.extend([esphome_command, str(output_file)]) + if toolchain: + cmd.extend(["--toolchain", toolchain]) + + # Add command + cmd.append(esphome_command) + + # Add config file + cmd.append(str(output_file)) # Build command string for display/logging cmd_str = " ".join(cmd) @@ -432,6 +442,7 @@ def run_grouped_test( tests_dir: Path, esphome_command: str, continue_on_fail: bool, + toolchain: str | None = None, ) -> TestResult: """Run esphome test for a group of components with shared bus configs. @@ -510,10 +521,16 @@ def run_grouped_test( "-s", "target_platform", platform, - esphome_command, - str(output_file), ] + if toolchain: + cmd.extend(["--toolchain", toolchain]) + + # Add command + cmd.append(esphome_command) + + cmd.append(str(output_file)) + # Build command string for display/logging cmd_str = " ".join(cmd) @@ -576,6 +593,7 @@ def run_grouped_component_tests( esphome_command: str, continue_on_fail: bool, additional_isolated: set[str] | None = None, + toolchain: str | None = None, ) -> tuple[set[tuple[str, str]], list[TestResult]]: """Run grouped component tests. @@ -879,6 +897,7 @@ def run_grouped_component_tests( tests_dir=tests_dir, esphome_command=esphome_command, continue_on_fail=continue_on_fail, + toolchain=toolchain, ) # Mark all components as tested @@ -902,6 +921,7 @@ def run_individual_component_test( continue_on_fail: bool, tested_components: set[tuple[str, str]], test_results: list[TestResult], + toolchain: str | None = None, ) -> None: """Run an individual component test if not already tested in a group. @@ -930,6 +950,7 @@ def run_individual_component_test( build_dir=build_dir, esphome_command=esphome_command, continue_on_fail=continue_on_fail, + toolchain=toolchain, ) test_results.append(test_result) @@ -942,6 +963,7 @@ def test_components( enable_grouping: bool = True, isolated_components: set[str] | None = None, base_only: bool = False, + toolchain: str | None = None, ) -> int: """Test components with optional intelligent grouping. @@ -1018,6 +1040,7 @@ def test_components( esphome_command=esphome_command, continue_on_fail=continue_on_fail, additional_isolated=isolated_components, + toolchain=toolchain, ) test_results.extend(grouped_results) @@ -1046,6 +1069,7 @@ def test_components( continue_on_fail=continue_on_fail, tested_components=tested_components, test_results=test_results, + toolchain=toolchain, ) else: # Platform-specific test @@ -1078,6 +1102,7 @@ def test_components( continue_on_fail=continue_on_fail, tested_components=tested_components, test_results=test_results, + toolchain=toolchain, ) # Separate results into passed and failed @@ -1098,17 +1123,18 @@ def test_components( print("\n" + "=" * 80) print("Commands to reproduce failures (copy-paste to reproduce locally):") print("=" * 80) + extra_arguments = f" --toolchain {toolchain}" if toolchain else "" platform_components = group_components_by_platform(failed_results) for platform, test_type in sorted(platform_components.keys()): components_csv = ",".join(platform_components[(platform, test_type)]) print( - f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}" + f"script/test_build_components.py -c {components_csv} -t {platform} -e {test_type}{extra_arguments}" ) print() # Write GitHub Actions job summary if in CI if os.environ.get("GITHUB_STEP_SUMMARY"): - write_github_summary(test_results) + write_github_summary(test_results, toolchain=toolchain) if failed_results: return 1 @@ -1161,6 +1187,10 @@ def main() -> int: action="store_true", help="Only test base test files (test.*.yaml), not variant files (test-*.yaml)", ) + parser.add_argument( + "--toolchain", + help="Select toolchain for compiling.", + ) args = parser.parse_args() @@ -1180,6 +1210,7 @@ def main() -> int: enable_grouping=not args.no_grouping, isolated_components=isolated_components, base_only=args.base_only, + toolchain=args.toolchain, ) diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 203f4841072..f0f96e9adcc 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -16,8 +16,8 @@ from esphome.const import ( CONF_ESPHOME, CONF_IGNORE_PIN_VALIDATION_ERROR, CONF_NUMBER, - KEY_NATIVE_IDF, PlatformFramework, + Toolchain, ) from esphome.core import CORE from tests.component_tests.types import SetCoreConfigCallable @@ -266,7 +266,7 @@ def test_native_idf_enables_reproducible_build( CORE.config_path = component_config_path("reproducible_build.yaml") CORE.config = read_config({}) - CORE.data[KEY_NATIVE_IDF] = True + CORE.toolchain = Toolchain.ESP_IDF generate_cpp_contents(CORE.config) sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 9dc37918ae9..1a52e6b29ed 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -855,7 +855,7 @@ class TestEsphomeCore: def test_bootloader_bin__native_idf(self, target): """Native ESP-IDF builds emit the bootloader under build/bootloader/bootloader.bin.""" - target.data[const.KEY_NATIVE_IDF] = True + target.toolchain = const.Toolchain.ESP_IDF assert target.bootloader_bin == Path( "foo/build/build/bootloader/bootloader.bin" @@ -864,7 +864,7 @@ class TestEsphomeCore: def test_bootloader_bin__platformio(self, target): """For PlatformIO builds bootloader.bin lives in the env-specific .pioenvs directory.""" target.name = "test-device" - target.data[const.KEY_NATIVE_IDF] = False + target.toolchain = const.Toolchain.PLATFORMIO assert target.bootloader_bin == Path( "foo/build/.pioenvs/test-device/bootloader.bin" diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py new file mode 100644 index 00000000000..caef10eea32 --- /dev/null +++ b/tests/unit_tests/test_espidf_component.py @@ -0,0 +1,357 @@ +import json +import os +from unittest.mock import MagicMock + +import pytest + +from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + Framework, + Platform, +) +from esphome.core import CORE, Library +import esphome.espidf.component +from esphome.espidf.component import ( + GitSource, + IDFComponent, + InvalidIDFComponent, + URLSource, + _check_library_data, + _collect_filtered_files, + _convert_library_to_component, + _detect_requires, + _parse_library_json, + _parse_library_properties, + _process_dependencies, + _split_list_by_condition, + generate_cmakelists_txt, + generate_idf_component_yml, +) + + +@pytest.fixture(name="tmp_component") +def fixture_tmp_component(tmp_path): + c = IDFComponent("owner/name", "1.0.0", source=MagicMock()) + c.path = tmp_path + return c + + +@pytest.fixture(name="esp32_idf_core") +def fixture_esp32_idf_core(): + CORE.data[KEY_CORE] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = str(Platform.ESP32) + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = str(Framework.ESP_IDF) + + +def test_idf_component_str(): + c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com")) + assert str(c) == "foo/bar@1.0=http://dummy.com" + + +def test_idf_component_sanitized_name(): + c = IDFComponent("foo/bar bar-bar", "1.0", source=URLSource("http://dummy.com")) + assert c.get_sanitized_name() == "foo/bar_bar-bar" + + +def test_idf_component_require_name(): + c = IDFComponent("foo/bar", "1.0", source=URLSource("http://dummy.com")) + assert c.get_require_name() == "foo__bar" + + +def test_collect_filtered_files_basic(tmp_path): + f1 = tmp_path / "a.c" + f2 = tmp_path / "b" / "b.cpp" + f1.write_text("int a;") + f2.parent.mkdir(parents=True) + f2.write_text("int b;") + + result = _collect_filtered_files(tmp_path, ["+<*>"]) + assert str(f1) in result + assert str(f2) in result + + +def test_collect_filtered_files_exclude(tmp_path): + f1 = tmp_path / "a.c" + f2 = tmp_path / "b.cpp" + f1.write_text("int a;") + f2.write_text("int b;") + + result = _collect_filtered_files(tmp_path, ["+<*> -<*.cpp>"]) + assert str(f1) in result + assert str(f2) not in result + + +def test_detect_requires(tmp_path): + f = tmp_path / "main.c" + f.write_text('#include "mbedtls/foo.h"') + + result = _detect_requires([str(f)]) + assert "mbedtls" in result + + +def test_detect_requires_ignores_invalid_file(tmp_path): + result = _detect_requires([str(tmp_path / "missing.c")]) + assert result == set() + + +def test_split_list_by_condition(): + items = ["-Iinclude", "-Llib", "-Wall"] + + matched, rest = _split_list_by_condition( + items, lambda x: x[2:] if x.startswith("-I") else None + ) + + assert matched == ["include"] + assert "-Llib" in rest + assert "-Wall" in rest + + +def test_generate_cmakelists_txt_basic(tmp_component): + src_dir = tmp_component.path / "src" + src_dir.mkdir() + f = src_dir / "main.c" + f.write_text("int main() {}") + + tmp_component.data = {} + + content = generate_cmakelists_txt(tmp_component) + + assert "idf_component_register" in content + assert "main.c" in content + + +def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path): + src_dir = tmp_component.path / "src" + src_dir.mkdir() + (src_dir / "main.c").write_text("int main() {}") + + dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com")) + dep.path = tmp_path / "dep" + tmp_component.dependencies = [dep] + + tmp_component.data = { + "build": {"flags": ["-Iinclude", "-Llib", "-lmylib", "-Wall", "-DTEST"]} + } + + content = generate_cmakelists_txt(tmp_component) + sep = "\\\\" if os.name == "nt" else "/" + assert ( + content + == f"""idf_component_register( + SRCS "src{sep}main.c" + INCLUDE_DIRS "src" + REQUIRES dep +) +target_compile_options(${{COMPONENT_LIB}} PUBLIC + "-DTEST" +) +target_compile_options(${{COMPONENT_LIB}} PRIVATE + "-Wall" +) +target_link_directories(${{COMPONENT_LIB}} INTERFACE + "lib" +) +target_link_libraries(${{COMPONENT_LIB}} INTERFACE + "mylib" +) +""" + ) + + +def test_generate_idf_component_yml_basic(tmp_component): + tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}} + result = generate_idf_component_yml(tmp_component) + + assert result == "description: test\nversion: 1.0.0\nrepository: http://aaa\n" + + +def test_generate_idf_component_yml_with_dependencies(tmp_component, tmp_path): + dep = IDFComponent("dep", "1.0", source=URLSource("http://dummy.com")) + dep.path = tmp_path / "dep" + + tmp_component.dependencies = [dep] + tmp_component.data = {} + + result = generate_idf_component_yml(tmp_component) + + assert ( + result + == f"""version: 1.0.0 +dependencies: + dep: + version: '1.0' + override_path: {dep.path} +""" + ) + + +def test_generate_idf_component_yml_arduino_registry_dep(tmp_component): + # Synthetic arduino-esp32 dep with no source / no path: should emit a + # version-only entry so the IDF component manager resolves it from the + # registry instead of via git. + dep = IDFComponent("espressif/arduino-esp32", "3.3.8", source=None) + + tmp_component.dependencies = [dep] + tmp_component.data = {} + + result = generate_idf_component_yml(tmp_component) + + assert ( + result + == """version: 1.0.0 +dependencies: + espressif/arduino-esp32: + version: 3.3.8 +""" + ) + + +def test_generate_idf_component_yml_missing_path_reraises(tmp_component): + # A dep without a path and without a recognised source should re-raise + # the underlying RuntimeError instead of silently producing a bad manifest. + dep = IDFComponent("foo/bar", "1.0", source=None) + + tmp_component.dependencies = [dep] + tmp_component.data = {} + + with pytest.raises(RuntimeError): + generate_idf_component_yml(tmp_component) + + +def test_check_library_data_valid(esp32_idf_core): + _check_library_data({"platforms": "*", "frameworks": "*"}) + + +def test_check_library_data_valid2(esp32_idf_core): + _check_library_data({"platforms": "*"}) + + +def test_check_library_data_valid3(esp32_idf_core): + _check_library_data({}) + + +def test_check_library_data_valid4(esp32_idf_core): + _check_library_data({"platforms": "espressif32", "frameworks": "*"}) + + +def test_check_library_data_valid5(esp32_idf_core): + _check_library_data({"platforms": "*", "frameworks": "espidf"}) + + +def test_check_library_data_invalid_platform(esp32_idf_core): + with pytest.raises(InvalidIDFComponent): + _check_library_data({"platforms": ["other"], "frameworks": "*"}) + + +def test_check_library_data_invalid_framework(esp32_idf_core): + with pytest.raises(InvalidIDFComponent): + _check_library_data({"platforms": "*", "frameworks": ["other"]}) + + +def test_extra_script_logs_warning(caplog, esp32_idf_core): + extra_script = "myscript.sh" + + with caplog.at_level("WARNING"): + _check_library_data({"build": {"extraScript": extra_script}}) + + assert "not supported" in caplog.text + assert "myscript.sh" in caplog.text + + +def test_parse_library_json(tmp_path): + f = tmp_path / "library.json" + f.write_text(json.dumps({"name": "test"})) + + result = _parse_library_json(f) + assert result["name"] == "test" + + +def test_parse_library_properties(tmp_path): + f = tmp_path / "library.properties" + f.write_text( + """ +name=Test +version=1.0 +# description=ABCD +empty= +""" + ) + + result = _parse_library_properties(f) + + assert result["name"] == "Test" + assert result["version"] == "1.0" + assert "empty" not in result + + +def test_convert_library_with_repository(): + lib = Library("name", None, "https://github.com/foo/bar.git#v1.2.3") + + result = _convert_library_to_component(lib) + + assert result.name == "foo/bar" + assert result.version == "1.2.3" + assert isinstance(result.source, GitSource) + + +def test_convert_library_missing_ref(): + lib = Library("name", None, "https://github.com/foo/bar.git") + + with pytest.raises(ValueError): + _convert_library_to_component(lib) + + +def test_convert_library_registry(monkeypatch): + lib = Library("foo/bar", "^1.0.0", None) + + monkeypatch.setattr( + esphome.espidf.component, + "_get_package_from_pio_registry", + lambda o, n, r: ("foo", "bar", "1.2.3", "http://example.com/pkg.zip"), + ) + + result = _convert_library_to_component(lib) + + assert result.name == "foo/bar" + assert result.version == "1.2.3" + assert isinstance(result.source, URLSource) + + +def test_process_dependencies_adds_valid_dependency(tmp_component, monkeypatch): + tmp_component.data = { + "dependencies": [ + { + "name": "foo", + "version": "1.0", + } + ] + } + + monkeypatch.setattr( + esphome.espidf.component, + "_generate_idf_component", + lambda lib: esphome.espidf.component.IDFComponent( + lib.name, lib.version, source=URLSource("http://dummy.com") + ), + ) + + monkeypatch.setattr(esphome.espidf.component, "_check_library_data", lambda x: None) + + _process_dependencies(tmp_component) + + assert len(tmp_component.dependencies) == 1 + + +def test_process_dependencies_skips_invalid(tmp_component): + tmp_component.data = { + "dependencies": [ + {"name": "foo", "version": "1.0", "platforms": ["arduino"]}, + {"invalid": "entry"}, + ] + } + + _process_dependencies(tmp_component) + + assert tmp_component.dependencies == [] diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index b22ad461132..d7fcedfd66d 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -604,7 +604,8 @@ def test_run_ota_wrapper(mock_run_ota_impl: Mock) -> None: def test_progress_bar(capsys: CaptureFixture[str]) -> None: """Test ProgressBar functionality.""" - progress = espota2.ProgressBar() + progress = espota2.ProgressBar("Uploading") + progress.enabled = True # Fake TTY # Test initial update progress.update(0.0) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 2823310f0e4..3eb50de76b4 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -88,6 +88,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + Toolchain, ) from esphome.core import CORE, EsphomeError from esphome.espota2 import ( @@ -148,6 +149,7 @@ def setup_core( config[CONF_WIFI] = {CONF_USE_ADDRESS: address} CORE.config = config + CORE.toolchain = Toolchain.PLATFORMIO if platform is not None: CORE.data[KEY_CORE] = {} From c4e85fbfc1b17a78514ba92851321fa85beb6b11 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 May 2026 16:57:10 +1200 Subject: [PATCH 1915/2056] [ci] sync-device-classes: mint least-privilege App token (#16350) --- .github/workflows/sync-device-classes.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index be1457387d9..c6c829fbb4e 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -6,12 +6,27 @@ on: schedule: - cron: "45 6 * * *" +# Repo writes (branch push, PR open) happen via the App token minted below, +# so the workflow's GITHUB_TOKEN does not need any write scopes. +permissions: + contents: read # actions/checkout for this repo and home-assistant/core + jobs: sync: name: Sync Device Classes runs-on: ubuntu-latest if: github.repository == 'esphome/esphome' steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # Scope the minted App token to the minimum needed by peter-evans/create-pull-request. + permission-contents: write # git.createCommit + refs.create/update to push the sync/device-classes branch + permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -50,4 +65,4 @@ jobs: delete-branch: true title: "Synchronise Device Classes from Home Assistant" body-path: .github/PULL_REQUEST_TEMPLATE.md - token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} From 5dadfe636771f06c4113abe13dc9f3b491231ece Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 May 2026 17:04:09 +1200 Subject: [PATCH 1916/2056] [ci] codeowner-review-request: mint least-privilege App token (#16351) --- .github/workflows/codeowner-review-request.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 76be6ecd7bf..cd6c1d34c65 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -17,9 +17,10 @@ on: - release - beta +# PR/review writes (requestReviewers, issues.createComment) are performed with the App token minted below, +# so the workflow's GITHUB_TOKEN only needs read access for checkout. permissions: - pull-requests: write - contents: read + contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper jobs: request-codeowner-reviews: @@ -32,9 +33,20 @@ jobs: with: ref: ${{ github.event.pull_request.base.sha }} + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # Scope the minted App token to the minimum needed by the github-script step below. + permission-pull-requests: write # pulls.listFiles, pulls.get, pulls.listReviews, pulls.requestReviewers + permission-issues: write # issues.listComments and issues.createComment (PR comments use the issues API) + - name: Request reviews from component codeowners uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: + github-token: ${{ steps.generate-token.outputs.token }} script: | const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); From fe66f9ba41927c05e211056f818d381c62959eda Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 May 2026 17:15:53 +1200 Subject: [PATCH 1917/2056] [ci] Tighten workflow permissions to least-privilege (#16349) --- .github/workflows/auto-label-pr.yml | 9 +++++++-- .github/workflows/ci-api-proto.yml | 4 ++-- .github/workflows/ci-clang-tidy-hash.yml | 4 ++-- .github/workflows/ci-docker.yml | 3 +-- .github/workflows/ci-memory-impact-comment.yml | 6 +++--- .github/workflows/ci.yml | 6 +++--- .../close-pr-from-fork-default-branch.yml | 4 ++-- .../codeowner-approved-label-update.yml | 6 +++--- .github/workflows/codeql.yml | 16 +++++++--------- .github/workflows/external-component-bot.yml | 5 ++--- .github/workflows/issue-codeowner-notify.yml | 4 ++-- .github/workflows/lock.yml | 6 ++++++ .github/workflows/pr-title-check.yml | 4 ++-- .github/workflows/release.yml | 17 ++++++++++------- .github/workflows/stale.yml | 4 ++-- .github/workflows/status-check-labels.yml | 3 +++ 16 files changed, 57 insertions(+), 44 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index ea22f75ef04..2d000658a2d 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -6,9 +6,10 @@ on: pull_request_target: types: [labeled, opened, reopened, synchronize, edited] +# All PR/label/review writes are performed with the App token minted below, +# so the workflow's GITHUB_TOKEN only needs read access for checkout. permissions: - pull-requests: write - contents: read + contents: read # actions/checkout reads the workflow source env: SMALL_PR_THRESHOLD: 30 @@ -31,6 +32,10 @@ jobs: with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # Scope the minted App token to the minimum needed by auto-label-pr/*.js. + permission-contents: read # repos.getContent for CODEOWNERS and file lookups in detectors.js + permission-issues: write # listLabelsOnIssue, addLabels, removeLabel, list/createComment + permission-pull-requests: write # pulls.listFiles, list/create/update/dismissReview - name: Auto Label PR uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index e5143911d94..2f7fd271ba2 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -12,8 +12,8 @@ on: - ".github/workflows/ci-api-proto.yml" permissions: - contents: read - pull-requests: write + contents: read # actions/checkout for the PR head + pull-requests: write # pulls.createReview / listReviews / dismissReview when generated proto files are stale jobs: check: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 40cdff0cba7..d9148fb06dc 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -12,8 +12,8 @@ on: - ".github/workflows/ci-clang-tidy-hash.yml" permissions: - contents: read - pull-requests: write + contents: read # actions/checkout for the PR head + pull-requests: write # pulls.createReview / listReviews / dismissReview when the clang-tidy hash is out of date jobs: verify-hash: diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 4009ac1e174..3fd17888c7c 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -22,8 +22,7 @@ on: - "script/platformio_install_deps.py" permissions: - contents: read - packages: read + contents: read # actions/checkout only; the build does not push images concurrency: # yamllint disable-line rule:line-length diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index fbcf5ea5841..025b9609859 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -7,9 +7,9 @@ on: types: [completed] permissions: - contents: read - pull-requests: write - actions: read + contents: read # actions/checkout of the base repo at the PR's target branch + pull-requests: write # gh api to look up the PR by head SHA and post/update the memory-impact comment + actions: read # gh run download for the memory-analysis artifacts produced by the CI workflow run jobs: memory-impact-comment: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9909d7a5ddf..7cb8e07afa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ on: merge_group: permissions: - contents: read + contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write env: DEFAULT_PYTHON: "3.11" @@ -1147,8 +1147,8 @@ jobs: - memory-impact-pr-branch if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true' permissions: - contents: read - pull-requests: write + contents: read # actions/checkout to load the comment-posting script + pull-requests: write # ci_memory_impact_comment.py posts/updates the memory-impact comment on the PR env: GH_TOKEN: ${{ github.token }} steps: diff --git a/.github/workflows/close-pr-from-fork-default-branch.yml b/.github/workflows/close-pr-from-fork-default-branch.yml index 1cd70f5efcd..5180a071806 100644 --- a/.github/workflows/close-pr-from-fork-default-branch.yml +++ b/.github/workflows/close-pr-from-fork-default-branch.yml @@ -6,8 +6,8 @@ on: types: [opened, reopened] permissions: - pull-requests: write - issues: write + pull-requests: write # pulls.update to close the PR opened from a fork's default branch + issues: write # issues.createComment to explain to the contributor why the PR was closed jobs: close: diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml index 49653b6fb3b..013517bde6d 100644 --- a/.github/workflows/codeowner-approved-label-update.yml +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -15,9 +15,9 @@ on: - beta permissions: - issues: write - pull-requests: read - contents: read + issues: write # issues.addLabels / removeLabel to manage the 'code-owner-approved' label on the PR + pull-requests: read # listReviews to determine whether a codeowner has approved + contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper jobs: codeowner-approved: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 949e45e45c9..0a4dd9a92dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,6 +16,9 @@ on: schedule: - cron: "30 18 * * 4" +# Deny by default; the analyze job opts in to exactly what it needs. +permissions: {} + jobs: analyze: name: Analyze (${{ matrix.language }}) @@ -26,15 +29,10 @@ jobs: # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read + security-events: write # upload CodeQL SARIF results to the Code Scanning API + packages: read # fetch internal or private CodeQL query packs + actions: read # required by codeql-action when run from a private repo + contents: read # actions/checkout to scan the repository strategy: fail-fast: false diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 3165b170780..6e2bf780b8a 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -5,9 +5,8 @@ on: types: [opened, synchronize] permissions: - contents: read # Needed to fetch PR details - issues: write # Needed to create and update comments (PR comments are managed via the issues REST API) - pull-requests: write # also needed? + issues: write # issues.createComment / updateComment to post the external-component usage instructions on the PR + pull-requests: read # pulls.listFiles to enumerate which components changed jobs: external-comment: diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index b211c13985d..bc892b64e0b 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -9,8 +9,8 @@ on: types: [labeled] permissions: - issues: write - contents: read + issues: write # issues.createComment to mention component codeowners on the newly labelled issue + contents: read # repos.getContent to fetch CODEOWNERS from the default branch jobs: notify-codeowners: diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 8d1dfe857de..5e701176524 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -6,6 +6,12 @@ on: - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: +# Deny by default; the lock job opts in to exactly what the reusable workflow needs. +permissions: {} + jobs: lock: + permissions: + issues: write # issues.lock on closed issues + pull-requests: write # issues.lock on closed pull requests uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 87009962719..ed0bff9664f 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -8,8 +8,8 @@ on: - beta permissions: - contents: read - pull-requests: read + contents: read # actions/checkout to load detect-tags.js + pull-requests: read # pulls.listFiles to map changed files to component/core/dashboard/ci tags jobs: check: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a16af92b6fb..d07c8fe6333 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: - cron: "0 2 * * *" permissions: - contents: read + contents: read # actions/checkout for all jobs; deploy jobs add their own scopes when they need to write jobs: init: @@ -57,8 +57,8 @@ jobs: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest permissions: - contents: read - id-token: write + contents: read # actions/checkout to build the sdist/wheel + id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python @@ -78,8 +78,8 @@ jobs: name: Build ESPHome ${{ matrix.platform.arch }} if: github.repository == 'esphome/esphome' permissions: - contents: read - packages: write + contents: read # actions/checkout to load Dockerfile and build context + packages: write # docker/login-action + build-push-action push image digests to ghcr.io runs-on: ${{ matrix.platform.os }} needs: [init] strategy: @@ -152,8 +152,8 @@ jobs: - deploy-docker if: github.repository == 'esphome/esphome' permissions: - contents: read - packages: write + contents: read # actions/checkout to load Dockerfile and build context + packages: write # docker/login-action + build-push-action push image digests to ghcr.io strategy: fail-fast: false matrix: @@ -227,6 +227,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: home-assistant-addon + permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token) - name: Trigger Workflow uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -262,6 +263,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: esphome-schema + permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token) - name: Trigger Workflow uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -293,6 +295,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} owner: esphome repositories: version-notifier + permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token) - name: Trigger Workflow uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ba5c32e0167..2e57093bbb9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,8 +7,8 @@ on: workflow_dispatch: permissions: - issues: write - pull-requests: write + issues: write # actions/stale labels, comments on, and closes stale issues + pull-requests: write # actions/stale labels, comments on, and closes stale pull requests concurrency: group: lock diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index 709342e5ae6..d27cc0cbec2 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -4,6 +4,9 @@ on: pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] +permissions: + pull-requests: read # issues.listLabelsOnIssue to detect blocking labels (needs-docs, merge-after-release, chained-pr) + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true From c82adc3892f453b09c2a7f15fd37dcc6841f83f0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 May 2026 19:52:39 +1200 Subject: [PATCH 1918/2056] [ci] Fix external-component-bot 403 on PR comments (#16354) --- .github/workflows/external-component-bot.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 6e2bf780b8a..2e96bec1dea 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -4,19 +4,29 @@ on: pull_request_target: types: [opened, synchronize] -permissions: - issues: write # issues.createComment / updateComment to post the external-component usage instructions on the PR - pull-requests: read # pulls.listFiles to enumerate which components changed +# All API calls (pulls.listFiles + issues.{list,create,update}Comment) are performed with +# the App token minted below, so the workflow's GITHUB_TOKEN does not need any scopes. +permissions: {} jobs: external-comment: name: External component comment runs-on: ubuntu-latest steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # pulls.listFiles + issues.{list,create,update}Comment on PRs. For PR resources + # the issues.*Comment APIs require the pull-requests scope, not issues. + permission-pull-requests: write + - name: Add external component comment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.generate-token.outputs.token }} script: | // Generate external component usage instructions function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) { From 267836d09886c66797062bf45a7be6e86ea93f94 Mon Sep 17 00:00:00 2001 From: Samuel Sieb <samuel-github@sieb.net> Date: Mon, 11 May 2026 01:27:07 -0700 Subject: [PATCH 1919/2056] [tuya] allow status pin that doesn't match the reported one (#16353) --- esphome/components/tuya/tuya.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index d682adffe38..b29905f9a0d 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -209,13 +209,12 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff bool is_pin_equals = this->status_pin_ != nullptr && this->status_pin_->get_pin() == this->status_pin_reported_; // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send - if (is_pin_equals) { - ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); - this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); }); - } else { - ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. TuyaMcu will work in limited mode.", + if (!is_pin_equals) { + ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. Using supplied pin anyway.", this->status_pin_reported_); } + ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_->get_pin()); + this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); }); } else { this->init_state_ = TuyaInitState::INIT_WIFI; ESP_LOGV(TAG, "Configured WIFI_STATE periodic send"); From e4d9786f00c0c271495a0d3cf457533eaea069e9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 11 May 2026 05:34:47 -0400 Subject: [PATCH 1920/2056] [core] Move platformio files to subfolder (#16347) --- esphome/__main__.py | 68 ++++++------ esphome/analyze_memory/__init__.py | 2 +- esphome/analyze_memory/cli.py | 2 +- esphome/components/esp32/__init__.py | 4 +- esphome/components/esp8266/__init__.py | 4 +- esphome/components/nrf52/__init__.py | 4 +- esphome/components/rp2040/__init__.py | 2 +- esphome/dashboard/web_server.py | 5 +- esphome/espidf/runner.py | 2 +- esphome/espidf/{api.py => toolchain.py} | 2 +- esphome/platformio/__init__.py | 0 .../runner.py} | 2 +- .../toolchain.py} | 2 +- script/build_helpers.py | 2 +- script/ci_memory_impact_extract.py | 2 +- tests/dashboard/test_web_server.py | 4 +- tests/integration/conftest.py | 2 +- tests/unit_tests/conftest.py | 16 +-- tests/unit_tests/test_main.py | 40 +++---- ...io_api.py => test_platformio_toolchain.py} | 100 +++++++++--------- 20 files changed, 134 insertions(+), 131 deletions(-) rename esphome/espidf/{api.py => toolchain.py} (99%) create mode 100644 esphome/platformio/__init__.py rename esphome/{platformio_runner.py => platformio/runner.py} (99%) rename esphome/{platformio_api.py => platformio/toolchain.py} (99%) rename tests/unit_tests/{test_platformio_api.py => test_platformio_toolchain.py} (92%) diff --git a/esphome/__main__.py b/esphome/__main__.py index 01b33eb8acf..54d6384bfc0 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -766,24 +766,24 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - rc = api.run_compile(config, CORE.verbose) + rc = toolchain.run_compile(config, CORE.verbose) if rc != 0: return rc # Create factory.bin, ota.bin, and firmware.elf copy - api.create_factory_bin() - api.create_ota_bin() - api.create_elf_copy() + toolchain.create_factory_bin() + toolchain.create_ota_bin() + toolchain.create_elf_copy() else: - from esphome import platformio_api + from esphome.platformio import toolchain - rc = platformio_api.run_compile(config, CORE.verbose) + rc = toolchain.run_compile(config, CORE.verbose) if rc != 0: return rc - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: return 1 @@ -879,13 +879,15 @@ def upload_using_esptool( if file is not None: flash_images = [FlashImage(path=file, offset="0x0")] elif CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - flash_images = [FlashImage(path=api.get_factory_firmware_path(), offset="0x0")] + flash_images = [ + FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0") + ] else: - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" flash_images = [ @@ -958,13 +960,13 @@ def upload_using_esptool( def upload_using_platformio(config: ConfigType, port: str) -> int: - from esphome import platformio_api + from esphome.platformio import toolchain # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for # the upload target, but 'nobuild' skips the build phase that creates it. # Create it here so the upload doesn't fail. if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040: - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) build_dir = Path(idedata.firmware_elf_path).parent firmware_bin = build_dir / "firmware.bin" signed_bin = build_dir / "firmware.bin.signed" @@ -974,15 +976,15 @@ def upload_using_platformio(config: ConfigType, port: str) -> int: upload_args = ["-t", "upload", "-t", "nobuild"] if port is not None: upload_args += ["--upload-port", port] - return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + return toolchain.run_platformio_cli_run(config, CORE.verbose, *upload_args) def _find_picotool() -> Path | None: """Find the picotool binary from PlatformIO packages.""" - from esphome import platformio_api + from esphome.platformio import toolchain try: - idedata = platformio_api.get_idedata(CORE.config) + idedata = toolchain.get_idedata(CORE.config) except Exception: # noqa: BLE001 # pylint: disable=broad-except return None return get_picotool_path(idedata.cc_path) @@ -995,9 +997,9 @@ def upload_using_picotool(config: ConfigType) -> int: the mass storage copy approach that causes "disk not ejected properly" warnings on macOS. """ - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) firmware_elf = Path(idedata.firmware_elf_path) if not firmware_elf.is_file(): @@ -1457,11 +1459,11 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: return exit_code if CORE.is_host: if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - program_path = str(api.get_elf_path()) + program_path = str(toolchain.get_elf_path()) else: - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Successfully compiled program to path '%s'", program_path) @@ -1515,11 +1517,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: _LOGGER.info("Successfully compiled program.") if CORE.is_host: if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - program_path = str(api.get_elf_path()) + program_path = str(toolchain.get_elf_path()) else: - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Running program from path '%s'", program_path) @@ -1719,12 +1721,12 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: ) return 1 - from esphome import platformio_api + from esphome.platformio import toolchain logging.disable(logging.INFO) logging.disable(logging.WARNING) - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: return 1 @@ -1753,16 +1755,16 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: # Get idedata for analysis idedata = None if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - objdump_path = str(api.get_objdump_path()) - readelf_path = str(api.get_readelf_path()) + objdump_path = str(toolchain.get_objdump_path()) + readelf_path = str(toolchain.get_readelf_path()) - firmware_elf = api.get_elf_path() + firmware_elf = toolchain.get_elf_path() else: - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: _LOGGER.error("Failed to get IDE data for memory analysis") return 1 diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 33854ac2896..1198562218d 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -24,7 +24,7 @@ from .helpers import ( from .toolchain import find_tool, resolve_tool_path, run_tool if TYPE_CHECKING: - from esphome.platformio_api import IDEData + from esphome.platformio.toolchain import IDEData _LOGGER = logging.getLogger(__name__) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index b7561e8ffc0..8f1f39e1d65 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -739,7 +739,7 @@ def main(): import json from pathlib import Path - from esphome.platformio_api import IDEData + from esphome.platformio.toolchain import IDEData build_path = Path(build_dir) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ba32d13ab34..bb823937aa0 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2582,9 +2582,9 @@ def copy_files(): def _decode_pc(config, addr): - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if not idedata.addr2line_path or not idedata.firmware_elf_path: _LOGGER.debug("decode_pc no addr2line") return diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index b6383653f4e..38df282fb98 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -463,9 +463,9 @@ ESP8266_EXCEPTION_CODES = { def _decode_pc(config, addr): - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if not idedata.addr2line_path or not idedata.firmware_elf_path: _LOGGER.debug("decode_pc no addr2line") return diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index d2ed3b15e9c..38efccab11d 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -397,11 +397,11 @@ def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: def _upload_using_platformio( config: ConfigType, port: str, upload_args: list[str] ) -> int | str: - from esphome import platformio_api + from esphome.platformio import toolchain if port is not None: upload_args += ["--upload-port", port] - return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + return toolchain.run_platformio_cli_run(config, CORE.verbose, *upload_args) def upload_program(config: ConfigType, args, host: str) -> bool: diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 7e450578cdd..81809c05519 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -510,7 +510,7 @@ def process_stacktrace(config, line: str, backtrace_state: bool) -> bool: if backtrace_state: if match := _CRASH_ADDR_RE.search(line): - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata idedata = get_idedata(config) if idedata.addr2line_path: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index d67245967c5..916e937a532 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -40,8 +40,9 @@ import voluptuous as vol import yaml from yaml.nodes import Node -from esphome import const, platformio_api, yaml_util +from esphome import const, yaml_util from esphome.helpers import get_bool_env, mkdir_p, sort_ip_addresses +from esphome.platformio import toolchain from esphome.storage_json import ( StorageJSON, archive_storage_path, @@ -1090,7 +1091,7 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404 if rc == 2 else 500) return - idedata = platformio_api.IDEData(json.loads(stdout)) + idedata = toolchain.IDEData(json.loads(stdout)) found = False for image in idedata.extra_flash_images: diff --git a/esphome/espidf/runner.py b/esphome/espidf/runner.py index e740ab72854..34e3e7694b5 100644 --- a/esphome/espidf/runner.py +++ b/esphome/espidf/runner.py @@ -190,7 +190,7 @@ def main() -> int: script_path = sys.argv[1] - # Mirror the platformio_runner behaviour: verbose mode disables the + # Mirror the platformio runner behaviour: verbose mode disables the # line filter so all output reaches the user. is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[2:]) filter_lines = None if is_verbose else FILTER_IDF_LINES or None diff --git a/esphome/espidf/api.py b/esphome/espidf/toolchain.py similarity index 99% rename from esphome/espidf/api.py rename to esphome/espidf/toolchain.py index 847de249a7f..da6d3a8a37f 100644 --- a/esphome/espidf/api.py +++ b/esphome/espidf/toolchain.py @@ -15,7 +15,7 @@ from esphome.espidf.framework import check_esp_idf_install, get_framework_env _LOGGER = logging.getLogger(__name__) -DOMAIN = "espidf_api" +DOMAIN = "espidf_toolchain" @dataclass diff --git a/esphome/platformio/__init__.py b/esphome/platformio/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/platformio_runner.py b/esphome/platformio/runner.py similarity index 99% rename from esphome/platformio_runner.py rename to esphome/platformio/runner.py index 5b14a725577..976979dc57b 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio/runner.py @@ -1,6 +1,6 @@ """Subprocess entry point that applies ESPHome's PlatformIO patches. -Invoked via ``python -m esphome.platformio_runner`` instead of +Invoked via ``python -m esphome.platformio.runner`` instead of ``python -m platformio`` so that the patches (incremental rebuild preservation, download retries) apply inside the subprocess. Running PlatformIO in a subprocess keeps its ``sys.path`` mutations and other diff --git a/esphome/platformio_api.py b/esphome/platformio/toolchain.py similarity index 99% rename from esphome/platformio_api.py rename to esphome/platformio/toolchain.py index 81ff01306a6..073e134ac4b 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio/toolchain.py @@ -64,7 +64,7 @@ def run_platformio_cli(*args, **kwargs) -> str | int: # a user-provided value (or the unmodified path on platforms that # don't need the strip). os.environ["PYTHONEXEPATH"] = python_exe - cmd = [python_exe, "-m", "esphome.platformio_runner"] + list(args) + cmd = [python_exe, "-m", "esphome.platformio.runner"] + list(args) return run_external_process(*cmd, **kwargs) diff --git a/script/build_helpers.py b/script/build_helpers.py index 0e0e8170a0a..fa722aa0990 100644 --- a/script/build_helpers.py +++ b/script/build_helpers.py @@ -23,7 +23,7 @@ from esphome.config import validate_config from esphome.const import CONF_PLATFORM from esphome.core import CORE from esphome.loader import get_component, get_platform -from esphome.platformio_api import get_idedata +from esphome.platformio.toolchain import get_idedata from tests.testing_helpers import ComponentManifestOverride, set_testing_manifest # This must coincide with the version in /platformio.ini diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index dd91fa861ce..2aa7394b112 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -26,7 +26,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # pylint: disable=wrong-import-position from esphome.analyze_memory import MemoryAnalyzer -from esphome.platformio_api import IDEData +from esphome.platformio.toolchain import IDEData from script.ci_helpers import write_github_output # Regex patterns for extracting memory usage from PlatformIO output diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 1a62cfda904..626aea02162 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -128,8 +128,8 @@ def mock_storage_json() -> Generator[MagicMock]: @pytest.fixture def mock_idedata() -> Generator[MagicMock]: - """Fixture to mock platformio_api.IDEData.""" - with patch("esphome.dashboard.web_server.platformio_api.IDEData") as mock: + """Fixture to mock platformio toolchain.IDEData.""" + with patch("esphome.dashboard.web_server.toolchain.IDEData") as mock: yield mock diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f36543b7cd9..fb025ce427f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -23,7 +23,7 @@ import pytest_asyncio import esphome.config from esphome.core import CORE -from esphome.platformio_api import get_idedata +from esphome.platformio.toolchain import get_idedata from .const import ( API_CONNECTION_TIMEOUT, diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 626f4168a60..13450b10f08 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -64,15 +64,15 @@ def mock_copy_file_if_changed() -> Generator[Mock, None, None]: @pytest.fixture def mock_run_platformio_cli() -> Generator[Mock, None, None]: - """Mock run_platformio_cli for platformio_api.""" - with patch("esphome.platformio_api.run_platformio_cli") as mock: + """Mock run_platformio_cli for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_platformio_cli") as mock: yield mock @pytest.fixture def mock_run_platformio_cli_run() -> Generator[Mock, None, None]: - """Mock run_platformio_cli_run for platformio_api.""" - with patch("esphome.platformio_api.run_platformio_cli_run") as mock: + """Mock run_platformio_cli_run for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_platformio_cli_run") as mock: yield mock @@ -92,8 +92,8 @@ def mock_esp8266_decode_pc() -> Generator[Mock, None, None]: @pytest.fixture def mock_run_external_process() -> Generator[Mock, None, None]: - """Mock run_external_process for platformio_api.""" - with patch("esphome.platformio_api.run_external_process") as mock: + """Mock run_external_process for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_external_process") as mock: yield mock @@ -113,8 +113,8 @@ def mock_subprocess_run() -> Generator[Mock, None, None]: @pytest.fixture def mock_get_idedata() -> Generator[Mock, None, None]: - """Mock get_idedata for platformio_api.""" - with patch("esphome.platformio_api.get_idedata") as mock: + """Mock get_idedata for platformio toolchain.""" + with patch("esphome.platformio.toolchain.get_idedata") as mock: yield mock diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3eb50de76b4..3648de443dc 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -18,7 +18,6 @@ import pytest from pytest import CaptureFixture from zeroconf import ServiceStateChange -from esphome import platformio_api from esphome.__main__ import ( Purpose, _get_configured_xtal_freq, @@ -96,6 +95,7 @@ from esphome.espota2 import ( OTA_TYPE_UPDATE_BOOTLOADER, OTA_TYPE_UPDATE_PARTITION_TABLE, ) +from esphome.platformio import toolchain from esphome.util import BootselResult, FlashImage from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -287,7 +287,7 @@ def mock_run_external_process() -> Generator[Mock]: @pytest.fixture def mock_run_external_command_main() -> Generator[Mock]: - """Mock run_external_command in __main__ module (different from platformio_api).""" + """Mock run_external_command in __main__ module (different from platformio toolchain).""" with patch("esphome.__main__.run_external_command") as mock: mock.return_value = 0 # Default to success yield mock @@ -1199,7 +1199,7 @@ def test_upload_using_esptool_path_conversion( CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} # Create mock IDEData with Path objects - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), @@ -1277,7 +1277,7 @@ def test_upload_using_esptool_skips_missing_extra_flash_images( missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin" - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), @@ -1389,8 +1389,8 @@ def test_upload_using_platformio_creates_signed_bin_for_rp2040( mock_idedata.firmware_elf_path = str(firmware_elf) with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), - patch("esphome.platformio_api.run_platformio_cli_run", return_value=0), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0), ): result = upload_using_platformio({}, "/dev/ttyACM0") @@ -1406,7 +1406,7 @@ def test_upload_using_platformio_skips_signed_bin_for_non_rp2040( """Test that upload_using_platformio doesn't create signed bin for non-RP2040.""" setup_core(platform=PLATFORM_ESP32) - with patch("esphome.platformio_api.run_platformio_cli_run", return_value=0): + with patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0): result = upload_using_platformio({}, "/dev/ttyUSB0") assert result == 0 @@ -1504,7 +1504,7 @@ def test_upload_using_picotool_success(tmp_path: Path) -> None: config = {} with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), patch("subprocess.run", return_value=mock_result), ): exit_code = upload_using_picotool(config) @@ -1524,7 +1524,7 @@ def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: mock_idedata.cc_path = "/fake/path/gcc" config = {} - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + with patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata): exit_code = upload_using_picotool(config) assert exit_code == 1 @@ -1544,7 +1544,7 @@ def test_upload_using_picotool_not_found(tmp_path: Path) -> None: mock_idedata.cc_path = "/fake/path/gcc" config = {} - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + with patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata): exit_code = upload_using_picotool(config) assert exit_code == 1 @@ -1578,7 +1578,7 @@ def test_upload_using_picotool_permission_error(tmp_path: Path) -> None: config = {} with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), patch("subprocess.run", return_value=mock_result), ): exit_code = upload_using_picotool(config) @@ -4696,7 +4696,7 @@ def test_command_analyze_memory_success( firmware_elf.write_text("mock elf file") # Mock idedata - mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj = MagicMock(spec=toolchain.IDEData) mock_idedata_obj.firmware_elf_path = str(firmware_elf) mock_idedata_obj.objdump_path = "/path/to/objdump" mock_idedata_obj.readelf_path = "/path/to/readelf" @@ -4768,7 +4768,7 @@ def test_command_analyze_memory_with_external_components( firmware_elf.write_text("mock elf file") # Mock idedata - mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj = MagicMock(spec=toolchain.IDEData) mock_idedata_obj.firmware_elf_path = str(firmware_elf) mock_idedata_obj.objdump_path = "/path/to/objdump" mock_idedata_obj.readelf_path = "/path/to/readelf" @@ -4859,16 +4859,18 @@ def test_command_analyze_memory_no_idedata( @pytest.fixture def mock_compile_build_info_run_compile() -> Generator[Mock]: - """Mock platformio_api.run_compile for build_info tests.""" - with patch("esphome.platformio_api.run_compile", return_value=0) as mock: + """Mock toolchain.run_compile for build_info tests.""" + with patch("esphome.platformio.toolchain.run_compile", return_value=0) as mock: yield mock @pytest.fixture def mock_compile_build_info_get_idedata() -> Generator[Mock]: - """Mock platformio_api.get_idedata for build_info tests.""" + """Mock toolchain.get_idedata for build_info tests.""" mock_idedata = MagicMock() - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata) as mock: + with patch( + "esphome.platformio.toolchain.get_idedata", return_value=mock_idedata + ) as mock: yield mock @@ -5778,7 +5780,7 @@ def test_upload_using_esptool_passes_crystal_callback( sdkconfig = build_dir / "sdkconfig.test" sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [] mock_get_idedata.return_value = mock_idedata @@ -5808,7 +5810,7 @@ def test_upload_using_esptool_subprocess_passes_crystal_callback( sdkconfig = build_dir / "sdkconfig.test" sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [] mock_get_idedata.return_value = mock_idedata diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_toolchain.py similarity index 92% rename from tests/unit_tests/test_platformio_api.py rename to tests/unit_tests/test_platformio_toolchain.py index 7a88ec4d9e1..f771437dd47 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -1,4 +1,4 @@ -"""Tests for platformio_api.py path functions.""" +"""Tests for esphome.platformio.toolchain path functions.""" # pylint: disable=protected-access @@ -11,8 +11,8 @@ from unittest.mock import MagicMock, Mock, call, patch import pytest -from esphome import platformio_api, platformio_runner from esphome.core import CORE, EsphomeError +from esphome.platformio import runner, toolchain from esphome.util import FlashImage @@ -21,7 +21,7 @@ def test_idedata_firmware_elf_path(setup_core: Path) -> None: CORE.build_path = setup_core / "build" / "test" CORE.name = "test" raw_data = {"prog_path": "/path/to/firmware.elf"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) assert idedata.firmware_elf_path == Path("/path/to/firmware.elf") @@ -32,7 +32,7 @@ def test_idedata_firmware_bin_path(setup_core: Path) -> None: CORE.name = "test" prog_path = str(Path("/path/to/firmware.elf")) raw_data = {"prog_path": prog_path} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.firmware_bin_path assert isinstance(result, Path) @@ -47,7 +47,7 @@ def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None CORE.name = "test" prog_path = str(Path("/complex/path/to/build/firmware.elf")) raw_data = {"prog_path": prog_path} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.firmware_bin_path expected = Path("/complex/path/to/build/firmware.bin") @@ -67,7 +67,7 @@ def test_idedata_extra_flash_images(setup_core: Path) -> None: ] }, } - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) images = idedata.extra_flash_images assert len(images) == 2 @@ -83,7 +83,7 @@ def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: CORE.build_path = setup_core / "build" / "test" CORE.name = "test" raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) images = idedata.extra_flash_images assert images == [] @@ -97,7 +97,7 @@ def test_idedata_cc_path(setup_core: Path) -> None: "prog_path": "/path/to/firmware.elf", "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", } - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) assert ( idedata.cc_path @@ -132,7 +132,7 @@ def test_load_idedata_returns_dict( mock_run_platformio_cli_run.return_value = '{"prog_path": "/test/firmware.elf"}' config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) assert result is not None assert isinstance(result, dict) @@ -161,7 +161,7 @@ def test_load_idedata_uses_cache_when_valid( os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1)) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should not call _run_idedata since cache is valid mock_run_platformio_cli_run.assert_not_called() @@ -194,7 +194,7 @@ def test_load_idedata_regenerates_when_platformio_ini_newer( mock_run_platformio_cli_run.return_value = json.dumps(new_data) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should call _run_idedata since platformio.ini is newer mock_run_platformio_cli_run.assert_called_once() @@ -228,7 +228,7 @@ def test_load_idedata_regenerates_on_corrupted_cache( mock_run_platformio_cli_run.return_value = json.dumps(new_data) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should call _run_idedata since cache is corrupted mock_run_platformio_cli_run.assert_called_once() @@ -253,7 +253,7 @@ def test_run_idedata_parses_json_from_output( f"Some preamble\n{json.dumps(expected_data)}\nSome postamble" ) - result = platformio_api._run_idedata(config) + result = toolchain._run_idedata(config) assert result == expected_data @@ -267,7 +267,7 @@ def test_run_idedata_raises_on_no_json( mock_run_platformio_cli_run.return_value = "No JSON in this output" with pytest.raises(EsphomeError): - platformio_api._run_idedata(config) + toolchain._run_idedata(config) def test_run_idedata_raises_on_invalid_json( @@ -279,7 +279,7 @@ def test_run_idedata_raises_on_invalid_json( # The ValueError from json.loads is re-raised with pytest.raises(ValueError): - platformio_api._run_idedata(config) + toolchain._run_idedata(config) def test_run_platformio_cli_sets_environment_variables( @@ -290,7 +290,7 @@ def test_run_platformio_cli_sets_environment_variables( with patch.dict(os.environ, {}, clear=False): mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") # Check environment variables were set assert os.environ["PLATFORMIO_FORCE_COLOR"] == "true" @@ -303,11 +303,11 @@ def test_run_platformio_cli_sets_environment_variables( assert "PYTHONWARNINGS" in os.environ # Check command was called correctly — runs PlatformIO as a subprocess - # via the esphome.platformio_runner entry point. + # via the esphome.platformio.runner entry point. mock_run_external_process.assert_called_once() args = mock_run_external_process.call_args[0] assert "-m" in args - assert "esphome.platformio_runner" in args + assert "esphome.platformio.runner" in args assert "test" in args assert "arg" in args @@ -342,8 +342,8 @@ def test_strip_win_long_path_prefix( platform: str, input_path: str, expected: str ) -> None: r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32.""" - with patch("esphome.platformio_api.sys.platform", platform): - assert platformio_api._strip_win_long_path_prefix(input_path) == expected + with patch("esphome.platformio.toolchain.sys.platform", platform): + assert toolchain._strip_win_long_path_prefix(input_path) == expected def test_run_platformio_cli_strips_win_long_path_prefix( @@ -366,15 +366,15 @@ def test_run_platformio_cli_strips_win_long_path_prefix( with ( patch.dict(os.environ, {}, clear=False), - patch("esphome.platformio_api.sys.platform", "win32"), - patch("esphome.platformio_api.sys.executable", prefixed_exe), + patch("esphome.platformio.toolchain.sys.platform", "win32"), + patch("esphome.platformio.toolchain.sys.executable", prefixed_exe), ): # Pop any pre-existing PYTHONEXEPATH so the assertion below reflects # what run_platformio_cli set, not whatever the test runner's # environment happened to contain. os.environ.pop("PYTHONEXEPATH", None) mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") # The subprocess is invoked with the stripped executable path. mock_run_external_process.assert_called_once() @@ -398,12 +398,12 @@ def test_run_platformio_cli_does_not_set_pythonexepath_without_strip( with ( patch.dict(os.environ, {}, clear=False), - patch("esphome.platformio_api.sys.platform", "linux"), - patch("esphome.platformio_api.sys.executable", plain_exe), + patch("esphome.platformio.toolchain.sys.platform", "linux"), + patch("esphome.platformio.toolchain.sys.executable", plain_exe), ): os.environ.pop("PYTHONEXEPATH", None) mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") mock_run_external_process.assert_called_once() args = mock_run_external_process.call_args[0] @@ -419,7 +419,7 @@ def test_run_platformio_cli_run_builds_command( mock_run_platformio_cli.return_value = 0 config = {"name": "test"} - platformio_api.run_platformio_cli_run(config, True, "extra", "args") + toolchain.run_platformio_cli_run(config, True, "extra", "args") mock_run_platformio_cli.assert_called_once_with( "run", "-d", CORE.build_path, "-v", "extra", "args" @@ -434,7 +434,7 @@ def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> Non config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}} mock_run_platformio_cli_run.return_value = 0 - platformio_api.run_compile(config, verbose=True) + toolchain.run_compile(config, verbose=True) mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4") @@ -461,22 +461,22 @@ def test_get_idedata_caches_result( config = {"name": "test"} # First call should load and cache - result1 = platformio_api.get_idedata(config) + result1 = toolchain.get_idedata(config) mock_run_platformio_cli_run.assert_called_once() # Second call should use cache from CORE.data - result2 = platformio_api.get_idedata(config) + result2 = toolchain.get_idedata(config) mock_run_platformio_cli_run.assert_called_once() # Still only called once assert result1 is result2 - assert isinstance(result1, platformio_api.IDEData) + assert isinstance(result1, toolchain.IDEData) assert result1.firmware_elf_path == Path("/test/firmware.elf") def test_idedata_addr2line_path_windows(setup_core: Path) -> None: """Test IDEData.addr2line_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.addr2line_path assert result == "C:\\tools\\addr2line.exe" @@ -485,7 +485,7 @@ def test_idedata_addr2line_path_windows(setup_core: Path) -> None: def test_idedata_addr2line_path_unix(setup_core: Path) -> None: """Test IDEData.addr2line_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.addr2line_path assert result == "/usr/bin/addr2line" @@ -494,7 +494,7 @@ def test_idedata_addr2line_path_unix(setup_core: Path) -> None: def test_idedata_objdump_path_windows(setup_core: Path) -> None: """Test IDEData.objdump_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.objdump_path assert result == "C:\\tools\\objdump.exe" @@ -503,7 +503,7 @@ def test_idedata_objdump_path_windows(setup_core: Path) -> None: def test_idedata_objdump_path_unix(setup_core: Path) -> None: """Test IDEData.objdump_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.objdump_path assert result == "/usr/bin/objdump" @@ -512,7 +512,7 @@ def test_idedata_objdump_path_unix(setup_core: Path) -> None: def test_idedata_readelf_path_windows(setup_core: Path) -> None: """Test IDEData.readelf_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.readelf_path assert result == "C:\\tools\\readelf.exe" @@ -521,7 +521,7 @@ def test_idedata_readelf_path_windows(setup_core: Path) -> None: def test_idedata_readelf_path_unix(setup_core: Path) -> None: """Test IDEData.readelf_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.readelf_path assert result == "/usr/bin/readelf" @@ -547,7 +547,7 @@ def test_patch_structhash(setup_core: Path) -> None: }, ): # Call patch_structhash - platformio_runner.patch_structhash() + runner.patch_structhash() # Verify both modules had clean_build_dir patched # Check that clean_build_dir was set on both modules @@ -599,7 +599,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -649,7 +649,7 @@ def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -697,7 +697,7 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -727,7 +727,7 @@ def test_patch_file_downloader_succeeds_first_try() -> None: ), }, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -766,7 +766,7 @@ def test_patch_file_downloader_retries_on_failure() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -807,7 +807,7 @@ def test_patch_file_downloader_raises_after_max_retries() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -855,7 +855,7 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() -> ), patch("time.sleep"), ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -890,9 +890,9 @@ def test_patch_file_downloader_idempotent() -> None: }, ): # Patch multiple times - platformio_runner.patch_file_downloader() - platformio_runner.patch_file_downloader() - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() + runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -910,9 +910,7 @@ def _filter_through_redirect(line: str) -> str: from esphome.util import RedirectText captured = io.StringIO() - redirect = RedirectText( - captured, filter_lines=platformio_runner.FILTER_PLATFORMIO_LINES - ) + redirect = RedirectText(captured, filter_lines=runner.FILTER_PLATFORMIO_LINES) redirect.write(line + "\n") return captured.getvalue() From b967adeb9d6b1ed30cbb296ad41145d55db0335f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 11 May 2026 08:11:36 -0500 Subject: [PATCH 1921/2056] [wifi] Accept lowercase variant in variant_has_wifi (#16345) --- esphome/components/wifi/__init__.py | 8 +++++- tests/unit_tests/components/test_wifi.py | 33 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 316d4321403..bad57fc4810 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -76,11 +76,17 @@ def variant_has_wifi(variant: str) -> bool: Variants without a native PHY (ESP32-H2, ESP32-P4) need the ``esp32_hosted`` co-processor to use ``wifi:``. + Case-insensitive on *variant* so external callers can pass either + the upstream uppercase form (e.g. ``"ESP32H2"`` from + ``const.VARIANT_ESP32H2``) or a lowercase form their own enum + surfaces (e.g. ``"esp32h2"`` from device-builder's + ``Esp32Variant``). Both classify identically. + Used by device-builder (esphome/device-builder) to decide whether its basic-setup wizard emits a ``wifi:`` block — please keep the signature stable. """ - return variant not in NO_WIFI_VARIANTS + return variant.upper() not in NO_WIFI_VARIANTS _WIFI_FIRST_PLATFORMS: frozenset[str] = frozenset( diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py index e93ae4b5036..71a14d78177 100644 --- a/tests/unit_tests/components/test_wifi.py +++ b/tests/unit_tests/components/test_wifi.py @@ -10,27 +10,44 @@ from esphome.const import Platform @pytest.mark.parametrize( "variant", [ + # Upstream's canonical uppercase form. const.VARIANT_ESP32, const.VARIANT_ESP32S2, const.VARIANT_ESP32S3, const.VARIANT_ESP32C3, const.VARIANT_ESP32C6, + # Lowercase form external callers (e.g. device-builder's + # ``Esp32Variant`` StrEnum) surface. + "esp32", + "esp32s3", + "esp32c3", + # Mixed-case — defence in depth against future callers that + # pull the value off some other serialisation. + "Esp32", ], ) def test_variant_has_wifi_for_native_phy_variants(variant: str) -> None: - """Variants with a native WiFi PHY → True.""" + """Variants with a native WiFi PHY → True, case-insensitive.""" assert variant_has_wifi(variant) is True @pytest.mark.parametrize( "variant", [ + # Upstream's canonical uppercase form. const.VARIANT_ESP32H2, const.VARIANT_ESP32P4, + # Lowercase form external callers (e.g. device-builder's + # ``Esp32Variant`` StrEnum) surface. + "esp32h2", + "esp32p4", + # Mixed-case — defence in depth against future callers that + # pull the value off some other serialisation. + "Esp32H2", ], ) def test_variant_has_wifi_for_no_phy_variants(variant: str) -> None: - """Variants that need ``esp32_hosted`` → False.""" + """Variants that need ``esp32_hosted`` → False, case-insensitive.""" assert variant_has_wifi(variant) is False @@ -44,6 +61,18 @@ def test_has_native_wifi_dispatches_esp32_to_variant_check() -> None: ) +def test_has_native_wifi_esp32_variant_case_insensitive() -> None: + """has_native_wifi accepts lowercase variant input. + + External callers (device-builder's wizard, etc.) may surface + variant strings from their own enums that don't match upstream's + uppercase convention. The dispatcher should classify them + identically. + """ + assert has_native_wifi(platform=Platform.ESP32, variant="esp32h2") is False + assert has_native_wifi(platform=Platform.ESP32, variant="esp32c3") is True + + def test_has_native_wifi_dispatches_rp2040_to_board_check() -> None: """RP2040 platform routes through ``rp2040.board_id_has_wifi``.""" assert has_native_wifi(platform=Platform.RP2040, board="rpipicow") is True From 4d9d6e02e5c53dcd4db3ce84be0300a8b6fb453f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 11 May 2026 08:13:16 -0500 Subject: [PATCH 1922/2056] [core] Add stable esphome.upload_targets module for port classification (#16346) --- esphome/__main__.py | 30 +-------- esphome/components/nrf52/__init__.py | 5 +- esphome/upload_targets.py | 66 ++++++++++++++++++++ tests/unit_tests/test_upload_targets.py | 81 +++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 31 deletions(-) create mode 100644 esphome/upload_targets.py create mode 100644 tests/unit_tests/test_upload_targets.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 54d6384bfc0..ce24f44b3a6 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -64,6 +64,7 @@ from esphome.enum import StrEnum from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log from esphome.types import ConfigType +from esphome.upload_targets import PortType, get_port_type from esphome.util import ( PICOTOOL_PACKAGE, FlashImage, @@ -194,14 +195,6 @@ class Purpose(StrEnum): LOGGING = "logging" -class PortType(StrEnum): - SERIAL = "SERIAL" - NETWORK = "NETWORK" - MQTT = "MQTT" - MQTTIP = "MQTTIP" - BOOTSEL = "BOOTSEL" - - # Magic MQTT port types that require special handling _MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP}) @@ -597,27 +590,6 @@ def _resolve_network_devices( return network_devices -def get_port_type(port: str) -> PortType: - """Determine the type of port/device identifier. - - Returns: - PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) - PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool - PortType.MQTT for MQTT logging - PortType.MQTTIP for MQTT IP lookup - PortType.NETWORK for IP addresses, hostnames, or mDNS names - """ - if port == "BOOTSEL": - return PortType.BOOTSEL - if port.startswith("/") or port.startswith("COM"): - return PortType.SERIAL - if port == "MQTT": - return PortType.MQTT - if port == "MQTTIP": - return PortType.MQTTIP - return PortType.NETWORK - - def run_miniterm(config: ConfigType, port: str, args) -> int: from aioesphomeapi import LogParser import serial diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 38efccab11d..37854d4ab7f 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -405,11 +405,12 @@ def _upload_using_platformio( def upload_program(config: ConfigType, args, host: str) -> bool: - from esphome.__main__ import check_permissions, get_port_type + from esphome.__main__ import check_permissions + from esphome.upload_targets import PortType, get_port_type mcumgr_device: str | None = None - if get_port_type(host) == "SERIAL": + if get_port_type(host) == PortType.SERIAL: check_permissions(host) if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: mcumgr_device = host diff --git a/esphome/upload_targets.py b/esphome/upload_targets.py new file mode 100644 index 00000000000..302ecf73011 --- /dev/null +++ b/esphome/upload_targets.py @@ -0,0 +1,66 @@ +"""Stable classification of ``--device`` / port strings. + +External tooling (the device-builder dashboard at +esphome/device-builder, and other consumers) needs to decide whether +a user-supplied port string names a local serial device, an OTA +network target, an MQTT magic string, or an RP2040 BOOTSEL upload. + +This module is the single stable home for that classification. The +upstream CLI (``esphome.__main__``) re-exports ``PortType`` and +``get_port_type`` from here for its own use; external callers should +import directly from ``esphome.upload_targets`` so the surface stays +stable across releases (``esphome/__main__`` is a CLI entrypoint and +not a stable import path). + +Please keep ``PortType`` member names / values and the +``get_port_type`` signature stable — see the docstrings on each for +the contract. +""" + +from __future__ import annotations + +from esphome.enum import StrEnum + + +class PortType(StrEnum): + """Port classification returned by :func:`get_port_type`. + + Used by device-builder (esphome/device-builder) and other + external tooling to route a user-supplied ``--device`` value to + the right upload / log path. Member names and string values are + part of the stable surface — adding new members is fine, but + existing names / values must not be renamed or changed. + """ + + SERIAL = "SERIAL" + NETWORK = "NETWORK" + MQTT = "MQTT" + MQTTIP = "MQTTIP" + BOOTSEL = "BOOTSEL" + + +def get_port_type(port: str) -> PortType: + """Determine the type of port/device identifier. + + Used by device-builder (esphome/device-builder)'s dashboard to + decide whether a user-supplied ``--device`` value names a local + serial port (must build / flash locally), an OTA network target + (eligible for remote builds), an MQTT magic string, or an RP2040 + BOOTSEL upload. Please keep the signature stable. + + Returns: + PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) + PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool + PortType.MQTT for MQTT logging + PortType.MQTTIP for MQTT IP lookup + PortType.NETWORK for IP addresses, hostnames, or mDNS names + """ + if port == "BOOTSEL": + return PortType.BOOTSEL + if port.startswith("/") or port.startswith("COM"): + return PortType.SERIAL + if port == "MQTT": + return PortType.MQTT + if port == "MQTTIP": + return PortType.MQTTIP + return PortType.NETWORK diff --git a/tests/unit_tests/test_upload_targets.py b/tests/unit_tests/test_upload_targets.py new file mode 100644 index 00000000000..52587ca4e62 --- /dev/null +++ b/tests/unit_tests/test_upload_targets.py @@ -0,0 +1,81 @@ +"""Tests for the stable upload-targets classification helpers.""" + +import pytest + +from esphome.upload_targets import PortType, get_port_type + + +@pytest.mark.parametrize( + "port", + [ + "/dev/ttyUSB0", + "/dev/ttyACM0", + "/dev/cu.usbserial-1410", + "/dev/tty.usbmodem1101", + "COM1", + "COM23", + ], +) +def test_get_port_type_serial(port: str) -> None: + """Local serial devices classify as SERIAL.""" + assert get_port_type(port) is PortType.SERIAL + + +def test_get_port_type_bootsel() -> None: + """``BOOTSEL`` magic string classifies as BOOTSEL.""" + assert get_port_type("BOOTSEL") is PortType.BOOTSEL + + +def test_get_port_type_mqtt() -> None: + """``MQTT`` magic string classifies as MQTT.""" + assert get_port_type("MQTT") is PortType.MQTT + + +def test_get_port_type_mqttip() -> None: + """``MQTTIP`` magic string classifies as MQTTIP.""" + assert get_port_type("MQTTIP") is PortType.MQTTIP + + +@pytest.mark.parametrize( + "port", + [ + "192.168.1.10", + "fe80::1", + "device.local", + "my-esp.example.com", + ], +) +def test_get_port_type_network(port: str) -> None: + """IP addresses, mDNS, and hostnames classify as NETWORK.""" + assert get_port_type(port) is PortType.NETWORK + + +def test_port_type_values_are_stable() -> None: + """Member values are part of the stable surface. + + External tooling (device-builder, etc.) may compare against the + string values directly. Renaming or changing these breaks + downstream consumers — guard against accidental edits. + """ + assert PortType.SERIAL.value == "SERIAL" + assert PortType.NETWORK.value == "NETWORK" + assert PortType.MQTT.value == "MQTT" + assert PortType.MQTTIP.value == "MQTTIP" + assert PortType.BOOTSEL.value == "BOOTSEL" + + +def test_main_re_exports_for_backwards_compat() -> None: + """``esphome.__main__`` re-exports the stable surface. + + The CLI entry point pre-dated the stable module and existing + internal callers (and any third-party code that snuck in via + ``__main__``) still import from there. The re-export must + resolve to the same objects. + """ + from esphome.__main__ import ( + PortType as MainPortType, + get_port_type as main_get_port_type, + ) + + assert MainPortType is PortType + assert main_get_port_type is get_port_type From 105842366ebf95b982c7de6681691d5c4c662a41 Mon Sep 17 00:00:00 2001 From: rwrozelle <rwrozelle@gmail.com> Date: Mon, 11 May 2026 09:57:01 -0400 Subject: [PATCH 1923/2056] [openthread] Remove-freertos-portmacro-header-include (#16338) --- esphome/components/openthread/openthread.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 21dad4f8670..8557427096f 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -2,8 +2,6 @@ #ifdef USE_OPENTHREAD #include "openthread.h" -#include <freertos/portmacro.h> - #include <openthread/cli.h> #include <openthread/instance.h> #include <openthread/logging.h> From 68534ea24d5dae6b4d82e78c5e2cfa909afb89cb Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 11 May 2026 16:04:48 +0200 Subject: [PATCH 1924/2056] [logger] fix crash on zephyr (#16330) --- esphome/components/logger/logger.cpp | 3 +++ esphome/components/logger/logger_zephyr.cpp | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 23b69c36c60..a035525101d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -243,6 +243,9 @@ void Logger::dump_config() { #endif #ifdef USE_ZEPHYR dump_crash_(); + if (!device_is_ready(this->uart_dev_)) { + ESP_LOGE(TAG, " %s is not ready.", LOG_STR_ARG(get_uart_selection_())); + } #endif // Warn users that VERBOSE/VERY_VERBOSE logging impacts performance. // Only the compiled log level matters — all log calls up to this level diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index e9caa8d9d99..240bcc57c79 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -74,9 +74,7 @@ void Logger::pre_setup() { break; #endif } - if (!device_is_ready(uart_dev)) { - ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); - } else { + if (device_is_ready(uart_dev)) { this->uart_dev_ = uart_dev; #if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) uint32_t dtr = 0; From 2edb7ca5c224f8f67b3189f9573a6542c2d4328d Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 11 May 2026 16:05:41 +0200 Subject: [PATCH 1925/2056] [nrf52] add message that west update is running (#16321) --- .clang-tidy.hash | 2 +- esphome/components/nrf52/__init__.py | 2 +- platformio.ini | 2 +- tests/components/nrf52/test.nrf52-adafruit.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 4c4b4e5c9c7..da2e8632817 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -edce6cd78b33b296cef3caa5869d237061345eb346e3f9cb21e3239d2711051f +96c95feaa60831da5f43e3c6a7c7a3a237e17c5d12995a730dbc3884c8dcd11c diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 37854d4ab7f..a9be7e56e70 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -182,7 +182,7 @@ CONFIG_SCHEMA = cv.All( default={}, ): cv.Schema( { - cv.Optional(CONF_VERSION, default="2.6.1-a"): cv.string_strict, + cv.Optional(CONF_VERSION, default="2.6.1-b"): cv.string_strict, cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional( diff --git a/platformio.ini b/platformio.ini index 3023a157323..42b74007793 100644 --- a/platformio.ini +++ b/platformio.ini @@ -233,7 +233,7 @@ extends = common platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip framework = zephyr platform_packages = - platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-a.zip + platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-b.zip build_flags = ${common.build_flags} -DUSE_ZEPHYR diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 300cb7b5d75..3ae48b2a5f8 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -20,4 +20,4 @@ nrf52: voltage: 2.1V uicr_erase: true framework: - version: "2.6.1-a" + version: "2.6.1-b" From a7299cb95b2f8d1b4c43492554d6f4958fd1f8f3 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" <rubenkelevra@gmail.com> Date: Mon, 11 May 2026 16:09:15 +0200 Subject: [PATCH 1926/2056] [esp32_camera] Downgrade esp32-camera to 2.1.5 (#16293) --- esphome/components/camera_encoder/__init__.py | 2 +- esphome/components/esp32_camera/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index 3bbeae78356..a0c59a517a7 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None: buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: - add_idf_component(name="espressif/esp32-camera", ref="2.1.6") + add_idf_component(name="espressif/esp32-camera", ref="2.1.5") cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER") var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 5165956806f..9883a0a43e0 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -399,7 +399,7 @@ async def to_code(config): if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") - add_idf_component(name="espressif/esp32-camera", ref="2.1.6") + add_idf_component(name="espressif/esp32-camera", ref="2.1.5") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 8ffcffa7051..45aaa827c87 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -20,7 +20,7 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.1.6 + version: 2.1.5 espressif/mdns: version: 1.11.0 espressif/esp_wifi_remote: From 30e2f7e8e9da26f68ff4fa24980344407c9ee81d Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Mon, 11 May 2026 09:13:43 -0500 Subject: [PATCH 1927/2056] [thermostat] Fix supplemental action never firing via max run time (#16308) --- .../thermostat/thermostat_climate.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d8478d2648d..2390a96337c 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -506,8 +506,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_IDLE: if (this->idle_action_ready_()) { this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); - if (this->action == climate::CLIMATE_ACTION_COOLING) + if (this->action == climate::CLIMATE_ACTION_COOLING) { this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + } if (this->action == climate::CLIMATE_ACTION_FAN) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); @@ -515,8 +517,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); } } - if (this->action == climate::CLIMATE_ACTION_HEATING) + if (this->action == climate::CLIMATE_ACTION_HEATING) { this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); + } // trig = this->idle_action_trigger_; ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); this->cooling_max_runtime_exceeded_ = false; @@ -599,16 +603,6 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { - // Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off, - // even if supplemental_action_ is already idle (early-return path). This prevents a stale - // heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle - // when HEATING_MAX_RUN_TIME fires while the main action is already IDLE. - if (action == climate::CLIMATE_ACTION_OFF || action == climate::CLIMATE_ACTION_IDLE) { - this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); - this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); - this->cooling_max_runtime_exceeded_ = false; - this->heating_max_runtime_exceeded_ = false; - } // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->supplemental_action_) && this->setup_complete_) { // already in target mode From 8cf0eba0437e5eb378fa902d865228fb0d8eef65 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Mon, 11 May 2026 16:47:06 +0200 Subject: [PATCH 1928/2056] [nrf52][zephyr] prepare for native builds (#16193) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/nrf52/__init__.py | 117 ++++++++++++++---- esphome/components/nrf52/boards.py | 8 ++ .../{zephyr => nrf52}/pre_build.py.script | 0 esphome/components/zephyr/__init__.py | 96 +++++--------- 4 files changed, 131 insertions(+), 90 deletions(-) rename esphome/components/{zephyr => nrf52}/pre_build.py.script (100%) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a9be7e56e70..2aba208af7e 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -9,6 +9,7 @@ import subprocess from esphome import pins import esphome.codegen as cg from esphome.components.zephyr import ( + add_extra_script, copy_files as zephyr_copy_files, zephyr_add_overlay, zephyr_add_pm_static, @@ -21,6 +22,7 @@ from esphome.components.zephyr import ( from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, CONF_CDC_ACM, + KEY_BOARD, KEY_BOOTLOADER, KEY_ZEPHYR, CdcAcm, @@ -36,6 +38,7 @@ from esphome.const import ( CONF_OTA, CONF_RESET_PIN, CONF_SAFE_MODE, + CONF_TOOLCHAIN, CONF_VERSION, CONF_VOLTAGE, KEY_CORE, @@ -44,10 +47,12 @@ from esphome.const import ( KEY_TARGET_PLATFORM, PLATFORM_NRF52, ThreadModel, + Toolchain, ) from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.core.config import BOARD_MAX_LENGTH import esphome.final_validate as fv +from esphome.helpers import write_file_if_changed from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -67,8 +72,35 @@ AUTO_LOAD = ["zephyr", "preferences"] IS_TARGET_PLATFORM = True _LOGGER = logging.getLogger(__name__) +FAKE_BOARD_MANIFEST = """ +{ + "frameworks": [ + "zephyr" + ], + "name": "esphome nrf52", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200 + }, + "url": "https://esphome.io/", + "vendor": "esphome", + "build": { + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_fwid": "0x00B6" + } + } +} +""" + def set_core_data(config: ConfigType) -> ConfigType: + # Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default. + if CORE.toolchain is None: + CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO) zephyr_set_core_data(config) CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR @@ -80,10 +112,18 @@ def set_core_data(config: ConfigType) -> ConfigType: def set_framework(config: ConfigType) -> ConfigType: + if CONF_VERSION not in config[CONF_FRAMEWORK]: + default_version = "2.6.1-b" if CORE.using_toolchain_platformio else "2.9.2" + config = { + **config, + CONF_FRAMEWORK: {**config[CONF_FRAMEWORK], CONF_VERSION: default_version}, + } framework_ver = cv.Version.parse( cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION]) ) CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver + if not CORE.using_toolchain_platformio: + return config if framework_ver < cv.Version(2, 9, 2): return cv.require_framework_version( nrf52_zephyr=cv.Version(2, 6, 1, "a"), @@ -182,7 +222,7 @@ CONFIG_SCHEMA = cv.All( default={}, ): cv.Schema( { - cv.Optional(CONF_VERSION, default="2.6.1-b"): cv.string_strict, + cv.Optional(CONF_VERSION): cv.string_strict, cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional( @@ -238,40 +278,51 @@ FINAL_VALIDATE_SCHEMA = _final_validate @coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config: ConfigType) -> None: """Convert the configuration to code.""" - cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_NRF52") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "NRF52") # nRF52 processors are single-core cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) - cg.add_platformio_option( - "platform", - "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip", - ) - cg.add_platformio_option( - "platform_packages", - [ - f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip", - ], - ) + if CORE.using_toolchain_platformio: + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_platformio_option( + CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] + ) + cg.add_platformio_option( + "platform", + "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip", + ) + cg.add_platformio_option( + "platform_packages", + [ + f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip", + ], + ) + if config[KEY_BOOTLOADER] != BOOTLOADER_MCUBOOT: + # make sure that firmware.zip is created + # for Adafruit_nRF52_Bootloader + cg.add_platformio_option("board_upload.protocol", "nrfutil") + cg.add_platformio_option("board_upload.use_1200bps_touch", "true") + cg.add_platformio_option("board_upload.require_upload_port", "true") + cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + # build is done by west so bypass board checking in platformio + cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: cg.add_define("USE_BOOTLOADER_MCUBOOT") - else: - if "_sd" in config[KEY_BOOTLOADER]: - bootloader = config[KEY_BOOTLOADER].split("_") - sd_id = bootloader[2][2:] - cg.add_define("USE_SOFTDEVICE_ID", int(sd_id)) - if (len(bootloader)) > 3: - sd_version = bootloader[3][1:] - cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version)) - # make sure that firmware.zip is created - # for Adafruit_nRF52_Bootloader - cg.add_platformio_option("board_upload.protocol", "nrfutil") - cg.add_platformio_option("board_upload.use_1200bps_touch", "true") - cg.add_platformio_option("board_upload.require_upload_port", "true") - cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + elif "_sd" in config[KEY_BOOTLOADER]: + bootloader = config[KEY_BOOTLOADER].split("_") + sd_id = bootloader[2][2:] + cg.add_define("USE_SOFTDEVICE_ID", int(sd_id)) + if (len(bootloader)) > 3: + sd_version = bootloader[3][1:] + cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version)) zephyr_setup_preferences() zephyr_to_code(config) @@ -341,6 +392,16 @@ async def _dfu_to_code(dfu_config): def copy_files() -> None: """Copy files to the build directory.""" + + if CORE.using_toolchain_platformio and ( + zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT + or zephyr_data()[KEY_BOARD] == "xiao_ble" + ): + write_file_if_changed( + CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), + FAKE_BOARD_MANIFEST, + ) + zephyr_copy_files() @@ -415,6 +476,8 @@ def upload_program(config: ConfigType, args, host: str) -> bool: if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT: mcumgr_device = host else: + if not CORE.using_toolchain_platformio: + raise EsphomeError("Not implemented yet") result = _upload_using_platformio(config, host, ["-t", "upload"]) if result != 0: raise EsphomeError(f"Upload failed with result: {result}") diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py index 4c33cd99399..564bf560d69 100644 --- a/esphome/components/nrf52/boards.py +++ b/esphome/components/nrf52/boards.py @@ -25,6 +25,14 @@ BOARDS_ZEPHYR = { BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, ] }, + "adafruit_itsybitsy": { + KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + ] + }, } # https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go diff --git a/esphome/components/zephyr/pre_build.py.script b/esphome/components/nrf52/pre_build.py.script similarity index 100% rename from esphome/components/zephyr/pre_build.py.script rename to esphome/components/nrf52/pre_build.py.script diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 5dccecc0974..57f5778d547 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -10,9 +10,7 @@ from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType from .const import ( - BOOTLOADER_MCUBOOT, CONF_CDC_ACM, - KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, KEY_KCONFIG, @@ -50,8 +48,8 @@ class Section: class ZephyrData(TypedDict): board: str bootloader: str - prj_conf: dict[str, tuple[PrjConfValueType, bool]] - overlay: str + prj_conf: dict[str, dict[str, tuple[PrjConfValueType, bool]]] + overlay: dict[str, str] extra_build_files: dict[str, Path] pm_static: list[Section] user: dict[str, list[str]] @@ -63,7 +61,9 @@ def zephyr_set_core_data(config: ConfigType) -> None: board=config[CONF_BOARD], bootloader=config[KEY_BOOTLOADER], prj_conf={}, - overlay="", + overlay={ + "": "", + }, # set empty to make sure that overlay is cleared after config change extra_build_files={}, pm_static=[], user={}, @@ -76,12 +76,14 @@ def zephyr_data() -> ZephyrData: def zephyr_add_prj_conf( - name: str, value: PrjConfValueType, required: bool = True + name: str, value: PrjConfValueType, required: bool = True, image: str = "" ) -> None: """Set an zephyr prj conf value.""" if not name.startswith("CONFIG_"): name = "CONFIG_" + name - prj_conf = zephyr_data()[KEY_PRJ_CONF] + if image not in zephyr_data()[KEY_PRJ_CONF]: + zephyr_data()[KEY_PRJ_CONF][image] = {} + prj_conf = zephyr_data()[KEY_PRJ_CONF][image] if name not in prj_conf: prj_conf[name] = (value, required) return @@ -94,8 +96,11 @@ def zephyr_add_prj_conf( prj_conf[name] = (value, required) -def zephyr_add_overlay(content): - zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content) +def zephyr_add_overlay(content: str, image: str = "") -> None: + data = zephyr_data() + if image not in data[KEY_OVERLAY]: + data[KEY_OVERLAY][image] = "" + data[KEY_OVERLAY][image] += textwrap.dedent(content) def add_extra_build_file(filename: str, path: Path) -> bool: @@ -118,8 +123,6 @@ def zephyr_to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ZEPHYR") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") - # build is done by west so bypass board checking in platformio - cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) # c++ support zephyr_add_prj_conf("NEWLIB_LIBC", True) zephyr_add_prj_conf("FPU", True) @@ -132,18 +135,12 @@ def zephyr_to_code(config: ConfigType) -> None: # <err> os: Illegal load of EXC_RETURN into PC zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048) - add_extra_script( - "pre", - "pre_build.py", - Path(__file__).parent / "pre_build.py.script", - ) - CORE.add_job(_cdc_acm_to_code, config) @coroutine_with_priority(CoroPriority.FINAL) async def _cdc_acm_to_code(config: ConfigType) -> None: - if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF]: + if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF][""]: var = cg.new_Pvariable(config[CONF_CDC_ACM]) await cg.register_component(var, {}) @@ -219,55 +216,28 @@ def copy_files(): """ ) - want_opts = zephyr_data()[KEY_PRJ_CONF] - - prj_conf = ( - "\n".join( - f"{name}={_format_prj_conf_val(value[0])}" - for name, value in sorted(want_opts.items()) + for image, want_opts in zephyr_data()[KEY_PRJ_CONF].items(): + prj_conf = ( + "\n".join( + f"{name}={_format_prj_conf_val(value[0])}" + for name, value in sorted(want_opts.items()) + ) + + "\n" ) - + "\n" - ) - write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf) + if image: + path = CORE.relative_build_path(f"sysbuild/{image}.conf") + else: + path = CORE.relative_build_path("zephyr/prj.conf") - write_file_if_changed( - CORE.relative_build_path("zephyr/app.overlay"), - zephyr_data()[KEY_OVERLAY], - ) + write_file_if_changed(CORE.relative_build_path(path), prj_conf) - if ( - zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT - or zephyr_data()[KEY_BOARD] == "xiao_ble" - ): - fake_board_manifest = """ -{ - "frameworks": [ - "zephyr" - ], - "name": "esphome nrf52", - "upload": { - "maximum_ram_size": 248832, - "maximum_size": 815104, - "speed": 115200 - }, - "url": "https://esphome.io/", - "vendor": "esphome", - "build": { - "bsp": { - "name": "adafruit" - }, - "softdevice": { - "sd_fwid": "0x00B6" - } - } -} -""" - - write_file_if_changed( - CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), - fake_board_manifest, - ) + for image, content in zephyr_data()[KEY_OVERLAY].items(): + if image: + path = CORE.relative_build_path(f"sysbuild/{image}.overlay") + else: + path = CORE.relative_build_path("zephyr/app.overlay") + write_file_if_changed(path, content) for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items(): copy_file_if_changed( From 4ac7bc46064250ca119d335d37cbc931806c931c Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Mon, 11 May 2026 16:51:47 +0200 Subject: [PATCH 1929/2056] [zigbee] Add sensor support on esp32 (#16026) --- esphome/components/zigbee/__init__.py | 11 +- esphome/components/zigbee/const.py | 113 ++++++++++++++++++ esphome/components/zigbee/const_esp32.py | 4 + .../zigbee/zigbee_attribute_esp32.h | 11 ++ esphome/components/zigbee/zigbee_ep_esp32.py | 29 +++++ esphome/components/zigbee/zigbee_esp32.py | 69 +++++++++-- .../components/zigbee/zigbee_helpers_esp32.c | 7 ++ esphome/components/zigbee/zigbee_zephyr.py | 66 +--------- esphome/const.py | 1 + tests/components/zigbee/common_esp32.yaml | 6 +- 10 files changed, 238 insertions(+), 79 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 8605b4fa1a3..a7e9d1096f7 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -36,6 +36,7 @@ from .const_zephyr import ( from .zigbee_esp32 import ( final_validate_esp32, validate_binary_sensor_esp32, + validate_sensor_esp32, zigbee_require_vfs_select, ) from .zigbee_zephyr import ( @@ -49,8 +50,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@luar123", "@tomaszduda23"] - -BINARY_SENSOR_SCHEMA = cv.Schema( +BASE_SCHEMA = cv.Schema( { cv.Optional(CONF_REPORT): cv.All( cv.requires_component("zigbee"), @@ -58,8 +58,9 @@ BINARY_SENSOR_SCHEMA = cv.Schema( cv.enum(REPORT, lower=True), ) } -).extend(zephyr_binary_sensor) -SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) +) +BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_binary_sensor) +SENSOR_SCHEMA = cv.Schema({}).extend(BASE_SCHEMA).extend(zephyr_sensor) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number) @@ -227,7 +228,7 @@ def validate_sensor(config: ConfigType) -> ConfigType: if "zigbee" not in CORE.loaded_integrations or config.get(CONF_INTERNAL): return config if CORE.is_esp32: - return config + return validate_sensor_esp32(config) return consume_endpoint(config) diff --git a/esphome/components/zigbee/const.py b/esphome/components/zigbee/const.py index 26ae2cc0ec4..7d0e14c67aa 100644 --- a/esphome/components/zigbee/const.py +++ b/esphome/components/zigbee/const.py @@ -1,4 +1,47 @@ +from enum import IntEnum + import esphome.codegen as cg +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLUME_FLOW_RATE, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_CENTIMETER, + UNIT_DECIBEL, + UNIT_HECTOPASCAL, + UNIT_HERTZ, + UNIT_HOUR, + UNIT_KELVIN, + UNIT_KILOMETER, + UNIT_KILOWATT, + UNIT_KILOWATT_HOURS, + UNIT_LITRE_PER_SECOND, + UNIT_LUX, + UNIT_METER, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_MILLIAMP, + UNIT_MILLIGRAMS_PER_CUBIC_METER, + UNIT_MILLIMETER, + UNIT_MILLISECOND, + UNIT_MILLIVOLT, + UNIT_MINUTE, + UNIT_OHM, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, + UNIT_PASCAL, + UNIT_PERCENT, + UNIT_SECOND, + UNIT_VOLT, + UNIT_WATT, + UNIT_WATT_HOURS, +) zigbee_ns = cg.esphome_ns.namespace("zigbee") ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) @@ -30,3 +73,73 @@ POWER_SOURCE = { } KEY_ZIGBEE = "zigbee" + +# BACnet engineering units mapping (ZCL uses BACnet unit codes) +# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py +BACNET_UNITS = { + UNIT_CELSIUS: 62, + UNIT_KELVIN: 63, + UNIT_VOLT: 5, + UNIT_MILLIVOLT: 124, + UNIT_AMPERE: 3, + UNIT_MILLIAMP: 2, + UNIT_OHM: 4, + UNIT_WATT: 47, + UNIT_KILOWATT: 48, + UNIT_WATT_HOURS: 18, + UNIT_KILOWATT_HOURS: 19, + UNIT_PASCAL: 53, + UNIT_HECTOPASCAL: 133, + UNIT_HERTZ: 27, + UNIT_MILLIMETER: 30, + UNIT_CENTIMETER: 118, + UNIT_METER: 31, + UNIT_KILOMETER: 193, + UNIT_MILLISECOND: 159, + UNIT_SECOND: 73, + UNIT_MINUTE: 72, + UNIT_HOUR: 71, + UNIT_PARTS_PER_MILLION: 96, + UNIT_PARTS_PER_BILLION: 97, + UNIT_MICROGRAMS_PER_CUBIC_METER: 219, + UNIT_MILLIGRAMS_PER_CUBIC_METER: 218, + UNIT_LUX: 37, + UNIT_DECIBEL: 199, + UNIT_PERCENT: 98, +} +BACNET_UNIT_NO_UNITS = 95 + + +class AnalogInputType(IntEnum): + TEMP_DEGREES_C = 0x00 + RELATIVE_HUMIDITY_PERCENT = 0x01 + PRESSURE_PASCAL = 0x02 + FLOW_LITERS_PER_SEC = 0x03 + PERCENTAGE = 0x04 + PARTS_PER_MILLION = 0x05 + ROTATIONAL_SPEED_RPM = 0x06 + CURRENT_AMPS = 0x07 + FREQUENCY_HZ = 0x08 + POWER_WATTS = 0x09 + POWER_KILO_WATTS = 0x0A + ENERGY_KILO_WATT_HOURS = 0x0B + COUNT = 0x0C + ENTHALPY_KJOULES_PER_KG = 0x0D + TIME_SECONDS = 0x0E + + +ANALOG_INPUT_APPTYPE = { + (DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS): AnalogInputType.TEMP_DEGREES_C, + (DEVICE_CLASS_HUMIDITY, UNIT_PERCENT): AnalogInputType.RELATIVE_HUMIDITY_PERCENT, + (DEVICE_CLASS_PRESSURE, UNIT_PASCAL): AnalogInputType.PRESSURE_PASCAL, + ( + DEVICE_CLASS_VOLUME_FLOW_RATE, + UNIT_LITRE_PER_SECOND, + ): AnalogInputType.FLOW_LITERS_PER_SEC, + (DEVICE_CLASS_CURRENT, UNIT_AMPERE): AnalogInputType.CURRENT_AMPS, + (DEVICE_CLASS_FREQUENCY, UNIT_HERTZ): AnalogInputType.FREQUENCY_HZ, + (DEVICE_CLASS_POWER, UNIT_WATT): AnalogInputType.POWER_WATTS, + (DEVICE_CLASS_POWER, UNIT_KILOWATT): AnalogInputType.POWER_KILO_WATTS, + (DEVICE_CLASS_ENERGY, UNIT_KILOWATT_HOURS): AnalogInputType.ENERGY_KILO_WATT_HOURS, + (DEVICE_CLASS_DURATION, UNIT_SECOND): AnalogInputType.TIME_SECONDS, +} diff --git a/esphome/components/zigbee/const_esp32.py b/esphome/components/zigbee/const_esp32.py index 682638439e9..bb507320eb0 100644 --- a/esphome/components/zigbee/const_esp32.py +++ b/esphome/components/zigbee/const_esp32.py @@ -11,6 +11,7 @@ CONF_CLUSTER = "cluster" SCALE = "scale" CONF_ATTRIBUTE_ID = "attribute_id" KEY_BS_EP = "binary_sensor_ep" +KEY_SENSOR_EP = "sensor_ep" ha_standard_devices = cg.esphome_ns.enum("zb_ha_standard_devs_e") DEVICE_ID = { @@ -22,6 +23,7 @@ cluster_id = cg.esphome_ns.enum("esp_zb_zcl_cluster_id_t") CLUSTER_ID = { "BASIC": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BASIC, "BINARY_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT, + "ANALOG_INPUT": cluster_id.ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, } cluster_role = cg.esphome_ns.enum("esp_zb_zcl_cluster_role_t") CLUSTER_ROLE = { @@ -32,4 +34,6 @@ ATTR_TYPE = { "BOOL": attr_type.ESP_ZB_ZCL_ATTR_TYPE_BOOL, "8BITMAP": attr_type.ESP_ZB_ZCL_ATTR_TYPE_8BITMAP, "CHAR_STRING": attr_type.ESP_ZB_ZCL_ATTR_TYPE_CHAR_STRING, + "SINGLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_SINGLE, + "DOUBLE": attr_type.ESP_ZB_ZCL_ATTR_TYPE_DOUBLE, } diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.h b/esphome/components/zigbee/zigbee_attribute_esp32.h index 5a0cfc4fbd7..90a5cf8ff9b 100644 --- a/esphome/components/zigbee/zigbee_attribute_esp32.h +++ b/esphome/components/zigbee/zigbee_attribute_esp32.h @@ -12,6 +12,9 @@ #include "esp_zigbee_core.h" #include "zigbee_esp32.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -42,6 +45,9 @@ class ZigbeeAttribute : public Component { template<typename T> void set_attr(const T &value); uint8_t attr_type() { return attr_type_; } void set_report(bool force); +#ifdef USE_SENSOR + template<typename T> void connect(sensor::Sensor *sensor); +#endif #ifdef USE_BINARY_SENSOR template<typename T> void connect(binary_sensor::BinarySensor *sensor); #endif @@ -78,6 +84,11 @@ template<typename T> void ZigbeeAttribute::set_attr(const T &value) { this->enable_loop(); } +#ifdef USE_SENSOR +template<typename T> void ZigbeeAttribute::connect(sensor::Sensor *sensor) { + sensor->add_on_state_callback([this](float value) { this->set_attr((T) (this->scale_ * value)); }); +} +#endif #ifdef USE_BINARY_SENSOR template<typename T> void ZigbeeAttribute::connect(binary_sensor::BinarySensor *sensor) { sensor->add_on_state_callback([this](bool value) { this->set_attr((T) (this->scale_ * value)); }); diff --git a/esphome/components/zigbee/zigbee_ep_esp32.py b/esphome/components/zigbee/zigbee_ep_esp32.py index 791232d4633..5dd76e99038 100644 --- a/esphome/components/zigbee/zigbee_ep_esp32.py +++ b/esphome/components/zigbee/zigbee_ep_esp32.py @@ -46,6 +46,35 @@ ep_configs: dict[str, dict[str, Any]] = { }, ], }, + "analog_input": { + DEVICE_TYPE: "CUSTOM_ATTR", + CONF_CLUSTERS: [ + { + CONF_ID: "ANALOG_INPUT", + ROLE: CLUSTER_ROLE["SERVER"], + CONF_ATTRIBUTES: [ + { + CONF_ATTRIBUTE_ID: 0x55, + CONF_TYPE: "SINGLE", + CONF_REPORT: REPORT["enable"], + CONF_DEVICE: None, + }, + { + CONF_ATTRIBUTE_ID: 0x51, + CONF_TYPE: "BOOL", + }, + { + CONF_ATTRIBUTE_ID: 0x6F, + CONF_TYPE: "8BITMAP", + }, + { + CONF_ATTRIBUTE_ID: 0x1C, + CONF_TYPE: "CHAR_STRING", + }, + ], + }, + ], + }, } diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index 9081582c7be..99238f758e8 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -16,11 +16,13 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AP, CONF_DEVICE, + CONF_DEVICE_CLASS, CONF_ID, CONF_MAX_LENGTH, CONF_MODEL, CONF_NAME, CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, CONF_WIFI, ) @@ -29,7 +31,16 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.types import ConfigType -from .const import CONF_REPORT, CONF_ROUTER, KEY_ZIGBEE, REPORT, ZigbeeAttribute +from .const import ( + ANALOG_INPUT_APPTYPE, + BACNET_UNIT_NO_UNITS, + BACNET_UNITS, + CONF_REPORT, + CONF_ROUTER, + KEY_ZIGBEE, + REPORT, + ZigbeeAttribute, +) from .const_esp32 import ( ATTR_TYPE, CLUSTER_ID, @@ -40,6 +51,7 @@ from .const_esp32 import ( DEVICE_ID, DEVICE_TYPE, KEY_BS_EP, + KEY_SENSOR_EP, ROLE, SCALE, ) @@ -55,6 +67,10 @@ def get_c_size(bits: str, options: list[int]) -> str: def get_c_type(attr_type: str) -> Any | None: if attr_type == "BOOL": return cg.bool_ + if attr_type == "SINGLE": + return cg.float_ + if attr_type == "DOUBLE": + return cg.double if "STRING" in attr_type: return cg.std_string test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) @@ -66,19 +82,23 @@ def get_c_type(attr_type: str) -> Any | None: def get_cv_by_type(attr_type: str) -> Any | None: if attr_type == "BOOL": return cv.boolean + if attr_type in ["SINGLE", "DOUBLE"]: + return cv.float_ if "STRING" in attr_type: return cv.string test = re.match(r"(^U?)(\d{1,2})(BITMAP$|BIT$|BIT_ENUM$|$)", attr_type) if test and test.group(2): return cv.positive_int - return None + raise cv.Invalid(f"Zigbee: type {attr_type} not supported or implemented") -def get_default_by_type(attr_type: str) -> str | bool | int: +def get_default_by_type(attr_type: str) -> str | bool | int | float: if attr_type == "CHAR_STRING": return "" if attr_type == "BOOL": return False + if attr_type in ["SINGLE", "DOUBLE"]: + return float("nan") return 0 @@ -134,9 +154,8 @@ def final_validate_esp32(config: ConfigType) -> ConfigType: return config -def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType: - ep = copy.deepcopy(ep_configs["binary_input"]) - for cl in ep.get(CONF_CLUSTERS, []): +def setup_attributes(config: ConfigType, clusters: list[dict[str, Any]]) -> None: + for cl in clusters: for attr in cl[CONF_ATTRIBUTES]: if ( attr[CONF_ATTRIBUTE_ID] == 0x1C @@ -159,6 +178,41 @@ def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType: else: attr[CONF_ID] = None validate_attributes(attr) + + +def validate_sensor_esp32(config: ConfigType) -> ConfigType: + ep = copy.deepcopy(ep_configs["analog_input"]) + # get application type from device class and meas unit + # if none get BACNET unit from meas unit + dev_class = config.get(CONF_DEVICE_CLASS) + unit = config.get(CONF_UNIT_OF_MEASUREMENT) + apptype = ANALOG_INPUT_APPTYPE.get((dev_class, unit)) + bacunit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS) + if apptype is not None: + ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append( + { + CONF_ATTRIBUTE_ID: 0x100, + CONF_VALUE: (apptype << 16) | 0xFFFF, + CONF_TYPE: "U32", + }, + ) + ep[CONF_CLUSTERS][0][CONF_ATTRIBUTES].append( + { + CONF_ATTRIBUTE_ID: 0x75, + CONF_VALUE: bacunit, + CONF_TYPE: "16BIT_ENUM", + }, + ) + setup_attributes(config, ep[CONF_CLUSTERS]) + zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) + sensor_ep: list[dict] = zb_data.setdefault(KEY_SENSOR_EP, []) + sensor_ep.append(ep) + return config + + +def validate_binary_sensor_esp32(config: ConfigType) -> ConfigType: + ep = copy.deepcopy(ep_configs["binary_input"]) + setup_attributes(config, ep[CONF_CLUSTERS]) zb_data = CORE.data.setdefault(KEY_ZIGBEE, {}) binary_sensor_ep: list[dict] = zb_data.setdefault(KEY_BS_EP, []) binary_sensor_ep.append(ep) @@ -254,8 +308,9 @@ async def esp32_to_code(config: ConfigType) -> None: # create endpoints zb_data = CORE.data.get(KEY_ZIGBEE, {}) + sensor_ep: list[dict] = zb_data.get(KEY_SENSOR_EP, []) binary_sensor_ep: list[dict] = zb_data.get(KEY_BS_EP, []) - ep_list = create_ep(binary_sensor_ep, config.get(CONF_ROUTER)) + ep_list = create_ep(sensor_ep + binary_sensor_ep, config.get(CONF_ROUTER)) # setup zigbee components var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/zigbee/zigbee_helpers_esp32.c b/esphome/components/zigbee/zigbee_helpers_esp32.c index 4ba71ec6098..5254818df49 100644 --- a/esphome/components/zigbee/zigbee_helpers_esp32.c +++ b/esphome/components/zigbee/zigbee_helpers_esp32.c @@ -33,6 +33,9 @@ esp_err_t esphome_zb_cluster_list_add_or_update_cluster(uint16_t cluster_id, esp case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: ret = esp_zb_cluster_list_add_identify_cluster(cluster_list, attr_list, role_mask); break; + case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: + ret = esp_zb_cluster_list_add_analog_input_cluster(cluster_list, attr_list, role_mask); + break; case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: ret = esp_zb_cluster_list_add_binary_input_cluster(cluster_list, attr_list, role_mask); break; @@ -49,6 +52,8 @@ esp_zb_attribute_list_t *esphome_zb_default_attr_list_create(uint16_t cluster_id return esp_zb_basic_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: return esp_zb_identify_cluster_create(NULL); + case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: + return esp_zb_analog_input_cluster_create(NULL); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: return esp_zb_binary_input_cluster_create(NULL); default: @@ -63,6 +68,8 @@ esp_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, esp_zb_attribute_list return esp_zb_basic_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY: return esp_zb_identify_cluster_add_attr(attr_list, attr_id, value_p); + case ESP_ZB_ZCL_CLUSTER_ID_ANALOG_INPUT: + return esp_zb_analog_input_cluster_add_attr(attr_list, attr_id, value_p); case ESP_ZB_ZCL_CLUSTER_ID_BINARY_INPUT: return esp_zb_binary_input_cluster_add_attr(attr_list, attr_id, value_p); default: diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index b74074e50f8..033511691cc 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -10,35 +10,6 @@ from esphome.const import ( CONF_MODEL, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, - UNIT_AMPERE, - UNIT_CELSIUS, - UNIT_CENTIMETER, - UNIT_DECIBEL, - UNIT_HECTOPASCAL, - UNIT_HERTZ, - UNIT_HOUR, - UNIT_KELVIN, - UNIT_KILOMETER, - UNIT_KILOWATT, - UNIT_KILOWATT_HOURS, - UNIT_LUX, - UNIT_METER, - UNIT_MICROGRAMS_PER_CUBIC_METER, - UNIT_MILLIAMP, - UNIT_MILLIGRAMS_PER_CUBIC_METER, - UNIT_MILLIMETER, - UNIT_MILLISECOND, - UNIT_MILLIVOLT, - UNIT_MINUTE, - UNIT_OHM, - UNIT_PARTS_PER_BILLION, - UNIT_PARTS_PER_MILLION, - UNIT_PASCAL, - UNIT_PERCENT, - UNIT_SECOND, - UNIT_VOLT, - UNIT_WATT, - UNIT_WATT_HOURS, __version__, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -50,6 +21,8 @@ from esphome.cpp_generator import ( from esphome.types import ConfigType from .const import ( + BACNET_UNIT_NO_UNITS, + BACNET_UNITS, CONF_ON_JOIN, CONF_POWER_SOURCE, CONF_ROUTER, @@ -86,41 +59,6 @@ ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component) ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component) ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", cg.Component) -# BACnet engineering units mapping (ZCL uses BACnet unit codes) -# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py -BACNET_UNITS = { - UNIT_CELSIUS: 62, - UNIT_KELVIN: 63, - UNIT_VOLT: 5, - UNIT_MILLIVOLT: 124, - UNIT_AMPERE: 3, - UNIT_MILLIAMP: 2, - UNIT_OHM: 4, - UNIT_WATT: 47, - UNIT_KILOWATT: 48, - UNIT_WATT_HOURS: 18, - UNIT_KILOWATT_HOURS: 19, - UNIT_PASCAL: 53, - UNIT_HECTOPASCAL: 133, - UNIT_HERTZ: 27, - UNIT_MILLIMETER: 30, - UNIT_CENTIMETER: 118, - UNIT_METER: 31, - UNIT_KILOMETER: 193, - UNIT_MILLISECOND: 159, - UNIT_SECOND: 73, - UNIT_MINUTE: 72, - UNIT_HOUR: 71, - UNIT_PARTS_PER_MILLION: 96, - UNIT_PARTS_PER_BILLION: 97, - UNIT_MICROGRAMS_PER_CUBIC_METER: 219, - UNIT_MILLIGRAMS_PER_CUBIC_METER: 218, - UNIT_LUX: 37, - UNIT_DECIBEL: 199, - UNIT_PERCENT: 98, -} -BACNET_UNIT_NO_UNITS = 95 - zephyr_binary_sensor = cv.Schema( { cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent), diff --git a/esphome/const.py b/esphome/const.py index c39225fdec0..a256a10e627 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1240,6 +1240,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LITRE = "L" +UNIT_LITRE_PER_SECOND = "L/s" UNIT_LUX = "lx" UNIT_MEGAJOULE = "MJ" UNIT_METER = "m" diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml index 4494b4081d4..94e3f3c8c04 100644 --- a/tests/components/zigbee/common_esp32.yaml +++ b/tests/components/zigbee/common_esp32.yaml @@ -1,10 +1,10 @@ +packages: + - !include common.yaml + binary_sensor: - platform: template name: "Garage Door Open 10" report: "enable" - - platform: template - name: "Garage Door Open 11" - report: "coordinator" - platform: template name: "Garage Door Open 12" report: "force" From a52ca4f80a29d234183e2a1a8de61bf86075d0e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 11 May 2026 10:51:08 -0500 Subject: [PATCH 1930/2056] [ota] Implement host platform OTA backend with re-exec for integration testing (#16304) --- esphome/components/esphome/ota/__init__.py | 1 + .../components/esphome/ota/ota_esphome.cpp | 10 +- esphome/components/host/core.cpp | 74 +++- esphome/components/host/core.h | 22 ++ esphome/components/host/hal.cpp | 18 +- esphome/components/ota/__init__.py | 9 +- esphome/components/ota/ota_backend_host.cpp | 329 +++++++++++++++++- esphome/components/ota/ota_backend_host.h | 21 +- .../components/socket/bsd_sockets_impl.cpp | 10 +- esphome/core/__init__.py | 3 + .../fixtures/host_ota_rejects_garbage.yaml | 9 + .../fixtures/host_ota_self_update.yaml | 9 + tests/integration/test_host_ota.py | 152 ++++++++ tests/unit_tests/test_core.py | 24 ++ 14 files changed, 664 insertions(+), 27 deletions(-) create mode 100644 esphome/components/host/core.h create mode 100644 tests/integration/fixtures/host_ota_rejects_garbage.yaml create mode 100644 tests/integration/fixtures/host_ota_self_update.yaml create mode 100644 tests/integration/test_host_ota.py diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index ee3b7f0c20d..f7793b1493c 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -130,6 +130,7 @@ CONFIG_SCHEMA = cv.All( bk72xx=8892, ln882x=8820, rtl87xx=8892, + host=8082, ): cv.port, cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean, cv.Optional(CONF_PASSWORD): cv.string, diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 843028fc974..f1857ed6642 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -68,7 +68,7 @@ void ESPHomeOTAComponent::setup() { return; } - err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); + err = this->server_->bind((struct sockaddr *) &server, sl); if (err != 0) { this->server_failed_(LOG_STR("bind")); return; @@ -133,12 +133,12 @@ void ESPHomeOTAComponent::dump_config() { } void ESPHomeOTAComponent::loop() { - // Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select - // listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back - // to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more - // iteration so a connection queued mid-session is still caught here. + // Self-disable idle loop where a wake path re-enables on listener readiness + // (fast-select, raw-TCP accept_fn_). Host BSD select doesn't, so stay enabled. if (this->client_ == nullptr && !this->server_->ready()) { +#ifndef USE_HOST this->disable_loop(); +#endif return; } this->handle_handshake_(); diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 91239758848..9292cd77f61 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,20 +1,92 @@ #ifdef USE_HOST +#include "core.h" + #include "esphome/core/application.h" #include "preferences.h" +#include <climits> #include <csignal> +#include <cstdlib> +#include <string> + +#ifdef __APPLE__ +#include <mach-o/dyld.h> +#endif + +#ifdef __linux__ +#include <unistd.h> +#endif namespace { volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void signal_handler(int signal) { s_signal_received = signal; } + +char **s_argv = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::string *s_exe_path = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +std::string *s_reexec_path = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +std::string resolve_exe_path(const char *argv0) { +#ifdef __linux__ + char buf[PATH_MAX]; + ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (len > 0) { + buf[len] = '\0'; + return std::string(buf); + } +#endif +#ifdef __APPLE__ + char buf[PATH_MAX]; + uint32_t size = sizeof(buf); + if (_NSGetExecutablePath(buf, &size) == 0) { + char real[PATH_MAX]; + if (::realpath(buf, real) != nullptr) + return std::string(real); + return std::string(buf); + } +#endif + if (argv0 == nullptr) + return {}; + char real[PATH_MAX]; + if (::realpath(argv0, real) != nullptr) + return std::string(real); + return std::string(argv0); +} } // namespace +namespace esphome::host { + +char **get_argv() { return s_argv; } + +const std::string &get_exe_path() { + static const std::string empty; + return s_exe_path != nullptr ? *s_exe_path : empty; +} + +void arm_reexec(const std::string &path) { + if (s_reexec_path != nullptr) + *s_reexec_path = path; +} + +const char *get_reexec_path() { + if (s_reexec_path == nullptr || s_reexec_path->empty()) + return nullptr; + return s_reexec_path->c_str(); +} + +} // namespace esphome::host + // HAL functions live in hal.cpp. void setup(); void loop(); -int main() { +int main(int argc, char **argv) { + s_argv = argv; + static std::string exe_path = resolve_exe_path(argc > 0 ? argv[0] : nullptr); + s_exe_path = &exe_path; + static std::string reexec_path; + s_reexec_path = &reexec_path; + // Install signal handlers for graceful shutdown (flushes preferences to disk) std::signal(SIGINT, signal_handler); std::signal(SIGTERM, signal_handler); diff --git a/esphome/components/host/core.h b/esphome/components/host/core.h new file mode 100644 index 00000000000..ab64119415b --- /dev/null +++ b/esphome/components/host/core.h @@ -0,0 +1,22 @@ +#pragma once +#ifdef USE_HOST + +#include <string> + +namespace esphome::host { + +/// argv captured by main(); stable for process lifetime. +char **get_argv(); + +/// Absolute path to running exe (resolved at startup); empty on failure. +const std::string &get_exe_path(); + +/// Arm an execv on the next arch_restart(). Pass empty to disarm. +void arm_reexec(const std::string &path); + +/// Armed re-exec path, or nullptr. +const char *get_reexec_path(); + +} // namespace esphome::host + +#endif // USE_HOST diff --git a/esphome/components/host/hal.cpp b/esphome/components/host/hal.cpp index c7fef8d2e86..9108c1ea9d3 100644 --- a/esphome/components/host/hal.cpp +++ b/esphome/components/host/hal.cpp @@ -2,10 +2,14 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "core.h" #include <time.h> +#include <unistd.h> #include <cerrno> #include <cstdlib> +#include <cstring> // Empty host namespace block to satisfy ci-custom's lint_namespace check. // HAL functions live in namespace esphome (root) — they are not part of the @@ -50,7 +54,19 @@ void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { res = nanosleep(&ts, &ts); } while (res != 0 && errno == EINTR); } -void arch_restart() { exit(0); } +void arch_restart() { + // Host OTA: if a re-exec is armed, swap binaries instead of exiting. + if (const char *target = host::get_reexec_path()) { + char **argv = host::get_argv(); + if (argv != nullptr) { + execv(target, argv); + // execv only returns on failure. + ESP_LOGE("host", "execv('%s') failed: %s", target, std::strerror(errno)); + exit(1); + } + } + exit(0); +} uint32_t arch_get_cpu_cycle_count() { struct timespec spec; diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 579491fe1a3..83d8c611d5e 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,5 +1,3 @@ -import logging - from esphome import automation import esphome.codegen as cg from esphome.config_helpers import filter_source_files_from_platform @@ -38,8 +36,6 @@ CONF_ON_PROGRESS = "on_progress" CONF_ON_STATE_CHANGE = "on_state_change" -_LOGGER = logging.getLogger(__name__) - ota_ns = cg.esphome_ns.namespace("ota") OTAComponent = ota_ns.class_("OTAComponent", cg.Component) OTAState = ota_ns.enum("OTAState") @@ -58,10 +54,6 @@ def _ota_final_validate(config): raise cv.Invalid( f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality" ) - if CORE.is_host: - _LOGGER.warning( - "OTA not available for platform 'host'. OTA functionality disabled." - ) FINAL_VALIDATE_SCHEMA = _ota_final_validate @@ -172,5 +164,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "ota_backend_host.cpp": {PlatformFramework.HOST_NATIVE}, } ) diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp index a2c9f2cc33a..ee503a49e16 100644 --- a/esphome/components/ota/ota_backend_host.cpp +++ b/esphome/components/ota/ota_backend_host.cpp @@ -1,26 +1,339 @@ #ifdef USE_HOST #include "ota_backend_host.h" -#include "esphome/core/defines.h" +#include "esphome/components/host/core.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include <cerrno> +#include <cstdint> +#include <cstdio> +#include <cstring> + +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef __linux__ +#include <elf.h> +#include <endian.h> +#endif + +#ifdef __APPLE__ +#include <mach-o/loader.h> +#endif namespace esphome::ota { -// Stub implementation - OTA is not supported on host platform. -// All methods return error codes to allow compilation of configs with OTA triggers. +namespace { + +const char *const TAG = "ota.host"; + +constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB +constexpr size_t HEADER_PEEK_SIZE = 64; + +ssize_t read_header_(const char *path, uint8_t *buf, size_t len) { + int fd = ::open(path, O_RDONLY); + if (fd < 0) + return -1; + ssize_t got = ::read(fd, buf, len); + ::close(fd); + return got; +} + +#ifdef __linux__ +struct ElfIdent { + bool valid; + uint8_t ei_class; + uint8_t ei_data; + uint16_t e_machine; + uint16_t e_type; +}; + +ElfIdent parse_elf_(const uint8_t *buf, size_t len) { + ElfIdent out{}; + if (len < EI_NIDENT + 4) + return out; + if (buf[EI_MAG0] != ELFMAG0 || buf[EI_MAG1] != ELFMAG1 || buf[EI_MAG2] != ELFMAG2 || buf[EI_MAG3] != ELFMAG3) + return out; + out.ei_class = buf[EI_CLASS]; + out.ei_data = buf[EI_DATA]; + // e_type @ 16, e_machine @ 18, both in EI_DATA endianness. + uint16_t e_type; + uint16_t e_machine; + std::memcpy(&e_type, buf + 16, sizeof(e_type)); + std::memcpy(&e_machine, buf + 18, sizeof(e_machine)); + if (out.ei_data == ELFDATA2LSB) { + out.e_type = le16toh(e_type); + out.e_machine = le16toh(e_machine); + } else if (out.ei_data == ELFDATA2MSB) { + out.e_type = be16toh(e_type); + out.e_machine = be16toh(e_machine); + } else { + return out; + } + out.valid = true; + return out; +} + +bool validate_elf_(const char *staging_path, const std::string &exe_path) { + uint8_t new_buf[HEADER_PEEK_SIZE]; + uint8_t cur_buf[HEADER_PEEK_SIZE]; + ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf)); + ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf)); + if (new_n < static_cast<ssize_t>(EI_NIDENT + 4) || cur_n < static_cast<ssize_t>(EI_NIDENT + 4)) { + ESP_LOGE(TAG, "ELF header read failed"); + return false; + } + ElfIdent new_id = parse_elf_(new_buf, new_n); + ElfIdent cur_id = parse_elf_(cur_buf, cur_n); + if (!new_id.valid) { + ESP_LOGE(TAG, "Uploaded payload is not a valid ELF"); + return false; + } + if (!cur_id.valid) { + ESP_LOGE(TAG, "Could not parse running exe ELF header"); + return false; + } + if (new_id.ei_class != cur_id.ei_class) { + ESP_LOGE(TAG, "ELF class mismatch (uploaded=%u, running=%u)", new_id.ei_class, cur_id.ei_class); + return false; + } + if (new_id.ei_data != cur_id.ei_data) { + ESP_LOGE(TAG, "ELF endianness mismatch"); + return false; + } + if (new_id.e_machine != cur_id.e_machine) { + ESP_LOGE(TAG, "ELF e_machine mismatch (uploaded=0x%04x, running=0x%04x)", new_id.e_machine, cur_id.e_machine); + return false; + } + if (new_id.e_type != ET_EXEC && new_id.e_type != ET_DYN) { + ESP_LOGE(TAG, "ELF e_type=%u is not executable", new_id.e_type); + return false; + } + return true; +} +#endif // __linux__ + +#ifdef __APPLE__ +struct MachOIdent { + bool valid; + uint32_t cputype; + uint32_t cpusubtype; +}; + +MachOIdent parse_macho_(const uint8_t *buf, size_t len) { + MachOIdent out{}; + // mach_header is the common prefix of mach_header and mach_header_64; + // cputype/cpusubtype/filetype have identical offsets in both. + if (len < sizeof(struct mach_header)) + return out; + uint32_t magic; + std::memcpy(&magic, buf, sizeof(magic)); + bool swap; + if (magic == MH_MAGIC || magic == MH_MAGIC_64) { + swap = false; + } else if (magic == MH_CIGAM || magic == MH_CIGAM_64) { + swap = true; + } else { + return out; + } + struct mach_header hdr; + std::memcpy(&hdr, buf, sizeof(hdr)); + if (swap) { + hdr.cputype = OSSwapInt32(hdr.cputype); + hdr.cpusubtype = OSSwapInt32(hdr.cpusubtype); + hdr.filetype = OSSwapInt32(hdr.filetype); + } + if (hdr.filetype != MH_EXECUTE) + return out; + out.cputype = hdr.cputype; + out.cpusubtype = hdr.cpusubtype; + out.valid = true; + return out; +} + +bool validate_macho_(const char *staging_path, const std::string &exe_path) { + uint8_t new_buf[HEADER_PEEK_SIZE]; + uint8_t cur_buf[HEADER_PEEK_SIZE]; + ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf)); + ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf)); + if (new_n < static_cast<ssize_t>(sizeof(struct mach_header)) || + cur_n < static_cast<ssize_t>(sizeof(struct mach_header))) { + ESP_LOGE(TAG, "Mach-O header read failed"); + return false; + } + MachOIdent new_id = parse_macho_(new_buf, new_n); + MachOIdent cur_id = parse_macho_(cur_buf, cur_n); + if (!new_id.valid) { + ESP_LOGE(TAG, "Uploaded payload is not a valid thin Mach-O executable"); + return false; + } + if (!cur_id.valid) { + ESP_LOGE(TAG, "Could not parse running exe Mach-O header"); + return false; + } + if (new_id.cputype != cur_id.cputype || new_id.cpusubtype != cur_id.cpusubtype) { + ESP_LOGE(TAG, "Mach-O arch mismatch (uploaded=0x%x/0x%x, running=0x%x/0x%x)", new_id.cputype, new_id.cpusubtype, + cur_id.cputype, cur_id.cpusubtype); + return false; + } + return true; +} +#endif // __APPLE__ + +bool validate_executable_(const char *staging_path, const std::string &exe_path) { +#ifdef __linux__ + return validate_elf_(staging_path, exe_path); +#elif defined(__APPLE__) + return validate_macho_(staging_path, exe_path); +#else + (void) staging_path; + (void) exe_path; + ESP_LOGE(TAG, "Host OTA validation not implemented for this OS"); + return false; +#endif +} + +} // namespace std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); } OTAResponseTypes HostOTABackend::begin(size_t image_size, OTAType ota_type) { - return OTA_RESPONSE_ERROR_UPDATE_PREPARE; + if (ota_type != OTA_TYPE_UPDATE_APP) + return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; + // 0 = unknown size (web_server multipart); cap at MAX_OTA_SIZE. + if (image_size > MAX_OTA_SIZE) { + ESP_LOGE(TAG, "Refusing OTA of size %zu (exceeds %zu)", image_size, MAX_OTA_SIZE); + return OTA_RESPONSE_ERROR_UPDATE_PREPARE; + } + + const std::string &exe = host::get_exe_path(); + if (exe.empty()) { + ESP_LOGE(TAG, "Could not resolve running executable path; cannot stage OTA"); + return OTA_RESPONSE_ERROR_UPDATE_PREPARE; + } + this->final_path_ = exe; + this->staging_path_ = exe + ".ota.new"; + + // Clean up any leftover from a prior aborted OTA. + ::unlink(this->staging_path_.c_str()); + + this->fd_ = ::open(this->staging_path_.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0755); + if (this->fd_ < 0) { + ESP_LOGE(TAG, "Open '%s' failed: %s", this->staging_path_.c_str(), std::strerror(errno)); + return OTA_RESPONSE_ERROR_UPDATE_PREPARE; + } + + this->expected_size_ = image_size; + this->bytes_written_ = 0; + this->md5_set_ = false; + this->md5_.init(); + + ESP_LOGD(TAG, "OTA begin: staging=%s, size=%zu", this->staging_path_.c_str(), image_size); + return OTA_RESPONSE_OK; } -void HostOTABackend::set_update_md5(const char *expected_md5) {} +void HostOTABackend::set_update_md5(const char *md5) { + if (parse_hex(md5, this->expected_md5_, 16)) + this->md5_set_ = true; +} -OTAResponseTypes HostOTABackend::write(uint8_t *data, size_t len) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } +OTAResponseTypes HostOTABackend::write(uint8_t *data, size_t len) { + if (this->fd_ < 0) + return OTA_RESPONSE_ERROR_WRITING_FLASH; + size_t limit = this->expected_size_ != 0 ? this->expected_size_ : MAX_OTA_SIZE; + if (this->bytes_written_ + len > limit) { + ESP_LOGE(TAG, "Write past size limit (%zu)", limit); + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } -OTAResponseTypes HostOTABackend::end() { return OTA_RESPONSE_ERROR_UPDATE_END; } + size_t remaining = len; + const uint8_t *p = data; + while (remaining > 0) { + ssize_t n = ::write(this->fd_, p, remaining); + if (n < 0) { + if (errno == EINTR) + continue; + ESP_LOGE(TAG, "Write failed: %s", std::strerror(errno)); + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + p += n; + remaining -= n; + } + this->md5_.add(data, len); + this->bytes_written_ += len; + return OTA_RESPONSE_OK; +} -void HostOTABackend::abort() {} +OTAResponseTypes HostOTABackend::end() { + if (this->fd_ < 0) + return OTA_RESPONSE_ERROR_UPDATE_END; + + if (this->bytes_written_ == 0) { + ESP_LOGE(TAG, "OTA ended with no data written"); + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + if (this->expected_size_ != 0 && this->bytes_written_ != this->expected_size_) { + ESP_LOGE(TAG, "Size mismatch: got %zu, expected %zu", this->bytes_written_, this->expected_size_); + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_bytes(this->expected_md5_)) { + ESP_LOGE(TAG, "MD5 mismatch"); + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } + } + + if (::fsync(this->fd_) != 0) { + ESP_LOGW(TAG, "fsync failed: %s", std::strerror(errno)); + } + ::close(this->fd_); + this->fd_ = -1; + + if (!validate_executable_(this->staging_path_.c_str(), this->final_path_)) { + ::unlink(this->staging_path_.c_str()); + this->staging_path_.clear(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + if (::chmod(this->staging_path_.c_str(), 0755) != 0) { + ESP_LOGW(TAG, "chmod failed: %s", std::strerror(errno)); + } + + if (::rename(this->staging_path_.c_str(), this->final_path_.c_str()) != 0) { + ESP_LOGE(TAG, "rename '%s' -> '%s' failed: %s", this->staging_path_.c_str(), this->final_path_.c_str(), + std::strerror(errno)); + ::unlink(this->staging_path_.c_str()); + this->staging_path_.clear(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // arch_restart() (via App::safe_reboot) will execv this path with the original argv. + host::arm_reexec(this->final_path_); + this->staging_path_.clear(); + ESP_LOGI(TAG, "OTA staged at %s; will re-exec on reboot", this->final_path_.c_str()); + return OTA_RESPONSE_OK; +} + +void HostOTABackend::abort() { + if (this->fd_ >= 0) { + ::close(this->fd_); + this->fd_ = -1; + } + if (!this->staging_path_.empty()) { + ::unlink(this->staging_path_.c_str()); + this->staging_path_.clear(); + } + this->expected_size_ = 0; + this->bytes_written_ = 0; + this->md5_set_ = false; +} } // namespace esphome::ota #endif diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h index 4451fdfe182..51ffdaeda3e 100644 --- a/esphome/components/ota/ota_backend_host.h +++ b/esphome/components/ota/ota_backend_host.h @@ -2,11 +2,16 @@ #ifdef USE_HOST #include "ota_backend.h" +#include "esphome/components/md5/md5.h" + +#include <cstddef> +#include <cstdint> +#include <string> + namespace esphome::ota { -/// Stub OTA backend for host platform - allows compilation but does not implement OTA. -/// All operations return error codes immediately. This enables configurations with -/// OTA triggers to compile for host platform during development. +/// Host OTA backend: stages new binary to `<exe>.ota.new`, validates ELF/Mach-O +/// matches the running arch, renames over `<exe>`, and arms execv via arch_restart(). class HostOTABackend final { public: OTAResponseTypes begin(size_t image_size, OTAType ota_type = OTA_TYPE_UPDATE_APP); @@ -15,6 +20,16 @@ class HostOTABackend final { OTAResponseTypes end(); void abort(); bool supports_compression() { return false; } + + protected: + md5::MD5Digest md5_{}; + std::string staging_path_; + std::string final_path_; + size_t expected_size_{0}; + size_t bytes_written_{0}; + uint8_t expected_md5_[16]{}; + int fd_{-1}; + bool md5_set_{false}; }; std::unique_ptr<HostOTABackend> make_ota_backend(); diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 8e9968e05c0..ee22e4b97b2 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -14,7 +14,15 @@ namespace esphome::socket { BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) { this->fd_ = fd; - if (!monitor_loop || this->fd_ < 0) + if (this->fd_ < 0) + return; +#ifdef USE_HOST + // Release listening ports on OTA re-exec. + int flags = ::fcntl(this->fd_, F_GETFD, 0); + if (flags >= 0) + ::fcntl(this->fd_, F_SETFD, flags | FD_CLOEXEC); +#endif + if (!monitor_loop) return; #ifdef USE_LWIP_FAST_SELECT this->cached_sock_ = hook_fd_for_fast_select(this->fd_); diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index ef0eddc603a..e13d5668afc 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -782,6 +782,9 @@ class EsphomeCore: return self.relative_build_path("build", f"{self.name}.bin") if self.is_libretiny: return self.relative_pioenvs_path(self.name, "firmware.uf2") + if self.is_host: + # Host builds produce a native ELF/Mach-O named `program`. + return self.relative_pioenvs_path(self.name, "program") return self.relative_pioenvs_path(self.name, "firmware.bin") @property diff --git a/tests/integration/fixtures/host_ota_rejects_garbage.yaml b/tests/integration/fixtures/host_ota_rejects_garbage.yaml new file mode 100644 index 00000000000..ebf7977123d --- /dev/null +++ b/tests/integration/fixtures/host_ota_rejects_garbage.yaml @@ -0,0 +1,9 @@ +esphome: + name: host-ota-test +host: +api: +ota: + - platform: esphome + port: __OTA_PORT__ +logger: + level: DEBUG diff --git a/tests/integration/fixtures/host_ota_self_update.yaml b/tests/integration/fixtures/host_ota_self_update.yaml new file mode 100644 index 00000000000..ebf7977123d --- /dev/null +++ b/tests/integration/fixtures/host_ota_self_update.yaml @@ -0,0 +1,9 @@ +esphome: + name: host-ota-test +host: +api: +ota: + - platform: esphome + port: __OTA_PORT__ +logger: + level: DEBUG diff --git a/tests/integration/test_host_ota.py b/tests/integration/test_host_ota.py new file mode 100644 index 00000000000..e1036fdf1cf --- /dev/null +++ b/tests/integration/test_host_ota.py @@ -0,0 +1,152 @@ +"""End-to-end OTA tests on the host platform. + +Exercises the native OTA protocol against a real host binary, then asserts +pid is preserved across the post-OTA execv. A second OTA on the post-exec +instance covers the FD_CLOEXEC path. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Generator +from contextlib import contextmanager +import socket + +import pytest + +from esphome import espota2 + +from .conftest import run_binary, wait_and_connect_api_client +from .const import LOCALHOST, PORT_POLL_INTERVAL, PORT_WAIT_TIMEOUT +from .types import CompileFunction, ConfigWriter + +DEVICE_NAME = "host-ota-test" + + +@contextmanager +def _reserve_port() -> Generator[tuple[int, socket.socket]]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("", 0)) + try: + yield s.getsockname()[1], s + finally: + s.close() + + +async def _wait_for_port(host: str, port: int, timeout: float) -> None: + """Poll until a TCP port accepts connections, or raise TimeoutError.""" + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while loop.time() < deadline: + try: + _, writer = await asyncio.open_connection(host, port) + except (ConnectionRefusedError, OSError): + await asyncio.sleep(PORT_POLL_INTERVAL) + continue + writer.close() + await writer.wait_closed() + return + raise TimeoutError(f"Port {port} on {host} did not open within {timeout}s") + + +@pytest.mark.asyncio +async def test_host_ota_self_update( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> None: + """Self-OTA: upload the running binary back to itself, expect re-exec.""" + api_port, api_socket = reserved_tcp_port + with _reserve_port() as (ota_port, ota_socket): + yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + api_socket.close() + ota_socket.close() + + loop = asyncio.get_running_loop() + ota_staged = loop.create_future() + rebooted = loop.create_future() + + def on_log(line: str) -> None: + if not ota_staged.done() and "OTA staged at" in line: + ota_staged.set_result(True) + if not rebooted.done() and "Rebooting safely" in line: + rebooted.set_result(True) + + async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + pid_before = proc.pid + async with wait_and_connect_api_client(port=api_port) as client: + info_before = await client.device_info() + assert info_before.name == DEVICE_NAME + + # espota2 is blocking; run in executor. + rc, _ = await loop.run_in_executor( + None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path + ) + assert rc == 0, "espota2 reported failure" + + await asyncio.wait_for(ota_staged, timeout=10.0) + await asyncio.wait_for(rebooted, timeout=10.0) + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + + # execv preserves pid; mismatch means external respawn. + assert proc.returncode is None, "process exited instead of execing" + assert proc.pid == pid_before + + async with wait_and_connect_api_client(port=api_port) as client: + info_after = await client.device_info() + assert info_after.name == DEVICE_NAME + assert info_after.name == info_before.name + + # Second OTA: catches FD_CLOEXEC regressions (EADDRINUSE on rebind). + rc, _ = await loop.run_in_executor( + None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path + ) + assert rc == 0, "second OTA failed -- listener leaked across execv" + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + assert proc.pid == pid_before + + +@pytest.mark.asyncio +async def test_host_ota_rejects_garbage( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], + integration_test_dir, +) -> None: + """Bogus payload is rejected and the device keeps running.""" + api_port, api_socket = reserved_tcp_port + with _reserve_port() as (ota_port, ota_socket): + yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + + # 192 bytes that are neither ELF nor Mach-O. + bogus_path = integration_test_dir / "bogus.bin" + bogus_path.write_bytes(b"NOT-AN-EXECUTABLE-AT-ALL" * 8) + + api_socket.close() + ota_socket.close() + + async with run_binary(binary_path) as (proc, _lines): + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + pid_before = proc.pid + + loop = asyncio.get_running_loop() + rc, _ = await loop.run_in_executor( + None, espota2.run_ota, LOCALHOST, ota_port, None, bogus_path + ) + assert rc == 1 + + await asyncio.sleep(0.5) + assert proc.returncode is None, "process died on rejected OTA" + assert proc.pid == pid_before + + async with wait_and_connect_api_client(port=api_port) as client: + info = await client.device_info() + assert info.name == DEVICE_NAME diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1a52e6b29ed..2322fdd014f 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -591,6 +591,30 @@ class TestEsphomeCore: assert target.is_esp32 is False assert target.is_esp8266 is True + def test_firmware_bin__default(self, target): + """Default platforms produce <pioenvs>/<name>/firmware.bin.""" + target.name = "test-device" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "esp32"} + assert target.firmware_bin == Path( + "foo/build/.pioenvs/test-device/firmware.bin" + ) + + def test_firmware_bin__libretiny(self, target): + """The libretiny platform produces firmware.uf2.""" + target.name = "test-device" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "bk72xx"} + assert target.firmware_bin == Path( + "foo/build/.pioenvs/test-device/firmware.uf2" + ) + + def test_firmware_bin__host(self, target): + """Host platform produces a native ELF/Mach-O named `program`, + not firmware.bin -- needed for `esphome upload` to find the + right artifact for the host OTA backend.""" + target.name = "test-device" + target.data[const.KEY_CORE] = {const.KEY_TARGET_PLATFORM: "host"} + assert target.firmware_bin == Path("foo/build/.pioenvs/test-device/program") + @pytest.mark.skipif(os.name == "nt", reason="Unix-specific test") def test_data_dir_default_unix(self, target): """Test data_dir returns .esphome in config directory by default on Unix.""" From ee8ca2a3bf77ab3ba8dd2e7e09bfc80060d60345 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Mon, 11 May 2026 17:54:35 +0200 Subject: [PATCH 1931/2056] [zigbee] add on_join trigger for esp32 (#16060) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 17 +++++++++++------ .../zigbee/time/zigbee_time_zephyr.cpp | 2 +- esphome/components/zigbee/zigbee_esp32.cpp | 16 ++++++++++++++-- esphome/components/zigbee/zigbee_esp32.h | 10 ++++++++-- esphome/components/zigbee/zigbee_esp32.py | 4 +++- esphome/components/zigbee/zigbee_zephyr.cpp | 11 +++++------ esphome/components/zigbee/zigbee_zephyr.h | 8 +++----- esphome/components/zigbee/zigbee_zephyr.py | 9 +++------ tests/components/zigbee/common_esp32.yaml | 3 +++ 9 files changed, 51 insertions(+), 29 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index a7e9d1096f7..286b395e183 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -79,10 +79,7 @@ CONFIG_SCHEMA = cv.All( cv.string, cv.Length(max=31) ), cv.Optional(CONF_ROUTER, default=False): cv.boolean, - cv.Optional(CONF_ON_JOIN): cv.All( - cv.requires_component("nrf52"), - automation.validate_automation(single=True), - ), + cv.Optional(CONF_ON_JOIN): automation.validate_automation({}), cv.OnlyWith(CONF_WIPE_ON_BOOT, "nrf52", default=False): cv.All( cv.Any( cv.boolean, @@ -146,17 +143,25 @@ FINAL_VALIDATE_SCHEMA = cv.All( ) +_CALLBACK_AUTOMATIONS = [ + automation.CallbackAutomation(CONF_ON_JOIN, "add_on_join_callback", [(bool, "x")]), +] + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_define("USE_ZIGBEE") + var = None if CORE.using_zephyr: from .zigbee_zephyr import zephyr_to_code - await zephyr_to_code(config) + var = await zephyr_to_code(config) if CORE.is_esp32: from .zigbee_esp32 import esp32_to_code - await esp32_to_code(config) + var = await esp32_to_code(config) + if var is not None: + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp index 70ceb60abee..92d238629a1 100644 --- a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp @@ -26,7 +26,7 @@ void ZigbeeTime::setup() { global_time = this; this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); synchronize_epoch_(EPOCH_2000); - this->parent_->add_join_callback([this]() { zb_zcl_time_server_synchronize(this->endpoint_, sync_time); }); + this->parent_->add_on_join_callback([this](bool x) { zb_zcl_time_server_synchronize(this->endpoint_, sync_time); }); } void ZigbeeTime::dump_config() { diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index c16736236a4..733f45cc2ae 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -59,11 +59,13 @@ void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) { ESP_LOGD(TAG, "Device started up in %sfactory-reset mode", esp_zb_bdb_is_factory_new() ? "" : "non "); global_zigbee->started = true; if (esp_zb_bdb_is_factory_new()) { + global_zigbee->factory_new = true; ESP_LOGD(TAG, "Start network steering"); esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING); } else { ESP_LOGD(TAG, "Device rebooted"); - global_zigbee->connected = true; + global_zigbee->joined = true; + global_zigbee->enable_loop_soon_any_context(); } } else { ESP_LOGE(TAG, "FIRST_START. Device started up in %sfactory-reset mode with an error %d (%s)", @@ -78,7 +80,8 @@ void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct) { steering_retry_count = 0; ESP_LOGI(TAG, "Joined network successfully (PAN ID: 0x%04hx, Channel:%d)", esp_zb_get_pan_id(), esp_zb_get_current_channel()); - global_zigbee->connected = true; + global_zigbee->joined = true; + global_zigbee->enable_loop_soon_any_context(); } else { ESP_LOGI(TAG, "Network steering was not successful (status: %s)", esp_err_to_name(err_status)); if (steering_retry_count < 10) { @@ -283,6 +286,15 @@ void ZigbeeComponent::setup() { } } xTaskCreate(esp_zb_task_, "Zigbee_main", 4096, NULL, 24, NULL); + this->disable_loop(); // loop is only needed for processing events, so disable until we join a network +} + +void ZigbeeComponent::loop() { + if (this->joined.exchange(false)) { + this->connected_ = true; + this->join_cb_.call(this->factory_new); + } + this->disable_loop(); } void ZigbeeComponent::dump_config() { diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h index 80ecbfd639e..a9072f6c8d4 100644 --- a/esphome/components/zigbee/zigbee_esp32.h +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -39,6 +39,7 @@ class ZigbeeAttribute; class ZigbeeComponent : public Component { public: void setup() override; + void loop() override; void dump_config() override; esp_err_t create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id, esp_zb_cluster_list_t *esp_zb_cluster_list); @@ -59,10 +60,13 @@ class ZigbeeComponent : public Component { esp_zb_lock_release(); } + template<typename F> void add_on_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } + bool is_started() { return this->started; } - bool is_connected() { return this->connected; } - std::atomic<bool> connected = false; + bool is_connected() { return this->connected_; } std::atomic<bool> started = false; + std::atomic<bool> joined = false; + std::atomic<bool> factory_new = false; protected: struct { @@ -70,6 +74,7 @@ class ZigbeeComponent : public Component { uint8_t *manufacturer; uint8_t *date; } basic_cluster_data_; + bool connected_ = false; #ifdef ZB_ED_ROLE esp_zb_nwk_device_type_t device_role_ = ESP_ZB_DEVICE_TYPE_ED; #else @@ -89,6 +94,7 @@ class ZigbeeComponent : public Component { // key tuple could be replaced by single 64 (48) bit int with bit fields for endpoint, cluster, role and attr_id std::map<std::tuple<uint8_t, uint16_t, uint8_t, uint16_t>, ZigbeeAttribute *> attributes_; esp_zb_ep_list_t *esp_zb_ep_list_ = esp_zb_ep_list_create(); + CallbackManager<void(bool)> join_cb_{}; }; extern "C" void esp_zb_app_signal_handler(esp_zb_app_signal_t *signal_struct); diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index 99238f758e8..5b1808ea60d 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -28,6 +28,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.coroutine import CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv from esphome.types import ConfigType @@ -289,7 +290,7 @@ async def attributes_to_code( cg.add(attr_var.connect(template_arg, device)) -async def esp32_to_code(config: ConfigType) -> None: +async def esp32_to_code(config: ConfigType) -> "MockObj": add_idf_component( name="espressif/esp-zboss-lib", ref="1.6.4", @@ -332,3 +333,4 @@ async def esp32_to_code(config: ConfigType) -> None: ) ) await attributes_to_code(var, ep[CONF_NUM], cl) + return var diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index 26bef8fb174..81aad7dcb17 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -40,7 +40,7 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) { case ZB_BDB_SIGNAL_DEVICE_REBOOT: ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_REBOOT, status: %d", status); if (status == RET_OK) { - on_join_(); + on_join_(false); } break; case ZB_BDB_SIGNAL_STEERING: @@ -88,7 +88,7 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) { for (int i = 0; i < addr_len; ++i) { if (ieee_addr_buf[i] != '0') { - on_join_(); + on_join_(true); break; } } @@ -130,11 +130,10 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { p_device_cb_param->status = RET_NOT_IMPLEMENTED; } -void ZigbeeComponent::on_join_() { - this->defer([this]() { +void ZigbeeComponent::on_join_(bool factory_new) { + this->defer([this, factory_new]() { ESP_LOGD(TAG, "Joined the network"); - this->join_trigger_.trigger(); - this->join_cb_.call(); + this->join_cb_.call(factory_new); }); } diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 0a189ac1e04..d462d2a4031 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -74,25 +74,23 @@ class ZigbeeComponent : public Component { // endpoints are enumerated from 1 this->callbacks_[endpoint - 1] = std::move(cb); } - template<typename F> void add_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } + template<typename F> void add_on_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } void zboss_signal_handler_esphome(zb_bufid_t bufid); void after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, zb_zcl_attr_addr_info_t *attr_addr_info); void factory_reset(); - Trigger<> *get_join_trigger() { return &this->join_trigger_; }; void force_report(); void loop() override; void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; } protected: static void zcl_device_cb(zb_bufid_t bufid); - void on_join_(); + void on_join_(bool factory_new); #ifdef USE_ZIGBEE_WIPE_ON_BOOT void erase_flash_(int area); #endif void dump_reporting_(); std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{}; - CallbackManager<void()> join_cb_; - Trigger<> join_trigger_; + CallbackManager<void(bool)> join_cb_; bool force_report_{false}; uint32_t sleep_time_{}; uint32_t sleep_remainder_{}; diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 033511691cc..aa16bbef538 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -1,7 +1,6 @@ import datetime import random -from esphome import automation import esphome.codegen as cg from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv @@ -23,7 +22,6 @@ from esphome.types import ConfigType from .const import ( BACNET_UNIT_NO_UNITS, BACNET_UNITS, - CONF_ON_JOIN, CONF_POWER_SOURCE, CONF_ROUTER, CONF_WIPE_ON_BOOT, @@ -96,7 +94,7 @@ zephyr_number = cv.Schema( ) -async def zephyr_to_code(config: ConfigType) -> None: +async def zephyr_to_code(config: ConfigType) -> "MockObj": zephyr_add_prj_conf("ZIGBEE", True) zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True) if config[CONF_ROUTER]: @@ -141,15 +139,14 @@ async def zephyr_to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) - if on_join_config := config.get(CONF_ON_JOIN): - await automation.build_automation(var.get_join_trigger(), [], on_join_config) - await cg.register_component(var, config) CORE.add_job(_ctx_to_code, config) cg.add(var.set_sleepy(config[CONF_SLEEPY])) + return var + async def _attr_to_code(config: ConfigType) -> None: # Create the basic attributes structure and attribute list diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml index 94e3f3c8c04..77e202b5235 100644 --- a/tests/components/zigbee/common_esp32.yaml +++ b/tests/components/zigbee/common_esp32.yaml @@ -12,3 +12,6 @@ binary_sensor: zigbee: model: zigbee_test router: true + on_join: + then: + - logger.log: "Joined network" From e479e8b6414464674c8dc0637e192fbd045b88e4 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Mon, 11 May 2026 20:08:52 +0200 Subject: [PATCH 1932/2056] [zigbee] Add power_source option to esp32 (#16062) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 5 ++--- esphome/components/zigbee/zigbee_esp32.cpp | 12 +++++++++--- esphome/components/zigbee/zigbee_esp32.h | 4 +++- esphome/components/zigbee/zigbee_esp32.py | 3 +++ tests/components/zigbee/common_esp32.yaml | 1 + 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 286b395e183..69e3fe9c5ae 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -87,9 +87,8 @@ CONFIG_SCHEMA = cv.All( ), cv.requires_component("nrf52"), ), - cv.OnlyWith(CONF_POWER_SOURCE, "nrf52", default="DC_SOURCE"): cv.All( - cv.enum(POWER_SOURCE, upper=True), - cv.requires_component("nrf52"), + cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum( + POWER_SOURCE, upper=True ), cv.Optional(CONF_IEEE802154_VENDOR_OUI): cv.All( cv.Any( diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index 733f45cc2ae..c534a384320 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -153,7 +153,7 @@ void ZigbeeComponent::add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint this->attribute_list_[{endpoint_id, cluster_id, role}] = attr_list; } -void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer) { +void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source) { char date_buf[16]; time_t time_val = App.get_build_time(); struct tm *timeinfo = localtime(&time_val); @@ -162,13 +162,14 @@ void ZigbeeComponent::set_basic_cluster(const char *model, const char *manufactu .model = get_zcl_string(model, 31), .manufacturer = get_zcl_string(manufacturer, 31), .date = get_zcl_string(date_buf, 15), + .power_source = power_source, }; } esp_zb_attribute_list_t *ZigbeeComponent::create_basic_cluster_() { esp_zb_basic_cluster_cfg_t basic_cluster_cfg = { .zcl_version = ESP_ZB_ZCL_BASIC_ZCL_VERSION_DEFAULT_VALUE, - .power_source = 0, + .power_source = this->basic_cluster_data_.power_source, }; esp_zb_attribute_list_t *attr_list = esp_zb_basic_cluster_create(&basic_cluster_cfg); esp_zb_basic_cluster_add_attr(attr_list, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, @@ -192,7 +193,12 @@ static void esp_zb_task_(void *pvParameters) { ESP_LOGE(TAG, "Could not setup Zigbee"); vTaskDelete(NULL); } - esp_zb_set_node_descriptor_power_source(1); + if (global_zigbee->is_battery_powered()) { + ESP_LOGD(TAG, "Battery powered!"); + esp_zb_set_node_descriptor_power_source(0); + } else { + esp_zb_set_node_descriptor_power_source(1); + } esp_zb_stack_main_loop(); } diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h index a9072f6c8d4..03d3286ab82 100644 --- a/esphome/components/zigbee/zigbee_esp32.h +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -43,7 +43,7 @@ class ZigbeeComponent : public Component { void dump_config() override; esp_err_t create_endpoint(uint8_t endpoint_id, zb_ha_standard_devs_e device_id, esp_zb_cluster_list_t *esp_zb_cluster_list); - void set_basic_cluster(const char *model, const char *manufacturer); + void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source); void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role); void create_default_cluster(uint8_t endpoint_id, zb_ha_standard_devs_e device_id); @@ -62,6 +62,7 @@ class ZigbeeComponent : public Component { template<typename F> void add_on_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); } + bool is_battery_powered() { return this->basic_cluster_data_.power_source == ESP_ZB_ZCL_BASIC_POWER_SOURCE_BATTERY; } bool is_started() { return this->started; } bool is_connected() { return this->connected_; } std::atomic<bool> started = false; @@ -73,6 +74,7 @@ class ZigbeeComponent : public Component { uint8_t *model; uint8_t *manufacturer; uint8_t *date; + uint8_t power_source; } basic_cluster_data_; bool connected_ = false; #ifdef ZB_ED_ROLE diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index 5b1808ea60d..e446377a06b 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -36,9 +36,11 @@ from .const import ( ANALOG_INPUT_APPTYPE, BACNET_UNIT_NO_UNITS, BACNET_UNITS, + CONF_POWER_SOURCE, CONF_REPORT, CONF_ROUTER, KEY_ZIGBEE, + POWER_SOURCE, REPORT, ZigbeeAttribute, ) @@ -320,6 +322,7 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": var.set_basic_cluster( config[CONF_MODEL], "esphome", + cg.RawExpression(POWER_SOURCE[config[CONF_POWER_SOURCE]]), ) ) for ep in ep_list: diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml index 77e202b5235..82a523fc7c4 100644 --- a/tests/components/zigbee/common_esp32.yaml +++ b/tests/components/zigbee/common_esp32.yaml @@ -12,6 +12,7 @@ binary_sensor: zigbee: model: zigbee_test router: true + power_source: MAINS_SINGLE_PHASE on_join: then: - logger.log: "Joined network" From 55ef66cc265ceae81e9c872eb49b47ab1662c6d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Mon, 11 May 2026 13:12:55 -0500 Subject: [PATCH 1933/2056] [helpers] Re-enable ProgressBar under --dashboard mode (#16357) --- esphome/helpers.py | 11 ++++++++- tests/unit_tests/test_helpers.py | 42 ++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 62ddc489ba2..d7ddb5c4161 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -618,10 +618,19 @@ class ProgressBar: """A simple terminal progress bar for upload operations.""" def __init__(self, header: str, stream: TextIO | None = None) -> None: + # Local import to avoid a top-level cycle with esphome.core. + from esphome.core import CORE + self.header = header self.stream = stream or sys.stderr self.last_progress: int | None = None - self.enabled = hasattr(self.stream, "isatty") and self.stream.isatty() + # Enable when writing to an interactive TTY *or* when running under + # ``--dashboard``. The dashboard captures our stderr via + # ``stdout=PIPE, stderr=STDOUT`` and parses the ``\rUploading: NN%`` + # frames to drive its own progress UI -- gating purely on ``isatty()`` + # silently disables every dashboard-side flash-progress indicator. + is_tty = hasattr(self.stream, "isatty") and self.stream.isatty() + self.enabled = is_tty or CORE.dashboard def update(self, progress: float) -> None: if not self.enabled: diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index f2faf3ba8f9..bb00a15beef 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,9 +1,10 @@ +import io import logging import os from pathlib import Path import socket import stat -from unittest.mock import patch +from unittest.mock import MagicMock, patch from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr from hypothesis import given @@ -12,7 +13,8 @@ import pytest from esphome import helpers from esphome.address_cache import AddressCache -from esphome.core import EsphomeError +from esphome.core import CORE, EsphomeError +from esphome.helpers import ProgressBar @pytest.mark.parametrize( @@ -1024,3 +1026,39 @@ def test_resolve_ip_address_mixed_cached_uncached() -> None: assert "192.168.1.10" in addresses # Direct IP assert "192.168.1.50" in addresses # From cache assert "192.168.1.100" in addresses # From resolver + + +def test_progressbar_enabled_on_tty(monkeypatch) -> None: + """Interactive TTY: progress writes through (pre-existing behaviour).""" + stream = MagicMock(spec=io.TextIOWrapper) + stream.isatty.return_value = True + monkeypatch.setattr(CORE, "dashboard", False) + + bar = ProgressBar("Uploading", stream=stream) + assert bar.enabled is True + + +def test_progressbar_disabled_on_pipe_without_dashboard(monkeypatch) -> None: + """Piped output without --dashboard: progress suppressed.""" + stream = MagicMock(spec=io.TextIOWrapper) + stream.isatty.return_value = False + monkeypatch.setattr(CORE, "dashboard", False) + + bar = ProgressBar("Uploading", stream=stream) + assert bar.enabled is False + + +def test_progressbar_enabled_on_pipe_with_dashboard(monkeypatch) -> None: + r"""Piped output under --dashboard: progress writes through. + + The dashboard captures stderr through a pipe (so ``isatty()`` is False) + and parses ``\rUploading: NN%`` frames to drive its progress UI. + Gating purely on ``isatty()`` silently disables every dashboard-side + flash-progress indicator. + """ + stream = MagicMock(spec=io.TextIOWrapper) + stream.isatty.return_value = False + monkeypatch.setattr(CORE, "dashboard", True) + + bar = ProgressBar("Uploading", stream=stream) + assert bar.enabled is True From 4e31b713048d0dd9385434d2d9599baa168d506d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 May 2026 08:52:33 +1000 Subject: [PATCH 1934/2056] [lvgl] Add new trigger `on_update` and new number option (#16312) --- esphome/components/lvgl/defines.py | 11 ++++- esphome/components/lvgl/lvcode.py | 3 +- esphome/components/lvgl/number/__init__.py | 37 ++++++++++----- esphome/components/lvgl/number/lvgl_number.h | 9 +--- esphome/components/lvgl/schemas.py | 25 +++++++++- esphome/components/lvgl/sensor/__init__.py | 18 +++---- esphome/components/lvgl/trigger.py | 8 ++++ esphome/components/lvgl/types.py | 3 +- esphome/components/lvgl/widgets/__init__.py | 13 ++--- esphome/components/lvgl/widgets/spinbox.py | 4 +- tests/components/lvgl/common.yaml | 2 +- tests/components/lvgl/lvgl-package.yaml | 50 ++++++++++++++++++++ 12 files changed, 135 insertions(+), 48 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 7bfd26bb6ec..15a24f1ad2f 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -20,7 +20,6 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType LOGGER = logging.getLogger(__name__) -lvgl_ns = cg.esphome_ns.namespace("lvgl") DOMAIN = "lvgl" KEY_COLOR_FORMATS = "color_formats" @@ -400,6 +399,13 @@ LV_EVENT_MAP = { LV_PRESS_EVENTS = ("PRESS", "PRESSING", "RELEASE") +VALUE_ON_CHANGE = "on_change" +VALUE_ON_UPDATE = "on_update" +VALUE_ON_VALUE = "on_value" +VALUE_ON_RELEASE = "on_release" + +LV_VALUE_EVENTS = (VALUE_ON_CHANGE, VALUE_ON_UPDATE, VALUE_ON_VALUE, VALUE_ON_RELEASE) + def is_press_event(event: str) -> bool: return event.removeprefix("on_").upper() in LV_PRESS_EVENTS @@ -788,6 +794,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_THEME = "theme" CONF_TICK_STYLE = "tick_style" CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" @@ -799,7 +806,7 @@ CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSFORM_ROTATION = "transform_rotation" CONF_TRANSFORM_SCALE = "transform_scale" CONF_TRANSPARENCY_KEY = "transparency_key" -CONF_THEME = "theme" +CONF_TRIGGER = "trigger" CONF_UPDATE_ON_RELEASE = "update_on_release" CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle" CONF_VISIBLE_ROW_COUNT = "visible_row_count" diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 32fb02e3d21..de005937730 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -20,7 +20,8 @@ from esphome.cpp_generator import ( ) from esphome.yaml_util import ESPHomeDataBase -from .defines import literal, lvgl_ns +from .defines import literal +from .types import lvgl_ns LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp() diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 0c7e4b9524a..be51963ba12 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -1,10 +1,16 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import CONF_RESTORE_VALUE +from esphome.const import CONF_ON_RELEASE, CONF_RESTORE_VALUE from esphome.cpp_generator import MockObj -from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..defines import ( + CONF_ANIMATED, + CONF_TRIGGER, + CONF_UPDATE_ON_RELEASE, + CONF_WIDGET, + LOGGER, +) from ..lv_validation import animated from ..lvcode import ( EVENT_ARG, @@ -14,7 +20,8 @@ from ..lvcode import ( lv_obj, lvgl_static, ) -from ..types import LV_EVENT, LvNumber, lvgl_ns +from ..schemas import TRIGGER_EVENT_MAP, VALUE_TRIGGER_SCHEMA +from ..types import LvNumber, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component) @@ -22,14 +29,22 @@ LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component) CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + **VALUE_TRIGGER_SCHEMA, cv.Optional(CONF_ANIMATED, default=True): animated, - cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + cv.Optional(CONF_UPDATE_ON_RELEASE): cv.boolean, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } ) async def to_code(config): + trigger = config[CONF_TRIGGER] + if CONF_UPDATE_ON_RELEASE in config: + LOGGER.warning( + "Option 'update_on_release' is deprecated and will be removed in 2026.11.0 - use 'trigger: on_release' instead" + ) + if config[CONF_UPDATE_ON_RELEASE]: + trigger = CONF_ON_RELEASE widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -40,19 +55,13 @@ async def to_code(config): "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr) - event_code = ( - LV_EVENT.VALUE_CHANGED - if not config[CONF_UPDATE_ON_RELEASE] - else LV_EVENT.RELEASED - ) var = await number.new_number( config, await control.get_lambda(), await value.get_lambda(), - event_code, config[CONF_RESTORE_VALUE], - max_value=widget.type.get_max(widget.config), - min_value=widget.type.get_min(widget.config), + max_value=await widget.type.get_max(widget.config), + min_value=await widget.type.get_min(widget.config), step=widget.type.get_step(widget.config), ) async with LambdaContext(EVENT_ARG) as event: @@ -60,6 +69,8 @@ async def to_code(config): await cg.register_component(var, config) cg.add( lvgl_static.add_event_cb( - widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code + widget.obj, + await event.get_lambda(), + *TRIGGER_EVENT_MAP[trigger], ) ) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index ba16b1f0b39..3fda9427c50 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -10,12 +10,8 @@ namespace esphome::lvgl { class LVGLNumber : public number::Number, public Component { public: - LVGLNumber(std::function<void(float)> control_lambda, std::function<float()> value_lambda, lv_event_code_t event, - bool restore) - : control_lambda_(std::move(control_lambda)), - value_lambda_(std::move(value_lambda)), - event_(event), - restore_(restore) {} + LVGLNumber(std::function<void(float)> control_lambda, std::function<float()> value_lambda, bool restore) + : control_lambda_(std::move(control_lambda)), value_lambda_(std::move(value_lambda)), restore_(restore) {} void setup() override { float value = this->value_lambda_(); @@ -42,7 +38,6 @@ class LVGLNumber : public number::Number, public Component { } std::function<void(float)> control_lambda_; std::function<float()> value_lambda_; - lv_event_code_t event_; bool restore_; ESPPreferenceObject pref_{}; }; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b71c2875e7..553e0f7398d 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_GROUP, CONF_ID, CONF_ON_BOOT, + CONF_ON_UPDATE, CONF_ON_VALUE, CONF_STATE, CONF_TEXT, @@ -29,7 +30,13 @@ from .defines import ( CONF_SCROLL_SNAP_Y, CONF_SCROLLBAR_MODE, CONF_TIME_FORMAT, + CONF_TRIGGER, LV_GRAD_DIR, + LV_VALUE_EVENTS, + VALUE_ON_CHANGE, + VALUE_ON_RELEASE, + VALUE_ON_UPDATE, + VALUE_ON_VALUE, get_remapped_uses, is_press_event, ) @@ -41,8 +48,9 @@ from .layout import ( grid_alignments, ) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity -from .lvcode import LvglComponent, lv_event_t_ptr +from .lvcode import UPDATE_EVENT, LvglComponent, lv_event_t_ptr from .types import ( + LV_EVENT, LVEncoderListener, LvType, lv_group_t, @@ -355,6 +363,19 @@ SET_STATE_SCHEMA = cv.Schema( FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of) +VALUE_TRIGGER_SCHEMA = { + cv.Optional(CONF_TRIGGER, default=CONF_ON_VALUE): cv.one_of( + *LV_VALUE_EVENTS, lower=True + ), +} + +TRIGGER_EVENT_MAP = { + VALUE_ON_CHANGE: (LV_EVENT.VALUE_CHANGED,), + VALUE_ON_UPDATE: (UPDATE_EVENT,), + VALUE_ON_VALUE: (LV_EVENT.VALUE_CHANGED, UPDATE_EVENT), + VALUE_ON_RELEASE: (LV_EVENT.RELEASED,), +} + def part_schema(parts): """ @@ -370,7 +391,7 @@ def part_schema(parts): def automation_schema(typ: LvType): events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS if typ.has_on_value: - events = events + (CONF_ON_VALUE,) + events = events + (CONF_ON_VALUE, CONF_ON_UPDATE) args = typ.get_arg_type() def get_trigger_args(event): diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 682d84ede3e..e69ea9771a0 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -1,21 +1,16 @@ from esphome.components.sensor import Sensor, new_sensor, sensor_schema import esphome.config_validation as cv -from ..defines import CONF_WIDGET -from ..lvcode import ( - EVENT_ARG, - UPDATE_EVENT, - LambdaContext, - LvContext, - lv_add, - lvgl_static, -) -from ..types import LV_EVENT, LvNumber +from ..defines import CONF_TRIGGER, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lv_add, lvgl_static +from ..schemas import TRIGGER_EVENT_MAP, VALUE_TRIGGER_SCHEMA +from ..types import LvNumber from ..widgets import Widget, get_widgets, wait_for_widgets CONFIG_SCHEMA = sensor_schema(Sensor).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + **VALUE_TRIGGER_SCHEMA, } ) @@ -33,7 +28,6 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), - LV_EVENT.VALUE_CHANGED, - UPDATE_EVENT, + *TRIGGER_EVENT_MAP[config[CONF_TRIGGER]], ) ) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 64590d56f61..5f524969e28 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.const import ( CONF_ID, CONF_ON_BOOT, + CONF_ON_UPDATE, CONF_ON_VALUE, CONF_TRIGGER_ID, CONF_X, @@ -92,6 +93,13 @@ async def generate_triggers(): UPDATE_EVENT, ) + for conf in config.get(CONF_ON_UPDATE, ()): + await add_trigger( + conf, + w, + UPDATE_EVENT, + ) + await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 1872ce2d32a..509d5cc7824 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -3,8 +3,6 @@ from esphome.const import CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj from esphome.cpp_types import Component, esphome_ns -from .defines import lvgl_ns - class LvType(cg.MockObjClass): def __init__(self, *args, **kwargs): @@ -47,6 +45,7 @@ PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template()) DrawEndTrigger = esphome_ns.class_( "Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32) ) +lvgl_ns = cg.esphome_ns.namespace("lvgl") IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 534ebe0b93d..ab1c61ff88f 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -48,6 +48,7 @@ from ..defines import ( join_enums, literal, ) +from ..lv_validation import lv_int from ..lvcode import ( LvConditional, add_line_marks, @@ -207,10 +208,10 @@ class WidgetType: """ return () - def get_max(self, config: dict): + async def get_max(self, config: dict): return sys.maxsize - def get_min(self, config: dict): + async def get_min(self, config: dict): return -sys.maxsize def get_step(self, config: dict): @@ -637,8 +638,8 @@ async def widget_to_code(w_cnfig, w_type: WidgetType | str, parent) -> Widget: class NumberType(WidgetType): - def get_max(self, config: dict): - return int(config.get(CONF_MAX_VALUE, 100)) + async def get_max(self, config: dict): + return await lv_int.process(config.get(CONF_MAX_VALUE, 100)) - def get_min(self, config: dict): - return int(config.get(CONF_MIN_VALUE, 0)) + async def get_min(self, config: dict): + return await lv_int.process(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index 58e3435c5c7..ab68a76e9cb 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -125,10 +125,10 @@ class SpinboxType(WidgetType): def get_uses(self): return CONF_TEXTAREA, CONF_LABEL - def get_max(self, config: dict): + async def get_max(self, config: dict): return config[CONF_RANGE_TO] - def get_min(self, config: dict): + async def get_min(self, config: dict): return config[CONF_RANGE_FROM] def get_step(self, config: dict): diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 652ae7e7a13..f500002f401 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -38,7 +38,7 @@ number: - platform: lvgl widget: slider_id name: LVGL Slider Number - update_on_release: true + trigger: on_release restore_value: true - platform: lvgl widget: lv_arc diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 4bf5b9d494a..53984bb006d 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -309,6 +309,11 @@ lvgl: - logger.log: format: "Roller changed = %d: %s" args: [x, text.c_str()] + on_update: + then: + - logger.log: + format: "Roller updated = %d: %s" + args: [x, text.c_str()] - animimg: height: 60 id: anim_img @@ -630,6 +635,10 @@ lvgl: logger.log: format: "state now %d" args: [x] + on_update: + logger.log: + format: "button updated %d" + args: [x] on_short_click: lvgl.widget.hide: hello_label on_long_press: @@ -758,6 +767,9 @@ lvgl: lambda: return tile == id(tile_1); then: - logger.log: "tile 1 is now showing" + on_update: + then: + - logger.log: "tileview updated programmatically" tiles: - id: tile_1 scroll_snap_y: center @@ -983,6 +995,11 @@ lvgl: - logger.log: format: "Arc value is %f" args: [x] + on_update: + then: + - logger.log: + format: "Arc updated to %f" + args: [x] scroll_on_focus: true value: 75 min_value: 1 @@ -1085,6 +1102,11 @@ lvgl: - logger.log: format: "slider value %f" args: [x] + on_update: + then: + - logger.log: + format: "slider updated to %f" + args: [x] on_click: then: - lvgl.slider.update: @@ -1197,6 +1219,10 @@ lvgl: logger.log: format: "Dropdown changed = %d: %s" args: [x, text.c_str()] + on_update: + logger.log: + format: "Dropdown updated = %d: %s" + args: [x, text.c_str()] on_cancel: logger.log: format: "Dropdown closed = %d" @@ -1437,6 +1463,30 @@ color: blue_int: 64 white_int: 255 +sensor: + - platform: lvgl + widget: lv_arc_1 + id: lvgl_arc1_sensor_on_change + name: LVGL Arc1 Sensor on_change + trigger: on_change + - platform: lvgl + widget: bar_id + id: lvgl_bar_sensor_on_release + name: LVGL Bar Sensor on_release + trigger: on_release + +number: + - platform: lvgl + widget: lv_arc_1 + id: lvgl_arc1_number_on_update + name: LVGL Arc1 Number on_update + trigger: on_update + - platform: lvgl + widget: spinbox_id + id: lvgl_spinbox_number_on_change + name: LVGL Spinbox Number on_change + trigger: on_change + select: - platform: lvgl id: lv_roller_select From a232aedebda88a04ef7abf27e335c27d48bdf206 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 May 2026 12:09:42 +1000 Subject: [PATCH 1935/2056] [lvgl] Check for user defined LV_USE items (#16362) --- esphome/components/lvgl/__init__.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 31131d253f9..91b101cd258 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,6 +1,7 @@ import importlib from pathlib import Path import pkgutil +import re from esphome.automation import Trigger, build_automation, validate_automation import esphome.codegen as cg @@ -30,12 +31,14 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, CONF_BUFFER_SIZE, + CONF_ESPHOME, CONF_GROUP, CONF_ID, CONF_LAMBDA, CONF_LOG_LEVEL, CONF_ON_IDLE, CONF_PAGES, + CONF_PLATFORMIO_OPTIONS, CONF_ROTATION, CONF_TIMEOUT, CONF_TRIGGER_ID, @@ -51,6 +54,7 @@ from . import defines as df, lv_validation as lvalid, widgets from .automation import layers_to_code, lvgl_update from .defines import ( CONF_ALIGN_TO_LAMBDA_ID, + LOGGER, get_focused_widgets, get_lv_images_used, get_refreshed_widgets, @@ -148,9 +152,28 @@ def generate_lv_conf_h(): all_defines = set( df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES) ) - # Get the defines that are actually used based on the config + build_flags = ( + CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS).get("build_flags", []) + ) + if not isinstance(build_flags, list): + build_flags = [build_flags] + # Extract define names from build flags like '-DLV_USE_CHART=1', '-D LV_USE_CHART', + # or multiple defines in one string. + define_pattern = r'-D\s*([A-Z_][A-Z0-9_]*)(?:=[^\s\'"\]]*)?' + defines_from_flags = { + m.group(1) for flag in build_flags for m in re.finditer(define_pattern, flag) + } + + # Get the defines that are actually used based on the config, lv_defines = df.get_defines() - unused_defines = all_defines - set(lv_defines) + clashes = defines_from_flags & lv_defines.keys() + if clashes: + LOGGER.warning( + "Some defines are set both by ESPHome build flags and by LVGL configuration which may lead to unexpected behavior: %s", + sorted(list(clashes)), + ) + unused_defines = all_defines - lv_defines.keys() - defines_from_flags + # Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [ as_macro(m, "0") for m in unused_defines From 7dce58c58d74279ab1a81c52533d97dbde6ffa88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 02:17:42 +0000 Subject: [PATCH 1936/2056] Bump requests from 2.33.1 to 2.34.0 (#16364) Signed-off-by: dependabot[bot] <support@github.com> 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 818453dc8f9..dd539d4f867 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.33.1 +requests==2.34.0 # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 From 49df1bd30e543b870c02998d6cc3eb04d0c06f85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 02:18:06 +0000 Subject: [PATCH 1937/2056] Bump actions/cache from 5.0.3 to 5.0.5 (#16365) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cb8e07afa8..bf9d474d7a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -804,7 +804,7 @@ jobs: cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache ESPHome - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }} @@ -863,7 +863,7 @@ jobs: - name: Save ESPHome cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }} From b5e50144e314dfc66da12640725c0779f399679d Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Tue, 12 May 2026 02:32:58 +0000 Subject: [PATCH 1938/2056] [ota] Improve OTA error messages (#16327) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> --- .../components/ota/ota_backend_esp_idf.cpp | 3 + esphome/espota2.py | 29 +++++----- tests/unit_tests/test_espota2.py | 58 ++++++++++--------- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index f391c1791aa..ade726da1fb 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -68,6 +68,9 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; + } else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) { + // This error appears with 1 factory and 1 ota partition + return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } return OTA_RESPONSE_ERROR_UNKNOWN; } diff --git a/esphome/espota2.py b/esphome/espota2.py index c13c3ea207f..701a125bcdb 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -118,7 +118,8 @@ _ERROR_MESSAGES: dict[int, str] = { ), RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: ( "The OTA partition on the ESP is too small. ESPHome needs to resize " - "this partition, please flash over USB." + "this partition. Please flash over USB or update the partition table " + "over the air." ), RESPONSE_ERROR_NO_UPDATE_PARTITION: ( "The OTA partition on the ESP couldn't be found. ESPHome needs to " @@ -202,19 +203,19 @@ def receive_exactly( try: data += recv_decode(sock, 1, decode=decode) # type: ignore[operator] except OSError as err: - raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err + raise OTAError(f"receiving {msg} response: {err}") from err try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError(f"Error {msg}: {err}") from err + raise OTAError(f"receiving {msg}: {err}") from err while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) # type: ignore[operator] except OSError as err: - raise OTAError(f"Error receiving {msg}: {err}") from err + raise OTAError(f"receiving {msg}: {err}") from err return data @@ -231,14 +232,14 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None # silently passed through and surface later as cryptic decode/timeout failures. if not data: raise OTAError( - "Error: Device closed connection without responding. " + "Device closed connection without responding. " "This may indicate the device ran out of memory, " "a network issue, or the connection was interrupted." ) dat = data[0] error_msg = _ERROR_MESSAGES.get(dat) if error_msg is not None: - raise OTAError(f"Error: {error_msg}") + raise OTAError(error_msg) if expect is None: return if not isinstance(expect, (list, tuple)): @@ -267,7 +268,7 @@ def send_check( sock.sendall(data) except OSError as err: - raise OTAError(f"Error sending {msg}: {err}") from err + raise OTAError(f"sending {msg}: {err}") from err def perform_ota( @@ -376,7 +377,7 @@ def perform_ota( raise OTAError("ESP requests password, but no password given!") nonce_bytes = receive_exactly( - sock, nonce_size, f"{hash_name} authentication nonce", None, decode=False + sock, nonce_size, f"{hash_name} auth nonce", None, decode=False ) assert isinstance(nonce_bytes, bytes) nonce = nonce_bytes.decode() @@ -424,13 +425,13 @@ def perform_ota( (upload_size >> 0) & 0xFF, ] send_check(sock, upload_size_encoded, "binary size") - receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK) + receive_exactly(sock, 1, "update prepare result", RESPONSE_UPDATE_PREPARE_OK) upload_md5 = hashlib.md5(upload_contents).hexdigest() _LOGGER.debug("MD5 of upload is %s", upload_md5) send_check(sock, upload_md5, "file checksum") - receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK) + receive_exactly(sock, 1, "file checksum result", RESPONSE_BIN_MD5_OK) # Disable nodelay for transfer sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) @@ -451,10 +452,10 @@ def perform_ota( try: sock.sendall(chunk) if version >= OTA_VERSION_2_0: - receive_exactly(sock, 1, "chunk OK", RESPONSE_CHUNK_OK) + receive_exactly(sock, 1, "chunk result", RESPONSE_CHUNK_OK) except OSError as err: sys.stderr.write("\n") - raise OTAError(f"Error sending data: {err}") from err + raise OTAError(f"sending data: {err}") from err progress.update(offset / upload_size) progress.done() @@ -465,8 +466,8 @@ def perform_ota( _LOGGER.info("Upload took %.2f seconds, waiting for result...", duration) - receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) - receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) + receive_exactly(sock, 1, "update receive result", RESPONSE_RECEIVE_OK) + receive_exactly(sock, 1, "update end result", RESPONSE_UPDATE_END_OK) send_check(sock, RESPONSE_OK, "end acknowledgement") _LOGGER.info("OTA successful") diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index d7fcedfd66d..9413fbcf296 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -135,7 +135,9 @@ def test_receive_exactly_with_error_response(mock_socket: Mock) -> None: """Test receive_exactly raises OTAError on error response.""" mock_socket.recv.return_value = bytes([espota2.RESPONSE_ERROR_AUTH_INVALID]) - with pytest.raises(espota2.OTAError, match="Error auth:.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth:.*Authentication invalid" + ): espota2.receive_exactly(mock_socket, 1, "auth", [espota2.RESPONSE_OK]) mock_socket.close.assert_called_once() @@ -145,69 +147,69 @@ def test_receive_exactly_socket_error(mock_socket: Mock) -> None: """Test receive_exactly handles socket errors.""" mock_socket.recv.side_effect = OSError("Connection reset") - with pytest.raises(espota2.OTAError, match="Error receiving acknowledge test"): + with pytest.raises(espota2.OTAError, match="receiving test response"): espota2.receive_exactly(mock_socket, 1, "test", espota2.RESPONSE_OK) @pytest.mark.parametrize( ("error_code", "expected_msg"), [ - (espota2.RESPONSE_ERROR_MAGIC, "Error: Invalid magic byte"), - (espota2.RESPONSE_ERROR_UPDATE_PREPARE, "Error: Couldn't prepare flash memory"), - (espota2.RESPONSE_ERROR_AUTH_INVALID, "Error: Authentication invalid"), + (espota2.RESPONSE_ERROR_MAGIC, "Invalid magic byte"), + (espota2.RESPONSE_ERROR_UPDATE_PREPARE, "Couldn't prepare flash memory"), + (espota2.RESPONSE_ERROR_AUTH_INVALID, "Authentication invalid"), ( espota2.RESPONSE_ERROR_WRITING_FLASH, - "Error: Writing OTA data to flash memory failed", + "Writing OTA data to flash memory failed", ), - (espota2.RESPONSE_ERROR_UPDATE_END, "Error: Finishing update failed"), + (espota2.RESPONSE_ERROR_UPDATE_END, "Finishing update failed"), ( espota2.RESPONSE_ERROR_INVALID_BOOTSTRAPPING, - "Error: Please press the reset button", + "Please press the reset button", ), ( espota2.RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG, - "Error: ESP has been flashed with wrong flash size", + "ESP has been flashed with wrong flash size", ), ( espota2.RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG, - "Error: ESP does not have the requested flash size", + "ESP does not have the requested flash size", ), ( espota2.RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE, - "Error: ESP does not have enough space", + "ESP does not have enough space", ), ( espota2.RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE, - "Error: The OTA partition on the ESP is too small", + "The OTA partition on the ESP is too small", ), ( espota2.RESPONSE_ERROR_NO_UPDATE_PARTITION, - "Error: The OTA partition on the ESP couldn't be found", + "The OTA partition on the ESP couldn't be found", ), - (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Error: Application MD5 code mismatch"), + (espota2.RESPONSE_ERROR_MD5_MISMATCH, "Application MD5 code mismatch"), ( espota2.RESPONSE_ERROR_SIGNATURE_INVALID, - "Error: Firmware signature verification failed", + "Firmware signature verification failed", ), ( espota2.RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE, - "Error: The requested OTA type is not supported by the device", + "The requested OTA type is not supported by the device", ), ( espota2.RESPONSE_ERROR_PARTITION_TABLE_VERIFY, - "Error: The partition table update could not be verified", + "The partition table update could not be verified", ), ( espota2.RESPONSE_ERROR_PARTITION_TABLE_UPDATE, - "Error: An error occurred while updating the partition table", + "An error occurred while updating the partition table", ), ( espota2.RESPONSE_ERROR_BOOTLOADER_VERIFY, - "Error: The bootloader update could not be verified", + "The bootloader update could not be verified", ), ( espota2.RESPONSE_ERROR_BOOTLOADER_UPDATE, - "Error: An error occurred while updating the bootloader", + "An error occurred while updating the bootloader", ), (espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"), ], @@ -262,7 +264,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None: """Test send_check handles socket errors.""" mock_socket.sendall.side_effect = OSError("Broken pipe") - with pytest.raises(espota2.OTAError, match="Error sending test"): + with pytest.raises(espota2.OTAError, match="sending test"): espota2.send_check(mock_socket, b"data", "test") @@ -417,7 +419,9 @@ def test_perform_ota_md5_auth_wrong_password( mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth.*Authentication invalid" + ): espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") # Verify the socket was closed after auth failure @@ -441,7 +445,9 @@ def test_perform_ota_sha256_auth_wrong_password( mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error auth.*Authentication invalid"): + with pytest.raises( + espota2.OTAError, match="receiving auth.*Authentication invalid" + ): espota2.perform_ota(mock_socket, "wrongpassword", mock_file, "test.bin") # Verify the socket was closed after auth failure @@ -484,7 +490,7 @@ def test_perform_ota_unexpected_auth_response(mock_socket: Mock) -> None: # This will actually raise "Unexpected response from ESP" from check_error with pytest.raises( - espota2.OTAError, match=r"Error auth: Unexpected response from ESP: 0x03" + espota2.OTAError, match=r"receiving auth: Unexpected response from ESP: 0x03" ): espota2.perform_ota(mock_socket, "password", mock_file, "test.bin") @@ -520,7 +526,7 @@ def test_perform_ota_upload_error(mock_socket: Mock, mock_file: io.BytesIO) -> N mock_socket.recv.side_effect = recv_responses - with pytest.raises(espota2.OTAError, match="Error receiving acknowledge chunk OK"): + with pytest.raises(espota2.OTAError, match="receiving chunk result response"): espota2.perform_ota(mock_socket, None, mock_file, "test.bin") @@ -1109,7 +1115,7 @@ def test_check_error_detects_errors_when_expect_is_none() -> None: during feature negotiation and nonce reads) silently passed error bytes through, turning clean device errors into confusing later failures. """ - with pytest.raises(espota2.OTAError, match="Error: Authentication invalid"): + with pytest.raises(espota2.OTAError, match="Authentication invalid"): espota2.check_error([espota2.RESPONSE_ERROR_AUTH_INVALID], None) From 4ff946ac15e8fbe040020b487dbe1a0d9c955974 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 May 2026 21:52:07 +1000 Subject: [PATCH 1939/2056] [cli] Add --no-states flag to run command (#16366) --- esphome/__main__.py | 7 +++ tests/unit_tests/test_main.py | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index ce24f44b3a6..988f28a55fe 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2170,6 +2170,13 @@ def parse_args(argv): parser_run.add_argument( "--no-logs", help="Disable starting logs.", action="store_true" ) + + parser_run.add_argument( + "--no-states", + action="store_true", + help="Do not show entity state changes in log output.", + ) + parser_run.add_argument( "--reset", "-r", diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3648de443dc..0104854e1f5 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -30,6 +30,7 @@ from esphome.__main__ import ( command_bundle, command_clean_all, command_rename, + command_run, command_update_all, command_wizard, compile_program, @@ -45,6 +46,7 @@ from esphome.__main__ import ( has_resolvable_address, has_web_server_ota, mqtt_get_ip, + parse_args, run_esphome, run_miniterm, show_logs, @@ -5823,3 +5825,96 @@ def test_upload_using_esptool_subprocess_passes_crystal_callback( call_kwargs = mock_run_external_process.call_args[1] assert "line_callbacks" in call_kwargs assert len(call_kwargs["line_callbacks"]) == 1 + + +def test_parse_args_run_no_states() -> None: + """Test that --no-states is parsed for the run command.""" + args = parse_args(["esphome", "run", "--no-states", "device.yaml"]) + assert args.no_states is True + + +def test_parse_args_run_no_states_default() -> None: + """Test that no_states defaults to False for the run command.""" + args = parse_args(["esphome", "run", "device.yaml"]) + assert args.no_states is False + + +def test_parse_args_logs_no_states() -> None: + """Test that --no-states is parsed for the logs command.""" + args = parse_args(["esphome", "logs", "--no-states", "device.yaml"]) + assert args.no_states is True + + +@patch("esphome.components.api.client.run_logs") +def test_command_run_passes_no_states_to_show_logs( + mock_run_logs: Mock, +) -> None: + """Test that command_run propagates --no-states through to run_logs.""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + args.no_states = True + args.no_logs = False + args.device = None + + with ( + patch("esphome.__main__.write_cpp", return_value=0), + patch("esphome.__main__.compile_program", return_value=0), + patch( + "esphome.__main__.choose_upload_log_host", + return_value=["192.168.1.100"], + ), + patch("esphome.__main__.upload_program", return_value=(0, "192.168.1.100")), + patch("esphome.__main__.get_serial_ports", return_value=[]), + ): + result = command_run(args, CORE.config) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=False + ) + + +@patch("esphome.components.api.client.run_logs") +def test_command_run_defaults_subscribe_states_true( + mock_run_logs: Mock, +) -> None: + """Test that command_run subscribes states by default (no --no-states).""" + setup_core( + config={ + "logger": {}, + CONF_API: {}, + CONF_MDNS: {CONF_DISABLED: False}, + }, + platform=PLATFORM_ESP32, + ) + mock_run_logs.return_value = 0 + + args = MockArgs() + args.no_logs = False + args.device = None + + with ( + patch("esphome.__main__.write_cpp", return_value=0), + patch("esphome.__main__.compile_program", return_value=0), + patch( + "esphome.__main__.choose_upload_log_host", + return_value=["192.168.1.100"], + ), + patch("esphome.__main__.upload_program", return_value=(0, "192.168.1.100")), + patch("esphome.__main__.get_serial_ports", return_value=[]), + ): + result = command_run(args, CORE.config) + + assert result == 0 + mock_run_logs.assert_called_once_with( + CORE.config, ["192.168.1.100"], subscribe_states=True + ) From 365ed1931920256a6d85eb0f9105516dacb49e98 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 12 May 2026 11:19:10 -0400 Subject: [PATCH 1940/2056] [core] Fix PROGMEM_STRING_TABLE placement on ESP8266 without flash log strings (#16373) --- esphome/core/progmem.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 031860e3a64..d349418d02e 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -25,6 +25,16 @@ #define ESPHOME_strncasecmp_P strncasecmp_P // Type for pointers to PROGMEM strings (for use with ESPHOME_F return values) using ProgmemStr = const __FlashStringHelper *; +// Storage class for PROGMEM_STRING_TABLE data. Mirrors the logger's choice of +// LOG_STR_ARG: when LOG_STR_ARG treats the LogString as PROGMEM (PGM_P), the +// table data must actually be in flash; when LOG_STR_ARG treats it as a plain +// const char* (assumes RAM), the table data must live in RAM or non-logger +// consumers (ArduinoJson, Print, MQTT publish) crash on unaligned flash reads. +#ifdef USE_STORE_LOG_STR_IN_FLASH +#define ESPHOME_PROGMEM_STRING_TABLE_STORAGE PROGMEM +#else +#define ESPHOME_PROGMEM_STRING_TABLE_STORAGE +#endif #else #define ESPHOME_F(string_literal) (string_literal) #define ESPHOME_PGM_P const char * @@ -38,6 +48,8 @@ using ProgmemStr = const __FlashStringHelper *; #define ESPHOME_strncasecmp_P strncasecmp // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; +// No-op on non-ESP8266 platforms where PROGMEM itself is a no-op. +#define ESPHOME_PROGMEM_STRING_TABLE_STORAGE #endif namespace esphome { @@ -100,8 +112,8 @@ struct LogString; static constexpr size_t COUNT = Table::COUNT; \ static constexpr uint8_t LAST_INDEX = COUNT - 1; \ static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ - static constexpr auto BLOB PROGMEM = Table::make_blob(); \ - static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ + static constexpr auto BLOB ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_blob(); \ + static constexpr auto OFFSETS ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_offsets(); \ static const char *get_(uint8_t idx, uint8_t fallback) { \ if (idx >= COUNT) \ idx = fallback; \ From 727c74da3f72e9df2a6a9756e4752c188fcc775d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 12 May 2026 12:17:23 -0500 Subject: [PATCH 1941/2056] [script] Fix array-type parameters in script.execute (#16374) --- esphome/components/script/__init__.py | 2 + tests/components/script/common.yaml | 16 +++++ .../fixtures/script_array_params.yaml | 36 ++++++++++ tests/integration/test_script_array_params.py | 71 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 tests/integration/fixtures/script_array_params.yaml create mode 100644 tests/integration/test_script_array_params.py diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 51cae695b76..e92850fd63a 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -169,6 +169,8 @@ async def script_execute_action_to_code(config, action_id, template_arg, args): return value if type == "bool": return cg.RawExpression(str(value).lower()) + if isinstance(value, (list, tuple)): + return cg.ArrayInitializer(*value) return cg.RawExpression(str(value)) return converter diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml index bfd5d0e7ffc..c1dc68513fe 100644 --- a/tests/components/script/common.yaml +++ b/tests/components/script/common.yaml @@ -7,6 +7,12 @@ esphome: prefix: "Test" param2: 0 param3: true + - script.execute: + id: my_script_with_array_params + ints: [42, 100] + floats: [1.5, 2.5] + bools: [true, false] + strings: ["a", "b"] - script.wait: my_script - script.stop: my_script - if: @@ -34,6 +40,16 @@ script: mode: restart then: - lambda: 'ESP_LOGD("main", "Hello World!");' + - id: my_script_with_array_params + parameters: + ints: int[] + floats: float[] + bools: bool[] + strings: string[] + then: + - lambda: |- + ESP_LOGD("main", "ints=%d floats=%f bools=%d strings=%s", + ints[0], floats[0], bools[0], strings[0].c_str()); - id: my_script_with_params parameters: prefix: string diff --git a/tests/integration/fixtures/script_array_params.yaml b/tests/integration/fixtures/script_array_params.yaml new file mode 100644 index 00000000000..699f00a1f39 --- /dev/null +++ b/tests/integration/fixtures/script_array_params.yaml @@ -0,0 +1,36 @@ +esphome: + name: test-script-array-params + +host: + +api: + actions: + - action: run_array_script + then: + - script.execute: + id: array_script + ints: [42, 100] + floats: [1.5, 2.5] + bools: [true, false] + strings: ["hello", "world"] + +logger: + level: DEBUG + +script: + - id: array_script + parameters: + ints: int[] + floats: float[] + bools: bool[] + strings: string[] + then: + - lambda: |- + ESP_LOGI("test", "ints size=%u [0]=%d [1]=%d", + (unsigned) ints.size(), ints[0], ints[1]); + ESP_LOGI("test", "floats size=%u [0]=%.2f [1]=%.2f", + (unsigned) floats.size(), floats[0], floats[1]); + ESP_LOGI("test", "bools size=%u [0]=%d [1]=%d", + (unsigned) bools.size(), (int) bools[0], (int) bools[1]); + ESP_LOGI("test", "strings size=%u [0]=%s [1]=%s", + (unsigned) strings.size(), strings[0].c_str(), strings[1].c_str()); diff --git a/tests/integration/test_script_array_params.py b/tests/integration/test_script_array_params.py new file mode 100644 index 00000000000..2fba8a58fe2 --- /dev/null +++ b/tests/integration/test_script_array_params.py @@ -0,0 +1,71 @@ +"""Integration test for script array parameters (issue #16367). + +Verifies that script parameters of array types (`int[]`, `float[]`, `bool[]`, +`string[]`) compile and execute correctly. Prior to the fix in +`esphome/components/script/__init__.py`, the `script.execute` codegen emitted +the Python `repr` of the list (e.g. `return [42, 100];`) instead of a C++ +braced initializer, causing compile failures. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_array_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Execute a script with int[], float[], bool[], string[] parameters.""" + loop = asyncio.get_running_loop() + seen: dict[str, str] = {} + done = loop.create_future() + + patterns = { + "ints": re.compile(r"ints size=(\d+) \[0\]=(-?\d+) \[1\]=(-?\d+)"), + "floats": re.compile( + r"floats size=(\d+) \[0\]=(-?\d+\.\d+) \[1\]=(-?\d+\.\d+)" + ), + "bools": re.compile(r"bools size=(\d+) \[0\]=(\d+) \[1\]=(\d+)"), + "strings": re.compile(r"strings size=(\d+) \[0\]=(\w+) \[1\]=(\w+)"), + } + + def check_output(line: str) -> None: + for key, pat in patterns.items(): + if (m := pat.search(line)) and key not in seen: + seen[key] = m.group(0) + if len(seen) == len(patterns) and not done.done(): + done.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + _, services = await client.list_entities_services() + service = next((s for s in services if s.name == "run_array_script"), None) + assert service is not None, "run_array_script service not found" + await client.execute_service(service, {}) + + try: + await asyncio.wait_for(done, timeout=5.0) + except TimeoutError: + pytest.fail(f"Did not receive all expected log lines. Saw: {seen}") + + assert (m := patterns["ints"].search(seen["ints"])) + assert m.group(1) == "2" and m.group(2) == "42" and m.group(3) == "100" + + assert (m := patterns["floats"].search(seen["floats"])) + assert m.group(1) == "2" and m.group(2) == "1.50" and m.group(3) == "2.50" + + assert (m := patterns["bools"].search(seen["bools"])) + assert m.group(1) == "2" and m.group(2) == "1" and m.group(3) == "0" + + assert (m := patterns["strings"].search(seen["strings"])) + assert m.group(1) == "2" and m.group(2) == "hello" and m.group(3) == "world" From 76ce45c59ef453be351dca454ee4ac50b1b5783e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 May 2026 06:28:39 +1200 Subject: [PATCH 1942/2056] [script] Preserve source order of enum options in language schema (#16371) --- script/build_language_schema.py | 10 ++- tests/script/test_build_language_schema.py | 98 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/script/test_build_language_schema.py diff --git a/script/build_language_schema.py b/script/build_language_schema.py index a7142fa8b5e..921ee9d3d70 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -119,7 +119,15 @@ from esphome.util import Registry # noqa: E402 def sort_obj(obj): if isinstance(obj, dict): - return {k: sort_obj(v) for k, v in sorted(obj.items(), key=lambda x: str(x[0]))} + is_enum = obj.get(S_TYPE) == "enum" + result = {} + for k, v in sorted(obj.items(), key=lambda x: str(x[0])): + if is_enum and k == "values" and isinstance(v, dict): + # Preserve source order of enum options + result[k] = {vk: sort_obj(vv) for vk, vv in v.items()} + else: + result[k] = sort_obj(v) + return result if isinstance(obj, list): return [sort_obj(item) for item in obj] return obj diff --git a/tests/script/test_build_language_schema.py b/tests/script/test_build_language_schema.py new file mode 100644 index 00000000000..59b8c7484b2 --- /dev/null +++ b/tests/script/test_build_language_schema.py @@ -0,0 +1,98 @@ +"""Unit tests for script/build_language_schema.py.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +SCRIPT_PATH = ( + Path(__file__).resolve().parent.parent.parent + / "script" + / "build_language_schema.py" +) + + +def _extract_sort_obj(): + # build_language_schema.py runs argparse, loads every component, and + # calls build_schema() at import time, so a plain import isn't viable + # in a unit test. Pull just the pure helper out via AST instead. + tree = ast.parse(SCRIPT_PATH.read_text()) + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == "sort_obj": + namespace: dict = {"S_TYPE": "type"} + module = ast.Module(body=[node], type_ignores=[]) + exec(compile(module, str(SCRIPT_PATH), "exec"), namespace) + return namespace["sort_obj"] + raise AssertionError("sort_obj not found in build_language_schema.py") + + +sort_obj = _extract_sort_obj() + + +def test_sort_obj_sorts_dict_keys() -> None: + result = sort_obj({"b": 1, "a": 2, "c": 3}) + assert list(result.keys()) == ["a", "b", "c"] + + +def test_sort_obj_sorts_nested_dicts() -> None: + result = sort_obj({"outer": {"z": 1, "a": 2}}) + assert list(result["outer"].keys()) == ["a", "z"] + + +def test_sort_obj_preserves_enum_values_order() -> None: + config = { + "type": "enum", + "values": { + "2MB": None, + "4MB": None, + "8MB": None, + "16MB": None, + "32MB": None, + }, + } + result = sort_obj(config) + assert list(result["values"].keys()) == ["2MB", "4MB", "8MB", "16MB", "32MB"] + + +def test_sort_obj_sorts_non_enum_values_key() -> None: + config = {"type": "schema", "values": {"z": 1, "a": 2}} + result = sort_obj(config) + assert list(result["values"].keys()) == ["a", "z"] + + +def test_sort_obj_sorts_other_keys_in_enum() -> None: + config = { + "type": "enum", + "default": "4MB", + "key": "Optional", + "values": {"2MB": None, "4MB": None}, + } + result = sort_obj(config) + assert list(result.keys()) == ["default", "key", "type", "values"] + assert list(result["values"].keys()) == ["2MB", "4MB"] + + +def test_sort_obj_recurses_into_enum_value_entries() -> None: + config = { + "type": "enum", + "values": { + "esp32": {"name": "ESP32", "docs": "Original"}, + "esp32-c3": {"name": "ESP32-C3", "docs": "RISC-V"}, + }, + } + result = sort_obj(config) + assert list(result["values"].keys()) == ["esp32", "esp32-c3"] + assert list(result["values"]["esp32"].keys()) == ["docs", "name"] + + +def test_sort_obj_handles_lists() -> None: + result = sort_obj([{"b": 1, "a": 2}, {"d": 3, "c": 4}]) + assert list(result[0].keys()) == ["a", "b"] + assert list(result[1].keys()) == ["c", "d"] + + +def test_sort_obj_passes_through_scalars() -> None: + assert sort_obj("hello") == "hello" + assert sort_obj(42) == 42 + assert sort_obj(None) is None + assert sort_obj(True) is True From c511dddf2a3ccaf26068fe0d9db043f2c326a225 Mon Sep 17 00:00:00 2001 From: tomaszduda23 <tomaszduda23@gmail.com> Date: Tue, 12 May 2026 20:59:54 +0200 Subject: [PATCH 1943/2056] [core] allow defining run_compile in external_components (#16179) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 988f28a55fe..561391708ee 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -737,7 +737,11 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) - if CORE.using_toolchain_esp_idf: + module = importlib.import_module("esphome.components." + CORE.target_platform) + platform_run_compile = getattr(module, "run_compile", None) + if platform_run_compile is not None and platform_run_compile(args, config): + pass + elif CORE.using_toolchain_esp_idf: from esphome.espidf import toolchain rc = toolchain.run_compile(config, CORE.verbose) From 57893a8eb1c64e0f58f54aedc3e475f1ddec8888 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:37:31 +0000 Subject: [PATCH 1944/2056] Bump aioesphomeapi from 44.23.0 to 44.24.2 (#16376) Signed-off-by: dependabot[bot] <support@github.com> 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 dd539d4f867..c0350518900 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==44.23.0 +aioesphomeapi==44.24.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 34f69e0d6ef69fe27892e1f934a2e7859fd20622 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 12 May 2026 14:42:23 -0500 Subject: [PATCH 1945/2056] [ci] Comment on PRs that touch the legacy dashboard (#16378) --- .../dashboard-deprecation-comment.yml | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/dashboard-deprecation-comment.yml diff --git a/.github/workflows/dashboard-deprecation-comment.yml b/.github/workflows/dashboard-deprecation-comment.yml new file mode 100644 index 00000000000..e15c61df5ea --- /dev/null +++ b/.github/workflows/dashboard-deprecation-comment.yml @@ -0,0 +1,113 @@ +name: Add Dashboard Deprecation Comment + +on: + pull_request_target: + types: [opened, synchronize] + +# All API calls (pulls.listFiles + issues.{list,create,update}Comment) are performed with +# the App token minted below, so the workflow's GITHUB_TOKEN does not need any scopes. +permissions: {} + +jobs: + dashboard-deprecation-comment: + name: Dashboard deprecation comment + runs-on: ubuntu-latest + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # pulls.listFiles + issues.{list,create,update}Comment on PRs. For PR resources + # the issues.*Comment APIs require the pull-requests scope, not issues. + permission-pull-requests: write + + - name: Add dashboard deprecation comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const commentMarker = "<!-- This comment was generated automatically by the dashboard-deprecation-comment workflow. -->"; + + const commentBody = `Thanks for opening this PR! + + Heads up: the legacy ESPHome dashboard (\`esphome/dashboard/\` and \`tests/dashboard/\`) is **deprecated** and is being replaced by [ESPHome Device Builder](https://github.com/esphome/device-builder). We are not adding new features to the legacy dashboard and it will eventually be removed from this repository. + + What this means for your PR: + + - **New features / enhancements**: please port the change to [esphome/device-builder](https://github.com/esphome/device-builder) instead. We are unlikely to review or merge new dashboard features here. + - **Bug fixes**: small fixes may still be considered, but please check first whether the same issue exists in Device Builder, where the fix will have a longer life. + - **Security issues**: please do not file a public PR. Report privately via [GitHub security advisories](https://github.com/esphome/esphome/security/advisories/new) so we can coordinate a fix. + + We appreciate the contribution and apologize for the friction; flagging this early so your time isn't spent on a change that may not land. + + --- + (Added by the PR bot) + + ${commentMarker}`; + + async function getDashboardChanges(github, owner, repo, prNumber) { + const changedFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner: owner, + repo: repo, + pull_number: prNumber, + per_page: 100, + } + ); + + return changedFiles.filter(file => + file.filename.startsWith('esphome/dashboard/') || + file.filename.startsWith('tests/dashboard/') + ); + } + + async function findBotComment(github, owner, repo, prNumber) { + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: owner, + repo: repo, + issue_number: prNumber, + per_page: 100, + } + ); + + return comments.find(comment => + comment.body.includes(commentMarker) && comment.user.type === "Bot" + ); + } + + const prNumber = context.payload.pull_request.number; + const { owner, repo } = context.repo; + + const dashboardChanges = await getDashboardChanges(github, owner, repo, prNumber); + const existingComment = await findBotComment(github, owner, repo, prNumber); + + if (dashboardChanges.length === 0) { + // PR doesn't (or no longer) touches the legacy dashboard. If we previously + // commented (e.g. files were removed in a later push), leave the comment in + // place for history rather than thrash on edit/delete. + return; + } + + if (existingComment) { + if (existingComment.body === commentBody) { + return; + } + await github.rest.issues.updateComment({ + owner: owner, + repo: repo, + comment_id: existingComment.id, + body: commentBody, + }); + } else { + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: prNumber, + body: commentBody, + }); + } From f54480ec48abaec9218caef3a6b8b8a4086f0e8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Tue, 12 May 2026 15:20:25 -0500 Subject: [PATCH 1946/2056] [rp2040] Bump arduino-pico framework to 5.6.0 (#16375) --- .clang-tidy.hash | 2 +- esphome/components/rp2040/__init__.py | 6 +++--- esphome/components/rp2040/boards.py | 18 ++++++++++++++++++ platformio.ini | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index da2e8632817..77b4f5323fa 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -96c95feaa60831da5f43e3c6a7c7a3a237e17c5d12995a730dbc3884c8dcd11c +593fd53fa09944a59af3f38521e31d87fe10b60326b8d82bb76413c5149b312c diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 81809c05519..862d532645a 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -139,7 +139,7 @@ def _parse_platform_version(value): # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 1) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 6, 0) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags @@ -149,8 +149,8 @@ RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460" def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 5, 1), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(5, 5, 1), None), + "dev": (cv.Version(5, 6, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(5, 6, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index aac12eae5ae..1f2b3a93f43 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -457,6 +457,19 @@ RP2040_BOARD_PINS = { "SS": 17, "TX": 12, }, + "challenger_2350_nbiot": { + "LED": 15, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 17, + "TX": 12, + }, "challenger_2350_wifi6_ble5": { "LED": 7, "MISO": 16, @@ -1711,6 +1724,11 @@ BOARDS = { "mcu": "rp2350", "max_pin": 47, }, + "challenger_2350_nbiot": { + "name": "iLabs Challenger 2350 NB-IoT", + "mcu": "rp2350", + "max_pin": 47, + }, "challenger_2350_wifi6_ble5": { "name": "iLabs Challenger 2350 WiFi/BLE", "mcu": "rp2350", diff --git a/platformio.ini b/platformio.ini index 42b74007793..8a89f96b39a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -193,7 +193,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git#v1.4.0-gcc14-arduinopico460 platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.5.1/rp2040-5.5.1.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/5.6.0/rp2040-5.6.0.zip framework = arduino lib_deps = From ee72efa76094edfd7a1bc28c2e98aba228282437 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 16:27:25 -0400 Subject: [PATCH 1947/2056] [sendspin] Fix client_id MAC mismatch with ethernet (#16331) --- esphome/components/sendspin/sendspin_hub.cpp | 19 +++++++++++++++++-- esphome/components/sendspin/sendspin_hub.h | 8 +++++++- .../sendspin/test-ethernet.esp32-idf.yaml | 9 +++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/components/sendspin/test-ethernet.esp32-idf.yaml diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index d27c5672eb6..04426b8b1da 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -3,6 +3,9 @@ #ifdef USE_ESP32 #include "esphome/components/network/util.h" +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -63,7 +66,7 @@ void SendspinHub::dump_config() { "Sendspin Hub:\n" " Client ID: %s\n" " Task stack in PSRAM: %s", - get_mac_address_pretty_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_)); + get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_)); } // --- Delegating methods --- @@ -89,11 +92,23 @@ void SendspinHub::update_state(sendspin::SendspinClientState state) { } } +const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) { + // The server matches client_id against the L2 source MAC of the device's multicast traffic. + // ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the + // eFuse base MAC when ethernet is the active interface. +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) { + return ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf); + } +#endif + return get_mac_address_pretty_into_buffer(buf); +} + sendspin::SendspinClientConfig SendspinHub::build_client_config_() { sendspin::SendspinClientConfig config; char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - config.client_id = get_mac_address_pretty_into_buffer(mac_buf); + config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf); config.name = App.get_friendly_name(); config.product_name = App.get_name(); config.manufacturer = "ESPHome"; diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 12fbf156ea6..c6b1ed97f74 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -35,7 +35,9 @@ namespace esphome::sendspin_ { /// without each subcomponent having to pick a priority independently. Children run /// one step later than hub so they can assume hub's setup() has already completed. namespace sendspin_priority { -inline constexpr float HUB = esphome::setup_priority::PROCESSOR; +// AFTER_WIFI so the hub runs after the wifi/ethernet drivers are up and we can read the active +// interface's MAC for client_id. +inline constexpr float HUB = esphome::setup_priority::AFTER_WIFI; inline constexpr float CHILD = HUB - 1.0f; } // namespace sendspin_priority @@ -149,6 +151,10 @@ class SendspinHub final : public Component, /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. sendspin::SendspinClientConfig build_client_config_(); + /// @brief Writes the active network interface's MAC into @p buf and returns its data pointer. + /// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi). + static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf); + // --- SendspinClientListener overrides --- void on_group_update(const sendspin::GroupUpdateObject &group) override; diff --git a/tests/components/sendspin/test-ethernet.esp32-idf.yaml b/tests/components/sendspin/test-ethernet.esp32-idf.yaml new file mode 100644 index 00000000000..069e397d992 --- /dev/null +++ b/tests/components/sendspin/test-ethernet.esp32-idf.yaml @@ -0,0 +1,9 @@ +ethernet: + type: OPENETH + +psram: + mode: quad + +sendspin: + id: sendspin_hub_id + task_stack_in_psram: true From 66e4a1dfa831ca497028f3cc2a7bddcc6865c740 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Tue, 12 May 2026 22:39:21 +0200 Subject: [PATCH 1948/2056] [mitsubishi_cn105] Add C++ API for setting/clearing remote room temperature (#15558) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- .../components/mitsubishi_cn105/climate.py | 71 ++++++++- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 91 ++++++++--- .../mitsubishi_cn105/mitsubishi_cn105.h | 33 ++-- .../mitsubishi_cn105_climate.h | 22 ++- .../climate/mitsubishi_cn105_tests.cpp | 147 ++++++++++++++++++ tests/components/mitsubishi_cn105/common.h | 3 + tests/components/mitsubishi_cn105/common.yaml | 10 ++ 7 files changed, 338 insertions(+), 39 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/climate.py b/esphome/components/mitsubishi_cn105/climate.py index 7fa6825ea60..cc44494d894 100644 --- a/esphome/components/mitsubishi_cn105/climate.py +++ b/esphome/components/mitsubishi_cn105/climate.py @@ -1,8 +1,11 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import climate, uart import esphome.config_validation as cv -from esphome.const import CONF_UPDATE_INTERVAL -from esphome.types import ConfigType +from esphome.const import CONF_ID, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL +from esphome.core import ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType, TemplateArgsType DEPENDENCIES = ["uart"] AUTO_LOAD = ["climate"] @@ -19,6 +22,18 @@ MitsubishiCN105Climate = mitsubishi_ns.class_( uart.UARTDevice, ) +SetRemoteTemperatureAction = mitsubishi_ns.class_( + "SetRemoteTemperatureAction", + automation.Action, + cg.Parented.template(MitsubishiCN105Climate), +) + +ClearRemoteTemperatureAction = mitsubishi_ns.class_( + "ClearRemoteTemperatureAction", + automation.Action, + cg.Parented.template(MitsubishiCN105Climate), +) + CONFIG_SCHEMA = ( climate.climate_schema(MitsubishiCN105Climate) .extend(uart.UART_DEVICE_SCHEMA) @@ -53,3 +68,55 @@ async def to_code(config: ConfigType) -> None: config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL] ) ) + + +@automation.register_action( + "climate.mitsubishi_cn105.set_remote_temperature", + SetRemoteTemperatureAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Climate), + cv.Required(CONF_TEMPERATURE): cv.templatable( + cv.All( + cv.temperature, + cv.Range(min=8.0, max=39.5), + ) + ), + } + ), + synchronous=True, +) +async def set_remote_temperature_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + temperature = await cg.templatable(config[CONF_TEMPERATURE], args, float) + cg.add(var.set_temperature(temperature)) + + return var + + +@automation.register_action( + "climate.mitsubishi_cn105.clear_remote_temperature", + ClearRemoteTemperatureAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Climate), + } + ), + synchronous=True, +) +async def clear_remote_temperature_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index f04a5906c16..2a173997f3f 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -1,3 +1,4 @@ +#include <algorithm> #include <array> #include <cmath> #include <numeric> @@ -92,6 +93,7 @@ void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } bool MitsubishiCN105::update() { if (const auto start = this->status_update_start_ms_) { if (this->pending_updates_.any()) { + this->status_update_wait_credit_ms_ = std::min(this->update_interval_ms_, get_loop_time_ms() - *start); this->cancel_waiting_and_transition_to_(State::APPLYING_SETTINGS); return false; } @@ -105,6 +107,7 @@ bool MitsubishiCN105::update() { if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { this->write_timeout_start_ms_.reset(); this->frame_parser_.reset(); + this->status_update_wait_credit_ms_ = 0; this->set_state_(State::READ_TIMEOUT); return false; } @@ -191,14 +194,14 @@ void MitsubishiCN105::did_transition_(State to) { } case State::SCHEDULE_NEXT_STATUS_UPDATE: - this->status_update_start_ms_ = get_loop_time_ms(); + this->status_update_start_ms_ = get_loop_time_ms() - this->status_update_wait_credit_ms_; + this->status_update_wait_credit_ms_ = 0; this->current_status_msg_type_ = STATUS_MSG_SETTINGS; this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); break; case State::APPLYING_SETTINGS: this->apply_settings_(); - this->pending_updates_.clear(); break; case State::SETTINGS_APPLIED: @@ -309,21 +312,21 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) return false; } - if (!this->pending_updates_.has(UpdateFlag::POWER)) { + if (!this->pending_updates_.contains(UpdateFlag::POWER)) { this->status_.power_on = payload[2] != 0; } this->use_temperature_encoding_b_ = payload[10] != 0; - if (!this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { + if (!this->pending_updates_.contains(UpdateFlag::TEMPERATURE)) { this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET); } - if (!this->pending_updates_.has(UpdateFlag::MODE)) { + if (!this->pending_updates_.contains(UpdateFlag::MODE)) { const bool i_see = payload[3] > 0x08; this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); } - if (!this->pending_updates_.has(UpdateFlag::FAN)) { + if (!this->pending_updates_.contains(UpdateFlag::FAN)) { this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); } @@ -342,6 +345,27 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz return true; } +void MitsubishiCN105::set_remote_temperature(float temperature) { + if (std::isnan(temperature)) { + ESP_LOGD(TAG, "Ignoring NaN remote temperature"); + return; + } + if (temperature < 8.0f || temperature > 39.5f) { + ESP_LOGD(TAG, "Ignoring out-of-range remote temperature: %.1f", temperature); + return; + } + this->set_remote_temperature_half_deg_(static_cast<uint8_t>(std::round(temperature * 2.0f))); +} + +void MitsubishiCN105::clear_remote_temperature() { + this->set_remote_temperature_half_deg_(REMOTE_TEMPERATURE_DISABLED); +} + +void MitsubishiCN105::set_remote_temperature_half_deg_(uint8_t temperature_half_deg) { + this->remote_temperature_half_deg_ = temperature_half_deg; + this->pending_updates_.set(UpdateFlag::REMOTE_TEMPERATURE); +} + void MitsubishiCN105::set_power(bool power_on) { this->status_.power_on = power_on; this->pending_updates_.set(UpdateFlag::POWER); @@ -377,30 +401,47 @@ void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { } void MitsubishiCN105::apply_settings_() { - std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {0x01}; + std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{}; - if (this->pending_updates_.has(UpdateFlag::POWER)) { - payload[1] |= 0x01; - payload[3] = this->status_.power_on ? 0x01 : 0x00; - } - - if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { - payload[1] |= 0x04; - if (this->use_temperature_encoding_b_) { - payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128); + // Apply all other pending settings first; handle REMOTE_TEMPERATURE last + if (this->pending_updates_.contains_only(UpdateFlag::REMOTE_TEMPERATURE)) { + payload[0] = 0x07; + if (this->remote_temperature_half_deg_ == REMOTE_TEMPERATURE_DISABLED) { + payload[3] = 0x80; } else { - payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature)); + payload[1] = 0x01; + payload[2] = static_cast<uint8_t>(this->remote_temperature_half_deg_ - 16); + payload[3] = static_cast<uint8_t>(this->remote_temperature_half_deg_ + 128); + } + this->pending_updates_.clear(UpdateFlag::REMOTE_TEMPERATURE); + } else { + payload[0] = 0x01; + if (this->pending_updates_.contains(UpdateFlag::POWER)) { + payload[1] |= 0x01; + payload[3] = this->status_.power_on ? 0x01 : 0x00; } - } - if (this->pending_updates_.has(UpdateFlag::MODE) && - reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) { - payload[1] |= 0x02; - } + if (this->pending_updates_.contains(UpdateFlag::TEMPERATURE)) { + payload[1] |= 0x04; + if (this->use_temperature_encoding_b_) { + payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128); + } else { + payload[5] = + static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature)); + } + } - if (this->pending_updates_.has(UpdateFlag::FAN) && - reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) { - payload[1] |= 0x08; + if (this->pending_updates_.contains(UpdateFlag::MODE) && + reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) { + payload[1] |= 0x02; + } + + if (this->pending_updates_.contains(UpdateFlag::FAN) && + reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) { + payload[1] |= 0x08; + } + + this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN); } this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload)); diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 68d98bf6d9d..52b78efccda 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -2,6 +2,7 @@ #include <optional> #include "esphome/components/uart/uart.h" +#include "esphome/core/finite_set_mask.h" namespace esphome::mitsubishi_cn105 { @@ -60,6 +61,8 @@ class MitsubishiCN105 { void set_target_temperature(float target_temperature); void set_mode(Mode mode); void set_fan_mode(FanMode fan_mode); + void set_remote_temperature(float temperature); + void clear_remote_temperature(); protected: enum class State : uint8_t { @@ -91,20 +94,25 @@ class MitsubishiCN105 { }; enum class UpdateFlag : uint8_t { - TEMPERATURE = 1 << 0, - POWER = 1 << 1, - MODE = 1 << 2, - FAN = 1 << 3, + TEMPERATURE = 0, + POWER = 1, + MODE = 2, + FAN = 3, + REMOTE_TEMPERATURE = 4, }; struct UpdateFlags { - void set(UpdateFlag f) { flags_ |= static_cast<uint8_t>(f); } - void clear() { flags_ = 0; } - bool any() const { return flags_ != 0; } - bool has(UpdateFlag f) const { return (flags_ & static_cast<uint8_t>(f)) != 0; } + template<typename... Flags> void set(Flags... flags) { (this->mask_.insert(flags), ...); } + template<typename... Flags> void clear(Flags... flags) { (this->mask_.erase(flags), ...); } + bool any() const { return !this->mask_.empty(); } + bool contains(UpdateFlag flag) const { return this->mask_.count(flag); } + bool contains_only(UpdateFlag flag) const { return this->mask_.get_mask() == Mask{flag}.get_mask(); } protected: - uint8_t flags_{0}; + using Mask = + FiniteSetMask<UpdateFlag, DefaultBitPolicy<UpdateFlag, static_cast<int>(UpdateFlag::REMOTE_TEMPERATURE) + 1>>; + + Mask mask_; }; void set_state_(State new_state); @@ -119,12 +127,14 @@ class MitsubishiCN105 { void cancel_waiting_and_transition_to_(State state); bool should_request_room_temperature_() const; void apply_settings_(); + void set_remote_temperature_half_deg_(uint8_t temperature_half_deg); template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); static const LogString *state_to_string(State state); uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; + uint32_t status_update_wait_credit_ms_{0}; uint32_t room_temperature_min_interval_ms_{60000}; std::optional<uint32_t> write_timeout_start_ms_; std::optional<uint32_t> status_update_start_ms_; @@ -133,8 +143,11 @@ class MitsubishiCN105 { State state_{State::NOT_CONNECTED}; UpdateFlags pending_updates_; bool use_temperature_encoding_b_{false}; - uint8_t current_status_msg_type_{0}; FrameParser frame_parser_; + uint8_t current_status_msg_type_{0}; + + static constexpr uint8_t REMOTE_TEMPERATURE_DISABLED = 0; + uint8_t remote_temperature_half_deg_{REMOTE_TEMPERATURE_DISABLED}; }; } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h index eee4c209669..e09158bfcff 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" @@ -18,8 +19,11 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public climate::ClimateTraits traits() override; void control(const climate::ClimateCall &call) override; - void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } - void set_current_temperature_min_interval(uint32_t ms) { hp_.set_room_temperature_min_interval(ms); } + void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); } + void set_current_temperature_min_interval(uint32_t ms) { this->hp_.set_room_temperature_min_interval(ms); } + + void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); } + void clear_remote_temperature() { this->hp_.clear_remote_temperature(); } protected: void apply_values_(); @@ -27,4 +31,18 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public MitsubishiCN105 hp_; }; +template<typename... Ts> +class SetRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> { + public: + TEMPLATABLE_VALUE(float, temperature) + + void play(const Ts &...x) override { this->parent_->set_remote_temperature(this->temperature_.value(x...)); } +}; + +template<typename... Ts> +class ClearRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> { + public: + void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); } +}; + } // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index 86faaeac784..7615b62d03f 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -375,14 +375,22 @@ TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) { TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { auto ctx = TestContext{}; + ctx.sut.set_update_interval(2000); + ctx.sut.set_current_time(5000); + // Waiting for next scheduled status update ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); // Nothing to do in update (rx empty, no timeout) + ctx.sut.set_current_time(5500); ASSERT_FALSE(ctx.sut.update()); EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); // Write new values ctx.sut.use_temperature_encoding_b_ = true; @@ -392,11 +400,52 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); // Waiting for next status update must be interrupted and new values send to AC + ctx.sut.set_current_time(6000); ASSERT_FALSE(ctx.sut.update()); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB)); + // Write ACK response + ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); + ctx.sut.set_current_time(6500); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{6500 - 1000}); + EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); +} + +TEST(MitsubishiCN105Tests, SetAndClearRemoteRoomTemp) { + auto ctx = TestContext{}; + + // Set remote temperature + ctx.sut.set_remote_temperature(28.5f); + + ctx.sut.state_ = TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE; + ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x01, 0x29, 0xB9, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94)); + + // Write ACK response + ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.uart.tx.clear(); + + // Clear remote temperature + ctx.sut.clear_remote_temperature(); + + ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7)); + // Write ACK response ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); @@ -404,4 +453,102 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); } +TEST(MitsubishiCN105Tests, ApplyQueuedSettingsThenRemoteRoomTempInSecondWrite) { + auto ctx = TestContext{}; + + // Queue normal settings plus remote temperature together. + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_power(false); + ctx.sut.set_target_temperature(25.0f); + ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT); + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); + ctx.sut.set_remote_temperature(28.5f); + + // First apply sends only the normal settings write. + ctx.sut.state_ = TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE; + ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB)); + EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::POWER)); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::TEMPERATURE)); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::MODE)); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::FAN)); + + // ACK the first write. Remote temperature should still be pending afterward. + ctx.uart.tx.clear(); + ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); + ASSERT_FALSE(ctx.sut.update()); + + EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); + + // The next apply sends the remote-temperature packet and clears the last pending flag. + ctx.uart.tx.clear(); + ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x01, 0x29, 0xB9, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94)); + EXPECT_FALSE(ctx.sut.pending_updates_.any()); +} + +TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) { + auto ctx = TestContext{}; + ctx.sut.set_update_interval(2000); + ctx.sut.set_current_time(5000); + + // Start in the scheduled status update wait state. + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + ASSERT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); + + // Interrupt that wait with a write so credit is accumulated. + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_power(false); + ctx.sut.set_target_temperature(25.0f); + ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT); + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); + ctx.sut.set_current_time(6000); + ASSERT_FALSE(ctx.sut.update()); + ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); + ASSERT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000); + + // Do not ACK the write. Advance time far enough to force timeout/reconnect + // handling and verify that stale wait credit is cleared during recovery. + ctx.sut.set_current_time(36000); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_NE(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); + EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); +} + +TEST(MitsubishiCN105Tests, SetOutOfRangeRemoteRoomTempIsIgnored) { + auto ctx = TestContext{}; + + ctx.sut.set_remote_temperature(7.0f); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); + + ctx.sut.set_remote_temperature(40.0f); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); + + ctx.sut.set_remote_temperature(NAN); + EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); +} + +TEST(MitsubishiCN105Tests, SetMinRemoteRoomTemp) { + auto ctx = TestContext{}; + ctx.sut.set_remote_temperature(8.0f); + EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); +} + +TEST(MitsubishiCN105Tests, SetMaxRemoteRoomTemp) { + auto ctx = TestContext{}; + ctx.sut.set_remote_temperature(39.5f); + EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE)); +} + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index 0862d64fa7e..d0fdca1ea5a 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -42,10 +42,13 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { public: using MitsubishiCN105::MitsubishiCN105; using MitsubishiCN105::State; + using MitsubishiCN105::UpdateFlag; using MitsubishiCN105::state_; using MitsubishiCN105::write_timeout_start_ms_; using MitsubishiCN105::status_update_start_ms_; using MitsubishiCN105::use_temperature_encoding_b_; + using MitsubishiCN105::status_update_wait_credit_ms_; + using MitsubishiCN105::pending_updates_; void set_state(State s) { this->set_state_(s); } void apply_settings() { this->apply_settings_(); } diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml index e885ceef817..4b64f51261f 100644 --- a/tests/components/mitsubishi_cn105/common.yaml +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -1,4 +1,14 @@ climate: - platform: mitsubishi_cn105 + id: ac name: "AC Test" uart_id: uart_bus + +esphome: + on_boot: + then: + - climate.mitsubishi_cn105.set_remote_temperature: + id: ac + temperature: 22.0 + - climate.mitsubishi_cn105.clear_remote_temperature: + id: ac From b512cc42a8cd04492049b87e23bfedb0062e5987 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 16:42:44 -0400 Subject: [PATCH 1949/2056] [sendspin] Updates sendspin-cpp to v0.5.0 (#16380) --- esphome/components/sendspin/__init__.py | 7 +++++-- .../sendspin/media_source/sendspin_media_source.cpp | 8 -------- .../sendspin/media_source/sendspin_media_source.h | 3 --- esphome/idf_component.yml | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 1348bf2bfc3..35280020bab 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -206,12 +206,15 @@ async def to_code(config: ConfigType) -> None: ) # sendspin-cpp library - esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.4.0") + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.5.0") cg.add_define("USE_SENDSPIN", True) # for MDNS data = _get_data() + # The color role is not yet wired up in ESPHome; disable it in the library for now. + esp32.add_idf_sdkconfig_option("CONFIG_SENDSPIN_ENABLE_COLOR", False) + # Configure Sendspin roles based on requested features (ESPHome internally via USE_SENDSPIN_*) # and disable building unused code paths in the sendspin-cpp library (IDF SDKConfig via CONFIG_SENDSPIN_ENABLE_*). if data.artwork_support: @@ -264,7 +267,7 @@ async def to_code(config: ConfigType) -> None: # Library defaults: priority 18 (one above httpd_priority 17 so the decoder is not # starved by the HTTP server during the initial encoded-audio burst at stream start), - # interpolation/decode buffer locations PREFER_EXTERNAL. + # decode buffer location PREFER_EXTERNAL. player_struct_fields = [ ("audio_formats", audio_format_structs), ("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]), diff --git a/esphome/components/sendspin/media_source/sendspin_media_source.cpp b/esphome/components/sendspin/media_source/sendspin_media_source.cpp index 0fdfb01c55f..88ff234e831 100644 --- a/esphome/components/sendspin/media_source/sendspin_media_source.cpp +++ b/esphome/components/sendspin/media_source/sendspin_media_source.cpp @@ -188,14 +188,6 @@ void SendspinMediaSource::on_stream_end() { } } -// THREAD CONTEXT: Main loop (PlayerRoleListener lifecycle callback) -void SendspinMediaSource::on_stream_clear() { - if (this->get_state() != media_source::MediaSourceState::IDLE) { - // Only set to IDLE if we were previously in a non-IDLE state, to avoid duplicate state changes - this->set_state_(media_source::MediaSourceState::IDLE); - } -} - // THREAD CONTEXT: Main loop (PlayerRoleListener callback) void SendspinMediaSource::on_volume_changed(uint8_t volume) { this->request_volume_(volume / 100.0f); } diff --git a/esphome/components/sendspin/media_source/sendspin_media_source.h b/esphome/components/sendspin/media_source/sendspin_media_source.h index 3b31716127c..843578783e0 100644 --- a/esphome/components/sendspin/media_source/sendspin_media_source.h +++ b/esphome/components/sendspin/media_source/sendspin_media_source.h @@ -49,9 +49,6 @@ class SendspinMediaSource : public SendspinChild, /// @brief Called when the audio stream ends (main loop thread). void on_stream_end() override; - /// @brief Called when the audio stream is cleared (main loop thread). - void on_stream_clear() override; - /// @brief Called when volume changes (main loop thread). void on_volume_changed(uint8_t volume) override; diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 45aaa827c87..814a4031c12 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -100,6 +100,6 @@ dependencies: esp32async/asynctcp: version: 3.4.91 sendspin/sendspin-cpp: - version: 0.4.0 + version: 0.5.0 lvgl/lvgl: version: 9.5.0 From 76d34334250ea1539bea23d3d2ce15adb29652d3 Mon Sep 17 00:00:00 2001 From: Cody Cutrer <cody@cutrer.us> Date: Tue, 12 May 2026 14:43:48 -0600 Subject: [PATCH 1950/2056] [cli] Add config-hash command (#15548) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/__main__.py | 17 +++++++++++++++++ tests/unit_tests/test_main.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 561391708ee..bca86729171 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1415,6 +1415,15 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: return 0 +def command_config_hash(args: ArgsProtocol, config: ConfigType) -> int | None: + # generating code might modify config, so it must be done in order to generate + # a hash that will match what was generated when compiling and then running + # on the device + generate_cpp_contents(config) + safe_print(f"0x{CORE.config_hash:08x}") + return 0 + + def command_vscode(args: ArgsProtocol) -> int | None: from esphome import vscode @@ -1950,6 +1959,7 @@ PRE_CONFIG_ACTIONS = { POST_CONFIG_ACTIONS = { "config": command_config, + "config-hash": command_config_hash, "compile": command_compile, "upload": command_upload, "logs": command_logs, @@ -2063,6 +2073,13 @@ def parse_args(argv): "--show-secrets", help="Show secrets in output.", action="store_true" ) + parser_config_hash = subparsers.add_parser( + "config-hash", help="Calculate the hash of the configuration." + ) + parser_config_hash.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs="+" + ) + parser_compile = subparsers.add_parser( "compile", help="Read the configuration and compile a program." ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 0104854e1f5..6ec0069b3a1 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -29,6 +29,7 @@ from esphome.__main__ import ( command_analyze_memory, command_bundle, command_clean_all, + command_config_hash, command_rename, command_run, command_update_all, @@ -3439,6 +3440,33 @@ def test_command_wizard(tmp_path: Path) -> None: mock_wizard.assert_called_once_with(config_file) +def test_command_config_hash( + tmp_path: Path, + capfd: CaptureFixture[str], +) -> None: + """command_config_hash runs codegen then prints CORE.config_hash. + + The printed format must match `0x{config_hash:08x}` used by + generate_build_info_data_cpp so the value can be compared byte-for-byte + against the ESPHOME_CONFIG_HASH embedded in firmware. + """ + setup_core(tmp_path=tmp_path, config={"esphome": {"name": "test"}}) + args = MockArgs() + + # generate_cpp_contents requires real components to be loaded; mock it out + # so this test isolates the command's output contract. The command must + # still call it (codegen can mutate config, which affects the hash). + with patch("esphome.__main__.generate_cpp_contents") as mock_generate: + result = command_config_hash(args, CORE.config) + + assert result == 0 + mock_generate.assert_called_once_with(CORE.config) + + output = strip_ansi_codes(capfd.readouterr().out).strip() + assert re.fullmatch(r"0x[0-9a-f]{8}", output) + assert output == f"0x{CORE.config_hash:08x}" + + def test_command_rename_invalid_characters( tmp_path: Path, capfd: CaptureFixture[str] ) -> None: From 057fc4c1a8e758622e60376f5fe5b965a2124b95 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 May 2026 09:19:27 +1200 Subject: [PATCH 1951/2056] Move AI instructions to AGENTS.md (#16382) --- .github/copilot-instructions.md | 2 +- .ai/instructions.md => AGENTS.md | 0 CLAUDE.md | 2 +- GEMINI.md | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename .ai/instructions.md => AGENTS.md (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a4b2fa310ce..be77ac83a18 120000 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1 +1 @@ -../.ai/instructions.md \ No newline at end of file +../AGENTS.md \ No newline at end of file diff --git a/.ai/instructions.md b/AGENTS.md similarity index 100% rename from .ai/instructions.md rename to AGENTS.md diff --git a/CLAUDE.md b/CLAUDE.md index 49e811ff055..47dc3e3d863 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -.ai/instructions.md \ No newline at end of file +AGENTS.md \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index 49e811ff055..47dc3e3d863 120000 --- a/GEMINI.md +++ b/GEMINI.md @@ -1 +1 @@ -.ai/instructions.md \ No newline at end of file +AGENTS.md \ No newline at end of file From 907ae46aba7f977e47a2b4f04cf898662081dc9e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 12 May 2026 19:13:04 -0400 Subject: [PATCH 1952/2056] [zigbee] Fix init-order and missing-field warnings on native ESP-IDF (#16389) --- .../zigbee/zigbee_attribute_esp32.cpp | 22 +++++++++---------- .../zigbee/zigbee_attribute_esp32.h | 4 ++-- esphome/components/zigbee/zigbee_esp32.cpp | 7 +++--- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.cpp b/esphome/components/zigbee/zigbee_attribute_esp32.cpp index 4d736001715..0a06792c594 100644 --- a/esphome/components/zigbee/zigbee_attribute_esp32.cpp +++ b/esphome/components/zigbee/zigbee_attribute_esp32.cpp @@ -32,10 +32,9 @@ void ZigbeeAttribute::report_(bool has_lock) { return; } if (has_lock or esp_zb_lock_acquire(10 / portTICK_PERIOD_MS)) { - esp_zb_zcl_report_attr_cmd_t cmd = { - .address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT, - .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI, - }; + esp_zb_zcl_report_attr_cmd_t cmd = {}; + cmd.address_mode = ESP_ZB_APS_ADDR_MODE_16_ENDP_PRESENT; + cmd.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_CLI; cmd.zcl_basic_cmd.dst_addr_u.addr_short = 0x0000; cmd.zcl_basic_cmd.dst_endpoint = 1; cmd.zcl_basic_cmd.src_endpoint = this->endpoint_id_; @@ -50,14 +49,13 @@ void ZigbeeAttribute::report_(bool has_lock) { } esp_zb_zcl_reporting_info_t ZigbeeAttribute::get_reporting_info() { - esp_zb_zcl_reporting_info_t reporting_info = { - .direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV, - .ep = this->endpoint_id_, - .cluster_id = this->cluster_id_, - .cluster_role = this->role_, - .attr_id = this->attr_id_, - .manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC, - }; + esp_zb_zcl_reporting_info_t reporting_info = {}; + reporting_info.direction = ESP_ZB_ZCL_CMD_DIRECTION_TO_SRV; + reporting_info.ep = this->endpoint_id_; + reporting_info.cluster_id = this->cluster_id_; + reporting_info.cluster_role = this->role_; + reporting_info.attr_id = this->attr_id_; + reporting_info.manuf_code = ESP_ZB_ZCL_ATTR_NON_MANUFACTURER_SPECIFIC; reporting_info.dst.profile_id = ESP_ZB_AF_HA_PROFILE_ID; reporting_info.u.send_info.min_interval = 10; /*!< Actual minimum reporting interval */ reporting_info.u.send_info.max_interval = 0; /*!< Actual maximum reporting interval */ diff --git a/esphome/components/zigbee/zigbee_attribute_esp32.h b/esphome/components/zigbee/zigbee_attribute_esp32.h index 90a5cf8ff9b..35aa60848f6 100644 --- a/esphome/components/zigbee/zigbee_attribute_esp32.h +++ b/esphome/components/zigbee/zigbee_attribute_esp32.h @@ -37,8 +37,8 @@ class ZigbeeAttribute : public Component { role_(role), attr_id_(attr_id), attr_type_(attr_type), - scale_(scale), - max_size_(max_size) {} + max_size_(max_size), + scale_(scale) {} void loop() override; template<typename T> void add_attr(T value); esp_zb_zcl_reporting_info_t get_reporting_info(); diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index c534a384320..ade9e165720 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -204,10 +204,9 @@ static void esp_zb_task_(void *pvParameters) { void ZigbeeComponent::setup() { global_zigbee = this; - esp_zb_platform_config_t config = { - .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), - .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), - }; + esp_zb_platform_config_t config = {}; + config.radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(); + config.host_config = ESP_ZB_DEFAULT_HOST_CONFIG(); #ifdef USE_WIFI if (esp_coex_wifi_i154_enable() != ESP_OK) { this->mark_failed(); From aec48cf2316f3d20c601757401692c8ecf5733e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 23:19:07 +0000 Subject: [PATCH 1953/2056] Bump aioesphomeapi from 44.24.2 to 45.0.0 (#16391) Signed-off-by: dependabot[bot] <support@github.com> 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 c0350518900..6be6a95fe6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==44.24.2 +aioesphomeapi==45.0.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 9195b9898ee7b8b31e36d0a19d078d6f15b71466 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 12 May 2026 19:20:09 -0400 Subject: [PATCH 1954/2056] [ms8607] Pin humidity i2c_id in test fixture (#16386) --- tests/components/ms8607/common.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/ms8607/common.yaml b/tests/components/ms8607/common.yaml index 09a3f5a617a..ad2a34308b6 100644 --- a/tests/components/ms8607/common.yaml +++ b/tests/components/ms8607/common.yaml @@ -4,6 +4,7 @@ sensor: temperature: name: Temperature humidity: + i2c_id: i2c_bus name: Humidity pressure: name: Pressure From 45a8bd49c383bd8ebba205ec25f5eb763437e79c Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 19:33:03 -0400 Subject: [PATCH 1955/2056] [audio] Add a RingBufferAudioSource (#16314) --- .../audio/audio_transfer_buffer.cpp | 131 ++++++++++++++++++ .../components/audio/audio_transfer_buffer.h | 74 ++++++++++ 2 files changed, 205 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 6ee9e4d28c0..d9ce8060e26 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -207,6 +207,137 @@ void ConstAudioSourceBuffer::consume(size_t bytes) { this->data_start_ += bytes; } +std::unique_ptr<RingBufferAudioSource> RingBufferAudioSource::create( + std::shared_ptr<ring_buffer::RingBuffer> ring_buffer, size_t max_fill_bytes, uint8_t alignment_bytes) { + if (ring_buffer == nullptr || max_fill_bytes == 0 || alignment_bytes == 0 || alignment_bytes > MAX_ALIGNMENT_BYTES) { + return nullptr; + } + return std::unique_ptr<RingBufferAudioSource>( + new RingBufferAudioSource(std::move(ring_buffer), max_fill_bytes, alignment_bytes)); +} + +RingBufferAudioSource::~RingBufferAudioSource() { + if (this->acquired_item_ != nullptr) { + this->ring_buffer_->receive_release(this->acquired_item_); + this->acquired_item_ = nullptr; + } +} + +void RingBufferAudioSource::release_item_() { + if (this->acquired_item_ == nullptr) { + return; + } + if (this->item_trailing_length_ > 0) { + // Copy the trailing sub-frame bytes into the splice buffer before returning the item; the next + // fill() will complete the frame from the head of the next chunk. + std::memcpy(this->splice_buffer_, this->item_trailing_ptr_, this->item_trailing_length_); + this->splice_length_ = this->item_trailing_length_; + this->item_trailing_ptr_ = nullptr; + this->item_trailing_length_ = 0; + } + this->ring_buffer_->receive_release(this->acquired_item_); + this->acquired_item_ = nullptr; +} + +void RingBufferAudioSource::consume(size_t bytes) { + bytes = std::min(bytes, this->current_available_); + this->current_data_ += bytes; + this->current_available_ -= bytes; + // Promotion of queued data is deferred to fill() so callers see new data as a fresh return value + // rather than appearing silently after consume(). When the held item has nothing left depending + // on it (no exposed bytes and no queued region), release it now so the ring buffer can be + // reclaimed by writers even if fill() is never called again. + if (this->current_available_ == 0 && this->queued_length_ == 0) { + this->release_item_(); + } +} + +bool RingBufferAudioSource::has_buffered_data() const { + // splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion + // bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports. + // Counting it separately would strand a drain loop when a stream ends mid-frame and those completion + // bytes never come. + return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0); +} + +size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait, bool /*pre_shift*/) { + if (this->current_available_ > 0) { + // Caller has not finished consuming the current exposure + return 0; + } + + // If a queued region (the aligned remainder of the new chunk after a splice frame) is waiting, + // promote it to the exposed region and report its size as fresh data. + if (this->queued_length_ > 0) { + this->current_data_ = this->queued_data_; + this->current_available_ = this->queued_length_; + this->queued_data_ = nullptr; + this->queued_length_ = 0; + return this->current_available_; + } + + // Nothing exposed and nothing queued: release the previously held item (saving any sub-frame tail + // to splice_buffer_) and acquire a new chunk. + this->release_item_(); + + size_t chunk_length = 0; + void *item = this->ring_buffer_->receive_acquire(chunk_length, this->max_fill_bytes_, ticks_to_wait); + if (item == nullptr) { + return 0; + } + + uint8_t *chunk_data = static_cast<uint8_t *>(item); + bool exposing_splice_frame = false; + + // Complete any pending splice frame from the head of the new chunk. + if (this->splice_length_ > 0) { + const size_t needed = static_cast<size_t>(this->alignment_bytes_) - this->splice_length_; + if (chunk_length < needed) { + // Not enough data to complete the spliced frame yet; absorb everything and wait for more. + std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, chunk_length); + this->splice_length_ += chunk_length; + this->ring_buffer_->receive_release(item); + return 0; + } + std::memcpy(this->splice_buffer_ + this->splice_length_, chunk_data, needed); + chunk_data += needed; + chunk_length -= needed; + this->splice_length_ = 0; + exposing_splice_frame = true; + } + + this->acquired_item_ = item; + + // Split the remaining chunk into its aligned region and a (possibly zero) sub-frame trailing tail. + const size_t trailing = (this->alignment_bytes_ > 1) ? (chunk_length % this->alignment_bytes_) : 0; + const size_t aligned_bytes = chunk_length - trailing; + if (trailing > 0) { + this->item_trailing_ptr_ = chunk_data + aligned_bytes; + this->item_trailing_length_ = trailing; + } + + if (exposing_splice_frame) { + // Expose the spliced frame from splice_buffer_, queuing the chunk's aligned region for the next + // fill() call. + this->current_data_ = this->splice_buffer_; + this->current_available_ = this->alignment_bytes_; + this->queued_data_ = chunk_data; + this->queued_length_ = aligned_bytes; + return this->alignment_bytes_; + } + + if (aligned_bytes == 0) { + // The entire chunk is a sub-frame tail (only possible when alignment exceeds chunk size). Save it + // to the splice buffer and release the item so the next fill() can complete the frame. + this->release_item_(); + return 0; + } + + this->current_data_ = chunk_data; + this->current_available_ = aligned_bytes; + return aligned_bytes; +} + } // namespace esphome::audio #endif diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index 68151bf4e21..d83032582e6 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -214,6 +214,80 @@ class ConstAudioSourceBuffer : public AudioReadableBuffer { size_t length_{0}; }; +/// @brief Zero-copy audio source that reads directly from a ring buffer's internal storage. +/// +/// Optionally enforces a minimum read alignment (e.g. one audio frame). When alignment_bytes > 1, the +/// source transparently stitches frames that straddle the ring buffer's wrap boundary by buffering the +/// trailing partial frame from one chunk and joining it with the head of the next chunk in a small +/// internal splice buffer, so callers always see frame-aligned data. +class RingBufferAudioSource : public AudioReadableBuffer { + public: + /// Maximum supported alignment. Sized to cover 32-bit samples across up to 2 channels (8 bytes). + static constexpr size_t MAX_ALIGNMENT_BYTES = 8; + + /// @brief Creates a new ring-buffer-backed audio source after validating its parameters. + /// @param ring_buffer The ring buffer to read from. Must be non-null. + /// @param max_fill_bytes Soft cap on bytes acquired per fill() call. Must be > 0. + /// @param alignment_bytes Minimum exposed-region alignment in bytes (defaults to 1, i.e. byte-aligned). + /// Pass bytes_per_frame to make every exposed region a whole number of frames. Must be in + /// [1, MAX_ALIGNMENT_BYTES]. + /// @return unique_ptr if parameters are valid, nullptr otherwise + static std::unique_ptr<RingBufferAudioSource> create(std::shared_ptr<ring_buffer::RingBuffer> ring_buffer, + size_t max_fill_bytes, uint8_t alignment_bytes = 1); + + ~RingBufferAudioSource() override; + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->current_data_; } + size_t available() const override { return this->current_available_; } + void consume(size_t bytes) override; + bool has_buffered_data() const override; + size_t fill(TickType_t ticks_to_wait, bool pre_shift) override; + + /// @brief Returns a mutable pointer to the currently exposed audio data. + /// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame + /// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data + /// should be discarded after use, since the underlying storage will be reused on the next fill(). + /// Use only when the caller is the sole consumer of this source. + uint8_t *mutable_data() { return this->current_data_; } + + protected: + /// @brief Constructs a new ring-buffer-backed audio source. Use create() instead, which validates + /// arguments before construction. + explicit RingBufferAudioSource(std::shared_ptr<ring_buffer::RingBuffer> ring_buffer, size_t max_fill_bytes, + uint8_t alignment_bytes) + : ring_buffer_(std::move(ring_buffer)), max_fill_bytes_(max_fill_bytes), alignment_bytes_(alignment_bytes) {} + + /// @brief Releases the currently held ring buffer item, first copying any trailing sub-frame bytes + /// into the splice buffer so they can be stitched with the next chunk. + void release_item_(); + + std::shared_ptr<ring_buffer::RingBuffer> ring_buffer_; + size_t max_fill_bytes_; + + void *acquired_item_{nullptr}; + uint8_t *current_data_{nullptr}; + + // Sub-frame trailing bytes inside the held item that will be copied to splice_buffer_ on release. + uint8_t *item_trailing_ptr_{nullptr}; + + // After the currently-exposed splice frame is consumed, fill() will promote this region (the aligned + // remainder of the new chunk) to the exposed region. queued_length_ == 0 when nothing is queued. + uint8_t *queued_data_{nullptr}; + + // Splice buffer holds the start of a partial frame whose remainder lives at the head of the next + // chunk. While splice_length_ > 0, the buffer is incomplete and waiting for completion bytes. + uint8_t splice_buffer_[MAX_ALIGNMENT_BYTES]; + + size_t current_available_{0}; + size_t queued_length_{0}; + + // item_trailing_length_ and splice_length_ are bounded by MAX_ALIGNMENT_BYTES. + uint8_t alignment_bytes_; + uint8_t item_trailing_length_{0}; + uint8_t splice_length_{0}; +}; + } // namespace esphome::audio #endif From cb2dbcd70de6de08196e3e9b53a29b841c6d2444 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 May 2026 11:37:33 +1200 Subject: [PATCH 1956/2056] [ci] Add validate.*.yaml for config-only component tests (#16384) --- .github/workflows/ci.yml | 38 ++++- AGENTS.md | 18 ++- script/analyze_component_buses.py | 9 +- script/ci-custom.py | 7 +- script/determine-jobs.py | 46 +++++- script/helpers.py | 28 +++- script/split_components_for_ci.py | 11 +- script/test_build_components.py | 59 +++++++- tests/script/test_determine_jobs.py | 227 ++++++++++++++++++++++++++++ tests/script/test_helpers.py | 168 ++++++++++++++++++++ 10 files changed, 589 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf9d474d7a2..ad39b3f3467 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,6 +261,7 @@ jobs: cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }} + validate-only-components: ${{ steps.determine.outputs.validate-only-components }} benchmarks: ${{ steps.determine.outputs.benchmarks }} steps: - name: Check out code from GitHub @@ -305,6 +306,7 @@ jobs: echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT + echo "validate-only-components=$(echo "$output" | jq -c '.validate_only_components')" >> $GITHUB_OUTPUT echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' @@ -775,13 +777,45 @@ jobs: echo "Config validation passed! Starting compilation..." echo "" + # Compute the compile-stage component list. Components whose only + # changes are validate.*.yaml files are config-only -- their source + # and test fixtures didn't move, so rebuilding firmware adds no + # signal. Subtract them from this batch before invoking compile. + validate_only_json='${{ needs.determine-jobs.outputs.validate-only-components }}' + if [ -z "$validate_only_json" ]; then + validate_only_json='[]' + fi + if ! validate_only_csv=$(echo "$validate_only_json" | jq -r 'join(",")'); then + echo "::error::Failed to render validate-only-components as CSV from: $validate_only_json" + exit 1 + fi + if [ -z "$validate_only_csv" ]; then + compile_csv="$components_csv" + else + components_sorted=$(echo "$components_csv" | tr ',' '\n' | sort -u) + validate_sorted=$(echo "$validate_only_csv" | tr ',' '\n' | sort -u) + if ! diff_out=$(comm -23 <(echo "$components_sorted") <(echo "$validate_sorted")); then + echo "::error::Failed to compute compile component subset." + exit 1 + fi + compile_csv=$(echo "$diff_out" | paste -sd ',' -) + skipped=$(comm -12 <(echo "$components_sorted") <(echo "$validate_sorted") | paste -sd ',' -) + if [ -n "$skipped" ]; then + echo "Validate-only components in this batch (skipping compile): $skipped" + fi + fi + # Show disk space before compilation echo "Disk space before compilation:" df -h echo "" - # Run compilation with grouping and isolation - python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv" + if [ -n "$compile_csv" ]; then + # Run compilation with grouping and isolation + python3 script/test_build_components.py -e compile -c "$compile_csv" -f --isolate "$directly_changed_csv" + else + echo "All components in this batch are validate-only -- skipping compile stage." + fi test-native-idf: name: Test components with native ESP-IDF diff --git a/AGENTS.md b/AGENTS.md index 86f554e9cec..2139a2b796d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -398,13 +398,23 @@ This document provides essential context for AI models interacting with this pro │ ├── i2c/ # I2C bus │ └── spi/ # SPI bus └── components/[component]/ - ├── common.yaml # Component-only config (no bus definitions) - ├── test.esp32-idf.yaml - ├── test.esp8266-ard.yaml - └── test.rp2040-ard.yaml + ├── common.yaml # Component-only config (no bus definitions) + ├── test.esp32-idf.yaml # config + compile + ├── test.esp8266-ard.yaml # config + compile + ├── test-variant.esp32-idf.yaml # variant test, config + compile + ├── validate.esp32-idf.yaml # config-only (never compiled) + └── validate-legacy.esp32-idf.yaml # config-only variant ``` Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms. + * **Config-only test files (`validate.*.yaml`):** Use this prefix when a YAML file only needs to exercise schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with `esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`: + - `validate.<platform>.yaml` — base config-only test + - `validate-<variant>.<platform>.yaml` — config-only variant + + Use this for things like deprecated-syntax migration tests, schema edge cases, or platform-specific validation branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and `validate.*.yaml` files. Validate files never participate in bus-grouping; each one runs as its own `esphome config` invocation. + + When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes. + * **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`: ```yaml # test.esp32-idf.yaml — use packages for buses diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 17af7af5771..1d86d5c71ca 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -34,7 +34,7 @@ from typing import Any # Add esphome to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from helpers import BASE_BUS_COMPONENTS +from helpers import BASE_BUS_COMPONENTS, is_validate_only_file from esphome import yaml_util from esphome.config_helpers import Extend, Remove @@ -283,6 +283,13 @@ def analyze_component(component_dir: Path) -> tuple[dict[str, list[str]], bool, # Analyze all YAML files in the component directory for yaml_file in component_dir.glob("*.yaml"): + # validate.*.yaml files are config-only -- they don't compile, so + # their contents must not influence compile-time grouping decisions + # (e.g. a !extend used only to exercise schema validation must not + # disqualify the whole component from being grouped). + if is_validate_only_file(yaml_file): + continue + analysis = analyze_yaml_file(yaml_file) # Track if any file uses extend/remove diff --git a/script/ci-custom.py b/script/ci-custom.py index 25db32105c6..56ca0d0355e 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1068,7 +1068,12 @@ PACKAGE_BUS_RE = re.compile( ) -@lint_content_check(include=["tests/components/*/test.*.yaml"]) +@lint_content_check( + include=[ + "tests/components/*/test.*.yaml", + "tests/components/*/validate.*.yaml", + ] +) def lint_test_package_key_matches_bus(fname, content): """Ensure package keys match the common bus directory name. diff --git a/script/determine-jobs.py b/script/determine-jobs.py index b8f324784d8..57b3c6eb88e 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -29,6 +29,8 @@ The CI workflow uses this information to: - Skip or run downstream esphome/device-builder tests against the PR's Python code - Determine which components to test individually - Decide how to split component tests (if there are many) +- Identify directly-changed components whose only edits are validate.*.yaml files, + so CI can skip the compile stage for them and run config validation only - Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes Usage: @@ -68,6 +70,7 @@ from helpers import ( get_integration_test_files_for_components, get_target_branch, git_ls_files, + is_validate_only_file, parse_test_filename, root_path, ) @@ -600,14 +603,41 @@ def _component_has_tests(component: str) -> bool: """Check if a component has test files. Cached to avoid repeated filesystem operations for the same component. + Validate files (validate.*.yaml) count -- they exercise schema validation + in CI even though they are never compiled. Args: component: Component name to check Returns: - True if the component has test YAML files + True if the component has test or validate YAML files """ - return bool(get_component_test_files(component, all_variants=True)) + return bool( + get_component_test_files(component, all_variants=True, include_validate=True) + ) + + +def _component_change_is_validate_only(component: str, changed: list[str]) -> bool: + """Return True if every changed file for this component is a validate.*.yaml. + + Used to decide whether a directly-changed component can skip the compile + stage in CI. A component qualifies when: + - at least one file under ``tests/components/<component>/`` changed, AND + - no source file under ``esphome/components/<component>/`` changed, AND + - every changed test file is a ``validate.*.yaml`` or + ``validate-*.yaml`` (i.e. no regular ``test.*.yaml`` was touched). + """ + test_prefix = f"tests/components/{component}/" + src_prefix = f"esphome/components/{component}/" + test_changes: list[Path] = [] + for path in changed: + if path.startswith(src_prefix): + return False + if path.startswith(test_prefix): + test_changes.append(Path(path)) + if not test_changes: + return False + return all(is_validate_only_file(p) for p in test_changes) def _select_platform_by_preference( @@ -977,6 +1007,17 @@ def main() -> None: if component not in directly_changed_components ] + # Components whose only changes are validate.*.yaml files can skip the + # compile stage in CI -- their source and test fixtures didn't move, so + # rebuilding firmware adds no signal. Only directly-changed components + # qualify: a component pulled in transitively (because a dependency + # changed) still needs the compile to catch regressions. + validate_only_components = sorted( + component + for component in directly_changed_with_tests + if _component_change_is_validate_only(component, changed) + ) + # Detect components for memory impact analysis (merged config) memory_impact = detect_memory_impact_config(args.branch) @@ -1073,6 +1114,7 @@ def main() -> None: "cpp_unit_tests_run_all": cpp_run_all, "cpp_unit_tests_components": cpp_components, "component_test_batches": component_test_batches, + "validate_only_components": validate_only_components, "benchmarks": run_benchmarks, } diff --git a/script/helpers.py b/script/helpers.py index 7a6d7ecef68..cf82a89f932 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -117,7 +117,7 @@ def get_component_from_path(file_path: str) -> str | None: def get_component_test_files( - component: str, *, all_variants: bool = False + component: str, *, all_variants: bool = False, include_validate: bool = False ) -> list[Path]: """Get test files for a component. @@ -126,6 +126,10 @@ def get_component_test_files( all_variants: If True, returns all test files including variants (test-*.yaml). If False, returns only base test files (test.*.yaml). Default is False. + include_validate: If True, also returns config-only files (validate.*.yaml, + and validate-*.yaml when all_variants is True). These files + are validated with `esphome config` but never compiled. + Default is False. Returns: List of test file paths for the component, or empty list if none exist @@ -136,9 +140,27 @@ def get_component_test_files( if all_variants: # Match both test.*.yaml and test-*.yaml patterns - return list(tests_dir.glob("test[.-]*.yaml")) + files = list(tests_dir.glob("test[.-]*.yaml")) + if include_validate: + files.extend(tests_dir.glob("validate[.-]*.yaml")) + return files # Match only test.*.yaml (base tests) - return list(tests_dir.glob("test.*.yaml")) + files = list(tests_dir.glob("test.*.yaml")) + if include_validate: + files.extend(tests_dir.glob("validate.*.yaml")) + return files + + +def is_validate_only_file(test_file: Path) -> bool: + """Return True if the given path is a config-only validate file. + + Validate files follow the same grammar as test files but with a + ``validate`` prefix instead of ``test``: ``validate.<platform>.yaml`` + or ``validate-<variant>.<platform>.yaml``. They are exercised with + ``esphome config`` only and skipped during compile. + """ + name = test_file.name + return name.startswith("validate.") or name.startswith("validate-") @dataclass(frozen=True) diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index d95cdcbe815..0d10246bb48 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -44,14 +44,21 @@ ALL_PLATFORMS = "all" def has_test_files(component_name: str, tests_dir: Path) -> bool: """Check if a component has test files. + Validate files (validate.*.yaml) count -- a component with only config-only + test files still needs a CI runner for schema validation. + Args: component_name: Name of the component tests_dir: Path to tests/components directory (unused, kept for compatibility) Returns: - True if the component has test.*.yaml or test-*.yaml files + True if the component has test.*.yaml, test-*.yaml, or validate.*.yaml files """ - return bool(get_component_test_files(component_name, all_variants=True)) + return bool( + get_component_test_files( + component_name, all_variants=True, include_validate=True + ) + ) def create_intelligent_batches( diff --git a/script/test_build_components.py b/script/test_build_components.py index 10c5e5463fe..43b71004eb8 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -39,7 +39,11 @@ from script.analyze_component_buses import ( merge_compatible_bus_groups, uses_local_file_references, ) -from script.helpers import get_component_test_files, split_conflicting_groups +from script.helpers import ( + get_component_test_files, + is_validate_only_file, + split_conflicting_groups, +) from script.merge_component_configs import merge_component_configs @@ -83,7 +87,10 @@ def show_disk_space_if_ci(esphome_command: str) -> None: def find_component_tests( - components_dir: Path, component_pattern: str = "*", base_only: bool = False + components_dir: Path, + component_pattern: str = "*", + base_only: bool = False, + include_validate: bool = False, ) -> dict[str, list[Path]]: """Find all component test files. @@ -91,6 +98,8 @@ def find_component_tests( components_dir: Path to tests/components directory component_pattern: Glob pattern for component names base_only: If True, only find base test files (test.*.yaml), not variant files (test-*.yaml) + include_validate: If True, also include config-only files (validate.*.yaml). + These are run with `esphome config` only and never compiled. Returns: Dictionary mapping component name to list of test files @@ -102,7 +111,11 @@ def find_component_tests( continue # Get test files using helper function - test_files = get_component_test_files(comp_dir.name, all_variants=not base_only) + test_files = get_component_test_files( + comp_dir.name, + all_variants=not base_only, + include_validate=include_validate, + ) if test_files: component_tests[comp_dir.name] = test_files @@ -836,12 +849,25 @@ def run_grouped_component_tests( # With grouping: # - 1 build per group (regardless of how many components) # - Individual components still need all their platform builds + # - Validate files of grouped components still run individually + # (they're config-only and bypass the grouped compile, see + # run_individual_component_test), so each adds one more invocation. individual_test_file_count = sum( len(all_tests[comp]) for comp in individual_tests if comp in all_tests ) + grouped_component_set = {c for _, _, comps in groups_to_test for c in comps} + grouped_validate_file_count = sum( + 1 + for comp in grouped_component_set + for test_file in all_tests.get(comp, []) + if is_validate_only_file(test_file) + ) + total_grouped_components = sum(len(comps) for _, _, comps in groups_to_test) - total_builds_with_grouping = len(groups_to_test) + individual_test_file_count + total_builds_with_grouping = ( + len(groups_to_test) + individual_test_file_count + grouped_validate_file_count + ) builds_saved = total_test_files - total_builds_with_grouping print(f"\n{'=' * 80}") @@ -854,6 +880,10 @@ def run_grouped_component_tests( print( f" • {individual_test_file_count} individual builds ({len(individual_tests)} components)" ) + if grouped_validate_file_count: + print( + f" • {grouped_validate_file_count} validate-only invocations for grouped components" + ) if total_test_files > 0: reduction_pct = (builds_saved / total_test_files) * 100 print(f" • Saves {builds_saved} builds ({reduction_pct:.1f}% reduction)") @@ -937,8 +967,13 @@ def run_individual_component_test( tested_components: Set of already tested components test_results: List to append test results """ - # Skip if already tested in a group - if (component, platform_with_version) in tested_components: + # Validate files (validate.*.yaml) are config-only and never participate + # in compile-time bus grouping, so always run them individually even when + # the (component, platform) pair was covered by a group test. + if ( + not is_validate_only_file(test_file) + and (component, platform_with_version) in tested_components + ): return test_result = run_esphome_test( @@ -992,13 +1027,23 @@ def test_components( # Get platform base files platform_bases = get_platform_base_files(build_components_dir) + # Validate files (validate.*.yaml) are config-only -- they exercise + # schema/validation paths but are never compiled. Include them when running + # `config` or `clean`; exclude them under `compile` so they never reach a + # toolchain build. + include_validate = esphome_command != "compile" + # Find all component tests all_tests = {} for pattern in component_patterns: # Skip empty patterns (happens when components list is empty string) if not pattern: continue - all_tests.update(find_component_tests(tests_dir, pattern, base_only)) + all_tests.update( + find_component_tests( + tests_dir, pattern, base_only, include_validate=include_validate + ) + ) # If no components found, build a reference configuration for baseline comparison # Create a synthetic "empty" component test that will build just the base config diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index cc795bc5532..5e2dd670dc1 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -2215,3 +2215,230 @@ def test_should_run_benchmarks_with_branch() -> None: mock_changed.return_value = [] determine_jobs.should_run_benchmarks("release") mock_changed.assert_called_with("release") + + +# --------------------------------------------------------------------------- +# _component_change_is_validate_only +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("component", "changed", "expected"), + [ + # Only a base validate file changed. + ( + "foo", + ["tests/components/foo/validate.esp32-idf.yaml"], + True, + ), + # Only a validate variant changed. + ( + "foo", + ["tests/components/foo/validate-legacy.esp32-idf.yaml"], + True, + ), + # Multiple validate files (all validate). + ( + "foo", + [ + "tests/components/foo/validate.esp32-idf.yaml", + "tests/components/foo/validate-legacy.esp32-idf.yaml", + ], + True, + ), + # Mixed: validate + regular test must NOT be classified as validate-only. + ( + "foo", + [ + "tests/components/foo/validate.esp32-idf.yaml", + "tests/components/foo/test.esp32-idf.yaml", + ], + False, + ), + # Regular test only. + ( + "foo", + ["tests/components/foo/test.esp32-idf.yaml"], + False, + ), + # Source change disqualifies even if a validate file is also touched. + ( + "foo", + [ + "esphome/components/foo/foo.cpp", + "tests/components/foo/validate.esp32-idf.yaml", + ], + False, + ), + # No matching files at all. + ("foo", ["esphome/core/helpers.cpp"], False), + # Filenames merely starting with "validate" but not following the + # grammar must not match (defensive against accidental classification). + ( + "foo", + ["tests/components/foo/validatesomething.yaml"], + False, + ), + # An unrelated component's validate change doesn't affect this one. + ( + "foo", + ["tests/components/bar/validate.esp32-idf.yaml"], + False, + ), + # common.yaml change in the component dir disqualifies. + ( + "foo", + [ + "tests/components/foo/common.yaml", + "tests/components/foo/validate.esp32-idf.yaml", + ], + False, + ), + ], +) +def test_component_change_is_validate_only( + component: str, changed: list[str], expected: bool +) -> None: + """The validate-only classifier rejects anything beyond validate.* edits.""" + assert ( + determine_jobs._component_change_is_validate_only(component, changed) + is expected + ) + + +def test_main_emits_validate_only_components( + mock_determine_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Directly-changed components whose only edits are validate.*.yaml are + listed in `validate_only_components` so CI can skip their compile stage. + """ + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_determine_integration_tests.return_value = (False, []) + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) + + # foo: only validate file changed (qualifies) + # bar: test file changed (does not qualify) + mock_changed_files.return_value = [ + "tests/components/foo/validate.esp32-idf.yaml", + "tests/components/bar/test.esp32-idf.yaml", + ] + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=["foo", "bar"], + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("tests/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: ["foo", "bar"], + ), + patch.object(determine_jobs, "_component_has_tests", return_value=True), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["foo", "bar"]], {}), + ), + ): + determine_jobs.main() + + output = json.loads(capsys.readouterr().out) + assert output["validate_only_components"] == ["foo"] + + +def test_main_validate_only_excludes_transitive_components( + mock_determine_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A component pulled in only as a dependency must NOT be considered + validate-only, even if it has no source changes -- its dependency moved, + so the compile is still required. + """ + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_determine_integration_tests.return_value = (False, []) + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) + + # Only foo's validate file changed directly. bar is a transitive dep. + mock_changed_files.return_value = [ + "tests/components/foo/validate.esp32-idf.yaml", + ] + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=["foo", "bar"], # bar pulled in via dependencies + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("tests/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + # deps=False -> directly_changed = [foo]; deps=True -> [foo, bar] + side_effect=lambda files, deps: ["foo", "bar"] if deps else ["foo"], + ), + patch.object(determine_jobs, "_component_has_tests", return_value=True), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["foo", "bar"]], {}), + ), + ): + determine_jobs.main() + + output = json.loads(capsys.readouterr().out) + # Only foo (directly changed, validate-only). bar is a transitive dep + # and still needs compile despite no source change of its own. + assert output["validate_only_components"] == ["foo"] diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index db0d2908f47..10f258aa830 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1624,3 +1624,171 @@ def test_split_conflicting_groups_preserves_original_signature_for_first_bucket( platform, signature = next(iter(extra)) assert platform == "esp32" assert signature.startswith("i2c__conflict") + + +# --------------------------------------------------------------------------- +# get_component_test_files / is_validate_only_file +# --------------------------------------------------------------------------- + + +@pytest.fixture +def fake_component_tests(tmp_path: Path) -> Path: + """Create a fake tests/components/ tree and return the repo root. + + Layout for component "demo": + test.esp32-idf.yaml + test.esp8266-ard.yaml + test-variant.esp32-idf.yaml + validate.esp32-idf.yaml + validate-legacy.esp32-idf.yaml + + Layout for component "validate_only": + validate.esp32-idf.yaml (only validate files) + + Layout for component "no_tests": + common.yaml (no test/validate files at all) + """ + tests_dir = tmp_path / "tests" / "components" + + demo = tests_dir / "demo" + demo.mkdir(parents=True) + (demo / "test.esp32-idf.yaml").write_text("") + (demo / "test.esp8266-ard.yaml").write_text("") + (demo / "test-variant.esp32-idf.yaml").write_text("") + (demo / "validate.esp32-idf.yaml").write_text("") + (demo / "validate-legacy.esp32-idf.yaml").write_text("") + + validate_only = tests_dir / "validate_only" + validate_only.mkdir(parents=True) + (validate_only / "validate.esp32-idf.yaml").write_text("") + + no_tests = tests_dir / "no_tests" + no_tests.mkdir(parents=True) + (no_tests / "common.yaml").write_text("") + + return tmp_path + + +def _names(paths: list[Path]) -> set[str]: + return {p.name for p in paths} + + +def test_get_component_test_files_default_excludes_validate( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """Default behaviour: only base test.*.yaml; no variants, no validate.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + files = helpers.get_component_test_files("demo") + + assert _names(files) == {"test.esp32-idf.yaml", "test.esp8266-ard.yaml"} + + +def test_get_component_test_files_all_variants_excludes_validate( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """all_variants=True picks up test variants but still skips validate.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + files = helpers.get_component_test_files("demo", all_variants=True) + + assert _names(files) == { + "test.esp32-idf.yaml", + "test.esp8266-ard.yaml", + "test-variant.esp32-idf.yaml", + } + + +def test_get_component_test_files_include_validate_base_only( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """include_validate=True with base-only adds validate.*.yaml only.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + files = helpers.get_component_test_files("demo", include_validate=True) + + assert _names(files) == { + "test.esp32-idf.yaml", + "test.esp8266-ard.yaml", + "validate.esp32-idf.yaml", + } + + +def test_get_component_test_files_include_validate_all_variants( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """include_validate=True with all_variants adds validate variants too.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + files = helpers.get_component_test_files( + "demo", all_variants=True, include_validate=True + ) + + assert _names(files) == { + "test.esp32-idf.yaml", + "test.esp8266-ard.yaml", + "test-variant.esp32-idf.yaml", + "validate.esp32-idf.yaml", + "validate-legacy.esp32-idf.yaml", + } + + +def test_get_component_test_files_validate_only_component( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """A component with only validate files is invisible without the flag.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + assert helpers.get_component_test_files("validate_only") == [] + assert helpers.get_component_test_files("validate_only", all_variants=True) == [] + + files = helpers.get_component_test_files( + "validate_only", all_variants=True, include_validate=True + ) + assert _names(files) == {"validate.esp32-idf.yaml"} + + +def test_get_component_test_files_missing_component( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """Unknown components return an empty list, regardless of flags.""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + assert ( + helpers.get_component_test_files( + "does_not_exist", all_variants=True, include_validate=True + ) + == [] + ) + + +def test_get_component_test_files_component_without_tests( + fake_component_tests: Path, monkeypatch: MonkeyPatch +) -> None: + """A component with only common.yaml and no test/validate files returns [].""" + monkeypatch.setattr(helpers, "root_path", str(fake_component_tests)) + + assert ( + helpers.get_component_test_files( + "no_tests", all_variants=True, include_validate=True + ) + == [] + ) + + +@pytest.mark.parametrize( + ("filename", "expected"), + [ + ("validate.esp32-idf.yaml", True), + ("validate-legacy.esp32-idf.yaml", True), + ("validate.host.yaml", True), + ("test.esp32-idf.yaml", False), + ("test-variant.esp32-idf.yaml", False), + ("common.yaml", False), + # Defensive: a hypothetical name starting with "validate" but not + # following the grammar must not be classified as a validate file. + ("validatesomething.yaml", False), + ], +) +def test_is_validate_only_file(filename: str, expected: bool, tmp_path: Path) -> None: + assert helpers.is_validate_only_file(tmp_path / filename) is expected From 531367d7e16b3928e4fd88c2f2a1a04748aef7ed Mon Sep 17 00:00:00 2001 From: George Galt <george@galtfamily.net> Date: Tue, 12 May 2026 19:47:54 -0400 Subject: [PATCH 1957/2056] [micro_wake_word] Increase INFERENCE_TASK_STACK_SIZE to 8192 for P4 chip (#16390) --- esphome/components/micro_wake_word/micro_wake_word.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index c031a9f269d..6877e9e5df5 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -23,7 +23,13 @@ static const size_t DATA_TIMEOUT_MS = 50; static const uint32_t RING_BUFFER_DURATION_MS = 120; +#ifdef CONFIG_IDF_TARGET_ESP32P4 +// ESP32-P4 PIE-optimized esp-nn kernels (e.g. depthwise_conv_s8_ch1_pie) require +// significantly more stack than other variants, causing stack protection faults at 3072. +static const uint32_t INFERENCE_TASK_STACK_SIZE = 8192; +#else static const uint32_t INFERENCE_TASK_STACK_SIZE = 3072; +#endif static const UBaseType_t INFERENCE_TASK_PRIORITY = 3; enum EventGroupBits : uint32_t { From 8b6cbc9f2b45cba08efef7c416d6ab0667690f40 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 19:58:03 -0400 Subject: [PATCH 1958/2056] [i2s_audio] Speaker uses new RingBufferAudioSource (#16315) Thanks! --- .../i2s_audio/speaker/i2s_audio_spdif.cpp | 34 +++++++++--------- .../speaker/i2s_audio_speaker_standard.cpp | 35 ++++++++++--------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp index d257dd1d8f4..8f67562a777 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp @@ -143,7 +143,11 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info - const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration); + const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1); + // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and + // avoids unnecessary single-frame splices. + const size_t ring_buffer_size = + (this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame; // For SPDIF mode, one DMA buffer = one SPDIF block = 192 PCM frames const uint32_t frames_to_fill_single_dma_buffer = SPDIF_BLOCK_SAMPLES; @@ -151,13 +155,13 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); bool successful_setup = false; - std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = - audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + std::unique_ptr<audio::RingBufferAudioSource> audio_source; - if (transfer_buffer != nullptr) { + { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); - if (temp_ring_buffer.use_count() == 1) { - transfer_buffer->set_source(temp_ring_buffer); + audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, bytes_to_fill_single_dma_buffer, + static_cast<uint8_t>(bytes_per_frame)); + if (audio_source != nullptr) { this->audio_ring_buffer_ = temp_ring_buffer; successful_setup = true; } @@ -297,24 +301,24 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { if (!this->pause_state_) { while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) { - if (transfer_buffer->available() == 0) { - size_t bytes_read = transfer_buffer->transfer_data_from_source(read_timeout_ticks); + if (audio_source->available() == 0) { + size_t bytes_read = audio_source->fill(read_timeout_ticks, false); if (bytes_read == 0) { break; // No upstream data within the read budget; silence-pad the remainder. } - uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; + uint8_t *new_data = audio_source->mutable_data(); this->apply_software_volume_(new_data, bytes_read); this->swap_esp32_mono_samples_(new_data, bytes_read); } const uint32_t frames_still_needed = SPDIF_BLOCK_SAMPLES - real_frames_in_block; const size_t bytes_still_needed = this->current_stream_info_.frames_to_bytes(frames_still_needed); - const size_t bytes_to_feed = std::min(transfer_buffer->available(), bytes_still_needed); + const size_t bytes_to_feed = std::min(audio_source->available(), bytes_still_needed); uint32_t blocks_sent = 0; size_t pcm_consumed = 0; - esp_err_t err = this->spdif_encoder_->write(transfer_buffer->get_buffer_start(), bytes_to_feed, - write_timeout_ticks, &blocks_sent, &pcm_consumed); + esp_err_t err = this->spdif_encoder_->write(audio_source->data(), bytes_to_feed, write_timeout_ticks, + &blocks_sent, &pcm_consumed); if (err != ESP_OK) { // A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer; // resuming would credit the next iteration's bytes against an old block. Bail and @@ -325,7 +329,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { } if (pcm_consumed > 0) { - transfer_buffer->decrease_buffer_length(pcm_consumed); + audio_source->consume(pcm_consumed); real_frames_in_block += this->current_stream_info_.bytes_to_frames(pcm_consumed); } if (blocks_sent > 0) { @@ -387,9 +391,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { this->spdif_encoder_->reset(); } - if (transfer_buffer != nullptr) { - transfer_buffer.reset(); - } + audio_source.reset(); xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index 51f2b225e20..0c8b8be5226 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -44,19 +44,23 @@ void I2SAudioSpeaker::run_speaker_task() { const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_); // The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info - const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration); + const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1); + // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and + // avoids unnecessary single-frame splices. + const size_t ring_buffer_size = + (this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame; const uint32_t frames_to_fill_single_dma_buffer = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); const size_t bytes_to_fill_single_dma_buffer = this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); bool successful_setup = false; - std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = - audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + std::unique_ptr<audio::RingBufferAudioSource> audio_source; - if (transfer_buffer != nullptr) { + { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); - if (temp_ring_buffer.use_count() == 1) { - transfer_buffer->set_source(temp_ring_buffer); + audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, bytes_to_fill_single_dma_buffer, + static_cast<uint8_t>(bytes_per_frame)); + if (audio_source != nullptr) { this->audio_ring_buffer_ = temp_ring_buffer; successful_setup = true; } @@ -129,15 +133,15 @@ void I2SAudioSpeaker::run_speaker_task() { // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; - size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); - uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read; + size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(read_delay), false); + uint8_t *new_data = audio_source->mutable_data(); if (bytes_read > 0) { this->apply_software_volume_(new_data, bytes_read); this->swap_esp32_mono_samples_(new_data, bytes_read); } - if (transfer_buffer->available() == 0) { + if (audio_source->available() == 0) { if (stop_gracefully && tx_dma_underflow) { break; } @@ -150,18 +154,17 @@ void I2SAudioSpeaker::run_speaker_task() { i2s_channel_disable(this->tx_handle_); const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr}; i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this); - i2s_channel_preload_data(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), - &bytes_written); + i2s_channel_preload_data(this->tx_handle_, audio_source->data(), audio_source->available(), &bytes_written); } else { // Audio is already playing, use regular write to add to the DMA buffers - i2s_channel_write(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), - &bytes_written, DMA_BUFFER_DURATION_MS); + i2s_channel_write(this->tx_handle_, audio_source->data(), audio_source->available(), &bytes_written, + DMA_BUFFER_DURATION_MS); } if (bytes_written > 0) { last_data_received_time = millis(); frames_written += this->current_stream_info_.bytes_to_frames(bytes_written); - transfer_buffer->decrease_buffer_length(bytes_written); + audio_source->consume(bytes_written); if (tx_dma_underflow) { tx_dma_underflow = false; @@ -178,9 +181,7 @@ void I2SAudioSpeaker::run_speaker_task() { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); - if (transfer_buffer != nullptr) { - transfer_buffer.reset(); - } + audio_source.reset(); xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); From 7f37ee3c53a1e10a0450f4b3a3a2b44b199489f1 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 19:58:32 -0400 Subject: [PATCH 1959/2056] [mixer] Use RingBufferAudioSource (#16316) --- .../mixer/speaker/mixer_speaker.cpp | 97 ++++++++++--------- .../components/mixer/speaker/mixer_speaker.h | 14 +-- 2 files changed, 59 insertions(+), 52 deletions(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 8dea4560c24..1a995a6edfd 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -182,7 +182,7 @@ void SourceSpeaker::loop() { break; } case speaker::STATE_RUNNING: - if (!this->transfer_buffer_->has_buffered_data() && + if (!this->audio_source_->has_buffered_data() && (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) { // No audio data in buffer waiting to get mixed and no frames are pending playback if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) || @@ -254,15 +254,12 @@ void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); } esp_err_t SourceSpeaker::start_() { - const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_); - if (this->transfer_buffer_.use_count() == 0) { - this->transfer_buffer_ = - audio::AudioSourceTransferBuffer::create(this->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - - if (this->transfer_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - + const size_t bytes_per_frame = this->audio_stream_info_.frames_to_bytes(1); + // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and + // avoids unnecessary single-frame splices. + const size_t ring_buffer_size = + (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame; + if (this->audio_source_.use_count() == 0) { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (!temp_ring_buffer) { temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); @@ -271,9 +268,15 @@ esp_err_t SourceSpeaker::start_() { if (!temp_ring_buffer) { return ESP_ERR_NO_MEM; - } else { - this->transfer_buffer_->set_source(temp_ring_buffer); } + + std::unique_ptr<audio::RingBufferAudioSource> source = audio::RingBufferAudioSource::create( + temp_ring_buffer, this->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS), + static_cast<uint8_t>(bytes_per_frame)); + if (source == nullptr) { + return ESP_ERR_NO_MEM; + } + this->audio_source_ = std::move(source); } return this->parent_->start(this->audio_stream_info_); @@ -284,7 +287,7 @@ void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); } void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); } bool SourceSpeaker::has_buffered_data() const { - return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data()); + return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data()); } void SourceSpeaker::set_mute_state(bool mute_state) { @@ -301,16 +304,18 @@ void SourceSpeaker::set_volume(float volume) { float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); } -size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer, +size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBufferAudioSource> &audio_source, TickType_t ticks_to_wait) { - // Store current offset, as these samples are already ducked - const size_t current_length = transfer_buffer->available(); + if (audio_source->available() > 0) { + // Existing exposure was ducked when fill() promoted it; do not re-duck on partial-consume re-entry. + return 0; + } - size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait); + size_t bytes_read = audio_source->fill(ticks_to_wait, false); uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read); if (samples_to_duck > 0) { - int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length); + int16_t *current_buffer = reinterpret_cast<int16_t *>(audio_source->mutable_data()); duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_, &this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_, @@ -406,7 +411,7 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t void SourceSpeaker::enter_stopping_state_() { this->state_ = speaker::STATE_STOPPING; this->stopping_start_ms_ = millis(); - this->transfer_buffer_.reset(); + this->audio_source_.reset(); } void MixerSpeaker::dump_config() { @@ -612,9 +617,9 @@ void MixerSpeaker::audio_mixer_task(void *params) { // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) FixedVector<SourceSpeaker *> speakers_with_data; - FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; + FixedVector<std::shared_ptr<audio::RingBufferAudioSource>> audio_sources_with_data; speakers_with_data.init(this_mixer->source_speakers_.size()); - transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); + audio_sources_with_data.init(this_mixer->source_speakers_.size()); while (true) { uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); @@ -629,27 +634,27 @@ void MixerSpeaker::audio_mixer_task(void *params) { this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); speakers_with_data.clear(); - transfer_buffers_with_data.clear(); + audio_sources_with_data.clear(); for (auto &speaker : this_mixer->source_speakers_) { if (speaker->is_running() && !speaker->get_pause_state()) { // Speaker is running and not paused, so it possibly can provide audio data - std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); - if (transfer_buffer.use_count() == 0) { - // No transfer buffer allocated, so skip processing this speaker + std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock(); + if (audio_source.use_count() == 0) { + // No audio source allocated, so skip processing this speaker continue; } - speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + speaker->process_data_from_source(audio_source, 0); // Exposes and ducks audio from source ring buffers - if (transfer_buffer->available() > 0) { - // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop - transfer_buffers_with_data.push_back(transfer_buffer); + if (audio_source->available() > 0) { + // Retain shared ownership across the mixing pass so the source isn't released mid-mix + audio_sources_with_data.push_back(audio_source); speakers_with_data.push_back(speaker); } } } - if (transfer_buffers_with_data.empty()) { + if (audio_sources_with_data.empty()) { // No audio available for transferring, block task temporarily delay(TASK_DELAY_MS); continue; @@ -657,7 +662,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { uint32_t frames_to_mix = output_frames_free; - if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { + if ((audio_sources_with_data.size() == 1) || this_mixer->queue_mode_) { // Only one speaker has audio data, just copy samples over audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); @@ -667,10 +672,10 @@ void MixerSpeaker::audio_mixer_task(void *params) { // Speaker's sample rate matches the output speaker's, copy directly const uint32_t frames_available_in_buffer = - active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); + active_stream_info.bytes_to_frames(audio_sources_with_data[0]->available()); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), - active_stream_info, reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), + copy_frames(reinterpret_cast<const int16_t *>(audio_sources_with_data[0]->data()), active_stream_info, + reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), this_mixer->audio_stream_info_.value(), frames_to_mix); // Set playback delay for newly contributing source @@ -682,7 +687,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { // Update source speaker pending frames speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + audio_sources_with_data[0]->consume(active_stream_info.frames_to_bytes(frames_to_mix)); // Update output transfer buffer length and pipeline frame count output_transfer_buffer->increase_buffer_length( @@ -709,25 +714,25 @@ void MixerSpeaker::audio_mixer_task(void *params) { } } else { // Determine how many frames to mix - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames( - transfer_buffers_with_data[i]->available()); + for (size_t i = 0; i < audio_sources_with_data.size(); ++i) { + const uint32_t frames_available_in_buffer = + speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(audio_sources_with_data[i]->available()); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); } - int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()); + const int16_t *primary_buffer = reinterpret_cast<const int16_t *>(audio_sources_with_data[0]->data()); audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); // Mix two streams together - for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { + for (size_t i = 1; i < audio_sources_with_data.size(); ++i) { mix_audio_samples(primary_buffer, primary_stream_info, - reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), + reinterpret_cast<const int16_t *>(audio_sources_with_data[i]->data()), speakers_with_data[i]->get_audio_stream_info(), reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), this_mixer->audio_stream_info_.value(), frames_to_mix); - if (i != transfer_buffers_with_data.size() - 1) { + if (i != audio_sources_with_data.size() - 1) { // Need to mix more streams together, point primary buffer and stream info to the already mixed output - primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()); + primary_buffer = reinterpret_cast<const int16_t *>(output_transfer_buffer->get_buffer_end()); primary_stream_info = this_mixer->audio_stream_info_.value(); } } @@ -735,8 +740,8 @@ void MixerSpeaker::audio_mixer_task(void *params) { // Get current pipeline depth for delay calculation (before incrementing) uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + // Update source audio source consumption and add new audio durations to the source speaker pending playbacks + for (size_t i = 0; i < audio_sources_with_data.size(); ++i) { // Set playback delay for newly contributing sources if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); @@ -744,7 +749,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { } speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[i]->decrease_buffer_length( + audio_sources_with_data[i]->consume( speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); } diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index f392e83081a..f57bead679f 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -67,11 +67,13 @@ class SourceSpeaker : public speaker::Speaker, public Component { void set_pause_state(bool pause_state) override { this->pause_state_ = pause_state; } bool get_pause_state() const override { return this->pause_state_; } - /// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring. - /// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null) + /// @brief Exposes the next ring buffer chunk (zero-copy) and ducks the freshly exposed bytes in place. + /// If the source still has bytes from a prior partial consume, this is a no-op (those bytes were already + /// ducked on the fill that exposed them). + /// @param audio_source Locked shared_ptr to the audio source (must be valid, not null) /// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer. - /// @return Number of bytes transferred from the ring buffer. - size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer, + /// @return Number of bytes newly exposed from the ring buffer. + size_t process_data_from_source(std::shared_ptr<audio::RingBufferAudioSource> &audio_source, TickType_t ticks_to_wait); /// @brief Sets the ducking level for the source speaker. @@ -83,7 +85,7 @@ class SourceSpeaker : public speaker::Speaker, public Component { void set_parent(MixerSpeaker *parent) { this->parent_ = parent; } void set_timeout(uint32_t ms) { this->timeout_ms_ = ms; } - std::weak_ptr<audio::AudioSourceTransferBuffer> get_transfer_buffer() { return this->transfer_buffer_; } + std::weak_ptr<audio::RingBufferAudioSource> get_audio_source() { return this->audio_source_; } protected: friend class MixerSpeaker; @@ -106,7 +108,7 @@ class SourceSpeaker : public speaker::Speaker, public Component { MixerSpeaker *parent_; - std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer_; + std::shared_ptr<audio::RingBufferAudioSource> audio_source_; std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; uint32_t buffer_duration_ms_; From 1c2043e0544bfa7d1193d3a86144574d7fee0655 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 12 May 2026 20:04:54 -0400 Subject: [PATCH 1960/2056] [esp32] Relax -Werror=reorder and -Werror=maybe-uninitialized on native ESP-IDF (#16392) --- esphome/components/esp32/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index bb823937aa0..221c84c1491 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1754,7 +1754,9 @@ async def to_code(config): ) else: cg.add_build_flag("-Wno-error=format") + cg.add_build_flag("-Wno-error=maybe-uninitialized") cg.add_build_flag("-Wno-error=missing-field-initializers") + cg.add_build_flag("-Wno-error=reorder") cg.add_build_flag("-Wno-error=volatile") cg.set_cpp_standard("gnu++20") From dc95b22c763133354873a57f9c5ba1dfd391d85e Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Wed, 13 May 2026 00:07:49 +0000 Subject: [PATCH 1961/2056] [safe_mode] Allow recovering soft-bricked devices via reboot to recovery partition (#16339) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/components/safe_mode/safe_mode.cpp | 76 +++++++++++++++++++++- esphome/components/safe_mode/safe_mode.h | 5 +- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index bae5e42b9b5..5c0047dca0a 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -15,6 +15,7 @@ #elif defined(USE_ESP32) #include <esp_ota_ops.h> #include <esp_system.h> +#include <esp_image_format.h> #endif #endif @@ -22,6 +23,37 @@ namespace esphome::safe_mode { static const char *const TAG = "safe_mode"; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) && !defined(USE_OTA_PARTITIONS) +// Find a non-running app partition. If verify is true, only returns a partition +// whose image passes verification (expensive: reads flash). Returns nullptr if none found. +static const esp_partition_t *find_alternate_app_partition(bool verify) { + const esp_partition_t *running = esp_ota_get_running_partition(); + const esp_partition_t *result = nullptr; + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + while (it != nullptr) { + const esp_partition_t *p = esp_partition_get(it); + if (p->address != running->address) { + if (!verify) { + result = p; + break; + } + esp_image_metadata_t data = {}; + const esp_partition_pos_t part_pos = { + .offset = p->address, + .size = p->size, + }; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &data) == ESP_OK) { + result = p; + break; + } + } + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); + return result; +} +#endif + void SafeModeComponent::dump_config() { ESP_LOGCONFIG(TAG, "Safe Mode:\n" @@ -34,7 +66,11 @@ void SafeModeComponent::dump_config() { #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) const char *state_str; if (this->ota_state_ == ESP_OTA_IMG_NEW) { +#ifdef USE_OTA_PARTITIONS + state_str = "support unknown"; +#else state_str = "not supported"; +#endif } else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) { state_str = "supported"; } else { @@ -64,6 +100,18 @@ void SafeModeComponent::dump_config() { " See https://esphome.io/guides/faq.html#brownout-detector-was-triggered"); } } + if (!this->app_ota_possible_) { + ESP_LOGW(TAG, "OTA updates are impossible."); +#ifdef USE_OTA_PARTITIONS + ESP_LOGW(TAG, " OTA partition table update or serial flashing is required."); +#else + if (find_alternate_app_partition(false) != nullptr) { + ESP_LOGW(TAG, " Activate safe mode to reboot to the recovery partition."); + } else { + ESP_LOGE(TAG, " No recovery partition available; serial flashing is required."); + } +#endif + } #endif } @@ -124,8 +172,10 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Check partition state to detect if bootloader supports rollback - const esp_partition_t *running = esp_ota_get_running_partition(); - esp_ota_get_state_partition(running, &this->ota_state_); + const esp_partition_t *running_part = esp_ota_get_running_partition(); + esp_ota_get_state_partition(running_part, &this->ota_state_); + const esp_partition_t *next_part = esp_ota_get_next_update_partition(nullptr); + this->app_ota_possible_ = (next_part != nullptr && next_part != running_part); #endif uint32_t rtc_val = this->read_rtc_(); @@ -151,6 +201,28 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en ESP_LOGE(TAG, "Boot loop detected"); } +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) && !defined(USE_OTA_PARTITIONS) + // Allow recovery of soft-bricked devices + // Instead of starting safe_mode, reboot to the other app partition if all conditions are met: + // - app OTA is impossible (for example because the other app partition has type 'factory') + // - the other app partition contains a valid app (for example Tasmota safeboot image or ESPHome) + // - allow_partition_access is not configured making recovery via partition table update impossible + // Image verification is deferred until here so the cost is only paid when entering safe mode, + // not on every boot. + if (!this->app_ota_possible_) { + const esp_partition_t *rollback_part = find_alternate_app_partition(true); + if (rollback_part != nullptr) { + esp_err_t err = esp_ota_set_boot_partition(rollback_part); + if (err == ESP_OK) { + ESP_LOGW(TAG, "OTA updates are impossible. Rebooting to recovery app."); + App.reboot(); + } else { + ESP_LOGE(TAG, "Failed to set recovery boot partition: %s", esp_err_to_name(err)); + } + } + } +#endif + this->status_set_error(); this->set_timeout(enable_time, []() { ESP_LOGW(TAG, "Timeout, restarting"); diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index b458a9a3021..94db4357eb9 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -48,11 +48,14 @@ class SafeModeComponent final : public Component { uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for uint32_t safe_mode_rtc_value_{0}; uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED}; // 4-byte enum +#endif // Group 1-byte members together to minimize padding bool boot_successful_{false}; ///< set to true after boot is considered successful uint8_t safe_mode_num_attempts_{0}; #if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) - esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED}; + bool app_ota_possible_{true}; #endif // Larger objects at the end ESPPreferenceObject rtc_; From 3df0527c1fb8676e2eb0997b7c5bd4e8ee073836 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 20:10:32 -0400 Subject: [PATCH 1962/2056] [audio] Document ring buffer source thread safety (#16393) --- esphome/components/audio/audio_transfer_buffer.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index d83032582e6..b713326141c 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -220,6 +220,10 @@ class ConstAudioSourceBuffer : public AudioReadableBuffer { /// source transparently stitches frames that straddle the ring buffer's wrap boundary by buffering the /// trailing partial frame from one chunk and joining it with the head of the next chunk in a small /// internal splice buffer, so callers always see frame-aligned data. +/// +/// Not thread-safe. The underlying ring_buffer::RingBuffer supports one producer and one consumer +/// running concurrently, but a given RingBufferAudioSource (its acquired item, splice buffer, and +/// queued region) must be used by only one thread, and that thread is the ring buffer's consumer. class RingBufferAudioSource : public AudioReadableBuffer { public: /// Maximum supported alignment. Sized to cover 32-bit samples across up to 2 channels (8 bytes). @@ -242,6 +246,8 @@ class RingBufferAudioSource : public AudioReadableBuffer { size_t available() const override { return this->current_available_; } void consume(size_t bytes) override; bool has_buffered_data() const override; + /// pre_shift is ignored: there is no intermediate transfer buffer to compact, so an unconsumed + /// exposure stays in place and fill() returns 0 until it is fully consumed. size_t fill(TickType_t ticks_to_wait, bool pre_shift) override; /// @brief Returns a mutable pointer to the currently exposed audio data. From 65b53692bdd5ba21d90cfe18d97de4f9102a2b97 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Tue, 12 May 2026 21:36:26 -0400 Subject: [PATCH 1963/2056] [i2s_audio] Properly track DMA input/output (#16317) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- .../i2s_audio/speaker/i2s_audio_speaker.h | 4 +- .../speaker/i2s_audio_speaker_standard.cpp | 252 ++++++++++++------ 3 files changed, 178 insertions(+), 80 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 27961050e6c..680ca069c0e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -99,7 +99,7 @@ void I2SAudioSpeakerBase::loop() { } if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { - ESP_LOGE(TAG, "Not enough memory"); + ESP_LOGE(TAG, "Speaker task setup failed (allocation, preload, or channel enable)"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index c57af2775b3..20bb05e3226 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -36,9 +36,7 @@ enum SpeakerEventGroupBits : uint32_t { ERR_ESP_NO_MEM = (1 << 19), ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event - ERR_PARTIAL_WRITE = (1 << 21), // a DMA write returned fewer bytes than requested (or the encoder - // failed to commit a complete block), which breaks the lockstep - // invariant for every subsequent event + ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index 0c8b8be5226..e69601e87ae 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -17,7 +17,14 @@ namespace esphome::i2s_audio { static const char *const TAG = "i2s_audio.speaker.std"; static constexpr size_t DMA_BUFFERS_COUNT = 4; -static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; +// Sized to comfortably absorb scheduling jitter: at most DMA_BUFFERS_COUNT events can be in flight, +// doubled so that a transient backlog never overruns the queue (which would desync the lockstep +// invariant between i2s_event_queue_ and write_records_queue_). +static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT * 2; +// Generous timeout for ``i2s_channel_write`` blocking. A buffer frees roughly every +// DMA_BUFFER_DURATION_MS, so a multiple of that gives plenty of slack against scheduling jitter +// without masking real failures. +static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * (DMA_BUFFERS_COUNT + 1)); void I2SAudioSpeaker::dump_config() { I2SAudioSpeakerBase::dump_config(); @@ -49,30 +56,73 @@ void I2SAudioSpeaker::run_speaker_task() { // avoids unnecessary single-frame splices. const size_t ring_buffer_size = (this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame; - const uint32_t frames_to_fill_single_dma_buffer = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); - const size_t bytes_to_fill_single_dma_buffer = - this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); + const uint32_t frames_per_dma_buffer = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + const size_t dma_buffer_bytes = this->current_stream_info_.frames_to_bytes(frames_per_dma_buffer); bool successful_setup = false; + std::unique_ptr<audio::RingBufferAudioSource> audio_source; - { + // Pre-zeroed buffer used to silence-pad each DMA descriptor whenever real audio doesn't fully fill it. + RAMAllocator<uint8_t> silence_allocator; + uint8_t *silence_buffer = silence_allocator.allocate(dma_buffer_bytes); + + if (silence_buffer != nullptr) { + memset(silence_buffer, 0, dma_buffer_bytes); + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); - audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, bytes_to_fill_single_dma_buffer, - static_cast<uint8_t>(bytes_per_frame)); + audio_source = + audio::RingBufferAudioSource::create(temp_ring_buffer, dma_buffer_bytes, static_cast<uint8_t>(bytes_per_frame)); + if (audio_source != nullptr) { + // audio_source is nullptr if the ring buffer fails to allocate this->audio_ring_buffer_ = temp_ring_buffer; successful_setup = true; } } + if (successful_setup) { + // Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer. + // This guarantees that every on_sent event has a corresponding write record from the start, so + // ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime. + for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) { + size_t bytes_loaded = 0; + esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded); + if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) { + ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err, + (unsigned) bytes_loaded); + successful_setup = false; + break; + } + uint32_t zero_real_frames = 0; + if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) { + // Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries. + ESP_LOGV(TAG, "Failed to push preload write record"); + successful_setup = false; + break; + } + } + } + + if (successful_setup) { + // Register the on_sent callback BEFORE enabling the channel so the very first transmitted buffer + // generates a queued event that pairs with the first preloaded silence record. + const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; + i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); + + if (i2s_channel_enable(this->tx_handle_) != ESP_OK) { + ESP_LOGV(TAG, "Failed to enable I2S channel"); + successful_setup = false; + } + } + if (!successful_setup) { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); } else { bool stop_gracefully = false; - bool tx_dma_underflow = true; - - uint32_t frames_written = 0; + // Number of records currently in ``write_records_queue_`` that carry real audio. Used by graceful + // stop to wait until every real-audio buffer has been confirmed played by an ISR event. + uint32_t pending_real_buffers = 0; uint32_t last_data_received_time = millis(); xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); @@ -81,11 +131,21 @@ void I2SAudioSpeaker::run_speaker_task() { // - Paused, OR // - No timeout configured, OR // - Timeout hasn't elapsed since last data + // + // Always-fill model: every iteration writes exactly one DMA buffer's worth, mixing real audio + // and silence padding as needed. The blocking ``i2s_channel_write`` paces the loop at the DMA + // consumption rate, and every buffer write is matched 1:1 with a record on ``write_records_queue_``. + // + // While paused, the real-audio fill is skipped and the entire DMA buffer is filled with silence; + // the same blocking ``i2s_channel_write`` provides natural pacing (one buffer per ~DMA_BUFFER_DURATION_MS), + // so the lockstep invariant is preserved without burning CPU. while (this->pause_state_ || !this->timeout_.has_value() || (millis() - last_data_received_time) <= this->timeout_.value()) { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { + // COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion + // event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases). xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); ESP_LOGV(TAG, "Exiting: COMMAND_STOP received"); break; @@ -101,80 +161,115 @@ void I2SAudioSpeaker::run_speaker_task() { break; } + // Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_ + // entry by construction (preloaded records at startup, plus exactly one record pushed per + // iteration alongside exactly one DMA-buffer-sized write). int64_t write_timestamp; + bool lockstep_broken = false; while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) { - // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes - // on the timing info via the audio_output_callback. - uint32_t frames_sent = frames_to_fill_single_dma_buffer; - if (frames_to_fill_single_dma_buffer > frames_written) { - tx_dma_underflow = true; - frames_sent = frames_written; - const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; - write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed); - } else { - tx_dma_underflow = false; - } - frames_written -= frames_sent; - - // Standard I2S mode: fire callback immediately for each event - if (frames_sent > 0) { - this->audio_output_callback_(frames_sent, write_timestamp); - } - } - - if (this->pause_state_) { - // Pause state is accessed atomically, so thread safe - // Delay so the task yields, then skip transferring audio data - vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); - continue; - } - - // Wait half the duration of the data already written to the DMA buffers for new audio data - // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 - uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; - - size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(read_delay), false); - uint8_t *new_data = audio_source->mutable_data(); - - if (bytes_read > 0) { - this->apply_software_volume_(new_data, bytes_read); - this->swap_esp32_mono_samples_(new_data, bytes_read); - } - - if (audio_source->available() == 0) { - if (stop_gracefully && tx_dma_underflow) { + uint32_t real_frames = 0; + if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { + // Should never happen: would indicate the lockstep invariant is broken. + ESP_LOGV(TAG, "Event without matching write record"); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + lockstep_broken = true; break; } - vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); - } else { - size_t bytes_written = 0; - - if (tx_dma_underflow) { - // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue - i2s_channel_disable(this->tx_handle_); - const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr}; - i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this); - i2s_channel_preload_data(this->tx_handle_, audio_source->data(), audio_source->available(), &bytes_written); - } else { - // Audio is already playing, use regular write to add to the DMA buffers - i2s_channel_write(this->tx_handle_, audio_source->data(), audio_source->available(), &bytes_written, - DMA_BUFFER_DURATION_MS); + if (real_frames > 0) { + pending_real_buffers--; + // Real audio is packed at the start of each DMA buffer with any silence padding on the + // tail, so the real audio finished playing earlier than the buffer-completion timestamp + // by the duration of the trailing zeros. + const uint32_t silence_frames = frames_per_dma_buffer - real_frames; + const int64_t adjusted_ts = + write_timestamp - this->current_stream_info_.frames_to_microseconds(silence_frames); + this->audio_output_callback_(real_frames, adjusted_ts); } + } + if (lockstep_broken) { + break; + } - if (bytes_written > 0) { - last_data_received_time = millis(); - frames_written += this->current_stream_info_.bytes_to_frames(bytes_written); - audio_source->consume(bytes_written); + // Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring + // buffer has nothing left to hand over, and every real-audio buffer we submitted has been + // confirmed played. ``has_buffered_data()`` returns bytes still sitting in the ring buffer + // awaiting fill(). + if (stop_gracefully && audio_source->available() == 0 && !this->has_buffered_data() && + pending_real_buffers == 0) { + ESP_LOGV(TAG, "Exiting: graceful stop complete"); + break; + } - if (tx_dma_underflow) { - tx_dma_underflow = false; - // Enable the on_sent callback and channel after preload - xQueueReset(this->i2s_event_queue_); - const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - i2s_channel_enable(this->tx_handle_); + // Compose exactly one DMA buffer's worth: drain as much real audio as the source currently + // exposes (may take multiple fill() calls when crossing a ring buffer wrap), then pad any + // remainder with silence. All writes pack into the next free DMA descriptor in order, so the + // descriptor ends up holding [real audio][silence padding]. + size_t bytes_written_total = 0; + size_t real_bytes_total = 0; + bool partial_write_failure = false; + + if (!this->pause_state_) { + while (bytes_written_total < dma_buffer_bytes) { + size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2, false); + if (bytes_read > 0) { + uint8_t *new_data = audio_source->mutable_data() + audio_source->available() - bytes_read; + this->apply_software_volume_(new_data, bytes_read); + this->swap_esp32_mono_samples_(new_data, bytes_read); } + + const size_t to_write = std::min(audio_source->available(), dma_buffer_bytes - bytes_written_total); + if (to_write == 0) { + // Ring buffer has nothing more to hand over right now; pad the rest of this DMA buffer + // with silence so the lockstep invariant (one write per iteration) is preserved. + break; + } + + size_t bw = 0; + i2s_channel_write(this->tx_handle_, audio_source->data(), to_write, &bw, WRITE_TIMEOUT_TICKS); + if (bw != to_write) { + // A short real-audio write breaks DMA descriptor alignment for every subsequent event; + // the only safe recovery is to restart the task. + ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) to_write); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + partial_write_failure = true; + break; + } + audio_source->consume(bw); + bytes_written_total += bw; + real_bytes_total += bw; } + if (real_bytes_total > 0) { + last_data_received_time = millis(); + } + } + + if (partial_write_failure) { + break; + } + + const size_t silence_bytes = dma_buffer_bytes - bytes_written_total; + if (silence_bytes > 0) { + size_t bw = 0; + i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS); + if (bw != silence_bytes) { + // Same descriptor-alignment hazard as a partial real-audio write. + ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + break; + } + } + + const uint32_t real_frames_in_buffer = this->current_stream_info_.bytes_to_frames(real_bytes_total); + // Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this + // succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep + // invariant is broken and every subsequent timestamp would be silently wrong, so bail. + if (xQueueSend(this->write_records_queue_, &real_frames_in_buffer, 0) != pdTRUE) { + ESP_LOGV(TAG, "Exiting: write records queue full"); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + break; + } + if (real_frames_in_buffer > 0) { + pending_real_buffers++; } } } @@ -183,6 +278,11 @@ void I2SAudioSpeaker::run_speaker_task() { audio_source.reset(); + if (silence_buffer != nullptr) { + silence_allocator.deallocate(silence_buffer, dma_buffer_bytes); + silence_buffer = nullptr; + } + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED); while (true) { @@ -301,7 +401,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream return err; } - i2s_channel_enable(this->tx_handle_); + // The speaker task will enable the channel after preloading. return ESP_OK; } From f94735dc621dc494d7383233cdd904e631fbcdad Mon Sep 17 00:00:00 2001 From: Michael Hansen <mike@rhasspy.org> Date: Tue, 12 May 2026 20:38:39 -0500 Subject: [PATCH 1964/2056] [api][voice_assistant] Add second audio channel for voice_assistant (#16265) Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 7 + esphome/components/api/api_pb2.h | 4 +- esphome/components/api/api_pb2_dump.cpp | 1 + .../components/voice_assistant/__init__.py | 38 +++-- .../voice_assistant/voice_assistant.cpp | 142 +++++++++++++++--- .../voice_assistant/voice_assistant.h | 8 + .../voice_assistant/common-idf.yaml | 14 +- .../voice_assistant/test.esp32-idf.yaml | 1 + 9 files changed, 179 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4d72be5407b..f4f15c10428 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2026,6 +2026,7 @@ message VoiceAssistantAudio { bytes data = 1 [(pointer_to_buffer) = true]; bool end = 2; + bytes data2 = 3 [(pointer_to_buffer) = true]; } enum VoiceAssistantTimerEvent { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 68be7550eeb..c711ef167c8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2893,6 +2893,11 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited this->data_len = value.size(); break; } + case 3: { + this->data2 = value.data(); + this->data2_len = value.size(); + break; + } default: return false; } @@ -2902,12 +2907,14 @@ uint8_t *VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->end); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->data2, this->data2_len); return pos; } uint32_t VoiceAssistantAudio::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->data_len); size += ProtoSize::calc_bool(1, this->end); + size += ProtoSize::calc_length(1, this->data2_len); return size; } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7b82f1884d1..7e926ee0d47 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2436,13 +2436,15 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { class VoiceAssistantAudio final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; - static constexpr uint8_t ESTIMATED_SIZE = 21; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("voice_assistant_audio"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; + const uint8_t *data2{nullptr}; + uint16_t data2_len{0}; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 5258b355ceb..850ad37bc96 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2174,6 +2174,7 @@ const char *VoiceAssistantAudio::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudio")); dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); dump_field(out, ESPHOME_PSTR("end"), this->end); + dump_bytes_field(out, ESPHOME_PSTR("data2"), this->data2, this->data2_len); return out.c_str(); } const char *VoiceAssistantTimerEventResponse::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 9387797ba2e..958d1cbf91e 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -53,6 +53,8 @@ CONF_ON_TIMER_CANCELLED = "on_timer_cancelled" CONF_ON_TIMER_FINISHED = "on_timer_finished" CONF_ON_TIMER_TICK = "on_timer_tick" +MAX_MICROPHONE_SOURCES = 2 + voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant") VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) @@ -90,13 +92,20 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(VoiceAssistant), - cv.Optional( - CONF_MICROPHONE, default={} - ): microphone.microphone_source_schema( - min_bits_per_sample=16, - max_bits_per_sample=16, - min_channels=1, - max_channels=1, + cv.Optional(CONF_MICROPHONE, default=[{}]): cv.All( + cv.ensure_list( + microphone.microphone_source_schema( + min_bits_per_sample=16, + max_bits_per_sample=16, + min_channels=1, + max_channels=1, + ) + ), + cv.Length( + min=1, + max=MAX_MICROPHONE_SOURCES, + msg=f"Voice Assistant supports at most {MAX_MICROPHONE_SOURCES} microphone sources", + ), ), cv.Exclusive(CONF_MEDIA_PLAYER, "output"): cv.use_id( media_player.MediaPlayer @@ -179,10 +188,10 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All( cv.Schema( { - cv.Optional( - CONF_MICROPHONE - ): microphone.final_validate_microphone_source_schema( - "voice_assistant", sample_rate=16000 + cv.Optional(CONF_MICROPHONE): cv.ensure_list( + microphone.final_validate_microphone_source_schema( + "voice_assistant", sample_rate=16000 + ) ), }, extra=cv.ALLOW_EXTRA, @@ -194,9 +203,14 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) + mic_sources = config[CONF_MICROPHONE] + mic_source = await microphone.microphone_source_to_code(mic_sources[0]) cg.add(var.set_microphone_source(mic_source)) + if len(mic_sources) > 1: + mic_source2 = await microphone.microphone_source_to_code(mic_sources[1]) + cg.add(var.set_microphone_source2(mic_source2)) + if CONF_MICRO_WAKE_WORD in config: mww = await cg.get_variable(config[CONF_MICRO_WAKE_WORD]) cg.add(var.set_micro_wake_word(mww)) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 50a82652977..286e6645d23 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,11 +31,21 @@ VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; } void VoiceAssistant::setup() { this->mic_source_->add_data_callback([this](const std::vector<uint8_t> &data) { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_; - if (this->ring_buffer_.use_count() > 1) { + if (temp_ring_buffer != nullptr) { temp_ring_buffer->write((void *) data.data(), data.size()); } }); + // Second microphone channel + if (this->mic_source2_ != nullptr) { + this->mic_source2_->add_data_callback([this](const std::vector<uint8_t> &data) { + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer2_; + if (temp_ring_buffer != nullptr) { + temp_ring_buffer->write((void *) data.data(), data.size()); + } + }); + } + #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { this->media_player_->add_on_state_callback([this](media_player::MediaPlayerState state) { @@ -115,9 +125,9 @@ bool VoiceAssistant::allocate_buffers_() { } #endif - if (this->ring_buffer_.use_count() == 0) { + if (this->ring_buffer_ == nullptr) { this->ring_buffer_ = ring_buffer::RingBuffer::create(RING_BUFFER_SIZE); - if (this->ring_buffer_.use_count() == 0) { + if (this->ring_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate ring buffer"); return false; } @@ -132,6 +142,26 @@ bool VoiceAssistant::allocate_buffers_() { } } + // Second microphone channel + if (this->mic_source2_ != nullptr) { + if (this->ring_buffer2_ == nullptr) { + this->ring_buffer2_ = ring_buffer::RingBuffer::create(RING_BUFFER_SIZE); + if (this->ring_buffer2_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate second ring buffer"); + return false; + } + } + + if (this->send_buffer2_ == nullptr) { + RAMAllocator<uint8_t> send_allocator; + this->send_buffer2_ = send_allocator.allocate(SEND_BUFFER_SIZE); + if (this->send_buffer2_ == nullptr) { + ESP_LOGW(TAG, "Could not allocate second send buffer"); + return false; + } + } + } + return true; } @@ -144,6 +174,15 @@ void VoiceAssistant::clear_buffers_() { this->ring_buffer_->reset(); } + // Second microphone channel + if (this->send_buffer2_ != nullptr) { + memset(this->send_buffer2_, 0, SEND_BUFFER_SIZE); + } + + if (this->ring_buffer2_ != nullptr) { + this->ring_buffer2_->reset(); + } + #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); @@ -162,10 +201,17 @@ void VoiceAssistant::deallocate_buffers_() { this->send_buffer_ = nullptr; } - if (this->ring_buffer_.use_count() > 0) { - this->ring_buffer_.reset(); + this->ring_buffer_.reset(); + + // Second microphone channel + if (this->send_buffer2_ != nullptr) { + RAMAllocator<uint8_t> send_deallocator; + send_deallocator.deallocate(this->send_buffer2_, SEND_BUFFER_SIZE); + this->send_buffer2_ = nullptr; } + this->ring_buffer2_.reset(); + #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { RAMAllocator<uint8_t> speaker_deallocator; @@ -183,7 +229,8 @@ void VoiceAssistant::reset_conversation_id() { void VoiceAssistant::loop() { if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && this->state_ != State::STOPPING_MICROPHONE) { - if (this->mic_source_->is_running() || this->state_ == State::STARTING_MICROPHONE) { + if (this->mic_source_->is_running() || (this->mic_source2_ && this->mic_source2_->is_running()) || + this->state_ == State::STARTING_MICROPHONE) { this->set_state_(State::STOP_MICROPHONE, State::IDLE); } else { this->set_state_(State::IDLE, State::IDLE); @@ -215,11 +262,14 @@ void VoiceAssistant::loop() { this->clear_buffers_(); this->mic_source_->start(); + if (this->mic_source2_) { + this->mic_source2_->start(); + } this->set_state_(State::STARTING_MICROPHONE); break; } case State::STARTING_MICROPHONE: { - if (this->mic_source_->is_running()) { + if (this->mic_source_->is_running() && (!this->mic_source2_ || this->mic_source2_->is_running())) { this->set_state_(this->desired_state_); } break; @@ -266,15 +316,44 @@ void VoiceAssistant::loop() { break; // State changed when udp server port received } case State::STREAMING_MICROPHONE: { - size_t available = this->ring_buffer_->available(); - while (available >= SEND_BUFFER_SIZE) { - size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); - if (this->audio_mode_ == AUDIO_MODE_API) { + if (this->audio_mode_ == AUDIO_MODE_API) { + // API audio + // Both microphone channels are sent, if configured + bool is_available = this->ring_buffer_->available() >= SEND_BUFFER_SIZE; + bool is_available2 = false; + if (this->mic_source2_) { + is_available2 = this->ring_buffer2_->available() >= SEND_BUFFER_SIZE; + } + + while (is_available || is_available2) { api::VoiceAssistantAudio msg; - msg.data = this->send_buffer_; - msg.data_len = read_bytes; + + if (is_available) { + size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); + msg.data = this->send_buffer_; + msg.data_len = read_bytes; + } + + // Second microphone channel + if (is_available2) { + size_t read_bytes = this->ring_buffer2_->read((void *) this->send_buffer2_, SEND_BUFFER_SIZE, 0); + msg.data2 = this->send_buffer2_; + msg.data2_len = read_bytes; + } + this->api_client_->send_message(msg); - } else { + is_available = this->ring_buffer_->available() >= SEND_BUFFER_SIZE; + if (this->mic_source2_) { + is_available2 = this->ring_buffer2_->available() >= SEND_BUFFER_SIZE; + } else { + is_available2 = false; + } + } + } else { + // UDP (will eventually be deprecated) + // Only the primary microphone channel is used + while (this->ring_buffer_->available() >= SEND_BUFFER_SIZE) { + size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { this->set_state_(State::STOP_MICROPHONE, State::IDLE); @@ -284,14 +363,23 @@ void VoiceAssistant::loop() { this->socket_->sendto(this->send_buffer_, read_bytes, 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); } - available = this->ring_buffer_->available(); - } - + } // audio mode break; } case State::STOP_MICROPHONE: { - if (this->mic_source_->is_running()) { - this->mic_source_->stop(); + // Check both microphone channels + bool is_running = this->mic_source_->is_running(); + bool is_running2 = false; + if (this->mic_source2_) { + is_running2 = this->mic_source2_->is_running(); + } + if (is_running || is_running2) { + if (is_running) { + this->mic_source_->stop(); + } + if (is_running2) { + this->mic_source2_->stop(); + } this->set_state_(State::STOPPING_MICROPHONE); } else { this->set_state_(this->desired_state_); @@ -299,7 +387,13 @@ void VoiceAssistant::loop() { break; } case State::STOPPING_MICROPHONE: { - if (this->mic_source_->is_stopped()) { + // Check both microphone channels + bool is_stopped = this->mic_source_->is_stopped(); + bool is_stopped2 = true; + if (this->mic_source2_) { + is_stopped2 = this->mic_source2_->is_stopped(); + } + if (is_stopped && is_stopped2) { this->set_state_(this->desired_state_); } break; @@ -504,7 +598,8 @@ void VoiceAssistant::start_streaming() { ESP_LOGD(TAG, "Client started, streaming microphone"); this->audio_mode_ = AUDIO_MODE_API; - if (this->mic_source_->is_running()) { + // Both microphone channels + if (this->mic_source_->is_running() && (!this->mic_source2_ || this->mic_source2_->is_running())) { this->set_state_(State::STREAMING_MICROPHONE, State::STREAMING_MICROPHONE); } else { this->set_state_(State::START_MICROPHONE, State::STREAMING_MICROPHONE); @@ -520,6 +615,10 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por ESP_LOGD(TAG, "Client started, streaming microphone"); this->audio_mode_ = AUDIO_MODE_UDP; + if (this->mic_source2_ != nullptr) { + ESP_LOGW(TAG, "UDP audio mode does not support a second microphone channel; only the primary will be streamed"); + } + memcpy(&this->dest_addr_, addr, sizeof(this->dest_addr_)); if (this->dest_addr_.ss_family == AF_INET) { ((struct sockaddr_in *) &this->dest_addr_)->sin_port = htons(port); @@ -534,6 +633,7 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por return; } + // Only primary microphone channel over UDP if (this->mic_source_->is_running()) { this->set_state_(State::STREAMING_MICROPHONE, State::STREAMING_MICROPHONE); } else { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 3de46730018..c4fa7eb6152 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -40,6 +40,7 @@ enum VoiceAssistantFeature : uint32_t { FEATURE_TIMERS = 1 << 3, FEATURE_ANNOUNCE = 1 << 4, FEATURE_START_CONVERSATION = 1 << 5, + FEATURE_MULTI_CHANNEL_AUDIO = 1 << 6, }; enum class State { @@ -120,6 +121,7 @@ class VoiceAssistant : public Component { void failed_to_start(); void set_microphone_source(microphone::MicrophoneSource *mic_source) { this->mic_source_ = mic_source; } + void set_microphone_source2(microphone::MicrophoneSource *mic_source2) { this->mic_source2_ = mic_source2; } #ifdef USE_MICRO_WAKE_WORD void set_micro_wake_word(micro_wake_word::MicroWakeWord *mww) { this->micro_wake_word_ = mww; } #endif @@ -149,6 +151,9 @@ class VoiceAssistant : public Component { uint32_t flags = 0; flags |= VoiceAssistantFeature::FEATURE_VOICE_ASSISTANT; flags |= VoiceAssistantFeature::FEATURE_API_AUDIO; + if (this->mic_source2_ != nullptr) { + flags |= VoiceAssistantFeature::FEATURE_MULTI_CHANNEL_AUDIO; + } #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { flags |= VoiceAssistantFeature::FEATURE_SPEAKER; @@ -276,6 +281,7 @@ class VoiceAssistant : public Component { bool timer_tick_running_{false}; microphone::MicrophoneSource *mic_source_{nullptr}; + microphone::MicrophoneSource *mic_source2_{nullptr}; #ifdef USE_SPEAKER void write_speaker_(); speaker::Speaker *speaker_{nullptr}; @@ -301,6 +307,7 @@ class VoiceAssistant : public Component { std::string wake_word_; std::shared_ptr<ring_buffer::RingBuffer> ring_buffer_; + std::shared_ptr<ring_buffer::RingBuffer> ring_buffer2_; bool use_wake_word_; uint8_t noise_suppression_level_; @@ -309,6 +316,7 @@ class VoiceAssistant : public Component { uint32_t conversation_timeout_; uint8_t *send_buffer_{nullptr}; + uint8_t *send_buffer2_{nullptr}; bool continuous_{false}; bool silence_detection_; diff --git a/tests/components/voice_assistant/common-idf.yaml b/tests/components/voice_assistant/common-idf.yaml index 85656837003..0fa09033700 100644 --- a/tests/components/voice_assistant/common-idf.yaml +++ b/tests/components/voice_assistant/common-idf.yaml @@ -31,6 +31,11 @@ microphone: i2s_din_pin: ${i2s_din_pin} adc_type: external pdm: false + - platform: i2s_audio + id: mic_id_external2 + i2s_din_pin: ${i2s_din_pin2} + adc_type: external + pdm: false speaker: - platform: i2s_audio @@ -40,9 +45,12 @@ speaker: voice_assistant: microphone: - microphone: mic_id_external - gain_factor: 4 - channels: 0 + - microphone: mic_id_external + gain_factor: 4 + channels: 0 + - microphone: mic_id_external2 + gain_factor: 4 + channels: 0 speaker: speaker_id micro_wake_word: mww_id conversation_timeout: 60s diff --git a/tests/components/voice_assistant/test.esp32-idf.yaml b/tests/components/voice_assistant/test.esp32-idf.yaml index 1c5c9ddf994..0cc670a77ef 100644 --- a/tests/components/voice_assistant/test.esp32-idf.yaml +++ b/tests/components/voice_assistant/test.esp32-idf.yaml @@ -3,6 +3,7 @@ substitutions: i2s_bclk_pin: GPIO5 i2s_mclk_pin: GPIO15 i2s_din_pin: GPIO13 + i2s_din_pin2: GPIO14 i2s_dout_pin: GPIO12 <<: !include common-idf.yaml From 1dfd3fe9c26706285cc8fca70acf6acf7591ec4a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 12 May 2026 22:52:11 -0400 Subject: [PATCH 1965/2056] [esp32] Print PlatformIO-format RAM/Flash summary after native ESP-IDF builds (#16394) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/build_gen/espidf.py | 10 +++ esphome/espidf/runner.py | 9 +++ esphome/espidf/size_summary.py | 111 +++++++++++++++++++++++++++++++++ esphome/espidf/toolchain.py | 9 ++- 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 esphome/espidf/size_summary.py diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index b1443edac31..dfe2d72b9de 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -89,6 +89,16 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {extra_compile_options} project({CORE.name}) + +# Emit raw JSON size data for ESPHome to read post-build. +add_custom_command( + TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD + COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw + -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json + ${{CMAKE_PROJECT_NAME}}.map + WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} + VERBATIM +) """ diff --git a/esphome/espidf/runner.py b/esphome/espidf/runner.py index 34e3e7694b5..65df37c7b24 100644 --- a/esphome/espidf/runner.py +++ b/esphome/espidf/runner.py @@ -57,6 +57,15 @@ FILTER_IDF_LINES: list[str] = [ # line, so a NOTICE often arrives prefixed with ".NOTICE:" or # "...........NOTICE:". r"\.*NOTICE: ", + # ``idf.py size`` prefaces its table with a centered banner; the + # per-region table below already makes the structure obvious. + r"\s*Memory Type Usage Summary", + # Prefix match for esp-idf-size's trailing "Note:" paragraph (no + # upstream flag suppresses it). + r"Note: The reported total sizes may be smaller than those in the", + # Drop the blank line rich emits after the note so the build log + # doesn't end with an orphan gap before ESPHome's own status lines. + r"\s*$", ] diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py new file mode 100644 index 00000000000..9477e664b34 --- /dev/null +++ b/esphome/espidf/size_summary.py @@ -0,0 +1,111 @@ +"""PlatformIO-format RAM/Flash one-liners after a native ESP-IDF build. + +``idf.py size`` (chained onto ``idf.py build`` in +``toolchain.run_compile``) prints the per-region table inline as part +of the build. This module adds two summary lines underneath, +byte-identical to PlatformIO's output: + + RAM: [==== ] 26.5% (used 47932 bytes from 180736 bytes) + Flash: [=== ] 48.4% (used 888511 bytes from 1835008 bytes) + +The format matches ``script/ci_memory_impact_extract.py`` so CI memory +analysis works unchanged on native ESP-IDF builds. RAM total is the +DRAM region size from the linker map; Flash total is taken from +``partitions.csv`` using PlatformIO's rule (first app partition whose +subtype is ``factory`` or ``ota_0``; see +``platform-espressif32/builder/main.py::_update_max_upload_size``). + +Structured size data is produced at link time by a CMake POST_BUILD +custom command (see ``build_gen/espidf.py``) which writes +``esp_idf_size.json`` next to the ELF. We read that file here rather +than re-running ``esp_idf_size`` from Python. +""" + +from __future__ import annotations + +import csv +import json +import logging +from pathlib import Path + +_LOGGER = logging.getLogger(__name__) +_SIZE_SUFFIXES = {"K": 1024, "M": 1024 * 1024} + + +def _parse_size(token: str) -> int: + token = token.strip() + if not token: + return 0 + if token.startswith(("0x", "0X")): + return int(token, 16) + suffix = token[-1].upper() + if suffix in _SIZE_SUFFIXES: + return int(token[:-1]) * _SIZE_SUFFIXES[suffix] + return int(token) + + +def _find_app_partition_size(partitions_csv: Path) -> int: + """Return the size of the firmware's app partition. + + Mirrors PlatformIO's ``platform-espressif32/builder/main.py:: + _update_max_upload_size``: take the first ``app``-type partition + whose subtype is ``factory`` or ``ota_0``. Order matters because + layouts like Adafruit's ``partitions-4MB-tinyuf2.csv`` repurpose + ``factory`` for a UF2 bootloader before the real OTA slot, so a + naive "prefer factory" rule would pick the wrong row. Raises + ``ValueError`` if no qualifying partition is present. + """ + if not partitions_csv.is_file(): + raise ValueError(f"partitions.csv not found at {partitions_csv}") + for row in csv.reader(partitions_csv.read_text().splitlines()): + cells = [c.strip() for c in row] + if not cells or cells[0].startswith("#") or len(cells) < 5: + continue + ptype, psubtype, psize = cells[1], cells[2], cells[4] + if ptype in ("app", "0") and psubtype in ("factory", "ota_0"): + return _parse_size(psize) + raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}") + + +def _format_bar(used: int, total: int) -> str: + """Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly.""" + pct_raw = used / total if total else 0 + blocks = 10 + filled = min(int(round(blocks * pct_raw)), blocks) + progress = "=" * filled + return ( + f"[{progress:<{blocks}}] {pct_raw: 6.1%} " + f"(used {used:d} bytes from {total:d} bytes)" + ) + + +def print_summary(size_json: Path, partitions_csv: Path | None) -> None: + """Print PlatformIO-shaped RAM and Flash one-liners. + + Failures are non-fatal: the build has already succeeded, we just couldn't + summarize. Logs the cause at debug level. + """ + if not size_json.is_file(): + _LOGGER.debug("Skipping size summary: %s not found", size_json) + return + try: + data = json.loads(size_json.read_text()) + except (OSError, json.JSONDecodeError) as e: + _LOGGER.debug("Skipping size summary: %s", e) + return + + dram = data.get("memory_types", {}).get("DRAM") or {} + ram_used = dram.get("used") + ram_total = dram.get("size") + if ram_total and ram_used is not None: + print(f"RAM: {_format_bar(ram_used, ram_total)}") + + image_size = data.get("image_size") + if image_size is None or partitions_csv is None: + return + try: + app_size = _find_app_partition_size(partitions_csv) + except ValueError as e: + _LOGGER.debug("Skipping Flash summary: %s", e) + return + print(f"Flash: {_format_bar(image_size, app_size)}") diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index da6d3a8a37f..ecb759ed10c 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -12,6 +12,7 @@ import subprocess from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION from esphome.core import CORE, EsphomeError from esphome.espidf.framework import check_esp_idf_install, get_framework_env +from esphome.espidf.size_summary import print_summary _LOGGER = logging.getLogger(__name__) @@ -341,8 +342,14 @@ def run_compile(config, verbose: bool) -> int: args.extend(_get_sdkconfig_args()) args.append("build") + args.append("size") - return run_idf_py(*args) + rc = run_idf_py(*args) + if rc == 0: + size_json = CORE.relative_build_path("build", "esp_idf_size.json") + partitions = CORE.relative_build_path("partitions.csv") + print_summary(size_json, partitions if partitions.is_file() else None) + return rc def get_firmware_path() -> Path: From 480c23012c6eefdc9ca647a0a03b48b936b885c3 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Tue, 12 May 2026 22:13:29 -0500 Subject: [PATCH 1966/2056] [radio_frequency] Add on_control trigger; ir_rf_proxy driver-agnostic (#16368) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --- .../components/ir_rf_proxy/ir_rf_proxy.cpp | 5 +- esphome/components/ir_rf_proxy/ir_rf_proxy.h | 5 +- .../components/ir_rf_proxy/radio_frequency.py | 12 +++-- .../components/radio_frequency/__init__.py | 9 +++- .../radio_frequency/radio_frequency.cpp | 4 ++ .../radio_frequency/radio_frequency.h | 11 ++++ .../components/ir_rf_proxy/common-cc1101.yaml | 50 +++++++++++++++++++ .../ir_rf_proxy/test-cc1101.esp32-idf.yaml | 9 ++++ .../ir_rf_proxy/test-cc1101.esp8266-ard.yaml | 9 ++++ 9 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 tests/components/ir_rf_proxy/common-cc1101.yaml create mode 100644 tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml create mode 100644 tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp index 60b0cd513bb..c13c6198cb6 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -106,7 +106,6 @@ void RfProxy::setup() { void RfProxy::dump_config() { ESP_LOGCONFIG(TAG, "RF Proxy '%s'\n" - " Backend: remote_transmitter/receiver\n" " Supports Transmitter: %s\n" " Supports Receiver: %s", this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), @@ -124,7 +123,9 @@ void RfProxy::dump_config() { } void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) { - // RF: no IR carrier modulation + // RF: no IR carrier modulation. Any RF front-end coordination (state turnaround, retuning) + // happens via the radio_frequency entity's on_control trigger and remote_transmitter's + // on_transmit/on_complete triggers — wired up in user YAML. transmit_raw_timings(this->transmitter_, 0, call); } diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 973e9e20514..d0467e822d1 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -43,7 +43,10 @@ class IrRfProxy : public infrared::Infrared { #endif // USE_IR_RF #ifdef USE_RADIO_FREQUENCY -/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend +/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend. +/// Driver-agnostic: integration with specific RF front-end chips (CC1101, RFM69, etc.) is done +/// in YAML by wiring their actions to `remote_transmitter`'s on_transmit/on_complete triggers and +/// to this entity's on_control trigger (see radio_frequency component docs). class RfProxy : public radio_frequency::RadioFrequency { public: RfProxy() = default; diff --git a/esphome/components/ir_rf_proxy/radio_frequency.py b/esphome/components/ir_rf_proxy/radio_frequency.py index 9982f5e4d10..a243909837f 100644 --- a/esphome/components/ir_rf_proxy/radio_frequency.py +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -35,17 +35,19 @@ def _final_validate(config: ConfigType) -> None: if CONF_REMOTE_TRANSMITTER_ID not in config: return - transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID] full_config = fv.full_config.get() - transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1] + transmitter_path = full_config.get_path_for_id(config[CONF_REMOTE_TRANSMITTER_ID])[ + :-1 + ] transmitter_config = full_config.get_config_for_path(transmitter_path) duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT) if duty_percent is not None and duty_percent != 100: raise cv.Invalid( - f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' " - "set to 100% for RF transmission. Dedicated RF hardware handles modulation; " - "applying a carrier duty cycle would corrupt the signal" + f"Transmitter '{config[CONF_REMOTE_TRANSMITTER_ID]}' must have " + f"'{CONF_CARRIER_DUTY_PERCENT}' set to 100% for RF transmission. " + "Dedicated RF hardware handles modulation; applying a carrier duty cycle " + "would corrupt the signal" ) diff --git a/esphome/components/radio_frequency/__init__.py b/esphome/components/radio_frequency/__init__.py index a54ab6e2492..9fdafe428a6 100644 --- a/esphome/components/radio_frequency/__init__.py +++ b/esphome/components/radio_frequency/__init__.py @@ -8,9 +8,10 @@ breaking changes policy. Use at your own risk. Once the API is considered stable, this warning will be removed. """ +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ON_CONTROL from esphome.core import CORE, coroutine_with_priority from esphome.core.entity_helpers import queue_entity_register, setup_entity from esphome.coroutine import CoroPriority @@ -42,6 +43,7 @@ def radio_frequency_schema(class_: type[cg.MockObjClass]) -> cv.Schema: return entity_schema.extend( { cv.GenerateID(): cv.declare_id(class_), + cv.Optional(CONF_ON_CONTROL): automation.validate_automation({}), } ) @@ -59,6 +61,11 @@ async def register_radio_frequency(var: cg.Pvariable, config: ConfigType) -> Non await setup_radio_frequency_core_(var, config) CORE.register_platform_component("radio_frequency", var) + for conf in config.get(CONF_ON_CONTROL, []): + await automation.build_callback_automation( + var, "add_on_control_callback", [(RadioFrequencyCall, "x")], conf + ) + async def new_radio_frequency(config: ConfigType, *args) -> cg.Pvariable: """Create a new RadioFrequency instance. diff --git a/esphome/components/radio_frequency/radio_frequency.cpp b/esphome/components/radio_frequency/radio_frequency.cpp index 3c000ae1cad..3e0a905737e 100644 --- a/esphome/components/radio_frequency/radio_frequency.cpp +++ b/esphome/components/radio_frequency/radio_frequency.cpp @@ -54,6 +54,10 @@ RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) { void RadioFrequencyCall::perform() { if (this->parent_ != nullptr) { + // Fire any on_control hooks (user-wired automations) before handing off to + // the platform-specific control() — gives users a chance to react to call + // parameters (e.g. retune an external RF front-end based on call.get_frequency()). + this->parent_->control_callback_.call(*this); this->parent_->control(*this); } } diff --git a/esphome/components/radio_frequency/radio_frequency.h b/esphome/components/radio_frequency/radio_frequency.h index db73a844ede..7dfd2dd77e1 100644 --- a/esphome/components/radio_frequency/radio_frequency.h +++ b/esphome/components/radio_frequency/radio_frequency.h @@ -170,6 +170,15 @@ class RadioFrequency : public Component, public EntityBase, public remote_base:: this->receive_callback_.add(std::forward<F>(callback)); } + /// Add a callback to invoke when a transmit call is made on this entity. + /// Fires before the platform-specific control() runs, with the call object + /// (containing frequency, modulation, repeat count, etc.). Used by the + /// `on_control` YAML trigger so users can wire any RF front-end driver + /// (CC1101, RFM69, custom) to react to per-call parameters. + template<typename F> void add_on_control_callback(F &&callback) { + this->control_callback_.add(std::forward<F>(callback)); + } + protected: friend class RadioFrequencyCall; @@ -182,6 +191,8 @@ class RadioFrequency : public Component, public EntityBase, public remote_base:: // Callback manager for receive events (lazy: saves memory when no callbacks registered) LazyCallbackManager<void(remote_base::RemoteReceiveData)> receive_callback_; + // Callback manager for on_control trigger (lazy: same memory savings) + LazyCallbackManager<void(const RadioFrequencyCall &)> control_callback_; }; } // namespace esphome::radio_frequency diff --git a/tests/components/ir_rf_proxy/common-cc1101.yaml b/tests/components/ir_rf_proxy/common-cc1101.yaml new file mode 100644 index 00000000000..392e6db22ef --- /dev/null +++ b/tests/components/ir_rf_proxy/common-cc1101.yaml @@ -0,0 +1,50 @@ +cc1101: + id: cc1101_radio + cs_pin: ${cs_pin} + frequency: 433.92MHz + modulation_type: ASK/OOK + output_power: 10 + +# Dual-pin wiring (recommended by the CC1101 docs): +# CC1101 GDO0 → ${gdo0_pin} (remote_transmitter) +# CC1101 GDO2 → ${gdo2_pin} (remote_receiver) +remote_transmitter: + id: rf_tx + pin: ${gdo0_pin} + carrier_duty_percent: 100% + # Switch the chip into TX state for the duration of each transmission and back to RX + # afterwards. Driver-agnostic: any RF front-end with begin_tx/begin_rx-style actions + # can be wired this way. + on_transmit: + then: + - cc1101.begin_tx: cc1101_radio + on_complete: + then: + - cc1101.begin_rx: cc1101_radio + +remote_receiver: + id: rf_rx + pin: ${gdo2_pin} + +radio_frequency: + - platform: ir_rf_proxy + id: rf_proxy_cc1101_tx + name: "CC1101 RF Transmitter" + frequency: 433.92MHz + remote_transmitter_id: rf_tx + # Optional: retune the CC1101 per-transmit when the API request specifies a + # different carrier frequency. Demonstrates the on_control trigger. + on_control: + then: + - if: + condition: + lambda: "return x.get_frequency().has_value() && *x.get_frequency() > 0;" + then: + - cc1101.set_frequency: + id: cc1101_radio + value: !lambda "return *x.get_frequency();" + - platform: ir_rf_proxy + id: rf_proxy_cc1101_rx + name: "CC1101 RF Receiver" + frequency: 433.92MHz + remote_receiver_id: rf_rx diff --git a/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml b/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml new file mode 100644 index 00000000000..bf6f3d9815f --- /dev/null +++ b/tests/components/ir_rf_proxy/test-cc1101.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + gdo2_pin: GPIO16 + +packages: + common: !include common.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + cc1101: !include common-cc1101.yaml diff --git a/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml b/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml new file mode 100644 index 00000000000..e25c47ab236 --- /dev/null +++ b/tests/components/ir_rf_proxy/test-cc1101.esp8266-ard.yaml @@ -0,0 +1,9 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + gdo2_pin: GPIO16 + +packages: + common: !include common.yaml + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + cc1101: !include common-cc1101.yaml From 65ea29b44ac9f8e3dcb1dd64809b4a6d57dc853c Mon Sep 17 00:00:00 2001 From: Dmitrii Kuminov <coding@dmik.org> Date: Tue, 12 May 2026 20:41:30 -0700 Subject: [PATCH 1967/2056] [core] Fix !include vars not being substituted in !lambda values (#16320) Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> --- esphome/yaml_util.py | 2 +- tests/unit_tests/test_substitutions.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 3cfc9c4b15d..b153d160a72 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -139,7 +139,7 @@ def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: value.set_context({**value.vars, **(context_vars or {})}) return value - if context_vars and isinstance(value, (dict, list, str)): + if context_vars and isinstance(value, (dict, list, str, Lambda)): value = add_class_to_obj(value, ConfigContext) value.set_context(context_vars) return value diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index cf6d4adbf51..4783112578c 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -818,3 +818,23 @@ def test_resolve_include_error_no_expanded_from_for_literal_filename( substitutions.resolve_include(include, [], substitutions.ContextVars()) assert "expanded from" not in str(exc_info.value) + + +def test_include_vars_applied_to_lambda_value(tmp_path: Path) -> None: + """!include vars: must substitute into a top-level !lambda value in the included file. + + Regression test for the case where the included file's root is a Lambda; + add_context() previously only tagged dict/list/str, so the include's vars + never reached the substitution pass for Lambda content. + """ + included = tmp_path / "lambda.yaml" + included.write_text('!lambda |-\n return "${foo}";\n') + + include = yaml_util.IncludeFile( + tmp_path / "main.yaml", "lambda.yaml", {"foo": "bar"}, yaml_util.load_yaml + ) + config = OrderedDict({"value": include.load()}) + result = substitutions.do_substitution_pass(config) + + assert isinstance(result["value"], Lambda) + assert result["value"].value == 'return "bar";' From 45a4811bb424f399515ccb08a49ef5d9f81fb634 Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Wed, 13 May 2026 08:40:19 +0200 Subject: [PATCH 1968/2056] [mitsubishi_cn105] Unified timeout handling (#16385) --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 56 +++++++++---------- .../mitsubishi_cn105/mitsubishi_cn105.h | 5 +- .../climate/mitsubishi_cn105_tests.cpp | 42 ++++++-------- tests/components/mitsubishi_cn105/common.h | 3 +- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 2a173997f3f..56f1ee1b3f4 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -8,7 +8,7 @@ namespace esphome::mitsubishi_cn105 { static const char *const TAG = "mitsubishi_cn105.driver"; -static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; +static constexpr uint32_t RESPONSE_TIMEOUT_MS = 2000; static constexpr uint8_t TARGET_TEMPERATURE_ENC_A_OFFSET = 31; @@ -91,25 +91,31 @@ static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } bool MitsubishiCN105::update() { - if (const auto start = this->status_update_start_ms_) { - if (this->pending_updates_.any()) { - this->status_update_wait_credit_ms_ = std::min(this->update_interval_ms_, get_loop_time_ms() - *start); - this->cancel_waiting_and_transition_to_(State::APPLYING_SETTINGS); - return false; - } + switch (this->state_) { + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + if (this->pending_updates_.any()) { + this->status_update_wait_credit_ms_ = + std::min(this->update_interval_ms_, get_loop_time_ms() - this->operation_start_ms_); + this->set_state_(State::APPLYING_SETTINGS); + return false; + } + if (this->has_timed_out_(this->update_interval_ms_)) { + this->set_state_(State::UPDATING_STATUS); + return false; + } + break; - if ((get_loop_time_ms() - *start) >= this->update_interval_ms_) { - this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); - return false; - } - } + case State::CONNECTING: + case State::UPDATING_STATUS: + case State::APPLYING_SETTINGS: + if (this->has_timed_out_(RESPONSE_TIMEOUT_MS)) { + this->set_state_(State::READ_TIMEOUT); + return false; + } + break; - if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { - this->write_timeout_start_ms_.reset(); - this->frame_parser_.reset(); - this->status_update_wait_credit_ms_ = 0; - this->set_state_(State::READ_TIMEOUT); - return false; + default: + break; } return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) { @@ -171,7 +177,6 @@ void MitsubishiCN105::did_transition_(State to) { break; case State::CONNECTED: - this->write_timeout_start_ms_.reset(); this->current_status_msg_type_ = STATUS_MSG_SETTINGS; this->set_state_(State::UPDATING_STATUS); break; @@ -181,7 +186,6 @@ void MitsubishiCN105::did_transition_(State to) { break; case State::STATUS_UPDATED: { - this->write_timeout_start_ms_.reset(); if (this->pending_updates_.any() && this->is_status_initialized()) { this->set_state_(State::APPLYING_SETTINGS); } else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) { @@ -194,7 +198,7 @@ void MitsubishiCN105::did_transition_(State to) { } case State::SCHEDULE_NEXT_STATUS_UPDATE: - this->status_update_start_ms_ = get_loop_time_ms() - this->status_update_wait_credit_ms_; + this->operation_start_ms_ = get_loop_time_ms() - this->status_update_wait_credit_ms_; this->status_update_wait_credit_ms_ = 0; this->current_status_msg_type_ = STATUS_MSG_SETTINGS; this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); @@ -205,11 +209,12 @@ void MitsubishiCN105::did_transition_(State to) { break; case State::SETTINGS_APPLIED: - this->write_timeout_start_ms_.reset(); this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); break; case State::READ_TIMEOUT: + this->frame_parser_.reset(); + this->status_update_wait_credit_ms_ = 0; this->set_state_(State::CONNECTING); break; @@ -233,7 +238,7 @@ bool MitsubishiCN105::should_request_room_temperature_() const { void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { FrameParser::dump_buffer_vv("TX", packet, len); this->device_.write_array(packet, len); - this->write_timeout_start_ms_ = get_loop_time_ms(); + this->operation_start_ms_ = get_loop_time_ms(); } void MitsubishiCN105::update_status_() { @@ -241,11 +246,6 @@ void MitsubishiCN105::update_status_() { this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload)); } -void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) { - this->status_update_start_ms_.reset(); - this->set_state_(state); -} - bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { switch (type) { case PACKET_TYPE_CONNECT_RESPONSE: diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 52b78efccda..60ca81cf9e0 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -124,9 +124,9 @@ class MitsubishiCN105 { bool parse_status_room_temperature_(const uint8_t *payload, size_t len); void send_packet_(const uint8_t *packet, size_t len); void update_status_(); - void cancel_waiting_and_transition_to_(State state); bool should_request_room_temperature_() const; void apply_settings_(); + bool has_timed_out_(uint32_t timeout) const { return ((get_loop_time_ms() - this->operation_start_ms_) >= timeout); } void set_remote_temperature_half_deg_(uint8_t temperature_half_deg); template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); @@ -135,9 +135,8 @@ class MitsubishiCN105 { uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; uint32_t status_update_wait_credit_ms_{0}; + uint32_t operation_start_ms_{0}; uint32_t room_temperature_min_interval_ms_{60000}; - std::optional<uint32_t> write_timeout_start_ms_; - std::optional<uint32_t> status_update_start_ms_; std::optional<uint32_t> last_room_temperature_update_ms_; Status status_{}; State state_{State::NOT_CONNECTED}; diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index 7615b62d03f..db2fbced1c3 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -16,13 +16,13 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) { ctx.sut.set_current_time(123); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED); EXPECT_TRUE(ctx.uart.tx.empty()); - EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.operation_start_ms_, 0); ctx.sut.initialize(); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 123); } TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { @@ -32,8 +32,7 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { ctx.uart.tx.clear(); // Remove first connect packet bytes EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.operation_start_ms_, 0); // Connect response ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55}); @@ -47,8 +46,7 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{200}); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.operation_start_ms_, 200); // Clear TX bytes. ctx.uart.tx.clear(); @@ -77,8 +75,7 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A)); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{300}); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.operation_start_ms_, 300); // Clear TX bytes. ctx.uart.tx.clear(); @@ -101,8 +98,7 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); - EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); - EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{400}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 400); } TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { @@ -115,21 +111,21 @@ TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 0); // Still no response after 1999ms, no retry yet ctx.sut.set_current_time(1999); ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 0); // Stop waiting after 2s and retry connect ctx.sut.set_current_time(2000); ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); - EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 2000); } TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { @@ -233,15 +229,12 @@ TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) { ctx.sut.set_update_interval(2000); ctx.sut.set_current_time(80000); - // No scheduled status update - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); - // Status update completed, schedule next status update ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); - EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{80000}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 80000); // Wait for update_interval (ms) before doing another status update ASSERT_FALSE(ctx.sut.update()); @@ -257,7 +250,7 @@ TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) { ASSERT_FALSE(ctx.sut.update()); EXPECT_FALSE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.operation_start_ms_, 82000); } TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { @@ -382,14 +375,14 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); - EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 5000); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); // Nothing to do in update (rx empty, no timeout) ctx.sut.set_current_time(5500); ASSERT_FALSE(ctx.sut.update()); EXPECT_TRUE(ctx.uart.tx.empty()); - EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 5000); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); // Write new values @@ -402,7 +395,6 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { // Waiting for next status update must be interrupted and new values send to AC ctx.sut.set_current_time(6000); ASSERT_FALSE(ctx.sut.update()); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, @@ -414,7 +406,7 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { ctx.sut.set_current_time(6500); ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); - EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{6500 - 1000}); + EXPECT_EQ(ctx.sut.operation_start_ms_, 6500 - 1000); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); } @@ -502,7 +494,7 @@ TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); - ASSERT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000}); + ASSERT_EQ(ctx.sut.operation_start_ms_, 5000); ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); // Interrupt that wait with a write so credit is accumulated. @@ -514,7 +506,7 @@ TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) ctx.sut.set_current_time(6000); ASSERT_FALSE(ctx.sut.update()); ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); - ASSERT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + ASSERT_EQ(ctx.sut.operation_start_ms_, 6000); ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000); // Do not ACK the write. Advance time far enough to force timeout/reconnect @@ -522,8 +514,8 @@ TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) ctx.sut.set_current_time(36000); ASSERT_FALSE(ctx.sut.update()); EXPECT_NE(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); + ASSERT_EQ(ctx.sut.operation_start_ms_, 36000); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0); - EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); } TEST(MitsubishiCN105Tests, SetOutOfRangeRemoteRoomTempIsIgnored) { diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index d0fdca1ea5a..73e09d6c84b 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -44,8 +44,7 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::State; using MitsubishiCN105::UpdateFlag; using MitsubishiCN105::state_; - using MitsubishiCN105::write_timeout_start_ms_; - using MitsubishiCN105::status_update_start_ms_; + using MitsubishiCN105::operation_start_ms_; using MitsubishiCN105::use_temperature_encoding_b_; using MitsubishiCN105::status_update_wait_credit_ms_; using MitsubishiCN105::pending_updates_; From 0e4922a3400d5b186214ff48ee6298be04bcba89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 05:14:19 -0500 Subject: [PATCH 1969/2056] [core] Cache validated config to skip re-validation on upload/logs (#16381) --- esphome/__main__.py | 26 ++- esphome/compiled_config.py | 76 ++++++ esphome/storage_json.py | 24 +- esphome/writer.py | 6 + tests/unit_tests/test_compiled_config.py | 282 +++++++++++++++++++++++ 5 files changed, 409 insertions(+), 5 deletions(-) create mode 100644 esphome/compiled_config.py create mode 100644 tests/unit_tests/test_compiled_config.py diff --git a/esphome/__main__.py b/esphome/__main__.py index bca86729171..d733534a5c0 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2434,10 +2434,28 @@ def run_esphome(argv): # Commands that don't need fresh external components: logs just connects # to the device, and clean is about to delete the build directory. skip_external = args.command in ("logs", "clean") - config = read_config( - dict(args.substitution) if args.substitution else {}, - skip_external_update=skip_external, - ) + command_line_substitutions = dict(args.substitution) if args.substitution else {} + + # Fast path for upload/logs: reuse the validated-config cache the + # last compile wrote. Falls back to read_config when missing/stale. + # Skipped when -s overrides are passed, since the cache was written + # against the previous substitution set. + config: ConfigType | None = None + if args.command in ("upload", "logs") and not command_line_substitutions: + from esphome.compiled_config import load_compiled_config + + config = load_compiled_config(conf_path) + if config is not None: + _LOGGER.info( + "Loaded validated config cache for %s, skipping validation.", + conf_path.name, + ) + + if config is None: + config = read_config( + command_line_substitutions, + skip_external_update=skip_external, + ) if config is None: return 2 CORE.config = config diff --git a/esphome/compiled_config.py b/esphome/compiled_config.py new file mode 100644 index 00000000000..92cbb7348a4 --- /dev/null +++ b/esphome/compiled_config.py @@ -0,0 +1,76 @@ +"""Validated-config cache for the upload/logs fast path. + +compile dumps the validated config to <data_dir>/storage/<file>.validated.yaml; +the next upload/logs for that YAML reuses it instead of running the full +read_config pipeline. YAML round-trip (yaml_util.dump/load_yaml) keeps +!lambda/!include/IDs/paths intact; mtime gates staleness. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from esphome.core import CORE +from esphome.helpers import write_file +from esphome.storage_json import StorageJSON, ext_storage_path +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + + +def compiled_config_path(config_filename: str) -> Path: + """Path to the cached validated config alongside the storage sidecar.""" + return CORE.data_dir / "storage" / f"{config_filename}.validated.yaml" + + +def _cache_is_fresh(cache_path: Path, source_path: Path) -> bool: + """True iff the cache file exists and isn't older than the source.""" + try: + return cache_path.stat().st_mtime >= source_path.stat().st_mtime + except OSError: + return False + + +def save_compiled_config(config: ConfigType) -> None: + """Write the validated-config cache. Always-write so mtime stays fresh. + + Mode 0600 because show_secrets=True resolves !secret inline. + Failures are non-fatal: the fast path falls back to read_config. + """ + from esphome import yaml_util + + try: + rendered = yaml_util.dump(config, show_secrets=True) + write_file(compiled_config_path(CORE.config_filename), rendered, private=True) + except Exception as err: # pylint: disable=broad-except + _LOGGER.debug("Skipping compiled config cache write: %s", err) + + +def load_compiled_config(conf_path: Path) -> ConfigType | None: + """Load the cached validated config and apply storage metadata to CORE. + + Returns None (caller falls back to read_config) when the cache is + missing, older than the source YAML, unparseable, or the sidecar + is incomplete. + """ + cache_path = compiled_config_path(conf_path.name) + if not _cache_is_fresh(cache_path, conf_path): + return None + + from esphome import yaml_util + + try: + config = yaml_util.load_yaml(cache_path, clear_secrets=False) + except Exception: # pylint: disable=broad-except + return None + + storage = StorageJSON.load(ext_storage_path(conf_path.name)) + if storage is None: + return None + # apply_to_core assumes a real compile wrote the sidecar; wizard-only + # sidecars leave both of these unset and can't drive upload/logs. + if not storage.core_platform and not storage.target_platform: + return None + storage.apply_to_core() + return config diff --git a/esphome/storage_json.py b/esphome/storage_json.py index c6df16ce78d..7d26b22f96c 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -8,7 +8,13 @@ import os from pathlib import Path from esphome import const -from esphome.const import CONF_DISABLED, CONF_MDNS +from esphome.const import ( + CONF_DISABLED, + CONF_MDNS, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) from esphome.core import CORE from esphome.helpers import write_file_if_changed from esphome.types import CoreType @@ -256,6 +262,22 @@ class StorageJSON: except Exception: # pylint: disable=broad-except return None + def apply_to_core(self) -> None: + """Populate CORE with the metadata upload/logs read. + + Inverse of :meth:`from_esphome_core`. Keep paired -- a new + attribute upload/logs needs has to be captured there too. + Validator-only fields (loaded_integrations/platforms, + friendly_name) are skipped; the fast path doesn't run + validation and CORE.__init__ defaults them. + """ + CORE.name = self.name + CORE.build_path = self.build_path + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(), + KEY_TARGET_FRAMEWORK: self.framework, + } + def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() diff --git a/esphome/writer.py b/esphome/writer.py index 2fa43fa5eb1..cf04e4f8d2a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -7,6 +7,7 @@ import re import time from esphome import loader +from esphome.compiled_config import save_compiled_config from esphome.config import iter_component_configs, iter_components from esphome.const import ( HEADER_FILE_EXTENSIONS, @@ -109,6 +110,11 @@ def update_storage_json() -> None: path = storage_path() old = StorageJSON.load(path) new = StorageJSON.from_esphome_core(CORE, old) + + # Refresh the cache upload/logs read on the next call. + if CORE.config is not None: + save_compiled_config(CORE.config) + if old == new: return diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py new file mode 100644 index 00000000000..34e811b97bd --- /dev/null +++ b/tests/unit_tests/test_compiled_config.py @@ -0,0 +1,282 @@ +"""Tests for the validated-config cache used by upload/logs.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from unittest.mock import patch + +import pytest + +from esphome.__main__ import run_esphome +from esphome.compiled_config import ( + compiled_config_path, + load_compiled_config, + save_compiled_config, +) +from esphome.const import ( + CONF_API, + CONF_ESPHOME, + CONF_NAME, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE + +_VALIDATED_CONFIG_YAML = """\ +esphome: + name: lite_test + friendly_name: Lite Test Device +esp32: + board: nodemcu-32s +logger: + baud_rate: 115200 +api: + port: 6053 + encryption: + key: 6dGhpcyBpcyBhIHRlc3Q= +ota: + - platform: esphome + port: 3232 + password: secret +wifi: + ssid: ssid + use_address: 192.168.1.42 +""" + + +def _write_storage(storage_path: Path) -> None: + """Write a vanilla StorageJSON sidecar for the cache tests.""" + storage_path.parent.mkdir(parents=True, exist_ok=True) + data = { + "storage_version": 1, + "name": "lite_test", + "friendly_name": "Lite Test Device", + "comment": None, + "esphome_version": "2026.1.0", + "src_version": 1, + "address": "192.168.1.42", + "web_port": None, + "esp_platform": "ESP32", + "build_path": "/build/lite_test", + "firmware_bin_path": "/build/lite_test/firmware.bin", + "loaded_integrations": ["api", "logger", "ota", "wifi"], + "loaded_platforms": [], + "no_mdns": False, + "framework": "arduino", + "core_platform": "esp32", + } + storage_path.write_text(json.dumps(data)) + + +def _write_cache(cache_path: Path, body: str = _VALIDATED_CONFIG_YAML) -> Path: + """Write the cache file and return it.""" + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.write_text(body) + return cache_path + + +def _set_cache_mtime(cache_path: Path, yaml_path: Path, *, offset: int) -> None: + """Force the cache file's mtime relative to the source YAML. + + Positive offset → cache is fresh. Negative → cache is stale. + """ + yaml_stat = yaml_path.stat() + os.utime(cache_path, (yaml_stat.st_atime, yaml_stat.st_mtime + offset)) + + +@pytest.fixture +def fresh_cache_files(tmp_path: Path) -> Path: + """YAML + StorageJSON + cache, all consistent and fresh.""" + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage(storage_dir / "lite_test.yaml.json") + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + return yaml_path + + +def test_compiled_config_path_lives_alongside_sidecar(setup_core: Path) -> None: + """The cache file shape is predictable from the YAML filename.""" + path = compiled_config_path("device.yaml") + assert path.name == "device.yaml.validated.yaml" + assert path.parent.name == "storage" + + +def test_load_compiled_config_happy_path(fresh_cache_files: Path) -> None: + """Fresh cache + sidecar → returns config and populates CORE.""" + config = load_compiled_config(fresh_cache_files) + + assert config is not None + assert config[CONF_ESPHOME][CONF_NAME] == "lite_test" + assert config[CONF_API]["encryption"]["key"] == "6dGhpcyBpcyBhIHRlc3Q=" + assert config["ota"][0]["password"] == "secret" + + # apply_to_core populated exactly what upload/logs read off CORE. + assert CORE.name == "lite_test" + assert CORE.build_path == Path("/build/lite_test") + assert CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] == "esp32" + assert CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] == "arduino" + + +@pytest.mark.parametrize( + "scenario", + ["missing_cache", "stale_cache", "corrupt_cache", "missing_sidecar"], +) +def test_load_compiled_config_falls_back(tmp_path: Path, scenario: str) -> None: + """All non-happy cases return None so the caller falls back.""" + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + storage_dir = tmp_path / ".esphome" / "storage" + cache_path = storage_dir / "lite_test.yaml.validated.yaml" + sidecar_path = storage_dir / "lite_test.yaml.json" + + if scenario == "missing_cache": + pass # no cache, no sidecar + elif scenario == "stale_cache": + _write_storage(sidecar_path) + _set_cache_mtime(_write_cache(cache_path), yaml_path, offset=-60) + elif scenario == "corrupt_cache": + _write_storage(sidecar_path) + _set_cache_mtime( + _write_cache(cache_path, "not: valid: yaml: ["), yaml_path, offset=5 + ) + elif scenario == "missing_sidecar": + # Cache fresh + parseable, but no StorageJSON → can't populate CORE. + _set_cache_mtime(_write_cache(cache_path), yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is None + + +@pytest.mark.parametrize("command", ["upload", "logs"]) +def test_run_esphome_upload_and_logs_use_cache_when_fresh( + command: str, + fresh_cache_files: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """upload/logs skip read_config() when the cache is fresh.""" + captured: dict = {} + + def _stub(_args, config): + captured["config"] = config + return 0 + + with ( + caplog.at_level("INFO", logger="esphome.__main__"), + patch("esphome.__main__.read_config") as mock_read, + patch.dict("esphome.__main__.POST_CONFIG_ACTIONS", {command: _stub}), + ): + assert run_esphome(["esphome", command, str(fresh_cache_files)]) == 0 + + mock_read.assert_not_called() + assert captured["config"][CONF_ESPHOME][CONF_NAME] == "lite_test" + assert captured["config"][CONF_API]["encryption"]["key"] == "6dGhpcyBpcyBhIHRlc3Q=" + # The success-branch log line is part of the patch; assert on it so + # branch coverage stays unambiguous in CI. + assert "Loaded validated config cache" in caplog.text + + +@pytest.mark.parametrize("command", ["upload", "logs"]) +def test_run_esphome_upload_and_logs_fall_back_when_no_cache( + tmp_path: Path, command: str +) -> None: + """Without a cache, the dispatcher falls back to read_config().""" + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + + with ( + patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch.dict( + "esphome.__main__.POST_CONFIG_ACTIONS", + {command: lambda args, config: 0}, + ), + ): + assert run_esphome(["esphome", command, str(yaml_path)]) == 2 + + mock_read.assert_called_once() + + +def test_run_esphome_upload_with_substitution_skips_cache( + fresh_cache_files: Path, +) -> None: + """`-s key value` forces a fresh validation -- the cache was written + against the prior substitution set, so reusing it would silently + ignore the override.""" + with ( + patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch.dict( + "esphome.__main__.POST_CONFIG_ACTIONS", + {"upload": lambda args, config: 0}, + ), + ): + run_esphome(["esphome", "-s", "var", "val", "upload", str(fresh_cache_files)]) + + mock_read.assert_called_once() + + +def test_run_esphome_compile_does_not_use_cache(fresh_cache_files: Path) -> None: + """The compile subcommand always re-validates -- it's what writes the cache.""" + with ( + patch("esphome.__main__.read_config", return_value=None) as mock_read, + patch.dict( + "esphome.__main__.POST_CONFIG_ACTIONS", + {"compile": lambda args, config: 0}, + ), + ): + run_esphome(["esphome", "compile", str(fresh_cache_files)]) + + mock_read.assert_called_once() + + +def test_save_compiled_config_writes_cache(tmp_path: Path) -> None: + """`save_compiled_config` writes the dumped YAML next to the sidecar.""" + CORE.config_path = tmp_path / "lite_test.yaml" + save_compiled_config({"esphome": {"name": "lite_test"}, "logger": {}}) + + cache_path = compiled_config_path("lite_test.yaml") + assert cache_path.is_file() + body = cache_path.read_text() + assert "name: lite_test" in body + assert "logger:" in body + + +def test_save_compiled_config_swallows_dump_errors( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Failures during the dump are non-fatal -- a bad cache just means + the next fast path falls back to read_config().""" + CORE.config_path = tmp_path / "lite_test.yaml" + with patch("esphome.yaml_util.dump", side_effect=RuntimeError("boom")): + save_compiled_config({"esphome": {"name": "lite_test"}}) + assert not compiled_config_path("lite_test.yaml").exists() + + +def test_load_compiled_config_rejects_wizard_only_sidecar(tmp_path: Path) -> None: + """A wizard-only sidecar (no compile -- no core_platform / target_platform) + can't drive upload/logs, so the fast path falls back.""" + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + storage_dir.mkdir(parents=True, exist_ok=True) + # StorageJSON with both core_platform and target_platform unset. + (storage_dir / "lite_test.yaml.json").write_text( + '{"storage_version": 1, "name": "lite_test", "friendly_name": null, ' + '"comment": null, "esphome_version": null, "src_version": 1, ' + '"address": null, "web_port": null, "esp_platform": null, ' + '"build_path": null, "firmware_bin_path": null, ' + '"loaded_integrations": [], "loaded_platforms": [], "no_mdns": false, ' + '"framework": null, "core_platform": null}' + ) + cache_path = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache_path, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is None From b86652543791a8c17da0c0f061606564f96dbf92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 10:01:11 -0500 Subject: [PATCH 1970/2056] [ci] Skip native ESP-IDF compile test when no relevant files changed (#16395) --- .github/workflows/ci.yml | 12 ++- script/determine-jobs.py | 123 +++++++++++++++++++++ tests/script/test_determine_jobs.py | 160 ++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad39b3f3467..06c6c0fec17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,6 +252,8 @@ jobs: python-linters: ${{ steps.determine.outputs.python-linters }} import-time: ${{ steps.determine.outputs.import-time }} device-builder: ${{ steps.determine.outputs.device-builder }} + native-idf: ${{ steps.determine.outputs.native-idf }} + native-idf-components: ${{ steps.determine.outputs.native-idf-components }} changed-components: ${{ steps.determine.outputs.changed-components }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} @@ -297,6 +299,8 @@ jobs: echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT + echo "native-idf=$(echo "$output" | jq -r '.native_idf')" >> $GITHUB_OUTPUT + echo "native-idf-components=$(echo "$output" | jq -r '.native_idf_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT @@ -823,10 +827,14 @@ jobs: needs: - common - determine-jobs - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.native-idf == 'true' env: ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf - TEST_COMPONENTS: esp32,api,heatpumpir,bme280_i2c,bh1750,aht10,esp32_ble,esp32_ble_beacon,esp32_ble_client,esp32_ble_server,esp32_ble_tracker,ble_client,ble_presence,ble_rssi,ble_scanner + # Comma-joined subset of the native-IDF representative component list, + # computed by script/determine-jobs.py (native_idf_components_to_test). + # Single source of truth -- the full list lives in + # script/determine-jobs.py::NATIVE_IDF_TEST_COMPONENTS. + TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }} steps: - name: Check out code from GitHub uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 57b3c6eb88e..0a55b2a8481 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -495,6 +495,125 @@ def should_run_device_builder(branch: str | None = None) -> bool: return False +# Components tested by the native ESP-IDF compile-test job. This is the +# single source of truth: the workflow reads the comma-joined list from the +# `native-idf-components` output of `determine-jobs` and uses it as the +# `TEST_COMPONENTS` env on the `test-native-idf` job. +NATIVE_IDF_TEST_COMPONENTS = frozenset( + { + "esp32", + "api", + "heatpumpir", + "bme280_i2c", + "bh1750", + "aht10", + "esp32_ble", + "esp32_ble_beacon", + "esp32_ble_client", + "esp32_ble_server", + "esp32_ble_tracker", + "ble_client", + "ble_presence", + "ble_rssi", + "ble_scanner", + } +) + +# Path prefixes whose changes always trigger the native ESP-IDF compile +# test: anything under esphome/espidf/ (the native IDF runner / API / +# framework / component generator). +NATIVE_IDF_TRIGGER_PATH_PREFIXES = ("esphome/espidf/",) + +# Standalone files that, when changed, also trigger the native ESP-IDF +# compile test: +# - esphome/build_gen/espidf.py -- the native IDF build generator +# (other files under build_gen/ target PlatformIO and don't affect +# the native IDF path) +# - script/test_build_components.py -- the harness the job invokes +# - .github/workflows/ci.yml -- the job's own definition +NATIVE_IDF_TRIGGER_FILES = frozenset( + { + "esphome/build_gen/espidf.py", + "script/test_build_components.py", + ".github/workflows/ci.yml", + } +) + + +def _native_idf_path_or_file_trigger(files: list[str]) -> bool: + """Whether any changed file is a native IDF infrastructure / harness trigger.""" + for file in files: + if file in NATIVE_IDF_TRIGGER_FILES: + return True + if any(file.startswith(prefix) for prefix in NATIVE_IDF_TRIGGER_PATH_PREFIXES): + return True + return False + + +def native_idf_components_to_test(branch: str | None = None) -> list[str]: + """Subset of ``NATIVE_IDF_TEST_COMPONENTS`` the job needs to compile. + + The job builds components with the native ESP-IDF toolchain (no + PlatformIO). When only a specific component (or something it depends + on) changed, there's no value in re-building every other unrelated + component in the test list -- the regular ``component-test`` matrix + already covers them via PlatformIO. So we narrow to the intersection + of ``NATIVE_IDF_TEST_COMPONENTS`` and the changed-component dependency + closure. + + Returns the full list (sorted) when we can't safely narrow: + + 1. Core C++/Python files changed (``esphome/core/*``). + 2. Native IDF infrastructure changed (``esphome/espidf/*`` or + ``esphome/build_gen/espidf.py``). + 3. The test harness or workflow itself changed + (``script/test_build_components.py``, ``.github/workflows/ci.yml``). + + Otherwise returns the intersection (sorted), which may be empty -- an + empty list signals the job should be skipped. + + The dependency closure is derived from ``files`` via + ``get_components_with_dependencies()`` (the same primitive ``main()`` + uses) so the result honors ``branch``. ``get_changed_components()`` + is deliberately not used here: it re-invokes ``changed_files()`` with + its own default branch, which would silently ignore our ``branch`` + argument. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + Sorted list of component names to compile. + """ + files = changed_files(branch) + + if core_changed(files) or _native_idf_path_or_file_trigger(files): + return sorted(NATIVE_IDF_TEST_COMPONENTS) + + component_files = [f for f in files if filter_component_and_test_files(f)] + changed = get_components_with_dependencies(component_files, True) + + return sorted(NATIVE_IDF_TEST_COMPONENTS & set(changed)) + + +def should_run_native_idf(branch: str | None = None) -> bool: + """Determine if the `test-native-idf` compile-test job should run. + + Runs whenever ``native_idf_components_to_test()`` returns a non-empty + list. Skipping the job on unrelated Python-only PRs avoids ~5 min of + CI per PR (worse on cold caches). The regular ``component-test`` + matrix still exercises the same components through PlatformIO when + those components change. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if the native ESP-IDF compile test should run, False otherwise. + """ + return bool(native_idf_components_to_test(branch)) + + def determine_cpp_unit_tests( branch: str | None = None, ) -> tuple[bool, list[str]]: @@ -957,6 +1076,8 @@ def main() -> None: run_python_linters = should_run_python_linters(args.branch) run_import_time = should_run_import_time(args.branch) run_device_builder = should_run_device_builder(args.branch) + native_idf_components = native_idf_components_to_test(args.branch) + run_native_idf = bool(native_idf_components) changed_cpp_file_count = count_changed_cpp_files(args.branch) # Get changed components @@ -1102,6 +1223,8 @@ def main() -> None: "python_linters": run_python_linters, "import_time": run_import_time, "device_builder": run_device_builder, + "native_idf": run_native_idf, + "native_idf_components": ",".join(native_idf_components), "changed_components": changed_components, "changed_components_with_tests": changed_components_with_tests, "directly_changed_components_with_tests": list(directly_changed_with_tests), diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 5e2dd670dc1..9139c6e0956 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -70,6 +70,17 @@ def mock_should_run_device_builder() -> Generator[Mock, None, None]: yield mock +@pytest.fixture +def mock_native_idf_components_to_test() -> Generator[Mock, None, None]: + """Mock native_idf_components_to_test from determine_jobs. + + main() drives both the ``native_idf`` boolean output and the + ``native_idf_components`` CSV from this one function. + """ + with patch.object(determine_jobs, "native_idf_components_to_test") as mock: + yield mock + + @pytest.fixture def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]: """Mock determine_cpp_unit_tests from helpers.""" @@ -107,6 +118,7 @@ def test_main_all_tests_should_run( mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, mock_should_run_device_builder: Mock, + mock_native_idf_components_to_test: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -122,6 +134,7 @@ def test_main_all_tests_should_run( mock_should_run_python_linters.return_value = True mock_should_run_import_time.return_value = True mock_should_run_device_builder.return_value = True + mock_native_idf_components_to_test.return_value = ["api", "esp32"] mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -203,6 +216,8 @@ def test_main_all_tests_should_run( assert output["python_linters"] is True assert output["import_time"] is True assert output["device_builder"] is True + assert output["native_idf"] is True + assert output["native_idf_components"] == "api,esp32" assert output["changed_components"] == ["wifi", "api", "sensor"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -236,6 +251,7 @@ def test_main_no_tests_should_run( mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, mock_should_run_device_builder: Mock, + mock_native_idf_components_to_test: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -251,6 +267,7 @@ def test_main_no_tests_should_run( mock_should_run_python_linters.return_value = False mock_should_run_import_time.return_value = False mock_should_run_device_builder.return_value = False + mock_native_idf_components_to_test.return_value = [] mock_determine_cpp_unit_tests.return_value = (False, []) # Mock changed_files to return no component files @@ -291,6 +308,8 @@ def test_main_no_tests_should_run( assert output["python_linters"] is False assert output["import_time"] is False assert output["device_builder"] is False + assert output["native_idf"] is False + assert output["native_idf_components"] == "" assert output["changed_components"] == [] assert output["changed_components_with_tests"] == [] assert output["component_test_count"] == 0 @@ -313,6 +332,7 @@ def test_main_with_branch_argument( mock_should_run_python_linters: Mock, mock_should_run_import_time: Mock, mock_should_run_device_builder: Mock, + mock_native_idf_components_to_test: Mock, mock_changed_files: Mock, mock_determine_cpp_unit_tests: Mock, capsys: pytest.CaptureFixture[str], @@ -328,6 +348,7 @@ def test_main_with_branch_argument( mock_should_run_python_linters.return_value = True mock_should_run_import_time.return_value = True mock_should_run_device_builder.return_value = True + mock_native_idf_components_to_test.return_value = ["esp32"] mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"]) # Mock changed_files to return non-component files (to avoid memory impact) @@ -366,6 +387,7 @@ def test_main_with_branch_argument( mock_should_run_python_linters.assert_called_once_with("main") mock_should_run_import_time.assert_called_once_with("main") mock_should_run_device_builder.assert_called_once_with("main") + mock_native_idf_components_to_test.assert_called_once_with("main") # Check output captured = capsys.readouterr() @@ -379,6 +401,8 @@ def test_main_with_branch_argument( assert output["python_linters"] is True assert output["import_time"] is True assert output["device_builder"] is True + assert output["native_idf"] is True + assert output["native_idf_components"] == "esp32" assert output["changed_components"] == ["mqtt"] # changed_components_with_tests will only include components that actually have test files assert "changed_components_with_tests" in output @@ -827,6 +851,142 @@ def test_should_run_device_builder_skips_beta_release(target_branch: str) -> Non mock_changed.assert_not_called() +_NATIVE_IDF_FULL_LIST_FILES = [ + # Core C++/Python changes -- caught by core_changed() + ["esphome/core/component.cpp"], + ["esphome/core/config.py"], + # Native IDF infrastructure paths + ["esphome/espidf/framework.py"], + ["esphome/espidf/component.py"], + ["esphome/espidf/api.py"], + ["esphome/build_gen/espidf.py"], + # Workflow / harness files + ["script/test_build_components.py"], + [".github/workflows/ci.yml"], +] + + +@pytest.mark.parametrize("changed_files", _NATIVE_IDF_FULL_LIST_FILES) +def test_native_idf_components_to_test_returns_full_list_on_infrastructure( + changed_files: list[str], +) -> None: + """Infrastructure / core / harness changes fall back to the full component list.""" + with ( + patch.object(determine_jobs, "changed_files", return_value=changed_files), + # The dep-closure path shouldn't be consulted at all -- if it is, + # the obviously-wrong "wifi" sneaks in and the assertion catches it. + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=["wifi"] + ), + ): + result = determine_jobs.native_idf_components_to_test() + assert result == sorted(determine_jobs.NATIVE_IDF_TEST_COMPONENTS) + + +@pytest.mark.parametrize( + ("changed_files", "dependency_closure", "expected"), + [ + # Single tested component changed -- narrow to just that component. + ( + ["esphome/components/esp32/__init__.py"], + ["esp32"], + ["esp32"], + ), + # Dependency closure: multiple BLE components in the changed set + # are all intersected with the test list and returned sorted. + ( + ["esphome/components/esp32_ble/ble.cpp"], + ["esp32_ble", "esp32_ble_tracker", "ble_scanner"], + ["ble_scanner", "esp32_ble", "esp32_ble_tracker"], + ), + # api in the test set -- narrow to [api] even though the closure + # has other (unrelated to native-IDF coverage) entries. + ( + ["esphome/components/api/api_connection.cpp"], + ["api", "logger"], + ["api"], + ), + # Components outside the test set return an empty list (job skipped). + ( + ["esphome/components/wifi/wifi_component.cpp"], + ["wifi", "network"], + [], + ), + # Pure Python-only change outside trigger paths -> empty. + (["esphome/yaml_util.py"], [], []), + # Non-IDF files in esphome/build_gen/ do NOT trigger the full + # list -- only esphome/build_gen/espidf.py is a trigger. + (["esphome/build_gen/platformio.py"], [], []), + # Docs / unrelated files -> empty. + (["README.md"], [], []), + ([], [], []), + ], +) +def test_native_idf_components_to_test_narrowing( + changed_files: list[str], + dependency_closure: list[str], + expected: list[str], +) -> None: + """Component changes narrow the test list to the intersection.""" + with ( + patch.object(determine_jobs, "changed_files", return_value=changed_files), + patch.object( + determine_jobs, + "get_components_with_dependencies", + return_value=dependency_closure, + ), + ): + result = determine_jobs.native_idf_components_to_test() + assert result == expected + + +def test_native_idf_components_to_test_with_branch() -> None: + """native_idf_components_to_test passes branch argument through. + + Regression test: an earlier version called ``get_changed_components()``, + which silently ignored the branch argument because that helper re-runs + ``changed_files()`` with its own default. The current implementation + derives the closure from ``files = changed_files(branch)`` directly, + so a branch arg has to flow through ``changed_files``. + """ + with ( + patch.object(determine_jobs, "changed_files") as mock_changed, + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=[] + ), + ): + mock_changed.return_value = [] + determine_jobs.native_idf_components_to_test("release") + mock_changed.assert_called_once_with("release") + + +@pytest.mark.parametrize( + ("components_to_test", "expected"), + [ + ([], False), + (["esp32"], True), + (["esp32", "api"], True), + ], +) +def test_should_run_native_idf(components_to_test: list[str], expected: bool) -> None: + """should_run_native_idf is a thin wrapper around the component list.""" + with patch.object( + determine_jobs, + "native_idf_components_to_test", + return_value=components_to_test, + ): + assert determine_jobs.should_run_native_idf() is expected + + +def test_should_run_native_idf_with_branch() -> None: + """Test should_run_native_idf passes branch argument through.""" + with patch.object( + determine_jobs, "native_idf_components_to_test", return_value=[] + ) as mock_inner: + determine_jobs.should_run_native_idf("release") + mock_inner.assert_called_once_with("release") + + @pytest.mark.parametrize( ("changed_files", "expected_result"), [ From 8bce32ec35b3506bfc5950487d7c5bf8df18e06f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 10:01:26 -0500 Subject: [PATCH 1971/2056] [tests] Cover top-level !include failure path in track_yaml_loads (#16396) --- tests/unit_tests/test_yaml_util.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 3815ac1d752..ace92fbf6f8 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -390,6 +390,21 @@ def test_track_yaml_loads_cleanup_on_exception(tmp_path: Path) -> None: assert len(yaml_util._load_listeners) == before +def test_track_yaml_loads_no_duplicate_load_on_top_level_include_failure( + tmp_path: Path, +) -> None: + """A failed top-level !include must not record any file twice in track_yaml_loads.""" + main = tmp_path / "main.yaml" + main.write_text("!include missing.yaml\n") + + with yaml_util.track_yaml_loads() as loaded, pytest.raises(EsphomeError): + yaml_util.load_yaml(main) + + assert len(loaded) == len(set(loaded)), ( + f"Files loaded more than once during a failed top-level include: {loaded}" + ) + + @pytest.mark.parametrize( "data", [ From cb520cda6bf111cb333a8b71ffb067a00df7ab7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 10:01:42 -0500 Subject: [PATCH 1972/2056] [core] Retry PlatformIO downloads on transport-layer errors (#16397) --- esphome/platformio/runner.py | 12 +- tests/unit_tests/test_platformio_toolchain.py | 121 ++++++++++++++++++ 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/esphome/platformio/runner.py b/esphome/platformio/runner.py index 976979dc57b..caab47dcc24 100644 --- a/esphome/platformio/runner.py +++ b/esphome/platformio/runner.py @@ -51,9 +51,13 @@ def patch_file_downloader() -> None: """Retry PlatformIO package downloads with exponential backoff. PlatformIO's ``FileDownloader`` uses an ``HTTPSession`` without built-in - retry for 502/503 errors. We wrap ``__init__`` to retry on - ``PackageException`` and close the session between attempts so a new - TCP connection can route to a different CDN edge node. + retry. We wrap ``__init__`` to retry on transient failures and close the + session between attempts so a new TCP connection can route to a different + CDN edge node. We catch both ``PackageException`` (raised when the server + returns a non-200 status such as 502/503) and ``OSError`` -- which covers + ``requests.exceptions.ConnectionError``, ``ReadTimeout``, and + ``ChunkedEncodingError`` (all subclasses of ``OSError``) that get raised + when the connection is aborted before a response is parsed. """ from platformio.package.download import FileDownloader from platformio.package.exception import PackageException @@ -70,7 +74,7 @@ def patch_file_downloader() -> None: try: original_init(self, *args, **kwargs) return - except PackageException as e: + except (PackageException, OSError) as e: if attempt < max_retries - 1: delay = 2 ** (attempt + 1) _LOGGER.warning( diff --git a/tests/unit_tests/test_platformio_toolchain.py b/tests/unit_tests/test_platformio_toolchain.py index f771437dd47..c1d16530cbf 100644 --- a/tests/unit_tests/test_platformio_toolchain.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -2,10 +2,13 @@ # pylint: disable=protected-access +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import os from pathlib import Path import shutil +import threading from types import SimpleNamespace from unittest.mock import MagicMock, Mock, call, patch @@ -867,6 +870,56 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() -> mock_session.close.assert_called_once() +def test_patch_file_downloader_retries_on_connection_error() -> None: + """Test patch_file_downloader retries on transport-layer errors (OSError subclasses). + + ``requests.exceptions.ConnectionError`` and ``ReadTimeout`` subclass + ``OSError`` and are raised when the connection is aborted before any HTTP + response is parsed -- e.g. ``RemoteDisconnected`` mid-download. These must + retry too, not just ``PackageException``. + """ + mock_exception_cls = type("PackageException", (Exception,), {}) + call_count = 0 + + def failing_init(self, *args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise ConnectionError( + f"Connection aborted attempt {call_count}: RemoteDisconnected" + ) + + with ( + patch.dict( + "sys.modules", + { + "platformio": MagicMock(), + "platformio.package": MagicMock(), + "platformio.package.download": SimpleNamespace( + FileDownloader=type( + "FileDownloader", (), {"__init__": failing_init} + ) + ), + "platformio.package.exception": SimpleNamespace( + PackageException=mock_exception_cls + ), + }, + ), + patch("time.sleep") as mock_sleep, + ): + runner.patch_file_downloader() + + from platformio.package.download import FileDownloader + + instance = object.__new__(FileDownloader) + FileDownloader.__init__(instance, "http://example.com/file.zip") + + assert call_count == 3 + assert mock_sleep.call_count == 2 + mock_sleep.assert_any_call(2) + mock_sleep.assert_any_call(4) + + def test_patch_file_downloader_idempotent() -> None: """Test patch_file_downloader does not stack wrappers when called multiple times.""" mock_exception_cls = type("PackageException", (Exception,), {}) @@ -903,6 +956,74 @@ def test_patch_file_downloader_idempotent() -> None: assert call_count == 1 +@contextmanager +def _flaky_http_server(fail_first_n: int, fail_mode: str): + """Local HTTP server that fails the first ``fail_first_n`` requests. + + ``fail_mode="drop"`` closes the TCP connection without responding, so + the client raises ``RemoteDisconnected`` -- the exact CI failure mode. + ``fail_mode="502"`` returns an HTTP 502, triggering ``PackageException``. + """ + state = {"hits": 0} + + class _Handler(BaseHTTPRequestHandler): + def handle_one_request(self) -> None: + state["hits"] += 1 + if state["hits"] <= fail_first_n and fail_mode == "drop": + return # Skip read+respond → kernel sends FIN → RemoteDisconnected + super().handle_one_request() + + def do_GET(self) -> None: # noqa: N802 + if state["hits"] <= fail_first_n and fail_mode == "502": + self.send_error(502) + return + body = b"esphome-test-payload" + self.send_response(200) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass # silence default stderr logging + + server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield server.server_address[1], state + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) + + +@pytest.mark.parametrize("fail_mode", ["drop", "502"]) +def test_patch_file_downloader_recovers_against_real_server( + tmp_path: Path, fail_mode: str +) -> None: + """End-to-end: real PlatformIO ``FileDownloader`` against a local server + that fails twice then succeeds. Exercises the real + requests/urllib3/http.client stack for both failure modes: + + - ``drop``: TCP close mid-request → ``RemoteDisconnected`` → caught as + ``OSError`` by the retry patch (the CI failure path). + - ``502``: HTTP error response → ``PackageException`` (the original path). + """ + runner.patch_file_downloader() + from platformio.package.download import FileDownloader + + with ( + _flaky_http_server(fail_first_n=2, fail_mode=fail_mode) as (port, state), + patch("time.sleep"), + ): + fd = FileDownloader(f"http://127.0.0.1:{port}/payload.bin") + fd.set_destination(str(tmp_path / "out.bin")) + fd.start(with_progress=False, silent=True) + + assert state["hits"] == 3 # 2 failures + 1 success + assert (tmp_path / "out.bin").read_bytes() == b"esphome-test-payload" + + def _filter_through_redirect(line: str) -> str: """Write a line through RedirectText with FILTER_PLATFORMIO_LINES and return what passes.""" import io From 3fee97ae5a81e67f0bc0c2a2f3437949f396ad48 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 12:08:51 -0400 Subject: [PATCH 1973/2056] [espidf] Partition pio_components cache by framework (#16401) --- esphome/espidf/component.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index bb675d2c77c..8cd77dc6d1b 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -86,7 +86,10 @@ class URLSource(Source): self.url = url def download(self, dir_suffix: str, force: bool = False) -> Path: - base_dir = Path(CORE.data_dir) / DOMAIN + # Partition by framework: generated idf_component.yml content + # depends on CORE.using_arduino, so caches can't be shared. + framework = "arduino" if CORE.using_arduino else "idf" + base_dir = Path(CORE.data_dir) / DOMAIN / framework h = hashlib.new("sha256") h.update(self.url.encode()) path = base_dir / h.hexdigest()[:8] / dir_suffix @@ -121,11 +124,12 @@ class GitSource(Source): self.ref = ref def download(self, dir_suffix: str, force: bool = False) -> Path: + framework = "arduino" if CORE.using_arduino else "idf" path, _ = git.clone_or_update( url=self.url, ref=self.ref, refresh=git.NEVER_REFRESH if not force else None, - domain=DOMAIN, + domain=f"{DOMAIN}/{framework}", submodules=[], subpath=Path(dir_suffix), ) From d7b00047bd2d359c4ca56d38db8c012d9965e835 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 12:27:06 -0400 Subject: [PATCH 1974/2056] [espidf] Emit -W warning flags at project scope so managed components also see them (#16403) --- esphome/build_gen/espidf.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index dfe2d72b9de..82c8537befd 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -54,11 +54,22 @@ def get_project_cmakelists() -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") - # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag for flag in sorted(CORE.build_flags) if flag.startswith("-D")] + # Project-wide compile options: -D defines and -W warning flags (skip + # -Wl, linker flags — those go on the src component via + # target_link_options below). Emitted via idf_build_set_property so the + # flags propagate to every IDF component (including managed ones like + # esphome__micro-mp3) rather than just src/. Required so suppressions + # like ``-Wno-error=maybe-uninitialized`` actually silence warnings in + # third-party components we don't author. + project_compile_opts = [ + flag + for flag in sorted(CORE.build_flags) + if flag.startswith("-D") + or (flag.startswith("-W") and not flag.startswith("-Wl,")) + ] extra_compile_options = "\n".join( - f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' - for compile_def in compile_defs + f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)' + for flag in project_compile_opts ) return f"""\ @@ -107,15 +118,9 @@ def get_component_cmakelists(minimal: bool = False) -> str: idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) - # Extract compile options (-W flags, excluding linker flags) - compile_opts = [ - flag - for flag in CORE.build_flags - if flag.startswith("-W") and not flag.startswith("-Wl,") - ] - compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else "" - - # Extract linker options (-Wl, flags) + # Extract linker options (-Wl, flags). Compile flags (-D, -W) are + # emitted project-wide via idf_build_set_property in + # get_project_cmakelists so they reach every component, not just src/. link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else "" @@ -137,11 +142,6 @@ idf_component_register( # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) -# ESPHome compile options -target_compile_options(${{COMPONENT_LIB}} PUBLIC - {compile_opts_str} -) - # ESPHome linker options target_link_options(${{COMPONENT_LIB}} PUBLIC {link_opts_str} From 445d841229ec155fa13c6752ec54451fde553e1b Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Wed, 13 May 2026 18:49:32 +0200 Subject: [PATCH 1975/2056] [mitsubishi_cn105] Simplified protocol lookups (#16399) --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 56f1ee1b3f4..3a42616d8a6 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -30,44 +30,53 @@ static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41; static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61; -static constexpr std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = { - std::nullopt, // 0x00 +template<auto Unknown, size_t N> struct LookupMap { + using value_type = decltype(Unknown); + static constexpr auto UNKNOWN_VALUE = Unknown; + const std::array<value_type, N> table; + + constexpr value_type lookup(uint8_t raw) const { return (raw < N) ? this->table[raw] : UNKNOWN_VALUE; } + + constexpr bool reverse_lookup(value_type value, uint8_t &out) const { + static_assert(N <= std::numeric_limits<uint8_t>::max()); + if (value == UNKNOWN_VALUE) { + return false; + } + for (uint8_t i = 0; i < static_cast<uint8_t>(N); ++i) { + if (this->table[i] == value) { + out = i; + return true; + } + } + return false; + } +}; + +template<auto Unknown, class T, std::size_t N> static constexpr auto make_map(const T (&values)[N]) { + return LookupMap<Unknown, N>{std::to_array(values)}; +} + +static constexpr auto PROTOCOL_MODE_MAP = make_map<MitsubishiCN105::Mode::UNKNOWN>({ + MitsubishiCN105::Mode::UNKNOWN, // 0x00 MitsubishiCN105::Mode::HEAT, // 0x01 MitsubishiCN105::Mode::DRY, // 0x02 MitsubishiCN105::Mode::COOL, // 0x03 - std::nullopt, // 0x04 - std::nullopt, // 0x05 - std::nullopt, // 0x06 + MitsubishiCN105::Mode::UNKNOWN, // 0x04 + MitsubishiCN105::Mode::UNKNOWN, // 0x05 + MitsubishiCN105::Mode::UNKNOWN, // 0x06 MitsubishiCN105::Mode::FAN_ONLY, // 0x07 MitsubishiCN105::Mode::AUTO // 0x08 -}; +}); -static constexpr std::array<std::optional<MitsubishiCN105::FanMode>, 7> PROTOCOL_FAN_MODE_MAP = { +static constexpr auto PROTOCOL_FAN_MODE_MAP = make_map<MitsubishiCN105::FanMode::UNKNOWN>({ MitsubishiCN105::FanMode::AUTO, // 0x00 MitsubishiCN105::FanMode::QUIET, // 0x01 MitsubishiCN105::FanMode::SPEED_1, // 0x02 MitsubishiCN105::FanMode::SPEED_2, // 0x03 - std::nullopt, // 0x04 + MitsubishiCN105::FanMode::UNKNOWN, // 0x04 MitsubishiCN105::FanMode::SPEED_3, // 0x05 MitsubishiCN105::FanMode::SPEED_4 // 0x06 -}; - -template<typename T, size_t N> -static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> &table, uint8_t value) { - return (value < N) ? table[value] : std::nullopt; -} - -template<typename T, size_t N> -static constexpr bool reverse_lookup(const std::array<std::optional<T>, N> &table, T value, uint8_t &placeholder) { - for (size_t i = 0; i < N; ++i) { - const auto &table_value = table[i]; - if (table_value.has_value() && table_value == value) { - placeholder = i; - return true; - } - } - return false; -} +}); static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); @@ -323,11 +332,11 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) if (!this->pending_updates_.contains(UpdateFlag::MODE)) { const bool i_see = payload[3] > 0x08; - this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); + this->status_.mode = PROTOCOL_MODE_MAP.lookup(payload[3] - (i_see ? 0x08 : 0)); } if (!this->pending_updates_.contains(UpdateFlag::FAN)) { - this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); + this->status_.fan_mode = PROTOCOL_FAN_MODE_MAP.lookup(payload[5]); } return true; @@ -382,7 +391,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) { void MitsubishiCN105::set_mode(Mode mode) { uint8_t placeholder; - if (!reverse_lookup(PROTOCOL_MODE_MAP, mode, placeholder)) { + if (!PROTOCOL_MODE_MAP.reverse_lookup(mode, placeholder)) { ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode)); return; } @@ -392,7 +401,7 @@ void MitsubishiCN105::set_mode(Mode mode) { void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { uint8_t placeholder; - if (!reverse_lookup(PROTOCOL_FAN_MODE_MAP, fan_mode, placeholder)) { + if (!PROTOCOL_FAN_MODE_MAP.reverse_lookup(fan_mode, placeholder)) { ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode)); return; } @@ -432,12 +441,12 @@ void MitsubishiCN105::apply_settings_() { } if (this->pending_updates_.contains(UpdateFlag::MODE) && - reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) { + PROTOCOL_MODE_MAP.reverse_lookup(this->status_.mode, payload[4])) { payload[1] |= 0x02; } if (this->pending_updates_.contains(UpdateFlag::FAN) && - reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) { + PROTOCOL_FAN_MODE_MAP.reverse_lookup(this->status_.fan_mode, payload[6])) { payload[1] |= 0x08; } From 03f5e4775cee9b13d8a8d09af63117f6e1687cae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 12:06:20 -0500 Subject: [PATCH 1976/2056] [tests] Add CodSpeed benchmark for compiled-config cache fast path (#16402) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 5 +- requirements_test.txt | 5 + tests/benchmarks/python/__init__.py | 0 tests/benchmarks/python/conftest.py | 22 ++++ .../fixtures/bluetooth_proxy_device.yaml | 62 ++++++++++ .../python/test_compiled_config_bench.py | 116 ++++++++++++++++++ 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/benchmarks/python/__init__.py create mode 100644 tests/benchmarks/python/conftest.py create mode 100644 tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml create mode 100644 tests/benchmarks/python/test_compiled_config_bench.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06c6c0fec17..819dac926e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -423,7 +423,10 @@ jobs: - name: Run CodSpeed benchmarks uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 with: - run: ${{ steps.build.outputs.binary }} + run: | + . venv/bin/activate + ${{ steps.build.outputs.binary }} + pytest tests/benchmarks/python/ --codspeed --no-cov mode: simulation clang-tidy-single: diff --git a/requirements_test.txt b/requirements_test.txt index 568d79d6764..218bc0083c9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,10 @@ pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 +# CodSpeed benchmarks under tests/benchmarks/python/ +# (skipped via pytest.importorskip when missing -- only required for the +# benchmarks job in .github/workflows/ci.yml) +pytest-codspeed==5.0.1 + # Used by the import-time regression check (.github/workflows/ci.yml → import-time job) importtime-waterfall==1.0.0 diff --git a/tests/benchmarks/python/__init__.py b/tests/benchmarks/python/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/benchmarks/python/conftest.py b/tests/benchmarks/python/conftest.py new file mode 100644 index 00000000000..9b0f1a3d2bb --- /dev/null +++ b/tests/benchmarks/python/conftest.py @@ -0,0 +1,22 @@ +"""Shared fixtures for the Python benchmark suite.""" + +from __future__ import annotations + +from collections.abc import Generator + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core_state() -> Generator[None]: + """Reset CORE before and after every benchmark. + + Per-iteration setups inside benchmarks reset CORE for the loop body; + this fixture handles the test-level boundary so stale state from + fixture priming doesn't leak across benchmarks. + """ + CORE.reset() + yield + CORE.reset() diff --git a/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml b/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml new file mode 100644 index 00000000000..dfa5a487b85 --- /dev/null +++ b/tests/benchmarks/python/fixtures/bluetooth_proxy_device.yaml @@ -0,0 +1,62 @@ +substitutions: + devicename: bluetooth_proxy_device + friendly_name: bluetooth_proxy_device + +esphome: + name: $devicename + friendly_name: $friendly_name + +esp32: + board: esp32-poe-iso + framework: + type: esp-idf + advanced: + sram1_as_iram: true + minimum_chip_revision: "3.0" + +esp32_ble_tracker: + scan_parameters: + active: false + +bluetooth_proxy: + active: true + +ethernet: + type: LAN8720 + mdc_pin: GPIO23 + mdio_pin: GPIO18 + clk_mode: GPIO17_OUT + phy_addr: 0 + power_pin: GPIO12 + +debug: +logger: +api: +ota: + platform: esphome + +button: + - platform: restart + name: Restart + +time: + - platform: homeassistant + id: homeassistant_time + - platform: sntp + id: sntp_time + +sensor: + - platform: uptime + name: Ethernet Uptime + - platform: template + name: Free Memory + lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + unit_of_measurement: B + state_class: measurement + - platform: debug + free: + name: Heap Free + fragmentation: + name: Heap Fragmentation + min_free: + name: Heap Min Free diff --git a/tests/benchmarks/python/test_compiled_config_bench.py b/tests/benchmarks/python/test_compiled_config_bench.py new file mode 100644 index 00000000000..5c8892f8d00 --- /dev/null +++ b/tests/benchmarks/python/test_compiled_config_bench.py @@ -0,0 +1,116 @@ +"""CodSpeed benchmarks for the validated-config cache fast path. + +PR #16381 added a cache that lets ``esphome upload`` / ``esphome logs`` +skip re-running the full config-validation pipeline. These benchmarks +compare the cached path (``load_compiled_config``) against the slow +path (``read_config``) on the same input. + +The fixture YAML is a modest bluetooth-proxy device. The two paths +end up close on a config this small -- the win grows with config +complexity (external components, large package trees, deeply nested +schemas), where the slow path can be orders of magnitude slower than +the cache load. + +Skipped when ``pytest-codspeed`` isn't installed so the regular +unit-test suite keeps working unchanged. +""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +import shutil +from typing import Any + +import pytest + +from esphome.compiled_config import compiled_config_path, load_compiled_config +from esphome.config import read_config +from esphome.core import CORE +from esphome.storage_json import ext_storage_path +from esphome.writer import update_storage_json + +pytest.importorskip("pytest_codspeed") + +HERE = Path(__file__).parent +FIXTURE_YAML = HERE / "fixtures" / "bluetooth_proxy_device.yaml" + + +def _stage_yaml(tmp_path: Path) -> Path: + """Copy fixture YAML into a fresh tmp dir. + + Each benchmark gets its own copy so the cache files (under + ``.esphome/storage/`` next to the YAML) don't bleed between cases. + """ + target = tmp_path / FIXTURE_YAML.name + shutil.copy2(FIXTURE_YAML, target) + return target + + +def _prime_cache(yaml_path: Path) -> None: + """Run full validation once and persist the cache + sidecar. + + Mirrors ``esphome compile``: ``read_config`` populates ``CORE.config``, + then ``update_storage_json`` writes both the StorageJSON sidecar and + the ``.validated.yaml`` compiled-config cache. + """ + CORE.config_path = yaml_path + config = read_config({}, skip_external_update=True) + assert config is not None, f"fixture YAML failed to validate: {yaml_path}" + CORE.config = config + update_storage_json() + + +@pytest.fixture +def staged_yaml(tmp_path: Path) -> Path: + """YAML copied into tmp_path; no cache files written yet.""" + return _stage_yaml(tmp_path) + + +@pytest.fixture +def primed_yaml(staged_yaml: Path) -> Path: + """YAML plus a fresh cache + sidecar on disk.""" + _prime_cache(staged_yaml) + assert compiled_config_path(staged_yaml.name).is_file() + assert ext_storage_path(staged_yaml.name).is_file() + return staged_yaml + + +def _resetting_setup( + yaml_path: Path, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Callable[[], tuple[tuple[Any, ...], dict[str, Any]]]: + """Build a per-iteration setup that resets CORE and re-pins config_path.""" + + def setup() -> tuple[tuple[Any, ...], dict[str, Any]]: + CORE.reset() + CORE.config_path = yaml_path + return args, kwargs + + return setup + + +def test_load_compiled_config_cached(primed_yaml: Path, benchmark) -> None: + """Fast path: deserialize the cached, already-validated config.""" + benchmark.pedantic( + load_compiled_config, + setup=_resetting_setup(primed_yaml, (primed_yaml,), {}), + rounds=5, + iterations=1, + ) + + +def test_read_config_uncached(primed_yaml: Path, benchmark) -> None: + """Slow path: full validation pipeline (yaml load + schema + components). + + Uses the same primed fixture as the cached path -- ``read_config`` + ignores the cache file on disk, so the two benchmarks measure the + same input from two different code paths. + """ + benchmark.pedantic( + read_config, + setup=_resetting_setup(primed_yaml, ({},), {"skip_external_update": True}), + rounds=3, + iterations=1, + ) From 1c6966b7612cc2ad2a86070b05fbce3f2234260c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 13:07:59 -0400 Subject: [PATCH 1977/2056] [espidf] Run PIO extraScript with SCons-env shim (#16404) --- esphome/espidf/component.py | 47 +++++-- esphome/espidf/extra_script.py | 161 ++++++++++++++++++++++ tests/unit_tests/test_espidf_component.py | 122 +++++++++++++++- 3 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 esphome/espidf/extra_script.py diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index 8cd77dc6d1b..af8640949d1 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -322,6 +322,36 @@ def _patch_component(component: IDFComponent, first_pass: bool): (component.path / "idf_component.yml").write_text("") +def _apply_extra_script(component: IDFComponent) -> None: + """Run a PIO ``extraScript`` and fold its captured env vars into + ``component.data["build"]["flags"]`` so the existing -L/-l/-D + extraction in ``generate_cmakelists_txt`` picks them up.""" + extra_script = component.data.get("build", {}).get("extraScript") + if not extra_script: + return + # Resolve and confine to the component dir so a malicious library.json + # can't escape (e.g. ``"extraScript": "../../etc/passwd"``). + library_root = component.path.resolve() + script_path = (component.path / extra_script).resolve() + if not script_path.is_relative_to(library_root) or not script_path.is_file(): + return + from esphome.components.esp32 import get_esp32_variant + from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script + + idf_target = get_esp32_variant().lower().replace("-", "") + result = run_extra_script( + script_path, library_dir=component.path, idf_target=idf_target + ) + extra_flags = captured_as_build_flags(result, library_dir=component.path) + if not extra_flags: + return + flags = component.data.setdefault("build", {}).setdefault("flags", []) + if isinstance(flags, str): + flags = [flags] + flags.extend(extra_flags) + component.data["build"]["flags"] = flags + + T = TypeVar("T") @@ -748,13 +778,6 @@ def _check_library_data(data: dict): if not valid_framework: raise InvalidIDFComponent(f"Unsupported library frameworks: {frameworks}") - extra_script = data.get("build", {}).get("extraScript", None) - if extra_script: - _LOGGER.warning( - 'Extra scripts are not supported. The script "%s" will not be executed.', - extra_script, - ) - def _process_dependencies(component: IDFComponent): """ @@ -899,9 +922,17 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone # Apply additional patches to the library metadata _patch_component(component, False) - # Check if the component is usable with ESP-IDF + # Check if the component is usable with ESP-IDF before executing any + # third-party Python from the library (``_apply_extra_script`` below). _check_library_data(component.data) + # If the library declares a PIO ``extraScript``, run it against a + # fake SCons env so we can fold its captured LIBPATH/LIBS/etc into + # the build-flag pipeline ``generate_cmakelists_txt`` consumes + # below. Without this, libraries that wire per-MCU archive linking + # via extraScript fail to link under native ESP-IDF. + _apply_extra_script(component) + # Handle the dependencies (convert PlatformIO library to ESP-IDF component if needed) _process_dependencies(component) diff --git a/esphome/espidf/extra_script.py b/esphome/espidf/extra_script.py new file mode 100644 index 00000000000..2f22f23c100 --- /dev/null +++ b/esphome/espidf/extra_script.py @@ -0,0 +1,161 @@ +"""Run a PlatformIO ``extraScript`` against a captured SCons-env stand-in. + +PlatformIO libraries occasionally configure per-target link/build state +via a Python ``extraScript`` declared in ``library.json``'s ``build`` +section instead of static fields. The script runs under SCons during +PIO's build and mutates the active ``Environment`` (``env.Append``, +``env.Replace``, …) — chiefly to set ``LIBPATH``/``LIBS`` per chip MCU. + +ESPHome's PIO→IDF converter (``_generate_idf_component``) doesn't run +SCons, so these scripts were previously ignored and any library +relying on them failed to link under ``toolchain: esp-idf``. This +module provides a small shim that ``exec``s an extra-script with a +fake ``env`` object, captures the common ``env.Append(...)`` calls, +and returns the captured vars so the caller can fold them back into +the library's generated CMakeLists. + +Caveats +------- +* Only the ``env.Append`` API is captured. ``env.Replace``, + ``env.Prepend``, ``env.AddPreAction``, SCons file generators, and any + arbitrary I/O are silently no-ops. Scripts that depend on those will + produce incomplete output. +* Running arbitrary Python from third-party libraries is a non-trivial + trust decision. The shim does no sandboxing — anything in the + script's process can run. Use only with libraries whose source you + trust. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import logging +import os +from pathlib import Path + +_LOGGER = logging.getLogger(__name__) + +# Keys we know how to translate back into ESPHome's build-flag pipeline. +# Other env.Append kwargs are recorded but ignored downstream. +_CAPTURED_KEYS = frozenset({"LIBPATH", "LIBS", "CPPDEFINES", "LINKFLAGS", "CPPFLAGS"}) + + +@dataclass +class ExtraScriptResult: + """Build-var deltas captured from a PIO extra-script ``env.Append`` call.""" + + libpath: list[str] = field(default_factory=list) + libs: list[str] = field(default_factory=list) + cppdefines: list[str | tuple[str, str]] = field(default_factory=list) + linkflags: list[str] = field(default_factory=list) + cppflags: list[str] = field(default_factory=list) + + +class _FakeSConsEnv: + """Minimal stand-in for SCons ``Environment`` exposed to extra-scripts. + + Implements just enough surface area to let scripts query ``BOARD_MCU`` + / ``PIOENV`` and call ``env.Append(LIBPATH=…, LIBS=…, …)``. Every + other env method swallows silently so unrelated calls don't raise + ``AttributeError`` and abort the script. + """ + + def __init__(self, *, board_mcu: str, pio_env: str) -> None: + self._vars: dict[str, str] = { + "BOARD_MCU": board_mcu, + "PIOPLATFORM": "espressif32", + "PIOENV": pio_env, + } + self.result = ExtraScriptResult() + + # ----- SCons env API the common scripts use ----- + + def get(self, key: str, default: str | None = None) -> str | None: + return self._vars.get(key, default) + + def Append(self, **kwargs) -> None: # noqa: N802 (SCons API name) + for key, value in kwargs.items(): + if key not in _CAPTURED_KEYS: + continue + items = list(value) if isinstance(value, (list, tuple)) else [value] + bucket = getattr(self.result, key.lower()) + bucket.extend(items) + + # ----- Everything else is a no-op so unsupported scripts don't crash ----- + + def __getattr__(self, name: str): + def _noop(*args, **kwargs): + return None + + return _noop + + +def run_extra_script( + script_path: Path, *, library_dir: Path, idf_target: str +) -> ExtraScriptResult: + """Execute ``script_path`` with a fake SCons env and return captured vars. + + ``idf_target`` is the active ESP-IDF target name (e.g. ``esp32``, + ``esp32s3``); it's exposed to the script as PlatformIO's + ``BOARD_MCU`` so chip-conditional logic resolves the same way it + would under PIO. The script runs with ``library_dir`` as the + process CWD so relative-path lookups (``join``, ``realpath``, + ``open``) resolve against the library tree. + + On any exception inside the script we log at debug level and return + an empty result — extra-scripts are best-effort, and an unsupported + script shouldn't block the build. + """ + env = _FakeSConsEnv(board_mcu=idf_target, pio_env=f"esphome_{idf_target}") + code = compile(script_path.read_text(), str(script_path), "exec") + old_cwd = os.getcwd() + try: + os.chdir(library_dir) + exec( # noqa: S102 pylint: disable=exec-used + code, + { + "Import": lambda *_args: None, # SCons-side import; harmless here + "env": env, + "__file__": str(script_path), + "__name__": "__pio_extra_script__", + }, + ) + except Exception as e: # pylint: disable=broad-exception-caught + _LOGGER.warning("PIO extra-script %s raised %s; skipping", script_path, e) + return ExtraScriptResult() + finally: + os.chdir(old_cwd) + return env.result + + +def captured_as_build_flags( + result: ExtraScriptResult, *, library_dir: Path +) -> list[str]: + """Translate captured env vars into the ``-L`` / ``-l`` / ``-D`` / + raw-flag form ``_generate_cmakelists_txt`` already knows how to consume. + + ``LIBPATH`` entries are made relative to ``library_dir`` so the + generated CMakeLists is portable; absolute paths outside the library + tree are kept as-is (CMake handles absolute paths in + ``target_link_directories`` fine). + """ + flags: list[str] = [] + library_root = library_dir.resolve() + for path in result.libpath: + # Anchor relative paths to library_dir (not the current CWD, which + # has been restored by the time we get here). Joining an absolute + # path against library_dir returns the absolute path unchanged. + resolved = (library_dir / path).resolve() + try: + flags.append(f"-L{resolved.relative_to(library_root)}") + except ValueError: + flags.append(f"-L{resolved}") + flags.extend(f"-l{lib}" for lib in result.libs) + for define in result.cppdefines: + if isinstance(define, tuple) and len(define) == 2: + flags.append(f"-D{define[0]}={define[1]}") + else: + flags.append(f"-D{define}") + flags.extend(result.linkflags) + flags.extend(result.cppflags) + return flags diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py index caef10eea32..3988c997a76 100644 --- a/tests/unit_tests/test_espidf_component.py +++ b/tests/unit_tests/test_espidf_component.py @@ -250,14 +250,126 @@ def test_check_library_data_invalid_framework(esp32_idf_core): _check_library_data({"platforms": "*", "frameworks": ["other"]}) -def test_extra_script_logs_warning(caplog, esp32_idf_core): - extra_script = "myscript.sh" +def test_extra_script_captures_libpath_libs_and_defines(tmp_path): + from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script + + (tmp_path / "src" / "esp32").mkdir(parents=True) + script = tmp_path / "extra_script.py" + script.write_text( + "Import('env')\n" + "mcu = env.get('BOARD_MCU')\n" + "env.Append(\n" + " LIBPATH=[join('src', mcu)],\n" + " LIBS=['algobsec'],\n" + " CPPDEFINES=['FOO', ('BAR', '1')],\n" + " LINKFLAGS=['-Wl,--gc-sections'],\n" + ")\n" + ) + # The script uses bare ``join`` (PIO's extra-scripts run inside SCons + # where this is in scope). Inject it via the script header so the + # shim's exec namespace can resolve it. + script.write_text("from os.path import join\n" + script.read_text()) + + result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") + + assert result.libpath == [os.path.join("src", "esp32")] + assert result.libs == ["algobsec"] + assert ("BAR", "1") in result.cppdefines + assert "FOO" in result.cppdefines + assert result.linkflags == ["-Wl,--gc-sections"] + + flags = captured_as_build_flags(result, library_dir=tmp_path) + sep = os.sep + assert f"-Lsrc{sep}esp32" in flags + assert "-lalgobsec" in flags + assert "-DFOO" in flags + assert "-DBAR=1" in flags + assert "-Wl,--gc-sections" in flags + + +def test_extra_script_libpath_relative_resolves_against_library_dir( + tmp_path, monkeypatch +): + """Relative LIBPATH entries must resolve against ``library_dir``, not the + caller's CWD (the shim restores CWD before ``captured_as_build_flags`` + runs).""" + from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags + + (tmp_path / "lib" / "esp32").mkdir(parents=True) + elsewhere = tmp_path.parent / "not_the_library_dir" + elsewhere.mkdir(exist_ok=True) + monkeypatch.chdir(elsewhere) + + result = ExtraScriptResult(libpath=["lib/esp32"]) + flags = captured_as_build_flags(result, library_dir=tmp_path) + + sep = os.sep + assert flags == [f"-Llib{sep}esp32"] + + +def test_extra_script_libpath_absolute_outside_library_dir(tmp_path): + from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags + + outside = tmp_path.parent / "system_lib" + outside.mkdir(exist_ok=True) + result = ExtraScriptResult(libpath=[str(outside)]) + + flags = captured_as_build_flags(result, library_dir=tmp_path) + assert flags == [f"-L{outside.resolve()}"] + + +def test_extra_script_failure_returns_empty_result(tmp_path, caplog): + from esphome.espidf.extra_script import run_extra_script + + script = tmp_path / "broken.py" + script.write_text("raise RuntimeError('boom')\n") with caplog.at_level("WARNING"): - _check_library_data({"build": {"extraScript": extra_script}}) + result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") - assert "not supported" in caplog.text - assert "myscript.sh" in caplog.text + assert result.libpath == [] + assert result.libs == [] + assert "broken.py" in caplog.text + + +def test_apply_extra_script_path_traversal_is_rejected(tmp_path): + from esphome.espidf.component import _apply_extra_script + + library_dir = tmp_path / "lib" + library_dir.mkdir() + outside = tmp_path / "evil.py" + outside.write_text("env.Append(LIBS=['pwned'])\n") + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = library_dir + c.data = {"build": {"extraScript": "../evil.py"}} + + _apply_extra_script(c) + + # Nothing was folded into flags: the traversal was rejected before + # the script could run. + assert "flags" not in c.data["build"] + + +def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch): + from esphome.components import esp32 as esp32_module + + monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32") + + from esphome.espidf.component import _apply_extra_script + + (tmp_path / "src").mkdir() + script = tmp_path / "extra.py" + script.write_text("env.Append(LIBS=['algobsec'])\n") + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}} + + _apply_extra_script(c) + + assert "-DEXISTING" in c.data["build"]["flags"] + assert "-lalgobsec" in c.data["build"]["flags"] def test_parse_library_json(tmp_path): From ce8810bc42984d9b69ca795106b039f8be95eecb Mon Sep 17 00:00:00 2001 From: Boris Krivonog <boris.krivonog@inova.si> Date: Wed, 13 May 2026 20:25:32 +0200 Subject: [PATCH 1978/2056] [mitsubishi_cn105] Add vane and wide-vane support (#16405) --- .../mitsubishi_cn105/mitsubishi_cn105.cpp | 90 +++++++++++++++++-- .../mitsubishi_cn105/mitsubishi_cn105.h | 35 +++++++- .../mitsubishi_cn105_climate.cpp | 2 +- .../climate/mitsubishi_cn105_tests.cpp | 67 +++++++++++++- tests/components/mitsubishi_cn105/common.h | 1 + 5 files changed, 178 insertions(+), 17 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 3a42616d8a6..4782a2ef93e 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -50,6 +50,11 @@ template<auto Unknown, size_t N> struct LookupMap { } return false; } + + constexpr bool is_valid(value_type value) const { + uint8_t raw; + return reverse_lookup(value, raw); + } }; template<auto Unknown, class T, std::size_t N> static constexpr auto make_map(const T (&values)[N]) { @@ -78,6 +83,33 @@ static constexpr auto PROTOCOL_FAN_MODE_MAP = make_map<MitsubishiCN105::FanMode: MitsubishiCN105::FanMode::SPEED_4 // 0x06 }); +static constexpr auto PROTOCOL_VANE_MODE_MAP = make_map<MitsubishiCN105::VaneMode::UNKNOWN>({ + MitsubishiCN105::VaneMode::AUTO, // 0x00 + MitsubishiCN105::VaneMode::POSITION_1, // 0x01 + MitsubishiCN105::VaneMode::POSITION_2, // 0x02 + MitsubishiCN105::VaneMode::POSITION_3, // 0x03 + MitsubishiCN105::VaneMode::POSITION_4, // 0x04 + MitsubishiCN105::VaneMode::POSITION_5, // 0x05 + MitsubishiCN105::VaneMode::UNKNOWN, // 0x06 + MitsubishiCN105::VaneMode::SWING // 0x07 +}); + +static constexpr auto PROTOCOL_WIDE_VANE_MODE_MAP = make_map<MitsubishiCN105::WideVaneMode::UNKNOWN>({ + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x00 + MitsubishiCN105::WideVaneMode::FAR_LEFT, // 0x01 + MitsubishiCN105::WideVaneMode::LEFT, // 0x02 + MitsubishiCN105::WideVaneMode::CENTER, // 0x03 + MitsubishiCN105::WideVaneMode::RIGHT, // 0x04 + MitsubishiCN105::WideVaneMode::FAR_RIGHT, // 0x05 + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x06 + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x07 + MitsubishiCN105::WideVaneMode::LEFT_RIGHT, // 0x08 + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x09 + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x0A + MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x0B + MitsubishiCN105::WideVaneMode::SWING // 0x0C +}); + static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); } @@ -91,7 +123,7 @@ static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, Payloa return packet; } -static float decode_temperature(int temp_a, int temp_b, int delta) { +static constexpr float decode_temperature(int temp_a, int temp_b, int delta) { return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a; } @@ -290,9 +322,10 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) this->set_state_(State::STATUS_UPDATED); } - bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode || - previous.fan_mode != this->status_.fan_mode || - previous.target_temperature != this->status_.target_temperature; + bool changed = + previous.power_on != this->status_.power_on || previous.mode != this->status_.mode || + previous.fan_mode != this->status_.fan_mode || previous.target_temperature != this->status_.target_temperature || + previous.vane_mode != this->status_.vane_mode || previous.wide_vane_mode != this->status_.wide_vane_mode; if (this->is_room_temperature_enabled()) { changed |= previous.room_temperature != this->status_.room_temperature; @@ -339,6 +372,15 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) this->status_.fan_mode = PROTOCOL_FAN_MODE_MAP.lookup(payload[5]); } + if (!this->pending_updates_.contains(UpdateFlag::VANE)) { + this->status_.vane_mode = PROTOCOL_VANE_MODE_MAP.lookup(payload[6]); + } + + this->set_wide_vane_high_bit_ = (payload[9] & 0xF0) == 0x80; + if (!this->pending_updates_.contains(UpdateFlag::WIDE_VANE)) { + this->status_.wide_vane_mode = PROTOCOL_WIDE_VANE_MODE_MAP.lookup(payload[9] & 0x0F); + } + return true; } @@ -390,8 +432,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) { } void MitsubishiCN105::set_mode(Mode mode) { - uint8_t placeholder; - if (!PROTOCOL_MODE_MAP.reverse_lookup(mode, placeholder)) { + if (!PROTOCOL_MODE_MAP.is_valid(mode)) { ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode)); return; } @@ -400,8 +441,7 @@ void MitsubishiCN105::set_mode(Mode mode) { } void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { - uint8_t placeholder; - if (!PROTOCOL_FAN_MODE_MAP.reverse_lookup(fan_mode, placeholder)) { + if (!PROTOCOL_FAN_MODE_MAP.is_valid(fan_mode)) { ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode)); return; } @@ -409,6 +449,24 @@ void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { this->pending_updates_.set(UpdateFlag::FAN); } +void MitsubishiCN105::set_vane_mode(VaneMode vane_mode) { + if (!PROTOCOL_VANE_MODE_MAP.is_valid(vane_mode)) { + ESP_LOGD(TAG, "Setting invalid vane mode: %u", static_cast<uint8_t>(vane_mode)); + return; + } + this->status_.vane_mode = vane_mode; + this->pending_updates_.set(UpdateFlag::VANE); +} + +void MitsubishiCN105::set_wide_vane_mode(WideVaneMode wide_vane_mode) { + if (!PROTOCOL_WIDE_VANE_MODE_MAP.is_valid(wide_vane_mode)) { + ESP_LOGD(TAG, "Setting invalid wide vane mode: %u", static_cast<uint8_t>(wide_vane_mode)); + return; + } + this->status_.wide_vane_mode = wide_vane_mode; + this->pending_updates_.set(UpdateFlag::WIDE_VANE); +} + void MitsubishiCN105::apply_settings_() { std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{}; @@ -450,7 +508,21 @@ void MitsubishiCN105::apply_settings_() { payload[1] |= 0x08; } - this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN); + if (this->pending_updates_.contains(UpdateFlag::VANE) && + PROTOCOL_VANE_MODE_MAP.reverse_lookup(this->status_.vane_mode, payload[7])) { + payload[1] |= 0x10; + } + + if (this->pending_updates_.contains(UpdateFlag::WIDE_VANE) && + PROTOCOL_WIDE_VANE_MODE_MAP.reverse_lookup(this->status_.wide_vane_mode, payload[13])) { + payload[2] |= 0x01; + if (this->set_wide_vane_high_bit_) { + payload[13] |= 0x80; + } + } + + this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN, + UpdateFlag::VANE, UpdateFlag::WIDE_VANE); } this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload)); diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 60ca81cf9e0..dbeb43068e9 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -29,12 +29,36 @@ class MitsubishiCN105 { UNKNOWN, }; + enum class VaneMode : uint8_t { + AUTO, + POSITION_1, + POSITION_2, + POSITION_3, + POSITION_4, + POSITION_5, + SWING, + UNKNOWN, + }; + + enum class WideVaneMode : uint8_t { + FAR_LEFT, + LEFT, + CENTER, + RIGHT, + FAR_RIGHT, + LEFT_RIGHT, + SWING, + UNKNOWN, + }; + struct Status { - bool power_on{false}; float target_temperature{NAN}; + float room_temperature{NAN}; + bool power_on{false}; Mode mode{Mode::UNKNOWN}; FanMode fan_mode{FanMode::UNKNOWN}; - float room_temperature{NAN}; + VaneMode vane_mode{VaneMode::UNKNOWN}; + WideVaneMode wide_vane_mode{WideVaneMode::UNKNOWN}; }; explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} @@ -61,6 +85,8 @@ class MitsubishiCN105 { void set_target_temperature(float target_temperature); void set_mode(Mode mode); void set_fan_mode(FanMode fan_mode); + void set_vane_mode(VaneMode vane_mode); + void set_wide_vane_mode(WideVaneMode mode); void set_remote_temperature(float temperature); void clear_remote_temperature(); @@ -98,7 +124,9 @@ class MitsubishiCN105 { POWER = 1, MODE = 2, FAN = 3, - REMOTE_TEMPERATURE = 4, + VANE = 4, + WIDE_VANE = 5, + REMOTE_TEMPERATURE = 6, }; struct UpdateFlags { @@ -142,6 +170,7 @@ class MitsubishiCN105 { State state_{State::NOT_CONNECTED}; UpdateFlags pending_updates_; bool use_temperature_encoding_b_{false}; + bool set_wide_vane_high_bit_{false}; FrameParser frame_parser_; uint8_t current_status_msg_type_{0}; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 284339e57f9..67a561397a1 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -56,7 +56,7 @@ void MitsubishiCN105Climate::dump_config() { ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms", this->hp_.get_room_temperature_min_interval()); } else { - ESP_LOGCONFIG(TAG, " Current temperature: disabled"); + ESP_LOGCONFIG(TAG, " Current temperature: DISABLED"); } ESP_LOGCONFIG(TAG, " Update interval: %" PRIu32 " ms\n" diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index db2fbced1c3..ef3cdd0fffe 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -53,13 +53,15 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { // Settings response ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99}); + 0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C}); // Settings should still have initial values EXPECT_FALSE(ctx.sut.status().power_on); EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::UNKNOWN); EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().vane_mode, MitsubishiCN105::VaneMode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::UNKNOWN); ctx.sut.set_current_time(300); ASSERT_FALSE(ctx.sut.update()); @@ -70,6 +72,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::AUTO); EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::AUTO); + EXPECT_EQ(ctx.sut.status().vane_mode, MitsubishiCN105::VaneMode::POSITION_4); + EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::SWING); // Now fetch room temperature (0x03) EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); @@ -303,6 +307,30 @@ TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) { EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f); } +TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitNotSet) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::CENTER); + EXPECT_FALSE(ctx.sut.set_wide_vane_high_bit_); +} + +TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitSet) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::CENTER); + EXPECT_TRUE(ctx.sut.set_wide_vane_high_bit_); +} + TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) { auto ctx = TestContext{}; @@ -365,6 +393,37 @@ TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) { 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73)); } +TEST(MitsubishiCN105Tests, ApplyVaneModeSwing) { + auto ctx = TestContext{}; + + ctx.sut.set_vane_mode(MitsubishiCN105::VaneMode::SWING); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66)); +} + +TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitNotSet) { + auto ctx = TestContext{}; + + ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x7A)); +} + +TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitSet) { + auto ctx = TestContext{}; + + ctx.sut.set_wide_vane_high_bit_ = true; + ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0xFA)); +} + TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { auto ctx = TestContext{}; @@ -391,15 +450,15 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { ctx.sut.set_target_temperature(25.0f); ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT); ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); + ctx.sut.set_vane_mode(MitsubishiCN105::VaneMode::AUTO); // Waiting for next status update must be interrupted and new values send to AC ctx.sut.set_current_time(6000); ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); - EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB)); - + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x1F, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xAB)); // Write ACK response ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index 73e09d6c84b..59b62037326 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -46,6 +46,7 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::state_; using MitsubishiCN105::operation_start_ms_; using MitsubishiCN105::use_temperature_encoding_b_; + using MitsubishiCN105::set_wide_vane_high_bit_; using MitsubishiCN105::status_update_wait_credit_ms_; using MitsubishiCN105::pending_updates_; From c8aba6913b97dbca2e77b768b96cfde93fc89476 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 15:38:13 -0500 Subject: [PATCH 1979/2056] Bump requests from 2.34.0 to 2.34.1 (#16408) Signed-off-by: dependabot[bot] <support@github.com> 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 6be6a95fe6b..6291b5cd414 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.34.0 +requests==2.34.1 # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 From 910cc38dd704644e3dc2985af3feeabf566a8dc3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 19:25:35 -0400 Subject: [PATCH 1980/2056] [writer] Clean ESP-IDF build artifacts in clean_build (#16410) --- esphome/writer.py | 8 ++++++++ tests/unit_tests/test_writer.py | 28 +++++++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index cf04e4f8d2a..72c2c355dcc 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -490,6 +490,14 @@ def clean_build(clear_pio_cache: bool = True): if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) dependencies_lock.unlink() + # Native ESP-IDF toolchain artifacts: the IDF CMake/ninja build dir + # and the Component Manager's fetched managed components live under + # the project's build path, not under .pioenvs / .piolibdeps. + for name in ("build", "managed_components"): + idf_path = CORE.relative_build_path(name) + if idf_path.is_dir(): + _LOGGER.info("Deleting %s", idf_path) + rmtree(idf_path) if not clear_pio_cache: return diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index e76769e6a83..91b4bd8e87b 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -443,6 +443,14 @@ def test_clean_build( dependencies_lock = tmp_path / "dependencies.lock" dependencies_lock.write_text("lock file") + # Native ESP-IDF toolchain artifacts. + idf_build_dir = tmp_path / "build" + idf_build_dir.mkdir() + (idf_build_dir / "CMakeCache.txt").write_text("cache") + managed_components_dir = tmp_path / "managed_components" + managed_components_dir.mkdir() + (managed_components_dir / "espressif__arduino-esp32").mkdir() + # Create PlatformIO cache directory platformio_cache_dir = tmp_path / ".platformio" / ".cache" platformio_cache_dir.mkdir(parents=True) @@ -454,12 +462,14 @@ def test_clean_build( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify all exist before assert pioenvs_dir.exists() assert piolibdeps_dir.exists() assert dependencies_lock.exists() + assert idf_build_dir.exists() + assert managed_components_dir.exists() assert platformio_cache_dir.exists() # Mock PlatformIO's ProjectConfig cache_dir @@ -482,6 +492,8 @@ def test_clean_build( assert not pioenvs_dir.exists() assert not piolibdeps_dir.exists() assert not dependencies_lock.exists() + assert not idf_build_dir.exists() + assert not managed_components_dir.exists() assert not platformio_cache_dir.exists() # Verify logging @@ -489,6 +501,8 @@ def test_clean_build( assert ".pioenvs" in caplog.text assert ".piolibdeps" in caplog.text assert "dependencies.lock" in caplog.text + assert str(idf_build_dir) in caplog.text + assert str(managed_components_dir) in caplog.text assert "PlatformIO cache" in caplog.text @@ -510,7 +524,7 @@ def test_clean_build_partial_exists( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify only pioenvs exists assert pioenvs_dir.exists() @@ -547,7 +561,7 @@ def test_clean_build_nothing_exists( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify nothing exists assert not pioenvs_dir.exists() @@ -583,7 +597,7 @@ def test_clean_build_platformio_not_available( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir - mock_core.relative_build_path.return_value = dependencies_lock + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify all exist before assert pioenvs_dir.exists() @@ -621,7 +635,7 @@ def test_clean_build_empty_cache_dir( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify pioenvs exists before assert pioenvs_dir.exists() @@ -1349,7 +1363,7 @@ def test_clean_build_handles_readonly_files( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name # Verify file is read-only assert not os.access(readonly_file, os.W_OK) @@ -1413,7 +1427,7 @@ def test_clean_build_reraises_for_other_errors( # Setup mocks mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" - mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + mock_core.relative_build_path.side_effect = lambda name: tmp_path / name try: # Mock os.access in writer module to return True (writable) From 06786da7dda30b243f3f20d263e5d03f3538adc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 16:28:47 -0700 Subject: [PATCH 1981/2056] Bump actions/create-github-app-token from 3.1.1 to 3.2.0 (#16409) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/codeowner-review-request.yml | 2 +- .github/workflows/dashboard-deprecation-comment.yml | 2 +- .github/workflows/external-component-bot.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/sync-device-classes.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 2d000658a2d..6c80d36d20b 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -28,7 +28,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index cd6c1d34c65..7cdbfcf3285 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -35,7 +35,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/dashboard-deprecation-comment.yml b/.github/workflows/dashboard-deprecation-comment.yml index e15c61df5ea..04a2a2151b3 100644 --- a/.github/workflows/dashboard-deprecation-comment.yml +++ b/.github/workflows/dashboard-deprecation-comment.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 2e96bec1dea..104988d7a58 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07c8fe6333..c1086c858c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,7 +221,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -257,7 +257,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -289,7 +289,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index c6c829fbb4e..f69c7530f76 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From a3b6f92433c1ddf27d5ec936bda5e1e1bce31b53 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 19:58:48 -0400 Subject: [PATCH 1982/2056] [espidf] Regenerate bundled CMakeLists; auto-REQUIRE via IDF build properties (#16406) --- esphome/build_gen/espidf.py | 96 ++++++++++--- esphome/components/esp32/__init__.py | 12 ++ esphome/espidf/component.py | 161 ++++------------------ esphome/espidf/toolchain.py | 17 ++- tests/unit_tests/build_gen/test_espidf.py | 159 +++++++++++++++++++++ tests/unit_tests/test_espidf_component.py | 90 +++++++----- 6 files changed, 344 insertions(+), 191 deletions(-) create mode 100644 tests/unit_tests/build_gen/test_espidf.py diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 82c8537befd..5ad2072c5b3 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -10,11 +10,14 @@ from esphome.writer import update_storage_json def get_available_components() -> list[str] | None: - """Get list of available ESP-IDF components from project_description.json. + """Get list of built-in ESP-IDF components from project_description.json. - Returns only internal ESP-IDF components, excluding external/managed - components (from idf_component.yml). + Excludes ``src``, IDF-managed components (``managed_components/``), and + converted PIO libs (``pio_components/``). Returns ``None`` if the build + dir or ``project_description.json`` isn't ready yet. """ + if CORE.build_path is None: + return None project_desc = Path(CORE.build_path) / "build" / "project_description.json" if not project_desc.exists(): return None @@ -31,9 +34,9 @@ def get_available_components() -> list[str] | None: if name == "src": continue - # Exclude managed/external components + # Exclude IDF-managed and converted-PIO components (external). comp_dir = info.get("dir", "") - if "managed_components" in comp_dir: + if "managed_components" in comp_dir or "pio_components" in comp_dir: continue result.append(name) @@ -48,8 +51,12 @@ def has_discovered_components() -> bool: return get_available_components() is not None -def get_project_cmakelists() -> str: - """Generate the top-level CMakeLists.txt for ESP-IDF project.""" +def get_project_cmakelists(minimal: bool = False) -> str: + """Generate the top-level CMakeLists.txt for ESP-IDF project. + + When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS`` + since ``project_description.json`` may be stale on the first write. + """ # Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3) variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") @@ -72,6 +79,37 @@ def get_project_cmakelists() -> str: for flag in project_compile_opts ) + # Per-project list exposed as a CMake variable so converted PIO libs + # can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking + # project-specific names into their cached CMakeLists. + # + # Emit via idf_build_set_property (not plain set()) so the value is + # serialised into build_properties.temp.cmake and visible to IDF's + # early requirements-expansion pass (component_get_requirements.cmake + # runs as a separate CMake script invocation that doesn't load the + # project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_ + # MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty). + from esphome.components.esp32 import get_managed_component_require_names + + managed_components_property = "\n".join( + f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)" + for name in get_managed_component_require_names() + ) + + # Built-in IDF components exposed via our own property (not IDF's + # __COMPONENT_REQUIRES_COMMON, which would append them to every + # component's REQUIRES including real IDF components). Referenced by + # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped + # on minimal writes because project_description.json may be stale. + builtin_components_property = ( + "" + if minimal + else "\n".join( + f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)" + for name in sorted(get_available_components() or []) + ) + ) + return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) @@ -99,6 +137,10 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) {extra_compile_options} +{managed_components_property} + +{builtin_components_property} + project({CORE.name}) # Emit raw JSON size data for ESPHome to read post-build. @@ -113,11 +155,12 @@ add_custom_command( """ -def get_component_cmakelists(minimal: bool = False) -> str: - """Generate the main component CMakeLists.txt.""" - idf_requires = [] if minimal else (get_available_components() or []) - requires_str = " ".join(idf_requires) +def get_component_cmakelists() -> str: + """Generate the main component CMakeLists.txt. + REQUIRES pulls in the discovered built-in IDF components via the + project-level variables set in the top-level CMakeLists. + """ # Extract linker options (-Wl, flags). Compile flags (-D, -W) are # emitted project-wide via idf_build_set_property in # get_project_cmakelists so they reach every component, not just src/. @@ -126,17 +169,30 @@ def get_component_cmakelists(minimal: bool = False) -> str: return f"""\ # Auto-generated by ESPHome -file(GLOB_RECURSE app_sources - "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" - "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" - "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" - "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" -) +# CONFIGURE_DEPENDS asks CMake to re-check the glob each build so test +# runs that reuse the build dir don't compile stale source paths. It's +# invalid in script mode (cmake -P), which is how IDF's +# component_get_requirements.cmake includes us, so skip it there. +if(CMAKE_SCRIPT_MODE_FILE) + file(GLOB_RECURSE app_sources + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" + ) +else() + file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/*.c" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp" + "${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c" + ) +endif() idf_component_register( SRCS ${{app_sources}} INCLUDE_DIRS "." "esphome" - REQUIRES {requires_str} + REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}} ) # Apply C++ standard @@ -162,11 +218,11 @@ def write_project(minimal: bool = False) -> None: # Write top-level CMakeLists.txt write_file_if_changed( CORE.relative_build_path("CMakeLists.txt"), - get_project_cmakelists(), + get_project_cmakelists(minimal=minimal), ) # Write component CMakeLists.txt in src/ write_file_if_changed( CORE.relative_src_path("CMakeLists.txt"), - get_component_cmakelists(minimal=minimal), + get_component_cmakelists(), ) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 221c84c1491..1eb0bb2174f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -588,6 +588,18 @@ def add_idf_component( } +def get_managed_component_require_names() -> list[str]: + """Return sorted IDF require names for components added via + ``add_idf_component`` (``owner/name`` -> ``owner__name``). + + The build_gen layer (``build_gen.espidf.get_project_cmakelists``) + feeds this list into ``ESPHOME_PROJECT_MANAGED_COMPONENTS`` so + converted PIO libraries can REQUIRE them by name at configure time. + """ + components_registry = CORE.data.get(KEY_ESP32, {}).get(KEY_COMPONENTS, {}) + return sorted(name.replace("/", "__") for name in components_registry) + + def exclude_builtin_idf_component(name: str) -> None: """Exclude an ESP-IDF component from the build. diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index af8640949d1..b9202fb6bf4 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -12,7 +12,6 @@ from typing import TypeVar from urllib.parse import urlparse, urlsplit, urlunsplit from esphome import git, yaml_util -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, Library from esphome.espidf.framework import archive_extract_all, download_from_mirrors, rmdir from esphome.helpers import write_file_if_changed @@ -50,28 +49,6 @@ SRC_FILE_EXTENSIONS = [ ESP32_PLATFORM = "espressif32" DOMAIN = "pio_components" -# -# Constants for workarounds -# - -REQUIRES_DETECT_PATTERNS = { - "mbedtls": [re.compile(r'^\s*#\s*include\s*[<"]mbedtls[^">]*[">]', re.MULTILINE)], - "esp_netif": [ - re.compile(r'^\s*#\s*include\s*[<"]esp_netif[^">]*[">]', re.MULTILINE) - ], - "esp_driver_gpio": [ - re.compile(r'^\s*#\s*include\s*[<"]driver/gpio\.h[^">]*[">]', re.MULTILINE) - ], - "esp_timer": [ - re.compile(r'^\s*#\s*include\s*[<"]esp_timer\.h[^">]*[">]', re.MULTILINE) - ], - "esp_wifi": [ - re.compile( - r'^\s*#\s*include\s*[<"]WiFi\.h[^">]*[">]', re.MULTILINE - ) # Arduino WiFi - ], -} - ESPHOME_DATA_KEY = "ESPHOME" ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE" @@ -86,10 +63,7 @@ class URLSource(Source): self.url = url def download(self, dir_suffix: str, force: bool = False) -> Path: - # Partition by framework: generated idf_component.yml content - # depends on CORE.using_arduino, so caches can't be shared. - framework = "arduino" if CORE.using_arduino else "idf" - base_dir = Path(CORE.data_dir) / DOMAIN / framework + base_dir = Path(CORE.data_dir) / DOMAIN h = hashlib.new("sha256") h.update(self.url.encode()) path = base_dir / h.hexdigest()[:8] / dir_suffix @@ -124,12 +98,11 @@ class GitSource(Source): self.ref = ref def download(self, dir_suffix: str, force: bool = False) -> Path: - framework = "arduino" if CORE.using_arduino else "idf" path, _ = git.clone_or_update( url=self.url, ref=self.ref, refresh=git.NEVER_REFRESH if not force else None, - domain=f"{DOMAIN}/{framework}", + domain=DOMAIN, submodules=[], subpath=Path(dir_suffix), ) @@ -282,46 +255,6 @@ def _get_package_from_pio_registry( return owner, name, version["name"], pkgfile["download_url"] -def _patch_component(component: IDFComponent, first_pass: bool): - """ - Apply patches/workarounds to specific components that have known issues. - - This function modifies component data to fix compatibility issues or missing - dependencies for certain libraries. It applies different patches based on - whether it's the first or second pass of processing. - - Args: - component: The IDFComponent object to potentially patch - first_pass: Boolean indicating if this is the first pass of processing - """ - - # Patch only on the second step - if not first_pass and CORE.using_arduino: - # Add the missing dependency to Arduino framework. Source is None so - # the IDF component manager resolves it from the registry instead of - # cloning the 2 GB arduino-esp32 git history. - component.dependencies.append( - IDFComponent( - "espressif/arduino-esp32", - str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]), - None, - ) - ) - - # - # fastled/FastLED - # - - # Patch only on the first step - if ( - first_pass - and component.name == _owner_pkgname_to_name("fastled", "FastLED") - and not (component.path / "idf_component.yml").is_file() - ): - # Force fake idf_component: This project already support ESP-IDF - (component.path / "idf_component.yml").write_text("") - - def _apply_extra_script(component: IDFComponent) -> None: """Run a PIO ``extraScript`` and fold its captured env vars into ``component.data["build"]["flags"]`` so the existing -L/-l/-D @@ -506,43 +439,6 @@ def _convert_library_to_component(library: Library) -> IDFComponent: return IDFComponent(name, version, source) -def _detect_requires(build_src_files: list[str]) -> set[str]: - """ - Detect required components from source files. - - Args: - build_src_files: List of source file paths to analyze - - Returns: - Set of detected required components - """ - detected = set() - - # 1. Process each source file - for file in build_src_files: - path = Path(file) - - if not path.is_file(): - continue - - try: - content = path.read_text(encoding="utf-8", errors="ignore") - except Exception: # pylint: disable=broad-exception-caught - continue - - # 2. Add required component if one of these patterns matches - for require_name, patterns in REQUIRES_DETECT_PATTERNS.items(): - if require_name in detected: - continue # already found - - for pattern in patterns: - if pattern.search(content): - detected.add(require_name) - break - - return detected - - def _split_list_by_condition( items: list[str], match_fn: Callable[[str], str | None] ) -> tuple[list[str], list[str]]: @@ -609,13 +505,14 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: component.path / Path(build_src_dir), build_src_filter ) - # Detect in the files which requirements to add - # By default in platformio, all the components are added: we need to detect them when using ESP-IDF - requires = _detect_requires(build_src_files) - - # Dependencies are required - for dependency in component.dependencies: - requires.add(dependency.get_require_name()) + # Only bake library.json-declared deps here. Project-managed and + # built-in components come in via ${ESPHOME_PROJECT_MANAGED_COMPONENTS} + # / ${ESPHOME_PROJECT_BUILTIN_COMPONENTS} set in the top-level + # CMakeLists, so this file stays project-agnostic when shared from + # the pio_components cache. + requires: set[str] = { + dependency.get_require_name() for dependency in component.dependencies + } # Only keep sources build_src_files = [os.path.relpath(p, component.path) for p in build_src_files] @@ -654,9 +551,19 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: if build_include_dirs: str_include_dirs = " ".join([escape_entry(p) for p in build_include_dirs]) content += f" INCLUDE_DIRS {str_include_dirs}\n" - if requires: - str_requires = " ".join(sorted(requires)) - content += f" REQUIRES {str_requires}\n" + # Project-managed and built-in component lists are set per-project + # via idf_build_set_property in the top-level CMakeLists; expanded + # here at configure time. Keeping them out of the per-lib REQUIRES + # means this CMakeLists is project-agnostic and reusable from the + # pio_components cache across builds. + str_requires = " ".join( + [ + *sorted(requires), + "${ESPHOME_PROJECT_MANAGED_COMPONENTS}", + "${ESPHOME_PROJECT_BUILTIN_COMPONENTS}", + ] + ) + content += f" REQUIRES {str_requires}\n" content += ")\n" # Add public and private build flags @@ -732,13 +639,10 @@ def generate_idf_component_yml(component: IDFComponent) -> str: try: dep["override_path"] = str(dependency.path) except RuntimeError as e: - # No local path; let the IDF component manager resolve. - # GitSource gives an explicit URL; arduino-esp32 is resolved by - # version from the registry. Anything else is a bug. - if isinstance(dependency.source, GitSource): - dep["git"] = dependency.source.url - elif dependency.name != "espressif/arduino-esp32": + # No local path: only a GitSource can substitute its URL. + if not isinstance(dependency.source, GitSource): raise e + dep["git"] = dependency.source.url data["dependencies"][dependency.get_sanitized_name()] = dep @@ -903,12 +807,9 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone cmakelists_txt_path = component.path / "CMakeLists.txt" idf_component_yml_path = component.path / "idf_component.yml" - # Apply patches to the library metadata - _patch_component(component, True) - - if cmakelists_txt_path.is_file() and idf_component_yml_path.is_file(): - # Already an ESP-IDF component - return component + # Bundled CMakeLists.txt / idf_component.yml are ignored -- library + # authors' IDF support is frequently broken (bogus REQUIRES, hard-coded + # arduino-esp32, etc.). We always regenerate. if library_json_path.is_file(): component.data = _parse_library_json(library_json_path) @@ -919,9 +820,6 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone "Invalid PIO library: missing library.json and/or library.properties" ) - # Apply additional patches to the library metadata - _patch_component(component, False) - # Check if the component is usable with ESP-IDF before executing any # third-party Python from the library (``_apply_extra_script`` below). _check_library_data(component.data) @@ -936,7 +834,6 @@ def _generate_idf_component(library: Library, force: bool = False) -> IDFCompone # Handle the dependencies (convert PlatformIO library to ESP-IDF component if needed) _process_dependencies(component) - # Generate files _LOGGER.debug("Generating CMakeLists.txt for %s@%s ...", name, version) write_file_if_changed( cmakelists_txt_path, diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index ecb759ed10c..583f3409964 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -302,10 +302,21 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) + # The post-discovery rewrite leaves CMakeLists newer than + # CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a + # configure that only changes idf_build_set_property values + # (those aren't cache variables), so has_outdated_files() would + # return True on every subsequent build, perpetually retriggering + # the two-pass. Touch CMakeCache.txt now so its mtime stays past + # the rewritten CMakeLists. + cmakecache = CORE.relative_build_path("build/CMakeCache.txt") + if cmakecache.is_file(): + os.utime(cmakecache) if CORE.testing_mode: - # Reconfigure again so cmake is up to date with the full component - # list. This ensures idf.py build won't re-run cmake, which would - # regenerate memory.ld and wipe the DRAM/IRAM patches applied below. + # Reconfigure again so cmake is up to date with the full + # component list before the build's idf.py invocation runs -- + # idf.py build would otherwise re-run cmake and regenerate + # memory.ld, wiping the DRAM/IRAM patches applied below. rc = run_reconfigure() if rc != 0: _LOGGER.error("Reconfigure with discovered components failed") diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py new file mode 100644 index 00000000000..36f04423557 --- /dev/null +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -0,0 +1,159 @@ +"""Tests for esphome.build_gen.espidf module.""" + +from __future__ import annotations + +import json +from pathlib import Path +from unittest.mock import patch + +import pytest + +from esphome.components.esp32 import ( + KEY_COMPONENTS, + KEY_ESP32, + KEY_PATH, + KEY_REF, + KEY_REPO, +) +from esphome.const import KEY_CORE +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def _reset_core(tmp_path: Path) -> None: + """Give each test its own CORE.build_path and a clean esp32 data slot.""" + CORE.build_path = str(tmp_path) + CORE.data.setdefault(KEY_CORE, {}) + CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}} + + +def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None: + """Stub a project_description.json with the given component_name -> dir map.""" + build_dir = tmp_path / "build" + build_dir.mkdir(exist_ok=True) + (build_dir / "project_description.json").write_text( + json.dumps( + { + "build_component_info": { + name: {"dir": dir_} for name, dir_ in components.items() + } + } + ) + ) + + +def test_get_available_components_returns_none_without_build_path() -> None: + """No build_path set yet: must not raise on Path(None).""" + CORE.build_path = None + from esphome.build_gen.espidf import get_available_components + + assert get_available_components() is None + + +def test_get_available_components_returns_none_without_project_description( + tmp_path: Path, +) -> None: + from esphome.build_gen.espidf import get_available_components + + assert get_available_components() is None + + +def test_get_available_components_filters_src_managed_and_pio(tmp_path: Path) -> None: + """Built-ins are returned; src/, managed_components/, pio_components/ skipped.""" + _write_project_description( + tmp_path, + { + "src": f"{tmp_path}/src", + "esp_lcd": "/idf/components/esp_lcd", + "espressif__arduino-esp32": f"{tmp_path}/managed_components/arduino", + "JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC", + "freertos": "/idf/components/freertos", + }, + ) + from esphome.build_gen.espidf import get_available_components + + assert sorted(get_available_components()) == ["esp_lcd", "freertos"] + + +def test_get_project_cmakelists_minimal_omits_builtin_components_property( + tmp_path: Path, +) -> None: + """Minimal write must not emit ESPHOME_PROJECT_BUILTIN_COMPONENTS even + when project_description.json exists (the data may be stale on the + first write before the discovery pass refreshes it).""" + _write_project_description(tmp_path, {"esp_lcd": "/idf/components/esp_lcd"}) + + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + ): + from esphome.build_gen.espidf import get_project_cmakelists + + content = get_project_cmakelists(minimal=True) + + assert "ESPHOME_PROJECT_BUILTIN_COMPONENTS" not in content + + +def test_get_project_cmakelists_full_emits_builtin_components_property( + tmp_path: Path, +) -> None: + """Non-minimal write emits one idf_build_set_property line per built-in, + sorted, and excludes src/managed/pio components.""" + _write_project_description( + tmp_path, + { + "src": f"{tmp_path}/src", + "esp_lcd": "/idf/components/esp_lcd", + "freertos": "/idf/components/freertos", + "espressif__esp-dsp": f"{tmp_path}/managed_components/esp-dsp", + "JPEGDEC": f"{tmp_path}/pio_components/arduino/abc/bitbank2/JPEGDEC", + }, + ) + + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + ): + from esphome.build_gen.espidf import get_project_cmakelists + + content = get_project_cmakelists(minimal=False) + + assert ( + "idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS esp_lcd APPEND)" + in content + ) + assert ( + "idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS freertos APPEND)" + in content + ) + # Excluded by get_available_components filtering. + assert "espressif__esp-dsp APPEND" not in content + assert "JPEGDEC APPEND" not in content + + +def test_get_project_cmakelists_emits_managed_components_property( + tmp_path: Path, +) -> None: + """ESPHOME_PROJECT_MANAGED_COMPONENTS is always emitted (both modes) + from the esp32 add_idf_component registry.""" + CORE.data[KEY_ESP32][KEY_COMPONENTS] = { + "espressif/esp-dsp": {KEY_REPO: None, KEY_REF: "1.7.1", KEY_PATH: None}, + "espressif/arduino-esp32": {KEY_REPO: None, KEY_REF: "3.3.8", KEY_PATH: None}, + } + + with ( + patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"), + patch.object(CORE, "name", "test"), + ): + from esphome.build_gen.espidf import get_project_cmakelists + + for minimal in (True, False): + content = get_project_cmakelists(minimal=minimal) + assert ( + "idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS" + " espressif__arduino-esp32 APPEND)" + ) in content + assert ( + "idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS" + " espressif__esp-dsp APPEND)" + ) in content diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py index 3988c997a76..8977b05d237 100644 --- a/tests/unit_tests/test_espidf_component.py +++ b/tests/unit_tests/test_espidf_component.py @@ -1,5 +1,6 @@ import json import os +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -21,7 +22,6 @@ from esphome.espidf.component import ( _check_library_data, _collect_filtered_files, _convert_library_to_component, - _detect_requires, _parse_library_json, _parse_library_properties, _process_dependencies, @@ -83,19 +83,6 @@ def test_collect_filtered_files_exclude(tmp_path): assert str(f2) not in result -def test_detect_requires(tmp_path): - f = tmp_path / "main.c" - f.write_text('#include "mbedtls/foo.h"') - - result = _detect_requires([str(f)]) - assert "mbedtls" in result - - -def test_detect_requires_ignores_invalid_file(tmp_path): - result = _detect_requires([str(tmp_path / "missing.c")]) - assert result == set() - - def test_split_list_by_condition(): items = ["-Iinclude", "-Llib", "-Wall"] @@ -142,7 +129,7 @@ def test_generate_cmakelists_txt_with_flags(tmp_component, tmp_path): == f"""idf_component_register( SRCS "src{sep}main.c" INCLUDE_DIRS "src" - REQUIRES dep + REQUIRES dep ${{ESPHOME_PROJECT_MANAGED_COMPONENTS}} ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}} ) target_compile_options(${{COMPONENT_LIB}} PUBLIC "-DTEST" @@ -160,6 +147,58 @@ target_link_libraries(${{COMPONENT_LIB}} INTERFACE ) +def test_generate_cmakelists_txt_references_project_managed_components_variable( + tmp_component: IDFComponent, +) -> None: + # The CMakeLists is cached under pio_components/<hash>/ and shared + # across projects, so the project-managed REQUIRES list is exposed via + # a CMake variable expanded at configure time rather than baked here. + src_dir = tmp_component.path / "src" + src_dir.mkdir() + (src_dir / "main.c").write_text("int main() {}") + tmp_component.data = {} + + content = generate_cmakelists_txt(tmp_component) + assert "${ESPHOME_PROJECT_MANAGED_COMPONENTS}" in content + + +def test_generate_idf_component_overwrites_bundled_files( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + esp32_idf_core: None, +) -> None: + # A library that ships its own CMakeLists.txt + idf_component.yml must + # have both replaced by ESPHome's generated content. Library authors' + # bundled IDF metadata is frequently broken (bogus REQUIRES, hard-coded + # frameworks), so we always regenerate from library.json. + from esphome.espidf.component import _generate_idf_component + + (tmp_path / "src").mkdir() + (tmp_path / "src" / "main.cpp").write_text("// dummy\n") + (tmp_path / "library.json").write_text(json.dumps({"name": "tripwire-lib"})) + (tmp_path / "CMakeLists.txt").write_text("# TRIPWIRE_BUNDLED_CMAKELISTS\n") + (tmp_path / "idf_component.yml").write_text("# TRIPWIRE_BUNDLED_MANIFEST\n") + + fake_component = IDFComponent( + "owner/tripwire-lib", "1.0.0", source=URLSource("http://dummy") + ) + fake_component.path = tmp_path + monkeypatch.setattr( + esphome.espidf.component, + "_convert_library_to_component", + lambda _lib: fake_component, + ) + monkeypatch.setattr(fake_component, "download", lambda force=False: None) + + _generate_idf_component(Library("owner/tripwire-lib", "1.0.0", None)) + + cml = (tmp_path / "CMakeLists.txt").read_text() + manifest = (tmp_path / "idf_component.yml").read_text() + assert "TRIPWIRE_BUNDLED_CMAKELISTS" not in cml + assert "TRIPWIRE_BUNDLED_MANIFEST" not in manifest + assert "idf_component_register" in cml + + def test_generate_idf_component_yml_basic(tmp_component): tmp_component.data = {"description": "test", "repository": {"url": "http://aaa"}} result = generate_idf_component_yml(tmp_component) @@ -187,27 +226,6 @@ dependencies: ) -def test_generate_idf_component_yml_arduino_registry_dep(tmp_component): - # Synthetic arduino-esp32 dep with no source / no path: should emit a - # version-only entry so the IDF component manager resolves it from the - # registry instead of via git. - dep = IDFComponent("espressif/arduino-esp32", "3.3.8", source=None) - - tmp_component.dependencies = [dep] - tmp_component.data = {} - - result = generate_idf_component_yml(tmp_component) - - assert ( - result - == """version: 1.0.0 -dependencies: - espressif/arduino-esp32: - version: 3.3.8 -""" - ) - - def test_generate_idf_component_yml_missing_path_reraises(tmp_component): # A dep without a path and without a recognised source should re-raise # the underlying RuntimeError instead of silently producing a bad manifest. From 78b60ac6fa8b173163d74651a719cc1ed3d2e919 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 May 2026 12:33:43 +1200 Subject: [PATCH 1983/2056] Bump version to 2026.6.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 7fce941c9b4..3537516996b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.5.0-dev +PROJECT_NUMBER = 2026.6.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index a256a10e627..fa1ea42bc6d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.5.0-dev" +__version__ = "2026.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 09a926fa13fa82ebe804ce8798a2a3ed4f9870cf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 May 2026 12:33:43 +1200 Subject: [PATCH 1984/2056] Bump version to 2026.5.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 7fce941c9b4..a29a78ea9c0 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.5.0-dev +PROJECT_NUMBER = 2026.5.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index a256a10e627..91bc52708c3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.5.0-dev" +__version__ = "2026.5.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d2107e40c8c0e019de1984009ba9a4c851d88736 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 13 May 2026 21:03:45 -0500 Subject: [PATCH 1985/2056] [ci] Prohibit curly braces in PR titles for MDX safety (#16412) --- .github/workflows/pr-title-check.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index ed0bff9664f..e15d09da823 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -68,14 +68,15 @@ jobs: return; } - // Check for angle brackets not wrapped in backticks. - // Astro docs MDX treats bare < as JSX component opening tags. + // Check for MDX syntax characters not wrapped in backticks. + // Astro docs MDX treats bare `<` as JSX component opening tags and + // bare `{` as JS expressions, so both must be escaped in changelog entries. const stripped = title.replace(/`[^`]*`/g, ''); - if (/[<>]/.test(stripped)) { + if (/[<>{}]/.test(stripped)) { core.setFailed( - 'PR title contains `<` or `>` not wrapped in backticks.\n' + - 'Astro docs MDX interprets bare `<` as JSX components.\n' + - 'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support' + 'PR title contains `<`, `>`, `{`, or `}` not wrapped in backticks.\n' + + 'Astro docs MDX interprets bare `<` as JSX components and bare `{` as JS expressions.\n' + + 'Please wrap these characters with backticks, e.g.: [component] Add `<feature>` support' ); return; } From e593cb6efc2f9a8243718cedbc3f048bda5356db Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 23:19:30 -0400 Subject: [PATCH 1986/2056] [espidf] Stop perpetual reconfigure loop on native ESP-IDF builds (#16415) --- esphome/components/esp32/__init__.py | 7 ++++++- esphome/espidf/toolchain.py | 30 ++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1eb0bb2174f..0c24dbf7b90 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2509,7 +2509,12 @@ def _write_idf_component_yml(): stubs_dir = CORE.relative_build_path("component_stubs") stubs_dir.mkdir(exist_ok=True) - for component_name in components_to_stub: + # Sort so the dict insertion order (and thus the generated + # src/idf_component.yml) is deterministic across runs; otherwise + # the manifest content shuffles every build, write_file_if_changed + # always writes, and ninja keeps triggering CMake re-runs on + # otherwise-cached rebuilds. + for component_name in sorted(components_to_stub): # Create stub directory with minimal CMakeLists.txt stub_path = stubs_dir / _idf_component_stub_name(component_name) stub_path.mkdir(exist_ok=True) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 583f3409964..1245c643e11 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,13 +191,19 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if configuration inputs - are newer than the generated CMake/Ninja build artifacts. + Returns True if required build files are missing or if external + configuration inputs (IDF install, sdkconfig, CMake's own build/config + dir) are newer than CMakeCache.txt. We deliberately don't watch the + top-level/src ``CMakeLists.txt`` here -- those are written by + ``write_project`` via ``write_file_if_changed`` (so an mtime bump + means our content actually changed) and ninja already tracks them as + configure-time deps via ``build.ninja``. Including them in this check + causes a perpetual reconfigure loop: the two-pass write leaves + CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the + cache when only ``idf_build_set_property`` values change, so the + check would trip on every subsequent build. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") - - cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") - cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" @@ -221,8 +227,6 @@ def has_outdated_files(): os.path.getmtime(f) > cmakecache_txt_mtime for f in [ _get_idf_path(), - cmakelists_txt_build_path, - cmakelists_txt_src_path, sdkconfig_internal_path, build_config_path, ] @@ -302,21 +306,13 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) - # The post-discovery rewrite leaves CMakeLists newer than - # CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a - # configure that only changes idf_build_set_property values - # (those aren't cache variables), so has_outdated_files() would - # return True on every subsequent build, perpetually retriggering - # the two-pass. Touch CMakeCache.txt now so its mtime stays past - # the rewritten CMakeLists. - cmakecache = CORE.relative_build_path("build/CMakeCache.txt") - if cmakecache.is_file(): - os.utime(cmakecache) if CORE.testing_mode: # Reconfigure again so cmake is up to date with the full # component list before the build's idf.py invocation runs -- # idf.py build would otherwise re-run cmake and regenerate # memory.ld, wiping the DRAM/IRAM patches applied below. + # Outside testing mode ninja's own configure-time dep on + # CMakeLists.txt handles the re-run as part of the build step. rc = run_reconfigure() if rc != 0: _LOGGER.error("Reconfigure with discovered components failed") From f89a6f4f9c804d118efb7da701962920186dc44b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 00:02:22 -0400 Subject: [PATCH 1987/2056] [espidf] Trim has_outdated_files watch list; embed IDF version in sdkconfig (#16416) --- esphome/components/esp32/__init__.py | 8 ++++- esphome/espidf/toolchain.py | 45 +++++++++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c24dbf7b90..f1125498326 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2464,8 +2464,14 @@ def _write_sdkconfig(): ) want_opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + # Include the resolved framework version as a Kconfig comment so a + # version switch that happens to leave the option set unchanged still + # bumps this file's content -- which is what has_outdated_files() + # uses to decide whether to reconfigure. + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] contents = ( - "\n".join( + f"# ESPHOME_IDF_VERSION={framework_version}\n" + + "\n".join( f"{name}={_format_sdkconfig_val(value)}" for name, value in sorted(want_opts.items()) ) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 1245c643e11..e0bc5bb393a 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,23 +191,38 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if external - configuration inputs (IDF install, sdkconfig, CMake's own build/config - dir) are newer than CMakeCache.txt. We deliberately don't watch the - top-level/src ``CMakeLists.txt`` here -- those are written by - ``write_project`` via ``write_file_if_changed`` (so an mtime bump - means our content actually changed) and ninja already tracks them as - configure-time deps via ``build.ninja``. Including them in this check - causes a perpetual reconfigure loop: the two-pass write leaves - CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the - cache when only ``idf_build_set_property`` values change, so the - check would trip on every subsequent build. + Returns True if required build files are missing or if ESPHome's + resolved build inputs are newer than CMakeCache.txt: + + - ``sdkconfig.<name>.esphomeinternal`` -- the canonical "what state + did ESPHome resolve the YAML to" snapshot. Any change in build + flags, enabled components, framework version, or target ends up + rewriting it (we embed a ``# ESPHOME_IDF_VERSION=`` comment line + for the version case where the option set would otherwise be + identical). + - ``src/idf_component.yml`` -- the project manifest. Managed + component additions/removals (e.g. via ``add_idf_component``) can + happen without any sdkconfig impact, and ``_write_idf_component_yml`` + already deletes ``dependencies.lock`` on a change but that signal + gets lost as soon as the lock is missing. + + We deliberately don't watch: + - The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and + ninja already tracks them as configure-time deps. Including them + causes a perpetual reconfigure loop because CMake doesn't restamp + ``CMakeCache.txt`` when only ``idf_build_set_property`` values + change between configures. + - ``$IDF_PATH`` and CMake's ``build/config/`` -- both have mtime + semantics that fire after the wrong configure (or not at all in + common cases like in-place IDF version replacement). The sdkconfig + and manifest hashes subsume the meaningful signal. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" ) + idf_component_yml_path = CORE.relative_build_path("src/idf_component.yml") dependency_lock_path = CORE.relative_build_path("dependencies.lock") build_ninja_path = CORE.relative_build_path("build/build.ninja") @@ -225,12 +240,8 @@ def has_outdated_files(): cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) return any( os.path.getmtime(f) > cmakecache_txt_mtime - for f in [ - _get_idf_path(), - sdkconfig_internal_path, - build_config_path, - ] - if f and os.path.exists(f) + for f in [sdkconfig_internal_path, idf_component_yml_path] + if f.exists() ) From 348b92910ed508a641467ec246bac01ae4f731da Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 14 May 2026 15:07:38 -0500 Subject: [PATCH 1988/2056] [tinyusb] Reject `tinyusb:` configured without a USB class companion (#16413) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/components/tinyusb/__init__.py | 20 +++++++++++++++++++ .../components/tinyusb/tinyusb_component.cpp | 15 ++++++++++++++ tests/components/tinyusb/common.yaml | 5 +++++ 3 files changed, 40 insertions(+) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index df94ad75346..724f65721b7 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -1,3 +1,4 @@ +from esphome import final_validate as fv import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( @@ -20,6 +21,13 @@ CONF_USB_PRODUCT_STR = "usb_product_str" CONF_USB_SERIAL_STR = "usb_serial_str" CONF_USB_VENDOR_ID = "usb_vendor_id" +# Components that provide a USB device class (CDC, HID, MSC, ...) on top of +# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in +# esp_tinyusb's driver install (descriptors_set fails with no class and no +# user-provided full_speed_config), which trips the task watchdog before +# loop() ever runs. +_USB_CLASS_COMPONENTS = ("usb_cdc_acm",) + tinyusb_ns = cg.esphome_ns.namespace("tinyusb") TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) @@ -41,6 +49,18 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config): + full_config = fv.full_config.get() + if not any(name in full_config for name in _USB_CLASS_COMPONENTS): + raise cv.Invalid( + "The 'tinyusb' component requires at least one USB class component" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index 2ec696c3e43..3cefc0454a3 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -26,6 +26,21 @@ void TinyUSB::setup() { .string_count = SIZE, }; + // Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with + // ESP_ERR_INVALID_ARG when no configuration descriptor is provided and + // no class that has a built-in default (CDC/MSC/NCM) is compiled in. In + // that case the internal task exits without notifying us, and + // tinyusb_driver_install() blocks 5s on the notify-take -- long enough + // to trip the task watchdog. Bail early so the rest of the device can + // still boot. +#if !(CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0) + if (this->tusb_cfg_.descriptor.full_speed_config == nullptr) { + ESP_LOGE(TAG, "No USB class configured"); + this->mark_failed(); + return; + } +#endif + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); if (result != ESP_OK) { ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result)); diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml index cb3f48836ab..674e89dbe87 100644 --- a/tests/components/tinyusb/common.yaml +++ b/tests/components/tinyusb/common.yaml @@ -6,3 +6,8 @@ tinyusb: usb_product_str: ESPHomeTestProduct usb_serial_str: ESPHomeTestSerialNumber usb_vendor_id: 0x2345 + +# tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that. +usb_cdc_acm: + interfaces: + - id: tinyusb_test_cdc From 7436d1c1999cffa4e2121fd223a992808469dab9 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 14 May 2026 15:29:56 -0500 Subject: [PATCH 1989/2056] [tinyusb] Reject `logger.hardware_uart: USB_CDC` (#16417) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/components/tinyusb/__init__.py | 13 ++++++++++++- tests/components/tinyusb/test.esp32-s2-idf.yaml | 5 +++++ tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 724f65721b7..0e02ff87247 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_HARDWARE_UART, CONF_ID CODEOWNERS = ["@kbx81"] CONFLICTS_WITH = ["usb_host"] @@ -55,6 +55,17 @@ def _final_validate(config): raise cv.Invalid( "The 'tinyusb' component requires at least one USB class component" ) + # tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes + # the ROM console through that same peripheral, so the two cannot coexist. + # (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.) + logger_config = full_config.get("logger") + if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC": + raise cv.Invalid( + "'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' " + "because both share the USB OTG peripheral. Set " + "'logger.hardware_uart' to a hardware UART (e.g. UART0), or to " + "USB_SERIAL_JTAG on variants that support it (ESP32-S3, ESP32-P4)" + ) return config diff --git a/tests/components/tinyusb/test.esp32-s2-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml index dade44d145b..09b98ada401 100644 --- a/tests/components/tinyusb/test.esp32-s2-idf.yaml +++ b/tests/components/tinyusb/test.esp32-s2-idf.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 diff --git a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml index f159b38ff6f..ff757315097 100644 --- a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml +++ b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml @@ -1,5 +1,10 @@ <<: !include tinyusb_common.yaml +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 + usb_cdc_acm: interfaces: - id: usb_cdc_acm1 From a8e69a15e40b9fe4809ee4c3555053baf23503d4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:38:09 -0400 Subject: [PATCH 1990/2056] [clang-tidy] Enable readability-container-contains (#16438) --- .clang-tidy | 1 - .clang-tidy.hash | 2 +- esphome/components/lvgl/lvgl_esphome.cpp | 2 +- esphome/components/spi/spi.cpp | 4 ++-- esphome/components/touchscreen/touchscreen.cpp | 2 +- esphome/components/uponor_smatrix/uponor_smatrix.cpp | 2 +- esphome/components/web_server/web_server.cpp | 4 ++-- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ea7370a3b21..6dab84fbd95 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -116,7 +116,6 @@ Checks: >- -portability-template-virtual-member-function, -readability-ambiguous-smartptr-reset-call, -readability-avoid-nested-conditional-operator, - -readability-container-contains, -readability-container-data-pointer, -readability-convert-member-functions-to-static, -readability-else-after-return, diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 77b4f5323fa..52d75d16016 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -593fd53fa09944a59af3f38521e31d87fe10b60326b8d82bb76413c5149b312c +27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 678ed9dbbfb..12bf6d9f37f 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -572,7 +572,7 @@ void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { auto key_idx = lv_buttonmatrix_get_selected_button(self->obj); if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE) return; - if (self->key_map_.count(key_idx) != 0) { + if (self->key_map_.contains(key_idx)) { self->send_key_(self->key_map_[key_idx]); return; } diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 20359135ba0..dfdc9fa6245 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -16,7 +16,7 @@ GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-a SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, GPIOPin *cs_pin, bool release_device, bool write_only) { - if (this->devices_.count(device) != 0) { + if (this->devices_.contains(device)) { ESP_LOGE(TAG, "Device already registered"); return this->devices_[device]; } @@ -27,7 +27,7 @@ SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIB } void SPIComponent::unregister_device(SPIClient *device) { - if (this->devices_.count(device) == 0) { + if (!this->devices_.contains(device)) { esph_log_e(TAG, "Device not registered"); return; } diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 5687213eb53..f4ef66ef3e0 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -78,7 +78,7 @@ void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_r if (this->swap_x_y_) { std::swap(x_raw, y_raw); } - if (this->touches_.count(id) == 0) { + if (!this->touches_.contains(id)) { tp.state = STATE_PRESSED; tp.id = id; } else { diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 3f1feaa9273..0ba19f5cd73 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -154,7 +154,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { } // Log unknown device addresses - if (!found && !this->unknown_devices_.count(device_address)) { + if (!found && !this->unknown_devices_.contains(device_address)) { ESP_LOGI(TAG, "Received packet for unknown device address 0x%08" PRIX32 " ", device_address); this->unknown_devices_.insert(device_address); } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 198267204d9..150f70aa6b0 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2638,9 +2638,9 @@ bool WebServer::isRequestHandlerTrivial() const { return false; } void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { #ifdef USE_WEBSERVER_SORTING - if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { + if (this->sorting_entitys_.contains(entity)) { root[ESPHOME_F("sorting_weight")] = this->sorting_entitys_[entity].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { + if (this->sorting_groups_.contains(this->sorting_entitys_[entity].group_id)) { root[ESPHOME_F("sorting_group")] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; } } From f291dc8d2feeb1a22882e3bba103793d9916e111 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:39:16 -0400 Subject: [PATCH 1991/2056] [esp32] Sweep ESP-IDF toolchain warnings + bump deprecated mark_failed (#16432) --- esphome/components/esp32/__init__.py | 4 +++- esphome/components/hdc2080/hdc2080.cpp | 2 +- esphome/components/heatpumpir/climate.py | 1 - esphome/components/pulse_meter/pulse_meter_sensor.cpp | 4 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/tuya/tuya.cpp | 2 ++ esphome/components/tx20/tx20.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- esphome/components/wiegand/wiegand.cpp | 4 ++-- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f1125498326..e9b0f1fd0a6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1767,9 +1767,11 @@ async def to_code(config): else: cg.add_build_flag("-Wno-error=format") cg.add_build_flag("-Wno-error=maybe-uninitialized") - cg.add_build_flag("-Wno-error=missing-field-initializers") + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_build_flag("-Wno-error=reorder") cg.add_build_flag("-Wno-error=volatile") + # -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates + cg.add_build_flag("-Wno-missing-field-initializers") cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp index dcb207e099b..bf3a4bc79f1 100644 --- a/esphome/components/hdc2080/hdc2080.cpp +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -22,7 +22,7 @@ static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only void HDC2080Component::setup() { const uint8_t data = 0x00; // automatic measurement mode disabled, heater off if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index b7e04374807..aa3a08c2949 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,7 +125,6 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.41") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 3fe1c722ebb..d6959d1a964 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -150,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { edge_state.last_sent_edge_us_ = now; state.last_detected_edge_us_ = now; state.last_rising_edge_us_ = now; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // This ISR is bound to rising edges, so the pin is high @@ -173,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge pulse_state.latched_ = true; state.last_detected_edge_us_ = pulse_state.last_intr_; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // Due to order of operations this includes diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index b8e97b11211..13b9888e052 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -126,7 +126,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { break; } - // Else fall thru ... + [[fallthrough]]; } case STATE_CHECK_SMS: send_cmd_("AT+CMGL=\"ALL\""); diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b29905f9a0d..fd14844908f 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -684,8 +684,10 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType case 4: data.push_back(value >> 24); data.push_back(value >> 16); + [[fallthrough]]; case 2: data.push_back(value >> 8); + [[fallthrough]]; case 1: data.push_back(value >> 0); break; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 353cb31513a..3574bd7c2d5 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -135,7 +135,7 @@ void Tx20Component::decode_and_publish_() { } if (tx20_se == tx20_sb) { tx20_wind_direction = tx20_se; - if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { + if (tx20_wind_direction < 16) { wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; } ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); @@ -164,7 +164,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; return; } const uint32_t delay = now - arg->start_time; @@ -195,7 +195,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index e88c41c0cb3..fb8425f6cdc 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -135,7 +135,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon // Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1). static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT; - USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {} + USBUartChannel(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {} void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 150f70aa6b0..909a27c81c2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -611,7 +611,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c } // Helper to get request detail parameter -static JsonDetail get_request_detail(AsyncWebServerRequest *request) { +[[maybe_unused]] static JsonDetail get_request_detail(AsyncWebServerRequest *request) { return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index e1d3e4bf34f..c32acaf03ea 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -66,7 +66,7 @@ namespace { * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). * - HTTPD_SOCK_ERR_FAIL for other errors. */ -int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { +[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { if (buf == nullptr) { return HTTPD_SOCK_ERR_INVALID; } diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index e5c29f8b115..df64cd48aa7 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { if (arg->d0.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value <<= 1; arg->last_bit_time = millis(); arg->done = false; @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { if (arg->d1.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value = (arg->value << 1) | 1; arg->last_bit_time = millis(); arg->done = false; From f3d77434604b29122d71cad46877d9b991cf1614 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:40:40 -0400 Subject: [PATCH 1992/2056] [tests] Fix -Wformat= mismatches in test YAML lambdas/logger.log (#16435) --- esphome/components/logger/__init__.py | 14 +++++----- esphome/components/lvgl/helpers.py | 28 +++++++++---------- .../components/esp32_ble_tracker/common.yaml | 4 +-- tests/components/ld2412/common.yaml | 2 +- tests/components/lvgl/lvgl-package.yaml | 6 ++-- tests/components/modbus_server/common.yaml | 2 +- tests/components/mqtt/common.yaml | 2 +- tests/components/nextion/common.yaml | 2 +- .../remote_receiver/common-actions.yaml | 2 +- tests/components/script/common.yaml | 2 +- tests/components/udp/common.yaml | 2 +- tests/components/udp/test.host.yaml | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9d7dc8d92c1..c6c440564ac 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -506,13 +506,13 @@ async def _late_logger_init(config: ConfigType) -> None: def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" - ( # start of capture group 1 - % # literal "%" - (?:[-+0 #]{0,5}) # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + (?:[-+0 #]{0,5}) # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index baa618d472b..6f70a1e3bd9 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -9,13 +9,13 @@ CONF_IF_NAN = "if_nan" # noqa f_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - f # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + f # type ) """, flags=re.VERBOSE, @@ -23,13 +23,13 @@ f_regex = re.compile( # noqa c_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """, flags=re.VERBOSE, diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml index 018bbb42b3b..564cf1f6ea6 100644 --- a/tests/components/esp32_ble_tracker/common.yaml +++ b/tests/components/esp32_ble_tracker/common.yaml @@ -29,12 +29,12 @@ esp32_ble_tracker: - service_uuid: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of service data is %i", x.size()); + ESP_LOGD("main", "Length of service data is %zu", x.size()); on_ble_manufacturer_data_advertise: - manufacturer_id: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); + ESP_LOGD("main", "Length of manufacturer data is %zu", x.size()); on_scan_end: - then: - lambda: |- diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml index c5bda688dc9..7a86b6fbdac 100644 --- a/tests/components/ld2412/common.yaml +++ b/tests/components/ld2412/common.yaml @@ -123,7 +123,7 @@ select: - lambda: |- id(uart_bus).flush(); uint32_t new_baud_rate = stoi(x); - ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate); + ESP_LOGD("change_baud_rate", "Changing baud rate from %" PRIu32 " to %" PRIu32, id(uart_bus).get_baud_rate(), new_baud_rate); if (id(uart_bus).get_baud_rate() != new_baud_rate) { id(uart_bus).set_baud_rate(new_baud_rate); #if defined(USE_ESP8266) || defined(USE_ESP32) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 53984bb006d..0f4b9612979 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -660,13 +660,13 @@ lvgl: on_release: logger.log: format: Button released at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_long_press_repeat: logger.log: Button clicked on_pressing: logger.log: format: Button pressing at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_press_lost: logger.log: Button press lost on_single_click: @@ -944,7 +944,7 @@ lvgl: on_release: logger.log: format: Slider released at %d/%d with value %.0f - args: [point.x, point.y, x] + args: ['(int) point.x', '(int) point.y', x] - button: styles: spin_button id: spin_up diff --git a/tests/components/modbus_server/common.yaml b/tests/components/modbus_server/common.yaml index 3522c9248c9..2e4a81a1aa5 100644 --- a/tests/components/modbus_server/common.yaml +++ b/tests/components/modbus_server/common.yaml @@ -21,7 +21,7 @@ modbus_server: read_lambda: |- return 31; write_lambda: |- - printf("address=%d, value=%d", x); + printf("address=%d, value=%" PRId32 "\n", (int) address, x); return true; - id: modbus_server4 modbus_id: mod_bus2 diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 8c58e9b0806..6af2ce3939a 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -64,7 +64,7 @@ mqtt: topic: some/topic payload: Good-bye - lambda: |- - ESP_LOGD("MQTT", "Disconnect reason %d", reason); + ESP_LOGD("MQTT", "Disconnect reason %d", (int) reason); publish_nan_as_none: false binary_sensor: diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 0616b9a41a8..fba6a22b97f 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -299,7 +299,7 @@ display: - lambda: |- // key: StringRef, value: int32_t if (key == "temperature_raw") { - ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value); + ESP_LOGD("nextion.custom", "%s=%" PRId32, key.c_str(), value); } on_custom_binary_sensor: then: diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index 30b99eeb700..26a02d4dabc 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -12,7 +12,7 @@ on_brennenstuhl: then: - logger.log: format: "on_brennenstuhl: %u" - args: ["x.code"] + args: ["(unsigned) x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml index c1dc68513fe..f4818e22962 100644 --- a/tests/components/script/common.yaml +++ b/tests/components/script/common.yaml @@ -49,7 +49,7 @@ script: then: - lambda: |- ESP_LOGD("main", "ints=%d floats=%f bools=%d strings=%s", - ints[0], floats[0], bools[0], strings[0].c_str()); + ints[0], floats[0], (int) bools[0], strings[0].c_str()); - id: my_script_with_params parameters: prefix: string diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 3466e8d2ee0..a40ca455cbc 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -11,7 +11,7 @@ udp: - "10.0.0.255" on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml index 84e78894e51..825d86c19e1 100644 --- a/tests/components/udp/test.host.yaml +++ b/tests/components/udp/test.host.yaml @@ -4,7 +4,7 @@ udp: addresses: ["239.0.60.53"] on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp From 5d9d6e83f789b2453f748d6fa3feca206a25167f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 15:41:32 -0700 Subject: [PATCH 1993/2056] Bump ruff from 0.15.12 to 0.15.13 (#16437) Signed-off-by: dependabot[bot] <support@github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 218bc0083c9..9050132e705 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.12 # also change in .pre-commit-config.yaml when updating +ruff==0.15.13 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From 1bb191aa77b181da81880a88f65643adbdd187d1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:51:36 -0400 Subject: [PATCH 1994/2056] [ci] Skip dashboard-deprecation bot on release/beta-bump PRs (#16427) --- .github/workflows/dashboard-deprecation-comment.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/dashboard-deprecation-comment.yml b/.github/workflows/dashboard-deprecation-comment.yml index 04a2a2151b3..ffd5ec7bd92 100644 --- a/.github/workflows/dashboard-deprecation-comment.yml +++ b/.github/workflows/dashboard-deprecation-comment.yml @@ -12,6 +12,12 @@ jobs: dashboard-deprecation-comment: name: Dashboard deprecation comment runs-on: ubuntu-latest + # Release-bump PRs (bump-X.Y.Z -> beta, beta -> release) inevitably + # roll up everything merged into dev since the last cut, which can + # include dashboard changes that have already been reviewed once. + # The bot's purpose is to warn new contributors before they invest + # time -- that only applies to PRs entering dev. + if: github.event.pull_request.base.ref == 'dev' steps: - name: Generate a token id: generate-token From 1d86d856d1153fef03f5b3185ae795a943511faa Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:51:59 -0400 Subject: [PATCH 1995/2056] [docker] Install libusb-1.0 so ESP-IDF tools can validate openocd (#16424) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docker/Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 540d28be7f1..25de9472b63 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,12 +13,16 @@ RUN git config --system --add safe.directory "*" \ && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation -# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) +# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager). +# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can +# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without +# it idf_tools.py rejects the openocd install with exit 127 and aborts +# the whole framework setup. RUN if command -v apk > /dev/null; then \ - apk add --no-cache build-base; \ + apk add --no-cache build-base libusb; \ else \ apt-get update \ - && apt-get install -y --no-install-recommends build-essential \ + && apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \ && rm -rf /var/lib/apt/lists/*; \ fi From 313d97498391084a085e9f99451928ce310f5f27 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:53:42 -0400 Subject: [PATCH 1996/2056] [multiple] Fix -Wformat= mismatches in component .cpp sources (#16433) --- esphome/components/bme680_bsec/bme680_bsec.cpp | 4 ++-- .../esp32_hosted/update/esp32_hosted_update.cpp | 6 +++--- esphome/components/esphome/ota/ota_esphome.cpp | 14 +++++++------- esphome/components/fastled_base/fastled_light.cpp | 2 +- esphome/components/inkplate/inkplate.cpp | 2 +- esphome/components/midea/air_conditioner.cpp | 4 ++-- esphome/components/ota/ota_partitions_esp_idf.cpp | 4 ++-- .../components/remote_receiver/remote_receiver.cpp | 8 ++++---- esphome/components/sendspin/sendspin_hub.cpp | 4 ++-- .../total_daily_energy/total_daily_energy.cpp | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index b7f8c0da77b..823f32c446a 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() { " IAQ Mode: %s\n" " Supply Voltage: %sV\n" " Sample Rate: %s\n" - " State Save Interval: %ims", + " State Save Interval: %" PRIu32 "ms", this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_); @@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe } void BME680BSECComponent::delay_ms(uint32_t period) { - ESP_LOGV(TAG, "Delaying for %ums", period); + ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period); delay(period); } diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index af35d328881..7f3ba778951 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -92,7 +92,7 @@ void Esp32HostedUpdate::setup() { if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { // 16 bytes: "255.255.255" (11 chars) + null + safety margin char buf[16]; - snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1); this->update_info_.current_version = buf; } else { this->update_info_.current_version = "unknown"; @@ -120,8 +120,8 @@ void Esp32HostedUpdate::setup() { this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { - ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, - ESP_APP_DESC_MAGIC_WORD); + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", + app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1857ed6642..fb0cc2e56d3 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -108,8 +108,8 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Partition access allowed\n" " Running app:\n" - " Partition address: 0x%X\n" - " Used size: %zu bytes (0x%X)", + " Partition address: 0x%" PRIX32 "\n" + " Used size: %zu bytes (0x%zX)", this->running_app_offset_, this->running_app_size_, this->running_app_size_); #ifdef USE_ESP32 @@ -378,7 +378,7 @@ void ESPHomeOTAComponent::handle_data_() { } ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) | (static_cast<size_t>(buf[2]) << 8) | buf[3]; - ESP_LOGV(TAG, "Size is %u bytes", ota_size); + ESP_LOGV(TAG, "Size is %zu bytes", ota_size); #ifndef USE_OTA_PARTITIONS if (ota_type != ota::OTA_TYPE_UPDATE_APP) { @@ -749,7 +749,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { this->auth_buf_[0] = this->auth_type_; hasher.get_hex(buf); - ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf); + ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf); } // Try to write auth_type + nonce @@ -809,13 +809,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() { hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher.calculate(); - ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce); + ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator) hasher.get_hex(computed_hash); - ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash); + ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash); #endif - ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response); + ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response); // Compare response bool matches = hasher.equals_hex(response); diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 8d1dd49dad3..0fa69a23b4d 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -19,7 +19,7 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, "FastLED light:\n" " Num LEDs: %u\n" - " Max refresh rate: %u", + " Max refresh rate: %" PRIu32, this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 39110ca83b9..2e837fb614e 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -319,7 +319,7 @@ void Inkplate::fill(Color color) { memset(this->partial_buffer_, fill, this->get_buffer_length_()); } - ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display() { diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 594f7fa661b..7603dd52542 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -130,8 +130,8 @@ ClimateTraits AirConditioner::traits() { void AirConditioner::dump_config() { ESP_LOGCONFIG(Constants::TAG, "MideaDongle:\n" - " [x] Period: %dms\n" - " [x] Response timeout: %dms\n" + " [x] Period: %" PRIu32 "ms\n" + " [x] Response timeout: %" PRIu32 "ms\n" " [x] Request attempts: %d", this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts()); #ifdef USE_REMOTE_TRANSMITTER diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index f91e88bde0a..a7fc7093135 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -210,7 +210,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, + ESP_LOGD(TAG, "Copying running app from 0x%" PRIX32 " to 0x%" PRIX32 " (size: 0x%zX)", running_app_part->address, plan.copy_dest_part->address, running_app_size); err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size); if (err != ESP_OK) { @@ -261,7 +261,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Selected app partition not found after partition table update"); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address); + ESP_LOGD(TAG, "Setting next boot partition to 0x%" PRIX32, new_boot_partition->address); err = esp_ota_set_boot_partition(new_boot_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err); diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index d59ee636955..222dae8f7f8 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -78,10 +78,10 @@ void RemoteReceiverComponent::setup() { void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:\n" - " Buffer Size: %u\n" - " Tolerance: %u%s\n" - " Filter out pulses shorter than: %u us\n" - " Signal is done after %u us of no changes", + " Buffer Size: %" PRIu32 "\n" + " Tolerance: %" PRIu32 "%s\n" + " Filter out pulses shorter than: %" PRIu32 " us\n" + " Signal is done after %" PRIu32 " us of no changes", this->buffer_size_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 04426b8b1da..57709306cd0 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -153,7 +153,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { LastPlayedServerPref pref{.server_id_hash = hash}; bool ok = this->last_played_server_pref_.save(&pref); if (ok) { - ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash); + ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash); } else { ESP_LOGW(TAG, "Failed to persist last played server hash"); } @@ -164,7 +164,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { std::optional<uint32_t> SendspinHub::load_last_server_hash() { LastPlayedServerPref pref{}; if (this->last_played_server_pref_.load(&pref)) { - ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash); + ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash); return pref.server_id_hash; } return std::nullopt; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 161c712cc16..7c9dbb604f7 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -72,7 +72,7 @@ void TotalDailyEnergy::schedule_midnight_reset_() { timeout_seconds = seconds_until_midnight + 1; } - ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + ESP_LOGD(TAG, "Scheduling midnight check in %" PRIu32 "s", timeout_seconds); this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, [this]() { this->schedule_midnight_reset_(); }); } From a92b607754c3babd607f3e689bb017955485072f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:54:13 -0400 Subject: [PATCH 1997/2056] [ci] Add ci-run-all label to force full CI matrix (#16421) --- .github/workflows/ci.yml | 39 ++++++-- script/determine-jobs.py | 79 ++++++++++++---- tests/script/test_determine_jobs.py | 139 ++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 819dac926e0..abd2d1b3a54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,6 +249,7 @@ jobs: integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} + clang-tidy-full-scan: ${{ steps.determine.outputs.clang-tidy-full-scan }} python-linters: ${{ steps.determine.outputs.python-linters }} import-time: ${{ steps.determine.outputs.import-time }} device-builder: ${{ steps.determine.outputs.device-builder }} @@ -287,7 +288,12 @@ jobs: GH_TOKEN: ${{ github.token }} run: | . venv/bin/activate - output=$(python script/determine-jobs.py) + EXTRA_ARGS="" + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-run-all') }}" == "true" ]]; then + EXTRA_ARGS="--force-all" + echo "::notice::ci-run-all label detected -- forcing every CI job to run" + fi + output=$(python script/determine-jobs.py $EXTRA_ARGS) echo "Test determination output:" echo "$output" | jq @@ -296,6 +302,7 @@ jobs: echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT + echo "clang-tidy-full-scan=$(echo "$output" | jq -r '.clang_tidy_full_scan')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT @@ -500,7 +507,13 @@ jobs: id: check_full_scan run: | . venv/bin/activate - if python script/clang_tidy_hash.py --check; then + # determine-jobs.clang-tidy-full-scan is true when core C++ changed + # OR the ci-run-all label forced --force-all. Independent of the + # hash check, both must produce a full scan in the job itself. + if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=determine_jobs" >> $GITHUB_OUTPUT + elif python script/clang_tidy_hash.py --check; then echo "full_scan=true" >> $GITHUB_OUTPUT echo "reason=hash_changed" >> $GITHUB_OUTPUT else @@ -512,7 +525,7 @@ jobs: run: | . venv/bin/activate if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then - echo "Running FULL clang-tidy scan (hash changed)" + echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})" script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} else echo "Running clang-tidy on changed files only" @@ -572,7 +585,13 @@ jobs: id: check_full_scan run: | . venv/bin/activate - if python script/clang_tidy_hash.py --check; then + # determine-jobs.clang-tidy-full-scan is true when core C++ changed + # OR the ci-run-all label forced --force-all. Independent of the + # hash check, both must produce a full scan in the job itself. + if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=determine_jobs" >> $GITHUB_OUTPUT + elif python script/clang_tidy_hash.py --check; then echo "full_scan=true" >> $GITHUB_OUTPUT echo "reason=hash_changed" >> $GITHUB_OUTPUT else @@ -584,7 +603,7 @@ jobs: run: | . venv/bin/activate if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then - echo "Running FULL clang-tidy scan (hash changed)" + echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})" script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy else echo "Running clang-tidy on changed files only" @@ -661,7 +680,13 @@ jobs: id: check_full_scan run: | . venv/bin/activate - if python script/clang_tidy_hash.py --check; then + # determine-jobs.clang-tidy-full-scan is true when core C++ changed + # OR the ci-run-all label forced --force-all. Independent of the + # hash check, both must produce a full scan in the job itself. + if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=determine_jobs" >> $GITHUB_OUTPUT + elif python script/clang_tidy_hash.py --check; then echo "full_scan=true" >> $GITHUB_OUTPUT echo "reason=hash_changed" >> $GITHUB_OUTPUT else @@ -673,7 +698,7 @@ jobs: run: | . venv/bin/activate if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then - echo "Running FULL clang-tidy scan (hash changed)" + echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})" script/clang-tidy --all-headers --fix ${{ matrix.options }} else echo "Running clang-tidy on changed files only" diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 0a55b2a8481..3259fb5836d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -1062,22 +1062,42 @@ def main() -> None: parser.add_argument( "-b", "--branch", help="Branch to compare changed files against" ) + parser.add_argument( + "--force-all", + action="store_true", + help=( + "Force every job to run regardless of what changed. Used by CI " + "when the ci-run-all label is applied to a PR (escape hatch for " + "changes that need full-matrix validation but don't touch enough " + "files to trigger it organically)." + ), + ) args = parser.parse_args() # Determine what should run - integration_run_all, integration_test_files = determine_integration_tests( - args.branch - ) + if args.force_all: + integration_run_all, integration_test_files = True, [] + run_clang_tidy = True + run_clang_format = True + run_python_linters = True + run_import_time = True + run_device_builder = True + native_idf_components = sorted(NATIVE_IDF_TEST_COMPONENTS) + run_native_idf = True + else: + integration_run_all, integration_test_files = determine_integration_tests( + args.branch + ) + run_clang_tidy = should_run_clang_tidy(args.branch) + run_clang_format = should_run_clang_format(args.branch) + run_python_linters = should_run_python_linters(args.branch) + run_import_time = should_run_import_time(args.branch) + run_device_builder = should_run_device_builder(args.branch) + native_idf_components = native_idf_components_to_test(args.branch) + run_native_idf = bool(native_idf_components) run_integration, integration_test_buckets = _compute_integration_test_buckets( integration_run_all, integration_test_files ) - run_clang_tidy = should_run_clang_tidy(args.branch) - run_clang_format = should_run_clang_format(args.branch) - run_python_linters = should_run_python_linters(args.branch) - run_import_time = should_run_import_time(args.branch) - run_device_builder = should_run_device_builder(args.branch) - native_idf_components = native_idf_components_to_test(args.branch) - run_native_idf = bool(native_idf_components) changed_cpp_file_count = count_changed_cpp_files(args.branch) # Get changed components @@ -1106,11 +1126,27 @@ def main() -> None: changed_components = changed_components_result is_core_change = False - # Filter to only components that have test files - # Components without tests shouldn't generate CI test jobs - changed_components_with_tests = [ - component for component in changed_components if _component_has_tests(component) - ] + if args.force_all: + # Force every component with tests into the CI matrix. Each disk entry + # under tests/components/<name> is treated as a component; filtered + # below by _component_has_tests so components without YAML tests are + # still excluded. + tests_root = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + all_components = sorted(d.name for d in tests_root.iterdir() if d.is_dir()) + changed_components_with_tests = [ + component for component in all_components if _component_has_tests(component) + ] + # Treat as a core change so downstream logic (clang-tidy full scan, + # dep expansion) sees the same world as when esphome/core/ changes. + is_core_change = True + else: + # Filter to only components that have test files + # Components without tests shouldn't generate CI test jobs + changed_components_with_tests = [ + component + for component in changed_components + if _component_has_tests(component) + ] # Get directly changed components with tests (for isolated testing) # These will be tested WITHOUT --testing-mode in CI to enable full validation @@ -1143,8 +1179,10 @@ def main() -> None: memory_impact = detect_memory_impact_config(args.branch) # Determine clang-tidy mode based on actual files that will be checked + is_full_scan = False if run_clang_tidy: # Full scan needed if: hash changed OR core files changed + # (is_core_change is forced True under --force-all) is_full_scan = _is_clang_tidy_full_scan() or is_core_change if is_full_scan: @@ -1177,10 +1215,12 @@ def main() -> None: # Build output # Determine which C++ unit tests to run - cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) - - # Determine if benchmarks should run - run_benchmarks = should_run_benchmarks(args.branch) + if args.force_all: + cpp_run_all, cpp_components = True, [] + run_benchmarks = True + else: + cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) + run_benchmarks = should_run_benchmarks(args.branch) # Split components into batches for CI testing # This intelligently groups components with similar bus configurations @@ -1219,6 +1259,7 @@ def main() -> None: "integration_test_buckets": integration_test_buckets, "clang_tidy": run_clang_tidy, "clang_tidy_mode": clang_tidy_mode, + "clang_tidy_full_scan": is_full_scan, "clang_format": run_clang_format, "python_linters": run_python_linters, "import_time": run_import_time, diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 9139c6e0956..3fd5eada940 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -2602,3 +2602,142 @@ def test_main_validate_only_excludes_transitive_components( # Only foo (directly changed, validate-only). bar is a transitive dep # and still needs compile despite no source change of its own. assert output["validate_only_components"] == ["foo"] + + +def test_main_force_all_overrides_detection( + mock_determine_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, + mock_native_idf_components_to_test: Mock, + mock_determine_cpp_unit_tests: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """--force-all bypasses per-feature detection and runs every job. + + Detection mocks all return False/empty (which would normally skip + everything) -- the flag must override them. Also verifies clang-tidy + goes to ``split`` (full scan) and the component-test matrix is + populated from disk rather than from changed-files. + """ + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_determine_integration_tests.return_value = (False, []) + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False + mock_native_idf_components_to_test.return_value = [] + mock_determine_cpp_unit_tests.return_value = (False, []) + mock_changed_files.return_value = [] + + with ( + patch("sys.argv", ["determine-jobs.py", "--force-all"]), + patch.object(determine_jobs, "get_changed_components", return_value=[]), + patch.object( + determine_jobs, "filter_component_and_test_files", return_value=False + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=[] + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object(determine_jobs, "should_run_benchmarks", return_value=False), + ): + determine_jobs.main() + + output = json.loads(capsys.readouterr().out) + + assert output["integration_tests"] is True + assert output["clang_tidy"] is True + assert output["clang_tidy_mode"] == "split" + assert output["clang_tidy_full_scan"] is True + assert output["clang_format"] is True + assert output["python_linters"] is True + assert output["import_time"] is True + assert output["device_builder"] is True + assert output["native_idf"] is True + # native_idf_components is a CSV of NATIVE_IDF_TEST_COMPONENTS + assert "esp32" in output["native_idf_components"].split(",") + assert output["cpp_unit_tests_run_all"] is True + assert output["cpp_unit_tests_components"] == [] + assert output["benchmarks"] is True + # Detection helpers must not be consulted when --force-all is set + mock_determine_integration_tests.assert_not_called() + mock_should_run_clang_tidy.assert_not_called() + mock_should_run_clang_format.assert_not_called() + mock_should_run_python_linters.assert_not_called() + mock_should_run_import_time.assert_not_called() + mock_should_run_device_builder.assert_not_called() + mock_native_idf_components_to_test.assert_not_called() + mock_determine_cpp_unit_tests.assert_not_called() + # Component matrix is populated from disk (tests/components/ in the repo) + assert output["component_test_count"] > 0 + assert len(output["component_test_batches"]) > 0 + + +def test_main_force_all_off_uses_detection( + mock_determine_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_should_run_import_time: Mock, + mock_should_run_device_builder: Mock, + mock_native_idf_components_to_test: Mock, + mock_determine_cpp_unit_tests: Mock, + mock_changed_files: Mock, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Without --force-all, detection helpers drive the decision (regression guard).""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + mock_determine_integration_tests.return_value = (False, []) + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_should_run_import_time.return_value = False + mock_should_run_device_builder.return_value = False + mock_native_idf_components_to_test.return_value = [] + mock_determine_cpp_unit_tests.return_value = (False, []) + mock_changed_files.return_value = [] + + with ( + patch("sys.argv", ["determine-jobs.py"]), + patch.object(determine_jobs, "get_changed_components", return_value=[]), + patch.object( + determine_jobs, "filter_component_and_test_files", return_value=False + ), + patch.object( + determine_jobs, "get_components_with_dependencies", return_value=[] + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, "create_intelligent_batches", return_value=([], {}) + ), + patch.object(determine_jobs, "should_run_benchmarks", return_value=False), + ): + determine_jobs.main() + + output = json.loads(capsys.readouterr().out) + + assert output["integration_tests"] is False + assert output["clang_tidy"] is False + assert output["clang_format"] is False + assert output["python_linters"] is False + assert output["native_idf"] is False + assert output["component_test_count"] == 0 + mock_determine_integration_tests.assert_called_once() + mock_should_run_clang_tidy.assert_called_once() From 56983f414fb9316062a09bbd57ae1584297cc595 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:41:36 -0400 Subject: [PATCH 1998/2056] [espidf] Gate esp_idf_size --ng on IDF version (#16441) --- esphome/build_gen/espidf.py | 10 ++++++++-- tests/unit_tests/build_gen/test_espidf.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 5ad2072c5b3..96f84ebbd14 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -3,7 +3,8 @@ import json from pathlib import Path -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, idf_version +import esphome.config_validation as cv from esphome.core import CORE from esphome.helpers import mkdir_p, write_file_if_changed from esphome.writer import update_storage_json @@ -61,6 +62,11 @@ def get_project_cmakelists(minimal: bool = False) -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and + # removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get + # --format=raw because the legacy mode doesn't support it. + size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else "" + # Project-wide compile options: -D defines and -W warning flags (skip # -Wl, linker flags — those go on the src component via # target_link_options below). Emitted via idf_build_set_property so the @@ -146,7 +152,7 @@ project({CORE.name}) # Emit raw JSON size data for ESPHome to read post-build. add_custom_command( TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD - COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw + COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json ${{CMAKE_PROJECT_NAME}}.map WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 36f04423557..540dd067318 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -11,10 +11,12 @@ import pytest from esphome.components.esp32 import ( KEY_COMPONENTS, KEY_ESP32, + KEY_IDF_VERSION, KEY_PATH, KEY_REF, KEY_REPO, ) +import esphome.config_validation as cv from esphome.const import KEY_CORE from esphome.core import CORE @@ -24,7 +26,10 @@ def _reset_core(tmp_path: Path) -> None: """Give each test its own CORE.build_path and a clean esp32 data slot.""" CORE.build_path = str(tmp_path) CORE.data.setdefault(KEY_CORE, {}) - CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}} + CORE.data[KEY_ESP32] = { + KEY_COMPONENTS: {}, + KEY_IDF_VERSION: cv.Version(5, 5, 4), + } def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None: From d046dd7276747918784bb46a6f4ccb257f5e738a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:51:14 -0400 Subject: [PATCH 1999/2056] [esp32_hosted] Bump esp_hosted to 2.12.7 (#16440) --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index eca7c24b102..71d1fd3ac1d 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -249,7 +249,7 @@ async def to_code(config): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1") esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 814a4031c12..49c4cdbb2e2 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -36,7 +36,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.6 + version: 2.12.7 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From c5c627d534df0f96043b40f1bf1267de6bc33b5a Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 14 May 2026 23:31:11 -0400 Subject: [PATCH 2000/2056] [audio] Bump microMP3 to v0.2.1 (#16429) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 44371e87ab2..13b379ba3ad 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -395,7 +395,7 @@ async def to_code(config): ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") - add_idf_component(name="esphome/micro-mp3", ref="0.2.0") + add_idf_component(name="esphome/micro-mp3", ref="0.2.1") _emit_memory_pair( data.mp3.buffer_memory, "CONFIG_MP3_DECODER_PREFER_PSRAM", diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 49c4cdbb2e2..35c55cbb4de 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: esphome/micro-flac: version: 0.2.0 esphome/micro-mp3: - version: 0.2.0 + version: 0.2.1 esphome/micro-opus: version: 0.4.1 esphome/micro-wav: From d663d80fdefdba965a08e8436ab00cc1e332b734 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 14 May 2026 23:33:36 -0400 Subject: [PATCH 2001/2056] [sound_level] Use RingBufferAudioSource (#16436) --- .../components/sound_level/sound_level.cpp | 58 ++++++++++--------- esphome/components/sound_level/sound_level.h | 9 +-- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index fb8bfd30856..a93e3963674 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -11,7 +11,7 @@ namespace esphome::sound_level { static const char *const TAG = "sound_level"; -static const uint32_t AUDIO_BUFFER_DURATION_MS = 30; +static const uint32_t MAX_FILL_DURATION_MS = 30; static const uint32_t RING_BUFFER_DURATION_MS = 120; // Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2 @@ -30,8 +30,7 @@ void SoundLevelComponent::dump_config() { void SoundLevelComponent::setup() { this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); - if (this->ring_buffer_.use_count() == 2) { - // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write + if (temp_ring_buffer != nullptr) { temp_ring_buffer->write((void *) data.data(), data.size()); } }); @@ -81,10 +80,11 @@ void SoundLevelComponent::loop() { return; } - // Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop - this->audio_buffer_->transfer_data_from_source(0); + // Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop. + // pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact). + this->audio_source_->fill(0, false); - if (this->audio_buffer_->available() == 0) { + if (this->audio_source_->available() == 0) { // No new audio available for processing return; } @@ -92,11 +92,11 @@ void SoundLevelComponent::loop() { const uint32_t samples_in_window = this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_); const uint32_t samples_available_to_process = - this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available()); + this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_source_->available()); const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process); // MicrophoneSource always provides int16 samples due to Python codegen settings - const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_buffer_->get_buffer_start()); + const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_source_->data()); // Process all the new audio samples for (uint32_t i = 0; i < samples_to_process; ++i) { @@ -115,9 +115,8 @@ void SoundLevelComponent::loop() { ++this->sample_count_; } - // Remove the processed samples from ``audio_buffer_`` - this->audio_buffer_->decrease_buffer_length( - this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); + // Remove the processed samples from ``audio_source_`` + this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); if (this->sample_count_ == samples_in_window) { // Processed enough samples for the measurement window, compute and publish the sensor values @@ -158,36 +157,39 @@ void SoundLevelComponent::stop() { } bool SoundLevelComponent::start_() { - if (this->audio_buffer_ != nullptr) { + if (this->audio_source_ != nullptr) { return true; } - // Allocate a transfer buffer - this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); - if (this->audio_buffer_ == nullptr) { - this->status_momentary_error("transfer_buffer", 15000); + const auto &stream_info = this->microphone_source_->get_audio_stream_info(); + const size_t bytes_per_frame = stream_info.frames_to_bytes(1); + + // Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple + // of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices. + this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation + const size_t ring_buffer_size = + (stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame; + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer == nullptr) { + this->status_momentary_error("ring_buffer", 15000); return false; } - // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it - this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation - std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); - if (temp_ring_buffer.use_count() == 0) { - this->status_momentary_error("ring_buffer", 15000); - this->stop_(); + // Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads + // ensure multi-channel frames are never split across the ring buffer's wrap boundary. + this->audio_source_ = audio::RingBufferAudioSource::create( + temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame)); + if (this->audio_source_ == nullptr) { + this->status_momentary_error("audio_source", 15000); return false; - } else { - this->ring_buffer_ = temp_ring_buffer; - this->audio_buffer_->set_source(temp_ring_buffer); } + this->ring_buffer_ = temp_ring_buffer; this->status_clear_error(); return true; } -void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); } +void SoundLevelComponent::stop_() { this->audio_source_.reset(); } } // namespace esphome::sound_level diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 4f0081a5104..aabea62ca42 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -36,11 +36,12 @@ class SoundLevelComponent : public Component { void stop(); protected: - /// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which - /// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful. + /// @brief Internal start command that, if necessary, allocates a ring buffer and a zero-copy + /// ``RingBufferAudioSource`` that reads directly from it. ``ring_buffer_`` weakly references the + /// ring buffer owned by ``audio_source_``. Returns true if allocations were successful. bool start_(); - /// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer) + /// @brief Internal stop command that deallocates ``audio_source_`` (which releases its ring buffer) void stop_(); microphone::MicrophoneSource *microphone_source_{nullptr}; @@ -48,7 +49,7 @@ class SoundLevelComponent : public Component { sensor::Sensor *peak_sensor_{nullptr}; sensor::Sensor *rms_sensor_{nullptr}; - std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer_; + std::unique_ptr<audio::RingBufferAudioSource> audio_source_; std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; int32_t squared_peak_{0}; From d832ce51cd207f3cea82b04f2a1b7d60c0b68c6e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 15 May 2026 05:42:10 +0200 Subject: [PATCH 2002/2056] [nextion] Replace `connect_info` vector with fixed-size field parser, always log device info (#16059) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/nextion/display.py | 21 ++++++-- esphome/components/nextion/nextion.cpp | 75 +++++++++++++++----------- esphome/components/nextion/nextion.h | 15 +++--- esphome/core/defines.h | 1 - tests/components/nextion/common.yaml | 1 - 5 files changed, 70 insertions(+), 43 deletions(-) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index dc3d5c6d091..89e9b93520f 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart @@ -39,6 +41,8 @@ from .base_component import ( CONF_WAKE_UP_PAGE, ) +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] @@ -55,6 +59,15 @@ NextionSetBrightnessAction = nextion_ns.class_( ) +def _deprecated_dump_device_info(value): + _LOGGER.warning( + "'dump_device_info' is deprecated and will be removed in ESPHome 2026.11.0. " + "Device info is now always logged at connection time. " + "Please remove this option from your configuration." + ) + return value + + def _validate_tft_upload(config): has_tft_url = CONF_TFT_URL in config for conf_key in ( @@ -81,7 +94,10 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=TimePeriod(milliseconds=255)), ), - cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean, + # Deprecated — device info is now always logged. Remove before 2026.11.0. + cv.Optional(CONF_DUMP_DEVICE_INFO): cv.All( + cv.boolean, _deprecated_dump_device_info + ), cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All( cv.positive_time_period_milliseconds, @@ -277,9 +293,6 @@ async def to_code(config): cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) - if config[CONF_DUMP_DEVICE_INFO]: - cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO") - if config[CONF_EXIT_REPARSE_ON_START]: cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index b644cad5077..4ebc7175523 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -117,30 +117,41 @@ bool Nextion::check_connect_() { ESP_LOGN(TAG, "connect: %s", response.c_str()); - size_t start; + // Parse comok response fields directly + // Format: comok <touch>,<reserved>,<model>,<fw>,<mcu_code>,<serial>,<flash> + size_t field_count = 0; + size_t start = 0; size_t end = 0; - std::vector<std::string> connect_info; + auto copy_field = [&](char *dst, size_t cap) { + size_t len = (end == std::string::npos ? response.size() : end) - start; + size_t n = len < cap ? len : cap; + std::memcpy(dst, response.data() + start, n); + dst[n] = '\0'; + }; while ((start = response.find_first_not_of(',', end)) != std::string::npos) { end = response.find(',', start); - connect_info.push_back(response.substr(start, end - start)); + switch (field_count) { + case 2: + copy_field(this->device_model_, this->NEXTION_MODEL_MAX); + break; + case 3: + copy_field(this->firmware_version_, this->NEXTION_FW_MAX); + break; + case 5: + copy_field(this->serial_number_, this->NEXTION_SERIAL_MAX); + break; + case 6: + this->flash_size_ = static_cast<uint32_t>(std::strtoul(response.data() + start, nullptr, 10)); + break; + default: + break; + } + ++field_count; } - this->is_detected_ = (connect_info.size() == 7); + this->is_detected_ = (field_count == 7); if (this->is_detected_) { - ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); -#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - this->device_model_ = connect_info[2]; - this->firmware_version_ = connect_info[3]; - this->serial_number_ = connect_info[5]; - this->flash_size_ = connect_info[6]; -#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - ESP_LOGI(TAG, - " Device Model: %s\n" - " FW Version: %s\n" - " Serial Number: %s\n" - " Flash Size: %s\n", - connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str()); -#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + ESP_LOGN(TAG, "Connect info: %zu fields", field_count); } else { ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str()); } @@ -178,24 +189,26 @@ void Nextion::dump_config() { #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE ESP_LOGCONFIG(TAG, " Skip handshake: YES"); #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE -#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + if (this->is_setup()) { + ESP_LOGCONFIG(TAG, + " Device Model: %s\n" + " FW Version: %s\n" + " Serial Number: %s\n" + " Flash Size: %" PRIu32 " bytes", + this->device_model_, this->firmware_version_, this->serial_number_, this->flash_size_); + } else { + ESP_LOGCONFIG(TAG, " Device info: not yet detected"); + } ESP_LOGCONFIG(TAG, - " Device Model: %s\n" - " FW Version: %s\n" - " Serial Number: %s\n" - " Flash Size: %s\n" - " Max queue age: %u ms\n" - " Startup override: %u ms\n", - this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), - this->flash_size_.c_str(), this->max_q_age_ms_, this->startup_override_ms_); -#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START - ESP_LOGCONFIG(TAG, " Exit reparse: YES\n"); + " Exit reparse: YES\n" #endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START - ESP_LOGCONFIG(TAG, + " Max queue age: %u ms\n" + " Startup override: %u ms\n" " Wake On Touch: %s\n" " Touch Timeout: %" PRIu16, - YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_); + this->max_q_age_ms_, this->startup_override_ms_, YESNO(this->connection_state_.auto_wake_on_touch_), + this->touch_sleep_timeout_); #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c62772ac757..ef030e71da4 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1610,12 +1610,15 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe nextion_writer_t writer_; optional<float> brightness_; -#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - std::string device_model_; - std::string firmware_version_; - std::string serial_number_; - std::string flash_size_; -#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + // Device info populated from comok response (fixed-size, no heap allocation). + // Sizes derived from Nextion Upload Protocol documentation and observed hardware. + static constexpr size_t NEXTION_MODEL_MAX = 24; ///< Max observed ~18 chars from product numbering rules + static constexpr size_t NEXTION_FW_MAX = 7; ///< 'S' prefix + integer (e.g. 'S99' or `123`) + static constexpr size_t NEXTION_SERIAL_MAX = 20; ///< Consistently 16 hex chars across all documented examples + char device_model_[NEXTION_MODEL_MAX + 1]{}; + char firmware_version_[NEXTION_FW_MAX + 1]{}; + char serial_number_[NEXTION_SERIAL_MAX + 1]{}; + uint32_t flash_size_ = 0; ///< Flash size in bytes — plain integer, no string needed void remove_front_no_sensors_(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 162a6034b88..ee8e89de8bd 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -134,7 +134,6 @@ #define USE_MEDIA_SOURCE #define USE_NEXTION_COMMAND_SPACING #define USE_NEXTION_CONF_START_UP_PAGE -#define USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #define USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START #define USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #define USE_NEXTION_MAX_COMMANDS_PER_LOOP diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index fba6a22b97f..d79e3ee2ed2 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -276,7 +276,6 @@ display: auto_wake_on_touch: true brightness: 80% command_spacing: 5ms - dump_device_info: true exit_reparse_on_start: true lambda: |- ESP_LOGD("display","Display is being tested!"); From ff968a4629915b4bf448da32773b7cc8e4204b60 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 15 May 2026 12:36:01 -0400 Subject: [PATCH 2003/2056] [ci] Fix sync-device-classes workflow (failing daily for weeks) (#16448) --- .github/workflows/sync-device-classes.yml | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index f69c7530f76..879d7d5e0c0 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -30,6 +30,11 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Create sync branch + # Switch off dev before pre-commit runs so the + # no-commit-to-branch hook passes (it blocks dev/release/beta). + run: git checkout -B sync/device-classes + - name: Checkout Home Assistant uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -45,15 +50,27 @@ jobs: run: | python -m pip install --upgrade pip pip install -e lib/home-assistant - pip install -r requirements_test.txt pre-commit + # Project requirements are needed so pylint can resolve runtime + # imports (e.g. smpclient in esphome/components/nrf52/ota.py). + pip install -r requirements.txt -r requirements_test.txt pre-commit - name: Sync run: | python ./script/sync-device_class.py - - name: Run pre-commit hooks - run: | - python script/run-in-env.py pre-commit run --all-files + - name: Apply pre-commit auto-fixes + # First pass: let formatters (ruff, end-of-file-fixer, etc.) modify + # files. pre-commit exits non-zero whenever a hook touches anything, + # which would otherwise abort the workflow before the auto-fixes + # can flow into the sync PR. + run: python script/run-in-env.py pre-commit run --all-files || true + + - name: Verify pre-commit clean + # Second pass: re-run all hooks against the now-fixed tree. + # Auto-fixers exit 0 (nothing to change); any remaining failure + # from a check-only hook (pylint / flake8 / yamllint / ci-custom) + # is a real issue and fails the workflow loudly. + run: python script/run-in-env.py pre-commit run --all-files - name: Commit changes uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 From 5b6c54c961d800bbc284729463060ec008ce849d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 09:45:11 -0700 Subject: [PATCH 2004/2056] [ci] sync-device-classes: use uv for installs and skip pylint (#16449) --- .github/workflows/sync-device-classes.yml | 31 ++++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 879d7d5e0c0..7a6f272ff2a 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -46,13 +46,20 @@ jobs: with: python-version: "3.14" + - name: Set up uv + # An order of magnitude faster than pip on cold boots, with its + # own wheel cache. ``--system`` (below) installs into the + # setup-python interpreter so subsequent ``pre-commit`` / + # ``script/run-in-env.py`` steps find the deps without a + # ``uv run`` prefix. + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + - name: Install Home Assistant run: | - python -m pip install --upgrade pip - pip install -e lib/home-assistant - # Project requirements are needed so pylint can resolve runtime - # imports (e.g. smpclient in esphome/components/nrf52/ota.py). - pip install -r requirements.txt -r requirements_test.txt pre-commit + uv pip install --system -e lib/home-assistant + uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit - name: Sync run: | @@ -63,13 +70,23 @@ jobs: # files. pre-commit exits non-zero whenever a hook touches anything, # which would otherwise abort the workflow before the auto-fixes # can flow into the sync PR. + # + # pylint is skipped: this workflow only installs a subset of the + # runtime deps (HA + requirements*.txt), so pylint surfaces + # import-error / relative-beyond-top-level noise that the main CI + # already gates on real PRs. + env: + SKIP: pylint run: python script/run-in-env.py pre-commit run --all-files || true - name: Verify pre-commit clean # Second pass: re-run all hooks against the now-fixed tree. # Auto-fixers exit 0 (nothing to change); any remaining failure - # from a check-only hook (pylint / flake8 / yamllint / ci-custom) - # is a real issue and fails the workflow loudly. + # from a check-only hook (flake8 / yamllint / ci-custom) is a + # real issue and fails the workflow loudly. pylint stays skipped + # for the same reason as above. + env: + SKIP: pylint run: python script/run-in-env.py pre-commit run --all-files - name: Commit changes From 1b1e21d470589a65a63e8456a8a19de88af27d4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 09:54:01 -0700 Subject: [PATCH 2005/2056] [ci] sync-device-classes: drop branch-switch hack, skip no-commit-to-branch instead (#16450) --- .github/workflows/sync-device-classes.yml | 25 +++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 7a6f272ff2a..23a63c5d8a5 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -30,11 +30,6 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Create sync branch - # Switch off dev before pre-commit runs so the - # no-commit-to-branch hook passes (it blocks dev/release/beta). - run: git checkout -B sync/device-classes - - name: Checkout Home Assistant uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -71,22 +66,26 @@ jobs: # which would otherwise abort the workflow before the auto-fixes # can flow into the sync PR. # - # pylint is skipped: this workflow only installs a subset of the - # runtime deps (HA + requirements*.txt), so pylint surfaces - # import-error / relative-beyond-top-level noise that the main CI - # already gates on real PRs. + # SKIP: + # - no-commit-to-branch is a local guard against committing on + # dev/release/beta; CI runs on dev by definition, and + # peter-evans/create-pull-request creates the branch itself. + # - pylint surfaces import-error / relative-beyond-top-level + # noise here because this workflow installs only a subset of + # the runtime deps (HA + requirements*.txt); main CI already + # gates pylint on real PRs. env: - SKIP: pylint + SKIP: pylint,no-commit-to-branch run: python script/run-in-env.py pre-commit run --all-files || true - name: Verify pre-commit clean # Second pass: re-run all hooks against the now-fixed tree. # Auto-fixers exit 0 (nothing to change); any remaining failure # from a check-only hook (flake8 / yamllint / ci-custom) is a - # real issue and fails the workflow loudly. pylint stays skipped - # for the same reason as above. + # real issue and fails the workflow loudly. Same SKIP list as + # above for the same reasons. env: - SKIP: pylint + SKIP: pylint,no-commit-to-branch run: python script/run-in-env.py pre-commit run --all-files - name: Commit changes From 4189979391cb9616ca8a812da678cbb89c50e698 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 09:57:49 -0700 Subject: [PATCH 2006/2056] Synchronise Device Classes from Home Assistant (#16452) Co-authored-by: esphomebot <esphome@openhomefoundation.org> --- esphome/components/sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index f076c7f17b0..6bbab763638 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -96,6 +96,7 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_UPTIME, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, @@ -174,6 +175,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE_DELTA, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_UPTIME, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, DEVICE_CLASS_VOLTAGE, diff --git a/esphome/const.py b/esphome/const.py index fa1ea42bc6d..4557380c739 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1367,6 +1367,7 @@ DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TEMPERATURE_DELTA = "temperature_delta" DEVICE_CLASS_TIMESTAMP = "timestamp" DEVICE_CLASS_UPDATE = "update" +DEVICE_CLASS_UPTIME = "uptime" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" From 4381a8baaaa369ddfdda112e90337c5f87660f3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 09:59:35 -0700 Subject: [PATCH 2007/2056] [ci] pr-title-check: skip all bot authors, not just dependabot (#16453) --- .github/workflows/pr-title-check.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index e15d09da823..e8320672d2b 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -29,10 +29,11 @@ jobs: } = require('./.github/scripts/detect-tags.js'); const title = context.payload.pull_request.title; - const author = context.payload.pull_request.user.login; + const user = context.payload.pull_request.user; - // Skip bot PRs (e.g. dependabot) - they have their own title format - if (author === 'dependabot[bot]') { + // Skip bot PRs (e.g. dependabot, esphome[bot] device-class sync) - + // they have their own title formats. + if (user.type === 'Bot') { return; } From 8b3bc47547d33f96bb3b23e23e5596de27b847f6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli <simone.chemelli@gmail.com> Date: Fri, 15 May 2026 19:16:01 +0200 Subject: [PATCH 2008/2056] [uptime] Update device_class for Uptime sensor (#16434) Co-authored-by: J. Nick Koston <nick@koston.org> --- esphome/components/uptime/sensor/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/uptime/sensor/__init__.py b/esphome/components/uptime/sensor/__init__.py index e2a7aee1a2f..6ce0795cdb7 100644 --- a/esphome/components/uptime/sensor/__init__.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -4,7 +4,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_TIME_ID, DEVICE_CLASS_DURATION, - DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_UPTIME, ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER, STATE_CLASS_TOTAL_INCREASING, @@ -33,9 +33,8 @@ CONFIG_SCHEMA = cv.typed_schema( ).extend(cv.polling_component_schema("60s")), "timestamp": sensor.sensor_schema( UptimeTimestampSensor, - icon=ICON_TIMER, accuracy_decimals=0, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=DEVICE_CLASS_UPTIME, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) .extend( From ec1826a6ed47d9a94ad64b6751ec46a2088f7ce4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:17:50 -0700 Subject: [PATCH 2009/2056] [yaml_util] Promote include-discovery helper, share it with bundle (#16447) --- esphome/bundle.py | 97 ++----------- esphome/yaml_util.py | 125 +++++++++++++++++ tests/unit_tests/test_bundle.py | 12 +- tests/unit_tests/test_yaml_util.py | 216 +++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 91 deletions(-) diff --git a/esphome/bundle.py b/esphome/bundle.py index 70c4fad0fda..4537cbce9d1 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -260,42 +260,20 @@ class ConfigBundleCreator: def _discover_yaml_includes(self) -> None: """Discover YAML files loaded during config parsing. - Deliberately uses a fresh re-parse and force-loads every deferred - ``IncludeFile`` to include *all* potentially-reachable includes, - even branches not selected by the local substitutions. Bundles are - meant to be compiled on another system where command-line - substitution overrides may choose a different branch — e.g. - ``!include network/${eth_model}/config.yaml`` must ship every - candidate so the remote build can pick any one. - - Entries with unresolved substitution variables in the filename - path are skipped with a warning (they cannot be resolved without - the substitution pass). - - Secrets files are tracked separately so we can filter them to - only include the keys this config actually references. + Delegates to :func:`yaml_util.discover_user_yaml_files`, which does a + fresh re-parse and force-loads every deferred ``IncludeFile`` so that + *all* potentially-reachable includes are captured (even branches not + selected by local substitutions). Bundles are meant to be compiled on + another system where command-line substitution overrides may choose a + different branch — e.g. ``!include network/${eth_model}/config.yaml`` + must ship every candidate so the remote build can pick any one. """ - # Must be a fresh parse: IncludeFile.load() caches its result in - # _content, and we discover files by listening for loader calls. On - # an already-parsed tree the cache is populated, .load() returns - # without calling the loader, the listener never fires, and the - # referenced files would be silently dropped from the bundle. - with yaml_util.track_yaml_loads() as loaded_files: - try: - data = yaml_util.load_yaml(self._config_path) - except EsphomeError: - _LOGGER.debug( - "Bundle: re-loading YAML for include discovery failed, " - "proceeding with partial file list" - ) - else: - _force_load_include_files(data) - - for fpath in loaded_files: - if fpath == self._config_path.resolve(): + discovered = yaml_util.discover_user_yaml_files(self._config_path) + self._secrets_paths.update(discovered.secrets) + config_resolved = self._config_path.resolve() + for fpath in discovered.files: + if fpath == config_resolved: continue # Already added as config - if fpath.name in const.SECRETS_FILES: - self._secrets_paths.add(fpath) self._add_file(fpath) def _discover_component_files(self) -> None: @@ -625,57 +603,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None: tar.addfile(info, io.BytesIO(data)) -def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None: - """Recursively resolve any ``IncludeFile`` instances in a YAML tree. - - Nested ``!include`` returns a deferred ``IncludeFile`` that is only - resolved during the substitution pass. During bundle discovery we need - the referenced files to actually load so the ``track_yaml_loads`` - listener fires for them. - - ``IncludeFile`` instances with unresolved substitution variables in the - filename cannot be loaded — we skip and warn about those. - """ - if _seen is None: - _seen = set() - - if isinstance(obj, yaml_util.IncludeFile): - if id(obj) in _seen: - return - _seen.add(id(obj)) - if obj.has_unresolved_expressions(): - _LOGGER.warning( - "Bundle: cannot resolve !include %s (referenced from %s) " - "with substitutions in path", - obj.file, - obj.parent_file, - ) - return - try: - loaded = obj.load() - except EsphomeError as err: - _LOGGER.warning( - "Bundle: failed to load !include %s (referenced from %s): %s", - obj.file, - obj.parent_file, - err, - ) - return - _force_load_include_files(loaded, _seen) - elif isinstance(obj, dict): - if id(obj) in _seen: - return - _seen.add(id(obj)) - for value in obj.values(): - _force_load_include_files(value, _seen) - elif isinstance(obj, (list, tuple)): - if id(obj) in _seen: - return - _seen.add(id(obj)) - for item in obj: - _force_load_include_files(item, _seen) - - def _resolve_include_path(include_path: Any) -> Path | None: """Resolve an include path to absolute, skipping system includes.""" if isinstance(include_path, str) and include_path.startswith("<"): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index b153d160a72..b56d0244189 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Generator from contextlib import contextmanager, suppress +from dataclasses import dataclass, field import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper @@ -233,6 +234,130 @@ class IncludeFile: return has_substitution_or_expression(str(self.file)) +def force_load_include_files( + obj: Any, + *, + warn_on_unresolved: bool = True, + _seen: set[int] | None = None, +) -> None: + """Recursively resolve any deferred ``IncludeFile`` instances in a YAML tree. + + Nested ``!include`` returns a deferred ``IncludeFile`` that is only resolved + later (substitution / packages pass). Callers that need every referenced + file to actually load — bundle discovery, on-device YAML recovery — invoke + this while a :func:`track_yaml_loads` listener is active so the underlying + loader fires and records every reachable file. + + ``IncludeFile`` instances whose path contains unresolved substitution + variables cannot be loaded. By default a warning is logged for each one; + pass ``warn_on_unresolved=False`` (used by discovery paths that run on a + fresh re-parse where substitutions haven't been applied yet) to demote it + to a debug log. + """ + if _seen is None: + _seen = set() + + if isinstance(obj, IncludeFile): + if id(obj) in _seen: + return + _seen.add(id(obj)) + if obj.has_unresolved_expressions(): + log = _LOGGER.warning if warn_on_unresolved else _LOGGER.debug + log( + "Cannot resolve !include %s (referenced from %s) with substitutions in path", + obj.file, + obj.parent_file, + ) + return + try: + loaded = obj.load() + except EsphomeError as err: + _LOGGER.warning( + "Failed to load !include %s (referenced from %s): %s", + obj.file, + obj.parent_file, + err, + ) + return + force_load_include_files( + loaded, warn_on_unresolved=warn_on_unresolved, _seen=_seen + ) + elif isinstance(obj, dict): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for value in obj.values(): + force_load_include_files( + value, warn_on_unresolved=warn_on_unresolved, _seen=_seen + ) + elif isinstance(obj, (list, tuple)): + if id(obj) in _seen: + return + _seen.add(id(obj)) + for item in obj: + force_load_include_files( + item, warn_on_unresolved=warn_on_unresolved, _seen=_seen + ) + + +@dataclass(slots=True) +class DiscoveredYamlFiles: + """Result of :func:`discover_user_yaml_files`. + + ``files`` contains every resolved path the YAML loader touched while we + were re-parsing the user's config; ``secrets`` is the subset whose + *un-resolved* filename matched :data:`esphome.const.SECRETS_FILES` (so + a ``secrets.yaml`` symlinked to a differently-named target is still + flagged as secrets). + """ + + files: list[Path] = field(default_factory=list) + secrets: set[Path] = field(default_factory=set) + + +def discover_user_yaml_files(config_path: Path) -> DiscoveredYamlFiles: + """Fresh-re-parse ``config_path`` and report every file the YAML loader + pulled in, plus which of them came in under a secrets filename. + + Does NOT run schema validation, substitutions, or package resolution — so + component-internal YAML loaded by validators (LVGL helpers, dashboard + imports, etc.) is *not* captured. Deferred ``!include`` references whose + paths don't depend on substitutions are force-loaded here so they're + captured too. + + Must run on a fresh parse because :meth:`IncludeFile.load` caches its + result; on an already-resolved tree :meth:`load` returns without invoking + the loader and the listener would not fire for the referenced files. + """ + from esphome.const import SECRETS_FILES + + secrets: set[Path] = set() + + def _capture_secret(fname: Path) -> None: + if Path(fname).name in SECRETS_FILES: + secrets.add(Path(fname).resolve()) + + with track_yaml_loads() as loaded: + _load_listeners.append(_capture_secret) + try: + try: + data = load_yaml(config_path) + except EsphomeError: + return DiscoveredYamlFiles(list(loaded), secrets) + force_load_include_files(data, warn_on_unresolved=False) + finally: + _load_listeners.remove(_capture_secret) + + # Deduplicate while preserving first-seen order. + seen: set[Path] = set() + unique: list[Path] = [] + for path in loaded: + if path not in seen: + seen.add(path) + unique.append(path) + return DiscoveredYamlFiles(unique, secrets) + + def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 5d046252dae..f15bbf2e297 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -22,13 +22,13 @@ from esphome.bundle import ( _add_bytes_to_tar, _default_target_dir, _find_used_secret_keys, - _force_load_include_files, extract_bundle, is_bundle_path, prepare_bundle_for_compile, read_bundle_manifest, ) from esphome.core import CORE, EsphomeError +from esphome.yaml_util import force_load_include_files # --------------------------------------------------------------------------- # Helpers @@ -947,7 +947,7 @@ def test_discover_files_nested_include_load_failure( paths = [f.path for f in files] assert "test.yaml" in paths assert any( - "failed to load !include" in r.message and "missing.yaml" in r.message + "failed to load !include" in r.message.lower() and "missing.yaml" in r.message for r in caplog.records ) @@ -974,8 +974,8 @@ def test_force_load_skips_duplicate_include_file() -> None: # Same instance appears twice — second visit must hit the _seen guard. tree = {"a": stub, "b": [stub]} - with patch("esphome.bundle.yaml_util.IncludeFile", _StubInclude): - _force_load_include_files(tree) + with patch("esphome.yaml_util.IncludeFile", _StubInclude): + force_load_include_files(tree) assert stub.load_calls == 1 @@ -989,8 +989,8 @@ def test_force_load_handles_cyclic_containers() -> None: cyclic_list.append(cyclic_list) # Should return without recursing forever - _force_load_include_files(cyclic_dict) - _force_load_include_files(cyclic_list) + force_load_include_files(cyclic_dict) + force_load_include_files(cyclic_list) def test_discover_files_yaml_reload_failure( diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index ace92fbf6f8..e97a188be41 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -12,11 +12,15 @@ import esphome.config_validation as cv from esphome.core import DocumentLocation, DocumentRange, EsphomeError from esphome.util import OrderedDict from esphome.yaml_util import ( + DiscoveredYamlFiles, ESPHomeDataBase, ESPLiteralValue, + discover_user_yaml_files, + force_load_include_files, format_path, make_data_base, make_literal, + track_yaml_loads, ) @@ -966,3 +970,215 @@ def test_make_literal_blocks_substitution() -> None: # undefined in the context. assert result == {"pin": "${PIN}"} assert isinstance(result, ESPLiteralValue) + + +# --------------------------------------------------------------------------- +# force_load_include_files / discover_user_yaml_files +# --------------------------------------------------------------------------- + + +class _StubInclude: + """Stand-in for `IncludeFile` that records how `load()` was called. + + Patched in via `esphome.yaml_util.IncludeFile` so the recursion in + `force_load_include_files` treats instances as deferred includes without + needing an actual on-disk file. + """ + + def __init__( + self, + file: str = "stub.yaml", + parent_file: Path | None = None, + *, + unresolved: bool = False, + load_result: object = None, + raise_on_load: EsphomeError | None = None, + ) -> None: + self.file = Path(file) + self.parent_file = parent_file or Path("/tmp/parent.yaml") + self._unresolved = unresolved + self._load_result = load_result if load_result is not None else {} + self._raise = raise_on_load + self.load_calls = 0 + + def has_unresolved_expressions(self) -> bool: + return self._unresolved + + def load(self) -> object: + self.load_calls += 1 + if self._raise is not None: + raise self._raise + return self._load_result + + +@pytest.fixture +def patch_include_file(): + """Replace `IncludeFile` with `_StubInclude` so isinstance checks in + `force_load_include_files` match the stubs constructed by tests.""" + with patch("esphome.yaml_util.IncludeFile", _StubInclude): + yield + + +def test_force_load_include_files_resolves_nested_includes( + patch_include_file: None, +) -> None: + """A tree of dict/list/IncludeFile is walked and every IncludeFile is loaded.""" + inner = _StubInclude("inner.yaml") + outer = _StubInclude("outer.yaml", load_result={"nested": inner}) + force_load_include_files([{"a": outer}, "scalar"]) + assert outer.load_calls == 1 + assert inner.load_calls == 1 + + +def test_force_load_include_files_seen_guard_prevents_double_load( + patch_include_file: None, +) -> None: + """The same IncludeFile referenced from two branches loads once.""" + stub = _StubInclude("once.yaml") + force_load_include_files({"a": stub, "b": [stub]}) + assert stub.load_calls == 1 + + +def test_force_load_include_files_handles_cyclic_containers() -> None: + """Cyclic dict/list references don't trigger infinite recursion.""" + cyclic_dict: dict[str, object] = {} + cyclic_dict["self"] = cyclic_dict + cyclic_list: list[object] = [] + cyclic_list.append(cyclic_list) + # Both calls must return without recursing forever. + force_load_include_files(cyclic_dict) + force_load_include_files(cyclic_list) + + +@pytest.mark.parametrize( + ("warn_on_unresolved", "expect_level"), + [ + pytest.param(True, "WARNING", id="default-warns"), + pytest.param(False, "DEBUG", id="opt-in-demotes"), + ], +) +def test_force_load_include_files_unresolved_log_level( + patch_include_file: None, + caplog: pytest.LogCaptureFixture, + warn_on_unresolved: bool, + expect_level: str, +) -> None: + """Substitution-templated include paths skip the load and log at the + level chosen by `warn_on_unresolved`.""" + stub = _StubInclude("${var}.yaml", unresolved=True) + with caplog.at_level("DEBUG", logger="esphome.yaml_util"): + force_load_include_files({"k": stub}, warn_on_unresolved=warn_on_unresolved) + assert stub.load_calls == 0 + matching = [ + r.levelname for r in caplog.records if "Cannot resolve !include" in r.message + ] + assert matching == [expect_level] + + +def test_force_load_include_files_warns_on_load_failure( + patch_include_file: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """An `EsphomeError` raised by `load()` is caught and logged, not propagated.""" + stub = _StubInclude("missing.yaml", raise_on_load=EsphomeError("boom")) + with caplog.at_level("WARNING", logger="esphome.yaml_util"): + force_load_include_files({"k": stub}) + assert any( + "Failed to load !include" in r.message and "missing.yaml" in r.message + for r in caplog.records + ) + + +def test_discovered_yaml_files_holds_files_and_secrets() -> None: + """`DiscoveredYamlFiles` is a small data carrier; both fields are mandatory.""" + files = [Path("/tmp/a.yaml")] + secrets = {Path("/tmp/a.yaml")} + discovered = DiscoveredYamlFiles(files, secrets) + assert discovered.files is files + assert discovered.secrets is secrets + + +def _write(tmp_path: Path, name: str, content: str) -> Path: + """Write `content` to `tmp_path/name`, creating parent dirs as needed.""" + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + return path + + +def _write_entry_including(tmp_path: Path, included_name: str) -> Path: + """Write a minimal entry yaml that `!include`s `included_name`.""" + return _write( + tmp_path, + "entry.yaml", + f"esphome:\n name: test\nwifi: !include {included_name}\n", + ) + + +def test_discover_user_yaml_files_captures_includes(tmp_path: Path) -> None: + """A `!include` in the entry yaml is force-loaded so the listener fires.""" + _write(tmp_path, "wifi.yaml", "ssid: my_ssid\npassword: my_pw\n") + discovered = discover_user_yaml_files(_write_entry_including(tmp_path, "wifi.yaml")) + names = {p.name for p in discovered.files} + assert names == {"entry.yaml", "wifi.yaml"} + assert discovered.secrets == set() + + +@pytest.mark.parametrize( + "secret_name", + [ + pytest.param("secrets.yaml", id="yaml"), + pytest.param("secrets.yml", id="yml"), + ], +) +def test_discover_user_yaml_files_flags_secrets_filename( + tmp_path: Path, secret_name: str +) -> None: + """Both `secrets.yaml` and `secrets.yml` get flagged in `.secrets`.""" + _write(tmp_path, secret_name, "key: value\n") + discovered = discover_user_yaml_files(_write_entry_including(tmp_path, secret_name)) + assert (tmp_path / secret_name).resolve() in discovered.secrets + + +def test_discover_user_yaml_files_flags_secrets_symlink(tmp_path: Path) -> None: + """`secrets.yaml` symlinked to a non-secrets-named target is still flagged + because the un-resolved basename is what gets recorded.""" + target = _write(tmp_path, "real_creds.yaml", "key: value\n") + (tmp_path / "secrets.yaml").symlink_to(target) + discovered = discover_user_yaml_files( + _write_entry_including(tmp_path, "secrets.yaml") + ) + # The recorded "secret path" is the resolved target — even though its + # basename is `real_creds.yaml`, it's still in `.secrets`. + assert target.resolve() in discovered.secrets + + +def test_discover_user_yaml_files_swallows_parse_errors(tmp_path: Path) -> None: + """A YAML parse failure returns whatever was tracked so far without raising.""" + entry = _write(tmp_path, "entry.yaml", "esphome: [unterminated\n") + discovered = discover_user_yaml_files(entry) + assert isinstance(discovered, DiscoveredYamlFiles) + + +def test_discover_user_yaml_files_deduplicates(tmp_path: Path) -> None: + """The same file referenced twice appears once in `.files`.""" + _write(tmp_path, "wifi.yaml", "ssid: a\n") + entry = _write( + tmp_path, + "entry.yaml", + "esphome:\n name: test\nwifi: !include wifi.yaml\nfoo: !include wifi.yaml\n", + ) + discovered = discover_user_yaml_files(entry) + wifi_resolved = (tmp_path / "wifi.yaml").resolve() + assert discovered.files.count(wifi_resolved) == 1 + + +def test_track_yaml_loads_records_resolved_paths(tmp_path: Path) -> None: + """`track_yaml_loads` is the building block — sanity-check it resolves + symlinks so callers can dedupe by identity.""" + target = _write(tmp_path, "actual.yaml", "esphome:\n name: t\n") + link = tmp_path / "alias.yaml" + link.symlink_to(target) + with track_yaml_loads() as loaded: + yaml_util.load_yaml(link) + assert target.resolve() in loaded From 46be0f4f62141a6dc5bfe7b55acb753b2b64c343 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:18:07 -0700 Subject: [PATCH 2010/2056] [ci] Log top 30 pytest durations (#16455) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abd2d1b3a54..57c896fe197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,7 +181,7 @@ jobs: # own CI). No ``--cov`` here -- this is purely a downstream # smoke check against this PR's esphome code. working-directory: device-builder - run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks + run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks pytest: name: Run pytest @@ -222,12 +222,12 @@ jobs: if: matrix.os == 'windows-latest' run: | . ./venv/Scripts/activate.ps1 - pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ + pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ - name: Run pytest if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . venv/bin/activate - pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ + pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: @@ -370,7 +370,7 @@ jobs: . venv/bin/activate mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]') echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" - pytest -vv --no-cov --tb=native -n auto "${test_files[@]}" + pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}" cpp-unit-tests: name: Run C++ unit tests From 1674ed9744414bc00f91a8ae5b9255dbeac2f19f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:18:27 -0700 Subject: [PATCH 2011/2056] [ci] Use uv for pip installs across CI workflows (#16451) --- .github/actions/restore-python/action.yml | 16 ++++++++++++---- .github/workflows/ci-api-proto.yml | 8 +++++++- .github/workflows/ci.yml | 22 ++++++++++++++++++---- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 21393f2aba3..751f9ecf58c 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -27,6 +27,14 @@ runs: path: venv # yamllint disable-line rule:line-length key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} + - name: Set up uv + # Only needed on cache miss to populate the venv. ``uv pip install`` + # detects the activated venv via ``VIRTUAL_ENV`` so the venv layout + # downstream jobs rely on is preserved. + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows' shell: bash @@ -34,8 +42,8 @@ runs: python -m venv venv source venv/bin/activate python --version - pip install -r requirements.txt -r requirements_test.txt - pip install -e . + uv pip install -r requirements.txt -r requirements_test.txt + uv pip install -e . - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' shell: bash @@ -43,5 +51,5 @@ runs: python -m venv venv source ./venv/Scripts/activate python --version - pip install -r requirements.txt -r requirements_test.txt - pip install -e . + uv pip install -r requirements.txt -r requirements_test.txt + uv pip install -e . diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 2f7fd271ba2..1dc0ccb7fee 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -26,6 +26,12 @@ jobs: uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" + - name: Set up uv + # ``--system`` (below) installs into the setup-python interpreter; + # no venv is created or restored by this workflow. + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true - name: Install apt dependencies run: | @@ -34,7 +40,7 @@ jobs: sudo apt install -y protobuf-compiler protoc --version - name: Install python dependencies - run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt + run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt - name: Generate files run: script/api_protobuf/api_protobuf.py - name: Check for changes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57c896fe197..de21456841e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,14 +52,22 @@ jobs: path: venv # yamllint disable-line rule:line-length key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }} + - name: Set up uv + # Only needed on cache miss to populate the venv. ``uv pip install`` + # detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs + # that ``. venv/bin/activate`` see an identical layout. + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit - pip install -e . + uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit + uv pip install -e . pylint: name: Check pylint @@ -351,14 +359,20 @@ jobs: with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} + - name: Set up uv + # Only needed on cache miss to populate the venv. + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install -r requirements.txt -r requirements_test.txt - pip install -e . + uv pip install -r requirements.txt -r requirements_test.txt + uv pip install -e . - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run integration tests From 96106d25bc4555423ce7a05f2a8ee0888ed2641e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:20:15 -0700 Subject: [PATCH 2012/2056] [wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444) --- esphome/__main__.py | 8 +++ esphome/components/wifi/__init__.py | 52 +++++++++++++++- esphome/const.py | 9 +++ tests/unit_tests/components/test_wifi.py | 78 +++++++++++++++++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index d733534a5c0..16a05ad552b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -50,6 +50,7 @@ from esphome.const import ( CONF_TOPIC, CONF_USERNAME, CONF_WEB_SERVER, + CONF_WIFI, ENV_NOGITIGNORE, KEY_CORE, KEY_TARGET_PLATFORM, @@ -733,6 +734,13 @@ def write_cpp_file() -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int: + # Keep this gate here, NOT in config validation: device-builder needs + # `esphome config` to keep succeeding with placeholders so onboarding can run. + if CONF_WIFI in config: + from esphome.components.wifi import check_placeholder_credentials + + check_placeholder_credentials(config) + # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index bad57fc4810..f9cb391442c 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,10 +54,18 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, Platform, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + EsphomeError, + HexInt, + coroutine_with_priority, +) import esphome.final_validate as fv from esphome.types import ConfigType @@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO}, } ) + + +def _placeholder_wifi_credentials(config: ConfigType) -> list[str]: + """Return human-readable locations where the dashboard's placeholder wifi + values still appear. Empty list means no placeholders were found. + """ + placeholders: list[str] = [] + wifi_conf = config.get(CONF_WIFI) + if not wifi_conf: + return placeholders + + for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])): + ssid = network.get(CONF_SSID) + if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append(f"wifi.networks[{idx}].ssid") + + ap_conf = wifi_conf.get(CONF_AP) + if ap_conf: + ap_ssid = ap_conf.get(CONF_SSID) + if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append("wifi.ap.ssid") + + return placeholders + + +def check_placeholder_credentials(config: ConfigType) -> None: + """Raise EsphomeError if any wifi credential is the dashboard placeholder. + + Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, + or any path reached by `esphome config`; device-builder relies on + validation passing with the placeholders still in place. + """ + locations = _placeholder_wifi_credentials(config) + if not locations: + return + formatted = ", ".join(locations) + raise EsphomeError( + f"wifi configuration still contains the dashboard placeholder value " + f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. " + f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') " + f"with your real wifi credentials before flashing." + ) diff --git a/esphome/const.py b/esphome/const.py index 4557380c739..9dd77a7cb8a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1416,3 +1416,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" # The corresponding constant exists in c++ # when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds SCHEDULER_DONT_RUN = 4294967295 + +# Sentinel values written by the esphome-device-builder dashboard into +# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password +# references resolve cleanly through validation before the user has finished +# the onboarding wizard. Compilation refuses if these reach the binary so that +# a user who dismisses onboarding can't accidentally flash a device that will +# never associate with their wifi. +PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK" +PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105 diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py index 71a14d78177..9598c1bdd86 100644 --- a/tests/unit_tests/components/test_wifi.py +++ b/tests/unit_tests/components/test_wifi.py @@ -3,8 +3,20 @@ import pytest from esphome.components.esp32 import const -from esphome.components.wifi import has_native_wifi, variant_has_wifi -from esphome.const import Platform +from esphome.components.wifi import ( + check_placeholder_credentials, + has_native_wifi, + variant_has_wifi, +) +from esphome.const import ( + CONF_AP, + CONF_NETWORKS, + CONF_SSID, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, + Platform, +) +from esphome.core import EsphomeError, Lambda @pytest.mark.parametrize( @@ -123,3 +135,65 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: """RP2040 without a board id falls open to True (custom-board default).""" assert has_native_wifi(platform=Platform.RP2040) is True + + +def _wifi_config( + *, + networks: list[dict] | None = None, + ap: dict | None = None, +) -> dict: + """Build a minimal config dict matching the post-validation shape.""" + wifi: dict = {} + if networks is not None: + wifi[CONF_NETWORKS] = networks + if ap is not None: + wifi[CONF_AP] = ap + return {CONF_WIFI: wifi} + + +def test_check_placeholder_credentials_passes_with_real_ssid() -> None: + """A real SSID compiles without complaint.""" + config = _wifi_config(networks=[{CONF_SSID: "home_network"}]) + assert check_placeholder_credentials(config) is None + + +def test_check_placeholder_credentials_refuses_placeholder_ssid() -> None: + """The placeholder SSID is rejected with an actionable message.""" + config = _wifi_config(networks=[{CONF_SSID: PLACEHOLDER_WIFI_SSID}]) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + message = str(exc_info.value) + assert "wifi.networks[0].ssid" in message + assert "secrets.yaml" in message + + +def test_check_placeholder_credentials_refuses_placeholder_in_second_network() -> None: + """Index reporting picks the placeholder out of a mixed network list.""" + config = _wifi_config( + networks=[ + {CONF_SSID: "home_network"}, + {CONF_SSID: PLACEHOLDER_WIFI_SSID}, + ], + ) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.networks[1].ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_refuses_placeholder_ap_ssid() -> None: + """An AP using the placeholder broadcast name is also refused.""" + config = _wifi_config(ap={CONF_SSID: PLACEHOLDER_WIFI_SSID}) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.ap.ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_no_wifi_passes() -> None: + """Ethernet-only / wifi-less configs skip the check entirely.""" + assert check_placeholder_credentials({}) is None + + +def test_check_placeholder_credentials_skips_template_ssid() -> None: + """A templated (Lambda) SSID is not a string and is skipped.""" + config = _wifi_config(networks=[{CONF_SSID: Lambda('return "x";')}]) + assert check_placeholder_credentials(config) is None From 35631be260c0fd6fae1e4c945f16790979ba777c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:20:31 -0700 Subject: [PATCH 2013/2056] [writer] Mark storage_should_clean as public API for device-builder (#16443) --- esphome/writer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/esphome/writer.py b/esphome/writer.py index 72c2c355dcc..ad3877465d2 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -87,6 +87,21 @@ def replace_file_content(text, pattern, repl): def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool: + """Return True when the build tree must be wiped before reuse. + + Predicate is True when *old* is missing (first build), + ``src_version`` differs, ``build_path`` differs, or a previously + loaded integration was removed in *new*. Adding integrations or + changing unrelated fields (friendly name, esphome version, etc.) + does not trigger a clean. + + Used by esphome-device-builder (esphome/device-builder) to gate + its remote-build artifact materialiser so a local → remote → local + cycle preserves PlatformIO's local object cache instead of wiping + it on every cycle. The signature, semantics, and ``None`` handling + for *old* are part of the public contract; keep them stable so the + offloader's wipe decision tracks core's. + """ if old is None: return True From 47eb2adbf2c789d62405d69954d468ddb51d40a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:29:15 -0700 Subject: [PATCH 2014/2056] [core] Fix KeyError: 'esp32' on upload when validated-config cache is used (#16457) --- esphome/storage_json.py | 13 +++++- tests/unit_tests/test_compiled_config.py | 56 ++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 7d26b22f96c..e4818270804 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -273,10 +273,21 @@ class StorageJSON: """ CORE.name = self.name CORE.build_path = self.build_path + target_platform = self.core_platform or self.target_platform.lower() CORE.data[KEY_CORE] = { - KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(), + KEY_TARGET_PLATFORM: target_platform, KEY_TARGET_FRAMEWORK: self.framework, } + # The compile pipeline populates CORE.data[KEY_ESP32] when esp32's + # validator runs; on the cache fast path that validator is skipped, + # so populate the variant upload_using_esptool reads via + # esp32.get_esp32_variant(). target_platform on disk is the variant + # (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32"). + if target_platform == const.PLATFORM_ESP32: + from esphome.components.esp32.const import KEY_ESP32 + from esphome.const import KEY_VARIANT + + CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform} def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index 34e811b97bd..8c9cfa8101b 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -22,6 +22,7 @@ from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + KEY_VARIANT, ) from esphome.core import CORE @@ -47,7 +48,12 @@ wifi: """ -def _write_storage(storage_path: Path) -> None: +def _write_storage( + storage_path: Path, + *, + esp_platform: str = "ESP32", + core_platform: str | None = "esp32", +) -> None: """Write a vanilla StorageJSON sidecar for the cache tests.""" storage_path.parent.mkdir(parents=True, exist_ok=True) data = { @@ -59,14 +65,14 @@ def _write_storage(storage_path: Path) -> None: "src_version": 1, "address": "192.168.1.42", "web_port": None, - "esp_platform": "ESP32", + "esp_platform": esp_platform, "build_path": "/build/lite_test", "firmware_bin_path": "/build/lite_test/firmware.bin", "loaded_integrations": ["api", "logger", "ota", "wifi"], "loaded_platforms": [], "no_mdns": False, "framework": "arduino", - "core_platform": "esp32", + "core_platform": core_platform, } storage_path.write_text(json.dumps(data)) @@ -123,6 +129,50 @@ def test_load_compiled_config_happy_path(fresh_cache_files: Path) -> None: assert CORE.build_path == Path("/build/lite_test") assert CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] == "esp32" assert CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] == "arduino" + # upload_using_esptool reads get_esp32_variant() off CORE.data[KEY_ESP32]. + from esphome.components.esp32.const import KEY_ESP32 + + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32" + + +def test_load_compiled_config_populates_esp32_variant(tmp_path: Path) -> None: + """ESP32 variants survive the cache fast path so esptool gets the right --chip.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage(storage_dir / "lite_test.yaml.json", esp_platform="ESP32S3") + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32S3" + + +def test_load_compiled_config_skips_esp32_block_for_other_platforms( + tmp_path: Path, +) -> None: + """Non-esp32 targets shouldn't fabricate an esp32 data block.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage( + storage_dir / "lite_test.yaml.json", + esp_platform="ESP8266", + core_platform="esp8266", + ) + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert KEY_ESP32 not in CORE.data @pytest.mark.parametrize( From 65d6bb18ed09c4d916ed7c6f1ad98eae2284a34b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 15 May 2026 13:32:51 -0400 Subject: [PATCH 2015/2056] [esp32_hosted][fingerprint_grow] Fix two remaining ESP32 toolchain warnings (#16442) --- esphome/components/esp32_hosted/update/esp32_hosted_update.cpp | 2 +- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 7f3ba778951..70fa41b3124 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -121,7 +121,7 @@ void Esp32HostedUpdate::setup() { } } else { ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", - app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); + app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD)); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 3f57789034e..b38d42191bc 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { break; case ENROLL_MISMATCH: ESP_LOGE(TAG, "Scans do not match"); + [[fallthrough]]; default: return this->data_[0]; } From fb70095ba194b3d01f1a0801dd2cfdec9d7e0290 Mon Sep 17 00:00:00 2001 From: david-collett <david.collett@gmail.com> Date: Sat, 16 May 2026 03:47:26 +1000 Subject: [PATCH 2016/2056] [esp32_ble_server] Fix incorrect BLECharacteristic read truncation (#16420) (#16422) Co-authored-by: Dave <dave@morty> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../esp32_ble_server/ble_characteristic.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index cc519846be4..842c78a8aa7 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -218,13 +218,14 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } } else { response.attr_value.offset = 0; - if (this->value_.size() + 1 > max_offset) { - response.attr_value.len = max_offset; - this->value_read_offset_ = max_offset; - } else { - response.attr_value.len = this->value_.size(); + response.attr_value.len = this->value_.size(); + if (response.attr_value.len > ESP_GATT_MAX_ATTR_LEN) { + ESP_LOGW(TAG, "Characteristic length %u exceeds buffer size of %u, truncating", response.attr_value.len, + ESP_GATT_MAX_ATTR_LEN); + response.attr_value.len = ESP_GATT_MAX_ATTR_LEN; } memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len); + this->value_read_offset_ = 0; } response.attr_value.handle = this->handle_; From 59f8c1019f23b603344014ddba4258e95f4e30c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:37:28 -0700 Subject: [PATCH 2017/2056] Bump pytest-codspeed from 5.0.1 to 5.0.2 (#16459) Signed-off-by: dependabot[bot] <support@github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9050132e705..ea4941a8824 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ hypothesis==6.92.1 # CodSpeed benchmarks under tests/benchmarks/python/ # (skipped via pytest.importorskip when missing -- only required for the # benchmarks job in .github/workflows/ci.yml) -pytest-codspeed==5.0.1 +pytest-codspeed==5.0.2 # Used by the import-time regression check (.github/workflows/ci.yml → import-time job) importtime-waterfall==1.0.0 From ec6669fa679ffed9d371c42ad8d3c9ee935b0bb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:37:41 -0700 Subject: [PATCH 2018/2056] Bump requests from 2.34.1 to 2.34.2 (#16460) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6291b5cd414..a0a7ea5674a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.34.1 +requests==2.34.2 # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 From 4c090c6b8512adf16cf45fc0b71a031bb0b12407 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:37:56 -0700 Subject: [PATCH 2019/2056] Bump github/codeql-action from 4.35.4 to 4.35.5 (#16461) Signed-off-by: dependabot[bot] <support@github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0a4dd9a92dd..fbef0f51577 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -56,7 +56,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -84,6 +84,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: category: "/language:${{matrix.language}}" From b78b78cbbbf1ee8ca65ef6ff563faadb449fe2ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 14:42:51 -0700 Subject: [PATCH 2020/2056] Bump aioesphomeapi from 45.0.0 to 45.0.1 (#16467) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a0a7ea5674a..33a6c5b5557 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.0 +aioesphomeapi==45.0.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From ff34e1061bc9ce55e39ff61f04a57a0258e94c76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 14:43:08 -0700 Subject: [PATCH 2021/2056] Bump resvg-py from 0.3.1 to 0.3.2 (#16466) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 33a6c5b5557..1996de09289 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==12.2.0 -resvg-py==0.3.1 +resvg-py==0.3.2 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 From 1a287bf785af01ce50b52dcecc5a938babd75a01 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:30:04 +1000 Subject: [PATCH 2022/2056] [ft5x06] Fix setting calibration values (#16446) --- .../ft5x06/touchscreen/ft5x06_touchscreen.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp index 835dc4aac03..24d3529fb4c 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -15,6 +15,16 @@ void FT5x06Touchscreen::setup() { this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); } + // reading the chip registers to get max x/y does not seem to work. + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + } + // wait 200ms after reset. this->set_timeout(200, [this] { this->continue_setup_(); }); } @@ -39,15 +49,6 @@ void FT5x06Touchscreen::continue_setup_() { this->mark_failed(); return; } - // reading the chip registers to get max x/y does not seem to work. - if (this->display_ != nullptr) { - if (this->x_raw_max_ == this->x_raw_min_) { - this->x_raw_max_ = this->display_->get_native_width(); - } - if (this->y_raw_max_ == this->y_raw_min_) { - this->y_raw_max_ = this->display_->get_native_height(); - } - } } void FT5x06Touchscreen::update_touches() { @@ -71,7 +72,7 @@ void FT5x06Touchscreen::update_touches() { uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); - ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); if (status == 0 || status == 2) { this->add_raw_touch_position_(id, x, y); } From df100681e0286ad13557a81027d46f962a21f349 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:36:53 +1000 Subject: [PATCH 2023/2056] [lvgl] Fix image define (#16468) --- esphome/components/lvgl/__init__.py | 5 +++++ esphome/components/lvgl/lvgl_esphome.h | 10 ++++++---- esphome/components/lvgl/styles.py | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 91b101cd258..4277c14dd7c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -55,6 +55,7 @@ from .automation import layers_to_code, lvgl_update from .defines import ( CONF_ALIGN_TO_LAMBDA_ID, LOGGER, + add_lv_use, get_focused_widgets, get_lv_images_used, get_refreshed_widgets, @@ -71,6 +72,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( + BASE_PROPS, DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, STYLE_REMAP, @@ -100,6 +102,7 @@ from .widgets import ( get_screen_active, set_obj_properties, ) +from .widgets.img import CONF_IMAGE # Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code @@ -433,6 +436,8 @@ async def to_code(configs): # This must be done after all widgets are created styles_used = df.get_styles_used() + if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used): + add_lv_use(CONF_IMAGE) for use in df.get_lv_uses(): df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 218f9a60ab9..3f7f1dce14e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -74,11 +74,11 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) -#if LV_USE_IMAGE + +#ifdef USE_IMAGE +#ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda. inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); } -#endif // LV_USE_IMAGE inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { ::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); @@ -93,7 +93,8 @@ inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) { inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) { ::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc()); } -#endif // USE_LVGL_IMAGE +#endif + #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img)); @@ -109,6 +110,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size()); } #endif // USE_LVGL_ANIMIMG +#endif // USE_IMAGE #ifdef USE_LVGL_METER int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index c1441526f95..59115055550 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -9,6 +9,7 @@ from .defines import ( CONF_THEME, LValidator, add_lv_use, + get_styles_used, get_theme_widget_map, literal, ) @@ -25,6 +26,7 @@ def has_style_props(config) -> bool: async def style_set(svar, style): for prop, validator in ALL_STYLES.items(): if (value := style.get(prop)) is not None: + get_styles_used().add(prop) if isinstance(validator, LValidator): value = await validator.process(value) if isinstance(value, list): From 48d17571c82d8a048d263595865d2cf15bce2225 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 15:50:58 -0700 Subject: [PATCH 2024/2056] [tests] Mock determine_cpp_unit_tests in clang_tidy_mode tests (#16456) --- tests/script/test_determine_jobs.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 3fd5eada940..202ae9030f9 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1518,6 +1518,7 @@ def test_clang_tidy_mode_full_scan( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, + mock_determine_cpp_unit_tests: Mock, mock_changed_files: Mock, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, @@ -1529,6 +1530,9 @@ def test_clang_tidy_mode_full_scan( mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False + # Without this mock, main() runs the real determine_cpp_unit_tests + # which loads the full component graph (~5s import of every component). + mock_determine_cpp_unit_tests.return_value = (False, []) # Mock changed_files to return no component files mock_changed_files.return_value = [] @@ -1584,6 +1588,7 @@ def test_clang_tidy_mode_targeted_scan( mock_should_run_clang_tidy: Mock, mock_should_run_clang_format: Mock, mock_should_run_python_linters: Mock, + mock_determine_cpp_unit_tests: Mock, mock_changed_files: Mock, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, @@ -1595,6 +1600,9 @@ def test_clang_tidy_mode_targeted_scan( mock_should_run_clang_tidy.return_value = True mock_should_run_clang_format.return_value = False mock_should_run_python_linters.return_value = False + # Without this mock, main() runs the real determine_cpp_unit_tests + # which loads the full component graph (~5s import of every component). + mock_determine_cpp_unit_tests.return_value = (False, []) # Create component names components = [f"comp{i}" for i in range(component_count)] @@ -2651,6 +2659,15 @@ def test_main_force_all_overrides_detection( return_value={"should_run": "false"}, ), patch.object(determine_jobs, "should_run_benchmarks", return_value=False), + # create_intelligent_batches scans every tests/components/<name>/*.yaml + # under --force-all (~2500 YAML loads, ~10s in CI). This test only + # asserts that main() routes to it and returns non-empty -- the + # batching logic itself has its own dedicated tests. + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["fake_batch"]], None), + ), ): determine_jobs.main() From fb0bfea1c8955d24667da80872845598778d4865 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 22:06:33 -0700 Subject: [PATCH 2025/2056] Bump aioesphomeapi from 45.0.1 to 45.0.2 (#16469) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1996de09289..d117e47d912 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.1 +aioesphomeapi==45.0.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 7c5d5f75dc378683b02e645f435153c3891a3226 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 22:16:52 -0700 Subject: [PATCH 2026/2056] [ci] Use larger app partition for esp32-s3-idf component test grouping (#16430) --- .../build_components_base.esp32-s3-idf.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml index ee209000e93..f3122f977ed 100644 --- a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -7,6 +7,9 @@ esp32: variant: ESP32S3 framework: type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv logger: level: VERY_VERBOSE From 01c0d3163e4f0eedd4efc29dbf8668d9b8ff4d2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 18:41:09 -0700 Subject: [PATCH 2027/2056] Bump aioesphomeapi from 45.0.2 to 45.0.3 (#16479) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d117e47d912..eddc403820d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.2 +aioesphomeapi==45.0.3 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 155232875a671e903e62565764784fbb50bcd79e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:08:19 -0700 Subject: [PATCH 2028/2056] Bump zeroconf from 0.148.0 to 0.149.3 (#16480) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eddc403820d..6eef8ac643e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 aioesphomeapi==45.0.3 -zeroconf==0.148.0 +zeroconf==0.149.3 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From 66907258603a2e96a71f85242bef096515b05705 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 17 May 2026 15:29:38 -0400 Subject: [PATCH 2029/2056] [espidf] Switch direct framework downloader to esphome-libs/esp-idf tarballs (#16484) --- esphome/espidf/framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 7ff373aba83..32bcf4fb3b4 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -69,7 +69,7 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str( ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str( os.environ.get( "ESPHOME_IDF_FRAMEWORK_MIRRORS", - "https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip", + "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz;https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz", ) ) From 42ad2a627281c4826f14312c18eecae0666fc066 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 May 2026 10:49:03 +1200 Subject: [PATCH 2030/2056] [espidf] Accept list input in _str_to_lst_of_str helper (#16485) --- esphome/espidf/framework.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 32bcf4fb3b4..8996ff1e02a 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -26,16 +26,18 @@ _LOGGER = logging.getLogger(__name__) _SCRIPTS_DIR = Path(__file__).parent -def _str_to_lst_of_str(a: str) -> list[str]: +def _str_to_lst_of_str(a: str | list[str]) -> list[str]: """ Convert a string to a list of string Args: - a: A string containing semicolon-separated values + a: A string containing semicolon-separated values, or an already-split list Returns: list of strings """ + if isinstance(a, list): + return a return list(f.strip() for f in a.split(";") if f.strip()) @@ -67,10 +69,11 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str( ) ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str( - os.environ.get( - "ESPHOME_IDF_FRAMEWORK_MIRRORS", - "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz;https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz", - ) + os.environ.get("ESPHOME_IDF_FRAMEWORK_MIRRORS") + or [ + "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz", + "https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz", + ] ) ESP_IDF_CONSTRAINTS_MIRRORS = _str_to_lst_of_str( From c863d589992079b43998471eb80ad783691fd290 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 13 May 2026 23:19:30 -0400 Subject: [PATCH 2031/2056] [espidf] Stop perpetual reconfigure loop on native ESP-IDF builds (#16415) --- esphome/components/esp32/__init__.py | 7 ++++++- esphome/espidf/toolchain.py | 30 ++++++++++++---------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1eb0bb2174f..0c24dbf7b90 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2509,7 +2509,12 @@ def _write_idf_component_yml(): stubs_dir = CORE.relative_build_path("component_stubs") stubs_dir.mkdir(exist_ok=True) - for component_name in components_to_stub: + # Sort so the dict insertion order (and thus the generated + # src/idf_component.yml) is deterministic across runs; otherwise + # the manifest content shuffles every build, write_file_if_changed + # always writes, and ninja keeps triggering CMake re-runs on + # otherwise-cached rebuilds. + for component_name in sorted(components_to_stub): # Create stub directory with minimal CMakeLists.txt stub_path = stubs_dir / _idf_component_stub_name(component_name) stub_path.mkdir(exist_ok=True) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 583f3409964..1245c643e11 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,13 +191,19 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if configuration inputs - are newer than the generated CMake/Ninja build artifacts. + Returns True if required build files are missing or if external + configuration inputs (IDF install, sdkconfig, CMake's own build/config + dir) are newer than CMakeCache.txt. We deliberately don't watch the + top-level/src ``CMakeLists.txt`` here -- those are written by + ``write_project`` via ``write_file_if_changed`` (so an mtime bump + means our content actually changed) and ninja already tracks them as + configure-time deps via ``build.ninja``. Including them in this check + causes a perpetual reconfigure loop: the two-pass write leaves + CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the + cache when only ``idf_build_set_property`` values change, so the + check would trip on every subsequent build. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") - - cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt") - cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" @@ -221,8 +227,6 @@ def has_outdated_files(): os.path.getmtime(f) > cmakecache_txt_mtime for f in [ _get_idf_path(), - cmakelists_txt_build_path, - cmakelists_txt_src_path, sdkconfig_internal_path, build_config_path, ] @@ -302,21 +306,13 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) - # The post-discovery rewrite leaves CMakeLists newer than - # CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a - # configure that only changes idf_build_set_property values - # (those aren't cache variables), so has_outdated_files() would - # return True on every subsequent build, perpetually retriggering - # the two-pass. Touch CMakeCache.txt now so its mtime stays past - # the rewritten CMakeLists. - cmakecache = CORE.relative_build_path("build/CMakeCache.txt") - if cmakecache.is_file(): - os.utime(cmakecache) if CORE.testing_mode: # Reconfigure again so cmake is up to date with the full # component list before the build's idf.py invocation runs -- # idf.py build would otherwise re-run cmake and regenerate # memory.ld, wiping the DRAM/IRAM patches applied below. + # Outside testing mode ninja's own configure-time dep on + # CMakeLists.txt handles the re-run as part of the build step. rc = run_reconfigure() if rc != 0: _LOGGER.error("Reconfigure with discovered components failed") From 84b5931299de172ba87dc9c5cfdf88ba7ae15773 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 00:02:22 -0400 Subject: [PATCH 2032/2056] [espidf] Trim has_outdated_files watch list; embed IDF version in sdkconfig (#16416) --- esphome/components/esp32/__init__.py | 8 ++++- esphome/espidf/toolchain.py | 45 +++++++++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c24dbf7b90..f1125498326 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2464,8 +2464,14 @@ def _write_sdkconfig(): ) want_opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + # Include the resolved framework version as a Kconfig comment so a + # version switch that happens to leave the option set unchanged still + # bumps this file's content -- which is what has_outdated_files() + # uses to decide whether to reconfigure. + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] contents = ( - "\n".join( + f"# ESPHOME_IDF_VERSION={framework_version}\n" + + "\n".join( f"{name}={_format_sdkconfig_val(value)}" for name, value in sorted(want_opts.items()) ) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 1245c643e11..e0bc5bb393a 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -191,23 +191,38 @@ def run_reconfigure() -> int: def has_outdated_files(): """Check if the build configuration is stale. - Returns True if required build files are missing or if external - configuration inputs (IDF install, sdkconfig, CMake's own build/config - dir) are newer than CMakeCache.txt. We deliberately don't watch the - top-level/src ``CMakeLists.txt`` here -- those are written by - ``write_project`` via ``write_file_if_changed`` (so an mtime bump - means our content actually changed) and ninja already tracks them as - configure-time deps via ``build.ninja``. Including them in this check - causes a perpetual reconfigure loop: the two-pass write leaves - CMakeLists newer than CMakeCache.txt, and CMake doesn't restamp the - cache when only ``idf_build_set_property`` values change, so the - check would trip on every subsequent build. + Returns True if required build files are missing or if ESPHome's + resolved build inputs are newer than CMakeCache.txt: + + - ``sdkconfig.<name>.esphomeinternal`` -- the canonical "what state + did ESPHome resolve the YAML to" snapshot. Any change in build + flags, enabled components, framework version, or target ends up + rewriting it (we embed a ``# ESPHOME_IDF_VERSION=`` comment line + for the version case where the option set would otherwise be + identical). + - ``src/idf_component.yml`` -- the project manifest. Managed + component additions/removals (e.g. via ``add_idf_component``) can + happen without any sdkconfig impact, and ``_write_idf_component_yml`` + already deletes ``dependencies.lock`` on a change but that signal + gets lost as soon as the lock is missing. + + We deliberately don't watch: + - The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and + ninja already tracks them as configure-time deps. Including them + causes a perpetual reconfigure loop because CMake doesn't restamp + ``CMakeCache.txt`` when only ``idf_build_set_property`` values + change between configures. + - ``$IDF_PATH`` and CMake's ``build/config/`` -- both have mtime + semantics that fire after the wrong configure (or not at all in + common cases like in-place IDF version replacement). The sdkconfig + and manifest hashes subsume the meaningful signal. """ cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt") build_config_path = CORE.relative_build_path("build/config") sdkconfig_internal_path = CORE.relative_build_path( f"sdkconfig.{CORE.name}.esphomeinternal" ) + idf_component_yml_path = CORE.relative_build_path("src/idf_component.yml") dependency_lock_path = CORE.relative_build_path("dependencies.lock") build_ninja_path = CORE.relative_build_path("build/build.ninja") @@ -225,12 +240,8 @@ def has_outdated_files(): cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) return any( os.path.getmtime(f) > cmakecache_txt_mtime - for f in [ - _get_idf_path(), - sdkconfig_internal_path, - build_config_path, - ] - if f and os.path.exists(f) + for f in [sdkconfig_internal_path, idf_component_yml_path] + if f.exists() ) From ab273a1f8fe4419bd67d9e2a2c05ed222a0beca7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 14 May 2026 15:07:38 -0500 Subject: [PATCH 2033/2056] [tinyusb] Reject `tinyusb:` configured without a USB class companion (#16413) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/components/tinyusb/__init__.py | 20 +++++++++++++++++++ .../components/tinyusb/tinyusb_component.cpp | 15 ++++++++++++++ tests/components/tinyusb/common.yaml | 5 +++++ 3 files changed, 40 insertions(+) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index df94ad75346..724f65721b7 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -1,3 +1,4 @@ +from esphome import final_validate as fv import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( @@ -20,6 +21,13 @@ CONF_USB_PRODUCT_STR = "usb_product_str" CONF_USB_SERIAL_STR = "usb_serial_str" CONF_USB_VENDOR_ID = "usb_vendor_id" +# Components that provide a USB device class (CDC, HID, MSC, ...) on top of +# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in +# esp_tinyusb's driver install (descriptors_set fails with no class and no +# user-provided full_speed_config), which trips the task watchdog before +# loop() ever runs. +_USB_CLASS_COMPONENTS = ("usb_cdc_acm",) + tinyusb_ns = cg.esphome_ns.namespace("tinyusb") TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) @@ -41,6 +49,18 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config): + full_config = fv.full_config.get() + if not any(name in full_config for name in _USB_CLASS_COMPONENTS): + raise cv.Invalid( + "The 'tinyusb' component requires at least one USB class component" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index 2ec696c3e43..3cefc0454a3 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -26,6 +26,21 @@ void TinyUSB::setup() { .string_count = SIZE, }; + // Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with + // ESP_ERR_INVALID_ARG when no configuration descriptor is provided and + // no class that has a built-in default (CDC/MSC/NCM) is compiled in. In + // that case the internal task exits without notifying us, and + // tinyusb_driver_install() blocks 5s on the notify-take -- long enough + // to trip the task watchdog. Bail early so the rest of the device can + // still boot. +#if !(CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0) + if (this->tusb_cfg_.descriptor.full_speed_config == nullptr) { + ESP_LOGE(TAG, "No USB class configured"); + this->mark_failed(); + return; + } +#endif + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); if (result != ESP_OK) { ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result)); diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml index cb3f48836ab..674e89dbe87 100644 --- a/tests/components/tinyusb/common.yaml +++ b/tests/components/tinyusb/common.yaml @@ -6,3 +6,8 @@ tinyusb: usb_product_str: ESPHomeTestProduct usb_serial_str: ESPHomeTestSerialNumber usb_vendor_id: 0x2345 + +# tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that. +usb_cdc_acm: + interfaces: + - id: tinyusb_test_cdc From fb659f9ac4e35022d87317a4bbfab8af9076cba0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski <kbx81x@gmail.com> Date: Thu, 14 May 2026 15:29:56 -0500 Subject: [PATCH 2034/2056] [tinyusb] Reject `logger.hardware_uart: USB_CDC` (#16417) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- esphome/components/tinyusb/__init__.py | 13 ++++++++++++- tests/components/tinyusb/test.esp32-s2-idf.yaml | 5 +++++ tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 724f65721b7..0e02ff87247 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_HARDWARE_UART, CONF_ID CODEOWNERS = ["@kbx81"] CONFLICTS_WITH = ["usb_host"] @@ -55,6 +55,17 @@ def _final_validate(config): raise cv.Invalid( "The 'tinyusb' component requires at least one USB class component" ) + # tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes + # the ROM console through that same peripheral, so the two cannot coexist. + # (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.) + logger_config = full_config.get("logger") + if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC": + raise cv.Invalid( + "'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' " + "because both share the USB OTG peripheral. Set " + "'logger.hardware_uart' to a hardware UART (e.g. UART0), or to " + "USB_SERIAL_JTAG on variants that support it (ESP32-S3, ESP32-P4)" + ) return config diff --git a/tests/components/tinyusb/test.esp32-s2-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml index dade44d145b..09b98ada401 100644 --- a/tests/components/tinyusb/test.esp32-s2-idf.yaml +++ b/tests/components/tinyusb/test.esp32-s2-idf.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 diff --git a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml index f159b38ff6f..ff757315097 100644 --- a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml +++ b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml @@ -1,5 +1,10 @@ <<: !include tinyusb_common.yaml +# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared +# USB OTG peripheral; route the logger to UART0 so the fixture builds. +logger: + hardware_uart: UART0 + usb_cdc_acm: interfaces: - id: usb_cdc_acm1 From dd1818661c29d03f83153e95ba3bbd5dd40c4796 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:39:16 -0400 Subject: [PATCH 2035/2056] [esp32] Sweep ESP-IDF toolchain warnings + bump deprecated mark_failed (#16432) --- esphome/components/esp32/__init__.py | 4 +++- esphome/components/hdc2080/hdc2080.cpp | 2 +- esphome/components/heatpumpir/climate.py | 1 - esphome/components/pulse_meter/pulse_meter_sensor.cpp | 4 ++-- esphome/components/sim800l/sim800l.cpp | 2 +- esphome/components/tuya/tuya.cpp | 2 ++ esphome/components/tx20/tx20.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- esphome/components/wiegand/wiegand.cpp | 4 ++-- 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f1125498326..e9b0f1fd0a6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1767,9 +1767,11 @@ async def to_code(config): else: cg.add_build_flag("-Wno-error=format") cg.add_build_flag("-Wno-error=maybe-uninitialized") - cg.add_build_flag("-Wno-error=missing-field-initializers") + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_build_flag("-Wno-error=reorder") cg.add_build_flag("-Wno-error=volatile") + # -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates + cg.add_build_flag("-Wno-missing-field-initializers") cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp index dcb207e099b..bf3a4bc79f1 100644 --- a/esphome/components/hdc2080/hdc2080.cpp +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -22,7 +22,7 @@ static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only void HDC2080Component::setup() { const uint8_t data = 0x00; // automatic measurement mode disabled, heater off if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index b7e04374807..aa3a08c2949 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,7 +125,6 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.41") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 3fe1c722ebb..d6959d1a964 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -150,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { edge_state.last_sent_edge_us_ = now; state.last_detected_edge_us_ = now; state.last_rising_edge_us_ = now; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // This ISR is bound to rising edges, so the pin is high @@ -173,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge pulse_state.latched_ = true; state.last_detected_edge_us_ = pulse_state.last_intr_; - state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) + state.count_ += 1; } // Due to order of operations this includes diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index b8e97b11211..13b9888e052 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -126,7 +126,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { break; } - // Else fall thru ... + [[fallthrough]]; } case STATE_CHECK_SMS: send_cmd_("AT+CMGL=\"ALL\""); diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b29905f9a0d..fd14844908f 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -684,8 +684,10 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType case 4: data.push_back(value >> 24); data.push_back(value >> 16); + [[fallthrough]]; case 2: data.push_back(value >> 8); + [[fallthrough]]; case 1: data.push_back(value >> 0); break; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 353cb31513a..3574bd7c2d5 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -135,7 +135,7 @@ void Tx20Component::decode_and_publish_() { } if (tx20_se == tx20_sb) { tx20_wind_direction = tx20_se; - if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { + if (tx20_wind_direction < 16) { wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; } ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); @@ -164,7 +164,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; return; } const uint32_t delay = now - arg->start_time; @@ -195,7 +195,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->buffer_index += 1; } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index e88c41c0cb3..fb8425f6cdc 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -135,7 +135,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon // Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1). static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT; - USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {} + USBUartChannel(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {} void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 198267204d9..91f249a6a5b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -611,7 +611,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c } // Helper to get request detail parameter -static JsonDetail get_request_detail(AsyncWebServerRequest *request) { +[[maybe_unused]] static JsonDetail get_request_detail(AsyncWebServerRequest *request) { return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index e1d3e4bf34f..c32acaf03ea 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -66,7 +66,7 @@ namespace { * - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK). * - HTTPD_SOCK_ERR_FAIL for other errors. */ -int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { +[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) { if (buf == nullptr) { return HTTPD_SOCK_ERR_INVALID; } diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index e5c29f8b115..df64cd48aa7 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { if (arg->d0.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value <<= 1; arg->last_bit_time = millis(); arg->done = false; @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { if (arg->d1.digital_read()) return; - arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile) + arg->count += 1; arg->value = (arg->value << 1) | 1; arg->last_bit_time = millis(); arg->done = false; From d5c6efb2fe2891cde35aeae11654b776947f3d8c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:40:40 -0400 Subject: [PATCH 2036/2056] [tests] Fix -Wformat= mismatches in test YAML lambdas/logger.log (#16435) --- esphome/components/logger/__init__.py | 14 +++++----- esphome/components/lvgl/helpers.py | 28 +++++++++---------- .../components/esp32_ble_tracker/common.yaml | 4 +-- tests/components/ld2412/common.yaml | 2 +- tests/components/lvgl/lvgl-package.yaml | 6 ++-- tests/components/modbus_server/common.yaml | 2 +- tests/components/mqtt/common.yaml | 2 +- tests/components/nextion/common.yaml | 2 +- .../remote_receiver/common-actions.yaml | 2 +- tests/components/script/common.yaml | 2 +- tests/components/udp/common.yaml | 2 +- tests/components/udp/test.host.yaml | 2 +- 12 files changed, 34 insertions(+), 34 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9d7dc8d92c1..c6c440564ac 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -506,13 +506,13 @@ async def _late_logger_init(config: ConfigType) -> None: def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" - ( # start of capture group 1 - % # literal "%" - (?:[-+0 #]{0,5}) # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + (?:[-+0 #]{0,5}) # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index baa618d472b..6f70a1e3bd9 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -9,13 +9,13 @@ CONF_IF_NAN = "if_nan" # noqa f_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - f # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + f # type ) """, flags=re.VERBOSE, @@ -23,13 +23,13 @@ f_regex = re.compile( # noqa c_regex = re.compile( r""" - ( # start of capture group 1 - % # literal "%" - [-+0 #]{0,5} # optional flags - (?:\d+|\*)? # width - (?:\.(?:\d+|\*))? # precision - (?:h|l|ll|w|I|I32|I64)? # size - [cCdiouxXeEfgGaAnpsSZ] # type + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size + [cCdiouxXeEfgGaAnpsSZ] # type ) """, flags=re.VERBOSE, diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml index 018bbb42b3b..564cf1f6ea6 100644 --- a/tests/components/esp32_ble_tracker/common.yaml +++ b/tests/components/esp32_ble_tracker/common.yaml @@ -29,12 +29,12 @@ esp32_ble_tracker: - service_uuid: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of service data is %i", x.size()); + ESP_LOGD("main", "Length of service data is %zu", x.size()); on_ble_manufacturer_data_advertise: - manufacturer_id: ABCD then: - lambda: !lambda |- - ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); + ESP_LOGD("main", "Length of manufacturer data is %zu", x.size()); on_scan_end: - then: - lambda: |- diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml index c5bda688dc9..7a86b6fbdac 100644 --- a/tests/components/ld2412/common.yaml +++ b/tests/components/ld2412/common.yaml @@ -123,7 +123,7 @@ select: - lambda: |- id(uart_bus).flush(); uint32_t new_baud_rate = stoi(x); - ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate); + ESP_LOGD("change_baud_rate", "Changing baud rate from %" PRIu32 " to %" PRIu32, id(uart_bus).get_baud_rate(), new_baud_rate); if (id(uart_bus).get_baud_rate() != new_baud_rate) { id(uart_bus).set_baud_rate(new_baud_rate); #if defined(USE_ESP8266) || defined(USE_ESP32) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 53984bb006d..0f4b9612979 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -660,13 +660,13 @@ lvgl: on_release: logger.log: format: Button released at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_long_press_repeat: logger.log: Button clicked on_pressing: logger.log: format: Button pressing at %d/%d - args: [point.x, point.y] + args: ['(int) point.x', '(int) point.y'] on_press_lost: logger.log: Button press lost on_single_click: @@ -944,7 +944,7 @@ lvgl: on_release: logger.log: format: Slider released at %d/%d with value %.0f - args: [point.x, point.y, x] + args: ['(int) point.x', '(int) point.y', x] - button: styles: spin_button id: spin_up diff --git a/tests/components/modbus_server/common.yaml b/tests/components/modbus_server/common.yaml index 3522c9248c9..2e4a81a1aa5 100644 --- a/tests/components/modbus_server/common.yaml +++ b/tests/components/modbus_server/common.yaml @@ -21,7 +21,7 @@ modbus_server: read_lambda: |- return 31; write_lambda: |- - printf("address=%d, value=%d", x); + printf("address=%d, value=%" PRId32 "\n", (int) address, x); return true; - id: modbus_server4 modbus_id: mod_bus2 diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 8c58e9b0806..6af2ce3939a 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -64,7 +64,7 @@ mqtt: topic: some/topic payload: Good-bye - lambda: |- - ESP_LOGD("MQTT", "Disconnect reason %d", reason); + ESP_LOGD("MQTT", "Disconnect reason %d", (int) reason); publish_nan_as_none: false binary_sensor: diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 0616b9a41a8..fba6a22b97f 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -299,7 +299,7 @@ display: - lambda: |- // key: StringRef, value: int32_t if (key == "temperature_raw") { - ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value); + ESP_LOGD("nextion.custom", "%s=%" PRId32, key.c_str(), value); } on_custom_binary_sensor: then: diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index 30b99eeb700..26a02d4dabc 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -12,7 +12,7 @@ on_brennenstuhl: then: - logger.log: format: "on_brennenstuhl: %u" - args: ["x.code"] + args: ["(unsigned) x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/script/common.yaml b/tests/components/script/common.yaml index c1dc68513fe..f4818e22962 100644 --- a/tests/components/script/common.yaml +++ b/tests/components/script/common.yaml @@ -49,7 +49,7 @@ script: then: - lambda: |- ESP_LOGD("main", "ints=%d floats=%f bools=%d strings=%s", - ints[0], floats[0], bools[0], strings[0].c_str()); + ints[0], floats[0], (int) bools[0], strings[0].c_str()); - id: my_script_with_params parameters: prefix: string diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 3466e8d2ee0..a40ca455cbc 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -11,7 +11,7 @@ udp: - "10.0.0.255" on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml index 84e78894e51..825d86c19e1 100644 --- a/tests/components/udp/test.host.yaml +++ b/tests/components/udp/test.host.yaml @@ -4,7 +4,7 @@ udp: addresses: ["239.0.60.53"] on_receive: - logger.log: - format: "Received %d bytes" + format: "Received %zu bytes" args: [data.size()] - udp.write: id: my_udp From da8286f5542a9a8a80bfd6e4590707a78b30cd04 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:51:59 -0400 Subject: [PATCH 2037/2056] [docker] Install libusb-1.0 so ESP-IDF tools can validate openocd (#16424) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docker/Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 540d28be7f1..25de9472b63 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,12 +13,16 @@ RUN git config --system --add safe.directory "*" \ && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation -# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) +# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager). +# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can +# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without +# it idf_tools.py rejects the openocd install with exit 127 and aborts +# the whole framework setup. RUN if command -v apk > /dev/null; then \ - apk add --no-cache build-base; \ + apk add --no-cache build-base libusb; \ else \ apt-get update \ - && apt-get install -y --no-install-recommends build-essential \ + && apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \ && rm -rf /var/lib/apt/lists/*; \ fi From 3831aa809f3e5fd425165274d0c2165b58546d8d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 18:53:42 -0400 Subject: [PATCH 2038/2056] [multiple] Fix -Wformat= mismatches in component .cpp sources (#16433) --- esphome/components/bme680_bsec/bme680_bsec.cpp | 4 ++-- .../esp32_hosted/update/esp32_hosted_update.cpp | 6 +++--- esphome/components/esphome/ota/ota_esphome.cpp | 14 +++++++------- esphome/components/fastled_base/fastled_light.cpp | 2 +- esphome/components/inkplate/inkplate.cpp | 2 +- esphome/components/midea/air_conditioner.cpp | 4 ++-- esphome/components/ota/ota_partitions_esp_idf.cpp | 4 ++-- .../components/remote_receiver/remote_receiver.cpp | 8 ++++---- esphome/components/sendspin/sendspin_hub.cpp | 4 ++-- .../total_daily_energy/total_daily_energy.cpp | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index b7f8c0da77b..823f32c446a 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() { " IAQ Mode: %s\n" " Supply Voltage: %sV\n" " Sample Rate: %s\n" - " State Save Interval: %ims", + " State Save Interval: %" PRIu32 "ms", this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile", this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_); @@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe } void BME680BSECComponent::delay_ms(uint32_t period) { - ESP_LOGV(TAG, "Delaying for %ums", period); + ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period); delay(period); } diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index af35d328881..7f3ba778951 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -92,7 +92,7 @@ void Esp32HostedUpdate::setup() { if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { // 16 bytes: "255.255.255" (11 chars) + null + safety margin char buf[16]; - snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1); this->update_info_.current_version = buf; } else { this->update_info_.current_version = "unknown"; @@ -120,8 +120,8 @@ void Esp32HostedUpdate::setup() { this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { - ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, - ESP_APP_DESC_MAGIC_WORD); + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", + app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f1857ed6642..fb0cc2e56d3 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -108,8 +108,8 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Partition access allowed\n" " Running app:\n" - " Partition address: 0x%X\n" - " Used size: %zu bytes (0x%X)", + " Partition address: 0x%" PRIX32 "\n" + " Used size: %zu bytes (0x%zX)", this->running_app_offset_, this->running_app_size_, this->running_app_size_); #ifdef USE_ESP32 @@ -378,7 +378,7 @@ void ESPHomeOTAComponent::handle_data_() { } ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) | (static_cast<size_t>(buf[2]) << 8) | buf[3]; - ESP_LOGV(TAG, "Size is %u bytes", ota_size); + ESP_LOGV(TAG, "Size is %zu bytes", ota_size); #ifndef USE_OTA_PARTITIONS if (ota_type != ota::OTA_TYPE_UPDATE_APP) { @@ -749,7 +749,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { this->auth_buf_[0] = this->auth_type_; hasher.get_hex(buf); - ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf); + ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf); } // Try to write auth_type + nonce @@ -809,13 +809,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() { hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) hasher.calculate(); - ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce); + ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator) hasher.get_hex(computed_hash); - ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash); + ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash); #endif - ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response); + ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response); // Compare response bool matches = hasher.equals_hex(response); diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index 8d1dd49dad3..0fa69a23b4d 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -19,7 +19,7 @@ void FastLEDLightOutput::dump_config() { ESP_LOGCONFIG(TAG, "FastLED light:\n" " Num LEDs: %u\n" - " Max refresh rate: %u", + " Max refresh rate: %" PRIu32, this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 39110ca83b9..2e837fb614e 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -319,7 +319,7 @@ void Inkplate::fill(Color color) { memset(this->partial_buffer_, fill, this->get_buffer_length_()); } - ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display() { diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 594f7fa661b..7603dd52542 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -130,8 +130,8 @@ ClimateTraits AirConditioner::traits() { void AirConditioner::dump_config() { ESP_LOGCONFIG(Constants::TAG, "MideaDongle:\n" - " [x] Period: %dms\n" - " [x] Response timeout: %dms\n" + " [x] Period: %" PRIu32 "ms\n" + " [x] Response timeout: %" PRIu32 "ms\n" " [x] Request attempts: %d", this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts()); #ifdef USE_REMOTE_TRANSMITTER diff --git a/esphome/components/ota/ota_partitions_esp_idf.cpp b/esphome/components/ota/ota_partitions_esp_idf.cpp index f91e88bde0a..a7fc7093135 100644 --- a/esphome/components/ota/ota_partitions_esp_idf.cpp +++ b/esphome/components/ota/ota_partitions_esp_idf.cpp @@ -210,7 +210,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address, + ESP_LOGD(TAG, "Copying running app from 0x%" PRIX32 " to 0x%" PRIX32 " (size: 0x%zX)", running_app_part->address, plan.copy_dest_part->address, running_app_size); err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size); if (err != ESP_OK) { @@ -261,7 +261,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { ESP_LOGE(TAG, "Selected app partition not found after partition table update"); return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE; } - ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address); + ESP_LOGD(TAG, "Setting next boot partition to 0x%" PRIX32, new_boot_partition->address); err = esp_ota_set_boot_partition(new_boot_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err); diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index d59ee636955..222dae8f7f8 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -78,10 +78,10 @@ void RemoteReceiverComponent::setup() { void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:\n" - " Buffer Size: %u\n" - " Tolerance: %u%s\n" - " Filter out pulses shorter than: %u us\n" - " Signal is done after %u us of no changes", + " Buffer Size: %" PRIu32 "\n" + " Tolerance: %" PRIu32 "%s\n" + " Filter out pulses shorter than: %" PRIu32 " us\n" + " Signal is done after %" PRIu32 " us of no changes", this->buffer_size_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 04426b8b1da..57709306cd0 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -153,7 +153,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { LastPlayedServerPref pref{.server_id_hash = hash}; bool ok = this->last_played_server_pref_.save(&pref); if (ok) { - ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash); + ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash); } else { ESP_LOGW(TAG, "Failed to persist last played server hash"); } @@ -164,7 +164,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) { std::optional<uint32_t> SendspinHub::load_last_server_hash() { LastPlayedServerPref pref{}; if (this->last_played_server_pref_.load(&pref)) { - ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash); + ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash); return pref.server_id_hash; } return std::nullopt; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 161c712cc16..7c9dbb604f7 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -72,7 +72,7 @@ void TotalDailyEnergy::schedule_midnight_reset_() { timeout_seconds = seconds_until_midnight + 1; } - ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + ESP_LOGD(TAG, "Scheduling midnight check in %" PRIu32 "s", timeout_seconds); this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, [this]() { this->schedule_midnight_reset_(); }); } From ecac6b64ec87b1e9d22a2181e724e1cb93f2b15e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:41:36 -0400 Subject: [PATCH 2039/2056] [espidf] Gate esp_idf_size --ng on IDF version (#16441) --- esphome/build_gen/espidf.py | 10 ++++++++-- tests/unit_tests/build_gen/test_espidf.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 5ad2072c5b3..96f84ebbd14 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -3,7 +3,8 @@ import json from pathlib import Path -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, idf_version +import esphome.config_validation as cv from esphome.core import CORE from esphome.helpers import mkdir_p, write_file_if_changed from esphome.writer import update_storage_json @@ -61,6 +62,11 @@ def get_project_cmakelists(minimal: bool = False) -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and + # removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get + # --format=raw because the legacy mode doesn't support it. + size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else "" + # Project-wide compile options: -D defines and -W warning flags (skip # -Wl, linker flags — those go on the src component via # target_link_options below). Emitted via idf_build_set_property so the @@ -146,7 +152,7 @@ project({CORE.name}) # Emit raw JSON size data for ESPHome to read post-build. add_custom_command( TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD - COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw + COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json ${{CMAKE_PROJECT_NAME}}.map WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 36f04423557..540dd067318 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -11,10 +11,12 @@ import pytest from esphome.components.esp32 import ( KEY_COMPONENTS, KEY_ESP32, + KEY_IDF_VERSION, KEY_PATH, KEY_REF, KEY_REPO, ) +import esphome.config_validation as cv from esphome.const import KEY_CORE from esphome.core import CORE @@ -24,7 +26,10 @@ def _reset_core(tmp_path: Path) -> None: """Give each test its own CORE.build_path and a clean esp32 data slot.""" CORE.build_path = str(tmp_path) CORE.data.setdefault(KEY_CORE, {}) - CORE.data[KEY_ESP32] = {KEY_COMPONENTS: {}} + CORE.data[KEY_ESP32] = { + KEY_COMPONENTS: {}, + KEY_IDF_VERSION: cv.Version(5, 5, 4), + } def _write_project_description(tmp_path: Path, components: dict[str, str]) -> None: From c037058c199ff660e828474e2b284cca787b5f9b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 14 May 2026 22:51:14 -0400 Subject: [PATCH 2040/2056] [esp32_hosted] Bump esp_hosted to 2.12.7 (#16440) --- esphome/components/esp32_hosted/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index eca7c24b102..71d1fd3ac1d 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -249,7 +249,7 @@ async def to_code(config): esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1") esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 814a4031c12..49c4cdbb2e2 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -36,7 +36,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.12.6 + version: 2.12.7 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: From 4f895425cac87e79e7b0396d41994d08181fd438 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 14 May 2026 23:31:11 -0400 Subject: [PATCH 2041/2056] [audio] Bump microMP3 to v0.2.1 (#16429) --- esphome/components/audio/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 44371e87ab2..13b379ba3ad 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -395,7 +395,7 @@ async def to_code(config): ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") - add_idf_component(name="esphome/micro-mp3", ref="0.2.0") + add_idf_component(name="esphome/micro-mp3", ref="0.2.1") _emit_memory_pair( data.mp3.buffer_memory, "CONFIG_MP3_DECODER_PREFER_PSRAM", diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 49c4cdbb2e2..35c55cbb4de 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -10,7 +10,7 @@ dependencies: esphome/micro-flac: version: 0.2.0 esphome/micro-mp3: - version: 0.2.0 + version: 0.2.1 esphome/micro-opus: version: 0.4.1 esphome/micro-wav: From 25dbef83de8d4168133dbba41cf4d76cc79c6f59 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org> Date: Thu, 14 May 2026 23:33:36 -0400 Subject: [PATCH 2042/2056] [sound_level] Use RingBufferAudioSource (#16436) --- .../components/sound_level/sound_level.cpp | 58 ++++++++++--------- esphome/components/sound_level/sound_level.h | 9 +-- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index fb8bfd30856..a93e3963674 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -11,7 +11,7 @@ namespace esphome::sound_level { static const char *const TAG = "sound_level"; -static const uint32_t AUDIO_BUFFER_DURATION_MS = 30; +static const uint32_t MAX_FILL_DURATION_MS = 30; static const uint32_t RING_BUFFER_DURATION_MS = 120; // Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2 @@ -30,8 +30,7 @@ void SoundLevelComponent::dump_config() { void SoundLevelComponent::setup() { this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) { std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); - if (this->ring_buffer_.use_count() == 2) { - // ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write + if (temp_ring_buffer != nullptr) { temp_ring_buffer->write((void *) data.data(), data.size()); } }); @@ -81,10 +80,11 @@ void SoundLevelComponent::loop() { return; } - // Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop - this->audio_buffer_->transfer_data_from_source(0); + // Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop. + // pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact). + this->audio_source_->fill(0, false); - if (this->audio_buffer_->available() == 0) { + if (this->audio_source_->available() == 0) { // No new audio available for processing return; } @@ -92,11 +92,11 @@ void SoundLevelComponent::loop() { const uint32_t samples_in_window = this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_); const uint32_t samples_available_to_process = - this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available()); + this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_source_->available()); const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process); // MicrophoneSource always provides int16 samples due to Python codegen settings - const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_buffer_->get_buffer_start()); + const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_source_->data()); // Process all the new audio samples for (uint32_t i = 0; i < samples_to_process; ++i) { @@ -115,9 +115,8 @@ void SoundLevelComponent::loop() { ++this->sample_count_; } - // Remove the processed samples from ``audio_buffer_`` - this->audio_buffer_->decrease_buffer_length( - this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); + // Remove the processed samples from ``audio_source_`` + this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process)); if (this->sample_count_ == samples_in_window) { // Processed enough samples for the measurement window, compute and publish the sensor values @@ -158,36 +157,39 @@ void SoundLevelComponent::stop() { } bool SoundLevelComponent::start_() { - if (this->audio_buffer_ != nullptr) { + if (this->audio_source_ != nullptr) { return true; } - // Allocate a transfer buffer - this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); - if (this->audio_buffer_ == nullptr) { - this->status_momentary_error("transfer_buffer", 15000); + const auto &stream_info = this->microphone_source_->get_audio_stream_info(); + const size_t bytes_per_frame = stream_info.frames_to_bytes(1); + + // Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple + // of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices. + this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation + const size_t ring_buffer_size = + (stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame; + std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer == nullptr) { + this->status_momentary_error("ring_buffer", 15000); return false; } - // Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it - this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation - std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create( - this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); - if (temp_ring_buffer.use_count() == 0) { - this->status_momentary_error("ring_buffer", 15000); - this->stop_(); + // Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads + // ensure multi-channel frames are never split across the ring buffer's wrap boundary. + this->audio_source_ = audio::RingBufferAudioSource::create( + temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame)); + if (this->audio_source_ == nullptr) { + this->status_momentary_error("audio_source", 15000); return false; - } else { - this->ring_buffer_ = temp_ring_buffer; - this->audio_buffer_->set_source(temp_ring_buffer); } + this->ring_buffer_ = temp_ring_buffer; this->status_clear_error(); return true; } -void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); } +void SoundLevelComponent::stop_() { this->audio_source_.reset(); } } // namespace esphome::sound_level diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 4f0081a5104..aabea62ca42 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -36,11 +36,12 @@ class SoundLevelComponent : public Component { void stop(); protected: - /// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which - /// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful. + /// @brief Internal start command that, if necessary, allocates a ring buffer and a zero-copy + /// ``RingBufferAudioSource`` that reads directly from it. ``ring_buffer_`` weakly references the + /// ring buffer owned by ``audio_source_``. Returns true if allocations were successful. bool start_(); - /// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer) + /// @brief Internal stop command that deallocates ``audio_source_`` (which releases its ring buffer) void stop_(); microphone::MicrophoneSource *microphone_source_{nullptr}; @@ -48,7 +49,7 @@ class SoundLevelComponent : public Component { sensor::Sensor *peak_sensor_{nullptr}; sensor::Sensor *rms_sensor_{nullptr}; - std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer_; + std::unique_ptr<audio::RingBufferAudioSource> audio_source_; std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_; int32_t squared_peak_{0}; From 50495c7085d618e28fa7f75531eb739673fe5d98 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:20:15 -0700 Subject: [PATCH 2043/2056] [wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444) --- esphome/__main__.py | 8 +++ esphome/components/wifi/__init__.py | 52 +++++++++++++++- esphome/const.py | 9 +++ tests/unit_tests/components/test_wifi.py | 78 +++++++++++++++++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index d733534a5c0..16a05ad552b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -50,6 +50,7 @@ from esphome.const import ( CONF_TOPIC, CONF_USERNAME, CONF_WEB_SERVER, + CONF_WIFI, ENV_NOGITIGNORE, KEY_CORE, KEY_TARGET_PLATFORM, @@ -733,6 +734,13 @@ def write_cpp_file() -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int: + # Keep this gate here, NOT in config validation: device-builder needs + # `esphome config` to keep succeeding with placeholders so onboarding can run. + if CONF_WIFI in config: + from esphome.components.wifi import check_placeholder_credentials + + check_placeholder_credentials(config) + # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # If you change this format, update the regex in that script as well _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index bad57fc4810..f9cb391442c 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,10 +54,18 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, Platform, PlatformFramework, ) -from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + EsphomeError, + HexInt, + coroutine_with_priority, +) import esphome.final_validate as fv from esphome.types import ConfigType @@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO}, } ) + + +def _placeholder_wifi_credentials(config: ConfigType) -> list[str]: + """Return human-readable locations where the dashboard's placeholder wifi + values still appear. Empty list means no placeholders were found. + """ + placeholders: list[str] = [] + wifi_conf = config.get(CONF_WIFI) + if not wifi_conf: + return placeholders + + for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])): + ssid = network.get(CONF_SSID) + if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append(f"wifi.networks[{idx}].ssid") + + ap_conf = wifi_conf.get(CONF_AP) + if ap_conf: + ap_ssid = ap_conf.get(CONF_SSID) + if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID: + placeholders.append("wifi.ap.ssid") + + return placeholders + + +def check_placeholder_credentials(config: ConfigType) -> None: + """Raise EsphomeError if any wifi credential is the dashboard placeholder. + + Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, + or any path reached by `esphome config`; device-builder relies on + validation passing with the placeholders still in place. + """ + locations = _placeholder_wifi_credentials(config) + if not locations: + return + formatted = ", ".join(locations) + raise EsphomeError( + f"wifi configuration still contains the dashboard placeholder value " + f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. " + f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') " + f"with your real wifi credentials before flashing." + ) diff --git a/esphome/const.py b/esphome/const.py index 91bc52708c3..1819502201d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1415,3 +1415,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" # The corresponding constant exists in c++ # when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds SCHEDULER_DONT_RUN = 4294967295 + +# Sentinel values written by the esphome-device-builder dashboard into +# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password +# references resolve cleanly through validation before the user has finished +# the onboarding wizard. Compilation refuses if these reach the binary so that +# a user who dismisses onboarding can't accidentally flash a device that will +# never associate with their wifi. +PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK" +PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105 diff --git a/tests/unit_tests/components/test_wifi.py b/tests/unit_tests/components/test_wifi.py index 71a14d78177..9598c1bdd86 100644 --- a/tests/unit_tests/components/test_wifi.py +++ b/tests/unit_tests/components/test_wifi.py @@ -3,8 +3,20 @@ import pytest from esphome.components.esp32 import const -from esphome.components.wifi import has_native_wifi, variant_has_wifi -from esphome.const import Platform +from esphome.components.wifi import ( + check_placeholder_credentials, + has_native_wifi, + variant_has_wifi, +) +from esphome.const import ( + CONF_AP, + CONF_NETWORKS, + CONF_SSID, + CONF_WIFI, + PLACEHOLDER_WIFI_SSID, + Platform, +) +from esphome.core import EsphomeError, Lambda @pytest.mark.parametrize( @@ -123,3 +135,65 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None: def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None: """RP2040 without a board id falls open to True (custom-board default).""" assert has_native_wifi(platform=Platform.RP2040) is True + + +def _wifi_config( + *, + networks: list[dict] | None = None, + ap: dict | None = None, +) -> dict: + """Build a minimal config dict matching the post-validation shape.""" + wifi: dict = {} + if networks is not None: + wifi[CONF_NETWORKS] = networks + if ap is not None: + wifi[CONF_AP] = ap + return {CONF_WIFI: wifi} + + +def test_check_placeholder_credentials_passes_with_real_ssid() -> None: + """A real SSID compiles without complaint.""" + config = _wifi_config(networks=[{CONF_SSID: "home_network"}]) + assert check_placeholder_credentials(config) is None + + +def test_check_placeholder_credentials_refuses_placeholder_ssid() -> None: + """The placeholder SSID is rejected with an actionable message.""" + config = _wifi_config(networks=[{CONF_SSID: PLACEHOLDER_WIFI_SSID}]) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + message = str(exc_info.value) + assert "wifi.networks[0].ssid" in message + assert "secrets.yaml" in message + + +def test_check_placeholder_credentials_refuses_placeholder_in_second_network() -> None: + """Index reporting picks the placeholder out of a mixed network list.""" + config = _wifi_config( + networks=[ + {CONF_SSID: "home_network"}, + {CONF_SSID: PLACEHOLDER_WIFI_SSID}, + ], + ) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.networks[1].ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_refuses_placeholder_ap_ssid() -> None: + """An AP using the placeholder broadcast name is also refused.""" + config = _wifi_config(ap={CONF_SSID: PLACEHOLDER_WIFI_SSID}) + with pytest.raises(EsphomeError) as exc_info: + check_placeholder_credentials(config) + assert "wifi.ap.ssid" in str(exc_info.value) + + +def test_check_placeholder_credentials_no_wifi_passes() -> None: + """Ethernet-only / wifi-less configs skip the check entirely.""" + assert check_placeholder_credentials({}) is None + + +def test_check_placeholder_credentials_skips_template_ssid() -> None: + """A templated (Lambda) SSID is not a string and is skipped.""" + config = _wifi_config(networks=[{CONF_SSID: Lambda('return "x";')}]) + assert check_placeholder_credentials(config) is None From 5ec0879a1049bfccc1179efc05da072327ed5de9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 10:29:15 -0700 Subject: [PATCH 2044/2056] [core] Fix KeyError: 'esp32' on upload when validated-config cache is used (#16457) --- esphome/storage_json.py | 13 +++++- tests/unit_tests/test_compiled_config.py | 56 ++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 7d26b22f96c..e4818270804 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -273,10 +273,21 @@ class StorageJSON: """ CORE.name = self.name CORE.build_path = self.build_path + target_platform = self.core_platform or self.target_platform.lower() CORE.data[KEY_CORE] = { - KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(), + KEY_TARGET_PLATFORM: target_platform, KEY_TARGET_FRAMEWORK: self.framework, } + # The compile pipeline populates CORE.data[KEY_ESP32] when esp32's + # validator runs; on the cache fast path that validator is skipped, + # so populate the variant upload_using_esptool reads via + # esp32.get_esp32_variant(). target_platform on disk is the variant + # (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32"). + if target_platform == const.PLATFORM_ESP32: + from esphome.components.esp32.const import KEY_ESP32 + from esphome.const import KEY_VARIANT + + CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform} def __eq__(self, o) -> bool: return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index 34e811b97bd..8c9cfa8101b 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -22,6 +22,7 @@ from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + KEY_VARIANT, ) from esphome.core import CORE @@ -47,7 +48,12 @@ wifi: """ -def _write_storage(storage_path: Path) -> None: +def _write_storage( + storage_path: Path, + *, + esp_platform: str = "ESP32", + core_platform: str | None = "esp32", +) -> None: """Write a vanilla StorageJSON sidecar for the cache tests.""" storage_path.parent.mkdir(parents=True, exist_ok=True) data = { @@ -59,14 +65,14 @@ def _write_storage(storage_path: Path) -> None: "src_version": 1, "address": "192.168.1.42", "web_port": None, - "esp_platform": "ESP32", + "esp_platform": esp_platform, "build_path": "/build/lite_test", "firmware_bin_path": "/build/lite_test/firmware.bin", "loaded_integrations": ["api", "logger", "ota", "wifi"], "loaded_platforms": [], "no_mdns": False, "framework": "arduino", - "core_platform": "esp32", + "core_platform": core_platform, } storage_path.write_text(json.dumps(data)) @@ -123,6 +129,50 @@ def test_load_compiled_config_happy_path(fresh_cache_files: Path) -> None: assert CORE.build_path == Path("/build/lite_test") assert CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] == "esp32" assert CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] == "arduino" + # upload_using_esptool reads get_esp32_variant() off CORE.data[KEY_ESP32]. + from esphome.components.esp32.const import KEY_ESP32 + + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32" + + +def test_load_compiled_config_populates_esp32_variant(tmp_path: Path) -> None: + """ESP32 variants survive the cache fast path so esptool gets the right --chip.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage(storage_dir / "lite_test.yaml.json", esp_platform="ESP32S3") + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert CORE.data[KEY_ESP32][KEY_VARIANT] == "ESP32S3" + + +def test_load_compiled_config_skips_esp32_block_for_other_platforms( + tmp_path: Path, +) -> None: + """Non-esp32 targets shouldn't fabricate an esp32 data block.""" + from esphome.components.esp32.const import KEY_ESP32 + + yaml_path = tmp_path / "lite_test.yaml" + yaml_path.write_text("esphome:\n name: lite_test\n") + CORE.config_path = yaml_path + + storage_dir = tmp_path / ".esphome" / "storage" + _write_storage( + storage_dir / "lite_test.yaml.json", + esp_platform="ESP8266", + core_platform="esp8266", + ) + cache = _write_cache(storage_dir / "lite_test.yaml.validated.yaml") + _set_cache_mtime(cache, yaml_path, offset=5) + + assert load_compiled_config(yaml_path) is not None + assert KEY_ESP32 not in CORE.data @pytest.mark.parametrize( From c6a74222f1c198cd2032fc7f6a89f6eddbf28477 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 15 May 2026 13:32:51 -0400 Subject: [PATCH 2045/2056] [esp32_hosted][fingerprint_grow] Fix two remaining ESP32 toolchain warnings (#16442) --- esphome/components/esp32_hosted/update/esp32_hosted_update.cpp | 2 +- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 7f3ba778951..70fa41b3124 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -121,7 +121,7 @@ void Esp32HostedUpdate::setup() { } } else { ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")", - app_desc->magic_word, ESP_APP_DESC_MAGIC_WORD); + app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD)); this->state_ = update::UPDATE_STATE_NO_UPDATE; } } else { diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 3f57789034e..b38d42191bc 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { break; case ENROLL_MISMATCH: ESP_LOGE(TAG, "Scans do not match"); + [[fallthrough]]; default: return this->data_[0]; } From 26907f17f5833f08a8b9b7e3baf0f9900f22aafb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 14:42:51 -0700 Subject: [PATCH 2046/2056] Bump aioesphomeapi from 45.0.0 to 45.0.1 (#16467) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6291b5cd414..ae50c4046b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.0 +aioesphomeapi==45.0.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 6a8f24b951ea396e60b6ef1f2ec1acce5be2f5b6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:30:04 +1000 Subject: [PATCH 2047/2056] [ft5x06] Fix setting calibration values (#16446) --- .../ft5x06/touchscreen/ft5x06_touchscreen.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp index 835dc4aac03..24d3529fb4c 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -15,6 +15,16 @@ void FT5x06Touchscreen::setup() { this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); } + // reading the chip registers to get max x/y does not seem to work. + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + } + // wait 200ms after reset. this->set_timeout(200, [this] { this->continue_setup_(); }); } @@ -39,15 +49,6 @@ void FT5x06Touchscreen::continue_setup_() { this->mark_failed(); return; } - // reading the chip registers to get max x/y does not seem to work. - if (this->display_ != nullptr) { - if (this->x_raw_max_ == this->x_raw_min_) { - this->x_raw_max_ = this->display_->get_native_width(); - } - if (this->y_raw_max_ == this->y_raw_min_) { - this->y_raw_max_ = this->display_->get_native_height(); - } - } } void FT5x06Touchscreen::update_touches() { @@ -71,7 +72,7 @@ void FT5x06Touchscreen::update_touches() { uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); - ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); if (status == 0 || status == 2) { this->add_raw_touch_position_(id, x, y); } From da237b5070cad2c0f2c430d7f54e77cf6bf8ca6b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 16 May 2026 08:36:53 +1000 Subject: [PATCH 2048/2056] [lvgl] Fix image define (#16468) --- esphome/components/lvgl/__init__.py | 5 +++++ esphome/components/lvgl/lvgl_esphome.h | 10 ++++++---- esphome/components/lvgl/styles.py | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 91b101cd258..4277c14dd7c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -55,6 +55,7 @@ from .automation import layers_to_code, lvgl_update from .defines import ( CONF_ALIGN_TO_LAMBDA_ID, LOGGER, + add_lv_use, get_focused_widgets, get_lv_images_used, get_refreshed_widgets, @@ -71,6 +72,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( + BASE_PROPS, DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, STYLE_REMAP, @@ -100,6 +102,7 @@ from .widgets import ( get_screen_active, set_obj_properties, ) +from .widgets.img import CONF_IMAGE # Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code @@ -433,6 +436,8 @@ async def to_code(configs): # This must be done after all widgets are created styles_used = df.get_styles_used() + if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used): + add_lv_use(CONF_IMAGE) for use in df.get_lv_uses(): df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 218f9a60ab9..3f7f1dce14e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -74,11 +74,11 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) -#if LV_USE_IMAGE + +#ifdef USE_IMAGE +#ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda. inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); } -#endif // LV_USE_IMAGE inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { ::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); @@ -93,7 +93,8 @@ inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) { inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) { ::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc()); } -#endif // USE_LVGL_IMAGE +#endif + #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) { auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img)); @@ -109,6 +110,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size()); } #endif // USE_LVGL_ANIMIMG +#endif // USE_IMAGE #ifdef USE_LVGL_METER int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value); diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index c1441526f95..59115055550 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -9,6 +9,7 @@ from .defines import ( CONF_THEME, LValidator, add_lv_use, + get_styles_used, get_theme_widget_map, literal, ) @@ -25,6 +26,7 @@ def has_style_props(config) -> bool: async def style_set(svar, style): for prop, validator in ALL_STYLES.items(): if (value := style.get(prop)) is not None: + get_styles_used().add(prop) if isinstance(validator, LValidator): value = await validator.process(value) if isinstance(value, list): From 2dbaaf1efda5625b4552dbd71036bd7f2584764c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 22:06:33 -0700 Subject: [PATCH 2049/2056] Bump aioesphomeapi from 45.0.1 to 45.0.2 (#16469) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae50c4046b5..7d497e28343 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.1 +aioesphomeapi==45.0.2 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From f301e90fd9eea419dd82166f17f4ad3cd6477eb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 15 May 2026 22:16:52 -0700 Subject: [PATCH 2050/2056] [ci] Use larger app partition for esp32-s3-idf component test grouping (#16430) --- .../build_components_base.esp32-s3-idf.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml index ee209000e93..f3122f977ed 100644 --- a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -7,6 +7,9 @@ esp32: variant: ESP32S3 framework: type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv logger: level: VERY_VERBOSE From 20f92ad5e96cb565f898b94c79b06f2b9a699536 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 18:41:09 -0700 Subject: [PATCH 2051/2056] Bump aioesphomeapi from 45.0.2 to 45.0.3 (#16479) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d497e28343..92e36297a65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 -aioesphomeapi==45.0.2 +aioesphomeapi==45.0.3 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 4f188bf9bb07a98ea06cdd915e962a290362a957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:08:19 -0700 Subject: [PATCH 2052/2056] Bump zeroconf from 0.148.0 to 0.149.3 (#16480) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92e36297a65..63a25c8e36b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 aioesphomeapi==45.0.3 -zeroconf==0.148.0 +zeroconf==0.149.3 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From df31c72e4e4908a040a4e87fc5e6ca315bd7c3ef Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 17 May 2026 15:29:38 -0400 Subject: [PATCH 2053/2056] [espidf] Switch direct framework downloader to esphome-libs/esp-idf tarballs (#16484) --- esphome/espidf/framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 7ff373aba83..32bcf4fb3b4 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -69,7 +69,7 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str( ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str( os.environ.get( "ESPHOME_IDF_FRAMEWORK_MIRRORS", - "https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip", + "https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz;https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz", ) ) From cdf74c180e16cc9297c476953d477e2605516921 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 May 2026 11:11:54 +1200 Subject: [PATCH 2054/2056] Bump version to 2026.5.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a29a78ea9c0..641a4918282 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.5.0b1 +PROJECT_NUMBER = 2026.5.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 1819502201d..d6d533a7021 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.5.0b1" +__version__ = "2026.5.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 9c696f5de1f86671a983ab322e72a5e06921c7d5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 May 2026 15:56:44 +1200 Subject: [PATCH 2055/2056] [ci] Move ha-addon and schema release triggers to version-notifier (#16490) --- .github/workflows/release.yml | 70 +---------------------------------- 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1086c858c3..9799f882dbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -212,74 +212,6 @@ jobs: docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) - deploy-ha-addon-repo: - if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' - runs-on: ubuntu-latest - needs: - - init - - deploy-manifest - steps: - - name: Generate a token - id: generate-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} - private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - owner: esphome - repositories: home-assistant-addon - permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token) - - - name: Trigger Workflow - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ steps.generate-token.outputs.token }} - script: | - let description = "ESPHome"; - if (context.eventName == "release") { - description = ${{ toJSON(github.event.release.body) }}; - } - github.rest.actions.createWorkflowDispatch({ - owner: "esphome", - repo: "home-assistant-addon", - workflow_id: "bump-version.yml", - ref: "main", - inputs: { - version: "${{ needs.init.outputs.tag }}", - content: description - } - }) - - deploy-esphome-schema: - if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' - runs-on: ubuntu-latest - needs: [init] - environment: ${{ needs.init.outputs.deploy_env }} - steps: - - name: Generate a token - id: generate-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} - private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - owner: esphome - repositories: esphome-schema - permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token) - - - name: Trigger Workflow - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ steps.generate-token.outputs.token }} - script: | - github.rest.actions.createWorkflowDispatch({ - owner: "esphome", - repo: "esphome-schema", - workflow_id: "generate-schemas.yml", - ref: "main", - inputs: { - version: "${{ needs.init.outputs.tag }}", - } - }) - version-notifier: if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' runs-on: ubuntu-latest @@ -302,7 +234,7 @@ jobs: with: github-token: ${{ steps.generate-token.outputs.token }} script: | - github.rest.actions.createWorkflowDispatch({ + await github.rest.actions.createWorkflowDispatch({ owner: "esphome", repo: "version-notifier", workflow_id: "notify.yml", From edb59476b1841b932fbb74754659f885dc58d31f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 23:29:03 -0700 Subject: [PATCH 2056/2056] Bump zeroconf from 0.149.3 to 0.149.7 (#16492) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6eef8ac643e..e3de4a134c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ esptool==5.2.0 click==8.3.3 esphome-dashboard==20260425.0 aioesphomeapi==45.0.3 -zeroconf==0.149.3 +zeroconf==0.149.7 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import